大數據開發面試的總結-第四篇

1、hdfs讀寫文件的機制

(1) HDFS集羣角色:NameNode、DataNode

HDFS集羣分爲兩大角色:NameNode、DataNode
NameNode負責管理整個文件系統的元數據
DataNode 負責管理用戶的文件數據塊
文件會按照固定的大小(blocksize)切成若干塊後分布式存儲在若干臺datanode上
每一個文件塊可以有多個副本,並存放在不同的datanode上
Datanode會定期向Namenode彙報自身所保存的文件block信息,而namenode則會負責保持文件的副本數量
HDFS的內部工作機制對客戶端保持透明,客戶端請求訪問HDFS都是通過向namenode申請來進行

(2) HDFS寫數據流程

1)概述
客戶端要向HDFS寫數據,首先要跟namenode通信以確認可以寫文件並獲得接收文件block的datanode,然後,客戶端按順序將文件逐個block傳遞給相應datanode,並由接收到block的datanode負責向其他datanode複製block的副本
2) 詳細步驟圖
在這裏插入圖片描述
3) 詳細步驟解析
1、根namenode通信請求上傳文件,namenode檢查目標文件是否已存在,父目錄是否存在
2、namenode返回是否可以上傳
3、client請求第一個 block該傳輸到哪些datanode服務器上
4、namenode返回3個datanode服務器ABC
5、client請求3臺dn中的一臺A上傳數據(本質上是一個RPC調用,建立pipeline),A收到請求會繼續調用B,然後B調用C,將真個pipeline建立完成,逐級返回客戶端
6、client開始往A上傳第一個block(先從磁盤讀取數據放到一個本地內存緩存),以packet爲單位,A收到一個packet就會傳給B,B傳給C;A每傳一個packet會放入一個應答隊列等待應答
7、當一個block傳輸完成之後,client再次請求namenode上傳第二個block的服務器。

(3)HDFS讀數據流程

1) 概述
客戶端將要讀取的文件路徑發送給namenode,namenode獲取文件的元信息(主要是block的存放位置信息)返回給客戶端,客戶端根據返回的信息找到相應datanode逐個獲取文件的block並在客戶端本地進行數據追加合併從而獲得整個文件
2) 詳細步驟圖
在這裏插入圖片描述

3) 詳細步驟解析
1、跟namenode通信查詢元數據,找到文件塊所在的datanode服務器
2、挑選一臺datanode(就近原則,然後隨機)服務器,請求建立socket流
3、datanode開始發送數據(從磁盤裏面讀取數據放入流,以packet爲單位來做校驗)
4、客戶端以packet爲單位接收,先在本地緩存,然後寫入目標文件

2、spark和MapReduce的關係

目前的大數據處理可以分爲以下三個類型:
複雜的批量數據處理(batch data processing),通常的時間跨度在數十分鐘到數小時之間;
基於歷史數據的交互式查詢(interactive query),通常的時間跨度在數十秒到數分鐘之間;
基於實時數據流的數據處理(streaming data processing),通常的時間跨度在數百毫秒到數秒之間。
大數據處理勢必需要依賴集羣環境,而集羣環境有三大挑戰,分別是並行化、單點失敗處理、資源共享,分別可以採用以並行化的方式重寫應用程序、對單點失敗的處理方式、動態地進行計算資源的分配等解決方案來面對挑戰。

(1)MapReduce

MapReduce 是爲 Apache Hadoop 量身訂做的,它非常適用於 Hadoop 的使用場景,即大規模日誌處理系統、批量數據提取加載工具 (ETL 工具) 等類似操作。但是伴隨着 Hadoop 地盤的不斷擴張,Hadoop 的開發者們發現 MapReduce 在很多場景下並不是最佳選擇,於是 Hadoop 開始把資源管理放入到自己獨立的組件 YARN 裏面。此外,類似於 Impala 這樣的項目也開始逐漸進入到我們的架構中,Impala 提供 SQL 語義,能查詢存儲在 Hadoop 的 HDFS 和 HBase 中的 PB 級大數據。之前也有類似的項目,例如 Hive。Hive 系統雖然也提供了 SQL 語義,但由於 Hive 底層執行使用的是 MapReduce 引擎,仍然是一個批處理過程,難以滿足查詢的交互性。相比之下,Impala 的最大特點也是最大賣點就是它的效率。

