电竞比分网-中国电竞赛事及体育赛事平台

分享

滴滴大型微服務(wù)框架設(shè)計實踐

 甘甘灰 2019-06-17

大家好,我是杜歡,很榮幸能代表滴滴來做分享。我來滴滴的第一件事情就是幫助公司統(tǒng)一技術(shù)棧,在服務(wù)端我們要把以前拿 PHP 和 Java 做的服務(wù)統(tǒng)一起來,經(jīng)過很多思考和選擇之后我們決定用 Go 來重構(gòu)大部分業(yè)務(wù)服務(wù)?,F(xiàn)在,滴滴內(nèi)部已經(jīng)有非常多的用 Go 實現(xiàn)的服務(wù)和大量 Go 開發(fā)者。

《?型微服務(wù)框架設(shè)計實踐》是一個很大的話題,這個題目其實分為三個方面,“微服務(wù)框架”、“大型”和“設(shè)計實踐”。我們?nèi)粘?吹降母鞣N開源微服務(wù)框架,在我看來都不算“大型”,解決的問題比較單純。大型微服務(wù)框架究竟是什么,又應(yīng)該怎么去一步步落地實踐,我會從問題出發(fā),分別從以下幾個方面來探討這個話題。

· 發(fā)現(xiàn)問題:服務(wù)開發(fā)過程中的痛點

· 以史鑒今:從服務(wù)框架的演進歷程中找到規(guī)律

· ?道?簡:大型微服務(wù)框架的設(shè)計要點

· 精雕細琢:框架關(guān)鍵實現(xiàn)細節(jié)

********▍************發(fā)現(xiàn)問題:服務(wù)開發(fā)過程中的痛點****

▍****復(fù)雜業(yè)務(wù)開發(fā)過程中的痛點

我們在進行復(fù)雜業(yè)務(wù)開發(fā)的過程中,有以下幾個常見的痛點:

· 時間緊、任務(wù)多、團隊?、業(yè)務(wù)增?快,如何還能保證架構(gòu)穩(wěn)定可靠?

· 研發(fā)?平參差不?、項?壓??顧不暇,如何保證質(zhì)量基線不被突破?

· 公司有各種?具平臺、SDK、最佳實踐,如何盡可能的在業(yè)務(wù)中使??

互聯(lián)網(wǎng)業(yè)務(wù)研發(fā)的特點是“快”、“糙”、“猛”:開發(fā)節(jié)奏快、質(zhì)量較粗糙、增長迅猛。我們能否做到“快”、“猛”而“不糙”呢?這就需要有一些技術(shù)架構(gòu)來守住質(zhì)量基線,在業(yè)務(wù)快速堆砌代碼的時候也能保持技術(shù)架構(gòu)的健康。

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

公司內(nèi)有大量優(yōu)秀的技術(shù)平臺和工具,業(yè)務(wù)中肯定是希望盡可能都用上的,但又不想付出太多的使用成本,必定需要有一些技術(shù)手段讓業(yè)務(wù)與公司基礎(chǔ)設(shè)施無縫集成起來。

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

********▍************以史鑒今:從服務(wù)框架的演進歷程中找到規(guī)律****

▍****服務(wù)框架進化史

image

服務(wù)框架的歷史可以追溯到 1995 年,PHP 在那一年誕生。PHP 是一個服務(wù)框架,這個語言首先是一個模板,其次才是一種語言,默認情況下所有的 PHP 文件內(nèi)容都被直接發(fā)送到客戶端,只有使用了 標簽的部分才是代碼。在這段時間里,我們也稱作 Web 1.0 時代里,瀏覽器功能還不算強,很多的設(shè)計理念來源于 C/S 架構(gòu)的想法。這時候的服務(wù)框架的巔峰是 2002 年推出的 ASP.net,當年真的是非常驚艷,我們可以在 Visual Studio 里面通過拖動界面、雙擊按鈕寫代碼來完成一個網(wǎng)頁的開發(fā),非常具有顛覆性。當然,由于當時技術(shù)所限,這樣做出來的網(wǎng)頁體驗并不行,最終沒有成為主流。

接著,Web 2.0 時代來臨了,大家越來越覺得傳統(tǒng)軟件中經(jīng)常使用的 MVC 模式特別適合于服務(wù)端開發(fā)。Django 發(fā)布于 2003 年,這是一款非常經(jīng)典的 MVC 框架,包含了所有 MVC 框架必有的設(shè)計要素。MVC 框架的巔峰當屬 Ruby on Rails,它給我們帶來了非常多先進的設(shè)計理念,例如“約定大于配置”、Active Record、非常好用的工具鏈等。

2005 年后,各種 MVC 架構(gòu)的服務(wù)框架開始井噴式出現(xiàn),這里我就不做一一介紹。

▍****標志性服務(wù)框架

image.gif

隨著互聯(lián)網(wǎng)業(yè)務(wù)越來越復(fù)雜,前端邏輯越來越重,我們發(fā)現(xiàn)業(yè)務(wù)服務(wù)開始慢慢分化:頁面渲染的工作回到了前端;Model 層逐步下沉成獨立服務(wù),并且催生了 RPC 協(xié)議的流行;業(yè)務(wù)接入層只需要提供 API。于是,MVC 中的 V 和 M 逐步消失,演變成了路由框架和 RPC 框架兩種形態(tài),分別滿足不同的需求。2007 年,Sinatra 發(fā)布了,它是一個非常極致的純路由框架,大量使用 middleware 設(shè)計來擴展框架能力,業(yè)務(wù)代碼可以實現(xiàn)的非常簡潔優(yōu)雅。這個框架相對小眾(Github Stars 10k,實際也算很有名了),其設(shè)計思想影響了很多后續(xù)框架,包括 Express.js、Go martini 等。同年,Thrift 開源,這是 Facebook 內(nèi)部使用 RPC 框架,現(xiàn)在被廣泛用于各種微服務(wù)之中。Google 其實更早就在內(nèi)部使用 Protobuf,不過直到 2008 年才首次開源。

再往后,我們的基礎(chǔ)設(shè)施開始發(fā)生重大變革,微服務(wù)概念興起,虛擬化、docker 開始越來越流行,服務(wù)框架與業(yè)務(wù)越發(fā)解耦,甚至可以做到業(yè)務(wù)幾乎無感知。2018 年剛開源的 Istio 就是其中的典型,它專注于解決網(wǎng)絡(luò)觸達問題,包括服務(wù)治理、負載均衡、動態(tài)擴縮容等。

▍****服務(wù)框架的演進趨勢

通過回顧服務(wù)框架的發(fā)展史,我們發(fā)現(xiàn)服務(wù)框架變得越來越像一種新的“操作系統(tǒng)”,越來越多的框架讓我們忘記了 Web 開發(fā)有多么復(fù)雜,讓我們能專注于業(yè)務(wù)本身。就像操作系統(tǒng)一樣,我們在業(yè)務(wù)代碼中以為直接操作了內(nèi)存,但其實并不然,操作系統(tǒng)為我們屏蔽了總線尋址、虛地址空間、缺頁中斷等一系列細節(jié),這樣我們才能將注意力放在怎么使用內(nèi)存上,而不是這些跟業(yè)務(wù)無關(guān)的細節(jié)。

image

隨著框架對底層的抽象越來越高,框架的入門門檻在變低,以前我們需要逐步學(xué)習(xí)框架的各種概念之后才能開始寫業(yè)務(wù)代碼,到現(xiàn)在,很多框架都提供了非常簡潔好用的工具鏈,使用者很快就能專注輸出業(yè)務(wù)代碼。不過這也使得使用者更難以懂得框架背后發(fā)生的事情,想要做一些更深層次定制和優(yōu)化時變得相對困難很多,這使得框架的學(xué)習(xí)曲線越發(fā)趨近于“階躍式”。

隨著技術(shù)進步,框架也從代碼框架變成一種運行環(huán)境,框架代碼與業(yè)務(wù)代碼也不斷解耦。這時候就體現(xiàn)出 Go 的一些優(yōu)越性了,在容器生態(tài)里面,Go 占據(jù)著先發(fā)優(yōu)勢,同時 Go 的 interface 也非常適合于實現(xiàn) duck-typing 模式,避免業(yè)務(wù)代碼顯式的與框架耦合,同時 Go 的語法相對簡單,也比較容易用一些編譯器技巧來透明的增強業(yè)務(wù)代碼。

******▍********?道?簡:?型微服務(wù)框架的設(shè)計要點**

▍****站在全局視角觀察微服務(wù)架構(gòu)

服務(wù)框架的演進過程是有歷史必然性的。

傳統(tǒng) Web 網(wǎng)站最開始只是在簡單的呈現(xiàn)內(nèi)容和完成一些單純的業(yè)務(wù)流程,傳統(tǒng)的“三層結(jié)構(gòu)”(網(wǎng)站、中間件、存儲)就可以非常好的滿足需求。

Web 2.0 時代,隨著網(wǎng)絡(luò)帶寬和瀏覽器技術(shù)升級,更多的網(wǎng)站開始使用前端渲染,服務(wù)端則更多的退化成 API Gateway,前后端有了明顯的分層。同時,由于互聯(lián)網(wǎng)業(yè)務(wù)越來越復(fù)雜,存儲變得越來越多,不同業(yè)務(wù)模塊之間的存儲隔離勢在必行,這種場景催生了微服務(wù)架構(gòu),并且讓微服務(wù)框架、服務(wù)發(fā)現(xiàn)、全鏈路跟蹤、容器化等技術(shù)日漸興盛,成為現(xiàn)在討論的熱點話題,并且也出現(xiàn)了大量成熟可用的技術(shù)方案。

再往后呢?我們在滴滴的實踐中發(fā)現(xiàn),當一個公司的組織結(jié)構(gòu)成長為多事業(yè)群架構(gòu),每個事業(yè)群里面又有很多事業(yè)部,下面還有各種獨立的部門,在這種場景下,微服務(wù)之間也需要進行隔離和分層,一個部門往往會需要提供一個 API 或 broker 服務(wù)來屏蔽公司內(nèi)其他服務(wù)對這個部門服務(wù)的調(diào)用,在邏輯上就形成了由多個獨立微服務(wù)構(gòu)成的“大型微服務(wù)”。

image

在大型微服務(wù)架構(gòu)中,技術(shù)挑戰(zhàn)會發(fā)生什么變化?

據(jù)我所知,國內(nèi)某一線互聯(lián)網(wǎng)公司的一個事業(yè)群里部署了超過 10,000 個微服務(wù)。大家可以思考一下,假如一個項目里面有 10,000 個 class 并且互相會有各種調(diào)用關(guān)系,要設(shè)計好這樣的項目并且讓它容易擴展和維護是不是很困難?這是一定的。如果我們把一個微服務(wù)類比成一個 class,為了能夠讓這么復(fù)雜的體系可以正常運轉(zhuǎn),我們必須給 class 進行更進一步的分類,形成各種 class 之上的設(shè)計模式,比如 MVC。以我們開發(fā)軟件的經(jīng)驗來看,當開發(fā)單個 class 不再成為一件難事的時候,如何架構(gòu)這些 class 會變成我們設(shè)計的焦點。

我們看到前面是框架,更多解決是日?;A(chǔ)的東西,但是對于人與人之間如何高效合作、非常復(fù)雜的軟件架構(gòu)如何設(shè)計與維護,這些方面并沒有解決太好。

大型微服務(wù)的挑戰(zhàn)恰好就在于此。當我們解決了最基本的微服務(wù)框架所面臨的挑戰(zhàn)之后,如何進一步方便架構(gòu)師像操作 class 一樣來重構(gòu)微服務(wù)架構(gòu),這成了大型微服務(wù)框架應(yīng)該解決的問題。這對于互聯(lián)網(wǎng)公司來說是一個問題,比如我所負責的業(yè)務(wù)整個代碼量幾百萬行,看起來聽多了,但跟傳統(tǒng)軟件比就沒那么嚇人。以前 Windows 7 操作系統(tǒng),整體代碼量一億行,其中最大的單體應(yīng)用是 IE 有幾百萬行代碼,里面的 class 也有上萬個了。對于這樣規(guī)模的軟件要注意什么呢?是各種重構(gòu)工具,要能一鍵生成或合并或拆分 class,要讓軟件的組織形式足夠靈活。這里面的解決方法可以借鑒傳統(tǒng)軟件的開發(fā)思路。

▍****大型微服務(wù)框架的設(shè)計目標

image.gif

結(jié)合上面這些分析,我們意識到大型微服務(wù)框架實際上是開發(fā)人員的“效率產(chǎn)品”,我們不但要讓一線研發(fā)專注于業(yè)務(wù)開發(fā),也要讓大家?guī)缀鯚o感知的使用公司各種基礎(chǔ)設(shè)計,還要讓架構(gòu)師能夠非常輕易的調(diào)整微服務(wù)整體架構(gòu),方便像重構(gòu)代碼一樣重構(gòu)微服務(wù)整體架構(gòu),從而提升架構(gòu)的可維護性。

公司現(xiàn)有架構(gòu)就是業(yè)務(wù)軟件的操作系統(tǒng),不管公司現(xiàn)有架構(gòu)是什么,所有業(yè)務(wù)架構(gòu)必須基于公司現(xiàn)有基礎(chǔ)進行構(gòu)建,沒有哪個部門會在做業(yè)務(wù)的時候分精力去做運維系統(tǒng)?,F(xiàn)在所有的開源微服務(wù)框架都不知道大家底層實際在用什么,只解決一些通用性問題,要想真的落地使用還需要做很多改造以適應(yīng)公司現(xiàn)有架構(gòu),典型的例子就是 dubbo 和阿里內(nèi)部的 HSF。為什么內(nèi)部不直接使用 dubbo?因為 HSF 做了很多跟內(nèi)部系統(tǒng)綁定的事情,這樣可以讓開發(fā)人員用的更爽,但也就跟開源的系統(tǒng)漸行漸遠了。

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

▍****Rule of least power

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

這個原則是由 WWW 發(fā)明者 Tim Berners-Lee 提出的,它被廣泛用于指導(dǎo)各種 W3C 標準制定。Tim BL 說,最好的設(shè)計不是解決所有問題,而是恰好解決當下問題。就是因為我們面對的需求實際上是多變的,我們也不確定別人會怎么用,所以我們要盡可能只設(shè)計最本質(zhì)的東西,減少復(fù)雜性,這樣做反而讓框架具有更多可能性。

Rule of least power 其實跟我們通常的設(shè)計思想相左,一般在設(shè)計框架的時候,架構(gòu)師會比較傾向于“大而全”,由于我們一般都很難預(yù)測框架的使用者會如何使用,于是自然而然的會提供想象中“可能會被用到”的各種功能,導(dǎo)致設(shè)計越來越可擴展的同時也越來越復(fù)雜。各種軟件框架的演進歷史告訴我們,“大而全”的框架最終都會被使用者拋棄,而且拋棄它的理由往往都是“太重了”,非常具有諷刺意味。

框架要想設(shè)計的“好”,就需要抓住需求的本質(zhì),只有真正不變的東西才能進入框架,還沒想清楚的部分不要輕易納入框架,這種思想就是 Rule of least power 的一種應(yīng)用方式。

▍****大型微服務(wù)框架的設(shè)計要點

結(jié)合 Rule of least power 設(shè)計思想,我們在這里列舉了大型微服務(wù)框架的設(shè)計要點。

image

最基本的,我們需要實現(xiàn)各種微服務(wù)框架必有的功能,例如服務(wù)治理、水平擴容等。需要注意的是,在這里我們并不會再次重復(fù)造輪子,而是大量使用公司內(nèi)外已有的技術(shù)積累,框架所做的事情是統(tǒng)一并抽象相關(guān)接口,讓業(yè)務(wù)代碼與具體實現(xiàn)解耦。

從工具鏈層面來說,我們讓業(yè)務(wù)無需操心開發(fā)調(diào)試之外的事情,這也要求與公司各種進行無縫集成,降低使用難度。

從設(shè)計風格上來說,我們提供非常有限度的擴展度,僅在必要的地方提供 interceptor 模式的擴展接口,所有框架組件都是以“組合”(composite)而不是“繼承”(inherit)方式提供給開發(fā)者??蚣軙峁┮蕾囎⑷氲哪芰Γ@種依賴注入與傳統(tǒng)意義上 IoC 有一點區(qū)別,我們并不追求框架所有東西都可以 IoC,只在我們覺得必要的地方有限度的開放這種能力,用來方便框架兼容一些開源的框架或者庫,而不是讓業(yè)務(wù)代碼輕易的改變框架行為。

大型微服務(wù)框架最有特色的部分是提供了非常多的“可靠性”設(shè)計。我們刻意讓 RPC 調(diào)用的使用體驗跟普通的函數(shù)調(diào)用保持一致,使用者只用關(guān)系返回值,永遠不需要思考崩潰處理、重試、服務(wù)異常處理等細節(jié)。訪問基礎(chǔ)服務(wù)時,開發(fā)者可以像訪問本地文件一樣的訪問分布式存儲,也是不需要關(guān)心任何可用性問題,正常的處理各種返回值即可。在服務(wù)拆分和合并過程中,我們的框架可以讓拆分變得非常簡單,真的就跟類重構(gòu)類似,只需要將一個普通的 struct methods 進行拆分即可,剩下的所有事情自然而然會由框架做好。

******▍********精雕細琢:框架關(guān)鍵實現(xiàn)細節(jié)**

▍****業(yè)務(wù)實踐

接下來,我們聊聊這個框架在具體項目中的表現(xiàn),以及我們在打磨細節(jié)的過程中積累的一些經(jīng)驗。

我們落地的場景是一個非常大型的業(yè)務(wù)系統(tǒng),2017 年底開始設(shè)計并開發(fā)。這個業(yè)務(wù)已經(jīng)出現(xiàn)了五年,各個巨頭已經(jīng)投入上千名研發(fā)持續(xù)開發(fā),非常復(fù)雜,我們不可能在上線之初就完善所有功能,要這么做起碼得幾百人做一年,我們等不起。實際落地過程中,我們投入上百人從一個最小系統(tǒng)慢慢迭代出來,最初版本只開發(fā)了四個多月。

最開始做技術(shù)選型時,我們也在思考應(yīng)該用什么技術(shù),甚至什么語言。由于滴滴從 2015 年以來已經(jīng)積累了 1,500+ Go 代碼模塊、上線了 2,000+ 服務(wù)、儲備了 1000+ Go 開發(fā)者,這使得我們非常自然的就選擇 Go 作為最核心的開發(fā)語言。

在這個業(yè)務(wù)中我們實現(xiàn)了非常多的核心能力,基本實現(xiàn)了前面所說大型微服務(wù)框架的各種核心功能,并達成預(yù)期目標。

image

同時,也因為滴滴擁有相對完善的基礎(chǔ)設(shè)施,我們在開發(fā)框架的時候也并沒有花費太多時間重復(fù)造一些業(yè)務(wù)無關(guān)的輪子,這讓我們在開發(fā)框架的時候也能專注于實現(xiàn)最具有特色的部分,客觀上幫助我們快速落地了整體架構(gòu)思想。

