滴滴基於Go語言的DevOps重塑之路
研發效率和系統穩定性是研發團隊永遠無法繞開的話題,前者決定業務迭代效率,而後者決定交付質量。多年來,滴滴在保障穩定性的前提下不斷探索更高效的技術手段,積累了大量實踐經驗。網約車研發效率與穩定性負責人魏靜武分享了滴滴在相關領域的實踐。
很多技術同學或許都有過類似經歷:處理故障的過程中除了要承受巨大壓力之外,還會承受來自業務方新需求上線的壓力。似乎穩定性和研發效率很難調和,我們一直在探索如何在保障穩定性的前提下不斷提升我們的交付效率,既然是技術上的問題,還得從技術上找解法,DevOps就是我們的突破口,因為它幾乎包含了整個研發交付流程。
當然在保證穩定性的前提下,提高效率是一個很龐大的課題,整個行業甚至整個社會都在尋求更優解。今天我的分享只是拋磚引玉,希望我們的實踐,能給大家帶來一些啟發。
一、DevOps——新的挑戰
從下圖來看,滴滴與業界其他公司在DevOps面臨的挑戰幾乎是類似的,無非是圍繞DevOps上層的業務挑戰和下層的技術架構挑戰。
我們分開說,從業務挑戰來看,滴滴有專車、快車、豪華車、拼車等多個產品。隨著業務發展,產品會越來越精細,比如拼車裡有極速拼車、特價拼車等,同時滴滴還開放了運力,讓第三方網約車平臺可以接入滴滴運力。這些複雜的業務細節,投射到技術上就是複雜的微服務架構。
從架構挑戰來講,滴滴早已經全面上雲,並且實現了業務的同城雙活和異地多活。雖然這些技術升級有明顯收益,但不可否認,它也進一步加劇了技術架構的複雜度,原有的微服務要部署到多個機房,當微服務數量只有幾十、幾百個的時候,可能不會有太大問題,一旦微服務數量達到幾千甚至上萬的時候,任何一個原本我們認為很小的問題,都會被無限放大。具體來說,分為以下三方面問題:
1.開發方面
隨著業務的發展,微服務越做越小,功能越來越單一。相對於單體服務來說,微服務只是業務邏輯少了,但會引入很多新的功能,比如服務發現、RPC呼叫、鏈路追蹤等等,“微服務雖小,五臟俱全",重複工作繁重。
2.測試方面
對於小體量的微服務架構,靠個人或者一個團隊就可以快速把測試環境搭建起來,那大家可以設想一下,如果有幾千甚至上萬個微服務時,靠個人或者一個團隊很難構建出一套穩定的測試環境,更別說人手一套。
其次,在微服務架構下如何進行迴歸測試,也就是說這次變更到底影響了什麼,沒有人能說清楚,即使能說清楚,那能不能把所有的場景都準備好,進行迴歸測試呢?——很難。
3.運維方面
隨著微服務數量變多,為了避免引起線上事故,我們需要建設更多的監控和報警,比如針對業務指標、技術指標等不同維度的監控報警。
但建設多了之後就又引起另外兩個問題:一個是報警太多,誤報也多;另一個是報警策略可能要隨時調整,因為業務在不斷變化,持續保障報警的有效性需要投入巨大的人力成本。還有就是定位難,任何一個環節出問題都可能會導致整個系統的崩潰,如何快速根因定位並止損,也是運維階段面臨的巨大挑戰。
接下來,我會從不同階段面臨的挑戰出發,聊聊滴滴的應對之策。
二、開發——雲原生腳手架
首先是上面提到的開發階段繁重的重複性工作,比如服務發現、鏈路追蹤、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 到部署回滾都變成無人值守,完全依靠自動化的保障手段來提前發現問題,並通知負責人,不但可以提升上線效率,也可以把穩定性保障下限提高到一定水平。當然所有這一切都離不開滴滴多年在基礎能力上的建設,比如雲原生能力的建設、可觀測能力的建設、混沌工程的建設等等。
來自 “ 滴滴技術 ”, 原文作者:魏靜武;原文連結:https://server.it168.com/a2024/0218/6839/000006839866.shtml,如有侵權,請聯絡管理員刪除。
相關文章
- 第 69 期 DevOps 實踐之路 - 基於 Go 語言周邊生態打造的行業技術中臺devGo行業
- goweb,基於go語言的API框架GoWebAPI框架
- goweb,基於go語言API框架GoWebAPI框架
- Go語言基於go module方式管理包(package)GoPackage
- 基於Go語言來理解TensorflowGo
- 基於 Go 語言來理解 TensorflowGo
- 【工具】一款基於go語言的agentGo
- 基於go語言學習工廠模式Go模式
- Go 語言 Web tail -f 工具, 基於 WebSocketGoWebAI
- Go語言基礎Go
- 基於go語言gin框架的web專案骨架Go框架Web
- 【Python】一款基於go語言的agentPythonGo
- Go語言基礎-序言Go
- 【Go語言基礎】sliceGo
- [Go]Go 語言基礎拾遺(一)Go
- DevOps工程師需要學習Go語言? - iximiuzdev工程師Go
- 基於 Web 的 Go 語言 IDE - Wide 1.5.2 釋出!WebGoIDE
- 對話專家:Go是DevOps時代最好的程式語言Godev
- [06 Go語言基礎-包]Go
- Go 語言關於 Type Assertions 的 坑Go
- go語言基礎之——iota的用法Go
- Go語言基礎語法總結Go
- 基於GO語言框架Gin開發的MVC輪子框架:GinLaravelGo框架MVCLaravel
- Go語言學習之路-11-方法與介面Go
- Go語言專案實戰:基於開源資料的成語查詢Go
- Go語言————1、初識GO語言Go
- 【搞定Go語言】第2天4:Go語言基礎之流程控制Go
- 基於Go語言的社群系統:mlog-club1.0.3 釋出Go
- 基於 Go 語言的社群系統:mlog-club1.0.3 釋出Go
- 基於物件的JavaScript語言(轉)物件JavaScript
- Go語言核心36講(Go語言基礎知識一)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識二)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識三)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識四)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識五)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識六)--學習筆記Go筆記
- 從零開始——GO語言基礎語法Go
- go語言的介面Go