分布式系統常見問題總結(二)
參考:
微信公眾號:架構師之路
互聯網分層架構的本質
上圖是一個典型的互聯網分層架構:
-
客戶端層:典型調用方是browser或者APP
-
站點應用層:實現核心業務邏輯,從下游獲取數據,對上游返回html或者json
-
數據-緩存層:加速訪問存儲
-
數據-數據庫層:固化數據存儲
如果實施了服務化,這個分層架構圖可能是這樣:
中間多了一個服務層。
同一個層次的內部,例如端上的APP,以及web-server,也都有進行MVC分層:
-
view層:展現
-
control層:邏輯
-
model層:數據
可以看到,每個工程師骨子里,都潛移默化的實施着分層架構。
那么,互聯網分層架構的本質究竟是什么呢?
如果我們仔細思考會發現,不管是跨進程的分層架構,還是進程內的MVC分層,都是一個“數據移動”,然后“被處理”和“被呈現”的過程,歸根結底一句話:互聯網分層架構,是一個數據移動,處理,呈現的過程,其中數據移動是整個過程的核心。
如上圖所示:
數據處理和呈現要CPU計算,CPU是固定不動的:
-
db/service/web-server都部署在固定的集群上
-
端上,不管是browser還是APP,也有固定的CPU處理
數據是移動的:
-
跨進程移動:數據從數據庫和緩存里,轉移到service層,到web-server層,到client層
-
同進程移動:數據從model層,轉移到control層,轉移到view層
數據要移動,所以有兩個東西很重要:
-
數據傳輸的格式
-
數據在各層次的形態
先看數據傳輸的格式,即協議很重要:
-
service與db/cache之間,二進制協議/文本協議是數據傳輸的載體
-
web-server與service之間,RPC的二進制協議是數據傳輸的載體
-
client和web-server之間,http協議是數據傳輸的載體
再看數據在各層次的形態,以用戶數據為例:
-
db層,數據是以“行”為單位存在的row(uid, name, age)
-
cache層,數據是以kv的形式存在的kv(uid -> User)
-
service層,會把row或者kv轉化為對程序友好的User對象
-
web-server層,會把對程序友好的User對象轉化為對http友好的json對象
-
client層:最終端上拿到的是json對象
結論:互聯網分層架構的本質,是數據的移動。
為什么要說這個,這將會引出“分層架構演進”的核心原則與方法:
-
讓上游更高效的獲取與處理數據,復用
-
讓下游能屏蔽數據的獲取細節,封裝
弄清楚這個原則與方法,再加上一些經驗積累,就能回答網友經常在評論中提出的這些問題了:
-
是否需要引入DAO層,什么時機引入
-
是否需要服務化,什么時機服務化
-
是否需要抽取通用中台業務,什么時機抽取
-
是否需要前后端分離,什么時機分離
(網友們的這些提問,其實很難回答。在不了解業務發展階段,業務規模,數據量並發量的情況下,妄下YES或NO的結論,本身就是不負責任的。)
更具體的分層架構演進細節,下一篇和大家細究。
總結
-
互聯網分層架構的本質,是數據的移動
-
互聯網分層架構中,數據的傳輸格式(協議)與數據在各層次的形態很重要
-
互聯網分層架構演進的核心原則與方法:封裝與復用
互聯網分層架構之-DAO與服務化
互聯網分層架構的本質,是數據的移動。
互聯網分層架構演進的核心原則:
-
讓上游更高效的獲取與處理數據,復用
-
讓下游能屏蔽數據的獲取細節,封裝
這些在上一篇《互聯網分層架構的本質》中有詳盡的描述,在實際系統架構演進過程中,如何利用這兩個原則,對系統逐步進行分層抽象呢?咱們先從后端系統開始講解。
本文主要解答兩個問題:
-
后端架構,什么時候進行DAO層的抽象
-
后端架構,什么時候進行數據服務層的抽象
核心問題一:什么時候進行DAO層的抽象
一個業務系統最初的后端結構如上:
-
web-server層從db層獲取數據並進行加工處理
-
db層存儲數據
此時,web-server層如何獲取底層的數據呢?
web-server層獲取數據的一段偽代碼如上,不用糾結代碼的細節,也不用糾結不同編程語言與不同數據庫驅動的差異,其獲取數據的過程大致為:
-
創建一個與數據庫的連接,初始化資源
-
根據業務拼裝一個SQL語句
-
通過連接執行SQL語句,並獲得結果集
-
通過游標遍歷結果集,取出每行數據,亦可從每行數據中取出屬性數據
-
關閉數據庫連接,回收資源
如果業務不復雜,這段代碼寫1次2次還可以,但如果業務越來越復雜,每次都這么獲取數據,就略顯低效了,有大量冗余、重復、每次必寫的代碼。
如何讓數據的獲取更加高效快捷呢?
通過技術手段實現:
-
表與類的映射
-
屬性與成員的映射
-
SQL與函數的映射
絕大部分公司正在用的ORM,DAO等技術,就是一種分層抽象,可以提高數據獲取的效率,屏蔽連接,游標,結果集這些復雜性。
結論
當手寫代碼從DB中獲取數據,成為通用痛點的時候,就應該抽象出DAO層,簡化數據獲取過程,提高數據獲取效率,向上游屏蔽底層的復雜性。
核心問題二:什么時候要進行數據服務層的抽象
抽象出DAO層之后,系統架構並不會一成不變:
-
隨着業務越來越復雜,業務系統會不斷進行垂直拆分
-
隨着數據量越來越大,數據庫會進行水平切分
-
隨着讀並發的越來越大,會增加緩存降低數據庫的壓力
於是系統架構變成了這個樣子:
業務系統垂直拆分,數據庫水平切分,緩存這些都是常見的架構優化手段。
此時,web-server層如何獲取底層的數據呢?
根據樓主的經驗,以用戶數據為例,流程一般是這樣的:
-
先查緩存:先用uid嘗試從緩存獲取數據,如果cache hit,數據獲取成功,返回User實體,流程結束
-
確定路由:如果cache miss,先查詢路由配置,確定uid落在哪個數據庫實例的哪個庫上
-
查詢DB:通過DAO從對應庫獲取uid對應的數據實體User
-
插入緩存:將kv(uid, User)放入緩存,以便下次緩存查詢數據能夠命中緩存
如果業務不復雜,這段代碼寫1次2次還可以,但如果業務越來越復雜,每次都這么獲取數據,就略顯低效了,有大量冗余、重復、每次必寫的代碼。
特別的,業務垂直拆分成非常多的子系統之后:
-
一旦底層有稍許變化,所有上游的系統都需要升級修改
-
子系統之間很可能出現代碼拷貝
-
一旦拷貝代碼,出現一個bug,多個子系統都需要升級修改
不相信業務會垂直拆分成多個子系統?舉兩個例子:
-
58同城有招聘、房產、二手、二手車、黃頁等5大頭部業務,都需要訪問用戶數據
-
58到家有月嫂、保姆、麗人、速運、平台等多個業務,也都需要訪問用戶數據
如果每個子系統都需要關注緩存,分庫,讀寫分離的復雜性,調用層會瘋掉的。
如何讓數據的獲取更加高效快捷呢?
服務化,數據服務層的抽象勢在必行。
通過抽象數據服務層:
-
web-server層可以通過RPC接口,像調用本地函數一樣調用遠端的數據
-
數據服務層,只有這一處需要關注緩存,分庫,讀寫分離這些復雜性
服務化這里就不展開,更詳細的可參考《互聯網架構為什么要做服務化?》。
結論
當業務越來越復雜,垂直拆分的系統越來越多,數據庫實施了水平切分,數據層實施了緩存加速之后,底層數據獲取復雜性成為通用痛點的時候,就應該抽象出數據服務層,簡化數據獲取過程,提高數據獲取效率,向上游屏蔽底層的復雜性。
互聯網分層架構是一個很有意思的問題,服務化的引入,並不是越早越好:
-
請求處理時間可能會增加
-
運維可能會更加復雜
-
定位問題可能會更加麻煩
千萬別魯莽的在“微服務”大流之下,草率的進行微服務改造,看似“高大上架構”的背后,隱藏着更多並未接觸過的“大坑”。還是那句話,架構和業務的特點和階段有關:一切脫離業務的架構設計,都是耍流氓。
這一篇先到這里,分層架構,還有很多內容要和大家聊:
-
后端架構,是否需要抽取中台業務,什么時機抽取
-
后端架構,是否需要前后端分離,什么時機分離
-
前端架構,如何進行分層實踐
末了,再次強調下,互聯網分層架構的本質,是數據的移動。
互聯網分層架構演進的核心原則,是讓上游更高效的獲取與處理數據,讓下游能屏蔽掉數據的復雜性獲取細節。
互聯網架構為什么要做服務化?
近期參加一些業界的技術大會,“微服務架構”的話題非常之火,也在一些場合聊過服務化架構實踐,最近幾期文章期望用通俗易懂的語言聊聊了個人對服務化以及微服務架構的理解,希望能給大伙一些啟示。如果有遺漏,也歡迎大家補充。
一、互聯網高可用架構,為什么要服務化?
【服務化之前高可用架構】
在服務化之前,互聯網的高可用架構大致是這樣一個架構:
(1)用戶端是瀏覽器browser,APP客戶端
(2)后端入口是高可用的nginx集群,用於做反向代理
(3)中間核心是高可用的web-server集群,研發工程師主要編碼工作就是在這一層
(4)后端存儲是高可用的db集群,數據存儲在這一層
更典型的,web-server層是通過DAO/ORM等技術來訪問數據庫的。
可以看到,最初都是沒有服務層的,此時架構會碰到一些什么痛點呢?
【架構痛點一:代碼到處拷貝】
舉一個最常見的業務的例子->用戶數據的訪問,絕大部分公司都有一個數據庫存儲用戶數據,各個業務都有訪問用戶數據的需求:
在有用戶服務之前,各個業務線都是自己通過DAO寫SQL訪問user庫來存取用戶數據,這無形中就導致了代碼的拷貝。
【架構痛點二:復雜性擴散】
隨着並發量的越來越高,用戶數據的訪問數據庫成了瓶頸,需要加入緩存來降低數據庫的讀壓力,於是架構中引入了緩存,由於沒有統一的服務層,各個業務線都需要關注緩存的引入導致的復雜性:
對於用戶數據的寫請求,所有業務線都要升級代碼:
(1)先淘汰cache
(2)再寫數據
對於用戶數據的讀請求,所有業務線也都要升級代碼:
(1)先讀cache,命中則返回
(2)沒命中則讀數據庫
(3)再把數據放入cache
這個復雜性是典型的“業務無關”的復雜性,業務方需要被迫升級。
隨着數據量的越來越大,數據庫需要進行水平拆分,於是架構中又引入了分庫分表,由於沒有統一的服務層,各個業務線都需要關注分庫分表的引入導致的復雜性:
這個復雜性也是典型的“業務無關”的復雜性,業務方需要被迫升級。
包括bug的修改,發現一個bug,多個地方都需要修改。
【架構痛點三:庫的復用與耦合】
服務化並不是唯一的解決上述兩痛點的方法,抽象出統一的“庫”是最先容易想到的解決:
(1)代碼拷貝
(2)復雜性擴散
的方法。抽象出一個user.so,負責整個用戶數據的存取,從而避免代碼的拷貝。至於復雜性,也只有user.so這一個地方需要關注了。
解決了舊的問題,會引入新的問題,庫的版本維護與業務線之間代碼的耦合:
業務線A將user.so由版本1升級至版本2,如果不兼容業務線B的代碼,會導致B業務出現問題;
業務線A如果通知了業務線B升級,則是的業務線B會無故做一些“自身業務無關”的升級,非常郁悶。當然,如果各個業務線都是拷貝了一份代碼則不存在這個問題。
【架構痛點四:SQL質量得不到保障,業務相互影響】
業務線通過DAO訪問數據庫:
本質上SQL語句還是各個業務線拼裝的,資深的工程師寫出高質量的SQL沒啥問題,經驗沒有這么豐富的工程師可能會寫出一些低效的SQL,假如業務線A寫了一個全表掃描的SQL,導致數據庫的CPU100%,影響的不只是一個業務線,而是所有的業務線都會受影響。
【架構痛點五:瘋狂的DB耦合】
業務線不至訪問user數據,還會結合自己的業務訪問自己的數據:
典型的,通過join數據表來實現各自業務線的一些業務邏輯。
這樣的話,業務線A的table-user與table-A耦合在了一起,業務線B的table-user與table-B耦合在了一起,業務線C的table-user與table-C耦合在了一起,結果就是:table-user,table-A,table-B,table-C都耦合在了一起。
隨着數據量的越來越大,業務線ABC的數據庫是無法垂直拆分開的,必須使用一個大庫(瘋了,一個大庫300多個業務表 =_=)。
【架構痛點六:…】
二、服務化解決什么問題?
為了解決上面的諸多問題,互聯網高可用分層架構演進的過程中,引入了“服務層”。
以上文中的用戶業務為例,引入了user-service,對業務線響應所用用戶數據的存取。引入服務層有什么好處,解決什么問題呢?
【好處一:調用方爽】
有服務層之前:業務方訪問用戶數據,需要通過DAO拼裝SQL訪問
有服務層之后:業務方通過RPC訪問用戶數據,就像調用一個本地函數一樣,非常之爽
User = UserService::GetUserById(uid);
傳入一個uid,得到一個User實體,就像調用本地函數一樣,不需要關心序列化,網絡傳輸,后端執行,網絡傳輸,范序列化等復雜性。
【好處二:復用性,防止代碼拷貝】
這個不展開敘述,所有user數據的存取,都通過user-service來進行,代碼只此一份,不存在拷貝。
升級一處升級,bug修改一處修改。
【好處三:專注性,屏蔽底層復雜度】
在沒有服務層之前,所有業務線都需要關注緩存、分庫分表這些細節。
在有了服務層之后,只有服務層需要專注關注底層的復雜性了,向上游屏蔽了細節。
【好處四:SQL質量得到保障】
原來是業務向上游直接拼接SQL訪問數據庫。
有了服務層之后,所有的SQL都是服務層提供的,業務線不能再為所欲為了。底層服務對於穩定性的要求更好的話,可以由更資深的工程師維護,而不是像原來SQL難以收口,難以控制。
【好處五:數據庫解耦】
原來各個業務的數據庫都混在一個大庫里,相互join,難以拆分。
服務化之后,底層的數據庫被隔離開了,可以很方便的拆分出來,進行擴容。
【好處六:提供有限接口,無限性能】
在服務化之前,各業務線上游想怎么操縱數據庫都行,遇到了性能瓶頸,各業務線容易扯皮,相互推諉。
服務化之后,服務只提供有限的通用接口,理論上服務集群能夠提供無限性能,性能出現瓶頸,服務層一處集中優化。
啊,業務層是否也需要服務化?
《互聯網分層架構的本質》簡述了兩個觀點:
-
互聯網分層架構的本質,是數據的移動
-
互聯網分層架構演進的核心原則:是讓上游更高效的獲取與處理數據,讓下游能屏蔽數據的獲取細節
《分層架構:什么時候抽象DAO層,什么時候抽象數據服務層》中的觀點是:
-
當手寫代碼從DB中獲取數據,成為通用痛點的時候,就應該抽象出DAO層,簡化數據獲取過程,提高數據獲取效率,向上游屏蔽底層的復雜性
-
當業務越來越復雜,垂直拆分的系統越來越多,數據庫實施了水平切分,數據層實施了緩存加速之后,底層數據獲取復雜性成為通用痛點的時候,就應該抽象出數據服務層,簡化數據獲取過程,提高數據獲取效率,向上游屏蔽底層的復雜性
文本將要解答的問題是:
-
基礎數據的訪問需要服務化,業務層是否需要服務化
-
如果需要服務化,什么時候服務化
基礎數據的訪問服務化之后,一個業務系統的后端架構如上:
-
web-server通過RPC接口,從基礎數據service獲取數據
-
基礎數據service通過DAO,從db/cache獲取數據
-
db/cache存儲數據
隨着時間的推移,系統架構並不會一成不變:
-
隨着業務越來越復雜,業務會不斷進行垂直拆分
-
隨着數據越來越復雜,基礎數據service也會越來越多
於是系統架構變成了上圖這個樣子,業務垂直拆分,有若干個基礎數據服務:
-
垂直業務要通過多個RPC接口訪問不同的基礎數據service,service共有是服務化的特征
-
每個基礎數據service訪問自己的數據存儲,數據私有也是服務化的特征
這個架構圖中的依賴關系是不是看上去很別扭?
-
基礎數據service與存儲層之前連接關系很清晰
-
業務web-server層與基礎數據service層之間的連接關系錯綜復雜,變成了蜘蛛網
再舉一個更具體的例子,58同城列表頁web-server如何獲取底層的數據?
-
首先調用商業基礎service,獲取商業廣告帖子數據,用於頂部置頂/精准的廣告帖子展示
-
再調用搜索基礎service,獲取自然搜索帖子數據,用於中部自然搜索帖子展示
-
再調用推薦基礎service,獲取推薦帖子數據,用於底部推薦帖子展示
-
再調用用戶基礎service,獲取用戶數據,用於右側用戶信息展示
-
…
如果只有一個列表頁這么寫還行,但如果有招聘、房產、二手、二手車、黃頁…等多個大部分是共性數據,少部分是個性數據的列表頁,每次都這么獲取數據,就略顯低效了,有大量冗余、重復、每次必寫的代碼。
特別的,不同業務上游列表頁都依賴於底層若干相同服務:
-
一旦一個服務RPC接口有稍許變化,所有上游的系統都需要升級修改
-
子系統之間很可能出現代碼拷貝
-
一旦拷貝代碼,出現一個bug,多個子系統都需要升級修改
如何讓數據的獲取更加高效快捷呢?
業務服務化,通用業務服務層的抽象勢在必行。
通過抽象通用業務服務層,例如58同城“通用列表服務”:
-
web-server層,可以通過RPC接口,像調用本地函數一樣,調用通用業務service,一次性獲取所有通用數據
-
通用業務service,也可以通過多次調用基礎數據service提供的RPC接口,分別獲取數據,底層數據獲取的復雜性,全都屏蔽在了此處
是不是連接關系也看起來更清晰?
這樣的好處是:
-
復雜的從基礎服務獲取數據代碼,只有在通用業務service處寫了一次,沒有代碼拷貝
-
底層基礎數據service接口發生變化,只有通用業務service一處需要升級修改
-
如果有bug,不管是底層基礎數據service的bug,還是通用業務service的bug,都只有一處需要升級修改
-
業務web-server獲取數據更便捷,獲取所有數據,只需一個RPC接口調用
結論:
當業務越來越復雜,垂直拆分的系統越來越多,基礎數據服務越來越多,底層數據獲取復雜性成為通用痛點的時候,就應該抽象出通用業務服務,簡化數據獲取過程,提高數據獲取效率,向上游屏蔽底層的復雜性。
最后再強調兩點:
-
是否需要抽象通用業務服務,和業務復雜性,以及業務發展階段有關,不可一概而論
-
需要抽象什么通用業務服務,和具體業務相關
任何脫離業務的架構設計,都是耍流氓。
互聯網分層架構,為啥要前后端分離?
通用業務服務化之后,系統的典型后端結構如上:
-
web-server通過RPC接口,從通用業務服務獲取數據
-
biz-service通過RPC接口,從多個基礎數據service獲取數據
-
基礎數據service通過DAO,從獨立db/cache獲取數據
-
db/cache存儲數據
隨着時間的推移,系統架構並不會一成不變,業務越來越復雜,改版越來越多,此時web-server層雖然使用了MVC架構,但以下諸多痛點是否似曾相識?
-
產品追求絢麗的效果,並對設備兼容性要求高,這些需求不斷折磨着使用MVC的Java工程師們(本文以Java舉例)
-
不管是PC,還是手機H5,還是APP,應用前端展現的變化頻率遠遠大於后端邏輯的變化頻率(感謝那些喜歡做改版的產品經理),改velocity模版並不是Java工程師喜歡和擅長的工作
此時,為了緩解這些問題,一般會成立單獨的前端FE部門,來負責交互與展現的研發,其職責與后端Java工程師分離開,但痛點依然沒有完全解決:
-
一點點展現的改動,需要Java工程師們重新編譯,打包,上線,重啟tomcat,效率極低
-
原先Java工程師負責所有MVC的研發工作,現在分為Java和FE兩塊,需要等前端和后端都完成研發,才能一起調試整體效果,不僅增加了溝通成本,任何一塊出問題,都可能導致項目延期
更具體的,看一個這樣的例子,最開始產品只有PC版本,此時其系統分層架構如下:
客戶端,web-server,service,非常清晰。
隨着業務的發展,產品需要新增Mobile版本,Mobile版本和PC版本大部分業務邏輯都一樣,唯一的區別是屏幕比較小:
-
信息展現的條數會比較少,即調用service服務時,傳入的參數會不一樣
-
產品功能會比較少,大部分service的調用一樣,少數service不需要調用
-
展現,交互會有所區別
由於工期較緊,Mobile版本的web-server一般怎么來呢?
沒錯,把PC版本的工程拷貝一份,然后再做小量的修改:
-
service調用的參數有些變化
-
大部分service的調用一樣,少數service的調用去掉
-
修改展現,交互相關的代碼
業務繼續發展,產品又需要新增APP版本,APP版本和Mobile版本業務邏輯完全相同,唯一的區別是:
-
Mobile版本返回html格式的數據,APP版本返回json格式的數據,然后進行本地渲染
由於工期較緊,APP版本的web-server一般怎么來呢?
沒錯,把Mobile版本的工程拷貝一份,然后再做小量的修改:
-
把拼裝html數據的代碼,修改為拼裝json數據
這么迭代,演化,發展,架構會變成這個樣子:
-
端,是PC,Mobile,APP
-
web-server接入,是PC站,M站,APP站
-
服務層,通用的業務服務,以及基礎數據服務
這個架構圖中的依賴關系是不是看上去很別扭?
-
端到web-server之間連接關系很清晰
-
web-server與service之間的連接關系變成了蜘蛛網
PC/H5/APP的web-server層大部分業務是相同的,只有少數的邏輯/展現/交互不一樣:
-
一旦一個服務RPC接口有稍許變化,所有web-server系統都需要升級修改
-
web-server之間存在大量代碼拷貝
-
一旦拷貝代碼,出現一個bug,多個子系統都需要升級修改
如何讓數據的獲取更加高效快捷,如何讓數據生產與數據展現解耦分離呢?
前后端分離的分層抽象勢在必行。
通過前后端分離分層抽象:
-
站點展示層,node.js,負責數據的展現與交互,由FE維護
-
站點數據層,web-server,負責業務邏輯與json數據接口的提供,由Java工程師維護
這樣的好處是:
-
復雜的業務邏輯與數據生成,只有在站點數據層處寫了一次,沒有代碼拷貝
-
底層service接口發生變化,只有站點數據層一處需要升級修改
-
底層service如果有bug,只有站點數據層一處需要升級修改
-
站點展現層可以根據產品的不同形態,傳入不同的參數,調用不同的站點數據層接口
除此之外:
-
產品追求絢麗的效果,並對設備兼容性要求高,不再困擾Java工程師,由更專業的FE對接
-
一點點展現的改動,不再需要Java工程師們重新編譯,打包,上線,重啟tomcat
-
約定好json接口后,Java和FE分開開發,FE可以用mock的接口自測,不再等待一起聯調
結論:
當業務越來越復雜,端上的產品越來越多,展現層的變化越來越快越來越多,站點層存在大量代碼拷貝,數據獲取復雜性成為通用痛點的時候,就應該進行前后端分離分層抽象,簡化數據獲取過程,提高數據獲取效率,向上游屏蔽底層的復雜性。
最后再強調兩點:
-
是否需要前后端分離,和業務復雜性,以及業務發展階段有關,不可一概而論
-
本文強調的前后端分離的思路,實際情況下有多種實現方式,文章並沒有透徹展開實現細節
任何脫離業務的架構設計,都是耍流氓。
思路比細節重要。
閱讀前序文章,“分層架構設計”的背景與來龍去脈更加清晰: