天天看點

滴滴杜歡:大型微服務架構設計實踐

作者:閃念基因

大家好,我是杜歡,很榮幸能代表滴滴來做分享。我來滴滴的第一件事情就是幫助公司統一技術棧,在服務端我們要把以前拿 PHP 和 Java 做的服務統一起來,經過很多思考和選擇之後我們決定用 Go 來重構大部分業務服務。現在,滴滴内部已經有非常多的用 Go 實作的服務和大量 Go 開發者。

《⼤型微服務架構設計實踐》是一個很大的話題,這個題目其實分為三個方面,“微服務架構”、“大型”和“設計實踐”。我們日常看到的各種開源微服務架構,在我看來都不算“大型”,解決的問題比較單純。大型微服務架構究竟是什麼,又應該怎麼去一步步落地實踐,我會從問題出發,分别從以下幾個方面來探讨這個話題。

• 發現問題:服務開發過程中的痛點

• 以史鑒今:從服務架構的演進曆程中找到規律

• ⼤道⾄簡:⼤型微服務架構的設計要點

• 精雕細琢:架構關鍵實作細節

▍發現問題:服務開發過程中的痛點

▍複雜業務開發過程中的痛點

我們在進行複雜業務開發的過程中,有以下幾個常見的痛點:

• 時間緊、任務多、團隊⼤、業務增⻓快,如何還能保證架構穩定可靠?

• 研發⽔平參差不⻬、項⽬壓⼒⾃顧不暇,如何保證品質基線不被突破?

• 公司有各種⼯具平台、SDK、最佳實踐,如何盡可能的在業務中使⽤?

網際網路業務研發的特點是“快”、“糙”、“猛”:開發節奏快、品質較粗糙、增長迅猛。我們能否做到“快”、“猛”而“不糙”呢?這就需要有一些技術架構來守住品質基線,在業務快速堆砌代碼的時候也能保持技術架構的健康。

在大型項目中,我們也經常會短時間聚集一批人參與開發,很顯然我們沒有辦法保證這些人的能力和風格是完全拉齊的,我們需要盡可能減少“人”在項目品質中的影響。

公司内有大量優秀的技術平台和工具,業務中肯定是希望盡可能都用上的,但又不想付出太多的使用成本,必定需要有一些技術手段讓業務與公司基礎設施無縫內建起來。

很自然我們會想到,有沒有一種“架構”可以解決這個問題,帶着這個問題我們探索了所有的可能性并找到一些答案。

▍以史鑒今:從服務架構的演進曆程中找到規律

▍服務架構進化史

滴滴杜歡:大型微服務架構設計實踐

服務架構的曆史可以追溯到 1995 年,PHP 在那一年誕生。PHP 是一個服務架構,這個語言首先是一個模闆,其次才是一種語言,預設情況下所有的 PHP 檔案内容都被直接發送到用戶端,隻有使用了 <?php ?> 标簽的部分才是代碼。在這段時間裡,我們也稱作 Web 1.0 時代裡,浏覽器功能還不算強,很多的設計理念來源于 C/S 架構的想法。這時候的服務架構的巅峰是 2002 年推出的 ASP.net,當年真的是非常驚豔,我們可以在 Visual Studio 裡面通過拖動界面、輕按兩下按鈕寫代碼來完成一個網頁的開發,非常具有颠覆性。當然,由于當時技術所限,這樣做出來的網頁體驗并不行,最終沒有成為主流。

接着,Web 2.0 時代來臨了,大家越來越覺得傳統軟體中經常使用的 MVC 模式特别适合于服務端開發。Django 釋出于 2003 年,這是一款非常經典的 MVC 架構,包含了所有 MVC 架構必有的設計要素。MVC 架構的巅峰當屬 Ruby on Rails,它給我們帶來了非常多先進的設計理念,例如“約定大于配置”、Active Record、非常好用的工具鍊等。

2005 年後,各種 MVC 架構的服務架構開始井噴式出現,這裡我就不做一一介紹。

▍标志性服務架構

滴滴杜歡:大型微服務架構設計實踐

