2021SC@SDUSC
2021SC@SDUSC
2021SC@SDUSC
Hbase写特性:
Hbase是一个比较少见的写比读快的数据库,原因是在写的过程中,既要写Hlog文件也要将数据写到内存;读的时候需要将所有数据数据文件合起来(包括MemoryStore)最后读出来。因此Hbase进行读操作所需的时间较多。
HBase服务端没有提供update,delete接口,HBase中对数据的更新、删除操作都认为是写入操作,更新操作会写入一个最小版本数据,删除操作写写入一条标记为deleted的KV数据
Hbase写入流程概况:
- 客户端处理阶段
- Region写入阶段
- MemStore Flush阶段
相关源码分析:
2.Region写入阶段
数据写入Region的流程可以抽象为两步:追加写入HLog,随机写入MemStore。
服务器端RegionServer接收到客户端的写入请求后,首先会反序列化为put对象,然后执行各种检查操作,比如检查Region是否是只读、MemStore大小是否超过blockingMemstoreSize等。检查完成之后,执行一系列核心操作:
流程图:
- Acquire locks :获取行锁,HBase中使用行锁保证对同一行数据的更新都是互斥操作,用以保证更新的原子性,要么更新成功,要么更新失败。
- Update LATEST_TIMESTAMP timestamps:更新所有待写入/更新KeyValue的时间戳为当前系统时间。
- Build WAL edit:HBase使用WAL机制保证数据可靠性,即首先写日志再写缓存,即使发生宕机,也可以通过恢复HLog还原出原始数据。
该步骤就是在内存中构建WALEdit对象,为了保证Region级别事务的写入原子性,一次写入操作中所有KeyValue会构建成一条WALEdit记录。 - Append WALEdit To WAL :将步骤3中构造在内存中的WALEdit记录顺序写入HLog中,此时不需要执行sync操作。当前版本的HBase使用了disruptor实现了高效的生产者消费者队列,来实现WAL的追加写入操作。
- Write back to MemStore:写入WAL之后再将数据写入MemStore。
- Release row locks:释放行锁。
- Sync wal:HLog真正sync到HDFS,在释放行锁之后执行sync操作是为了尽量减少持锁时间,提升写性能。如果sync失败,执行回滚操作将MemStore中已经写入的数据移除。
- 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写入都需要经过三个阶段:
- 首先将数据写入本地缓存
- 然后将本地缓存写入文件系统
- 最后执行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;
}
根据封装的属性不同,分别执行不同的方法:
- 如果封装的是FSWALEntry,会执行append操作,将记录追加到HDFS文件中。
- 如果是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
未完待续。。。