我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。
事件背景
某天凌晨被一个急促的电话叫醒,电话中传来了急促的声音:“老李在不在,现在 Kubernetes 中 Pod 相互调用全部中断了,全盘中断了,快点起来帮忙修复下。”,吓得我一激灵从床上起来,在企微中默默看完了事情的前因后果,随后登录上故障 Kubernetes 集群上,看到 Pod 中的日志都平凡出现无法建联,无法解析域名的错误,这个时候我才意识到这个问题的严重性。
通过监控大盘,我发现 Kubernetes 集群中的 CoreDNS 服务的 ReadinessProbe 端口 8181 端口异常,导致服务无法正常启动,进而导致 Pod 之间无法相互调用,业务全盘中断。
现象获取
我登录到 Kubernetes 集群中 NetworkDebug 容器中,对运行的 CoreDNS 的 8080,8181,9153 联通性测试,发现 8080 和 9153 端口工作均正常,只有 8181 端口工作异常。
CoreDNS 运行端口功能如下表:
# | 端口号 | 描述 |
---|---|---|
1 | 8080 | 用于查询 CoreDNS 的状态 |
2 | 8181 | 用于检测 CoreDNS 是否正常工作 |
3 | 9153 | 用于 Prometheus 监控 |
CoreDNS Pod 都在 0/1 Running 状态。
kubectl 直接 describe CoreDNS Pod,显示 ReadinessProbe 探针异常,导致 Pod 无法正常启动。
Readiness probe failed: Get "http://x.x.x.x:8181/ready": dial tcp x.x.x.x:8181: connect: connection refused
由于 ReadinessProbe 探针主要是确认 Pod 能否进入转发列表,也就是说 Pod 的 IP 和 Port 能否成为 Service 的 Endpoint 中的一个。
如果 ReadinessProbe 探针失败,那么 Pod 就不会被加入到 Service 的 Endpoint 中,这样就会导致 Service 无法正常工作。我也对 CoreDNS 的 Service 也做了检查,发现 CoreDNS 的 Service 中的 Endpoint 为空,这就是为什么 Pod 之间无法相互调用的原因。
确认了故障触发逻辑以后,想要解决问题就简单了,只要解决 CoreDNS 的 ReadinessProbe 探针异常,恢复 CoreDNS Pod 就行了。
原理分析
为什么小伙伴完成执行变更操作,重启 CoreDNS Pod 以后,ReadinessProbe 探针依然异常呢? 要能解答这个疑问,就必须要了解 CoreDNS 中 8181 这个端口是如何工作的,直接看源代码。
coredns/plugin/ready/setup.go
// 注册 setup 函数, 命名为 ready
func init() { plugin.Register("ready", setup) }
func setup(c *caddy.Controller) error {
addr, err := parse(c) // 解析 caddy 配置文件
if err != nil {
return plugin.Error("ready", err)
}
rd := &ready{Addr: addr} // 创建 ready 对象, Addr 是 8181 端口
uniqAddr.Set(addr, rd.onStartup)
c.OnStartup(func() error { uniqAddr.Set(addr, rd.onStartup); return nil }) // 启动一个 Http Server 相应 8181 端口的请求
... // 省略部分代码
return nil
}
// 解析 caddy 配置文件
func parse(c *caddy.Controller) (string, error) {
addr := ":8181"
i := 0
for c.Next() {
if i > 0 {
return "", plugin.ErrOnce
}
i++
args := c.RemainingArgs()
switch len(args) {
case 0:
case 1:
addr = args[0] // 这里可能就是应发现 8181 端口异常的原因,传入的参数是 IP:Port 的格式,但是 net.SplitHostPort 解析没有报错,导致 addr 变量的值是空的或者别的值
if _, _, e := net.SplitHostPort(addr); e != nil {
return "", e
}
default:
return "", c.ArgErr()
}
}
return addr, nil
}
coredns/plugin/ready/ready.go
func (rd *ready) onStartup() error {
ln, err := reuseport.Listen("tcp", rd.Addr)
if err != nil {
return err
}
rd.Lock()
rd.ln = ln
rd.mux = http.NewServeMux()
rd.done = true
rd.Unlock()
// 8181 端口健康检测的处理方法,主要相应 /ready 的请求
rd.mux.HandleFunc("/ready", func(w http.ResponseWriter, _ *http.Request) {
rd.Lock()
defer rd.Unlock()
if !rd.done {
w.WriteHeader(http.StatusServiceUnavailable)
io.WriteString(w, "Shutting down")
return
}
ok, todo := plugins.Ready() // 判断当前组件是否准备就绪
if ok { // 如果准备就绪,返回 200
w.WriteHeader(http.StatusOK)
io.WriteString(w, http.StatusText(http.StatusOK))
return
}
log.Infof("Still waiting on: %q", todo)
w.WriteHeader(http.StatusServiceUnavailable) // 如果没有准备就绪,返回 503
io.WriteString(w, todo)
})
go func() { http.Serve(rd.ln, rd.mux) }() // 启动一个 Http Server 相应 8181 端口的请求
return nil
}
实际我看完了整个 ready 组件的代码,我也百思不得其解,因为之前我们在 CoreDNS 的 Configmap 中从来没有启动或者配置过 ready 组件,那个时候的 CoreDNS 的 ReadinessProbe 探针是正常的,为什么现在就不正常了呢?
各种问题在我脑袋里面翻滚,但是没有一个能够解释的通的,最后我只能通过最快和最有把握的方式来解决问题。
如果要最快的方式解决:我想是不是在 parse 函数让 args[0] 传入一个 :8181 的格式,也许就不会有问题了。
当然真正的问题,我们团队还在排查中,如果有结果,我会在文章中更新。
处理方法
假设 CoreDNS ready 组件自动启动有问题,那我们可不可以手动的方式启动呢?通过官方文档中的描述,我们可以在 CoreDNS 的配置文件中添加一个 ready 插件,来解决这个问题。 相当通过手动设置 ready 插件的端口,来解决 ReadinessProbe 探针异常的问题。
coredns.io/plugins/rea…
具体配置如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
ready :8181 ## 增加此行
forward . /etc/resolv.conf {
max_concurrent 1000
}
log
cache 30
loop
reload
loadbalance
}
最后重启 CoreDNS Pod,问题解决。
当然后面我们通过 CoreDNS 的 Github 中 Issue 查找,发现了一个和我们类似的问题,也是 ReadinessProbe 探针异常(8181 端口),也是通过配置中添加 ready 插件解决的问题。
github.com/coredns/cor…
有兴趣的小伙伴可以自行移步查看。
最终效果
CoreDNS Pod 运行正常,ReadinessProbe 探针正常,业务恢复正常。