Hadoop 執行在 Kubernetes平臺實踐_Kubernetes中文社群
Hadoop與Kubernetes就好像江湖裡的兩大絕世高手,一個是成名已久的長者,至今仍然名聲遠揚,一個則是初出茅廬的青澀少年,骨骼驚奇,不走尋常路,一出手便驚詫了整個武林。Hadoop與Kubernetes之間有很深的淵源,因為都出自IT豪門——Google,只不過,後者是親兒子,正因為有大佬背書,所以Kubernetes一出山,江湖各路門派便都蜂擁而至,擁護稱王。
不知道是因為Hadoop是乾兒子的緣故還是因為“廉頗老矣”,總之,Hadoop朋友圈的後輩們如Spark、Storm等早都有了在Kubernetes上部署執行的各種資料和案例,但Hadoop卻一直遊離於Kubernetes體系之外,本文我們給出Hadoop在Kubernetes上的實踐案例,以彌補這種缺憾。
Hadoop容器化的資料不少,但Hadoop部署在Kubernetes上的資料幾乎沒有,這主要是以下幾個原因導致的:
第一, Hadoop叢集重度依賴DNS機制,一些元件還使用了反向域名解析,以確定叢集中的節點身份,這對Hadoop在Kubernetes上的建模和執行帶來極大挑戰,需要深入瞭解Hadoop叢集工作原理並且精通Kubernetes,才能很好解決這一難題。
第二, Hadoop新的Map-Reduce計算框架Yarn的模型出現的比較晚,它的叢集機制要比HDFS複雜,資料也相對較少,增加了Hadoop整體建模與遷移Kubernetes平臺的難度。
第三, Hadoop與Kubernetes分別屬於兩個不同的領域,一個是傳統的大資料領域,一個是新興的容器與微服務架構領域,這兩個領域之間交集本來很小,加之Hadoop最近幾年已經失去焦點(這點從百度搜索關鍵詞就能發現),所以,沒有多少人關注和研究Hadoop在Kubernetes的部署問題,也是情理之中的事情。
Hadoop 2.0其實是由兩套完整的叢集所組成,一個是基本的HDFS檔案叢集,一個是YARN資源排程叢集,如下圖所示:
因此在Kubernetes建模之前,我們需要分別對這兩種叢集的工作機制和執行原理做出深入的分析,下圖是HDFS叢集的架構圖:
我們看到,HDFS叢集是由NameNode(Master節點)和Datanode(資料節點)等兩類節點所組成,其中,客戶端程式(Client)以及DataNode節點會訪問NameNode,因此,NameNode節點需要建模為Kubernetes Service以提供服務,以下是對應的Service定義檔案:
apiVersion: v1 kind: Service metadata: name: k8s-hadoop-master spec: type: NodePort selector: app: k8s-hadoop-master ports: - name: rpc port: 9000 targetPort: 9000 - name: http port: 50070 targetPort: 50070 nodePort: 32007
其中,NameNode節點暴露2個服務埠:
- 9000埠用於內部IPC通訊,主要用於獲取檔案的元資料
- 50070埠用於HTTP服務,為Hadoop 的Web管理使用
為了減少Hadoop映象的數量,我們構建了一個映象,並且通過容器的環境變數HADOOP_NODE_TYPE來區分不同的節點型別,從而啟動不同的Hadoop元件,下面是映象裡的啟動指令碼startnode.sh的內容:
#!/usr/bin/env bash sed -i "s/@[email protected]/$HDFS_MASTER_SERVICE/g" $HADOOP_HOME/etc/hadoop/core-site.xml sed -i "s/@[email protected]/$HDOOP_YARN_MASTER/g" $HADOOP_HOME/etc/hadoop/yarn-site.xml yarn-master HADOOP_NODE="${HADOOP_NODE_TYPE}" if [ $HADOOP_NODE = "datanode" ]; then echo "Start DataNode ..." hdfs datanode -regular else if [ $HADOOP_NODE = "namenode" ]; then echo "Start NameNode ..." hdfs namenode else if [ $HADOOP_NODE = "resourceman" ]; then echo "Start Yarn Resource Manager ..." yarn resourcemanager else if [ $HADOOP_NODE = "yarnnode" ]; then echo "Start Yarn Resource Node ..." yarn nodemanager else echo "not recoginized nodetype " fi fi fi fi
我們注意到,啟動命令裡把Hadoop配置檔案(core-site.xml與yarn-site.xml)中的HDFS Master節點地址用環境變數中的引數HDFS_MASTER_SERVICE來替換,YARN Master節點地址則用HDOOP_YARN_MASTER來替換。下圖是Hadoop HDFS 2節點叢集的完整建模示意圖:
圖中的圓圈表示Pod,可以看到,Datanode並沒有建模Kubernetes Service,而是建模為獨立的Pod,這是因為Datanode並不直接被客戶端所訪問,因此無需建模Service。當Datanode執行在Pod容器裡的時候,我們需要修改配置檔案中的以下引數,取消DataNode節點所在主機的主機名(DNS)與對應IP地址的檢查機制:
dfs.namenode.datanode.registration.ip-hostname-check=false
如果上述引數沒有修改,就會出現DataNode叢集“分裂”的假象,因為Pod的主機名無法對應Pod的IP地址,因此介面會顯示2個節點,這兩個節點都狀態都為異常狀態。
下面是HDFS Master節點Service對應的Pod定義:
apiVersion: v1 kind: Pod metadata: name: k8s-hadoop-master labels: app: k8s-hadoop-master spec: containers: - name: k8s-hadoop-master image: kubeguide/hadoop imagePullPolicy: IfNotPresent ports: - containerPort: 9000 - containerPort: 50070 env: - name: HADOOP_NODE_TYPE value: namenode - name: HDFS_MASTER_SERVICE valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDFS_MASTER_SERVICE - name: HDOOP_YARN_MASTER valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDOOP_YARN_MASTER restartPolicy: Always
下面是HDFS的Datanode的節點定義(hadoop-datanode-1):
apiVersion: v1 kind: Pod metadata: name: hadoop-datanode-1 labels: app: hadoop-datanode-1 spec: containers: - name: hadoop-datanode-1 image: kubeguide/hadoop imagePullPolicy: IfNotPresent ports: - containerPort: 9000 - containerPort: 50070 env: - name: HADOOP_NODE_TYPE value: datanode - name: HDFS_MASTER_SERVICE valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDFS_MASTER_SERVICE - name: HDOOP_YARN_MASTER valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDOOP_YARN_MASTER restartPolicy: Always
實際上,Datanode可以用DaemonSet方式在每個Kubernerntes節點上部署一個,在這裡為了清晰起見,就沒有用這個方式 定義。接下來,我們來看看Yarn框架如何建模,下圖是Yarn框架的叢集架構圖:
我們看到,Yarn叢集中存在兩種角色的節點:ResourceManager以及NodeManger,前者屬於Yarn叢集的頭腦(Master),後者是工作承載節點(Work Node),這個架構雖然與HDFS很相似,但因為一個重要細節的差別,無法沿用HDFS的建模方式,這個細節就是Yarn叢集中的ResourceManager要對NodeManger節點進行嚴格驗證,即NodeManger節點的節點所在主機的主機名(DNS)與對應IP地址嚴格匹配,簡單來說,就是要符合如下規則:
NodeManger建立TCP連線時所用的IP地址,必須是該節點主機名對應的IP地址,即主機DNS名稱解析後返回節點的IP地址。
所以我們採用了Kubernetes裡較為特殊的一種Service——Headless Service來解決這個問題,即為每個NodeManger節點建模一個Headless Service與對應的Pod,下面是一個ResourceManager與兩個NodeManger節點所組成的Yarn叢集的建模示意圖:
Headless Service的特殊之處在於這種Service沒有分配Cluster IP,在Kuberntes DNS裡Ping這種Service的名稱時,會返回後面對應的Pod的IP地址,如果後面有多個Pod例項,則會隨機輪詢返回其中一個的Pod地址,我們用Headless Service建模NodeManger的時候,還有一個細節需要注意,即Pod的名字(容器的主機名)必須與對應的Headless Service的名字一樣,這樣一來,當執行在容器裡的NodeManger程序向ResourceManager發起TCP連線的過程中會用到容器的主機名,而這個主機名恰好是NodeManger Service的服務名,而這個服務名解析出來的IP地址又剛好是容器的IP地址,這樣一來,就巧妙的解決了Yarn叢集的DNS限制問題。
下面以yarn-node-1為例,給出對應的Service與Pod的YAM檔案,首先是yarn-node-1對應的Headless Service的YAM定義:
apiVersion: v1 kind: Service metadata: name: yarn-node-1 spec: clusterIP: None selector: app: yarn-node-1 ports: - port: 8040
注意到定義中“clusterIP:None”這句話,表明這是一個Headless Service,沒有自己的Cluster IP地址,下面給出YAM檔案定義:
apiVersion: v1 kind: Pod metadata: name: yarn-node-1 labels: app: yarn-node-1 spec: containers: - name: yarn-node-1 image: kubeguide/hadoop imagePullPolicy: IfNotPresent ports: - containerPort: 8040 - containerPort: 8041 - containerPort: 8042 env: - name: HADOOP_NODE_TYPE value: yarnnode - name: HDFS_MASTER_SERVICE valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDFS_MASTER_SERVICE - name: HDOOP_YARN_MASTER valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDOOP_YARN_MASTER restartPolicy: Always
ResourceManager的YAML定義沒有什麼特殊的地方,其中Service定義如下:
apiVersion: v1 kind: Service metadata: name: ku8-yarn-master spec: type: NodePort selector: app: yarn-master ports: - name: "8030" port: 8030 - name: "8031" port: 8031 - name: "8032" port: 8032 - name: http port: 8088 targetPort: 8088 nodePort: 32088
對應的Pod定義如下:
apiVersion: v1 kind: Pod metadata: name: yarn-master labels: app: yarn-master spec: containers: - name: yarn-master image: kubeguide/hadoop imagePullPolicy: IfNotPresent ports: - containerPort: 9000 - containerPort: 50070 env: - name: HADOOP_NODE_TYPE value: resourceman - name: HDFS_MASTER_SERVICE valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDFS_MASTER_SERVICE - name: HDOOP_YARN_MASTER valueFrom: configMapKeyRef: name: ku8-hadoop-conf key: HDOOP_YARN_MASTER restartPolicy: Always
目前這個方案,還遺留了一個問題有待解決:HDFS NameNode節點重啟後的檔案系統格式化問題,這個問題可以通過啟動指令碼來解決,即判斷HDFS檔案系統是否已經格式化過,如果沒有,就啟動時候執行格式化命令,否則跳過格式化命令。
安裝完畢後,我們可以通過瀏覽器訪問Hadoop的HDFS管理介面,點選主頁上的Overview頁籤會顯示我們熟悉的HDFS介面:
切換到Datanodes頁籤,可以看到每個Datanodes的的資訊以及當前狀態:
接下來,我們可以登入到NameNode所在的Pod裡並執行HDSF命令進行功能性驗證,下面的命令執行結果是建立一個HDFS目錄,並且上傳一個檔案到此目錄中:
[email protected]:/usr/local/hadoop/bin# hadoop fs -ls / [email protected]:/usr/local/hadoop/bin# hadoop fs -mkdir /leader-us [email protected]:/usr/local/hadoop/bin# hadoop fs -ls / Found 1 items drwxr-xr-x - root supergroup 0 2017-02-17 07:32 /leader-us [email protected]:/usr/local/hadoop/bin# hadoop fs -put hdfs.cmd /leader-us
然後,我們可以在HDFS管理介面中瀏覽HDFS檔案系統,驗證剛才的操作結果:
接下來,我們再登入到hadoop-master對應的Pod上,啟動一個Map-Reduce測試作業——wordcount,作業啟動後,我們可以在Yarn的管理介面中看到作業的執行資訊,如下圖所示:
當作業執行完成後,可以通過介面看到詳細的統計資訊,比如wordcount的執行結果如下圖所示:
最後,我們進行了裸機版Hadoop叢集與Kubernetes之上的Hadoop叢集的效能對比測試,測試環境為十臺伺服器組成的叢集,具體引數如下:
硬體:
- CPU:2*E5-2640v3-8Core
- 記憶體:16*16G DDR4
- 網絡卡:2*10GE多模光口
- 硬碟:12*3T SATA
軟體:
- BigCloud Enterprise Linux 7(GNU/Linux 3.10.0-514.el7.x86_64 x86_64)
- Hadoop2.7.2
- Kubernetes 1.7.4+ Calico V3.0.1
我們執行了以下這些標準測試項:
- TestDFSIO:分散式系統讀寫測試
- NNBench:NameNode測試
- MRBench:MapReduce測試
- WordCount:單詞頻率統計任務測試
- TeraSort:TeraSort任務測試
綜合測試下來,Hadoop跑在Kuberntes叢集上時,效能有所下降,以TestDFSIO的測試為例,下面是Hadoop叢集檔案讀取的效能測試對比:
我們看到,Kubernetes叢集上的檔案讀效能與物理機相比,下降了差不多30%左右,並且任務執行時間也增加不少,再來對比檔案寫入的效能,測試結果如下下圖所示:
我們看到,寫檔案效能的差距並不大,這裡的主要原因是在測試過程中,HDFS寫磁碟的速度遠遠低於讀磁碟的速度,因此無法拉開差距。
之所以部署在Kuberntes上的Hadoop叢集的效能會有所下降,主要一個原因是容器虛擬網路所帶來的效能損耗,如果用Host Only模型,則兩者之間的差距會進一步縮小,下圖是TestDFSIO測試中Hadoop叢集檔案讀取的效能測試對比:
因此我們建議在生產環境中採用Host Only的網路模型,以提升Hadoop的叢集效能。
攻下Hadoop在Kubernetes上的部署,並且在生產中加以驗證,我們可以很自豪的說,現在沒有什麼能夠難倒應用向Kubernetes的遷移的步伐,採用統一的PaaS構建企業的應用叢集和大資料叢集,實現資源的共享和服務的統一管理將會大大的提升企業的業務部署速度和管理的效率。