微服務架構的責任困境

TaoWen發表於2017-03-23

引言

創世記第11章1-9句記錄了“巴別城”的故事。當時地上的人們都說同一種語言,當人們離開東方之後,他們來到了示拿之地。在那裡,人們想方設法燒磚好讓他們能夠造出一座城和一座高聳入雲的塔來傳播自己的名聲,以免他們分散到世界各地。上帝來到人間後看到了這座城和這座塔,說一群只說一種語言的人以後便沒有他們做不成的事了;於是上帝將他們的語言打亂,這樣他們就不能聽懂對方說什麼了,還把他們分散到了世界各地,這座城市也停止了修建。這座城市就被稱為“巴別城”。《欽定版聖經》是這樣描寫的:

4 他們說,“來吧,我們要建造一座城和一座塔,塔頂通天,為了揚我們的名,免得我們被分散到世界各地。”

5 但是耶和華降臨看到了世人所建造的城和塔。

6 耶和華說,“看哪,他們都是一樣的人,說著同一種語言,如今他們既然能做起這事,以後他們想要做的事就沒有不成功的了。”

7 讓我們下去,在那裡打亂他們的語言,讓他們不能知曉別人的意思。

8 於是耶和華使他們分散到了世界各地,他們也就停止建造那座城。

9 因為耶和華在那裡打亂了天下人的言語,使眾人分散到了世界各地,所以那座城名叫巴別。——Genesis 11:4–9
[10]

願景

每人都可以隨時獲取一個開發環境。在其中做開發。並且隨時可以驗證自己寫的程式碼在整個系統裡整合起來是否工作正常。我可以立即得到有效反饋,從而提高工作效率。

嘗試一:書同文

最理想的情況是所有的開發者使用完全相同的技術。他們使用同樣的程式語言,他們使用同樣的開發框架。他們使用同樣的作業系統。他們使用同樣的IDE。他們使用同樣的風格管理 GIT 倉庫。他們使用同一種語言來寫構建指令碼。如果這一些都是真的,想要獲得一個開發環境,那會是非常簡單的事情。

曾經嘗試過各種方式把自己認為“最優”的開發環境兜售給同僚,給下級,甚至是給我的僱員。但是都失敗了。讓這幫 geek 們使用同樣的方式工作,這個難題比我們這裡要解決的問題更難。上帝來到人間後看到了這座城和這座塔,說一群只說一種語言的程式設計師以後便沒有他們做不成的事了。於是神創造了emacs和vim。

打著提高開發效率的方式去強姦別人的開發環境是行不通的。無論你說你寫的庫再好,都不如我的庫寫得好。無論我今天寫的庫在好,也不如我明天想造的下一個輪子好。這個道理普世於各種IDE工具的執念,各種程式語言的執念。

不要只看到始皇帝做了書同文的偉業。還要看到人家焚書坑儒的本事。

嘗試二:自動化指令碼

退而求其次的辦法是,不要求所有的元件都是同樣的方式開發出來的。我們只要求你們各個模組的owner都提供一個共同的自動化指令碼。當我們把這些指令碼拼接到一起之後,整個系統就實現了自動化的部署。

這條道路演進的盡頭就是一堆bash/python/msbuild指令碼,加上一個cmdb資料庫組成的怪獸。我已經在 閒談叢集管理模式 - taowen - SegmentFault 一文裡描述了這個模式的問題

  • 無法審查的指令碼:給定一個 f(),你無法知道這個 f() 到底做了什麼。這個是所有基於指令碼組合的方式(函式套函式)做自動化的根本問題。最近這次 S3 的故障,就是因為誤執行一個 saltstack 指令碼導致的。而任何執行這個指令碼的程式,是無法在載入指令碼之後做任何事情來驗證其許可權和危害的。對於任何runner來說,f() 就是一個黑盒的 f()。
  • 狀態漂移:開發環境指令碼在一個新的開發機上可能執行,但是在一個已經安裝模組A的機器上就可能執行失敗。或者在安裝了A,B的,但是沒有裝C的機器上會失敗。指令碼執行的起始狀態可能有千千萬萬,不可能測試齊全。指令碼執行完之後留下的系統的狀態也千千萬萬。

這兩點技術上的硬傷,導致了基於一堆指令碼堆砌出來的自動化彷彿浮沙之上築高臺一般。

嘗試三:Docker + 組網技術

