本节摘要:Cassandra 运行在 JVM 上,MemTable 在堆内、Bloom/Key Cache 在堆外。4.x 推荐 G1GC,堆通常 4–8GB;一次 300ms+ STW 会让 Gossip 误判、CL 超时重试。SOURCE 5.1 强调 GC 停顿是分布式故障信令。
阅读完本节,你应当能够:
MaxGCPauseMillis 与堆大小jstat/GC log 关联 WriteTimeoutException监控上 Pending Compactions 正常,却突发 P99 写超时——翻 GC log 常见 Full GC 800ms。期间所有线程 STW,StorageProxy 认为副本超时,客户端重试 → 负载翻倍。MongoDB 同样怕大堆 Full GC;HBase RegionServer 常配 G1 + 较大堆但 MemStore flush 策略不同。Cassandra 因 高 churn 短生命周期对象(MemTable Cell),Young GC 频率本身就高。
内存分层(SOURCE 5.1):
| 区域 | 内容 | 配置 |
|---|---|---|
| 堆内 | MemTable、请求上下文 | memtable_heap_space_in_mb |
| 堆外 | Bloom、Key Cache、CL buffer | offheap_* |
| 磁盘 | CommitLog、SSTable | 独立 SSD |
G1 推荐(jvm.options):
-XX:+UseG1GC -XX:MaxGCPauseMillis=300 -Xms8G -Xmx8G -XX:G1RSetUpdatingPauseTimePercent=5
堆 >8GB 时 G1 Region 扫描成本上升,Full GC 风险增——更大内存应加节点而非无限扩堆。
关联症状:
| GC 现象 | Cassandra 表现 |
|---|---|
| Full GC 长 | UN 抖动、Hint 堆积 |
| Young GC 过频 | 写 P99 毛刺 |
| 堆 OOM | 拒写、节点 down |
CMS 已废弃:4.x 勿用 CMS;JDK 11+ 验证 G1 默认参数。
容器:K8s limit 必须大于 heap + offheap + OS cache 预算;OOMKill 触发 hint 风暴。
诊断:启用 -Xlog:gc*;对照 nodetool tpstats MutationStage 阻塞。
⚠️ 常见坑:16GB 堆「一劳永逸」——G1 Full GC 停顿随堆线性恶化。
💡 关键直觉:调 JVM 就是调分布式超时语义——GC 日志与 CL 告警同一屏看。
下一节讨论批写、缓存与 Compaction 协同。
# 启用 GC 日志(JDK 11+) -Xlog:gc*=info:file=/var/log/cassandra/gc.log:time,uptime # 定位 Full GC 与长停顿 grep -E "Full|Pause Full" /var/log/cassandra/gc.log | tail -20 # 用 GCViewer 等工具画停顿分布
GC 日志关键字段解读: [Full GC (System.gc()) ... 800ms] ← 全停顿,检查是否触发了 System.gc() [GC (Allocation Failure) ... 30ms] ← Young GC,频率高但单次短 [GC (Metadata GC Threshold) ...] ← 类元数据膨胀,检查代码类加载
诊断顺序:先确认停顿类型与频率,再对照同一时刻的 WriteTimeoutException 时间戳,如果高度重合,GC 停顿就是分布式超时的根因——这印证了「调 JVM 就是调分布式超时语义」。
-Xms8G -Xmx8G -XX:+UseG1GC -XX:MaxGCPauseMillis=300 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC # 防止应用误调 System.gc() -Xlog:gc*=info:file=/var/log/cassandra/gc.log:time,uptime
# 应用配置后验证生效 jcmd <pid> VM.flags | grep -E "UseG1GC|MaxGCPauseMillis"
注意 DisableExplicitGC 双刃:它能防误触发的 Full GC,但也让需要显式 GC 的工具(如某些 Profiler)失效。生产建议开启,调试期可暂时关闭。MaxGCPauseMillis 是目标不是保证,G1 会尽力逼近,超限时要先看堆用量与对象分配速率。
# Kubernetes 资源限制:heap + offheap + OS cache + 元数据余量 resources: requests: memory: 12Gi limits: memory: 16Gi # 8Gi heap + 4Gi offheap/OS + 4Gi 余量
# 容器内核对实际可用内存 free -h cat /sys/fs/cgroup/memory.max # cgroup v2
容器最危险的是「limit 卡在 heap 附近」:JVM 按 limit 申请元空间与堆外,OOMKiller 在堆还没耗尽时先杀进程,触发 hint 风暴与全环重试。预算原则:limit ≈ heap + offheap + OS cache + 20% 余量,并在启动脚本用 -Xms/-Xmx 显式固定堆,让 JVM 不随 limit 波动。
时间线还原(典型的分布式故障链): T0 节点 A 触发 Full GC(停顿 1.2s) T1 Gossip 误判 A 为 DOWN(Phi 值超阈值) T2 写 CL=QUORUM 时 A 应答超时 → WriteTimeoutException T3 客户端重试 → 负载翻倍 T4 其他节点开始为 A 生成 hint,内存上涨 T5 GC 结束后 A 恢复,回放 hint,又一轮 I/O 峰值
# 事后诊断:用 GC 日志时间戳对齐 tpstats 丢包窗口 grep "Pause Full" /var/log/cassandra/gc.log | tail -5 nodetool tpstats | grep -i "WriteTimeout\|Dropped" # 两条时间线吻合,即可确认 GC 停顿是根因
防治组合拳:堆控制在 8GB 内、DisableExplicitGC 防误触发、MaxGCPauseMillis=300 约束目标、监控 GC 停顿超过 500ms 立即告警。MongoDB 与 HBase 同样有这类 JVM 连锁故障——这不是 Cassandra 独有,而是「JVM 分布式系统」的通病,只是 Cassandra 的 Gossip 与 CL 把停顿放大了。
# 生产必配的 GC 相关指标 jstat -gcutil <pid> 5000 # 实时 GC 利用率 grep "Pause Full" gc.log | wc -l # 24h Full GC 次数 # 告警阈值:Full GC > 3 次/小时,或单次停顿 > 500ms
| 指标 | 健康基线 | 告警 |
|---|---|---|
| Young GC 频率 | 每秒数次 | 持续升高需查分配速率 |
| Full GC 频率 | 0–3 次/天 | 超过即介入 |
| 单次 STW | < 300ms | 超过 500ms 立即查 |
| Heap Used | < 70% | 超 85% 连续 5 分钟 |
调优闭环是「配置 → 观察 → 再配置」:每次改一个参数,用同一压测脚本对比基线,避免多变量混在一起无法归因。Cassandra 的 JVM 调优没有银弹参数,只有可观测的纪律。