前言
在前面的文章HDFS的滾動升級: Rolling Upgrade中,介紹了HDFS滾動升級相關的內容。在HDFS滾動升級的過程中,會涉及到DataNode重啟服務的操作。對於這里的DataNode服務重啟的操作,其實是有一定講究的。比如說,我們批量重啟部分節點的時候,不能同時重啟過多的節點,否則會造成部分塊副本所在節點都處於正在重啟的機器中,導致數據不可用的情況發生。如果我們想依然保證集群數據的可用性,可能想到的最簡單的辦法就是逐個重啟。但是這又會有一個問題:如果集群規模達到1w個節點,假設集群每個DataNode重啟服務大約花2~3分鍾,那么重啟一次集群估計要花1周的時間了。所以本文將要關注的一個主題是我們如何保證集群重啟服務時同時保證高效性與數據可用性。對於此問題,HDFS引入了Upgrade Domain(升級域)的概念。在下文中,將會對此功能進行詳細的介紹,包括它的概念的定義,核心原理以及部分關鍵代碼的實現。
HDFS Upgrade Domain概念
HDFS Upgrade Domain本質上來理解指的是HDFS對各個同一個塊的各個副本進行了邏輯上的划分,使之位於不同的域下。然后進行升級操作的時候,按照各個域依次進行重啟服務即可。因為塊的各個副本分別位於不同的域下,就不會存在數據不可用的情況了。升級域對副本塊的邏輯划分效果如圖1-1所示。

圖 1-1 HDFS升級域
從圖1-1中可以看出,升級域與機架類似,都是將DataNode進行划分的一個維度,唯一的不同點,是機架位置信息是物理上的,而升級域則表現在邏輯空間上的。DataNode所在機架我們可以用機架名稱來表示,那么升級域我們用什么名稱表示呢,答案如下:
HDFS節點的升級域可以采用指定升級域的方式,否則用DataNodeId作為當前DataNode的升級域(在這種情況下,每個DataNode的升級域都不相同)。
指定升級域的方式,可以寫相應腳本進行獲取,與機架感知時的獲取腳本完全類似。節點->機架位置->升級域關系映射如圖1-2所示。

