用docker-compose搭建Weaviate三节点向量数据库集群实战

发布时间:2026/10/22 11:04:48
用docker-compose搭建Weaviate三节点向量数据库集群实战
1. 先搞清楚Weaviate是什么再谈部署如果你最近在做RAG、语义搜索、向量化推荐或者去重系统那“向量数据库”这四个字应该没少听。我自己的实践场景是一个内部知识库的检索层升级原来用的是传统关键词索引结果用户搜“水管漏水怎么处理”的时候文档里写“给排水管路渗漏修复”完全匹配不上召回质量很感人。后来我把文档切片转成向量存进Weaviate用向量检索配合关键词过滤效果立竿见影。这篇文章就把我从选型到用docker-compose搭起三节点集群的完整过程记录下来包括当时踩过的坑和最后验证过的配置。先说结论Weaviate是一个开源的向量数据库既能存对象数据也能存向量并且把两套检索逻辑做进了同一个引擎里。你既可以纯用向量检索做语义相似度匹配也可以用传统的BM25关键词搜索还可以把两者混在一起做hybrid search这在做RAG的时候非常实用。适合谁看呢如果你已经跑通过单机版正准备上多节点集群或者还没选型、想搞清楚Weaviate到底能干嘛这篇文章都值得花十分钟读完。1.1 向量检索与传统搜索差在哪传统搜索依赖倒排索引和关键词匹配搜索引擎得先把文本拆成词再建立一个“词到文档”的映射。问题是同一个意思可以有很多种表达方式关键词对不上内容再好也召回不出来。向量检索的思路完全不同先用embedding模型把文本、图片甚至音频映射成一串固定维度的浮点数数组语义相近的内容在向量空间里离得近用余弦相似度或欧氏距离一算就能找到。这个转变带来的好处非常实际。拿我那个知识库的例子来说用户问“报销发票需要什么材料”文档里写的是“费用报销单应附增值税发票”两句话没有重叠词但语义距离很近向量检索一下就把文档捞出来了。这种能力在问答、推荐、异常检测、图片搜索这些场景里都能直接用上。不过向量检索也有自己的短板比如精度会受embedding模型质量影响而且它不擅长精确匹配。所以像Weaviate这类产品才会把关键词检索和向量检索同时做进去让使用者自己组合而不是逼你只能二选一。1.2 Weaviate的核心能力概览Weaviate给我的第一印象是“数据模型很规整”。它把一条数据称为ObjectObject归属于ClassClass类似关系数据库里的表。每个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模型的接口生产环境里网络、鉴权、成本都要提前评估。所以我最终的选型策略是向量检索核心用Weaviateembedding部分完全由业务侧自建服务生成和传入把依赖面缩到最小。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集群是更正统的路径。但很多中小团队和我这边的情况类似没有专职的SREK8s集群本身就没人维护为了一个向量数据库先学会容器编排性价比太低。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-1weaviate-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-3status字段是HEALTHY。有一点要提醒如果后两个节点启动太快可能赶在第一个节点完全就绪之前发起加入请求所以我加了depends_on配合健康检查避免出现这种启动竞态。3.4 建Class验证数据有没有真的分布到集群集群起来的下一步不是急着灌数据而是先验证数据分布是否如预期。我在本地用Python客户端连到8080端口创建一个开了分片和副本的Classimport weaviate client weaviate.connect_to_local(port8080, grpc_port50051) client.collections.create( nameArticle, replication_factor2, shard_count3, vectorizer_configNone, ) 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新版本客户端默认优先走gRPCREST端口通不代表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服务把向量算好再写入WeaviateWeaviate只负责存储和检索职责单一故障面小很多。第四升级版本前先看变更日志。Weaviate迭代速度不慢集群协议、环境变量名称都可能变化尤其是从旧版集群模式切换到新版Raft模式时配置方式差异很明显。我踩过升级后节点互相不认识的坑现在的习惯是先在测试环境完整跑一遍数据导入和查询再动生产集群。第五不要把AUTHENTICATION_ANONYMOUS_ACCESS_ENABLEDtrue带到生产。这个配置适合本地调试但线上服务只要暴露了端口没有鉴权等于把数据库裸奔在网络上。切到API Key后客户端要多传一个请求头这点改动很值得。回到我开头那个知识库项目三节点集群跑到现在数据量从十万级涨到千万级查询P99一直稳定在几十毫秒量级。当时从docker-compose单机切到docker-compose集群整个过程半天就完成了。对我来说Weaviate最省心的地方恰恰是它把集群的复杂度封装在配置项里而不是逼着你再去维护一套外部协调系统。如果你正准备做向量检索的集群化改造从这个方案入手应该是个不错的方向。