1. 程式人生 > >標準庫 svc—程序及服務控制

標準庫 svc—程序及服務控制

pack pipe timeout 方便 改進 errors type 註銷 elf

對於程序及服務的控制,本質上而言就是正確的啟動,並可控的停止或退出。在go語言中,其實就是程序安全退出、服務控制兩個方面。核心在於系統信號獲取、Go Concurrency Patterns、以及基本的代碼封裝

程序安全退出

執行代碼非安全寫法

在代碼部署後,我們可能因為服務配置發生變化或其他各種原因,需要將服務停止或者重啟。通常就是for循環阻塞,運行代碼,然後通過control+C或者kill來強制退出。代碼如下:

//file svc1.go
package main
import (
    "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.go
package main

import (
    "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.go
package main

import (
    "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.go
package svc

import (
    "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的官方資料:https://blog.golang.org/pipelines

利用這個特征,我們可以通過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.go
package main

import (
    "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信息驅動。由於擴展之後代碼增加,顯得冗余,這裏不再贅述。

轉自:http://blog.csdn.net/qq_26981997/article/details/52275456

標準庫 svc—程序及服務控制