<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>DevOps on Jiangwan&#39;s Blog</title>
        <link>https://jiangwan.ink/categories/devops/</link>
        <description>Recent content in DevOps on Jiangwan&#39;s Blog</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sun, 10 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jiangwan.ink/categories/devops/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>用 systemd timer 实现轻量服务告警</title>
        <link>https://jiangwan.ink/p/lightweight-service-monitoring-systemd-email-alert/</link>
        <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
        
        <guid>https://jiangwan.ink/p/lightweight-service-monitoring-systemd-email-alert/</guid>
        <description>&lt;h2 id=&#34;一故障背景与监控需求&#34;&gt;一、故障背景与监控需求
&lt;/h2&gt;&lt;p&gt;这次接入监控的直接原因，是前端在短时间内出现过两次离线，而我都没有在第一时间感知到。&lt;/p&gt;
&lt;p&gt;第一次问题来自部署流程。前端和反向代理相关的 Caddy 配置还在分支上，没有合并到主线；当时线上仍然使用旧的部署脚本，导致前端入口离线。这个问题不是服务代码本身异常，而是部署脚本和真实配置状态没有对齐。&lt;/p&gt;
&lt;p&gt;第二次问题更直接。前端容器已经创建出来，但停留在 &lt;code&gt;Created&lt;/code&gt; 状态，没有真正启动。后端、数据库、Redis、模型网关这些服务都还在，API 健康检查也能返回，但用户访问前端页面时仍然打不开。最后手动启动 web 容器后，服务才恢复。&lt;/p&gt;
&lt;p&gt;这两次故障暴露的问题不是缺少复杂指标，而是缺少基础可用性告警。当前阶段，我需要及时知道服务是否仍在正常运行。&lt;/p&gt;
&lt;h2 id=&#34;二监控方案选型&#34;&gt;二、监控方案选型
&lt;/h2&gt;&lt;p&gt;Prometheus、Alertmanager 和 Grafana 是更完整的监控组合。它们适合采集时间序列指标，做告警路由，画长期趋势图，也适合后面分析“什么时候开始变慢”“哪类请求错误率更高”“资源是否接近瓶颈”。&lt;/p&gt;
&lt;p&gt;但这次需求还没到那个阶段。当前服务规模不大，告警接收人也只有我自己，真正要解决的是“服务异常时能否及时感知”。如果为了这个目标再部署一整套 Prometheus 体系，反而会引入新的维护成本：Prometheus 自己要部署，Alertmanager 要配置，规则要维护，Grafana 也要管理。&lt;/p&gt;
&lt;p&gt;所以这次选型很克制：用 &lt;code&gt;systemd timer&lt;/code&gt; 每分钟触发一个 Python 脚本，脚本检查公网入口和 Docker 容器状态，连续失败达到阈值后通过 SMTP 发邮件。它不是完整可观测性平台，但足够回答当前最关键的问题：服务是否还可用。&lt;/p&gt;
&lt;h2 id=&#34;三健康检查范围&#34;&gt;三、健康检查范围
&lt;/h2&gt;&lt;p&gt;检查对象分成两层：用户入口是否可访问，以及内部组件是否处于健康运行状态。&lt;/p&gt;
&lt;p&gt;公网入口只保留最关键的几个点：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;检查项&lt;/th&gt;
          &lt;th&gt;目的&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;首页是否能正常返回 HTML&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/app&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;主要业务页面入口是否能打开&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/admin&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;管理或次级业务页面入口是否能打开&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;后端 API 健康检查是否正常&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这几项不是为了覆盖所有业务细节，而是为了判断用户入口和后端基础状态是否可用。前端离线这类问题，单看后端 health endpoint 是看不出来的，所以页面入口必须单独检查。&lt;/p&gt;
&lt;p&gt;容器侧则只看关键组件：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;容器&lt;/th&gt;
          &lt;th&gt;作用&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;project-web-1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;前端静态站点&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;project-api-1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;API 后端&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;project-worker-1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;后台任务 worker&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;project-postgres-1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;PostgreSQL 数据库&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;project-redis-1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Redis&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;project-gateway-1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;内部网关或下游依赖&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里检查的是 Docker 状态里的 &lt;code&gt;running&lt;/code&gt; 和 healthcheck 结果。它不能证明每条业务链路都完整可用，但能及时发现“容器未启动”“容器 unhealthy”这类基础故障。&lt;/p&gt;
