记一次ETCD OOM问题排查

本文记录了一次线上ETCD 集群频繁发生OOM问题的排查定位过程,现整理成文分享给大家。

故事开始于6月的一个晚上,那天晚上下班走到小区楼下,收到部门老大的信息说,某一个数据库业务依赖的ETCD 集群最近频繁发生OOM的问题,存在重大的安全隐患,需要排查,因为本人在某电商公司负责数据库开发,6月正好是6.18各个电商大搞促销的时间段,我们又是核心的数据库业务,服务整个公司的数据存储业务,因此问题变得很严重。

那个时候我还不太懂ETCD,没有阅读它的源码,但是大致的架构和设计我是知道的,因此就着手分析,整个分析花费了两天时间,大致可以分成四个阶段。以下是整个分析定位的过程。

第一阶段。因为我知道ETCD 3.0之后存储使用了boltDB。boltDB我是熟悉的,它的写事务是全局排他的,因为已经很久没有碰过boltDB的代码了,因此简单的推理(当天回家没有携带工作电脑),怀疑是不是读写抢占锁导致的,因为如果存在一个很大耗时很长的写事务,会把所有的读事务饿死。因此结合当前晚上的分析,初步认为是读写阻塞引起的,最大的可能是一个节点在做快照,因为一般之后这个才会涉及到很大的事务,因为etcd是go语言编写的,如果使用系统提供的锁,那么client端无法终止这个读请求,client不断的超时重试使内存占用不断增加(整个过程持续了三个小时左右)。这个结论看起来是合理的。

下图就是一个ETCD节点的监控截图

13632999-4262462cd9087ca2.png

第二阶段。为了验证昨天晚上的结论,我开始搜集ETCD的相关的设计资料,其中有一篇写的很不错,对我帮助很大,这里推荐给需要的同学。高可用分布式存储 etcd 的实现原理。好在ETCD的代码还是比较容易阅读的,比cockroach的代码好阅读多了,不过我个人认为cockroach的代码质量比ETCD要好一个量级,无论是架构设计还是代码风格。

言归正传,仔细阅读了ETCD 跟快照相关的代码以及读写代码,发现昨晚的推理不成立,ETCD并不是简单的使用boltDB做存储,这里不展开讲这部分,总之问题并不是因为读写锁导致的。

第三阶段。我开始分析日志(如果不懂ETCD的代码逻辑,看日志并不能获得更多的帮助),发现出现内存持续增长的期间,系统并没有什么异常,CPU,网络,磁盘都很正常,TCP的连接数也很平稳。这个时候陷入了僵局。不过转机也很快到来。负责维护ETCD集群的同事摘掉了集群中一个经常发生OOM的节点,据说赶巧这个节点存在一点儿磁盘故障,重新添加了一个节点,同事尝试摘掉了几个proxy(可以认为就是etcd client),起先这个重要的信息并不被我知道,然后神奇的事情发生了,集群没有发生OOM,以前每个大约三四个小时就会发生一次。事情似乎到此就结束了。不过请看下面的这个监控图


13632999-6d97753ffcd64095.png

虽然没有发生OOM,但是内存仍然在新加入的节点上增长了很多,这个时间ETCD在做什么?仔细分析这个节点对应时间段的日志,发现在出现内存断崖式下跌的时间点,日志中存在大量的网络链接断开日志。问题关键就在这里,结合其他的几个节点发生了内存增加但是幸好没有OOM的的日志,发现所有出现内存断崖式下跌的时间点都存在这种网络异常的日志。

于是我开始注意到所有出现OOM的时间点,不同的节点的OOM在时间线上存在连续排队的现象,见下图


13632999-488c986e3e6c4bd0.png

大家会发现当一个节点因为OOM crash后,马上另一个节点的内存就开始增长直到OOM,如此循环。经过分析初步可以断定是客户端的读导致的(实际业务中写很少),这个业务主要是存储数据库集群的拓扑信息,存在很多的watch操作,这也解释了为什么会在OOM的时候出现这种读压力转移。

