[前言]
前一个Blog我们使用了一个叫cs的程序作为例子,那个程序是我为了举例子临时写的,这个代码我共享在这里:GitHub - nekin2012/btest。后面我要再举例子的话,就都加到这个地方来。由于这些代码没有经过最基本的软件质量保证工艺,所以质量相当低,读者不要直接使用这些代码。另外,这个代码中的cs程序已经经过上次推演的调整,现在的性能已经可以达到调度最优了,CPU占用率会全部100%,和上一个程序中的样子已经不太一样了。
cs这个程序的模型,是我们很多软件的基础模型。虽然经过很多模块和队列的分解,我们的程序会变得愈加难以辨认,但模型永远都是为不同的队列安排多组线程池的问题。这种模型的大部分优化工作是平衡线程的数量来保证CPU的利用率,然后通过限制每个队列的长度,来控制时延和和通量之间的关系。
而这里面需要特别小心的就是那个Provider-Consumer陷阱,也就是前面提到的,如果一个线程总用不完它的时间片,这个线程就会被自动提权为交互线程,这样,只要发生调度它就会抢占,这样会大幅降低整个系统的性能。解决这个问题的方法通常是两个:一个就是在那个cs中看到的,控制队列的长度,没有足够的长度根本不要发起调度。第二个就是我们要有意识控制线程的设置,特别是不要一个模块一个线程(这是最失败的设计),如果某个线程的执行时间特别短,这个工作就应该和其他线程合并,而不是独立线程。比如你发一个消息,仅仅是为了分配一个会话号,这个时间可能就是几十个时钟周期,你就不能图方便为了排队,使用一个模块队列+线程来完成这样的工作。正确的做法是把这个模块的接口直接做成函数,然后用锁保护起来。
[Amdahl模型]
前面说的这种不使用短时线程会话的策略,在大部分时候是不会引起问题的,除非你线程配置不平衡,让你的调度序列又出现交互线程。这些问题,都可以通过ftrace跟踪出来。
但不少程序员没有从这个角度考虑这个问题,他们就会试图通过spinlock来降低这种调度的可能性。当所有线程共同分享这个公共的模块的时候,我们就会形成Amdahl定律所描述的模型了。
Amdahl定律是并行计算最基础的理论了,所有学计算机的人都学过,我这里就不专门介绍了,读者如果不知道自己上网查去。
现在大家都不怎么把Amdahl当回事,因为现在大部分系统的核数远远没有达到让Amdahl触顶的规模,下面这个是我用999:1的比例配置并行-串行比时(程序参考btest的amdahl的例子),在72核的x86平台上得到的效果:
这时增加核数基本上就会达到提升处理能力的目的。
但时延上仍是有影响的:
在串行的密度非常低的时候,我们还感觉一切受控,但如果我们把并行串行比提升到99:1,乃至90:10的时候,情况就变得非常糟糕了:
因为这不再是一个Amdahl模型了,Amdahl模型的依赖是在等待的时候,你的CPU还能干其他并行的工作,而使用spinlock,你在等待的时候什么都干不了。这实际上是一个马可夫链的排队模型,
这里有四条曲线,我们先看spin的两条曲线,你会发现,当你把串行的配比增加的时候,系统在20个核左右就开始进入拐点,性能大幅跳水。而且串行的比例越大,跳水就越早。
根据一些研究报告,如果这是个标准的马可夫链的排队模型,曲线影响应该像后面两条标记为MCS曲线那样,仅仅是接近瓶颈。而这个跳水是因为,纯粹的spinlock不但引起等待时间的增加,而且因为有更多的等待者,会导致Cache更新时间的延长,从而得到一个修正的马可夫链模型,形成了跳水。如果用perf对这两种情况进行跟踪,你会发现,在系统发生跳水后,系统在的指令执行效率10个cycle执行不了一条指令:
很低的指令执行率,表明执行指令本身的执行效率低(基本在stalled-cycles上,这个指标的含义,我们后面专门写一篇blog介绍),问题要不出现在cache/总线上,要不出现在处理器自身的调度器上。
我在一台64核的ARM64服务器上做同样的实验,得到同样的结果。有论文提出使用MCS锁来避免这种情况,我在那个btest工程中快速用spinlock临时封装了一个MCS锁,可以拟合出类似马可夫链的模型(当然,这个实现没有使用原子指令,速度肯定是比较慢的,只是为了拟合模型)。
使用MCS锁后,上面的测试结果是这样的:
同样的行为在ARM64上测试结果也是一致的,理论上说,如果用ticket锁,在ARM上可以获得MCS锁一样的结果,我晚点加一个测试看看。
[Amdahl模型的启示]
我们很多人更愿意花时间去反复尝试各种设置参数,尝试这样提高系统的性能。我个人收到不少性能分析报告都是这样的。我觉得这样的分析报告相对来说价值是比较低的(当然也有其作用),因为即使一个参数进行调整带来了好处,但这种好处和其他参数组合后可能就会消失。仅仅关注一个参数的效果,最多就是给我们建立模型提供参考,我们的分析还是要聚焦到模型上,发现系统真正的瓶颈是什么。我前面的perf介绍下面,有人说perf的数据也就只是能“看看”,我认为抱有这种观点,是因为他从来只是关注效果,而不关注模型,而在系统性能优化的时候,模型远远比效果重要。效果你确实可以拿去报功,但在软件自身的架构进展上是没有用的,要得到正确的构架调整方向,模型才是第一位的。
回到Amdahl这个模型,很多系统在进行架构调整的时候,决策下得非常早,一看见mutex发生了切换,就考虑用一个“不会切换”的spin_lock取代“有可能切换”的mutex锁,而不愿意花时间去分析“为什么调度器”要做这个切换。这样头痛医头,脚痛医脚的方法(对,我说的就是MySQL),会让整个系统陷入更深层次的混乱之中,这样整个架构就变得不可控了。
[小结]
在调度上,除了IO导致的等待外,消费者-使用者模型和Amdahl模型是两个最常见的陷阱,好好基于btest类似的简化模型对CPU和调度器的行为进行分析,会有助于我们正确理解更复杂系统的运行模型。
调度模型平衡了,我们就有可能进行下一步,针对CPU执行效率的分析了。