提高服务器性能的建议

对于这里所说的服务器,更精确的定义应该是每秒处理大量离散消息或者请求的服务程序,网络服务器更符合这种情况,但并非所有的网络程序都是严格意义上的服务器。使用“高性能请求处理程序”是一个很糟糕的标题,为了叙述起来简单,下面将简称为“服务器”。

本文将从下面几个方面进行总结:
  (1) 池化技术
  (2) 环境切换
  (3) 数据复制
  (4) 锁竞争

池化技术(Pooling)

池化的概念

池是一组资源的集合,这组资源在服务器启动之初就完全被创建好并初始化,这称为静态资源分配,也就是池化。

为什么需要池化?

当服务器进入正式运行阶段,即开始处理客户端请求时,如果它需要相关的资源,就可以直接从池中获取,无需再动态分配。显然,从池中直接获取资源的开销要比动态分配小很多,因为动态分配系统资源的系统调用往往是很耗时间的。当服务器处理完一个请求后,可以把相应的资源重新放回到池中,无须执行系统调用来释放资源。所以,池就相当于服务器管理系统资源的应用层设施,它避免了服务器对内核的频繁访问。

池的应用有哪些地方?

根据资源的类型不同,池被我们分为多种类型。常见的有:内存池,进程池,线程池,连接池等。

内存池 常用于 socket 的接收缓存和发送缓存。

对于长度受限的客户请求,比如:
HTTP请求,预先分配一个大小足够的接收缓冲区是非常合理的。当客户端请求的长度超过缓存的大小时,有两种处理方案:
1> 丢弃掉这次请求
2> 动态扩大缓冲区空间

进程池&线程池 通常用于并发编程。在高并发编程的时候根据场景使用这两种类型的池,就能减少动态调用 fork / pthead_create的次数,从而减小服务器的开销。

连接池 通常用于服务器或者服务器集群的内部永久连接。

比方说,在一个程序中有很多逻辑单元都需要跟数据库打交道,如果不采用池化技术,那么就只能在逻辑单元每次需要访问数据库的时候想数据库层序发起连接,访问完毕后释放连接。但是这样的效率会非常低下。这种情况比较科学的方案就是采用连接池。 连接池是服务器预先和数据库程序建立一组连接并维护这个集合,当程序中某个逻辑单元需要访问数据库时,直接从连接池中获取一个连接的实体并使用,使用后将连接返还给连接池即可。

环境/上下文切换(Context Switches)

多线程服务器的一个优点是不同的线程可以运行在不同的CPU上,但是引起环境切换的第一个原因往往是活跃线程数比CPU个数多。随着活跃线程数相对于CPU个数的增加,线程间的切换将占用大量的CPU时间,服务器真正用于处理业务逻辑的CPU时间的比重就显得不足了。这个简单的事实解释了为什么每个连接对应一个单独线程的多线程设计模式的可伸缩性更差,所以这种方案是不可取的。对于一个可伸缩性的系统来说,限制活跃线程数少于或等于CPU个数是更有实际意义的方案。 曾经这种方案的一个变种是只使用一个活跃线程,虽然这种方案避免了环境争用,同时也避免了锁,但它不能有效利用多CPU在增加总吞吐量上的价值,因此除非程序无CPU限制(non-CPU-bound),(通常是网络I/O限制 network-I/O-bound),应该继续使用更实际的方案。
  对于最简单的多线程事件驱动服务器的概念模型, 其内部有一个请求缓存队列,客户端请求被一个或者多个监听线程获取后放到队列里,然后一个或者多个工作线程从队列里面取出请求并处理。从概念上来说,这是一个很好的模型,有很多用这种方式来实现他们的代码。这会产生什么问题吗?引起环境切换的第二个原因是把对请求的处理从一个线程转移到另一个线程。 有些人甚至把对请求的回应又切换回最初的线程去做,这真是雪上加霜,因为每一个请求至少引起了2次环境切换。把一个请求从监听线程转换到成工作线程,又转换回监听线程的过程中,使用一种“平滑”的方法来避免环境切换是非常重要的。此时,是否把连接请求分配到多个线程,或者让所有线程依次作为监听线程来服务每个连接请求,反而不重要了。
  即使在将来,也不可能有办法知道在服务器中同一时刻会有多少激活线程.毕竟,每时每刻都可能有请求从任意连接发送过来,一些进行特殊任务的“后台”线程也会在任意时刻被唤醒。那么如果你不知道当前有多少线程是激活的,又怎么能够限制激活线程的数量呢? 最简单同时也是最有效的方法之一是:用一个老式的带计数的信号量,每一个线程执行的时候就先持有信号量。如果信号量已经到了最大值,那些处于监听模式的线程被唤醒的时候可能会有一次额外的环境切换,(监听线程被唤醒是因为有连接请求到来, 此时监听线程持有信号量时发现信号量已满,所以即刻休眠), 接着它就会被阻塞在这个信号量上,一旦所有监听模式的线程都这样阻塞住了,那么它们就不会再竞争资源了,直到其中一个线程释放信号量,这样环境切换对系统的影响就可以忽略不计。更主要的是,这种方法使大部分时间处于休眠状态的线程避免在激活线程数中占用一个位置,这种方式比其它的替代方案更优雅。
  一旦处理请求的过程被分成两个阶段(监听和工作),那么更进一步,这些处理过程在将来被分成更多的阶段(更多的线程)就是很自然的事了。最简单的情况是一个完整的请求先完成第一步,然后是第二步(比如回应)。然而实际会更复杂:一个阶段可能产生出两个不同执行路径,也可能只是简单的生成一个应答(例如返回一个缓存的值)。由此每个阶段都需要知道下一步该如何做,根据阶段分发函数的返回值有三种可能的做法:

  • 请求需要被传递到另外一个阶段(返回一个描述符或者指针)

  • 请求已经完成(返回ok)

  • 请求被阻塞(返回"请求阻塞")。这和前面的情况一样,阻塞到直到别的线程释放资源

    应该注意到在这种模式下,对阶段的排队是在一个线程内完成的,而不是经由两个线程中完成。这样避免不把请求放在下一阶段的队列里,紧接着又从该队列取出这个请求来执行。这种经由很多活动队列和锁的阶段很没必要。这种把一个复杂的任务分解成多个较小的互相协作的部分的方式,看起来很熟悉,这是因为这种做法确实很经典了。

