天天看點

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

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、車機、開放平台等來源所有的圖面渲染。大家在使用高德時,看到的建築物、地形圖、名稱、路線、地鐵站、公交站、紅綠燈等等所有圖面,都是由渲染引擎通過渲染網關透出到端。下面放幾張圖,友善大家有一些更感性的認識。

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

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

3.2 重構難點

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

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

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

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

3.3 技術方案

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

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

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)技術細節

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

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

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檔案總覽

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

檔案頭的格式和長度都是固定的,如下:

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

我們可以在讀取tcpdump檔案後,往後移動23位元組,然後開始處理每個資料包。每個資料包的格式如下:

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

我們處理每個資料包,将前邊的標頭,資料鍊路頭,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請求實際上已可以重新分别請求新老服務,對比響應二進制流。

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

一條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 流水線工作步驟圖

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
  • 指令讀取(instruction fetch,IF)
  • 指令解碼(instruction decode,ID)
  • 執行(execute,EXE)
  • 記憶體通路(memory access,MEM)
  • 寄存器回寫(register write-back,WB)

主要優化MEM步驟,利用CPU緩存盡可能減少MEM步驟所占時鐘周期,進而降低CPU負載。

高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望
高德渲染網關Go語言重構實踐1.導讀2. 高德為什麼要落地Go應用3. 大流量場景Go應用落地4. 技術幹貨5.未來展望

類似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!