4.4. 碎片管理¶
4.4.1. 介绍¶
本文讨论了分片在CouchDB中的工作原理,以及如何安全地添加、移动、删除和创建碎片和碎片副本的放置规则。
A shard 是数据库中数据的水平分区。将数据划分为碎片,并将每个碎片的副本(称为“shard replicas”或仅仅是“replicas”)分发到集群中的不同节点,这样可以提高数据的耐用性,防止节点丢失。CouchDB自动地对数据库进行分片,并在节点之间分发组成每个分片的文档子集。修改集群成员资格和分片行为必须手动完成。
4.4.1.1. 碎片和副本¶
每个数据库有多少碎片和副本可以在全局级别设置,也可以按每个数据库设置。相关参数为 q
和 n
.
q 要维护的数据库碎片数。 n 是要分发的每个文档的副本数。的默认值 n
是 3
为了 q
是 2
. 用 q=2
,将数据库分成2个碎片。与 n=3
,集群会分发每个碎片的三个副本。总共,一个数据库有6个碎片副本。
在具有 q=8
,每个节点将收到8个碎片。在4节点集群中,每个节点将接收6个碎片。在一般情况下,我们建议集群中的节点数应该是 n
,以便碎片均匀分布。
CouchDB节点有一个 etc/default.ini
file with a section named cluster 看起来像这样:
[cluster]
q=2
n=3
这些设置为新创建的数据库指定默认的分片参数。这些可以在 etc/local.ini
复制上面的文本,并用新的默认值替换这些值。也可以通过指定 q
和 n
创建数据库时查询参数。例如:
$ curl -X PUT "$COUCH_URL:5984/database-name?q=4&n=2"
这将创建一个分为4个碎片和2个副本的数据库,生成分布在整个集群中的8个碎片副本。
4.4.1.2. 法定人数¶
根据集群的大小、每个数据库的碎片数量和碎片副本的数量,不是每个节点都可以访问每个碎片,但是每个节点都知道每个碎片的所有副本可以通过CouchDB的内部shard map找到。
每个进入CouchDB集群的请求都由任意一个随机协调节点处理。协调该请求的其他节点可能不包括该请求本身。协调节点在 quorum 的数据库节点已响应;默认情况下为2。仲裁的默认要求大小等于 r=w=((n+1)/2)
在哪里? r
指读取仲裁的大小, w
指写入仲裁的大小,以及 n
指每个碎片的副本数。在默认群集中,其中 n
是3, ((n+1)/2)
会是2。
注解
群集中的每个节点可以是任何一个请求的协调节点。群集中的节点没有特殊的角色。
可以在请求时通过设置 r
用于文档和视图读取的参数,以及 w
用于文档写入的参数。例如,以下是一个请求,该请求指示协调节点在至少两个节点响应后发送响应:
$ curl "$COUCH_URL:5984/{db}/{doc}?r=2"
下面是编写文档的类似示例:
$ curl -X PUT "$COUCH_URL:5984/{db}/{doc}?w=2" -d '{...}'
设置 r
or w
to be equal to n
(the number of replicas) means you will only receive a response once all nodes with relevant shards have responded or timed out, and as such this approach does not guarantee ACIDic consistency . 设置 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 endpoint将告诉您数据库的分片参数:
$ 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": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node4@127.0.0.1"
],
"20000000-3fffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
],
"40000000-5fffffff": [
"node2@127.0.0.1",
"node3@127.0.0.1",
"node4@127.0.0.1"
],
"60000000-7fffffff": [
"node1@127.0.0.1",
"node3@127.0.0.1",
"node4@127.0.0.1"
],
"80000000-9fffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node4@127.0.0.1"
],
"a0000000-bfffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
],
"c0000000-dfffffff": [
"node2@127.0.0.1",
"node3@127.0.0.1",
"node4@127.0.0.1"
],
"e0000000-ffffffff": [
"node1@127.0.0.1",
"node3@127.0.0.1",
"node4@127.0.0.1"
]
}
}
现在我们看到这个集群中实际上有4个节点,CouchDB已经将这24个分片副本均匀地分布在所有4个节点上。
我们还可以通过 /db/_shards/doc 终结点:
$ curl -s $COUCH_URL:5984/mydb/_shards/joan | jq .
{
"range": "e0000000-ffffffff",
"nodes": [
"node1@127.0.0.1",
"node3@127.0.0.1",
"node4@127.0.0.1"
]
}
$ curl -s $COUCH_URL:5984/mydb/_shards/robert | jq .
{
"range": "60000000-7fffffff",
"nodes": [
"node1@127.0.0.1",
"node3@127.0.0.1",
"node4@127.0.0.1"
]
}
CouchDB向我们展示了两个示例文档映射到的特定碎片。
4.4.3. 移动碎片¶
在集群上移动碎片或执行其他碎片操作时,建议停止集群上的所有重硬作业。看到了吗 停止重新加固作业 了解更多详细信息。
本节介绍如何手动放置和替换碎片。当您确定集群太大或太小,并希望成功地调整集群大小时,或者您从服务器指标中注意到数据库/碎片布局不是最佳的,并且您有一些需要解决的“热点”时,这些活动是关键步骤。
考虑一个q=8和n=3的三节点集群。每个数据库有24个碎片,分布在三个节点上。如果你 add a fourth node 对于集群,CouchDB不会将现有的数据库碎片重新分发给它。这会导致负载不平衡,因为新节点只会承载加入集群后创建的数据库的碎片。为了平衡现有数据库中碎片的分布,必须手动移动它们。
在群集中的节点之间移动碎片涉及以下步骤:
- Ensure the target node has joined the cluster .
- 复制碎片和任何辅助文件 index shard(s) onto the target node .
- Set the target node to maintenance mode .
- 更新群集元数据 to reflect the new target shard(s) .
- 监视内部复制 to ensure up-to-date shard(s) .
- Clear the target node's maintenance mode .
- 再次更新群集元数据 to remove the source shard(s)
- 删除碎片文件和辅助索引文件 from the source node .
4.4.3.1. 复制碎片文件¶
注解
从技术上讲,复制数据库和辅助索引碎片是可选的。如果继续下一步而不执行此数据复制,CouchDB将使用内部复制来填充新添加的shard复制副本。但是,复制文件比内部复制快,尤其是在繁忙的群集上,这就是我们建议首先执行此手动数据复制的原因。
碎片文件位于 data/shards
CouchDB安装的目录。在这些子目录中是碎片文件本身。例如,对于 q=8
已调用数据库 abc
,以下是其数据库碎片文件:
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返回 404 Not Found
关于 /_up
端点,并确保它不参与对其碎片的常规交互式群集请求。一个正确配置的负载平衡器,它使用 GET /_up
检查节点的运行状况将检测到该404并将该节点从循环中移除,从而阻止向该节点发送请求。例如,要将HAProxy配置为使用 /_up
端点,使用:
http-check disable-on-404
option httpchk GET /_up
如果未设置维护模式,或者负载平衡器忽略此维护模式状态,则在执行下一步之后,集群在咨询相关节点时可能会返回不正确的响应。你不要这个!在接下来的步骤中,我们将确保这个shard在允许它参与最终用户请求之前是最新的。
要启用维护模式:
$ 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目标节点(必须已经是 joined to the cluster )应该托管给定数据库的碎片副本。
要更新集群元数据,请使用 /_dbs
数据库,这是一个内部CouchDB数据库,它将数据库映射到碎片和节点。此数据库在节点之间自动复制。只有通过特殊的 /_node/_local/_dbs
端点。
首先,检索数据库的当前元数据:
$ curl http://localhost/_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", "node1@xxx.xxx.xxx.xxx"],
["add", "00000000-1fffffff", "node2@xxx.xxx.xxx.xxx"],
["add", "00000000-1fffffff", "node3@xxx.xxx.xxx.xxx"],
…
],
"by_node": {
"node1@xxx.xxx.xxx.xxx": [
"00000000-1fffffff",
…
],
…
},
"by_range": {
"00000000-1fffffff": [
"node1@xxx.xxx.xxx.xxx",
"node2@xxx.xxx.xxx.xxx",
"node3@xxx.xxx.xxx.xxx"
],
…
}
}
以下是对该文件的简要分析:
_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", "node1@xxx.xxx.xxx.xxx"],
["add", "00000000-1fffffff", "node2@xxx.xxx.xxx.xxx"],
["add", "00000000-1fffffff", "node3@xxx.xxx.xxx.xxx"],
...
["add", "00000000-1fffffff", "node4@xxx.xxx.xxx.xxx"]
],
"by_node": {
"node1@xxx.xxx.xxx.xxx": [
"00000000-1fffffff",
...
],
...
"node4@xxx.xxx.xxx.xxx": [
"00000000-1fffffff"
]
},
"by_range": {
"00000000-1fffffff": [
"node1@xxx.xxx.xxx.xxx",
"node2@xxx.xxx.xxx.xxx",
"node3@xxx.xxx.xxx.xxx",
"node4@xxx.xxx.xxx.xxx"
],
...
}
}
现在你可以了 PUT
此新元数据:
$ curl -X PUT http://localhost/_node/_local/_dbs/{name} -d '{...}'
4.4.3.4. 强制同步碎片¶
2.4.0 新版功能.
无论是否将碎片预复制到新节点,都可以强制CouchDB将数据库中所有碎片的所有副本与 /db/_sync_shards 终结点:
$ 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
,shard replica已经准备好为数据提供服务,我们可以让节点退出维护模式。
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
.
首先,每个节点都必须使用zone属性进行标记。这将定义每个节点所在的区域。您可以通过在特殊 /_nodes
数据库,通过位于的特殊节点本地API终结点进行访问 /_node/_local/_nodes/{{node-name}}
. 添加窗体的键值对:
"zone": "{zone-name}"
对集群中的所有节点执行此操作。例如:
$ curl -X PUT http://localhost/_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将确保一个shard的两个副本将托管在zone属性设置为的节点上 {{zone-name-1}}
并且一个复制副本将托管在一个新的分区属性设置为 {{zone-name-2}}
.
这种方法非常灵活,因为您还可以在创建数据库时通过将放置设置指定为查询参数,在每个数据库的基础上指定分区,使用与ini文件相同的语法:
curl -X PUT $COUCH_URL:5984/{db}?zone={zone}
这个 placement
也可以指定参数。注意这个 will 重写确定已创建副本数量的逻辑!
请注意,您还可以使用此系统确保群集中的某些节点不为新创建的数据库承载任何副本,方法是为这些节点提供一个区域属性,该属性不会出现在 [cluster]
放置字符串。
4.4.5. 分裂碎片¶
这个 /_reshard 是一个用于碎片操作的httpapi。目前只支持分片。要执行碎片合并,请参阅中概述的手动过程 合并碎片 部分。
主要的互动方式 /_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
作业数需要密切关注,因为每个节点都有一个最大的重硬体作业数,并且在达到限制之后创建新作业将导致错误。在开始新的工作之前,最好把已经完成的工作删除。看到了吗 reshard configuration section 对于默认值 max_jobs
参数以及如何根据需要进行调整。
例如,如果作业已完成,要删除所有作业,请运行:
$ curl -s $COUCH_URL:5984/_reshard/jobs | jq -r '.jobs[].id' |\
while read -r jobid; do\
curl -s -XDELETE $COUCH_URL:5984/_reshard/jobs/$jobid\
done
然后看看db shard映射是什么样子是个好主意。
$ curl -s $COUCH_URL:5984/db1/_shards | jq '.'
{
"shards": {
"00000000-7fffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
],
"80000000-ffffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
]
}
}
在本例中,我们将拆分 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": "node1@127.0.0.1",
"shard": "shards/00000000-7fffffff/db1.1554242778"
},
{
"ok": true,
"id": "001-cec63704a7b33c6da8263211db9a5c74a1cb585d1b1a24eb946483e2075739ca",
"node": "node2@127.0.0.1",
"shard": "shards/00000000-7fffffff/db1.1554242778"
},
{
"ok": true,
"id": "001-fc72090c006d9b059d4acd99e3be9bb73e986d60ca3edede3cb74cc01ccd1456",
"node": "node3@127.0.0.1",
"shard": "shards/00000000-7fffffff/db1.1554242778"
}
]
请求返回三个作业,三个副本各一个作业。
要检查这些作业的进度,请使用 GET /_reshard/jobs
或 GET /_reshard/jobs/{{jobid}}
.
最终,这些工作应该完成,碎片图应该如下所示:
$ curl -s $COUCH_URL:5984/db1/_shards | jq '.'
{
"shards": {
"00000000-3fffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
],
"40000000-7fffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
],
"80000000-ffffffff": [
"node1@127.0.0.1",
"node2@127.0.0.1",
"node3@127.0.0.1"
]
}
}
4.4.6. 停止重新加固作业¶
可以停止集群级别的重新加固,然后重新启动。这有助于允许外部工具操纵碎片映射,以避免干扰重硬作业。要停止群集上的所有重新加固作业,请发出问题a PUT
到 /_reshard/state
具有 "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值指定为查询参数,使用所需的shard设置创建一个临时数据库。
- 停止客户端访问数据库。
- 将主数据库复制到临时数据库。如果主数据库处于活动使用状态,则可能需要多个复制。
- 删除主数据库。 确保没人在用它!
- 使用所需的碎片设置重新创建主数据库。
- 客户端现在可以再次访问数据库。
- 将临时复制回主服务器。
- 删除临时数据库。
完成所有步骤后,可以再次使用数据库。集群将根据放置规则自动创建和分发碎片。
如果可以指示客户机应用程序使用新数据库而不是旧数据库,并且在非常短的停机时间内执行切换,则可以避免生产中的停机。