在上一篇博客介绍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

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有些不同,主要是两点:

  1. overseer添加了Fetcher,当Fetcher返回有效的二进位流(io.Reader) 时,主进程会将它保存到临时位置并验证它,替换当前的二进制文件并启动。

    Fetcher运行在一个goroutine中,预先会配置好检查的间隔时间。Fetcher支持File、GitHub、HTTP和S3的方式。详细可查看包package fetcher
  2. 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学习--平滑重启的更多相关文章

  1. golang 服务平滑重启小结

    背景 golang 程序平滑重启框架 supervisor 出现 defunct 原因 使用 master/worker 模式 背景 在业务快速增长中,前期只是验证模式是否可行,初期忽略程序发布重启带 ...

  2. 【学习笔记】启动Nginx、查看nginx进程、查看nginx服务主进程的方式、Nginx服务可接受的信号、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级

     1.启动nginx的方式: cd /usr/local/nginx ls ./nginx -c nginx.conf 2.查看nginx的进程方式: [root@localhost nginx] ...

  3. Golang学习:sublime text3配置golang环境

    最近导师让学习golang, 然后我就找了些有关golang的学习视频和网站. 昨天在电脑上下载了go tools, 之后在sublime上配置了golang的运行环境.By the way, 我的电 ...

  4. Golang学习系列:(一)介绍和安装

    Golang学习系列:(一)介绍和安装 Java程序员带你来到Go的世界,让我们开始探索吧! Go是一种新的语言,一种并发的,带有垃圾回收的.快速编译的语言,它具有一下特点: 他可以在一台计算机上用几 ...

  5. nginx 平滑重启的实现方法

    一.背景 在服务器开发过程中,难免需要重启服务加载新的代码或配置,如果能够保证server重启的过程中服务不间断,那重启对于业务的影响可以降为0.最近调研了一下nginx平滑重启,觉得很有意思,记录下 ...

  6. beego框架(golang)学习验证码

    beego框架(golang)学习验证码 登录页面使用验证码 路由设置 /beego_admin_template/routers/router.go get请求页面, post验证用户名密码和验证码 ...

  7. Gong服务实现平滑重启分析

    平滑重启是指能让我们的程序在重启的过程不中断服务,新老进程无缝衔接,实现零停机时间(Zero-Downtime)部署: 平滑重启是建立在优雅退出的基础之上的,之前一篇文章介绍了相关实现:Golang中 ...

  8. 【golang学习记录】环境搭建

    [golang学习记录]环境搭建 一. 概述 本文是[golang学习记录]系列文章的第一篇,安装Go语言及搭建Go语言开发环境,接下来将详细记录自己学习 go 语言的过程,一方面是为了巩固自己学到的 ...

  9. yarn关于app max attempt深度解析,针对长服务appmaster平滑重启

    在YARN上开发长服务,需要注意fault-tolerance,本篇文章对appmaster的平滑重启的一个参数做了解析,如何设置可以有助于达到appmaster平滑重启. 在yarn-site.xm ...

随机推荐

  1. C++中不同变量、函数在内存中的内存情况《转》

    一.一个C++编译的程序占用的内存分为以下几个部分 1.栈区:由编译器自动分配 存放函数的参数值,局部变量的值等,操作方式类似于数据结构中的栈. 2.堆区:一般由程序员分配释放,若程序员不释放,程序结 ...

  2. oracle使用中的一些问题

    一.设置自增主键(假设表的主键名为:company_id) 1)创建序列(company_autoinc): maxvalue start increment nocache; 2)创建触发器(com ...

  3. Handsontable添加超链接

    本文在上文的基础上,返回的数据中多了一个link超链接跳转的字段,,需要在Handsontable中显示超链接. <!DOCTYPE html> <html> <head ...

  4. Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  5. three.js 实现全景以及优化(1)

    实现一个三维全景;  然后思考优化问题; 于是我问了下webgl技术交流群朋友有啥解决方案; 对于krpano.js 的了解,只是知道百度全景用了这个技术; 最后还是选择了群友给出的three.js  ...

  6. python统计词频

    arr = [1,2,3,4,5,6,4,5,2,3,6,8,9,6,5,3,6,2,4]dic={}for item in arr: if item in dic.keys(): dic[item] ...

  7. jquery validate 动态增加删除验证规则

    增加规则示例: $('.class').rules('add',{ required: true, messages:{ required: '这是必填,请填写', } }); 删除规则示例: $(' ...

  8. Ios 若干兼容处理

    最后处理兼容真是各种苦逼,还算好,最后解决了,再此总结一下 position:fixed 和 input 的问题? ios 对position:fixed 的定位处理兼容性不是很好,例如,在同时又in ...

  9. Algorithm --> 爬楼梯求最大分数

    爬楼梯求最大分数 如下图,最大分数是: 10+20+25+20=75.        要求: 1.每次只能走一步或者两步: 2.不能连续三步走一样的,即最多连续走两次一步,或者连续走两次两步: 3.必 ...

  10. KVM之二:配置网络

    1.安装KVM a.通过yum安装虚拟化的软件包 [root@kvm ~ ::]#yum install -y kvm virt-* libvirt bridge-utils qemu-img 说明: ...