Go中優(yōu)雅的HTTP服務(wù)關(guān)閉

雖然寫出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)閉,真心不是一件很簡單的事情。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容