16. 服务器调制、调试和测试
第16章 服务器调制、调试和测试
Linux平台的一个优秀特性是内核微调,即我们可以通过修改文件的方式来调整内核参数。16.2节将讨论与服务器性能相关的部分内核参数。这些内核参数中,系统或进程能打开的最大文件描述符数尤其重要,所以我们在16.1节单独讨论之。
在服务器的开发过程中,我们可能碰到各种意想不到的错误。一种调试方法是用tcpdump抓包,正如本书前面章节介绍的那样。不过这种方法主要用于分析程序的输入和输出。对于服务器的逻辑错误,更方便的调试方法是使用gdb调试器。我们将在16.3节讨论如何用gdb调试多进程和多线程程序。
编写压力测试工具通常被认为是服务器开发的一个部分。压力测试工具模拟现实世界中高并发的客户请求,以测试服务器在高压状态下的稳定性。我们将在16.4节给出一个简单的压力测试程序。
1. 最大文件描述符数
文件描述符是服务器程序的宝贵资源,几乎所有的系统调用都是和文件描述符打交道。系统分配给应用程序的文件描述符数量是有限的,所以我们必须总是关闭那些已经不再使用的文件描述符,以释放它们占用的资源。比如作为守护进程运行的服务器程序就应该总是关闭标准输入、标准输出和标准错误这3个文件描述符。
Linux对应用程序能打开的最大文件描述符数量有两个层次的限制:用户级限制和系统级限制。用户级限制是指目标用户运行的所有进程总共能打开的文件描述符数;系统级的限制是指所有用户总共能打开的文件描述符数。
下面这个命令是最常用的查看用户级文件描述符数限制的方法:
1
$ulimit-n
1
$ulimit-SHn max-file-number
1
2* hard nofile max-file-number
* soft nofile max-file-number
如果要修改系统级文件描述符数限制,则可以使用如下命令: 1
sysctl-w fs.file-max=max-file-number
1
fs.file-max=max-file-number
2. 调整内核参数
几乎所有的内核模块,包括内核核心模块和驱动程序,都在/proc/sys文件系统下提供了某些配置文件以供用户调整模块的属性和行为。通常一个配置文件对应一个内核参数,文件名就是参数的名字,文件的内容是参数的值。我们可以通过命令sysctl -a查看所有这些内核参数。本节将讨论其中和网络编程关系较为紧密的部分内核参数。
2.1 /proc/sys/fs目录下的部分文件
/proc/sys/fs目录下的内核参数都与文件系统相关。对于服务器程序来说,其中最重要的是如下两个参数:
/proc/sys/fs/file-max
,系统级文件描述符数限制。直接修改这个参数和16.1节讨论的修改方法有相同的效果(不过这是临时修改)。一般修改/proc/sys/fs/file-max后,应用程序需要把/proc/sys/fs/inode-max设置为新/proc/sys/fs/file-max值的3~4倍,否则可能导致i节点数不够用。/proc/sys/fs/epoll/max_user_watches
,一个用户能够往epoll内核事件表中注册的事件的总量。它是指该用户打开的所有epoll实例总共能监听的事件数目,而不是单个epoll实例能监听的事件数目。往epoll内核事件表中注册一个事件,在32位系统上大概消耗90字节的内核空间,在64位系统上则消耗160字节的内核空间。所以,这个内核参数限制了epoll使用的内核内存总量。
2.2 /proc/sys/net目录下的部分文件
内核中网络模块的相关参数都位于/proc/sys/net目录下,其中和TCP/IP协议相关的参数主要位于如下三个子目录中:core、ipv4和ipv6。在前面的章节中,我们已经介绍过这些子目录下的很多参数的含义,现在再总结一下和服务器性能相关的部分参数。
/proc/sys/net/core/somaxconn
,指定listen监听队列里,能够建立完整连接从而进入ESTABLISHED状态的socket的最大数目。读者不妨修改该参数并重新运行代码清单5-3,看看其影响。/proc/sys/net/ipv4/tcp_max_syn_backlog
,指定listen监听队列里,能够转移至ESTABLISHED或者SYN_RCVD状态的socket的最大数目。/proc/sys/net/ipv4/tcp_wmem
,它包含3个值,分别指定一个socket的TCP写缓冲区的最小值、默认值和最大值。/proc/sys/net/ipv4/tcp_rmem
,它包含3个值,分别指定一个socket的TCP读缓冲区的最小值、默认值和最大值。在代码清单3-6中,我们正是通过修改这个参数来改变接收通告窗口大小的。/proc/sys/net/ipv4/tcp_syncookies
,指定是否打开TCP同步标签(syncookie)。同步标签通过启动cookie来防止一个监听socket因不停地重复接收来自同一个地址的连接请求(同步报文段),而导致listen监听队列溢出(所谓的SYN风暴)。
除了通过直接修改文件的方式来修改这些系统参数外,我们也可以使用方法sysctl命令来修改它们。这两种修改方式都是临时的。永久的修改方法是在/etc/sysctl.conf文件中加入相应网络参数及其数值,并执行sysctl -p使之生效,就像修改系统最大允许打开的文件描述符数那样。
3. gdb调试
Linux程序员必然都使用过gdb调试器来调试程序。我们也假设读者懂得基本的gdb调试方法,比如设置断点,查看变量等。这一节要讨论的是如何使用gdb来调试多进程和多线程程序,因为这是后台程序调试不可避免而又比较困难的部分。
3.1 用gdb调试多进程程序
如果一个进程通过fork系统调用创建了子进程,gdb会继续调试原来的进程,子进程则正常运行。那么该如何调试子进程呢?常用的方法有如下两种。
- 单独调试子进程
子进程从本质上说也是一个进程,因此我们可以用通用的gdb调试方法来调试它。举例来说,如果要调试代码清单15-2描述的CGI进程池服务器的某一个子进程,则我们可以先运行服务器,然后找到目标子进程的PID,再将其附加(attach)到gdb调试器上,具体操作如代码清单16-1所示。
代码清单16-1 通过附加子进程的PID来调试子进程
1 |
|
- 使用调试器选项follow-fork-mode
gdb调试器的选项follow-fork-mode允许我们选择程序在执行fork系统调用后是继续调试父进程还是调试子进程。其用法如下:
1 |
|
其中,mode的可选值是parent和child,分别表示调试父进程和子进程。还是使用前面的例子,这次考虑使用follow-fork-mode选项来调试子进程,如代码清单16-2所示。
代码清单16-2 使用follow-fork-mode选项调试子进程
1 |
|
3.2 用gdb调试多线程程序
gdb有一组命令可辅助多线程程序的调试。下面我们仅列举其中常用的一些:
info threads
,显示当前可调试的所有线程。gdb会为每个线程分配一个ID,我们可以使用这个ID来操作对应的线程。ID前面有“*”号的线程是当前被调试的线程。thread ID
,调试目标ID指定的线程。set scheduler-locking [off|on|step]
。调试多线程程序时,默认除了被调试的线程在执行外,其他线程也在继续执行,但有的时候我们希望只让被调试的线程运行。这可以通过这个命令来实现。该命令设置scheduler-locking的值:off表示不锁定任何线程,即所有线程都可以继续执行,这是默认值;on表示只有当前被调试的线程会继续执行;step表示在单步执行的时候,只有当前线程会执行。
举例来说,如果要依次调试代码清单15-6所描述的Web服务器(名为websrv)的父线程和子线程,则可以采用代码清单16-3所示的方法。
代码清单16-3 独立调试父线程和子线程
1 |
|
最后,关于调试进程池和线程池程序的一个不错的方法,是先将池中的进程个数或线程个数减少至1,以观察程序的逻辑是否正确,比如代码清单16-3就是这样做的;然后逐步增加进程或线程的数量,以调试进程或线程的同步是否正确。
16.4 压力测试
压力测试程序有很多种实现方式,比如I/O复用方式,多线程、多进程并发编程方式,以及这些方式的结合使用。不过,单纯的I/O复用方式的施压程度是最高的,因为线程和进程的调度本身也是要占用一定CPU时间的。因此,我们将使用epoll来实现一个通用的服务器压力测试程序,如代码清单16-4所示。
代码清单16-4 服务器压力测试程序
1 |
|
下面考虑使用该压力测试程序(名为stress_test)来测试代码清单15-6所描述的Web服务器的稳定性。我们先在测试机器ernest-laptop上运行websrv,然后从Kongming20上执行stress_test,向websrv服务器发起1000个连接。具体操作如下:
1 |
|
如果websrv服务器程序足够稳定,那么websrv和stress_test这两个程序将一直运行下去,并不断交换数据。