本文經過更改配置及數據結構改造,快速解決HDFS Decommission緩慢問題。
上篇文章回顧: 記一次Open-Falcon-Graph頻繁OOM問題排查
c3prc-xiami有大量raid單副本文件,decommission單個datanode速度很慢,觀察監控指標,發現:java
網卡流量始終保持低速,60~80mb/snode
磁盤io util也是單個磁盤100%在某一個時刻,也便是同一時間只有一個磁盤在工做算法
這樣下線速度就十分緩慢,一個datanode一天只能下4w個block。而一個datanode平均有20w個block,這個速度明顯不符合要求。apache
最開始認爲是配置問題,咱們分析了幾種配置,有必定的效果,提升到了6w個block天天,但仍是慢。緩存
最後咱們從代碼層面着手,改變相關數據結構,使下線速度明顯提高,一個datanode下線平均1到2天就能下完。bash
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的塊在被複制則不會再選中它做爲源了。oop
(2)HeartbeatManager每次會想dn發送DNA_TRANSFER命令,會從DatanodeDescriptor.replicateBlocks取必定數量的block進行傳輸,而每一個DN可以啓動的DataTransfer線程數最大不能超過maxReplicationStreams。性能
blockmanage.replicationStreamsHardLimit同上一個變量相似,只是在chooseSourceDatanode(),若是block的優先級最高,這個dn還能再多複製2個(默認值分別是2,4),可是不能>replicationStreamsHardLimit。ui
因此在同一時刻最多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被取到,因此仍是要有序的。
實際上,這麼作是合理的,先進入隊列能夠優先被取到。但對於咱們這種場景,並不要求取出的順序性和放入順序一致。若是能打亂順序,再取出就能使一次迭代取出的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正常服務。
本文首發於公衆號「小米雲技術」,點擊閱讀原文。