<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Observability · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/observability/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/observability/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>在 Fly.io 上监控 Django：我留下的 DevOps 工具，以及弃用的那些</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-10-22-monitoring-tools-two-products/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-10-22-monitoring-tools-two-products/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>DevOps</category>
      <category>Monitoring</category>
      <category>Observability</category>
      <category>Site Reliability</category>
      <category>Fly.io</category>
      <category>Django</category>
      <category>Cloudflare</category>
      <category>Sentry</category>
      <category>Crash Reporting</category>
      <category>Small Teams</category>
      <category>Tech Stack</category>
      <description>我怎样用每月约 2 美元监控 Fly.io 上的两个小 Django 应用：错误发到聊天频道、日志存在 S3 用 DuckDB 查询、Fly 自带指标、崩溃报告，以及我弃用了什么。</description>
      <content:encoded><![CDATA[<p>我有两个小产品：一个我做的集市兼社区应用，一个给上门报价的手艺人用的估价应用，都跑在 Fly.io 最便宜的机器上。我的 DevOps 监控归结起来就五样：把 ERROR 日志发到一个聊天频道；把每一行日志都归档到对象存储里，用 DuckDB 查询；Fly 自带的 Prometheus 指标；手机上的崩溃报告，用 Sentry 的开源 SDK 发到我自己的 API；以及每天早上一条汇总。</p>
<p>没有托管的 APM，生产环境没有链路追踪，也没有需要记着去打开的仪表板。唯一额外花钱的是日志归档，每月大约 2 美元。下面是完整清单，以及我弃用或不再信任的东西，其中一样让我追了好几天一个根本没有用户碰到过的故障。</p>

<h2 class="relative group">我说的监控是什么，以及规模有多小
    <div id="我说的监控是什么以及规模有多小" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%91%e8%af%b4%e7%9a%84%e7%9b%91%e6%8e%a7%e6%98%af%e4%bb%80%e4%b9%88%e4%bb%a5%e5%8f%8a%e8%a7%84%e6%a8%a1%e6%9c%89%e5%a4%9a%e5%b0%8f" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>我说的 DevOps 监控指两件事：在别人告诉我之前就发现东西坏了，以及不靠猜就能查出原因。下面每个工具都按这两条来评价。</p>
<p>去年我写过一篇<a href="https://jared.lynskey.co.nz/zh-cn/posts/2025/2025-10-13-devops-monitoring/" >搭建开发者真正会用的监控</a>，那时主要考虑的是团队：小团队用云厂商自带的监控，服务上百个了再上托管的 APM。这篇是同一个思路里最小的那一端。两个产品都是 Django，跑在 Fly 的 shared-cpu-1x 机器上，<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-07-22-my-current-stack/" >我当前的技术栈</a>里写过，而且两边的开发者都只有我一个。如果说监控是一个产品、开发者是它的用户，那我这套监控的用户就只有一个人。九月我从 Fly 的指标里拉了一次基线，交易平台的 API 平均 CPU 占用是 1%。这不是企业级的技术栈，我也不打算把它包装成那样。</p>
<p>指标、日志、链路这三根支柱，我有两根。两个产品都没有分布式追踪：各自就是一套 Django 代码加一个数据库，没有服务边界让链路去跨。我平时看的链路只有一种，就是<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-10-01-cloudflare-warp-dev-network/" >我工作站</a>上反向代理的链路。</p>

<h2 class="relative group">清单
    <div id="清单" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%b8%85%e5%8d%95" aria-label="锚点">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th>工具</th>
					<th>盯的是什么</th>
					<th>保留还是弃用，为什么</th>
					<th>费用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>团队聊天频道，由 Django 的日志 handler 推送</td>
					<td>API、WebSocket 服务器和 worker 的所有 ERROR；部署；集中出现的错误登录验证码</td>
					<td>保留。这是我不用别人提醒也会去看的唯一地方</td>
					<td>免费</td>
			</tr>
			<tr>
					<td>Fly 的日志转发器写到兼容 S3 的 bucket，用 DuckDB 查询</td>
					<td>交易平台各个应用的每一行日志</td>
					<td>保留。Fly 自己的缓冲只存几分钟，这里存全部，还能用 SQL 查</td>
					<td>每月约 2 美元，外加存储</td>
			</tr>
			<tr>
					<td>Fly 自带指标（Prometheus API）</td>
					<td>CPU、内存、突发额度、响应；托管 Postgres 的 exporter</td>
					<td>保留。是它证明了数据库卡顿不是我代码的问题</td>
					<td>Fly 自带</td>
			</tr>
			<tr>
					<td><code>/api/health/</code> 上的 Fly 健康检查（两个应用）</td>
					<td>某台机器该不该接流量</td>
					<td>用于路由，保留。当作“用户没事”的证据，弃用</td>
					<td>Fly 自带</td>
			</tr>
			<tr>
					<td>Sentry 的 SDK，DSN 指向我自己的 API</td>
					<td>手机上的应用崩溃</td>
					<td>SDK 保留。从没注册 sentry.io</td>
					<td>API 之外不额外花钱</td>
			</tr>
			<tr>
					<td>团队聊天里的早间汇总</td>
					<td>前一天的错误、使用量、搜索、聊天和看板</td>
					<td>保留。从 CI 的定时任务挪到了我的 Mac 上</td>
					<td>免费</td>
			</tr>
			<tr>
					<td>应用分析 SDK（两个应用）和搜索引擎的站长后台</td>
					<td>Web、iOS、Android 的使用情况；搜索引擎怎么看这个站</td>
					<td>保留，通过 API 读取</td>
					<td>免费</td>
			</tr>
			<tr>
					<td>Cloudflare 的 GraphQL 分析</td>
					<td>某个状态码或路径背后是谁</td>
					<td>保留，但相信它之前先按 user agent 分组</td>
					<td>免费套餐，每次查询一天</td>
			</tr>
			<tr>
					<td>一个托管 OTA 更新服务的仪表板</td>
					<td>手机上跑的是哪个 OTA 版本；崩溃回滚</td>
					<td>从一个什么都看不出来的后台列表换到了这里</td>
					<td>属于更新服务的一部分</td>
			</tr>
			<tr>
					<td>一个小的探测脚本</td>
					<td>部署或重启过程中失败的请求</td>
					<td>保留。直接打源站，缓存藏不住空档</td>
					<td>免费</td>
			</tr>
			<tr>
					<td><code>flyctl logs</code></td>
					<td>应用最近几分钟的输出</td>
					<td>当实时查看用，保留。当记录用，弃用</td>
					<td>Fly 自带</td>
			</tr>
			<tr>
					<td>Cloudflare Early Hints</td>
					<td>什么都不盯，它是个提速功能</td>
					<td>保留了，前提是让源站发出 <code>Link</code> 头。整个 zone 的 504 全是它的缓存未命中</td>
					<td>免费</td>
			</tr>
	</tbody>
</table>
<p>表里没写“两个应用”的，都只说交易平台。估价应用的东西更少：Fly 的健康检查、日志和指标，iOS 上的应用分析，再加一个每天把数据库导出成 Parquet、我用 DuckDB 查询的任务。它没有崩溃报告，也没有告警频道，这是这篇里最大的缺口。（关于更新服务那一行要说明一下：做这个服务的公司付钱请我给他们的博客写文章。）</p>

<h2 class="relative group">错误：把日志发到聊天频道的 logging handler
    <div id="错误把日志发到聊天频道的-logging-handler" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%94%99%e8%af%af%e6%8a%8a%e6%97%a5%e5%bf%97%e5%8f%91%e5%88%b0%e8%81%8a%e5%a4%a9%e9%a2%91%e9%81%93%e7%9a%84-logging-handler" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>九月之前，未处理的 500、发送失败的推送通知、坏掉的 WebSocket 帧，唯一会出现的地方是 <code>flyctl logs</code>，而那条流没人会主动去读。现在由一个日志 handler 把 ERROR 及以上的每条记录都发到聊天频道，不管来自 API、WebSocket 服务器还是后台 worker。WARNING 故意不告警。</p>
<p>handler 在后台线程里发送，所以绝不会拖慢请求。它内部不写任何日志，所以不会递归。同一个错误最多每十分钟发一次，每小时上限 30 条，所以出故障时你读到的是一条消息，而不是一千条。</p>
<p>这个频道里还有我想拿来和错误对照着看的东西：部署在开始、上线成功和失败的地方都会发消息，带上 commit 和执行人，OTA 更新和 <a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-14-web-releases-without-redeploying/" >Web 发布</a>也一样。某个账号短时间内集中输错登录验证码，也会记成 ERROR，所以有人在乱猜的话，会和故障出现在同一个地方。</p>
<p>十月来过一条告警，任何日志里都找不到对应的行。从那以后，每条告警都带上一个短的随机 id、主机名和进程的启动命令，同一行也写进那个进程自己的日志。找不到 id，就说明它来自一个不往那里写日志的进程，比如一个 SSH shell。频道里的所有消息也会进一条信息流，和我一起干活的编程 agent 可以读，所以在改代码的 agent 和我同时看到这些错误。</p>

<h2 class="relative group">日志：用 Fly 的 log shipper 存到 S3，用 DuckDB 查询
    <div id="日志用-fly-的-log-shipper-存到-s3用-duckdb-查询" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%97%a5%e5%bf%97%e7%94%a8-fly-%e7%9a%84-log-shipper-%e5%ad%98%e5%88%b0-s3%e7%94%a8-duckdb-%e6%9f%a5%e8%af%a2" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>Fly 自己的缓冲只有几分钟的输出，大概最后一百行，盯部署够用，查昨天的事就没用了。所以第三个 Fly 应用跑着 Fly 的日志转发器，就是订阅了组织日志流的 Vector，我只改了它的 S3 sink：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">sinks</span><span class="p">.</span><span class="nx">aws_s3</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;aws_s3&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">&#34;myapp-logs&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">compression</span> <span class="p">=</span> <span class="s2">&#34;gzip&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">key_prefix</span> <span class="p">=</span> <span class="s2">&#34;logs/{{fly.app.name}}/%F/&#34;</span>   <span class="c"># one folder per app per day</span>
</span></span><span class="line"><span class="cl">  <span class="nx">batch</span><span class="p">.</span><span class="nx">timeout_secs</span> <span class="p">=</span> <span class="mi">300</span>                  <span class="c"># a quiet app still lands its lines</span></span></span></code></pre></div></div>
<p>交易平台各个应用的每一行日志，都以 gzip 压缩的 JSON 落进一个兼容 S3 的 bucket。转发器是一台 256 MB 的 shared-cpu-1x 机器，每月大约 2 美元，存储费可以忽略。DuckDB 用一个宏直接就地读取这些文件，一天的数据一秒左右就出来：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">MACRO</span><span class="w"> </span><span class="n">logs</span><span class="p">(</span><span class="n">app</span><span class="p">,</span><span class="w"> </span><span class="k">day</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;*&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">TABLE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">read_json_auto</span><span class="p">(</span><span class="s1">&#39;s3://myapp-logs/logs/&#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">app</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39;/&#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="k">day</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39;/*.log.gz&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                               </span><span class="n">union_by_name</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="k">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">timestamp</span><span class="p">,</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="p">(</span><span class="s1">&#39;myapp-api&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-09-06&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;% 500 %&#39;</span><span class="p">;</span></span></span></code></pre></div></div>
<p>我在<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-09-scale-to-zero-504s/" >缩容到零那篇</a>里测冷启动，用的就是这个宏：启动脚本每个阶段都会打一行，每行都带着机器 id，剩下的就是一个 <code>GROUP BY</code>。</p>
<p>所以同一条日志流按读者分成三层：归档保留 INFO 及以上（这也是生产环境的应用用 INFO 级别打日志的原因），实时查看脚本只显示警告和错误，聊天频道收 ERROR。不过三者都不知道是谁发的请求。访问日志里没有客户端 IP，也没有 user agent，这个得去 Cloudflare 查。</p>

<h2 class="relative group">指标：Fly 自带的 Prometheus
    <div id="指标fly-自带的-prometheus" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8c%87%e6%a0%87fly-%e8%87%aa%e5%b8%a6%e7%9a%84-prometheus" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>Fly 给每个组织跑一个托管的 Prometheus，所以我自己不维护任何指标系统。有问题时直接查它的 API：每台机器的内存、CPU 和突发额度（共享 CPU 上 <code>fly_instance_cpu_balance</code> 很要紧），每个应用的响应数和并发数。有两件事我希望早点知道。用 <code>fly tokens create</code> 创建的 token 要以 <code>Authorization: FlyV1 &lt;token&gt;</code> 传入；<code>Bearer</code> 只对 <code>flyctl auth token</code> 给出的个人 token 有效，而只读 token 会返回 &ldquo;something went wrong resolving organization&rdquo;。另外数据大约只保留两周（Fly 的说法是 15 天左右）。</p>
<p>它在九月真正派上了用场。那时日志里一天好几次出现 &ldquo;Could not open a database connection&rdquo;，看上去完全就是应用的 bug。托管 Postgres 集群也在同一个 Prometheus 里，每次出现这个错误的那一分钟，主库的抓取耗时都从 80 毫秒左右跳到一到十秒，副本一切正常，Postgres 的启动时间也没变。Postgres 没重启过，是它底下的宿主机卡住了。我把时间戳发给了 Fly 的支持团队；十月初一次维护重启把主库挪到另一台宿主机之后，每天的卡顿就没了。</p>

<h2 class="relative group">崩溃报告：指向我自己 API 的 Sentry SDK
    <div id="崩溃报告指向我自己-api-的-sentry-sdk" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b4%a9%e6%ba%83%e6%8a%a5%e5%91%8a%e6%8c%87%e5%90%91%e6%88%91%e8%87%aa%e5%b7%b1-api-%e7%9a%84-sentry-sdk" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>以前应用在手机上崩了，我能看到的地方一点痕迹都没有。最直接的答案是 Sentry，我只用了它的一半：应用里装的是 Sentry 的开源 SDK，但 DSN 指向我自己 Django API 里的一个接口，这个接口接收 Sentry 的 envelope 格式，把崩溃归类，再通过同一个聊天 handler 告警。链路追踪、会话和回放都关掉了，只收错误。</p>
<p>没注册 sentry.io，是因为一天就几条报告，为此多一家掌握用户数据的厂商、隐私政策里多一行、再多一张账单，不值得。代价是符号化（symbolication），这块我还没补上。堆栈要么是压缩过的 JavaScript，要么是原始的 native 地址，所以一个 issue 能告诉我崩的是什么、哪个版本、哪台设备、用户崩之前做了什么，但说不出是源码的哪一行。</p>

<h2 class="relative group">唯一的仪表板是一条消息
    <div id="唯一的仪表板是一条消息" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%94%af%e4%b8%80%e7%9a%84%e4%bb%aa%e8%a1%a8%e6%9d%bf%e6%98%af%e4%b8%80%e6%9d%a1%e6%b6%88%e6%81%af" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>两个应用我都没做仪表板。去年那篇里写过一个检验标准：没人主动打开的仪表板，就是没证明自己的价值。自己送上门的消息不用打开，所以我最接近仪表板的东西，是每天早上发到团队聊天里的一条汇总。</p>
<p>开头是过去 24 小时的生产错误，从日志归档里读，而不是从聊天频道里读，因为告警限了频率，说不出一件事到底发生了多少次。数字、id 和地址先打码再归类，所以同一个失败出现一百次也只是一行加个计数，邮箱地址也不会流进频道。</p>
<p>接着是失败的健康检查、昨天的使用量和上周同一天的对比（周日永远不会像周一），最近一周的搜索流量、等着回复的聊天、任务看板，还有开销。哪一部分加载不出来就照实写，汇总照样发出去。</p>
<p>通过 API 读应用分析数据，会让你对这些数字多一点怀疑。八月份，阿塞拜疆来了 121 次 Android 首次打开，随后是 119 次卸载。那个月的 Android 总数，我是扣掉这些来看的。</p>
<p>发布有自己的界面。以前 OTA 更新从我自己的服务器发的时候，我看一次发布的方式就是后台里一个带 &ldquo;is active&rdquo; 复选框的列表，根本不知道有多少台手机真的装上了。现在这个服务的仪表板能看到设备实际在跑哪个版本、安装情况如何，坏掉的 bundle 崩溃、SDK 退回上一个版本时还会显示这次崩溃回滚。<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-13-moving-to-codemagic-patch/" >迁移到托管更新服务的经过</a>我另外写过。</p>

<h2 class="relative group">弃用的，或者不再相信的
    <div id="弃用的或者不再相信的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%bc%83%e7%94%a8%e7%9a%84%e6%88%96%e8%80%85%e4%b8%8d%e5%86%8d%e7%9b%b8%e4%bf%a1%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这里面没有一样是付费工具。大多是我一直当成信号、其实不是信号的东西，还有一个放错了地方的任务。</p>

<h3 class="relative group">按 user agent 分组之前的 Cloudflare 504 比例
    <div id="按-user-agent-分组之前的-cloudflare-504-比例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8c%89-user-agent-%e5%88%86%e7%bb%84%e4%b9%8b%e5%89%8d%e7%9a%84-cloudflare-504-%e6%af%94%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>九月，Cloudflare 面板上显示一天里有 22% 的请求以 504 失败，集中在应用每 30 秒轮询一次的那几个接口上，而健康检查全是绿的，应用自己的日志也说这些请求很快。我在上面花了好几天，发现机器冷启动要 25 到 33 秒、一天要冷启动 92 次，修好了，然后在<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-09-scale-to-zero-504s/" >缩容到零那篇</a>里把它写成了原因。</p>
<p>第二天早上，我把同样的 504 按 user agent 分组。9 月 5 日到 9 日之间的 7,344 个 504，全部来自 Cloudflare 自家的 Early Hints 缓存查询（user agent 是 <code>bastion early hints</code> 和 <code>nginx-ssl early hints</code>），它们根本不会到达源站：这些行里的 504 记录的是一次缓存未命中，而不是超时。它们照搬真实请求的路径，所以都落在最忙的接口上，看起来和用户请求失败一模一样。应用、浏览器、curl 和搜索引擎爬虫一个 504 都没有。源站当时不发 <code>Link</code> 头，Early Hints 什么作用都没起，于是我先把它关了，后来才<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/cloudflare-504-early-hints/" >让它真正生效</a>。</p>
<p>冷启动的优化并没有白做，部署和唤醒都因此变快了，但我以为自己在修的那个故障，从来没有落到任何用户身上。</p>
<p>所以我最想分享的一条规则是：相信 CDN 的错误率之前，先按 user agent 分组，确认那不是 CDN 自己的机制产生的。在 Cloudflare 的免费套餐上，就是对 <code>httpRequestsAdaptiveGroups</code> 发 GraphQL 查询，一次查一天。下一次虚惊也是靠同一个查询解决的：有个轮询接口从每天 70 到 260 次请求涨到了 2,880 次。不是爬虫，是我自己的 Mac，测试完把应用开在 iOS 模拟器里忘了关。</p>

<h3 class="relative group">一路绿灯的健康检查
    <div id="一路绿灯的健康检查" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%80%e8%b7%af%e7%bb%bf%e7%81%af%e7%9a%84%e5%81%a5%e5%ba%b7%e6%a3%80%e6%9f%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Fly 每 30 秒检查一次两个应用的 <code>/api/health/</code>，用结果决定哪些机器接流量，这件事它做得很好。但健康检查跑在已经起来的机器上，看不到机器起来之前发生的事：冷启动、代理前面的排队、根本没到达的请求。现在我只指望它做路由。</p>
<p>部署时改用探测脚本，它在整个部署过程中持续打源站，只要有一次失败就以退出码 1 结束。它抓到过一次普通的 <code>flyctl deploy</code> 留下了 7.3 秒的空档；换成先唤醒一台备用机器的部署脚本之后，2,514 个请求里一个失败都没有。</p>
<p>健康检查自己还造成过两次故障。七月，Django 的 <code>SECURE_SSL_REDIRECT</code> 给 Fly 的明文 HTTP 探测回了 301，所有机器检查失败，所有人都收到了 503。八月，我把健康检查视图里的 <code>except Exception</code> 收窄了，结果 &ldquo;connection already closed&rdquo; 错误以 500 漏了出去，Fly 把机器摘掉了，而视图里的重试恰恰就是为这种情况准备的。现在这个检查只问一件事：这个进程能不能处理请求。数据库卡住时，两个应用也不会再被一起摘出路由。</p>
<p>早间汇总在计数上也学到了同样的教训。交易平台会把空闲机器挂起，被请求唤醒的机器在启动后几百毫秒时第一次检查会失败，下一次就通过了。把这些都算上，每天早上的汇总读起来都像出了故障。所以现在一分钟内重新通过的失败会单独列出，标注为机器唤醒和部署。</p>

<h3 class="relative group">CI 上的定时任务
    <div id="ci-上的定时任务" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ci-%e4%b8%8a%e7%9a%84%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>早间汇总一开始是一个按 cron 定时跑的 CI workflow，不到一天就挪走了。它要读应用分析和搜索数据之后，需要的密钥就成了分析项目的服务账号密钥，这个密钥在那个项目里什么都能干，我不想把它放进 CI 的 secrets。现在由我 Mac 上的定时调度程序每 20 分钟运行一次，只在奥克兰时间早上八点以后、而且当天还没发过的时候才发，所以八点时在睡眠的 Mac 会在醒来时补发。workflow 留着当手动备用，只是不带应用分析和搜索那两部分。</p>

<h2 class="relative group">如果从头再来
    <div id="如果从头再来" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a6%82%e6%9e%9c%e4%bb%8e%e5%a4%b4%e5%86%8d%e6%9d%a5" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>我会在第一个用户到来之前、而不是第一次故障之后，先搭好三样东西：把错误发到团队本来就在看的聊天频道，并且限流；一个能用 SQL 查询的对象存储日志归档；以及平台本来就在收集的指标。每月花几美元而已。应用上手机的那一周加上崩溃报告，不然手机上的崩溃谁也不会知道。等要查看的地方多到我干脆一个都不看了，再加早间汇总。</p>
<p>链路追踪，我会等到出现第二个服务、请求真的要跨服务时再上；托管的 APM，等到有一个需要共享仪表板、值班轮换、也有预算替别人付账单的团队时再说。去年那篇里提到的工具，是从那时候开始才回本的。在那之前，最有用的习惯不花钱：一个数字看起来很吓人时，先搞清楚是什么产生了它，再判断是什么坏了。这篇里的好几次虚惊，都出在监控本身，而不是产品。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-22-monitoring-tools-two-products/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>开发者真正会用的监控系统：指标、日志、链路追踪，以及 CloudWatch 和 Datadog 怎么选</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2025/2025-10-13-devops-monitoring/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2025/2025-10-13-devops-monitoring/</guid>
      <pubDate>Sun, 12 Oct 2025 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>DevOps</category>
      <category>Monitoring</category>
      <category>Observability</category>
      <category>CloudWatch</category>
      <category>Datadog</category>
      <category>Developer Experience</category>
      <category>Site Reliability</category>
      <description>怎么搭一套开发者真正会用的监控系统：把指标、日志和链路打通，让告警直接说明从哪查起，以及什么时候 CloudWatch 就够、什么时候值得上 Datadog。</description>
      <content:encoded><![CDATA[<p>给开发者用的监控系统，说到底只有一件事要做：出了问题尽快让人知道，并且直接告诉他坏了什么、坏在哪。要做到这一点，指标、日志和链路得互相打通，告警要指明从哪开始查，仪表板得是大家不用催也会主动打开的那种。不管是自己开发监控系统，还是用现成的工具，标准都一样；跑在 AWS 上的话，小团队用 CloudWatch 就够了，规模上去了，Datadog 才物有所值。</p>
<p>&ldquo;在我机器上能跑&quot;和&quot;在生产环境能跑&quot;之间有一道鸿沟，监控本该把它填上。可大多数时候填不上，因为监控往往被当成运维工具来建，而不是开发者出事时会伸手去拿的东西。</p>
<p>我最近开始以自由职业身份做 DevOps 和基础设施的活儿，每到一个新环境，最先看的东西之一就是监控配置——它比 README 更能说明一个团队的真实状态。失败的方式几乎千篇一律：工具都在，但和开发者的实际工作方式是脱节的。有人在一个 bug 上耗了三小时，其实能看生产日志的话五分钟就完事；故障不是靠告警发现的，是靠客服工单；或者反过来，一天 200 条告警，绝大部分是噪音，于是所有人干脆全部无视，每次出事的前半小时都花在搞清&quot;到底坏了什么&quot;上，而不是修它。</p>
<p>这篇讲的是通用做法。我现在给两个产品<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-10-22-monitoring-tools-two-products/" >实际在用的工具</a>，另外写了一篇。</p>

<h2 class="relative group">指标、日志、链路：打通了才有用
    <div id="指标日志链路打通了才有用" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8c%87%e6%a0%87%e6%97%a5%e5%bf%97%e9%93%be%e8%b7%af%e6%89%93%e9%80%9a%e4%ba%86%e6%89%8d%e6%9c%89%e7%94%a8" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>指标告诉你出问题了（错误率飙升、延迟突增），日志告诉你发生了什么（堆栈、请求内容、报错信息），链路告诉你问题在哪（哪个服务、哪个接口、哪次数据库调用）。这些都是常识。关键在于三者有没有打通：告警一响，你应该能从指标一路点到相关日志，再点到链路。如果开发者还得在三个工具之间手动比对时间戳，那不管每个工具单看多漂亮，这套方案就是失败的。</p>
<p>仪表板也用同一条标准检验。CPU、内存、磁盘 I/O 对容量规划很重要，但对排查一个 500 错误毫无帮助。大家会主动打开的仪表板，上面放的是：技术指标旁边摆着业务指标（错误率旁边是每小时注册数）、时间轴上叠加的最近部署、最近一小时的前五个错误（带日志链接），以及延迟百分位——p50、p95、p99——而不只是平均值。一个没人主动打开的仪表板，就是没有证明自己的价值。</p>
<p>告警同理。每条告警都该回答两个问题：坏了什么？从哪开始查？</p>
<p>坏告警：&ldquo;web-server-3 CPU 高&rdquo;</p>
<p>好告警：&ldquo;14:32 起 /api/payments 错误率 &gt; 5%。最近部署：14:15，@sarah。[查看日志] [查看链路]&rdquo;</p>
<p>往第二种靠拢有几个办法：按基线偏离告警，而不是按拍脑袋定的阈值；按归属路由（支付的错误发给支付团队，别全组织广播）；相关错误合并成一条 Slack 消息，别发 50 条。反馈环也要收紧——从&quot;出事了&quot;到&quot;开发者知道了&quot;应该在一分钟以内，这意味着实时日志流而不是五分钟一批、每个仪表板都有部署标记、以及跨服务边界不断链的分布式追踪。</p>

<h2 class="relative group">为用它的人而建
    <div id="为用它的人而建" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ba%e7%94%a8%e5%ae%83%e7%9a%84%e4%ba%ba%e8%80%8c%e5%bb%ba" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>运维团队最大的错误，是给自己建监控。解决办法一点也不高深：在开发者调试的时候坐到旁边，看他们在哪卡住；问问事故当中大家在问什么——&ldquo;哪个服务？&ldquo;&ldquo;什么变了？&ldquo;&ldquo;请求长什么样？&quot;；再搞清楚对产品真正要紧的指标是什么。基本不会是 CPU，多半是结账完成率、上传成功率这类东西。</p>
<p>然后是消除阻力。给新服务接监控要花一整天的话，没人会做。要提供能自动接入你们在用框架（Django、Express、Spring）的库、可以直接复制的仪表板和告警模板，以及不用提工单就能自己加指标的自助工具。整套东西还要当代码来管：配置进版本控制，告警规则要测试（该响的时候真的会响），要做多区域的话一开始就规划进去。</p>
<p>建好之后，还得靠同样的习惯让它活下去。埋点代码和其他生产代码一样走代码评审，自定义指标要配测试。每次复盘都要问一句&quot;什么样的监控能让我们更早发现&rdquo;，答案当周就落到仪表板和告警里，而不是只留在复盘报告中。另外我一年会清理几次，把没人打开的东西删掉，因为一条没人处理的告警，只会教大家忽略下一条。</p>

<h2 class="relative group">AWS 上的小团队？先用 CloudWatch
    <div id="aws-上的小团队先用-cloudwatch" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#aws-%e4%b8%8a%e7%9a%84%e5%b0%8f%e5%9b%a2%e9%98%9f%e5%85%88%e7%94%a8-cloudwatch" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>不到 50 个工程师、又跑在 AWS 上的话，CloudWatch 就是正确起点，第一天就想买更高级工具的冲动最好忍住。EC2、Lambda、RDS、ECS 全都自动上报 CloudWatch，不写一行埋点代码就有了可见性；小团队每月 10~50 美元，没有固定平台费；指标、日志、链路（走 X-Ray）、告警都在一个地方。像样的告警和仪表板，一个下午就能搭出来，用不了几周。</p>
<p>最被低估的是 CloudWatch Logs Insights——不用架 Elasticsearch 就能查日志：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">fields @timestamp, @message
</span></span><span class="line"><span class="cl">| filter @message like /ERROR/
</span></span><span class="line"><span class="cl">| stats count() by bin(5m)</span></span></code></pre></div></div>
<p>其他几点：</p>
<ul>
<li>复合告警能压噪音：错误率高&quot;且&quot;响应时间劣化时才告警，别每个条件单独响。</li>
<li>真正的价值在自定义指标。用 CloudWatch SDK 把业务指标（注册、交易、功能使用）和基础设施指标放在一起。</li>
<li>CloudWatch Synthetics 能按计划跑金丝雀测试，模拟真实用户路径，让你赶在用户之前发现结账坏了。</li>
<li>X-Ray 用极少的配置就能拿到分布式追踪，对大多数微服务架构够用了。</li>
</ul>
<p>什么时候 CloudWatch 不够用了，也很好判断：要上多云了、想要正经的异常检测了、仪表板定制开始难受了，或者团队过了 50 人、需要真正的协作功能了。</p>

<h2 class="relative group">团队大了：Datadog
    <div id="团队大了datadog" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9b%a2%e9%98%9f%e5%a4%a7%e4%ba%86datadog" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>Datadog 很贵——大组织一年 2 万到 10 万美元以上很正常——但到了规模就值回票价。AWS、Azure、GCP、本地机房、容器、Serverless、数据库、前端，一个视图全看到。Watchdog 不用手动调阈值就能标记异常模式，这点很关键，因为几千个服务是不可能靠人盯的。它还补上了 CloudWatch 缺的协作层：团队仪表板、RBAC、共享调查笔记本、PagerDuty/Opsgenie 集成、多条件告警、阈值突破预测、维护窗口，以及能下钻到代码级性能分析的 APM 和自动生成的服务拓扑图。</p>
<p>推行顺序我大致会这么排：先给关键服务接入，第一天就按团队和环境把标签打全；指标命名规范、仪表板模板、告警级别、SLO 定义要趁早定好——事后补标准是苦役；接入 CI/CD 部署标记、事件管理和 Slack；给培训留出时间，Datadog 强归强，不是看看就会的；最后盯紧账单：过滤噪音指标、大流量服务对链路采样、定期盘点到底在为哪些功能付费。</p>
<p>签约之前值得比较一下：New Relic（定位类似，大流量追踪有时更便宜）、Dynatrace（AIOps 很强，金融行业常见）、Splunk（日志分析顶级，安全团队已经在用的话更合适）、Grafana Cloud（已经在用 Prometheus/Loki 的话是自然归宿）。</p>
<p>实践中很多团队最后落在混合方案上：AWS 原生服务交给自动又便宜的 CloudWatch，应用层交给 Datadog，再让 Datadog 采集 CloudWatch 指标拼出统一视图。不花哨，但好使。</p>

<h2 class="relative group">CloudWatch 和 Datadog 对比
    <div id="cloudwatch-和-datadog-对比" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#cloudwatch-%e5%92%8c-datadog-%e5%af%b9%e6%af%94" aria-label="锚点">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>CloudWatch</th>
					<th>Datadog</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>适合谁</td>
					<td>不到 50 个工程师、全在 AWS 上的团队</td>
					<td>服务多、用不止一家云的大组织</td>
			</tr>
			<tr>
					<td>费用结构</td>
					<td>按量付费，没有平台费；小团队每月 10~50 美元</td>
					<td>大组织一年 2 万到 10 万美元以上很正常，账单得一直盯着</td>
			</tr>
			<tr>
					<td>上手成本</td>
					<td>EC2、Lambda、RDS、ECS 自动上报，一个下午就能搭出像样的告警</td>
					<td>先给关键服务接入，趁早定好标签和命名规范，还要留出培训时间</td>
			</tr>
			<tr>
					<td>多云</td>
					<td>以 AWS 为中心，要上多云就说明该换了</td>
					<td>AWS、Azure、GCP、本地机房、容器、Serverless 一个视图全看到</td>
			</tr>
			<tr>
					<td>异常检测</td>
					<td>基本是自己调的告警，想要更多也说明该换了</td>
					<td>Watchdog 不用手动调阈值就能标记异常</td>
			</tr>
			<tr>
					<td>协作</td>
					<td>比较薄弱，团队过了 50 人就会不够用</td>
					<td>团队仪表板、RBAC、共享调查笔记本、PagerDuty/Opsgenie 集成</td>
			</tr>
			<tr>
					<td>链路追踪</td>
					<td>X-Ray，配置很少</td>
					<td>能下钻到代码级性能分析的 APM，自动生成服务拓扑图</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">从零开始：监控系统搭建顺序
    <div id="从零开始监控系统搭建顺序" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bb%8e%e9%9b%b6%e5%bc%80%e5%a7%8b%e7%9b%91%e6%8e%a7%e7%b3%bb%e7%bb%9f%e6%90%ad%e5%bb%ba%e9%a1%ba%e5%ba%8f" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>我的顺序是：</p>
<ol>
<li>先找开发者聊，问清事故当中什么最痛。</li>
<li>给要紧的流程定义 SLO：对登录、结账、搜索来说，&ldquo;正常&quot;到底是什么意思。</li>
<li>从这条关键路径开始埋点。</li>
<li>接着加分布式追踪，在微服务架构里它的排障回报率最高。</li>
<li>告警挂在这些流程上，而不是挂在 CPU 上，每条告警发给负责的团队。</li>
<li>给每条告警配一份运维手册，让凌晨三点的自己知道该查什么。</li>
<li>在日历上排一个季度回顾，删掉过期告警、修正漂移的阈值、确认仪表板还对得上现在的架构。</li>
</ol>
<p>再列几个我反复把团队从里面捞出来的坑：工具太多（能配合的 3 个胜过各自为战的 6 个）；图表没有基线（500 rps 是正常水位还是十倍突增？）；监控了用户根本感知不到的东西（无状态容器的磁盘 I/O）；还有监控系统本身没有冗余——告警系统在事故中间挂掉，你就在最不能瞎的时候瞎了。</p>
<p>这些没有一件需要最花哨的工具。需要的只是：让开发者在他们真正会看的地方，快速拿到需要的信息。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2025/2025-10-13-devops-monitoring/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
