性能优化

从去年12月份开始至今年年初,做了一个高性能网络处理服务器的性能优化的项目。在此简单记录
项目过程中的一些知识与方法,以备将来参考之用。

背景介绍

这个“高性能处理服务器”其实一点也不高性能。为什么呢,其中夹杂很多历史遗留原因。我厂的
项目存在很多“售前吹牛逼,开发很苦逼”的现象。这个产品也一样,许多年前,为了满足客户要求,
没有在性能上有过多的考虑。以致许多年后的今天,客户的要求变了,产品满足不了。
服务器处理的流程很简单:

网络收包 => 协议处理 => 分发


我们可以认为这个服务器完全是一个CPU Bound的程序,没有过多的IO,也没有大量的并发连接。
需要留意的是CPU的处理能力,网络的处理能力以及内存的使用。

什么是性能优化

用程序语言来表达的话:

1
2
3
4
5
6
while( performance != expect ) {
/* 查找系统性能瓶颈 */
find(bottlenecks);
/* 优化系统性能瓶颈 */
optimize(bottlenecks);
}

性能优化,即在性能达不到预期时,查找性能瓶颈并进行优化,重复这一过程知道达到要求。性能瓶颈的概念
我们可以借助维基百科的定义来理解。

系统瓶颈(Bottleneck): 整个系统的性能或容量,受少数部件或资源的限制。
从软件的角度上说,资源指的是CPU、内存、磁盘、网络等;部件指的是某些模块,某些流程。

  • 查找系统性能瓶颈:通过性能分析工具、热点分析工具等手段追踪瓶颈,甚至代码Review也是手段之一。
  • 优化系统性能瓶颈:采用负载均衡,算法优化,系统调优等方法优化系统性能瓶颈。
    下面我会以先描述项目过程中的实践,再提取理论总结的方式来介绍。

如何查找瓶颈

实践

我拿到这个项目的时候,领导只是说了四个字:“性能不足”

  1. 我做的第一件事是量化系统,把系统的各个处理流程都进行量化:资源的使用,处理的负荷,处理的时间等。
    例如在网络收包的每个模块,每个线程,对每一种数据包都进行了统计跟踪,从而对系统的热点能有一个直观的理解。
  2. 接着,就是准备优化前的各项工作,自动化测试工程、性能测试环境、回归测试用例等等。这些工作也相当重要,
    我们可以在每次修改以后立马知道性能是否提升了,功能是否影响了。然后就可以进入前面程序描述的逻辑了。
  3. 前面说过,这是一个CPU Bound的系统。那么,先执行top命令看下CPU的负荷情况,马上就发现了某个CPU usr跑到100%了。
    同时对应的量化指标也出现了丢包,显然,这里的CPU就是目前系统的一处瓶颈。对于这种情况下面“优化”一节,具体地描述了优化的手段。
  4. top提供的信息非常宏观,不能很好的地位到瓶颈部件,只是知道系统整体的情况。所以我使用oprofile对系统的运行情况进行跟踪,
    当我拿到profile的结果时就很明确的知道了哪些代码是热点,进而分析出哪些是需要优化的。

在我整个项目的过程,最主要的两个工具就是top和oprofile。其实如果对系统熟悉的话,只需top和量化的系统指标,
就能对系统的运行情况就能有足够的了解。再者,就是对操作系统知识的理解,如各种锁如何在cpu上体现、网络io和磁盘io在cpu上的体现、
内存的使用在什么地方等等。进一步分析时,就可以配合一些其他工具sar、vmstat等来发现问题。

理论

谈及理论,不得不提到Brendan Gregg,以下是他写的书和Slide:

我总结出的观点是:理解“系统”,理解“系统”。

第一个系统指的是需要优化的系统软件;第二个系统指的是操作系统。Brendan的Slide就像一个理解系统软件的工具箱,
用好了这些工具就理解了第一个“系统”;Brendan的书对于我就是一本操作系统性能的知识手册,读好了这本书就理解了第二个“系统”;

如何优化瓶颈

实践

负载均衡

前面提到发现的第一个瓶颈是某个核的CPU负荷过载了,而我们系统是运行在32核的服务器上的,其余的核远远未到满负荷。
因此,我需要做的是将过载的CPU的负荷均衡到其他CPU上。而这个均衡的方法就要结合具体业务来实施了,在我们的系统里,我们通过oprofile很快
定位到了过载的模块,再进一步分析,发现系统的处理模块居然是单线程运行的,系统的设计者肯定没有预想到scale out的情景。
此时,我们需要梳理业务的处理流程,分析出处理模块里的子模块哪些可以多线程运行,哪些是必须单线程运行的。(此处不得不提的是,对于较为完美
的scale out设计应该是各个模块都支持多线程并发运行,但由于历史遗留问题,该系统的的确确存在单点瓶颈)

  1. 第一次切分

  2. 第二次切分

经过切分过后的系统,能够将服务器上的其他核利用起来,我们可以使用set_schedaffinity来设置线程CPU的亲和力,使各线程尽量保持在同一个
CPU上运行,减少线程切换带来的消耗。

Cache优化

当我们将单线程切分为多线程以后,会引入线程间通信的问题。线程间的通信,经验里使用“无锁环形队列”作为桥梁是最为简单且高效的实现方式。(可以
参加http://www.github.com/wilsonwen/lockfreekit/)最理想的方式是内存零拷贝的实现方式,就是我们常说的zero copy。不过在优化过程中,发现历史
遗留的模块存在太多的自定义接口,没办法实现零拷贝,各线程交互时需要拷贝内存。这里就引入Cache优化的问题,如何部署线程才能使得内存拷贝是Cache
友好的呢?下面这张图或许能告诉我们。

为使内存拷贝尽量高效,有交互的线程要尽量放在同一个物理CPU上,这样对于L3 cache是友好的。对于scale out的同功能多线程可尽量发在同一个物理核上,
这样对L2 cache是友好的。

理论

对于多核负载均衡,Intel有本书《Multi-Core Programming Increasing Performance through Software Multithreading》
有非常清楚的讲解。此外,在系统设计的过程中,应该尽量避免单点的出现,针对高负荷的模块
更要做好scale out的设计。

后记

优化过程遇到过一些其他类型的性能问题:

  1. 操作系统的IO cache导致内存swap in/out严重,影响了系统的正常运行。
    该问题在褚霸的博客有消息的描述。