妄谈时间序列表格型大数据系统设计

一直在特定领域的分布式系统一线摸爬滚打,曾取得一些微不足道的成绩,也犯过一些相当低级的错误。回头一看,每一个成绩和错误都是醉人的一课,让我在兴奋和懊恼的沉迷中成长。自己是个幸运儿,作为一个 freshman 就能够有机会承担许多 old guy 才能够有的职责。战战兢兢、如履薄冰的同时,在一线的实作和思考也让我获得了一些珍贵的经验,却直至今日才够胆量写出来一晒。这篇文章标题前面是“妄谈”两字,所持观点未必被所有人认可,我姑妄言之,有心之人姑听之。若有些友好的讨论,亦我所愿也。

我做的虽然也是分布式系统,却不够胆去讨论通用分布式系统的设计原则。因而这篇文章的主题限定到一个特定领域的分布式系统设计,这样即使别人有疑惑,我也可以把 TA 拖到我擅长的领域打败 TA :)

既然要限定,我们需要给这个系统下个定义,就有必要解释一下标题。

大数据(Big Data),这是由于分布式系统和云计算的风靡而变得很火的一个词。那么多大的规模才算大数据呢?目前没有定义,但要讨论这个问题,就必须给个确定的范围。在本文中,这个范围暂时定义为 10TB~1PB 的数据量。为什么是这个范围?我的理由是,小于 10TB 的数据规模有比较多的可选方案;大于 1PB 的数据规模,讨论的意义不大,下面会谈到。

表格型数据,是指数据是有结构的,类似于关系型数据库中的表,但不是关系型,至少不是完整的关系型。在大数据的范围内,不能说完全没有关系型的需求,但这个需求实际上是很小的。因为关系操作的复杂性,使得其在大数据上的性能非常差,此类的需求往往使用数据冗余等其它方式来实现。是性能原因,而不仅是实现难度导致它不被需求。

时间序列数据,是指数据是按照时间产生的,跟随时间而变化的分析型数据。其实分析型数据一般都是时间序列的。与操作型数据不同,在分析型数据中单单一条记录的信息是很小的,只有与其它数据进行对比、组合、分解,这条记录才会体现出其价值。

在这些限定词下,这个系统的用途就比较清楚了。它可以被用到很多地方:比如网站访问统计(Google Analytics 和百度统计)、APP 的数据统计、集群服务器状态收集、在线广告的展现和点击量等等。它是一个数据仓库,但庞大于一般的数据仓库,功能需求却少于一般的数据仓库,而且很强调性能。在这个级别上,我还没看到成熟的开放系统解决这个问题(也许我是孤陋寡闻),基本上每家都是自己实现,所以它也更值得讨论。

由于不知该如何系统地探讨,我下面只能把自己发散的思维整理为一条条简单的原则,可能会有很大的跳跃性。但是,谁在乎它连不连贯呢?

latency 对你很重要时,不要采用分层设计,优化做得越底层越好

事实上,对于有兴趣做这样一套系统的公司,latency 都很重要。因为 latency 不重要时它们完全可以使用 HBase。而且,当你有超过 1PB 数据时,你会发现其中很大一部分的 latency 不重要,那剥离出来这部分,用 HBase 吧。

在这个数据量上,必须采用分布式的实现方案。但不要为了系统逻辑的清晰而做存储层与应用层分离的实现,像 BigTable 那样。因为 locality 可以显著地降低 latency,做了存储层和应用层的分离,那你就放弃了很多可以优化的地方。否则你必须破坏分层的封装性,像 Facebook 对 HBase 做的那样。

MySQL 不是一个选项,分布式 MySQL 也不是,分布式 KV 也不是,做自己的系统吧

总会有人问这些问题:为什么数据库(分布式数据库、分布式 KV 存储)不能用于这样的场景?我只能说,原因关键是上面三个形容词:时间序列数据、表格型数据、大数据。此外可能还要加上性能、成本等其它因素。

问出上面这个问题的人,其实都可以去用数据库或者 KV 系统,大部分情况下他们的需求会被满足。因为实践过且不满足需求的人,不会问上面这个问题,所以自己找出为什么吧,更容易些。

索引很重要,但要注意控制粒度

上面说过,对于分析型数据而言,单条记录没那么重要,所以快速地获取一条记录不会成为此类系统的目标,而且索引会降低数据更新的性能。但是能不要索引吗?开玩笑,那你怎么查询!索引必须要有,但要考虑到业务场景,做到合适的粒度。所谓合适的粒度,就是能快速获得目标数据而又不至于影响数据更新的性能。

内存很重要,能省则省,能用就用完

内存的重要性大家都明白,但很少人能真正理解。能省则省——说的是不要用浪费空间的数据结构;能用就用完——说的是在保证服务器能正常工作的前提下,使用最多的内存。

IO 很重要,做任何能减少 IO 次数和数据量的事,如果要折衷,选择优化次数

对于分析型数据而言,CPU 向来不是瓶颈,IO 才是。做任何能减少 IO 次数和数据量的事,比如各种缓存(块缓存、索引缓存、请求结果缓存),比如数据压缩。如果在减少 IO 次数和减少数据量上做折衷,选择减少 IO 次数,除非这会导致数据量爆炸。

即使没分层,也不要随机写

即使能直接访问到本地文件系统,也不要使用随机写,不要向一个文件中插入内容,而是将更新与基准合并写入另一个文件。这样性能更高,真的。

支持 CRUD?不,只支持 CRA,A for aggregate

其实很多数据都可以表示成时间序列型数据,例如 MySQL 的数据表内容完全可以用时间序列的操作日志来表示,这也是 Twitter 首席工程师 Nathan Marz 提倡的,他说有 CR 就够了。虽然我没有那么极端,但是朋友,我们处理的就是时间序列数据啊,所以我们完全不需要 UD。增加 A 的原因是,聚合会减少数据量,聚合会提升查询性能。

一定要压缩数据,选择一个合适的压缩算法

原因很简单,这能够减少 IO 数据量。但不要傻乎乎地压缩整个文件,跟 BigTable 学,分块压缩。考虑到对数据更新和读取的性能偏重不同,选择对自己合适的压缩算法。因为列存储的压缩比一般而言更高,所以

如果能做列存储,就做吧

尽量分离更新和读取的压力

如果数据需要做清洗,可以聚合,那么在导入系统前做这件事,而不是让承担查询压力的系统做这件事。

实时性没那么重要,批量更新会让你更轻松

如果能接受一天的延迟,就每天一批;能接受一个小时的延迟,就不做分钟级更新。更新次数越少,预聚合效果越好,数据量越小;更新次数越少,一致性越容易保证;更新次数越少,事故处理越从容。实时更新的话,很多事情会变得非常复杂,尤其是故障处理。

用数据冗余实现关系型需求或者高性能需求

如果有关系型运算需求,一定要逼 PM 改掉。实在改不掉,在导入系统前(或者过一段时间后)计算得到结果,直接导入到系统中。高性能需求也是这样,提前在系统外聚合好再导入,让系统做最少的事情它才能更快。

分布式架构?不重要,重要的是可靠性

至于采取什么样的分布式架构,其实不重要。只要它能实现 IO 的(大致)负载均衡,并且可靠就够了。另外,值得一提的是,如果想实现中心机,选举,分片自动分裂、合并、迁移等 fancy 分布式技术,首先想想自己公司是不是行业领导者。Perfect is the enemy of good. 对于很多人来说,Zookeeper 足够了。

好的运维工具,比完美的设计更靠谱[20110222]

在完成一个大规模系统时,往往很难做到完美,尤其是当这个完美设计很复杂时。事实上 fancy 的功能也不是不能折中,例如可伸缩性通过运维工具而不是内建于系统中实现,其复杂度会大大下降,稳定性会大大提高。所以如果没有足够的能力或者时间去实现一个完美的系统,不如好好地去做一些简洁方便的运维工具。

借鉴别人经验

这个不用我解释了吧。找一切可利用的信息,和一些人讨论,自己做决定。 :)

(暂时写到这里,但我可能会更新这篇文章,当我想到更多时。)

灯泡接口

我以前是一个略具 geek 精神的人。现在不算了,写出来的好玩的计算机技术文章也没那么多了。虽然变无趣了,但我还有生活,所以我决定发掘一下其它的领域。生活中的琐事虽小,仔细琢磨下却有一些有趣的知识在里面。某些人从极小的时候就掌握的常识,对其他人来说可能到老都不明白。典型的例子有识别地图、指南针、手表或者分辨麦苗、韭菜等。

今天说的灯泡接口,也是类似。我从小到大,做过不少次爬上跳下换灯泡的活儿,本来觉得是一件很稀松平常的事情。但自食其力后才发现,原来也没那么简单,至少面临着一个复杂的问题:怎样选购正确的灯泡?

在我老家那个落后的小城镇里,很多东西都是二元的。免费电视信号只有两个:县台和县教育台;(铁路)地下道只有两个:东地下道和西地下道;灯泡也只有两种:(螺)丝口或者挂口。这样购买的风险很低。白炽灯泡一元一个,实在不行的话,买两个不同接口的灯泡就完了。反正在我小的时候有过买错灯泡的经历,算不得难堪。

长大后忽然发现,这世界不再是二元的了。典型的例子就是普通灯泡接口不再是两种,灯泡商品也不再是两种,当然价钱也不再是一元。去年年初,我老婆从公司带回来一个小台灯。它有一个圆盘形的底座,底座中央是一个笔直的灯杆,看起来像是一个倒立的图钉。在图钉的钉尖儿上是灯泡的接口,有一个可爱的圆柱状塑料灯罩可以把灯泡罩起来。灯泡的接口很奇怪,看起来是丝口,但又比丝口细。包装盒上的文字介绍极少,少到几乎无法阅读。在这些几乎无法阅读的文字中,我找到一个神奇的代码:E14。凭着直觉,我认出这应该是灯泡接口的型号。

我用来认知灯泡世界的模型改变了,只好重新建立模型。然后我才知道,原来我平常说的丝口,学名应该叫做“爱迪生螺旋(Edison Screw)接口” ,更确切一些,应该叫做“中型爱迪生螺旋(Medium Edison Screw)” 或者“E27”接口,即直径为 27 毫米的爱迪生螺旋接口。显而易见,除了E27,肯定还有其它的 E* 接口,例如上文提到的“E14”。此外,原来旧式手电筒上常见的小灯泡接口,也属于这一家族:“E10”。

从螺旋接口的型号上来看,中国普遍使用的是欧制接口。我不知道标准是如何制定的,但从查到的信息来看,中国最早的电灯公司是1861年英国商人办的“汉口电灯公司”,采用欧制接口可能跟我国 19 世纪首先被欧洲入侵的这一段历史有一定的关系。

与螺旋接口类似,卡口(或挂口,Bayonet Mount)也是一个家族 。我们通常家庭使用的,应该是“B22d”接口,即直径为 22 毫米,带双(double)接触点的卡口。此外还有射灯常用的“GU5.3”或者“GU10”接口,即插脚式U型接口,也属于卡口的一种。

在我看来,卡口要比丝口更安全,因为其露出的金属部分是不带电的,无意中摸到内部带电的弹簧突起比较困难。但奇怪的是周围的卡口灯座越来越少,我对这个现象非常好奇,却不知道其原因。

当然,除了上面说的这两个系列之外,还有其它的系列接口,可以参考这篇文档《灯头、灯座的型号命名方法及常用型号》 。不过其中一些,例如预聚焦式、凹点式或者汽车用灯接口,一般就只有专业人士才用得着了。

史蒂夫·乔布斯传

我一直羞于去崇拜某个人,死去的或者正在死去的。我总害怕他做了或者将要做一件我并不认同的事情,这就好比冬至吃饺子时吃到一块脏兮兮的硬币,不是每个人都能享受这种运气。所幸的是《史蒂夫·乔布斯传》的作者所持角度还算客观。读完这本书之后你不免去崇拜乔布斯,但又会有所保留。你会想:我真希望能达到他那样的成就,却不想成为他那样的人。

每个人做事都有自己的方法,但乔布斯采用的那种,恐怕要被归到异类中去。只有他那种天生的特质,结合独特的人生经历,还有过早成功的光环,才能造就那样强大的现实扭曲力场。跋扈如他那样,做一个打工仔的话,恐怕不出半月就要卷铺盖走人了。

