配置解析
nsqd的主函数位于apps/nsqd.go中的main函数
flagSet := nsqFlagset()
flagSet.Parse(os.Args[1:])
首先main函数调用nsqFlagset和Parse进行命令行参数集初始化, 然后判断version参数是否存在,若存在,则打印版本号并退出程序
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
//...
<-signalChan
nsqd.Exit()
接下来钩住系统的syscall.SIGINT和syscall.SIGTERM消息,用来阻塞主goroutine防止退出
var cfg config
configFile := flagSet.Lookup("config").Value.String()
if configFile != "" {
_, err := toml.DecodeFile(configFile, &cfg)
if err != nil {
log.Fatalf("ERROR: failed to load config file %s - %s", configFile, err.Error())
}
}
cfg.Validate()
随后判断config参数是否存在,若存在的话还需进行配置文件的读取, nsq使用toml格式的配置文件,并通过github.com/BurntSushi/toml库进行配置文件的读取和解析
func (cfg config) Validate() {
// special validation/translation
if v, exists := cfg["tls_required"]; exists {
var t tlsRequiredOption
err := t.Set(fmt.Sprintf("%v", v))
if err == nil {
cfg["tls_required"] = t.String()
} else {
log.Fatalf("ERROR: failed parsing tls required %v", v)
}
}
if v, exists := cfg["tls_min_version"]; exists {
var t tlsVersionOption
err := t.Set(fmt.Sprintf("%v", v))
if err == nil {
newVal := fmt.Sprintf("%v", t.Get())
if newVal != "0" {
cfg["tls_min_version"] = newVal
} else {
delete(cfg, "tls_min_version")
}
} else {
log.Fatalf("ERROR: failed parsing tls min version %v", v)
}
}
}
如果配置文件存在并且符合toml格式,则调用cfg.Validate对配置文件的各项进行进一步的合法性检查。 主要是检查配置文件中有关tls的选项(是否支持以及支持的版本)
opts := nsqd.NewOptions()
options.Resolve(opts, flagSet, cfg)
nsqd := nsqd.New(opts)
配置文件检查通过后,创建默认配置opts,并于命令行参数和配置文件进行合并。 合并时用到了github.com/mreiferson/go-options库。 若出现冲突,则优先级从高到低排序依次是命令行、配置文件和默认配置 使用合并后的参数集初始化真正的nsqd对象
nsqd.LoadMetadata()
err := nsqd.PersistMetadata()
if err != nil {
log.Fatalf("ERROR: failed to persist metadata - %s", err.Error())
}
nsqd.Main()
最后,nsqd对象进行初始化和检查后,启动nsqd包的主函数,程序从跳转apps/nsqd.go到nsqd/nsqd.go
初始化
nsqd真正开始运行前需要执行nsqd/nsqd.go中的LoadMetadata和PersistMetadata两个函数
LoadMetadata
// nsqd/nsqd.go
atomic.StoreInt32(&n.isLoading, 1)
defer atomic.StoreInt32(&n.isLoading, 0)
初始化nsqd的LoadMetadata函数使用atomic包中的方法来保证方法执行前和执行后isLoading值的改变
元数据以json格式保存在nsqd可执行文件目录下的*nsqd.%d.dat*中。其中%d为代表该程序的ID, 通过在启动时的命令行worker-id或者配置文件中的id指定。默认ID是通过对主机名散列后获得。 因此保证了同一台机器每次启动的ID相同。
解析元数据的文件得到系统中的存在的topic列表,遍历topic列表中的每个topic:
- 检查topic名称是否合法(长度在1-64之间,满足正则表达式
^[\.a-zA-Z0-9_-]+(#ephemeral)?$) ,若不合法则忽略 - 使用
GetTopic函数通过名字获得topic对象 - 判断当前topic对象是否处于暂停状态,是的话调用
Pause函数暂停topic - 获取当前topic下所有的channel,并且遍历channel,执行的操作与topic基本一致
- 检查channel名称是否合法(长度在1-64之间,满足正则表达式
^[\.a-zA-Z0-9_-]+(#ephemeral)?$) ,若不合法则忽略 - 使用
GetChannel函数通过名字获得channel对象 - 判断当前channel对象是否处于暂停状态,是的话调用
Pause函数暂停channel
- 检查channel名称是否合法(长度在1-64之间,满足正则表达式
至此,元数据的载入完成
PersistMetadata
PersistMetadata将当前的topic和channel信息写入*nsqd.%d.dat*文件中, 主要步骤是忽略#ephemeral结尾的topic和channel后将topic和channel列表json序列化后写回文件中
写入文件时先创建扩展名为tmp的临时文件,写入内容后并保存后再调用atomicRename函数将tmp文件重命名为*nsqd.%d.dat*。 其中atomicRename函数在windows和其他操作系统下实现方式不同,分别位于nsqd/rename_windows.go 和rename.go中。在Linux下直接调用了os.Rename函数,而Windows下则使用Win32
API实现了文件的重命名。 这是因为go的早期版本中Windows下调用os.Rename函数时如果重命名后的文件已经存在则会失败。 这个bug在os: make Rename atomic on Windows中提到, 并且已经在os: windows Rename should overwrite destination file.提交中被修复,
因此,Golang1.5不存在这一bug
Main
在Main函数中,nsqd真正开始运行。Main监听tcp,https(如果设置了相关参数),http端口并通过WaitGroupWrapper的Wrap函数以goroutine方式启动主要的组件。
type WaitGroupWrapper struct {
sync.WaitGroup
}
func (w *WaitGroupWrapper) Wrap(cb func()) {
w.Add(1)
go func() {
cb()
w.Done()
}()
}
其中WaitGroupWrapper是对sync.WaitGroup的简单包装
执行完Main函数后,配置和初始化工作全部完成,各个组件启动运行,而主goroutine会阻塞在<-signalChan处,直到收到中断程序的信号,随后执行nsqd.Exit函数。 Exit函数将进行socket关闭等清理工作,随后结束整个程序的运行。