Hadoop運行原理總結(詳細)


  本編隨筆是小編參照個人的筆記、官方文檔以及網上的資料等后對HDFS的概念以及運行原理進行系統性地歸納,說起來真的慚愧呀,自學了很長一段時間也沒有對Hadoop知識點進行歸納,有時候在實戰中或者與別人交流Hadoop相關技術時,很多概念也只是模模糊糊記得,並非很熟練。哈哈哈,趁着本科中最后一個暑假,把自己這兩年自學的大數據開發技術都系統性歸納,免得以后自己忘記了,順便分享到自己的博客上,也給初學者等有需要的人參考。

  寫博客不易,如果文章有錯誤,請指出,覺得不錯的話,請給個贊哈,謝謝~

1、HDFS的介紹

  Hadoop分布式文件系統(HDFS)被設計成適合運行在通用硬件(commodity hardware)上的分布式文件系統。它和現有的分布式文件系統有很多共同點。但同時,它和其他的分布式文件系統的區別也是很明顯的。HDFS是一個高度容錯性的系統,適合部署在廉價的機器上。HDFS能提供高吞吐量的數據訪問,非常適合大規模數據集上的應用。HDFS放寬了一部分POSIX約束,來實現流式讀取文件系統數據的目的。HDFS在最開始是作為Apache Nutch搜索引擎項目的基礎架構而開發的。HDFS是Apache Hadoop Core項目的一部分。

  Hadoop 是Apache基金會下一個開源的分布式計算平台,它以分布式文件系統HDFS和MapReduce算法為核心,為用戶提供了系統底層細節透明的分布式基礎架構。用戶可以在不了解分布式底層細節的情況下,充分利用分布式集群進行高速運算和存儲。

  Hadoop是一個能夠讓用戶輕松架構和使用的分布式計算平台。它主要有以下幾個 優點:  
  ① 高可靠性。Hadoop按位存儲和處理數據的能力值得人們信賴。  
  ② 高擴展性。Hadoop是在可用的計算機集簇間分配數據並完成計算任務的,這些集簇可以方便地擴展到數以千計的節點中。  
  ③ 高效性。Hadoop能夠在節點之間動態地移動數據,並保證各個節點的動態平衡,因此處理速度非常快。  
  ④ 高容錯性。Hadoop能夠自動保存數據的多個副本,並且能夠自動將失敗的任務重新分配。  
  ⑤ 低成本。與一體機、商用數據倉庫以及QlikView、Yonghong Z-Suite等數據集市相比,Hadoop是開源的,項目的軟件成本因此會大大降低。  

  缺點:  

  ①不適合低延遲數據訪問。  
  ②無法高效存儲大量小文件,會占用大量的namenode內存。  
  ③不支持多用戶寫入以及任意修改文件。
 

2、 HDFS的架構與設計

  2.1 HDFS的設計

  HDFS以流式數據訪問模式來存儲超大文件,運行於商用硬件集群上。

  以下是對HDFS的設計簡單描述(詳細可以參閱該文章):

  • 超大文件 :“超大文件”在這里指具有幾百MB、幾百GB甚至幾百TB大小的文件。目前已經有存儲PB級數據的Hadoop集群了。
  • 流式數據訪問 :HDFS的構建思路是這樣的:一次寫入、多次讀取是最高效的訪問模式。數據集通常由數據源生成或從數據源復制而來,接着長時間 在此數據集上進行各種分析。每次分析都將涉及該數據集的大部分數據甚至全部,因此讀取整個數據集的時間延遲比讀取第一條記錄的時間延遲更重要。
  • 商用硬件 :Hadoop並不需要運行在昂貴且高可靠的硬件上。
  • 低時間延遲的數據訪問 :要求低時間延遲數據訪問的應用,例如幾十毫秒范圍,不適合在HDFS上運行。HDFS是為高數據吞吐量應用優化的,這可能會以提高時間延遲為代價。對於低延遲的訪問需求,HBase是更好的選擇。
  • 大量的小文件 :由於namenode將文件系統的元數據存儲在內存中,因此該文件系統所能存儲的文件總數受限於NameNode的內存容量。
  • 多用戶寫入,任意修改文件 :HDFS中的文件寫入只支持單個寫入者,而且寫操作總是以“只添加”方式在文件末尾寫數據。它不支持多個寫入者的操作,也不支持在文件的任意位置進行修改。但可能以后會支持,不過這種相對比較低效。

  2.2 HDFS核心組件

  HDFS采用master/slave架構。一個HDFS集群是有一個Namenode和一定數目的Datanode組成。Namenode是一個中心服務器,負責管理文件系統的namespace和客戶端對文件的訪問。Datanode在集群中一般是一個節點一個,負責管理節點上它們附帶的存儲。在內部,一個文件其實分成一個或多個block,這些block存儲在Datanode集合里。Namenode執行文件系統的namespace操作,例如打開、關閉、重命名文件和目錄,同時決定block到具體Datanode節點的映射。Datanode在Namenode的指揮下進行block的創建、刪除和復制。Namenode和Datanode都是設計成可以跑在普通的廉價的運行Linux的機器上。HDFS采用java語言開發,因此可以部署在很大范圍的機器上。一個典型的部署場景是一台機器跑一個單獨的Namenode節點,集群中的其他機器各跑一個Datanode實例。這個架構並不排除一台機器上跑多個Datanode,不過這比較少見。集群中單一Namenode的結構大大簡化了系統的架構。Namenode是所有HDFS元數據的仲裁者和管理者,這樣,用戶數據永遠不會流過Namenode。

  下圖是Hadoop的架構設計圖:

Hadoop的架構設計圖

  

