ES 进阶篇
😉 本文共7000字,阅读时间约30min
ES 集群
分片Shards & 副本Replicas
-
分片好处:查询、吞吐量与存储的水平扩展能力,允许在分片上进行并行的操作。
-
副本好处:提供故障恢复/转移能力,扩展搜索量/吞吐量,读操作可以在所有副本上并行运行
ES集群角色
Master节点
- master节点控制整个集群的元数据。只有Master Node节点可以修改元数据,比如索引的新增、删除、 Mapping 、Setting 配置等等。
- 过半选举:为了避免产生脑裂,ES采用了常见的分布式系统思路,保证选举出的master被多数派(quorum)的master-eligible node认可,以此来保证只有一个master。
Coordinating Node协调节点
协调节点,是一种角色,而不是真实的Elasticsearch的节点,你没有办法通过配置项来配置哪个节点为协调节点。集群中的任何节点,都可以充当协调节点的角色。
协调节点接受客户端搜索请求后将请求转发到与查询条件相关的多个data节点的分片上。然后多个data节点的分片执行查询语句或者查询结果再返回给协调节点,协调节点把各个data节点的返回结果进行整合、排序等一系列操作后再将最终结果返回给用户请求。
路由
路由计算
插入和查询时如何知道文档应该在哪一个分片?
shard = hash(routing) % 主分片个数
,routing
默认为文档_id
。
ES中创建索引时就确认好主分片个数,并且不能修改主分片个数。如果数量变化,要做大量的rehash重新分布数据。
ES写数据流程
- 新建、索引和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
- 延时:主分片的延时 + 并行写入副本的最大延迟
- 数据一致性参数:
consistency
- 写操作,主分片要求必须要有规定数量quorum(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行写操作(其中分片副本 可以是主分片或者副本分片)。规定数量即: int((primary + number_of_replicas) / 2 ) + 1,这个数量可以防止网络分区的脑裂,保证写一致性。
- 数据写入是先写入主分片,再由主分片节点推送到其他副分片上去,此时主分片线程等待副分片回应。副分片写入成功后回复主分片,然后主分片节点会把请求成功(不需要所有副分片都返回成功,也有说需要过半副分片)消息返回给协调节点,再由协调节点回复客户端成功。
- 这是Push模式。很多分布式系统都是使用副本Pull模式,比如redis、mysql。
ES读数据流程
-
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
-
这种转发请求的设计和Redis不一样,Redis是返回目标节点的地址给客户端自己去重定向。
ES更新流程
- 客户端向Node 1发送更新请求,协调节点将请求转发到主分片所在的Node 3 。
- Node 3从主分片检索文档,修改_source字段中的JSON,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试,超过retry_on_conflict次后放弃。
- 如果更新成功,主分片会并行push到副本分片。一旦所有副本分片都返回成功,Node 3向协调节点也返回成功,协调节点向客户端返回成功。
注意ES中没有修改的概念,只是删除-新增。
批量操作模式
bulk API的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。
- 协调节点将整个多文档请求分解成每个主分片的多文档请求,并且将这些请求并行转发到每个参与节点
- (主分片push到副本分片)。一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功。
- 协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端。
ES 写入原理 & 搜索原理
写入原理
可以看到与Kafka相似的Page Cache与和Mysql相似的WAL技术(redo log),其实kafka也有磁盘顺序写,就是写日志。
整体流程分四步
- write(数据写入buffer和translog)
- refresh(buffer数据提交刷新到segment file和os cache,此步骤以后,数据才可见)
- flush(os cache缓存数据同步到磁盘,清空当前时间段内translog)
- merge(每次refresh产生的小segment file合并成大的segment file)
-
write:数据先写入内存buffer(基于内存的缓冲),在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件。
-
refresh:到一定时间(默认1s),就会将内存buffer数据refresh 到os cache中一个新的segment file。
操作系统里面,磁盘文件其实都有一个东西,叫做os cache,操作系统缓存,就是说
数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去,再进入磁盘
-
flush准实时:只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了
-
merge:buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒钟一个segment file,segment file会越来越多,此时会定期执行merge,每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,
搜索原理
- 搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch;
- 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索。 PS:在搜索的时候是会查询Filesystem Cache 的,但是有部分数据还在 Memory Buffer,所以搜索是近实时的。
- 每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
- 接下来就是取回阶段, 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。一旦每个分片的文档都被取回了,协调节点返回结果给客户端。
ES 倒排索引 & 分词器
倒排索引
全文检索要求能从关键词 -> 文档ID。ES使用一种称为倒排索引的结构,它适用于快速的全文搜索。
什么是正向索引
正向索引指的是文档->关键词的形式,查询时需要遍历每一个文件。例如,“文档1”的ID > 单词1:出现次数,出现位置列表;单词2:出现次数,出现位置列表;…………。
倒排索引不可改变 & 按段管理
以下都是磁盘层面的
- 倒排索引被写入磁盘后是不可改变的:它永远不会修改。
- 好处:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 坏处:需要让一个新的文档可被搜索,你需要重建整个索引。
- 好处:
按段管理 & Segment Merge机制
如何在保留不变性的前提下实现倒排索引的更新?
- 按段搜索,用更多的倒排索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。(index & merge)。
- segment是lucene索引的一种存储结构,每个segment都是一部分数据的完整索引,它是lucene每次flush或merge时候形成。
- 因为segment是不可变的,删除操作不会改变segment内部数据,只是会在另外的地方记录某些数据删除,这样可能会导致segment中存在大量无用数据。
- 搜索时,每个segment都需要一个reader来读取里面的数据,大量segment严重影响搜索效率。
- merge是lucene的底层机制,merge过程会将index中的segment进行合并,生成更大的segment,提高搜索效率。
- merge过程,会将小的segment写到一起形成一个大的segment,减少其数量。同时重写过程会抛弃那些已经删除的数据。因此segment的merge是有利于查询效率的。
分词器
将一块文本分成适合于倒排索引的独立的词条。
字符过滤器
字符串按顺序通过每个字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and。
分词器
字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。(ik_max_words和ik_smart)
设置自定义词典并写入ik配置文件 ,自定义单词,不会被分词。
Token过滤器
词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,大写转小写),删除停用词,或者增加词条(例如,像jump和leap这种同义词)。
IK分词器
ES 打分原理
- 使用BM25,基于TF-IDF的改进版本
TF-IDF
- TF:搜索的词在单个文档中出现的次数越多,相关性越高。TF表示的是词与文档的关联程度。
- IDF:搜索的词在所有文档中出现的次数越少,相关性越高。IDF表示的是区分度、稀缺性,用以评估一个单词在语料库中的重要程度。
BM25
BM25相比TF/IDF的一大优化是降低了tf在过大时的权重,避免词频对查询影响过大。
总结一句话就是:BM25算法的优化,如果你单个词超过一定数量,这个词超过的那部分,将不进行算分!
ES 同步 & 导入
同步
当业务量上升后,由于mysql对全文检索或模糊查询支持的能力不强,在系统中查询的地方,往往会出现慢sql等,拖累系统其他模块,造成性能低下。
随着ES使用普及率的升高,ES是mysql的一个有效补充。我们可以将数据发送到搜索引擎(如ES)上,由搜索引擎来提供专业的服务。
同步双写
将数据写到mysql时,同时将数据写到ES,实现数据的双写。
优点:业务逻辑简单。
缺点:1. 存在双写失败丢数据风险;2. 性能较差:本来mysql的性能就不是很高,再加写一个ES,系统的性能必然会下降。
异步双写(MQ)
针对第一种同步双写的性能和数据丢失问题,可以考虑引入MQ,从而形成了异步双写的方案,由于MQ的性能基本比mysql高出一个数量级,所以性能可以得到显著的提高。
缺点:1. 性能高;不存在丢数据问题。可能存在MQ消费时延问题 2. 系统中增加了mq的代码
Binlog 同步
-
读取mysql的binlog日志,获取指定表的日志信息;
-
将读取的信息转为MQ;
-
编写一个MQ消费程序;
-
不断消费MQ,每消费完一条消息,将消息写入到ES中。
优点:无代码侵入,业务解耦
缺点:存在MQ延时的风险
canal主要用途是对MySQL数据库增量日志进行解析,提供增量数据的订阅和消费,简单说就是可以对MySQL的增量数据进行实时同步,支持同步到MySQL、Elasticsearch、HBase等数据存储中去。
canal会模拟MySQL主库和从库的交互协议,从而伪装成MySQL的从库,从而获取binlog
导入优化
1. 用SSD
机械60 - 80M/s
SSD 300 - 500M/s
2. 多线程 & bulk导入
批量写入意味着相同时间产生的段会大,段的总个数自然会少。逐步尝试上限。
单线程发送bulk请求是无法最大化es集群写入的吞吐量的。如果要利用集群的所有资源,就需要使用多线程并发将数据bulk写入集群中。
3. 设置合理Mapping & 使用系统自增ID
- 不需要全文搜索的字段,index 设置为 false
- 不需要分词的字段设置为 not_analyzed。
如果使用外部 id,Elasticsearch 会先尝试读取原来文档的版本号,以判断是否需要更新。也就是说,使用外部控制 id 比系统自动生成id要多一次读取磁盘操作。
4. 参数配置
1. 副本写入前置为0
副本设置一个,每个副本也会执行分析、索引及可能的合并过程,所以 Replicas 的数量会严重影响写索引的效率。当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。
副本分片写入前置为0,等写入完成后还原副本。设置index.number_of_replicas: 0
。
实际上副本数量就设置成0了,因为ES保存的都是从其他数据库获得的最新数据,丢失了还可以读回,业务影响不大。况且副本数量是可以随时修改的,区别分片数量。
2. 加大refresh_inteval
每隔1min才会创建一个segment file。目的是减少Segment Merge的次数。
禁止refresh和replia
如果我们要一次性加载大批量的数据进es,可以先禁止refresh和replia复制,将index.refresh_interval设置为-1,将index.number_of_replicas设置为0即可。这可能会导致我们的数据丢失,因为没有refresh和replica机制了。但是不需要创建segment file,也不需要将数据replica复制到其他的replica shasrd上面去。此时写入的速度会非常快,一旦写完之后,可以将refresh和replica修改回正常的状态
3. 加大index_buffer_size
缓存区越大,意味着能缓存数据量越大,相同时间段内,写盘频次低、磁盘 IO 小,间接提升写入性能。
5. 合理设置分片数
- 好处:提高吞吐量
- 坏处:不是越多越好,一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU运转。
ES 深度分页
from+size
使用from+size方式进行分页,受max_result_window默认参数10000条文档的限制,不建议针对该参数进行修改。
默认分页方式,适用小数据量场景,大数据量场景应避免使用。
通过性能测试,随着分页越来越深,执行时间和堆内存使用逐渐升高的趋势,在并发情况下from+size容易 造成集群服务的OutOfMemory问题。
Scroll
Scroll游标方式分页查询适用大数据量场景,只能向后增量查找,无法向前或者跳页查询,适用增量滚动抽取、数据迁移、重建索引等场景。
通过性能案例分析,滚动分页查找性能消耗相差不大,不会像from+size方式随着分页的深入性能逐渐升高的问题,且不会存在OOM问题。该分页方式是查询的历史快照,对文档的更改(索引的更新或者删除)只会影响以后的搜索请求,响应时间很长,且不适用实时性查询场景。
search_after
分页方式弥补了 scroll 方式打开scroll 占用内存资源问题。
search_after可并行的拉取大量数据。search_after分页方式通过唯一排序值定位,将每次需要处理的数据控制在一定范围,避免深度分页带来的开销,适用深度分页的场景。
CK 入门
表引擎
MergeTree
- MergeTree表引擎主要用于海量数据分析,也就是插入极大量数据到一张表中
- 数据可以以数据片段的形式一个接着一个的快速写入,数据片段在后台按照一定的规则进行合并。相比在插入时不断修改(重写)已存储的数据,这种策略会高效很多。
- 支持数据分区、存储有序、主键索引、稀疏索引、数据TTL等。
- 特点:指定分区键,可以使用分区
- 在相同数据集和相同结果集的情况下 ClickHouse 中某些带分区的操作会比普通操作更快。查询中指定了分区键时 ClickHouse 会自动截取分区数据。这也有效增加了查询性能。
AggregatingMergeTree
- 继承自MergeTree,改变了数据片段的合并逻辑,适用于能够按照一定的规则缩减行数的情况。
- ClickHouse 会将一个数据片段内所有具有相同主键(准确的说是 排序键)的行替换成一行,这一行会存储一系列聚合函数的状态。
- 可以使用
AggregatingMergeTree
表来做增量数据的聚合统计,包括物化视图的数据聚合。
Kafka
- 属于集成引擎:用于与其他的数据存储与处理系统集成的引擎
- 使用引擎创建一个 Kafka 消费者并作为一条数据流。创建一个结构表。创建物化视图,改视图会在后台转换引擎中的数据并将其放入之前创建的表中。
分区分片
-
分区是表的分区,具体的DDL操作关键词是
PARTITION BY
,指的是一个表按照某一列数据(比如日期)进行分区,对应到最终的结果就是不同分区的数据会写入不同的文件中。 -
数据分区-允许查询在指定了分区键的条件下,尽可能的少读取数据
-
数据分片-允许多台机器/节点同并行执行查询,实现了分布式并行计算
物化视图
- 普通视图:虚拟表,普通视图没有真正存储数据,只是读取数据的执行操作,可以看作是一条保存过的SQL查询。
- 物化视图:特殊物理表,包含一个查询结果的数据库对象
列式存储
-
好处:
-
分析类场景:在行存模式下,数据按行连续存储,所有列的 数据都存储在一个block中,不参与计算的列在IO时也要全部读出,读取操作被严重放大。而列存模式下,只需要读取参与计算的列即可,极大的减低了IO cost,加速了查询。
-
同一列中的数据属于同一类型,压缩效果显著
。列存往往有着高达十倍甚至更高的压缩比,节省了大量的 存储空间,降低了存储成本。
- 更高的压缩比意味着更小的data size,从磁盘中读取相应数据耗时更短。
- 高压缩比,意味着同等大小的内存能够存放更多数据,系统cache效果更好。
-
ClickHouse为什么快?
-
列式存储、数据压缩
-
向量引擎,利用 SIMD 指令实现并行计算。对多个数据块来说,一次 SIMD 指令会同时操作多个块,大大减少了命令执行次数,缩短了计算时间。一个batch调用一次SIMD指令(而非每一行调用一次),不仅减少了函数调用次数、降低了cache miss,而且可以充分发挥SIMD指令的并行能力
-
很多 MergeTree,在索引上也有不同,采用了稀疏索引及跳数索引。
-
分区分片
-
ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。
CK 与 ES 适用场景比较
场景不同点:
- ES不适合聚合查询,用了倒排索引,本质上是一个搜索引擎。es也有预写入。
- CK比较适合聚合查询,纯粹适合大宽表做数仓OLAP。不适合join操作,甚至不适合分布式。
性能不同点:
- CK能很好地压缩数据,ES的数据存储成本大。
- CK并发不如ES,单条查询使用多cpu,就不利于同时并发多条查询。
- ES吞吐写入能力不如CK