&lt;h2 id=&#34;四实现与部署方式&#34;&gt;四、实现与部署方式
&lt;/h2&gt;&lt;p&gt;实现上没有写成长驻进程，而是用 &lt;code&gt;systemd timer&lt;/code&gt; 定时调起脚本。健康检查本身是短任务，执行完成后即可退出，不需要再维护一个额外的服务进程。&lt;/p&gt;
&lt;p&gt;整体流程是：timer 每分钟触发一次 Python 脚本，脚本依次检查 URL 和容器状态，然后把结果写进本地状态文件。如果连续失败次数达到阈值，就发告警邮件；如果之前处于异常状态，后来恢复了，就发恢复邮件。&lt;/p&gt;
&lt;pre class=&#34;mermaid&#34;&gt;
  graph TD
    A[systemd timer 每分钟触发] --&amp;gt; B[Python 健康检查脚本]
    B --&amp;gt; C[检查公网 URL]
    B --&amp;gt; D[检查 Docker 容器状态]
    B --&amp;gt; E[更新本地状态文件]
    E --&amp;gt; F{连续失败达到阈值?}
    F --&amp;gt;|是| G[SMTP 发送告警邮件]
    F --&amp;gt;|否| H[只记录状态]
    E --&amp;gt; I{从失败恢复?}
    I --&amp;gt;|是| J[SMTP 发送恢复邮件]
&lt;/pre&gt;

&lt;p&gt;告警策略也尽量简单：每 60 秒检查一次，连续 2 次失败才发邮件。同一故障持续存在时，不每分钟重复发，而是隔一段时间再提醒；恢复后单独发一封恢复邮件。这样能过滤短暂网络抖动，也不会让真实故障沉默太久。&lt;/p&gt;
&lt;p&gt;配置放在 root 只读的 env 文件里，SMTP 授权码不写进日志，也不写进文章。类似这样：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;PROJECT_HEALTH_WATCHDOG_FAILURE_THRESHOLD&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;PROJECT_HEALTH_WATCHDOG_REPEAT_INTERVAL_SECONDS&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;10800&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;PROJECT_HEALTH_WATCHDOG_TIMEOUT_SECONDS&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;SMTP_HOST&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;smtp.example.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;SMTP_PORT&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;465&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;SMTP_USER&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;sender@example.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;SMTP_PASSWORD&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;smtp-authorization-code&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;SMTP_USE_TLS&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;ALERT_TO&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;receiver@example.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;部署时把脚本、env 文件、systemd service 和 timer 都收进 Ansible role。Ansible 负责创建目录、渲染配置、安装 unit，并启用 timer。这样以后调整检查项或者换机器，不需要再靠手工记忆去服务器上改文件。&lt;/p&gt;
&lt;p&gt;上线后只看 &lt;code&gt;active&lt;/code&gt; 还不够，还要确认 timer 真的在触发、日志里每个检查项都是预期结果，并且测试邮件能送达。健康检查脚本能跑通，不代表 SMTP 链路一定可用；邮件链路必须单独验证。&lt;/p&gt;
&lt;h2 id=&#34;五适用边界与后续演进&#34;&gt;五、适用边界与后续演进
&lt;/h2&gt;&lt;p&gt;这套方案适合服务规模还不大、维护者较少、第一目标是“异常时能够收到通知”的阶段。它的价值在于低成本、低维护、可解释：出现问题后，可以直接查看 &lt;code&gt;systemctl&lt;/code&gt;、&lt;code&gt;journalctl&lt;/code&gt;、状态文件和脚本输出。&lt;/p&gt;
&lt;p&gt;它不适合替代完整的可观测性平台。请求耗时趋势、错误率统计、资源曲线、下游服务调用成本、多级告警、长期历史指标，这些都不是它擅长的事情。等服务规模变大，或者开始需要分析“为什么慢”“什么时候开始变差”，Prometheus 这类体系仍然应该引入。&lt;/p&gt;
&lt;p&gt;但在这次场景里，先做轻量监控是更实际的选择。前端曾经因为部署脚本和容器状态问题离线过，说明当前最缺的不是复杂指标，而是一套基础可用性告警机制。先把服务从“异常无人感知”变成“连续异常会触发邮件通知”，这一步已经足够有价值。&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
