追踪将服务器CPU耗光的凶手!

TCP连接耗时之谜

前面我们讨论系统调用的时候结论是耗时200ns-15us不等。不过我今天说的我的这个遭遇可能会让你进一步认识系统调用的真正开销。在本节里你会看到一个耗时2.5ms的connect系统调用,注意是毫秒,相当于2500us!

问题描述

当时是我的一个线上云控接口,是nginx+lua写的。正常情况下,单虚机8核8G可以抗每秒2000左右的QPS,负载还比较健康。但是该服务近期开始出现一些500状态的请求了,监控时不时会出现报警。通过sar -u查看峰值时cpu余量只剩下了20-30%。

图3.jpg

第一步、迅速锁定嫌疑人

top命令查看cpu使用,通过top命令发现峰值的时候cpu确实消耗的比较多,idle只有20-30%左右。在使用的cpu里,软中断的占比也比较高,1/3左右。
再通过cat /proc/softirqs查看到软中断是都是网络IO产生的NET_TX,NET_RX,和时钟TIMER。
既然软中断这个贼人吃掉了我这么多的CPU时间,所以案件的嫌疑人就这么初步被我锁定了。

处理,那既然是NET_TX,NET_RX和TIMER都高,那咱就挑可以削减的功能砍一砍呗。

  • 1.砍掉多余的gettimeofday系统调用
  • 2.每个请求砍掉一次非必须Redis访问,只留了必要的。

结果:峰值的cpu余量从确实多出来一些了。报警频率确实下来了,但是还是偶尔会有零星的报警。可见该嫌疑人并非主犯。。

第二步、干掉一大片,真凶在其中

接着查看网络连接的情况ss -n -t -a发现,ESTABLISH状态的链接不是很多,但是TIME-WAIT有11W多。继续研究发现针对..*.122:6390的TIME-WAIT已经超过了3W。所以端口有限。原来呀,上一步执行时只干掉了连接上的数据请求,但是tcp握手请求仍然存在。

处理:彻底干掉了针对..*.122:6390的网络连接请求,只保留了必须保留的逻辑。
结果:问题彻底解决。sar -u查看cpu的idle余量竟然达到了90%多。

Tips:单台机器如果作为TCP的客户端,有如下限制

  1. ESTABLISH状态的连接只能有ip_local_port_range范围内的个数。
  2. 只有针对特定ip,特定port的TIME-WAIT过多,超过或接近ip_local_port_range,再新建立连接可能会出现无端口可用的情况。( 总的TIME-WAIT过多并不一定有问题 )

没想到一个简单砍掉一个对redis server的tcp连接,能把cpu优化到这么多。大大出乎意料,而且也想不明白。 根据我之前的性能测试经验,每个tcp连接的建立大约只需要消耗36usec的cpu时间。我们来估算一下:

当时server的qps大约在2000左右,假设是均匀分布的,则8个核每个核每秒只需要处理250个请求。也就是说每秒一条tcp连接需要消耗的cpu时间为:250*36usec = 9ms.

也就是说,正常来讲砍掉这些握手开销只能节约1%左右的cpu,不至于有这么大的提升。(即使我上面的估算只考虑了建立连接,没有统计释放连接的cpu开销,但是连接释放cpu开销也和建立连接差不多。)

总之,这一步确实解决了问题,但是代价是牺牲了一个业务逻辑。

最终、审出真凶,真相大白于天下

我在某一台机器上把老的有问题的代码回滚了回来,恢复问题现场。然后只修改一下ip_local_port_range。 然后请出了strace这个命令。
通过strace -c 统计到对于所有系统调用的开销汇总。 结果我们发现了connect系统调用这个二货,在正常的机器上只需要22us左右,在有问题的机器上竟然花掉来 2500us,上涨了100倍。我们用strace -c $PID查看一下出问题时和正常时的connect系统调用耗时对比:

图1.png

 

<centor>图1:正常情况下</centor>

图2.png

 

<centor>图2:出问题时</centor>

然后回想起了..*.122:6390的TIME-WAIT已经超过了3W,会不会TIME_WAIT占用了太多端口导致端口不足呢。因此查看端口内核参数配置:

