再见,YY

不知不觉地,有如忧伤
夏日竟然消逝了
如此地难以觉察,简直
不像是有意潜逃

当我无意中在电台节目听到狄金森这首诗的时候,2016年的夏天已经过去了,如此地迅捷轻快,就像诗中所说,即使没有翅膀,夏日也能如此轻逸地逃去。这个夏天,我从工作了5年多的公司YY离职了。

在一个城市呆上几年之后,这个城市的脉搏和节奏就会慢慢渗入你的生活,不知不觉地就会变成你的一部分,就像我已经渐渐地习惯了广州白天人流的拥挤,还有夜市街道的喧闹。在一个公司呆上几年之后,它的风格与精神也会慢慢变成你的一部分,潜移默化地,如此地难以觉察,以致于我已经无法回想起现在的自己与当初进入YY的时候有哪些不同之处,仿佛一直从未变过,而其实变化实在不少。

几年前入职YY的时候,我还是一个毕业两年多的年轻小伙,而现在已经变成一个中年大叔。这几年间,YY从一个500人的小企业发展成一个3000人的大公司,办公室越换越大。一般人都可能会有这种错觉,当一个大公司的员工谈他所在的公司有如何如何成就的时候,就好像收到了一种暗示:说话人肯定参与其中并为此做出了很大的贡献。前面对YY的叙述好像暗示对此我有很大的功劳,然而实际上人微位低,我只是数不清的众多一线员工中的其中一个,努力工作却只对大事件有非常微小的作用。但无论是非常直接地还是间接地,至少每个人都是参与其中的,能感知YY到办公室的氛围与变化,陪伴经历了一些事情:公司上市了,然后准备私有化又取消;架构调整,部门成立了又分拆;员工流动,新员工来了变成老员工,然后又离开。因为有上市的机会,很多员工都拿到不错的股票回报,有人因为拿得多退休离开,有人因为拿得少不满离开,有人因为公司给的钱多而入职,也有人因为公司薪酬太低而离职,有毕业一年跟到好产品就飞黄腾达的明星员工,也有加入多年还是在一线耕耘的无闻小兵。在这里,财富和机会是如此的既近在眼前,又远在天边。在这里,事情和旋律是如此的井然有序,又混乱不堪。在这里,决择和人生是如此的灿烂光辉,又黯淡无声。这样的描述像是在描写淘金热潮时代的旧金山,或者改革开放初期的深圳,却也是对这几年的YY的最好概括。

活在这样一个变化频繁落差巨大的时间地点上,我想每个人都会多多少少发展出一种淡定的态度,你不知道什么时候会有一些好事或坏事落在你的头上,正如你不知道哪一天哪个部门会做什么的调动,又或者某个同事在某一天会兑现大量股票买个房车,或者哪个同事又在某一天突然离职,变化来得快而不能掌控,所以慢慢的你就有了一种对变化的淡泊,或者叫冷漠。很多事情我们单个人都无法掌控,所以活在当下做好眼前就已经足够。得到了不必过于欢喜,失去了也未必是灾祸。这种淡定,我想应该是这几年在YY的经历得到的独特体验。

写到这里,我知道很多读者都想问同一个问题:别扯这么多那些都没有人关心,你就说说YY上市你拿到了多少股票多少钱,发财了没有。而且问这个问题的同时,脑海中自然地会出现一个暴富退休的年轻人,在飞机游艇上写回忆录传授成功经验的画面。遗憾的是,这个画面并没有发生在我的身上。所以我还是继续在YY做着一线小兵的工作,过着属于我的平淡生活。

