雖然寫出7x24小時(shí)不間斷運(yùn)行的服務(wù)是一件很酷的事情,但是我們?nèi)匀辉谀承r(shí)候,譬如服務(wù)升級,配置更新等,得考慮如何優(yōu)雅的結(jié)束這個(gè)服務(wù)。
當(dāng)然,最暴力的做法直接就是kill -9,但這樣直接導(dǎo)致的后果就是可能干掉了很多運(yùn)行到一半的任務(wù),最終導(dǎo)致數(shù)據(jù)不一致,這個(gè)苦果只有遇到過的人才能深深地體會,數(shù)據(jù)的修復(fù)真的挺蛋疼,有時(shí)候還得給用戶賠錢啦。
所以,通常我們都是給服務(wù)發(fā)送一個(gè)信號,SIGTERM也行,SIGINTERRUPT也成,反正要讓服務(wù)知道該結(jié)束了。而服務(wù)收到結(jié)束信號之后,首先會拒絕掉所有外部新的請求,然后等待當(dāng)前所有正在執(zhí)行的請求完成之后,在結(jié)束。當(dāng)然很有可能當(dāng)前在執(zhí)行一個(gè)很耗時(shí)間的任務(wù),導(dǎo)致服務(wù)長時(shí)間不能結(jié)束,這時(shí)候就得決定是否強(qiáng)制結(jié)束了。
具體到go的HTTP Server里面,如何優(yōu)雅的結(jié)束一個(gè)HTTP Server呢?
首先,我們需要顯示的創(chuàng)建一個(gè)listener,讓其循環(huán)不斷的accept新的連接供server處理,為啥不用默認(rèn)的http.ListenAndServe,主要就在于我們可以在結(jié)束的時(shí)候通過關(guān)閉這個(gè)listener來主動的拒絕掉外部新的連接請求。代碼如下:
l, _ := net.Listen("tcp", address)
svr := http.Server{Handler: handler}
svr.Serve(l)
Serve這個(gè)函數(shù)是個(gè)死循環(huán),我們可以在外部通過close對應(yīng)的listener來結(jié)束。
當(dāng)listener accept到新的請求之后,會開啟一個(gè)新的goroutine來執(zhí)行,那么在server結(jié)束的時(shí)候,我們怎么知道這個(gè)goroutine是否完成了呢?
在很早之前,大概go1.2的時(shí)候,筆者通過在handler入口處使用sync WaitGroup來實(shí)現(xiàn),因?yàn)槲覀冇薪y(tǒng)一的一個(gè)入口handler,所以很容易就可以通過如下方式知道請求是否完成,譬如:
func (h *Handler) ServeHTTP(w ResponseWriter, r *Request) {
h.svr.wg.Add(1)
defer h.svr.wg.Done()
......
}
但這樣其實(shí)只是用來判斷請求是否結(jié)束了,我們知道在HTTP 1.1中,connection是能夠keepalived的,也就是請求處理完成了,但是connection仍是可用的,我們沒有一個(gè)好的辦法close掉這個(gè)connection。不過話說回來,我們只要保證當(dāng)前請求能正常結(jié)束,connection能不能正常close真心無所謂,畢竟服務(wù)都結(jié)束了,connection自動就close了。但誰叫筆者是典型的處女座呢。
在go1.3之后,提供了一個(gè)ConnState的hook,我們能通過這個(gè)來獲取到對應(yīng)的connection,這樣在服務(wù)結(jié)束的時(shí)候我們就能夠close掉這個(gè)connection了。該hook會在如下幾種ConnState狀態(tài)的時(shí)候調(diào)用。
- StateNew:新的連接,并且馬上準(zhǔn)備發(fā)送請求了
- StateActive:表明一個(gè)connection已經(jīng)接收到一個(gè)或者多個(gè)字節(jié)的請求數(shù)據(jù),在server調(diào)用實(shí)際的handler之前調(diào)用hook。
- StateIdle:表明一個(gè)connection已經(jīng)處理完成一次請求,但因?yàn)槭莐eepalived的,所以不會close,繼續(xù)等待下一次請求。
- StateHijacked:表明外部調(diào)用了hijack,最終狀態(tài)。
- StateClosed:表明connection已經(jīng)結(jié)束掉了,最終狀態(tài)。
通常,我們不會進(jìn)入hijacked的狀態(tài)(如果是websocket就得考慮了),所以一個(gè)可能的hook函數(shù)如下,參考http://rcrowley.org/talks/gophercon-2014.html
s.ConnState = func(conn net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
// 新的連接,計(jì)數(shù)加1
s.wg.Add(1)
case http.StateActive:
// 有新的請求,從idle conn pool中移除
s.mu.Lock()
delete(s.conns, conn.LocalAddr().String())
s.mu.Unlock()
case http.StateIdle:
select {
case <-s.quit:
// 如果要關(guān)閉了,直接Close,否則加入idle conn pool中。
conn.Close()
default:
s.mu.Lock()
s.conns[conn.LocalAddr().String()] = conn
s.mu.Unlock()
}
case http.StateHijacked, http.StateClosed:
// conn已經(jīng)closed了,計(jì)數(shù)減一
s.wg.Done()
}
當(dāng)結(jié)束的時(shí)候,會走如下流程:
func (s *Server) Close() error {
// close quit channel, 廣播我要結(jié)束啦
close(s.quit)
// 關(guān)閉keepalived,請求返回的時(shí)候會帶上Close header??蛻舳司椭酪猚lose掉connection了。
s.SetKeepAlivesEnabled(false)
s.mu.Lock()
// close listenser
if err := s.l.Close(); err != nil {
return err
}
//將當(dāng)前idle的connections設(shè)置read timeout,便于后續(xù)關(guān)閉。
t := time.Now().Add(100 * time.Millisecond)
for _, c := range s.conns {
c.SetReadDeadline(t)
}
s.conns = make(map[string]net.Conn)
s.mu.Unlock()
// 等待所有連接結(jié)束
s.wg.Wait()
return nil
}
好了,通過以上方法,我們終于能從容的關(guān)閉server了。但這里僅僅是針對跟客戶端的連接,實(shí)際還有MySQL連接,Redis連接,打開的文件句柄,等等,總之,要實(shí)現(xiàn)優(yōu)雅的服務(wù)關(guān)閉,真心不是一件很簡單的事情。