Go 語言網絡庫 getty 的那些事
個人從事互聯網基礎架構系統研發十年餘,包括我自己在內的很多朋友都是輪子黨。
2011 年我在某大廠幹活時,很多使用 C 語言進行開發的同事都有一個自己的私人 SDK 庫,尤其是網絡通信庫。個人剛融入這個環境時,覺得不能基於 epoll/iocp/kqueue 接口封裝一個異步網絡通信庫,會在同事面前矮人三分。現在想起來當時很多同事很大膽,把自己封裝的通信庫直接在測試生產環境上線使用,據說那時候整個公司投入生產環境運行的 RPC 通信庫就有 197 個之多。
個人當時耗費兩年週末休息時間造了這麼一個私人 C 語言 SDK 庫:大部分 C++ STL 容器的 C 語言實現、定時器、TCP/UDP 通信庫、輸出速度可達 150MiB/s 的日誌輸出庫、基於 CAS 實現的各種鎖、規避了 ABA 問題的多生產者多消費者無鎖隊列 等等。自己當時不懂 PHP,其實稍微封裝下就可以做出一個類似於 Swoole 的框架。如果一直堅持寫下來,可能它也堪媲美老朋友鄭樹新老師的 ACL 庫了。
自己 2014 年開始接觸 Go 語言,經過一段時間學習之後,發現了它有 C 語言一樣的特點:基礎庫太少 -- 又可以造輪子了。我清晰地記得自己造出來的第一個輪子每個 element 只使用一個指針的雙向鏈表 xorlist【見參考 1】。
2016 年 6 月我在做一個即時通訊項目時,原始網關是一個基於 netty 實現的 Java 項目,後來使用 Go 語言重構時其 TCP 網路庫各個接口實現就直接借鑑了 netty。同年 8 月份在其上添加了 websocket 支持時,覺得 websocket 提供的 onopen/onclose/onmessage 網絡接口極其方便,就把它的網絡接口改爲 OnOpen/OnClose/OnMessage/OnClose ,把全部代碼放到了 github 上,並在小範圍內進行了宣傳【見參考 2】。
Getty 分層設計
Getty 嚴格遵循着分層設計的原則。主要分爲數據交互層、業務控制層、網絡層,同時還提供非常易於擴展的監控接口,其實就是對外暴露的網絡庫使用接口。
1.1
數據交互層
很多人提供的網絡框架自己定義好了網絡協議格式,至少把網絡包頭部格式定義好,只允許其上層使用者在這個頭部以下做擴展,這就限制了其使用範圍。Getty 不對上層協議格式做任何假設,而是由使用者自己定義,所以向上提供了數據交互層。
就其自身而言,數據交互層做的事情其實很單一,專門處理客戶端與服務器的數據交互,是序列化協議的載體。使用起來也非常簡單,只要實現 ReadWriter interface 即可。
Getty 定義了 ReadWriter 接口,具體的序列化 / 反序列化邏輯 則交給了用戶手動實現。當網絡連接的一端通過 net.Conn 讀取到了 peer 發送來的字節流後,會調用 Read 方法進行反序列化。而 Writer 接口則是在網絡發送函數中被調用,一個網絡包被髮送前,Getty 先調用 Write 方法將發送的數據序列化爲字節流,再寫入到 net.Conn 中。
ReadWriter 接口定義代碼如上。Read 接口之所以有三個返回值,是爲了處理 TCP 流粘包情況:
- 如果發生了網絡流錯誤,如協議格式錯誤,返回 (nil, 0, error)
**- **如果讀到的流很短,其頭部 (header) 都無法解析出來,則返回 (nil, 0, nil)
- 如果讀到的流很短,可以解析出其頭部 (header) 但無法解析出整個包 (package),則返回 (nil, pkgLen, nil)
- 如果能夠解析出一個完整的包 (package),則返回 (pkg, 0, error)
1.2
業務控制層
業務控制層是 Getty 設計的精華所在,由 Connection 和 Session 組成。
**- **Connection ****
負責建立的 Socket 連接的管理,主要包括:連接狀態管理、連接超時控制、連接重連控制、數據包的相關處理,如數據包壓縮、數據包拼接重組等。
- Session****
負責客戶端的一次連接建立的管理、記錄着本次連接的狀態數據、管理 Connection 的創建、關閉、控制數據的發送 / 接口的處理。
** 1.2.1 Session **
Session 可以說是 Getty 中最核心的接口了,每個 Session 代表着一次會話連接。
- 向下
Session 對 Go 內置的網絡庫做了完善的封裝,包括對 net.Conn 的數據流讀寫、超時機制等。
-** 向上**
Session 提供了業務可切入的接口,用戶只需實現 EventListener 就可以將 Getty 接入到自己的業務邏輯中。
目前 Session 接口的實現只有 session 結構體,Session 作爲接口僅僅是提供了對外可見性以及遵循面向編程接口的機制,之後我們談到 Session,其實都是在講 session 結構體。
** 1.2.2 Connection **
Connection 根據不同的通信模式對 Go 內置網絡庫進行了抽象封裝,Connection 分別有三種實現:
**- **gettyTCPConn:底層是 *net.TCPConn
-** gettyUDPConn**:底層是 *net.UDPConn
**- **gettyWSConn:底層使用第三方庫實現
1.3
網絡 API 接口 EventListener
本文開頭提到,Getty 網絡 API 接口命名是從 WebSocket 網絡 API 接口借鑑而來。Getty 維護者之一 郝洪範 同學喜歡把它稱爲 “監控接口”,理由是:網絡編程最麻煩的地方當出現問題時不知道如何排查,通過這些接口可以知道每個網絡連接在每個階段的狀態。
**「OnOpen」:**連接建立時提供給用戶使用,若當前連接總數超過用戶設定的連接數,則可以返回一個非 nil 的 error,Getty 就會在初始階段關閉這個連接。
**「OnError」:**用於連接有異常時的監控,Getty 執行這個接口後關閉連接。
**「OnClose」:**用於連接關閉時的監控,Getty 執行這個接口後關閉連接。
**「OnMessage」:**當 Getty 調用 Reader 接口成功從 TCP 流 / UDP/WebSocket 網絡中解析出一個 package 後,通過這個接口把數據包交給用戶處理。
**「OnCron」:**定時接口,用戶可以在這裏接口函數中執行心跳檢測等一些定時邏輯。
這五個接口中最核心的是 OnMessage,該方法有一個 interface{} 類型的參數,用於接收對端發來的數據。
可能大家有個疑惑,網絡連接最底層傳輸的是二進制,到我們使用的協議層一般以字節流的方式對連接進行讀寫,那這裏爲什麼要使用 interface{} 呢?
這是 Getty 爲了讓我們能夠專注編寫業務邏輯,將序列化和反序列化的邏輯抽取到了 EventListener 外面,也就是前面提到的 Reader/Writer 接口,session 在運行過程中,會先從 net.Conn 中讀取字節流,並通過 Reader 接口進行反序列化,再將反序列化的結果傳遞給 OnMessage 方法。
如果想把對應的指標接入到 Prometheus,在這些 EventListener 接口中很容易添加各種 metrics 的收集。
Getty 網絡端數據流程
下圖是 Getty 核心結構的類圖,囊括了整個 Getty 框架的設計。
|說明:圖中灰色部分爲 Go 內置庫
下面以 TCP 爲例介紹下 Getty 如何使用以及該類圖裏各個接口或對象的作用。其中 server/client 是提供給用戶使用的封裝好的結構,client 的邏輯與 server 很多程度上一致,因此本章只講 server。
Getty server 啓動代碼流程圖如上。
在 Getty 中,server 服務的啓動流程只需要兩行代碼:
第一行非常明顯是一個創建 server 的過程,options 是一個 func (*ServerOptions) 函數,用於給 server 添加一些額外功能設置,如啓用 ssl,使用任務隊列提交任務的形式執行任務等。
第二行的 server.RunEventLoop(NewHelloServerSession) 則是啓動 server,同時也是整個 server 服務的入口,它的作用是監聽某個端口(具體監聽哪個端口可以通過 options 指定),並處理 client 發來的數據。RunEventLoop 方法需要提供一個參數 NewSessionCallback,該參數的類型定義如下:
這是一個回調函數,將在成功建立和 client 的連接後被調用,一般提供給用戶用於設置網絡參數,如設置連接的 keepAlive 參數、緩衝區大小、最大消息長度、read/write 超時時間等,但最重要的是,用戶需要通過該函數,爲 session 設置好要用的 Reader、Writer 以及 EventListener。
至此,Getty 中 server 的處理流程大體如下圖:
對於這些接口的使用,除了 getty 自身提供的代碼示例外,另一篇極好的例子就是 seata-golang,感興趣的朋友可參閱 《分佈式事務框架 seata-golang 通信模型》一文【參考 6】。
優 化
軟件開發一條經驗法則是:“Make it work, make it right, make it fast”,過早優化是萬惡之源。
比較早期的一個例子是 erlang 的發明者 Joe Armstrong 早年花費了相當多的精力去熬夜加班改進 erlang 性能,其後果之一是他在後期發現早期做的一些優化工作很多是無用功,其二是過早優化損壞了 Joe 的健康,導致 2019 年在他 68 歲的年紀便掛掉了。
把時間單位拉長到五年甚至是十年,可能會發現早期所做的一些優化工作在後期會成爲維護工作的累贅。2006 年時很多專家還在推薦大家只用 Java 做 ERP 開發,不要在互聯網後臺編程中使用 Java,理由是在當時的單核 CPU 機器上與 C/C++ 相比其性能確實不行,理由當然可以怪罪於其解釋語言的本質和 JVM GC,但是 2010 年之後就幾乎很少聽見有人再抱怨其性能了。
類比之,作爲一個比 Java 年輕很多的新語言,Go 語言定義了一種編程範式,編程效率是其首要考慮,至於其程序性能尤其是網絡 IO 性能,這類問題可以交給時間,五年之後當前大家抱怨的很多問題可能就不是問題了。如果程序真的遇到網絡 IO 性能瓶頸且機器預算緊張,可以考慮換成更低級的語言如 C/C++/Rust。
2019 年時 MOSN 的底層網絡庫使用了 Go 語言原生網絡庫,每個 TCP 網絡連接使用了兩個 goroutine 分別處理網絡收發,當然後來經優化後做到了單個 TCP 連接僅使用一個 goroutine,並沒有採用 epoll 系統調用的方式進行優化。
再舉個栗子。
字節跳動從 2020 年便在知乎開始發文宣傳其 Go 語言網絡框架 kitex 的優秀性能【見參考 3】,說是基於原生的 epoll 後 “性能已遠超官方 net 庫” 云云。當時它沒開源代碼,大家也只能姑妄信之。2021 年年初,頭條又開始出來宣傳了一把【見參考 4】,宣稱 “測試數據表明,當前版本 (2020.12) 相比於上次分享時 (2020.05),吞吐能力 ↑30%,延遲 AVG ↓25%,TP99 ↓67%,性能已遠超官方 net 庫”。然後終於把代碼開源了。8 月初鳥窩大佬經過測試,並在《2021 年 Go 生態圈 rpc 框架 benchmark》(鏈接見 參考 5)一文中給出了測試結論。
說了這麼多,收回話題,總結一句話就是:Getty 只考慮使用 Go 語言原生的網絡接口,如果遇到網絡性能瓶頸也只會在自身層面尋找優化突破點。
Getty 每年都會一次重大的升級,本文給出 Getty 近年的幾次重大升級。
3.1
Goroutine Pool
Getty 初始版本針對一個網絡連接啓用兩個 goroutine:一個 goroutine 進行網絡字節流的接收、調用 Reader 接口拆解出網絡包 (package)、調用 EventListener.OnMessage() 接口進行邏輯處理;另一個 goroutine 負責發送網絡字節流、調用 EventListener.OnCron() 執行定時邏輯。
後來出於提升網絡吞吐的需要,Getty 進行了一次大的優化:將邏輯處理這步邏輯從第一個 goroutine 任務中分離,添加 Goroutine Pool【下文簡稱 Gr pool】專門處理網絡邏輯。
即網絡字節流接收、邏輯處理和網絡字節流發送都有單獨的 goroutine 處理。
Gr Pool 成員有任務隊列【其數目爲 M】和 Gr 數組【其數目爲 N】以及任務【或者稱之爲消息】,根據 N 的數目變化其類型分爲可伸縮 Gr pool 與固定大小 Gr pool。可伸縮 Gr Pool 好處是可以隨着任務數目變化增減 N 以節約 CPU 和內存資源。
3.1.1 固定大小 Gr Pool
按照 M 與 N 的比例,固定大小 Gr Pool 又區分爲 1:1、1:N、M:N 三類。
1:N 類型的 Gr Pool 最易實現,個人 2017 年在項目 kafka-connect-elasticsearch 中實現過此類型的 Gr Pool:作爲消費者從 kafka 讀取數據然後放入消息隊列,然後各個 worker gr 從此隊列中取出任務進行消費處理。
這種模型的 Gr pool 整個 pool 只創建一個 chan, 所有 Gr 去讀取這一個 chan,其缺點是:隊列讀寫模型是 一寫多讀,因爲 go channel 的低效率【整體使用一個 mutex lock】造成競爭激烈,當然其網絡包處理順序更無從保證。
Getty 初始版本的 Gr pool 模型爲 1:1,每個 Gr 多有自己的 chan,其讀寫模型是一寫一讀,其優點是可保證網絡包處理順序性, 如讀取 kafka 消息時候,按照 kafka message 的 key 的 hash 值以取餘方式【hash(message key) % N】將其投遞到某個 task queue,則同一 key 的消息都可以保證處理有序。但這種模型的缺陷:每個 task 處理要有時間,此方案會造成某個 Gr 的 chan 裏面有 task 堵塞,就算其他 Gr 閒着,也沒辦法處理之【任務處理 “飢餓”】。
更進一步的 1:1 模型的改進方案:每個 Gr 一個 chan,如果 Gr 發現自己的 chan 沒有請求,就去找別的 chan,發送方也儘量發往消費快的協程。這個方案類似於 go runtime 內部的 MPG 調度算法使用的 goroutine 隊列,但其算法和實現會過於複雜。
Getty 後來實現了 M:N 模型版本的 Gr pool,每個 task queue 被 N/M 個 Gr 消費,這種模型的優點是兼顧處理效率和鎖壓力平衡,可以做到總體層面的任務處理均衡,Task 派發採用 RoundRobin 方式。
其整體實現如上圖所示。具體代碼實現請參見 gr pool【參考 7】連接中的 TaskPool 實現。
3.1.2 無限制 Gr Pool
使用固定量資源的 Gr pool,在請求量加大的情況下無法保證吞吐和 RT,有些場景下用戶希望儘可能用盡所有的資源保證吞吐和 RT。
後來借鑑 "A Million WebSockets and Go" 一文【參考 8】中的 “Goroutine pool” 實現了一個 可無限擴容的 gr pool。
具體代碼實現請參見 gr pool【參考 7】 連接中的 taskPoolSimple 實現。
3.1.3 網絡包處理順序
固定大小的 gr pool 優點是限定了邏輯處理流程對機器 CPU/MEMORY 等資源的使用,而 無限制 Gr Pool 雖然保持了彈性但有可能耗盡機器的資源導致容器被內核殺掉。但無論使用何種形式的 gr pool,getty 無法保證網絡包的處理順序。
譬如 Getty 服務端收到了同一個客戶端發來的 A 和 B 兩個網絡包,Gr pool 模型可能造成服戶端先處理 B 包後處理 A 包。同樣,客戶端也可能先收到服務端對 B 包的 response,然後才收到 A 包的 response。
如果客戶端的每次請求都是獨立的,沒有前後順序關係,則帶有 Gr pool 特性的 Getty 不考慮順序關係是沒有問題的。如果上層用戶關注 A 和 B 請求處理的前後順序,則可以把 A 和 B 兩個請求合併爲一個請求,或者把 gr pool 特性關閉。
3.2
Lazy Reconnect
Getty 中 session 代表一個網絡連接,client 其實是一個網絡連接池,維護一定數量的連接 session,這個數量當然是用戶設定的。Getty client 初始版本【2018 年以前的版本】中,每個 client 單獨啓動一個 goroutine 輪詢檢測其連接池中 session 數量,如果沒有達到用戶設定的連接數量就向 server 發起新連接。
當 client 與 server 連接斷開時,server 可能是被下線了,可能是意外退出,也有可能是假死。如果上層用戶判定對端 server 確實不存在【如收到註冊中心發來的 server 下線通知】後,調用 client.Close()
接口把連接池關閉掉。如果上層用戶沒有調用這個接口把連接池關閉掉,client 就認爲對端地址還有效,就會不斷嘗試發起重連,維護連接池。
綜上,從一箇舊 session 關閉到創建一個新 session,getty client 初始版本的重連處理流程是:
-
1 舊 session 關閉
網絡接收 goroutine
; -
2 舊 session
網絡發送 goroutine
探測到網絡接收 goroutine
退出後終止網絡發送,進行資源回收後設定當前 session 無效; -
3 client 的輪詢 goroutine 檢測到無效 session 後把它從 session 連接池刪除;
-
4 client 的輪詢 goroutine 檢測到有效 session 數目少於 getty 上層使用者設定的數目 且 getty 上層使用者沒有通過
client.Close()
接口關閉連接池時,就調用連接接口發起新連接。
上面這種通過定時輪詢方式不斷查驗 client 中 session pool 中每個 session 有效性的方式,可稱之爲主動連接。主動連接的缺點顯然是每個 client 都需要單獨啓用一個 goroutine。當然,其進一步優化手段之一是可以啓動一個全局的 goroutine,定時輪詢檢測所有 client 的 session pool,不必每個 client 單獨啓動一個 goroutine。但是個人從 2016 年開始一直在思考一個問題:能否換一種 session pool 維護方式,去掉定時輪詢機制,完全不使用任何的 goroutine 維護每個 client 的 session pool?
2018 年 5 月個人在一次午飯後遛彎時,把 getty client 的重連邏輯又重新梳理了一遍,突然想到了另一種方法,在步驟 2 中完全可以對 網絡發送 goroutine
進行 “廢物利用”,在這個 goroutine 標記當前 session 無效的邏輯步驟之後再加上一個邏輯:
-
1 如果當前 session 的維護者是一個 client【因爲 session 的使用者也可能是 server】;
-
2 且如果其當前 session pool 的 session 數量少於上層使用者設定的 session number;
-
3 且如果上層使用者還沒有通過
client.Close()
設定當前 session pool 無效【即當前 session pool 有效,或者說是對端 server 有效】 -
4 滿足上面三個條件,
網絡發送 goroutine
執行連接重連即可; -
5 新網絡連接 session 建立成功且被加入 client 的 session pool 後,
網絡發送 goroutine
使命完成直接退出。
我把這種重連方式稱之爲 lazy reconnect
,網絡發送 goroutine
在其生命週期的最後階段應該被稱之爲 網絡重連 goroutine
。通過 lazy reconnect
這種方式,上述重連步驟 3 和 步驟 4 的邏輯被合入了步驟 2,client 當然也就沒必要再啓動一個額外的 goroutine 通過定時輪詢的方式維護其 session pool 了。
lazy reconnect
整體流程圖如上。如果對相關代碼流程感興趣,請移步 "參考 13" 給出的鏈接,很容易自行分析出來。
3.3
定時器
在引入 Gr pool 後,一個網絡連接至少使用三個 goroutine:
-
一個 goroutine 進行網絡字節流的接收、調用 Reader 接口拆解出網絡包 (package)
-
第二個 goroutine 調用
EventListener.OnMessage()
接口進行邏輯處理 -
第三個 goroutine 負責發送網絡字節流、調用
EventListener.OnCron()
執行定時邏輯以及lazy reconnect
在連接較少的情況下這個模型尚可穩定運行。但當集羣規模到了一定規模,譬如每個服務端的連接數達 1k 以上時,單單網絡連接就至少使用 3k 個 goroutine,這是對 CPU 計算資源和內存資源極大地浪費。上面三個 goroutine 中,第一個 goroutine 無可拆解,第二個 goroutine 實際是 gr pool 一部分,可優化的對象就是第三個 goroutine 的任務。
2020 年底 Getty 維護團隊首先把網絡字節流任務放入了第二個 goroutine:處理完邏輯任務後立即進行同步網絡發送。此處改進後,第三個 goroutine 就只剩下一個 EventListener.OnCron() 定時處理任務。這個定時邏輯其實可以拋給 Getty 上層調用者處理,但出於方便用戶和向後兼容的考慮,我們使用了另一種優化思路:引入時間輪管理定時心跳檢測。
2017 年 9 月時,我曾實現了一個 Go 語言 timer 時間輪庫 timer wheel
(鏈接見參考 10),兼容 Go 的 timer 所有原生接口,其優點是所有時間任務都在一個 goroutine 內執行。2020 年 12 月把它引入 getty 後,getty 所有的EventListener.OnCron()
定時處理任務均交由 timer wheel 處理,第三個 goroutine 就可以完美地消失了【後續:兩個月後發現 timer 庫被另一個擅長拉 star 的 rpc 項目 “借鑑” 走了 ^+^】。
此時第三個 goroutine 就剩下最後一個任務:lazy reconnect
。當第三個 goroutine 不存在後,這個任務完全可以放入第一個 goroutine:在當網絡字節流接收 goroutine
檢測到網絡錯誤退出前的最後一個步驟,執行 lazy reconnect
。
優化改進後的一個網絡連接最多隻使用兩個 goroutine:
-
一個 goroutine 進行網絡字節流的接收、調用 Reader 接口拆解出網絡包 (package)、
lazy reconnect
-
第二個 goroutine 調用
EventListener.OnMessage()
接口進行邏輯處理、發送網絡字節流
第二個 goroutine 來自 gr pool。考慮到 gr pool 中的 goroutine 都是可複用的公共資源,單個連接實際上只單獨佔用了第一個 goroutine。
3.4
**sync.Pool **
Getty 每個鏈接的網絡字節流接收函數 handleTCPPackage 使用了 bytes.Buffer 緩存每次收到的字節流。爲了提高 bytes.Buffer 的複用效率,就基於 sync.Pool 封裝了一個 bytes.Buffer 緩存池,代碼如下:
// https://github.com/dubbogo/gost/blob/v1.11.16/bytes/bytes_buffer_pool.go
var defaultPool *ObjectPool
func init() {
defaultPool = NewObjectPool(func() PoolObject {
return new(bytes.Buffer)
})
}
// GetBytesBuffer returns bytes.Buffer from pool
func GetBytesBuffer() *bytes.Buffer {
return defaultPool.Get().(*bytes.Buffer)
}
// PutIoBuffer returns IoBuffer to pool
func PutBytesBuffer(buf *bytes.Buffer) {
defaultPool.Put(buf)
}
// https://github.com/apache/dubbo-getty/blob/v1.4.5/session.go
func (s *session) handleTCPPackage() error {
...
pktBuf = gxbytes.GetBytesBuffer()
defer func() {
gxbytes.PutBytesBuffer(pktBuf)
}()
...
}
Getty 使用這段代碼安穩運行了很久。但 9 月 11 日【真是一個好日子】集團相關同學反饋 getty “在一個大量使用短鏈接的場景,XX 發現造成內存大量佔用,因爲大塊的 buffer 被收集起來了,沒有被釋放”。
通過定位,發現原因是 sync.Pool 把大量的 bytes.Buffer 對象緩存起來後沒有釋放。集團的同學簡單粗暴地去掉了 sync.Pool 後,問題得以解決。上面的代碼塊被簡化到如下形式:
// https://github.com/apache/dubbo-getty session.go
func (s *session) handleTCPPackage() error {
...
pktBuf = pktBuf = new(bytes.Buffer)
...
}
覆盤整個問題,我纔想起來 Go 1.13 對 sync.Pool 進行了優化:在 1.13 之前 pool 中每個對象的生命週期是兩次 gc 之間的時間間隔,每次 gc 後 pool 中的對象會被釋放掉,1.13 之後可以做到 pool 中每個對象在每次 gc 後不會一次將 pool 內對象全部回收。
根據 Go 1.13 的這個改進,我對 bytes.Buffer 做出瞭如下改進:
-
1 放入 bytes pool 中的 buf 的 size 在 [1kiB, 20kiB] 之間,防止因爲過小導致內存碎片過多,也防止過大內存被緩存導致系統內存使用緊張;
-
2 限制緩存對象數目,防止內存緩存過多導致內存使用緊張
相關代碼見 pr https://github.com/dubbogo/gost/pull/70。理論上,這個改進使得 bytes.Buffer 最多緩存 80MiB 的內存。
3.5
Getty 壓測
Getty 維護團隊的郝洪範同學,借鑑了 rpcx 的 benchmark 程序後實現了 getty benchmark 【參考 11】,對優化後的 v1.4.3 版本進行過壓測。
「壓測環境」:
**「壓測結果」: **
壓測結果如上,服務端 TPS 數達 12556,網絡吞吐可達 12556 * 915 B/s ≈ 11219 KiB/s ≈ 11 MiB/s。
如上圖,可見網絡壓測前後服務單的 CPU/MEMORY 資源變化,getty 服務端僅僅使用了 6% 的 CPU,內存佔用也僅越 100MiB,且在測試壓力去掉後很快就把資源還給了系統。
測試時 getty 服務端的 goroutine 使用見下圖:單個 TCP 連接僅使用一個 goroutine。
整個測試結果以及相關代碼請參加 benmark result (鏈接見【參考 12】)。這個壓測當然沒有壓出 Getty 的極限性能,但已經能夠滿足阿里主要場景下的使用需求。
發展 timeline
從我個人 2016 年時寫 Getty 開始,到目前有一個專門的開源團隊維護 Getty,Getty 一路走來殊爲不易。
梳理其 timeline,其主要發展時間節點如下:
- 2016 年 6 月份開發出第一個生產可用版本,支持 TCP/websocket 兩種通信協議,同年 10 月在 gocn 上發帖 https://gocn.vip/topics/8229 推廣;
**- **2017 年 9 月時,實現了一個 Go 語言 timer 時間輪庫 timer wheel
https://github.com/AlexStocks/goext/blob/master/time/time.go
- 2018 年 3 月在其上加入 UDP 通信支持;
- 2018 年 5 月支持基於 protobuf 和 json 的 RPC;
- 2018 年 8 月加入基於 zookeeper 和 etcd 的服務註冊和發現功能,取名 micro;
- 2019 年 5 月 getty 的底層 tcp 通信實現被獨立拆出遷入 github.com/dubbogo,後遷入 github.com/apache/dubbo-getty;
**- **2019 月 5 月 Getty RPC 包被攜程的兩位同學遷入 https://github.com/apache/dubbo-go/tree/master/protocol/dubbo, 構建了 dubbogo 基於 hessian2 協議的 RPC 層;
- 2019 年 5 月,加入固定大小 goroutine pool;
- 2019 年底,劉曉敏同學告知其基於 Getty 實現了 seata-golang;
- 2020 年 11 月,把網絡發送與邏輯處理合併放入 gr pool 中處理;
- 2021 年 5 月,完成定時器優化;
最後,還是如第三節優化開頭部分所說,Getty 維護團隊不追求無意義的 benchmark 數據,不做無意義的炫技式優化,只根據生產環境需求來進行自身改進。只要維護團隊在,Getty 穩定性和性能定會越來越優秀。
|****作 者|
「於雨」 GitHub 賬號 AlexStocks
dubbogo 社區負責人。一個有十一年服務端基礎架構和中間件研發一線工作經驗的程序員。陸續參與和改進過 Redis/Pika/Pika-Port/etcd/Muduo/Dubbo/dubbo-go/Sentinel-golang/Seata-golang 等知名項目,目前在螞蟻集團可信原生技術部從事雲原生工作。
**「郝洪範」 **GitHub 賬號 georgehao
Apache Dubbo Committer,getty 維護團隊成員。目前在京東零售部大數據平臺,熟練掌握 Go runtime 底層技術。
「董劍輝」 GitHub 賬號 Mulavar
2021 年 5 月份從浙江大學著名 VLIS 實驗室研究生畢業。曾在螞蟻集團杭州總部支付業務資金服務技術部實習,目前就職於美團大數據北京總部計算引擎組,從事 flink 相關開發工作。
** |參 考****|**
-
1 https://github.com/alexstocks/goext/blob/master/container/xorlist/xorlist.go
-
2 兼容 tcp 和 websocket 的一個簡潔網絡框架 getty https://gocn.vip/topics/8229
-
3 字節跳動在 Go 網絡庫上的實踐 https://juejin.cn/post/6844904153173458958
-
4 字節跳動 Go RPC 框架 KiteX 性能優化實踐 https://mp.weixin.qq.com/s/Xoaoiotl7ZQoG2iXo9_DWg
-
5 2021 年 Go 生態圈 rpc 框架 benchmark https://colobu.com/2021/08/01/benchmark-of-rpc-frameworks/
-
6 分佈式事務框架 seata-golang 通信模型 https://mp.weixin.qq.com/s/7xoshM4lzqHX9WqANbHAJQ
-
7 gr pool https://github.com/dubbogo/gost/blob/master/sync/task_pool.go
-
8 A Million WebSockets and Go https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/
-
9 task pool https://github.com/dubbogo/gost/blob/master/sync/base_worker_pool.go
-
10 timer wheel https://github.com/dubbogo/gost/blob/master/time/timer.go
-
11 getty benchmark https://github.com/apache/dubbo-getty/tree/master/benchmark
-
12 benmark result https://github.com/apache/dubbo-getty/pull/61#issuecomment-865698715
-
13 lazy connection https://github.com/AlexStocks/getty/blob/73b0928b957ab6b6773f6cfe95c909ae39001687/transport/client.go#L412
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/hihY7sEatJCHTTZHs3urfQ