1. 程式人生 > >治理Go模塊 服務治理 中臺業務 Golang的net.Conn接口,double close

治理Go模塊 服務治理 中臺業務 Golang的net.Conn接口,double close

分布式框架 什麽是對象 系統底層 規模 直觀 翻譯 鏈接 調用 形式

小結:

1、中臺業務 前臺業務

快車、專車、順風車,在滴滴這些業務線叫做前臺服務,他們有一些共同的特性,都有司機信息,訂單的狀態,收銀,賬號等等這些業務邏輯,我們會把專門的業務邏輯集合起來,形成專職的服務,這些就是中臺服務。

2、

通過TraceID對所有請求的進行串聯,通過SpanID記錄每個節點的耗時情況。

3、

我們開發的時候,大家都會使用一些動態語言的特性,比如說可能對String和數值類型沒有做區分,或者PHP的關聯數組和普通數組混合起來操作。這個時候硬性翻譯,就需要Golang代碼做大量的adapter適配語言差異,這個對Golang業務代碼的汙染很嚴重。

除了翻譯Go的代碼,還需要額外做一層Proxy,或SDK。我們希望Go server專註於它的業務,關註業務邏輯就好了,不要關註接口細節,如果接口不一致,或者因為一些特別的原因接口類型不一致的話,通過Proxy層,我們對Golang的接口和Proxy的接口的差異進行屏蔽,保證我們的Go server比較純凈,只關註自己的業務就可以。同時Proxy層也能幫助我們導流,比如說切流的時候用Proxy切流,通過Proxy的間隔,就可以保證Client對我們Go server遷移是無感知的,同時這個Proxy也可以把線下流量進行記錄,幫助線下Go server進行測試。

https://mp.weixin.qq.com/s/dWTqPOOadcjoiDjqDgNyow

基於Go構建滴滴核心業務平臺的實踐

作者簡介:

石松然,滴滴資深開發工程師,負責中臺業務的維護和開發工作。本文主要內容是基於Go構建滴滴核心業務平臺的實踐經驗。

內容大綱:

1?Golang 在滴滴業務的應用發展及規模

2?滴滴使用Go治理模塊的經驗

3?分享兩個具體的Go在應用上的問題

4?推薦兩個開源工具

作者簡介:

石松然,滴滴資深開發工程師,負責中臺業務的維護和開發工作。本文主要內容是基於Go構建滴滴核心業務平臺的實踐經驗。

內容大綱:

1?Golang 在滴滴業務的應用發展及規模

2?滴滴使用Go治理模塊的經驗

3?分享兩個具體的Go在應用上的問題

4?推薦兩個開源工具

正文

1.Go 在滴滴內部的應用和發展的情況

在滴滴的代碼倉庫裏面有超過 1500 多個模塊是含有 Golang 的代碼片斷的,有1800多位 Gopher 在滴滴提交過 Golang 編寫的代碼,僅僅是我們的中臺服務,就有2000多臺機器在跑 Go 的服務。

1.1 我們用Go做了什麽

DUSE 是滴滴的分單引擎,負責滴滴司機和乘客的撮合,每秒鐘負責萬級的撮合需求。

DOS 是滴滴訂單系統,負責實時訂單狀態的流轉,同時也負責滴滴歷史訂單的檢索,是一個百億級別數據的檢索服務。

DISE 是我們自主開發的 schemaless 數據存儲服務,使用了類似 Bigtable 的實現。

DESE 服務是一個serverless 分布式框架,只需完成業務函數即可完成分布式業務的搭建,類似亞馬遜的 Lambda 業務。

1.2 中臺業務

技術分享圖片

1.3 挑戰

開發中臺服務的時候遇到一些挑戰,主要來自三個方面:

1?高可用:中臺服務支撐了所有的前臺服務,出現問題就會導致前臺服務集體掛掉,高可用是非常重要的。

2?高並發:中臺服務是所有流量的承載體,需要非常高的承載能力和很快的響應速度。

3?業務復雜:中臺服務也是一個業務服務,所以業務的復雜程度直接決定了中臺系統有多復雜。

1.4 Why Golang

第一是執行效率非常高

第二是優秀的開發效率,Golang的語法比較簡潔清楚,可以屏蔽很多技術細節,讓業務開發更加順暢。

第三是Golang活躍的社區和豐富的庫,幫我們解決很多問題。

第四是學習成本低,我們剛開始使用Go的時候,發現工程師非常難招,其他語言的工程師通過很快的學習就可以了解Go,熟悉Go,開發Go的程序。

2.滴滴在治理Go模塊的經驗

2.1 龐大的業務系統

滴滴業務比較特殊,每一個請求都涉及到司機、乘客、訂單的三者的狀態信息,我們有很多微服務保證服務的狀態。我做過簡單的統計,如果把一個快車訂單拿出來看,涉及的子服務有50多個,Rpc請求達到了300個,日誌行數是1000多條,這樣人工進行分析非常困難。

2.2 服務治理的難題

微服務過多帶來很多的問題,比如異常定位比較困難;系統鏈路不清楚哪一塊好,哪一塊差;做服務優化和服務遷移的也會比較困難。針對這三點,我介紹一下滴滴是怎麽做的。

異常定位

隨著初期滴滴業務的野蠻增長,很多服務沒有遵循開發規範,導致我們滴滴日誌混亂,異常定位也非常困難,缺少上下遊基本定位的信息。 大量工程師的人力都浪費在異常定位、異常分析上面了,隨著我們的業務發展,後期人力投入會越來越大。我們就想能不能做異常的分布式追蹤,還有日誌規範化的工作。

日誌規範化

技術分享圖片

日誌串流

技術分享圖片

我們參考了OpenTracing的一套邏輯,通過SpanID和TraceID。在日誌中全部存留TraceID和SpanID,通過TraceID對所有請求的進行串聯,通過SpanID記錄每個節點的耗時情況。同時把日誌結構規範成對人友好,對機器可解析的一套日誌規範,通過DLTAG表達此條日誌記錄了什麽的。僅僅是規範日誌還不夠,只是比較容易將日誌串聯起來了,如果依賴人工分析的話,還是非常困難的。這個時候我們做了統一的處理系統,統一對日誌的數據進行采集、計算、存儲、索引,以及最後的可視化。這套系統怎麽做的呢?我們的日誌一般來自於服務端以及APP端,我們通過日誌采集服務SWAN將這些日誌收集過來,發送到消息隊列中;SRIUS服務從消息隊列中讀取日誌信息,把日誌變成我們結構化的數據,再將結構化的數據存儲於ARIUS系統當中,ARIUS系統底層實際上是ES的檢索,通過對日誌索引的建設,能夠幫助我們對日誌快速的檢索。最後有一個把脈,實際上是建立在ARIUS系統上的一個應用,通過ARIUS查詢完成滴滴對業務的鏈路還原,服務分析和性能的追查。

技術分享圖片

這是我們把脈生成的線上服務追蹤的鏈路,大家可以比較清楚地看到每個請求的耗時,協議。

技術分享圖片

把脈解決了滴滴異常追蹤的問題,但是我們還是不能回答系統的吞吐的瓶頸是什麽,系統總的容量是多少,新建機房是否用,以及災備預案是不是可靠的。一般來說在業界我們解決這些問題,最好的方式就是跑壓測,可惜的是由於滴滴業務比較特殊——我自己發明了一個詞叫非函數式的業務——就是說我們相同的輸入得到的輸出是不同的,輸入和輸出之間有很多狀態信息,司機狀態、訂單狀態,包括今天是不是下雨,是不是高峰期等等這些東西,都會影響業務結果,把這些都做到完全一致非常困難。這種情況下我們很難通過流量回放的方式來進行壓測,同時也由於涉及到狀態信息,很難通過線下壓測等比放大估計整個系統的容量。

既然線下壓不到,就線上壓測好了,內部稱之為全鏈路壓測。基礎的邏輯就是通過對壓測流量添加一個額外的標識,比如thrift協議加一個額外的參數,HTTP協議添加額外的Header,就可以把這些流量進行區分開。

技術分享圖片

技術分享圖片

我們將流量進行標識以後,流量經過的每個業務模塊都需要進行額外的開發工作。當業務模塊識別到壓測流量的時候,業務模塊需要對這個流量進行標記的透傳,保證所有的業務模塊都可以感知到壓測的標識。我們的Cache模塊會對這個流量設計一個較短的超時,以保證在壓測結束後,緩存資源能夠被盡快的釋放。最後就是數據庫這些地方,我們會建立一套和線上完全一樣的數據結構,一套table,我們叫影子表,影子表只負責處理壓測流量。我們管剛剛那套流量標記的邏輯和各個模塊的修改叫壓測通道。滴滴的壓測頻率非常頻繁,除了對新機房進行壓測以外,也會周期性都會對業務壓測,以保證在業務快速變更的同時,能夠滿足系統容量設計的預期。壓測範圍包括了我們所有主流程模塊,以保證我們的服務比較穩定。

解決了線上壓測的問題,那怎麽發測呢?剛剛講到,業務很難通過流量回放來做壓力測試。

滴滴采取的方案是,通過Mock滴滴司機端和乘客端的打車行為,利用事件引擎模擬司乘客打車的操作,以完成司乘訂單完整流程的測試。壓力控制則通過同時在線的司機數量和乘客數量來模擬。

滴滴的做法是寫了兩個事件引擎,一個是Driver Agent模擬司機上線,等待接單,接到訂單,完成訂單,模擬司機對系統的反饋。我們通過另一個Agent模擬乘客的行為,從上車到完成支付的過程,這樣就避免了傳統流量回放的壓測,因為狀態錯誤,傳達不到系統底層的問題。壓測流量的壓力怎麽控制?就是通過同時在線的司機數和乘客數來控制整個系統的壓力,這塊和我們業務場景是非常相似的,非常直觀。

通過我們這麽一個大的線上軍事演習,就能知道這個系統層級詳細的性能數據,機房流量上限,系統瓶頸分析。故障處理預案也可以進行評估,看看是不是有效的。但同時也有其他的問題,第一就是成本過高,我們所有的模塊和平臺都要維護壓測通道,另外就是風險過高,線上壓測如果壓掛就很麻煩。實際生產中,可以通過一些基礎組件和操作規範,保證風險在可控範圍,壓測成本則可以通過基礎組建的建設來降低。

通過把脈和壓測,我們發現部分服務成為了系統瓶頸,我們嘗試優化,或者重構這些服務到Go服務。

第一是性能問題。我們希望它遷移到性能比較好的平臺上去。

第二是接口準確性。我們是希望接口有一個準確的特性,而一些動態語言,導致一些接口是不定的,簡單來說是可靠性不足。

最後就是異步業務邏輯。一般來說,可能在做一些在線服務的時候,要做異步的邏輯,而進程模型的動態語言很難完成異步邏輯。

這些模塊可能是性能上的問題;可能是邏輯問題,錯誤率較高;又或者說,舊模塊由於補丁太多,的確需要重新梳理了。

2.3 希望什麽

滴滴如何遷移業務

說到遷移,我們希望能夠做到三點。

第一、業務是無感知的。我們希望中臺服務遷移過程當中,前臺業務無感知的,他完全不知道我們遷移了,或者說他只是幫我們觀察服務,微感知就可以了。

第二、服務遷移穩定,不要在遷移中掛掉了。

第三、遷移後的新老模塊的功能沒有什麽差異。

遷移經驗

技術分享圖片

這塊以PHP的典型MVC框架作為例子,這是典型後端服務,理想狀態下是拿Go對著PHP代碼直接翻譯,API和功能,最後完全一致,皆大歡喜。實際上我們做的時候發現並沒有那麽簡單。我們開發的時候,大家都會使用一些動態語言的特性,比如說可能對String和數值類型沒有做區分,或者PHP的關聯數組和普通數組混合起來操作。這個時候硬性翻譯,就需要Golang代碼做大量的adapter適配語言差異,這個對Golang業務代碼的汙染很嚴重。

技術分享圖片

所以,除了翻譯Go的代碼,還需要額外做一層Proxy,或SDK。我們希望Go server專註於它的業務,關註業務邏輯就好了,不要關註接口細節,如果接口不一致,或者因為一些特別的原因接口類型不一致的話,通過Proxy層,我們對Golang的接口和Proxy的接口的差異進行屏蔽,保證我們的Go server比較純凈,只關註自己的業務就可以。同時Proxy層也能幫助我們導流,比如說切流的時候用Proxy切流,通過Proxy的間隔,就可以保證Client對我們Go server遷移是無感知的,同時這個Proxy也可以把線下流量進行記錄,幫助線下Go server進行測試。假如說我們終於把Go的代碼開發完了,測試也通過了,大家覺得好像沒有什麽問題了,準備上線了,這時也是整個過程最危險的時候——要切流了。

