erlang 给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以像写单线程同步操作一样来写异步操作,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/linux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。
我已经有一部分代码是用 Boost.Asio + Boost.Coroutine 来做的。对于比较复杂的网络协议,解析的过程需要保留中间状态的,这种编码有好处。
我对erlang不熟悉,这一段需要更多解释:-) 或者指向erlang书的哪章哪节也好。On Dec 5, 2007 12:08 PM, lijie <cpu...@gmail.com> wrote:erlang 给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以像写单线程同步操作一样来写异步操作,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/linux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。
是个类似网络游戏的东西。TCP 连接,需要切包,但是包里面并没有长度,也就是说需要解析包的内容才知道长度。我就把收包的代码放到一个 coroutine 里面,这个 coroutine 是运行在主线程的。数据来了就切到解析协议的 coroutine ,在解析协议的 coroutine 里面需要读数据,就切到主循环(主循环挂在 io_service::run 上)。这样,解析协议的代码可以写成同步的,但实际上却是异步 IO 。这就是所谓"化异步为同步"。在07-12-5,pongba <pon...@gmail.com> 写道:
> Must<int>(0) = sock.Bind (2345, "0.0.0.0");
在 07-12-5,lijie<cpu...@gmail.com> 写道:
1. process per connection / thread per connection
这在连接数不多的时候编码方便.
并且, 在连接数不多而要求网络吞吐量高的场合, 这种方式是最容易实现, 效果也好.
2. 单一 selector/preactor
需要编写大量的状态机代码, 比较麻烦.
配合 epoll/kqueue/ devpoll 等机制, 没有 io block 问题的时候, 这种模式可
以轻易处理大量连接, cpu 消耗少.
需要注意的是, 文件访问不一定会造成大的 io block, 看业务目标和设计. 例如,
发送文件内容的时候, 可以使用 sendfile 调用, 或者 (有人提出过, 我还没有研
究过) 将文件 mmap 到内存中, 然后用 zero copy api 去发送这段内存.
外界的数据库访问之类的, 则一定会造成 block.
3. 线程池
half-sync / half-async 方式
一个 selector 负责接收请求, 一堆线程处理请求
缺点是 切换的 thread context 太多, 对 cpu cache 也不利.
leader / follower 方式
优点是减少了 thread context 切换, 对 cpu cache 也更有利.
ACE 的书上写这个模式的缺点是, 编写程序更复杂.
但是根据我自己的实践, 这里面还有一个对性能很不利的点:
hs ha 方式, selector 可以有很多数据作为自己线程的私有数据, 访问的时候不
必加锁.
领导者跟随者模式中, 由于所有线程都可以访问所有的数据结构, 造成所有的数据
访问之前都需要加锁(可能有的数据结构: 发送队列, 全局连接表 或者 os 具体
poll 设施的数据结构), 这在多core 的情况下, 会引起很大的性能损失. 如果使
用小的加锁粒度, 那么要申请的锁数目众多, 做一件事情可能要加解多次锁; 如果
使用大粒度锁, 那么锁竞争冲突可能会严重.
我没有做实际的测试, 但是担心领导者追随者这种方式, 在大连接数, 多 cpu
core 的情况下表现会差, 所以放弃了这种方式.
我现在写的库, 实际上是使用 ha hs 方式, 但是, 多数简单的任务, 直接就在
selector 线程中处理了, 计算量大的任务, 或者是有 io block 的任务, 才交给
线程池进行处理, 对于这种复杂的任务来说, thread context 的切换开销, cpu
cache 的损失, 也就不算什么了.
4. erlang 的方式
我想过写状态机不方便, 如果能够象写顺序执行的代码一样写代码就好了, 但是再
想到要处理的种种问题, 以及状态机其实写熟了也就那样了, 就算了 :)
BTW: ACE 的selector 代码, 在事件发生的时候, 先将这个事件源上的事件关闭,
处理完了再打开, 这在大连接数的情况下会造成很大的开销, OS call 的开销不小.
asio 的我忘了.
在 07-12-7,red...@gmail.com<red...@gmail.com> 写道:
--
新的理论从少数人的主张到一统天下,并不是因为这个理论说服了别人抛弃旧观点,而是因为一代人的逝去。
My blog: http://googollee.blog.163.com
2. 单一 selector/preactor
需要编写大量的状态机代码, 比较麻烦.
配合 epoll/kqueue/ devpoll 等机制, 没有 io block 问题的时候, 这种模式可
以轻易处理大量连接, cpu 消耗少.
需要注意的是, 文件访问不一定会造成大的 io block, 看业务目标和设计. 例如,
发送文件内容的时候, 可以使用 sendfile 调用, 或者 (有人提出过, 我还没有研
究过) 将文件 mmap 到内存中, 然后用 zero copy api 去发送这段内存.
3. 线程池
half-sync / half-async 方式
一个 selector 负责接收请求, 一堆线程处理请求
缺点是 切换的 thread context 太多, 对 cpu cache 也不利.
leader / follower 方式
优点是减少了 thread context 切换, 对 cpu cache 也更有利.
ACE 的书上写这个模式的缺点是, 编写程序更复杂.
但是根据我自己的实践, 这里面还有一个对性能很不利的点:
hs ha 方式, selector 可以有很多数据作为自己线程的私有数据, 访问的时候不
必加锁.
领导者跟随者模式中, 由于所有线程都可以访问所有的数据结构, 造成 所有的数据
访问之前都需要加锁(可能有的数据结构: 发送队列, 全局连接表 或者 os 具体
poll 设施的数据结构), 这在多core 的情况下, 会引起很大的性能损失. 如果使
用小的加锁粒度, 那么要申请的锁数目众多, 做一件事情可能要加解多次锁; 如果
使用大粒度锁, 那么锁竞争冲突可能会严重.
并发的文件访问会造成 io等待,实际测试发现这和文件系统类型、文件数也有关系。文件数多了以后,通常IO操作比较耗时的是open/close,由于读写都有缓冲,反而很少 出在读写上。我的测试环境环境是2T的磁盘空间写100k左右的文件,写到几百G就变慢很多了。
也有另一种解决办法,就是开多个selector,不过调度也是个麻烦,而且要求各个连接之间没有交互,web服务器这样的倒是很适合。
状态机主要不是写起来麻烦,而是本来很顺畅的想法被拆成几段,看着有点恶心。。
说一个实际的应用,一个存储项目。由于要处理很多低流量的连接,所以连接那部分是epoll处理的。这个存储除了要保存本地文件以外,还要往另一个备份设 备(同样的上传服务器)上写一份,而且要确保备份成功才返回。
第一个实现是收够包头后就交给线程池处理。这个实现是把文件上传成功后再写备份,后来发现这样不好,用户都上传完了还要等一会,这段时间备份设备可能流量 突增,备份设备负载也可能一会大一会小。
第二个版本改成一边上传一边备份,收一次就写一次并同步一次。这也有问题,磁盘可能突然很忙,备份设备也会忙,不能因为这个就降低用户上传的流量。所以上 传时先写到缓冲区,写文件和备份可以异步进行。另一个问题,文件可能很大,1000个同时上传的文件,每个10M内在都不够用了,又改成维护一个链表,写 文件和备份都完成的部分就删掉以释放内存空间。
线程池来处理也很有问题,线程数量开大了很影响性能,开小了并发数又受限,这些低流量的连接又占着线程不拉XX。
所以第三个版本这部分改成aio了,其它部分都在主线程里面完成,主线程只是完成调度。接着发现aio不支持文件open/close,经过统计这两个操 作又是最耗时的,主线程不能因为这个去阻塞,所以加个线程池来完成open/close。
结果性能还是很理想的,用aio换掉线程池就让IO性能提升了很多。问题是这几个版本一个比一个难写,第一个开发只要2-3天,第三个版本花掉2周时间才 写完并排除大部分BUG,而且这部分代码看着就头大,每添加一个功能就要去修改状态机那部分,每个连接可是有好几个状态机分别标记socket/file 和同步。
refs辅以合适的目录hash算法,较ext3而言,可以极大地提高大量小文件的io性能,实际项目经验。
open /close 速度有几个地方可以进行尝试的, 不知道你做了没有:
并发的文件访问会造成 io等待,实际测试发现这和文件系统类型、文件数也有关系。文件数多了以后,通常IO操作比较耗时的是open/close,由于读写都有缓冲,反而很少 出在读写上。我的测试环境环境是2T的磁盘空间写100k左右的文件,写到几百G就变慢很多了。
1. 如果是大量小文件, 那么文件系统类型是值得尝试的, 通常认为linux 下面 reiserfs 处理大量小文件性能较好, open close 性能较高
2. 采用多级目录的方法, 给最终用到的文件名做 hash, 对应到这些目录上, 这样每个目录下面的文件不会太多
3. 如果一个文件比较零碎, open 之后做 seek 也是比较慢的, linux 2.6 自某个版本之后, 具体忘了, 多了一个新 kernel api, 可以给文件保留空间, 这样就不会出现文件一直往后写, 越来越零碎了.
哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)
也搜索了一些别人做过的测试,结果都是xfs综合性能最高,所以目前使用的是xfs,reiserfs据说前景不妙,所以没考虑用呢,做个 综合性的测试也比较费精力。
2. 采用多级目录的方法, 给最终用到的文件名做 hash, 对应到这些目录上, 这样每个目录下面的文件不会太多
原来也是这么想的,不过有同事做了几天测试后结论是:xfs几百万个文件放在同一个目录下比分级性能还高!有点出乎意料,不过也抽不出时间来验证了。不过 为了便于管理,还是给它分级了。。
3. 如果一个文件比较零碎, open 之后做 seek 也是比较慢的, linux 2.6 自某个版本之后, 具体忘了, 多了一个新 kernel api, 可以给文件保留空间, 这样就不会出现文件一直往后写, 越来越零碎了.
这个倒是没关注,有时间研究下。
理论上没有问题, 代码看起来也可以更漂亮.
状态机也只能这么做了,要不然也不叫状态机。。还有另一种做法,就是把stage做成对象,各个stage按顺序做成一个链表,每次处理完了就取链表的 next处理,结果和状态机是一样的,也算是一种分解吧,貌似ace里面也有类似的东西,以前大致看到一点,没使用过。
哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)
基本上是从头写,通讯框架倒是前两个月刚做完一套。AIO的例子网上也不多,所以都是从头摸索。架构、原理、性能上参考erlang比较多,到目前为止我 认为erlang算是个完美平台,只是写port麻烦了点。erlang对操作系统效率最高的异步文件IO也基本上没用上,所以对于我这里的项目来说,性 能上超过它还是有胜算的,没有胜算的部分是需要利用多CPU调度的。
上传了一个文档:threads-hotos-2003.pdf
没细看,似乎是说线程方式比基于事件方式更好一些.不过前面举的例子有明显问题,文件是从内存cache读取的,每个请求8k左右,
可以很容易地全部塞进socket缓冲区,这种方式自然是线程更容易处理,因为事件方式多一次系统调用。另外这个数据统计也有 问题,并发系统的测试非常复杂,需要模拟随机的慢客户,突发的大并发。我在项目中就发现模拟压力测试测不出所有问题,挂到真实系统中就很容易找到真正的瓶 颈,通常会得出和模拟压力测试不同的结论。对, 互联网上情况复杂, 如果通信程序的通信对方程序不是自己控制代码, 就更复杂, 例如, 会碰到慢速客户, 发了请求就不收应答的客户, 带宽巨大的客户, 短时间内产生大量新连接的客户等等, 用每连接一个线程方式, 其实很容易被弄出问题来.
使用我前面描述的轻量级线程方式或者是基于事件方式,可以异步IO很好地配合,而操作系统实现的异步IO可以对读写请求进行重排以优化性能,虽然也听到说 这个特性的效果不怎么样,但测试发现性能相差还是非常多的。
上传了一个文档:threads-hotos-2003.pdf
没细看,似乎是说线程方式比基于事件方式更好一些.不过前面举的例子有明显问题,文件是从内存cache读取的,每个请求8k左右,可以很容易地全部塞进 socket缓冲区,这种方式自然是线程更容易处理,因为事件方式多一次系统调用。另外这个数据统计也有问题,并发系统的测试非常复杂,需要模拟随机的慢 客户,突发的大并发。我在项目中就发现模拟压力测试测不出所有问题,挂到真实系统中就很容易找到真正的瓶颈,通常会得出和模拟压力测试不同的结论。
在 2007-12-07五的 17:26 +0800,lijie写道:
> stage做成对象,各个stage按顺序做成一个链表,每次处理完了就取链表的next
> 处理,结果和状态机是一样的,也算是一种分解吧,貌似ace里面也有类似的东
> 西,以前大致看到一点,没使用过。
>
>
>
>
> 哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有
> 好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框
> 架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)
>
> 基本上是从头写,通讯框架倒是前两个月刚做完一套。AIO的例子网上也不多,
> 所以都是从头摸索。架构、原理、性能上参考erlang比较多,到目前为止我认为
> erlang算是个完美平台,只是写port麻烦了点。erlang对操作系统效率最高的异
> 步文件IO也基本上没用上,所以对于我这里的项目来说,性能上超过它还是有胜
> 算的,没有胜算的部分是需要利用多CPU调度的。
>
>
> >
--
"Live Long and Prosper"
- oldrev
前一阵以ponda 和 longshanksmo 为首讨论的深层内容, 技术含量才真是高啊.
oldrev 写道:
> 整版最精华的帖子了 :)
>
岂敢岂敢.
前一阵以ponda 和 longshanksmo 为首讨论的深层内容, 技术含量才真是高啊.
On Dec 9, 2007 5:34 PM, <red...@gmail.com> wrote:oldrev 写道:
> 整版最精华的帖子了 :)
>
岂敢岂敢.
前一阵以ponda 和 longshanksmo 为首讨论的深层内容, 技术含量才真是高啊.哎哟哟,可不能这么说。这是两个领域的东西啊。都是很重要的。我已经把它star了,我的"缺门",正要好好学习哦。:)
建议pongba把这两个帖子页顶置。:)
>
> 2、send操作默认是直接在主线程中阻塞发送的,某些情况下这样效率比较高,
> 因为直接发到缓冲区了。如果数据比较大,则可以设置发送选项为delay_send,
> 这时候并不马上发送,而是注册OUT事件,有事件时才真正发送,缺点是对于
> 短连接小数据包方式,多一次系统调用。erlang在这上面切换很灵活
>
> 3、(前面提过)文件IO操作在没有打开异步线程时,是直接在主线程中完成
> 的,打开以后就会交给线程池完成,程序不用改变。
>
> 4、erlang调度线程和port线程池的通讯方式实现比较普通,说普通是因为大家
> 都是这么做的。线程池那边阻塞等待信号,调度线程有任务就放队列并触发信
> 号,线程池处理完以后放到队列并发送一字节到socket来唤醒调度线程。由于调
> 度线程这边是不需要加锁的,所以它使用的肯定是个无锁队列,只需要在线程池
> 那一端加锁。
>
> 有了这些特性,可以用它轻松编写各种模式的代码,除了LF模式和每连接一线
> 程方式以外,这和它的内核实现有关,它选择了一种它认为最好的方式。erlang
> 的smp方式我没研究过,不过通常HS/HA方式总是可以充分利用CPU,使用smp
> 有可能效率反而降低了。
>
> 注:以上是使用erlang编写代码,strace跟踪的结果。由于实际测试过erlang的
> 性能还是很高的,虽然语言性能稍低点,但目前我写过的所有的压力测试程序,
> 用erlang写的那个是可以把程序负载压到最高的,所以在使用C++做项目时通
> 常是尽可能向它学习。
>
好东西的成本就是使用代价高 :(
BTW:
ACE, asio, zeroc ice 的代码我都看过, 觉得ACE, asio可以适合那些要求不那
么非常高的代码, 例如连接数不要太多, 不要求将网络带宽尽量用掉, 网络延时尽
量减小等, 还是可以用的.
ice 优点是容易跨语言, 说到性能指标, 其实比较不行, 适合较轻的应用;
thread per connection 对于大连接数, 开销厉害; 没有必要的复杂逻辑, 对于要
求低延时等也不是很有利. 但是, 如果只是用于较轻的应用, 在程序已经集成脚本
语言的时候, 通常有非常容易的选择, 例如 python 的 pyro.
ACE_OS 里面的东西我倒是很喜欢, 处理平台差异做得很好, 如果是独立的就好了.