高德渲染網關Go語言重構實踐

​1.導讀

高德啓動Go業務建設已經有段時間了,主要包含Go應用落地Go中間件建設雲原生三個部分。經過持續的發力,在這些方面取得了不錯的進展。高德Go業務落地過程是如何實現的,遇到過哪些問題,如何解決?本文將爲大家介紹相關經驗,希望對感興趣的同學有所幫助。

2. 高德爲什麼要落地Go應用

現在高德內主流的語言還是Java,Java應用最多,機器數十分驚人。而且高德整體業務也在快速向前奔跑,成本增加的速度非常快。在減少機器負載方面,Go語言在語言級別對Java語言有相當優勢。減少機器成本是我們落地Go應用的第一個考慮因素。

其次,Go語言近幾年發展勢頭迅猛,不論是阿里集團內部,還是在高德內部,對使用Go語言的呼聲越來愈高。落地Go應用可以很好的驗證Go中間件的穩定性。當然我們可以通過混沌工程等手段去驗證,但經過生產環境考驗才最具有說服力。驗證沉澱Go語言中間件穩定性是我們落地Go應用的第二個考慮因素。

最後,Go語言作爲雲原生基礎框架使用較多的語言,提前落地Go應用,對後續落地雲原生可以減少不少阻力。高德目前落地的Serverless/Faas規模相當大。落地Go應用的第三個考慮因素是爲後續雲原生落地鋪路。

3. 大流量場景Go應用落地

3.1 渲染網關介紹

本文所述中提到的高德渲染網關,是我們落地的Go應用中業務流量、改造難度、風險,收益均處前列的應用。渲染網關在接入層,佔高德總流量的一半,重要性可想而知。

接下來簡要介紹下渲染網關承接的業務,方便大家有一些更立體的認識。

渲染網關承接高德手機App、車機、開放平臺等來源所有的圖面渲染。大家在使用高德時,看到的建築物、地形圖、名稱、路線、地鐵站、公交站、紅綠燈等等所有圖面,都是由渲染引擎通過渲染網關透出到端。下面放幾張圖,方便大家有一些更感性的認識。

上面圖一爲行前,圖二爲行中,圖三爲打車頁面,圖四爲景區手繪圖。渲染網關涉及業務衆多,以上僅爲舉例,其他業務就不在這裏貼圖了。

3.2 重構難點

做過重構項目的同學相信都深有體會,重構項目中最大難點有二,一是要保證業務正確性,二是要保證服務穩定性。

對於保證業務正確性,一般來說,重構的服務大多數爲老服務,老服務面臨的最大問題是歷史邏輯複雜,人員更迭,文檔缺失,這些因素都是重構過程中的“攔路虎”。

渲染網關重構同樣如此,它涉及高德手機端、車機端、開放平臺、打車等各個業務線,所有的歷史版本,再加上上述因素,所以保證業務正確性是一件非常困難的工作。

對於保證服務穩定性,做過網關的同學應該都知道,網關本身的屬性就決定了它並不會有頻繁的業務迭代,穩定性是網關的第一訴求。我們要保證,無論外部環境/依賴是否正常,網關始終能保持高可用。由於Go版本中間件缺乏在大流量場景的充分驗證,這一難點需要仔細評測,用合適的方法和手段,儘可能的在仿真環境裏驗證各種邊界情況,從而保證在生產環境不出問題。

3.3 技術方案

在重構高德渲染網關時,我們整體技術方案分三大步走:

3.3.1 線上流量對比

如何驗證新服務的業務正確性呢?我們採用了線上流量對比的方式。

我們前期做了大量調研,希望找到一個滿足(近)實時,二進制級對比的工具,但可惜並沒有找到一個滿足要求的工具。由於渲染業務的特殊屬性,渲染網關絕大多數接口返回的是二進制矢量數據,所以理想的工具不僅要能支持常規數據對比,也要能支持二進制級對比。

二進制級對比的另一個好處是,可以排除字符集差異,不同語言庫函數差異。更能保證對比的準確性。有些同學可能會想到打日誌,然後離線讀取比較的方式來做對比,這種方式有很多弊端。

