☰
用docker-compose搭建Weaviate三节点向量数据库集群实战
2026/10/22 11:04:30 网站建设 项目流程

1. 先搞清楚Weaviate是什么,再谈部署

如果你最近在做RAG、语义搜索、向量化推荐或者去重系统,那“向量数据库”这四个字应该没少听。我自己的实践场景是一个内部知识库的检索层升级:原来用的是传统关键词索引,结果用户搜“水管漏水怎么处理”的时候,文档里写“给排水管路渗漏修复”,完全匹配不上,召回质量很感人。后来我把文档切片转成向量存进Weaviate,用向量检索配合关键词过滤,效果立竿见影。这篇文章就把我从选型到用docker-compose搭起三节点集群的完整过程记录下来,包括当时踩过的坑和最后验证过的配置。

先说结论:Weaviate是一个开源的向量数据库,既能存对象数据,也能存向量,并且把两套检索逻辑做进了同一个引擎里。你既可以纯用向量检索做语义相似度匹配,也可以用传统的BM25关键词搜索,还可以把两者混在一起做hybrid search,这在做RAG的时候非常实用。适合谁看呢?如果你已经跑通过单机版,正准备上多节点集群,或者还没选型、想搞清楚Weaviate到底能干嘛,这篇文章都值得花十分钟读完。

1.1 向量检索与传统搜索差在哪

传统搜索依赖倒排索引和关键词匹配,搜索引擎得先把文本拆成词,再建立一个“词到文档”的映射。问题是同一个意思可以有很多种表达方式,关键词对不上,内容再好也召回不出来。向量检索的思路完全不同:先用embedding模型把文本、图片甚至音频映射成一串固定维度的浮点数数组,语义相近的内容在向量空间里离得近,用余弦相似度或欧氏距离一算就能找到。

这个转变带来的好处非常实际。拿我那个知识库的例子来说,用户问“报销发票需要什么材料”,文档里写的是“费用报销单应附增值税发票”,两句话没有重叠词,但语义距离很近,向量检索一下就把文档捞出来了。这种能力在问答、推荐、异常检测、图片搜索这些场景里都能直接用上。

不过向量检索也有自己的短板,比如精度会受embedding模型质量影响,而且它不擅长精确匹配。所以像Weaviate这类产品才会把关键词检索和向量检索同时做进去,让使用者自己组合,而不是逼你只能二选一。

1.2 Weaviate的核心能力概览

Weaviate给我的第一印象是“数据模型很规整”。它把一条数据称为Object,Object归属于Class,Class类似关系数据库里的表。每个Object可以有自己的属性字段,也可以挂一个向量。在创建Class的时候,你可以指定用什么向量索引(默认是HNSW),也可以指定是否用内置模块自动生成向量。

说几个我实际用下来最顺手的特性:

  • 支持REST和GraphQL两套API。GraphQL接口尤其适合做复杂条件查询,把where过滤、向量搜索、对象排序拼在一个请求里,前端对接很舒服。
  • 模块化做得干净。官方提供了text2vec、multi2vec、generative等模块,也可以完全不用模块,自己在业务侧调外部的embedding服务,再把向量写进去。
  • 多租户支持。同一个Class可以按tenant隔离数据,我做知识库权限隔离就是靠这个功能,租户之间数据完全分开。
  • 集群能力原生支持。节点之间通过gRPC通信,数据的shard和replica在Class级别配置,扩容不需要改业务代码。

1.3 我为什么在众多方案里选了Weaviate

当时我也对比了几个开源向量数据库方案,最后选Weaviate不是因为它的搜索性能一定碾压别人,而是它的整体体验最符合我的项目节奏。首先它是纯Go写的,部署产物就是一个容器,依赖很少,不像某些方案还要额外维护一堆配套组件。其次它的文档质量在线,集群部署、模块配置、客户端调用都有比较完整的示例,这对我们这种小团队来说太重要了。最后,它的hybrid search能力是内置原生的,用一份数据同时做关键词和语义检索,省掉了同时维护两套索引的麻烦。