第一代 Hadoop MapReduce 是一個在計算機集羣上分佈式處理海量數據集的軟件框架,包括一個 JobTracker 和一定數量的 TaskTracker。運行流程圖如圖 1 所示。
在這裏插入圖片描述
圖 1. MapReduce 運行流程圖
在最上層有 4 個獨立的實體,即客戶端、jobtracker、tasktracker 和分佈式文件系統。客戶端提交 MapReduce 作業;jobtracker 協調作業的運行;jobtracker 是一個 Java 應用程序,它的主類是 JobTracker;tasktracker 運行作業劃分後的任務,tasktracker 也是一個 Java 應用程序,它的主類是 TaskTracker。Hadoop 運行 MapReduce 作業的步驟主要包括提交作業、初始化作業、分配任務、執行任務、更新進度和狀態、完成作業等 6 個步驟。

(2)Spark 簡介

Spark 生態系統的目標就是將批處理、交互式處理、流式處理融合到一個軟件框架內。Spark 是一個基於內存計算的開源的集羣計算系統,目的是讓數據分析更加快速。Spark 非常小巧玲瓏,由加州伯克利大學 AMP 實驗室的 Matei 爲主的小團隊所開發。使用的語言是 Scala,項目的 core 部分的代碼只有 63 個 Scala 文件,非常短小精悍。Spark 啓用了內存分佈數據集,除了能夠提供交互式查詢外,它還可以優化迭代工作負載。Spark 提供了基於內存的計算集羣,在分析數據時將數據導入內存以實現快速查詢,速度比基於磁盤的系統,如 Hadoop 快很多。Spark 最初是爲了處理迭代算法,如機器學習、圖挖掘算法等,以及交互式數據挖掘算法而開發的。在這兩種場景下,Spark 的運行速度可以達到 Hadoop 的幾百倍。

Spark 允許應用在內存中保存工作集以便高效地重複利用,它支持多種數據處理應用,同時也保持了 MapReduce 的重要特性,如高容錯性、數據本地化、大規模數據處理等。此外,提出了彈性分佈式數據集 (Resilient Distributed Datasets) 的概念:
RDD 表現爲一個 Scala 對象,可由一個文件創建而來;
分佈在一個集羣內的,不可變的對象切分集;
通過並行處理(map、filter、groupby、join)固定數據(BaseRDD)創建模型,生成 Transformed RDD;
故障時可使用 RDD 血統信息重建;
可高速緩存,以便再利用。
圖 2 所示是一個日誌挖掘的示例代碼,首先將日誌數據中的 error 信息導入內存,然後進行交互搜索。
在這裏插入圖片描述
圖 2. RDD 代碼示例
在導入數據時,模型以 block 形式存在於 worker 上,由 driver 向 worker 分發任務,處理完後 work 向 driver 反饋結果。也可在 work 上對數據模型建立高速緩存 cache,對 cache 的處理過程與 block 類似,也是一個分發、反饋的過程。

Spark 的 RDD 概念能夠取得和專有系統同樣的性能,還能提供包括容錯處理、滯後節點處理等這些專有系統缺乏的特性。如下:

迭代算法:這是目前專有系統實現的非常普遍的一種應用場景,比如迭代計算可以用於圖處理和機器學習。RDD 能夠很好地實現這些模型,包括 Pregel、HaLoop 和 GraphLab 等模型。

關係型查詢:對於 MapReduce 來說非常重要的需求就是運行 SQL 查詢,包括長期運行、數小時的批處理作業和交互式的查詢。然而對於 MapReduce 而言,對比並行數據庫進行交互式查詢,有其內在的缺點,比如由於其容錯的模型而導致速度很慢。利用 RDD 模型,可以通過實現許多通用的數據庫引擎特性,從而獲得很好的性能。

MapReduce 批處理:RDD 提供的接口是 MapReduce 的超集,所以 RDD 能夠有效地運行利用 MapReduce 實現的應用程序,另外 RDD 還適合更加抽象的基於 DAG 的應用程序。