数据复制(Data Copies)

高性能服务器应该避免不必要的数据复制,尤其是当 当数据复制发生在用户代码和内核代码之间的时候。如果内核可以直接处理从socket或者文件读入的数据。则应用程序就没必要将这些数据从内核缓冲区复制到应用程序缓冲区中。这里说的“直接处理”指的是应用程序不关心这些数据的内容,不需要对它们做任何分析。比如:

  • ftp服务器。当客户请求一个文件时,服务器只需要检测目标文件是否存在,以及客户是否拥有读取它的权限,而绝对不会关心文件的具体内容。这样的话,ftp服务器就无须把目标文件的内容完整地读入到应用程序缓冲区中并调用send函数来发送,而是可以使用“零拷贝”函数sendfile来直接将其发送到客户端。

此外,用户代码内部(不访问内核)的数据复制也是应该避免的。 比如:

  • 当两个工作进程之间要传递大量的数据时,我们就应该考虑使用共享内存来在它们之间直接共享这些数据,而不是使用管道或者消息队列来传递。

还有一种可以避免数据拷贝的方法是使用buffer的描述符(或者buffer chains的描述符)来取代直接使用buffer指针,每个buffer描述符应该由以下元素组成:

  • 一个指向buffer的指针和整个buffer的长度

  • 一个指向buffer中真实数据的指针和真实数据的长度,或者长度的偏移

  • 以双向链表的形式提供指向其它buffer的指针

  • 一个引用计数

现在,代码可以简单的在相应的描述符上增加引用计数来代替内存中数据的拷贝。这种做法在某些条件下表现的相当好,包括在典型的网络协议栈的操作上,但有些情况下这做法也令人很头大。一般来说,在buffer chains的开头和结尾增加buffer很容易,对整个buffer增加引用计数,以及对buffer chains的即刻释放也很容易。在chains的中间增加buffer,一块一块的释放buffer,或者对部分buffer增加引用技术则比较困难。而分割,组合chains会让人立马崩溃。
  当然,我不建议在任何情况下都使用这种技术,因为当你想在链上搜索你想要的一个块时,就不得不遍历一遍描述符链,这甚至比数据拷贝更糟糕。最适用这种技术地方是在程序中大的数据块上,这些大数据块应该按照上面所说的那样独立的分配描述符,以避免发生拷贝,也能避免影响服务器其它部分的工作.(大数据块拷贝很消耗CPU,会影响其它并发线程的运行)。
  关于数据拷贝最后要指出的是:在避免数据拷贝时不要走极端。我看到过太多的代码为了避免数据拷贝,最后结果反而比拷贝数据更糟糕,比如产生环境切换或者一个大的I/O请求被分解了。数据拷贝是昂贵的,但是在避免它时,是收益递减的(意思是做过头了,效果反而不好)。为了除去最后少量的数据拷贝而改变代码,继而让代码复杂度翻番,不如把时间花在其它方面。

锁竞争(Lock Contention)

高效率的锁是非常难规划的, 一方面, 锁的简单化(粗粒度锁)会导致并行处理的串行化,因而降低了并发的效率和系统可伸缩性; 另一方面, 锁的复杂化(细粒度锁)在空间占用上和操作时的时间消耗上都可能产生对性能的侵蚀。偏向于粗粒度锁会有死锁发生,而偏向于细粒度锁则会产生竞争。
  由于锁倾向于对程序逻辑产生束缚,所以如果要在不影响程序正常工作的基础上规划出完美的锁方案基本是不可能的。所以一般情况下,如果服务器又更好的解决方案,我们应该尽量避免使用锁;如果服务器必须要使用锁,则可以考虑减小锁的粒度,比如使用读写锁。当所有工作线程都只读取一块共享内存的内容时,读写锁并不会增加系统的额外开销。只有当其中的某一个工作线程需要写这块内存时,系统才需要访问内核资源,锁住这块区域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值