這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
雖然寫出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串連,開啟的檔案控制代碼,等等,總之,要實現優雅的服務關閉,真心不是一件很簡單的事情。