文章目录
nsqd入口文件分析
./apps/nsqd/main.go
nsq项目都使用了go-svc这个包,使用方式如下
func main() {
prg := &program{}
// 运行程序
if err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != nil {
log.Fatal(err)
}
}
// 初始化方法
func (p *program) Init(env svc.Environment) error {
fmt.Println("init.....")
return nil
}
// 启动方法
func (p *program) Start() error {
fmt.Println("start.....")
fmt.Println(syscall.Getpid())
go func() {
ticker := time.NewTicker(2 * time.Second)
for t := range ticker.C {
fmt.Println("tick at", t)
}
}()
return nil
}
// 结束方法
func (p *program) Stop() error {
fmt.Println("stop.....")
}
// linux下收到信号时触发调用,可用于自定义信号处理,若为svc.ErrStop则直接走结束流程
func (p *program) Handle(s os.Signal) error {
return svc.ErrStop
}
// 返回程序启动关闭的上下文对象,用于程序内部通知外部优雅结束
func (p *program) Context() context.Context {
return p.ctx
}
这里我们只需要实现program结构体下的接口方法即可,实现完后使用svc.Run(prg, syscall.SIGINT, syscall.SIGTERM)
方式就可以让go-svc包自动调用我们实现的接口方法,进行条理化的完成程序的生命周期
nsqd初始化流程
nsqd重点分了五步完成
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())
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()
options.Resolve(opts, flagSet, cfg)
nsqd, err := nsqd.New(opts)
if err != nil {
logFatal("failed to instantiate nsqd - %s", err)
}
p.nsqd = nsqd
return nil
}
一、初始化nsqd.Options对象
这里是做opts对象的初始化同时赋默认值,后面会详细讲解其中的用法
opts := nsqd.NewOptions()
二、初始化flag.FlagSet对象
给flagSet对象初始化,解析命令行后续所有命令到flagSet对象中,参数opts中的默认值在nsqdFlagSet方法中用于命令行缺省值设置,需要注意opts对象中的值未作更新
flagSet := nsqdFlagSet(opts)
flagSet.Parse(os.Args[1:])
三、解析config并校验
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()
从命令行中如果读取到了config文件路径就会解析文件中的数据到cfg对象中,然后通过cfg.Validate()
函数校验cfg对象的数据,并更新cfg对象的值
四、更新nsqd.Options对象
options.Resolve(opts, flagSet, cfg)
在此方法内部解析flagSet、cfg对象,将数据更新到opts对象中,优先级如下
- 命令行获取
- 弃用的命令Key获取
- 配置文件config中获取
- 命令行给的默认值获取
- 结构体初始化给的初始值
具体赋值相关重要代码如下
var v interface{}
if hasArg(flagSet, flagName) { // 检查命令行是否将flagName对象传递进来,若是则赋值
v = flagInst.Value.(flag.Getter).Get()
} else if deprecatedFlagName != "" && hasArg(flagSet, deprecatedFlagName) { // 检查命令行是否将弃用命令deprecatedFlagName 对象传递进来,若是则赋值
v = deprecatedFlag.Value.(flag.Getter).Get()
log.Printf("WARNING: use of the --%s command line flag is deprecated (use --%s)",
deprecatedFlagName, flagName)
} else if cfgVal, ok := cfg[cfgName]; ok { // 检查配置文件config是否存在cfgName对应值,若是则赋值
v = cfgVal
} else if getter, ok := flagInst.Value.(flag.Getter); ok { // 检查命令行是否设置默认值,若是则赋值
// if the type has a Get() method, use that as the default value
v = getter.Get()
} else { // 赋值opts对象已存在的默认值
// otherwise, use the struct's default value
v = val.Field(i).Interface()
}
fieldVal := val.FieldByName(field.Name)
if fieldVal.Type() != reflect.TypeOf(v) {
newv, err := coerce(v, fieldVal.Interface())
if err != nil {
log.Fatalf("ERROR: Resolve failed to coerce value %v (%+v) for field %s - %s",
v, fieldVal, field.Name, err)
}
v = newv
}
fieldVal.Set(reflect.ValueOf(v)) // 更新opts对象数据
五、初始化nsqd对象
在这里会根据opts对象中的数据给内部所有需要用到的资源对象做统一的初始化,同时还会加载磁盘中的数据到nsqd进程中以完成nsqd进程的基本协程的初始化
nsqd, err := nsqd.New(opts)
if err != nil {
logFatal("failed to instantiate nsqd - %s", err)
}
p.nsqd = nsqd
初始化nsqd对象会按以下步骤逐一完成
- 第一步就是去检查是否设置了磁盘数据存放的根路径,没有设置的情况下会设置当前可执行文件的上级路径为磁盘数据存放根路
- 第二步判断日志对象有没有被初始化,如果没有也会进行一个初始化
- 第三步就是nsqd对象的详细初始化,详细请见注释
func New(opts *Options) (*NSQD, error) {
var err error
dataPath := opts.DataPath
if opts.DataPath == "" { // 检查是否设置磁盘数据存放根路径,若未设置则设置当前可执行文件的上级路径为磁盘数据存放根路径
cwd, _ := os.Getwd()
dataPath = cwd
}
if opts.Logger == nil { // 未设置日志对象,则初始化日志对象
opts.Logger = log.New(os.Stderr, opts.LogPrefix, log.Ldate|log.Ltime|log.Lmicroseconds)
}
n := &NSQD{
startTime: time.Now(), // nsqd启动时间,用于nsqd运行信息和统计信息展示
topicMap: make(map[string]*Topic), // topicMap对象,用于存放所有的topic与channel之间的关系
exitChan: make(chan int), // nsqd服务退出通知通道
notifyChan: make(chan interface{}), // 通知通道,用于topic、channel对象创建和退出信号的通知使用
optsNotificationChan: make(chan struct{}, 1), // 配置修改通道,用于修改时热加载配置信息
dl: dirlock.New(dataPath), // data目录锁
}
n.ctx, n.ctxCancel = context.WithCancel(context.Background())
httpcli := http_api.NewClient(nil, opts.HTTPClientConnectTimeout, opts.HTTPClientRequestTimeout)
n.ci = clusterinfo.New(n.logf, httpcli) // nsqd仅用于同步topic下注册的所有channel,后续会讲解所使用的地方
n.lookupPeers.Store([]*lookupPeer{}) // 缓存空的服务发现对象列表,后面lookupLoop协程会更新,用于n.ci对象同步注册信息的参数
n.swapOpts(opts) // 缓存opts对象到n.opts对象中
n.errValue.Store(errStore{}) // 设置空错误结构体,后面用于http协议下的ping命令检查返回
err = n.dl.Lock() // 上目录锁,这里内部具体方法还未做实现,目前形同虚设
if err != nil {
return nil, fmt.Errorf("failed to lock data-path: %v", err)
}
if opts.MaxDeflateLevel < 1 || opts.MaxDeflateLevel > 9 { // 控制客户端deflate压缩数据等级范围[1,9],用于客户端下使用compress/flate包对应的NewWriter方法,代码fw, _ := flate.NewWriter(conn, level)
return nil, errors.New("--max-deflate-level must be [1,9]")
}
if opts.ID < 0 || opts.ID >= 1024 { // 节点ID范围控制[0,1024)
return nil, errors.New("--node-id must be [0,1024)")
}
if opts.TLSClientAuthPolicy != "" && opts.TLSRequired == TLSNotRequired { // tls客户端身份验证策略检查,若存在则修改设置漏掉的参数tls-required
opts.TLSRequired = TLSRequired
}
tlsConfig, err := buildTLSConfig(opts) // 根据opts对象生成tls配置对象
if err != nil {
return nil, fmt.Errorf("failed to build TLS config - %s", err)
}
if tlsConfig == nil && opts.TLSRequired != TLSNotRequired {
return nil, errors.New("cannot require TLS client connections without TLS key and cert")
}
n.tlsConfig = tlsConfig
for _, v := range opts.E2EProcessingLatencyPercentiles { // 性能评估百分位数范围限制(0.00,1.00]
if v <= 0 || v > 1 {
return nil, fmt.Errorf("invalid E2E processing latency percentile: %v", v)
}
}
n.logf(LOG_INFO, version.String("nsqd")) // nsqd版本号输出
n.logf(LOG_INFO, "ID: %d", opts.ID) // nsqd节点ID
n.tcpServer = &tcpServer{nsqd: n} // 初始化tcpServer对象
n.tcpListener, err = net.Listen(util.TypeOfAddr(opts.TCPAddress), opts.TCPAddress) // 初始化tcpListener对象
if err != nil {
return nil, fmt.Errorf("listen (%s) failed - %s", opts.TCPAddress, err)
}
if opts.HTTPAddress != "" { // HTTPAddress存在则初始化httpListener对象
n.httpListener, err = net.Listen(util.TypeOfAddr(opts.HTTPAddress), opts.HTTPAddress)
if err != nil {
return nil, fmt.Errorf("listen (%s) failed - %s", opts.HTTPAddress, err)
}
}
if n.tlsConfig != nil && opts.HTTPSAddress != "" { // tlsConfig和HTTPSAddress 存在则初始化httpsListener对象
n.httpsListener, err = tls.Listen("tcp", opts.HTTPSAddress, n.tlsConfig)
if err != nil {
return nil, fmt.Errorf("listen (%s) failed - %s", opts.HTTPSAddress, err)
}
}
if opts.BroadcastHTTPPort == 0 { // 广播HTTP端口号不存在则将httpListener对应端口号做广播端口号
tcpAddr, ok := n.RealHTTPAddr().(*net.TCPAddr)
if ok {
opts.BroadcastHTTPPort = tcpAddr.Port
}
}
if opts.BroadcastTCPPort == 0 { // 广播TCP端口号不存在则将tcpListener对应端口号做广播端口号
tcpAddr, ok := n.RealTCPAddr().(*net.TCPAddr)
if ok {
opts.BroadcastTCPPort = tcpAddr.Port
}
}
if opts.StatsdPrefix != "" { // 设置统计前缀时则更新统计前缀
var port string = fmt.Sprint(opts.BroadcastHTTPPort)
statsdHostKey := statsd.HostKey(net.JoinHostPort(opts.BroadcastAddress, port))
prefixWithHost := strings.Replace(opts.StatsdPrefix, "%s", statsdHostKey, -1)
if prefixWithHost[len(prefixWithHost)-1] != '.' {
prefixWithHost += "."
}
opts.StatsdPrefix = prefixWithHost
}
return n, nil
}
nsqd启动流程
nsqd启动分了三步完成
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() {
err := p.nsqd.Main()
if err != nil {
p.Stop()
os.Exit(1)
}
}()
return nil
}
一、加载元数据
err := p.nsqd.LoadMetadata()
if err != nil {
logFatal("failed to load metadata - %s", err)
}
nsqd加载元数据会按以下步骤逐一完成,详细请见注释
func (n *NSQD) LoadMetadata() error {
atomic.StoreInt32(&n.isLoading, 1) // 设置nsqd对象为加载状态
defer atomic.StoreInt32(&n.isLoading, 0) // LoadMetadata方法退出时取消加载状态
fn := newMetadataFile(n.getOpts()) // 获取nsqd.dat文件地址
data, err := readOrEmpty(fn) // 获取nsqd.dat文件数据
if err != nil {
return err
}
if data == nil { // nsqd第一次启动,没有任何元数据
return nil // fresh start
}
var m Metadata
err = json.Unmarshal(data, &m) // 序列化元数据
if err != nil {
return fmt.Errorf("failed to parse metadata in %s - %s", fn, err)
}
for _, t := range m.Topics { // 遍历初始化topic
if !protocol.IsValidTopicName(t.Name) {
n.logf(LOG_WARN, "skipping creation of invalid topic %s", t.Name)
continue
}
topic := n.GetTopic(t.Name) // ***重点:获取topic对象,此方法下逻辑相对比较复杂,下面会提出来详解
if t.Paused {
topic.Pause()
}
for _, c := range t.Channels { // 遍历初始化topic对应的所有channel
if !protocol.IsValidChannelName(c.Name) {
n.logf(LOG_WARN, "skipping creation of invalid channel %s", c.Name)
continue
}
channel := topic.GetChannel(c.Name) // 获取channel对象,channel不存在则创建同时通知到topic的channelUpdateChan通道中,使topic对应的消息泵messagePump更新需要同步分发消息msg的channel列表
if c.Paused {
channel.Pause()
}
}
topic.Start() // 通知此topic对应的消息泵messagePump启动
}
return nil
}
1. 详解(n *NSQD) GetTopic方法
func (n *NSQD) GetTopic(topicName string) *Topic {
// 很可能我们已经有了这个主题topic,所以请先尝试读取锁定
n.RLock()
t, ok := n.topicMap[topicName]
n.RUnlock()
if ok {
return t
}
// 前面读取锁定未拿到指定主题topic,现在写锁定同样尝试获取(防止并发模式下释放读锁的一瞬间加入了此主题topic)
n.Lock()
t, ok = n.topicMap[topicName]
if ok {
n.Unlock()
return t
}
// 上面绝对的保证了主题topic不存在,同时加入了写锁不可能在操作期间并发创建此主题topic,所以下面进行topic对象的真正创建
deleteCallback := func(t *Topic) {
n.DeleteExistingTopic(t.name)
}
t = NewTopic(topicName, n, deleteCallback) // ***重点:此方法下逻辑相对比较复杂,下面会提出来详解
n.topicMap[topicName] = t
n.Unlock()
n.logf(LOG_INFO, "TOPIC(%s): created", t.name)
// 主题已创建完成,但消息泵尚未启动,后面会由topic.Start()方法通过通道方式启动消息泵
// 如果该主题是在启动时加载元数据时创建的,请不要进行任何进一步的初始化(加载完成后将“启动”该主题);由于加载配置更新了n.isLoading字段为1,所以这里会提前结束
if atomic.LoadInt32(&n.isLoading) == 1 {
return t
}
// 如果使用lookupd,请进行阻塞调用以获取通道并立即创建它们,以确保所有通道都接收到已发布的消息;上面创建新的主题topic对象后需要检查服务发现lookupd中是否已注册对应的topic,若注册则同步对应topic下所有的频道channel
lookupdHTTPAddrs := n.lookupdHTTPAddrs()
if len(lookupdHTTPAddrs) > 0 {
// 遍历所有lookupd服务器获取所有频道channel(去重频道channel列表)
channelNames, err := n.ci.GetLookupdTopicChannels(t.name, lookupdHTTPAddrs)
if err != nil {
n.logf(LOG_WARN, "failed to query nsqlookupd for channels to pre-create for topic %s - %s", t.name, err)
}
for _, channelName := range channelNames {
if strings.HasSuffix(channelName, "#ephemeral") { // 带有#ephemeral前缀的频道channel是临时频道
continue // 不要在没有消费者客户端的情况下创建临时频道
}
t.GetChannel(channelName) // 获取channel对象,channel不存在则创建同时通知到topic的channelUpdateChan通道中,使topic对应的消息泵messagePump更新需要同步分发消息msg的channel列表
}
} else if len(n.getOpts().NSQLookupdTCPAddresses) > 0 { // 由于lookupdHTTPAddrs列表为空,则检查配置中的NSQLookupdTCPAddresses是否为空,若存在则打印错误日志(没有可用的nsqlookupd服务来查询是否需要为主题topic同步已存在的所有频道channel)
n.logf(LOG_ERROR, "no available nsqlookupd to query for channels to pre-create for topic %s", t.name)
}
// 现在已经添加了所有通道,启动该topic对应的消息泵messagePump
t.Start()
return t
}
2. 详解NewTopic方法
func NewTopic(topicName string, nsqd *NSQD, deleteCallback func(*Topic)) *Topic {
t := &Topic{
name: topicName,
channelMap: make(map[string]*Channel), // 初始化频道channelMap对象
memoryMsgChan: make(chan *Message, nsqd.getOpts().MemQueueSize), // 初始化指定大小的内存消息通道,对应参数mem-queue-size。默认10000
startChan: make(chan int, 1), // 初始化启动通道,由方法t.Start()触发通知
exitChan: make(chan int), // 初始化退出通道,由(t *Topic) Delete()和(t *Topic) Close()两个方法触发通知
channelUpdateChan: make(chan int), // 初始化频道channel更新通道,当此topic下频道有新增或删除时触发通知
nsqd: nsqd, // 初始化nsqd对象,用于便捷获取对象中的参数和此topic下的消息推送失败等信息同步以及对象中Notify方法的调用
paused: 0, // 初始化暂停信号【0:启动,1:暂停】,用于此topic对象是否向下面所有频道channel分发消息
pauseChan: make(chan int), // 初始化暂停通道,当触发暂停或启动时会通知消息泵messagePump更新是否继续分发消息状态
deleteCallback: deleteCallback, // 初始化删除回调方法,此topic若时临时的ephemeral=true,在没有频道channel对象的情况下将调用此方法
idFactory: NewGUIDFactory(nsqd.getOpts().ID), // 根据nsqd对象的node-id生成一个guid工厂,用于生成消息对象唯一的消息id
}
if strings.HasSuffix(topicName, "#ephemeral") { // topic名字包含#ephemeral后缀则视为临时topic对象
t.ephemeral = true // 标记为临时topic
t.backend = newDummyBackendQueue() // 初始化一个虚拟磁盘队列,仅临时topic对象占位使用,无任何作用
} else {
dqLogf := func(level diskqueue.LogLevel, f string, args ...interface{}) {
opts := nsqd.getOpts()
lg.Logf(opts.Logger, opts.LogLevel, lg.LogLevel(level), f, args...)
}
t.backend = diskqueue.New( // ***重点:初始化磁盘队列对象,此方法中存在磁盘队列的管理逻辑,下面会提出来详解
topicName,
nsqd.getOpts().DataPath, // 磁盘数据存放根路径data-path,没有默认值,若为空则设置当前可执行文件的上级路径为磁盘数据存放根路径
nsqd.getOpts().MaxBytesPerFile, // 每个磁盘文件存储的最大容量max-bytes-per-file,默认100MB
int32(minValidMsgLength), // 最小有效消息长度26(16+8+2)
int32(nsqd.getOpts().MaxMsgSize)+minValidMsgLength, // 最大有效消息长度1048602(1024*1024+16+8+2)
nsqd.getOpts().SyncEvery, // 设置磁盘队列的读取操作次数值sync-every,达到此值时异步持久化一次fsync(默认2500次)
nsqd.getOpts().SyncTimeout, // 设置磁盘队列的轮循时间sync-timeout,达到此值时异步持久化一次fsync(默认为2s)
dqLogf, // 磁盘队列日志对象
)
}
t.waitGroup.Wrap(t.messagePump) // ***重点:异步调用messagePump方法,此方法逻辑相对比较复杂,下面会提出来详解
t.nsqd.Notify(t, !t.ephemeral) // 异步调用nsqd.Notify方法将topic对象通过notifyChan通道注册到所有的lookup服务中
return t
}
3. 详解diskqueue.New方法
func New(name string, dataPath string, maxBytesPerFile int64,
minMsgSize int32, maxMsgSize int32,
syncEvery int64, syncTimeout time.Duration, logf AppLogFunc) Interface {
d := diskQueue{
name: name,
dataPath: dataPath, // 磁盘数据存放根路径data-path,没有默认值,若为空则设置当前可执行文件的上级路径为磁盘数据存放根路径
maxBytesPerFile: maxBytesPerFile, // 每个磁盘文件存储的最大容量max-bytes-per-file,默认100MB
minMsgSize: minMsgSize, // 最小有效消息长度26(16+8+2)
maxMsgSize: maxMsgSize, // 最大有效消息长度1048602(1024*1024+16+8+2)
readChan: make(chan []byte), // 初始化读取磁盘数据通道
depthChan: make(chan int64), // 初始化磁盘队列消息总数通知通道
writeChan: make(chan []byte), // 初始化写入磁盘数据通道
writeResponseChan: make(chan error), // 初始化写入磁盘结果输出(写入成功返回nil,否则返回对应错误error对象)
emptyChan: make(chan int), // 初始化清空磁盘数据通道,收到通知将会删除磁盘队列存储的所有数据
emptyResponseChan: make(chan error), // 初始化清空磁盘结果输出(写入成功返回nil,否则返回对应错误error对象)
exitChan: make(chan int), // 初始化退出通道,收到通知后关闭磁盘队列消息循环(d *diskQueue) ioLoop
exitSyncChan: make(chan int), // 初始化同步退出通道,收到通知则优雅退出磁盘队列对象
syncEvery: syncEvery, // 设置磁盘队列的读取操作次数值sync-every,达到此值时异步持久化一次fsync(默认2500次)
syncTimeout: syncTimeout, // 设置磁盘队列的轮循时间sync-timeout,达到此值时异步持久化一次fsync(默认为2s)
logf: logf, // 磁盘队列日志对象
}
// 根据停止状态(磁盘元数据文件diskqueue.meta.dat)恢复磁盘队列状态【磁盘消息队列大小、读取文件编号、读取文件偏移量、写入文件编号、写入文件偏移量】
err := d.retrieveMetaData()
if err != nil && !os.IsNotExist(err) {
d.logf(ERROR, "DISKQUEUE(%s) failed to retrieveMetaData - %s", d.name, err)
}
go d.ioLoop() // ***重点:异步启动磁盘队列管理中心,磁盘管理核心方法,下面会提出来详解
return &d
}
4. 详解(d *diskQueue) ioLoop方法
// 此方法是nsqd的核心方法之一(磁盘队列管理)
func (d *diskQueue) ioLoop() {
var dataRead []byte
var err error
var count int64
var r chan []byte
syncTicker := time.NewTicker(d.syncTimeout) // 根据磁盘队列的轮循时间生成轮询定时器对象syncTicker
for {
// 不要一直同步
if count == d.syncEvery { // 读取操作次数达到sync-every(默认2500次)时,设置需要同步持久化数据
d.needSync = true
}
if d.needSync {
err = d.sync() // 写磁盘对象开启同步数据,同步完成后更新d.needSync = false
if err != nil {
d.logf(ERROR, "DISKQUEUE(%s) failed to sync - %s", d.name, err)
}
count = 0 // 重新计算读取操作次数
}
if (d.readFileNum < d.writeFileNum) || (d.readPos < d.writePos) { // 当读取文件小于写入文件编号或读取偏移量小于写入文件偏移量时进行下面的操作
if d.nextReadPos == d.readPos {
dataRead, err = d.readOne() // 读取偏移量和下次读取偏移量相等时读取此偏移量下的数据并更新下次读取偏移量
if err != nil {
d.logf(ERROR, "DISKQUEUE(%s) reading at %d of %s - %s",
d.name, d.readPos, d.fileName(d.readFileNum), err)
d.handleReadError()
continue
}
}
r = d.readChan // 更新监听的读取文件通道对象
} else {
r = nil // 关闭监听的读取文件通道对象
}
select {
// Go通道规范规定无通道操作(读或写)
// 在跳过的选择中,只有当有数据要读取时,我们才会将r设置为d.readChan
case r <- dataRead:
count++
// moveForward在删除文件时设置needSync标志
d.moveForward() // 判断下次读取的文件编号是否和读取文件编号一致,若不一致(代表读取文件编号下的所有消息已消费完)则删除读取文件编号下的整个文件
case d.depthChan <- d.depth: // 若通道未阻塞,则此时存在获取磁盘消息数的需求,将磁盘消息数depth推送到磁盘队列消息总数通知通道中depthChan
case <-d.emptyChan: // 存在清空通知时执行下面逻辑
d.emptyResponseChan <- d.deleteAllFiles() // 删除磁盘队列存储的所有数据(重置所有状态),并返回删除结果输出(写入成功返回nil,否则返回对应错误error对象)
count = 0 // 重新计算读取操作次数
case dataWrite := <-d.writeChan: // 若写入磁盘数据通道存在数据则执行下面逻辑
count++
d.writeResponseChan <- d.writeOne(dataWrite) // 写入消息数据到指定写入文件编号下的指定偏移量下,并返回写入磁盘结果输出(写入成功返回nil,否则返回对应错误error对象)
case <-syncTicker.C: // 达到磁盘队列的轮循时间判断是否需要同步持久化磁盘数据
if count == 0 {
// 没有活动时避免同步
continue
}
d.needSync = true
case <-d.exitChan: // 收到退出信号,走退出逻辑
goto exit
}
}
exit:
d.logf(INFO, "DISKQUEUE(%s): closing ... ioLoop", d.name)
syncTicker.Stop() // 关闭磁盘队列轮询定时器对象syncTicker
d.exitSyncChan <- 1 // 通知退出流程可以继续往下执行
}
5. 详解(t *Topic) messagePump方法
// 此方法是nsqd的核心方法之一(消息核心管理)
func (t *Topic) messagePump() {
var msg *Message
var buf []byte
var err error
var chans []*Channel
var memoryMsgChan chan *Message
var backendChan <-chan []byte
// 不要在Start()之前传递消息,但要避免Pause()或GetChannel()方法引起对应通道阻塞
for {
select {
case <-t.channelUpdateChan: // 此topic下的频道channel有新增或删除时触发通知,这里避免阻塞
continue
case <-t.pauseChan: // 此topic有暂停或启动操作时触发通知,这里避免阻塞
continue
case <-t.exitChan: // 此topic收到退出信号时,直接走退出流程
goto exit
case <-t.startChan: // 收到topic的启动通知,启动消息泵开始处理数据
}
break
}
t.RLock()
for _, c := range t.channelMap { // 读锁下加载此topic下的所有频道channel对象
chans = append(chans, c)
}
t.RUnlock()
if len(chans) > 0 && !t.IsPaused() { // 此topic下存在频道且topic是启动状态时更新监听的内存消息通道对象和磁盘数据通道对象
memoryMsgChan = t.memoryMsgChan
backendChan = t.backend.ReadChan()
}
// 消息循环(核心)
for {
select {
case msg = <-memoryMsgChan: // 实时接受内存消息通道提供的消息对象
case buf = <-backendChan: // 实时接受磁盘数据通道提供的消息数据
msg, err = decodeMessage(buf) // 解码消息数据到消息对象中
if err != nil {
t.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err)
continue
}
case <-t.channelUpdateChan: // 此topic下的频道channel有新增或删除通知,这里收到后及时更新频道channel列表
chans = chans[:0] // 删除频道列表中的所有频道
t.RLock()
for _, c := range t.channelMap { // 读锁下加载此topic下的所有频道channel对象
chans = append(chans, c)
}
t.RUnlock()
if len(chans) == 0 || t.IsPaused() { // 此topic下不存在频道或topic是暂停状态时关闭监听的内存消息通道对象和磁盘数据通道对象
memoryMsgChan = nil
backendChan = nil
} else { // 此topic下存在频道且topic是启动状态时更新监听的内存消息通道对象和磁盘数据通道对象
memoryMsgChan = t.memoryMsgChan
backendChan = t.backend.ReadChan()
}
continue
case <-t.pauseChan: // 收到启动或暂停通知时,检查是否需要关闭或更新监听的内存消息通道对象和磁盘数据通道对象
if len(chans) == 0 || t.IsPaused() {
memoryMsgChan = nil
backendChan = nil
} else {
memoryMsgChan = t.memoryMsgChan
backendChan = t.backend.ReadChan()
}
continue
case <-t.exitChan: // 此topic收到退出信号时,直接走退出流程
goto exit
}
for i, channel := range chans {
chanMsg := msg
// 复制消息对象,因为每个通道都需要一个唯一的实例
// 如果是第一个频道channel(主题已经创建了第一个消息对象),则跳过复制
if i > 0 {
chanMsg = NewMessage(msg.ID, msg.Body) // 保证每个频道获取的消息对象对应的地址唯一
chanMsg.Timestamp = msg.Timestamp
chanMsg.deferred = msg.deferred
}
if chanMsg.deferred != 0 { // 如果时延迟消息,则放到延迟队列中
channel.PutMessageDeferred(chanMsg, chanMsg.deferred) // 放到延迟队列前会根据延迟时间升序排序,调用container/heap包下的heap.Push方法
continue
}
err := channel.PutMessage(chanMsg) // 将消息放到频道channel的实时消息队列中(内存消息通道或磁盘消息队列)
if err != nil {
t.nsqd.logf(LOG_ERROR,
"TOPIC(%s) ERROR: failed to put msg(%s) to channel(%s) - %s",
t.name, msg.ID, channel.name, err)
}
}
}
exit:
t.nsqd.logf(LOG_INFO, "TOPIC(%s): closing ... messagePump", t.name)
}
二、持久元数据
err = p.nsqd.PersistMetadata()
if err != nil {
logFatal("failed to persist metadata - %s", err)
}
nsqd持久元数据会按以下步骤逐一完成,详细请见注释
func (n *NSQD) PersistMetadata() error {
// 持久化我们所拥有的主题topic和频道channel信息
fileName := newMetadataFile(n.getOpts())
n.logf(LOG_INFO, "NSQ: persisting topic/channel metadata to %s", fileName)
data, err := json.Marshal(n.GetMetadata(false)) // json序列化元数据结构体,准备持久化到元数据文件中
if err != nil {
return err
}
tmpFileName := fmt.Sprintf("%s.%d.tmp", fileName, rand.Int())
err = writeSyncFile(tmpFileName, data) // 同步到临时元数据文件中
if err != nil {
return err
}
err = os.Rename(tmpFileName, fileName) // 将临时元数据文件名更新为正式的元数据文件名
if err != nil {
return err
}
// technically should fsync DataPath here
return nil
}
三、启动nsqd主程序服务
go func() {
err := p.nsqd.Main() // nsqd主程序启动
if err != nil { // 发生异常时调用停止流程
p.Stop()
os.Exit(1)
}
}()
启动nsqd主程序服务会按以下步骤逐一完成,详细请见注释
func (n *NSQD) Main() error {
exitCh := make(chan error) // 初始化错误通知通道
var once sync.Once
exitFunc := func(err error) { // 临时错误通知方法,所有的核心服务必须通过此方法包装才能将错误信息通知到错误通道中
once.Do(func() {
if err != nil {
n.logf(LOG_FATAL, "%s", err)
}
exitCh <- err
})
}
n.waitGroup.Wrap(func() { // ***重点:协程方式启动nsqd的TCP服务,内容复杂后续文章详解
exitFunc(protocol.TCPServer(n.tcpListener, n.tcpServer, n.logf))
})
if n.httpListener != nil { // 存在HTTP的net.Listener对象时执行下面逻辑
httpServer := newHTTPServer(n, false, n.getOpts().TLSRequired == TLSRequired)
n.waitGroup.Wrap(func() { // ***重点:协程方式启动nsqd的HTTP服务,内容复杂后续文章详解
exitFunc(http_api.Serve(n.httpListener, httpServer, "HTTP", n.logf))
})
}
if n.httpsListener != nil { // 存在HTTPS的net.Listener对象时执行下面逻辑
httpsServer := newHTTPServer(n, true, true)
n.waitGroup.Wrap(func() { // ***重点:协程方式启动nsqd的HTTPS服务,服务内容与http一致
exitFunc(http_api.Serve(n.httpsListener, httpsServer, "HTTPS", n.logf))
})
}
n.waitGroup.Wrap(n.queueScanLoop) // ***重点:协程方式启动queueScanLoop服务,处理消费中和延迟优先级队列中的消息,内容较多下面讲解里面的逻辑。
n.waitGroup.Wrap(n.lookupLoop) // 协程方式启动lookupLoop服务,用于同步所有的topic和channel到每个服务发现lookup服务中。
if n.getOpts().StatsdAddress != "" { // 存在统计服务时执行下面逻辑
n.waitGroup.Wrap(n.statsdLoop) // 协程方式启动statsdLoop服务,使用UDP协议周期上传nsqd服务状态信息
}
err := <-exitCh // 阻塞等待退出信号,有错误时返回error对象,无错误时返回nil
return err
}
1. 详解(n *NSQD) queueScanLoop方法
// queueScanLoop在单个goroutine中运行,以处理消费中和延迟优先级队列。
// 它管理一个队列ScanWorker池(可配置的最大值为
// QueueScanWorkerPoolMax(默认值:4))同时处理通道。
//
// 它复制Redis的概率过期算法:它每隔QueueScanInterval(默认值:100ms)就随机选择
// QueueScanSelectionCount(默认值:20)个本地缓存列表中的频道加入到处理频道channel方法中检查处理
//
// 每隔QueueScanRefreshInterval(默认值:5s)将更新一次频道channel列表。
//
// 如果任何一个队列都有工作要做,则通道被认为是“脏的”。
//
// 如果所选频道的QueueScanDirtyPercent(默认值:25%)为脏通道,则循环随机处理选中的频道列表,直到小于QueueScanDirtyPercent值。
func (n *NSQD) queueScanLoop() {
workCh := make(chan *Channel, n.getOpts().QueueScanSelectionCount) // 根据queue-scan-selection-count生成指定大小需要检查处理频道channel的通道
responseCh := make(chan bool, n.getOpts().QueueScanSelectionCount) // 根据queue-scan-selection-count生成指定大小需要接受处理频道channel结果的通道
closeCh := make(chan int) // 生成关闭通道,根据此通道可以通知到所有处理频道的方法中
workTicker := time.NewTicker(n.getOpts().QueueScanInterval) // 生成轮询检查定时器(固定100毫秒)
refreshTicker := time.NewTicker(n.getOpts().QueueScanRefreshInterval) // 生成轮询刷新定时器(固定5秒)
channels := n.channels() // 获取nsqd对象下的所有频道channel对象
n.resizePool(len(channels), workCh, responseCh, closeCh) // 开启指定大小(1 <= pool <= min(len(channels) * 0.25, QueueScanWorkerPoolMax))的处理频道channel方法(协程方式启动)
for {
select {
case <-workTicker.C: // 到达轮询检查时间片,检查是否处理的频道channel数是否为0,若为0跳过后续处理逻辑等待下一个触发通道通知
if len(channels) == 0 {
continue
}
case <-refreshTicker.C: // 到达刷新时间片,重新获取所有频道channel对象并按新频道数开放需要的处理频道channel方法(协程方式启动)
channels = n.channels()
n.resizePool(len(channels), workCh, responseCh, closeCh)
continue
case <-n.exitChan: // 收到退出信号,走退出流程
goto exit
}
// 根据queue-scan-selection-count和频道channel的长度生成最小num(用于随机频道加入到处理频道channel方法中检查处理)
num := n.getOpts().QueueScanSelectionCount
if num > len(channels) {
num = len(channels)
}
loop:
for _, i := range util.UniqRands(num, len(channels)) { // 生成num个随机channels的下标数组
workCh <- channels[i] // 通过通道放到处理频道channel方法中检查处理
}
numDirty := 0 // 用于统计此次处理中频道channel列表中需要处理的个数
for i := 0; i < num; i++ {
if <-responseCh {
numDirty++
}
}
if float64(numDirty)/float64(num) > n.getOpts().QueueScanDirtyPercent { // 当处理命中率达到QueueScanDirtyPercent(默认0.25)时继续轮询处理
goto loop
}
}
exit:
n.logf(LOG_INFO, "QUEUESCAN: closing")
close(closeCh)
workTicker.Stop()
refreshTicker.Stop()
}