随着網際網路業務越來越複雜,前端邏輯越來越重,我們發現業務服務開始慢慢分化:頁面渲染的工作回到了前端;Model 層逐漸下沉成獨立服務,并且催生了 RPC 協定的流行;業務接入層隻需要提供 API。于是,MVC 中的 V 和 M 逐漸消失,演變成了路由架構和 RPC 架構兩種形态,分别滿足不同的需求。2007 年,Sinatra 釋出了,它是一個非常極緻的純路由架構,大量使用 middleware 設計來擴充架構能力,業務代碼可以實作的非常簡潔優雅。這個架構相對小衆(Github Stars 10k,實際也算很有名了),其設計思想影響了很多後續架構,包括 Express.js、Go martini 等。同年,Thrift 開源,這是 Facebook 内部使用 RPC 架構,現在被廣泛用于各種微服務之中。Google 其實更早就在内部使用 Protobuf,不過直到 2008 年才首次開源。

再往後,我們的基礎設施開始發生重大變革,微服務概念興起,虛拟化、docker 開始越來越流行,服務架構與業務越發解耦,甚至可以做到業務幾乎無感覺。2018 年剛開源的 Istio 就是其中的典型,它專注于解決網絡觸達問題,包括服務治理、負載均衡、動态擴縮容等。

▍服務架構的演進趨勢

通過回顧服務架構的發展史,我們發現服務架構變得越來越像一種新的“作業系統”,越來越多的架構讓我們忘記了 Web 開發有多麼複雜,讓我們能專注于業務本身。就像作業系統一樣,我們在業務代碼中以為直接操作了記憶體,但其實并不然,作業系統為我們屏蔽了總線尋址、虛位址空間、缺頁中斷等一系列細節,這樣我們才能将注意力放在怎麼使用記憶體上,而不是這些跟業務無關的細節。

随着架構對底層的抽象越來越高,架構的入門門檻在變低,以前我們需要逐漸學習架構的各種概念之後才能開始寫業務代碼,到現在,很多架構都提供了非常簡潔好用的工具鍊,使用者很快就能專注輸出業務代碼。不過這也使得使用者更難以懂得架構背後發生的事情,想要做一些更深層次定制和優化時變得相對困難很多,這使得架構的學習曲線越發趨近于“階躍式”。

随着技術進步,架構也從代碼架構變成一種運作環境,架構代碼與業務代碼也不斷解耦。這時候就展現出 Go 的一些優越性了,在容器生态裡面,Go 占據着先發優勢,同時 Go 的 interface 也非常适合于實作 duck-typing 模式,避免業務代碼顯式的與架構耦合,同時 Go 的文法相對簡單,也比較容易用一些編譯器技巧來透明的增強業務代碼。

▍⼤道⾄簡:⼤型微服務架構的設計要點

▍站在全局視角觀察微服務架構

服務架構的演進過程是有曆史必然性的。

傳統 Web 網站最開始隻是在簡單的呈現内容和完成一些單純的業務流程,傳統的“三層結構”(網站、中間件、存儲)就可以非常好的滿足需求。

Web 2.0 時代,随着網絡帶寬和浏覽器技術更新,更多的網站開始使用前端渲染,服務端則更多的退化成 API Gateway,前後端有了明顯的分層。同時,由于網際網路業務越來越複雜,存儲變得越來越多,不同業務子產品之間的存儲隔離勢在必行,這種場景催生了微服務架構,并且讓微服務架構、服務發現、全鍊路跟蹤、容器化等技術日漸興盛,成為現在讨論的熱點話題,并且也出現了大量成熟可用的技術方案。

再往後呢?我們在滴滴的實踐中發現,當一個公司的組織結構成長為多事業群架構,每個事業群裡面又有很多事業部,下面還有各種獨立的部門,在這種場景下,微服務之間也需要進行隔離和分層,一個部門往往會需要提供一個 API 或 broker 服務來屏蔽公司内其他服務對這個部門服務的調用,在邏輯上就形成了由多個獨立微服務構成的“大型微服務”。

在大型微服務架構中,技術挑戰會發生什麼變化?

據我所知,國内某一線網際網路公司的一個事業群裡部署了超過 10,000 個微服務。大家可以思考一下,假如一個項目裡面有 10,000 個 class 并且互相會有各種調用關系,要設計好這樣的項目并且讓它容易擴充和維護是不是很困難?這是一定的。如果我們把一個微服務類比成一個 class,為了能夠讓這麼複雜的體系可以正常運轉,我們必須給 class 進行更進一步的分類,形成各種 class 之上的設計模式,比如 MVC。以我們開發軟體的經驗來看,當開發單個 class 不再成為一件難事的時候,如何架構這些 class 會變成我們設計的焦點。

我們看到前面是架構,更多解決是日常基礎的東西,但是對于人與人之間如何高效合作、非常複雜的軟體架構如何設計與維護,這些方面并沒有解決太好。

大型微服務的挑戰恰好就在于此。當我們解決了最基本的微服務架構所面臨的挑戰之後,如何進一步友善架構師像操作 class 一樣來重構微服務架構,這成了大型微服務架構應該解決的問題。這對于網際網路公司來說是一個問題,比如我所負責的業務整個代碼量幾百萬行,看起來聽多了,但跟傳統軟體比就沒那麼吓人。以前 Windows 7 作業系統,整體代碼量一億行,其中最大的單體應用是 IE 有幾百萬行代碼,裡面的 class 也有上萬個了。對于這樣規模的軟體要注意什麼呢?是各種重構工具,要能一鍵生成或合并或拆分 class,要讓軟體的組織形式足夠靈活。這裡面的解決方法可以借鑒傳統軟體的開發思路。

▍大型微服務架構的設計目标

結合上面這些分析,我們意識到大型微服務架構實際上是開發人員的“效率産品”,我們不但要讓一線研發專注于業務開發,也要讓大家幾乎無感覺的使用公司各種基礎設計,還要讓架構師能夠非常輕易的調整微服務整體架構,友善像重構代碼一樣重構微服務整體架構,進而提升架構的可維護性。

公司現有架構就是業務軟體的作業系統,不管公司現有架構是什麼,所有業務架構必須基于公司現有基礎進行建構,沒有哪個部門會在做業務的時候分精力去做運維系統。現在所有的開源微服務架構都不知道大家底層實際在用什麼,隻解決一些通用性問題,要想真的落地使用還需要做很多改造以适應公司現有架構,典型的例子就是 dubbo 和阿裡内部的 HSF。為什麼内部不直接使用 dubbo?因為 HSF 做了很多跟内部系統綁定的事情,這樣可以讓開發人員用的更爽,但也就跟開源的系統漸行漸遠了。

大型微服務架構是微服務架構之上的東西,它是在一個或多個微服務架構之上,進一步解決效率問題的架構。提升效率的核心是讓所有業務方真正專注于業務本身,而不是想很多很重複的問題。如果 10,000 個服務花 5,000 人維護,每個人都思考怎麼接公司系統和怎麼做好穩定性,就算每次開發過程中花 10% 的時間思考這些,也浪費了 5,000 人的 10% 時間,想想都很多,省下來可以做很多業務。

▍Rule of least power

要想設計好大型微服務框,我們必須遵循“Rule of least power”(夠用就好)的原則。

這個原則是由 WWW 發明者 Tim Berners-Lee 提出的,它被廣泛用于指導各種 W3C 标準制定。Tim BL 說,最好的設計不是解決所有問題,而是恰好解決當下問題。就是因為我們面對的需求實際上是多變的,我們也不确定别人會怎麼用,是以我們要盡可能隻設計最本質的東西,減少複雜性,這樣做反而讓架構具有更多可能性。

Rule of least power 其實跟我們通常的設計思想相左,一般在設計架構的時候,架構師會比較傾向于“大而全”,由于我們一般都很難預測架構的使用者會如何使用,于是自然而然的會提供想象中“可能會被用到”的各種功能,導緻設計越來越可擴充的同時也越來越複雜。各種軟體架構的演進曆史告訴我們,“大而全”的架構最終都會被使用者抛棄,而且抛棄它的理由往往都是“太重了”,非常具有諷刺意味。

架構要想設計的“好”,就需要抓住需求的本質,隻有真正不變的東西才能進入架構,還沒想清楚的部分不要輕易納入架構,這種思想就是 Rule of least power 的一種應用方式。

