这两天我把在公司产品中用于无间断更新服务器(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 -SIGUSR1
或 kill -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 变得更好用。