# sysctl -a | grep ip_local_port_range
net.ipv4.ip_local_port_range = 32768    65000

果然发现该机器上的端口范围只开了3W多个,也就是说端口已经几乎快用满了。那就提高端口可用数量:

# vim /etc/sysctl.conf
net.ipv4.ip_local_port_range = 10000 65000

connect系统调用恢复理性状态,整体服务器的CPU使用率非常健康。

问题的根本原因是建立TCP连接使用的端口数量上(ip_local_port_range)不充裕,导致connect系统调用开销上涨了将近100倍!

后来我们的一位开发同学帮忙翻到了connect系统调用里的一段源码

int inet_hash_connect(struct inet_timewait_death_row *death_row,
               struct sock *sk)
{
    return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),
            __inet_check_established, __inet_hash_nolisten);
}

int __inet_hash_connect(struct inet_timewait_death_row *death_row,
                struct sock *sk, u32 port_offset,
                int (*check_established)(struct inet_timewait_death_row *,
                        struct sock *, __u16, struct inet_timewait_sock **),
                int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))
{
        struct inet_hashinfo *hinfo = death_row->hashinfo;
        const unsigned short snum = inet_sk(sk)->inet_num;
        struct inet_bind_hashbucket *head;
        struct inet_bind_bucket *tb;
        int ret;
        struct net *net = sock_net(sk);
        int twrefcnt = 1;

        if (!snum) {
                int i, remaining, low, high, port;
                static u32 hint;
                u32 offset = hint + port_offset;
                struct inet_timewait_sock *tw = NULL;

                inet_get_local_port_range(&low, &high);
                remaining = (high - low) + 1;

                local_bh_disable();
                for (i = 1; i <= remaining; i++) {
                        port = low + (i + offset) % remaining;
                        if (inet_is_reserved_local_port(port))
                                continue;
                        ......
        }
}

static inline u32 inet_sk_port_offset(const struct sock *sk)
{
        const struct inet_sock *inet = inet_sk(sk);  
        return secure_ipv4_port_ephemeral(inet->inet_rcv_saddr,  
                                          inet->inet_daddr,  
                                          inet->inet_dport);  
}

从上面源代码可见,临时端口选择过程是生成一个随机数,利用随机数在ip_local_port_range范围内取值,如果取到的值在ip_local_reserved_ports范围内 ,那就再依次取下一个值,直到不在ip_local_reserved_ports范围内为止。原来临时端口竟然是随机撞。出。来。的。。 也就是说假如就有range里配置了5W个端口可以用,已经使用掉了49999个。那么新建立连接的时候,可能需要调用这个随机函数5W次才能撞到这个没用的端口身上。

所以请记得要保证你可用临时端口的充裕,避免你的connect系统调用进入SB模式。正常端口充足的时候,只需要22usec。但是一旦出现端口紧张,则一次系统调用耗时会上升到2.5ms,整整多出100倍。这个开销比正常tcp连接的建立吃掉的cpu时间(每个30usec左右)的开销要大的多。

解决TIME_WAIT的办法除了放宽端口数量限制外,还可以考虑设置net.ipv4.tcp_tw_recycle和net.ipv4.tcp_tw_reuse这两个参数,避免端口长时间保守地等待2MSL时间。

欢迎关注个人公众号“开发内功修炼”,打通理论与实践的任督二脉

参考文献

 

<think> 嗯,用户现在在追问网络缓存耗尽的告警信息,看来ta对系统监控和故障恢复很关注。之前讨论过缓存耗尽会导致网络阻塞和ping不通的问题,现在ta想了解具体如何通过告警机制来捕捉这类故障。 从引用来看,用户提供的资料都是高可用系统设计案例:引用[1]强调分钟级故障恢复需要关键路径监控,引用[2]展示背压机制下的指标监控实践,其他引用也涉及性能瓶颈分析和资源优化。用户应该是个运维工程师或系统架构师,正在搭建容灾体系。 需要重点提取三点:一是告警触发条件(如缓存使用率阈值),二是关联指标(流量、丢包率等),三是引用[1][2]提到的实施框架。用户可能希望看到具体指标名称和联动逻辑,比如当socket缓冲区使用率持续超过90%且丢包率上升时触发告警。 数学表达要严谨但简洁,比如用$Q_{used}/Q_{max} > 0.9$表示缓存阈值。告警延迟公式$\Delta t = t_{detect} + t_{notify}$可以量化响应时效性,但用户更关注实践细节,所以放在补充说明里即可。 用户没问但可能需要的延伸点:如何区分应用层和传输层缓存告警?引用[4]提到的内存参数调整其实也属于预防措施,可以糅合在优化建议里。最后生成的问题要偏向工具链建设,比如Prometheus配置或ELK日志分析,这符合ta的系统视角。 </think> 以下是网络缓存耗光时系统告警的关键信息及实施要点,结合引用内容分析: --- ### 一、告警触发条件与核心指标 网络缓存耗尽告警需监控以下核心指标,阈值根据系统容量动态设定: 1. **缓存使用率** - 定义:$ \text{缓存使用率} = \frac{\text{当前已用缓存}}{\text{缓存总量}} \times 100\%$ - 告警点:持续 >90% 时触发(如Linux的`net.ipv4.tcp_mem`或应用层缓冲区)[^1][^4] - *示例*:Direct ByteBuffer耗尽时通过`-XX:MaxDirectMemorySize`阈值告警[^4] 2. **丢包率与队列堆积** - 缓存满导致丢包率上升(如引用[2]中**背压场景丢包率达20%**) - 监控队列长度:`netstat`中的`Send-Q`/`Recv-Q`持续增长[^2] 3. **关联性能指标** - **带宽打满**:如引用[3]中网络带宽被日志读取打满,触发CPU/带宽瓶颈告警 - **GC异常**:内存缓存频繁GC(引用[2]提到需监控GC抖动) - **响应延迟**:PING延迟突增或超时(如TCP重传率 >5%) --- ### 二、告警系统设计要点 基于引用[1][2][3]的实践: 1. **分层监控** - **基础设施层**:OS内核缓存(`/proc/net/snmp`)、网卡队列 - **中间件层**:Kafka等消息队列的背压丢弃率(如引用[2]) - **应用层**:Direct ByteBuffer、Socket缓冲区使用量[^4] 2. **关键路径覆盖** - 围绕业务核心链路部署探针(如API网关→服务→DB) - 模糊查询等重IO操作需单独监控(引用[3]优化案例)[^3] 3. **动态阈值调整** - 流量高峰时段自动放宽阈值(如带宽阈值 +20%) - 结合历史基线预测异常(如环比上升300%触发) --- ### 三、告警响应与优化措施 | **告警阶段** | **动作** | **案例参考** | |-------------|---------|-------------| | **检测** | 分钟级日志采集+指标聚合 | 引用[1]的"分钟级故障恢复"[^1] | | **定位** | 关联堆栈、网络流量、GC日志 | 引用[4]用Arthas诊断内存问题[^4] | | **恢复** | 扩容缓存/启用背压/限流 | 引用[2]背压丢弃20%流量保稳定[^2] | | **预防** | 资源隔离/逻辑下沉(如引用[3]优化) | 模糊匹配下沉至分布式文件系统[^3] | --- ### 四、典型告警信息示例 ```log [CRITICAL] 网络缓存耗尽告警 主机: websvr-01 指标: TCP接收缓冲区使用率=98% 关联现象: - 丢包率升至15%(阈值:5%) - Ping平均延迟>500ms 建议动作: 1. 紧急扩容: sysctl -w net.core.rmem_max=16777216 2. 限流: tc qdisc add dev eth0 root tbf rate 1gbit burst 10mb 3. 检查应用内存泄漏(参考引用[4])[^4] ``` --- ### 五、优化告警有效性的关键 1. **避免误报** - 排除短时峰值:连续3个采样点超阈值才告警 - 关联多指标:仅当`高缓存使用率+高丢包率`同时发生才触发 2. **根因分析辅助** - 自动捕获`jstack`/`tcpdump`(如引用[4]内存分析) - 日志保留策略优化(引用[5]提到磁盘空间不足导致日志丢失)[^5] 3. **自动化处理** - 脚本自动扩容缓冲区或重启异常进程 - 背压机制自动降级非核心服务(引用[2]实践)[^2] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值