▍大型微服務架構的設計要點

結合 Rule of least power 設計思想,我們在這裡列舉了大型微服務架構的設計要點。

最基本的,我們需要實作各種微服務架構必有的功能,例如服務治理、水準擴容等。需要注意的是,在這裡我們并不會再次重複造輪子,而是大量使用公司内外已有的技術積累,架構所做的事情是統一并抽象相關接口,讓業務代碼與具體實作解耦。

從工具鍊層面來說,我們讓業務無需操心開發調試之外的事情,這也要求與公司各種進行無縫內建,降低使用難度。

從設計風格上來說,我們提供非常有限度的擴充度,僅在必要的地方提供 interceptor 模式的擴充接口,所有架構元件都是以“組合”(composite)而不是“繼承”(inherit)方式提供給開發者。架構會提供依賴注入的能力,但這種依賴注入與傳統意義上 IoC 有一點差別,我們并不追求架構所有東西都可以 IoC,隻在我們覺得必要的地方有限度的開放這種能力,用來友善架構相容一些開源的架構或者庫,而不是讓業務代碼輕易的改變架構行為。

大型微服務架構最有特色的部分是提供了非常多的“可靠性”設計。我們刻意讓 RPC 調用的使用體驗跟普通的函數調用保持一緻,使用者隻用關系傳回值,永遠不需要思考崩潰處理、重試、服務異常處理等細節。通路基礎服務時,開發者可以像通路本地檔案一樣的通路分布式存儲,也是不需要關心任何可用性問題,正常的處理各種傳回值即可。在服務拆分和合并過程中,我們的架構可以讓拆分變得非常簡單,真的就跟類重構類似,隻需要将一個普通的 struct methods 進行拆分即可,剩下的所有事情自然而然會由架構做好。

▍精雕細琢:架構關鍵實作細節

▍業務實踐

接下來,我們聊聊這個架構在具體項目中的表現,以及我們在打磨細節的過程中積累的一些經驗。

我們落地的場景是一個非常大型的業務系統,2017 年底開始設計并開發。這個業務已經出現了五年,各個巨頭已經投入上千名研發持續開發,非常複雜,我們不可能在上線之初就完善所有功能,要這麼做起碼得幾百人做一年,我們等不起。實際落地過程中,我們投入上百人從一個最小系統慢慢疊代出來,最初版本隻開發了四個多月。

最開始做技術選型時,我們也在思考應該用什麼技術,甚至什麼語言。由于滴滴從 2015 年以來已經積累了 1,500+ Go 代碼子產品、上線了 2,000+ 服務、儲備了 1000+ Go 開發者,這使得我們非常自然的就選擇 Go 作為最核心的開發語言。

在這個業務中我們實作了非常多的核心能力,基本實作了前面所說大型微服務架構的各種核心功能,并達成預期目标。

滴滴杜歡:大型微服務架構設計實踐

同時,也因為滴滴擁有相對完善的基礎設施,我們在開發架構的時候也并沒有花費太多時間重複造一些業務無關的輪子,這讓我們在開發架構的時候也能專注于實作最具有特色的部分,客觀上幫助我們快速落地了整體架構思想。

上圖隻是簡單列了一些我們業務中常用的基礎設施,其實還有大量基礎設施也在公司中被廣泛使用,沒有提及。

▍整體架構

上圖是我們架構的整體架構。綠色部分是業務代碼,黃色部分是我們的架構,其他部分是各種基礎設施和第三方架構。

可以看到,綠色的業務代碼被架構整個包起來,屏蔽了業務代碼與底層的所有聯系。其實我們的架構隻做了一點微小的工作:将業務與所有的 I/O 隔離。未來底層發生任何變化,即使換了下面的服務,我們能夠通過黃色的相容層解決掉,業務一行代碼不用,底層 driver 做了任何更新業務也完全不受影響。

結合微服務開發的經驗,我們發現微服務開發與傳統軟體開發唯一的差別就是在于 I/O 的可靠程度不同,以前我們花費了大量的時間在各種不同的業務中處理“穩定性”問題,其實歸根結底都是類似的問題,本質上就是 I/O 不夠可靠。我們并不是要真的讓 I/O 變得跟讀取本地檔案一樣可靠,而是由架構統一所有的 I/O 操作并針對各種不可靠場景進行各種兜底,包括重試、節點摘除、鍊路逾時控制等,讓業務得到一個确定的傳回值——要麼成功,要麼就徹底失敗,無需再掙紮。

