一、日志分析场景的需求和技术挑战1、日志分析(1)什么是日志

广义来讲,日志记录的是一个不可变的信息,它是一个事实,也就是在某个时点,发生了某一件事情。如果狭义地去理解,日志就是系统后台的 log。通常,log 都会有一个时间戳,记录了这个时间戳上发生的事,比如 server 启动,或者是产生了错误等等。

(2)日志分析的价值

日志分析有着很大的价值。在 To C 场景中,比如淘宝、拼多多这样的电商系统,会记录用户的浏览行为,并推荐商品。这些推荐系统就是依赖于日志的。常见的内容优化、用户画像、AI 算法等也都依赖于日志。


【资料图】

To B 场景下,对于一个企业来说,分析日志主要有两个目的,第一是希望利用这些数据去更好地赚钱(开源);第二是利用这些数据提高企业的运行效率,帮助企业去省钱(节流)。从开源的角度来讲,比如近几年非常火爆的流程挖掘,就是把企业在生产中产生的各种日志数据收集起来,基于这些事实做统计或者机器学习,帮企业进行各种挖掘分析,找到一些规律,比如优化制造工艺、提高流水线的生产效率、精简流程、让业务跑得更快等,这些都能够帮助企业去更好地赚钱。从节流角度来讲,比如 IT 运维,如果故障率低、故障响应速度快,那么就可以为公司省很多钱,避免潜在损失。

(3)日志分析的技术挑战

第一是日志的写入速率快,存储成本高。日志都是由机器产生的,机器生成的一大特点就是数据量非常大,这就要求系统在数据摄入的时候,速率必须要非常高,同时面对大量的文本数据存储,压缩比必须要做得比较好,才能够节省存储成本。

第二是日志格式很难统一,并且格式经常变化。因为日志处理系统通常是存在于业务系统的下游,很难去规定让所有业务系统都符合统一的日志的格式,尤其是在那些 IT 建设尚不成熟的公司。只有在一些 IT Infra 建得特别好的互联网公司,做日志的团队才有可能去推动业务团队去统一日志格式。即便如此,日志里面的 schema 也是经常会变化的。随着系统迭代,会有新的功能和业务,日志信息也会越来越多。如何能够让下游日志处理系统更好地去响应这些日志格式、日志字段的变化,是一个非常大的挑战,在后文中将着重分享。

第三是批量查询分析和即席交互式查询分析(Ad Hoc)需求并存,数据进入系统之后需要尽快可以查询。比如在 IT 运维、网络安全监控等常见的日志处理场景中,当要去排障或者寻找安全入侵的时候,或者要去做我们称之为安全专家的 sweat hunting 的过程,都会有大量的即席交互查询,所以会需要查询日志系统的快速响应。在做交互式查询的时候,数据写入系统之后,要保证能够快速地查到。同时日志也会有很多跑批的要求,比如运维监控,需要跑批的去做一些告警,可能是每分钟、每 5 分钟、每一个小时。所以查询会是一个比较复杂的需求。

2、日志处理技术流派

接下来看一看日志处理的技术流派。读时建模的英文叫做 Schema on read,可能有一些同学了解过数据湖或者 ELT 技术,对 Schema on read 不太陌生。Schema on read 和 Schema on write 这两个概念并不是一个特定的系统或者算法,可以称之为技术流派。

(1)写时建模(Scheme On Write)

上图的右侧(蓝色)部分就是写时建模(scheme on write)。常用的关系型数据库,以及一些数据仓库类的产品,都是写时建模的工作模式。在用数据库的时候,需要先定义数据的表结构,比如在上图的例子里面,有一个 4 列的表,需要定义好每一列数据的属性,比如 code 列是整数型、client 列是字符串类型、time 列是 datetime 类型。这些定义好之后,后续所有的数据进入到系统,必须要符合之前定义的 schema 标准,否则就不能写入了,这就是一个典型的 schema on write 的系统。

如果我们有这样 3 种类型的日志,一个是 Nginx access log、一个是 Apache 的 access log、一个是 windows IIS 的 access log。大家可以看到这些 log 很相像,但格式又不太一样。但是如果要把这些日志都采集到写时建模的数据库里面(蓝色的这条通路),就要去做 ETL 了。针对三个不同的日志就需要维护三个不同的 ETL 任务。

(2)读时建模(Schema On Read)

读时建模的理念是先把原始数据存储下来,即我们把这三份不同产品的原始的日志,直接存储到我的系统里。比如这里有查询 1、查询 2 和查询 3,大家会看到针对每一个查询用的字段不一样。我们会在查询运行的时候去指定一些字段提取的规则。比如查询 1,要用 method 字段、time 字段和 client 字段,要定义这三个字段提取的计算规则是什么,在查询运行的时候,去把这些规则动态地应用到所有的原始日志上面,从中提取出需要的信息。

这个过程有一个专业的术语,叫做字段提取,英文叫 field extraction。图中跑 3 次查询,橙色的箭头就是一次 field extraction,这样会生成一个预期的 schema 格式。再针对 schema 格式进一步做比如统计分析、过滤、转换或者 pivotal 透视等。右边蓝色的写时建模是不需要去动态算 schema 的,因为 schema 已经写在这里了,所以每次查的时候,只要从存储好的表里面查数据就行了。

读时建模和写时建模的共性是它们都是有 schema 的,只不过读时建模的 schema 是动态的。读时建模是用硬件的 CPU 算力去换取查询的灵活性。假设要分析一个 user agent,如果使用读时建模,那么只需要去定义好 user agent 字段怎么来的,field extraction 的规则是什么就可以了。但如果是写时建模,就要多算一个 user agent 的字段,要做 schema 更改,修改数据库的表结构。其次,还要把所有的数据再定义一个 ETL 的任务去抽取 user agent 这个字段,通过跑批任务把数据重新填入到表中。从解决问题的角度来讲都是 OK 的,但是如果经常有 schema 变更,或者称之为 schema 的 evolvement,那么用写时建模代价是比较高的,因为每次调整后,还有历史数据需要进行重新回填等这样很繁杂的操作。

任何一个技术都不可能是具有全面优势的,有好处,也会有缺陷。写时建模最大的好处就是用空间去换时间,可以提前做很多优化,比如可以针对字段去做一些索引,让查询跑得更快。甚至可以像现在的 MPP 数仓一样提前做很多预计算、pre-aggregation 等加速后续的查询。

二、读时建模技术的特点1、读时建模和写时建模的对比

大家其实不难发现这两个流派的区别,如果我们要查询灵活敏捷,就应该选读时建模。但是如果要求极致的性能和极致的快,而且查询不太变化,那么就选择写时建模。

读时建模有三个比较明显的优势,第一个是查询灵活敏捷;第二个优势是存储空间比较小,因为写时建模是用存储空间去换查询的时间,而读时建模只存原始数据,不会去建索引,也不会把抽取好的字段再次做存储,所以能够节省存储的成本;第三个优势是写入速度快,因为读时建模只存原始数据不建索引,也不做很多预处理的工作,数据进入系统的过程是比较轻量化的。对于写时建模来说,数据要进入系统,需要做很多工作,首先要整理成合适的 schema,还要保证写进来的时候可能会针对性地去建一些索引、做预计算等工作。所以数据写入的速度肯定是不如读时建模快。

写时建模也有自身优势,比如查询速度很快。最典型的就是 BI 场景,由于 BI 分析的指标都是固定的,查询不会经常变化,而且每天要看很多次,每次都希望能查询很快。那么通过写时建模去做好预计算,带来的收益是非常大的。

2、读时建模的优势场景

对比完读时建模和写时建模,读时建模的理念是与日志日处理这个场景天然契合的。前文中提到日志处理的第一个问题是日志量大、存储成本高,读时建模就正好能发挥其优势。另外,日志格式很繁杂,不清楚接入的日志到底有多少种格式、多少个 pattern,读时建模只要把日志先接入进来,之后在使用中发现有新的 pattern 出现时,只需要定义新的字段提取规则,就能够响应各种各样的数据格式变化需求。甚至已经接入的日志格式有变化的时候,依旧可以通过调整读时建模的字段提取规则,很好地去处理 schema evolvement 的情况。日志查询的时候,有很多 Ad hoc 查询的情况。这种查询很多时候都是没有出现在常规的业务报表查询里面,经常会利用一些想要用的动态字段来做过滤和加工。这种场景用读时建模也是有天然的优势的,能够节省很多的成本。

所以总结下来,读时建模技术流派用相对廉价的硬件的 CPU、内存这些算力,去换取了更快速的数据处理的需求落地时间,可以让整个端到端的处理变得更加快速和便捷。

三、鸿鹄-免费的⼀站式异构数据即时分析平台