几年前在加入YY的时候,机缘巧合之下由老傅带入了后台存储团队,之后便一直呆在这个团队做存储相关的工作,中间经历了多次团队调整和人员调动,虽然后来也曾想过离开去做其它方面的事情,然而终于没有成行。从最早的10人小分队到后来的数十人大团队,从Oracle到Mysql,又从早期的只有关系数据到后来的文件系统,KV,Cache,YY存储团队这几年的经历可以说是一个互联网公司后台存储团队发展壮大所走的典型道路,产品线分类越来越丰富,系统也变得越来越大。很幸运地我能跟随着存储团队的发展在这几年间接触到了各种不同的技术和应用,扩展了眼界。当在技术上已经把存储产品线丰富起来之后,更多的工作就落在接入业务上面,以业务的体量来做为KPI,然而一个公司的快速膨胀期总是有限的,毕竟快速增长状态不是常态不能长期保持,当业务体量上无法取得大突破的时候,这种KPI制度就会让技术人员觉得非常难受,毕竟业务的发展有独特的周期和特点,并非技术人能完全把握。做平台的人总是希望接入所有种类的业务,就像做业务的人总是希望能服务尽量多的用户,然而实际情况并不总是那么理想。在YY存储团队混迹了几年之后,我已经能明显的感知到这种不能突破的天花板。路难行,行路难,停杯投箸,拔剑四顾,最终我还是选择了离开。

一份工作就如一份关系,从一个工作离职跟恋人一次分手也有类似的地方:曾经彼此有过美好的时光,当不能继续携手向前的时候,你所能做的只有放手并祝福对方。即使后来在某个遥远的异乡,夜里你孤独地醒来,回想起过去属于恋人的甜蜜的点点滴滴,也只能一声叹息。

祝YY越来越好。感谢在YY每一位同事,愿你们都得到幸福。

念念不忘,亦有回响

电影《一代宗师》佛前灯

多年以前,当我在Linux下选择Emacs做为编辑器的时候,似乎只是一个比较随机的选择。当时我已经接触并使用VI有一段日子,然而我使用它的体验并不算愉快,也许是我被VI命令模式和编辑模式搞乱了太多次,于是想找个其它编辑器试试,而当时我所知道的Linux终端字符界面编辑器并不多,Emacs是其中一个,然后我就开始使用Emacs,从此一发不可收拾,走上了一条不断修炼的漫漫长路。这段经历后来浓缩成了《Emacs修炼之道》这篇文章。Emacs不仅给我提供了一个舒适高效的编辑器工具与环境,也引领我走入了Lisp语言的世界。

Lisp语言诞生自几十年前,这几十年间世事沧桑,一个又一个浪潮在计算机行业冒升又落下,一门又一门新的编程语言流行而又衰落,Lisp语言早已退出流行的行列,然而却始终吸引着一小群程序员前仆后继地不断进入它的世界。除了传统的几种Lisp方言,2007年出现的Clojure将Lisp在JVM平台重新做了一个现代的实现,也掀起了一波新的潮流,吸引了很多人的关注和使用。几十年前诞生的编程语言,大多早已消失在历史的长河中了,为什么Lisp能够保持长期的生命力呢?正如看待黑客文化时有人主要关注它的政治正确,有人则将它看成是嗡嗡作响的经济引擎,不同的人对Lisp优点的看法也大不相同。有人喜欢Lisp以List作为唯一代码组织方式的简单明了,有人欣赏Lisp由核心几个特殊Form支撑起来的体系,对应着数学领域里面的公理上搭建大厦的体系结构,有人则更实利化只关注可以帮助提高开发效率的那些方面。在我看来,Lisp最吸引我的一点就是强大的元编程能力,非常便于编写DSL。当然这并不是说其它方面不重要,如交互式编程,强调函数式风格但又不强制函数式为唯一风格等特点也是可圈可点。

