Golang学习--平滑重启
在上一篇博客介绍TOML配置的时候,讲到了通过信号通知重载配置。我们在这一篇中介绍下如何的平滑重启server。
与重载配置相同的是我们也需要通过信号来通知server重启,但关键在于平滑重启,如果只是简单的重启,只需要kill掉,然后再拉起即可。平滑重启意味着server升级的时候可以不用停止业务。
我们先来看下Github上有没有相应的库解决这个问题,然后找到了如下三个库:
- facebookgo/grace - Graceful restart & zero downtime deploy for Go servers.
- fvbock/endless - Zero downtime restarts for go servers (Drop in replacement for http.ListenAndServe)
- jpillora/overseer - Monitorable, gracefully restarting, self-upgrading binaries in Go (golang)
我们分别来学习一下,下面只讲解http server的重启。
使用方式
我们来分别使用这三个库来做平滑重启的事情,之后来对比其优缺点。
这三个库的官方都给了相应的例子,例子如下:
但三个库官方的例子不太一致,我们来统一一下:
- grace例子 https://github.com/facebookgo/grace/blob/master/gracedemo/demo.go
- endless例子 https://github.com/fvbock/endless/tree/master/examples
- overseer例子 https://github.com/jpillora/overseer/tree/master/example
我们参考官方的例子分别来写下用来对比的例子:
grace
package main
import (
"time"
"net/http"
"github.com/facebookgo/grace/gracehttp"
)
func main() {
gracehttp.Serve(
&http.Server{Addr: ":5001", Handler: newGraceHandler()},
&http.Server{Addr: ":5002", Handler: newGraceHandler()},
)
}
func newGraceHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/sleep", func(w http.ResponseWriter, r *http.Request) {
duration, err := time.ParseDuration(r.FormValue("duration"))
if err != nil {
http.Error(w, err.Error(), 400)
return
}
time.Sleep(duration)
w.Write([]byte("Hello World"))
})
return mux
}
endless
package main
import (
"log"
"net/http"
"os"
"sync"
"time"
"github.com/fvbock/endless"
"github.com/gorilla/mux"
)
func handler(w http.ResponseWriter, r *http.Request) {
duration, err := time.ParseDuration(r.FormValue("duration"))
if err != nil {
http.Error(w, err.Error(), 400)
return
}
time.Sleep(duration)
w.Write([]byte("Hello World"))
}
func main() {
mux1 := mux.NewRouter()
mux1.HandleFunc("/sleep", handler)
w := sync.WaitGroup{}
w.Add(2)
go func() {
err := endless.ListenAndServe(":5003", mux1)
if err != nil {
log.Println(err)
}
log.Println("Server on 5003 stopped")
w.Done()
}()
go func() {
err := endless.ListenAndServe(":5004", mux1)
if err != nil {
log.Println(err)
}
log.Println("Server on 5004 stopped")
w.Done()
}()
w.Wait()
log.Println("All servers stopped. Exiting.")
os.Exit(0)
}
overseer
package main
import (
"fmt"
"net/http"
"time"
"github.com/jpillora/overseer"
)
//see example.sh for the use-case
// BuildID is compile-time variable
var BuildID = "0"
//convert your 'main()' into a 'prog(state)'
//'prog()' is run in a child process
func prog(state overseer.State) {
fmt.Printf("app#%s (%s) listening...\n", BuildID, state.ID)
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
duration, err := time.ParseDuration(r.FormValue("duration"))
if err != nil {
http.Error(w, err.Error(), 400)
return
}
time.Sleep(duration)
w.Write([]byte("Hello World"))
fmt.Fprintf(w, "app#%s (%s) says hello\n", BuildID, state.ID)
}))
http.Serve(state.Listener, nil)
fmt.Printf("app#%s (%s) exiting...\n", BuildID, state.ID)
}
//then create another 'main' which runs the upgrades
//'main()' is run in the initial process
func main() {
overseer.Run(overseer.Config{
Program: prog,
Addresses: []string{":5005", ":5006"},
//Fetcher: &fetcher.File{Path: "my_app_next"},
Debug: false, //display log of overseer actions
})
}
对比
对比示例的操作步骤
- 分别构建上面的示例,并记录pid
- 调用API,在其未返回时,修改内容(Hello World -> Hello Harry),重新构建。查看旧API是否返回旧的内容
- 调用新API,查看返回的内容是否是新的内容
- 查看当前运行的pid,是否与之前一致
下面给一下操作命令
# 第一次构建项目
go build grace.go
# 运行项目,这时就可以做内容修改了
./grace &
# 请求项目,60s后返回
curl "http://127.0.0.1:5001/sleep?duration=60s" &
# 再次构建项目,这里是新内容
go build grace.go
# 重启,2096为pid
kill -USR2 2096
# 新API请求
curl "http://127.0.0.1:5001/sleep?duration=1s"
# 第一次构建项目
go build endless.go
# 运行项目,这时就可以做内容修改了
./endless &
# 请求项目,60s后返回
curl "http://127.0.0.1:5003/sleep?duration=60s" &
# 再次构建项目,这里是新内容
go build endless.go
# 重启,22072为pid
kill -1 22072
# 新API请求
curl "http://127.0.0.1:5003/sleep?duration=1s"
# 第一次构建项目
go build -ldflags '-X main.BuildID=1' overseer.go
# 运行项目,这时就可以做内容修改了
./overseer &
# 请求项目,60s后返回
curl "http://127.0.0.1:5005/sleep?duration=60s" &
# 再次构建项目,这里是新内容,注意版本号不同了
go build -ldflags '-X main.BuildID=2' overseer.go
# 重启,28300为主进程pid
kill -USR2 28300
# 新API请求
curl "http://127.0.0.1:5005/sleep?duration=1s"
对比结果
| 示例 | 旧API返回值 | 新API返回值 | 旧pid | 新pid | 结论 |
|---|---|---|---|---|---|
| grace | Hello world | Hello Harry | 2096 | 3100 | 旧API不会断掉,会执行原来的逻辑,pid会变化 |
| endless | Hello world | Hello Harry | 22072 | 22365 | 旧API不会断掉,会执行原来的逻辑,pid会变化 |
| overseer | Hello world | Hello Harry | 28300 | 28300 | 旧API不会断掉,会执行原来的逻辑,主进程pid不会变化 |
原理分析
可以看出grace和endless是比较像的。
- 监听信号
- 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程
- 子进程监听父进程的socket,这个时候父进程和子进程都可以接收请求
- 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
- 父进程退出,升级完成
overseer是与grace和endless有些不同,主要是两点:
- overseer添加了Fetcher,当Fetcher返回有效的二进位流(io.Reader) 时,主进程会将它保存到临时位置并验证它,替换当前的二进制文件并启动。
Fetcher运行在一个goroutine中,预先会配置好检查的间隔时间。Fetcher支持File、GitHub、HTTP和S3的方式。详细可查看包package fetcher - overseer添加了一个主进程管理平滑重启。子进程处理连接,能够保持主进程pid不变。
如下图表示的很形象