流式處理:目前的流式系統也只提供了有限的容錯處理,需要消耗系統非常大的拷貝代碼或者非常長的容錯時間。特別是在目前的系統中,基本都是基於連續計算的模型,常住的有狀態的操作會處理到達的每一條記錄。爲了恢復失敗的節點,它們需要爲每一個操作複製兩份操作,或者將上游的數據進行代價較大的操作重放,利用 RDD 實現離散數據流,可以克服上述問題。離散數據流將流式計算當作一系列的短小而確定的批處理操作,而不是常駐的有狀態的操作,將兩個離散流之間的狀態保存在 RDD 中。離散流模型能夠允許通過 RDD 的繼承關係圖進行並行性的恢復而不需要進行數據拷貝。

(3)Spark 內部進程術語解釋

Application:基於 Spark 的用戶程序,包含了 driver 程序和集羣上的 executor;
Driver Program:運行 main 函數並且新建 SparkContext 的程序;
Cluster Manager:在集羣上獲取資源的外部服務 (例如:standalone,Mesos,Yarn);
Worker Node:集羣中任何可以運行應用代碼的節點;
Executor:是在一個 worker node 上爲某應用啓動的一個進程,該進程負責運行任務,並且負責將數據存在內存或者磁盤上。每個應用都有各自獨立的 executors;
Task:被送到某個 executor 上的工作單元;
Job:包含很多任務的並行計算,可以與 Spark 的 action 對應;
Stage:一個 Job 會被拆分很多組任務,每組任務被稱爲 Stage(就像 Mapreduce 分 map 任務和 reduce 任務一樣)。

(4)MapReduce對比Spark的實現方式

場景:計算一個文本文件裏每一行的字符數量。在 Hadoop MapReduce 裏,我們需要爲 Mapper 方法準備一個鍵值對,key 用作行的行數,value 的值是這一行的字符數量。

清單 1. MapReduce 方式 Map 函數

public class LineLengthCountMapper
 extends Mapper<LongWritable,Text,IntWritable,IntWritable> {
 @Override
 protected void map(LongWritable lineNumber, Text line, Context context)
 throws IOException, InterruptedException {
 context.write(new IntWritable(line.getLength()), new IntWritable(1));
 }
}

清單 1 所示代碼,由於 Mappers 和 Reducers 只處理鍵值對,所以對於類 LineLengthCountMapper 而言,輸入是 TextInputFormat 對象,它的 key 由行數提供,value 就是該行所有字符。換成 Spark 之後的代碼如清單 2 所示。

清單 2. Spark 方式 Map 函數

lines.map(line => (line.length, 1))

在 Spark 裏,輸入是彈性分佈式數據集 (Resilient Distributed Dataset),Spark 不需要 key-value 鍵值對,代之的是 Scala 元祖 (tuple),它是通過 (line.length, 1) 這樣的 (a,b) 語法創建的。以上代碼中 map() 操作是一個 RDD,(line.length, 1) 元祖。當一個 RDD 包含元祖時,它依賴於其他方法,例如 reduceByKey(),該方法對於重新生成 MapReduce 特性具有重要意義。

清單 3 所示代碼是 Hadoop MapReduce 統計每一行的字符數,然後以 Reduce 方式輸出。

清單 3. MapReduce 方式 Reduce 函數

public class LineLengthReducer
 extends Reducer<IntWritable,IntWritable,IntWritable,IntWritable> {
 @Override
 protected void reduce(IntWritable length, Iterable<IntWritable> counts, Context context)
 throws IOException, InterruptedException {
 int sum = 0;
 for (IntWritable count : counts) {
 sum += count.get();
 }
 context.write(length, new IntWritable(sum));
 }
}

Spark 裏面的對應代碼如清單 12 所示。

清單 4. Spark 方式 Reduce 函數

val lengthCounts = lines.map(line => (line.length, 1)).reduceByKey(_ + _)

Spark 的 RDD API 有一個 reduce() 方法,它會 reduce 所有的 key-value 鍵值對到一個獨立的 value。

我們現在需要統計大寫字母開頭的單詞數量,對於文本的每一行而言,一個 Mapper 可能需要統計很多個鍵值對,代碼如清單 5 所示。

清單 5. MapReduce 方式計算字符數量

public class CountUppercaseMapper
 extends Mapper<LongWritable,Text,Text,IntWritable> {
 @Override
 protected void map(LongWritable lineNumber, Text line, Context context)
 throws IOException, InterruptedException {
 for (String word : line.toString().split(" ")) {
 if (Character.isUpperCase(word.charAt(0))) {
 context.write(new Text(word), new IntWritable(1));
 }
 }
 }
}

