Elasticsearch document深度剖析
1. 针对Elasticsearch并发冲突问题,ES内部是如何解决的?
1)ES内部是线程异步并发修改的,是基于_version版本号进行乐观锁并发控制的;
2)若后修改的先到了,那么修改后版本发生变化,先修改的后到发现版本不一致就扔掉了,保证了数据的正确性;
3)primary shard与replica shard同步请求是多线程异步的;
2. 基于版本号的实际操作
1)PUT /index/type/id?version=1;
es中的数据和客户端的数据的版本号必须是一致的,才能修改;
2)基于external version 进行了乐观锁并发控制;
不使用es内部的版本号,使用自己维护的版本号进行并发控制
?version=2&version.type=external
此方法保证只要version比ES中version大,就可以完成修改;
3. partial update
POST /index/type/id/_update
{
"doc":{
要修改的数据。
}
}
和全量替换相比优点:
1)所有的查询,修改和写回操作都是发生在es的内部,避免了所在的网络数据传输的开销,大大提升了性能;
2)减少了查询和修改的时间间隔,可以减少并发冲突的情况;
partial update的实现原理:
和全量替换差不多:内部先获取document,将传过来的field更新到document的json中,将老的document标记为deleted,最后将修改后的新的document创建出来;
partial update的并发控制原理:
内部自动执行并发乐观锁的并发控制策略。
POST /index/type/id/_update?retry_on_conflict=5 使用了重试策略,再次去新的版本号在更新;
4. 批量查询
优点:减少网络请求的开销
1) GET /_mget
GET /_mget
{
"docs" : [
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : 1
},
{
"_index" : "test_index",
"_type" : "test_type",
"_id" : 2
}
]
}
2) 如果查询的document是一个index下的不同type种的话
GET /test_index/_mget
{
"docs" : [
{
"_type" : "test_type",
"_id" : 1
},
{
"_type" : "test_type",
"_id" : 2
}
]
}
3) 如果查询的数据都在同一个index下的同一个type下,最简单了
GET /test_index/test_type/_mget
{
"ids": [1, 2]
}
5. 批量的增删改 bulk
POST /_bulk
{ "delete": { "_index": "test_index", "_type": "test_type", "_id": "3" }}
{ "create": { "_index": "test_index", "_type": "test_type", "_id": "12" }}
{ "test_field": "test12" }
{ "index": { "_index": "test_index", "_type": "test_type", "_id": "2" }}
{ "test_field": "replaced test2" }
{ "update": { "_index": "test_index", "_type": "test_type", "_id": "1", "_retry_on_conflict" : 3} }
{ "doc" : {"test_field2" : "bulk test1"} }
(1)delete:删除一个文档,只要1个json串就可以了
(2)create:PUT /index/type/id/_create,强制创建
(3)index:普通的put操作,可以是创建文档,也可以是全量替换文档
(4)update:执行的partial update操作
bulk api对json的语法,有严格的要求,每个json串不能换行,只能放一行,同时一个json串和一个json串之间,必须有一个换行;
bulk操作中,任意一个操作失败,是不会影响其他的操作的,但是在返回结果里,会告诉你异常日志;
bulk size的最佳大小:
bulk request会加载到内存里,如果太大的话,性能反而会下降,因此需要反复尝试一个最佳的bulk size。一般从1000~5000条数据开始,尝试逐渐增加。
6. document的数据路由
路由算法:shard = hash(routing) % number_of_primary_shards
routing值,默认是_id,也可以手动指定,相同的routing值,每次过来,从hash函数中,产出的hash值一定是相同的;
可以手动指定put /index/type/id?routing=user_id ;以保证说,某一类document一定被路由到一个shard上去,那么在后续进行应用级别的负载均衡,以及提升批量读取的性能的时候,是很有帮助的
这也是 primary shard数量不可变的谜底;
7. 增删改的内部原理
1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点);
2)coordinating node,对document进行路由,将请求转发给对应的node(primary shard);
3)实际的node上的primary shard处理请求,然后将数据同步到replica node;
4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端;
8. 写一致性
增删改操作 put /index/type/id,都可以带上一个consistency参数 put /index/type/id?consistency=quorum
一致性策略:
one:要求我们这个写操作,只要有一个primary shard是active活跃可用的,就可以执行
all:要求我们这个写操作,必须所有的primary shard和replica shard都是活跃的,才可以执行这个写操作
quorum:默认的值,要求所有的shard中,必须是大部分的shard都是活跃的,可用的,才可以执行这个写操作
quorum机制,写之前必须确保大多数shard都可用,int( (primary + number_of_replicas) / 2 ) + 1,当number_of_replicas>1时才生效
quroum = int( (primary + number_of_replicas) / 2 ) + 1
如果节点数少于quorum数量,可能导致quorum不齐全,进而导致无法执行任何写操作,es提供了一种特殊的处理场景,就是说当number_of_replicas>1时才生效;
quorum不齐全时,会进行wait,默认1分钟,自己可以设置timeout的时间;
(注意:一个primary shard 的多个replica shard也不能在同一个node上)
9. document 查询的内部原理
1)客户端发送请求到任意一个node,成为coordinate node;
2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡;
3)接收请求的node返回document给coordinate node;
4)coordinate node返回document给客户端;
5)特殊情况:document如果还在建立索引过程中,可能只有primary shard有,任何一个replica shard都没有,此时可能会导致无法读取到document,但是document完成索引建立之后,primary shard和replica shard就都有了。
10._bulk api 的奇特JSON格式和性能优化的关系
如果采用比较良好的json数组格式,允许任意的换行,整个可读性非常棒,读起来很爽,es要按照下述流程去进行处理:
(1)将json数组解析为JSONArray对象,这个时候,整个数据,就会在内存中出现一份一模一样的拷贝,一份数据是json文本,一份数据是JSONArray对象;
(2)解析json数组里的每个json,对每个请求中的document进行路由;
(3)为路由到同一个shard上的多个请求,创建一个请求数组;
(4)将这个请求数组序列化;
(5)将序列化后的请求数组发送到对应的节点上去;
耗费更多内存,更多的jvm gc开销;
奇特的格式:
{"action": {"meta"}}\n
{"data"}\n
(1)不用将其转换为json对象,不会出现内存中的相同数据的拷贝,直接按照换行符切割json;
(2)对每两个一组的json,读取meta,进行document路由;
(3)直接将对应的json发送到node上去;
最大的优势在于,不需要将json数组解析为一个JSONArray对象,形成一份大数据的拷贝,浪费内存空间,尽可能地保证性能;