2021SC@SDUSC hbase源码分析(四)写入流程(3)


2021SC@SDUSC
2021SC@SDUSC
2021SC@SDUSC

Hbase写特性:

Hbase是一个比较少见的写比读快的数据库,原因是在写的过程中,既要写Hlog文件也要将数据写到内存;读的时候需要将所有数据数据文件合起来(包括MemoryStore)最后读出来。因此Hbase进行读操作所需的时间较多。

HBase服务端没有提供update,delete接口,HBase中对数据的更新、删除操作都认为是写入操作,更新操作会写入一个最小版本数据,删除操作写写入一条标记为deleted的KV数据

Hbase写入流程概况:

  1. 客户端处理阶段
  2. Region写入阶段
  3. MemStore Flush阶段

相关源码分析:

2.Region写入阶段

数据写入Region的流程可以抽象为两步:追加写入HLog,随机写入MemStore。

服务器端RegionServer接收到客户端的写入请求后,首先会反序列化为put对象,然后执行各种检查操作,比如检查Region是否是只读、MemStore大小是否超过blockingMemstoreSize等。检查完成之后,执行一系列核心操作:

流程图:
请添加图片描述

  1. Acquire locks :获取行锁,HBase中使用行锁保证对同一行数据的更新都是互斥操作,用以保证更新的原子性,要么更新成功,要么更新失败。
  2. Update LATEST_TIMESTAMP timestamps:更新所有待写入/更新KeyValue的时间戳为当前系统时间。
  3. Build WAL edit:HBase使用WAL机制保证数据可靠性,即首先写日志再写缓存,即使发生宕机,也可以通过恢复HLog还原出原始数据。
    该步骤就是在内存中构建WALEdit对象,为了保证Region级别事务的写入原子性,一次写入操作中所有KeyValue会构建成一条WALEdit记录。
  4. Append WALEdit To WAL :将步骤3中构造在内存中的WALEdit记录顺序写入HLog中,此时不需要执行sync操作。当前版本的HBase使用了disruptor实现了高效的生产者消费者队列,来实现WAL的追加写入操作。
  5. Write back to MemStore:写入WAL之后再将数据写入MemStore。
  6. Release row locks:释放行锁。
  7. Sync wal:HLog真正sync到HDFS,在释放行锁之后执行sync操作是为了尽量减少持锁时间,提升写性能。如果sync失败,执行回滚操作将MemStore中已经写入的数据移除。
  8. Advance mvcc:此时该线程的更新操作才会对其他读请求可见,更新才实际生效
(1)追加到HLog
HLOG相关解析

关于Hbase中HLog的文件格式、生命周期在下一篇博客中会详细介绍。

HLog持久化等级:

HLog保证成功写入MenStore中的数据不会因为进程异常退出或者机器宕机而丢失,但实际并不完全如此,Hbase定义了多个HLog持久化等级,使得用户在数据高可靠和写入性能之间进行权衡:
示例代码:

@Override
public Put setDurability(Durability d) {
  return (Put) super.setDurability(d);
}
public enum Durability {
  USE_DEFAULT,
    
  SKIP_WAL,
    
  ASYNC_WAL,

  SYNC_WAL,

  FSYNC_WAL
}
  • SKIP_WAL:只写缓存,不写HLog日志。因为只写内存,因此这种方式可以极大地提升写入性能,但是数据有丢失的风险。在实际应用过程中并不建议设置此等级,除非确认不要求数据的可靠性。
  • ASYNC_WAL:异步将数据写入HLog日志中。
  • SYNC_WAL:同步将数据写入日志文件中,需要注意的是,数据只是被写入文件系统中,并没有真正落盘。HDFS Flush策略详见HADOOP-6313。
  • FSYNC_WAL:同步将数据写入日志文件并强制落盘。这是最严格的日志写入等级,可以保证数据不会丢失,但是性能相对比较差。
  • USER_DEFAULT:如果用户没有指定持久化等级,默认HBase使用SYNC_WAL等级持久化数据。
HLog写入模型

在之前的版本中,HLog写入都需要经过三个阶段:

  1. 首先将数据写入本地缓存
  2. 然后将本地缓存写入文件系统
  3. 最后执行sync操作同步到磁盘。

当前版本中,HBase使用LMAX Disruptor框架实现了无锁有界队列操作

如图:
请添加图片描述

流程源码分析:

private WriteEntry doWALAppend(WALEdit walEdit, Durability durability, List<UUID> clusterIds,
    long now, long nonceGroup, long nonce, long origLogSeqNum) throws IOException {
  Preconditions.checkArgument(walEdit != null && !walEdit.isEmpty(),
      "WALEdit is null or empty!");
  Preconditions.checkArgument(!walEdit.isReplay() || origLogSeqNum != SequenceId.NO_SEQUENCE_ID,
  WALKeyImpl walKey = walEdit.isReplay()?
      new WALKeyImpl(this.getRegionInfo().getEncodedNameAsBytes(),
        this.htableDescriptor.getTableName(), SequenceId.NO_SEQUENCE_ID, now, clusterIds,
          nonceGroup, nonce, mvcc) :
      new WALKeyImpl(this.getRegionInfo().getEncodedNameAsBytes(),
          this.htableDescriptor.getTableName(), SequenceId.NO_SEQUENCE_ID, now, clusterIds,
          nonceGroup, nonce, mvcc, this.getReplicationScope());
  if (walEdit.isReplay()) {
    walKey.setOrigLogSeqNum(origLogSeqNum);
  }

  if (this.coprocessorHost != null && !walEdit.isMetaEdit()) {
    this.coprocessorHost.preWALAppend(walKey, walEdit);
  }
                              
     //。。。                         
  WriteEntry writeEntry = null;
    //。。。                         
                              
  try {
    long txid = this.wal.appendData(this.getRegionInfo(), walKey, walEdit);
    if (txid != 0) {
        //调用sync
      sync(txid, durability);
    }
    writeEntry = walKey.getWriteEntry();
  } catch (IOException ioe) {
    if (walKey != null && walKey.getWriteEntry() != null) {
      mvcc.complete(walKey.getWriteEntry());
    }
    throw ioe;
  }
  return writeEntry;
}

上述代码属于append操作,WALEdit和HLogKey所在的List封装为WriteEntry类,并在之后调用sync操作,在sync方法中代码如下:

public void sync(boolean forceSync) throws IOException {
  try (TraceScope scope = TraceUtil.createTrace("AsyncFSWAL.sync")) {
    long txid = waitingConsumePayloads.next();
      //生成SyncFuture对象
    SyncFuture future;
    try {
      future = getSyncFuture(txid, forceSync);
      RingBufferTruck truck = waitingConsumePayloads.get(txid);
      truck.load(future);
    } finally {
      waitingConsumePayloads.publish(txid);
    }
    if (shouldScheduleConsumer()) {
      consumeExecutor.execute(consumer);
    }
    blockOnSync(future);
  }
}

在sync方法中会生成一个SyncFuture对象,再封装成RingBufferTruck类放到同一个队列中,然后工作线会阻塞,等待notify()来唤醒。

RingBufferTruck中的属性:

private SyncFuture sync;
private FSWALEntry entry;

右侧部分会根据从RingBufferTruck对象中取出的进行判断,我们可以看到RingBufferTruck中的参数不同的同名方法:

/**
 * Load the truck with a {@link FSWALEntry}.
 */
void load(FSWALEntry entry) {
  this.entry = entry;
  this.type = Type.APPEND;
}

/**
 * Load the truck with a {@link SyncFuture}.
 */
void load(final SyncFuture syncFuture) {
  this.sync = syncFuture;
  this.type = Type.SYNC;
}

根据封装的属性不同,分别执行不同的方法:

  1. 如果封装的是FSWALEntry,会执行append操作,将记录追加到HDFS文件中。
  2. 如果是SyncFuture,则会调用线程池的线程异步的批量刷盘,刷盘成功后唤醒工作线程完成HLog的sunc操作。

相关代码:

public FSHLog(。。。) throws IOException {
  。。。

  this.disruptor = new Disruptor<>(RingBufferTruck::new,
      getPreallocatedEventCount(),
      Threads.newDaemonThreadFactory(hostingThreadName + ".append"),
      ProducerType.MULTI, new BlockingWaitStrategy());

   //获取ringbuffer...
  this.disruptor.getRingBuffer().next();
    
  int syncerCount = conf.getInt(SYNCER_COUNT, DEFAULT_SYNCER_COUNT);
  int maxBatchCount = conf.getInt(MAX_BATCH_COUNT,
      conf.getInt(HConstants.REGION_SERVER_HANDLER_COUNT, DEFAULT_MAX_BATCH_COUNT));
  this.ringBufferEventHandler = new RingBufferEventHandler(syncerCount, maxBatchCount);
  this.disruptor.setDefaultExceptionHandler(new RingBufferExceptionHandler());
  this.disruptor.handleEventsWith(new RingBufferEventHandler[] { this.ringBufferEventHandler });

  this.disruptor.start();
}
(2)随机写入MemStore

未完待续。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值