书
我在当当买了一本笑谈大先生,陈丹青写的,大先生是鲁迅。
我花了两个晚上,把它看完了。
书是陈丹青演讲稿的集子,通常,演讲稿里总会有些废话,滥调;这本书的好处是这些寻常的演讲里有很多精彩的议论。
Clean Code, Uncle Bob的书。这本书豆瓣上好评如云。我零零碎碎的看了大概3/4,老实说,我有点失望。它或许对你的胃口,但它不对一个靠谱的,有理想的,心怀傲气的程序员的胃口,哈哈,你自己看看就知道我为什么这么说了。
观止-微软创建NT和未来的夺命狂奔,这本书看的我的我心潮澎湃啊。有兄弟说拿到手后通宵看了一晚上,就是停不下来。我也停不下来的看。
最近看了Effective C++和C++ Primer,并且单独做了两个还蛮算重要的C++项目(都是分布式文件系统),我觉得我开始慢慢步入靠谱的C++程序员的行列了。不过,等我尝试去细读STL(因为我发现libstdc++的实现总是有许多奇怪的问题,甚至都不敢深入的使用STL,因此才打算细读)实现的时候,我发现我的生命被迅速的浪费了。我又买了一本More Effective C++。我打算再看看这本书,我的CPP之路或许最远也只能走到这里。不知道有没有兄弟觉得微软的STL实现要不libstdc++好?
做为一个自觉已经靠谱的Java程序员,我已经很少看Java的书了,现在市面上也确实没啥可看的Java书。
这两天在看TCP/IP高效编程 , TCP/IP卷3。做了两年的后端,我觉得自己对网络编程有了蛮多零碎的了解,但缺乏系统性。于是买了这两本书看。不过这也仅仅是两本零碎的书,果然,要想破除这种零碎的感觉,似乎除了看RFC别无它法?
BUG
本来上一篇日志是: Java与性能,写了一半,发现实在是心有余而力不足,于是就让它躺在了草稿箱,等将来对这个问题有更深入了解的时候,再写。
1月份开始感冒,之后就是好不了的咳,脑子都咳坏了,写代码时考虑不周,结构不好,BUG多多,清醒的脑子跟不清醒的脑子差别还真是蛮大的。
第一个麻烦是日志。
我接手的一个CPP 写的系统,用的日志系统是自己做的,为了支持32位机器,我给它增加了一个快到2G时,就按size轮转的机制,没想到这样一个简单的功能竟然出现了3个BUG,折腾了我快1天,SA被我折腾了好几次。
第二个麻烦是一个Java写的Mail相关系统。
为了支持Mail解析器可以不堵塞的写入数据到分布式存储系统,我在这两者之间加了Buffer层,这个Buffer由内存和文件组成。后来有了新的需求,需要有多个观察者观察Buffer上的数据变动,以便异步的将数据组装并发送给不同的需要数据的Server(比如,反垃圾服务)。
- 我第一个土鳖的地方是:Buffer上的观察者引用计数计算错误,从而导致Buffer用完后无法被收回;
- 第二个土鳖的地方是:采用了可重入(每个观察者拿到的是独立的Buffer对象,虽然它们底层的数据存储部分是共享的)的方式来读Buffer(DataBuffer.read(buf, off, len, bufPossion),可虑到内存Buffer,该可重入方式实现简单,就算是文件做的Buffer,在内核较新的Linux Server上,该操作也可以被映射为(ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);的系统调用),不用加锁,非常迅速。这种考虑其实是不错的,但是我竟然忘了自己去维护当前的POSITION,导致buf永远从position 0开始读数据。
- 第三个土鳖的地方是:在向外写数据时,我用了Mina框架来维护客户端和数据传输。我写了类似代码:
if (cachePosition > 0) {
buffer = IoBuffer.wrap(cache, 0, cachePosition);
session.write(buffer);
}
悲剧的是cache是一个byte array buf, 就是说该方法调用完后,cache是完全可能被新的数据更新的,可是,IoBuffer is backed by cache. session.write()又是异步的,这就导致了一件悲剧的事情:真正发生的数据可能乱掉,或者被覆盖掉。最后改成了:
if (cachePosition > 0) {
buffer = IoBuffer.wrap(cache, 0, cachePosition);
WriteFuture future = session.write(buffer);
future.awaitUninterruptibly();
}
这个悲剧的事情折腾了我1整天,写了n多测试代码来找原因。 - 其他还有一堆考虑不周,架构乱的土鳖地方。
头脑要清醒,能力急需提高啊。
新的读书计划
是不是常常会有这样的问题:做完了计划,反而不去执行了?对于觉得需要做的事情,立刻去做,有时候会比什么样的计划都要好。
最近买了很多书,一本Masterminds of Programming,一本Effective Java2中文版,一本Programming Pearls,还有一本著名的GEB(哥德尔 爱舍儿 巴赫:集异璧之大成)。
Effectiva Java2与Programming Pearls是重读,Masterminds of Programming是当八卦书看的,GEB是源于我对某些问题的爱好。
Effectiva Java2真是好书,非常适合Java库的开发人员阅读,就算是新手,如果按着它的准则做,也真能写出漂亮的代码,所以我特别推荐Java新手看这本书,它可以帮你的代码和人生减少很多错误。
Effectiva Java2讲的是招数,但它的招数特别:招数成了,心法也会了。我打算在这周把它看完。
Programming Pearls是经典的面试书,原来的一本在搬家的时候丢了,就又补了一本,既然补了,就打算重看一遍。
MasterMinds of Programming,我虽然当他是八卦书,但是看起来真的很累。很多陌生语言的特性,我不熟悉;他们的文化,我也不了解;于是就很难去理解他们的创造者的呓语。
GEB,我读了开头,很精彩,打算让老婆也看一下;或许哥德尔和他的世界太过奇异,有的人认为这是一种绚丽,有的人认为是悲剧。但其实,你是不是偶尔也会这样觉得:不完备性其实更符合直觉?哥德尔,图灵,丘奇的工作只是在讲述一件直白的事情?
看私人的书要带只笔,涂涂写写,我总是匆匆的过了很多精彩的书,真是遗憾。
关闭一个线程
注意:由于个人能力及知识水平问题,以下内容可能有原则性错误,请批判着阅读!
最近搜东西,又搜到了这个关于pthread_kill的话题,相信有不少人看过这个讨论。NPTL的作者真霸道。看了几遍pthread_kill的手册,然后放心的写代码,然后得到一个core dump,相信谁都会不爽的。
但用pthread_kill(pid, 0)来检测一个线程的运行情况是一件不靠谱的行为,虽然这是非常普遍的行为:Joinable的线程完结了,还没执行pthread_join(),这个状态,pthread_kill(pid, 0)能正常识别么?规范没有说,其实很多实现都没有能够区分这两种状态吧。(1 运行中, 2 运行结束,还没join)还有一种比较不大可能发生的事情——通常,你的代码能提前发现这个问题:pid如果被重用了呢?这种时候,其实你检查的是一个全新的线程。
pthread_kill()是一个非常难用的方法,在确定你的线程能正确的处理信号之前,不能随意的向某个线程发出信号;如果你的程序不能正确处理,你的整个系统都会由于这个方法的调用而终止。出于对其的害怕,我从来没有在自己的代码里使用过它。
如果要关闭一个Server线程,其实可以为线程提供一个shutdown()方法,像所有Java程序员都会的那样;如果要立刻终止一个线程,可以用pthread_cancel(pthread_t),当然这个方法也会有风险(你不知道你要关闭的线程是不是已经终止了,你也不知道你要关闭的线程或许是一个全新的线程)。所以,pthread_cancel(pthread_t)方法需要在同步块内调用,调用前,确认 线程处于有效的状态。而线程也需要在同步块内才能改变自己的状态。
我觉得安全的做法,不论是pthread_cancel,或是pthread_kill,都需要利用同步机制,确认线程先有效,再操作。当然,很多人不这么认为,因为他们的代码从不这样写。
thrift, close wait
上篇日志提到Thrift ThreadPoolServer有时候会出现较多的close wait状态,有朋友问我这是不是thrift的bug?写过Server比较多的同志们应该能意识到这个问题的原因,不值得说,可是我今天实在是太郁闷无聊了,我就写写我的想法吧。
我觉得这当然不能算是Thrift的Bug,如果出现了这样的问题,其实是因为错误的选择了Server的类型,错误的实现了Client,过于保守的Server Max Connection配置等等原因。
对于ThreadPoolServer而言,每一个客户端连接,Server端都需要提供一个固定的线程来维护,在空闲时,线程堵塞在read()操作,等待客户端数据的到来。Thrift ThreadPoolServer中使用的默认线程池是定长线程池,意味着Server端能提供的线程池数是有限的。当线程用完时,新的连接将不能得到Server殷勤的服务,它不会在乎你的生死,你必须等待。
举个例子:
我有一个用Thrift ThreadPoolServer(使用SimpleThreadPool)实现的Server,最大支持100个连接,现在有100个客户端连接到我的Server。实现客户端的程序员都很在乎TCP连接建立的开销,因此,他们都维护了一个长连接。这个时候,如果有第101个客户端连接到Server了,会发生什么情况呢?
- Server会接受这个连接,连接成功建立;
- Server没有合适的线程来处理这个连接,于是将这个连接放到暂存列表;
- 如果这个时候有线程空闲了,则一切顺利,这个线程将接管这个连接;
- 但遗憾的是,我们没有空闲线程,所以这个连接一直处于空闲状态,直到客户端程序timeout(如果设置了timeout的话);
- 连接timeout,意味着暂存列表里的连接已经失效了,此时对应的socket处于CLOSE_WAIT中(出现了本文开头的情况),遗憾的是,我们依然没有空闲的线程来处理这个连接,所以它一直处于CLOSE_WAIT中。
- 终于,某一个时刻,有一个客户端关闭了连接,我们有了空闲线程,它去查看暂存列表。发现有一个socket fd,尝试去接管它,对这个fd执行read(),然后得到一个Connection Reset error,终于,我们可以优雅的关闭它了(CLOSE_WAIT结束)。
- 以上就是全部的故事。
那么,要怎么办?
- 如果连接数太多,为什么不用NonBlockingServer呢?Thrift有基于libevent的实现,虽然它的ThreadPool限制了NonblockingServer的性能,但是,你可以方便的实现一个存/取线程更高效的ThreadPool.
- 你可以实现一个TimeoutCachedThreadPool来替代SimpleThreadPool.
- 提高Max Connection的值.
tcp通道关闭时,发生了什么?
前段时间,同事发现一个采用Thrift TheadPoolServer作为TCP的服务出现了大量的CLOSE_WAIT状态的socket。当第一次遇到这种问题的时候,你可能会有如下的问题:
- 什么是CLOSE_WAIT? 为什么会发生?
- 还有其他类似状态我不知道的么?
其实,你真正想问的是:
- 当tcp通道关闭时,发生了什么事情?
- 我该怎么办?
TCP通道是一个连接,连接的两端都可以向通道里写数据或者从通道里读数据,连接的两端都可以发起关闭操作。整个TCP通道的关闭流程如下:
A(socketfd:10) <——–TCP Connction ———->B(socketfd:20)
- 关闭A,则A向B发送FIN;
- B接受到FIN后,返回一个ACK,表明收到了关闭通知, 当B返回ACK后,状态就转变为CLOSE_WAIT(!)了,它在等待什么呢?等待程序显式(手动)的关闭socket B。如果打开B的程序在发出ACK后,没有做任何处理,那么它就一直处于CLOSE_WAIT中了。
- 如果程序显式的关闭了B,那么B会向A发送一个FIN,然后B就处于LAST_ACK状态了;
- A在接受到B的FIN后,发出最后一个ACK,此时A就处于知名的TIME_WAIT状态了。TIME_WAIT时间一般会比较长。
如何避免CLOSE_WAIT? 太多的CLOSE_WAIT通常意味着你程序中存在着严重的问题:没有正确处理socket的关闭事件,去查看代码吧。
如何避免TIME_WAIT? 通常,这是一个不应该问的问题,应该先问自己,TIME_WAIT真的给你带来麻烦了么? TIME_WAIT是可靠性的保证机制,没有必要去避免它。但或许,你可以尝试一下使用RFC1323。如果某一端实在是有太多的TIME_WAIT而造成问题,我建议的策略是:
- 尽量避免TIME_WAIT过多的一端主动关闭socket
- 使用SocketPool,避免频繁创建/关闭socket
过去和近况(三)
11月初,我把座位从公司7楼的一端搬到了另一端(我至今还没弄清楚具体的方位),就这样,从网易杭研搬到了网易邮件部。
一切都在从头开始。
我读了一遍cyrus-sasl的代码,弄清楚其逻辑及各种结构后,为它写了一个新的认证机制。测试完后,我想我短期内不会再有C/C++的活了吧。
抽空大概的浏览了ZimbraServer的代码,我们并不打算从里面借鉴什么,我只是想尽快熟悉下邮件系统到底是怎么回事。我开始写一些底层的模块,主要是存储/解析,以及抽象一些结构,用Java开发真是感觉良好。
正在集中精神开发新Server的时候,我又同时参与了一个C++项目N(目前就我1个开发人员),项目已经开始稳定,不过还有很多后续功能要开发。我记得在1个月前做过一个梦,梦见自己开始接手这个N项目,程序不稳定,服务器一直坏,压力很大。没想到前半部分成真了,后半部分是否成真,那还得看天意。
过去和近况(二)
我和同事两人干了一件有趣的事情,我们把自己的Map-Reduce/DFS系统与Hadoop,以及另外一套某搜索引擎使用的Map-Reduc/DFS做了较为深入的分析和性能/功能测试。测试的结果非常符合我们的期望,NEMR/DFS的组合从文件的存取效率,Map-Reduce的任务分发/容错/易用性等都要明显好于Hadoop与另一套系统。当然,这三个系统的定位并不一致。
四月份,我接到的任务是重新整理DFS的Java接口,除了整理了一下包结构与异常机制,我基本没有做什么有价值的事情。原有的Java代码有较深的C/C++风格,另外,代码结构虽然凌乱但能完美的工作,我有些无从下手。当把整理完的结果commit,并关掉任务的时候,我有一种深深的负罪感。
我们组与其他部门开始合作一个应用于廉价存储设备的支持大文件的分布式文件系统(大文件意味着整个系统的元数据基本很小)。支持大文件的分布式系统开发其实比较简单,这是一个C++的项目,作为一个不是很称职的Java程序员,刚开始做C++项目的时候充满了惊奇的体验。我只负责一些边缘的开发工作,比如为这个文件系统做一个shell, 并参与了部分系统对外接口的开发,做为一个C++初学者都算不上的家伙,我在这个项目里每写一行代码都十分的小心,倒也没有出什么问题。
我一直期望把分布式文件系统的接口做的不让人惊奇。处理DFS里的文件像本地文件那样open/write/read/close/unlink,支持流式写入等。(最近开始全面接手上面的项目,可以对其好好改造啦!嘿嘿)
6月份开始了一个较大的项目,我们组开始做一个全新的支持廉价设备的(且低碳省电的)大量小文件存储的分布式文件系统。我负责开发文件的对外Java接口,以及文件系统的存储节点(C++)。其他同事开发管理工具和master。由于考虑到自己只是一个C++新人,我有意识的只使用C++的一个子集来开发系统以避免额外的复杂性。虽然在一开始就约束自己不要重做轮子,可是到了最后,还是造了很多基础设施(线程安全的集合, BlockingQueue, ThreadPool, Runnable, DailyRollingLog等等), Hacker了一个库的部分代码。可能是因为没有找到简单,可靠的C++实用库吧。这次开发感觉很顺利,我想很大程度上要归功于我们使用了一个非常酷的通信/rpc框架,它引起的麻烦远远要小于它所克服的麻烦。
之后的精力花在了大量线上数据的迁移/线上系统的维护上面,大概有10000亿个线上文件需要迁移到新开发的文件系统。
过去和近况(一)
从2009年7月份从天津大学毕业到现在,在网易杭州研究院工作已经快1年半了,这一年半发生了很多事情,跟老婆登记了/买房了/买车了/买了Mac,还做了一次甲状腺囊肿的手术,住了一个星期院,低调的涨了一回工资,生活压力巨大,基本入不敷出。
刚入职的时候那一个月里,基本都在熟悉组里的东西。分布式开发,都我而言,是个非常新鲜的领域。记得那段日子里,我大把大把的时间都在读Map-Reduce/DFS相关的论文,阅读/部署/安装/测试组里的MPR/DFS系统,阅读Hadoop/HDFS的代码。那个时候不知道自己有没有在进步,只是一直在尝试着往前努力。
慢慢的我开始负责完善我们的Map-Reduce系统,一点功能一点功能的加,到12月底,基本上重新调整了系统的架构,我们把他叫2.0。1.0->2.0从版本上看可是个巨大的进步,统计组的同事也开始使用新版本来进行日志分析/数据统计,由于系统压力的不断增大(说明我们公司在往前发展:) ),以及我欠缺的经验,2.0版本运行的非常不稳定,基本上隔几天就会有许多计算任务失败的情况发生,给统计组的同事填了许多麻烦,我也非常的懊恼。同事(大牛)建议我重写通信模块,先写一个基于事件的NIO框架,然后基于该框架重写所有通信模块,而不是像现在那样把一堆异步通信代码参杂在业务逻辑里。
我大概花了3天时间,写出了一个包含Server/Client,基于NIO,基于事件的异步通信框架,在处理Java NIO 的一些问题(包括JDK BUG, READ/WRITE事件的注册策略等 )上参考了Mina等其他流行系统的实现。用该框架重写Map-Reduce系统的通信模块后,果然少了很多之前觉得怪异的BUG。
之后的开发算是锦上添花之作,自定义调度策略/API支持/流程优化/动态改变作业参数 等,另一位同事实现了将我们的Map-Reduce与Hadoop在接口上的兼容,为此我们也改动了我们的DFS系统,做完这些工作后,我们实现了与HIVE的兼容!到2月底,这些工作基本完成了,而我,抽空去医院做了一个甲状腺囊肿的手术。
<待续>
