cmdr 04 - 簡單微服務 (daemon)

cmdr 04 - simple micro-service
based on cmdr v0.2.21

My ado is too much.

所以這次直入主題,謝絕吐槽。不知道 cmdr 幹嘛用的,無妨看看前文

那麼,golang適合做後端開發,無論是 gRPC 還是 RESTful 都是它的強項。

一旦我們想要開發一個微服務時,拋開核心邏輯不談,也不論 DevOps 方面究竟是 K8s,還是 Docker,還是裸機,總要面對一個啓動、調試、測試的日常問題。

cmdr 除了提供命令行參數的解釋能力之外,也額外提供了一個daemon插件,它可以幫助你簡化日常開發工作,也令你不必關心 pid 文件、日誌、退出信號等等問題,也無需重複編排 daemon 相關的命令行指令。

下面介紹怎麼使用 daemon 插件,怎麼編寫實際的業務邏輯。我們以 demo 爲例編寫一個簡單的示例性微服務,並解釋具體的做法。

使用 Daemon 插件

啓用 Daemon 插件

啓用 Daemon 插件只需一行代碼:

// Entry is app main entry
func Entry() {

    logrus.SetLevel(logrus.DebugLevel)
    logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})

    daemon.Enable(svr.NewDaemon(), nil, nil, nil)

    if err := cmdr.Exec(rootCmd); err != nil {
        logrus.Errorf("Error: %v", err)
    }

}

實現 daemon.Daemon 接口

啓用 daemon 插件,需要你實現 daemon.Daemon 接口,並編寫一定的包裝代碼來連接 cmdr, daemon 以及你的業務邏輯。

daemon.Daemon 接口

daemon.Daemon 接口是這樣的:

// Daemon interface should be implemented when you are using `daemon.Enable()`.
type Daemon interface {
    OnRun(cmd *cmdr.Command, args []string, stopCh, doneCh chan struct{}) (err error)
    OnStop(cmd *cmdr.Command, args []string) (err error)
    OnReload()
    OnStatus(cxt *Context, cmd *cmdr.Command, p *os.Process) (err error)
    OnInstall(cxt *Context, cmd *cmdr.Command, args []string) (err error)
    OnUninstall(cxt *Context, cmd *cmdr.Command, args []string) (err error)
}

對於一個微服務來說,你一定要編寫的是 OnRunOnStop 兩個方法。其他的幾個方法,通常是可選的,daemon插件針對 RESTful http2 完成了默認的邏輯來支持 reload,status。此外,daemon插件還針對 systemd 實現了默認的 install 和 uninstall 邏輯。

所以下面我們首先完成必須的部分:

OnRun
type daemonImpl struct {}

func (*daemonImpl) OnRun(cmd *cmdr.Command, args []string, stopCh, doneCh chan struct{}) (err error) {
    logrus.Debugf("demo daemon OnRun, pid = %v, ppid = %v", os.Getpid(), os.Getppid())
    go worker(stopCh, doneCh)
    return
}

func worker(stopCh, doneCh chan struct{}) {
LOOP:
    for {
        time.Sleep(time.Second) // this is work to be done by worker.
        select {
        case <-stopCh:
            break LOOP
        default:
        }
    }
    doneCh <- struct{}{}
}

daemon 提供兩個 channels,stopCh 應該促使你的代碼結束無限循環,當你的代碼退出循環之後應該觸發 doneCh 事件。這樣的邏輯保證了整個微服務的 graceful shutdown。

至於核心的邏輯,我們的 worker,現在僅僅是個無限循環而已,你可以根據實際業務需要對其完成替換。

下一次我們也許提供一個 RESTful micro-service 的樣本。

OnStop 以及其他

func (*daemonImpl) OnStop(cmd *cmdr.Command, args []string) (err error) {
    logrus.Debugf("demo daemon OnStop")
    return
}

func (*daemonImpl) OnReload() {
    logrus.Debugf("demo daemon OnReload")
}

func (*daemonImpl) OnStatus(cxt *daemon.Context, cmd *cmdr.Command, p *os.Process) (err error) {
    fmt.Printf("%v v%v\n", cmd.GetRoot().AppName, cmd.GetRoot().Version)
    fmt.Printf("PID=%v\nLOG=%v\n", cxt.PidFileName, cxt.LogFileName)
    return
}

