今天我们来一起来学习一下golang一个第三方进程管理工具goreman
其功能和supervisor类似,用于管理多个进程
github地址如下:
https://github.com/mattn/goreman
一、命令行参数
查看所有参数,最直接方法是
goreman help
常用命令
goreman start //启动所有进程
goreman run start COMMAND //启动一个进程
goreman run stop COMMAND //停止一个进程
goreman run list //查看goreman运行哪些进程
goreman run status //查看进程状态,带*的是运行中
goreman run restart/restart-all/stop/stop-all
goreman启动时依赖的配置文件是Procfile,来看看这个文件长什么样
web1: ./server -addr :9000
web2: ./server -addr :9001
web1是为进程起的名字,其后是该进程启动命令
编写一个简单的示例server
package main
import (
"fmt"
"net/http"
"flag"
)
var (
Addr string
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
}
func main() {
http.HandleFunc("/", indexHandler)
http.ListenAndServe(Addr, nil)
}
func init() {
flag.StringVar(&Addr, "addr", ":8088", "addr")
flag.Parse()
}
程序接受一个参数:地址,作为httpserver的启动地址,提供服务,输出hello world
启动goreman
$ goreman start
15:07:20 web1 | Starting web1 on port 5000
15:07:20 web2 | Starting web2 on port 5100
15:07:20 web1 | :9000
15:07:20 web2 | :9001
问题:
进程启动时,Starting web1 on port 5000是什么意思?
命令行参数中有一个可选参数port是什么port?
内部是如何启动各个进程的?
stop某个进程时是如何操作的?
goreman run status命令是怎么检测各个进程状态的?
带着这些问题,读源码一探究竟
二、整体逻辑梳理
goreman代码结构比较简单
主要逻辑:goreman.go
进程相关:proc.go
rpc定义:rpc.go
2.1 关键结构
type config struct {
Procfile string `yaml:"procfile"`
// Port for RPC server
Port uint `yaml:"port"` // rpc端口号
BaseDir string `yaml:"basedir"`
BasePort uint `yaml:"baseport"`
...
}
// -- process information structure.
type procInfo struct {
name string // 进程name
cmdline string // 进程命令行
cmd *exec.Cmd
port uint // 启动端口
setPort bool
colorIndex int // 命令编号
...
}
2.2 主要逻辑
main()
readConfig() // 读取并初始化配置(Procfile、port、baseport等)
start()
readProcfile() // 解析procfile文件,把进程名和启动命令分别解析为k,v
go startServer() // 在port端口启动一个rpc server,接收rpc请求,默认端口号8555
startProcs() // 遍历所有procs命令,在routine里启动一个个进程
//接受终止Ctrl+c命令或者stop命令退出
2.3 rpc命令处理
以stop命令为例:当执行goremon run stop web1命令时,实际调用的是goremon的对应的rpc stop方法,找到proc.cmd.Process的pid对应的进程,给进程发送终止信号os.Interrupt,从而结束进程
func run(cmd string, args []string, serverPort uint) error {
client, err := rpc.Dial("tcp", defaultServer(serverPort))
if err != nil {
return err
}
defer client.Close()
var ret string
switch cmd {
case "start":
return client.Call("Goreman.Start", args, &ret)
case "stop":
return client.Call("Goreman.Stop", args, &ret)
case "stop-all":
return client.Call("Goreman.StopAll", args, &ret)
case "restart":
return client.Call("Goreman.Restart", args, &ret)
case "restart-all":
return client.Call("Goreman.RestartAll", args, &ret)
case "list":
err := client.Call("Goreman.List", args, &ret)
fmt.Print(ret)
return err
case "status":
err := client.Call("Goreman.Status", args, &ret)
fmt.Print(ret)
return err
}
return errors.New("unknown command")
}
// Stop do stop
func (r *Goreman) Stop(args []string, ret *string) (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
errChan := make(chan error, 1)
r.rpcChan <- &rpcMessage{
Msg: "stop",
Args: args,
ErrCh: errChan,
}
err = <-errChan
return
}
// 主进程从channel里读取rpcMsg处理,目前看只支持stop命令
// 其他命令实现是直接调用方法来处理,比如start web1,直接调用startProc方法
select {
case rpcMsg := <-rpcCh:
switch rpcMsg.Msg {
case "stop":
for _, proc := range rpcMsg.Args {
if err := stopProc(proc, nil); err != nil {
rpcMsg.ErrCh <- err
break
}
}
close(rpcMsg.ErrCh)
default:
panic("unimplemented rpc message type " + rpcMsg.Msg)
}
...
}
func terminateProc(proc *procInfo, signal os.Signal) error {
p := proc.cmd.Process
if p == nil {
return nil
}
pgid, err := unix.Getpgid(p.Pid)
if err != nil {
return err
}
pid := p.Pid
if pgid == p.Pid {
pid = -1 * pid
}
target, err := os.FindProcess(pid)
if err != nil {
return err
}
// 给process发送signal
return target.Signal(signal)
}
2.4 再看看start命令是怎么实现的
// 直接调用startProc方法,创建进程,这个方法也是goremon程序启动时调用的方法
func (r *Goreman) Start(args []string, ret *string) (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
for _, arg := range args {
if err = startProc(arg, nil, nil); err != nil {
break
}
}
return err
}
三、问题解答
进程启动时,Starting web1 on port 5000是什么意思?
每个进程的port env环境变量(暂时没发现有什么用)
命令行参数中有一个可选参数port是什么port?
rpc服务监听的端口号。
内部是如何启动各个进程的?
见2.2部分。
stop某个进程时是如何操作的?
见2.3部分。
goreman run status命令是怎么检测各个进程状态的?
通过判断procs里存储的cmd是否为nil来判断进程状态,如果proc.cmd不为nil,就加个*标识运行中;如果进程被stop了,那么cmd为nil
func (r *Goreman) Status(args []string, ret *string) (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
*ret = ""
for _, proc := range procs {
if proc.cmd != nil {
*ret += "*" + proc.name + "\n"
} else {
*ret += " " + proc.name + "\n"
}
}
return err
}
$ goreman run status
*web1
*web2
$ goreman run stop web1
$ goreman run status
web1
*web2
总结:
相较于linux的supervisor工具,goremon更简单易用,管理进程
文章来自微信专栏:迷人的少侠