Lisp的思想和设计是如此优秀,然而传统的几种方言的实现Scheme,Common Lisp还有另外几个方言都没能紧跟这个时代的步伐,Clojure的作者Rich Hickey必定是深知这点,才会创造了Clojure。感谢Rich,我们得以知道在某个语言的成熟VM上实现一个现代Lisp的可行性,也欣赏到了Clojure设计与哲学的种种优雅美丽之处。Clojure设计于运行在JVM之上,固然是它的成功关键,因为如此一来就可以重用成熟的JVM环境以及丰富的代码库,同时Java程序员这个群体也非常庞大,能吸引到的Clojure使用者也多。另一方面也产生了一些缺点,因为底层的JVM原本是为Java设计实现的,而Java的并发模型还停留在操作系统进程/线程和锁这一种方式,因此Clojure除了保留Java的并发原语之外,还在并发上做了多种尝试,像软事务内存,几种不同的原语delay, future, promise,还有几种不同的引用类型。如此多的新概念和原语不可谓不丰富,同时也是繁多复杂。每当我的脑细胞因为折腾可变与不变或者write skew而被烧死一部分的时候,我总是怀念Erlang Actor并发模型的简单。

既然要顺适多核化的趋势,就需要语言本身的并发机制支持用户态进程/协程。那么为什么不直接使用Erlang呢?虽然Erlang并不是Lisp,但Erlang的设计也受到Lisp的影响,很多地方都有Lisp的影子,比如交互shell,热更新等。然而我对Erlang也有觉得不满意的地方,除了经常被, ; .几个符号搞晕之外,最大的不满是来自于在Erlang里做元编程的不便,但是Erlang代码又有很强烈的元编程需求,比如OTP里面的大片模板代码。因此在编写Erlang代码的时候,我又相当怀念Lisp的Macro。

那么,能否把Erlang和Lisp两者的优点结合起来呢,就像在JVM上实现Clojure一样,也许可以在Erlang VM上实现一门Lisp语言?到我的脑海中冒出这个想法的时候,距离我最早接触Erlang已经有三年多时间,距离学习Clojure已经有两年,另外当时在工作上我也写了大半年的Golang,是的,近几年中我学习/使用过的编程语言也不少,基本上每年都会学一到两门新语言,虽然不是每个新学语言都有机会用来写大量的代码,但也算是见识了不少编程语言,也许是对很多好的语言有着更高的期望吧,没有哪种已知的编程语言令我觉得非常满意。带着把Erlang和Lisp结合的想法,我发现原来Erlang VM就像Java VM,VM之上也有中间语言层,可以基于这中间层来实现一门Lisp,而且,早在几年前,就已经有人这样做了。我发现了Robert Virding写的LFE,和Eric Merritt写的Joxa。

作为参与设计Erlang的元老,Robert一直以来也是探索Erlang应用方面的先行者,早在2007年就写了LFE,并且影响了一众后来基于Erlang VM的新语言,如Elixir。LFE是Lisp Flavored Erlang的缩写,顾名思义,LFE就是把Erlang写成Lisp,除了多了一些括号之外(去掉了Erlang原来的分隔符如, ; .等)跟Erlang十分相似,LFE的设计目标在于提供一个可扩展语法的Erlang,它的实现也达到这个目标。然后跟我理想中的Lisp语言相去有一定距离,我并不喜欢它的Lisp-2风格,跟Erlang相似的模块设计如显式的export,只在编译时可用的Macro等方面。

另外一个实现Joxa则是由Eric在2011年开始写的,Eric也是Erlang界的老前辈了,他先是试用过LFE,然后觉得并不如意才重新设计和实现了Joxa,之前我写过的文章《Joxa: 一种基于Erlang VM的现代Lisp编程语言》详细描述了两者的渊源和差别,这里就不再展开了。总的来说,Joxa的Lisp味相对更浓一些,更符合我的个人口味。在过去的一年多时间里,我仔细阅读了Joxa的实现并添加了Erlang R17后引入的Map的语法。Joxa虽好,但有两个主要原因让我不得不考虑重新做一个实现:

  1. Joxa目前的实现是用自举的方式将Lisp代码经过编译转换成Core Erlang,自举是很cool的一种实现编译器的方式,然而用在语法不够稳定的场景下,会提高后续语言开发的复杂度和难度。Core Erlang也是一个规模小巧设计良好的中间层,然而文档方面相当缺乏,很多时候必需花大量时间人手去获取从上层到它的翻译细节。相对来说更上层的Abstract Format虽然规模更大,然而文档较多,一些工具也只工作在这一层,所以更适合用于做为编译器的目标语言。在这一点上我和Eric讨论得出的一致结论是,要将Joxa用Erlang重写并将目标输出定位到Abstract Format,却因为彼此都忙暂时未有足够精力将其重写。

  2. Joxa的设计目标强调它要小而简,希望成为一个小的核心语言,如果其他人有需要就在其上定制扩展。这种设计也是Lisp语言的一种传统特色,由于扩展性强,可以做到核心小扩展大,类似的思想也被人在不同的语言和项目上实现,比如PyPy和Python 3。然而我更希望有一套功能较多较全的编程语言,因此需要在语法和代码规模上都需要添加(或者改动,但主要是添加)很多内容,这就与Joxa的目标有冲突。据我所知Eric对Joxa有明确的定位,应该不会乐意在Joxa里面接受这些添加或改变,因此最好我还是用一个新项目名字来重写一个基于Erlang VM的Lisp实现。

