什麼是 Hadoop ?它主要能解決 “大資料” 的哪兩個問題?

熬夜不加班發表於2021-05-29

前言

之前工作中,有接觸到大資料的需求,雖然當時我們體系有專門的大資料部門,但是由於當時我們中臺重構,整個體系的開發量巨大,共用一個大資料部門,人手已經忙不過來,沒法辦,為了趕時間,我自己負責的系統的大資料相關操作,由我們自己承擔了。此前對大資料的知識瞭解的很少,於是晚上回去花時間突擊大資料知識,白天就開始上手幹,一邊學一邊做,總算在部門規定的時間,跟系統一起上線了。後來的維護迭代就交給大資料去了,雖然接觸大資料的時間不長,但是對我來說,確是很有意思的一段經歷,覺得把當時匆匆學的知識點,再仔細回顧回顧,整理下。

01 大資料概述

大資料: 就是對海量資料進行分析處理,得到一些有價值的資訊,然後幫助企業做出判斷和決策.

處理流程:

  • 1:獲取資料

  • 2:處理資料

  • 3:展示結果

02 Hadoop介紹

Hadoop是一個分散式系基礎框架,它允許使用簡單的程式設計模型跨大型計算機的大型資料集進行分散式處理.它主要解決兩個問題

  • 大資料儲存問題: HDFS

  • 大資料計算問題:MapReduce

2.1 問題一: 大檔案怎麼儲存?

假設一個檔案非常非常大,大小為1PB/a.txt, 大到世界上所有的高階計算機都儲存不下, 怎麼辦?

  • 為了儲存大檔案, 需要把檔案放在多個機器上

  • 檔案要分塊 block(128M)

  • 不同的塊放在不同的 HDFS 節點

  • 同時為了對外提供統一的訪問, 讓外部可以像是訪問本機一樣訪問分散式檔案系統

  • 有一個統一的 HDFS Master

  • 它儲存整個系統的檔案資訊

  • 所有的檔案後設資料的修改都從 Master 開始

2. 2 問題二: 大資料怎麼計算?

從一個網路日誌檔案中計算獨立 IP, 以及其出現的次數 如果資料量特別大,我們可以將,整個任務拆開, 劃分為比較小的任務, 從而進行計算呢。

2.3 問題三: 如何將這些計算任務跑在叢集中?

如果能夠在不同的節點上並行執行, 更有更大的提升, 如何把這些任務跑在叢集中?

  • 可以設定一個叢集的管理者, 這個地方叫做 Yarn這個叢集管理者有一個 Master, 用於接收和分配任務這個叢集管理者有多個 Slave, 用於執行任務

2.4 Hadoop 的組成

  • Hadoop分散式檔案系統(HDFS) 提供對應用程式資料的高吞吐量訪問的分散式檔案系統

  • Hadoop Common 其他Hadoop模組所需的Java庫和實用程式。這些庫提供檔案系統和作業系統級抽象,幷包含啟動Hadoop所需的必要Java檔案和指令碼

  • Hadoop MapReduce 基於YARN的大型資料集並行處理系統

  • Hadoop YARN 作業排程和叢集資源管理的框架

2.5 Hadoop前生今世

  1. Hadoop最早起源於Nutch。Nutch的設計目標是構建一個大型的全網搜尋引擎,包括網頁抓取、索引、查詢等功能,但隨著抓取網頁數量的增加,遇到了嚴重的可擴充套件性問題——如何解決數十億網頁的儲存和索引問題。

  2. 2003年、2004年穀歌發表的兩篇論文為該問題提供了可行的解決方案。

——分散式檔案系統(GFS),可用於處理海量網頁的儲存

——分散式計算框架MAPREDUCE,可用於處理海量網頁的索引計算問題。

  1. Nutch的開發人員完成了相應的開源實現HDFS和MAPREDUCE,並從Nutch中剝離成為獨立專案HADOOP,到2008年1月,HADOOP成為Apache頂級專案.

狹義上來說,hadoop就是單獨指代hadoop這個軟體,

HDFS :分散式檔案系統

MapReduce : 分散式計算系統

廣義上來說,hadoop指代大資料的一個生態圈,包括很多其他的軟體

image
image

2.6 hadoop的架構模型

1.x的版本架構模型介紹

image
image

檔案系統核心模組:

NameNode:叢集當中的主節點,管理後設資料(檔案的大小,檔案的位置,檔案的許可權),主要用於管理叢集當中的各種資料

secondaryNameNode:主要能用於hadoop當中後設資料資訊的輔助管理

DataNode:叢集當中的從節點,主要用於儲存叢集當中的各種資料

資料計算核心模組:

JobTracker:接收使用者的計算請求任務,並分配任務給從節點

TaskTracker:負責執行主節點JobTracker分配的任務

2.x的版本架構模型介紹

第一種:NameNode與ResourceManager單節點架構模型

image
image

檔案系統核心模組:

NameNode:叢集當中的主節點,主要用於管理叢集當中的各種資料

secondaryNameNode:主要能用於hadoop當中後設資料資訊的輔助管理

DataNode:叢集當中的從節點,主要用於儲存叢集當中的各種資料

資料計算核心模組:

ResourceManager:接收使用者的計算請求任務,並負責叢集的資源分配

NodeManager:負責執行主節點APPmaster分配的任務

第二種:NameNode單節點與ResourceManager高可用架構模型

image
image

檔案系統核心模組:

NameNode:叢集當中的主節點,主要用於管理叢集當中的各種資料

secondaryNameNode:主要能用於hadoop當中後設資料資訊的輔助管理

DataNode:叢集當中的從節點,主要用於儲存叢集當中的各種資料

資料計算核心模組:

ResourceManager:接收使用者的計算請求任務,並負責叢集的資源分配,以及計算任務的劃分,透過zookeeper實現ResourceManager的高可用

NodeManager:負責執行主節點ResourceManager分配的任務

第三種:NameNode高可用與ResourceManager單節點架構模型

image
image

檔案系統核心模組:

NameNode:叢集當中的主節點,主要用於管理叢集當中的各種資料,其中nameNode可以有兩個,形成高可用狀態

DataNode:叢集當中的從節點,主要用於儲存叢集當中的各種資料

JournalNode:檔案系統後設資料資訊管理

資料計算核心模組:

ResourceManager:接收使用者的計算請求任務,並負責叢集的資源分配,以及計算任務的劃分

NodeManager:負責執行主節點ResourceManager分配的任務

第四種:NameNode與ResourceManager高可用架構模型

image
image

檔案系統核心模組:

NameNode:叢集當中的主節點,主要用於管理叢集當中的各種資料,一般都是使用兩個,實現HA高可用

JournalNode:後設資料資訊管理程式,一般都是奇數個

DataNode:從節點,用於資料的儲存

資料計算核心模組:

ResourceManager:Yarn平臺的主節點,主要用於接收各種任務,透過兩個,構建成高可用

NodeManager:Yarn平臺的從節點,主要用於處理ResourceManager分配的任務

03 Hadoop 核心介紹

3.1 HDFS

HDFS(Hadoop Distributed File System) 是一個 Apache Software Foundation 專案, 是 Apache Hadoop 專案的一個子專案. Hadoop 非常適於儲存大型資料 (比如 TB 和 PB), 其就是使用 HDFS 作為儲存系統. HDFS 使用多臺計算機儲存檔案, 並且提供統一的訪問介面, 像是訪問一個普通檔案系統一樣使用分散式檔案系統. HDFS 對資料檔案的訪問透過流的方式進行處理, 這意味著透過命令和 MapReduce 程式的方式可以直接使用 HDFS. HDFS 是容錯的, 且提供對大資料集的高吞吐量訪問.

HDFS 的一個非常重要的特點就是一次寫入、多次讀取, 該模型降低了對併發控制的要求, 簡化了資料聚合性, 支援高吞吐量訪問. 而吞吐量是大資料系統的一個非常重要的指標, 吞吐量高意味著能處理的資料量就大.

3.1.1 設計目標

  • 透過跨多個廉價計算機叢集分佈資料和處理來節約成本

  • 透過自動維護多個資料副本和在故障發生時來實現可靠性

  • 它們為儲存和處理超大規模資料提供所需的擴充套件能力。

3.1.2 HDFS 的歷史

  1. Doug Cutting 在做 Lucene 的時候, 需要編寫一個爬蟲服務, 這個爬蟲寫的並不順利, 遇到了一些問題, 諸如: 如何儲存大規模的資料, 如何保證叢集的可伸縮性, 如何動態容錯等

  2. 2013年的時候, Google 釋出了三篇論文, 被稱作為三駕馬車, 其中有一篇叫做 GFS, 是描述了 Google 內部的一個叫做 GFS 的分散式大規模檔案系統, 具有強大的可伸縮性和容錯性

  3. Doug Cutting 後來根據 GFS 的論文, 創造了一個新的檔案系統, 叫做 HDFS

3.1.3 HDFS 的架構

  1. NameNode 是一箇中心伺服器, 單一節點(簡化系統的設計和實現), 負責管理檔案系統的名字空間(NameSpace)以及客戶端對檔案的訪問

  2. 檔案操作, NameNode 是負責檔案後設資料的操作, DataNode 負責處理檔案內容的讀寫請求, 跟檔案內容相關的資料流不經過 NameNode, 只詢問它跟哪個 DataNode聯絡, 否則 NameNode 會成為系統的瓶頸

  3. 副本存放在哪些 DataNode 上由 NameNode 來控制, 根據全域性情況作出塊放置決定, 讀取檔案時 NameNode 儘量讓使用者先讀取最近的副本, 降低讀取網路開銷和讀取延時

  4. NameNode 全權管理資料庫的複製, 它週期性的從叢集中的每個DataNode 接收心跳信合和狀態報告, 接收到心跳訊號意味著 DataNode 節點工作正常, 塊狀態報告包含了一個該 DataNode 上所有的資料列表

[圖片上傳失敗...(image-e2ad83-1622277945017)]

3.1.4 HDFS檔案副本和Block塊儲存

所有的檔案都是以 block 塊的方式存放在 HDFS 檔案系統當中, 在 Hadoop1 當中, 檔案的 block 塊預設大小是64M, hadoop2 當中, 檔案的 block 塊大小預設是 128M, block 塊的大小可以透過 hdfs-site.xml 當中的配置檔案進行指定

<property>
    <name>dfs.block.size</name>
    <value>塊大小 以位元組為單位</value></property>

