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 demo@msuser.service

然后我们通过 sudo systemctl status demo@vagrant.service 查看到 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 这种前台运行模式来启动。

参考

阅读 677

推荐阅读
logger
用户专栏

DevOps方向,微服务方向,包括运维各种层面(Bash,容器,架构等),研发各种层面(开发语言,架构等)

2 人关注
17 篇文章
专栏主页