本周处理了好几例负载高问题,原因竟然都是因为微信对代理场景的支持不好导致的。

回顾十分曲折的排查过程,记录下来!顺带吐槽一下微信。

一、问题描述

背景:我们的设备作为客户处的上网出口,代理内网用户上网。

问题:设备流量不高,但是负载特别高,经常性产生断网事件,流量突降为0。

二、排查过程

2.1 分析负载和CPU

先画出问题时间点的负载趋势图,可以看到负载确实是很高的:

这个设备只是2核的,负载超过4就可以认为负载高了,而记录下来的负载基本都在10以上,远超出设备负载。

然后分析CPU采样,找到对应时间点的mpstat记录,采样中显示较高的是iowaitsoft

Fri Mar 13 08:40:08 CST 2020
Linux 2.6.30-gentoo-r8 (localhost)     03/13/20     _x86_64_    (2 CPU)

Average:     CPU    %sys %iowait    %irq   %soft  %idle
Average:     all    2.50   82.00    3.00   10.50   0.00
Average:       0    1.01   81.82    5.05   10.10   0.00
Average:       1    3.00   83.00    1.00   11.00   0.00

soft高一般都是流量过高或者连接数过大导致的,而iowait高大部分时候都是内存不足导致的,因为内存不足会导致内存cache回收,大量的磁盘脏数据刷到磁盘,导致IO高。因此下一步应该先分析内存。

2.2 分析内存占用

分析问题时间点前后的内存状态,发现了内存cache回收的情况:

2020-3-13 08:37:57
             total       used       free     shared    buffers     cached
Mem:          1809       1675        133          0          6        130

2020-3-13 08:38:02
             total       used       free     shared    buffers     cached
Mem:          1809       1546        262          0          1         27

设备只有2G内存,属于低端平台了,内存不足应该属于常态,所以经常回收cache,导致了io高。

然后统计slab占用,发现占用最高的是TCP和TCPv6,总共占用了接近三百兆:

正常情况下,这两个东西不可能占用这么大比例。这只能说明,网络流量有异常!!

内核给TCPv6和TCP分配slab的场景

为了搞清楚内核在什么场景下会分配这两块内存好对症下药,所以在内核中定位了相关代码,最后发现是创建TCP套接字的时候分配的。

slab是内核中的的内存分配机制,所有的内存都通过kmem_cache_create创建,而每块被分配的内存块都是有名字的(如上面的TCPv6和TCP)。因此在内核代码中直接搜索:

kmem_cache_create("TCPv6")

不出意料,没有搜索到相关代码。因为根据linux代码的特性,这个字符串肯定是被宏定义代替或者结构体引用了。

于是先搜索"TCPv6",在net/ipv6/tcpv6_prot中找到了定义:

struct proto tcpv6_prot = {
    .name = "TCPv6",
    // ...
}

整个内核代码中只有这一处出现了关键字段,除此之外没有其他地方有这个字符串,因此肯定一定是哪里引用这个字段。继续通过正则查找kmem_cache_create\(.*->namne,找到:

int proto_register(struct proto *prot, int alloc_slab)
{
    if (alloc_slab) {
        prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,
                    SLAB_HWCACHE_ALIGN | prot->slab_flags,
                    NULL);
    // ...
    }
}

proto_register是注册协议到内核的协议栈,产生对应协议请求的时候会调用这个方法来创建socket对象,TCPv6TCP内存块占用内存高,说明肯定是这里走得多了,这就间接说明设备肯定产生了大量连接。

2.3 分析网络流量

分析出流量有异常之后,第一个想到的是不是网络环境有问题,因为上周刚好也查了一个类似的问题:一次孤儿socket过多导致系统异常的问题排查过程。但是抓包分析后发现,流量并没有出现类似的特征,而且设备总共的孤儿socket并不多,这说明两个问题不是同一个原因。

于是只能继续分析了,因为流量特征(各种数据包的占比)都比较正常,所以排查的重点放在的包的内容身上。终于在经过了大量的分析后发现了数据异常:

以上是内网发到我们设备的代理请求,设备地址是10.115.5.2,因为是代理场景,所以目的地址是它,这没有问题。但问题出在了数据包身上,也就是最后一列,可以看到他们都是:

POST http://10.115.5.2/download HTTP/1.1

这个HTTP代理请求是请求代理本机,这是明显有问题的,也是不符合RFC规定的,正常的代理数据包应该是被代理的地址如www.biadu.com,绝不会是10.115.5.2,如:

POST http://www.baidu.com/download HTTP/1.1

以下是一些正常的代理请求:

如果代理的请求是本机,就会导致本机自己再去连接自己的80端口,造成的结果就是设备会多产生一个发往本机nginx的连接。nginx收到请求后因为不存在这个uri资源会返回404到代理进程,代理进程再返回给客户端。流程为:

本来到这里也没有很大的问题,只是会导致本机多一个连接而已。但关键的问题是,返回错误给PC客户端后,客户端会频繁发起重连,产生大量的重试请求。频率有多高呢?从下面的这个抓包截图来看,不到0.5ms的时间内内网就产生了10+个请求:

代理场景并不像路一样由,只要做NAT把地址转换一下发出去就可以了,路由对设备的资源占用并不大。代理场景除了要和内网PC执行三次握手以外,还要主动发起连接请求WAN端,要产生双倍的资源消耗。如此大规模的重连请求,占用了大量的连接资源,足以算得上dos攻击了。因此,可以认为本次问题就是内网的异常连接导致的。

这些异常的请求具体是什么呢,通过分析数据包内容和特征,很容易就能看出是微信的请求,是在请求下载小视频:

三、和微信的交涉

为了确认微信的问题,找微信客服反馈。他们承认确实是有问题,但是并不想改:

四、微信代理存在的问题

微信代理场景目前存在以下问题:

  1. 代理数据包构造不正确,也就是上面描述的场景。
  2. 微信视频不支持代理,使用代理后无法发起微信视频。
  3. 微信传输大文件时,不会经过代理,直接发到了服务端了。
  4. 不支持代理长连接。

第三个问题是出在另外一个客户身上,现象上是无法传输超出20M以上的文件。实际上通过抓包分析发现是微信直接绕过了代理,目的地址不是代理设备地址:

而不支持长连接问题就更有意思了,问题现象是微信代理后无法正常收到图片等相关资源,微信给出的反馈竟然是不支持嵌套,嵌套这个词说出来就很有意思了,刚说出来我还琢磨是什么嵌套。最后抓包发现竟然指的是HTTP长连接。

五、吐槽

从技术角度而言,这几个问题实际上并不能算成是问题,调整起来的技术难度并不高。不知道为什么就是不调整,反馈了很多次了。突然想起前两天微信的热搜,面对用户,好像并不“怂”:

最后修改:2020 年 03 月 14 日
喜欢就给我点赞吧