基於 Go 語言的滴滴 DevOps 重塑之路

研發效率和系統穩定性是研發團隊永遠無法繞開的話題,前者決定業務迭代效率,而後者決定交付質量。多年來,滴滴在保障穩定性的前提下不斷探索更高效的技術手段,積累了大量實踐經驗。近期由網約車研發效率與穩定性負責人魏靜武在 Gopher China 上分享了滴滴在相關領域的實踐。

以下爲演講實錄。

很多技術同學或許都有過類似經歷:處理故障的過程中除了要承受巨大壓力之外,還會承受來自業務方新需求上線的壓力。似乎穩定性和研發效率很難調和,我們一直在探索如何在保障穩定性的前提下不斷提升我們的交付效率,既然是技術上的問題,還得從技術上找解法,DevOps 就是我們的突破口,因爲它幾乎包含了整個研發交付流程。

當然在保證穩定性的前提下,提高效率是一個很龐大的課題,整個行業甚至整個社會都在尋求最優解。今天我的分享只是拋磚引玉,希望我們的實踐,能給大家帶來一些啓發。

DevOps ——新的挑戰

從下圖來看,滴滴與業界其他公司在 DevOps 面臨的挑戰幾乎是類似的,無非是圍繞 DevOps 上層的業務挑戰和下層的技術架構挑戰。

我們分開說,從業務挑戰來看,滴滴有專車、快車、豪華車、拼車等多個產品。隨着業務發展,產品會越來越精細,比如拼車裏有極速拼車、特價拼車等,同時滴滴還開放了運力,讓第三方網約車平臺可以接入滴滴運力。這些複雜的業務細節,投射到技術上就是複雜的微服務架構。

從架構挑戰來講,滴滴早已經全面上雲,並且實現了業務的同城雙活和異地多活。雖然這些技術升級有明顯收益,但不可否認,它也進一步加劇了技術架構的複雜度,原有的微服務要部署到多個機房,當微服務數量只有幾十、幾百個的時候,可能不會有太大問題,一旦微服務數量達到幾千甚至上萬的時候,任何一個原本我們認爲很小的問題,都會被無限放大。具體來說,分爲以下三方面問題:

開發方面

隨着業務的發展,微服務越做越小,功能越來越單一。相對於單體服務來說,微服務只是業務邏輯少了,但會引入很多新的功能,比如服務發現、RPC 調用、鏈路追蹤等等,“微服務雖小,五臟俱全 ",重複工作繁重。

測試方面

對於小體量的微服務架構,靠個人或者一個團隊就可以快速把測試環境搭建起來,那大家可以設想一下,如果有幾千甚至上萬個微服務時,靠個人或者一個團隊很難構建出一套穩定的測試環境,更別說人手一套。其次,在微服務架構下如何進行迴歸測試,也就是說這次變更到底影響了什麼,沒有人能說清楚,即使能說清楚,那能不能把所有的場景都準備好,進行迴歸測試呢?——很難。

運維方面

隨着微服務數量變多,爲了避免引起線上事故,我們需要建設更多的監控和報警,比如針對業務指標、技術指標等不同維度的監控報警。但建設多了之後就又引起另外兩個問題:一個是報警太多,誤報也多;另一個是報警策略可能要隨時調整,因爲業務在不斷變化,持續保障報警的有效性需要投入巨大的人力成本。還有就是定位難,任何一個環節出問題都可能會導致整個系統的崩潰,如何快速根因定位並止損,也是運維階段面臨的巨大挑戰。

接下來,我會從不同階段面臨的挑戰出發,聊聊滴滴的應對之策。

開發 - 雲原生腳手架

首先是上面提到的開發階段繁重的重複性工作,比如服務發現、鏈路追蹤、RPC 調用等等。但還有其他的挑戰,滴滴在最初技術選型的時候,針對業務開發選的是弱類型語言——PHP。PHP 最大的優勢是快,它幫助滴滴快速實現了產品落地,搶佔了市場先機,但隨着業務複雜程度的提高,或者說微服務的交互越來越多,弱類型語言也暴露出來一些問題。一方面由於數據是弱類型,導致我們在跟一些強類型語言,比如 C++、Java 等進行交互時,經常會導致強類型語言崩潰;另一方面是性能問題,大家知道,在出行領域是有比較明顯的谷峯流量特徵的,比如早晚高峯、節假日高峯等,尤其是像五一、十一這種長假前一天的晚高峯,流量會很高,這時弱類型語言的性能問題會消耗大量機器資源,雖然滴滴通過彈性伸縮降低很大一部分成本,但高峯期的消耗依然很高。