介绍完技术流派,再来回顾一下这两个流派的日志处理产品。Splunk 是 schema on read 流派的开山鼻祖。但是 Splunk 整个架构其实相对来讲还是比较经典的 MPP(Shared Nothing)架构,随着云的 Infra 越来越成熟之后,无法满足异构数据处理的需求。有很长一段时间的产品都是以写时建模为主,比如 ES、ClickHouse 等。到 2018 年以后,出现了 Grafana 的 Loki 这样一个产品,它是偏向于读时建模的。2020 年,炎凰数据推出了主打读时建模的鸿鹄。

这里要明确一点,所有的产品在这里划分的流派,只是它偏向于哪一个,不代表这个产品只能做这一个。比如炎凰数据平台,虽然是主打 schema on read,但同时也有 schema on write 的能力。当一个分析任务固化之后,schema 已经确定了,很少改动,就可以把这些字段提取的逻辑固化下来,让这些字段提取先预计算,并把预计算的结果先存好,用空间换时间。再比如 ClickHouse 的写时建模很强大,它能够让查询变得很快。但是 ClickHouse 最近的版本里面也提供了动态解析 JSON,即动态地去解析一个 JSON 的 column 字段,也是某种最简单直白的 schema on read 的实现方法。所以总的来说,schema on read 和 schema on write 两个手段都会需要用到。

四、鸿鹄 SQL 和鸿鹄的读时建模引擎1、读时建模在炎凰的实践

接下来就来介绍炎凰数据平台——鸿鹄。它定位为一个一站式的异构数据即时分析平台。这里有三个关键词:一站式、异构和即时。

一站式指的是,整个炎凰数据平台会像 ELK 那样,有一个读时建模的存储计算的核心引擎。在这个之上有一些基础的服务,比如数据可视化的仪表盘服务、数据接入服务、权限管理服务、用户认证的告警管理服务等。它像是一个中间层的服务,我们希望提供一个一站式的日志处理的开箱即用体验,把系统装好之后,就可以通过 UI 快速导入数据,接入数据在 UI 上面就进行查询分析了,甚至是做一些可视化。一站式最大的好处就是降低用户的运维成本。

第二个关键词是异构,是鸿鹄很重要的一个属性,也是读时建模引擎来保证的。

第三个关键词是即时分析,系统能够很好地响应 Ad hoc 需求。并且鸿鹄上面有很多接口的扩展,我们从一开始设计系统的时候,就希望系统能够尽可能地开放,所以我们会提供一些标准的 API,比如所有服务都有 REST API,还有一些 Java script SDK,能够让用户非常方便地去把鸿鹄当成一个底层的数据库来使用。

2、鸿鹄中的数据存储

下面介绍如何在鸿鹄当中去设计读时建模。要讨论读时建模,分为存储和计算两部分。

首先来介绍存储。鸿鹄的数据存储没有表结构的概念,也就是数据进入到鸿鹄的时候,不需要定义有几个字段,以及这些字段是整型还是字符串。所以总的来说,我们定义了数据集,大家可以把它理解为是一个数据的容器。可以把各种各样不同格式的日志,简单、快速地放到容器中。

我们将容器里存储的对象抽象成了 event 事件这样一个概念,每个日志就是一个事件,日志上包含一个时间戳,记录了某个时点发生了什么事情。同时我们的存储模型抽象上还做了一些元信息字段的定义,它标识了日志是什么系统生成的、可能是什么格式等。

这里的时间戳字段是整数型,之所以不用存字符串形式是因为所有的查询都会有一个时间窗口的概念,在日志分析里面时间窗是一个天然用来做过滤的条件。所以我们把时间戳提取出来,变成整数,并且对时间戳的字段做一个索引,这样能够提供更快的查询效率。

假设一开始数据集是空的,现在放了一条日志进来,这条日志有五个字段或者列。接下来又来了两条不同的日志,而且第二条日志除了有数据类型、主机和数据源的这几个字段之外,还多了一个叫环境的字段,那么鸿鹄提供的可自定义扩展的元信息字段就能很好地帮你解决这个问题。后面有更多的元字段信息进来的时候,鸿鹄平台也是能够支持存储的,并且不需要在定义数据集的时候声明。在鸿鹄平台上,数据集的 schema 具有支持横向扩展的特性。而且整个 schema 的变化,对用户是无感的。因为你创建数据集和导入数据的时候,都不需要关心 schema 是什么。查询的时候,比如有新的 environment 字段,就可以针对 environment 元信息字段去做过滤,这也是存储引擎和计算引擎自动去保证的。对用户来讲,不需要做任何数据库 DDL。

