zoukankan      html  css  js  c++  java
  • 标准库 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
    }
    

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

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

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

    • 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接口,我们通过几种不同的方法来获取不同的实现。包括:

    WithDeadlineWithTimeout,获取到基于时间相关的退出句柄,以控制服务退出。
    WithCancel,获取到cancelFunc句柄,以控制服务的退出。
    WithValue,获取到k-v键值对,实现类似于session信息保存的业务支持。
    BackgroundTODO,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.
    ", name)
            }
        }
    OutLoop:
        fmt.Printf("%s is end.
    ", 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

  • 相关阅读:
    StringHelper类的代码也写得不错,值得好好学习学习
    开发感慨
    取地址参数的方法
    在C#中运用SharpZipLib和unrar进行解压缩
    我的模板分析引擎类PHP的.net开发方法功能介绍篇
    分析模板的一段简单快速的算法片段
    关于文件操作的一些感悟
    关于自定义模板的设计
    JS中的函数、Bom、DOM及JS事件 pixel
    什么是CSS盒模型及利用CSS对HTML元素进行定位的实现(含h5/css3新增属性) pixel
  • 原文地址:https://www.cnblogs.com/yorkyang/p/8143683.html
Copyright © 2011-2022 走看看