上一篇: Go消息中间件Nsq系列(三)------apps/nsq_to_nsq源码阅读
apps/nsq_to_file程序
功能描述: nsq客户端读取(消费)所有的topic数据,然后写入到文件,通过配置阈值或定时时间去切割消息记录文件
通过此次nsq_to_file程序源码阅读, 可以学习到
- flag参数, goroutine,channel,select使用案例
- 一些os,file的api, 比如os.HostName, OpenFile()
- 消费者consumer 结合channel,select使用
- go的接口实现封装, 例如FileLogger的Writer
- 文件的同步, fsync , page cache这个可以去百度一下
- gzip的使用,特别是关闭后再重新新开一个在Close方法,
- 其他慢慢体会
apps/nsq_to_file 目录结构
file_logger.go // 文件写出封装类
nsq_to_file.go // 程序入口
options.go // 程序配置参数
strftime.go // 时间格式化
topic_discoverer.go // 根据配置或者服务发现获取所有的topic开始消费消息, 监听信号进行收尾工作
程序执行从nsq_to_file.go, main()入口开始
func main() {
// 解析输入参数, 与 默认opt 进行合并
// 然后一系列参数效验
fs := flagSet()
fs.Parse(os.Args[1:])
// ... 省略
// 填充配置数据, 监听singal信号,并以此做为topicDiscoverer构造参数,
cfg := nsq.NewConfig()
// ... 省略
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
// 开始消费消息, 写出文件
discoverer := newTopicDiscoverer(logf, opts, cfg, hupChan, termChan)
discoverer.run()
}
main() 实现了解析命令行参数与默认options合并, 然后在构造默认Config, 在监听三个Signal信号源, 通过这些参数作为TopicDiscoverer的构造函数, 并调用了discover.run()方法,下面接着看
topic_discoverer.go的run()方法
func (t *TopicDiscoverer) run() {
var ticker <-chan time.Time
// 如果没有配置topics, 则通过配置的定时器去lookupd查找
if len(t.opts.Topics) == 0 {
ticker = time.Tick(t.opts.TopicRefreshInterval)
}
// 为新topic进行分配goroutine消费消息写出文件
t.updateTopics(t.opts.Topics)
forloop:
for {
select {
case <-ticker:
// 获取所有的topics
newTopics, err := t.ci.GetLookupdTopics(t.opts.NSQLookupdHTTPAddrs)
// ... 省略
t.updateTopics(newTopics)
case <-t.termChan:
//收中断,退出信号,遍历topics逐个关闭消费,并退出循环
for _, fl := range t.topics {
close(fl.termChan)
}
break forloop
case <-t.hupChan:
// 收到hup信号, 遍历topics逐个往fl.hupChan发送通知
for _, fl := range t.topics {
fl.hupChan <- true
}
}
}
// Wait()方法阻塞直到WaitGroup计数器减为0。
t.wg.Wait()
}
run() 实现了如果配置了topics那么将不使用定时轮询去lookupd去获取所有topics,否则将使用配置的轮询时间去调用ci.GetLookupdTopics(),获取所有的topics.获取的topics交由函数updateTopics()去处理. 然后监听signal信号处理程序中断,关闭释放工作
func (t *TopicDiscoverer) updateTopics(topics []string) {
for _, topic := range topics {
if _, ok := t.topics[topic]; ok {
continue
}
if !t.isTopicAllowed(topic) {
continue
}
fl, err := NewFileLogger(t.logf, t.opts, topic, t.cfg)
if err != nil {
continue
}
t.topics[topic] = fl
t.wg.Add(1)
go func(fl *FileLogger) {
fl.router()
t.wg.Done()
}(fl)
}
}
updateTopics() 方法功能主要是遍历所有topics 检查是否已经分配消息消费写出(fileLogger), 是否过滤该topic,否则分配fileLogger,使用waitGroup 添加goroutine异步调用fileLogger.router() 方法
整个nsq_to_file代码的核心就在file_logger, FileLogger实现了Handler,Writer接口,在NewFileLogger()的时候,通过传递进来的配置,topic去计算输出文件名称,初始化FileLogger,然后初始化消费者,连接进行消费消息
func NewFileLogger(logf lg.AppLogFunc, opts *Options, topic string, cfg *nsq.Config) (*FileLogger, error) {
// 计算文件名
computedFilenameFormat, err := computeFilenameFormat(opts, topic)
// 初始化 消息消费者
consumer, err := nsq.NewConsumer(topic, opts.Channel, cfg)
f := &FileLogger{
// ... 省略, 实现了 HandleMessage(m *nsq.Message) error 方法
}
// 接收到的消息通过channel (logChan )转发,在router()方法进行处理
consumer.AddHandler(f)
// 连接消费者
//consumer.ConnectToNSQDs(opts.NSQDTCPAddrs)
//consumer.ConnectToNSQLookupds(opts.NSQLookupdHTTPAddrs)
return f, nil
}
在初始化FileLogger后 调用router( ) 方法开始处理数据
func (f *FileLogger) router() {
pos := 0
output := make([]*nsq.Message, f.opts.MaxInFlight)
sync := false
// 同步定时器
ticker := time.NewTicker(f.opts.SyncInterval)
closeFile := false
exit := false
for {
select {
// 接收到consumer停止信号, 退出循环 关闭并文件
case <-f.consumer.StopChan:
sync = true
closeFile = true
exit = true
// 接收到中断,程序退出信号, 关闭定时器,停止消息消费,同步文件
case <-f.termChan:
ticker.Stop()
f.consumer.Stop()
sync = true
// 接收到挂起暂停信号, 关闭并同步文件
case <-f.hupChan:
sync = true
closeFile = true
// 同步定时器, 是否需要切割文件, 在进行同步
case <-ticker.C:
if f.needsRotation() {
if f.opts.SkipEmptyFiles {
closeFile = true
} else {
f.updateFile()
}
}
sync = true
// 接收到新的消息, 是否需要切割文件, 然后写入数据流, 并记录阈值 满了在同步
case m := <-f.logChan:
if f.needsRotation() {
f.updateFile()
sync = true
}
_, err := f.Write(m.Body)
// ... 省略
_, err = f.Write([]byte("\n"))
// ... 省略
output[pos] = m
pos++
if pos == cap(output) {
sync = true
}
}
// 同步标志 或者 可能RDY为0但是未关闭的状态
if sync || f.consumer.IsStarved() {
if pos > 0 {
// ... 省略
err := f.Sync() // fsync同步
// ... 省略
// 阈值递减, 消息反馈已消费, 释放
for pos > 0 {
pos--
m := output[pos]
m.Finish()
output[pos] = nil
}
}
// 重置标志位
sync = false
}
// FileLogger Close() 方法功能概要
// 1. 释放文件资源之前先同步数据
// 2. 如果需要,将文件从工作目录移动到输出目录,注意不要覆盖现有文件,如果文件移动出现失败, 使用rev序号拼接文件名进行重试
if closeFile {
f.Close()
closeFile = false
}
// 退出循环
if exit {
break
}
}
}
router( ) 方法主要功能是开启定时同步落盘数据, 等待接收新消息,记录阈值,满值在进行同步落盘. 等待挂起,中断,退出信号去关闭同步文件,停止consumer消费.
router中调用的updateFile()方法代码如下,
func (f *FileLogger) updateFile() {
// FileLogger Close() 方法功能概要
// 1. 释放文件资源之前先同步数据
// 2. 如果需要,将文件从工作目录移动到输出目录,注意不要覆盖现有文件,如果文件移动出现失败, 使用rev序号拼接文件名进行重试
f.Close() // uses current f.filename and f.rev to resolve rename dst conflict
// 文件名格式 "...log",
filename := f.currentFilename()
if filename != f.filename {
f.rev = 0 // reset revision to 0 if it is a new filename
} else {
f.rev++
}
f.filename = filename
f.openTime = time.Now()
// 创建文件夹
fullPath := path.Join(f.opts.WorkDir, filename)
err := makeDirFromPath(f.logf, fullPath)
if err != nil {
f.logf(lg.FATAL, "[%s/%s] unable to create dir: %s", f.topic, f.opts.Channel, err)
os.Exit(1)
}
var fi os.FileInfo
// 文件命名冲突检测
// 需要开启gzip和需要切割文件的情况新建,或者就是追加模式
// 切割文件判断时间 和 文件大小
// 打开文件流,
for ; ; f.rev++ {
absFilename := strings.Replace(fullPath, "", fmt.Sprintf("-%06d", f.rev), -1)
// If we're using a working directory for in-progress files,
// proactively check for duplicate file names in the output dir to
// prevent conflicts on rename in the normal case
if f.opts.WorkDir != f.opts.OutputDir {
outputFileName := filepath.Join(f.opts.OutputDir, strings.TrimPrefix(absFilename, f.opts.WorkDir))
err := makeDirFromPath(f.logf, outputFileName)
if err != nil {
f.logf(lg.FATAL, "[%s/%s] unable to create dir: %s", f.topic, f.opts.Channel, err)
os.Exit(1)
}
_, err = os.Stat(outputFileName)
if err == nil {
f.logf(lg.WARN, "[%s/%s] output file already exists: %s", f.topic, f.opts.Channel, outputFileName)
continue // next rev
} else if !os.IsNotExist(err) {
f.logf(lg.FATAL, "[%s/%s] unable to stat output file %s: %s", f.topic, f.opts.Channel, outputFileName, err)
os.Exit(1)
}
}
openFlag := os.O_WRONLY | os.O_CREATE
if f.opts.GZIP || f.opts.RotateInterval > 0 {
openFlag |= os.O_EXCL
} else {
openFlag |= os.O_APPEND
}
f.out, err = os.OpenFile(absFilename, openFlag, 0666)
if err != nil {
if os.IsExist(err) {
f.logf(lg.WARN, "[%s/%s] working file already exists: %s", f.topic, f.opts.Channel, absFilename)
continue // next rev
}
f.logf(lg.FATAL, "[%s/%s] unable to open %s: %s", f.topic, f.opts.Channel, absFilename, err)
os.Exit(1)
}
f.logf(lg.INFO, "[%s/%s] opening %s", f.topic, f.opts.Channel, absFilename)
fi, err = f.out.Stat()
if err != nil {
f.logf(lg.FATAL, "[%s/%s] unable to stat file %s: %s", f.topic, f.opts.Channel, f.out.Name(), err)
}
f.filesize = fi.Size()
if f.opts.RotateSize > 0 && f.filesize > f.opts.RotateSize {
f.logf(lg.INFO, "[%s/%s] %s currently %d bytes (> %d), rotating...",
f.topic, f.opts.Channel, f.out.Name(), f.filesize, f.opts.RotateSize)
continue // next rev
}
break // good file
}
if f.opts.GZIP {
f.gzipWriter, _ = gzip.NewWriterLevel(f.out, f.opts.GZIPLevel)
f.writer = f.gzipWriter
} else {
f.writer = f.out
}
}
还有其他函数,例如currentFilename(),needsRotation(),makeDirFromPath(),exclusiveRename(),computeFilenameFormat()就不细说了