在存储的逻辑模型清楚之后,再来介绍一下存储中用到的一些技术的选型和大概的思路。

(1)按时间分片

首先面对大量的日志数据,其实我们没有办法把所有的日志数据全存到一个文件,而是需要对数据去做分片。默认的分片就是按照时间戳来分,因为所有的查询都会要有时间戳过滤,这样能够达到比较好的查询效率。

(2)每片数据采用列式存储

列式存储往往是和写时建模系统一起配合使用的,为什么读时建模也会有列式存储呢?其实如果我们把数据集抽象成是只有这五个字段的一张表,那么每一个字段也都是按列存储的。所以底层存的时候是把每一列都按照列式存储下来,只不过每一个数据分片上面的列数可能不一样。因为有 schema evolvement,所以相当于每一个数据分片里的列式可能不同,但是每一列还是按照列式存储。用列式存储的初衷其实也很简单,因为第一我们只提供分析型服务,用列存能够很好地去配合向量计算,做到数据处理的提速。这也是当前 OLAP 系统的一个标配。第二原因是使用列式存储可以提高数据的压缩比,数据压缩比越高存储成本越低。

为了支持 Ad hoc 查询和一些快速的检索查询,系统默认只做两个索引。这里也是为了平衡存储膨胀的开销和查询的效率所做的一个折中。因为索引做得越多查询越快,但存储成本也越高。但如果不建任何索引,查询速度又会较低。所以我们综合了日志场景中最常用的两类查询,创建了时间戳索引和倒排索引。

(3)时间戳索引

第一类时间戳索引是带上时间范围的,比如运维场景中,基本都是查过去 5 分钟、半小时或者其它时间范围内的日志,所以我们会针对日志的时间做一个索引,每次查询的时候,都会用到时间戳的索引来加速查询。

(4)倒排索引

第二类是会比较轻量化的倒排索引(inverted index)。之所以叫轻量化是为了和 search engine 区分开来,搜索引擎不仅有倒排索引,还会有 scoring 和 ranking 的问题。但是在我们的系统里面不做 scoring 和 ranking,只是去做关键词到日志的倒排。比如在上图查询包含了 evaluation 单词的日志有哪些,这个时候会命中到橙色的第 1 条和第 2 条。如果查询包含了 login 是哪一个,可以命中到第三条。在倒排索引上面,有一些简单的与或逻辑,能够让查询变得更加快速。比如要查既包含 evaluation 又包含 login,那么很快就可以回答出来是个空集,就不需要读任何数据。之所以要做这样的索引,是因为往后做读时建模的从原始数据去提取字段的时候,都是要消耗 CPU 去计算,如果能在更早时候就排除掉一些完全没有必要命中的数据,就能够节省掉这些不必要的 CPU 开销,查询也会非常快速。

所以在我们的系统里,所有围绕优化的思路都会把数据的过滤尽可能地推到读时建模、字段提取这件事情之前。比如包含关键字在有的时候可以推断出来它可以隐含关键字过滤。比如要看某一个字段是不是等于 evaluation,首先也可以翻译成先查询包含 evaluation 关键词的日志再去做过滤,这样能够尽可能更早地把这些数据过滤掉,平台内部会有很多的这样的优化手段。

3、鸿鹄里的计算引擎

接下来介绍计算部分。

(1)Schemaless SQL

采用了 SQL 接口来做计算。上图中可以看到 From 子句后面是一个数据集,它能查询出不同格式的日志。绿色框中是一个 key-value 结构的日志、红色框中是一个 JSON 结构的日志。鸿鹄读时建模引擎可以自动解析 key-value 和 JSON 格式的数据,并且不需要做任何配置。

比如在上图中,可以直接引用到 http.url 和 http.method,这些字段是从 JSON body(上一个图)里面解析出来的,系统会自动帮你完成。同时这些 key-value 里面也会有解析出来的字段,就是我们这里拿到的 url 和 method。上图中有两个不同的 data type,一个是 audit 格式的审计日志,一个是 JSON 格式的 access log。自然而然会产生一个需求,url 和 http.url 应该是同一个意思,我们希望它们合并到同一个列;method 和 http.method 也是同一个语义,也合并成同一个列。这在鸿鹄的读时建模引擎中很简单,它提供了大量的标量函数,做字段的转换和变换。

