高德渲染閘道器Go語言重構實踐

amap_tech發表於2021-08-31

​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)) 
}
高德渲染閘道器Go語言重構實踐高德渲染閘道器Go語言重構實踐

字串轉位元組切片

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)) 
}
高德渲染閘道器Go語言重構實踐
高德渲染閘道器Go語言重構實踐

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

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能力帶來的好處,可大大加強全鏈路服務可觀測性,這於對後續開展服務穩定性,全鏈路故障快速定位等工作有極大幫助。

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

誠招同路人

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

Happy Hacking!

相關文章