Golang开发支持平滑升级(优雅重启)的HTTP服务 - tabalt的博客 http://tabalt.net/blog/graceful-http-server-for-golang/

https://github.com/tabalt/gracehttp

 

前段时间用Golang在做一个HTTP的接口,因编译型语言的特性,修改了代码需要重新编译可执行文件,关闭正在运行的老程序,并启动新程序。对于访问量较大的面向用户的产品,关闭、重启的过程中势必会出现无法访问的情况,从而影响用户体验。

使用Golang的系统包开发HTTP服务,是无法支持平滑升级(优雅重启)的,本文将探讨如何解决该问题。

一、平滑升级(优雅重启)的一般思路

一般情况下,要实现平滑升级,需要以下几个步骤:

  1. 用新的可执行文件替换老的可执行文件(如只需优雅重启,可以跳过这一步)

  2. 通过pid给正在运行的老进程发送 特定的信号(kill -SIGUSR2 $pid)

  3. 正在运行的老进程,接收到指定的信号后,以子进程的方式启动新的可执行文件并开始处理新请求

  4. 老进程不再接受新的请求,等待未完成的服务处理完毕,然后正常结束

  5. 新进程在父进程退出后,会被init进程领养,并继续提供服务

二、Golang Socket 网络编程

Socket是程序员层面上对传输层协议TCP/IP的封装和应用。Golang中Socket相关的函数与结构体定义在net包中,我们从一个简单的例子来学习一下Golang Socket 网络编程,关键说明直接写在注释中。

1、服务端程序 server.go

package main

import (
"fmt"
"log"
"net"
"time"
) func main() {
// 监听8086端口
listener, err := net.Listen("tcp", ":8086")
if err != nil {
log.Fatal(err)
}
defer listener.Close() for {
// 循环接收客户端的连接,没有连接时会阻塞,出错则跳出循环
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
break
} fmt.Println("[server] accept new connection.") // 启动一个goroutine 处理连接
go handler(conn)
}
} func handler(conn net.Conn) {
defer conn.Close() for {
// 循环从连接中 读取请求内容,没有请求时会阻塞,出错则跳出循环
request := make([]byte, 128)
readLength, err := conn.Read(request) if err != nil {
fmt.Println(err)
break
} if readLength == 0 {
fmt.Println(err)
break
} // 控制台输出读取到的请求内容,并在请求内容前加上hello和时间后向客户端输出
fmt.Println("[server] request from ", string(request))
conn.Write([]byte("hello " + string(request) + ", time: " + time.Now().Format("2006-01-02 15:04:05")))
}
}

2、客户端程序 client.go

package main

import (
"fmt"
"log"
"net"
"os"
"time"
) func main() { // 从命令行中读取第二个参数作为名字,如果不存在第二个参数则报错退出
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s name ", os.Args[0])
os.Exit(1)
}
name := os.Args[1] // 连接到服务端的8086端口
conn, err := net.Dial("tcp", "127.0.0.1:8086")
checkError(err) for {
// 循环往连接中 写入名字
_, err = conn.Write([]byte(name))
checkError(err) // 循环从连接中 读取响应内容,没有响应时会阻塞
response := make([]byte, 256)
readLength, err := conn.Read(response)
checkError(err) // 将读取响应内容输出到控制台,并sleep一秒
if readLength > 0 {
fmt.Println("[client] server response:", string(response))
time.Sleep(1 * time.Second)
}
}
} func checkError(err error) {
if err != nil {
log.Fatal("fatal error: " + err.Error())
}
}

3、运行示例程序

# 运行服务端程序
go run server.go # 在另一个命令行窗口运行客户端程序
go run client.go "tabalt"

三、Golang HTTP 编程

HTTP是基于传输层协议TCP/IP的应用层协议。Golang中HTTP相关的实现在net/http包中,直接用到了net包中Socket相关的函数和结构体。

我们再从一个简单的例子来学习一下Golang HTTP 编程,关键说明直接写在注释中。

1、http服务程序 http.go

package main

