Zookeeper 事务操作刷盘行为分析

Zookeeper 性能影响最相关的两个方面,网络速度和磁盘写性能。

影响 zookeeper 事务刷盘的相关参数

参数 默认值 说明
flushDelay 0 两次刷盘之间相隔多久
maxBatchSize 1000 两次刷盘直接允许堆积多少个写请求
forceSync true 刷盘操作本身是否需要立即刷盘,默认 true 意味着刷盘操作必须同步到硬件,不丢失数据

影响刷盘操作的三个参数,前两个控制业务逻辑,后一个控制具体的刷盘行为。

每一次事务操作落盘前用来检查是否需要落盘的方法 shouldFlush:

1private boolean shouldFlush() { 2  long flushDelay = zks.getFlushDelay(); 3  long maxBatchSize = zks.getMaxBatchSize(); 4  if ((flushDelay > 0) && (getRemainingDelay() == 0)) { 5    return true; 6  } 7  return (maxBatchSize > 0) && (toFlush.size() >= maxBatchSize); 8}

shouldFlush 方法用来确定是否需要刷盘操作,可以看到在默认的情况下,只要堆积了 maxBatchSize 个写入请求后才会刷盘,因为 flushDelay 是 0,不生效,这样要等到堆积 1000 个写请求会导致数据不安全。

1Request si = queuedRequests.poll(pollTime, TimeUnit.MILLISECONDS); 2if (si == null) { 3    /* We timed out looking for more writes to batch, go ahead and flush immediately */ 4    flush(); 5    si = queuedRequests.take(); 6}

通过以上代码可知,在 SyncRequestProcessor 的 run 方法每读取到一个 request 时,会立刻做一个判断,如果当前的请求队列已经空了(读取到的 request(si) 是 null),在循环任务阻塞前会立即调用 flush 方法,所以即使只有一个写请求,也会被立即 flush,并不会有响应延迟的问题。只有在请求量非常大并且前置环节速度快的情况下写请求才会在两次刷盘之间堆积。

如果前置处理环节的速度非常快,两次刷盘操作之间堆积的写请求超过了 1000 个,下次刷盘操作会立即写入,即使堆积的写请求还没到 1000 个,sync 处理逻辑循环到下一次时会拿不到下一个写请求,在工作线程阻塞前会立即把堆积的不到 1000 个的请求刷盘。

这样既保证了响应速度,也保证了写入性能,因为批量最多默认堆积1000个就会刷盘,而批量刷盘操作和刷一个性能没有太大区别(文件一次顺序追加写操作)。

只有完成了以上所有操作,具体的一个事务写操作才能结束,才能响应给客户端,也就是只要客户端收到 leader 节点的确认消息后,必定已经完成了 flush 操作(不管内部是缓冲或者等待了多久,反正写入结束才会调用下一个责任链触发结果回复),数据也必定落盘了。

也可以得出结论,对以上参数的 forceSync 的调整会影响 zk 的数据一致性保证。而 flushDelay 和 maxBatchSize 并不影响数据一致性,但是 flushDelay 在事务堆积的情况下会影响事务响应时长,maxBatchSize 性价比更高。

刷盘操作本身分析:

刷盘操作先文件流的 flush,根据 Java Api 说明可知 OutputSteam.flush 只是把数据从 Java 的缓冲写入了下层缓冲(也就是交给了操作系统,操作系统控制具体什么时间落盘),并不能确保操作系统把数据刷到磁盘,也就是 OutputStream 的 flush 方法并不是刷盘操作,性能非常高。

然后如果 zk 的参数 forceSync 是 true(默认值),才会调用 FileChannel 的 force 方法(force 方法强制数据全部写入硬件存储设备 to the media),调用 force 方法的参数 metaData 是 false,说明只要强制把文件内容刷到物理设备就可以了,如果 metaData 是 true, 意味着文件的元信息也要刷新到磁盘中,意味着更多的磁盘 IO 操作,而 zookeeper 的文件大小是预分配好的(提前扩容的),所以没必要刷新文件的元数据。

Java 流的 flush 方法和 Channel 的 force 方法部分说明

OutputSream.flush()

Flushes this buffered output stream. This forces any buffered

output bytes to be written out to the underlying output stream.(只能保证 Jvm 崩溃后 Jvm 层面数据安全,并不能保证操作系统崩溃后数据安全)


FileChannel.force()

Forces any updates to this channel’s file to be written to the storage device that contains it. If this channel’s file resides on a local storage device then when this method returns it is guaranteed that all changes made to the file since this channel was created, or since this method was last invoked, will have been written to that device. This is useful for ensuring that critical information is not lost in the event of a system crash.

通过高亮部分可知,force 方法调用才能保证数据安全的写入到了硬件磁盘中,至于磁盘本身是写入了永久存储还是磁盘缓冲,需要进一步确认才可知。