Kubernetes 惊天地泣鬼神之大 Bug

2018 年 5 月 24 日 云头条

作者:纤夫张,文章来源:知乎

链接:https://zhuanlan.zhihu.com/p/37217575


自从三月份生产、非生产全面上线 Kubernetes 后,本以为部署的问题可以舒一口气了,但是断断续续在生产、非生产环境发现一个诡异的问题,这礼拜又调试了三天,在快要放弃的时候居然找到原因了,此缺陷影响目前(2018-5-23)为止所有 Kubernetes 版本(见后面更新,夸大了),包括 Github 当前 master 分支,后果是某种情况触发下,Kubernetes service 不能提供服务,影响非常恶劣。


问题的现象是,在某种情况下,一个或者多个 Kubernetes service 对应的 Kubernetes endpoints 消失几分钟至几十分钟,然后重新出现,然后又消失,用 "kubectl get ep --all-namespaces" 不断查询,可以观察到一些 AGE 在分钟级别的 endpoints 要么忽然消失,要么 AGE 突然变小从一分钟左右起步。Endpoints 的不稳定,必然导致对应的 Kubernetes service 无法稳定提供服务。有人在 Github 上报告了 issue,但是一直没得到开发人员注意和解决。


首先解释下 Kubernetes 大体上的实现机制,有助于理解下面的破案过程。


Kubernetes 的实现原理跟配置管理工具 CFengine、Ansible、Salt 等非常类似:configuration convergence——不断的比较期望的配置和实际的配置,修订实际配置以收敛到期望配置。


Kubernetes 的关键系统服务有 api-server, scheduler, controller-manager 三个。api-server 一方面作为 Kubernetes 系统的访问入口点,一方面作为背后 etcd 存储的代理服务器,Kubernetes 里的所有资源对象,包括 Service、Deployment、ReplicaSet、DaemonSet,Pod、Endpoint、ConfigMap、Secret 等等,都是通过 api-server 检查格式后,以 protobuf 格式序列化并存入 etcd。这就是一个写入“期望配置”的过程。


Controller-manager 服务里包含了很多 controller 实例,对应各种资源类型:



v1.12.0-alpha.0/cmd/kube-controller-manager/app/controllermanager.go#L317

这些 controller 做的事情就是收敛:它通过 api-server 的 watch API 去间接的 watch etcd,以收取 etcd 里数据的 changelog,对改变(包括ADD,DELETE,UPDATE)的资源(也就是期望的“配置“),逐个与通过 kubelet + docker 收集到的信息(实际“配置”)做对比并修正差异。


比如 etcd 里增加了一个 Pod 资源,那么 controller 就要请求 scheduler 调度,然后请求 kubelet 创建 Pod,如果etcd里删除了一个 Service 资源,那么就要删除其对应的 endpoint 资源,而这个 endpoint 删除操作会被 kube-proxy 监听到而触发实际的 iptables 命令。


注意上面 controller 通过 watch 获取 changelog 只是一个实现上的优化,它没有周期性的拿出所有的资源对象然后逐个判断,在集群规模很大时,这样的全量收敛是很慢的,后果就是集群的调度、自我修复进行的非常慢。


有了大框架的理解后,endpoints 被误删的地方是很容易找到的:



v1.12.0-alpha.0/pkg/controller/endpoint/endpoints_controller.go#L396


然后问题来了,什么情况下那个 Services(namespace).Get(name) 会返回错误呢?通过注释,可以看到在 service 删除时会走到 397 行里去,把无用的 endpoints 删掉,因为 endpoint 是给 service 服务的,service 不存在时,endpoint 没有存在的意义。


然后问题来了,通过 "kubectl get svc" 可以看到出问题期间,服务资源一直都在,并没有重建,也没有刚刚部署,甚至很多服务资源都是创建了几个月,为啥它对应的 endpoints 会被误删然后重建呢?这个问题困扰了我两个月,花了很长时间和很多脑细胞,今天在快放弃时突然有了重大发现。


由于我司搭建的 Kubernetes 集群开启了 X509 认证以及 RBAC 权限控制,为了便于审查,我开启了 kube-apiserver 的审计日志,在出问题时,审计日志中有个特别的模式:



用 jq 命令摘取的审计日志片段


审计日志中,在每个 endpoint delete & create 发生前,都有一个 "/api/v1/services...watch=true" 的 watch 调用,上面讲到,controller-manager 要不断的去爬 etcd 里面资源的 changelog,这里很奇怪的问题是,这个调用的 "resourceVersion=xxx" 的版本值始终不变,难道不应该不断从 changelog 末尾继续爬取因为 resourceVersion 不断递增么?


通过一番艰苦卓绝的连猜带蒙,终于找到“爬取changelog”对应的代码



v1.12.0-alpha.0/staging/src/k8s.io/client-go/tools/cache/reflector.go#L394


上面的代码对 resourceVersion 的处理逻辑是: 遍历 event 列表,取当前 event 的 "resourceVersion" 字段作为新的要爬取的“起始resourceVersion",所以很显然遍历结束后,"起始resourceVersion" 也就是最后一条 event 的 "resourceVersion"。


好了,我们来看看 event 列表涨啥样,把上面 api-server 的请求照搬就可以看到了:

kubectl proxy curl -s localhost:8001/api/v1/services?resourceVersion=xxxx&timeoutSeconds=yyy&watch=true



/api/v1/services 的输出,仅示意,注意跟上面的审计日志不是同一个时间段的


可以很明显看到,坑爹,resourceVersion 不是递增的!


这个非递增的问题其实很容易想明白,resourceVersion 并不是 event 的序列号,它是 Kubernetes 资源自身的版本号,如果把 etcd 看作 Subversion,两者都有一个全局递增的版本号,把 Kubernetes 资源看作 Subversion 里保存的文件。 在 svn revision=100 时存入一个文件 A,那么A的版本是 100,然后不断提交其它文件的修改到 svn,然后在某个版本删掉 A,此时用 svn log 看到的相关文件的“自身最后一次修改版本”并不是递增的,最后一条的“自身最后一次修改版本”是 100.


所以真相大白了,reflector.go 那段遍历 etcd "changelog" 的代码,误以为 event 序列的 resourceVersion 是递增的了,虽然绝大部分时候是这样的。由于这段代码是通用的,所有资源都受这个影响,所以是非常严重的 bug,之前看到很多人报告(我自己也遇到)controller-manager 报错 "reflector.go:341] k8s.io/kubernetes/pkg/controller/bootstrap/bootstrapsigner.go:152: watch of *v1.ConfigMap ended with: too old resource version: " 很可能也是这个导致的,因为它失误从很老的 resourceVersion 开始爬取 etcd "changelog",但 etcd 已经把太老的 changelog 给 “compact” 掉了。


最后总结下怎么触发以及绕过这个 bug:


  • 触发:陆续创建、删除、创建 Kubernetes service 对象,然后"kubectl delete svc xxx"删掉创建时间靠前的 service,也就是往 service event list 末尾插入了一条 resourceVersion 比较小的记录,这将使得 controller-manager 去从已经爬过的 service event list 位置重新爬取重放,然后就重放了 service 的 ADDED、DELETED event,于是 controller-manager 内存里缓存的 service 对象被删除,导致 EndpointController 删除了“不存在的”service 对应的 endpoints。

  • 绕过

    • 用 docker restart 重启 kube-controller-manager 容器,强迫其从 event list 最新位置开始爬取

    • 创建一个 service,其 resourceVersion 会是 etcd 的最新全局版本号,这个 ADDED event 会出现在 event list 末尾,从而 controller-manager 从这个最新的 resourceVersion 开始爬取

    • "kubectl edit" 修改某个 service,加点无意义的 label 之类的,保存,这样其 resourceVersion 也会更新,之后跟上一个 workaround 原理一样了。

  • 错误的绕过方式

    • "kubectl delete pod/xxx":不影响 service event list

    • "kubectl delete deploy/xxxx": 不影响 service event list


Kubernetes 里凡是创建后基本不变的资源,比如 service、configmap、secrets 都会受这个影响,它们的版本号都很久,删一个后都会触发这个 bug,比如 configmap,可能就会被重复的更新,映射到容器里的 config file 也就不断更新,对于不支持 config hot reload 的服务没有影响,对于支持 config hot reload 的服务,会频繁的重新加载配置文件重新初始化,可能导致意想不到的问题。


Bug 的修复很简单,在遍历 event list 选择 resourceVersion 时,总是取 max(event.resourceVersion, currentResourceVersion) 即可,我提了个 pull request 给 Kubernetes 官方看看,希望我这个粗暴的修正不会带来新的问题😓


【更新】:avoid wrongly replaying old events in kube-controller-manager by Dieken · Pull Request #64209 · kubernetes/kubernetes  官方开发同学很快指出这个问题已经修正了,就在 1.9.3,我正好用的 1.9.2,看了下 1.9.3 的更新说明,他妈的轻描淡写啊!


Fixes an issue where the resourceVersion of an object in a DELETE watch

event was not the resourceVersion of the delete itself, but of the last

update to the object. This could disrupt the ability of clients clients

to re-establish watches properly. (#58547, @liggitt)

按 liggitt 的说法,升级到这些版本就好了:v1.8.8+, v1.9.3+, and v1.10.0+


登录查看更多
0

相关内容

Kubernetes 是一个自动化部署,扩展,以及容器化管理应用程序的开源系统。
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【课程】伯克利2019全栈深度学习课程(附下载)
专知会员服务
56+阅读 · 2019年10月29日
MIT新书《强化学习与最优控制》
专知会员服务
275+阅读 · 2019年10月9日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
工行基于MySQL构建分布式架构的转型之路
炼数成金订阅号
15+阅读 · 2019年5月16日
专访阿里亚顿:Serverless与BFF与前端
前端之巅
45+阅读 · 2019年5月8日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
我的if else代码纯净无暇,一个字也不能简化
机器之心
3+阅读 · 2018年12月28日
浅显易懂的分布式TensorFlow入门教程
专知
7+阅读 · 2018年6月22日
Arxiv
6+阅读 · 2020年4月14日
Generalization and Regularization in DQN
Arxiv
6+阅读 · 2019年1月30日
VIP会员
相关VIP内容
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【课程】伯克利2019全栈深度学习课程(附下载)
专知会员服务
56+阅读 · 2019年10月29日
MIT新书《强化学习与最优控制》
专知会员服务
275+阅读 · 2019年10月9日
相关资讯
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
工行基于MySQL构建分布式架构的转型之路
炼数成金订阅号
15+阅读 · 2019年5月16日
专访阿里亚顿:Serverless与BFF与前端
前端之巅
45+阅读 · 2019年5月8日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
我的if else代码纯净无暇,一个字也不能简化
机器之心
3+阅读 · 2018年12月28日
浅显易懂的分布式TensorFlow入门教程
专知
7+阅读 · 2018年6月22日
Top
微信扫码咨询专知VIP会员