GitLab在线仓库服务是世界上最大的Git仓库之一。那么GitLab的工程师是如何定位和解决故障的呢?前几天GitLab博客介绍了这样一个例子。今天Bug带领大家一起学习。
原因
运维团队收到客户报告,他们在访问Gitlab时会间歇性地报告Git错误,通常来自CI工作段或类似的自动化系统。错误内容是:
ssh_exchange_identification:连接被远程主机关闭
致命错误:无法从远程存储库中读取
更重要的是,错误信息是间歇性的和随机的,不可预测的,无法重现的。并没有明确指出截图和日志来说明问题的各种现象和情况。当然,错误信息过于笼统,没有太大意义。SSH客户端报告这些信息的可能原因:碎片化的客户端或虚拟机、网络防火墙行为、ISP的操纵,当然也可能是GitLab应用问题。GitLab在线仓库每天处理2600万个Git-over-SSH连接,平均每秒约300个链接。找几个失败的显然是个有挑战性的问题。
第一条线索。
经过初步分析,运维团队联系了有问题的客户。他说这个问题每天都会发生很多次,所以以此为起点显然是合适的。客户还提供了问题对应的公网IP地址,因此可以代表HAproxy节点在GitLab前端捕获并分析数据包,定位具体的问题数据包信息。好消息是,客户使用了备用的ssh端口,因此只有两个HAProxy服务器可以进行分析,进一步将问题位置范围从16个减少到2个。
在数据包捕获之后,对于筛选的IP,在大约6.5小时内捕获了大约500MB的数据包。在数据包中发现了一些短连接:TCP连接建立,客户端发送了一个版本字符串标识符,然后HAProxy立即用正确的TCP FIN序列删除了连接。这是第一条好线索。说明问题的源头还是服务器端因为GitLab服务器端主动删除链接。
体验:Wireshark statistics menu有很多有用的工具,如果你没有真正练习过,你不会注意到它们。
比如“Session”会在抓图中显示每个TCP连接的时间、包和字节大小,并用它们来过滤。您应该在开始时使用它,而不是手动拖移捕获。例如,在本例中,您需要找到一个具有少量数据包的连接,这可以通过conversation view轻松显示。然后,可以使用该函数查找其他实例,并验证第一个实例不是一个一个的。
日志分析
那么是什么原因导致HAProxy主动删除客户端的连接呢?第二阶段似乎不是任意的。应该有更深层次的原因。除了数据包捕获日志,HAProxy日志是下一个要检查的地方。GitLab存储在GCP BigQuery中,可以通过它们浏览分析日志。首先,包捕获中一个事件的日志条目可以根据时间和TCP端口来识别,这是一个重大突破。这个条目中最有趣的细节是t_state属性,即SD。HAProxy文件显示:
s:服务器停止,或者服务器明确拒绝。
d:会话处于数据阶段。
很明显,TCP连接已经正确建立,数据正在发送,这与数据包捕获的证据相匹配。S HAProxy从后端收到RST或ICMP失败消息。发生了什么样的情况或者可能的原因,没有直接的线索。可能有许多原因,从网络问题到应用程序级别的问题。使用BigQuery通过Git backend进行聚合,很明显它并不特定于任何VM。
原来带SD的日志并不是这个问题独有的。在备用SSH端口上,HTTPS将被大量扫描,这将导致当SSH服务器在看到TLS ClientHello消息时遇到SSH问候时记录SD。
在捕捉到HAProxy和Git服务器之间的一些流量并再次使用Wireshark统计工具的同时,很明显Git服务器上的SSHD在TCP三次握手后立即拆除了与TCP FIN-ACK的连接。HAProxy还是没有发第一个包,但是马上就要发了。当它将要被发送时,Git服务器用TCP RST进行响应。所以HAProxy有理由用SD来记录连接失败。SSH正在关闭连接,显然是关闭了。只是RST SSH服务器在FIN-ACK之后接收数据包。这没有任何意义。
统计图表
在BigQuery中观察分析SD日志时,很明显在时间维度上有相当多的集群,在每分钟顶部后的前10秒达到峰值,在开始后约5-6秒达到峰值:
连接错误,按秒分组
上面的图表是基于几个小时整理的数据创建的,因此实际事实表明,它在几分钟和几小时内是一致的,甚至可能在一天的特定时间变得更糟。更有趣的是,平均峰值负载是基本负载的3倍,所以这意味着我们存在扩展问题,简单地为虚拟机配置“更多资源”来满足峰值负载可能非常昂贵。这也表明GitLab正在达到一些硬极限,这是潜在系统性问题的第一个线索。
Cron或类似的调度系统只能细分为分钟。如此多的任务在分钟或小时或它们的整数开始时运行。如果他们在从GitLab获取git之前花了几秒钟做准备,就会说明连接模式在几秒钟到一分钟内增加,导致这些时间点的误差累积。
第二课:显然,许多人正确地设置了时间同步。
如果他们没有,问题就不会这么清楚了。那么什么可能导致SSH断开连接呢?
接近问题的本质
通过查看SSHD的文档,我们找到了MaxStartups参数,它可以控制预认证状态下的最大连接数。高峰时刻,在互联网预定任务的冲击下,超过连接限制似乎是合理的。MaxStartups实际上有三个数字:低水位线,任何高于水位线的连接的掉线百分比,以及所有新连接的绝对最大值。默认值为10:30和100,GitLab的设置为100:30:200。显然,我们过去已经增加了连接设置,我们需要再次增加配置。
问题是Gitlab服务器上的openssh版本是7.2,在这个版本中查看MaxStartups设置的唯一方式需要打开调试级日志记录。这可能会导致数据泄露,所以只能在一台服务器上测试。打开调试日志后,没过几分钟,就明显超过了MaxStartups设置,于是提前放弃了连接。
事实证明,OpenSSH 7.6具有更好的MaxStartups日志记录,只需要详细的日志记录就可以获得这些信息。不太理想,但比调试级别好。
经验:在默认日志级别记录有用的信息是有意义的。服务器因为任何原因主动丢弃连接,绝对是一个运维的锅。
问题的原因找到了,那么怎么解决呢?我们可以调高MaxStartups配置,但代价是什么?这样会增加少量内存,但是会有其他影响?不能只是推测,需要实际测试。首先,将MaxStartups的值增加到50%,调整为150:30:300。这种配置带来了很大的积极作用,而没有明显的消极影响:
请看图,注意01:15之后错误数量大大减少。很明显,大量的错误被消除了,尽管仍然有非常微不足道的数字。该图显示了每30分钟、15分钟和10分钟在同一峰值聚集的时间。重要时刻是最高峰,这也和实际使用情况一致。很多人会把自己的任务安排在每小时0分钟以内每小时运行一次。这一发现进一步证实了连接高峰是由预定工作引起的,连接错误是由于最大连接数的限制。
没有明显的负面影响,SSH服务器上的CPU利用率保持不变,没有任何明显的负载增加,并且通过简单的配置释放了很多之前删除的连接。
速度限制
但是,我们不能简单的提高MaxStartups的配置。虽然通过配置提升了50%的价值,但是这个数字是不可能继续提升的。还有其他可以优化的地方。
HAProxy为其前端监听器提供了一个很好的“限速会话”选项。配置完成后,它将限制前端每秒传递给后端的新TCP连接的数量,并保持TCP套接字上的其他连接。如果传入速率超过限制,新连接将被延迟。TCP客户端只是看到了建立TCP连接前的延迟,所以不会有太大影响,只要总数不超过限制太久就行。
由于Gitlab有27个SSH后端,18个HAproxy前端,前端之间没有速率限制优化,调整比较麻烦。需要考虑新的SSH会话通过认证需要多长时间:假设MaxStartups为150,如果auth阶段需要两秒钟,那么每秒只能向每个后端发送75个新会话。计算速率限制需要四个量:两种服务器类型的数量、MaxStartups和T的值,T是SSH会话进行身份验证所需的时间。t的值很关键,但只能估计。最后,为每个前端配置的速率限制约为112.5,四舍五入为110。
调整之后,一切都还好吧?错误连接数应该为零,但实际上这种配置对错误率没有明显影响。深思,你一定错过了什么!
因此,请回到日志,确保您可以验证速率限制是否有效,并且历史记录显示该数字一直较高,因此您成功地限制了发送连接的速率。但显然这个比例还是太高了,还没有接近正确的数字,无法产生可衡量的影响。
看一下后端会发现一个奇怪的现象:在一个小时的高峰期,后端连接并不是均匀分布在所有的SSH服务器上。在选定的时间内,它在给定的一秒内从30变为121,这意味着负载平衡不是很平衡。检查配置使用的“平衡源”,这样给定的客户端IP地址将总是连接到同一个后端。如果需要会话粘性,这是一个很好的策略,但是对于SSH,没有这样的要求。因此,尝试将其更改为“leastconn ”,并用最少数量的当前连接将新的传入连接分发到后端。使用新策略后SSHCPU利用率的结果:
显然,这个策略非常有效。低使用率的两行显示金丝雀服务器,可以忽略。但在政策改变之前,由于源IP分布不均,服务器之间的负载差为2: 1。很明显,我们的一些后端比其他的更忙。.
经验四:选择特定的非默认设置时,需要对文档/问题进行评论或链接。为什么?后来者会感谢你的。
启用leastconn策略后,错误率也显著降低。继续,试试限速配置,把限速降到100,进一步降低了错误率,说明T的初始估计可能是错的。但是,如果这样的限制将导致过高的速率限制,甚至非常低的100/s,我们不想进一步放弃它。在速率限制尽可能低,短缺最小化的情况下,尝试增加MaxStartups:先200,有效,再到250,误差几乎消失,没有任何负面结果。
经验# 4:虽然违背常识,但MaxStartups似乎对性能影响不大,即使其提升远高于默认值。
如果有必要,这可能是未来可以提供的更大的级别。如果增加到几千或者几万,可能会注意到它的影响。
解释对T的估计,建立和验证SSH会话的时间是多少?通过测试,我们知道max startups 200不够,250就够了。可以计算出T可能在2.7和3.4秒之间。.
处理事故引起的问题
查看日志,经过一番思考,发现可以识别出一个具体的故障模式:t_state为SD,b_read为0。如上所述,GitLab每天处理大约26-28万个SSH连接。不幸的是,在最坏的情况下,大约有1.5%的连接被严重丢弃。显然,问题比开始时意识到的要大,以前不可能确定这一点。
经验:尽早衡量错误的比例。
如果你能意识到问题的严重性,你可能会更早地优先考虑它,尽管它仍然取决于对识别特征的了解。好的一面是,遇到MaxStartups和速率限制后,错误率下降到0.001%,也就是每天几千。这个好一点,但是还是比预期的高。在解决了其他一些运营问题后,希望最小的改动也能正式部署,错误也能彻底消除。
进一步的工作
显然,SSH验证阶段仍然需要很长时间,可能长达3.4秒。GitLab可以通过使用AuthorizedKeysCommand直接在数据库中查找SSH密钥。当有大量用户时,这对于快速操作是必不可少的,否则SSHD必须读取非常大的authorized_keys文件以便找到用户的公钥,并且这不能很好地向外扩展。目前一些调用内部HTTP API的ruby实现是用来查找的。工程研究人员Stan Hu和GitLab的常驻知识源确定Git/ SH服务器上的实例正在经历大量的队列。这可能是~3秒预认证阶段的重要贡献因素,因此需要进一步研究和解决。
当您看到表明问题正在发生的HAProxy日志时,您也需要发出警报。如果资源有限,将来可能需要进一步增加MaxStartup配置,或者添加更多Git/SSH节点。
摘要
本文分析了GitLab在线服务的一个实例,展示了在线故障排除的全过程。通过抓包、日志定位、参数配置调整测试、问题总结等过程,充分展示了从问题发现、定位到解决的全过程,并在此过程中总结了经验。总之,这个问题不是特例,它的解决方法、步骤和思路都是可以学习和推广的好案例。