在他的传奇一生中,有一股认真劲儿让人不能不佩服。包括他对事件策划、产品外观、字体、广告、包装,甚至于名片美学上的执念,无不让你觉得这家伙真倔,真难办。但当你转了一圈儿,看到那么多平庸的玩意儿时,你就会想:哦,这家伙还是有一定的道理的。

除了认真之外,他还有一点让我非常羡慕的,就是不屈于现实。软驱、吸入式光驱、玻璃屏幕、拉丝铝外框,到处都透着不妥协。但奇迹的是,那些硅谷工程师们居然办到了!这一点也被我归结为我们信息技术落后于硅谷的原因,奇思妙想得不到尊重,创新的阻力远大于推动力,有批判性,无建设性。

如果在这里赞扬他,能够列出的点还有很多。看完这本传记后,我甚至有去买苹果产品的冲动。但就像前面所说,我,一个平庸的家伙,对乔布斯的革命性的设计和产品,一直没有足够到转化为购买力的欣赏。创新是一样高投入的工作,必须有高回报的支持,而这依赖于掏腰包的人的欣赏和价值认可。对于某个产品来说是这样,对于一个公司来说,同样如此。可惜的是,很多公司的老板更像我,而不是乔布斯。

std::sort 的仿函数参数

因为习惯了 qsort 的函数指针参数,以前用 std::sort 的时候一般也是传函数指针而不是仿函数(functor)。从很多示例程序来看,貌似没有什么大的不同。但是直到今天我才醒悟,原来是示例太简单了啊!

具体来说,我今天遇到了一个问题:要对一个表进行排序,每个字段可能是升序,可能是降序,也有不同的类型,所以排序的时候需要根据这些信息进行比较。比较函数不能是类成员函数,但我又的确要用到类成员的信息,函数接口又不能变,着实发愁。愁了就只能 Google,发现原来仿函数可以轻松地搞定这件事情。

// 来自 http://stackoverflow.com/a/1902360
class MyClass{

   // ...
   struct doCompare
   {
       doCompare( const MyClass& info ) : m_info(info) { } // only if you really need the object state
       const MyClass& m_info;

       bool operator()( const int & i1, const int & i2  )
       {
            // comparison code using m_info
       }
   };

    doSort()
    { std::sort( arr, arr+someSize, doCompare(*this) ); }
};

简单点儿说,因为仿函数是个类,也可以有成员变量,构造的时候可以传参进去初始化,这样就能实现更灵活的比较方法。这么简单的道理,为什么之前我就是想不到呢?

此外值得一提的是,std::sort 要求比较的结果是 strict weak order,就是说要严格小于才返回 true。这就意味着,仅仅对比较结果取反,是无法实现逆序的。因为小于的取反不是大于,而是大于等于。

我们有过经验,如果相等的时候也返回 true,可能会导致某些标准库实现的 sort 函数指针越界,导致程序出错。所以要千万避免犯这个错误。

第一次香山

下午在电视上看了个《李献计历险记》,直接没把我脑壳看坏掉。之后七荤八素地躺在床上,在似睡似醒中迷瞪了一个小时,睁开眼忽然看到一幅奇怪的景象,从窗帘缝隙中透过的一点亮光在三四秒内以肉眼可见的速度暗了下去。直到现在我也不确定那是现实还是梦中,但可以确定的一点是,我随后陷入了白日梦后的各种恶心、难受和不适中。

周末就是这样,可能安排个一到两件事,其它时间睡也不是,坐也不是。徒留些无聊烦躁心情在那,如此一来就特别羡慕那些把日程排得满满的人。为了不让自己烦躁,我也在尝试找一些不那么无聊的事情去做,比如骑车爬香山。

这个念头由来已久,但付诸实施却只是昨天的事。有各种原因,懒惰,周末不愿意动弹;住的太靠里,出城太远;没有人一起玩,不认识路之类的。真正促使我成行的是微博上的一句话,做好一件事的最好方法,就是去做这件事。

在我很久很久以前拟定计划时,最大的困难就是不认识路。值得一提的是,这个困难一部分来自于没有好的地图,另一部分来自于一些所谓老鸟的行话。在香山路线中,出现频率最高的有以下几个:海二招、鬼笑石、果快、茶棚、马道等等。理解这几个名字费了我不少劲,比如海二招原来是海军第二招待所的简称,而不是海淀区第二招待所;果快原来不是地名,是果园和快活林的简称。不过,惭愧地是,我现在还是不知道这几个地方在哪儿。

有人可能觉得很习惯,但我真厌倦这些个装逼的词儿。就比如拿 5D Mark II 相机非得叫“无敌兔”,佳能 EF 50/1.8 镜头叫“小痰盂”,还有各种“小白、小小白、爱死小白”等。唉,难道就不能正正经经地说话?我以后可能慢慢地会理解这些词的含义,但我也会尽力地不去使用这些词。

再说回来,在我很久很久以前拟定计划时,是希望从珠联大酒楼-海军第二招待所-松鹤山庄那个香山入口进去,骑防火道熟悉一下香山的地形。以后有机会了再跟着别人走山地。但没成想到的是,因为什么森林公园养护完成开放之类,除正门外的其它门大多关闭不让走了。有一个老大爷在那拦各种车,行人如果不是当地居民也会被拦下。

无奈只能另想它法,有人给我说珠联大酒楼南面不远有一个山地入口。于是我就战战兢兢地从一个挂着军事禁区牌子的路口进去了,进去之后右转走到尽头有个看起来像部队的院子。大门右侧有条小路,我就很冒失地骑了进去。刚开始没有什么坡,还能骑。

山路的开始阶段
山路的开始阶段

骑没多久就到了一个挺长的围墙边,从那儿开始就有些变态了。再加上从我家到香山路上已经花了 30 公里了,体力也跟不上,于是很多坡就是推过去的了。下面是一个特别变态的地方,下降约有一米,路外侧有个大石块,内侧小沟不到一尺宽,还拐个弯儿。我觉得如果不从大石头上飞过去,走内侧不倒也得磕牙盘,只好无奈地扛着车过去了。

变态小坡
变态小坡

实话说,走的这段山路我是又累又怕,我这种平原长大的孩子哪儿骑过这种路呀?左侧是石壁,右侧是山崖,掉下去即使有小树挡着,也好不到哪儿去。我是好不容易捱到了防火道的出口啊!这个出口外面应该是梅园。

防火道-梅园
防火道-梅园

