前言
在HDFS中,所有的數據都是存在各個DataNode上的.而這些DataNode上的數據都是存放於節點機器上的各個目錄中的,而一般每個目錄 我們會對應到1個獨立的盤,以便我們把機器的存儲空間基本用上.這麼多的節點,這麼多塊盤,HDFS在進行寫操作時如何進行有效的磁盤選擇呢,選擇不當必 然造成寫性能下降,從而影響集羣整體的性能.本文來討論一下目前HDFS中存在的幾個磁盤選擇策略的特點和不足,然後針對其不足,自定義1個新的磁盤選擇 策略.
HDFS現有磁盤選擇策略
上文前言中提到,隨着節點數的擴增,磁盤數也會跟着線性變化,這麼的磁盤,會造成1個問題,數據不均衡現象,這個是最容易發生的.原因可能有下面2個:
1.HDFS寫操作不當導致.
2.新老機器上線使用時間不同,造成新機器數據少,老機器數據多的問題.
第二點這個通過Balancer操作可以解決.第一個問題纔是最根本的,爲了解決磁盤數據空間不均衡的現象,HDFS目前的2套磁盤選擇策略都是圍繞着"數據均衡"的目標設計的.下面介紹這2個磁盤選擇策略.
一.RoundRobinVolumeChoosingPolicy
上面這個比較長的類名稱可以拆成2個單詞,RoundRobin和 VolumeChoosingPolicy,VolumeChoosingPolicy理解爲磁盤選擇策略,RoundRobin這個是一個專業術語,叫 做"輪詢",類似的還有一些別的類似的術語,Round-Robin Scheduling(輪詢調度),Round-Robin 算法等.RoundRobin輪詢的意思用最簡單的方式翻譯就是一個一個的去遍歷,到尾巴了,再從頭開始.下面是一張解釋圖:
下面給出在HDFS中他的核心代碼如下,我加了註釋上去,幫助大家理解:
/** * Choose volumes in round-robin order. */ public class RoundRobinVolumeChoosingPolicy<v extends="" fsvolumespi=""> implements VolumeChoosingPolicy<v> { public static final Log LOG = LogFactory.getLog(RoundRobinVolumeChoosingPolicy.class); private int curVolume = 0; @Override public synchronized V chooseVolume(final List<v> volumes, long blockSize) throws IOException { //如果磁盤數目小於1個,則拋異常 if(volumes.size() < 1) { throw new DiskOutOfSpaceException("No more available volumes"); } //如果由於失敗磁盤導致當前磁盤下標越界了,則將下標置爲0 // since volumes could've been removed because of the failure // make sure we are not out of bounds if(curVolume >= volumes.size()) { curVolume = 0; } //賦值開始下標 int startVolume = curVolume; long maxAvailable = 0; while (true) { //獲取當前所下標所代表的磁盤 final V volume = volumes.get(curVolume); //下標遞增 curVolume = (curVolume + 1) % volumes.size(); //獲取當前選中磁盤的可用剩餘空間 long availableVolumeSize = volume.getAvailable(); //如果可用空間滿足所需要的副本塊大小,則直接返回這塊盤 if (availableVolumeSize > blockSize) { return volume; } //更新最大可用空間值 if (availableVolumeSize > maxAvailable) { maxAvailable = availableVolumeSize; } //如果當前指標又回到了起始下標位置,說明已經遍歷完整個磁盤列 //沒有找到符合可用空間要求的磁盤 if (curVolume == startVolume) { throw new DiskOutOfSpaceException("Out of space: " + "The volume with the most available space (=" + maxAvailable + " B) is less than the block size (=" + blockSize + " B)."); } } } }
理論上來說這種策略是蠻符合數據均衡的目標的,因爲一個個的寫嗎,每塊盤寫入的次數都差不多,不存在哪塊盤多寫少寫的現象,但是唯一的不足之處在於 每次寫入的數據量是無法控制的,可能我某次操作在A盤上寫入了512字節的數據,在輪到B盤寫的時候我寫了128M的數據,數據就不均衡了,所以說輪詢策 略在某種程度上來說是理論上均衡但還不是最好的.更好的是下面這種.
二.AvailableSpaceVolumeChoosingPolicy
剩餘可用空間磁盤選擇策略.這個磁盤選擇策略比第一種設計的就精妙很多了,首選他根據1個閾值,將所有的磁盤分爲了2大類,高可用空間磁盤列表和低 可用空間磁盤列表.然後通過1個隨機數概率,會比較高概率下選擇高剩餘磁盤列表中的塊,然後對這些磁盤列表進行輪詢策略的選擇,下面是相關代碼:
/** * A DN volume choosing policy which takes into account the amount of free * space on each of the available volumes when considering where to assign a * new replica allocation. By default this policy prefers assigning replicas to * those volumes with more available free space, so as to over time balance the * available space of all the volumes within a DN. */ public class AvailableSpaceVolumeChoosingPolicy<v extends="" fsvolumespi=""> implements VolumeChoosingPolicy<v>, Configurable { ... //用於一般的需要平衡磁盤的輪詢磁盤選擇策略 private final VolumeChoosingPolicy<v> roundRobinPolicyBalanced = new RoundRobinVolumeChoosingPolicy<v>(); //用於可用空間高的磁盤的輪詢磁盤選擇策略 private final VolumeChoosingPolicy<v> roundRobinPolicyHighAvailable = new RoundRobinVolumeChoosingPolicy<v>(); //用於可用空間低的剩餘磁盤的輪詢磁盤選擇策略 private final VolumeChoosingPolicy<v> roundRobinPolicyLowAvailable = new RoundRobinVolumeChoosingPolicy<v>(); @Override public synchronized V chooseVolume(List<v> volumes, long replicaSize) throws IOException { if (volumes.size() < 1) { throw new DiskOutOfSpaceException("No more available volumes"); } //獲取所有磁盤包裝列表對象 AvailableSpaceVolumeList volumesWithSpaces = new AvailableSpaceVolumeList(volumes); //如果所有的磁盤在數據平衡閾值之內,則在所有的磁盤塊中直接進行輪詢選擇 if (volumesWithSpaces.areAllVolumesWithinFreeSpaceThreshold()) { // If they're actually not too far out of whack, fall back on pure round // robin. V volume = roundRobinPolicyBalanced.chooseVolume(volumes, replicaSize); if (LOG.isDebugEnabled()) { LOG.debug("All volumes are within the configured free space balance " + "threshold. Selecting " + volume + " for write of block size " + replicaSize); } return volume; } else { V volume = null; // If none of the volumes with low free space have enough space for the // replica, always try to choose a volume with a lot of free space. //如果存在數據不均衡的現象,則從低剩餘空間磁盤塊中選出可用空間最大值 long mostAvailableAmongLowVolumes = volumesWithSpaces .getMostAvailableSpaceAmongVolumesWithLowAvailableSpace(); //得到高可用空間磁盤列表 List<v> highAvailableVolumes = extractVolumesFromPairs( volumesWithSpaces.getVolumesWithHighAvailableSpace()); //得到低可用空間磁盤列表 List<v> lowAvailableVolumes = extractVolumesFromPairs( volumesWithSpaces.getVolumesWithLowAvailableSpace()); float preferencePercentScaler = (highAvailableVolumes.size() * balancedPreferencePercent) + (lowAvailableVolumes.size() * (1 - balancedPreferencePercent)); //計算平衡比值,balancedPreferencePercent越大,highAvailableVolumes.size()所佔的值會變大 //整個比例值也會變大,就會有更高的隨機概率在這個值下 float scaledPreferencePercent = (highAvailableVolumes.size() * balancedPreferencePercent) / preferencePercentScaler; //如果低可用空間磁盤列表中最大的可用空間無法滿足副本大小 //或隨機概率小於比例值,就在高可用空間磁盤中進行輪詢調度選擇 if (mostAvailableAmongLowVolumes < replicaSize || random.nextFloat() < scaledPreferencePercent) { volume = roundRobinPolicyHighAvailable.chooseVolume( highAvailableVolumes, replicaSize); if (LOG.isDebugEnabled()) { LOG.debug("Volumes are imbalanced. Selecting " + volume + " from high available space volumes for write of block size " + replicaSize); } } else { //否則在低磁盤空間列表中選擇磁盤 volume = roundRobinPolicyLowAvailable.chooseVolume( lowAvailableVolumes, replicaSize); if (LOG.isDebugEnabled()) { LOG.debug("Volumes are imbalanced. Selecting " + volume + " from low available space volumes for write of block size " + replicaSize); } } return volume; } }
低剩餘空間磁盤和高剩餘空間磁盤的標準是這樣定義的:
/** * @return the list of volumes with relatively low available space. */ public List getVolumesWithLowAvailableSpace() { long leastAvailable = getLeastAvailableSpace(); List ret = new ArrayList(); for (AvailableSpaceVolumePair volume : volumes) { //可用空間小於最小空間與平衡空間閾值的和的磁盤加入低磁盤空間列表 if (volume.getAvailable() <= leastAvailable + balancedSpaceThreshold) { ret.add(volume); } } return ret; } /** * @return the list of volumes with a lot of available space. */ public List getVolumesWithHighAvailableSpace() { long leastAvailable = getLeastAvailableSpace(); List ret = new ArrayList(); for (AvailableSpaceVolumePair volume : volumes) { //高剩餘空間磁盤選擇條件與上面相反 if (volume.getAvailable() > leastAvailable + balancedSpaceThreshold) { ret.add(volume); } } return ret; }
現有HDFS磁盤選擇策略的不足
OK,我們已經瞭解了HDFS目前存在的2種磁盤選擇策略,我們看看HDFS在使用這些策略的是不是就是完美的呢,答案顯然不是,下面是我總結出的2點不足之處.
1.HDFS的默認磁盤選擇策略是RoundRobinVolumeChoosingPolicy,而不是更優的 AvailableSpaceVolumeChoosingPolicy,我猜測的原因估計是 AvailableSpaceVolumeChoosingPolicy是後來纔有的,但是默認值的選擇沒有改,依然是老的策略.
2.磁盤選擇策略考慮的因素過於單一,磁盤可用空間只是其中1個因素,其實還有別的指標比如這個塊目前的IO情況,如果正在執行許多讀寫操作的時 候,我們當然希望找沒有進行任何操作的磁盤進行數據寫入,否則只會更加影響當前磁盤的寫入速度,這個維度也是下面我自定義的新的磁盤選擇策略的1個根本需 求點.
自定義磁盤選擇策略之ReferenceCountVolumeChoosingPolicy
新的磁盤選擇策略的根本依賴點在於ReferenceCount,引用計數,他能讓你瞭解有多少對象正在操作你,引用計數在很多地方都有用到,比如 jvm中通過引用計數,判斷是否進行垃圾回收.在磁盤相關類FsVolume中也有類似的1個變量,剛好可以滿足我們的需求,如下:
/** * The underlying volume used to store replica. * * It uses the {@link FsDatasetImpl} object for synchronization. */ @InterfaceAudience.Private @VisibleForTesting public class FsVolumeImpl implements FsVolumeSpi { ... private CloseableReferenceCount reference = new CloseableReferenceCount();
然後我們需要將此變量值開放出去,便於我們調用.
@Override public int getReferenceCount() { return this.reference.getReferenceCount(); }
然後模仿AvailableSpaceVolumeChoosingPolicy策略進行選擇,核心代碼如下:
@Override public synchronized V chooseVolume(final List<v> volumes, long blockSize) throws IOException { if (volumes.size() < 1) { throw new DiskOutOfSpaceException("No more available volumes"); } V volume = null; //獲取當前磁盤中被引用次數最少的1塊盤 int minReferenceCount = getMinReferenceCountOfVolumes(volumes); //根據最少引用次數以及引用計數臨界值得到低引用計數磁盤列表 List<v> lowReferencesVolumes = getLowReferencesCountVolume(volumes, minReferenceCount); //根據最少引用次數以及引用計數臨界值得到高引用計數磁盤列表 List<v> highReferencesVolumes = getHighReferencesCountVolume(volumes, minReferenceCount); //判斷低引用磁盤列表中是否存在滿足要求塊大小的磁盤,如果有優選從低磁盤中進行輪詢磁盤的選擇 if (isExistVolumeHasFreeSpaceForBlock(lowReferencesVolumes, blockSize)) { volume = roundRobinPolicyLowReferences.chooseVolume(lowReferencesVolumes, blockSize); } else { //如果低磁盤塊中沒有可用空間的塊,則再從高引用計數的磁盤列表中進行磁盤的選擇 volume = roundRobinPolicyHighReferences.chooseVolume(highReferencesVolumes, blockSize); } return volume; }
附上相應的單元測試,測試已經通過
@Test public void testReferenceCountVolumeChoosingPolicy() throws Exception { @SuppressWarnings("unchecked") final ReferenceCountVolumeChoosingPolicy<fsvolumespi> policy = ReflectionUtils.newInstance(ReferenceCountVolumeChoosingPolicy.class, null); initPolicy(policy); final List<fsvolumespi> volumes = new ArrayList<fsvolumespi>(); // Add two low references count volumes. // First volume, with 1 reference. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(0).getReferenceCount()).thenReturn(1); Mockito.when(volumes.get(0).getAvailable()).thenReturn(100L); // First volume, with 2 references. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(1).getReferenceCount()).thenReturn(2); Mockito.when(volumes.get(1).getAvailable()).thenReturn(100L); // Add two high references count volumes. // First volume, with 4 references. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(2).getReferenceCount()).thenReturn(4); Mockito.when(volumes.get(2).getAvailable()).thenReturn(100L); // First volume, with 5 references. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(3).getReferenceCount()).thenReturn(5); Mockito.when(volumes.get(3).getAvailable()).thenReturn(100L); // initPolicy(policy, 1.0f); Assert.assertEquals(volumes.get(0), policy.chooseVolume(volumes, 50)); volumes.clear(); // Test when the low-references volumes has not enough available space for // block // First volume, with 1 reference. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(0).getReferenceCount()).thenReturn(1); Mockito.when(volumes.get(0).getAvailable()).thenReturn(50L); // First volume, with 2 references. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(1).getReferenceCount()).thenReturn(2); Mockito.when(volumes.get(1).getAvailable()).thenReturn(50L); // Add two high references count volumes. // First volume, with 4 references. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(2).getReferenceCount()).thenReturn(4); Mockito.when(volumes.get(2).getAvailable()).thenReturn(200L); // First volume, with 5 references. volumes.add(Mockito.mock(FsVolumeSpi.class)); Mockito.when(volumes.get(3).getReferenceCount()).thenReturn(5); Mockito.when(volumes.get(3).getAvailable()).thenReturn(200L); Assert.assertEquals(volumes.get(2), policy.chooseVolume(volumes, 100)); }
我在代碼註釋中已經進行了很詳細的分析了,這裏就不多說了.
總結
當然根據引用計數的磁盤選擇策略也不見得是最好的,因爲這裏忽略了磁盤間數據不均衡的問題,顯然這個弊端會慢慢凸顯出來,所以說你很難做到1個策略 是絕對完美的,可能最好的辦法是根據用戶使用場景使用最合適的磁盤選擇策略,或者定期更換策略以此達到最佳的效果.引用計數磁盤選擇策略的相關代碼可以從 我的github patch鏈接中查閱,學習.