最近在做一个企业内部知识库问答系统,第一步选型就锁定了 Milvus。原因很直接:它是我接触过的向量数据库里生态最完整的那个,从单机到分布式路径平滑,社区资料也多。部署方式我没走裸机装依赖那条路,而是直接用 Docker 把整套环境拉起来。今天这篇就是把整个部署过程复盘一遍,包括 compose 文件、Windows 和 Linux 上的环境坑、第一次用 pymilvus 写入检索的完整链路,以及我实际踩过的几个性能和排查问题。
文章适合这几类人看:刚接触向量数据库、想在本地或服务器上用 Docker 跑一个 Milvus 实例的开发者;做 RAG 知识库但不确定怎么把向量检索和业务数据接起来的人;以及已经在跑 Milvus,但对数据持久化、备份和常见报错还有些含糊的人。我不会只给你能跑起来的配置,还会把每个配置为什么这么写、踩坑时怎么定位都讲清楚。
1. 先搞清楚 Milvus 在整套系统里到底扛什么活
1.1 向量数据库和普通数据库的差别
很多人一上来就装 Milvus,但没想明白它和 MySQL、PostgreSQL 这类数据库到底有什么不一样。普通数据库适合精确匹配:给你一个用户 ID,把那一行查出来;给你一个商品分类,把属于这个分类的记录筛出来。向量数据库处理的是“找相似”的问题:给你一段文本的向量表示,找出库里和它语义最接近的 TopK 条。
这个差别直接决定了使用方式和“语言”。MySQL 的交互语言是 SQL,Milvus 的交互语言是 pymilvus 或 MilvusClient 这类 SDK。你不需要像 SQL 一样写复杂的 JOIN,而是按 collection、partition、index、search 这套概念来组织数据。collection 可以简单理解成 MySQL 里的表,只不过它存的不是一行行的结构化记录,而是一批向量和附带在向量旁边的标量字段。
做向量检索时,文本、图片、音频都会先被模型转成一串浮点数,也就是 embedding。Milvus 本身不做这个转换,它只负责把这些 embedding 存下来,并在你发起检索时按距离度量做相似度排序。常见距离度量有三种:IP 内积、L2 欧氏距离、COSINE 余弦相似度。选择哪个取决于你用的 embedding 模型是否做过归一化,这个细节后面讲到检索结果时再展开。
1.2 Milvus 单机版为什么要拆成三个容器
我见过不少人在 Docker 里直接docker run milvusdb/milvus一把梭,然后数据一删容器就丢了个精光。问题就出在没搞明白 Milvus 单机版的架构:它不是一个单体进程,而是由 etcd、MinIO、Milvus 三个核心组件协作。
etcd 负责元数据,相当于整个集群的“值班登记表”,记录你创建了哪些 collection、哪个 segment 在哪个节点、索引状态如何。MinIO 负责对象存储,真正的向量数据文件和索引文件最终落到这里。Milvus 本体负责计算,接收 SDK 请求、构建索引、执行检索。
这三个角色可以分成两个容器、三个容器,甚至各多副本一起跑。但官方单机版默认推荐三容器编排,而不是把 etcd 和 MinIO 全塞进 Milvus 镜像里。原因很简单:拆开之后每个组件都可以独立扩展、独立换版本、独立做数据持久化。你想想,如果全部挤在一个容器里,etcd 的 WAL 文件和 MinIO 的 chunk 文件混在一起,备份和恢复会非常痛苦。
1.3 什么时候你真的需要 Milvus
不是所有相似检索场景都必须上专门的向量数据库。比如数据量只有几千条、对延迟又不敏感,用 Python 在内存里算一遍余弦相似度完全够用。但一旦数据量上到几十万、几百万,或者你需要在检索时同时做标量过滤,比如“只查某个租户的文档”,在内存里硬扫就不现实了。
Milvus 的典型适用场景是 RAG 知识库问答、以图搜图、推荐系统的召回阶段、异常日志聚类。它的优势在于把索引构建、分段存储、标量过滤和向量检索整合在一起,而不是让你自己拼装 Faiss 加 Redis 加关系库。拼装方案也能用,但需要操心的事情太多:索引存哪里、过滤怎么做、服务怎么水平扩展。Milvus 把这些统一了,代价是你需要先理解它那套 collection、index、partition 的模型。理解清楚之后,后续怎么调优都有方向。
2. 部署前的硬检查:Windows 和 Linux 各有哪些绕不过去的坎
2.1 Windows 上 Docker Desktop 启动失败的经典报错
如果是在 Windows 上部署,很多人会遇到这条报错:
Docker Desktop failed to start because virtualization support wasn't detected我第一次看到这条报错也以为是自己电脑太老,后来才发现大部分情况不是硬件不支持,而是虚拟化功能没开全。Docker Desktop 在 Windows 上依赖两套虚拟化方案,一个是 Hyper-V,一个是 WSL2。现在默认推荐 WSL2。
如果报这个错,先按下面顺序排查。
第一,进 BIOS 确认 CPU 的虚拟化开关是开的。Intel 对应 VT-x,AMD 对应 SVM Mode。这个选项在部分品牌机里默认是关的,找“Virtualization"或"Intel Virtualization Technology”改 Enabled。
第二,以管理员身份打开 PowerShell,确认 Windows 功能里有没有启用“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。可以直接执行下面两条命令补上:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完重启,再把 WSL 默认版本设成 2:
wsl --set-default-version 2第三,如果还是启动失败,打开命令行执行wsl --status看 WSL 内核是否正常。有些人电脑上之前装过旧版 WSL,内核版本过老,用wsl --update更新到新内核就能解决。
还有一类公司电脑,组策略把 Hyper-V 相关功能锁死了,这种就不是自己电脑能解决的问题,要么找 IT 开权限,要么干脆换一台 Linux 机器或云服务器。
2.2 Linux 服务器上安装 Docker 的正确姿势
服务器上部署比 Windows 省心,但有一个前置问题:很多 Linux 发行版自带的 Docker 版本太旧,对 Compose V2、健康检查这些特性的支持不完整。所以建议直接用官方脚本装 Docker CE,然后在同一台机器上确认 compose 插件存在。
curl -fsSL https://get.docker.com | sh sudo systemctl enable --now docker docker compose version如果你是在没有外网的环境中离线安装,那就得提前把 Docker 安装包和需要的镜像文件准备好,再用docker load导入。离线安装最容易漏的是 compose 插件,记得单独下载docker-compose-plugin的对应版本。
装完之后还有个高频问题:非 root 用户执行 docker 命令报权限错误,类似:
Got permission denied while trying to connect to the Docker daemon socket这不是什么复杂问题,就是把当前用户加进 docker 组。
sudo usermod -aG docker $USER newgrp docker加入 docker 组之后,当前终端可以立即生效,新开的终端也不会再报这个错。
2.3 资源规划:别在小内存机器上硬跑
Milvus 单机版看起来只是几个容器,但内存占用比想象中大。我实际观察过一套刚启动完、没有任何数据写入的 Milvus 2.4 环境,etcd 大概占 400-600MB,MinIO 占 600MB 到 1GB,Milvus 主进程根据配置不同会占 2GB 到 4GB。也就是说,光空跑一套环境,建议至少留 8GB 内存。等数据量上来、开始建索引后,HNSW 这类基于内存的索引会再吃掉一部分内存。
下面是个人建议的最低配置和推荐配置:
| 配置项 | 最低标准 | 推荐标准 |
|---|---|---|
| CPU | 2 核 | 4 核及以上 |
| 内存 | 8GB | 16GB 及以上 |
| 磁盘 | 30GB 可用 | 100GB SSD 及以上 |
| 操作系统 | Windows 10/11 或主流 Linux | Linux 服务器 |
磁盘建议用 SSD,尤其是索引构建和检索量大的情况下,SSD 和机械盘差距非常明显。MinIO 底层是对象存储,但读写都是真刀真枪的磁盘 IO,机械盘在并发写入时容易成为瓶颈。
2.4 拉取镜像慢的应对办法
如果镜像拉取一直转圈或者超时,优先检查 Docker 镜像加速配置。中国网络环境下,直接连 Docker Hub 经常不稳定,可以编辑/etc/docker/daemon.json(Windows 上对应 Docker Desktop 的 Docker Engine 配置)加上 registry-mirrors:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.net" ] }改完daemon.json后重启 Docker 服务:
sudo systemctl restart docker这里要注意,不同镜像加速站点的可用性会随着时间变化。如果某个加速器失效,表现出来的症状往往是“从 Docker Hub 拉镜像时卡在等待状态”,而不是直接报错。遇到这种情况,换一个可用镜像源再试就行。Windows 上修改 Docker Engine 配置后,重启 Docker Desktop 即可。
3. 用 docker-compose 拉起 etcd、MinIO 和 Milvus 三件套
3.1 一份能直接用的 compose 文件
我习惯把整套环境放在一个目录下,用 docker-compose 统一管理。下面这份 compose 文件是基于 Milvus 2.4 官方编排改的,适合单机部署,也保留了健康检查和数据卷挂载。
services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.18 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 - ETCD_SNAPSHOT_COUNT=50000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd healthcheck: test: ["CMD", "etcdctl", "endpoint", "health"] interval: 30s timeout: 20s retries: 3 minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin MINIO_ACCESS_KEY_ID: minioadmin MINIO_SECRET_ACCESS_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data ports: - "9000:9000" - "9001:9001" command: minio server /minio_data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 milvus: container_name: milvus image: milvusdb/milvus:v2.4.13 command: ["milvus", "run", "standalone"] security_opt: - seccomp:unconfined environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 MINIO_ACCESS_KEY_ID: minioadmin MINIO_SECRET_ACCESS_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - "19530:19530" - "9091:9091" depends_on: etcd: condition: service_healthy minio: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"] interval: 30s start_period: 90s timeout: 20s retries: 3这份文件里几个关键点需要解释一下。
depends_on配合condition: service_healthy是为了让 Milvus 容器在 etcd 和 MinIO 健康检查通过之后再启动,避免三容器同时启动时 Milvus 因为连不上 etcd 而反复重试。seccomp: unconfined是官方建议,主要解决某些内核版本下容器内进程权限受限的问题,不加在某些环境里可能启动异常。
etcd 的环境变量里,ETCD_QUOTA_BACKEND_BYTES默认设了 4GB,这是给 etcd 后端存储设置的上限。如果你的 metadata 量特别大,比如有上万个 collection,这个值可能要调大。但单机场景下一般不需要动。
MinIO 的控制台端口 9001 可访问,访问地址是http://localhost:9001,账号密码默认都是minioadmin。生产环境务必改掉,这些密钥如果直接暴露在公网,等于把整个向量数据仓库敞开给别人看。
3.2 为什么不建议只docker run单容器
我在网上看到很多教程用一条docker run milvusdb/milvus:v2.4.13就把服务跑起来了。这种用法在纯体验场景下确实快,但不适合作为真正的项目起点。
最大的问题在于持久化。一条docker run命令如果不额外挂载数据卷,容器内部写入的文件会随着容器删除而消失。你以为调通了接口、插入了数据,结果某天不小心执行了docker rm,所有 collection 都没了。其次,单容器默认接的是内嵌 etcd 和本地存储,这套模型和官方推荐的 etcd + MinIO 组合在行为上不完全一致,后续想切到分布式会多一次改造。
所以除非你只是想在十分钟内看一眼 Milvus 长什么样,否则我建议从 compose 文件开始。一开始就把数据卷、健康检查、日志策略配好,后面省心得多。
3.3 启动顺序与验证
在 compose 文件所在目录执行:
docker compose up -d第一次执行会拉取三个镜像,耗时取决于网络情况。全部拉完后,用docker compose ps查看状态,正常情况下三个容器都应该是 Up 状态。
然后看 Milvus 日志里有没有明显报错:
docker logs -f milvus如果日志里出现etcd connect success或者minio connect success,基本说明容器层面已经通了。接着验证端口是否监听:
ss -lntp | grep -E '19530|9091'不过我想强调一点:端口能通不代表整个链路就是好的。Milvus 的端口只是入口,真正的验证必须通过 SDK 去建 collection、插一条向量、再查一次。这也是下一节要讲的内容。
4. 第一次写入:pymilvus 从建表到召回的全过程
4.1 连接参数和版本匹配
pymilvus 的安装很简单:
pip install pymilvus==2.4.13版本号最好和服务端镜像版本保持一致。Milvus 服务端升到 2.5 后,旧版客户端连接可能会出现协议不兼容,检索时报奇怪的异常。我第一次用 2.3 的客户端连 2.4 的服务端,直接遇到grpc status code 2,后来才发现是版本不匹配。
连接代码:
from pymilvus import connections connections.connect( alias="default", host="localhost", port="19530" )如果 Mirvus 部署在远程服务器,host要填服务器 IP。在 WSL2 环境里,Docker Desktop 会自动做端口转发,所以localhost也能通。
4.2 Collection Schema:最容易翻车的地方
连接成功后,第一件事是定义 collection 的结构。很多人第一次接触 vector database,容易拿关系库的思维套 schema。我用一个知识库 chunk 的简单示例来说明。
from pymilvus import CollectionSchema, FieldSchema, DataType, Collection fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=512), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768) ] schema = CollectionSchema(fields, description="knowledge base chunks") collection = Collection("kb_demo", schema)几个容易踩的坑:
第一,向量字段的dim必须和你实际使用的 embedding 模型输出维度一致。如果你用的是 bge-large-zh,输出维度通常是 1024;用 bge-base-zh 可能是 768。维度填错了,插入时不会立刻报错,但检索时会出现维度不匹配。
第二,is_primary=True的主键字段可以设置auto_id=True,让 Milvus 自己生成主键。如果业务上需要拿 id 去关联原文,也可以不用 auto_id,自己传入全局唯一值。
第三,schema 一旦创建,改起来非常麻烦。Milvus 不像关系数据库有ALTER TABLE。字段定义错了,最省事的方法是把 collection 删掉重建。所以正式环境建议先用一个小集合做验证,确认 schema 没问题后再建正式的。
4.3 索引选择:不要千人一面用默认
向量检索有索引才能快,但不同类型索引的侧重点完全不同。
| 索引类型 | 适合场景 | 特点 |
|---|---|---|
| FLAT | 数据量小,追求精确 | 暴力计算,结果最准,但太慢 |
| IVF_FLAT | 中等数据量 | 对内存要求低,通过 nlist 和 nprobe 换速度 |
| HNSW | 大数据量、要求低延迟 | 内存占用高,召回率和速度都很好 |
| DISKANN | 海量数据、内存有限 | 索引落盘,速度比纯内存慢一批 |
我在 RAG 知识库场景里多数用 HNSW,因为几百 GB 的文本 chunk 在向量化后也就是几百万条向量,HNSW 的内存开销还能接受,而且检索延迟通常能控制在几十毫秒内。
建索引代码:
from pymilvus import MetricType index_params = { "index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 16, "efConstruction": 200} } collection.create_index("embedding", index_params)M表示每个节点的最大邻居数,值越大,索引构建越慢、内存占用越高,但召回率也越高。efConstruction是构建时的搜索范围,同样越大越慢越准。业务上线前可以在这两个参数上做交叉测试,不必一上来就追求最大。
索引类型不是越高级越好。如果你的数据量只有十万条,用一个 IVF_FLAT 加合理的 nprobe 可能比 HNSW 更省内存,速度也不会差太多。
4.4 写入后必须 flush 和 load
很多新手插完数据马上 search,结果发现查不到任何结果,于是怀疑数据没写进去。其实这是 Milvus 的机制问题:新插入的数据会先进入内存 buffer,搜索前要把 collection 加载到内存才可检索。
from pymilvus import Collection collection = Collection("kb_demo") collection.flush() collection.load()flush会把内存里未落盘的数据刷到存储层,load会把 collection 的索引和数据装载到内存。数据量大的时候,load可能需要一段时间,如果 collection 有多个 partition,还可以只 load 需要的 partition,节省内存。
4.5 检索结果里的 score 怎么看
写入几条数据后,模拟一次查询:
import random dim = 768 query_vector = [random.uniform(-1, 1) for _ in range(dim)] collection = Collection("kb_demo") res = collection.search( data=[query_vector], anns_field="embedding", param={"metric_type": "COSINE", "params": {"ef": 128}}, limit=3, output_fields=["text"] ) for hits in res: for hit in hits: print(hit.score, hit.entity.get("text"))这里有一个重要概念:score的含义取决于你建索引时选的metric_type。
用 COSINE 时,score 越大越相似,最高接近 1。用 L2 时,score 越小越相似,表示距离越近。用 IP 时,如果 embedding 没做过归一化,score 的数值范围可能非常宽,甚至可能是负值,单纯比大小没有直观意义。所以只看 score 绝对值是不行的,必须先明确距离度量方式。
很多人做 RAG 时踩过坑:用 COSINE 建索引,查询时参数误写成metric_type: "IP",结果返回的结果排序看起来完全不对。这个参数从建索引到查询必须保持一致。
5. 数据持久化、备份和升级:别把容器生命周期当成数据保险箱
5.1 三个卷分别存了什么
compose 文件里我把三个数据卷都挂到了宿主机的volumes目录下,分别是volumes/etcd、volumes/minio、volumes/milvus。
etcd 卷存放的是 collection 元数据、segment 状态、索引元数据。MinIO 卷存放的是真正的向量数据文件和索引文件。Milvus 卷存放的是本地节点的一些临时数据。
如果这三卷没挂好,容器一删,就算镜像还在,数据也全没了。所以判断一套 Docker 化部署是否合格,第一步就是看数据卷是否真正落到了宿主机。
5.2 备份不是只拷贝一个目录
我见过有人备份时只把volumes/minio拷走,以为 MinIO 里有数据就够了。恢复之后,新环境里的 etcd 没有对应元数据,Milvus 根本认不出这些 segment。检索时报错五花八门,最常出现的是 segment 找不到或者 collection 为空。
正确做法是同时备份 etcd 和 MinIO 两个目录。更稳妥的方式是先用客户端把 collection 数据统一导出,再去备份文件。如果只是做环境迁移,可以停掉 Milvus 容器后打包。
一个可参考的冷备脚本思路:
BACKUP_DIR=/backup/milvus-$(date +%F) mkdir -p "$BACKUP_DIR" docker compose stop milvus docker run --rm \ -v $(pwd)/volumes:/milvus_data \ -v "$BACKUP_DIR":/backup \ alpine tar czf /backup/milvus_volumes.tar.gz -C /milvus_data . docker compose start milvus停掉 Milvus 容器是为了避免备份过程中数据还在写入,导致文件不一致。etcd 和 MinIO 的服务如果也能短暂停机,那整体一致性更好。备份完成后,把压缩包放到独立存储或对象存储里,不要和 Docker 数据卷放在同一块磁盘上。
5.3 升级版本前先读官方升级说明
Milvus 的版本更新比较快,2.3 到 2.4 到大版本之间,etcd 里的元数据格式可能有变化。不要只改镜像 tag 就认为升级完成。我之前遇到过一次从 2.3 升 2.4 后,老 collection 能查到数据但无法建新索引,排查半天发现是索引元数据格式不兼容,最后用工具做了迁移才恢复。
升级前一定先做完整备份,再查官方 upgrade note,确认有没有 breaking change。别图省事直接拉最新 tag,稳定优先。
6. 踩坑链路:从连不上到磁盘被日志塞满的排查过程
6.1 容器看起来在跑,客户端却连不上
这个问题的排查链路人人都应该走一遍。先看三个容器是不是真的健康:
docker compose ps如果容器都是 Up,但客户端连接报 connection refused,那就看端口占用。比如本地有别的应用占了 19530,compose 文件里的端口映射会失败,表现就是docker compose ps里 Milvus 容器还是起来了,但端口映射没生效。解决办法是把宿主机端口改掉,比如19531:19530,客户端也对应连接 19531。
如果部署在云服务器,还要确认安全组是否放行了 19530 和 9091。很多人本地跑得好好的,换到服务器就连不上,最后发现是安全组只开了 22 和 80,把 19530 忘了。
6.2 写入越来越慢,先看段文件是不是太多
Milvus 会把数据不断切分成小段,如果不主动合并 compaction,小文件太多会导致写入和检索都会变慢。这种问题通常不是单条命令能解决的,而是数据生命周期管理没做对。定期执行数据 compaction 或清理历史 segment 是保持长稳运行的常规手段。
不过在实际项目里,我遇到更多的写入慢其实不是 Milvus 本身的问题,而是 embedding 模型推理太慢。很多 RAG 项目的瓶颈在文本向量化环节,不在数据库写入环节。数据量一大,逐条调用模型接口嵌入就会很慢,这个锅不该让 Milvus 背。
6.3 日志刷屏把磁盘塞满
Docker 容器默认的 json-file 日志驱动不会自动清理日志,长时间跑下来,Milvus、etcd、MinIO 三个容器的日志可能占用大量磁盘。我见过一台测试服务器,几个月没管,/var/lib/docker/containers下单个日志文件超过 30GB。
解决办法是给 Docker daemon 配置日志轮转。在/etc/docker/daemon.json里加:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }配置完成后重启 Docker,新容器会自动按这个策略滚动日志。已经产生的巨型日志文件需要手动清理,docker logs --tail只是看后缀,不会删文件。
另外,etcd 的日志也可以在 compose 环境变量里收敛一下,比如加ETCD_LOG_LEVEL=warn,减少日常 INFO 日志刷屏。
6.4 和 RAG 知识库场景的衔接
最后说一个很多人关心的问题:Milvus 和 RAG 知识库到底怎么搭配。
标准流程是先对文档做切分,每个 chunk 用 embedding 模型转成向量,再把向量和 chunk 原文标识存入 Milvus。用户提问时,同样把问题转成向量,在 Milvus 里召回 TopK 相关 chunk,最后把这些 chunk 拼进 prompt 交给大模型回答。
这里有一个设计建议:不要把上万字的大段原文直接一股脑塞进 Milvus 的字段里。Milvus 定位是向量检索引擎,不是文档数据库。大段原文应该放在 MySQL 或 OSS 等存储里,Milvus 里只存 chunk 的 id、简短的题目或摘要,以及向量本身。这样既能快速检索,又不会让 Milvus 的存储负担过重。
我实际跑下来还有一个体会:在搭建 RAG 知识库时,先通过 Milvus 的标量过滤把候选范围缩到一个 partition,再做向量检索,准确率和速度都能提升不少。比如按文档类型、所属部门或业务线拆 partition,查询时指定 partition,效果比全库搜索更可控。这套方案在 Docker 部署的 Milvus 上完全可行,只需要在建 collection 时定义好 partition 的划分逻辑,后续所有插入和搜索都带上 partition 参数就行。
最后想补一句,Milvus 毕竟是基础组件,运维问题一定是围绕数据可靠性展开的。Docker 部署的确方便,但任何“容器一删所有数据还在”的前提,都是数据卷和备份策略从一开始就设计好了。先跑通再优化,跑通之后第一件事就是做备份演练,这个顺序不要搞反过来。