滴滴因為有了很多的經驗,總結了三步,保證切流過程中服務比較穩定,第一是旁路引流,第二是流量切換,第三是線上觀察。

技術分享圖片

第一步先部署Go Server,通過Proxy引百分之百的旁路流量到Go Server,實際上是對Go Server的壓測。而客戶端的返回值是以的PHP的返回值為準,等於說Proxy異步調了一下Go Server,但是不會把數據吐給前端。這個時候我們會在Proxy去做Diff看他們的數據是不是一致的,同時會在Go Server和Proxy的底層,去DIff他們底層的存儲,看看業務邏輯上是不是一致的,如果出現問題就去進行修復。當整個流程持續一段時間以後,diff的量到一定可控的地步後,進行下一步——小流量切流。

技術分享圖片

我們將Proxy將Go Server的返回值逐漸的透給Client,這是一個比較慢的過程,1%、2%、10%、20%,切流持續時間可能比較長。這個時候我們要求Client業務端去觀察,看看有沒有什麽異常,這個時候Proxy層還在繼續Diff返回值有沒有問題,底層也在看是不是存儲是一致的。假如說這個過程非常順利,沒有出現問題,邏輯是一致的,就會進入下一步。

技術分享圖片

跟第一張圖比較相似,PHP Server變成了一個旁路流量,而Go Server變成了一個主流量,Client已經完全是Go Server的邏輯了。我們會持續的在線上觀察一段時間,可能是以月來計的,通過這個方式驗證Go Server是不是可行的,如果觀察沒有問題,會在合適的時機把PHP Server下掉,否則的話,遇到風吹草動我們就切回去了。

技術分享圖片

講完了切流,剛剛又說到了把脈,說到壓測,說到流量遷移,每一個中臺服務可能都要去接入這些服務治理的組件,接入壓測、把脈和服務發現,還有負載均衡等等模塊,如果中臺服務都按部就班的接,額外的開發工作量非常大。同時每個服務治理組件接入都不是很容易,導致開發周期長,浪費了很多人力,推廣起來很困難。這個時候服務治理的同學就提出了DIRPC的設想,這實際上是一套標準化的SDK組件。上下遊交互通過標準SDK形式劃分,提供統一的、一站式的服務發現、容錯調度、監控采集等,進而降低服務開發、運維成本。

3.在討論什麽?

我們討論RPC、SDK的時候到底在討論什麽,服務治理的同學希望能提供統一的一站式服務治理接入方案,通過一站式服務平臺,能夠完成對SDK,完成對服務治理的一站式接入,降低服務開發的成本和運維成本,保證服務的穩定性。除了C端容錯、服務發現、請求埋點、以及服務規範外,DIRPC中也實現了剛剛介紹的壓測通道,把脈等邏輯。

怎麽做的呢?我們在底層基礎組件封裝了一個基礎庫,這裏面有服務狀態、負載均衡等基礎組件,然後在上層開發我們自己所屬的client。中臺服務利用這些Client進行業務SDK的開發,同時需要滿足一定的規範,這樣SDK就能夠開發出來了。這是我們的理想,但是開發當中有很多困難的

第一他們完全符合一定的規範是不可能的,因為組件多,時間花的非常長。

第二如果有些模塊已經有SDK了,再遷移到新的SDK,遷移成本過高,會有穩定性風險。這個時候,在剛剛的DiRPC上進一步的做了一套東西,叫DiRPCGen。這是一套CodeGen的工具,通過對thrift的Idl擴充,業務只要寫一套IDL,便可以通過Dirpc的這套工具,直接生成SDK組件,非常方便,同時直接將所有的規範全部做到DiRPCGen中。這是比較大的工程,目前來說,這個IDL是可以兼容thrift語法的,在thrift的IDL上面做了一些擴充,以支持我們http協議和額外的滴滴的東西。這樣我們每一個中臺服務遷移的時候,成本都非常低,而且獲得的收益非常大,各個模塊就願意遷移,推廣也比較順利。

4.現在說兩個Golang的兩個小問題

