一个日志分析工具的心路历程

2019-01-21 11:52:49 浏览数 (1)

背景

  • 语言选择:一方面,个人喜好选择了golang,另一方面,编译型语言,理论上速度会好一些。
  • 其他原因:历史工具是shell使用各种linux命令实现的,在过滤日志这一块不是很精准。

工具构想

  • 解决当前存在的问题,日志查询不完整
  • 效率更高效
  • 分析功能更全面
  • 支持多种输出,方便后续告警分析使用

工具实现历程

工具设想:

  • 首先,如何准确的找到日志数据?
    • 日志目录下会有很多的日志文件,各式各样的日志内容
    • 日志会不断的回滚,每一种日志都会有回滚数量个日志文件
  • 解决方案的迭代过程
  • 筛选必要文件
    • 第一个版本
      • 个人的想法是做一些下面类似的结构体来表示每一个文件
代码语言:javascript复制
// AccessFile 访问日志对象
type AccessFile struct {
    FirstLine *AccessLog    //文件第一行
    LastLine *AccessLog     //文件最后一行
    Stat os.FileInfo     //文件句柄
    Filename string      //文件名 
    File *os.File。         //文件句柄 
    StartFlag int64         //需要数据匹配的第一个位置   
    EndFlag int64           //需要数据匹配的最后一个位置  
    All bool                //需求内容全包含  
    Some bool               //需求内容部分包含 
}   
 
读取第一行和最后一行的函数是这样的: 
// ReadFileFirstLine 读取文件的第一行 
func ReadFileFirstLine(filename string) (line string) { 
    file, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) 
    defer file.Close() 
    if err != nil { 
        panic(err) 
    } 
    var linebyte = make([]byte, 5*logger.KB) 
    length, err := file.Read(linebyte) 
    if length < 0 { 
        return "" 
    } 
    if err != nil && err != io.EOF { 
        panic(err) 
    } 
    linebuf := bytes.NewReader(linebyte) 
    linebufio := bufio.NewReader(linebuf) 
    lineb, _, err := linebufio.ReadLine() 
    if err != nil { 
        panic(err) 
    } 
    if err == io.EOF { 
        return 
    } 
    return string(lineb) 
} 
// ReadFileLastLine 读取文件的最后一行 
func ReadFileLastLine(filename string) (line string) { 
    stat, err := os.Stat(filename) 
    if err != nil { 
        panic(err) 
    } 
    file, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) 
    defer file.Close() 
    if err != nil { 
        panic(err) 
    } 
    var linebyte = make([]byte, 5*logger.KB) 
    indexlog := stat.Size() - 5*logger.KB 
    if indexlog < 0 { 
        indexlog = 0 
    } 
    DeBugPrintln("file: ", filename, "filesize:", stat.Size()) 
    length, err := file.ReadAt(linebyte, indexlog) 
    if length < 0 { 
        return "" 
    } 
    if err != nil && err != io.EOF { 
        panic(err) 
    } 
    linebuf := string(linebyte) 
    linelist := strings.Split(linebuf, "n") 
    if len(linelist) < 2 { 
        return "" 
    } 
    line = linelist[len(linelist)-2:][0] 
    DeBugPrintln(string(line)) 
    return line 
} 
  • 通过如上的结构体,映射每一个对应目录下的日志文件,然后通过下面一个比较笨的方法筛除没用的文件
代码语言:javascript复制
伪思维:
假设我们需要的日志区间是 logstime ~ logetime (logstime 是小于 logetime的)
一个文件第一行和最后一行的时间是 filestime ~ fileetime  (回想写日志的场景, filestime 必定 小于 fileetime)
然后会有如下六种情况:
1.  Filestime -> fileetime —> logstime —> logetime     f.All=false , f.Some=false 
2.  Filestime -> logstime -> fileetime -> logetime     f.All=false , f.Some=true 
3.  Logstime -> filestime -> logetime -> fileetime     f.All=false , f.Some=true 
4.  Logstime -> logetime -> filestime -> fileetime     f.All=false , f.Some=false 
5.  Filestime -> logstime -> logetime -> Fileetime     f.All=false , f.Some=true 
6.  Logstime -> Filestime -> Fileetime -> Logetime.    f.All=true , f.Some=false
通过如上算法实现,我们可以筛选掉一些不在我们需要的范围内的文件,然后留下需要分析的文件处理。 
  • 第二个版本
    • 经过和一些开发同学的讨论,发现其实每一个文件是有一个mtime的,我们一般需要的日志信息所在的日志文件,mtime是应该在日志需要时间之内,或者是在之后,所以上述筛选可以优化一下
