Go 項目 2 次架構演變,算是入了微服務的門吧!

作者:Ciusyan

https://juejin.cn/post/7203247258850312251

一、初見 Dousheng

(1)架構思路

因爲自己以前是一個Javer,對傳統的MVC三層架構還算比較熟悉,就巨石架構而言,使用MVC的架構方式,模塊還算是比較清晰了。

因爲接觸了一門新的語言GoLang,利用一些熟悉的事物過渡到不太熟悉的領域。是我們人性所擅長的。

所以在 Dousheng 的架構演變過程中,借鑑了 MVC 的思想,引入似MVC的架構方式。

也就是:

  1. 控制層(Handler):用於控制網絡請求。

  2. 業務層(Service):用於處理具體業務,還有簡單的數據庫操作。

  3. 持久層(Dao):用於進行數據庫的操作。

又因爲沒有類似 Spring 的框架來管理依賴(雖然我們的項目中引入了IoC,在後面介紹~),我們這裏並沒有嚴格的區分業務層和持久層。所以我們最初的架構是 "兩層半":Handler \-> Service + Dao

瞭解了初次架構的設計思路後,我們來看一些直觀的表達。

1、架構圖

簡單畫一幅圖來表示上面的思路就是:

這樣比較傳統的單體架構,較容易理解,就不多解釋了。直接來看看拆分後的目錄結構。

2、目錄結構

再來看看架構的目錄結構,一些輔助包,暫時不需要關注,可查看對應的文檔。現在只需要關注業務模塊的分層即可:apps包下面是業務模塊的

目錄結構概覽 [解讀]
DouSheng            # 極簡版抖音 APP
├── apps            # 所有服務模塊
│   ├── all         # 統一管理所有模塊實例的註冊[驅動加載的方式]
│   ├── comment     # ===評論模塊===
│   │   ├── api     # 控制層(Handler)
│   │   ├── impl    # 業務層(Service) + 持久層(Dao)
│   │   └── pb     # interface 、model 
│   ├── user        # ===用戶模塊===
│   │   ├── api     
│   │   ├── impl
│   │   └── pb
│   └── video       # ===視頻模塊===
│       ├── api
│       ├── impl
│       └── pb
├── cmd             # CLI
├── common.pb       # 放置公共的protobuf文件[可抽離]
├── conf            # 項目配置對象
├── docs            # 項目相關文檔
├── etc             # 項目具體配置
├── ioc             # IoC容器[可抽離]
├── protocol        # 提供協議
├── utils           # 工具包
└── version         # 版本信息
部分主要文件概覽 [解讀]
├── apps                            # 所有的業務模塊
│   ├── all                         # 驅動註冊所有的IOC容器實例
│   │   └── auto_register.go
│   ├── user                        # 以用戶模塊舉例
│   │   ├── api                     # 提供的 API 接口
│   │   │   ├── http.go             # 使用 HTTP 的方式暴露 控制層邏輯
│   │   │   └── user.go             # user服務模塊暴露的方法
│   │   ├── app.go                  # user模塊的結構體方法
│   │   ├── impl                    # user.ServerService 的實現
│   │   │   ├── dao.go              # 可以看作是 持久層邏輯
│   │   │   ├── impl.go             # 可以看作是 業務層邏輯
│   │   │   ├── user.go             # user.ServerService 接口方法的實現
│   │   │   └── user_test.go        # 此模塊測試用例【注:必寫,一般用於測試本模塊CURD的功能】
│   │   ├── pb                      # 此模塊的protobuf文件,裏面有(接口方法、請求model、響應model、本模塊model)
│   │   │   └── user.proto      
│   │   ├── README.md               # 本模塊說明
│   │   ├── user.pb.go              # 利用 protoc 生成(結構體)
│   │   └── user_grpc.pb.go         # 利用 protoc 生成(接口)
├── cmd                             # 用於啓動項目
│   ├── root.go                     
│   └── start.go                    # 啓動邏輯在這
├── common                          # 定義的公共的protobuf文件,可抽離
│   ├── common.pb.go
│   └── pb
│       └── common.proto
├── conf                            # 項目配置對象
│   ├── app.go                      # 此項目的配置
│   ├── config.go                   # 統一配置
│   ├── config_test.go              
│   ├── load.go                     # 加載所有配置
│   ├── log.go                      # 日誌相關配置
│   └── mysql.go                    # mysql相關配置
├── etc
│   ├── dousheng.toml               # 項目配置文件位置【可換成其他的,用其他庫解析】[禁止上傳github]
│   └── dousheng.toml.template      # 配置文件模板[可上傳github]
├── ioc                             # IoC容器
│   ├── all.go                      # 統一所有容器
│   ├── gin.go                      # Gin HTTP 服務容器
│   ├── grpc.go                     # GRPC 服務容器
│   └── internal.go                 # 內部服務容器
├── Makefile                        # 利用Makefile管理項目[相當於一個腳手架]
├── utils                           # 放置一些通用的工具
│   └── md5.go

