关于linux:追踪将服务器CPU耗光的凶手

62次阅读

共计 3875 个字符,预计需要花费 10 分钟才能阅读完成。

后面咱们探讨零碎调用的时候论断是耗时 200ns-15us 不等。不过我明天说的我的这个遭逢可能会让你进一步意识零碎调用的真正开销。在本节里你会看到一个耗时 2.5ms 的 connect 零碎调用,留神是毫秒, 相当于 2500us!

问题形容

过后是我的一个线上云控接口,是 nginx+lua 写的。失常状况下,单虚机 8 核 8G 能够抗每秒 2000 左右的 QPS,负载还比拟衰弱。然而该服务近期开始呈现一些 500 状态的申请了,监控时不时会呈现报警。通过 sar -u 查看峰值时 cpu 余量只剩下了 20-30%。

第一步、迅速锁定嫌疑人

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 零碎调用耗时比照:


<centor> 图 1:失常状况下 </centor>


<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 工夫。



开发内功修炼之 CPU 篇专辑:

  • 1. 你认为你的多核 CPU 都是真核吗?多核“假象”
  • 2. 据说你只知内存,而不知缓存?CPU 示意很伤心!
  • 3.TLB 缓存是个神马鬼,如何查看 TLB miss?
  • 4. 过程 / 线程切换到底须要多少开销?
  • 5. 协程到底比线程牛在什么中央?
  • 6. 软中断会吃掉你多少 CPU?
  • 7. 一次零碎调用开销到底有多大?
  • 8. 一次简略的 php 申请 redis 会有哪些开销?
  • 9. 函数调用太多了会有性能问题吗?

我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~

正文完
 0