在 Spark 裏面,對應的代碼如清單 6所示。

清單 6. Spark 方式計算字符數量

lines.flatMap(
_.split(" ").filter(word => Character.isUpperCase(word(0))).map(word => (word,1))
)

MapReduce 依賴的 Map 方法這裏並不適用,因爲每一個輸入必須對應一個輸出,這樣的話,每一行可能佔用到很多的輸出。相反的,Spark 裏面的 Map 方法比較簡單。Spark 裏面的方法是,首先對每一行數據進行彙總後存入一個輸出結果物數組,這個數組可能是空的,也可能包含了很多的值,最終這個數組會作爲一個 RDD 作爲輸出物。這就是 flatMap() 方法的功能,它對每一行文本里的單詞轉換成函數內部的元組後進行了過濾。

在 Spark 裏面,reduceByKey() 方法可以被用來統計每篇文章裏面出現的字母數量。如果我們想統計每一篇文章裏面出現的大寫字母數量,在 MapReduce 里程序可以如清單 7所示。

清單 7. MapReduce 方式

public class CountUppercaseReducer
 extends Reducer<Text,IntWritable,Text,IntWritable> {
 @Override
 protected void reduce(Text word, Iterable<IntWritable> counts, Context context)
 throws IOException, InterruptedException {
 int sum = 0;
 for (IntWritable count : counts) {
 sum += count.get();
 }
 context.write(new Text(word.toString().toUpperCase()), new IntWritable(sum));
 }
}

在 Spark 裏,代碼如清單 8所示。

清單 8. Spark 方式
1
groupByKey().map { case (word,ones) => (word.toUpperCase, ones.sum) }
groupByKey() 方法負責收集一個 key 的所有值,不應用於一個 reduce 方法。本例中,key 被轉換成大寫字母,值被直接相加算出總和。但這裏需要注意,如果一個 key 與很多個 value 相關聯,可能會出現 Out Of Memory 錯誤。

Spark 提供了一個簡單的方法可以轉換 key 對應的值,這個方法把 reduce 方法過程移交給了 Spark,可以避免出現 OOM 異常。

> reduceByKey(_ + _).map { case (word,total) => (word.toUpperCase,total)
> }

setup() 方法在 MapReduce 裏面主要的作用是在 map 方法開始前對輸入進行處理,常用的場景是連接數據庫,可以在 cleanup() 方法中釋放在 setup() 方法裏面佔用的資源。

清單 9. MapReduce 方式

> public class SetupCleanupMapper extends
> Mapper<LongWritable,Text,Text,IntWritable> {  private Connection
> dbConnection;  @Override  protected void setup(Context context) { 
> dbConnection = ...;  }  ...  @Override  protected void cleanup(Context
> context) {  dbConnection.close();  } }

spark中不存在這樣的方式,不需要這樣處理。

3、當hdfs寫文件的時候,如果某一塊數據寫入失敗,會怎樣處理?

如果第一個備份就寫入失敗:
如果在數據寫入期間datanode發生故障,則執行以下操作(對寫入的數據的客戶端是透明的)。首先關閉管道,然後將確認隊列(ack queue)中的數據包都添加回數據隊列(data queue)的最前端,以確保故障節點下游的datanode不會漏掉任何一個數據包。爲存儲在正常的datanode上的數據塊指定一個新的標識,並將該標識傳遞給namenode,以便故障的datanode在恢復後可以刪除存儲的部分數據塊。從管線(pipeline)中刪除故障數據節點,並把使用剩下的正常的datanode構建一個新的管線(pipeline)。餘下的數據塊寫入新的管線中。namenode注意到塊副本量不足時,會在另一個節點上創建一個新的複本,後續的數據塊繼續正常接受處理。這裏接受正常處理的意思,我的理解是:因爲namenode 發現副本數小於我們配置的數目,從新找一個datanode,然後把副本數不足的數據塊都複製到新的datanode上,處理完這些數據塊後,後面的新數據塊就可以又按照管線(pipeline)的方式去處理了。

可以看出,故障的datanode會刪除,namenode會用正常的datanode寫入數據,後面再通過異步複製的方式恢復故障的datanode數據。