看完了apps下的目錄結構,應該能很清晰的看出分了三個模塊(user、comment、video),並且每一個模塊都有自己完全獨立的 “兩層半” 架構。

既然還算清晰的對模塊進行了劃分。那爲什麼還要演變呢?

(2)遇到的問題

儘管也是分模塊開發,但是最終還是會打包並部署,還是爲單體應用。不是說不行,但是可會遇到一些問題:

當業務體量不大的時候,單體架構可能會更受人們青睞,也不會引入更多額外的資源、技術複雜度...

但是當業務體量、用戶體量一旦增長了起來,單體架構很難穩定的抗住衝擊。再加上Go-To-Byte也想學習一下微服務開發。

所以,我們進行了架構的第一次演變...

二、第一次演變

(1)架構思路

人類自古就有化繁爲簡、分而治之的思想,我們可以將一個複雜而龐大的業務,抽象成一個個簡單的服務,然後單獨的分開處理。我覺得這也是微服務的核心思路。

但是,在每一個單獨的服務中,我們還是保留了 MVC 的” 兩層半架構 “。再來看看一些直觀的表達:

1、架構圖

我們原先根據業務功能,對模塊進行了垂直劃分,然後在劃分出來的模塊中,進行了水平劃分,如下圖所示:

從圖中可以發現,拆分出來的每一個服務,我們都用不一樣的端口,不一樣的進程,運行了起來。對外部提供的服務,通過 HTTP 的方式暴露出去。而內部服務間的調用,就不再是通過文件路由引用了,而是通過 GRPC 協議暴露出去。

看完了架構圖,我們來看看大致的目錄結構。

2、目錄結構

總目錄結構概覽 [解讀]

還是以用戶中心、視頻服務、評論服務舉例。

DouSheng
├── dou_kit    # ===簡單的分Kit公共包===
│ .....
├── user_center    # ===用戶服務===
│ .....
└── video_service                # ===視頻服務===
│ .....
└── comment_service              # ===評論服務===
│ .....
詳細一些的結構概覽 [解讀]

這裏以用戶中心爲例,展開目錄結構:

DouSheng
├── dou_kit    # ===簡單的分Kit公共包===
│   ├── conf    # 配置文件
│   ├── constant   # 常量
│   ├── docs.sql   # 部分文檔
│   ├── exception   # 統一error處理
│   └── ioc    # IOC容器
├── user_center    # ===用戶服務===
│   ├── apps                     # 包含的模塊
│   │   ├── token                # token模塊
│   │   │   ├── impl
│   │   │   └── pb
│   │   ├── user                 # 用戶模塊
│   │   │   ├── api
│   │   │   ├── impl
│   │   │   └── pb
│   ├── client.rpc.middlerware   # 用戶中心提供的客戶端 
│   ├── cmd                      # 命令行工具
│   ├── common                   # 模塊內公共工具
│   │   ├── constant
│   │   └── utils
│   ├── docs                     # 模塊內文檔
│   │   ├── example
│   │   ├── sql
│   │   └── static.image
│   ├── etc                      # 用戶中心的配置文件
│   ├── protocol                 # 對外暴露的協議     
│   └── version                  # 用於注入版本信息
└── video_service                # ===視頻服務===
│ .....
└── comment_service              # ===評論服務===
│ .....

看完了演進後的架構圖和目錄結構。其實這就是一個簡單的微服務拆分了。核心就是化繁爲簡,分而治之的思想。我們這裏僅對項目架構簡單說明,很多微服務的知識並未在這一節體現。

這樣進行簡單的拆分之後,分出了若干服務,並且服務間通過 rpc 調用,每個服務可以單獨部署、單獨編寫、本來已經解決了單體架構的很多問題了。而且是通過功能模塊劃分的,更容易理解了。那爲什麼還有一次架構演進呢?我們又遇到了什麼問題呢?

(2)遇到的問題

我們在這裏,首先遇到的問題就是:對外暴露的接口不統一,比如官方提供的測試 APP,需要配置後端接口的主機地址 + 端口。只能訪問一個進程內的接口。

而我們這樣的拆分方式,會同時啓動很多個對外暴露 HTTP 服務的進程。若想要完整的通過 APP 測試,是幾乎不可能的事情。

必行之事,何必問天。光是因爲上面所述的一個理由,我們的架構,就不得不再一次演變。還不談會遇到的其他問題。