實際業務中,我們使用 I/O 的種類其實很少,也就不過十幾種,我們這個架構封裝了所有可能用到的 I/O 接口,把它們全部變成 Go interface 提供給業務。

▍實作要點

前面說了很多思路和概念,接下來我來聊聊具體的細節。

我們的架構跟很多架構都不一樣,為了實作架構與業務正交,這個架構幹脆連最基本的架構特征都沒有,MVC、middleware、AOP 等各種耳熟能詳的架構要素在這裡都不存在,我們隻是設計了一個執行環境,業務隻需要提供一個入口 type,它實作了所有業務需要對外暴露的公開方法,架構就會自動讓業務運轉起來。

我們同時使用兩種技術來實作這一點。一方面,我們提供了工具鍊,對于 IDL-based 的服務架構,我們可以直接分析 IDL 和生成的 Go interface 代碼的 AST,根據這些資訊透明的生成架構代碼,在每個接口調用前後插入必要的 stub 友善架構擴充各種能力。另一方面,我們在程式啟動的時候,通過反射拿到業務 type 的資訊,動态生成業務路由。

做到了這些事情之後業務開發就完全無需關注架構細節了,甚至我們可以做到業務像調試本地程式一樣調試微服務。同時,我們用這種方式避免業務思考“版本”這個問題,我們看到,很多服務架構都因為版本分裂造成了很大的維護成本,當我們這個架構成為一個開發環境之後,架構更新就變得完全透明,實際中我們會要求業務始終使用最新的架構代碼,從來不會使用 semver 标記版本号或者相容性,這樣讓架構的維護成本也大大降低。“更大的權力意味着更大的責任”,我們也為架構寫了大量的單元測試用例保證架構品質,并且規定架構無限向前相容,這種責任讓我們非常謹慎的開發上線功能,非常收斂的提供接口,進而保持業務對架構的信任。

滴滴杜歡:大型微服務架構設計實踐

大家也許聽說過,Go 官方的 database/sql 的 Stmt 很好用但是有可能會出現連接配接洩漏的問題,當這個問題剛被發現的時候,公司很多業務線都不得不修改了代碼,在業務中避免使用 Stmt,而我們的業務代碼完全不需要做任何修改,架構用很巧妙的方法直接修複了這個問題。

下圖是架構的啟動邏輯,可以看到,這個邏輯非常簡單:首先建立一個 Server 執行個體 s,傳入必要的配置參數;然後建立一個業務類型執行個體 handler,這個業務類型隻是個簡單的 type,并沒有任何限制;最後将接口 IDL interface 和 handler 傳入 s,啟動服務即可。

我們在 handler 和 IDL interface 之間加一個夾層并做了很多事情,這相當于在業務代碼的執行開始和結束前後插入了代碼,做了參數預處理、日志、崩潰恢複和清理工作。

我們還需要設計一個接口層來隔絕業務和底層之間的聯系。接口層本身沒什麼特别技術含量,隻是需要認真思考如何保證底層接口非常非常穩定,并且如何避免穿透接口直接調用底層能力,要做好這一點需要非常多的心力。

這個接口層的收益是比較容易了解的,可以很好的幫助業務減少無謂的代碼修改。開源架構就不能保證這一點,說不定什麼時候作者心情好了改了一個架構細節,無法向前相容,那麼業務就必須跟着做修改。公司内部架構則一般不太敢改接口,生怕造成不相容被業務投訴,但有些接口一開始設計的并不好,隻好不斷打更新檔,讓架構越來越亂。

要是真能做到接口層設計出來就不再變更,那就太好了。

那我們真的能做到麼?是的,我們做到了,其中的訣竅就是始終思考最本質最不變的東西是什麼,隻抽象這些不變的部分。

上圖就是一個經典案例,展示一下我們是怎麼設計 Redis 接口的。