在一個塊被寫入期間可能會有多個datanode同時發生故障,但非常少見。只要寫入了dfs.replication.min的副本書(默認爲一),寫入操作就會成功,並且這個塊可以在集羣中異步複製,直到達到其目標複本數(默認三個複本),原因見下方:

4、向datanode寫入文件時,ACK 是否三個備份都寫成功之後才確認成功操作?

不是的,只要成功寫入的節點數量達到dfs.replication.min(默認爲1),那麼就任務是寫成功的
正常情況下:
① 在進行寫操作的時候(以默認備份3份爲例),DataNode_1接受數據後,首先將數據寫入buffer,再將數據寫入DatNode_2,寫入成功後將 buffer 中的數據寫入本地磁盤,並等待ACK信息
② 重複上一個步驟,DataNode入本地磁盤後,等待ACK信息
③ 如果ACK都成功返回後,發送給Client,本次寫入成功

如果一個節點或多個節點寫入失敗:
① 只要成功寫入的節點數量達到dfs.replication.min(默認爲1),那麼就任務是寫成功的。然後NameNode會通過異步的方式將block複製到其他節點,使數據副本達到dfs.replication參數配置的個數

5、yarn的工作機制和調度策略

(1)YARN 基本架構

YARN是Hadoop 2.0中的資源管理系統,它的基本設計思想是將MRv1中的JobTracker拆分成了兩個獨立的服務:一個全局的資源管理器ResourceManager和每個應用程序特有的ApplicationMaster。

其中ResourceManager負責整個系統的資源管理和分配,而ApplicationMaster負責單個應用程序的管理。

(2)YARN基本組成結構

YARN總體上仍然是Master/Slave結構,在整個資源管理框架中,ResourceManager爲Master,NodeManager爲Slave,ResourceManager負責對各個NodeManager上的資源進行統一管理和調度。當用戶提交一個應用程序時,需要提供一個用以跟蹤和管理這個程序的ApplicationMaster,它負責向ResourceManager申請資源,並要求NodeManger啓動可以佔用一定資源的任務。由於不同的ApplicationMaster被分佈到不同的節點上,因此它們之間不會相互影響。在本小節中,我們將對YARN的基本組成結構進行介紹。

下圖描述了YARN的基本組成結構,YARN主要由ResourceManager、NodeManager、ApplicationMaster(圖中給出了MapReduce和MPI兩種計算框架的ApplicationMaster,分別爲MR AppMstr和MPI AppMstr)和Container等幾個組件構成。
在這裏插入圖片描述
1.ResourceManager(RM)

RM是一個全局的資源管理器,負責整個系統的資源管理和分配。它主要由兩個組件構成:調度器(Scheduler)和應用程序管理器(Applications Manager,ASM)。

(1)調度器

調度器根據容量、隊列等限制條件(如每個隊列分配一定的資源,最多執行一定數量的作業等),將系統中的資源分配給各個正在運行的應用程序。

需要注意的是,該調度器是一個“純調度器”,它不再從事任何與具體應用程序相關的工作,比如不負責監控或者跟蹤應用的執行狀態等,也不負責重新啓動因應用執行失敗或者硬件故障而產生的失敗任務,這些均交由應用程序相關的ApplicationMaster完成。調度器僅根據各個應用程序的資源需求進行資源分配,而資源分配單位用一個抽象概念“資源容器”(Resource Container,簡稱Container)表示,Container是一個動態資源分配單位,它將內存、CPU、磁盤、網絡等資源封裝在一起,從而限定每個任務使用的資源量。此外,該調度器是一個可插拔的組件,用戶可根據自己的需要設計新的調度器,YARN提供了多種直接可用的調度器,比如Fair Scheduler和Capacity Scheduler等。

(2) 應用程序管理器

應用程序管理器負責管理整個系統中所有應用程序,包括應用程序提交、與調度器協商資源以啓動ApplicationMaster、監控ApplicationMaster運行狀態並在失敗時重新啓動它等。

  1. ApplicationMaster(AM)

用戶提交的每個應用程序均包含1個AM,主要功能包括:

與RM調度器協商以獲取資源(用Container表示);

將得到的任務進一步分配給內部的任務;

與NM通信以啓動/停止任務;