(1)引入塊機制的好處

  • 一個檔案有可能大於叢集中任意一個磁碟

  • 使用塊抽象而不是檔案可以簡化儲存子系統

  • 塊非常適合用於資料備份進而提供資料容錯能力和可用性

(2)塊快取

通常 DataNode 從磁碟中讀取塊, 但對於訪問頻繁的檔案, 其對應的塊可能被顯式的快取在 DataNode 的記憶體中, 以堆外塊快取的形式存在. 預設情況下,一個塊僅快取在一個 DataNode 的記憶體中,當然可以針對每個檔案配置 DataNode 的數量. 作業排程器透過在快取塊的 DataNode 上執行任務, 可以利用塊快取的優勢提高讀操作的效能.

例如:

連線(join) 操作中使用的一個小的查詢表就是塊快取的一個很好的候選

使用者或應用透過在快取池中增加一個 Cache Directive 來告訴 NameNode 需要快取哪些檔案及存多久. 快取池(Cache Pool) 是一個擁有管理快取許可權和資源使用的管理性分組.

例如一個檔案 130M, 會被切分成 2 個 block 塊, 儲存在兩個 block 塊裡面, 實際佔用磁碟 130M 空間, 而不是佔用256M的磁碟空間

(3)HDFS 檔案許可權驗證

HDFS 的檔案許可權機制與 Linux 系統的檔案許可權機制類似

r:read  w:write  x:execute

許可權 x 對於檔案表示忽略, 對於資料夾表示是否有許可權訪問其內容 如果 Linux 系統使用者 zhangsan 使用 Hadoop 命令建立一個檔案, 那麼這個檔案在 HDFS 當中的 Owner 就是 zhangsan HDFS 檔案許可權的目的, 防止好人做錯事, 而不是阻止壞人做壞事. HDFS相信你告訴我你是誰, 你就是誰

3.1.5 HDFS 的元資訊和 SecondaryNameNode

當 Hadoop 的叢集當中, 只有一個 NameNode 的時候, 所有的後設資料資訊都儲存在了 FsImage 與 Eidts 檔案當中, 這兩個檔案就記錄了所有的資料的後設資料資訊, 後設資料資訊的儲存目錄配置在了 hdfs-site.xml 當中

<property>
  <name>dfs.namenode.name.dir</name>
  <value>file:///export/servers/hadoop-3.1.1/datas/namenode/namenodedatas</value></property><property>
  <name>dfs.namenode.edits.dir</name>
  <value>file:///export/servers/hadoop-3.1.1/datas/dfs/nn/edits</value></property>

(1) FsImage 和 Edits 詳解

  • editsedits 存放了客戶端最近一段時間的操作日誌客戶端對 HDFS 進行寫檔案時會首先被記錄在 edits 檔案中edits 修改時後設資料也會更新每次 HDFS 更新時 edits 先更新後客戶端才會看到最新資訊

  • fsimageNameNode 中關於後設資料的映象, 一般稱為檢查點, fsimage 存放了一份比較完整的後設資料資訊因為 fsimage 是 NameNode 的完整的映象, 如果每次都載入到記憶體生成樹狀拓撲結構,這是非常耗記憶體和CPU, 所以一般開始時對 NameNode 的操作都放在 edits 中fsimage 內容包含了 NameNode 管理下的所有 DataNode 檔案及檔案 block 及 block 所在的 DataNode 的後設資料資訊.隨著 edits 內容增大, 就需要在一定時間點和 fsimage 合併

(2)fsimage 中的檔案資訊檢視

官方檢視文件

使用命令 hdfs oiv

cd /export/servers/hadoop-3.1.1/datas/namenode/namenodedatas
hdfs oiv -i fsimage_0000000000000000864 -p XML -o hello.xml

(3) edits 中的檔案資訊檢視

官方檢視文件

使用命令 hdfs oev

cd /export/servers/hadoop-3.1.1/datas/dfs/nn/edits
hdfs oev -i  edits_0000000000000000865-0000000000000000866 -o myedit.xml -p XML

(4) SecondaryNameNode 如何輔助管理 fsimage 與 edits 檔案?

  • SecondaryNameNode 定期合併 fsimage 和 edits, 把 edits 控制在一個範圍內

  • 配置 SecondaryNameNodeSecondaryNameNode 在 conf/masters 中指定在 masters 指定的機器上, 修改 hdfs-site.xml<property> <name>dfs.http.address</name> <value>host:50070</value> </property> 修改 core-site.xml, 這一步不做配置保持預設也可以 <property> <name>fs.checkpoint.period</name> <value>3600</value> </property> <property> <name>fs.checkpoint.size</name> <value>67108864</value> </property>

  1. SecondaryNameNode 通知 NameNode 切換 editlog

  2. SecondaryNameNode 從 NameNode 中獲得 fsimage 和 editlog(透過http方式)

  3. SecondaryNameNode 將 fsimage 載入記憶體, 然後開始合併 editlog, 合併之後成為新的 fsimage

  4. SecondaryNameNode 將新的 fsimage 發回給 NameNode

  5. NameNode 用新的 fsimage 替換舊的 fsimage

(5)特點

  • 完成合並的是 SecondaryNameNode, 會請求 NameNode 停止使用 edits, 暫時將新寫操作放入一個新的檔案中 edits.new

  • SecondaryNameNode 從 NameNode 中透過 Http GET 獲得 edits, 因為要和 fsimage 合併, 所以也是透過 Http Get 的方式把 fsimage 載入到記憶體, 然後逐一執行具體對檔案系統的操作, 與 fsimage 合併, 生成新的 fsimage, 然後透過 Http POST 的方式把 fsimage 傳送給 NameNode. NameNode 從 SecondaryNameNode 獲得了 fsimage 後會把原有的 fsimage 替換為新的 fsimage, 把 edits.new 變成 edits. 同時會更新 fstime

  • Hadoop 進入安全模式時需要管理員使用 dfsadmin 的 save namespace 來建立新的檢查點

  • SecondaryNameNode 在合併 edits 和 fsimage 時需要消耗的記憶體和 NameNode 差不多, 所以一般把 NameNode 和 SecondaryNameNode 放在不同的機器上

3.1.6 HDFS 檔案寫入過程

  1. Client 發起檔案上傳請求, 透過 RPC 與 NameNode 建立通訊, NameNode 檢查目標檔案是否已存在, 父目錄是否存在, 返回是否可以上傳

  2. Client 請求第一個 block 該傳輸到哪些 DataNode 伺服器上

  3. NameNode 根據配置檔案中指定的備份數量及機架感知原理進行檔案分配, 返回可用的 DataNode 的地址如: A, B, C

  4. Hadoop 在設計時考慮到資料的安全與高效, 資料檔案預設在 HDFS 上存放三份, 儲存策略為本地一份, 同機架內其它某一節點上一份, 不同機架的某一節點上一份。

  5. Client 請求 3 臺 DataNode 中的一臺 A 上傳資料(本質上是一個 RPC 呼叫,建立 pipeline ), A 收到請求會繼續呼叫 B, 然後 B 呼叫 C, 將整個 pipeline 建立完成, 後逐級返回 client

  6. Client 開始往 A 上傳第一個 block(先從磁碟讀取資料放到一個本地記憶體快取), 以 packet 為單位(預設64K), A 收到一個 packet 就會傳給 B, B 傳給 C. A 每傳一個 packet 會放入一個應答佇列等待應答

  7. 資料被分割成一個個 packet 資料包在 pipeline 上依次傳輸, 在 pipeline 反方向上, 逐個傳送 ack(命令正確應答), 最終由 pipeline 中第一個 DataNode 節點 A 將 pipelineack 傳送給 Client

  8. 當一個 block 傳輸完成之後, Client 再次請求 NameNode 上傳第二個 block 到服務 1

3.1.7. HDFS 檔案讀取過程

  1. Client向NameNode發起RPC請求,來確定請求檔案block所在的位置;

  2. NameNode會視情況返回檔案的部分或者全部block列表,對於每個block,NameNode 都會返回含有該 block 副本的 DataNode 地址; 這些返回的 DN 地址,會按照叢集拓撲結構得出 DataNode 與客戶端的距離,然後進行排序,排序兩個規則:網路拓撲結構中距離 Client 近的排靠前;心跳機制中超時彙報的 DN 狀態為 STALE,這樣的排靠後;

  3. Client 選取排序靠前的 DataNode 來讀取 block,如果客戶端本身就是DataNode,那麼將從本地直接獲取資料(短路讀取特性);

  4. 底層上本質是建立 Socket Stream(FSDataInputStream),重複的呼叫父類 DataInputStream 的 read 方法,直到這個塊上的資料讀取完畢;

  5. 當讀完列表的 block 後,若檔案讀取還沒有結束,客戶端會繼續向NameNode 獲取下一批的 block 列表;

  6. 讀取完一個 block 都會進行 checksum 驗證,如果讀取 DataNode 時出現錯誤,客戶端會通知 NameNode,然後再從下一個擁有該 block 副本的DataNode 繼續讀。

  7. read 方法是並行的讀取 block 資訊,不是一塊一塊的讀取;NameNode 只是返回Client請求包含塊的DataNode地址,並不是返回請求塊的資料;

  8. 最終讀取來所有的 block 會合併成一個完整的最終檔案。

3.1.8. HDFS 的 API 操作

(1)匯入 Maven 依賴

<repositories>
    <repository>
        <id>cloudera</id>
        <url>
    </repository></repositories><dependencies>
  <dependency>
      <groupId>jdk.tools</groupId>
      <artifactId>jdk.tools</artifactId>
      <version>1.8</version>
      <scope>system</scope>
      <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-common</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-hdfs</artifactId>
      <version>3.0.0</version>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-hdfs-client</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client</artifactId>
      <version>3.0.0</version>
  </dependency>
  <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
  </dependency></dependencies>

(2)概述

在 Java 中操作 HDFS, 主要涉及以下 Class:

  • Configuration該類的物件封轉了客戶端或者伺服器的配置

  • FileSystem該類的物件是一個檔案系統物件, 可以用該物件的一些方法來對檔案進行操作, 透過 FileSystem 的靜態方法 get 獲得該物件FileSystem fs = FileSystem.get(conf) get 方法從 conf 中的一個引數 fs.defaultFS 的配置值判斷具體是什麼型別的檔案系統如果我們的程式碼中沒有指定 fs.defaultFS, 並且工程 ClassPath 下也沒有給定相應的配置, conf 中的預設值就來自於 Hadoop 的 Jar 包中的 core-default.xml預設值為 file:///, 則獲取的不是一個 DistributedFileSystem 的例項, 而是一個本地檔案系統的客戶端物件

(3)獲取 FileSystem 的幾種方式

  • 第一種方式
@Testpublic void getFileSystem() throws URISyntaxException, IOException {
    Configuration configuration = new Configuration();
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), configuration);
    System.out.println(fileSystem.toString());
}
  • 第二種方式
@Testpublic void getFileSystem2() throws URISyntaxException, IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS","hdfs://192.168.52.250:8020");
    FileSystem fileSystem = FileSystem.get(new URI("/"), configuration);
    System.out.println(fileSystem.toString());
}
  • 第三種方式
@Testpublic void getFileSystem3() throws URISyntaxException, IOException {
    Configuration configuration = new Configuration();
    FileSystem fileSystem = FileSystem.newInstance(new URI("hdfs://192.168.52.250:8020"), configuration);
    System.out.println(fileSystem.toString());
}
  • 第四種方式
@Testpublic void getFileSystem4() throws  Exception{
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS","hdfs://192.168.52.250:8020");
    FileSystem fileSystem = FileSystem.newInstance(configuration);
    System.out.println(fileSystem.toString());
}

(4)遍歷 HDFS 中所有檔案

  • 遞迴遍歷
@Testpublic void listFile() throws Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.100:8020"), new Configuration());
    FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));    for (FileStatus fileStatus : fileStatuses) {        if(fileStatus.isDirectory()){
            Path path = fileStatus.getPath();
            listAllFiles(fileSystem,path);
        }else{
            System.out.println("檔案路徑為"+fileStatus.getPath().toString());
        }
    }
}public void listAllFiles(FileSystem fileSystem,Path path) throws  Exception{
    FileStatus[] fileStatuses = fileSystem.listStatus(path);    for (FileStatus fileStatus : fileStatuses) {        if(fileStatus.isDirectory()){
            listAllFiles(fileSystem,fileStatus.getPath());
        }else{
            Path path1 = fileStatus.getPath();
            System.out.println("檔案路徑為"+path1);
        }
    }
}
  • 使用 API 遍歷
@Testpublic void listMyFiles()throws Exception{    //獲取fileSystem類
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());    //獲取RemoteIterator 得到所有的檔案或者資料夾,第一個引數指定遍歷的路徑,第二個參數列示是否要遞迴遍歷
    RemoteIterator<LocatedFileStatus> locatedFileStatusRemoteIterator = fileSystem.listFiles(new Path("/"), true);    while (locatedFileStatusRemoteIterator.hasNext()){
        LocatedFileStatus next = locatedFileStatusRemoteIterator.next();
        System.out.println(next.getPath().toString());
    }
    fileSystem.close();
}

(5)下載檔案到本地

@Testpublic void getFileToLocal()throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());
    FSDataInputStream open = fileSystem.open(new Path("/test/input/install.log"));
    FileOutputStream fileOutputStream = new FileOutputStream(new File("c:\\install.log"));
    IOUtils.copy(open,fileOutputStream );
    IOUtils.closeQuietly(open);
    IOUtils.closeQuietly(fileOutputStream);
    fileSystem.close();
}

(6)HDFS 上建立資料夾

@Testpublic void mkdirs() throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());    boolean mkdirs = fileSystem.mkdirs(new Path("/hello/mydir/test"));
    fileSystem.close();
}

(7)HDFS 檔案上傳

@Testpublic void putData() throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());
    fileSystem.copyFromLocalFile(new Path("file:///c:\\install.log"),new Path("/hello/mydir/test"));
    fileSystem.close();
}

(8)偽造使用者

  1. 停止hdfs叢集,在node01機器上執行以下命令
cd /export/servers/hadoop-3.1.1
sbin/stop-dfs.sh
  1. 修改node01機器上的hdfs-site.xml當中的配置檔案
cd /export/servers/hadoop-3.1.1/etc/hadoop
vim hdfs-site.xml<property>
    <name>dfs.permissions.enabled</name>
    <value>true</value></property>
  1. 修改完成之後配置檔案傳送到其他機器上面去
scp hdfs-site.xml node02:$PWDscp hdfs-site.xml node03:$PWD
  1. 重啟hdfs叢集
cd /export/servers/hadoop-3.1.1
sbin/start-dfs.sh
  1. 隨意上傳一些檔案到我們hadoop叢集當中準備測試使用
cd /export/servers/hadoop-3.1.1/etc/hadoop
hdfs dfs -mkdir /config
hdfs dfs -put *.xml /config
hdfs dfs -chmod 600 /config/core-site.xml
  1. 使用程式碼準備下載檔案
@Testpublic void getConfig()throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration(),"hadoop");
    fileSystem.copyToLocalFile(new Path("/config/core-site.xml"),new Path("file:///c:/core-site.xml"));
    fileSystem.close();
}

(9)小檔案合併

由於 Hadoop 擅長儲存大檔案,因為大檔案的後設資料資訊比較少,如果 Hadoop 叢集當中有大量的小檔案,那麼每個小檔案都需要維護一份後設資料資訊,會大大的增加叢集管理後設資料的記憶體壓力,所以在實際工作當中,如果有必要一定要將小檔案合併成大檔案進行一起處理