上圖只是簡單列了一些我們業(yè)務(wù)中常用的基礎(chǔ)設(shè)施,其實還有大量基礎(chǔ)設(shè)施也在公司中被廣泛使用,沒有提及。

▍****整體架構(gòu)

image

上圖是我們框架的整體架構(gòu)。綠色部分是業(yè)務(wù)代碼,黃色部分是我們的框架,其他部分是各種基礎(chǔ)設(shè)施和第三方框架。

可以看到,綠色的業(yè)務(wù)代碼被框架整個包起來,屏蔽了業(yè)務(wù)代碼與底層的所有聯(lián)系。其實我們的框架只做了一點微小的工作:將業(yè)務(wù)與所有的 I/O 隔離。未來底層發(fā)生任何變化,即使換了下面的服務(wù),我們能夠通過黃色的兼容層解決掉,業(yè)務(wù)一行代碼不用,底層 driver 做了任何升級業(yè)務(wù)也完全不受影響。

結(jié)合微服務(wù)開發(fā)的經(jīng)驗,我們發(fā)現(xiàn)微服務(wù)開發(fā)與傳統(tǒng)軟件開發(fā)唯一的區(qū)別就是在于 I/O 的可靠程度不同,以前我們花費了大量的時間在各種不同的業(yè)務(wù)中處理“穩(wěn)定性”問題,其實歸根結(jié)底都是類似的問題,本質(zhì)上就是 I/O 不夠可靠。我們并不是要真的讓 I/O 變得跟讀取本地文件一樣可靠,而是由框架統(tǒng)一所有的 I/O 操作并針對各種不可靠場景進行各種兜底,包括重試、節(jié)點摘除、鏈路超時控制等,讓業(yè)務(wù)得到一個確定的返回值——要么成功,要么就徹底失敗,無需再掙扎。

實際業(yè)務(wù)中,我們使用 I/O 的種類其實很少,也就不過十幾種,我們這個框架封裝了所有可能用到的 I/O 接口,把它們?nèi)孔兂?Go interface 提供給業(yè)務(wù)。

▍****實現(xiàn)要點

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

我們的框架跟很多框架都不一樣,為了實現(xiàn)框架與業(yè)務(wù)正交,這個框架干脆連最基本的框架特征都沒有,MVC、middleware、AOP 等各種耳熟能詳?shù)目蚣芤卦谶@里都不存在,我們只是設(shè)計了一個執(zhí)行環(huán)境,業(yè)務(wù)只需要提供一個入口 type,它實現(xiàn)了所有業(yè)務(wù)需要對外暴露的公開方法,框架就會自動讓業(yè)務(wù)運轉(zhuǎn)起來。

我們同時使用兩種技術(shù)來實現(xiàn)這一點。一方面,我們提供了工具鏈,對于 IDL-based 的服務(wù)框架,我們可以直接分析 IDL 和生成的 Go interface 代碼的 AST,根據(jù)這些信息透明的生成框架代碼,在每個接口調(diào)用前后插入必要的 stub 方便框架擴展各種能力。另一方面,我們在程序啟動的時候,通過反射拿到業(yè)務(wù) type 的信息,動態(tài)生成業(yè)務(wù)路由。

做到了這些事情之后業(yè)務(wù)開發(fā)就完全無需關(guān)注框架細節(jié)了,甚至我們可以做到業(yè)務(wù)像調(diào)試本地程序一樣調(diào)試微服務(wù)。同時,我們用這種方式避免業(yè)務(wù)思考“版本”這個問題,我們看到,很多服務(wù)框架都因為版本分裂造成了很大的維護成本,當我們這個框架成為一個開發(fā)環(huán)境之后,框架升級就變得完全透明,實際中我們會要求業(yè)務(wù)始終使用最新的框架代碼,從來不會使用 semver 標記版本號或者兼容性,這樣讓框架的維護成本也大大降低?!案蟮臋?quán)力意味著更大的責任”,我們也為框架寫了大量的單元測試用例保證框架質(zhì)量,并且規(guī)定框架無限向前兼容,這種責任讓我們非常謹慎的開發(fā)上線功能,非常收斂的提供接口,從而保持業(yè)務(wù)對框架的信任。

image.gif

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

下圖是框架的啟動邏輯,可以看到,這個邏輯非常簡單:首先創(chuàng)建一個 Server 實例 s,傳入必要的配置參數(shù);然后新建一個業(yè)務(wù)類型實例 handler,這個業(yè)務(wù)類型只是個簡單的 type,并沒有任何約束;最后將接口 IDL interface 和 handler 傳入 s,啟動服務(wù)即可。

我們在 handler 和 IDL interface 之間加一個夾層并做了很多事情,這相當于在業(yè)務(wù)代碼的執(zhí)行開始和結(jié)束前后插入了代碼,做了參數(shù)預(yù)處理、日志、崩潰恢復(fù)和清理工作。

image

