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")

乍一看没毛病吧,就是简简单单的读文件写文件嘛。

但是问题来了,这样的代码并不生效。就是虽然每次运行都会发送告警并写入文件,但是并没有完全达到检查去重的效果。说没有完全达到是因为,在两次运行周期内的告警能被过滤掉,但运行一段时间以后,发现某些曾经被告警过的消息也被发了出来,更具体一点是:

  1. 程序检测到警报A、B、C,并发送A、B、C,写入A、B、C到文件
  2. 程序再次检测到A、B,发现文件中已经有过A、B,则不再告警
  3. 程序检测到警报D,发现文件中没有D,告警D并写入文件
  4. 程序再次检测到A,发现文件中没有A,告警A并写入文件
  5. 程序再次检测到警报D,发现文件中没有D,告警D并写入文件
  6. 如此循环

很坑爹啊是不是!

结果

排查问题比较恶心,而且代码写的也完全没有问题,完全能够满足初始设计要求,也就是上述的 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()
	...
comments powered by Disqus