基于版本:2.3.2
这次分析的读流程指 GET/MGET 过程,不包含搜索过程。
GET/MGET 必须指定三元组: index type id。 type 可以使用 _all 表示从所有 type 获取第一个匹配 id 的 doc。
mget 时, type 留空表示 _all,例如可以这样:
GET 则必须明确指定 _all ,例如必须这样:
而不能
GET 流程
整体分为五个阶段:准备集群信息,内容路由,协调请求,数据读取,回复客户端。
在处理入口,根据action字符串获取对应的TransportAction实现类,对于一个单个doc的get请求,获取到的是一个
|
TransportSingleShardAction
TransportAction
<
Request
,
Response
>
transportAction
=
actions
.
get
(
action
)
;
|
一个 TransportSingleShardAction 对象用来处理存在于一个单个主分片或者副本分片上的读请求。
准备集群信息
1.在 TransportSingleShardAction 构造函数中,已准备好 clusterState、nodes 列表等信息
2.resolveRequest函数从ClusterState中获取IndexMetaData,更新可能存在的自定义routing信息
内容路由
确定目标节点,获取shard迭代器,其中包含了目的node信息
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private
AsyncSingleAction
(
Request
request
,
ActionListener
<
Response
>
listener
)
{
ClusterState
clusterState
=
clusterService
.
state
(
)
;
//集群nodes列表
nodes
=
clusterState
.
nodes
(
)
;
resolveRequest
(
clusterState
,
internalRequest
)
;
//根据hash和shard数量取余计算一个随机目的shard,或者走优先级规则
this
.
shardIt
=
shards
(
clusterState
,
internalRequest
)
;
}
|
作协调请求,向目标节点发送请求,处理响应,回复客户端,主要代码如下 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private
void
perform
(
@
Nullable
final
Throwable
currentFailure
)
{
DiscoveryNode
node
=
nodes
.
get
(
shardRouting
.
currentNodeId
(
)
)
;
if
(
node
==
null
)
{
onFailure
(
shardRouting
,
new
NoShardAvailableActionException
(
shardRouting
.
shardId
(
)
)
)
;
}
else
{
internalRequest
.
request
(
)
.
internalShardId
=
shardRouting
.
shardId
(
)
;
transportService
.
sendRequest
(
node
,
transportShardAction
,
internalRequest
.
request
(
)
,
new
BaseTransportResponseHandler
<
Response
>
(
)
{
//上面的 sendRequest 不管是发送到网络还是由本地节点直接处理的,下面的函数用于处理后续的响应操作
@
Override
public
void
handleResponse
(
final
Response
response
)
{
listener
.
onResponse
(
response
)
;
}
@
Override
public
void
handleException
(
TransportException
exp
)
{
onFailure
(
shardRouting
,
exp
)
;
}
}
)
;
}
}
|
代码入口:
rest请求接受和处理的类位于:
HttpRequestHandler::messageReceived
接收到请求后根据action获取handle,调用不同handle进行处理的的实现位于:RequestHandlerRegistry::processMessageReceived
单个shard读请求处理实现位于:
TransportSingleShardAction::messageReceived
index 读取的核心实现位于:
InternalEngine::get
内容路由,获取shardit过程
shardit 是一个List的迭代器,默认情况下是所有 activeShard 中随机选择的一个位置的迭代器,如果存在优先级参数会有些其他的过滤条件.ShardRouting类是对一个独一无二的 shard 相关的路由信息.因此这个环节就是要确定最终目的 shard 是哪个,他的相关节点是哪个。
调用OperationRouting::getShards实现
|
public
ShardIterator
getShards
(
ClusterState
clusterState
,
String
index
,
String
type
,
String
id
,
@
Nullable
String
routing
,
@
Nullable
String
preference
)
{
return
preferenceActiveShardIterator
(
shards
(
clusterState
,
index
,
type
,
id
,
routing
)
,
clusterState
.
nodes
(
)
.
localNodeId
(
)
,
clusterState
.
nodes
(
)
,
preference
)
;
}
|
1.计算 shardid,然后从路由表获取匹配index 和 shard 的activeShardsgenerateShardId()通过对id等进行hash,对主分片取余,获得目的shardid,然后获取 shardid 对应的内容路由表.期间,会检查索引是否存在,不存在则抛异常。
|
protected
IndexShardRoutingTable
shards
(
ClusterState
clusterState
,
String
index
,
String
type
,
String
id
,
String
routing
)
{
int
shardId
=
generateShardId
(
clusterState
,
index
,
type
,
id
,
routing
)
;
return
clusterState
.
getRoutingTable
(
)
.
shardRoutingTable
(
index
,
shardId
)
;
}
|
其中内容路由规则:
generateShardId中实现
|
shard
=
hash
(
routing
)
%
number_of_primary
_shards
|
routing 是一个可变值,默认是文档的 _id ,另外可以根据routing指定的值,或者同时参考id与type
读取的时候也是这样hash计算出的目的shard
2.从 activeShard s 中选择目标.调用OperationRouting::preferenceActiveShardIterator()实现后续流程。首先检查是否存在优先级:preference如果不存在,调用ctiveInitializingShardsRandomIt();从activeshards中返回一个随机的node,随机算法在CollectionUtils.rotate实现,只是用一个随机数对activeShards.size()取余如果请求中存在优先级设置,进入分片查询优先级判断逻辑,优先级算法只是将对 activeShard 的随机选择改成了按一定条件把某个shard 放到List 最前面,然后返回第一个.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
ShardIterator
preferenceActiveShardIterator
(
IndexShardRoutingTable
indexShard
,
String
localNodeId
,
DiscoveryNodes
nodes
,
@
Nullable
String
preference
)
{
if
(
preference
==
null
||
preference
.
isEmpty
(
)
)
{
String
[
]
awarenessAttributes
=
awarenessAllocationDecider
.
awarenessAttributes
(
)
;
if
(
awarenessAttributes
.
length
==
0
)
{
return
indexShard
.
activeInitializingShardsRandomIt
(
)
;
}
else
{
return
indexShard
.
preferAttributesActiveInitializingShardsIt
(
awarenessAttributes
,
nodes
)
;
}
}
if
(
preference
.
charAt
(
0
)
==
'_'
)
{
.
.
}
}
|
协调请求过程
本节点作为协调节点,向目标 node 转发请求,或者目标是本地节点,直接通过函数调用读取数据.发送流程封装了对请求的发送,并且声明了如何对 Response 进行处理:AsyncSingleAction 类中声明的对 Response 进行处理的函数,无论请求在本节点处理还是发送到其他节点,都会经过这个函数处理:
|
public
void
handleResponse
(
final
Response
response
)
{
listener
.
onResponse
(
response
)
;
}
|
最终调用到给客户端回复 Response ,在RestResponseListener类发送:
|
protected
final
void
processResponse
(
Response
response
)
throws
Exception
{
channel
.
sendResponse
(
buildResponse
(
response
)
)
;
}
|
下面看下具体过程:
1.TransportService::sendRequest中检查目标是否本地node
|
if
(
node
.
equals
(
localNode
)
)
{
sendLocalRequest
(
requestId
,
action
,
request
)
;
}
else
{
transport
.
sendRequest
(
node
,
requestId
,
action
,
request
,
options
)
;
}
|
2.如果是本地node,进入TransportService::sendLocalRequest流程sendLocalRequest不发送到网络,直接根据action获取注册的reg,执行processMessageReceived
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//sendRequest发现目标node是本地时(if (node.equals(localNode))),调用到本函数
private
void
sendLocalRequest
(
long
requestId
,
final
String
action
,
final
TransportRequest
request
)
{
final
DirectResponseChannel
channel
=
new
DirectResponseChannel
(
logger
,
localNode
,
action
,
requestId
,
adapter
,
threadPool
)
;
try
{
final
RequestHandlerRegistry
reg
=
adapter
.
getRequestHandler
(
action
)
;
final
String
executor
=
reg
.
getExecutor
(
)
;
if
(
ThreadPool
.
Names
.
SAME
.
equals
(
executor
)
)
{
//noinspection unchecked
reg
.
processMessageReceived
(
request
,
channel
)
;
}
.
.
.
.
.
.
.
}
|
3.进入数据读取流程
4.如果是发送到网络,请求被异步发送,sendRequest的时候注册 handle:
|
transportService
.
sendRequest
(
node
,
transportShardAction
,
internalRequest
.
request
(
)
,
new
BaseTransportResponseHandler
<
Response
>
(
)
|
在TransportService::sendRequest中,这个 handle 最终被 添加到
|
clientHandlers
:
clientHandlers
.
put
(
requestId
,
new
RequestHolder
<>
(
handler
,
node
,
action
,
timeoutHandler
)
)
;
|
然后,设置超时,等待处理 Response:
|
public
TransportResponseHandler
onResponseReceived
(
final
long
requestId
)
{
RequestHolder
holder
=
clientHandlers
.
remove
(
requestId
)
;
holder
.
cancelTimeout
(
)
;
if
(
traceEnabled
(
)
&&
shouldTraceAction
(
holder
.
action
(
)
)
)
{
traceReceivedResponse
(
requestId
,
holder
.
node
(
)
,
holder
.
action
(
)
)
;
}
return
holder
.
handler
(
)
;
}
|
收到其他节点的 Response 后,通过之前声明的handleResponse,给客户端返回响应内容.
本地节点数据读取和发送流程
RequestHandlerRegistry::processMessageReceived作为Request消息处理的总入口,根据action获取handle,调用对应的handler.messageReceived
进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//对于所有Request消息处理的入口
public
void
processMessageReceived
(
Request
request
,
TransportChannel
channel
)
throws
Exception
{
final
Task
task
=
taskManager
.
register
(
channel
.
getChannelType
(
)
,
action
,
request
)
;
if
(
task
==
null
)
{
handler
.
messageReceived
(
request
,
channel
)
;
}
else
{
boolean
success
=
false
;
try
{
handler
.
messageReceived
(
request
,
new
TransportChannelWrapper
(
taskManager
,
task
,
channel
)
,
task
)
;
success
=
true
;
}
finally
{
if
(
success
==
false
)
{
taskManager
.
unregister
(
task
)
;
}
}
}
}
|
对于单个shard的读请求,进入
|
TransportSingleShardAction
::
ShardTransportHandler
::
messageReceived
(
)
|
读取数据组织成 Response, 给客户端 channel 返回。
|
public
void
messageReceived
(
final
Request
request
,
final
TransportChannel
channel
)
throws
Exception
{
Response
response
=
shardOperation
(
request
,
request
.
internalShardId
)
;
channel
.
sendResponse
(
response
)
;
}
|
shardOperation主要处理请求中是否有 refresh 选项,然后调用indexShard.getService().get() 读取数据,存储到 GetResult.为什么需要在 realtime 未开启的状态下 refresh 选项才能生效呢?如果一个GET操作要求先刷新数据,以此实现实时读取,这意味着数据从 lucene 获取,不走 translog.那他确实没必要开启 realtime 选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
protected
GetResponse
shardOperation
(
GetRequest
request
,
ShardId
shardId
)
{
IndexService
indexService
=
indicesService
.
indexServiceSafe
(
shardId
.
getIndex
(
)
)
;
IndexShard
indexShard
=
indexService
.
shardSafe
(
shardId
.
id
(
)
)
;
if
(
request
.
refresh
(
)
&&
!
request
.
realtime
(
)
)
{
indexShard
.
refresh
(
"refresh_flag_get"
)
;
}
GetResult
result
=
indexShard
.
getService
(
)
.
get
(
request
.
type
(
)
,
request
.
id
(
)
,
request
.
fields
(
)
,
request
.
realtime
(
)
,
request
.
version
(
)
,
request
.
versionType
(
)
,
request
.
fetchSourceContext
(
)
,
request
.
ignoreErrorsOnGeneratedFields
(
)
)
;
return
new
GetResponse
(
result
)
;
}
|
ShardGetService::get()中,调用GetResult getResult = innerGet()获取到结果.
GetResult类用于存储读取到的真实数据内容.而 Engine::GetResult类封装的是响应的 lucene IndexSearch 和translog 等信息因此,核心的数据读取实现在ShardGetService::innerGet()函数中.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private
GetResult
innerGet
(
String
type
,
String
id
,
String
[
]
gFields
,
boolean
realtime
,
long
version
,
VersionType
versionType
,
FetchSourceContext
fetchSourceContext
,
boolean
ignoreErrorsOnGeneratedFields
)
{
fetchSourceContext
=
normalizeFetchSourceContent
(
fetchSourceContext
,
gFields
)
;
Engine
.
GetResult
get
=
null
;
if
(
type
==
null
||
type
.
equals
(
"_all"
)
)
{
.
.
.
}
else
{
get
=
indexShard
.
get
(
new
Engine
.
Get
(
realtime
,
new
Term
(
UidFieldMapper
.
NAME
,
Uid
.
createUidAsBytes
(
type
,
id
)
)
)
.
version
(
version
)
.
versionType
(
versionType
)
)
;
.
.
.
}
DocumentMapper
docMapper
=
mapperService
.
documentMapper
(
type
)
;
try
{
// break between having loaded it from translog (so we only have _source), and having a document to load
if
(
get
.
docIdAndVersion
(
)
!=
null
)
{
return
innerGetLoadFromStoredFields
(
type
,
id
,
gFields
,
fetchSourceContext
,
get
,
docMapper
,
ignoreErrorsOnGeneratedFields
)
;
}
.
.
.
}
}
|
1.首先,通过indexShard.get()获取Engine.GetResult,里面有重要的 lucene indexsearch,或者Translog.Source 等信息。
get()函数最终实现在InternalEngine::get()
先获取读锁:
|
try
(
ReleasableLock
lock
=
readLock
.
acquire
(
)
)
|
如果是 数据位于本机的最新数据(versionMap中存在),则从 translog 获取.非realtime通过 lucene 获取,如果指定了 version 且 version 不存在,读取失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public
GetResult
get
(
Get
get
)
throws
EngineException
{
try
(
ReleasableLock
lock
=
readLock
.
acquire
(
)
)
{
ensureOpen
(
)
;
if
(
get
.
realtime
(
)
)
{
//versionMap 中的值是写入索引的时候添加的,并且不做持久化.
VersionValue
versionValue
=
versionMap
.
getUnderLock
(
get
.
uid
(
)
.
bytes
(
)
)
;
//一般不进入下面的 if, 有两个条件:1.最近写入的数据(具体多新未知),2,读取的时候指定了 version
if
(
versionValue
!=
null
)
{
if
(
versionValue
.
delete
(
)
)
{
//删除标识,数据已通过 delete 接口删除了
return
GetResult
.
NOT_EXISTS
;
}
if
(
get
.
versionType
(
)
.
isVersionConflictForReads
(
versionValue
.
version
(
)
,
get
.
version
(
)
)
)
{
Uid
uid
=
Uid
.
createUid
(
get
.
uid
(
)
.
text
(
)
)
;
throw
new
VersionConflictEngineException
(
shardId
,
uid
.
type
(
)
,
uid
.
id
(
)
,
versionValue
.
version
(
)
,
get
.
version
(
)
)
;
}
Translog
.
Operation
op
=
translog
.
read
(
versionValue
.
translogLocation
(
)
)
;
if
(
op
!=
null
)
{
return
new
GetResult
(
true
,
versionValue
.
version
(
)
,
op
.
getSource
(
)
)
;
}
}
}
// no version, get the version from the index, we know that we refresh on flush
return
getFromSearcher
(
get
)
;
}
}
|
2.调用ShardGetService::innerGetLoadFromStoredFields(),根据 type,id,DocumentMapper 等信息从刚刚get 到的信息中获取数据,对指定的field,source,进行过滤(source 过滤只支持对字段),把结果存于GetResult对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private
GetResult
innerGetLoadFromStoredFields
(
String
type
,
String
id
,
String
[
]
gFields
,
FetchSourceContext
fetchSourceContext
,
Engine
.
GetResult
get
,
DocumentMapper
docMapper
,
boolean
ignoreErrorsOnGeneratedFields
)
{
Map
<
String
,
GetField
>
fields
=
null
;
BytesReference
source
=
null
;
Versions
.
DocIdAndVersion
docIdAndVersion
=
get
.
docIdAndVersion
(
)
;
FieldsVisitor
fieldVisitor
=
buildFieldsVisitors
(
gFields
,
fetchSourceContext
)
;
if
(
fieldVisitor
!=
null
)
{
try
{
docIdAndVersion
.
context
.
reader
(
)
.
document
(
docIdAndVersion
.
docId
,
fieldVisitor
)
;
}
catch
(
IOException
e
)
{
throw
new
ElasticsearchException
(
"Failed to get type ["
+
type
+
"] and id ["
+
id
+
"]"
,
e
)
;
}
source
=
fieldVisitor
.
source
(
)
;
.
.
.
}
return
new
GetResult
(
shardId
.
index
(
)
.
name
(
)
,
type
,
id
,
get
.
version
(
)
,
get
.
exists
(
)
,
source
,
fields
)
;
}
|
MGET流程
mget主要处理类:TransportMultiGetAction,其集成关系如下,通过封装单个 GET 请求实现
主要流程如下:
1.request 的 doc 数目中遍历,计算出由 sharid 为 key 组成的 request map.这个过程不在 TransportSingleShardAction 中实现,
是因为如果在那边实现, shardid 会重复
2.循环处理组织好的每个请求,走TransportSingleShardAction中处理单个 doc 的流程.与处理单个 doc 时相比,只是在构建TransportSingleShardAction对象时,传入的泛型:Request,Response不同.这就是说大约只是 shardid 内部计算还是外部计算等3.收集Response,全部 Response返回后执行finishHim(),给客户端返回结果
总结:回复的消息中 doc 顺序与请求的顺序一致如果部分 doc检索失败,不影响其他结果,检索失败的 doc 会在回复信息中标出
通过分析读流程,我们思考以下问题:
读失败是怎么处理的?
没有重试处理.无论是否指定优先级,都不会尝试重读,优先级只是在处理 avtiveshard 的ArrayList时将匹配
的放到了ArrayList前面而已.
怎么选择从主分片还是副本分片读取的?
从 activeshard 中随机选择,通过指定优先级可以从主分片读
分配是 shardit迭代器, 而不是单个目标 node, 难道想挨个尝试?
没有实现挨个尝试,只是向一个发请求后不管
读请求命中 translog 的条件是什么?
写完数据之后,短时间内发起的读操作(无论读操作到达哪个节点,都可以命中,每个主\副分片所在节点都写了 translog)
为什么要加读锁?怕读的时候有人删了改?需要分布式锁吗?
用于多线程间的同步.不需要分布式锁,只锁本节点本进程即可.设想 A 节点在读, B 节点要删除,B 节点删除成功, B 作为协调节点向 A 发送删除请求,该请求会阻塞在读写锁.锁的范围:每个shard有一个读写锁.读写锁是Engine类的成员变量,集群启动的时候, 为存储于本地节点的 index::shard创建一个Engine对象.循环位于:IndicesClusterStateService::applyNewOrUpdatedShards()
读取指定 route 如何处理的?
对GET 请求中 routing 参数的的处理,就是 把默认对 id 进行 hash, 改为对 routing 指定的值进行 hash
对 _source,_field 等过滤器如何处理的?在哪个环节处理的?是否 lucene 处理的?全部读取出来之后才做的 filter 吗?
_source,_field 是在读取了完整的 doc 之后在innerGetLoadFromStoredFields函数中做过滤的
refresh参数在哪实现的?
TransportGetAction::shardOperation()函数,设置 refesh 为 true,并且关闭 realtime 才会刷新 shard.
cache 机制是如何的?
早期版本缓存一切可以缓存的数据
使用频率较高,数据量较大的才进行缓存
缓存老化算法为 LRU: 最近最少使用
参考:
https://www.elastic.co/guide/en/elasticsearch/guide/current/filter-caching.htmlhttps://www.elastic.co/guide/cn/elasticsearch/guide/current/filter-caching.html
读取操作是实时还是准实时?
实时指写入完成后立刻读取,是否能读到。
读取是实时的。因为三元组明确,不需要走倒排索引
search 给的的是关键词,必须走倒排才能查到,不走 translog, 所以是近实时的.
参考:
https://www.elastic.co/guide/en/elasticsearch/guide/current/near-real-time.htmlhttps://www.elastic.co/guide/en/elasticsearch/reference/2.3/docs-get.html#realtimeGET/MGET
为什么不默认读本地节点?
??
GET 相关参数
realtime
默认开启2.3.2版本:尝试从 translog 读取.命中条件:写完数据之后,短时间内发起的读操作会命中,无论这个读取操作被发到哪个节点.因为当一个写操作返回时,所有主,副分片所在节点都有 translog 可以命中5.5的版本中,不受索引刷新速率的影响,如果一个document没有被更新了,但是还没有刷新,那么get API获取此文档的时候会先刷新,然后再get
Optional Type
如果想要查询所有的类型,可以直接指定_type为_all,从而匹配所有的类型。返回匹配 id 的第一个 doc
Source filtering
默认情况下get操作会返回_source字段,除非你使用了fields字段或者禁用了_source字段。通过设置_source属性,可以禁止返回source内容:
|
curl
-
XGET
'http://localhost:9200/twitter/tweet/1?_source=false'
|
如果想要返回特定的字段,可以使用_source_include或者_source_exclude进行过滤。可以使用逗号分隔来设置多种匹配模式,比如:
|
curl
-
XGET
'http://localhost:9200/twitter/tweet/1?_source_include=*.id&_source_exclude=entities'
|
如果希望返回特定的字段,也可以直接写上字段的名称:
|
curl
-
XGET
'http://localhost:9200/twitter/tweet/1?_source=*.id,retweeted'
|
Fields
get操作允许设置fields字段,返回特定的字段:
curl -XGET ‘http://localhost:9200/twitter/tweet/1?fields=title,content’
如果请求的字段没有被存储,那么他们会从source中分析出来,这个功能也可以用 source filter来替代。
元数据比如_routing和_parent是永远不会被返回的。
只有叶子字段才能通过field选项返回.所以对象字段这种是不能返回的,这种请求也会失败。
Routing
当索引的时候指定了路由,那么查询的时候就一定要指定路由。
curl -XGET ‘http://localhost:9200/twitter/tweet/1?routing=kimchy’
如果路由信息不正确,就会查找不到文档
Preference
控制为get请求维护一个分片的索引,这个索引可以设置为:
_primary 这个操作仅仅会在主分片上执行。
_local 这个操作会在本地的分片上执行。
Custom (string) value 用户可以自定义值,对于相同的分片可以设置相同的值。这样可以保证不同的刷新状态下,查询不同的分片。就像sessionid或者用户名一样。
Refresh
refresh参数可以让每次get之前都刷新分片,使这个值可以被搜索。设置true的时候,尽量要考虑下性能问题,因为每次刷新都会给系统带来一定的压力
Versioning support
使用 version 参数检索文档,只有当前版本号与指定版本号相同时才能成功,这种机制对所有版本类型都有效,除了FOUCE, 他总是返回 doc.
ref:
http://blog.csdn.net/u010994304/article/details/50441419
http://www.code123.cc/2582.html
http://www.jianshu.com/p/62febe581fcb
http://blog.csdn.net/july_2/article/details/24777931
http://blog.csdn.net/laigood/article/details/8450331
http://www.cnblogs.com/xing901022/p/5317698.html