|
|
# 24|管理进程:如何设计完善的运行命令?
|
|
|
|
|
|
你好,我是轩脉刃。
|
|
|
|
|
|
在[第13章](https://time.geekbang.org/column/article/426765)我们引入命令行的时候,将Web启动方式改成了一个命令行。但是当时只完成了一个最简单的启动Web服务的命令,这节课,我们要做的是完善这个Web服务运行命令,让Web服务的运行有完整的启动、停止、重启、查询的进程管理功能。
|
|
|
|
|
|
这套完整的进程管理功能,能让应用管理者非常方便地通过一套命令来统一管控一个应用,降低应用管理者的管理成本,后续也能为实现应用自动化部署到远端服务的工具提供了基础。下面我们来具体看下如何设计这套命令并且实现它吧。
|
|
|
|
|
|
### 运行命令的设计
|
|
|
|
|
|
首先照惯例需要设计一下运行命令,一级命令为 app,二级命令设计如下:
|
|
|
|
|
|
* `./hade app start` 二级命令,启动一个app服务
|
|
|
* `./hade app state` 二级命令,获取启动的app的信息
|
|
|
* `./hade app stop` 二级命令,停止已经启动的app服务
|
|
|
* `./hade app restart` 二级命令,重新启动一个app服务
|
|
|
|
|
|
这四个二级命令,有app服务的启动、停止、重启、查询,基本上已经把一个app服务启动的状态变更都包含了,能基本满足后面我们对于一个应用的管理需求。下面来讨论下每个命令的功能和设计。
|
|
|
|
|
|
### 启动命令
|
|
|
|
|
|
首先是start这个命令,写在framework/command/app.go中。我们先分析下参数。
|
|
|
|
|
|
想要启动app服务,至少需要一个参数,就是**启动服务的监听地址**。如何获取呢?首先可以直接从默认配置获取,另外因为这是一个控制台命令,也一定可以直接从命令行获取。除了这两种方式,我们回顾下之前的配置项获取方法,还有环境变量和配置项。
|
|
|
|
|
|
所以总结起来,环境变量这个参数我们设计为有四个方式可以获取,一个是直接从命令行参数获取address参数,二是从环境变量ADDRESS中获取,然后是从配置文件中获取配置项app.address,最后如果以上三个方式都没有设置,就使用默认值:8888。关键的代码逻辑如下:
|
|
|
|
|
|
```go
|
|
|
if appAddress == "" {
|
|
|
envService := container.MustMake(contract.EnvKey).(contract.Env)
|
|
|
if envService.Get("ADDRESS") != "" {
|
|
|
appAddress = envService.Get("ADDRESS")
|
|
|
} else {
|
|
|
configService := container.MustMake(contract.ConfigKey).(contract.Config)
|
|
|
if configService.IsExist("app.address") {
|
|
|
appAddress = configService.GetString("app.address")
|
|
|
} else {
|
|
|
appAddress = ":8888"
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
除了监听地址的参数,回忆之前cron命令运行的时候,启动app服务,我们是有两种启动方式的,一种是启动后直接挂在控制台,这种启动方式适合调试开发使用;而另外一种,以守护进程daemon的方式启动,直接挂载在后台。所以,对于这两种启动方式,我们也需要有一个参数daemon,标记是使用哪种方式启动。
|
|
|
|
|
|
有了appAddress、daemon这两个参数,我们顺着继续想**启动服务时需要的记录文件**。
|
|
|
|
|
|
不管是使用挂载方式,还是daemon方式启动进程,都能获取到一个进程PID,启动app服务的时候,要将这个PID记录在一个文件中,这里我们就存储在 app/storage/runtime/app.pid 文件中。在运行时候,需要保证这个目录和文件是存在的。
|
|
|
|
|
|
同时也会产生日志,日志存放在app/storage/log/app.log中,所以我们要确认这个目录是否存在。
|
|
|
|
|
|
关于app.pid和app.log对应的代码:
|
|
|
|
|
|
```go
|
|
|
appService := container.MustMake(contract.AppKey).(contract.App)
|
|
|
|
|
|
pidFolder := appService.RuntimeFolder()
|
|
|
if !util.Exists(pidFolder) {
|
|
|
if err := os.MkdirAll(pidFolder, os.ModePerm); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
serverPidFile := filepath.Join(pidFolder, "app.pid")
|
|
|
logFolder := appService.LogFolder()
|
|
|
if !util.Exists(logFolder) {
|
|
|
if err := os.MkdirAll(logFolder, os.ModePerm); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
// 应用日志
|
|
|
serverLogFile := filepath.Join(logFolder, "app.log")
|
|
|
currentFolder := util.GetExecDirectory()
|
|
|
|
|
|
```
|
|
|
|
|
|
好到这里,准备工作都做好了,我们看看Web服务的启动,逻辑和之前设计的基本上没有什么区别,使用net/http来启动一个Web服务。
|
|
|
|
|
|
**重点是启动的时候注意设置优雅关闭机制**。先使用[第六章](https://time.geekbang.org/column/article/421354)实现的优雅关闭机制:开启一个Goroutine启动服务,主Goroutine监听信号,当获取到信号之后,等待所有请求都结束或者超过最长等待时长,就结束信号。当然,这里的最长等待时长可以设置为配置项,从app.close\_wait配置项中获取,如果没有配置项,我们默认使用5s的最长等待时长。
|
|
|
|
|
|
启动相关代码:
|
|
|
|
|
|
```go
|
|
|
// 启动AppServer, 这个函数会将当前goroutine阻塞
|
|
|
func startAppServe(server *http.Server, c framework.Container) error {
|
|
|
// 这个goroutine是启动服务的goroutine
|
|
|
go func() {
|
|
|
server.ListenAndServe()
|
|
|
}()
|
|
|
|
|
|
// 当前的goroutine等待信号量
|
|
|
quit := make(chan os.Signal)
|
|
|
// 监控信号:SIGINT, SIGTERM, SIGQUIT
|
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
|
|
// 这里会阻塞当前goroutine等待信号
|
|
|
<-quit
|
|
|
|
|
|
// 调用Server.Shutdown graceful结束
|
|
|
closeWait := 5
|
|
|
configService := c.MustMake(contract.ConfigKey).(contract.Config)
|
|
|
if configService.IsExist("app.close_wait") {
|
|
|
closeWait = configService.GetInt("app.close_wait")
|
|
|
}
|
|
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(closeWait)*time.Second)
|
|
|
defer cancel()
|
|
|
|
|
|
if err := server.Shutdown(timeoutCtx); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
但是这里还出现了一个问题,挂在控制台的启动,比较简单,直接调用封装好的 startAppServe 就行了。但daemon方式如何启动呢?它是不能直接在主进程中调用startAppServe方法的,会把主进程给阻塞挂起来了,怎么办呢?
|
|
|
|
|
|
这个其实在[第十四章](https://time.geekbang.org/column/article/427090)定时任务中有说到,我们可以使用和定时任务一样的实现机制,使用开源库 [go-daemon](https://github.com/sevlyar/go-daemon)。比较重要,所以这里再啰嗦一下,**理解go-daemon库的使用,要理解最核心的daemon.Context结构**。
|
|
|
|
|
|
在我们框架这个需求中,daemon方式启动命令为 `./hade app start --daemon=true` 。所以在daemon.Context结构中的Args参数填写如下:
|
|
|
|
|
|
```go
|
|
|
// 创建一个Context
|
|
|
cntxt := &daemon.Context{
|
|
|
...
|
|
|
// 子进程的参数,按照这个参数设置,子进程的命令为 ./hade app start --daemon=true
|
|
|
Args: []string{"", "app", "start", "--daemon=true"},
|
|
|
}
|
|
|
// 启动子进程,d不为空表示当前是父进程,d为空表示当前是子进程
|
|
|
d, err := cntxt.Reborn()
|
|
|
|
|
|
if d != nil {
|
|
|
// 父进程直接打印启动成功信息,不做任何操作
|
|
|
fmt.Println("app启动成功,pid:", d.Pid)
|
|
|
fmt.Println("日志文件:", serverLogFile)
|
|
|
return nil
|
|
|
}
|
|
|
...
|
|
|
|
|
|
```
|
|
|
|
|
|
有的同学对这个启动子进程的Reborn可能有些疑惑。
|
|
|
|
|
|
我们把Reborn理解成fork,当调用这个函数的时候,父进程会继续往下走,但是返回值d不为空,它的信息是子进程的进程号等信息。而子进程会重新运行对应的命令,再次进入到Reborn函数的时候,返回的d就为nil。所以**在Reborn的后面,我们让父进程直接return,而让子进程继续往后进行操作,这样就达到了fork一个子进程的效果了**。
|
|
|
|
|
|
理解了这一点,对应的代码就很简单了:
|
|
|
|
|
|
```go
|
|
|
// daemon 模式
|
|
|
if appDaemon {
|
|
|
// 创建一个Context
|
|
|
cntxt := &daemon.Context{
|
|
|
// 设置pid文件
|
|
|
PidFileName: serverPidFile,
|
|
|
PidFilePerm: 0664,
|
|
|
// 设置日志文件
|
|
|
LogFileName: serverLogFile,
|
|
|
LogFilePerm: 0640,
|
|
|
// 设置工作路径
|
|
|
WorkDir: currentFolder,
|
|
|
// 设置所有设置文件的mask,默认为750
|
|
|
Umask: 027,
|
|
|
// 子进程的参数,按照这个参数设置,子进程的命令为 ./hade app start --daemon=true
|
|
|
Args: []string{"", "app", "start", "--daemon=true"},
|
|
|
}
|
|
|
// 启动子进程,d不为空表示当前是父进程,d为空表示当前是子进程
|
|
|
d, err := cntxt.Reborn()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
if d != nil {
|
|
|
// 父进程直接打印启动成功信息,不做任何操作
|
|
|
fmt.Println("app启动成功,pid:", d.Pid)
|
|
|
fmt.Println("日志文件:", serverLogFile)
|
|
|
return nil
|
|
|
}
|
|
|
defer cntxt.Release()
|
|
|
// 子进程执行真正的app启动操作
|
|
|
fmt.Println("deamon started")
|
|
|
gspt.SetProcTitle("hade app")
|
|
|
if err := startAppServe(server, container); err != nil {
|
|
|
fmt.Println(err)
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
到这里服务的进程启动成功,最后还有一点细节,对于启动的进程,我们一般都希望能自定义它的进程名称。
|
|
|
|
|
|
这里可以使用一个第三方库 [gspt](https://github.com/erikdubbelboer/gspt)。它使用MIT协议,虽然star数不多,但是我个人亲测是功能齐全且有效的。在Golang中没有现成的设置进程名称的方法,只能调用C的设置进程名称的方法 setproctitle。所以这个库使用的方式是,使用cgo从Go中调用C的方法来实现进程名称的修改。
|
|
|
|
|
|
它的使用非常简单,就是一个函数SetProcTitle方法:
|
|
|
|
|
|
```go
|
|
|
gspt.SetProcTitle("hade app")
|
|
|
|
|
|
```
|
|
|
|
|
|
现在,进程的启动就基本完成了。当然最后还有非常重要的关闭逻辑也记得加上。
|
|
|
|
|
|
好了,以上我们讨论了start的关键设计,再回头梳理一遍这个命令的实现步骤:
|
|
|
|
|
|
* 从四个方式获取参数appAddress
|
|
|
* 获取参数daemon
|
|
|
* 确认runtime目录和PID文件存在
|
|
|
* 确认log目录的log文件存在
|
|
|
* 判断是否是daemon方式。如果是,就使用go-daemon来启动一个子进程;如果不是,直接进行后续调用
|
|
|
* 使用gspt来设置当前进程名称
|
|
|
* 启动app服务
|
|
|
|
|
|
具体的实现步骤相信你已经很清楚了,完整代码我们写在 [framework/command/app.go](https://github.com/gohade/coredemo/blob/geekbang/24/framework/command/app.go)中了。
|
|
|
|
|
|
### 获取进程
|
|
|
|
|
|
已经完成了启动进程的命令,那么第二个获取进程PID的命令就非常简单了。因为启动命令的时候创建了一个PID文件,app/storage/runtime/app.pid,读取这个文件就可以获取到进程的PID信息了。
|
|
|
|
|
|
但是这里我们可以更谨慎一些加一步,获取到PID之后,去操作系统中查询这个PID的进程是否存在,存在的话,就确定这个PID是可行的。
|
|
|
|
|
|
如何根据PID查询一个进程是否存在呢?常用的比如Linux的ps和grep命令,基本上都是通过Linux的其他命令来检查输出,**但最为可靠的方式是直接使用信号对接要查询的进程:通过给进程发送信号来检测,这个信号就是信号0**。
|
|
|
|
|
|
给进程发送信号0之后什么都不会操作,如果进程存在,不返回错误信息;如果进程不存在,会返回不存在进程的错误信息。在Golang中,我们可以用os库的Process结构来发送信号。
|
|
|
|
|
|
代码在 framework/util/exec.go 中,逻辑也很清晰,先用os.FindProcess来获取这个PID对应的进程,然后给进程发送signal 0, 如果返回nil,代表进程存在,否则进程不存在。
|
|
|
|
|
|
```go
|
|
|
// CheckProcessExist 检查进程pid是否存在,如果存在的话,返回true
|
|
|
func CheckProcessExist(pid int) bool {
|
|
|
// 查询这个pid
|
|
|
process, err := os.FindProcess(pid)
|
|
|
if err != nil {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 给进程发送signal 0, 如果返回nil,代表进程存在, 否则进程不存在
|
|
|
err = process.Signal(syscall.Signal(0))
|
|
|
if err != nil {
|
|
|
return false
|
|
|
}
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
这个关键函数实现之后,其他的就很容易了。
|
|
|
|
|
|
这里我们也简单说一下进程获取的具体步骤:获取PID文件内容之后,做判断,如果有PID文件且有内容就继续,否则返回无进程;然后:
|
|
|
|
|
|
* 将内容转换为PID的int类型,转换失败视为无进程;
|
|
|
* **使用signal 0 确认这个进程是否存在,存在返回结果有进程,不存在返回结构无进程**。
|
|
|
|
|
|
具体代码如下,存放在 framework/command/app.go文件中:
|
|
|
|
|
|
```go
|
|
|
// 获取启动的app的pid
|
|
|
var appStateCommand = &cobra.Command{
|
|
|
Use: "state",
|
|
|
Short: "获取启动的app的pid",
|
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
|
container := c.GetContainer()
|
|
|
appService := container.MustMake(contract.AppKey).(contract.App)
|
|
|
|
|
|
// 获取pid
|
|
|
serverPidFile := filepath.Join(appService.RuntimeFolder(), "app.pid")
|
|
|
|
|
|
content, err := ioutil.ReadFile(serverPidFile)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
if content != nil && len(content) > 0 {
|
|
|
pid, err := strconv.Atoi(string(content))
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
if util.CheckProcessExist(pid) {
|
|
|
fmt.Println("app服务已经启动, pid:", pid)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
fmt.Println("没有app服务存在")
|
|
|
return nil
|
|
|
},
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
### 停止命令
|
|
|
|
|
|
命令的启动和获取完成了,就到了第三个停止命令了。既然有了进程号,需要停止一个进程,我们还是可以使用第六章说的信号量方法,回顾下当时说的四个关闭信号:
|
|
|
![](https://static001.geekbang.org/resource/image/e9/50/e93163afd641b744b2b3f8faf46f4e50.jpg?wh=1920x1080)
|
|
|
|
|
|
由于启动进程监听了SIGINT、SIGQUIT、SIGTERM 这三个信号,所以我们在这三个信号中选取一个发送给PID所在的进程即可,这里就选择更符合“关闭”语义的SIGTERM信号。
|
|
|
|
|
|
同样实现步骤也很清晰,获取PID文件内容之后,判断如果有PID文件且有内容再继续,否则什么都不做,之后就是:
|
|
|
|
|
|
* 将内容转换为PID的int类型,转换失败则什么都不做
|
|
|
* 直接给这个PID进程发送SIGTERM信号
|
|
|
* 将PID文件内容清空
|
|
|
|
|
|
对应代码同样在framework/command/app.go中:
|
|
|
|
|
|
```go
|
|
|
// 停止一个已经启动的app服务
|
|
|
var appStopCommand = &cobra.Command{
|
|
|
Use: "stop",
|
|
|
Short: "停止一个已经启动的app服务",
|
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
|
container := c.GetContainer()
|
|
|
appService := container.MustMake(contract.AppKey).(contract.App)
|
|
|
|
|
|
// GetPid
|
|
|
serverPidFile := filepath.Join(appService.RuntimeFolder(), "app.pid")
|
|
|
|
|
|
content, err := ioutil.ReadFile(serverPidFile)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
if content != nil && len(content) != 0 {
|
|
|
pid, err := strconv.Atoi(string(content))
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
// 发送SIGTERM命令
|
|
|
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
if err := ioutil.WriteFile(serverPidFile, []byte{}, 0644); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
fmt.Println("停止进程:", pid)
|
|
|
}
|
|
|
return nil
|
|
|
},
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
### 重启命令
|
|
|
|
|
|
最后我们要完成重启命令,还是在framework/command/app.go中。大致逻辑也很清晰,读取PID文件之后判断,如果PID文件中没有PID,说明没有进程在运行,直接启动新进程;如果PID文件中有PID,检查旧进程是否存在,如果不存在,直接启动新进程,如果存在,这里就有一些需要注意的了。
|
|
|
|
|
|
```go
|
|
|
//获取pid
|
|
|
...
|
|
|
|
|
|
if content != nil && len(content) != 0 {
|
|
|
// 解析pid是否存在
|
|
|
if util.CheckProcessExist(pid) {
|
|
|
// 关闭旧的pid进程
|
|
|
...
|
|
|
}
|
|
|
}
|
|
|
|
|
|
appDaemon = true
|
|
|
// 启动新的进程
|
|
|
return appStartCommand.RunE(c, args)
|
|
|
|
|
|
```
|
|
|
|
|
|
因为重启的逻辑是先结束旧进程,再启动新进程。结束进程和停止命令一样,使用SIGTERM信号就能保证进程的优雅关闭了。但是**由于新、旧进程都是使用同一个端口,所以必须保证旧进程结束,才能启动新的进程**。
|
|
|
|
|
|
而怎么保证旧进程确实结束了呢?
|
|
|
|
|
|
这里可以使用前面定义的 CheckProcessExist 方法,每秒做一次轮询,检测PID对应的进程是否已经关闭。那么轮询多少次呢?
|
|
|
|
|
|
我们知道在启动进程的时候,设置了一个优雅关闭的最大超时时间closeWait,这个closeWait的时间设置为秒。那么**为了轮询检查旧进程是否关闭,我们只需要设置次数超过closeWait的轮询时间即可**。考虑到net/http 在closeWait之后还有一些程序运行的逻辑,这里我们可以设置为2 \* closeWait,时间是非常充裕的。关键代码如下:
|
|
|
|
|
|
```go
|
|
|
// 确认进程已经关闭,每秒检测一次, 最多检测closeWait * 2秒
|
|
|
for i := 0; i < closeWait*2; i++ {
|
|
|
if util.CheckProcessExist(pid) == false {
|
|
|
break
|
|
|
}
|
|
|
time.Sleep(1 * time.Second)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
再严谨一些,可以这么设置,如果在2\*closeWait时间内,旧进程还未关闭,那么就不能启动新进程了,需要直接返回错误。所以,在 2 \* closeWait 轮询之后,我们还需要再做一次检查,检查进程是否关闭,如果没有关闭的话,直接返回error:
|
|
|
|
|
|
```go
|
|
|
// 确认进程已经关闭,每秒检测一次, 最多检测closeWait * 2秒
|
|
|
for i := 0; i < closeWait*2; i++ {
|
|
|
if util.CheckProcessExist(pid) == false {
|
|
|
break
|
|
|
}
|
|
|
time.Sleep(1 * time.Second)
|
|
|
}
|
|
|
|
|
|
// 如果进程等待了2*closeWait之后还没结束,返回错误,不进行后续的操作
|
|
|
if util.CheckProcessExist(pid) == true {
|
|
|
fmt.Println("结束进程失败:"+strconv.Itoa(pid), "请查看原因")
|
|
|
return errors.New("结束进程失败")
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
在确认旧进程结束后,记得把PID文件清空,再启动一个新进程。启动进程的逻辑还是比较复杂的,就不重复写了,我们直接调用appStartCommand的RunE方法来实现,会更优雅一些。
|
|
|
|
|
|
同其他命令一样,这里再梳理一下判断旧进程存在之后详细的实现步骤,如果存在:
|
|
|
|
|
|
* 发送SIGTERM信号
|
|
|
* 循环2\*closeWait次数,每秒执行一次查询进程是否已经结束
|
|
|
* 如果某次查询进程已经结束,或者等待2\*closeWait循环结束之后,再次查询一次进程
|
|
|
* 如果还未结束,返回进程结束失败
|
|
|
* 如果已经结束,将PID文件清空,启动新进程
|
|
|
|
|
|
在framework/command/app.go中,整体代码如下:
|
|
|
|
|
|
```go
|
|
|
// 重新启动一个app服务
|
|
|
var appRestartCommand = &cobra.Command{
|
|
|
Use: "restart",
|
|
|
Short: "重新启动一个app服务",
|
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
|
container := c.GetContainer()
|
|
|
appService := container.MustMake(contract.AppKey).(contract.App)
|
|
|
|
|
|
// GetPid
|
|
|
serverPidFile := filepath.Join(appService.RuntimeFolder(), "app.pid")
|
|
|
|
|
|
content, err := ioutil.ReadFile(serverPidFile)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
if content != nil && len(content) != 0 {
|
|
|
pid, err := strconv.Atoi(string(content))
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
if util.CheckProcessExist(pid) {
|
|
|
// 杀死进程
|
|
|
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
if err := ioutil.WriteFile(serverPidFile, []byte{}, 0644); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// 获取closeWait
|
|
|
closeWait := 5
|
|
|
configService := container.MustMake(contract.ConfigKey).(contract.Config)
|
|
|
if configService.IsExist("app.close_wait") {
|
|
|
closeWait = configService.GetInt("app.close_wait")
|
|
|
}
|
|
|
|
|
|
// 确认进程已经关闭,每秒检测一次, 最多检测closeWait * 2秒
|
|
|
for i := 0; i < closeWait*2; i++ {
|
|
|
if util.CheckProcessExist(pid) == false {
|
|
|
break
|
|
|
}
|
|
|
time.Sleep(1 * time.Second)
|
|
|
}
|
|
|
|
|
|
// 如果进程等待了2*closeWait之后还没结束,返回错误,不进行后续的操作
|
|
|
if util.CheckProcessExist(pid) == true {
|
|
|
fmt.Println("结束进程失败:"+strconv.Itoa(pid), "请查看原因")
|
|
|
return errors.New("结束进程失败")
|
|
|
}
|
|
|
|
|
|
fmt.Println("结束进程成功:" + strconv.Itoa(pid))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
appDaemon = true
|
|
|
// 直接daemon方式启动apps
|
|
|
return appStartCommand.RunE(c, args)
|
|
|
},
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
### 测试
|
|
|
|
|
|
下面来测试一下。首先记得使用 `./hade build sef` 命令编译,我们设置的默认服务启动地址为 “:8888”,这里就不用这个默认启动地址,使用环境变量ADDRESS=:8080 来启动服务。这样能测试到环境变量是否能生效。
|
|
|
|
|
|
调用命令 `ADDRESS=:8080 ./hade app start --daemon=true` 以daemon方式启动一个8080端口的服务:
|
|
|
![](https://static001.geekbang.org/resource/image/5d/a4/5dc59ecb06330bd5097c79fc2040e6a4.png?wh=1316x150)
|
|
|
|
|
|
使用浏览器打开 localhost:8080/demo/demo:
|
|
|
![](https://static001.geekbang.org/resource/image/aa/97/aa16b16c741b837d94682664d1c0yy97.png?wh=1458x372)
|
|
|
|
|
|
服务启动成功,且正常提供服务。
|
|
|
|
|
|
使用 `./hade app state` 查看进程状态:
|
|
|
![](https://static001.geekbang.org/resource/image/eb/ee/eb02d0c5f341cbc3632037a6f119dcee.png?wh=922x98)
|
|
|
使用命令 `ADDRESS=:8080 ./hade app restart` 重新启动进程:
|
|
|
![](https://static001.geekbang.org/resource/image/2a/c4/2a1b9e077e357364529e43b786e943c4.png?wh=1152x194)
|
|
|
|
|
|
再次访问浏览器 localhost:8080/demo/demo,正常提供服务:
|
|
|
![](https://static001.geekbang.org/resource/image/95/f4/95a1ce5d43a1bf28133a75f773341cf4.png?wh=804x280)
|
|
|
最后调用停止进程命令 `./hade app stop` :
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/0f/40/0fb19b39412d970ccb72e43635830040.png?wh=964x100)
|
|
|
|
|
|
到这里,对进程的启动、关闭、查询和重启的命令就验证完成了。
|
|
|
|
|
|
今天我们的所有代码都保存在GitHub上的[geekbang/24](https://github.com/gohade/coredemo/tree/geekbang/24)分支了。只修改了framework/command/app.go 和 framework/util/exec.go文件,其他保持不变。
|
|
|
![](https://static001.geekbang.org/resource/image/c1/6b/c137079e767c7fb3a4e1bd2292b6ca6b.png?wh=584x1626)
|
|
|
|
|
|
### 小结
|
|
|
|
|
|
今天我们完成了运行app相关的命令,包括app一级命令和四个二级命令,启动app服务、停止app服务、重启app服务、查询app服务。基本上已经把一个app服务启动的状态变更都包含了。有了这些命令,我们对app的控制就方便很多了。特别是daemon运行模式,为线上运行提供了不少方便。
|
|
|
|
|
|
在实现这四个命令的过程中,我们使用了不少第三方库,gspt、go-daemon,这些库的使用你要能熟练掌握,特别是go-daemon库,我们已经不止一次使用到它了。确认一个进程是否已经结束,我们使用每秒做一次轮询的 CheckProcessExist 方法实现了检查机制,并仔细考虑了轮训的次数和效果,你可以多多体会这么设计的好处。
|
|
|
|
|
|
### 思考题
|
|
|
|
|
|
我们在启动应用的时候,使用的地址格式为“:8080”,其实这里也可以为“localhost:8080”、“127.0.0.1:8080”或者“10.11.22.33:8080”(10.11.22.33为本机绑定的IP)。你了解localhost、127.0.0.1、10.11.22.33 以及不填写IP的区别么?
|
|
|
|
|
|
欢迎在留言区分享你的思考。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。我们下节课见~
|
|
|
|