自己实现
我们下面来尝试自己实现下第一种处理,代码如下,代码来自《热重启golang服务器》:
package main
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
var (
server *http.Server
listener net.Listener
graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)
func sleep(w http.ResponseWriter, r *http.Request) {
duration, err := time.ParseDuration(r.FormValue("duration"))
if err != nil {
http.Error(w, err.Error(), 400)
return
}
time.Sleep(duration)
w.Write([]byte("Hello World"))
}
func main() {
flag.Parse()
http.HandleFunc("/sleep", sleep)
server = &http.Server{Addr: ":5007"}
var err error
if *graceful {
log.Print("main: Listening to existing file descriptor 3.")
// cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
// when we put socket FD at the first entry, it will always be 3(0+3)
f := os.NewFile(3, "")
listener, err = net.FileListener(f)
} else {
log.Print("main: Listening on a new file descriptor.")
listener, err = net.Listen("tcp", server.Addr)
}
if err != nil {
log.Fatalf("listener error: %v", err)
}
go func() {
// server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine
err = server.Serve(listener)
log.Printf("server.Serve err: %v\n", err)
}()
signalHandler()
log.Printf("signal end")
}
func reload() error {
tl, ok := listener.(*net.TCPListener)
if !ok {
return errors.New("listener is not tcp listener")
}
f, err := tl.File()
if err != nil {
return err
}
args := []string{"-graceful"}
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// put socket FD at the first entry
cmd.ExtraFiles = []*os.File{f}
return cmd.Start()
}
func signalHandler() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
for {
sig := <-ch
log.Printf("signal: %v", sig)
// timeout context for shutdown
ctx, _ := context.WithTimeout(context.Background(), 100*time.Second)
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
// stop
log.Printf("stop")
signal.Stop(ch)
server.Shutdown(ctx)
log.Printf("graceful shutdown")
return
case syscall.SIGUSR2:
// reload
log.Printf("reload")
err := reload()
if err != nil {
log.Fatalf("graceful restart error: %v", err)
}
server.Shutdown(ctx)
log.Printf("graceful reload")
return
}
}
}
代码可参考:https://github.com/CraryPrimitiveMan/go-in-action/tree/master/ch4
关于这一部分,个人的理解也不是特别深入,如果又不正确的地方请大家指正。
参考文章
热重启golang服务器
overseer, 進入(golang),Monitorable,正常重新啟動,自升級二進位文件
Golang学习--平滑重启的更多相关文章
- golang 服务平滑重启小结
背景 golang 程序平滑重启框架 supervisor 出现 defunct 原因 使用 master/worker 模式 背景 在业务快速增长中,前期只是验证模式是否可行,初期忽略程序发布重启带 ...
- 【学习笔记】启动Nginx、查看nginx进程、查看nginx服务主进程的方式、Nginx服务可接受的信号、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级
1.启动nginx的方式: cd /usr/local/nginx ls ./nginx -c nginx.conf 2.查看nginx的进程方式: [root@localhost nginx] ...
- Golang学习:sublime text3配置golang环境
最近导师让学习golang, 然后我就找了些有关golang的学习视频和网站. 昨天在电脑上下载了go tools, 之后在sublime上配置了golang的运行环境.By the way, 我的电 ...
- Golang学习系列:(一)介绍和安装
Golang学习系列:(一)介绍和安装 Java程序员带你来到Go的世界,让我们开始探索吧! Go是一种新的语言,一种并发的,带有垃圾回收的.快速编译的语言,它具有一下特点: 他可以在一台计算机上用几 ...
- nginx 平滑重启的实现方法
一.背景 在服务器开发过程中,难免需要重启服务加载新的代码或配置,如果能够保证server重启的过程中服务不间断,那重启对于业务的影响可以降为0.最近调研了一下nginx平滑重启,觉得很有意思,记录下 ...
- beego框架(golang)学习验证码
beego框架(golang)学习验证码 登录页面使用验证码 路由设置 /beego_admin_template/routers/router.go get请求页面, post验证用户名密码和验证码 ...
- Gong服务实现平滑重启分析
平滑重启是指能让我们的程序在重启的过程不中断服务,新老进程无缝衔接,实现零停机时间(Zero-Downtime)部署: 平滑重启是建立在优雅退出的基础之上的,之前一篇文章介绍了相关实现:Golang中 ...
- 【golang学习记录】环境搭建
[golang学习记录]环境搭建 一. 概述 本文是[golang学习记录]系列文章的第一篇,安装Go语言及搭建Go语言开发环境,接下来将详细记录自己学习 go 语言的过程,一方面是为了巩固自己学到的 ...
- yarn关于app max attempt深度解析,针对长服务appmaster平滑重启
在YARN上开发长服务,需要注意fault-tolerance,本篇文章对appmaster的平滑重启的一个参数做了解析,如何设置可以有助于达到appmaster平滑重启. 在yarn-site.xm ...
随机推荐
- Android 音视频编解码——YUV视频格式详解
一.YUV 介绍 YUV是一种颜色编码方方式,通常由彩色摄像机进行取像,然后把取得的彩色图像信号经过分色.分别放大校正后得到RGB,再经过矩阵变换得到亮度信号Y和两个色差信号B-Y(即U).R-Y(即 ...
- MyBatis关联关系
1.一对多:一个国家对应多个城市 01.实体类 package cn.pb.bean;import java.util.Set;/** * 国家的实体类 */public class Country ...
- Linux shell 脚本(一)
一.初识脚本 shell:一类介于系统内核与用户之间的解释程序.脚本:一类使用特定语言,按预设顺序执行的文件批处理.宏.解释型程序创建shell脚本:理清任务过程--整理执行语句--完善文件结构1.任 ...
- 在Simplicity Studio下创建适用于EFR32的工程项目
1.使用平台 使用平台描述了在Simplicity Stdio下创建工程时所使用的操作系统与软件版本等. 操作系统:Windows 10 Simplcity Studio版本:Simlicity St ...
- centos7上安装ELK
author:headsen chen data :2017-12-04 18:00:57 notice:created by chen himself and not allowed to cop ...
- 从JavaScript的事件循环到Promise
JS线程是单线程运行机制,就是自己按顺序做自己的事,浏览器线程用于交互和控制,JS可以操作DOM元素, 说起JS中的异步时,我们需要注意的是,JS中其实有两种异步,一种是基于浏览器的异步IO,比如Aj ...
- spy++捕获窗口消息
打开spy++,窗口截图如下,点击窗口搜索按钮(红框标识) ,如果找不到对应的窗口,鼠标右键刷新即可. 鼠标左键点击窗口搜索图标,按住不放,拖到需要抓取消息的窗口上: spy++会自动在列表中高亮定位 ...
- 笔记:XML-解析文档-流机制解析器(SAX、StAX)
DOM 解析器完整的读入XML文档,然后将其转换成一个树型的数据结构,对于大多数应用,DOM 都运行很好,但是,如果文档很大,并且处理算法又非常简单,可以在运行时解析节点,而不必看到完整的树形结构,那 ...
- 继续吐槽在net下没有合适的Disk Cache之使用EhCache
说起缓存,大家可能口若悬河,各种类型的缓存都能一一分析,但在net下找到一款合适的Disk Cache貌似还是有一点难度的. 一:背景 事情是这样的,最近的一个项目中,需要在web端绘制一些报表,因为 ...
- poj3358 Period of an Infinite Binary Expansion
Period of an Infinite Binary Expansion 题目大意:给你一个分数,求这个分数二进制表示下从第几位开始循环,并求出最小循环节长度. 注释:int范围内. 想法:这题说 ...