做 OpenStack 运维久了,Live Migrate 是个绕不开的操作。中文叫法很多,在线迁移、热迁移、动态迁移,说的都是同一件事:让一台正在运行的云主机,不关机不打断业务,从当前计算节点搬到另一台计算节点上。在虚拟化和运维这两个圈子里,它算得上云平台最能体现“高级感”的能力之一。
我第一次在人前演示这个操作的时候,手心里其实全是汗。命令敲下去,源节点上的实例状态开始变化,目标节点冒出同名实例,业务侧一个 ping 都没断,整个过程像变魔术。后来在生产环境里被它折腾过几次,才真正明白:Live Migrate 这玩意儿,懂原理、按流程、留后手,它是运维的救命技能;要是随手乱用、不看共享存储不看 CPU,它就是给自己埋雷。
这篇不打算抄官方文档,我从一个实际维护过 OpenStack 集群的角度,把热迁移的适用场景、底层逻辑、命令行操作和排障实录串一遍。适合正在做 OpenStack 运维、虚拟化平台支撑,或者准备考云计算相关认证的工程师参考。先说好,所有操作都以测试环境优先,别一上来就在生产集群开火。
1. 为什么需要 Live Migrate:从一次凌晨的磁盘告警说起
1.1 热迁移到底解决了什么痛点
想象一个很现实的场景:一台物理计算节点的磁盘开始报 SMART 错误,或者 RAID 卡日志里出现了越来越多无法纠正的读错误。你作为运维,必须把上面所有云主机挪走,但又不能让业务停机。这时候你就会意识到 Live Migrate 不是“锦上添花”,而是“刚需”。
往大了说,热迁移的核心价值可以概括成三个词:不中断、可调度、可维护。不中断是业务层面,虚机内部跑的 nginx、数据库、消息队列不会感知到底层物理位置的变动;可调度是资源层面,集群里哪台机器空闲、哪台繁忙,管理员可以像搬快递一样把负载均衡到不同节点;可维护是运维层面,补丁升级、内核替换、硬件维修都能在一个可控窗口内完成,不用再约业务方半夜停机。
这类操作通常被用在下面几个典型场景里:
硬件故障前预防性迁移,比如磁盘、内存、网卡出现早期告警时,先把业务挪走。
计算节点负载不均衡,一部分节点 CPU 占用率很高,另一部分闲着,通过热迁移做资源梳理。
计算节点需要整个下线维护,比如更换物理机、升级宿主机操作系统、迁移机房。
集群整体缩容,把散落在多台低负载节点上的实例集中到少量节点上,然后释放物理机。
这些场景的共同点就是一个字:急。越是急的时候,越不能靠关机迁移,因为业务不可用每多一分钟,带来的后果都可能被放大。热迁移的价值不在平时,而是在这种“窗口极短、要求极高”的时刻。
1.2 冷迁移和热迁移怎么选
很多刚接触 OpenStack 的朋友会把“迁移”当成一个词,其实在 Nova 里至少要分两种:冷迁移(Cold Migration)和热迁移(Live Migration)。Cold Migration 会先把实例关机,把磁盘数据搬到目标节点,再开机;Live Migration 则是实例保持运行,内存状态被同步到目标节点,业务几乎无感。
我用一个表格把两者的区别列一下,方便你选型时参考:
| 对比项 | 冷迁移 | 热迁移 |
|---|---|---|
| 业务中断 | 有,必须关机 | 基本无感知,但可能造成毫秒级暂停 |
| 耗时 | 取决于磁盘大小,通常较慢 | 取决于内存大小和脏页速率,可能很快也可能很慢 |
| 对存储依赖 | 本机磁盘或共享存储都行 | 共享存储最稳,本地磁盘需配合 block migration |
| 失败风险 | 低 | 中高,条件不满足时容易失败 |
| 运维场景 | 业务允许做计划停机 | 业务不能接受停机 |
| 调度方式 | 可指定目标节点 | 可指定或由调度器选择 |
我自己的习惯是:能不做热迁移就不做,但是业务一旦要求“零停机”,就只能硬着头皮上热迁移。所以你要明白一个前提:冷迁移不是热迁移的低配版,而是另一个适用场景的产物。比如要对虚机磁盘做跨存储池搬迁,或者底层真的不太稳定时,冷迁移反而是更安全的选择。
1.3 哪些场景不该用热迁移
热迁移虽然听起来万能,但它并不是所有场景的答案。我踩过几次坑之后,总结出几类不适合用热迁移的情况,你可以当做一个反例清单。
先说大内存高负载实例。假设一台云主机有 256G 内存,内部跑着一个持续写内存的 Java 应用,脏页产生的速度比网络同步速度还快,这种实例热迁移很难收敛,甚至会一直卡在 migrating 状态。遇到这种情况,要么提前在业务低峰期迁,要么用冷迁移。
再说底层存储已经明显损坏的情况。热迁移读源节点磁盘,如果磁盘已经坏到连源数据都快读不出来了,迁移过程中极可能报 I/O 错误,反而加速节点崩溃。这时候不如先把实例紧急冷迁移或备份。
还要注意直通设备场景。有些实例使用了 PCI passthrough、SR-IOV、GPU 直通这类把物理设备直接分给虚拟机的特性,这类实例通常不能直接做标准 Live Migrate,需要借助专门的 vDPA 或相关增强特性,否则迁移会失败。
最后是资源余量不足的情况。热迁移要求目标节点能承接源实例的内存和 CPU 占用,如果目标节点内存本来已经吃紧,迁移上去之后可能出现内存超分严重、虚机被 OOM 的风险。迁移前不看资源余量,等于把马路中间的车强行换上路况不明的路,翻车概率极高。
2. 开工前先弄懂背后的技术原理
2.1 迁移流程里每一环是谁在干活
Live Migrate 不是简单的一条命令,而是一个多组件协同的过程。了解每一环在干什么,排障时才能快速定位问题。
一条执行链大致是这样的:用户发起迁移请求后,请求先进 Nova API,随后交给 nova-conductor 和 scheduler。scheduler 根据目标节点的资源、可用域、CPU 型号等信息选出一个目标计算节点。接着,源节点上的 nova-compute 接收 RPC 消息,开始调用底层 libvirt;libvirt 会通过 QEMU 的迁移线程,把虚拟机的内存状态推送到目标节点上的 libvirtd。目标节点上的 nova-compute 再负责把新出现的虚拟网络端口绑定到本机的虚拟交换机网桥上,把虚拟机“接”进网络。
可以理解成有两个仓库,源仓库正在发货,目标仓库准备收货。nova 是调度系统,负责告诉双方“什么时候搬、搬到哪”;libvirt 是中间人,负责搬运过程中的握手和状态同步;QEMU 则是真正执行搬家的人,它把内存页一份份拷过去,直到全部到位。
迁移期间,源节点和目标节点上会同时出现同一个实例。源节点上的实例进入 migrating 或 paused 状态,目标节点上则先有一个 incoming migration 的虚拟机,等内存传输完毕后才真正运行起来。也就是说,在迁移过程中,这台虚机“暂时的分身”是存在的,这个是正常现象,别看到两个同名实例就慌了。
2.2 QEMU 的内存迁移是怎么做到“不感知”的
热迁移能保持业务不断,单纯看表象会觉得玄乎,其实底层机制在虚拟化领域已经非常成熟。QEMU 使用的主流方式叫预拷贝(pre-copy),原理很简单:先把虚拟机的全部内存页从源节点传输到目标节点;在传输期间,虚拟机还在继续运行,因此有些内存页会被修改,这些被修改过的页面称为脏页;紧接着把脏页再传一遍,如果传输过程中又产生了新的脏页,就继续传,直到脏页数量收敛到一个很小的阈值。
当剩余脏页足够少时,QEMU 会短暂暂停源虚拟机,停止所有 CPU 线程,把最后一小批脏页同步过去,然后在目标节点恢复运行,最后通知源节点清理残留数据。这个暂停时间通常在几十毫秒到几百毫秒之间,对绝大多数业务来说几乎无感。
这里涉及几个关键参数,生产环境排障时非常有用:
迁移带宽:Nova 配置里可以设置
live_migration_bandwidth,限制热迁移使用的网络带宽上限,单位是 Mbps 或者 MiB/s,按你的部署版本而定。带宽设置过小,大内存实例会迁移很慢。最大停机时间:QEMU/libvirt 侧的
migration_max_downtime影响最后一轮同步的阈值,设置过小会导致收敛时间变长,设置过大可能导致业务感知到明显卡顿。迭代次数和收敛速度:预拷贝不是无限迭代的,当达到最大迭代次数或者脏页收敛速度不够快时,会强制进入停机拷贝阶段。
你可以这样理解:系统先在两个仓库之间把所有货物拉一遍,然后一遍遍补发新产生的货物,等到新货数量少了,最后临时锁门几秒钟,把剩余货物送完,再开门营业。这套机制保证绝大多数内存页面实际上都是在线传输的,真正停机的窗口非常短。
2.3 为什么必须关注 CPU 和共享存储
很多迁移失败都发生在两个地方:CPU 不兼容、存储不共享。这两点必须在迁移前判断好。
先说 CPU。QEMU 迁移要求目标节点的 CPU 逻辑特性至少覆盖源节点虚拟机使用的全部特性。如果源节点是 Intel 的 Cascadelake,目标节点是同样品牌的 Broadwell,或者干脆一台 Intel 一台 AMD,虚拟机在目标节点上往往起不来,或者运行到一半触发非法指令。解决办法是尽量统一集群的 CPU 型号,同时在 nova.conf 里合理配置 CPU mode。常用的 mode 有三种:
custom:指定具体 CPU 模型,兼容性最可控,但可能浪费新 CPU 的部分特性。host-model:宿主机有多强,虚拟机就自动选择最接近的模型,比较灵活,但跨代迁移仍可能受限。host-passthrough:虚拟机直接使用物理 CPU 的全部特性,性能最好,但要求目标节点硬件高度一致,否则迁移兼容性最差。很多部署默认会用
host-model或host-passthrough,这在同机房同批次的硬件下问题不大,但如果你有两批不同代的服务器混在一个集群里,就一定要提前检查 CPU 兼容。我踩过的坑是:一批新节点和一批老节点混用,没注意 CPU 差异,热迁移五台有三台失败,失败原因清一色是 CPU 特性不兼容。后来我把新节点和老节点做了分组,尽量只在相同硬件代际之间迁移,问题才消停。
再说共享存储。热迁移如果只需要搬内存,那么速度极快,前提是源节点和目标节点能看到同一份虚拟磁盘。这个“同一份”可以通过两种方式实现:一是使用共享存储,比如 NFS、Ceph RBD、GlusterFS;二是让 nova 的--block-migrate在迁移时把本地磁盘一块块复制到目标节点。共享存储模式下,目标节点只需要重新挂载同一个镜像路径;本地磁盘模式下,迁移时间会被磁盘大小和网络速度直接影响,失败概率也成倍增加。
这也是为什么生产环境里大家偏爱 Ceph RBD 的原因之一:虚拟机磁盘本身在 Ceph 集群里,计算节点只是通过 RBD 协议访问同一个块设备。热迁移时不需要复制磁盘,只需同步内存,速度快,逻辑也简单。
3. 实战:从环境检查到迁移完成
3.1 动手前的一页纸检查清单
我有一个原则:宁可多花五分钟检查,也不要迁移到一半去救火。下面这份检查清单是我每次操作之前必过的,你可以直接抄下来:
- 计算服务状态:所有 nova-compute 必须是 UP。用
openstack compute service list看,如果有某个节点已经 down,千万别把目标指向它。 - 目标节点资源余量:用
openstack hypervisor show <host>查看 free RAM 和 vCPU。实例迁移后要占用目标节点内存,必须预留足够空间,别已经超卖了还硬塞。 - 存储状态:如果使用 NFS,检查源和目标节点都能
df -h看到共享目录;如果使用 Ceph,检查ceph -s集群状态是 HEALTH_OK。 - 网络代理状态:
openstack network agent list确认目标节点上的 neutron agent 是 alive,否则目标节点没法把虚机接入网络。 - 实例当前状态:
openstack server show <server>,status 是 ACTIVE,task_state 为空。如果实例本身正处于创建、快照、调整配置等任务中,不要发起迁移。 - CPU 兼容性:
openstack hypervisor show <host>看cpu_info,或者直接看 nova-compute 日志里的 CPU 模型,判断两个节点是否同代。 - 安全组与浮动 IP:确认这个实例有正确的安全组配置,迁移过程中浮动 IP 不会被踢掉。
这套检查不是走过场。有一次我实在犯懒,没看目标节点 RBD 连接,结果迁移到一半提示找不到磁盘设备。后来回头查,发现目标节点上的 Ceph 配置文件和源节点不一致,连的认证 key 不同。从那次以后,存储连接是我每次必查的第一项。
3.2 用命令行完成一次热迁移
命令行操作是最可靠的方式,我用一台 Victoria 版本的测试环境举例。先找到要迁移的实例:
openstack server show 8d1f4d9e-3c2a-4b8e-8e98-6e0f2f6a5c1d确认状态正常后,找到可用的目标计算节点:
openstack hypervisor list使用 nova 命令指定目标节点发起热迁移:
nova live-migration 8d1f4d9e-3c2a-4b8e-8e98-6e0f2f6a5c1d compute-node-02如果不指定目标节点,会由 scheduler 自动选择:
nova live-migration 8d1f4d9e-3c2a-4b8e-8e98-6e0f2f6a5c1d如果你的虚机磁盘是本机磁盘,又想用热迁移,需要加--block-migrate参数,把磁盘一起复制过去:
nova live-migration --block-migrate 8d1f4d9e-3c2a-4b8e-8e98-6e0f2f6a5c1d compute-node-02注意,发起命令后不代表迁移完成。用这条命令查看迁移状态:
openstack server migration list --server 8d1f4d9e-3c2a-4b8e-8e98-6e0f2f6a5c1d输出里会看到类似queued、prepping、migrating、completed的状态。如果长时间停留在prepping或migrating,就得按后面“常见问题”部分排查了。
迁移完成后,再查看实例所在宿主机是否已经变化:
openstack server show 8d1f4d9e-3c2a-4b8e-8e98-6e0f2f6a5c1d看OS-EXT-SRV-ATTR:host字段,如果是compute-node-02,那基本就成功了。我习惯在迁移结束后再跑一次ping测试业务 IP,确认网络链路干净。
3.3 在 Dashboard 上点击操作
如果你们团队给业务方开放了 Horizon,或者你手头没有安装 python-openstackclient,那在界面上也能做热迁移。登录到 Horizon 控制台后,进入“项目”页面下的“实例”列表,找到目标实例,在行尾的下拉菜单里找“在线迁移”或者“Live Migrate”按钮。不同版本按钮名称可能叫“迁移”“在线迁移”“Live Migrate”,本质都是触发同一个 Nova API。
点击后,页面会弹出一个对话框,让你选择目标宿主机。如果不选,就由调度器自动选择。这里建议业务方或操作者尽量手动明确一个目标节点,不要完全交给 scheduler,尤其当集群里存在不同硬件代际时,手工指定能减少 CPU 不兼容带来的麻烦。
Dashboard 的优点是直观,缺点是操作反馈弱。界面上点完迁移,你不会看到太细的过程日志,还是得靠命令行去查openstack server migration list。所以我的建议是:生产环境优先用命令行,界面操作留给演示或非技术人员用。
3.4 参数解析:block migration 和调度方式
很多人会被--block-migrate这个参数搞晕。简单说,它就是“把磁盘也一起搬走”的意思。在共享存储模式下,源和目标节点都能访问同一份磁盘文件,不需要复制磁盘;在非共享模式下,目标节点看不到源节点的本地磁盘,就必须把磁盘文件完整复制过去,这时候就必须加--block-migrate。
需要注意,--block-migrate会大幅增加迁移时长。假设一台云主机有 100G 本地磁盘,就算网络速度不错,拷完这块盘也要几分钟到几十分钟。而且块复制过程中如果出现网络抖动,迁移失败的风险会显著升高。因此有条件的环境务必上共享存储,把热迁移的操作成本降下来。
关于调度方式还有一个细节:nova live-migration <server>交给 scheduler 自动选择目标节点,但如果集群里所有节点资源都紧张,scheduler 可能直接报错。另外在某些版本中,指定 host 的语法有一定差异,如果你手头的 nova 命令支持,在命令末尾显式写目标主机名最稳。比如:
nova live-migration --block-migrate <server-id> <target-host>如果你的环境习惯用 openstack 命令而不是 nova 命令,也可以这样触发:
openstack server migrate --live <server-id> --wait--wait会让命令行一直等待迁移完成,适合在脚本里用。不过 openstack 命令不一定能显式指定目标主机,想指定时还是回到 nova 命令比较省事。
3.5 迁移完成后的验证和回退手段
命令输出 completed 不代表可以高枕无忧,我每次还会做四个验证:
openstack server show <id>确认状态是 ACTIVE,host 已经变为目标节点。- 在目标计算节点上执行
virsh list,能看到这个实例的 domain 正在运行。 - 登录实例内部看
uptime,系统启动时间没有变化,说明虚拟机内部一直没有重启。 - 业务侧发送几个测试请求确认网络和中间件链路正常。
如果迁移后发现问题,比如目标节点性能异常、网络丢包、业务应用报错,最直接的回退手段就是再发起一次反向热迁移,把实例迁回原节点。但注意,反向迁移前要先查清楚问题原因,如果是因为目标节点硬件故障导致的异常,盲目迁回原节点也是给自己找麻烦。
还有一些版本支持abort操作来中止未完成的迁移,但生产环境里我不建议中途强行中止,容易留下半迁移状态,处理起来比等待迁移完成更麻烦。平时尽量让迁移自然完成,除非看到它已经明显卡死超过合理时间。
4. 各种翻车现场与排查技巧
4.1 迁移卡在 migrating 不动
这是群里问得最多的一个问题:迁移状态停在migrating半个小时不动,虚机也还活着,但就是迁不完。按我排查的经验,先不要急,按下面顺序看:
第一,看源节点 nova-compute 的日志:
tail -f /var/log/nova/nova-compute.log | grep -i migrate如果日志里反复出现“Cannot get memory dirty rate”或者 migration 线程慢得像蜗牛,基本可以判断是脏页收敛不了。第二,检查迁移网络带宽是否被打满。热迁移走的是管理网络或专用迁移网络,如果这个网段还在跑 ceph 同步、备份流量,迁移带宽会被抢占。
第三,看看源虚机内部的负载。高负载大内存实例在业务高峰期很难收敛,因为刚传完的内存页马上又被写过。处理办法是:把迁移窗口挪到低峰期,或者在允许的情况下临时降低实例负载;如果都不行,只能中止迁移改走冷迁移。
我自己遇到过一次最夸张的,迁移一台 128G 内存的数据库实例,整整卡了 40 分钟。后来把live_migration_bandwidth从默认值调到 500Mbps,又等了几分钟就收敛完成了。脏页速率再高,只要带宽给足,收敛概率就会大幅上升。
4.2 报错无法获取虚拟机磁盘/存储连接失败
报错信息里经常能看到这样的关键字:disk not found、Cannot access storage、failed to connect to socket。这类问题八成出在存储层。
排查先从目标节点看共享存储挂载。如果是 NFS:
df -h | grep /var/lib/nova/instances showmount -e nfs-server如果是 Ceph RBD,先看集群状态:
ceph -s检查计算节点上 RBD 映射和 nova.conf 里的[libvirt] images_type = rbd配置是否一致。常见问题包括目标节点的 Ceph keyring 过期、目标节点根本看不到 RBD 池、共享存储路径在源和目标节点上不一致。
还有一个容易忽略的点:NFS 挂载选项。如果源节点挂载时用的是rw,noatime,目标节点挂载时却用了ro,或者挂载点路径少了一层目录,Nova 在迁移时同样访问不到磁盘文件。所以存储路径的一致性检查一定要做,不要只看“有挂载”,还要看“挂载在哪、权限对不对”。
日志位置:源节点 nova-compute.log 和目标节点 nova-compute.log 里都会有更详细的异常堆栈,排障时两个节点一起看,对比找线索最快。
4.3 CPU 不兼容引发的迁移失败
报错信息类似:
Target host CPU model does not match source operation failed: guest processor ... not supported如果你看到这类报错,基本就是 CPU 型号差异。处理办法没有一个万能公式,但有经验法则:
- 优先在同型号、同代次的机器之间迁移。集群规划时就不要搞“Intel 和 AMD 混用、四代和三代混跑”。
- 检查 nova.conf 里的 libvirt cpu_mode。如果集群内部存在跨代 CPU,用
host-model比host-passthrough更容易兼容,但它也不是万能药,目标机器太老仍然会失败。 - 如果业务对性能要求不是极致,也可以考虑把 CPU mode 设为统一的
custom模型,比如Skylake-Client,让所有虚拟机都固定使用这一套 CPU 特性,这样跨节点迁移时兼容性最强。代价是新硬件的某些特性被遮蔽掉。
我在测试环境里试过把两台不同代的 Intel CPU 节点混在一个 cell 里,热迁移十次能失败三次。后来把新节点单独建了一个 aggregate,并且配置了 CPU 亲和策略,才彻底告别这类问题。如果你的集群规模不大,最简单的办法就是物理上区分资源池,不让跨代迁移发生。
4.4 迁完机器起来了但业务网络不通
这是最隐蔽的一类问题,因为从 Nova 视角看迁移已经完全成功,虚机也 ACTIVE,但业务方反馈服务“有响应但访问超时”,或者“虚拟机内部网络不通”。
先做快速定位:进入目标节点检查虚机网卡绑定情况。
virsh domiflist <domain-id>如果返回的 vif 设备对应的 bridge 名和源节点不一致,说明网络配置出了问题。再查 neutron 侧的端口绑定:
openstack port show <port-id>看binding:host_id字段,正常应该变为目标计算节点的主机名。如果显示的还是源节点,说明 Nova 和 Neutron 之间的 VIF plug 事件没有处理干净,需要在 nova-compute 日志里找plug vif相关记录。
检查目标节点上的 Open vSwitch 或 Linux Bridge agent 状态:
openstack network agent list确保 agent 是 alive 且已经配置了正确的物理网桥。我碰到过一次案例:目标节点上的 OVS agent 半死不活,网络端口虽然绑定过去了,但数据通路没有建立,直到重启 agent 才恢复。所以迁移前检查 neutron agent 状态不是口头上说说,真的能保命。
另外,跨节点迁移后,虚拟机的 ARP 表项和交换机 MAC 表可能需要一点时间刷新,如果你用了传统 VLAN 网络而不是隧道网络,这个现象会更明显。一般等几十秒会自愈,如果长时间不通,再往上排查物理交换机端口配置。
4.5 我总结的避坑清单
最后放一份压箱底的避坑清单,都是真金白银换来的教训:
- 不要一次并发迁移太多实例。别尝试一口气迁 20 台,存储和网络都会被打爆。我会控制在 5 台以内,并且每批之间留几分钟让存储缓冲。
- 迁移前检查目标节点的内存余量。热迁移会把实例剩余内存迁过去,目标节点内存不够时,虚机可能被 OOM,那不是迁移失败的问题,是直接业务宕机。
- 注意迁移带宽限制。如果集群里所有迁移流量和业务流量共用一张网络,最好在 Nova 配置里限制 live_migration_bandwidth,否则热迁移能把网络打满,正常业务先受影响。
- 迁移窗口避开业务高峰期。别在双十一、月底出账这种时刻搞大规模迁移,除非你想体验业务方连环夺命 call。
- 迁移之前把实例的 guest agent 和内部服务状态确认一遍。某些老系统内部网卡配置和云平台配置不匹配,迁移过去后虽然虚拟网卡还在,但系统内部可能因为 udev 规则导致网络起不来。
- 永远留一条回退路径。无论是反向迁移还是备份快照,确保你还有一张牌能打,再开始操作。
热迁移对 OpenStack 运维来说,就像开车时的变道超车。平时顺畅的时候,它只是个普通动作;一旦路况复杂、车辆状态不好,技术不到家的人很容易把整个车带沟里。但这些坑并不是不可控的,只要你按流程检查、按原理排障,大多数 Live Migrate 问题其实都能在十分钟内定位到根因。
最后再分享一个我自己的小习惯:发起迁移之后,不要傻等,直接在另一个终端里跑一个轮询脚本,每秒检查一次迁移状态,一旦异常马上弹日志,效果比一直盯着openstack server migration list舒服得多。类似下面这样,用你的实例 ID 替换变量即可:
while true; do STATUS=$(openstack server migration list --server $INSTANCE_UUID -f value -c Status) echo "$(date +%H:%M:%S) $STATUS" if echo "$STATUS" | grep -q completed; then break fi sleep 5 done这样既不会漏掉状态变化,也不会让你的终端刷屏刷到看不到重点。Live Migrate 本身不复杂,但它是典型的“九十九次正常,一次翻车就要命”的操作,所以请一定把准备工作做扎实。