左邊是 github.com/go-redis/redis 代碼(簡稱 go-redis),這是一個非常著名的 Redis driver;右邊是我們的 Redis 接口設計。

Go-redis 非常優秀,設計了一些很不錯的機制,比如 Cmder,巧妙的解決了 Pipeline 讀取結果的問題,每個接口的傳回值都是一個 Cmder 執行個體。但這種設計并不本質,包括函數的參數與傳回值類型都出現多次修改,包括我自己都曾經提過 Pull Request 修正它的一個參數錯誤問題,這種修改對于業務來說是非常頭疼的。

而我們的接口設計相比 go-redis 則更加貼近本質,我閱讀了 Redis 官方所有指令的協定設計和相關設計思路文檔,Redis 裡面最本質不變的東西是什麼呢?當然是 Redis 協定本身。Redis 在設計各種指令時非常嚴謹,做到了極為嚴格的向前相容,無論 Redis 從 1.0 到 3.x 如何變化,各個指令字的協定從未發生過不相容的變化。是以,我嚴格參照 Redis 指令字協定設計了我們的 Redis 接口,連接配接口的參數名都盡量與 Redis 官方保持一緻,并嚴格規定各種參數的類型。

我們小心的進行接口封裝之後,還有一些其他收獲。

還是以 Redis 為例,最開始我們底層的 Redis driver 使用的是公司廣泛采用的 github.com/gomodule/redigo,但後來發現不能很好的适配公司自研的 Redis 叢集一些功能,是以考慮切換成 go-redis。由于我們有這樣一層 Redis 接口封裝,這使得切換完全透明。

滴滴杜歡:大型微服務架構設計實踐

我們為了能夠讓業務研發不要關心很多的傳輸方面細節,我們實作了協定劫持。HTTP 很好劫持,這裡不再贅述,我主要說一下如何劫持 thrift。

劫持協定的目的是控制業務參數收到或發送的協定細節,可以友善我們根據傳輸内容輸出必要的日志或打點,還可以自動處理各種輸入或輸出參數,把必要參數帶上,免得業務忘記。

劫持思路非常簡單,我們做了一個有限狀态機(FSM),在旁路監聽協定的 read/write 過程并還原整個資料結構全貌。比如 Thrift Protocol,我們利用 Thrift 内置的責任鍊設計,自己實作了一個 protocol factory 來包裝底層的 protocol,在實際 protocol 之上做了一個 proxy 層攔截所有的 ReadXXX/WriteXXX 方法,就像是在外部的觀察者,記錄現在 read/write 到哪一個層級、讀寫了什麼結構。當我們發現現在正在 read/write 我們感興趣的内容,則開始劫持過程:對于 read,如果要“欺騙”應用層提供一些額外的架構資料或者屏蔽架構才關心的資料,我們就會篡改各種 ReadXXX 傳回值來讓應用層誤以為讀到了真實資料;對于 write,如果要偷偷注入架構才關心的内容,我們會在調用 WriteXXX 時主動調用底層 protocol 的相關 write 函數來提前寫入内容。

協定可以劫持之後,很多東西的處理就很簡單了。比如 context,我們隻要求業務在各個接口裡帶上 context,RPC 過程中則無需關心這個細節,架構會自動将 context 通過協定傳遞到下遊。

滴滴杜歡:大型微服務架構設計實踐

我們實作了協定劫持之後,要想實作跨服務邊界的 context 就變得很簡單了。

我們根據 context interface 和設計規範實作了自己的 context 類型,用來做一些序列化與反序列化的事情,當上下遊調用發生時,我們會從 context 裡提取架構關心的内容并注入到協定裡面,在下遊再透明解析出來重新放入 context。

使用 context 時候還有個小坑:context.WithDeadline 或者 context.WithTimeout 很容易被不小心忽略傳回的 cancel 函數,導緻 timer 資源洩露。我們為了避免出現這種情況設計了一個低精度 timer 來盡可能避免建立真正的 time.Time 執行個體。

