MIT 6.824 分散式系統課程第二課:RPC 和多執行緒
課程概要
主要討論 Golang 中的多執行緒和 RPC,以及實驗相關的內容。
正文
為什麼是 Golang?
- 對多執行緒的良好支援,那個執行緒擁有自己的執行棧
- RPC 特別方便
- 型別安全
- 自動垃圾回收(不用擔心記憶體釋放問題)
- 多執行緒 +GC 非常更吸引人
- 簡單
- 教程 Effective Go
多執行緒
I/O 併發(做多件事情)、並行(CPU)。
多執行緒是一個結構化工具,但是有一些坑
Go 叫多執行緒為 goroutines;
Thread = 執行執行緒
多執行緒允許一個程式在執行時去做很多事情。
每個執行緒都是序列執行,就像是非執行緒程式
執行緒可以共享記憶體
每個執行緒都有自己的執行緒狀態:程式計數器、暫存器、棧
執行緒和程式是包含的關係,一個程式可以產生很多執行緒
為什麼選擇多執行緒?
在分散式系統中,需要併發執行。而多執行緒是實現併發已經廉價的方式
I/O 併發:多個客戶端同時傳送請求到多臺 server,並且等待響應。伺服器處理多個客戶端請求,每個請求可能會阻塞。譬如客戶端 X 讀取磁碟資料的同時接到處理客戶端 Y 的請求。
多核效能:並行的在多個 CPU 執行程式碼
方便:通過後臺執行 worker,進行每秒查詢某個值是否有效
是否有執行緒的替代方案?
yes:在單執行緒中寫非序列邏輯。這種方式也被稱為事件驅動。譬如 JavaScript。
做法:使用一個狀態表儲存所有活動的狀態,假設是每個客戶端的請求
事件迴圈:在收到伺服器的響應時,傳入新的輸入值,檢查狀態表中的每個活動的狀態。執行每個活動的下一步。並且更新狀態。依次迴圈完成整個狀態表。
利用事件驅動來達到 I/O 併發的目的:這個相比較多執行緒要更大節約成本。但它不能利用多核,寫起程式碼來也比較痛苦
執行緒的挑戰
共享資料
例如:有兩個執行緒,同時做一個 n=n+1 的邏輯?有一個執行緒正在讀取,而另外有一個執行緒在做自增操作?類似資料庫事務中遇到的資料共享問題。
上面兩個問題都遇到了 “競爭” 問題,這個會成為一個程式的 Bug。在電商領域會有超賣的風險。如何解決?
- 使用鎖(Go 中使用 sync.Mutex)
- 避免使用共享可變資料(immutable and mutable)
執行緒間協調
例如:一個執行緒生產資料,另一個執行緒消費資料。生產與消費者
- 消費者如何等待(不能總是佔著 CPU)
- 生產者如何喚醒消費者?
如何解決?
- 使用 Go channel 通訊
- sync.Cond
- WaitGroup
死鎖
週期性的鎖檢查或者是通訊(RPC 或 Go channels)
案例學習:Web 爬蟲
什麼是 Web 爬蟲?
- 獲取一個或多個站點的所有的 web 網頁。例如:用來構建索引
- 網頁和連結構成是一個圖結構
- 指向某些頁面的多個連結
- 圖具有環特徵
爬蟲的挑戰
如何利用 I/O 併發
爬蟲的很多瓶頸都是因為網路頻寬的限制。在同一時刻抓取多個 URL 來提高併發(每秒抓取數量)=> 利用多執行緒來提高併發
避免重複抓取
多次抓取是網路的浪費,這裡需要記錄訪問狀態
要知道何時停止
不能沒有邊界,遇到什麼情況就需要停止繼續爬取
以爬取 Golang.org 為例子
程式碼:https://pdos.csail.mit.edu/6.824/notes/crawler.go
解決方案 1: 序列爬取
通過遞迴呼叫執行按序爬取,定義的 fetched map 會記錄哪些地址已經爬取。函式中所傳遞的引數為引用。
缺點:同一時刻,一次只能爬取一個頁面。不過可以試試執行 go Serial() 來改善效能。
解決方案 2: 併發互斥爬取(concurrentMutex)
-
為每個爬取的頁面建立一個執行緒
併發爬取、提高爬取速度。 “go func” 可以建立一個 goroutine 並且執行該函式(可以是匿名函式)
-
執行緒之間是共享變數 feteched map
通過互斥鎖的方式來控制,同時只能一個執行緒訪問該變數
為什麼需要互斥?(Lock()、Unlock())
不同的頁面可能包含相同的 URL
兩個執行緒同時讀取該 URL。譬如 T1 獲取 fetched[url]、執行緒 T2 也獲取 feteched[url],此時因為該 URL 還沒有爬取完,already 還是 false 狀態。那麼會導致兩個執行緒都在爬取。這裡就需要藉助鎖來處理。鎖會保證讀取和更新這兩個操作是具有原子性。
在 Go 語言內部,map 是一個複雜的資料結構(tree or 擴充套件的 hash)。併發更新/更新會導致內部的不可變性。併發的 update/read 可能會導致 read 失敗
思考:假設註釋掉 Lock() / Unlock() 會發生什麼?
- go run crawler.go 為什麼可以正常工作?
- 即使剛剛列印的結果都是正確的
- 即使剛剛列印的結果都是正確的,不過可以執行 go run -race crawler.go 列出變數競爭的日誌
ConcurrentMutex 何時認為執行已經完成?
sync.WaitGroup
Wait() 等待所有的 Add() 操作,但遇見呼叫 Done() 時表示已經執行完成。譬如:可以用來處理等待所有的子程式處理完成。
該爬蟲程式會建立多少個執行緒?等於 URL 數量
ConcurrentChannel 爬蟲
- go channel:channel 是一個物件,ch := make(chan int)。通道可以讓一個執行緒傳送一個物件給另外一個執行緒。傳送的執行緒使用 ch <- x 把 j 傳送給其他 Goroutine 接收者。接收者通過 y := <- ch 把結果儲存到 y 物件中。for y := range ch 等待通道訊息。
- 通道的通訊是同步的
- 通道可以被多個執行緒傳送或者是接收訊息
- 通道對於作業系統非常廉價
- 傳送方在接收者接到訊息之前會一直阻塞,直到此條訊息被消費(同步,小心死鎖)
ConcurrentChannel master()
master() 會建立 worker 進行頁面爬取,worker() 把頁面的 URL 格式化為 slice,並且傳送結果到 channel 中。master 作為接收者監聽來自其他 worker 傳送過來的訊息。feteched 不需要鎖,因為不存在資料共享訪問。
問題: master 中的 wait 應該寫在哪兒?等待的時候是否會佔用 CPU 時間?
master 是如何知道已經完成爬取?使用計數的方式 n,每個 worker 只傳送一次訊息到 channel
為什麼多個執行緒使用同一個通道不會造成競爭?(因為通道的傳送和接收是同步的)
當 worker 向 URLs slice 中寫入資料時,是否存在競爭?master 讀取時,為什麼不需要鎖?
- worker 只會在傳送前寫入
- master 只會在接收訊息後讀取
- 並不存在同時使用 URLs 物件的情況
何時應該使用共享變數和鎖?對比通道有什麼好處?
其實所有的問題都有兩種解決方案,依賴你如何去思考。
- 狀態-- 共享和鎖
- 通訊-- 通道
對於 6.824 的實驗,推薦如果是狀態就使用共享變數 + 鎖的方式,如果是等待和訊息就使用 sync.Cond 或 channel 或 time.Sleep()
遠端呼叫(RPC)
遠端呼叫是分散式系統的關鍵,所有的實驗都有涉及
RPC 的目的是為了解決客戶端程式和服務端易於程式設計的資料通訊,隱藏網路協議、把資料轉換成統一的格式,譬如 strings, arrays, maps, &c 轉換為 wire 格式
RPC 訊息圖
軟體結構
案例學習:kv.go
程式碼:https://pdos.csail.mit.edu/6.824/notes/kv.go
介紹:這是一個簡單的模擬 KV 儲存伺服器,僅支援 Put(key,value), Get(key)->value
程式碼中使用了 Go 的 RPC 庫 net/rpc
程式碼分為三部分:
- common
宣告一些引數以及 server 響應的資料結構
-
client
connect() 建立一個 tcp 連線,並連線到 server
get() 和 put() 是客戶端的操作,函式內部的 Call() 呼叫 RPC 庫。主要包含動作、操作、值以及錯誤處理
-
server
宣告供 RPC 呼叫的方法、完成物件註冊到 RPC 庫。庫包含:連線讀取、為每個連線建立 goroutine、解碼請求、查詢儲存結果、呼叫物件方法、編碼響應結果、回寫結果給客戶端;Get() 和 Put() 都必須使用鎖,因為多執行緒會存在同時運算元據。
一些其他的細節:
- Binding:客戶端如何連線需要通訊的服務端?對於 Go rpc。使用主機名加埠號的形式。在大型系統中,會存在各種命名和配置伺服器
- Marshalling:編碼,傳送時的資料格式。Go 中支援傳入字串、陣列、物件、map、指標等,不支援 channel 和函式。可以通過指標來複制需要訪問的資料
RPC 的問題:呼叫失敗怎麼辦?
例如:丟包、網路中斷、伺服器慢、伺服器崩潰
對於連線的客戶端來說,故障會表現出什麼樣子?
- 客戶端得不到服務端響應
- 客戶端不知道服務端是否已經收到了請求,這裡有幾種情況。請求未收到、請求已經收到正在處理,但在返回結果時崩潰了、服務端傳送了結果,但是網路出現故障
簡單的故障處理方式:best effort
- Call() 時等待一段時間的響應
- 重發請求
- 當等待一段時間後,發現還是沒有響應就放棄並返回錯誤。
Q:開發的應用程式如何做到處理邏輯更簡單,又有效?
假設一個場景,客戶端執行 Put(“k”,10);Put(“k”,20);兩者都成功執行成功。再執行 Get(“k”) 時會得到什麼結果?中間會有一些問題:超時、重發、先傳送的後到。
Q:這種 Best effort 的形式是否就夠了?
只讀操作、保持冪等性。譬如,資料庫在進行 inserted 前可以進行檢查記錄是否存在(唯一鍵)
優秀的 RPC 服務:只做一次
解決方式:RPC 伺服器能識別什麼是重複的請求,如果是重複的請求,則返回前面生成的結果。
問題:如何識別出重複的請求?
客戶端在請求時,針對每個請求,可以生成一個唯一請求 ID(request id)。當進行重發時,該 ID 不變。邏輯虛擬碼如下:
該方案的複雜點:(lab3)
假設有兩個客戶端生成了相同的 xid 怎麼辦?更多的隨機數?使用客戶端 ID(譬如 IP)?
服務端最終需要丟棄掉舊的 RPC 請求,那何時丟棄才是安全的?
思路:
- 每個客戶端有一個唯一 ID、每個客戶端 會生成一個 RPC 序列號、每個客戶端都包含"seen all replies <= X"(像是 TCP 序列號和 ack)。
- 第二種方案,在同一時刻,只允許一個 RPC 連線。seq+1 的訊息被允許。小於 seq 的訊息就丟棄
當遇到衝突的請求時,前一個請求還在處理中。如何解決衝突?為每個執行的 RPC 都定一個 flag 標示。最終來決定是等待還是丟棄。
at-most-once 的策略,當遇到伺服器崩潰或者重啟如何處理?如果重複記錄的資訊是儲存在記憶體中,當伺服器重啟時,之前的記錄就會丟失。這裡可以:
- 從副本從獲取
- 每次寫結果前,先落盤
GO RPC 就是 at-more-once 的策略模式,只執行一次。
- 開啟 TCP 連線
- 傳送請求到 TCP
- 永不進行重發(伺服器就看不到重複呼叫了)
- 如果沒有得到響應,就返回錯誤.(TCP 超時、服務端未收到請求、服務端處理了但是網路故障)
擴充套件:恰好一次的策略(exactly once)
無限制的重試,重複檢測以及容錯服務(Raft)
參考文獻
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- MIT 6.824 分散式系統課程第四課:主備複製MIT分散式
- MIT6.824 分散式系統實驗MIT分散式
- 課程 3: 執行緒與並行執行緒並行
- Python-多執行緒-尹成-專題視訊課程Python執行緒
- C++ 11 多執行緒-黃棒清-專題視訊課程C++執行緒
- MIT 6.824 學習筆記(一)--- RPC 詳解MIT筆記RPC
- 課程管理系統
- 微核心、多執行緒、SMP對稱多處理、分散式作業系統執行緒分散式作業系統
- 課程排課系統:智慧排課+線上約課+直播上課+作業打卡!
- 多執行緒和多執行緒同步執行緒
- 【Google官方教程】第二課:在非UI執行緒處理BitmapGoUI執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 作業系統:多執行緒作業系統執行緒
- Java併發和多執行緒3:執行緒排程和有條件取消排程Java執行緒
- 從構建分散式秒殺系統聊聊執行緒池分散式執行緒
- 2400門課:MIT開放迄今最全CS+電氣工程課程MIT
- windows核心程式設計課程實踐---多執行緒檔案搜尋器(MFC介面)Windows程式設計執行緒
- 分散式叢集與多執行緒高併發分散式執行緒
- MIT6S081課程筆記MIT筆記
- java大資料最全課程學習筆記(2)--Hadoop完全分散式執行模式Java大資料筆記Hadoop分散式模式
- 【騏程】多執行緒(上)執行緒
- 多執行緒-執行緒排程及獲取和設定執行緒優先順序執行緒
- Python——程式、執行緒、協程、多程式、多執行緒(個人向)Python執行緒
- 【分散式-6.824】Lecture3-GFS分散式
- IO多路複用和多執行緒會影響Redis分散式鎖嗎?執行緒Redis分散式
- 為什麼要學習嵌入式系統課程?
- 多執行緒-程式和執行緒的概述執行緒
- 多執行緒常用函式執行緒函式
- 分散式、高併發與多執行緒有何區別分散式執行緒
- 程式,核心執行緒,使用者執行緒,協程,纖程......作業系統世界觀執行緒作業系統
- 作業系統課程設計感受作業系統
- 多執行緒-執行緒組的概述和使用執行緒
- 多執行緒-執行緒池的概述和使用執行緒
- Haskell影片和課程Haskell
- .NET多執行緒程式設計(1):多工和多執行緒 (轉)執行緒程式設計
- 你分得清分散式、高併發與多執行緒嗎?分散式執行緒
- 分散式、高併發與多執行緒、你分辨的清嗎?分散式執行緒
- 多執行緒:執行緒池理解和使用總結執行緒