做了3年的後端開發, 經歷一款SaaS產品從0到10(還沒有到100, 哈哈哈)的過程, 3年間後端的架構逐步演變, 在微服務的實踐過程中遇到的問題也越來越多, 在這裡總結下.
產品是一款服務於人力資源的SaaS線上服務, 面向HR有Web Android/iOS 小程式多個客戶端, 後端採用RESTful風格API來提供服務. 主要使用Python語言, 方便快速迭代.
架構的演進經歷了4個大的階段: 1. MVC 2. 服務拆分 3. 微服務架構 4. 領域驅動設計.
1. MVC
專案剛開始的時候, 後端同事不超過5個, 這個階段主要的工作是實現產品的原型, 沒有太多的考慮架構, 使用Django來快速實現功能, DB的表結構設計好之後, 抽象出功能View, 由於產品設計也很不完善, 後端需要很多的預留設計, 避免產品邏輯的變更帶來整個表結構的變動, 在這個階段程式碼上最重要的是確定適合團隊的程式碼規範, 程式碼檢查規則.
整體上架構如上圖, Nginx負責負載均衡, 分發流量到多個Django服務, Django處理邏輯, 需要非同步任務就交給Celery, 然後資料量比較大的地方使用Redis做快取. 同時還有實時訊息通知的需要使用了Nginx Push Module.
問題與優化方式:
Django併發效能差 使用uWSGI Master+Worker 配合 gevent 攜程支援高併發
Redis連線數過多 使用redis-py自帶的連線池來實現連線複用
MySQL連線數過多 使用djorm-ext-pool連線池複用連線
Celery配置gevent支援併發任務
隨著開發的功能越來越多, Django下的app也越來越多, 這就帶了釋出上的不方便, 每次釋出版本都需要重啟所有的Django服務, 如果釋出遇到問題, 只能加班解決了. 而且單個Django工程下的程式碼量也越來越多, 不好維護.
2. 服務拆分
隨著後端團隊的壯大, 分給每個同事的需求也越來越細, 如果繼續在一個工程裡面開發所有的程式碼, 維護起來的代價太高, 而我們的上一個架構中在Django裡面已經按模組劃分了一個個app, app內高類聚, app之間低耦合, 這就為服務的拆分帶來了便利. 拆分的過程沒有遇到太大的問題, 初期的拆分只是程式碼的分離, 把公用的程式碼抽離出來實現一個公用的Python庫, 資料庫, Redis還是共用, 隨著負載的增加, 資料庫也做了多例項.
如上圖, 服務之間儘量避免相互呼叫, 需要互動的地方採用http請求的方式, 內網的呼叫使用hosts指向內網地址.
問題與優化方式:
Nginx Push Module由於長時間沒有維護, 長連線最大數量不夠, 使用Tornado + ZeroMQ實現了tormq服務來支撐訊息通知
服務之間的呼叫採用http的方式, 並且要求有依賴的服務主機配置hosts指向被呼叫的地址, 這樣帶來的維護上的不方便. 以及在呼叫鏈的過程中沒有重試, 錯誤處理, 限流等等的策略, 導致服務可用性差. 隨著業務拆分, 繼續使用Nginx維護配置非常麻煩, 經常因為修改Nginx的配置引發呼叫錯誤. 每一個服務都有一個完整的認證過程, 認證又依賴於使用者中心的資料庫, 修改認證時需要重新發布多個服務.
3. 微服務架構
首先是在接入層引入了基於OpenResty的Kong API Gateway, 定製實現了認證, 限流等外掛. 在接入層承接並剝離了應用層公共的認證, 限流等功能. 在釋出新的服務時, 釋出指令碼中呼叫Kong admin api註冊服務地址到Kong, 並載入api需要使用外掛.
為了解決相互呼叫的問題, 維護了一個基於gevent+msgpack的RPC服務框架doge, 藉助於etcd做服務治理, 並在rpc客戶端實現了限流, 高可用, 負載均衡這些功能.
在這個階段最難的技術選型, 開源的API閘道器大多用Golang與OpenResty(lua)實現, 為了應對我們業務的需要還要做定製. 前期花了1個月時間學習OpenResty與Golang, 並使用OpenResty實現了一個短網址服務shorturl用在業務中. 最終選擇Kong是基於Lua釋出的便利性, Kong的開箱即用以及外掛開發比較容易. 效能的考量倒不是最重要的, 為了支撐更多的併發, 還使用了雲平臺提供的LB服務分發流量到2臺Kong伺服器組成的叢集. 叢集之間自動同步配置.
餓了麼維護一個純Python實現的thrift協議框架thriftpy, 並提供很多配套的工具, 如果團隊足夠大, 這一套RPC方案其實是合適的, 但是我們的團隊人手不足, 水平參差不齊, 很難推廣這一整套學習成本高昂的方案. 最終我們開發了類Duboo的RPC框架doge, 程式碼主要參考了weibo開源的motan.
4. 領域驅動設計
在這一架構中我們嘗試從應用服務中抽離出資料服務層, 每一個數據服務包含一個或多個界限上下文, 界限上下文類只有一個聚合根來暴露出RPC呼叫的方法. 資料服務不依賴於應用服務, 應用服務可以依賴多個數據服務. 有了資料服務層, 應用就解耦了相互之間的依賴, 高層服務只依賴於底層服務.
在我離職時領域驅動設計還在學習設計階段, 還沒有落地, 但是我相信前公司的後端架構一定會往這個方向繼續演進.
總結
架構的設計, 技術的選型, 不能完全按照流行的技術走, 最終還是服務於產品, 服務於客戶的需求. 設計過程中由於團隊, 人員的結構問題, 有很多的妥協之處, 如何在妥協中找到最優解才是最大的挑戰.