回来后我回顾了一下这段上山线路,我估计我走的可能是果园的上山路线,但是后来可能走岔了,所以才从梅园上了防火道。中午到下午这段路上太阳不多,基本上都在阴影下,不会很热。虽然看到防火道很兴奋,但无奈地发现自己已经木有力气了,于是只稍往上爬了一点儿,就溜车下去了。

不知名建筑
不知名建筑

唉,本来想继续写点儿的,被一个工作电话给弄烦了,罢了。

TCP Fast Open by Google 浅析

Google 将在今年 12 月的 ACM CoNEXT 会议上发表他们在改善 Web 应用响应时延方面的一个工作,通过修改 TCP 协议利用三次握手时进行数据交换的“TCP Fast Open”。虽然 paper 是两天前才释出,但相关的 RFC 草案则早在 2011 年 3 月份就提交到了 IETF,并且在两周前进行了一次 UPDATE,这里是 DIFF

对于 TCP Fast Open 方案的内容,淘宝的一个朋友已经根据 RFC 草案进行了解读。我就不再赘述,感兴趣的朋友可以去看 paper 或者 RFC。我这里只是想讨论一下这个东西的应用前景。

由于对背景并没有做深入了解,我相信已经有很多人尝试去做过类似的工作,但我想类似的工作应该没有得到过大规模的应用。对于已经成型很久很久的 TCP 协议,让人很难有修改它的欲望,因为改那么底层的东西意味着很多很多的麻烦。

但是是否愿意付出代价,有一个前提是有没有足够的好处。TFO 给出的好处是:在 RTT (Round Trip Time) 比较低时,客户端页面加载时间优化大约在 4%~5%;RTT 越长,好处越大,平均大约在 25%。

Google TCP Fast Open Evaluation
Google TCP Fast Open Evaluation

除了页面加载变快改善了用户体验之外,TFO 给服务器端也带来了一些好处。由于每个请求都节省了一个 RTT,相应地也减少了服务器端的 CPU 消耗。paper 中给出的数据是每秒事务数由 2876.4 上升到 3548.7。

虽然 paper 中大部分时间在强调 TFO 对 web 页面加载的显著加速作用,但我认为即使 TFO 能成为互联网标准,它目前的状态离成为标准还有很长一段距离,因而在短期内它是无法影响到主流互联网世界的。但这并不意味着它没有机会,依小弟的愚见,目前它的推广应用可能有两个方向:

1. 移动互联网。移动互联网的 RTT 目前远大于传统互联网(常理推测,需数据支撑),因而一个 RTT 节省的效果无法被忽视;另外移动互联网终端操作系统多样化,不像桌面终端系统那么单一。 Google 自己就掌握着其中一个很重要的 android,百度也计划推出自己的“易平台”。这些互联网公司有动力去改善移动用户访问自身网站的用户体验。

2. 互联网企业数据中心。虽然数据中心内部访问时延很低,但对于典型的请求/响应的服务而言,减少一次 RTT 带来的好处还是有吸引力的,最起码能减少计算能力浪费和增加吞吐吧。再加上很多企业内部使用的都是定制的开源操作系统和定制的网络库,升级的代价并不是那么高。如果我是企业基础设施的负责人,我想我会很慎重地考虑这个方案的。

代码行统计工具-CLOC

在工作中有时会有需要统计代码的行数,一般会用 wc 给出一个大致的结果。只不过在源代码文件分布比较分散,且存在多种不同类型语言的源代码时,wc 就不是特别适合了。

在公司内部也见过一些同事实现类似功能的脚本,但我想这应该是一个通用的需求,于是就找到了这个工具 - CLOC。其实就是一个 perl 脚本,很好用,统计报告也很清晰。在这里推荐一下。下面是一个统计 leveldb 源代码行数的例子。

$ cloc .
     128 text files.
     123 unique files.                                          
     353 files ignored.

http://cloc.sourceforge.net v 1.55  T=0.5 s (238.0 files/s, 46718.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C++                             60           2012           1258          13124
C/C++ Header                    52            968           1458           2690
HTML                             3             84              0           1094
C                                1             33              7            255
make                             1             43             17            153
CSS                              1             10              1             78
Bourne Shell                     1              9             19             46
-------------------------------------------------------------------------------
SUM:                           119           3159           2760          17440
-------------------------------------------------------------------------------

关于自动分裂的思考

自动分裂是分布式系统中的一项重要技术,通常与自动迁移和负载均衡一起考虑,提供了系统的可扩展性和良好的性能。例如 Google 的 BigTable 和 Yahoo 的 PNUTS 都实现了类似的功能,我之前也认为这应该是一个好的分布式系统标配。

读了 Facebook 关于实时 Hadoop 的文章后,结合我自己在工程上的实践,我开始反思这一想法,认识到了这个功能的一些局限性。

Facebook 在打造实时 HBase 系统时,放弃了 HBase 提供的自动分裂,而专门开发了手工分裂功能。对此, Facebook 的解释是:

  1. 由于业务数据的均匀增长性,所有子表可能在相近的时间触发自动分裂,导致分裂风暴;合理安排的手工分裂可以避免这一情况,减少对生产环境的影响。
  2. 手工分裂时在某个时间,子表的数目是稳定的,有利于进行调试和调优;自动分裂时很难把握住系统中子表的变化。
  3. 在对日志文件问题进行后期处理时,子表没有分裂比有分裂要容易处理很多。因为应用日志到子表上时不用考虑是否已经分裂。

Facebook 给出的三个原因是非常合理的,我也很赞同,但我想补充一下我对自动分裂局限性的两个考虑:

  1. 较难进行事故影响评估。对于一个严肃服务来说,发生系统事故时不仅要求尽快恢复,更为紧迫的要求是迅速给出影响评估。手工分裂时运维人员对系统中子表的分布情况有着更好的了解,能够更快地做出评估(而且一般影响面也可控一些)。
  2. 较难进行数据恢复。当子表数据出现问题,或者数据源本身就有问题,要进行数据恢复时,手工分裂一方面能够准确地定位错误数据的位置,另一方面便于进行错误数据的处理(后台直接替换错误文件等,不单指 HBase)。而自动分裂时寻找错误数据位置本身就比较麻烦,由于子表可能一直在变动中,对错误数据进行处理也不容易。

从上面列出的几点来看,使用、改造或者实现一个分布式系统时,不能仅仅考虑方案是否漂亮,还要考虑到该系统的具体应用场景。脱离了应用场景的系统实现,如同漂亮的水果,吃起来不一定甜。但令人感到讽刺的是,漂亮的水果一般比较贵。

青岛行记

在种种客观、主观条件下,我和媳妇儿同时决定上周休假一周散散心。选在十一之后,主要是为了人少点儿、住店便宜点儿和玩的舒服点儿。玩的舒服点儿主要是指,十一我还得值班处理服务器告警,但十一之后别人都在上班,我就可以放心地玩了。

之所以选择去青岛,主要是因为某个人太懒,对爬山和走远路很抵触,只好到海边溜达。我在去哪儿上团购了个旅馆,一天才七十块,这样能够比较放心地多住几天。

在青岛的日子是比较闲适的,每天睡到中午才起床。起来后就背个小包去逛,逛到哪儿算哪儿。没有奢求逛完所有的地方,但是由于时间充裕,基本上有意思的地方也都去看了下,唯一没去的就是崂山...

上班以后,工作赚钱成了主要目的,即使是下班回到家,也时不时地要处理一些工作上的事情,因而平时感觉生活完全被工作给充斥了。真正抛开所有事情,手机调成静音,才回归到了比较简单的状态,心情畅快不少。

我从青岛回来之后,看了一个名为《硬汉2》的电影。整个故事是在青岛取景拍摄的,一开头就是青岛天主教堂,熟悉的画面让我感觉好生激动。想要休闲的话,在家里宅着也未尝不可。但到一个陌生的地方总是一个更优的选择。在那里,困扰你的不再是日常的琐碎小事,而是完全不同的问题域。除此之外,还有增长见闻的好处,这一点对我是颇有吸引力的。特别是在当地能够发生一些有趣的、甜蜜的、或者忧伤的故事,就再好不过了。

我不太善于描述旅行中的人和事,再加上到青岛玩纯粹是休息,没办法作为攻略分享。只好挑几张图片出来,随便看看吧。

《三体三部曲》赠书计划

上周我又重读了一遍《三体》系列,在路上,用 Kindle。

还记得原来《三体I》好像是在《科幻世界》上连载读完的;《三体Ⅱ》曾经买过一本书,不知道放哪里了;《三体Ⅲ》是读的 txt 电子版。这次重读,看的是网友精心排版过的三体系列 PDF 电子版。

第一次读时主要是跟着情节往前追,急切地想知道后面会发生什么,看重的是酣畅淋漓。这次重读,更注意细节和推理,故事情节发展的脉络,人物感情和心理的变化。虽然重读时对书中一些地方产生了疑惑,但我觉得和整个故事的图景来比瑕不掩瑜,况且还有可能是我没有想透呢?

《三体》三部曲是一个好的科幻故事,好到当想到它描述的宇宙有可能是真实的时,忍不住会打个冷战。刘慈欣是一个非常杰出的科幻小说家。在我眼里,相信也是在很多科幻迷眼里,他代表了中国当代长篇科幻小说的最高水平。我很惭愧阅读电子版小说侵犯了大刘的权益,但我实在对收藏实体书无爱,书架已经够沉了。因而我想赠出一套《三体三部曲》给我的博客读者,一来我的良心能够稍觉安稳,二来希望对扩大国产科幻的影响力有所助益。

获得这套赠书的方法很简单,你只需要在评论中留言说明你想要这套书的理由:

我想要《三体三部曲》,因为...

我会选择喜欢的一个理由,然后根据你留下的电子信箱联系你,通过某个电商(卓越、京东之类)直接发货给你。这不是广告,不是福利,也不是读后感评比,没有任何客观性,一切都凭我的主观感觉,所以对于没拿到的同学我最后只能表示遗憾。

理由可以随便写,我相信应该不会有人为了这几十块钱的书而费劲脑筋来编故事。此外,我不会考虑认识的朋友,世界已经很小了,我们有必要把它搞得更小吗?还有,仅限大陆的朋友,不然运费都可以买好几套了...

考虑到我博客的影响力,应该不会有太多理由让我挑选,那么等待的时间就稍微长一点吧。最后的时间放到 2011 年 10 月 25 日,如果到时还没有我喜欢的理由出现,我就从已有的留言中随机挑选一个送出。

★★★★★
书已经送给来自浙江杭州的 Arcthur 同学
★★★★★

Leveldb 编译错误背后的C++标准变化

在编译 Levedb 时,我遇到了这个错误:

g++ -c -I. -I./include -fno-builtin-memcmp -DLEVELDB_PLATFORM_POSIX -pthread -DOS_LINUX -O2 -DNDEBUG db/version_set.cc -o db/version_set.o
db/version_set.cc: In member function `void leveldb::VersionSet::Builder::Apply(leveldb::VersionEdit*)':
./db/version_edit.h:100: error: `std::vector, std::allocator > > leveldb::VersionEdit::compact_pointers_' is private
db/version_set.cc:461: error: within this context
...

在网上容易搜到解决方案,由于归根结底是访问控制问题,方法是把所有涉及到的的 private 变量或类型修改为 public。由于不是所有的编译器都会报错,我就很好奇产生这个错误的根本原因。

BTW: 一种不修改代码的 work around 方法是,在编译这个文件时加上 -fno-access-control 参数,这样 g++ 就不会进行访问控制检查,自然也就没问题了。这个参数同样可以用于对 private 成员函数进行单元测试。

简单地分析一下这个错误。发生错误的地方是在 VersionSet::Builder 这个类的成员函数中,而错误则是其成员函数无法访问 VersionEdit 和 Version 类的私有成员变量。VersionSet 是 VersionEdit 和 Version 类的友元类,Builder 是 VersionSet 的嵌套类。简化一下,代码如下所示:

class VersionSet;

class VersionEdit {
    friend class VersionSet;
    static int compact_pointers_;
};

class VersionSet {
    class Builder {
        int foo()
        {  
            return VersionEdit::compact_pointers_;
        }  
    }; 
};

把这段代码拿给编译器去编译,g++ 3.4.4/5 会报类似的 `int VersionEdit::compact_pointers_' is private 错误,但是 g++ 4.5.3 则能够编译通过。

由于 VersionSet 是 VersionEdit 的友元类,那么 VersionSet 是能够访问 VersionEdit 私有成员的,这样问题就集中在 Builder 是否能够获得与 VersionEdit 的友元关系。如果语法规定嵌套类 Builder 能够从 VersionSet “获得”友元关系,那么 Builder就能够访问 VersionEdit::compact_pointers_,反之就不能访问。

在 C++98 标准中,关于嵌套类的权限有如下描述:

$11.8/1 [class.access.nest],

The members of a nested class have no special access to members of an enclosing class, nor to classes or functions that have granted friendship to an enclosing class; the usual access rules (clause 11) shall be obeyed. The members of an enclosing class have no special access to members of a nested class; the usual access rules (clause 11) shall be obeyed.

Example:

class E {
    int x;
    class B { };
    class I {
        B b;                 // error: E::B is private
        int y;
        void f(E* p, int i) {
           p->x = i;         // error: E::x is private
        }
   };
   int g(I* p)
   {
       return p->y;          // error: I::y is private
   }
};

但是在 C++11 中,这段描述变更为:

$11.7/1 Nested classes [class.access.nest]

A nested class is a member and as such has the same access rights as any other member. The members of an enclosing class have no special access to members of a nested class; the usual access rules (Clause 11) shall be obeyed.

Example:

class E {
    int x;
    class B { };
    class I {
        B b;                  // OK: E::I can access E::B
        int y;
        void f(E* p, int i) {
            p->x = i;         // OK: E::I can access E::x
        }  
    }; 
    int g(I* p) {
        return p->y;          // error: I::y is private
    }  
};

从上面的描述和示例代码对比中我们可以明显看出,在旧标准中嵌套类和“被嵌套类”没有什么特殊的关系,就像两个普通类一样;但是在新标准中嵌套类已经完全视为“被嵌套类”的成员,那么自然也获得了“被嵌套类”成员应该有的访问控制权限。这也就意味着“被嵌套类”的普通成员拥有的访问“被友元类”私有成员变量的权限,嵌套类也能够获得,那么 Leveldb 在新版本的编译器下能够编译通过也不足为奇了。

不过 gcc3.4 的编译错误问题还不能单单归究于标准的变化。因为 gcc3.4 已经能够支持嵌套类访问“被嵌套类”的私有成员(因为在很早以前这就被确认为一个缺陷),只是不能够支持友元关系到嵌套类的传递。友元关系的传递可能是在 4.1 或者 4.2 版本中实现的,应该属于上述标准变化的衍生特性。

通过科目三路考

前天晚上通过科目三路考,我为期半年的驾校学习总算结束了。

想起来这半年也是折腾不断。我是 2 月底参加水木团购版的东方时尚驾校报名团购,3 月中旬考科目一。之后因为懒得约模拟机,光模拟机 6 个小时花了三周时间才上完。后来从淘宝上买了个约车软件,专门用来约周末的散段,效率还挺高的,也就三周就把散段上完了。后来科目二考试也算挺顺利。只是5月该考科目三的时候忽然忙起来了,工作上各种事情,再加上天气热,就把科目三考试拖了下来,一直拖到 9 月。

工作以后考驾照呢,我觉得主要还是得看执行力。执行力高的,两个月以内就能搞定,像我这样执行力差的,光拖就能拖四个月。我自己的一个特性是有压力的时候不希望分心。前段时间在做一个比较大的升级,对我个人的设计能力、架构把握和编码能力都是一个挑战。再加上合作半年的一个同事离职,新加入的同事也不太熟悉,我只好一个人吭哧吭哧把剩下的事情做完。

不过这个项目让我很有成就感!重构了一个 2 万多行的模块,精简到不到 1 万行;重整了 4 万多行代码,架构上进行了很大的调整,脉络上清晰很多。另外也加了些新功能。但是由于我负责的系统涉及产品线太多,平时工作中新需求、沟通、线上问题处理等日常事务占到一大半,很多时候只能加班写代码。这样大的工作量让人有些筋疲力尽。现在项目完成了,我也想休息休息调整一下,就把后续的编码工作让给同事去做,我下面这个月主要任务就是跟进测试和校园招聘的工作了。

算了,又跑题了,最后做个广告吧:如果 2012 应届生同学对百度提供的工作机会感兴趣的话,欢迎邮件简历给我!优秀的朋友我可以帮助内部推荐,相信你知道在哪里能找到我的邮箱地址 :)

双人桌游体验

比较惭愧地说,最近有点儿迷上了玩桌游。上下班地铁上有时候都会一直玩手机上的安卓游戏《Catan》(卡坦岛)。xixi 自从体验过桌游《三国杀》、《只言片语》后,也成天嚷嚷着要参加我们的同事聚会玩游戏。只是从家到单位的距离实在太远,只有进城 building 的时候才有机会一起玩玩游戏。

由于比较经典的桌游大部分需要三人或以上才可以玩,除非是合租的朋友,不然在家很难凑齐人。于是我就略微探索了一下适合双人玩的游戏,淘宝买了三款,《Lost Cities》(失落的城市、遗失的古迹)、《Carcassonne》(卡卡颂、卡卡城)、《Hailli Galli》(德国心脏病)。下面简单介绍一下初步的体验,供同样喜欢桌游的小情侣儿们参考 :)

《Lost Cities》是一款卡牌游戏,主线是两个人去竞争开发五座古城,只能两人玩。卡牌分两种,一种是赌注牌,每个古城对应三张相同的赌注牌,以颜色区分;一种是开发牌,每个古城对应 9 张从 2~10 的开发牌,以颜色区分。限制有 3 个:1. 一旦决定开发某座古城,首先付出 20 点的成本;2. 每张赌注牌能够提高一倍的收入/损失,但是赌注牌必须先于开发牌去开发古城;3. 开发牌必须按照点数从小到大用于开发某座古城。

规则就不详细描述了,主要谈谈体验。《Lost Cities》的双人对抗性还是很强的,每个人必须规划好自己的出牌顺序。如果刚开始出的点数大,那么后面点数小的牌只能弃掉了,弃牌可能被对方拿走;赌注牌也是这样,如果先出开发牌,赌注牌也只能弃掉;牌不好的话,开发的城市还不能太多,否则可能会亏本;追求的有一个目标是某座城的赌注牌+开发牌数大于等于 8 张,这样可以有 20 点的奖励。可能对结局影响比较大的状态是摸到开发牌点数的大小,如果某个人摸到的大部分是大于 5 的开发牌,那么赢的概率就会高很多。总的来讲,这个游戏的可玩性还是很强的,看似强势的一方到最后结算时发现分数反而更少也是有可能的。

《Carcassonne》是一款纸牌建造游戏,和拼图类似,适合2~6人。所有的纸牌都是正方形硬纸板,上面有河流、城市、寺庙、草原等场景。两个人轮流抽牌进行拼图,每个人有 7 个人偶(Meeple)。人偶可以放置在未建造成功的场景上,一旦建造成功,就可以根据场景不同结算分数,人偶被释放走去占领其它未建造成功的场景。

这款游戏的计分规则还是比较复杂的。如果多人玩,由于人偶太多,可能必须考虑复杂规则。但是两人玩的时候我觉得可以简化一些,比如我们玩的时候根本没有考虑农场场景和不允许二人占领同一场景。除了进展比较慢,游戏还是挺有意思的,而且值得一提的是,这种拼图类的游戏一般比较讨女孩子喜欢...

《Hailli Galli》也是一款纸牌游戏,规则更简单,适合2~4人。先均分所有纸牌,每个人轮流出牌,放到面前的自己弃牌堆上。一旦某个出牌触发了某个规则,所有人去抢按中间的铃铛,先抢到的就拥有所有人的弃牌堆,最终赢光所有人牌的人获胜。规则简单到什么程度呢,纸牌内容就是水果组合和动物,两张花色一样的牌需要抢铃、出现猴子时,台面上没有柠檬时抢铃、出现大象时台面上没有草莓时抢铃等等。好幼稚啊!我觉得把它称为家庭桌游更合适。

不过,话说回来,如果你想光明正大地摸摸姑娘的小手,强烈推荐这款游戏!因为大家都去抢按一个铃的时候,嘿嘿,你懂的!此外,估计也适合同事之间的破冰活动,大家有了肌肤之亲,自然距离感就会少一点儿~~~

承德行记

来北京几年,周边去的地方也不多。今年大组 building 选择了承德,那么自然愿意去到彼一游了。由于是跟团去,游记攻略可说的不多,就以照片为引,流水一番吧。

这是避暑山庄大门,从这样的景象中可以看到暑假里的承德是多么的人山人海,据导游介绍说避暑山庄每天要接待 10 万人。反正心理预期是早就有的,对这样的情况也没什么可抱怨的。

避暑山庄大门
避暑山庄大门

下面这张应该是正门,康熙爷写的“避”字多了一横。

避暑山庄牌匾
避暑山庄牌匾

在纷纷扰扰之中,偶尔也会有几片宁静。

小伙子你肿么了,是在练习蹲坑吗?

等待游览车中
等待游览车中
二马道上遥望小布达拉宫

烟雨楼附近避暑山庄的湖光山色真是太漂亮了,即使在那么多游客的情况下也掩饰不住它的美丽。什么故宫、北海、颐和园都是浮云啊!遥想当年,我不禁发出一句响亮的评论:“操!”

避暑山庄烟雨楼

小布达拉宫地方小,游客密度较避暑山庄更胜一筹。拍出来能看的照片不多,下面这个是大红台的近景。

普陀宗乘之庙(小布达拉宫)大红台近景

从外面看大红台和一堵墙似的,以为里面会是很窄小的空间,但事实上却是别有洞天。

大红台内院西北最高处
一窗一世界

虽然与小布达拉宫相比略小,但我觉得班禅行宫的金顶还是挺霸气的,因为上面有八条龙!

须弥福寿之寺大殿金顶
须弥福寿之寺红墙一角

最后再从另一个角度看看小布达拉宫,看看你能发现什么!

照片正中那棵树,放大了看,有没有觉得很眼熟?哈哈 :)

虽然被赶着只能走马观花地看,虽然游人多到恨不得马上买票回家,但总的来说,这次承德游还是长了不少见识。我自己的体悟主要在园林和建筑上,皇家园林和寺庙的确是费了不少的心机在里面。廊庑婉转、雉堞衔连,起承转合,相当精妙。很多地方都有让人柳暗花明、豁然开朗的感觉。风景实在是太好了!若有机会穿越回清朝,在里面倘佯几回,岂不快哉?

技术人员的眼界

意识到眼界的重要性,最初是在大学时学长的交流会上。南大数学系有一个传统,每年总有那么两三次组织高年级的同学开经验交流会。这些交流会可能有明确主题,例如留学或是找工作,也可能没有明确主题。幼稚如我,在大一阶段拒绝参加任何形式的社团或者活动,认为踏踏实实做好眼前的事情足矣,闲暇时间基本花费在小说上。后来的种种经验证明,这是多么傻的一种做法!

我在数学方面是一个资质一般的学生,是几位师长和朋友打开了我在计算机行业的眼界,得以投身到信息技术的洪流中。写到这里忽然觉得有变成回忆长文的迹象,就此打住,几位是谁就不介绍了。唯一值得一提的是,李开复先生也是其中之一,这也是我从不参于对他的争议和讽刺话题的原因。在帮助和启发中国学生方面,我觉得他是一个值得尊敬的人。

但是在进研究生院之后,我自己闭塞了眼界的发展。一方面有当时身处的环境使然的原因,另一方面有成绩羞见江东父老的自闭自卑成分。非常后悔的有两点,一是虽然在某个研究专题方面有所收获,但在计算机科学系统性知识方面的进展不够大;二是身处远离实业的象牙塔,虽然亲身经历了一次严重的经济危机,却没能趁机深入地去学习和观察隐藏在这场危机之后的经济和科技发展规律。

我用来勉励自己,有时候也用来鼓励他人的一句话是:“只要你读的是自己不知道的东西,那就是在进步。”因而我经常不择食地去读一些书,了解一些知识。但渐渐地认识到了一点,当你着手去学习时,眼界决定了你汲取知识的效率和效果。具体到计算机科学,眼界可能决定了你读的书是经典还是垃圾,花时间在过时的还是新兴的技术上。

这个周末在家看吴军先生的《浪潮之巅》,爱不释手。作为一个曾经拜读过 Google 中国黑板报上“浪潮之巅”系列连载的读者,我本以为这会是本信息企业史,但阅读后发现新增的三分之一内容更精彩。我不曾想过一个技术人员的眼界会如此之广,能够从各个角度去观察和分析身处的这个行业、这个时代。我不否认对于书中的一些观点我并不完全赞同,但作者的眼界和思考能力着实让我佩服。

现在我已经初步开始自己的职业生涯,未来该何去何从,心中有一些惶恐。该打造哪方面的能力,亦不知该如何着手。有一些短期的规划,但并无具体的长远目标,我知道自己又到了一个眼界的瓶颈期。都说中国的年青人被房子票子绑架了,我希望自己能在忧愁这些之外,分些时间来读读好书,交交优秀的朋友,想想深刻的问题,聊以自勉。

2011年13月32日24点60分60秒

这是一个测试时候发现的问题,2011 年 13 月 32 日 24 点 60 分 60 秒,这个时间存在吗?

我觉得这应该是个错误的时间,但很不幸地是,如果往 struct tm 中填入这个时间,mktime() 时并不会报错。下面是一段简单的测试代码:

#include <stdio.h>
#include <string.h>
#include <time.h>

int main()
{
    struct tm t;
    memset(&t, 0, sizeof(t));
    t.tm_year = 2011-1900;
    t.tm_mon = 13-1;
    t.tm_mday = 32;
    t.tm_hour = 24;
    t.tm_min = 60;
    t.tm_sec = 60;
    printf("tm_mon=%d, tm_mday=%d, tm_hour=%d, tm_min=%d, tm_sec=%d\n",
            t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
    time_t tt = mktime(&t);
    printf("%s", ctime(&tt));
    printf("tm_mon=%d, tm_mday=%d, tm_hour=%d, tm_min=%d, tm_sec=%d\n",
            t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
}

这个程序执行的结果是:

$ ./a.out  
tm_mon=12, tm_mday=32, tm_hour=24, tm_min=60, tm_sec=60
Thu Feb  2 01:01:00 2012
tm_mon=1, tm_mday=2, tm_hour=1, tm_min=1, tm_sec=0

查看 man mktime,的确有对这个特性的解释:

...if structure members are outside their valid interval, they will be normalized
 (so that, for example,  40  October  is  changed into  9  November);...

这就意味着,对输入时间做严格的检查无法依赖于标准库中的 mktime() 函数,只能自己来进行。这应该也是 date 命令曾经改进过的地方。

$ date -d "2011-13-32" # date (coreutils) 5.2.1
Wed Feb  1 00:00:00 CST 2012
$ date -d "2011-13-32" # date (GNU coreutils) 8.5
date: invalid date `2011-13-32' 

