微服務的災難

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務的災難,雖然都是故事,但也都是真實。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"通用語言的災難","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在架構師們很喜歡的 Domain Driven Design,即 DDD 中,第一課就是教導團隊形成自己獨有的通用語言(Ubiquitous Language),作爲業務概念沉澱下來。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲非英語母語的國家,我們在日常交流中使用的是中文,在公司業務戰略描述上使用的是中文,在高層進行任務拆分的時候使用的是中文,在領導安排工作的時候使用的是中文。唯獨到了具體實現,即代碼這一環節便變成了英文。當然這裏我們不考慮有些公司會有漢語拼音這種尷尬的情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩種語言天生便有難以填平的鴻溝,在業務人員編寫代碼時,從中文到英文的轉換,往往丟失一部分業務信息,產生一部分信息噪音,或者發生概念上的偏移。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"英文語系的人對業務進行建模時,與業務方(領域專家)交流時,產生的概念和反饋可以直接落實到代碼上,他們所使用的詞彙不會發生變化。而其它語系的人就會在編寫代碼的時候發生概念偏移,比如我司是做打車業務,快車在不同的系統中會存在不同的翻譯,有人稱之爲 fastcar,有人稱之爲 quickcar,有人甚至就直接是 kuaiche。甚至同一個系統中,對於同一個概念也會存在不同形式的自創翻譯。即使以文檔的形式記錄了業務的標準翻譯,但顯然以國內業務疊代的速度,這種詞彙上的統一是做不到的。即使在一個只有 7~8 個人的組中都做不到。並不是所有人的英文都可以達到可用的程度,有些代碼中的詞彙可能根本就是詞不達意,是某些搜索引擎中給出的直譯結果,與真實的含義相差十萬八千里。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的代碼會給後來人帶來理解上的困惑。一旦在同一個系統中,針對同一個業務概念存在三種以上的不同詞彙,就需要閱讀者在這些“錯誤”的詞彙上不停地進行上下文切換,以正確地理解錯誤詞彙的涵義。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可能有些人會提出反駁意見,碰上這種情況我們只要對代碼進行重構就可以了,並不需要被這種弱智的事情折磨啊。重構雖好,在很多情況下,詞彙的重構是不可能的。打個比方,上面提到的 fastcar 出現在我們系統提供給別人所用的 api 的關鍵字段中,quickcar 出現在我們內部數據庫的字段名中,kuaiche 出現在異步發送的消息中。這種時候修改任何一個單詞,對於我們來說都是不可能的事情。api 和事件中的字段名是我們對於外部系統的承諾,這種承諾也是編程契約中的一部分,不能隨便修改。即使我們想要修改,在當今大多數互聯網公司的架構下,根本就沒法知道到底是誰在使用你的哪一個字段。也就是說,我們沒有辦法獲得粒度細到“字段”級別的外部使用信息,所以我們沒有辦法對契約本身進行重構。如果未來的微服務管理能對服務間的依賴進行標準化,並且能夠對服務之間字段的依賴進行顯式管理,那麼契約就是可以進行變更的了(就像單模塊的重構那樣),不過這也就是個設想,顯然不太可能。而數據庫中的字段雖然有重命名方法,並且在 《Refactoring Databases》這本書中也給出了各種數據庫重構的完善方案。但同樣的,上了體量的互聯網公司,想要動動數據庫結構,是比登天還難的(等五年後應該好一些)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以當你接手到這樣的系統時,讀代碼的時候肯定是會罵孃的,但是讀完之後也確實沒有什麼辦法。只要你負責維護,就持續地接受這種痛苦吧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通用語言的問題不只是單模塊中存在,跨模塊時也存在。在微服務的架構下,很多需求是必然會跨越模塊的。別說不可能,那些鼓吹中臺的公司跨模塊的需求更普遍。一個需求改 20 個模塊都不奇怪。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊間負責人探討新功能的實現時,混亂的命名和詞彙也很可能讓兩邊的溝通變得驢頭不對馬嘴。在服務之間是接力棒式運作,沒有中心服務時,這種情況特別普遍。相信你也遇得到。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遺憾的是,目前推崇的微服務架構是沒有辦法解決這樣的問題的。在肉眼可見的將來,程序員依然會因爲概念產生的歧義而不斷地受苦。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些苦痛最終都會體現到業務開發迭代的速度上。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"技術棧的災難","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務的佈道師們特別喜歡鼓吹一個觀點:拆分微服務之後,我們可以隨意地對小模塊進行重構,選擇最合適的技術棧,並且如果寫失敗了隨時對這個模塊拿其它語言進行重寫。這一點被大多數佈道師當作微服務的重點優勢。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是佈道師們有意地把這樣做所帶來的問題忽略了,或者更惡意的是,他們明知道有問題,但是不說?啊哈哈。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個公司業務上有多種語言的話,理論上可以吸引到各種“語言的人才”。這確實不假,並且可以提供給各種語言大佬一個互相掐架的優秀競技場,只要幹掉對手(其它語言的大佬)了,我就可以擴張團隊,讓團隊把所有其它語言的模塊用我們擅長的語言重寫一遍,善哉善哉。小夥子們一年的事情就都安排上了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是顯然這種說辭是有問題的。在現行的微服務架構下,除了業務本身的研發人力投入之外,在業務之外的支持系統的研發工作也有很大的工作量,比如典型的,服務發現,熔斷,優雅重啓,存儲系統 client,消息隊列 client,緩存 client,配置系統 client 等等。。各種周邊系統版本依次疊代下來,那可能也是幾百上千人一兩年的工作。爲什麼會帶來這麼多的工作量?其中很大一部分就是因爲語言和技術棧混亂造成的。比如一個公司的技術棧能夠統一到 java 的話,那沒什麼說的,大家都用 Spring Cloud 全家桶或者 Dubbo 全家桶就可以了。但是你們既有 java 又有 Go 又有 PHP 又有 C++ 又有 NodeJS 又有 Rust,這樣顯然就很難在衆多神仙中達成一致。比如你想要選用 java 的 Spring Cloud 生態,但是這裏面的服務發現或者配置系統並沒有打算對其它語言進行支持,即使支持可能也支持地不全面。一旦支持不全面,公司內的輪子黨們一定會跳出來,強行給你找出幾十個缺點,一杆子打回去,最終得到一定要自己造這些輪子的結論。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好傢伙,五種語言八種框架,一個服務發現的 client 輪子造五遍都能算少的了。目前開源界的趨勢是將那些和業務無關的非功能性需求從模塊中剝離出來,比如 service mesh 就是很好的嘗試,只不過現階段用過的都說坑。說好的那都是不懷好意,拉人入坑。對於研發人員來說,一個輪子造五遍真的沒什麼意思,可能也就是熟悉了五種語言的語法,並且寫出了五種風格各異的 bug。只不過滿足了部分中層管理老闆的人員擴張野心。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"語言和框架太多,對於公司來說顯然是災難。比如常見的公司組織架構調整,業務技術部門進行重組,不同部門的系統一般會進行暴力交接。這裏說的“暴力”的意思是,不管你能不能接得下來,反正我是給你了。之後的維護也別來找我,甚至連簡單的問答可能原部門都不一定願意做。雖然公司對程序員的要求是可以隨意地在不同語言技術棧之間切換,但程序員一般都有自己執著的美學偏好。讓 java 程序員寫 Go,往往是會翻車的。大多數 java 程序員在語言內的生態足夠好,能滿足幾乎所有需求時,沒有任何意願去學習一門新語言。這是天然的抗拒心理,可以理解。我們這裏已經有無數的案例表明,java 程序員轉 Go,在寫不滿三個月的時間內就會離職 2333。就算不說 java,國內的 php 專家們也是不願意寫 Go 的,那些 PHP 大佬們哪怕在 swoole 之類的框架中重新實現一套 goroutine,也不願意直接去寫更原生的 Go 語言,因爲用別人的東西體現不出輪子哥的價值啊。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於一個公司來說,不應該聽信那些微服務佈道師的胡言,任由公司內的技術棧隨意分裂。最終在公司調整或者變化的時刻才發現積重難返。那些幾千甚至上萬研發的“大公司”,大多都沒有做到技術棧的統一,哪怕是在線業務技術棧也是如此。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"拆分與收斂的災難","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在之前寫事故驅動開發的時候,提到過,在企業中的項目進行開發時,只要是自己方便,一個人可以用拆分和收斂同時作爲自己的標準。所以大家都是雙標狗。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前業界的微服務方法論一般也沒有固定的套路,比如在 《Building Microservice》 一書中,作者也講到了服務之間協作的時候,可以選擇編排(orchestration)和協同(choreography)這兩種方式來對服務進行架構。所以在拆分階段,就沒有什麼硬性的標準了,每個公司可能風格都有差別,並且都可以闡述出自己的條條以支持自己的架構是“正確”的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,這件事情沒有絕對正確的解法。無論哪種拆分方式,都會遇到業務邊界的問題。在大企業中,頂着“架構師”頭銜的這些架構師們根本就不會管任何實現上的細節。相對較大的業務需求,一般也是一線的 RD 商量怎麼進行實現上的拆分。想要達到合適的職責劃分,需要多個合作方的所有人都靠譜纔行。這個要求實在是有點強人所難。比如在目前的公司內進行了三年多的開發,就和各種類型的人都打過交道。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兢兢業業的人比較多,但也不乏一些完全不負責任的人。有的素質奇差,只會逢迎甩鍋,自己模塊的職責都搞不清楚。本來自己該做的兜底不做,讓所有下游系統給他擦屁股。從應聘要求上來講,程序員應該是一個注重邏輯能力,編碼能力的職業。然而你總髮現有些人是沒有辦法講道理的(可能是早期的一些能力一般的員工?),在與這些人討論技術實現時,會陷入無窮無盡的無意義循環。而他能說(逼)服(迫)所有其它人就範的法寶,就是在兩個小時的會議中不斷地復讀自己的觀點,而完全不聽取任何別人的觀點。一旦這樣的人在你的某個系統邊界上待着,那你所面臨的也是持續的痛苦。並且不斷地在自己的系統中進行妥協,做那些職責上跟你的系統完全沒什麼關係的東西。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除去人的問題,業務部門的大多數一線領導是需要有業務上的業績的。這種業績怎麼來?一般都是攬各種各樣的活兒,能攬多少就攬多少。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從設計原則上來講,邏輯上相同或者類似的代碼應該放在一個地方來實現。這個稍微學過一點 SOLID 中的 SRP 原則就應該知道。這樣可以避免邏輯本身過於分散,好處是:“一個類(模塊)只會因爲一個理由而發生變化”,其實就是相同的需求,儘量能夠控制在單模塊內完成。當然了這是一種理想狀態,確實有些情況下達不到這種完美的平衡狀態。微服務場景下這種業務邊界往往劃分都非常糟糕。即使 Domain Driven Design 的觀點講述了再多的上下文劃分技巧,你在實際工作中會發現沒有多少人把這些思想、原則當回事,一線 leader 在乎的就是攬活兒而已。他們在劃分模塊的職責時只考慮這麼幾點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這件事情有沒有業務收益,我能不能摻一腳如果沒有收益,這件事能不能甩給別人,我就不去做了把業務上的影響全刨去,纔會考慮架構上的事情。有些大佬會講,系統是演化出來,而不是設計出來的,而這些“演化論”的大佬也是不參與一線開發的。你再看看實際的情況,只靠演化,可能演化出合理的系統麼?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可能的,對人的要求實在太高。而且是對所有開發人員要求都高。大多數企業的業務系統,都缺乏較爲頂層的設計,有人管這個叫“戰略設計”。相對複雜的業務邏輯,在企業的系統中根本沒法合理解耦,很多時候實現一套業務流程的代碼會隨意地散落在多個模塊,在你把所有模塊的代碼都看過之前,你是沒法確定哪部分邏輯在哪個模塊裏的。可以稱之爲薛定諤的業務邏輯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣在模塊發生負責人離職或者工作交接時,所有 RD 都會進入非常痛苦的階段,我只看自己的模塊,根本沒法理清全局的業務邏輯!對於產品來說也一樣,所有邏輯的分佈都具有不確定性,在哪裏控制我需要去問研發,而研發還要再問其他的研發,其他的研發如果是剛接手,又要去看大量的代碼才能確定到底是怎麼回事。如果代碼寫的爛(對於大多數業務系統來說,如果可以去掉),那可能連看都看不懂。就隨便胡謅應付過去好了。現實就是如此荒誕。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在所有服務都在單體的時代,我們可以在合適的時間,參考《重構》書裏的觀點對我們的模塊進行重構。重構對系統本身的要求其實也不多,只要測試覆蓋率足夠,然後是強類型語言,大多數 IDE 對重構支持都很不錯了。但演化到微服務的時代,原來很簡單的重構就沒那麼簡單了。在 http://xargin.com/disaster-of-microservice-ul/ 中我們提到,開放的 API、消息、數據庫中的字段名根本沒有辦法進行重構,爲什麼沒有辦法,因爲我們的模塊都被切開了,原本在代碼中的強聯繫變成了分佈式系統中的弱聯繫,薛定諤的聯繫。如果我們想要實現和單體服務一樣的重構功能要怎麼辦?根本實現不了。你至少需要下面這些設施支持才能完成這樣的偉業:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有其它模塊對你都有集成測試,並且有統一的 API 平臺管理所有你們之間的“聯繫”。有全局所有模塊的“同時重構”工具上線時,能針對舊版和新版的流量自動進行識別,防止新流量訪問到舊系統這個顯然不可能啊,目前業界提出的 API 版本管理,也只是緩解了這種情況,新功能我如果在新版 API 實現,把舊版 API deprecated 掉,這樣就可以逼迫用戶放棄對我原來版本的依賴,平滑遷移到新版 API 上來。但顯然加上版本,也並沒有從本質上來解決問題,API 的用戶在沒有迭代需求的前提下,因爲依賴方進行了修改就不得不進行修改,這是額外的工作量。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你也看到了,拆分給我們帶來的並不全是好事,當前中大規模公司的開發日常流程,可能最終還是會把系統整體引向一片混沌。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"難以治理的依賴地獄","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務模式下,我們的系統中往往需要集成進各種各樣的 SDK,這些 SDK 部分來自於非功能性的業務需求,例如 bool 表達式解析,http router,日期時間解析;一部分來自於對公司內基礎設施的綁定,如 MQ Client,配置下發 Client,其它服務的調用 SDK 等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般的觀點會認爲公司內的 SDK 是較爲可靠的,而開源庫的穩定性不可控,所以人們在升級公司內部庫時往往較爲激進,開源庫版本升級較爲保守。具體到 Go 語言,公司內的庫,我們可能會直接指定依賴的版本爲 master(glide 中每次構建會使用 master 分支的代碼)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際上真的如此麼?業界有個名詞叫 dependency hell,指的是軟件系統因依賴過多,或依賴無法滿足時會導致軟件無法運行。導致依賴地獄的問題有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依賴過多一個軟件包可能依賴於衆多的庫,因此安裝一個軟件包的同時要安裝幾個甚至幾十個庫包。多重依賴指從所需軟件包到最底層軟件包之間的層級數過多。這會導致依賴性解析過於複雜,並且容易產生依賴衝突和環形依賴。依賴衝突即兩個軟件包無法共存的情況。除兩個軟件包包含內容直接衝突外,也可能因爲其依賴的低層軟件包互相沖突。因此,兩個看似毫無關聯的軟件包也可能因爲依賴性衝突而無法安裝。依賴循環即依賴性關係形成一個閉合環路,最終導致:在安裝A軟件包之前,必須要安裝A、B、C、D軟件包,然而這是不可能的。我們編寫的服務也屬於軟件系統的範疇,所以也難以擺脫依賴地獄的問題。在微服務場景下,因爲本文開頭所述的原因,我們必然會依賴一大堆外部 SDK。對於開發者來說,實際上真正有選擇權力的就只有我可以使用什麼樣的開源庫。公司內的 SDK 是沒有自己造輪子的價值的。畢竟自己造的司內 SDK 也沒有人會幫你修 bug,原生 SDK 至少有單獨的團隊維護。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在開發 lib 時,比較好的做法是儘量引入少的依賴,以避免上面提到的問題 1。實際上沒有幾個提供 SDK 的團隊能做得到,想想當初 javascript 圈子的 leftpad 事件吧,即使是一行代碼的庫,被人刪除就引起了無數大公司系統無法 build 的悲劇。對於目前 Go 編寫的系統,實際上存在同樣的風險,我們的依賴大多來自於 github,如果你們沒有使用 vendor 方案把依賴緩存到自己的系統中,別人刪庫了你有轍麼?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般 star 數比較高的開源庫作者還是比較有節操的,刪庫的人畢竟是少,所以我們先假裝這個問題不存在。公司內的實際開發過程中,我們遇到的依賴地獄大多體現在依賴衝突上,這個比較好理解,比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"A --> B --> D.v1A --> C --> D.v2A 模塊依賴 B 和 C,而 B 和 C 分別依賴 D 的不同版本,如果 D.v1 和 D.v2 恰好進行了 API 不兼容的更新,且都是在 github.com/xxx/D 路徑下,通過 tag 來區分版本。那麼就會對我們造成很大的麻煩,B 和 C 如果又恰好是公司內的不同部門的不同團隊,要求他們因爲這種原因進行兼容性更新就像是去求大爺一樣難。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"印象中之前 Go 社區是沒有辦法解決這種問題的,爲了解決這個問題,我司還專門對 glide 進行了定製,以在依賴衝突的時候提醒用戶,阻止構建,防止之後出現問題。Go 的社區在這方面也做得確實不太好,gomod 我還沒有試用,不清楚是否對依賴衝突有優雅的解決方案。之前社區裏大多數人對 Go 的依賴管理也一直頗有微詞,希望 gomod 能徹底解決這些問題吧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了語言本身的問題,我發現公司內的 library 研發們,根本沒有任何開源界的節操,版本升級時根本不考慮向前兼容或者向後兼容的問題,並且出現問題的時候也不會做任何提示,連日誌都基本不打印。經常會有配置管理 v1 和配置管理 v2 的 sdk 同時存在模塊中時,發生同一個全局變量初始化兩次,發生衝突,邏輯不能正常運行,結果啓動階段沒有任何 warning,直到執行階段纔出現詭異錯誤,導致用戶在線上埋下定時炸彈的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實在是不知道該說什麼好。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多模塊之間的循環依賴就更不用說了,如果循環依賴出現在單機系統中,至少在 Go 語言中是沒法編譯通過的,而因爲微服務的關係,循環依賴往往會存在那些沒有合理劃分業務邊界的系統當中。據我觀察,出現得還不少。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時候你能重新劃分職責,讓循環依賴消失麼?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然是不行的。程序員在當前的微服務架構下,將持續地被外部的垃圾 SDK 和各種莫名其妙的依賴問題所困。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"嘴上最終一致,實則最終不一致","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在的架構師總喜歡把最終一致掛在嘴上,好像最終一致是解決分佈式場景下數據一致問題的金科玉律。事實上又怎麼樣呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上的這些人嘴裏的最終一致,往往都是最終不一致。在多個系統之間進行數據傳遞時,無非通過 RPC 或者異步消息。RPC 能保證一致性麼?當 B 系統需要 A 系統提供數據時,想要達到一致的效果,那麼在 A call B 時發生失敗,那麼必須讓 A 中的邏輯終止。這樣才能夠使 B 中的狀態或數據與 A 中的完全一致。這樣實際上需要讓 A 和 B 成爲生死共同體,B 掛了,那 A 也得掛。可能麼?在中大型規模的互聯網公司的業務系統中,其下游系統往往有幾十個,因此實際的場景是 A call B -> A call C -> A call D .... -> A call Z。這種情況下,你想讓所有系統中的狀態都一致,那是不可能的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有的架構師又拿出 saga pattern 來說事,我如果有寫數據的邏輯,那麼我自然會有一套回滾邏輯,只要在中間發生錯誤,那麼我就對之前的所有調用執行回滾邏輯即可。然而回滾是需要開發量的。我所有下游系統那都得支持回滾纔行啊,你覺得做得到麼? saga pattern 的異常處理就更扯蛋了:回滾過程中發生失敗的話,那需要人工介入,人肉處理。顯然人肉是處理不過來的,機房網絡抖動實在太正常了,可能一天兩天的就會有一次,每次抖動都造成 bad case,研發人員不用幹別的事情了,都去處理 bad case 好了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然上面這種情況比較極端,一般公司內有靠譜的 MQ 方案的話,會選用 MQ 對這種數據同步的場景進行解耦。之前我做的一些總結也都提到過,只要往 MQ 發一條消息,在字段上儘量滿足下游系統,那麼我就不用挨個兒去調用他們了,可以很好地進行解耦。從設計的角度上來講,這確實是比較好的解耦形式。但是你要考慮,邏輯執行和消息發送這兩步操作並不具備原子性,除非 MQ 支持事務消息,我才能完成兩個操作同時成功或者失敗,何況邏輯執行內部可能還有更多的子操作,這件事情遠沒有打打嘴炮那麼簡單。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也有的公司會將發送失敗的消息進行落盤,比如落進 MySQL 或者寫入到磁盤,在發送失敗之後,由後臺線程在合適的時間進行重發,以讓消息能夠最終發出。一些簡單的場景,這樣確實算是解決了問題。如果下游對於消息本身有順序要求呢?比如訂單的狀態流轉,如果順序錯了,那狀態機最終的狀態都錯亂了。又是一個麻煩的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在當前的開發環境下,想要達到最終一致的效果需要上下游同時進行很多工作,例如上面說的異步消息的場景,上游至少需要做失敗落盤和後臺發送。而下游需要在狀態機的正常狀態流轉之外,處理各種麻煩的亂序問題。這種亂序處理基本和業務是強相關的,並沒有通用方案。即使是同一套狀態機,針對不同的業務場景可能還需要定製不相同的業務邏輯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了網絡抖動,數據不一致的問題可能還會因爲模塊上線導致。有些公司(比如我司)爲了簡單 MQ 的消費邏輯,提供了一套由 MQ 平臺消費,然後通過 http post 來將消息發送給業務系統的邏輯,降低了業務系統的消息消費開發成本(這樣就不用使用 MQ 的 client)了。這種情況下如果模塊發生上線的話,即使在 MQ 平臺側有 post 重試,但在模塊上線時,還是有概率發生消息丟失。如果有一些狀態機流轉強依賴於這些消息,那也會造成一部分 bad case。而且這種 bad case 查起來真是沒什麼意思。之後的數據修復也基本只能靠研發人員自行修復。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種惡劣的場景下,也有一些人想到了一種方法,我在業務模塊中插入多個樁,只要可以每過一段時間觸發狀態的全量更新,那麼我就找一個其它模塊來持續地刷新我係統中的數據狀態。從而達到“最終一致”。只要這些最終一致的數據沒有暴露給用戶,沒人看得見,那就是最終一致。倒確實是個可用的方案。但架構師們在吹牛逼的時候,對於這種噁心的邏輯一定是絕口不提的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大多數公司的架構師嘴裏的最終一致,依靠的都是人肉而非技術。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"合作的災難","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"架構師們常講的設計定律之中最爲重要的是康威定律,康威定律的定義:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Conway's law is an adage named after computer programmer Melvin Conway, who introduced the idea in 1967. It states that. organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的 'are constrained to' 即是該定律的精髓所在。如果一個公司的組織架構已經基本成型了,那麼基本上設計出的系統架構和其人員組織架構必然是一致的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在微服務場景下,團隊會按照其所負責的模塊被依次切開成爲一個 5-10 人小團隊,然後再由更爲頂層的架構少來按照組織架構設計相應的系統。但是這裏面有一個先後關係,是先設計系統,再根據系統來形成對應的團隊。但很多時候也並不一定是如此,因爲某些公司招聘速度過快(笑),可能團隊先形成了,然後纔有系統設計,這時候,系統設計可能甚至會被團隊架構所反作用(大概)。還是比較荒唐的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即使是正常的設計流程,業務需求總是難以預測的。架構師們一般在設計完最初版本的系統架構之後,便會抽身到新的系統中繼續挖坑。新的需求卻在後續的實現過程中漸漸發現無法與最初的架構設計相匹配,具體體現在很難在當前架構上實現,或實現成本過於高昂,單模塊幾人天的事情,在當前架構上需要以月計的工時,這顯然是不可接受的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時候該怎麼辦呢?沒什麼好辦法。一套系統的架構一旦形成了,如果不是發生重大事件(例如迭代龜速導致公司在響應速度上跟不上競爭對手的步調;或者發生輿論事件,導致公司陷入風口浪尖,高層承諾短時間無法兌現),一般系統本身並不會有架構上的變動。一線的開發人員最能體會這時候的痛苦,但是痛苦也沒有什麼卵用,因爲這時候沒有人有動力去推進架構上的變動。試想,在風平浪靜的平日,沒有任何一個一線 RD 能有能力去推動一堆比他們高兩三級的“專家”做事。而一線的 leader 也沒有動力去做這種於自己於自己組完全無益的變動,哪怕明知道現行架構已完全無法滿足業務需求,多一鍋不如少一鍋。Manager 們就更不用說了,多一事不如少一事,能堆人解決的問題就儘量不用技術去解決。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以你也看到了,這種問題是無法解決的。曾經在和同事討論的時候,同事提出,按照這種說法來看的話,小公司的架構可能比大公司還要靠譜?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這當然也不一定,小公司一般開不出優秀人才的價格,所以優秀的架構師基本上是不會去小公司的,這就意味着大多數小公司的架構,肯定更加不靠譜。除非他們能持續發展壯大,公司財務健康,在不進行服務治理沒有辦法繼續做業務的困境時,招入了合適的架構師來做全局把控,完成一次大的整體重構,徹底償還歷史技術棧,纔會慢慢有所好轉。當然這也只是個空想,業務驅動的公司不可能把業務完全停下來支持這種技術上的整體重構,記得阿里的人說在 09 年的時候進行公司的服務化,讓整個公司的業務停了半年?這種項目如果最後效果不好,那負責人肯定是要離職謝罪的。大多數技術老闆也是一定沒有這個魄力讓業務半年沒有進展的,這樣搞不好直接就被 CEO 幹掉了好嗎。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從技術上來講有解決方案的問題,如果把政治也考慮在內,可能就變成了無解的問題。大多數公司內的業務系統所要承受的這個痛苦的過程從公司發展的歷程上來講,是必然的。所以各位技術同學,就不要抱怨業務代碼寫得亂了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"技術人員所能發揮作用的範圍被限制於自己的模塊內,或者那些願意接自己需求的其它支持系統間。除了前面說的組織架構的問題,還需要考慮 KPI 的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前和同事一起得到了一個在大公司內推進事情的靠譜結論,如果一件事情在一個部門內就可以解決,那可以開開心心地推動它解決。如果一件事情需要跨部門,那還需要本部門的大領導出面才能解決,哪怕這事情再小。如果一件事情需要跨兩個部門,那就沒治了,誰出面都不行。這種事情做不了的。而如果一件事情和你要跨的部門 KPI 有衝突,那就更別想了,把部門重組了才能解決,這是 CTO 才能乾的事情。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果想要在大公司得到較好的績效,遵循 KPI 規則是必然的。沒有辦法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3cc601867d89dba36184eaa90b7f6cbf.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章