基於此,滴滴在開發階段做了三個 “統一”:

統一 Go 技術棧

至於爲什麼統一到 Go,除了上面提到的問題外,還有一個更重要的原因,就是整個技術團隊的轉型成本,滴滴是比較早應用 Go 語言的公司,在 2016 我還未加入滴滴之前,滴滴內部就已經有很多團隊在使用 Go 語言了,所以基於這些原因,我們最終決策統一 Go 技術棧。

但說實話,選定 Go 技術棧,其實大家只是達成了一個思想上的統一,而在具體操作上,我們不可能一刀切地要求大家全部遷移到 Go,或者要求大家暫停業務開發來完成遷移,業務方也不答應。

統一框架

我們通過框架的方式把所有非業務邏輯全部封裝起來,以幫助業務在遷移或者新建服務過程中成本足夠低。同時爲了兼容原有服務,滿足業務的一些歷史 “債務” 需求,我們選擇用 Thrift IDL 爲中心,進行擴展(見下圖)。

Thrift 是一個提供可擴展,跨語言的 RPC 框架,IDL 是它的接口描述語言。雖然 Thrift 有自己的協議,但通過我們在 Thrift IDL 的擴充,使得它可以在兼容 Thrift 原生協議的情況下支持了 HTTP 和 gRPC 協議。這樣既可以解決原來已經使用 Thrift 服務的兼容問題,也能順利完成 HTTP 服務或者 gRPC 服務的遷移工作。

同時基於擴展後的 IDL ,對上可以生成不同於語言的 SDK、文檔以及 Mock 服務;對下可以生成一個支持多協議的 Server,在這個 Server 裏,除了一些基礎能力之外,還包括一些中間件封裝,可以說我們幾乎封裝了滴滴所有中間件 SDK,比如 MQ、RDS、KV 等,開箱即用。

統一數據

我個人認爲這對滴滴來講,是更爲重要且難的事情。因爲人多了,服務多了,數據統一的難度會指數級上升。在框架去統一數據不但可以提升服務治理能力,同時也降低了大量重複開發工作。

這三件事做完之後,業務的遷移成本就相對低了很多。尤其是新服務的開發,業務研發工程師只需要專注業務邏輯即可,底層的複雜性都被屏蔽了。

測試 - 流量回放與測試環境

上面我們提到了框架遷移的問題,統一了技術棧,準備好了框架,但對於遷移老服務來說,更大的挑戰在於如何保障遷移過來的服務足夠穩定,能實現平滑遷移。

對於滴滴來說,一個微服務,下游依賴服務可能有幾百個,如果靠傳統單測或者自動化測試,很難覆蓋所有場景。大家不妨細想一下,不管是小型的微服務架構,還是大型的微服務架構,對於它們來說,真正的測試成本大頭在哪?通常情況下,不是在新功能的測試上,而是在那些已經積累了很長時間的迴歸測試上 (大家可以回憶下自己的經歷,是不是每次故障大多是因爲影響了老功能,反而新功能沒啥問題)。在實際的測試過程中,由於各種錯綜複雜的原因,我們很難做到每次上線就把所有的場景都覆蓋一遍,因爲不管從人力上還是依靠傳統技術都很難實現。

在測試階段的另外一個挑戰也是我們剛剛提到過的,就是如何搭建測試環境。當涉及到成千上萬的微服務時,要怎樣構建測試環境?怎樣保證每個人都有一套穩定測試環境而且互不影響?

我瞭解到,業界關於測試環境構建分爲兩派,一派以契約測試爲代表,他們認爲就不應該構建微服務的測試環境,應該通過契約測試的方式把下游全部 Mock 掉來進行測試。但是,試想一下,如果我們下游有幾百個依賴,每次請求會帶來幾十上百次的下游次的下游調用,要如何 Mock 呢,就算 Mock 了,又怎麼持續保障 Mock 有效呢,這不是靠堆人可以解決的。

另一派叫做 Test in Production,即在線上進行測試。他們認爲既然線下搭建測試環境成本太高,那在線上環境通過一些測試賬號做邏輯隔離不就行了。這種方式對於滴滴來說並不能被接受,因爲雖然做了邏輯隔離,但它並不能完全保障對線上無污染,尤其是線上數據的污染。