我們還需要設(shè)計一個接口層來隔絕業(yè)務(wù)和底層之間的聯(lián)系。接口層本身沒什么特別技術(shù)含量,只是需要認真思考如何保證底層接口非常非常穩(wěn)定,并且如何避免穿透接口直接調(diào)用底層能力,要做好這一點需要非常多的心力。

這個接口層的收益是比較容易理解的,可以很好的幫助業(yè)務(wù)減少無謂的代碼修改。開源框架就不能保證這一點,說不定什么時候作者心情好了改了一個框架細節(jié),無法向前兼容,那么業(yè)務(wù)就必須跟著做修改。公司內(nèi)部框架則一般不太敢改接口,生怕造成不兼容被業(yè)務(wù)投訴,但有些接口一開始設(shè)計的并不好,只好不斷打補丁,讓框架越來越亂。

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

image

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

image

上圖就是一個經(jīng)典案例,展示一下我們是怎么設(shè)計 Redis 接口的。

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

Go-redis 非常優(yōu)秀,設(shè)計了一些很不錯的機制,比如 Cmder,巧妙的解決了 Pipeline 讀取結(jié)果的問題,每個接口的返回值都是一個 Cmder 實例。但這種設(shè)計并不本質(zhì),包括函數(shù)的參數(shù)與返回值類型都出現(xiàn)多次修改,包括我自己都曾經(jīng)提過 Pull Request 修正它的一個參數(shù)錯誤問題,這種修改對于業(yè)務(wù)來說是非常頭疼的。

而我們的接口設(shè)計相比 go-redis 則更加貼近本質(zhì),我閱讀了 Redis 官方所有命令的協(xié)議設(shè)計和相關(guān)設(shè)計思路文檔,Redis 里面最本質(zhì)不變的東西是什么呢?當然是 Redis 協(xié)議本身。Redis 在設(shè)計各種命令時非常嚴謹,做到了極為嚴格的向前兼容,無論 Redis 從 1.0 到 3.x 如何變化,各個命令字的協(xié)議從未發(fā)生過不兼容的變化。因此,我嚴格參照 Redis 命令字協(xié)議設(shè)計了我們的 Redis 接口,連接口的參數(shù)名都盡量與 Redis 官方保持一致,并嚴格規(guī)定各種參數(shù)的類型。

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

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

image.gif

我們?yōu)榱四軌蜃寴I(yè)務(wù)研發(fā)不要關(guān)心很多的傳輸方面細節(jié),我們實現(xiàn)了協(xié)議劫持。HTTP 很好劫持,這里不再贅述,我主要說一下如何劫持 thrift。

劫持協(xié)議的目的是控制業(yè)務(wù)參數(shù)收到或發(fā)送的協(xié)議細節(jié),可以方便我們根據(jù)傳輸內(nèi)容輸出必要的日志或打點,還可以自動處理各種輸入或輸出參數(shù),把必要參數(shù)帶上,免得業(yè)務(wù)忘記。

劫持思路非常簡單,我們做了一個有限狀態(tài)機(FSM),在旁路監(jiān)聽協(xié)議的 read/write 過程并還原整個數(shù)據(jù)結(jié)構(gòu)全貌。比如 Thrift Protocol,我們利用 Thrift 內(nèi)置的責任鏈設(shè)計,自己實現(xiàn)了一個 protocol factory 來包裝底層的 protocol,在實際 protocol 之上做了一個 proxy 層攔截所有的 ReadXXX/WriteXXX 方法,就像是在外部的觀察者,記錄現(xiàn)在 read/write 到哪一個層級、讀寫了什么結(jié)構(gòu)。當我們發(fā)現(xiàn)現(xiàn)在正在 read/write 我們感興趣的內(nèi)容,則開始劫持過程:對于 read,如果要“欺騙”應(yīng)用層提供一些額外的框架數(shù)據(jù)或者屏蔽框架才關(guān)心的數(shù)據(jù),我們就會篡改各種 ReadXXX 返回值來讓應(yīng)用層誤以為讀到了真實數(shù)據(jù);對于 write,如果要偷偷注入框架才關(guān)心的內(nèi)容,我們會在調(diào)用 WriteXXX 時主動調(diào)用底層 protocol 的相關(guān) write 函數(shù)來提前寫入內(nèi)容。

協(xié)議可以劫持之后,很多東西的處理就很簡單了。比如 context,我們只要求業(yè)務(wù)在各個接口里帶上 context,RPC 過程中則無需關(guān)心這個細節(jié),框架會自動將 context 通過協(xié)議傳遞到下游。

image

我們實現(xiàn)了協(xié)議劫持之后,要想實現(xiàn)跨服務(wù)邊界的 context 就變得很簡單了。

我們根據(jù) context interface 和設(shè)計規(guī)范實現(xiàn)了自己的 context 類型,用來做一些序列化與反序列化的事情,當上下游調(diào)用發(fā)生時,我們會從 context 里提取框架關(guān)心的內(nèi)容并注入到協(xié)議里面,在下游再透明解析出來重新放入 context。