在發現了指令碼自動化的缺陷之後,我陷入了對 Docker 技術的痴迷。Docker 的技術上的好處顯而易見。它完美地把業務程式碼封裝到了一個容器裡,無論你是什麼語言寫的,什麼框架開發的,最終都是一個 docker 執行命令。放佛巴別塔問題解決了,只要有了 docker,什麼東西都是一樣部署的。docker 映象由模組owner提供,他們怎麼弄出來是他們自己的事情。你想用 node.js,還是 haskell,隨便。而且 Docker 完美解決了基於指令碼技術的狀態累積漂移問題。所謂immutable infrastructure。

Docker 還稍微有一些問題,那就是還需要把多個服務通過網路組裝到一起。這個也不是什麼大問題。雖然通過服務發現這樣的方式推廣起來有難度,但是我們還有網路代理的大招。比如指定100.64.0.1 代表 mysql,100.64.0.2 代表 redis。通過網路層攔截這些 ip 的請求,我們可以不修改業務程式碼的情況下,把這些 docker 容器給組裝起來。

docker 基於了 linux ABI,代理版本的服務發現基於了 tcp/ip。這兩個東西在巴別塔的時代就是一個bug級別的存在,但是本質上還是書同文。上帝留了一個口子,給我們鑽了漏洞。於是我們又可以把這些亂七八糟的東西攢一起了。

Docker 也無法解決書同文的問題

這種方式完美了嗎?我們再次發現了巴別塔問題。實際上的軟體是這樣工作的

微服務架構的責任困境

某種程度上來說,我們通過Docker,通過組網,構建出來的系統只是一個空殼而已。它只是一門開發語言,它通過各種配置介面提供了自己的開發工具。PM通過產品配置,配置出了產品,而運營再基於產品,配置出了真正執行起來可以賺錢的系統。

Docker 可以利用 linux ABI 這個“書同文”的介面,整合了後臺服務。網路代理利用 TCP/IP 這個“書同文”的介面把這些服務串聯了起來。但是對於各個模組自己提供的產品配置和運營配置,我們就沒有這麼幸運了。有的模組使用了csv,有的使用了json,有的使用了資料庫。當你要重現一個“環境”的時候。它不僅僅是意味著程式的啟動,意味著網路的連線。它還需要把各種配置資料灌入到系統。而搞清楚有哪些配置,讓這些模組使用完全相同的方式來定義和使用配置,我們就又回到了第一步,書同文的路數裡了。書同文的成功實施,來自於強大的中央權威。如果擁有強大的中央權威,完全可以從一開始就強推書同文的開發模式(比如都是java,比如都是finagle)。這就形成悖論了。

微服務架構的責任困境

過去的開發模式是這樣的。我負責一個服務,它從DB往上都是我的。產品經理的需求,我全部負責。中間會有少量的呼叫外部介面進行支付,郵件群發之類的事情。但是這些呼叫一般處於流程的末端,不參與主要的業務邏輯並且介面清晰。

微服務架構的責任困境

這個年代的測試非常清楚,mock掉外部依賴(支付,簡訊閘道器,ftp介面),啟動真實的資料庫,從使用者的介面測試我的服務。我直接向使用者負責。這種工作模式,我至今認為是效率最高的方式。

微服務架構的責任困境

現在的微服務的模式是疊羅漢式的。我負責了B,上面有A,下面還有C。A依賴B和C才能跑起來,B依賴A和C才能和客戶端完整互動。

在這種拆分下,我們強調了每個團隊的自主性。而且大家共同承擔了最終的業務敏捷性和穩定性的要求。對於生產環境,確實是這樣的。如果C掛了,C會立即去修,因為影響了生產環境。如果B掛了,B也會立即去修。

微服務架構的責任困境

但是把這個問題改成線下和線下分開呢?如果是在服務B的開發環境裡,服務C出問題了,有一個設定沒有配置對。或者這個設定改了,B沒有更新程式碼。你認為C會積極地幫B解決問題麼?你認為B會進入到C的程式碼目錄下,看他們打的日誌麼,然後自己就知道怎麼修復問題了?即便可以解決,B也會遇到巨大的相應延遲的問題。C可能在開會,C可能在忙別的事情。

那麼解法是什麼?線下環境直接利用生產環境的部署指令碼?一套指令碼,可隨時複製環境?前面已經討論過這些自動化部署指令碼,以及docker等模式的技術缺陷了。事實上,部署兩套一模一樣的環境是相當困難的。而且引入複雜的部署工具,以及專職的團隊還有一個更加有趣的現象,它把兩方的關係,變成了三方的互動