3、HDFS的概念

  3.1 數據塊

  每個磁盤都有默認的數據塊大小,這是磁盤進行數據讀/寫的最小單位。構建於單個磁盤之上的文件系統通過磁盤塊來管理該文件系統中的塊,該文件系統塊的大小可以是磁盤塊的整數倍。文件系統塊一般為幾千字節,而磁盤塊一般為512字節。但這些對於需要讀/寫文件的文件系統用戶來說是透明的。

  HDFS同樣也有塊(block)的概念,但是大得多,默認為128MB。與單一磁盤上的文件系統相似,HDFS上的文件也被划分為塊大小的多個分塊,作為獨立的存儲單元。但與面向單一磁盤的文件系統不同的是,HDFS中小於一個塊大小的文件不會占據整個塊的空間,例如當一個1MB的文件存儲在一個128MB的塊中時,文件只使用1MB的磁盤空間,而不是128MB。

  HDFS中的塊為什么這么大?HDFS的塊比磁盤的塊大,其目的是為了最小化尋址開銷。如果塊足夠大,從磁盤傳輸數據的時間會明顯大於定位這個塊開始位置所需的時間。因為,傳輸一個由多個塊組成的大文件的時間取決於磁盤傳輸速率。但是塊大小這個參數也不會設置得過大,MapReduce中map任務通常一次只處理一個塊中的數據,因此如果任務數太少(少於集群中的節點數量),作業的運行速度就會比較慢。

  對分布式文件系統中的塊進行抽象會帶來很多好處。

  1. 第一好處是一個文件的大小可以大於網絡中任意一個磁盤的容量。
  2. 第二個好處是使用抽象塊而非整個文件作為存儲單元,大大簡化了存儲子系統的設計。
  3. 第三個好處是塊還非常適合用於數據備份進而提供數據容錯能力和提高可用性。

  HDFS將每個塊復制到少數幾個物理上相互獨立的機器上(默認為3個),可以確保在塊、磁盤或機器發生故障后數據不會丟失。如果發現一個塊不可用,系統會從其他地方讀取另一個復本,而這個過程對用戶是透明的。一個因損壞或機器故障而丟失的塊可以從其他候選地點復制到另一台可以正常運行的機器上,以保證復本的數量回到正常水平。同樣,有些應用程序可能選擇為一些常用的文件塊設置更高的復本數量進而分散集群中的讀取負載。

  在HDFS中顯示塊信息:

# hdfs fsck / -files -blocks

   可以執行命令修改HDFS的數據塊大小以及復本數量:

# vim $HADOOP_HOME/etc/hadoop/hdfs-site.xml

 

  3.2 NameNode(管理節點)

  3.2.1 NameNode目錄結構

  運行中的NameNode有如下所示的目錄結構:

  

  •  VERSION文件 :是一個Java屬性文件,其中包含正在運行的HDFS的版本信息。該文件一般包含以下內容:
#Mon Sep 29 09:54:36 BST 2014
namespaceID=1342387246
clusterID=CID-01b5c398-959c-4ea8-aae6-1e0d9bd8b142
cTime=0
storageType=NAME_NODE
blockpoolID=BP-526805057-127.0.0.1-1411980876842
layoutVersion=-57
    • layoutVersion :這是一個負整數,描述HDFS持久性數據結構(也稱布局)的版本,但是該版本號與Hadoop發布包的版本號無關。只要布局變更,版本號將會遞減,此時HDFS也要升級。否則,新版本的NameNode(或DataNode)就無法正常工作。
    • namespaceID :文件系統命名空間的唯一標識符,是在NameNode首次格式化時創建的。
    • clusterID : 在HDFS集群上作為一個整體賦予的唯一標識符,這對於聯邦HDFS非常重要。
    • blockpoolID :數據塊池的唯一標識符,數據塊池中包含了由一個NameNode管理的命名空間中的所有文件。
    • cTime :標記了NameNode存儲系統的創建時間。剛格式化的存儲系統,值為0,但升級后,該值會更新到新的時間戳。
    • storageType :該存儲目錄包含的時NameNode的數據結構。
  • 編輯日志(edits log)與命名空間鏡像文件(fsimage):

  編輯日志(edits log) :文件系統客戶端執行寫操作時,這些事務首先被記錄到edits中。NameNode在內存中維護文件系統的元數據;當被修改時,相關元數據信息也同步更新。內存中的元數據可支持客戶端的讀請求。我們可以使用OEV查看edits文件

選項解析:

-i,--inputFile <arg>:要處理的編輯文件
-o,--outputFile <arg>:輸出文件的名稱;如果指定的文件存在,它將被覆蓋
-p,--processor <arg>:選擇要應用於編輯文件的處理器類型 (XML|FileDistribution|Web|Delimited)

oev中的e指定了鏡像文件

    命令如下:

<?xml version="1.0" encoding="UTF-8"?>
<EDITS>
    <EDITS_VERSION>-63</EDITS_VERSION>
    <RECORD>
        <!-- 開始日志段-->
        <OPCODE>OP_START_LOG_SEGMENT</OPCODE>
        <DATA>
            <!-- 事務id-->
            <TXID>1</TXID>
        </DATA>
    </RECORD>
    <RECORD>
        <!-- 結束日志段-->
        <OPCODE>OP_END_LOG_SEGMENT</OPCODE>
        <DATA>
            <TXID>2</TXID>
        </DATA>
    </RECORD>
</EDITS>

  命名空間鏡像文件(fsimage):文件系統元數據的持久檢查點,每個fsimage文件包含文件系統中的所有目錄和文件inode的序列化信息(從Hadoop-2.4.0起,FSImage開始采用Google Protobuf編碼格式),每個inodes表征一個文件或目錄的元數據信息以及文件的副本數、修改和訪問時間等信息。數據塊存儲在DataNode中,但fsimage文件並不描述DataNode。我們可以使用OIV查看fsimage文件

選項解析:

-i,--inputFile <arg>:要處理的鏡像文件
-o,--outputFile <arg>:輸出文件的名稱;如果指定的文件存在,它將被覆蓋
-p,--processor <arg>:選擇要應用於鏡像文件的處理器類型 (XML|FileDistribution|Web|Delimited)

oiv中的i指定了image文件

    命令如下:

# hdfs oiv -p XML -i fsimage_0000000000000014026 -o fsimage.xml
<?xml version="1.0"?>
<fsimage>
    <NameSection>
        <!-- 默認的開啟編號-->
        <genstampV1>1000</genstampV1>
        <!-- 最后一個塊的編號-->
        <genstampV2>2215</genstampV2>
        <genstampV1Limit>0</genstampV1Limit>
         <!-- 最后一個分配的塊的塊id-->
        <lastAllocatedBlockId>1073743027</lastAllocatedBlockId>
        <!-- 開始的事務id號-->
        <txid>14026</txid>
    </NameSection>
    <INodeSection>
        <!-- 最后一個文件(目錄)的inode號-->
        <lastInodeId>18763</lastInodeId>
        <!--當前文件系統中只有根目錄,以下為根目錄的相關信息-->
        <inode>
            <id>16385</id>
            <type>DIRECTORY</type>
            <name></name>
            <mtime>1560256204322</mtime>
            <permission>root:root:rwxrwxrwx</permission>
            <nsquota>9223372036854775807</nsquota>
            <dsquota>-1</dsquota>
        </inode>
        <inode>
            <id>16417</id>
            <type>DIRECTORY</type>
            <name>myInfo</name>
            <mtime>1552974220469</mtime>
            <permission>root:root:rwxrwxrwx</permission>
            <nsquota>-1</nsquota>
            <dsquota>-1</dsquota>
        </inode>
        <inode>
            <id>16418</id>
            <type>FILE</type>
            <name>myInfo.txt</name>
            <replication>1</replication>
            <mtime>1552830434241</mtime>
            <atime>1552974031814</atime>
            <perferredBlockSize>134217728</perferredBlockSize>
            <permission>root:root:rwxrwxrwx</permission>
            <blocks>
                <block>
                    <id>1073741855</id>
                    <genstamp>1031</genstamp>
                    <numBytes>147</numBytes>
                </block>
            </blocks>
        </inode>
        .........   // inode文件太多,省略
    </INodeSection>
    <INodeReferenceSection></INodeReferenceSection>
    <SnapshotSection>
        <snapshotCounter>0</snapshotCounter>
    </SnapshotSection>
    <INodeDirectorySection>
        <directory>
            <parent>16385</parent>
            <inode>18543</inode>
            <inode>16474</inode>
            <inode>16419</inode>
            <inode>16417</inode>
            <inode>16427</inode>
            <inode>17544</inode>
            <inode>17561</inode>
        </directory>
        <directory>
            <parent>16417</parent>
            <inode>16420</inode>
        </directory>
        <directory>
            <parent>16419</parent>
            <inode>17399</inode>
            <inode>17258</inode>
            <inode>16418</inode>
            <inode>17294</inode>
        </directory>
         ......        // 省略其他<directory>標簽
    </INodeDirectorySection>
    <FileUnderConstructionSection>        
    </FileUnderConstructionSection>
    <SecretManagerSection>
        <currentId>0</currentId>
        <tokenSequenceNumber>0</tokenSequenceNumber>
    </SecretManagerSection>
    <CacheManagerSection>
        <nextDirectiveId>1</nextDirectiveId>
    </CacheManagerSection>
</fsimage>
View Code
  • seen_txid文件 :該文件對於NameNode非常重要,它是存放transactionId的文件,format之后是0,它代表的是NameNode里面的edits_*文件的尾數,NameNode重啟的時候,會按照seen_txid的數字,循序從頭跑edits_000*01~到seen_txid的數字。當hdfs發生異常重啟的時候,一定要比對seen_txid內的數字是不是你edits最后的尾數,不然會發生建置NameNode時元數據信息缺失,導致誤刪DataNode上多余block。

  • in_use.lock文件 :是一個鎖文件,NameNode使用該文件為存儲目錄加鎖。可以避免其他NameNode實例同時使用(可能會破壞)同一個存儲目錄的情況。  

  3.2.2 NameNode的工作原理

  NameNode管理文件系統的命名空間。它維護着文件系統樹及整棵樹內所有的文件和目錄。這些信息以兩個文件形式永久保存在本地磁盤上:命名空間鏡像文件(fsimage)和編輯日志文件(edits log)。它也記錄着每個文件中各個塊所在的數據節點信息,但它並不永久保存塊的位置信息,因為這些信息會在系統啟動時根據DataNode節點信息重建,塊信息存儲在內存中。

  可以看得出來NameNode的正常運行是非常重要的,如果運行的NameNode服務的機器毀壞,文件系統上所有的文件將會丟失,因為我們不知道如何根據DataNode的塊重建文件。因此,Hadoop為此提供兩種實現NameNode容錯機制:

  1. 備份組成文件系統元數據持久狀態的文件。一般是將持久狀態寫入本地磁盤的同時,寫入一個遠程掛載的網絡文件系統(NFS),HDFS與NFS安裝配置可以參考該文章(小編目前還沒實現,但未來會實現)。
  2. 運行一個輔助NameNode。但它不能作為主NameNode,這個輔助NameNode的重要作用是定期合並編輯日志(edits)與命名空間鏡像文件(fsimage),以防止編輯日志過大。一般來說輔助NameNode在一個單獨的機器上運行,因為它需要占用大量CPU時間並且一樣多的內存來執行合並操作。設計成這樣的好處在於,一旦主NameNode發生故障,輔助NameNode立刻就可以接替它的工作,但是由於保存數據是定時進行的,所以難免會有損失的數據,此時就可以把保存在其他地方(NFS)的數據復制到輔助NameNode,然后輔助NameNode作為新的主NameNode運行(注意,也可以運行熱備份NameNode代替運行輔助NameNode)。

 

  3.3 SecondaryNamenode(輔助NameNode)

  Hadoop SecondaryNameNode並不是Hadoop的第二個namanode,它不提供NameNode服務,而僅僅是NameNode的一個工具,這個工具幫助NameNode管理元數據信息。可能是由於SecondaryNameNode這個名字給人帶來的混淆,Hadoop后面的版本(1.0.4)建議不要使用,而使用CheckPoint Node。但在這小節中,小編還是使用SecondaryNamenode。

  運行中的SecondaryNamenode(輔助NameNode)的目錄結構與主NameNode的目錄結構幾乎一樣,但有部分時間不相同,它為主NameNode內存中的文件系統元數據創建檢查點(后面解釋)尚未成功時兩者不相同。運行中的SecondaryNamenode有如下所示的目錄結構:

  

  當NameNode 啟動時,需要合並fsimage和edits文件,按照edits文件內容將fsimage進行事務處理,從而得到HDFS的最新狀態。實際應用中,NameNode很少重新啟動。假如存在一個龐大的集群,且關於HDFS的操作相當頻繁與復雜,那么就會產生一個非常大的edits文件用於記錄操作,這就帶來了以下問題:

  • edits文件過大會帶來管理問題;
  • 一旦需要重啟HDFS時,就需要花費很長一段時間對edits和fsimage進行合並,這就導致HDFS長時間內無法啟動;
  • 如果NameNode掛掉了,會丟失部分操作記錄(這部分記錄存儲在內存中,還未寫入edits);

  此時,Secondary NameNode就要發揮它的作用了:合並edits文件,防止edits文件持續增長。該輔助NameNode會為主NameNode內存中的文件系統元數據創建檢查點(fsimage文件),創建檢查點前HDFS會自動進入安全模式(safe mode),當NameNode處在安全模式,管理員也可手動調用hdfs dfsadmin -saveNameSpace命令來創建檢查點。創建檢查點的步驟如下所示(如圖中也簡單地描述)。

  1. 輔助NameNode請求主NameNode停止使用正在進行中的edits文件,這樣新的編輯操作記錄到一個新文件中。主NameNode還會更新所有存儲目錄中的seen_txid文件。
  2. 輔助NameNode從主NameNode獲取最近的fsimage和edits文件(采用HTTP GET)。
  3. 輔助NameNode將fsimage文件載入內存,逐一執行edits文件中的事務,創建新的合並后的fsimage文件。
  4. 輔助NameNode將新的fsimage文件發送回主NameNode(使用HTTP PUT),主NameNode將其保存為臨時的.ckpt文件。
  5. 主NameNode重新命名臨時的fsimage文件,便於日后使用。

