03多线程服务器使用场合与常用模型

最后修改:

进程与线程

进程是操作系统里最重要的两个概念之一(另一个是文件)。粗略为内存中正常运行的程序,在 linux 端是通过 fork() 系统调用出来的东西,windows 下用 CreateProcess() 的产物。

进程独立的地址空间,线程共享地址空间。

单线程服务器常用编程模型

non-blocking IO + IO multiplexing 即 Reactor 模型。如下框架:

  • lighttpd,单线程服务器。(与 Nginx 类似,每个工作进程又一个 event loop)
  • libevent,libev。
  • ACE,Poo C++ libraries。
  • Java NIO,包括 Apache Mina 和 Netty
  • POE (Perl)
  • Twisted (Python)

Boost.Asio 和 Windows I/O Completion Ports 实现了 Proactor 模型。ACE 也实现了Proactor 模型。

多线程服务器常用编程模型

  1. 每个请求创建一个线程,使用阻塞 IO 操作。可伸缩性不佳。
  2. 使用线程池,同样使用阻塞 IO 操作,与 1 比,提高性能的措施。
  3. 使用 non-blocking-IO + IO multiplexing 。即 Java NIO 方式
  4. Leader/Follower 等高级模式

one loop per thread

程序中每个 IO 线程有一个 event loop (或叫 Reactor)。 用于处理读写或定时事件。

好处:

  • 线程数据基本固定,可在程序启动时设置,不会频繁创建和销毁。
  • 可以方便的在线程间调配负载。
  • IO 事件发生的线程是固定的,同一个 TCP 连接不必考虑事件并发

线程池

muduo 中 ThreadPool,BlockingQueue, BoundedBlockingQueue

BlockingQueue 可参考 java.util.concurrent 中的 (Array|Linked) BlockingQueue,使用基本数据结构和一个 mutex 2个 condition variables

Intel Threading Building Blocks 里的 concurrent_queue

推荐模式

one loop per thread + threadpool

  • event loop (也叫 IO loop) 用作 IO multiplexing 。 配合 non-blocking IO 和定时器
  • thread pool 用来作计算,具体可以是任务队列或生产者消费者队列

进程间通信只用 TCP

进程间通信首先 Sockets ,可跨主机,具有伸缩性。

tcpdump + WireShark 分析网络利器,可编写自动化回归测试,也可用 tcpcopy 之类工具进行压力测试

数据格式推荐 Google Protocol Buffers

TCP 长连接好处:

  1. 容易定位分布式系统中的服务之间的依赖关系。netstat -tpna | grep :port 可以找到服务的客户端地址。在客户端使用 netstat 或 lsof 查看哪个进程发起的连接。
  2. 通过接收和发送队列长度也容易定位网络或程序故障。netstat 的 Recv-Q 或 Send-Q 正常在 0 上下波动或为 0,如果 Recv-Q 不变或增加可能发生死锁或阻塞。如果 Send-Q 不变或增加,可能服务器太忙,来不及处理,也可能中间网络路由器故障,丢包,甚至对方服务器掉线。

多线程服务器的适用场合

服务端网络处理并发连接两种方式:

  1. 当“线程”很廉价时,一台机器上可以创建远高于 CPU 数目的“线程”。这时一个线程只处理一个 TCP 连接(甚至半个),通常使用阻塞 IO (至少看起来如此)。例如:Python gevent、Go goroutine、Erlang actor。这里的“线程”由语言的 runtime 自行调度,与操作系统线程不是一回事。
  2. 当线程很宝贵时,一台机器上只能创建与 CPU 数目相当的线程。这时一个线程要处理多个 TCP 连接上的 IO,通常使用非阻塞 IO 和 IO multiplexing。例如:libevent、muduo、Netty。这是原生线程,能被操作系统的任务调度看见。

model:

  1. 运行一个单线程的进程
  2. 运行一个多线程的进程
  3. 运行多个单线程的进程
  4. 运行多个多线程的进程
  • 模式1是不可伸缩的,不能发挥多核的能力。
  • 模式3是目前公认的主流模式。有以下两种子模式:
    • 简单的把模式1的进程运行多份
    • 主进程 +worker 进程,如果必须绑定到一个 TCP port ,比如 httpd + fastcgi

必须用单线程的场合

  1. 程序可能会 fork(2)
  2. 限制程序的 CPU 占有率

