IT技术博客大学习 共学习 共进步

HBase如何合理设置客户端Write Buffer

量子恒道官方博客 2012-11-27 13:36:42 浏览 3,724 次

   HBase客户端API提供了Write Buffer的方式,即批量提交一批Put对象到HBase服务端。本文将结合HBase相关源码,对其进行深入介绍,分析如何在实际项目中合理设置和使用它。

什么时候需要Write Buffer?

默认情况下,一次Put操作即要与Region Server执行一次RPC操作,其执行过程可以被拆分为以下三个部分:

  • T1:RTT(Round-Trip Time),即网络往返时延,它指从客户端发送数据开始,到客户端收到来自服务端的确认,总共经历的时延,不包括数据传输的时间;

  • T2:数据传输时间,即Put所操作的数据在客户端与服务端之间传输所消耗的时间开销,当数据量大的时候,T2的开销不容忽略;

  • T3:服务端处理时间,对于Put操作,即写入WAL日志(如果设置了WAL标识为true)、更新MemStore等。

  • 其中,T2和T3都是不可避免的时间开销,那么能不能减少T1呢?假设我们将多次Put操作打包起来一次性提交到服务端,则可以将T1部分的总时间从T1 * N降低为T1,其中T1为一次RTT时间,N为Put的记录条数。

    正是出于上述考虑,HBase为用户提供了客户端缓存批量提交的方式(即Write Buffer)。假设RTT的时间较长,如1ms,则该种方式能够显著提高整个集群的写入性能。

    那么,什么场景下适用于该种模式呢?下面简单分析一下:

  • 如果Put提交的是小数据(如KB级别甚至更小)记录,那么T2很小,因此,通过该种模式减少T1的开销,能够明显提高写入性能。

  • 如果Put提交的是大数据(如MB级别)记录,那么T2可能已经远大于T1,此时T1与T2相比可以被忽略,因此,使用该种模式并不能得到很好的性能提升,不建议通过增大Write Buffer大小来使用该种模式。

  • 如何配置使用Write Buffer?

       如果要启动Write Buffer模式,则调用HTable的以下API将auto flush设置为false:

    void setAutoFlush(boolean autoFlush)

       默认配置下,Write Buffer大小为2MB,可以根据应用实际情况,通过以下任意方式进行自定义:

       1)  调用HTable接口设置,仅对该HTable对象起作用:

    void setWriteBufferSize(long writeBufferSize) throws IOException

       2)  在hbase-site.xml中配置,所有HTable都生效(下面设置为5MB):

    <property>

    <name>hbase.client.write.buffer</name>

    <value>5242880</value>

    </property>

       该种模式下向服务端提交的时机分为显式和隐式两种情况:

       1)  显式提交:用户调用flushCommits()进行提交;

       2)  隐式提交:当Write Buffer满了,客户端会自动执行提交;或者调用了HTable的close()方法时无条件执行提交操作。

    如何确定每次flushCommits()时实际的RPC次数?

       客户端提交后,所有的Put操作可能涉及不同的行,然后客户端负责将这些Put对象根据row key按照 region server分组,再按region server打包后提交到region server,每个region server做一次RPC请求。如下图所示:

       

    如何确定每次flushCommits()时提交的记录条数?

       下面我们先从HBase存储原理层面“粗略”分析下HBase中的一条Put记录格式:

       HBase中Put对象的大小主要由若干个KeyValue对的大小决定(Put继承自org/apache/hadoop/hbase/client/Mutation.java,具体见Mutation的代码所示),而KeyValue类中自带的字段占用约50~60 bytes(参考源码:org/apache/hadoop/hbase/KeyValue.java),那么客户端Put一行数据时,假设column qualifier个数为N,row key长度为L1 bytes,value总长度为L2 bytes,则该Put对象占用大小可按以下公式预估:

    Put Size = ((50~60) + L1) * N + L2) bytes

       下面我们通过对HBase的源码分析来进一步验证以上理论估算值:

       HBase客户端执行put操作后,会调用put.heapSize()累加当前客户端buffer中的数据,满足以下条件则调用flushCommits()将客户端数据提交到服务端:

       1)每次put方法调用时可能传入的是一个List<Put>,此时每隔DOPUT_WB_CHECK条(默认为10条),检查当前缓存数据是否超过writeBufferSize,超过则强制执行刷新;

       2)autoFlush被设置为true,此次put方法调用后执行一次刷新;

       3)autoFlush被设置为false,但当前缓存数据已超过设定的writeBufferSize,则执行刷新。

            private void doPut(final List<Put> puts) throws IOException {

                      int n = 0;

                      for (Put put : puts) {

                               validatePut(put);

                               writeBuffer.add(put);

                               currentWriteBufferSize += put.heapSize();

                               // we need to periodically see if the writebuffer is full instead

                               // of waiting until the end of the List

                               n++;

                               if (n % DOPUT_WB_CHECK == 0

                                                  && currentWriteBufferSize > writeBufferSize) {

                                        flushCommits();

                               }

                      }

                      if (autoFlush || currentWriteBufferSize > writeBufferSize) {

                               flushCommits();

                      }

                }

       由上述代码可见,通过put.heapSize()累加客户端的缓存数据,作为判断的依据;那么,我们可以编写一个简单的程序生成Put对象,调用其heapSize()方法,就能得到一行数据实际占用的客户端缓存大小(该程序需要传递上述三个变量:N,L1,L2作为参数):

    import org.apache.hadoop.hbase.client.Put;

    import org.apache.hadoop.hbase.util.Bytes;

    public class PutHeapSize {

            /**

             * @param args

             */

            public static void main(String[] args) {

                      if (args.length != 3) {

                               System.out.println(“Invalid number of parameters: 3 parameters!”);

                               System.exit(1);

                      }

                      int N = Integer.parseInt(args[0]);

                      int L1 = Integer.parseInt(args[1]);

                      int L2 = Integer.parseInt(args[2]);

                      byte[] rowKey = new byte[L1];

                      byte[] value = null;

                      Put put = new Put(rowKey);

                      for (int i = 0; i < N; i++) {

                               put.add(Bytes.toBytes(“cf”), Bytes.toBytes(“c” + i), value);

                      }

                      System.out.println(“Put Size: ” + (put.heapSize() + L2) + ” bytes”);

            }

       }

       该程序可以用来预估当前设置的Write Buffer可以一次性批量提交的记录数:

    Puts Per Commit = Write Buffer Size / Put Size

       更进一步地,如果知道业务中的每秒产生的数据量,就可知道客户端大概多长时间会隐式调用flushCommits()向服务端提交一次,同时也可反过来根据数据实时刷新频率调整Write Buffer大小。

    Write Buffer有什么潜在的问题?

       首先,Write Buffer存在于客户端的本地内存中,那么当客户端运行出现问题时,会导致在Write Buffer中未提交的数据丢失;由于HBase服务端还未收到这些数据,因此也无法通过WAL日志等方式进行数据恢复。

       其次,Write Buffer方式本身会占用客户端和HBase服务端的内存开销,具体见下节的详细分析。

    如何预估Write Buffer占用的内存?

       客户端通过Write Buffer方式提交的话,会导致客户端和服务端均有一定的额外内存开销,Write Buffer Size越大,则占用的内存越大。客户端占用的内存开销可以粗略地使用以下公式预估:

    hbase.client.write.buffer * number of HTable object for writing

       而对于服务端来说,可以使用以下公式预估占用的Region Server总内存开销:

    hbase.client.write.buffer * hbase.regionserver.handler.count * number of region server

       其中,hbase.regionserver.handler.count为每个Region Server上配置的RPC Handler线程数。

建议继续学习

  1. HBase集群出现NotServingRegionException问题的排查及解决方法 (阅读 17,124)
  2. HFile存储格式 (阅读 15,823)
  3. hbase运维 (阅读 14,784)
  4. hbase介绍 (阅读 12,225)
  5. HBase技术介绍 (阅读 7,944)
  6. Buffer和cache的区别是什么? (阅读 7,843)
  7. HBase随机写以及随机读性能测试 (阅读 7,425)
  8. HBase性能优化方法总结 (阅读 6,946)
  9. HBase二级索引与Join (阅读 6,862)
  10. HBase Thrift 接口使用注意事项 (阅读 6,603)