HDFS全量块汇报(FBR)的限流机制
前言
众所周知在HDFS中,DataNode会进行定期的全量块汇报操作到NameNode中,来向NameNode表明其上所存储的所有块数据。这个动作在HDFS中称之为Full Block Report,简称FBR。FBR其实是一个十分expensive的操作,尤其当DataNode存储有大量block块的时候。又因为HDFS FSN全局单一锁的设计,当NameNode同时需要处理来自其下多个DataNode的FBR的时候,就可能陷入overload的情况。进而表现在用户应用层面,就是用户请求响应延时等等。本文笔者来聊聊HDFS的全量块汇报问题,以及目前社区对此的优化方案。
HDFS全量块汇报(FBR)的性能问题
如上文所提到的,由于HDFS内部的全局单一锁的设计,当NameNode对系统元数据做修改操作时,其它所有别的操作都将被block住。因此假设每个DN的FBR都包含有大量块信息的时候,NameNode将会花费很大的开销来处理这些汇报的块。
Hadoop社区在很早的时候就发现了这个问题,它提出的一个改进思路是:既然一整个大的FBR行为会造成很大的性能问题,那么我们是否可以将它拆分成多个小部分的块,并且分多次RPC进行发送呢?基于这个思路,社区在HDFS-5153: Datanode should send block reports for each storage in a separate message.实现了基于每个Storage的块汇报实现。在此优化下,当DataNode发现自身全量块汇报的总数大于阈值块汇报数(默认为100w)时,会将块按照每个Storage存储目录进行汇报,这样一个大的FBR RPC就变为了多次小的RPC行为。这样的话,远端NameNode处理DataNode的FBR压力会小许多。,相关逻辑代码如下:
// 当全量块汇报数小于阈值块时,进行一次性汇报行为
if (totalBlockCount < dnConf.blockReportSplitThreshold) {
// Below split threshold, send all reports in a single message.
DatanodeCommand cmd = bpNamenode.blockReport(
bpRegistration, bpos.getBlockPoolId(), reports,
new BlockReportContext(1, 0, reportId, fullBrLeaseId, true));
blockReportSizes.add(
calculateBlockReportPBSize(useBlocksBuffer, reports));
numRPCs = 1;
numReportsSent = reports.length;
if (cmd != null) {
cmds.add(cmd);
}
} else {
// 否则按照Storage,进行多次RPC汇报
// Send one block report per message.
for (int r = 0; r < reports.length; r++) {
StorageBlockReport singleReport[] = { reports[r] };
DatanodeCommand cmd = bpNamenode.blockReport(
bpRegistration, bpos.getBlockPoolId(), singleReport,
new BlockReportContext(reports.length, r, reportId,
fullBrLeaseId, true));
NameNode的FBR限流机制
上面提到的HDFS-5153改进虽然在一定程度上的确是优化了HDFS的FBR处理,但是随着当今存储技术的进步,单块磁盘能够存储的数据量也在不断提升。进一步地来说,DataNode将FBR拆分出的小的Storage Report也可能存在包含有大量块信息的情况。面对Storage存储密度不断上升的情况,HDFS-5153并没有解决掉本质问题。
那么这里我们有什么别的优化方向呢?我们是否能在NameNode端做特殊处理,能够使得它避免长时间忙碌于FBR的处理之中呢?为此,社区在block report中引入了租约的概念来控制DataNode的全量块汇报行为,简称BR Lease。在BR Lease机制下,只有那些获得了NameNode所授予的BR Lease的DataNode节点,才能进行FBR行为。有了这层控制,NameNode就能够减缓底层大量DataNode所带来的FBR操作压力了。
用一个轻松、简单的对话来模拟BR Lease下的FBR行为:
DN: Hi NN,我现在能进行全量块汇报行为吗?
NN:我现在太忙了,不能让你汇报。
过了一会儿,DN又来问了:
DN: Hi NN,我现在可以进行全量块汇报行为吗?
NN:现在不忙了,可以汇报了,我来分配给你一个租约。
DN拿到了NN所授予的租约,NN验证了DN租约的有效性后,然后进行了FBR处理。
其实从中我们可以看到,BR Lease在这里变相起到了Rate Limit的作用,至于这个Rate到底控制在什么值,可以根据具体场景进行具体设置。
BR Lease的管理控制
社区在HDFS-7923: The DataNodes should rate-limit their full block reports by asking the NN on heartbeat messages中实现了基于BR Lease的FBR的限流控制。在此实现内,新增了一个专门管理BR Lease的管理类BlockReportLeaseManager。BlockReportLeaseManager负责有以下2类功能:
- 分配DataNode租约Id
- 处理块汇报前验证DataNode提供的租约Id是否有效
BlockReportLeaseManager对DataNode的BR Lease做了额外两项的限制:
- 当前最多允许的Lease分配数,进而限制DataNode的FBR上报数,DataNode只有拿到Lease Id才能进行下一步的FBR。
- 每个Lease有其过期时间,过期时间设置是为了限制Lease的有效使用时间范围,借此避免DataNode长时间占用Lease。
基于BR Lease的FBR限流逻辑
下面我们通过具体代码来展示基于BR Lease的FBR限流逻辑。
首先是DataNode端,相关操作方法BPServiceActor#offerServic
// 1. 判断是否需要发送请求BR Lease
boolean requestBlockReportLease = (fullBlockReportLeaseId == 0) &&
scheduler.isBlockReportDue(startTime);
if (!dn.areHeartbeatsDisabledForTests()) {
// 2. 发送心跳给NN,获取BR Lease信息
resp = sendHeartBeat(requestBlockReportLease);
assert resp != null;
if (resp.getFullBlockReportLeaseId() != 0) {
if (fullBlockReportLeaseId != 0) {
...
fullBlockReportLeaseId = resp.getFullBlockReportLeaseId();
}
..
// 3. 如果获取到的Lease Id不为0,则进行FBR操作(顺带带上LeaseId)
if ((fullBlockReportLeaseId != 0) || forceFullBr) {
cmds = blockReport(fullBlockReportLeaseId);
// 重置FBR的Lease Id, 为了让DN下次重新申请Lease Id
fullBlockReportLeaseId = 0;
}
然后我们紧接着来看服务端的逻辑方法,首先是FSNamesystem.handleHeartbeat对于request FBR Lease的处理,相关方法FSNamesystemhandleHeartbeat
HeartbeatResponse handleHeartbeat(DatanodeRegistration nodeReg,
StorageReport[] reports, long cacheCapacity, long cacheUsed,
int xceiverCount, int xmitsInProgress, int failedVolumes,
VolumeFailureSummary volumeFailureSummary,
boolean requestFullBlockReportLease,
@Nonnull SlowPeerReports slowPeers,
@Nonnull SlowDiskReports slowDisks)
throws IOException {
readLock();
try {
//get datanode commands
...
long blockReportLeaseId = 0;
// 从BlockManage中获取租约
if (requestFullBlockReportLease) {
blockReportLeaseId = blockManager.requestBlockReportLeaseId(nodeReg);
}
//...
return new HeartbeatResponse(cmds, haState, rollingUpgradeInfo,
blockReportLeaseId);
} finally {
readUnlock("handleHeartbeat");
}
}
我们进入BlockManager的requestBlockReportLeaseId方法,在里面分配BR LeaseId时就有着限流逻辑,通过当前可允许分配的最大Lease数来做,相关方法BlockReportLeaseManager#requestLease
public synchronized long requestLease(DatanodeDescriptor dn) {
NodeData node = nodes.get(dn.getDatanodeUuid());
...
// 1. 在分配新的Lease之前先移除过期的Lease
pruneExpiredPending(monotonicNowMs);
// 2. 如果当前的有效Lease超过最大可允许值,返回0代表请求Lease Id失败
if (numPending >= maxPending) {
if (LOG.isDebugEnabled()) {
StringBuilder allLeases = new StringBuilder();
String prefix = "";
for (NodeData cur = pendingHead.next; cur != pendingHead;
cur = cur.next) {
allLeases.append(prefix).append(cur.datanodeUuid);
prefix = ", ";
}
LOG.debug("Can't create a new BR lease for DN {}, because " +
"numPending equals maxPending at {}. Current leases: {}",
dn.getDatanodeUuid(), numPending, allLeases.toString());
}
return 0;
}
...
}
接着是BlockManager对于DataNode上报的FBR的处理,相关方法NameNodeRpcServer#blockReport
public DatanodeCommand blockReport(final DatanodeRegistration nodeReg,
String poolId, final StorageBlockReport[] reports,
final BlockReportContext context) throws IOException {
checkNNStartup();
...
try {
// 1. 检查FBR租约的有效性,主要包含两方面的检查
// 1)租约Id是否为当前已分配给此DN的Lease Id值
// 2)租约Id是否已过期
if (bm.checkBlockReportLease(context, nodeReg)) {
for (int r = 0; r < reports.length; r++) {
final BlockListAsLongs blocks = reports[r].getBlocks();
...
final int index = r;
// FBR租约验证通过,BlockManager进行FBR的处理
noStaleStorages = bm.runBlockOp(() ->
bm.processReport(nodeReg, reports[index].getStorage(),
blocks, context));
}
...
附上上述逻辑的流程图:
以上就是基于Lease的FBR限流控制原理,但是回过头来再细看这个方案,它依然还不是最完美的。因为DataNode的FBR在本质上依然没有被彻底改造,可能在未来更好的做法将FBR行为进行分段拆分,然后NN再要求DN汇报这些分段的block report,然后进行处理。这里笔者想表达一个核心point是FBR的行为应该由NN这边来控制,它来要求DN怎么去发,而不是目前单方面的由DN一股脑地将自身的全量块汇报给NN。
引用
[1]. https://issues.apache.org/jira/browse/HDFS-5153
[2]. https://issues.apache.org/jira/browse/HDFS-7923