過早的部署這種行為非常粗略,尤其是在部署還要中斷用戶請求的情況下更是如此,因此,我們在Betable構建的Go服務要在不中斷任何用戶請求的情況下優雅的中止服務。其基本思想就是停止偵聽(llistening),假定會有一個新的進程來接管這些偵聽,讓所有已經建立起來的連接在最終停止服務前繼續處理進行中的請求。順便說一句,我們采用了goagain,從而可以甚至在不停止偵聽的情況下重啟服務,但這個話題超出了本文的討論范圍。
main.main做了四件事情:偵聽、生成一個Service並將其發送到後台(background)執行、阻塞直到收到相應的信號(signal)、 然後優雅的停止服務。偵聽以及信號處理完全出自於標准庫的一般打法,只有一點令我十分惱火,就是,不僅需要使用anet.Listener或者net.Connto調用SetDeadline,竟然還需要*net.TCPListener或者*net.TCPConnand。
service.Serve(listener)用來接受連接請求並在它自己的goroutine之內處理它所接受的每個連接請求。由於它設置了一個截至時間,所以listener.AcceptTCP()不會永久性地處於阻塞狀態,而且它還會在下一輪循環時檢查它是否應該停止偵聽。
service.serve(conn)進行讀寫操作,而且同樣的,它也具有一個截至時間。由於它設置了截至時間,所以conn.Read(buf)不會永久性地處於阻塞狀態,而且在寫入響應數據和讀取下一個請求之間或者超過了conn.Read(buf)截至時間後,檢查它是否應該關閉該連接。
因為不會有別的東西發送到service的channel中,只有在service.Stop()關閉該channel後才會向該channel中發送一個值,所以,只要從service的channel中接收到一個值,各個goroutine就會決定關閉相關的連接和偵聽器(listener)。
難題的最後一個部分是通過調用標准庫sync.WaitGroup實現等待所有的goroutine結束執行。
https://gist.github.com/rcrowley/5474430包含了所有的代碼:
package main
import (
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// An uninteresting service.
type Service struct {
ch chan bool
waitGroup *sync.WaitGroup
}
// Make a new Service.
func NewService() *Service {
return &Service{
ch: make(chan bool),
waitGroup: &sync.WaitGroup{},
}
}
// Accept connections and spawn a goroutine to serve each one. Stop listening
// if anything is received on the service's channel.
func (s *Service) Serve(listener *net.TCPListener) {
s.waitGroup.Add(1)
defer s.waitGroup.Done()
for {
select {
case <-s.ch:
log.Println("stopping listening on", listener.Addr())
listener.Close()
return
default:
}
listener.SetDeadline(time.Now().Add(1e9))
conn, err := listener.AcceptTCP()
if nil != err {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
continue
}
log.Println(err)
}
log.Println(conn.RemoteAddr(), "connected")
go s.serve(conn)
}
}
// Stop the service by closing the service's channel. Block until the service
// is really stopped.
func (s *Service) Stop() {
close(s.ch)
s.waitGroup.Wait()
}
// Serve a connection by reading and writing what was read. That's right, this
// is an echo service. Stop reading and writing if anything is received on the
// service's channel but only after writing what was read.
func (s *Service) serve(conn *net.TCPConn) {
defer conn.Close()
s.waitGroup.Add(1)
defer s.waitGroup.Done()
for {
select {
case <-s.ch:
log.Println("disconnecting", conn.RemoteAddr())
return
default:
}
conn.SetDeadline(time.Now().Add(1e9))
buf := make([]byte, 4096)
if _, err := conn.Read(buf); nil != err {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
continue
}
log.Println(err)
return
}
if _, err := conn.Write(buf); nil != err {
log.Println(err)
return
}
}
}
func main() {
// Listen on 127.0.0.1:48879. That's my favorite port number because in
// hex 48879 is 0xBEEF.
laddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:48879")
if nil != err {
log.Fatalln(err)
}
listener, err := net.ListenTCP("tcp", laddr)
if nil != err {
log.Fatalln(err)
}
log.Println("listening on", listener.Addr())
// Make a new service and send it into the background.
service := NewService()
go service.Serve(listener)
// Handle SIGINT and SIGTERM.
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
// Stop the service gracefully.
service.Stop()
}