1. 问题背景
某外围 JAVA 长连贯服务应用 MongoDB 作为次要存储,客户端数百台机器连贯同一 MongoDB 集群,短期内呈现屡次性能抖动问题,此外,还呈现一次“雪崩”故障,同时流量霎时跌零,无奈主动复原。本文剖析这两次故障的根本原因,包含客户端配置应用不合理、MongoDB 内核链接认证不合理、代理配置不全等一系列问题,最终通过多方致力确定问题本源。
该集群有十来个业务接口拜访,每个接口部署在数十台业务服务器下面,拜访该 MongoDB 机器的客户端总数超过数百台,局部申请一次拉取数十行甚至百余行数据。
该集群为 2 机房同城多活集群(选举节不耗费太多资源,异地的第三机房来部署选举节点),架构图如下:
从上图能够看出,为了实现多活,在每个机房都部署有对应代理,对应机房客户端链接对应机房的 mongos 代理,每个机房多个代理。代理层部署 IP:PORT 地址列表 (留神:不是实在 IP 地址) 如下:
A 机房代理地址列表:1.1.1.1:111,2.2.2.2:1111,3.3.3.3:1111
B 机房代理地址列表:4.4.4.4:1111,4.4.4.4:2222
A 机房三个代理部署在三台不同物理机,B 机房 2 个代理部署在同一台物理机。此外,A 机房和 B 机房为同城机房,跨机房拜访时延能够疏忽。
集群存储层和 config server 都采纳同样的架构:A 机房 (1 主节点 + 1 从节点) + B 机房(2 从节点)+ C 机房(1 个选举节点 arbiter),即 2(数据节点)+2(数据节点)+1(选举节点) 模式。
该机房多活架构能够保障任一机房挂了,对另一机房的业务无影响,具体机房多活原理如下:
- 如果 A 机房挂掉,因为代理是无状态节点,A 机房挂掉不会影响 B 机房的代理。
- 如果 A 机房挂掉,同时主节点在 A 机房,这时候 B 机房的 2 个数据节点和 C 机房的选举节点一共三个节点,能够保障新选举须要大于一半以上节点这个条件,于是 B 机房的数据节点会在短时间内选举出一个新的主节点,这样整个存储层拜访不受任何影响。
本文重点剖析如下 6 个疑难点:
- 为什么突发流量业务会抖动?
- 为什么数据节点没有任何慢日志,然而代理负载缺 100%?
- 为何 mongos 代理引起数小时的“雪崩”,并且长时间不可复原?
- 为何一个机房代理抖动,对应机房业务切到另一个机房后,还是抖动?
- 为何异样时候抓包剖析,客户端频繁建链断链,并且同一个链接建链到断链距离很短?
- 实践上代理就是七层转发,耗费资源更少,相比 mongod 存储应该更快,为何 mongod 存储节点无任何抖动,mongos 代理缺有抖动?
2. 故障过程
2.1 业务偶然流量顶峰,业务抖动?
该集群一段时间内有屡次短暂的抖动,当 A 机房客户端抖动后,发现 A 机房对应代理负载很高,于是切换 A 机房拜访 B 机房代理,然而切换后 B 机房代理同样抖动,也就是多活切换没有作用,具体过程剖析如下。
2.1.1 存储节点慢日志剖析
首先,剖析该集群所有 mongod 存储节点零碎 CPU、MEM、IO、load 等监控信息,发现一切正常,于是剖析每个 mongod 节点慢日志(因为该集群对时延敏感,因而慢日志调整为 30ms),剖析后果如下:
从上图能够看出,存储节点在业务抖动的时候没有任何慢日志,因而能够判断存储节点一切正常,业务抖动和 mongod 存储节点无关。
2.1.2 mongos 代理剖析
存储节点没有任何问题,因而开始排查 mongos 代理节点。因为历史起因,该集群部署在其余平台,该平台对 QPS、时延等监控不是很全,造成晚期抖动的时候监控没有及时发现。抖动后,迁徙该平台集群到 oppo 自研的新管控平台,新平台有具体的监控信息,迁徙后 QPS 监控曲线如下:
每个流量徒增工夫点,对应业务监控都有一波超时或者抖动,如下:
剖析对应代理 mongos 日志,发现如下景象:抖动工夫点 mongos.log 日志有大量的建链接和断链接的过程,如下图所示:
从上图能够看出,一秒钟内有几千个链接建设,同时有几千个链接断开,此外抓包发现很多链接短期内即断开链接,景象如下(断链工夫 - 建链工夫 =51ms, 局部 100 多 ms 断开):
对应抓包如下:
此外,该机器代理上客户端链接低峰期都很高,甚至超过失常的 QPS 值,QPS 大概 7000-8000,然而 conn 链接缺高达 13000,mongostat 获取到监控信息如下:
2.1.3 代理机器负载剖析
每次突发流量的时候,代理负载很高,通过部署脚本定期采样,抖动工夫点对应监控图如下图所示:
从上图能够看出,每次流量顶峰的时候 CPU 负载都十分的高,而且是 sy% 负载,us% 负载很低,同时 Load 甚至高达好几百,偶然甚至过千。
2.1.4 抖动剖析总结
从下面的剖析能够看出,某些工夫点业务有突发流量引起零碎负载很高。根因真的是因为突发流量吗?其实不然,请看后续剖析,这其实是一个谬误论断。没过几天,同一个集群雪崩了。
于是业务梳理突发流量对应接口,梳理进去后下掉了该接口,QPS 监控曲线如下:
为了缩小业务抖动,因而下掉了突发流量接口,尔后几个小时业务不再抖动。当下掉突发流量接口后,咱们还做了如下几件事件:
- 因为没找到 mongos 负载 100% 真正起因,于是每个机房扩容 mongs 代理,放弃每个机房 4 个代理,同时保障所有代理在不同服务器,通过分流来尽量减少代理负载。
- 告诉 A 机房和 B 机房的业务配置上所有的 8 个代理,不再是每个机房只配置对应机房的代理(因为第一次业务抖动后,咱们剖析 MongoDB 的 java sdk,确定 sdk 平衡策略会主动剔除申请时延高的代理,下次如果某个代理再出问题,也会被主动剔除)。
- 告诉业务把所有客户端超时工夫进步到 500ms。
然而,心里始终有很多纳闷和悬念,次要在以下几个点:
- 存储节点 4 个,代理节点 5 个,存储节点无任何抖动,反而七层转发的代理负载高?
- 为何抓包发现很多新连贯几十 ms 或者一百多 ms 后就断开连接了?频繁建链断链?
- 为何代理 QPS 只有几万,这时代理 CPU 耗费就十分高,而且全是 sy% 零碎负载?以我多年中间件代理研发教训,代理耗费的资源很少才对,而且 CPU 只会耗费 us%,而不是 sy% 耗费。
2.2 同一个业务几天后“雪崩”了
好景不长,业务下掉突发流量的接口没过几天,更重大的故障呈现了,机房 B 的业务流量在某一时刻间接跌 0 了,不是简略的抖动问题,而是业务间接流量跌 0,零碎 sy% 负载 100%,业务简直 100% 超时重连。
2.2.1 机器系统监控剖析
机器 CPU 和零碎负载监控如下:
从上图能够看出,简直和后面的突发流量引起的零碎负载过高景象统一,业务 CPU sy% 负载 100%,load 很高。登陆机器获取 top 信息,景象和监控统一。
同一时刻对应网络监控如下:
磁盘 IO 监控如下:
从下面的系统监控剖析能够看出,出问题的时间段,零碎 CPU sy%、load 负载都很高,网络读写流量简直跌 0,磁盘 IO 一切正常,能够看出整个过程简直和之前突发流量引起的抖动问题完全一致。
2.2.2 业务如何复原
第一次突发流量引起的抖动问题后,咱们扩容所有的代理到 8 个,同时告诉业务把所有业务接口配置上所有代理。因为业务接口泛滥,最终 B 机房的业务没有配置全副代理,只配置了原先的两个处于同一台物理机的代理(4.4.4.4:1111,4.4.4.4:2222),最终触发 MongoDB 的一个性能瓶颈(详见前面剖析),引起了整个 MongoDB 集群”雪崩”
最终,业务通过重启服务,同时把 B 机房的 8 个代理同时配置上,问题得以解决。
2.2.3 mongos 代理实例监控剖析
剖析该时间段代理日志,能够看出和 2.1 同样得景象,大量的新键连贯,同时新连贯在几十 ms、一百多 ms 后又敞开连贯。整个景象和之前剖析统一,这里不在统计分析对应日志。
此外,剖析过后的代理 QPS 监控,失常 query 读申请的 QPS 拜访曲线如下,故障时间段 QPS 简直跌零雪崩了:
Command 统计监控曲线如下:
从下面的统计能够看出,当该代理节点的流量故障工夫点有一波尖刺,同时该工夫点的 command 统计霎时飙涨到 22000(理论可能更高,因为咱们监控采样周期 30s, 这里只是平均值),也就是霎时有 2.2 万个连贯霎时进来了。Command 统计实际上是 db.ismaster()统计,客户端 connect 服务端胜利后的第一个报文就是 ismaster 报文,服务端执行 db.ismaster()后应答客户端,客户端收到后开始正式的 sasl 认证流程。
失常客户端拜访流程如下:
- 客户端发动与 mongos 的链接
- Mongos 服务端 accept 接管链接后,链接建设胜利
- 客户端发送 db.isMaster()命令给服务端
- 服务端应答 isMaster 给客户端
- 客户端发动与 mongos 代理的 sasl 认证(屡次和 mongos 交互)
- 客户端发动失常的 find()流程
客户端 SDK 链接建设胜利后发送 db.isMaster()给服务端的目标是为了负载平衡策略和判断节点是什么类型,保障客户端疾速感知到拜访时延高的代理,从而疾速剔除往返时延高的节点,同时确定拜访的节点类型。
此外,通过提前部署的脚本, 该脚本在零碎负载高的时候主动抓包,从抓包剖析后果如下图所示:
上图时序剖析如下:
- 11:21:59.506174 链接建设胜利
- 11:21:59.506254 客户端发送 db.IsMaster()到服务端
- 11:21:59.656479 客户端发送 FIN 断链申请
- 11:21:59.674717 服务端发送 db.IsMaster()应答给客户端
- 11:21:59.675480 客户端间接 RST
第 3 和第 1 个报文之间相差大概 150ms,最初和业务确定该客户端 IP 对应的超时工夫配置,确定就是 150ms。此外,其余抓包中有相似 40ms、100ms 等超时配置,通过对应客户端和业务确认,确定对应客户端业务接口超时工夫配置的就是 40ms、100ms 等。因而,联合抓包和客户端配置,能够确定当代理超过指定超时工夫还没有给客户端 db.isMaster()返回值,则客户端立马超时,超时后立马发动重连申请。
总结: 通过抓包和 mongos 日志剖析,能够确定链接建设后疾速断开的起因是:客户端拜访代理的第一个申请 db.isMaster()超时了,因而引起客户端重连。重连后又开始获取 db.isMaster()申请,因为负载 CPU 100%, 很高,每次重连后的申请都会超时。其中配置超时工夫为 500ms 的客户端,因为 db.isMaster()不会超时,因而后续会走 sasl 认证流程。
因而能够看出,零碎负载高和重复的建链断链无关,某一时刻客户端大量建设链接 (2.2W) 引起负载高,又因为客户端超时工夫配置不一,超时工夫配置得比拟大得客户端最终会进入 sasl 流程,从内核态获取随机数,引起 sy% 负载高,sy% 负载高又引起客户端超时,这样整个拜访过程就成为一个“死循环”,最终引起 mongos 代理雪崩。
2.3 线下模仿故障
到这里,咱们曾经大略确定了问题起因,然而为什么故障突发工夫点那一瞬间 2 万个申请就会引起 sy% 负载 100% 呢,实践上一秒钟几万个链接不会引起如此重大的问题,毕竟咱们机器有 40 个 CPU。因而,剖析重复建链断链为何引起零碎 sy% 负载 100% 就成为了本故障的关键点。
2.3.1 模仿故障过程
模仿频繁建链断链故障步骤如下:
- 批改 mongos 内核代码,所有申请全副延时 600ms
- 同一台机器起两个同样的 mongos,通过端口辨别
- 客户端启用 6000 个并发链接,超时工夫 500ms
通过下面的操作,能够保障所有申请超时,超时后客户端又会立马开始从新建链,再次建链后拜访 MongoDB 还会超时,这样就模仿了重复建链断链的过程。此外,为了保障和雪崩故障环境统一,把 2 个 mongos 代理部署在同一台物理机。
2.3.2 故障模拟测试后果
为了保障和故障的 mongos 代理硬件环境统一,因而抉择故障同样类型的服务器,并且操作系统版本一样(2.6.32-642.el6.x86_64),程序都跑起来后,问题立马浮现:
因为出故障的服务器操作系统版本 linux-2.6 过低,因而狐疑可能和操作系统版本有问题,因而降级同一类型的一台物理机到 linux-3.10 版本,测试后果如下:
从上图能够看出,客户端 6000 并发重复重连,服务端压力失常,所有 CPU 耗费在 us%,sy% 耗费很低。用户态 CPU 耗费 3 个 CPU,内核态 CPU 耗费简直为 0,这是咱们期待的失常后果,因而感觉该问题可能和操作系统版本有问题。
为了验证更高并重复建链断链在 Linux-3.10 内核版本是否有 2.6 版本同样的 sy% 内核态 CPU 耗费高的问题,因而把并发从 6000 晋升到 30000,验证后果如下:
测试后果: 通过批改 MongoDB 内核版本成心让客户端超时重复建链断链,在 linux-2.6 版本中,1500 以上的并发重复建链断链零碎 CPU sy% 100% 的问题即可浮现。然而,在 Linux-3.10 版本中,并发到 10000 后,sy% 负载逐渐减少,并发越高 sy% 负载越高。
总结: linux-2.6 零碎中,MongoDB 只有每秒有几千的重复建链断链,零碎 sy% 负载就会靠近 100%。Linux-3.10,并发 20000 重复建链断链的时候,sy% 负载能够达到 30%,随着客户端并发减少,sy% 负载也相应的减少。Linux-3.10 版本相比 2.6 版本针对重复建链断链的场景有很大的性能改善,然而不能解决基本问题。
2.4 客户端重复建链断链引起 sy% 100% 根因
为了剖析 %sy 零碎负载高的起因,装置 perf 获取零碎 top 信息,发现所有 CPU 耗费在如下接口:
从 perf 剖析能够看出,cpu 耗费在_spin_lock_irqsave 函数,持续剖析内核态调用栈,失去如下堆栈信息:
– 89.81% 89.81% [kernel] [k] _spin_lock_irqsave ▒
– _spin_lock_irqsave ▒
– mix_pool_bytes_extract ▒
– extract_buf ▒
extract_entropy_user ▒
urandom_read ▒
vfs_read ▒
sys_read ▒
system_call_fastpath ▒
0xe82d
下面的堆栈信息阐明,MongoDB 在读取 /dev/urandom,并且因为多个线程同时读取该文件,导致耗费在一把 spinlock 上。
到这里问题进一步清朗了,故障 root case 不是每秒几万的连接数导致 sys 过高引起。根本原因是每个 mongo 客户端的新链接会导致 MongoDB 后端新建一个线程,该线程在某种状况下会调用 urandom_read 去读取随机数 /dev/urandom,并且因为多个线程同时读取,导致内核态耗费在一把 spinlock 锁上,呈现 cpu 高的景象。
2.5 MongoDB 内核随机数优化
2.5.1 MongoDB 内核源码定位剖析
下面的剖析曾经确定,问题本源是 MongoDB 内核多个线程读取 /dev/urandom 随机数引起,走读 MongoDB 内核代码,发现读取该文件的中央如下:
下面是生成随机数的外围代码,每次获取随机数都会读取”/dev/urandom”系统文件,所以只有找到应用该接口的中央即可即可剖析出问题。
持续走读代码,发现次要在如下中央:
// 服务端收到客户端 sasl 认证的第一个报文后的解决,这里会生成随机数
// 如果是 mongos,这里就是接管客户端 sasl 认证的第一个报文的解决流程
Sasl_scramsha1_server_conversation::_firstStep(...) {
... ...
unique_ptr<SecureRandom> sr(SecureRandom::create());
binaryNonce[0] = sr->nextInt64();
binaryNonce[1] = sr->nextInt64();
binaryNonce[2] = sr->nextInt64();
... ...
}
//mongos 相比 mongod 存储节点就是客户端,mongos 作为客户端也须要生成随机数
SaslSCRAMSHA1ClientConversation::_firstStep(...) {
... ...
unique_ptr<SecureRandom> sr(SecureRandom::create());
binaryNonce[0] = sr->nextInt64();
binaryNonce[1] = sr->nextInt64();
binaryNonce[2] = sr->nextInt64();
... ...
}
2.5.2 MongoDB 内核源码随机数优化
从 2.5.1 剖析能够看出,mongos 解决客户端新连贯 sasl 认证过程都会通过 ”/dev/urandom” 生成随机数,从而引起零碎 sy% CPU 过高,咱们如何优化随机数算法就是解决本问题的要害。
持续剖析 MongoDB 内核源码,发现应用随机数的中央很多,其中有局部随机数通过用户态算法生成,因而咱们能够采纳同样办法,在用户态生成随机数,用户态随机数生成外围算法如下:
class PseudoRandom {
... ...
uint32_t _x;
uint32_t _y;
uint32_t _z;
uint32_t _w;
}
该算法能够保障产生的数据随机散布,该算法原理详见:
http://en.wikipedia.org/wiki/…
也能够查看如下 git 地址获取算法实现:MongoDB 随机数生成算法正文
总结: 通过优化 sasl 认证的随机数生成算法为用户态算法后,CPU sy% 100% 的问题得以解决,同时代理性能在短链接场景下有了数倍 / 数十倍的性能晋升。
3. 问题总结及疑难解答
从下面的剖析能够看出,该故障由多种因素连环触发引起,包含客户端配置使用不当、MongoDB 服务端内核极其情况异常缺点、监控不全等。总结如下:
- 客户端配置不对立,同一个集群多个业务接口配置千奇百怪,超时配置、链接配置各不相同,减少了抓包排查故障的难度,超时工夫设置太小容易引起重复重连。
- 客户端须要配全所有 mongos 代理,这样当一个代理故障的时候,客户端 SDK 默认会剔除该故障代理节点,从而能够保障业务影响最小,就不会存在单点问题。
- 同一集群多个业务接口应该应用同一配置核心对立配置,防止配置不对立。
- MongoDB 内核的新连贯随机算法存在重大缺点,在极其状况下引起重大性能抖动,甚至业务“雪崩”。
剖析到这里,咱们能够答复第 1 章节的 6 个疑难点了,如下:
为什么突发流量业务会抖动?
答:因为业务是 java 业务,采纳链接池形式链接 mongos 代理,当有突发流量的时候,链接池会减少链接数来晋升拜访 MongoDB 的性能,这时候客户端就会新增链接,因为客户端泛滥,造成可能霎时会有大量新连贯和 mongos 建链。链接建设胜利后开始做 sasl 认证,因为认证的第一步须要生成随机数,就须要拜访操作系统 ”/dev/urandom” 文件。又因为 mongos 代理模型是默认一个链接一个线程,所以会造成霎时多个线程拜访该文件,进而引起内核态 sy% 负载过高。
为何 mongos 代理引起“雪崩”,流量为何跌零不可用?
答:起因客户端某一时刻可能因为流量忽然有减少,链接池中链接数不够用,于是减少和 mongos 代理的链接,因为是老集群,代理还是默认的一个链接一个线程模型,这样霎时就会有大量链接,每个链接建设胜利后,就开始 sasl 认证,认证的第一步服务端须要产生随机数,mongos 服务端通过读取 ”/dev/urandom” 获取随机数,因为多个线程同时读取该文件触发内核态 spinlock 锁 CPU sy% 100% 问题。因为 sy% 零碎负载过高,因为客户端超时工夫设置过小,进一步引起客户端拜访超时,超时后重连,重连后又进入 sasl 认证,又加剧了读取 ”/dev/urandom” 文件,如此重复循环继续。
此外,第一次业务抖动后,服务端扩容了 8 个 mongos 代理,然而客户端没有批改,造成 B 机房业务配置的 2 个代理在同一台服务器,无奈利用 mongo java sdk 的主动剔除负载高节点这一策略,所以最终造成”雪崩”。
为什么数据节点没有任何慢日志,然而代理负载却 CPU sy% 100%?
答:因为客户端 java 程序间接拜访的是 mongos 代理,所以大量链接只产生在客户端和 mongos 之间,同时因为客户端超时工夫设置太短(有接口设置位几十 ms,有的接口设置位一百多 ms,有的接口设置位 500ms),就造成在流量峰值的时候引起连锁反应(突发流量零碎负载高引起客户端疾速超时,超时后疾速重连,进一步引起超时,有限死循环)。Mongos 和 mongod 之间也是链接池模型,然而 mongos 作为客户端拜访 mongod 存储节点的超时很长,默认都是秒级别,所以不会引起重复超时建链断链。
为何 A 机房代理抖动的时候,A 机房业务切到 B 机房后,还是抖动?
答:当 A 机房业务抖动,业务切换到 B 机房的时候,客户端须要从新和服务端建设链接认证,又会触发大量重复建链断链和读取随机数 ”/dev/urandom” 的流程,所以最终造成机房多活失败。
为何异样时候抓包剖析,客户端频繁建链断链,并且同一个链接建链到断链距离很短?
答:频繁建链断链的根本原因是零碎 sy% 负载高,客户端极短时间内建设链接后又端口的起因是客户端配置超时工夫太短。
实践上代理就是七层转发,耗费资源更少,相比 mongod 存储应该更快,为何 mongod 存储节点无任何抖动,mongos 代理却有重大抖动?
答:因为采纳分片架构,所有 mongod 存储节点后面都有一层 mongos 代理,mongos 代理作为 mongod 存储节点的客户端,超时工夫默认秒级,不会呈现超时景象,也就不会呈现频繁的建链断链过程。
如果 MongoDB 集群采纳一般复制集模式,客户端频繁建链断链是否可能引起 mongod 存储节点同样的“雪崩”?
答:会。如果客户端过多,操作系统内核版本过低,同时超时工夫配置过段,间接拜访复制集的 mongod 存储节点,因为客户端和存储节点的认证过程和与 mongos 代理的认证过程一样,所以还是会触发引起频繁读取 ”/dev/urandom” 文件,引起 CPU sy% 负载过高,极其状况下引起雪崩。
4.“雪崩”解决办法
从下面的一系列剖析,问题在于客户端配置不合理,加上 MongoDB 内核认证过程读取随机数在极其状况下存在缺点,最终造成雪崩。如果没有 MongoDB 内核研发能力,能够通过规范化客户端配置来防止该问题。当然,如果客户端配置规范化,同时 MongoDB 内核层面解决极其状况下的随机数读取问题,这样问题能够失去彻底解决。
4.1 JAVA SDK 客户端配置规范化
在业务接口很多,客户端机器很多的业务场景,客户端配置肯定要做到如下几点:
- 超时工夫设置为秒级,防止超时工夫设置过端引起重复的建链断链。
- 客户端须要配置所有 mongos 代理地址,不能配置单点,否则流量到一个 mongos 很容易引起霎时流量峰值的建链认证。
- 减少 mongos 代理数量,这样能够分流,保障同一时刻每个代理的新键链接尽可能的少,客户端在多代理配置时,默认是平衡流量散发的,如果某个代理负载高,客户端会主动剔除。
如果没有 MongoDB 内核源码研发能力,能够参考该客户端配置办法,同时淘汰 linux-2.6 版本内核,采纳 linux-3.10 或者更高版本内核,基本上能够躲避踩同样类型的坑。
4.2 MongoDB 内核源码优化(摈弃内核态获取随机数,抉择用户随机数算法)
详见 2.5.2 章节。
4.3 PHP 短链接业务,如何躲避踩坑
因为 PHP 业务属于短链接业务,如果流量很高,不可避免的要频繁建链断链,也就会走 sasl 认证流程,最终多线程频繁读取 ”/dev/urandom” 文件,很容易引起后面的问题。这种状况,能够采纳 4.1 java 客户端相似的标准,同时不要应用低版本的 Linux 内核,采纳 3.x 以上内核版本,就能够躲避该问题的存在。
5. MongoDB 内核源码设计与实现剖析
本文相干的 MongoDB 线程模型及随机数算法实现相干源码剖析如下:
MongoDB 动静线程模型源码设计与实现剖析:
https://github.com/y123456yz/…
MongoDB 一个链接一个线程模型源码设计与实现剖析:
https://github.com/y123456yz/…
MongoDB 内核态及用户态随机数算法实现剖析:
https://github.com/y123456yz/…