使用 context 時候還有個小坑:context.WithDeadline 或者 context.WithTimeout 很容易被不小心忽略返回的 cancel 函數(shù),導(dǎo)致 timer 資源泄露。我們?yōu)榱吮苊獬霈F(xiàn)這種情況設(shè)計了一個低精度 timer 來盡可能避免創(chuàng)建真正的 time.Time 實例。

image

我們發(fā)現(xiàn),業(yè)務(wù)中根本不需要那么高精度的 timer,我們說的各種超時一般精度都只到 ms,于是一個精度達 0.5ms 的 timer 就能滿足所有業(yè)務(wù)需求。同時,在業(yè)務(wù)中也不是特別需要使用 Context interface 的 Done() 方法,更多的只是判斷一下是否已經(jīng)超時即可。為了避免大量創(chuàng)建 timer 和 channel,也為了避免讓業(yè)務(wù)使用 cancel 函數(shù),我們實現(xiàn)了一個低精度 timer pool。這是一個 timer 的循環(huán)數(shù)組,將 1s 分割成若干個時間間隔,設(shè)置 timer 的時候其實就是在這個數(shù)組上找到對應(yīng)的時刻。默認情況下,done channel 都不需要初始化,直到真正有業(yè)務(wù)方需要 done channel 的時候才會 make 出來。在框架里我們非常注意的避免使用任何 done channel,從而避免消耗資源且極大的提高了性能。

業(yè)務(wù)壓力大的時候,我們比較容易在代碼層面上犯錯,不小心就放大單點故障造成雪崩,我們借用前面所有的技術(shù),讓調(diào)用超時約束從上游傳遞到下游,如果單點崩潰了,框架會自動摘除故障節(jié)點并自動 fail-fast 避免壓力進一步上升,從而實現(xiàn)防雪崩。

image

防雪崩的具體實現(xiàn)原理很簡單:上游調(diào)用時會設(shè)置一個超時時間,這個時間通過跨邊界 context 傳遞到下游,每個下游節(jié)點在收到請求時開始記錄自己消耗的時間,如果自己耗時已經(jīng)超出上游規(guī)定的超時時間就會主動停止一切 I/O 調(diào)用,快速返回錯誤。

image

比如上游 A 調(diào)用下游 B 前設(shè)置 500ms 超時,B 收到請求后就知道只有 500ms 可用,從收到請求那一刻開始計時,每次在調(diào)用其他下游服務(wù)前,比如訪問 B 的下游 C 本身需要 200ms,但當前 B 已經(jīng)消耗了 400ms,只剩 100ms 了,那么框架會自動將 C 的超時收斂到 100ms,這樣 C 就知道給自己的時間不多了,一旦 C 沒能在 100ms 內(nèi)返回就會主動 fail-fast,避免無謂的消耗系統(tǒng)資源,幫助 C 和 B 快速向上游報告錯誤。

▍****業(yè)務(wù)收益

我們實現(xiàn)的這個框架切實的給業(yè)務(wù)帶來了顯著的收益。

image

我們總共用超過 100 名 Go 語言開發(fā)者,在非常大的壓力下開發(fā)了好幾個月便完成一個完整可運營的系統(tǒng),實現(xiàn)了大量功能,開發(fā)效率相當?shù)母?。我們后來代碼量和服務(wù)數(shù)量也不斷增加,并且由于業(yè)務(wù)發(fā)展我們還支持了國際化,實現(xiàn)了多機房部署,這個過程是比較順暢的。

我覺得非常自豪的是,我們剛上線一個月就做了全鏈路壓測,框架層稍作修改就搞定了,顯著提升了整體系統(tǒng)穩(wěn)定性和抗壓能力,而這個過程對業(yè)務(wù)是完全透明的,對業(yè)務(wù)未來的迭代也是完全透明的。我們在線上也沒有出現(xiàn)過任何單點故障造成的雪崩,各種監(jiān)控和關(guān)鍵日志也是自動的透明的做好,服務(wù)注冊發(fā)現(xiàn)、底層 driver 升級、一些框架 bug 修復(fù)等對業(yè)務(wù)都十分透明,業(yè)務(wù)只用每次升級到最新版就好了,十分省心。

▍****版本管理

最后提一個細節(jié):管理框架的各個庫版本。

我相信很多開發(fā)者都有一種煩惱,就是管理各種分裂的代碼版本。一方面由于框架會不斷升級,需要不斷用 semver 規(guī)則升級版本,另一方面業(yè)務(wù)方又沒有動力及時升級到最新版,導(dǎo)致框架各個庫的版本事實上出現(xiàn)了分裂。這個事情其實是不應(yīng)該發(fā)生的,就像我們用操作系統(tǒng),比如大家開發(fā)業(yè)務(wù)需要跑在線上 linux 服務(wù)器上,我們會關(guān)心 linux kernel 版本么?或者用 Go 開發(fā),我們會總是關(guān)心用什么 Go 版本么?一般都不會關(guān)心的,這跟開發(fā)業(yè)務(wù)沒什么關(guān)系。我們關(guān)心的是系統(tǒng)提供了哪些跟業(yè)務(wù)開發(fā)相關(guān)的接口,只要接口不變且穩(wěn)定,業(yè)務(wù)代碼就能正常的工作。

