索引是加速数据库查询的重要手段,Lindorm除了提供高性能的二级索引外,同时支持搜索索引 (SearchIndex),主要面向复杂的多维查询场景,并能够覆盖模糊查询、聚合分析、排序、分页等场景。本文主要介绍SearchIndex的技术原理和核心能力。
背景
在海量数据存储的背景下,伴随着云原生、5G/IoT时代的到来,新的业务模型在不断涌现,除了简单的主键查询和范围查询外,简单分析、多维检索成为业务的基本需求。常见的一些查询需求如下:
多维查询。即席查询 (adhoc),一般是不固定的列随机组合。
count计数。获取数据表的总行数,或者返回一次查询命中的数据条数。
指定列排序。按照指定列降序或升序,比方说按照订单时间降序输出结果。
分词检索。支持文本字段的分词检索,返回相关性较高的结果数据。
统计聚合。按照某个字段进行聚类统计,求取sum/max/min/avg等,或者返回去重后的结果集。
模糊查询。查询以'阿里'开头的数据,可以匹配出'阿里云'的结果集,类似MySQL的like语法。
诸如此类对海量数据低成本存储和检索多样化的需求,成为越来越多业务的基本诉求。Lindorm系统持续探索如何在Lindorm系统本身的高扩展、低成本的优势之上,同时去支撑业务的多样化查询场景。
Lindorm SearchIndex 设计思路
为了在有限的资源下尽可能高效的满足业务复杂查询的诉求,Lindorm期望设计一种新的引擎,以数据库特性的方式即开即用,帮助业务解决海量数据下的复杂查询问题。索引通常用来加速查询,可以通过增加一种新的索引类型来解决海量数据的复杂查询问题,Lindorm作为一个多模数据库,原生支持搜索引擎,天然具备全文索引能力。因此,通过融合搜索引擎,Lindorm宽表增加了SearchIndex,使得业务在不用感知底层的多个引擎以及数据流转的情况下,通过申请一个新的索引即可解决复杂的查询问题,就像使用Lindorm二级索引一样方便快捷。SearchIndex重点构建以下能力:
统一元数据 多套系统运维复杂的根因是各自的元数据不统一,需要使用各自专用的命令才可以操作,例如在一个系统中建完表,还需要在另外一个系统中建索引。通过维护统一的分布式元数据,我们可以屏蔽掉不同引擎间的Schema差异,提供统一的命令完成DDL类的操作。
统一接口 多个系统间的接口存在差异,通过实现一套专有的统一接口可以有效降低开发的复杂度,但这也需要业务学习和理解新的接口,应用开发成本没有明显的降低。SQL作为众多数据库系统的开发语言,使用和学习成本都较低,Lindorm SearchIndex原生支持类SQL接口:CQL,业务开发过程中不感知索引的存在,在使用体验上与原始的宽表访问保持一致。
强一致性 数据在多个引擎间流转必然会涉及到一致性问题,通常只能提供最终一致性的语义,数据的正确性和访问延迟无法有效保障。Lindorm SearchIndex提供了最终一致性和强一致性两种语义,对于访问量大、数据延迟性要求不高的场景采用最终一致性,可以提供非常高的吞吐和可用性,而业务访问延迟敏感的业务可以选择强一致性模型,数据写入成功后,索引立即可查。
资源隔离 异构系统对资源的使用各不相同,必须建立有效的隔离机制确保资源使用最大化,Lindorm通过
存储和索引
分离的模式来保障系统的健壮和弹性。宽表引擎负责存储原始数据,具备极低的存储成本,搜索引擎负责索引和检索,两个引擎可以配置不同的CPU、内存资源,并且可以独立扩缩容。
Lindorm SearchIndex 功能解析
使用举例
使用SearchIndex创建索引表时只需要枚举出索引列名即可,查询时不需要感知索引表的存在。以下示例介绍如何使用Lindorm CQL操作SearchIndex。
CQL是Cassandra的查询语言,Lindorm无缝兼容Cassandra API。
原始表
CREATE TABLE myTable ( id bigint, name text, age int, sex text, city text, address text, PRIMARY KEY (id) ) WITH compression = {'class': 'ZstdCompressor'};
索引
对姓名 (name)、年龄 (age)、性别 (sex)、城市 (city)、地址 (address) 建立全文索引。
CREATE SEARCH INDEX myIndex ON myTable WITH COLUMNS (name, age, sex, city, address);
说明索引列的先后顺序不影响,即索引列 (c3, c2, c1)与索引列 (c1, c2, c3)最终的效果是一致的。
查询
标准查询语句
模糊查询:SELECT * FROM myTable WHERE name LIKE '小%' 多维查询排序:SELECT * FROM myTable WHERE city='杭州' AND age>=18 ORDER BY age ASC 多维查询翻页:SELECT * FROM myTable WHERE name='小刘' AND sex=false OFFSET 100 LIMIT 10 ORDER BY age DESC
高级查询语句
多维查询排序:SELECT * FROM myTable WHERE search_query='+city:杭州 +age:[18 TO *] ORDER BY age ASC' 文本检索:SELECT * FROM myTable WHERE search_query='address:西湖区'
适用场景
SearchIndex在阿里内部已经成功应用多个业务场景,当前该特性在公有云上已经发布,支持的重要功能列表如下:
多维查询:多个条件任意组合的精确查询、范围查询等。
通配符查询:* 代表任意个字符;? 代表任意单个字符。
统计聚合:求最小值、求最大值、求和、求平均值、统计行数。
排序分页:任意索引列的排序输出。
文本分词:支持中文/英文分词,分隔符分词,拼音分词等。
地理位置:距离查询、长方形/多边形范围查询。
有了这些功能,可以很容易的将Lindorm应用到多样化的业务场景中,经典的使用场景主要有以下几个:
订单详情,例如物流订单、交易账单,支持订单的多维查询、排序等。
标签画像,例如基于商家对买家进行标签圈选,定向投递信息。
文本搜索:例如日志分析,异常信息检索等。
实现原理
Lindorm作为一款多模数据库,同时支持多种模型,将搜索引擎与宽表模型深度融合,对外提供简单易用的SearchIndex,整体的分层架构如下:
查询接入
由多个QueryProcessor节点组成,主要负责查询接入,进行SQL解析,基于RBO自动选择合适的索引。
索引预处理
基于索引列的元信息将新插入或者更新的原始数据转换为索引数据,并且针对不同的场景可以选择与之匹配的Mutability属性,比较典型的例如日常监控,数据写入后不更新,可以选择Immutable模式,直接生成索引原始数据;而那些有状态的数据,大多需要局部更新,此时通过回读历史数据组装成索引原始数据,并且能够支持业务自定义时间戳的写入,确保数据不乱序。
索引同步
对于最终一致模式(默认),LTS (Lindorm Tunnel Service)作为Lindorm生态的数据同步服务,具备高效的实时同步和全量迁移能力。可以实时监听WAL的变化,将索引原始数据转换后写入到搜索引擎,同时支持一读多写,即一份WAL可以同步到多个索引表中,极大提升同步效率;对于强一致模式,索引原始数据构建完毕后同步写入到搜索引擎,由搜索引擎实时生成全文索引,为业务提供写入即可查的强一致体验。
索引引擎
由多个节点组成的分布式Lucene集群,数据按照Hash或者Range来划分为多个Shard,对外提供全文检索能力。
索引存储
索引数据存储在分布式文件系统Lindorm DFS上,存算分离的架构具有极好的扩展性,同时存储层的透明压缩和智能冷热分离可以显著降低索引的存储成本。
核心特性
Online DDL Operations
作为一个分布式数据库,Lindorm可以横向扩展支持高达亿次每秒的处理能力,如果索引DDL需要阻塞DML,对高并发的业务应用影响将会被放大。借助Lindorm的分布式元数据管理,SearchIndex通过合理的扩展,可以支持在线DDL操作,并且不会破坏数据的完整性。
动态增加、删除索引列 在宽表的应用场景中,列可能不会固定,尤其是标签画像场景,需要经常性的增加或删除索引列。SearchIndex提供Java API/CQL接口来动态操作索引列。
在线变更索引列属性 每个索引列支持多种属性:indexed(是否索引,默认true)、stored(是否存储原始值,默认false)、docvalues(是否维护正排索引,默认true)、分词类型等。例如某个索引列起初并未设置stored,那么在索引表中是不会存储原始值的,服务端会自动回查Lindorm宽表获取原始数据。此时,可以通过接口变更索引列的stored为true。
动态修改压缩 Lindorm宽表在创建时可以设置压缩算法(例如ZSTD、Snappy),也可以创建表后动态修改。SearchIndex是一个独立的索引表,底层依赖Lucene,仅支持LZ4和ZLIB两种压缩算法,为了保证原始主表与索引表的属性统一,我们通过修改Lucene,让其支持ZSTD、Snappy压缩算法。因此,在修改原始主表压缩算法时,也会联动修改SearchIndex,有效降低索引的存储大小。
动态修改TTL 为了自动淘汰历史数据,Lindorm支持动态修改表的TTL,例如设置TTL=30days,代表从此刻起30天前的数据将会被淘汰,并且无法查询。我们在Lucene的基础上实现了行级TTL能力,可以与原始主表的TTL联动,自动淘汰过期数据,确保主表和索引表的数据一致。
动态修改索引表状态 索引表的状态主要有三种:DISABLED(不可写,不可查)、BUILDING(可写不可查)、ACTIVE(可写可查)。在创建、删除索引或者对历史数据构建索引时,我们经常需要动态变更索引的状态,此时也不能够影响到运行中的DML请求。
多一致性
如同Lindorm宽表,在设计SearchIndex时,Lindorm针对不同的业务场景提供了多一致性的支持。
一致性等级 | 读写一致性保障 | 可用性 |
最终一致性(EC) | 数据写入后,需要等待一段时间可读(秒级)。 | 读写可规避任何hang及毛刺; 宕机读写恢复10毫秒。 |
强一致性(SC) | 数据写入后,100%可以立即读到。 | 只有主副本提供读写服务; 主副本宕机恢复一般在秒级。 |
可选的索引构建成本
索引可以加速查询,助力业务进一步挖掘数据的价值,但会带来写入成本和存储成本的增加。一方面,Lindorm通过多种高效的压缩算法显著降低索引的存储体积;另一方面,通过提供可选的索引构建方式降低索引构建对写入吞吐的影响。索引WAL的构建
快慢将直接影响到原始数据的写入性能。Lindorm宽表是一个KV数据库,天然支持更新部分列,但是搜索引擎Lucene只能整行更新,不能够局部更新。因此,在构建索引时需要回读原表获取历史数据,才能够拼接出完整的索引WAL。Lindorm将这个回读操作按照业务场景进行分类,支持不同的选择。
构建方式 | 适用场景 | 属性 |
IMMUTABLE (成本最低) | 数据只增不删的场景(可以TTL淘汰数据)。 例如监控数据/日志数据,数据写入后不会更新和删除。 | 索引构建非常高效,不需要回查旧数据,直接依据当前的数据生成索引。 |
MUTABLE_LATEST(成本中等) | 通用场景(除UDT)。 | 假设索引列为c1,c2,第一次写入c1列,第二次写入c2列。那么在第二次写入c2的值时,需要读出原始的c1值,才能够拼接出完整的索引数据c1,c2。 |
MUTABLE_ALL (成本最高) | 写入数据时,业务自定义时间戳(User-Defined Timestamp)。 例如:全量任务和增量并存的场景,往往需要在全量任务时携带时间戳,这样可以确保不会覆盖增量写入的数据。 |
|
高效同步
在最终一致性的模型中,索引数据同步依赖LTS服务。LTS服务作为Lindorm生态的数据通道,具备高效的实时同步和全量迁移能力,写入宽表的数据,可以在毫秒内感知到,快速的同步到搜索引擎中。
同步可视化
LTS提供Web访问,可以查看到索引同步的详细信息。例如同步的耗时、索引表信息、同步的数据量等。另外,LTS可以将这些信息对外吐出监控指标,对接告警体系,实时监测同步链路的健康度。
同步高效率
LTS内部通过高并发的生产者/消费者模式,支持快速消化大量的数据,一份WAL只需要读取一次。并且支持横向扩展,新加入的节点可以快速加入到同步链路中,加速索引数据的同步。
WAL保序
通过隐藏的时间戳属性,保证在宽表中先写入的数据先写入搜索,后写入的数据后写入搜索,确保宽表和搜索的数据一致性,彻底解决LilyIndexer存在的数据错乱问题。
全量构建快
对于已有的历史数据,可以借助LTS的全量任务运行机制,高效的从宽表中获取原始数据生成索引,TB级别的数据量分钟内即可完成索引构建。
数据快速校验
支持对已有数据的比对校验,可以快速筛选出不一致的索引数据,帮助业务及时发现问题。
索引实时可见(RealTime Search)
写入成功后的索引数据可以立即可查,即为索引的实时可见,是一种强一致的模型。SearchIndex底层依赖Lucene,Lucene有一个明显的"缺陷":数据写入后不能立即可查,必须要显示的执行Flush或者Commit操作才可以查询到。这导致基于Lucene的服务无法应用到实时业务场景,只能适用于监控、日志等弱实时的场景。在业界,基于Lucene的分布式搜索引擎Elasticsearch/Solr为了缓解这个问题,提供近实时查询(NRT)
功能,可以确保索引数据在某个时间范围内(通常在秒级)一定可查,但还是达不到实时性的要求。
为了解决写入的数据无法立即可查
的问题,Lindorm基于Lucene实现了一种索引实时可见的方案,通过精细化的数据结构设计和动态的内存管理机制,可以保证索引数据一旦写入成功后可以立即查询到,真正做到实时性。
CQL API
CQL是Cassandra的官方查询语言,是一种适合NoSQL数据库特点的SQL方言,由于Lindorm无缝兼容Cassandra,因此默认推荐使用CQL来访问SearchIndex。
DDL
创建索引
CREATE SEARCH INDEX index_name [ IF NOT EXISTS ] ON [keyspace_name.]table_name | [ WITH [ COLUMNS (column1,...,columnn) ] | [ WITH [ COLUMNS (*) ]
删除索引
DROP SEARCH INDEX [IF EXISTS] ON [keyspace_name.]table_name;
重构索引
REBUILD SEARCH INDEX [IF EXISTS] ON [keyspace_name.]table_name;
修改索引
ALTER SEARCH INDEX SCHEMA [IF EXISTS] ON [keyspace_name.]table_name ( ADD column_name | DROP column_name) ;
DML
标准查询,WHERE后面紧跟具体的条件。
search_query查询
SELECT selectors FROM table WHERE (indexed_column_expression | search_query = 'search_expression') [ LIMIT n ] [ ORDER BY column_name ]
当标准的查询语法无法满足检索需求时,可以考虑通过search_query
来直接从搜索引擎中检索数据,使用的语法也是Lucene的语法。例如,下面的用例中,相当于检索city为'hangzhou',并且age包含1到18的数据。
SELECT name,id FROM myTable WHERE search_query = '+city:hangzhou +age:[1 TO 18]';
详细的CQL语法可参考CREATE SEARCH INDEX。
案例介绍
订单场景
对于物流、第三方支付和移动出行等业务场景,订单数据的存储是核心需求。而且订单数据往往有其特殊的天然属性。
高增长:数据可能随时会爆发式的增长,例如双11或大促节日。
低成本:订单数据一般不会直接产生经济效益,是业务对外呈现的附加价值,需要低成本存储。
多维查询:对于C端用户而言,往往会从不同角度对订单进行分类、标记以及查看和过滤自己的订单。
在之前,面对上面的诉求,一般的解决方案是MySQL+搜索引擎。业务双写到两个系统中,或者借助binlog进行实时同步,查询时分别从不同的系统中获取结果。随着数据量的增长,可能会演变为MySQL热数据+Lindorm冷数据+搜索引擎的架构,基于多套系统可以有效解决业务问题,但需要面临多套系统维护的时间成本和人力成本。
现在,Lindorm可一一站式解决上述问题,不用关心数据的流转,统一的API访问。
通过冷热分离、压缩优化等手段显著降低存储成本。
横向弹性扩展适应海量数据的写入。
SearchIndex CQL提供丰富的查询语法。
用户画像
用户画像的数据一般有两种:一个是基础数据,另一个是经过分析得到的标签数据。这些数据可以被应用到营销、推荐等场景中,可以助力企业营收快速增长。画像数据的主要痛点如下:
数据量大:画像数据与用户基数强相关,往往在千万甚至亿级别,而且数据维度非常高,在我们服务的客户中,有的场景支持的标签数在5000个以上。
高并发。画像数据通常需要全量刷新,需要在基线的时间内完成才能有效辅助后续的推荐、广告投放等。
动态列。数据维度在不断的变化中,因此需要支持动态增/删列。
多维查询。面向不同的业务需求,画像数据查询需求也会有差异,运营人员通常会统计任意一个维度的数据。
画像场景一般没有强事务需求,而是大数据量、高并发读写的场景,关系数据库不太适合。Lindorm作为一款NoSQL数据库,非常适合这样的场景。
多列族、动态列、TTL等特性,适合表结构不固定,经常需要进行变更的业务场景。
高性能吞吐。
SearchIndex CQL支持任意维度的查询和统计。
日志检索
日志的来源非常广泛,例如系统日志、数据库审计日志、用户行为日志等,这些数据在互联网公司中通常存储于开源Elasticsearch(ES)中,借助ELK体系构建一站式的日志平台。但ES的存储成本是非常高的,而且存储和计算往往需要同机部署,在海量数据下系统运维面临非常多的挑战,数据迁移、节点扩缩容等都需要人工介入。多模数据库Lindorm的SearchIndex是日志检索场景的更优选择,通过宽表引擎存储海量数据降低成本,搜索引擎构建合适的索引加速查询,统一的API操作进一步降低业务开发成本。