在我們的 HDFS 的 Shell 命令模式下,可以透過命令列將很多的 hdfs 檔案合併成一個大檔案下載到本地

cd /export/servers
hdfs dfs -getmerge /config/*.xml ./hello.xml

既然可以在下載的時候將這些小檔案合併成一個大檔案一起下載,那麼肯定就可以在上傳的時候將小檔案合併到一個大檔案裡面去

@Testpublic void mergeFile() throws  Exception{    //獲取分散式檔案系統
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration(),"hadoop");
    FSDataOutputStream outputStream = fileSystem.create(new Path("/bigfile.xml"));    //獲取本地檔案系統
    LocalFileSystem local = FileSystem.getLocal(new Configuration());    //透過本地檔案系統獲取檔案列表,為一個集合
    FileStatus[] fileStatuses = local.listStatus(new Path("file:///F:\\傳智播客大資料離線階段課程資料\\3、大資料離線第三天\\上傳小檔案合併"));    for (FileStatus fileStatus : fileStatuses) {
        FSDataInputStream inputStream = local.open(fileStatus.getPath());
       IOUtils.copy(inputStream,outputStream);
        IOUtils.closeQuietly(inputStream);
    }
    IOUtils.closeQuietly(outputStream);
    local.close();
    fileSystem.close();
}

04 MapReduce介紹

MapReduce思想在生活中處處可見。或多或少都曾接觸過這種思想。MapReduce的思想核心是“分而治之”,適用於大量複雜的任務處理場景(大規模資料處理場景)。

Map負責“分”,即把複雜的任務分解為若干個“簡單的任務”來並行處理。可以進行拆分的前提是這些小任務可以平行計算,彼此間幾乎沒有依賴關係。 Reduce負責“合”,即對map階段的結果進行全域性彙總。 MapReduce執行在yarn叢集

  • ResourceManager

  • NodeManager

這兩個階段合起來正是MapReduce思想的體現。

4.1 MapReduce設計思想和架構

MapReduce是一個分散式運算程式的程式設計框架,核心功能是將使用者編寫的業務邏輯程式碼和自帶預設元件整合成一個完整的分散式運算程式,併發執行在Hadoop叢集上。

Hadoop MapReduce構思: 分而治之 對相互間不具有計算依賴關係的大資料,實現並行最自然的辦法就是採取分而治之的策略。平行計算的第一個重要問題是如何劃分計算任務或者計算資料以便對劃分的子任務或資料塊同時進行計算。不可分拆的計算任務或相互間有依賴關係的資料無法進行平行計算! 統一構架,隱藏系統層細節 如何提供統一的計算框架,如果沒有統一封裝底層細節,那麼程式設計師則需要考慮諸如資料儲存、劃分、分發、結果收集、錯誤恢復等諸多細節;為此,MapReduce設計並提供了統一的計算框架,為程式設計師隱藏了絕大多數系統 層面的處理細節。 MapReduce最大的亮點在於透過抽象模型和計算框架把需要做什麼(what need to do)與具體怎麼做(how to do)分開了,為程式設計師提供一個抽象和高層的程式設計介面和框架。程式設計師僅需要關心其應用層的具體計算問題,僅需編寫少量的處理應用本身計算問題的程式程式碼。如何具體完成這個平行計算任務所相關的諸多系統層細節被隱藏起來,交給計算框架去處理:從分佈程式碼的執行,到大到數千小到單個節點叢集的自動排程使用。 構建抽象模型:Map和Reduce MapReduce借鑑了函式式語言中的思想,用Map和Reduce兩個函式提供了高層的並行程式設計抽象模型 Map: 對一組資料元素進行某種重複式的處理; Reduce: 對Map的中間結果進行某種進一步的結果整理。 Map和Reduce為程式設計師提供了一個清晰的操作介面抽象描述。MapReduce 處理的資料型別是鍵值對。 MapReduce中定義瞭如下的Map和Reduce兩個抽象的程式設計介面,由使用者去程式設計實現: Map: (k1; v1) → [(k2; v2)] Reduce: (k2; [v2]) → [(k3; v3)]

MapReduce 框架結構 一個完整的mapreduce程式在分散式執行時有三類例項程式:

  1. MRAppMaster 負責整個程式的過程排程及狀態協調

  2. MapTask 負責map階段的整個資料處理流程

  3. ReduceTask 負責reduce階段的整個資料處理流程

4.2 MapReduce程式設計規範

MapReduce 的開發一共有八個步驟, 其中 Map 階段分為 2 個步驟,Shuffle 階段 4個步驟,Reduce 階段分為 2 個步驟

Map 階段 2 個步驟

  1. 設定 InputFormat 類, 將資料切分為 Key-Value(K1和V1) 對, 輸入到第二步

  2. 自定義 Map 邏輯, 將第一步的結果轉換成另外的 Key-Value(K2和V2) 對, 輸出結果

Shuffle 階段 4 個步驟

  1. 對輸出的 Key-Value 對進行分割槽

  2. 對不同分割槽的資料按照相同的 Key 排序

  3. (可選) 對分組過的資料初步規約, 降低資料的網路複製

  4. 對資料進行分組, 相同 Key 的 Value 放入一個集合中

Reduce 階段 2 個步驟

  1. 對多個 Map 任務的結果進行排序以及合併, 編寫 Reduce 函式實現自己的邏輯, 對輸入的 Key-Value 進行處理, 轉為新的 Key-Value(K3和V3)輸出

  2. 設定 OutputFormat 處理並儲存 Reduce 輸出的 Key-Value 資料

轉換為程式碼,例子如下

Map階段

public class WordCountMapper extends Mapper<Text,Text,Text, LongWritable> {    /**
     * K1-----V1
     * A -----A
     * B -----B
     * C -----C
     *
     * K2-----V2
     * A -----1
     * B -----1
     * C -----1
     *
     * @param key
     * @param value
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(Text key, Text value, Context context) throws IOException, InterruptedException {
        context.write(key,new LongWritable(1));
    }
}

Reduce階段

public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {        long count = 0L;        for (LongWritable value : values) {
            count += value.get();
        }
        context.write(key, new LongWritable(count));
    }
}

shuffle階段,舉一個分割槽的例子:

public class WordCountPartitioner extends Partitioner<Text, LongWritable> {    @Override
    public int getPartition(Text text, LongWritable longWritable, int i) {        if (text.toString().length() > 5) {            return 1;
        }        return 0;
    }
}

主方法

public class JobMain extends Configured implements Tool {    public static void main(String[] args) throws Exception {
        ToolRunner.run(new Configuration(),new JobMain(),args);
    }    @Override
    public int run(String[] strings) throws Exception {
        Job job = Job.getInstance(super.getConf(), "wordcout");
        job.setJarByClass(JobMain.class);        //輸入
        job.setInputFormatClass(TextInputFormat.class);
        TextInputFormat.addInputPath(job,new Path("/"));        //map
        job.setMapperClass(WordCountMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);        //shuffle階段
        job.setPartitionerClass(WordCountPartitioner.class);
        job.setNumReduceTasks(2);        //reduce階段
        job.setReducerClass(WordCountReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);        //輸出
        job.setOutputFormatClass(TextOutputFormat.class);
        TextOutputFormat.setOutputPath(job,new Path("/"));        return 0;
    }
}

4.4 MapTask執行機制

image
image

具體步驟:

  1. 讀取資料元件 InputFormat(預設 TextInputFormat) 會透過 getSplits 方法對輸入目錄中檔案進行邏輯切片規劃得到 splits, 有多少個 split 就對應啟動多少個MapTask . split 與 block 的對應關係預設是一對一

  2. 將輸入檔案切分為 splits 之後, 由 RecordReader 物件 (預設是LineRecordReader)進行讀取, 以 \n 作為分隔符, 讀取一行資料, 返回 <key,value> . Key 表示每行首字元偏移值,Value 表示這一行文字內容

  3. 讀取 split 返回 <key,value> , 進入使用者自己繼承的 Mapper 類中,執行使用者重寫的 map 函式, RecordReader 讀取一行這裡呼叫一次

  4. Mapper 邏輯結束之後, 將 Mapper 的每條結果透過 context.write 進行collect資料收集. 在 collect 中, 會先對其進行分割槽處理,預設使用 HashPartitioner。

  • MapReduce 提供 Partitioner 介面, 它的作用就是根據 Key 或 Value 及Reducer 的數量來決定當前的這對輸出資料最終應該交由哪個 Reduce task處理, 預設對 Key Hash 後再以 Reducer 數量取模. 預設的取模方式只是為了平均 Reducer 的處理能力, 如果使用者自己對 Partitioner 有需求, 可以訂製並設定到 Job 上。
  1. 接下來, 會將資料寫入記憶體, 記憶體中這片區域叫做環形緩衝區, 緩衝區的作用是批次收集Mapper 結果, 減少磁碟 IO 的影響. 我們的 Key/Value 對以及 Partition 的結果都會被寫入緩衝區. 當然, 寫入之前,Key 與 Value 值都會被序列化成位元組陣列。
  • 環形緩衝區其實是一個陣列, 陣列中存放著 Key, Value 的序列化資料和 Key,Value 的後設資料資訊, 包括 Partition, Key 的起始位置, Value 的起始位置以及Value 的長度. 環形結構是一個抽象概念

  • 緩衝區是有大小限制, 預設是 100MB. 當 Mapper 的輸出結果很多時, 就可能會撐爆記憶體, 所以需要在一定條件下將緩衝區中的資料臨時寫入磁碟, 然後重新利用這塊緩衝區. 這個從記憶體往磁碟寫資料的過程被稱為 Spill, 中文可譯為溢寫. 這個溢寫是由單獨執行緒來完成, 不影響往緩衝區寫 Mapper 結果的執行緒.溢寫執行緒啟動時不應該阻止 Mapper 的結果輸出, 所以整個緩衝區有個溢寫的比例 spill.percent . 這個比例預設是 0.8, 也就是當緩衝區的資料已經達到閾值 buffer size * spill percent = 100MB * 0.8 = 80MB , 溢寫執行緒啟動,鎖定這 80MB 的記憶體, 執行溢寫過程. Mapper 的輸出結果還可以往剩下的20MB 記憶體中寫, 互不影響

  1. 當溢寫執行緒啟動後, 需要對這 80MB 空間內的 Key 做排序 (Sort). 排序是 MapReduce模型預設的行為, 這裡的排序也是對序列化的位元組做的排序如果 Job 設定過 Combiner, 那麼現在就是使用 Combiner 的時候了. 將有相同 Key 的 Key/Value 對的 Value 加起來, 減少溢寫到磁碟的資料量.Combiner 會最佳化 MapReduce 的中間結果, 所以它在整個模型中會多次使用那哪些場景才能使用 Combiner 呢? 從這裡分析, Combiner 的輸出是Reducer 的輸入, Combiner 絕不能改變最終的計算結果. Combiner 只應該用於那種 Reduce 的輸入 Key/Value 與輸出 Key/Value 型別完全一致, 且不影響最終結果的場景. 比如累加, 最大值等. Combiner 的使用一定得慎重, 如果用好, 它對 Job 執行效率有幫助, 反之會影響 Reducer 的最終結果

  2. 合併溢寫檔案, 每次溢寫會在磁碟上生成一個臨時檔案 (寫之前判斷是否有 Combiner),如果 Mapper 的輸出結果真的很大, 有多次這樣的溢寫發生, 磁碟上相應的就會有多個臨時檔案存在. 當整個資料處理結束之後開始對磁碟中的臨時檔案進行 Merge 合併, 因為最終的檔案只有一個, 寫入磁碟, 並且為這個檔案提供了一個索引檔案, 以記錄每個reduce對應資料的偏移量

4.5 ReduceTask工作機制

image
image

Reduce 大致分為 copy、sort、reduce 三個階段,重點在前兩個階段。copy 階段包含一個 eventFetcher 來獲取已完成的 map 列表,由 Fetcher 執行緒去 copy 資料,在此過程中會啟動兩個 merge 執行緒,分別為 inMemoryMerger 和 onDiskMerger,分別將記憶體中的資料 merge 到磁碟和將磁碟中的資料進行 merge。待資料 copy 完成之後,copy 階段就完成了,開始進行 sort 階段,sort 階段主要是執行 finalMerge 操作,純粹的 sort 階段,完成之後就是 reduce 階段,呼叫使用者定義的 reduce 函式進行處理

詳細步驟:

  1. Copy階段 ,簡單地拉取資料。Reduce程式啟動一些資料copy執行緒(Fetcher),透過HTTP方式請求maptask獲取屬於自己的檔案。

  2. Merge階段 。這裡的merge如map端的merge動作,只是陣列中存放的是不同map端copy來的數值。Copy過來的資料會先放入記憶體緩衝區中,這裡的緩衝區大小要比map端的更為靈活。merge有三種形式:記憶體到記憶體;記憶體到磁碟;磁碟到磁碟。預設情況下第一種形式不啟用。當記憶體中的資料量到達一定閾值,就啟動記憶體到磁碟的merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設定有Combiner,也是會啟用的,然後在磁碟中生成了眾多的溢寫檔案。第二種merge方式一直在執行,直到沒有map端的資料時才結束,然後啟動第三種磁碟到磁碟的merge方式生成最終的檔案。

  3. 合併排序 。把分散的資料合併成一個大的資料後,還會再對合並後的資料排序。

  4. 對排序後的鍵值對呼叫reduce方法 ,鍵相等的鍵值對呼叫一次reduce方法,每次呼叫會產生零個或者多個鍵值對,最後把這些輸出的鍵值對寫入到HDFS檔案中。

4.6 Shuffle具體流程

map 階段處理的資料如何傳遞給 reduce 階段,是 MapReduce 框架中最關鍵的一個流程,這個流程就叫 shuffle shuffle: 洗牌、發牌 ——(核心機制:資料分割槽,排序,分組,規約,合併等過程)

image
image
  1. Collect階段 :將 MapTask 的結果輸出到預設大小為 100M 的環形緩衝區,儲存的是key/value,Partition 分割槽資訊等。

  2. Spill階段 :當記憶體中的資料量達到一定的閥值的時候,就會將資料寫入本地磁碟,在將資料寫入磁碟之前需要對資料進行一次排序的操作,如果配置了 combiner,還會將有相同分割槽號和 key 的資料進行排序。

  3. Merge階段 :把所有溢位的臨時檔案進行一次合併操作,以確保一個 MapTask 最終只產生一箇中間資料檔案。

  4. Copy階段 :ReduceTask 啟動 Fetcher 執行緒到已經完成 MapTask 的節點上覆制一份屬於自己的資料,這些資料預設會儲存在記憶體的緩衝區中,當記憶體的緩衝區達到一定的閥值的時候,就會將資料寫到磁碟之上。

  5. Merge階段 :在 ReduceTask 遠端複製資料的同時,會在後臺開啟兩個執行緒對記憶體到本地的資料檔案進行合併操作。

  6. Sort階段 :在對資料進行合併的同時,會進行排序操作,由於 MapTask 階段已經對資料進行了區域性的排序,ReduceTask 只需保證 Copy 的資料的最終整體有效性即可。Shuffle 中的緩衝區大小會影響到 mapreduce 程式的執行效率,原則上說,緩衝區越大,磁碟io的次數越少,執行速度就越快

緩衝區的大小可以透過引數調整, 引數:mapreduce.task.io.sort.mb 預設100M


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

相關文章