离职系列文章之Cassandra使用经验

离职“系列文章”只写一篇肯定是不对的……

前文写了Redis集群之后,大家纷纷表示我在吐槽公司太穷机器挫,其实我真不是故意要吐槽的……于是很纠结写完Redis之后要不要写Cassandra,因为Cassandra用的机器更挫,直到离职前不久才换了几台新机器……

从去年3月开始调研依赖,接触C*也有一年多了。中间写过一些文章,但总体上不是很系统,而且其实也不是很确定写的是不是一定正确;一些是读代码之后的笔记,主要是针对2.1的。所以在这个离职的欢乐祥和的时刻,想跳过细节谈经验和感受。当然我觉得我写这种文章也是有局限的,因为除了C*之外,其他的开源分布式数据库(尤其是HBase)都没用过,所以很难用实战来对比说到底哪个更好、哪个适合什么场景、哪个坑更少。

本来是想接着像前文一样先说技术背景的。但是写了上千字的历史故事后看了下感觉对文章主旨没啥帮助,而且怎么看怎么还像是在吐槽,于是就都删了,直接进入正题……当然,阅读本文需要一定的Cassandra基础,比如数据模型、大概的原理等等。入门的东西就不太想说了。

Cassandra是靠时间戳谁大谁赢来维护版本的,这个时间戳在初期是由client提供,后来变成默认由接受client请求的节点提供,同时可选由client指定任意一个timestamp。有些地方说这属于最终一致性,有些地方说看W+R和N的关系,大于是强一致,小于等于是最终一致性,我觉得都不对。因为分布式系统的时间戳是无法协调一致的,一旦因为时间戳不准,先写的时间戳反而更大,后写的数据是永远读不到的。因此C*只能说是如果W+R>N那么可以马上读到时间戳最大的,如果<=那么能最终读到时间戳最大的,这属于哪种一致性我就不清楚了。当然这里会有些小优化,比如一个C*进程内提供的时间戳是保证绝对单增的,来抵消闰秒等特殊情况。本质上这都是把分布式系统中一个非常难搞的问题——时序——给简化了从而完全不是问题了。于是如果这个模型的简化你不可接受,那么C*就肯定没法用了;如果能接受,那么很多问题会简单,性能或可用性也至少能提升一个。

其实,有NTP尽量减少时间戳的误差(NTP的误差取决于网络延迟,一个机房内的延迟小于1ms,所以时间戳的误差也小于甚至远小于1ms),而且因为Cassandra有列的概念可以只update其中一个列,而非一些单纯的kv数据库那样想update其中一个字段就需要整行读出来、更新、回写,从而减少了冲突导致数据出错的概率。所以如果在单机房的情况下,如果能保证1ms之内不会有两个不同的client(因为client如果相同那么可以靠client提供单增时间戳保证一致性)来写同一行的同一个字段,那么在W+R>N的情况下是可以把系统当作强一致性的。

我们的业务也不太可能短时间频繁写一个key,更别说同时写同一列了,即使同时我们的业务也不太在乎这种情况导致的一致性问题,所以Cassandra对我们来说只有好处了——方便运维、可用性有极强的保障。这里的可用性比一般能做到“高可用”更强,高可用的系统一般还是需要有一定的恢复时间(比如master选举、log恢复等等),虽然可能也用不了多长时间,但C*的“可用性”是时刻保持可用的,集群挂一个节点是几乎不影响任何读写请求的(除非是client请求的那个节点挂了,可以马上找另一个节点重试)。现在用C*较多的很多是一些视频公司,感觉也是跟他们的业务正好符合上述这些需求有关系。

 

使用场景说完了,开始case by case的说说遇到的一些经验。

首先是增减节点。C*使用虚拟节点(默认256个)来扩充一致性哈希上的node从而保证数据均匀。靠streaming负责一致性哈希的环发生变化时的数据迁移——说白了就是增减机器的时候导数据,都导完了才能用。但是这会遇到一个问题,就是每个vnode都意味着一个迁移操作,每个迁移操作意味着对方节点需要把他每个SSTable上对应的range传输给你,每个range传输完毕后会生成一个新的SSTable,于是新加进来的节点的SSTable数会非常多,影响性能,一个表上千个SSTable都很正常,要compaction很久才能恢复到正常水平。当然一次只加一个节点的话对集群总体性能影响不大,而且毕竟增减节点也不是一个常用的操作。但最坑的是C*的streaming似乎有bug,存在一种情况是某两个节点导数据卡在那,显示进度是100%,但不成功也不失败,永远说还在导,但实际上早就不导数据甚至可能已经导完了,设了timeout也不管用。遇到这个情况只能强制关闭节点,让他认为导数据失败了,然后重启节点重新导。然后根据经验,我发现基本上新重启过的节点从来不出导数据卡住的问题,于是每次需要增减节点的话,我都是把所有node挨个重启一遍来绕开这个坑……

