前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个日志分析工具的心路历程

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

原创
作者头像
希望_
修改2019-01-21 11:52:49
6610
修改2019-01-21 11:52:49
举报
文章被收录于专栏:golang开发历程golang开发历程

背景

  • 语言选择:一方面,个人喜好选择了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是一致的,直接使用当前存在的命令文件。反之就对本地的文件进行更新。
  • 注: 如果没有这方面的操作,很多风险点一开始真的是想不到的,做开发的很多时候还是要和大哥大姐们多多探讨探讨,别人真的有很多经验可以给我们新手很多的指导。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 程序上线过程中的一些经历
相关产品与服务
内容分发网络 CDN
内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档