文章目录
前言
nsqd启动加锁流程及源码分析
总结
前言
前面几篇博客我们讲了nsq是什么,nsq的安装等,大家想过下面这样的问题没有,就是
问题:一个目录下能启动多个nsqd进程吗?
答案:不能
问题:为什么不能呢?它是怎么实现的呢?
答案:nsqd需要保存topic,channel等数据到文件中,文件名都是nsqd.dat。如果启动多个nsqd进程,这些进程保存的文件会冲突或相互覆盖。所以一个nsqd进程启动时会对启动目录加锁,其他nsqd进程若再加锁该目录就会失败,最终启动失败
问题:一个目录下能启动多个nsqlookupd,nsqadmin吗?
答案:可以,因为这些进程没有读取,保存文件的需求,各进程绑定的端口不一致即可
这一篇博客我们就讲nsqd进程启动时加锁目录的实现原理
nsqd启动加锁流程及源码分析
先看下nsqd进程启动的步骤,nsqd启动的main函数在文件 nsq\apps\nsqd\main.go,main函数源码如下(已加注释)
// nsqd的启动入口
func main() {
prg := &program{}
// Run内部会调用Init(),Start(),监听到这两个信号时会调用Stop()
if err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != nil {
logFatal("%s", err)
}
}
要注意的是:nsqd,nsqlookupd,nsqadmin等所有进程都是使用了go-svc包,这个包很简单,就是三个函数 Init(), Start(), Stop(),各个使用者实现了这三个接口就行
上面的main()里面调用了svc.Run(),这个函数内部会调用实现者的Init()函数
nsqd实现的Init()源码如下(已加注释)
// nsqd的初始化,主要是配置及检测相关,最后会创建nsqd实例
func (p *program) Init(env svc.Environment) error {
// 生成nsqd的默认配置
opts := nsqd.NewOptions()
......
// 重点:根据opts新建一个nsqd实例
nsqd, err := nsqd.New(opts)
......
}
省略了一些不重要的代码,nsqd的Init函数主要就是生成配置opts,然后使用opts来创建nsqd实例
重点来了:nsqd.New(opts),源码如下(已加注释)
// 根据配置opts新建一个nsqd对象
func New(opts *Options) (*NSQD, error) {
var err error
// 没有指定运行路径,就使用当前目录
dataPath := opts.DataPath
if opts.DataPath == "" {
cwd, _ := os.Getwd()
dataPath = cwd
}
......
n := &NSQD{
startTime: time.Now(),
topicMap: make(map[string]*Topic),
exitChan: make(chan int),
notifyChan: make(chan interface{}),
optsNotificationChan: make(chan struct{}, 1),
dl: dirlock.New(dataPath), // 创建目录锁
}
......
// 加锁运行的目录
err = n.dl.Lock()
if err != nil {
return nil, fmt.Errorf("failed to lock data-path: %v", err)
}
......
}
上面的代码,我们主要关注的几个点
1. 如果启动配置中没有指定data目录,nsqd会以当前目录为运行目录,赋给dataPath
2. 创建nsqd实例时,会以dataPth来创建一个目录锁dl,即dl : dirlock.New(dataPath)
3. 创建nsqd实例后,会调用dl.Lock()对目录进行加锁,加锁失败就返回了,也就是nsqd进程启动失败
核心:dl.Lock()这里真正实现了目录的加锁
有意思的是,dirlock包对不同的系统实现的加锁方式也不同,但是只有linux下dirlock.go真正有实现,另外的illumos.go,dirlock_windows.go的实现都是空的。
dirlock包位置在nsq/internal/dirlock,如下图
我们只看linux下dirlock.go即可,代码如下(已加注释)
// 目录锁(内部靠文件锁来实现)
type DirLock struct {
dir string // 要锁定的目录(全路径),锁定时会把这个目录当成文件来锁定
f *os.File // 以该目录创建的文件对象
}
// 加锁
func (l *DirLock) Lock() error {
f, err := os.Open(l.dir) // linux下目录也是文件,可以像文件一样打开
if err != nil {
return err
}
l.f = f
// 锁定文件(LOCK_EX表放置互斥锁, LOCK_NB表非阻塞锁)
err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return fmt.Errorf("cannot flock directory %s - %s (possibly in use by another instance of nsqd)", l.dir, err)
}
return nil
}
// 解锁
func (l *DirLock) Unlock() error {
defer l.f.Close() // 关闭文件
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) // LOCK_UN表解锁
}
上面代码,主要就两个函数
Lock() 加锁,内部调用syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB)
fd是文件打开后的fd
LOCK_EX :加锁标记。只有一个进程能加锁成功,其他进程再尝试加锁时会阻塞,等同于我们常用的写锁
LOCK_NB :不阻塞标记。如果其他进程已加锁成功,自己去尝试加锁时就不再阻塞,而是直接返回错误
Unlock() 解锁,内部调用 syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
LOCK_UN :解锁标记。如果自己已经加锁成功,可以用此标记去解锁
nsqd进程在关闭的时候,会调用Stop()函数,函数内部会调用dl.Unlock()对目录进行解锁,代码如下(已加注释)
func (n *NSQD) Exit() {
......
// 解锁目录
n.dl.Unlock()
n.logf(LOG_INFO, "NSQ: bye")
n.ctxCancel()
}
总结
1. nsqd进程之所以要对目录加锁是因为nsqd需要保存topic,channel等数据到文件,且文件名字固定为nsqd.dat。nsqd进程启动时需要加载文件以恢复数据。不加锁的话,多个nsqd进程会导致文件冲突,相互覆盖
2. nsqd进程在启动时,创建nsqd对象后就进行了加锁目录,加锁失败则进程启动失败
3. nsqd进程在退出时,会解锁目录
4. nsqd进程加锁,解锁目录用的是syscall包,最终使用的是函数syscall.Flock(),其中LOCK_EX标记用来加锁,LOCK_UN标记用来解锁
如果大家想对go语言的syscall包有更详细地了解,可以参考我的这篇博客
golang文件锁,目录锁,syscall包的使用_YZF_Kevin的博客-CSDN博客