作者:datonli,腾讯WXG后台开发工程师后台开发在定位问题的时候需要查找日志,而企业微信业务模块的日志存储在本地磁盘,会导致以下问题:日志查询效率低下:一个用户请求涉及近十个模块,几十台机器。要查找日志,您需要登录到机器的grep日志文件。这个过程通常需要10多分钟,效率很低;日志存储时间短:单机磁盘存储容量有限,为了保存最新日志,清理脚本会定期清理旧日志文件,释放磁盘空间,例如:livenetwork-corestorage7的每日日志占用90%的磁盘空间,7天前的日志会被清除。因清除日志导致用户投诉无法解决;日志丢失:虽然现网保留了7天的最新日志,但是由于部分模块请求量大或者日志打印不合理,我们也会限制一小时的日志打印量,不会保存它在超过阈值之后。比如现网核心存储的10G日志,前10分钟达到阈值,则后50分钟不保存日志。因日志缺失无法解决投诉。我们希望有这样一个日志系统:保存全量日志:由于ToB业务的特殊性,至少需要保存30天的全量日志(几PB日志,万亿级日志),以方便审查日志位置问题;日志快速定位:根据模块+时间段+关键词或用户请求信息快速定位日志;实时性:日志峰值达到数亿/秒,需要秒级存储,秒级查询;支持日志的模糊匹配和统计:单机日志查询常用于模糊匹配、awk/uniq/sort等复杂统计,预计在新的日志系统中也会支持;支持模块级全量日志查询:日常操作中,部分用户投诉不确定具体发生时间,需要对模块进行全量日志(日志量达到TB级)查询。相比行业解决方案,公司内外有很多日志系统解决方案,根据是否对日志进行全文搜索分为两类:全文搜索的日志系统:将日志内容分词并创建倒排,通过查询关键字倒排获得交集支持模糊匹配,这类系统一般比较消耗存储资源,不支持日志统计。典型实现包括:ELK、Hermes、腾讯云日志服务(CloudLogService,CLS)等系统;部分字段检索的日志系统:只对部分字段建立索引,支持特定字段的快速检索,占用存储资源少,但这类系统对模糊匹配支持不好,不支持日志统计,不支持全量模块级别的日志查询,例如wxlog,LogTrace等系统。我们新设计的检索系统在低资源消耗的前提下,很好地满足了背景中提到的所有检索需求。方案设计考虑了存储时间短和日志丢失的问题。单机存储空间限制导致日志丢失,日志无法长期保存。如何突破单机存储空间的限制?嗯,是的,用分布式文件系统来代替单机文件。系统没问题!在可水平扩展的分布式文件系统的支持下,存储空间是无限的,日志不再因为存储空间的原因而丢失。日志搜索效率低问题日志搜索效率低。根本原因是日志分散到多台机器上,需要登录机器做loggrep。引入分布式文件系统存储全网日志后,我们仍然看到一个个不相关的日志文件,仍然很难快速定位日志。如何提高日志定位的效率?指数!就像使用索引来提高数据库表的查询效率一样,我们对日志数据进行索引,可以快速定位到需要的日志。那么,我们需要建立什么样的索引呢?首先来看一下我们面临的两个问题定位场景:开发收到模块告警,通过告警信息结合代码查找关键字,使用关键字查找模块告警时间段内的日志;根据用户投诉查找用户请求信息,利用用户请求信息查找所有关联模块的日志。从以上场景可以看出,我们通常是根据模块+时间段+关键词或者用户请求信息来查找日志。因此,对模块、时间、用户请求信息进行索引,可以提高日志查找的效率。存储资源消耗为了支持模糊查询,行业解决方案一般会在日志内容的分词上建立索引,这会消耗大量的资源。日志查询系统有两个特点:每天只有几百个查询请求,日志存储模块(分布式文件系统)IO密集,CPU利用率低。为了支持用户的模糊查询请求,日志内容在入库时不分词索引。当用户查询时,日志存储模块通过关键字对日志内容进行匹配过滤(利用本地空闲CPU)。这样既解决了存储资源消耗高的问题,也解决了存储机器CPU利用率低的问题。挑战我们通过分布式文件系统和索引解决了目前的问题,同时也带来了新的挑战:高性能:目前企业微信日志量在每月PB级、万亿级、每天数百TB,面对如此海量的日志,如何做到高性能的入库和查询?可靠性:分布式文件系统和索引的引入带来了更大的复杂性,如何保证整个日志系统的可靠性?支持灵活多变的用户查询需求:通过调研发现,用户主要有以下四种日志查询使用场景:a)与用户请求关联的所有模块日志查询;b)一段时间内的模块日志模糊查询;c)模块全量日志模糊查询;d)查询日志统计(如:awk/uniq/sort命令等)。如何支持如此灵活多变的用户查询需求?术语解释在介绍系统之前,先解释一下用到的术语:callid:唯一标识一个用户请求,每条日志都会携带callid信息;模糊查询:根据用户输入模块、时间段、关键字查询日志;全链路查询:根据callid查询用户请求的所有关联模块日志。系统架构企业微信日志检索系统主要分为6个模块:LogAgent:与业务模块部署在同一台机器上,聚合模块内的日志,将数据批量写入分布式文件系统,发送callid索引批量到LogMergeSvr进行聚合;LogMergeSvr:为一段时间内的callid索引在模块间进行聚合,批量写入分布式文件系统;存储模块(分布式文件系统):存储原始日志数据、时间索引和callid索引数据;LogIdxSvr:聚合全网callid索引,底层存储使用Rocksdb;WebSvr:接收用户网页请求,并发查询QuerySvr。QuerySvr:查询执行模块,支持全链路查询、模糊查询、awk统计等。接下来分别介绍系统设计和实现中面临的挑战和解决方案。系统日志存储如何实现高性能高性能目前企业微信全网日志存储峰值qps为数亿/秒,而分布式文件系统数据节点只有20个(单12个SATA盘,而单盘IOPS大约是100左右),我们如何用少量的数据节点来支持秒级的如此高峰值的日志存储呢?数据存储高性能在模糊查询场景下,用户使用模块/机器+时间段+关键词进行查询。为了提高数据存储的性能,我们使用每台机器的IP作为分布式文件系统的目录,将模块在机器上打印的日志写入到小时粒度的日志文件中,让不同的机器写入自己的自己独享的日志数据文件,相互之间没有数据写入竞争,存储性能最好。同时,目录结构相当于一个索引,可以快速区分不同的模块/机器,也可以提高日志查询的效率。为了进一步提高数据存储的性能,LogAgent使用缓冲队列来缓存日志数据。累积8MB数据后,依次批量写入日志文件,写入qps降为原来的1/40,000。同时,为了快速查找日志数据,对8MB日志数据的时间戳进行采样,分批写入同目录下的时间索引文件中。高性能callid索引入库同一个callid索引分散在不同模块、不同机器中。对于全链路查询,需要每秒对上亿个callid索引进行秒级聚合,以支持秒级入库和秒级搜索。绝对是技术问题。为了解决这个问题,我们通过三重聚合来降低callid索引的写入压力,最终达到qps降低到千万分之一,一次IO读取callid所有log位置的效果:模块中的聚合:LogAgent聚合模块中的callid索引,批量写入LogMergeSvr,qps降低到1/10000左右;模块间聚合:LogMergeSvr聚合一段时间内模块间的callid索引,批量写入分布式文件系统,qps降低到1/10000左右;全网聚合:callid索引文件不利于高效读取。LogIdxSvr利用Rocksdb的Merge聚合了全网的callid索引,一个IO可以读取callid的所有log位置。日志查询性能通过增加索引来提升查询性能开发中通常会根据module、timeperiod、callid三个维度来查询日志。为了加快查询性能,在这三个维度上也加入了索引:模块:一个模块包含若干台机器,每台机器在分布式文件系统中都有一个专属的日志目录(以IP区分),用于保存机器小时粒度日志文件。通过模块找到所有机器IP后,可以快速找到模块的日志在分布式文件系统中的日志目录。时间段:日志数据保存在机器目录下的小时粒度文件中,通过日志时间采样保存为对应的时间索引文件。按时间段查找日志时,可以根据时间索引文件快速找到该时间段的日志位置范围。callid:解析日志,建立从callid到日志位置的索引。分散在多个模块中的callid索引,经过LogAgent、LogMergeSvr、LogIdxSvr三重聚合,最终存储在LogIdxSvr的Rocksdb中。全链路日志查询,一次读取Rocksdb即可获取所有相关日志位置,快速读取需要的日志。模糊查询高性能原版:并发检索WebSvr收到用户模糊查询请求(模块+时间段+关键词),根据模块获取机器列表后,并发请求到多个QuerySvr根据机器执行机器粒度日志查询list:通过机器IP找到本机的日志目录,根据时间段拉取时间索引文件,确定日志数据的范围,同时拉取日志到本机,与关键词做模糊匹配。最后将匹配的日志返回给WebSvr进行聚合展示给用户。通过并发检索的优化方法,将一个模块(12台机器,7.95GB日志量)1小时日志的模糊查询时间从1分钟降低到5.6秒。优化版:模糊匹配下沉分布式文件系统在系统压测过程中,我们发现QuerySvr带宽和cpu存在性能瓶颈。原因是QuerySvr读取了大量没有经过模糊匹配的日志数据,占满了网络带宽,在QuerySvr匹配中进行fuzzing也消耗了大量的cpu资源。我们需要做性能优化。考虑到分布式文件系统是重IO操作,CPU使用率很低,将模糊匹配逻辑下放到分布式文件系统,既解决了QuerySvr带宽和CPU性能的瓶颈问题,又使得全使用文件系统的CPU。避免浪费资源。通过模糊匹配下沉的优化方法,模糊查询一个模块(12台机器,7.95GB日志量)1小时日志所需时间从5.6秒减少到2.5秒。全链接查询高性能全链接查询类似于模糊查询,也是利用并发来提高查询性能。略有不同的是,全链路查询根据callid读取LogIdxSvr确定日志位置列表,并根据位置列表并发读取日志数据,聚合后返回给用户。如何保证系统可靠性我们通过引入分布式文件系统和索引服务解决了日志丢失、存储时间短、定位快等问题,但系统复杂性带来的可靠性问题是我们面临的第二大挑战。数据可靠性保证日志数据缓冲队列(共享内存+本地磁盘文件)LogAgent负责将日志数据和时间索引写入分布式文件系统。当分布式文件系统发生波动时,为了不丢弃要写入的日志数据,LogAgent使用缓冲队列(共享内存+本地磁盘文件)缓存日志数据,抖动恢复后,将缓存的数据读出并写入文件系统。索引可靠性保障服务抖动LogIdxSvr使用Rocksdb作为底层存储聚合全网callid索引,但Rocksdb在高并发写入时容易出现写入抖动,导致索引丢失。为了保证callid索引的可靠性,LogMergeSvr首先将callid索引写入分布式文件系统中,LogIdxSvr从分布式文件系统中拉取,以分布式文件系统为队列削峰填谷,保证callid索引的可靠性。本机LogIdxSvr磁盘损坏会导致聚合到本机的callid索引数据丢失。新上线的LogIdxSvr可以重新拉取分布式文件系统的callid索引文件,重建Rocksdb的callid索引,保证系统可靠性。如何支持灵活多变的用户查询请求通过之前的设计,可以通过模块+时间段+关键字或者callids来查找日志,但这还不够。用户经常需要对日志进行任意维度的模糊匹配和日志统计(如:uniq/sort/awk等)和模块级的全量日志查询。支持任意维度的模糊匹配如前所述,通过在分布式文件系统中实现模糊匹配逻辑,系统支持日志任意维度的模糊匹配需求。通过比较,选择性能最好的RE2正则匹配库来实现模糊匹配逻辑。支持awk/uniq/sort等统计命令支持统计命令用户不仅需要对日志进行模糊匹配,还需要对匹配到的日志执行awk/uniq/sort等统计命令,其中涉及命令的嵌套执行,这是非常复杂的。难以调用相关库实现。我们通过调用系统shell的子进程来支持此要求。QuerySvr从分布式文件系统拉取日志数据到本地后,子进程shell调用用户传入统计指令对日志数据进行处理,最终结果返回给WebSvr。当子进程处理超时时,父进程会kill掉子进程,防止用户统计任务消耗QuerySvr资源。安全考虑由于用户命令可以由用户输入,因此需要考虑命令执行的安全问题。两种方式保证执行指令的安全:changeroot:利用Linux的changeroot,避开用户指令操作系统的重要目录;沙箱限制:利用Linux支持的沙箱隔离技术,只允许执行特定的指令。支持模块级全量日志查询——异步任务模块级全量日志查询通常涉及TB级日志量,由于涉及的数据量太大,查询一般耗时较长,无法为用户提供实时性回报。我们提供异步任务功能支持这种需求。用户的异步任务请求通过WebSvr转发给QuerySvr。为了避免QuerySvr宕机导致异步任务丢失,QuerySvr会将异步任务写入一致性锁服务进行存储,空闲的QuerySvr会从一致性锁服务中抢锁。成功抢到锁后执行异步任务。QuerySvr根据异步任务的模块信息读取机器列表,根据机器列表并发读取匹配的日志数据,依次写入本地磁盘,更新一致性锁服务状态(存储机器ip和路径)查询完成后。刷新用户页面将获取异步任务的最新状态。