然后是compaction。Cassandra现在提供三种compaction的策略。Size-tiered、Leveled、Date-tiered。Size就是把大小相近的SSTable合成大的,是最常用的;Leveled就是跟leveldb一样,适合读>>写而且插入多于修改和删除的;Date-tiered是最近刚加的,适合类似发微博之类的对每个partition key插入的column key永远单增(比如当前时间戳)而且不修改旧数据的。第三个没用过,只有一个表用过leveled,是词典的发音数据。词典发音数据是一堆真人发音+例句的TTS发音文件,当数据库没有数据的时候,去请求TTS服务器来合成语音并存入Cassandra,所以这个表的写入量非常小,几乎没有,因此很适合leveled compaction。除了这个表还用了另一个表存“海量发音”,就是词典去年新出的能看一个词全世界各国人民的当地发音的数据,这个表只用了Size-tiered,因为要不断从合作方抓数据,更新的频率比前一个表大不少。对比来看的话,使用leveled compaction的表的平均读取延迟少了一半。

然后是tombstone。LSM类的数据库都是把删除替换为写入一个标记,叫tombstone。HBase也差不多的原理,删除数据的话把tombstone写到log里,compaction的时候如果遇到tombstone就可以不用保留已经被删的数据,读数据的时候遇到tombstone就屏蔽掉数据并且跳过,最后返回所有没删的数据。但是,C*因为并不是靠单个region server来读写数据,而是N个节点同时读写,所以跟HBase最大的区别,就是每个节点维护tombstone的时候不能只考虑自己。于是某单个节点读数据的时候读到tombstone后,不能屏蔽掉data然后跳过,而是要把tombstone也返回给接受client请求的那个coordinator,因为你不知道其他节点是否写入了这个tombstone(因为写入W个就算成功)。而且在compaction的时候不能判断说当前节点跟这个tombstone有关的数据都删完了就把自己也删了,因为你不确定别的节点有没有这个tombstone——如果别的节点在写tombstone的时候没写成(毕竟写W个节点就算成功,最多N-W个节点没有这个tombstone),自己又在compaction的时候把tombstone删了,那么再读数据的时候有节点有这行数据,又没有节点有这行数据的tombstone,于是这个数据就复活了……

处理这两个tombstone导致的问题,解决方案就是,首先避免大量tombstone的生成。如果不能避免量删column key,那么一定要避免对column key做范围查找。因为读的时候会把所有范围内包含的tombstone返回给coordinator,而tombstone又不是有效的返回数据不能用来分页,导致可能client设limit 100,期望读100行数据,但对节点来说是100行数据+中间穿插的1000个tombstone,gc压力、响应时间跟读1100行数据差不多。所以C*默认会在一次读取遇到几万个tombstone的时候强制终止读取数据抛异常。对于数据复活的问题,解决方案只能是定期做repair操作,并且repair的间隔小于tombstone的保留时间(默认十天,因此官方是建议一周repair一次)。当然如果不在乎数据复活,删数据只是为了节约磁盘,那么不repair也行。复活就复活吧。

然后是一个feature,需要注意。就是C*不对column key做bloom filter的优化。曾经有,后来干掉了,不知道为啥。而HBase我记得是可以有的。于是这就导致如果读特定某行数据,如果table的定义是所有primary key都是partition key,那么可能bloom filter过滤完只需要读一个SSTable,但如果有column key,而且partition key下面有非常多行数据,那么bloom filter对column key不起作用,这个partition key下面的数据分布在多个SSTable里,就需要读所有这些SSTable,影响性能。因此,如果对范围查询没需求,那么就尽量把所有主键都设成partition key。

 

说完坑了,说下在有道折腾Cassandra的一年里的几个遗憾。

一个是没用过secondary index。因为Cassandra毕竟是NoSQL,他对列建索引跟MySQL是完全不同的。有道的同事试图用这玩意的时候,几乎都是因为没仔细看C*文档误把它当MySQL的index用。因为限制大,所以也没啥需求用这个,所以对这个东西就不是很了解,没法深入的研究了。

一个是内存小,没怎么试过row cache。我一直认为在大多数场景中,磁盘数据库和内存缓存是可以合二为一,靠数据库自身保证缓存一致性并方便应用层使用的。这应该是不需要跨行事务的NoSQL的发展方向。合并的思路有两个,一个是redis这类内存数据库支持持久化,一个是Cassandra这类磁盘数据库开内存当cache。我个人很难说是倾向于哪个方案,但是至少第二种方案已经有现成的了,而第一种方案好像还没完全成熟。