首先,流量無法重放至指定機器。其次,這種使用方式一般爲固定語料,語料完整度不夠,不能完全模擬線上環境。此外,打日誌對比帶來的字符集和語言庫函數差異,會對比較準確性有較大影響,特別是對於特殊字符(當7層協議爲二進制協議時更加明顯)。沒有現成的稱手工具,怎麼辦?"逢山開路,遇水搭橋"。

我們自主研發了一款(近)實時流量對比工具,它保障了此次重構的業務正確性,並且還能服務於高德其他業務的重構。其技術細節對TCP/IP涉及較多,非常有意思,感興趣的同學可以直接跳至《流量對比工具(ln)技術細節》一節。

3.3.2 仿真環境壓測

做服務的同學相信都深有體會,想讓服務保障做到5個9的可用性並不是一件容易的事。真實生產環境中可能會出現各種情況,我們要想辦法驗證各種邊界情況下服務的穩定性,才能保障服務高可用。對於重構完成的新服務,更需要一個仿真環境,進行各種情況驗證。

構建仿真環境,我們需要保持機器基線、外部依賴、外部流量均一致(比如從線上引流)。仿真環境不僅要提供正常態環境的能力,更要能提供異常態環境的能力。

異常態包括斷網,網絡丟包等等。有句話說的好:20%的代碼完成功能,80%的代碼來處理各種異常情況。我們在實踐中構建異常態的主要手段爲混沌工程,通過混沌工程模擬下至操作系統級的異常(如斷網,丟包等),上至應用層的異常(如消息中間件積壓,JVM方法前後Hook模擬業務異常等等)。

在仿真環境裏,同時進行長時間極限壓測,語料從線上導流,壓測在正常態,異常態均進行,觀察服務在一段較長時間內的表現,從而得出服務的穩定性,可用性結論。

觀測指標包括基礎指標,例如CPU、磁盤利用率、內存利用率、連接數,以及業務指標,例如業務接口成功率、成功量、總量、TP99。通過這種方式,基本上完全覆蓋了可能出現各種情況,充分保證了服務穩定性和高可用。

3.3.3 平滑灰度切流

前邊講了如何保證業務正確性和服務穩定性。接下來說說如何保證平滑灰度切流。牢牢遵守阿里發佈三原則是平滑灰度切流的“法寶”:可灰度可監控可回滾

在具體實踐中,我們按照如下步驟灰度切流

a. 原Java集羣不動,新申請一套Go集羣。修改路由規則,部分白名單用戶使用Go集羣服務。

b. 逐個接口修改路由規則至Go集羣,慢慢灰度,期間密切觀察機器姿態,業務日誌,監控指標。如有異常一鍵切回至Java集羣。

c. 接口全量切至Go集羣后,Java集羣/Go集羣同時共存一段時間。

d. 逐漸下掉Java集羣機器。

3.4 主要收益

第一個重要收益:降本提效。高德渲染網關由Java換成Go語言之後,機器數減少近一半。用原來一半的資源完成了相同的工作,大大降低了成本,提高了資源利用率,更好支持了業務發展,大大降低了業務流量快速增長帶來的接入層機器增長速度。

第二個重要的收益是:驗證了高德與集團合作共建的Go版本中間件的穩定性,一定程度上完善繁榮了集團Go生態。在大流量場景考驗過後,高德與集團合作共建的Go版本中間件穩定性得到了相當充分的驗證。

第三個重要的收益是:爲網關雲原生化鋪路。網關Go化只是第一步,Go是雲原生基礎設施實現使用較多的語言,第一步抹平語言差異,對於網關後續雲原生化,好處多多,可降低改造風險和成本。

當然,高德渲染網關重構過程中還有許多非常有用的工具沉澱。可爲後續業務重構提供關鍵性保障,比如自研的流量對比工具ln。

4. 技術乾貨

4.1 流量對比工具(ln)技術細節

先提一個問題,做一款(近)實時流量對比工具需要完成哪些功能?沒錯,就是流量複製,流量解析,流量重放,流量比對。其實不止這些,在實踐中更多是一個流量回歸閉環,如下圖:

4.1.1 流量複製

爲了支持所有的7層協議,流量獲取必須從3層或4層開始。有同學會立馬想到tcpdump。沒錯,就是tcpdump。tcpdump出的文件就是實實在在的流量。複製流量這一步已經有着落了,至於實時,可以兩到三個進程錯開時間,時間段首尾互相重疊即可完成實時。

另外,設計此工具的另一個考量點是,對線上機器不能有太重的負載,避免對線上機器產生穩定性影響。此種流量複製方式非常輕量,對線上機器增加的負載非常小,可以忽略不計。

4.1.2 流量上傳&流量拉取

流量上傳和流量拉取均使用內部文件服務。

4.1.3 流量對比

流量對比爲了保證對比的嚴謹性,排除可能的字符集干擾/不同庫函數實現干擾,我們原生支持了二進制流對比。

4.1.4 問題流量本地重放Debug

迴歸流量時,可能會發現部分流量比對不一致,這時我們希望只重放特定流量到指定機器,以便於Debug或其他操作,ln原生支持了此功能。

4.1.5 流量解析

流量解析非常有意思,這種單純的快樂來自於對網絡協議的"把玩"。

實際做法就是如何解析tcpdump文件,拿到tcp payload,還原出http請求。

這裏有兩個關鍵點,一是我們如何從tcpdump文件中拿到tcp payload,二是我們如何把四層的tcp payload重新聚合成七層的http請求。

4.1.5.1 tcpdump文件格式

先說如何從tcpdump文件拿到tcp payload,如果能知道tcpdump文件的格式,不就可以知道tcp payload在哪個位置,長度如何了麼?這一趴我們就來看看tcpdump文件格式。

先看tcpdump文件總覽

文件頭的格式和長度都是固定的,如下:

我們可以在讀取tcpdump文件後,往後移動23字節,然後開始處理每個數據包。每個數據包的格式如下:

我們處理每個數據包,將前邊的包頭,數據鏈路頭,ip層頭,tcp協議頭依次跳過,最終偏移到tcp payload第一個字節位置。其中的更多實現細節(不同層的頭字段值的判斷,不同長度的判斷,大小端的判斷,請求數據包與響應數據包如何對應等等)在此不再展開。這裏只介紹大體思路,感興趣的同學可以深挖網絡協議。

4.1.5.2 tcp payload還原http請求

這一部分介紹如何將tcp payload還原成http請求(此處http指http1.0/1.1,不含http2),ln工具中的完整實現是由tcp payload還原出請求及對應的響應,此處爲了便於理解,僅講解如何解析http請求。解析出http請求實際上已可以重新分別請求新老服務,對比響應二進制流。

一條tcp連接,多個payload發送(這裏僅做示意,判斷丟包重發等諸多情況屬於代碼細節,在此不再展開)。可能多個payload對應一個http請求;也可能一個payload的前一部分對應一個http請求,後一部分對應另一個http請求。我們要做的就是把多個payload形成的字節流讀入,按http幀的格式,聚合http請求即可。另外,http2的請求不能按這種方式聚合。

4.2 一些go語言最佳實踐

4.2.1 sync.pool 實踐

由於Go語言和Java語言的內存管理機制不相同,在內存的申請,釋放開銷也有差別。

對於Go語言來說,sync.pool是複用內存的一把利器。sync.pool優點有許多,比如減少內存的申請,減少了系統調用,減少了gc的壓力。但事物都有兩面性,sync.pool同樣如此,我們在使用sync.pool的時候需要注意,存放在sync.pool裏的對象會在不通知的情況下被回收掉,所以類似數據庫連接等資源不適合使用sync.pool。

總之,sync.pool可以複用內存,減少機器負載,非常適合臨時對象。

4.2.2 Golang Byte

Go語言Byte類型爲無符號,Java語言Byte類型爲有符號,在Java服務遷移Go服務過程中,Java代碼中Byte類型正、負、零的比較要注意。

4.2.3 Golang字節切片與字符串高效轉換

字節切片轉字符串

