谷歌工程师如何在 K8S 线上环境调试问题?

背景

Kubernetes(K8S)是目前市场占有率最高的开源 Docker 编排和运行平台,K8S 前身是谷歌内部的容器平台 Borg 衍生出来的开源 Docker 运行平台,具备企业级容器编排的能力,深受国内外 Docker 用户喜爱。


随着 K8S 的普及,如何在生产环境调试 K8S 的问题成为了大家普遍关注的问题。在 JFrog SwampUp 2017 用户大会上,谷歌的架构师 Ray Tsang 介绍了如何在 K8S 线上环境做调试,排查问题。

Docker 测试环境和生产环境不一致

技术背景


示例应用程序由多个微服务组成:Guestbook UI,Hello World Service, Guestbook Service, Redis 和 Mysql 服务,其中 Hello World Service 由 Java 语言开发,并且在 K8S 集群里运行了10个实例。



在软件发布流程里,环境的差异是最难解决的问题,Docker 的目标就是使用相同的容器镜像,解决不同环境的差异问题。在 K8S 里,可以通过不同的 Namespace 区分不同的环境。本示例里使用 Staging 作为测试环境的 Namespace。


在 Staging 环境启动服务,并查看服务的状态: kubectl get pods --namespace=staging

kubectl get svc --namespace=staging

从 Web 端访问服务器都正常,Staging 环境 OK!



如何找到出问题的镜像?


Staging 环境里部署和测试都没问题,这时可以将镜像部署到 K8S 的生产环境集群,得到以下画面:


为什么同样的镜像在 Staging 环境运行没问题,而部署到 K8S 的生产环境,却报错了?来调试下,首先打开 'helloworld-ui' 服务的 YAML 文件,看看部署失败的镜像版本号是多少:



可以看到出错的服务镜像版本是 latest。根据持续交付的概念,每次部署最新版本的镜像到测试环境,这是没问题的。(在生产环境用 latest 需要谨慎)


但问题来了,我们并不知道这个 'latest' 是哪个版本。不过通过镜像的 SHA256 码可以找到对应的镜像,Docker inspect 可以显示某个镜像的 SHA256 码。K8S 也提供了 'describe pod' 方法来显示镜像的信息,包括 SHA256 码。



有了这个特征码,我们可以在 Artifactory 里面进行 Checksum 搜索,快速查找镜像:



搜索的结果显示,这个 latest 镜像的特征码,对应的版本号是50:



看看 Jenkins 构建任务里的错误信息?


好的,问题镜像版本找到了,是不是这个镜像构建时出错了?去 Jenkins 的日志里面去找找。等一下,貌似我并不知道 Jenkins 任务 ID 和镜像 ID 直接的关系。还好,从 Artifactory 里面可以看到这个关系。点击50号镜像,构建信息里体现 Jenkins 任务的地址:



点击 CI Server 链接跳转到 Jenkins,发现50号构建任务并没有异常。


镜像没问题,看看服务器异常日志?


由于在 K8S 集群里已经运行了多个服务的实例,如何在同一个服务的多个实例里统一进行日志聚合查询?


使用 K8S 里可以方便的下载并运行 ELK 的 Helm Charts,做日志的聚合查询。谷歌内部提供了服务叫做 Big Query,也是谷歌搜索引擎用到的查询服务,查询 TB 级别的数据,能够做到秒级的响应时间。



通过拖拽式的查询条件输入,Big Query 可以帮助你找出 'helloworld-ui' 服务的所有实例里 Exception 的个数,从上图来看,某一个实例抛出了25个异常。


出现这种情况怎么处理?因为坏的实例已经影响到了用户服务,并且这个服务在 K8S 还有多个健康的实例在运行,所以很多公司会选择马上 Kill 这个实例,甚至有些公司会在每天夜晚 Kill 掉所有的实例,来避免内存泄漏的问题。


Ray 认为这并不可取,这样做当时可能避免了问题,但过几天这个问题会反复出现,所以需要分析日志,解决问题。


既不影响用户使用,又能够 Debug?


有错误日志可以查看是工程师最欣慰的事情,但这个实例已经影响用户使用了,我必须要停掉这个实例,这就意味着无法重现问题。有没有一个方法让我既能够在运行环境里调试,查看实时日志,重现问题,又不影响用户的使用?


K8S 里的 Service 和 Label 的概念可以解决这个难题。K8S 会为集群里 service (helloworkd-ui) 的所有实例默认创建一个负载均衡,并且自动维护所有实例的个数。使用 label pod serving=false 可以实现将某个实例移除负载均衡的效果。



从上图可以看到,helloworld-ui 服务有11个实例。这就是 K8S 的神奇之处,它不仅将有问题的 helloworld-ui 实例移除了负载均衡,工程师可以登录这个实例进行调试,同时 K8S 的 Replication Controller 会自动新起一个实例,保持实例运行的个数始终为10个,从而保证不影响用户的体验。


如何调试?


可以用熟悉的 docker exec -it instanceId bash 登录到实例里进行调试。当然 K8S 也提供了更强大的调试方式:使用端口转发(port-forward)的方式。



它能够将本地端口收到的请求转发到这个问题镜像的实例里进行远程调试,快速定位问题。


那么传统排查问题的方式当然是增加日志,把变量信息输出到日志里,然后修复问题,这是普通的调试方式,往往这种方式定位并修改一个 bug 需要花费一个小时或更多。


谷歌云为开发者提供了一个强大的调试工具 StackDriver Debugger,通过这个工具能够在不重新部署应用的情况下,增加日志输出(logpoint)。StackDriver Debugger 也是从谷歌内部孵化出的调试工具。



增加 logpoint 之后,再次访问这个服务,对应的变量内容会立刻显示在日志里,而无需重新部署应用。这样极大的提高了调试的效率。



继续调试


从上图的日志可以看出,请求里的 name 为空。从源代码去查看原因:



这个 name 是来自 Session 的属性,也就是说第一次用户注册的时候,使用了空的用户名,并且存入 Session,当再次读取的时候发生了异常,这下问题找到了,改代码,提交,测试,部署,收工!


还有更酷的 Snapshot 功能


远程 debug 的功能很酷,但它的问题在于它阻断了用户的返回,也叫做 Stop the world,从而无法为线上服务进行远程 debug。谷歌云的调试工具还支持 Snapshot 的概念,在谷歌云平台里,你可以为你的某行代码设置 snapshot 点,设置之后,这个点一旦被触发,调试工具会记录当时的所有变量信息,而不会阻断程序,进而让生产环境的实时调试变得可能。

总结

K8S 为用户提供的强大的 Docker 容器管理和运行的能力,并且支持了负载均衡,Label 分组服务实例,日志聚合检索,生产环境调试的工具等等,为容器的开发和调试提供了极大的便利。文中提到的谷歌的日志聚合检索功能 Big Query 以及谷歌的 Debug 工具 StackDriver Debugger 在谷歌云里都提供了服务,你只需注册一个谷歌云账号即可使用。


从谷歌的产品设计可以看出,谷歌是专注的为开发者提高生产效率,这也是谷歌自身工程师文化基因的体现。即便不是谷歌云的用户,也可以借鉴谷歌的思路,找到快速解决线上问题的办法,提高效率,降低服务不可用时间。


参考资料:

谷歌云调试工具:

https://cloud.google.com/debugger/

原视频地址

https://youtu.be/INJMA8gHnro?list=PLY0Zjn5rFo4MFIwbYtQx4wD1KK7HleIzk