由於每一個磁盤或者是網絡上的I/O操作可能會對正在讀寫的數據處理不慎而出現錯誤,所以HDFS提供了下面兩種數據檢驗方式,以此來保證數據的完整性,而且這兩種檢驗方式在DataNode節點上是同時工作的:
一.校驗和
檢測損壞數據的常用方法是在第一次進行系統時計算數據的校驗和,在通道傳輸過程中,如果新生成的校驗和不完全匹配原始的校驗和,那麼數據就會被認爲是被損壞的。
二.數據塊檢測程序(DataBlockScanner)
在DataNode節點上開啓一個後臺線程,來定期驗證存儲在它上所有塊,這個是防止物理介質出現損減情況而造成的數據損壞。
關於校驗和,HDFS以透明的方式檢驗所有寫入它的數據,並在默認設置下,會在讀取數據時驗證校驗和。正對數據的每一個校驗塊,都會創建一個單獨的校驗和,默認校驗塊大小是512字節,對應的校驗和是4字節。DataNode節點負載在存儲數據(當然包括數據的校驗和)之前驗證它們收到的數據,如果此DataNode節點檢測到錯誤,客戶端會收到一個CheckSumException。客戶端讀取DataNode節點上的數據時,會驗證校驗和,即將其與DataNode上存儲的校驗和進行比較。每一個DataNode節點都會維護着一個連續的校驗和和驗證日誌,裏面有着每一個Block的最後驗證時間。客戶端成功驗證Block之後,便會告訴DataNode節點,Datanode節點隨之更新日誌。這一點也就涉及到前面說的DataBlockScanner了,所以接下來我將主要討論DataBlockScanner。
還是先來看看與DataBlockScanner相關聯的類吧!
blockInfoSet:數據塊掃描信息集合,按照上一次掃描時間和數據塊id升序排序,以便快速獲取驗證到期的數據塊;
blockMap:數據塊和數據塊掃描信息的映射,以便能夠根據數據塊快速獲取對應的掃描信息;
totalBytesToScan:一個掃描週期中需要掃描的總數據量;
bytesLeft:一個掃描週期中還剩下需要掃描的數據量;
throttler:掃描時I/O速度控制器,需要根據totalBytesToScan和bytesLeft信息來衡量;
scanPeriod:一個掃描週期,可以由Datanode的配置文件來設置,配置項是:dfs.datanode.scan.period.hours,單位是小時,默認的值是21*24*60*60*1000 ms。
DataBlockScanner是作爲DataNode的一個後臺線程工作的,跟着DataNode一塊啓動,它的工作流程如下:
DataBlockScanner被DataNode節點用來檢測它所管理的所有Block數據塊的一致性,因此,對已DataNode節點上的每一個Block,它都會每隔scanPeriod ms利用Block對應的校驗和文件來檢測該Block一次,看看這個Block的數據是否已經損壞。由於scanPeriod 的值一般比較大,因爲對DataNode節點上的每一個Block掃描一遍要消耗不少系統資源,這就可能帶來另外一個問題就是在一個掃描週期內可能會出現DataNode節點重啓的情況,所以爲了提供系統性能,避免DataNode節點在啓動之後對還沒有過期的Block又掃描一遍,DataBlockScanner在其內部使用了日誌記錄器來持久化保存每一個Block上一次掃描的時間,這樣的話, DataNode節點在啓動之後通過日誌文件來恢復之前所有Block的有效時間。另外,DataNode爲了節約系統資源,它對Block的驗證不僅僅只依賴於DataBlockScanner後臺線程(VERIFICATION_SCAN方式),他還會在向某一個客戶端傳送Block的時候來更行該Block的掃描時間(REMOTE_READ方式),這是因爲DataNode向客戶端傳送一個Block的時候要必須校驗該數據塊。那麼這個時候日誌記錄器並不會馬上把該數據塊的掃描信息寫到日誌,畢竟頻繁的磁盤I/O會導致性能下降,至於何時對該Block的最新掃描時間寫日誌有一個判斷條件:
1.如果是VERIFICATION_SCAN方式的Block驗證,必須記日誌;
2.如果是REMOTE_READ方式,那麼該Block上一次的記錄日誌到現在的時間間隔超過24小時或者超過scanPeriod/3
ms 的話,記日誌。
下面來結合源碼詳細討論這個過程:
1.初始化
在整個掃描驗證過程中都一個速度控制器,
- private void init() {
- Block arr[] = dataset.getBlockReport();//從“磁盤”上獲取所有的數據塊基本信息
- Collections.shuffle(Arrays.asList(arr));
- blockInfoSet = new TreeSet<BlockScanInfo>();
- blockMap = new HashMap<Block, BlockScanInfo>();
- long scanTime = -1;
- for (Block block : arr) {
- //爲每一個Block建立掃描驗證信息
- BlockScanInfo info = new BlockScanInfo( block );
- info.lastScanTime = scanTime--;
- addBlockInfo(info);
- }
- /* 尋找一個合適的掃描驗證日誌文件
- */
- File dir = null;
- FSDataset.FSVolume[] volumes = dataset.volumes.volumes;
- for(FSDataset.FSVolume vol : volumes) {
- if (LogFileHandler.isFilePresent(vol.getDir(), verificationLogFile)) {
- dir = vol.getDir();
- break;
- }
- }
- if (dir == null) {
- dir = volumes[0].getDir();
- }
- try {
- // 創建一個日誌記錄器
- verificationLog = new LogFileHandler(dir, verificationLogFile, 100);
- } catch (IOException e) {
- LOG.warn("Could not open verfication log. " + "Verification times are not stored.");
- }
- synchronized (this) {
- //創建一個掃描速度控制器
- throttler = new BlockTransferThrottler(200, MAX_SCAN_RATE);
- }
- }
- private void updateBytesToScan(long len, long lastScanTime) {
- // len could be negative when a block is deleted.
- totalBytesToScan += len;
- //新添加的Block需要在需要在此次中掃描驗證
- if ( lastScanTime < currentPeriodStart ) {
- bytesLeft += len;
- }
- }
- private synchronized void addBlockInfo(BlockScanInfo info) {
- boolean added = blockInfoSet.add(info);
- blockMap.put(info.block, info);
- if ( added ) {
- LogFileHandler log = verificationLog;
- if (log != null) {
- log.setMaxNumLines(blockMap.size() * verficationLogLimit);
- }
- //用新添加的Block掃描信息來更新此次掃描的任務量
- updateBytesToScan(info.block.getNumBytes(), info.lastScanTime);
- }
- }
2.初始化上一次驗證時間
- private synchronized void delBlockInfo(BlockScanInfo info) {
- boolean exists = blockInfoSet.remove(info);
- blockMap.remove(info.block);
- if ( exists ) {
- LogFileHandler log = verificationLog;
- if (log != null) {
- log.setMaxNumLines(blockMap.size() * verficationLogLimit);
- }
- //更新此次掃描驗證的工作量
- updateBytesToScan(-info.block.getNumBytes(), info.lastScanTime);
- }
- }
- private synchronized void updateBlockInfo(LogEntry e) {
- BlockScanInfo info = blockMap.get(new Block(e.blockId, 0, e.genStamp));
- if(info != null && e.verificationTime > 0 && info.lastScanTime < e.verificationTime) {
- delBlockInfo(info);
- info.lastScanTime = e.verificationTime;
- info.lastScanType = ScanType.VERIFICATION_SCAN;
- addBlockInfo(info);
- }
- }
- //爲每一個Block分配上一次驗證的時間
- private boolean assignInitialVerificationTimes() {
- int numBlocks = 1;
- synchronized (this) {
- numBlocks = Math.max(blockMap.size(), 1);
- }
- //讀取數據塊的驗證日誌文件
- LogFileHandler.Reader logReader = null;
- try {
- if (verificationLog != null) {
- logReader = verificationLog.new Reader(false);
- }
- } catch (IOException e) {
- LOG.warn("Could not read previous verification times : " + StringUtils.stringifyException(e));
- }
- if (verificationLog != null) {
- verificationLog.updateCurNumLines();
- }
- try {
- //用日誌信息來更新記錄的Block上一次驗證時間
- while (logReader != null && logReader.hasNext()) {
- if (!datanode.shouldRun || Thread.interrupted()) {
- return false;
- }
- LogEntry entry = LogEntry.parseEntry(logReader.next());
- if (entry != null) {
- updateBlockInfo(entry);
- }
- }
- } finally {
- IOUtils.closeStream(logReader);
- }
- /* 計算Blocks之間驗證的間隔時間
- */
- long verifyInterval = (long) (Math.min( scanPeriod/2.0/numBlocks, 10*60*1000 ));
- long lastScanTime = System.currentTimeMillis() - scanPeriod;
- /* 初始化剩餘Blocks的上一次驗證時間
- */
- synchronized (this) {
- if (blockInfoSet.size() > 0 ) {
- BlockScanInfo info;
- while ((info = blockInfoSet.first()).lastScanTime < 0) {
- delBlockInfo(info);
- info.lastScanTime = lastScanTime;
- lastScanTime += verifyInterval;
- addBlockInfo(info);
- }
- }
- }
- return true;
- }
在一次Blocks掃描驗證週期中,DataBlockScanner需要進行大量的磁盤I/O,爲了不影響DataNode節點上其它線程的工作資源,同時也爲了自身工作的有效性,所以DataBlockScanner採用了掃描驗證速度控制器,根據當前的工作量來控制當前數據塊的驗證速度。
- private synchronized void adjustThrottler() {
- //本次掃描驗證還剩餘的時間
- long timeLeft = currentPeriodStart+scanPeriod - System.currentTimeMillis();
- //根據本次驗證掃描剩餘的工作量和時間來計算速度
- long bw = Math.max(bytesLeft*1000/timeLeft, MIN_SCAN_RATE);
- throttler.setBandwidth(Math.min(bw, MAX_SCAN_RATE));
- }
DataNode節點在向客戶端或者其它DataNode節點傳輸數據時,客戶端或者其它DataNode節點會根據接收的數據校驗和來驗證接收到的數據,當驗證出錯時,它們會通知傳送節點。DataBlockScanner通過自己扮演傳輸者又扮演接受者來實現數據塊的驗證的;同時爲了防止本地磁盤的I/O的錯誤,DataBlockScanner採用了兩次傳輸-接收來確保驗證的Block的數據是出錯了(損壞了)。當發現有出錯的Block是,就需要向NameNode節點報告,由NameNode來決定如何處理這個數據塊,而不是由DataNode節點擅自作主清除該Block數據信息。
- private void verifyBlock(Block block) {
- BlockSender blockSender = null;
- for (int i=0; i<2; i++) {
- boolean second = (i > 0);
- try {
- adjustThrottler();
- blockSender = new BlockSender(block, 0, -1, false, false, true, datanode);
- DataOutputStream out = new DataOutputStream(new IOUtils.NullOutputStream());
- blockSender.sendBlock(out, null, throttler);
- LOG.info((second ? "Second " : "") + "Verification succeeded for " + block);
- if ( second ) {
- totalTransientErrors++;
- }
- updateScanStatus(block, ScanType.VERIFICATION_SCAN, true);
- return;
- } catch (IOException e) {
- totalScanErrors++;
- updateScanStatus(block, ScanType.VERIFICATION_SCAN, false);
- //在“磁盤”上沒有該Block對應的文件
- if ( dataset.getFile(block) == null ) {
- LOG.info("Verification failed for " + block + ". Its ok since " + "it not in datanode dataset anymore.");
- deleteBlock(block);
- return;
- }
- LOG.warn((second ? "Second " : "First ") + "Verification failed for " + block + ". Exception : " + StringUtils.stringifyException(e));
- //兩次驗證都出錯
- if (second) {
- datanode.getMetrics().blockVerificationFailures.inc();
- handleScanFailure(block);
- return;
- }
- } finally {
- IOUtils.closeStream(blockSender);
- datanode.getMetrics().blocksVerified.inc();
- totalScans++;
- totalVerifications++;
- }
- }
- private synchronized void updateScanStatus(Block block, ScanType type, boolean scanOk) {
- BlockScanInfo info = blockMap.get(block);
- if ( info != null ) {
- delBlockInfo(info);
- } else {
- // It might already be removed. Thats ok, it will be caught next time.
- info = new BlockScanInfo(block);
- }
- //更新該Block的驗證信息
- long now = System.currentTimeMillis();
- info.lastScanType = type;
- info.lastScanTime = now;
- info.lastScanOk = scanOk;
- addBlockInfo(info);
- if (type == ScanType.REMOTE_READ) {
- totalVerifications++;
- }
- // Don't update meta data too often in case of REMOTE_READ
- // of if the verification failed.
- long diff = now - info.lastLogTime;
- if (!scanOk || (type == ScanType.REMOTE_READ && diff < scanPeriod/3 && diff < ONE_DAY)) {
- return;
- }
- info.lastLogTime = now;
- LogFileHandler log = verificationLog;
- if (log != null) {
- log.appendLine(LogEntry.newEnry(block, now));//記錄通過驗證的Block驗證信息
- }
- }
- //處理髮生錯誤的Block
- private void handleScanFailure(Block block) {
- try {
- DatanodeInfo[] dnArr = { new DatanodeInfo(datanode.dnRegistration) };
- LocatedBlock[] blocks = { new LocatedBlock(block, dnArr) };
- //向NameNode節點發送出錯的Block
- datanode.namenode.reportBadBlocks(blocks);
- } catch (IOException e){
- /* One common reason is that NameNode could be in safe mode.
- * Should we keep on retrying in that case?
- */
- LOG.warn("Failed to report bad block " + block + " to namenode : " + " Exception : " + StringUtils.stringifyException(e));
- }