解释“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 运维三板斧结合起来,给出 可落地的排查闭环。
知识点
- CouchDB 资源模型:每个数据库在首次访问时由
couch_db_updater进程打开,句柄缓存在couch_server_ets表,引用计数归零才调用close_db释放 fd。 - Erlang 进程泄漏 或 异步消息堆积 会导致引用计数无法归零,形成“僵尸句柄”。
- Linux 层 fd 查看:
lsof -p <beam.smp pid>与/proc/<pid>/fd/可交叉验证;若 fd 类型以 .couch 与 .view 为主,则泄漏发生在 CouchDB 而非业务 Nginx。 - Erlang recon 利器:
recon:bin_leak(10)找出占用 fd 最多的进程;recon:proc_count(memory, 10)辅助判断是否为 死循环写视图。 - 国内云主机默认 ulimit 仅 65535,极易踩坑;需通过 systemd override 或 /etc/security/limits.conf 永久调大。
- CouchDB 2.x/3.x 已知 Bug:视图索引 compaction 期间若触发 kill -9 或 节点 OOM,可能留下 未关闭的 #file{} 端口,需升级到 3.3.2+ 或打 COUCHDB-3287 补丁。
答案
当监控发现 open_databases 曲线持续走高,我按以下 六步闭环 排查:
-
确认现象:
在 Grafana 中观察 open_databases > 历史基线 3 倍 且 回收斜率为零;同时lsof -p <beam> | wc -l与/proc/sys/fs/file-nr均同步上涨,排除监控误报。 -
定位泄漏层:
执行lsof -p <beam> | awk '{print $9}' | sort | uniq -c | sort -nr | head
若前 20 条全是/var/lib/couchdb/shards/xxx.couch或.view文件,则锁定 CouchDB 内部泄漏;若出现大量 socket,则优先排查 HTTP 长连接未关闭。 -
Erlang 层取证:
进入remsh执行
recon:bin_leak(10).
找出持有 #file{} port 最多的进程 PID;再执行
rp(process_info(Pid, messages)).
若消息队列堆积{db_updated, ...}或{compact_done, ...},说明 引用计数消息未消费。 -
代码级验证:
查看couch_server的open_databases计数与couch_db_updater的fd字段是否匹配;若ets:tab2list(couch_server)中存在 dbname 重复条目 且fd=undefined,则命中 COUCHDB-3287,需 滚动重启 并升级小版本。 -
临时止血:
通过curl -XPOST http://127.0.0.1:5986/_restart温和重启 couchjs 与 index_server 进程,不重启整个节点,可瞬间释放 80% 僵尸 fd;同时把 ulimit -n 655350 写入/etc/systemd/system/couchdb.service.d/override.conf并systemctl daemon-reload防止再次打满。 -
长期治理:
在 CI 阶段 加入soak test:持续压测 6 小时并采样/stats;若 open_databases 增长率 > 0.1/min 则自动失败。上线后通过 夜维脚本 每日对比open_databases与lsof数,差值超过 500 即告警,提前 3 天 发现泄漏。
拓展思考
-
如果泄漏的是 view 文件 而非 db 文件,如何区分 索引 compaction 异常 与 js 视图死循环?
答:在lsof结果中过滤.view后,用recon:proc_count(binary, 10)找出持有最大 binary 的couchjs进程;若其reductions/second持续 > 20 万,则基本判定 js map 函数死循环,需 下线对应设计文档 并修复代码。 -
国内金融客户常把 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 指标,避免误判。 -
若节点已无法
remsh登录(fd 打满),如何 不重启 拿到 Erlang 内部状态?
答:利用 国内阿里云 SLS 插件 预置的erlang:system_info(allocated_areas)与erlang:memory(system)通过 statsd 提前外发;紧急时可在宿主机gdb -p <beam>执行print erts_printf("%T", couch_server_ets)导出表快照,离线分析 泄漏源头。