func (*daemonImpl) OnInstall(cxt *daemon.Context, cmd *cmdr.Command, args []string) (err error) {
    logrus.Debugf("demo daemon OnInstall")
    return
}

func (*daemonImpl) OnUninstall(cxt *daemon.Context, cmd *cmdr.Command, args []string) (err error) {
    logrus.Debugf("demo daemon OnUninstall")
    return
}

其它的接口方法基本上什麼也不做,因爲對於我們的worker來說,不需要在 OnStop 時清理數據庫連接、釋放其它資源,也不需要在 OnReload 時加載新的配置文件。

測試 demo

現在我們可以將 demo 跑起來看看了。首先研究下有什麼命令行指令可供使用,我們採用 --tree 來看看:

可以注意到 s, server, serve, svr, daemon 命令是新增的,它包含一組子命令以提供 daemon 相關的操作。

其中 server start 子命令的解說是這樣的:

也就是說,start子命令的兩個變形允許你在前臺運行微服務,這是爲了便於 docker 集成,以及在 IDE 中調試微服務的目的:

# 在前臺運行微服務,而不是進入 daemon 模式
demo run
demo start --foreground

對於 daemon 模式,沒有標準的規範定義來要求一定具備哪些要素,不過大體上還是有約定俗成的東西。daemon 在中文中常常被稱作 守護進程

daemon 模式一般來說包含這些要素:

  • 進程啓動後,fork自己的一份副本在操作系統中運行,這樣副本和 tty 的關聯就被detach了,此外子進程也具有獨立的環境和進程空間,甚至是身份,不會收到其它服務、其它 ttys 的干擾。
  • 子進程在 /var/run 中保持一個 pid 文件,這指示了子進程的基本狀態
  • 子進程通過 syscall signals 來與前臺交互,一般地說,SIGHUP信號使得子進程 reload 配置信息完成重啓動、卻不被關閉進程和重新啓動進程;SIGTERM等信號通知子進程結束服務。等等。
  • 子進程將日誌輸出爲 /var/log/ 下的日誌文件
前臺運行

所以,我們運行下demo在前臺:

然後按下 CTRL-C 終止它,可以看到這個”微服務“能夠正常地跑起來,也能正常地自行銷燬。

守護進程運行

而如果我們要運行 demo 爲守護進程的話,首先你需要將它編譯成可執行文件,然後才能啓動爲守護進程模式。

通過 vagrant 環境,我們可以看到守護進程啓動了,然後被我們的 stop 指令正確地關閉了。

systemd 服務運行

在支持 systemd 的 Linux 發行版中,我們可以測試將微服務安裝爲 systemd 服務。

其中,sudo /vagrant/demo server install 完成安裝動作,成功之後demo服務就安裝就緒了,並且已經被預設爲隨系統啓動而自動啓動的模式。

然後我們依照 systemd 的規範啓動它:sudo systemctl start demo@$USER.service

值得注意的是,我們將 demo 安裝爲了通配模式,因此 demo 是可以在不同用戶身份下被啓動的。如果你想用專用的微服務賬戶啓動它,你可以使用:sudo systemctl start [email protected]

然後我們通過 sudo systemctl status [email protected] 查看到 demo 已經啓動成功了,其中有三個錯誤,然而他們是可以被忽略的,它們都是爲了嘗試建立幾個相關文件夾的目的,所以只是預防性的指令。而 demo 的正主,也就是 ExecStart 行表示啓動時成功的,而且 Active 的狀態也是 running 狀態。

此時,log/logrus 等日誌輸出也被轉發到了日誌文件 /var/log/demo/demo.log 中。

那麼我們也可以通過 sudo systemctl stop demo@$USER.service 來停止服務。

小結

由於 systemd 在 macOS 中並不被直接支持,所以對於這個部分的測試是放在 vagrant 中完成的。

對於 Windows 來說,你只能使用 server run 前臺運行的方式,我們也暫無支持 NT Service 的計劃。但你可以通過前臺運行的方式完成日常開發調試工作。

實際的生產環境中,你可以選擇 centos,ubuntu 等發行版,部署需要的只是 sudo demo server install 一條指令。

對於容器的環境,你應該使用 demo server run 這種前臺運行模式來啓動。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章