程式內優雅管理多個服務

kevinwan發表於2022-04-18

前言

在 go-zero 社群裡,經常會有同學問,把 API gatewayRPC service 放在同一個程式內可不可以?怎麼弄?有時也會有同學把對外服務和消費佇列放在一個程式內。我們們姑且不說此種用法合理與否,因為各個公司的業務場景和開發模式的差異,我們就只來看看此類問題怎麼解比較優雅。

問題舉例

我們用兩個 HTTP 服務來舉例,我們有這樣兩個服務,需要啟動在一個程式內的兩個不同埠。程式碼如下:

package main

import (
  "fmt"
  "net/http"
)

func morning(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintln(w, "morning!")
}

func evening(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintln(w, "evening!")
}

type Morning struct{}

func (m Morning) Start() {
  http.HandleFunc("/morning", morning)
  http.ListenAndServe("localhost:8080", nil)
}

func (m Morning) Stop() {
  fmt.Println("Stop morning service...")
}

type Evening struct{}

func (e Evening) Start() {
  http.HandleFunc("/evening", evening)
  http.ListenAndServe("localhost:8081", nil)
}

func (e Evening) Stop() {
  fmt.Println("Stop evening service...")
}

func main() {
  // todo: start both services here
}

程式碼是足夠簡單的,就是有請求 morning 介面,服務返回 morning!,請求 evening 介面,服務返回 evening 。讓我們來嘗試實現一下~

第一次嘗試

啟動兩個服務,不就是把兩個服務在 main 裡都啟動一下嗎?我們來試試

func main() {
  var morning Morning
  morning.Start()
  defer morning.Stop()

  var evening Evening
  evening.Start()
  defer evening.Stop()
}

啟動完,我們用 curl 來驗證一下

$ curl -i http://localhost:8080/morning
HTTP/1.1 200 OK
Date: Mon, 18 Apr 2022 02:10:34 GMT
Content-Length: 9
Content-Type: text/plain; charset=utf-8

morning!
$ curl -i http://localhost:8081/evening
curl: (7) Failed to connect to localhost port 8081 after 4 ms: Connection refused

為什麼只有 morning 成功,而 evening 無法請求呢?

我們在 main 裡面加上列印語句試試

func main() {
  fmt.Println("Start morning service...")
  var morning Morning
  morning.Start()
  defer morning.Stop()

  fmt.Println("Start evening service...")
  var evening Evening
  evening.Start()
  defer evening.Stop()
}

重新啟動

$ go run main.go
Start morning service...

發現只列印了 Start morning service…,原來 evening 服務壓根沒有啟動。究其原因,是因為 morning.Start() 阻塞了當前 goroutine,後續程式碼就得不到執行了。

第二次嘗試

這時,WaitGroup 就可以派上用場了。WaitGroup 顧名思義,就是用來 wait 一組操作,等待它們通知可以繼續。讓我們來嘗試一下。

func main() {
  var wg sync.WaitGroup
  wg.Add(2)

  go func() {
    defer wg.Done()
    fmt.Println("Start morning service...")
    var morning Morning
    defer morning.Stop()
    morning.Start()
  }()

  go func() {
    defer wg.Done()
    fmt.Println("Start evening service...")
    var evening Evening
    defer evening.Stop()
    evening.Start()
  }()

  wg.Wait()
}

啟動試試

$ go run main.go
Start evening service...
Start morning service...

好,兩個服務都起來了,我們用 curl 驗證一下

$ curl -i http://localhost:8080/morning
HTTP/1.1 200 OK
Date: Mon, 18 Apr 2022 02:28:33 GMT
Content-Length: 9
Content-Type: text/plain; charset=utf-8

morning!
$ curl -i http://localhost:8081/evening
HTTP/1.1 200 OK
Date: Mon, 18 Apr 2022 02:28:36 GMT
Content-Length: 9
Content-Type: text/plain; charset=utf-8

evening!

確實都可以了,我們看到我們使用 WaitGroup 的流程是

  1. 記得我們有幾個需要 wait 的服務
  2. 一個一個新增服務
  3. 等待所有服務結束

讓我們看看 go-zero 是怎麼做的~

第三次嘗試

go-zero 裡,我們提供了一個 ServiceGroup 工具,方便管理多個服務的啟動和停止。讓我們看看帶入我們的場景是怎麼做的。

import "github.com/zeromicro/go-zero/core/service"

// more code

func main() {
  group := service.NewServiceGroup()
  defer group.Stop()
  group.Add(Morning{})
  group.Add(Evening{})
  group.Start()
}

可以看到,程式碼的可讀性好了很多,並且我們也不會不小心算錯該給 WaitGroup 加幾了。並且 ServiceGroup 還保證了後啟動的服務先 Stop,跟 defer 效果一致,這樣的行為便於資源的清理。

ServiceGroup 不光只是管理了每個服務的 Start/Stop,同時也提供了 graceful shutdown,當收到 SIGTERM 訊號的時候會主動呼叫每個服務的 Stop 方法,對於 HTTP 服務,可以通過 server.Shutdown 來優雅退出,對於 gRPC 服務來說,可以通過 server.GracefulStop() 來優雅退出。

總結

ServiceGroup 的實現其實也是比較簡單的,程式碼一共82行。

$ cloc core/service/servicegroup.go
------------------------------------------------------------------
Language        files          blank        comment           code
------------------------------------------------------------------
Go                 1             22             14             82
------------------------------------------------------------------

雖然程式碼短小精悍,但是在 go-zero 裡卻每個服務(Restful, RPC, MQ)基本都是通過 ServiceGroup 來管理的,可以說非常方便,程式碼值得一讀。

專案地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。

相關文章