代码语言:javascript复制
代码逻辑:
假设文件的修改时间为mtime, 日志区间还是logstime ~ logetime 
会有如下几种情况: 
mtime -> logstime -> logetime    直接忽略,一定不会包含我们需要的文件 
Logstime -> mtime -> logstime    一定包含我们需要的信息

Logstime -> logetime -> mtime    可能包含我们需要的信息
  • 至此,我们找到了所有的包含我们需要的信息的日志文件。
  • 筛选后的文件信息读取处理
    • 第一个版本
      • 判断文件抽象对象的 All 和 Some 变量,如果All = true ,说明文件全匹配。
      • 通过一次遍历所有的结构体,然后筛选掉所有的不含所需内容的对象,清理掉
      • 接下来依次处理剩下的文件对象,都是包含我们所需要内容的文件,直接从文件句柄开始抓取文件内容,然后如果some是true使用正则找到我们需要的第一个数据,然后开始读取数据进行加载,将有效数据放到fiterpro中。
      • 此处有坑,在后期实践中发现的,因为大多数的时候日志文件都是蛮大的,然后就会引发一个问题,这里正则需要将文件全部加载进内存,然后就很容易导致内存不够用,程序挂掉。
    • 第二个版本
      • 判断、筛选文件模块还是没有变,就是使用第一行和最后一行比对时间
      • 主要就是处理上面全部加载日志然后进行匹配的问题,这个明显是很不合理的,因为我们不一定需要这么多信息,同时这么多的信息加载也是需要消耗很多的I/O的 
      • 解决方案:将文件切分成指定大小的块(比如50M),然后偏移数据去读取,同时,对块的处理,程序速度会快很多,而且读取小块的数据,I/O也不会有太大的压力,这样可能是会遗漏掉一些数据,但是对于日志信息的处理,这一点点数据是完全可以接受的。
      • 代码块:
代码语言:javascript复制
 for _, uf := range upro.LogFile { 
        var n int64 
        DeBugPrintln(uf.Filename) 
        for n < uf.Stat.Size() { 
            var linedata = make([]byte, zonesize) 
            nu, err := uf.File.ReadAt(linedata, n) 
            DeBugPrintln(nu, n, err) 
            if err != nil && err != io.EOF { 
                break 
            } 
            wg.Add(1) 
            go proUpstreamLogFile(uf.All, uf.Some, linedata, upro, host, directory, &wg) 
            n  = int64(nu) 
        } 
        wg.Wait() 
        if upro.AllNum >= upro.MaxSize { 
            break 
        } 
    } 
  • 数据过滤处理
    • 第一个版本
      • 经过上面的处理我们已经抓取到了需要的数据到我们的过滤器中了。
      • 过滤器数据处理,筛选出我们需要的数据。
      • 此处按需处理,暂时没有什么大的改动,就是尽量减少程序重复无用执行的次数。

程序上线过程中的一些经历

背景:日志都在服务器上存储,当有上千台的服务需要处理日志时,我们应该这么做呢?

下面是我对于我遇到的问题的思考:

  • 命令文件下发服务器,是不是可以使用CDN,因为文件如果比较大2M,几千个多则上万的下载还是有压力的。
  • 命令是通用功能是不是可以下发一次,保存在本地下次直接调用就ok? 
  • 文件更新应该怎么做呢? 哦,好像可以算md5,比对md5,然后决定程序是否需要更新。

下面是我当前实施的方案:

  • 当我们需要调用文件数据时,在调用前将最新的命令文件的md5下发到本地,然后本地先查看是否有命令文件存在,如果不存在直接下载最新的命令文件,然后执行下发指令的操作。如果存在,就加载本地的文件计算下md5和服务器下发的md5进行比对,如果两边计算的md5是一致的,直接使用当前存在的命令文件。反之就对本地的文件进行更新。
  • 注: 如果没有这方面的操作,很多风险点一开始真的是想不到的,做开发的很多时候还是要和大哥大姐们多多探讨探讨,别人真的有很多经验可以给我们新手很多的指导。

最后感谢各位的查阅,有更多的想法,欢迎和我一起探讨。

0 人点赞