創建檢查點的步驟圖

  最終,主NameNode擁有最新的fsimage文件和一個更小的正在進行中的edits文件(edits文件可能非空,因為在創建檢查點過程中主NameNode還可能收到一些編輯請求)。這個過程清晰解釋了輔助NameNode和主NameNode擁有相近內存需求的原因(因為輔助NameNode也把fsimage文件載入內存)。因此,在大型集群中,輔助NameNode需要運行在一台專用機器上。

  在hdfs-site.xml中可以配置與檢查點觸發點有關的屬性:

<property>
  <name>dfs.namenode.checkpoint.period</name>
  <value>3600</value>
  <description>兩個定期檢查點之間的秒數
  </description>
</property>
 
<property>
  <name>dfs.namenode.checkpoint.txns</name>
  <value>1000000</value>
  <description>secondarynamenode或檢查點節點將創建檢查點
        每個“dfs.namenode.checkpoint.txns”事務的名稱空間
        判斷“dfs.namenode.checkpoint.period”是否已過期
  </description>
</property>
 
<property>
  <name>dfs.namenode.checkpoint.check.period</name>
  <value>60</value>
  <description>SecondaryNameNode和CheckpointNode將輪詢NameNode    
        每隔'dfs.namenode.checkpoint.check.period'秒查詢一次
        未存入檢查點事務
  </description>
</property>

  默認情況下,輔助NameNode每隔一個小時創建檢查點;此外,如果從上一個檢查點開始編輯日志的大小已經達到100萬個事務時,即使不到一小時,也會創建檢查點,檢查頻率為每分鍾一次。

  這個過程namesecondary目錄發生了更新;secondaryNameNode的檢查點目錄的布局與NameNode的是相同的,這種設計的好處是NameNode發生故障時,可以從secondaryNameNode恢復數據;有兩種實現方法:一是將相關存儲目錄復制到新的NameNode中;二是使用-importCheckpoint選項啟動NameNode守護進程,從而將secondaryNameNode用作新的NameNode

  與第一次開啟hdfs過程不同的是此次有30多秒的安全模式:

  在安全模式中在等待塊報告,這也關系到DataNode的運行過程。

 

  3.4 DataNode(工作節點)

  DataNode是文件系統的工作節點。它們根據需要存儲並檢索數據塊(受客戶端或NameNode調度),並且定期向NameNode發送它們所存儲的塊的列表。

  3.4.1 DataNode目錄結構

    和NameNode不同的是,DataNode的存儲目錄是初始階段自動創建的,不需要額外格式化。DataNode的關鍵文件和目錄如下所示:

  

  分析:從上圖可以看出,dataNode的文件結構主要由blk_前綴文件、BP-random integer-NameNode-IP address-creation time和VERSION構成。

  • BP-random integer-NameNode-IP address-creation time
    • BP代表BlockPool的,就是Namenode的VERSION中的集群唯一blockpoolID
    • 從上圖可以看出我的DataNode是一個BP,也就是說只有一個NameNode管理全部的文件系統命名空間,如果有兩個以上的BP,該HDFS是Federation HDFS,所以該目錄下有兩個BP開頭的目錄,IP部分和時間戳代表創建該BP的NameNode的IP地址和創建時間戳。
  •  finalized/rbw :
    • 這兩個目錄都是用於實際存儲HDFS BLOCK的數據,里面包含許多block_xx文件以及相應的.meta后綴的元數據文件,.meta文件包含了checksum信息。
    • rbw是“replica being written”的意思,該目錄用於存儲用戶當前正在寫入的數據。
  • blk_前綴文件 :
    • HDFS中的文件塊,存儲的是原始文件內容。
    • 塊的元數據信息,每一個塊有一個相關聯的.meta文件,一個文件塊由存儲的原始文件字節組成。
    • .meta文件包括頭部(含版本和類型信息)和該塊各區段的一系列的校驗和。
    • 每個塊屬於一個數據塊池(在本篇文章中,只有一個數據塊池),每個數據塊池都有自己的存儲目錄,目錄根據數據塊池ID形成(和NameNode的VERSION文件中的數據塊池ID相同)

  注 :當目錄中數據塊的數量增加到一定規模時,DataNode會創建一個子目錄來存放新的數據塊及其元數據信息。如果當前目錄已經存儲了64個(通過dfs.datanode.numblocks屬性設置)數據塊時,就創建一個子目錄。終極目標是設計一棵高扇出的目錄樹,即使文件系統中的塊數量非常多,目錄樹的層數也不多。通過這種方式,DataNode可以有效管理各個目錄中的文件,避免大多數操作系統遇到的管理難題,即很多(成千上萬個)文件放在同一個目錄之中。

  • VERSION
#Mon Sep 29 09:54:36 BST 2014storageID=DS-c478e76c-fe1b-44c8-ba45-4e4d6d266547
clusterID=CID-01b5c398-959c-4ea8-aae6-1e0d9bd8b142
cTime=0
datanodeUuid=75ffabf0-813c-4798-9a91-e7b1a26ee6f1
storageType=DATA_NODE layoutVersion=-57
    • storageID :相對於DataNode來說是唯一的,用於在NameNode處標識DataNode 
    • clusterID :是系統生成或手動指定的集群ID 
    • cTime :表示NameNode存儲時間的創建時間 
    • datanodeUuid :表示DataNode的ID號
    • storageType :將這個目錄標志位DataNode數據存儲目錄。 
    • layoutVersion :是一個負整數,保存了HDFS的持續化在硬盤上的數據結構的格式版本號。
  • in_use.lock :  

      是一個鎖文件,NameNode使用該文件為存儲目錄加鎖。可以避免其他NameNode實例同時使用(可能會破壞)同一個存儲目錄的情況。  

 

   3.5 塊緩存

  通常DataNode從磁盤中讀取塊,但對於訪問頻繁的文件,其對應的塊可能被顯式地緩存在DataNode內存中,以堆外塊緩存(off-heap block cache)的形式存在。默認情況下,一個塊僅緩存在一個DataNode的內存中,當然可以對每個文件配置DataNode的數量。作業調度器(用於MapReduce、Spark和其他框架的)通過在緩存塊的DataNode上運行任務,可以利用塊緩存的優勢提高讀操作的性能。

  用戶或應用通過在緩存池(cache pool)中增加一個 cache directive來告訴NameNode需要緩存哪些文件及存多久。緩存池是一個用於管理緩存權限和資源使用的管理性分組。

  本小節只簡單描述,有關HDFS的緩存管理請查閱官方文檔或者其他等相關資料。

 

  3.6 聯邦HDFS

  NameNode在內存中保存文件系統中每個文件和每個數據塊的引用關系,這意味着對於一個擁有大量文件的超大集群來說,內存將成為限制系統橫向擴展的瓶頸。在2.X發行版本系列中引入的聯邦HDFS允許系統通過添加NameNode實現擴展,其中每個NameNode管理文件系統命名空間中的一部分

  在聯邦環境中,每個NameNode維護一個命名空間卷(namespace volume),由命名空間的元數據和一個數據塊池(block pool)組成,數據塊池包含該命名空間下文件的所有數據塊。命名空間卷之間是相互獨立的,兩兩之間並不相互通信,甚至其中一個NameNode的失效也不會影響由其他NameNode維護的命名空間的可用性。

  集群中的DataNode還需要注冊到每個NameNode,並且存儲着來自多個數據塊池中的數據塊。

  聯邦HDFS的架構圖如下圖所示:

聯邦HDFS架構圖

  聯邦HDFS更詳細的請查閱官方文檔

 

  3.7 HDFS的高可用性(High Availability)

  通過聯合使用在多個文件系統中備份NameNode的元數據和通過備用NameNode創建監測點能防止數據丟失,但是依舊無法實現文件系統的高可用性。NameNode依舊存在單點失效(SPOF)的問題。如果NameNode失效了,那么所有的客戶端,包括MapReduce作業,均無法讀、寫或列舉文件,因為NameNode是唯一存儲元數據與文件到數據塊映射的地方,對於一個大型並擁有大量文件和數據塊的集群,NameNode的冷啟動需要30分鍾,甚至更長時間,系統恢復時間太長了,也會影響到日常維護。在這一情況下,Hadoop系統無法提供服務直到有新的NameNode上線。

   在這樣的情況下要向從一個失效的NameNode恢復,系統管理員得啟動一個擁有文件系統元數據副本得新的NameNode,並配置DataNode和客戶端以便使用這個新的NameNode。新的NameNode直到滿足以下情形才能相應服務:

  1. 將命名空間鏡像文件導入內存中;
  2. 重演編輯日志;
  3. 接收到足夠多的來自DataNode的數據塊報告並退出安全模式

   Hadoop2.X以上版本針對上述問題增加了對HDFS高可用性(HA)的支持。在這一實現中,配置了一對活動-備用(active-standby) NameNode。當活動NameNode失效,備用NameNode就會接管它的任務並開始服務於來自客戶端的請求,不會有任何明顯中斷。實現這一目標需要在架構上做如下修改。HDFS HA架構圖如下所示:

HDFS HA架構圖

    • NameNode之間需要通過高可用共享存儲實現編輯日志的共享。當備用NameNode接管工作之后,它將通讀共享編輯日志直至末尾,以實現與活動NameNode的狀態同步,並繼續讀取由活動NameNode寫入的新條目。
    • DataNode需要同時向兩個NameNode發送數據塊處理報告,因為數據塊的映射信息存儲在NameNode的內存中,而非磁盤。
    • 客戶端需要使用特定的機制來處理NameNode的失效問題,這一機制對用戶是透明的。
    • 輔助NameNode的角色被備用NameNode所包含,備用NameNode為活動的NameNode命名空間設置周期性檢查點。

  有兩種高可用性共享存儲可以做出選擇:NFS過濾器群體日志管理器(QJM, quorum journal manager)。QJM是一個專用的HDFS實現,為提供一個高可用的編輯日志而設計,被推薦用於大多數HDFS部署中,同時,QJM的實現並沒使用Zookeeper,但在HDFS HA選取活動的NameNode時使用了Zookeeper技術。QJM以一組日志節點(journalnode)的形式運行,一般是奇數點結點組成,每個JournalNode對外有一個簡易的RPC接口,以供NameNode讀寫EditLog到JN本地磁盤。當寫EditLog時,NameNode會同時向所有JournalNode並行寫文件,只要有N/2+1結點寫成功則認為此次寫操作成功,遵循Paxos協議。其內部實現框架如下:

QJM內部實現框架

  從圖中可看出,主要是涉及EditLog的不同管理對象和輸出流對象,每種對象發揮着各自不同作用:

    • FSEditLog:所有EditLog操作的入口。
    • JournalSet:集成本地磁盤和JournalNode集群上EditLog的相關操作。
    • FileJournalManager:實現本地磁盤上EditLog操作。
    • QuorumJournalManager:實現JournalNode集群EditLog操作。
    • AsyncLoggerSet:實現JournalNode集群EditLog的寫操作集合。
    • AsyncLogger:發起RPC請求到JN,執行具體的日志同步功能。
    • JournalNodeRpcServer:運行在JournalNode節點進程中的RPC服務,接收NameNode端的AsyncLogger的RPC請求。
    • JournalNodeHttpServer:運行在JournalNode節點進程中的Http服務,用於接收處於Standby狀態的NameNode和其它JournalNode的同步EditLog文件流的請求。

  3.7.1 QJM寫過程分析

  上面提到EditLog,NameNode會把EditLog同時寫到本地和JournalNode。寫本地由配置中參數dfs.namenode.name.dir控制,寫JN由參數dfs.namenode.shared.edits.dir控制,在寫EditLog時會由兩個不同的輸出流來控制日志的寫過程,分別為:EditLogFileOutputStream(本地輸出流)和QuorumOutputStream(JN輸出流)。寫EditLog也不是直接寫到磁盤中,為保證高吞吐,NameNode會分別為EditLogFileOutputStream和QuorumOutputStream定義兩個同等大小的Buffer,大小大概是512KB,一個寫Buffer(buffCurrent),一個同步Buffer(buffReady),這樣可以一邊寫一邊同步,所以EditLog是一個異步寫過程,同時也是一個批量同步的過程,避免每寫一筆就同步一次日志。

  這個是怎么實現邊寫邊同步的呢,這中間其實是有一個緩沖區交換的過程,即bufferCurrent和buffReady在達到條件時會觸發交換,如bufferCurrent在達到閾值同時bufferReady的數據又同步完時,bufferReady數據會清空,同時會將bufferCurrent指針指向bufferReady以滿足繼續寫,另外會將bufferReady指針指向bufferCurrent以提供繼續同步EditLog。上面過程用流程圖就是表示如下:

EditLog輸出流程圖

問題一:  

  既然EditLog是異步寫的,怎么保證緩存中的數據不丟呢,其實這里雖然是異步,但實際所有日志都需要通過logSync同步成功后才會給client返回成功碼,假設某一時刻NameNode不可用了,其內存中的數據其實是未同步成功的,所以client會認為這部分數據未寫成功。還有EditLog怎么在多個JN上保持一致的呢?

  解決方案:

1. 隔離雙寫

  在ANN每次同步EditLog到JN時,先要保證不會有兩個NN同時向JN同步日志,也就是說同一時間QJM僅允許一個NameNode向編輯日志中寫入數據。這個隔離是怎么做的。這里面涉及一個很重要的概念Epoch Numbers,很多分布式系統都會用到。Epoch有如下幾個特性:

    • 當NN成為活動結點時,其會被賦予一個EpochNumber。
    • 每個EpochNumber是唯一的,不會有相同的EpochNumber出現。
    • EpochNumber有嚴格順序保證,每次NN切換后其EpochNumber都會自增1,后面生成的EpochNumber都會大於前面的EpochNumber。

  但QJM是怎么保證上面的特性的呢,主要有以下幾點:

    1. 在對EditLog作任何修改前,QuorumJournalManager(NameNode上)必須被賦予一個EpochNumber;
    2. QJM把自己的EpochNumber通過newEpoch(N)的方式發送給所有JN結點
    3. 當JN收到newEpoch請求后,會把QJM的EpochNumber保存到一個lastPromisedEpoch變量中並持久化到本地磁盤;
    4. ANN同步日志到JN的任何RPC請求(如logEdits(),startLogSegment()等),都必須包含ANN的EpochNumber;
    5. JN在收到RPC請求后,會將之與lastPromisedEpoch對比,如果請求的EpochNumber小於lastPromisedEpoch,將會拒絕同步請求,反之,會接受同步請求並將請求的EpochNumber保存在lastPromisedEpoch。

  這樣就能保證主備NN發生切換時,就算同時向JN同步日志,也能保證日志不會寫亂,因為發生切換后,原ANN的EpochNumber肯定是小於新ANN的EpochNumber,所以原ANN向JN的發起的所有同步請求都會拒絕,實現隔離功能,防止了腦裂。

2. 恢復in-process日志

  如果在寫過程中寫失敗了,可能各個JN上的EditLog的長度都不一樣,需要在開始寫之前將不一致的部分恢復。恢復機制如下:

  1.  ANN先向所有JN發送getJournalState請求;
  2. JN會向ANN返回一個Epoch(lastPromisedEpoch);
  3. ANN收到大多數JN的Epoch后,選擇最大的一個並加1作為當前新的Epoch,然后向JN發送新的newEpoch請求,把新的Epoch下發給JN;
  4. JN收到新的Epoch后,和lastPromisedEpoch對比,若更大則更新到本地並返回給ANN自己本地一個最新EditLogSegment起始事務Id,若小則返回NN錯誤;
  5. ANN收到多數JN成功響應后認為Epoch生成成功,開始准備日志恢復;
  6. ANN會選擇一個最大的EditLogSegment事務ID作為恢復依據,然后向JN發送prepareRecovery; RPC請求,對應Paxos協議2p階段的Phase1a,若多數JN響應prepareRecovery成功,則可認為Phase1a階段成功;
  7. ANN選擇進行同步的數據源,向JN發送acceptRecovery RPC請求,並將數據源作為參數傳給JN。
  8. JN收到acceptRecovery請求后,會從JournalNodeHttpServer下載EditLogSegment並替換到本地保存的EditLogSegment,對應Paxos協議2p階段的Phase1b,完成后返回ANN請求成功狀態。
  9. ANN收到多數JN的響應成功請求后,向JN發送finalizeLogSegment請求,表示數據恢復完成,這樣之后所有JN上的日志就能保持一致。
    數據恢復后,ANN上會將本地處於in-process狀態的日志更名為finalized狀態的日志,形式如edits[start-txid][stop-txid]。

3. 日志同步

  日志從ANN同步到JN的過程,具體如下:

  1. 執行logSync過程,將ANN上的日志數據放到緩存隊列中;
  2. 將緩存中數據同步到JN,JN有相應線程來處理logEdits請求
  3. JN收到數據后,先確認EpochNumber是否合法,再驗證日志事務ID是否正常,將日志刷到磁盤,返回ANN成功碼;
  4.  ANN收到JN成功請求后返回client寫成功標識,若失敗則拋出異常。

 

  通過上面一些步驟,日志能保證成功同步到JN,同時保證JN日志的一致性,進而備NN上同步日志時也能保證數據是完整和一致的。

3.7.2 QJM讀過程分析

  這個讀過程是面向備NN(SNN)的,SNN定期檢查JournalNode上EditLog的變化,然后將EditLog拉回本地。SNN上有一個線程StandbyCheckpointer,會定期將SNN上FSImage和EditLog合並,並將合並完的FSImage文件傳回主NN(ANN)上,就是所說的Checkpointing過程。下面我們來看下Checkpointing是怎么進行的。

  在2.x版本中,已經將原來的由SecondaryNameNode主導的Checkpointing替換成由SNN主導的Checkpointing。下面是一個CheckPoint的流向圖:

