HDFS Decommission問題分析

小米運維發表於2019-05-30

本文透過更改配置及資料結構改造,快速解決HDFS Decommission緩慢問題。
背景

c3prc-xiami有大量raid單副本檔案,decommission單個datanode速度很慢,觀察監控指標,發現:

  • 網路卡流量始終保持低速,60~80mb/s

  • 磁碟io util也是單個磁碟100%在某一個時刻,也即是同一時間只有一個磁碟在工作

這樣下線速度就十分緩慢,一個datanode一天只能下4w個block。而一個datanode平均有20w個block,這個速度明顯不符合要求。

最開始認為是配置問題,我們分析了幾種配置,有一定的效果,提高到了6w個block每天,但還是慢。

最後我們從程式碼層面著手,改變相關資料結構,使下線速度明顯提升,一個datanode下線平均1到2天就能下完。

分析配置

public static final String DFS_NAMENODE_REPLICATION_MAX_STREAMS_KEY = "dfs.namenode.replication.max-streams";public static final int DFS_NAMENODE_REPLICATION_MAX_STREAMS_DEFAULT = 2;public static final String  DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_KEY = "dfs.namenode.replication.max-streams-hard-limit";public static final int DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_DEFAULT = 4;

blockmanager.maxReplicationStreams這個變數有兩個作用:

(1)在選擇從哪裡把塊複製出去的時候chooseSourceDatanode(),如果某個dn上已經有>maxReplicationStreams的塊在被複制則不會再選中它作為源了。

(2)HeartbeatManager每次會想dn傳送DNA_TRANSFER命令,會從DatanodeDescriptor.replicateBlocks取一定數量的block進行傳輸,而每個DN能夠啟動的DataTransfer執行緒數最大不能超過maxReplicationStreams。

blockmanage.replicationStreamsHardLimit同上一個變數類似,只是在chooseSourceDatanode(),如果block的優先順序最高,這個dn還能再多複製2個(預設值分別是2,4),但是不能>replicationStreamsHardLimit。

所以在同一時刻最多replicationStreamsHardLimit被選出,而且是在同一個dn中。但是單純調整hardLimit並沒有多少效果。

真正控制一個dn往外複製資料還是maxReplicationStreams,dn透過心跳向NN報告正在進行Transfer的執行緒數,而後NN向dn傳送maxTransfer個DNA_transfer CMD:

//get datanode commandsfinal int maxTransfer = blockManager.getMaxReplicationStreams()
- xmitsInProgress;

xmitsInProgress=正在傳輸數量

對於每個dn:從待複製blocks佇列取出maxTransfers

public List<BlockTargetPair> getReplicationCommand(int maxTransfers) {  
return replicateBlocks.poll(maxTransfers);
}

結論1:

透過調大上述2個引數,從2到4,再調整到8,效果還是比較明顯,dn中的日誌也反映出同一個時刻傳輸執行緒數有所增加。

但當調整為12或者更大時,就沒有多少效果了。整體網速也沒有上來。

目前調整為12是比較合理的。和單個dn磁碟數量對應。

publicstatic final String DFS_NAMENODE_REPLICATION_MAX_STREAMS_KEY ="dfs.namenode.replication.max-streams";
publicstatic final int DFS_NAMENODE_REPLICATION_MAX_STREAMS_DEFAULT = 2;
publicstatic final String DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_KEY ="dfs.namenode.replication.max-streams-hard-limit";
publicstatic final int DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_DEFAULT = 4;

namenode中的blockmanager. ReplicationMonitor每3秒會computeDatanodeWork並且取一批block,然後告訴dn去複製這些blocks取的數量:

final int blocksToProcess = numlive
* this.blocksReplWorkMultiplier;

開始我認為調大能加快下線速度,但這個引數影響小,它的作用僅僅是把取出來的block放入DatanodeDescriptor的等待佇列中(replicateBlocks)

同過觀察NN日誌發現如下問題:

  • ReplicationMonitor每次迭代會列印日誌:

askdn_ip to replicate blk_xxx to another_dn.

並且數量為blocksToProcess。在同一時刻,這個dn_ip都是一樣的。

  • 每次迭代(在一段時間內迴圈)多次要求同一個dn複製blocksToProcess(c3prc-xiaomi=800個)blocks,而dn每次最多複製maxReplicationStreams

  • 也就是說NN做了很多無效工作,取的blocks都是同一個dn,當取到輪到下一個dn時,又是同樣的問題。並不是每個dn都在同時工作。觀察監控發現dn間歇性往外複製資料。

ReplicationMonitor每次取blocksToProcess個blocks的時候,這些blocks可能是同一個dn上,甚至同一個dn的同一個磁碟上。

因此,要分析每次的取法。目的是能取出不連續的blocks,能讓不同dn,不同磁碟同時工作。

//
分析資料結構
//

和下線主要有關的程式碼集中在blockmanager,以及UnderReplicatedBlocks

/**
* Store set of Blocks that need to be replicated 1 or more times.
* We also store pending replication-orders.
*/
public final UnderReplicatedBlocks neededReplications = new UnderReplicatedBlocks();

所有需要做replicate的block都會放在blockmanager.neededReplications中。

UnderReplicatedBlocks是一個複合結構,儲存者5個(LEVEL=5)帶有優先順序的佇列:

private final List<LightWeightHashSet<Block>> priorityQueues

對於只有一個副本的block,或者replica都在decommision節點上,它在優先順序最高的佇列中,raid副本下線就是這種情況。

對於每一個優先順序的佇列,實現是LightWeightLinkedSet,它是一個有序的hashset,元素收尾相連。

UnderReplicatedBlocks的實現是保證每個佇列的元素都會被取到,同時,每個佇列中的元素按順序依次被取出,不會讓某些block永遠沒機會被取出。

具體做法是為每一個佇列儲存一個偏移:

private finalList<LightWeightHashSet<Block>> priorityQueues

如果取到最後一個佇列(LEVEL-1)末尾了,就重置所有佇列的偏移=0,從頭再取。

這5個佇列都是先進先被選,隊尾進,而且優先順序Level=0的更容易被取到。具體取的演算法是在UnderReplicatedBlocks.chooseUnderReplicatedBlocks()中。

在進行decommission操作的時候,可能整個dn的塊都是要加入neededReplication佇列(raid叢集如此,如果有3副本,那一個block有3個source.單副本的source dn只有一個)。這時候,加入某一個優先順序佇列(LightWeightLinkedSet)的blocks是有序的,而且連續上w個blocks屬於同一個dn,甚至連續在同一個磁碟上。由於從佇列是從頭到尾順序取,所以會有問題,尤其是對單副本的情況。

因此,我們想要隨機從優先順序佇列中取出block.但又要保證每個block被取到,所以還是要有序的。

//
資料結構改造-ShuffleAddSet
//

實際上,這麼做是合理的,先進入佇列可以優先被取到。但對於我們這種場景,並不要求取出的順序性和放入順序一致。如果能打亂順序,再取出就能使一次迭代取出的blocks儘可能在不同dn或者不同磁碟上。

LightWeightLinkedSet:UnderReplicatedBlocks預設採用的優先順序佇列的實現。本身是一個hashset繼承LightWeightHashSet,同時元素雙向連線,帶有Head tail

LightWeightHashSet:輕量級hashSet

ShuffleAddSet:Look like LightWeightLinkedSet

我們實現了一種ShuffleAddSet繼承LightWeightHashSet,儘可能表現和LightWeightLinkedSet一致,這樣對外UnderReplicatedBlocks不需要做過多修改。

ShuffleAddSet中有兩個佇列,而對外表現為一個優先順序佇列。

第一個佇列,同時也是Set和LightWeightLinkedSet是一樣的,雙向有序,外部呼叫方法取元素也是從這個Set取。

第二個佇列,快取佇列cachedAddList,一開始用ArrayList、LinkedList,由於效能問題不使用了。現在也是用LightWeightHashSet,HashSet具有天然無序性質。

每當有新的元素加入,首先會放入cachedAddList中,隨後當第一個佇列資料空或者取到末尾,立即將cachedAddList資料shuffle,並複製到第一個佇列中,然後清空自己,繼續接收新元素。由於HashSet本身無序,因此少一步shuffle操作,直接從cachedAddList複製至第一個佇列即可。

需要注意的是取資料(呼叫迭代器取)和add操作必須是同步的,因為取的時候第一個佇列到達末尾或著空,會觸發shuffle and add操作,清空cachedlist。

綜上,第一個佇列只有為空或者取到末尾的時候,會從第二個隊里加入資料,如果都為空說明整個優先順序佇列空。每從cachedlist加入一批,這一批就是隨機順序,雖然第一個佇列不是整個佇列都隨機打亂,總體上,第一個佇列還是是亂序的。

這樣做的問題:

(1)外部呼叫這個優先順序佇列的add操作,先進入佇列的不一定是先被排程,後加入cachedList的元素,也可能排在第一個佇列的前面先排程。

(2)極端情況,如果每次加入1個,然後再取1個元素,少量的元素,或者取出數量和頻率遠大於add數量,(比如cachedlist加入一個元素,第一個佇列立刻到達末尾了)實際上沒有達到隨機效果。