func Bytes2String(b []byte) string { 
    return *(*string)(unsafe.Pointer(&b)) 
}

字符串轉字節切片

func String2Bytes(s string) []byte {     
    x := (*[2]uintptr)(unsafe.Pointer(&s))     
    h := [3]uintptr{x[0], x[1], x[1]}     
    return *(*[]byte)(unsafe.Pointer(&h)) 
}

使用此種方式轉換,性能很高。原因在於底層無新的內存申請與拷貝。但是不論是字節切片轉字符串,還是字符串轉字節切片,字節切片中的值更改都會影響字符串的值,使用者要根據業務邏輯判斷能否接受,要更精確的把控生命週期。

4.2.4 Golang庫函數重寫

對於網關來說,耗CPU比較多的一部分是Hash函數/編解碼函數/加解密函數/序列化反序列化函數等。在實踐中我們重寫了相關的庫函數,在CPU負載上做了大量優化。

想要降低CPU負載,我們得先知道CPU是如何工作的,才能知道如何寫代碼會更好的降低CPU負載。這裏會介紹粗略的CPU工作原理。

放張CPU 流水線工作步驟圖

  • 指令讀取(instruction fetch,IF)
  • 指令解碼(instruction decode,ID)
  • 執行(execute,EXE)
  • 內存訪問(memory access,MEM)
  • 寄存器回寫(register write-back,WB)

主要優化MEM步驟,利用CPU緩存儘可能減少MEM步驟所佔時鐘週期,從而降低CPU負載。

類似NUMA架構,affinity等降低CPU負載的方式也是同樣的思想,儘可能減少Load數據所需的時鐘週期。

對於優化Golang庫函數來說,可以提升的點有兩個:優化算法本身;優化CPU緩存親和度。

我們專注於第二種,拿base64編解碼函數舉例,傳入的Byte切片與返回Byte切片,底層並非爲同一數組,同一內存。這中間就涉及兩塊可以額外消耗CPU時鐘週期的點,一是內存的申請與釋放,二是兩塊內存分別訪問帶來的CPU緩存爭用問題(與僞共享不完全一樣)。

如果我們複用傳入的內存呢?即邊解碼邊覆寫同一塊內存。美妙的事情發生了,上邊所說的問題不存在了。用更少的時鐘週期完成了一樣的工作。需要注意的是,由於函數的輸入和輸出使用同一塊內存,對程序開發者來說有更高的編碼要求,即對數據在程序中流轉的生命週期有更精準的把控力,代碼要打磨的很細緻。

5.未來展望

網關的下一步是雲原生化,採用Service Mesh方式實現。這可以解決目前中心化網關的弊端,去中心化可以提升接入層穩定性,減少爆炸半徑,增強隔離能力,實現更精細粒度的管控。

其次,降低機器成本,按照目前內部壓測及業界已有的實踐壓測結論,Mesh化後成本會進一步減少,考慮到現有RPC框架本身的消耗,成本會進一步縮減。且數據面代理也在不斷優化中,後續性能表現會更優異,額外兩跳對機器的負載將進一步下降。

再有,**網絡層能力集大大增強。**網關Mesh化,可以帶動上游業務Mesh化,最後在整個網絡層做一個能力超集。

現有的Service Mesh框架提供的能力可以概括爲Connect,Secure,Control,Observe四大部分,其能力是現有網關能力的超集,可以做到之前做不到的事情,最明顯的是Observe能力帶來的好處,可大大加強全鏈路服務可觀測性,這於對後續開展服務穩定性,全鏈路故障快速定位等工作有極大幫助。

以上要做的事情任重而道遠,另外我們在會做更多雲原生的試點和落地,技術同學都清楚,從技術選型到技術原型,再到實際業務落地,中間有很長的路要走。但路選對了,就不怕遠。

誠招同路人

筆者所在團隊求賢若渴,盼有熱情的技術小夥伴一起做些有趣的事,各技術棧均可,有意願的小夥伴請盡情砸簡歷到郵箱[email protected],郵件主題爲:姓名-技術方向-來自高德技術。

Happy Hacking!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章