import (
"log"
"net/http"
"os"
) // 定义http请求的处理方法
func handlerHello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("http hello on golang\n"))
} func main() { // 注册http请求的处理方法
http.HandleFunc("/hello", handlerHello) // 在8086端口启动http服务,会一直阻塞执行
err := http.ListenAndServe("localhost:8086", nil)
if err != nil {
log.Println(err)
} // http服务因故停止后 才会输出如下内容
log.Println("Server on 8086 stopped")
os.Exit(0)
}

2、运行示例程序

# 运行HTTP服务程序
go run http.go # 在另一个命令行窗口curl请求测试页面
curl http://localhost:8086/hello/ # 输出如下内容:
http hello on golang

四、Golang net/http包中 Socket操作的实现

从上面的简单示例中,我们看到在Golang中要启动一个http服务,只需要简单的三步:

  1. 定义http请求的处理方法

  2. 注册http请求的处理方法

  3. 在某个端口启动HTTP服务

而最关键的启动http服务,是调用http.ListenAndServe()函数实现的。下面我们找到该函数的实现:

func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

这里创建了一个Server的对象,并调用它的ListenAndServe()方法,我们再找到结构体Server的ListenAndServe()方法的实现:

func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

从代码上看到,这里监听了tcp端口,并将监听者包装成了一个结构体 tcpKeepAliveListener,再调用srv.Serve()方法;我们继续跟踪Serve()方法的实现:

func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, e := l.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve()
}
}

可以看到,和我们前面Socket编程的示例代码一样,循环从监听的端口上Accept连接,如果返回了一个net.Error并且这个错误是临时性的,则会sleep一个时间再继续。 如果返回了其他错误则会终止循环。成功Accept到一个连接后,调用了方法srv.newConn()对连接做了一层包装,最后启了一个goroutine处理http请求。

五、Golang 平滑升级(优雅重启)HTTP服务的实现

我创建了一个新的包gracehttp来实现支持平滑升级(优雅重启)的HTTP服务,为了少写代码和降低使用成本,新的包尽可能多地利用net/http包的实现,并和net/http包保持一致的对外方法。现在开始我们来看gracehttp包支持平滑升级 (优雅重启)Golang HTTP服务涉及到的细节如何实现。

1、Golang处理信号

Golang的os/signal包封装了对信号的处理。简单用法请看示例:

package main

import (
"fmt"
"os"
"os/signal"
"syscall"
) func main() { signalChan := make(chan os.Signal) // 监听指定信号
signal.Notify(
signalChan,
syscall.SIGHUP,
syscall.SIGUSR2,
) // 输出当前进程的pid
fmt.Println("pid is: ", os.Getpid()) // 处理信号
for {
sig := <-signalChan
fmt.Println("get signal: ", sig)
}
}

2、子进程启动新程序,监听相同的端口

在第四部分的ListenAndServe()方法的实现代码中可以看到,net/http包中使用net.Listen函数来监听了某个端口,但如果某个运行中的程序已经监听某个端口,其他程序是无法再去监听这个端口的。解决的办法是使用子进程的方式启动,并将监听端口的文件描述符传递给子进程,子进程里从这个文件描述符实现对端口的监听。

具体实现需要借助一个环境变量来区分进程是正常启动,还是以子进程方式启动的,相关代码摘抄如下:

// 启动子进程执行新程序
func (this *Server) startNewProcess() error { listenerFd, err := this.listener.(*Listener).GetFd()
if err != nil {
return fmt.Errorf("failed to get socket file descriptor: %v", err)
} path := os.Args[0] // 设置标识优雅重启的环境变量
environList := []string{}
for _, value := range os.Environ() {
if value != GRACEFUL_ENVIRON_STRING {
environList = append(environList, value)
}
}
environList = append(environList, GRACEFUL_ENVIRON_STRING) execSpec := &syscall.ProcAttr{
Env: environList,
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFd},
} fork, err := syscall.ForkExec(path, os.Args, execSpec)
if err != nil {
return fmt.Errorf("failed to forkexec: %v", err)
} this.logf("start new process success, pid %d.", fork) return nil
} func (this *Server) getNetTCPListener(addr string) (*net.TCPListener, error) { var ln net.Listener
var err error if this.isGraceful {
file := os.NewFile(3, "")
ln, err = net.FileListener(file)
if err != nil {
err = fmt.Errorf("net.FileListener error: %v", err)
return nil, err
}
} else {
ln, err = net.Listen("tcp", addr)
if err != nil {
err = fmt.Errorf("net.Listen error: %v", err)
return nil, err
}
}
return ln.(*net.TCPListener), nil
}

3、父进程等待已有连接中未完成的请求处理完毕

这一块是最复杂的;首先我们需要一个计数器,在成功Accept一个连接时,计数器加1,在连接关闭时计数减1,计数器为0时则父进程可以正常退出了。Golang的sync的包里的WaitGroup可以很好地实现这个功能。

然后要控制连接的建立和关闭,我们需要深入到net/http包中Server结构体的Serve()方法。重温第四部分Serve()方法的实现,会发现如果要重新写一个Serve()方法几乎是不可能的,因为这个方法里调用了好多个不可导出的内部方法,重写Serve()方法几乎要重写整个net/http包。

幸运的是,我们还发现在 ListenAndServe()方法里传递了一个listener给Serve()方法,并最终调用了这个listener的Accept()方法,这个方法返回了一个Conn的示例,最终在连接断开的时候会调用Conn的Close()方法,这些结构体和方法都是可导出的!

我们可以定义自己的Listener结构体和Conn结构体,组合net/http包中对应的结构体,并重写Accept()和Close()方法,实现对连接的计数,相关代码摘抄如下:

type Listener struct {
*net.TCPListener waitGroup *sync.WaitGroup
} func (this *Listener) Accept() (net.Conn, error) { tc, err := this.AcceptTCP()
if err != nil {
return nil, err
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute) this.waitGroup.Add(1) conn := &Connection{
Conn: tc,
listener: this,
}
return conn, nil
} func (this *Listener) Wait() {
this.waitGroup.Wait()
} type Connection struct {
net.Conn
listener *Listener closed bool
} func (this *Connection) Close() error { if !this.closed {
this.closed = true
this.listener.waitGroup.Done()
} return this.Conn.Close()
}

4、gracehttp包的用法

gracehttp包已经应用到每天几亿PV的项目中,也开源到了github上:github.com/tabalt/gracehttp,使用起来非常简单。

如以下示例代码,引入包后只需修改一个关键字,将http.ListenAndServe 改为 gracehttp.ListenAndServe即可。

package main

import (
"fmt"
"net/http" "github.com/tabalt/gracehttp"
) func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
}) err := gracehttp.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}

测试平滑升级(优雅重启)的效果,可以参考下面这个页面的说明:

https://github.com/tabalt/gracehttp#demo

使用过程中有任何问题和建议,欢迎提交issue反馈,也可以Fork到自己名下修改之后提交pull request。

本文链接:http://tabalt.net/blog/graceful-http-server-for-golang/,转载请注明。

Golang开发支持平滑升级(优雅重启)的HTTP服务的更多相关文章

  1. nginx启动、重启、重新加载配置文件和平滑升级

    Nginx有一个主进程和几个工作进程,主进程的主要作用就是读取.评估配置文件和管理工作进程,工作进程对请求做实际处理.工作进程的数量是在配置文件中配置的,一般设置为cpu的核心数*线程数. nginx ...

  2. RocketMQ 整合 DLedger(多副本)即主从切换实现平滑升级的设计技巧

    目录 1.阅读源码之前的思考 2.从 Broker 启动流程看 DLedger 2.1 构建 DefaultMessageStore 2.2 增加节点状态变更事件监听器 2.3 调用 DefaultM ...

  3. [译]Golang中的优雅重启

    原文 Graceful Restart in Golang 作者 grisha 声明:本文目的仅仅作为个人mark,所以在翻译的过程中参杂了自己的思想甚至改变了部分内容,其中有下划线的文字为译者添加. ...

  4. openresty开发系列3--nginx的平滑升级

    openresty开发系列3--nginx的平滑升级 nginx服务器从低版本升级为高版本,如果强行停止服务,会影响正在运行的进程. 平滑升级不会停掉正在运行中的进程,这些进程会继续处理请求.但不会接 ...

  5. Nginx的平滑重启和平滑升级

    一,Nginx的平滑重启如果改变了Nginx的配置文件(nginx.conf),想重启Nginx,可以发送系统信号给Nginx主进程的方式来进行.在重启之前,要确认Nginx配置文件的语法是正确的. ...

  6. Nginx 启动、停止、平滑重启和平滑升级 graceful shutdown of worker processes

    Controlling nginx http://nginx.org/en/docs/control.html nginx can be controlled with signals. The pr ...

  7. 让你的网站免费支持 HTTPS 及 Nginx 平滑升级

    为什么要使用 HTTPS ? 首先来说一下 HTTP 与 HTTPS 协议的区别吧,他们的根本区别就是 HTTPS 在 HTTP 协议的基础上加入了 SSL 层,在传输层对网络连接进行加密.简单点说在 ...

  8. nginx平滑重启与平滑升级的方法

    如何实现nginx平滑重启与平滑升级? 平滑重启 kill -HUP `cat /usr/local/www/nginx/logs/nginx.pid` 平滑升级nginx: cd /yujialin ...

  9. Nginx 的启动、停止、平滑重启、信号控制和平滑升级

    Nginx 的启动         假设 nginx 安装在 /usr/local/nginx 目录中,那么启动 nginx 的命令就是: [root@localhost ~]# /usr/local ...

随机推荐

  1. NB 命令安装需似机(无图型化安装)

    [root@ok ~]# virt-install -v -n 09ng04 -r 512 --vcpus=4 --location=/home/ISO/CentOS-6.7-x86_64-bin-D ...

  2. Makefile 使用小结

    Makefile的基本格式 #目标:依赖(条件) # 命令 #all: add.c sub.c dive.c mul.c main.c # gcc add.c sub.c div.c mul.c ma ...

  3. swift 函数.和匿名函数

    函数 注意: 没有定义返回类型的函数会返回特殊的值,叫 Void.它其实是一个空的元组(tuple),没有任何元素,可以写成(). 使用元组作为返回参数,返回多个参数 func count(strin ...

  4. 2、Reactive Extensions for .NET(译)

    实验3-引入 .net 中的 events 到 Rx 目标:前面实验中的使用各种工厂构造方法创建一个 可观察序列是一个部分.把 .net 中现有的异步数据源进行关联 是更重要的事情.在这次实验中我们将 ...

  5. Lucene用法10个小结 (zhuan)

    http://www.cfanz.cn/index.PHP?c=article&a=read&id=303149 *********************************** ...

  6. Django QuerySet 方法梳理 。model外键 多对多的保存

    引用:https://feifeiyum.github.io/2017/03/28/python-django-queryset/ 说明 Models 层是 Django 框架中最强大的部分之一, 大 ...

  7. org.apache.hadoop.hbase.DoNotRetryIOException: Class org.apache.phoenix.coprocessor.MetaDataEndpointImpl cannot be loaded Set hbase.table.sanity.checks to false at conf or table descriptor if you want

    https://stackoverflow.com/questions/38495331/apache-phoenix-unable-to-connect-to-hbase 这个坑不该啊 首选配置hb ...

  8. Github优秀开源项目

    王潜升 https://github.com/code4craft/webmagic 一个爬虫框架,除了不会反爬虫外(当然可以自己加)其他都很牛逼.这个项目更新还是很快的.   ansi分词  htt ...

  9. jenkins环境搭建&配置(二)

    Jenkins介绍: Jenkins用于监控持续重复的工作,功能包括: 1.持续的软件版本发布/测试项目. 2.监控外部调用执行的工作. 安装环境: 操作系统:linux(centOS) 软件:jdk ...

  10. 【转】【C#】迭代器IEnumerable和IEnumerator

    迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式.简单来说,迭代器模式使得你能够获取到序列中的所有元素而 ...