本文主要分为两大部分
- 数据模型
- 读写方式
数据模型
- 业务场景
Elasticsearch是一个实时的分布式搜索和分析引擎,它可以帮助我们用很快的速度去处理大规模数据,可以用于全文检索、结构化检索、推荐、分析以及统计聚合等多种场景。
Elasticsearch是一个建立在全文搜索引擎库Apache Lucene 基础上的分布式搜索引擎。
- Lucene数据模型
- Index:索引,由很多的Document组成。
- Document:由很多的Field组成,是Index和Search的最小单位。
- Field:由很多的Term组成,包括name(String)、fieldsData(BytesRef)和type(FieldType)这3个属性。
- Term:由很多的字节组成,可以分词。
上述四种类型在Elasticsearch中同样存在,意思也一样。
Lucene中存储的索引主要分为三种类型:
- Invert Index:倒排索引,或者简称Index,通过Term可以查询到拥有该Term的文档。可以配置为是否分词,如果分词可以配置不同的分词器。
- DocValues:正排索引,采用列式存储。通过DocID可以快速读取到该Doc的特定字段的值。由于是列式存储,性能会比较好。一般用于sort,agg等需要高频读取Doc字段值的场景。
- Store:字段原始内容存储,同一篇文章的多个Field的Store会存储在一起,适用于一次读取少量且多个字段内存的场景,比如摘要等。
由于Lucene中没有主键概念和更新逻辑,所有对Lucene的更新都是Append一个新Doc,类似于一个只能Append的队列,所有Doc都被同等处理。其中的Doc由众多Field组成,没有特殊Field,每个Field都被同等对待。
Lucene只是提供了一个索引和查询的最基本的功能,距离一个完全可用的搜索引擎还有一些距离:
Lucene未考虑的事情
- Lucene是一个单机的搜索库,如何以分布式形式支持海量数据?
- Lucene中没有更新,每次都是Append一个新文档,如何做部分字段的更新?
- Lucene中没有主键索引,如何处理同一个Doc的多次写入?
- 在稀疏列数据中,如何判断某些文档是否存在特定字段?
- Lucene中生成完整Segment后,该Segment就不能再被更改,此时该Segment才能被搜索,这种情况下,如何做实时搜索?
上述几个问题,对于搜索而言都是至关重要的功能诉求,我们接下来看看Elasticsearch中是如何来解这些问题的。
Elasticsearch怎么做
在Elasticsearch中,为了支持分布式,增加了一个系统字段_routing(路由),通过_routing将Doc分发到不同的Shard,不同的Shard可位于不同的机器上,这样就能实现简单的分布式了。
采用类似的方式,Elasticsearch增加了_id、_version、_source和_seq_no等多个系统字段,通过这些Elasticsearch中特有的系统字段可以有效解决上述的几个问题,如下:
1. _id
Doc的主键,在写入的时候,可以指定该Doc的ID值,如果不指定,则系统自动生成一个唯一的UUID值。
Lucene中没有主键索引,要保证系统中同一个Doc不会重复,Elasticsearch引入了_id字段来实现主键。每次写入的时候都会先查询id,如果有,则说明已经有相同Doc存在了。
通过_id值(ES内部转换成_uid)可以唯一在Elasticsearch中确定一个Doc。
Elasticsearch中,_id只是一个用户级别的虚拟字段,在Elasticsearch中并不会映射到Lucene中,所以也就不会存储到该字段的值中。
_id的值可以由_uid解析而来(_uid = type + '#' + id),Elasticsearch中会存储_uid。
2. _version
Elasticsearch中每个Doc都会有一个Version,该Version可以由用户指定,也可由系统自动生成。如果是系统自动生成,那么每次Version都是递增1。
Elasticsearch通过使用version来保证对文档的变更能以正确的顺序执行,避免乱序造成的数据丢失:
- 首次写入Doc的时候,会为Doc分配一个初始的Version:V0,该值根据VersionType不同而不同。
- 再次写入Doc的时候,如果Request中没有指定Version,则会先加锁,然后去读取该Doc的最大版本V1,然后将V1+1后的新版本写入Lucene中。
- 再次写入Doc的时候,如果Request中指定了Version:V1,则继续会先加锁,然后去读该Doc的最大版本V2,判断V1==V2,如果不相等,则发生版本冲突。版本吻合则继续写入Lucene。
- 当做部分更新的时候,会先通过GetRequest读取当前id的完整Doc和V1,接着和当前Request中的Doc合并为一个完整Doc。然后执行一些逻辑后,加锁,再次读取该Doc的最大版本号V2,判断V1==V2,如果不相等,则在刚才执行其他逻辑时被其他线程更改了当前文档,需要报错后重试。如果相等,则期间没有其他线程修改当前文档,继续写入Lucene中。这个过程就是一个典型的read-then-update事务。
3. _source
Elasticsearch中有一个重要的概念是source,存储原始文档,也可以通过过滤设置只存储特定Field。
Elasticsearch中使用_source字段可以实现以下功能:
- Update:部分更新时,需要从读取文档保存在_source字段中的原文,然后和请求中的部分字段合并为一个完整文档。如果没有_source,则不能完成部分字段的Update操作。
- Rebuild:最新的版本中新增了rebuild接口,可以通过Rebuild API完成索引重建,过程中不需要从其他系统导入全量数据,而是从当前文档的_source中读取。如果没有_source,则不能使用Rebuild API。
- Script:不管是Index还是Search的Script,都可能用到存储在Store中的原始内容,如果禁用了_source,则这部分功能不再可用。
- Summary:摘要信息也是来源于_source字段。
4. _seq_no
严格递增的顺序号,每个文档一个,Shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。
任何类型的写操作,包括index、create、update和Delete,都会生成一个_seq_no。
每个文档在使用Lucene的document操作接口之前,会获取到一个_seq_no,这个_seq_no会以系统保留Field的名义存储到Lucene中,文档写入Lucene成功后,会标记该seq_no为完成状态,这时候会使用当前seq_no更新local_checkpoint。
5. _primary_term
_primary_term也和_seq_no一样是一个整数,每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1。
_primary_term主要是用来恢复数据时处理当多个文档的_seq_no一样时的冲突,避免Primary Shard上的写入被覆盖。
6. _routing
路由规则,写入和查询的routing需要一致,否则会出现写入的文档没法被查到情况。
在mapping中,或者Request中可以指定按某个字段路由。默认是按照_Id值路由。
Elasticsearch的写
Elasticsearch采用多Shard方式,通过配置routing规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据routing规则,将文档发送给特定Shard中建立索引。这样就能实现分布式了。
此外,Elasticsearch整体架构上采用了一主多副的方式:
每个Index由多个Shard组成,通常每个Shard有一个主节点和多个副本节点,副本个数可配。但每次写入时,写入请求会先根据_routing规则选择发给哪个Shard,Index Request中可以设置使用哪个值作为路由参数(如果没设置,则使用Mapping中的配置,如果mapping中也没有配,则使用_id作为路由参数)然后通过_routing的Hash值选择出Shard,最后从集群的Meta中找出该Shard的Primary节点。
请求接着会先发送给Primary Shard,在Primary Shard上执行成功后,再从Primary Shard上将请求同时发送给多个Replica Shard,请求在多个Replica Shard上执行成功并返回给Primary Shard后,写入请求才算执行成功,最后返回结果给客户端。
这种模式下,写入操作的延时latency = Latency(Primary Write) + Max(Replicas Write)。只要有副本在,写入延时最小也是两次单Shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失。
但是Elasticsearch为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证?
对于这种问题,Elasticsearch学习了数据库中的处理方式:增加CommitLog模块,Elasticsearch中叫TransLog。
在每一个Shard中,写入流程分为两部分,先写入Lucene,再写入TransLog。
Elasticsearch的读
Elasticsearch使用了Lucene作为搜索引擎库,可通过Lucene完成特定字段的搜索等功能。而对于分布式,数据写入的时候根据_routing规则将数据写入某一个Shard中,这样就能将海量数据分布在多个Shard以及多台机器上,也导致了查询的时候潜在数据会分散在当前index的所有Shard中,所以Elasticsearch查询的时候需要查询所有Shard,同一个Shard的Primary和Replica选择一个即可,查询请求会分发给所有Shard,每个Shard中都是一个独立的查询引擎,比如需要返回Top 10的结果,那么每个Shard就会查询并且返回Top 10的结果,然后在Client Node里面会接收所有Shard的结果,然后通过优先级队列二次排序,再挑出最终的Top 10,返回给用户。
Elasticsearch中的查询主要分为两类,Get请求:通过ID查询特定Doc;Search请求:通过Query查询匹配Doc。