一直以来我有个朦胧的想法,希望在下一个我能够自己决定命名的项目,能够用上一个带有我居住所在地中国华南地区的风物,或者带有东南亚色彩的名字,在给这个新项目取名时,老家旧居楼房前面的两棵高大的木棉树出现在我的脑海,那是一种在南方非常常见非常普通的树,平时不会特别觉察到它们的存在,然而有时觉察到它们的存在却又不禁觉得特别,像鲁迅笔下所说:“在我的后园,可以看见墙外有两株树,一株是枣树,还有一株也是枣树”。于是就将这个新项目的名字取为木棉,英文是Kapok。

我想,Kapok这个新的基于Erlang VM上的Lisp实现会包含如下这些内容:

  1. 一套类似Clojure风格的现代Lisp语法(Lisp-1),当然在语法上并不能无脑照抄Clojure,比如方括号[]在Clojure中表示Vector,在Erlang却是List,那么Kapok里如何选择?若语义跟随前者,是用Erlang的tuple来实现Vector语义还是另外再做一套实现?若语义跟随后者,和普通圆括号()又如何区分,各自的使用场景如何确定?每处细节都需要仔细考量,这些细节也会影响其它方面,如下面将提到的兼容性。另外Clojure语法糖较多,而我不喜欢太甜。

  2. 与Erlang VM保持最大的兼容,在Erlang VM上实现的多种语言,都选择了将语法及语义尽量向Erlang靠拢,比如新语言的函数直接编译成Erlang的函数,如此得以保持良好的兼容性,新语言的代码可以直接与Erlang VM基础设施与已有库代码进行相互调用,不像Clojure那样需要通过中间包装。Erlang是一门函数式语言,这意味着新语言最好也保持这种函数式的语义,当然也可以通过上层逻辑修改这种限制,比如另一门基于Erlang VM的语言Elixir通过在编译器做变量名映射的技巧使得在Elixir里同一个变量可以做多次绑定,如此一来Elixir在变量使用上更像命令式语言。基于两个原因我个人更偏向于在Kapok里面完全保留Erlang的函数式语义,一是它利于并发,二是它语义更清晰。

  3. 新的语言机制,目前已经在考虑中的有:支持Clojure的Protocol(运行时类型分派,让语言更动态),探索一套带类型标注的DSL来做强类型编程(强制编译时类型检查,让语言更静态),Lazy API。其中第Protocal和Lazy API源自Clojure(当然Clojure也借鉴了其它语言),并且已经在Elixir中得到实现。同时我对其它新想法持开放的态度。

  4. 功能丰富接口现代的标准库,包括常用宏,文件,网络等方面,特别一提的是Erlang原生用字符列表来表示字符串并不理想,因此需要有一整套高效易用的unicode字符串标准库,在LFE和Elixir中都重新定义了binary string,这是一个比较好的方案,可以借鉴。

  5. 强大且现代的开发工具集,包括:文档及Apropos接口/工具,编辑器集成(如Emacs mode), 项目管理工具(参考Mix),交互性开发环境(集成Slime和Swank)等

上面的几点综合了Clojure, Erlang, Joxa, LFE, Elixir, Emacs等多种语言或机制,希望可以取多家之长,提供一套强大完整的开发环境。当然也可以说是一个大杂烩,然而不是无脑照搬,库和工具集可以丰富多样,然而基础语法需要保持简约节制,避免出现C++或Scala那样过于烧脑的设计。同时也要了解到上面几点内容只是一个初步的想法,后面可能会根据实际情况有所添加或改变。我一直有个用一门新编程语言编写一个开源数据库的朦胧想法,Kapok完善到一定程度之后,也许可以用Kapok来完成我这个想法。

有了简要设计,就可以开工编写代码了,首先要实现的是1, 2两点,亦即核心编译器部分。要写Kapok的想法早在去年就已经产生,然而一直拖拖拉拉未能动手。去年年中祖母去世,让我明白到时间匆匆人生太短,有些事情想做就抓紧时间去做,迟早人生的终点只是青山上的一把黄土。于是开始动手,由于平时上下班剩余时间不多,生活琐事缠身,进展较为缓慢,拖拉了几个月逐渐完成了编译器前端,大概到今年初开始写后端,也算是慢慢重温了一遍大学时开的编译课程。最近有了一些时间,终于折腾到可以跑起来的程度。我想Kapok这个项目不可谓不大,终究不能短期内完成,要做到非常完善的程度,更是有待时日。就像长跑,并非重在一时之快慢,积跬步恒坚持,方能走得更久更远。我所需要做的,就是调整呼吸,跨开步子。

于是就把Kapok开源了,github地址在这里,虽然暂时还有很多问题和缺点,也有很多想法未能实现,但我想这也算是一个不错的长跑起步。

十年之前,当我第一次安装并启动Emacs的时候,决没有料到会十年之后它带领我在Lisp的世界走了这么远。几年之前在接触Erlang,Clojure的时候,没有想到有一天会用Erlang重写一门Clojure风格的Lisp语言。一年之前开始着手Kapok的时候,我也不知道这个项目要做到何时,做到什么程度。然后事情冥冥中就这样发生了,而我将沿着这条路继续向前走。王家卫电影《一代宗师》里面,宫先生说“念念不忘,必有回响”,行知做人跟练武一样,秉着意念坚持下去,终究能有一点回响。

Cloudtable: 分布式一致性的大数据存储系统

我们YY云存储团队负责向公司各部门提供存储服务,随着YY的发展壮大,我们面对着数量级越来越大的存储需求。存储和访问PB级别的数据是一个很大的挑战,为了使得存储服务越加的高效和快速,我们持续地改进和翻新我们的工具和系统。最近,我们开发了Cloudtable,它是具有分布式一致性的,可扩展的,大容量数据存储系统。

为什么开发一个新数据库

随着业务的不断发展,过去将数据存储在传统的关系数据库的做法已经面临着诸多问题,特别是对于某些在性能方面或扩展性方面要求很高的场景,使用关系数据库来存储和查询数据就会遇到很多难以克服的困难,例如需要很高的写性能,需要超大容量的数据存储能力以及在线扩容能力等。目前我们收集到如下几种典型的存储需求:

  1. 对读写性能要求较平均或读性能要求较高的,数据总量为G量级的小容量数据存储(如元数据)
  2. 对读写性能要求不高可以容忍一定时延,但要求跨机房高可用,对数据一致性要求很高的小容量数据存储(如关键业务数据,金钱相关业务数据)
  3. 对写性能要求较高的,每天数十G量级,需要长期保存的大容量数据存储(如日志)

这些需求使用传统关系数据库得不到很好的解决。另一方面,我们注意到近年在服务器上快速flash存储的兴起,单个SSD硬盘的IOPS相对传统的机械盘有着数十倍的提升,在传统数据库中的锁竞争以及频繁上下文切换不能充分利用如此之高的IOPS。因此,我们需要一个强大灵活的新数据库,以解决遇到的存储需求,充分利用硬件的发展趋势。

我们对目前市面上多种非结构化数据库进行了调研,对较主流的几个数据库进行过评估,认为它们都不适合直接应用于我们遇到的业务场景:HBase使用Pipeline写3份(默认配置下)数据来保证数据的高可用和一致性,如果跨机房部署,datanode之间的时延较大会造成写性能的急剧下降;Cassandra用timestamp作为版本号,需要用户解决版本冲突, 综合它在设计上的完全去中心化自动化,以及项目的成熟度有待改善两方面来考虑,运维难度较大;MongoDB可以通过write concern, read Preference设置一致性级别,但无论如何设置都可能读到正在写入的数据,返回给客户的数据也可能被回滚,因此在一致性方面达不到我们的要求,并且Ark协议也不是非常完备。因此,我们决定开发一个能解决我们自己独特需求的分布式数据库。

设计

我们将这个新数据库命名为Cloudtable,它被设计为一个分布式的,可扩展的,大容量数据存储系统,并满足以下几点设计目标:

  1. 具有分布式一致性,即在跨机房环境下保证数据读写的一致性,而且提供尽可能高的可用性
  2. 具有灵活的数据模型以解决多变的存储需求,包括高效支持变长节段,灵活地增删数据列,稀疏的数据分布等
  3. 充分利用SSD硬盘的快速读写能力,提供较高的读写性能
  4. 具有良好的集群管理能力和运维特性,如支持PB级别的数据容量,在线无缝扩容等

分布式一致性

CAP定理告诉我们,一个分布式系统不可能同时满足一致性,可用性和分区容错性这三个需求,最多只能同时满足两个。在互联网业务中,往往选择降低一致性要求来提高可用性和分区容错性,由此推动了各种最终一致性存储系统的流行,但最终一致性强逼系统使用者使用者自己处理由多点读写,同步延时等带来的数据不一致性,给上层业务开发带来不小的复杂度,因此在某些场景下分布式强一致会比最终一致性更加优胜。我们观察到,在近年新推出的部分大规模分布式存储系统上,分布式一致性已经成为标准特性,如Google的Spanner。这些系统并非完全舍弃可用性或分区容错性,而是在CAP三者之间取得一个平衡。综合我们过去几年在存储领域的经验,我们认为在目前的跨机房环境下,引入分布式一致性有助于解决我们遇到的存储需求。

在分布式一致性领域,Paxos占有垄断地位已经超过10年:大多数一致性算法的实现都是基于Paxos或受到Paxos的影响,在教学等场景下一致性算法的首选都是Paxos。但是Paxos既不易理解,也难于工程化。因此我们采用了在2013年推出的Raft来实现分布式一致性,Raft是根据易理解性为目标,采用了一系列如分解问题,缩减状态空间等技巧来重新设计的分布式一致性算法,它明确定义了下述几个新颖的特性:强Leader,Leader选举,成员变更。与Paxos相比,Raft更适合用来做工程实现。

灵活的数据模型

我们选择了BigTable模型作为Cloudtable的数据模型,一方面是因为半结构化的数据模型比较适合我们解决多种存储需求,另一方面由于市面上多个基于BigTable模型的存储系统,如HBase, Cassandra等已经得到广泛的应用,使用这个模型便于我们新数据库的推广。BigTable模型是指以BigTable论文中提到的,一个稀疏的,分布式的,持久化存储的多维度排序Map,其中数据以Table/Row/Column Family/Column/Timestamp多个维度作区分,其中Column Family相对固定一般在建表时指定,Column Family内部的Column可以灵活增减,读写数据都通过客户端API。另外,在底层存储,它使用了LSM树为磁盘顺序读写做了优化。每个Table都拆分为多个Tablet分布在不同机器,通过动态管理众多的Tablet来达到PB级容量的支持。

充分利用SSD

