多线程服务器

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

写一个高性能的服务器,传统的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等待较多的情况,新版本应该会表现出更好的综合性能。

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

Code 2011

成功人士是不写总结的,所以我来写个总结。

今年的coding从ABAP开始,这个很多人现在不知道以后也不用知道最好永远也不要知道的语言和他的平台,是一个脱胎于Pascal,在发展过程里又杂糅了SQL和C++的怪物,最后几乎变成了满屏幕关键字。再加上缺乏文档,实在是一段不堪回首的记忆。

3月份开始,我又重新开始了一个python项目叫做jip,他兼容maven,可以与virtualenv和setuptools集成,帮助你解决jython项目的java依赖,简化jython项目的发布,提高工作效率。这个小工具倾注了我不少时间,如果你某一天要和jython打交道的话,可以试试看。

今年还尝试了coffeescript,它恰到好处地消除了javascript里一些ugly的部分,大大提高了编码速度。我用它改写了网站首页的js

6月份我又重拾起1年前的javascript库heatcanvas,和lbt05协作完善了程序。通过HTML5的Web worker API改善了渲染canvas时的用户体验,增加了对Google Maps,OpenLayers,Baidu Map以及Leaflet的支持。这个项目的介绍被顶到hackernews的首页,这是一种前所未有的感觉。

7月份开始迎来了一个重大的转折。断断续续学习了一年半的clojure,终于开始写第一个库,reddit.clj,用clojure封装reddit网站的API。通过这个入口算是真正走进了clojure的世界,七月底reddit.clj基本完成之后我开始写他的第一个应用,rageviewer。这是个clojure的web项目,借着这个契机又接触了compojure和ring。而且写rageviewer的时候恰逢clojurescript公开,一不做二不休,于是这个项目就成了一个full stack的clojure项目:前后端都是clojure。最后rageviewer部署在当时刚刚开始邀请测试的cloudfoundry上。

8月参加了在上海的cn-clojure列表第一次聚会后,我开始用clojure克隆一个已有的并不复杂的程序,当时选择了beanstalkd,一个轻量级的task queue。这个项目取名为clojalk。最后它成功地支持了beanstalkd协议的所有命令,支持了通过Write-ahead log做持久化和恢复。这个项目用到了aleph,见识了一把clojure思维下的网络编程。

另外我还帮clojure-control写了一个leiningen的插件,现在这个插件已经合并到clojure-control项目里了。clojure能有这么快的发展,leiningen作为构建工具也有很大的帮助。就好比上半年做jip时,感受到jython的小世界里就没有一个好的方案来同时解决java和python的项目管理问题,而且人们也不重视这个问题。

年底还有一个clojure RPC框架的诞生,这个项目叫做slacker。项目还没有到总结的时候,我的org file列表上还有一长串的TODO。

总得来说,我觉得今年学习clojure的这个过程很有借鉴价值。对于一个新语言新平台新生态系统,如何入门并且getting real。你可以从一个功能简单的库开始,比如包装一个网站的API,或者(对于clojure来说),包装一个已有的java的库。在完成之后,利用这个库,写一个web应用,进而去了解这个平台上的web开发。再下一步,可以去克隆一个其他平台上的项目,规模不要太大。如此循序渐进,学习的效果很不错。另外,无论做了什么,只要是有用的,就应该说出来,这不仅是自我鼓励,有时候也能找到志同道合的朋友一起参与。

最后除了clojure之外,今年还接着gnome-shell的发布和更新,接触了gnome-shell的gjs扩展开发。又是一个不堪回首的平台,也许是还没有finalize吧,没有任何文档,而且一个平台上的库连变量拼写的风格都不一样!我是不会再浪费时间了,当然,以后的这个豆瓣电台的control还是会继续跟着gnome-shell的发布一直维护的。

除了上面提到的,今年还尝了一些groovy,common-lisp,甚至octave,不管怎么说都算是一个big year了。但愿明年能把这种状态保持下去,享受这种愉悦。

普通青年、二逼青年与文艺青年的Java代码缩进

普通青年

while(true) {
    if (something) {
        System.out.println(something);
        break;
    }
}

特点: tab与空格混用,无其他特点。
常见于:各类代码仓库。

二逼青年

while(true)
{


    if (something)
    {
        System.out.println(something);
        break;
    }

}

特点:总担心代码不够长
见于:各类劣质技术书籍

文艺青年

while(true)
  {
  if (something)
    {
      System.out.println(something);
      break;
    }
  }

特点:普通Java青年永远不会理解的缩进,lisp程序员会心一笑
见于:Clojure源码

使用defrecord与defprotocol的注意事项

简单地说,protocol是clojure中的接口,record是clojure中的数据类型。

可以通过这样的code定义一个protocol

(defprotoco DummyProtocol
  "doc string..."
  (method-one [self x] "doc string..."))

需要注意的是,protocol里所有方法的第一个参数都是self/this参数(类似python),从第二个开始才是调用时传入的参数。如果方法要重载呢?

(defprotocol DummyProtocol
  "doc string..."
  (method-one [self x] [self x y] "doc string")
)

Apress的 Practical Clojure 书里的例子,给重载的参数表加上了括号,这样会导致编译错误(注记)。

定义一个record实现protocol

(defrecord DummyRecord [a b c]
  DummyProtocol
  (method-one [self x] (+ a x))
  (method-one [self x y] (+ a x y)))

Practical Clojure里关于这部分的代码,又丢掉了self参数(注记)。

最后还有一个问题,如果直接use你的ns,你会发现调用record时出现:
java.lang.IllegalArgumentException: Unable to resolve classname: DummyRecord

怎么回事,不是都use了吗?原因是record被编译成了java对象,所以引用时要用java对象的引用方式,import之。