CAT实时监控系统,是面向应用的监控系统,提供了应用性能、日志等监控,包括java的异常堆栈的监控。github地址:https://github.com/dianping/cat ,目前由美团点评监控团队进行维护。本文主要是对CAT中logviw(即:日志)存储设计的介绍。
1.介绍
首先看一下logview的结构:
图1
- 其中每一行,都代表程序中一段代码的耗时,如果有异常,异常堆栈也会被列出来。
- 对于上图的一条日志,我们称为一个logview,每个logview都会对应一个ID,我们称为logviewId,或者msgId
每个日志,都会生成一个唯一的msgId,用户在查看日志时,通过msgId来查询到具体的日志
2.msgId生成逻辑
每个日志,都会生成一个唯一的msgId,用户在查看日志时,通过msgId来查询到具体的日志
a. msgId组成
{domain}-{ip}-{hour}-{index} 示例:ShopWeb-0a010680-375030-2
CAT消息的MessageId格式如上,msgId一共分为四段
- 第一段是应用名shop-web。
- 第二段是当前这台机器的ip的16进制格式,01010680表示10.1.6.108。
- 第三段的375030,是系统当前时间除以小时得到的整点数。即:System.currentTimeMillis()/ONE_HOUR
- 第四段的2,是表示当前这个客户端在当前小时的顺序递增号。
b.msgId生成方法
CAT监控系统是面向应用的监控,每个应用(即:domain)会有很多机器,对于特定的一台机器,其domain、ip是固定的,hour跟随当前小时数进行变化,index就代表当前小时第几个日志。msgId是在客户端的每台机器生成的,生成后的msgId连同其对应的日志一起发送到监控系统服务端,服务端进行处理后,给用户提供查询展示。这样由每台机器进行生成msgId,保证了msgId的唯一性。
index就是简单的一个计数器,其初始值为0。当生成一条日志时,index进行累加1,并把domain、ip、hour拼接在一起,生成msgId。当机器上的进程进行重启时,index数据就会丢失,所以重启后index又从0开始,这样就导致生成的msgId和之前重复了,所以index需要持久化。
CAT监控系统,每3s会持久化一次index,并且通过内存映射文件方式实现,在高性能的情况下,尽量保证index少的重复。
MappedByteBuffer m_byteBuffer = m_markFile.getChannel().map(MapMode.READ_WRITE, 0, 1024 * 1024L);
持久化的文件默认保存在 /data/appdatas/cat目录下,文件名为{domain}.mark
c.多进程
如果同一个服务上的一台机器部署多个进程,每个进程的index都是从0进行累加,各个进程的domin、ip、hour也相同,会造成msgId重复,CAT对于这种多进程(主要是python),会在msgId上加上一个进程id,来解决重复问题。如下
{domain}-{ip}.{pid}-{hour}-{index}
3.日志处理
CAT监控日志如上图1所示,日志在应用机器生成后,会序列化,并加上msgId等对象头,上传到服务端进行处理。日志最终会存储到磁盘中,对应两个文件,一个是数据文件,存储日志本身;另一个是索引文件,用于索引每个日志的位置。处理流程大致如下:
图2
a.写入
对于每条日志,通过解析msgId,就可以获取这条日志上报的domain(应用名)、ip(机器ip)。
- 首先日志上报到服务端后,统一到DefaultMessageDumper类中处理,MessageDumper解析出日志ip,并根据ip 进行hash,将日志放到对应的DefaultMessageProcessor内部队列中,进行处理
- MessageProcessor会维持Map<Domain,Block>对象,其中Domain就是对应日志的应用名称,Block对应一段内存块。Block内部会申请一段大小为256k Data内存块(就是一个数组),用于存放日志;Map<MsgId,offset>用户记录每个日志放到Data中的偏移量。
- MessageProcessor对每条日志进行处理,首先从Map<Domain,Block>找到对应的Block,然后将该日志,通过snappy算法进行压缩,追加到Block中Data内存块中,并且将改日志的msgId、连同日志在Data的偏移量放入Map<MsgId,offset>中。这样,通过msgId,和对应的Block就可以查到该日志。(日志在写入Data内存块中,会在最开始4个字节写入日志的长度,这样通过日志的偏移量就可以定位出日志了)
- 当Block中的Data内存块,写满256K时,会生成一个新的Block,原先的Block会放到DefaultBlcokDumper进行写入磁盘中
- 每一个Block对应一个domain下的所有日志,BlockDumper通过 domain进行hash,将Block放入到DefaultBlockWriter进行磁盘写入。
- BlockWriter将每个Block进行写入磁盘中。他会从LocalBucketManager获取到对应的LocalBucket,通过LocalBucket,将Block数据写入磁盘中。
- LocalBucket将block中的 Data内存块的数据,写入data文件中。Map<MsgId,offset>写入索引文件中
- 每个应用每小时都对应一个LocakBucket,所以日志进行磁盘存储时,每个应用每小时会生成两个文件,一个索引文件、一个数据文件
b.读取
当用户通过msgId进行日志查询是,首先会在内存查找存在对应的Block,如果存在,就在Block中查找日志,如果不存在,就通过LocalBucket进行在磁盘文件上进行查找。
c.日志文件
CAT日志文件默认保存在/data/appdatas/cat/bucket/dump 这个目录
yitiandedeMacBook-Pro:dump yitian$ pwd
/data/appdatas/cat/bucket/dump
yitiandedeMacBook-Pro:dump yitian$ ls
20190602 20191111 20191112
yitiandedeMacBook-Pro:dump yitian$ cd 20191112/
yitiandedeMacBook-Pro:20191112 yitian$ ls
01 04 06 08 09 10 11 12 13 14 15 16 17 18
yitiandedeMacBook-Pro:20191112 yitian$ cd 12
yitiandedeMacBook-Pro:12 yitian$ ls
cat-172.23.55.91.dat cat-172.23.55.91.idx cat-web-172.23.55.91.dat cat-web-172.23.55.91.idx
yitiandedeMacBook-Pro:12 yitian$
比如/data/appdatas/cat/bucket/dump/20191112/12这个目录下,表示2019.11.12日,12点的所有日志文件。其中cat-172.23.55.91.dat 、cat-172.23.55.91.idx分别代表cat应用的数据文件和索引文件(后面的ip是服务器的ip地址)。cat-web-172.23.55.91.dat、cat-web-172.23.55.91.idx代表cat-web应用的数据文件和索引文件。
所以CAT日志文件存储是分小时、分应用分别存储的。日志文件分别对应一个数据文件和索引文件
4.日志存储索引设计
下面我们讲一下,LocalBucket中数据和索引文件的设计。
a. 数据文件
数据文件的存储比较简单,就是把Block中Data数据取出来,直接append到文件中,然后索引文件,会记录每个msgId对应的日志在数据文件的偏移量,这样通过索引文件,就可以查到一个msgId对应的日志。
b.索引文件
我们知道一个数据文件,其实是对应同一个应用(domain)下的所有日志。不同应用会生成不同的文件(通过文件名来区分是哪个应用的日志文件)。索引文件是通过简单的二级索引对日志进行索引的。
1)索引结构
上图显示索引文件的结构。索引文件内部会拆分成很多块,一个块大小为4k*8=32k。其中1个一级索引后面最多会跟着4k-1个二级索引。如果当前的索引文件用满,会在索引文件后面在加上 1个一级索引和4k-1个二级索引,一直这样持续下去。
其中一个索引条目会占用8个字节,这样一个块可以容纳4k个索引条目。
2)索引创建过程
日志写入磁盘时,Block中的Data数据会顺序写入数据文件中,Map<msgId,offset>就会在索引文件中创建索引。(Map<msgId,offset>其实就是Block中的Data数据的全量索引,但是磁盘没有这种kv结构,索引我们要设计索引,更快通过msgId查找到日志)
上图就是整个索引的结构。
我们之前说过索引的每个条目都会占用8个字节。
- 一级索引,每个条目存储的是 ip+index/4k。前4个字节是ip,后四个自己是msgId中的index/4k
- 二级索引,每个条目存储的是 日志在数据文件的偏移量。存储的就是一个偏移量。
Map<msgId,offset> 在索引文件中创建索引过程
- 遍历Map<msgId,offset>,对每个msgId都创建一个索引
- 通过msgId解析出ip、index
- ip、index/4k,这两个值进行拼接成一个8字节的条目,我们称为T
- 遍历一级索引,发现T不存在,则在一级索引最近的一个空位置B,插入T。如果T存在,则返回T所在的位置B。(位置B,其实就是一级索引第几个条目)
- 一级索引的位置B,其实对应的就是第B个二级索引。由于每个块的大小都是固定的,所以第B个二级索引,也就能定位到具体位置。
- index%4k 这个值就是 该msgId 存在二级索引的位置,将对应的offset插入二级索引位置上。
3)读取
用户通过msgId进行读取日志文件的时候,我们通过msgId解析出 domain、hour、ip、index。通过domain、hour定位到具体的文件(每个domain每小时都会生成一个日志文件),然后通过ip、index定位到日志在该文件中的位置。
ip、index定位到日志过程
- 首先我们把一级索引文件内容加载到内存中。
- ip,index/4k ,拼接成8字节的条目T2
- 查找T2在一级索引的位置 B2 (此msgId对应的二级索引就是第B2个二级索引)
- 取出第B2个二级索引,并取出index%4k 位置的数据,该数据就是msgId对应的日志,在数据文件的偏移量。
4)难点
每个索引文件都是块为单位,数据文件也是。所以在进行日志查找时,会把整个块加载到内存中,并缓存一段时间,加速下次的查找,索引内存管理很重要,我们要限制加载块的数量,管理好内存,不然会因为加载太多的块,导致内存溢出。
5.总结
- 通过上面我们可以看到,msgId生成的时候,其实不是一个随机的序列号,CAT通过把domain、ip、时间等信息加入到了msgId生成过程,这样我们通过msgId就可以获取很多有用的信息,进一步也让我们存储设计更加简化、高效。
- 存储设计的时候,以块为单位,减少了IO次数,同时也让我们能够缓存一部分数据。
网友评论