Mc 的 IO 處理執行緒分主執行緒和工作執行緒,每個執行緒各有一個 event_base,來監聽網路事件。主執行緒負責監聽及建立連線。工作執行緒負責對建立的連線進行網路 IO 讀取、命令解析、處理及響應。
主執行緒
主執行緒在監聽埠時,當有連線到來,主執行緒 accept 該連線,並將連線排程給工作執行緒。排程處理邏輯,主執行緒先將 fd 封裝成一個 CQ_ITEM 結構,並存入新連線佇列中,然後輪詢一個工作執行緒,並通過管道向該工作執行緒傳送通知。工作執行緒監聽到通知後,會從新連線佇列獲取一個連線,然後開始從這個連線讀取網路 IO 並處理,如下圖所示。主執行緒的這個處理邏輯主要在狀態機中執行,對應的連線狀態為 conn_listening。
工作執行緒
工作執行緒監聽到主執行緒的管道通知後,會從連線佇列彈出一個新連線,然後就會建立一個 conn 結構體,註冊該 conn 讀事件,然後繼續監聽該連線上的 IO 事件。後續這個連線有命令進來時,工作執行緒會讀取 client 發來的命令,進行解析並處理,最後返回響應。工作執行緒的主要處理邏輯也是在狀態機中,一個名叫 drive_machine 的函式。
狀態機
這個狀態機由主執行緒和工作執行緒共享,實際是採用 switch-case 來實現的。狀態機函式如下圖所示,switch 連線的 state,然後根據連線的不同狀態,執行不同的邏輯操作,並進行狀態轉換。接下來我們開始分析 Mc 的狀態機。
工作執行緒狀態事件及邏輯處理
conn_new_cmd
主執行緒通過呼叫 dispatch_conn_new,把新連線排程給工作執行緒後,worker 執行緒建立 conn 物件,這個連線初始狀態就是 conn_new_cmd。除了通過新建連線進入 conn_new_cmd 狀態之外,如果連線命令處理完畢,準備接受新指令時,也會將連線的狀態設定為 conn_new_cmd 狀態。
進入 conn_new_cmd 後,工作執行緒會呼叫 reset_cmd_handler 函式,重置 conn 的 cmd 和 substate 欄位,並在必要時對連線 buf 進行收縮。因為連線在處理 client 來的命令時,對於寫指令,需要分配較大的讀 buf 來存待更新的 key value,而對於讀指令,則需要分配較大的寫 buf 來緩衝待傳送給 client 的 value 結果。持續執行中,隨著大 size value 的相關操作,這些緩衝會佔用很多記憶體,所以需要設定一個閥值,超過閥值後就進行緩衝記憶體收縮,避免連線佔用太多記憶體。在後端服務以及中介軟體開發中,這個操作很重要,因為線上服務的連線很容易達到萬級別,如果一個連線佔用幾十 KB 以上的記憶體,後端系統僅連線就會佔用數百 MB 甚至數 GB 以上的記憶體空間。conn_parse_cmd
工作執行緒處理完 conn_new_cmd 狀態的主要邏輯後,如果讀緩衝區有資料可以讀取,則進入 conn_parse_cmd 狀態,否則就會進入到 conn_waiting 狀態,等待網路資料進來。conn_waiting
連線進入 conn_waiting 狀態後,處理邏輯很簡單,直接通過 update_event 函式註冊讀事件即可,之後會將連線狀態更新為 conn_read。conn_read
當工作執行緒監聽到網路資料進來,連線就進入 conn_read 狀態。對 conn_read 的處理,是通過 try_read_network 從 socket 中讀取網路資料。如果讀取失敗,則進入 conn_closing 狀態,關閉連線。如果沒有讀取到任何資料,則會返回 conn_waiting,繼續等待 client 端的資料到來。如果讀取資料成功,則會將讀取的資料存入 conn 的 rbuf 緩衝,並進入 conn_parse_cmd 狀態,準備解析 cmd。conn_parse_cmd
conn_parse_cmd 狀態的處理邏輯就是解析命令。工作執行緒首先通過 try_read_command 讀取連線的讀緩衝,並通過 \n 來分隔資料包文的命令。如果命令首行長度大於 1024,關閉連線,這就意味著 key 長度加上其他各項命令欄位的總長度要小於 1024位元組。當然對於 key,Mc 有個預設的最大長度,key_max_length,預設設定為 250位元組。校驗完畢首行報文的長度,接下來會在 process_command 函式中對首行指令進行處理。
process_command 用來處理 Mc 的所有協議指令,所以這個函式非常重要。process_command 會首先按照空格分拆報文,確定命令協議型別,分派給 process_XX_command 函式處理。
Mc 的命令協議從直觀邏輯上可以分為獲取型別、變更型別、其他型別。但從實際處理層面區分,則可以細分為 get 型別、update 型別、delete 型別、算術型別、touch 型別、stats 型別,以及其他型別。對應的處理函式為,process_get_command, process_update_command, process_arithmetic_command, process_touch_command等。每個處理函式能夠處理不同的協議,具體參見下圖所示思維導圖。
conn_parse_cmd
注意 conn_parse_cmd 的狀態處理,只有讀取到 \n,有了完整的命令首行協議,才會進入 process_command,否則會跳轉到 conn_waiting,繼續等待客戶端的命令資料包文。在 process_command 處理中,如果是獲取類命令,在獲取到 key 對應的 value 後,則跳轉到 conn_mwrite,準備寫響應給連線緩衝。而對於 update 變更型別的指令,則需要繼續讀取 value 資料,此時連線會跳轉到 conn_nread 狀態。在 conn_parse_cmd 處理過程中,如果遇到任何失敗,都會跳轉到 conn_closing 關閉連線。complete_nread
對於 update 型別的協議指令,從 conn 繼續讀取 value 資料。讀取到 value 資料後,會呼叫 complete_nread,進行資料儲存處理;資料處理完畢後,向 conn 的 wbuf 寫響應結果。然後 update 型別處理的連線進入到 conn_write 狀態。conn_write
連線 conn_write 狀態處理邏輯很簡單,直接進入 conn_mwrite 狀態。或者當 conn 的 iovused 為 0 或對於 udp 協議,將響應寫入 conn 訊息緩衝後,再進入 conn_mwrite 狀態。conn_mwrite
進入 conn_mwrite 狀態後,工作執行緒將通過 transmit 來向客戶端寫資料。如果寫資料失敗,跳轉到 conn_closing,關閉連線退出狀態機。如果寫資料成功,則跳轉到 conn_new_cmd,準備下一次新指令的獲取。conn_closing
最後一個 conn_closing 狀態,前面提到過很多次,在任何狀態的處理過程中,如果出現異常,就會進入到這個狀態,關閉連線,這個連線也就 Game Over了。
Mc 啟動後,主執行緒監聽並準備接受新連線接入。當有新連線接入時,主執行緒進入 conn_listening 狀態,accept 新連線,並將新連線排程給工作執行緒。
Worker 執行緒監聽管道,當收到主執行緒通過管道傳送的訊息後,工作執行緒中的連線進入 conn_new_cmd 狀態,建立 conn 結構體,並做一些初始化重置操作,然後進入 conn_waiting 狀態,註冊讀事件,並等待網路 IO。
有資料到來時,連線進入 conn_read 狀態,讀取網路資料。
讀取成功後,就進入 conn_parse_cmd 狀態,然後根據 Mc 協議解析指令。
對於讀取指令,獲取到 value 結果後,進入 conn_mwrite 狀態。
對於變更指令,則進入 conn_nread,進行 value 的讀取,讀取到 value 後,對 key 進行變更,當變更完畢後,進入 conn_write,然後將結果寫入緩衝。然後和讀取指令一樣,也進入 conn_mwrite 狀態。
進入到 conn_mwrite 狀態後,將結果響應傳送給 client。傳送響應完畢後,再次進入到 conn_new_cmd 狀態,進行連線重置,準備下一次命令處理迴圈。
在讀取、解析、處理、響應過程,遇到任何異常就進入 conn_closing,關閉連線
本作品採用《CC 協議》,轉載必須註明作者和本文連結