Checkpointing流向圖

   總的來說,就是在SNN上先檢查前置條件,前置條件包括兩個方面:距離上次Checkpointing的時間間隔和EditLog中事務條數限制。前置條件任何一個滿足都會觸發Checkpointing,然后SNN會將最新的NameSpace數據即SNN內存中當前狀態的元數據保存到一個臨時的fsimage文件( fsimage.ckpt)然后比對從JN上拉到的最新EditLog的事務ID,將fsimage.ckpt_中沒有,EditLog中有的所有元數據修改記錄合並一起並重命名成新的fsimage文件,同時生成一個md5文件。將最新的fsimage再通過HTTP請求傳回ANN。通過定期合並fsimage有什么好處呢,主要有以下幾個方面:

    • 可以避免EditLog越來越大,合並成新fsimage后可以將老的EditLog刪除;
    • 可以避免主NN(ANN)壓力過大,合並是在SNN上進行的;
    • 可以避免fsimage保存的是一份最新的元數據,故障恢復時避免數據丟失。

3.7.3 HDFS HA如何實現故障切換與規避?

  在活動namenode(ANN)失效之后,備用namenode(SNN)能夠快速(幾十秒的時間)實現任務接管,因為最新的狀態存儲在內存中:包括最新的編輯日志條目和最新的數據塊映射信息。實際觀察到的失效時間略長一點(需要1分鍾左右),這是因為系統需要保守確定活動namenode是否真的失效了。活動namenode失效且備用namenode也失效的情況下,當然這類情況發生的概率非常低非常低的,現在Hadoop 3.X發行版本已經支持運行更多備用namenode來提供更高的容錯性

  系統中有一個稱為故障轉移控制器(failover controller)的新實體,管理着將活動namenode轉移為備用namenode的轉換過程。有多種故障轉移控制器,但默認一種是使用了Zookeeper來確保有且僅有一個活動namenode。每一個namenode運行着一個輕量級的故障轉移控制器,其工作就是監視宿主namenode是否失效(通過一個簡單的心跳機制實現)並在namenode失效時進行故障轉移,這就是HA的主備切換機制,主備選舉依賴於Zookeeper。下面是主備切換的狀態圖:

Failover流程圖

  從圖中可以看出,整個切換過程是由ZKFC(即故障轉移控制器,全稱Zookeeper Failover Controller)來控制的,具體又可分為HealthMonitor、ZKFailoverController和ActiveStandbyElector三個組件。

    • ZKFailoverController:是HealthMontior和ActiveStandbyElector的母體,執行具體的切換操作。
    • HealthMonitor:監控NameNode健康狀態,若狀態異常會觸發回調ZKFailoverController進行自動主備切換。
    • ActiveStandbyElector:通知ZK執行主備選舉,若ZK完成變更,會回調ZKFailoverController相應方法進行主備狀態切換。

  在故障切換期間,Zookeeper主要是發揮什么作用呢,有以下幾點:

    • 失敗保護:集群中每一個NameNode都會在Zookeeper維護一個持久的session,機器一旦掛掉,session就會過期,故障遷移就會觸發。
    • Active NameNode選擇:Zookeeper有一個選擇ActiveNN的機制,一旦現有的ANN宕機,其他NameNode可以向Zookeeper申請成為下一個Active節點。
    • 防腦裂:ZK本身是強一致和高可用的,可以用它來保證同一時刻只有一個活動節點。

  在哪些場景會觸發自動切換呢,從HDFS-2185中歸納了以下幾個場景:

    • ANN JVM崩潰:ANN上HealthMonitor狀態上報會有連接超時異常,HealthMonitor會觸發狀態遷移至SERVICE_NOT_RESPONDING,然后ANN上的ZKFC會退出選舉,SNN上的ZKFC會獲得Active Lock,作相應隔離后成為Active節點。
    • ANN JVM凍結:這個是JVM沒崩潰,但也無法響應,同崩潰一樣,會觸發自動切換。
    • ANN 機器宕機:此時ActiveStandbyElector會失去同ZK的心跳,會話超時,SNN上的ZKFC會通知ZK刪除ANN的活動鎖,作相應隔離后完成主備切換。
    • ANN 健康狀態異常:此時HealthMonitor會收到一個HealthCheckFailedException,並觸發自動切換。
    • Active ZKFC崩潰:雖然ZKFC是一個獨立的進程,但因設計簡單也容易出問題,一旦ZKFC進程掛掉,雖然此時NameNode是正常運行的,但系統也認為需要切換,此時SNN會發一個請求到ANN要求ANN放棄主節點位置,ANN收到請求后,會觸發自動切換。
    • Zookeeper集群崩潰:如果ZK集群崩潰了,主備NN上的ZKFC都會感知並斷連,此時主備NN會進入一個NeutralMode模式,同時不改變主備NN的狀態,繼續發揮作用,只不過此時,如果ANN也故障了,那集群無法發揮Failover,也就無法使用集群了,所以對於此種場景,ZK集群一般是不允許掛掉到多台,至少要有(N / 2 + 1)台保持服務才算是安全的。

  管理員也可以通過手動發起故障轉移,例如在進行日常維護時,這稱為”平穩的故障轉移“(graceful failover),因為故障轉移控制器可以組織兩個namenode有序地切換角色。命令參考如下所示。

// 將 active 狀態由 nn1 切換到 nn2
# hdfs haadmin -failover --forcefence --forceactive nn1 nn2

// 在啟用自動故障轉移的集群上 --forcefence -- forceactive 參數不起作用
// 使用以下方法檢查名稱節點狀態(假設 nn1 為 active,nn2 standby):
# hdfs haadmin -getServiceState nn1
active
# hdfs haadmin -getServiceState nn2
standby

// 於是我們人為制造故障,在 nn1 上查看 NameNode 進程
# jps
# kill -9 [進程ID]
// 自動故障轉移將會激活 nn2 節點,狀態從 standby 轉換為 active

  但在非平穩故障轉移的情況下,無法確切直到失效NameNode是否已經停止運行。例如網速較慢或者網絡被分割的情況下,可能激發故障轉移,但Active NameNode依然運行着並且依舊是Active NameNode。高可用實現做了更一步的優化,以確保先前Active NameNode不會執行危害系統並導致系統崩潰的操作,該方法稱為”規避“。

  規避機制包括:撤銷NameNode訪問共享存儲目錄的權限(通常使用供應商指定的NFS命令)、通過遠程管理命令屏蔽相應的網絡端口。最不行的話,可以通過“一槍爆頭”(斷電關機)等制造人為故障技術。

 