好在我們的場景不要求FIFO,而且每次Decommision初始加入UnderReplicatedBlocks的block數量很大,ReplicationMonitor每次取/處理的數量blockToProcess,相對而言(下線8臺節點,UnderReplicatedBlocks會達到180w)較小。發生shuffle and add不是很頻繁,也不是效能瓶頸。觀測到最長時間是200ms。

同時,我們將這個功能ShuffleAddSet作為一種可配置專案,UnderReplicatedBlocks可以在初始化時候選擇用ShuffleAddSet或者LightWeightLinkedSet

dfs.namenode.blockmanagement.queues.shuffle

如下:

/** the queues themselves */private final List<LightWeightHashSet<Block>> priorityQueues
= new ArrayList<LightWeightHashSet<Block>>();public static final String NAMENODE_BLOCKMANAGEMENT_QUEUES_SHUFFLE =    "dfs.namenode.blockmanagement.queues.shuffle";/** Stores the replication index for each priority */private Map<Integer, Integer> priorityToReplIdx = new HashMap<Integer, Integer>(LEVEL);/** Create an object. */UnderReplicatedBlocks() {
 Configuration conf = new HdfsConfiguration(); boolean useShuffle = conf.getBoolean(NAMENODE_BLOCKMANAGEMENT_QUEUES_SHUFFLE, false); for (int i = 0; i < LEVEL; i++) {    if (useShuffle) {
     priorityQueues.add(new ShuffleAddSet<Block>());
} else {
     priorityQueues.add(new LightWeightLinkedSet<Block>());
}
   priorityToReplIdx.put(i, 0);
}
}
//
效能問題
//

我們發現使用ShuffleAddSet時候,開始下線8臺dn時會卡住,主要是卡在DecommissionManager$Monitor,每次要檢查此dn上全部的blocks是否 underReplicated,這樣blocks很多拿寫鎖的時間會很長。

也會檢查ShuffleAddSet.contains(blocks),由於有兩個佇列,所以contain開銷會比之前大。

2019-04-12,12:09:35,876 INFO org.apache.hadoop.hdfs.server.namenode.FSNamesystem: Long read lock is held at 1555042175235. And released after 641 milliseconds.Call stack is:
java.lang.Thread.getStackTrace(Thread.java:1479)
org.apache.hadoop.util.StringUtils.getStackTrace(StringUtils.java:914)
org.apache.hadoop.hdfs.server.namenode.FSNamesystemLock.checkAndLogLongReadLockDuration(FSNamesystemLock.java:104)
org.apache.hadoop.hdfs.server.namenode.FSNamesystem.writeUnlock(FSNamesystem.java:1492)
org.apache.hadoop.hdfs.server.blockmanagement.BlockManager.isReplicationInProgress(BlockManager.java:3322)
org.apache.hadoop.hdfs.server.blockmanagement.DatanodeManager.checkDecommissionState(DatanodeManager.java:751)
org.apache.hadoop.hdfs.server.blockmanagement.DecommissionManager$Monitor.check(DecommissionManager.java:93)
org.apache.hadoop.hdfs.server.blockmanagement.DecommissionManager$Monitor.run(DecommissionManager.java:70)
java.lang.Thread.run(Thread.java:662)

透過修改isReplicationInProgress方法,類似處理blockreport,每隔一定數量放一次鎖的方式,緩解寫鎖時間太長導致其他rpc請求沒有響應。

++processed;// Release lock per 5w blocks processed and has too many underReplicatedBlocks.if (processed == numReportBlocksPerIteration &&
   namesystem.hasWriteLock() && underReplicatedBlocks > numReportBlocksPerIteration) {
 namesystem.writeUnlock();
processed = 0;
namesystem.writeLock();
}
//
結論
//

對於單副本較多的叢集,可採用如下方式下線:

dfs.namenode.blockmanagement.queues.shuffle= truedfs.namenode.replication.max-streams= 12 預設是2,限制一個datanode複製數量
dfs.namenode.replication.max-streams-hard-limit=12 預設是4dfs.namenode.replication.work.multiplier.per.iteration= 4 預設2 / namenode一次排程的數量=該值×datanodes數量

開啟shuffle and add,並調整單個dn最大複製數為物理磁碟數量,對於小叢集可以調大work.multiplier一次處理4倍LiveDatanode數量block.使下線速度最大化。

注意的問題:

每次操作下線2臺節點(refreshNodes),隔10分鐘再下2臺,一直到8臺。同時下線的dn最好不要超過8臺。不然DecommissionManager的開銷會很大,影響NN正常服務。


HDFS Decommission問題分析

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559359/viewspace-2646250/,如需轉載,請註明出處,否則將追究法律責任。

相關文章