第四阶段。既然是watch引起的,我就开始着手分析ETCD的watch机制,但是在“合理”的推演下,无法完美的解释这些监控数据,我们注意到在proxy与etcd server之间还存在一个HA proxy,它负责负载均衡。相关的同事告诉我确实发生过HA proxy关闭了客户端,但是没有关闭server端的问题,因此我们开始怀疑是否是HA proxy的问题,验证的方法也很简单,我们重启了HA proxy,发现没有奇迹发生,也就是说问题不在HA proxy。这个时候一个重要的监控数据引起了我们的注意,ETCD的监控统计中watcher的数量异常,watch的链接(客户端)只有几百个,但是watcher的数量确有几万个,这个完全不符合业务逻辑,因为业务根本没有watch这么多,我们越来月接近真相了。于是我们开始关注etcd client的代码和使用,之所以一开始没有怀疑这里是因为一方面这个proxy服务运行了很久一直没有问题,另一方面相关的代码确实很简单,仅仅几行,但是就是这几行代码却存在具体的坑。

一开始因为没有怀疑这几行简单的代码的问题,我被另一个问题吸引了,我开始研究为什么ETCD client的watch没有取消的方法,期间花费半天时间梳理etcd client的代码逻辑,确认不会是client自身的bug导致watch多了。同时我发现proxy使用etcd client的时候没有取消watch的机制。我们具体看一下代码:


13632999-9eb9077b3ea7dc48.png

我一开始专注于什么情况下会出现watcher关闭(watcher是一个channel),因为watcher关闭的时候会自动触发重新watch,也许这就是导致watcher很多的原因。但是反复阅读和验证ETCD client的代码,发现如果出现watcher被关闭,那么一定会取消订阅。于是我开始查看是否存在外部重复调用这个watch API的逻辑,于是真相出现了。


13632999-6e32166209d58097.png


13632999-dd8c0ad94e2df2af.png

从代码中我们可以看出当出现错误的时候,程序并没有取消之前的watch,而是直接重新开始watch。这就是导致watch数量越来越多的原因。找到了问题,那么解决的方法就很简单了,这里不赘述了。

总结

经历这次排查定位,首先熟悉了ETCD的设计和实现,将来遇到相关的问题可以快速分析和定位了。另一方面也看到了ETCD的一些需要注意的细节,整理如下:

a. watch太多会造成etcd server内存暴增,极端情况下会出现OOM。

 b. 每一个客户端创建的watch在不使用的时候都需要取消订阅(请务必这样做)。

具体操作可以参考:http://holys.im/2016/10/19/how-to-stop-etcd-watcher/

        etcd client允许同时watch多个key(可以相同或者不同的key),但是第一个watch的watcher拥有取消所有watcher订阅的权限,当第一个watcher取消订阅后,其他的watcher自动全部取消订阅。

        其他的watcher可以仅仅取消自己的订阅,这种取消仅仅是在etcd client层面取消,此时etcd server端仍然持有这个订阅,直到需要推送key新的变更时,客户端才会被触发真正向服务端申请取消这个订阅。

       如果不取消订阅watch,同时不消费watch队列中的数据,会造成内存占用增加,目前etcd client没有限制(TODO)。

    c. etcd server不适合存储大量的数据,其中一个原因是,它的raft快照处理比较粗暴,会在内存中缓存全量的数据,造成内存占用增加,尤其是在容器中使用,容易引起OOM。

在定位问题过程中千万不要忽略各种细节,真相很多时候就隐藏在其中,所谓灯下黑也许就是这样。另外越是简单的代码存在隐患也许更大(基本上只要出现就是大问题),越需要特别的关注。

还有一点就是平时多积累一些相关领域的知识,当有一天要用的时候不至于一脸懵比。

转载于:https://www.jianshu.com/p/05486908814d

猜你喜欢

转载自blog.csdn.net/weixin_33674976/article/details/91087349