解释“open_databases”持续飙高可能预示的 fd 泄漏排查步骤。

解读

在国内生产环境中,CouchDB 的 open_databases 指标由 /stats/couchdb/open_databases 暴露,代表当前被 Erlang VM 保持打开状态的数据库句柄数。该值本应随请求波峰波谷而上下浮动;若 持续单向走高 且重启节点后才回落,则极大概率是 文件描述符(fd)泄漏 的前兆。一旦 fd 数逼近系统 ulimit -n 上限,新连接、视图访问、甚至内部 replicator 都会报 EMFILE"too many open files",触发 集群雪崩。面试官问此题,意在考察候选人能否把 Erlang 资源模型、CouchDB 内部引用计数与 Linux 运维三板斧结合起来,给出 可落地的排查闭环

知识点

  1. CouchDB 资源模型:每个数据库在首次访问时由 couch_db_updater 进程打开,句柄缓存在 couch_server_ets 表,引用计数归零才调用 close_db 释放 fd。
  2. Erlang 进程泄漏异步消息堆积 会导致引用计数无法归零,形成“僵尸句柄”。
  3. Linux 层 fd 查看lsof -p <beam.smp pid>/proc/<pid>/fd/ 可交叉验证;若 fd 类型以 .couch.view 为主,则泄漏发生在 CouchDB 而非业务 Nginx。
  4. Erlang recon 利器recon:bin_leak(10) 找出占用 fd 最多的进程;recon:proc_count(memory, 10) 辅助判断是否为 死循环写视图
  5. 国内云主机默认 ulimit 仅 65535,极易踩坑;需通过 systemd override/etc/security/limits.conf 永久调大。
  6. CouchDB 2.x/3.x 已知 Bug:视图索引 compaction 期间若触发 kill -9节点 OOM,可能留下 未关闭的 #file{} 端口,需升级到 3.3.2+ 或打 COUCHDB-3287 补丁。

答案

当监控发现 open_databases 曲线持续走高,我按以下 六步闭环 排查:

  1. 确认现象
    在 Grafana 中观察 open_databases > 历史基线 3 倍回收斜率为零;同时 lsof -p <beam> | wc -l/proc/sys/fs/file-nr 均同步上涨,排除监控误报。

  2. 定位泄漏层
    执行 lsof -p <beam> | awk '{print $9}' | sort | uniq -c | sort -nr | head
    若前 20 条全是 /var/lib/couchdb/shards/xxx.couch.view 文件,则锁定 CouchDB 内部泄漏;若出现大量 socket,则优先排查 HTTP 长连接未关闭

  3. Erlang 层取证
    进入 remsh 执行
    recon:bin_leak(10).
    找出持有 #file{} port 最多的进程 PID;再执行
    rp(process_info(Pid, messages)).
    若消息队列堆积 {db_updated, ...}{compact_done, ...},说明 引用计数消息未消费

  4. 代码级验证
    查看 couch_serveropen_databases 计数与 couch_db_updaterfd 字段是否匹配;若 ets:tab2list(couch_server) 中存在 dbname 重复条目fd=undefined,则命中 COUCHDB-3287,需 滚动重启 并升级小版本。

  5. 临时止血
    通过 curl -XPOST http://127.0.0.1:5986/_restart 温和重启 couchjsindex_server 进程,不重启整个节点,可瞬间释放 80% 僵尸 fd;同时把 ulimit -n 655350 写入 /etc/systemd/system/couchdb.service.d/override.confsystemctl daemon-reload 防止再次打满。

  6. 长期治理
    CI 阶段 加入 soak test:持续压测 6 小时并采样 /stats;若 open_databases 增长率 > 0.1/min 则自动失败。上线后通过 夜维脚本 每日对比 open_databaseslsof 数,差值超过 500 即告警,提前 3 天 发现泄漏。

拓展思考

  1. 如果泄漏的是 view 文件 而非 db 文件,如何区分 索引 compaction 异常js 视图死循环
    答:在 lsof 结果中过滤 .view 后,用 recon:proc_count(binary, 10) 找出持有最大 binary 的 couchjs 进程;若其 reductions/second 持续 > 20 万,则基本判定 js map 函数死循环,需 下线对应设计文档 并修复代码。

  2. 国内金融客户常把 CouchDB 部署在 K8s,sidecar 采集 /stats 发现 open_databases 高但 lsof 数正常,原因何在?
    答:这是 cgroup v1 vs v2 的统计差异;容器中 /proc/sys/fs/file-nr 看到的是 宿主机的全局 fd,而 open_databases 只统计 Erlang 内部端口。此时应 进入容器 netns 再执行 lsof,或直接用 kubectl top pod --containers 对比 ephemeral-storage 指标,避免误判。

  3. 若节点已无法 remsh 登录(fd 打满),如何 不重启 拿到 Erlang 内部状态?
    答:利用 国内阿里云 SLS 插件 预置的 erlang:system_info(allocated_areas)erlang:memory(system) 通过 statsd 提前外发;紧急时可在宿主机 gdb -p <beam> 执行 print erts_printf("%T", couch_server_ets) 导出表快照,离线分析 泄漏源头。