也谈地铁迷药

这是篇临时起意的文章。最近经常看到有人讨论“地铁迷药”或者“地铁迷药辟谣”,很多人抱着宁可信其有,不可信其无的态度。而且还有人在微博上、论坛上吐槽,怀疑是被下迷药了,吐槽当时怎样怎样头晕,怎样怎样难受。

简单来说,我的基本态度是“信其无”,但却无法证明“其无”。所以我不讨论它到底有没有,只是就我自己的“丰富”经历,谈谈头晕、恶心、难受、两眼发黑和迷药不一定有关系。

我站着不动久了容易头晕,这是小时候就知道的事情,但至今不晓得是什么原因导致的。高中之前都是我父亲用推子给我理发,他个子比较高,所以我必须得站着让他理发才舒服。我一般站个二十分钟上下,就会头晕恶心,必须得蹲一会儿才能继续站。因而我从小怀疑自己心脏有问题,但后来发现自己足球、篮球、羽毛球各种剧烈运动都不怕,检查也没出过问题,这个怀疑只能不了了之。

长大之后,好了一些。不过大一结束时军训站军姿的时候,我晕倒过两次。晕倒的过程很奇妙,最开始是胃疼,但不是疼得受不了那种,就是悠悠地疼,然后头开始晕,觉得四周景物有点儿晃,然后觉得呼吸困难,接着是两眼开始冒金星(很奇妙,真的!),最后一黑就倒了。值得一提的是这种晕倒不是毫无知觉那种,即使是倒地,仍然有意识觉得自己要倒了,因而没有发生“咚”的一声以头抢地的情况。晕倒以后被同学扶到树荫下,喝点儿水,大概十几分钟就能缓过劲儿来。

来北京之后的第一个夏天,在 619 路玉泉路到中关村的公交车上晕过一次。当年的 619 是很破的公交车,而且很难占到座位。我站着站着忽然觉得军训时候晕倒的症状来了,而且还伴有恶心的症状,久病成良医,马上蹲下靠着椅子大口喘气。坐在椅子上的女孩儿看我状况不对,连忙把座位让给了我,我坐在那趴了好大一会儿才缓过来。

后来还有一次,是和女友从北海公园回来的公交车上晕的。几路不记得了,应该是北海公园西面某站到知春里东站的线路,过新街口的。那辆车也是那种两节的老车,大热天的闷得透不过气,我就站在两节车厢的连接处。上车感觉还挺正常的,大半程没啥问题,快到了开始头晕。也是赶紧蹲下,女友扶着,忍到下车。下车后直奔中关村海关旁边的麦当劳去买冷饮,在里面坐了好久才敢出来。

前面这几次都应该是天热闹的,后来有一次估计是因为空调太冷导致的。研究生毕业最后一天搬家,天很热,把行李搬到西二旗智学苑租的房子后,到中科院奥运园区找同学吃饭。吃了驴肉火锅,喝了啤酒,略微有点儿上头,就直接去坐地铁回家了。8号线大家应该知道,奥运支线,人特别少。一进地铁我就觉得冷风呼呼灌得我不舒服,等倒了两趟到知春路换乘,两腿都已经软了。我忍着头晕难受坐到西二旗站,头重脚轻,两腿飘飘回到家中,闷头躺床上就睡,第二天一点事儿都没有。

我的这几次遭遇里面,真正晕倒的是在军训场上,因为没法采取什么措施。其它情况下都是蹲下或者坐下,扶着什么东西尽量让自己舒服一些。

我仔细回想过,我头晕基本发生在暑期,或者太阳直射的情况下,因而我认为是体质不耐热导致的中暑症状。至于迷药,虽然好多次头晕发生在公共交通工具上,但我从来没怀疑过,因为根据我的经历根本不可能想到迷药上去。