Skip to content

Bug: SessionTracker ZSet 成员无限增长导致 Redis 内存泄漏 #714

@sususu98

Description

@sususu98

问题描述

SessionTracker 模块中的 provider:*:active_sessions 等 ZSet 会无限增长,导致 Redis 内存持续增加。在我的生产环境中,单个 provider 的 ZSet 累积了超过 12 万个过期成员,所有 ZSet 合计超过 21 万个过期成员

环境信息

  • CCH 版本:最新 dev 分支
  • Redis 版本:7-alpine
  • SESSION_TTL 配置:18000(5 小时)
  • 运行时长:约 2 个月

问题根因

1. 写入与清理的不对称

写入操作(每次请求都执行)

  • trackSession() - 添加到 global、key、user ZSets
  • updateProvider() - 添加到 provider ZSet
  • refreshSession() - 更新所有 ZSet 的时间戳

清理操作(仅在读取时执行)

  • countFromZSet() - 调用 zremrangebyscore 清理过期成员
  • 仅在 getProviderSessionCount()getProviderSessionCountBatch() 等读取方法中触发

2. expire(3600) 无法生效

代码中设置了 expire(key, 3600),但由于每次 zadd 都会刷新 TTL,只要有请求就永远不会过期:

// updateProvider() - 第 130-131 行
pipeline.zadd(`provider:${providerId}:active_sessions`, now, sessionId);
pipeline.expire(`provider:${providerId}:active_sessions`, 3600);

3. SESSION_TTL 硬编码问题

SessionTracker 硬编码了 5 分钟的 TTL,未读取环境变量:

// session-tracker.ts 第 20 行
private static readonly SESSION_TTL = 300000; // 5 分钟(毫秒)

// session-manager.ts 第 87 行 - 正确读取环境变量
private static readonly SESSION_TTL = parseInt(process.env.SESSION_TTL || "300", 10);

影响的 Redis Keys

Key 模式 清理触发条件 泄漏严重程度
global:active_sessions Dashboard 查看
key:${keyId}:active_sessions 限流检查
user:${userId}:active_sessions 限流检查
provider:${providerId}:active_sessions Dashboard 查看 / 限流检查

实际数据

provider:14:active_sessions: 120861 members, TTL=-1
provider:2:active_sessions: 44764 members, TTL=-1
provider:34:active_sessions: 13539 members, TTL=-1
provider:45:active_sessions: 11772 members, TTL=-1
provider:46:active_sessions: 7232 members, TTL=-1
...
Total stale members: 216,866

最早的成员时间戳距今 1777 小时(约 74 天),从未被清理。

建议修复方案

方案 1:写入时增加概率性清理(推荐)

trackSession()updateProvider()refreshSession() 中增加概率性清理,避免每次请求都执行清理:

// 在 pipeline 执行前,约 5% 概率触发清理
if (Math.random() < 0.05) {
  const expireTime = Date.now() - SessionTracker.SESSION_TTL;
  pipeline.zremrangebyscore(`provider:${providerId}:active_sessions`, "-inf", expireTime);
}

方案 2:统一 SESSION_TTL

从环境变量读取 TTL,保持一致性:

private static readonly SESSION_TTL = parseInt(process.env.SESSION_TTL || "300", 10) * 1000;

方案 3:后台定时清理任务

增加一个定时任务,定期扫描并清理所有 *:active_sessions ZSet。

临时缓解措施

# 手动清理过期成员(5 分钟前)
redis-cli ZREMRANGEBYSCORE "provider:14:active_sessions" -inf $(( $(date +%s%3N) - 300000 ))

# 或直接删除(会重建)
redis-cli DEL "provider:14:active_sessions"

相关代码位置

  • src/lib/session-tracker.ts - 主要问题代码
  • src/lib/session-manager.ts - SESSION_TTL 环境变量读取参考
  • src/lib/rate-limit/service.ts - 限流检查触发清理
  • src/actions/providers.ts - Dashboard 触发清理

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:sessionbugSomething isn't workingoncallCritical blocking issue requiring immediate oncall attention

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions