这两天我把在公司产品中用于无间断更新服务器(update binary) 的代码重构成一个名为 Octopus 的开源项目。这是自去年8月份学习GO语言以来,我的第一个GO语言开源项目。之所以把它命名为 Octopus 大概是觉得它再生能力强,生命力旺盛吧。其寓意也是希望有Octopus加持的服务器是稳定可靠的。

Octopus 很大程度上受了“Gracefully Restarting a Go Program Without Downtime” 这篇文章的启发。所以,大部分credits理应给予这篇文章的作者 Russell Jones。

Octopus 的主要功能可以概括为以下两点:

  • 可以在不阻断用户请求的前提下,“优雅”地更新服务器(代码);
  • 老服务器在“退休”的过程中,还有一段时间可以消化用户请求。这给新服务器的启用争取了一点时间。

Octopus 提供两种“新老交替”的模式,通过一个名为 killMaster 的参数来控制:

  • killMaster = true 时,在成功fork出一个child进程后,master(也叫 parent) 进程会被“杀死”;
  • killMaster = false 时,在成功fork出一个child进程后,master 会被暂时留着活口。这时,用户的请求会以 round-robin 的方式被分配给其中一个进程。

Octopus 提供了 GracefulServe() 和 GracefulServeTLS() 两个方法。前者用户启动一个HTTP服务,而后者用于启动一个HTTPS服务。用户只需要传入一个 http.Server 类型的对象即可启动服务器。So easy对吧。让我们看一个完整的例子,来了解Octopus是如何使用的。

范例 – 如何创建一个优雅的 HTTP Server

创建

首先我们来创建一个简单的HTTP Server, simpleHTTPServer.go,他只提供了一个叫 ping 的 API。用户可以通过 http://<host>:<port>/ping 的方式来访问,服务器会回一个‘pong’。

package main

import (
    "flag"
    "fmt"
    "github.com/NBCFB/Octopus"
    "github.com/go-chi/chi"
    "net/http"
    "time"
)

func ping (w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("pong\n"))
}

func main() {
    var host string
    var port int

    // Handle command line flag
    flag.StringVar(&host, "h", "localhost", "specify host")
    flag.IntVar(&port, "p", 8080, "specify port")
    flag.Parse()

    // Make up addr with default settings
    addr := fmt.Sprintf("%s:%d", host, port)

    // Set up router
    r := chi.NewRouter()
    r.Get("/ping", ping)

    s := &http.Server{
        Addr: addr,
        WriteTimeout: time.Second * 15,
        ReadTimeout:  time.Second * 15,
        IdleTimeout:  time.Second * 60,
        Handler:      r,
    }

    // Start server
    Octopus.GracefulServe(s, false)
}

现在,我们通过 go run启动它:

$ go run test_servers/simpleHTTPServer.go -h 172.18.1.239 -p 8080 &
[1] 52215
$ 2019/04/11 16:09:12 [INFO] Created a new listener on 172.18.1.239:8080.
2019/04/11 16:09:12 [INFO] The server has started (52233).

我们可以看到,服务器已经启动,并开始监听来自 172.18.1.239:8080 的请求。进程ID为52233。我们可以通过 curl来查看服务器的API是否能正常使用:

$ curl http://172.18.1.239:8080/ping
pong

到这里,服务器看起来是正常的工作的。紧接着,我们就来优雅地更新服务器。

优雅地更新服务器

我们对服务器代码做些微调,把 ping 中返回的文本改成 pong pong

func ping (w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("pong pong\n"))
}

然后,我们用 go build 来编译生成新的可执行文件,并覆盖原有的服务器可执行文件。以前通常的做法是走一个“stop-and-start”的流程,即,我们先把服务停了,然后以迅雷不及掩耳盗铃之势,把服务启动。如果这个过程中,用户的几个请求被丢弃或出现返回异常了,我们也只能内心默默抱歉着。有了 Octopus 之后,我们可以直接通过 kill -SIGUSR1kill -SIGUSR2 命令fork一个child出来。然后把master杀掉。这样就完成了优雅的更新过程。这个过程中用户的请求也不会被阻断。

现在,我们发送一个 kill -SIGUSR1 命令给处于运行状态的服务器:

$ kill -SIGUSR1 52233
$ 2019/04/11 16:18:11 [INFO] Server (52233) received signal "user defined signal 1".
2019/04/11 16:18:11 [INFO] Forked child (55344).
2019/04/11 16:18:11 [INFO] Master (52233) is still alive.
2019/04/11 16:18:11 [INFO] Unable to import a listener from file: unable to find listener on localhost:8080. Trying to create a new one.
2019/04/11 16:18:11 [INFO] Created a new listener on localhost:8080.
2019/04/11 16:18:11 [INFO] The server has started (55344).

我们可以看到,一个ID为55344的child进程被fork出来了。由于我们并没有把 killMaster 设置成 true,master还活着呢。这时,我们可以看到用户的请求被以round-robin的方式分配给master或child服务进程处理:

$ curl http://172.18.1.239:8080/ping
pong pong
$ curl http://172.18.1.239:8080/ping
pong

这时,我们把master杀掉来完成整个update流程:

$ kill -SIGTERM 52233
$ 2019/04/11 16:24:54 [INFO] Server (52233) received signal "terminated".
2019/04/11 16:24:54 [INFO] Digesting requests will be timed out at 2019-04-11 16:24:59
2019/04/11 16:24:54 [INFO] The server has shut down.

我们可以从上面的输出结果看到,已经到达的服务请求会继续被处理,而所有未完成处理的,都会在2019–04–11 16:24:59以后过期。新来的请求自然会被新的服务接手:

$ curl http://172.18.1.239:8080/ping
pong pong

至此,我们完成了整个优雅地更新服务的过程。

如果大家对 Octopus 这个项目感兴趣,欢迎访问 Octopus 项目站点。我们也欢迎你的加入,让 Octopus 变得更好用。

发表评论

电子邮件地址不会被公开。 必填项已用*标注