自托管 Sentry 报 Unknown API Error、issues 页 504 排查:snuba-api 卡死与 ClickHouse 无关

自托管 Sentry 打开 issues 页面突然报 Unknown API Error,浏览器网络面板里一连串 504 Gateway Timeout。先把结论放前面:这不是 ClickHouse 挂了,是查询层 snuba-api 进程卡死(僵而不死)——容器还是 Up,但已经不响应任何请求。处置就一条 docker restart;但更值钱的是中间"怎么不靠猜、一步步把锅从 ClickHouse 头上摘下来,精确钉到 snuba-api"的过程。

这是自托管 Sentry 排障系列的第三篇,前两篇分别是《自托管 Sentry 安全加固实录》《Sentry 客户端上报全部 403 排查》,都是同一套 docker-compose 自托管栈上的坑。

环境

  • 部署方式:Sentry self-hosted(官方 getsentry/self-hosted docker-compose 栈,project 名 sentry-self-hosted,共 70+ 容器)
  • 查询层:snuba-api(Snuba,基于 ClickHouse 的事件查询服务,HTTP 监听容器内 :1218,进程用 granian)
  • 存储:clickhouse 25.3.x、postgreskafkaredis
  • 现象入口:https://sentry.example.com/organizations/sentry/issues/?statsPeriod=24h
  • 宿主:单机 docker,45G 内存、2T 盘

下文 IP/域名均为占位,替换成你自己的即可。容器名是官方 self-hosted 的默认命名,可直接对照。

Sentry 的 issues 页面(以及 tags、issues-count 这些)走的链路是:

浏览器 → sentry-web(Django)→ snuba-api(:1218)→ ClickHouse(:9000)

任何一段慢或挂,前端都会统一吞成一句没头没尾的 Unknown API Error。所以排障第一原则:别被这句笼统报错带跑,要把链路一段段拆开看

Sentry issues 查询链路与断点

一、现象:issues 页 Unknown API Error,网络层全是 504

页面顶部红条 Unknown API Error,刷新无效。打开浏览器网络面板,真正在报错的是这几个后端接口:

  • GET /api/0/organizations/sentry/issues/ → 504
  • GET /api/0/organizations/sentry/tags/ → 504
  • GET /api/0/organizations/sentry/issues-count/ → 500

Unknown API Error 只是前端对这些 5xx 的兜底文案。现象先记成客观事实:多个后端接口 504/500,不是个别接口——这指向某个公共依赖,而不是某个页面逻辑。

二、第一刀:docker ps 找出唯一不健康的容器

self-hosted 容器多(这套 72 个),先看健康状态的分布,而不是一个个翻:

docker ps -a --format '{{.Status}}' | awk '{print $1}' | sort | uniq -c
#   72 Up

# 把"不健康/重启/退出"的揪出来
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -iE 'unhealthy|restart|exited|dead'
# sentry-self-hosted-snuba-api-1   Up 3 weeks (unhealthy)

72 个容器全 Up,但唯一一个 (unhealthy) 就是 snuba-api。它正好在那条查询链路的中间,嫌疑最大。但"unhealthy"只是健康检查没过,还不能直接定罪——健康检查可能是探活逻辑本身的问题,也可能是它后面的 ClickHouse 拖累了它。继续往下拆。

三、别急着重启:先分清是 snuba-api 还是 ClickHouse

这一步是整篇的核心。snuba-api 不健康,可能是它自己的问题,也可能是它依赖的 ClickHouse 的问题——重启前必须分清,否则重启错对象就是白折腾

3.1 snuba-api 的健康检查为什么失败

docker inspect --format '{{json .State.Health}}' sentry-self-hosted-snuba-api-1
{"Status":"unhealthy","FailingStreak":12,
 "Log":[{"Output":"Health check exceeded timeout (1m30s)"}, ...]}

连续失败 12 次,每次都是 Health check exceeded timeout (1m30s)——健康检查探针超时,不是返回了错误码。再看它的自身日志:

docker logs sentry-self-hosted-snuba-api-1 --tail 60
... Error on clickhouse:9000 ping: Unexpected EOF while reading bytes
... Connection was closed, reconnecting.
... Error on socket shutdown: [Errno 107] Transport endpoint is not connected

满屏 Unexpected EOF——看着像 ClickHouse 把连接掐了。但注意两点:这些 EOF 是几小时一次的零星断连,不是连续刷屏;而且最后一条日志停在凌晨 02:32,之后到现在(健康检查 11:0x 还在失败)再没有任何日志。一个还在服务的进程不会几个小时不吭声。先把这个矛盾记下来,去对面看 ClickHouse 到底有没有问题。

