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. jae的mongo数据库管理工具(原创)

    园里前段时间有人介绍了京东的jae,申请了试用了一下,各种坑,勉强可以测试用用. jae一直没有Mongo数据库的管理工具,没办法,自己写了一个凑合着先用着. 使用方法: 1.修改配置:下载后面的程序 ...

  2. makefile之wildcard函数

    $(wildcard PATTERN) 函数功能: 获取匹配 PATTERN 的所有对象 返回值: 使用空格分割的匹配对象列表 1. 示例1

  3. Storyboard 全解析

    XCode 4.3.2 新功能 - Storyboard 最近开始比较有空在玩 XCode 4.3.2,赫然发现它多了个 Storyboard 的东东. Storyboard 这个东西一般来说是在做创 ...

  4. Excel TargetRange.Validation为空的

    做Excel的时候遇到过TargetRange.Validation为空,赋值类似空指针一样的情况. 这样的情况,不懂Excel调试了好久,最后还知道,这个对象需要自己去定义才能够进行赋值, 这样定义 ...

  5. HeadFirst 设计模式 04 工厂模式

    除了 new 操作符之外, 还有更多创造对象的方法. 工厂处理创建对象的细节. 这么做的目的是为了抽象, 例如把创建比萨的代码包装进一个类, 当以后实现改变时, 只需修改这个类即可. 利用静态方法定义 ...

  6. js实现EasyUI-datagrid前台分页

    //实现假分页 function myLoader(param, success, error) { var that = $(this); var opts = that.datagrid(&quo ...

  7. 004Maven_Pom.xml文档的介绍

    很重要的一个文档,具体介绍如下:

  8. Sencha Touch快速入门(译)

    翻译自:http://www.sencha.com/learn/sencha-touch-quick-start/ 1.下载Sencha Touch SDK——下载链接 2.安装Safari或Chro ...

  9. c# http请求添加cookie

    CookieCollection cookList = new CookieCollection(); cookList.Add(new Cookie("cf_clearance" ...

  10. ItcastOA_整体说明_准备环境

    1. 整体说明 1.1. 项目说明 1.1.1. OA概述 OA是Office Automation的缩写,本意为利用技术的手段提高办公的效率,进而实现办公的自动化处理.实现信息化.无纸化办公,可方便 ...