圖 1-2 DataNode節點映射關系
HDFS Upgrade Domain核心實現點
HDFS升級域的引入對於現有HDFS最為核心的影響是副本塊的放置選擇。因此在此功能中,我們將會引入一種新的副本放置策略:BlockPlacementPolicyWithUpgradeDomain。在此放置策略類中,我們將會多考慮到升級域的影響,保證塊的各個副本位於不同的升級域下。
除了以上這個核心需要點外,我們還需要實現周邊相關的操作。
第一點,每個塊放置策略類會有自身的待刪除多余副本的選擇邏輯,同樣在BlockPlacementPolicyWithUpgradeDomain類中,我們也需要對此進行實現。
第二點,Balancer數據平衡工具在移動塊的時候,依然要遵循當前放置策略的原則。目前Balancer在數據平衡時保持的一個原則是保證各個機架內block的數量不變。這個原則表明了它更多的使用場合在於同一機架內的數據平衡。
HDFS Upgrade Domain關鍵代碼實現
根據上節提到的關鍵實現點,在本節中我們對應核心代碼的分析。首先,這里的主要邏輯都是實現在新的放置策略類BlockPlacementPolicyWithUpgradeDomain中的。在這里,我們要重載isGoodDatanode和pickupReplicaSet方法。
升級域放置策略下的放置位置選擇
首先是isGoodDatanode方法,為了沿用原本存儲節點的選擇策略,我們只需額外添加升級域的篩選判斷即可,代碼如下。
protected boolean isGoodDatanode(DatanodeDescriptor node,
int maxTargetPerRack, boolean considerLoad,
List<DatanodeStorageInfo> results, boolean avoidStaleNodes) {
// 沿用父邏輯來判斷當前選擇的節點是否為一個好的目標節點
boolean isGoodTarget = super.isGoodDatanode(node,
maxTargetPerRack, considerLoad, results, avoidStaleNodes);
// 如果滿足原始邏輯,繼續判斷升級域的條件,保證當前選擇的各個副本的目標存儲位置位於不同的升級域下
if (isGoodTarget) {
// 如果當前已選擇完畢的節點位置大於0並且小於升級域個數時
if (results.size() > 0 && results.size() < upgradeDomainFactor) {
// 取出當前升級域的去重集合
Set<String> upgradeDomains = getUpgradeDomains(results);
// 如果當前選擇的節點已經包含在這個集合中,表明是重復的升級域,則此節點不是一個適合的節點,設置為false
if (upgradeDomains.contains(node.getUpgradeDomain())) {
isGoodTarget = false;
}
}
}
return isGoodTarget;
}
這里的upgradeDomainFactor變量時可配置的,默認值為副本系數,也就是說理論情況下,各個副本有對應各自的升級域。
升級域放置策略下的多余副本塊的選擇
放置策略下的多余副本塊的選擇過程可以詳細閱讀本人之前的一篇文章:HDFS如何檢測並刪除多余副本塊。在這篇文章中,有詳細的多余副本塊的選出以及刪除邏輯。而在本小節,我們將主要關注升級域放置策略下的多余副本快的選出邏輯這塊,代碼如下:
protected Collection<DatanodeStorageInfo> pickupReplicaSet(
Collection<DatanodeStorageInfo> moreThanOne,
Collection<DatanodeStorageInfo> exactlyOne,
Map<String, List<DatanodeStorageInfo>> rackMap) {
// 首先將所有多余的副本位置信息合並
Collection<DatanodeStorageInfo> all = combine(moreThanOne, exactlyOne);
// 在里面選出屬於同個升級域的位置信息
List<DatanodeStorageInfo> shareUDSet = getShareUDSet(
getUpgradeDomainMap(all));
// 新建同升級域、同機架的節點位置信息列表
List<DatanodeStorageInfo> shareRackAndUDSet = new ArrayList<>();
// 如果同升級域的列表為空,代表以上節點位置都為獨立的升級域,則退一步,沿用父邏輯
if (shareUDSet.size() == 0) {
return super.pickupReplicaSet(moreThanOne, exactlyOne, rackMap);
} else if (moreThanOne != null) {
// 否則遍歷共享升級域的列表,用moreThanOne來判斷是否此升級域
for (DatanodeStorageInfo storage : shareUDSet) {
// 如果包含,則表明此節點位置既屬於同升級域,又屬於同一機架下,
// 因為moreThanOne已經代表的意思是當前機架下的副本數大於1個的位置集合
if (moreThanOne.contains(storage)) {
shareRackAndUDSet.add(storage);
}
}
}
// 如果同機架,同升級域對象不為空,則返回此對象,否則返回同升級域的集合
return (shareRackAndUDSet.size() > 0) ? shareRackAndUDSet : shareUDSet;
}
歸納一下上面的選出過程:
1)首先選出屬於同個升級域的位置信息。
2)如果同升級域的列表信息為空,代表所有節點位置都為獨立的升級域,則退一步,沿用父類的選出邏輯。
3)否則在此條件下選出同機架的節點信息。
4)如果同機架,同升級域對象不為空,則返回此對象,否則返回同升級域的集合。
從上面的選出邏輯中我們可以看出,它的一個核心原則是盡可能地讓具有高重復屬性的副本塊優先移除。然后這些副本塊選出來了之后,會按照心跳更新時間,磁盤剩余空間再進行刪除順序的選擇。
Balancer過程的局部改造
Blancer過程的局部改造在於移動數據塊時的放置策略的判斷。原始邏輯如下,是一個寫死的的判斷原則:
private boolean isGoodBlockCandidate(StorageGroup source, StorageGroup target,
StorageType targetStorageType, DBlock block) {
...
// 原始邏輯固定地按照機架屬性來判斷
if (cluster.isNodeGroupAware()
&& isOnSameNodeGroupWithReplicas(source, target, block)) {
return false;
}
if (reduceNumOfRacks(source, target, block)) {
return false;
}
return true;
}
現在優化為以下邏輯:
private boolean isGoodBlockCandidate(StorageGroup source, StorageGroup target,
StorageType targetStorageType, DBlock block) {
...
// 原邏輯此處被更改為按照當前放置策略的邏輯來判斷
if (!isGoodBlockCandidateForPlacementPolicy(source, target, block)) {
return false;
}
return true;
}
isGoodBlockCandidateForPlacementPolicy方法如下:
private boolean isGoodBlockCandidateForPlacementPolicy(StorageGroup source,
StorageGroup target, DBlock block) {
// 新建空副本位置信息列表
List<DatanodeInfo> datanodeInfos = new ArrayList<>();
synchronized (block) {
// 遍歷當前塊的副本存儲位置信息,加入到位置信息列表中
for (StorageGroup loc : block.locations) {
datanodeInfos.add(loc.getDatanodeInfo());
}
datanodeInfos.add(target.getDatanodeInfo());
}
// 將當前塊的副本位置信息傳入放置策略類中,進行是否可移動的判斷
return placementPolicies.getPolicy(false).isMovable(
datanodeInfos, source.getDatanodeInfo(), target.getDatanodeInfo());
}
當然上述代碼的改動,需要BlockPlacementPolicy底層類新加一個isMovable方法,然后其實現子類都需實現整個方法。在此本人進行了省略,讀者可自行閱讀相關放置策略類的源碼進行進一步的學習。
BlockPlacementPolicyWithUpgradeDomain策略類的使用
升級域策略類的使用需要通過配置下面的配置項進行開啟。
<property>
<name>dfs.block.replicator.classname</name>
<value>org.apache.hadoop.hdfs.server.blockmanagement.BlockPlacementPolicyWithUpgradeDomain</value>
<description>
Class representing block placement policy for non-striped files.
</description>
</property>
那么現在有個問題來了,原本使用的是默認的放置策略類了,現在使用此策略類能達到效果嗎?沒錯,這里的確會有這樣的一個問題,官方設計文檔提出的解決辦法如下:
1)寫好腳本,將集群中的DataNode都指定好對應的升級域中。
2)啟用升級域策略類,此時新創建的塊將會滿足此策略類的放置規則。
3)老的塊通過HDFS的mover工具進行再同步,期間會進行副本塊的遷移。
4)如果上述過程中出現問題了,可以使用默認副本策略進行退回。
升級域策略類啟用完畢之后,我們在下一次的升級操作或是集群服務重啟的時候就可以做到按照升級域來做。這樣的話,不管集群是1000個節點,還是10000個節點,都將不會導致特別耗時的影響,而不是必須按照一個個節點逐個重啟的方式。此功能屬性目前暫未發布,預計發布版本2.9,3.0,詳見相關JIRAHDFS-7541(Upgrade Domains in HDFS)。