微服務架構的責任困境

這三方的關係是互相不信任的,因為程式碼不是自己寫的。服務消費方遇到了困難,去找服務整合方。整合方會認為可能是提供方的程式碼有問題。於是把提供方拉進來定位問題。提供方會覺得我的服務線上上是好好的, 為什麼線上下就不行了呢?是不是你部署得有問題,gcc版本是不是不對。這種責任的鏈式傳導很快就會讓環境的使用方覺得,能用就用,不能用我也沒法推動去定位問題。

從根本上來說,這個困境在於模組的owner,只對生產環境負責,不對別人使用的開發環境裡自己的服務負責。花時間幫別人解決問題,提高團隊的整體效率,對於模組的owner來說不是最優解。因為他花費了額外的時間去幫別人完成KPI,而不是專注於領導佈置的下一個任務。

而服務的整合方,既不知道服務是如何消費的,也不知道服務是如何提供的。他不可能有足夠的精力,時間與動機去深入瞭解所有的模組的工作細節。即便有意願把這個整合工作做好,也沒有能力在脫離模組owner的情況下把工作真正做好。

解法一:可複製的環境列入KPI

一種解法是把環境的可複製性變成每個人的KPI。這樣每個模組不僅僅負責一個環境(生產環境)的工作正常。他還要負責保持這種可複製性。如果你的業務模式恰好依賴於這一點,則可以努力推動。比如你要去加拿大運營一套獨立的系統,而能夠一鍵部署一套環境給加拿大的運營使用,則變成了一件有業務收益的事情。

這種做法就是要把全流程的持續整合列為所有人的KPI。如果線上下環境整合失敗,亮紅燈,所有人負責來定位問題。就和生產環境出告警了一樣來對待。如果做不到這一點,所謂可複製的環境就是鏡花水月了。

解法二:生產環境自檢

如果大家都只認同我只需要為生產環境負責,那就把生產環境變得更強大好了。複雜的機器都有“自檢”的功能。我們要做的就是讓生產環境可以跑測試的流量實現自檢,類似於 windows “列印測試頁” 這樣的功能。四色建模裡的 party/place/thing/moment interval,大部分要測的行為都是moment interval。通過把 party 這個主體給換掉,把 place/thing 這兩個維度的程式碼加以改造(比如一些分城市統計邏輯),可以實現 moment interval 的重放。

這種方式的實質是仍然是推動全流程的整合。因為全流程持續整合線上下推行失敗,而退而求其次,選擇在生產環境來做。

解法三:基於介面契約的開發

強化介面契約的作用。通過mock掉所有的外部依賴,單獨測試我自己負責的模組。通過線上tcpdump的方式,方便構造這些mock的請求和響應,減少mock的工作量,以及讓mock真實可信。避免查了半天問題,結果發現是mock介面和線上實際行為不一致,這樣的烏龍問題。

同時進行介面的形式化定義,並且通過 consumer driven contract 的方式把呼叫方的mock,變成提供方的測試。

這種做法就是承認微服務架構的實質是利用組織邊界來強化軟體架構的邊界。既然人為構建了組織牆,與其忽視其存在,還不如對它好好對待。既然我呼叫對方,對方不願意幫我解決環境問題,我也沒有能力獨立把對方的程式碼跑起來。那不如我們就劃界而治吧。大家把介面約定好,我用假的實現來替代你的真實實現來做開發。這種做法和我們呼叫國企銀行的介面,雙方聯調的方式並無本質不同。

刻度尺

微服務架構的責任困境

在中央威權強的場景,書同文的方式是最經濟的方式。無論是統一開發語言,還是統一RPC框架,還是統一服務發現。本質上來自於書同文帶來的收益大於威權推進的成本。

在越鬆散的組織下,越會趨向於面向介面開發。

後記

從統一的自動化指令碼,puppet/chef/saltstack

到 docker + 網路代理

到 線上 tcpdump 線下流量回放,基於介面契約的開發

技術難度逐步升級。通過技術上的技巧(比如 docker 利用了大家都是基於統一的作業系統介面)確實可以解決一些問題。但是技術再怎麼升級,也無法解決所有問題。畢竟,所謂環境問題,所謂測試問題,所謂我的程式碼跑不起來的問題,都是人與人之間如何整體地高效協作的問題。技術的盡頭,是政治。

相關文章