<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Shirone</title><description>A Material 3 anime blog</description><link>https://meow.us.kg/</link><language>zh_CN</language><item><title>备份与恢复策略：3-2-1 原则与 rsync、restic 实践</title><link>https://meow.us.kg/posts/backup-strategy-321-rsync-restic/</link><guid isPermaLink="true">https://meow.us.kg/posts/backup-strategy-321-rsync-restic/</guid><description>备份不等于同步：理解 3-2-1 备份原则，用 rsync 做本地快照、restic 做加密去重备份，并建立定期恢复演练的习惯。</description><pubDate>Wed, 21 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;运维圈流传一句话：「数据丢失只有两种人——没备份的，和没验证过恢复的。」备份是系列文章的最后一篇，因为它是所有操作的底线：配置可以重建，系统可以重装，唯独数据丢了就是丢了。&lt;/p&gt;
&lt;h2&gt;先纠正一个致命误区：备份 ≠ 同步&lt;/h2&gt;
&lt;p&gt;很多人用「网盘同步」「RAID 1」「rsync 镜像」当备份，这些方案有一个共同的盲区：&lt;strong&gt;它们忠实地复制错误&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;误删一个文件，同步工具立刻把副本也删了。&lt;/li&gt;
&lt;li&gt;勒索软件加密了数据，RAID 的两块盘一起变成密文。&lt;/li&gt;
&lt;li&gt;数据库写入了脏数据，镜像忠实复制了脏数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正的备份必须能回答：「我想回到&lt;strong&gt;三天前&lt;/strong&gt;的那个版本」——这需要&lt;strong&gt;历史版本&lt;/strong&gt;和&lt;strong&gt;不可变性&lt;/strong&gt;，是同步类方案给不了的。&lt;/p&gt;
&lt;h2&gt;3-2-1 原则&lt;/h2&gt;
&lt;p&gt;经典的备份策略只有一句话：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3&lt;/strong&gt; 份数据副本（生产数据 + 2 份备份）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2&lt;/strong&gt; 种不同介质（如服务器磁盘 + 对象存储）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1&lt;/strong&gt; 份在异地（防火灾、机房故障、云账号被封）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对一台个人云服务器，可落地的组合是：本地快照（快速恢复）+ 异地对象存储（灾难恢复）。成本不过每月几元，换来的是睡得着觉。&lt;/p&gt;
&lt;h2&gt;工具一：rsync 做本地快照&lt;/h2&gt;
&lt;p&gt;rsync 是增量传输的瑞士军刀，适合做本机或内网的快速备份层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rsync -avz --delete \
  --exclude={&apos;/dev/*&apos;,&apos;/proc/*&apos;,&apos;/sys/*&apos;,&apos;/tmp/*&apos;} \
  /var/www/ /backup/www-$(date +%F)/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-a&lt;/code&gt; 归档模式（保留权限、时间戳、软链）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v -z&lt;/code&gt; 显示过程、传输压缩&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--delete&lt;/code&gt; 让目标与源一致（&lt;strong&gt;用前想清楚&lt;/strong&gt;：源目录误删会同步到备份，这就是它只能算「快照」不能算唯一备份的原因）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配合 &lt;code&gt;--link-dest&lt;/code&gt; 可以实现「占空间很小的每日全量快照」：未变化的文件用硬链接共享同一份数据，每天一个目录却几乎不占额外空间。&lt;/p&gt;
&lt;p&gt;数据库备份注意：&lt;strong&gt;不能直接 rsync 数据目录&lt;/strong&gt;——运行中的数据库文件处于不一致状态。正确顺序是先导出逻辑备份：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# PostgreSQL
pg_dump -U myapp myapp | gzip &amp;gt; /backup/db/myapp-$(date +%F).sql.gz

# MySQL
mysqldump --single-transaction -u myapp -p myapp | gzip &amp;gt; /backup/db/myapp-$(date +%F).sql.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;code&gt;--single-transaction&lt;/code&gt; 对 InnoDB 做一致性快照导出，不锁表。）&lt;/p&gt;
&lt;h2&gt;工具二：restic 做加密、去重、版本化备份&lt;/h2&gt;
&lt;p&gt;restic 是现代备份工具的代表：增量去重、端到端加密、支持 S3 兼容存储，一份数据加密后才离开服务器——就算存储端被攻破，攻击者拿到的也是密文。&lt;/p&gt;
&lt;p&gt;初始化仓库（以 S3 兼容对象存储为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export AWS_ACCESS_KEY_ID=xxx
export AWS_SECRET_ACCESS_KEY=xxx
export RESTIC_PASSWORD=备份仓库的加密口令

restic -r s3:https://s3.example.com/mybucket/backups init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日常备份一条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;restic -r s3:https://s3.example.com/mybucket/backups \
  backup /var/www /etc /backup/db \
  --exclude-file /etc/restic/excludes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;restic 每次备份生成一个快照，自动去重——重复备份 100 GB 数据，第二次只传变化的部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;保留策略&lt;/strong&gt;：旧快照不是无限留的，按策略清理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;restic -r s3:... forget --prune \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保留最近 7 个日备份、4 个周备份、6 个月备份，其余删除并回收空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定时化&lt;/strong&gt;：把备份命令写成脚本，用 cron 或 systemd timer 每天凌晨执行，并把结果输出到日志，纳入监控告警——「备份任务失败」必须是一条会叫醒人的告警，静默失败的备份等于没有备份。&lt;/p&gt;
&lt;h2&gt;恢复演练：备份的真正验收&lt;/h2&gt;
&lt;p&gt;没恢复过的备份只能算「愿望」。restic 的恢复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 列出快照
restic -r s3:... snapshots

# 恢复指定快照到临时目录（先在临时位置验证，别直接覆盖生产）
restic -r s3:... restore latest --target /tmp/restore-check

# 只取回单个文件
restic -r s3:... dump latest /var/www/config.yml &amp;gt; config.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建议每季度做一次完整演练：把备份恢复到一台干净的机器，验证应用真的能跑起来。演练会暴露各种想当然——比如备份里漏了某个关键目录、数据库导出脚本早就因为密码轮换而失败。&lt;/p&gt;
&lt;h2&gt;一张个人服务器备份方案清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;每日凌晨：&lt;code&gt;pg_dump&lt;/code&gt;/&lt;code&gt;mysqldump&lt;/code&gt; 导出数据库到本地目录。&lt;/li&gt;
&lt;li&gt;随后：restic 把「应用数据 + /etc 配置 + 数据库导出」加密备份到异地对象存储，保留 7 日/4 周/6 月。&lt;/li&gt;
&lt;li&gt;备份失败 → 触发告警，当天处理。&lt;/li&gt;
&lt;li&gt;每季度：恢复到测试环境演练一次。&lt;/li&gt;
&lt;li&gt;加密口令、存储密钥记录在离线的密码管理器里——备份全在，口令丢了，一样是数据丢失。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套方案全部用开源工具和对象存储搭建，月成本一杯咖啡钱，换的是任何单点事故都不致命的底气。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://restic.readthedocs.io/en/latest/&quot;&gt;restic 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://download.samba.org/pub/rsync/rsync.1&quot;&gt;rsync 手册页&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/backup.html&quot;&gt;PostgreSQL 官方文档：Backup and Restore&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>云服务器初始化安全清单：安全组、SSH 加固与 fail2ban</title><link>https://meow.us.kg/posts/cloud-server-initial-hardening/</link><guid isPermaLink="true">https://meow.us.kg/posts/cloud-server-initial-hardening/</guid><description>新购云服务器上架后的 30 分钟加固流程：安全组最小开放、创建普通用户、SSH 密钥登录与端口策略、fail2ban 防爆破、自动安全更新。</description><pubDate>Mon, 19 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;一台新云服务器从拿到公网 IP 那一刻起，就开始被全互联网的扫描器光顾——SSH 爆破尝试通常在几分钟内出现。默认配置的服务器在公网上等于裸奔。这套清单按「先挡住外面的，再管好里面的」排序，30 分钟内可以全部完成。&lt;/p&gt;
&lt;h2&gt;第 1 步：安全组只开必要端口（云厂商层面）&lt;/h2&gt;
&lt;p&gt;安全组是云平台的虚拟防火墙，规则在系统之外生效——就算系统内防火墙没配，安全组也能兜底。原则：&lt;strong&gt;默认全拒，按需放行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一台 Web 服务器的典型放行清单：&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;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;22（或自定义 SSH 端口）&lt;/td&gt;
&lt;td&gt;你的办公/家庭 IP 段&lt;/td&gt;
&lt;td&gt;SSH 管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;HTTP（跳转 HTTPS 及证书验证）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;数据库 3306、Redis 6379、管理面板端口&lt;strong&gt;永远不要&lt;/strong&gt;对公网开放，内部访问走 VPC 内网。SSH 来源限制为固定 IP 段是性价比最高的一条规则，直接砍掉 99% 的爆破流量。&lt;/p&gt;
&lt;h2&gt;第 2 步：创建普通用户，告别 root 日常操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;adduser deploy
usermod -aG sudo deploy        # Debian/Ubuntu；RHEL 系是 wheel 组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此后所有日常操作用 &lt;code&gt;deploy&lt;/code&gt; 登录，需要特权时 &lt;code&gt;sudo&lt;/code&gt;。直接以 root 工作，一个手滑的 &lt;code&gt;rm -rf&lt;/code&gt; 就没有任何挽回余地。&lt;/p&gt;
&lt;h2&gt;第 3 步：SSH 密钥登录，关掉密码&lt;/h2&gt;
&lt;p&gt;在&lt;strong&gt;本地&lt;/strong&gt;生成密钥（如果还没有）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &quot;deploy@myserver&quot;
ssh-copy-id deploy@服务器IP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证密钥能登录后，修改服务器 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PasswordAuthentication no       # 禁用密码登录
PermitRootLogin no              # 禁止 root 直接登录
PubkeyAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改完务必&lt;strong&gt;新开一个终端验证能登录，再关掉旧会话&lt;/strong&gt;——配置写错又断开会话，就只能靠云厂商的 VNC 控制台救场了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sshd -t                    # 语法检查
sudo systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于改 SSH 端口（如 2222）：它能减少日志噪音，但对有目的的扫描者作用有限（全端口扫描照样找到）。可做可不做；如果改，记得同步更新安全组。&lt;/p&gt;
&lt;h2&gt;第 4 步：fail2ban 封禁爆破 IP&lt;/h2&gt;
&lt;p&gt;即使有密钥登录，爆破流量仍会灌满日志。fail2ban 监控日志，把反复失败的 IP 自动拉进防火墙黑名单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;jail.local&lt;/code&gt; 中调整 sshd 段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：10 分钟内失败 5 次，封禁 1 小时。启动并查看状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 5 步：系统防火墙（ufw）作为第二道门&lt;/h2&gt;
&lt;p&gt;安全组在云侧，ufw 在系统侧，两层独立互备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp            # 若改了 SSH 端口则填新端口
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;ufw enable&lt;/code&gt; 前确认 SSH 端口已放行，否则会把自己锁在门外。云服务器至少还有安全组和 VNC 可以补救，裸金属机器上这条错误可能意味着一趟机房。
:::&lt;/p&gt;
&lt;h2&gt;第 6 步：开启自动安全更新&lt;/h2&gt;
&lt;p&gt;系统漏洞补丁不及时打，前面的加固都是沙上筑塔。Debian/Ubuntu 用 &lt;code&gt;unattended-upgrades&lt;/code&gt; 自动安装安全更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades   # 交互式确认开启
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认只自动更新安全源（security），不动功能更新——这个平衡对服务器正合适。&lt;/p&gt;
&lt;h2&gt;第 7 步：时间同步与基础可观测&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;确认 &lt;code&gt;timedatectl&lt;/code&gt; 显示 NTP 同步正常——证书验证、日志时间线、排障对齐全依赖准确时间。&lt;/li&gt;
&lt;li&gt;顺手装上排障工具：&lt;code&gt;apt install htop iotop ncdu mtr-tiny&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在云平台控制台配置&lt;strong&gt;监控告警&lt;/strong&gt;（CPU、磁盘、带宽），这是系统外的一层眼睛。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;加固完成后的自查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ss -tlnp                         # 监听端口是否与预期一致，没有多余服务
grep &quot;PasswordAuthentication&quot; /etc/ssh/sshd_config
sudo fail2ban-client status      # jail 是否生效
sudo ufw status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对照清单逐项打钩。安全不是一次性动作——定期检查登录日志（&lt;code&gt;journalctl -u ssh&lt;/code&gt;）、关注所用软件的安全公告，才是常态。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://documentation.ubuntu.com/server/how-to/security/openssh-server/&quot;&gt;Ubuntu 官方文档：OpenSSH Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fail2ban.readthedocs.io/en/latest/&quot;&gt;fail2ban 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cisecurity.org/cis-benchmarks&quot;&gt;CIS Benchmarks（系统加固基线）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>用 GitHub Actions 搭建静态博客自动部署流水线</title><link>https://meow.us.kg/posts/github-actions-auto-deploy-blog/</link><guid isPermaLink="true">https://meow.us.kg/posts/github-actions-auto-deploy-blog/</guid><description>从 push 触发、构建、部署到通知的完整链路：为 Astro/Hugo 类静态博客编写 GitHub Actions 工作流，实现提交即上线。</description><pubDate>Sun, 18 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;静态博客的发布流程本应是「写完、push、完事」，但很多人还在手动 &lt;code&gt;npm run build&lt;/code&gt;、手动 &lt;code&gt;rsync&lt;/code&gt; 上传——每次发文都多一步出错的机会。GitHub Actions 可以把这条链路完全自动化：push 到主分支后，构建、部署、通知全部自动完成。本文以一个 Node 系静态站（Astro/Hugo 同理）为例走通全流程。&lt;/p&gt;
&lt;h2&gt;工作流全貌&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;push 到 main
   │
   ▼
GitHub Actions Runner 启动
   ├─ 1. 拉代码
   ├─ 2. 装依赖、构建静态产物
   ├─ 3. 部署产物（rsync 到服务器 / 推到 Pages）
   └─ 4. 失败时通知
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作流文件放在仓库的 &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Deploy Blog

on:
  push:
    branches: [main]
  workflow_dispatch:          # 允许在网页上手动触发

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 拉取代码
        uses: actions/checkout@v4

      - name: 安装 Node
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: 安装依赖并构建
        run: |
          npm ci
          npm run build

      - name: 部署到服务器
        uses: burnett01/rsync-deployments@7.0.1
        with:
          switches: -avzr --delete
          path: dist/
          remote_path: /var/www/blog/
          remote_host: ${{ secrets.DEPLOY_HOST }}
          remote_user: ${{ secrets.DEPLOY_USER }}
          remote_key: ${{ secrets.DEPLOY_KEY }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;逐段拆解&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;触发器 &lt;code&gt;on&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;push: branches: [main]&lt;/code&gt; 表示推送到 main 分支时触发；&lt;code&gt;workflow_dispatch&lt;/code&gt; 加一个手动按钮，改完配置想立刻验证时不用特意造一个 commit。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;npm ci&lt;/code&gt; 而不是 &lt;code&gt;npm install&lt;/code&gt;&lt;/strong&gt;：CI 环境的铁律。&lt;code&gt;ci&lt;/code&gt; 严格按 &lt;code&gt;package-lock.json&lt;/code&gt; 安装，保证每次构建依赖版本完全一致；&lt;code&gt;install&lt;/code&gt; 可能按 semver 范围悄悄升级某个包，让「本地能过、CI 挂掉」变成悬案。搭配 &lt;code&gt;setup-node&lt;/code&gt; 的 &lt;code&gt;cache: npm&lt;/code&gt;，依赖下载在多次运行间缓存，构建快很多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;部署环节的安全&lt;/strong&gt;：服务器地址、用户名、SSH 私钥全部放仓库的 Settings → Secrets and variables → Actions 里，以 &lt;code&gt;${{ secrets.XXX }}&lt;/code&gt; 引用。私钥内容&lt;strong&gt;绝不能&lt;/strong&gt;写进 YAML——这个文件在仓库里是公开的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--delete&lt;/code&gt; 参数让 rsync 删除服务器上多余的旧文件，保持目录与构建产物一致；它对部署目录是安全的，但务必确认 &lt;code&gt;remote_path&lt;/code&gt; 没填错——填成 &lt;code&gt;/var/www/&lt;/code&gt; 会把兄弟站点的目录清掉。&lt;/p&gt;
&lt;h2&gt;为部署准备专用 SSH 密钥&lt;/h2&gt;
&lt;p&gt;不要在多个地方复用同一把私钥，给 Actions 单独生成一对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &quot;github-actions-blog&quot; -f ~/.ssh/gh-actions-deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;公钥追加到服务器部署用户的 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;，私钥全文粘贴到仓库 Secret &lt;code&gt;DEPLOY_KEY&lt;/code&gt;。这样即便 CI 侧泄露，也只暴露这一把受限密钥，可以随时吊销更换。更严格的做法是在服务器上给该用户配置 &lt;code&gt;command=&lt;/code&gt; 前缀限制它只能执行 rsync。&lt;/p&gt;
&lt;h2&gt;不想维护服务器？推到 GitHub Pages&lt;/h2&gt;
&lt;p&gt;如果博客就托管在 GitHub Pages，部署步骤换成官方 Action，连服务器和密钥都不用管：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: 配置 Pages
        uses: actions/configure-pages@v5

      - name: 上传产物
        uses: actions/upload-pages-artifact@v3
        with:
          path: dist/

      - name: 部署到 Pages
        id: deployment
        uses: actions/deploy-pages@v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（需要在仓库 Settings → Pages 里把来源选为 GitHub Actions。）&lt;/p&gt;
&lt;h2&gt;让失败可见&lt;/h2&gt;
&lt;p&gt;流水线最大的谎言是「绿钩即一切正常」，第二大的谎言是「失败了会有人发现」。加一步失败通知，比如用 curl 调用任意 webhook（Server 酱、Telegram Bot、企业微信均可）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: 失败通知
        if: failure()
        run: |
          curl -s &quot;https://api.day.app/${{ secrets.BARK_KEY }}/博客部署失败/${{ github.sha }}?level=critical&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;if: failure()&lt;/code&gt; 表示仅当前面步骤失败才执行。同理 &lt;code&gt;if: success()&lt;/code&gt; 可以发成功确认——初期建议两种都开，跑稳定后只留失败通知，避免消息疲劳。&lt;/p&gt;
&lt;h2&gt;值得加的两个增强&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建前内容校验&lt;/strong&gt;：在 build 步骤前加 &lt;code&gt;npm run check&lt;/code&gt;（如果项目提供），让 Front Matter 缺字段这类问题在 CI 阶段就拦住，而不是部署后才发现页面异常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发控制&lt;/strong&gt;：连续 push 两个 commit 时不希望两个部署任务打架：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;concurrency:
  group: deploy
  cancel-in-progress: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一时刻只允许一个部署在跑，新的 push 会取消未完成的旧任务——反正部署的目标是「最新状态」，中间的半成品没有意义。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions&quot;&gt;GitHub Actions 官方文档：Workflow syntax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions&quot;&gt;GitHub Actions 官方文档：Using secrets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/actions&quot;&gt;actions/checkout 等官方 Action 索引&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Kubernetes 核心对象：Pod、Deployment 与 Service 的关系</title><link>https://meow.us.kg/posts/kubernetes-pod-deployment-service/</link><guid isPermaLink="true">https://meow.us.kg/posts/kubernetes-pod-deployment-service/</guid><description>用「声明式」心智模型理解 K8s：Pod 为什么是最小单位、Deployment 如何管副本与滚动更新、Service 怎样提供稳定访问入口。</description><pubDate>Sat, 17 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;Kubernetes 的学习曲线陡峭，很大程度是因为一上来就被几十个对象类型淹没。其实日常 80% 的工作只围绕三个对象：&lt;strong&gt;Pod 跑应用，Deployment 管 Pod，Service 暴露 Pod&lt;/strong&gt;。理清这三者的关系，K8s 的大门就打开了。&lt;/p&gt;
&lt;h2&gt;先换脑子：声明式，不是命令式&lt;/h2&gt;
&lt;p&gt;传统运维是命令式的：「启动 3 个容器」「这台挂了，重启它」。K8s 是声明式的：你只提交&lt;strong&gt;期望状态&lt;/strong&gt;（&quot;这个应用要有 3 个副本&quot;），K8s 的控制循环持续观察实际状态，发现偏差就自动纠正——副本少了就补，节点挂了就迁。&lt;/p&gt;
&lt;p&gt;你提交的每一份 YAML 都是一张「愿望清单」，而不是操作手册。这个模型是理解一切 K8s 行为的前提。&lt;/p&gt;
&lt;h2&gt;Pod：最小调度单位&lt;/h2&gt;
&lt;p&gt;Pod 是一个或多个&lt;strong&gt;共享网络与存储&lt;/strong&gt;的容器组，是 K8s 调度的最小单位。同一 Pod 内的容器共享 IP 和端口空间，通过 &lt;code&gt;localhost&lt;/code&gt; 互访。&lt;/p&gt;
&lt;p&gt;为什么不是直接调度容器？因为存在「主容器 + 紧密耦合的辅助容器」场景：比如应用容器写日志到共享目录，边车（sidecar）容器负责把日志转发出去——它们必须同生共死、共享存储，打包成一个 Pod 正合适。&lt;/p&gt;
&lt;p&gt;实践中很少手写 Pod 定义，但要知道它的两个特性：&lt;strong&gt;IP 是临时的&lt;/strong&gt;（Pod 重建就换）、&lt;strong&gt;Pod 是不可变的&lt;/strong&gt;（想改配置？删掉重建）。&lt;/p&gt;
&lt;h2&gt;Deployment：让 Pod 保持「想要的样子」&lt;/h2&gt;
&lt;p&gt;直接创建 Pod 是自虐：它挂了就挂了，没人管。Deployment 在 Pod 之上加了副本管理和更新策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3                     # 期望 3 个副本
  selector:
    matchLabels:
      app: web
  template:                       # Pod 模板
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: registry.example.com/myapp:1.4.2
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f deployment.yaml
kubectl get pods                  # 看到 3 个 web-xxx 的 Pod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;滚动更新&lt;/strong&gt;：改 &lt;code&gt;image&lt;/code&gt; 版本后再 &lt;code&gt;apply&lt;/code&gt;，Deployment 默认用 RollingUpdate 策略——先起一个新版本 Pod，健康了再灭一个旧的，如此滚动，全程服务不中断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回滚&lt;/strong&gt;：历史版本被自动记录，新版出问题时一键回到上一个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl rollout status deployment/web      # 观察更新进度
kubectl rollout undo deployment/web        # 回滚
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;自愈&lt;/strong&gt;：手动 &lt;code&gt;kubectl delete pod web-xxx&lt;/code&gt; 试试——Deployment 立刻补一个新 Pod 维持 &lt;code&gt;replicas: 3&lt;/code&gt;。这就是声明式控制循环的直观体现。&lt;/p&gt;
&lt;h2&gt;Service：给会漂移的 Pod 一个稳定地址&lt;/h2&gt;
&lt;p&gt;Pod IP 会变、副本会扩缩，客户端该连哪个地址？Service 用&lt;strong&gt;标签选择器&lt;/strong&gt;圈定一组 Pod，提供一个稳定的虚拟 IP 和 DNS 名，并把流量转发过去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web              # 选中所有带 app=web 标签的 Pod
  ports:
    - port: 80            # Service 自己的端口
      targetPort: 8080    # 转发到 Pod 的端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群内其他应用现在可以通过 &lt;code&gt;http://web&lt;/code&gt;（同命名空间下）访问你的服务——不用关心后面是 3 个还是 30 个 Pod、IP 换了几次。&lt;/p&gt;
&lt;p&gt;Service 的三种常用类型：&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;ClusterIP&lt;/code&gt;（默认）&lt;/td&gt;
&lt;td&gt;仅集群内访问，内部服务间调用的标准方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NodePort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在每个节点开一个端口（30000-32767），外部可经节点 IP 访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LoadBalancer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向云厂商申请一个外部负载均衡器，生产暴露服务的常用方式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对外提供 HTTP 服务时，更常见的组合是 Service + Ingress（统一域名、路径路由和 TLS 终结），Ingress 可以认为是「HTTP 层的 Service」。&lt;/p&gt;
&lt;h2&gt;三者关系一图流&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;                 管理副本与更新
   Deployment ──────────────────► Pod × 3（标签 app=web）
                                      ▲
                                      │ 标签选择器
   Service ───────────────────────────┘
   （稳定 IP/DNS，负载均衡转发）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端访问 Service 的固定地址；Service 按标签把请求分发给当前存活的 Pod；Deployment 保证 Pod 的数量和版本符合期望。三层各司其职，任何一层都不需要知道其他层的实现细节。&lt;/p&gt;
&lt;h2&gt;动手前的建议&lt;/h2&gt;
&lt;p&gt;本地学习用 minikube 或 kind（Kubernetes in Docker）起单节点集群；云厂商的托管 K8s（ACK、TKE、EKS）省掉控制面运维，适合生产起步。学会 &lt;code&gt;kubectl get/describe/logs/apply&lt;/code&gt; 四件套，就足以完成日常 80% 的操作——&lt;code&gt;describe&lt;/code&gt; 尤其重要，任何对象「不正常」，先看它末尾的 Events。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/&quot;&gt;Kubernetes 官方文档：Workloads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/&quot;&gt;Kubernetes 官方文档：Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/reference/kubectl/quick-reference/&quot;&gt;Kubernetes 官方文档：kubectl 快速参考&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Docker Compose 实战：一键编排多容器应用</title><link>https://meow.us.kg/posts/docker-compose-multi-container-app/</link><guid isPermaLink="true">https://meow.us.kg/posts/docker-compose-multi-container-app/</guid><description>以「Web 应用 + PostgreSQL + Redis」为例编写 docker-compose.yml，讲清服务定义、依赖启动顺序、环境变量与数据卷管理的完整实践。</description><pubDate>Fri, 16 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;一个真实应用很少是单个容器：Web 服务要连数据库，数据库要挂数据卷，也许还有缓存和任务队列。用一连串 &lt;code&gt;docker run&lt;/code&gt; 维护这些，命令越攒越长、参数越记越乱。Docker Compose 把整套应用写进一个 YAML 文件，&lt;code&gt;docker compose up -d&lt;/code&gt; 一条命令完整拉起。&lt;/p&gt;
&lt;h2&gt;示例场景：Web + PostgreSQL + Redis&lt;/h2&gt;
&lt;p&gt;目标架构：一个 Web 应用，连接 PostgreSQL 存数据、Redis 做缓存；数据库数据要持久化；Redis 不对外暴露端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  web:
    image: registry.example.com/myapp:1.4.2
    build: .
    ports:
      - &quot;8080:8080&quot;
    environment:
      DATABASE_URL: postgres://myapp:${DB_PASSWORD}@db:5432/myapp
      REDIS_URL: redis://cache:6379/0
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped
    networks:
      - appnet

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U myapp&quot;]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped
    networks:
      - appnet

  cache:
    image: redis:7
    restart: unless-stopped
    networks:
      - appnet

volumes:
  pgdata:

networks:
  appnet:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存为 &lt;code&gt;compose.yml&lt;/code&gt;（新版标准文件名，旧的 &lt;code&gt;docker-compose.yml&lt;/code&gt; 同样兼容），同目录执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d        # 创建并后台启动全部服务
docker compose ps           # 查看服务状态
docker compose logs -f web  # 跟踪某个服务日志
docker compose down         # 停止并删除容器与网络（数据卷保留）
docker compose down -v      # 连数据卷一起删（危险操作）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关键配置项逐条解释&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;build&lt;/code&gt; 与 &lt;code&gt;image&lt;/code&gt; 共存&lt;/strong&gt;：&lt;code&gt;build: .&lt;/code&gt; 表示用当前目录构建；同时写 &lt;code&gt;image:&lt;/code&gt; 则构建产物以该名字存储。有现成镜像时省略 &lt;code&gt;build&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;depends_on&lt;/code&gt; 的正确用法&lt;/strong&gt;：默认只保证启动&lt;strong&gt;顺序&lt;/strong&gt;，不保证「数据库真的就绪了」。Web 应用在 Postgres 还没初始化完时启动照样崩。正确姿势是 &lt;code&gt;condition: service_healthy&lt;/code&gt; 配合数据库的 &lt;code&gt;healthcheck&lt;/code&gt;——Compose 会等到健康检查通过才启动依赖方。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务名即主机名&lt;/strong&gt;：同一 network 下的服务可以用服务名互相访问。&lt;code&gt;DATABASE_URL&lt;/code&gt; 里的 &lt;code&gt;@db:5432&lt;/code&gt; 和 &lt;code&gt;redis://cache:6379&lt;/code&gt; 就是靠这个机制，不需要写死 IP。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;端口映射的最小暴露原则&lt;/strong&gt;：只有 &lt;code&gt;web&lt;/code&gt; 映射了宿主端口；&lt;code&gt;db&lt;/code&gt; 和 &lt;code&gt;cache&lt;/code&gt; 不写 &lt;code&gt;ports&lt;/code&gt;，外界（包括宿主机之外的机器）就无法访问它们，天然隔离。&lt;/p&gt;
&lt;h2&gt;环境变量与密钥管理&lt;/h2&gt;
&lt;p&gt;Compose 自动读取同目录的 &lt;code&gt;.env&lt;/code&gt; 文件做变量替换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .env（不要提交到 Git）
DB_PASSWORD=s3cure-random-string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;${DB_PASSWORD}&lt;/code&gt; 在 YAML 里被替换成实际值。纪律：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 加入 &lt;code&gt;.gitignore&lt;/code&gt;，仓库里只放 &lt;code&gt;.env.example&lt;/code&gt; 模板。&lt;/li&gt;
&lt;li&gt;不要在 YAML 里直接写明文密码——这个文件通常会进版本库。&lt;/li&gt;
&lt;li&gt;不同环境用多个文件区分：&lt;code&gt;docker compose --env-file .env.prod up -d&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;日常运维操作速查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 重新构建并更新某个服务（改了代码后）
docker compose up -d --build web

# 在服务容器里执行一次性命令（如数据库迁移）
docker compose exec web ./migrate up

# 临时跑一个一次性容器
docker compose run --rm web ./bin/console

# 看整体资源占用
docker compose top
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多份 Compose 文件做环境分层&lt;/h2&gt;
&lt;p&gt;开发环境要挂源码、开调试端口，生产不要。用多文件叠加而不是复制整份配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose -f compose.yml -f compose.dev.yml up -d
docker compose -f compose.yml -f compose.prod.yml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面的文件覆盖前面的同名配置。&lt;code&gt;compose.dev.yml&lt;/code&gt; 里给 web 加源码挂载，生产文件里加资源限制，公共部分只维护一份。&lt;/p&gt;
&lt;h2&gt;Compose 的边界&lt;/h2&gt;
&lt;p&gt;Compose 解决的是&lt;strong&gt;单机&lt;/strong&gt;多容器编排。它的定位很清晰：开发环境、小项目部署、CI 集成测试。需要跨机器调度、自动扩缩容、滚动更新时，就该上 Kubernetes 了——那是下一篇文章的话题。但即便上了 K8s，本地开发用 Compose 起一套依赖依然是高效的做法。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker 官方文档：Compose overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/reference/compose-file/&quot;&gt;Docker 官方文档：Compose file reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/how-tos/environment-variables/&quot;&gt;Docker 官方文档：Environment variables in Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Dockerfile 最佳实践：多阶段构建与镜像瘦身</title><link>https://meow.us.kg/posts/dockerfile-multi-stage-best-practices/</link><guid isPermaLink="true">https://meow.us.kg/posts/dockerfile-multi-stage-best-practices/</guid><description>通过前后对比展示如何把 Go 应用镜像从 900MB 压到 20MB 以内，讲清层缓存、.dockerignore、多阶段构建与非 root 运行的实践要点。</description><pubDate>Thu, 15 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;「能跑起来的 Dockerfile」和「能进生产的 Dockerfile」之间隔着体积、安全和构建速度三道坎。本文以一个 Go Web 应用为例，把一个粗糙的 Dockerfile 逐步改到合格，每一步都解释为什么。&lt;/p&gt;
&lt;h2&gt;反面教材：一把梭&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o server .
CMD [&quot;./server&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它能工作，但问题一堆：镜像近 900MB（整个 Go 工具链都在里面）、构建上下文混入无关文件、以 root 运行、每次改代码都全量重装依赖。&lt;/p&gt;
&lt;h2&gt;优化一：选对基础镜像&lt;/h2&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;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;golang:1.23&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~850MB&lt;/td&gt;
&lt;td&gt;完整工具链，只用于构建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;debian:bookworm-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~80MB&lt;/td&gt;
&lt;td&gt;精简 Debian，适合需要 libc 的动态链接程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~8MB&lt;/td&gt;
&lt;td&gt;musl libc，极小，但注意 glibc 兼容问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scratch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;空镜像，静态编译程序的终极选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;distroless&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~20MB&lt;/td&gt;
&lt;td&gt;Google 出品，只有运行时依赖，无 shell 无包管理器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
Alpine 用 musl 替代 glibc，某些依赖 glibc 特定行为的程序（典型如部分 DNS 解析、某些加密库）在 Alpine 上会出现难以排查的问题。静态链接的 Go 程序不受影响，但 Python、Node 等含原生扩展的生态要慎重。
:::&lt;/p&gt;
&lt;h2&gt;优化二：多阶段构建——构建环境与运行环境分离&lt;/h2&gt;
&lt;p&gt;核心思想：&lt;strong&gt;编译用的镜像不进入最终镜像&lt;/strong&gt;。第一阶段构建，第二阶段只拷贝产物：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ---- 构建阶段 ----
FROM golang:1.23 AS builder
WORKDIR /app

# 先拷贝依赖清单，利用层缓存
COPY go.mod go.sum ./
RUN go mod download

# 再拷贝源码并构建（CGO_ENABLED=0 产出静态二进制）
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags=&quot;-s -w&quot; -o server .

# ---- 运行阶段 ----
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT [&quot;/app/server&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：最终镜像从 ~900MB 降到 &lt;strong&gt;20MB 以内&lt;/strong&gt;。拉取更快、攻击面更小、漏洞扫描报告更干净。&lt;/p&gt;
&lt;p&gt;几个细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-ldflags=&quot;-s -w&quot;&lt;/code&gt; 去掉符号表和调试信息，二进制再小一截。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USER nonroot&lt;/code&gt; 让进程以非特权用户运行，容器被攻破时攻击者拿到的不是 root。distroless 自带 &lt;code&gt;nonroot&lt;/code&gt; 用户。&lt;/li&gt;
&lt;li&gt;多阶段构建对 Node、Java 同样适用：Node 的最终镜像只拷 &lt;code&gt;node_modules&lt;/code&gt; 生产依赖和构建产物；Java 用 jlink 定制 JRE。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优化三：利用层缓存，让重复构建秒级完成&lt;/h2&gt;
&lt;p&gt;Docker 按层缓存：某一层变了，它之后的所有层全部重建。因此 &lt;strong&gt;Dockerfile 的指令顺序按「变化频率从低到高」排列&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COPY go.mod go.sum ./     # 依赖清单很少变
RUN go mod download       # 上面没变这层就用缓存
COPY . .                  # 源码天天变，放最后
RUN go build ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖下载层被缓存后，日常改代码的构建不再访问网络，速度差一个数量级。反例就是开头那把梭的写法：&lt;code&gt;COPY . .&lt;/code&gt; 在前，任何文件变动都会让 &lt;code&gt;go mod download&lt;/code&gt; 缓存失效。&lt;/p&gt;
&lt;h2&gt;优化四：.dockerignore 管住构建上下文&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;docker build&lt;/code&gt; 的第一步是把整个目录发给 Docker daemon。没有 &lt;code&gt;.dockerignore&lt;/code&gt; 的话，&lt;code&gt;.git&lt;/code&gt;、本地依赖、测试数据、密钥文件全都会被发送，甚至可能被 &lt;code&gt;COPY . .&lt;/code&gt; 打进镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.git
node_modules
*.log
.env
.dockerignore
Dockerfile
docs/
tests/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这既是体积问题，更是&lt;strong&gt;安全问题&lt;/strong&gt;——&lt;code&gt;.env&lt;/code&gt; 进了镜像层，就算后面 &lt;code&gt;RUN rm .env&lt;/code&gt;，文件依然存在于历史层中，&lt;code&gt;docker history&lt;/code&gt; 可以翻出来。&lt;/p&gt;
&lt;h2&gt;优化五：其他值得遵守的细节&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个容器一个进程&lt;/strong&gt;：用 &lt;code&gt;ENTRYPOINT&lt;/code&gt; 直接启动应用，不要在容器里跑 supervisord 管一堆服务——那是虚拟机的玩法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要装调试工具进生产镜像&lt;/strong&gt;：curl、vim 留给排障 sidecar 或临时 &lt;code&gt;docker exec&lt;/code&gt; 挂载，常驻只会扩大攻击面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;明确 tag，不用 latest&lt;/strong&gt;：&lt;code&gt;myapp:1.4.2&lt;/code&gt; 可追溯可回滚；&lt;code&gt;latest&lt;/code&gt; 让「线上现在跑的到底是哪版」变成悬案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健康检查写进镜像&lt;/strong&gt;：&lt;code&gt;HEALTHCHECK CMD wget -qO- http://localhost:8080/healthz || exit 1&lt;/code&gt;，让 &lt;code&gt;docker ps&lt;/code&gt; 直接显示健康状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;验收清单&lt;/h2&gt;
&lt;p&gt;构建完成后自查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images myapp          # 体积是否达标（Go 静态程序应 &amp;lt; 50MB）
docker history myapp:1.0     # 每层是否合理，有没有敏感文件痕迹
docker run --rm myapp:1.0    # 能否以非 root 正常启动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;体积、缓存、安全三件事做到位，这个 Dockerfile 就可以进生产了。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/build/building/best-practices/&quot;&gt;Docker 官方文档：Dockerfile best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/build/building/multi-stage/&quot;&gt;Docker 官方文档：Multi-stage builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleContainerTools/distroless&quot;&gt;GoogleContainerTools/distroless&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Docker 快速上手：镜像、容器、数据卷与网络</title><link>https://meow.us.kg/posts/docker-images-containers-volumes/</link><guid isPermaLink="true">https://meow.us.kg/posts/docker-images-containers-volumes/</guid><description>理清 Docker 四个核心对象的关系，掌握容器生命周期操作、数据持久化的三种方式和容器网络的常用模式，附常用排错入口。</description><pubDate>Wed, 14 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;Docker 的文档铺天盖地，但初学者真正需要建立的只是一张对象关系图：&lt;strong&gt;镜像（Image）是模板，容器（Container）是模板的运行实例，数据卷（Volume）让数据活下来，网络（Network）让容器互相找到对方。&lt;/strong&gt; 本文围绕这四个对象展开。&lt;/p&gt;
&lt;h2&gt;镜像与容器：类与实例的关系&lt;/h2&gt;
&lt;p&gt;镜像是一个只读的、分层的文件系统快照，里面打包了应用及其全部依赖。容器是从镜像启动的进程，拥有自己的文件系统视图（在镜像之上加一层可写层）、进程空间、网络空间。&lt;/p&gt;
&lt;p&gt;一个镜像可以启动多个互不干扰的容器，就像一个类可以 new 出多个实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name web1 -p 8081:80 nginx
docker run -d --name web2 -p 8082:80 nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两条命令用同一个 &lt;code&gt;nginx&lt;/code&gt; 镜像起了两个独立容器，分别映射到宿主机 8081、8082。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker run&lt;/code&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;-d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后台运行（detached）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;给容器命名，否则随机生成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p 宿主端口:容器端口&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;端口映射&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-e KEY=VALUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注入环境变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v 卷名或路径:容器内路径&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;挂载数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--restart unless-stopped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;崩溃或重启后自动拉起&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;容器生命周期操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker ps                 # 运行中的容器
docker ps -a              # 包括已停止的
docker stop web1          # 优雅停止（先发 SIGTERM）
docker start web1         # 重新启动
docker restart web1       # 重启
docker rm web1            # 删除已停止的容器
docker rm -f web1         # 强制删除运行中的（慎用）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;排错三件套，记牢：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs -f web1       # 看容器标准输出（应用日志都该打到这里）
docker exec -it web1 sh   # 进容器里看看
docker inspect web1       # 查看容器全部配置（IP、挂载、环境变量）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
容器的正确心智模型是「一次性的进程」，而不是「轻量虚拟机」。不要在容器里改配置然后指望它一直活着——容器删掉就没了，配置应该通过镜像、环境变量或挂载注入。
:::&lt;/p&gt;
&lt;h2&gt;数据持久化：三种挂载&lt;/h2&gt;
&lt;p&gt;容器删了，可写层里的数据就没了。需要持久化的数据必须挂载出来：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 命名卷（Volume，推荐）&lt;/strong&gt;——由 Docker 管理存储位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 绑定挂载（Bind Mount）&lt;/strong&gt;——直接映射宿主机路径，开发时常用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name app -v /opt/myapp/config:/app/config myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. tmpfs&lt;/strong&gt;——只存在于内存，容器停即消失，适合敏感临时数据。&lt;/p&gt;
&lt;p&gt;选择原则：生产数据用命名卷（可备份、可迁移）；需要宿主机直接读写的配置用绑定挂载。&lt;/p&gt;
&lt;h2&gt;容器网络：三种常用模式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;bridge（默认）&lt;/strong&gt;：Docker 建一个虚拟网桥 &lt;code&gt;docker0&lt;/code&gt;，每个容器分配独立 IP。同网桥上的容器可以互相通信，配合 &lt;code&gt;--link&lt;/code&gt; 已废弃的旧做法不同，现在用&lt;strong&gt;自定义网络&lt;/strong&gt;实现服务发现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name app --network appnet myapp
# app 容器里可以直接用主机名 &quot;db&quot; 访问数据库
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义网络内置 DNS，容器名即主机名——这是多容器应用的标准做法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;host&lt;/strong&gt;：容器直接用宿主机网络栈，没有隔离也没有端口映射，性能最好。适合对网络性能敏感的组件，但端口冲突要自己管。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;none&lt;/strong&gt;：完全断网，只留回环口，用于批量计算任务。&lt;/p&gt;
&lt;h2&gt;镜像管理的日常&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker images                    # 本地镜像列表
docker pull redis:7              # 拉取指定 tag
docker build -t myapp:1.0 .      # 用当前目录 Dockerfile 构建
docker tag myapp:1.0 reg.example.com/myapp:1.0   # 打私仓标签
docker push reg.example.com/myapp:1.0
docker system prune              # 清理停止的容器、悬空镜像、未用网络
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;磁盘空间告警时，&lt;code&gt;docker system df&lt;/code&gt; 先看占用分布，再决定 prune 范围（加 &lt;code&gt;-a&lt;/code&gt; 连未使用的镜像一起删，下手前确认）。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/get-started/&quot;&gt;Docker 官方文档：Get started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/storage/&quot;&gt;Docker 官方文档：Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/network/&quot;&gt;Docker 官方文档：Networking overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Ansible 自动化运维入门：Inventory、Playbook 与常用模块</title><link>https://meow.us.kg/posts/ansible-inventory-playbook-modules/</link><guid isPermaLink="true">https://meow.us.kg/posts/ansible-inventory-playbook-modules/</guid><description>理解 Ansible 无代理架构与幂等性设计，从 Inventory 分组、Ad-hoc 命令到 Playbook 编写，掌握批量服务器管理的基本功。</description><pubDate>Tue, 13 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;新加一台服务器，手动执行那套「建用户、装软件、改配置」的流程要半小时，还总会漏一步；第五台服务器到来时，这套流程就是不可承受之重。Ansible 解决的问题是：&lt;strong&gt;把运维操作写成可重复执行的代码&lt;/strong&gt;，一台和一百台用同一份剧本。&lt;/p&gt;
&lt;h2&gt;为什么选 Ansible：无代理架构&lt;/h2&gt;
&lt;p&gt;Ansible 与被管机器之间&lt;strong&gt;只走 SSH&lt;/strong&gt;——不需要在目标机器预装任何 agent 或服务。控制节点装好 Ansible，目标机有 Python 和 SSH 就行。对比 Puppet/SaltStack 需要先给全网装客户端，Ansible 的接入成本几乎为零，这也是它成为中小团队首选的原因。&lt;/p&gt;
&lt;h2&gt;Inventory：把机器编成名册&lt;/h2&gt;
&lt;p&gt;Inventory 是被管机器的清单，INI 格式（也支持 YAML）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[web]
web1 ansible_host=10.0.1.11
web2 ansible_host=10.0.1.12

[db]
db1 ansible_host=10.0.2.11

[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_ed25519
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方括号定义&lt;strong&gt;分组&lt;/strong&gt;，一台机器可以属于多个组； playbook 按组指定操作对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ansible_host&lt;/code&gt;、&lt;code&gt;ansible_user&lt;/code&gt; 这类连接变量可以按主机、按组、按 &lt;code&gt;all&lt;/code&gt; 全局三个层级设置，就近覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证连通性（Ansible 的「hello world」）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -i inventory.ini -m ping
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-m ping&lt;/code&gt; 不是 ICMP，而是实际 SSH 上去执行一个 Python 模块，返回 &lt;code&gt;pong&lt;/code&gt; 说明连接、认证、Python 环境全部就绪。&lt;/p&gt;
&lt;h2&gt;Ad-hoc：单行命令批量执行&lt;/h2&gt;
&lt;p&gt;不写剧本也能直接批量操作，适合一次性任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看所有 web 机器的负载
ansible web -i inventory.ini -m shell -a &quot;uptime&quot;

# 给所有机器安装 htop
ansible all -i inventory.ini -m apt -a &quot;name=htop state=present&quot; --become
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--become&lt;/code&gt; 表示在目标机提权（默认 sudo）。&lt;/p&gt;
&lt;h2&gt;Playbook：把流程写成 YAML&lt;/h2&gt;
&lt;p&gt;重复性任务要写成 playbook。例如「部署 Nginx 并启动」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: 配置 web 服务器
  hosts: web
  become: true
  tasks:
    - name: 安装 nginx
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: 下发站点配置
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: 重载 nginx

    - name: 确保服务运行
      service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: 重载 nginx
      service:
        name: nginx
        state: reloaded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook -i inventory.ini site.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;幂等性：Ansible 最重要的设计&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;同一个 playbook 执行一次和执行一百次，结果应该相同。&lt;/strong&gt; Ansible 的模块大多遵循这个原则：&lt;code&gt;apt: name=nginx state=present&lt;/code&gt; 表示「确保 nginx 处于已安装状态」——已安装就跳过，没装才装。这与 shell 脚本里裸写 &lt;code&gt;apt install nginx&lt;/code&gt; 有本质区别：脚本每次执行都产生副作用，playbook 描述的是&lt;strong&gt;目标状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;写 playbook 时警惕破坏幂等的写法：&lt;code&gt;shell&lt;/code&gt;/&lt;code&gt;command&lt;/code&gt; 模块没有状态概念，默认每次都执行。能用专用模块（&lt;code&gt;apt&lt;/code&gt;、&lt;code&gt;copy&lt;/code&gt;、&lt;code&gt;template&lt;/code&gt;、&lt;code&gt;file&lt;/code&gt;、&lt;code&gt;user&lt;/code&gt;……）就不用 shell；必须用 shell 时，加 &lt;code&gt;creates&lt;/code&gt;/&lt;code&gt;removes&lt;/code&gt; 参数告诉 Ansible 如何判断「已经做过了」。&lt;/p&gt;
&lt;h3&gt;template 与 handler&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt; 模块用 Jinja2 渲染配置文件，变量在运行时注入，同一份模板可以给测试、生产环境生成不同配置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler&lt;/code&gt; 是「变更后动作」：上面的例子里，只有配置文件&lt;strong&gt;真的发生变化&lt;/strong&gt;时才会触发 nginx reload，配置没动就不打扰服务。这也是幂等思想的一部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;高频模块速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&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;apt&lt;/code&gt; / &lt;code&gt;dnf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;包管理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name=vim state=latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;copy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;直接下发文件内容&lt;/td&gt;
&lt;td&gt;&lt;code&gt;content=&quot;...&quot; dest=/etc/motd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jinja2 模板下发&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src=app.conf.j2 dest=...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;建目录、改权限&lt;/td&gt;
&lt;td&gt;&lt;code&gt;path=/data state=directory mode=0755&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户管理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name=deploy shell=/bin/bash groups=sudo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service&lt;/code&gt; / &lt;code&gt;systemd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务管理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name=nginx state=restarted enabled=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cron&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定时任务&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name=backup job=&quot;/opt/backup.sh&quot; hour=3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lineinfile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;确保某行存在/不存在&lt;/td&gt;
&lt;td&gt;改 sshd 配置单条参数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;让 playbook 可维护的三条经验&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;敏感信息入 Vault&lt;/strong&gt;：密码、私钥用 &lt;code&gt;ansible-vault encrypt_string&lt;/code&gt; 加密后再写进变量文件，仓库里只存密文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--check&lt;/code&gt; + &lt;code&gt;--diff&lt;/code&gt; 先演习&lt;/strong&gt;：&lt;code&gt;ansible-playbook site.yml --check --diff&lt;/code&gt; 只演算不执行，并展示将发生的变化，上线前必跑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务命名要具体&lt;/strong&gt;：&lt;code&gt;name: 安装 nginx&lt;/code&gt; 比 &lt;code&gt;name: apt&lt;/code&gt; 好——执行输出和失败定位都靠任务名。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/getting_started/index.html&quot;&gt;Ansible 官方文档：Getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/collections/index_module.html&quot;&gt;Ansible 官方文档：Module index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ansible.com/ansible/latest/tips_tricks/ansible_tips_tricks.html&quot;&gt;Ansible 官方文档：Best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>集中式日志管理：从 rsyslog 到 Loki 与 ELK 的选型</title><link>https://meow.us.kg/posts/centralized-logging-loki-elk/</link><guid isPermaLink="true">https://meow.us.kg/posts/centralized-logging-loki-elk/</guid><description>服务器变多之后日志分散难查，梳理 rsyslog 集中收集、ELK 全文检索、Loki 标签索引三代方案的原理、成本与适用规模。</description><pubDate>Sun, 11 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;一台服务器时，&lt;code&gt;tail -f&lt;/code&gt; 加 &lt;code&gt;grep&lt;/code&gt; 就是全部日志方案。服务器变成五台、十台之后，「登录每台机器逐个翻日志」彻底失效，集中式日志管理成为刚需。本文按技术演进的顺序讲三种方案，帮你按团队规模选择，而不是盲目上最重的那个。&lt;/p&gt;
&lt;h2&gt;为什么必须集中&lt;/h2&gt;
&lt;p&gt;分散日志有三个致命伤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;跨机器关联不可能&lt;/strong&gt;：一次请求经过网关、应用、任务队列三台机器，时间对不齐就拼不出完整链路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器随时消失&lt;/strong&gt;：容器和弹性伸缩场景下，实例销毁后本地日志随之蒸发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限矛盾&lt;/strong&gt;：开发想查日志，但不该给所有人生产机 SSH 权限。集中到日志平台后，用平台权限替代机器权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;方案一：rsyslog 集中转发（极简派）&lt;/h2&gt;
&lt;p&gt;Linux 自带的 rsyslog 本身就支持把日志转发到远端。发送端 &lt;code&gt;/etc/rsyslog.d/50-forward.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*.* @@log-server:514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;code&gt;@@&lt;/code&gt; 表示 TCP，&lt;code&gt;@&lt;/code&gt; 是 UDP；生产用 TCP 防丢。）&lt;/p&gt;
&lt;p&gt;接收端开启监听并写入按来源分目录的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ModLoad imtcp
$InputTCPServerRun 514
$template RemoteHost,&quot;/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log&quot;
*.* ?RemoteHost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：零新增组件、资源占用忽略不计、稳定跑了几十年的老方案。
&lt;strong&gt;缺点&lt;/strong&gt;：只能 &lt;code&gt;grep&lt;/code&gt;，没有结构化解析，没有 Web 界面，检索大数据量时全靠手动。&lt;/p&gt;
&lt;p&gt;适用：服务器少于十台、日志量小、团队习惯命令行。&lt;/p&gt;
&lt;h2&gt;方案二：ELK（全文检索派）&lt;/h2&gt;
&lt;p&gt;ELK = Elasticsearch + Logstash + Kibana，是日志分析领域的事实标准。管道是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Filebeat(采集) → Logstash(解析/富化) → Elasticsearch(存储/索引) → Kibana(查询/展示)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际部署常用 Filebeat 直接写 ES，跳过 Logstash 以省资源，解析交给 ES 的 ingest pipeline。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心能力是全文检索&lt;/strong&gt;：每条日志的每个词都被倒排索引，「找出所有包含 &lt;code&gt;timeout&lt;/code&gt; 且来自 &lt;code&gt;api-server&lt;/code&gt; 的日志」毫秒返回。Kibana 的 Discover 界面、可视化图表和告警插件构成了完整的日志分析工作流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代价是资源&lt;/strong&gt;：Elasticsearch 基于 JVM，给全文索引建的倒排表让存储放大到原始日志的 1.5~3 倍，一个中等规模集群轻松吃掉几十 GB 内存。个人项目单节点 ES 最低建议 4GB 堆内存起步。&lt;/p&gt;
&lt;p&gt;适用：日志需要复杂检索与分析、有专门运维人力、资源预算充足。&lt;/p&gt;
&lt;h2&gt;方案三：Loki（轻量标签派）&lt;/h2&gt;
&lt;p&gt;Grafana Labs 出品的 Loki 提出了一个关键洞察：&lt;strong&gt;大部分时候人们查日志是先按「哪个服务、哪台机器」缩小范围，再在里面 grep&lt;/strong&gt;。所以 Loki 只为标签建索引（如 &lt;code&gt;job=&quot;nginx&quot;&lt;/code&gt;, &lt;code&gt;host=&quot;web1&quot;&lt;/code&gt;），日志原文压缩存储，不做全文索引。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promtail(采集) → Loki(存储) → Grafana(查询)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;架构和 Prometheus 几乎同构（Loki 自称是「日志界的 Prometheus」），查询语言 LogQL 也神似 PromQL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{job=&quot;nginx&quot;} |= &quot;timeout&quot; | json | status &amp;gt;= 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：取 job=nginx 的日志流 → 过滤含 timeout 的行 → 按 JSON 解析 → 筛 status 字段 ≥ 500。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：存储成本低一个数量级（压缩率通常 10:1 以上）、内存友好、与 Grafana 天然集成——点一下就能从指标曲线跳到同时间段的日志。
&lt;strong&gt;缺点&lt;/strong&gt;：不做全文索引意味着「不按标签过滤的全文搜索」很慢；高基数标签（如把 user_id 做成标签）会拖垮索引，使用上有纪律要求。&lt;/p&gt;
&lt;p&gt;适用：已经在用 Prometheus + Grafana、日志主要用于排障而非复杂分析、追求低成本。&lt;/p&gt;
&lt;h2&gt;选型决策表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;rsyslog&lt;/th&gt;
&lt;th&gt;ELK&lt;/th&gt;
&lt;th&gt;Loki&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;部署复杂度&lt;/td&gt;
&lt;td&gt;极低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全文检索&lt;/td&gt;
&lt;td&gt;无（靠 grep）&lt;/td&gt;
&lt;td&gt;强&lt;/td&gt;
&lt;td&gt;弱（标签+过滤）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储成本&lt;/td&gt;
&lt;td&gt;原始大小&lt;/td&gt;
&lt;td&gt;1.5~3 倍&lt;/td&gt;
&lt;td&gt;约 1/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;学习成本&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适合规模&lt;/td&gt;
&lt;td&gt;&amp;lt; 10 台&lt;/td&gt;
&lt;td&gt;中大规模、重分析&lt;/td&gt;
&lt;td&gt;中小规模、重排障&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;无论选哪个，先把日志写成结构化&lt;/h2&gt;
&lt;p&gt;三个方案有个共同前提：&lt;strong&gt;应用输出结构化日志（JSON）比纯文本好处理一百倍&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;ts&quot;:&quot;2026-10-11T10:30:00+08:00&quot;,&quot;level&quot;:&quot;error&quot;,&quot;service&quot;:&quot;api&quot;,&quot;trace_id&quot;:&quot;abc123&quot;,&quot;msg&quot;:&quot;db query timeout&quot;,&quot;latency_ms&quot;:2300}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段明确后，任何管道都能准确解析；而 &lt;code&gt;2026/10/11 ERROR something happened&lt;/code&gt; 这种自由文本，到了平台侧还得写正则猜字段。改造应用日志格式，是投入产出比最高的日志治理动作。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rsyslog.com/doc/&quot;&gt;rsyslog 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/get-started&quot;&gt;Elastic 官方文档：Get started with the Elastic Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/docs/loki/latest/&quot;&gt;Grafana Loki 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>搭建 Prometheus + Grafana：从指标采集到可视化面板</title><link>https://meow.us.kg/posts/prometheus-grafana-monitoring-stack/</link><guid isPermaLink="true">https://meow.us.kg/posts/prometheus-grafana-monitoring-stack/</guid><description>以 node_exporter 采集 Linux 主机指标为例，完成 Prometheus 安装、抓取配置、PromQL 基础查询与 Grafana 面板搭建的完整流程。</description><pubDate>Fri, 09 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;上一篇讲了监控方法论，这篇动手落地：用 Prometheus 采集一台 Linux 主机的指标，再用 Grafana 画成面板。这是目前云原生生态里最主流的监控组合，理解它的工作流程后，再监控 MySQL、Nginx 或自己的应用都是同一套路。&lt;/p&gt;
&lt;h2&gt;整体架构：拉模型&lt;/h2&gt;
&lt;p&gt;Prometheus 的核心特点是&lt;strong&gt;主动拉取（Pull）&lt;/strong&gt;：它按配置的时间间隔访问各目标暴露的 &lt;code&gt;/metrics&lt;/code&gt; HTTP 接口，把文本格式的指标抓回来存进内置时序数据库。被监控方只需要「暴露」，不需要知道监控系统的存在。&lt;/p&gt;
&lt;p&gt;三个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Exporter&lt;/strong&gt;：运行在被监控机器上，把系统/服务状态翻译成 Prometheus 文本格式。监控 Linux 主机用 &lt;code&gt;node_exporter&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prometheus Server&lt;/strong&gt;：定时抓取、存储、提供 PromQL 查询接口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt;：连接 Prometheus 做可视化展示（Prometheus 自带 UI 只适合临时查询）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第一步：部署 node_exporter&lt;/h2&gt;
&lt;p&gt;在被监控机器上（以 Linux amd64 为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 下载官方发布的二进制（版本号以 releases 页面为准）
wget https://github.com/prometheus/node_exporter/releases/download/v1.9.1/node_exporter-1.9.1.linux-amd64.tar.gz
tar xzf node_exporter-1.9.1.linux-amd64.tar.gz
sudo cp node_exporter-1.9.1.linux-amd64/node_exporter /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 systemd 管理，新建 &lt;code&gt;/etc/systemd/system/node_exporter.service&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Node Exporter
After=network.target

[Service]
User=nobody
ExecStart=/usr/local/bin/node_exporter
Restart=on-failure

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
curl http://localhost:9100/metrics | head
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后一行应该输出大量 &lt;code&gt;node_cpu_seconds_total&lt;/code&gt;、&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt; 之类的指标——采集端就绪。&lt;/p&gt;
&lt;h2&gt;第二步：配置 Prometheus 抓取&lt;/h2&gt;
&lt;p&gt;Prometheus 的配置文件 &lt;code&gt;prometheus.yml&lt;/code&gt; 里加一段 job：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scrape_configs:
  - job_name: &quot;node&quot;
    scrape_interval: 15s
    static_configs:
      - targets: [&quot;10.0.1.11:9100&quot;, &quot;10.0.1.12:9100&quot;]
        labels:
          env: &quot;production&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scrape_interval: 15s&lt;/code&gt; 是全局常用值，太密浪费存储，太疏丢失细节。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;labels&lt;/code&gt; 会附加到这个 job 抓到的所有序列上，是多环境区分的标准做法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改完执行 &lt;code&gt;promtool check config prometheus.yml&lt;/code&gt; 校验，再重启或调用 &lt;code&gt;/-/reload&lt;/code&gt; 热加载。打开 Prometheus 自带 UI（&lt;code&gt;:9090/targets&lt;/code&gt;），目标状态变为 UP 即成功。&lt;/p&gt;
&lt;h2&gt;第三步：PromQL 入门五连&lt;/h2&gt;
&lt;p&gt;PromQL 是这套体系的核心资产，掌握五个常用查询就能应对日常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. CPU 使用率（按实例）：1 减去空闲比例，irate 取最近两点瞬时变化率
100 - (avg by (instance) (irate(node_cpu_seconds_total{mode=&quot;idle&quot;}[5m])) * 100)

# 2. 内存使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# 3. 根分区磁盘使用率
100 - (node_filesystem_avail_bytes{mountpoint=&quot;/&quot;} / node_filesystem_size_bytes{mountpoint=&quot;/&quot;} * 100)

# 4. 磁盘 IO 繁忙度
irate(node_disk_io_time_seconds_total[5m]) * 100

# 5. 网络入向流量（字节/秒）
irate(node_network_receive_bytes_total{device=&quot;eth0&quot;}[5m])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个关键点：&lt;code&gt;irate&lt;/code&gt; 对计数器求「每秒增长」；&lt;code&gt;[5m]&lt;/code&gt; 是采样窗口，至少为抓取间隔的 2~4 倍；&lt;code&gt;by (instance)&lt;/code&gt; 是聚合维度。在 UI 的 Graph 页逐个输入观察曲线，比背语法更快建立直觉。&lt;/p&gt;
&lt;h2&gt;第四步：接入 Grafana&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Grafana 里添加 Data source → Prometheus，填 &lt;code&gt;http://&amp;lt;prometheus地址&amp;gt;:9090&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Dashboards → Import，输入官方共享面板 ID——Node Exporter Full 的面板 ID 是 &lt;code&gt;1860&lt;/code&gt;，导入后选择数据源即得一套完整主机监控页。&lt;/li&gt;
&lt;li&gt;之后可以基于导入的面板删改：保留 CPU、内存、磁盘、网络四组核心图，去掉用不上的硬件传感器图表。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
grafana.com 的共享面板质量参差，导入后务必检查两件事：查询语句里的变量（如 &lt;code&gt;$job&lt;/code&gt;、&lt;code&gt;$instance&lt;/code&gt;）是否在你的标签体系里有值；面板的默认时间范围和刷新频率是否合适。
:::&lt;/p&gt;
&lt;h2&gt;数据保留与容量规划&lt;/h2&gt;
&lt;p&gt;Prometheus 默认把数据存 15 天（&lt;code&gt;--storage.tsdb.retention.time&lt;/code&gt;）。粗略估算磁盘占用：每个活跃时间序列每秒样本约 1~2 字节。一台 node_exporter 约暴露 1000 个序列，15 秒抓取一次，15 天约 3~6 GB——几十台机器的规模单机 Prometheus 完全扛得住。&lt;/p&gt;
&lt;p&gt;需要更长保留期或全局查询时，再考虑 Thanos、VictoriaMetrics 这类横向扩展方案，初期不必过度设计。&lt;/p&gt;
&lt;h2&gt;下一步&lt;/h2&gt;
&lt;p&gt;主机监控跑通后，按相同模式扩展：MySQL 用 &lt;code&gt;mysqld_exporter&lt;/code&gt;，Nginx 用 &lt;code&gt;nginx-prometheus-exporter&lt;/code&gt;，自己的应用则引入客户端库（如 Go 的 &lt;code&gt;prometheus/client_golang&lt;/code&gt;）在代码里暴露 &lt;code&gt;/metrics&lt;/code&gt;。采集-存储-展示的管道不变，只是数据来源变多。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/getting_started/&quot;&gt;Prometheus 官方文档：Getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/querying/basics/&quot;&gt;Prometheus 官方文档：Querying basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prometheus/node_exporter&quot;&gt;node_exporter 仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/docs/grafana/latest/datasources/&quot;&gt;Grafana 官方文档：Data sources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux 服务监控入门：指标、日志与告警思路</title><link>https://meow.us.kg/posts/linux-service-monitoring-fundamentals/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-service-monitoring-fundamentals/</guid><description>在装任何监控工具之前先建立方法论：监控的四层模型、指标日志追踪的分工、告警疲劳的成因，以及如何设计真正 actionable 的告警规则。</description><pubDate>Wed, 07 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;很多团队的监控建设路径是：装上 Prometheus → 导入一堆 Grafana 面板 → 把默认告警全开 → 三天后所有告警被静音。问题不在工具，在思路。本文先建立方法论，后续文章再谈具体工具。&lt;/p&gt;
&lt;h2&gt;监控回答两个问题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「系统现在正常吗？」&lt;/strong&gt;——用于告警，要求快、准、少误报。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「出问题的时候，哪里出了问题？」&lt;/strong&gt;——用于排障，要求数据足够细、维度足够多。&lt;/p&gt;
&lt;p&gt;这两个目标有时矛盾：告警要求聚合后的少量信号，排障要求保留大量原始数据。好的监控体系同时服务两者，而不是用同一套东西勉强应付。&lt;/p&gt;
&lt;h2&gt;四层监控模型&lt;/h2&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;th&gt;典型指标&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;基础设施层&lt;/td&gt;
&lt;td&gt;机器活着吗、资源够吗&lt;/td&gt;
&lt;td&gt;CPU、内存、磁盘、网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统服务层&lt;/td&gt;
&lt;td&gt;依赖的中间件健康吗&lt;/td&gt;
&lt;td&gt;MySQL 连接数、Redis 命中率、Nginx 状态码分布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;应用层&lt;/td&gt;
&lt;td&gt;我的服务正常吗&lt;/td&gt;
&lt;td&gt;QPS、响应延迟分位数、错误率、队列堆积&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;业务层&lt;/td&gt;
&lt;td&gt;用户在受影响吗&lt;/td&gt;
&lt;td&gt;下单成功率、支付转化率、在线人数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;新手常犯的错误是只监控第一层——CPU 60% 告警响了，但用户一切正常；反过来数据库连接池耗尽导致全站 500，CPU 却风平浪静。&lt;strong&gt;越靠近用户的层级，告警价值越高。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;三大支柱的分工&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;指标（Metrics）&lt;/strong&gt;：按时间间隔采样的数值，如「过去 1 分钟平均延迟」。特点是聚合后体积小、查询快，适合趋势图和告警条件。局限是维度爆炸——给指标加 &lt;code&gt;user_id&lt;/code&gt; 标签，时间序列数量直接失控。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;日志（Logs）&lt;/strong&gt;：离散事件的记录，信息量最大但检索成本高。排障时回答「这个具体请求发生了什么」靠它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;追踪（Traces）&lt;/strong&gt;：一个请求跨多个服务的完整路径，微服务架构下定位慢调用靠它。&lt;/p&gt;
&lt;p&gt;三者的关系：指标告诉你&lt;strong&gt;出事了&lt;/strong&gt;，日志告诉你&lt;strong&gt;发生了什么&lt;/strong&gt;，追踪告诉你&lt;strong&gt;卡在哪一环&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;告警疲劳：监控系统的头号杀手&lt;/h2&gt;
&lt;p&gt;告警太多 = 没有告警。值班的人每天收到 50 条告警，其中 49 条可以安全忽略，那么第 50 条也必然被忽略。设计告警时遵守几条纪律：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 只对「需要人立即行动」的情况告警。&lt;/strong&gt; CPU 高需要人半夜爬起来吗？通常不需要。磁盘 95% 需要吗？可能只需要工作日处理。用户支付失败率飙升呢？需要，立刻。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 告警基于症状，不是原因。&lt;/strong&gt; 「数据库连接数高」是原因，「接口错误率超过 5%」是症状。原因类信号适合做成巡检报表，症状类信号才配叫醒人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 每条告警都要有处理手册。&lt;/strong&gt; 收到告警的人（可能是三个月后的你）应该能照着 runbook 一步步排查，而不是对着一个指标名干瞪眼。告警消息里附上相关面板链接和处理文档地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 设置抑制与静默。&lt;/strong&gt; 主机宕机时，它上面的 30 个服务告警应该被收敛成一条；计划内维护期间对应告警提前静默，别让噪音训练大家忽略告警。&lt;/p&gt;
&lt;h2&gt;延迟告警的正确姿势：分位数与窗口&lt;/h2&gt;
&lt;p&gt;「平均响应时间」是最具欺骗性的指标——99 个 10ms 的请求和 1 个 10s 的请求，平均下来 109ms，看起来还行，但有一个用户等了 10 秒。&lt;/p&gt;
&lt;p&gt;正确做法是监控&lt;strong&gt;分位数&lt;/strong&gt;：p50 看中位体验，p95/p99 看尾部体验。告警条件再加上&lt;strong&gt;持续时间窗口&lt;/strong&gt;，避免毛刺触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 伪代码：p99 延迟连续 5 分钟超过 2 秒才告警
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) &amp;gt; 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的思路适用于错误率：「5 分钟内错误率 &amp;gt; 2%」比「单次错误」可靠得多。&lt;/p&gt;
&lt;h2&gt;从一台机器开始的最小实践&lt;/h2&gt;
&lt;p&gt;暂时不上复杂系统，一台 Linux 服务器也能做好基础监控：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;指标采集&lt;/strong&gt;：装 &lt;code&gt;node_exporter&lt;/code&gt;，暴露机器指标到 &lt;code&gt;:9100/metrics&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可视化&lt;/strong&gt;：Grafana 连上 Prometheus，导入官方 Node Exporter 面板。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;告警&lt;/strong&gt;：只配三条——磁盘使用率 &amp;gt; 85%（预警）、内存可用 &amp;lt; 10%（预警）、服务端口不通（严重）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志&lt;/strong&gt;：确认 &lt;code&gt;journalctl -u &amp;lt;你的服务&amp;gt;&lt;/code&gt; 能查到应用日志，rsyslog 正常轮转。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套组合足够支撑个人项目和小团队初期，等它不够用时，痛点会自然告诉你下一步该补什么。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sre.google/sre-book/monitoring-distributed-systems/&quot;&gt;Google SRE Book: Monitoring Distributed Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/&quot;&gt;Prometheus 官方文档：Alerting rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.brendangregg.com/usemethod.html&quot;&gt;The USE Method（Brendan Gregg）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>免费 HTTPS：Let&apos;s Encrypt 证书申请与 Certbot 自动续期</title><link>https://meow.us.kg/posts/letsencrypt-certbot-auto-renewal/</link><guid isPermaLink="true">https://meow.us.kg/posts/letsencrypt-certbot-auto-renewal/</guid><description>解释 ACME 协议如何用「挑战-应答」验证域名所有权，用 Certbot 为 Nginx 申请免费证书并配置自动续期，附续期失败的排查思路。</description><pubDate>Mon, 05 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;HTTPS 证书曾经是每年几百美元的支出，Let&apos;s Encrypt 把它变成了零成本+全自动。这背后是 ACME 协议（RFC 8555）：CA 不再靠人工审核域名归属，而是让申请者当场「表演」对服务器的控制权。理解这个机制，续期失败时才知道往哪查。&lt;/p&gt;
&lt;h2&gt;ACME 如何验证「域名是你的」&lt;/h2&gt;
&lt;p&gt;申请证书的本质是向 CA 证明两件事：你持有域名的控制权，以及你持有证书私钥。ACME 用「挑战-应答」完成验证，主流有两种方式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTP-01 挑战&lt;/strong&gt;：Let&apos;s Encrypt 给你一个随机 token，要求你的服务器在 &lt;code&gt;http://&amp;lt;域名&amp;gt;/.well-known/acme-challenge/&amp;lt;token&amp;gt;&lt;/code&gt; 路径上返回指定内容。CA 从公网访问这个 URL，能拿到正确内容就证明你控制该域名指向的服务器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DNS-01 挑战&lt;/strong&gt;：要求你创建一个 &lt;code&gt;_acme-challenge.&amp;lt;域名&amp;gt;&lt;/code&gt; 的 TXT 记录。能改 DNS 就证明控制权。它是申请&lt;strong&gt;通配符证书&lt;/strong&gt;（&lt;code&gt;*.example.com&lt;/code&gt;）的唯一方式，也适合服务器不暴露 80 端口的场景；缺点是需要 DNS 服务商 API 才能自动化。&lt;/p&gt;
&lt;h2&gt;用 Certbot 申请证书&lt;/h2&gt;
&lt;p&gt;Certbot 是 EFF 维护的 ACME 客户端，各大发行版均可安装。以 Debian/Ubuntu + Nginx 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt install -y certbot python3-certbot-nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 80 端口已经由 Nginx 提供服务，最省事的方式是让 Certbot 直接改 Nginx 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certbot --nginx -d example.com -d www.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Certbot 会依次完成：生成私钥 → 发起 HTTP-01 挑战 → 通过验证拿到证书 → 修改 Nginx 配置启用 443 监听和 HTTP 跳转 HTTPS → 输出证书存放路径。&lt;/p&gt;
&lt;p&gt;证书与私钥位置（Debian/Ubuntu 默认）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/letsencrypt/live/example.com/fullchain.pem   # 证书（含中间证书链）
/etc/letsencrypt/live/example.com/privkey.pem     # 私钥
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
不想让 Certbot 动配置文件的话，用 &lt;code&gt;certbot certonly --webroot -w /var/www/html -d example.com&lt;/code&gt;，挑战文件会放进 webroot，拿到证书后自己改 Nginx 配置。这种方式流程更透明，适合需要精细控制的场景。
:::&lt;/p&gt;
&lt;p&gt;申请成功后检查证书内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates -subject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出里能看到有效期——Let&apos;s Encrypt 证书&lt;strong&gt;只有 90 天&lt;/strong&gt;，这是故意的：短周期倒逼自动化，私钥泄露后的暴露窗口也更短。&lt;/p&gt;
&lt;h2&gt;自动续期：装好就忘&lt;/h2&gt;
&lt;p&gt;Certbot 安装时会自动注册一个 systemd timer（或 cron 任务），每天跑两次续期检查。验证它存在且工作正常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl list-timers | grep certbot    # 查看定时器
certbot renew --dry-run                 # 演习一次续期流程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--dry-run&lt;/code&gt; 是续期排错的第一件武器：它走完整的验证流程但不实际签发，能提前发现 80 端口不通、Nginx 配置变化导致的失败。续期默认在证书剩余 30 天内才实际执行，此前的调用只是空转。&lt;/p&gt;
&lt;p&gt;续期成功后需要让服务加载新证书。用钩子把重载动作挂进去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certbot renew --deploy-hook &quot;systemctl reload nginx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者写入 &lt;code&gt;/etc/letsencrypt/renewal/example.com.conf&lt;/code&gt; 的 &lt;code&gt;[renewalparams]&lt;/code&gt; 段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deploy_hook = systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;续期失败的常见原因&lt;/h2&gt;
&lt;p&gt;按出现频率排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;80 端口被封或跳转&lt;/strong&gt;。HTTP-01 必须走 80 端口（不允许跟随跳转到 443 之外的端口）。安全组、防火墙、CDN 回源配置都可能挡住 &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt; 路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;域名解析变化&lt;/strong&gt;。迁移服务器后忘了改 A 记录，CA 请求打到了旧机器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6 不一致&lt;/strong&gt;。域名同时有 AAAA 记录，CA 优先走 IPv6，而服务器 v6 没监听或被防火墙拦截——&lt;code&gt;--dry-run&lt;/code&gt; 报错里如果看到 IPv6 地址就是它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;速率限制&lt;/strong&gt;。同一注册域名每周最多签发 50 张证书，测试环境反复申请容易触发，错误信息会明确提示 rate limit。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;查看续期日志定位具体原因：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -u certbot.timer --since &quot;7 days ago&quot;
less /var/log/letsencrypt/letsencrypt.log
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;上线后的安全检查清单&lt;/h2&gt;
&lt;p&gt;证书装完不等于高枕无忧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;curl -vI https://example.com&lt;/code&gt; 确认返回 200 且证书链完整（输出里能看到 &lt;code&gt;SSL certificate verify ok&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;HTTP 是否 301 跳转到 HTTPS；跳转后有没有带上 HSTS 头（&lt;code&gt;Strict-Transport-Security&lt;/code&gt;），让浏览器以后直接走 HTTPS。&lt;/li&gt;
&lt;li&gt;旧协议和弱套件是否关闭——Qualys SSL Labs 的在线检测（ssllabs.com/ssltest）能给出评分和具体弱点清单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://letsencrypt.org/how-it-works/&quot;&gt;Let&apos;s Encrypt 官方文档：How It Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eff-certbot.readthedocs.io/en/stable/&quot;&gt;Certbot 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8555&quot;&gt;RFC 8555: Automatic Certificate Management Environment (ACME)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Nginx 反向代理与负载均衡配置实战</title><link>https://meow.us.kg/posts/nginx-reverse-proxy-load-balancing/</link><guid isPermaLink="true">https://meow.us.kg/posts/nginx-reverse-proxy-load-balancing/</guid><description>从一份可直接修改使用的配置骨架出发，讲清 Nginx 反向代理、负载均衡算法、健康检查与常见性能参数的含义和配置方法。</description><pubDate>Sat, 03 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;Nginx 在生产环境最常见的角色不是「Web 服务器」，而是站在所有应用前面的&lt;strong&gt;反向代理&lt;/strong&gt;：对外统一收流量，对内分发给多个后端实例。本文给出一份可以改改就用的配置骨架，并解释每个关键参数为什么存在。&lt;/p&gt;
&lt;h2&gt;反向代理：最小可用配置&lt;/h2&gt;
&lt;p&gt;假设后端应用监听本机 3000 端口，希望通过 80 端口对外提供 HTTP 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四行 &lt;code&gt;proxy_set_header&lt;/code&gt; 不是装饰，缺了它们后端会丢失关键信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Host&lt;/code&gt;：默认情况下 Nginx 会把请求头改成上游地址，后端基于域名的虚拟主机路由会失效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Real-IP&lt;/code&gt; / &lt;code&gt;X-Forwarded-For&lt;/code&gt;：后端看到的来源 IP 全是 127.0.0.1，日志和限流就废了。&lt;code&gt;X-Forwarded-For&lt;/code&gt; 用追加模式，保留完整代理链。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-Proto&lt;/code&gt;：告诉后端原始请求是 HTTP 还是 HTTPS，避免应用生成错误的重定向地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修改配置后先测试再生效，这是 Nginx 运维的铁律：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx -t            # 语法检查
systemctl reload nginx   # 平滑重载，不断开已有连接
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;负载均衡：upstream 块&lt;/h2&gt;
&lt;p&gt;多个后端实例时，用 &lt;code&gt;upstream&lt;/code&gt; 定义服务器组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream app_backend {
    # 默认算法：加权轮询
    server 10.0.1.11:3000 weight=3;
    server 10.0.1.12:3000;
    server 10.0.1.13:3000 backup;   # 备用节点，前面全挂才启用

    # keepalive 32;  # 与后端保持的长连接数，减少 TCP 握手
}

server {
    listen 80;
    location / {
        proxy_pass http://app_backend;
        # ... 同上 proxy_set_header
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调度算法选择&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;算法&lt;/th&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;/td&gt;
&lt;td&gt;&lt;code&gt;server ... weight=N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后端无状态、配置不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最少连接&lt;/td&gt;
&lt;td&gt;&lt;code&gt;least_conn;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;请求处理时长差异大（如报表导出）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP 哈希&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip_hash;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;需要会话粘滞但又没有共享 session 存储&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;code&gt;ip_hash&lt;/code&gt; 用客户端 IP 做哈希，同一个 IP 永远落到同一台后端。它解决的是「会话粘滞」问题，但会带来负载不均（比如大量用户从同一个公司出口 IP 进来）。更好的做法是应用层改用 JWT 或 Redis 集中存 session，让后端彻底无状态。
:::&lt;/p&gt;
&lt;h3&gt;被动健康检查&lt;/h3&gt;
&lt;p&gt;开源版 Nginx 没有主动探活，但可以用两个参数实现被动摘除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：30 秒内连续 3 次连接失败，就把该节点标记为不可用，30 秒后再试探。配合 &lt;code&gt;proxy_next_upstream&lt;/code&gt; 可以定义「失败」的范围和重试策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;静态资源与缓存：让后端少干活&lt;/h2&gt;
&lt;p&gt;反向代理层顺手做两件事，后端压力能降一个数量级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 静态文件直接由 Nginx 返回
location ~* \.(jpg|jpeg|png|css|js|ico|woff2)$ {
    root /var/www/app/public;
    expires 7d;
    add_header Cache-Control &quot;public, immutable&quot;;
}

# 开启 gzip
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1k;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见超时参数&lt;/h2&gt;
&lt;p&gt;反向代理挂起的连接会拖垮 worker 进程，这组参数值得显式设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_connect_timeout 5s;    # 与后端建立 TCP 连接的超时
proxy_send_timeout 60s;      # 向后端写请求的超时
proxy_read_timeout 60s;      # 从后端读响应的超时（慢接口要调大）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果后端是长轮询或 SSE 接口，&lt;code&gt;proxy_read_timeout&lt;/code&gt; 必须大于推送间隔，否则连接会被 Nginx 掐断。&lt;/p&gt;
&lt;h2&gt;排查入口&lt;/h2&gt;
&lt;p&gt;代理出问题时，两个日志位置记牢：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;error_log&lt;/code&gt;（默认 &lt;code&gt;/var/log/nginx/error.log&lt;/code&gt;）：后端连接失败、上游超时都记在这里。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;access_log&lt;/code&gt;：用自定义格式记录上游耗时，快速定位慢后端：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;log_format upstream_log &apos;$remote_addr - $upstream_addr &apos;
                        &apos;rt=$request_time urt=$upstream_response_time &apos;
                        &apos;status=$status &quot;$request&quot;&apos;;
access_log /var/log/nginx/upstream.log upstream_log;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$request_time&lt;/code&gt; 是总耗时，&lt;code&gt;$upstream_response_time&lt;/code&gt; 是后端处理耗时——两者差值就是 Nginx 排队和网络开销，一眼看出瓶颈在哪一层。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nginx.org/en/docs/http/ngx_http_proxy_module.html&quot;&gt;Nginx 官方文档：Module ngx_http_proxy_module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nginx.org/en/docs/http/ngx_http_upstream_module.html&quot;&gt;Nginx 官方文档：Module ngx_http_upstream_module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nginx.org/en/docs/http/load_balancing.html&quot;&gt;Nginx 官方文档：Using nginx as HTTP load balancer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Redis 实战：核心数据结构、缓存三大经典问题与持久化选择</title><link>https://meow.us.kg/posts/redis-structures-cache-persistence/</link><guid isPermaLink="true">https://meow.us.kg/posts/redis-structures-cache-persistence/</guid><description>梳理 Redis 五种核心数据结构的适用场景，分析缓存穿透、击穿、雪崩的成因与对策，对比 RDB 与 AOF 两种持久化方案的取舍。</description><pubDate>Thu, 01 Oct 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;Redis 常被一句话概括为「内存里的键值数据库」，但真要用好它，需要回答三个问题：该用哪种数据结构存？缓存出问题时系统会怎样？重启后数据还在不在？本文按这个顺序展开。&lt;/p&gt;
&lt;h2&gt;五种核心结构与适用场景&lt;/h2&gt;
&lt;p&gt;Redis 的 value 不只是字符串。选对结构，很多看似复杂的逻辑一条命令就能解决：&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;th&gt;典型场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;最基本，可存文本、数字、序列化数据&lt;/td&gt;
&lt;td&gt;缓存对象、计数器（&lt;code&gt;INCR&lt;/code&gt;）、分布式锁（&lt;code&gt;SET NX PX&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;字段-值对的集合&lt;/td&gt;
&lt;td&gt;存对象的部分字段，避免整取整存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;有序、可两端操作&lt;/td&gt;
&lt;td&gt;简单消息队列（&lt;code&gt;LPUSH&lt;/code&gt; + &lt;code&gt;BRPOP&lt;/code&gt;）、最新 N 条记录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set&lt;/td&gt;
&lt;td&gt;无序去重、支持交并差&lt;/td&gt;
&lt;td&gt;标签、共同好友（&lt;code&gt;SINTER&lt;/code&gt;）、抽奖去重&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorted Set&lt;/td&gt;
&lt;td&gt;每个成员带分数、按分排序&lt;/td&gt;
&lt;td&gt;排行榜（&lt;code&gt;ZADD&lt;/code&gt;/&lt;code&gt;ZREVRANGE&lt;/code&gt;）、延迟队列&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以排行榜为例，用 MySQL 实现需要 &lt;code&gt;ORDER BY score DESC LIMIT 10&lt;/code&gt; 扫索引；用 Sorted Set 只需要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ZADD rank 2580 &quot;player:1001&quot;
ZREVRANGE rank 0 9 WITHSCORES   # 前十名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Redis 7 之后还内置了 Stream（消息流）、Bitmap、HyperLogLog、GEO 等高级结构。HyperLogLog 做 UV 统计只需约 12KB 内存就能估算上亿基数，误差约 0.81%，适合「只要近似值」的大数据统计。
:::&lt;/p&gt;
&lt;h2&gt;缓存三大经典问题&lt;/h2&gt;
&lt;p&gt;把 Redis 放在数据库前面挡流量，是标准用法；但缓存层本身也会引入新的故障模式。这三个问题名字相似，成因和对策完全不同。&lt;/p&gt;
&lt;h3&gt;缓存穿透：查一个根本不存在的数据&lt;/h3&gt;
&lt;p&gt;请求恶意或异常地查询不存在的 key，缓存必然不命中，每次都打到数据库。&lt;/p&gt;
&lt;p&gt;对策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存空值&lt;/strong&gt;：查不到也把「空结果」缓存起来，设较短 TTL（如 60 秒）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;布隆过滤器&lt;/strong&gt;：在缓存前加一层概率型过滤器，「一定不存在」的请求直接拦截。代价是极小概率误判（存在的 key 被判不存在）和不支持删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存击穿：热点 key 刚好过期&lt;/h3&gt;
&lt;p&gt;某个被百万并发访问的热点 key 过期瞬间，所有请求同时涌向数据库。&lt;/p&gt;
&lt;p&gt;对策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥重建&lt;/strong&gt;：第一个发现缓存失效的请求先拿分布式锁（&lt;code&gt;SET lock NX PX 3000&lt;/code&gt;），由它查库回填，其余请求短暂等待后读缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑过期&lt;/strong&gt;：key 不物理过期，value 里存一个逻辑过期时间；发现过期后由后台线程异步更新，前台先返回旧值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存雪崩：大批 key 同时过期&lt;/h3&gt;
&lt;p&gt;通常是两个原因：大量 key 设了相同 TTL，或 Redis 实例整体宕机。&lt;/p&gt;
&lt;p&gt;对策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TTL 加随机抖动&lt;/strong&gt;：&lt;code&gt;3600 + random(600)&lt;/code&gt; 秒，打散过期时间点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可用部署&lt;/strong&gt;：主从 + Sentinel 或 Cluster，避免单点宕机演变成全量回源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级缓存与降级&lt;/strong&gt;：本地缓存（如 Caffeine）挡一层；缓存不可用时对非核心请求直接降级返回默认值，保住数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;持久化：RDB 与 AOF&lt;/h2&gt;
&lt;p&gt;纯内存意味着重启即清空。Redis 提供两种把数据落盘的方式，可以单独用也可以同时开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDB（快照）&lt;/strong&gt;：按配置的时间间隔（如 &lt;code&gt;save 900 1&lt;/code&gt; 表示 900 秒内至少 1 次修改）fork 子进程把全量数据写成二进制文件 &lt;code&gt;dump.rdb&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：文件紧凑、恢复速度快，适合做备份和灾难恢复。&lt;/li&gt;
&lt;li&gt;缺点：两次快照之间的修改在宕机时会丢失；数据量大时 fork 可能带来毫秒级停顿。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;AOF（追加日志）&lt;/strong&gt;：把每条写命令追加到 &lt;code&gt;appendonly.aof&lt;/code&gt;，配合三种 fsync 策略：&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;th&gt;性能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendfsync always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每命令落盘，几乎不丢&lt;/td&gt;
&lt;td&gt;最差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendfsync everysec&lt;/code&gt;（默认）&lt;/td&gt;
&lt;td&gt;宕机最多丢 1 秒&lt;/td&gt;
&lt;td&gt;折中，推荐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendfsync no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;交给操作系统，可能丢几十秒&lt;/td&gt;
&lt;td&gt;最好&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AOF 文件会持续增长，Redis 通过 &lt;strong&gt;AOF rewrite&lt;/strong&gt;（重写）在后台把它压缩成当前数据的最小命令集；7.0 起改用「主文件 + 增量文件」的多部分 AOF，重写期间更省内存和磁盘。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实际选择&lt;/strong&gt;：缓存场景可以不持久化；需要数据可靠性时开 AOF everysec，再用 RDB 定时快照做异地备份，两者互补。&lt;/p&gt;
&lt;h2&gt;使用 Redis 的几条纪律&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;所有缓存 key 都要设 TTL&lt;/strong&gt;，除非有明确的淘汰策略，否则内存迟早被撑爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禁止在生产环境用 &lt;code&gt;KEYS *&lt;/code&gt;&lt;/strong&gt;——它 O(N) 全量扫描，会直接阻塞单线程的 Redis。用 &lt;code&gt;SCAN&lt;/code&gt; 分批遍历。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大 key 是隐形炸弹&lt;/strong&gt;：一个几百 MB 的 Hash 会让删除、迁移、RDB 都变慢。拆分成多个小 key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除用 &lt;code&gt;UNLINK&lt;/code&gt; 替代 &lt;code&gt;DEL&lt;/code&gt;&lt;/strong&gt;（4.0+），把内存释放放到后台线程，避免阻塞。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/&quot;&gt;Redis 官方文档：Data types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/&quot;&gt;Redis 官方文档：Persistence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/reference/eviction/&quot;&gt;Redis 官方文档：Eviction policies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>事务、ACID 与隔离级别：并发异常从何而来</title><link>https://meow.us.kg/posts/database-transaction-acid-isolation/</link><guid isPermaLink="true">https://meow.us.kg/posts/database-transaction-acid-isolation/</guid><description>用转账场景讲清事务的 ACID 四个特性，通过可复现的 SQL 实验观察脏读、不可重复读与幻读，理解四种隔离级别的取舍与 MySQL 的默认选择。</description><pubDate>Tue, 29 Sep 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;「扣了钱但没到账」——这类 bug 几乎都与事务有关。事务是数据库提供给应用的一层承诺：把一组操作打包成不可分割的整体。理解这层承诺的具体内容（ACID）和它的折扣（隔离级别），才能写出在并发下依然正确的代码。&lt;/p&gt;
&lt;h2&gt;事务解决什么问题&lt;/h2&gt;
&lt;p&gt;经典转账场景：A 给 B 转 100 元，需要两条 UPDATE：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE accounts SET balance = balance - 100 WHERE name = &apos;A&apos;;
UPDATE accounts SET balance = balance + 100 WHERE name = &apos;B&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果第一条执行完系统宕机，钱就凭空消失了。用 &lt;code&gt;START TRANSACTION&lt;/code&gt; 把两条语句包起来，数据库保证&lt;strong&gt;要么都生效，要么都不生效&lt;/strong&gt;。这就是事务存在的意义。&lt;/p&gt;
&lt;h2&gt;ACID 四个承诺&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&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;原子性 Atomicity&lt;/td&gt;
&lt;td&gt;事务内操作全做或全不做&lt;/td&gt;
&lt;td&gt;undo log（回滚日志）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一致性 Consistency&lt;/td&gt;
&lt;td&gt;事务前后数据满足业务约束（如总额不变）&lt;/td&gt;
&lt;td&gt;由其他三个特性 + 应用代码共同保证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;隔离性 Isolation&lt;/td&gt;
&lt;td&gt;并发事务互不干扰&lt;/td&gt;
&lt;td&gt;锁与 MVCC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;持久性 Durability&lt;/td&gt;
&lt;td&gt;提交后的修改不因宕机丢失&lt;/td&gt;
&lt;td&gt;redo log（重做日志）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中「一致性」最容易被误解：它不是数据库单方面能保证的。比如「余额不能为负」这条约束，需要应用代码或 &lt;code&gt;CHECK&lt;/code&gt; 约束配合，数据库只提供原子、隔离、持久这三块地基。&lt;/p&gt;
&lt;h2&gt;并发的代价：三种读异常&lt;/h2&gt;
&lt;p&gt;如果所有事务都严格串行执行，隔离性完美但吞吐量惨不忍睹。为了性能，数据库允许事务并发交错执行，于是产生了三种经典异常。可以用两个会话（Session 1 和 Session 2）亲手复现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;脏读（Dirty Read）&lt;/strong&gt;：读到了别的事务&lt;strong&gt;还没提交&lt;/strong&gt;的修改。如果对方随后回滚，你读到的就是从未存在过的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不可重复读（Non-Repeatable Read）&lt;/strong&gt;：同一事务内，两次读同一行结果不同——因为中间有别的事务修改并提交了它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;幻读（Phantom Read）&lt;/strong&gt;：同一事务内，两次按相同条件范围查询，第二次多出了「幽灵行」——别的事务插入并提交了符合条件的新行。&lt;/p&gt;
&lt;h2&gt;四种隔离级别：用正确性换性能&lt;/h2&gt;
&lt;p&gt;SQL 标准定义了四个隔离级别，逐级收紧：&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;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;READ UNCOMMITTED&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;可能*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;* MySQL InnoDB 在 REPEATABLE READ 下通过 Next-Key Lock 基本杜绝了幻读，这是它比 SQL 标准要求更严格的地方。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MySQL（InnoDB）默认 REPEATABLE READ&lt;/strong&gt;，Oracle、PostgreSQL 默认 READ COMMITTED。从 MySQL 迁移到 PostgreSQL 的应用，要特别注意这个差异——同样的代码在 PG 下可能开始出现不可重复读。&lt;/p&gt;
&lt;p&gt;查看与设置当前隔离级别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查看
SELECT @@transaction_isolation;

-- 仅对下一个事务生效
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动手实验：观察不可重复读&lt;/h2&gt;
&lt;p&gt;准备数据后开两个终端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 准备
CREATE TABLE t (id INT PRIMARY KEY, val INT);
INSERT INTO t VALUES (1, 100);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 会话 1                          -- 会话 2
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT val FROM t WHERE id = 1;   -- 返回 100
                                   START TRANSACTION;
                                   UPDATE t SET val = 200 WHERE id = 1;
                                   COMMIT;
SELECT val FROM t WHERE id = 1;   -- READ COMMITTED 下返回 200
                                   -- REPEATABLE READ 下仍返回 100
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把隔离级别换成 REPEATABLE READ 再跑一遍，会话 1 两次读取结果相同——InnoDB 的 MVCC 让事务看到的是「事务开始那一刻」的一致性快照。&lt;/p&gt;
&lt;p&gt;:::note
MVCC（多版本并发控制）的核心思路：读不阻塞写、写不阻塞读。每行数据背后挂着一条 undo log 版本链，事务按自己的「 Read View」沿链条找到对自己可见的那个版本。
:::&lt;/p&gt;
&lt;h2&gt;实践建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;事务要短&lt;/strong&gt;。长事务持有锁和快照，会阻塞其他事务、撑大 undo log。把耗时操作（远程调用、文件处理）挪到事务外。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认级别通常够用&lt;/strong&gt;。不要为了「更安全」随手调 SERIALIZABLE，它本质是把并发读写成串行，高并发下性能崩塌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式处理死锁&lt;/strong&gt;。并发事务加锁顺序不一致会死锁，InnoDB 检测到后会杀掉一个事务。应用层应捕获死锁错误（MySQL 错误码 1213）并重试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一加锁顺序&lt;/strong&gt;。所有代码路径按相同顺序更新多张表/多行，能消除大部分死锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html&quot;&gt;MySQL 8.4 官方文档：InnoDB Transaction Isolation Levels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html&quot;&gt;MySQL 8.4 官方文档：InnoDB Multi-Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/transaction-iso.html&quot;&gt;PostgreSQL 文档：Transaction Isolation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>索引为什么快：B+ 树原理与索引失效常见场景</title><link>https://meow.us.kg/posts/sql-index-btree-explained/</link><guid isPermaLink="true">https://meow.us.kg/posts/sql-index-btree-explained/</guid><description>从「书页目录」讲清 B+ 树的结构优势，解释联合索引最左前缀原则，列举七种让索引失效的常见写法，并用 EXPLAIN 验证。</description><pubDate>Sun, 27 Sep 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;给表加个索引，查询从 3 秒变 30 毫秒——几乎每个后端开发者都经历过这种「魔法时刻」。但索引不是玄学：知道它的数据结构，就能预判什么查询能用它、什么写法会让它失效。本文以 InnoDB（MySQL 默认引擎）为背景。&lt;/p&gt;
&lt;h2&gt;全表扫描为什么慢&lt;/h2&gt;
&lt;p&gt;没有索引时，找 &lt;code&gt;WHERE id = 9527&lt;/code&gt; 要把表里的数据页从头到尾翻一遍，一百万行就是一百万次比较，还伴随大量磁盘 I/O。优化目标很明确：少读数据页。&lt;/p&gt;
&lt;h2&gt;B+ 树：为磁盘设计的平衡树&lt;/h2&gt;
&lt;p&gt;二叉查找树在内存里很好，但树太深——百万级数据也有 20 层，每层一次磁盘 I/O 就受不了。B+ 树的做法是让每个节点「变胖」：一个节点放一个数据页（InnoDB 默认 16KB），存几百到上千个键。这样千万级数据的索引也只有 3~4 层，绝大多数查询 3 次 I/O 内命中。&lt;/p&gt;
&lt;p&gt;B+ 树的两个结构特征决定了它的使用规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据全在叶子节点&lt;/strong&gt;，非叶子节点只存导航用的键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叶子节点按键有序并用链表串联&lt;/strong&gt;——所以范围查询（&lt;code&gt;BETWEEN&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;）和排序（&lt;code&gt;ORDER BY 索引列&lt;/code&gt;）可以顺着叶子链表扫，不需要回树。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;类比：非叶子节点是书的章节目录，叶子链表是按页码排好的正文。&lt;/p&gt;
&lt;h2&gt;主键索引与回表&lt;/h2&gt;
&lt;p&gt;InnoDB 的表数据本身就按主键组织成一棵 B+ 树（聚簇索引），叶子节点存整行。二级索引（普通索引）的叶子节点存的是「索引列 + 主键值」。所以用二级索引查到后，如果 SELECT 的列不在索引里，还要拿主键再去主键树查一次——这叫&lt;strong&gt;回表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;覆盖索引&lt;/strong&gt;是避免回表的技巧：&lt;code&gt;SELECT order_id, status FROM orders WHERE status = &apos;paid&apos;&lt;/code&gt;，若 &lt;code&gt;(status, order_id)&lt;/code&gt; 上有索引，需要的列全在索引里，EXPLAIN 的 Extra 列会显示 &lt;code&gt;Using index&lt;/code&gt;，一跳到位。&lt;/p&gt;
&lt;h2&gt;联合索引与最左前缀&lt;/h2&gt;
&lt;p&gt;索引 &lt;code&gt;(a, b, c)&lt;/code&gt; 的排序规则是：先按 a 排，a 相同按 b，b 相同按 c。因此它能服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WHERE a = 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE a = 1 AND b = 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE a = 1 AND b = 2 AND c = 3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但不能直接服务 &lt;code&gt;WHERE b = 2&lt;/code&gt;（跳过 a）或 &lt;code&gt;WHERE a = 1 AND c = 3&lt;/code&gt;（跳过 b 后 c 不再有序，c 条件只能过滤不能导航）。这就是&lt;strong&gt;最左前缀原则&lt;/strong&gt;：条件必须从索引最左列连续匹配。范围条件（&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;LIKE &apos;x%&apos;&lt;/code&gt;）之后，索引的导航能力也到此为止，后面的列只能用来过滤。&lt;/p&gt;
&lt;h2&gt;七种常见的索引失效写法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对索引列做运算&lt;/strong&gt;：&lt;code&gt;WHERE DATE(create_time) = &apos;2026-09-27&apos;&lt;/code&gt; → 改成范围 &lt;code&gt;create_time &amp;gt;= &apos;2026-09-27&apos; AND create_time &amp;lt; &apos;2026-09-28&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;函数包裹&lt;/strong&gt;：&lt;code&gt;WHERE UPPER(email) = &apos;A@B.COM&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐式类型转换&lt;/strong&gt;：字符串列用数字查 &lt;code&gt;WHERE phone = 13800000000&lt;/code&gt;，列被套了 CAST，索引失效。手机号、验证码列务必用字符串类型并带引号查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前导通配符&lt;/strong&gt;：&lt;code&gt;LIKE &apos;%abc&apos;&lt;/code&gt; 无法利用有序性；&lt;code&gt;LIKE &apos;abc%&apos;&lt;/code&gt; 可以。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OR 连接非索引列&lt;/strong&gt;：&lt;code&gt;WHERE a = 1 OR d = 2&lt;/code&gt;，d 无索引时整体退化为全表扫描。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;!=&lt;/code&gt;、&lt;code&gt;NOT IN&lt;/code&gt;&lt;/strong&gt;：选择性太差时优化器宁可全表扫（严格说这是优化器的成本选择，不是绝对失效）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引列参与表达式&lt;/strong&gt;：&lt;code&gt;WHERE age + 1 &amp;gt; 30&lt;/code&gt; → 写成 &lt;code&gt;age &amp;gt; 29&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;用 EXPLAIN 验证，而不是猜&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = &apos;paid&apos;;
&lt;/code&gt;&lt;/pre&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;type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从好到差大致是 &lt;code&gt;const&lt;/code&gt; &amp;gt; &lt;code&gt;eq_ref&lt;/code&gt; &amp;gt; &lt;code&gt;ref&lt;/code&gt; &amp;gt; &lt;code&gt;range&lt;/code&gt; &amp;gt; &lt;code&gt;index&lt;/code&gt; &amp;gt; &lt;code&gt;ALL&lt;/code&gt;（全表扫描，危险信号）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;实际用了哪个索引；NULL 就是没用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;预估扫描行数，越小越好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Extra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Using index&lt;/code&gt;（覆盖索引，好事）；&lt;code&gt;Using filesort&lt;/code&gt;、&lt;code&gt;Using temporary&lt;/code&gt;（通常需要优化）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;（MySQL 8.0.18+）还能给出实际执行耗时，比预估更可靠。&lt;/p&gt;
&lt;h2&gt;索引不是免费的&lt;/h2&gt;
&lt;p&gt;每个索引都让写入变慢（INSERT/UPDATE 要维护树），还占磁盘。给表设计索引的原则是：&lt;strong&gt;按真实查询模式建少量联合索引&lt;/strong&gt;，而不是给每列都建一个。慢查询日志（&lt;code&gt;slow_query_log&lt;/code&gt; + &lt;code&gt;long_query_time = 1&lt;/code&gt;）是发现「该建什么索引」的最佳输入。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/optimization-indexes.html&quot;&gt;MySQL 8.4 官方文档：Optimization and Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/explain-output.html&quot;&gt;MySQL: EXPLAIN Output Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://use-the-index-luke.com/&quot;&gt;Use The Index, Luke（免费在线）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SSH 远程管理进阶：密钥登录、端口转发与 tmux 会话保持</title><link>https://meow.us.kg/posts/ssh-keys-tunnel-tmux/</link><guid isPermaLink="true">https://meow.us.kg/posts/ssh-keys-tunnel-tmux/</guid><description>从密码登录迁移到密钥登录，用 SSH 三种端口转发解决内网访问问题，再用 tmux 让长任务在断线后继续运行。</description><pubDate>Fri, 25 Sep 2026 13:10:00 GMT</pubDate><content:encoded>&lt;p&gt;SSH 是每个 Linux 用户每天都用的工具，但多数人只用了它 10% 的能力。本文讲三件能立刻提升效率和安全的事：密钥认证、端口转发、tmux 会话保持。&lt;/p&gt;
&lt;h2&gt;第一步：换掉密码登录&lt;/h2&gt;
&lt;p&gt;密码登录暴露在公网上，等于把门交给全世界的爆破脚本。密钥认证则是「你有一把私钥，服务器存着公钥」，即使密码泄露也进不来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 本地生成密钥（ed25519 是当前推荐算法，更短更快）
ssh-keygen -t ed25519 -C &quot;alice@laptop&quot;

# 把公钥推送到服务器（仍需密码登录一次）
ssh-copy-id alice@server.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证 &lt;code&gt;ssh alice@server.example.com&lt;/code&gt; 免密登录成功后，再修改服务器 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; 加固：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PasswordAuthentication no      # 关闭密码登录
PermitRootLogin no             # 禁止 root 直连
PubkeyAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
改配置后&lt;strong&gt;不要关闭当前会话&lt;/strong&gt;，先 &lt;code&gt;sudo systemctl reload sshd&lt;/code&gt;，另开终端验证新会话能登录，再退出旧会话。顺序反了可能把自己锁在门外。
:::&lt;/p&gt;
&lt;p&gt;私钥设置强口令（passphrase）+ &lt;code&gt;ssh-agent&lt;/code&gt; 缓存，兼顾安全与便利：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eval &quot;$(ssh-agent -s)&quot;
ssh-add ~/.ssh/id_ed25519     # 输一次口令，本次会话内免密
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;免敲长命令：~/.ssh/config&lt;/h2&gt;
&lt;p&gt;本地 &lt;code&gt;~/.ssh/config&lt;/code&gt; 可以给每台服务器起别名、指定端口和密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host prod
    HostName 203.0.113.20
    User alice
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Host db-via-prod
    HostName 10.0.1.15
    User dba
    ProxyJump prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后 &lt;code&gt;ssh prod&lt;/code&gt; 即可登录；&lt;code&gt;ssh db-via-prod&lt;/code&gt; 通过 &lt;code&gt;prod&lt;/code&gt; 作跳板直连内网数据库服务器——&lt;code&gt;ProxyJump&lt;/code&gt;（-J）替代了手动两段跳转。&lt;/p&gt;
&lt;h2&gt;端口转发的三种形态&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;本地转发（-L）&lt;/strong&gt;：把远端服务「搬到」本地端口。场景：数据库只在服务器内网监听 3306，本地想用 GUI 客户端连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -L 13306:127.0.0.1:3306 prod -N
# 本地连接 127.0.0.1:13306，流量经 SSH 隧道到 prod 的 3306
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-N&lt;/code&gt; 表示只转发不开 shell。语义记忆：&lt;code&gt;-L 本地端口:目标(从SSH服务器视角):目标端口&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;远程转发（-R）&lt;/strong&gt;：把本地服务「暴露」给远端。场景：本地开发了一个 webhook 接收服务，想让外部测试环境回调：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -R 9090:127.0.0.1:3000 prod -N
# 访问 prod 的 9090 端口 = 访问你本地的 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意服务器默认只把转发端口绑在回环上，要对公网开放需服务端开启 &lt;code&gt;GatewayPorts&lt;/code&gt;（谨慎评估暴露面）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动态转发（-D）&lt;/strong&gt;：开一个 SOCKS5 代理，浏览器或 curl 的全部流量都经服务器出口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -D 1080 prod -N
curl --socks5 127.0.0.1:1080 https://ifconfig.me   # 显示的是 prod 的出口 IP
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;断线不慌：tmux&lt;/h2&gt;
&lt;p&gt;耗时任务（编译、导入数据、训练）最怕 SSH 断线——进程收到 SIGHUP 直接终止。tmux 让会话运行在服务器端，断线后重连再贴回去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tmux new -s import        # 新建名为 import 的会话
# 在会话里跑长任务；随时 Ctrl+b 然后按 d 脱离（detach）
tmux ls                   # 查看会话列表
tmux attach -t import     # 重新接管
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个高频快捷键（前缀键 &lt;code&gt;Ctrl+b&lt;/code&gt; 之后按）：&lt;code&gt;c&lt;/code&gt; 新建窗口、&lt;code&gt;数字&lt;/code&gt; 切换窗口、&lt;code&gt;%&lt;/code&gt; 左右分屏、&lt;code&gt;&quot;&lt;/code&gt; 上下分屏、&lt;code&gt;[&lt;/code&gt; 进入滚屏模式（q 退出）。&lt;/p&gt;
&lt;p&gt;团队协作时 &lt;code&gt;tmux attach&lt;/code&gt; 到同一会话还能实现「双人同屏排障」。&lt;/p&gt;
&lt;h2&gt;保持连接与断线重试&lt;/h2&gt;
&lt;p&gt;网络不稳时，客户端定时发心跳防止 NAT/防火墙静默断连（写入本地 &lt;code&gt;~/.ssh/config&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host *
    ServerAliveInterval 30
    ServerAliveCountMax 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要求更高的场景可以用 &lt;code&gt;mosh&lt;/code&gt;（基于 UDP，支持漫游和本地回显），在高铁、弱 Wi-Fi 下体验明显更好，但服务端需额外安装并放行 UDP 端口。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;man 1 ssh&lt;/code&gt;、&lt;code&gt;man 5 ssh_config&lt;/code&gt;、&lt;code&gt;man 5 sshd_config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.openssh.com/manual.html&quot;&gt;OpenSSH 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tmux/tmux/wiki&quot;&gt;tmux 官方 Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mosh.org/&quot;&gt;Mosh 官网&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>通过 MAS 激活 Windows 与 Office：完整操作指南</title><link>https://meow.us.kg/posts/activate-windows-office-with-mas/</link><guid isPermaLink="true">https://meow.us.kg/posts/activate-windows-office-with-mas/</guid><description>使用 Microsoft Activation Scripts（MAS）一键激活 Windows 与 Office，支持 HWID 永久数字许可、Ohook Office 永久激活及 KMS 续期模式。</description><pubDate>Thu, 24 Sep 2026 01:23:09 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MAS（Microsoft Activation Scripts）&lt;/strong&gt; 是由 massgrave 维护的一套开源激活脚本集合，它提供了多种激活 Windows 与 Microsoft Office 的方式，所有操作通过 PowerShell 一行命令即可完成。&lt;/p&gt;
&lt;p&gt;本文整理了 MAS 的三种核心激活模式：HWID（Windows 永久数字许可）、Ohook（Office 永久激活）、KMS（通用 180 天续期模式），供有正版授权但遇到激活问题的用户参考。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 本文面向已持有合法正版授权或可接受开源工具试用的读者。请遵守当地法律法规与 Microsoft 服务条款。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前置条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10 或 Windows 11（Home / Pro / Enterprise 等主流版本）&lt;/li&gt;
&lt;li&gt;Microsoft Office 2016 / 2019 / 2021 / 365（已安装）&lt;/li&gt;
&lt;li&gt;以&lt;strong&gt;管理员身份&lt;/strong&gt;运行 PowerShell（或 Windows Terminal / PowerShell 7）&lt;/li&gt;
&lt;li&gt;已连通的网络环境&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;打开管理员 PowerShell&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;按 &amp;lt;kbd&amp;gt;Win&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;S&amp;lt;/kbd&amp;gt; 打开搜索&lt;/li&gt;
&lt;li&gt;输入 &lt;code&gt;PowerShell&lt;/code&gt; 或 &lt;code&gt;pwsh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;右键 → &lt;strong&gt;以管理员身份运行&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;PowerShell 窗口标题会显示「管理员」字样，确认无误后再继续。&lt;/p&gt;
&lt;h2&gt;一键激活脚本&lt;/h2&gt;
&lt;p&gt;在管理员 PowerShell 中复制粘贴以下命令并回车：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;irm https://get.activated.win | iex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果官方链接不稳定，可以使用镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;irm https://zf.meow.us.kg/https://get.activated.win | iex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本执行后会弹出交互式菜单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Microsoft Activation Scripts (MAS)

1. HWID          [Windows 永久激活]
2. Ohook         [Office 永久激活]
3. AAct          [可选，支持更多产品]
4. KMS38 / Online KMS  [180 天续期]
5. 查看当前激活状态
0. 退出
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;激活模式详解&lt;/h2&gt;
&lt;h3&gt;模式 1：HWID —— Windows 永久数字许可&lt;/h3&gt;
&lt;p&gt;HWID 是 MAS 最受欢迎的功能。它会生成与你的机器硬件指纹绑定的数字授权，写入系统后由 Microsoft 官方服务器在线识别——&lt;strong&gt;效果等同于原生数字激活&lt;/strong&gt;，重装系统后只要硬件不变会自动恢复激活。&lt;/p&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10 / 11 的 Home、Pro、Enterprise 等版本&lt;/li&gt;
&lt;li&gt;希望获得永久激活状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：在菜单中按 &lt;code&gt;1&lt;/code&gt;，脚本会自动执行硬件指纹采集、公钥上传和许可证写入，全程无需人工干预。完成后系统提示 &lt;strong&gt;Activation successful&lt;/strong&gt; 即成功。&lt;/p&gt;
&lt;h3&gt;模式 2：Ohook —— Office 永久激活&lt;/h3&gt;
&lt;p&gt;Ohook（Office Hoook）采用 Hook 方式在 Office 运行时拦截授权校验，实现永久激活。&lt;/p&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Office 2016 / 2019 / 2021 永久版（Volume License / VL 渠道）&lt;/li&gt;
&lt;li&gt;Office 365 不适用（订阅制，需替换许可证）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：先确保 Office 已安装，在菜单中按 &lt;code&gt;2&lt;/code&gt;，脚本会自动完成 Ohook 的部署。&lt;/p&gt;
&lt;h3&gt;模式 4：KMS —— 通用 180 天续期&lt;/h3&gt;
&lt;p&gt;KMS（Key Management Service）是 Microsoft 官方设计的企业批量授权机制。通过 KMS 续期激活后，每 180 天自动向 KMS 服务器续一次，实际使用体验等同于永久。&lt;/p&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HWID 不生效的机器&lt;/li&gt;
&lt;li&gt;Office 365 等 Ohook 不支持的产品&lt;/li&gt;
&lt;li&gt;不愿修改硬件指纹的用户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：在菜单中按 &lt;code&gt;4&lt;/code&gt;，脚本会自动寻找可用 KMS 服务器并配置自动续期计划任务。&lt;/p&gt;
&lt;h2&gt;验证激活状态&lt;/h2&gt;
&lt;h3&gt;检查 Windows 激活&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式一：系统设置&lt;/strong&gt;
&lt;code&gt;设置&lt;/code&gt; → &lt;code&gt;系统&lt;/code&gt; → &lt;code&gt;激活&lt;/code&gt;，页面会显示激活状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式二：命令行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slmgr /xpr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;弹出窗口会显示激活期限。若为 HWID 激活则显示「永久许可证」。&lt;/p&gt;
&lt;h3&gt;检查 Office 激活&lt;/h3&gt;
&lt;p&gt;打开任意 Office 应用（Word / Excel / PowerPoint），进入 &lt;strong&gt;账户&lt;/strong&gt; 页面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ohook 激活：产品信息下方显示「已激活」&lt;/li&gt;
&lt;li&gt;KMS 激活：显示「已授权（剩余 XXX 天）」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也可以通过命令查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 找到 Office 安装路径（常见）
cd &quot;C:\Program Files\Microsoft Office\Office16&quot;
cscript ospp.vbs /dstatus
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q：HWID 激活后重装系统会失效吗？&lt;/h3&gt;
&lt;p&gt;不会。HWID 激活绑定的是你的主板 MAC 地址、TPM、CPU 等硬件指纹，Microsoft 服务器端存储该指纹与合法许可证的映射。只要&lt;strong&gt;硬件不发生大规模更换&lt;/strong&gt;（如换主板），重装 Windows 后首次联网会自动识别并重新激活。&lt;/p&gt;
&lt;h3&gt;Q：Ohook 激活的 Office 可以通过官方正版验证吗？&lt;/h3&gt;
&lt;p&gt;Ohook 是运行时 Hook，不会篡改 Office 安装包或注册表核心键，Windows 更新和 Office 更新通常不会将其视为异常。但它无法通过 Microsoft Online 账户的设备管理页面查询到。&lt;/p&gt;
&lt;h3&gt;Q：脚本下载地址安全吗？&lt;/h3&gt;
&lt;p&gt;MAS 是 GitHub 上高星（&amp;gt;20k stars）的开源项目，源码全部公开可审计。建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 GitHub release 页下载 ZIP 本地运行（最安全）&lt;/li&gt;
&lt;li&gt;或使用 &lt;code&gt;irm https://raw.githubusercontent.com/massgrave/Microsoft-Activation-Scripts/master/MAS/All-In-One-Version/MAS_AIO.cmd | iex&lt;/code&gt; 指向官方仓库 raw 地址&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Q：可以同时激活 Windows 和 Office 吗？&lt;/h3&gt;
&lt;p&gt;可以。先执行模式 1 激活 Windows，再执行模式 2 或 4 激活 Office，两次操作互不干扰。&lt;/p&gt;
&lt;h3&gt;Q：Windows Defender 或 360 杀毒会拦截吗？&lt;/h3&gt;
&lt;p&gt;MAS 本身是 PowerShell 脚本，不含病毒。个别安全软件对未知签名脚本会有提示，属于正常行为。如果下载受阻，可以先临时关闭实时防护再运行，运行完毕后重新开启。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;官方仓库：&lt;a href=&quot;https://github.com/massgrave/Microsoft-Activation-Scripts&quot;&gt;https://github.com/massgrave/Microsoft-Activation-Scripts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;官方一键入口：&lt;a href=&quot;https://get.activated.win&quot;&gt;https://get.activated.win&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;激活原理与 FAQ：参见仓库 README 的「How it Works」章节&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux 网络排错工具箱：ss、ping、mtr 与 tcpdump 的使用场景</title><link>https://meow.us.kg/posts/linux-network-troubleshooting-toolbox/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-network-troubleshooting-toolbox/</guid><description>按「服务起不来、连不上、慢、间歇断」四类症状组织网络排错工具，给出每个工具的核心命令与结果判读方法。</description><pubDate>Wed, 23 Sep 2026 01:45:00 GMT</pubDate><content:encoded>&lt;p&gt;网络排错最大的浪费是「用错工具」：服务没监听却去测带宽，防火墙丢包却去查应用日志。本文按症状组织工具，每个工具只讲排障中最常用的那一部分。&lt;/p&gt;
&lt;h2&gt;症状一：服务起来了，但端口不通&lt;/h2&gt;
&lt;p&gt;先在&lt;strong&gt;本机&lt;/strong&gt;确认监听状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ss -tlnp | grep 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ss&lt;/code&gt; 是 netstat 的现代替代（更快、信息更全）。选项：&lt;code&gt;-t&lt;/code&gt; TCP、&lt;code&gt;-l&lt;/code&gt; 监听中、&lt;code&gt;-n&lt;/code&gt; 数字端口、&lt;code&gt;-p&lt;/code&gt; 显示进程。输出看两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Local Address&lt;/code&gt; 是 &lt;code&gt;0.0.0.0:8080&lt;/code&gt;（监听所有网卡）还是 &lt;code&gt;127.0.0.1:8080&lt;/code&gt;（只听回环）。&lt;strong&gt;外网连不上而本机 curl 通，八成是服务只监听了 127.0.0.1&lt;/strong&gt;，改配置绑到 &lt;code&gt;0.0.0.0&lt;/code&gt; 或具体网卡。&lt;/li&gt;
&lt;li&gt;进程列显示的服务名是不是你预期的那个——端口被别的程序占用的情况并不少。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本机监听正常，再从&lt;strong&gt;外部&lt;/strong&gt;测连通性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在客户端执行
curl -v telnet://服务器IP:8080     # 或 nc -vz 服务器IP 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接超时（timeout）= 中间有设备丢包，查防火墙/安全组；连接拒绝（refused）= 包到了但没人接，回到上一步查监听。这个「超时 vs 拒绝」的区分是最有价值的判断。&lt;/p&gt;
&lt;p&gt;防火墙检查（在服务器上）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo iptables -L -n | grep 8080     # 或 nft list ruleset
sudo firewall-cmd --list-ports      # firewalld 系
# 云服务器还要检查控制台的安全组规则
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;症状二：整体网络不通&lt;/h2&gt;
&lt;p&gt;分层验证，从近到远：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr                    # 网卡有没有拿到 IP
ip route                   # 默认网关是否存在
ping -c 4 网关IP            # 局域网通不通
ping -c 4 223.5.5.5        # 出网通不通（用 IP 排除 DNS 干扰）
ping -c 4 example.com      # DNS 是否正常
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哪一步失败就停在哪一层：网关不通查网线和交换机，IP 通而域名不通去查 DNS（参考本系列 DNS 一文），都通但应用不通回到症状一。&lt;/p&gt;
&lt;h2&gt;症状三：能通但慢、丢包&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ping&lt;/code&gt; 只能告诉你「到终点丢不丢」，定位「在哪一跳开始丢」用 &lt;code&gt;mtr&lt;/code&gt;（My Traceroute）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mtr -rwbzc 100 目标IP     # 发 100 个包，输出报告模式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报告里逐跳显示丢包率和延迟。判读要点：&lt;strong&gt;从某一跳开始丢包率显著升高且延续到终点，那一跳附近就是问题点&lt;/strong&gt;；中间某跳丢包但终点不丢，多半是该设备限速响应 ICMP，属误报。&lt;code&gt;mtr&lt;/code&gt; 同时给出 Best/Wrst/Avg 延迟，看抖动比看平均值更有用。&lt;/p&gt;
&lt;p&gt;带宽是否跑满用 &lt;code&gt;iperf3&lt;/code&gt; 两台机器对测；实时看网卡流量用 &lt;code&gt;iftop&lt;/code&gt; 或 &lt;code&gt;nload&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;症状四：玄学问题，上抓包&lt;/h2&gt;
&lt;p&gt;「间歇性断连」「应用说收到了乱码」这类问题只能靠看报文。&lt;code&gt;tcpdump&lt;/code&gt; 常用模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 抓与某主机的 HTTP 流量并显示内容
sudo tcpdump -i any -nnA &apos;host 203.0.113.5 and port 80&apos;

# 只看 TCP 标志位（找 RST、重传）
sudo tcpdump -i any -nn &apos;tcp[tcpflags] &amp;amp; tcp-rst != 0&apos;

# 写到文件，回头用 Wireshark 分析
sudo tcpdump -i any -nn -w /tmp/issue.pcap &apos;port 3306&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产环境抓包务必加过滤条件并限制数量（&lt;code&gt;-c&lt;/code&gt;）或文件大小（&lt;code&gt;-C&lt;/code&gt;/&lt;code&gt;-W&lt;/code&gt;），避免打满磁盘。&lt;code&gt;.pcap&lt;/code&gt; 文件传到本地用 Wireshark 打开，「Follow TCP Stream」能把一条连接的完整会话重组出来，排查应用层问题极其高效。&lt;/p&gt;
&lt;h2&gt;决策速查表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;症状&lt;/th&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;/td&gt;
&lt;td&gt;&lt;code&gt;ss -tlnp&lt;/code&gt; + 客户端 nc&lt;/td&gt;
&lt;td&gt;超时=丢包，拒绝=未监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;整网不通&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip route&lt;/code&gt;、&lt;code&gt;ping&lt;/code&gt; 分层&lt;/td&gt;
&lt;td&gt;从网关向外逐层推进&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;慢/丢包&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mtr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;找丢包率跳变的那一跳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;带宽跑满&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iperf3&lt;/code&gt;、&lt;code&gt;iftop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;确认瓶颈在链路还是应用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;间歇断连&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tcpdump&lt;/code&gt; + Wireshark&lt;/td&gt;
&lt;td&gt;看 RST 出现在哪一侧&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;排错的通用节奏是：先本机（监听、防火墙）→ 再链路（ping/mtr）→ 最后报文（tcpdump）。每用一个工具都要能回答「这个结果排除了什么可能性」，工具才不会白用。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;man 8 ss&lt;/code&gt;、&lt;code&gt;man 8 mtr&lt;/code&gt;、&lt;code&gt;man 8 tcpdump&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://iperf.fr/iperf-doc.php&quot;&gt;iperf3 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wireshark.org/docs/wsug_html_chunked/&quot;&gt;Wireshark User&apos;s Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>HTTP 与 HTTPS：TLS 握手、证书链与 curl 调试技巧</title><link>https://meow.us.kg/posts/http-https-tls-curl-debug/</link><guid isPermaLink="true">https://meow.us.kg/posts/http-https-tls-curl-debug/</guid><description>用 curl -v 拆解一次 HTTPS 请求的全过程，看懂 TLS 1.2/1.3 握手差异、证书链验证逻辑，以及常见证书报错的排查方法。</description><pubDate>Mon, 21 Sep 2026 11:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;curl -v&lt;/code&gt; 输出的每一行都对应协议栈里的一个阶段。学会读它，HTTP/HTTPS 问题就排掉了一半。本文以一次真实请求为样本，讲清 HTTP 报文结构、TLS 握手在干什么、证书报错怎么定位。&lt;/p&gt;
&lt;h2&gt;一次请求的完整解剖&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;curl -v https://example.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; 开头是客户端发出的，&lt;code&gt;&amp;gt;&lt;/code&gt; 之外带 &lt;code&gt;&amp;lt;&lt;/code&gt; 的是收到的，&lt;code&gt;*&lt;/code&gt; 是 curl 自己的注释。发出部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/2
Host: example.com
User-Agent: curl/8.x
Accept: */*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是 HTTP 请求报文的全部骨架：&lt;strong&gt;请求行&lt;/strong&gt;（方法 + 路径 + 版本）+ &lt;strong&gt;首部&lt;/strong&gt;（键值对）+ 可选的&lt;strong&gt;请求体&lt;/strong&gt;。响应同理：状态行（&lt;code&gt;HTTP/2 200&lt;/code&gt;）+ 首部 + 正文。&lt;/p&gt;
&lt;p&gt;要单独看首部用 &lt;code&gt;curl -I&lt;/code&gt;（发 HEAD 请求）；要忽略正文只看交换过程用 &lt;code&gt;curl -sv https://... -o /dev/null&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;TLS 握手在 HTTP 之前&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;curl -v&lt;/code&gt; 里 &lt;code&gt;*&lt;/code&gt; 部分有一段 TLS 日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;握手的两个目标：&lt;strong&gt;协商加密参数&lt;/strong&gt;和&lt;strong&gt;验证服务器身份&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TLS 1.2：密钥协商走 RSA 或 ECDHE，需要两个完整往返（2-RTT）。&lt;/li&gt;
&lt;li&gt;TLS 1.3（2018 年定稿，现为主流）：客户端在 Client Hello 里直接带上密钥材料，1-RTT 完成握手，且支持 0-RTT 会话恢复；同时把证书之后的握手消息也加了密。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对使用者来说结论很简单：&lt;strong&gt;TLS 1.3 更快更安全，服务端应优先启用并保留 1.2 兜底，1.0/1.1 必须禁用&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;证书链：浏览器凭什么信任这个网站&lt;/h2&gt;
&lt;p&gt;服务器在握手中出示的不是一张证书，而是一条链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;服务器证书（example.com）
  ↑ 由 中间 CA（如 R3）签发
  ↑ 由 根 CA（如 ISRG Root X1）签发 ← 预置在操作系统/浏览器信任库
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证逻辑：逐级用上级公钥校验签名，直到碰到信任库里已有的根。&lt;strong&gt;中间证书必须由服务器一起下发&lt;/strong&gt;——最常见的「浏览器正常、curl/旧安卓报证书错误」，就是服务器漏配中间证书（fullchain），浏览器能自动补齐而别的客户端不能。&lt;/p&gt;
&lt;p&gt;用 OpenSSL 查看证书细节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl s_client -connect example.com:443 -servername example.com &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 -noout -subject -issuer -dates
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-dates&lt;/code&gt; 看有效期（过期是第二大常见故障），&lt;code&gt;-servername&lt;/code&gt; 用于一台服务器托管多个 HTTPS 站点（SNI）时指定域名。&lt;/p&gt;
&lt;h2&gt;常见证书报错对号入座&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;curl 报错&lt;/th&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;certificate has expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;证书过期&lt;/td&gt;
&lt;td&gt;续期，检查自动续期任务是否失效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unable to get local issuer certificate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务器没发中间证书，或本地 CA 库太旧&lt;/td&gt;
&lt;td&gt;服务端配置 fullchain；客户端更新 ca-certificates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject name mismatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;证书域名与访问域名不符&lt;/td&gt;
&lt;td&gt;证书上 SAN 没包含该域名（注意 &lt;code&gt;www.&lt;/code&gt; 前缀差异）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;self signed certificate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自签名证书&lt;/td&gt;
&lt;td&gt;内网可 &lt;code&gt;-k&lt;/code&gt; 跳过或分发 CA；公网换正规证书&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;code&gt;curl -k&lt;/code&gt;（不验证证书）只能在临时测试时使用。写进脚本等于放弃了 HTTPS 防中间人攻击的核心价值。
:::&lt;/p&gt;
&lt;h2&gt;把请求拆开计时&lt;/h2&gt;
&lt;p&gt;接口慢，到底是 DNS 慢、握手慢还是服务器处理慢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -so /dev/null -w &apos;DNS:%{time_namelookup}s TCP:%{time_connect}s TLS:%{time_appconnect}s 首字节:%{time_starttransfer}s 总:%{time_total}s\n&apos; https://example.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;time_connect - time_namelookup&lt;/code&gt; ≈ TCP 握手耗时（约等于 RTT）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;time_appconnect - time_connect&lt;/code&gt; ≈ TLS 握手耗时；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;time_starttransfer - time_appconnect&lt;/code&gt; ≈ 服务器处理并返回首字节的耗时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一眼就能看出慢在哪一段，比凭感觉猜高效得多。&lt;/p&gt;
&lt;h2&gt;HTTP/2 与 HTTP/3 简述&lt;/h2&gt;
&lt;p&gt;HTTP/2 在单条 TCP 连接上多路复用（解决 HTTP/1.1 队头阻塞），首部走 HPACK 压缩；HTTP/3 把底层换成基于 UDP 的 QUIC，进一步消除 TCP 层队头阻塞并改善弱网切换。对开发者透明——curl 和浏览器会自动协商（&lt;code&gt;curl -v&lt;/code&gt; 里能看到 &lt;code&gt;ALPN: server accepted h2&lt;/code&gt;），服务端用 Nginx/云 CDN 开启即可。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8446&quot;&gt;RFC 8446: TLS 1.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110&quot;&gt;RFC 9110: HTTP Semantics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://curl.se/docs/&quot;&gt;curl 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ssllabs.com/ssltest/&quot;&gt;SSL Labs 服务器测试&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DNS 解析全链路与 dig 排错实战</title><link>https://meow.us.kg/posts/dns-resolution-dig-troubleshooting/</link><guid isPermaLink="true">https://meow.us.kg/posts/dns-resolution-dig-troubleshooting/</guid><description>从 /etc/hosts 到根服务器的完整解析链路，讲清递归与迭代的区别，用 dig +trace 逐跳追踪，给出常见 DNS 故障的定位套路。</description><pubDate>Sat, 19 Sep 2026 03:20:00 GMT</pubDate><content:encoded>&lt;p&gt;「域名打不开」可能是 DNS 的问题，也可能不是。要快速判断，得先搞清楚一次域名解析到底经过了哪些环节，以及每个环节用什么工具验证。&lt;/p&gt;
&lt;h2&gt;解析链路的完整顺序&lt;/h2&gt;
&lt;p&gt;应用发起解析（如 curl 访问 &lt;code&gt;example.com&lt;/code&gt;）时，典型的查找顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;浏览器/应用自身缓存&lt;/strong&gt;：Chrome 在 &lt;code&gt;chrome://net-internals/#dns&lt;/code&gt; 可查看。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本机 hosts 文件&lt;/strong&gt;：&lt;code&gt;/etc/hosts&lt;/code&gt;，优先级最高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统缓存与本地解析器&lt;/strong&gt;：Linux 桌面常见 systemd-resolved（监听 127.0.0.53）；服务器上也可能直接读 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 里配置的 DNS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归解析器&lt;/strong&gt;：如运营商 DNS、&lt;code&gt;223.5.5.5&lt;/code&gt;（阿里）、&lt;code&gt;119.29.29.29&lt;/code&gt;（腾讯）、&lt;code&gt;8.8.8.8&lt;/code&gt;（Google）。它代替客户端完成剩下的全部工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;递归解析器手上没有答案时，执行&lt;strong&gt;迭代查询&lt;/strong&gt;：问根服务器（&lt;code&gt;.&lt;/code&gt;）→ 得到 &lt;code&gt;.com&lt;/code&gt; 顶级域服务器地址 → 问顶级域 → 得到 &lt;code&gt;example.com&lt;/code&gt; 权威服务器地址 → 问权威 → 拿到 A 记录。结果按 TTL 缓存后返回给客户端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;递归 vs 迭代&lt;/strong&gt;一句话区分：递归是「你给我最终答案」（客户端↔解析器），迭代是「你告诉我下一步问谁」（解析器↔各级服务器）。&lt;/p&gt;
&lt;h2&gt;dig：逐段验证每一环&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dig&lt;/code&gt;（bind-utils / dnsutils 包）是 DNS 排障的标准工具。基础用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dig example.com              # 用系统默认解析器查 A 记录
dig example.com A +short     # 只要答案：IP
dig example.com AAAA +short  # IPv6
dig example.com MX           # 邮件记录
dig example.com NS           # 权威服务器
dig @223.5.5.5 example.com   # 指定解析器，绕开本机配置
dig -x 93.184.216.34         # 反解 PTR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出里先看三个位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status: NOERROR&lt;/code&gt;：查询成功；&lt;code&gt;NXDOMAIN&lt;/code&gt; 表示域名不存在；&lt;code&gt;SERVFAIL&lt;/code&gt; 通常是解析器或权威故障。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ANSWER SECTION&lt;/code&gt;：真正的答案和 TTL。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SERVER: 127.0.0.53#53&lt;/code&gt;：这次是谁回答的——判断是否命中本地缓存的关键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;逐跳追踪&lt;/strong&gt;：&lt;code&gt;dig example.com +trace&lt;/code&gt; 从根开始把迭代过程完整走一遍，可以定位「到底是哪一级给的答案不对」。&lt;/p&gt;
&lt;h2&gt;四种典型故障的定位套路&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 别人能打开，我这台机器打不开。&lt;/strong&gt; 先 &lt;code&gt;dig example.com&lt;/code&gt; 对比 &lt;code&gt;dig @223.5.5.5 example.com&lt;/code&gt;。默认解析器答案错、公共解析器答案对 → 本机 DNS 配置或运营商劫持问题，检查 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;。两者都错 → 域名本身的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 刚改了 DNS 记录，有的生效有的不生效。&lt;/strong&gt; TTL 缓存所致。&lt;code&gt;dig example.com&lt;/code&gt; 看 ANSWER 里的 TTL 秒数和 &lt;code&gt;SERVER&lt;/code&gt; 是谁——不同递归解析器缓存过期时间不同，全球生效需要等到旧记录 TTL 全部到期。变更前把 TTL 临时调低（如 300 秒）是标准操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 域名能解析，但 IP 不对。&lt;/strong&gt; 检查 &lt;code&gt;/etc/hosts&lt;/code&gt; 是否有人写过静态映射；再确认查的记录类型对不对——把 AAAA 当 A 看会以为「解析异常」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 内网域名解析失败。&lt;/strong&gt; 内网域名通常只存在于内网 DNS。&lt;code&gt;dig @内网DNS地址 域名&lt;/code&gt; 能通而默认不行，说明 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 没指向内网 DNS，或 systemd-resolved 的分流规则没配对。&lt;/p&gt;
&lt;p&gt;:::warning
&lt;code&gt;/etc/resolv.conf&lt;/code&gt; 在很多发行版上由 NetworkManager / systemd-resolved / 云厂商 cloud-init 自动管理，手改后会被覆盖。要持久修改，应改对应管理器的配置（如 &lt;code&gt;/etc/systemd/resolved.conf&lt;/code&gt; 或 Netplan/NetworkManager 的连接配置）。
:::&lt;/p&gt;
&lt;h2&gt;记录类型最小知识&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&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;A / AAAA&lt;/td&gt;
&lt;td&gt;域名 → IPv4 / IPv6&lt;/td&gt;
&lt;td&gt;一个域名可有多条，客户端轮询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;td&gt;域名 → 另一个域名&lt;/td&gt;
&lt;td&gt;不能与其他记录共存在同名节点；根域名（如 example.com）按标准不应使用 CNAME&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;邮件投递目标&lt;/td&gt;
&lt;td&gt;指向域名，优先级数字越小越优先&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;任意文本&lt;/td&gt;
&lt;td&gt;SPF 反垃圾、域名所有权验证都靠它&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NS&lt;/td&gt;
&lt;td&gt;指定该域的权威服务器&lt;/td&gt;
&lt;td&gt;改 NS 等于把整个域的解析权移交&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PTR&lt;/td&gt;
&lt;td&gt;IP → 域名（反解）&lt;/td&gt;
&lt;td&gt;邮件服务器常校验，普通网站可不管&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;DNS 排障的核心动作只有三个：对比不同解析器的答案（&lt;code&gt;dig @&lt;/code&gt;）、看状态码和 TTL、必要时 &lt;code&gt;+trace&lt;/code&gt; 走全程。先确认「解析出来的 IP 对不对」，再去怀疑网络和服务器——顺序反了会浪费大量时间。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cloudflare.com/learning/dns/what-is-dns/&quot;&gt;Cloudflare Learning: What is DNS?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1034&quot;&gt;RFC 1034/1035（DNS 基础规范）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 1 dig&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>抓包看懂 TCP 三次握手与四次挥手</title><link>https://meow.us.kg/posts/tcp-handshake-teardown-capture/</link><guid isPermaLink="true">https://meow.us.kg/posts/tcp-handshake-teardown-capture/</guid><description>不再死记 SYN/ACK 序列：在 Linux 上用 tcpdump 抓取一次完整的 TCP 连接建立与断开，把每个标志位和状态对应到真实报文。</description><pubDate>Thu, 17 Sep 2026 12:40:00 GMT</pubDate><content:encoded>&lt;p&gt;三次握手、四次挥手几乎人人会背，但「为什么握手是三次、挥手是四次」「TIME_WAIT 到底在等谁」，离开真实报文很难真正理解。本文用 &lt;code&gt;tcpdump&lt;/code&gt; 在本地抓一次完整连接，逐包对照。&lt;/p&gt;
&lt;h2&gt;准备：抓一次 curl 请求&lt;/h2&gt;
&lt;p&gt;开两个终端。终端 A 抓包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tcpdump -i any -nn &apos;tcp port 8000 and host 127.0.0.1&apos; -c 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端 B 起一个简单的 HTTP 服务并访问它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m http.server 8000 &amp;amp;
curl -s http://127.0.0.1:8000/ &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;code&gt;-nn&lt;/code&gt; 不做域名/端口名解析，输出更快更直观。）&lt;/p&gt;
&lt;h2&gt;握手：三个包建立连接&lt;/h2&gt;
&lt;p&gt;抓到的前三行类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IP 127.0.0.1.53462 &amp;gt; 127.0.0.1.8000: Flags [S], seq 3829000, ...
IP 127.0.0.1.8000 &amp;gt; 127.0.0.1.53462: Flags [S.], seq 871000, ack 3829001, ...
IP 127.0.0.1.53462 &amp;gt; 127.0.0.1.8000: Flags [.], ack 871001, ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标志位对照：&lt;code&gt;[S]&lt;/code&gt; = SYN，&lt;code&gt;[.]&lt;/code&gt; = 纯 ACK，&lt;code&gt;[S.]&lt;/code&gt; = SYN+ACK。&lt;/p&gt;
&lt;p&gt;每个包在做的事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SYN&lt;/strong&gt;：客户端说「我想连接，我的初始序号是 3829000」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SYN+ACK&lt;/strong&gt;：服务端说「收到（ack 3829001 = 你的序号+1），我的初始序号是 871000」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACK&lt;/strong&gt;：客户端说「也收到你的了（ack 871001）」。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;为什么是三次而不是两次？&lt;/strong&gt; 因为双方都要确认对方的初始序号。第二次握手把「服务端的 SYN」和「对客户端的 ACK」合并在一个包里，所以三次就够了。只握手两次的话，服务端无法确认客户端真的收到了自己的序号——一个延迟到达的旧 SYN 就可能让服务端白白建立连接浪费资源，三次握手用第三个 ACK 杜绝了这种情况。&lt;/p&gt;
&lt;h2&gt;数据传输与挥手&lt;/h2&gt;
&lt;p&gt;中间 &lt;code&gt;Flags [P.]&lt;/code&gt; 的是带数据的包（PSH）。结束时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IP ...8000 &amp;gt; ...53462: Flags [F.], seq ..., ack ...   # 服务端 FIN
IP ...53462 &amp;gt; ...8000: Flags [.], ack ...             # 客户端 ACK
IP ...53462 &amp;gt; ...8000: Flags [F.], seq ..., ack ...   # 客户端 FIN
IP ...8000 &amp;gt; ...53462: Flags [.], ack ...             # 服务端 ACK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么挥手是四次？&lt;/strong&gt; 因为 TCP 是全双工的，两个方向要分别关闭。一端发 FIN 只表示「我没数据要发了」，另一端的 ACK 确认后，反向通道还能继续传数据——这叫半关闭（half-close）。等对端也发 FIN 并收到 ACK，连接才真正结束。HTTP/1.1 短连接里通常是服务端先发 FIN（如本例）。&lt;/p&gt;
&lt;h2&gt;状态机里的两个常客&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;TIME_WAIT&lt;/strong&gt;：主动关闭方在发完最后一个 ACK 后等待 2 倍 MSL（报文最大生存时间，Linux 默认合计约 60 秒）。它在防两件事：最后一个 ACK 丢了可以重发；旧连接的迟到报文不会混进下一条同五元组的新连接。用 &lt;code&gt;ss -tan state time-wait | wc -l&lt;/code&gt; 可以统计。高并发短连接的机器上 TIME_WAIT 堆积是正常现象，只有当它耗尽端口资源（&lt;code&gt;ss -s&lt;/code&gt; 查看）时才需要调优（如开启 &lt;code&gt;tcp_tw_reuse&lt;/code&gt; 或改用长连接），不要盲目调小。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLOSE_WAIT&lt;/strong&gt;：被动关闭方收到 FIN 回复 ACK 后进入的状态，意味着「对端已关闭，但本端应用还没调用 close()」。大量 CLOSE_WAIT 几乎总是应用代码没正确关闭连接，属于程序 bug，不是系统参数能调的。&lt;/p&gt;
&lt;h2&gt;排障场景里的标志位速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&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;/td&gt;
&lt;td&gt;只有 SYN 发出，无回应&lt;/td&gt;
&lt;td&gt;防火墙丢包、对端不存在&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连接被拒&lt;/td&gt;
&lt;td&gt;SYN 后立即收到 &lt;code&gt;[R.]&lt;/code&gt;（RST+ACK）&lt;/td&gt;
&lt;td&gt;端口没人监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连接突然断开&lt;/td&gt;
&lt;td&gt;中途出现 &lt;code&gt;[R]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;应用崩溃、中间设备超时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大量重传&lt;/td&gt;
&lt;td&gt;同一序号反复出现&lt;/td&gt;
&lt;td&gt;丢包、链路质量差&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;tcpdump&lt;/code&gt; 输出里 &lt;code&gt;ack&lt;/code&gt; 值长期不动说明对方数据没被确认，也是定位单向丢包的线索。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;握手三次 = 双方各发一次 SYN、各确认一次；挥手四次 = 全双工两个方向各关一次。抓一次包比背十遍定义更有用——下次看到 &lt;code&gt;ss -tan&lt;/code&gt; 里的状态时，你脑子里浮现的会是报文本身。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc793&quot;&gt;RFC 793: Transmission Control Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 8 tcpdump&lt;/code&gt;、&lt;code&gt;man 8 ss&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tcpdump.org/&quot;&gt;tcpdump 官方教程&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>TCP/IP 四层模型：一个数据包从网卡到应用的旅程</title><link>https://meow.us.kg/posts/tcpip-four-layer-model/</link><guid isPermaLink="true">https://meow.us.kg/posts/tcpip-four-layer-model/</guid><description>用「寄快递」的类比讲清 TCP/IP 四层模型、封装与解封装过程，以及 IP、端口、MAC 地址各自解决的寻址问题。</description><pubDate>Tue, 15 Sep 2026 01:00:00 GMT</pubDate><content:encoded>&lt;p&gt;学网络最大的障碍是概念太多、层次不清。其实 TCP/IP 协议栈的设计初衷很朴素：把「把数据从 A 进程送到 B 进程」这件复杂的事，拆成四层，每层只解决一个问题。本文跟着一个 HTTP 请求走完全程，看每层都做了什么。&lt;/p&gt;
&lt;h2&gt;四层各管一件事&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;解决的问题&lt;/th&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;/td&gt;
&lt;td&gt;数据的内容是什么&lt;/td&gt;
&lt;td&gt;请求/响应、域名&lt;/td&gt;
&lt;td&gt;HTTP、DNS、SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传输层&lt;/td&gt;
&lt;td&gt;交给本机哪个进程、要不要可靠&lt;/td&gt;
&lt;td&gt;端口、连接&lt;/td&gt;
&lt;td&gt;TCP、UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网际层&lt;/td&gt;
&lt;td&gt;送到哪台主机&lt;/td&gt;
&lt;td&gt;IP 地址、路由&lt;/td&gt;
&lt;td&gt;IP、ICMP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络接口层&lt;/td&gt;
&lt;td&gt;怎么在物理链路上传&lt;/td&gt;
&lt;td&gt;MAC 地址、帧&lt;/td&gt;
&lt;td&gt;Ethernet、Wi-Fi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;类比寄快递：你写的信是&lt;strong&gt;应用层&lt;/strong&gt;内容；信封上写「某公司前台转张三」相当于&lt;strong&gt;传输层&lt;/strong&gt;指定接收人（端口）；收件地址的城市街道是&lt;strong&gt;网际层&lt;/strong&gt;负责的跨网络寻址；而快递车在高速上一站站转运，是&lt;strong&gt;网络接口层&lt;/strong&gt;的事。&lt;/p&gt;
&lt;h2&gt;一次发送的封装过程&lt;/h2&gt;
&lt;p&gt;假设浏览器访问 &lt;code&gt;http://192.168.1.20:8080/hello&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;生成报文：&lt;code&gt;GET /hello HTTP/1.1\r\nHost: ...&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层&lt;/strong&gt;在前面加上 TCP 首部：源端口 50000（系统随机分配）、目标端口 8080、序号等。这段数据叫 TCP 段（segment）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网际层&lt;/strong&gt;再加 IP 首部：源 IP 192.168.1.10、目标 IP 192.168.1.20。这叫 IP 数据报（packet）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络接口层&lt;/strong&gt;加以太网首部和尾部：源 MAC、下一跳的 MAC。这叫帧（frame），最终变成电信号/无线电波发出去。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每层都把上一层的整体当作「数据」原样打包，自己只关心自己加的首部——这就是&lt;strong&gt;封装&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;途中的两台设备，两种视野&lt;/h2&gt;
&lt;p&gt;数据包从你的电脑到服务器，会经过交换机和路由器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;交换机&lt;/strong&gt;（二层）只看以太网首部的 MAC 地址，决定从哪个端口转发，不关心 IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由器&lt;/strong&gt;（三层）剥掉旧的以太网首部，看 IP 首部里的目标地址，查路由表选择下一跳，再套上新的以太网首部转发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 MAC 地址每过一台路由器就换一次（只负责「本段链路内的下一跳」），而 IP 地址全程不变（负责「最终目的地」）。这正回答了「有了 IP 为什么还要 MAC」：IP 是端到端的全局寻址，MAC 是逐跳的局部交付。&lt;/p&gt;
&lt;h2&gt;到达之后的解封装&lt;/h2&gt;
&lt;p&gt;服务器网卡收到帧，逐级向上：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以太网层校验帧完整，剥掉首尾，上交给 IP 层；&lt;/li&gt;
&lt;li&gt;IP 层检查目标 IP 是自己，剥掉 IP 首部，按协议号（6=TCP）交给传输层；&lt;/li&gt;
&lt;li&gt;TCP 层按&lt;strong&gt;目标端口&lt;/strong&gt;找到监听 8080 的进程，把数据放进它的接收缓冲区；&lt;/li&gt;
&lt;li&gt;应用（比如 Nginx）读出 HTTP 报文，解析并生成响应，再沿相反方向封装回去。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;发送方的「封装」和接收方的「解封装」完全对称。同层之间仿佛在直接对话——TCP 不知道自己经过了几个路由器，HTTP 也不知道下面是 TCP 还是别的什么，这正是分层带来的解耦。&lt;/p&gt;
&lt;h2&gt;三种「地址」不要再混淆&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址&lt;/th&gt;
&lt;th&gt;长度&lt;/th&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;MAC&lt;/td&gt;
&lt;td&gt;48 bit&lt;/td&gt;
&lt;td&gt;同一段链路/局域网&lt;/td&gt;
&lt;td&gt;快递员手上这一站的交接对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP&lt;/td&gt;
&lt;td&gt;32 bit（v4）/128 bit（v6）&lt;/td&gt;
&lt;td&gt;跨网络端到端&lt;/td&gt;
&lt;td&gt;快递单上的收件地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;端口&lt;/td&gt;
&lt;td&gt;16 bit&lt;/td&gt;
&lt;td&gt;主机内部区分进程&lt;/td&gt;
&lt;td&gt;收件地址里的「转交某部门」&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;完整标识一条连接的是五元组：协议 + 源 IP + 源端口 + 目标 IP + 目标端口。这就是为什么同一台服务器能同时承载成千上万个 TCP 连接而不混乱。&lt;/p&gt;
&lt;h2&gt;为什么值得学分层&lt;/h2&gt;
&lt;p&gt;分层模型不是考试知识点，而是排障的决策树：能 ping 通但打不开网页，说明三层以下通、问题在四层以上；curl 返回 403，说明 TCP 都正常，问题在应用层。后续文章讲的抓包、DNS 排查，都是把这一模型落到具体工具上。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1122&quot;&gt;RFC 1122: Requirements for Internet Hosts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cloudflare.com/learning/ddos/glossary/open-systems-interconnection-model-osi/&quot;&gt;Cloudflare Learning: OSI Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wireshark.org/docs/&quot;&gt;Wireshark 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux 软件包管理：APT 与 DNF 日常操作速查</title><link>https://meow.us.kg/posts/linux-package-manager-apt-dnf/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-package-manager-apt-dnf/</guid><description>Debian 系 APT 与 RHEL 系 DNF 的命令对照，重点讲清 update/upgrade 语义差异、查文件归属、版本锁定与卸载残留处理。</description><pubDate>Sun, 13 Sep 2026 02:10:00 GMT</pubDate><content:encoded>&lt;p&gt;Linux 发行版虽然多，包管理基本分两大家族：Debian/Ubuntu 用 APT（&lt;code&gt;.deb&lt;/code&gt; 包），RHEL/CentOS/Fedora/Rocky 用 DNF（&lt;code&gt;.rpm&lt;/code&gt; 包，YUM 的后继）。本文给出对照速查，并讲几件手册之外但天天会碰到的事。&lt;/p&gt;
&lt;h2&gt;日常操作对照表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目的&lt;/th&gt;
&lt;th&gt;APT (Debian/Ubuntu)&lt;/th&gt;
&lt;th&gt;DNF (RHEL 系)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;刷新软件源索引&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo dnf check-update&lt;/code&gt;（通常可省，自动刷新）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安装&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt install nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo dnf install nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;升级全部&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt upgrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo dnf upgrade&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;卸载&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt remove nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo dnf remove nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;卸载+清配置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt purge nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo dnf remove nginx&lt;/code&gt;（rpm 无独立配置清理，需手动删）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt search 关键词&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf search 关键词&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查看包信息&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt show nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf info nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;列出包内文件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dpkg -L nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rpm -ql nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查文件属于哪个包&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dpkg -S /usr/sbin/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rpm -qf /usr/sbin/nginx&lt;/code&gt; 或 &lt;code&gt;dnf provides */nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;清理不用的依赖&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt autoremove&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo dnf autoremove&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;最常见的误解：apt update 不升级任何东西&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;apt update&lt;/code&gt; 只是从软件源下载最新的&lt;strong&gt;包索引&lt;/strong&gt;（有哪些包、什么版本），系统上的软件一个都没动。真正的升级是 &lt;code&gt;apt upgrade&lt;/code&gt;。所以正确流程是两条一起跑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只跑第二条会用旧索引升级，经常提示「已是最新」而实际上源里有新版。DNF 系没有这个问题，&lt;code&gt;dnf upgrade&lt;/code&gt; 会自动刷新元数据。&lt;/p&gt;
&lt;p&gt;另外区分：&lt;code&gt;apt full-upgrade&lt;/code&gt;（旧称 &lt;code&gt;dist-upgrade&lt;/code&gt;）允许为升级而增删包，内核大版本、桌面环境升级时才需要；日常安全更新用 &lt;code&gt;upgrade&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h2&gt;查文件归属：排障神器&lt;/h2&gt;
&lt;p&gt;服务器上发现一个陌生文件 &lt;code&gt;/usr/local/bin/xyz&lt;/code&gt;，想知道是不是包管理器装的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Debian 系
dpkg -S /usr/local/bin/xyz

# RHEL 系
rpm -qf /usr/local/bin/xyz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果查不到，说明它是手工编译安装或脚本释放的，卸载和升级都得自己管。反过来，想找「提供 &lt;code&gt;dig&lt;/code&gt; 命令的包叫什么」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt-file search bin/dig        # 需先装 apt-file
dnf provides &apos;*/bin/dig&apos;       # DNF 原生支持路径通配
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案分别是 &lt;code&gt;dnsutils&lt;/code&gt;（新版为 &lt;code&gt;bind9-dnsutils&lt;/code&gt;）和 &lt;code&gt;bind-utils&lt;/code&gt;——包名和命令名不一致是常态，查一下比猜快。&lt;/p&gt;
&lt;h2&gt;版本锁定与暂缓升级&lt;/h2&gt;
&lt;p&gt;数据库这类对版本敏感的服务，你可能不希望它被 &lt;code&gt;upgrade&lt;/code&gt; 顺手升级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# APT
sudo apt-mark hold mysql-server     # 锁定
sudo apt-mark unhold mysql-server   # 解锁
apt-mark showhold                   # 查看锁定列表

# DNF（需 versionlock 插件）
sudo dnf install python3-dnf-plugin-versionlock
sudo dnf versionlock add mysql-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;锁定只是「不自动升」，手动 &lt;code&gt;install 包名=版本号&lt;/code&gt; 仍可指定版本。&lt;/p&gt;
&lt;h2&gt;卸载残留与自动依赖&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;apt remove&lt;/code&gt; 会保留配置文件（方便重装后找回配置），要彻底清理用 &lt;code&gt;purge&lt;/code&gt;。装包时被顺带拉进来的依赖，卸载主包后不会自动消失，定期 &lt;code&gt;sudo apt autoremove --purge&lt;/code&gt; 保持系统干净。&lt;/p&gt;
&lt;p&gt;DNF 的 &lt;code&gt;remove&lt;/code&gt; 默认行为接近 &lt;code&gt;remove + autoremove&lt;/code&gt; 的组合（会提示移除孤儿依赖），但 &lt;code&gt;/etc&lt;/code&gt; 下手工改过的配置文件不会动，需要时手动删。&lt;/p&gt;
&lt;h2&gt;几点安全建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;生产环境别用 &lt;code&gt;-y&lt;/code&gt; 盲升&lt;/strong&gt;：先 &lt;code&gt;apt list --upgradable&lt;/code&gt; 或 &lt;code&gt;dnf check-update&lt;/code&gt; 看看要动哪些包，数据库、内核类单独安排窗口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别混用第三方源&lt;/strong&gt;：一个 &lt;code&gt;.deb&lt;/code&gt; 包强行装在不匹配的发行版版本上，依赖地狱就是这么来的。优先用官方源，第三方源确认其支持你的发行版代号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核升级要重启才生效&lt;/strong&gt;：&lt;code&gt;needrestart&lt;/code&gt;（Debian）或 &lt;code&gt;dnf needs-restarting -r&lt;/code&gt; 可以告诉你哪些服务或系统本身需要重启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;个人软件装到用户目录&lt;/strong&gt;：&lt;code&gt;/opt&lt;/code&gt;、&lt;code&gt;~/bin&lt;/code&gt;、或 Flatpak/AppImage，别把手工编译的东西塞进 &lt;code&gt;/usr/bin&lt;/code&gt;，与包管理器抢地盘。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.debian.org/doc/manuals/apt-guide/&quot;&gt;Debian APT 用户指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dnf.readthedocs.io/&quot;&gt;DNF 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 8 apt&lt;/code&gt;、&lt;code&gt;man 8 dnf&lt;/code&gt;、&lt;code&gt;man 1 dpkg&lt;/code&gt;、&lt;code&gt;man 8 rpm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>systemd 服务管理与 journalctl 日志排查入门</title><link>https://meow.us.kg/posts/systemd-unit-journalctl-guide/</link><guid isPermaLink="true">https://meow.us.kg/posts/systemd-unit-journalctl-guide/</guid><description>看懂 unit 文件的三个段落，用 systemctl 管理服务全生命周期，再用 journalctl 按服务、时间、优先级过滤日志。</description><pubDate>Sat, 12 Sep 2026 13:30:00 GMT</pubDate><content:encoded>&lt;p&gt;现代主流发行版（Ubuntu 16.04+、Debian 8+、RHEL/CentOS 7+、Fedora）都用 systemd 做初始化系统。日常打交道最多的是两件事：用 &lt;code&gt;systemctl&lt;/code&gt; 管服务，用 &lt;code&gt;journalctl&lt;/code&gt; 看日志。本文再进一步：写一个自己的 unit 文件，把脚本变成受管服务。&lt;/p&gt;
&lt;h2&gt;systemctl 日常操作速查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;systemctl status nginx       # 状态 + 最近几行日志
sudo systemctl start nginx   # 启动
sudo systemctl stop nginx    # 停止
sudo systemctl restart nginx # 重启
sudo systemctl reload nginx  # 重载配置（进程不中断，需服务支持）
sudo systemctl enable nginx  # 开机自启（不改当前状态）
sudo systemctl enable --now nginx  # 自启 + 立即启动
sudo systemctl disable --now nginx # 取消自启并停止
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;status&lt;/code&gt; 输出里最值钱的两行：&lt;code&gt;Active: active (running) since ...&lt;/code&gt; 和 &lt;code&gt;Main PID&lt;/code&gt;。如果看到 &lt;code&gt;failed&lt;/code&gt;，先别急着 restart——失败原因往往就在 status 显示的最近日志里。&lt;/p&gt;
&lt;p&gt;几个容易混淆的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enable&lt;/code&gt; 只管开机自启，不影响当前运行状态；&lt;code&gt;--now&lt;/code&gt; 把两件事一起做。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reload&lt;/code&gt; 依赖服务自己支持（ExecReload），不支持时会报错，这时只能用 &lt;code&gt;restart&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl is-active nginx&lt;/code&gt;、&lt;code&gt;is-enabled&lt;/code&gt;、&lt;code&gt;is-failed&lt;/code&gt; 返回简单状态，适合写脚本判断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;看懂 unit 文件&lt;/h2&gt;
&lt;p&gt;unit 文件通常在 &lt;code&gt;/etc/systemd/system/&lt;/code&gt;（自定义）和 &lt;code&gt;/usr/lib/systemd/system/&lt;/code&gt;（软件包安装）下。&lt;code&gt;systemctl cat nginx&lt;/code&gt; 能直接查看。&lt;/p&gt;
&lt;p&gt;一个最小可用的自定义服务，把 &lt;code&gt;/opt/myapp/run.sh&lt;/code&gt; 变成服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=My App
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/run.sh
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个段落的分工：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[Unit]&lt;/code&gt;：元信息和启动顺序。&lt;code&gt;After=network-online.target&lt;/code&gt; 保证网络就绪后再启动——依赖网络的服务只写 &lt;code&gt;network.target&lt;/code&gt; 是不够的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[Service]&lt;/code&gt;：怎么跑。&lt;code&gt;Type=simple&lt;/code&gt; 表示 ExecStart 的进程就是主进程；&lt;code&gt;Restart=on-failure&lt;/code&gt; 异常退出时自动拉起；&lt;code&gt;User=&lt;/code&gt; 让服务以非 root 身份运行，是安全基线。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[Install]&lt;/code&gt;：何时被启用。&lt;code&gt;WantedBy=multi-user.target&lt;/code&gt; 表示 &lt;code&gt;enable&lt;/code&gt; 后随正常多用户模式启动。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改完 unit 文件必须执行 &lt;code&gt;sudo systemctl daemon-reload&lt;/code&gt; 让 systemd 重新读取，然后 &lt;code&gt;sudo systemctl enable --now myapp&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;:::tip
想让用户自己的脚本免 root 开机运行，可以用 &lt;code&gt;loginctl enable-linger 用户名&lt;/code&gt; 加 &lt;code&gt;systemctl --user&lt;/code&gt; 体系，比往系统服务里塞更干净。
:::&lt;/p&gt;
&lt;h2&gt;journalctl：结构化日志的正确用法&lt;/h2&gt;
&lt;p&gt;systemd 把服务标准输出、错误输出和内核日志统一收进 journal。基本用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -u myapp              # 某个服务的全部日志
journalctl -u myapp -n 200       # 最近 200 行
journalctl -u myapp -f           # 实时跟踪（类似 tail -f）
journalctl -u myapp --since &quot;1 hour ago&quot;
journalctl -u myapp --since &quot;2026-09-12 20:00&quot; --until &quot;2026-09-12 21:00&quot;
journalctl -p err                # 只看 err 及以上优先级（所有服务）
journalctl -k                    # 内核日志（等价 dmesg）
journalctl -b                    # 本次开机以来的日志
journalctl --disk-usage          # 日志占了多少磁盘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个实用技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-o json-pretty&lt;/code&gt; 输出完整结构化字段，能看到 &lt;code&gt;_PID&lt;/code&gt;、&lt;code&gt;_EXE&lt;/code&gt; 等元数据，写日志收集脚本时用得上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-x&lt;/code&gt; 给部分日志附加解释文字（如 OOM、core dump），排障时值得一看。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;排查套路：服务起不来怎么办&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;systemctl status myapp&lt;/code&gt;：看退出码和最后几行输出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;journalctl -u myapp -n 100 --no-pager&lt;/code&gt;：看完整错误。&lt;/li&gt;
&lt;li&gt;常见根因对号入座：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status=203/EXEC&lt;/code&gt;：ExecStart 路径错误或文件不可执行；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status=217/USER&lt;/code&gt;：&lt;code&gt;User=&lt;/code&gt; 指定的用户不存在；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;code=exited, status=1&lt;/code&gt;：程序自己报错，去读程序日志；&lt;/li&gt;
&lt;li&gt;权限问题：检查 WorkingDirectory、日志目录对 &lt;code&gt;User=&lt;/code&gt; 是否可写。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修复后 &lt;code&gt;daemon-reload&lt;/code&gt;（改了 unit 才需要）→ &lt;code&gt;reset-failed&lt;/code&gt; → &lt;code&gt;start&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;journal 默认持久化程度因发行版而异：&lt;code&gt;/var/log/journal/&lt;/code&gt; 存在才持久，否则重启即丢。生产机器建议确认 &lt;code&gt;Storage=persistent&lt;/code&gt;（&lt;code&gt;/etc/systemd/journald.conf&lt;/code&gt;），并用 &lt;code&gt;SystemMaxUse=&lt;/code&gt; 限制体积。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://systemd.io/&quot;&gt;systemd 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 5 systemd.service&lt;/code&gt;、&lt;code&gt;man 1 journalctl&lt;/code&gt;、&lt;code&gt;man 5 journald.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html&quot;&gt;freedesktop: systemd Service 单元说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>进程管理实战：ps、top 定位问题与信号的正确使用</title><link>https://meow.us.kg/posts/linux-process-management-ps-top-kill/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-process-management-ps-top-kill/</guid><description>服务器变慢时先别急着重启：用 ps 和 top 找到异常进程，看懂 STAT 状态列，按 TERM→KILL 的顺序优雅处理。</description><pubDate>Thu, 10 Sep 2026 00:50:00 GMT</pubDate><content:encoded>&lt;p&gt;「服务器好卡」是每个 Linux 用户都会遇到的报案。处理流程只有三步：找到是谁、看懂它在干什么、选择合适的处置方式。本文把 &lt;code&gt;ps&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt;、信号这三件事按这个流程组织起来。&lt;/p&gt;
&lt;h2&gt;先定位：找到可疑进程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ps -eo pid,ppid,user,stat,etime,%cpu,%mem,args --sort=-%cpu | head
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令按 CPU 占用倒序列出前几个进程。各列含义：PPID 是父进程，STAT 是状态，ETIME 是运行时长，ARGS 是完整命令行。&lt;/p&gt;
&lt;p&gt;如果已经知道名字，&lt;code&gt;pgrep -af nginx&lt;/code&gt; 直接列出 PID 和命令行；&lt;code&gt;pgrep -x&lt;/code&gt; 要求进程名精确匹配，避免误伤。要按内存排序就把 &lt;code&gt;--sort=-%cpu&lt;/code&gt; 换成 &lt;code&gt;--sort=-%mem&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;再看懂：STAT 状态列&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&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;R&lt;/td&gt;
&lt;td&gt;运行中或排队等 CPU&lt;/td&gt;
&lt;td&gt;多核机器上大量 R 说明 CPU 饱和&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;可中断睡眠（在等事件）&lt;/td&gt;
&lt;td&gt;正常服务大多时间在此状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;不可中断睡眠（等 I/O）&lt;/td&gt;
&lt;td&gt;持续 D 状态指向磁盘/NFS 故障，查 &lt;code&gt;dmesg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;被暂停&lt;/td&gt;
&lt;td&gt;谁按了 Ctrl+Z 或发了 STOP？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z&lt;/td&gt;
&lt;td&gt;僵尸&lt;/td&gt;
&lt;td&gt;父进程没回收，杀它没用，要找父进程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;僵尸进程本身不耗资源，只是进程表里的一个条目，正确做法是重启它的父进程。&lt;code&gt;ps -eo ppid,stat,args | awk &apos;$2 ~ /Z/&apos;&lt;/code&gt; 可以快速找到僵尸的父进程。&lt;/p&gt;
&lt;h2&gt;动态观察：top 的正确打开方式&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;top&lt;/code&gt; 打开后几个按键显著提高效率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;：展开每个 CPU 核心的使用率，判断是单线程打满还是整体饱和。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M&lt;/code&gt;：按内存排序，&lt;code&gt;P&lt;/code&gt; 回到按 CPU 排序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt;：显示完整命令行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt;：在 top 里直接发信号（输入 PID 和信号编号）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;头部 &lt;code&gt;load average&lt;/code&gt; 三个值是 1/5/15 分钟的平均负载。经验法则：负载长期超过 CPU 核心数，说明有进程在排队。但负载高不等于 CPU 高——大量 D 状态进程等磁盘也会推高负载，需要结合 &lt;code&gt;%wa&lt;/code&gt;（iowait）一起看。&lt;/p&gt;
&lt;h2&gt;处置：信号的正确顺序&lt;/h2&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;th&gt;行为&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TERM&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;请求退出，程序可以捕获并做清理（关闭连接、写盘）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HUP&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;挂起；许多守护进程约定为重载配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KILL&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;内核直接终止，不给程序任何清理机会&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;正确顺序是&lt;strong&gt;先 TERM，等几秒，再考虑 KILL&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kill 12345            # 默认发 TERM
sleep 5
kill -0 12345 2&amp;gt;/dev/null &amp;amp;&amp;amp; kill -9 12345   # 还活着才补 KILL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kill -9&lt;/code&gt; 的代价：事务没写完、锁没释放、临时文件残留。对数据库类进程尤其危险。&lt;code&gt;kill -0&lt;/code&gt; 不真的发信号，只探测进程是否存在。&lt;/p&gt;
&lt;p&gt;别用 &lt;code&gt;killall 名字&lt;/code&gt; 或 &lt;code&gt;pkill 模式&lt;/code&gt; 模糊批量杀，测试环境和生产环境混用的机器上很容易误伤。&lt;/p&gt;
&lt;h2&gt;别忘了 systemd 管的服务&lt;/h2&gt;
&lt;p&gt;如果进程是由 systemd 管理的服务，直接 kill 会被 &lt;code&gt;Restart=&lt;/code&gt; 策略自动拉起来，越杀越多。正确入口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl status myapp        # 看状态和最近日志
sudo systemctl stop myapp     # 停止
journalctl -u myapp -n 100    # 看日志
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断进程归谁管：&lt;code&gt;systemctl status &amp;lt;PID&amp;gt;&lt;/code&gt; 直接传 PID 也行，会显示它属于哪个 unit。&lt;/p&gt;
&lt;h2&gt;一个完整的排查实例&lt;/h2&gt;
&lt;p&gt;症状：网站响应慢。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;top&lt;/code&gt; 发现 load 12（8 核），按 &lt;code&gt;1&lt;/code&gt; 看 CPU 有空闲，但 &lt;code&gt;%wa&lt;/code&gt; 高达 40%——瓶颈在 I/O。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps -eo pid,stat,args | awk &apos;$2 ~ /D/&apos;&lt;/code&gt; 找到一批 D 状态的备份脚本进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dmesg | tail&lt;/code&gt; 看到磁盘 I/O error——是硬件/存储层问题，不是业务进程的问题。&lt;/li&gt;
&lt;li&gt;处置：通知存储运维，而不是 kill 业务进程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;工具只是入口，「先形成假设、再找证据」才是排障的核心。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;man 1 ps&lt;/code&gt;、&lt;code&gt;man 1 top&lt;/code&gt;、&lt;code&gt;man 7 signal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gitlab.com/procps-ng/procps&quot;&gt;procps-ng 项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.brendangregg.com/linuxperf.html&quot;&gt; Brendan Gregg 的 Linux 性能分析资料&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>看懂 Linux 权限：rwx、umask 与团队共享目录配置</title><link>https://meow.us.kg/posts/linux-permissions-rwx-umask-acl/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-permissions-rwx-umask-acl/</guid><description>把 rwx、属主属组、umask、SGID 与默认 ACL 串成一条线，用一个团队共享目录的完整配置案例讲清权限设计的实际做法。</description><pubDate>Tue, 08 Sep 2026 11:40:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Permission denied&lt;/code&gt; 是 Linux 新手最高频的报错。排查它需要的不是背 chmod 数字，而是一张决策图：进程以谁的身份运行？路径每一级目录能不能进？文件本身让不让读写？本文先把权限模型讲清楚，再用一个共享目录案例把 umask、SGID、ACL 串起来。&lt;/p&gt;
&lt;h2&gt;权限三元组&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ls -l&lt;/code&gt; 输出的 &lt;code&gt;-rw-r--r--&lt;/code&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;th&gt;对目录&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;r&lt;/td&gt;
&lt;td&gt;读内容&lt;/td&gt;
&lt;td&gt;列出文件名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;w&lt;/td&gt;
&lt;td&gt;改内容&lt;/td&gt;
&lt;td&gt;增删改目录项（需配合 x）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;作为程序执行&lt;/td&gt;
&lt;td&gt;进入目录、访问其中已知名字的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最容易误判的是&lt;strong&gt;目录的 x 权限&lt;/strong&gt;：文件本身可读，但路径上任何一级目录没有 x，照样打不开。用 &lt;code&gt;namei -l /srv/app/data/config.yml&lt;/code&gt; 可以把路径逐级权限一次列出来，是排查权限问题的首选命令。&lt;/p&gt;
&lt;p&gt;另一个反直觉点：删除文件看的是&lt;strong&gt;父目录&lt;/strong&gt;的 w+x，与文件本身是否可写无关。这就是为什么 &lt;code&gt;/tmp&lt;/code&gt; 里自己的文件能被自己删掉，而 sticky bit（&lt;code&gt;t&lt;/code&gt;）又阻止你删别人的文件。&lt;/p&gt;
&lt;h2&gt;数字与归属&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;r=4, w=2, x=1&lt;/code&gt; 按位相加：&lt;code&gt;644&lt;/code&gt; = 属主读写、组内只读、其他人只读；&lt;code&gt;755&lt;/code&gt; = 属主全权、其他人可读可进入。修改归属需要管理员权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod 640 report.txt        # 或 chmod u=rw,g=r,o= report.txt
chown alice:project report.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;chmod -R 777&lt;/code&gt; 不是权限问题的解药，它只是把门拆了。先 &lt;code&gt;namei -l&lt;/code&gt; 找到真正缺失的权限位。
:::&lt;/p&gt;
&lt;h2&gt;umask：新文件的默认权限&lt;/h2&gt;
&lt;p&gt;程序创建文件时会声明一个期望权限（普通文件通常 666，目录 777），umask 负责&lt;strong&gt;屏蔽&lt;/strong&gt;其中部分位：umask 为 022 时，新文件是 644、新目录是 755。注意它是按位掩码，不是算术减法——umask 011 作用于 666 得到的是 666 而非 655（奇数位本来就没有执行位可减）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;umask&lt;/code&gt; 命令查看当前值。改 umask 只影响之后创建的文件，已有文件不受影响。&lt;/p&gt;
&lt;h2&gt;实战：配置团队共享目录&lt;/h2&gt;
&lt;p&gt;目标：&lt;code&gt;project&lt;/code&gt; 组的成员都能在 &lt;code&gt;/srv/project&lt;/code&gt; 里协作，新建文件自动继承组归属，组内成员默认可互相修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo groupadd -f project
sudo usermod -aG project alice      # 把成员加入组（重新登录生效）
sudo mkdir -p /srv/project
sudo chown root:project /srv/project
sudo chmod 2775 /srv/project
sudo setfacl -d -m g:project:rwx /srv/project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐条解释：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;2775&lt;/code&gt; 中的 2 是 SGID 位&lt;/strong&gt;：目录带 SGID 后，其中新建的文件和子目录自动继承 &lt;code&gt;project&lt;/code&gt; 组，而不是创建者的私人主组——这是共享目录的关键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认 ACL（&lt;code&gt;-d&lt;/code&gt;）&lt;/strong&gt;：仅靠 SGID 只解决「组归属」，不解决「组内可写」——如果成员的 umask 是 022，新文件的组权限仍没有 w。默认 ACL 强制组内新建对象带 rwx，绕开各成员 umask 不一致的问题。&lt;/li&gt;
&lt;li&gt;成员加组后&lt;strong&gt;需要重新登录&lt;/strong&gt;，已有终端会话里的组列表不会自动刷新。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;验证：两名成员各自 &lt;code&gt;touch&lt;/code&gt; 一个文件，互相 &lt;code&gt;getfacl&lt;/code&gt; 确认 effective 权限含 &lt;code&gt;w&lt;/code&gt;，再交叉修改一次。&lt;/p&gt;
&lt;h2&gt;权限排查决策表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;症状&lt;/th&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;/td&gt;
&lt;td&gt;&lt;code&gt;namei -l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;某级父目录缺 x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能读不能改&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ls -l&lt;/code&gt;、&lt;code&gt;getfacl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;缺 w，或 ACL mask 收窄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能改不能删&lt;/td&gt;
&lt;td&gt;父目录权限&lt;/td&gt;
&lt;td&gt;父目录缺 w+x，或有 sticky bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加组后还是 denied&lt;/td&gt;
&lt;td&gt;重开终端后 &lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;会话未刷新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权限全对仍拒绝&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getenforce&lt;/code&gt;、&lt;code&gt;dmesg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SELinux/AppArmor 策略&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;sudo 的最小授权原则&lt;/h2&gt;
&lt;p&gt;能用到组权限解决的共享问题，就不要发 sudo。确实需要授权时，用 &lt;code&gt;visudo -f /etc/sudoers.d/team&lt;/code&gt; 写只覆盖必要命令的规则，例如只允许重启指定服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%project ALL=(root) /usr/bin/systemctl restart myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;允许 &lt;code&gt;sudo vim&lt;/code&gt;、&lt;code&gt;sudo bash&lt;/code&gt; 或执行用户可写的脚本，等于把整个 root 交出去。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/coreutils/manual/html_node/File-permissions.html&quot;&gt;GNU Coreutils：File permissions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 5 acl&lt;/code&gt;、&lt;code&gt;man 1 namei&lt;/code&gt;、&lt;code&gt;man 5 sudoers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>写出健壮的 Bash 脚本：选项、陷阱与防御式写法</title><link>https://meow.us.kg/posts/bash-scripting-robust-practices/</link><guid isPermaLink="true">https://meow.us.kg/posts/bash-scripting-robust-practices/</guid><description>从 set -euo pipefail 讲起，覆盖变量引用、退出码判断、日志输出与定时任务环境差异，给运维脚本加上防错骨架。</description><pubDate>Sun, 06 Sep 2026 02:20:00 GMT</pubDate><content:encoded>&lt;p&gt;在终端里能跑的命令，写进脚本就出事，是新手写 Bash 的日常。大多数事故来自几个可预期的陷阱：变量没加引号、命令失败没被察觉、空变量拼出危险路径、定时任务环境和交互终端不一样。本文给出一套防御式写法骨架。&lt;/p&gt;
&lt;h2&gt;起手式：set -euo pipefail&lt;/h2&gt;
&lt;p&gt;脚本开头建议固定写上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个选项各自的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-e&lt;/code&gt;（errexit）：任何命令返回非零退出码，脚本立即退出。没有它，备份脚本里「进入目录失败」之后，&lt;code&gt;rm&lt;/code&gt; 照样会在错误的位置执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u&lt;/code&gt;（nounset）：使用未定义的变量直接报错。把 &lt;code&gt;rm -rf &quot;$bakup_dir/&quot;&lt;/code&gt;（手滑拼错变量名，展开为空）这类事故扼杀在摇篮。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pipefail&lt;/code&gt;：管道中任一环节失败，整条管道算失败。&lt;code&gt;grep error app.log | wc -l&lt;/code&gt; 在日志文件不存在时默认仍返回 0 并输出 0，开了 pipefail 才会暴露 &lt;code&gt;grep&lt;/code&gt; 的报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意 &lt;code&gt;-e&lt;/code&gt; 有些反直觉的例外：&lt;code&gt;if&lt;/code&gt; 条件里、&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;、&lt;code&gt;||&lt;/code&gt; 左侧的命令失败不会触发退出，这是特意保留的判断能力。&lt;/p&gt;
&lt;h2&gt;变量：永远加引号&lt;/h2&gt;
&lt;p&gt;单词分割是 Bash 事故第一大来源。文件名含空格时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file=&quot;daily report.txt&quot;
cat $file        # 报错：被拆成 &quot;daily&quot; 和 &quot;report.txt&quot; 两个参数
cat &quot;$file&quot;      # 正确：作为一个参数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则只有一条：&lt;strong&gt;变量展开一律加双引号&lt;/strong&gt;，除非你明确需要分词（很少见）。相关约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令替换用 &lt;code&gt;$( )&lt;/code&gt; 而不是反引号，可嵌套、可读性好。&lt;/li&gt;
&lt;li&gt;只读变量用 &lt;code&gt;readonly BACKUP_DIR=/data/backup&lt;/code&gt;，脚本开头的配置项建议大写。&lt;/li&gt;
&lt;li&gt;拼路径时确认变量非空：&lt;code&gt;[ -n &quot;$dir&quot; ] || { echo &quot;dir 为空&quot; &amp;gt;&amp;amp;2; exit 1; }&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;条件与退出码&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;[ ]&lt;/code&gt; 是命令不是语法糖，常见判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /etc/nginx/nginx.conf ]   # 文件存在
[ -d /var/log ]                # 目录存在
[ -z &quot;$var&quot; ]                  # 字符串为空
[ &quot;$code&quot; -eq 200 ]            # 数值比较用 -eq/-ne/-gt/-lt
[ &quot;$ans&quot; = &quot;y&quot; ]               # 字符串比较用 =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断上一条命令成败，直接把它放进 &lt;code&gt;if&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if systemctl is-active --quiet nginx; then
    echo &quot;nginx 运行中&quot;
