golang服务控制实践 - svc包

对于程序及服务的控制,本质上而言就是正确的启动,并可控的停止或退出。在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
}

此时,我们实现了程序退出时的信号拦截,补充业务代码就可以了。但实际业务逻辑至少涉及到初始化、业务处理、退出三大块,代码量多了,会显得比较混乱,这就需要规范代码的结构。

执行代码的改进:信号拦截包装器

考虑上述情况,我们将正常的程序定义为:

  1. Init: 系统初始化,比如识别操作系统、初始化服务发现Consul、Zookeper的agent、数据库连接池等。
  2. Start:程序主要业务逻辑,包括但不限于数据加载、服务注册、具体业务响应。
  3. 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的实例,通常情况下足以使用。

单机多服务的应用启动、退出框架

在网络应用,可能会有更复杂的情况,我们需要考虑:

  1. 程序启动
  2. 程序不退出的情况下,多服务启动、并行运行与退出
  3. 程序退出,并清理运行中的服务

可以做一个简单的Demon程序,来实现以上三点,其中,程序退出可以通过键盘输入命令,也可以Control+D。基于golang1.7,我们可以采用以下知识点:

  1. 利用cancelContext来控制服务的退出
  2. 利用之前实现的svc来实现程序的安全退出
  3. 利用os.Stdin来获取键盘输入命令来模拟服务加载与退出的消息驱动。实际可能是网络rpc或http数据触发

golang1.7的context包

我们知道,当通道chan被close之后,任何<-chan都会得到立即执行。如果不清楚,可以查阅相关资料或写个测试代码,最好研读golang的官方资料:Go Concurrency Patterns: Pipelines and cancellation。

利用这个特征,我们可以通过golang1.7标准库新增的context包,通过注入的方式来实现全局或单个服务的控制。
context中定义了Context接口,我们通过几种不同的方法来获取不同的实现。包括:

  1. WithDeadline\WithTimeout,获取到基于时间相关的退出句柄,以控制服务退出。
  2. WithCancel,获取到cancelFunc句柄,以控制服务的退出。
  3. WithValue,获取到k-v键值对,实现类似于session信息保存的业务支持。
  4. WithValue,获取到k-v键值对,实现类似于session信息保存的业务支持。

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()
    }()
}

代码运行的时候,可以:

  1. 通过输入”s-“或者”start-“+服务名,来启动一个服务
  2. 用”c-“或”cancel-“+服务名,来退出指定服务
  3. 可以用 “exit”或者Control+C、kill来退出程序(除了kill -9)。

在此基础上,还可以利用context包实现服务超时退出,利用for range限制服务数量,利用HTTP实现微服务RestFUL信息驱动。由于扩展之后代码增加,显得冗余,这里不再赘述。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容