滴滴大型微服務框架設計實踐

 

 

 

 

 

 

 

 

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

 

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

 

咱們在進行復雜業務開發的過程當中,有如下幾個常見的痛點:php

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

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

• 公司有各類⼯具平臺、SDK、最佳實踐,如何儘量的在業務中使⽤?git

 

互聯網業務研發的特色是「快」、「糙」、「猛」:開發節奏快、質量較粗糙、增加迅猛。咱們可否作到「快」、「猛」而「不糙」呢?這就須要有一些技術架構來守住質量基線,在業務快速堆砌代碼的時候也能保持技術架構的健康。github

 

在大型項目中,咱們也常常會短期彙集一批人蔘與開發,很顯然咱們沒有辦法保證這些人的能力和風格是徹底拉齊的,咱們須要儘量減小「人」在項目質量中的影響。redis

 

公司內有大量優秀的技術平臺和工具,業務中確定是但願儘量都用上的,但又不想付出太多的使用成本,一定須要有一些技術手段讓業務與公司基礎設施無縫集成起來。sql

 

很天然咱們會想到,有沒有一種「框架」能夠解決這個問題,帶着這個問題咱們探索了全部的可能性並找到一些答案。docker

服務框架進化史

服務框架的歷史能夠追溯到 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% 可能性會超過這個值。

相關文章
相關標籤/搜索