☰
基于Docker的Cassandra三节点集群部署与排障实践
2026/10/13 4:07:20 网站建设 项目流程

上周刚帮某团队把一套三节点 Cassandra 集群从虚拟机迁到了 Docker 环境,整个过程比预想中顺利,但几个隐蔽的坑也确实让人印象深刻。很多人聊到“Cassandra 集群化 Docker 部署”第一反应就是“docker run 一个镜像,多开几个容器不就完了”,实际上远没那么简单。容器化的核心价值在于让集群的创建、销毁、扩容变得可重复、可编排,但 Cassansdra 的分布式特性(gossip 协议、种子节点、数据副本)决定了它在容器环境里的网络和存储配置需要格外谨慎。这篇文章我会从集群设计、docker-compose 编写、配置参数逻辑、以及真实排障过程这几个角度,完整拆解一套三节点 Cassandra 集群的部署方案,适合已经具备 Docker 基础、想用容器方式管理 Cassandra 的开发者参考。

1. 部署前必须想清楚:Cassandra 集群到底在解决什么问题

1.1 单机 Cassandra 的瓶颈在哪里

很多团队最初用 Cassandra 只是因为它的数据模型比 Redis 复杂、比 MySQL 更易横向扩展。但单机部署 Cassandra 其实非常尴尬:它本质上是一个分布式系统,所有高可用、一致性、负载均衡的能力都建立在“多节点、跨副本”的前提下。单节点时你不仅要面对磁盘容量的天花板,更严重的是,一旦进程崩溃或所在机器硬件故障,数据恢复完全依赖快照和备份策略,而 Cassandra 引以为傲的“无单点故障”特性根本发挥不出来。

我在实际项目中见过不少团队把 Cassandra 当作“增强版 MongoDB”来用,单机部署、本地磁盘,结果流量一涨,compaction(压实操作)直接把 IO 打满,查询延迟飙升到秒级。这时候再想迁移到集群,就会发现单机上的数据导入导出、schema 同步、负载均衡都是额外成本。所以这里有个很朴素的道理:如果你确定要用 Cassandra,那集群化部署不该是“以后再说”的选项,而是第一步就该考虑的事。

1.2 集群化之后,Docker 的角色是什么

Cassandra 集群化之后,Docker 的核心价值不是省机器,而是把“搭建集群”这件事从手工操作变成代码描述。传统方式下,你要准备三台物理机或虚拟机,手动安装 JDK、下载 Cassandra 包、修改配置文件、启动服务、确认种子节点、再一台台加入集群。这个过程一旦要重复执行(比如环境从测试迁移到生产),几乎是灾难。

用 Docker 之后,镜像本身就固化了 JDK 版本、Cassandra 版本、系统依赖,你的 docker-compose.yml 文件就是集群拓扑的“声明式文档”。想加一个节点,复制一段服务定义、改一下容器名和种子节点配置,docker compose up -d就能拉起来一个新节点。整个过程不仅快,而且可追溯、可回滚。当然,容器化也不是没有代价,网络性能损耗、存储驱动的选型、日志的管理都需要额外关注,后面我会逐一展开。

2. 集群拓扑设计与资源规划:三节点起步,不要贪多

2.1 副本因子、种子节点与机架感知的基本逻辑

部署之前先要设计集群拓扑。最常用也最稳妥的起步配置是三节点集群、复制因子(replication factor)= 3。三节点的意义在于:当复制因子等于节点数时,任意一个节点宕机,只要配置的一致性级别为 QUORUM(即至少 2 个副本响应),集群仍然可以正常读写,不会丢失数据。这就是 Cassandra 最基本的容错模型:容忍 N-1 个节点故障(在 RF=3 时容忍 1 个节点故障)。

种子节点(seed node)是一个经常被误解的概念。种子节点不负责存储额外的数据,只是用于帮助新节点加入集群时找到“联系人”。新节点启动时通过 seed 列表里的节点发起 gossip 握手,进而获得整个集群的拓扑信息。官方推荐每个节点的 seed 配置保持一致,并且不要把所有节点都设为 seed,一般建议 2 到 3 个即可。在这个方案里,我把 node1 和 node2 作为 seed,node3 只通过配置指向它们。

机架感知(rack awareness)这个概念,在单机房三节点场景下其实可以简化。默认的GossipingPropertyFileSnitch已经能识别机架信息,Docker 部署时通常只需要设置环境变量CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch,然后通过每个容器的cassandra-rackdc.properties文件去指定机架名。如果是跨机房部署,就需要针对不同机房配置dc1、dc2这样的数据中心名,并且调整 keyspace 的复制策略为NetworkTopologyStrategy。这个方案里只涉及单数据中心,用SimpleStrategy即可,但我会在配置里提前把GossipingPropertyFileSnitch写好,方便以后扩展。