4.1 第一個問題,是Golang的net.Conn接口,double close的問題

下圖是我們之前嘗試在web服務上建設優雅重啟的邏輯,我們希望服務退出的時候能夠保證已經建立鏈接的請求可以處理完,不會因為重啟導致錯誤增高。怎麽實現?

技術分享圖片

我們實現了服務全局的計數器,在鏈接建立的時候+1,在鏈接關閉的時候-1。服務退出的時候就檢查這個是不是歸零了,如果歸零就退出,否則就等待歸零,看得出來,底層實際上就是WaitGroup。鏈接操作是托管給底層net.http這個包來做的,我們對本身的net.Conn沒有任何直接的操作。

技術分享圖片

結果當我們把這個邏輯放到線上時,服務Panic掉了,原因是計數器變成了負數。我們的想法就是一個鏈接只有一次打開,只有一次關閉,所以肯定不會有負數出現。除非,net.http包對鏈接有多次的關閉操作?結果,發現的確是這樣,我們發現Golang底層net.http在處理鏈接的時候,可能會對鏈接進行多次的關閉操作,我這裏列了兩處。這塊是不是bug?

技術分享圖片

要不要提出一個issue?實際上並不是bug,如果你註意到net.Conn接口的註釋就會發現,“多個協程可能會同時並發的調用net.Conn接口中的函數”,意味著實現鏈接操作的時候,一定要保證第一要防並發,第二是防重入,而不能認為Golang底層只會有一次打開和關閉,大家要註意這一點。

4.2 第二是GC的問題

我們之前準備上線一個模型服務,這個模型服務維度比較大,各種各樣的參數比較多。服務上線以後,線下測試沒有什麽問題,都是很穩定的。服務丟到線上去發現隨著流量增大,超時請求越來越多,但平均耗時並不是很高。如果看99分位的耗時,毛刺非常嚴重,已經到秒級的請求了。

技術分享圖片

首先排查了機器的CPU內存,變化並不大,沒有太大的浮動,另外排查了網絡等。

技術分享圖片

排除了機器的客觀因素後,我們就懷疑是不是代碼寫出問題了。

技術分享圖片

我們用Golang的工具分析我們的代碼,很快發現,大量的CPU資源被golang的GC三色標記算法的掃描函數占據;同時服務中的in_used的對象數量,也達到了1000W之多。雖然目前GC的STW時間比較短,但是三色算法是並發標記的,可能就會用大量的CPU資源去遍歷這些對象,導致了我們這些CPU資源消耗率比較多,間接影響了服務的吞吐和質量。

技術分享圖片

知道了原因,優化的思路就很清晰了,我們想辦法減少一些不必要的對象類型的分配,那麽在Golang中什麽是對象類型呢?除了比較熟悉的指針,String,map,slice都是對象類型。通過把string變成定長的數組來避免三色算法遍歷,還有一些不必要的slice,全部變成數組等,可以減少對象類型的分配。雖然浪費了一些內存資源,但能夠幫助我們減少GC的消耗,優化以後的效果很明顯,很快99分位的耗時就降下來了。這個解決方案比較通用,如果大家有發現99平均耗時比較高,毛刺比較嚴重的話,大家可以看看是不是有這個因素在。

5. 最後推兩個開源的輪子給大家

5.1 第一個輪子是滴滴開源的數據庫操作輔助工具——gendry

它提供三個工具,分別幫助管理數據庫鏈接,構建SQL語句,以及 完成數據關系映射。

第一個組件是連接池管理類,幫助你管理連接池信息,處理一些基本的操作。

第二個是SQL構建工具,可以幫助你完成SQL的拼接操作。

最後一個scanner是結構映射工具,將你查出來原始數據映射到對象中去。

5.2 第二個輪子是Jsoniter

它是一套Json編解碼工具。在兼容原生golang的json編解碼庫的同時,效率上有6倍左右的提升。我非常推薦這個庫,相比easyJson的好處在於它不需要額外生成json處理代碼,只需要替換一個引用,就可以完美的幫你達到一個六倍的收益。

以上就是所有的內容,謝謝大家!

【提問環節】

提問者1

提問:說上線的PHP時候是通過旁路,另外一個服務狀態加上數據庫加上存儲是怎麽做到不相沖突的?