当然它也不是没有缺点。生态和社区比一些老牌开源项目还是小一圈,遇到冷门问题可能查不到现成答案。另外如果你要跑完整的text2vec模块,官方镜像会拉取或调用外部AI模型的接口,生产环境里网络、鉴权、成本都要提前评估。所以我最终的选型策略是:向量检索核心用Weaviate,embedding部分完全由业务侧自建服务生成和传入,把依赖面缩到最小。

2. 单机明明能跑,为什么还要折腾集群

很多团队一开始都会问这个问题:我先起一个Weaviate容器,几千条测试数据跑得飞快,有必要上三节点集群吗?我的答案是分情况。如果你只是做Demo、原型验证,单机完全够;但如果数据量上了千万级,或者你的服务不允许单点故障,那集群就不是可选项,而是必选项。

2.1 单机版与集群版的能力边界

单机版Weaviate把所有数据落在一个节点上,索引、查询、写入都是本地进行,性能很好,架构也简单。但它有两个硬伤:一是资源天花板有限,内存和磁盘是固定的,数据量涨到一定程度就无路可走;二是单点故障,机器一挂服务就断,磁盘损坏数据就没了,这对线上系统来说是不可接受的。

集群版解决的问题正好对应这两点。通过shard分片,把同一个Class的数据拆到多个节点上并行存储和处理;通过replica副本,让每份数据在多个节点上各存一份,某个节点宕机后查询请求会自动落到其他副本上。配合Weaviate的HNSW索引,查询时多节点并行搜索再合并结果,海量数据下的响应延迟也能保持稳定。

2.2 挂起集群前要理解的几个角色概念

Weaviate的集群模型不是传统的“一主多从”,节点之间没有绝对的主从关系。每个节点都会参与集群的成员管理和数据存储,客户端请求可以打到任意节点,收到请求的节点会负责协调其他节点上的shard查询,最后合并结果返回。这个设计让扩缩容变得很灵活,但也要注意:如果你的Class没有设置副本数,数据其实只存一份,某个节点挂了,这部分数据在故障期间就是不可用的。

数据在集群里是怎么分布的,关键看Class的两类配置:shardCount决定一个Class分成多少物理分片,replicationFactor决定每个分片保存几份。默认情况下一个Class只有1个分片、1份副本,也就是说虽然你起了三个节点,数据可能全部堆在其中一个节点上。要让集群真正分摊压力,创建Class时必须主动调整这两个参数。

这里顺带提一句集群的协调机制。较新版本的Weaviate引入了基于Raft协议的集群状态管理,节点之间自行协商,不再依赖额外的外部协调组件;在之前的版本里则通常需要部署一个提供共识能力的辅助服务。单看部署层面,新版方案明显省事得多。docker-compose部署时只要把节点配置对,剩下的事情交给Weaviate自己处理就行。

2.3 为什么我选docker-compose而不是K8s

坦白说,如果公司里已经有一套成熟的Kubernetes环境,那直接用Helm Chart部署Weaviate集群是更正统的路径。但很多中小团队和我这边的情况类似:没有专职的SRE,K8s集群本身就没人维护,为了一个向量数据库先学会容器编排,性价比太低。

docker-compose的优势在于,它对单机多容器场景非常友好。一个YAML文件把三个节点的镜像、端口、环境变量、数据卷全部定义清楚,docker compose up -d一条命令拉起整个集群,出问题用docker compose logs就能看日志,整个生命周期都在自己掌控里。对于测试环境、预生产环境甚至中小规模的线上服务,这套方案足够稳。

当然,如果节点要跨多台物理机部署,docker-compose就不太够了,调整网络、传数据卷都很别扭。那种场景建议继续往K8s或Swarm方向演进。我个人的选型逻辑是:先让业务跑起来,再根据规模决定要不要引入更重的编排系统。

3. docker-compose搭建三节点集群的完整过程

接下来进入正题。我以三节点集群为例,把每一步都拆开讲,包含完整的docker-compose配置和验证命令。部署环境是一台Linux服务器,Docker和Compose插件已经装好,端口没有冲突。

3.1 部署前的节点与端口规划

在写compose文件之前,我先把要用的端口和数据目录列了个表格,避免配置到一半乱套。Weaviate按功能分了几类通信端口:REST API默认8080,供HTTP客户端使用;gRPC客户端API默认50051,供各语言SDK使用;集群内部协调、节点间通信还有7100、7101、7102等端口。在同一台机器上跑三个节点,内部端口不需要映射到宿主机,因为容器之间通过Compose自定义网络可以直接按服务名访问,我只把对外API端口错开映射出来。

节点服务名CLUSTER_HOSTNAME宿主机REST端口宿主机gRPC端口数据卷加入方式
weaviate-node-1node-1808050051weaviate-data-1首个节点
weaviate-node-2node-2808150052weaviate-data-2加入node-1
weaviate-node-3node-3808250053weaviate-data-3加入node-1

三个节点都配置了独立的宿主机端口,这样在本地测试时可以分别访问每个节点的REST和gRPC接口,方便观察数据分布情况。数据使用命名卷持久化,容器重建后数据不会丢。

3.2 docker-compose.yml完整配置

下面是我的docker-compose.yml,拿去改改就能用。需要说明的是,Weaviate版本更新比较频繁,环境变量在不同版本之间可能有微调,示例以1.28.x版本为准,实际部署时请到官方文档核对当前版本的变量名。

version: "3.8" services: weaviate-node-1: image: semitechnologies/weaviate:1.28.5 restart: on-failure:0 ports: - "8080:8080" - "50051:50051" environment: AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true" PERSISTENCE_DATA_PATH: "/var/lib/weaviate" DEFAULT_VECTORIZER_MODULE: "none" CLUSTER_HOSTNAME: "node-1" CLUSTER_INTERNAL_IP: "weaviate-node-1" CLUSTER_JOIN_INTERNAL_PORT: "7100" CLUSTER_GOSSIP_BIND_PORT: "7101" CLUSTER_DATA_BIND_PORT: "7102" DISABLE_TELEMETRY: "true" volumes: - weaviate-data-1:/var/lib/weaviate healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/v1/.well-known/ready"] interval: 30s timeout: 5s retries: 5 weaviate-node-2: image: semitechnologies/weaviate:1.28.5 restart: on-failure:0 ports: - "8081:8080" - "50052:50051" environment: AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true" PERSISTENCE_DATA_PATH: "/var/lib/weaviate" DEFAULT_VECTORIZER_MODULE: "none" CLUSTER_HOSTNAME: "node-2" CLUSTER_JOIN: "weaviate-node-1:7100" CLUSTER_INTERNAL_IP: "weaviate-node-2" CLUSTER_JOIN_INTERNAL_PORT: "7100" CLUSTER_GOSSIP_BIND_PORT: "7101" CLUSTER_DATA_BIND_PORT: "7102" DISABLE_TELEMETRY: "true" volumes: - weaviate-data-2:/var/lib/weaviate depends_on: - weaviate-node-1 weaviate-node-3: image: semitechnologies/weaviate:1.28.5 restart: on-failure:0 ports: - "8082:8080" - "50053:50051" environment: AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true" PERSISTENCE_DATA_PATH: "/var/lib/weaviate" DEFAULT_VECTORIZER_MODULE: "none" CLUSTER_HOSTNAME: "node-3" CLUSTER_JOIN: "weaviate-node-1:7100" CLUSTER_INTERNAL_IP: "weaviate-node-3" CLUSTER_JOIN_INTERNAL_PORT: "7100" CLUSTER_GOSSIP_BIND_PORT: "7101" CLUSTER_DATA_BIND_PORT: "7102" DISABLE_TELEMETRY: "true" volumes: - weaviate-data-3:/var/lib/weaviate depends_on: - weaviate-node-1 volumes: weaviate-data-1: weaviate-data-2: weaviate-data-3:

几个关键点我单独说一下。AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED设为true只适合内网测试环境,生产环境一定要换成API Key或OIDC认证,否则等于裸奔。DEFAULT_VECTORIZER_MODULE我设为none,表示不使用内置向量化模块,向量由业务侧生成好后直接写入,这样部署最简单,不依赖外部AI服务的网络连通性。DISABLE_TELEMETRY是关闭遥测上报,非必需,但我在内部环境默认关掉。

CLUSTER_HOSTNAME是每个节点的唯一标识,相当于集群里的身份证号,三个节点必须各不相同。CLUSTER_JOIN是节点加入集群时连接的目标地址,后两个节点都指向第一个节点的7100端口。CLUSTER_INTERNAL_IP用来告诉其他节点“怎么找到我”,在Compose网络里直接写服务名就可以,因为Compose会自动做DNS解析。

3.3 启动集群并验证节点状态

配置写好后,启动过程非常简单:

docker compose up -d

然后看容器状态:

docker compose ps

如果一切正常,三个容器都是running状态,且weaviate-node-1的健康检查显示healthy。这时候我想确认集群注册情况,直接调节点的接口:

curl -s http://localhost:8080/v1/nodes | jq '.nodes[] | {name: .name, status: .status, shards: .shards}'

正常情况下返回结果里能看到三个节点,名字对应node-1、node-2、node-3,status字段是HEALTHY。有一点要提醒:如果后两个节点启动太快,可能赶在第一个节点完全就绪之前发起加入请求,所以我加了depends_on配合健康检查,避免出现这种启动竞态。

3.4 建Class验证数据有没有真的分布到集群

集群起来的下一步不是急着灌数据,而是先验证数据分布是否如预期。我在本地用Python客户端连到8080端口,创建一个开了分片和副本的Class:

import weaviate client = weaviate.connect_to_local(port=8080, grpc_port=50051) client.collections.create( name="Article", replication_factor=2, shard_count=3, vectorizer_config=None, ) client.close()

这个配置的含义是:Article类分成3个分片,每个分片保存2份副本。3个节点、3个分片、2份副本,数据会相对均匀地打散到各节点上。插入一批测试对象后,再调/v1/nodes接口,就能看到每个节点各自负责了哪些shard。如果发现所有shard都堆在同一个节点上,那就是创建Class时没配shard_count,数据在集群里并没有真正分散。

4. 这些核心配置项,部署前一定要吃透

光把compose文件跑通还不够,有几个配置项当时让我研究了好一阵,搞懂背后的原理之后才算真正放心地上线。

4.1 CLUSTER_环境变量逐个拆解

我们先看一张表,把集群相关的环境变量和它的作用对应起来。

环境变量默认值作用注意事项
CLUSTER_HOSTNAME无节点的唯一标识集群内必须唯一,容器重建后也要保持
CLUSTER_JOIN无加入集群时连接的目标地址首个节点不需要设置,后续节点指向已有节点
CLUSTER_INTERNAL_IP自动探测其他节点访问当前节点的地址跨主机部署时必须设置为可路由的地址
CLUSTER_JOIN_INTERNAL_PORT7100内部协调通信端口容器内端口,跨主机部署要保证互通
CLUSTER_GOSSIP_BIND_PORT7101节点状态传播端口用于集群成员存活探测
CLUSTER_DATA_BIND_PORT7102gRPC数据同步端口分片数据在节点间迁移、复制走的端口

我在配置里故意把7100、7101、7102这些内部端口在容器内固定下来,而不是依赖随机分配,这样节点间的通信地址是可预期的。如果你只是单机Compose,内部端口不需要映射到宿主机;但如果是多主机部署,这些端口必须对外暴露并放通防火墙,否则节点之间互相找不到,集群一直起不来。

4.2 数据持久化与容器重启策略

Weaviate把数据存在PERSISTENCE_DATA_PATH指定的目录下,容器里的默认路径是/var/lib/weaviate。我用的是命名卷(named volume),这样即使执行docker compose down把容器删掉,数据卷还在,重新up之后数据原样恢复。

这里有个容易踩的坑:如果直接挂载宿主机目录,比如./data:/var/lib/weaviate,可能会碰到文件权限问题。容器里的进程对挂载目录需要有读写权限,不同宿主机、不同文件系统下的表现不一定一样,测试时一切正常,换到生产环境突然报权限错误的情况我见过不止一次。命名卷由Docker自己管理,权限问题会少很多,所以我默认推荐命名卷,除非你明确知道bind mount的操作系统权限细节。

restart: on-failure:0的意思是容器异常退出时自动重启,但不做指数退避限制。这个策略对测试环境比较合适;生产环境建议用unless-stopped,这样服务器重启后Docker服务起来也会自动拉起容器。

4.3 资源规划与HNSW索引参数权衡

Weaviate默认的向量索引是HNSW,这是一种基于图的近邻搜索算法,查询速度快,但索引本身很吃内存。在实际使用前,我对容量估算的粗线条公式是:单个向量维度 × 4字节(float32)× 向量总数,这是一份原始向量数据在内存里的基础占用;再算上HNSW的图结构和各层连接,实际占用通常会到原始数据的2到3倍。比如768维的向量,100万条原始数据约占3GB,加上索引结构,建议给节点预留6GB到10GB内存。

HNSW的参数也会直接影响资源占用和查询质量。maxConnections控制图中每个节点的最大连接数,值越大索引越精细、内存越高;efConstruction控制建索引时的搜索宽度,值越大索引质量越好、构建越慢。这两个参数在创建Class时通过vectorIndexConfig设置,实际生产环境我踩过的建议是:不要盲目追求最大,先用默认值跑通,再根据查询延迟和recall评估结果微调。

另外还有同步索引和异步索引的区别。默认情况下,写入对象时向量会同步完成索引,写入完成后立刻可查,但写入吞吐会受影响。如果你的场景是批量导入数据,可以在Class上开启异步索引,先把数据快速写入,后台慢慢构建索引,查询延迟会有一段时间的衰退,但导入速度能提升很多。

4.4 健康检查与监控手段

集群起来了不代表一切正常,还要有持续的可观测手段。Weaviate提供了两个轻量级接口:/v1/.well-known/ready判断节点是否就绪,/v1/.well-known/live判断进程是否存活。我在compose里加的healthcheck就是依赖ready接口,用于让编排系统知道节点什么时候可以对外提供服务。

更细粒度的状态要看/v1/nodes接口,它返回的不只是节点存活状态,还包括每个节点上的shard列表、shard的健康状态、对象数量等。我把这个接口接进了内部监控系统,定时抓取并计算各个节点上的shard分布和对象数,一旦发现某个节点上的对象数明显不均,就要排查是不是Class配置有问题,或者节点之间数据迁移没完成。

日志方面,docker compose logs -f weaviate-node-1可以实时看某个节点的输出。集群协调异常、gRPC连接失败、Raft状态变更这类问题都会在日志里留下痕迹,排查问题第一步永远是先看日志。

5. 部署和使用过程中的踩坑实录

这一节记录我在实际操作中真实遇到的几个问题,以及对应的排查思路。很多坑官网文档不会专门写,但对后来者很有价值。

5.1 问题一:节点一直显示STARTING状态

有一次我把node-2的CLUSTER_JOIN填成了weaviate-node-1:8080,结果这个节点启动后一直处于STARTING状态,怎么等都变不成HEALTHY。原因是我把REST端口当成了集群内部通信端口。Weaviate节点之间走的是内部协调端口(默认7100),不是REST API的8080端口。

排查过程是这样的:先看node-2的日志,发现它反复报连接被拒绝,目标都是weaviate-node-1:8080;再检查docker-compose配置,才发现CLUSTER_JOIN写错了端口。改成weaviate-node-1:7100之后重启node-2,节点很快就加入了集群。这个问题的教训是:所有CLUSTER_*相关配置里的地址,都是指集群内部通信地址,别和对外API端口混为一谈。

5.2 问题二:容器重启后集群成员信息异常

我做过一次测试:只重启node-2,结果重启之后用/v1/nodes查看,发现node-2的hostname变成了容器ID,而不是我配置的node-2。原因是Weaviate在集群中的标识依赖CLUSTER_HOSTNAME,但我当时图省事没有在node-2的环境变量里显式设置,而是让它自动生成。

这导致的连锁问题是,集群里出现了两个“看起来不同”的节点,一个叫node-2,一个叫一堆随机字符串。我自己的建议是,不管是不是集群部署,都一定要显式配置CLUSTER_HOSTNAME,而且要固定。这样容器重建、节点迁移都不会产生身份漂移,数据复制和shard归属关系才稳定。

5.3 问题三:客户端连不上gRPC端口

我的Python客户端一开始怎么都连不上,报错信息指向gRPC连接失败。检查之后发现,我只在compose里映射了8080端口,忘了映射50051。Weaviate新版本客户端默认优先走gRPC,REST端口通不代表gRPC端口通。

解决办法是把50051:50051在compose文件中暴露出来。如果你的宿主机已经占用了50051,可以用类似50052:50051的映射方式,但客户端配置里的gRPC端口也要同步改成50052。另外还要注意,某些容器网络环境对gRPC的HTTP/2支持不是默认开启的,如果是在特殊网络里部署,建议先在宿主机上直接测试localhost端口连通性,再排查网络策略。

5.4 常见问题速查表

现象可能原因处理办法
节点长时间STARTINGCLUSTER_JOIN地址或端口配错检查内部端口7100是否可达,看节点日志里的连接错误
集群里出现随机字符串节点CLUSTER_HOSTNAME未设置为每个节点显式配置固定的CLUSTER_HOSTNAME
数据全部落在同一个节点创建Class未配置shard_count重建Class,设置shard_count大于1
客户端报gRPC连接失败50051端口未映射或被防火墙拦截检查compose端口映射和主机防火墙策略
节点重启后数据丢失未挂载持久化卷使用命名卷或bind mount挂载/var/lib/weaviate
查询很慢但CPU不高HNSW索引参数过大或ef设置偏低评估maxConnections和efConstruction参数
批量导入吞吐很低同步索引开启对批量任务开启异步索引

6. 个人验证过的实操建议

最后分享几个我自己的操作习惯,都是一条条试出来的经验,不一定适合所有场景,但至少能让你少走几趟弯路。

第一,新环境务必从单机版开始。哪怕你最终目标是三节点集群,我也建议先用一个容器把数据模型、模块选型、客户端调用方式全部跑通,再上集群。集群的热点是集群特有的,单机能把业务逻辑问题先过滤掉,排查起来事半功倍。

第二,创建Class时一定要从业务需求反推shard_count和replication_factor。不是节点多就一定可靠,副本数决定容灾能力,分片数决定并行度和数据分布。测试环境副本数设为1没问题,但生产环境建议至少2,这样单个节点维护时查询才能继续。

第三,embedding模块能省则省。官方模块虽然方便,但会引入外部模型服务的依赖,网络抖动、接口限流、成本控制都是新问题。我现在的做法是业务侧统一调一个内部embedding服务,把向量算好再写入Weaviate,Weaviate只负责存储和检索,职责单一,故障面小很多。

第四,升级版本前先看变更日志。Weaviate迭代速度不慢,集群协议、环境变量名称都可能变化,尤其是从旧版集群模式切换到新版Raft模式时,配置方式差异很明显。我踩过升级后节点互相不认识的坑,现在的习惯是先在测试环境完整跑一遍数据导入和查询,再动生产集群。

第五,不要把AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true带到生产。这个配置适合本地调试,但线上服务只要暴露了端口,没有鉴权等于把数据库裸奔在网络上。切到API Key后,客户端要多传一个请求头,这点改动很值得。

回到我开头那个知识库项目,三节点集群跑到现在,数据量从十万级涨到千万级,查询P99一直稳定在几十毫秒量级。当时从docker-compose单机切到docker-compose集群,整个过程半天就完成了。对我来说,Weaviate最省心的地方恰恰是它把集群的复杂度封装在配置项里,而不是逼着你再去维护一套外部协调系统。如果你正准备做向量检索的集群化改造,从这个方案入手,应该是个不错的方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询