else
    echo &quot;nginx 已停止，尝试启动&quot; &amp;gt;&amp;amp;2
    sudo systemctl start nginx
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要先执行命令再判断 &lt;code&gt;$?&lt;/code&gt;，那会让 &lt;code&gt;set -e&lt;/code&gt; 提前把脚本杀掉。&lt;/p&gt;
&lt;h2&gt;日志与错误输出&lt;/h2&gt;
&lt;p&gt;脚本的输出分两类：结果是 stdout，诊断信息走 stderr（&lt;code&gt;&amp;gt;&amp;amp;2&lt;/code&gt;）。这样 &lt;code&gt;脚本 &amp;gt; result.txt&lt;/code&gt; 不会把日志混进数据。一个简单可用的日志函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;log() { printf &apos;[%s] %s\n&apos; &quot;$(date &apos;+%F %T&apos;)&quot; &quot;$*&quot; &amp;gt;&amp;amp;2; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环里处理文件列表，用 &lt;code&gt;find -print0&lt;/code&gt; 配合 &lt;code&gt;read -d &apos;&apos;&lt;/code&gt;，避免文件名含空格、换行时出错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find /data -name &apos;*.tmp&apos; -print0 | while IFS= read -r -d &apos;&apos; f; do
    rm -- &quot;$f&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;定时任务环境差异&lt;/h2&gt;
&lt;p&gt;「手动跑没问题，cron 里就失败」几乎总是环境差异：cron 的 &lt;code&gt;PATH&lt;/code&gt; 很短、没有交互终端、工作目录不同。对策：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;脚本里调用外部命令写绝对路径，或在开头显式 &lt;code&gt;export PATH=/usr/local/bin:/usr/bin:/bin&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;一律使用绝对路径引用文件；&lt;/li&gt;
&lt;li&gt;cron 条目里追加 &lt;code&gt;&amp;gt;&amp;gt; /var/log/myjob.log 2&amp;gt;&amp;amp;1&lt;/code&gt;，留好现场；&lt;/li&gt;
&lt;li&gt;需要交互输入的命令（如未配置免密的 ssh）不要放进 cron。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;提交前的最后一步：shellcheck&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.shellcheck.net/&quot;&gt;ShellCheck&lt;/a&gt; 是 Bash 静态检查工具，能抓出未加引号、弃用语法、误判退出码等绝大多数低级错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shellcheck backup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把它当成 Bash 的编译器警告来对待，告警清零再上线。配合上面这套骨架，脚本的可靠性会提升一个量级。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/&quot;&gt;Bash 参考手册（GNU）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.shellcheck.net/&quot;&gt;ShellCheck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://google.github.io/styleguide/shellguide.html&quot;&gt;Google Shell 风格指南&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vim 模式编辑入门：从手足无措到肌肉记忆</title><link>https://meow.us.kg/posts/vim-modal-editing-guide/</link><guid isPermaLink="true">https://meow.us.kg/posts/vim-modal-editing-guide/</guid><description>讲清 Vim 的模式设计思想，给出服务器场景下的最小可用命令集、常用组合技和一份不超过 20 行的实用 vimrc。</description><pubDate>Fri, 04 Sep 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;几乎每个 Linux 用户都经历过「误闯进 Vim 然后不知道怎么退出」的时刻。会一点 Vim 的实际价值很朴素：SSH 到服务器上改配置时，它是几乎必定存在的编辑器。本文目标是让你在服务器上高效完成「打开 → 定位 → 修改 → 保存退出」，而不是成为 Vim 玩家。&lt;/p&gt;
&lt;h2&gt;为什么是「模式」&lt;/h2&gt;
&lt;p&gt;普通编辑器里，键盘的每个键都用来输入字符；Vim 把键盘复用了：&lt;strong&gt;普通模式&lt;/strong&gt;下按键是命令（删除、跳转、复制），&lt;strong&gt;插入模式&lt;/strong&gt;下按键才是输入文本，&lt;strong&gt;可视模式&lt;/strong&gt;用来选区，&lt;strong&gt;命令行模式&lt;/strong&gt;用来执行保存、查找等动作。&lt;/p&gt;
&lt;p&gt;刚接触时最大的混乱来源是「不知道自己处于哪个模式」。两条救命规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;迷路了就按 &lt;code&gt;Esc&lt;/code&gt;，连按几下一定回到普通模式；&lt;/li&gt;
&lt;li&gt;普通模式下输入 &lt;code&gt;:q!&lt;/code&gt; 回车，不保存强制退出——这是你误进 Vim 的逃生门。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最小可用命令集&lt;/h2&gt;
&lt;p&gt;服务器改配置，掌握下面这些就足够应付 90% 的场景：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进入与退出&lt;/strong&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;/td&gt;
&lt;td&gt;&lt;code&gt;vim nginx.conf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进入插入模式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i&lt;/code&gt;（光标前）、&lt;code&gt;a&lt;/code&gt;（光标后）、&lt;code&gt;o&lt;/code&gt;（下一行新建）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;回到普通模式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保存退出&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:wq&lt;/code&gt; 或 &lt;code&gt;ZZ&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不保存退出&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:q!&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;移动与定位&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h j k l&lt;/code&gt;：左下上右（先用方向键也行，别纠结）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gg&lt;/code&gt; 到文件头，&lt;code&gt;G&lt;/code&gt; 到文件尾，&lt;code&gt;42G&lt;/code&gt; 跳到第 42 行——看报错里的行号时直接用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/关键词&lt;/code&gt; 回车搜索，&lt;code&gt;n&lt;/code&gt; 下一个，&lt;code&gt;N&lt;/code&gt; 上一个。找 &lt;code&gt;server_name&lt;/code&gt;、&lt;code&gt;listen&lt;/code&gt; 这类配置项极快。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; 行首，&lt;code&gt;$&lt;/code&gt; 行尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;编辑&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dd&lt;/code&gt; 删整行，&lt;code&gt;yy&lt;/code&gt; 复制整行，&lt;code&gt;p&lt;/code&gt; 粘贴。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; 删光标处字符，&lt;code&gt;u&lt;/code&gt; 撤销，&lt;code&gt;Ctrl+r&lt;/code&gt; 重做。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cw&lt;/code&gt;：删一个词并进入插入模式（change word），改参数值最常用。&lt;/li&gt;
&lt;li&gt;数字前缀：&lt;code&gt;5dd&lt;/code&gt; 删 5 行，&lt;code&gt;3yy&lt;/code&gt; 复制 3 行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;典型改配置流程：&lt;code&gt;:set nu&lt;/code&gt; 显示行号 → &lt;code&gt;42G&lt;/code&gt; 跳到出错行 → &lt;code&gt;cw&lt;/code&gt; 改掉参数 → &lt;code&gt;Esc&lt;/code&gt; → &lt;code&gt;:wq&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;三个提升幸福感的组合技&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注释/取消注释一段配置。&lt;/strong&gt; &lt;code&gt;Ctrl+v&lt;/code&gt; 进入块可视模式，&lt;code&gt;j&lt;/code&gt; 选中若干行，&lt;code&gt;I&lt;/code&gt;（大写 i）输入 &lt;code&gt;#&lt;/code&gt; 后按 &lt;code&gt;Esc&lt;/code&gt;，选区每行行首都插入 &lt;code&gt;#&lt;/code&gt;。删除注释则选中后按 &lt;code&gt;x&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;全文替换。&lt;/strong&gt; &lt;code&gt;:%s/旧值/新值/g&lt;/code&gt; 回车，加 &lt;code&gt;c&lt;/code&gt; 变确认模式（&lt;code&gt;:%s/8080/80/gc&lt;/code&gt;），逐个确认避免误伤。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不退出 Vim 执行 shell 命令。&lt;/strong&gt; &lt;code&gt;:!nginx -t&lt;/code&gt; 测试配置，看完输出回车回到编辑——改完 Nginx 配置立即验证，不用退出编辑器。&lt;/p&gt;
&lt;h2&gt;一份克制的 vimrc&lt;/h2&gt;
&lt;p&gt;服务器上不需要花哨插件，以下配置写入 &lt;code&gt;~/.vimrc&lt;/code&gt; 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set number          &quot; 显示行号
set relativenumber  &quot; 相对行号，配合 5j 这类跳转
syntax on           &quot; 语法高亮
set tabstop=4       &quot; Tab 显示宽度
set shiftwidth=4    &quot; 缩进宽度
set expandtab       &quot; Tab 转空格
set ignorecase      &quot; 搜索忽略大小写
set smartcase       &quot; 含大写时恢复精确匹配
set incsearch       &quot; 边输入边高亮
set hlsearch        &quot; 高亮搜索结果（:noh 取消）
set cursorline      &quot; 高亮当前行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保留原有缩进风格的注意事项：改 Python 或 YAML 时 &lt;code&gt;expandtab&lt;/code&gt; 很重要，混用 Tab 和空格是常见的语法错误来源。&lt;/p&gt;
&lt;h2&gt;练习建议与常见疑问&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;要不要背命令表？&lt;/strong&gt; 不用。先用 &lt;code&gt;i&lt;/code&gt;、&lt;code&gt;Esc&lt;/code&gt;、&lt;code&gt;:wq&lt;/code&gt;、&lt;code&gt;dd&lt;/code&gt;、&lt;code&gt;/关键词&lt;/code&gt; 活下来，用得多了自然扩展。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vim 教程哪里练？&lt;/strong&gt; 终端输入 &lt;code&gt;vimtutor&lt;/code&gt;，官方交互教程，约半小时走完一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vim 还是 nvim？&lt;/strong&gt; 服务器上有什么用什么，本文命令两者通用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编辑大文件卡？&lt;/strong&gt; Vim 打开几百 MB 日志没问题，但语法高亮会变慢，可临时 &lt;code&gt;:syntax off&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;学会 Vim 的意义不在于炫技，而在于任何一台干净的服务器上，你都有一个可靠的编辑能力兜底。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vimtutor&lt;/code&gt;：Vim 自带交互教程&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.vim.org/docs.php&quot;&gt;Vim 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://neovim.io/&quot;&gt;Neovim 项目&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>日志排查利器：grep、sed、awk 文本处理实战</title><link>https://meow.us.kg/posts/linux-text-processing-grep-sed-awk/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-text-processing-grep-sed-awk/</guid><description>以排查 Nginx 访问日志为主线，串起 grep 过滤、sed 替换、awk 字段统计的组合用法，附常用管道套路与正则避坑指南。</description><pubDate>Wed, 02 Sep 2026 12:15:00 GMT</pubDate><content:encoded>&lt;p&gt;服务器上绝大多数信息都是纯文本：日志、配置、命令输出。&lt;code&gt;grep&lt;/code&gt;、&lt;code&gt;sed&lt;/code&gt;、&lt;code&gt;awk&lt;/code&gt; 是处理这类文本的三件基础工具。本文不复述参数手册，而是用一条「排查 Nginx 访问日志」的主线把它们串起来。&lt;/p&gt;
&lt;p&gt;示例日志格式（combined）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;203.0.113.10 - - [02/Sep/2026:10:15:01 +0800] &quot;GET /api/users HTTP/1.1&quot; 200 1234 &quot;-&quot; &quot;curl/8.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;grep：先缩小范围&lt;/h2&gt;
&lt;p&gt;排查的第一步永远是过滤。&lt;code&gt;grep 模式 文件&lt;/code&gt; 把匹配行打印出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep &apos; 500 &apos; /var/log/nginx/access.log     # 找状态码 500 的行
grep -c &apos; 500 &apos; /var/log/nginx/access.log  # 只统计行数
grep -v &apos; 200 &apos; access.log                 # 反向：排除正常请求
grep -i &apos;timeout&apos; app.log                  # 忽略大小写
grep -r &apos;database&apos; /etc/nginx/             # 递归搜目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个高频技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;grep -A 5 -B 2 &apos;ERROR&apos; app.log&lt;/code&gt;：同时显示匹配行后 5 行、前 2 行，看异常堆栈时必备。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -E &apos;401|403&apos; access.log&lt;/code&gt;：&lt;code&gt;-E&lt;/code&gt; 启用扩展正则，多模式「或」匹配。&lt;/li&gt;
&lt;li&gt;想按词匹配（避免 &lt;code&gt;error&lt;/code&gt; 匹配到 &lt;code&gt;terror&lt;/code&gt;），加 &lt;code&gt;-w&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
日志很大时不要先 &lt;code&gt;cat&lt;/code&gt; 再管道，&lt;code&gt;grep&lt;/code&gt; 直接接文件名即可，少一次无谓的读取。
:::&lt;/p&gt;
&lt;h2&gt;awk：按列统计&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;awk&lt;/code&gt; 默认按空白切分列，&lt;code&gt;$1&lt;/code&gt; 是第一列。统计访问日志里各状态码的数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;awk &apos;{print $9}&apos; access.log | sort | uniq -c | sort -rn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆解这条管道：&lt;code&gt;$9&lt;/code&gt; 取出状态码列 → &lt;code&gt;sort&lt;/code&gt; 排序 → &lt;code&gt;uniq -c&lt;/code&gt; 去重计数 → 按数量倒序。&lt;/p&gt;
&lt;p&gt;awk 真正的威力在条件与计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 只看 500 错误，打印时间和路径
awk &apos;$9 == 500 {print $4, $7}&apos; access.log

# 统计响应体总字节数（$10 求和）
awk &apos;{sum += $10} END {print sum/1024/1024 &quot; MB&quot;}&apos; access.log

# 找出响应最大的 5 个请求
awk &apos;{print $10, $7}&apos; access.log | sort -rn | head -5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行筛选条件写在 &lt;code&gt;{}&lt;/code&gt; 前面，多列处理写在里面，「先条件后动作」是 awk 的基本句式。&lt;/p&gt;
&lt;h2&gt;sed：批量改写&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sed&lt;/code&gt; 按行处理，最常用于替换和删行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sed &apos;s/404/找不到页面/&apos; app.log        # 每行只替换第一个匹配
sed &apos;s/404/找不到页面/g&apos; app.log       # 加 g 全部替换
sed -n &apos;100,200p&apos; big.log              # 只打印第 100~200 行
sed &apos;/DEBUG/d&apos; app.log                 # 删除含 DEBUG 的行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认只把结果打印到屏幕，不改文件。确认无误后再加 &lt;code&gt;-i&lt;/code&gt; 就地修改，macOS 下 &lt;code&gt;-i&lt;/code&gt; 需要跟一个备份后缀参数（如 &lt;code&gt;sed -i &apos;.bak&apos;&lt;/code&gt;），与 GNU sed 行为不同。&lt;/p&gt;
&lt;p&gt;改配置文件前先备份是铁律：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp nginx.conf nginx.conf.bak
sed -i &apos;s/listen 80;/listen 8080;/&apos; nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组合拳：定位「哪个 IP 在刷接口」&lt;/h2&gt;
&lt;p&gt;线上接口告警，怀疑被恶意爬取。三步定位：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 近一小时的日志里，按 IP 统计请求数
grep &apos;02/Sep/2026:10&apos; access.log | awk &apos;{print $1}&apos; | sort | uniq -c | sort -rn | head

# 2. 看看第一名 IP 都在访问什么路径
grep &apos;^203.0.113.10 &apos; access.log | awk &apos;{print $7}&apos; | sort | uniq -c | sort -rn | head

# 3. 确认它的 User-Agent
grep &apos;^203.0.113.10 &apos; access.log | awk -F&apos;&quot;&apos; &apos;{print $6}&apos; | sort -u
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 3 步用了 &lt;code&gt;-F&apos;&quot;&apos;&lt;/code&gt; 以双引号切分，因为 User-Agent 里含空格，按空格切会错位。日志分析里「选对分隔符」比记更多参数更重要。&lt;/p&gt;
&lt;h2&gt;正则避坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt; 在正则里匹配任意字符，想匹配字面句点要写 &lt;code&gt;\.&lt;/code&gt;（比如匹配 IP）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt; 表示「前一个字符重复零次或多次」，不是 shell 通配符里的「任意串」；任意串是 &lt;code&gt;.*&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;grep 默认是基础正则（BRE），&lt;code&gt;+&lt;/code&gt;、&lt;code&gt;?&lt;/code&gt;、&lt;code&gt;|&lt;/code&gt; 需要 &lt;code&gt;-E&lt;/code&gt; 或转义。&lt;/li&gt;
&lt;li&gt;拿不准正则时，先用小样本验证：&lt;code&gt;grep -E &apos;你的模式&apos; sample.log | head&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么时候该换工具&lt;/h2&gt;
&lt;p&gt;三剑客适合几十 MB 到几 GB 的单机文本。如果日志已经集中收集到 ELK/Loki，或者要做复杂的关联分析，就该用平台查询而不是在服务器上敲管道了——工具的价值在于「就地、快速、零依赖」。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/grep/manual/&quot;&gt;GNU grep 手册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/sed/manual/&quot;&gt;GNU sed 手册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/gawk/manual/&quot;&gt;GNU awk（gawk）手册&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux 命令行第一步：目录导航与文件操作的安全姿势</title><link>https://meow.us.kg/posts/linux-command-line-first-steps/</link><guid isPermaLink="true">https://meow.us.kg/posts/linux-command-line-first-steps/</guid><description>从零理解 Linux 目录树、绝对路径与相对路径，掌握 ls、cd、cp、mv、rm 的正确用法，并给 rm 加上几道防误删保险。</description><pubDate>Tue, 01 Sep 2026 01:30:00 GMT</pubDate><content:encoded>&lt;p&gt;第一次面对黑色终端窗口时，最大的障碍不是记命令，而是不知道「我现在在哪、这条命令会动到谁」。本文先建立路径与目录树的心智模型，再讲最常用的文件操作命令，最后专门处理 &lt;code&gt;rm&lt;/code&gt; 这个新手事故高发区。&lt;/p&gt;
&lt;h2&gt;一棵树，而不是几个盘&lt;/h2&gt;
&lt;p&gt;Windows 以 &lt;code&gt;C:&lt;/code&gt;、&lt;code&gt;D:&lt;/code&gt; 划分磁盘，Linux 则把所有设备挂载到同一棵以 &lt;code&gt;/&lt;/code&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;th&gt;日常是否需要碰&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/home/alice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户 alice 的家目录&lt;/td&gt;
&lt;td&gt;主要活动区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/etc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统与服务的配置文件&lt;/td&gt;
&lt;td&gt;改配置时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/var/log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日志&lt;/td&gt;
&lt;td&gt;排查问题时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/tmp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;临时文件，重启可能清空&lt;/td&gt;
&lt;td&gt;随手放，别存重要数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/usr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安装的程序与库&lt;/td&gt;
&lt;td&gt;一般只读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;超级用户的家目录&lt;/td&gt;
&lt;td&gt;普通用户进不去&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;几个快捷符号：&lt;code&gt;.&lt;/code&gt; 表示当前目录，&lt;code&gt;..&lt;/code&gt; 表示上一级，&lt;code&gt;~&lt;/code&gt; 表示当前用户家目录，&lt;code&gt;-&lt;/code&gt; 在 &lt;code&gt;cd&lt;/code&gt; 中表示「上一个所在目录」。&lt;/p&gt;
&lt;h2&gt;路径：绝对与相对&lt;/h2&gt;
&lt;p&gt;绝对路径从 &lt;code&gt;/&lt;/code&gt; 写起，如 &lt;code&gt;/var/log/nginx/access.log&lt;/code&gt;；相对路径从当前位置出发，如当前在 &lt;code&gt;/var&lt;/code&gt; 时写 &lt;code&gt;log/nginx/access.log&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;判断用哪个的原则很简单：写脚本、写定时任务一律用绝对路径，避免工作目录变化导致找不到文件；交互式操作图省事可以用相对路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwd                 # 我在哪
cd /var/log         # 绝对路径跳转
cd ..               # 回上一级
cd -                # 回上一次所在目录
ls -lh              # 列表 + 人类可读的文件大小
ls -la              # 显示隐藏文件（以 . 开头）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ls&lt;/code&gt; 的两个高频选项值得记牢：&lt;code&gt;-h&lt;/code&gt; 把字节数换算成 K/M/G，&lt;code&gt;-t&lt;/code&gt; 按修改时间排序，找最新日志时特别好用：&lt;code&gt;ls -lht /var/log | head&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;创建、复制、移动、删除&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p projects/demo/assets   # -p 一次创建多级，存在也不报错
touch projects/demo/notes.md    # 创建空文件或刷新时间戳
cp -r projects/demo /tmp/demo-backup   # 复制目录必须加 -r
mv notes.md todo.md             # 重命名
mv todo.md /tmp/                # 移动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个容易踩的细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cp&lt;/code&gt; 不加 &lt;code&gt;-r&lt;/code&gt; 复制目录会直接报错 &lt;code&gt;omitting directory&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mv&lt;/code&gt; 同名目标已存在时会&lt;strong&gt;静默覆盖&lt;/strong&gt;，加 &lt;code&gt;-i&lt;/code&gt; 让它覆盖前先问你。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir -p&lt;/code&gt; 是脚本里的好习惯：目录已存在时不报错退出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;给 rm 上三道保险&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt; 删除不走回收站，命令回车即生效，且 Linux 没有统一的「撤销删除」。三道保险：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一道：确认目标再回车。&lt;/strong&gt; 删除前先 &lt;code&gt;ls&lt;/code&gt; 同一个路径，确认列出的是你想删的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls /tmp/demo-backup
rm -r /tmp/demo-backup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二道：交互模式。&lt;/strong&gt; 给重要操作加 &lt;code&gt;-i&lt;/code&gt;，每删一个文件都要确认：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -ri old-logs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在自己 shell 的配置里加别名 &lt;code&gt;alias rm=&apos;rm -i&apos;&lt;/code&gt;，但注意脚本里不要依赖别名。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三道：通配符先打印后执行。&lt;/strong&gt; &lt;code&gt;rm *.log&lt;/code&gt; 之前先 &lt;code&gt;echo *.log&lt;/code&gt;，看展开结果是否符合预期。尤其警惕变量为空导致的路径漂移——脚本里 &lt;code&gt;rm -rf &quot;$dir/&quot;&lt;/code&gt; 在 &lt;code&gt;$dir&lt;/code&gt; 为空时会变成 &lt;code&gt;rm -rf /&lt;/code&gt;，这是真实发生过的生产事故。安全写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -n &quot;$dir&quot; ] &amp;amp;&amp;amp; rm -rf -- &quot;$dir/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--&lt;/code&gt; 告诉命令「后面没有选项了」，防止文件名以 &lt;code&gt;-&lt;/code&gt; 开头被当成选项。&lt;/p&gt;
&lt;p&gt;:::warning
&lt;code&gt;rm -rf&lt;/code&gt; 会强制递归删除且不询问，执行前务必确认路径里没有变量展开错误，也不要用 sudo 运行来源不明的清理脚本。
:::&lt;/p&gt;
&lt;h2&gt;学会自救：man 与 --help&lt;/h2&gt;
&lt;p&gt;记不清参数时优先问系统而不是凭印象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;man ls          # 完整手册，q 退出，/关键词 搜索
ls --help       # 快速速查
type cd         # 看这个命令是内建还是外部程序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令行的肌肉记忆来自高频使用，但「先确认、再执行」的习惯从第一天就该养成。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/coreutils/manual/&quot;&gt;GNU Coreutils 官方手册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kernel.org/doc/man-pages/&quot;&gt;Linux man-pages 项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linuxcommand.org/tlcl.php&quot;&gt;The Linux Command Line（免费在线版）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>