golang的服務控制實踐

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

對於程式及服務的控制,本質上而言就是正確的啟動,並可控的停止或退出。在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資訊驅動。由於擴充之後代碼增加,顯得冗餘,這裡不再贅述。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.