2.2 网络、存储与资源隔离的选型

Docker 部署 Cassandra 集群,网络模式是个绕不开的话题。我推荐使用 Docker 内置的 bridge 网络(通过 docker-compose 自动创建),而不是 host 网络。原因有两点:第一,bridge 网络会自动为每个容器分配可解析的服务名(例如 node1、node2),Cassandra 节点之间可以直接用服务名通信,配置起来非常方便;第二,bridge 网络自带 DNS 解析,容器重启后即便 IP 变化,服务名仍然稳定。

存储方面,Cassandra 对磁盘 IO 要求很高,尤其是 commit log 的同步写入和 compaction 的随机读写。在 Docker 环境里,最常见的错误是把数据卷挂到 overlay2 驱动的默认路径上,这样性能会打很大折扣。如果你的宿主机有独立的数据盘,强烈建议把 Cassandra 的数据目录挂载到宿主机磁盘的独立目录或独立卷上,并且确保挂载方式是rw而不是通过网络文件系统(NFS、CIFS 这类)来承载。一个基本的经验公式是:生产环境中,每节点建议至少分配 4 核 CPU、8GB 内存,数据盘使用本地 SSD,IOPS 不低于 2000。如果只是开发或测试环境,2 核 4GB 勉强能跑,但千万不要把测试环境的数据盘性能当作生产参考。

3. 实际部署:docker-compose 拉起三节点 Cassandra 集群

3.1 目录结构与镜像选择

镜像方面,官方cassandra镜像一直维护得不错,我使用的是cassandra:4.1这个版本系列。选择 4.x 而不是 3.x 的原因很简单:4.0 之后 Cassandra 在审计日志、Java 11 支持、压缩算法、内存管理方面都有明显改进,新项目没有理由再抱守 3.x。需要特别提醒的是,不要使用latest标签,因为大版本升级之间数据文件格式和生产配置差异很大,锁定具体版本号(如4.1.3)才能保证后续重建环境时行为一致。

目录结构我一般是这么组织的:

cassandra-deploy/ ├── docker-compose.yml ├── config/ │ ├── node1/ │ │ ├── cassandra.yaml │ │ └── cassandra-rackdc.properties │ ├── node2/ │ │ └── ... │ └── node3/ │ └── ... ├── scripts/ │ └── init-keyspace.sh └── data/ ├── node1/ ├── node2/ └── node3/

把cassandra.yaml单独挂载出来而不是依赖环境变量,是因为环境变量覆盖默认配置的手段有限,某些关键参数(如memtable_heap_space_in_mb、concurrent_writes)无法通过CASSANDRA_*环境变量直接控制。直接挂载配置文件最可靠,也最容易做配置审计。

3.2 docker-compose.yml 配置逐项说明

下面是我用来部署三节点集群的 docker-compose 配置文件,我把每个关键字段的来历和作用都标注在注释里:

