前面两篇文章 Elasticsearch 入门学习 和 Elasticsearch 搜索的高级功能学习 中介绍了 Elasticsearch 的基本知识点以及相关搜索功能,这篇文章对 Elasticsearch 分布式原理以及在分布式下是如何进行文档的查询和更新相关逻辑学习总结:
Elasticsearch 支持集群部署,一个集群下可以部署多个节点,每个节点就是一个 ES 的实例(一个 JAVA 进程),可以通过启动参数 cluster.name 和 node.name 来分别指定集群和节点的名称:
bin/elasticsearch -E node.name=node1 -E cluster.name=myEs -d
bin/elasticsearch -E node.name=node2 -E cluster.name=myEs -d
bin/elasticsearch -E node.name=node3 -E cluster.name=myEs -d
Elasticsearch 分布式集群部署主要有以下优点:
一个集群下面针对不同的功能划分成不同类型的节点:
- 不能单点部署,为一个集群分配多个 Master Node
- 每个 Master Node 只承担一个角色
- 所以的节点信息
- 所有的索引和其 Mapping 和 Setting 的信息
- 分片路由信息
- 只有 Master Node 可以更新 Cluster State,并负责同步到其它 Node
节点类型 | 配置参数 | 默认值 |
---|---|---|
master eligble | node.master | true |
data | node.data | true |
ingest | node.ingest | true |
coordinating only | 无 | 上面三个参数全部为false |
machine learning | node.ml | true(需要 enable x-pack) |
- 节点之间可能会存在大量的空闲 TCP 连接
- 如果这些链接被关闭可能会破坏集群的正常运行
- 尽量保证开启了 tcp.keep_alive 选项且使该 keepalive 间隔时间小于任何其它可能使连接关闭的 timeout
- 如果 keepalives 无法配置,可以使用 transport.ping_schedule 进行连接检查
PUT _cluster/settings
{
"transient" : {
"logger.org.elasticsearch.transport.TransportService.tracer" : "TRACE"
}
}
- 通过参数 transport.compress 可以开启请求的压缩,默认关闭的
- 请求压缩会造成 CPU 的消耗,如果对于网络传输性能高的集群不建议开启
节点发现逻辑用于处理一个新节点如何正确的加入到集群中,当一个节点启动后可以通过 seed hosts providers 得到用于节点发现的种子(seed addresses)。seed addresses 可能是一个 IP,或者是 IP + PORT 或者是一个 HOST 的组合,对应的是集群中的一些节点的 Transport 通信地址。
ES 提供了两种 seed hosts providers,可以通过启动参数 discovery.seed_providers 进行设置,默认是 “settings-based”:
- discovery.seed_providers:
- settings-based
- discovery.seed_hosts:
- 192.168.1.10:9300 # ip + port
- 192.168.1.11 # ip
- seeds.mydomain.com # host
// 配置信息存储在文件 $ES_PATH_CONF/unicast_hosts.txt 中
// unicast_hosts.txt 一旦有改动,ES 都会监听到,并实时获取到新的 hosts
- discovery.seed_providers: file
当一个刚刚启动的节点通过 seed hosts providers 拿到 seed addresses 后,通过以下步骤进行集群发现并加入集群:
下面是集群发现机制的一些参数配置:
- transport.profiles.default.port:如果 seed address 给了 IP 但是没有提供端口那么就使用该配置的端口
- transport.port:如果 seed address 给了 IP 没有提供端口且 transport.profiles.default.port 也没有设置,那么就使用该配置的端口
- discovery.seed_resolver.timeout:每次查询试探的超时时间
- discovery.seed_resolver.max_concurrent_resolvers:同时探测的 seed addresss 个数
- 当你启动一个 Elasticsearch 节点时,该节点需要找到集群中的其它节点
- 当一个 Master 节点发生故障时,节点之间需要通过选举的方式找到新的 Master 节点
- 选主流程出现当一个新节点启动或者已经存在的 Master Node 出现故障时
- 任何一个 Master-Eligible Node 可以开启一个选举,一般情况下第一个开始选举的 Master-Eligible Node 会成为 Master Node
- 每个 Master-Eligible Node 节点选举的时间点是随机安排的,以防出现多个节点同一时间点进行选举的情况
- 如果同一时间点出现两个 Master-Eligible Node 进行选举,那么选举将会失败,后续会进行选举重试
- 什么是脑裂问题?
脑裂问题是指在分布式系统中当网络出现问题时,一个节点无法与其它节点建立连接时,这个节点会自己作为 Master Node 组成一个集群,而其它节点同时也会组成一个新的集群,这时会出现两个 Master Node,并且维护不同的 Cluster State,导致当网络恢复时,无法正确恢复数据
- 如何避免脑裂问题
可以通过限制选举条件 discovery.zen.minimum_master_nodes 的值,只有 Master Eligible Node 节点的个数大于 discovery.zen.minimum_master_nodes 时,才具备选举条件,可以有效避免脑裂问题
比如当有三个 Master Eligible Node 时,可以设置 discovery.zen.minimum_master_nodes = 2,即可避免脑裂问题,但是当只有 2 个 Master Eligible Node 时是无法避免脑裂问题的
从 ES7.0 以后,为了避免 discovery.zen.minimum_master_nodes 参数设置错误,ES 移除了该参数的设置,让 ES 自己选择可以形成仲裁的节点数
PUT users
{
"settings": {
"number_of_shards": 3, //设置主分片数
"number_of_replicas": 1 //设置副分片数
}
}
可以通过 API:GET /_cluster/health 进行集群的监控检查
GET /_cluster/health
返回结果 =>
{
"cluster_name" : "es_learning",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 2,
"number_of_data_nodes" : 2,
"active_primary_shards" : 6,
"active_shards" : 12,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}
shard = hash(_routing) % number_of_primary_shards
//插入数据时,通过 routing 参数指定 _routing
PUT users/_doc/100?routing=china
{
"title": "china"
}
1. 更新文档的请求先发送到某一个 Coordinating Node 上
2. Coordinating Node 根据 hash 算法计算该分配哪个分片上
3. 在对应的分片上先删除文档再新建文档
- indices.memory.index_buffer_size:设置 Index Buffer 的大小,可以是百分比相对 JVM 的值,也可以是一个绝对值
- indices.memory.min_index_buffer_size:如果 index_buffer_size 是相对百分比设置,那么这个可以控制 index_buffer_size 的下限,默认 48 mb
- indices.memory.max_index_buffer_size: 如果 index_buffer_size 是相对百分比设置,那么这个可以控制 index_buffer_size 的上限,默认无限大
ES 查询分为两个阶段: Query 和 Fetch,假设我们进行分页查询从 FROM 到 FROM + SIZE 的文档信息,下面是查询流程:
- 在数据量不大的情况下,可以将分片个数设置为 1,这样只在一个分片上查询就不会出现相关性算分偏离了
- DFS Query Then Fectch:可以在查询时加入 search_type=dfs_query_then_fetch 可以使每个分片把各个分片的词频和文档频率进行搜集,然后在 Coordinating Node 再进行一次相关性算分,这种情况耗费更多的 CPU 和内存,非必要情况不建议使用
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
// 不支持指定 From,必须指定 sort,并且要保证排序的唯一性,可以加入 _id 保证唯一性
// 使用 search_after 指定的 sort 对应的字段值,他替代了 From 的值,表示大于 search_after 的值后面 Size 个数据
// 这样在每个分片上每次只需要获取 Size 个文档就可以了,而不需要获取 From + Size 个文档,减少内存占用和 CPU 消耗
// 我们可以使用上一次获取到的最后一个文档进行迭代查询
"search_after": [10, "ZQ0vYGsBrR8X3IP75QqX"],
"sort": [
{"age": "desc"},
{"_id": "asc"}
]
}
// 和 search_after 类似,每次查询时,指定上一次查询到的 scroll_id
// Scroll API 会创建一个快照进行查询,如果有新的数据写入后,新数据将无法被查到
// Scroll API 一般用于需要全部文档,例如导出全部数据之类的
POST /_search/scroll
{
"scroll" : "1m", // 快照的存活时间
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}
ElasticSearch 在查询时支持对多个字段进行排序查询:
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {
}
},
"sort": [
{"order_date": {"order": "desc"}},
{"_doc":{"order": "asc"}},
{"_score":{ "order": "desc"}}
]
}
对比属性 | Doc Values | Field Data |
---|---|---|
何时创建 | 创建文档时和倒排索引一起创建 | 搜索时候动态创建 |
创建位置 | 磁盘文件 | JVM Heap |
优点 | 避免大量内存占用 | 索引速度快,不占用磁盘空间 |
缺点 | 降低索引速度,占用额外磁盘文件 | 动态创建开销大,占用过多的 JVM Heap |
默认值 | ES 2.x 以后 | ES 1.x 之前 |
//打开 text 类型的字段的 Field Data 排序方法
PUT kibana_sample_data_ecommerce/_mapping
{
"properties": {
"customer_full_name" : {
"type" : "text",
"fielddata": true
}
}
}
PUT test_keyword/_mapping
{
"properties": {
"user_name":{
"type": "keyword",
"doc_values":false //将 doc_values 排序方法关掉
}
}
}
同时对文档进行变更可能会导致数据丢失的问题,必须采用锁机制来控制对同一个资源的操作,ES 采用的乐观并发控制逻辑,由应用程序告知当前的文档的版本信息,如果版本信息不匹配,ES 将会报错,由用户来决定如何解决冲突,比如重试更新,或者将报错报告给用户等。
当我们更新获取获取某个文档的信息时,会返回该文档对应的 _seq_no 和 _primary_term
GET report/_doc/1
=> 返回结果:
{
"_index" : "report",
"_type" : "_doc",
"_id" : "1",
"_version" : 5,
"_seq_no" : 4, // 版本控制信息
"_primary_term" : 1, // 版本控制信息
"found" : true,
"_source" : {
"name" : "未命名报告",
"id" : 1,
"setting" : {
"data" : "helloworld"
},
"projectId" : 1
}
}
当我们对该文档进行更新时,可以带上参数 if_seq_no 和 if_primary_term 两个参数,用来判断版本是否匹配,如果版本不匹配,系统会抛出错误
// 如果不匹配会报错:version_conflict_engine_exception
PUT report/_doc/1?if_seq_no=4&if_primary_term=1
{
"name": "未命名报告",
"id": 1,
"projectId": 1
}