3.2 ClickHouse 实测:容器 healthy ≠ 真能查

docker ps 显示 clickhouseUp 3 weeks (healthy)。但"容器 healthy"只代表它的健康检查(通常是个轻量 ping)过了,不等于它能正常服务查询。直接打一组实测:

# 磁盘:排除盘满
df -h /
# /dev/vda3  2.0T  248G  1.7T  13%   ← 13%,不是盘满

# 实测活查询能不能秒回
docker exec sentry-self-hosted-clickhouse-1 clickhouse-client \
  -q 'SELECT count() FROM system.processes'
# 1   ← 秒回,当前就 1 个查询(我自己)

# 关键健康指标:分区 part 数 / 常驻内存
docker exec sentry-self-hosted-clickhouse-1 clickhouse-client -q \
  "SELECT metric,value FROM system.asynchronous_metrics
   WHERE metric IN ('MaxPartCountForPartition','MemoryResident') FORMAT TSV"
# MaxPartCountForPartition  10        ← 远低于 300 的 too-many-parts 阈值
# MemoryResident            1336406016 ← 1.3G,很轻

# 近期错误日志
docker logs sentry-self-hosted-clickhouse-1 --tail 400 \
  | grep -iE '<Error>|Exception|Memory limit|Too many parts|No space'
# (只有 5/27 的旧 IPv6 监听 Warning,无近期错误)

逐条对账:盘 13% 不满、活查询秒回、part 数 10(健康)、内存 1.3G、无近期错误日志。ClickHouse 本身是好的,3.1 里那些 Unexpected EOF 只是偶发的空闲连接被掐,ClickHouse 端没有任何过载或故障痕迹。

3.3 直接探 snuba-api:确认它是真卡死

ClickHouse 排除了,回头直接验 snuba-api 到底还活不活。它的健康检查其实就是探容器内 :1218/health,我们手动探一次,给 15 秒超时:

timeout 15 docker exec sentry-self-hosted-snuba-api-1 \
  curl -s -m 10 -o /dev/null -w 'HTTP=%{http_code}' http://localhost:1218/health
# HTTP=000   (exit=124)

exit=124timeout 超时杀掉的,HTTP=000 表示根本没拿到任何响应。snuba-api 对自己的 /health 都不回包了——确认是进程卡死,不是 ClickHouse 慢

再看进程态印证:

docker top sentry-self-hosted-snuba-api-1
worker 1671751  ...  snuba-api            ← 主进程,5/27 起
worker 1689694  ...  snuba-api worker-1   ← 只有 1 个 worker
worker 2690811  ...  grep ok -s -m 1      ← 健康检查探针卡住没退出
worker 2693774  ...  grep ok -s -m 1      ← 一个个堆积
...

只有一个 worker,而且健康检查的 grep ok(读探活 socket)一个个卡住不返回、越堆越多——典型的单 worker 死锁:那个唯一的 worker 卡在一个没恢复的 ClickHouse 连接上,新请求全部排队等死。

3.4 web 端的铁证:Read timed out → 504

最后从调用方 sentry-web 的日志,拿到这次故障最直接的因果链:

docker logs sentry-self-hosted-web-1 --tail 500 \
  | grep -iE 'snuba|timed out|504|500'
sentry.utils.snuba.SnubaError: HTTPConnectionPool(host='snuba-api', port=1218):
    Read timed out. (read timeout=30)
[ERROR] django.request: Gateway Timeout: /api/0/organizations/sentry/tags/ (status_code=504 ...)
[ERROR] django.request: Internal Server Error: /api/0/organizations/sentry/issues-count/ (status_code=500 ...)

完整链条闭合了:web 调 snuba-api 卡在 30 秒读超时 → 抛 SnubaError → 对外吐 504/500 → 前端显示 Unknown API Error。issues 页要的 tagsissues-countissues 三个接口全栽在这上面。

四、根因

  • 直接原因:snuba-api 进程僵死。它只有 1 个 worker,某次与 ClickHouse 的连接 Unexpected EOF 断开后没有干净恢复,worker 卡在坏连接上,导致所有查询请求 30s 读超时。
  • 被排除项:ClickHouse 健康(盘、查询、part、内存、日志全正常),不是存储侧问题。
  • 底层诱因(隐患):那条几小时一次的 Error on clickhouse:9000 ping: Unexpected EOF,是空闲连接被网络层/ClickHouse 的 idle 超时掐断,snuba-api 连接池里留下了陈旧连接。平时偶发不影响,这次正好赶上唯一 worker 卡死、又没有自愈,才酿成整页打不开。

下面这张动图把整条故障与恢复过程串起来:① 请求进来 → ② web 调 snuba-api → ③ snuba-api 卡死、30s 读超时 → ④ 前端 504 / Unknown API Error → ⑤ 重启后链路恢复正常。

snuba-api 卡死到重启恢复的数据流向

五、处置:重启 snuba-api 并验证恢复

snuba-api 是无状态查询层(数据都在 ClickHouse,重启不丢数据),它当前本来就是坏的,重启是标准且可逆的处置:

docker restart sentry-self-hosted-snuba-api-1

重启后不要默认成功,按"探活 + 健康态 + 调用方日志"三处复核:

# 1) 直接探活:应秒回 200
docker exec sentry-self-hosted-snuba-api-1 bash -c \
 'exec 3<>/dev/tcp/127.0.0.1/1218; printf "GET /health HTTP/1.1\r\nhost: x\r\n\r\n" >&3; timeout 5 cat <&3 | head -1'
# HTTP/1.1 200 OK   ✅

# 2) 健康态:FailingStreak 归零、转 healthy
docker ps --format '{{.Names}}\t{{.Status}}' | grep snuba-api-1
# sentry-self-hosted-snuba-api-1   Up 30 seconds (healthy)

# 3) snuba-api 自身日志:干净重启,无报错
docker logs sentry-self-hosted-snuba-api-1 --tail 10
# Initializing Snuba... / Listening at: http://0.0.0.0:1218 / Started worker-1   ✅

刷新 issues 页,Unknown API Error 消失,504 不再出现。

复核时的一个坑:重启后立刻去 grep web 日志,可能还看到几条 504——要核对时间戳。如果最后一条 504 比 snuba-api 完成启动的时刻更早,那是重启完成前残留的,不是新故障。别被这种"时间错位"的旧日志骗着以为没修好。

六、隐患没除:长效修法

重启只是止血,3.x 里那条周期性 Unexpected EOF 还在,以后可能复发。三个方向治本:

  1. 加 worker 数:snuba-api 现在只有 1 个 worker,一卡全挂。多开几个 worker,单个连接卡死不至于整体不可用(对应 Snuba 的 gunicorn/granian worker 配置)。
  2. 健康检查失败自动重启:给容器加重启策略,让 unhealthy 能自愈,而不是靠人发现。最简单是配合 autoheal 之类的 sidecar,监听 health 状态自动 docker restart
  3. 把 unhealthy 接进监控告警:用一个小脚本周期性 docker inspect 各容器健康态、推成指标(Prometheus/夜莺等),snuba-api 不健康超过 N 分钟就告警到群——至少不再靠用户打开页面才发现。

快速参考

一句话定位流程(自托管 Sentry 页面 Unknown API Error / 504)

# 1. 找不健康容器
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -iE 'unhealthy|exited|restart'

# 2. 看调用方 web 的真实报错(最直接)
docker logs sentry-self-hosted-web-1 --tail 500 | grep -iE 'SnubaError|timed out|504|500'

# 3. 分清 snuba-api 还是 ClickHouse
docker exec sentry-self-hosted-clickhouse-1 clickhouse-client -q 'SELECT 1'   # ClickHouse 活不活
timeout 15 docker exec sentry-self-hosted-snuba-api-1 \
  curl -s -m 10 -o /dev/null -w 'HTTP=%{http_code}' http://localhost:1218/health  # snuba-api 活不活

# 4. snuba-api 卡死则重启(无状态,不丢数据)
docker restart sentry-self-hosted-snuba-api-1

几条铁律

  • 前端 Unknown API Error 是兜底文案,真因在后端,必看 web/snuba 日志,别盯着前端猜。
  • 容器 (healthy) ≠ 服务正常,容器 Up ≠ 进程在干活——都要实测一条真实查询/探活
  • 重启前先分清是查询层(snuba-api)还是存储层(ClickHouse),对账磁盘/活查询/part/内存/日志再下手。
  • snuba-api、web 是无状态,放心重启;有状态的 ClickHouse/Postgres/Kafka 不要随手重启
  • 复核成功以"探活 200 + healthy + 无新 504"为准,注意排除重启完成前的残留旧日志。
posted @ 2026-06-24 14:24  Hello_worlds  阅读(8)  评论(0)    收藏  举报