服务器软件性能优化

本文介绍了服务器程序性能优化的一般性方法,以及部分常见服务器程序的性能优化步骤。服务器程序指的是接收客户端程序请求,执行对应操作,并将结果返回给客户端的程序,如Nginx、Tomcat、SQLite、Berkeley DB等。

1 优化方法

服务器性能优化是为了提高服务器性能而进行的一系列操作,本文关注的是程序(包括操作系统)层面的优化,因此不涉及诸如增加硬件、升级硬件或升级固件版本等方法。本文提到的性能优化,指的是通过调整程序参数或程序代码,提高程序性能的行为。本文主要关注工程方面的优化,不涉及算法优化等技术。

2 优化目标

本文关注于服务器程序,因此采用吞吐量(throughput)和时延(latency)作为性能度量指标。其他的性能度量指标,比如网络流量和耗电量等,不在考虑范围之内。

吞吐量是单位时间内服务器处理的请求数量平均值。时延是客户端从发送请求到接收应答所经历的时间平均值。在本文中,性能优化的目标是提高吞吐量,降低时延。

3 计算机模型

计算机分为处理器、存储器和通信线路。处理器负责执行指令,进行运算。存储器负责存储数据,数据以字节为单位。存储器分为顺序存储器和随机存储器。顺序存储器只能按顺序存取字节,随机存储器没有这样的限制。通信线路有两个端点,一个连接到处理器,另外一个连接到存储器或处理器。通信线路负责将数据在两个端点之间传递。通信线路上传递的数据也叫做消息。由多个通信线路连接在一起的一组处理器和存储器组成网络。

下面的Java代码展示了这些模型的接口:

public interface Processor {
        void get_next_instrument();
        void execute_instrument();
};

public interface SequentialStorage {
        long getBlockSize();
        void rewind();
        // Block 是固定长度的字节数组,比如byte[512]。
        Block read();
        void write(Block data);
}

public interface RandomAccessStorage extends SequentialStorage {
        long getSize();
        void moveTo(long position);
}

public interface CommunicationLine {
        // End可以是处理器或存储器,但不允许两个End都是存储器。
        void establish(End end1, End end2);
        void sendToEnd1(byte[] data);
        void sendToEnd2(byte[] data);
};

度量处理器性能的指标是每秒执行的指令数(MIPS)。存储器的性能指标是访问时间和容量。对于随机存储器,访问数据操作包含寻找数据位置和传输数据两个操作,因此访问时间是这两个步骤耗时之和。对于顺序存储器,我们可以将moveTo操作定义为

void moveTo(long position) {
        rewind();
        int skipBlockCount = position / BLOCK_COUNT;
        while (skipBlockCount-- > 0) {
                read();
        }
}

这样就可以基于顺序存储器构建一个随机存储器。因此“访问时间=寻址时间+传输时间”这一公式也适用于顺序存储器。度量通信线路性能的指标是带宽和时延。带宽是单位时间内通信线路可以传递的比特数,以bps为单位。时延时从开始发送消息到接收第一个字节所经历的时间。时延通常由通信线路的长度所决定。

如果可以保证数据的接收顺序和发送顺序一致,那么通信线路看起来很像是一个顺序存储器。但二者存在两点重要区别:一是从存储器中读取数据后,数据仍然保存在存储器上,可以再次读取。而从通信线路中接收消息后,消息从从通信线路中移除。二是从存储器中存取单位数据时,无论成功或失败,操作时间存在一个上限。而通信线路无法满足这样的条件,因为某个端点可能长时间不发送消息。

4 性能优化模型

4.1 基础模型

客户端程序(简称客户端)是一个处理器,服务器程序(简称服务器)由若干个处理器、存储器和通信线路组成。客户端和服务器以通信线路连接。客户端发送消息给服务器,服务器程序执行相应的操作,然后将处理结果返回给客户端。客户端发送的消息叫做请求,服务器返回的对应请求的处理结果叫做应答。

这里提到的处理器、存储器和通信线路都是逻辑上的模型,并非特指CPU、硬盘和以太网。比如CPU、线程、进程都可以是处理器,L1缓存、内存、磁盘、磁带都可以是存储器,TCP连接、消息队列、数据总线都可以是通信线路。

客户端从发送请求到接收应答的过程可以分为三个阶段:客户端将请求发送到服务器、服务器处理请求、服务器将应答发送给客户端。假设这三个阶段分别耗时t1、t2和t3,并假各客户端按顺序依次发送请求,那么服务器的时延是t1+t2+t3,吞吐量是1/(t1+t2+t3)。

4.2 队列模型

假设有两个客户端向服务器发送请求,在基础模型下,服务器的处理过程如下:

接收请求1。
处理请求1。
发送应答1。
接收请求2。
处理请求2。
发送应答2。

显然“发送应答1”和“接收请求2”两个任务之间没有依赖关系,因此可以并行处理,以提高系统吞吐量。这就是队列模型。 在队列模型中,服务器将收到请求保存到请求队列,处理器循环从请求队列中读取请求并进行处理。这个处理过程和从其他客户端接收消息的操作是并行或并发的。类似的,应答的发送和业务逻辑处理也是并行或并发的。假设请求队列的长度是l,那么第二阶段的耗时变为

