最近开始大面积使用 ES, 很多地方都是知其然不知其所以然, 特地翻看了很多资料和大牛的文档, 简单汇总一篇内容多为摘抄, 说是深入其实也是一点浅尝辄止的理解希望大家领会精神
首先学习要从官方开始地址如下
索引(Index)
ES 将数据存储于一个或多个索引中, 索引是具有类似特性的文档的集合类比传统的关系型数据库领域来说, 索引相当于 SQL 中的一个数据库, 或者一个数据存储方案 (schema) 索引由其名称 (必须为全小写字符) 进行标识, 并通过引用此名称完成文档的创建搜索更新及删除操作一个 ES 集群中可以按需创建任意数目的索引
如果不懂这块可以看我的写的上一篇入门的内容
http://www.cnblogs.com/wenBlog/p/8482326.html
我们了解索引的写操作后可知, 更新索引删除文档都是写操作, 这些操作必须在 primary shard 完全成功后才能拷贝至其对应的 replicas 上, 默认情况下主分片等待所有备份完成索引后才返回客户端
步骤:
客户端向 Node1 发送索引文档请求
Node1 根据文档 ID(_id 字段)计算出该文档应该属于 shard0, 然后请求路由到 Node3 的 P0 分片上
Node3 在 P0 上执行了请求如果请求成功, 则将请求并行的路由至 Node1,Node2 的 R0 上当所有的 Replicas 报告成功后, Node3 向请求的 Node(Node1)发送成功报告, Node1 再报告至 Client
当客户端收到执行成功后, 操作已经在 Primary shard 和所有的 replica shards 上执行成功了
读操作
一个文档可以在 primary shard 和所有的 replica shard 上读取见 Figure10
读操作步骤:
1. 客户端发送 Get 请求到 NODE1
2.NODE1 使用文档的_id 决定文档属于 shard 0.shard 0 的所有拷贝存在于所有 3 个节点上这次, 它将请求路由至 NODE2
3.NODE2 将文档返回给 NODE1,NODE1 将文档返回给客户端 对于读请求, 请求节点 (NODE1) 将在每次请求到来时都选择一个不同的 replica
shard 来达到负载均衡使用轮询策略轮询所有的 replica shards
更新操作
更新操作, 结合了以上的两个操作: 读写见 Figure11
步骤:
1. 客户端发送更新操作请求至 NODE1
2.NODE1 将请求路由至 NODE3,Primary shard 所在的位置
3.NODE3 从 P0 读取文档, 改变 source 字段的 JSON 内容, 然后试图重新对修改后的数据在 P0 做索引如果此时这个文档已经被其他的进程修改了, 那么它将重新执行 3 步骤, 这个过程如果超过了 retryon_conflict 设置的次数, 就放弃
4. 如果 NODE3 成功更新了文档, 它将并行的将新版本的文档同步到 NODE1 和 NODE2 的 replica shards 重新建立索引一旦所有的 replica
shards 报告成功, NODE3 向被请求的节点 (NODE1) 返回成功, 然后 NODE1 向客户端返回成功
2.6 SHARD
本节将解决以下问题:
为什么搜索是实时的
为什么文档的 CRUD 操作是实时的
ES 怎么保障你的更新在宕机的时候不会丢失
为什么删除文档不会立即释放空间
2.6.1 不变性
写到磁盘的倒序索引是不变的: 自从写到磁盘就再也不变 这会有很多好处:
不需要添加锁如果你从来不用更新索引, 那么你就不用担心多个进程在同一时间改变索引
一旦索引被内核的文件系统做了 Cache, 它就会待在那因为它不会改变只要内核有足够的缓冲空间, 绝大多数的读操作会直接从内存而不需要经过磁盘这大大提升了性能
其他的缓存 (例如 fiter cache) 在索引的生命周期内保持有效, 它们不需要每次数据修改时重构, 因为数据不变
写一个单一的大的倒序索引可以让数据压缩, 减少了磁盘 I/O 的消耗以及缓存索引所需的 RAM
当然, 索引的不变性也有缺点如果你想让新修改过的文档可以被搜索到, 你必须重新构建整个索引这在一个 index 可以容纳的数据量和一个索引可以更新的频率上都是一个限制
2.6.2 动态更新索引
如何在不丢失不变形的好处下让倒序索引可以更改? 答案是: 使用不只一个的索引 新添额外的索引来反映新的更改来替代重写所有倒序索引的方案 Lucene 引进了 per-segment 搜索的概念一个 segment 是一个完整的倒序索引的子集, 所以现在 index 在 Lucene 中的含义就是一个 segments 的集合, 每个 segment 都包含一些提交点 (commit point) 见 Figure16 新的文档建立时首先在内存建立索引 buffer, 见 Figure17 然后再被写入到磁盘的 segment, 见 Figure18
一个 per-segment 的工作流程如下:
1. 新的文档在内存中组织, 见 Figure17
2. 每隔一段时间, buffer 将会被提交: 一个新的 segment(一个额外的新的倒序索引)将被写到磁盘 一个新的提交点 (commit point) 被写入磁盘, 将包含新的 segment 的名称 磁盘 fsync, 所有在内核文件系统中的数据等待被写入到磁盘, 来保障它们被物理写入
3. 新的 segment 被打开, 使它包含的文档可以被索引
4. 内存中的 buffer 将被清理, 准备接收新的文档
当一个新的请求来时, 会遍历所有的 segments 词条分析程序会聚合所有的 segments 来保障每个文档和词条相关性的准确通过这种方式, 新的文档轻量的可以被添加到对应的索引中
删除和更新
segments 是不变的, 所以文档不能从旧的 segments 中删除, 也不能在旧的 segments 中更新来映射一个新的文档版本取之的是, 每一个提交点都会包含一个. del 文件, 列举了哪一个 segmen 的哪一个文档已经被删除了 当一个文档被删除了, 它仅仅是在. del 文件里被标记了一下被删除的文档依旧可以被索引到, 但是它将会在最终结果返回时被移除掉
文档的更新同理: 当文档更新时, 旧版本的文档将会被标记为删除, 新版本的文档在新的 segment 中建立索引也许新旧版本的文档都会本检索到, 但是旧版本的文档会在最终结果返回时被移除
2.6.3 实时索引
在上述的 per-segment 搜索的机制下, 新的文档会在分钟级内被索引, 但是还不够快 瓶颈在磁盘将新的 segment 提交到磁盘需要 fsync 来保障物理写入但是 fsync 是很耗时的它不能在每次文档更新时就被调用, 否则性能会很低 现在需要一种轻便的方式能使新的文档可以被索引, 这就意味着不能使用 fsync 来保障 在 ES 和物理磁盘之间是内核的文件系统缓存之前的描述中, Figure19,Figure20, 在内存中索引的文档会被写入到一个新的 segment 但是现在我们将 segment 首先写入到内核的文件系统缓存, 这个过程很轻量, 然后再 flush 到磁盘, 这个过程很耗时但是一旦一个 segment 文件在内核的缓存中, 它可以被打开被读取
2.6.4 更新持久化
不使用 fsync 将数据 flush 到磁盘, 我们不能保障在断电后或者进程死掉后数据不丢失 ES 是可靠的, 它可以保障数据被持久化到磁盘 在 2.6.2 中, 一个完全的提交会将 segments 写入到磁盘, 并且写一个提交点, 列出所有已知的 segments 当 ES 启动或者重新打开一个 index 时, 它会利用这个提交点来决定哪些 segments 属于当前的 shard 如果在提交点时, 文档被修改会怎么样? 不希望丢失这些修改:
1. 当一个文档被索引时, 它会被添加到 in-memory buffer, 并且添加到 Translog 日志中, 见 Figure21.
2.refresh 操作会让 shard 处于 Figure22 的状态: 每秒中, shard 都会被 refreshed:
在 in-memory buffer 中的文档会被写入到一个新的 segment, 但没有 fsync
in-memory buffer 被清空
3. 这个过程将会持续进行: 新的文档将被添加到 in-memory buffer 和 translog 日志中, 见 Figure23
4. 一段时间后, 当 translog 变得非常大时, 索引将会被 flush, 新的 translog 将会建立, 一个完全的提交进行完毕见 Figure24
在 in-memory 中的所有文档将被写入到新的 segment
内核文件系统会被 fsync 到磁盘
旧的 translog 日志被删除
translog 日志提供了一个所有还未被 flush 到磁盘的操作的持久化记录当 ES 启动的时候, 它会使用最新的 commit point 从磁盘恢复所有已有的 segments, 然后将重现所有在 translog 里面的操作来添加更新, 这些更新发生在最新的一次 commit 的记录之后还未被 fsync
translog 日志也可以用来提供实时的 CRUD 当你试图通过文档 ID 来读取更新删除一个文档时, 它会首先检查 translog 日志看看有没有最新的更新, 然后再从响应的 segment 中获得文档这意味着它每次都会对最新版本的文档做操作, 并且是实时的
2.6.5 Segment 合并
通过每隔一秒的自动刷新机制会创建一个新的 segment, 用不了多久就会有很多的 segmentsegment 会消耗系统的文件句柄, 内存, CPU 时钟最重要的是, 每一次请求都会依次检查所有的 segmentsegment 越多, 检索就会越慢
ES 通过在后台 merge 这些 segment 的方式解决这个问题小的 segment merge 到大的, 大的 merge 到更大的
这个过程也是那些被删除的文档真正被清除出文件系统的过程, 因为被标记为删除的文档不会被拷贝到大的 segment 中
合并过程如 Figure25:
1. 当在建立索引过程中, refresh 进程会创建新的 segments 然后打开他们以供索引
2.merge 进程会选择一些小的 segments 然后 merge 到一个大的 segment 中这个过程不会打断检索和创建索引
3.Figure26, 一旦 merge 完成, 旧的 segments 将被删除
新的 segment 被 flush 到磁盘
一个新的提交点被写入, 包括新的 segment, 排除旧的小的 segments
新的 segment 打开以供索引
旧的 segments 被删除
merge 大的 segments 会消耗大量的 I/O 和 CPU, 严重影响索引性能默认, ES 会节制 merge 过程来给留下足够多的系统资源
近实时搜索, 段数据刷新, 数据可见性更新和事务日志
理想的搜索解决方案是这样的: 新的数据一添加到索引中立马就能搜索到第一眼看上去, 这不正是 ElasticSearch 的工作方式吗, 即使是多服务器环境也是如此但是真实情况不是这样的(至少现在不是), 后面会讲到为什么它是似是而非首先, 我们往新创建的索引中添加一个新的文档, 命令如下:
curl -XPOST localhost:9200/test/test/1 -d '{"title":"test"}'
接下来, 我们在替换文档的同时查找该文档我们用如下的链式命令来实现这一点:
- curl -XPOST localhost:9200/test/test/1 -d '{"title":"test2"}' ; curl
- localhost:9200/test/test/_search?pretty
上面命令的结果类似如下:
- {"ok":true,"_index":"test","_type":"test","_id":"1","_version":2}
- {
- "took" : 1,
- "timed_out" : false,
- "_shards" : {
- "total" : 5,
- "successful" : 5,
- "failed" : 0
- },
- "hits" : {
- "total" : 1,
- "max_score" : 1.0,
- "hits" : [ {
- "_index" : "test",
- "_type" : "test",
- "_id" : "1",
- "_score" : 1.0, "_source" : { "title": "test" }
- } ]
- }
- }
第一行是第一个命令, 即索引命令的返回结果可以看到, 数据更新成功因此, 第二个命令, 即查询命令查询到的文档 title 域值应该为 test2 但是, 可以看到结果并不如人所愿这背后发生了什么呢?
在揭开前一个问题的答案之前, 我们先退一步, 来了解底层的 Apache Lucene 工具包是如何让新添加的文档对搜索可见的
更新索引并且将改动提交
从 第 1 章 介绍 ElasticSearch 的 介绍 Apache Lucene 一节中, 我们已经了解到, 在索引过程中, 新添加的文档都是写入到段 (segments) 中每个段都是有着独立的索引结构, 这意味着查询与索引两个过程是可以并行存在的, 索引过程中, 系统会不定期创建新的段 Apache Lucene 通过在索引目录中创建新的 segments_N 文件来标识新的段段创建的过程就称为索引的提交 Lucene 可以一种安全的方式实现索引的提交我们可以确定段文件要么全部创建成功, 要么失败如果错误发生, 我们可以确保索引状态的一致性
回到我们的例子中, 第一条命令添加文档到索引中, 但是没有提交这就是它的工作方式然而, 索引数据的提交也不能保证数据是搜索可见的 Lucene 工具包使用一个名为 Searcher 的抽象类来读取索引索引提交操作完成后, Searcher 对象需要重新打开才能加载到新创建的索引段这整个过程称为更新出于性能的考虑, ElasticSearch 会将推迟开销巨大的更新操作, 默认情况下, 单个文档的添加并不会触发搜索器的更新, Searcher 对象会每秒更新一次这个频率已经比较高了, 但是在一些应用程序中, 需要更频繁的更新对面这个需求, 我们可以考虑使用其它的解决方案或者再次核实我们是否真的需要这样做如果确实需要, 那么可以使用 ElasticSearch API 强制更新比如, 上面的例子中, 我们可以执行如下的命令强制更新:
curl XGET localhost:9200/test/_refresh
如果在搜索前执行了上面的命令, 那么 ElasticSearch 就可以搜索到修改后的文档
修改 Searcher 对象默认的更新时间
Searcher 对象的默认更新时间可以通过使用
index.refresh_interval
参数来修改, 该参数无论是添加到 ElasticSearch 的配置文件中或者使用 update settings API 都可以生效例如:
- curl -XPUT localhost:9200/test/_settings -d '{"index": {"refresh_interval":"5m"
- }
- }'
上面的命令将使 Searcher 每 5 秒钟自动更新一次请记住在更新两个时间点之间添加到索引的数据对查询是不可见的
总结
本篇从索引的创建操作和原理等方面介绍了 ES 索引的一些内容, 很多都来自各位大神的总结经过使用 ES 越来越多开始作为数据库的辅助希望大家多多指点
来源: https://www.cnblogs.com/wenBlog/p/8489197.html