【NSQ源码】Go 实时分布式消息平台 nsqd 的生命周期-初始化、启动和退出

本篇文章涉及到的Go语言的知识点

  • channel 、select
  • os.Signal
  • context.Context
  • sync.Once

fork 源码

访问 GitHub NSQ源码 ,为了以后方便代码管理,我们可以 fork nsq 源码到自己的 GitHub 帐号仓库下,比如我fork 到自己仓库的地址 github.com/geekymv/nsq

下载源码

下载代码到本地机器,访问自己的 GitHub 帐号刚刚 fork 点击 nsq 源码,点击 Code -> SSH -> 复制地址 在自己本地机器执行 git clone 下载源码

git clone [email protected]:geekymv/nsq.git
复制代码

创建 git 分支

git checkout -b nsq-annotated
复制代码

这样我们以后看代码都在这个分支上进行,可以任意修改代码、添加注释等

git push origin nsq-annotated
复制代码

导入到开发工具

nsq 源码的依赖是通过 go mod 管理的,关于 go mod 的使用不熟悉的朋友可以自己搜索资料学习下,我这里就不过多赘述了。 将下载下来的源码导入到自己熟悉的开发工具,我这里导入到 GoLand。

下载依赖

go mod tidy
复制代码

目录结构

源码下载下来了,有了源码我们就有了全部,我们先看下 NSQ 代码目录结构

+-- apps
    +-- nsq_stat
    +-- nsq_tail
    ...
    +-- nsqadmin
    +-- nsqd
         main.go
         ...
    +-- nsqlookupd
    ...
+-- bench
+-- contrib
+-- internal
+-- nsqadmin
+-- nsqd
+-- nsqlookupd
...
复制代码

良好的代码结构非常重要,让我们一看就知道每个目录是干嘛用的,NSQ 的代码结构非常清晰,apps/nsqd 目录下的main.go 就是 nsqd 入口。

启动 nsqd

apps/nsqd 目录下的main.gonsqd 入口,在 GoLand 开发工具中右键 直接执行 Run 'go build main.go' 可能会报如下错误

# command-line-arguments
./main.go:36:13: undefined: nsqdFlagSet
./main.go:46:10: undefined: config

Compilation finished with exit code 2
复制代码

Run 'go build main.go'这种方式只会运行 main.go 文件,nsqd 代码当然不会只有一个文件,我们这里可以以目录的方式运行,在 Goland 的右上方找到 Edit Configurations 并单击,编辑配置,把 Run Kind 改为 Directory ,把下面的 Directory 指定到 main.go 所在的 nsqd 目录,保存,再次运行点击右上角的 RunDebug 即可运行。

nsqd.png

扫描二维码关注公众号,回复: 14471437 查看本文章

运行 NSQD

NSQ 使用 go-svc库来启动 Service,执行 svc.Run 的时候调用 Service 实现的 InitStart 方法, 同时监听两个信号量 syscall.SIGINTsyscall.SIGTERM,当信号量触发的时候调用 Service 实现的 Stop 方法优雅退出。

type program struct {
   once sync.Once
   nsqd *nsqd.NSQD
}

func main() {
   prg := &program{}
   if err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != nil {
      logFatal("%s", err)
   }
}
复制代码

首先定义了结构体 program, 它包含两个成员变量 once sync.Oncensqd *nsqd.NSQDonce 保证退出操作 Stop 只会执行一次,nsqd 就代表我们要启动的 nsqd 服务。

program 实现了 go-svc 中的 ServiceContextHandler 接口,主要是 Service 接口中的3个方法 InitStartStop

svc.png

  • Init

Init 方法主要做了两件事,获取启动配置参数和用这些参数初始化 NSQD, 这些初始化需要的参数信息来源主要有两个,一个是通过命令行参数指定,比如我们之前启动 nsqd 服务使用的 ./nsqd --lookupd-tcp-address=192.168.56.101:4160,另一个是来源是配置文件。

