陳皓:我做系統架構的一些原則
工作 20 多年了,這 20 來年看到了很多公司系統架構,也看到了很多問題,在跟這些公司進行交流和討論的時候,包括進行實施和方案比較的時候,都有很多各種方案的比較和妥協,因為相關的經歷越來越多,所以,逐漸形成了自己的邏輯和方法論。今天,想寫下這篇文章,把我的這些個人的經驗和想法總結下來,希望能夠讓更多的人可以參考和借鑒,并能夠做出更好的架構來。另外,我的這些思維方式和原則都針對于現有市面上眾多不合理的架構和方案,所以,也算是一種“糾正”……(注意,這篇文章所說的這些架構上的原則,一般適用于相對比較復雜的業務,如果只是一些簡單和訪問量不大的應用,那么你可能會得出相反的結論)
目錄
- 原則一:關注于真正的收益而不是技術本身
- 原則二:以應用服務和 API 為視角,而不是以資源和技術為視角
- 原則三:選擇最主流和成熟的技術
- 原則四:完備性會比性能更重要
- 原則五:制定并遵循符從標準、規范和最佳實踐
- 原則六:重視架構擴展性和可運維性
- 原則七:對控制邏輯進行全面收口
- 原則八:不要遷就老舊系統的技術債務
- 原則九:不要依賴自己的經驗,要依賴于數據和學習
- 原則十:千萬要小心 X – Y 問題,要追問最初的原因
- 原則十一:激進勝于保守,創新與實用并不沖突
原則一:關注于真正的收益而不是技術本身
對于軟件架構來說,我覺得第一重要的是架構的收益,如果不說收益,只是為了技術而技術,而沒有任何意義。對于技術收益來說,我覺得下面這幾個收益是非常重要的:
- 是否可以降低技術門檻加快整個團隊的開發流程。能夠加快整個團隊的工程流程,快速發布,是軟件工程一直在解決的問題,所以,系統架構需要能夠進行并行開發,并行上線和并行運維,而不會讓某個團隊成為瓶頸點。(注:就算拖累團隊的原因是組織構架,也不妨礙我們做出并行的系統架構設計)
- 是否可以讓整個系統可以運行的更穩定。要讓整個系統可以運行的更為的穩定,提升整個系統的 SLA,就需要對有計劃和無計劃的停機做相應的解決方案(參看《關于高可用的架構》)
- 是否可以通過簡化和自動化降低成本。最高優化的成本是人力成本,人的成本除了慢和貴,還有經常不斷的 human error。如果不能降低人力成本,反而需要更多的人,那么這個架構設計一定是失敗的。除此之外,是時間成本,資金成本。
如果一個系統架構不能在上面三個事上起到作用,那就沒有意義了。
原則二:以應用服務和 API 為視角,而不是以資源和技術為視角
國內很多公司都會有很多分工,基本上都會分成運維和開發,運維又會分成基礎運維和應用運維,開發則會分成基礎核心開發和業務開發。不同的分工會導致完全不同的視角和出發點。比如,基礎運維和開發的同學更多的只是關注資源的利用率和性能,而應用運維和業務開發則更多關注的是應用和服務上的東西。這兩者本來相關無事,但是因為分布式架構的演進,導致有一些系統已經說不清楚是基礎層的還是應用層的了,比如像服務治理上的東西,里面即有底層基礎技術,也需要業務的同學來配合,包括 k8s 也樣,里面即有底層的如網絡這樣的技術,也有需要業務配合的 readniess 和 liveness 這樣的健康檢查,以及業務應用需要 configMap 等等 ……
這些東西都讓我感覺到所謂 DevOps,其實就是因為很多技術和組件已經分不清是 Dev 還是 Ops 的了,所以,需要合并 Dev 和 Ops。而且,整個組織和架構的優化,已經不能通過調優單一分工或是單一組件能夠有很大提升的了。其需要有一種自頂向下的,整體規劃,統一設計的方式,才能做到整體的提升(可以試想一下城市交通的優化,當城市規模到一定程度的時候,整體的性能你是無法通過優化幾條路或是幾條街區來完成的,你需要對整個城市做整體的功能體的規劃才可能達到整體效率的提升)。而為了做到整體的提升,需要所有的人都要有一個統一的視角和目標,這幾年來,我覺得這個目標就是——要站在服務和對外 API 的視角來看問題,而不是技術和底層的角度。
原則三:選擇最主流和成熟的技術
技術選型是一件很重要的事,技術一旦選錯,那會導致整個架構需要做調整,而對架構的調整重來都不是一件簡單的事,我在過去幾年內,當系統越來越復雜的時候,用戶把他們的 PHP,Python, .NET,或 Node.js 的架構完全都遷移到 Java + Go 的架構上來的案例不斷的發生。這個過程還是非常痛苦的,但是你沒有辦法,當你的系統越來越復雜,越來越大時,你就再也不能在一些玩具技術上玩了,你需要的更為工業化的技術。
- 盡可能的使用更為成熟更為工業化的技術棧,而不是自己熟悉的技術棧。所謂工業化的技術棧,你可以看看大多數公司使用的技術棧,比如:互聯網,金融,電信……等等 ,大公司會有更多的技術投入,也需要更大規模的生產,所以,他們使用的技術通常來說都是比較工業化的。在技術選型上,千萬不要被——“你看某個公司也在用這個技術”,或是一些在論壇上看到的一些程序員吐槽技術的觀點來決定自己的技術,還是看看主流大多數公司實際在用的技術棧,會更靠譜一些。
- 選擇全球流行的技術,而不是中國流行的技術。技術這個東西一定是一個全球化的東西,不是一個局域化的事。所以,一定要選國際化的會更好。另外,千萬不要被某些公司的“特別案例”騙過去了,那怕這個案例很性感,關鍵還是要看解決問題的思路和采用的技術是否具有普世性。只有普世性的技術有更強的生命力。
- 盡可能的使用紅利大的主流技術,而不要自己發明輪子,更不要魔改。我見過好些個公司魔改開源軟件,比如有個公司同魔改 mesos,最后改著改著發現自己發明另一個 kubernetes。我還見過很多公司或技術團隊喜歡自己發明自己的專用輪子,最后都會被主流開源軟件所取代。完全沒有必要。不重新發明輪子,不魔改,不是因為自己技術不能,而是因為,這個世界早已不是自己干所有事的年代了,這個時代是要想盡方法跟整個產業,整個技術社區融合和合作,這樣才會有最大的收益。那些試圖因為某個特例需要自成一套的玩法,短期沒問題,但長期來說,我都不看好。
- 絕大多數情況下,如無非常特殊要求,選 Java 基本是不會錯的。一方面,這是因為 Java 的業務開發的生產力是非常好的,而且有 Spring 框架保障,代碼很難寫爛,另外,Java 的社區太成熟了,你需要的各種架構和技術都可以很容易獲得,技術紅利實在是太大。這種運行在 JVM 上的語言有太多太多的好處了。在 Java 的技術棧上,你的架構風險和架構的成本(無論是人力成本,時間成本和資金成本)從長期來說都是最優的
在我見過的公司中,好些公司的架構都被技術負責人個人的喜好、擅長和個人經驗給綁架了,完全不是從一個客觀的角度來進行技術選型。其實,從 0 到 1 的階段,你用什么樣的技術都行,如果你做一個簡單的應用,沒有事務處理沒有復雜的交易流程,比如一些論壇、社交之類的應用,你用任何語言都行。但是如果有一天你的系統變復雜了,需要處理交易了,量也上來了,從 1 到 10,甚至從 10 到 100,你的開發團隊也變大了,需要構建的系統越來越大,你可能會發現你只有一個選擇,就是 Java。想想京東從 .NET 到 Java,淘寶從 PHP 到 Java……
原則四:完備性會比性能更重要
我發現好些公司的架構師做架構的時候,首要考慮的是架構的性能是否能夠撐得住多大多大的流量,而不是考慮系統的完備性和擴展性。所以,我已經多次見過這樣的案例了,一開始直接使用 MongoDB 這樣的非關系型數據庫,或是把數據直接放在 Redis 里,而直接放棄關系型數據庫的數據完備性的模型,而在后來需要在數據上進行關系查詢的時候,發現 NoSQL 的數據庫在 Join 上都表現的太差,然后就開始各種飛線,為了不做 Join 就開始冗余數據,然而自己又維護不好冗余數據后帶來的數據一致性的問題,導致數據上的各種錯亂丟失。
所以,我給如下的一些如下的架構原則:
- 使用最科學嚴謹的技術模型為主,并以不嚴謹的模型作為補充。對于上面那個案例來說,就是——永遠使用完備支持 ACID 的關系型數據庫,然后用 NoSQL 作補充,而不是完全放棄關系型數據庫。這里的原則就是所謂的“先緊后松”,一開始緊了,你可以慢慢松,但是開始松了,以后你想緊再也緊不過來了。
- 性能上的東西,總是有很多解的。我這么多年的經歷告訴我,性能上的事,總是有解的,手段也是最多的,這個比起架構的完備性和擴展性來說真的不必太過擔心。
為了追求所謂的性能,把整個系統的完備性丟失掉,相當地得不償失。
原則五:制定并遵循符從標準、規范和最佳實踐
這個原則是非常重要的,因為只有符從了標準,你的架構才能夠有更好的擴展性。比如:我經常性的見到很多公司的系統既沒有符從業界標準,也沒有形成自己公司的標準,感覺就像一群烏合之眾一樣。最典型的例子就是 HTTP 調用的狀態返回碼。業內給你的標準是 200 表示成功,3xx 跳轉,4xx 表示調用端出錯,5xx 表示服務端出錯,我實在是不明白為什么無論成功和失敗大家都喜歡返回 200,然后在 body 里指出是否 error(前兩年我在微信公眾號里看到一個有一定名氣的互聯網老兵推薦使用無論正確還是出錯都返回 200 的做法,我在后臺再三確認后,我發現這樣的架構師真是害人不淺)。這樣做最大的問題是——監控系統將在一種低效的狀態下工作。監控系統需要把所有的網絡請求包打開后才知道是否是錯誤,而且完全不知道是調用端出錯還是服務端出錯,于是一些像重試或熔斷這樣的控制系統完全不知道怎么搞(如果是 4xx 錯,那么重試或熔斷是沒有意義的,只有 5xx 才有意義)。有時候,我會有種越活越退步的感覺,錯誤碼設計這種最基本最基礎的東西為什么會沒有?并且一個公司會任由著大家亂來?這些基礎技能怎么就這樣丟掉了?
還有,我還見過一些公司,他們整個組織沒有一個統一的用戶 ID 的設計,各個系統之間同步用戶的數據是通過用戶的身份證 ID,是的,就是現實世界的身份證 ID,包括在網關上設置的用戶白名單居然也是用身份證 ID。我對這個公司的內的用戶隱私管理有很大的擔憂。一個企業,一個組織,如果沒有標準和規范,也就會有抽象,這一定是要出各種亂子的。
下面,我羅列一些你需要注意的標準和規范(包括但不限于):
- 服務間調用的協議標準和規范。這其中包括 Restful API 路徑, HTTP 方法、狀態碼、標準頭、自定義頭等,返回數據 JSon Scheme……等。
- 一些命名的標準和規范。這其中包括如:用戶 ID,服務名、標簽名、狀態名、錯誤碼、消息、數據庫……等等
- 日志和監控的規范。這其中包括:日志格式,監控數據,采樣要求,報警……等等
- 配置上的規范。這其中包括:操作系統配置、中間件配置,軟件包……等等
- 中間件使用的規范。數據庫,緩存、消息隊列……等等
- 軟件和開發庫版本統一。整個組織架構內,軟件或開發庫的版本最好每年都升一次級,然后在各團隊內統一。
這里重要說一下兩個事:
- Restful API 的規范。我覺得是非常重要的,這里給兩個我覺得寫得最好的參考:Paypal 和 Microsoft 。Restful API 有一個標準和規范最大的好處就是監視可以很容易地做各種統計分析,控制系統可以很容器的做流量編排和調度。
- 另一個是服務調用鏈追蹤。對于服務調用鏈追蹤來說,基本上都是參考于 Google Dapper 這篇論文,目前有很多的實現,最嚴格的實現是 Zipkin,這也是 Spring Cloud Sleuth 的底層實現。Zipkin 貼近 Google Dapper 論文的好處在于——無狀態,快速地把 Span 發出來,不消耗服務應用側的內存和 CPU。這意味著,監控系統寧可自己死了也不能干擾實際應用。
- 軟件升級。我發現很多公司包括 BAT,他們完全沒有軟件升級的活動,全靠開發人員自發。然而,這種成體系的活動,是永遠不可能靠大的自發形成的。一個公司至少一年要有一次軟件版本升級的 review,然后形成軟件版本的統一和一致,這樣會極太簡化系統架構的復雜度。
原則六:重視架構擴展性和可運維性
在我見過很多架構里,技術人員只考慮當下,但從來不考慮系統的未來擴展性和可運維性。所謂的管生不管養。如果你生下來的孩子胳膊少腿,嚴重畸形,那么未來是很難玩的。因為架構不是和軟件不是寫好就完的,是需要不斷修改不斷維護的,80% 的軟件成本都是在維護上。所以,如何讓你的架構有更好的擴展性,可以更容易地運維,這個是比較重要的。所謂的擴展性,意味著,我可以很容易地加更多的功能,或是加入更多的系統,而所謂可運維,就是說我可以對線上的系統做任意的變更。擴展性要求的是有標準規范且不耦合的業務架構,可運維性要求的則是可控的能力,也就是一組各式各樣的控制系統。
- 通過服務編排架構來降低服務間的耦合。比如:通過一個業務流程的專用服務,或是像 Workflow,Event Driven Architecture , Broker,Gateway,Service Discovery 等這類的的中間件來降低服務間的依賴關系。
- 通過服務發現或服務網關來降低服務依賴所帶來的運維復雜度。服務發現可以很好的降低相關依賴服務的運維復雜度,讓你可以很輕松的上線或下線服務,或是進行服務伸縮。
- 一定要使用各種軟件設計的原則。比如:像 SOLID 這樣的原則,IoC/DIP,SOA 或 Spring Cloud 等架構的最佳實踐(參看《SteveY 對 Amazon 和 Google 平臺的吐槽》中的 Service Interface 的那幾條軍規),分布式系統架構的相關實踐,或微軟件的 ……等等
原則七:對控制邏輯進行全面收口
所有的程序都會有兩種邏輯,一種是業務邏輯,一種是控制邏輯,業務邏輯就是完成業務的邏輯,控制邏輯是輔助,比如你用多線程,還是用分布式,是用數據庫還是用文件,如何配置、部署,運維、監控,事務控制,服務發現,彈性伸縮,灰度發布,高并發,等等,等等 ……這些都是控制邏輯,跟業務邏輯沒有一毛錢關系。控制邏輯的技術深度會通常會比業務邏輯要深一些,門檻也會要高一些,所以,最好要專業的程序員來負責控制邏輯的開發,統一規劃統一管理,進行收口。這其中包括:
- 流量收口。包括南北向和東西向的流量的調度,主要通過流量網關,開發框架 SDK 或 Service Mesh 這樣的技術。
- 服務治理收口。包括:服務發現、健康檢查,配置管理、事務、事件、重試、熔斷、限流……主要通過開發框架 SDK – 如:Spring Cloud,或服務網格 Service Mesh 等技術。
- 監控數據收口。包括:日志、指標、調用鏈……主要通過一些標準主流的探針,再加上后臺的數據清洗和數據存儲來完成,最好是使用無侵入式的技術。監控的數據必需統一在一個地方進行關聯,這樣才會產生信息。
- 資源調度有應用部署的收口。包括:計算、網絡和存儲的收口,主要是通過容器化的方案,如 k8s 來完成。
- 中間件的收口。包括:數據庫,消息,緩存,服務發現,網關……等等。這類的收口方式一般要在企業內部統一建立一個共享的云化的中間件資源池。
對此,這里的原則是:
- 你要選擇容易進行業務邏輯和控制邏輯分離的技術。這里,Java 的 JVM+ 字節碼注入 +AOP 式的 Spring 開發框架,會帶給你太多的優勢。
- 你要選擇可以享受“前人種樹,后人乘涼”的有技術紅利的技術。如:有龐大社區而且相互兼容的技術,如:Java, Docker, Ansible,HTTP,Telegraf/Collectd……
- 中間件你要使用可以支持 HA 集群和多租戶的技術。這里基本上所有的主流中間件都會支持 HA 集群方式的。
原則八:不要遷就老舊系統的技術債務
我發現很多公司都很非常大的技術債務,這些債務具體表現如下:
- 使用老舊的技術。比如,使用 HTTP1.0, Java 1.6,Websphere,ESB,基于 socket 的通訊協議,過時的模型……等等
- 不合理的設計。比如,在 gateway 中寫大量的業務邏輯,單體架構,數據和業務邏輯深度耦合,錯誤的系統架構(把緩存當數據庫,用消息隊列同步數據)……等等
- 缺少配套設施。比如,沒有自動化測試,沒有好的軟件文檔,沒有質量好的代碼,沒有標準和規范……等等
來找我尋求技術幫助的人都有各種各樣的問題。我都會對他們苦口婆心地說同樣的一句話——“如果你是來找我 case-by-case 解決問題,我興趣不大,因為,你們千萬不要寄希望能夠很簡單的把一輛夏利車改成一輛法拉利跑車,或是把一棟地基沒打好的歪樓搞正。以前欠下的技術債,都得要還,沒打好的地基要重新打,沒建配套設施都要建。這些基礎設施如果不按照正確科學的方式建立的話,你是不可能有一個好的的系統,我也沒辦法幫你 case-by-case 的解決問題……”,一開始,他們都會對我說,沒問題,我們就是要還債,但是,最后發現要還的債真多,有點承受不了,就開始現原形了。
他們開始為自己的“欠的技術債”找各種合理化的理由——給你解釋各種各樣的歷史原因和不得以而為之的理由。談著談著,讓我有一種感覺——他們希望得到一種什么都不改什么都不付出的方式就可以進步的心態,他們寧可讓新的技術 low 下來遷就于這些技術債,把新的技術濫用地亂七八糟的。有一個公司,他們的系統架構和技術選型基本都搞錯了,使用錯誤的模型構建系統,導致整個系統的性能非常之差,也才幾千萬條數據,但他們想的不是還債,不是把地基和配套設施建好,而且要把樓修的更高,上更多的系統——他們覺得現有的系統挺好,性能問題的原因是他們沒一個大數據平臺,所以要建大數據平臺……
我見過很多很多公司,包括大如 BAT 這樣的公司,都會在原來的技術債上進行更多的建設,然后,技術債越來越大,利息越來越大,最終成為一個高利貸,再也還不了(我在《開發團隊的效率》一文中講過一個 WatchDog 的架構模式,一個系統爛了,不是去改這個系統,而是在旁邊建一個系統來看著它,我很難理解為什么會有這樣的邏輯,也許是為了要解決更多的就業……)
這里有幾個原則和方法我是非常堅持的,分享給大家:
- 與其花大力氣遷就技術債務,不如直接還技術債。是所謂的長痛不如短痛。
- 建設沒有技術債的“新城區”,并通過“防腐層 ”的架構模型,不要讓技術債侵入“新城區”。
原則九:不要依賴自己的經驗,要依賴于數據和學習
有好些人來找我跟我說他們的技術問題,然后希望我能夠給他們一個答案。我說,我需要了解一下你現有系統的情況,也就是需要先做個診斷,我只有得到這些數據后,我才可能明白真正的原因是什么 ,我才可能給你做出一個比較好的技術方案。我個人覺得這是一種對對方負責的方法,因為技術手段太多了,所有的技術手段都有適應的場景,并且有各種 trade-off,所以,只有調研完后才能做出決定。這跟醫生看病是一樣的,確診病因不能靠經驗,還是要靠診斷數據。在科學面前,所有的經驗都是靠不住的……
另外,如果有一天你在做技術決定的時候,開始憑自己以往的經驗,那么你就已經不可能再成長了。人都是不可能通過不斷重復過去而進步的,人的進步從來都是通過學習自己不知道的東西。所以,千萬不要依賴于自己的經驗做決定。做任何決定之前,最好花上一點時間,上網查一下相關的資料,技術博客,文章,論文等 ,同時,也看看各各公司,或是各個開源軟件他們是怎么做的?然后,比較多種方案的 Pros/Cons,最終形成自己的決定,這樣,才可能做出一個更好的決定。
原則十:千萬要小心 X – Y 問題,要追問最初的原因
對于 X-Y 問題,也就是說,用戶為了解決 X 問題,他覺得用 Y 可以解,于是問我 Y 怎么搞,結果搞到最后,發現原來要解決的 X 問題,這個時候最好的解決方案不是 Y,而是 Z。 這種 X-Y 問題真是相當之多,見的太多太多了。所以,每次用戶來找我的時候,我都要不斷地追問什么是 X 問題。
比如,好些用戶都會來問我他們要一個大數據流式處理,結果追問具體要解決什么樣的問題時,才發現他們的問題是因為服務中有大量的狀態,需要把相同用戶的數據請求放在同一個服務上處理,而且設計上導致一個慢請拖慢整個應用服務。最終就是做一下性能調優就好了,根本沒有必要上什么大數據的流式處理。
我很喜歡追問為什么 ,這種追問,會讓客戶也跟著來一起重新思考。比如,有個客戶來找我評估的一個技術架構的決定,從理論上來說,好像這個架構在用戶的這個場景下非常不錯。但是,這個場景和這個架構是我職業生涯從來沒有見過的。于是,我開始追問這個為什么會是這么一個場景?當我追問的時候,我發現用戶都感到這個場景的各種不合理。最后引起了大家非常深刻的研論,最終用戶把那個場景修正后,而架構就突然就變成了一個常見且成熟的的模型……
原則十一:激進勝于保守,創新與實用并不沖突
我對技術的態度是比較激進的,但是,所謂的激進并不是瞎搞,也不是見新技術就上,而是積極擁抱會改變未來的新技術,如:Docker/Go,我就非常快地跟進,但是像區塊鏈或是 Rust 這樣的,我就不是很積極。因為,其并沒有命中我認為的技術趨勢的幾個特征(參看《Go,Docker 和新技術 》)。當然,我也不是不喜歡的就不學了,我對區塊鏈和 Rust 我一樣學習,我也知道這些技術的優勢,但我不會大規模使用它們。另外,我也尊重保守的決定,這里面沒有對和錯。但是,我個人覺得對技術激進的態度比起保守來說有太多的好處了。一方面來說,對于用戶來說,很大程度上來說,新技術通常都表面有很好的競爭力,而且我見太多這樣成功的公司都在積極擁抱新的技術的,而保守的通常來說都越來越不好。
有一些人會跟我說,我們是實用主義,我們不需要創新,能解決當下的問題就好,所以,我們不需要新技術,更有的技術用好就行了。這類的公司,他們的技術設計第一天就在負債,雖然可以解決當下問題,但是馬上就會出現新的問題,然后他們會疲于解決各種問題。最后呢,最后還是會走到新的技術上。
這里的邏輯很簡單 —— 進步永遠來自于探索,探索是要付出代價的,但是收益更大。對我而言,不敢冒險才是最大的冒險,不敢犯錯才是最大的錯誤,害怕失去會讓你失去的更多……