石松然:兩個系統的底層存儲是隔離的。舉個例子,我們今天跑一天旁路,會使用腳本Diff兩個系統底層存儲的數據,如果發現存儲上的差異,就說明有一些邏輯差異導致了存儲不一致。兩個存儲數據是通過腳本定期,或者周期性的方式同步的,一開始是以PHP為準,後期是以Go為準。

提問:你們開始說全量的壓測,一開始說數據源很難構造,後續的怎麽做的?

石松然:因為我們滴滴的每個業務訂單信息,包含了司機乘客的狀態信息,不是在接口輸入範圍之內,是系統額外的狀態信息。傳統壓測,一般的方式是根據時間維度聚合線上流量,然後再去回放,以保證壓測的成功。如果滴滴采用這種方式的話,流量很可能因為司乘狀態不符導致請求直接失敗,進不到底層去。我們的做法是通過事件引擎模擬司機和乘客的行為來完成壓測,這樣子保證不會因為司機乘客訂單狀態的信息導致壓測的失敗。

提問者2

提問:從PHP Server遷移到Go Server,做的時候,你說要把線上的流量遷過來,要把PHP Server和 Go Server進行diff,你業務裏面用戶的請求和返回是根據實際的情況不一樣的,那這個PHC Server返回和Go Server返回一樣嗎?

石松然:確定性的業務是可以完全diff的,不確定的業務是看接口的結果是不是符合預期的,並不是要求結果完全一樣的。所以小流量切流的時候還要繼續觀察,如果能夠做到完全一樣的話,也就不需要後面的小流量,也不需要上線觀察了。

提問:我看到兩個Agent,這兩個Agent你們怎麽樣做到把它模擬地比較像真實的環境?

石松然:我找同學們聊了一下,他們底層就是兩個事件引擎,根據一段時間模擬司機上線,通過系統返回值不同觸發不同的函數。收到一個訂單的時候就根據事件引擎去選擇相應的函數去處理,就是模擬司機、乘客的選擇不同的函數操作來實現的。

提問者3

提問:你剛才提到API的一致性,就是切流的這一部分,中間有一個部分是Proxy,這是怎麽實現的?是拿一個請求向上遊服務器去發呢,還是翻成代理實現呢?下遊請求和上遊請求的延遲怎麽解決的?這個延遲的有多少,壓力會不會是系統裏面的瓶頸?

石松然:Proxy是無狀態的服務,所以是可以無限橫向擴容的,這個性能問題不是什麽大的障礙。您剛剛說的因為雙次調動導致的延遲,這個是異步的過程,不會有耗時問題。

提問:會有超時的問題嗎?這種差異導致的異常不一致怎麽做的?

石松然:會有,超時問題也可能是網絡問題造成的。這種情況肯定會出現的,我們很難做到接口層百分之百的結果一致,所以後面小流量切流過程當中,還需要人工判斷,是因為網絡抖動的超時,還是邏輯有問題。需要通過小流量持續觀察,來判斷是不是網絡異常導致的問題。

提問者4

提問:後臺服務大規模更新的時候,有沒有出現過短暫的停止服務,還是都是平滑的過渡過來?

石松然:重啟的過程當中,我們運維服務有一些額外的邏輯。中臺服務進行重啟,不是完全掛掉再啟動起來的過程,而是一臺機器,一臺機器重啟。在重試的場景下,可能業務失敗後,再進行一次重啟就成功了。上線更新過程當中,我們會讓我們的Proxy,前端的業務進行一個額外的重試以保證業務成功率。

提問:所以用戶端的表現可能就是卡了一下是嗎?

石松然:大部分是無感知的,因為重試的話,假如說你一百臺機器,你一臺機器一臺機器的上線的話,你很難遇到兩次重試都打到一臺情況,這樣的概率是很小的。

提問:你完成一單需要300次Rpc的操作,這個是比較耗費性能的,這個過程全部是異步的嗎?

石松然:不是,300次Rpc其實是指整個訂單的流程,這不是一個Rpc請求導致的300個Rpc。比如說這個是司機出車是一次RPC,這是乘客的發單是一次RPC,它們都可能涉及到十幾個,二十幾個關聯的請求,這個不是一次性的Rpc請求。

治理Go模塊 服務治理 中臺業務 Golang的net.Conn接口,double close