那滴滴是如何應對上述兩個挑戰的呢?我們先來第一個問題,針對這個問題的解法,滴滴採用了比較創新的手段——流量錄製和回放,不同於業界的流量錄製和回放,我們實現了每一次請求的上下文全部錄製。什麼意思呢?比如打車的時候用戶會進行價格預估,預估的流量會請求到後端服務,這個時候我們除了會把這次預估接口的請求流量和返回流量錄製下來,還會把涉及調用下游的 RPC、MySQL、Redis 等 outbound 流量也都錄製下來,並把這些流量綁定在一起,這就是剛剛所說的請求的上下文,我們叫一次 Session,同時能保證併發請求之間的流量不會串。

具體怎麼實現呢?關注滴滴開源的同學可能會知道,我們開源過兩個項目,分別針對 PHP 和 Go 語言的方案,對於 Go 語言是通過更改 Go 源碼的方式來解決錄製的問題。但現在滴滴內部升級了一種新的方案——內核錄製,無需修改源碼。

如下圖所示,在我們的新方案中,針對 Go 和 C++ 服務,我們採用 Cilium 提供的 eBPF-Go 的庫,通過內核將所有相關係統調用 Hook 起來,比如 Accept、Recv、Send 等系統調用。

在這個基礎上,還需要通過 Uprobe 的方式去 Hook 一下用戶態的調用,因爲我們是綁定在進程上進行錄製的,而不是通過網絡層面進行錄製的,所以我們就可以把 inbound 和 outbound 流量全部錄製下來並進行綁定,同時也忽略了進程外的噪音流量。

對於 PHP,我們實現的邏輯是採用 CGO +LD_PRELOAD,其實是在更上一層(libc 這一層) Hook 了系統調用。其實,這種方式更好用一些,但 Go 語言默認不依賴 libc ,所以我們針對不同語言採用了兩種方案來進行錄製,並在線上進行不間斷的實時錄製。

錄製完以後,還需要進行流量過濾,也就是降採樣或者異常流量過濾,最後把所錄製的流量落到 ElasticSearch 中,這個過程其實就是在構建測試 Case,且不需要人工干預。有了這些構建好的 Case,用戶只需要在 ElasticSearch 中檢索自己需要的場景,或者直接選取最新的一批流量來進行批量回放。

檢索方面,其實就是對 ElasticSearch 進行搜索,比如接口請求和返回有什麼參數,下游請求 MySQL 發了什麼字段,甚至一些異常場景也能夠搜索出來。爲了能夠讓大家能檢索更多的場景,我們針對不同的協議做了文本化處理,比如 Thrift 協議、MySQL 協議等,都能在 ES 中檢索出來。

將流量查詢出來後,接下來就需要進行回放。做回放,除了將所有的網絡請求 Mock 掉之外,最重要的一點,還需要將時間、本地緩存、配置等回溯回去,即回到錄製的時間點。爲什麼這麼做,大家可以想一下,我們在進行回放時,業務邏輯裏是不是有很多是根據時間戳處理的,比如緩存時間等,如果不把時間回溯回去,它的邏輯跟線上可能就完全不一樣了。

最後就是進行線上和線下流量 Diff,比如 Diff 線上的 Response 與測試的 Response 是不是一致,發送下游的 MySQL、Thrift 、HTTP 等請求流量是否跟線上一致,如果不一致,就是有 Diff,當然這裏會有噪音,我們也做了很多降噪處理。最終,基於流量的 Diff 來判斷回放的成功與否。

我們做過驗證,我們用大概一萬條錄製的流量進行回放測試,與線上的服務做代碼覆蓋率的對比,最終發現,大概只有 2% 到 3% 的差距,也就是說幾乎可以覆蓋所有場景。

基於此,我們可以在不需要人工干預的情況下,就可以自動化的構建高覆蓋率測試場景,然後大批量的進行回放,從而達到高覆蓋率的迴歸測試。像我們前面提到的遷移框架、遷移 Go 技術棧,都依賴於這種方式。

以上就是我們解決迴歸測試挑戰的解法。

這套技術方案已經在滴滴的線上流水線跑了,上游的模塊已經全部接入進來。每次上線或者提交代碼時,會批量的全跑一遍。但是我們不會跑一萬條,我們會對每個模塊進行流量篩選,大概會從一萬條中篩選出幾千條。當然有些模塊的流量,只篩選出幾百條,因爲大部分流量是重複的,這樣上線的流水線其實只需要跑幾百或者幾千個 Case 就可以了,大概 5 分鐘就能夠回放完成。

接下來說測試階段的第二個挑戰,也就是如何在微服務架構下構建測試環境,滴滴其實也走過一段彎路,想靠個人或者一個團隊就構建起整個測試環境。剛開始,我們把所有的服務都塞到一個鏡像裏,在最初服務少的時候還能運轉,大家用得也比較順暢,畢竟每個人只需要申請自己的鏡像就可以了。

