陈硕 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
2010 Feb 28
这篇文章原本是前一篇博客《多线程服务器的常用编程模型》(以下简称《常用
模型》)计划中的一节,今天终于写完了。
“服务器开发”包罗万象,本文所指的“服务器开发”的含义请见《常用模型》一文,
一句话形容是:跑在多核机器上的 Linux 用户态的没有用户界面的长期运行的网
络应用程序。“长期运行”的意思不是指程序 7x24 不重启,而是程序不会因为无事
可做而退出,它会等着下一个请求的到来。例如 wget 不是长期运行的,httpd
是长期运行的。
***正名
与前文相同,本文的“进程”指的是 fork() 系统调用的产物。“线程”指的是
pthread_create() 的产物,而且我指的 pthreads 是 NPTL 的,每个线程由
clone() 产生,对应一个内核的 task_struct。本文所用的开发语言是 C++,
运行环境为 Linux。
首先,一个由多台机器组成的分布式系统必然是多进程的(字面意义上),
因为进程不能跨 OS 边界。在这个前提下,我们把目光集中到一台机器,
一台拥有至少 4 个核的普通服务器。如果要在一台多核机器上提供一种服务
或执行一个任务,可用的模式有:
* 运行一个单线程的进程
* 运行一个多线程的进程
* 运行多个单线程的进程
* 运行多个多线程的进程
这些模式之间的比较已经是老生常谈,简单地总结:
* 模式 1 是不可伸缩的 (scalable),不能发挥多核机器的计算能力;
* 模式 3 是目前公认的主流模式。它有两种子模式:
- 3a 简单地把模式 1 中的进程运行多份,如果能用多个 tcp port 对外提供服务的话;
- 3b 主进程+woker进程,如果必须绑定到一个 tcp port,比如 httpd+fastcgi。
* 模式 2 是很多人鄙视的,认为多线程程序难写,而且不比模式 3 有什么优势;
* 模式 4 更是千夫所指,它不但没有结合 2 和 3 的优点,反而汇聚了二者的缺点。
本文主要想讨论的是模式 2 和模式 3b 的优劣,即:什么时候一个服务器程序应该是多线程的。
从功能上讲,没有什么是多线程能做到而单线程做不到的,反之亦然,都是状态机嘛
(我很高兴看到反例)。从性能上讲,无论是 IO bound 还是 CPU bound 的服务,
多线程都没有什么优势。那么究竟为什么要用多线程?
在回答这个问题之前,我先谈谈必须用必须用单线程的场合。
***必须用单线程的场合
据我所知,有两种场合必须使用单线程:
* 程序可能会 fork()
* 限制程序的 CPU 占用率
先说 fork(),我在《Linux 新增系统调用的启示》中提到:
fork() 一般不能在多线程程序中调用,因为 Linux 的 fork() 只克隆当前线程的
thread of control,不克隆其他线程。也就是说不能一下子 fork() 出一个和父进程
一样的多线程子进程,Linux 也没有 forkall() 这样的系统调用。forkall() 其实也是
很难办的(从语意上),因为其他线程可能等在 condition variable 上,可能阻塞
在系统调用上,可能等着 mutex 以跨入临界区,还可能在密集的计算中,这些都
不好全盘搬到子进程里。
更为糟糕的是(根据网友指出),如果在 fork() 的一瞬间某个别的线程 a 已经获取
了 mutex,由于 fork() 出的新进程里没有这个“线程a”,那么这个 mutex 永远也不会
释放,新的进程就不能再获取那个 mutex,否则会死锁。(这一点仅为推测,还没
有做实验,不排除 fork() 会释放所有 mutex 的可能。)
综上,一个设计为可能调用 fork() 的程序必须是单线程的,比如我在《启示》一文
中提到的“看门狗进程”。多线程程序不是不能调用 fork(),而是这么做会遇到很多
麻烦,我想不出做的理由。
一个程序 fork() 之后一般有两种行为:
* 立刻执行 exec(),变身为另一个程序。例如 shell 和 inetd;又比如 lighttpd fork() 出子进程,然后运
行 fastcgi 程序。或者集群中运行在计算节点上的负责启动 job 的守护进程(即我所谓的“看门狗进程”)。
* 不调用 exec(),继续运行当前程序。要么通过共享的文件描述符与父进程通信,协同完成任务;要么接过父进程传来的文件描述符,独立完成工
作,例如 80 年代的 web 服务器 NCSA httpd。
这些行为中,我认为只有“看门狗进程”必须坚持单线程,其他的均可替换为多线程程序(从功能上讲)。
***单线程程序能限制程序的 CPU 占用率。
这个很容易理解,比如在一个 8-core 的主机上,一个单线程程序即便发生 busy-wait
(无论是因为 bug 还是因为 overload),其 CPU 使用率也只有 12.5%,即占满 1 个
core。在这种最坏的情况下,系统还是有 87.5% 的计算资源可供其他服务进程使用。
因此对于一些辅助性的程序,如果它必须和主要功能进程运行在同一台机器的话
(比如它要监控其他服务进程的状态),那么做成单线程的能避免过分抢夺系统
的计算资源。
***基于进程的分布式系统设计
《常用模型》一文提到,分布式系统的软件设计和功能划分一般应该以“进程”为
单位。我提倡用多线程,并不是说把整个系统放到一个进程里实现,而是指功能
划分之后,在实现每一类服务进程时,在必要时可以借助多线程来提高性能。
对于整个分布式系统,要做到能 scale out,即享受增加机器带来的好处。
对于上层的应用而言,每个进程的代码量控制在 10 万行 C++ 以下,这不包括
现成的 library 的代码量。这样每个进程都能被一个脑子完全理解,不会出现混
乱。(其实我更想说 5 万行。)
这里推荐一篇 Google 的好文《Introduction to Distributed System Design》。
其中点睛之笔是:分布式系统设计,是 design for failure。
本文继续讨论一个服务进程什么时候应该用多线程,先说说单线程的优势。
***单线程程序的优势
从编程的角度,单线程程序的优势无需赘言:简单。程序的结构一般如《常用
模型》所言,是一个基于 IO multiplexing 的 event loop。或者如云风所言,
直接用阻塞 IO。
event loop 的典型代码框架是:
while (!done) {
int retval = ::poll(fds, nfds, timeout_ms);
if (retval < 0) {
处理错误
} else {
处理到期的 timers
if (retval > 0) {
处理 IO 事件
}
}
}
event loop 有一个明显的缺点,它是非抢占的(non-preemptive)。假设事件 a 的
优先级高于事件 b,处理事件 a 需要 1ms,处理事件 b 需要 10ms。如果事件 b
稍早于 a 发生,那么当事件 a 到来时,程序已经离开了 poll() 调用开始处理事
件 b。事件 a 要等上 10ms 才有机会被处理,总的响应时间为 11ms。这等于
发生了优先级反转。
这可缺点可以用多线程来克服,这也是多线程的主要优势。
***多线程程序有性能优势吗?
前面我说,无论是 IO bound 还是 CPU bound 的服务,多线程都没有什么绝对
意义上的性能优势。这里详细阐述一下这句话的意思。
这句话是说,如果用很少的 CPU 负载就能让的 IO 跑满,或者用很少的 IO 流量
就能让 CPU 跑满,那么多线程没啥用处。举例来说:
对于静态 web 服务器,或者 ftp 服务器,CPU 的负载较轻,主要瓶颈在磁盘 IO
和网络 IO。这时候往往一个单线程的程序(模式 1)就能撑满 IO。用多线程并
不能提高吞吐量,因为 IO 硬件容量已经饱和了。同理,这时增加 CPU 数目也
不能提高吞吐量。
CPU 跑满的情况比较少见,这里我只好虚构一个例子。假设有一个服务,它的
输入是 n 个整数,问能否从中选出 m 个整数,使其和为 0 (这里 n < 100, m > 0)。
这是著名的 subset sum 问题,是 NP-Complete 的。对于这样一个“服务”,哪怕
很小的 n 值也会让 CPU 算死,比如 n = 30,一次的输入不过 120 字节(32-bit
整数),CPU 的运算时间可能长达几分钟。对于这种应用,模式 3a 是最适合的,
能发挥多核的优势,程序也简单。
也就是说,无论任何一方早早地先到达瓶颈,多线程程序都没啥优势。
说到这里,可能已经有读者不耐烦了:你讲了这么多,都在说单线程的好处,
那么多线程究竟有什么用?
***适用多线程程序的场景
我认为多线程的适用场景是:提高响应速度,让 IO 和“计算”相互重叠,降低 latency。
虽然多线程不能提高绝对性能,但能提高平均响应性能。
一个程序要做成多线程的,大致要满足:
* 有多个 CPU 可用。单核机器上多线程的优势不明显。
* 线程间有共享数据。如果没有共享数据,用模型 3b 就行。虽然我们应该把线程间的共享数据降到最低,但不代表没有;
* 共享的数据是可以修改的,而不是静态的常量表。如果数据不能修改,那么可以在进程间用 shared memory,模式 3 就能胜任;
* 提供非均质的服务。即,事件的响应有优先级差异,我们可以用专门的线程来处理优先级高的事件。防止优先级反转;
* latency 和 throughput 同样重要,不是逻辑简单的 IO bound 或 CPU bound 程序;
* 利用异步操作。比如 logging。无论往磁盘写 log file,还是往 log server 发送消息都不应该阻塞
critical path;
* 能 scale up。一个好的多线程程序应该能享受增加 CPU 数目带来的好处,目前主流是 8 核,很快就会用到 16 核的机器了。
* 具有可预测的性能。随着负载增加,性能缓慢下降,超过某个临界点之后急速下降。线程数目一般不随负载变化。
* 多线程能有效地划分责任与功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是把所有逻辑都塞到一个 event loop 里,就
像 Win32 SDK 程序那样。
这些条件比较抽象,这里举一个具体的(虽然是虚构的)例子。
假设要管理一个 Linux 服务器机群,这个机群里有 8 个计算节点,1 个控制节点。
机器的配置都是一样的,双路四核 CPU,千兆网互联。现在需要编写一个简单的
机群管理软件(参考 LLNL 的 SLURM),这个软件由三个程序组成:
* 运行在控制节点上的 master,这个程序监视并控制整个机群的状态。
* 运在每个计算节点上的 slave,负责启动和终止 job,并监控本机的资源。
* 给最终用户的 client 命令行工具,用于提交 job。
根据前面的分析,slave 是个“看门狗进程”,它会启动别的 job 进程,因此必须是个单线程程序。另外它不应该占用太多的 CPU 资源,这也
适合单线程模型。
master 应该是个模式 2 的多线程程序:
* 它独占一台 8 核的机器,如果用模型 1,等于浪费了 87.5% 的 CPU 资源。
* 整个机群的状态应该能完全放在内存中,这些状态是共享且可变的。如果用模式 3,那么进程之间的状态同步会成大问题。而如果大量使用共享内存,
等于是掩耳盗铃,披着多进程外衣的多线程程序。
* master 的主要性能指标不是 throughput,而是 latency,即尽快地响应各种事件。它几乎不会出现把 IO 或
CPU 跑满的情况。
* master 监控的事件有优先级区别,一个程序正常运行结束和异常崩溃的处理优先级不同,计算节点的磁盘满了和机箱温度过高这两种报警条件的
优先级也不同。如果用单线程,可能会出现优先级反转。
* 假设 master 和每个 slave 之间用一个 TCP 连接,那么 master 采用 2 个或 4 个 IO 线程来处理 8
个 TCP connections 能有效地降低延迟。
* master 要异步的往本地硬盘写 log,这要求 logging library 有自己的 IO 线程。
* master 有可能要读写数据库,那么数据库连接这个第三方 library 可能有自己的线程,并回调 master 的代码。
* master 要服务于多个 clients,用多线程也能降低客户响应时间。也就是说它可以再用 2 个 IO 线程专门处理和
clients 的通信。
* master 还可以提供一个 monitor 接口,用来广播 (pushing) 机群的状态,这样用户不用主动轮询
(polling)。这个功能如果用单独的线程来做,会比较容易实现,不会搞乱其他主要功能。
* master 一共开了 10 个线程:
- 4 个用于和 slaves 通信的 IO 线程
- 1 个 logging 线程
- 1 个数据库 IO 线程
- 2 个和 clients 通信的 IO 线程
- 1 个主线程,用于做些背景工作,比如 job 调度
- 1 个 pushing 线程,用于主动广播机群的状态
* 虽然线程数目略多于 core 数目,但是这些线程很多时候都是空闲的,可以依赖 OS 的进程调度来保证可控的延迟。
综上所述,master 用多线程方式编写是自然且高效的。
***线程的分类
据我的经验,一个多线程服务程序中的线程大致可分为 3 类:
* IO 线程,这类线程的的主循环是 io multiplexing,等在 select/poll/epoll 系统调用上。这类线程也处理
定时事件。当然它的功能不止 IO,有些计算也可以放入其中。
* 计算线程,这类线程的主循环是 blocking queue,等在 condition variable 上。这类线程一般位于
thread pool 中。
* 第三方库所用的线程,比如 logging,又比如 database connection。
服务器程序一般不会频繁地启动和终止线程。甚至,在我写过的程序里,create thread 只在程序启动的时候调用,在服务运行期间是不调用
的。
在多核时代,多线程编程是不可避免的,“鸵鸟算法”不是办法。
On 2月28日, 下午11时18分, jigsaw <jig...@gmail.com> wrote:
> -_- 。。。。。。。。。。。。。这篇。。。blog?论文?随笔?whatever....
>
> 里面不严谨和错误的地方有点多。。。
>
> 就说最后一句吧,多核时代的解决方案不是多线程,而是并行。
>
> 其它的。。。真的很多。。。也许大部分是我错了吧,呵呵
>
> 2010/2/28 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
On 3月1日, 上午12时02分, jigsaw <jig...@gmail.com> wrote:
> 我先列两本书:
>
> UNIX(R) Network Programming Volume 1, Third Edition: The Sockets Networking
> API
>
> Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and
> Networked Objects
>
> 可以说这两本书已经把方方面面都说完了。而这两本书分别是03和00年出的。到现在差不多10年过去了,服务器端编程一直没能有新概念和新研究出现。
>
> 以上说的是服务器端编程。而至于如何更高效的利用多核,是另外一个话题。
> multithread是早期的解决方案,而FP的重新崛起则摒弃了multithread的概念。AMD/Intel作为通用CPU的霸主,投入的精力也早就 从multithread转向了parallel。
>
> 关于并行,我没什么认知,但是这方面的参考文献很多,MS剑桥研究院在这方面聚集了一些大牛,有兴趣的可以自己去搜索下。
> 从软件的角度来说,怎么避免锁,怎么让算法pipeline化才是现在的研究方向--而不是怎么深化multithread。
>
> 这篇文章,可以说跟这些被真正---大部分人所认可---的书和文献是有明显抵触之处的。
>
> multithread的整套API其实很简单,所以不是说因为mutex难用所以不用。
> 而是因为引入锁的概念后,整个软件的架构设计会到处出现对锁的依赖,而这种依赖不仅让架构难看,而且会对具体实现的算法带来额外的负担---而这些往往都是系统 性能的瓶颈,以及bug的根源。
>
> 干coder这个行当,要多接触最前沿的理论,哪怕还没法应用的理论,都会对开阔思路有很大帮助。因为这个行业很年轻,一份paper可能10年后就会成为工业 界的主流。
>
> 2010/2/28 jigsaw <jig...@gmail.com>
>
>
>
> > -_- 。。。。。。。。。。。。。这篇。。。blog?论文?随笔?whatever....
>
> > 里面不严谨和错误的地方有点多。。。
>
> > 就说最后一句吧,多核时代的解决方案不是多线程,而是并行。
>
> > 其它的。。。真的很多。。。也许大部分是我错了吧,呵呵
>
> > 2010/2/28 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
On Feb 28, 10:18 am, jigsaw <jig...@gmail.com> wrote:
> -_- 。。。。。。。。。。。。。这篇。。。blog?论文?随笔?whatever....
>
> 里面不严谨和错误的地方有点多。。。
>
> 就说最后一句吧,多核时代的解决方案不是多线程,而是并行。
>
> 其它的。。。真的很多。。。也许大部分是我错了吧,呵呵
>
> 2010/2/28 Shuo Chen <giantc...@gmail.com>
> ...
>
> read more >>
httpd 如何才能高效,这不是我工作中关心的话题,虽然我很有兴趣听听。
On 3月1日, 上午1时04分, jigsaw <jig...@gmail.com> wrote:
> ok
>
> 这个master,首先它不是一个典型的服务器端程序。因为无论是连接数还是流量都极小。一个典型的服务器端程序,比如你开始提到的httpd,又或者prox y,都是面对成千上万的并发连接,并且流量起码是百兆bps。
>
> 其次,据你的描述的方案,这个master开的线程大部分是辅助性质的,也就是说,不是对uplink的请求来服务的。这不属于我们讨论的范畴。我们讨论的范畴 是,通过多线程来提高服务器的性能--我们并不关心这个服务器是否有个CLI线程来响应terminal,或者有个线程去写数据库,或者有个线程来重新load 配置等等。
>
> 事实上,我说得多进程模式并不是说一个进程就绝对不能用到thread。如果有需要响应其它的逻辑(服务uplink请求之外的逻辑),当然需要开多个线程。
>
> master这个场景,按你的描述来看,瓶颈不会在跟slave的interface上,除非slave象load
> generator一样发大规模的数据来压master。因此关注的地方可能在--数据库是否要求实时,广播的密度,等。
>
> 如果让我来设计这个master,我会先评估系统的瓶颈在哪里。假设说系统瓶颈在数据库上,我会在master和数据库之间加一个queue,让master无 需考虑与数据库连接以及同步的问题,直接把数据丢到queue里面就即刻返回。那如果是这样的话,我会再建一个process--注意,这里并不需要是fork 出来的进程,因为pipe
> fd的可扩展性不如domain socket,而性能没有明显差距。
>
> 而如果唯一的关注点是slave的latency,那么这里是多进程还是多线程一点也不重要--除非引入更复杂的机制(下面会提到)。因为master-sla ve接口是唯一一个TCP连接,所以你要关注的不在于这个TCP连接是否够快,而是master怎样才能更快的处理完自己的逻辑,把消息返回给slave。
>
> 但是你可以走的更远。如果可以把slave和master通讯的逻辑切成几个无关的逻辑,则可以降低latency。假设,master要把计算任务发给sla ve,slave返回计算结果,master根据结果再分配新的任务(注意:这个逻辑是一个典型的同步过程,下面我们可以看怎么把这个任务给pipeline了 )。
> 那么就可以把一个TCP连接扩展成两个。
>
> 其中一个连接是master把任务分配过去。这个任务只需要slave返回ack,所以slave可以立即返回ack;第二个连接是slave把结果送给mas ter,master只需要返回ack,同理,master也可以迅速返回。
> 那么这里就需要在master和slave两方引入queue。master在发送了任务之后,就把这个任务从queue里面删除,当接受到slave的返回之 后,就通过可能一些很复杂的计算(意味着高latency)把新的任务加入到queue里。slave那里做同样的事情。
>
> 这样一来,在TCP连接上,双方都可以迅速对对方的请求做出反应,由在各自隐藏的queue来慢慢消化。
>
> 在实际中,TCP层的ack就可以用来作为事务逻辑的ack。但考虑到如果queue满了,或者其它资源不足,可能会返回其它事物层的信号,我们会要设计一个简 单的L7
> interface。 这方面有无数例子可以参考,多看看IETF即可。
>
> 在我描述的方案里,有一个明显的用到锁的地方。那就是queue。无论加入新节点还是删除新节点,都需要锁住整个queue。注意,这里的锁是逻辑意义上的,并 不对等mutex。但是这个锁对latency是没有影响的。所以我们可以看到,在这里例子里无论线程还是进程,对latency没有任何影响。
>
> 当然,在这个例子里,我会选用线程,因为进程之间的锁需要系统的支持,并且对queue的访问在多线程之间更方便。注意,这个选择不是因为latency,而是 从implementation细节上来考量的。
>
> 让我们回到一开始的话:这个master不是一个典型的服务器端。如果我们要讨论一个httpd怎样才能高效,这是一个不恰当的例子。
>
> 如果你想讨论httpd的latency和throughput,请另行讨论。
>
> 2010/2/28 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
在 2010年3月1日 下午3:23,Feng Yu <mryu...@gmail.com> 写道:
--
Regards.
Chen Ming
On Mar 1, 5:16 pm, jinhu wang <wangjinhu...@gmail.com> wrote:
> 关于单线程程序的优势我想补充一下我见过的一些东西。
> 因为只用简单来概括似乎还不够。
> 见过一个叫DC的协议软件包,通过select系列多路事件分离机制,实现一个操作系统之上的小操作系统,数据无锁,多个伪线程,通过限制代码逻辑片段的大小来保证优先级。
> 软件架构师们总能想办法把简单的东西复杂化:)
>
> 在 2010年2月28日 下午10:57,Shuo Chen <giantc...@gmail.com>写道:
>
> > 多线程服务器的适用场合
>
> > 陈硕 (giantchen_AT_gmail)
> > Blog.csdn.net/Solstice <http://blog.csdn.net/Solstice>
> ...
>
> read more >>
ACE为了包容尽可能多的东西,自然不可能符合每个人的口味的,
我也找过轮子,虽然可以更符合自己的应用场景,但是你要付出更多的时间去排错。
我认为在这些上面,ACE提供了更通用的解决方案,虽然在性能上付出一些代价(只是一些),
但能够提供可靠的健壮性还是值得。鱼和熊掌不能兼得嘛。
--
Regards.
Chen Ming
On 3月1日, 下午8时44分, chuang <lichuang1...@gmail.com> wrote:
> "以前的时候做哭过"
> 呃,我其实想确定一下这句话里面是不是有错别字....
>
> 2010/3/1 Feng Yu <mryuf...@gmail.com>
>
>
>
> > 我个人来讲 很久没做多线程的程序了 以前的时候做哭过. 如非必要,还是用简单的队列耦合来处理,就像现在流行的Erlang的进程队列的模式,简单有效!
>
> > 专注 高性能容错分布式服务器的研究和实现
> >http://blog.yufeng.info
>
> > 2010/3/1 Feng Yu <mryuf...@gmail.com>
>
> > 支持jigsaw关于ACE作为学习资源的观点. 毕竟网络的这些东西ACE都总结出来了,这点非常有价值.
>
> >> 专注 高性能容错分布式服务器的研究和实现
> >>http://blog.yufeng.info
>
> >> 2010/3/1 jigsaw <jig...@gmail.com>
>
> >> 性能瓶颈本来就是各不相同的 有硬盘IO 有网络延迟 有CPU 有memory bus 具体问题具体分析
> >>> 但是这个世界上跑的最快的服务器们估计没有一个是在ace上的
> >>> 本来就是个大家做轮子的时代 谁还稀罕绑定在ace上
> >>> 当然 话说回来 ace是个可以学习的不错的资源
>
> >>> 2010/3/1 ChenMing <mockey.c...@gmail.com>
>
> >>> 我想知道你是如何得出"ACE的原语太低了,用成高性能很困难!!!"这样的结论的,能分享你们对程序profile数据吗?
> >>>> 我也写过些基于ACE的服务器,并没有发现它是性能的瓶颈的,我遇到的性能瓶颈都是在IO上面。
>
> >>>> 在 2010年3月1日 下午3:23,Feng Yu <mryuf...@gmail.com> 写道:
> >>>> > ACE的源码我看了好几遍 写了几十万行电信应用 几万行库代码,最后觉得放弃不用,除非是移植和兼容的目的.
> >>>> ACE的原语太低了,用成高性能很困难!!!
>
> >>>> > 专注 高性能容错分布式服务器的研究和实现
> >>>> >http://blog.yufeng.info
>
> >>>> > 2010/3/1 Yuan Liu <xiaolang...@gmail.com>
>
> >>>> >> ACE是一个不错的工程库,但是学习代价比较大
>
> >>>> 最大的问题是,我觉得,文档缺失太严重了,不可否认可以通过他的测试用例来学习,但是太花费精力(谁有除了那3本纸质书籍和马维达电子书之外的教程么?)
> >>>> >> 代码也比较难以阅读,层次太深,风格还有我比较厌恶的 delete this
> >>>> >> 有用ACE比较得心应手的高手么?能否指教一下使用心得?
>
> >>>> >> 2010/3/1 Feng Yu <mryuf...@gmail.com>
>
> >>>> >>> ACE的那几本书和库本身就这个问题进行了很好的归纳总结的...
>
> >>>> >>> 专注 高性能容错分布式服务器的研究和实现
> >>>> >>>http://blog.yufeng.info
>
> >>>> >>> 2010/2/28 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
先插句玩笑话:
你说我做的东西根本算不上"网络服务器",我赶紧检讨自己,在最近写的多线程
和服务端的四篇文章中:
《当析构函数遇到多线程──C++ 中线程安全的对象回调》
http://blog.csdn.net/Solstice/archive/2010/01/22/5238671.aspx
《多线程服务器的常用编程模型》
http://blog.csdn.net/Solstice/archive/2010/02/12/5307710.aspx
《Linux 新增系统调用的启示》
http://blog.csdn.net/Solstice/archive/2010/02/26/5327881.aspx
《多线程服务器的适用场合》
http://blog.csdn.net/Solstice/archive/2010/02/28/5334243.aspx
我从来没有用过"网络服务器"这个术语,用的是"多线程服务器"或者"网络应用
程序",你说我做的不是"网络服务器",可我也没说它是,不是吗?
说正经的。
一开始我就意识到服务端开发包罗万象,因此第一段就定义了范畴:本文所指的
"服务器开发"的含义请见《常用模型》一文, 一句话形容是:跑在多核机器上
的 Linux 用户态的没有用户界面的长期运行的网络应用程序。而我在《常用模型》
一文的头两段也把范围限定得很小:"文中的'多线程服务器'是指运行在 Linux
操作系统上的独占式网络应用程序。硬件平台为 Intel x64 系列的多核 CPU,
单路或双路 SMP 服务器(每台机器一共拥有四个核或八个核,十几 GB 内存),
机器之间用百兆或千兆以太网连接。这大概是目前民用 PC 服务器的主流配置。"
"本文不涉及 Windows 系统,不涉及人机交互界面(无论命令行或图形);
不考虑文件读写(往磁盘写 log 除外),不考虑数据库操作,不考虑 Web 应用;
不考虑低端的单核主机或嵌入式系统,不考虑手持式设备,不考虑专门的网络设备,
不考虑高端的 >=32 核 Unix 主机;只考虑 TCP,不考虑 UDP,也不考虑除了
局域网络之外的其他数据收发方式(例如串并口、USB口、数据采集板卡、实时控制等)。"
设了这么多限制条件,我就差说我只讨论低延迟服务器了。
按照你的定义,似乎要瓶颈要跟网络有关的软硬件才算"网络服务器",那么说来
只有网络基础设施,比如骨干网交换机,路由器,防火墙,网关才算"网络服务器"啰?
常用的网络服务程序是不是"网络服务器"呢?比如 HTTPD, FTPD, NFS, NTP, NIS,
DNS, proxy 等等,这些东西有的瓶颈跟网络有关,有的则无关(比如 NTP,远在网络
达到瓶颈之前,其性能已经下降到不可用了。)
在你那里,似乎只有高并发高吞吐大流量的才能配叫"网络服务器",你觉得像我做的
那些低延迟的东西(比如 master)应该起个什么名字才不侵犯你的地盘呢?
如果叫"网络服务器"就是挂羊头卖狗肉的话。
我原文说的模式 1、2、3、4,不是 pattern,而是 model,算我表达不严谨好了,
用"方式"可能更准确些。
关于多线程的编程模型被鄙视,这个我是有切身体会的,可能跟你的认知不符。
比如下面这些人都提倡单线程多进程模型(排名不分先后):
参与本贴的余锋先生、网易的云风先生、银杏的霍炬先生。
前面二人可以用"名字 服务器设计 线程"在 google 上找到他们的观点,霍炬是私人聊天。
从现有的流行的网络服务器程序来看,也是单线程居多,比如 lighttpd, nginx 等等。
而除了我们公司的同事,我几乎找不到和我持同样观点的人。
你的意思是,10 年前就已经充分讨论的东西,现在不能再拿出来说了?10 年前
多核CPU还远未普及呢。你这话听上去的意思是:如果观点和 W. Richard Stevens
和 Douglas Schmidt 一样,那就不用说了,反正说出来也是废话;如果观点和他们
不一样,也不用说了,反正说出来也是错的。大家抱着 UNP 和 POSA 几本书,
如法炮制,万事大吉?
你认为我最近写的多线程和服务器端的文章会误导初学者,ok,你有你的观点,虽然
我不觉得自己是在误导初学者。
你可以写 blog、写文章,无论批驳我也好,无论正面立论来"正导"初学者也好,
这些我都没有意见,我还能学习新知识。
但是,因为我的观念和你不同你就让我改口,这个我不能接受。我也不打算修改那几篇文章。
技术本来就是有分歧的,我提倡在 C++ 里不使用异常,恐怕有人会说我误导初学者;
我提倡用智能指针来管理资源,肯定有人会说我误导初学者。这顶帽子一扣下了,干脆
技术讨论都不要做了,大家为了安全起见,一律照本宣科,引经据典,每句话都要从经
典著作上找得到出处......
我这几篇文章提到的内容都是从工作中提炼的,经过简化和模糊化,将来我还打算写更多
的文章,继续讲我天天面对的"非典型"网络服务器(好吧,请允许我用一次你的术语)。
"非典型"不代表不能说,对吧?典型的10年前都已经说透说烂了,现在讲点"非典型"
的,有什么不可以的呢?
你可以分享你开发主流网络服务器的经验,我也可以讲我开发"非主流"网络应用程序的经验,
少数派也有发言权嘛。
On 3月1日, 下午4时30分, jigsaw <jig...@gmail.com> wrote:
> 这个master根本就算不上是个网络服务器。因为它的瓶颈根本就跟网络无关--如果我之前的假设是正确的话。
> 当然你可以说我错了,因为你对master的描述太少了,我必须做很多假设才能继续讨论。
>
> 你在一开头列举的所谓4种模式,根本谈不上是模式。就好比,你能说构造函数是一种模式吗?至少你得说工厂是一种模式吧。
>
> 再有,你对所谓四种模式的所谓定论也有谬误。所谓"目前公认","很多人鄙视",其实跟事实不符---至少跟我的认知不符。
>
> 有些技术上的讨论,因为出发点不同,考虑的角度不同,考量的重点不同,或者因为目前业界技术还没到一个高度,会出现两边都正确,或者很难说谁对谁错的场景。但是 你现在提出的这个话题,属于10年前就已经有充分的讨论和理论成册出版,这10年中已经有很多成功的实现来佐证,因此不属于我刚才说的范畴。
>
> 你可以说你提的场景是如此的罕见、非主流,但是你却把它放到网络服务器的范畴里来讨论,不客气的说,这属于挂羊头卖狗肉。
>
> 我的话是尖锐了点。但是我之所这么做,是因为我发现原来你近期颇写了一些关于多线程和服务器端的文章,里面的观点我认为对初学者会产生误导,因此在此提请你能做 出适当的修改。
>
> 2010/3/1 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
> ...
>
> read more >>
> ...
>
> read more >>
《多线程服务器的适用场合》(以下简称《适用场合》)一文在博客登出之后,有热心读者提出质疑,我自己也觉得原文没有把道理说通说透,这篇文章试图用一
些实例来解答读者的疑问。我本来打算修改原文,但是考虑到已经读过的读者不一定会注意到文章的变动,干脆另写一篇。为方便阅读,本文以问答体呈现。这篇
文章可能会反复修改扩充,请注意上面的版本号。
本文所说的“多线程服务器”的定义与前文一样,同时参见《多线程服务器的常用编程模型》(以下简称《常用模型》)一文的详细界定,以下“连接、端口”均
指 TCP 协议。
1. Linux 能同时启动多少个线程?
对于 32-bit Linux,一个进程的地址空间是 4G,其中用户态能访问 3G 左右,而一个线程的默认栈 (stack) 大小是 10M,
心算可知,一个进程大约最多能同时启动 300 个线程。如果不改线程的调用栈大小的话,300 左右是上限,因为程序的其他部分(数据段、代码段、
堆、动态库、等等)同样要占用内存(地址空间)。
对于 64-bit 系统,线程数目可大大增加,具体数字我没有测试,因为我实际用不到那么多线程。
以下的关于线程数目的讨论以 32-bit Linux 为例。
2. 多线程能提高并发度吗?
如果指的是“并发连接数”,不能。
由问题 1 可知,假如单纯采用 thread per connection 的模型,那么并发连接数最多 300,这远远低于基于事件的单线程程序
所能轻松达到的并发连接数(几千上万,甚至几万)。所谓“基于事件”,指的是用 IO multiplexing event loop 的编程模型,
又称 Reactor 模式,在《常用模型》一文中已有介绍。
那么采用《常用模型》一文中推荐的 event loop per thread 呢?至少不逊于单线程程序。
小结:thread per connection 不适合高并发场合,其 scalability 不佳。event loop per
thread 的并发度不比单线程程序差。
3. 多线程能提高吞吐量吗?
对于计算密集型服务,不能。
假设有一个耗时的计算服务,用单线程算需要 0.8s。在一台 8 核的机器上,我们可以启动 8 个线程一起对外服务(如果内存够用,启动 8 个进
程也一样)。这样完成单个计算仍然要 0.8s,但是由于这些进程的计算可以同时进行,理想情况下吞吐量可以从单线程的 1.25cps (calc
per second) 上升到 10cps。(实际情况可能要打个八折——如果不是打对折的话。)
假如改用并行算法,用 8 个核一起算,理论上如果完全并行,加速比高达 8,那么计算时间是 0.1s,吞吐量还是 10cps,但是首次请求的响应
时间却降低了很多。实际上根据 Amdahl's law,即便算法的并行度高达 95%,8 核的加速比也只有 6,计算时间为 0.133s,这样
会造成吞吐量下降为 7.5cps。不过以此为代价,换得响应时间的提升,在有些应用场合也是值得的。
这也回答了问题 4。
如果用 thread per request 的模型,每个客户请求用一个线程去处理,那么当并发请求数大于某个临界值 T’ 时,吞吐量反而会下
降,因为线程多了以后上下文切换的开销也随之增加(分析与数据请见《A Design Framework for Highly
Concurrent Systems》 by Matt Welsh et al.)。thread per request 是最简单的使用线程的
方式,编程最容易,简单地把多线程程序当成一堆串行程序,用同步的方式顺序编程,比如 Java Servlet 中,一次页面请求由一个函数
HttpServlet#service(HttpServletRequest req, HttpServletResponse resp) 同
步地完成。
为了在并发请求数很高时也能保持稳定的吞吐量,我们可以用线程池,线程池的大小应该满足“阻抗匹配原则”,见问题 7。
线程池也不是万能的,如果响应一次请求需要做比较多的计算(比如计算的时间占整个 response time 的 1/5 强),那么用线程池是合理
的,能简化编程。如果一次请求响应中,thread 主要是在等待 IO,那么为了进一步提高吞吐,往往要用其它编程模型,比如 Proactor,见
问题 8。
4. 多线程能降低响应时间吗?
如果设计合理,充分利用多核资源的话,可以。在突发 (burst) 请求时效果尤为明显。
例1: 多线程处理输入。
以 memcached 服务端为例。memcached 一次请求响应大概可以分为 3 步:
1.读取并解析客户端输入
2.操作 hashtable
3.返回客户端
在单线程模式下,这 3 步是串行执行的。在启用多线程模式时,它会启用多个输入线程(默认是 4 个),并在建立连接时按 round-robin
法把新连接分派给其中一个输入线程,这正好是我说的 event loop per thread 模型。这样一来,第 1 步的操作就能多线程并行,
在多核机器上提高多用户的响应速度。第 2 步用了全局锁,还是单线程的,这可算是一个值得继续改进的地方。
比如,有两个用户同时发出了请求,这两个用户的连接正好分配在两个 IO 线程上,那么两个请求的第 1 步操作可以在两个线程上并行执行,然后汇总到
第 2 步串行执行,这样总的响应时间比完全串行执行要短一些(在“读取并解析”所占的比重较大的时候,效果更为明显)。请继续看下面这个例子。
例2: 多线程分担负载。
假设我们要做一个求解 Sudoku 的服务(见《谈谈数独》),这个服务程序在 9981 端口接受请求,输入为一行 81 个数字(待填数字用
0 表示),输出为填好之后的 81 个数字 (1 ~ 9),如果无解,输出 “NO\r\n”。
由于输入格式很简单,用单个线程做 IO 就行了。先假设每次求解的计算用时 10ms,用前面的方法计算,单线程程序能达到的吞吐量上限为
100req/s,在 8 核机器上,如果用线程池来做计算,能达到的吞吐量上限为 800req/s。下面我们看看多线程如何降低响应时间。
假设 1 个用户在极短的时间内发出了 10 个请求,如果用单线程“来一个处理一个”的模型,这些 reqs 会排在队列里依次处理(这个队列是操作
系统的 TCP 缓冲区,不是程序里自己的任务队列)。在不考虑网络延迟的情况下,第 1 个请求的响应时间是 10ms;第 2 个请求要等第 1
个算完了才能获得 CPU 资源,它等了 10ms,算了 10ms,响应时间是 20ms;依次类推,第 10 个请求的响应时间为 100ms;
10个请求的平均响应时间为 55ms。
如果 Sudoku 服务在每个请求到达时开始计时,会发现每个请求都是 10ms 响应时间,而从用户的观点,10 个请求的平均响应时间为
55ms,请读者想想为什么会有这个差异。
下面改用多线程:1 个 IO 线程,8 个计算线程(线程池)。二者之间用 BlockingQueue 沟通。同样是 10 个并发请求,第 1
个请求被分配到计算线程1,第 2 个请求被分配到计算线程 2,以此类推,直到第 8 个请求被第 8 个计算线程承担。第 9 和第 10 号请求
会等在 BlockingQueue 里,直到有计算线程回到空闲状态才能被处理。(请注意,这里的分配实际上是由操作系统来做,操作系统会从处于
Waiting 状态的线程里挑一个,不一定是 round-robin 的。)
这样一来,前 8 个请求的响应时间差不多都是 10ms,后 2 个请求属于第二批,其响应时间大约会是 20ms,总的平均响应时间是 12ms。
可以看出比单线程快了不少。
由于每道 Sudoku 题目的难度不一,对于简单的题目,可能 1ms 就能算出来,复杂的题目最多用 10ms。那么线程池方案的优势就更明显,它
能有效地降低“简单任务被复杂任务压住”的出现概率。
以上举的都是计算密集的例子,即线程在响应一次请求时不会等待 IO,下面谈谈更复杂的情况。
5. 多线程程序如何让 IO 和“计算”相互重叠,降低 latency?
基本思路是,把 IO 操作(通常是写操作)通过 BlockingQueue 交给别的线程去做,自己不必等待。
例1: logging
在多线程服务器程序中,日志 (logging) 至关重要,本例仅考虑写 log file 的情况,不考虑 log server。
在一次请求响应中,可能要写多条日志消息,而如果用同步的方式写文件(fprintf 或 fwrite),多半会降低性能,因为:
* 文件操作一般比较慢,服务线程会等在 IO 上,让 CPU 闲置,增加响应时间。
* 就算有 buffer,还是不灵。多个线程一起写,为了不至于把 buffer 写错乱,往往要加锁。这会让服务线程互相等待,降低并发度。(同时
用多个 log 文件不是办法,除非你有多个磁盘,且保证 log files 分散在不同的磁盘上,否则还是受到磁盘 IO 瓶颈制约。)
解决办法是单独用一个 logging 线程,负责写磁盘文件,通过一个或多个 BlockingQueue 对外提供接口。别的线程要写日志的时候,
先把消息(字符串)准备好,然后往 queue 里一塞就行,基本不用等待。这样服务线程的计算就和 logging 线程的磁盘 IO 相互重叠,降
低了服务线程的响应时间。
尽管 logging 很重要,但它不是程序的主要逻辑,因此对程序的结构影响越小越好,最好能简单到如同一条 printf 语句,且不用担心其他性
能开销,而一个好的多线程异步 logging 库能帮我们做到这一点。(Apache 的 log4cxx 和 log4j 都支持
AsyncAppender 这种异步 logging 方式。)
例2: memcached 客户端
假设我们用 memcached 来保存用户最后发帖的时间,那么每次响应用户发帖的请求时,程序里要去设置一下 memcached 里的值。这一步
如果用同步 IO,会增加延迟。
对于“设置一个值”这样的 write-only idempotent 操作,我们其实不用等 memcached 返回操作结果,这里也不用在乎
set 操作失败,那么可以借助多线程来降低响应延迟。比方说我们可以写一个多线程版的 memcached 的客户端,对于 set 操作,调用方只
要把 key 和 value 准备好,调用一下 asyncSet() 函数,把数据往 BlockingQueue 上一放就能立即返回,延迟很
小。剩下的时就留给 memcached 客户端的线程去操心,而服务线程不受阻碍。
其实所有的网络写操作都可以这么异步地做,不过这也有一个缺点,那就是每次 asyncWrite 都要在线程间传递数据,其实如果 TCP 缓冲区是
空的,我们可以在本线程写完,不用劳烦专门的 IO 线程。Jboss 的 Netty 就使用了这个办法来进一步降低延迟。
以上都仅讨论了“打一枪就跑”的情况,如果是一问一答,比如从 memcached 取一个值,那么“重叠 IO”并不能降低响应时间,因为你无论如何
要等 memcached 的回复。这时我们可以用别的方式来提高并发度,见问题8。(虽然不能降低响应时间,但也不要浪费线程在空等上,对吧)
另外以上的例子也说明,BlockingQueue 是构建多线程程序的利器。
6. 为什么第三方库往往要用自己的线程?
往往因为 event loop 模型没有标准实现。如果自己写代码,尽可以按所用 Reactor 的推荐方式来编程,但是第三方库不一定能很好地适
应并融入这个 event loop framework。有时需要用线程来做一些串并转换。
对于 Java,这个问题还好办一些,因为 thread pool 在 Java 里有标准实现,叫 ExecutorService。如果第三方库
支持线程池,那么它可以和主程序共享一个 ExecutorService ,而不是自己创建一堆线程。(比如在初始化时传入主程序的 obj。)对
于 C++,情况麻烦得多,Reactor 和 Thread pool 都没有标准库。
例1:libmemcached 只支持同步操作
libmemcached 支持所谓的“非阻塞操作”,但没有暴露一个能被 select/poll/epoll 的 file describer,
它的 memcached_fetch 始终会阻塞。它号称 memcached_set 可以是非阻塞的,实际意思是不必等待结果返回,但实际上这个
函数会同步地调用 write(),仍可能阻塞在网络 IO 上。
如果在我们的 reactor event handler 里调用了 libmemcached 的函数,那么 latency 就堪忧了。如果想继
续用 libmemcached,我们可以为它做一次线程封装,按问题 5 例 2 的办法,同额外的线程专门做 memcached 的 IO,而程
序主体还是 reactor。甚至可以把 memcached “数据就绪”作为一个 event,注入到我们的 event loop 中,以进一步
提高并发度。(例子留待问题 8 讲)
万幸的是,memcached 的协议非常简单,大不了可以自己写一个基于 reactor 的客户端,但是数据库客户端就没那么幸运了。
例2:MySQL 的官方 C API 不支持异步操作
MySQL 的客户端只支持同步操作,对于 UPDATE/INSERT/DELETE 之类只要行为不管结果的操作(如果代码需要得知其执行结果则另
当别论),我们可以用一个单独的线程来做,以降低服务线程的延迟。可仿照前面 memcached_set 的例子,不再赘言。麻烦的是
SELECT,如果要把它也异步化,就得动用更复杂的模式了,见问题 8。
相比之下,PostgreSQL 的 C 客户端 libpq 的设计要好得多,我们可以用 PQsendQuery() 来发起一次查询,然后用标准
的 select/poll/epoll 来等待 PQsocket,如果有数据可读,那么用 PQconsumeInput 处理之,并用
PQisBusy 判断查询结果是否已就绪,最后用 PQgetResult 来获取结果。借助这套异步 API,我们可以很容易地为 libpq 写
一套 wrapper,使之融入到程序所用的 reactor 模型中。
7. 什么是线程池大小的阻抗匹配原则?
我在《常用模型》中提到“阻抗匹配原则”,这里大致讲一讲。
如果池中线程在执行任务时,密集计算所占的时间比重为 P (0 < P <= 1),而系统一共有 C 个 CPU,为了让这 C 个 CPU 跑满
而又不过载,线程池大小的经验公式 T = C/P。(T 是个 hint,考虑到 P 值的估计不是很准确,T 的最佳值可以上下浮动 50%。)
以后我再讲这个经验公式是怎么来的,先验证边界条件的正确性。
假设 C = 8, P = 1.0,线程池的任务完全是密集计算,那么 T = 8。只要 8 个活动线程就能让 8 个 CPU 饱和,再多也没
用,因为 CPU 资源已经耗光了。
假设 C = 8, P = 0.5,线程池的任务有一半是计算,有一半等在 IO 上,那么 T = 16。考虑操作系统能灵活合理地调度
sleeping/writing/running 线程,那么大概 16 个“50% 繁忙的线程”能让 8 个 CPU 忙个不停。启动更多的线程
并不能提高吞吐量,反而因为增加上下文切换的开销而降低性能。
如果 P < 0.2,这个公式就不适用了,T 可以取一个固定值,比如 5*C。
另外,公式里的 C 不一定是 CPU 总数,可以是“分配给这项任务的 CPU 数目”,比如在 8 核机器上分出 4 个核来做一项任务,那么
C=4。
8. 除了你推荐的 reactor + thread poll,还有别的 non-trivial 多线程编程模型吗?
有,Proactor。
如果一次请求响应中要和别的进程打多次交道,那么 proactor 模型往往能做到更高的并发度。当然,代价是代码变得支离破碎,难以理解。
这里举 http proxy 为例,一次 http proxy 的请求如果没有命中本地 cache,那么它多半会:
1.解析域名 (不要小看这一步,对于一个陌生的域名,解析可能要花半秒钟)
2.建立连接
3.发送 HTTP 请求
4.等待对方回应
5.把结果返回客户
这 5 步里边跟 2 个 server 发生了 3 次 round-trip:
1.向 DNS 问域名,等待回复;
2.向对方 http 服务器发起连接,等待 TCP 三路握手完成;
3.向对方发送 http request,等待对方 response。
而实际上 http proxy 本身的运算量不大,如果用线程池,池中线程的数目会很庞大,不利于操作系统管理调度。
这时我们有两个解决思路:
1.把“域名已解析”,“连接已建立”,“对方已完成响应”做成 event,继续按照 Reactor 的方式来编程。这样一来,每次客户请求就不能
用一个函数从头到尾执行完成,而要分成多个阶段,2并且要管理好请求的状态(“目前到了第几步?”)。
2.用回调函数,让系统来把任务串起来。比如收到用户请求,如果没有命中本地 cache,立刻发起异步的 DNS 解析
startDNSResolve(),告诉系统在解析完之后调用 DNSResolved() 函数;在 DNSResolved() 中,发起连接,
告诉系统在连接建立之后调用 connectionEstablished();在 connectionEstablished() 中发送
http request,告诉系统在收到响应之后调用 httpResponsed();最后,在 httpResponsed() 里把结果返回给
客户。.NET 大量采用的 Begin/End 操作也是这个编程模式。当然,对于不熟悉这种编程方式的人,代码会显得很难看。
Proactor 模式的例子可看 boost::asio 的文档,这里不再多说。
Proactor 模式依赖操作系统或库来高效地调度这些子任务,每个子任务都不会阻塞,因此能用比较少的线程达到很高的 IO 并发度。
Proactor 能提高吞吐,但不能降低延迟,所以我没有深入研究。
9. 模式 2 和模式 3a 该如何取舍?
这里的“模式”不是 pattern,而是 model,不巧它们的中译是一样的。《适用场合》中提到,模式 2 是一个多线程的进程,模式 3a 是
多个相同的单线程进程。
我认为,在其他条件相同的情况下,可以根据工作集 (work set) 的大小来取舍。工作集是指服务程序响应一次请求所访问的内存大小。
如果工作集较大,那么就用多线程,避免 CPU cache 换入换出,影响性能;否则,就用单线程多进程,享受单线程编程的便利。
例如,memcached 这个内存消耗大户用多线程服务端就比在同一台机器上运行多个 memcached instance 要好。(除非你在
16G 内存的机器上运行 32-bit memcached,那么多 instance 是必须的。)
又例如,求解 Sudoku 用不了多大内存,如果单线程编程更方便的话,可以用单线程多进程来做。再在前面加一个单线程的 load
balancer,仿 lighttpd + fastcgi 的成例。
线程不能减少工作量,即不能减少 CPU 时间。如果解决一个问题需要执行一亿条指令(这个数字不大,不要被吓到),那么用多线程只会让这个数字增加。
但是通过合理调配这一亿条指令在多个核上的执行情况,我们能让工期提早结束。这听上去像统筹方法,确实也正是统筹方法。
64位OS线程数的限制问题已经解决了,64位系统的用户地址空间和内核地址空间在目前的软硬件发展水平上,在x86
64位系统都可以认为是无限制(AMD64以TB计)的,所以基本取决于物理内存大小
下面的文字来自SUN的《多线程编程指南》仅供参考:
“在 64 位环境中,进程的虚拟地址空间最高可达 64 位(即 18 EB)。目前,32 位进程的最大地址空间为 4
GB,较大的虚拟地址空间大约是其 40 亿倍。但是由于硬件限制,某些平台可能并不支持完整的 64 位地址空间。
大地址空间增加了可创建的具有缺省栈大小的线程数。在 32 位和 64 位系统中,栈的大小分别为 1 MB 和 2 MB。 在 32 位和
64 位系统中,具有缺省栈大小的线程数分别是大约 2000 个和 80000 亿个。 ”
--
Cheers,
Oliver Yang
Twitter: http://twitter.com/yangoliver
Blog: http://blog.csdn.net/yayong
--------------------------------------------------------------------
An OpenSolaris Developer
>>>我认为,在其他条件相同的情况下,可以根据工作集 (work set) 的大小来取舍。工作集是指服务程序响应一次请求所访问的内存大小。
>>>如果工作集较大,那么就用多线程,避免 CPU cache 换入换出,影响性能;否则,就用单线程多进程,享受单线程编程的便利。
> 这里未免托大了。首先,cpu的cache-line-size是千差万别的。当然你可以通过几行宏来缓解这个问题。
> 其次,如果要考虑到cache-line protection的话,你要考虑的就不是"模式2"还是"模式3a"了,而是要考虑你的每行源代码怎么写;
> 要考虑在哪几个fields之间加上cache-line padding,要考虑对每个field的每次访问是否会导致penalty。
说得对。
cache的问题的确是要数据结构和代码上都要小心的,多线程更要小心。
例如,数据结构的成员如果被多个线程访问,那可能需要小心的padding或者直接让成员cache line对齐,让他们在不同cache line上。
代码上,尽量利用局部性原理来提高cache的命中程度。
> 就到这里吧。我也不想做个职业挑刺运动员,因为我没那么多精力也没那么高水平。
> 之所以近几天来猛挑lz的刺,我估计大致是源于一股惯性。
> 向lz道一声抱歉,我有些话可能是狠了点;另外也向lz道声byebye,以后我不会再评论你的文章了。
技术讨论要忽略掉那些情绪上的东西才对 :)
他说的是数据结构上的调整。 通过padding可以让结构的成员处于不同的cache line,这样就不会在多线程共同访问时引发cache的颠簸。
int main()
{
size_t stacksize;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_getstacksize (&attr, &stacksize);
printf("Default stack size = %d\n", stacksize);
}
在我的机器上结果是 10M,别的机器我见过 8M 的。
NPTL 文档说默认值是 ulimit -s ,如果其不为 unlimited 的话,你的 ulimis -s 是多少?。
libmemcached 的 write() 是不是阻塞,你看了源码就知道,它有固定大小的输出缓冲,每次缓冲区满了就得 flush。我还不至于
不知道 non-blocking IO。
以太网的 Duplex model 是"双工模式"还是"双工模型"?
> 2010/3/3 Shuo Chen <giantc...@gmail.com>
>
>
>
> > 例释与答疑。
>
> > 《多线程服务器的适用场合》(以下简称《适用场合》)一文在博客登出之后,有热心读者提出质疑,我自己也觉得原文没有把道理说通说透,这篇文章试图用一
> > 些实例来解答读者的疑问。我本来打算修改原文,但是考虑到已经读过的读者不一定会注意到文章的变动,干脆另写一篇。为方便阅读,本文以问答体呈现。这篇
> > 文章可能会反复修改扩充,请注意上面的版本号。
>
> > 本文所说的"多线程服务器"的定义与前文一样,同时参见《多线程服务器的常用编程模型》(以下简称《常用模型》)一文的详细界定,以下"连接、端口"均
> > 指 TCP 协议。
>
> > 1. Linux 能同时启动多少个线程?
>
> > 对于 32-bit Linux,一个进程的地址空间是 4G,其中用户态能访问 3G 左右,而一个线程的默认栈 (stack) 大小是 10M,
> > 心算可知,一个进程大约最多能同时启动 300 个线程。如果不改线程的调用栈大小的话,300 左右是上限,因为程序的其他部分(数据段、代码段、
> > 堆、动态库、等等)同样要占用内存(地址空间)。
>
> > 对于 64-bit 系统,线程数目可大大增加,具体数字我没有测试,因为我实际用不到那么多线程。
>
> > 以下的关于线程数目的讨论以 32-bit Linux 为例。
>
> > 2. 多线程能提高并发度吗?
>
> > 如果指的是"并发连接数",不能。
>
> > 由问题 1 可知,假如单纯采用 thread per connection 的模型,那么并发连接数最多 300,这远远低于基于事件的单线程程序
> > 所能轻松达到的并发连接数(几千上万,甚至几万)。所谓"基于事件",指的是用 IO multiplexing event loop 的编程模型,
> > 又称 Reactor 模式,在《常用模型》一文中已有介绍。
>
> > 那么采用《常用模型》一文中推荐的 event loop per thread 呢?至少不逊于单线程程序。
>
> > 小结:thread per connection 不适合高并发场合,其 scalability 不佳。event loop per
> > thread 的并发度不比单线程程序差。
>
> > 3. 多线程能提高吞吐量吗?
>
> > 对于计算密集型服务,不能。
>
> > 假设有一个耗时的计算服务,用单线程算需要 0.8s。在一台 8 核的机器上,我们可以启动 8 个线程一起对外服务(如果内存够用,启动 8 个进
> > 程也一样)。这样完成单个计算仍然要 0.8s,但是由于这些进程的计算可以同时进行,理想情况下吞吐量可以从单线程的 1.25cps (calc
> > per second) 上升到 10cps。(实际情况可能要打个八折----如果不是打对折的话。)
> > 0 表示),输出为填好之后的 81 个数字 (1 ~ 9),如果无解,输出 "NO\r\n"。
>
> ...
>
> 阅读更多 >>
例如, "高性能" 的理解, 可能就有下面一些:
1. 高network throughoutput
例如少量连接情况下的 samba, 少量中量连接情况下静态页面 web server
2. low latency
例如以及 network filter 之类的
3. masssive connection 下有可用性的系统
一些 web 应用的前端
4. real time
例如您提到的 master, 最劣时间有保证的 low latency
5. high tps
交易系统, 网游逻辑服务器之类的东西
......
设计优化要点都不一样, 如果参与者心中各自的靶子不一样, 讨论的结果恐怕不会很好.
我说 libmemcached 的 write() 会阻塞,就会有人怀疑我不知道 non-blocking IO。
我说 Linux 的 stack size 默认是 10M,就会有人怀疑我不懂怎么设置 stack size,并用 Win32 的 1M 来反
驳我。
下面这几个页面都列出 10M 这个默认值,足以证明我的机器不是个例:
http://stackoverflow.com/questions/2279052/increase-stack-size-in-linux-with-setrlimit
http://rcsg.rice.edu/rcsg/shared/ulimit.html
http://www.akadia.com/services/ora_enable_core.html
要是下次我说 Linux 一个进程默认最多能打开 1024 个文件描述符,是不是又会有人认为我不知道怎么增大这个上限呢?
On 3月4日, 下午5时07分, Lu JunZhu <akins...@gmail.com> wrote:
> win32堆栈默认是1m吧,而且可以自己设置。楼主认为是10m的信心从何而来,就是拿一个小程序就拍胸脯证明了,写操作系统人员的水平和智商要多低才能把默 认设成10m?就算默认是10m,也没必要大惊小怪阿,你都调用了pthread_attr_getstacksize,应该会看到还有pthread_att r_SETstacksize吧,又不是不能改,为啥就限制你多开线程了。
> 我觉得楼主的语言逻辑要加强一下,因为所以不搭边会让看的人很郁闷。
> 这儿提到相关如何设置栈大小http://hi.baidu.com/xnshasha/blog/item/807007cb9e4a014cf31fe775.html
>
> 在 2010年3月4日 下午2:17,chuang <lichuang1...@gmail.com> 写道:
>
>
>
> > 呃....chenshuo说的那个是系统级别的线程的数据吧,erlang只是语言层面抽象出来的概念,不是一个东西吧?
>
> > 2010/3/4 Feng Yu <mryuf...@gmail.com>
>
> >> 这个数据可以随便该的 erlang的默认很小 才64K好像
>
> >> 专注 高性能容错分布式服务器的研究和实现
> >>http://blog.yufeng.info
>
> >> 2010/3/4 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
其实对于Parallel的研究,已经趋向于对于某些问题,抽象地讨论如何同步,比如说生产者消费者模型(wiki上对这种模型的三种代码我看了是觉得
受益很多);而对Parallel的另一个方向,则是对大规模的Parallel的研究,比如说,当你的计算单元数量已经达到几百,几千(类似
GPU),如何充分利用它们。
对于服务器程序来说,重要的除了这些模型,还有就是细节。lighty和nginx性能领先apache,除了基本的模式不同之外,细节非常重要。而
nignx领先于lighty,则几乎完全是细节处理上的问题(当然也可以说是算法问题)。另外,对于并行程序来说,保证算法实现的正确性,会花费相当
大的一块时间。
-- Thanks & Regards Linux Developer: Devil Wang <wxje...@gmail.com> |
On 3月4日, 下午5时48分, Changsheng Jiang <jiangzuo...@gmail.com> wrote:
> 这个大小限制了线程数不能太多. 实际上, 程序里如果没有错误的递归, 一般用不了这么多, 64位机器没有内存地址空间的问题, 线程数可以大很多.
> 但这个值也不能改的太小, 小了, 较深递归的程序, 栈上分配临时空间的程序都会段错误.
>
> 多的是, 我们要的不是线程, 只是要告诉状态机, 这个任务可以跑了. Go语言在这方面的抽象不错. 这些东西真是语言上做容易, 库上做很难的.
>
> Erlang 等脚本语言和 C 不同. 可以很方便的在脚本语言的实现里处理这个栈增长, 但要求 C 程序员处理栈大小并增大限制不太现实吧.
>
> Changsheng Jiang
>
> 2010/3/4 Feng Yu <mryuf...@gmail.com>
>
> > 这个数据可以随便该的 erlang的默认很小 才64K好像
>
> > 专注 高性能容错分布式服务器的研究和实现
> >http://blog.yufeng.info
>
> > 2010/3/4 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
On 3月5日, 下午6时39分, Feng Yu <mryuf...@gmail.com> wrote:
> 我说的就是Erlang调度器的线程, 就是pthread创建的, 是同一个东西.
>
> 专注 高性能容错分布式服务器的研究和实现http://blog.yufeng.info
>
> 2010/3/4 chuang <lichuang1...@gmail.com>
>
>
>
> > 呃....chenshuo说的那个是系统级别的线程的数据吧,erlang只是语言层面抽象出来的概念,不是一个东西吧?
>
> > 2010/3/4 Feng Yu <mryuf...@gmail.com>
>
> > 这个数据可以随便该的 erlang的默认很小 才64K好像
>
> >> 专注 高性能容错分布式服务器的研究和实现
> >>http://blog.yufeng.info
>
> >> 2010/3/4 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>
On 3月5日, 下午7时46分, Feng Yu <mryuf...@gmail.com> wrote:
> 默认是 CPU数量+4 + async线程数目
>
> 专注 高性能容错分布式服务器的研究和实现http://blog.yufeng.info
>
> 2010/3/5 Shuo Chen <giantc...@gmail.com>
> ...
>
> 阅读更多 >>