看完此篇你会知道,如何优雅的使用 HTTP Server
问题背景
在 http
应用程序重启时,如果我们直接 kill -9
使程序退出,然后在启动,会有以下几个问题:
- 旧的请求未处理完,如果服务端进程直接退出,会造成客户端链接中断(收到
RST
); - 新请求打过来,服务还没重启完毕,造成
connection refused
; - 即使是要退出程序,直接
kill -9
仍然会让正在处理的请求中断; - 面对海量请求,如何对链接数进行限制,并进行过载保护;
- 避免
open too many files
错误;
这些问题会造成不好的客户体验,严重的甚至影响客户业务。所以,我们需要以一种优雅的方式重启/关闭我们的应用,来达到热启动的效果,即:Zero Downtime
。
(Tips:名词解释)
热启动
:新老程序(进程)无缝替换,同时可以保持对client的服务。让client端感觉不到你的服务挂掉了;
Zero Downtime
: 0 宕机时间,即不间断的服务;
解决问题
Github: gracehttp
平滑启动
一般情况下,我们是退出旧版本,再启动新版本,总会有时间间隔,时间间隔内的请求怎么办?而且旧版本正在处理请求怎么办?
那么,针对这些问题,在升级应用过程中,我们需要达到如下目的:
- 旧版本为退出之前,需要先启动新版本;
- 旧版本继续处理完已经接受的请求,并且不再接受新请求;
- 新版本接受并处理新请求的方式;
这样,我们就能实现 Zero Downtime
的升级效果。
实现原理
首先,我们需要用到以下基本知识:
1.linux
信号处理机制:在程序中,通过拦截 signal
,并针对 signal
做出不同处理;
2.子进程继承父进程的资源:一切皆文件,子进程会继承父进程的资源句柄,网络端口也是文件;
3.通过给子进程重启标识(比如:重启时带着 -continue
参数),来实现子进程的初始化处理;
重启时,我们可以在程序中捕获 HUP
信号(通过 kill -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()
}
}
startNewProcess
shutdown
具体实现可以参考 Github
过载保护
通过限制 HTTP Server
的 accept
数量实现链接数的限制,来达到如果并发量达到了最大值,客户端超时时间内可以等待,但不会消耗服务端文件句柄数(我们知道 Linux 系统对用户可以打开的最大文件数有限制,网络请求也是文件操作)
实现原理
- 利用
channel
的缓冲机制实现,每个请求都会获取缓冲区的一个单元大小,知道缓冲区满了,后边的请求就会阻塞; - 如果客户端请求被阻塞,达到了客户端设置的超时时间,这时候链接会断开,那我们利用
go
的select
机制,退出阻塞,并返回,不再进行accept
处理代码如下:
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-Downtime
); - 平滑关闭;
- 多
Server
添加(支持HTTP
、HTTPS
); - 自定义日志组件;
- 支持单个端口 server 链接数限流,默认值为:C100K。超过该限制之后,链接阻塞进入等待,但是不消耗系统文件句柄,避免发生雪崩,压坏服务。
使用指南
添加服务
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
如上所示,只需创建好 Server
对象,调用 gracehttp.AddServer
添加即可。
退出或者重启服务
- 重启:
kill -HUP pid
- 退出:
kill -QUIT pid
添加自定义日志组件
gracehttp.SetErrorLogCallback(logger.LogConfigLoadError)
此处提供了三个 Set*
方法,分别对应不同的日志等级:
SetInfoLogCallback
SetNoticeLogCallback
SetErrorLogCallback
最后
实际中,很多情况会用到这种方式,不妨点个 star 吧!
欢迎一起来完善这个小项目,共同贡献代码。