天池中間件大賽Golang版Service Mesh思路分享
天池中間件大賽Golang版Service Mesh思路分享
這次天池中間件性能大賽初賽和複賽的成績都正好是,出乎意料的是作為Golang是這次比賽的「稀缺物種」,這次在前十名中我也是僥倖存活在C大佬和Java大佬的中間。
關於這次初賽《Service Mesh for Dubbo》難度相對複賽《單機百萬消息隊列的存儲設計》簡單一些,最終成績是,因為一些Golang的小夥伴在正式賽512並發壓測的時候大多都卡在6000分大關,這裡主要跟大家分享下我在這次Golang版本的一些心得和踩過的坑。
由於工作原因實在太忙,比賽只有周末的時間可以突擊,下一篇我會抽空整理下複賽《單機百萬消息隊列的存儲設計》的思路方案分享給大家,個人感覺實現方案上也是決賽隊伍中比較特別的。
What"s Service Mesh?
Service Mesh另闢蹊徑,實現服務治理的過程不需要改變服務本身。通過以proxy或sidecar形式部署的 Agent,所有進出服務的流量都會被Agent攔截並加以處理,這樣一來微服務場景下的各種服務治理能力都可以通過Agent來完成,這大大降低了服務化改造的難度和成本。而且Agent作為兩個服務之間的媒介,還可以起到協議轉換的作用,這能夠使得基於不同技術框架和通訊協議建設的服務也可以實現互聯互通,這一點在傳統微服務框架下是很難實現的。
下圖是一個官方提供的一個評測框架,整個場景由5個Docker 實例組成(藍色的方框),分別運行了 etcd、Consumer、Provider服務和Agent代理。Provider是服務提供者,Consumer是服務消費者,Consumer消費Provider提供的服務。Agent是Consumer和Provider服務的代理,每個Consumer或 Provider都會伴隨一個Agent。etcd是註冊表服務,用來記錄服務註冊信息。從圖中可以看出,Consumer 與Provider 之間的通訊並不是直接進行的,而是經過了Agent代理。這看似多餘的一環,卻在微服務的架構演進中帶來了重要的變革。
有關Service Mesh的更多內容,請參考下列文章:
What』s a service mesh? And why do I need one? (中文翻譯)
聊一聊新一代微服務技術 Service Mesh
賽題要求
服務註冊和發現
協議轉換(這也是實現不同語言、不同框架互聯互通的關鍵)
負載均衡
限流、降級、熔斷、安全認證(不作要求)
當然Agent Proxy最重要的就是通用性、可擴展性強,通過增加不同的協議轉換可以支持更多的應用服務。
Why Golang?
個人認為關於Service Mesh的選型一定會在Cpp和Golang之間,這個要參考公司的技術棧。如果追求極致的性能還是首選Cpp,這樣可以避免Gc問題。因為Service Mesh鏈路相比傳統Rpc要長,Agent Proxy需要保證輕量、穩定、性能出色。
關於技術選型為什麼是Golang?這裡不僅僅是為了當做一次鍛煉自己Golang的機會,當然還出於以下一些原因:
一些大廠的經驗沉澱,比如螞蟻Sofa Mesh,新浪Motan Mesh等。
K8s、docker在微服務領域很火,而且以後Agent的部署一定依託於k8s,所以Go是個不錯的選擇,親和度高。
Go有協程,有高質量的網路庫,高性能方面應該佔優勢。
優化點剖析
官方提供了一個基於Netty實現的Java Demo,由於是阻塞版本,所以性能並不高,當然這也是對Java選手的一個福音了,可以快速上手。其他語言相對起步較慢,全部都要自己重新實現。
不管什麼語言,大家的優化思路大部分都是一樣的。這裡分享一下Kirito徐靖峰非常細緻的思路總結(Java版本):天池中間件大賽dubboMesh優化總結(qps從1000到6850),大家可以作為參考。
下面這張圖基本涵蓋了在整個agent所有優化的工作,圖中綠色的箭頭都是用戶可以自己實現的。
全部過程變成,所有請求均採用非同步回調的形式。這也是提升最大的一點。
自己實現Http服務解析。
Agent之間通信採用最簡單的自定義協議。
網路傳輸中。
Agent之間通信發送。
Provider負載均衡:加權輪詢、(效果並不是非常明顯)
Tcp連接負載均衡:支持按最小請求數選擇Tcp連接。
Dubbo請求。
Tcp參數的優化:開啟TCP_NODELAY(disable Nagle algorithm),調整Tcp發送和讀寫的緩衝區大小。
網路辛酸史 —— (預熱賽256並發壓測4400~4500)
Go因為有協程以及高質量的網路庫,協程切換代價較小,所以大部分場景下Go推薦的網路玩法是每個連接都使用對應的協程來進行讀寫。
這個版本的網路模型也取得了比較客觀的成績,QPS最高大約在4400~4500。對這個網路選型簡單做下總結:
Go因為有goroutine,可以採用多協程來解決並發問題。
在linux上Go的網路庫也是採用的epoll作為最底層的數據收發驅動。
Go網路底層實現中同樣存在「上下文切換」的工作,只是切換工作由runtime調度器完成。
網路辛酸史 —— (正式賽512並發壓測)
然而在正式賽512並發壓測的時候我們的程序並沒有取得一個穩定提升的成績,大約5500 ~ 5600左右,。
獲得高分的秘訣分析:
Consumer Agent壓力繁重,給Consumer Agent減壓。
由於Consumer的性能很差,Consumer以及Consumer Agent共生於一個Docker實例(4C 8G)中,只有避免資源爭搶,才能達到極致性能。
Consumer在壓測過程中Cpu佔用高達約350%。
為了避免與Consumer爭搶資源,需要把Consumer Agent的資源利用率降到極致。
通過上述分析,我們確定了優化的核心目標:。
a. 優化方案1:協程池 + 任務隊列(廢棄)
這是一個比較簡單、常用的優化思路,類似線程池。雖然有所突破,但是並沒有達到理想的效果,cpu還是高達約70~80%。Goroutine雖然開銷很小,畢竟高並發情況下還是有一定上下文切換的代價,只能想辦法再去尋找一些性能的突破。
。關於Netty的架構學習在這就不再贅述,推薦同事的一些分享總結閃電俠的博客。
b. 優化方案2:Reactor網路模型
選型之前諮詢了幾位好朋友,都是遭到一頓吐槽。當然他們沒法理解我只有不到50%的Cpu資源可以利用的困境,最終還是毅然決然地走向這條另類的路。
經過一番簡單的調研,我找到了一個看上去還挺靠譜(Github Star2000, 沒有一個PR)的開源第三方庫evio,但是真正實踐下來遇到太多坑,而且功能非常簡易。不禁感慨Java擁有Netty真的是太幸福了!Java取得成功的原因在於它的生態如此成熟,Go語言這方面還需要時間的磨鍊,高質量的資源太少了。
當然不能全盤否定evio,它可以作為一個學習網路方面很好的資源。先看Github上一個簡單的功能介紹:
為了能夠達到極致的性能,我對evio進行了大量改造:
支持主動連接(默認只支持被動連接)
支持多種協議
減少無效的喚醒次數
支持非同步寫,提高吞吐率
修復Linux下諸多bug造成的性能問題
改造之後的網路模型也是取得了很好的效果,可以達到的分數,但這還遠遠不夠,還需要再去尋找一些突破。
c. 復用EventLoop
對優化之後的網路模式再進行一次梳理(見下圖):
可以把eventLoop理解為io線程,在此之前每個網路通信c->ca,ca->pa,pa->p都單獨使用的一個eventLoop。。於是做了最後一個關於網路模型的優化:,通過判斷連接類型分別處理不同的邏輯請求。
復用eventloop得到了一個比較穩健的成績提升,每個階段的eventloop的資源數都設置為1個,最終512並發壓測下cpu資源佔用率約50%。
Go語言層面的一些優化嘗試
最後階段只能喪心病狂地尋找一些細節點,所以也對語言層面做了一些嘗試:
Ringbuffer來替代Go channel實現任務分發
RingBuffer在高並發任務分發的場景中比Channel性能有小幅度提升,但是站在工程的角度,個人還是推薦Go channel這種更加優雅的做法。
Go自帶的encoding/json包是基於反射實現的,性能是個詬病
使用字元串自己拼裝Json數據,這樣壓測的數據越多,節省的時間越多。
Goroutine線程綁定
修改調度器默認時間片大小,自己編譯Go語言(沒啥效果)
總結
劍走偏鋒,花費了大量時間去改造網路,功夫不負有心人,結果是令人欣慰的。
Golang在高性能方面是足夠出色的,值得深入研究學習。
。
最後拋出幾個想繼續探討的Go網路問題,和大家一起討論,有經驗的朋友還希望能指點一二:
在資源稀少的情況下,處理高並發請求的網路模型你會怎麼選型?(假設並發為1w長連接或者短連接)
百萬連接的規模下又將如何選型?
TAG:碼農的純真年代 |