func (p *program) Init(env svc.Environment) error {
   opts := nsqd.NewOptions()
   // 解析命令行参数
   flagSet := nsqdFlagSet(opts)
   flagSet.Parse(os.Args[1:])

   // 使用时间作为随机种子值
   rand.Seed(time.Now().UTC().UnixNano())

   // 启动命令 `nsqd -version` 用于打印版本号,并退出
   if flagSet.Lookup("version").Value.(flag.Getter).Get().(bool) {
      fmt.Println(version.String("nsqd"))
      os.Exit(0)
   }

   // 读取配置文件
   var cfg config
   configFile := flagSet.Lookup("config").Value.String()
   if configFile != "" {
      _, err := toml.DecodeFile(configFile, &cfg)
      if err != nil {
         logFatal("failed to load config file %s - %s", configFile, err)
      }
   }
   cfg.Validate()
   // 将 flagSet 和 config 的配置信息合并到 opts
   options.Resolve(opts, flagSet, cfg)

   // 创建 nsqd
   nsqd, err := nsqd.New(opts)
   if err != nil {
      logFatal("failed to instantiate nsqd - %s", err)
   }
   p.nsqd = nsqd

   return nil
}
复制代码
  • Start

启动一个 goroutine 执行 nsqd 的 Main 函数

func (p *program) Start() error {
   // 加载元数据
   err := p.nsqd.LoadMetadata()
   if err != nil {
      logFatal("failed to load metadata - %s", err)
   }
   // 持久化元数据
   err = p.nsqd.PersistMetadata()
   if err != nil {
      logFatal("failed to persist metadata - %s", err)
   }

   go func() {
      // 启动 nsqd
      err := p.nsqd.Main()
      if err != nil {
         p.Stop()
         os.Exit(1)
      }
   }()

   return nil
}
复制代码
  • Stop 这里使用了 sync.Once 保证退出只会执行一次。
func (p *program) Stop() error {
   // once.Do() 只会执行一次
   p.once.Do(func() {
      p.nsqd.Exit()
   })
   return nil
}
复制代码

我们这里只是大概浏览了InitStartStop 实现了哪些逻辑,具体的我们在后面继续分析。

svc.Run 运行服务

svc.Run() 方法是如何调用 program 实现的3个方法 InitStartStop 的呢

package svc

import (
   "context"
   "os"
   "syscall"
)

// Run runs your Service.
//
// Run will block until one of the signals specified in sig is received or a provided context is done.
// If sig is empty syscall.SIGINT and syscall.SIGTERM are used by default.
func Run(service Service, sig ...os.Signal) error {
   env := environment{}
   // 调用 Init 初始化
   if err := service.Init(env); err != nil {
      return err
   }
    // 调用 Start 启动
   if err := service.Start(); err != nil {
      return err
   }
    // 设置默认的两个信号量
   if len(sig) == 0 {
      sig = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
   }

   signalChan := make(chan os.Signal, 1)
   signalNotify(signalChan, sig...)

   var ctx context.Context
   // 判断 service 是否实现 Context 接口
   if s, ok := service.(Context); ok {
      ctx = s.Context()
   } else {
      ctx = context.Background()
   }

   for {
      select {
      // 会阻塞,等待系统信号量
      case s := <-signalChan:
         // 判断 service 是否实现 Handler 接口
         if h, ok := service.(Handler); ok {
            if err := h.Handle(s); err == ErrStop {
               goto stop
            }
         } else {
            // this maintains backwards compatibility for Services that do not implement Handle()
            goto stop
         }
      // 会阻塞,等待 Done 
      case <-ctx.Done():
         goto stop
      }
   }

stop:
   // 调用 Stop 方法
   return service.Stop()
}

type environment struct{}

func (environment) IsWindowsService() bool {
   return false
}

复制代码

go-svc 这里的实现有点模版方法的意思,接口定义规范(InitStartStop 方法),我们的程序 program 具体实现接口中定义的这些方法,go-svc 提供了统一的入口(Run方法)去执行这行方法逻辑,将整个流程串起来。 这里的设计也是值得我们学习的地方,我们在业务程序中也可以直接复用 go-svc 库,同样会有 InitStartStop 三个步骤。

下一篇文章我们将介绍 nsqd 的启动过程,敬请关注。

猜你喜欢

转载自juejin.im/post/7130574447437676574