我們發現,業務中根本不需要那麼高精度的 timer,我們說的各種逾時一般精度都隻到 ms,于是一個精度達 0.5ms 的 timer 就能滿足所有業務需求。同時,在業務中也不是特别需要使用 Context interface 的 Done() 方法,更多的隻是判斷一下是否已經逾時即可。為了避免大量建立 timer 和 channel,也為了避免讓業務使用 cancel 函數,我們實作了一個低精度 timer pool。這是一個 timer 的循環數組,将 1s 分割成若幹個時間間隔,設定 timer 的時候其實就是在這個數組上找到對應的時刻。預設情況下,done channel 都不需要初始化,直到真正有業務方需要 done channel 的時候才會 make 出來。在架構裡我們非常注意的避免使用任何 done channel,進而避免消耗資源且極大的提高了性能。

業務壓力大的時候,我們比較容易在代碼層面上犯錯,不小心就放大單點故障造成雪崩,我們借用前面所有的技術,讓調用逾時限制從上遊傳遞到下遊,如果單點崩潰了,架構會自動摘除故障節點并自動 fail-fast 避免壓力進一步上升,進而實作防雪崩。

防雪崩的具體實作原理很簡單:上遊調用時會設定一個逾時時間,這個時間通過跨邊界 context 傳遞到下遊,每個下遊節點在收到請求時開始記錄自己消耗的時間,如果自己耗時已經超出上遊規定的逾時時間就會主動停止一切 I/O 調用,快速傳回錯誤。

滴滴杜歡:大型微服務架構設計實踐

比如上遊 A 調用下遊 B 前設定 500ms 逾時,B 收到請求後就知道隻有 500ms 可用,從收到請求那一刻開始計時,每次在調用其他下遊服務前,比如通路 B 的下遊 C 本身需要 200ms,但目前 B 已經消耗了 400ms,隻剩 100ms 了,那麼架構會自動将 C 的逾時收斂到 100ms,這樣 C 就知道給自己的時間不多了,一旦 C 沒能在 100ms 内傳回就會主動 fail-fast,避免無謂的消耗系統資源,幫助 C 和 B 快速向上遊報告錯誤。

▍業務收益

我們實作的這個架構切實的給業務帶來了顯著的收益。

滴滴杜歡:大型微服務架構設計實踐

我們總共用超過 100 名 Go 語言開發者,在非常大的壓力下開發了好幾個月便完成一個完整可營運的系統,實作了大量功能,開發效率相當的高。我們後來代碼量和服務數量也不斷增加,并且由于業務發展我們還支援了國際化,實作了多機房部署,這個過程是比較順暢的。

我覺得非常自豪的是,我們剛上線一個月就做了全鍊路壓測,架構層稍作修改就搞定了,顯著提升了整體系統穩定性和抗壓能力,而這個過程對業務是完全透明的,對業務未來的疊代也是完全透明的。我們線上上也沒有出現過任何單點故障造成的雪崩,各種監控和關鍵日志也是自動的透明的做好,服務注冊發現、底層 driver 更新、一些架構 bug 修複等對業務都十分透明,業務隻用每次更新到最新版就好了,十分省心。

▍版本管理

最後提一個細節:管理架構的各個庫版本。

我相信很多開發者都有一種煩惱,就是管理各種分裂的代碼版本。一方面由于架構會不斷更新,需要不斷用 semver 規則更新版本,另一方面業務方又沒有動力及時更新到最新版,導緻架構各個庫的版本事實上出現了分裂。這個事情其實是不應該發生的,就像我們用作業系統,比如大家開發業務需要跑線上上 linux 伺服器上,我們會關心 linux kernel 版本麼?或者用 Go 開發,我們會總是關心用什麼 Go 版本麼?一般都不會關心的,這跟開發業務沒什麼關系。我們關心的是系統提供了哪些跟業務開發相關的接口,隻要接口不變且穩定,業務代碼就能正常的工作。

這是為什麼我們在設計架構的時候會花費很多心力保證接口穩定的原因,我們就是希望架構即作業系統,隻有做到這一點,業務才能放心大膽的用架構做業務,真正把業務做到快而不糙。也正因為這一點,我們甚至于不會給架構的各個庫打 tag,每次上線都必須全部将架構更新到最新版,徹底的解決了版本分裂的問題。

▍未來方向

未來我們還是有很多工作值得去做,比如完善工具鍊、接入更多的一些公司基礎設施等。