t2' = 请求在队列中等待调度的时间 + 实际处理时间 = (l - 1)t2 + t2 = lt2

因此服务器的时延变为t1+l*t2+t3,吞吐量变为1/t2。队列机制会增加吞吐量,代价是时延也随之增加。队列模型是基本模型一般化推广,当队列长度为1时,队列模型和基本模型非常相似。

按照前面的计算机模型,当系统中存在n个客户端时,请求队列由一个处理器(叫做队列处理器)和n+1个消息线路组成。队列处理器和每个客户端之间都存在一个通信线路,最后一个通信线路连接到服务器的业务逻辑处理器上。队列处理器从每个客户端接收消息,将消息发送到最后一个通信线路上,传递给业务逻辑处理器。应答队列也是类似的,只是消息传递的顺序相反。请求队列和应答队列也可以叫做输入队列和输出队列。

在服务器中通常包含多个模块,每个模块都可以看成是由一个业务逻辑处理器、一个输入队列、一个输出队列组成的网络。假设模块i的处理时间是t[i],输入队列长度是len[i],那么这个模块的时延就是len[i]*t[i],服务器的时延就是这些模块时延的和。

此外,t1和t3的任务是传输数据,t2的任务是执行业务逻辑。这是两类不同类型的任务。如果没有队列,CPU和程序需要在这两类任务之间频繁切换,一方面使程序变得复杂,容易出错;另一方面,不利于充分发挥CPU性能,也不方便进行针对性优化。

队列有两种常见的实现方式,一种是单线程批处理方式,一种是多线程异步队列方式。下面的代码展示了这两种方式。

public class SingleThreadBatch {
        public void processLoop() {
                while (notQuit) {
                        Queue<Request> inputQueue = receiveFromAllClients(maxQueueSize, maxWaitTime);

                        Queue<Response> outputQueue = new Queue<>();
                        for (Request request: inputQueue) {
                                Response response = process(request);
                                outputQueue.put(response);
                        }

                        sendAllResponse(outputQueue);
                }
        }
}

public class MultiThreadAsyncQueue {
        AsyncQueue<Request> inputQueue = new AsyncQueue<>();
        AsyncQueue<Request> outputQueue = new AsyncQueue<>();

        private receiveThread = new Thread() {
                        @Override
                        public void run() {
                                while (notQuit) {
                                        for (Client client: allClients) {
                                                Request request = client.receiveNoWait();
                                                if (request != null) {
                                                        inputQueue.enqueue(request);
                                                }
                                        }
                                }
                        }
                };

        private sendThread = new Thread() {
                        @Override
                        public void run() {
                                while (notQuit) {
                                        Response response = outputQueue.dequeue();
                                        send(response);
                                }
                        }
                };

         public void processLoop() {
                while (notQuit) {
                        Request request = inputQueue.dequeue();
                        Response response = process(request);
                        outputQueue.enqueue(response);
                }
        }
}

5 性能优化思路

为了提高吞吐量,必须充分利用CPU资源,让CPU满载。CPU满载后,请求不断堆积在队列中。为了避免时延过长,服务器需要进行控制队列长度。这个操作叫做流控。因此性能优化分为两步:提高CPU使用率、然后进行流控。

5.1 提高CPU使用率

CPU使用率低的原因有三点:一是过早流控,引发处理线程饥饿;二是处理线程在等待IO;三是线程调度不充分,没有充分利用多核的优势。

过早流控是因为队列长队设置得过小,通过观察队列丢包情况可以判断这一点。确认后适当增加队列长度,就可以提高CPU使用率。对于等待IO的情况,可以使用异步调用或多线程同时处理IO,降低CPU等待IO的时间。线程调度不充分的表现为部分CPU核心使用率非常高,其余核心使用率非常低,这时可以通过调整处理线程数量来进行优化。

CPU满载并非表示这个阶段的优化完成,必须保证CPU时间都用在处理业务逻辑上。要确认这一点需要对程序进行跟踪。通常CPU满载却没有用于处理业务逻辑的原因在于同步和线程调用。

5.2 流控

流控要保证在CPU满载的同时,尽量缩短队列长度。流控通常在接收客户端请求的队列进行。如果在中间队列进行,会浪费CPU处理时间和队列空间。

6 参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • 专业考题类型管理运行工作负责人一般作业考题内容选项A选项B选项C选项D选项E选项F正确答案 变电单选GYSZ本规程...
    小白兔去钓鱼阅读 8,984评论 0 13
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,096评论 1 32
  • 一、计算机网络在信息时代中的作用 网络分为:电信网络、有线电视网络、计算机网络 网络向用户提供的功能:①连通性(用...
    dmmy大印阅读 1,607评论 0 2
  • 在服务器端程序开发领域,性能问题一直是备受关注的重点。业界有大量的框架、组件、类库都是以性能为卖点而广为人知。然而...
    dreamer_lk阅读 1,010评论 0 17
  • 每年的6、7月份总会有同一个热门话题,席卷大江南北,就是高考。前两天,我看到这样一篇报道,大意是说现在的高考状元,...
    我是王小钰阅读 934评论 0 6