在某天凌晨,欧美服出现了卡顿,但是服务器的 CPU 并不高,内部服务监控日志来看,是 clusteragent 的 mq overload 了,网卡 IO 从流量上来看,也没达到瓶颈。后来,对方 SA 说是参数配错了,8 核的机器配了 24 线程,改成 8 线程后重启,就不卡顿了。这里排查的时候用到了 tracetag,虽然有点麻烦,还是值得的。

后来,又再一次出现了同样的问题。反复调参后,发现 center 配 4 线程就有问题,配 6 线程就不会卡顿。子熏怀疑是 线程权重设置有问题 ,前面的 -1 都改成 0 就解决了,改成 0 以后即使再配 4 线程,也没有问题了。出问题的代码如下:

    static int weight[] = { 
        -1, -1, -1, -1, 0, 0, 0, 0,
        1, 1, 1, 1, 1, 1, 1, 1, 
        2, 2, 2, 2, 2, 2, 2, 2, 
        3, 3, 3, 3, 3, 3, 3, 3, };
    struct worker_parm wp[thread];
    for (i=0;i<thread;i++) {
        wp[i].m = m;
        wp[i].id = i;
        if (i < sizeof(weight)/sizeof(weight[0])) {
            wp[i].weight= weight[i];
        } else {
            wp[i].weight = 0;
        }
        create_thread(&pid[i+3], thread_worker, &wp[i]);
    }

这里线程创建的时候,会定义一个权重数组,4 核的时候,相当于每个线程权重都是 -1,权重的用途 可以看这里

 1     int i,n=1;
 2     struct skynet_message msg;
 3 
 4     for (i=0;i<n;i++) {
 5         if (skynet_mq_pop(q,&msg)) {
 6             skynet_context_release(ctx);
 7             return skynet_globalmq_pop();
 8         } else if (i==0 && weight >= 0) {
 9             n = skynet_mq_length(q);
10             n >>= weight;
11         }
12         int overload = skynet_mq_overload(q);
13         if (overload) {
14             skynet_error(ctx, "May overload, message queue length = %d", overload);
15         }
16 
17         skynet_monitor_trigger(sm, msg.source , handle);
18 
19         if (ctx->cb == NULL) {
20             skynet_free(msg.data);
21         } else {
22             dispatch_message(ctx, &msg);
23         }
24 
25         skynet_monitor_trigger(sm, 0,0);
26     }

这里 10 行的 n 表示 mq 的未处理消息条数,n 右移 -1 位就是 0,下面 22 行的 dispatch_message 就只会执行一次。右移 0 位,就表示有多少条消息,就处理多少条消息,右移 1 位,就是只处理(未处理消息条数 /2)这么多。skynet_mq_pop 是说从 mq 里尝试弹消息,成功返回 0,mq 是空则返回 1。如果第一次进入 for 循环,mq 有未处理消息,这时候权重为 0,就会处理当前积压的消息。即使处理过程中来了新的消息,因为第 8 行的代码不会重复跑,所以会归并到下次消息调度来处理。

子熏提到可以将权重改成 -1,0,-1,0 这样排布,目前项目是直接将最低线程数改成 8 来解决的,不用改代码。远期来看,可能引入优先级比较好,高优先级的多处理一下。子熏提的另一个问题值得思考,线程每次处理一个消息,然后切换出去,跟每次处理完当下所有消息,再切换出去,不考虑切换开销的话,工作量是不变的。为什么现在会出现 CPU 不高的情况呢?而且消息处理明显慢了很多

我觉得这从侧面反映了当前的测量体系是有问题的,CPU 不高不能反映游戏就正常运行了。有可能是线程总是在切换 ctx,所以 cache miss 比较高,效率就比较低了。最好是能够搞出来一个工作模型,里面跑的是跟线上业务类似的工作负载,每条消息处理不会太快也不会太慢,然后比较一下处理一条就切换,跟处理完再切换,两种不同调度的吞吐量区别。

参考阅读:

http://www.brendangregg.com/blog/2017-05-09/cpu-utilization-is-wrong.html

CPU Utilization is Wrong 作者认为 CPU 使用率指导意义不大,应该看的是 Instruction Per Cycle,小于 1 的话往往是 memory 限制了 CPU 运行效率。吐槽下 virtualbox,没提供相应的计数器,在 perf 里看不出来。。。

后续

上次子熏提到,如果消息很多处理不过来,CPU 不可能这么低。最后,子熏在卡顿的时候直接用 gdb attach 进去了,发现卡顿的时候在调 poll。捋了一遍代码,结果是连接平台的 mq 用了 block 住线程的 poll 接口,然后最近合服,跑了多条 mq。。。应该一条 mq 注册多个 serverid,而不是每个 serverid 单独起一个服务,每个服务都去连平台 mq 的。

调了权重后能极大改善,是因为 mq 服务卡了几个线程,剩余线程每次只处理一个的话,吞吐量就不够了,换成 0 会改善很多,但是如果 mq 服务更多(合服数量更多),还是会有同样的问题的。

这里用 gdb 有点蛋疼,如果 attach 进去没遇上那个窗口,就比较难看到了。可惜用 systemtap 对系统要求太高了,装完一堆东西,还是这个没有那个没有,没法跑出来看。最后发现 strace 可以看到,strace -k -fp 进程 pid -T -tt -o /tmp/strace.log 可以打出来系统调用,还有每个调用的耗时,-k 可以打调用栈。-k 依赖于 libunwind,我是自己编译后的版本才有的。。