看完此篇你会知道,如何优雅的使用 HTTP Server

问题背景

httpkill -9
RSTconnection refusedkill -9open too many files
Zero Downtime
热启动Zero Downtime

解决问题

平滑启动

一般情况下,我们是退出旧版本,再启动新版本,总会有时间间隔,时间间隔内的请求怎么办?而且旧版本正在处理请求怎么办?
那么,针对这些问题,在升级应用过程中,我们需要达到如下目的:

  • 旧版本为退出之前,需要先启动新版本;
  • 旧版本继续处理完已经接受的请求,并且不再接受新请求;
  • 新版本接受并处理新请求的方式;
Zero Downtime

实现原理

linuxsignalsignal-continue
HUPkill -HUP pid
package gracehttp

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

var sig chan os.Signal
var notifySignals []os.Signal

func init() {
    sig = make(chan os.Signal)
    notifySignals = append(notifySignals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGQUIT)
    signal.Notify(sig, notifySignals...) // 注册需要拦截的信号
}

// 捕获系统信号,并处理
func handleSignals() {
    capturedSig := <-sig
    srvLog.Info(fmt.Sprintf("Received SIG. [PID:%d, SIG:%v]", syscall.Getpid(), capturedSig))
    switch capturedSig {
    case syscall.SIGHUP: // 重启信号
        startNewProcess() // 开启新进程
        shutdown() // 退出旧进程
    case syscall.SIGINT:
        fallthrough
    case syscall.SIGTERM:
        fallthrough
    case syscall.SIGTSTP:
        fallthrough
    case syscall.SIGQUIT:
        shutdown()
    }
}
startNewProcessshutdown

过载保护

HTTP Serveraccept

实现原理

channelgoselectaccept

处理代码如下:

package gracehttp

// about limit @see: "golang.org/x/net/netutil"

import (
    "net"
    "sync"
    "time"
)

type Listener struct {
    *net.TCPListener
    sem       chan struct{}
    closeOnce sync.Once     // ensures the done chan is only closed once
    done      chan struct{} // no values sent; closed when Close is called
}

func newListener(tl *net.TCPListener, n int) net.Listener {
    return &Listener{
        TCPListener: tl,
        sem:         make(chan struct{}, n),
        done:        make(chan struct{}),
    }
}

func (l *Listener) Fd() (uintptr, error) {
    file, err := l.TCPListener.File()
    if err != nil {
        return 0, err
    }
    return file.Fd(), nil
}

// override
func (l *Listener) Accept() (net.Conn, error) {
    acquired := l.acquire()
    tc, err := l.AcceptTCP()
    if err != nil {
        if acquired {
            l.release()
        }
        return nil, err
    }
    tc.SetKeepAlive(true)
    tc.SetKeepAlivePeriod(time.Minute)

    return &ListenerConn{Conn: tc, release: l.release}, nil
}

// override
func (l *Listener) Close() error {
    err := l.TCPListener.Close()
    l.closeOnce.Do(func() { close(l.done) })
    return err
}

// acquire acquires the limiting semaphore. Returns true if successfully
// accquired, false if the listener is closed and the semaphore is not
// acquired.
func (l *Listener) acquire() bool {
    select {
    case <-l.done:
        return false
    case l.sem <- struct{}{}:
        return true
    }
}

func (l *Listener) release() { <-l.sem }

type ListenerConn struct {
    net.Conn
    releaseOnce sync.Once
    release     func()
}

func (l *ListenerConn) Close() error {
    err := l.Conn.Close()
    l.releaseOnce.Do(l.release)
    return err
}

gracehttp

现在我们把这个功能做得更优美有点,并提供一个开箱即用的代码库。
地址:Github-gracehttp

支持功能

Zero-DowntimeServerHTTPHTTPS

使用指南

添加服务

    import "fevin/gracehttp"
    
    ....

    // http
    srv1 := &http.Server{
        Addr:    ":80",
        Handler: sc,
    }
    gracehttp.AddServer(srv1, false, "", "")

    // https

    srv2 := &http.Server{
        Addr:    ":443",
        Handler: sc,
    }
    gracehttp.AddServer(srv2, true, "../config/https.crt", "../config/https.key")

    gracehttp.Run() // 此方法会阻塞,直到进程收到退出信号,或者 panic
Servergracehttp.AddServer

退出或者重启服务

kill -HUP pidkill -QUIT pid

添加自定义日志组件

    gracehttp.SetErrorLogCallback(logger.LogConfigLoadError)
Set*
SetInfoLogCallbackSetNoticeLogCallbackSetErrorLogCallback

最后

实际中,很多情况会用到这种方式,不妨点个 star 吧!
欢迎一起来完善这个小项目,共同贡献代码。