作者:datonli,腾讯 WXG 后台开发工程师
开发在定位问题时需要查找日志,但企业微信业务模块日志存储在本机磁盘,这会造成以下问题:
我们希望有这样一个日志系统:
公司内外有很多日志系统方案,根据是否对日志做全文检索可以分为两类:
全文检索的日志系统:对日志内容切分词和建倒排,通过查询关键词的倒排取交集支持模糊匹配,这类系统一般入库资源消耗较多,也不支持日志统计,典型实现有:ELK、Hermes 以及腾讯云日志服务(Cloud Log Service, CLS)等系统;
部分字段检索的日志系统:只对部分字段建索引,支持特定字段的快速检索,入库资源消耗较低,但是这类系统对模糊匹配未能很好支持,也不支持日志统计,不支持模块级全量日志查询,如 wxlog、LogTrace 等系统。
我们新设计的检索系统在资源消耗较小的前提下,很好满足背景所提的所有检索需求。
单机存储空间的限制导致日志丢失,日志也没法长时间保存,如何突破单机存储空间限制呢?
嗯,是的,使用分布式文件系统替换单机文件系统就可以了!在可水平扩展的分布式文件系统支撑下,存储空间无限大,日志不再因存储空间而丢失了。
日志查找效率低下,其根源是日志散落到多台机器,需要登录到机器做日志 grep。引入了分布式文件系统存储全网日志后,我们看到的仍然是一个一个不相关的日志文件,快速定位日志仍然困难。如何提高日志定位的效率呢?
索引!就像是利用索引提升数据库表查询效率一样,我们对日志数据建立索引,快速定位到所需日志。那么,需要构建怎样的索引呢?先看看面临的两种问题定位场景:
为了支持模糊查询,业界方案一般都会对日志内容分词建索引,这会消耗大量资源。日志查询系统有两个特点:每天只有数百次查询请求,日志存储模块(分布式文件系统)IO 密集、CPU 利用率低。为了支持用户模糊查询请求,入库时不对日志内容分词建索引。用户查询时,日志存储模块使用关键字对日志内容正则匹配过滤(利用本机空闲 CPU)。这样既解决了入库资源消耗高的问题,又解决了存储机 CPU 低利用率的问题。
我们通过分布式文件系统和索引解决了目前的问题,同时也带来了新的挑战:
名词解释
在介绍系统前,先对使用的名词进行解释:
企业微信日志检索系统主要分为 6 个模块:
接下来分别阐述系统设计和实现中面临的挑战点以及解决办法。
目前,企业微信全网日志入库峰值 qps 数亿条每秒,而分布式文件系统数据节点仅仅 20 台(单台 12 块 SATA 盘,单盘 IOPS 约 100 左右),我们如何使用少量数据节点支撑如此高峰值的日志秒级入库呢?
在模糊查询场景下,用户使用模块/机器+时间段+关键字进行查询。为提升数据入库性能,我们以每台机器的 IP 作为分布式文件系统的目录,机器上模块打印的日志写入小时粒度的日志文件,这样不同机器写入自己独占的日志数据文件,相互间数据写入无竞争,入库性能最佳。与此同时,目录结构就相当于一个快速区分不同模块/机器的索引,这也能提升日志查询效率。
为了进一步提升数据入库性能,LogAgent 使用缓冲队列缓存日志数据,累积 8MB 数据后批量顺序写入日志文件中,写 qps 降低为原本的 4 万分之一。同时为了快速查找日志数据,对 8MB 日志数据的时间戳采样,批量写入同目录下的时间索引文件中。
同一 callid 索引散落在不同模块不同机器,为了全链路查询,需要对数亿条/秒的 callid 索引做秒级聚合,以支持秒级入库、秒级可查,这无疑是一个技术难题。
为了解决这一难题,我们通过三重聚合减少 callid 索引写入压力,最终达到 qps 减少到千万分之一、一次 IO 读取 callid 所有日志位置的效果:
开发通常依据模块、时间段、callid 这 3 个维度查询日志,为了加快查询性能也对这 3 个维度分别增加索引:
模块:一个模块包含若干机器,每台机器在分布式文件系统中拥有独占的日志目录(用 IP 区分),用于保存机器小时粒度日志文件。通过模块找到所有机器 IP 后,可快速找到该模块的日志在分布式文件系统中的日志目录。
时间段:日志数据保存在机器目录的小时粒度文件中,通过对日志时间采样保存为相应时间索引文件。当按照时间段查找日志时,可根据时间索引文件快速找到该时间段的日志位置范围。
callid:解析日志建立 callid 到日志位置的索引,散落在多个模块的 callid 索引通过 LogAgent、LogMergeSvr 以及 LogIdxSvr 三重聚合后,最终存储在 LogIdxSvr 的 Rocksdb 中。全链路日志查询可通过读取一次 Rocksdb 获取所有相关日志位置,快速读取到所需日志。
原始版本:并发检索 WebSvr 接收用户模糊查询请求(模块+时间段+关键字),依据模块获取机器列表后,按机器列表并发请求到多台 QuerySvr 执行机器粒度日志查询:通过机器 IP 找到机器日志目录,根据时间段拉取时间索引文件,确定日志数据范围,并发拉取日志到本机用关键字做模糊匹配。最终将匹配后的日志返回给 WebSvr 聚合展示给用户。
通过并发检索的优化手段,模糊查询一个模块一小时日志(12 台机器,7.95GB 日志量)耗时从 1 分钟降到 5.6 秒。
全链路查询和模糊查询类似,同样利用了并发提升查询性能,稍有不同的是全链路查询根据 callid 读取 LogIdxSvr 确定日志位置列表,按照位置列表并发读取日志数据,聚合后将日志返回给用户。
我们通过引入了分布式文件系统和索引服务解决了日志丢失、保存时间短和快速定位问题,但系统复杂性导致的可靠性问题,是我们面临的第二大挑战。
LogAgent 负责将日志数据和时间索引写入分布式文件系统,当分布式文件系统抖动时,为了不丢弃待写日志数据,LogAgent 使用缓冲队列(共享内存+本机磁盘文件)缓存日志数据,待抖动恢复后读出缓存数据写入文件系统。
服务抖动 LogIdxSvr 使用 Rocksdb 作为底层存储聚合全网 callid 索引,但是 Rocksdb 在高并发写入时容易出现写入抖动进而导致索引丢失,为了保证 callid 索引可靠性,LogMergeSvr 先将 callid 索引写入分布式文件系统保存,LogIdxSvr 从分布式文件系统拉,分布式文件系统当做 queue 使用起到削峰填谷作用,保证 callid 索引可靠性。
机器坏盘 LogIdxSvr 出现坏盘会导致已聚合到本机的 callid 索引数据丢失,新起的 LogIdxSvr 重新拉取分布式文件系统的 callid 索引文件,可以重建 Rocksdb 的 callid 索引,保证系统可靠性。
通过前面的设计,目前可以根据模块+时间段+关键字或者 callid 查找到日志了,但是还不够,用户往往还需要对日志做任意维度模糊匹配、日志统计(如:uniq/sort/awk 等)以及模块级全量日志查询。
如前所述,通过在分布式文件系统实现模糊匹配逻辑,系统支持对日志做任意维度模糊匹配的需求。通过对比,选择性能最优的 RE2 正则匹配库实现模糊匹配逻辑。
changeroot:使用 Linux 的 changeroot 避免用户指令操作系统重要目录;
沙盒限制:使用 Linux 支持的沙盒隔离技术,只允许执行特定指令。
模块级全量日志查询通常涉及 TB 级别日志量,因为涉及的数据量过大,查询耗时一般较长,无法给用户提供实时返回,我们通过提供异步任务功能支持这一需求。
用户异步任务请求通过 WebSvr 转发到 QuerySvr,为避免 QuerySvr 宕机导致异步任务丢失,QuerySvr 会将异步任务写入一致性锁服务中存储,空闲的 QuerySvr 会从一致性锁服务抢锁,抢锁成功后执行该异步任务。
QuerySvr 根据异步任务的模块信息读取机器列表,按照机器列表并发读取匹配的日志数据,按顺序写入本机磁盘中,在查询结束后更新一致性锁服务状态(存储机 ip 和路径),用户页面刷新会拉取到异步任务最新状态。
视频号最新视频
5月28-29日
QECon全球软件质量&效能大会
欢迎关注