以下為面試過程中提問,崗位為大資料開發:
自我介紹+項目介紹
為什麼用 kafka、sparkstreaming、hbase?有什麼替代方案嗎?
聊聊你覺得大資料的整個體系?
你看過 hdfs 源碼?nn 的高可用說一下
zookeeper 簡單介紹一下,為什麼要用 zk?zk 的架構?zab?
hbase 的架構,讀寫緩存?
blockcache 的底層實作?你提到了 lru 那除了 lru 還可以有什麼方案?
聊聊 sparkstreaming 和 flink?flink 流批一體解釋一下?
spark 的幾種 shuffle 說下?為什麼要丢棄 hashshuffle?
java gc 可達性分析+垃圾回收器+垃圾回收算法+為什麼分代垃圾回收+調優
資料庫引擎,innodb 索引實作+聚集和非聚集差別+為什麼用 b+樹不用 hash
聊聊 tcp 和 udp 的差別
http 知道嗎?說一下
http 版本之間的比較
讓你設計一個 hash 表,怎麼設計?
時間不多了,手撸一個二分查找
答案解析
本文首發公衆号【五分鐘學大資料】,可以搜尋關注下,超多大資料精品文章
1. 自我介紹+項目介紹
自我介紹可以參考美團面試的這篇文章: 美團優選大資料開發崗面試題
2. 為什麼用 kafka、sparkstreaming、hbase?有什麼替代方案嗎?
根據履歷中寫的項目,談談為什麼用這幾個架構,是公司大資料平台曆史選擇還是更适合公司業務。
然後在說下每個架構的優點:
kafka:
高吞吐量、低延遲:kafka 每秒可以處理幾十萬條消息,它的延遲最低隻有幾毫秒;
可擴充性:kafka 叢集支援熱擴充;
持久性、可靠性:消息被持久化到本地磁盤,并且支援資料備份防止資料丢失;
容錯性:允許叢集中節點故障(若副本數量為 n,則允許 n-1 個節點故障);
高并發:支援數千個用戶端同時讀寫。
kafka 應用場景:
日志收集:一個公司可以用 kafka 可以收集各種服務的 log,通過 kafka 以統一接口服務的方式開放給各種 consumer;
消息系統:解耦生産者和消費者、緩存消息等;
使用者活動跟蹤:kafka 經常被用來記錄 web 使用者或者 app 使用者的各種活動,如浏覽網頁、搜尋、點選等活動,這些活動資訊被各個伺服器釋出到 kafka 的 topic 中,然後消費者通過訂閱這些 topic 來做實時的監控分析,亦可儲存到資料庫;
營運名額:kafka 也經常用來記錄營運監控資料。包括收集各種分布式應用的資料,生産各種操作的集中回報,比如報警和報告;
流式處理:比如 spark streaming 和 flink。
spark streaming 優點:
spark streaming 會被轉化為 spark 作業執行,由于 spark 作業依賴 dagscheduler 和 rdd,是以是粗粒度方式而不是細粒度方式,可以快速處理小批量資料,獲得準實時的特性;
以 spark 作業送出和執行,很友善的實作容錯機制;
dstreaming 是在 rdd 上的抽象,更容易與 rdd 進行互動操作。需要将流式資料與批資料結合分析的情況下,非常友善。
因為我們的業務對實時性要求不是特别高,是以使用 spark streaming 是非常合适的。
hbase 優點:
hdfs 有高容錯,高擴充的特點,而 hbase 基于 hdfs 實作資料的存儲,是以 hbase 擁有與生俱來的超強的擴充性和吞吐量。
hbase 采用的是 key/value 的存儲方式,這意味着,即便面臨海量資料的增長,也幾乎不會導緻查詢性能下降。
hbase 是一個列式資料庫,相對于于傳統的行式資料庫而言。當你的單張表字段很多的時候,可以将相同的列(以 regin 為機關)存在到不同的服務執行個體上,分散負載壓力。
有什麼替代方案,就可以聊聊和這幾個功能類似的架構,它們的優缺點等,比如 apache kafka 對應的 apache pulsar;spark streaming 對應的 flink;hbase 對應的列式資料庫可以舉幾個例子,如 cassandra、mongodb 等。
3. 聊聊你覺得大資料的整個體系?
這個是一個開放性題,把你知道的大資料架構都可以說下,下面是我做的一個 apache 大資料架構的集合圖,當然也沒有包含全部,隻是比較常見的幾個:
說的時候盡量按照它們的功能劃分及時間先後順序講解。
4. 你看過 hdfs 源碼?nn 的高可用說一下
一個 namenode 有單點故障的問題,那就配置雙 namenode,配置有兩個關鍵點,一是必須要保證這兩個 nn 的中繼資料資訊必須要同步的,二是一個 nn 挂掉之後另一個要立馬補上。
中繼資料資訊同步在 ha 方案中采用的是“共享存儲”。每次寫檔案時,需要将日志同步寫入共享存儲,這個步驟成功才能認定寫檔案成功。然後備份節點定期從共享存儲同步日志,以便進行主備切換。
監控 nn 狀态采用 zookeeper,兩個 nn 節點的狀态存放在 zk 中,另外兩個 nn 節點分别有一個程序監控程式,實施讀取 zk 中有 nn 的狀态,來判斷目前的 nn 是不是已經 down 機。如果 standby 的 nn 節點的 zkfc 發現主節點已經挂掉,那麼就會強制給原本的 active nn 節點發送強制關閉請求,之後将備用的 nn 設定為 active。
如果面試官再問 ha 中的 共享存儲 是怎麼實作的知道嗎?
可以進行解釋下:namenode 共享存儲方案有很多,比如 linux ha, vmware ft, qjm 等,目前社群已經把由 clouderea 公司實作的基于 qjm(quorum journal manager)的方案合并到 hdfs 的 trunk 之中并且作為預設的共享存儲實作
基于 qjm 的共享存儲系統主要用于儲存 editlog,并不儲存 fsimage 檔案。fsimage 檔案還是在 namenode 的本地磁盤上。qjm 共享存儲的基本思想來自于 paxos 算法,采用多個稱為 journalnode 的節點組成的 journalnode 叢集來存儲 editlog。每個 journalnode 儲存同樣的 editlog 副本。每次 namenode 寫 editlog 的時候,除了向本地磁盤寫入 editlog 之外,也會并行地向 journalnode 叢集之中的每一個 journalnode 發送寫請求,隻要大多數 (majority) 的 journalnode 節點傳回成功就認為向 journalnode 叢集寫入 editlog 成功。如果有 2n+1 台 journalnode,那麼根據大多數的原則,最多可以容忍有 n 台 journalnode 節點挂掉
注:hadoop3.x 允許使用者運作多個備用 namenode。例如,通過配置三個 namenode 和五個 journalnode,群集能夠容忍兩個節點而不是一個節點的故障。
hdfs 的其他内容可看之前寫的這篇文章: hdfs 分布式檔案系統詳解
5. zookeeper 簡單介紹一下,為什麼要用 zk?zk 的架構?zab?
zk 介紹及功能:
zookeeper 是一個分布式協調服務的開源架構。 主要用來解決分布式叢集中應用系統的一緻性問題,例如怎樣避免同時操作同一資料造成髒讀的問題。
zookeeper 本質上是一個分布式的小檔案存儲系統。提供基于類似于檔案系統的目錄樹方式的資料存儲,并且可以對樹中的節點進行有效管理。進而用來維護和監控你存儲的資料的狀态變化。通過監控這些資料狀态的變化,進而可以達到基于資料的叢集管理。 諸如: 統一命名服務(dubbo)、分布式配置管理(solr 的配置集中管理)、分布式消息隊列(sub/pub)、分布式鎖、分布式協調等功能。
zk 架構:
zk 架構圖:
leader:
zookeeper 叢集工作的核心;
事務請求(寫操作) 的唯一排程和處理者,保證叢集事務處理的順序性;
叢集内部各個伺服器的排程者。
對于 create, setdata, delete 等有寫操作的請求,則需要統一轉發給 leader 處理, leader 需要決定編号、執行操作,這個過程稱為一個事務。
follower:
處理用戶端非事務(讀操作) 請求,
轉發事務請求給 leader;
參與叢集 leader 選舉投票 2n-1 台可以做叢集投票。
此外,針對通路量比較大的 zookeeper 叢集, 還可新增觀察者角色。
observer:
觀察者角色,觀察 zookeeper 叢集的最新狀态變化并将這些狀态同步過來,其對于非事務請求可以進行獨立處理,對于事務請求,則會轉發給 leader
伺服器進行處理。
不會參與任何形式的投票隻提供非事務服務,通常用于在不影響叢集事務
處理能力的前提下提升叢集的非事務處理能力。
簡答:說白了就是增加并發的讀請求
zab 協定全稱:zookeeper atomic broadcast(zookeeper 原子廣播協定)。
zab 協定是專門為 zookeeper 實作分布式協調功能而設計。zookeeper 主要是根據 zab 協定是實作分布式系統資料一緻性。
zookeeper 根據 zab 協定建立了主備模型完成 zookeeper 叢集中資料的同步。這裡所說的主備系統架構模型是指,在 zookeeper 叢集中,隻有一台 leader 負責處理外部用戶端的事物請求(或寫操作),然後 leader 伺服器将用戶端的寫操作資料同步到所有的 follower 節點中。
6. hbase 的架構,讀寫緩存?
hbase 的架構可以看這篇文章,非常詳細: hbase 底層原理詳解
下面說下hbase 的讀寫緩存:
hbase 的 regionserver 的緩存主要分為兩個部分,分别是memstore和blockcache,其中 memstore 主要用于寫緩存,而 blockcache 用于讀緩存。
hbase 執行寫操作首先會将資料寫入 memstore,并順序寫入 hlog,等滿足一定條件後統一将 memstore 中資料重新整理到磁盤,這種設計可以極大地提升 hbase 的寫性能。
不僅如此,memstore 對于讀性能也至關重要,假如沒有 memstore,讀取剛寫入的資料就需要從檔案中通過 io 查找,這種代價顯然是昂貴的!
blockcache 稱為讀緩存,hbase 會将一次檔案查找的 block 塊緩存到 cache 中,以便後續同一請求或者鄰近資料查找請求,可以直接從記憶體中擷取,避免昂貴的 io 操作。
7. blockcache 的底層實作?你提到了 lru 那除了 lru 還可以有什麼方案?
我們知道緩存有三種不同的更新政策,分别是fifo(先入先出)、lru(最近最少使用)和 lfu(最近最不常使用)。
hbase 的 block 預設使用的是 lru 政策:lrublockcache。此外還有 bucketcache、slabcache(此緩存在 0.98 版本已經不被建議使用)
lrublockcache 實作機制:
lrublockcache 是 hbase 目前預設的 blockcache 機制,實作機制比較簡單。它使用一個 concurrenthashmap 管理 blockkey 到 block 的映射關系,緩存 block 隻需要将 blockkey 和對應的 block 放入該 hashmap 中,查詢緩存就根據 blockkey 從 hashmap 中擷取即可。
同時該方案采用嚴格的 lru 淘汰算法,當 block cache 總量達到一定門檻值之後就會啟動淘汰機制,最近最少使用的 block 會被置換出來。在具體的實作細節方面,需要關注幾點:
緩存分層政策
hbase 在 lru 緩存基礎上,采用了緩存分層設計,将整個 blockcache 分為三個部分:single-access、mutil-access 和 inmemory。
需要特别注意的是,hbase 系統中繼資料存放在 inmemory 區,是以設定資料屬性 inmemory = true 需要非常謹慎,確定此列族資料量很小且通路頻繁,否則有可能會将 hbase.meta 中繼資料擠出記憶體,嚴重影響所有業務性能。
lru 淘汰算法實作
系統在每次 cache block 時将 blockkey 和 block 放入 hashmap 後都會檢查 blockcache 總量是否達到門檻值,如果達到門檻值,就會喚醒淘汰線程對 map 中的 block 進行淘汰。
系統設定三個 minmaxpriorityqueue 隊列,分别對應上述三個分層,每個隊列中的元素按照最近最少被使用排列,系統會優先 poll 出最近最少使用的元素,将其對應的記憶體釋放。可見,三個分層中的 block 會分别執行 lru 淘汰算法進行淘汰。
8. 聊聊 sparkstreaming 和 flink?flink 流批一體解釋一下?
flink 是标準的實時處理引擎,基于事件驅動。而 spark streaming 是微批( micro-batch )的模型。
下面就分幾個方面介紹兩個架構的主要差別:
架構模型:
spark streaming 在運作時的主要角色包括:master、worker、driver、executor;
flink 在運作時主要包:jobmanager、taskmanager 和 slot。
任務排程:
spark streaming 連續不斷的生成微小的資料批次,建構有向無環圖 dag, spark streaming 會依次創 dstreamgraph、jobgenerator、jobscheduler;
flink 根據使用者送出的代碼生成 streamgraph,經過優化生成 jobgraph,然後送出給 jobmanager 進行處理, jobmanager 會根據 jobgraph 生成 executiongraph,executiongraph 是 flink 排程最核心的資料結構,jobmanager 根據 executiongraph 對 job 進行排程。
時間機制:
spark streaming 支援的時間機制有限,隻支援處理時間。
flink 支援了流處理程式在時間上的三個定義:處理時間、事件時間、注入時間。同時也支援 watermark 機 制來處理滞後資料。
容錯機制:
對于 spark streaming 任務,我們可以設定 checkpoint,然後假如發生故障并重新開機,我們可以從上次 checkpoint 之處恢複,但是這個行為隻能使得資料不丢失,可能 會重複處理,不能做到恰好一次處理語義。
flink 則使用兩階段送出協定來解決這個問題。
flink 的兩階段送出協定具體可以看這篇文章: 八張圖搞懂 flink 端到端精準一次處理語義 exactly-once
9. spark 的幾種 shuffle 說下?為什麼要丢棄 hashshuffle?
前段時間剛寫的,可以看下: spark 的兩種核心 shuffle 詳解
10. java gc 可達性分析+垃圾回收器+垃圾回收算法+為什麼分代垃圾回收+調優
jvm 相關的面試題可看這篇文章,文中第四、五題和本問題相關: 精選大資料面試真題 jvm 專項
11. 資料庫引擎,innodb 索引實作+聚集和非聚集差別+為什麼用 b+樹不用 hash
innodb 索引實作:
innodb使用的是聚集索引,将主鍵組織到一棵b+樹中,而行資料就儲存在葉子節點上,若使用"where id = 14"這樣的條件查找主鍵,則按照b+樹的檢索算法即可查找到對應的葉節點,之後獲得行資料。若對name列進行條件搜尋,則需要兩個步驟:第一步在輔助索引b+樹中檢索name,到達其葉子節點擷取對應的主鍵。
第二步使用主鍵在主索引b+樹中再執行一次b+樹檢索操作,最終到達葉子節點即可擷取整行資料。
聚集索引和非聚集索引的差別:
聚集索引一個表隻能有一個,而非聚集索引一個表可以存在多個。
聚集索引存儲記錄是實體上連續存在,而非聚集索引是邏輯上的連續,實體存儲并不連續。
聚集索引:實體存儲按照索引排序;聚集索引是一種索引組織形式,索引的鍵值邏輯順序決定了表資料行的實體存儲順序。
非聚集索引:實體存儲不按照索引排序;非聚集索引則就是普通索引了,僅僅隻是對資料列建立相應的索引,不影響整個表的實體存儲順序。
索引是通過二叉樹的資料結構來描述的,我們可以這麼了解聚簇索引:索引的葉節點就是資料節點。而非聚簇索引的葉節點仍然是索引節點,隻不過有一個指針指向對應的資料塊。
資料庫索引使用b+樹原因:
innodb采用b+樹結構,是因為b+樹能夠很好地配合磁盤的讀寫特性,減少單次查詢的磁盤通路次數,降低io、提升性能等。
資料庫索引不适合用hash的原因:
區間值難找。因為單個值計算會很快,而找區間值,比如 100 < id < 200 就悲催了,需要周遊全部hash節點。
排序難。通過hash算法,也就是壓縮算法,可能會很大的值和很小的值落在同一個hash桶裡,比如一萬個數壓縮成1000個數存到hash桶裡,也就是會産生hash沖突。
不支援利用索引完成排序、以及like ‘xxx%’ 這樣的部分模糊查詢。
不支援聯合索引的最左字首比對規則。
12. 聊聊 tcp 和 udp 的差別
簡單說下:
tcp面向連接配接 (如打電話要先撥号建立連接配接);udp是無連接配接的,即發送資料之前不需要建立連接配接。
tcp提供可靠的服務。也就是說,通過tcp連接配接傳送的資料,無差錯,不丢失,不重複,且按序到達;udp盡最大努力傳遞,即不保證可靠傳遞。
tcp通過校驗和,重傳控制,序号辨別,滑動視窗、确認應答實作可靠傳輸。如丢包時的重發控制,還可以對次序亂掉的分包進行順序控制。
udp具有較好的實時性,工作效率比tcp高,适用于對高速傳輸和實時性有較高的通信或廣播通信。
每一條tcp連接配接隻能是點到點的;udp支援一對一,一對多,多對一和多對多的互動通信。
tcp對系統資源要求較多;udp對系統資源要求較少。
13. http 知道嗎?說一下
超文本傳輸協定(縮寫:http)是一種用于分布式、協作式和超媒體資訊系統的應用層協定。http是網際網路的資料通信的基礎。
http協定定義web用戶端如何從web伺服器請求web頁面,以及伺服器如何把web頁面傳送給用戶端。http協定采用了請求/響應模型。用戶端向伺服器發送一個請求封包,請求封包包含請求的方法、url、協定版本、請求頭部和請求資料。伺服器以一個狀态行作為響應,響應的内容包括協定的版本、成功或者錯誤代碼、伺服器資訊、響應頭部和響應資料。
以下是 http 請求/響應的步驟:
用戶端連接配接到web伺服器:
一個http用戶端,通常是浏覽器,與web伺服器的http端口(預設為80)建立一個tcp套接字連接配接。例如, http://www.baidu.com。
發送http請求:
通過tcp套接字,用戶端向web伺服器發送一個文本的請求封包,一個請求封包由請求行、請求頭部、空行和請求資料4部分組成。
伺服器接受請求并傳回http響應:
web伺服器解析請求,定位請求資源。伺服器将資源複本寫到tcp套接字,由用戶端讀取。一個響應由狀态行、響應頭部、空行和響應資料4部分組成。
釋放連接配接tcp連接配接:
若connection 模式為close,則伺服器主動關閉tcp連接配接,用戶端被動關閉連接配接,釋放tcp連接配接;若connection 模式為keepalive,則該連接配接會保持一段時間,在該時間内可以繼續接收請求;
用戶端浏覽器解析html内容:
用戶端浏覽器首先解析狀态行,檢視表明請求是否成功的狀态代碼。然後解析每一個響應頭,響應頭告知以下為若幹位元組的html文檔和文檔的字元集。用戶端浏覽器讀取響應資料html,根據html的文法對其進行格式化,并在浏覽器視窗中顯示。
14. http 版本之間的比較
這道題位元組經常問,需要記住:
http0.9:
最初的http版本,僅支援get方法,隻能傳輸純文字内容,是以請求結束服務段會給用戶端傳回一個html格式的字元串,然後由浏覽器自己渲染。
http0.9是典型的無狀态連接配接(無狀态是指協定對于事務處理沒有記憶功能,對同一個url請求沒有上下文關系,每次的請求都是獨立的,伺服器中沒有儲存用戶端的狀态)
http1.0:
這個版本後任何檔案形式都可以被傳輸,本質上支援長連接配接,但是預設還是短連接配接,增加了keep-alive關鍵字來由短連結變成長連接配接。
http的請求和回應格式也發生了變化,除了要傳輸的資料之外,每次通信都包含頭資訊,用來描述一些資訊。
還增加了狀态碼(status code)、多字元集支援、多部分發送(multi-part type)、權限(authorization)、緩存(cache)、内容編碼(content encoding)等。
http1.1:
http1.1最大的變化就是引入了長連結,也就是tcp連結預設是不關閉的可以被多個請求複用。用戶端或者伺服器如果長時間發現對方沒有活動就會關閉連結,但是規範的做法是用戶端在最後一個請求的時候要求伺服器關閉連結。對于同一個域名,目前浏覽器支援建立6個長連結。
節約帶寬,http1.1支援隻發送header頭資訊不帶任何body資訊,如果伺服器認為用戶端有權限請求指定資料那就傳回100,沒有就傳回401,當用戶端收到100的時候可以才把要請求的資訊發給伺服器。并且1.1還支援了請求部分内容,如果目前用戶端已經有一部分資源了,隻需要向伺服器請求另外的部分資源即可,這也是支援檔案斷點續傳的基礎。
1.1版本中增加了host處理,在http1.0中認為每台伺服器都綁定一個唯一的ip位址,是以在url中并沒有傳遞主機名,但是随着虛拟機技術的發展,可能在一台實體機器上存在多個虛拟主機,并且他們共享了一個ip位址,http1.1中請求消息和響應消息都支援host頭域,如果不存在還會報出錯誤。
http2.0:
多路複用:在一個連接配接裡面并發處理請求,不像http1.1在一個tcp連接配接中各個請求是串行的,花銷很大。
在1.0版本後增加了header頭資訊,2.0版本通過算法把header進行了壓縮這樣資料體積就更小,在網絡上傳輸就更快。
服務端有了推送功能,将用戶端感興趣的東西推給用戶端,當用戶端請求這些時,直接去緩存中取就行。
15. 讓你設計一個 hash 表,怎麼設計?
可以把java的hashmap的實作原理說下,因為這就是hash表的經典設計!内容較多,并且網上資料很多,可以自己搜尋檢視!
16. 時間不多了,手撸一個二分查找
二分查找圖解:
二分查找:時間複雜度o(log2n);空間複雜度o(1)
def binarysearch(arr:array[int],left:int,right:int,findval:int): int={
if(left>right){//遞歸退出條件,找不到,傳回-1
-1
}
val midindex = (left+right)/2
if (findval < arr(midindex)){//向左遞歸查找
binarysearch(arr,left,midindex,findval)
}else if(findval > arr(midindex)){//向右遞歸查找
binarysearch(arr,midindex,right,findval)
}else{//查找到,傳回下标
midindex
拓展需求:當一個有序數組中,有多個相同的數值時,如何将所有的數值都查找到。
/*
{1,8, 10, 89, 1000, 1000,1234} 當一個有序數組中,有多個相同的數值時,如何将所有的數值都查找到,比如這裡的 1000.
//分析
1. 傳回的結果是一個可變數組 arraybuffer
2. 在找到結果時,向左邊掃描,向右邊掃描 [條件]
3. 找到結果後,就加入到arraybuffer
*/
def binarysearch2(arr: array[int], l: int, r: int,
findval: int): arraybuffer[int] = {
//找不到條件?
if (l > r) {
return arraybuffer()
val midindex = (l + r) / 2
val midval = arr(midindex)
if (midval > findval) {
//向左進行遞歸查找
binarysearch2(arr, l, midindex - 1, findval)
} else if (midval < findval) { //向右進行遞歸查找
binarysearch2(arr, midindex + 1, r, findval)
} else {
println("midindex=" + midindex)
//定義一個可變數組
val resarr = arraybuffer[int]()
//向左邊掃描
var temp = midindex - 1
breakable {
while (true) {
if (temp < 0 || arr(temp) != findval) {
break()
if (arr(temp) == findval) {
resarr.append(temp)
temp -= 1
//将中間這個索引加入
resarr.append(midindex)
//向右邊掃描
temp = midindex + 1
if (temp > arr.length - 1 || arr(temp) != findval) {
temp += 1
return resarr