那我們來看看是如何進行第二次架構演變的。

三、第二次演變

(1)架構思路

“沒有什麼是加一層解決不了的事情,如果有,那就兩層”。相信大家都聽過這句話。

是啊,我們遇到了上面的問題之後,嘗試加入了一層:Api Rooter來解決這個問題。

解決了嗎?加入了這一層,我們對外暴露的 HTTP 接口,就可以統一在這一層做了。而由這一層,通過 GRPC 去調用內部服務實際的業務邏輯。

來看一些較爲直觀的表達,再繼續探討。

1、架構圖

主要呈現的是服務的拆分關係。

如圖所示,對外暴露的 HTTP 服務,全是經過Api Rooter這一層出去的。在這一層,主要做兩件事情。

  1. 管理Token的認證 [提供 Gin 的認證中間件]

  2. 組裝Api,對外提供 HTTP 服務

因爲 Token 相當於是用戶的身份憑證,以前是放在用戶中心的,現在是放在Api Rooter的,因爲放在這裏,當有請求過來的時候,若需要校驗信息,直接調用方法即可。就不需要額外走 GRPC 去調用user_center的方法了。

我們這裏其實並沒有太多組合 Api 的接口。我們的接口大多數是已經在內部服務組裝好的。然後在這一層直接暴露出去即可。相當於這是各個 HTTP 服務 Handler 的聚集地。在這裏聚集,然後統一暴露給外界。

值得一提的是,這一層,是通過 GRPC 去調用內部服務的,並不是通過 HTTP 協議去調用的。主要是因爲這是自定義的 Api 組合層,支持 GRPC 去調用自己的服務。

2、目錄結構

加入了 Api 這一層、把一些公共模塊更進一步的抽離出來後,現在的目錄結構是這樣的:

DouSheng
├── .github.workflows
├── api_rooter              # ===簡易版網關===
│   ├── apps                
│   │   ├── token           # Token的 RPC Server
│   │   │   ├── impl        
│   │   │   └── pb
│   │   ├── user.api        # 用戶中心的HTTP接口
│   │   └── video.api       # 視頻服務的HTTP接口
│   ├── client.rpc          # Token的RPC Client
│   ├── common  
│   │   ├── all
│   │   └── utils
│   ├── docs
│   ├── etc
│   └── protocol
├── dou_kit                 # ===封裝的公共庫===
│   ├── client
│   ├── cmd
│   ├── conf
│   ├── constant
│   ├── docs
│   │   ├── sql
│   │   └── static
│   ├── exception.custom
│   ├── ioc
│   ├── protocol
│   └── version
├── guidance.docs           # ===項目文檔===
├── user_center             # ===用戶中心===
│   ├── apps.user
│   │   ├── impl
│   │   └── pb
│   ├── client.rpc
│   ├── common
│   │   ├── all
│   │   └── utils
│   ├── docs
│   │   ├── example
│   │   ├── sql
│   │   └── static.image
│   └── etc
└── video_service           # ===視頻服務===
    ├── apps.video
    │   ├── impl
    │   └── pb
    ├── client.rpc
    ├── common
    │   ├── all
    │   ├── pb
    │   └── utils
    ├── docs.sql
    ├── etc
    └── store.aliyun

在加入這一層後,對外暴露接口的方式、樣式、和端口,都統一了。這下就完事了嘛?未來真的不會出問題了嗎?

(2)可能會遇到的問題

我們現在是通過Api Rooter來統一暴露接口的。其中最致命的就是整個 App Rooter 屬於 single point of failure,若在這一層出現嚴重的代碼缺陷,或者流量洪峯,可能會引發集羣宕機,出現單點故障。這個故障並不是說某一個服務宕機了,而是對外提供的 HTTP 接口會崩掉。

但是由於一些原因:如項目進度、未學習的知識、技術成本.... 等問題。目前還沒有辦法再次演進。所以 Dousheng 最終的架構,暫定爲這樣了。

四、未來的設想

未來架構演進思路

既然每一個 API 服務太龐大了,那我們繼續利用大禹治水,分而治之的思想。將其拆分成多個服務獨立的網關小組。這樣就算某一服務提供的 API 宕機了,也不會導致所有服務宕機。也就是解決了單體故障的問題。

在引入一層真正的網關技術(API Geteway),來處理轉發用戶的請求。而且將一些橫切面的邏輯放置到這一層。比如日誌監控、安全認證等等

大致畫一幅圖,也就是這個樣子的:

至此,我們通過兩次架構的演進,相信你已經基本瞭解了 Dousheng 的架構思路。也算是入了微服務的門了~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/WxnjyZre8MMMyOszMRNjnA