容器mtu设置不对,引发的网关502问题

背景

先来看看我们的技术架构。我们使用了比较"流行"的java微服务框架spring cloud。
主要由:注册中心+配置中心+网关+各业务微服务 模块组成。我们在阿里云上买了4台ecs。ecs 用rancher 搭建了k8s集群。容器内网关服务监听端口为8081,以nodeport的形式绑定宿主机端口为30004。

使用DNS负载弊端-ecs流量不好控制

最开始,我们通过DNS负载,将流量打到4台ecs外网ip。随着访问量增加,我们发现流量不好控制。为了限制服务器成本,我们一般会选固定水管包月的ecs。4台ecs服务都要开流量,水管开大了浪费,小了又报警。而且此时服务老不稳定(这里的不稳定原因是多方面的,后面介绍)。我们以为是带宽问题,于是买了阿里云负载均衡器slb。slb使用7层负载(因为我们提供https服务,直接在负载均衡配了)连到4台ecs的30004端口。这样我们就可以不为ecs开流量了,所有流量从负载均衡过来,ecs直接走内网。这种走内网流量不算钱,而且没有带宽限制,只需要为负载均衡配置流量带宽就行了。

开始暴露问题

问题一,负载均衡开始暴健康检查错误。

这个错误基本就是5xx等。我们发了工单,阿里云说我们upstream服务本身有问题。我们排查了业务bug。但是问题依然存在。由于团队小没有精通k8s和spring cloud的人,让我们一度怀疑使用错了框架。理由是流量可能被多次负载。slb 随机负载到4台物理机,流量又可能会从物理机转发到其它物理机的网关(稍微了解k8是的人可能知道,假如你有两台机子,ip为100和101。如果你的网关仅仅部署了一个实例在101上,那么你通过101,和100都能访问到,这中间其实有流量转发),然后网关转发流量给业务模块时,又可能负载到了其他物理机。很复杂吧,稍后我们揭开这个流量转发的秘密。我们怀疑包转圈不出来搞丢了。这个阴霾一直伴随着我们。

解决问题:系统调优是一道必须的工序

经过阿里云工单的交流,可能是我们没有对系统调优。发来一大堆系统调优参数。以前从来没认为系统调优是必须的一道工序。其中有一个参数是非常重要的net.core.somaxconn.具体调优过程,请参考后面的文章。
调优过后,健康检查不再报错。这个问题就解决了。

问题二,接口低概率爆502

我们接口反馈经常超时,爆网关502。排除这种问题,首要想到的是查看日志。阿里云的负载均衡访问日志默认是没有的,需要额外的开通服务。这里跟我们的后端工程师考虑的一样。认为这种访问日志巨大,所以咱们的网关访问日志也没有开,只开了错误日志。但是不开不行,根本不知道哪里有问题。于是强烈要求下,配好了阿里云负载均衡日志。从日志中我们看到,有5%的接口暴502,还有5%左右爆504.这是不能忍受的,其实这里跟我们系统调优不彻底有关系。由于涉及到容器,调优有小坑。这篇文章不展开。当我们确定调优彻底后,发现仍然有0.04%的接口爆502。

我们每15分钟约有10万次请求,其中有20-30次爆502.这个值不算高,不是所有人都关注了。但是随着时间推移,越来越多研发测试表明服务不稳定,老是遇到502.特别是在ios支付后同步凭据接口(这是一个post接口,且body比较大)。一开始我们还没找出这个规律,大的请求,502高。最开始只是一个人用postman 模拟,后来大家都在自己机子上用postman 模拟才找出来了这个规律。因为其中一个人的接口报错概率非常大,而把这个接口给别人模拟调用的时候,概率就非常小。就比对差异,发现是这个人发送接口的时候不小心附了一个大的body。果然,稳定复现。大body 触发502的概率基本是70%-80%。

keepalive ?

这个问题陷入僵局,我们从网上找了很多相关文章。其中两个最接近我们的case。一个是keepalive。总的来说就是阿里云负载均衡开启了keepalive和我们网关保持长连接。当连接处于空闲一定时间后,两端会释放这个连接。阿里云是一个连接池,相当于阿里云从连接池中拿了一个被释放的链接出来使用。解决方案就是把网关的这个keepalive 空闲释放时间设置得比阿里云的要大1s。这里其实有很多知识点。首先,我们网关spring cloud gateway 没提供一个可配置时间的地方,还得搞搞代码才行。spring cloud gateway 底层用的reactor netty(它是spring cloud基于netty框架的一个封装)。于是乎查了一遍netty,几乎很少文章提到怎么去改这个设置。所以spring cloud 我还是“流行”我打了引号。只有翻netty 源码了,然后必须让后端同学把网关的访问日志打开。后来发现访问日志没啥价值,反而是开起debug级别的日志看到了有价值的东西,日志显示xx连接永久存活字样,对框架不熟悉,大概理解是这个意思。日志结合代码,基本确定netty 会永久性保持长连接。所以排除了keepalive的锅。一切又回到了0。另一个case,是怀疑rmem 没有系统调优,一翻操作后,依然没有解除bug。

tcpdump抓包定位大致问题

只能抓包看了。有容器的抓包会比较难点。有些容器镜像没有tcpdump这种工具,yum都没有。所以只能在宿主机上切换网络空间,去抓包。这里要单独搞一篇文章来讲。
最后,我们在4个宿主机上抓包,在网关所在在4个容器抓包,发现了一些眉目。当流量从阿里云负载均衡打到ip为207的宿主机上时,3000端口捕捉到了这个请求,然后这个请求跑了到宿主机为205的容器的网关8081端口上去了。分析207宿主机的包,和容器的包。有一个tcp 包丢了。207接到了slb的包,但是这个包没有在205所在容器出现。为啥会丢这个包呢?我们怎么debug才能找出原因呢?最大的一个怀疑是iptables有问题,k8s转这个包出去,需要经过iptables。于是我们找了debug iptables的方法。期间又有一篇文章很像我们的case。但是最后没有和这篇文章出现一样的问题。陷入了僵局。与阿里云多次工单沟通,我们描述了我们抓到的包的情况,新建了一个负载均衡,提供ecs秘密让阿里云帮我们看。结论终于出来了。阿里云工单回复说怀疑我们mtu 有问题。让我们排查ecs和容器内的mtu。后面肯定一顿mtu的排查。
bug解除。

解决问题:

出现问题的根本原因就是我们的mtu设置有问题。rancher 傻瓜式安装集群,有一个重要的mtu 参数需要设置,如果默认不设置容器内的mtu 会是宿主的网卡的mtu,1500。rancher的文档中提了选择这种网络驱动时,mtu必须设置为1450,因为这种overlay网络需要预留一些协议头空间。但是我们的工程师在部署集群的时候没看到这个点。

总结了一些关键概念和技巧,将在后续文章中介绍。

1、net.core.somaxconn相关的tcp两个队列,以及操作系统如何观察这两个队列。以及容器如何调优,且确保生效。
2、tcpdump 分析包的过程,海量日志中如何收集,锁定跟踪某次请求。滑动窗口,mtu,mss相关概念,以及ping工具在网络调试的重要性。
3、iptable debugg过程,和基本操作。
4、负载均衡+k8s+spring-cloud-gateway这套流量是如何负载。