監控所有任務運行狀態,並在任務運行失敗時重新爲任務申請資源以重啓任務。

當前YARN自帶了兩個AM實現,一個是用於演示AM編寫方法的實例程序distributedshell,它可以申請一定數目的Container以並行運行一個Shell命令或者Shell腳本;另一個是運行MapReduce應用程序的AM—MRAppMaster,我們將在第8章對其進行介紹。此外,一些其他的計算框架對應的AM正在開發中,比如Open MPI、Spark等。

  1. NodeManager(NM)

NM是每個節點上的資源和任務管理器,一方面,它會定時地向RM彙報本節點上的資源使用情況和各個Container的運行狀態;另一方面,它接收並處理來自AM的Container啓動/停止等各種請求。
4. Container

Container是YARN中的資源抽象,它封裝了某個節點上的多維度資源,如內存、CPU、磁盤、網絡等,當AM向RM申請資源時,RM爲AM返回的資源便是用Container表示的。YARN會爲每個任務分配一個Container,且該任務只能使用該Container中描述的資源。

需要注意的是,Container不同於MRv1中的slot,它是一個動態資源劃分單位,是根據應用程序的需求動態生成的。截至本書完成時,YARN僅支持CPU和內存兩種資源,且使用了輕量級資源隔離機制Cgroups進行資源隔離。

(4)YARN工作流程

當用戶向YARN中提交一個應用程序後,YARN將分兩個階段運行該應用程序:

第一個階段是啓動ApplicationMaster;

第二個階段是由ApplicationMaster創建應用程序,爲它申請資源,並監控它的整個運行過程,直到運行完成。

如下圖所示,YARN的工作流程分爲以下幾個步驟:

 ![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200617175410220.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpbmF0XzI2NTY2MTM3,size_16,color_FFFFFF,t_70)

步驟1 用戶向YARN中提交應用程序,其中包括ApplicationMaster程序、啓動ApplicationMaster的命令、用戶程序等。

步驟2 ResourceManager爲該應用程序分配第一個Container,並與對應的Node-Manager通信,要求它在這個Container中啓動應用程序的ApplicationMaster。

步驟3 ApplicationMaster首先向ResourceManager註冊,這樣用戶可以直接通過ResourceManager查看應用程序的運行狀態,然後它將爲各個任務申請資源,並監控它的運行狀態,直到運行結束,即重複步驟4~7。

步驟4 ApplicationMaster採用輪詢的方式通過RPC協議向ResourceManager申請和領取資源。

步驟5 一旦ApplicationMaster申請到資源後,便與對應的NodeManager通信,要求它啓動任務。

步驟6 NodeManager爲任務設置好運行環境(包括環境變量、JAR包、二進制程序等)後,將任務啓動命令寫到一個腳本中,並通過運行該腳本啓動任務。

步驟7 各個任務通過某個RPC協議向ApplicationMaster彙報自己的狀態和進度,以讓ApplicationMaster隨時掌握各個任務的運行狀態,從而可以在任務失敗時重新啓動任務。

 在應用程序運行過程中,用戶可隨時通過RPC向ApplicationMaster查詢應用程序的當前運行狀態。

步驟8 應用程序運行完成後,ApplicationMaster向ResourceManager註銷並關閉自己。

(5)多角度理解YARN

在這裏插入圖片描述
可將YARN看做一個雲操作系統,它負責爲應用程序啓動ApplicationMaster(相當於主線程),然後再由ApplicationMaster負責數據切分、任務分配、啓動和監控等工作,而由ApplicationMaster啓動的各個Task(相當於子線程)僅負責自己的計算任務。當所有任務計算完成後,ApplicationMaster認爲應用程序運行完成,然後退出。

6、參考

HDFS讀寫數據的原理:https://blog.csdn.net/qq_16633405/article/details/78907070

如何將 MapReduce 轉化爲 Spark:
https://www.ibm.com/developerworks/cn/opensource/os-cn-mapreduce-spark/index.html

HDFS 從客戶端寫入到 DataNode 時,ACK 是否三個備份都寫成功之後再確認成功操作?:
https://blog.csdn.net/HeatDeath/article/details/79012340

Hadoop Yarn 框架原理及運作機制:https://blog.csdn.net/liuwenbo0920/article/details/43304243

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章