找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 6843|回复: 5

高性能服务器设计

[复制链接]
发表于 2009-6-18 15:22:26 | 显示全部楼层 |阅读模式
先后查看了haproxyl7swlighttpd的相关源码,无一例外,他们一致认为多路复用是性能最好的服务器架构。事实也确实应该如此,进程的出现一方面就是为了保存任务的执行上下文从而简化应用程序设计,如果程序的逻辑结构不是很复杂,那么用整个进程控制块来保存执行上下文未免有些大材小用,加上进程调度和其他的一些额外开销,程序设计上的高效很可能会被执行时的低效所抵消。代价也是有的:程序设计工作将更加具有挑战性。
体系结构选定之后,我们就要考虑更加细节的部分,比如说用什么操作系统,用操作系统提供的那些API。在这方面,前辈们已经做过很多,我们只需要简单的“拿来”即可,如果再去枉费唇舌,简直就是浪费时间,图财害命。High-Performance Server Architecture从根本上分析了导致服务器低效的罪魁祸首:数据拷贝、(用户和内核)上下文切换、内存申请(管理)和锁竞争;The C10K Problem列举并分析了UNIX、Linux甚至是部分Windows为提高服务器性能而设计的一些系统调用接口,这篇文档的难能可贵之处还在于它一致保持更新;Benchmarking BSD and Linux更是通过实测数据用图表的形式把BSD和Linux的相关系统调用的性能直观地陈列在我们眼前,结果还是令人激动的:Linux 2.6的相关系统调用的时间复杂度竟然是O(1)。
简单的总结如下:

1. 操作系统采用Linux 2.6.x内核,不仅因为它的高性能,更因为它大开源(这并不是说其他的UNIX或者是BSD衍生物不开源)给程序设计带来的便利,我们甚至可以把服务做到内核空间。

2. 多路复用采用epoll的“电平触发”(Level Triggered)模式,必要时可以采用“边缘触发”(Edge Triggered),但要注意防止数据停滞。

3. 为避免数据拷贝可以采用sendfile系统调用发送小文件,或者是文件的小部分,注意避免sendfile因磁盘IO而导致的阻塞。

4. 如果服务操作设计大量磁盘IO操作,应选用Linux内核提供的异步IO机制,其对应的用户空间库为libaio,注意:这里提到异步IO库并非目前glibc中附带的异步IO实现。

5. 如果同时有多个数据需要传输,采用writev/readv来减少系统调用所带来的上下文切换开销,如果数据要写到网络套接字文件描述符,这也能在一定程度上防止网络上出现比较小帧,为此,还可以有选择地开启TCP_CORK选项。

6. 实现自己的内存管理,比如说缓存数据,复用常用数据结构等。

7. 用多线程替代多进程,线程库当然选择nptl。

8. 避免进程/线程间非必要的同步,保持互斥区的短小。

上面这些琐碎的细节在ESR看来可能都是过早优化,他可能又会建议我们等待硬件的升级。哈哈,提醒还是不无道理的,算法的设计部分,我们更要下大力气,因地制宜地降低算法的时间复杂度。为什么不提空间复杂度呢?内存的价格还是相对低廉吧,不过还是不要忘记现在的计算机瓶颈多在内存的访问。
有一点需要提醒一下,目前SMP系统和多核心CPU比较常见,如果还是仅采用单进程(线程)的多路复用模型,那么同一时间将只有一个CPU为这个进程(线程)服务,并不能充分发挥CPU的计算能力,所以需要至少CPU(CPU核心)数目个进程(线程)来分担系统负担。有一个变通的解决方案:不用修改源码,在服务器上运行两个服务程序的实例,当然这个时候服务端口应该是不同的,然后在其前端放置负载均衡器将流量和连接平均分配到两个服务端口,可以简单的通过DNAT来实现负载均衡。其实,这个时候我们已经把多CPU或者是多核系统看成了多个系统组成的集群。
为了提高服务器的性能,单纯的依靠提高单个服务器的处理能力似乎不能奏效,况且配置越高的服务器花销也就越高,为此人们经常采用服务器集群的方式,通过把计算尽可能地分配到相对比较廉价的机器上单独完成,籍此来提升服务器的整体性能,事实证明,这种体系结构不仅是切实可行的,而且还能提高服务器的可用性,容错能力也较强。在网络服务器方面,Linux内核中的由国人章文嵩先生设计的IP层负载均衡解决方案LVS比较有名,还有就是工作于应用层的haproxy和刚刚起步的l7sw。
http://blog.chinaunix.net/u/5251/showart_236329.html

发表于 2009-7-21 09:38:38 | 显示全部楼层

回复 #1 winston 的帖子

关于“内存申请”这一点,我做了如下试验:
1. 开5个线程,每个线程在循环里面重复5000次的申请一个ACE_Message_Block,对这个Block进行一些操作,然后release掉;

2. 做一个MessageBlockManager,预先分配25000个ACE_Message_Block。同样开5个线程,每个线程在循环里面重复5000次的向MessageBlockManager申请一个ACE_Message_Block,对这个Block进行同样的操作后,把Block归还给MessageBlockManager。
其中,MessageBlockManager维护一个Vector,里面存放的预分配的ACE_Message_Block的指针。申请和归还操作都需要对这个Vector上锁。

以上两种方法测试下来,效率相当。5000*5次操作,第2中方法快那么几百毫秒。当然第二种方法的好处是内存集中管理,可以降低内存泄露的风险,但是也会但来一定的复杂性。

各位有什么好的建议吗,如果效率上没有什么提升的话,似乎没多大的价值?
 楼主| 发表于 2009-7-21 10:14:03 | 显示全部楼层
内存池的一个优点在于,有效防止内存碎片出现。等你的程序运行时间很久后,内存碎片就会多了起来,性能就会下降。
发表于 2009-11-10 12:32:11 | 显示全部楼层
楼上正解。能力有限,楼主的贴子我没懂,但感谢一楼做的实验,一直为跨线程的内存池效率担忧,看来还可以
发表于 2010-4-8 07:46:10 | 显示全部楼层
感谢楼主,支持楼主发的好贴子!
发表于 2010-4-27 18:42:28 | 显示全部楼层
最近恶补了这方面的知识. 非常同意楼主的观点.
我说说我的看法.
(1) 使用事件驱动. 因为事件驱动是由内核支持.
(2) 尽量使用少量的线程. (有的应用使用的是多进程,这里因为各个应用情况不同也不能说多线程一定好,多进程就一定不好.)
      windowsr的iocp模型给的例子中.只创建了跟cpu个数相同的线程数. 因为线程数太好. 线程的切换开销也很大. 线程间的同步开销也很头痛.
(3) 减少不必要的内存分配置和数据拷贝
(4) 使用队列.
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-5-6 09:34 , Processed in 0.045496 second(s), 6 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表