這是為什么我們在設(shè)計框架的時候會花費很多心力保證接口穩(wěn)定的原因,我們就是希望框架即操作系統(tǒng),只有做到這一點,業(yè)務(wù)才能放心大膽的用框架做業(yè)務(wù),真正把業(yè)務(wù)做到快而不糙。也正因為這一點,我們甚至于不會給框架的各個庫打 tag,每次上線都必須全部將框架升級到最新版,徹底的解決了版本分裂的問題。

▍****未來方向

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

我們不確定是否能夠開源,大概率是不會開源,因為這個框架并不重要,它與滴滴各種基礎(chǔ)設(shè)施綁定,服務(wù)于滴滴研發(fā),重要的是設(shè)計理念和思路,大家可以用類似方法因地制宜的在自己的公司里實踐這種設(shè)計思想。

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

▍****Q&A

提問:我也一直在寫 Go 服務(wù),你們每一個服務(wù)啟動是單進程還是多進程,每個進程怎么限制核數(shù)?

杜歡:對于 Go 來講這個問題不是問題,一般都用單進程模式,然后通過 GOMAXPROCS 設(shè)置需要占用的核數(shù),默認會占滿機器所有的核。

提問:我看到有 70+ 個微服務(wù),微服務(wù)之間的接口和依賴關(guān)系怎么維護?接口變更或者兼容性怎么解決?

杜歡:微服務(wù)業(yè)務(wù)層的接口變更這個事情無法避免,我們是通過 IDL 進行依賴管理,不是框架層保證,業(yè)務(wù)需要保證這個 IDL 是向前兼容的??蚣苣軒臀覀冏鍪裁茨??它可以幫我們做業(yè)務(wù)代碼遷移,根據(jù)我們的設(shè)計,只要把一個名為 service 的目錄進行拆分合并即可,這里面只有一個簡單的類型 type Service struct {},以及很多 Service 類型的方法,每個文件都實現(xiàn)了這個類型的一個或多個方法,我們可以方便的整合或者拆分這個目錄里面的代碼,從而就能更改微服務(wù)的接口實現(xiàn)。

你剛剛問題是很業(yè)務(wù)的問題,怎么管理之間依賴變化,這個沒有什么好辦法,我們做重構(gòu)的時候,還是通知上下游,這個確實不是我們真正在框架層能夠解決的問題,我們只能讓重構(gòu)的過程變得簡單一些。

提問:上下游傳輸 context 時設(shè)置超時時間,每一個接口超時時間是怎么設(shè)計的?

杜歡:我們設(shè)的超時時間就是通常意義上的這次請求從發(fā)起到收到應(yīng)答的總時間。

提問:超時時間怎么定?各個模塊超時時間不一樣么?

杜歡:現(xiàn)在做得比較粗糙,還沒有做到統(tǒng)一管理所有的超時時間,依然是業(yè)務(wù)方自己根據(jù)預(yù)期,在調(diào)用下游前自己在代碼里面寫的,希望未來這個可以做到統(tǒng)一管理。

提問:開發(fā)者怎么知道下游經(jīng)過了怎樣的處理流程,能多長時間返回呢?

杜歡:這個東西一般開發(fā)者都是知道的,因為所有業(yè)務(wù)服務(wù)接口都會有 SLA,所有服務(wù)對上游承諾 SLA 是多少預(yù)先會定好。比如一個服務(wù)接口承諾 SLA 是 90 分位 50ms,上游就會在這個基礎(chǔ)上打一些 buffer,將調(diào)用超時設(shè)置成 70ms,比 SLA 大一點。實際中我們會結(jié)合這個服務(wù)接口在壓測和線上實際表現(xiàn)來設(shè)置超時。我們其實很希望把 SLA 線上化管理,不過現(xiàn)在沒有完全做到這一點。

提問:咱們這邊有沒有出現(xiàn)類似的超時情況?在測試期間或者線上?

杜歡:服務(wù)的時間超時情況非常常見,但業(yè)務(wù)影響很小,框架會自動重試。

提問:一般什么情況下會出現(xiàn)呢?

杜歡:最多的情況是調(diào)用外部的服務(wù),比如我們會調(diào)用 Google Map 一些接口,他們就相對比較不穩(wěn)定,調(diào)用一次可能會超過 2s 才返回結(jié)果,導(dǎo)致這條鏈路上的所有接口都會超時。

提問:超時的情況可以避免么?

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

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多