但是隨着越來越多的服務加入,環境越來越不穩定了,也很難構建起來,沒有人可以搞定,甚至有些人開始去預發環境進行測試,風險非常高。

其實在 “如何在微服務架構下構建測試環境” 這個問題上,主要有兩個難點,也可以說是挑戰,一個是如何低成本的構建測試環境,最起碼是在線下把整套測試環境搭建起來;而另一個挑戰則是如何低成本的保證人手一套測試環境,並且能夠互不影響。

通過下圖可以看到,滴滴的完整測試環境叫做 “線下基準環境”,不得不提,這得益於滴滴在雲原生方面的投入,大幅降低了新增機房的成本。我們只需要在線上節點下增加一個機房節點,把基準環境與線上節點綁定到一起,這樣做的好處,除了能複用線上能力,比如報警、監控、日誌採集等, 還能讓業務代碼在上線、回滾時都與線上服務保持同步,保證了測試環境的仿真度。

爲了實現人手一套測試環境且互不影響,我們基於 Go 語言自研了一套 Sidecar,即在所有的基準環境前都部署一個 Sidecar,通過流量染色的方案按需構建測試環境,這是借鑑了 Service Mesh 和業界一些關於流量染色的思路。

以上圖爲例,假設我只需要開發 S1 ,那我只需要把 S1 構建好,其他依賴都用基準環境即可。當前,在網約車開發高峯期,我們基本上能保持 1000 套左右的測試環境,能保證每人一套環境,甚至可以一人多套環境。當然,我們還會複用線上雲原生能力進行資源超賣、定期釋放等能力,極致壓縮成本。

運維 - AIOps

在運維方面,我們前面也提到過,主要問題有兩個,一個是報警多、但不準、難維護,另一個是定位難、止損慢,而針對這兩類問題,除了組織和機制的優化外,在技術層面,我認爲最好的解法就是 AIOps。AIOps 的概念是由 Gartner 在 2016 年提出的,核心思想是通過機器學習來分析海量的運維數據,幫助運維提高運營能力。

根據滴滴的業務場景,我們把 AIOps 分爲了三個方向:監控告警、根因定位以及故障恢復。

稍微展開講一下,在監控告警中,最重要的事情:一是時序異常檢測,因爲大多數的指標都是時序數據,並且不同時間會有不同的數據,不同的指標也有不同的曲線,這是異常檢測很重要的一個地方。二是指標拆解、分類,我們需要把一個指標拆解爲不同維度的指標。舉個例子,我們監控 “完單量”,不只是簡單監控完單量就夠了,而是需要把不同的產品線、不同的城市等維度指標做笛卡爾乘積,衍生出 N 條曲線,然後進行曲線分類。三是自動建設告警,在指標拆解完成後,需要根據不同的曲線類別配置不同的告警策略,由於曲線很多,此過程不需要自動化建設,同時可以定期的根據業務變化來調整曲線的報警策略。

而根因定位的邏輯,其實就是模擬人的定位過程,大家可以回想一下自己在進行根因定位的時候思路是什麼樣的。是不是根據報警信息或者用戶反饋的信息,然後檢索大腦裏的知識圖譜,找到最有可能的原因,逐個去排除,直到找到根因。

在滴滴的事件中也是類似過程,只是我們把它做成了自動化,我們會通過一些相關性分析、因果推斷以及一些人工標註等構建好知識圖譜,並將其沉澱下來,就想我們大腦的知識圖譜一樣。接下來,我們會結合鏈路追蹤、報警信息、事件變更等數據,綜合決策出最有可能的根因。

拿一個具體的例子來說,主要分爲以下幾步:

收集黃金指標

這個數據非常重要,前面我們研發階段提到的統一數據,其實就是在做這個事情。在整個運維體系裏,最重要的就是標準化數據,其實就是服務機器學習的特徵工程,這個幾乎佔了 70% 以上的成本。

做指標分類

剛纔也提到過,時序數據的曲線是不一樣的,比如完單量,在一些大城市,完單量曲線可能會隨早晚高峯出現明顯的波峯波谷,而在一些小城市,完單量的曲線可能沒那麼明顯,這些指標都需要進行分類。

確定報警策略

