9. 数据复制模型
单处理和批处理操作主要围绕数据复制模型来理解,具体接口如下:
- 单文档处理接口:
Index
接口;Get
接口;Delete
接口;Update
接口;
- 多文档处理接口:
- Multi Get接口;
- 批处理接口;
- Delete By Query API;
- Update By Query API;
- Reindex API;
每个ES索引都被分割为碎片shards
,每个碎片又有多个副本(它们组成一个副本群,在文档添加和移除时保持同步),如果不这么干,从不同副本中读到的结果将会有差异,这种保持碎片同步并且从碎片中读取数据的服务就是数据复制模型,ES的数据复制模型是建立在主备份模型上,该模型基于具有来自复制组的单个副本,该副本充当主碎片(primary shard
),其他的副本叫副本碎片(replica shards
),主碎片充当所有索引操作的入口点,负责验证并确保这些操作是正确的,一旦一个索引操作被主碎片接受,他还负责将该操作复制到其副本上。
基本写模式
Elasticsearch中的每个索引操作通常基于文档ID首先使用路由解析为复制组,一旦复制组定下来了,那么将这个索引操作将会在复制组内部转发到这个复制组中的主碎片,然后主碎片验证并将该操作复制给组内其他的副本碎片。由于副本碎片可以是离线的状态,通常主碎片没有必要将操作复制到所有的副本碎片上,它只保证一系列的碎片可以接受到上述的操作行为,这些“一系列碎片”就是同步副本(由主节点负责维护),这些是“好的碎片副本”的集合,保证了所有的索引操作和删除操作都已经向用户确认过了。主碎片保持不变并将所有的操作复制给组内的每个副本碎片,主碎片的基本流程如下:
- 验证传入的操作,若结构无效则拒绝传入;
- 执行本地操作(如索引和删除相关文档document),这一步也会验证字段内容,如果有必要也将拒绝该操作;
- 在当前同步副本集合内传达该操作,如果有多个副本,那将会并行传达;
- 一旦所有的副本成功执行并给主碎片反馈了执行结果,主碎片向客户端确认请求已成功操作。
基本读模式
Elasticsearch中的读取可以是通过ID这样的轻量级查找,也可以是具有大量复杂聚合的搜索请求,这些聚合会占用大量CPU资源。主备份模型优点之一就是使得所有碎片相同,因此,单个同步副本可以高效的处理读请求。
当都请求被一个节点接受,那该节点就负责将这个请求转发到相关碎片的节点上并整理执行结果将其回复给客户端,基本流程如下:
- 将读取请求解析到相关碎片上,注意由于大部分的搜索将会被发送到一个或多个索引上,它们需要从多个碎片中读取,每个读取结果都代表了数据的不同子集;
- 从碎片复制组中的每个相关碎片中选择一个活动状态的碎片副本,可以是主碎片或副本碎片,默认情况下,Elasticsearch将简单地在碎片副本之间循环这些行为;
- 将碎片级读取请求发送到所选副本;
- 组装结果进行回复,注意如果仅仅是
get by id
这样仅有一个相关碎片的查询,那这一步可以省略;
【故障】
昨天ES在升级证书后出现问题,且今天启动Kibana时出现请求超时的异常,做如下处理:
- 加大ES内存,由1G增加至2G:
# 修改配置文件jvm.options
-Xms2g
-Xmx2g
- 加大Kibana请求的超时时间到60s:
# 修改kibana.yml文件
elasticsearch.requestTimeout: 60000
9.1 单处理接口
9.1.1 Index
接口
创建索引以及参数含义之前章节一致,ES具备自动创建索引的功能,主要流程是一个索引操作,但是这个索引之前并没有创建,如果尚未创建特定类型,还会自动为特定类型创建动态类型映射,映射本身非常灵活,无需架构。新字段和对象将自动添加到指定类型的映射定义中。自动索引创建可以通过设置action.auto_create_index
参数为false
来取消这种自动创建的动作,自动映射创建同样可以通过将index.mapper.dynamic
参数设置为false
来取消。另外,自动索引创建的设置可以包含白名单/黑名单,比如action.auto_create_index
设置为+aaa*,-bbb*,+ccc*,-*
(+
表示允许,-
表示不允许)。
创建文档的命令为:
PUT twitter/_doc/1
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
返回的结果为:
{
"_index": "twitter",
"_type": "_doc",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
其中
_shards
:提供了有关索引复制过程的信息;total
:指示应在其上执行索引操作的分片副本(主分片和副本分片)的数量;successful
:指示索引操作成功的分片副本数;failed
:在副本分片上索引操作失败的情况下包含复制相关错误的数组。
如果索引操作成功,那successful
至少为1。
【注意】
当一个索引操作成功返回时副本碎片都可能还没有开始(默认情况下,只有主碎片时必须的,但是这种机制可以更改)。
另外每个索引都有一个版本号,相关的version
都作为索引请求返回值的一部分,索引API可选地允许在指定version
参数时进行乐观并发控制。这种针对文档操作的版本控制就是为了标记被执行,版本控制一个很好的用例就是是执行事务性读取然后更新(read-then-update)。从最初读取的文档中指定版本可确保在此期间未发生任何更改(当为了更新先进行阅读时,建议将preference
设置为_primary
)。下面就是一个版本控制的小例子:
PUT twitter/_doc/1?version=2
{
"message" : "elasticsearch now has versioning support, double cool!"
}
上述操作先去twitter
索引中寻找_doc
类型且_id
为1的文档,过程并不是简单的去找就完事儿了,还要进行版本匹配,只有当找到的文档version
参数为2才能再进行更新,如果version
参数不是指定的2,ES就会出现version_conflict_engine_exception
。当使用外部版本控制时,就不是进行指定的版本号匹配了,而是检查传递给索引请求的版本号是否大于当前存储文档的版本号,如果版本号大于当前文档的版本号,则将索引文档并使用新版本号,如果提供的版本号小于等于当前文档的版本号,那就会发生版本冲突,索引操作失败。
【注意】
版本控制是完全实时的,并且不受近实时的搜索操作影响,但是如果未提供版本,则执行该操作而不进行任何版本检查。默认情况下,version
参数都是从1开始,每次操作(包括删除操作)递增1,具体的version
可以不使用这种方式,比如可以使用外部数据库的维护的值(),如果想这么搞,就要将version_type
设置为external
,另外version
值必须为数字且大于等于0,小于等于9.2e+18,虽然使用外部版本号可以从0开始,但是version=0
的文档既不能更新也不能删除(这是一个副作用)。使用外部版本号,一个好的副作用是,只要使用数据库中的版本号,就不需要维护由于数据库更改而执行的异步索引操作的严格排序。如果使用外部版本控制,即使使用数据库中的数据更新Elasticsearch索引的简单情况也会简化,因为如果索引操作由于某种原因而无序,则仅使用最新版本。
除了上述的内部版本号和外部版本(即internal
和external
),版本号还有其他的类型:
internal
:如果给定版本与存储文档的版本相同,则仅索引文档,即第一个例子;external
或external_gt
:只有给出的版本号大于当前文档的版本号或这个文档根本不存在才会进行索引(前提是提供的版本号是一个合法的数值),如果文档不存在就使用提供的版本号创建一个文档。external_gte
:只有给定的版本号大于或等于当前文档的版本号才会去索引文档,同样在给定版本号是合法数字的情况下,如果文档不存在,就会使用给定的版本号创建一个新的文档(这个类型的版本慎用,如果用不好会导致数据丢失);
操作类型
索引操作也接受op_type
参数,它可以用于强制执行一个create
行为,允许如果数据不存在就创建,当使用create
时,如果指定id的文档已经存在了,那么索引操作就会失败,出现版本冲突,下面是一个例子:
PUT twitter/_doc/1?op_type=create
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
// 也可以这样使用
PUT twitter/_doc/1/_create
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
自动生成ID
如果索引操作没有指定ID,那id将会自动生成,此外,op_type
将会自动设置为create
,比如下面的例子(注意使用的是POST
方法而不是PUT
):
POST twitter/_doc/
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
路由
默认碎片位置(或routing
)是由文档ID的哈希值控制的。为了更明确的控制,可以使用路由参数routing
在每个操作的基础上直接指定输入到路由器使用的散列函数的值,比如:
POST twitter/_doc?routing=kimchy
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
在上述的例子中,_doc
文档是通过routing
参数提供的kimchy
值路由到一个碎片,在设置显式映射时,可以选择使用_routing
字段来指示索引操作从文档本身中提取路由值,这确实是自另一个文档解析过程的(非常小的)成本,如果定义了_routing
映射并将其设置为required
,则如果未提供或提取路由值,则索引操作将失败。
等待活动的碎片
为了提高写入系统的弹性,索引操作可以在执行前等待一定数量的活跃副本碎片,如果必要的存活副本碎片没有达到指定的数量,写入操作就必须等待重试,直到有必要的副本碎片或者超时了,默认情况下,写入操作只等待主碎片就绪就执行写入了(配置为wait_for_active_shards=1
),这配置在索引配置中可以用index.write.wait_for_active_shards
进行动态配置,为了更改每个操作的这种行为,可以使用wait_for_active_shards
请求参数。有效值是all
或者任何正整数,最大为索引中每个分片的已配置副本总数(即number_of_replicas+1
),如果指定一个负数或者大于碎片分本的数目将会抛出一个异常。
比如一个集群中有3个节点A、B、C,索引副本设置为3(产生4个分片副本,比节点多一个副本),如果进行索引操作,默认情况只需要保证每个碎片的主副本碎片在执行前是就绪的就行了,就算B和C挂了,而节点A托管了主碎片副本,那么索引操作仍将仅使用一个数据副本。但是如果wait_for_active_shards
参数在请求设置为3(3个节点必须全部启动就绪),那在执行索引操作前必须有3个就绪的碎片副本,应该满足的要求,因为群集中有3个活动节点,每个活动节点都有一个碎片副本,这个集群中3个就绪的节点刚好满足要求,每个节点托管着一个碎片的副本,然而,如果我们将wait_for_active_shards
设置为all
(即4),那索引操作将不会执行,因为索引的每个碎片没有4个副本,此时这个操作只能等待超时或者集群启动一个新的节点来托管第4个碎片副本。
注意,上述的设置大幅度的降低了写入操作(不写入所需数量的分片副本)的可能性,但它并未完全消除这种可能性,因为此检查在写入操作开始之前发生。一旦写入操作正在进行了,复制在任何数量的碎片副本上仍然可能失败,但仍然可以在主碎片上成功。_shards
参数是写入操作的响应结果,表现的就是副本碎片的成功和碎片的数量。
空更新(Noop Update)
当使用索引API更新一个文档时,一个新版本的文档总是会被创建,即使这个文档并没改变,如果不想发生这样的行为,可以使用_update
接口,并将detect_noop
设置为true
。索引api上没有此选项,因为索引api不会获取旧源,也无法将其与新源进行比较。关于何时不接受空更新,没有一条硬性规定,它是由许多因素决定的,例如数据源发送实际noops更新的频率以及Elasticsearch在接收更新时在分片上运行的每秒查询数。
关于超时
执行索引操作时,被分配执行索引操作的主碎片可能存在暂时不可获取的状态,各种原因都有,可能是主分片当前正在从网关恢复或正在进行重定位。默认情况下,索引操作在返回错误前将等待主碎片1分钟,如果1分钟还没等到主碎片那就返回错误,timeout
参数可以用来明确的指定索引操作需要等待多长时间,下面是索引操作时指定超时时间的小栗子:
PUT twitter/_doc/1?timeout=5m
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
9.1.2 Get
接口
get
请求方式允许通过id获取一个JSON类型的文档,比如下面的请求的结果:
请求
GET twitter/_doc/0
结果
{
"_index": "twitter",
"_type": "_doc",
"_id": "1",
"_version": 4,
"found": true,
"_source": {
"user": "kimchy",
"post_date": "2009-11-15T14:12:12",
"message": "elasticsearch now has versioning support, double cool!3"
}
}
上面的命令直接是获取了结果,但在执行命令前它并不知道这个文档到底是不是存在,如果有这种需求可以使用HEAD
请求方式:
HEAD twitter/_doc/0
这就不是返回具体的文档结果了,如果文档存在,就返回200状态码,如果文档不存就返回404。
实时
默认情况下,GET
方法是实时的,并不会受到索引的刷新率影响(当数据对搜索可见时)。如果一个文档已经被更新但还没有被刷新,GET
API就可以发出刷新调用使得刚更新的文档可见,这也将使上次刷新后其他文档发生变化,如果不想让GET
API实时更新,可以将realtime
参数设成false
。
源过滤
默认情况下,get操作返回一个_source
域的文本,除非已经使用了