|
2009年9月27日
很久没有更新博客,没想到更新是搬迁公告。这个博客累计的访问量突破百万,是我建立的时候完全没有想过的事情。博客对我来说更多是记录、记忆的地方,我时常因为想不起某个东西,来翻自己的博客,查找旧知,发现新知。阅读很多人的博客,也是我跟踪、学习新知的主要方式。虽然微博兴起,不过博客作为更系统性的记录的地方,不会过时。 非常感谢blogjava提供这么优秀的平台。只是我今年给自己的一个目标是建立自己的博客,因此现在要搬迁,加上其实现在也写的少,其实搬迁不搬迁,意义也不大了。算是一个通告,有兴趣的可以订阅我的新博客,没兴趣的请自行略过,谢谢大家。 新博客地址: http://blog.fnil.net/RSS地址: http://blog.fnil.net/index.php/feed新博客的第一篇记忆是《 Leiningen教程中文版》,从现在开始,这个博客将不再发布任何新的文章,已有的也不会删除,部分可能会导到我的知识库上去。 最后,祝福blogjava越办越好。
It's my weekend project——node-shorten: URL Shortener just like t.cn,goo.gl etc.
Is is written in NodeJS,using express.js for MVC framework,and using MySQL for storage and Redis for caching.
A demo online: http://fnil.me/
The project is at https://github.com/killme2008/node-shorten
Feel free to modify and use it.Have fun.
很久没写博客,一是工作忙,二是没有太多的事情可说。 最近在公司大佬的支持下,建立了一个Clojure语言中文方面的博客和问答网站,欢迎任何对Clojure这门基于JVM之上的函数式语言感兴趣的童鞋贡献原创文章或者资料,申请帐号请看 这里。 博客地址: http://blog.clojure.cn/问答网站: http://ask.clojure.cn/欢迎转发和注册使用,谢谢。 邮件列表仍然使用google group: https://groups.google.com/group/cn-clojure/
Home: https://github.com/killme2008/ring.velocity
A Clojure library designed to render velocity template for ring in clojure.
UsageAdds dependency in leiningen project.clj: [ring.velocity "0.1.0-SNAPSHOT"]
Create a directory named templates in your project directory to keep all velocity templates. Create a template templates/test.vm : hello,$name,your age is $age.
Use ring.velocity in your namespace: (use '[ring.velocity.core :only [render]])
Use render function to render template with vars: (render "test.vm" :name "dennis" :age 29)
The test.vm will be interpreted equals to: hello,dennis,your age is 29.
Use ring.velocity in compojure: (defroutes app-routes (GET "/" [] (render "test.vm" :name "dennis" :age 29)) (route/not-found "Not Found"))
Use ring.velocity in ring: (use '[ring.util.response]) (response (render "test.vm" :name "dennis" :age 29))
Custom velocity properties,just put a file named ring-velocity.properties to your classpath or resource paths.The default velocity properties is in src/default/velocity.properties. LicenseCopyright © 2012 dennis zhuang[killme2008@gmail.com] Distributed under the Eclipse Public License, the same as Clojure.
Home: https://github.com/killme2008/ring.velocity
Clojure的一大优点就是跟Java语言的完美配合,Clojure和Java之间可以相互调用,Clojure可以天然地使用Java平台上的丰富资源。在Clojure里调用一个类的方法很简单,利用dot操作符:
user=> (.substring "hello" 3)
"lo"
user=> (.substring "hello" 0 3)
"hel"
上面的例子是在clojure里调用String的substring方法做字符串截取。Clojure虽然是一门弱类型的语言,但是它的Lisp Reader还是能识别大多数常见的类型,比如这里hello是一个字符串就可以识别出来,3是一个整数也可以,通过这些类型信息可以找到最匹配的substring方法,在生成字节码的时候避免使用反射,而是直接调用substring方法(INVOKEVIRTUAL指令)。
但是当你在函数里调用类方法的时候,情况就变了,例如,定义substr函数:
(defn substr [s begin end] (.substring s begin end))
我们打开*warn-on-reflection*选项,当有反射的时候告警:
user=> (set! *warn-on-reflection* true)
true
user=> (defn substr [s begin end] (.substring s begin end))
Reflection warning, NO_SOURCE_PATH:22 - call to substring can't be resolved.
#'user/substr
问题出现了,由于函数substr里没有任何关于参数s的类型信息,为了调用s的substring方法,必须使用反射来调用,clojure编译器也警告我们调用substring没办法解析,只能通过反射调用。众所周知,反射调用是个相对昂贵的操作(对比于普通的方法调用有)。这一切都是因为clojure本身是弱类型的语言,对参数或者返回值你不需要声明类型而直接使用,Clojure会自动处理类型的转换和调用。ps.在 leiningen里启用反射警告很简单,在project.clj里设置:
;; Emit warnings on all reflection calls. :warn-on-reflection true
过多的反射调用会影响效率,有没有办法避免这种情况呢?有的,Clojure提供了type hint机制,允许我们帮助编译器来生成更高效的字节码。所谓type hint就是给参数或者返回值添加一个提示:hi,clojure编译器,这是xxx类型,我想调用它的yyy方法,请生成最高效的调用代码,谢谢合作:
user=> (defn substr [^String s begin end] (.substring s begin end))
#'user/substr
这次没有警告,^String就是参数s的type hint,提示clojure编译器说s的类型是字符串,那么clojure编译器会从java.lang.String类里查找 名称为substring并且接收两个参数的方法,并利用invokevirtual指令直接调用此方法,避免了反射调用。除了target对象(这里的s)可以添加type hint,方法参数和返回值也可以添加type hint:
user=> (defn ^{:tag String} substr [^String s ^Integer begin ^Integer end] (.substring s begin end))
#'user/substr
返回值添加type hint是利用tag元数据,提示substr的返回类型是String,其他函数在使用substr的时候可以利用这个类型信息来避免反射;而参数的type hint跟target object的type hint一样以^开头加上类型,例如这里begin和end都提示说是Integer类型。
问题1,什么时候应该为参数添加type hint呢?我的观点是,在任何为target object添加type hint的地方,都应该相应地为参数添加type hint,除非你事先不知道参数的类型。为什么呢?因为clojure查找类方法的顺序是这样:
1.从String类里查找出所有参数个数为2并且名称为substring方法
2.遍历第一步里查找出来的Method,如果你有设置参数的type hint,则
查找最匹配参数类型的Method;否则,如果第一步查找出来的Method就一个,直接使用这个Method,相反就认为没有找到对应的Method。
3.如果第二步没有找到Method,使用反射调用;否则根据该Method元信息生成调用字节码。
因此,如果substring方法的两个参数版本刚好就一个,方法参数有没有type hint都没有关系(有了错误的type hint反而促使反射的发生),我们都会找到这个唯一的方法;但是如果目标方法的有多个重载方法并且参数相同,而只是参数类型不同(Java里是允许方法的参数类型重载的,Clojure只允许函数的参数个数重载),那么如果没有方法参数的type hint,Clojure编译器仍然无法找到合适的调用方法,而只能通过反射。
看一个例子,定义get-bytes方法调用String.getBytes:
user=> (defn get-bytes [s charset] (.getBytes s charset))
Reflection warning, NO_SOURCE_PATH:26 - call to getBytes can't be resolved.
#'user/get-bytes
user=> (defn get-bytes [^String s charset] (.getBytes s charset))
Reflection warning, NO_SOURCE_PATH:27 - call to getBytes can't be resolved.
#'user/get-bytes
第一次定义,s和charset都没有设置type hint,有反射警告;第二次,s设置了type hint,但是还是有反射警告。原因就在于String.getBytes有两个重载方法,参数个数都是一个,但是接收不同的参数类型,一个是String的charset名称,一个Charset对象。如果我们明确地知道这里charset是字符串,那么还可以为charset添加type hint:
user=> (defn get-bytes [^String s ^String charset] (.getBytes s charset))
#'user/get-bytes
这次才真正的没有警告了。总结:在设置type hint的时候,不要只考虑被调用的target object,也要考虑调用的方法参数。
问题2:什么时候应该添加tag元数据呢?理论上,在任何你明确知道返回类型的地方都应该添加tag,但是这不是教条,如果一个偶尔被调用的方法是无需这样做的。这一点只对写库的童鞋要特别注意。
Type hint的原理在上文已经大概描述了下,具体到clojure源码级别,请参考clojure.lang.Compiler.InstanceMethodExpr类的构造函数和emit方法。最后,附送是否使用type hint生成substr函数的字节码之间的差异对比:
未使用type hint |
使用type hint |
// access flags 1
public invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
L0
LINENUMBER 14 L0
L1
LINENUMBER 14 L1
ALOAD 1
ACONST_NULL
ASTORE 1
LDC "substring"
ICONST_2
ANEWARRAY java/lang/Object
DUP
ICONST_0
ALOAD 2
ACONST_NULL
ASTORE 2
AASTORE
DUP
ICONST_1
ALOAD 3
ACONST_NULL
ASTORE 3
AASTORE
INVOKESTATIC clojure/lang/Reflector.invokeInstanceMethod (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;
L2
LOCALVARIABLE this Ljava/lang/Object; L0 L2 0
LOCALVARIABLE s Ljava/lang/Object; L0 L2 1
LOCALVARIABLE begin Ljava/lang/Object; L0 L2 2
LOCALVARIABLE end Ljava/lang/Object; L0 L2 3
ARETURN
MAXSTACK = 0
MAXLOCALS = 0
|
public invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
L0
LINENUMBER 15 L0
L1
LINENUMBER 15 L1
ALOAD 1
ACONST_NULL
ASTORE 1
CHECKCAST java/lang/String
ALOAD 2
ACONST_NULL
ASTORE 2
CHECKCAST java/lang/Number
INVOKESTATIC clojure/lang/RT.intCast (Ljava/lang/Object;)I
ALOAD 3
ACONST_NULL
ASTORE 3
CHECKCAST java/lang/Number
INVOKESTATIC clojure/lang/RT.intCast (Ljava/lang/Object;)I
INVOKEVIRTUAL java/lang/String.substring (II)Ljava/lang/String;
L2
LOCALVARIABLE this Ljava/lang/Object; L0 L2 0
LOCALVARIABLE s Ljava/lang/Object; L0 L2 1
LOCALVARIABLE begin Ljava/lang/Object; L0 L2 2
LOCALVARIABLE end Ljava/lang/Object; L0 L2 3
ARETURN
MAXSTACK = 0
MAXLOCALS = 0
|
对比很明显,没有使用type hint,调用clojure.lang.Reflector的invokeInstanceMethod方法,使用反射调用(具体见clojure.lang.Reflector.java),而使用了type hint之后,则直接使用invokevirtual指令(其他方法可能是invokestatic或者invokeinterface等指令)调用该方法,避免了反射。
参考:
HouseMD是淘宝的聚石写的一个非常优秀的Java进程运行时诊断和调试工具,如果你接触过btrace,那么HouseMD也许你应该尝试下,它比btrace更易用,不需要写脚本,类似strace的方式attach到jvm进程做跟踪调试。
基本的安装和使用请看这篇文档《 UserGuide》,恕不重复。以下内容都假设你正确安装了housemd。
本文主要介绍下怎么用housemd诊断跟踪clojure进程。Clojure的java实现也是跑在JVM里,当然也可以用housemd。
我们以一个简单的例子开始,假设我们有如下clojure代码:
(loop [x 1]
(Thread/sleep 1000)
(prn x)
(recur (inc x)))
这段很简单,只是间隔一秒不断地打印递增的数字x。我们准备用housemd跟踪这个程序的运行,首先运行这个程序,你可以用lein,也可以直接java命令运行:
java -cp clojure.jar clojure.main test.clj
运行时不断地在控制台打印数字,通过jps或者ps查询到该进程的id,假设为pid,使用housemd连接到该进程:
housemd <pid>
顺利进入housemd的交互控制台,通过help命令可以查询支持的命令:
housemd> help
quit terminate the process.
help display this infomation.
trace display or output infomation of method invocaton.
loaded display loaded classes information.
要用housemd调试clojure,你需要对clojure的实现有一点点了解,有兴趣可以看过去的一篇blog《 clojure hacking guide》,简单来说,clojure的编译器会将clojure代码编译成java类并运行。对于JVM来说,clojure生成的类,跟java编译器生成类没有什么不同。
具体到上面的clojure代码,会生成一个名为 user$eval1的类,user是默认的namespace,而eval1是clojure编译器自动生成的一个标示类名,通过 loaded命令查询类的加载情况:
housemd> loaded user$eval1 -h
user$eval1 -> null
- clojure.lang.DynamicClassLoader@1d25d06e
- clojure.lang.DynamicClassLoader@1d96f4b5
- sun.misc.Launcher$AppClassLoader@a6eb38a
- sun.misc.Launcher$ExtClassLoader@69cd2e5f
通过-h选项打印了加载user$eval1的类加载器的层次关系,因为user$eval1是动态生成的(clojure启动过程中),因此它不在任何一个class或者jar文件中。除了查询user namespace的类之外,你还可以查询clojure.core,clojure.lang,clojure.java等任何被加载进来的类,例如查询clojure.core.prn的类,在clojure里这是一个函数,在jvm看来这只是一个类: housemd> loaded -h core$prn clojure.core$prn -> /Volumes/HDD/Users/apple/clojure/clojure.jar - sun.misc.Launcher$AppClassLoader@a6eb38a - sun.misc.Launcher$ExtClassLoader@69cd2e5f 注意,不需要完整的namespace——clojure.core,直接core$prn即可。其他也是类似。 小技巧:如果你实在不知道clojure编译器生成的类名,你可以利用jvm自带的jmap命令来查询。 接下来,我们尝试用trace命令跟踪方法的运行,例如例子中的clojure代码用到了loop和recur两个sepcial form,我们跟踪下loop: housemd> trace -t 5 core$loop INFO : probe class clojure.core$loop core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
INFO : Ended by timeout INFO : reset class clojure.core$loop 在5秒内,clojure.core$loop类有两个方法各被调用了5次,doInvoke是实际的调用,而getRequiredArity用来查询loop所需要的参数个数。trace还可以跟踪到具体的方法,例如我们跟踪prn函数的调用情况: housemd> trace -t 5 core$prn.doInvoke INFO : probe class clojure.core$prn core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 1 1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 2 <1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 3 <1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 4 <1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 5 <1ms clojure.core$prn@3e4ac866
INFO : Ended by timeout INFO : reset class clojure.core$prn trace打印了方法的调用次数(5秒内)和每次调用的时间(毫秒级别),以及调用的target object 。小技巧:没有可变参数的函数生成类最终调用的是invoke方法(参数个数可能重载),有可变参数的函数调用的是doInvoke方法。
trace命令还支持打印调用堆栈到文件,例如: trace -t 5 -d -s core$prn.doInvoke 利用-s和-d命令会将详细的调用信息输出到临时目录,临时目录的路径可以通过trace help命令查询到,在我的机器上是/tmp/trace/<pid>@host目录下。调用堆栈的输出类似: example$square.invoke(Long) call by thread [main] example$eval9.invoke(test.clj:11) clojure.lang.Compiler.eval(Compiler.java:6465) clojure.lang.Compiler.load(Compiler.java:6902) clojure.lang.Compiler.loadFile(Compiler.java:6863) clojure.main$load_script.invoke(main.clj:282) clojure.main$script_opt.invoke(main.clj:342) clojure.main$main.doInvoke(main.clj:426) clojure.lang.RestFn.invoke(RestFn.java:421) clojure.lang.Var.invoke(Var.java:405) clojure.lang.AFn.applyToHelper(AFn.java:163) clojure.lang.Var.applyTo(Var.java:518) clojure.main.main(main.java:37) 上面这个简单的例子展示了使用housemd跟踪诊断clojure进程的方法。 自定义ns和函数的调试与此类似,假设我们有下面的clojure代码: (ns example) (defn square [x] (* x x))
(loop [x 1] (Thread/sleep 1000) (square x) (recur (inc x))) ns为example,自定义函数square并定期循环调用。使用housemd诊断这段代码: loaded -h example$square #查询square的加载情况 trace -t 10 -d -s example$square.invoke #跟踪10秒内square的调用情况
我们在维护的淘宝开源消息中间件的metaq的github分支,今天发布了1.4.2版本,主要做了如下改进:
1.支持发送和订阅分离,可以细粒度地控制Broker或者某个Topic是否接收消息和接受订阅。服务端添加新选项acceptPublish和acceptSubscribe。
2.更友好地关闭Broker,梳理关闭流程并通过JMX调用方法关闭替代原来简单的kill。
3.更新python客户端到0.2版本,可以通过pip安装: pip install metaq
4.发布ruby语言客户端meta-ruby 0.1版本。
5.其他小改进:升级gecko到1.1.1版本,升级quartz到2.1.4版本,添加集成测试工程和内部重构等。
6.新文档《使用log4j扩展发送消息》
简介:https://github.com/killme2008/Metamorphosis/wiki/介绍 下载:https://github.com/killme2008/Metamorphosis/downloads 文档:https://github.com/killme2008/Metamorphosis/wiki
你有个任务,需要用到某个开源项目;或者老大交代你一个事情,让你去了解某个东西。怎么下手呢?如何开始呢?我的习惯是这样:
1.首先,查找和阅读该项目的博客和资料,通过google你能找到某个项目大体介绍的博客,快速阅读一下就能对项目的目的、功能、基本使用有个大概的了解。
2.阅读项目的文档,重点关注类似Getting started、Example之类的文档,从中学习如何下载、安装、甚至基本使用该项目所需要的知识。
3.如果该项目有提供现成的example工程,首先尝试按照开始文档的介绍运行example,如果运行顺利,那么恭喜你顺利开了个好头;如果遇到问题,首先尝试在项目的FAQ等文档里查找答案,再次,可以将问题(例如异常信息)当成关键词去搜索,查找相关的解决办法,你遇到了,别人一般也会遇到,热心的朋友会记录下解决的过程;最后,可以将问题提交到项目的邮件列表,请大家帮你看看。在没有成功运行example之前,不要尝试修改example。
4.运行了第一个example之后,尝试根据你的理解和需要修改example,测试高级功能等。
5.在了解基本使用后,需要开始深入的了解该项目。例如项目的配置管理、高级功能以及最佳实践。通常一个运作良好的项目会提供一份从浅到深的用户指南,你并不需要从头到尾阅读这份指南,根据时间和兴趣,特别是你自己任务的需要,重点阅读部分章节并做笔记(推荐evernote)。
6.如果时间允许,尝试从源码构建该项目。通常开源项目都会提供一份构建指南,指导你如何搭建一个用于开发、调试和构建的环境。尝试构建一个版本。
7.如果时间允许并且有兴趣,可以尝试阅读源码: (1)阅读源码之前,查看该项目是否提供架构和设计文档,阅读这些文档可以了解该项目的大体设计和结构,读源码的时候不会无从下手。 (2)阅读源码之前,一定要能构建并运行该项目,有个直观感受。 (3)阅读源码的第一步是抓主干,尝试理清一次正常运行的代码调用路径,这可以通过debug来观察运行时的变量和行为。修改源码加入日志和打印可以帮助你更好的理解源码。 (4)适当画图来帮助你理解源码,在理清主干后,可以将整个流程画成一张流程图或者标准的UML图,帮助记忆和下一步的阅读。 (5)挑选感兴趣的“枝干”代码来阅读,比如你对网络通讯感兴趣,就阅读网络层的代码,深入到实现细节,如它用了什么库,采用了什么设计模式,为什么这样做等。如果可以,debug细节代码。 (6)阅读源码的时候,重视单元测试,尝试去运行单元测试,基本上一个好的单元测试会将该代码的功能和边界描述清楚。 (7)在熟悉源码后,发现有可以改进的地方,有精力、有意愿可以向该项目的开发者提出改进的意见或者issue,甚至帮他修复和实现,参与该项目的发展。
8.通常在阅读文档和源码之后,你能对该项目有比较深入的了解了,但是该项目所在领域,你可能还想搜索相关的项目和资料,看看有没有其他的更好的项目或者解决方案。在广度和深度之间权衡。
以上是我个人的一些习惯,我自己也并没有完全按照这个来,但是按照这个顺序,基本上能让你比较高效地学习和使用某个开源项目。
很久没更新博客了,在北京工作,忙碌并且充实。目前来说,Clojure最好的开发编辑器应该是Emacs + Slime的组合,利用 swank-clojure这个项目,加上clojure-mode,可以完美地运行slime。编译、运行、跳转、文档和引用查看甚至 debug都可以搞定。具体配置恕不重复,看swank-clojure的文档即可自己安装起来,或者这篇 中文博客, windows上配置。
分享几个Tip,也期待大家分享你们的使用心得。
首先是自动在打开clj后缀文件的时候启动执行clojure-jack-in与slime连接,可以在emacs配置里加上个callback:
(eval-after-load "clojure-mode" '(progn (require 'slime) (require 'clojure-mode) (unless (slime-connected-p) (save-excursion (clojure-jack-in)))))
这样在打开clj为后缀的文件的时候,将自动启动clojure-mode执行clojure-jack-in函数并且连接slime。 将clj后缀的文件自动关联到clojure-mode:
(setq auto-mode-alist (cons '("\\.clj$" . clojure-mode) auto-mode-alist))
通常来说如果你是利用 marmalade安装的,会自动关联的。 另外,启动自动匹配括号、字符串引号等的paredit模式一定要启动: (defun paredit-mode-enable () (paredit-mode 1)) (add-hook 'clojure-mode-hook 'paredit-mode-enable) (add-hook 'clojure-test-mode-hook 'paredit-mode-enable)
在使用clojure-mode或者clojure-test-mode的时候自动启用paredit模式,括号再也不是问题。括号匹配提示一般是开启的,如果没有,强制开启: ;; 显示括号匹配 (show-paren-mode t) (setq show-paren-style 'parentheses) slime更多配置,启用IO重定向(多线程IO输出都定向到SLIME repl)以及设置通讯字符编码等: (eval-after-load "slime" '(progn (slime-setup '(slime-repl slime-fuzzy)) ;;(setq slime-truncate-lines t) (setq swank:*globally-redirect-io* t) ;; (setq slime-complete-symbol-function ' slime-fuzzy-complete-symbol) (setq slime-net-coding-system 'utf-8-unix))) 细心的朋友可能注意到我注释了slime-fuzzy-complete的配置,这是一个支持更好的自动补全功能的SLIME插件(可以用缩写来自动补全),可惜在我机器上没有尝试配置成功,有兴趣你可以尝试下。 在REPL里支持语法高亮,一定要配置上: (add-hook 'slime-repl-mode-hook (defun clojure-mode-slime-font-lock () (require 'clojure-mode) (let (font-lock-mode) (clojure-mode-font-lock-setup)))) 单独在clojure-mode(在其他mode里这些快捷键不会起作用)里配置快捷键可以这样: (eval-after-load "clojure-mode" '(progn (require 'slime) (require 'clojure-mode) (define-key clojure-mode-map (kbd "M-/") (quote slime-complete-symbol)) (define-key clojure-mode-map (kbd "C-c s") (quote slime-selector))) 例如我这里将M-/作为自动补全的快捷键,因为meta键在我的Mac机器上设置为command键,因此自动补全的操作习惯就跟Eclipse类似。而 slime-selector是一个非常有用的函数,用来跳转到slime的一系列buffer,因此我绑定了C-c s快捷键。 额外一提,在Mac osx下,将command作为meta键: ;;; I prefer cmd key for meta (setq mac-option-key-is-meta nil mac-command-key-is-meta t mac-command-modifier 'meta mac-option-modifier 'none) 最后,期待大家不吝分享你的心得。
My weekend project clj.monitor is beta release,it's a clojure DSL for monitoring system and applications based on SSH.
Home: https://github.com/killme2008/clj.monitor
An example:
(ns clj.monitor.example
(:use [clj.monitor.core]
[control.core]
[clj.monitor.tasks]))
;;define a mysql cluster
(defcluster mysql
:clients [{:user "deploy" :host "mysql.app.com"}])
;;define a monitor for mysql cluster
(defmonitor mysql-monitor
:tasks [(ping-mysql "root" "password")
(system-load :5 3)]
:clusters [:mysql])
;;start monitors
(start-monitors
:cron "* 0/5 * * * ?"
:alerts [(mail :from "alert@app.com" :to "yourname@app.com")]
:monitors [mysql-monitor])
API document: http://fnil.net/clj.monitor
It is just a beta release,if you have any questions or find issues ,please let me know,thanks.
我们在维护的淘宝开源消息中间件的 metaq的github分支,今天发布了1.4.2版本,主要做了如下改进:
1.添加了大量的使用和原理文档,参见 Wiki。 2.合并tools和server-wrapper工程,提供统一的脚本来管理Broker,管理Broker的工作变得非常容易,全部工作都可以通过metaServer.sh的脚本来执行。同时提供了bat启动脚本,用于在windows上启动Broker做测试。 3.新功能: (1)新的客户端API用来获取topic的分区列表 (2)新的客户端API用来获取Broker的统计信息 (3)异步复制的Slave可以自动获取Master的配置变更,例如Master在配置文件中新增或者删除了topic并顺利reload热加载成功后,slave可自动复制或者移除变更的topic,无需重启。 (4)新的统计项目,可以通过'stats config'协议获取Broker的配置文件。 4.添加meta-python项目,一个python的客户端,暂时仅支持发送消息功能。 5.其他小改进,如统计信息的优化、构建工具的整合等。
更详细的发行日志请看 RelaseNotes。
下载地址: https://github.com/killme2008/Metamorphosis/downloads
入门指南: 《 如何开始》 更多文档请看 Wiki。
我发现很多人没办法高效地解决问题的关键原因是不熟悉工具,不熟悉工具也还罢了,甚至还不知道怎么去找工具,这个问题就大条了。我想列下我能想到的一个Java程序员会用到的常用工具。
一、编码工具
1.IDE: Eclipse或者 IDEA,熟悉尽可能多的快捷键,《 Eclipse常见快捷键列表》
2.插件:
(1) Findbugs,在release之前进行一次静态代码检查是必须的
(2) Clover,关心你的单元测试覆盖率
(3) Checkstyle 代码风格检查
3.构建和部署工具: ant或者 maven,现在主流都是maven了吧, 使用nexus搭建maven私服,再加上持续集成 jenkins。代码质量不用愁。
4.版本管理工具: svn或者 git
5. diff和patch
6.设置你的eclipse或者IDEA,如formatter, save actions以及code template等。代码风格,直接用google的也可以啊。《 Google style guide》
7.掌握一个文本编辑器,Emacs或者VIM,熟悉常用快捷键。这在你需要在线编辑代码,或者编写其他语言代码时候特别有用。《 神器圣战》
二、JDK相关
1.jstat : 观察GC情况,如:
jstat -gcutil pid 2000
2.jmap,查看heap情况,如查看存活对象列表:
jmap -histo:live pid |grep com.company |less
或者dump内存用来分析:
jmap -dump:file=test.bin pid
3.分析dump的堆文件,可以用jhat:
jhat test.bin
分析完成后可以用浏览器查看堆的情况。这个工具的分析结果还比较原始,你还可以用 Eclipse MAT插件进行图形化分析,或者IBM的 Heap Analyzer.
4.jvisualvm和jconsole: JVM自带的性能分析和监控工具,怎么用? 请自己看文档。
5.jstack:分析线程堆栈,如
jstack pid > thread_dump
查看CPU最高的线程在干什么的方法结合top和jstack: http://www.iteye.com/topic/1114219
6.更多JVM工具,参见官方文档: http://docs.oracle.com/javase/6/docs/technotes/tools/
7.学习使用btrace分析java运行时问题。《 Btrace使用简介》
8.GC日志分析工具: GC viewer、 GC-console或者 自己挑吧。
9.性能分析工具,除了自带的jvisualvm外,还可以用商业的 jprofiler。
10.JVM参数大全
11.《 JVM调优标准参数陷阱》,iteye神贴。
三、Linux工具
1. 熟悉常用的shell命令,
3.使用 htop替换top。
4.熟悉下 strace,gdb甚至 systemtap来分析问题。
5.熟悉vmstat,iostat,sar等性能统计工具。
5.自动化部署脚本, py-fabric或者自荐下我的 clojure-control。
四、其他
1.掌握一门脚本语言, Python或者 Ruby,高效解决一些需要quick and dirty的任务:比如读写文件、导入导出数据库、网页爬虫等。注意不是python.com,咔咔。
2.使用Linux或者Mac os系统作为你的开发环境。
3.升级你的“硬件工具”,双屏大屏显示器、SSD、8G内存甚至更多。 4.你懂的: https://code.google.com/p/goagent/
五、如何查找工具?
1.搜索引擎,google或者baidu,《 搜索技巧》
2.万能的stack overflow: http://stackoverflow.com/
3.虚心问牛人。 六、最重要的是⋯⋯ 一颗永不停止学习的心。
最近陆陆续续补充了不少metaq的文档,部分是直接从官方文档里摘抄出来,放在了github工程的wiki页,有兴趣了解甚至使用meta的可以仔细阅读下,一份目录:
后续还会继续补充。
Java世界里有 findbugs这样的神器,可以让你避免很多“简单愚蠢”的bug。同样,Clojure世界里也有相应的替代品,这就是今天要介绍的 kibit。不过kibit现在还比较年轻,判断的规则较少,但是已经可以使用起来做clojure代码的静态检查。
项目主页: https://github.com/jonase/kibit
使用:
1.安装lein插件:
lein plugin install jonase/kibit 0.0.2
2.在项目的根目录运行
lein kibit
kibit会分析项目里所有clojure源码,每个namespace分别分析,例如我分析 clojure-control的输出:
== control.commands ==
== control.core ==
[186] Consider (zero? (:status (ssh host user cluster (str "test -e " file)))) instead of (= (:status (ssh host user cluster (str "test -e " file))) 0)
== control.main ==
== leiningen.control ==
[null] Consider Integer/parseInt instead of (fn* [p1__61444#] (Integer/parseInt p1__61444#))
[null] Consider Integer/parseInt instead of (fn* [p1__65254#] (Integer/parseInt p1__65254#))
显然,kibit一个一个namespace分析过去,并且按照规则对它认为有问题的地方打印出来,并提出建议。例如这里它建议我用 (zero? (:status (ssh host user cluster (str "test -e " file))))
替换control.core里186行的: (= (:status (ssh host user cluster (str "test -e " file))) 0) 目前kibit大多数是这类代码风格上的检查,还没有做到类似findbugs那样更丰富的检查,例如NPE异常检查等。此外kibit还提供反射检查,任何有反射调用的地方都给出警告。 kibit是基于 core.logic实现的,它的规则都放在了 这里,通过defrules宏来定义检查规则,源码中对算术运算的规则定义: (defrules rules [(+ ?x 1) (inc ?x)] [(+ 1 ?x) (inc ?x)] [(- ?x 1) (dec ?x)]
[(* ?x (* . ?xs)) (* ?x . ?xs)] [(+ ?x (+ . ?xs)) (+ ?x . ?xs)]) 第一个规则,任何对类似(+ 1 x)的代码,都建议替换成(inc x),后面的与此类似。理论上你也可以自定义规则,并提交给官方。总体上说kibit仍然是比不上findbugs的,期待未来发展的更好。
我们经常需要在程序中测量某段代码的性能,或者某个函数的性能,在Java中,我们可能简单地循环调用某个方法多少次,然后利用System.currentTimeMillis()方法测量下时间。在Ruby中,一般都是用 Benchmark module做测试,提供了更详细的报告信息。
同样,在Clojure里你可以做这些事情,你仍然可以使用System.currentTimeMillis()来测量运行时间,例如:
user=> (defn sum1 [& args] (reduce + 0 args))
#'user/sum1
user=> (defn sum2 [& args]
(loop [rt 0
args args]
(if args
(recur (+ rt (first args)) (next args))
rt)))
#'user/sum2
user=> (defn bench [sum n]
(let [start (System/currentTimeMillis)
nums (range 0 (+ n 1))]
(dotimes [_ n] (apply sum nums))
(println (- (System/currentTimeMillis) start))))
user=> (bench sum1 10000)
1818
nil
user=> (bench sum2 10000)
4220
nil
定义两个求和函数sum1和sum2,一个是利用reduce,一个是自己写loop,然后写了个bench函数循环一定次数执行sum函数并给出执行时间,利用System.currentTimeMillis()方法。显然sum1比sum2快了一倍多。为什么更快?这不是我们的话题,有兴趣可以自己看reduce函数的实现。
除了用System.currentTimeMillis()这样的java方式测量运行时间外,clojure还提供了time宏来包装这一切:
user=> (doc time)
-------------------------
clojure.core/time
([expr])
Macro
Evaluates expr and prints the time it took. Returns the value of
expr.
nil
time宏用的不是currentTimeMillis方法,而是JDK5引入的nanoTime方法更精确。重写bench函数:
user=> (defn bench [sum n]
(time (dotimes [_ n] (apply sum (range 0 (+ n 1))))))
#'user/bench
user=> (bench sum1 10000)
"Elapsed time: 5425.074 msecs"
nil
user=> (bench sum2 10000)
"Elapsed time: 7893.412 msecs"
nil
尽管精度不一致,仍然可以看出来sum1比sum2快。
这样的测试仍然是比较粗糙的,真正的性能测试需要考虑到JVM JIT、warm up以及gc带来的影响,例如我们可能需要预先执行函数多少次来让JVM“预热”这些代码。庆幸的是clojure世界里有一个开源库 Criterium帮你自动搞定这一切,它的项目主页也在github上:https://github.com/hugoduncan/criterium
首先在你的项目里添加criterium依赖:
:dependencies [[org.clojure/clojure "1.3.0"]
[criterium "0.2.0"]])
接下来引用criterium.core这个ns,因为criterium主要宏也叫bench,因此我们原来的bench函数不能用了,换个名字叫bench-sum:
user=> (use 'criterium.core)
nil
user=> (defn bench-sum [sum n]
(with-progress-reporting (bench (apply sum (range 0 (+ 1 n))) :verbose)))
#'user/bench-sum
调用criterium的 bench宏执行测试,使用 with-progress-reporting宏包装测试代码并汇报测试进展,测试进展会打印在标准输出上。请注意,我这里并没有利用dotimes做循环测试,因为criterium会自己计算应该运行的循环次数,我们并不需要明确指定,测试下结果:
user=> (bench-sum sum1 10000)
Cleaning JVM allocations 
Warming up for JIT optimisations 
Estimating execution count 
Running with sample-count 60 exec-count 1417
Checking GC 
Cleaning JVM allocations 
Finding outliers 
Bootstrapping 
Checking outlier significance
x86_64 Mac OS X 10.7.3 4 cpu(s)
Java HotSpot(TM) 64-Bit Server VM 20.4-b02-402
Runtime arguments: -Dclojure.compile.path=/Users/apple/programming/avos/logdashboard/test/classes -Dtest.version=1.0.0-SNAPSHOT -Dclojure.debug= false
Evaluation count : 85020
Execution time mean : 722.730169 us 95.0% CI: (722.552670 us, 722.957586 us)
Execution time std-deviation : 1.042966 ms 95.0% CI: (1.034972 ms, 1.054015 ms)
Execution time lower ci : 692.122089 us 95.0% CI: (692.122089 us, 692.260198 us)
Execution time upper ci : 768.239944 us 95.0% CI: (768.239944 us, 768.305222 us)
Found 2 outliers in 60 samples (3.3333 %)
low-severe 2 (3.3333 %)
Variance from outliers : 25.4066 % Variance is moderately inflated by outliers
nil
user=> (bench-sum sum2 10000)
Cleaning JVM allocations 
Warming up for JIT optimisations 
Estimating execution count 
Running with sample-count 60 exec-count 917
Checking GC 
Cleaning JVM allocations 
Finding outliers 
Bootstrapping 
Checking outlier significance
x86_64 Mac OS X 10.7.3 4 cpu(s)
Java HotSpot(TM) 64-Bit Server VM 20.4-b02-402
Runtime arguments: -Dclojure.compile.path=/Users/apple/programming/avos/logdashboard/test/classes -Dtest.version=1.0.0-SNAPSHOT -Dclojure.debug= false
Evaluation count : 55020
Execution time mean : 1.070884 ms 95.0% CI: (1.070587 ms, 1.071136 ms)
Execution time std-deviation : 1.057659 ms 95.0% CI: (1.050688 ms, 1.062877 ms)
Execution time lower ci : 1.024195 ms 95.0% CI: (1.024164 ms, 1.024195 ms)
Execution time upper ci : 1.145664 ms 95.0% CI: (1.145664 ms, 1.145741 ms)
Found 1 outliers in 60 samples (1.6667 %)
low-severe 1 (1.6667 %)
Variance from outliers : 19.0208 % Variance is moderately inflated by outliers
nil
这个报告是不是相当专业?不是搞统计还不一定读的懂。大概解读下,sample-count是取样次数,默认是60次,exec-count是测试的执行次数(不包括前期warm up和JIT在内),CI是可信区间,这里取95%,Execution time mean是平均执行时间,而lower和upper是测试过程中执行时间的最小和最大值。而outliers是这一组测试中的异常值,比如执行sum1测试发现了2组异常结果。从结果来看,sum1的平均执行时间是722微秒,而sum2的平均执行时间是1.07毫秒,因此还是sum1更快一些。
总结下,如果只是在开发过程中做一些小块代码的简单测试,可以直接利用内置的time宏,如果你希望做一次比较标准的性能测试,那么就应该利用 criterium这个优秀的开源库。
继续Clojure世界之旅,介绍下我今天的探索成果,使用clojure生成clojure项目的API文档。在java里,我们是利用javadoc生成API文档,各种build工具都提供了集成,例如maven和ant都提供了javadoc插件或者task。在Clojure世界里,同样有一系列工具帮助你从源码中自动化生成API文档。今天主要介绍三个工具。不过我不会介绍怎么在clojure里写doc,具体怎么做请看一些开源项目,或者直接看clojure.core的源码。
首先是 codox,使用相当简单,我们这里都假设你使用 Leiningen作为构建工具,在project.clj里添加codox依赖:
:dev-dependencies [[codox "0.5.0"]]
解下执行 lein doc命令即可生成文档,生成的文档放在doc目录,在浏览器里打开index.html即可观察生成的文档。我给 clojure-control生成的codox文档可以看 这个链接,效果还是不错的。
第二个要介绍的工具是 marginalia,使用方法类似codox,首先还是添加依赖:
:dev-dependencies [lein-marginalia "0.7.0"]
执行lein deps处理依赖关系,然后执行 lein marg命令即可在docs目录生成文档,与codox不同的是marginalia只生成一个html文件,没有带js和css,但是效果也不错,可以看我给clojure-control生成的marg文档链接。marginalia生成的文档说明和源码左右对照,很利于阅读源码。
最后要介绍的就是Clojure.org自己在使用的autodoc,如果你喜欢clojure.org上的API文档格式可以采用这个工具。并且autodoc可以跟github pages结合起来,生成完整的项目文档并展示在github上。例如以clojure-control为例来介绍整个过程。首先你需要配置你的github pages,参照这个链接http://pages.github.com/。
第一步,仍然是在project.clj添加依赖:
:dev-dependencies [[lein-autodoc "0.9.0"]]
第二步,在你的.gitignore里忽略autodoc目录:
autodoc/**
将这些更改提交到github上,接下来在你的项目目录clone一份项目源码到<project>/autodoc目录:
git clone git@github.com:<user name>/<project name>.git autodoc
进入autodoc目录,执行下列命令创建一个 gh-pages分支:
$ cd autodoc
$ git symbolic-ref HEAD refs/heads/gh-pages
$ rm .git/index
$ git clean -fdx
$ cd ..
回到项目根目录后,执行 lein autodoc命令在autodoc目录生成文档:
lein autodoc
接下来将生成的文档推送到github分支上:
$cd autodoc
$ git add -A
$ git commit -m"Documentation update"
$ git push origin gh-pages
等上几分钟,让github渲染你的文档,最终的效果看这个链接 http://killme2008.github.com/clojure-control
autodoc和 marginalia都支持maven,具体使用请看他们的文档。
前面一篇博客介绍了我在github上的一个 metaq分支,今天下午写了个metaq的python客户端,目前仅支持发送消息功能,不过麻雀虽小,五脏俱全,客户端和zookeeper的交互和连接管理之类都还具备,不出意外,我们会首先用上。第一次正儿八经地写python代码,写的不好的地方请尽管拍砖,多谢。 项目叫meta-python,仍然放在github上: https://github.com/killme2008/meta-python
使用需要先安装zkpython这个库,具体安装 这篇博客,使用很简单,发送消息: from metamorphosis import Message,MessageProducer,SendResult p=MessageProducer("topic") message=Message("topic","message body") print p.send(message) p.close() MessageProducer就是消息发送者,它的构造函数接受至少一个topic,默认的zk_servers为localhost:2181,可以通过zk_servers参数指定你的zookeeper集群: p=MessageProducer("topic",zk_servers="192.168.1.100:2191,192.168.1.101:2181") 更多参数请直接看源码吧。一个本机的性能测试(meta和客户端都跑在我的机器上,机器是Mac MC700,osx 10.7,磁盘没有升级过):
from metamorphosis import Message,MessageProducer from time import time p=MessageProducer("avos-fetch-tasks") message=Message("avos-fetch-tasks","http://www.taobao.com") start=time() for i in range(0,10000): sent=p.send(message) if not sent.success: print "send failed" finish=time() secs=finish-start print "duration:%s seconds" % (secs) print "tps:%s msgs/second" % (10000/secs) p.close() 结果:
duration:1.85962295532 seconds tps:5377.43415749 msgs/second
开源的memcached Java客户端——xmemcached发布1.3.6版本。
主要改进如下:
1. 为MemcachedClientBuilder添加两个新方法用于配置:
public void setConnectTimeout(long connectTimeout);
public void setSanitizeKeys(boolean sanitizeKeys);
2. 用于hibernate的XmemcachedClientFactoryd添加了connectTimeout属性,感谢网友 Boli.Jiang的贡献。
3. 添加新的枚举类型 net.rubyeye.xmemcached.transcoders.CompressionMode,用于指定Transcoder的压缩类型,默认是ZIP压缩,可选择GZIP压缩。Transcoder接口添加setCompressionMode方法。
4. 修改心跳规则,原来是在连接空闲的时候发起心跳,现在变成固定每隔5秒发起一次心跳检测连接。
5. 修改默认参数,默认禁用nagle算法,默认将批量get的合并因子下降到50。
6. 修复bug和改进,包括:161、163、165、169、172、173、176、179和180。
项目主页:http://code.google.com/p/xmemcached/
项目文档:http://code.google.com/p/xmemcached/w/list
下载:http://code.google.com/p/xmemcached/downloads/list
源码:https://github.com/killme2008/xmemcached
Maven依赖:
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>1.3.6</version>
</dependency>
最后感谢所有提出issue和改进意见的朋友们。
加入avos.com已经一个月多一点,很荣幸加入这个充满活力的跨国团队。去了趟北京,跟我的同事们终于当面认识了下,并且顺利举办了cn-clojure的第二次聚会。不过很悲剧的是在北京一周都在生病,哪也没去成,要见的人也没见到,要凑的饭局也没吃到,非常遗憾,希望以后再弥补。不过我暂时因为家庭的因素还在杭州远程办公,沟通都是通过gtalk和google+ hangout,很多东西对我来说都是全新的经历。远程办公有利有弊,其实我还是更喜欢能在一个办公室里工作,交流方便,只要转个椅子就好。 正式介绍下我们公司,大boss是youtube创始人查德•赫利(Chad Hurley)和陈士骏(Steve Chen),我们在中国的boss是前google员工/耶鲁的博士 江宏帅哥,更多关于团队成员的介绍请看 这里,我的同事们真的很强大,我要学习的地方很多。我们做的产品是从雅虎手上买下来的delicious.com,不过中国团队运作的是美味书签—— mei.fm。我们刚在3月1号开始做public beta,预计会在4月份的时候正式对外开放注册。我们的团队博客在 这里。 我在团队里做的事情还是偏向后端,这一个月来做的事情更偏向运维之类,搞搞solr复制、mysql复制、程序监控之类,将原来只是简单了解过的东西动手做了一遍,能亲手实践的感觉不错。在此过程中要感谢锋爷和刘帅哥的帮助,再次感谢。淘宝的同事们开源了metaq和gecko,我也做了点工作,都在 这里。几个维护的开源项目都没有太大进展,很惭愧,还被人催发新版本,能承诺的是周末发xmemcached的新版本,主要还是修bug。本来要写个clojure世界的系列文章,因为响应寥寥,也不是很有动力写下去。杂七杂八读了几本书,都没读完,这一个月杭州太冷了,雨下个不停,不过终于这一周开始晴天了,希望老天爷别再掉眼泪。 收拾收拾心情,整装待发,希望新的一年里能做出点不同的东西。
上周我在淘宝的同事开源了一个消息中间件 metamorphosis,放在了 淘蝌蚪上。我从淘蝌蚪的svn上fork了一个github的分支,放在了这里:
1.主体工程: https://github.com/killme2008/Metamorphosis
主要做了一些pom文件的简化,发布1.4.0.2版本到maven central仓库,并且写了几个简单的入门文档,提供了一个完整打包可运行的下载,有兴趣的自己看github页面吧。 Wiki文档放在:
https://github.com/killme2008/Metamorphosis/wiki
客户端Maven依赖包括,可自行选择添加:
<dependency>
<groupId>com.taobao.metamorphosis</groupId>
<artifactId>metamorphosis-client</artifactId>
<version>1.4.0.2</version>
</dependency>
<dependency>
<groupId>com.taobao.metamorphosis</groupId>
<artifactId>metamorphosis-client-extension</artifactId>
<version>1.4.0.2</version>
</dependency>
<dependency>
<groupId>com.taobao.metamorphosis</groupId>
<artifactId>storm-metamorphosis-spout</artifactId>
<version>1.0.0</version>
</dependency>
ps.我开通了新浪微博,有兴趣相互关注下: http://weibo.com/fnil,你看,偏见是可以改变的。
Clojure-control is a clojure DSL for system admin and deployment with many remote machines via ssh. I am pleased to annoucment that clojure-control 0.3.0 is out.It adds some powerful features in this release ,includes:
- ssh and scp both have a new option :sudo to be executed as root on remote machines,for example:
(ssh "/etc/init.d/ssh restart" :sudo true)
- scp has a new option :mode to change file modes copied from local:
(scp "start.sh" "/etc/init.d/start.sh" :sudo true :mode 755)
- A new function "exists?" to test if a file exists on remote machines:
(if (not (exists? (str "/home/deploy/.ssh")))
(ssh (sudo (str "mkdir -p /home/deploy/.ssh"))))
- Call other task in deftask with "call" function:
(deftask :ps "A task to grep process" [process]
(ssh (str "ps aux | grep " process)))
(deftask :start_ha []
(ssh "/etc/init.d/haproxy start")
(call :ps "haproxy"))
- A new function "append" to append a line to a file on remote machines:
(ssh (append "/etc/hosts" "192.168.1.100 web" :sudo true))
- A new function "sed" to replace lines in a file on remote machines,and comm/uncomm to comment/uncomment a line in a file:
(sed <file> <before> <after> :flags <flags> :limit <limit> :backup <backup>)
Equivalent to
sed -i<backup> -r -e "<limit> s/<before>/<after>/<flags>g <filename>"
Limits max output line to 10000.
Adds more documents in wiki: https://github.com/killme2008/clojure-control/wiki You can install the new version by :
lein plugin install control 0.3.0 #For clojure 1.3
lein plugin install control 0.3.1 #For clojure 1.2 More information please visit it on github: https://github.com/killme2008/clojure-control
XML处理也是个常见的编程工作,虽然说在Clojure里你很少使用XML做配置文件,但是跟遗留系统集成或者处理和其他系统通讯,可能都需要处理XML。 Clojure的标准库clojure.xml就是用来干这个事情的。一个简单的例子如下,首先我们要解析的是下面这个简单的XML: <?xml version="1.0" encoding="UTF-8"?> <books> <book> <title>The joy of clojure</title> <author>Michael Fogus / Chris House</author> </book> <book> <title>Programming clojure</title> <author>Stuart Halloway</author> </book> <book> <title>Practical clojure</title> <author>Luke Van der Hart</author> </book> </books> 解析xml用clojure.xml/parse方法即可,该方法返回一个clojure.xml/element这个struct-map组成的一棵树: user=> (use '[clojure.xml]) nil user=> (parse "test.xml") {:tag :books, :attrs nil, :content [{:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["The joy of clojure"]} {:tag :author, :attrs nil, :content ["Michael Fogus / Chris House"]}]} {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Programming clojure"]} {:tag :author, :attrs nil, :content ["Stuart Halloway"]}]} {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Practical clojure"]} {:tag :author, :attrs nil, :content ["Luke Van der Hart"]}]}]} 这是一个嵌套的数据结构,每个节点都是clojure.xml/element结构,element包括: (defstruct element :tag :attrs :content) tag、attrs和content属性,tag就是该节点的标签,attrs是一个属性的map,而content是它的内容或者子节点。element是一个struct map,它也定义了三个方法来分别获取这三个属性: user=> (def x (parse "test.xml")) #'user/x user=> (tag x) :books user=> (attrs x) nil user=> (content x) [{:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["The joy of clojure"]} {:tag :author, :attrs nil, :content ["Michael Fogus / Chris House"]}]} {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Programming clojure"]} {:tag :author, :attrs nil, :content ["Stuart Halloway"]}]} {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Practical clojure"]} {:tag :author, :attrs nil, :content ["Luke Van der Hart"]}]}] books节点是root node,它的content就是三个book子节点,子节点组织成一个vector,我们可以随意操作: user=> (tag (first (content x))) :book user=> (content (first (content x))) [{:tag :title, :attrs nil, :content ["The joy of clojure"]} {:tag :author, :attrs nil, :content ["Michael Fogus / Chris House"]}] user=> (content (first (content (first (content x))))) ["The joy of clojure"] 额外提下,clojure.xml是利用SAX API做解析的。同样它还有个方法,可以将解析出来的结构还原成xml,通过emit: user=> (emit x)
<?xml version='1.0' encoding='UTF-8'?> <books> <book> <title> The joy of clojure </title> <author> Michael Fogus / Chris House </author> </book> <book>
 如果你要按照深度优先的顺序遍历xml,可以利用xml-seq将解析出来的树构成一个按照深度优先顺序排列节点的LazySeq,接下来就可以按照seq的方式处理,比如利用for来过滤节点: user=> (for [node (xml-seq x) :when (= :author (:tag node))] (first (:content node))) ("Michael Fogus / Chris House" "Stuart Halloway" "Luke Van der Hart") 通过:when指定了条件,要求节点的tag是author,这样就可以查找出所有的author节点的content,是不是很方便?就像写英语描述一样。 更进一步,如果你想操作parse解析出来的这棵树,你还可以利用clojure.zip这个标准库,它有xml-zip函数将xml转换成zipper结构,并提供一系列方法来操作这棵树: user=>(def xz (xml-zip x)) #'user/xz user=> (node (down xz)) {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["The joy of clojure"]} {:tag :author, :attrs nil, :content ["Michael Fogus / Chris House"]}]} user=> (-> xz down right node) {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Programming clojure"]} {:tag :author, :attrs nil, :content ["Stuart Halloway"]}]} user=> (-> xz down right right node) {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Practical clojure"]} {:tag :author, :attrs nil, :content ["Luke Van der Hart"]}]} user=> (-> xz down right right lefts) ({:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["The joy of clojure"]} {:tag :author, :attrs nil, :content ["Michael Fogus / Chris House"]}]} {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Programming clojure"]} {:tag :author, :attrs nil, :content ["Stuart Halloway"]}]})
是不是酷得一塌糊涂?可以通过up,down,left,right,lefts,rights,来查找节点的邻近节点,可以通过node来得到节点本身。一切显得那么自然。更进一步,你还可以“编辑“这棵树,比如删除The joy of clojure这本书: user=> (def loc-in-new-tree (remove (down xz))) #'user/loc-in-new-tree user=> (root loc-in-new-tree) {:tag :books, :attrs nil, :content [{:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Programming clojure"]} {:tag :author, :attrs nil, :content ["Stuart Halloway"]}]} {:tag :book, :attrs nil, :content [{:tag :title, :attrs nil, :content ["Practical clojure"]} {:tag :author, :attrs nil, :content ["Luke Van der Hart"]}]}]} ok,只剩下两本书了,更多方法还包括replace做替换,edit更改节点等。因此编辑XML并重新生成,你一般可以利用clojure.zip来更改树,最后利用clojure.xml/emit将更改还原为xml。 生成xml除了emit方法,还有一个contrib库,也就是 prxml,这个库的clojure 1.3版本有人维护了一个分支,在 这里。主要方法就是prxml,它可以将clojure的数据结构转换成xml: user=>(prxml [:p [:raw! "<i>here & gone</i>"]]) <p><i>here & gone</i></p> 显然,它也可以用于生成HTML。 xpath的支持可以使用 clj-xpath这个开源库,遗憾的是它目前仅支持clojure 1.2。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/18/370233.html
文件读写是日常编程中最经常使用的操作之一。这篇blog将大概介绍下Clojure里对文件操作的常用类库。 首先介绍标准库 clojure.java.io,这是最经常用的IO库,定义了常见的IO操作。 首先,直接看一个例子,可以熟悉下大多数常用的函数: (ns io (:use [clojure.java.io]))
;;file函数,获取一个java.io.File对象 (def f (file "a.txt"))
;;拷贝文件使用copy (copy f (file "b.txt"))
;;删除文件,使用delete-file (delete-file f)
;;更经常使用reader和writer (def rdr (reader "b.txt" :encoding "utf-8")) (def wtr (writer "c.txt" :append true))
;;copy可以接受多种类型的参数 (copy rdr wtr :buffer-size 4096)
;;关闭文件 (.close wtr) (.close rdr)
这个例子基本上说明了大多数常见的操作。但是有些问题需要解释下。 首先,通过file这个函数可以将各种类型的对象转化为java.io.File对象,file可以接受String,URL,URI以及java.io.File本身作为参数,并返回java.io.File。有了File对象,你就可以调用java.io.File类中的各种方法,比如判断文件是否存在: (.exists (file "a.txt")) => true or false 其次,可以通过delete-file来删除一个文件,它是调用File的delete方法来执行的,但是File.delete会返回一个布尔值告诉你成功还是失败,如果返回false,那么delete-file会抛出IO异常,如果你不想被这个异常打扰,可以让它“保持安静”: (delete-file f true) 拷贝文件很简单,使用copy搞定,copy也可以很“宽容”,也可以接受多种类型的参数并帮你自动转换,input可以是InputStream, Reader, File, byte[] 或者String,而output可以是OutputStream, Writer或者File。是不是很给力?这都是通过Clojure的protocol和defmulti做到的。但是,copy并不帮你处理文件的关闭问题,假设你传入的input是一个File,output也是一个File,copy会自动帮你打开InputStream和OutputStream并建立缓冲区做拷贝,但是它不会帮你关闭这两个流,因此你要小心,如果你经常使用copy,这可能是个内存泄漏的隐患。 更常用的,我们一般都是用reader和writer函数来打开一个BufferedReader和BufferedWriter做读写,同样reader和writer也可以接受多种多样的参数类型,甚至包括Socket也可以。因为writer打开的通常是一个BufferedWriter,所以你如果用它写文件,有时候发现write之后文件还是没有内容,这是因为数据暂时写到了缓冲区里,没有刷入到磁盘,可以明确地调用(.flush wtr)来强制写入;或者在wtr关闭后系统帮你写入。reader和writer还可以传入一些配置项,如:encoding指定读写的字符串编码,writer可以指定是否为append模式等。 Clojure并没有提供关闭文件的函数或者宏,你简单地调用close方法即可。clojure.java.io的设计很有原则,它不准备将java.io都封装一遍,而是提供一些最常用方法的简便wrapper供你使用。 刚才提到copy不会帮你关闭打开的文件流,但是我们可以利用with-open这个宏来自动帮你管理打开的流: (with-open [rdr (reader "b.txt") wtr (writer "c.txt")] (copy rdr wtr)) with-open宏会自动帮你关闭在binding vector里打开的流,你不再需要自己调用close,也不用担心不小心造成内存泄漏。因此我会推荐你尽量用reader和writer结合with-open来做文件操作,而不要使用file函数。file函数应该用在一些判断是否存在,判断文件是否为目录等操作上。 在clojure.core里,还有两个最常用的函数slurp和spit,一个吃,一个吐,也就是slurp读文件,而spit写文件,他们类似Ruby的File里的read和write,用来完整地读或者写文件: (prn (slurp "c.txt")) (spit "c.txt" "hello world") 用法简单明了,slurp将文件完整地读出并返回字符串作为结果,它还接受:encoding参数来指定字符串编码,你猜的没错,它就是用reader和with-open实现的。spit同样很简单,将content转化为字符串写入文件,也接受:encoding和:append参数。 深度优先遍历目录,可以使用file-seq,返回一个深度优先顺序遍历的目录列表,这是一个LazySeq: (user=> (file-seq (java.io.File. "."))
(#<File .> #<File ./.gitignore> #<File ./.lein-deps-sum> #<File ./b.txt> #<File ./c.txt> #<File ./classes> ⋯⋯ )
上面的介绍已经足以让你对付大多数需求了。接下来,介绍下几个开源库。首先是 fs这个库,它封装了java.io.File类的大多数方法,让你用起来很clojure way,很舒服,例如: (exists? "a.txt") (directory? "file") (file? "file") (name "/home/dennis/.inputrc") (mkdir "/var/data") (rename "a.txt" "b.txt") (def tmp (temp-dir)) (glob #".*test.*") (chmod 744 "a.txt")
⋯⋯ 更多介绍请看它的 源码。读写二进制文件也是一个很常见的需求,Clojure有几个DSL库干这个事情,可以很直观地定义二进制格式来encode/decode,比如 byte-spec这个库,看看它的例子: defspec basic-spec :a :int8 :b :int16 :c :int32 :d :float32 :e :float64 :f :string)
;; An object to serialize (def foo {:a 10 :b 20 :c 40 :d 23.2 :e 23.2 :f "asddf"})
;; And serialize it to a byte array like this: (spec-write-bytes basic-spec foo) ;; => [ bytes ]
;; reading in a byte array with the basic-spec format works like this: (spec-read-bytes basic-spec bytes) 相当直观和给力吧。 Gloss是一个更强大的DSL库,非常适合做网络通讯的协议处理。这里就不多做介绍了,你可以自己看它的例子和文档。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/16/370144.html
单元测试也是一个开发中最常见的需求,在Java里我们用JUnit或者TestNG,在clojure里也内置了单元测试的库。标准库的 clojure.test,以及第三方框架 midje。这里我将主要介绍clojure.test这个标准库,midje是个更加强大的测试框架,广告下,midje的介绍在第二次cn-clojure聚会上将有个Topic,我就不画蛇添足了。通常来说,clojure.test足够让你对付日常的测试。 首先看一个最简单的例子,定义一个函数square来计算平方,然后我们测试这个函数: ;;引用clojure.test (ns example (:use [clojure.test :only [deftest is run-tests]])) ;;定义函数 (defn square [x] (* x x)) ;;测试函数 (deftest test-square (is (= 4 (square 2))) (is (= 9 (square -3)))) ;;运行测试 (run-tests 'example) 执行输出: Testing example
Ran 1 tests containing 2 assertions. 0 failures, 0 errors.
这个小例子基本说明了clojure.test的主要功能。 首先是断言is,类似JUnit里的assertTrue,用来判断form是否为true,它还可以接受一个额外的msg参数来描述断言: (is (= 4 (square 2)) "a test") 它还有两种变形,专门用来判断测试是否抛出异常: (is (thrown? RuntimeException (square "a"))) (is (thrown-with-msg? RuntimeException #"java.lang.String cannot be cast to java.lang.Number" (square "a"))) 上面的例子故意求"a"的平方,这会抛出一个java.lang.ClassCastException,一个运行时异常,并且异常信息为java.lang.String cannot be cast to java.lang.Number。我们可以通过上面的方式来测试这种意外情况。clojure.test还提供了另一个 断言are,用来判断多个form: (testing "test zero or one" (are (= 0 (square 0)) (= 1 (square 1)))) are接受多个form并判断是否正确。这里还用了testing这个宏来添加一段字符串来描述测试的内容。 其次,我们用deftest宏定义了一个测试用例,deftest定义的测试用例也可以组合起来: (deftest addition (is (= 4 (+ 2 2))) (is (= 7 (+ 3 4)))) (deftest subtraction (is (= 1 (- 4 3))) (is (= 3 (- 7 4)))) (deftest arithmetic (addition) (subtraction)) 但是组合后的tests运行就不能简单地传入一个ns,而需要定义一个test-ns-hook指定要跑的测试用例,否则组合的用例如上面的addition和subtraction会运行两次。我们马上谈到。 定义完用例后是运行测试,运行测试使用run-tests,可以指定要跑测试的ns,run-tests接受可变参数个的ns。刚才提到,组合tests的时候会有重复运行的问题,要防止重复运行,可以定义一个 test-ns-hook的函数: (defn test-ns-hook [] (test-square) (arithmetic)) 这样run-tests就会调用test-ns-hook按照给定的顺序执行指定的用例,避免了重复执行。 在你的测试代码里明确调用run-tests执行测试是一种方式,不过我们在开发中更经常使用的是 lein来管理project, lein会将src和test分开,将你的测试代码组织在专门的test目录,类似使用maven的时候我们将main和test分开一样。这时候就可以简单地调用: lein test 命令来执行单元测试,而不需要明确地在测试代码里调用run-tests并指定ns。更实用的使用例子可以看一些开源项目的组织。 单元测试里做mock也是比较常见的需求,在clojure里做mock很容易,原来clojure.contrib有个mock库,基本的原理都是利用binding来动态改变被mock对象的功能,但是在clojure 1.3里,binding只能改变标注为dynamic的变量,并且clojure.contrib被废弃,部分被合并到core里面,Allen Rohner编译了一个可以用于clojure 1.3的clojure.contrib,不过需要你自己install到本地仓库,具体看 这里。不过clojure.contrib.mock哪怕使用1.2的编译版本其实也是可以的。 clojure.contrib最重要的是expect宏,它类似EasyMock里的expect方法,看一个例子: (use [clojure.contrib.mock :only [times returns has-args expect]])
(deftest test-square2 (expect [square (has-args [number?] (times 2 (returns 9)))] (is (= 9 (square 4))))) has-args用来检测square的参数是不是number,times用来指定预期调用的次数,而returns用来返回mock值,是不是很像EasyMock?因为我们这个测试只调用了square一次,所以这个用例将失败: Testing example "Unexpected invocation count. Function name: square expected: 2 actual: 1" 这个例子要在Clojure 1.3里运行,需要将square定义成dynamic: (defn ^:dynamic square [x] (* x x)) 否则会告诉你没办法绑定square: actual: java.lang.IllegalStateException: Can't dynamically bind non-dynamic var: example/square 额外提下,还有个轻量级的测试框架 expections可以看一下,类似Ruby Facker的 facker库提供一些常见的模拟数据,如名称地址等。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/15/370040.html
Clojure的REPL非常方便,可以随时随地试验你的想法,REPL是read-eval-print-loop的简称。默认clojure.contrib有带一个shell脚本来启动REPL,具体看 这里。你也可以用JLine来增强REPL:
java -cp "%CLOJURE_DIR%\jline-VERSION.jar;%CLOJURE_JAR%" jline.ConsoleRunner clojure.main
不过,其实你还可以用 rlwrap这个GNU库来增强clojure REPL。使用它有如下好处:
1.Tab completion,使用tab做代码提示。
2.括号匹配
3.历史记录,哪怕你重启REPL
4.通过 .inputrc来绑定vi或者emacs
具体操作步骤如下:
1.首先,你需要在你的机器上安装rlwrap,你可以通过apt或者port,homebrew等工具安装或者自己下载安装:
sudo port install rlwrap
2.在你的home目录下创建一个clojure目录作为clojure home,并拷贝clojure.jar进去:
mkdir ~/clojure
cp .m2/repository/org/clojure/clojure/1.3.0/clojure-1.3.0.jar ~/clojure/clojure.jar
我是从maven的本地仓库里拷贝了clojure 1.3的jar包过去,重命名为clojure.jar
3.创建一个shell脚本名为clj,并放入你的path变量,脚本内容:
#!/bin/sh
breakchars="(){}[],^%$#@\"\";:''|\\"
CLOJURE_DIR=~/clojure
CLOJURE_JAR="$CLOJURE_DIR"/clojure.jar
JAVA_OPTS="-Xmx512m -XX:MaxPermSize=256m -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:+CMSClassUnloadingEnabled"
if [ $# -eq 0 ]; then
exec rlwrap --remember -c -b "$breakchars" \
-f "$HOME"/.clj_completions \
-t "Clojure REPL" \
-p red \
-H "$CLOJURE_DIR"/.repl_history -s 1000\
java "$JAVA_OPTS" -cp "$CLOJURE_JAR" clojure.main
else
exec java -cp "$CLOJURE_JAR" clojure.main $1 "$@"
fi
我们将命令历史输出到~/clojure/.repl_history文件中,并限制数目为1000。
4.clj脚本中通过-f选项指定了completions文件为~/.clj_completions,执行下列clojure程序生成此文件:
(def completions (keys (ns-publics (find-ns 'clojure.core))))
;(def completions (mapcat (comp keys ns-publics) (all-ns)))
(with-open [f (java.io.BufferedWriter. (java.io.FileWriter. (str (System/getenv "HOME") "/.clj_completions")))]
(.write f (apply str (interpose \newline completions))))
这个程序只生成clojure.core的completions文件,如果你想将所有ns都加入进去,注释掉第一行,使用第二行程序。
5.最后,配置下~/.inputrc文件:
set editing-mode emacs
tab: complete
set completion-query-items 150
set completion-ignore-case on
set blink-matching-paren on
set bell-style visible
我绑定为emacs,你可以选择vi。
6.一切搞定,接下来你可以敲入命令clj来使用rlwrap启动clojure REPL了,可以用tab做代码提示了,可以用Ctrl + r来搜索历史命令,运行截图:
参考: http://en.wikibooks.org/wiki/Clojure_Programming/Getting_Started#Enhancing_Clojure_REPL_with_rlwrap转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/14/369976.html
使用http client提交表单或者下载网页也是非常常见的任务,比如使用Java的时候可以用标准库的HttpURLConnection,也可以选择 Apache Http Client。在clojure里也有这样的类库,这里我将介绍三个各有特色的http client实现。 首先,我最先推荐使用clj-http这个类库,它是Apache HttpClient的clojure wrapper,是一个提供同步API的简单易用的Http Client。 名称: clj-http 主页: https://github.com/dakrone/clj-http依赖: [clj-http "0.3.1"] 例子: (require '[clj-http.client :as client]) (client/get "http://google.com") 结果: => {:cookies {"NID" {:domain ".google.com.hk", :expires #<Date Tue Aug 14 18:20:38 CST 2012>, :path "/", :value "56=qn2OWtODE2D3fUKi_vbi44jZepOeLI9xC4Ta1JQLEicqUvIZAqr7TCmft_hq8i_FRwnFXdTK1jV2S5IrSZFyYhlAN2KcQEXgWX1iK36gM2iYPaKPihuUZDCqgiAamDOl", :version 0}, "PREF" {:domain ".google.com.hk", :expires #<Date Wed Feb 12 18:20:38 CST 2014>, :path "/", :value "ID=8b73a654ff0a2783:FF=0:NW=1:TM=1329128438:LM=1329128438:S=uEM4SsFuHlkqtVhp", :version 0}}, :status 200 :headers {"date" "Sun, 01 Aug 2010 07:03:49 GMT" "cache-control" "private, max-age=0" "content-type" "text/html; charset=ISO-8859-1" } :body "<!doctype html> " :trace-redirects ["http://google.com" "http://www.google.com/" "http://www.google.fr/"]} 更多例子: (client/get "http://site.com/resources/3" {:accept :json})
;; Various options: (client/post "http://site.com/api" {:basic-auth ["user" "pass"] :body "{\"json\": \"input\"}" :headers {"X-Api-Version" "2"} :content-type :json :socket-timeout 1000 :conn-timeout 1000 :accept :json})
;; Need to contact a server with an untrusted SSL cert? (client/get "https://alioth.debian.org" {:insecure? true})
;; If you don't want to follow-redirects automatically: (client/get "http://site.come/redirects-somewhere" {:follow-redirects false})
;; Only follow a certain number of redirects: (client/get "http://site.come/redirects-somewhere" {:max-redirects 5})
;; Throw an exception if redirected too many times: (client/get "http://site.come/redirects-somewhere" {:max-redirects 5 :throw-exceptions true})
;; Send form params as a urlencoded body (client/post "http//site.com" {:form-params {:foo "bar"}})
;; Multipart form uploads/posts ;; a map or vector works as the multipart object. Use a vector of ;; vectors if you need to preserve order, a map otherwise. (client/post "http//example.org" {:multipart [["title" "My Awesome Picture"] ["Content/type" "image/jpeg"] ["file" (clojure.java.io/file "pic.jpg")]]}) ;; Multipart values can be one of the following: ;; String, InputStream, File, or a byte-array
;; Basic authentication (client/get "http://site.com/protected" {:basic-auth ["user" "pass"]}) (client/get "http://site.com/protected" {:basic-auth "user:pass"})
;; Query parameters (client/get "http://site.com/search" {:query-params {"q" "foo, bar"}}) clj-http的API相当的简洁漂亮,使用起来非常便利,强烈推荐。题外,学习clojure的一个好方法就是为现有的java类库实现一些方便的clojure wrapper。 如果你需要异步的http client,我会推荐http.async.client这个类库,它的API是异步形式的类似 Java的Future模式,对于clojure程序员来说应该更像是agent。 名称:http.async.client 主页: https://github.com/neotyk/http.async.client依赖: [http.async.client "0.4.1"] 例子: (require '[http.async.client :as c]) (with-open [client (c/create-client)] (let [response (c/GET client "http://neotyk.github.com/http.async.client/")] (prn (c/done? response)) (c/await response) (prn (c/string response)) (prn (c/status response)) (prn (c/done? response)))) 输出: false <!DOCTYPE html   {:code 200, :msg "OK", :protocol "HTTP/1.1", :major 1, :minor 1} true 更多例子: (c/POST client "http://example.com" :body "hello world" :timeout 3000) (c/DELETE client "http://example.com") (c/POST client "http://example.com" :body "hello world" :auth {:type :basic :user "admin" :password "admin"}) 请注意,这些方法都是异步调用的,你需要通过await来等待调用完成,或者通过done?来判断调用是否完成。 http.async.client有个比较重要的特性就是对Http Chunked编码的支持,分别通过LazySeq和callback的方式支持,首先看将Http chunked变成一个lazy seq: (with-open [client (client/create-client)] ; Create client (let [resp (client/stream-seq client :get url)] (doseq [s (s/string resp)] (println s)))) 这里非常关键的一点是stream-seq返回的chunk序列,每取一个就少一个(通过first函数),也就是说每次调用first取到的chunk都不一样,是顺序递增,不可重复获取的。 通过callback方式处理: (with-open [client (client/create-client)] ; Create client (let [parts (ref #{}) resp (client/request-stream client :get url (fn [state body] (dosync (alter parts conj (string body))) [body :continue]))] ;; do something to @parts )) 自己传入一个callback函数接收chunk,比如这里用一个ref累积。 http.async.client的详细文档看这里: http://neotyk.github.com/http.async.client/docs.html最后,有兴趣还可以看下 aleph这个异步通讯的框架,它支持Http协议,也提供了http server和client的实现。不过它的API就没有那么简单明了,它的模型是类似go语言里利用channel做异步通讯的模型,http只是它的一个模块罢了,这是另一个话题了。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/13/369890.html
处理日志是任何一个产品级的程序都需要仔细处理的模块。在Java中,我们经常使用的是log4j就是一个日志框架。在clojure里,同样有一套日志框架——clojure.tools.logging,它不仅提供了常用的日志输出功能,还屏蔽了Java各种日志框架之间的差异,如slf4j,commons-logging,log4j,java.util.logging等,让你可以透明地使用这些框架来处理日志。 名称:clojure.tools.logging 主页: https://github.com/clojure/tools.logging依赖: [org.clojure/tools.logging "0.2.3"]
<dependency> <groupId>org.clojure</groupId> <artifactId>tools.logging</artifactId> <version>0.2.3</version> </dependency> 使用: (ns example.core (:use [clojure.tools.logging :only (info error)]))
(defn divide [x y] (try (info "dividing" x "by" y) (/ x y) (catch Exception ex (error ex "There was an error in calculation"))))
常用宏和方法: 1.除了上面例子的info和error宏,还可以包括warn,trace,debug,fatal等常用宏,分别对应相应的日志级别。这些方法会自动判断当前logger的级别是否有效,有效的前提下才会输出日志。也就是说在Java里,你经常需要这样: if (logger.isDebugEnabled()) { logger.debug(x + " plus " + y + " is " + (x + y)); } 在使用 tools.logging的时候是不需要的,因为这些宏帮你做了这个判断。另外,我们在使用log4j的时候需要指定log的namespace,在tools.logging里不需要,默认会取当前的namespace也就是*ns*。 最后,info还有个infof的方法,用于输出格式化日志: (infof "%s is %d years old" "kid" 3) 日志输出: 2012-02-12 20:23:07,394 INFO log: kid is 3 years old 其他方法也有类似的如warnf,debugf等。 2.spy宏,同时输出表达式的form和结果,例如 (spy (+1 2)) 输出日志 2012-02-12 20:11:47,415 DEBUG log: (+ 1 2) => 3 3.with-logs宏可以在将*out*和*err*流重定向到日志的情况下求值表达式,例如: (with-logs *ns* (prn "hello world")) 输出日志: 2012-02-12 20:17:32,592 INFO log: "hello world" with-logs需要明确指定log-ns,默认out的输出级别是info,而err的级别是error,可以指定输出级别(with-logs [*ns* :info :error] ......) 4.事务中(dosync中)的日志输出,tools.logging做了特殊处理,默认情况下当且仅当事务成功提交的时候并且日志级别是warn或者info会通过agent异步写入日志。tools.logging定义了一个全局的agent——*logging-agent*。当判断当前是在事务中调用log宏,并且日志级别在集合*tx-agent-levels*内,就会在事务提交成功的时候将日志发送给*logging-agent*异步处理。可以通过*tx-agent-levels*改变使用agent输出日志的级别范围,默认是#{:info :warn}。还可以通过改变*force*变量来强制使用direct或者agent的方式输出日志,*force*可以为:agent或者:direct。 (binding [*force* :agent] (log :info "hello world")) 这里特别使用了log宏,需要明确指定日志级别为info。 5.默认日志框架的是从classpath查找的,查找的顺序是sl4j,commons-logging,log4j,java.util.logging,找到哪个可用就用哪个。如果你的classpath里存在多个日志框架,如同时存在sl4j和commons-logging,那么如果你希望强制使用commons-logging,可以通过改变*logger-factory*变量来使用: (ns example (:use [clojure.tools.logging.impl :only [cl-factory]])) (binding [*logger-factory* (cl-factory)] (info "hello world"))
*logger-factory*是dynamic变量,可以通过binding改变(前面提到的*force*等变量也一样),如果不希望每次都用binding,而是全局改变,则需要特殊处理: (alter-var-root (var *logger-factory*) (constantly (cl-factory)))
其他logger factory还包括slf4j-factory,log4j-factory,jul-factory。 6.每个日志框架的配置跟使用java没有什么两样,比如你用log4j,就需要在classpath下放置一个log4j.properties等。如果你希望用编程的方式配置,可以使用 clj-logging-config。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/12/369822.html
年前一篇blog提过,写了一个stm-profiler用于统计clojure STM的运行状况,放在了github上:
https://github.com/killme2008/stm-profiler
STM的事务在遇到写冲突(多个事务写同一个ref的时候)就会回滚事务并重试,通过stm-profiler你可以查看事务的重试次数,重试原因,以及每个reference的使用情况。使用很简单,在lein的project.clj引用stm-profiler:
[stm-profiler "1.0.2-SNAPSHOT"]
注意,目前stm profiler仅支持clojure 1.3。
我们写一个简单例子:
(use 'stm)
(def a (ref 1))
(def b (ref 2))
(dotimes [_ 100] (future (dosync (alter a + 1) (alter b - 1))))
(Thread/sleep 1000)
(prn @a)
(prn @b)
(Thread/sleep 1000)
(prn "stm statistics" (stm-stats))
(prn "reference a statistics" (ref-stats a))
(prn "reference b statistics" (ref-stats b))
定义了两个ref:a和b,然后用future启动100个线程并发地发起同一个事务操作,对a加一,对b减一。最后打印a和b的值,使用stm-stats函数获取stm的统计信息并打印,使用ref-stats获取a和b两个reference的统计信息并打印。
运行这个例子,在启动的时候会有些警告信息,忽略即可(主要是因为stm profiler重新定义了一些跟STM相关的函数和宏,如dosync等,但是仅仅是添加了统计功能,并没有修改他们原本的功能)。
在我机器上的一次输出:
101
-98
"stm statistics" {"(alter a + 1)(alter b - 1)" {:not-running 11, :average-retry 5, :total-cost 1233, :get-fault 44, :barge-fail 224, :change-committed 227, :total-times 100, :average-cost 12}}
"reference a statistics" {"(alter a + 1)(alter b - 1)" {:alter 609, :get-fault 44, :barge-fail 224, :change-committed 227}}
"reference b statistics" {"(alter a + 1)(alter b - 1)" {:alter 114, :not-running 11}}
a和b的结果都没问题。重点看打印的统计信息,(stm-stats)的输出结果是:
{"(alter a + 1)(alter b - 1)" {:not-running 11, :average-retry 5, :total-cost 1233, :get-fault 44, :barge-fail 224, :change-committed 227, :total-times 100, :average-cost 12}}
这个结果是一个map,key是事务的form,而value就是该form的统计信息,也是一个map,具体各项的含义如下:
total-cost
|
所有事务的总耗时
|
100个事务耗时1233毫秒
|
total-times
|
事务运行次数
|
100次
|
average-cost
|
平均每个事务耗时
|
平均一个事务耗时12毫秒
|
average-retry
|
平均每个事务的重试次数 |
平均每个事务重试了5次才成功 |
not-running |
当前事务不处于running状态,可能是被其他事务打断(barge),需要重试 |
因为not-running的原因重试了11次 |
get-fault
|
读取ref值的时候没有找到read point之前的值,被认为是一次读错误,需要重试
|
因为读ref错误重试了44次
|
barge-fail |
打断其他事务失败次数,需要重试 |
尝试打断其他事务失败而重试了224次 |
change-committed |
在本事务read point之后有ref值获得提交,则需要重试
|
因为ref值被其他事务提交而重试了227次 |
从输出结果来看,这么简单的一个事务操作,每次事务要成功平均都需要经过5次的重试,最大的原因是因为ref的值在事务中被其他事务更改了,或者尝试打断其他正在运行的事务失败而重试。关于clojure STM的具体原理推荐看这篇文章《 Software transactional memory》。STM不是完美的,事务重试和保存每个reference的历史版本的代价都不低。 再看 (ref-stats a)的输出:
{"(alter a + 1)(alter b - 1)" {:alter 609, :get-fault 44, :barge-fail 224, :change-committed 227}} 可以看到a在所有事务中的统计信息,返回的结果同样是个map,key是使用了a的事务,value是具体的统计信息。各项的含义类似上表,不过这里精确到了具体的reference。其中alter项是指对a调用alter函数了609次。ref-stats会输出所有在事务中调用了a的函数的调用次数。 通过stm profiler你可以分析具体每个事务的执行状况,甚至每个reference的运行状况,查找热点事务和热点reference等。stm-profiler还不完善,目前还不支持1.2(1.4测试是可以的)。希望有兴趣的朋友加入进来一起完善。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2012/02/09/369694.html
去年(我靠,已经是去年了)首次在上海组织了第一次cn-clojure的线下聚会,详细可以看 这篇blog。今年,我们将在北京举行第二次cn-clojure的聚会,时间大概在2月底或者3月初,具体地点待定,欢迎任何对clojure语言或者Lisp语言感兴趣的朋友参加,如果有想分享的技术topic更好 :D。 如果你要参加,请参加下面的报名调查,填写真实的姓名和邮箱。如果有想分享的topic,可以填写调查或者直接邮件给我。 报名链接: http://www.diaochapai.com/survey584561 我们将在议程、时间和地点确定后发邮件给报名的朋友,确认参会的具体时间和地点。 邮件列表: http://groups.google.com/group/cn-clojure
很久没写blog了,写写最近做的一些工作,给感兴趣的朋友做参考。 首先是我们的 kafka的“复制品”metamorphosis做了1.4版本,实现了同步复制方案,broker本身也做了很多优化,总体而言meta是一个非常成熟可用的产品了。甚至可以说是我在淘宝做的最好的一个产品。有些朋友总是问我们为什么不直接用kafka,而要另写一个?这里做个统一的解答。 (1)kafka是scala写的,我对scala不熟悉,也不待见,考虑到维护和语言熟悉程度,用java重写仍然是最好的选择。 (2)其次,kafka的整个社区非常不活跃,发展太慢,而我又不愿意去学习scala来参与社区发展,那么唯一的出路就是自己写。 (3)kafka的一些工作不能满足我们的要求,比如一开始它连producer的负载均衡都没有,它的消费者API设计还是比较蛋疼的。它也不支持事务,没有考虑作为一个通用的MQ系统来使用。并且它也没有高可用和数据高可靠的方案。 (4)我们做了什么呢? a.用java彻底重写整个系统,除了原理一致,整个实现是彻底重新实现的。 b.我们提供了生产者的负载均衡(仍然是基于zk),重新设计了消费者API,更符合 JMS的使用习惯。 c.我们提供了事务实现,包括producer和consumer端的,包括本地事务和符合XA规范的分布式事务实现。 d.我们提供了两种数据高可靠方案:类似mysql的异步复制和同步复制方案。通过将消息复制到多个节点上来保证数据的高可靠。 e.我们提供了http协议的实现,并且本身使用协议也是类似memcached的文本协议,内部也增加了很多统计项目,可以以memcached的stats协议的方式来获取纯文本的统计信息。整个系统运维很方便。 f.提供了很多扩展应用:广播消费者的实现,多种offset存储的实现(默认的zookeeper,还有文件和mysql),tail4j用于作为agent发送日志,log4j appender扩展用于透明地使用log4j发送消息,hdfs writer用于将消息写入hdfs,storm spout用于将消息接入storm做实时分析,基本上形成一套完整的工具链和扩展。 g.一些其他功能点:group commit提升数据可靠性和吞吐量,连接复用,集群下的顺序消息发送,消息数据的无痛迁移和水平扩展,web管理平台等。 meta未来会走开源的路子,不过不会是我来推动的,估计是在今年会有进展。 我最近还写了一些小项目值得一提,首先是 aviator这个轻量级的表达式执行引擎发布了2.2.1版本,主要是这么几个改进: (1)支持多维数组变量的访问,如a[0][0] (2)添加Expression#getVariableNames()用于返回表达式的变量列表 (3)添加AviatorEvaluator#exec方法来简化调用 (4)bug修正等。 maven直接升级: <dependency> <groupId>com.googlecode.aviator</groupId> <artifactId>aviator</artifactId> <version>2.2.1</version> </dependency> 其次, hs4j这个handler socket的客户端,由新浪微博的@赵鹏城实现了inc/dec协议,添加了incr和decr方法用于更新计数,感谢他的贡献,如果你需要这两个功能可以自己从github拉取源码并构建打包,暂时不准备发布到maven。 第三,关注高可用的Transaction Manager实现的可以关注下我的 ewok项目,这是一个基于 BTM这个开源JTA实现,提供基于bookkeeper的高可用的TM项目。将事务日志写到高可用的bookkeeper上,并利用zookeeper来做到故障的透明迁移,某个TM挂了,可以在其他机器上从bookkeeper拉取日志并恢复。代码已经稳定并做了性能测试,没有做进一步的破坏性测试。 BTM是一个比JOTM和atomikos更靠谱的开源JTA实现,并且性能也好上很多,代码质量更不用说,建议有兴趣的可以看一下。我也为它贡献了 一个事务日志写入优化的patch,日志写入性能提升了近一倍。 最后,我在clojure上做了一些事情,首先是为 storm项目贡献了两个patch:利用curator做zookeeper交互和添加storm.ui.context.path选项,前者被作者接受,后者暂时只对我们有用。前者让storm跟zk的交互更可用,后者是为storm ui添加了可选的相对路径。你都可以在 我fork的分支上尝试,curator的patch在storm 0.6.2上发布,现在还是snapshot状态。昨天晚上牙痛睡不着,半夜写了个 clojure STM profiler,用于统计分析clojure STM运行状况,诸如事务运行次数和时间,事务的重试原因和次数等,可以针对每个dosync的form做统计,有兴趣也可以看下。不过我其实更想将这个功能加入到clojure核心,会尝试提交下pull request。 还有个工作上的变迁,我将在2月1号正式从呆了近三年的淘宝离职,加入一支充满活力的创业团队。从稳定的大公司出来,去加入一家初创公司,不能说没有风险,但是我还是想去接受新的挑战,愿意更新我的知识结构,愿意向牛人们学习。我在某个blog上说我今年遇到了人生中最大的挑战和转折,并不是说这个事情,而是我的儿子今年患了一场重病,庆幸在很多人的帮助和关心下,他勇敢地挺了过来,度过最困难的一关,现在还在继续治疗。我要感谢很多人,感谢淘宝,感谢我的TL华黎和锋寒,感谢我的同事和朋友林轩,感谢我们的HR,感谢三年后打交道的很多同事。没有他们,我今年真的过不了关,没有他们,我也不能进入淘宝并呆上快三年。 最后的最后,我要特别感谢我的儿子,谢谢你的降生,谢谢你今年的勇敢,谢谢你给我们全家带来的快乐,谢谢你继续陪着我们 ,也希望你新年继续勇敢地坚持下去,我们必将战胜一切。
最近有朋友给我邮件问一些storm的问题,集中解答在这里。 一、我有一个数据文件,或者我有一个系统里面有数据,怎么导入storm做计算?你需要实现一个Spout,Spout负责将数据emit到storm系统里,交给bolts计算。怎么实现spout可以参考官方的kestrel spout实现: https://github.com/nathanmarz/storm-kestrel如果你的数据源不支持事务性消费,那么就无法得到storm提供的可靠处理的保证,也没必要实现ISpout接口中的ack和fail方法。 二、Storm为了保证tuple的可靠处理,需要保存tuple信息,这会不会导致内存OOM?Storm为了保证tuple的可靠处理,acker会保存该节点创建的tuple id的xor值,这称为ack value,那么每ack一次,就将tuple id和ack value做异或(xor)。当所有产生的tuple都被ack的时候, ack value一定为0。这是个很简单的策略,对于每一个tuple也只要占用约20个字节的内存。对于100万tuple,也才20M左右。关于可靠处理看这个: https://github.com/nathanmarz/storm/wiki/Guaranteeing-message-processing三、Storm计算后的结果保存在哪里?可以保存在外部存储吗?Storm不处理计算结果的保存,这是应用代码需要负责的事情,如果数据不大,你可以简单地保存在内存里,也可以每次都更新数据库,也可以采用NoSQL存储。storm并没有像s4那样提供一个Persist API,根据时间或者容量来做存储输出。这部分事情完全交给用户。 数据存储之后的展现,也是你需要自己处理的,storm UI只提供对topology的监控和统计。 四、Storm怎么处理重复的tuple?因为Storm要保证tuple的可靠处理,当tuple处理失败或者超时的时候,spout会fail并重新发送该tuple,那么就会有tuple重复计算的问题。这个问题是很难解决的,storm也没有提供机制帮助你解决。一些可行的策略: (1)不处理,这也算是种策略。因为实时计算通常并不要求很高的精确度,后续的批处理计算会更正实时计算的误差。 (2)使用第三方集中存储来过滤,比如利用mysql,memcached或者redis根据逻辑主键来去重。 (3)使用bloom filter做过滤,简单高效。 五、Storm的动态增删节点我在storm和s4里比较里谈到的动态增删节点,是指storm可以动态地添加和减少supervisor节点。对于减少节点来说,被移除的supervisor上的worker会被nimbus重新负载均衡到其他supervisor节点上。在storm 0.6.1以前的版本,增加supervisor节点不会影响现有的topology,也就是现有的topology不会重新负载均衡到新的节点上,在扩展集群的时候很不方便,需要重新提交topology。因此我在storm的邮件列表里提了这个问题,storm的开发者nathanmarz创建了一个issue 54并在0.6.1提供了rebalance命令来让正在运行的topology重新负载均衡,具体见: https://github.com/nathanmarz/storm/issues/54和0.6.1的变更: http://groups.google.com/group/storm-user/browse_thread/thread/24a8fce0b2e53246storm并不提供机制来动态调整worker和task数目。 六、Storm UI里spout统计的complete latency的具体含义是什么?为什么emit的数目会是acked的两倍?这个事实上是storm邮件列表里的一个问题。Storm作者marz的解答: The complete latency is the time from the spout emitting a tuple to that tuple being acked on the spout. So it tracks the time for the whole tuple tree to be processed.
If you dive into the spout component in the UI, you'll see that a lot of the emitted/transferred is on the __ack* stream. This is the spout communicating with the ackers which take care of tracking the tuple trees.
简单地说,complete latency表示了tuple从emit到被acked经过的时间,可以认为是tuple以及该tuple的后续子孙(形成一棵树)整个处理时间。其次spout的emit和transfered还统计了spout和acker之间内部的通信信息,比如对于可靠处理的spout来说,会在emit的时候同时发送一个_ack_init给acker,记录tuple id到task id的映射,以便ack的时候能找到正确的acker task。
原文: http://www.blogjava.net/killme2008/archive/2011/11/17/364112.html 作者:dennis (killme2008@gmail.com) 转载请注明出处。 最近一直在读twitter开源的这个分布式流计算框架——storm的源码,还是有必要记录下一些比较有意思的地方。我按照storm的主要概念进行组织,并且只分析我关注的东西,因此称之为浅析。 一、介绍 Storm的开发语言主要是Java和Clojure,其中Java定义骨架,而Clojure编写核心逻辑。源码统计结果: 180 text files. 177 unique files. 7 files ignored.
http://cloc.sourceforge.net v 1.55 T=1.0 s (171.0 files/s, 46869.0 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Java 125 5010 2414 25661 Lisp 33 732 283 4871 Python 7 742 433 4675 CSS 1 12 45 1837 ruby 2 22 0 104 Bourne Shell 1 0 0 6 Javascript 2 1 15 6 ------------------------------------------------------------------------------- SUM: 171 6519 3190 37160 ------------------------------------------------------------------------------- Java代码25000多行,而Clojure(Lisp)只有4871行,说语言不重要再次证明是扯淡。 二、Topology和Nimbus Topology是storm的核心理念,将spout和bolt组织成一个topology,运行在storm集群里,完成实时分析和计算的任务。这里我主要想介绍下topology部署到storm集群的大概过程。提交一个topology任务到Storm集群是通过StormSubmitter.submitTopology方法提交: StormSubmitter.submitTopology(name, conf, builder.createTopology()); 我们将topology打成jar包后,利用bin/storm这个python脚本,执行如下命令: bin/storm jar xxxx.jar com.taobao.MyTopology args 将jar包提交给storm集群。storm脚本会启动JVM执行Topology的main方法,执行submitTopology的过程。而submitTopology会将jar文件上传到nimbus,上传是通过socket传输。在storm这个python脚本的jar方法里可以看到: def jar(jarfile, klass, *args): exec_storm_class( klass, jvmtype="-client", extrajars=[jarfile, CONF_DIR, STORM_DIR + "/bin"], args=args, prefix="export STORM_JAR=" + jarfile + ";")
将jar文件的地址设置为环境变量STORM_JAR,这个环境变量在执行submitTopology的时候用到: //StormSubmitter.java private static void submitJar(Map conf) { if(submittedJar==null) { LOG.info("Jar not uploaded to master yet. Submitting jar "); String localJar = System.getenv("STORM_JAR"); submittedJar = submitJar(conf, localJar); } else { LOG.info("Jar already uploaded to master. Not submitting jar."); } } 通过环境变量找到jar包的地址,然后上传。利用环境变量传参是个小技巧。 其次,nimbus在接收到jar文件后,存放到数据目录的inbox目录, nimbus数据目录的结构: -nimbus -inbox -stormjar-57f1d694-2865-4b3b-8a7c-99104fc0aea3.jar -stormjar-76b4e316-b430-4215-9e26-4f33ba4ee520.jar
-stormdist -storm-id -stormjar.jar -stormconf.ser -stormcode.ser 其中inbox用于存放提交的jar文件,每个jar文件都重命名为stormjar加上一个32位的UUID。而stormdist存放的是启动topology后生成的文件,每个topology都分配一个唯一的id,ID的规则是“name-计数-时间戳”。启动后的topology的jar文件名命名为storm.jar ,而它的配置经过java序列化后存放在stormconf.ser文件,而stormcode.ser是将topology本身序列化后存放的文件。 这些文件在部署的时候,supervisor会从这个目录下载这些文件,然后在supervisor本地执行这些代码。 进入重点,topology任务的分配过程(zookeeper路径说明忽略root): 1.在zookeeper上创建/taskheartbeats/{storm id} 路径,用于任务的心跳检测。storm对zookeeper的一个重要应用就是利用zk的临时节点做存活检测。task将定时刷新节点的时间戳,然后nimbus会检测这个时间戳是否超过timeout设置。 2.从topology中获取bolts,spouts设置的并行数目以及全局配置的最大并行数,然后产生task id列表,如[1 2 3 4] 3.在zookeeper上创建/tasks/{strom id}/{task id}路径,并存储task信息 4.开始分配任务(内部称为assignment), 具体步骤: (1)从zk上获得已有的assignment(新的toplogy当然没有了) (2)查找所有可用的slot,所谓slot就是可用的worker,在所有supervisor上配置的多个worker的端口。 (3)将任务均匀地分配给可用的worker,这里有两种情况: (a)task数目比worker多,例如task是[1 2 3 4],可用的slot只有[host1:port1 host2:port1],那么最终是这样分配 {1: [host1:port1] 2 : [host2:port1] 3 : [host1:port1] 4 : [host2:port1]} ,可以看到任务平均地分配在两个worker上。 (b)如果task数目比worker少,例如task是[1 2],而worker有[host1:port1 host1:port2 host2:port1 host2:port2],那么首先会将woker排序, 将不同host间隔排列,保证task不会全部分配到同一个worker上,也就是将worker排列成 [host1:port1 host2:port1 host1:port2 host2:port2] ,然后分配任务为 {1: host1:port1 , 2 : host2:port2} (4)记录启动时间 (5)判断现有的assignment是否跟重新分配的assignment相同,如果相同,不需要变更,否则更新assignment到zookeeper的/assignments/{storm id}上。 5.启动topology,所谓启动,只是将zookeeper上/storms/{storm id}对应的数据里的active设置为true。 6.nimbus会检查task的心跳,如果发现task心跳超过超时时间,那么会重新跳到第4步做re-assignment。
所谓兵马未动,粮草先行,准备将 storm用在某个项目中做实时数据分析。无论任何系统,一定要有监控系统并存,当故障发生的时候你能第一个知道,而不是让别人告诉你,那处理故障就很被动了。 因此我写了这么个项目,取名叫storm-monitor,放在了github上 https://github.com/killme2008/storm-monitor 主要功能如下: 1.监控supervisor数目是否正确,当supervisor挂掉的时候会发送警告。 2.监控nimbus是否正常运行,monitor会尝试连接nimbus,如果连接失败就认为nimbus挂掉。 3.监控topology是否正常运行,包括它是否正常部署,是否有运行中的任务。 当故障发生的时候通过alarm方法警告用户,开放出去的只是简单地打日志。因为每个公司的告警接口不一样,所以你需要自己扩展,修改alarm.clj即可。我们这儿就支持旺旺告警和手机短信告警。 基本的原理很简单,对supervisor和topology的监控是通过zookeeper来间接地监控,通过定期查看path是否存在。对nimbus的监控是每次起一个短连接连上去,连不上去即认为挂掉。 整个项目也是用clojure写。你的机器需要安装 lein和 exec插件,然后将你的storm.yaml拷贝到conf目录下,编辑monitor.yaml设定监控参数如检查间隔等,最后启动start.sh脚本即可。默认日志输出在logs/monitor.log。
在豆瓣发了一些牢骚,索性多说一些我个人对人对事的偏见,既然是偏见,就不会让人舒服,事先声明是扯淡,不想浪费时间的人略过。
1.我们要远离新浪微博,新浪微博跟twitter不一样,twitter是为了让每个人的信息的更好更快地传播而设计的,而新浪微博是为了让权威的声音更好更快地传播而设计的。迷恋上新浪微博,你要么是权威,要么是跟随权威。成为权威的,免不了沾沾自喜,真以为自己成了“权威”。更可怕的是你不可避免地要生活在相互吹捧和喧嚣中。
2.在编写代码之外,我们可能需要更多的手艺傍身,例如木匠或者厨师,以免在乱世的时候因为不需要程序员而饿死。ps.计算弹道轨迹的程序员除外。
3.据说真正的牛人从不跳槽,作为大多数不是牛人,以及已经远离牛人行列的我们(跳槽超过3次以上),跳槽仍然是你提升自己的有效途径,无论是薪水还是技术。
4.写简历的技巧,我慢慢领悟到了,少点技术术语,多点成效和应用,打动了HR过了第一关之后,再去跟技术人员扯淡。
5.简历要定时更新,你可以理解成定时提醒下猎头和HR,关注我啊,关注我啊。
6.强烈地拥抱文本化,配置文本化(没人会脑残地用二进制当配置文件吧?),协议文本化,婚姻文本化。
7.一切不以加薪为目的的挽留,都是耍流氓,这不是我的原创。
8.有趣比实用重要,没趣味的东西,给钱也不去做(好吧,我说假话)。
9.对新潮的东西保持一点警惕,如果这个东西三个月后还有人在谈论,那可以关注下
10.代码永远比文档、博客真实和靠谱,阅读代码习惯了,跟阅读文档没啥区别。
11.少关注博客和新闻,戒掉看google reader的习惯。现在更多地看maillist上的讨论和问题,真正重要的东西你永远不会错过。
12.不追求完美,等你完美的时候别人已经是事实标准。
13.大型的技术聚会不是为技术人员准备的,这是大公司给员工的度假福利和领导们的吹水时间。只有在小型的技术聚会上才能看到一些有价值的东西,任何稍微跟商业沾一点边的几乎都没有太大价值,我说的是国内。
14.80%的分享都只对演讲者有益,该sb的还是sb,该牛b的还是牛b。最有效的分享是结对编程和结对review。分享和培训最大的意义是让行政们觉的自己的存在价值很大。
15.国内翻译国外经典>国内原创精品>国外原版,这个原则对英语好的人除外。
16.极其讨厌要求强制缩进的语言,比如python。
17.标榜是一种人生态度,装B装久了你就真牛B了。
18.凭啥不造轮子,你们造轮子舒坦了,爽快了,就不让别人造了。我造轮子我快乐。
19.偏见不全是坏事,坏的是不愿意改变偏见。
扯淡时间结束。
Items\Projects
|
Yahoo! s4
|
Twitter Storm
|
协议
|
Apache license 2.0
|
Eclipse Public License 1.0
|
开发语言
|
Java
|
Clojure,Java,Clojure编写了核心代码 |
结构
|
去中心化的对等结构
|
有中心节点nimbus,但非关键 |
通信
|
可插拔的通讯层,目前是基于UDP的实现 |
基于facebook开源的thrift框架 |
事件/Stream
|
<K,A>序列,用户可自定义事件类 |
提供Tuple类,用户不可自定义事件类, 但是可以命名field和注册序列化器 |
处理单元 |
Processing Elements,内置PE处理 count,join和aggregate等常见任务 |
Bolt,没有内置任务,提供IBasicBolt处理 自动ack |
第三方交互
|
提供API,Client Adapter/Driver,第三方客户端输入或者输出事件 |
定义Spout用于产生Stream,没有标准输出API |
持久化 |
提供Persist API规范,可根据频率或者次数做 持久化
|
无特定API,用户可自行选择处理
| 可靠处理 | 无,可能会丢失事件 | 提供对事件处理的可靠保证(可选) | 路由 | EventType + Keyed attribute + value匹配 内置count,join和aggregate标准任务 | Stream Groupings: Shuffle,Fields,All,Global,None,Direct 非常灵活的路由方式 | 多语言支持 | 暂时只支持Java | 多语言支持良好,本身支持Java,Clojure, 其他非JVM语言通过thrift和进程间通讯 | Failover
| 部分支持,数据无法failover | 部分支持,数据同样无法failover | Load Balance
| 不支持 | 不支持 | 并行处理 | 取决于节点数目,不可调节 | 可配置worker和task数目,storm会尽量将worker和task均匀分布 | 动态增删节点 | 不支持
| 支持 | 动态部署
| 不支持 | 支持 | web管理 | 不支持 | 支持 | 代码成熟度 | 半成品 | 成熟 | 活跃度 | 低 | 活跃 | 编程 | 编程 + XML配置
| 纯编程
| 参考文档 | http://docs.s4.io/ | https://github.com/nathanmarz/storm/wiki/ http://xumingming.sinaapp.com/category/storm/ (非常好的中文翻译)
|
Clj-xmemcached is an opensource memcached client for clojure wrapping xmemcached. Xmemcached is an opensource high performance memcached client for java.
Leiningen Usage
To include clj-xmemcached,add:
[clj-xmemcached "0.1.1"]
to your project.clj.
Usage
Create a client
(use [clj-xmemcached.core]) (def client (xmemcached "host:port")) (def client (xmemcached "host1:port1 host2:port2" :protocol "binary"))
Then we create a memcached client using binary protocol to talk with memcached servers host1:port1 and host2:port2.
Valid options including:
:name Client's name
:protocol Protocol to talk with memcached,a string value in text,binary or kestrel,default is text protocol.
:hash Hash algorithm,a string value in consistent or standard,default is standard hash.
:timeout Operation timeout in milliseconds,default is five seconds.
:pool Connection pool size,default is one.
Store items
(xset client "key" "dennis") (xset client "key" "dennis" 100) (xappend client "key" " zhuang") (xprepend client "key" "hello,")
The value 100 is the expire time for the item in seconds.Store
functions include xset,xadd,xreplace,xappend and xprepend.Please use doc
to print documentation for these functions.
Get items
(xget client "key") (xget client "key1" "key2" "key3") (xgets client "key")
xgets returns a value including a cas value,for example:
{:value "hello,dennis zhuang", :class net.rubyeye.xmemcached.GetsResponse, :cas 396}
And bulk get returns a HashMap contains existent items.
Increase/Decrease numbers
(xincr client "num" 1) (xdecr client "num" 1) (xincr client "num" 1 0)
Above codes try to increase/decrease a number in memcached with key "num",and if the item is not exists,then set it to zero.
Delete items
(xdelete client "num")
Compare and set
(xcas client "key" inc)
We use inc function to increase the current value in memcached and try to compare and set it at most Integer.MAX_VALUE times.
xcas can be called as:
(xcas client key cas-fn max-times)
The cas-fn is a function to return a new value,set the new value to
(cas-fn current-value)
Shutdown
(xshutdown client)
Flush
(xflush client) (xflush client (InetSocketAddress. host port))
Statistics
(xstats client)
Example
Please see the example code in example/demo.clj
License
Copyright (C) 2011-2014 dennis zhuang[killme2008@gmail.com]
Distributed under the Eclipse Public License, the same as Clojure.
将自己在googlecode和github上的所有项目过了一遍,整理一张列表,列下一些还有点价值和用处的项目,都不是什么great job,纯粹是为了工作需要或者乐趣写的东西,看官要是有兴趣也可以瞧瞧。 一 Java相关 1. Xmemcached,还算是比较多人使用的一个java memcached client,优点是效率和易用性,缺点是代码写的不怎么样,两年前发展到现在的东西,以后还会继续维护。 2. HS4J,看 handlersocket的时候顺手写的客户端,我们公司内部某些项目在用,可能还有其他公司外的朋友在用,后来同事聚石贡献了一个扩展项目 hs4j-kit,更易于使用,他写的代码很优雅漂亮,推荐一看。暂时没有精力维护。 3. Aviator,一个很初级的表达式执行引擎,行家看到肯定要笑话我。不过语法上很符合我自己的口味,我们自己的项目在用,也有几个朋友在用,会继续维护。 4. Jevent,一个玩具,其实是模仿libevent的一个java实现,对nio或者libevent的实现机制感兴趣的还可以看看。 5. Kilim,我fork的kilim实现,修改了nio调度器,使用多个reactor做调度效率更高,并添加了一个HttpClient的实现。 二 Android项目 学习android完全是玩票性质,有3个项目,对初学android开发的可能有点参考价值。 1. WhetherWeather,一个天气预报和告警的widget插件,UI太丑了。 2. UniqRecorder,写来记录儿子体重变化的小工具,可以自定义项目和生成曲线图,我自己还在用。 3. UniqTask,最近写的杀进程工具,绝对轻量级,没广告,也是我自己在用。 三 Clojure项目 1. cscheme,一个用clojure实现的scheme解释器,基于sicp这本书的解释器实现。 2. clojure-control,类似 node-control的分布式部署和管理的DSL实现,挺好玩的,也有朋友在用,我自己还用不上,sunny有写了个很方便的lein插件 node-control。 clojure还写了一堆烂尾项目,就不拿出来恶心人了。 四 其他 1. node-zk-browser,一个展现和管理zookeeper的web应用,我们自己在用,基于node.js实现。 2. erlwsh,一个erlang的web shell实现,可以在浏览器里做erlang编程,被一些开源项目比如membase用到了。 写这些东西对我自己最有好处,如果能顺便给他人带来好处,那是额外的好处。最近正处于我自己一生中也许是最大的转折关头,不能更新blog了,最后,祈求诸天神佛能带来奇迹。
xmemcached紧急发布1.3.5版本,主要是修复两个相对严重的bug: Issue 154: 在重连本地memcached的时候,有可能出现重连无法成功的情况,导致连接丢失,详情见 这里。 Issue 155: 重连导致文件句柄数超过限制的bug,这是由于重连失败情况下没有合理关闭socket引起的,详情见 这里。 如果你使用maven,简单升级版本即可: <dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>1.3.5</version> </dependency> 下载地址: http://code.google.com/p/xmemcached/downloads/list 此版本推荐升级,最后感谢两位老外开发者的帮助:
ilkinulas和 MrRubato
好吧,我知道现在是凌晨4点了,写完这个就睡觉。 我一直很不爽android的ES任务管理器,它的广告设置的地方非常恶心,就放在kill键的下面,而且每次都突然跳出来,让你很容易错误点击。我很佩服他们能想出这种提高点击率的办法,但是又无比鄙视这种做法。今天(哦,不是昨天)晚上在twitter上说了,我想自己写个任务管理器,类似ES任务管理器,并且没有广告。那好吧,说干就干,奋斗了一个晚上,终于搞出了成果,这就是隆重登场的UniqTask,先看看运行时截图: 这是运行在我的GS2上的截图。 UniqTask的功能跟ES任务管理器的功能完全一致,可以记录kill的历史,每次启动UniqTask的时候自动标记过去kill过的进程。但是UniqTask完全绿色无毒,绝对没有广告,咔咔。 许久没写android程序,拿起手来不是很顺利,折腾到现在才搞定,我将代码放到了github上,也提供了APK下载,非常欢迎试用啊。 源码地址: https://github.com/killme2008/UniqTask APK下载: https://github.com/killme2008/UniqTask/blob/master/UniqTask.apk 白天还有重要的事情要处理,睡觉去了。
Fel是最近javaeye比较火的关键词,这是由网友 lotusyu开发的一个高性能的EL,从作者给出的数据来看,性能非常优异,跟前段时间温少开源的 Simple EL有的一拼。首先要说,这是个好现象,国内的开源项目越来越多,可以看出开发者的水平是越来越高了,比如我最近还看到有人开源的类似kestel的轻量级MQ—— fqueue也非常不错,有兴趣可以看下我的分析《 fqueue初步分析》。
进入正文,本文是尝试分析下Fel的实现原理,以及优缺点和aviator——我自己开源的EL之间的简单比较。
Fel的实现原理跟Simple
EL是类似,都是使用template生成中间代码——也就是普通的java代码,然后利用javac编译成class,最后运行,当然,这个过程都是动
态的。JDK6已经引入了编译API,在此之前的版本可以调用sun的类来编译,因为javac其实就是用java实现的。回到Fel里
面,FelCompiler15就是用 com.sun.tools.javac.Main来编译,而FelCompiler16用标准的javax.tools.JavaCompiler来编译的。
文法和语法解释这块是使用antlr这个parse generator生成的,这块不多说,有兴趣可以看下antlr,整体一个运行的过程是这样:
expression string -> antlr -> AST -> comiple -> java source template -> java class -> Expression
这个思路我在实现aviator之前就想过,但是后来考虑到API需要用的sun独有的类,而且要求classpath必须有tools.jar这个依赖包,就放弃了这个思路,还是采用ASM生成字节码的方式。题外,velocity的优化可以采用这个思路,我们有这么一个项目是这么做的,也准备开源了。
看看Fel生成的中间代码,例如a+b这样的一个简单的表达式,假设我一开始不知道a和b的类型,编译是这样:
FelEngine fel = new FelEngineImpl(); Expression exp = fel.compile("a+b", null);
我稍微改了下FEL的源码,让它打印中间生成的java代码,a+b生成的中间结果为:
package com.greenpineyu.fel.compile; import com.greenpineyu.fel.common.NumberUtil; import com.greenpineyu.fel.Expression; import com.greenpineyu.fel.context.FelContext; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; public class Fel_0 implements Expression{ public Object eval(FelContext context) { java.lang.Object var_1 = (java.lang.Object)context.get("b"); //b java.lang.Object var_0 = (java.lang.Object)context.get("a"); //a return (ObjectUtils.toString(var_0))+(ObjectUtils.toString(var_1)); } }
可见,FEL对表达式解析和解释后,利用template生成这么一个普通的java类,而a和b都从context中获取并转化为Object类型,这里没有做任何判断就直接认为a和b是要做字符串相加,然后拼接字符串并返回。
问题出来了,因为没有在编译的时候传入context(我们这里是null),FEL会将a和b的类型默认都为java.lang.Object,a+b解释为字符串拼接。但是运行的时候,我完全可以传入a和b都为数字,那么结果就非常诡异了:
FelEngine fel = new FelEngineImpl(); Expression exp = fel.compile("a+b", null); Map<String, Object> env=new HashMap<String, Object>(); env.put("a", 1); env.put("b", 3.14); System.out.println(exp.eval(new MapContext(env)));
输出:
13.14
1+3.14的结果,作为字符串拼接就是13.14,而不是我们想要的4.14。如果将表达式换成a*b,就完全运行不了
com.greenpineyu.fel.exception.CompileException: package com.greenpineyu.fel.compile; import com.greenpineyu.fel.common.NumberUtil; import com.greenpineyu.fel.Expression; import com.greenpineyu.fel.context.FelContext; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; public class Fel_0 implements Expression{ public Object eval(FelContext context) { java.lang.Object var_1 = (java.lang.Object)context.get("b"); //b java.lang.Object var_0 = (java.lang.Object)context.get("a"); //a return (var_0)*(var_1); } } [Fel_0.java:14: 运算符 * 不能应用于 java.lang.Object,java.lang.Object] at com.greenpineyu.fel.compile.FelCompiler16.compileToClass(FelCompiler16.java:113) at com.greenpineyu.fel.compile.FelCompiler16.compile(FelCompiler16.java:87) at com.greenpineyu.fel.compile.CompileService.compile(CompileService.java:66) at com.greenpineyu.fel.FelEngineImpl.compile(FelEngineImpl.java:62) at TEst.main(TEst.java:14) Exception in thread "main" java.lang.NullPointerException at TEst.main(TEst.java:18)
这个问题对于Simple EL同样存在,如果没有在编译的时候能确定变量类型,这无法生成正确的中间代码,导致运行时出错,并且有可能造成非常诡异的bug。
这个问题的本质是因为Fel和Simple EL没有自己的类型系统,他们都是直接使用java的类型的系统,并且必须在编译的时候确定变量类型,才能生成高效和正确的代码,我们可以将它们称为“强类型的EL“。
现在让我们在编译的时候给a和b加上类型,看看生成的中间代码:
FelEngine fel = new FelEngineImpl(); fel.getContext().set("a", 1); fel.getContext().set("b", 3.14); Expression exp = fel.compile("a+b", null); Map<String, Object> env = new HashMap<String, Object>(); env.put("a", 1); env.put("b", 3.14); System.out.println(exp.eval(new MapContext(env)));
查看中间代码:
package com.greenpineyu.fel.compile; import com.greenpineyu.fel.common.NumberUtil; import com.greenpineyu.fel.Expression; import com.greenpineyu.fel.context.FelContext; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; public class Fel_0 implements Expression{ public Object eval(FelContext context) { double var_1 = ((java.lang.Number)context.get("b")).doubleValue(); //b double var_0 = ((java.lang.Number)context.get("a")).doubleValue(); //a return (var_0)+(var_1); } }
可以看到这次将a和b都强制转为double类型了,做数值相加,结果也正确了:
4.140000000000001
Simple EL我没看过代码,这里猜测它的实现也应该是类似的,也应该有同样的问题。
相比来说,aviator这是一个弱类型的EL,在编译的时候不对变量类型做任何假设,而是在运行时做类型判断和自动转化。过去提过,我给aviator的定位是一个介于EL和script之间的东西,它有自己的类型系统。
例如,3这个数字,在java里可能是long,int,short,byte,而aviator统一为AviatorLong这个类型。为了在这两个类
型之间做适配,就需要做很多的判断和box,unbox操作。这些判断和转化都是运行时进行的,因此aviator没有办法做到Fel这样的高效,但是已
经做到至少跟groovy这样的弱类型脚本语言一个级别,也超过了JXEL这样的纯解释EL,具体可以看这个性能测试。
强类型还是弱类型,这是一个选择问题,如果你能在运行前就确定变量的类型,那么使用Fel应该可以达到或者接近于原生java执行的效率,但是失去了灵活性;如果你无法确定变量类型,则只能采用弱类型的EL。
EL涌现的越来越多,这个现象有点类似消息中间件领域,越来越多面向特定领域的轻量级MQ的出现,而不是原来那种大而笨重的通用MQ大行其道,一方面是互
联网应用的发展,需求不是通用系统能够满足的,另一方面我认为也是开发者素质的提高,大家都能造适合自己的轮子。从EL这方面来说,我也认为会有越来越多
特定于领域的,优点和缺点一样鲜明的EL出现,它们包含设计者自己的目标和口味,选择很多,就看取舍。
fqueue是国产的一个类似memcacheq,kestrel这样的支持memcached协议的轻量级开源MQ。它的项目主页: http://code.google.com/p/fqueue/downloads/list,介绍和特点都可以看主页,我就不废话了。 今天老大提到, co了源码看了下,写个初步分析报告。 首先是它的存储层,主要是一个FQueue这么一个抽象队列,内部实现是FSQueue,也就是基于文件的FIFO队列。这个队列是多个文件组成的。每个文件默认大小在150M,超过即切换一个新文件来写。读的时候如果读到尾部,则查找下一个文件进行读取。数据文件名以idb为后缀,并且从编号1开始递增,除了数据文件外,每个队列还有个db为后缀的索引文件,记录当前写和读的数据文件编号和偏移量。目录结构大概是这样: --fqueue --fqueuedata_1.idb --fqueuedata_2.idb --…… --icqueue.db 文件的存储比较有特色,采用 MappedByteBuffer做文件读写, MappedByteBuffer是java nio引入的文件内存映射方案,读写性能极高,但是也有一定的问题,比如说内存占用,以及数据刷入设备的不确定性和关闭问题。在fqueue中,每隔10毫秒会强制force一次buffer,将修改过的数据刷入设备。对于关闭问题,则采用那个技巧,示例代码: /** * 关闭索引文件 */ public void close() { try { mappedByteBuffer.force(); AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]); getCleanerMethod.setAccessible(true); sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer, new Object[0]); cleaner.clean(); } catch (Exception e) { log.error("close logindexy file error:", e); } return null; } }); fc.close(); dbRandFile.close(); mappedByteBuffer = null; fc = null; dbRandFile = null; } catch (IOException e) { log.error("close logindex file error:", e); } } 利用反射,并且使用了sun特有的类,不具有可移植性。MappedByteBuffer还有一个问题是map的代价比较高,可能在切换文件的时候fqueue会有一定程度的阻塞现象。 存储的性能,我在我的机器测试了下,似乎没有作者宣称的那么高,我的机器是5400转的普通SATA盘,写入1K数据的平均QPS在8000左右。我估计fqueue的性能跟磁盘有很大关系,如果使用15000转的SAS盘应该能有很大改观。 网络层直接使用了jmemcached的实现, jmemcached是一个java实现的memcached,通常用于单元测试之类。看情况fqueue也支持memcached的二进制协议了。网络框架使用了netty3,这些就不多说了。自己看都明白。额外提一下,作者做的单元测试使用了 xmemcached,咔咔,广而告之。 总体来说fqueue是一个整体上很清爽和轻量级的MQ实现,适合一些特定的场景,至于性能,我们下周准备做个压测,到时候再谈吧。
开源的java memcached client—— xmemcached发布1.3.4版本,主要改进如下:
1、修复一个相对严重的bug,在解析二进制协议时如果遇到从服务端返回的错误信息,会导致连接异常断开;如果你没有使用binary协议,不会遇到此问题。建议使用xmemcached并且使用二进制协议的朋友升级到此版本。
2、允许XMemcachedClientFactoryBean配置opTimeout选项。
3、添加RoundRobinMemcachedSessionLocator,轮询的连接选择器,仅用于kestrel或者memcacheq集群,这些应用都不要求同一个key要保存在固定的服务器上,而仅是作为集群分担负载。
4、KetamaMemcachedSessionLocator添加额外选项,允许配置是否兼容 nginx-upstream-consistent,这个是网友
wolfg1969贡献的patch。如果要使得xmc的一致性哈希算法兼容nginx-upstream-consistent,只要设置cwNginxUpstreamConsistent为true即可,示范代码:
MemcachedClientBuilder builder = new XMemcachedClientBuilder( AddrUtil.getAddresses(servers)); builder.setSessionLocator(new KetamaMemcachedSessionLocator( true));
5、修复bug,包括issue 132 , issue 142 , issue 133 , issue 139 , issue 142 , issue 145 ,issue 150等。
如果你使用maven,只要简单升级版本即可:
<dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>1.3.4</version> </dependency>
下载地址:
http://code.google.com/p/xmemcached/downloads/list
这篇blog迟到了很久,本来是想写另一个跟网络相关bug的查找过程,偷偷懒,写下最近印象比较深刻的bug。这个bug是我的同事水寒最终定位到的。 前几个月同事报告称有一个线上MQ集群会同一时间抛出ArrayIndexOutOfBoundsException这个异常,也就是数组越界。查看源码,除去一些无关紧要的细节大概是这样子: public class ConnectionSelector{ private AtomicInteger sets=new AtomicInteger(0);
public void selectConnection(List<Connection> connList){ if(connList==null){ return null; } final int size = connList.size(); if (size == 0) { return null; } return connList.get(sets.incrementAndGet() % size); }
} 很显然,这里的本意是实现一个轮询的连接选择器,返回一个选中的连接。使用AtomicInteger递增并对链表大小取模,返回结果索引位置的连接。异常抛出的位置就是我代码中标红的位置。 显然,这里有两种可能,一种情况下是说在执行那一行代码的时候,connList的大小缩小了(也就是说连接可能被其他线程移出),那么导致取模的结果越界。另一种可能是取模的结果本身确实超过了列表范围。 第一种情况是完全可能的,因为服务器的连接可能随时断开或者重连,但是这种情况相对非常少见,因此我们这里并没有对这个选择过程做同步,主要是从性能的角度出发,偶尔的失败可以接受。很遗憾的是,我被我的思维惯性误导了,从来没有怀疑过第二种情况,总是认为是不是真的连接恰巧断开导致这个异常,但是却无法解释这个异常发生后就一直错误下去,无法自行恢复。 为什么说思维惯性误导呢?这里的问题其实是负数取模的问题,对一个负数进行取模,结果会是正数还是负数?答案是结果因语言而异。 我很早以前在使用Ruby的时候做过测试,负数取模结果为正数,例如在irb里尝试下: >> -1000%3 => 2 >> -2001%4 => 3 这个印象持续至今,在clojure里结果也是这样子: Clojure 1.2.1 user=> (mod -1000 3) 2 user=> (mod -2001 4) 3 可以再试试python: Python 2.7.1 (r271:86832, Jun 16 2011, 16:59:05) [GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> -10000%3 2 >>> -2001%4 3 这三种语言的结果完全一致,结果都为正数。这个惯性思维延续到java却不成立了,可惜我根本没做测试,让我们试下: public static void main(final String[] args) { System.out.println(-1000 % 3); System.out.println(-2001 % 4); } 打印结果为: -1 -1 果然,在java里负数取模的结果为负数,而不是我习惯性地认为是正数。因此最终的定位到的原因就是sets这个变量递增超过Integer.MAX_VALUE后越界变成负数了,取模的结果为负数,导致抛出数组越界的异常,这也解释了为什么同一个集群都在同一时间出问题,因为这个集群内的机器启动时间相邻并且调用这个方法次数相对平均。 修正问题很简单,加个Math.abs就好。 Update:加个abs是不够的,因为Math.abs的javadoc提醒了: Note that if the argument is equal to the value of Integer.MIN_VALUE, the most negative representable int value, the result is that same value, which is negative.
也就是说对Integer.MIN_VALUE做abs结果仍然是负数。尽管在这个场景中失败一次可以接受,但是最好的办法还是回复中steven提到的抵消符号位的做法: (sets.incrementAndGet() & 0x7FFFFFFF) % size 这个问题更详细的讨论后来我找到 这篇博客,作者讨论几种语言和计算器的这个问题的结果,给出了一些结论。不过我觉的这个结论可能也不是那么可靠,特别是对c/c++来说,很大程度上应该还是依赖于实现,最可靠的办法还是强制结果为正。 这个bug的几个教训: 1、首先是第一次出现的时候没有引起足够重视,重启解决问题后没有深究。有句玩笑话:99%的程序问题都可以通过重启解决。但是事实上问题仍然存在,该发生的终究还会发生。不管你信不信,它就是发生了,这是一个奇迹。 2、注意大脑的思维惯性,经验主义和教条主义都不可取。最近在读一本好书《 暗时间》,大脑误导我们的手段可是多种多样。 3、最后就是这个负数取模的结果因语言而异,不要依赖于特定实现。
没有耐心看经过的请直接拉到末尾看slide列表。 这个聚会是由江宏首先提议的,我参与协助。目的是让长三角地区对 clojure语言感兴趣,或者正在使用的朋友当面认识和交流一下。会议的组织过程一波三折,首先是会议地点本来定在了上海google办公室,但是后来google那边又说不让过去,我再联系了原来淘宝网的同事火狐,经过他的努力和帮助,最终将地点确定在了上海大众点评。要感谢大众点评网和火狐的帮助,不然这次活动估计就黄了。会议的日期选定也比较偶然,跟七夕撞在了一天,并且8月6号这天说台风“梅花”要在江浙一带登陆,上海要刮多少级多少级的大风,加上我和杭州的几个朋友过去要坐高铁,那心里就七上八下了,搞不好就要被“掩埋”了。我们还开玩笑说最好买火车中段车厢的票为妙。 8月6日一早,和同在淘宝的杨冬,加上两位做ROR的朋友一起出发,天气没有想象的糟糕,高铁一个小时就到了,转地铁到大众点评网大概也才中午12点左右。打了电话给火狐,一起吃了饭然后就直奔大众点评网。大众点评网的前台大厅装修也是非常熟悉的橙色,很意外周6有很多人,后来才知道是在搞招聘会和培训。这时候,江宏他们也从昆山赶到了,火狐帮我们定的会议室很大,足够容纳20号人左右。陆续有人达到,到约定的1点的时候,我记的是来了大概11还是12个人,还有几个朋友在路上,因此我们决定推迟到1点半再开始。最终来的人估计有15个以上,估计报名的都来了。 1点半正式开始,首先是我来讲《clojure概览》这个topic,主要是一个clojure语言的介绍。这个是我上周开始准备的,在去年《clojure的魅力》的基础上做了删减和增加,听取江宏的意见增加了示例和引子。上周也在我们的团队讲过一次。轻车熟路,也为了给后面的topic留出时间,我讲的比较快,大概40分钟就结束了。 接下来是孙宁(sunng87)讲《clojure开发的生命周期管理》,我对clojure的周边工具并不熟悉,趁机更好地了解了很多 clojure开发过程中用到的工具和资料,推荐对clojure开发感兴趣的朋友看下。尝试了下 clooj,比我预期的要好,遗憾的是还没有语法高亮,推荐初学clojure的朋友可以尝试下这个轻量级的IDE。目前最好的clojure IDE还是idea里的La Clojure插件。最后孙宁顺便广告了下 lein-control插件,这是孙宁构建的一个 clojure-control的lein插件,他还贡献了一个类似python里 fabric的clojure DSL实现,让clojure-control更易用。 接下来是江宏介绍他们开发 trakrapp.com这个纯clojure实现的网站中使用的技术,以及遇到的问题和经验。这个网站基于 compojure这个框架实现的,前端采用backbone.js,后端是MongoDB和postgresql,可以说都是非常“新潮”的技术。他在谈遇到问题的时候,提到clojure的stack trace又长又丑,这一点深有体会,clojure的异常堆栈包含了java和clojure的,整个调用链相对较长,非常不利于问题的排查,不知道后续clojure会不会对这一点做出改进。 接下来是林晴介绍他们一个用scala实现的类似domino的企业OA系统,不过他这个例子给我的感觉更多是发挥了mongodb的schema free的特点,没有体现出使用scala的好处来。我对scala的观点一直很明确,scala想做JVM上的c++,从个人角度不喜欢这种多范式的语言,并且语法不符合我的胃口,特别是类型系统这块特别复杂,我怕我在写scala的时候还要参考一本厚厚的reference,这不是我想要的。而clojure的核心就非常小,相对符合我的期望。 作为东道主的火狐介绍了 大众点评网的新架构以及他们从.net往java迁移的经验,他们的新架构也是做服务化和中心化,对于.net和java平台来说,迁移更多是从人力成本和一些其他因素决定的,当然,迁移最重要的还是要有公司高层的全力支持,特别重要的一点是如何让老员工也参与这个过程。因为老员工对现有系统和业务最熟悉,将他们排除在外闭门造车是注定要失败的。 最后是同样来自昆山文石的吴哲介绍如何在半天内实现一个HTML 5的游戏,他介绍的 processing.js非常有趣,processing本身是一门编程语言,有人将它移植到了js上,可以直接在支持html5上浏览器展现,效果相当cool。巧合的是我在回去后的第二天去书店的时候,竟然在某个角落看到《 processing互动编程艺术》这本书,买了下来准备了解下。做数据图形化的同学可以关注下。 总体来讲,这次聚会的效果超过我的预期,在超强台风的阴影下和七夕爱情的感召下还有这么多人赶过来,作为组织者之一非常感动。并且topic讲座也让我学习了一些东西,最重要的是当面认识了一些网上交流过的朋友,给我印象深刻的是看起来非常老成的孙宁,完全不像个85后。还有个印象深刻的细节是现场的5,6台mbp,这里面还是因为有同学是在搞ROR的因素。 最后,给下slide的链接如下: 1,我的《 clojure概览》,源码在 github上。 2,孙宁的《 Clojure开发的生命周期管理》, lein-control和 clojure-control。 3,江宏的《 Clojure web development》,他们开发的网站 4,吴哲的《 How to build a html5 game in half a day》 5,火狐的《 大众点评网新架构》 6, cn-clojure主页
最近看了篇在google reader里分享非常多的文章,我表示很无语,文章在 这里,题目是《 Peter Norvig:编程语言的选择并不重要》。简单来讲这文章就是鼓吹python的,然后举了很多例子说python描述算法比Lisp容易。这个无需多说,图灵模型本来就比lambda演算更适合描述算法。 我想说的是,文中明明提了,Peter norvig说的是: 就更一般意义上的编程而言,在Google和其他地方,我认为语言的选择并不如其他方面的选择那么重要:如果你有了正确的总体架构、正确的程序员团队、正确的开发过程(能够快速开发、持续改善),那么很多语言都能胜任;但如果以上的东西你没有,那无论选择什么语言,你都会陷入一团糟。 这句话的意思很明显,在google或者其他什么地方,编程语言的选择,比之正确的架构,正确的团队以及正确的开发过程,对最终任务的完成影响不是那么大。但并非所谓"编程语言的选择不重要“,这种断章取义的题目除了吸引眼球外,没有任何益处。 很多编程语言都可以胜任你要完成的编程任务,你完全可以用C去写CGI,用汇编去写消息中间件,只要你有正确的架构,正确的团队和开发过程,你应该总能完成任务。但是选择适当的编程语言可以让你事半功倍,更少的代码,更高的开发效率。从ROR以及动态语言的流行来看,选择编程语言,真的很重要。 除非你的规模达到google的程度,性能意味着美元,一秒的优化意味着成千甚至上亿的dollar的时候,也许你可以说下编程语言的选择不重要。 最后,我还想鄙视下分享这篇文章的大爷们,你们真的看了文章吗?还只是冲着这标题,急急忙忙地献宝式地分享了?咱们淡定点行不?
转自 http://hjiang.net/archives/484Clojure-CN要组织周期性的线下技术交流活动了。如果你热爱程序设计的相关技术,并且住在长三角一带,欢迎来参与活动。只要填一下这个调查表就可以: http://www.diaochapai.com/survey548296更多: 关注我的blog的朋友应该都知道我这一年都一直在关注 clojure这门语言,后来还搞了个 cn-clojure的google group。 hjiang的公司在使用clojure做商业项目,他们公司可能是国内唯一在使用clojure的商业团体,他上周跟我提起想搞这么个活动,促进对clojure学习和使用的交流,并且不局限在clojure语言本身。今年其实给自己一个目标也是去尝试推动一些事情,我对clojure纯粹是技术上的兴趣,未来也不排除去找一份专职写clojure的工作,如果你或者他(她)对clojure语言(或者函数式语言)感兴趣,欢迎来参加这次聚会,填写下这个调查表: http://www.diaochapai.com/survey548296。 我们在调查完成后统计下大家的兴趣点和地理分布,最后决定在哪里举办,以及确定talk列表和聚会形式等。 我还申请了一个域名 http://cnlojure.org,在github上建了个page,这件事的进展会放到这个网页上。
格式化源码是很常见的需求,emacs有个indent-region函数用于格式化选定的代码,前提是你处在某个非text mode下,如c-mode或者java-mode之类。如果要格式化整个文件,你需要先选定整个文件(C-x-h),然后调用indent-region(或者 C-M-\ )。两个命令总是麻烦,我们可以定义个函数搞定这一切,并绑定在一个特定键上,实现一键格式化: ;;格式化整个文件函数 (defun indent-whole () (interactive) (indent-region (point-min) (point-max)) (message "format successfully")) ;;绑定到F7键 (global-set-key [f7] 'indent-whole) 将这段代码添加到你的emacs配置文件(~/.emacs),重启emacs,以后格式化源码都可以用F7一键搞定。
1.选定宿主语言,最好选用元编程能力强悍的语言作为宿主语言。 2.确定DSL的样子,让脑袋空白,不去考虑任何实现问题,纯粹思考你想要实现的dsl是什么样子 3.用你想要的DSL写一个最基本的例子,只包括最基本的功能。 4.开始实现DSL,尽快让你的DSL例子以dirty and quick的方式跑起来。 5.写更多DSL的例子,慢慢包括你想要的所有功能,并一一实现,在这个过程中你可能改变DSL的样子,原来模糊的东西渐渐清楚。 6.大功告成,review你的代码并添加自动化测试,将代码中dirty和bad smell的部分一一剔除。 7.让你的DSL接受实际应用的考验吧。
update: Allow passing command line arguments to task now.
1.What is clojure-control?
The idea came from node-control.
Define clusters and tasks for system administration or code deployment, then execute them on one or many remote machines.
Clojure-control depends only on OpenSSH and clojure on the local
control machine.Remote machines simply need a standard sshd daemon.
2.Quick example
Get the current date from the two machines listed in the 'mycluster' config with a single command:
(ns samples (:use [control.core :only [task cluster scp ssh begin]])) ;;define clusters (cluster :mycluster :clients [ { :host "a.domain.com" :user "alogin"} { :host "b.domain.com" :user "blogin"} ]) ;;define tasks (task :date "Get date" [] (ssh "date")) ;;start running (begin)
If saved in a file named "controls.clj",run with
java -cp clojure.jar:clojure-contrib.jar:control-0.1-SNAPSHOT.jar clojure.main controls.clj mycluster date
Each machine execute "date" command ,and the output form the remote
machine is printed to the console.Exmaple console output
Performing mycluster Performing date for a.domain.com a.domain.com:ssh: date a.domain.com:stdout: Sun Jul 24 19:14:09 CST 2011 a.domain.com:exit: 0 Performing date for b.domain.com b.domain.com:ssh: date b.domain.com:stdout: Sun Jul 24 19:14:09 CST 2011 b.domain.com:exit: 0
Each line of output is labeled with the address of the machine the
command was executed on. The actual command sent and the user used to
send it is displayed. stdout and stderr output of the remote process
is identified as well as the final exit code of the local ssh
command.
3.How to scp files?
Let's define a new task named deploy
(task :deploy "scp files to remote machines" [] (scp ("release1.tar.gz" "release2.tar.gz") "/home/alogin/"))
Then it will copy release1.tar.gz and release2.tar.gz to remote
machine's /home/alogin directory.
4.Where is it? It's on github, https://github.com/killme2008/clojure-control Any suggestion or bug reports welcomed.
aviator是一个轻量级的、高性能的Java表达式求值器,主要应用在如工作流引擎节点条件判断、MQ中的消息过滤以及某些特定的业务场景。
自从上次发布1.0后,还发过1.01版本,不过都没怎么宣传。这次发布一个2.0的里程碑版本,主要改进如下:
1、完整支持位运算符,与java完全一致。位预算符对实现bit set之类的需求还是非常必须的。
2、性能优化,平均性能提升100%,函数调用性能提升200%,最新的与groovy和JEXL的性能测试看这里
http://code.google.com/p/aviator/wiki/Performance
3、添加了新函数,包括long、double、str用于类型转换,添加了string.indexOf函数。
4、完善了用户手册,更新性能测试。
下载地址: http://code.google.com/p/aviator/downloads/list
项目主页: http://code.google.com/p/aviator/
用户指南: http://code.google.com/p/aviator/w/list
性能报告: http://code.google.com/p/aviator/wiki/Performance
源码: https://github.com/killme2008/aviator
Maven引用(感谢许老大的帮助): <dependency> <groupId>com.googlecode.aviator</groupId> <artifactId>aviator</artifactId> <version>2.0</version> </dependency>
这个项目目前用在我们的MQ产品中做消息过滤,也有几个公司外的用户告诉我他们也在用,不过估计不会很多。有这种需求的场景还是比较少的。这个项目实际上是为我们的MQ定制的,我主要想做到这么几点:
(1)控制用户能够使用的函数,不允许调用任何不受控制的函数。
(2)轻量级,不需要嵌入groovy这么大的脚本引擎,我们只需要一个剪裁过的表达式语法即可。
(3)高性能,最终的性能在某些场景比groovy略差,但是已经非常接近。
(4)易于扩展,可以容易地添加函数扩展功能。语法相对固定。
(5)函数的调用避免使用反射。因此没使用dot运算符的函数调用方式,而是更类似c语言和lua语言的函数调用风格。函数是一等公民,seq库的风格很符合我的喜好。
seq这概念来自clojure,我将实现了java.util.Collection接口的类和数组都称为seq集合,可以统一使用seq库操作。例如假设我有个list:
Map<String, Object> env = new HashMap<String, Object>();
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(3);
list.add(100);
list.add(-100);
env.put("list", list);
可以做这么几个事情,度量大小: count(list) 判断元素是否存在: include(list,3) 过滤元素,返回大于0的元素组成的seq: filter(list,seq.gt(0))
对集合里的元素求和,应用reduce: reduce(list,+,0) 遍历集合元素并打印: map(list,println) 最后,你还可以排序: sort(list) 这些函数类似FP里的高阶函数,使用起来还是非常爽的。 对函数调用的优化,其实只干了一个事情,原来函数调用我是将所有参数收集到一个list里面,然后再转成数组元素交给AviatorFunction调用。这里创建了两个临时对象:list和数组。这其实是没有必要的,我只要在AviatorFunction里定义一系列重载方法,如: public AviatorObject call(Map<String, Object> env);
public AviatorObject call(Map<String, Object> env, AviatorObject arg1);
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2);
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3);
  就不需要收集参数,而是直接invokeinterface调用AviatorFunction相应的重载方法即可。我看到在JRuby和Clojure里的方法调用都这样干的。过去的思路走岔了。最终也不需要区分内部的method和外部的function,统一为一个对象即可,进一步减少了对象创建的开销。
转载请注明出处 http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html
上周在线上系统发现了两个bug,值得记录下查找的过程和原因。以后如果还有查找bug比较有价值的经历,我也会继续分享。
第一个bug的起始,是在线上日志发现一个频繁打印的异常——java.lang.ArrayIndexOutOfBoundsException。但是却没有堆栈,只有一行一行的ArrayIndexOutOfBoundsException。没有堆栈,不知道异常是从什么地方抛出来的,也就不能找到问题的根源,更谈不上解决。题外,工程师在用log4j记录错误异常的时候,我看到很多人这样用(假设e是异常对象):
log.error("发生错误:"+e);
或者:
log.error("发生错误:"+e.getMessage());
这样的写法是不对,只记录了异常的信息,而没有将堆栈输出到日志,正确的写法是利用error的重载方法:
log.error("xxx发生错误",e);
这样才能在日志中完整地输出异常堆栈来。如何写好日志是另一个话题,这里不展开。继续我们的找bug经历。刚才提到,我们线上日志一直出现一行错误信息ArrayIndexOutOfBoundsException却没有堆栈,是我们没有正确地写日志吗?检查代码不是的,这个问题其实是跟JDK5引入的一个新特性有关,对于一些频繁抛出的异常,JDK为了性能会做一个优化,在JIT重新编译后会抛出没有堆栈的异常。在使用server模式的时候,这个优化是开启的,我们的服务器跑在server模式下并且jdk版本是6,因此在频繁抛出ArrayIndexOutOfBoundsException异常一段时间后优化开始起作用,只抛出没有堆栈的异常信息了。
那么怎么解决这个问题呢?因为这个优化是在JIT重新编译后才起作用,因此一开始抛出异常的时候还是有堆栈的,所以可以查看较旧的日志,寻找有完整堆栈的异常信息。但是由于我们的日志太大,会定期删除,我们的服务器也启动了很长时间,因此查找日志不是很靠谱的方法。
另一个解决办法是暂时禁用这个优化,强制要求每次都要抛出有堆栈的异常。幸好JDK提供了选项来关闭这个优化,配置JVM参数
-XX:-OmitStackTraceInFastThrow
就可以禁止这个优化(注意选项中的减号,加号是启用)。
我们找了台机器,配置了这个参数并重启一下。过了一会就找到问题所在,堆栈类似这样
Caused by: java.lang.ArrayIndexOutOfBoundsException: -1831238
at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:436)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2081)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:1996)
at java.util.Calendar.setTimeInMillis(Calendar.java:1109)
at java.util.Calendar.setTime(Calendar.java:1075)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:876)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:869)
at java.text.DateFormat.format(DateFormat.java:316)
读者肯定猜到了,这个问题是由于SimpleDateFormat的误用引起的。 SimpleDateFormat的 javadoc中有这么句话:
Date formats are not synchronized. It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.
但是很悲剧的是这句话是放在整个doc的最后面,在我看来,这句话应该放在最前面才对。简单来说就是SimpleDateFormat不是线程安全的,你要么每次都new一个来用,要么做加锁来同步使用。
出问题的代码就是由于工程师认为SimpleDateFormat的创建代价很高,然后搞了个map做缓存,所有线程共用这个instance做format,同时没有做同步。悲剧就诞生了。
这里就引出我想提到的第二点问题,在使用一个类或者方法的时候 ,最好能详细地看下该类的javadoc,JDK的javadoc是做的非常好的,javadoc除了做说明之外,通常还会给示例,并且会点出一些关键问题,如线程安全性和平台移植性。
最后,我将做个测试,到底在使用SimpleDateFormat怎么做才是最好的方式?假设我们要实现一个formatDate方法将日期格式化成"yyyy-MM-dd"的格式。
第一个方法是每次使用都创建一个instance,并调用format方法:
public static String formatDate1(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return format.format(date);
}
第二个方法是只创建一个instance,但是在调用方法的时候做同步:
private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
public static synchronized String formatDate2(Date date) {
return formatter.format(date);
}
第三个方法比较特殊,我们为每个线程都缓存一个instance,存放在ThreadLocal里,使用的时候从ThreadLocal里取就可以了:
private static ThreadLocal<SimpleDateFormat> formatCache = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static String formatDate3(Date date) {
SimpleDateFormat format = formatCache.get();
return format.format(date);
}
然后我们测试下三个方法并发调用下的性能并做一个比较,并发100个线程循环调用1000万次,记录耗时。我们设置了JVM参数:
-Xmx512m -XX:CompileThreshold=10000
设置堆最大为512M,设置当一个方法被调用1万次的时候就被JIT编译。测试的结果如下:
|
第1次测试
|
第2次测试
|
第3次测试
|
formatDate1
|
50545
|
49365
|
53532
|
formatDate2
|
10895
|
10761
|
10673
|
formatDate3 |
10386 |
9919 |
9527 |
(单位:毫秒) 从结果来看,方法1最慢,方法3最快,但是就算是最慢的方法1也可以达到每秒钟 200 20万次的调用量,很少有系统能达到这个量级。这个点很难成为你系统的瓶颈所在。从我的角度出发,我会建议你用方法1或者方法2,如果你追求那么一点性能提升的话,可以考虑用方法3,也就是用ThreadLocal做缓存。 总结下本文找bug经历想表达的几点想法: (1)正确地打印错误日志 (2)在server模式下,最好都设置 -XX:-OmitStackTraceInFastThrow (3)使用类或者方法的时候,最好能详细阅读下javadoc,很多问题都能找到答案 (4)使用SimpleDateFormat的时候要注意线程安全性,要么每次new,要么做同步,两者的性能有差距,但是这个差距很难成为你的性能瓶颈。
下篇文章我再分享另一个bug的查找经历,也是比较有趣,可以看到一些工具的使用。
Nagle算法的立意是良好的,避免网络中充塞小封包,提高网络的利用率。但是当Nagle算法遇到 delayed ACK悲剧就发生了。Delayed ACK的本意也是为了提高TCP性能,跟应答数据捎带上ACK,同时避免 糊涂窗口综合症,也可以一个ack确认多个段来节省开销。 悲剧发生在这种情况,假设一端发送数据并等待另一端应答,协议上分为头部和数据,发送的时候不幸地选择了write-write,然后再read,也就是先发送头部,再发送数据,最后等待应答。发送端的伪代码是这样 write(head); write(body); read(response); 接收端的处理代码类似这样: read(request); process(request); write(response); 这里假设head和body都比较小,当默认启用nagle算法,并且是第一次发送的时候,根据nagle算法,第一个段head可以立即发送,因为没有等待确认的段;接收端收到head,但是包不完整,继续等待body达到并延迟ACK;发送端继续写入body,这时候nagle算法起作用了,因为head还没有被ACK,所以body要延迟发送。这就造成了发送端和接收端都在等待对方发送数据的现象,发送端等待接收端ACK head以便继续发送body,而接收端在等待发送方发送body并延迟ACK,悲剧的无以言语。这种时候只有等待一端超时并发送数据才能继续往下走。 正因为nagle算法和delayed ack的影响,再加上这种write-write-read的编程方式造成了很多网贴在讨论为什么自己写的网络程序性能那么差。然后很多人会在帖子里建议禁用Nagle算法吧,设置TCP_NODELAY为true即可禁用nagle算法。但是这真的是解决问题的唯一办法和最好办法吗? 其实问题不是出在nagle算法身上的,问题是出在write-write-read这种应用编程上。禁用nagle算法可以暂时解决问题,但是禁用nagle算法也带来很大坏处,网络中充塞着小封包,网络的利用率上不去,在极端情况下,大量小封包导致网络拥塞甚至崩溃。因此,能不禁止还是不禁止的好,后面我们会说下什么情况下才需要禁用nagle算法。对大多数应用来说,一般都是连续的请求——应答模型,有请求同时有应答,那么请求包的ACK其实可以延迟到跟响应一起发送,在这种情况下,其实你只要避免write-write-read形式的调用就可以避免延迟现象,利用writev做聚集写或者将head和body一起写,然后再read,变成write-read-write-read的形式来调用,就无需禁用nagle算法也可以做到不延迟。 writev是系统调用,在Java里是用到 GatheringByteChannel.write(ByteBuffer[] srcs, int offset, int length)方法来做聚集写。这里可能还有一点值的提下,很多同学看java nio框架几乎都不用这个writev调用,这是有原因的。主要是因为Java的write本身对ByteBuffer有做临时缓存,而writev没有做缓存,导致测试来看write反而比writev更高效,因此通常会更推荐用户将head和body放到同一个Buffer里来避免调用writev。 下面我们将做个实际的代码测试来结束讨论。这个例子很简单,客户端发送一行数据到服务器,服务器简单地将这行数据返回。客户端发送的时候可以选择分两次发,还是一次发送。分两次发就是write-write-read,一次发就是write-read-write-read,可以看看两种形式下延迟的差异。 注意,在windows上测试下面的代码,客户端和服务器必须分在两台机器上,似乎winsock对loopback连接的处理不一样。 服务器源码: package net.fnil.nagle;
import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket;
public class Server { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(8000)); System.out.println("Server startup at 8000"); for (;;) { Socket socket = serverSocket.accept(); InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream();
while (true) { try { BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line = reader.readLine(); out.write((line + "\r\n").getBytes()); } catch (Exception e) { break; } } } } }
服务端绑定到本地8000端口,并监听连接,连上来的时候就阻塞读取一行数据,并将数据返回给客户端。 客户端代码: package net.fnil.nagle;
import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception { // 是否分开写head和body boolean writeSplit = false; String host = "localhost"; if (args.length >= 1) { host = args[0]; } if (args.length >= 2) { writeSplit = Boolean.valueOf(args[1]); }
System.out.println("WriteSplit:" + writeSplit);
Socket socket = new Socket();
socket.connect(new InetSocketAddress(host, 8000)); InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String head = "hello "; String body = "world\r\n"; for (int i = 0; i < 10; i++) { long label = System.currentTimeMillis(); if (writeSplit) { out.write(head.getBytes()); out.write(body.getBytes()); } else { out.write((head + body).getBytes()); } String line = reader.readLine(); System.out.println("RTT:" + (System.currentTimeMillis() - label) + " ,receive:" + line); } in.close(); out.close(); socket.close(); }
}
客户端通过一个writeSplit变量来控制是否分开写head和body,如果为true,则先写head再写body,否则将head加上body一次写入。客户端的逻辑也很简单,连上服务器,发送一行,等待应答并打印RTT,循环10次最后关闭连接。 首先,我们将writeSplit设置为true,也就是分两次写入一行,在我本机测试的结果,我的机器是ubuntu 11.10: WriteSplit:true RTT:8 ,receive:hello world RTT:40 ,receive:hello world RTT:40 ,receive:hello world RTT:40 ,receive:hello world RTT:39 ,receive:hello world RTT:40 ,receive:hello world RTT:40 ,receive:hello world RTT:40 ,receive:hello world RTT:40 ,receive:hello world RTT:40 ,receive:hello world
可以看到,每次请求到应答的时间间隔都在40ms,除了第一次。linux的delayed ack是40ms,而不是原来以为的200ms。第一次立即ACK,似乎跟linux的quickack mode有关,这里我不是特别清楚,有比较清楚的同学请指教。 接下来,我们还是将writeSplit设置为true,但是客户端禁用nagle算法,也就是客户端代码在connect之前加上一行: Socket socket = new Socket(); socket.setTcpNoDelay(true); socket.connect(new InetSocketAddress(host, 8000)); 再跑下测试: WriteSplit:true RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:1 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world
这时候就正常多了,大部分RTT时间都在1毫秒以下。果然禁用Nagle算法可以解决延迟问题。 如果我们不禁用nagle算法,而将writeSplit设置为false,也就是将head和body一次写入,再次运行测试(记的将setTcpNoDelay这行删除): WriteSplit:false RTT:7 ,receive:hello world RTT:1 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world RTT:0 ,receive:hello world
结果跟禁用nagle算法的效果类似。既然这样,我们还有什么理由一定要禁用nagle算法呢?通过我在 xmemcached的压测中的测试,启用nagle算法在小数据的存取上甚至有一定的效率优势,memcached协议本身就是个连续的请求应答的模型。上面的测试如果在windows上跑,会发现RTT最大会在200ms以上,可见winsock的delayed ack超时是200ms。 最后一个问题,什么情况下才应该禁用nagle算法?当你的应用不是这种连续的请求——应答模型,而是需要实时地单向发送很多小数据的时候或者请求是有间隔的,则应该禁用nagle算法来提高响应性。一个最明显是例子是telnet应用,你总是希望敲入一行数据后能立即发送给服务器,然后马上看到应答,而不是说我要连续敲入很多命令或者等待200ms才能看到应答。 上面是我对nagle算法和delayed ack的理解和测试,有错误的地方请不吝赐教。 转载请注明出处: http://www.blogjava.net/killme2008/archive/2011/06/30/353441.html
去年做的分享,一直上传slideshare失败,今天又试了下,成功了。这个主题主要介绍Java NIO编程的技巧和陷阱,解读了一些NIO框架的源码,以及编写高性能NIO网络框架所需要注意的技巧和缺陷。关注这方面的朋友可以看一下。去年写了篇blog提供了pdf版本的下载,看 这里。
开源memcached的java客户端 xmemcached发布1.3.3,主要改进如下: 1、memcached 1.6添加了不少新特性,具体可以参考《what's new in memcached》( 1) ( 2)这两个帖子。xmemcached将及时跟进这些新特性。1.3.3这个版本 实现了二进制协议中新的两个命令touch和GAT(get and touch)。这两个功能可以说是千呼万唤始出来,终于可以不用get-set来重新设置数据的超时时间,利用touch或者GAT可以简单地更新数据的超时时间。1.3.3新增加四个方法: public boolean touch(final String key, int exp, long opTimeout) throws TimeoutException, InterruptedException, MemcachedException; public boolean touch(final String key, int exp) throws TimeoutException, InterruptedException, MemcachedException; public <T> T getAndTouch(final String key, int newExp, long opTimeout) throws TimeoutException, InterruptedException, MemcachedException; public <T> T getAndTouch(final String key, int newExp) throws TimeoutException, InterruptedException, MemcachedException; 其中touch用于设置数据新的超时时间,getAndTouch则是在获取数据的同时更新超时时间。例如用memcached存储session,可以在每次get的时候更新下数据的超时时间来保活。 请注意,这四个方法仅在使用memcached 1.6并且使用二进制协议的时候有效。 2、setLoggingLevelVerbosity方法可以作用于二进制协议。 3、重构错误处理模块,使得异常信息更友好。4、将KeyIterator和getKeyIterator声明为deprecated,因为memached 1.6将移除stats cachedump协议,并且stats cachedump返回数据有大小限制,遍历功能不具实用性。 5、修复Bug,包括 issue 126 , issue 127, issue 128, issue 129。 下载地址: http://code.google.com/p/xmemcached/downloads/list源码: https://github.com/killme2008/xmemcachedmaven引用: <dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>1.3.3</version> </dependency>
Update: 如果遇到在search不存在的path报段错误,这是node-zookeeper的一个bug,我暂时修复了下并提交了pull request,你可以暂时用我修改的node-zookeeper https://github.com/killme2008/node-zookeeper
我们已经开始在产品使用 zookeeper了,那么维护工具也必然需要,所谓兵马未动,粮草先行。请同事帮忙看过几个开源项目后,并没有特别让人满意的。 我想要的功能比较简单。首先,希望能将zookeeper集群的数据展示为树形结构,跟zookeeper模型保持一致。可以逐步展开每层的节点,每次展开都是延迟加载从zk里取数据,这样不会对zk造成太大压力。其次,除了展示树形结构外,我还希望它能展示每个path的属性和数据,更进一步,如果数据是文本的,我希望它可编辑。当然,因为编辑功能是比较危险的行为,我还希望这个管理工具有个简单的授权验证机制。 最终,我自己写了这么个东西,取名为 node-zk-browser,基于node.js的 express.js框架和 node-zookeeper客户端实现的。我将它放在了github上 https://github.com/killme2008/node-zk-browser 你可以自己搭建这个小app, npm几乎能帮你搞定大部分工作。界面不美观,实用为主,几张运行时截图   
Node.js Undocumented(1) Node.js Undocumented(2) 写这种系列blog,是为了监督自己,不然我估计我不会有动力写完。这一节,我将介绍下Buffer这个module。js本身对文本友好,但是处理二进制数据就不是特别方便,因此node.js提供了Buffer模块来帮助你处理二进制数据,毕竟node.js的定位在网络服务端,不能只对文本协议友好。 Buffer模块本身其实没有多少未公开的方法,重要的方法都在文档里提到了,有两个方法稍微值的提下。 Buffer.get(idx) 跟buffer[idx]是一样的,返回的是第idx个字节,返回的结果是数字,如果要转成字符,用String.fromCharCode(code)即可。 Buffer.inspect() 返回Buffer的字符串表示,每个字节用十六进制表示,当你调用console.dir的时候打印的就是这个方法返回的结果。 Buffer真正值的一提的是它的内部实现。Buffer在node.js内部的cpp代码对应的是SlowBuffer类(src/node_buffer.cc),但是两者之间并不是一一对应。 对于创建小于8K的Buffer,其实是从一个pool里slice出来,只有大于8K的Buffer才是每次都new一个SlowBuffer。查看源码(lib/buffer.js):
Buffer.poolSize = 8 * 1024; if (this.length > Buffer.poolSize) { // Big buffer, just alloc one. this.parent = new SlowBuffer(this.length); this.offset = 0;
} else { // Small buffer. if (!pool || pool.length - pool.used < this.length) allocPool(); this.parent = pool; this.offset = pool.used; pool.used += this.length; }
因此,我们可以修改Buffer.poolSize这个“静态”变量来改变池的大小 Buffer.poolSize Buffer类创建的池大小,大于此值则每次new一个SlowBuffer,否则从池中slice返回一个Buffer,如果池剩余空间不够,则新创建一个SlowBuffer做为池。下面的例子打印这个值并修改成16K: console.log(Buffer.poolSize); Buffer.poolSize=16*1024; SlowBuffer类 SlowBuffer类我们可以直接使用的,如果你不想使用Buffer类的话,SlowBuffer类有Buffer模块的所有方法实现,例子如下: var SlowBuffer=require('buffer').SlowBuffer var buf=new SlowBuffer(1024) buf.write("hello",'utf-8'); console.log(buf.toString('utf-8',0,5)); console.log(buf[0]); var sub=buf.slice(1,3); console.log(sub.length); 请注意,SlowBuffer默认不是Global的,需要require buffer模块。 使用建议和性能测试 Buffer的这个实现告诉我们,要使用好Buffer类还是有讲究的,每次创建小于8K的Buffer最好大小刚好能被8k整除,这样能充分利用空间;或者每次创建大于8K的Buffer,并充分重用。我们来看一个性能测试,分别循环1000万次创建16K,4096和4097大小的Buffer,看看耗时多少: function benchmark(size,repeats){ var total=0; console.log("create %d size buffer for %d times",size,repeats); console.time("times"); for(var i=0;i<repeats;i++){ total+=new Buffer(size).length; } console.timeEnd("times"); } var repeats=10000000;
console.log("warm up ") benchmark(1024,repeats); console.log("start benchmark") benchmark(16*1024,repeats); benchmark(4096,repeats); benchmark(4097,repeats);
创建1024的Buffer是为了做warm up。在我机器上的输出: start benchmark create 16384 size buffer for 10000000 times times: 81973ms create 4096 size buffer for 10000000 times times: 80452ms create 4097 size buffer for 10000000 times times: 138364ms
创建4096和创建4097大小的Buffer,只差了一个字节, 耗时却相差非常大,为什么会这样?读者可以自己根据上面的介绍分析下,有兴趣的可以留言。 另外,可以看到创建16K和创建4K大小的Buffer,差距非常小,平均每秒钟都能创建10万个以上的Buffer,这个效率已经足以满足绝大多数网络应用的需求。
leveldb是google最近开源的一个实现,但是它仅是个lib,还需要包装才能使用。 node-leveldb就是一个用node.js包装leveldb的项目,你可以用javascript访问leveldb。 node-leveldb仅提供API,不提供网络接口供外部访问。我fork了个分支,搞了个memcached的adapter,将node-leveldb的API暴露为memcached的文本协议,这样一来你可以直接用现有的memcached client甚至直接telnet上去进行测试。感兴趣的朋友可以测试下。adpater就一个文件 memcached.js。 fork的分支在: https://github.com/killme2008/node-leveldb
编译node-leveldb之后,执行 node memcached-adpater/memcached.js 即可启动memcached adapter在11211端口,你可以telnet上去测试。目前仅支持get/set/delete/quit协议,不支持flag和exptime。
node.js的API文档做的不是很好,有些部分干脆没文档,最终还是要看代码才能解决。我这里将记录下看源码过程中看到的一些API并补充一些测试例子。在玩 node.js的朋友可以瞧瞧。 process.reallyExit(status) 用于进程主动退出,status设置退出的状态码。请注意,reallyExit退出的进程不会调用'exit'事件,下面的代码不会有任何输出: var interval=setInterval(function(){ process.reallyExit(1); },1000); process.on('exit',function(){ console.log("hello"); }); process._kill(pid,sig) 用于给指定pid的进程发送指定信号(类似kill命令),这是个“private”方法,你需要慎重使用,下面的代码会杀死自身的进程: var pid=process.pid process._kill(pid,9); process.binding(name) 非常有用的方法,很奇怪API文档里竟然没提到,这个方法用于返回指定名称的内置模块。例如下面的代码将打印node_net模块所有的可以调用的方法或变量(很多是文档没有提到的并且没有exports的,后续我会介绍下): var binding=process.binding('net'); console.dir(binding); process.dlopen(filename,target) 看源码注释说是用来动态加载node.module的动态链接库的,以后尝试写扩展的时候也许可以尝试一下。 定时器 Node.js的定时器模块的实现是有讲究的,对于超时时间after<=0的callback,会在内部new一个Timer并start(本质是使用libev的timer机制);但是对于after>0的callback,却不是这样。因为在实际应用中,大多数定时器事件的超时时间都是一样的,如果每个事件都new一个Timer并start,代价太高。因此node.js采用了一个类似哈希表的方案,将相同after超时时间的定时器事件组织成链表,以after为key,以链表为value整体构成一张表。每个链表只new一个Timer,这个Timer负责整个链表的定时器事件,当某个事件超时调用后,利用ev_timer_again来高效地重新设置超时时间。 如果你确实希望对于after>0的定时器也每次new一个Timer来处理,那也可以做到,这就要用到前面提到的process.binding方法来获取timer模块,一个例子: var Timer = process.binding('timer').Timer;
var timer=new Timer(); timer.callback=function(){ console.log("callback called"); }; timer.start(1000,0); timer.callback 设定timer的回调函数,当超时的时候调用。 timer.start(after,repeat) 启动定时器,在after毫秒之后调用超时回调;如果repeat==0,则自动停止定时器;如果repeat>0,则在repeat毫秒之后再次调用callback,以repeat毫秒为间隔不断重复下去。 ps.这篇blog刚好是我的第499篇blog,不出意外,第500篇还是继续介绍node.js。
一个公司大了,总有部分人要去做一些通用的东西给大家用,我这里说的基础产品就是这类通用性质的东西,不一定高科技,但是一定很多人依赖你的东西来完成各种各样的功能。做这样的东西,有些体会可以说下。
首先,能集中存储的,就不要分布存储,数据集中存储有单点的危险,但是比之分布式存储带来的复杂度不可同日而语。况且集中式的存储也可以利用各种机制做备份,所谓单点风险远没有想象中那么大。
其次,能利用开源框架的,就不要重复造轮子。程序员都喜欢造轮子,但是造轮子的周期长,并且不一定造的更好。在强调开发效率的互联网时代,如果能直接利用现有框架组装出你想要的东西,迅速占领市场,比你造的高性能、高可用、高科技的轮子更实用。这个跟做新产品开发有点类似,迅速组装,高效开发,然后再想办法改进。
第三,要文本,不要二进制。协议要文本化,配置要文本化。不要担心性能,在可见的时间里,你基本不会因为文本化的问题遇到性能瓶颈。
第四,要透明,不要黑盒。基础产品尤其需要对用户透明,你的用户不是小白用户,他们也是程序员,而程序员天生对黑盒性质的东西充满厌恶,他们总想知道你的东西背后在做什么,这对于查找问题分析问题也很重要。怎么做到透明呢?设计,统计,监控,日志等等。
第五,要拥抱标准,不要另搞一套。已经有了久经考验的HTTP协议,你就不要再搞个STTP,有了AMQP协议,你就不要再搞个BMQP。被广泛认可的标准是一些业界的顶尖专家制定出来的,他们早就将你没有考虑到的问题都考虑进去了。你自己搞的那一套,随着时间推移你会发现跟业界标准越来越像,因为面对的问题是一样的。使用标准的额外好处是,你有一大堆可用的代码或者类库可以直接使用,特别是在面对跨语言的时候。
第六,能Share nothing,就不要搞状态复制。无状态的东西是最可爱的,天然的无副作用。水平扩展不要太容易。
第七,要将你的系统做的越不“重要”越好,如果太多的产品依赖你的系统,那么当你的系统故障的时候,整个应用就完蛋了。我们不要担这个责任,我们要将系统做的越来越“不重要”,别人万一没了你也能重启,也能一定时间内支撑正常的工作。
第八,要专注眼前,适当关注未来。有远见是好事,但是太多远见就容易好高骛远。为很小可能性设计的东西,没有机会经历实际检验,当故障真的发生的时候,你也不可能完全信赖它。更好的办法是将系统设计得可介入,可在紧急情况下人工去介入处理,可介入是不够的,还要容易介入。
第九,不要对用户有假设,假设你的用户都是smart programmer,假设你的用户不需要位运算,假设你的用户要同步不要异步。除非你对这个领域非常熟悉并实际使用过类似的东西,否则还是不要假设。
第十,咳咳,似乎没有第十了,一大早憋了这么篇无头无脑的Blog,大伙将就看看。
Kafka这个linkedin开源的MQ,我在过去的 blog简单介绍过。最近3周来,我的工作就是做它的一个Java移植版本,kafka是用scala写的,基于维护和定制的角度,这个拷贝的版本还是用Java。说拷贝,也不尽然,原理相同,但实现完全换过,从数据结构到通讯框架、通讯协议、程序组织,乃至一些重要功能点上都做了改进和更新。我将这个Java版本取名为metamorphosis,也就是卡夫卡的代表作《变形记》的英文名。
在原版本上,目前做了如下改进:
1、协议替换为文本协议,整个协议类似memcached,文本协议的优点自不必说。通讯框架也是采用内部使用的通讯框架,减少工作量。
2、存储结构上也采用自定义结构,更简洁紧凑。
3、kafka原来只支持consumer和broker之间的服务查找和负载均衡,meta加入了producer和broker之间的服务查找和负载均衡。
4、Consumer API没有采用kafka的stream方式,而是同时实现同步获取和异步订阅两种方式,更接近JMS和Notify。
5、改进了服务器端文件recover的性能,采用并发多线程recover的方式(可选)。
6、添加了实时统计功能和协议,类似memcached的stats协议,响应透明号召。
7、客户端的连接复用。
以后要做的事情,可能包括:
1、实现类似Mysql的master/slave方案,可能还要分为同步和异步两种模式。
2、分区扩展时候的数据自动迁移功能,做到无痛水平扩展。
3、高可用方案的另一个实现。
4、嵌入Http server做web管理。
工作在本周初步告一段落,接下来是要做集成测试和压测等,我在两台8核16G的机器上分别部署服务器和客户端(订阅者发布者同在一台),做的一个简单压测数据如下:并发100个线程发送5000万消息并同时消费,1K大小的消息TPS可以达到3.8万,4K大小的消息TPS可以达到1.8万,服务器load都维持在一个较低的水平。从这个数据来看,超过我一开始的预期。后续可能做下kakfa的测试对比下。
无论是消息系统,还是配置管理中心,甚至存储系统,你都要面临这样一个选择,push模型 or pull模型?是服务端主动给客户端推送数据,还是客户端去服务器拉数据,一张图表对比如下:
|
push模型 |
pull模型 |
描述 |
服务端主动发送数据给客户端 |
客户端主动从服务端拉取数据,通常客户端会定时拉取 |
实时性 |
较好,收到数据后可立即发送给客户端 |
一般,取决于pull的间隔时间 |
服务端状态 |
需要保存push状态,哪些客户端已经发送成功,哪些发送失败 |
服务端无状态 |
客户端状态 |
无需额外保存状态 |
需保存当前拉取的信息的状态,以便在故障或者重启的时候恢复 |
状态保存 |
集中式,集中在服务端 |
分布式,分散在各个客户端 |
负载均衡 |
服务端统一处理和控制 |
客户端之间做分配,需要协调机制,如使用zookeeper |
其他 |
服务端需要做流量控制,无法最大化客户端的处理能力。
其次,在客户端故障情况下,无效的push对服务端有一定负载。
|
客户端的请求可能很多无效或者没有数据可供传输,浪费带宽和服务器处理能力 |
缺点方案 |
服务器端的状态存储是个难点,可以将这些状态转移到DB或者key-value存储,来减轻server压力。 |
针对实时性的问题,可以将push加入进来,push小数据的通知信息,让客户端再来主动pull。
针对无效请求的问题,可以设置逐渐延长间隔时间的策略,以及合理设计协议尽量缩小请求数据包来节省带宽。
|
在面对大量甚至海量客户端的时候,使用push模型,保存大量的状态信息是个沉重的负担,加上复制N份数据分发的压力,也会使得实时性这唯一的优点也被放小。使用pull模型,通过将客户端状态保存在客户端,大大减轻了服务器端压力,通过客户端自身做流量控制也更容易,更能发挥客户端的处理能力,但是需要面对如何在这些客户端之间做协调的难题。
HandlerSocket是一个mysql插件,可以将mysql作为NoSQL来使用,具体可以看我过去写的 这篇Blog。 hs4j是HandlerSocket的一个java客户端,自认为它比日本人写的那个客户端更实用和易用一些。写完好久,经过不少朋友使用和测试,现在正式发一个0.1版本,并已同步到maven中心仓库。
项目主页: http://code.google.com/p/hs4j/
项目描述:hs4j is a practical java client for HandlerSocket,it is nio based and turned to get better performance.
使用文档: http://code.google.com/p/hs4j/w/list
下载地址: http://code.google.com/p/hs4j/downloads/list
源码仓库: https://github.com/killme2008/hs4j
如果你使用maven2,可以直接引用:
<dependency>
<groupId>com.googlecode.hs4j</groupId>
<artifactId>hs4j</artifactId>
<version>0.1</version>
</dependency>
有疑问和bug请联系我。
Xmemcached是一个开源的java memcached client,具有高性能、更易用、功能完善等优点,距离上次发布1.3.1已经超过两个月,现在正式发布1.3.2这个新版本,主要的改进如下:
1、Bug修复,从1.3.1版本以来发现的bug并修复,包括:
issue 112:: 新引入的failure模式在启动的时候,如果memcached故障,运行不符合预期的bug.
issue 113: 新增加一个delete方法,可以设置操作超时
public boolean delete(final String key, long opTimeout)
throws TimeoutException, InterruptedException, MemcachedException;
2、性能调优,存储操作(set/add/replace/prepend/append/cas)的性能提升5%。
3、修复pom.xml,使得xmemcached可以在其他机器上编译。
4、使用github作为源码仓库,版本管理使用git替换svn,源码转移到
https://github.com/killme2008/xmemcached
新版本下载地址:
http://code.google.com/p/xmemcached/downloads/list
使用maven可以直接引用:
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>1.3.2</version>
</dependency>
项目文档:
http://code.google.com/p/xmemcached/w/list
在国内, Clojure语言的用户估计是小众中的小众,没有多少人听说,也没有多少人使用,资料也大多数是英文的,讨论也只能上国外论坛。因此,我想建立一个CN-Clojure的google group,供大家交流和学习clojure语言。群组地址(需要翻墙): http://groups.google.com/group/cn-clojure
现在没人,就我一个  。我也会在群组里放些学习资料,欢迎任何对clojure感兴趣的朋友加入。
昨天晚上用clojure搞了个scheme解释器,基本上是sicp里的解释器的clojure翻译版本,可能唯一值的一提的是对transient集合的使用,实现副作用的set!。总共代码包含注释才366行,支持的feature包括
Feature |
Supported |
Comment |
define |
yes |
|
lambda |
yes |
|
variable lookup |
yes |
|
primitive procedure evaluation |
yes |
|
compound procedure evaluation |
yes |
no tail recursion yet |
if |
yes |
|
cond |
yes |
|
let |
yes
|
|
let* |
yes
|
no named let* yet
|
letrec |
no |
|
begin |
yes
|
|
set! |
yes
|
|
quote |
yes |
|
quasiquote |
no |
|
unquote |
no |
|
delay |
no |
|
define-syntax |
no |
|
支持的primitive procedure包括常见的四则运算、car/cdr、list以及display、newline等。代码放在了github上: https://github.com/killme2008/cscheme,有兴趣的可以玩玩吧。
最近因为空闲时间有一些,所以去看了不少开源项目,大部分东西如果看过不记录下来,其实还是相当于没看,所以想想还是有必要摘要记录一下。
首先是去了解了 zookeeper这个项目,基于paxos算法的分布式服务组件,同事对此有非常深入的研究和介绍,具体可以看我们的 团队Blog。令我感慨的是这么一个非常难以理解的算法,却用一个简单的树状目录模型表达出来,并且在这个模型的基础上衍生出种种应用:集群感知、分布式锁、分布式队列、分布式并发原语等等,具体可以看文档给出的 recipes。在实现这些应用的时候,突出强调的是避免网络风暴,例如分布式锁的实现,竞争创建子节点,节点序列号最小的获取锁,其他节点等待,但是等待在什么条件上是有讲究的,如果所有节点都等待最小节点的删除事件,那么当最小节点释放锁的时候,就需要广播消息给所有其他等待的节点;换一个思路,如果每个等待节点只是等待比它序列号小的节点上,那么就可以避免这种广播风暴,变成一个顺序唤醒的过程。因此尽管有了zookeeper帮助实现分布式这些服务,但是要实现好仍然有一定难度,具体可以参考官方例子。我本来萌生了基于zookeeper实现一套封装好的类似j.u.c的服务框架,后来在邮件列表发现已经有人搞了这么一个基础类库放在github上: https://github.com/openUtility/menagerie 。不过我没有继续深入了,有兴趣的朋友可以瞧瞧。
然后又去看了我们淘宝开源的 TimeTunnel。TimeTunnel你可以理解成一个消息中间件,它整个设计跟我们的产品相当接近,但是两者的目的完全不同,tt强调的是高吞吐量,而notify强调的则是可靠性。TT的通讯层直接采用Facebook的thrift,并且利用zookeeper做集群管理和路由。TT的代码质量很好,有兴趣可以拉出来看一下,并且对zookeeper的应用也是一个典型的案例。TT在高可用性上的方案也很有特色,所有的服务器节点形成一个环,两两相互主辅备份,一个节点挂了,后续节点仍然可以提供服务直到主节点回来,有点类似一致性哈希的概念。节点的主从关系和顺序也是通过zookeeper保证。消息顺序的实现是通过称为router的路由到固定节点做传输,router默认是策略不是固定而是RR。TT的数据存储优先放在内存,并设置了一个内存状况监视的组件,当发现内存放不下的时候,swap到磁盘文件缓存,实现类似内存换页的功能。正常情况数据都应该在内存,当然如果可靠级别要求高的话可以先存磁盘再传输。TT目前仍然还是比较适合传输日志这样的文本增量数据,并且提供了TailFile这样的python脚本帮你做这个事情,这个脚本可以通过checkpoint做断点续传。在学习这个项目的时候,发现文档有很大问题,要么错误,要么遗漏,并且代码也不是最新的,我估计开源出来外面的人用的还不太多,希望慢慢能搞的更好一些。
跟TT类似,另一个追求高吞吐量的MQ是linkedin开源的 kafka。Kafka就跟这个名字一样,设计非常独特。首先,kafka的开发者们认为不需要在内存里缓存什么数据,操作系统的文件缓存已经足够完善和强大,只要你不搞随机写,顺序读写的性能是非常高效的。kafka的数据只会顺序append,数据的删除策略是累积到一定程度或者超过一定时间再删除。Kafka另一个独特的地方是将消费者信息保存在客户端而不是MQ服务器,这样服务器就不用记录消息的投递过程,每个客户端都自己知道自己下一次应该从什么地方什么位置读取消息,消息的投递过程也是采用客户端主动pull的模型,这样大大减轻了服务器的负担。Kafka还强调减少数据的序列化和拷贝开销,它会将一些消息组织成Message Set做批量存储和发送,并且客户端在pull数据的时候,尽量以zero-copy的方式传输,利用sendfile(对应java里的FileChannel.transferTo/transferFrom)这样的高级IO函数来减少拷贝开销。可见,kafka是一个精心设计,特定于某些应用的MQ系统,这种偏向特定领域的MQ系统我估计会越来越多,垂直化的产品策略值的考虑。
在此期间,我还重新去看了activemq和hornetq的存储实现,从实现上大家都大同小异,append log + data file的模式。Activemq采用异步队列写来提高吞吐量,而Hornetq干脆就直接利用JNI调用原生aio来实现高性能。在搜索Java的aio实现的时候,碰巧发现Mina的沙箱里有个aioj的实现,源码在: https://svn.apache.org/repos/asf/mina/sandbox/mheath/aioj/ 。我测试了完全可用,也尝试改造我们的磁盘存储组件,可惜提升不多,估计不从整个设计上调整服务器,不大可能从aio上获益。
最近也重新看起了clojure的一些开源项目,clojure的开源资源在github上也非常丰富,有待挖掘,下次有机会再尝试介绍一二。
写着玩的,不使用任何网络框架从头构建的echo server,总共77行。
1 ;;Author:dennis (killme2008@gmail.com)
2 (ns webee.network
3 (:import (java.nio.channels Selector SocketChannel ServerSocketChannel SelectionKey)
4 (java.net InetSocketAddress)
5 (java.nio ByteBuffer)
6 (java.io IOException)))
7
8 (declare reactor process-keys accept-channel read-channel)
9
10 (defn bind [^InetSocketAddress addr fcol]
11 (let [selector (Selector/open)
12 ssc (ServerSocketChannel/open)
13 ag (agent selector)]
14 (do
15 (.configureBlocking ssc false)
16 (.. ssc (socket) (bind addr 1000))
17 (.register ssc selector SelectionKey/OP_ACCEPT)
18 (send-off ag reactor fcol)
19 ag)))
20
21 (defn- reactor [^Selector selector fcol]
22 (let [sel (. selector select 1000)]
23 (if (> sel 0)
24 (let [sks (. selector selectedKeys)]
25 (do
26 (dorun (map (partial process-keys selector fcol) sks))
27 (.clear sks))))
28 (recur selector fcol)))
29
30 (defn- process-keys [^Selector selector ^SelectionKey fcol sk]
31 (try
32 (cond
33 (.isAcceptable sk) (accept-channel sk selector fcol)
34 (.isReadable sk) (read-channel sk selector fcol)
35 )
36 (catch Throwable e (.printStackTrace e))))
37
38 (defn- accept-channel [^SelectionKey sk ^Selector selector fcol]
39 (let [^ServerSocketChannel ssc (. sk channel)
40 ^SocketChannel sc (. ssc accept)
41 created-fn (:created fcol)]
42 (do
43 (.configureBlocking sc false)
44 (.register sc selector SelectionKey/OP_READ)
45 (if created-fn
46 (created-fn sc)))))
47
48 (defn- close-channel [^SelectionKey sk ^SocketChannel sc fcol]
49 (let [closed-fn (:closed fcol)]
50 (do
51 (.close sc)
52 (.cancel sk)
53 (if closed-fn
54 (closed-fn sc)))))
55
56 (defn- read-channel [^SelectionKey sk ^Selector selector fcol]
57 (let [^SocketChannel sc (. sk channel)
58 ^ByteBuffer buf (ByteBuffer/allocate 4096)
59 read-fn (:read fcol)]
60 (try
61 (let [n (.read sc buf)]
62 (if (< n 0)
63 (close-channel sk sc fcol)
64 (do (.flip buf)
65 (if read-fn
66 (read-fn sc buf)))))
67 (catch IOException e
68 (close-channel sk sc fcol)))))
69
70 ;;Bind a tcp server to localhost at port 8080,you can telnet it.
71 (def server
72 (bind
73 (new InetSocketAddress 8080)
74 {:read #(.write %1 %2)
75 :created #(println "Accepted from" (.. % (socket) (getRemoteSocketAddress)))
76 :closed #(println "Disconnected from" (.. % (socket) (getRemoteSocketAddress)))
77 }))
Xmemcached是一个开源的memcached的Java客户端,最近引入了一些关键特性,因此版本号直接从1.2.6.2升级到1.3.0。主要的更改如下:
1、引入了failure模式,所谓failure模式是指在当一个memcached由于各种原因不可用的情况下,发往这个节点的请求将直接抛出异常,而非使用下一个可用的节点。具体可以看memached的这个文档。默认不启用,启用failure模式很简单:
MemcachedClientBuilder builder=……
//启用failure模式。
builder.setFailureMode(true);
也可以采用spring配置。
2、在启用failure模式的情况下,允许为每个memcached设置一个备份节点,当主节点挂掉的情况下,会将请求转交给备份节点,主节点恢复后又自动切换到主节点。请注意,要设置备份节点的前提是启用failure模式。假设我们已经有两个memcached节点:host1:port和host2:port,为host1:port设置一个备份节点host3:port可以实现为:
MemcachedClientBuilder builder=new XmemcachedClientBuilder(AddrUtil.getAddressMap("host1:port,host3:port host2:port"))
……
主备节点之间用逗号隔开,不同分组之间用空格隔开,完全兼容1.2。并且当备份节点连接意外断开的情况下,xmemcached也会自动修复备份节点的连接并加入映射。
关于failure模式和standby节点更多内容可以参考这篇blog.
3、修正BUG和新功能,包括issue 104,issue 105,issue 107等。
项目主页 http://code.google.com/p/xmemcached/
下载地址 http://code.google.com/p/xmemcached/downloads/list
用户指南 http://code.google.com/p/xmemcached/wiki/TableOfContents
如果你使用maven构建,可以直接引用:
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>1.3.1</version>
</dependency>
更新:发布1.3.1了,如果你还在使用1.3.0,建议升级。1.3.0因为改变了memcached地址服务器顺序,可能导致原有的缓存失效。
首先,还是利用下这个 小工具,查看下我10年读过的书,看过的电影
读书:读的太少,也可以看到,技术方面的更少,如果要说推荐,我只会推荐《Programming Clojure》作为学习clojure的入门,并且推荐《构建高性能web站点》作为了解一个网站构建的方放面面的入门书。
电影:今年进电影院的次数也寥寥无几,主要还是重看了莱昂纳多的作品,《盜梦空间》很惊艳,《钢铁侠2》很失望。
去年的愿望:读完《算法导论》——2/3,继续深入Erlang,探索Erlang在工作中的实际应用——几乎没有,加强对其他系统的了解以及大型网站构建方面的学习——小小一些了解,希望能全家一起去旅游一次,希望能将老爸老妈接过来玩一段时间——没有做到。
工作:状态并不好,还是尝试努力去做了一些事情,包括参与一些分享,更多参与他人的代码复查和设计审查等。抱怨、牢骚少了一些,相对淡定了。
2011年:还是不谈大的愿望,从以往的经验来说,很难靠谱。也许有一个相对明晰的目标:提高自制力和计划性。
写这篇文章的想法产生在昨天晚上读《面向对象分析与设计》的时候,我渐渐发现我们这个小组不知不觉地贯彻了很多非常有价值的实践经验,这些点点滴滴都对我们的最终的产品质量产生了或大或小的影响,保证我们的系统不会出现重大的故障。我想有必要将这些“隐性知识”稍微总结一下,以供参考和记录。
从过程的连续光谱来看,我们大概处于中间位置偏左的位置,更偏向一个轻量级团队的敏捷过程,但是也包含计划驱动过程中的因素。我们的小组是自管理的,没有专门的QA和SA,我们自己去想出最好的工作方法,但是在执行中我们的计划还是相对确定的,每个季度做什么都会有一个比较明确的计划和里程碑,并且对问题领域都相对熟悉;我们的过程是迭代式,一般一个季度至少会交付一个稳定可执行的新版本,我们在文档上做的不是特别好,很多都依赖于团队成员之间的“隐性知识”;同时我们对问题的改进基本还是有一个流程和机制,会持续的跟踪问题并改进。
下面分阶段总结下我们的一些实践经验。
一、分析和设计阶段
1、在这个阶段,我们会明确准备做什么,界定问题的边界,对功能进行一个取舍。一般在一个版本完成之后会马上开始这个过程。大家都想一想接下来做什么,经过几轮PK后确定重要紧急的事情优先做,定义下一个版本的功能列表。
2、功能列表出来之后,我们会针对每个功能提出各种方案做比较,在此期间,我们会邀请更大团队范围内的专家参与方案和设计的评审,剔除不切实际以及明显有缺陷的方案,针对一些风险点提出改进建议和防范措施。
3、在设计方案出来之后,我们会分配功能的开发任务,根据每个开发人员熟悉的领域,自主领取或者被动分配任务。这个过程不是一成不变的,考虑到团队内部知识交流的必要性,也可能让不熟悉某个领域的人去做他不熟悉的事情。
二、构造阶段
1、整个系统已经有一个关键的抽象机制,针对我们的服务器有一个核心的pipeline机制,针对我们的客户端,有一个核心的发送消息流程。将所有的功能模块组织在这个关键机制周围,形成一个强有力的整体。
2、开发完成不仅仅意味着功能代码的完成,还包括测试代码:单元测试和集成测试。如果你没办法做到全面的覆盖,那就要求必须覆盖运行的关键路径和极端场景。
3、单元测试我们使用JUnit,适当使用Mock可以简化测试。但是Mock对象如果太多,也许会失去测试的价值,这里有一个权衡。
4、在整个构造过程中,我们贯彻每日构建、持续集成的原则。使用hudson做持续集成,时刻关注测试状况,有问题及时反馈给开发者。
5、有一个功能强大的集成测试框架,模拟实际环境做各种测试,它的目的是尽量在接近真实状况下去执行系统并尽早暴露问题。
6、每个功能完成之后,立即发起review,请同事和你一起复审代码。复审代码的作用不仅是发现bug,改良设计,也是一个知识交流的最佳途径。我们经常能通过代码审查发现一些设计上的缺陷,以及功能实现上的BUG。我们团队应该说是非常看重代码审查的作用。
7、使用findbugs和clover等工具,分析代码质量并改进。
8、在发布之前,做一次集中的代码review,每个人介绍下自己的功能实现代码和设计,一般我们会申请一个会议室和投影仪,并邀请团队之外的人加入review。
9、在发布之前,有一个系统的压测流程,针对每个版本更新压测方案,并预留一到两周的时间做性能压测。压测不仅能尽早暴露性能隐患,还可以发现系统在特殊情况下的一些BUG。压测除了关注系统的吞吐量、GC情况之外,还应该关注硬件的性能指标。
三、发布和总结
1、发布之前,最好让使用我们系统的用户使用新版本做一个回归测试,一方面是测试兼容性,一方面也可以及早发现BUG。
2、我们的发布流程:线下、beta、线上。每个阶段通常都持续一到两周,才会进行到下一阶段。并且是从相对不重要的系统,到关键系统的顺序进行发布。
3、发布之后,通过日志、运行时监控、用户反馈等方式收集系统运行状况,发现BUG,修正BUG,补充测试,测试通过,重新发布。
4、每个版本发布后,需要总结下本次发布过程中遇到的所有BUG以及经验教训,并提出可能的改进建议。
5、需要一个跟踪线上问题的BUG跟踪系统,可以用JIRA之类的trace软件。跟踪不仅是记录,最好列出解决的时间点,在哪个版本确定解决,甚至确定交给谁去解决,并持续跟进。
Xmemcached在元旦左右准备发1.3这个版本,这个版本新增加的一个关键特性就是所谓的failure模式。关于这个,可以看下memcached官方文档的解释
《Failure,or Failover》。
展开来说,在某个memcached节点挂掉或者由于其他故障连接断开的时候,大部分客户端的默认策略都是failover的,也就是会查找下一个可用的memcached节点继续使用,挂掉或者连接不上的节点的数据会转移到其他节点上,路由的策略可以是Round Robin,也可以是一致性哈希。这样的模式在节点意外故障挂掉的情况下运行的很好,但是memached节点也完全可能因为一个意外的事故而 短暂挂掉,比如你不小心弄掉了网线又马上接上去,比如机房交换机突然停电又立即恢复了,假设在故障前,用户A正要更新数据到节点A,节点A意外断开,那么这些数据就更新到下一个有效节点B,但是节点A又马上恢复,这时候用户又从节点A去读数据,读到却是更新前的老数据了(新数据更新到B节点去了),这种情况对用户来说就非常困惑,你告诉我更新成功,但是看到却还是更新前的数据。
怎么解决呢?一个简单的方案就是所谓failure模式,当某个节点挂掉的时候,不会从节点列表移除,请求也不会转移到下一个有效节点,而是直接将请求置为失败,就刚才的场景来说,在用户更新数据到节点A的时候,节点A意外断开,那么用户的这次更新请求不会转移到节点B,而是直接告诉用户更新失败,用户再次查询数据则绕过节点A直接查询后端存储。这种模式很适合这种节点短暂不可用的状况,请求会穿透缓存到后端,但是避免了新旧数据的问题。
Xmemcached 1.3将支持failure模式,只要你设置下failureMode为true即可,简单示例:
XMemcachedClientBuilder builder =……
//设置使用failure模式
builder.setFailureMode(true);
在此模式下,某个节点挂掉的情况下,往这个节点的请求都将直接抛出MemcachedException的异常。
不仅如此,xmemcached 1.3还将引入standby node的概念,你可以设置某个memached节点的备份节点,当这个节点挂掉的时候会将请求转发给这个备份节点,不会简单地抛出异常,也不会转发给其他节点。要使用standby node,必须首先设置使用failure mode,一个例子:
XMemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil
.getAddressMap("192.168.1.99:11211,192.168.1.100:11211 192.168.1.101:11211,192.168.1.102:11211"));
builder.setFailureMode(true);
可以看到,新的服务器字符串格式变化为host:port,host:port host:port,host:port的格式,以空格隔开的是两个节点组成的一个分组,以逗号隔开的是主节点和备份节点,以上面的例子来说,我们设置客户端使用的节点是192.168.1.99和192.168.1.101,其中99对应的备份节点是100,而101的备份节点是102。并且我们需要设置使用failure mode为true。
Failure mode加上standby节点可以比较好的解决新旧数据的问题,并且也可以防止请求穿透缓存到DB,但是主备两个节点之间的数据同步,xmemcached不准备帮你做,我的建议是可以使用repcached这个patch做复制。
有的朋友可能希望,在使用备份节点之前先flush掉备份节点的数据,防止使用到老的数据,请求还是可以穿透缓存去DB查找,并存储到备份节点,我仔细考虑了这个方案,衡量之下还是不准备做自动flush,主要是并发上很难处理,并且flush数据这个事情可以手工来搞,根据我的经验,做的太透明太自动不一定是好事。你可以在主节点恢复之后,手工flush下备份节点的数据。
目前,xmemcached 1.3已经整装待发,对这些特性有兴趣的朋友可以先从 svn下载源码尝鲜,有任何改进的建议请发邮件给我。我的邮件地址在博客的右上角。
最近没更新blog,因为我忙着写程序,玩android  。入手了一台moto xt701,下单后悔来想换里程碑,但是京东打包太快,客服不让换,这个很奇怪,我想用更多钱买东西反而被拒绝,看来京东处理太快也不一定是优点  。
买了几本android相关的书,下决心好好学习一下,在读完一本加半本的情况下决定开始做个练习程序,最后的结果就是为乐天气这个小软件。为乐天气不仅仅是个烂大街的天气预报软件,它从google weather api抓取天气状况信息并显示在桌面widget,还提供了两个我个人很需要但还没有在其他天气软件上看到的功能:恶劣天气告警和气温变化告警——当有雨雪天气或者气温变化超过一定幅度的时候主动通知我,这对我这个常常不知道带伞并且家里有小孩的人比较有用。来几张运行在xt701上的截图:
写程序总共花了3天,程序虽小,但基本上覆盖了android提供的一些基本机制:Activity显示组件、service负责信息抓取、桌面widget、通过intent在组件之间交互、handler处理界面更新、国际化和资源管理、利用preferences保存配置以及使用Application保存全局数据等等。Android开发给我的感觉是,入门还是相当容易的,如果熟悉Java甚至J2ME,那么学习android的入门成本还是很低的,因此从长期来看,做这一行的一般应用门槛不高,也会像现在的Java市场一样吸引大量开发者。如果说对独立开发者特别有价值的方向,应该还是游戏方向,做游戏不仅仅是技术,更多还是创意和推广,另外想在android做出效果非常出色的游戏,还需要去学习OpenGL和数学算法之类,需要熟悉c/c++,本质上跟传统的游戏开发没有太大区别,这个门槛就相对高一些。
我将这个小程序发到了国内的几个market,从下载情况来看,尽管都非常少,但是91助手的应用汇还是最多,其次是安卓市场,再后面是爱米软件商店,从后台体验上来说,最好的还是eoeAndroid社区的优智市场,不过人气貌似不旺。
从我接触移动开发的这一周来看,我很兴奋,原来现在这个行业已经这么火热,有太多新鲜的东西我没有尝试过,有太多很有创意的小应用小游戏存在,有大量的开发者早就在从事这个激动人心的领域,我太out了,希望现在关注还来得及。
HandlerSocket是日本人 akira higuchi 写的一个MySql的插件,通过这个插件,你可以直接跟MySql后端的存储引擎做key-value式的交互,省去了MySql上层的SQL解释、打开关闭表、创建查询计划等CPU消耗型的开销,按照作者给出的数据可以在数据全部在内存的情况下可以达到75W的QPS查询。具体信息可以看这篇 Blog,中文介绍可以看这篇文章《 HandlerSocket in action》。
这个东西为什么让我很激动呢?首先性能是程序员的G点,一听高性能你不由地激动,其次,这也解决了缓存跟数据库的一致性问题,因为缓存就在数据库里面,第三,这个东西不仅仅是NoSQL,简单的CRUD你可以通过HandlerSocket,但是复杂的查询你仍然可以走MySql,完全符合我们应用的场景,并且从实际测试来看,性能确实非常优秀。但是呢,这个东西的代价也少不了,例如没有权限检查(未来可能添加);不能启用MySql的查询缓存,否则会导致数据的不一致; 协议设计也不合理,使用\t做分隔符,使用\n做换行符,那么你插入或者更新的字段数据就不能含有这些字符,否则行为将不如预期。
HandlerSocket有一个日本人的java客户端实现,我去尝试了下,结果发现这玩意完全不具实用性,封装的层次非常原始。因此我自己写了个新的客户端,这就是本文要介绍的HandlerSocket Client for Java,简称 hs4j,项目放在了 googlecode,代码的网络层复用xmemcached,重新实现了协议和上层接口,目前的状态完全可用,也希望有需要的朋友参与测试。
项目地址: http://code.google.com/p/hs4j/
HS4J的使用很简单,所有的操作都通过HSClient这个接口进行,如我们创建一个客户端对象:
import com.google.code.hs4j.HSClient;
import com.google.code.hs4j.impl.HSClientImpl;
HSClient hsClient = new HSClientImpl(new InetSocketAddress(9999));
假设HandlerSocket运行在本地的9999端口,默认的9998是只读的,9999才是允许读和写。HSClient是线程安全的。
在执行操作前需要先open index:
import com.google.code.hs4j.IndexSession;
IndexSession session = hsClient.openIndexSession(db, table,
"PRIMARY", columns);
其中db是数据库名,table是表名,"PRIMARY"表示使用主键索引,columns是一个字符串数组代表你要查询的字段名称。这里没有指定indexid,默认会产生一个indexid,你也可以指定indexid,返回表示一次open-index会话对象,IndexSession同样是线程安全的。
IndexSession session = hsClient.openIndexSession(indexid,db, table,
"PRIMARY", columns);
查询操作通过find方法:
import java.sql.ResultSet;
final String[] keys = { "dennis", "killme2008@gmail.com" };
ResultSet rs = session.find(keys);
while(rs.next()){
String name=rs.getString(1);
String mail=rs.getString(2);
}
find返回的是java.sql.ResultSet,你完全可以像使用jdbc那样去操作结果集。当然我的简单实现并不符合JDBC规范,只实现了最常见的一些方法,如getStrng、getLong等。find(keys)方法默认使用的op是"="。其他重载方法可以设置其他类型的op,统一封装为枚举类型FindOperator。
更新操作:
import com.google.code.hs4j.FindOperator;
int result=session.update(keys, new String[] { "1", "dennis",
"test@163.com", "109" }, FindOperator.EQ);
keys表示索引的字段列表对应的值数组,通过FindOperator.EQ比较这些值和索引,第二个参数values表示要更新的字段值,这些值跟你在open-index的时候传入的columns一一对应,最后返回作用的记录数。
删除操作:
int result= session.delete(new String[] { "dennis" },
FindOperator.EQ)
HS4J同样支持连接池,可以在构建客户端的时候传入连接池大小:
//100-connections pool
HSClient hsClient = new HSClientImpl(new InetSocketAddress(9999),100);
在open index的时候,会在连接池里所有的连接上都open。并且在连接因为意外情况(如网络错误)断开的时候,HS4J会自动重连,并在重连成功的情况下自动发送已经open的index,保证应用的操作不受重连影响。
因为HS4J是我在两天内写就的一个东西,可能还有不少隐藏的bug,并且HandlerSocket本身也是个新东西,如果有什么问题或者改进建议,随时欢迎告诉我,多谢。
上周在内部做的一个Java NIO框架的实现技巧和陷阱的分享,对编写NIO网络框架有兴趣的朋友可能有点帮助,上传slideshare.net一直出错,直接提供下载吧。
下载地址: Nio Trick and Trap.pdf.zip
前段时间对kilim的当前版本做了一些改进,集中在nio调度器这一块。Kilim新版本引入了nio调度器,可以跟非阻塞IO结合在一起,从这个版本开始,kilim才真正具有实用性。协程只有跟非阻塞IO结合起来才能发挥威力啊。但是Kilim的默认的nio调度器还只是使用一个nio worker做调度,这跟现有的NIO框架采用多个nio worker来提升效率比较起来相对落伍。我改进了 NioSelectorScheduler,引入了类似Netty3的boss和woker的概念,boss负责连接接入,而worker负责连接的IO读写,并且默认设置worker数目为CPU个数的两倍。经过我的测试,改进后的NIO调度器的效率远远超过了现有的调度器,有兴趣可以用netty的benchmark跑一下example里的 EchoServer。
Kilim默认还提供了一个简易Http Server框架,但是没有提供Http Client的实现,我的另一个改进是提供了一个简易的Http Client实现,也是利用Ragel做协议解析,一个简单的使用例子如下:
package kilim.examples;
import kilim.Pausable;
import kilim.Task;
import kilim.http.HttpClient;
import kilim.http.HttpResponse;
public class SimpleHttpClient {
static class SimpleTask extends Task {
@Override
public void execute() throws Pausable, Exception {
HttpClient client = new HttpClient();
HttpResponse resp = client.get("http://www.google.com.hk/");
System.out.println(resp.status());
System.out.println(resp.content());
}
}
public static void main(String[] args) {
SimpleTask task = new SimpleTask();
task.start();
}
}
这个简陋的HttpClient目前只支持GET/POST,同时支持Http chunk编码(得益于kilim原有代码),做一些简单的HTTP调用已经足够。我尝试在一个项目里使用这个HttpClient去替代java默认的HttpURLConnection,效率有部分提升,但是同时由于大量协程存在占用了很大部分的内存,给GC也带来了不小的压力。
我的代码直接从kilim的主干fork出来,有兴趣可以直接git clone下来玩玩,地址 https://github.com/killme2008/kilim
一直有这么个想法,列一下我个人认为在学习和使用Java过程中可以推荐一读的书籍,给初学者或者想深入的朋友一些建议,帮助成长。推荐的的都是我自己读过,也会推荐一些朋友读过并且口碑不错的书籍。
一、基础类
1、《Thinking in java》,入门第一位是建立正确的概念。
2、《Core Java》,我没系统读过,这本书更贴近实践,更多API的介绍,同样,更新也更频繁。
二、进阶类
1、《Effective Java》,在熟悉语法、API之后,你需要知道最佳实践和陷阱,没有比这本更好的。
2、《Java Puzzlers》,通过谜题介绍一些你可能没有注意到的边角料,作为趣味读物也不错
3、《深入Java虚拟机》,翻译一般,但不可不读,最好结合最新的JVM规范来读。
三、特定领域
1、网络编程:
(1) O'Reilly的《Java nio》,很多人都推荐,我个人觉的一般,基本上只是个API更详细的说明文档,O'reilly的java系列很多都是这样。
(2)我更推荐这本《Fundamental networking in java》,由浅入深教你怎么做java网络编程,并且介绍很多背景知识,甚至介绍了各种最佳实践、网络编程模型以及Java socket在不同平台之间的差异等等。
2、并发编程:
(1)《Java Concurrency in Practic》,并发领域必读经典。
(2)《Java并发编程:设计原则与模式》,同样是Doug lea的作品。
(3) 《java threads》,入门读物。
3、web编程,这块我许久未接触了,就不推荐了,有兴趣的朋友可以补充下。
四、模式与设计
1、《设计模式》,GOF的经典。
2、《设计模式精解》,应该有最新版,个人认为更适合入门。
3、《Head first设计模式》,更轻松的入门读物。
4、《企业应用架构模式》
5、《分析模式——可复用对象模型》
6、《面向模式的软件体系结构》,国内貌似翻译了3卷,绝对经典,可惜翻译较差。
7、《重构——改善既有代码设计》,想写好代码必读。
8、《重构与模式》,给我印象很深的 xml构建的例子,在我的代码里应用到了。
五、方法论
1、《敏捷软件开发》
2、《测试驱动开发》,你不一定要TDD,但是你一定要学会做单元测试。
3、《Agile Java》,也可以作为java入门读物。
4、《快速软件开发》
5、《面向对象分析与设计》,OO设计必读。
6、《Unix编程艺术》,打开你的眼界。
六、Java之外
0、《代码大全》,编程的百科全书,必读。
1、《unix网络编程》,学习网络编程必读书。
2、《C++网络编程》上下两卷,介绍ACE的,但是其中对各种模式运用的介绍非常值的一读。
3、《Joel说软件》,编程文化
4、《人月神话》、《人件》
5、《卓有成效的程序员》,给我很大启发的一本书。
6、《程序员修炼之道》
7、《计算机程序的构造与解释》,必读
8、《算法导论》,可以作为参考书
9、《深入理解计算机系统》
10、《编译原理》龙书,最新版用java解释,我没有读完,顺便提下。
我最近在实现一个基于Kilim的HttpClient,在处理响应body特别大的情形下遇到了kilim的一个BUG,有必要记录下。
问题是这样,Kilim将连接封装为EndPoint对象,EndPoint有个方法fill用于从管道读数据到缓冲区,并且可以指定希望至少读到多少个字节(atLeastN)才返回。那么在进入此方法的时候会判断缓冲区是否有足够空间容纳atLeastN个字节,如果没有,则创建一个更大的缓冲区,并将“老”的缓冲区的数据拷贝到新缓冲区,这部分代码是这样实现:
public ByteBuffer fill(ByteBuffer buf, int atleastN) throws IOException, Pausable {
if (buf.remaining() < atleastN) {
ByteBuffer newbb = ByteBuffer.allocate(Math.max(buf.capacity() * 3 / 2, buf.position() + atleastN));
buf.rewind();
newbb.put(buf);
buf = newbb;
}
……
}
后面的代码我省略了,这个BUG就出现在这段代码里。这段代码的逻辑很简单,先是创建一个新的更大的缓冲区,然后将老的缓冲区的数据put到新的缓冲区,在put之前调用rewind方法将老的缓冲区的position设置为0。查看rewind干了什么:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
仅仅是将position设置为0,并让mark失效。position指向下一个读或者写的位置,这里在写入到新缓冲区之前确实需要将position设置为0,以便写入从老的缓冲区第一个位置开始。问题是什么?问题是position仅仅指定了下一个读取数据的位置,却没有指定有效数据的大小,换句话说,没有指定老的缓冲区的limit。因此这里造成的后果是老的缓冲区整个被写入到新的老缓冲区,包括有效数据和无效数据,默认情况下缓冲区的limit等于capacity。
这个bug可以通过下面程序看出来:
ByteBuffer old = ByteBuffer.allocate(8);
old.putInt(99);
ByteBuffer newBuf = ByteBuffer.allocate(16);
old.rewind();
newBuf.put(old);
newBuf.putInt(100);
newBuf.flip();
System.out.println(newBuf.remaining());
System.out.println(newBuf.getInt());
System.out.println(newBuf.getInt());
System.out.println(newBuf.getInt());
先往old写入一个整数99,然后创建newBuf并写入old数据,并再写入一个整数100,最后从newBuf读数据。本来我们预期只应该读到两个整数99和100,但是中间却插入一个0,输出如下:
12
99
0
100
12表示缓冲区可读的数据,本来应该是8个字节,却多了4个字节的无效数据。
这个BUG解决很简单,将rewind修改为flip方法即可,flip不仅将position设置为0,也将limit设置为当前位置:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
修改上面的测试程序,符合我们的预期了:
ByteBuffer old = ByteBuffer.allocate(8);
old.putInt(99);
ByteBuffer newBuf = ByteBuffer.allocate(16);
old.flip();
newBuf.put(old);
newBuf.putInt(100);
newBuf.flip();
System.out.println(newBuf.remaining());
System.out.println(newBuf.getInt());
System.out.println(newBuf.getInt());;
输出:
8
99
100
总结,使用rewind的前提是limit已经正确设置,例如你将buffer写入成功并想记录这个buffer,可以使用rewind:
while (buffer.hasRemaining()) //发送数据
networkChannel.write(buffer);
buffer.rewind(); // 重置buffer,准备写入日志管道
while (buffer.hasRemaining()) // 写入日志
loggerChannel.write(buffer);
而flip用于缓冲区发送或者读取之前,也就是将缓冲区设置为等待传出状态。
这某文就是 这篇文章啦,咱也不废话,不遮掩。几点感想:
1、首先,这篇文章很多的“我听说”、“据说“、我和……聊脱,他们都表示很郁闷“、“我就听说过一些……”诸如此类的道听途说,我觉的这不是“工程师”该说的话,请不要用可能,好像,听说这样的字眼,请给我数据,给我地点,给我程序。
2、其次,文中作者提到的yahoo的优秀点,在淘宝我都发现了传承,我估计是学习yahoo中国的,比如发布流程、比如知识库、比如很有特色的技术大学培训、比如鼓励创建自动化工具等等,我不知道作者有没有在阿里集团的子公司待过,或者来淘宝待过,如果来过呆过,我想不会没看见。
3、第三,Google是商业公司,百度是商业公司,阿里更是商业公司,没有销售人员的付出,工程师的劳动成果何以体现?你的薪水从哪里来?工程师文化或者销售文化,这不重要的,重要的是你能否认同,能否感受到尊重,不能可以用脚投票。
4、个人感觉,阿里是非常富有理想主义的公司,点滴改变着我们的生活,不知不觉,淘宝、支付宝已经改变了很多人的生活。在我看来,这些都是很伟大的创造,伟大的“技术”,创造的社会效益显而易见。
5、脱离商业的技术不存在,计算炮弹轨迹的需求诞生了计算机,美国国防部催生了互联网,网络购物的需要诞生了淘宝,在不健全的信用社会网络交易的需要诞生了支付宝。
6、关于个人崇拜,你是成年人,你有自己的价值观,你有自己的世界观,如果你那么容易被人忽悠,那也是活该。
7、如果哪天公司免费发淘公仔,我也去抢啊,哦,抢过一回口碑卡。
8、以偏概全,会掩盖了很多人的努力工作。
javaeye的一个帖子介绍一道 面试题,取数组的最大元素和前n个大元素,取最大元素很简单,遍历即可。取前N大元素,可以利用排序,最简单的实现:
public static int[] findTopNValues(int[] anyOldOrderValues, int n) {
Arrays.sort(anyOldOrderValues);
int[] result = new int[n];
System.arraycopy(anyOldOrderValues, anyOldOrderValues.length - n,
result, 0, n);
return result;
}
Arrays.sort(int[])使用的是快排,平均的时间复杂度是O( n lg(n)),在一般情况下已经足够好。那么有没有平均情况下O(n)复杂度的算法?这个还是有的,这道题目其实是选择算法的变形,选择一个数组中的第n大元素,可以采用类似快排的方式划分数组,然后只要在一个子段做递归查找就可以,平均状况下可以做到O(n)的时间复杂度,对于这道题来说稍微变形下算法即可解决:
/**
* 求数组的前n个元素
*
* @param anyOldOrderValues
* @param n
* @return
*/
public static int[] findTopNValues(int[] anyOldOrderValues, int n) {
int[] result = new int[n];
findTopNValues(anyOldOrderValues, 0, anyOldOrderValues.length - 1, n,
n, result);
return result;
}
public static final void findTopNValues(int[] a, int p, int r, int i,
int n, int[] result) {
// 全部取到,直接返回
if (i == 0)
return;
// 只剩一个元素,拷贝到目标数组
if (p == r) {
System.arraycopy(a, p, result, n - i, i);
return;
}
int len = r - p + 1;
if (i > len || i < 0)
throw new IllegalArgumentException();
// if (len < 7) {
// Arrays.sort(a, p, r+1);
// System.arraycopy(a, r - i+1 , result, n - i, i);
// return;
// }
// 划分
int q = medPartition(a, p, r);
// 计算右子段长度
int k = r - q + 1;
// 右子段长度恰好等于i
if (i == k) {
// 拷贝右子段到结果数组,返回
System.arraycopy(a, q, result, n - i, i);
return;
} else if (k > i) {
// 右子段比i长,递归到右子段求前i个元素
findTopNValues(a, q + 1, r, i, n, result);
} else {
// 右子段比i短,拷贝右子段到结果数组,递归左子段求前i-k个元素
System.arraycopy(a, q, result, n - i, k);
findTopNValues(a, p, q - 1, i - k, n, result);
}
}
public static int medPartition(int x[], int p, int r) {
int len = r - p + 1;
int m = p + (len >> 1);
if (len > 7) {
int l = p;
int n = r;
if (len > 40) { // Big arrays, pseudomedian of 9
int s = len / 8;
l = med3(x, l, l + s, l + 2 * s);
m = med3(x, m - s, m, m + s);
n = med3(x, n - 2 * s, n - s, n);
}
m = med3(x, l, m, n); // Mid-size, med of 3
}
if (m != r) {
int temp = x[m];
x[m] = x[r];
x[r] = temp;
}
return partition(x, p, r);
}
private static int med3(int x[], int a, int b, int c) {
return x[a] < x[b] ? (x[b] < x[c] ? b : x[a] < x[c] ? c : a)
: x[b] > x[c] ? b : x[a] > x[c] ? c : a;
}
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static int partition(int a[], int p, int r) {
int x = a[r];
int m = p - 1;
int j = r;
while (true) {
do {
j--;
} while (j>=p&&a[j] > x);
do {
m++;
} while (a[m] < x);
if (j < m)
break;
swap(a, m, j);
}
swap(a, r, j + 1);
return j + 1;
}
选择算法还有最坏情况下O(n)复杂度的实现,有兴趣可以读算法导论和 维基百科。题外,我测试了下这两个实现,在我的机器上大概有2倍多的差距,还是很明显。
今年在阅读某个项目源码的时候看到DelayQueue的使用, xmemcached 1.2.6.1的重连任务也是采用DelayQueue管理,ReconnectRequest实现Delayed接口,我突然想起去review下xmc的源码,发现一个严重的BUG,原始代码如下:
public final class ReconnectRequest implements Delayed {
public long getDelay(TimeUnit unit) {
return nextReconnectTimestamp - System.currentTimeMillis();
}
}
getDelay返回该任务还剩下多少时间可以被执行,将下次执行的时间戳减去当前时间即可,问题在于这里返回的是毫秒,而没有调用getDelay传入的TimeUnit做转换,在DelayQueue内部其实是用纳秒做单位交给Condition对象去等待
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll(); // wake up other takers
return x;
}
}
}
最终导致的问题是, awaitNanos很快返回(awaitNanos接受的是纳秒,这里却传入毫秒),循环执行发现重新计算的delay仍然大于0,循环等到getDelay返回的越来越小直到0才执行相应的Task,,造成的现象是在重连的时候cpu占用率很高。
单元测试的时候没有发现这个问题,主要是因为功能正常,没有关注资源消耗情况,因此惭愧地忽略了。
解决的办法很简单,修改getDelay方法即可:
public long getDelay(TimeUnit unit) {
return unit.convert(
nextReconnectTimestamp - System.currentTimeMillis(),
TimeUnit.MILLISECONDS);
}
这个BUG比较严重,已经升级1.2.6.1的朋友建议马上升级到1.2.6.2,使用maven的朋友只要修改版本即可,没有使用maven的请到这里下载。
NIO中的Selector封装了底层的系统调用,其中wakeup用于唤醒阻塞在select方法上的线程,它的实现很简单,在linux上就是创建一个管道并加入poll的fd集合,wakeup就是往管道里写一个字节,那么阻塞的poll方法有数据可读就立即返回。证明这一点很简单,strace即可知道:
public class SelectorTest {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
selector.wakeup();
}
}
使用strace调用,只关心write的系统调用
sudo strace -f -e write java SelectorTest
输出:
Process 29181 attached
Process 29182 attached
Process 29183 attached
Process 29184 attached
Process 29185 attached
Process 29186 attached
Process 29187 attached
Process 29188 attached
Process 29189 attached
Process 29190 attached
Process 29191 attached
[pid 29181] write(36, "\1", 1) = 1
Process 29191 detached
Process 29184 detached
Process 29181 detached
有的同学说了,怎么证明这个write是wakeup方法调用的,而不是其他方法呢,这个很好证明,我们多调用几次:
public class SelectorTest {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
selector.wakeup();
selector.selectNow();
selector.wakeup();
selector.selectNow();
selector.wakeup();
}
}
修改程序调用三次wakeup,心细的朋友肯定注意到我们还调用了两次selectNow,这是因为在两次成功的select方法之间调用wakeup多次都只算做一次,为了显示3次write,这里就每次调用前select一下将前一次写入的字节读到,同样执行上面的strace调用,输出:
Process 29303 attached
Process 29304 attached
Process 29305 attached
Process 29306 attached
Process 29307 attached
Process 29308 attached
Process 29309 attached
Process 29310 attached
Process 29311 attached
Process 29312 attached
Process 29313 attached
[pid 29303] write(36, "\1", 1) = 1
[pid 29303] write(36, "\1", 1) = 1
[pid 29303] write(36, "\1", 1) = 1
Process 29313 detached
Process 29309 detached
Process 29306 detached
Process 29303 detached
果然是3次write的系统调用,都是写入一个字节,如果我们去掉selectNow,那么三次wakeup还是等于一次:
public class SelectorTest {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
selector.wakeup();
selector.wakeup();
selector.wakeup();
}
}
输出:
Process 29331 attached
Process 29332 attached
Process 29333 attached
Process 29334 attached
Process 29335 attached
Process 29336 attached
Process 29337 attached
Process 29338 attached
Process 29339 attached
Process 29340 attached
Process 29341 attached
[pid 29331] write(36, "\1", 1) = 1
Process 29341 detached
Process 29337 detached
Process 29334 detached
Process 29331 detached
wakeup方法的API说明没有欺骗我们。wakeup方法的API还告诉我们,如果当前Selector没有阻塞在select方法上,那么本次wakeup调用会在下一次select阻塞的时候生效,这个道理很简单,wakeup方法写入一个字节,下次poll等待的时候立即发现可读并返回,因此不会阻塞。
具体到源码级别,在linux平台上的wakeup方法其实调用了pipe创建了管道,wakeup调用了 EPollArrayWrapper的interrupt方法:
public void interrupt()
{
interrupt(outgoingInterruptFD);
}
实际调用的是interrupt(fd)的native方法,查看EPollArrayWrapper.c可见清晰的write系统调用:
JNIEXPORT void JNICALL
Java_sun_nio_ch_EPollArrayWrapper_interrupt(JNIEnv *env, jobject this, jint fd)
{
int fakebuf[1];
fakebuf[0] = 1;
if (write(fd, fakebuf, 1) < 0) {
JNU_ThrowIOExceptionWithLastError(env,"write to interrupt fd failed");
}
}
写入一个字节的fakebuf。有朋友问起这个问题,写个注记在此。strace充分利用对了解这些细节很有帮助。
Xmemcached 1.2.6.1正式发布,这个版本的主要是做bug fix以及一些细节改进,主要变动如下:
1、修复BUG,包括:
Issue 85:
当存在多个MemcachedClient的时候,JMX的统计只显示其中一个
Issue 87: 当使用一致性哈希的时候,连接池不起作用
Issue 90:
用户线程 中断引起的连接关闭
Issue 94: BinaryMemcachedClientUnitTest测试失败
Issue 95:
JMX addServer,removeServer存在缺陷
Issue 96:
OOM Error while decompressing 60 KB of actuall data
Issue 97:
使得关闭连接更友好
2、改进重连机制,重连不再是以固定间隔(默认2秒)做重试连接,而是以一个等差数列递增间隔时间,第一次2秒,第二次4秒,第三次6秒……直到最大间隔时间1分钟做重连尝试。
3、改善关闭机制,关闭连接前发送quit命令,尽量做到友好关闭,等待服务器主动断开连接。
4、添加新的方法用于设置XmemcachedClient实例名称,用于区分不同的缓存集群,方便统计显示:
public interface MemcachedClient{
/**
* Return the cache instance name
*
* @return
*/
public String getName();
/**
* Set cache instance name
*
* @param name
*/
public void setName(String name);
}
名称也可通过spring配置。
5、提供 英文的用户指南,非常感谢 cnscud的帮助,这份文档是他一人搞定的。
6、更新了 Java Memcached Client Benchmark,跟最新版本的 spymemcached, Java-Memcached-Client做对比测试,提供给需要的朋友参考。
项目首页 http://code.google.com/p/xmemcached/
下载地址 http://code.google.com/p/xmemcached/downloads/list
wiki地址 http://code.google.com/p/xmemcached/w/list
Xmemcached 1.2.6.1 released,所以更新了一下 Java Memcached Client Benchmark。对比下 Xmemached, Spymemcached和 Java-Memcached-Client这三个开源客户端的性能,具体的测试信息可以看这个 链接。
测试源码
svn co http://xmemcached.googlecode.com/svn/trunk/benchmark/
测试结果:
svn co http://xmemcached.googlecode.com/svn/trunk/benchmark/result
总结下测试结果,为还在选择和考察 java memcached client的朋友提供参考:
1、 Java-Memcached-Client 2.5.1这个版本果然有很大改进,性能上有非常大的提升,从测试结果来看在小于100并发下有非常明显的优势,同时耗费资源也相对较多。但是在300并发访问下,Java-Memcached-Client会不断地报错:
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.90:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.90:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000
并且无法正常地存取数据, 而xmc和spy却可以正常应对这一场景。因此可以看到在300并发下,Java-Memcached-Client测试的结果直接为0,因为测试无法完成。尽管我尝试将最大连接数调到2000,仍然是无法完成测试。
2、 Xmemcached无论在低并发还是高并发访问的情况下,都可以保持一个比较优秀的性能表现,从xmc和spy的对比来看,xmc的优势相当大。
3、 从用户选择角度来说,如果你的应用对memached的访问负载并不高,Java-Memcached-Client是一个不错的选择,但是在高峰访问的时候可能命中率会有个急剧的波动;如果你的应用访问memached的负载较高,此时我推荐你选择xmemcached;如果你需要异步的批量处理(future模式),可以选择spymemcached;如果你不知道你的应用是什么状况,我推荐你用xmemcached,可以在任何情况下获得一个比较好的性能表现。
Kilim是一个Java的actor框架,让你可以在JVM里使用基于协程的actor模型,bluedavy曾经介绍过,这里不再赘言。这篇blog的目的在于分析下kilim实现的基本原理,看看怎么在JVM上实现协程。
在一些语言层面上支持协程的语言,如lua、ruby,都是直接在VM级别支持协程,VM帮你做context的保存和恢复。JVM没有提供这样的指令来保存和恢复方法栈的状态,因此kilim的实现还是需要在bytecode级别做文章。首先,试想下,如果是你来实现协程,你会怎么做?协程的两个基本原语resume和yield,resume运行协程,yield让出执行权,下次resume的时候会从yield的地方重新执行,并且context保持不变。可见,你需要做这么几个事情:
1、在yield的时候保存当前context。
2、在resume的时候恢复context,并根据pc计数来决定从哪里恢复执行。
3、半协程的实现来说,还需要一个调度器来调度所有协程。
4、为了做到用户代码透明,可能需要某种手段去修改用户代码,自动帮你做上面三个事情。
kilim的实现就是干了这么几个事情:
1、利用字节码增强,将普通的java代码转换为支持协程的代码。
2、在调用pausable方法的时候,如果pause了就保存当前方法栈的State,停止执行当前协程,将控制权交给调度器
3、调度器负责调度就绪的协程
4、协程resume的时候,自动恢复State,根据协程的pc计数跳转到上次执行的位置,继续执行。
下面是来自kilim文档的一个例子,同一段代码,在字节码增前前后的变化:
左边是原始代码,右边是通过字节码增强后的代码。其中h方法是pausable的,也就是说可能被暂停阻塞的,g方法因为调用了h这个方法也变成了pausable。
首先看,原始的h方法是没有传入任何参数的,增强后的代码,多了个参数Fiber,指向当前的协程,同样,g方法本来只有一个参数n,现在在后面也多了个Fiber类型的参数,同样是指向当前执行的协程。
其次,在原始的g方法里,一旦调用,马上进入一个for循环。但是在增强后的代码,多了个switch派发的过程,这就是前面提到的,根据当前的Fiber的pc计数,跳转到上一次执行的地方执行。如果是第一次resume,也就是启动协程,那么就将初始循环的i设置为0,进入原始代码的循环部分。Fiber有一个pc计数,称为程序计数器,用于指向恢复context的时候需要跳转到位置。
第三,在g方法里调用h这个可被暂停阻塞的方法的时候,在h方法前后多了一些调用:
f.down();
h(f);
f.up();
kilim的Fiber将每个pauseable方法的调用组织成一个栈,每个pauseable方法都有一个activation frame,翻译过来可以称为活动栈帧,这个栈帧记录了当前的栈的State,注意这个栈跟java本身的方法调用栈区分开来,一个是VM层面的,一个是kilim框架层面的。这里的down方法就是将栈向下延伸,表示将调用一个pauseable方法,并且设置当前State和pc计数。
调用了down之后,才是调用实际的h方法,最后还要调用一次up,顾名思义,就是说一次pauseable方法调用完成,fiber的活动栈要递增一层,回到上一层。但是h方法调用可能出现四种情况:
1、正常的顺利返回,没有状态需要恢复,所谓NOT_PAUSING__NO_STATE
2、也是正常返回,有状态需要恢复,也就是NOT_PAUSING__HAS_STATE
3、h方法暂停阻塞,当前没有保存状态,需要保存状态,这是第一次暂停的时候,称为PAUSING__NO_STATE
4、h方法暂停阻塞,当前已经有状态,不需要保存状态,这是第一次暂停之后的resume再次暂停,称为PAUSING__HAS_STATE,通常不需要处理什么。
第四,可以看到,在up之后,就要根据up返回的上述4种状态执行不同的逻辑:
if (f.isPausing){
//第一次暂停,没有状态
if (!f.hasState){
//new一个State_I2,并保存i和n
f.state = new State_I2(i,n);
//记录pc,还记的前面的switch吗?
f.pc = H1;
}
return;
} else if (f.hasState)
//正常返回,有状态需要恢复,恢复i和n
State_I2 st = (State_I2) f.state;
i = st.i1; n = st.i2;
}
这里没有处理NOT_PAUSING__NO_STATE和PAUSING__HAS_STATE,因为这两种情况在这里不需要处理。
通过上面的分析,我想大家对kilim的实现应该已经有一个很基本的认识。下一步,我们分析一个实际的代码例子,查看整个运作流程。
java.util.LinkedList是双向链表,这个大家都知道,比如Java的基础面试题喜欢问ArrayList和LinkedList的区别,在什么场景下用。大家都会说LinkedList随机增删多的场景比较合适,而ArrayList的随机访问多的场景比较合适。更进一步,我有时候会问,LinkedList.remove(Object)方法的时间复杂度是什么?有的人回答对了,有的人回答错了。回答错的应该是没有读过源码。
理论上说,双向链表的删除的时间复杂度是O(1),你只需要将要删除的节点的前节点和后节点相连,然后将要删除的节点的前节点和后节点置为null即可,
//伪代码
node.prev.next=node.next;
node.next.prev=node.prev;
node.prev=node.next=null;
这个操作的时间复杂度可以认为是O(1)级别的。但是LinkedList的实现是一个通用的数据结构,因此没有暴露内部的节点Entry对象,remove(Object)传入的Object其实是节点存储的value,这里还需要一个查找过程:
public boolean remove(Object o) {
if (o==null) {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
//查找节点Entry
for (Entry<E> e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
//删除节点
remove(e);
return true;
}
}
}
return false;
}
删除节点的操作就是刚才伪代码描述的:
private E remove(Entry<E> e) {
E result = e.element;
e.previous.next = e.next;
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
因此,显然,LinkedList.remove(Object)方法的时间复杂度是O(n)+O(1),结果仍然是O(n)的时间复杂度,而非推测的O(1)复杂度。最坏情况下要删除的元素是最后一个,你都要比较N-1次才能找到要删除的元素。
既然如此,说LinkedList适合随机删减有个前提,链表的大小不能太大,如果链表元素非常多,调用remove(Object)去删除一个元素的效率肯定有影响,一个简单测试,插入100万数据,随机删除1000个元素:
final List<Integer> list = new LinkedList<Integer>();
final int count = 1000000;
for (int i = 0; i < count; i++) {
list.add(i);
}
final Random rand=new Random();
long start=System.nanoTime();
for(int i=0;i<1000;i++){
//这里要强制转型为Integer,否则调用的是remove(int)
list.remove((Integer)rand.nextInt(count));
}
System.out.println((System.nanoTime()-start)/Math.pow(10, 9));
在我的机器上耗时近9.5秒,删除1000个元素耗时9.5秒,是不是很恐怖?注意到上面的注释,产生的随机数强制转为Integer对象,否则调用的是remove(int)方法,而非remove(Object)。如果我们调用remove(int)根据索引来删除:
for(int i=0;i<1000;i++){
list.remove(rand.nextInt(list.size()-1));
}
随机数范围要递减,防止数组越界,换成remove(int)效率提高不少,但是仍然需要2.2秒左右(包括了随机数产生开销)。这是因为remove(int)的实现很有技巧,它首先判断索引位置在链表的前半部分还是后半部分,如果是前半部分则从head往前查找,如果在后半部分,则从head往后查找(LinkedList的实现是一个环):
Entry<E> e = header;
if (index < (size >> 1)) {
//前一半,往前找
for (int i = 0; i <= index; i++)
e = e.next;
} else {
//后一半,往后找
for (int i = size; i > index; i--)
e = e.previous;
}
最坏情况下要删除的节点在中点左右,查找的次数仍然达到n/2次,但是注意到这里没有比较的开销,并且比remove(Object)最坏情况下n次查找还是好很多。
总结下,LinkedList的两个remove方法,remove(Object)和remove(int)的时间复杂度都是O(n),在链表元素很多并且没有索引可用的情况下,LinkedList也并不适合做随机增删元素。在对性能特别敏感的场景下,还是需要自己实现专用的双向链表结构,真正实现O(1)级别的随机增删。更进一步,jdk5引入的ConcurrentLinkedQueue是一个非阻塞的线程安全的双向队列实现,同样有本文提到的问题,有兴趣可以测试一下在大量元素情况下的并发随机增删,效率跟自己实现的特定类型的线程安全的链表差距是惊人的。
题外,ArrayList比LinkedList更不适合随机增删的原因是多了一个数组移动的动作,假设你删除的元素在m,那么除了要查找m次之外,还需要往前移动n-m-1个元素。
摘要: update:更正选择中数的描述,在7到39之间的数组大小选择median-of-three来选择pivot,大小等于7的数组则直接使用中数作为pivot。
quicksort可以说是应用最广泛的排序算法之一,它的基本思想是分治法,选择一个pivot(中轴点),将小于pivot放在左边,将大于pivot放在右边,针对... 阅读全文
Aviator是一个表达式执行引擎,最近由于工作上的原因,又将这个东西扩充了一下,加入了静态的编译优化和seq库。
对于类似"1+2"这样由常量组成的表达式,会在编译的时候直接计算出结果而非生成字节码运行时计算。非常量组成的表达式如"3.14*R*R+4/2"也会在编译的时候优化成"3.14*R*R+2",也就是说能在编译的时候计算的都计算出来,不能在编译的时候确定的就生成字节码,运行时动态计算。默认不启用编译优化,除非设置:
AviatorEvaluator.setOptimize(AviatorEvaluator.EVAL);
另外,加入了seq库用于操作集合和数组,在aviator中,你可以用[ ]操作符直接访问数组和java.util.List,除此之外seq库添加了一些对数组和集合的常用操作,示例如下:
map(seq,println) //打印集合
map(seq,-) //取集合中元素的相反数组成的集合
include(seq,element) //判断element是否在集合中
sort(seq) //排序,返回新的集合
reduce(seq,+,0) //求和
reduce(seq,-,1) //求积
filter(seq,seq.gt(3) //大于3的元素组成的新集合
filter(seq,seq.exists()) //不为nil元素组成的新集合
count(seq) //集合大小
可以看到seq库的风格偏向FP,但是能做的事情其实有限,毕竟 aviator不是一门语言,seq库只提供了最常见的一些函数,其他的只有用户自己扩展了。
Aviator的一个介绍PPT
Aviator 1.0.1也已经放到maven的中心仓库,你可以直接引用:
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>1.0.1</version>
</dependency>
详解clojure递归(上)
详解clojure递归(下)
这篇blog拖到现在才写,如果再不写,估计就只有上篇没有下篇了,趁周末写一下。
上篇提到了Clojure仅支持有限的TCO,不支持间接的TCO,但是有一类特殊的尾递归clojure是支持,这就是相互递归。且看一个例子,定义两个函数用于判断奇数偶数:
(declare my-odd? my-even?)
(defn my-odd? [n]
(if (= n 0)
false
(my-even? (dec n))))
(defn my-even? [n]
(if (= n 0)
true
(my-odd? (dec n))))
这里使用declare做前向声明,不然在定义my-odd?的时候my-even?还没有定义,导致出错。可以看到,my-odd?调用了my-even?,而my-even?调用了my-odd?,这是一个相互递归的定义:如果n为0,那肯定不是奇数,否则就判断n-1是不是偶数,反之亦然。
这个递归定义看起来巧妙,但是刚才已经提到clojure通过recur支持直接的TCO优化,这个相互递归在大数计算上会导致堆栈溢出:
user=> (my-odd? 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)
user=> (my-even? 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)
怎么解决呢?一个办法是转化成一个直接递归调用,定义一个parity函数,当偶数的时候返回0,当奇数的时候返回1:
(defn parity [n]
(loop [n n par 0]
(if (= n 0)
par
(recur (dec n) (- 1 par)))))
user=> (parity 3)
1
user=> (parity 100)
0
user=> (parity 10000)
0
user=> (parity 10001)
1
parity是直接的尾递归,并且使用了recur优化,重新定义my-odd和my-even就很简单了:
(defn my-even? [n] (= 0 (parity n)))
(defn my-odd? [n] (= 1 (parity n)))
但是这样的方式终究不是特别优雅,相互递归的定义简洁优雅,有没有什么办法保持这个优雅的定义并且避免堆栈溢出呢?答案是trampoline,翻译过来是弹簧床,查看trampoline函数文档:
user=> (doc trampoline)
-------------------------
clojure.core/trampoline
([f] [f & args])
trampoline can be used to convert algorithms requiring mutual
recursion without stack consumption. Calls f with supplied args, if
any. If f returns a fn, calls that fn with no arguments, and
continues to repeat, until the return value is not a fn, then
returns that non-fn value. Note that if you want to return a fn as a
final value, you must wrap it in some data structure and unpack it
after trampoline returns.
简单来说 trampoline接收一个函数做参数并调用,如果结果为函数,那么就递归调用函数,如果不是,则返回结果,主要就是为了解决相互递归调用堆栈溢出的问题,查看trampoline的定义:
(defn trampoline
([f]
(let [ret (f)]
(if (fn? ret)
(recur ret)
ret)))
看到 trampoline利用recur做了尾递归优化,原有的my-odd?和my-even?需要做一个小改动,就可以利用trampoline来做递归优化了:
(declare my-odd? my-even?)
(defn my-odd? [n]
(if (= n 0)
false
#(my-even? (dec n))))
(defn my-even? [n]
(if (= n 0)
true
#(my-odd? (dec n))))
唯一的改动就是函数的末尾行前面加了个#,将递归调用修改成返回一个匿名函数了,使用 trampoline调用my-even?和my-odd?不会再堆栈溢出了:
user=> (trampoline my-odd? 10000000)
false
user=> (trampoline my-even? 10000)
true
user=> (trampoline my-even? (* 1000 1000))
true
弹簧床这个名称很形象,一次调用就是落到弹簧床上,如果是函数,反弹回去再来一次,如果不是,本次表演结束。
下班后记一些有趣的东西。
这个话题是阿宝同学问我为什么clojure的PersistentVector的节点Node为什么要有个原子引用指向一个线程:
static class Node implements Serializable {
//记录的线程
transient final AtomicReference<Thread> edit;
final Object[] array;
Node(AtomicReference<Thread> edit, Object[] array){
this.edit = edit;
this.array = array;
}
Node(AtomicReference<Thread> edit){
this.edit = edit;
this.array = new Object[32];
}
}
我还真不懂,没有细看过这部分代码,早上花点时间学习了下。
PersistentVector的实现是另一个话题,这里不提。我们都知道clojure的数据结构是immutable的,修改任意一个数据结构都将生成一个新的数据结构,原来的不变。为了减少复制的开销,clojure的数据结构同时是persistent,所谓持久数据结构是将数据组织为树形的层次结构,修改的时候只是root改变,指向不同的节点,原有的节点仍然复用,从而避免了大量的数据复制,具体可以搜索下 ideal hash trees这篇paper, paper难懂,可以看看 这篇blog。
但是在创建PersistentVector的时候,从一堆现有的元素或者集合创建一个PersistentVector,如果每次都重新生成一个PersistentVector,未免太浪费,创建过程的性能会受到影响。我们完全可以假设创建PersistentVector这个过程肯定是线程安全的,没有必要每添加一个元素就重新生成一个PersistentVector,完全可以在同一个PersistentVector上修改。这就是TransientVector的意义所在。
TransientVector就是一个可修改的Vector,调用它添加一个元素,删除一个元素,都是在同一个对象上进行,而不是生成新的对象。查看PersistentVector的创建:
static public PersistentVector create(ISeq items){
TransientVector ret = EMPTY.asTransient();
for(; items != null; items = items.next())
ret = ret.conj(items.first());
return ret.persistent();
}
static public PersistentVector create(List items){
TransientVector ret = EMPTY.asTransient();
for(Object item : items)
ret = ret.conj(item);
return ret.persistent();
}
static public PersistentVector create(Object items){
TransientVector ret = EMPTY.asTransient();
for(Object item : items)
ret = ret.conj(item);
return ret.persistent();
}
看到三个方法的第一步都是将EMPTY集合transient化,生成一个可修改的TransientVector:
TransientVector(PersistentVector v){
this(v.cnt, v.shift, editableRoot(v.root), editableTail(v.tail));
}
static Node editableRoot(Node node){
return new Node(new AtomicReference<Thread>(Thread.currentThread()), node.array.clone());
}
生成的时候记录了当前的线程在root节点。然后添加元素的时候直接调用TransientVector的conj方法,查看conj可以看到每次返回的都是this:
public TransientVector conj(Object val){
//确保修改过程合法
ensureEditable();
//忽略逻辑
return this;
}
查看ensureEditable方法:
void ensureEditable(){
Thread owner = root.edit.get();
if(owner == Thread.currentThread())
return;
if(owner != null)
throw new IllegalAccessError("Transient used by non-owner thread");
throw new IllegalAccessError("Transient used after persistent! call");
}
终于看到Node中的edit引用的线程被使用了,判断当前修改的线程是否是使得集合transient化的线程,如果不是则抛出异常,这是为了保证对TransientVector的编辑是在同一个线程里,防止因为意外发布TransientVector引用引起的线程安全隐患。
知道了transient集合的用途,我们能在clojure中使用吗?完全没问题,clojure.core有个transient方法,可以将一个集合transient化:
(defn transient
[^clojure.lang.IEditableCollection coll]
(.asTransient coll))
前提是这个集合是可编辑的,clojure的map、vector和set都是可编辑的。让我们确认下transient修改后的集合还是不是自身:
user=> (def v1 [1 2 3])
#'user/v1
user=> (def v2 (transient v1))
#'user/v2
user=> v2
#<TransientVector clojure.lang.PersistentVector$TransientVector@7eb366>
定义了集合v1,v2是调用了transient之后的集合,查看v2,果然是一个 TransientVector。查看v2的元素个数是不是3个:
user=> (.count v2)
3
没问题,注意,我们不能直接调用count函数,因为v2是个普通的java对象,我们必须使用dot操作符来调用java对象的方法。添加一个元素看看:
user=> (def v3 (.conj v2 4))
#'user/v3
user=> v3
#<TransientVector clojure.lang.PersistentVector$TransientVector@7eb366>
添加一个元素后形成集合v3,查看v3,跟v2是同一个对象 #<TransientVector clojure.lang.PersistentVector$TransientVector@7eb366>
。证明了transient集合修改的是自身,而不是生成一个新集合。确认下4有加入v2和v3:
user=> (.nth v3 3)
4
user=> (.count v2)
4
user=> (.count v3)
4
user=> (.nth v2 3)
4
果然没有问题。transient集合的使用应当慎重,除非能确认没有其他线程会去修改集合,并且对线程的可见性要求不高的时候,也许可以尝试下这个技巧。
这个问题的由来是有一个朋友报告xmemcached在高并发下会频繁断开重连,导致cache不可用,具体请看这个 issue。
我一开始以为是他使用方式上有问题,沟通了半天还是搞不明白。后来听闻他说他的代码会中断运行时间过长的任务,这些任务内部调用了xmemcached跟memcached交互。我才开始怀疑是不是因为中断引起了连接的关闭。
我们都知道,nio的socket channel都是实现了 java.nio.channels.InterruptibleChannel接口,看看这个接口描述:
A channel that can be asynchronously closed and interrupted.
A channel that implements this interface is asynchronously closeable: If a thread is blocked in an I/O operation on an interruptible channel then another thread may invoke the channel's close method. This will cause the blocked thread to receive an AsynchronousCloseException.
A channel that implements this interface is also interruptible: If a thread is blocked in an I/O operation on an interruptible channel then another thread may invoke the blocked thread's interrupt method. This will cause the channel to be closed, the blocked thread to receive a ClosedByInterruptException, and the blocked thread's interrupt status to be set.
If a thread's interrupt status is already set and it invokes a blocking I/O operation upon a channel then the channel will be closed and the thread will immediately receive a ClosedByInterruptException; its interrupt status will remain set.
意思是说实现了这个接口的channel,首先可以被异步关闭,阻塞的线程抛出AsynchronousCloseException,其次阻塞在该 channel上的线程如果被中断,会引起channel关闭并抛出ClosedByInterruptException的异常。如果在调用 channel的IO方法之前,线程已经设置了中断状态,同样会引起channel关闭和抛出ClosedByInterruptException。
回到xmemcached的问题,为什么中断会引起xmemcached关闭连接?难道xmemcached会在用户线程调用channel的IO operations。答案是肯定的,xmemcached的网络层实现了一个小优化,当连接里的缓冲队列为空的情况下,会直接调用 channel.write尝试发送数据;如果队列不为空,则放入缓冲队列,等待Reactor去执行实际的发送工作,这个优化是为了最大化地提高发送效率。这会导致在用户线程中调用channel.write(缓冲的消息队列为空的时候),如果这时候用户线程中断,就会导致连接断开,这就是那位朋友反馈的问题的根源。
Netty3的早期版本也有同样的优化,但是在之后的版本,这个优化被另一个方案替代,写入消息的时候无论如何都会放入缓冲队列,但是Netty会判断当前写入的线程是不是NioWorker, 如果是的话,就直接flush整个发送队列做IO写入,如果不是,则加入发送缓冲区等待NioWorker线程去发送。这个是在NioWorker的 writeFromUserCode方法里实现的:
void writeFromUserCode(final NioSocketChannel channel) {
if (!channel.isConnected()) {
cleanUpWriteBuffer(channel);
return;
}
if (scheduleWriteIfNecessary(channel)) {
return;
}
// 这里我们可以确认 Thread.currentThread() == workerThread.
if (channel.writeSuspended) {
return;
}
if (channel.inWriteNowLoop) {
return;
}
write0(channel);
}
我估计netty的作者后来也意识到了在用户线程调用channel的IO操作的危险性。xmemcached这个问题的解决思路也应该跟Netty差不多。但是从我的角度,我希望交给用户去选择,如果你确认你的用户线程没有调用中断,那么允许在用户线程去write可以达到更高的发送效率,更短的响应时间;如果你的用户线程有调用中断,那么最好有个选项去设置,发送的消息都将加入缓冲队列让Reactor去写入,有更好的吞吐量,同时避免用户线程涉及到 IO操作。
忘了啥时候听说盛大的电子书bambook要搞内测,然后某天抱着试一试的心态,翻出许久未用的起点帐号,一看我竟然还是04年注册的高级VIP用户,想起来那时候好像是为了看一些网络小说特意注册的。然后申请了内测邀请码,还预先存了50块起点币。内测开始那天是9号吧,中午2点多翻了下短信,没想到我运气不错,成了第一批bambook内测用户。哇卡卡,赶紧下单,付款的时候费了点功夫,用信用卡付不了,只好下午下班后存了些钱到工资卡,付款成功已经是晚上7点了。
接下来是漫长的等待,本以为上海到杭州最多两天,没想到查看订单状态一直停留在配货状态,这让习惯了京东神速的我很着急,不过看起来盛大文学还是一直在改进这个订单系统,后来将宅急送的订单查询也融合到订单里面,用户可以查看自己的东西当前在哪里。3天后终于收到货,中间发生了一个小插曲,我发现订单状态变成了完成,显示我已经签收,而我实际上没有收到快递公司的电话,更何况签收?打电话给客服,客服叫我别着急,他们先查查。后来才知道公司是统一代收快递,小邮局代收了,给我发了邮件才知道。盛大的客服还是很负责任的,后来打电话向我确认情况才知道是虚惊一场,实在不好意思。物流这块,我觉的盛大这次内测做的并不好,看看bambook官方论坛那么多抱怨就知道,发货太慢,并且客服的反馈都是复制粘帖,一律说什么产品销售火爆,发货快不起来的鬼话。这是内测呀,才发多少台,只能说前期准备不是特别充分。
先看照片,我儿子很喜欢bambook
外观我觉的还是很漂亮,我没有用过其他的电子书产品,bambook给我的感觉还不错,屏幕是6寸的电子墨水屏,还有一些快捷键方便翻页、查看书架等,还有一个摇杆作为确认键,也可以翻页以及调整选项等。背面是奥运火炬的祥云图案,这些网上已经有很多评测照片了,我就不多说了。从硬件上,bambook还是很让我满意的。不过没有提供阅读灯挺让人遗憾,不知道会不会作为配件提供,thinkpad的键盘灯是非常棒的设计,如果能有阅读灯,那就可以不打扰家人读书了。
这是我第一次接触电子墨水屏,曹老大跟我说会有残影的问题,入手后发现果然有这个现象,但是不细看还是很难看出来,对阅读没有任何影响。相反,这种屏幕对读书还是极好,在阳光下也可以正常阅读,并且只有在有变化的时候费电,非常省电,我拿到手连续看了两天小说,电池显示还至少一格电。通过云梯——bambook提供的客户端软件,可以修改字体,修改成微软雅黑,终于不用捧着个笔记本在床上看书。
bambook至少提供了3种上网方式:USB连接PC代理上网、WIFI以及3G网络。目前3G卡还不行,USB我没测试。公司和家里都是无线网络,在公司可以正常连接,但是在家里却不行。后来发现这是因为bambook无法输入SID,而我家的SID广播是禁止掉的,为了让bambook能联网,只好修改路由器配置了。这一点不是很好,对家庭用户来说,通常还是会禁止SID广播。bambook上网只能连接盛大的云中书城,包括起点、榕树下等盛大文学下面的网站。这些网站大多数是一些网络文学作品,我尝试搜索一些文史类的读物都没有找到。网络小说我还是会读,比如最近又重新看《庆余年》(bamboook里这本书是免费的),但是买bambook可不仅仅是为了读网络小说。还是希望云中书城能多一些其他方面的书籍,不过内容建设是个长期的过程,倒是不着急,毕竟还在内测阶段。
bambook允许传入自有书籍,但是需要转换成它的内部测试,利用云梯这个客户端软件。云梯目前只支持windows,我的机器是ubuntu,不过尝试了下在virtualbox虚拟机上的XP里也可以正常使用。我尝试转换了txt和pdf文件,都可以正常转换,比较离奇的是txt转换后的文件还更大。PDF这是重点,我还是想在bambook上阅读一些技术文档,这些文档大多数是PDF格式。遗憾的是bambook对PDF转换的支持并不完善,章节索引没了不说,转换后的格式在6寸屏幕上的排版很糟糕,诸如图表、代码之类的跟原文档比起来很不适合阅读。事实上,我是很不理解为什么要自定义格式,如果说是为了保护商业版权,但是你又允许上传自由书籍,自由书籍还需要经过这么一个转换步骤,还不如原生支持一些常见格式。bambook的os是基于anroid开发的,我相信原生支持PDF这样的常见格式肯定不是特别大的问题。pdf文档这个问题在官网上也有很多讨论,从客服的反馈来看是有计划改进,拭目以待吧。
接下来说说我认为bambook软件上可以改进的一些小地方,首先是刚才提到的网络连接问题,允许用户输入隐藏的SID。网络在不用一段时间后会自动断开,这个时间我估计了下是10-15分钟左右,但是没有找到可以这个时间的地方,这个选项还是应当开放给用户来选择。字体上我也希望能自定义字体,目前是预先定义了几个大类,你只能在这几个大类里选,我还是希望能输入数值自定义字体大小,好像字体的设置还有个Bug,设置的字体不会保存,在下次继续阅读的时候还需要重新设置。另外,就是那些提示框比较傻,非要我点下确认才消失,提示框应当在几秒内自动消失,这对用户体验更好。书架也应当允许分类,现在是一股脑堆在一起,几本书还好,多了就比较麻烦了。书架的更新和内容的更新应当分离,有时候我只是想更新下书架,并不想更新内容。
硬件上我希望尽快出个套套,这东西白的稀里哗啦,很容易弄脏和摔坏,有个套套比较好,最好套套上能再带个阅读灯那就更好了。充电方式也应当再多样化一点,目前只能连到PC充电,并且是无论如何都要会自动充电,应当让用户决定连接的时候是否充电才对。云梯在格式转换上还要下更大功夫才行,当然,我更希望是直接原生支持PDF了,不过从一个盛大的朋友那了解到这是比较困难的。
总之,内测998这个价,bambook还是物有所值的,硬件上很好,软件上问题不少,内容建设上还有很多要做的事情,例如我希望以后每天早上能直接用bambook看当天的报纸,哪怕是要钱订阅的。在淘宝上查了下,bambook内测这个版本转手也能卖到1200甚至更多,还是挺抢手的。庆幸的是bambook可以刷固件,等哪天升级的时候,希望这些软件上的小问题能有个比较好的解决方案。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
八、future、promise和线程
1 、Clojure中使用future是启动一个线程,并执行一系列的表达式,当执行完成的时候,线程会被回收:
user=> (def myfuture (future (+ 1 2)))
#'user/myfuture
user=> @myfuture
3
future接受一个或者多个表达式,并将这些表达式交给一个线程去处理,上面的(+ 1 2)是在另一个线程计算的,返回的future对象可以通过deref或者@宏来阻塞获取计算的结果。
future函数返回的结果可以认为是一个类似java.util.concurrent.Future的对象,因此可以取消:
user=> (future-cancelled? myfuture)
false
user=> (future-cancel myfuture)
false
也可以通过谓词future?来判断一个变量是否是future对象:
user=> (future? myfuture)
true
2、Future的实现,future其实是一个宏,它内部是调用future-call函数来执行的:
(defmacro future
[& body] `(future-call (fn [] ~@body)))
可以看到,是将body包装成一个匿名函数交给future-call执行,future-call接受一个Callable对象:
(defn future-call
[^Callable f]
(let [fut (.submit clojure.lang.Agent/soloExecutor f)]
(reify
clojure.lang.IDeref
(deref [_] (.get fut))
java.util.concurrent.Future
(get [_] (.get fut))
(get [_ timeout unit] (.get fut timeout unit))
(isCancelled [_] (.isCancelled fut))
(isDone [_] (.isDone fut))
(cancel [_ interrupt?] (.cancel fut interrupt?)))))i
将传入的Callable对象f提交给Agent的soloExecuture
final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();
执行,返回的future对象赋予fut,接下来是利用clojure 1.2引入的reify定义了一个匿名的数据类型,它有两种protocol:clojure.lang.IDeref和java.utill.concurrent.Future。其中IDeref定义了deref方法,而Future则简单地将一些方法委托给fut对象。protocol你可以理解成java中的接口,这里就是类似多态调用的作用。
这里有个地方值的学习的是,clojure定义了一个future宏,而不是直接让用户使用future-call,这符合使用宏的规则: 避免匿名函数。因为如果让用户使用future-call,用户需要将表达式包装成匿名对象传入,而提供一个宏就方便许多。
3、启动线程的其他方法,在clojure中完全可以采用java的方式去启动一个线程:
user=> (.start (Thread. #(println "hello")))
nil
hello
4、promise用于线程之间的协调通信,当一个promise的值还没有设置的时候,你调用deref或者@想去解引用的时候将被阻塞:
user=> (def mypromise (promise))
#'user/mypromise
user=> @mypromise
在REPL执行上述代码将导致REPL被挂起,这是因为mypromise还没有值,你直接调用了@mypromise去解引用导致主线程阻塞。
如果在调用@宏之前先给promise设置一个值的话就不会阻塞:
user=> (def mypromise (promise))
#'user/mypromise
user=> (deliver mypromise 5)
#<AFn$IDeref$db53459f@c0f1ec: 5>
user=> @mypromise
5
通过调用deliver函数给mypromise传递了一个值,这使得后续的@mypromise直接返回传递的值5。显然promise可以用于不同线程之间的通信和协调。
5、promise的实现:promise的实现非常简单,是基于CountDownLatch做的实现,内部除了关联一个CountDownLatch还关联一个atom用于存储值:
(defn promise
[]
(let [d (java.util.concurrent.CountDownLatch. 1)
v (atom nil)]
(reify
clojure.lang.IDeref
(deref [_] (.await d) @v)
clojure.lang.IFn
(invoke [this x]
(locking d
(if (pos? (.getCount d))
(do (reset! v x)
(.countDown d)
this)
(throw (IllegalStateException. "Multiple deliver calls to a promise"))))))))
d是一个CountDownLatch,v是一个atom,一开始值是nil。返回的promise对象也是通过reify定义的匿名数据类型,他也是有两个protocol,一个是用于deref的IDeref,简单地调用d.await()阻塞等待;另一个是匿名函数,接受两个参数,第一个是promise对象自身,第二个参数是传入的值x,当d的count还大于0的请看下,设置v的值为x,否则抛出异常的多次deliver了。查看下deliver函数,其实就是调用promise对象的匿名函数protocol:
(defn deliver
{:added "1.1"}
[promise val] (promise val))
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
七、并发函数pmap、pvalues和pcalls
1、pmap是map的进化版本,map将function依次作用于集合的每个元素,pmap也是这样,但是它对于每个集合中的元素都是提交给一个线程去执行function,也就是并行地对集合里的元素执行指定的函数。通过一个例子来解释下。我们先定义一个make-heavy函数用于延时执行某个函数:
(defn make-heavy [f]
(fn [& args]
(Thread/sleep 1000)
(apply f args)))
make-heavy接受一个函数f作为参数,返回一个新的函数,它延时一秒才实际执行f。我们利用make-heavy包装inc,然后执行下map:
user=> (time (doall (map (make-heavy inc) [1 2 3 4 5])))
"Elapsed time: 5005.115601 msecs"
(2 3 4 5 6)
可以看到总共执行了5秒,这是因为map依次将包装后的inc作用在每个元素上,每次调用都延时一秒,总共5个元素,因此延时了5秒左右。这里使用doall,是为了强制map返回的lazy-seq马上执行。
如果我们使用pmap替代map的话:
user=> (time (doall (pmap (make-heavy inc) [1 2 3 4 5])))
"Elapsed time: 1001.146444 msecs"
(2 3 4 5 6)
果然快了很多,只用了1秒多,显然pmap并行地将make-heavy包装后的inc作用在集合的5个元素上,总耗时就接近于于单个调用的耗时,也就是一秒。
2、pvalues和pcalls是在pmap之上的封装,pvalues是并行地执行多个表达式并返回执行结果组成的LazySeq,pcalls则是并行地调用多个无参数的函数并返回调用结果组成的LazySeq。
user=> (pvalues (+ 1 2) (- 1 2) (* 1 2) (/ 1 2))
(3 -1 2 1/2)
user=> (pcalls #(println "hello") #(println "world"))
hello
world
(nil nil)
3、pmap的并行,从实现上来说,是集合有多少个元素就使用多少个线程:
1 (defn pmap
2 {:added "1.0"}
3 ([f coll]
4 (let [n (+ 2 (.. Runtime getRuntime availableProcessors))
5 rets (map #(future (f %)) coll)
6 step (fn step [[x & xs :as vs] fs]
7 (lazy-seq
8 (if-let [s (seq fs)]
9 (cons (deref x) (step xs (rest s)))
10 (map deref vs))))]
11 (step rets (drop n rets))))
12 ([f coll & colls]
13 (let [step (fn step [cs]
14 (lazy-seq
15 (let [ss (map seq cs)]
16 (when (every? identity ss)
17 (cons (map first ss) (step (map rest ss)))))))]
18 (pmap #(apply f %) (step (cons coll colls))))))
在第5行,利用map和future将函数f作用在集合的每个元素上,future是将函数f(实现callable接口)提交给Agent的CachedThreadPool处理, 跟agent的send-off共用线程池。
但是由于有chunked-sequence的存在, 实际上调用的线程数不会超过chunked的大小,也就是32。事实上,pmap启动多少个线程取决于集合的类型,对于chunked-sequence,是以32个元素为单位来批量执行,通过下面的测试可以看出来,range返回的是一个chunked-sequence,clojure 1.1引入了chunked-sequence,目前那些返回LazySeq的函数如map、filter、keep等都是返回chunked-sequence:
user=> (time (doall (pmap (make-heavy inc) (range 0 32))))
"Elapsed time: 1003.372366 msecs"
(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32)
user=> (time (doall (pmap (make-heavy inc) (range 0 64))))
"Elapsed time: 2008.153617 msecs"
(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64)
可以看到,对于32个元素,执行(make-heavy inc)耗费了一秒左右;对于64个元素,总耗时是2秒,这可以证明64个元素是分为两个批次并行执行,一批32个元素,启动32个线程(可以通过jstack查看)。
并且pmap的执行是半延时的(semi-lazy),前面的总数-(cpus+2)个元素是一个一个deref(future通过deref来阻塞获取结果),后cpus+2个元素则是一次性调用map执行deref。
4、pmap的适用场景取决于将集合分解并提交给线程池并行执行的代价是否低于函数f执行的代价,如果函数f的执行代价很低,那么将集合分解并提交线程的代价可能超过了带来的好处,pmap就不一定能带来性能的提升。pmap只适合那些计算密集型的函数f,计算的耗时超过了协调的代价。
5、关于chunked-sequence可以看看 这篇报道,也可以参考 Rich Hickey的PPT。chunk sequence的思路类似批量处理来提高系统的吞吐量。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
前面几节基本介绍Clojure对并发的支持,估计这个系列还能写个两三节,介绍下一些常见的concurrent function的使用。谈了很多优点,现在想谈谈clojure的一些缺憾,例如Agent系统。
Agent在前面已经介绍过了,用于异步更新,基本的原理是将更新操作封装为action,提交给线程池处理。内部有两个线程池,固定大小(cpus+2)的线程池用于处理send发送的action,而send-off发送的action则是由一个Cached ThreadPool处理的。因此,如果你的更新操作很耗时或者会阻塞(如IO操作),那么通常是建议使用send-off,而send适合一些计算密集型的更新操作。两个线程池的声明如下(Agent.java):
1 final public static ExecutorService pooledExecutor =
2 Executors.newFixedThreadPool(2 + Runtime.getRuntime().availableProcessors());
3
4 final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();
说说我认为Agent可以改进的地方。
首先是从实践角度出发,通常我们要求new一个线程池的时候,要设置线程池内线程的name,以方便查看和调试。因此Clojure这里简单的new线程池是一个可以改进的地方,应当自定义一个ThreadFactory,给clojure的Agent线程提供明确的名称。
其次,由于这两个线程池是全局的,因此clojure提供了shutdown-agents的方法用于关闭线程池。但是由于这些线程池内的线程并非daemon,因此如果你没有明确地调用shutdown-agents,jvm也可以正常退出。 我们都知道,如果还有dadmon线程没有终止,JVM是无法退出的。如果JVM只剩下daemon线程,那么jvm就会自动退出。从实践角度,应当明确地要求用户调用shutdown-agents来关闭Agent系统,妥善终止线程,并且Agent的线程池应当延迟初始化,只在必要的时候创建,而非现在的静态变量。所以,在实现ThreadFactory的时候,应当设置生成的线程为daemon。
第三,同样由于线程池是全局的,关闭了却没有办法重新启动,这不能不说是一个缺憾。Clojure没有提供重新启动的方法。
第四,线程池简单地分为两类,从理论上足以满足大部分应用的要求。但是在real world的应用上,我们通常不敢用CachedThreadPool,这是为了防止内存不受控,导致线程创建过多直接OOM。通常我们会使用固定大小的线程池,但是clojure固定大小的线程池只有一个,并且大小写死为cpus+2,这就没有了控制的余地。我还是希望clojure能提供允许自定义Agent线程池的方法,可以在创建的时候传入线程池,如:
(agent :executor (java.util.concurrent.Executors/newFixedThreadPool 2))
或者提供新的API,如set-executor!来设置agent使用的线程池,如果没有自定义线程池再使用全局的。当然也需要提供一个关闭agent自定义线程池的API:
(shutdown-agent agent)
需要自定义线程池是另一个原因是为了最大化地发挥线程池的效率,我们知道,线程池只有在执行“同构”任务的时候才能发挥最大的效率,如果有的 action快,有的action慢,那么该快的快不起来,慢的却挤占了快的action的执行时间。通过给Agent设定自己的线程池某些程度上可以解决这个问题。
Agent的整个模型是很优雅的,但是确实还有这些地方不是特别让人满意,希望以后会得到改进。
今年我被分配了不少参与面试应聘者的任务,大多数是做电话面试,偶尔做F2F的面试。这是我第一次这么频繁地面试别人,过去我都是被别人面试。主要想谈谈一些感想,从面试官的角度给面试者一些建议。因为做面试官的经验就这么多,我大概地讲,有兴趣的大概地看。
面试者需要首先知道自己是想应聘什么职位,很多人的简历到我这里来,都不知道自己是面试什么职位,那么聊的时候就经常对不上号,原来你不是我们想要那种人,我不是去应聘你这个职位。我想在投简历的时候需要明确下自己想来这家公司做什么,这样可以更明确一些,节省大家时间。并且面试者准备起来也能更充分一些。
做电话面试的时候,我比较怕那种声音很小的朋友,听起来费劲,我问起来费劲。我提问的方式,一般是喜欢穷追到底,根据对方的描述即兴发问,一方面查看知识掌握得牢固不牢固,一方面查看反应能力怎么样。如果你声音很小,我发问的频率降低了,我对面试者的评价也自然降低了。
另外,做电话面试,面试者最好能在接电话的时候找个安静的地方,背景的嘈杂也会影响面试的效果,有时候不得不我去提醒对方找一个安静一些地方。您老做其他公司的电话面试,还是别当着同事老板的面嘛。
公司还有做视频面试,视频面试的时候一般是通过旺旺,做语音聊天,这时候面试者还是需要注意下个人的举止。面对面的时候,大家会小心注意一些,但是做视频面试的时候,由于你面对只是一个摄像头,没有那么紧张,一些举止可能不恰当。有一次我视频面试一个同学,面试聊着聊着,他都将脚抬到桌子上,对着摄像头了,尽管我并不介意,但是遇上其他面试官就不一定那么好说话了。这位同学最后没过,不过不是我这关刷下的了。
我看简历的时候,一般都是先看项目经历,基本上你做过什么项目,大体能告诉我你擅长的技术方向,接下来看个人简介,与你的项目经历是否符合,如果你描述了你擅长什么,精通什么,那么后续的面试我就问这些东西。当然,如果你的技术方向跟我们的要求完全不吻合,那我可能只是打个电话走过场。
简历上能让人眼前一亮的地方包括:除了Java之外,你还会python、Ruby甚至Scheme之类的其他语言;你参与了某些开源项目;你对某个领域的技术特别擅长,并且做过这方面的学习研究;你做过性能优化,系统调优之类的功能;你描述了对某个系统做出的一些优化或者改进;你有自己的技术博客;你读过某些开源项目的源码;你指导过别人的工作……
简历上让人疲倦的地方:一堆同类项目的罗列,没有鲜明的描述,没有说明你在项目里做了什么;只有项目罗列,没有个人描述;只有结果,没有过程;只有一堆名词,没有特别擅长的亮点。
这大半年来,也面试了不少牛人,但是最后能进来的却是寥寥无几,我觉的特别牛的,可能老大们觉的某个方面不合适,也可能待遇上谈不拢,最终都来不了,很可惜。我也提议过改进面试的方式,不仅仅是聊天式的面试,最好能增加一个上机面试,给一道很简单的题目,你做出来看看,不仅仅看程序写的对不对,更多看看你写的代码是否符合工程规范,是否有单元测试,变量命名是否合理,在思路上有没有亮点等等。我总觉的简单地通过面谈来考察一个人是很难靠谱的,程序员还是要以代码说话。后来也写了个上机面试的提纲,不过最终这件事情也是不了了之。
另外,面试通过其实应该是万里长征的第一步,公司在招进一个人之后,基本上是不管不问了,这也可以理解,毕竟都是成年人了;但是对于外地来的员工,很多都是独身一人就跑到杭州来的,情感上适应是第一步,还有找房子、熟悉周边环境等等琐事。既然在前期招聘已经花了很大力气,如果可以在员工入职后稍微关心下新入员工的状况,这是特别划算的买卖。入职刚转正的员工马上走人,这对公司的损失更大。
转眼间快到8月,已经过去了两个季度,是时候稍微总结下干了什么,以后想干什么。从春节到现在,我仍然是做淘宝的消息中间件Notify的开发,中间额外去做了一个多月的另一个项目的开发,重拾了web开发的一些东西。
这半年来Notify的改进集中在通讯层的改造,引入AMQP的订阅模型,以及将重要消息从oracle迁移到mysql做HA方案,这一过程是一个慢慢稳定系统的过程。新版本刚出来的时候有不少BUG,有些BUG很严重,幸运的是没有造成严重的后果,再一次提醒我小心,小心,再小心;小心是一方面,工作有没有做到位是另一个方面,暴露出来的问题还是单元测试不全面,以及麻痹大意心态下的不警惕,对关键代码认识不清,code review也没有做到位等等。
Notify做到现在,剩下的问题是一些很难解决,又非常关键性的问题,如消息的去重,消息的顺序性,以及消息的可靠存储。我说的这些问题都是在分布式集群环境下需要解决的问题,如果仅仅是单台服务器,这些问题的解决还算不上特别困难。消息的去重,基本的思路是在客户端和服务器之间各自维持一个状态,用于保存当前消息的处理情况,依据这个情况来做消息的去重,但是状态的保存对服务器和客户端来说都是一个额外的负担,并且很难做到可靠的存储,如果状态丢了,去重的目的就没办法做到。ActiveMQ里是在服务器和客户端都维持了一个bitmap做重复的检测,但是这个bitmap大小必然是受限的,并且无法持久保存的。消息的在集群环境下的顺序性,涉及到集群环境下的事件的时间顺序问题,除了使用分布式锁来保证一致性之外,暂时也没有好的思路去解决。消息的可靠存储,今年我们的目标至少是脱离oracle,目前实现的HA mysql双写的实现已经开始部署到交易这样的核心系统上,第三个季度将慢慢地全部切换过去。下一步的目标是将消息存储到key-value系统上,但是需要解决的是索引的问题,因为消息的恢复涉及到查询,并且需要根据一些特殊条件做查询以应付诸如尽快恢复重要消息这样的功能,因此目前的一些key-value系统要么在索引功能上太弱,要么在集群功能上太弱,要么在大数据量存储上有局限,因此不是特别切合我们的场景,因此一个可行的方案是将消息的header继续存储在关系数据库,方便做查询,而将数据较大的body存储在key-value上,减轻数据库的负担。今年,我们还是希望能在以上3个方向某个方向做出突破。
这半年来技术上的收获,第一个季度业余时间都去打游戏了,没方什么心思在学习和工作上,后来去学习了下ASM,总算对java的byte code,以及jvm执行byte code过程有了个理解,然后利用ASM去搞了aviator这个项目。接下来开始做服务器的SEDA改造,这个过程完成了部分,但是不满意,SEDA的模型过于理论化,模型是好的,但是在stage controller的实现上目前没有可供参考的经验,做到资源的自动控制更需要实际的测试和实践,基本的指导原则只能作为参考。另外,最近下决心去重构整个项目,从一个一个类看起,看到不爽的地方马上去做重构,这个过程,我又去重看了下《重构》中的原则,在谈论诸如分布式、海量数据存储、云计算这样的大词之前,我需要的做的仍然是将代码写好,写的漂亮。也许是时候回到本源,再去重读下《设计模式》,重读下《重构》,既然我还在写java代码,那还是希望写的更好点。
另外,我现在喜欢上了clojure语言,并且正儿八经地找了本书好好学习,从源码和bytecode入手去理解它的实现。我为什么喜欢clojure?
首先,它是LISP的方言之一,LISP的优点它全有,LISP的缺点它能想办法避免。Clojure也有宏,也有quote,也有将procedure as data的能力,Clojure的数据结构都是immutable,此外还是persistence,避免了immutable数据大量拷贝带来的开销。Clojure的数据结构还天生是lazy的,表达能力上一个台阶。Clojure在语法上更多变化,某些程度上降低了括号的使用频率,这一点有利有弊。Clojure的内在原则是一致的,核心语法非常简单,它没有多种范式可供选择,因此没有scala那样复杂的类型系统,没有为了包容java程序员引入的OO模型(有替代品),使用clojure最好的方式是函数式地,但是它的扩展能力允许你去尝试各种范式。
其次,Clojure对并发的支持更符合一般程序员的理解,它只是将锁替换成了事务,利用STM去保存可变状态,但是却避免锁带来的缺点——死锁、活锁、竞争条件。它没有引入新的模型,这对习惯于用锁编程的同学来说,STM没有很大差异,你可以将它理解成内存型数据库。
第三,最重要的一点,Clojure是实现于JVM之上的,Java上的任何东西它都能直接利用,并且利用type hint之类的手段可以做到性能上没有损失。尽管Java语言有千般不是,但是寄生于整个平台之上的开源生态系统是任何其他社区都很难比拟的,放弃Java平台这个宝库,暂时还做不到。过去学习scheme,学习common lisp,更多的目的是开阔眼界,现在能实际地使用,还能有比这更幸福的事情吗?
下半年技术上想学习什么呢?除了clojure之外,我想去看下haskell,了解下什么是mond,除此之外,就是收收心将《算法导论》读完吧。另外,收起对awk和shell编程的偏见,好好熟悉下这两个工具,dirty and quickly的干活有时候还是很重要的。
我还是个典型的码农,喜欢写代码,喜欢尝试新东西,至少热情和好奇心还残存一些,那么就继续当好码农吧。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
五、binding和let
前面几节已经介绍了Ref、Atom和Agent,其中Ref用于同步协调多个状态变量,Atom只能用于同步独立的状态变量,而Agent则是允许异步的状态更新。这里将介绍下binding,用于线程内的状态的管理。
1、binding和let:
当你使用def定义一个var,并传递一个初始值给它,这个初始值就称为这个var的root binding。这个root binding可以被所有线程共享,例如:
user=> (def foo 1)
#'user/foo
那么对于变量foo来说,1是它的root binding,这个值对于所有线程可见,REPL的主线程可见:
user=> foo
1
启动一个独立线程查看下foo的值:
user=> (.start (Thread. #(println foo)))
nil
1
可以看到,1这个值对于所有线程都是可见的。
但是,利用binding宏可以给var创建一个thread-local级别的binding:
(binding [bindings] & body)
binding的范围是动态的,binding只对于持有它的线程是可见的,直到线程执行超过binding的范围为止,binding对于其他线程是不可见的。
user=> (binding [foo 2] foo)
2
粗看起来,binding和let非常相似,两者的调用方式近乎一致:
user=> (let [foo 2] foo)
2
从一个例子可以看出两者的不同,定义一个print-foo函数,用于打印foo变量:
user=> (defn print-foo [] (println foo))
#'user/print-foo
foo不是从参数传入的,而是直接从当前context寻找的,因此foo需要预先定义。分别通过let和binding来调用print-foo:
user=> (let [foo 2] (print-foo))
1
nil
可以看到,print-foo仍然打印的是初始值1,而不是let绑定的2。如果用binding:
user=> (binding [foo 2] (print-foo))
2
nil
print-foo这时候打印的就是binding绑定的2。这是为什么呢?这是由于let的绑定是静态的, 它并不是改变变量foo的值,而是用一个词法作用域的foo“遮蔽”了外部的foo的值。但是print-foo却是 查找变量foo的值,因此let的绑定对它来说是没有意义的,尝试利用set!去修改let的foo:
user=> (let [foo 2] (set! foo 3))
java.lang.IllegalArgumentException: Invalid assignment target (NO_SOURCE_FILE:12)
Clojure告诉你,let中的foo不是一个有效的赋值目标 ,foo是不可变的值。set!可以修改binding的变量:
user=> (binding [foo 2] (set! foo 3) (print-foo))
3
nil
2、Binding的妙用:
Binding可以用于实现类似AOP编程这样的效果,例如我们有个fib函数用于计算阶乘:
user=> (defn fib [n]
(loop [ n n r 1]
(if (= n 1)
r
(recur (dec n) (* n r)))))
然后有个call-fibs函数调用fib函数计算两个数的阶乘之和:
user=> (defn call-fibs [a b]
(+ (fib a) (fib b)))
#'user/call-fibs
user=> (call-fibs 3 3)
12
现在我们有这么个需求,希望使用memoize来加速fib函数,我们不希望修改fib函数,因为这个函数可能其他地方用到,其他地方不需要加速,而我们希望仅仅在调用call-fibs的时候加速下fib的执行,这时候可以利用binding来动态绑定新的fib函数:
user=> (binding [fib (memoize fib)]
(call-fibs 9 10))
3991680
在没有改变fib定义的情况下,只是执行call-fibs的时候动态改变了原fib函数的行为,这不是跟AOP很相似吗?
但是这样做已经让call-fibs这个函数 不再是一个“纯函数”,所谓“纯函数”是指一个函数对于相同的参数输入永远返回相同的结果,但是由于binding可以动态隐式地改变函数的行为,导致相同的参数可能返回不同的结果,例如这里可以将fib绑定为一个返回平方值的函数,那么call-fibs对于相同的参数输入产生的值就改变了,取决于当前的context,这其实是引入了副作用。因此对于binding的这种使用方式要相当慎重。这其实有点类似Ruby中的open class做monkey patch,你可以随时随地地改变对象的行为,但是你要承担相应的后果。
3、binding和let的实现上的区别:
前面已经提到,let其实是词法作用域的对变量的“遮蔽”,它并非重新绑定变量值,而binding则是在变量的root binding之外在线程的ThreadLocal内存储了一个绑定值, 变量值的查找顺序是先查看ThreadLocal有没有值,有的话优先返回,没有则返回root binding。下面将从Clojure源码角度分析。
变量在clojure是存储为Var对象,它的内部包括:
//这是变量的ThreadLocal值存储的地方
static ThreadLocal<Frame> dvals = new ThreadLocal<Frame>(){
protected Frame initialValue(){
return new Frame();
}
};
volatile Object root; //这是root binding
public final Symbol sym; //变量的符号
public final Namespace ns; //变量的namespace
通过def定义一个变量,相当于生成一个Var对象,并将root设置为初始值。
先看下let表达式生成的字节码:
(let [foo 3] foo)
字节码:
public class user$eval__4349 extends clojure/lang/AFunction {
// compiled from: NO_SOURCE_FILE
// debug info: SMAP
eval__4349.java
Clojure
*S Clojure
*F
+ 1 NO_SOURCE_FILE
NO_SOURCE_PATH
*L
0#1,1:0
*E
// access flags 25
public final static Ljava/lang/Object; const__0
// access flags 9
public static <clinit>()V
L0
LINENUMBER 2 L0
ICONST_3
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTSTATIC user$eval__4349.const__0 : Ljava/lang/Object;
RETURN
MAXSTACK = 0
MAXLOCALS = 0
// access flags 1
public <init>()V
L0
LINENUMBER 2 L0
L1
ALOAD 0
INVOKESPECIAL clojure/lang/AFunction.<init> ()V
L2
RETURN
MAXSTACK = 0
MAXLOCALS = 0
// access flags 1
public invoke()Ljava/lang/Object; throws java/lang/Exception
L0
LINENUMBER 2 L0
GETSTATIC user$eval__4349.const__0 : Ljava/lang/Object;
ASTORE 1
L1
ALOAD 1
L2
LOCALVARIABLE foo Ljava/lang/Object; L1 L2 1
L3
LOCALVARIABLE this Ljava/lang/Object; L0 L3 0
ARETURN
MAXSTACK = 0
MAXLOCALS = 0
}
可以看到foo并没有形成一个Var对象,而仅仅是将3存储为静态变量,最后返回foo的时候,也只是取出静态变量,直接返回,没有涉及到变量的查找。let在编译的时候,将binding作为编译的context静态地编译body的字节码,body中用到的foo编译的时候就确定了,没有任何动态性可言。
再看同样的表达式替换成binding宏,因为binding只能重新绑定已有的变量,所以需要先定义foo:
user=> (def foo 100)
#'user/foo
user=> (binding [foo 3] foo)
binding是一个宏,展开之后等价于:
(let []
(push-thread-bindings (hash-map (var foo) 3))
(try
foo
(finally
(pop-thread-bindings))))
首先是将binding的绑定列表转化为一个hash-map,其中key为变量foo,值为3。函数push-thread-bindings:
(defn push-thread-bindings
[bindings]
(clojure.lang.Var/pushThreadBindings bindings))
其实是调用Var.pushThreadBindings这个静态方法:
public static void pushThreadBindings(Associative bindings){
Frame f = dvals.get();
Associative bmap = f.bindings;
for(ISeq bs = bindings.seq(); bs != null; bs = bs.next())
{
IMapEntry e = (IMapEntry) bs.first();
Var v = (Var) e.key();
v.validate(v.getValidator(), e.val());
v.count.incrementAndGet();
bmap = bmap.assoc(v, new Box(e.val()));
}
dvals.set(new Frame(bindings, bmap, f));
}
pushThreadBindings是将绑定关系放入一个 新的frame(新的context),并存入ThreadLocal变量dvals。 pop-thread-bindings函数相反,弹出一个Frame,它实际调用的是Var.popThreadBindings静态方法:
public static void popThreadBindings(){
Frame f = dvals.get();
if(f.prev == null)
throw new IllegalStateException("Pop without matching push");
for(ISeq bs = RT.keys(f.frameBindings); bs != null; bs = bs.next())
{
Var v = (Var) bs.first();
v.count.decrementAndGet();
}
dvals.set(f.prev);
}
在执行宏的body表达式,也就是取foo值的时候,实际调用的是Var.deref静态方法取变量值:
final public Object deref(){
//先从ThreadLocal找
Box b = getThreadBinding();
if(b != null)
return b.val;
//如果有定义初始值,返回root binding
if(hasRoot())
return root;
throw new IllegalStateException(String.format("Var %s/%s is unbound.", ns, sym));
}
看到是先尝试从ThreadLocal找:
final Box getThreadBinding(){
if(count.get() > 0)
{
IMapEntry e = dvals.get().bindings.entryAt(this);
if(e != null)
return (Box) e.val();
}
return null;
}
找不到,如果有初始值就返回初始的root binding,否则抛出异常:Var user/foo is unbound.
binding表达式最后生成的字节码,做的就是上面描述的这些函数调用,有兴趣地可以自行分析。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
四、 Agent和Actor
除了用于协调同步的Ref,独立同步的Ref,还有一类非常常见的需求:你可能希望状态的更新是异步,你通常不关心更新的结果,这时候你可以考虑下使用Agent。
1、创建agent:
user=> (def counter (agent 0))
#'user/counter
user=> counter
#<Agent@9444d1: 0>
通过agent函数你就可以创建一个agent,指向一个不可变的初始状态。
2、取agent的值,这跟Ref和Atom没啥两样,都是通过deref或者@宏:
user=> @counter
0
user=> (deref counter)
0
3、更新agent,通过send或者send-off函数给agent发送任务去更新agent:
user=> (send counter inc)
#<Agent@9444d1: 0>
send返回agent对象,内部的值仍然是0,而非inc递增之后的1,这是因为send是异步发送,更新是在另一个线程执行,两个线程(REPL主线程和更新任务的线程)的执行顺序没有同步,显示什么取决于两者谁更快。更新肯定是发生了,查看counter的值:
user=> @counter
1
果然更新到了1了。send的方法签名:
(send a f & args)
其中f是更新的函数,它的定义如下:
(f state-of-agent & args)
也就是它会在第一个参数接收当前agent的状态,而args是send附带的参数。
还有个方法,send-off,它的作用于send类似:
user=> (send-off counter inc)
#<Agent@9444d1: 1>
user=> @counter
2
send和send-off的区别在于,send是将任务交给一个 固定大小的线程池执行
final public static ExecutorService pooledExecutor =
Executors.newFixedThreadPool(2 + Runtime.getRuntime().availableProcessors());
默认线程池大小是 CPU核数加上2。因此 send执行的任务最好不要有阻塞的操作。而send-off则使用没有大小限制(取决于内存)的线程池:
final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();
因此, send-off比较适合任务有阻塞的操作,如IO读写之类。请注意, 所有的agent是共用这些线程池,这从这些线程池的定义看出来,都是静态变量。
4、异步转同步,刚才提到send和send-off都是异步将任务提交给线程池去处理,如果你希望同步等待结果返回,那么可以使用await函数:
(do (send counter inc) (await counter) (println @counter))
send一个任务之后,调用await等待agent所有派发的更新任务结束,然后打印agent的值。await是阻塞当前线程,直到至今为止所有任务派发执行完毕才返回。await没有超时,会一直等待直到条件满足,await-for则可以接受等待的超时时间,如果超过指定时间没有返回,则返回nil,否则返回结果。
(do (send counter inc) (await-for 100 counter) (println @counter))
await-for接受的单位是毫秒。
5、错误处理
agent也可以跟Ref和Atom一样设置validator,用于约束验证。由于agent的更新是异步的,你不知道更新的时候agent是否发生异常,只有等到你去取值或者更新的时候才能发现:
user=> (def counter (agent 0 :validator number?))
#'user/counter
user=> (send counter (fn[_] "foo"))
#<clojure.lang.Agent@4de8ce62: 0>
强制要求counter的值是数值类型,第二个表达式我们给counter发送了一个更新任务,想将状态更新为字符串"foo",由于是异步更新,返回的结果可能没有显示异常,当你取值的时候,问题出现了:
user=> @counter
java.lang.Exception: Agent has errors (NO_SOURCE_FILE:0)
告诉你agent处于不正常的状态,如果你想获取详细信息,可以通过agent-errors函数:
user=> (.printStackTrace (agent-errors counter))
java.lang.IllegalArgumentException: No matching field found: printStackTrace for class clojure.lang.PersistentList (NO_SOURCE_FILE:0)
你可以恢复agent到前一个正常的状态,通过clear-agent-errors函数:
user=> (clear-agent-errors counter)
nil
user=> @counter
0
6、加入事务
agent跟atom不一样,agent可以加入事务,在事务里调用send发送一个任务, 当事务成功的时候该任务将只会被发送一次,最多最少都一次。利用这个特性,我们可以实现在事务操作的时候写文件,达到ACID中的D——持久性的目的:
(def backup-agent (agent "output/messages-backup.clj" ))
(def messages (ref []))
(use '[clojure.contrib.duck-streams :only (spit)])
(defn add-message-with-backup [msg]
(dosync
(let [snapshot (commute messages conj msg)]
(send-off backup-agent (fn [filename]
(spit filename snapshot)
filename))
snapshot)))
定义了一个backup-agent用于保存消息, add-message-with-backup函数首先将状态保存到messages,这是个普通的Ref,然后调用send-off给backup-agent一个任务:
(fn [filename]
(spit filename snapshot)
filename)
这个任务是一个匿名函数,它利用spit打开文件,写入当前的快照,并且关闭文件,文件名来自backup-agent的状态值。注意到,我们是用send-off,send-off利用cache线程池,哪怕阻塞也没关系。
利用事务加上一个backup-agent可以实现类似数据库的ACID,但是还是不同的,主要区别在于 backup-agent的更新是异步,并不保证一定写入文件,因此持久性也没办法得到保证。
7、关闭线程池:
前面提到agent的更新都是交给线程池去处理,在系统关闭的时候你需要关闭这两个线程吃,通过shutdown-agents方法,你再添加任务将被拒绝:
user=> (shutdown-agents)
nil
user=> (send counter inc)
java.util.concurrent.RejectedExecutionException (NO_SOURCE_FILE:0)
user=> (def counter (agent 0))
#'user/counter
user=> (send counter inc)
java.util.concurrent.RejectedExecutionException (NO_SOURCE_FILE:0)
哪怕我重新创建了counter,提交任务仍然被拒绝,进一步证明这些 线程池是全局共享的。
8、原理浅析
前文其实已经将agent的实现原理大体都说了,agent本身只是个普通的java对象,它的内部维持一个状态和一个队列:
volatile Object state;
AtomicReference<IPersistentStack> q = new AtomicReference(PersistentQueue.EMPTY);
任务提交的时候,是封装成Action对象,添加到此队列
public Object dispatch(IFn fn, ISeq args, boolean solo) {
if (errors != null) {
throw new RuntimeException("Agent has errors", (Exception) RT.first(errors));
}
//封装成action对象
Action action = new Action(this, fn, args, solo);
dispatchAction(action);
return this;
}
static void dispatchAction(Action action) {
LockingTransaction trans = LockingTransaction.getRunning();
// 有事务,加入事务
if (trans != null)
trans.enqueue(action);
else if (nested.get() != null) {
nested.set(nested.get().cons(action));
}
else {
// 入队
action.agent.enqueue(action);
}
}
send和send-off都是调用Agent的dispatch方法,只是两者的参数不一样,dispatch的第二个参数 solo决定了是使用哪个线程池处理action:
(defn send
[#^clojure.lang.Agent a f & args]
(. a (dispatch f args false)))
(defn send-off
[#^clojure.lang.Agent a f & args]
(. a (dispatch f args true)))
send-off将solo设置为true,当为true的时候使用cache线程池:
final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();
final static ThreadLocal<IPersistentVector> nested = new ThreadLocal<IPersistentVector>();
void execute() {
if (solo)
soloExecutor.execute(this);
else
pooledExecutor.execute(this);
}
执行的时候调用更新函数并设置新的状态:
try {
Object oldval = action.agent.state;
Object newval = action.fn.applyTo(RT.cons(action.agent.state, action.args));
action.agent.setState(newval);
action.agent.notifyWatches(oldval, newval);
}
catch (Throwable e) {
// todo report/callback
action.agent.errors = RT.cons(e, action.agent.errors);
hadError = true;
}
9、跟actor的比较:
Agent跟Actor有一个显著的不同,agent的action来自于别人发送的任务附带的更新函数,而actor的action则是自身逻辑的一部分。因此,如果想用agent实现actor模型还是相当困难的,下面是我的一个尝试:
(ns actor)
(defn receive [& args]
(apply hash-map args))
(defn self [] *agent*)
(defn spawn [recv-map]
(agent recv-map))
(defn ! [actor msg]
(send actor #(apply (get %1 %2) (vector %2)) msg))
;;启动一个actor
(def actor (spawn
(receive :hello #(println "receive "%))))
;;发送消息 hello
(! actor :hello)
利用spawn启动一个actor,其实本质上是一个agent,而发送通过感叹号!,给agent发送一个更新任务,它从recv-map中查找消息对应的处理函数并将消息作为参数来执行。难点在于消息匹配,匹配这种简单类型的消息没有问题,但是如果匹配用到变量,暂时没有想到好的思路实现,例如实现两个actor的ping/pong。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
三、Atom和缓存
Ref适用的场景是系统中存在多个相互关联的状态,他们需要一起更新,因此需要通过dosync做事务包装。但是如果你有一个状态变量,不需要跟其他状态变量协作,这时候应该使用Atom了。可以将一个Atom和一个Ref一起在一个事务里更新吗?这没办法做到,如果你需要相互协作,你只能使用Ref。Atom适用的场景是状态是独立,没有依赖,它避免了与其他Ref交互的开销,因此性能会更好,特别是对于读来说。
1、定义Atom,采用atom函数,赋予一个初始状态:
(def mem (atom {}))
这里将mem的初始状态定义为一个map。
2、deref和@:可以用deref函数,也可以简单地用宏@,这跟Ref一样,取atom的值:
@mem => {}
(deref mem) => {}
3、reset!:重新设置atom的值,不关心当前值是什么:
(reset! mem {:a 1})
查看mem:
user=> @mem
{:a 1}
已经更新到新的map了。
4、swap!:如果你的更新需要依赖当前的状态值,或者只想更新状态的某个部分,那么就需要使用swap!(类似alter):
(swap! an-atom f & args)
swap! 将函数f作用于当前状态值和额外的参数args之上,形成新的状态值,例如我们给mem加上一个keyword:
user=> (swap! mem assoc :b 2)
{:b 2, :a 1}
看到,:b 2被加入了当前的map。
5、compare and set:
类似原子变量AtomicInteger之类,atom也可以做compare and set的操作:
(compare-and-set! atom oldValue newValue)
当且仅当atom的当前状态值等于oldValue的时候,将状态值更新为newValue,并返回一个布尔值表示成功或者失败:
user=> (def c (atom 1))
#'user/c
user=> (compare-and-set! c 2 3)
false
user=> (compare-and-set! c 1 3)
true
user=> @c
3
6、缓存和atom:
(1)atom非常适合实现缓存,缓存通常不会跟其他系统状态形成依赖,并且缓存对读的速度要求更高。上面例子中用到的mem其实就是个简单的缓存例子,我们来实现一个putm和getm函数:
;;创建缓存
(defn make-cache [] (atom {}))
;;放入缓存
(defn putm [cache key value] (swap! cache assoc key value))
;;取出
(defn getm [cache key] (key @cache))
这里key要求是keyword,keyword是类似:a这样的字符序列,你熟悉ruby的话,可以暂时理解成symbol。使用这些API:
user=> (def cache (make-cache))
#'user/cache
user=> (putm cache :a 1)
{:a 1}
user=> (getm cache :a)
1
user=> (putm cache :b 2)
{:b 2, :a 1}
user=> (getm cache :b)
2
(2) memoize函数作用于函数f,产生一个新函数,新函数内部保存了一个缓存,缓存从参数到结果的映射。第一次调用的时候,发现缓存没有,就会调用f去计算实际的结果,并放入内部的缓存;下次调用同样的参数的时候,就直接从缓存中取,而不用再次调用f,从而达到提升计算效率的目的。
memoize的实现就是基于atom,查看源码:
(defn memoize
[f]
(let [mem (atom {})]
(fn [& args]
(if-let [e (find @mem args)]
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret)))))
内部的缓存名为mem,memoize返回的是一个匿名函数,它接收原有的f函数的参数,if-let判断绑定的变量e是否存在,变量e是通过find从缓存中查询args得到的项,如果存在的话,调用val得到真正的结果并返回;如果不存在,那么使用apply函数将f作用于参数列表之上,计算出结果,并利用swap!将结果加入mem缓存,返回计算结果。
7、性能测试:
使用atom实现一个计数器,和使用java.util.concurrent.AtomicInteger做计数器,做一个性能比较,各启动100个线程,每个线程执行100万次原子递增,计算各自的耗时,测试程序如下,代码有注释,不再罗嗦:
(ns atom-perf)
(import 'java.util.concurrent.atomic.AtomicInteger)
(import 'java.util.concurrent.CountDownLatch)
(def a (AtomicInteger. 0))
(def b (atom 0))
;;为了性能,给java加入type hint
(defn java-inc [#^AtomicInteger counter] (.incrementAndGet counter))
(defn countdown-latch [#^CountDownLatch latch] (.countDown latch))
;;单线程执行缓存次数
(def max_count 1000000)
;;线程数
(def thread_count 100)
(defn benchmark [fun]
(let [ latch (CountDownLatch. thread_count) ;;关卡锁
start (System/currentTimeMillis) ] ;;启动时间
(dotimes [_ thread_count] (.start (Thread. #(do (dotimes [_ max_count] (fun)) (countdown-latch latch)))))
(.await latch)
(- (System/currentTimeMillis) start)))
(println "atom:" (benchmark #(swap! b inc)))
(println "AtomicInteger:" (benchmark #(java-inc a)))
(println (.get a))
(println @b)
默认clojure调用java都是通过反射,加入type hint之后编译的字节码就跟java编译器的一致,为了比较公平,定义了java-inc用于调用AtomicInteger.incrementAndGet方法,定义countdown-latch用于调用CountDownLatch.countDown方法,两者都为参数添加了type hint。如果不采用type hint,AtomicInteger反射调用的效率是非常低的。
测试下来,在我的ubuntu上,AtomicInteger还是占优,基本上比atom的实现快上一倍:
atom: 9002
AtomicInteger: 4185
100000000
100000000
按 照我的理解,这是由于AtomicInteger调用的是native的方法,基于硬件原语做cas,而atom则是用户空间内的clojure自己做的CAS,两者的性能有差距不出意料之外。
看了源码,Atom是基于java.util.concurrent.atomic.AtomicReference实现的,调用的方法是
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
而AtomicInteger调用的方法是:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
两者的效率差距有这么大吗?暂时存疑。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
在 介绍Ref的上一篇blog提到,基于snapshot做隔离的MVCC实现来说,有个现象,叫 写偏序——Write Skew。根本的原因是由于每个事务在更新过程中无法看到其他事务的更改的结果,导致各个事务提交之后的最终结果违反了一致性。为了理解这个现象,最好的办法是在代码中复现这个现象。考虑下列这个场景:
屁民Peter有两个账户account1和account2,简称为A1和A2,这两个账户各有100块钱,一个显然的约束就是这两个账户的余额之和必须大于或者等于零,银行肯定不能让你赚了去,你也怕成为下个许霆。现在,假设有两个事务T1和T2,T1从A1提取200块钱,T2则从A2提取200块钱。如果这两个事务按照先后顺序进行,后面执行的事务判断A1+A2-200>=0约束的时候发现失败,那么就不会执行,保证了一致性和隔离性。但是基于多版本并发控制的Clojure,这两个事务完全可能并发地执行,因为他们都是基于一个当前账户的快照做更新的, 并且在更新过程中无法看到对方的修改结果,T1执行的时候判断A1+A2-200>=0约束成立,从A1扣除了200块;同样,T2查看当前快照也满足约束A1+A2-200>=0,从A2扣除了200块,问题来了,最终的结果是A1和A2都成-100块了,身为屁民的你竟然从银行多拿了200块,你等着无期吧。
现在,我们就来模拟这个现象,定义两个账户:
;;两个账户,约束是两个账户的余额之和必须>=0
(def account1 (ref 100))
(def account2 (ref 100))
定义一个取钱方法:
;;定义扣除函数
(defn deduct [account n other]
(dosync
(if (>= (+ (- @account n) @other) 0)
(alter account - n))))
其中account是将要扣钱的帐号,other是peter的另一个帐号,在执行扣除前要满足约束@account-n+@other>=0
接下来就是搞测试了,各启动N个线程尝试从A1和A2扣钱,为了尽快模拟出问题,使得并发程度高一些,我们将线程设置大一些,并且使用java.util.concurrent.CyclicBarrier做关卡,测试代码如下:
;;设定关卡
(def barrier (java.util.concurrent.CyclicBarrier. 6001))
;;各启动3000个线程尝试去从账户1和账户2扣除200
(dotimes [_ 3000] (.start (Thread. #(do (.await barrier) (deduct account1 200 account2) (.await barrier)))))
(dotimes [_ 3000] (.start (Thread. #(do (.await barrier) (deduct account2 200 account1) (.await barrier)))))
(.await barrier)
(.await barrier)
;;打印最终结果
(println @account1)
(println @account2)
线程里干了三件事情:首先调用barrier.await尝试突破关卡,所有线程启动后冲破关卡,进入扣钱环节deduct,最后再调用barrier.await用于等待所有线程结束。在所有线程结束后,打印当前账户的余额。
这段代码在我的机器上每执行10次左右都至少有一次打印:
-100
-100
这表示A1和A2的账户都欠下了100块钱,完全违反了约束条件,法庭的传票在召唤peter。
那么怎么防止write skew现象呢?如果我们能在事务过程中保护某些Ref不被其他事务修改,那么就可以保证当前的snapshot的一致性,最终保证结果的一致性。通过ensure函数即可保护Ref,稍微修改下deduct函数:
(defn deduct [account n other]
(dosync (ensure account) (ensure other)
(if (>= (+ (- @account n) @other) 0)
(alter account - n))))
在执行事务更新前,先通过ensure保护下account和other账户不被其他事务修改。你可以再多次运行看看,会不会再次打印非法结果。
上篇blog最后也提到了一个士兵巡逻的例子来介绍write skew,我也写了段代码来模拟那个例子,有兴趣可以跑跑,非法结果是三个军营的士兵之和小于100(两个军营最后只剩下25个人)。
;1号军营
(def g1 (ref 45))
;2号军营
(def g2 (ref 45))
;3号军营
(def g3 (ref 45))
;从1号军营抽调士兵
(defn dispatch-patrol-g1 [n]
(dosync
(if (> (+ (- @g1 n) @g2 @g3) 100)
(alter g1 - 20)
))
)
;从2号军营抽调士兵
(defn dispatch-patrol-g2 [n]
(dosync
(if (> (+ @g1 (- @g2 n) @g3) 100)
(alter g2 - 20)
))
)
;;设定关卡
(def barrier (java.util.concurrent.CyclicBarrier. 4001))
;;各启动2000个线程尝试去从1号和2号军营抽调20个士兵
(dotimes [_ 2000] (.start (Thread. #(do (.await barrier) (dispatch-patrol-g1 20) (.await barrier)))))
(dotimes [_ 2000] (.start (Thread. #(do (.await barrier) (dispatch-patrol-g2 20) (.await barrier)))))
;(dotimes [_ 10] (.start (Thread. #(do (.await barrier) (dispatch-patrol-g3 20) (.await barrier)))))
(.await barrier)
(.await barrier)
;;打印最终结果
(println @g1)
(println @g2)
(println @g3)
我估计我不写这样的标题,吸引不了人气。问题的起因是Javaeye的一个帖子《 淘宝面试题:如何充分利用多核CPU,计算很大的 List中所有整数的和》,看见为了这么个问题写长长的Java代码,让我十分蛋疼。为了表示蛋定,我想介绍下用Clojure解决这个问题的方法。
题目很明确了,要你充分多核,这年头不“多核”不好意思出去跟人打招呼,为了多核,你要将list拆分下,每个子list并发去计算和,然后综合这些结果求出最终的和,您没搞错,这不是传说中人见人爱的MapReduce吗?你没听过?不好意思,你out了。
咱们先别着急并发,先搞个不并发的版本试试看,第一步,切分list,实在不好意思,clojure有现成的clojure.core/partition函数:
user=> (partition 3 3 [0] '(1 2 3 4 5 6 7 8 9 10))
((1 2 3) (4 5 6) (7 8 9) (10 0))
牛刀小试,将(1 2 3 4 5 6 7 8 9 10)切分成了3个子串,还有个可怜的犀利哥——10号同学没办法归入任意一个组,只好让他跟虚无的0为伴了。 partition的第三个参数指定一个凑伙的集合,当无法完全切分的时候,拿这个集合里的元素凑合。但是我们不能随便凑合啊,随便凑合加起来结果就不对了,用0就没问题了。
切分完了,计算子集的和,那不要太简单,该reduce同学上场,请大家欢呼、扔鸡蛋,千万别客气:
user=> (reduce + 0 '(1 2 3))
6
自然要reduce,总要指定规则怎么reduce,我们这里很简单,就是个加法运算,再给初始值0就好咯,reduce万岁。
慢着,有个问题, partition返回的是集合的集合((1 2 3) (4 5 6) (7 8 9) (10 0)),而上面的reduce却要作用在子集里,怎么办?无敌的map大神出场了,map的作用是将某个函数作用于集合上,并且返回作用后的集合结果,这里要干的事情就是将上面的reduce作用在partition返回的集合的集合上面:
user=> (map #(reduce + 0 % )(partition 3 3 [0] '(1 2 3 4 5 6 7 8 9 10)))
(6 15 24 10)
返回的是4个子集各自的和,答案肯定没错,最后一个结果不正是唯一的元素10吗?这里可能比较费解的是#(reduce + 0 %),这其实定义了一个匿名函数,它接收一个参数,这个参数用百分号%默认指代,因为是将map作用于集合的集合,因此这里的%其实就是各个子集。
map返回的是个集合,又要求集合的总和,是不是又该reduce上场了?不好意思,map同学,这次演出你就一跑龙套的:
user=> (reduce + 0 (map #(reduce + 0 % )(partition 3 3 [0] '(1 2 3 4 5 6 7 8 9 10))))
55
伟大的55出来了,它不是一个人在战斗,这一刻LISP、Scheme、Erlang、Scala、Clojure、JavaScript灵魂附体,它继承了FP的光荣传统,干净漂亮地解决了这个问题。
综合上面所述,我们给出一个非多核版本的解答:
(defn mysum [coll n]
(let [sub-colls (partition n n [0] coll)
result-coll (map #(reduce + 0 %) sub-colls) ]
(reduce + 0 result-coll)))
我们是使用了let语句绑定变量,纯粹是为了更好看懂一些。sub-colls绑定到partition返回的集合的集合,result-coll就是各个子集的结果组成的集合,#(reduce + 0 %)是个匿名函数,其中%指向匿名函数的第一个参数,也就是每个子集。最终,利用reduce将result-coll的结果综合在一起。
“我们要多核,我们要多核,我们不要西太平洋大学的野鸡MapReduce"。
台下别激动,神奇的“多核”马上出场,我要改动的代码只是那么一点点,用pmap替代map
(defn psum [coll n]
(let [sub-colls (partition n n [0] coll)
result-coll (pmap #(reduce + 0 %) sub-colls) ]
(reduce + 0 result-coll)))
完了吗?真完了,你要改动的只有这么一点点,就可以让切分出来的子集并发地计算了。(感谢网友@clojans的提醒)。
以下是原文:
首先是匿名函数改造一点点:
我干嘛了,我就加了个future,给你个未来。严肃点地说,future将启动一个单独的线程去reduce子集。现在result-coll里不再是直接的结果,而是各个子集的Future对象,为了得到真正的和,你需要等待线程结束并取得结果,因此最后的reduce也要小小地改动下:
(reduce #(+ %1 @%2) 0 result-coll))
reduce不再是简单地用加号了,替代的则是一个两个参数的匿名函数,第二个参数%2是Future对象,我们通过@操作符等待Future返回结果,并跟第一个参数%1(初始为0)作加法运算。
最终的多核版本:
(defn mysum2 [coll n]
(let [sub-colls (partition n n [0] coll)
result-coll (map #(future (reduce + 0 %)) sub-colls)]
(reduce #(+ %1 @%2) 0 result-coll)))
这个多核版本跟非多核版本区别大吗?不大吗?大吗?不大吗?……
可以看到,Clojure可以多么容易地在并发与非并发之间徘徊,习惯脚踏N只船了。
详解clojure递归(上)
详解clojure递归(下)
递归可以说是LISP的灵魂之一,通过递归可以简洁地描述数学公式、函数调用,Clojure是LISP的方言,同样需要递归来扮演重要作用。递归的价值在于可以让你的思维以what的形式思考,而无需考虑how,你写出来的代码就是数学公式,就是函数的描述,一切显得直观和透明。如果你不习惯递归,那只是因为命令式语言的思维根深蒂固,如x=x+1这样的表达式,从数学的角度来看完全不合法,但是在命令式语言里却是合法的赋值语句。
递归可以分为直接递归和间接递归,取决于函数是否直接或者间接地调用自身。如果函数的最后一个调用是递归调用,那么这样的递归调用称为尾递归,针对此类递归调用,编译器可以作所谓的尾递归优化(TCO),因为递归调用是最后一个,因此函数的局部变量等没有必要再保存,本次调用的结果可以完全作为参数传递给下一个递归调用,清空当前的栈并复用,那么就不需要为递归的函数调用保存一长串的栈,因此不会有栈溢出的问题。在Erlang、LISP这样的FP语言里,都支持TCO,无论是直接递归或者间接递归。
但是由于JVM自身的限制,Clojure和Scala一样,仅支持直接的尾递归优化,将尾递归调用优化成循环语句。例如一个求阶乘的例子:
;;第一个版本的阶乘函数
(defn fac [n]
(if (= 1 n)
1
(* n (fac (dec n)))))
第一个版本的阶乘并非尾递归,这是因为最后一个表达式的调用是一个乘法运算,而非(fac (dec n)),因此这个版本的阶乘在计算大数的时候会导致栈溢出:
user=> (fac 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)
将第一个版本改进一下,为了让最后一个调用是递归调用,那么我们需要将结果作为参数来传递,而不是倚靠栈来保存,并且为了维持接口一样,我们引入了一个内部函数fac0:
;;第二个版本,不是尾递归的“尾递归”
(defn fac [n]
(defn fac0 [c r]
(if (= 0 c)
r
(fac0 (dec c) (* c r))))
(fac0 n 1))
这个是第二个版本的阶乘,通过将结果提取成参数来传递,就将fac0函数的递归调用修改为尾递归的形式,这是个尾递归吗?这在Scala里,在LISP里,这都是尾递归,但是Clojure的TCO优化却是要求使用recur这个特殊形式,而不能直接用函数名作递归调用,因此我们这个第二版本在计算大数的时候仍然将栈溢出:
user=> (fac 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)
在Clojure里正确地TCO应该是什么样子的呢?其实只要用recur在最后调用那一下替代fac0即可,这就形成我们第三个版本的阶乘:
;;第三个版本,TCO起作用了
(defn fac [n]
(defn fac0 [c r]
(if (= 0 c)
r
(recur (dec c) (* c r))))
(fac0 n 1))
此时你再计算大数就没有问题了,计算(fac 10000)可以正常运行(结果太长,我就不贴出来了)。recur只能跟函数或者loop结合在一起使用,只有函数和loop会形成递归点。我们第三个版本就是利用函数fac0做了尾递归调用的优化。
loop跟let相似,只不过loop会在顶层形成一个递归点,以便recur重新绑定参数,使用loop改写阶乘函数,这时候就不需要定义内部函数了:
;;利用loop改写的第四个版本的阶乘函数
(defn fac [n]
(loop [n n r 1]
(if (= n 0)
r
(recur (dec n) (* n r)))))
loop初始的时候将n绑定为传入的参数n(由于作用域不同,同名没有问题),将r绑定为1,最后recur就可以将新的参数值绑定到loop的参数列表并递归调用。
Clojure的TCO是怎么做到的,具体可以看看我前两天写的 这篇博客,本质上是在编译的时候将最后的递归调用转化成一条goto语句跳转到开始的Label,也就是转变成了循环调用。
这个阶乘函数仍然有优化的空间,可以看到,每次计算其实都有部分是重复计算的,如计算(fac 5)也就是1*2*3*4*5,计算(fac 6)的1*2*3*4*5*6,如果能将前面的计算结果缓存下来,那么计算(fac 6)的时候将更快一些,这可以通过memoize函数来包装阶乘函数:
;;第五个版本的阶乘,缓存中间结果
(def fac (memoize fac))
第一次计算(fac 10000)花费的时间长一些,因为还没有缓存:
user=> (time (fac 10000))
"Elapsed time: 170.489622 msecs"
第二次计算快了非常多(其实没有计算,只是返回缓存结果):
user=> (time (fac 10000))
"Elapsed time: 0.058737 msecs"
可以看到,如果没有预先缓存,利用memoize包装的阶乘函数也是快不了。memoize的问题在于,计算(fac n)路径上的没有用到的值都不会缓存,它只缓存最终的结果,因此如果计算n前面的其他没有计算过的数字,仍然需要重新计算。那么怎么保存路径上的值呢?这可以将求阶乘转化成另一个等价问题来解决。
我们可以将所有的阶乘结果组织成一个无穷集合,求阶乘变成从 这个集合里取第n个元素,这是利用Clojure里集合是lazy的特性,集合里的元素如果没有使用到,那么就不会预先计算,而是等待要用到的时候才计算出来,定义一个阶乘结果的无穷集合,可以利用map将fac作用在整数集合上,map、reduce这样的高阶函数返回的是LazySeq:
(def fac-seq (map fac (iterate inc 0)))
(iterate inc 0)定义了正整数集合包括0,0的阶乘没有意义。这个集合的第0项其实是多余的。
查看fac-seq的类型,这是一个LazySeq:
user=> (class fac-seq)
clojure.lang.LazySeq
求n的阶乘,等价于从这个集合里取第n个元素:
user=> (nth fac-seq 10)
3628800
这个集合会比较耗内存,因为会缓存所有计算路径上的独立的值,哪怕他们暂时不会被用到。但是这种采用LazySeq的方式来定义阶乘函数的方式有个优点,那就是在定义fac-seq使用的f ac函数无需一定是符合TCO的函数,我们的第一个版本的阶乘函数稍微修改下也可以使用,并且不会栈溢出:
(defn fac [n]
(if (<= n 1)
1
(* n (fac (dec n)))))
(def fac (memoize fac))
(def fac-seq (map fac (iterate inc 0)))
(nth fac-seq 10000)
因为集合从0开始,因此只是修改了fac的if条件为n<=1的时候返回1。至于为什么这样就不会栈溢出,有兴趣的朋友可以自己思考下。
从这个例子也可以看出,一些无法TCO的递归调用可以转化为LazySeq来处理,这算是弥补JVM缺陷的一个办法。
Clojure
的并发(一) Ref和STM
Clojure
的并发(二)Write Skew分析
Clojure
的并发(三)Atom、缓存和性能
Clojure
的并发(四)Agent深入分析和Actor
Clojure
的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
Clojure处理并发的思路与众不同,采用的是所谓 STM的模型——软事务内存。你可以将STM想象成数据库,只不过是内存型的,它只支持事务的ACI,也就是原子性、一致性、隔离性,但是不包括持久性,因为状态的保存都在内存里。
Clojure的并发API分为四种模型:
1、管理协作式、同步修改可变状态的Ref
2、管理非协作式、同步修改可变状态的Atom
3、管理异步修改可变状态的Agent
4、管理Thread local变量的Var。
下面将对这四部分作更详细的介绍。
一、Ref和STM
1、ref:
通过ref函数创建一个可变的引用(reference),指向一个不可变的对象:
(ref x)
例子:创建一个歌曲集合:
(def song (ref #{}))
2、deref和@:
取引用的内容,解引用使用deref函数
(deref song)
也可以用reader宏@:
@song
3、ref-set和dosync:
改变引用指向的内容,使用ref-set函数
(ref-set ref new-value)
如,我们设置新的歌曲集合,加入一首歌:
(ref-set song #{"Dangerous"})
但是这样会报错:
java.lang.IllegalStateException: No transaction running (NO_SOURCE_FILE:0)
这是因为引用是可变的,对状态的更新需要进行保护,传统语言的话可能采用锁,Clojure是采用事务,将更新包装到事务里,这是通过dosync实现的:
(dosync (ref-set song #{"Dangerous"}))
dosync的参数接受多个表达式,这些表达式将被包装在一个事务里,事务支持ACI:
(1)Atomic,如果你在事务里更新多个Ref,那么这些更新对事务外部来说是一个独立的操作。
(2)Consistent,Ref的更新可以设置 validator,如果某个验证失败,整个事务将回滚。
(3)Isolated,运行中的事务无法看到其他事务部分完成的结果。
dosync更新多个Ref,假设我们还有个演唱者Ref,同时更新歌曲集合和演唱者集合:
(def singer (ref #{}))
(dosync (ref-set song #{"Dangerous"})
(ref-set singer #{"MJ"}) )
@song => #{"Dangerous"}
@singer => #{"MJ"}
4、alter:
完全更新整个引用的值还是比较少见,更常见的更新是根据当前状态更新,例如我们向歌曲集合添加一个歌曲,步骤大概是先查询集合内容,然后往集合里添加歌曲,然后更新整个集合:
(dosync (ref-set song (conj @song "heal the world")))
查询并更新的操作可以合成一步,这是通过alter函数:
(alter ref update-fn & args )
alter接收一个更新的函数,函数将在更新的时候调用,传入当前状态值并返回新的状态值,因此上面的例子可以改写为:
(dosync (alter song conj "heal the world"))
这里使用conj而非cons是因为conj接收的第一个参数是集合,也就是当前状态值,而cons要求第一个参数是将要加入的元素。
5、commute:
commute函数是alter的变形,commute顾名思义就是要求update-function是可交换的,它的顺序是可以任意排序。commute的允许的并发程度比alter更高一些,因此性能会更好。但是由于commute要求update-function是可交换的,并且会自动重排序,因此如果你的更新要求顺序性,那么commute是不能接受的,commute仅可用在对顺序性没有要求或者要求很低的场景:例如更新聊天窗口的聊天信息,由于网络延迟的因素和个人介入的因素,聊天信息可以认为是天然排序,因此使用commute还可以接受,更新乱序的可能性很低。
另一个例子就不能使用commute了,如实现一个计数器:
(def counter (ref 0))
实现一个next-counter函数获取计数器的下一个值,我们先使用commute实现:
(defn next-counter [] (dosync (commute counter inc)))
这个函数很简单,每次调用inc递增counter的值,接下来写个测试用例:启动50个线程并发去获取next counter:
(dotimes [_ 50] (.start (Thread. #(println (next-counter)))))
这段代码稍微解释下,dotimes是重复执行50次,每次启动new并启动一个Thread,这个Thread里干了两件事情:调用next-counter,打印调用结果,第一个版本的next-counter执行下,这是其中一次输出的截取:
23
23
23
23
23
23
23
23
23
23
23
23
28
23
21
23
23
23
23
25
28
可以看到有很多的重复数值,这是由于重排序导致事务结束后的值不同,但是你查看counter,确实是50:
@counter => 50
证明更新是没有问题的,问题出在commute的返回值上。
如果将next-counter修改为alter实现:
(defn next-counter [] (dosync (alter counter inc)))
此时再执行测试用例,可以发现打印结果完全正确了:
……
39
41
42
45
27
46
47
44
48
43
49
40
50
查看counter,也是正确更新到50了:
@counter => 50
最佳实践: 通常情况下,你应该优先使用alter,除非在遇到明显的性能瓶颈并且对顺序不是那么关心的时候,可以考虑用commute替换。
6、validator:
类似数据库,你也可以为Ref添加“约束”,在数据更新的时候需要通过validator函数的验证,如果验证不通过,整个事务将回滚。添加validator是通过ref函数传入metadata的map实现的,例如我们要求歌曲集合添加的歌曲名称不能为空:
(def validate-song
(partial every? #(not (nil? %))))
(def song (ref #{} :validator validate-song))
validate-song是一个验证函数,partial返回某个函数的半函数(固定了部分参数,部分参数没固定),你可以将partial理解成currying,虽然还是不同的。validate-song调用every?来验证集合内的所有元素都不是nil,其中#(not (nil? %))是一个匿名函数,%指向匿名函数的第一个参数,也就是集合的每个元素。ref指定了validator为validate-song,那么在每次更新song集合的时候都会将新的状态传入validator函数里验证一下,如果返回false,整个事务将回滚:
(dosync (alter song conj nil))
java.lang.IllegalStateException: Invalid reference state (NO_SOURCE_FILE:0)
更新失败,非法的reference状态,查看song果然还是空的:
@song => #{}
更新正常的值就没有问题:
(dosync (alter song conj "dangerous")) => #{"dangerous"}
7、ensure:
ensure函数是为了保护Ref不会被其他事务所修改,它的主要目的是为了防止所谓的“ 写偏序”( write skew)问题。写偏序问题的产生跟STM的实现有关,clojure的STM实现是基于 MVCC(Multiversion Concurrency Control)——多版本并发控制,对一个Ref保存多个版本的状态值,在更新的时候取得当前状态值的一个隔离的snapshot,更新是基于snapshot进行的。那么我们来看下写偏序是怎么产生,以一个比喻来描述:
想象有一个系统用于管理美国最神秘的军事禁区——51区的安全巡逻,你有3个营的士兵,每个营45个士兵,并且你 需要保证总体巡逻的士兵人数不能少于100个人。假设有一天,有两个指挥官都登录了这个管理系统,他们都想从某个军营里抽走20个士兵,假设指挥官A想从1号军营抽走,指挥官B想要从2号军营抽走士兵,他们同时执行下列操作:
Admin 1: if ((G1 - 20) + G2 + G3) > 100 then dispatchPatrol
Admin 2: if (G1 + (G2 - 20) + G3) > 100 then dispatchPatrol
我们刚才提到,Clojure的更新是基于隔离的snapshot,一个事务的更改无法看到另一个事务更改了部分的结果,因此这两个操作都因为满足(45-20)+45+45=115的约束而得到执行,导致实际抽调走了40个士兵,只剩下95个士兵,低于设定的安全标准100人,这就是写偏序现象。
写偏序的解决就很简单,在执行抽调前加入ensure即可保护ref不被其他事务所修改。ensure比(ref-set ref @ref)允许的并发程度更高一些。
Ref和STM的介绍暂时到这里,原理和源码的解析要留待下一篇文章了。
解释器求值的顺序可以分为应用序和正则序,应用序是先求值参数,再执行表达式;正则序则是先将表达式按照实际参数展开,然后再执行。具体可以看看过去写的 这篇文章。
Clojure的求值可以肯定是应用序的,如执行
(defn mytest [a b]
(if (= a 0)
a
b))
(mytest 0 1/0)
尽管在(mytest 0 1/0)中a绑定为0,如果求值器是完全展开再求值,那应该正常执行并返回a,也就是1;但是因为clojure是应用序,因此参数b的1/0会先计算,这显然会报错。
clojure的dosync用于将一些表达式包装成事务,Ref的更新操作没有包装在事务里,会抛出异常
;;定义mutable的Ref
(def song (ref #{}))
;;添加一首歌
(alter song conj "dangerous")
alter用于向Ref查询并添加元素,用conj将"dangerous"这首歌加入集合,但是alter要求执行在一个事务里,因此上面的代码会报错
java.lang.IllegalStateException: No transaction running (NO_SOURCE_FILE:0)
如果你用dosync包装就没有问题
user=> (dosync (alter song conj "dangerous"))
#{"dangerous"}
返回更新后的结果集合。这个跟我们要谈的正则序和应用序有什么关系呢?可能你看出来了,如果说clojure是应用序,那么在表达式 (dosync (alter song conj "dangerous"))中,alter也应该先执行,应当照样报" No transaction running"的错误才对,为何却没有呢?难道dosync是按照正则序执行?
查看dosync的文档
user=> (doc dosync)
-------------------------
clojure.core/dosync
([& exprs])
Macro
Runs the exprs (in an implicit do) in a transaction that encompasses
exprs and any nested calls. Starts a transaction if none is already
running on this thread. Any uncaught exception will abort the
transaction and flow out of dosync. The exprs may be run more than
once, but any effects on Refs will be atomic.
这是一个宏,他的作用是将表达式包装在一个事务里,如果当前线程没有事务,那么就启动一个。
查看源码:
(defmacro dosync
"Runs the exprs (in an implicit do) in a transaction that encompasses
exprs and any nested calls. Starts a transaction if none is already
running on this thread. Any uncaught exception will abort the
transaction and flow out of dosync. The exprs may be run more than
once, but any effects on Refs will be atomic."
[& exprs]
`(sync nil ~@exprs))
本质上dosync是调用了sync这个宏,sync干了些什么?
(defmacro sync
"transaction-flags => TBD, pass nil for now
Runs the exprs (in an implicit do) in a transaction that encompasses
exprs and any nested calls. Starts a transaction if none is already
running on this thread. Any uncaught exception will abort the
transaction and flow out of sync. The exprs may be run more than
once, but any effects on Refs will be atomic."
[flags-ignored-for-now & body]
`(. clojure.lang.LockingTransaction
(runInTransaction (fn [] ~@body))))
找到了,原来是调用了 clojure.lang.LockingTransaction.runInTransaction这个静态方法,并且将exps包装成一个匿名函数
fn [] ~@body
因此,dosync并非正则序, dosync是个宏,(dosync (alter song conj "dangerous"))展开之后,其实是
(sync nil (fun [] (alter song conj "dangerous")))
这就解答了为什么 (dosync (alter song conj "dangerous"))可以正常运行的疑问。宏的使用,首先是展开,然后才是按照应用序的顺序求值。
这题目起的哗众取宠,其实只是想介绍下怎么查看Clojure动态生成的字节码,这对分析Clojure的内部实现很重要。
第一步,下载最新的 Clojure 1.1.0源码并解压,并导入到你喜欢的IDE。
其次,下载 asm 3.0的源码并解压。
第三,删除Clojure 1.1.0源码中的clojure.asm包。clojure并不是引用asm的jar包,而是将asm的源码合并到clojure中,并且删除一些只会在调试阶段用到的package和class,保留使用asm的最小源码集合,这可能是处于防止asm不同版本的jar包冲突以及缩小clojure大小的考虑。
第四,将asm 3.0源码拷入clojure的源码中,并将包org.objectweb.asm包括子包整体重名名为clojure.asm。
第五步,修改Clojure源码,加入TraceClassVisitor的适配器,用于跟踪字节码生成,这需要修改clojure.lang.Compiler类中的两个compile方法,找到类似
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = cw;
这样的代码,将cv修改为TraceClassVisitor:
ClassVisitor cv = new TraceClassVisitor(new CheckClassAdapter(cw), new PrintWriter(System.out));
TraceClassVisitor的第二个参数指定将跟踪到的字节码输出到哪里,这里简单地输出到标准输出方便查看。
第六步,接下来可以尝试下我们修改过的clojure怎么动态生成字节码,启动REPL,
java clojure.main
启动阶段就会输出一些字节码信息,主要预先加载的一些标准库函数,如clojure.core中的函数等,REPL启动完毕,随便输入一个表达式都将看到生成的字节码
user=> (+ 1 2)
输出类似
compile 1
// class version 49.0 (49)
// access flags 33
public class user$eval__4346 extends clojure/lang/AFunction {
// compiled from: NO_SOURCE_FILE
// debug info: SMAP
eval__4346.java
Clojure
*S Clojure
*F
+ 1 NO_SOURCE_FILE
NO_SOURCE_PATH
*L
0#1,1:0
*E
// access flags 25
public final static Lclojure/lang/Var; const__0
// access flags 25
public final static Ljava/lang/Object; const__1
// access flags 25
public final static Ljava/lang/Object; const__2
// access flags 9
public static <clinit>()V
L0
LINENUMBER 2 L0
LDC "clojure.core"
LDC "+"
INVOKESTATIC clojure/lang/RT.var (Ljava/lang/String;Ljava/lang/String;)Lclojure/lang/Var;
CHECKCAST clojure/lang/Var
PUTSTATIC user$eval__4346.const__0 : Lclojure/lang/Var;
ICONST_1
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTSTATIC user$eval__4346.const__1 : Ljava/lang/Object;
ICONST_2
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTSTATIC user$eval__4346.const__2 : Ljava/lang/Object;
RETURN
MAXSTACK = 0
MAXLOCALS = 0
// access flags 1
public <init>()V
L0
LINENUMBER 2 L0
L1
ALOAD 0
INVOKESPECIAL clojure/lang/AFunction.<init> ()V
L2
RETURN
MAXSTACK = 0
MAXLOCALS = 0
// access flags 1
public invoke()Ljava/lang/Object; throws java/lang/Exception
L0
LINENUMBER 2 L0
L1
LINENUMBER 2 L1
GETSTATIC user$eval__4346.const__1 : Ljava/lang/Object;
GETSTATIC user$eval__4346.const__2 : Ljava/lang/Object;
INVOKESTATIC clojure/lang/Numbers.add (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Number;
L2
LOCALVARIABLE this Ljava/lang/Object; L0 L2 0
ARETURN
MAXSTACK = 0
MAXLOCALS = 0
}
3
3就是表达式的结果。可以看到,一个表达式生成了一个class。其中<clinit>是静态初始化块,主要是初始化表达式中的字面常量;<init>不用说,默认的构造函数;invoke是核心方法,表达式生成的class,new一个实例后调用的就是invoke方法,执行实际的代码,高亮部分加载了两个常量,并执行Number.add方法。
最后,请Happy hacking!。
Clojure由于是基于JVM,同样无法支持完全的尾递归优化(TCO),这主要是Java的安全模型决定的,可以看看这个 久远的bug描述。但是Clojure和Scala一样支持同一个函数的直接调用的尾递归优化,也就是同一个函数在函数体的最后调用自身,会优化成循环语句。让我们看看这是怎么实现的。
Clojure的recur的特殊形式(special form)就是用于支持这个优化,让我们看一个例子,经典的求斐波那契数:
(defn recur-fibo [n]
(letfn [(fib
[current next n]
(if (zero? n)
current
;recur将递归调用fib函数
(recur next (+ current next) (dec n))))]
(fib 0 1 n)))
recur-fibo这个函数的内部定义了一个fib函数,fib函数的实现就是斐波那契数的定义,fib函数的三个参数分别是当前的斐波那契数(current)、下一个斐波那契数(next)、计数器(n),当计数器为0的时候返回当前的斐波那契数字,否则就将当前的斐波那契数设置为下一个,下一个斐波那契数字等于两者之和,计数递减并递归调用fib函数。注意,你这里不能直接调用(fib next (+ current next) (dec n)),否则仍将栈溢出。这跟Scala不同,Clojure是用recur关键字而非原函数名作TOC优化。
Clojure是利用asm 3.0作字节码生成,观察下recur-fibo生成的字节码会发现它其实生成了两个类,类似user$recur_fibo__4346$fib__4348和user$recur_fibo__4346,user是namespace,前一个是recur-fibo中的fib函数的实现,后一个则是recur-fibo自身,这两个类都继承自 clojure.lang.AFunction类,值得一提的是前一个类是后一个类的内部类,这跟函数定义相吻合。所有的用户定义的函数都将继承 clojure.lang.AFunction。
在这两个类中都有一个invoke方法,用于实际的方法执行,让我们看看内部类fib的invoke方法(忽略了一些旁枝末节)
1 // access flags 1
2 public invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; throws java/lang/Exception
3 L0
4 LINENUMBER 2 L0
5 L1
6 LINENUMBER 4 L1
7 L2
8 LINENUMBER 4 L2
9 ALOAD 3
10 INVOKESTATIC clojure/lang/Numbers.isZero (Ljava/lang/Object;)Z
11 IFEQ L3
12 ALOAD 1
13 GOTO L4
14 L5
15 POP
16 L3
17 ALOAD 2
18 L6
19 LINENUMBER 6 L6
20 ALOAD 1
21 ALOAD 2
22 INVOKESTATIC clojure/lang/Numbers.add (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Number;
23 L7
24 LINENUMBER 6 L7
25 ALOAD 3
26 INVOKESTATIC clojure/lang/Numbers.dec (Ljava/lang/Object;)Ljava/lang/Number;
27 ASTORE 3
28 ASTORE 2
29 ASTORE 1
30 GOTO L0
31 L4
32 L8
33 LOCALVARIABLE this Ljava/lang/Object; L0 L8 0
34 LOCALVARIABLE current Ljava/lang/Object; L0 L8 1
35 LOCALVARIABLE next Ljava/lang/Object; L0 L8 2
36 LOCALVARIABLE n Ljava/lang/Object; L0 L8 3
37 ARETURN
38 MAXSTACK = 0
39 MAXLOCALS = 0
首先看方法签名,invoke接收三个参数,都是Object类型,对应fib函数里的current、next和n。
关键指令都已经加亮,9——11行,加载n这个参数,利用Number.isZero判断n是否为0,如果为0,将1弹入堆,否则弹入0。IFEQ比较栈顶是否为0,为0(也就是n不为0)就跳转到L3,否则继续执行(n为0,加载参数1,也就是current,然后跳转到L4,最后通过ARETURN返回值current作结果。
指令20——22行,加载current和next,执行相加操作,生成下一个斐波那契数。
指令25-——26行,加载n并递减。
指令27——29行,将本次计算的结果存储到local变量区,覆盖了原有的值。
指令30行,跳转到L0,重新开始执行fib函数,此时local变量区的参数值已经是上一次执行的结果。
有的朋友可能要问,为什么加载current是用aload 1,而不是aload 0,处在0位置上的是什么?0位置上存储的就是著名的this指针,invoke是实例方法,第一个参数一定是this。
从上面的分析可以看到,recur干的事情就两件:覆盖原有的local变量,以及跳转到函数开头执行循环操作,这就是所谓的软尾递归优化。这从RecurExp的实现也可以看出来:
//覆盖变量
for (int i = loopLocals.count() - 1; i >= 0; i--) {
LocalBinding lb = (LocalBinding) loopLocals.nth(i);
Class primc = lb.getPrimitiveType();
if (primc != null) {
gen.visitVarInsn(Type.getType(primc).getOpcode(Opcodes.ISTORE), lb.idx);
}
else {
gen.visitVarInsn(OBJECT_TYPE.getOpcode(Opcodes.ISTORE), lb.idx);
}
}
//执行跳转
gen.goTo(loopLabel);
recur分析完了,最后有兴趣可以看下recur-fibo的invoke字节码
1 L0
2 LINENUMBER 1 L0
3 ACONST_NULL
4 ASTORE 2
5 NEW user$recur_fibo__4346$fib__4348
6 DUP
7 INVOKESPECIAL user$recur_fibo__4346$fib__4348.<init> ()V
8 ASTORE 2
9 ALOAD 2
10 CHECKCAST user$recur_fibo__4346$fib__4348
11 POP
12 L1
13 L2
14 LINENUMBER 7 L2
15 ALOAD 2
16 CHECKCAST clojure/lang/IFn
17 GETSTATIC user$recur_fibo__4346.const__2 : Ljava/lang/Object;
18 GETSTATIC user$recur_fibo__4346.const__3 : Ljava/lang/Object;
19 ALOAD 1
20 ACONST_NULL
21 ASTORE 1
22 ACONST_NULL
23 ASTORE 2
24 INVOKEINTERFACE clojure/lang/IFn.invoke (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
25 L3
26 LOCALVARIABLE fib Ljava/lang/Object; L1 L3 2
27 L4
28 LOCALVARIABLE this Ljava/lang/Object; L0 L4 0
29 LOCALVARIABLE n Ljava/lang/Object; L0 L4 1
30 ARETURN
5——7行,实例化一个内部的fib函数。
24行,调用fib对象的invoke方法,传入3个初始参数。
简单来说,recur-fibo生成的对象里只是new了一个fib生成的对象,然后调用它的invoke方法,这也揭示了Clojure的内部函数的实现机制。
托尔多正式宣布退役了,下个赛季他将会留在国际米兰做青训方面的工作,他在2000年欧洲杯上的经典表现将永载史册。他为国际米兰效力了十年,这也是我看国米比赛的十年。
来源: http://moonbase.rydia.net/mental/blog/programming/the-biggest-mistake-everyone-makes-with-closures.html
看下面的Ruby代码
k = []
for x in 1..3
k.push(lambda { x })
end
执行
k[0].call
你可能预期返回1,实际的结果却是3。这是为何?这是因为在 迭代过程中共用了同一个context,导致k中的 三个闭包都引用了同一个变量x。不仅仅Ruby有这个问题,python也一样
k = [lambda: x for x in xrange(1, 4)]
k[0]()
Javascript同样如此
var k = [];
for (var x = 1; x < 4; x++) {
k.push(function () { return x; });
}
alert(k[0]())
解决这个问题很简单,就是将 闭包包装到一个函数里,建立新的context,那么迭代过程中生成的闭包所处的context不同:
def make_value_func(value)
lambda { value }
end
k = (1..3).map { |x| make_value_func(x) }
这个时候,k[0].call正确地返回1。
这个问题并非在所有支持闭包的语言里都存在,例如scheme中就没有问题
(define k '())
(do ((x 1 (+ x 1)))
((= x 4) '())
(set! k (cons (lambda () x) k)))
(set! k (reverse k))
((car k)) =>1
Erlang也没有问题
K=[ fun()->X end || X <- [1,2,3]].
lists:map(fun(F)-> F() end,K).
再试试Clojure:
(def k (for [i (range 1 4)] (fn [] i)))
(map #(%) k)
同样没有问题。这里Erlang和Clojure都采用列表推断。
一些常见的关于 xmemcached的问题,收集整理,集中解答在此。事实上这里的大部分问题都可以在 用户指南里找到。
一、XMemcached是什么?
经常碰到的一个问题是很多朋友对 memcached不了解,误以为 xmemcached本身是一个缓存系统。Memcached是一个开源的,C写的分布式key-value缓存,XMemcached只是它的一个访问客户端。Memcached通过网络协议跟客户端交互,通过客户端你才可以去使用memcached,xmemcached是它的java客户端之一。
二、为什么要选择xmemcached?
memcached的java客户端有多个选择,为什么要选择xmemcached?理由如下:
1、支持所有的文本协议和二进制协议,支持连接 Kestrel和 TokyoTyrant等memcached协议兼容的系统并作特殊处理。
2、支持动态添加和删除memcached节点。
3、支持客户端统计
4、支持JMX监控和统计,可以通过JMX增删节点。
5、高性能
6、支持节点的权重设置
7、支持nio的连接池,在高负载环境下提高吞吐量。
三、对jdk版本有什么要求?
Xmemcached仅支持jdk1.5及以上版本。
四、使用的时候需要创建多个MemcachedClient对象吗?MemcachedClient是不是线程安全?
MemcachedClient是线程安全的,由于xmemcached的网络层实现是基于nio长连接的,因此你并不需要重复创建多个MemcachedClient对象,通常来说将MemcachedClient设置为全局的唯一单例的服务使用,如果是使用spring配置,那更是简单,在spring配置文件里配置一个MemcachedClient,其他对象引用即可使用。
五、为什么会抛出java.util.TimeoutException?
这是由于xmemcached的通讯层是基于非阻塞IO的,那么在请求发送给memcached之后,需要等待应答的到来,这个等待时间默认是1秒,如果超过1秒就抛出java.util.TimeoutExpcetion给用户。如果你频繁抛出此异常,可以尝试将全局的等待时间设置长一些,如我在压测中设置为5秒:
MemcachedClient memcachedClient=……
memcachedClient.setOpTimeout(5000L);
请注意,setOpTimeout设置的是全局的等待时间,如果你仅仅是希望将get或者set等操作的超时延长一点,那么可以通过这些方法的重载方法来使用:
<T> T get(java.lang.String key,long timeout)
boolean set(java.lang.String key, int exp,java.lang.Object value,
long timeout)
……
六、Kestrel和TokyoTyrant不支持flag字段,xmemcached是怎么解决的?
Xmemcached在存储的value前面自动加上和去除4个字节的flag,这一切对应用来说是透明的。具体请看用户指南。
七、连接memcacheq,取出来的消息比放进去的多?
这是由于memcacheq和kestrel一样,不支持multi get协议,因此只要关闭xmemcached的multi get优化就可以了。
memcachedClient.setOptimizeGet(false);
所谓multi get优化是指xmemcached会将连续的单个get请求合并成一个multi get请求作批量获取,提高效率。
八、连接kestrel,为什么过一段时间会自动断开并重连?
你可能使用的是kestrel 1.2以下版本,kestrel 1.2才支持version协议,xmemcached是基于version协议做心跳检测,因此当使用kestrel 1.2以下版本的时候会发生心跳检测失败并断开连接重连的情况,你可以升级kestrel,也可以关闭心跳检测:
memcachedClient.setEnableHeartBeat(false);
九、我使用maven,怎么引用xmemcached?
xmemcached 1.2.5已经加入了maven的中心仓库,因此你可以直接引用
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>1.2.5</version>
</dependency>
如果是之前版本,我推荐你升级,或者自己手工加入私人的maven仓库。
十、连接池是怎么回事?设置多大为好?
在高负载环境下,nio的单连接也会遇到瓶颈,此时你可以通过设置连接池来让更多的连接分担memcached的请求负载,从而提高系统的吞吐量。设置连接池通过
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("localhost:12000"));
builder.setConnectionPoolSize(5);
MemcachedClient client=builder.build();
或者通过spring配置也可以。
连接池通常不建议设置太大,我推荐在0-30之间为好,太大则浪费系统资源,太小无法达到分担负载的目的。
十一、性能建议及优化手段
性能的调整只能给出一般性的原则,实际情况千差万别,每次调整都需要做实际的测量才能确定是否带来期望的效果。
1、如果你的数据较小,如在1K以下,默认的配置选项已经足够。如果你的数据较大,我会推荐你调整网络层的TCP选项,如设置socket的接收和发送缓冲区更大,启用Nagle算法等等:
MemcachedClientBuilder builder = new XMemcachedClientBuilder(
AddrUtil.getAddresses(servers));
builder.setSocketOption(StandardSocketOption.SO_RCVBUF, 32 * 1024); // 设置接收缓存区为32K,默认16K
builder.setSocketOption(StandardSocketOption.SO_SNDBUF, 16 * 1024); // 设置发送缓冲区为16K,默认为8K
builder.setSocketOption(StandardSocketOption.TCP_NODELAY, false); // 启用nagle算法,提高吞吐量,默认关闭
默认如果连接超过5秒没有任何IO操作发生即认为空闲并发起心跳检测,你可以调长这个时间:
builder.getConfiguration().setSessionIdleTimeout(10000); // 设置为10秒;
更多网络层配置选项请参见Configuration类。
2、Xmemcached默认会做两个优化:将连续的单个get合并成一个multi get批量操作获取,将连续的请求合并成socket发送缓冲区大小的buffer发送。
如果你对响应时间比较在意,那么可以将合并的因子减小,或者关闭合并buffer的优化:
memcachedClient.setMergeFactor(50); //默认是150,缩小到50
memcachedClient.setOptimizeMergeBuffer(false); //关闭合并buffer的优化
如果你对吞吐量更在意,那么也可将合并因子调大,默认是150。但是也不可太大,太大可能导致平均响应时间延长。
3、如果你对心跳检测不在意,也可以关闭心跳检测,减小系统开销
memcachedClient.setEnableHeartBeat(false);
这个关闭,仅仅是关闭了心跳的功能,客户端仍然会去统计连接是否空闲,禁止统计可以通过:
builder.getConfiguration().setStatisticsServer(false);
让你不带萨队,让你不带坎比亚所,我太他妈高兴了。
布尔迪索、德米凯利斯这样的著名漏勺,还有古铁雷斯这样的牛B后卫,想进决赛完全在做梦。
什么艺术足球,什么功利足球,看看德国队的配合,脸红不?
只会在弱队身上刷数据到伊瓜因,给米利托擦鞋都不配。
梅球王被吹捧得太多了,相对于真小人的C罗,小动作、下阴招的梅球王终于带着0进球到记录回家了。88,阿根廷,世界清净了。当然,马上有一帮恶心阿迷矫情地唱着“阿根廷别为我哭泣”为他们的“潘帕斯雄鹰”招魂呢。
摘要: 《飞行大亨》是我很喜欢的一部电影,不过这里我想介绍的是一个叫Aviator的开源的Java表达式求值器。
一、轮子的必要性
表达式的求值上,java的选择非常多,强大的如Groovy、JRuby,N年没维护的beanshell,包括javaeye上朋友的IKExpression。为什么还需要Aviator?或者说Avi... 阅读全文
Java memcached客户端——XMemcached发布1.2.5版本,这是1.2的最后一个小版本,主要的改进如下:
1、合并yanf4j到xmemcached,目前只是简单的源码合并,以及去除了不需要的udp支持。因此从1.2.5开始,xmemcached不再依赖yanf4j,仅依赖slf4j。
2、支持SASL验证。memcached 1.4.3新增了SASL授权特性,启用了SASL的memcached会要求客户端进行授权验证,否则将拒绝请求,对于需要验证的用户来说是个可选的特性,关于memcached的SASL支持更多请看这里。Xmemcached 1.2.5开始支持客户端的SASL验证,一个典型的使用例子如下:
MemcachedClientBuilder builder = new XMemcachedClientBuilder(
AddrUtil.getAddresses("localhost:11211"));
builder.addAuthInfo(AddrUtil.getOneAddress("localhost:11211"), AuthInfo
.typical("cacheuser", "123456"));
// Must use binary protocol
builder.setCommandFactory(new BinaryCommandFactory());
MemcachedClient client=builder.build();
3、加快MemcachedClient.shutdown()方法的速度,应用可以更快地关闭xmemcached。
4、完善中文用户指南,添加客户端分布和SASL验证两节。
如果你使用maven,1.2.5已经放入maven的中心仓库,因此添加依赖即可使用:
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>1.2.5</version>
</dependency>
更多信息请参见wiki和changelog
XMemcached是一个基于java nio的memcached客户端
项目主页: http://code.google.com/p/xmemcached/
下载地址: http://code.google.com/p/xmemcached/downloads/list
wiki地址:http://code.google.com/p/xmemcached/w/list
一、传统并发模型的缺点
基于线程的并发
特点:
每任务一线程
直线式的编程
使用资源昂高,
context切换代价高,竞争锁昂贵
太多线程可能导致吞吐量下降,响应时间暴涨。
基于事件的并发模型
特点:
单线程处理事件
每个并发流实现为一个有限状态机
应用直接控制并发
负载增加的时候,吞吐量饱和
响应时间线性增长
二、SEDA架构
特点:
(1)服务通过queue分解成stage:
每个stage代表FSM的一个状态集合
Queue引入了控制边界
(2)使用线程池驱动stage的运行:
将事件处理同线程的创建和调度分离
Stage可以顺序或者并行执行
Stage可能在内部阻塞,给阻塞的stage分配较少的线程
1、Stage-可靠构建的基础
(1)应用逻辑封装到Event Handler
接收到许多事件,处理这些事件,然后派发事件加入其他Stage的queue
对queue和threads没有直接控制
Event queue吸纳过量的负载,有限的线程池维持并发
(2)Stage控制器
负责资源的分配和调度
控制派发给Event Handler的事件的数量和顺序
Event Handler可能在内部丢弃、过滤、重排序事件。
2、应用=Stage网络
(1)有限队列
入队可能失败,如果队列拒绝新项的话
阻塞在满溢的队列上来实现吸纳压力
通过丢弃事件来降低负载
(2) 队列将Stage的执行分解
引入了显式的控制边界
提供了隔离、模块化、独立的负载管理
(3)方便调试和profile
事件的投递可显
时间流可跟踪
通过监测queue的长度发现系统瓶颈
3、动态资源控制器
(1)、线程池管理器
目标: 决定Stage合理的并发程度
操作:
观察queue长度,如果超过阀值就添加线程
移除空闲线程
(2)、批量管理器
目的:低响应时间和高吞吐量的调度
操作:
Batching因子:Stage一次处理的消息数量
小的batching因子:低响应时间
大的batching因子:高吞吐量
尝试找到具有稳定吞吐量的最小的batching因子
观察stage的事件流出率
当吞吐量高的时候降低batching因子,低的时候增加
三、小结
SEDA主要还是为了解决传统并发模型的缺点,通过将服务器的处理划分各个Stage,利用queue连接起来形成一个pipeline的处理链,并且在Stage中利用控制器进行资源的调控。资源的调度依据运行时的状态监视的数据来进行,从而形成一种反应控制的机制,而stage的划分也简化了编程,并且通过queue和每个stage的线程池来分担高并发请求并保持吞吐量和响应时间的平衡。简单来说,我看中的是服务器模型的清晰划分以及反应控制。
Java的垃圾收集算法是分代的,因为根据2/8原则,80%的Java对象都是速生速灭的,因此将Java Heap划分为new和old,对两个区域采用不同的垃圾回收算法,在new代存活下来的对象转移到old区,这样一来大大提高了Java GC的效率。
类似分代的思想在很多地方可以用到,分代的本质是根据对象生命周期的不同做区别处理,而不是采取一刀切的方式来提高系统的处理效率。推而广之,比如缓存的使用,现在很多web应用都采用了类似memcached这样的缓存挡在数据库前面分担负载,那么你可以将memcached理解成new代,而数据库是old代。memcached中存储的是查询的热点数据,新鲜火热,但是易失,并且在数据更新的时候被移除;而数据库保存了所有的数据,当缓存没有命中的时候才去查询数据库并存储到缓存。new和old这只是简单的二代划分,事实上现在越来越多的系统是多级缓存,页面缓存、memcached缓存、JVM内部缓存、查询缓存等等直到数据库,从web页面到后端是一个越来越old的过程,缓存对象持续的生命周期逐渐增长直到persist状态。
具体到JVM内部缓存,我们通常使用LRU算法来实现一个安全有限的缓存,如直接继承LinkedHashMap将可以实现一个简单的LRUMap。基于内存使用上的考虑,我们会给LRUMap设定一个最大的capacity,当数据量超过这个capacity的时候将最近最少访问的元素移除来容纳新的元素。这样处理产生的问题是这个capactity不能设置得太大,因为怕内存不够用,但是不够大的结果可能是命中率没有那么高(跟你的场景相关),那么如何在保存内存安全的前提下更进一步缓存更多的对象呢?答案也是分代。LRUMap默认存储的都是对象的强引用,我们知道Java还有其他3种引用类:soft,weak和 phantom。其中Soft引用是在jvm内存不够的时候进行回收,weak引用是在垃圾回收碰到的时候会被回收,显然weak->soft->strong三类引用的生命周期可以划分为3个代,我们将可以实现一个可以容纳更多对象的LRUMap:LRUMap设置两个阀值,一个是强引用的最大阀值,这个不能太大,一个是软引用的最大数目,当超过第一个阀值的时候,不是将LRU替换出来的对象移除,而是替代转换为软引用存储;如果软引用的数目也超过阀值,那么可以将软引用这个Map里的对象LRU替换成Weak引用存储或者简单移除。处理元素查询的时候,多了一个步骤,在查询强引用未果的情况下,需要再去查询软引用集合,不过都是O(1)复杂度的查询,不会成为明显的瓶颈。通过将缓存对象分代,我们实现了容难更多缓存对象的目标,大部分对象以强引用的形式存储,被LRU替换出去最近最少访问的元素以软引用存储,它们在内存不够的时候被垃圾回收,保证了内存使用上的安全性。我们在系统中采用了类似这样的缓存,缓存的命中率有了明显的提高。
题目是《缓存的分代》,其实谈的是分代这种常见的设计或者说技巧,在需要处理大量对象的场景中,不采用一刀切的方式,而是根据对象的特点进行适当的分代处理,带来的效率提升可能是惊人的。
PS.关于这个 招聘罗嗦两句,我是这个小组的成员,有人质疑我的目的是为了赚推荐费,这个不能说没有,不过主要目的还是招人,我们很缺人。那么多要求可以归结为一句话:我们找Java基础良好、对并发通信有丰富实践经验、写代码相对靠谱、为人相对靠谱的人。那些要求并非硬性,如果你觉的合适,尽管投简历,谢谢。我们小组做的东西我认为还是有价值的,也很有挑战,淘宝内部的很多应用都在使用,如果你希望你做的产品被成千上万的人每天使用,欢迎加入。
作为一个coder,我不仅在写程序,也在写bug。遇到bug总是比较尴尬的事情,如果这个bug还是别人发现,那更是心里不好受。责备自己是没有用的,能做的是建立一个BUG数据库,时常回顾下自己犯过那些愚蠢的事情,怎么避免以后再犯同样的事情。昨天读《程序员》看到一笑话,说优秀的程序员哪怕在过单行道的时候也会向左右两边看,笑话其实不好笑,反而再次提醒我:小心、小心、再小心。
毫不惭愧地说,我也是今年才开始有意识地去做回顾bug这件事情,今天回顾下最近写出来的这几个BUG:三个是麻痹大意导致的,一个是switch语句竟然没写break,一个是并发BUG,一个是考虑问题不全面导致的问题。这里主要还是想讲那三个麻痹大意写出来的BUG,都是在急匆匆修改问题的情况下写出来的,本意是为了解决原有的BUG,在自以为很有信心地情况下匆忙地修改代码,没有认真做review,没有添加单元测试,在解决问题的同时引入了新的问题。
这些愚蠢的BUG修正起来非常简单,但是为什么那个时候却没有发现呢?我自省下,还是盲目自信导致的,因为快速地修复BUG似乎很能给人一种虚假的快感:瞧,这个问题我修正起来很快,我是代码快枪手,哦耶~,修复也还罢了,如果能再补充下测试,也许这些问题就能避免,但是我却又一次自信过了头。我在想,如果下次还遇到这种需要快速修改问题的时候我该怎么做,我该先深呼吸下,停一停,先想想怎么改再动手,想想风险点,改完之后至少review三遍,并且一定要添加这些情况的测试。尽管我相信以后我还会写下一些愚蠢的BUG,但是希望能让自己少后悔一点点。
联系邮箱: boyan@taobao.com
简历格式: 最好是纯文本格式
淘宝分布式产品组诚聘Java工程师,有兴趣的请联系,谢谢,此招聘长期有效。具体职位和要求如下:
消息中间件资深Java工程师
工作地点:杭州
职位描述:
负责消息中间件的设计、开发等工作
职位要求:
1、扎实的Java开发基础知识
2、Java多线程、并发以及网络通信有深厚的经验
3、熟悉大规模分布式系统架构,熟悉分布式存储系统
4、热爱技术,对技术有不懈的追求。
5、熟悉消息中间件,并了解消息中间件原理的优先考虑
分布式数据层资深Java工程师
工作地点:杭州
职位描述:
负责分布式数据层的设计、开发等工作
职位要求:
1、扎实的Java开发基础知识
2、Java多线程、并发以及网络通信有深厚的经验
3、对JDBC,JPA,JTA有深厚的理解
4、熟悉常见的分布式存储解决方案
5、热爱技术,对技术有不懈的追求
6、熟悉数据层的原理和应用
7、熟悉关系数据库模型、有分布式事务相关经验优先
Java框架和工具开发工程师
工作地点:杭州
职位描述:
1、 设计、开发、改进基于Java的工具和框架。
2、 指导开发团队使用工具和框架,解决疑难问题。
职位要求:
1、 熟悉并自如运用Java语言及JDK类库,具备良好的编程习惯。
2、 熟悉多种Java开源项目,精通目前主流的Java开源项目的使用方法和设计理念。有独到见解者更佳。
3、 熟悉OOP理念及常见设计模式。
4、 熟悉Eclipse开发平台,了解Eclipse Plugin的开发。
5、 视野广阔,了解业内发展动态。
6、 喜爱专研,精益求精,有较强的学习能力。
7、 善于交流,乐于分享。
8、 具备英文阅读能力和书写能力。
应用管理工具开发工程师
工作地点:杭州
职位描述:
1、承担应用、系统管理工具及其他相关的工具、应用的开发
2、大规模应用管理相关技术的预研工作
职位要求:
1、两年及以上开发Java或Php开发经验
2、对技术充满兴趣,有扎实的编程基础
3、对经历过的产品、项目有深入的理解,而不仅停留在开发
联系邮箱: boyan@taobao.com
简历格式: 最好是纯文本格式
附注: 如果管理员觉的放在首页不合适,请拿下,不好意思,招人不容易啊。
每天下班后回家跟儿子的例行对话:
爸爸:你今天乖不乖?
儿子:啊
爸爸:有没有想爸爸?
儿子:啊
爸爸:吃饭了没?
儿子:哦
爸爸:有没有吃饱啊?
儿子:呃
……
今天是儿童节,昨天晚上带小家伙去买玩具,儿子很替老爸省钱,自己只挑了气球。最后还是老爸老妈狠狠心给他买了画板和音乐小屋子,庆幸的是儿子很喜欢,一个晚上在那边“打电话”。儿子,节日快乐,希望你健康成长,快乐每一天。
1、首先态度需要端正,做代码的自我审查并不是否定自己,而是给自己将工作做得更好的一次机会。在审查过程中要尽量将自己作为一个旁观者的心态去审查自己的代码,尽管这比较困难。
2、代码审查离不开重构,在审查过程中发现任何坏味道都请使用重构去改善,发现缺乏测试的地方要及时补充测试,不要让BUG遗漏。
3、代码的自我审查可能不是越早越好,隔一段时间之后回去看自己写的东西,对一些设计上的选择能有更客观的评价,在审查的过程中可能需要重新去理解代码,在此过程中可以检查自己代码的可读性,并思考如何改善可读性,切记代码首先是给人读的。
4、审查过程中需要记录下一些犯下的错误,以及当时为什么会犯下这样的错误,建立自己的bug数据库,并时常review,在以后的工作中避免同样的错误。
5、代码的自我审查应该是一个持续性的过程,而非特定时间的特定行动,时常审查自己的代码,不仅能辨析自己的得失,还能够进一步提高自己在未来工作中的设计能力和预见能力。
6、代码的自我审查跟团队成员之间的相互review并不矛盾,在相互review之前做一个自我审查,有助于提高review的效率,包括可读性的提高和一些一般错误的避免。
7、代码自我审查的一些常见注意点:
(0)自认为绝不会出错,并且从来没有审查过的代码。
(1)注意else语句,if条件下的子语句通常可能是个正常的流程,而else意味着异常的情况或者特殊的场景,你可能特别注意怎么处理正常的情况,却忽略了else子句的实现细节,如该释放的锁没释放,该递减的计数没有递减,该赋予特殊值却没有赋予等等。
(2)注意空的方法,没有方法体的方法,是不需要实现?还是忘了实现?
(3)注意switch语句,有没有忘了break?这种错误通过findbugs之类的静态代码检查工具都能避免。
(4)注意大块的注释,为什么这么多注释?是代码写的很糟糕?还是遗留的注释?遗留的注释会误导人,要及时删除。
(5)注意一些看起来“不合常理”的代码,这样的代码很多都是基于所谓性能考虑而优化过的代码,这样的优化是否还需要?是否能去除这些“奇怪”的代码也能实现正常的需求?
(6)对客户端的使用有假设的代码,假设用户只会这么用,假设用户只会用到返回对象中的某些属性,其他属性一定不会用到?不要对客户代码做假设!这个客户代码包括外部用户和调用这个模块的内部代码。
(7)标注了FIXME、TODO之类task标签的代码,是否忘了修复BUG,实现功能?
(8)任何超过15行以上的方法,这些方法是否能拆分成更细粒度的方法,并保持在同一个抽象层次上?
(9)任何在代码中出现的常量值,是否应该提取出来成为单独的常量,常量的默认值设置是否合理?
(10) 任何持有容器的代码,提供了放入容器的方法,是否提供了从容器中移除对象的方法?确保没有内存泄漏的隐患。
(11)重构中提到的其他坏味道,别放过它们,但是也不要追求完美,OO不是圣杯,如果能简单的实现一个算法,你不要引入3个对象和4个接口。
(12)在review最后能列出一张清单,开列下该项目面临的风险点,并提出解决办法,然后按照这张清单去review关键代码,着重检查异常情况下的处理。风险点的review,我建议可以放在后面,在一般性错误解决之后进行,此时你对代码也将再度变的熟悉。
笑死我了

Ruby Fiber指南(一)基础
Ruby Fiber指南(二)参数传递
Ruby Fiber指南(三)过滤器
Ruby Fiber指南(四)迭代器
Ruby Actor指南(五)实现Actor
写这个指南的时候,计划是第五章写一个Fiber的应用例子,但是一时没有想到比较好的例子,模仿《Programming in Lua》中的多任务下载的例子也不合适,因为Ruby中的异步HttpClient跟lua还是很不一样的,体现不了Fiber的优点。因此,这第五节一直拖着没写。
恰巧最近在小组中做了一次Erlang的分享,有人问到Erlang调度器的实现问题,这块我没注意过,那时候就根据我对coroutine实现actor的想法做了下解释,后来思考了下那个解释是错误的,Erlang的调度器是抢占式的,而通过coroutine实现的actor调度却是非抢占的,两者还是截然不同的。我在《 Actor、Coroutine和Continuation的概念澄清》中提到coroutine可以实现actor风格,actor跟coroutine并没有必然的联系,这篇文章的目的就在于证明这一点,使用Ruby Fiber实现一个简单的actor风格的库,整个代码不到100行。后面还会谈到这个实现的缺点,以及我对Erlang调度器实现的理解。
首先是monkey patch,给Thread和Fiber类加上两个方法,分别用于获取当前线程的调度器和Fiber对应的actor:
class Thread
#得到当前线程的调度器
def __scheduler__
@internal_scheduler||=FiberActor::Scheduler.new
end
end
class Fiber
#得到当前Fiber的actor
def __actor__
@internal_actor
end
end
这里实现的actor仍然是Thread内的,一个Thread只跑一个调度器,每个actor关联一个Fiber。
让我们来想想调度器该怎么实现,调度器顾名思义就是协调actor的运行,每次挑选适当的actor并执行,可以想象调度器内部应该维护一个等待调度的actor队列,Scheduler每次从队列里取出一个actor并执行,执行完之后取下一个actor执行,不断循环持续这个过程;在没有actor可以调度的时候,调度器应该让出执行权。因此调度器本身也是一个Fiber,它内部有个queue,用于维护等待调度的actor:
module FiberActor
class Scheduler
def initialize
@queue=[]
@running=false
end
def run
return if @running
@running=true
while true
#取出队列中的actor并执行
while actor=@queue.shift
begin
actor.fiber.resume
rescue => ex
puts "actor resume error,#{ex}"
end
end
#没有任务,让出执行权
Fiber.yield
end
end
def reschedule
if @running
#已经启动,只是被挂起,那么再次执行
@fiber.resume
else
#将当前actor加入队列
self << Actor.current
end
end
def running?
@running
end
def <<(actor)
#将actor加入等待队列
@queue << actor unless @queue.last == actor
#启动调度器
unless @running
@queue << Actor.current
@fiber=Fiber.new { run }
@fiber.resume
end
end
end
end
run方法是核心的调度方法,注释说明了主要的工作流程。因为调度器可能让出执行权,因此提供了reschedule方法重新resume启动调度器。<<方法用于将等待被调度的actor加入等待队列,如果调度器没有启动,那么就启动调度Fiber。
有了调度器,Actor的实现也很简单,Actor跟Fiber是一对一的关系,Actor内部维护一个mailbox,用来存储接收到的消息。最重要的是receive原语的实现,我们这里很简单,不搞模式匹配,只是接收消息。receive的工作流程大概是这样,判断mailbox中有没有消息,有消息的话,取出消息并调用block处理,没有消息的话就yield让出执行权。
module FiberActor
class Actor
attr_accessor :fiber
#定义类方法
class << self
def scheduler
Thread.current.__scheduler__
end
def current
Fiber.current.__actor__
end
#启动一个actor
def spawn(*args,&block)
fiber=Fiber.new do
block.call(args)
end
actor=new(fiber)
fiber.instance_variable_set :@internal_actor,actor
scheduler << actor
actor
end
def receive(&block)
current.receive(&block)
end
end
def initialize(fiber)
@mailbox=[]
@fiber=fiber
end
#给actor发送消息
def << (msg)
@mailbox << msg
#加入调度队列
Actor.scheduler << self
end
def receive(&block)
#没有消息的时候,让出执行权
Fiber.yield while @mailbox.empty?
msg=@mailbox.shift
block.call(msg)
end
def alive?
@fiber.alive?
end
end
end
Actor.spawn用于启动一个actor,内部其实是创建了一个fiber并包装成actor给用户,每个actor一被创建就加入调度器的等待队列。<<方法用于向actor传递消息,传递消息后,该actor也将加入等待队列,等待被调度。
我们的简化版actor库已经写完了,可以尝试写几个例子,最简单的hello world:
include FiberActor
Actor.spawn { puts "hello world!"}
输出:
hello world!
没有问题,那么试试传递消息:
actor=Actor.spawn{
Actor.receive{ |msg| puts "receive #{msg}"}
}
actor << :test_message
输出:
receive test_message
也成了,那么试试两个actor互相传递消息,乒乓一下下:
pong=Actor.spawn do
Actor.receive do |ping|
#收到ping,返回pong
ping << :pong
end
end
ping=Actor.spawn do
#ping一下,将ping作为消息传递
pong << Actor.current
Actor.receive do |msg|
#接收到pong
puts "ping #{msg}"
end
end
#resume调度器
Actor.scheduler.reschedule
输出:
ping pong
都没有问题,这个超级简单actor基本完成了。可以看到,利用coroutine来实现actor是完全可行的,事实上我这里描述的实现基本上是 revactor这个库的实现原理。 revactor是一个ruby的actor库,它的实现就是基于Fiber,并且支持消息的模式匹配和thread之间的actor调度,有兴趣地可以去玩下。更进一步,其实采用轻量级协程来模拟actor风格早就不是新鲜主意,比如在 cn-erlounge的第四次会议上就有两个topic是关于这个,一个是51.com利用基于ucontext的实现的类erlang进程模型,一个是许世伟的CERL。可以想见,他们的基本原理跟本文所描述不会有太大差别,那么面对的问题也是一样。
采用coroutine实现actor的主要缺点如下:
1、因为是非抢占式,这就要求actor不能有阻塞操作,任何阻塞操作都需要异步化。IO可以使用异步IO,没有os原生支持的就需要利用线程池,基本上是一个重复造轮子的过程。
2、异常的隔离,某个actor的异常不能影响到调度器的运转,简单的try...catch是不够的。
3、多核的利用,调度器只能跑在一个线程上,无法充分利用多核优势。
4、效率因素,在actor数量剧增的情况下,简单的FIFO的调度策略效率是个瓶颈,尽管coroutine的切换已经非常高效。
当然,上面提到的这些问题并非无法解决,例如可以使用多线程多个调度器,类似erlang smp那样来解决单个调度器的问题。但是如调度效率这样的问题是很难解决的。相反,erlang的actor实现就不是通过coroutine,而是自己实现一套类似os的调度程序。
首先明确一点,Erlang的process的调度是抢占式的,而非couroutine的协作式的。其次,Erlang早期版本是只有一个调度器,运行在一个线程上,随着erts的发展,现在erlang的调度器已经支持smp,每个cpu关联一个调度器,并且可以明确指定哪个调度器绑定到哪个cpu上。第三,Erlang的调度也是采用优先队列+时间片轮询的方式,每个调度器关联一个 ErtsRunQueue,ErtsRunQueue内部又分为三个ErtsRunPrioQueue队列,分别对应high,max和normal,low的优先级,其中normal和low共用一个队列;在Erlang中时间片是以reduction为单位,你可以将reduction理解成一次函数调用,每个被调度的process能执行的reduction次数是有限的。调度器每次都是从max队列开始寻找等待调度的process并执行,当前调度的队列如果为空或者执行的reductions超过限制,那么就降低优先级,调度下一个队列。
从上面的描述可以看出,Erlang优秀的地方不仅在于actor风格的轻量级process,另一个强悍的地方就是它的类os的调度器,再加上OTP库的完美支持,这不是一般方案能山寨的。
在小组内做的一次分享,基本上是在锋爷的一个PPT的基础上做了一些扩充,对Erlang没有了解过的朋友可以看看。
基本能说明整个设计以及运行时流程,最近搞文档画的。
基本组件结构图——典型的Reactor+handler模式
一次IO读请求的处理过程:
妈妈,我不想走
——纪念不幸逝去的孩子们!
妈妈
你为什么在哭泣
今天不是我的生日
为什么在我身旁点燃红烛
没有歌声
也没有小朋友
只有你的泪水
在不停地淌流
妈妈,拽紧我的手
我不想走
妈妈
你为什么在哭泣
今天我又惹你生气
是我没完成作业
还是我偷偷在玩游戏
你打我吧
你骂我吧
千万不要把我抛弃
妈妈,拽紧我的手
我不想走
妈妈
你为什么哭泣
是老天不公
还是你的爱没将我留住
天堂没有阳光
也没有鸟语花香
我不去那边当天使
今生只想做你的心头肉
妈妈,拽紧我的手
我不想走
来自网易网友评论,新闻链接《 福建南平男子在校门口杀害八名小学生》

Actor、 Coroutine和 Continuation这三个概念由于并发的受关注而被经常提到,这里主要想谈下这三者的区别和联系,以便更好的区分问题领域和讨论。首先,Actor和Coroutine在我看来是两种并发模型,仅针对于并发这个领域,而Continuation则是程序设计领域的一个概念,相比于Actor和Coroutine是一个更基础的概念。 那么,什么是Continuation?这个要从表达式的求值说起。一个表达式的求值可以分为两个阶段:“ What to evaluate?”和“ What to do with the value”,“What to do with the value”就是计算的Continuation。以下面这段代码为例: if x<3 then return x+1 else return x end 考察其中的表达式x<3,这个表达式就是“what to evaluate?”,代表你将计算的东西,然后根据x<3的结果决定是执行x+1还是直接返回x,这个根据x<3的值来决定下一步的过程就是这个表达式的Continuation,也就是"what to do with the value"。怎么得到某个表达式的Continuation呢?在支持Continuation的语言里提供了call-with-current-continuation的函数,通常简称为call/cc,使用这个函数你就可以在任何代码中得到Continuation。进一步,continuation有什么作用呢?它可以做的事情不少,如nonlocal exits、回溯、多任务的实现等等。例如在scheme中没有break语句,就可以用call/cc实现一些此类高级的控制结构: (call/cc (lambda (break) (for-each (lambda (x) (if (< x 0) (break x))) '(99 88 -77 66 55)) #t))
上面这段代码查找列表(99 88 -77 66 55)中的负数,当查找到的时候马上从迭代中退出并返回该值,其中的break就是一个continuation。刚才还提到continuation可以实现回溯,那么就可以实现一个穷举的机器出来用来搜索解空间,也就是类似Prolog中的回溯机制,在SICP这本书里就介绍了如何用call/cc实现一个简单的逻辑语言系统。更著名的就是神奇的amb操作符,有兴趣可以看看 这里。 接下来我们来看看如何continuation实现多任务,在Continuation的 维基百科里给了一段代码来展示如何用scheme来实现coroutine,我稍微修改了下并添加了注释: ;;continuation栈,保存了等待执行的continuation (define call/cc call-with-current-continuation) (define *queue* '())
(define (empty-queue?) (null? *queue*))
(define (enqueue x) (set! *queue* (append *queue* (list x))))
(define (dequeue) (let ((x (car *queue*))) (set! *queue* (cdr *queue*)) x)) ;;启动协程 (define (resume proc) (call/cc (lambda (k) ;;保存当前continuation,执行proc (enqueue k) (proc)))) ;;让出执行权 (define (yield) (call/cc (lambda (k) ;;保存当前continuation,弹出上一次执行的cont并执行 (enqueue k) ((dequeue))))) ;;停止当前协程或者当没有一个协程时停止整个程序,最简单的调度程序 (define (thread-exit) (if (empty-queue?) (exit) ((dequeue))))
(注:scheme以分号开头作为注释) 这其实就是一个coroutine的简单实现,context的保存、任务的调度、resume/yield原语……样样俱全。使用起来类似这样,下面这段程序轮流打印字符串: (define (display-str str) (lambda() (let loop() (display str) (newline) (yield) (loop))))
;;;创建两个协程并启动调度器 (resume (display-str "This is AAA")) (resume (display-str "Hello from BBB")) (thread-exit)
任务非常简单,打印下传入的字符串并换行,然后让出执行权给另一个任务执行,因此输出: This is AAA Hello from BBB This is AAA Hello from BBB This is AAA Hello from BBB This is AAA Hello from BBB …… 谈了这么多continuation的应用,事实上我想说明的是continuation可以用来实现协程,Ruby 1.9中call/cc和Fiber的实现(在cont.c)大体是一样的同样说明了这一点。 接下来我们讨论下Actor和Coroutine的关系,上面提到Actor是一种并发模型,我更愿意称之为一种编程风格,Actor跟message passing、Duck Typing是一脉相承的。Actor风格是可以这么描述:将物理世界抽象成一个一个的Actor,Actor之间通过发送消息相互通信,Actor不关心消息是否能被接收或者能否投递到,它只是简单地投递消息给其他actor,然后等待应答。Actor相比于Coroutine是一种更高层次的抽象,它提供的receive和pattern match的原语更接近于现实世界,而使用coroutine编程你还需要手工介入任务调度,这在Actor中是由一个调度器负责的。 同样,Actor可以用coroutine实现,例如Ruby有个 revactor项目,就是利用1.9引入的Fiber实现actor风格的编程,它的实现非常简单,有兴趣地可以看看,其实跟continuation实现coroutine类似。但是Actor并不是一定要用coroutine才能实现,Actor是一种编程风格,你在Java、C#、C++中同样可以模拟这样的方式去做并发编程,.net社区的老赵 实现过一个简单的Actor, Scala的Actor实现是基于外部库,利用scala强大的元编程能力使得库的使用像内置于语言。 总结下我想表达的:Continuation是程序设计领域的基础概念,它可以用于实现coroutine式的多任务,Actor是一种比之coroutine更为抽象的编程风格,Actor可以基于Coroutine实现但并非必须,Actor和Coroutine都是现在比较受关注的并发模型。
Google正式退出中国,跟朋友的聊天记录: 我: 真到那天,是中国IT业的悲哀 某人: 不仅仅是it业 这是一个标志 铁幕再次拉开 中国已经不在乎外部形象了 也不在乎自己的工程师是否能和世界交流了 只求政权稳定 能移民的赶紧吧,不能移民的继续被绑架,被代表,被河蟹,喝三鹿,打疫苗,得永生。
开源的Java Memcached Client——Xmemcached 发布1.2.4版本,这个版本主要的工作是BUG修正,主要改动如下:
1、修正bug,包括issue 68,issue 74。Issue 68修复后,现在可以正常地使用TokyoTyrantTranscoder来连接TokyoTyrant。
2、为修正的BUG添加新的单元测试。
3、将CachedData.MAX_VALUE修改为可修改状态,允许用户设置更大的值,这个值决定了可以向memcached存储的最大值,默认是1M(通过memcached的-I size选项),单位是字节:
CachedData.MAX_SIZE
=
2
*
1024
*
1024
;
//
修改为2M
4、更正用户指南的错误并补充部分资料。
下载地址: http://code.google.com/p/xmemcached/downloads/list
项目主页:http://code.google.com/p/xmemcached/
Wiki页 :http://code.google.com/p/xmemcached/w/list
Ruby Fiber指南(一)基础
Ruby Fiber指南(二)参数传递
Ruby Fiber指南(三)过滤器
Ruby Fiber指南(四)迭代器
Ruby Actor指南(五)实现Actor
上一节介绍了利用Fiber实现类unix管道风格的过滤链,这一节将介绍利用Fiber来实现迭代器, 我们可以将循环的迭代器看作生产者-消费者模式的特殊的例子。迭代函数产生值给循环体消费。所以可以使用Fiber来实现迭代器。协程的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保存迭代函数返回的状态,也就是说无需在迭代函数中保存状态,状态的保存和恢复交由Fiber自动管理。
这一节的介绍以一个例子贯穿前后,我们将不断演化这个例子,直到得到一个比较优雅的可重用的代码,这个例子就是求数组的全排列。如数组[1,2,3]的全排列包括6种排列:
2 3 1
3 2 1
3 1 2
1 3 2
2 1 3
1 2 3
全排列的递归算法实现很简单,我们用Ruby实现如下:
#全排列的递归实现
def permgen (a, n)
if n == 0 then
printResult(a)
else
n.times do |i|
a[n-1], a[i] = a[i], a[n-1]
permgen(a, n - 1)
a[n-1], a[i] = a[i], a[n-1]
end
end
end
def printResult (a)
puts a.join(" ")
end
permgen([1,2,3,4],4)
算法的思路是这样:将数组中的每一个元素放到最后,依次递归生成所有剩余元素的排列,没完成一个排列就打印出来。很显然,这里有消费者和生产者的关系存在,生产者负责产生排列,消费者负责打印任务,整个程序由消费者驱动,因此用Fiber改写如下:
第一步,将打印任务修改为Fiber#yield,生产者产生一个排列后将结果传递给消费者并让出执行权:
def permgen (a, n)
if n == 0 then
Fiber.yield(a)
……
end
第二步,实现一个迭代器工厂,返回一个匿名的迭代函数,迭代函数请求Fiber产生一个新的排列:
def perm(a)
f=Fiber.new do
permgen(a,a.size)
end
return lambda{ f.resume if f.alive? }
end
这样一来我们就可以利用一个while循环来打印全排列:
it=perm([1,2,3,4])
while a=it.call
printResult(a)
end
注意到,在perm方法中有一个很常见的模式,就是将对Fiber的resume封装在一个匿名函数内,在lua为了支持这种模式还特意提供了一个coroutine.wrap方法来方便编程,在Ruby Fiber中却没有,不过我们可以自己实现下,利用open-class的特性实现起来非常简单:
#为Fiber添加wrap方法
class Fiber
def self.wrap
if block_given?
f=Fiber.new do |*args|
yield *args
end
return lambda{|*args| f.resume(*args) if f.alive? }
end
end
end
Fiber#wrap方法跟new方法一样,创建一个新的Fiber,但是返回的是一个匿名函数,这个匿名函数负责去调用fiber的resume,利用wrap改写perm方法变得更简洁:
def perm(a)
Fiber.wrap{ permgen(a,a.size) }
end
但是还不够,while循环的方式还是不够优雅,每次都需要明确地调用迭代器的call方法,这一点让人挺不舒坦,如果能像for...in那样的泛型循环就好了,我们知道Ruby中的for...in其实是一个语法糖衣,都是转变成调用集合的each方法并传入处理的block,因此,要想实现一个优雅的迭代器,我们做下封装就好了:
class FiberIterator
def initialize
@fiber_wrap=Fiber.wrap do
yield
end
end
def each
while value=@fiber_wrap.call
yield value
end
end
end
那么现在的perm方法变成了创建一个迭代器FiberIterator:
def perm(a)
FiberIterator.new{ permgen(a,a.size) }
end
这样一来我们就可以通过for...in来调用迭代器了
it=perm([1,2,3,4])
for a in it
printResult(a)
end
Ruby Fiber指南(一)基础
Ruby Fiber指南(二)参数传递
Ruby Fiber指南(三)过滤器
Ruby Fiber指南(四)迭代器
Ruby Actor指南(五)实现Actor
在学习了Fiber的基础知识之后,可以尝试用Fiber去做一些比较有趣的事情。这一节将讲述如何使用Fiber来实现类似unix系统中的管道功能。在unix系统中,可以通过管道将多个命令组合起来做一些强大的功能,最常用的例如查找所有的java进程:
ps aux|grep java
通过组合ps和grep命令来实现,ps的输出作为grep的输入,如果有更多的命令就形成了一条过滤链。过滤器本质上还是生产者和消费者模型,前一个过滤器产生结果,后一个过滤器消费这个结果并产生新的结果给下一个过滤器消费。因此我们就从最简单的生产者消费者模型实现说起。
我们要展示的这个例子场景是这样:生产者从标准输入读入用户输入并发送给消费者,消费者打印这个输入,整个程序是由消费者驱动的,消费者唤醒生存者去读用户输入,生产者读到输入后让出执行权给消费者去打印,整个过程通过生产者和消费者的协作完成。
生产者发送是通过yield返回用户输入给消费者(还记的上一节吗?):
def send(x)
Fiber.yield(x)
end
而消费者的接收则是通过唤醒生产者去生产:
def receive(prod)
prod.resume
end
生产者是一个Fiber,它的任务就是等待用户输入并发送结果给消费者:
def producer()
Fiber.new do
while true
x=readline.chomp
send(x)
end
end
end
消费者负责驱动生产者,并且在接收到结果的时候打印,消费者是root fiber:
def consumer(producer)
while true
x=receive(producer)
break if x=='quit'
puts x
end
end
最终的调用如下:
consumer(producer())
完整的程序如下:
#生产者消费者
require 'fiber'
def send(x)
Fiber.yield(x)
end
def receive(prod)
prod.resume
end
def producer()
Fiber.new do
while true
x=readline.chomp
send(x)
end
end
end
def consumer(producer)
while true
x=receive(producer)
break if x=='quit'
puts x
end
end
if $0==__FILE__
consumer(producer())
end
读者可以尝试在ruby1.9下运行这个程序,每次程序都由消费者驱动生产者去等待用户输入,用户输入任何东西之后回车,生产者开始运行并将读到的结果发送给消费者并让出执行权(通过yield),消费者在接收到yield返回的结果后打印这个结果,因此整个交互过程是一个echo的例子。
最终的调用consumer(producer())已经有过滤器的影子在了,如果我们希望在producer和consumer之间插入其他过程对用户的输入做处理,也就是安插过滤器,那么新的过滤器也将作为fiber存在,新的fiber消费producer的输出,并输出新的结果给消费者,例如我们希望将用户的输入结果加上行号再打印,那么就插入一个称为filter的fiber:
def filter(prod)
return Fiber.new do
line=1
while true
value=receive(prod)
value=sprintf("%5d %s",line,value)
send(value)
line=line.succ
end
end
end
最终组合的调用如下:
consumer(filter(producer()))
类似unix系统那样,简单的加入新的fiber组合起来就可以为打印结果添加行号。
类似consumer(filter(producer()))的调用方式尽管已经很直观,但是我们还是希望能像unix系统那样调用,也就是通过竖线作为管道操作符:
producer | filter | consumer
这样的调用方式更将透明直观,清楚地表明整个过滤器链的运行过程。幸运的是在Ruby中支持对|方法符的重载,因此要实现这样的操作符并非难事,只要对Fiber做一层封装即可,下面给出的代码来自《Programming ruby》的作者Dave Thomas的 blog:
class PipelineElement
attr_accessor :source
def initialize
@fiber_delegate = Fiber.new do
process
end
end
def |(other)
other.source = self
other
end
def resume
@fiber_delegate.resume
end
def process
while value = input
handle_value(value)
end
end
def handle_value(value)
output(value)
end
def input
@source.resume
end
def output(value)
Fiber.yield(value)
end
end
这段代码非常巧妙,将Fiber和Ruby的功能展示的淋漓尽致。大致解说下,PipelineElement作为任何一个过滤器的父类,其中封装了一个fiber,这个fiber默认执行process,在process方法中可以看到上面生产者和消费者例子的影子,input类似receive方法调用前置过滤器(source),output则将本过滤器处理的结果作为参数传递给yield并让出执行权,让这个过滤器的调用者(也就是后续过滤器)得到结果并继续处理。PipelineElement实现了“|”方法,用于组合过滤器,将下一个过滤器的前置过滤器设置为本过滤器,并返回下一个过滤器。整个过滤链的驱动者是最后一个过滤器。
有了这个封装,那么上面生产者消费者的例子可以改写为:
class Producer < PipelineElement
def process
while true
value=readline.chomp
handle_value(value)
end
end
end
class Filter < PipelineElement
def initialize
@line=1
super()
end
def handle_value(value)
value=sprintf("%5d %s",@line,value)
output(value)
@line=@line.succ
end
end
class Consumer < PipelineElement
def handle_value(value)
puts value
end
end
现在的调用方式可以跟unix的管道很像了:
producer=Producer.new
filter=Filter.new
consumer=Consumer.new
pipeline = producer | filter | consumer
pipeline.resume
如果你打印pipeline对象,你将看到一条清晰的过滤链,
#<Consumer:0x8f08bf4 @fiber_delegate=#<Fiber:0x8f08a88>, @source=#<Filter:0x8f08db4 @line=1, @fiber_delegate=#<Fiber:0x8f08d60>, @source=#<Producer:0x8f09054 @fiber_delegate=#<Fiber:0x8f09038>>>>
Ruby Fiber指南(一)基础
Ruby Fiber指南(二)参数传递
Ruby Fiber指南(三)过滤器
Ruby Fiber指南(四)迭代器
Ruby Actor指南(五)实现Actor
这一篇其实也算是Fiber编程的基础篇,只不过参数传递算是一个比较重要的主题,因此独立一节。参数传递发生在两个Fiber之间,作为Fiber之间通讯的一个主要手段。
首先,我们可以通过resume调用给Fiber的block传递参数:
1 #resume传递参数给fiber
2 f=Fiber.new do |a,b,c|
3 p a,b,c
4 end
5
6 f.resume(1,2,3)
7
这个例子展示了怎么向fiber的block传递参数,f这个fiber简单地将传入的参数打印出来并终止。
其次,Fiber#yield也可以传递参数给调用resume作为返回结果,猜猜下面的代码打印什么?
1 #yield传递参数给resume
2 f=Fiber.new do |a,b|
3 Fiber.yield a+b,a-b
p a,b
4 end
5
6 p f.resume(10,10)
7 p f.resume(3,4)
8
正确的答案是:
[20, 0]
10
10
[10, 10]
让我们分析下代码的执行过程:
1、第6行第一次调用resume,传入10,10两个参数
2、f开始执行任务,它的任务是调用Fiber#yield,并将参数相加和相减的结果作为参数给yield,也就是执行Fiber.yield 20,10
3、f调用yield之后挂起,返回root fiber,yield的两个参数10、20作为返回结果打印。
4、第7行代码,root fiber再次调用resume并传入参数,f被切入并执行代码p a,b,打印a、b,a和b仍然是上次调用保存的10,而非resume传入的3和4。
5、f执行完毕,返回p a,b的结果作为resume结果,也就是[10,10]
刚才看到上面yield向resume传递参数的例子中第二次调用resume的参数3和4被忽略了,事实上如果还存在一次yield调用,那么3和4将被作为yield的返回结果使用,这就是我们接下来将看到的,通过resume调用传递参数作为fiber中yield的返回结果:
1 #resume传递参数给yield
2 f=Fiber.new do
3 1 + Fiber.yield
4 end
5
6 p f.resume(1)
7 p f.resume(2)
8
这次的打印结果将是:
nil
3
第一次调用resume传入的1将被忽略,因为f的block不需要参数,然后f执行1 + Fiber.yield,在yield的挂起,加法运算没有继续,因为yield的调用没有参数,因此第一次resume返回nil;第二次resume调用传入2,这时候2将作为Fiber#yield的调用结果跟1相加,完成加法运算,得到的结果就是3,这个结果作为fiber的返回值返回给调用者。
总结下上面我们谈到的四种传递参数的情形:通过resume向fiber的block传递参数、通过yield向调用者传递参数、通过resume向yield传递参数、fiber返回值传递给调用者。
Ruby Fiber指南(一)基础
Ruby Fiber指南(二)参数传递
Ruby Fiber指南(三)过滤器
Ruby Fiber指南(四)迭代器
Ruby Actor指南(五)实现Actor
这是一个Ruby Fiber的教程,基本是按照《Programming in lua》中讲述协程章节的顺序来介绍Ruby Fiber的,初步分为5节:基础、参数传递、过滤器、迭代器、应用。这是第一节,介绍下Ruby Fiber的基础知识。
Ruby 1.9引入了Fiber,通常称为纤程,事实上跟传统的coroutine——协程是一个概念,一种非抢占式的多线程模型。所谓非抢占式就是当一个协程运行的时候,你不能在外部终止它,而只能等待这个协程主动(一般是yield)让出执行权给其他协程,通过协作来达到多任务并发的目的。协程的优点在于由于全部都是用户空间内的操作,因此它是非常轻量级的,占用的资源很小,并且context的切换效率也非常高效(可以看看 这个测试),在编程模型上能简化对阻塞操作或者异步调用的使用,使得涉及到此类操作的代码变的非常直观和优雅;缺点在于容错和健壮性上需要做更多工作,如果某个协程阻塞了,可能导致整个系统挂住,无法充分利用多核优势,有一定的学习使用曲线。
上面都是场面话,先看看代码怎么写吧,比如我们写一个打印hello的协程:
1 require 'fiber'
2 f=Fiber.new do
3 p "hello"
4 end
5
6 p f.alive?
7 f.resume
8 p f.alive?
9
10 f.resume
11
附注:这里的代码都在ruby1.9.1-p378测试通过。
第一行先引入fiber库,事实上fiber库并不是必须的,这里是为了调用Fiber#alive?方法才引入。然后通过Fiber#new创建一个Fiber,Fiber#new接受一个block,block里就是这个Fiber将要执行的任务。Fiber#alive?用来判断Fiber是否存活,一个Fiber有三种状态:Created、Running、Terminated,分别表示创建完成、执行、终止,处于Created或者Running状态的时候Fiber#alive?都返回true。启动Fiber是通过Fiber#resume方法,这个Fiber将进入Running状态,打印"hello"并终止。当一个Fiber终止后,如果你再次调用resume将抛出异常,告诉你这个Fiber已经寿终正寝了。因此上面的程序输出是:
0
"hello"
false
fiber1.rb:10:in `resume': dead fiber called (FiberError)
from fiber1.rb:10:in `<main>'
眼尖的已经注意到了,这里alive?返回是0,而不是true,这是1.9.1这个版本的 一个BUG,1.9.2返回的就是true。不过在Ruby里,除了nil和false,其他都是true。
刚才提到,我们为了调用Fiber#alive?而引入了fiber库,Fiber其实是内置于语言的,并不需要引入额外的库,fiber库对Fiber的功能做了增强,具体可以先看看它的 文档,主要是引入了几个方法:Fiber#current返回当前协程,Fiber#alive?判断Fiber是否存活,最重要的是Fiber#transfer方法,这个方法使得Ruby的Fiber支持所谓全对称协程( symmetric coroutines),默认的resume/yield(yield后面会看到)是半对称的协程(asymmetric coroutines),这两种模型的区别在于“挂起一个正在执行的协同函数”与“使一个被挂起的协同再次执行的函数”是不是同一个。在这里就是Fiber#transfer一个方法做了resume/yield两个方法所做的事情。全对称协程就可以从一个协程切换到任意其他协程,而半对称则要通过调用者来中转。但是Ruby Fiber的调用不能跨线程(thread,注意跟fiber区分),只能在同一个thread内进行切换,看下面代码:
1 f = nil
2 Thread.new do
3 f = Fiber.new{}
4 end.join
5 f.resume
f在线程内创建,在线程外调用,这样的调用在Ruby 1.9里是不允许的,执行的结果将抛出异常
fiber_thread.rb:5:in `resume': fiber called across threads (FiberError)
from fiber_thread.rb:5:in `<main>'
刚才我们仅仅使用了resume,那么yield是干什么的呢?resume是使一个挂起的协程执行,那么yield就是让一个正在执行的Fiber挂起并将执行权交给它的调用者,yield只能在某个Fiber任务内调用,不能在root Fiber调用,程序的主进程就是一个root fiber,如果你在root fiber执行一个Fiber.yield,也将抛出异常:
Fiber.yield
FiberError: can't yield from root fiber
看一个resume结合yield的例子:
1 f=Fiber.new do
2 p 1
3 Fiber.yield
4 p 2
5 Fiber.yield
6 p 3
7 end
8
9 f.resume # =>打印1
10 f.resume # => 打印2
11 f.resume # =>打印3
f是一个Fiber,它的任务就是打印1,2,3,第一次调用resume时,f在打印1之后调用了Fiber.yield,f将让出执行权给它的调用者(这里就是root fiber)并挂起,然后root fiber再次调用f.resume,那么将从上次挂起的地方继续执行——打印2,又调用Fiber.yield再次挂起,最后一次f.resume执行后续的打印任务并终止f。
Fiber#yield跟语言中的yield关键字是不同的,block中的yield也有“让出”的意思, 但是这是在同一个context里,而Fiber#yield让出就切换到另一个context去了,这是完全不同的。block的yield其实是匿名函数的语法糖衣,它是切换context的,跟Fiber不同的是,它不保留上一次调用的context,这个可以通过一个例子来区分:
1 def test
2 yield
3 yield
4 yield
5 end
6 test{x ||= 0; puts x+= 1}
7
这里的test方法接受一个block,三次调用yield让block执行,block里先是初始化x=0,然后每次调用加1,你期望打印什么?
答案是:
1
1
1
这个结果刚好证明了yield是不保留上一次调用的context,每次x都是重新初始化为0并加上1,因此打印的都是1。让我们使用Fiber写同一个例子:
1 fiber=Fiber.new do
2 x||=0
3 puts x+=1
4 Fiber.yield
5 puts x+=1
6 Fiber.yield
7 puts x+=1
8 Fiber.yield
9 end
10
11 fiber.resume
12 fiber.resume
13 fiber.resume
14
执行的结果是:
1
2
3
这次能符合预期地打印1,2,3,说明Fiber的每次挂起都将当前的context保存起来,留待下次resume的时候恢复执行。因此关键字yield是无法实现Fiber的,fiber其实跟continuation相关,在底层fiber跟callcc的实现是一致的(cont.c)。
Fiber#current返回当前执行的fiber,如果你在root fiber中调用Fiber.current返回的就是当前的root fiber,一个小例子:
1 require 'fiber'
2 f=Fiber.new do
3 p Fiber.current
4 end
5
6 p Fiber.current
7 f.resume
这是一次输出:
#<Fiber:0x9bf89f4>
#<Fiber:0x9bf8a2c>
表明root fiber跟f是两个不同的Fiber。
基础的东西基本讲完了,最后看看Fiber#transfer的简单例子,两个协程协作来打印“hello world”:
1 require 'fiber'
2
3 f1=Fiber.new do |other|
4 print "hello"
5 other.transfer
6 end
7
8 f2=Fiber.new do
9 print " world\n"
10 end
11
12 f1.resume(f2)
通过这个例子还可以学到一点,resume可以传递参数,参数将作为Fiber的block的参数,参数传递将是下一节的主题。
最近重读了《Programming Lua》,对协程做了重点复习。众所周知,Ruby1.9引入了Fiber,同样是coroutine,不过Ruby Fiber支持全对称协程(通过fiber库),而Lua只支持所谓半对称协程。
这里将对Lua、LuaJIT和Ruby Fiber的切换效率做个对比测试,测试场景很简单:两个coroutine相互切换达到5000万次,统计每秒切换的次数,各测试多次取最佳。
lua的程序如下:
c1=coroutine.create(function ()
while true do
coroutine.yield()
end
end)
c2=coroutine.create(function ()
while true do
coroutine.yield()
end
end)
local start=os.clock() local count=50000000
for i=1,count do
coroutine.resume(c1)
coroutine.resume(c2)
end
print(4*count/(os.clock()-start))
考虑到在循环中事实上发生了四次切换:main->c1,c1->main,main->c2,c2->main,因此乘以4。
Ruby Fiber的测试分两种,采用transfer的例程如下: require 'fiber' require 'benchmark'
Benchmark.bm do |x|
MAX_COUNT=50000000
f1=Fiber.new do |other,count|
while count<MAX_COUNT
other,count=other.transfer(Fiber.current,count.succ)
end
end
f2=Fiber.new do |other,count|
while count<MAX_COUNT
other,count=other.transfer(Fiber.current,count.succ)
end
end
x.report{
f1.resume(f2,0)
}
end
Ruby Fiber采用resume/yield的例程如下: require 'benchmark'
f1=Fiber.new do
while true
Fiber.yield
end
end
f2=Fiber.new do
while true
Fiber.yield
end
end
COUNT=50000000
Benchmark.bm do |x|
x.report{
COUNT.times do
f1.resume
f2.resume
end
}
end
测试环境:
CPU : Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz
Memory: 3GB
System : Linux dennis-laptop 2.6.31-14-generic #48-Ubuntu SMP
Lua : 5.1.4
ruby : 1.9.1p378
LuaJIT: 1.1.5和2.0.0-beta2
测试结果如下:
| Lua | LuaJIT 1.1.5
| LuaJIT 2.0.0-beta2
| ruby-transfer
| ruby-resume/yield | 次数 | 6123698 | 9354536 | 24875620 | 825491 | 969649 |
结论:
1、lua的协程切换效率都是百万级别,luaJIT 2.0的性能更是牛叉,切换效率是原生lua的4倍,达到千万级别。
2、相形之下,Ruby Fiber的效率比较差了,十万级别。
3、Ruby使用transfer的效率比之resume/yield略差那么一点,排除一些测试误差,两者应该是差不多的,从ruby源码上看resume/yield和transfer的调用是一样的,resume还多了几条指令。
4、额外信息,Ruby Fiber和lua coroutine都只能跑在一个cpu上,这个测试肯定能跑满一个cpu,内存占用上,lua也比ruby小很多。
题外:从老家从早到晚总算折腾回了杭州,进站太早,火车晚点,提包带断,什么倒霉事也遇上了,先发个已经整理好的部分,后续仍待整理。
多道程序设计:分离进程为独立的功能
无论在协作进程还是在同一进程的协作子过程层面上,Unix设计风格都运用“做单件事并做好的方法“,强调用定义良好的进程间通信或共享文件来连通小型进程,提倡将程序分解为更简单的子进程,并专注考虑这些子进程间的接口,这至少需要通过以下三种方法来实现:
1、降低进程生成的开销(思考下Erlang的process)
2、提供方法(shellout、IO重定向、管道、消息传递和socket)简化进程间通信
3、提倡使用能由管道和socket传递的简单、透明的文本数据格式
Unix IPC方法的分类:
1、将任务转给专门程序,如system(3),popen调用等,称为shellout
2、Pipe、重定向和过滤器,如bc和dc
3、包装器,隐藏shell管线的复杂细节。
4、安全包装器和Bernstein链
5、主/从进程
6、对等进程间通信:
(1)临时文件
(2)信号
(3)系统守护程序和常规信号
(4)socket
(5)共享内存,mmap
远程过程调用(RPC)的缺憾:
1、RPC接口很难做到可显,如果不编写和被监控程序同样复杂的专用工具,也难以监控程序的行为。RPC接口和库一样具有版本不兼容的问题,由于是分布式的,因此更难被追查。
2、类型标记越丰富的接口往往越复杂,因而越脆弱。随着时间的推移,由于在接口之间传递的类型总量逐渐变大,单个类型越来越复杂,这些接口往往产生类型本体蠕变问题。这是因为接口比字符串更容易失配;如果两端程序的本体不能正确匹配,要让它们通信肯定很难,纠错更是难上加难。
3、支持RPC的常见理由是它比文本流方法允许“更丰富”的接口,但是接口的功能之一就是充当阻隔点,防止模块的实现细节彼此泄漏,因此,支持RPC的主要理由同时恰恰证明了RPC增加而不是降低了程序的全局复杂度。
Unix传统强烈赞成透明、可显的接口,这是unix文化不断坚持文本协议IPC的动力。
ESR在这里还谈到XML-RPC和SOAP等协议,认为是RPC和unix对文本流支持的一种融合,遗憾的是SOAP本身也成为一种重量级、不那么透明的协议了,尽管它也是文本协议。
线程是有害的:
线程是那些进程生成昂贵、IPC功能薄弱的操作系统所特有的概念。
尽管线程通常具有独立的局部变量栈,它们却共享同一个全局内存,在这个共享地址空间管理竞争和临界区的任务相当困难,而且成为增加整体复杂度和滋生bug的温床。除了普通的竞争问题之外,还产生了一类新问题:时序依赖。
当工具的作用不是控制而是增加复杂度的时候,最好扔掉从零开始。
微型语言:寻找歌唱的乐符
(注,这里谈的微型语言,就是现在比较热门的词汇DSL)
对软件错误模式进行的大量研究得出的一个最一致的结论是,程序员每百行程序出错率和所使用的编程语言在很大程度上是无关的。更高级的语言可以用更少的行数完成更多的任务,也意味着更少的bug。
防御设计不良微型语言的唯一方法是知道如何设计一个好的微型语言。
语言分类法:

对微型语言的功能测试:不读手册可以编写吗?
现代微型语言,要么就完全通用而不紧凑,要么就非常不通用而紧凑;不通用也不紧凑的语言则完全没有竞争力。
一些引申想法:我认为这个评判标准也可以用在任何编程语言上,以此来判断一些语言,C语言既通用又紧凑,Java是通用而不紧凑,ruby、Python之类的脚本语言也是如此,正则表达式(如果也算语言的话)是不通用而紧凑,Erlang也是通用而紧凑,awk却是既不通用也不紧凑,XSLT也可以归入不通用不紧凑的行列;Javascript是个另类,按理说它也是不通用不紧凑,说它不通用是因为它的主要应用范围还是局限在web开发的UI上,实际上Javascript也是门通用语言,但是很少会有人会用javascript去写批处理脚本,Javascript显然是不紧凑的,太多的边边角角甚至奇奇怪怪的东西需要你去注意,然而就是这样一门不通用不紧凑的语言现在却非常有前途,只能说时势所然。
设计微型语言:
1、选择正确的复杂度,要的是数据文件格式,还是微型语言?
2、扩展和嵌入语言
3、编写自定义语法,yacc和lex
4、慎用宏,宏的主要问题是滥用带来的奇怪、不透明的代码,以及对错误诊断的扰乱。
5、语言还是应用协议。
模块性:保持清晰,保持简洁
软件设计有两种方式:一种是设计得极为简洁,没有看得到的缺陷;另一种是设计得极为复杂,有缺陷也看不出来。第一种方式的难度要大得多。
模块化代码的首要特质就是封装,API在模块间扮演双重角色,实现层面作为模块之间的滞塞点,阻止各自的内部细节被相邻模块知晓;在设计层面,正是API真正定义了整个体系。
养成在编码前为API编写一段非正式书面描述的习惯,是一个非常好的方法。
模块的最佳大小,逻辑行200-400行,物理行在400-800之间。
紧凑性就是一个设计能否装入人脑中的特性。测试软件紧凑性的一个简单方法是:一个有经验的用户通常需要用户手册吗?如果不需要,那么这个设计是紧凑的。
理解紧凑性可以从它的“反面”来理解,紧凑性不等于“薄弱”,如果一个设计构建在易于理解且利于组合的抽象概念上,则
这个系统能在具有非常强大、灵活的功能的同时保持紧凑。紧凑也不等同于“容易学习”:对于某些紧凑
设计而言,在掌握其精妙的内在基础概念模型之前,要理解这个设计相当困难;但一旦理解了这个概念模型,整个视角就会改变,紧凑的奥妙也就十分简单了。紧凑也不意味着“小巧”。即使一个设计良好的系统,对有经验的用户来说没什么特异之处、“一眼”就能看懂,但仍然可能包含很多部分。
评测一个API紧凑性的经验法则是:API的入口点通常在7个左右,或者按《代码大全2》的说法,7+2和7-2的范围内。
重构技术中的很多坏味道,特别是重复代码,是违反正交性的明显例子,“重构的原则性目标就是提高正交性”。
DRY原则,或者称为SPOT原则(single
point of
truth)——真理的单点性。重复的不仅仅是代码,还包括数据结构,数据结构模型应该最小化,提倡寻找一种数据结构,使得模型中的状态跟真实世界系统的状态能够一一对应。
要提高设计的紧凑性,有一个精妙但强大的方法,就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造,将任务的核心形式化,建立明确的模型。
文本化:好协议产生好实践
文本流是非常有用的通用格式,无需专门工具就可以很容易地读写和编辑文本流,这些格式是透明的。如果担心性能问题,就在协议层面之上或之下压缩文本协议流,最终产生的设计会比二进制协议更干净,性能可能更好。使用二进制协议的唯一正当理由是:如果要处理大批量的数据,因而确实关注能否在介质上获得最大位密度,或者是非常关心将数据转化为芯片核心结构所必须的时间或指令开销。
数据文件元格式:
1、DSV风格,DElimiter-Seperated
Values
使用分隔符来分隔值,例如/etc/passwd
适合场景:数据为列表,名称(首个字段)为关键字,而且记录通常很短(小于80个字符)
2、RFC
822格式
互联网电子邮件信息采用的文本格式,使用属性名+冒号+值的形式,记录属性每行存放一个,如HTTP
1.1协议。
适合场景:任何带属性的或者与电子邮件类似的信息,非常适合具有不同字段集合而字段中数据层次又扁平的记录。
3、Cookie-jar格式。简单使用跟随%%的新行符(或者有时只有一个%)作为记录分隔符,很适用于记录非结构化文本的情况。
适合场景:词以上结构没有自然顺序,而且结构不易区别的文本段,或适用于搜索关键字而不是文本上下文的文本段。
4、Record-jar格式,cookie-jar和RFC-822的结合,形如
name:dennis
age:21
%%
name:catty
age:22
%%
name:green
age:10
这样的格式。
适合场景:那些类似DSV文件,但又有可变字段数据而且可能伴随无结构文本的字段属性关系集合。
5、XML格式,适合复杂递归和嵌套数据结构的格式,并且经常可以在无需知道数据语义的情况下仅通过语法检查就能发现形式不良损坏或错误生成的数据。缺点在于无法跟传统unix工具协作。
6、Windows
INI格式,形如
[DEFAULT]
account=esr
[python]
directory=/home/ers/cvs/python
developer=1
[sng]
directory=/home/esr/WWW/sng
numeric_id=1001
developer=1
[fetchmail]
numeric_id=4928492
这样的格式
适合场景:数据围绕指定的记录或部分能够自然分成“名称-属性对”两层组织结构。
Unix文本文件格式的约定:
1、如果可能,以新行符结束的每一行只存一个记录
2、如果可能,每行不超过80个字符
3、使用”#“引入注释
4、支持反斜杠约定
5、在每行一条记录的格式中,使用冒号或连续的空白作为字段分隔符。
6、不要过分区分tab和whitespace
7、优先使用十六进制而不是八进制。
8、对于复杂的记录,使用“节(stanza)”格式,要么让记录格式和RFC
822电子邮件头类似,用冒号终止的字段名关键字作为引导字段。
9、在节格式中,支持连续行,多个逻辑行折叠成一个物理行
10、要么包含一个版本号,要么将格式设计成相互独立的的自描述字节块。
11、注意浮点数取整。
12、不要仅对文件的一部分进行压缩或者二进制编码。
应用协议元格式
1、经典的互联网应用元协议
RFC
3117《论应用协议的设计》,如SMTP、POP3、IMAP等协议
2、作为通用应用协议的HTTP,在HTTP上构建专用协议,如互联网打印协议(IPP)
3、BEEP:块可扩展交换协议,既支持C/S模式,又支持P2P模式
4、XMP-RPC、SOAP和Jabber,基于XML的协议。
透明性:来点光
美在计算科学中的地位,要比在其他任何技术中的地位都重要,因为软件是太复杂了。美是抵御复杂的终极武器。
如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。
如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而设计,这个软件系统就是可显的。
不要让调试工具仅仅成为一种事后追加或者用过就束之高阁的东西。它们是通往代码的窗口:不要只在墙上凿出粗糙的洞,要修整这些洞并装上窗。如果打算让代码一直可被维护,就始终必须让光照进去。例如fetchmail的-v选项将处理SMTP、POP的处理过程打印到标准输出,使得fetchmail行为具有可显性。
在“这个设计能行吗?”之后要提出的头几个问题就是“别人能读懂这个设计吗?这个设计优雅吗?”我们希望,此时大家已经很清楚,这些问题不是废话,优雅不是一种奢侈。在人类对软件的反映中,这些品质对于减少软件bug和提高软件长期维护性是最基本的。
要追求代码的透明性,最有效的方法是很简单,就是不要在具体操作的代码上叠放太多的抽象层。
OO语言使得抽象变得容易——也许是太容易了。OO语言鼓励“具有厚重的胶合和复杂层次”的体系。当问题领域真的很复杂,确实需要大量抽象时,这可能是好事,但如果coder到头来用复杂的办法做简单的事情——仅仅是为他们能够这样做,结果便适得其反。
所有的OO语言都显示出某种使程序员陷入过度分层陷阱的倾向。对象框架和对象浏览器并不能代替良好的设计和文档,但却常常被混为一谈。过多的层次破坏了透明性:我们很难看清这些层次,无法在头脑中理清代码到底是怎样运行的。简洁、清晰和透明原则通通被破坏了,结果代码中充满了晦涩的bug,始终存在维护问题。
胶合层中的“智能数据”却经常不代表任何程序处理的自然实体——仅仅只是胶合物而已(典型现象就是抽象类和混入(mixin)类的不断扩散)
OO抽象的另一个副作用就是程序往往丧失了优化的机会。
OO在其取得成功的领域(GUI、仿真和图形)之所以能成功,主要原因之一可能是因为在这些领域里很难弄错类型的本体问题。例如,在GUI和图形系统中,类和可操作的可见对象之间有相当自然的映射关系。
Unix风格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相组合。
太多的OO设计就像是意大利空心粉一样,把“is-a”和“have
a”的关系弄得一团糟,或者以厚胶合层为特征,在这个胶合层中,许多对象的存在似乎只不过是为了在陡峭的抽象金字塔上占个位置罢了。这些设计都不透明,它们晦涩难懂并且难以调试。
为透明性和可显性而编码:
1、程序调用层次中(不考虑递归)最大的静态深度是多少?如果大于四,就要当心。
2、代码是否具有强大、明显的不变性质(约束)?不变性质帮助人们推演代码和发现有问题的情况。
3、每个API中各个函数调用是否正交?或者是否存在太多的magic
flags或者模式位?
4、是否存在一些顺手可用的关键数据结构或全局唯一的记录器,捕获系统的高层次状态?这个状态是否容易被形象化和检验,还是分布在数目众多的各个全局变量或对象中而难以找到?
5、程序的数据结构或分类和它们所代表的外部实体之间,是否存在清晰的一对一映射。
6、是否容易找到给定函数的代码部分?不仅单个函数、模块,还有整个代码,需要花多少精力才能读懂?
7、代码增加了特殊情况还是避免了特殊情况?每一个特殊情况可能对任何其他特殊情况产生影响:所有隐含的冲突都是bug滋生的温床。然而更重要的是,特殊情况使得代码更难理解。
8、代码中有多少个magic
number?通过审查是否很容易查出代码中的限制(比如关键缓冲区的大小)?
隐藏细节和无法访问细节有着重要区别。不要过度保护。
无论何时碰到涉及编辑某类复杂二进制对象的设计问题时,unix传统都提倡首先考虑,是否能够编写一个能够在可编辑的文本格式和二进制格式之间来回进行无损转换的工具?这类工具可称为文本化器(textualizer).
宁愿抛弃、重建代码也不愿修补那些蹩脚的代码。
“代码是活代码、睡代码还是死代码?”活代码周围存在一个非常活跃的开发社团。睡代码之所以“睡着”,经常是因为对作者而言,维护代码的痛苦超过了代码本身的效用。死代码则是睡得太久,重新实现一段等价代码更容易。
Unix哲学是自下而上,而不是自上而下的,注重实效,立足于丰富的经验,你不会在正规方法学和标准中找到它。
Unix管道的发明人Doug
McIlroy曾经说过:
1、让每个程序就做好一件事,如果有新任务就重新开始,不要往新程序中加入功能而搞的复杂。
2、假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序是未知的。输出中不要有无关的信息干扰,避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。
3、尽可能早将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下应该是几星期内,对抽劣的代码别犹豫,扔掉重写。
4、优先使用工具,而非拙劣的帮助来减轻编程任务的负担,工欲善其事,必先利其器。
Rob
Pike在《Notes on C
programming》中提到:
原则1:你无法断行程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
原则2:估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
原则3:花哨的算法,在n很小的适合通常很慢,而n通常很小。花哨算法的常数复杂度很大,除非你确定n一直很大,否则不要用花哨算法(即使n很大,也要优先考虑原则2)。
原则4:花哨的算法比简单的算法更容易出bug
,更难实现。尽量使用简单的算法配合简单的数据结构。
原则5:数据压倒一切。如果已经选择了正确的数据结构并且把一切组织得井井有条,正确的算法也就不言自明,编程的核心是数据结构,而不是算法。
原则6:没有原则6.
Ken
Thompson对原则4做了强调:
拿不准就穷举。
Unix哲学的17条原则:
1、模块原则:简洁的接口拼合简单的部件。
2、清晰原则:清晰胜于机巧。
3、组合原则:设计时考虑拼接组合。
4、分离原则:策略同机制分离,接口同引擎分离。
5、简洁原则:设计要简洁,复杂度能低则低。
6、吝啬原则:除非却无他法,不要编写庞大的程序。
7、透明性原则:设计要可见,以便审查和调试。
8、健壮原则:健壮源于透明与简洁。
9、表示原则:把知识叠入数据,以求逻辑质朴而健壮。
10、通俗原则:接口设计避免标新立异。
11、缄默原则:如果一个程序没什么好说的,就沉默。
12、补救原则:出现异常时,马上退出,并给出足够错误信息。
13、经济原则:宁花机器一分,不花程序员一秒。
14、生成原则:避免手工hack,尽量编写程序去生成程序。
15、优化原则:雕琢前先要有原型,跑之前先学会走。
16、多样原则:绝不相信所谓“不二法门”的断言。
17、扩展原则:设计着眼未来,未来总是比预想来得快。
Unix哲学之一言以蔽之:KISS
Keep
it simple,stupid!
应用unix哲学:
1、只要可行,一切都应该做成与来源和目标无关的过滤器。
2、数据流应尽可能的文本化(这样可以用标准工具来查看和过滤)。
3、数据库部署和应用协议应尽可能文本化(让人阅读和编辑)。
4、复杂的前端(用户界面)和后端应该泾渭分明。
5、如果可能,用c编写前,先用解释性语言搭建原型。
6、当且仅当只用一门编程语言会提高程序复杂度时,混用语言编程才比单一语言编程来得好。
7、宽收严发(对接收的东西要包容,对输出的东西要严格)
8、过滤时,不需要丢弃的消息绝不丢。
9、小就是美。在确保完成任务的基础上,程序功能尽可能的少。
最后强调的是态度:
要良好地运用unix哲学,你就应该不断地追求卓越,你必须相信,程序设计是一门技艺,值得你付出所有的智慧、创造力和激情。否则,你的视线就不会超越那些简单、老套的设计和实现;你就会在应该思考的时候急急忙忙跑去编程。你就会在该无情删繁就简的时候反而把问题复杂化——然后你还会反过来奇怪你的代码怎么会那么臃肿,那么难以调试。
要良好地运用unix哲学,你应该珍惜你的时间绝不浪费。一旦某人已经解决了某个问题,就直接拿来利用,不要让骄傲或偏见拽住你又去重做一遍。永远不要蛮干;要多用巧劲,省下力气在需要的时候用,好钢用到刀刃上。善用工具,尽可能将一切自动化。
软件设计和实现是一门充满快乐的艺术,一种高水平的游戏。如果这种态度对你来说听起来有些荒谬,或者令你隐约感到有些困窘,那么请停下来,想一想,问问自己是不是已经把什么给遗忘了。如果只是为了赚钱或者打发时间,你为什么要搞软件设计,而不是别的什么呢?你肯定曾经也认为软件设计值得你付出激情……
要良好地运用unix哲学,你需要具备(或者找回)这种态度。你需要用心。你需要去游戏。你需要乐于探索。
操作系统的风格元素:
1、什么是操作系统的统一性理念
2、多任务能力
3、协作进程(IPC)
4、内部边界
5、文件属性和记录结构
6、二进制文件格式
7、首选用户界面风格
8、目标受众
9、开发的门坎
经常在china-pub上买书,我的账号早已经到五星,再加上china-pub上很多新书首发,因此尽管当当网有时候更便宜,还是经常在china-pub买。不过这次我要出离愤怒了,同样是上个月29号下的单,当当在周一就送到了,而china-pub到今天5号竟然还没有送到,看订单信息是货已出库,并且发货时间在1月31号,从北京到杭州走了5天竟然还没到,选的什么快递公司。
不到也还罢了,更可恶的是售后服务,我在china-pub的客服论坛发帖,他们自己承诺工作时间内60分钟回复,回复个P啊,从昨天到今天没见一个人点击我的帖子,更何谈回复。OK,论坛不行,那么我发邮件吧,从china-pub的 客服服务页的客服email进去,填写表单,OK,提交失败?为什么,没有验证码,可是你TMD根本没显示验证码啊,这是什么狗屁程序,见下图
你看到验证码在哪里吗?刷新N遍愣是没出来,多牛B的客服email啊。
线上不行,那么我打电话可以吧,这电话是普通长途也还罢了,我自己掏钱没事,可总得有人接吧,事实是我早上打了3个电话,两个查询订单,等了N久没人接,靠,那我投诉吧,转投诉,一样没人接,我只能说china-pub你们真牛气,你们的客服是摆设不成?你们这么牛气,我也不敢买了,惹不起我还躲不起啊。
怎么让你对象跟Array或者Hash一样,可以使用[ ]操作符来获取属性值或者赋值? 问题其实就是如何定义 index操作符,在Ruby中可以这样做:
class Message
def initialize
@props=Hash.new
end
def [](key)
@props[key]
end
def []=(key,value)
@props[key]=value
end
end
m=Message.new
m[0]=1
p m[0]
m[:a]="hello"
p m[:a]
注意方法签名。
通讯层的改造使用了 google protocol buffers作为协议体,效率还是挺让人满意。编辑以.proto结尾的语法文件,没有语法高亮很不习惯,幸好protocolbuf提供了vim和emacs的扩展。下载非win32版本的protocol buffers的压缩包里,解压后有个editors目录,里面就是两个扩展文件:proto.vim是提供给vim爱好者的,而 protobuf-mode.el就是提供给emacs控的。
安装很简单,将protobuf-mode.el加入你的Emacs加载路径,然后在.emacs配置文件里加上这么两行代码:
(require 'protobuf-mode)
(setq auto-mode-alist (cons '(".proto$" . protobuf-mode) auto-mode-alist))
require是不够的,第二行将自动把.proto结尾的打开文件以protobuf-mode模式运行。运行时截图:
工具栏上多了个ProtocolBuffers菜单,有一些简单功能,如注释某段代码,代码跳转等等。
Java Memcached Client—— Xmemcached的新版本 1.2.2正式released。这个小版本最主要的改进是允许遍历所有在memcached中的key,这是通过stats协议实现,具体信息可以看 这里。
1.2.2的主要改进如下:
1、添加一个 KeyIterator接口,这个迭代器接口用于遍历memcached中的所有key。由于是基于stats协议实现的,因此这个迭代过程 并非高效,请 慎重使用,并且迭代返回的key也并非实时,而是 当前快照。KeyIterator目前 仅在文本协议下可用,使用例子如下:
MemcachedClient client=
KeyIterator it=client.getKeyIterator(AddrUtil.getOneAddress("localhost:11211"));
while(it.hasNext())
{
String key=it.next();
}
2、添加一个新类 net.rubyeye.xmemcached.Counter,用于封装原始的incr/decr方法,提供类似AtomicLong原子类的API方便计数器的使用:
Counter counter=client.getCounter("counter",0);
counter.incrementAndGet();
counter.decrementAndGet();
counter.addAndGet(-10);
3、修复BUG,如 issue 71,issue 72,issue 70 etc.
4、声明废弃 net.rubyeye.xmemcached.buffer.BufferAllocator,现在哪怕你设置了这一属性也将被忽略,这个类将在以后的某个版本中移除
5、升级 yanf4j到1.1.0
Wiki和 用户指南都已经更新,欢迎使用并反馈任何建议或者bug报告。
项目主页: http://code.google.com/p/xmemcached/
下载地址: http://code.google.com/p/xmemcached/downloads/list
利用这个 小工具,可以生成豆瓣上记录的每年读的书,看的电影,听的音乐。我的记录,看过的书
看过的电影:
《Joel on software》谈到所谓抽象漏洞,简单来说就是抽象能解决90%的一般情况,而其他10%的情况你仍然需要跟抽象层面下的细节打交道,也就是抽象本身只能减少你的工作时间,而无法减少你的学习时间。道理简单,举几个例子。
以SQL语言为例,SQL是所谓说明性的语言,你所写的语句只是一条what,而how是如何做的无需关心,但是真的无需关心吗?事实上是不行,低效的SQL语句对数据库的性能损害非常大,作为程序员你需要知道SQL这个抽象层次下的部分内容,知道数据库是怎么执行这些语句,知道如何去避免一些最差实践。再比如分布式调用希望做到能跟本地调用一样的透明,但实际上还是不行的,网络的不确定性让RPC调用根本无法做到的类似本地调用那样的透明性。隐藏在RPC这个抽象层次下的网络通信细节,你不能不去care。抽象能帮你解决大多数情况,提高你的工作效率,但是剩下的一公里问题,仍然需要你花费更多时间和精力去了解并解决。这事实上也是一个优秀程序员跟普通程序的差别之一,学习了java编程,知道了collection集合框架,不代表你无需再去学习数据结构和算法。
Joel将这个现象称为漏洞抽象。事实上,我并不认为这是抽象本身的漏洞,这反而是软件的本质复杂性在作怪,抽象只能去化简偶然复杂性,例如函数、类、模块化等手段去组织代码,而本质的复杂性是无法避免的。举个不是那么恰当的例子,例如我们有这么个方法,传进一个参数list,我们要遍历list做一些事情,(我知道用迭代器才是正途,先允许我犯这么个错误),你可能这么写:
public void doSomething(List<String> list){
for(int i=0;i<list.size();i++){
String str=list.get(i);
//do something
}
}
这样的代码我估计在1.5有for语句增强之前不少人都写过,这样的代码有什么问题呢?考虑下list是ArrayList和LinkedList这两种情况,List是链表的抽象,但是链表的实现形式却是可以用数组或者引用链接,链表的实现形式不同,List.get(index)这个方法的效率会很成问题。我们都知道ArrayList适宜于随机访问,而LinkedList方便插入添加移除,在这个doSomething方法中,显然随机访问的诉求大于添加移除。在通常情况下,这样写都不会成为问题,但是如果这个doSomething方法被经常调用,并且list是一个LinkedList的情况下,这个方法就很可能成为性能瓶颈。我们寄希望于List这个接口可以让我们无需关心list的具体实现,然而现实是你仍然需要知道各种实现的区别和原理,这就是所谓漏洞抽象。这并非抽象的无力,你肯定不会反对“针对接口编程”这条原则,而是抽象本身解决不了本质复杂性,这里的本质复杂性就是链表的实现方法,随机访问与添加移除的平衡问题。在我们无法找到更好的链表实现方法来平衡随机访问与添加移除之前,这个本质复杂性就不是抽象能够解决的。
同样的现象出现在String、StringBuffer、StringBuilder的使用上,字符串的实现方法你仍然需要知道,这是绕不过去的本质复杂性。这里谈到的本质复杂性根本上也是现实世界的本质复杂性的反映,扯远些就更虚了。就现实的工作情况来看,不知道其他人有没有这样的经验,就是在自以为解决某个难题的时候,最后却发现难题以另一个面目出现,问题本身没有得到解决,只是以更好的方式被掩盖了。
无论是过程式、OO、函数式编程,解决的问题都是为了更好的抽象,抽象是个好东西,但是抽象无法解决那些本质性的问题,因此《人月神话》断言没有银弹,我们仍然需要跟狼人作战。
你不得不承认,写代码的效率跟周期性的情绪相关。以我为例,总存在着周期性的情绪波动,那段时间内基本不想写代码,上班就是收收邮件,看看网页,遗憾的是每个月都有那么几天。事实上,我认为在一天8小时的工作中,能有2、3个小时能达到忘我状态的工作,那已经是非常不错的事情。如果你是程序员,你肯定知道我说的忘我状态是什么。我在这里说的局限了,其实任何工作都可能进入这种忘我状态,这种状态下你的思维非常活跃,全神贯注,哪怕有人跟你说话你也会听而不闻,这种状态在你读一本非常有趣的小说的时候也会出现。这种状态下的你效率会非常高,例如我前段时间内就在一周内写了13000多行代码,600多个测试用例,为我们的系统重新实现了一个通信层。
看过很多讨论程序员工作效率的文章,据称研究表明要进入这种状态是至少要15分钟的时间,因此频繁地打断工作会阻碍你的工作效率,毕竟酝酿情绪也是需要时间的嘛。我有思考过怎么去尽量多地保持这种状态,排除那种对工作厌烦的情绪,毕竟拿着工资不干活心里还是会不安,况且看到周围那么多高效率的人,压力是难免的,让人担心的不是每天只有两个小时的高效工作,而是那段什么都不想干的时间。最后让我发现一个方法,说起来很简单,就是在出现这种低效状态的时候,强迫自己打开eclipse,而不是 firefox,强迫自己去写几行代码,如果这段时间内没有被其他事情打断,那么你还是容易进入一种不那么高效和愉悦的工作状态,至少能做到专心致志。当然,跟自己的情绪对抗可能不是世界上最困难的事情,也是其中之一,不过请你相信,只要你打开eclipse开始写代码并进入思考状态,那么你至少是可以暂时遗忘那些负面情绪的,甚至你的情绪可能因为解决了某个难题而高昂起来。
这个方法肯定不是什么新发现,我估计很多人会有同样的经验,今早在看《joel说软件》其中一篇文章《开火与运动》也谈到了同样的问题,joel也提到相同的经验:开了头就好。你不知道要费多少劲才能将一辆带齿轮的山地车运转起来,不过一旦它转起来之后,一切都跟骑一辆没有齿轮的自行车没什么两样。Joel还延伸了更多,开火的策略不仅仅是工作效率的问题,也是竞争策略,当你向敌人开火的时候,同时向敌人靠过去,活力会迫使敌人低下头而不能向你开火。竞争也是如此,压迫性的不断推出新东西让你的竞争对手疲于奔命,反而遗忘产品的根本性的目的,这些新东西可能只是为了替换过去不易用的东西,为什么不易用的东西在过去也会被推出来?那只是了为每天进步不断开火,让敌人忘记开火。
趁着下班前的半小时,回顾下2009年我都干了什么,有什么收获,有什么不足。
09年最重要的事情是我的儿子出生了,小家伙的到来带给全家很多欢乐,烦恼也不少,比如半夜总要被吵醒,晚上的读书也没办法那么专心读了。此外,我还在学习怎么当爸爸,写过这篇《 新爸爸指南》,记录下新生儿遇到的种种问题,逐渐经历自己生命的又一个阶段,这个历程很美好。
年初从广州公司辞职后,到了厦门一家创业公司,这不是一次很愉快的经历,回想起来我的问题不小。首先不该贸然地想去转换一个语言平台,写C++实在不是很好的编程的体验,乃至于我根本提不起工作热情;其次,心态不成熟,遇到问题和困扰的时候还是比较被动地解决,事实上完全没必要搞成这样,主动提出并且离开公司并不是什么丢人的事情。这次经历告诉我做决定的时候最好再慎重一点,毕竟自己不是一个人了,养家糊口是实实在在的责任。
在厦门的失败经历后,我投简历到了淘宝,尽管对于待遇并不是很满意,出于对淘宝的向往和有点理想主义的小情怀还是来到了杭州。刚来的时候,工作很顺利,生活比较糟糕,老婆孩子接到杭州后才好了点,生活比较有规律了。在淘宝,我所做的仍然是开发,写代码还是我的最爱,不过做的离业务的比较远,这正符合我的期望。负责的是一个消息中间件的开发,这个产品本身已经成型,并且应用在了淘宝的核心系统当中,现在每天通过这个MQ发送的消息量已经接近两亿,整个系统拥有数个集群,近30台机器。工作不单纯是开发,包括一些方案的设计和日常的维护工作,总体来讲还是很愉悦的体验。不足的地方,我仍然还是将自己视为一个纯粹的技术人员,对淘宝本身的业务、对其他系统的架构设计的了解都比较少,甚至于认识的人还是很局限,不过这个跟我的性格有关了。
技术上,这一年自我感觉没多大进步,除了将 sicp读完之外(我准备再度几遍),一些零零散散的技术书籍也看了不少,很少留下深刻的印象,比较有价值的是《 卓有成效的程序员》和《 C++网络编程》上下两卷。前者使我开始有意识地将自己一些重复性的工作自动化,提高自己的工作效率,后者让我对网络框架的设计模式有了相对全面的认识,也促进了我对Java网络编程的认识。今年也开发了个Java Memcached Client—— Xmemcached,并在大家的鼓励下持续地在改进,总算有不少用户在用,没有枉费精力和时间,也算今年的一个小小自得的地方。这里要特别感谢下曹晓刚,没有他的鼓励和他们公司的使用, xmc还只是个人玩具。09年下半年又将不少精力放在了Erlang,过去学习是跟风,这次总算在项目中了有了个小应用,并且将《Erlang程序设计》和OTP设计原则来回读了几遍,对Erlang的兴趣越来越大,甚至于想是不是该去找份专职做Erlang的工作。对技术的学习,我还是没有一个明确的规划,任凭兴趣在几个领域里转来转去,这不是好现象,明年希望能更有计划和针对性地去学习,能跟自己的工作契合得更紧密一些。明年也希望能将《算法导论》读完,今年读了1/4,发现我的数学都已经抛到了Java国了,算法复杂度的推导总是看不懂,因此又去搞了几本数学书,从头再看看。
回顾完了,说说明年的愿望:
技术上:读完《算法导论》,继续深入Erlang,探索Erlang在工作中的实际应用,加强对其他系统的了解以及大型网站构建方面的学习
生活上:希望能全家一起去旅游一次,希望能将老爸老妈接过来玩一段时间。
昨天收到一个xmc的issue报告,大概的意思是将 Xmemcached与spring 2.5集成没有任何问题,但是将spring升级到3.0就会抛出一个异常,并且spring容器无法正常启动,异常信息类似“ Couldn't find a destroy method named 'shutdown' on bean XMemcachedClientFactoryBean”。更详细的情况可以看这里,这是这位朋友分析的结果,简单来说就是spring 3.0对于查找destroy method为空的情况处理不同了,过去是打个日志,现在是抛出一个异常。
问题说完,这里主要是介绍下这个问题的解决方式,事实上Xmemcached有一个没有被文档化的Spring配置方式,没有写入文档的主要考虑是以为wiki介绍的第一种方式已经足够,而builder的方式相对繁琐一些。通过XmemcachedClientBuilder的这个factory bean的factory-method,也就是build方法来构建MemcachedClient,这就可以绕开spring 3.0的这个问题。一个示范配置如下:
<bean name="memcachedClientBuilder" class="net.rubyeye.xmemcached.XMemcachedClientBuilder">
<constructor-arg>
<list>
<bean class="java.net.InetSocketAddress">
<constructor-arg>
<value>localhost</value>
</constructor-arg>
<constructor-arg>
<value>12000</value>
</constructor-arg>
</bean>
</list>
</constructor-arg>
</bean>
<bean name="memcachedClient" factory-bean="memcachedClientBuilder"
factory-method="build" destroy-method="shutdown" />
memcachedClientBuilder作为一个factory-bean,接受一个InetSocketAddress列表作为构造函数传入,最后MemcachedClient就可以通过factory-method——也就是build方法创建了。
多个节点情况下,可能你想设置权重,那么传入memcachedClientBuilder的第二个构造函数 参数权重数组即可:
<bean name="memcachedClientBuilder" class="net.rubyeye.xmemcached.XMemcachedClientBuilder">
<constructor-arg>
<list>
<bean class="java.net.InetSocketAddress">
<constructor-arg>
<value>localhost</value>
</constructor-arg>
<constructor-arg>
<value>12000</value>
</constructor-arg>
</bean>
<bean class="java.net.InetSocketAddress">
<constructor-arg>
<value>localhost</value>
</constructor-arg>
<constructor-arg>
<value>12001</value>
</constructor-arg>
</bean>
</list>
</constructor-arg>
<constructor-arg>
<list>
<value>1</value>
<value>2</value>
</list>
</constructor-arg>
</bean>
<bean name="memcachedClient" factory-bean="memcachedClientBuilder"
factory-method="build" destroy-method="shutdown" />
上面的例子将localhost:12000的权重设置为1,而localhost:12001的权重设置为2。除了这些配置外,XmemcachedClientBuilder还有其他选项,如配置一致性哈希算法、连接池等,完整的配置例子如下:
<bean name="memcachedClientBuilder" class="net.rubyeye.xmemcached.XMemcachedClientBuilder">
<!-- XMemcachedClientBuilder have two arguments.First is server list,and second is weights array. -->
<constructor-arg>
<list>
<bean class="java.net.InetSocketAddress">
<constructor-arg>
<value>localhost</value>
</constructor-arg>
<constructor-arg>
<value>12000</value>
</constructor-arg>
</bean>
<bean class="java.net.InetSocketAddress">
<constructor-arg>
<value>localhost</value>
</constructor-arg>
<constructor-arg>
<value>12001</value>
</constructor-arg>
</bean>
</list>
</constructor-arg>
<constructor-arg>
<list>
<value>1</value>
<value>2</value>
</list>
</constructor-arg>
<property name="connectionPoolSize" value="2"></property>
<property name="commandFactory">
<bean class="net.rubyeye.xmemcached.command.TextCommandFactory"></bean>
</property>
<property name="sessionLocator">
<bean class="net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator"></bean>
</property>
<property name="transcoder">
<bean class="net.rubyeye.xmemcached.transcoders.SerializingTranscoder" />
</property>
</bean>
<!-- Use factory bean to build memcached client -->
<bean name="memcachedClient" factory-bean="memcachedClientBuilder"
factory-method="build" destroy-method="shutdown"/>
最近读数理逻辑方面的资料,感慨自己没有早点接触逻辑学,如果在小学初中阶段就能有逻辑学的系统训练,我想我对数学的理解会完全不一样,对定理证明的思路也将大大开拓。中学时代对数学充满热情,基本上老师讲的都满足不了我,我的学习远远在课堂前面,初中的时候我已经接触了微积分、概率论、立体几何之类,教材是从我舅舅用过的教材垃圾堆里淘出来的,那时候我外婆准备卖了那堆书,就叫我去看看有没有想要的,印象中从那里掏出了不少好东西,武侠小说、马克思恩格斯全集、毛选、数学语文教材之流,在老家那个阅读资料极度缺少的地方,这些书成了我整个初中的精神食粮。上了高中后,家也搬到县城,离书城近了,并且学校有图书馆,更是如鱼得水。扯的太远,回到题目,以数学定理的证明为例,我们熟知的直接证明、反证法、数学归纳法、间接证明、存在性证明、唯一性证明其实背后都有牢固的逻辑定律在支撑,直接证明其实就是蕴含 p-> q,而反证法的原理是¬¬p ↔ p,间接证明则是原命题与逆否命题等价 p-> q ↔ ¬q -> ¬p等等。如果在教授数学的同时,教授给学生背后的逻辑原理,那么将极大地开阔学生的视野并且让数学证明变的有趣。数理逻辑不仅仅对数学学习有益,对软件开发同样有帮助,例如以谓词和量词来精确地描述和分析系统规格,推理逻辑的使用也能启发你的设计思路,帮你找到一个复杂的设计的等价物。此外,你能更快地比别人做出某些逻辑难题,这也挺有趣。
Xmemcached 1.2.1正式发布,这是1.2.0发布以来的第一个小版本,主要改进是修复BUG、内部重构以及添加一些新特性。主要改进如下:
1、为Kestrel 1.2添加delete方法支持,kestrel 1.2发布后正式支持memcached delete协议
2、添加了一个新的序列化转换器 net.rubyeye.xmemcached.transcoders.TokyoTyrantTranscoder,专门提供给使用xmemcached连接Tokyo Tyrant的用户,这个转换器默认在value前加上4个字节的flag,因为Tokyo Tyrant不支持flag,所以默认无法存储除String之外的Java序列化类型。
3、添加两个新选项:
Transcoder.setCompressionThreshold(threshold)
Transcoder.setCompressionThreshold(threshold)
Transcoder接口添加了setCompressionThreshold用于设置压缩阀值,序列化后的value如果超过这个阀值将启用压缩,默认阀值是16K。
MemcachedClient.setSanitizeKeys(true|false)
MemcachedClient.setSanitizeKeys(true|false)
setSanitizeKeys用于决定是否启用URLEncoding来编码key,如果你用url作为key存储,这一特性能方便你的使用,默认为开启。
4、添加中文用户指南,比较完整的使用说明和选项说明,在线阅读。
5、内部优化,移除一些老代码和一些在1.1中被声明为Deprecated的方法。添加了更多单元测试。
6、BUG修复和对binary协议实现的部分优化。
项目主页:http://code.google.com/p/xmemcached/
下载地址: http://code.google.com/p/xmemcached/downloads/list
欢迎试用和反馈。
最近在锋爷的建议下开始读rabbitmq的源码,锋爷说这个项目已经很成熟,并且代码也很有借鉴和学习的意义,在自己写erlang代码之前看看别人是怎么写的,可以少走弯路,避免养成一些不好的习惯,学习一些最佳实践。读了一个星期,这个项目果然非常棒,代码也写的非常清晰易懂,一些细节的处理上非常巧妙,比如我这里想分享的网络层一节。
Rabbitmq是一个MQ系统,也就是消息中间件,它实现了AMQP 0.8规范,简单来说就是一个TCP的广播服务器。AMQP协议,你可以类比JMS,不过JMS仅仅是java领域内的API规范,而AMQP比JMS更进一步,它有自己的wire-level protocol,有一套可编程的协议,中立于语言。简单介绍了Rabbitmq之后,进入正题。
Rabbitmq充分利用了Erlang的分布式、高可靠性、并发等特性,首先看它的一个结构图:
这张图展现了Rabbitmq的主要组件和组件之间的关系,具体到监控树的结构,我画了一张图:
顶层是rabbit_sup
supervisor,它至少有两个子进程,一个是rabbit_tcp_client_sup,用来监控每个connection的处理进程
rabbit_reader的supervisor;rabbit_tcp_listener_sup是监控tcp_listener和
tcp_acceptor_sup的supervisor,tcp_listener里启动tcp服务器,监听端口,并且通过tcp_acceptor_sup启动N个tcp_accetpor,tcp_acceptor发起accept请求,等待客户端连接;tcp_acceptor_sup负责监控这些acceptor。这张图已经能给你一个大体的印象。
讲完大概,进入细节,说说几个我觉的值的注意的地方:
1、 tcp_accepto.erl,r对于accept采用的是异步方式,利用 prim_inet:async_accept/2方
法,此模块没有被文档化,是otp库内部使用,通常来说没必要使用这一模块,gen_tcp:accept/1已经足够,不过rabbitmq是广播程
序,因此采用了异步方式。使用async_accept,需要打patch,以使得socket好像我们从gen_tcp:accept/1得到的一样:
handle_info({inet_async, LSock, Ref, {ok, Sock}},
State = #state{callback={M,F,A}, sock=LSock, ref=Ref}) ->
%%这里做了patch
%% patch up the socket so it looks like one we got from
%% gen_tcp:accept/1
{ok, Mod} = inet_db:lookup_socket(LSock),
inet_db:register_socket(Sock, Mod),
try
%% report
{Address, Port} = inet_op(fun () -> inet:sockname(LSock) end),
{PeerAddress, PeerPort} = inet_op(fun () -> inet:peername(Sock) end),
error_logger:info_msg("accepted TCP connection on ~s:~p from ~s:~p~n",
[inet_parse:ntoa(Address), Port,
inet_parse:ntoa(PeerAddress), PeerPort]),
%% 调用回调模块,将Sock作为附加参数
apply(M, F, A ++ [Sock])
catch {inet_error, Reason} ->
gen_tcp:close(Sock),
error_logger:error_msg("unable to accept TCP connection: ~p~n",
[Reason])
end,
%% 继续发起异步调用
case prim_inet:async_accept(LSock, -1) of
{ok, NRef} -> {noreply, State#state{ref=NRef}};
Error -> {stop, {cannot_accept, Error}, none}
end;
%%处理错误情况
handle_info({inet_async, LSock, Ref, {error, closed}},
State=#state{sock=LSock, ref=Ref}) ->
%% It would be wrong to attempt to restart the acceptor when we
%% know this will fail.
{stop, normal, State};
2、 rabbitmq内部是使用了多个并发acceptor,这在高并发下、大量连接情况下有效率优势, 类似java现在的nio框架采用多个reactor类似,查看tcp_listener.erl:
init({IPAddress, Port, SocketOpts,
ConcurrentAcceptorCount, AcceptorSup,
{M,F,A} = OnStartup, OnShutdown, Label}) ->
process_flag(trap_exit, true),
case gen_tcp:listen(Port, SocketOpts ++ [{ip, IPAddress},
{active, false}]) of
{ok, LSock} ->
%%创建ConcurrentAcceptorCount个并发acceptor
lists:foreach(fun (_) ->
{ok, _APid} = supervisor:start_child(
AcceptorSup, [LSock])
end,
lists:duplicate(ConcurrentAcceptorCount, dummy)),
{ok, {LIPAddress, LPort}} = inet:sockname(LSock),
error_logger:info_msg("started ~s on ~s:~p~n",
[Label, inet_parse:ntoa(LIPAddress), LPort]),
%%调用初始化回调函数
apply(M, F, A ++ [IPAddress, Port]),
{ok, #state{sock = LSock,
on_startup = OnStartup, on_shutdown = OnShutdown,
label = Label}};
{error, Reason} ->
error_logger:error_msg(
"failed to start ~s on ~s:~p - ~p~n",
[Label, inet_parse:ntoa(IPAddress), Port, Reason]),
{stop, {cannot_listen, IPAddress, Port, Reason}}
end.
这里有一个技巧,如果要循环N次执行某个函数F,可以通过lists:foreach结合lists:duplicate(N,dummy)来处理。
lists:foreach(fun(_)-> F() end,lists:duplicate(N,dummy)).
3、 simple_one_for_one策略的使用,可以看到对于tcp_client_sup和tcp_acceptor_sup都采用了simple_one_for_one策略,而非普通的one_fo_one,这是为什么呢?
这牵扯到simple_one_for_one的几个特点:
1)simple_one_for_one内部保存child是使用dict,而其他策略是使用list,因此simple_one_for_one更适合child频繁创建销毁、需要大量child进程的情况,具体来说例如网络连接的频繁接入断开。
2)使用了simple_one_for_one后,无法调用terminate_child/2 delete_child/2 restart_child/2
3)start_child/2
对于simple_one_for_one来说,不必传入完整的child
spect,传入参数list,会自动进行 参数合并。 在一个地方定义好child
spec之后,其他地方只要start_child传入参数即可启动child进程,简化child都是同一类型进程情况下的编程。
在
rabbitmq中,tcp_acceptor_sup的子进程都是tcp_acceptor进程,在tcp_listener中是启动了
ConcurrentAcceptorCount个tcp_acceptor子进程,通过supervisor:start_child/2方法:
%%创建ConcurrentAcceptorCount个并发acceptor
lists:foreach(fun (_) ->
{ok, _APid} = supervisor:start_child(
AcceptorSup, [LSock])
end,
lists:duplicate(ConcurrentAcceptorCount, dummy)),
注意到,这里调用的start_child只传入了 LSock一个参数,另一个参数CallBack是在定义child spec的时候传入的,参见tcp_acceptor_sup.erl:
init(Callback) ->
{ok, {{simple_one_for_one, 10, 10},
[{tcp_acceptor, {tcp_acceptor, start_link, [ Callback]},
transient, brutal_kill, worker, [tcp_acceptor]}]}}.
Erlang内部自动为simple_one_for_one做了 参数合并,最后调用的是tcp_acceptor的init/2:
init({ Callback, LSock}) ->
case prim_inet:async_accept(LSock, -1) of
{ok, Ref} -> {ok, #state{callback=Callback, sock=LSock, ref=Ref}};
Error -> {stop, {cannot_accept, Error}}
end.
对于tcp_client_sup的情况类似,tcp_client_sup监控的子进程都是rabbit_reader类型,在
rabbit_networking.erl中启动tcp_listenner传入的处理connect事件的回调方法是是
rabbit_networking:start_client/1:
start_tcp_listener(Host, Port) ->
start_listener(Host, Port, "TCP Listener",
%回调的MFA
{ ?MODULE, start_client, []}).
start_client(Sock) ->
{ok, Child} = supervisor:start_child(rabbit_tcp_client_sup, []),
ok = rabbit_net:controlling_process(Sock, Child),
Child ! {go, Sock},
Child.
start_client调用了supervisor:start_child/2来动态启动rabbit_reader进程。
4、 协议的解析,消息的读取这部分也非常巧妙,这一部分主要在rabbit_reader.erl中,对于协议的解析没有采用gen_fsm,而是实现了一个巧妙的状态机机制,核心代码在mainloop/4中:
%启动一个连接
start_connection(Parent, Deb, ClientSock) ->
process_flag(trap_exit, true),
{PeerAddressS, PeerPort} = peername(ClientSock),
ProfilingValue = setup_profiling(),
try
rabbit_log:info("starting TCP connection ~p from ~s:~p~n",
[self(), PeerAddressS, PeerPort]),
%延时发送握手协议
Erlang:send_after(?HANDSHAKE_TIMEOUT * 1000, self(),
handshake_timeout),
%进入主循环,更换callback模块,魔法就在这个switch_callback
mainloop(Parent, Deb, switch_callback(
#v1{sock = ClientSock,
connection = #connection{
user = none,
timeout_sec = ?HANDSHAKE_TIMEOUT,
frame_max = ?FRAME_MIN_SIZE,
vhost = none},
callback = uninitialized_callback,
recv_ref = none,
connection_state = pre_init},
%%注意到这里,handshake就是我们的回调模块,8就是希望接收的数据长度,AMQP协议头的八个字节。
handshake, 8))
魔法就在switch_callback这个方法上:
switch_callback(OldState, NewCallback, Length) ->
%发起一个异步recv请求,请求Length字节的数据
Ref = inet_op(fun () -> rabbit_net:async_recv(
OldState#v1.sock, Length, infinity) end),
%更新状态,替换ref和处理模块
OldState#v1{callback = NewCallback,
recv_ref = Ref}.
异步接收Length个数据,如果有,erlang会通知你处理。处理模块是什么概念呢?其实就是一个状态的概念,表示当前协议解析进行到哪一步,起一个label的作用,看看mainloop/4中的应用:
mainloop(Parent, Deb, State = #v1{sock= Sock, recv_ref = Ref}) ->
%%?LOGDEBUG("Reader mainloop: ~p bytes available, need ~p~n", [HaveBytes, WaitUntilNBytes]),
receive
%%接收到数据,交给handle_input处理,注意handle_input的第一个参数就是callback
{inet_async, Sock, Ref, {ok, Data}} ->
%handle_input处理
{State1, Callback1, Length1} =
handle_input(State#v1.callback, Data,
State#v1{recv_ref = none}),
%更新回调模块,再次发起异步请求,并进入主循环
mainloop(Parent, Deb,
switch_callback(State1, Callback1, Length1));
handle_input有多个分支,每个分支都对应一个处理模块,例如我们刚才提到的握手协议:
%handshake模块,注意到第一个参数,第二个参数就是我们得到的数据
handle_input( handshake, <<"AMQP",1,1,ProtocolMajor,ProtocolMinor>>,
State = #v1{sock = Sock, connection = Connection}) ->
%检测协议是否兼容
case check_version({ProtocolMajor, ProtocolMinor},
{?PROTOCOL_VERSION_MAJOR, ?PROTOCOL_VERSION_MINOR}) of
true ->
{ok, Product} = application:get_key(id),
{ok, Version} = application:get_key(vsn),
%兼容的话,进入connections start,协商参数
ok = send_on_channel0(
Sock,
#'connection.start'{
version_major = ?PROTOCOL_VERSION_MAJOR,
version_minor = ?PROTOCOL_VERSION_MINOR,
server_properties =
[{list_to_binary(K), longstr, list_to_binary(V)} ||
{K, V} <-
[{"product", Product},
{"version", Version},
{"platform", " Erlang/OTP"},
{"copyright", ?COPYRIGHT_MESSAGE},
{"information", ?INFORMATION_MESSAGE}]],
mechanisms = <<"PLAIN AMQPLAIN">>,
locales = <<"en_US">> }),
{State#v1{connection = Connection#connection{
timeout_sec = ?NORMAL_TIMEOUT},
connection_state = starting},
frame_header, 7};
%否则,断开连接,返回可以接受的协议
false ->
throw({bad_version, ProtocolMajor, ProtocolMinor})
end;
其他协议的处理也是类似,通过动态替换callback的方式来模拟状态机做协议的解析和数据的接收,真的很巧妙!让我们体会到Erlang的魅力,FP的魅力。
5、序列图:
1)tcp server的启动过程:
2)一个client连接上来的处理过程:
小结:从上面的分析可以看出,rabbitmq的网络层是非常健壮和高效的,通过层层监控,对每个可能出现的风险点都做了考虑,并且利用了prim_net模块做异步IO处理。分层也是很清晰,将业务处理模块隔离到client_sup监控下的子进程,将网络处理细节和业务逻辑分离。在协议的解析和业务处理上虽然没有采用gen_fsm,但是也实现了一套类似的状态机机制,通过动态替换Callback来模拟状态的变迁,非常巧妙。如果你要实现一个tcp server,强烈推荐从rabbitmq中扣出这个网络层,你只需要实现自己的业务处理模块即可拥有一个高效、健壮、分层清晰的TCP服务器。
入这行也有四年了,从过去对软件开发的懵懂状态,到现在可以算是有一个初步认识的过程,期间也参与了不少项目,从一开始单纯的编码,到现在可以部分地参与一些设计方案的讨论,慢慢对设计方案的评判标准有一点感受。读软件工程、架构设计、模式之类的书,对于书中强调的一些标准和原则的感受只是感官浅层的印象,你可以一股脑从嘴里蹦出一堆词,什么开闭原则、依赖倒转、针对接口编程、系统的可伸缩性、可维护性、可重用等等,也仅仅停留在知道的份子上。不过现在,我对一个设计方案的评价标准慢慢变的很明确:简单、符合当前和一定预期时间内的需求、可靠、直观(或者说透明)。
简单,不是简陋,这是废话了。但是要做到简单,却是绝不简单,简单跟第四点直观有直接的关系,简单的设计就是一个直观的设计,可以让你一眼看得清的设计方案,也就是透明。一个最简单的评判方法,你可以讲这个方案讲给一个局外人听,如果能在短时间内让人理解的,那么这个方案八成是靠谱的。记的有本书上讲过类似的方法,你有一个方案,那就拿起电话打给一个无关的人员,如果你能在电话里说清楚,就表示这个方案相当靠谱,如果不能,那么这个方案很可能是过度复杂,过度设计了。
简单的设计,往往最后得出的结果是一个可靠性非常高的系统。这很容易理解,一个复杂的设计方案,有很多方面会导致最后的实现会更复杂:首先是沟通上的困难,一个复杂的方案是很难在短时间内在团队内部沟通清楚,每个开发人员对这个方案的理解可能有偏差;其次,复杂的方案往往非常考验设计人员和开发人员的经验、能力和细致程度,复杂的方案要考量的方面肯定比简单方案多得多,一个地方没有考虑到或者不全面,结果就是一个充满了隐患的系统,时不时地蹦出一个BUG来恶心你,这并非开发人员的能力问题,而是人脑天然的局限性(天才除外,咳咳)。
第二点,符合当前和一定预期时间内的需求。我们都知道,不变的变化本身,指望一个方案永久解决所有问题是乌托邦的梦想。复杂方案的出炉通常都是因为设计人员过度考量了未来系统的需求和变化,我们的系统以后要达到10倍的吞吐量,我们的系统以后要有几十万的节点等等。当然,首先要肯定的是对未来需求的考量是必需的,一个系统如果实现出来只能应付短时间的需求,那肯定是不能接受的。但是我们要警惕的是过度考量导致的过度复杂的设计方案,这还有可能是设计人员“炫技”的欲望隐藏在里头。这里面有一个权衡的问题,比如这里有两个方案:一个是两三年内绝对实用的方案,简单并且可靠直观,未来的改进余地也不错;另一个方案是可以承载当前的几十倍流量的方案,方案看起来很优雅,很时尚,实现起来也相对复杂。如何选择?如果这个系统是我们当前就要使用的,并且是关键系统,那么我绝对会选择前一个方案,在预期时间内去改进这个方案,因为对于关键系统,简单和可靠是性命攸关的。况且,我坚定地认为一个复杂的设计方案中绝对隐藏着一个简单的设计,这就像一个复杂的数学证明,通常都可以用更直观更简单的方式重新证明(题外话,费尔马大定理的证明是极其复杂的,现在还有很多人坚信有一个直观简单的证明存在,也就是那个费尔马没有写下的证明)。最近我们的一个方案讨论也证明了这一点,一个消息优先级的方案,一开始的设想是相对复杂的,需要在存储结构和调度上动手脚,后来集思广益,最后定下的方案非常类似linux的进程调度策略,通过分级queue和时间片轮询的方式完美解决了优先级的问题。这让我想起了软件开发的“隐喻”的力量,很多东西都是相通相似的。
上面这些乱弹都是自己在最近碰到的一些讨论和系统故障想起的,想想还是有必要写下来做个记录。
Xmemcached 1.2.0发布到现在,从反馈来看,已经有不少用户尝试使用xmc作为他们的memcached client,并且1.2.0这个版本也比较稳定,没有发现严重的BUG。Xmemcached下一个版本是1.2.1,初步计划是在元旦左右发布,计划做出的改进如下:
1、重写所有的单元测试和集成测试,提高代码的健壮性
2、新增一些功能,如 issue 66。
3、移除deprecated方法
4、提供用户指南。
1.2.1之后初步的设想是开发1.3版本,现在xmc的最大问题是对yanf4j的依赖,耦合比较严重,1.3版本将抽象出网络层,解耦yanf4j和xmc,yanf4j也将重构并引入filter机制。1.3版本也将发布一个支持unix domain socket的附带项目,事实上这个项目已经初步开发完成,基于juds,但是性能并不理想,我的计划是自己写一个东西来替代juds,juds最大的问题是仅支持阻塞IO,没有使用poll/epoll、select之类。
总之,我可以确认的是xmc本身将继续发展,也希望更多的朋友来尝试使用,有任何问题和意见都可以反馈给我,我都将及时回复。
update:修复了在linux firefox上不兼容的BUG。
下午搞了个 Erlang web shell,可以在web页面上像eshell那样进行交互式的Erlang编程,方便学习和测试。这样一来,一个 erlwsh就可以服务多个client,只要你有网络和浏览器,随时随地可以敲上几行erlang看看结果。代码很简单,就不多说了,有兴趣的看看,通过 mochiweb的http chunk编码,client通过Ajax Post方式提交。眼见为实,看看运行截图:
工程在google code上: http://code.google.com/p/erlwsh/
安装很简单,首先确保你已经安装了 Erlang,接下来:
svn checkout http://erlwsh.googlecode.com/svn/trunk/ erlwsh-read-only
cd erlwsh-read-only
scripts/install_mochiweb.sh
make
./start.sh
因为需要使用mochiweb,所以提供了下载并自动安装的脚本,这是litaocheng的大作。启动后访问 http://localhost:8000/shell 即可,have fun.
上篇是在兴奋的状态下当天晚上写的,这下篇拖到现在,印象也开始有点模糊了,以下全凭记忆,如有谬误敬请原谅。
CN-Erlounge第二天的topic,一开始是来自汕头的一位朋友介绍他对 利用单机程序组建分布式模型的分析与实例,实话说这个Topic很一般,基本上文不对题,并且举的例子没有说服力,有点为了用Erlang而用Erlang的感觉,其实跟Erlang关系不大,并且用Erlang搭建原型的话,还不如用python、ruby脚本来搞,后来跟同事的交流说,他介绍的还只是作坊式的一些做法,没有多少可借鉴的意义。
接下来是侯明远的《 基于Erlang实现的MMO服务器连接管理服务》,也就是Erlang在他的网游项目替换c++的一些尝试,效果非常好,不仅代码量大大减少,而且维护起来也非常容易。特别是他介绍了用Erlang搭建测试环境的尝试给了我们不少启发,事实上在回来后,我也尝试用Erlang写了个用于压测的代理服务器,不过由于我们的client仍然是Java,无法做到类似的分布式压测管理,仅用Erlang做中心的代理转发服务器。感受是Erlang做网络编程确实非常容易,Erlang的网络层将底层封装的非常完美,对于用户来说完全屏蔽了网络编程的复杂细节,并且提供了gen_server、gen_fsm这样的基础设施,宁外Erlang对binary数据的操作非常容易,对协议解析来说也是个巨大优势,整个程序就200多行代码,这还包括了一个通用的tcp服务器框架,借鉴了mochiweb的socket server实现。过去我对Erlang的message passing风格的理解还局限在actor模型上,进程之间相互发送消息,而其实Erlang的消息传递风格还体现在语言本身以及整个otp库的实现上,例如在accept一个连接后,我们调用服务器的逻辑代码:
accept_loop({Server, LSocket, M}) ->
{ok, Socket} = gen_tcp:accept(LSocket),
% spawn a new process to accept
gen_server:cast(Server, {accepted, self()}),
% initialize
State=M:init(Socket),
M:loop(State,Socket).
其中的M是你的逻辑模块,我们直接调用M:loop(State,Socket)进入逻辑模块的处理,这里的init和loop方法都是 约定,或者说模块的回调方法,你也可以理解成Ruby的duck typing。我不知道M有没有这两个方法, 我只是尝试给它们传递消息(调用),等待响应,这同样是 消息传递风格。同样,理解gen_server这样的behaviour的关键也是回调,你只要实现这些behaviour的回调方法,响应这些模块的消息,你将天然地拥有它们的强大功能。
接下来是周爱民的《 谈谈erlang网络环境下的几种数据流转形式》,怎么说呢,我听的懂,但是似乎没有抓到key point,老大们理解问题、分析问题的层次似乎不同了。不过其中讲到如何解决异步的通讯顺序问题对我们有一定借鉴价值。听了快两天的课,非常疲倦,加上头天晚上没睡好,这上午稀里哗啦就过了,中午组委会提供披萨,实话说好难吃啊,口味不惯。
压轴的是阿里云老吴的《 XEngine介绍》,第一次听说xengine是在阿里云计算公司的成立展览上,我跟开发人员有个短暂的交流,大概明白Erlang在xengine中扮演的角色。XEngine的野心很大,做中国的EC2、AppEngine,Erlang在其中的角色扮演了监控和协调的作用,利用它天然的分布式编程模型,xengine需要依赖阿里云的飞天计划,涵盖了分布式文件系统、MQ、通讯组件、分布式持久层等等,这些基础设施没搞好,xengine 还只能是“云”。集团内部早有消息是希望能统一集团内的各种基础设施,包括我们现在的这个MQ系统,我跟老吴开玩笑说他们做好了我们就要失业了:)。说到云计算,新浪的app engine据说已经开始内部测试了。那天我们老大还在说貌似国内只有阿里在搞app engine,没想到新浪倒走到前面咯。
后来是提议到公园去走走,大家随意聊聊,因为比较累以及跟各位大佬们不熟,阿宝朱GG他们为了赶飞机也提早走了,我们三个就提前撤退咯。杭州的出租车3点半交班,加上举办马拉松,打不到车,走了N远的路才坐到公交回家,到家天色刚晚。
CN-Erlounge IV的质量是我参加过的技术会议里面最高的,不过我其实没参加过多少技术会议,哈哈。总结下感受,从CN-Erlounge的Topic来看,已经有很多公司在实践中应用Erlang,我问arbow这一届Erlang大会跟过去的区别(过去我没参加
过),arbow就说这一届的实践经验的分享相对比较多,一个侧面也可以反应Erlang在国内的发展程度。不过Erlang还是小众语言,这从参会的人数上可以看出来。搜狐、校内、阿里这样的互联网巨头都开始尝试Erlang,一方面可以证明Erlang这个平台的吸引力,一方面也可以说明Erlang在国内已经开始进入实际应用阶段,对于许多还在观望的人来说,这是个好消息。
今天和同事一起去参加了 CN-Erlounge IV大会,大会的精彩程度超过我的预期,每个Topic都是精心准备并且非常实在,并且见到了很多只闻其名未见其人的大牛,比如传说中的T1、许老大、庄表伟、周爱民老师等。我们3个太早去了,8点半到了会场,发现大多数还没来,阿宝同学和锋爷他们更是9点多才出的门,因此整个会议进程都相应推迟了。
首先是校内网的成立涛做了《Erlang开发实践》的演讲,主题是一个典型的Erlang项目的开发流程、目录结构、单元测试、集成测试、常见问题解决等的概括性介绍,并且他还特意写了一个工程作为Sample,就是放在google code上的 erlips,非常佩服这样的专业精神。交流提到校内网已经部署了30个以上的Erlang节点做广告推送系统。Topic本身非常实在,并且有实际的代码和工程文件提供,可以作为了解Erlang开发基本过程的骨架工程。接下来是锋爷的重头戏《Erlang系统调优》,锋爷的Topic其实超出了Erlang的范畴,对所有系统的性能调优都有借鉴的意义,主题本身将Erlang平台和unix操作系统做了比较,认为Erlang本身也是个OS平台,并且介绍了Erlang提供的方方面面的工具,包括调试、诊断、性能剖析、监控、语言级别的优化、系统的优化选项、协议的选型、应该避免的陷阱、最佳实践等等,你不得不承认Erlang实在是太牛x了,这样的平台难怪能做到7个9的可靠性。这个Topic非常精彩,从ppt本身能看到的只是局部信息,等有视频的时候准备重新看看。
中午组委会提供了午餐,还是很方便,会议的地点吃饭地方不好找,不过晚饭没提供,我们跑了不远的路找了家小饭馆解决晚饭问题。下午的Topic一开始是 饿狼战役的创建者老范的介绍 ,饿狼战役是一个Erlang编写的棋牌型的游戏,玩家可以编写自己的指挥进程参与竞赛,实际上是作为一个Erlang学习的良好环境,类似过去非常流行的robot code游戏一样。我因为跟阿宝他们去闲逛,错过了大部分介绍,源码已经读过,不过我对AI一点也不了解,写个程序干掉英格兰卫兵还是没问题的,哈哈。后来是python社区的大妈介绍了 erlbattle社区的养成问题,谈到了一个社区的生命周期问题,如何去建设一个技术社区,我没有多少感受,不多扯了。饿狼战役推荐去看看,如果你对AI或者erlang有兴趣的话,可以去试试。接下来是T1做的《CUDA编程》的Topic,这个Topic我在提前看ppt的时候就觉的估计自己完全听不懂,最后果然如此,这是一个关于现在很热门GPU编程的Topic,讲述了如何在10ms内完成jpeg的压缩的优化手段和编程技巧,最终的结果是2毫秒多一点就搞定了这个需求,T1介绍的非常详细关于算法和技巧方面的细节,完全跟做工程的是两个世界。T1大大是火星人,咱就不多说了,景仰就行了。
晚上的两个Topic都是关于如何在C++中借鉴Erlang来实现消息传递风格的编程,51.com的qiezi和崔博介绍了他们的actor框架,他们是基于协程和线程池调度来实现伪同步调用,可以实现类似Erlang的进程风格的消息传递,但是要做一个工作就是将类似socket读写、文件读写、sleep这样的阻塞调用封装一下,有异步io可以利用的就利用异步IO,没有的就使用线程池调度,事实上他们做的事情正是Erlang已经帮你做了,现场有很多争议,认为这样还不如使用Erlang,因为你这样做无法避免两个问题:协程的异常处理和阻塞调用可能导致整个进程内的协程不可用,毕竟协程是非抢占式的,并且无法充分利用多CPU的优势。但是许老大提到,从工程实践角度,Erlang毕竟还是小众语言,维护和招人上都不容易,而系统的高可靠性可以从更高层次上去解决,而我们可以从Erlang借鉴这些做法来改进我们的传统语言编程模型。许老大还提到他认为的Erlang编程模型的两个缺点:一个是同步调用的死锁问题,一个是资源进程的独占性问题,这两个问题最终还是要回归到异步模型。这两个问题,我认为其实是一个问题,还是由于资源的有限和独占性引起的,像IO这样的资源就是,你可以将请求并行化设置回调函数不阻塞调用本身,但是实际的IO读写本身仍然是串行的,只不过将这部分工作交给谁来做的问题,我觉的这个问题对于任何编程语言都是一样的无法解决的。对于同步模型和异步模型本身,我更偏向于同步模型,这从xmc的API就可以看出来,同步模型更符合人类直觉,也易于使用,而异步模型容易将业务碎片化,不直观并且使用上也不便利。
以上是今天的流水账,有兴趣看到这的估计没几个,哇咔咔。
补充,遗漏了一个香港老外作的topic演讲,是关于erlang实现的restms,restms是一种restful的message协议,现在想来他主要介绍了restms协议以及一个erlang实现,也就是fireflymq,其中特别介绍了riak这样一个key-value store,它类似amazon dynamo,同样采用consistent hash,多节点备份,vector clock同步等等,比较特殊的地方是他可以将数据组织成类似web超链接形成的网状结构并存储和查询。
看到这么一个题目:
{3,2,2,6,7,8}排序输出,7不在第二位,68不在一起。
这样的题目似乎避免不了遍历,关键还在于过滤条件的安排,怎么让过滤的范围尽量地小。通常的做法是循环遍历,对于类似Prolog这样的语言来说,由于内置了推理引擎,可以简单地描述问题,让引擎来帮你做递归遍历,解决这类问题是非常简单的。Prolog好久没写,以Ruby的amb操作符为例来解决这道题目:
#结果为hash,去重
$hash={}
amb=Amb.new
array=[3,2,2,6,7,8]
class << array
alias remove delete
def delete(*nums)
result=dup
nums.each do |n|
result.delete_at(result.index(n)) if result.index(n)
end
result
end
end
#从集合选元素
one=amb.choose(*array)
two=amb.choose(*(array.delete(one)))
three=amb.choose(*(array.delete(one,two)))
four=amb.choose(*(array.delete(one,two,three)))
five=amb.choose(*(array.delete(one,two,three,four)))
six=amb.choose(*(array.delete(one,two,three,four,five)))
#条件1:第二个位置不能是7
amb.require(two!=7)
#条件2:6跟8不能一起出现
def six_eight_not_join(a,b)
"#{a}#{b}"!="68"&&"#{a}#{b}"!="86"
end
amb.require(six_eight_not_join(one,two))
amb.require(six_eight_not_join(two,three))
amb.require(six_eight_not_join(three,four))
amb.require(six_eight_not_join(four,five))
amb.require(six_eight_not_join(five,six))
#条件3:不重复,利用全局hash判断
def distinct?(one,two,three,four,five,six)
if $hash["#{one},#{two},#{three},#{four},#{five},#{six}"].nil?
$hash["#{one},#{two},#{three},#{four},#{five},#{six}"]=1 #记录
true
else
false
end
end
amb.require(distinct?(one,two,three,four,five,six))
puts "#{one},#{two},#{three},#{four},#{five},#{six}"
amb.failure
三个条件的满足通过amb.require来设置,这里安排的只是一种顺序,可以根据实际测试结果来安排这些条件的顺序以最大程度地提高效率。代码注释很清楚了,我就不多嘴了。Ruby amb的实现可以看 这里。什么是amb可以看 这个。
我们小组还要招java方面的工程师和架构师,老大说还有两个社招名额,不用就浪费了。我可以帮忙推荐,情况介绍如下:
工作地点:杭州
公司:阿里旗下子公司
工作内容:消息中间件或者分布式持久框架
要求:
1、因为是社招,请应届直接忽略,公司有校园招聘
2、最好能有2年以上工作经验(非硬性)
3、java基础牢固
4、对java并发、网络或者数据库编程有丰富经验,如果对JVM方面也了解,那更好
5、有分布式系统开发和设计经验尤佳
6、有一定的性能调优经验
7、熟悉各种常用的开源框架
8、对技术有追求、有激情,有气度,能沟通,能交流。
来这里你能得到什么:
1、大规模分布式系统的设计和开发
2、处理海量数据系统的设计与开发
3、大量的技术交流机会
4、相对轻松的、和谐的团队和工作氛围
欢迎有兴趣的朋友投递简历,我的email : killme2008@gmail.com
unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,是IPC的方法之一,特定于*nix平台。使用unix domain socket有三个好处:
1)在同一主机上,unix domain socket比一般的tcp socket快上一倍,性能因素这是一个主要原因。
2)unix domain socket可以在同一主机的不同进程之间传递文件描述符
3)较新的unix domain socket实现把客户的ID和组ID提供给服务器,可以让服务器作安全检查。
memcached的FAQ中也提到为了安全验证,可以考虑让memcached监听unix domain socket。Memcached支持这一点,可以通过-s选项指定unix domain socket的路径名,注意,为了可移植性,尽量使用绝对路径,因为Posix标准声称给unix domain socket绑定相对路径将导致不可预计的后果,我在linux的测试是可以使用相对路径。假设我将memcached绑定到/home/dennis/memcached,可以这样启动memcached:
memcached -s /home/dennis/memcached
端口呢?没有端口了,/home/dennis/memcached这个文件你可以理解成FIFO的管道,unix domain socket的server/client通过这个管道通讯。
libmemcached支持通过unix domain socket来访问memcached,基于libmemcached实现的client应该都可以使用这一功能。目前来看,java平台由于不支持平台相关的unix domain socket,因此无法享受memcached的这一特性。
不过有一个开源项目通过jni支持实现了unix domain socket,这个项目称为 juds。核心类就三个,使用非常简单。下载文件后,解压缩,make & make install即可。注意,Makefile中写死了JAVA_HOME,手工修改即可。看一个例子,经典的Time server:
package com.google.code.juds.test;
import java.io.IOException;
import com.google.code.juds.*;
import java.io.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeServer {
public static void main(String[] args) {
try {
UnixDomainSocketServer server = new UnixDomainSocketServer(
"/home/dennis/time", UnixDomainSocket.SOCK_STREAM);
OutputStream output = server.getOutputStream();
Date date = new Date();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
output.write(dateFormat.format(date).getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
通过 UnixDomainSocketServer创建server,指定类型为SOCK_STREAM,juds也支持UDP类型。client的使用如下:
byte[] b = new byte[128];
UnixDomainSocketClient socket = new UnixDomainSocketClient("/home/dennis/time",
UnixDomainSocket.SOCK_STREAM);
InputStream in = socket.getInputStream();
in.read(b);
System.out.println("Text received: \"" + new String(b) + "\"");
socket.close();
显然,juds还只支持阻塞IO,考虑可进一步使用select、poll来扩展实现非阻塞IO。
最后一个例子,通过juds访问memcached的unix domain socket,简单的version协议调用:
byte[] b = new byte[128];
UnixDomainSocketClient socket = new UnixDomainSocketClient("/home/dennis/memcached",
UnixDomainSocket.SOCK_STREAM);
OutputStream out = socket.getOutputStream();
String text = "version\r\n";
out.write(text.getBytes());
InputStream in = socket.getInputStream();
in.read(b);
System.out.println("Text received: \"" + new String(b) + "\"");
socket.close();
输出
Text received: "VERSION 1.4.1"
字符串操作是任何一门编程语言中最常用的操作之一,scheme也提供了一系列procudure来操作字符串。
1、字符串的比较,有6个,分别是string=? string>? string<? string>=? string<=?
这与其他语言中对string的比较并无不同,比较字符和长度。
例子:
(string=? "mom" "mom")  #t
(string<? "mom" "mommy")  #t
(string>? "Dad" "Dad")  #f
(string=? "Mom and Dad" "mom and dad")  #f
(string<? "a" "b" "c")  #t
注意这些比较操作是大小写敏感。相应的,大小写不敏感的版本:
procedure: (string-ci=? string1 string2 string3 ...)
procedure: (string-ci<? string1 string2 string3 ...)
procedure: (string-ci>? string1 string2 string3 ...)
procedure: (string-ci<=? string1 string2 string3 ...)
procedure: (string-ci>=? string1 string2 string3 ...)
2、从字符构造字符串,使用string过程
(string #\a) => "a"
(string #\a #\b #\c) => "abc"
注意,换行字符是#\newline,回车字符是#\return
3、重复N个字符构造字符串
(make-string) => ""
(make-string 4 #\a) =>"aaaa")
4、字符串长度 string-length
(string-length "") =>0
(string-length "dennis") => 6
5、取第N个字符,相当于java中的charAt:
(string-ref "hi there" 0)  #\h
(string-ref "hi there" 5)  #\e
6、修改字符串的第N个字符:
(string-set! "hello" 0 #\H) => "Hello"
7、拷贝字符串:
(let ((str "abc"))
(eq? str (string-copy str))) => #f
(let ((str "abc"))
(equal? str (string-copy str))) => #t
8、拼接字符串,string-append
(string-append) => ""
(string-append "abc" "defg") => "abcdefg"
9、截取子串
(substring "hi there" 0 1)  "h"
(substring "hi there" 3 6)  "the"
(substring "hi there" 5 5)  ""
10、填充字符串
(let ((str (string-copy "sleepy")))
(string-fill! str #\Z)
str)  "ZZZZZZ"
11、与list的相互转换
(string->list "")  ()
(string->list "abc")  (#\a #\b #\c)
(list->string '())  ""
(list->string '(#\a #\b #\c))  "abc"
(list->string
(map char-upcase
(string->list "abc")))  "ABC"
一张截图,Java虽然号称跨平台,然而涉及到跟网络相关时,还是依赖于各个平台的实现。对写java网络编程的朋友有点价值。

基于java nio的java memcached client——xmemcached正式发布1.2.0-stable版本,这是一个稳定的版本,在1.2.0-RC2的基础上做了性能改进和BUG修复。在用户的反馈下,发现了数个比较严重的BUG,因此这个版本建议升级以规避这些可能出现的BUG。相比于1.2.0-RC2,主要的改进如下:
1、添加心跳检测,默认开启这个特性,你可以通过
memcachedClient.setEnableHeartBeat(false);
memcachedClient.setEnableHeartBeat(false);
来关闭。心跳检测出于兼容性考虑是基于version协议实现的。
2.添加新的incr/decr方法,允许传入初始值,如果指定的key不存在的时候,就将该值add到memcached。具体参见API文档。
3.修复数个BUG,如Issue 55,Issue 57,Issue 58,Issue ,Issue 60。具体请看这里。
总结1.2相比于1.1版本的主要新增特性列表如下:
1、支持完整的memcached二进制协议
2、支持java nio连接池。
3、支持kestrel。
4、支持与hibernate-memcached的集成
5、日志从common-logging迁移到slf4j
6、简化构建等。
7、兼容JDK5。
欢迎试用并反馈,我的email: killme2008@gmail.com
喜欢奇幻的朋友可以瞧瞧,据说是烟大推荐的,同样在17K上,国人写的西方奇幻,还未写完,但是长度已经足够你好好享受。味道呢?战斗类似《博德之门》,语言比较绕,没读过此类作品的可能不习惯,文笔没得说,写作水准暂未发现下降趋势,值得长期追。名字比较奇怪,《 昆古尼尔》,是北欧神话中大神奥丁的所有物——永恒之枪。故事就不剧透了,前两章可能比较晕,但是坚持看下去就能搞明白啦。如果你实在着急呢,可以看看这个 背景介绍。
采用的是 jboss netty的benchmark,环 境是两台linux机器,都是4核16G内存以及2.6内核,网络环境是公司内网,带宽是1Gbps ,JDK1.6.0_07。对比的是 mina 2.0M6和 yanf4j 1.0-stable,两者都在压到16K,5000并发的时候客户端退出,因此后面给出的图有个16K的在5000并发为0,事实上只是几个连接失败,但是benchmark client就忽略了这个数据。实际过程还测试了1万并发连接的情况,但是由于测试客户端很容易退出,因此最后还是选定最大并发5000。注意,并非mina和yanf4j无法支撑1万个连接,而是benchmark client本身的处理,再加上内核tcp参数没有调整造成的。
首先看源码,mina的Echo Server:
package org.jboss.netty.benchmark.echo.server;
import java.net.InetSocketAddress;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.executor.ExecutorFilter;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.jboss.netty.benchmark.echo.Constant;
/**
* @author The Netty Project (netty-dev@lists.jboss.org)
* @author Trustin Lee (tlee@redhat.com)
*
* @version $Rev: 394 $, $Date: 2008-10-03 12:55:27 +0800 (星期五, 03 十月 2008) $
*
*/
public class MINA {
public static void main(String[] args) throws Exception {
boolean threadPoolDisabled = args.length > 0 && args[0].equals("nothreadpool");
SocketAcceptor acceptor = new NioSocketAcceptor(Runtime.getRuntime().availableProcessors());
acceptor.getSessionConfig().setMinReadBufferSize(Constant.MIN_READ_BUFFER_SIZE);
acceptor.getSessionConfig().setReadBufferSize(Constant.INITIAL_READ_BUFFER_SIZE);
acceptor.getSessionConfig().setMaxReadBufferSize(Constant.MAX_READ_BUFFER_SIZE);
acceptor.getSessionConfig().setThroughputCalculationInterval(0);
acceptor.getSessionConfig().setTcpNoDelay(true);
acceptor.setDefaultLocalAddress(new InetSocketAddress(Constant.PORT));
if (!threadPoolDisabled) {
// Throttling has been disabled because it causes a dead lock.
// Also, it doesn't have per-channel memory limit.
acceptor.getFilterChain().addLast(
"executor",
new ExecutorFilter(
Constant.THREAD_POOL_SIZE, Constant.THREAD_POOL_SIZE));
}
acceptor.setHandler(new EchoHandler());
acceptor.bind();
System.out.println("MINA EchoServer is ready to serve at port " + Constant.PORT + ".");
System.out.println("Enter 'ant benchmark' on the client side to begin.");
System.out.println("Thread pool: " + (threadPoolDisabled? "DISABLED" : "ENABLED"));
}
private static class EchoHandler extends IoHandlerAdapter {
EchoHandler() {
super();
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
session.write(((IoBuffer) message).duplicate());
}
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
session.close();
}
}
}
再看Yanf4j的Echo Server,没有多大区别:
package org.jboss.netty.benchmark.echo.server;
import java.nio.ByteBuffer;
import org.jboss.netty.benchmark.echo.Constant;
import com.google.code.yanf4j.config.Configuration;
import com.google.code.yanf4j.core.Session;
import com.google.code.yanf4j.core.impl.HandlerAdapter;
import com.google.code.yanf4j.core.impl.StandardSocketOption;
import com.google.code.yanf4j.nio.TCPController;
public class Yanf4j {
public static void main(String[] args) throws Exception {
boolean threadPoolDisabled = args.length > 0
&& args[0].equals("nothreadpool");
Configuration configuration = new Configuration();
configuration.setCheckSessionTimeoutInterval(0);
configuration.setSessionIdleTimeout(0);
configuration
.setSessionReadBufferSize(Constant.INITIAL_READ_BUFFER_SIZE);
TCPController controller = new TCPController(configuration);
controller.setSocketOption(StandardSocketOption.SO_REUSEADDR, true);
controller.setSocketOption(StandardSocketOption.TCP_NODELAY, true);
controller.setHandler(new EchoHandler());
if (!threadPoolDisabled) {
controller.setReadThreadCount(Constant.THREAD_POOL_SIZE);
}
controller.bind(Constant.PORT);
System.out.println("Yanf4j EchoServer is ready to serve at port "
+ Constant.PORT + ".");
System.out
.println("Enter 'ant benchmark' on the client side to begin.");
System.out.println("Thread pool: "
+ (threadPoolDisabled ? "DISABLED" : "ENABLED"));
}
static class EchoHandler extends HandlerAdapter {
@Override
public void onMessageReceived(final Session session, final Object msg) {
session.write(((ByteBuffer) msg).duplicate());
}
@Override
public void onExceptionCaught(Session session, Throwable t) {
session.close();
}
}
}
两者都启用线程池(16个线程),开启TCP_NODELAY选项,Client采用SYNC模式,压测结果如下(仅供参考),分别是数据大小为128、1K、4K和16K情况下,随着并发client上升吞吐量的对比图:
系统的资源消耗来看,Mina的load相对偏高。
上篇文章我谈到了 java nio的一个严重BUG,并且介绍了jetty是如何规避这个BUG的。我在将这部分代码整合进yanf4j的过程中发现了不少误判的情况,让我们看看误判是怎么发生的。jetty的解决方案是通过在select返回为0的情况下,计量Selector.select(timeout)执行的时间是否与传入的timeout参数相差太大(小于timeout的一半),如果相差太大,那么认为发生一次bug,如果发生的次数超过设定值,依据严重程度进行处理:第一尝试取消任何有效并且interestOps等于0的SelectionKey;第二次就是重新创建一个Selector,并将有效的Channel注册到新的Selector。误判的发生就产生于这个时间的计量上,看javadoc可以发现它是这样描述这个方法的:
This method performs a blocking selection operation. It
returns only after at least one channel is selected, this selector's wakeup
method is invoked, or the current thread is interrupted, whichever comes first
意思就是说这个方法将阻塞select调用,直到下列三种情况之一发生才返回:至少一个channel被选中;同一个Selector的wakeup方法被调用;或者调用所处的当前线程被中断。这三种情况无论谁先发生,都将导致select(timeout)返回。因此为了减少误判,你需要将这三种情况加入判断条件。Jetty的方案已经将select返回为0的情况考虑了,但是却没有考虑线程被中断或者Selector被wakeup的情况,在jetty的运行时也许不会有这两种情况的发生,不过我在windows上用jdk 6u7跑jetty的时候就发现了误判的日志产生。除了wakeup和线程中断这两种情形外,为了进一步提高判断效率,应该将操作系统版本和jdk版本考虑进来,如果是非linux系统直接不进行后续的判断,如果是jdk6u4以后版本也直接忽略判断,因此yanf4j里的实现大致如下:
boolean seeing = false;
/**
* 非linux系统或者超过java6u4版本,直接返回
*/
if (!SystemUtils.isLinuxPlatform()
|| SystemUtils.isAfterJava6u4Version()) {
return seeing;
}
/**
* 判断是否发生BUG的要素:
* (1)select返回为0
* (2)wait时间大于0
* (3)select耗时小于一定值
* (4)非wakeup唤醒
* (5)非线程中断引起
*/
if (JVMBUG_THRESHHOLD > 0 && selected == 0
&& wait > JVMBUG_THRESHHOLD && now - before < wait / 4
&& !this.wakenUp.get() /* waken up */
&& !Thread.currentThread().isInterrupted()/* Interrupted */) {
this.jvmBug.incrementAndGet();
其中判断是否是线程中断引起的是通过Thread.currentThread().isInterrupted(),判断是否是wakeup是通过一个原子变量wakenUp,当调调用Selector.wakeup时候,这个原子变量更新为true。判断操作系统和jdk版本是通过System.getProperty得到系统属性做字符串处理即可。类似的代码示例:
public static final String OS_NAME = System.getProperty("os.name");
private static boolean isLinuxPlatform = false;
static {
if (OS_NAME != null && OS_NAME.toLowerCase().indexOf("linux") >= 0) {
isLinuxPlatform = true;
}
}
public static final String JAVA_VERSION = System
.getProperty("java.version");
private static boolean isAfterJava6u4Version = false;
static {
if (JAVA_VERSION != null) {
// java4 or java5
if (JAVA_VERSION.indexOf("1.4.") >= 0
|| JAVA_VERSION.indexOf("1.5.") >= 0)
isAfterJava6u4Version = false;
// if it is java6,check sub version
else if (JAVA_VERSION.indexOf("1.6.") >= 0) {
int index = JAVA_VERSION.indexOf("_");
if (index > 0) {
String subVersionStr = JAVA_VERSION.substring(index + 1);
if (subVersionStr != null && subVersionStr.length() > 0) {
try {
int subVersion = Integer.parseInt(subVersionStr);
if (subVersion >= 4)
isAfterJava6u4Version = true;
} catch (NumberFormatException e) {
}
}
}
// after java6
} else
isAfterJava6u4Version = true;
}
}
随着并发数量的提高,传统nio框架采用一个Selector来支撑大量连接事件的管理和触发已经遇到瓶颈,因此现在各种nio框架的新版本都采用多个Selector并存的结构,由多个Selector均衡地去管理大量连接。这里以Mina和Grizzly的实现为例。
在Mina 2.0中,Selector的管理是由org.apache.mina.transport.socket.nio.NioProcessor来处理,每个NioProcessor对象保存一个Selector,负责具体的select、wakeup、channel的注册和取消、读写事件的注册和判断、实际的IO读写操作等等,核心代码如下:
public NioProcessor(Executor executor) {
super(executor);
try {
// Open a new selector
selector = Selector.open();
} catch (IOException e) {
throw new RuntimeIoException("Failed to open a selector.", e);
}
}
protected int select(long timeout) throws Exception {
return selector.select(timeout);
}
protected boolean isInterestedInRead(NioSession session) {
SelectionKey key = session.getSelectionKey();
return key.isValid() && (key.interestOps() & SelectionKey.OP_READ) != 0;
}
protected boolean isInterestedInWrite(NioSession session) {
SelectionKey key = session.getSelectionKey();
return key.isValid() && (key.interestOps() & SelectionKey.OP_WRITE) != 0;
}
protected int read(NioSession session, IoBuffer buf) throws Exception {
return session.getChannel().read(buf.buf());
}
protected int write(NioSession session, IoBuffer buf, int length) throws Exception {
if (buf.remaining() <= length) {
return session.getChannel().write(buf.buf());
} else {
int oldLimit = buf.limit();
buf.limit(buf.position() + length);
try {
return session.getChannel().write(buf.buf());
} finally {
buf.limit(oldLimit);
}
}
}
这些方法的调用都是通过AbstractPollingIoProcessor来处理,这个类里可以看到一个nio框架的核心逻辑,注册、select、派发,具体因为与本文主题不合,不再展开。NioProcessor的初始化是在NioSocketAcceptor的构造方法中调用的:
public NioSocketAcceptor() {
super(new DefaultSocketSessionConfig(), NioProcessor.class);
((DefaultSocketSessionConfig) getSessionConfig()).init(this);
}
直接调用了父类AbstractPollingIoAcceptor的构造函数,在其中我们可以看到,默认是启动了一个SimpleIoProcessorPool来包装NioProcessor:
protected AbstractPollingIoAcceptor(IoSessionConfig sessionConfig,
Class<? extends IoProcessor<T>> processorClass) {
this(sessionConfig, null, new SimpleIoProcessorPool<T>(processorClass),
true);
}
这里其实是一个组合模式,SimpleIoProcessorPool和NioProcessor都实现了Processor接口,一个是组合形成的Processor池,而另一个是单独的类。调用的SimpleIoProcessorPool的构造函数是这样:
private static final int DEFAULT_SIZE = Runtime.getRuntime().availableProcessors() + 1;
public SimpleIoProcessorPool(Class<? extends IoProcessor<T>> processorType) {
this(processorType, null, DEFAULT_SIZE);
}
可以看到,默认的池大小是cpu个数+1,也就是创建了cpu+1个的Selector对象。它的重载构造函数里是创建了一个数组,启动一个CachedThreadPool来运行NioProcessor,通过反射创建具体的Processor对象,这里就不再列出了。
Mina当有一个新连接建立的时候,就创建一个NioSocketSession,并且传入上面的SimpleIoProcessorPool,当连接初始化的时候将Session加入SimpleIoProcessorPool:
protected NioSession accept(IoProcessor<NioSession> processor,
ServerSocketChannel handle) throws Exception {
SelectionKey key = handle.keyFor(selector);
if ((key == null) || (!key.isValid()) || (!key.isAcceptable()) ) {
return null;
}
// accept the connection from the client
SocketChannel ch = handle.accept();
if (ch == null) {
return null;
}
return new NioSocketSession(this, processor, ch);
}
private void processHandles(Iterator<H> handles) throws Exception {
while (handles.hasNext()) {
H handle = handles.next();
handles.remove();
// Associates a new created connection to a processor,
// and get back a session
T session = accept(processor, handle);
if (session == null) {
break;
}
initSession(session, null, null);
// add the session to the SocketIoProcessor
session.getProcessor().add(session);
}
}
加入的操作是递增一个整型变量并且模数组大小后对应的NioProcessor注册到session里:
private IoProcessor<T> nextProcessor() {
checkDisposal();
return pool[Math.abs(processorDistributor.getAndIncrement()) % pool.length];
}
if (p == null) {
p = nextProcessor();
IoProcessor<T> oldp =
(IoProcessor<T>) session.setAttributeIfAbsent(PROCESSOR, p);
if (oldp != null) {
p = oldp;
}
}
这样一来,每个连接都关联一个NioProcessor,也就是关联一个Selector对象,避免了所有连接共用一个Selector负载过高导致server响应变慢的后果。但是注意到NioSocketAcceptor也有一个Selector,这个Selector用来干什么的呢?那就是集中处理OP_ACCEPT事件的Selector,主要用于连接的接入,不跟处理读写事件的Selector混在一起,因此Mina的默认open的Selector是cpu+2个。
看完mina2.0之后,我们来看看Grizzly2.0是怎么处理的,Grizzly还是比较保守,它默认就是启动两个Selector,其中一个专门负责accept,另一个负责连接的IO读写事件的管理。Grizzly 2.0中Selector的管理是通过SelectorRunner类,这个类封装了Selector对象以及核心的分发注册逻辑,你可以将他理解成Mina中的NioProcessor,核心的代码如下:
protected boolean doSelect() {
selectorHandler = transport.getSelectorHandler();
selectionKeyHandler = transport.getSelectionKeyHandler();
strategy = transport.getStrategy();
try {
if (isResume) {
// If resume SelectorRunner - finish postponed keys
isResume = false;
if (keyReadyOps != 0) {
if (!iterateKeyEvents()) return false;
}
if (!iterateKeys()) return false;
}
lastSelectedKeysCount = 0;
selectorHandler.preSelect(this);
readyKeys = selectorHandler.select(this);
if (stateHolder.getState(false) == State.STOPPING) return false;
lastSelectedKeysCount = readyKeys.size();
if (lastSelectedKeysCount != 0) {
iterator = readyKeys.iterator();
if (!iterateKeys()) return false;
}
selectorHandler.postSelect(this);
} catch (ClosedSelectorException e) {
notifyConnectionException(key,
"Selector was unexpectedly closed", e,
Severity.TRANSPORT, Level.SEVERE, Level.FINE);
} catch (Exception e) {
notifyConnectionException(key,
"doSelect exception", e,
Severity.UNKNOWN, Level.SEVERE, Level.FINE);
} catch (Throwable t) {
logger.log(Level.SEVERE,"doSelect exception", t);
transport.notifyException(Severity.FATAL, t);
}
return true;
}
基本上是一个reactor实现的样子,在AbstractNIOTransport类维护了一个SelectorRunner的数组,而Grizzly用于创建tcp server的类TCPNIOTransport正是继承于AbstractNIOTransport类,在它的start方法中调用了startSelectorRunners来创建并启动SelectorRunner数组:
private static final int DEFAULT_SELECTOR_RUNNERS_COUNT = 2;
@Override
public void start() throws IOException {
if (selectorRunnersCount <= 0) {
selectorRunnersCount = DEFAULT_SELECTOR_RUNNERS_COUNT;
}
startSelectorRunners();
}
protected void startSelectorRunners() throws IOException {
selectorRunners = new SelectorRunner[selectorRunnersCount];
synchronized(selectorRunners) {
for (int i = 0; i < selectorRunnersCount; i++) {
SelectorRunner runner =
new SelectorRunner(this, SelectorFactory.instance().create());
runner.start();
selectorRunners[i] = runner;
}
}
}
可见Grizzly并没有采用一个单独的池对象来管理SelectorRunner,而是直接采用数组管理,默认数组大小是2。SelectorRunner实现了Runnable接口,它的start方法调用了一个线程池来运行自身。刚才我提到了说Grizzly的Accept是单独一个Selector来管理的,那么是如何表现的呢?答案在RoundRobinConnectionDistributor类,这个类是用于派发注册事件到相应的SelectorRunner上,它的派发方式是这样:
public Future<RegisterChannelResult> registerChannelAsync(
SelectableChannel channel, int interestOps, Object attachment,
CompletionHandler completionHandler)
throws IOException {
SelectorRunner runner = getSelectorRunner(interestOps);
return transport.getSelectorHandler().registerChannelAsync(
runner, channel, interestOps, attachment, completionHandler);
}
private SelectorRunner getSelectorRunner(int interestOps) {
SelectorRunner[] runners = getTransportSelectorRunners();
int index;
if (interestOps == SelectionKey.OP_ACCEPT || runners.length == 1) {
index = 0;
} else {
index = (counter.incrementAndGet() % (runners.length - 1)) + 1;
}
return runners[index];
}
getSelectorRunner这个方法道出了秘密,如果是OP_ACCEPT,那么都使用数组中的第一个SelectorRunner,如果不是,那么就通过取模运算的结果+1从后面的SelectorRunner中取一个来注册。
分析完mina2.0和grizzly2.0对Selector的管理后我们可以得到几个启示:
1、在处理大量连接的情况下,多个Selector比单个Selector好
2、多个Selector的情况下,处理OP_READ和OP_WRITE的Selector要与处理OP_ACCEPT的Selector分离,也就是说处理接入应该要一个单独的Selector对象来处理,避免IO读写事件影响接入速度。
3、Selector的数目问题,mina默认是cpu+2,而grizzly总共就2个,我更倾向于mina的策略,但是我认为应该对cpu个数做一个判断,如果CPU个数超过8个,那么更多的Selector线程可能带来比较大的线程切换的开销,mina默认的策略并非合适,幸好可以设置这个数值。
这个BUG会在linux上导致cpu 100%,使得nio server/client不可用,具体的详情可以看这里 http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933 。令人失望的是这个BUG直到jdk 6u4才解决,sun的拖沓让人难以相信。这个BUG在server端容易出现,因为server端有频繁地接入断开连接。
使用jdk 6u4之前版本的nio框架都有这个隐患,除非你的框架很好地处理了这个可能的隐患。Grizzly的处理方式比较简单,也就是BUG报告里面提到的方式,在SelectionKey.cancel()之后马上进行了一次select调用将fd从poll(epoll)中移除:
this.selectionKey.cancel();
try {
// cancel key,then select now to remove file descriptor
this.selector.selectNow();
} catch (IOException e) {
onException(e);
log.error("Selector selectNow fail", e);
}
实际上这样的解决方式还是留有隐患的,因为key的取消和这个selectNow操作很可能跟Selector.select操作并发地在进行,在两个操作之间仍然留有一个极小的时间窗口可能发生这个BUG。因此,你需要更安全地方式处理这个问题,jetty的处理方式是这样,连续的select(timeout)操作没有阻塞并返回0,并且次数超过了一个指定阀值,那么就遍历整个key set,将key仍然有效并且interestOps等于0的所有key主动取消掉;如果在这次修正后,仍然继续出现select(timeout)不阻塞并且返回0的情况,那么就重新创建一个新的Selector,并将Old Selector的有效channel和对应的key转移到新的Selector上,
long before=now;
int selected=selector.select(wait);
now = System.currentTimeMillis();
_idleTimeout.setNow(now);
_timeout.setNow(now);
// Look for JVM bugs
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933
if (__JVMBUG_THRESHHOLD>0 && selected==0 && wait>__JVMBUG_THRESHHOLD && (now-before)<(wait/2) )
{
_jvmBug++;
if (_jvmBug>=(__JVMBUG_THRESHHOLD2))
{
synchronized (this)
{
_lastJVMBug=now;
// BLOODY SUN BUG !!! Try refreshing the entire selector.
final Selector new_selector = Selector.open();
for (SelectionKey k: selector.keys())
{
if (!k.isValid() || k.interestOps()==0)
continue;
final SelectableChannel channel = k.channel();
final Object attachment = k.attachment();
if (attachment==null)
addChange(channel);
else
addChange(channel,attachment);
}
_selector.close();
_selector=new_selector;
_jvmBug=0;
return;
}
}
else if (_jvmBug==__JVMBUG_THRESHHOLD || _jvmBug==__JVMBUG_THRESHHOLD1)
{
// Cancel keys with 0 interested ops
for (SelectionKey k: selector.keys())
{
if (k.isValid()&&k.interestOps()==0)
{
k.cancel();
}
}
return;
}
}
else
_jvmBug=0;
这个方案能比较好的在jdk 6u4之前的版本上解决这个BUG可能导致的问题。Mina和Netty没有看到有处理这个BUG的代码,如果我看错了,请留言告诉我。Yanf4j一直采用的是grizzly的方式,准备加上jetty的处理方案。当然,最简单的方案就是升级你的JDK :D
泰山在线的周利朋友对xmemcached做了很多测试,他发现了一个比较严重的BUG,在linux平台的重连机制有时候会失效。表现的现象是这样,正常连接上memcached之后,kill掉其中的一台memcched server,xmemcached会开始自动重连这台server直到连接成功,然而事情没有像预想的那样,现象是有时候可以重连成功,有时候却没有,如果设置了connectionPoolSize,有时候建立的连接数达到connectionPoolSize,有时候却没有。他还向我描述了那时候的netstat观察到的网络情况,有比较多CLOSE_WAIT存在,这个显然是由于memcached主动断开,xmemcached被动进入CLOSE_WAIT,但是没有发送FIN的情况,如果有发送FIN那应该进入LAST_ACK而不是停留在CLOSE_WAIT。因此反应的第一个问题是xmemcached没有在接到memcached断开之后主动关闭socket发送FIN。检查代码发现其实是有这个逻辑,但是nio的channel关闭有个隐蔽的问题,就是在SelectionKey.cancel之后还需要调用select才能真正地关闭socket,这里会有个延迟,另外,为了防止CLOSE_WAIT现象的再次发生,设置SO_LINGER选项强制关闭也是必须的。做了这两个修改后,build了一个临时版本请周利朋友帮忙测试,重连失败的情况有所减轻,但是仍然会发生。因此根本的问题不在于CLOSE_WAIT的处理上,通过检查代码发现了下面这段代码:
if(!future.isDone()&&!future.get(DEFAULT_CONNECTION_TIMEOUT,TimeUnit.MILLISECONDS){
 
}else{
connected=true;
}
可能你已经发现问题在哪。这段代码的意图是通过future.get阻塞等待连接成功或者失败,如果失败做一些处理,如果成功将connected设置为true。这里判断失败有两个条件,future.isDone为false,并且future.get也返回false才认为失败,问题恰恰出在这里,因为future.isDone可能在连接的失败的情况下返回true,而这段逻辑将这种情况误判为连接成功,导致重试的请求被取消。修改很简单,将future.isDone这个条件去掉即可。
回想起来,我也忘了当初为什么加上这个条件,这里感谢下周利的帮助,并且向使用xmemcached的朋友们提个醒。这个问题在win32平台上不会出现(比较诡异,估计跟并发有关),在linux平台出现的几率比较大,预计在10月份发布的1.2.0-stable中修正,这个stable版主要工作是修复BUG。欢迎更多朋友反馈问题和BUG,我将及时修复和反馈。
By starting at the top of the triangle below and moving to adjacent numbers on the row below, the maximum total from top to bottom is 23.
3
7 4
2 4 6
8 5 9 3
That is, 3 + 7 + 4 + 9 = 23.
Find the maximum total from top to bottom of the triangle below:
75
95 64
17 47 82
18 35 87 10
20 04 82 47 65
19 01 23 75 03 34
88 02 77 73 07 63 67
99 65 04 28 06 16 70 92
41 41 26 56 83 40 80 70 33
41 48 72 33 47 32 37 16 94 29
53 71 44 65 25 43 91 52 97 51 14
70 11 33 28 77 73 17 78 39 68 17 57
91 71 52 38 17 14 91 43 58 50 27 29 48
63 66 04 68 89 53 67 30 73 16 69 87 40 31
04 62 98 27 23 09 70 98 73 93 38 53 60 04 23
NOTE: As there are only 16384 routes, it is possible to solve this problem by trying every route. However, Problem 67, is the same challenge with a triangle containing one-hundred rows; it cannot be solved by brute force, and requires a clever method! ;o)
最简单的方法就是穷举,从根节点出发,每个节点都有两个分叉,到达底部的路径有估计有2的指数级的数目(有会算的朋友请留言,我的组合数学都还给老师了),不过这道题显然是符合动态规划的特征,往下递增一层的某个节点的最佳结果f[i][j]肯定是上一层两个入口节点对应的最佳结果的最大值,也就是f[i-1][j]或者f[i-1][j+1],递归的边界就是定点f[0][0]=75。因此我的解答如下,考虑了金字塔边界的情况,数据按照金字塔型存储在numbers.txt中,
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Euler18Problem {
public static void maxSun(int[][] a, int rows, int cols) {
// 结果列表
int[][] f = new int[15][15];
// 路径,用于输出计算路径
int[][] path = new int[15][15];
// 递归边界
f[0][0] = a[0][0];
path[0][0] = 0;
// 递推
for (int i = 1; i < rows; i++) {
int col = i + 1;
// 决策
for (int j = 0; j < col; j++) {
// 左边界
if (j - 1 < 0) {
f[i][j] = f[i - 1][j] + a[i][j];
path[i][j] = j;
} else if (j + 1 > col) { // 右边界
f[i][j] = f[i - 1][j - 1] + a[i][j];
path[i][j] = j - 1;
} else {
// 处于中间位置
if (f[i - 1][j] <= f[i - 1][j - 1]) {
f[i][j] = f[i - 1][j - 1] + a[i][j];
path[i][j] = j - 1;
} else {
f[i][j] = f[i - 1][j] + a[i][j];
path[i][j] = j;
}
}
}
}
// 求出结果
int result = 0, col = 0;
for (int i = 0; i < cols; i++) {
if (f[14][i] > result) {
result = f[14][i];
col = i;
}
}
// 输出路径
System.out.println("row=14,col=" + col + ",value=" + a[14][col]);
for (int i = rows - 2; i >= 0; i--) {
col = path[i][col];
System.out.println("row=" + i + ",col=" + col + ",value="
+ a[i][col]);
}
System.out.println(result);
}
public static void main(String[] args) throws Exception {
int rows = 15;
int cols = 15;
int[][] a = new int[rows][cols];
BufferedReader reader = new BufferedReader(new InputStreamReader(
Euler18Problem.class.getResourceAsStream("/numbers.txt")));
String line = null;
int row = 0;
while ((line = reader.readLine()) != null && !line.trim().equals("")) {
String[] numbers = line.split(" ");
for (int i = 0; i < numbers.length; i++) {
a[row][i] = Integer.parseInt(numbers[i]);
}
row++;
}
reader.close();
maxSun(a, rows, cols);
}
}
执行结果如下,包括了路径输出:
row=14,col=9,value=93
row=13,col=8,value=73
row=12,col=7,value=43
row=11,col=6,value=17
row=10,col=5,value=43
row=9,col=4,value=47
row=8,col=3,value=56
row=7,col=3,value=28
row=6,col=3,value=73
row=5,col=2,value=23
row=4,col=2,value=82
row=3,col=2,value=87
row=2,col=1,value=47
row=1,col=0,value=95
row=0,col=0,value=75
1074
ps.并非我闲的蛋疼在半夜做题,只是被我儿子折腾的无法睡觉了,崩溃。
|