没有索引也能用SQL ?深度解析 SLS Schema-on-Read 分析原理与应用
阿里妹导读
为了能够更灵活的挖掘日志数据背后的价值,SLS提出了一种不需要事先建立索引就能对数据进行分析的模式。本文对SLS Schema-on-Read分析的提出背景、设计思路、实现过程、使用方式、适用场景进行了全面的介绍。
引言
日志数据的蓬勃发展 随着数字化浪潮下企业数字化转型进程的不断加速,以及云原生趋势下可观测性理念的逐渐普及,企业的日志数据的规模正在快速增长,同时日志数据的范畴也在不断扩大,包括但不限于:应用程序运行日志、Prometheus采集的云原生应用监控指标、服务器Syslog、网络访问日志、移动端监控数据、数据库binlog、业务埋点数据、账单数据、IoT设备上报数据等等。 这些数据有着丰富的用途,比如异常监测、故障诊断、安全风控、行为审计、运营报表、用户画像分析等等。为了能够发挥出数据背后的价值,企业不仅需要收集和存储海量的日志数据,同时更要能够对这些海量数据进行高效的计算和分析。 大数据技术的演进与SQL的回归 随着互联网的飞速发展,传统的关系型数据库逐渐难以满足海量数据的存储和查询需求,Google的“三驾马车”(MapReduce、GFS、BigTable)论文的发布,正式揭开了大数据时代的序幕。GFS 和 MapReduce 解决了数据大规模存储和计算的问题,只需要将大量普通的机器组织起来,就可以获得对海量数据的处理能力,从而大大降低了大数据技术的门槛。 这些新的数据技术的诞生,引发了一场传统数据库与大数据的论战,其中最出名的是数据库领域图灵奖得主Michael Stonebraker写的 MapReduce: A major step backwards ,批判MapReduce丢弃了传统数据库的理论精华,引用文章中的一段话:数据库领域这四十年来中学到了三条重要的经验: 1、Schemas are good. 2、Separation of the schema from the application is good. 3、High-level access languages are good. 然而MapReduce没有吸收上面三个经验中的任何一个,甚至可以说是退步到了现代DBMS发明前的60年代。 尽管以今天的视角看这篇文章对于MapReduce的批判整体上有些稍显偏激了,但从上面这段话我们可以看出,基于关系模型的Schema定义、声明式的SQL语言对于数据库的发展是至关重要的,在这个基础上,计算的“逻辑意图表达”与计算的“物理执行过程”被解耦开来,使用者只需要使用SQL去表达自己“需要算什么”,至于“怎么去算“则完全交给数据库执行引擎自己去优化,这种解耦大大降低了数据库的使用难度,从而极大地促进了数据库的普及。 这场论战最终的结果是大数据技术与传统数据库技术都发现了对方的优点,双方取长补短、相互融合。正如Google自己在后续发布的Spanner论文(Spanner: Becoming a SQL System)中这样说到:
虽然这些系统提供了数据库系统的一些优点,但它们缺乏应用程序开发人员经常依赖的许多传统数据库特性。一个关键的例子是健壮的查询语言,这意味着开发人员必须编写复杂的代码来处理和聚合应用程序中的数据。因此,我们决定把Spanner变成一个功能齐全的SQL系统,查询执行与Spanner的其他架构特性紧密结合。 大数据领域这边吸收了传统数据库中的索引优化、查询优化、SQL语言等技术,演进出了各种SQL-on-Hadoop的解决方案,如Hive、Spark SQL、Presto等,如今对于SQL的支持基本已经成为各种大数据计算引擎的标配。而数据库领域则向着分布式、HTAP等方向演进着。 这些融合标志着SQL重新回归,成为数据分析处理系统的通用接口。 关系模型的约束与Schema-on-Read的兴起 SQL之所以能够具备强大的计算表达能力和优越的执行友好性,是建立在对数据模型的严格要求上。 SQL背后的理论基础是关系代数,关系模型则要求数据预先定义好Schema(有哪些表,每张表有哪些列,每一列的类型是什么),然后按照定义好的Schema去写数据。相对应的,文档模型则对于数据Schema的要求非常灵活,每条数据就是一个文档,文档内部的数据格式不做严格限定。 正所谓天下没有免费的午餐,这种对数据模型的限制,反过来也会成为一种对灵活性的束缚。在大数据场景中,半结构化、无结构化的数据是非常常见的,因此也催生了 Schema-on-Write 和 Schema-on-Read 两种不同的处理方式。
日志分析场景下的SQL
日志数据天然是弱Schema的
- 来源多样性:日志数据种类繁多,不同来源的数据难以具有统一的Schema;
- 数据随机性:比如异常事件日志、用户行为日志,往往天然就是随机的,难以预测的;
- 业务复杂度:不同的参与方对schema的理解不同,比如开发流程中打日志的往往是开发者,但分析日志的往往是运维和运营,写日志过程中没有足够预见性导致不能被分析人员直接使用;
- 记录数据(record data):指的是原始数据,只会在系统中存一份;
- 派生数据(derived data):指的是从原始数据衍生出的各种形式的数据,如索引、缓存、物化视图等等。派生数据是原始数据的冗余,本质上都是为了不同的读取场景进行优化。
增效——固定Schema的限制
前面说到日志数据天然是弱Schema的,意味着Schema的不确定是常态。- 当 Schema发生变化时,必须要去更新索引,频繁索引操作带来运维负担;
- 非结构化的日志数据,可能字段数目非常多,无法一一枚举,或者会有新增字段难以提前预测,这些情况下难以去创建对应的字段索引;
- 如果字段发生变化未及时感知到,对于索引变更前的历史数据,需要重建索引,重建索引带来索引流量费用,且超过30天的历史数据无法重建;
- 构建列存时的字段长度是有限制的(默认2k,最大支持16k),如果字段过长,超出的部分会被截断无法分析;
降本——派生数据带来的成本
现有模式下,为了满足关系模型和保证查询的高性能,必须要构建索引和列存,从而产生索引流量和索引存储的成本。- 写多读少的场景,比如异常排查、审计,往往只在很少的时候需要分析,但是也必须全部开启索引;
- 非结构化数据中的长尾字段,即使数据价值密度很低 ,但是为了潜在的分析需求,也不得不提前开启索引;
SLS Schema-on-Read SQL设计与实现
需要解决的问题 我们来重新审视下基于索引和列存的Schema-on-Write模式的SQL实现,这里面索引和列存实际上提供了两个作用:- 创建字段索引的时候,指定了有哪些字段,每个字段是什么类型,相当于创建了一个关系模型的Schema
- 字段索引开启统计后,会为字段构建列存,从而在执行时可以高效读取指定列的数据
- 日志分析场景是非常强调交互式分析的灵活性的,试想一下,如果在每执行一条SQL语句对日志进行之前,都要先执行Create Table创建一个表,那么使用起来将会非常的不便捷;
- 在SLS的查询分析时,是指定了project和logstore信息的,也就是数据源的存储相关元信息已经知道了,唯一缺的只是列的信息(要分析哪些列,每个列是什么类型);
- 而 要分析哪些列,这个信息实际上是可以从SQL语句本身推断出来的,类型可以默认都为varchar(其他类型需要用cast语句转换下);
第二个要解决的问题是在执行数据读取的过程中,如何从非结构化的原始数据中,匹配提取出结构化的数据来参与计算。
这个过程在原理上是比较直观的,就是扫描原始数据,匹配提取出要读取的字段。在实现层面主要是要尽可能的提升这个扫描匹配过程的效率。 举个例子,比如 select count(1) as pv, Api, Status group by Api, Status 这条SQL语句,前面在Schema推断的过程中,会推断出有一张临时表,表里面包含了Api、Status这两列的数据。在执行阶段,就需要从原始数据中,读取出只属于这两列的数据。
- 通过时间范围和索引查询条件,过滤出需要扫描的原始数据
- 对分析的列名称求hash
- 对待扫描的原始数据进行逐行逐字段扫描,计算字段hash并和待分析的列的hash进行比较,匹配上则提取出对应的字段值,未匹配上则填充null
- 计算过程中优先比较上次匹配位置、记录已匹配情况提前终止等方式尽量减少循环次数
- 此外,结合LRU缓存、亲和性调度等优化,尽可能减少重复扫描
SLS 扫描分析模式介绍
扫描模式——SLS Schema-on-Read 查询/分析 解决方案 SLS的Schema-on-Read查询分析方案,在产品功能上称为“扫描模式”(也称为“Scan模式”),以区别于Schema-on-Write的索引模式。 扫描模式,顾名思义,不依赖于事先开启索引和列存,而是通过直接对原始数据进行“硬扫描”来进行相应的计算。 扫描模式和索引模式等不同的计算模式,以及查询型、分析型、冷热存储等不同存储选型,共同组合成SLS计算存储引擎面向不同场景下多样化的解决方案。
-
语法上竖线前都是索引字段过滤,竖线后是对原始数据硬扫描,并按照实际扫描的数据量计费。
-
扫描查询竖线后是“where + 布尔表达式”,用来条件过滤;扫描分析竖线后是标准SQL,用来进行聚合、关联分析。
- 扫描查询是检索原文,可以不断的往后翻页;扫描分析一般是用来算聚合指标,是对范围内数据的一次性分析。
控制台开启扫描模式
在控制台的“查询分析”右侧有一个“Scan扫描”的模式开关,打开这个开关表明开启 schema-on-read 能力(无需预先构建索引) 打开这个扫描模式的开关后,在控制台直接写分析语句即可:- 扫描分析模式下,分析语句是标准SQL,和索引模式下完全相同;
- 扫描分析模式下,SQL语句里待分析的字段(如下图中的UserAgent字段)无须事先建立索引;
- “分析模式:Scan模式”,这个表明当前工作在扫描模式下;
- “扫描数据量”表示在执行这次SQL的过程中,实际扫描的原始数据量,将会按照这个数据量计费。
通过Session参数开启扫描模式
更通用的开启扫描模式的做法是,在分析语句上设置Session参数 set session mode=scan , 比如 “* | set session mode=scan; select api, count(1) as pv group by api” 值得一提的是,对于SDK调用、告警、仪表盘、定时SQL等所有可以写sql语句的地方,都可以通过添加这个session参数来开启扫描模式。 没有银弹——扫描模式分析能力是有限的 在数据分析中,性能、成本、灵活性是一组“不可能三角”,Schema-on-Read分析模式增加了灵活性,降低了成本,则必然要付出性能上的牺牲。
- 读原始数据的IO放大
- 额外的扫描时间
SLS 扫描分析模式典型使用场景
情形一:Schema不确定的场景 扫描模式最重要的场景就是应对Schema无法事先确定(从而无法事先创建好对应的字段索引)的情况,这在日志场景中是非常常见的,比如:- 同一个logstore采集多个服务的日志,不同的采集配置对应的字段不完全相同;
- 日志文件本身是自描述的,比如通过logtail采集JSON格式日志,或者通过OSS导入CSV格式文件,这些格式下字段都是可变的;
- 通过sdk写入日志,根据运行时情况动态写入不同的日志字段;
- 通过数据加工或者定时SQL写入的日志数据,加工程序进行了修改导致字段发生变化
- 等等;
- 要么就是发生变化后去修改索引(问题:运维成本太高,而且可能字段太多无法都创建索引);
- 要么只能通过一定的约束保证产生的日志字段都相同(问题:可能无法做到);
- 要么通过数据加工进行二次加工清洗(问题:数据链路变长,额外的加工和存储成本);
- 或者甚至干脆用一个字段去存储所有数据,然后用sql里的json函数或者正则函数去提取需要分析的字段(问题:日志不便于阅读、单条长度可能超过字段索引长度限制、sql书写复杂、sql执行时每一条都要执行提取函数性能差);
一个JSON格式日志的例子
举个开发同学比较熟悉的例子,比如go语言中常用的Zap Logger,输出到日志文件中是打印成json形式,程序中可以通过zap.XXX添加json日志中的一个字段。 比如A同学通过这行代码,记录访问db的延时情况:打印出的日志是下面这样的json格式(略去时间戳、日志级别等公共字段),通过JSON模式采集到sls之后,在logstore就会新出现了status、url等新增字段,索引模式下为了能够分析这两个字段,需要给这两个字段增加索引。logger.Info("request-db",zap.Int("latency", latency),zap.String("url", url))
接下来B同学在增加了这样的日志,来记录某个临时增加的活动里发放的优惠券的类型和金额信息,这里面又新增了两个字段,有需要新增索引。{"msg":"request-db","latency": 103,"url": "mock.host"}
由于每个开发同学都可能会在代码中根据当前上下文打印自己需要的字段,其结果是最终日志文件中的可能的字段非常多,无法一一建立索引进行分析。为了解决这个问题,往往不得不要求开发同学将所有信息都打在公共的msg字段里,再只对msg字段建立索引并开启统计。然而这样实际上丢失了json格式的优势,分析的时候也非常不方便。 而在有了扫描分析模式之后,对于上面这种json日志格式,可以只对固定的msg字段开启索引,其他可变字段无须开启索引。logger.Info("discount-coupon",zap.Int("type", discountType),zap.String("amount", discountAmount))
msg:request-db | set session mode=scan; select avg(cast(latency as bigint)) as avgLatency, url group by url order by avgLatency desc
注意:扫描分析模式下字段被视为varchar,因此对这里的latency要转为数字类型才能求均值。
而B同学要去分析“不同类型的优惠券分别发放了多少金额”,则可以这样进行分析:
msg:discount-coupon | set session mode=scan; select sum(cast(amount as bigint)) as totalAmount, type group by type order by totalAmount desc
这样打印日志的时候,就可以针对特定场景自由的新增自己关心的特征日志用于分析,而不需要考虑索引的变更。
在分析的时候,先用相应的关键词过滤下(过滤后保证日志都是符合要分析的结构的,而且降低了扫描规模),然后再使用扫描分析模式,就可以直接通过sql分析自定义的指标字段。
情形二:写多读少的降本场景
对于写多读少的场景,如果日志的业务方经过分析,认为有部分字段不需要高频的查询分析,没有必要对100%的日志开启索引,希望合理降低日志的成本。
这里举个参考的例子,做如下假设:
- 每条日志大小是1KB,有10个字段,分别是 key_0/key_1/.../key_9,其中 key_0/key_1 是被经常会被查询分析,其余8个字段很少使用;
- 每天日志量10亿条(即1TB),存储周期7天,日志压缩率按6计算;
- 对key_2/.../key_9这8个低频字段的分析频次是每天200次,每次分析时需要扫描500万条日志(即5GB);
| 100%索引 方案 | 20%索引 + 扫描 方案 | |
| 写入流量(压缩后) | 0.17 TB/天 | 0.17 TB/天 |
| 索引流量 | 1TB/天 | 0.2 TB/天 |
| 索引存储量 | 7TB | 1.4 TB |
| 原文存储量(压缩后) | 1.17 TB | 1.17 TB |
| 扫描流量 | 0 | 1 TB/天 |
| 单日费用 | ¥486 | ¥183 |
- 扫描模式能够分析的数据量有限,耗时也会上升,需要评估业务上能否接受
- 扫描分析模式下评估成本的关键因素是扫描流量。 上述例子中每天1TB的扫描流量的费用是51.2元,对于不同的分析qps、单次扫描数日志行数、单条日志大小,最后算出来的扫描流量费用的差别会很大,需要根据实际业务情况评估。 如果需要分析的频次比较高并扫描的数据量大,建议还是使用索引模式。
查询型Logstore/Lite版本
查询型Logstore(也称为Lite版本)的定位是支持需要低成本长期存储、只需要查询不需要分析的场景,索引流量费用更低,但不支持SQL分析(无法为索引字段勾选“开启统计”)。 新推出的扫描分析模式也可以在查询型Logstore上进行使用,从而具备一定的分析能力。无索引的历史数据
某些情况下可能想分析的字段没有索引,比如:- 创建Logstore后没有及时开启索引配置,但是数据已经写入了;
- Schema发生变化后没有及时更新索引配置,因此新增的字段就没有索引;
日志字段超出索引长度限制
索引分析模式下,字段能够被分析最大长度有限制(默认2k,可以调整配置,最大调整到16k),背后的原因是因为构建列存的时候,对列存的单条记录的长度有限制。超出长度的部分被截断,无法参与分析。 在扫描分析模式下,因为是直接对原始数据进行扫描的,对于这种字段长度超出限制的场景,可以使用扫描分析模式进行分析。 不过需要注意的是,无论是从分析性能还是灵活性上考虑,日志采集的最佳实践仍然是尽可能的按照日志结构对字段进行拆分,而不是都放在一个字段里。正如前面JSON日志的例子中指出的,可以对固定的、需要高频分析的字段建立索引并开启统计,对低频的、可变的字段使用扫描模式。总结与展望
本文对SLS Schema-on-Read分析的提出背景、设计思路、实现过程、使用方式、适用场景进行了全面的介绍:-
大数据技术的发展历程中,SQL逐渐成为数据分析领域的通用语言,而由于关系模型对Schema的约束,催生了Schema-on-Read概念的兴起。
-
日志数据天然是弱Schema的,为了能够支持SQL,现有的做法是先定义好Schema,从原始非结构化数据中冗余一份结构化的符合关系模型的数据(列存),但这样又带来了灵活性不够和成本上升的问题。
-
为了满足增效降本的诉求,SLS基于Schma-on-Read的理念,推出了扫描分析模式,无须在查询前先声明Schema,直接书写SQL语句即可在原始的非结构化数据上进行计算分析,在Schema多变的场景下能够体现出极大的灵活性,同时也能用于写多读少场景下的降本。
-
同时也正如文中数次提到的,扫描模式不是银弹,并不能取代索引模式。
在分析的数据规模大、分析频次高、分析性能有要求的场景下仍然需要首选索引模式,扫描模式则主要面向中小数据规模、Schema不确定、写多读少以及一些无法使用索引分析的场景。
参考:
1、 Demystifying Data Explorer :https://techcommunity.microsoft.com/t5/azure-synapse-analytics-blog/demystifying-data-explorer/ba-p/3636191 2、 MapReduce: A major step backwards :https://homes.cs.washington.edu/~billhowe/mapreduce_a_major_step_backwards.html 3、 Designing Data Intensive Applications :https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/ 4、 Why SQL is beating NoSQL and what this means for the future of data :https://www.timescale.com/blog/why-sql-beating-nosql-what-this-means-for-future-of-data-time-series-database-348b777b847a/ 5、 Spanner: Becoming a SQL System :https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/46103.pdf 6、大数据技术发展简史:https://cloud.tencent.com/developer/article/1640405 7、扫描分析模式概述:https://help.aliyun.com/document_detail/473045.html 8、标准型与查询型Logstore介绍:https://help.aliyun.com/document_detail/48990.html