一个是Cassandra因为任意时间挂一个节点都基本不影响整个集群的操作。所以我一直有个想法就是主动的关闭响应来做full gc,来保证平时接受请求的时候完全不gc。而且因为Cassandra可以通过外部命令开关服务,关闭服务也不会无脑切断当前正在进行的请求只是通知其他节点不要发新的了,而JVM也可以外部强制GC,所以这东西都不需要改C*代码就可以很平稳的搞。只是因为没时间,没试过。前几天看到个文章是有个公司也这么干,但不是Cassandra而是WebServer,因为需要通知上层HA啥的所以反而麻烦,而C*的改动量应该小很多。这样对降低.99、.999、max延迟应该有一定帮助。当然因为GC的时候相当于W=R=N,可用性的问题倒是不大因为gc的时间不长,但读取数据的时候不能N选R从而做优化,在这方面会略微增加延迟,就看两者抵消后的最终效果如何了。

 

然后说说Cassandra的版本。去年搞2.1的时候,那帮开发者觉得要好好测试才能发布,于是发了好几个beta、好几个rc,然后才很谨慎的发布2.1.0正式版。当年的Cassandra官网上,会推荐你用2.1.x的latest version来作为新用户的使用版本。而且因为2.1改进了一些性能问题,当初为了优化下性能我就把集群给升到2.1了。但是,如一个邮件里总结的,2.1每个版本都有大问题以至于在生产环境用很容易被坑(邮件里提到的2.1.4后来变成一个安全更新,只改了一个安全漏洞,没任何patch,所以实际上应该是2.1.5)。我自己就遇到了至少两个bug,并亲自改了一个,分别是导致assert error无法读数据,以及启动的时候内存泄露把heap占满。后来基于现在来说介于2.1.3和2.1.5之间的一个2.1分支上的版本自己编译了一下,用这个非正式版本跑,至少走之前没遇到啥问题了。现在2.1.5也发布了,感觉上也许大概可能,算是生产环境可用了吧。然后我发现那帮人很鸡贼,不知道啥时候开始已经改了官网的描述,同时提供2.0.x和2.1.x俩版本,一个叫“most stable release”一个叫“latest release”。但是根据惯例,等3.0发布的时候,这个所谓的稳定版就EOL了……

然后不知道出于啥目的,Cassandra准备在3.x时代改变目前的branch的发行方式,改成每个月一个版本的tick-tock模型。大概就是3.0继续保持旧方案,不断的3.0.x来更新。然后下个月发一个提供新feature的3.1,再下一个月改3.1的bug叫3.2,然后再下一个月加feature叫3.3,然后一个月改bug叫3.4……说白了就是,你肯定要用偶数版本来用于生产环境,比如3.2,但如果这个版本有问题,除非很紧急值得发一个3.2.1啥的,否则基本上就得等一个月发3.3再升级,但3.3因为有新feature,没准就引入新bug……总之我是觉得这个模式非常不靠谱。也没听说哪个数据库是这么玩的,数据库终究还是一个先追求稳定再追求性能最后追求功能的东西。

 

最后说说HBase。当然我没用过HBase,只用过有道山寨的HBase。这里不考虑Master、ZooKeeper需要额外节点的问题。虽然说对小集群小公司来说确实是个问题。比如对用codis的人来说很多人是第一次接触zookeeper,单纯为了codis搭一套zookeeper其实也确实挺蛋疼的。

HBase按我的理解是不分散计算、只分散存储。持久化的东西和Cassandra也是存N份,但计算相关的都只有一份,比如compaction只费一个机器的CPU、MemTable只读写一个机器(不考虑RegionServer搞HA啥的),所以理论上感觉HBase对资源的消耗应该是小于Cassandra的。虽然可能也许大概是因为底层用HDFS而非直接读写磁盘,总体来说性能稍微比Cassandra差一点,但感觉在未来还是可以逐渐优化的,更何况其实大多数场景也不会榨干性能。把数据库用到极致的公司可能会考虑两者的优劣来选择,甚至俩一起用分别用作不同的场景,但对于中小公司来说,其实用啥都一样……

结尾插播半个广告,如果想持续关注本人的博客,又没有用RSS阅读器的习惯,可以关注下面这个微信公众号,会同步发博客上的文章。

qrcode_for_gh_59f1bacc54eb_258


已发布

分类

来自

标签:

评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注