快速flash设备近几年在服务器上有了广泛的应用,较新的SSD每块可以支持每秒数万次的IOPS,相比机械盘有了很大的提升。通过在同一台服务器插上多个SSD,我们就在单个服务器上获得了每秒数十万次IOPS的能力。传统的以原地更新B树方式实现的存储引擎,并不能很高效地利用SSD如此之高的读写能力,因此越来越多的NoSql存储系统使用了LSM树方式实现的存储。我们采用了Rocksdb作为存储引擎,Rocksdb是在Leveldb基础上为SSD开发的嵌入式持久Key-Value存储组件,通过专门为写进行优化,提供更好的compression,更小的写放大等手段,Rocksdb具有很高的性能以充分发挥SSD的读写能力。同时它提供丰富的配置选项,可以配置为频繁读硬盘型,全内存型,只写一次型等多种读写类型,并且内部多个地方实现为插件机制,使用者可以自由配置、替换其中的组件以满足定制需要。

良好的集群管理和运维能力

一个大容量的数据存储系统必然涉及数量众多的服务器与进程,它们的管理便成了系统中非常重要的一环。根据我们的经验,存储系统的运维友好性是在系统设计中最容易被忽略也是最容易出问题的地方。在Cloudtable中,每个Tablet都运行在一个由多台数据存储机器组成的集群中,每个称为StorageUnit的数据存储机器上面运行着多个进程以及Tablet,整个集群中分布着数量非常多的Tablet,我们需要把所有机器及Tablet信息,活跃Tablet与服务器映射关系,空闲机器及空间大小等系统元数据信息都统一管理起来。参考HBase,我们将使用与普通数据存储相同的机制存储系统元数据,在HBase中,这样的元数据存放于-ROOT-表及.META.表,在Cloudtable中,我们将这部分元数据的存储抽取出来成为一个独立的元数据集群,称之为Metastore。相应地,HBase中各HRegionServer的存活检测是通过Zookeeper临时结点心跳来实现的,而在Cloudtable中,我们使StorageUnit定期向Metastore写入心跳。另外,Cloudtable中有一个对应到HBase的HMaster的管理进程,我们称之为Controller,它主要负责Table,Tablet及StorageUnit的理工作,包括:

  1. 管理用户对表的增删修改操作
  2. 管理对Tablet的分配、回收以及分布,负责处理Tablet的Split
  3. 负责StorageUnit机器的负载均衡,失效处理,加减机器等

将管理控制功能分离独立成两个相对简单的组件Metastore和Controller,便于我们实现其中的每一个部分。通过清楚定义的运维流程,考虑充分的异常处理,不断改进的实现过程以及大量的测试和检验,良好的集群管理和运维能力是可以实现的。

架构

Cloudtable是一个用Go语言编写的基于列模式存储系统。Rocksdb作为底层存储引擎,嵌入到每一个StorageUnit中,后者即是负责存储数据的进程。Metastore用于存储系统元数据,Controller用于集群管理。在系统的最外层有Gateway进程,作为接入层用于处理外部的请求。整个架构如下图所示:

Cloudtable架构图

整体跟BigTable以及基于BigTable模型的存储系统有很多相似之处[1]。每个StorageUnit进程内部,管理着一定数量的Tablet实例,多个不同StorageUnit的Tablet实例相互通信,组成一个高可用、分布式强一致的Tablet集群。例如上图中的Tablet A,就分布在3个不同的StorageUnit进程上。Tablet内部运行着Raft协议,同一个Tablet的所有实例组成一个Raft组。在Raft协议原来的强一致读写的基础上,我们实现了多种不同一致性的读模式:

  1. Read Latest,一致性读
  2. Local Read,只读本地节点,可能读到过时数据
  3. Read After,读指定Timestamp之后的数据,如果本地读不到则转为一致性读

丰富的读模式既给了用户更多的自由度,也提高了系统的可用性。另外在Bigtable或HBase的Range分区模式的基础上,Cloudtable添加了对Hash分区的支持,混合分区的开发也在计划当中。整个系统对外以Thrift接口方式向用户提供服务,兼容HBase Thrift的接口。按语义分类,接口可以分为Get, Mutation(Put/Delete/Increment/Append), Scan, CAS等几大类,同时支持批量接口以及异步调用以充分发挥系统的高性能。

