Go中優雅的HTTP服務關閉

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

雖然寫出7x24小時不間斷啟動並執行服務是一件很酷的事情,但是我們仍然在某些時候,譬如服務升級,配置更新等,得考慮如何優雅的結束這個服務。

當然,最暴力的做法直接就是kill -9,但這樣直接導致的後果就是可能幹掉了很多運行到一半的任務,最終導致資料不一致,這個苦果只有遇到過的人才能深深地體會,資料的修複真的挺蛋疼,有時候還得給使用者賠錢啦。

所以,通常我們都是給服務發送一個訊號,SIGTERM也行,SIGINTERRUPT也成,反正要讓服務知道該結束了。而服務收到結束訊號之後,首先會拒絕掉所有外部新的請求,然後等待當前所有正在執行的請求完成之後,在結束。當然很有可能當前在執行一個很耗時間的任務,導致服務長時間不能結束,這時候就得決定是否強制結束了。

具體到go的HTTP Server裡面,如何優雅的結束一個HTTP Server呢?

首先,我們需要顯示的建立一個listener,讓其迴圈不斷的accept新的串連供server處理,為啥不用預設的http.ListenAndServe,主要就在於我們可以在結束的時候通過關閉這個listener來主動的拒絕掉外部新的串連請求。代碼如下:

l, _ := net.Listen("tcp", address)svr := http.Server{Handler: handler}svr.Serve(l)

Serve這個函數是個死迴圈,我們可以在外部通過close對應的listener來結束。

當listener accept到新的請求之後,會開啟一個新的goroutine來執行,那麼在server結束的時候,我們怎麼知道這個goroutine是否完成了呢?

在很早之前,大概go1.2的時候,筆者通過在handler入口處使用sync WaitGroup來實現,因為我們有統一的一個入口handler,所以很容易就可以通過如下方式知道請求是否完成,譬如:

func (h *Handler) ServeHTTP(w ResponseWriter, r *Request) {    h.svr.wg.Add(1)    defer h.svr.wg.Done()    ......}

但這樣其實只是用來判斷請求是否結束了,我們知道在HTTP 1.1中,connection是能夠keepalived的,也就是請求處理完成了,但是connection仍是可用的,我們沒有一個好的辦法close掉這個connection。不過話說回來,我們只要保證當前請求能正常結束,connection能不能正常close真心無所謂,畢竟服務都結束了,connection自動就close了。但誰叫筆者是典型的處女座呢。

在go1.3之後,提供了一個ConnState的hook,我們能通過這個來擷取到對應的connection,這樣在服務結束的時候我們就能夠close掉這個connection了。該hook會在如下幾種ConnState狀態的時候調用。

  • StateNew:新的串連,並且馬上準備發送請求了
  • StateActive:表明一個connection已經接收到一個或者多個位元組的請求資料,在server調用實際的handler之前調用hook。
  • StateIdle:表明一個connection已經處理完成一次請求,但因為是keepalived的,所以不會close,繼續等待下一次請求。
  • StateHijacked:表明外部調用了hijack,最終狀態。
  • StateClosed:表明connection已經結束掉了,最終狀態。

通常,我們不會進入hijacked的狀態(如果是websocket就得考慮了),所以一個可能的hook函數如下,參考http://rcrowley.org/talks/gophercon-2014.html

s.ConnState = func(conn net.Conn, state http.ConnState) {    switch state {    case http.StateNew:        // 新的串連,計數加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:            // 如果要關閉了,直接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已經closed了,計數減一        s.wg.Done()    }

當結束的時候,會走如下流程:

func (s *Server) Close() error {    // close quit channel, 廣播我要結束啦    close(s.quit)    // 關閉keepalived,請求返回的時候會帶上Close header。用戶端就知道要close掉connection了。    s.SetKeepAlivesEnabled(false)    s.mu.Lock()    // close listenser    if err := s.l.Close(); err != nil {        return err     }    //將當前idle的connections設定read timeout,便於後續關閉。    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()    // 等待所有串連結束    s.wg.Wait()    return nil}

好了,通過以上方法,我們終於能從容的關閉server了。但這裡僅僅是針對跟用戶端的串連,實際還有MySQL串連,Redis串連,開啟的檔案控制代碼,等等,總之,要實現優雅的服務關閉,真心不是一件很簡單的事情。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.