4.4. 分片管理¶
4.4.1. 简介¶
本文档讨论了 CouchDB 中分片的工作原理,以及如何安全地添加、移动、删除和创建分片和分片副本的放置规则。
一个 分片 是数据库中数据的水平分区。将数据分成分片并将每个分片的副本(称为“分片副本”或“副本”)分布到集群中的不同节点,可以提高数据在节点丢失时的持久性。CouchDB 集群会自动对数据库进行分片,并将构成每个分片的文档子集分布到各个节点。必须手动修改集群成员资格和分片行为。
4.4.1.1. 分片和副本¶
每个数据库有多少个分片和副本可以在全局级别或每个数据库的基础上设置。相关的参数是 q
和 n
。
q 是要维护的数据库分片数量。n 是要分布的每个文档的副本数量。n
的默认值为 3
,q
的默认值为 2
。使用 q=2
,数据库将被分成 2 个分片。使用 n=3
,集群将分布每个分片的三个副本。总共,单个数据库有 6 个分片副本。
在一个包含 q=8
的 3 节点集群中,每个节点将接收 8 个分片。在一个 4 节点集群中,每个节点将接收 6 个分片。我们建议在一般情况下,集群中的节点数量应该是 n
的倍数,这样分片就可以均匀分布。
CouchDB 节点有一个 etc/default.ini
文件,其中包含一个名为 cluster 的部分,如下所示
[cluster]
q=2
n=3
这些设置指定了新创建数据库的默认分片参数。这些参数可以在 etc/local.ini
文件中被覆盖,方法是复制上面的文本,并将值替换为新的默认值。如果 [couch_peruser]
q
被设置,则该值将用于每个用户数据库。(默认情况下,它被设置为 1,假设每个用户数据库都非常小,并且会有很多这样的数据库。)这些值也可以在每个数据库的基础上设置,方法是在创建数据库时指定 q
和 n
查询参数。例如
$ curl -X PUT "$COUCH_URL:5984/database-name?q=4&n=2"
这将创建一个被分成 4 个分片和 2 个副本的数据库,产生 8 个分片副本,分布在整个集群中。
4.4.1.2. 法定人数¶
根据集群的大小、每个数据库的分片数量和分片副本的数量,并非每个节点都可能访问每个分片,但每个节点都知道所有分片的副本在哪里,可以通过 CouchDB 的内部分片映射找到。
进入 CouchDB 集群的每个请求都由任何一个随机的协调节点处理。这个协调节点将请求代理到拥有相关数据的其他节点,这些节点可能包括它自己,也可能不包括它自己。协调节点在收到法定人数的数据库节点的响应后,将向客户端发送响应;默认情况下是 2 个节点。法定人数的默认所需大小等于 r=w=((n+1)/2)
,其中 r
指的是读取法定人数的大小,w
指的是写入法定人数的大小,n
指的是每个分片的副本数量。在一个默认的集群中,n
为 3,((n+1)/2)
将为 2。
注意
集群中的每个节点都可以是任何一个请求的协调节点。集群内部的节点没有特殊的角色。
所需法定人数的大小可以在请求时配置,方法是为文档读取设置 r
参数,为文档写入设置 w
参数。无论配置了什么样的法定人数,_view
、_find
和 _search
端点只读取一个副本,实际上使这些请求的法定人数为 1。
例如,以下是一个请求,它指示协调节点在至少两个节点响应后发送响应
$ curl "$COUCH_URL:5984/{db}/{doc}?r=2"
以下是一个类似的写入文档的示例
$ curl -X PUT "$COUCH_URL:5984/{db}/{doc}?w=2" -d '{...}'
将 r
或 w
设置为等于 n
(副本数量)意味着你只有在所有拥有相关分片的节点响应或超时后才会收到响应,因此这种方法不能保证 ACID 一致性。将 r
或 w
设置为 1 意味着你只在只有一个相关节点响应后就会收到响应。
4.4.2. 检查数据库分片¶
有一些 API 端点可以帮助你了解数据库是如何分片的。让我们先在一个集群上创建一个新的数据库,并在其中放入几个文档
$ curl -X PUT $COUCH_URL:5984/mydb
{"ok":true}
$ curl -X PUT $COUCH_URL:5984/mydb/joan -d '{"loves":"cats"}'
{"ok":true,"id":"joan","rev":"1-cc240d66a894a7ee7ad3160e69f9051f"}
$ curl -X PUT $COUCH_URL:5984/mydb/robert -d '{"loves":"dogs"}'
{"ok":true,"id":"robert","rev":"1-4032b428c7574a85bc04f1f271be446e"}
首先,顶层 /db 端点将告诉你数据库的分片参数是什么
$ curl -s $COUCH_URL:5984/db | jq .
{
"db_name": "mydb",
...
"cluster": {
"q": 8,
"n": 3,
"w": 2,
"r": 2
},
...
}
因此,我们知道这个数据库是用 8 个分片 (q=8
) 创建的,每个分片有 3 个副本 (n=3
),总共 24 个分片副本分布在集群中的所有节点上。
现在,让我们使用 /db/_shards 端点看看这些分片副本是如何放置在集群上的
$ curl -s $COUCH_URL:5984/mydb/_shards | jq .
{
"shards": {
"00000000-1fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"20000000-3fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"40000000-5fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"60000000-7fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"80000000-9fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"a0000000-bfffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"c0000000-dfffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"e0000000-ffffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
]
}
}
现在我们看到,这个集群实际上有 4 个节点,CouchDB 将这 24 个分片副本均匀地分布在所有 4 个节点上。
我们还可以使用 /db/_shards/doc 端点查看哪个分片包含给定的文档
$ curl -s $COUCH_URL:5984/mydb/_shards/joan | jq .
{
"range": "e0000000-ffffffff",
"nodes": [
"[email protected]",
"[email protected]",
"[email protected]"
]
}
$ curl -s $COUCH_URL:5984/mydb/_shards/robert | jq .
{
"range": "60000000-7fffffff",
"nodes": [
"[email protected]",
"[email protected]",
"[email protected]"
]
}
CouchDB 向我们展示了两个示例文档中的每一个被映射到的特定分片。
4.4.3. 移动分片¶
在移动分片或对集群执行其他分片操作时,建议停止集群上的所有分片作业。有关更多详细信息,请参阅 停止分片作业。
本节介绍如何手动放置和替换分片。当你确定你的集群太大或太小,并且想要成功地调整其大小,或者你从服务器指标中注意到数据库/分片布局不是最佳的,并且有一些需要解决的“热点”时,这些活动是关键步骤。
考虑一个包含三个节点的集群,其中 q=8,n=3。每个数据库有 24 个分片,分布在三个节点上。如果您添加第四个节点到集群中,CouchDB 不会将现有的数据库分片重新分配到该节点。这会导致负载不均衡,因为新节点只会托管在它加入集群后创建的数据库的分片。为了平衡现有数据库分片的分布,必须手动移动它们。
在集群中节点之间移动分片涉及以下步骤
将分片和任何辅助索引分片复制到目标节点。
更新集群元数据以反映新的目标分片。
监控内部复制以确保分片是最新的。
再次更新集群元数据以删除源分片
从源节点删除分片文件和辅助索引文件。
4.4.3.1. 复制分片文件¶
注意
从技术上讲,复制数据库和辅助索引分片是可选的。如果您在执行此数据复制之前继续执行下一步,CouchDB 将使用内部复制来填充新添加的分片副本。但是,复制文件比内部复制更快,尤其是在繁忙的集群中,因此我们建议首先执行此手动数据复制。
分片文件位于 CouchDB 安装的 data/shards
目录中。在这些子目录中是分片文件本身。例如,对于名为 abc
的 q=8
数据库,以下是其数据库分片文件
data/shards/00000000-1fffffff/abc.1529362187.couch
data/shards/20000000-3fffffff/abc.1529362187.couch
data/shards/40000000-5fffffff/abc.1529362187.couch
data/shards/60000000-7fffffff/abc.1529362187.couch
data/shards/80000000-9fffffff/abc.1529362187.couch
data/shards/a0000000-bfffffff/abc.1529362187.couch
data/shards/c0000000-dfffffff/abc.1529362187.couch
data/shards/e0000000-ffffffff/abc.1529362187.couch
辅助索引(包括 JavaScript 视图、Erlang 视图和 Mango 索引)也是分片的,它们的碎片应该被移动以节省新节点重建视图的努力。视图分片位于 data/.shards
中。例如
data/.shards
data/.shards/e0000000-ffffffff/_replicator.1518451591_design
data/.shards/e0000000-ffffffff/_replicator.1518451591_design/mrview
data/.shards/e0000000-ffffffff/_replicator.1518451591_design/mrview/3e823c2a4383ac0c18d4e574135a5b08.view
data/.shards/c0000000-dfffffff
data/.shards/c0000000-dfffffff/_replicator.1518451591_design
data/.shards/c0000000-dfffffff/_replicator.1518451591_design/mrview
data/.shards/c0000000-dfffffff/_replicator.1518451591_design/mrview/3e823c2a4383ac0c18d4e574135a5b08.view
...
由于它们是文件,您可以使用 cp
、rsync
、scp
或其他文件复制命令将它们从一个节点复制到另一个节点。例如
# one one machine
$ mkdir -p data/.shards/{range}
$ mkdir -p data/shards/{range}
# on the other
$ scp {couch-dir}/data/.shards/{range}/{database}.{datecode}* \
{node}:{couch-dir}/data/.shards/{range}/
$ scp {couch-dir}/data/shards/{range}/{database}.{datecode}.couch \
{node}:{couch-dir}/data/shards/{range}/
注意
请记住在数据库文件之前移动视图文件!如果视图索引领先于其数据库,则数据库将从头开始重建它。
4.4.3.2. 将目标节点设置为 true
维护模式¶
在告诉 CouchDB 关于节点上的这些新分片之前,必须将该节点置于维护模式。维护模式指示 CouchDB 在 /_up
端点上返回 404 Not Found
响应,并确保它不参与其分片的正常交互式集群请求。正确配置的负载均衡器使用 GET /_up
来检查节点的健康状况,将检测到此 404 并将节点从循环中移除,防止请求发送到该节点。例如,要配置 HAProxy 使用 /_up
端点,请使用
http-check disable-on-404
option httpchk GET /_up
如果您没有设置维护模式,或者负载均衡器忽略了此维护模式状态,那么在执行下一步后,集群在咨询相关节点时可能会返回不正确的响应。您不希望这样!在接下来的步骤中,我们将确保此分片是最新的,然后再允许它参与最终用户请求。
要启用维护模式
$ curl -X PUT -H "Content-type: application/json" \
$COUCH_URL:5984/_node/{node-name}/_config/couchdb/maintenance_mode \
-d "\"true\""
然后,通过对该节点的单个端点执行 GET /_up
来验证该节点是否处于维护模式
$ curl -v $COUCH_URL/_up
…
< HTTP/1.1 404 Object Not Found
…
{"status":"maintenance_mode"}
最后,检查您的负载均衡器是否已将该节点从可用后端节点池中移除。
4.4.3.3. 更新集群元数据以反映新的目标分片¶
现在我们需要告诉 CouchDB 目标节点(该节点必须已经加入集群)应该托管给定数据库的分片副本。
要更新集群元数据,请使用特殊的 /_dbs
数据库,这是一个内部 CouchDB 数据库,它将数据库映射到分片和节点。此数据库在节点之间自动复制。它只能通过特殊的 /_node/_local/_dbs
端点访问。
首先,检索数据库的当前元数据
$ curl https://127.0.0.1/_node/_local/_dbs/{name}
{
"_id": "{name}",
"_rev": "1-e13fb7e79af3b3107ed62925058bfa3a",
"shard_suffix": [46, 49, 53, 51, 48, 50, 51, 50, 53, 50, 54],
"changelog": [
["add", "00000000-1fffffff", "[email protected]"],
["add", "00000000-1fffffff", "[email protected]"],
["add", "00000000-1fffffff", "[email protected]"],
…
],
"by_node": {
"[email protected]": [
"00000000-1fffffff",
…
],
…
},
"by_range": {
"00000000-1fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
…
}
}
以下是该文档的简要结构
_id
:数据库的名称。_rev
:元数据的当前修订版。shard_suffix
:数据库创建的时间戳,标记为自 Unix 纪元以来的秒数,映射到 ASCII 数字的代码点。changelog
:数据库分片的历史记录。by_node
:每个节点上的分片列表。by_range
:每个分片位于哪些节点上。
要反映元数据中的分片移动,有三个步骤
添加适当的变更日志条目。
更新
by_node
条目。更新
by_range
条目。
警告
非常小心!在此过程中出错会导致集群无法修复地损坏!
截至撰写本文时,此过程必须手动完成。
要将分片添加到节点,请将类似于此的条目添加到数据库元数据的 changelog
属性中
["add", "{range}", "{node-name}"]
{range}
是分片的特定分片范围。 {node-name}
应与集群上 GET /_membership
中显示的节点的名称和地址匹配。
注意
从节点中删除分片时,请指定 remove
而不是 add
。
确定新的变更日志条目后,您需要更新 by_node
和 by_range
以反映谁存储了哪些分片。变更日志条目中的数据和这些属性中的数据必须匹配。如果不匹配,数据库可能会损坏。
继续我们的示例,以下是上面元数据的更新版本,它将分片添加到名为 node4
的附加节点
{
"_id": "{name}",
"_rev": "1-e13fb7e79af3b3107ed62925058bfa3a",
"shard_suffix": [46, 49, 53, 51, 48, 50, 51, 50, 53, 50, 54],
"changelog": [
["add", "00000000-1fffffff", "[email protected]"],
["add", "00000000-1fffffff", "[email protected]"],
["add", "00000000-1fffffff", "[email protected]"],
...
["add", "00000000-1fffffff", "[email protected]"]
],
"by_node": {
"[email protected]": [
"00000000-1fffffff",
...
],
...
"[email protected]": [
"00000000-1fffffff"
]
},
"by_range": {
"00000000-1fffffff": [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
],
...
}
}
现在您可以 PUT
此新的元数据
$ curl -X PUT https://127.0.0.1/_node/_local/_dbs/{name} -d '{...}'
4.4.3.4. 强制同步分片¶
2.4.0 版中的新增功能。
无论您是否将分片预先复制到新节点,您都可以使用 /db/_sync_shards 端点强制 CouchDB 同步数据库中所有分片的副本
$ curl -X POST $COUCH_URL:5984/{db}/_sync_shards
{"ok":true}
这将启动同步过程。请注意,这会给您的集群带来额外的负载,这可能会影响性能。
也可以通过写入存储在该分片中的文档来强制对每个分片进行同步。
注意
管理员可能希望在分片同步期间将他们的 [mem3] sync_concurrency
值提高到更大的数字。
4.4.3.5. 监控内部复制以确保分片是最新的¶
完成上一步后,CouchDB 将开始同步分片。您可以通过监控 /_node/{node-name}/_system
端点来观察这种情况,该端点包括 internal_replication_jobs
指标。
一旦此指标恢复到您开始分片同步之前的基线,或者为 0
,则分片副本已准备好提供数据,我们可以将节点从维护模式中取出。
4.4.3.6. 清除目标节点的维护模式¶
您现在可以通过将 "false"
放入维护模式配置端点来让节点开始服务数据请求,就像步骤 2 中一样。
通过对该节点的单个端点执行 GET /_up
来验证该节点是否未处于维护模式。
最后,检查您的负载均衡器是否已将该节点返回到可用后端节点池中。
4.4.3.7. 再次更新集群元数据以删除源分片¶
现在,从分片映射中删除源分片,方法与步骤 2 中将新的目标分片添加到分片映射的方法相同。请确保将 ["remove", {range}, {source-shard}]
条目添加到变更日志的末尾,以及修改数据库元数据文档的 by_node
和 by_range
部分。
4.4.3.8. 从源节点删除分片和辅助索引文件¶
最后,您可以通过从源主机上的命令行删除其文件以及任何视图分片副本,来删除源分片副本
$ rm {couch-dir}/data/shards/{range}/{db}.{datecode}.couch
$ rm -r {couch-dir}/data/.shards/{range}/{db}.{datecode}*
恭喜!您已移动数据库分片副本。通过这种方式添加和删除数据库分片副本,您可以更改集群的分片布局,也称为分片映射。
4.4.4. 指定数据库放置位置¶
您可以配置 CouchDB 在数据库创建时使用放置规则将分片副本放在特定节点上。
警告
使用 placement
选项将覆盖 n
选项,无论是在 .ini
文件中还是在 URL
中指定。
首先,每个节点必须用区域属性标记。这定义了每个节点所在的区域。您可以通过编辑 /_nodes
特殊数据库中的节点文档来实现,该数据库可以通过 /_node/_local/_nodes/{node-name}
特殊节点本地 API 端点访问。添加以下形式的键值对
"zone": "{zone-name}"
对集群中的所有节点执行此操作。例如
$ curl -X PUT https://127.0.0.1/_node/_local/_nodes/{node-name} \
-d '{ \
"_id": "{node-name}",
"_rev": "{rev}",
"zone": "{zone-name}"
}'
在每个节点的本地配置文件 (local.ini
) 中,定义一个一致的集群范围设置,例如
[cluster]
placement = {zone-name-1}:2,{zone-name-2}:1
在此示例中,CouchDB 将确保分片的两份副本将托管在区域属性设置为 {zone-name-1}
的节点上,而一份副本将托管在区域属性设置为 {zone-name-2}
的新节点上。
这种方法很灵活,因为您还可以通过在创建数据库时将放置设置指定为查询参数来在每个数据库的基础上指定区域,使用与 ini 文件相同的语法
curl -X PUT $COUCH_URL:5984/{db}?zone={zone}
也可以指定 placement
参数。请注意,这将覆盖确定创建副本数量的逻辑!
请注意,您还可以使用此系统来确保集群中的某些节点不托管任何新创建数据库的副本,方法是为它们提供一个在 [cluster]
放置字符串中未出现的区域属性。
4.4.5. 拆分分片¶
/_reshard 是一个用于分片操作的 HTTP API。目前它只支持分片拆分。要执行分片合并,请参考 合并分片 部分中概述的手动过程。
与 /_reshard 交互的主要方式是创建分片作业,监控这些作业,等待它们完成,删除它们,发布新作业,等等。以下是使用此 API 拆分分片可能采取的几个步骤。
首先,最好调用 GET /_reshard
以查看集群上分片的摘要。
$ curl -s $COUCH_URL:5984/_reshard | jq .
{
"state": "running",
"state_reason": null,
"completed": 3,
"failed": 0,
"running": 0,
"stopped": 0,
"total": 3
}
需要注意的两件重要事情是作业总数和状态。
state
字段指示集群上分片的状态。通常它应该是 running
,但是,另一个用户可能已暂时禁用分片。然后,状态将是 stopped
,并且希望 state_reason
字段的值中会有一个原因或注释。有关更多详细信息,请参阅 停止分片作业。
作业的 total
数量很重要,因为每个节点都有最大分片作业数量,并且在达到限制后创建新作业会导致错误。在开始新作业之前,最好删除已完成的作业。有关 max_jobs
参数的默认值以及如何在需要时调整它的信息,请参阅 分片配置部分。
例如,要删除所有已完成的作业,请运行
$ for jobid in $(curl -s $COUCH_URL:5984/_reshard/jobs | jq -r '.jobs[] | select (.job_state=="completed") | .id'); do \
curl -s -XDELETE $COUCH_URL:5984/_reshard/jobs/$jobid \
done
然后,最好查看数据库分片映射的样子。
$ curl -s $COUCH_URL:5984/db1/_shards | jq '.'
{
"shards": {
"00000000-7fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"80000000-ffffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
]
}
}
在此示例中,我们将拆分 00000000-7fffffff
范围的所有副本。API 允许组合使用参数,例如:拆分所有节点上的所有范围,仅拆分一个节点上的所有范围,或拆分一个特定节点上的一个特定范围。这些是通过 db
、node
和 range
作业参数指定的。
要拆分 00000000-7fffffff
的所有副本,我们发出如下请求
$ curl -s -H "Content-type: application/json" -XPOST $COUCH_URL:5984/_reshard/jobs \
-d '{"type": "split", "db":"db1", "range":"00000000-7fffffff"}' | jq '.'
[
{
"ok": true,
"id": "001-ef512cfb502a1c6079fe17e9dfd5d6a2befcc694a146de468b1ba5339ba1d134",
"node": "[email protected]",
"shard": "shards/00000000-7fffffff/db1.1554242778"
},
{
"ok": true,
"id": "001-cec63704a7b33c6da8263211db9a5c74a1cb585d1b1a24eb946483e2075739ca",
"node": "[email protected]",
"shard": "shards/00000000-7fffffff/db1.1554242778"
},
{
"ok": true,
"id": "001-fc72090c006d9b059d4acd99e3be9bb73e986d60ca3edede3cb74cc01ccd1456",
"node": "[email protected]",
"shard": "shards/00000000-7fffffff/db1.1554242778"
}
]
该请求返回了三个作业,每个副本一个作业。
要检查这些作业的进度,请使用 GET /_reshard/jobs
或 GET /_reshard/jobs/{jobid}
。
最终,这些作业应该完成,分片映射应该如下所示
$ curl -s $COUCH_URL:5984/db1/_shards | jq '.'
{
"shards": {
"00000000-3fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"40000000-7fffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
],
"80000000-ffffffff": [
"[email protected]",
"[email protected]",
"[email protected]"
]
}
}
4.4.6. 停止分片作业¶
可以停止集群级别的分片,然后重新启动。这有助于允许操作分片映射的外部工具避免干扰分片作业。要停止集群上的所有分片作业,请向 /_reshard/state
端点发出 PUT
请求,其中包含 "state": "stopped"
键和值。您还可以指定一个可选的注释或停止原因。
例如
$ curl -s -H "Content-type: application/json" \
-XPUT $COUCH_URL:5984/_reshard/state \
-d '{"state": "stopped", "reason":"Moving some shards"}'
{"ok": true}
然后,此状态将反映在全局摘要中
$ curl -s $COUCH_URL:5984/_reshard | jq .
{
"state": "stopped",
"state_reason": "Moving some shards",
"completed": 74,
"failed": 0,
"running": 0,
"stopped": 0,
"total": 74
}
要重新启动,请发出与上面类似的 PUT
请求,其中 running
作为状态。这应该从上次检查点恢复所有分片拆分作业。
有关更多详细信息,请参阅 API 参考:/_reshard。
4.4.7. 合并分片¶
数据库的 q
值可以在创建数据库时设置,也可以通过拆分一些分片 拆分分片 来在以后增加。为了减少 q
并将一些分片合并在一起,必须重新生成数据库。以下是步骤
如果集群上有正在运行的分片拆分作业,请通过 HTTP API 停止分片作业 停止它们。
通过在 PUT 操作期间将 q 值指定为查询参数,创建一个具有所需分片设置的临时数据库。
停止访问数据库的客户端。
将主数据库复制到临时数据库。如果主数据库处于活动使用状态,可能需要多次复制。
删除主数据库。确保没有人使用它!
使用所需的分片设置重新创建主数据库。
客户端现在可以再次访问数据库。
将临时数据库复制回主数据库。
删除临时数据库。
完成所有步骤后,数据库可以再次使用。集群将根据放置规则自动创建和分配其分片。
如果客户端应用程序可以被指示使用新数据库而不是旧数据库,并且在非常短暂的停机时间窗口内执行切换,则可以避免生产中的停机时间。