以全國的數據來說,數據的曲線相對平滑且有規律 (早晚高峯,工作日,節假日等),可以選擇 ETS(指數平滑) 策略,通過學習歷史數據預測未來數據,然後設置一個置信區間,超過置信區間即報警。但對於小城市無規律的曲線,可能需要兜底的策略。當然業界有很多種異常檢測算法,主要分爲監督和無監督的算法,滴滴是更傾向於無監督的方式,因爲監督學習需要大量標註數據,但線上出現的異常點非常少,數據量不夠。

告警屏蔽

當真正發生事故時,可能會出現告警風暴,這時想要短時間內找到哪個是因哪個是果很難,反而會擾亂大家的排查思路。報警屏蔽中大概有兩種思路,一種是進行單策略屏蔽,這是一種比較簡單的方式。比如我們配置了 CPU 告警,某一個節點有 1000 臺機器在報 “CPU 掉底”,那我們可以將這 1000 條報警合併爲 1 條;另外報警的時間可以設置爲 10 分鐘 1 次或者 30 分鐘 1 次。

第二種思路,主要是依賴於根因定位來判斷出哪些報警是可以被屏蔽掉的。比如完單掉了,可能是因預估失敗導致的,這時預估可以報警,而完單沒有必要報警。

根因定位

定位的邏輯也分爲兩類,一類是橫向定位,或者叫做業務定位。還是以完單量來舉例,完單量下降的時候,首先要判斷出是司機導致的和乘客乘客導致的,如果是乘客掉了,那是發單掉了,還是預估掉了?是小程序掉了還是主端掉了,如果是小程序掉了,那可能就從小程序出發去排查了。

另一類是縱向定位。如果已經定位出來是預估全國掉了,接下來就需要進行縱向定位,也叫技術定位,分析跟預估相關的服務,比如通過 Trace 鏈路追蹤,Metric 指標,飽和度指標、變更事件等,以定位到報警的根因,大多數事故都跟變更事件有關,所以所有線上變更都會有記錄,方便及時查詢。

故障恢復

定位到根因後或者定位到可以選擇降級預案後,接下來就是要恢復故障。所謂的故障恢復,其實並不是說在每次報警出現時,臨時選擇降級方案,而是要在報警出現之前做大量的放火和演練,提前將有可能的風險點建設好預案,然後止損階段只是執行提前建設好的預案。

大家可以看下面這幅圖,這是滴滴網約車現在正在使用的定位機器人,我們內部管叫它東海龍王。它基本上已經替代了我們大部分的定位工作,通過東海龍王我們可以幾秒內把所有相關信息檢索出來,比如根據接口成功率定位到是哪個機房出問題了 ,如果能夠定位到某一個具體的機房,那我們就不需要再進行根因定位,可以直接操作切流止損了,等恢復後再來排查根因。

另外,我們會把所有的狀態碼返回的信息,包括 Trace 都摘取出來,並把其中的一些關鍵信息提取到東海龍王中,比如上圖中的錯誤碼、錯誤信息等都會有提示,還有 RPC 定位和針對 RPC 的降級預案,CPU、內存、磁盤檢查信息等等。最重要的一點還會 at 負責人,因爲在實際的止損過程中,有很多降級還需要人工進行決策。

未來 - 自動化部署

如果對整個 DevOps 進行人工和自動化區分,我們發現在每個環節裏,尤其是在大規模的微服務架構下,人工的瓶頸越來越突出,比如在開發階段,人工主要是制定規範,然後進行 Code Review 等,但是每個人的能力、標準甚至同一個人不同時間點的心情都不一樣,很難到達一個相對平穩的水平,而通過自動化,我們可以通過靜態掃描、編譯檢查、框架約束等方式,把靜態檢查的能力提升到一個相對穩定的水平,包括我們剛提到的  AIOps 運維方式,也會將運維能力保持在一個穩定的水平,而我們可以通過不斷優化自動化能力,來提高我們的能力下限。

基於這個理論,滴滴在嘗試構建一個完全自動化的流水線,比如把從 Code Review 到部署回滾都變成無人值守,完全依靠自動化的保障手段來提前發現問題,並通知負責人,不但可以提升上線效率,也可以把穩定性保障下限提高到一定水平。當然所有這一切都離不開滴滴多年在基礎能力上的建設,比如雲原生能力的建設、可觀測能力的建設、混沌工程的建設等等。

作者及部門介紹 

本篇文章作者魏靜武,來自滴滴網約車出行技術團隊,出行技術作爲網約車業務研發團隊,通過建設終端用戶體驗平臺、C 端用戶產品生態、B 端運力供給生態、出行安全生態、服務治理生態、核心保障體系,打造安全可靠、高效便捷、用戶可信賴的出行平臺。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SV8Mu0TaOYHlDFInuzZfmQ