一个 fork(2) 之后一般有两种行为:

  1. 立刻执行 exec(), 变身为另一个程序。例如:shell 和 inetd。又比如 lighttpd fork() 出子进程,然后运行 fastcgi 程序。或者集群中运行再计算节点上的负责启动 job 的守护进程(即“看门狗进程")
  2. 不调用 exec(), 继续执行当前程序。

以上只有看门狗进程必须坚持单线程,其他均可替换为多线程程序(功能上)。

单线程程序的优缺点

优势:简单。

Event loop 明显缺点,非抢占。

多线程有优势吗?

  • IO bound 或 CPU bound 服务没有优势。用很少的 CPU 负载就能让 IO 跑满或用很少的 IO 流量就能让 CPU跑满,多线程没啥用。
  • 对于静态 web 页面,或者 ftp 服务器。cpu 负载很轻,主要瓶颈在磁盘 io 和网络 io 方面。这个使用用模式1 一个单线程程序就能撑满 io。用多线程并不能提高吞吐量,因为硬件容量已经饱和。同理增加 CPU 也不能提高吞吐量。

适用多线程程序的场景

提高响应速度,让 IO 和 “计算”相互重叠,降低 latency。虽然多线不能提高绝对性能,但是能提高平均响应性能。

一个程序做成多线程,大致满足:

  • 有多个 cpu 可用。
  • 线程间有共享数据,即内存中全局状态。如果没有共享数据用模式中主进程 +worker 进程。
  • 共享数据是可修改的,而不是静态的常量表。如果数据不能修改可以用进程间 shared memory。模式3即可。
  • 提供非均质的服务。即事件的响应有优先级差异,用专门的线程处理优先级高的事件。防止优先级反转。
  • latency 和 throughput 同样重要。不是简单的 io bound 和 cpu bound。程序有相当的计算量。
  • 利用异步操作。比如 logging,无法 log file 还是 log server,都不应该阻塞 critical path(关键路径)
  • 能 scale up。
  • 具有可预测的性能。随负载增加,性能缓慢下降,超过一个值后会急速下降。线程数目一般不随负载变化。
  • 多线程能有效的划分责任和功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是所有的逻辑塞到一个 event loop 里,不同类别的事件之间相互影响。

线程分类

  1. IO 线程。这类线程主循环的 IO multiplexing。阻塞的等待在 select/poll/epoll_wait 系统调用上。功能不止 IO,简单的计算也可以放入其中,比如消息的编码和解码
  2. 计算线程。这类线程主要循环是 blocking queue,阻塞的等待在 condition variable上。这类线程一般位于 thread pool 中。不涉及 IO,一般避免任何阻塞操作。
  3. 第三方库所用的线程。比如 logging。又比如 database connection

Linux 能启动多少个线程

对于 32bit linux,一个进程地址空间 4G,其用户态能访问 3GB 左右,一个线程默认栈大小10MB,一个继承大约启动 300 多个线程。如果不改调用栈大小,300左右是上限,程序的其他部分(数据段、代码段、堆、动态库等)同样要占用内存。

对于 64bit linux 线程数目大大增加,:64 位系统的虚拟地址空间理论上为 2⁶⁴ 字节(远超物理内存),但线程栈的虚拟地址分配会占用连续地址空间,极端情况下可能因地址碎片导致无法分配新栈(即使内存充足)。 不考虑内核对线程限制。(16GB 内存):若线程栈设为默认 8MB,且其他资源充足,实际可启动约 1000~2000 个线程(受限于物理内存)。

多线程能提高并发度吗

并发连接数 不能

thread per connection 不适合高并发场合,其 scalability 不佳。one loop per thread 的并发度足够大,其与 cpu 数目成正比。

维度thread-per-connectionone-loop-per-thread
线程数与连接数关系1:1(连接多则线程多)1:N(一个线程处理多个连接)
线程数与 CPU 关系无关(可能远多于 CPU 核心)成正比(通常等于 CPU 核心数)
资源开销高(线程栈 + 调度成本)低(固定线程 + 事件驱动)
并发上限低(受限于线程数和内存)高(受限于 IO 多路复用能力)
适用场景低并发、短连接(如内部服务)高并发、长连接(如 Web 服务器)

多线程能提高吞吐量吗

对于计算密集型服务,不能

多线程能降低响应时间吗

设计合理,充分利用多核 可以

多线程程序如何让 IO 和 “计算” 相互重叠,降低 latency

基本思路:把 IO 操作(通常是写操作)通过 BlockingQueue 交给别的线程去做,自己不必等待。

为什么第三方库往往用自己的线程

第三方库不一定能很好的适用并融入这个 event loop framework,有时需要用线程来做一些的串并转换。

什么是线程池大小的阻抗匹配原则

T 线程数 * P 每个线程占用 cpu 的时间 = C 占用 cpu 的个数

C = 8 T = 1.0 那么 T = 8 正好跑满 cpu

C = 8 T = 0.5 那么 T = 16 跑满 cpu 50% 的线程能让 cpu 繁忙,启动再多只会增加上下文的切换开销而降低性能,并不能再提高吞吐量

除了 Ractor 模型,其他的 non-trivial 模型

Proactor,如果一次请求响应中要和别的进程打多次交道,那么 Proactor 模型能做到更高的并发度。代价是,代码变得支离破碎,难以理解。

一个多线程得进程和多个相同得单线程进程的模型如何选择

工作集指服务程序响应一次请求访问的内存大小。

根据工作集做取舍,访问内存大,则用多线程,可以进程内共享数据。访问内存小,可以用单线程的多个进程,享受编程的简单性。

Reference

《Erlang 程序设计》

http://www.cs.wustl.edu/~schmidt/PDF/proactor.pdf

http://www.cs.wustl.edu/~schmidt/PDF/Reactor1-93.pdf,Reactor2-93.pdf,Reactor.pdf

http://www.kegel.com/c10k.html

http://www.cs.uwaterloo.ca/~brecht/pubs.html

http://hal.inria.fr/docs/00/67/44/75/PDF/paper.pdf

http://pod.tst.eu/http://cvs.schmorp.de/libdev/ev.pod#THREADS_AND_COROUTINES

http://blog.csdn.net/haoel/article/details/2224055

http://code.google.com/p/tcpcopy/

http://kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html

http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html

https://computing.llnl.gov/linux/slurm

http://www.slideshare.net/chenshuo/zurg-part-1

http://blog.csdn.net/Solstice/article/details/5334243

http://www.cs.berkeley.edu/~culler/papers/events.pdf

http://blog.csdn.net/Solstice/article/details/2096209

https://github.com/chaoslawful/drizzle-nginx-module

http://jscex.info/zh-cn/

山重水复疑无路,柳暗花明又一村
使用 Hugo 构建
主题 StackJimmy 设计