4、HDFS的讀寫原理

  4.1 HDFS讀數據

HDFS讀數據流程圖

  1.  HDFS客戶端通過調用FileSystem對象的open()方法來打開希望讀取的文件,對於HDFS來說,這個對象是DistributedFileSystem的一個實例。
  2. DistributedFileSystem通過使用遠程過程調用(RPC)來調用NameNode,以確定文件起始塊的位置。對於每個塊,NameNode返回具有該塊副本的DataNode地址。此外,這些DataNode根據它們與客戶端的距離來排序(根據集群的網絡拓撲),如果該客戶端本身就是一個DataNode,便從本地讀取數據。接着DistributedFileSystem類返回一個FSDataInputStream對象(一個支持文件定位的輸入流)給客戶端以便讀取數據。FSDataInputStream類轉而封裝DFSInputStream對象,該對象管理着DataNode和NameNode的 I/O。
  3. 客戶端對這個輸入流調用read()方法,存儲着文件起始幾個塊的DataNode地址的DFSInputStream隨即連接距離最近的文件中第一個塊所在的DataNode。
  4. 通過對數據流反復調用read()方法,可以將數據從DataNode傳輸到HDFS 客戶端。
  5. 讀取數據到塊的末端時,DFSInputStream關閉與該DataNode的連接,然后尋找下一個塊的最佳DataNode。在HDFS客戶端所看來它一直在讀取一個連續的流,這些對於客戶端來說是透明的。
  6. 客戶端從流中讀取數據時,塊是按照打開DFSInputStream與DataNode新建連接的順序讀取的,它也會根據需要詢問NameNode來檢索下一批數據塊的DataNode的位置,一旦客戶端讀取完畢,就會調用close()方法。但在讀取時,DFSInputStream與DataNode通信時遇到錯誤,會嘗試從這個塊的另外一個最鄰近DataNode讀取數據,它也會記住那個故障DataNode,以保證以后不會反復讀取該節點上后續的塊。DFSInputStream也會通過校驗和確認從DataNode發來的數據是否完整。如果發現有損壞的塊,DFSInputStream會試圖從其他DataNode讀取其復本,也會將被損壞的塊通知給NameNode。

 

  HDFS讀數據過程這一設計的一個重點是:客戶端可以直接連接到DataNode檢索數據,且NameNode告知客戶端每個塊所在的最佳DataNode,由於數據流分散在集群中的所有DataNode,所以這種設計能使HDFS擴展到大量的並發客戶端。同時,NameNode只需要響應塊位置的請求(這些信息存儲在內存中,因而非常高效),無需響應數據請求,否則隨着客戶端數量的增長,NameNode會很快稱為瓶頸。

     這里HdfsDataInputStream是FSDataInputStream的子類,這里是通過子類創建父類對象。

 

  4.2 HDFS寫數據

HDFS寫數據流程圖

  1. 客戶端通過對DistributedFileSystem對象調用create()來新建對象。
  2. DistributedFileSystem對NameNode創建一個RPC調用,在文件系統的命名空間中新建一個文件,此時該文件中還沒有響應的數據塊。NameNode執行各種不同的檢查以確保這個文件不存在以及客戶端有新建該文件的權限。
    1. 如果這些檢查均通過,NameNode就會為創建新文件記錄一條記錄;DistributedFileStream向客戶端返回一個FSDataOutputStream對象,由此客戶端可以開始寫入數據。就像讀取事件一樣,文件系統數據輸出流控制一個DFSOutputStream,負責處理datanode和namenode之間的通信。
    2. 否則,文件創建失敗並向客戶端拋出一個IOException異常。
  3. 在客戶端寫入數據時,DFSOutputStream將它分成一個個的數據包,並寫入內部隊列,稱為“數據隊列”(data queue)。DataStreamer處理數據隊列,它的責任是挑選出適合存儲數據復本的一組DataNode,並據此來要求NameNode分配新的數據塊。這一組DataNode構成一個管線——我們假設復本數為3,所以管線中有3個節點。
  4. DataStream將數據包流式傳輸到管線中第1個DataNode,該DataNode存儲數據包並將它發送到管線中的第2個DataNode。同樣,第2個DataNode存儲該數據包並且發送給管線中的第3個(也是最后一個)DataNode。
  5. DFSOutputStream也維護着一個內部數據包隊列來等待DataNode的收到確認回執,稱為“確認隊列”(ack queue)。收到管道中所有DataNode確認信息后,該數據包才會從確認隊列刪除。如果有DataNode在數據寫入期間發生故障,則執行以下操作(對寫入數據的客戶端是透明的)。
    1. 首先關閉管線,確認把隊列中的所有數據包都添加回數據隊列的最前端,以確保故障節點下游的DataNode不會漏掉任何一個數據包。
    2. 為存儲在另一正常DataNode的當前數據塊指定一個新的標識,並將該標識傳送給NameNode,以便故障DataNode在恢復后可以刪除存儲的部分數據塊。
    3. 從管線中刪除故障DataNode,基於兩個正常DataNode構建一條新管線。
    4. 余下的數據塊寫入管線中正常的DataNode。
    5. NameNode注意到塊復本量不足時,會在另一個節點上創建一個新的復本。后續的數據塊繼續正常接受處理。
  6. 客戶端完成數據的寫入后,對數據流調用close()。
  7. 在聯系到NameNode告知其文件寫入完成之前,此操作會將剩余的所有數據包寫入DataNode管線並等待確認。NameNode已經直到文件由哪些塊組成(因為Datastreamer請求分配數據塊),所以它在返回成功前只需要等待數據塊進行最小量的復制。

 

 


 

參考資料 :《Hadoop權威指南(第四版)》

     http://hadoop.apache.org/docs/stable/index.html

     https://blog.csdn.net/baiye_xing/article/details/76268495#commentBox

        https://www.jianshu.com/p/53e40d3b0f7d

     https://www.cnblogs.com/jstarseven/p/7682293.html

       https://blog.csdn.net/qq_39192827/article/details/88953472

     https://cloud.tencent.com/community/article/282177

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM