3.2 单线程服务器的常用模型
Reactor 模型
non-blocking IO + IO multiplexing。
这种基于事件驱动的编程模型也有其本质缺点:要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数中,不利于维护和理解。
3.3 多线程服务器的常用模型
默认情况下,使用 non-blocking IO + one loop per thread模式。
3.3.1 one loop per thread
在这种模式中,每个 IO 线程都有一个 event loop(Reactor),用于处理读写和定时事件。优点如下:
- 线程数量基本固定,不会频繁创建与销毁;
- 可以轻松在线程间调配负载;
- IO 事件发生的线程是固定的,同一个 TCP 连接不必考虑事件并发。
EventLoop 代表了线程的主循环,需要让哪个线程工作就把 timer 或 TCP 连接注册到哪个线程的 loop 中即可。对实时性有要求的 connection 可以单独用一个线程,数据量大的 connection 可以独占一个线程,并把数据处理任务分摊到另外几个计算线程中(线程池)。
3.4 进程间通信只用 TCP
TCP 端口由一个进程独占,且 OS 会自动回收。
分布式系统中使用 TCP 长连接
一个分布式系统由运行在多台机器上的多个进程组成,进程之间采用 TCP 长连接。优点如下:
- 容易定位分布式系统中的服务之间的依赖关系。在机器上运行
netstat -tpna|grep :port
就能列出用到某服务的客户端地址,然后在客户机器上用netstat
或lsof
找出是哪个进程发起的连接。于是在迁移服务时能防止出现 outage。TCP 短连接和 UDP 不具备这一特性。 - 通过接收/发送队列的长度也容易定位网络或程序故障。 正常运行时,
netstat
打印的 Recv-Q 和Send-Q 都应该接近 0,或者在 0 附近摆动。 如果 Recv-Q 保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞。如果 Send-Q 保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去。通过持续监控 Recv-Q 和 Send-Q 就能及早预警性能或可用性故障:
3.5 多线程服务器的适用场合
处理并发连接成为开发服务端程序的基本任务呀,主要有两种方式:
多核机器的运作模式有:
- 运行一个单线程的进程(不可伸缩,不能发挥多核机器的能力)
- 运行一个多线程的进程(相比③没有优势)
- 运行多个单线程的进程
- a. 简单把①中的进程运行多份
- b. 主进程+worker 进程,如果必须绑定一个 TCP port,比如 httpd+fastcgi
-
3.5.1 必须使用单线程的场合
有两种场合必须使用单线程:
程序可能会
fork()
。只有单线程程序可以**fork()**
。- 限制程序的 CPU 占用率。单线程最多占满一个核,不会影响客户主要服务。
3.5.2 单线程程序的优缺点
Event loop 有一个明显缺点:是非抢占式的。
多线程不一定有想象中的性能优势——如果用很少的 CPU 负载就能让 IO 跑满,或者用很少的 IO 流量就能让 CPU 跑满,那么多线程其实并无提升。
例如: 对于静态Web服务器,或者FTP服务器,CPU 的负载较轻,主要瓶颈在磁盘 IO 和网络 IO 方面。这时候往往一个单线程的程序(模式1)就 能撑满 IO。用多线程并不能提高吞吐量,因为 IO 硬件容量已经饱和了。 同理,这时增加 CPU 数目也不能提高吞吐量。
3.5.3 适用多线程的场景
场景:提高响应速度,让 IO 和计算互相重叠,降低 latency(从接收/等待到完成的全程时间)。
线程的分类
- IO 线程:主循环是 IO 多路复用,阻塞地等在 select/poll/epoll_wait 系统调用上。也处理定时事件。功能也不止 IO,简单的计算也可以放入(编解码)。
- 计算线程:主循环是 blockingqueue,阻塞等待在条件变量上。这类线程一般位于 thread pool 中。通常不涉及 IO,一般要避免任何阻塞操作。
- 第三方库所用的线程,比如 logging / database connection。
服务器程序一般不会频繁启动和终止线程,几乎只在程序启动的时候创建,在服务运行期间是不创建线程的。
第三章末的问答很有价值,多看多钻研。