多线程服务器

写了挺长时间网络程序了,有些事到最近才弄明白,记录一下。

写一个高性能的服务器,传统的BIO方式基本上已经被淘汰了,很难获得理想的性能。所以现在都是以事件驱动的方式来写,在Linux上用epoll,Java平台上就是NIO。再向上,有一些包装的库,比如twisted比如libevent比如Java上的Netty。

Netty的server需要至少两组线程,BossPool和WorkerPool,。前者负责accept,后者负责r/w。通常的例子里,这就是仅有的两组线程。用户在框架之上的业务逻辑,写在handler里,以单线程的方式运行在worker线程上。

这样的方式在很多例子里很普遍。但是如果handler里的业务逻辑比较复杂,尤其是IO的等待过长(比如查询数据库),就会由于在handler上阻塞的时间过长,导致服务器读写的效率下降。所以通常是不能在这样环境下的handler里写IO阻塞的代码。

解决这个问题,一种方式是多线程,一种是全异步化。后者最典型的例子就是nodejs,坦白说编程模型非常复杂,对开发者要求较高。一种相对简单的办法,就是以多线程的方式,增加CPU的利用效率,来平衡IO阻塞带来的影响。IO等待时间的比重越大,线程数就可以陪得越高。牺牲一些sy的CPU时间,换取更高的利用率。

但是随之而来了另一个问题,因为在handler中使用了多线程的模型。对顺序收到的包,交由不同的线程并行处理,就没有办法保证返回时的顺序。客户端就无法了解刚刚返回的包是对应哪个请求的。

这个情况也有两种办法处理。其一是在线程管理上做文章,采用一种折中的办法。对于每一个客户端连接来说,仍然是占用同一个线程来处理。这样首先任务不会占用worker线程,其次在整体上仍然提高了CPU的利用率。但是这样做的缺点是,在单一客户端看来,任务仍然是串行执行: 三个需要耗时500ms的请求同时顺序发出,第三个至少要在1500ms之后才能收到响应,客户端的latency比较高。

另一种,就是在应用协议层面做一个冗余字段,通常叫做serial id或者transaction id。客户端为每一个请求生成一个这样的id,并存储这个id对应的回调。服务器端不需要对包的顺序作任何关注,只需要把这个id原封不动地拷贝回去即可。这样,服务器就可以自由调度线程来处理请求。以上面的例子,在不繁忙的情况下,三个响应在500ms左右就都可以到达了。

这种方式对应用协议有特殊的要求,但是比较常见的协议都预留了这个字段,Diameter协议甚至留了两个这样的字段来便于代理的实现。

slacker 0.7.x基于aleph,由于aleph / lamina无法侵入协议层面,所以采用的都是顺序的客户端和服务器。这种方式编程非常简洁,协议设计简单,实现起来很快。但是作为RPC框架,一旦客户的函数阻塞较长,就会影响整体性能。0.8.0开始,通过新的协议和link库支持,slacker采用了transaction id的方式,服务器端默认使用并行处理,客户端不再依赖顺序指定响应和回调。尽管在一些CPU为局限的测试里性能有少许下降(与单线程相比,增加了调度的成本),但是针对实际应用里IO等待较多的情况,新版本应该会表现出更好的综合性能。

以上这些,就是最近的心得,希望能解释清楚事件驱动服务器里的这些事情。

Slacker performance enhanced

In the slacker framework, performance issue becomes more and more critical as the basic features are almost completed. As mentioned in cnclojure meetup, I will focus on the performance enhancement in next release.

Now I have worked out a testable version. The new slacker core has been moved to a new NIO library, link. Compared with aleph, link is a thin wrapper of Netty. It takes some nice features from aleph (gloss codec dsl, elegant API), and drops the heavy abstraction, lamina. The new slacker client runs on a real nonblocking connection. Connection pooling is no longer needed.

I have some performance benchmark to visualize the improvement. The test was made on my laptop (Intel(R) Core(TM)2 Duo CPU T5870 @ 2.00GHz). It ran 400,000 calls with 40 threads on a local slacker server.

slacker 0.7.0 (clojure 1.2, aleph 0.2.0): 614005.059259msecs
slacker 0.7.1-SNAPSHOT (clojure 1.3, aleph 0.2.1-beta2): 409110.909142msecs
slacker 0.8.0-SNAPSHOT (clojure 1.3, link 0.2.0-SNAPSHOT): 42468.401522msecs

tps chart

Check out the new slacker on the 0.8-dev branch.

Slacker slides on cnclojure meetup

从北京回来两天了,稍微有点累,还没来得及总结一下,先把slides上传分享一下: http://www.box.com/s/k0alcj1p115jq40bdkik

解压之后 lein deps && lein run 一下即可。

这是一个借助impress.js制作的幻灯片。之所以让它跑在webbit里,是因为我简单hack了impress.js让你可以通过websocket远程控制幻灯片播放。这样,就可以通过手机上的firefox浏览器(目前android上仅有firefox支持websocket)控制播放,把手机变成一款遥控器。

使用Enlive作为模板引擎

在所有的clojure web开发例子里,对模板的介绍都很少。很多的简单例子都是以hiccup作为页面生成的手段。hiccup是个clojure的html DSL,例子里用这样的DSL生成页面确实很酷,可是他是real world吗,当然不是。

好在clojure世界里早就有了enlive,它不仅是一个通过css selector解析html的库,本身也可以作为模板引擎应用在web开发中。我不知道这种通过css selector的方式是否是enlive首创,不过他实在是非常新颖独特,而且平滑了页面设计和程序的集成。

例如这样一个模板 index.html:

<div id="cc">Sample Text</div>

在clojure程序中,使用enlive的deftemplate

(deftemplate index "index.html"
  [ctx]
  [:div#cc] (content (:data ctx)))

在控制器里,可以很MVC地渲染页面

(index {:data "rendered text"})

除了content用于渲染文本,还有html-content可以渲染含html标签的内容,以及set-attr用来修改页面元素的属性。

和传统的模板引擎相比,最大的不同是enlive里没有嵌入模板的直观的控制流,没有循环和条件判断,但是并非不可实现。

循环输出一组list

页面 list.html

<ul id="the-list">
<li class="list-item"></li>
</ul>

定义一个enlive的snippet

(defsnippet item-model "page.html" [:.list-item]
  [ctx]
  [:.list-item] (content (:data ctx)))

在页面模板里

(deftemplate list-page "list.html"
  [ctx]
  [:ul#the-list] (content (map item-model (:some-list ctx))))

这样在页面里列表项会被循环输出,而在页面设计时这里可以放任意个li,并且直接交给后台作为模板。

条件判断

页面,设计时显示所有的内容 msg.html

<span id="msg">只在一定条件下显示</span>

在模板中通过clojure的if进行判断

(deftemplate msg "msg.html"
  [ctx]
  [:span#msg] (if (:show ctx) identity (html-content "")))

解决了这两个问题,基本上用enlive作为模板引擎就没有障碍了。不过enlive也有一点小问题,其一可能是性能的问题,方便的selector显然要比传统的模板语言消耗更多的CPU。另外,在开发过程里,页面文件在服务器启动后不能热加载,修改页面必须重启ring才能看到。也许有时间的话,可以给它加一个reload选项。