前言
- 本篇是關於 2022-MIT 6.824 的關於 MapReduce 的課程筆記及其實驗記錄;
- 課堂筆記部分基本摘自 Lecture,完全可以跳過;
- 如果發現內容上的紕漏,請不要吝嗇您的鍵盤。
正文
課堂筆記
You should try everything else, before you try to build a distributed system, because they're not simpler.
Lab's Goal?
Lab 1: MapReduce
- implement your own version of the paper
- Lab 2: replication for fault-tolerance using Raft
Lab 3: fault-tolerant key/value store
- use your Raft implementation to build a replicated K/V server
Lab 4: sharded key/value store
- clone the K/V server into a number of independent groups and split the data in your K/V storage system to get parallel speed-up and moving chunks of data between servers as they come and go without dropping any balls
Why do people build distributed systems?
to increase capacity via parallelism
- to get high-performance, lots of CPUs, memories, disk arms moving in parallel...
to tolerate faults via replication
- have two computers do the exact same things and if one of them fails you can cut over to the other one
to place computing physically close to external entities
- 美國西海岸和東海岸的兩個伺服器之間的通訊問題
- to achieve security via isolation
Challenges:
- many concurrent parts, complex interactions
- must cope with partial failure
- tricky to realize performance potential
Big Goal?
This is a course about infrastructure for applications.
- Storage system.
- Communication sytem.
- Computation system.
我們要儘可能地做 abstractions,使這些部分像是一個 non-distributed sytem 的部分。比如將 Storage system 抽象成熟悉的 file sytem,但背後實際上還是很複雜的 parrallel, fault-tolerated distributed system。因為這很難做到,所以這是我們的 big goal。
Topic:
implementation.
- here are three tools you'll need to build such a distributed system.
- RPC(remote procedure call), whose goal is to mask the fact that we're communicating over an unreliable network
- threads
- concurrenty control
performance.
- the high-level goal of building a distributed sytem is to get what people call scalable speed-up.
- scalability: 2x computers -> 2x throughput
fault tolerance
- 1000s of servers, big network -> always something broken
- We'd like to hide these failures from the application.(abstraction)
Availability -- app can make progress despite failures
- 有掛掉的也能繼續正常工作
Recoverability -- app will come back to life when failures are repaired
- 修護後也能繼續正常工作
- non-volatile storage(但嚴重影響效能,通常不會選擇)
- Replication
consistency
- 假設當前 distributed system 提供的是 K/V Service
- 操作只有兩種:
Put(k, v)
andGet(k) return v
- 若在 non-distributed sytem 中,這兩種操作都只有一種語義;
- 但在 distributed system,由於通常會有 replication 或 caching 的存在,資料(kv pair) 會有多個複製。當你在更新這些複製的途中出現 failure 了,如果保證資料的 consistency 將會是件很重要的事。
- Strong consistency 可以使你訪問到 the most recently updated data,但通訊的開銷太大;
- Weak consistency 盡力而為,但通常能 acceptable,取決於你的 trade-off。
實驗部分
實驗目標
Lab 1 的目標是把 MapReduce 這篇 2004 年 Google 的經典論文的內容簡單復現一遍(玩具),並透過測試。
MapReduce 設計了一個框架,這個框架能夠遮蔽分散式系統內部的複雜細節,使用者只需要編寫 map function 和 reduce function 就可以享受分散式平行計算帶來的高效能。完成著重閱讀論文第 2 節 Programming Model 和第 3 節 Implementation 的部分。這兩個部分直接關係到實驗的實現。
實驗技巧
asynchronous programming == event-driven programming(非同步程式設計 == 事件驅動程式設計)?
- single thread of control, sits an event-driven loop, waits for inputs, whenever it gets an input like a packet, it figures out which client did this packet come from, and it'll had a table of sort of what the state is of whatever activity it's managing for that client. it'll find I was in the middle of reading such-and-such a file, and now it's asked me to read the next block I'll go and be the next block I'll return it.
When to use sharing and locks, versus channels?
Most problems can be solved in either style
- state -- sharing and locks
- communication -- channels
- For the 6.824 labs, I recommend sharing+locks for state, and sync.Cond or channels or time.Sleep() for waiting/notification.
實驗過程
配置好虛擬機器,安裝最新版本的 Golang,安裝一個 Goland,用 Git 把實驗程式碼倉庫克隆下來,實驗環境就沒了。這相比於之前 6.S081 去安裝 qemu 簡單太多。試過用 VScode 寫 Golang,但發現安裝了語法檢查和自動補全的元件後直接程式碼編輯區直接爆紅,暫時找不到解決方案就直接上手 Goland 了。
先前沒有接觸過 Go,所以遵循實驗指導書的提示把 Go 官網的 Tutorial 過了一遍,效果真的很不錯。刷完後至少能把實驗的原始碼看得懂了。
關於實驗的實現,其實僅根據論文的描述,尤其是論文裡的那張彩圖,對照著做就好了。親測僅依賴論文和實驗指導書的內容就可以獨立完成實驗 1,自己的實現也就 500 行左右的程式碼,況且難度還只是 moderate,好日子還在後頭呢。
據說這門課直接貼具體程式碼是違規的,所以這裡就只貼三個檔案的 abstract。
src/mr/coordinator.go
:
src/mr/worker.c
:
src/mr/rpc.c
:
實驗踩坑
判斷哪些是共享變數
- 有些執行緒安全問題
-race option
都有可能檢測不出來,因此可能會出現資料不一致的問題。比如我原來在createWorkerId()
時,“workerId 的分配”和“將 worker 結構體新增到 workers 切片內”的操作不是繫結的的,因此後期可能會出現資料的不一致問題。
- 有些執行緒安全問題
慎用 append
- 將一個元素 append 一個切片後,當長度超過容量時,Go 會將切片中的所有元素複製到另一個更大的空閒區域。在這之前若有執行緒持有在切片某個元素的指標時,這個指標會指向原來的切片的位置,造成資料的不一致性。我的解決方法比較粗暴,初始化切片時直接給它分配 1000 的長度,再也不用擔心切片擴容了。
後記
個人理解的一個分散式系統開發週期,要分三個階段,各階段不是嚴格序列執行的,每個階段內可做數次迭代:
架構:做正確的事,系統分析與設計(預計花費 60% 的時間)
- 閱讀相關材料、分析,理解;
- 畫狀態時序圖,設計資料結構和演算法。
穩定:正確地做事,測試驅動開發(預計花費 40% 的時間)
- 先保證一個用例執行起來程式不報錯;
- 再保證一個用例能夠可以輸出正確的結果;
- 最後保證所有用例都能輸出正確的結果,而且無論何時都能輸出正確的結果;
- 迭代式地新增功能,按核心重要程度依次新增,每新增一個完整的功能就要做一次迴歸測試;
- 當資源充足並可行時,為每個功能模組做一次單元測試。
- 進行必要的重構,提高可維護性,實現新功能。
效能:精益求精,提升客戶體驗(規格外,無法預估)
- 降低資源使用門檻,壓榨硬體效能,直到達到架構設限的天花板
最後說一下 Coding 的一些習慣:
先不考慮程式碼風格和質量等這些高階話題,就說最常見的 Debug。寫好程式後跑測試掛掉了,然後翻日誌不斷進行比對,然後經過不知道多長時間後終於發現了資料不一致或不正確的原因。且不說這個過程有多麼地浪費時間且低效,它還會對你的眼睛和頸椎造成傷害。
所以於其“浪費時間” Debug,要保證一開始寫的時候就要考慮各種各樣的 corner case,多考慮考慮這麼寫會有什麼副作用,就算短時間實現不了也要留個註釋備註一下。就算真出現 Bug 了也要儘快縮短排錯時間,Error 資訊要規範,要完備。我知道這聽起來像是廢話,但我的意思 Coding 的時候要集中注意力,帶耳機聽歌什麼的還是不要了,不然你的那一點“馬虎”所付出的代價可能是極其痛苦的,特別是在這種分散式系統場景下。對自己的程式碼負責的同時,更要對自己的時間負責。
一句話概括:有意識地縮短 Debug 的時間,不管是從源頭上還是在過程上。