看完此篇你会知道,如何优雅的使用 HTTP Server
在 http
应用程序重启时,若是咱们直接 kill -9
使程序退出,而后在启动,会有如下几个问题:linux
RST
);connection refused
;kill -9
仍然会让正在处理的请求中断;open too many files
错误;这些问题会形成很差的客户体验,严重的甚至影响客户业务。因此,咱们须要以一种优雅的方式重启/关闭咱们的应用,来达到热启动的效果,即:Zero Downtime
。git
(Tips:名词解释)
热启动
:新老程序(进程)无缝替换,同时能够保持对client的服务。让client端感受不到你的服务挂掉了;
Zero Downtime
: 0 宕机时间,即不间断的服务;
Github: gracehttpgithub
通常状况下,咱们是退出旧版本,再启动新版本,总会有时间间隔,时间间隔内的请求怎么办?并且旧版本正在处理请求怎么办?
那么,针对这些问题,在升级应用过程当中,咱们须要达到以下目的:golang
这样,咱们就能实现 Zero Downtime
的升级效果。网络
首先,咱们须要用到如下基本知识:
1.linux
信号处理机制:在程序中,经过拦截 signal
,并针对 signal
作出不一样处理;
2.子进程继承父进程的资源:一切皆文件,子进程会继承父进程的资源句柄,网络端口也是文件;
3.经过给子进程重启标识(好比:重启时带着 -continue
参数),来实现子进程的初始化处理;并发
重启时,咱们能够在程序中捕获 HUP
信号(经过 kill -HUP pid
能够触发),而后开启新进程,退出旧进程。信号处理代码示例以下:app
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
具体实现能够参考 Githubide
经过限制 HTTP Server
的 accept
数量实现连接数的限制,来达到若是并发量达到了最大值,客户端超时时间内能够等待,但不会消耗服务端文件句柄数(咱们知道 Linux 系统对用户能够打开的最大文件数有限制,网络请求也是文件操做)ui
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 }
如今咱们把这个功能作得更优美有点,并提供一个开箱即用的代码库。
地址:Github-gracehttp
Zero-Downtime
);Server
添加(支持 HTTP
、HTTPS
);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 吧!欢迎一块儿来完善这个小项目,共同贡献代码。