version: "3.8" services: cassandra-node1: image: cassandra:4.1.3 container_name: cassandra-node1 restart: unless-stopped networks: - cassandra-net ports: - "9042:9042" # CQL 客户端端口,只映射到 node1,避免客户端连接串口混乱 volumes: - ./config/node1/cassandra.yaml:/etc/cassandra/cassandra.yaml - ./config/node1/cassandra-rackdc.properties:/etc/cassandra/cassandra-rackdc.properties - cassandra-node1-data:/var/lib/cassandra environment: CASSANDRA_CLUSTER_NAME: "MyCassandraCluster" CASSANDRA_SEEDS: "cassandra-node1,cassandra-node2" CASSANDRA_ENDPOINT_SNITCH: GossipingPropertyFileSnitch CASSANDRA_LISTEN_ADDRESS: cassandra-node1 CASSANDRA_BROADCAST_ADDRESS: 192.168.1.101 # 宿主机可路由 IP CASSANDRA_RPC_ADDRESS: 0.0.0.0 CASSANDRA_BROADCAST_RPC_ADDRESS: 192.168.1.101 CASSANDRA_AUTO_BOOTSTRAP: "false" # 仅 node1 首次启动时设为 false healthcheck: test: ["CMD", "cqlsh", "-e", "SELECT release_version FROM system.local"] interval: 30s timeout: 10s retries: 5 start_period: 120s cassandra-node2: image: cassandra:4.1.3 container_name: cassandra-node2 restart: unless-stopped networks: - cassandra-net ports: - "9043:9042" volumes: - ./config/node2/cassandra.yaml:/etc/cassandra/cassandra.yaml - ./config/node2/cassandra-rackdc.properties:/etc/cassandra/cassandra-rackdc.properties - cassandra-node2-data:/var/lib/cassandra environment: CASSANDRA_CLUSTER_NAME: "MyCassandraCluster" CASSANDRA_SEEDS: "cassandra-node1,cassandra-node2" CASSANDRA_ENDPOINT_SNITCH: GossipingPropertyFileSnitch CASSANDRA_LISTEN_ADDRESS: cassandra-node2 CASSANDRA_BROADCAST_ADDRESS: 192.168.1.102 CASSANDRA_RPC_ADDRESS: 0.0.0.0 CASSANDRA_BROADCAST_RPC_ADDRESS: 192.168.1.102 depends_on: - cassandra-node1 cassandra-node3: image: cassandra:4.1.3 container_name: cassandra-node3 restart: unless-stopped networks: - cassandra-net ports: - "9044:9042" volumes: - ./config/node3/cassandra.yaml:/etc/cassandra/cassandra.yaml - ./config/node3/cassandra-rackdc.properties:/etc/cassandra/cassandra-rackdc.properties - cassandra-node3-data:/var/lib/cassandra environment: CASSANDRA_CLUSTER_NAME: "MyCassandraCluster" CASSANDRA_SEEDS: "cassandra-node1,cassandra-node2" CASSANDRA_ENDPOINT_SNITCH: GossipingPropertyFileSnitch CASSANDRA_LISTEN_ADDRESS: cassandra-node3 CASSANDRA_BROADCAST_ADDRESS: 192.168.1.103 CASSANDRA_RPC_ADDRESS: 0.0.0.0 CASSANDRA_BROADCAST_RPC_ADDRESS: 192.168.1.103 depends_on: - cassandra-node1 - cassandra-node2 networks: cassandra-net: driver: bridge volumes: cassandra-node1-data: cassandra-node2-data: cassandra-node3-data:

这里有几个关键点需要解释。第一,CASSANDRA_LISTEN_ADDRESS我填的是容器名,而不是 IP,这样容器重建后即使 IP 变化也无须修改配置。第二,CASSANDRA_BROADCAST_ADDRESS填的是宿主机在局域网内可路由的 IP,这个 IP 是节点之间互相通讯时“公告”给彼此的地址。如果你的多个节点部署在同一台宿主机上,广播地址用宿主机 IP 没有问题;但如果跨宿主机部署,广播地址必须填各自宿主机的 IP,否则节点之间无法正常通信。第三,CASSANDRA_RPC_ADDRESS设置成0.0.0.0,是为了让 CQL 客户端能通过任意网卡连接,再配合BROADCAST_RPC_ADDRESS告诉客户端该用哪个地址访问。

还有一个细节,只有 node1 的CASSANDRA_AUTO_BOOTSTRAP我设置为"false",这是为了避免新节点加入集群时自动获取数据导致初始化异常。当 node1 作为首个空节点启动时,false可以避免 bootstrap 流程误判数据范围。node2 和 node3 没有显式设置这个变量,官方镜像默认情况下会正确执行 bootstrap。

3.3 启动集群与状态验证

配置完成后,启动过程分三步走。第一步,先在宿主机上确认防火墙没有拦截 Cassandra 的通讯端口。这里需要特别留意,Docker bridge 网络内部的通讯走的是容器虚拟网卡,不经过宿主机防火墙,但如果配置了端口映射并且在宿主机层面启用了firewalld或iptables规则,就要确保7000端口(gossip 通讯)和9042端口(CQL 客户端)没有被误伤。第二步,执行docker compose up -d,然后观察启动日志:

docker compose logs -f cassandra-node1

正常启动时,日志里会出现Starting listening for CQL clients on /0.0.0.0:9042 ...以及CassandraDaemon.java:... - Startup complete这样的关键行。看到这两行,说明节点已经成功启动。第三步,待三个节点都启动后,在任意一个节点容器内执行:

docker exec -it cassandra-node1 nodetool status

如果你看到类似下面的输出,说明集群已经正确组网:

Datacenter: datacenter1 ======================= Status=Up/Down |/ State=Normal/Leaving/Joining/Moving -- Address Load Tokens Owns Host ID Rack UN 192.168.1.101 1.2 MiB 16 33.3% xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx rack1 UN 192.168.1.102 1.2 MiB 16 33.3% xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx rack1 UN 192.168.1.103 1.2 MiB 16 33.3% xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx rack1

三个节点的状态都是UN(Up 且 Normal),Owns 列各占约 33.3%,说明数据分布合理。到此,集群已经具备对外服务能力。你可以用 cqlsh 连接测试:

docker exec -it cassandra-node1 cqlsh -e "CREATE KEYSPACE test WITH replication = {'class':'SimpleStrategy', 'replication_factor':3};"

创建 keyspace 后,写入几条数据,再到 node2、node3 上查询,验证副本是否同步。

4. 配置细节和参数逻辑:为什么有些默认值必须改

4.1 cassandra.yaml 里影响集群行为的几个关键项

虽然官方镜像自带的cassandra.yaml已经是生产可用的基线配置,但有几个参数我建议在挂载的配置文件中显式修改。第一个是cluster_name,这个值要和 docker-compose 环境变量里的CASSANDRA_CLUSTER_NAME完全一致。如果某个节点的 cluster_name 对不上,gossip 协议会直接拒绝该节点加入,日志里会出现Cannot start node. Server name does not match之类的报错。

第二个是listen_address。在自定义挂载cassandra.yaml时需要注意,如果你在 yaml 里写死了 IP,那么容器重建后 IP 变化会导致节点失联。我建议在配置文件里将listen_address设置为cassandra-node1这样的主机名,或者留空交给环境变量控制。Cassandra 官方镜像的环境变量机制本质上是在启动时动态改写 yaml 里的占位符,所以你挂载自定义 yaml 时,需要确保 yaml 中的关键项是环境变量能够覆盖的。最稳妥的方式是让 yaml 里的listen_address为空字符串,而把实际地址通过环境变量CASSANDRA_LISTEN_ADDRESS注入。

第三个是endpoint_snitch。如果要支持跨机架部署,必须使用GossipingPropertyFileSnitch,这样 Cassandra 在分配副本时会考虑机架容错。默认的SimpleSnitch不感知拓扑,跨机架或跨机房时可能导致所有副本集中在同一故障域内。

第四个是auto_bootstrap。这个参数控制节点启动时是否自动从其他节点拉取数据。对于集群里第一个节点,必须设为false;后续加入的节点保持true。如果在已有数据的集群里误将一个新节点设为false,它会变成一个“空壳”节点,副本分布会出大问题。

4.2 JVM 堆内存与容器资源限制的匹配

Cassandra 的 JVM 堆配置是很多容器化部署翻车的高发区。默认情况下,cassandra-env.sh会根据物理内存的1/4自动设置堆大小,但这个逻辑在 Docker 里并不准确,因为它读取的是宿主机内存。如果宿主机内存很大(比如 64GB),而容器只分配了 4GB,JVM 启动时可能直接 OOM。

我通常在挂载目录里额外准备一份jvm-server.options,并显式设置-Xms4G和-Xmx4G(对应容器内存 8GB 的场景)。堆内存并不是越大越好,超过 8GB 反而会有较长的 GC 暂停。另外需要关注的是 Cassandra 默认使用CMS垃圾回收器的场景已经过去了,4.x 版本默认是G1GC,这个不要随意回退。

容器层面还有一个容易被忽略的参数:vm.max_map_count。Cassandra 底层大量使用内存映射文件(MMap),如果系统配置过低,会出现Out of Memory Error或启动失败。宿主机上执行:

sysctl -w vm.max_map_count=1048575

测试环境如果内存吃紧,可以把堆降到 2GB 左右,但对应的 concurrent compaction 阈值、memtable 大小也要同步调整,否则极易触发长时间的 stop-the-world GC。我见过一个测试集群,堆调到 1GB 后没有调整memtable_heap_space_in_mb,结果写入稍微一涨,整个节点就进入“假死”状态,日志里全是 GC 超时警告。

5. 我遇到的典型问题和排查记录

5.1 节点互相看不到:种子节点与广播地址的错误

这个坑在我自己的部署里出现过,也在帮别人排查时遇到过。表象是:三个容器都起来了,nodetool status只能看到自己,看不到其他节点,或者能看到其他节点但状态是DN(Down)。

排查思路分三步:第一步,进入容器执行nodetool gossipinfo,确认 gossip 是否已经开始。如果 gossipinfo 里只有本节点,说明 seed 连接没有建立。第二步,检查 seed 配置。常见错误是三个节点的CASSANDRA_SEEDS都填了cassandra-node1,但 node1 自身启动时因为AUTO_BOOTSTRAP=false没有参与 seed 服务,导致 node2、node3 找不到联系点。正确做法是 seed 至少配两个节点,并且所有节点保持一致。第三步,检查broadcast_address和listen_address的匹配关系。Cassandra 4.x 对广播地址非常敏感,如果广播地址配置成容器 IP 而其他节点在另一个宿主机上,必然无法通信,因为容器 IP 在容器重建后就会变。

一个更隐蔽的情况是使用了host网络模式,此时listen_address不能设为容器名,必须设为宿主机的 IP 或回环地址。如果你没有特殊需求,还是老老实实使用 bridge 网络加服务名解析,能让排查简单很多。

5.2 重启后数据诡异丢失:持久化别只依赖容器层

有一次我用 docker-compose 重启集群后,发现原本写入的数据全部消失了,当时第一反应是“数据损坏了?”,查了半天才意识到问题出在卷配置上。我最初只映射了cassandra.yaml和cassandra-rackdc.properties,数据目录/var/lib/cassandra没有挂载 volume。Docker 容器一旦删除重建,容器层的所有写操作都会丢失。

后来我把数据目录挂到了命名卷上,再重启一切正常。这里顺带提醒:如果使用 bind mount 方式(即直接挂宿主机目录,比如./data/node1:/var/lib/cassandra),需要确保宿主机目录的属主和权限与容器内运行用户一致,否则 Cassandra 会由于无法写入数据文件而启动失败。官方镜像内部使用cassandra用户运行,uid 通常是999,宿主机目录的属主要设置成999:999,或者用chown -R 999:999 ./data调整。

5.3 跨主机部署时的细节差异

如果三节点分布在不同物理机或虚拟机上,和单机多容器的最大区别就是网络和配置。单机多容器可以直接依赖 docker-compose 创建的 bridge 网络,让服务名互相解析;但跨主机时,服务名解析不会自动工作,需要借助外部 DNS、/etc/hosts或者把listen_address明确设置成宿主机 IP。

跨主机的种子节点配置也必须使用对端可达的 IP:例如 node1 在宿主机 A,node2 在宿主机 B,node2 的CASSANDRA_SEEDS应该包含 A 宿主机的 IP,而不是 node1 的容器名。同时,宿主机 A 和 B 之间需要放通7000和9042端口。另一个跨主机的细节是时钟同步,Cassandra 对节点间时钟偏差很敏感,偏差超过 NTP 默认阈值容易出现数据版本冲突写冲突。部署时确保所有宿主机都配置了 NTP 同步,这在物理机时代是老生常谈,到了容器时代反而容易被遗忘。

5.4 常见问题速查表

症状可能原因处理方式
节点状态始终DN广播地址不可达 / 端口未放通检查宿主机防火墙、安全组、broadcast_address 配置
日志反复Unable to gossip withseed 节点配置不一致或错误统一所有节点 seed 列表,确保 seed 节点已启动
启动后堆内存溢出容器内存限制与 JVM 堆不匹配挂载 jvm-server.options,显式设置堆大小
删除容器后数据丢失数据目录未挂载 volume将/var/lib/cassandra挂载到命名卷或宿主机目录
节点加入后拥有 0% 数据误将 auto_bootstrap 设为 false后续节点必须保持 bootstrap=true
跨主机通信失败gossip 端口未放通放通 7000/7001 端口,检查监听地址

6. 一点经验体会

这套方案用下来,我最想强调的一点是:容器化部署 Cassandra 绝不是“把大象装进冰箱”的三步走,它要求你对 Cassandra 本身的工作原理有足够的敬畏。gossip、种子节点、副本同步、compaction,每一个环节都可能因为一个环境变量、一个端口映射、一个目录权限而出问题。但反过来看,一旦把这些坑都趟平,Docker 带来的便利性是实实在在的:环境标准化、扩容节点只需复制服务定义、回滚版本只需切换镜像标签。

最后再分享一个小技巧:在 docker-compose.yml 里给每个节点加上healthcheck(如前面配置所示),然后用docker compose ps查看健康状态,这比每次手动进容器敲nodetool status高效得多。另外,我习惯把nodetool status的检查结果集成到监控告警里,因为只要所有节点的状态都是UN,很多东西就不会出大问题。这套部署方案目前在我维护的多个环境里运行稳定,如果你正在规划自己的 Cassandra 容器化集群,完全可以按这个清单起步。

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

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

立即咨询