這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
對於程式及服務的控制,本質上而言就是正確的啟動,並可控的停止或退出。在go語言中,其實就是程式安全退出、服務控制兩個方面。核心在於系統訊號擷取、Go Concurrency Patterns、以及基本的代碼封裝。
程式安全退出
執行代碼非安全寫法
在代碼部署後,我們可能因為服務配置發生變化或其他各種原因,需要將服務停止或者重啟。通常就是for迴圈阻塞,運行代碼,然後通過control+C或者kill來強制退出。代碼如下:
//file svc1.gopackage mainimport ( "fmt" "time")//當接收到Control+c,kill -1,kill -2,kill -9 均無法正常執行defer函數func main() { fmt.Println("application is begin.") //以下代碼不會執行 defer fmt.Println("application is end.") for { time.Sleep(time.Second) fmt.Println("application is running.") }}
這種方式簡單粗暴,很多時候基本也夠用。但這種情況下,程式是不會執行defer的代碼的,因此無法正確處理結束操作,會丟失一些很關鍵的日誌記錄、訊息通知,非常不安全的。這時,需要引入一個簡單的架構,來執行退出。
執行代碼的基本:訊號攔截
由於go語言中的關鍵字go很好用,通過標準庫,我們可以很優雅的實現退出訊號的攔截:
//file svc2.gopackage mainimport ( "fmt" "time" "os/signal" "os")//當接收到Control+c,kill -1,kill -2 的時候,都可以執行執行defer函數// kill -9依然不會正常退出。func main() { fmt.Println("application is begin.") //當程式接受到退出訊號的時候,將會執行 defer fmt.Println("application is end.") //協程啟動的匿名函數,類比業務代碼 go func(){ for { time.Sleep(time.Second) fmt.Println("application is running.") } }() //捕獲程式退出訊號 msgChan:=make(chan os.Signal,1) signal.Notify(msgChan,os.Interrupt,os.Kill) <-msgChan}
此時,我們實現了程式退出時的訊號攔截,補充業務代碼就可以了。但實際商務邏輯至少涉及到初始化、業務處理、退出三大塊,代碼量多了,會顯得比較混亂,這就需要規範代碼的結構。
執行代碼的改進:訊號攔截封裝器
考慮上述情況,我們將正常的程式定義為:
- Init: 系統初始化,比如識別作業系統、初始化服務發現Consul、Zookeper的agent、資料庫連接池等。
- Start:程式主要商務邏輯,包括但不限於資料載入、服務註冊、具體業務響應。
- Stop: 程式退出時的業務,主要包括記憶體資料存放區、服務登出。
基於這個定義,之前的svc2.go僅保留業務代碼的情況下,可以這樣改寫:
//file svc3.gopackage mainimport ( "fmt" "time" "study1/svc")type Program struct {}func (p *Program) Start()error { fmt.Println("application is begin.") //必須非阻塞,因此通過協程封裝。 go func(){ for { time.Sleep(time.Second) fmt.Println("application is running.") } }() return nil}func (p *Program)Init()error{ //just demon,do nothing return nil}func (p *Program) Stop() error { fmt.Println("application is end.") return nil}//當接收到Control+C,kill -1,kill -2 的時候,都可執行defer函數// kill -9依然不會正常退出。func main() { p:=&Program{} svc.Run(p)}
上訴代碼中的Program的Init、Start、Stop事實上是實現了相關的介面定義,該介面在svc包中,被Run方法使用。代碼如下:
//file svc.gopackage svcimport ( "os" "os/signal")//標準程式執行和退出的執行介面,運行程式要實現介面定義的方法type Service interface { Init() error //當程式啟動啟動並執行時候,需要執行的代碼。不得阻塞。 Start() error //程式退出的時候,需要執行的代碼。不得阻塞。 Stop() error}var msgChan = make(chan os.Signal, 1)// 程式運行、退出的封裝容器,主程式直接調用。func Run(service Service) error { if err := service.Init(); err != nil { return err } if err := service.Start(); err != nil { return err } signal.Notify(msgChan, os.Interrupt, os.Kill) <-msgChan return service.Stop()}// 通常不需要調用,特殊情況下,在程式內其他模組中,需要通知程式退出才會使用。func Interrupt(){ msgChan<-os.Interrupt}
這段代碼中,svg包的Run只會被唯一的main調用。為了支援其他退出模式,比如使用者敲入字元命令的退出,因此加入了“後門”——Interrupt方法。後邊會有具體的使用案例。由於一個進程只會有一個svg.Service的執行個體,通常情況下足以使用。
單機多服務的應用啟動、退出架構
在網路應用,可能會有更複雜的情況,我們需要考慮:
- 程式啟動
- 程式不退出的情況下,多服務啟動、並行運行與退出
- 程式退出,並清理運行中的服務
可以做一個簡單的Demon程式,來實現以上三點,其中,程式退出可以通過鍵盤輸入命令,也可以Control+D。基於golang1.7,我們可以採用以下知識點:
- 利用cancelContext來控制服務的退出
- 利用之前實現的svc來實現程式的安全退出
- 利用os.Stdin來擷取鍵盤輸入命令來類比服務載入與退出的訊息驅動。實際可能是網路rpc或http資料觸發
golang1.7的context包
我們知道,當通道chan被close之後,任何<-chan都會得到立即執行。如果不清楚,可以查閱相關資料或寫個測試代碼,最好研讀golang的官方資料:Go Concurrency Patterns: Pipelines and cancellation。
利用這個特徵,我們可以通過golang1.7標準庫新增的context包,通過注入的方式來實現全域或單個服務的控制。
context中定義了Context介面,我們通過幾種不同的方法來擷取不同的實現。包括:
- WithDeadline\WithTimeout,擷取到基於時間相關的退出控制代碼,以控制服務退出。
- WithCancel,擷取到cancelFunc控制代碼,以控制服務的退出。
- WithValue,擷取到k-v索引值對,實作類別似於session資訊儲存的業務支援。
- Background\TODO,conext的根,通常作為以上三種方法的parent。
context包不是新東西,2014年就已經在google.org/x/net中,作為擴充庫被很多開源項目使用(GIN、IRIS等等)。其CSP的應用方式非常值得進一步研讀。
捕獲鍵盤輸入
通過os.stdin來擷取鍵盤輸入,其解析需要bufilo.Reader來協助處理。通常代碼格式就是:
//...//初始化鍵盤讀取reader:=bufilo.NewReader(os.Stdin)//阻塞,直到敲入Enter鍵input, _, _ := reader.ReadLine()command:=string(input)//...
範例程式碼
有了這兩個概念之後,就可以很方便的實現一個簡單的微服務載入、退出的架構。參考代碼如下:
//file svc4.gopackage mainimport ( "bufio" "context" "errors" "fmt" "os" "strings" "study1/svc" "sync" "time")type Program struct { ctx context.Context exitFunc context.CancelFunc cancelFunc map[string]context.CancelFunc wg WaitGroupWrapper}func main() { p := &Program{ cancelFunc: make(map[string]context.CancelFunc), } p.ctx, p.exitFunc = context.WithCancel(context.Background()) svc.Run(p)}func (p *Program) Init() error { //just demon,do nothing return nil}func (p *Program) Start() error { fmt.Println("本程式將會根據輸入,啟動或終止服務。") reader := bufio.NewReader(os.Stdin) go func() { for { fmt.Println("程式退出命令:exit;服務啟動命令:<start||s>-[name];服務停止命令:<cancel||c>-[name]。請注意大小寫!") input, _, _ := reader.ReadLine() command := string(input) switch command { case "exit": goto OutLoop default: command, name, err := splitInput(input) if err != nil { fmt.Println(err) continue } switch command { case "start", "s": newctx, cancelFunc := context.WithCancel(p.ctx) p.cancelFunc[name] = cancelFunc p.wg.Wrap(func() { Func(newctx, name) }) case "cancel", "c": cancelFunc, founded := p.cancelFunc[name] if founded { cancelFunc() } } } } OutLoop: //由於程式退出被Run的os.Notify阻塞,因此調用以下方法通知結束代碼執行。 svc.Interrupt() }() return nil}func (p *Program) Stop() error { p.exitFunc() p.wg.Wait() fmt.Println("所有服務終止,程式退出!") return nil}//用來轉換輸入字串為輸入命令func splitInput(input []byte) (command, name string, err error) { line := string(input) strs := strings.Split(line, "-") if strs == nil || len(strs) != 2 { err = errors.New("輸入不符合規則。") return } command = strs[0] name = strs[1] return}// 一個簡單的迴圈方法,類比被載入、釋放的微服務func Func(ctx context.Context, name string) { for { select { case <-ctx.Done(): goto OutLoop case <-time.Tick(time.Second * 2): fmt.Printf("%s is running.\n", name) } }OutLoop: fmt.Printf("%s is end.\n", name)}//WaitGroup封裝結構type WaitGroupWrapper struct { sync.WaitGroup}func (w *WaitGroupWrapper) Wrap(f func()) { w.Add(1) go func() { f() w.Done() }()}
代碼啟動並執行時候,可以:
- 通過輸入”s-“或者”start-“+服務名,來啟動一個服務
- 用”c-“或”cancel-“+服務名,來退出指定服務
- 可以用 “exit”或者Control+C、kill來退出程式(除了kill -9)。
在此基礎上,還可以利用context包實現服務逾時退出,利用for range限制服務數量,利用HTTP實現微服務RestFUL資訊驅動。由於擴充之後代碼增加,顯得冗餘,這裡不再贅述。