Elasticsearch 是一个开源的分布式搜索与分析引擎,提供了近实时搜索和聚合两大功能,内部使用 Lucene 做索引与搜索,虽然 Lucene 提供的功能己经很强大了,但 Lucene 只是一个由 Java 语言编写的库,存在语言限制,并且只提供建立索引、执行搜索等接口,不包含分布式服务,而 Elasticsearch 在 Lucene 的基础上提供了多种语言接口,并且更加专注于企业应用。
如今 Elasticsearch 已经作为一家公司(Elastic 公司)进行运作,定位为数据搜索和分析平台,并且积极为所有流行的语言和框架编写正式的客户端驱动,现在 Elasticsearch 已经可以与 Java、Ruby、Python、PHP 等多种客户端集成。此外也可以与 Hadoop、Spark 等大数据分析平台集成,功能十分强大。
基于 Elasticsearch 衍生出来了一系列开源软件,主要包括日志采集与解析工具 Logstash、可视化分析平台 Kibana、数据采集工具 Beats 家族等。Logstash 既可以作为日志搜集器又能解析日志,但 Logstash 会消耗较多的 CPU 和内存资源,容易造成服务器性能下降 。后来 Elastic 公司推出了 Beats 家族,在数据收集方面使用 Beats 取代 Logstash,解决了 Logstash 在各服务器节点上占用系统资源高的问题。另外 Beats 和 Logstash 之间支持 SSL/TLS 加密传输以及客户端和服务器的双向认证,保证了通信安全。
Beats 家族的 5 个成员简介如下:
- Filebeat:轻量级的日志采集器,可用于收集文件数据。
- Metricbeat:5.0 版本之前名为 Topbeat,搜集系统、进程和文件系统级别的 CPU 和内存使用情况。
- Packetbeat:收集网络流数据,可以实时监控系统应用和服务,可以将延迟时间、错误、响应时间、SLA 性能等信息发送到 Logstash 或 Elasticsearch。
- Winlogbeat:搜集 Windows 事件日志数据。
- Heartbeat:监控服务器运行状态。
为了避免版本混乱,从 5.0 版本开始,Elastic 公司将各组件的版本号统一。使用时,各组件版本号应该一致,Elastic Stack 的版本号格式为 X.Y.Z。其中,各组件的 Z 可以不相同,但 X、Y 必须一致。
基本概念
1. 文档(Document)
Elasticsearch 是面向文档的,文档是可被搜索的基础信息单元;文档可以是一封邮件、一条日志或者一个网页的内容,一般使用 JSON 作为文档的序列化格式;文档可以有很多字段,每个字段都有对应的字段类型,字段的类型可以手动指定或通过 Elasticsearch 自动推算,支持数组、嵌套文档;每个文档都有一个唯一 ID,可以自己指定或通过 Elasticsearch 自动生成。
一个文档的元数据有如下几项,用来标注文档的相关信息:
- _index:文档所属的索引名
- _type:文档所属的类型名
- _id:文档唯一 ID,在存储结构上,由 _index、_type、_id 唯一标识一个文档
- _source:文档的原始 JSON 数据
- _all:整合所有字段内容到该字段,方便对所有文档进行检索,ES 7.0 版本已废除
- _version:文档在系统中的版本信息,适用于并发读写时解决文档冲突的问题
- _score:相关性打分,用于全文检索,表示该文档在某个查询下的算分
{
"_index": "movies",
"_type": "_doc",
"_id": "1",
"_version" : 1,
"_score": "14.321",
"_source":
{
"id":"1",
"year": "1995",
"title": "Toy Story"
}
}
2. 索引(Index)
索引是文档的容器,一个索引就是一个拥有几分相似特征的文档的集合。索引的数据结构是倒排索引。一个索引由一个唯一名称(必须全部是小写字母)来标识,并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。
- Index 是逻辑空间的概念:每个索引都有自己的 Mapping 定义,定义了文档的字段名和字段类型。
- Shard 是物理空间的概念:索引中的数据最后是会被分散在 Shard 分片上。
在一个索引中,可以为其设置 Mapping 和 Settings。Mapping 定义了该索引中所有文档的字段类型及字段的 Analyzer 的相关配置;Settings 则定义了索引要使用的分片数量及不同的数据分布。
什么是倒排索引?
倒排索引包含两个部分:
单词词典(Term Dictionary):记录所有文档的单词,记录单词到倒排列表的关联关系。单词词典一般比较大,Elasticsearch 为了能快速找到某个 Term,会先将所有 Term 排序然后进行二分查找,时间复杂度为 logN。
但如果 Term 太多,整个词典会很大,放内存不现实,于是有了 Term Index。可以把 Term Index 理解为是一棵树,这棵树包含的是 Term 的一些前缀。通过 Term Index 可以快速地定位到词典的某个位置,然后从这个位置再往后顺序查找。
倒排列表(Posting List):记录了单词对应的文档集合,由倒排索引项组成。倒排索引项包括以下几部分:
- 文档 ID
- 词频(TF):该单词在文档中出现的次数,用于相关性评分
- 位置(Position):单词在文档中分词的位置,用于语句搜索
- 偏移(Offset):记录单词的开始结束位置,实现高亮显示
下图为 Elasticsearch 这个单词对应的倒排列表示例:
Elasticsearch 中的每个文档是基于 JSON 格式的,JSON 文档中的每个字段都有自己的倒排索引。当然我们可以在 Mapping 文件中指定对某些字段不做索引,这样能节省存储空间,但这个字段也无法被用于搜索了。
3. 类型(Type)
类型用于区分同一个集合中的不同细分,在不同的细分中,数据的整体模式是相同或相似的,不适合完全不同类型的数据。多个类型可以在相同的索引中存在,只要它们的字段不冲突即可(对于整个索引,映射在本质上被 “扁平化” 成一个单一的、全局的模式)。一个类型是你的索引的一个逻辑上的分类或分区,其语义完全由你来决定。通常会为具有一组共同字段的文档定义一个类型。
很多初学者喜欢套用 RDBMS 中的概念,将 index 理解为数据库,将 type 理解为表,这是很牵强的理解,实际上这是完全不同的概念,在 Elasticsearch 索引中,不同映射类型中具有相同名称的字段在内部是由相同的 Lucene 字段支持。在实际应用中,数据模型不同,有不同 type 需求时,我们应建立单独的索引,而不是在一个索引下使用不同的 type,因为索引之间是完全独立的。
最重要的是,在同一个索引中存储很少或没有共同字段的不同实体会导致数据稀疏,并影响 Lucene 有效压缩文档的能力。由于这些原因,在 ES 6.x 版本中,一个索引中只允许存在一个 type,并且从 ES 7.0 版本后 type 的概念已经被废弃,一个索引只能创建一个 type,即 _doc。
参考链接:https://www.elastic.co/guide/en/elasticsearch/reference/7.0/removal-of-types.html
集群概念
1. 集群
一个或多个安装 Elasticsearch 的服务器节点组织在一起就是集群,它们共同持有你整个的数据,并一起提供索引和搜索功能。一个集群有一个唯一的名字标识,称为 cluster name,集群名称默认是 “elasticsearch”。集群名称非常重要,就像一个组织的名称,具有相同集群名称的节点才会组成一个集群。
2. 节点
节点是一个 Elasticsearch 的实例,本质上就是一个 JAVA 进程。每一个节点在集群内都有一个唯一名称,节点作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。
一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,只有在同一台机器上运行的节点才会自动加入到一个集群名称为 “elasticsearch” 的集群中,但有时这种机制并不可靠,会发生脑裂现象,往往不如在每一个节点上配置节点的名字在启动时进行被动发现来的安全稳定。
Elasticsearch 的集群模式采用的是主从(Master-Slave)模式,主从模式可以简化系统设计,Master 作为权威节点,部分操作仅由 Master 执行,如:创建、删除索引,并负责维护集群元信息。缺点是 Master 节点存在单点故障,需要解决灾备问题,并且集群规模会受限于 Master 节点的管理能力。
在 Elasticsearch 中的每个节点上都保存了集群的状态,但只有 Master 节点才能修改集群的状态信息。集群状态(Cluster State)维护了一个集群中必要的信息,主要包括下面几项:
- 所有节点的信息
- 所有的索引和其相关的 Mapping 与 Settings 信息
- 分片的路由信息,即哪个分片位于哪个节点
下面分别介绍 Elasticsearch 中各种类型节点的职能:
主节点(Master node)
负责集群层面的相关操作,比如创建索引、删除索引,管理集群变更等。集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。通过配置 node.master:true(默认)来使一个节点成为主节点,Active Master 是全局唯一的,将从有资格成为 Master 的节点中进行选举,默认是集群中第一个启动的节点。
主节点也可以作为数据节点,但应尽可能做少量工作,稳定的主节点对集群的健康是非常重要的,因此生产环境应尽量分离主节点和数据节点,创建独立主节点的配置。
node.master:true
node.data:false
数据节点(Data node)
负责保存数据、执行数据相关的 CRUD、搜索、聚合等操作。在数据扩展上起到了至关重要的作用。数据节点对 CPU、内存、I/O 要求较高。一般情况下数据读写流程只和数据节点交互,不会和主节点打交道。通过配置 node.data:true(默认)来使一个节点成为数据节点,也可以通过下面的配置创建一个数据节点:
node.master:false
node.data:true
node.ingest:false
预处理节点(Ingest node)
这是从 ES 5.0 版本开始引入的概念。预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列的 processors 和 pipeline 对数据进行某种处理。processors 和 pipeline 可以拦截 bulk 和 index 请求,在执行相关操作后再将文档传回给 index 或 bulkAPI。
默认情况下,在所有的节点上启用 ingest,如果想在某个节点上禁用 ingest,则可以配置 node.ingest:false,也可以通过下面的配置创建一个仅用于预处理的节点:
node.master:false
node.data:false
node.ingest:true
协调节点(Coordinating node)
客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。协调节点将请求转发给保存数据的数据节点。每个数据节点在本地执行请求,并将结果返回协调节点。协调节点收集完数据后,将每个数据节点的结果合并为单个全局结果。对结果收集和排序的过程可能需要很多 CPU 和内存资源。
可通过下面的配置创建一个仅用于协调的节点:
node.master:false
node.data:false
node.ingest:false
3. 分片
在分布式系统中,单机无法存储规模巨大的数据,要依靠大规模集群处理和存储这些数据,一般通过增加机器数量来提高系统水平扩展能力。因此,需要将数据分成若干小块分配到各个机器上。然后通过某种路由策略找到某个数据块所在的位置。Elasticsearch 提供了将索引划分成多份的能力,这些份就叫作分片。
分片是底层的基本读写单元,是数据的容器,文档保存在分片内,不会跨分片存储。当你创建一个索引时,可指定想要的分片数,每个分片本身也是一个功能完善且独立的 Lucene 索引,可以独立执行建立索引和搜索任务的工作,这个 Lucene 索引可被放置到集群中的任何节点上。
Lucene 索引又由很多 Segment 组成,每个 Segment 都是一个倒排索引。Elasticsearch 每次 refresh 都会生成一个新的 Segment,其中包含若干文档的数据。在每个 Segment 内部,文档的不同字段被单独建立索引。每个字段的值由若干词(Term)组成,Term 是原文本内容经过分词器处理和语言处理后的最终结果。
Segment 具有不变性,一旦索引的数据被写入硬盘就不可再修改,这种机制使它在读写时几乎完全避免了锁的开销,大大提升了读写性能。Segment 被写入到磁盘后会生成一个提交点,一个 Segment 一旦拥有了提交点,就说明这个 Segment 只有读的权限,失去了写的权限。相反,当 Segment 在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。
分片之所以重要,主要有以下两方面原因:
- 允许水平分割、扩展你的存储容量。
- 允许在分片上进行分布式的、并行的操作,提高性能和吞吐量,至于一个分片怎样分布,它的文档怎样聚合回搜索请求,完全由 Elasticsearch 管理,对用户是透明的。当集群扩缩容时,ES 会自动在各节点中迁移分片,使数据仍均匀分布在集群里。
索引建立的时候就需要确定好主分片数,如果分片数设置过小,会导致后续无法增加节点实现水平扩展;如果分片数设置过大,则单个节点上会存在过多的分片,导致资源浪费,同时也会影响性能。在 ES 5.x 版本之前,主分片数量不可以修改,副分片数是可以随时修改的。在 ES 5.x 版本之后,已经支持在一定条件的限制下,对某个索引的主分片进行拆分或缩小。但我们仍然需要在一开始就尽量规划好主分片数量。
在 ES 7.0 版本之后,默认的主分片数改为了 1,默认的副本数改为了 0。当分片数不够时,我们可以考虑新建索引,实际上,搜索 1 个有着 50 个分片的索引与搜索 50 个每个都有 1 个分片的索引是完全等价的,或者可以使用 _splitAPI 来拆分索引( ES 6.1 版本开始支持)。
4. 副本
为了应对复杂网络环境下的节点异常宕机,Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫作副本。Elasticsearch 会将分片复制多份并放置到不同的机器中,这样既可以增加系统可用性,同时数据副本还可以使读操作并发执行,分担集群压力。副本数可以在索引创建时指定,也可以在索引创建后动态地修改。
但是多数据副本也带来了一致性的问题:部分副本写入成功,部分副本写入失败。Elasticsearch 为了应对并发更新问题将副本分为主从两部分:主分片(primary shard)为复制源的原来的分片,副分片(replica shard)为主分片的拷贝。在写入过程中先写主分片,成功后再写副分片,恢复阶段以主分片为准。
副本之所以重要,有以下两个主要原因:
- 在分片、节点失败时保证高可用性。因此副本分片不与主分片置于同一节点上,这一点非常重要
- 扩展吞吐量,因为搜索可以在所有的副本上并行运行。
数据分片和数据副本的关系如下图所示:
- number_of_shards:主分片数,为 3 表示该索引数据会分布到集群中的三个节点
- number_of_replicas:副本个数,为 1 表示每一个主分片有一个副本分片
5. 集群健康状态
从数据完整性的角度划分,集群健康状态分为三种:
- Green:所有的主分片和副分片都正常运行。
- Yellow:所有的主分片都正常运行,但不是所有的副分片都正常运行。这意味着存在单点故障风险。
- Red:有主分片没能正常运行。
每个索引也有上述这三种状态,假设丢失了一个副分片,该分片所属的索引和整个集群会变为 Yellow 状态,其他索引仍为 Green 状态。
curl -X GET "http://localhost:9200/_cluster/health?level=shards&pretty"
6. 扩缩容
当扩容集群、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成的。分片副本实现了数据冗余,从而防止硬件故障导致的数据丢失。
下面演示了当集群只有一个节点,到变成两个节点、三个节点时的 shard 迁移过程示例。假设我们为索引设置的分片数为 3,副本数为 1。刚开始我们只有一个节点,此时三个主分片都在 NODE1 上,并且副本分片没有节点可以分配,如下图所示:
其中,P 代表 Primary shard,R 代表 Replica shard。当我们向集群中添加第二个节点后,副本分片被分配到了 NODE2 中,如下图所示。
当添加第三个节点后,索引的六个分片被平均分配到了集群中的这三个节点,如下图所示。
分片在分配过程中除了需要让分片在节点之间均匀存储,还要保证不把主分片和副分片分配到同一节点,避免单个节点故障引起数据丢失。分布式系统中难免出现故障,当节点异常时,ES 会自动处理节点异常。当主节点异常时,集群会重新选举主节点。当某个主分片异常时,会将副分片提升为主分片。