目录

Elasticsearch 读写总结

我们整个日志系统是使用 EFK 这套开源方案来搭建的,在以前的文章中我们有提到过。在这个体系中,最关键的组件当属 Elasticsearch, 它负责数据的存储和搜索。在这里我们总结一下 es 的读写原理。

首先我们需要了解一下文档和分片的关系

文档元数据

在 es 中我们把存储的一条日志(记录)称为文档,一个文档不仅仅包含它的数据 ,也包含元数据 —— 有关文档的信息。 三个必须的元数据元素如下:

  • _index 文档在哪存放,_index 应该是因共同的特性被分组到一起的文档集合。比如相同格式的日志我们会放到同一个 index 中
  • _type 文档表示的对象类别,type 可以理解成是 index 的子类,不同的 type 包含的字段可能不同
  • _id 文档唯一标识, id 是一个字符串, 当它和 _index 以及 _type 组合就可以唯一确定 Elasticsearch 中的一个文档。 当创建一个新的文档,要么提供自己的 _id ,要么让 Elasticsearch 帮我们生成。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "_index": "ingress-access-2019.09.39",
  "_type": "_doc",
  "_id": "IY6KfG0BRpn5jI2pyyVf",
  "_version": 1,
  "_score": null,
  "_source": {
    "input": {
      "type": "log"
    },
    ...
  }
}

倒排索引

传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值(这里指单词)的能力。倒排索引就是这样一种数据结构,倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

1
2
3
4
5
6
Term  | Doc 1 | Doc 2 | Doc 3 | ...
------------------------------------
brown |   X   |       |  X    | ...
fox   |   X   |   X   |  X    | ...
quick |   X   |   X   |       | ...
the   |   X   |       |  X    | ...

倒排索引相比特定词项出现过的文档列表,会包含更多其它信息。它会保存每一个词项出现过的文档总数, 在对应的文档中一个具体词项出现的总次数,词项在文档中的顺序,每个文档的长度,所有文档的平均长度,等等。这些统计信息允许 Elasticsearch 决定哪些词比其它词更重要,哪些文档比其它文档更重要。

分片

一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点可能没有这样大的磁盘空间来存储或者单个节点处理搜索请求,响应会太慢。为了解决这个问题,Elasticsearch 提供了将索引划分成多片的能力,这些片叫做分片。当创建一个索引的时候,可以指定你分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引” 可以被放置到集群中的任何节点上。 分片之所以重要,主要有两方面的原因:

  • 允许水平分割/扩展容量
  • 允许并行操作,提高了性能/吞吐量

文档路由到分片

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢? 首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的: shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。 这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量, 因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

搜索过程

查询阶段

查询阶段包含以下三个步骤:

  1. 客户端发送一个 search 请求到 Node 3 , Node 3 会创建一个大小为 from + size 的空优先队列。
  2. Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
  3. 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。 当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。

第一步是广播请求到索引中每一个节点的分片拷贝,查询请求可以被某个主分片或某个副本分片处理, 这就是为什么更多的副本(当结合更多的硬件)能够增加搜索吞吐率。 协调节点将在之后的请求中轮询所有的分片拷贝来分摊负载。
每个分片在本地执行查询请求并且创建一个长度为 from + size 的优先队列—也就是说,每个分片创建的结果集足够大,均可以满足全局的搜索请求。 分片返回一个轻量级的结果列表到协调节点,它仅包含文档 ID 集合以及任何排序需要用到的值,例如 _score 。
协调节点将这些分片级的结果合并到自己的有序优先队列里,它代表了全局排序结果集合。至此查询过程结束。
query-doc

取回阶段

取回阶段由以下步骤构成:

  1. 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。
  2. 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节 点。
  3. 一旦所有的文档都被取回了,协调节点返回结果给客户端。
    协调节点首先决定哪些文档 确实 需要被取回。例如,如果我们的查询指定了 { “from”: 90, “size”: 10 } ,最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至全部分片。

协调节点会给持有相关文档的每个分片发送请求。分片加载文档体– _source 字段,如果有需要,用元数据丰富结果文档。 一旦协调节点接收到所有的结果文档,它就组装这些结果为单个响应返回给客户端。
retrieve

写入过程

主流程

  1. 客户端向 Node 1 发送新建、索引或者删除请求。
  2. 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3(因为分片 0 的主分片目前被分配在 Node 3 上)。
  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
    在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。 write

详细过程

  1. P0 收到 document, 同时将数据写入到 内存 buffer 和 translog 中
  2. 每隔 1s 或 buffer 满时, buffer 中的数据会 refresh 到 segment 中, 而后进入os cache, 一旦 segment进入到 cache 中,其中的数据, 则可以被搜索到
    refresh 时间可以手动设置, 也可以手动触发 refresh
  3. 清空buffer, translog不处理
  4. 随着 translog 不断增大, translog 每隔30分钟,或大到一定量时, 会触发commit操作
    • 将 buffer 中内容刷新到 segment中, 并清空buffer
    • 将一个commit point 写入到磁盘文件中, 标识此次commit 对应的 segment
    • 执行 fsync 将 os cache 中的数据强制刷新到磁盘文件中
    • 删除 translog 文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
|------------------||-------------------|
| buffer in memory || translog in cache |   
|------------------||-------------------|      
                   ||
                   || refresh
                   \/
|------------------||------------------|
| segment in cache || translog in file |   
|------------------||------------------|
                   ||
                   || flush
                   \/
|------------------||-------------------|
| segment in disk  || del old translog  |   
|------------------||-------------------|
  • 服务宕机重启, translog 日志作用
    translog 是先写入到 os cache 中, 然后每隔5s写入到磁盘文件中, 假如服务宕掉, 可能会失去5s数据, 也可以修改写入磁盘的时机, 但是可能会影响性能
    translog 中记录的是数据操作信息, 在服务宕机重启时, 会读取 translog 磁盘文件, 然后将 translog 中的数据重新恢复到 segment中, 然后进行后续操作
  • segment merge 过程
    segment 持续生成, 会导致 segment 不断变多, 占用文件句柄, cpu 资源等等
    es 后台有一个专门的程序负责合并 segment, 将小的 segment 合成大的 segment, 同时写一个 commit point, 标识 新的 segment file.
    打开新的 segment 供查询使用, 删除旧的 segment, segment 合并过程中, 被标记位 deleted 的 document 不会被合并. 即: 在合并 segment时, 才将 document 真正物理删除
    合并的 segment 可以使磁盘上已经 commit 的索引 也可以是内存中还未 commit 的索引。