我們不确定是否能夠開源,大機率是不會開源,因為這個架構并不重要,它與滴滴各種基礎設施綁定,服務于滴滴研發,重要的是設計理念和思路,大家可以用類似方法因地制宜的在自己的公司裡實踐這種設計思想。

今天這個活動就是一個很好的場所,我希望通過這個機會跟大家分享這樣的想法,如果大家有興趣也歡迎跟我交流,我可以幫助大家在公司裡實作類似的設計。

▍Q&A

提問:我也一直在寫 Go 服務,你們每一個服務啟動是單程序還是多程序,每個程序怎麼限制核數?

杜歡:對于 Go 來講這個問題不是問題,一般都用單程序模式,然後通過 GOMAXPROCS 設定需要占用的核數,預設會占滿機器所有的核。

提問:我看到有 70+ 個微服務,微服務之間的接口和依賴關系怎麼維護?接口變更或者相容性怎麼解決?

杜歡:微服務業務層的接口變更這個事情無法避免,我們是通過 IDL 進行依賴管理,不是架構層保證,業務需要保證這個 IDL 是向前相容的。架構能幫我們做什麼呢?它可以幫我們做業務代碼遷移,根據我們的設計,隻要把一個名為 service 的目錄進行拆分合并即可,這裡面隻有一個簡單的類型 type Service struct {},以及很多 Service 類型的方法,每個檔案都實作了這個類型的一個或多個方法,我們可以友善的整合或者拆分這個目錄裡面的代碼,進而就能更改微服務的接口實作。

你剛剛問題是很業務的問題,怎麼管理之間依賴變化,這個沒有什麼好辦法,我們做重構的時候,還是通知上下遊,這個确實不是我們真正在架構層能夠解決的問題,我們隻能讓重構的過程變得簡單一些。

提問:上下遊傳輸 context 時設定逾時時間,每一個接口逾時時間是怎麼設計的?

杜歡:我們設的逾時時間就是通常意義上的這次請求從發起到收到應答的總時間。

提問:逾時時間怎麼定?各個子產品逾時時間不一樣麼?

杜歡:現在做得比較粗糙,還沒有做到統一管理所有的逾時時間,依然是業務方自己根據預期,在調用下遊前自己在代碼裡面寫的,希望未來這個可以做到統一管理。

提問:開發者怎麼知道下遊經過了怎樣的處理流程,能多長時間傳回呢?

杜歡:這個東西一般開發者都是知道的,因為所有業務服務接口都會有 SLA,所有服務對上遊承諾 SLA 是多少預先會定好。比如一個服務接口承諾 SLA 是 90 分位 50ms,上遊就會在這個基礎上打一些 buffer,将調用逾時設定成 70ms,比 SLA 大一點。實際中我們會結合這個服務接口在壓測和線上實際表現來設定逾時。我們其實很希望把 SLA 線上化管理,不過現在沒有完全做到這一點。

提問:咱們這邊有沒有出現類似的逾時情況?在測試期間或者線上?

杜歡:服務的時間逾時情況非常常見,但業務影響很小,架構會自動重試。

提問:一般什麼情況下會出現呢?

杜歡:最多的情況是調用外部的服務,比如我們會調用 Google Map 一些接口,他們就相對比較不穩定,調用一次可能會超過 2s 才傳回結果,導緻這條鍊路上的所有接口都會逾時。

提問:逾時的情況可以避免麼?

杜歡:不可能完全避免。一個服務接口不可能 100% 承諾自己的處理時間,就算 SLA 是 99 分位小于 50ms,那依然有 1% 可能性會超過這個值。

▍END

滴滴杜歡:大型微服務架構設計實踐

杜 歡

滴滴 | R lab 進階專家工程師

先後在微軟和百度任職。曾自主創業作為創始⼈和 CTO ,專注于遊戲領域創新項⽬研發落地。2015 年⾄今:曆任滴滴出⾏平台産品中⼼技術負責⼈、出⾏創新業務技術負責⼈、R lab 配送業務技術負責⼈。

來源:微信公衆号:滴滴技術

出處:https://mp.weixin.qq.com/s/-NmasbmN-dZbGxN8Vyq--w

繼續閱讀