Cassandra源码阅读之Batch实现

作者: yangzhe1991 分类: 我是搞技术的 发布时间: 2014-12-29 15:48 ė 61条评论

Cassandra的batch,就是一次提交多个修改操作,节省传输请求的资源消耗。同时也可以理解为一种事务的解决方案——all or nothing,这些操作可以保证要么都成功要么都不成功,即原子性。

这里需要注意几点:

1:batch只保证原子性,要么都成功要么都失败,不保证隔离性。就是说可能存在某个时间点,batch的若干个修改只能读到一部分。同时batch也可以指定本次操作不需要保证原子性,这样显然性能会更高。另外counter因为不是幂等操作(多加了一次总和就变了)以及其实现比较复杂,不支持原子性的batch。

2:batch中针对同一个row的操作会先合并再写,直接变成了一条操作。所以Cassandra是可以通过static column的batch来实现订单和余额在一个表,一个人的全部订单和余额放在一个row,对一个row key(userid)同时写成交新订单和余额修改操作,这样可以通过batch来实现原子性+隔离性。换句话说,只涉及到一个人的余额变化及订单成交,没有另一个人的余额变化,是可以搞的,而另一种事务的典型场景——一个人给另一个人转账,一个余额增加一个余额减少——这种跨行事务Cassandra应该至少目前是搞不定的。

3:非batch的普通修改请求,在写失败的时候不会回滚,比如QUORUM的操作,写两份client才认为成功,假如3个节点只写成了一份另两份超时,client会报错说写入失败,但实际上写成了的那份也没回滚。不过这个倒是问题不大,毕竟写失败的时候client可以重试的,幂等的操作反复写也没事。

执行batch操作的逻辑,和其他逻辑一样,CQL解析完了之后发现这是batch,就把每个写操作封装成List<Mutation>执行StorageProxy.mutateWithTriggers,然后根据操作的对象或语句option来判断应该mutateAtomically还是mutate。mutate就跟普通的写操作没区别了,每条操作挨个执行。

为了保证原子性,写入之前会先生成一个batchlog,将整个batch内全部Mutation序列化,带上当前时间戳和batch请求的时间戳(可以是客户端指定也可以是用接受客户端请求的节点的时间戳),传给本地机房的若干个节点,要求满足一致性要求的节点数并且不小于2。因为batch请求的row key可能不同,这里存储batchlog的节点的选择其实只是找若干个尽可能不在同一个机架上的节点,就是为了防丢数据。

batchlog的表定义如上,也因为会把全部Mutation序列化成blob存储,如果操作占用的空间太大(默认是5K),会报warning,毕竟大的blob的读写会略微影响Cassandra的性能。如果这里写入超时了,整个请求报超时异常返回。

然后就可以正式的写数据了,逐条发送给row key所在的节点。这步总体是同步的,但batch中的每个Mutation是异步的发,最后阻塞等待全部返回。如果这里有某个Mutation超时,那整个请求抛超时异常返回。

执行完这步没抛异常,说明所有Mutation都写成功了,返回client成功并异步删除batchlog。

显然只有这些是不够的,因为如果在写Mutation的时候部分请求没写成,会导致整个batch部分成功部分失败。当然,如果这个时候马上来读(或者Mutation正在执行还不知道是否全部成功的时候),就是所谓不满足隔离性的时候,这个时候读只能读到那些已经成功的,还在执行的和已经失败的还是读不到。

而这个时候就需要batchlog起作用了。每个节点有个独立的BatchlogManagerMBean,来维护batchlog信息。这个MBean用scheduleWithFixedDelay间隔60秒来起一个线程把本地batchlog表中所有的log拿出来去再把每个Mutation写一遍(也因此batchlog要求必须是幂等的操作,也就是不能操作counter,因为不怕重复执行)。这里会涉及到一些细节:比如为了不加重负载写的时候有个限速(顺便说下Cassandra的所有限速都是通过guava的RateLimiter来实现);每次从batchlog里拿128条去写,然后while循环不断的取直到剩余的log不到128了就停止隔下一分钟再取;写batchlog的时候得记录batch生成的时间戳以及整个batch的超时时间,确保只把那些确实超时了之后还没删除的batchlog执行replay(因为这些很可能是因为写失败了没删掉log)。全部Mutation都写成了,就删除这条log,否则留着等下一次执行再试一次,直到最终都写成了。

因此,所谓batch保证原子性,要么都成功要么都失败,其实就是只要batchlog写成了,那最终一定保证都成功,只有写batchlog的时候就悲剧了,才会都失败。而且因为batchlog至少要写两份,只要有一份写成了,那最终也肯定都会把所有Mutation写进去。

另外,batch中的多次写操作如果其中有一个是用了CAS的,那么整个batch的所有操作必须是针对同一个row的,因为同一个row的操作会合并成一个操作,这样可以一次CAS请求写全部。如果多个row的batch有了CAS就是不可控的了,一旦这个CAS挂了其他row的操作没办法回滚,就不能保证原子性了。

总结起来说,就是这个batch,只能降低一点点client到node的网络传输,但并不能提高性能(反而下降因为要有batchlog),而且因为一旦跨行/跨表就不能保证隔离性,所以本质上跟client逐个写然后把没成功的若干个反复重试没有区别,顶多是他是俩节点存batchlog不怕宕机而客户端可能写一半机器挂了。甚至理论上client上用异步接口同时写N条可能更快,因为client driver一般会优化,在选择节点的时候是先看row key的位置的,也就是说虽然Cassandra不需要smart client,随便连一个node就能执行全部操作,但实际上一般client实现的时候还是自己很smart的从3个节点中选择一个来连,这样三备份的时候有其中一个请求是直接读写本地,速度肯定最快,对系统的负担也最小。

所以batch这个功能在大多数情况下是不建议你用的……只有前面提到的基于static column来实现在一个row中同时成交订单并修改余额,才有用batch的意义,因为CQL的写操作是针对一个column的,而batch可以合并同一个row的各种操作变成一次保证原子性+隔离性的操作。另外官方举的余额相关的例子是通过CAS改一个普通的数字column然后logged batch一个row来实现的,而非counter上直接减去多少,毕竟余额这玩意你得先判断有才能成交。

本文出自 杨肉的演讲台,转载时请注明出处及相应链接。

本文永久链接: https://yangzhe1991.org/blog/2014/12/cassandra-code-reading-batch-implementation/

一条评论

  1. Pingback: RAMP事务 - 杨肉的演讲台

发表评论

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

Ɣ回顶部