頭條是這批次面試中的一個理想公司,基礎架構部。從兩輪面試的情況來看,面試官的素質非常高,面試經驗也比較豐富。一方面提問抓的準,不會在你明確表示準備不足的方面硬扣;一方面深度廣度、代碼風格均有涉及。不過總共只讓我寫了兩道代碼題,希望不是放水。
一面
一面應該還問了其他內容,但是兩次面試問的太多,想不起來了。
項目
兩個項目都是簡單介紹,沒有深入問。
基礎
并發
- 條件變量內部有鎖,為什么在wait條件變量時,最外層還需要加鎖(跪)
- 除了notify,wait還有什么情況下可能被喚醒(跪)
書到用時方恨少,題到問時才知淺。并發復習的太少,這部分完全忘卻了。
算法
判斷二叉樹是否是二叉搜索樹
題目很熟悉了,倒是也寫過。
我有點記不清二叉搜索樹(BST)的定義,于是先跟面試官簡單確認了下。接下來,向面試官明確題目:
- 假設不包含重復值
- 給出明確的BST的定義:
- l 是BST
- r 是BST
- 滿足
l < root < r
向面試官表示用分治法,然后開始寫代碼。寫到一半,面試官又跟我說可以考慮二叉樹的先序、中序、后續遍歷這些。我反應過來可以直接中序遍歷,遍歷的同時判斷序列是否遞增。說了下思路,詢問面試官是否可以先寫我原來的解法,這樣就不用換思路了,省的出錯。面試官統一后我才繼續寫:
private class Result {
public boolean isBST;
public int min;
public int max;
public Result(boolean isBST, int min, int max) {
this.isBST = isBST;
this.min = min;
this.max = max;
}
}
public isBST(TreeNode root) {
if (root == null) {
return true;
}
Result result = dc(root);
return result.isBST;
}
private Result dc(TreeNode root) {
if (root == null) {
return null;
}
if (root.left == null && root.right == null) {
return new Result(true, root.val, root.val);
}
Result lResult = dc(root.left);
Result rResult = dc(root.right);
if (lResult != null && (!lResult.isBST || lResult.max >= root.val) {
return new Result(false, 0, 0);
}
if (rResult != null && (!rResult.isBST || rResult.min >= root.val)) {
return new Result(false, 0, 0);
}
// 忘記了這里的check,no face, no bug free
int min = root.val;
int max = root.val;
if (lResult != null) {
min = lResult.min;
}
if (rResult != null) {
max = rResult.max;
}
return new Result(true, min, max);
}
分治法代碼寫出來有點長,不過多長也得堅持編碼風格和bug free。
but,我第一次寫的還是有bug,剛把紙交給面試官我就想起來了——最后一句return沒有檢查空指針,,,媽的真是蠢。還好面試官沒有介意,感謝感謝。
中序遍歷的話,簡單做可以完全保存下序列,再去check,但是浪費空間;復雜做就得一邊遍歷一邊檢查,我還沒想清楚足夠簡潔的寫法。(待補充)
二面
項目
冷數據壓縮與存儲——vulture
可能是我講的越來越能抓住key point了?這是我唯一一次把vulture中的兩個難點都講了。特別是異常恢復,把壓縮過程抽象成狀態機還是有點講頭的。
- 為什么要依賴外部配置,而沒有跟HDFS整合在一起
我表示也考慮過這種方案,但當時的需求是盡量簡單的搞定壓縮,如果跟HDFS整合的話,可能需要將冷熱溫的生存期、溫度等都寫進FileStatus,而改動這種基礎類的影響太大了。
Hadoop集群監控——hawk
表示項目本身很簡單,難在如何確定所監控指標的準確含義。
話說一半,意在引導面試官提問指標相關的內容,秀源碼。
- 準確含義指什么
我把ContainerExitStatus=137時的case講了一下。
源碼
HDFS里的Federation怎么實現的
客戶端掛載,如何映射 balabala。
一般Federation里還要使用HA,講一下HA的原理
Zookeeper保障唯一時刻只有至多一個active節點;唯一的active向journalNode寫數據,其他standby從journalNode讀數據。
HA中兩個節點同步哪些內容(跪)
開始以為跟SecondaryNameNode機制一樣,后來仔細一想:standby既然能從journalNode讀數據,也就不需要像SecondaryNameNode那樣從active拉取editlog,自然也不需要在stanby上合并日志并同步回active。
所以,坦白自己沒看過這一塊內容,也沒什么想法。
Yarn的源碼你比較了解哪塊
狀態機、事件調度
container是怎么啟動的
說的比較糙:
- NM收到指令
- 創建Container狀態機、初始化資源等
- ContainerLaucher服務收到LAUNCH_CONTAINER事件
- 創建launch_container腳本
- 創建container_executor腳本,該腳本里會啟動該java進程
container運行需要下載一些資源,了解這個過程嗎
先講三個資源等級,假設最簡單的場景,剛說到ResourceLocalizer狀態機,面試官就表示,“好,不用說了,我知道你看過這塊”。
系統設計
用客戶端掛載實現Federation的缺點
- 掛載表配置在客戶端,所以修改掛載表后,客戶端也必須作出同步的修改,無法做到用戶無感知或低感知
- Federation的在擴展NameNode的同時也有負載均衡的目的,但是客戶端掛載相當于人工維護負載均衡,流量發生任何變化都無法維持均衡
只想出來這兩點。待完善。
不使用掛載表,如何實現Federation(半跪)
我不了解這方面的內容,嘗試以客戶端掛載方案為baseline進行改進。
Federation的核心是目錄到NameNode的映射關系。而客戶端掛載的本質缺點在于將映射關系保存在客戶端,因此所有功能都依賴于客戶端完成。所以優化的第一步是,將映射關系保存在服務端。下面給出兩種方案:
- 將映射保存在服務端。進一步的,如想訪問/logdata目錄,就創建/logdata文件,文件中保存/logdata目錄映射的NameNode。每次客戶端都先訪問/logdata文件,獲取映射的NameNode,再到該NameNode上訪問/logdata目錄。效果相當于把客戶端掛載移到了服務端。
- 在NameNode前架設Proxy服務,由Proxy在內存中維護映射關系。甚至可基于Proxy實現新的均衡器和自動化掛載,實現不同標準(跨NameNode、同NameNode)的數據均衡和流量均衡。
我回答的不完全是面試官想要的——面試官問我看過Hdaoop 3.0的源碼嗎,我表示完全沒看過,面試官表示沒看過的話應該答不上來。
如果用Proxy的方案,估計一下qps
1w個節點,每個節點都是
40核+256G
,全部跑MR,每個container1核+2G
。估計Proxy需要承受的qps。
假設就跑了一個AM,它申請了集群所有的container,全部跑mapper,可忽略該AM。假設每個mapper在1min(60s)內需要訪問5次NameNode,則至少需要訪問5次Proxy(獲取映射)。則:
qps = (10E4 * 40 / 1) * 5次/60s = 3.67 * 10E4 次/s
面試官問我這么大的規模單機扛得住嗎,,,我表示現在的技術扛萬級qps不是很輕松嗎。恩,,可能我還是沒有get到考點吧。
基礎
并發(半跪)
我一面中并發答的很差,就主動跟二面面試官表示并發這塊復習的不好。面試官心領神會,也沒有出很難的題目。萬謝萬謝。
- Java中可以通過哪些方式使用鎖
synchronized、ReentrantLock、用AQS裸寫一個鎖。
這里我本來也寫了Condition、CountDownLatch那些并發工具,后來覺得這更屬于同步,所以又把它們刪了。這種屬于偏概念的套路題,需要刷面經才能知道套路答案。
- 如何讓鎖實現“可重入”(半跪)
我一開始覺得先不用說重入次數,就只回答了需要在鎖內部記錄owner。結果面試官提醒多重入的場景我才說還要記錄重入次數,搞得好像面試官提醒我才想到。
所以說下次不能耍小聰明,能想到的點盡量說出來。如果面試官不攔著你,你就由簡到難,一直說下去,說到一個完整且你說不下去的地方為止。
- 講一個你熟悉的并發的內容
對比講了HashTable和ConcurrentHashMap。
估計面試官問我這個問題只是想看看我是不是并發一點都不會,畢竟我前面并發答的那么差。
問個簡單的,Object類中有哪些方法(半跪)
- wait, notify, notifyAll
- toString
- hashCode, equals
然后就說不上來了。
現補充如下:
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {...}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
}
主要漏答的是clone, getClass。finalize, registerNatives也盡量記住。
覆寫hashCode時有什么需要注意的
主要是需要判斷相等的時候,比如HashMap的get方法,對hashCode和equals的調用順序有要求:
if (table[pos].key.hashCode() == key.hashCode
&& (table[pos].key == key || table[pos].equals(key))) {
return table[pos];
}
只有hashCode相等的時候,equals才有可能相等,即,“覆寫后,如果equals相等,那么hashCode也必須相等”。
同時,對于HashMap還要保證插入前后,key的hashCode相等。
總結如下:
- 如果equals相等,那么hashCode也必須相等
- 對同一個對象,將其插入HashMap前后,hashCode不可變
工程代碼
按需求實現新的hash表(跪)
我們知道NameNode中保存了BlockId到BlockInfo的映射,以滿足快速查找的目的。如果直接用HashMap實現(當然,實際用的也不是HashMap,用的Google的LightWeightGSet),由于HashMap內部用拉鏈發實現,每個節點都有最有兩個指針,假設每個指針占2字節,那么1億個Block最少要占用4億字節,假設造成很大的空間浪費。你需要實現一個新的hash表,接口定義:
interface SimpleMap<K, V> { K get(K key); V put(K key, V value); V remove(K key); }
要求:
- 盡量提高空間利用率
- 可以犧牲部分性能
當即想到了線性探測、rehash等方法,覺得so easy,確認可以用線性探測后,就開始打草稿了,然后謄抄。需要注意的是del方法,我采取的思路是刪除后將tail節點填充到被刪除的位置。啪啪啪一頓寫,結果交了答案——我又意識到了大bug,思路就錯了;還發現了一個put方法中的一個死循環。不過這兩點都屬于我基本思路上的問題,就不秀錯誤代碼了。
回來自己想,現在補充正確代碼,未實現的非核心代碼參照HashMap:
public class LinearProbingHashMap<K, V> implements SimpleMap<K, V> {
@Override
public V get(Object key) {
if (key == null) {
return null;
}
int pos = indexFor(hash(key));
for (int i = 0;
i < table.length && table[pos] != null;
i++, pos = (pos + 1) % table.length) {
if (table[pos] == Entry.DELETED_ENTRY) {
continue;
}
if (table[pos].keyHash == key.hashCode()
&& (table[pos].key == key || table[pos].key.equals(key))) {
return table[pos].value;
}
}
return null;
}
@Override
public V put(K key, V value) {
if (key == null) {
return null;
}
int pos = indexFor(hash(key));
int lastEmptyPos = -1;
for (int i = 0;
i < table.length && table[pos] != null;
i++, pos = (pos + 1) % table.length) {
if (table[pos] == Entry.DELETED_ENTRY) {
lastEmptyPos = pos;
}
if (table[pos].keyHash == key.hashCode()
&& (table[pos].key == key || table[pos].key.equals(key))) {
V oldValee = table[pos].value;
table[pos].value = value;
return oldValee;
}
}
if (size + 1 < threshold) {
if (lastEmptyPos == -1) {
// assert table[pos] == null;
lastEmptyPos = pos;
}
table[lastEmptyPos] = new Entry<>(key, value);
size++;
return null;
}
LinearProbingHashMap<K, V> newMap = new LinearProbingHashMap<>(table.length * 2);
for (Entry<K, V> entry : table) {
// TODO: 2017/8/31 remove extra cost on copy entry
if (entry != null && entry != Entry.DELETED_ENTRY) {
newMap.put(entry.key, entry.value);
}
}
init(newMap); // update table, size, loadFactor, threshold, and etc.
put(key, value);
return null;
}
@Override
public V remove(Object key) {
if (key == null) {
return null;
}
int pos = indexFor(hash(key));
for (int i = 0;
i < table.length && table[pos] != null;
i++, pos = (pos + 1) % table.length) {
if (table[pos] == Entry.DELETED_ENTRY) {
continue;
}
if (table[pos].keyHash == key.hashCode()
&& (table[pos].key == key || table[pos].key.equals(key))) {
V oldValue = table[pos].value;
table[pos] = (Entry<K, V>) Entry.DELETED_ENTRY;
size--;
return oldValue;
}
}
return null;
}
}
提問
- 如果有幸加入,入職后可能負責的工作內容
- 總共幾輪面試
- 面試評價
恩,我以為到提問環節就是最后一輪技術面呢,問了問題1,面試官表示這是三面才會聊的。總共有三輪面試,一面二面水平差不多,三面是技術leader。
正面評價:
- 整體表現不錯
- 代碼風格挺好
負面評價:
- 最后一道題有死循環是大問題(也就是思路可以原諒,但是死循環不能原諒)
- 這道題看起來簡單,但我面試的人沒有一個能寫的沒有問題。以后應該想好再動手
- 你要搞分布式的話,并發是基礎。本來最后一道題也想考你并發,但是說并發掌握的不好,就沒考你
三面(HR面)
等了三面的面試官很久都沒有來,我實在憋不住就寫了個紙條,把門開著去上廁所了。。。結果回來——握草女面試官!再看正臉,握草化妝了!怎么看都不像是技術leader,一問才知道是HR。HR表示,技術leader在開會,剛才跟他碰了一下,他說看前面兩面面試官評價都不錯,他不需要面了,所以直接跳到了HR面。
給我整的分不清是好消息還是壞消息。聽說這個leader是90年的,天大本科畢業就工作了,帶著一群80、90后,非常屌,本來想借著面試瞻仰一下,真是可惜。后面就是正常的HR面,不過感覺頭條的HR問的真多啊,各種興趣愛好,評價,家庭什么的。第一次經歷這陣仗,學到了學到了。
最后問我有沒有拿到其他offer,老實回答了。我個人覺得,面試是個相互選擇的過程,希望公司和面試者都能坦誠相待。
總結
面試完,又分別跟大學同學(頭條暑期實習)和濤神聊了聊,兩個人都反應,頭條的壓力非常大,但是同時也主動表示食堂好。看來只要公司愿意給高價,什么加班什么壓力大都可以放一邊。學到了學到了。
上次阿里面試官簡歷的面試三原則非常管用,這次溝通明顯順暢了許多。頭條面試有一個好處,要不要你都會給通知,,,but,拒信和offer都要等好幾天啊,,,HR姐姐說盡量本周五之前給通知,希望一切順利!!
給自己的建議:
- bug free!bug free!bug free!
- 耐心學習分布式系統的理論知識,研究更多分布式系統的優秀案例
- 很多看起來簡單的問題,還是不要掉以輕心,耐心想想再動筆
- 學過的東西不要丟下,原來看書雖然快,但很難吸收;這段時間建議少看書,多通過實踐把知識固化。特別是并發部分,這是搞分布式的基本功,必須吃到肚子里!
本文鏈接:【面經】頭條-2017年8月30日,散招實習生
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協議發布,歡迎轉載,演繹或用于商業目的,但是必須保留本文的署名及鏈接。