Golang-Open-File
很久之前遇到的问题了,清任务的时候这个write排第一,就写一下。
起因
要做一个临时的告警系统,又不想引入DB,也不想用bolt,不想引入额外的包,所以就傻了吧唧的自己用文件实现一个简单的DB。
(现在回想起来真的很没必要,老老实实用boltdb多好,简单快速搞得定)
每一条告警都有自己的hash,如果在文件中没有发现这条hash,则发送告警并将hash写入文件,如果发现了则跳过,目的是防止重复告警。
很简单的逻辑,但bug是,还是会发送重复的告警,也就是说,如果这条告警半小时之前发过了,那么半小时之后还会再发。(程序是用cron job跑的,每半小时跑一次,查询各个状态,如果指标低于阈值则告警)
经过
程序的某个部分是这样写的:
fileName := "/tmp/alert-history.txt"
f, err := os.OpenFile(fileName, os.O_RDWR, 0644)
if err != os.ErrNotExist {
panic(fmt.Sprintf("open file %s error: %s", fileName, err))
} else {
f, err = os.Create(fileName)
if err != nil {
panic(fmt.Sprintf("create file %s error: %s", fileName, err))
}
}
defer f.Close()
// detect alarm
// ...
// if alarm not in file
// then send alarm and
// write its hash to the file
// ...
// f.WriteString("alarm-hash\n")
乍一看没毛病吧,就是简简单单的读文件写文件嘛。
但是问题来了,这样的代码并不生效。就是虽然每次运行都会发送告警并写入文件,但是并没有完全达到检查去重的效果。说没有完全达到是因为,在两次运行周期内的告警能被过滤掉,但运行一段时间以后,发现某些曾经被告警过的消息也被发了出来,更具体一点是:
- 程序检测到警报A、B、C,并发送A、B、C,写入A、B、C到文件
- 程序再次检测到A、B,发现文件中已经有过A、B,则不再告警
- 程序检测到警报D,发现文件中没有D,告警D并写入文件
- 程序再次检测到A,发现文件中没有A,告警A并写入文件
- 程序再次检测到警报D,发现文件中没有D,告警D并写入文件
- 如此循环
很坑爹啊是不是!
结果
排查问题比较恶心,而且代码写的也完全没有问题,完全能够满足初始设计要求,也就是上述的 1,2 部分。然而诡异的地方是4,5,6。
已经忘记当时是怎么排查的了,但结果是一定的,没有append,也就是说,新写入的内容会覆盖曾经的内容,从0开始seek写。就现象而言,如果能发现4.5.6这个规律,也能很简单的定位到是D把A覆盖了(当时没有考虑到这一点,总结上面的4,5,6,是现编的),而且写入的时候是警报的哈希,毫无规律的MD5,也没有特别注意。
其实是一个很简单的问题,但如果没踩过的话,就容易被自己坑了。
我记得一开始的版本还不是用的os.OpenFile()
,而是 os.Create()
,这样就更坑自己了。
且看注释:
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
// Create creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned
// File can be used for I/O; the associated file descriptor has mode
// O_RDWR.
// If there is an error, it will be of type *PathError.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
// OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.) and perm (before umask), if applicable. If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
testlog.Open(name)
return openFileNolog(name, flag, perm)
}
os.Create
的时候会truncate这个文件并从头写入!
感觉有时候太高级了也不好,容易漏掉很多细节。(其实就是自己不仔细,没认真看人家的注释!)
再copy一下其他的注释:
// Flags to OpenFile wrapping those of the underlying system. Not all
// flags may be implemented on a given system.
const (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // open the file read-only.
O_WRONLY int = syscall.O_WRONLY // open the file write-only.
O_RDWR int = syscall.O_RDWR // open the file read-write.
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing.
O_CREATE int = syscall.O_CREAT // create a new file if none exists.
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist.
O_SYNC int = syscall.O_SYNC // open for synchronous I/O.
O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened.
)
最后,修改之后的代码是这样的:
fileName := "/tmp/alert-history.txt"
f, err := os.OpenFile(fileName, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
panic(fmt.Sprintf("open file %s error: %s", fileName, err))
}
defer f.Close()
...