比如上图中这个简单的例子,可以用 coalesce() 函数把 URL 字段和 HTTP 的 URL 字段归一化成一个 URL 字段。大家会发现,其实 SQL 表达的就是一个读时建模,它建立的模型是把刚才 audit log 和 access log 中的 URL 字段和 method 字段整理成一个表的结构,一旦成表结构之后,这个表就是一个所谓的模型。它们的理念其实特别相似,编程里面有个 Duck typing(鸭子类型)的问题,就是建完模型之后不必关心底层数据到底是从哪里来的,有 URL 就可以用 URL 这个字段。就像鸭子类型一样,反正看起来像鸭子,就把它当鸭子用就好了。就是这样一个理念,只不过在 ETL 的世界里面,我们要先在数据库里定义一个表结构,并把 SQL 的逻辑写成 ETL 流水线,然后再把数据插入进来。

(2)标量函数+表函数

我们再来看一些更复杂有趣的事情。日志外层是 key-value 的结构,但是在 header 字段里面,其实是一个 JSON 的结构。在分析日志的时候往往很难在第一次就定义好需要哪些字段。除了用 header 字段之外,随着业务的演进或者问题排查的深入之后,还要用 header 里面的字段。鸿鹄读时建模引擎提供了标量函数和表函数这样的概念,帮助大家使用 SQL 来做字段的变换和提取。

(3)用 SQL 做字段提取

具体的做法很简单,大家可以看到其实很简单的一些 SQL 就是 parse_json 这样的一个功能。parse_json 的核心就是把 JSON 的 header 字符串 flatten 开来,展开成我们要的字段。在展开之后,我们可以很容易地去对这些字段进行过滤。因为一旦它形成字段之后,就跟数据库里表的字段没有任何区别了。

另外值得一提的是,第一行数据的 JSON 其实是没有 accept、encoding、Referer 这些的,因为 JSON 字段相对比较少。但是在第二行的 JSON 里面字段是很多的,在用 parse_json() 函数展开之后,读时建模引擎会自动去做两个展开表的 Schema 统合,我们称之为 union 的工作。而且会把这一行上不存在的字段变成 null 值,我们就可以很快地做出一张大宽表了,而做这个表的时候也不需要特别定义很多的表结构,这一切都是 schema 的 involvement,都由读时建模引擎来处理。

(4)SQL函数

大量的函数在读时建模时都要经常被调用,所以这些函数的性能是非常重要的。为了保证这些函数具备比较好的性能,我们一方面会尽可能地用向量计算,还有一些函数会去做 JIT 加速,把它编译成一些更快的可执行字节码,而不是用编译执行单元的方式来做。这些都是一些很细节的优化。

(5)SQL CTE

还有一个概念是数据处理的流水线(pipeline)。很多小伙伴可能感觉 SQL 很难写,因此我们在读时建模引擎里面支持了一个叫 CTE 的语法,上图中的 SQL 语句就是个典型的 CTE 语法。如果查询复杂,比如三步四步,我们就把每一步写在这个 common table expression 里面就可以了,会串行执行,依次执行第一步、第二步、第三步等等。

这样书写的一个好处是逻辑上代码更可读,第二是调试的时候也更简单。

(6)复用建模逻辑

读时建模的模型是一段 SQL,要将其固化和存储下来也是一个必须要解决的问题。我们在 SQL 引擎里也有创建视图这样的功能,可以把一个模型的封装逻辑写在视图中。这样就可以把建模的逻辑和后续基于模型的分析的逻辑做一个比较好的解耦,当建模的逻辑有变化时,只需要去调整视图的逻辑即可,不需要去更改后续的分析逻辑。

(7)查询加速

鸿鹄平台也会去想办法在写时建模的理念下去做一些查询加速,其中最典型的一个做法就是物化视图。比如我们把 url、method 这样的一个视图变成物化视图,这些 url 和 method 就会被实际存在物理磁盘上。下次再查询的时候,就不需要再计算了,是用空间去换时间。

这里的物化视图与一些数据库的物化视图有一点不一样。我们的物化视图自带时序属性,当发现有新的数据进入 my_eventset 的时候就会去调用相应的物化视图逻辑去把最新的数据物化。这对于终端用户是透明的,所以对于用户来说只是定义了一个物化视图的模型,后续会发现查询这个模型的速度比普通视图要更加快捷。

推荐内容