系统运行时,初始建表时预先分配的Tablet可能会超出预设大小而发生Split。Split的目标是,在原来单个Tablet实例上服务的区间[m, 2n)会分裂成两个Tablet实例[m, n), [n, 2n)并将它们分隔开放到不同的服务器。Tablet Split实现为先分离Raft Group,再后台复制存储DB并追赶日志的一整套过程。另外,我们使用Raft中的Member Change算法来处理Raft Group节点的增减,系统中有这几种情况需要增减节点:

  1. 数据迁移
    在Tablet Split流程的后面阶段,需要将新Tablet从旧Tablet迁移出来
  2. 节点替换
    遇到StorageUnit服务器故障崩溃,需要在一台新机器上做恢复
  3. 系统降维
    在极端情况下,部分机房故障/网络分区可能会破坏Raft运行的条件(即Quorum集群可用),可以通过将系统结点数降低到仍然可用的子集来继续服务

性能、运维

Cloudtable可以部署在单机房、同城多机房,也可以部署到异地多机房,一个典型的部署场景是,异地跨机房环境中每台服务器上都运行多个StorageUnit进程,每个StorageUnit进程内部运行着多个Tablet实例。Cloudtable可以很好地利用SSD的IOPS以提供高速读写性能,性能测试结果显示,在使用存储服务器A3机型(CPU E5 * 2/Mem 8G * 6/480G SSD * 4 Raid 0/千兆网卡 * 2)跨机房部署的5结点集群中,运行单个Tablet写吞吐可以达到15k每秒,加载多个Tablet实例整个集群写吞吐可以达到50k每秒。在延时方面,5个机房的平均ping RTT在10~20ms,大量的压力测试结果显示Cloudtable的平均写时延约为26ms,此时测试客户端跟Gateway在同一个机房,对于跨机房接入的应用,其时延为在此基数之上再加上接入到Gateway的耗时。

通过不断的努力,我们相信Cloudtable已经基本达到原来在运维管理方面设定的目标。目前系统能良好运行,对于包括负载均衡,结点失效检测/恢复,在线无缝扩容等常规运维操作进行自主管理,并提供Web管理界面,AdminShell,实时的吞吐/时延监控系统等管理运维工具帮助我们进行运维监控。

应用场景

Cloudtable可以用于需要分布式一致性,高性能读写以及大容量要求的应用中。目前在YY公司内部有移动IM消息,频道公屏消息,xcloud等多个项目正在使用Cloudtable作为存储后台。以频道公屏消息为例作说明,它是将YY客户端所有频道公屏消息都存储到Cloudtable,消息以频道号加上时间戳加用户号为主键的方式来存储,便于以频道号、频道号及时间范围作查询。目前每天新增的数据量在10GB+,每天的吞吐量为2000+每秒,凌晨的几个小时高峰期间吞吐量为8000+每秒。每条数据都有一定时长的有效保存期,超期数据由系统自动清理。

开源

Cloudtable已经在今年9月初在YY内部开源。我们希望用户或其它开发者,在使用云存储团队提供的Cloudtable的存储服务之余,可以有更多的自由来接触源代码本身,来根据自己的需要来定制和加强Cloudtable。我们期待听到他们在使用服务及源代码等各个层面的反馈,以继续改进Cloudtable或添加更多的功能。请关注公司内部开源平台参与进来。

注:
[1] 为方便讨论这里整理了各个BigTable模型系统中的不同命名:

Cloudtable BigTable HBase
Tablet Tablet Region
Storage unit Tablet server Region server
Flush Minor compaction Flush
Universal compaction Merging compaction Minor compaction
Level compaction Major compaction Major compaction
Write-ahead log Commit log Write-ahead log
memtable memtable MemStore
sstable SSTable HFile
Metastore Chubby Zookeeper