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

# MySQL
mysqldump --single-transaction -u myapp -p myapp | gzip &gt; /backup/db/myapp-$(date +%F).sql.gz
</code></pre>
<p>（<code>--single-transaction</code> 对 InnoDB 做一致性快照导出，不锁表。）</p>
<h2>工具二：restic 做加密、去重、版本化备份</h2>
<p>restic 是现代备份工具的代表：增量去重、端到端加密、支持 S3 兼容存储，一份数据加密后才离开服务器——就算存储端被攻破，攻击者拿到的也是密文。</p>
<p>初始化仓库（以 S3 兼容对象存储为例）：</p>
<pre><code>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
</code></pre>
<p>日常备份一条命令：</p>
<pre><code>restic -r s3:https://s3.example.com/mybucket/backups \
  backup /var/www /etc /backup/db \
  --exclude-file /etc/restic/excludes
</code></pre>
<p>restic 每次备份生成一个快照，自动去重——重复备份 100 GB 数据，第二次只传变化的部分。</p>
<p><strong>保留策略</strong>：旧快照不是无限留的，按策略清理：</p>
<pre><code>restic -r s3:... forget --prune \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6
</code></pre>
<p>保留最近 7 个日备份、4 个周备份、6 个月备份，其余删除并回收空间。</p>
<p><strong>定时化</strong>：把备份命令写成脚本，用 cron 或 systemd timer 每天凌晨执行，并把结果输出到日志，纳入监控告警——「备份任务失败」必须是一条会叫醒人的告警，静默失败的备份等于没有备份。</p>
<h2>恢复演练：备份的真正验收</h2>
<p>没恢复过的备份只能算「愿望」。restic 的恢复：</p>
<pre><code># 列出快照
restic -r s3:... snapshots

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

# 只取回单个文件
restic -r s3:... dump latest /var/www/config.yml &gt; config.yml
</code></pre>
<p>建议每季度做一次完整演练：把备份恢复到一台干净的机器，验证应用真的能跑起来。演练会暴露各种想当然——比如备份里漏了某个关键目录、数据库导出脚本早就因为密码轮换而失败。</p>
<h2>一张个人服务器备份方案清单</h2>
<ol>
<li>每日凌晨：<code>pg_dump</code>/<code>mysqldump</code> 导出数据库到本地目录。</li>
<li>随后：restic 把「应用数据 + /etc 配置 + 数据库导出」加密备份到异地对象存储，保留 7 日/4 周/6 月。</li>
<li>备份失败 → 触发告警，当天处理。</li>
<li>每季度：恢复到测试环境演练一次。</li>
<li>加密口令、存储密钥记录在离线的密码管理器里——备份全在，口令丢了，一样是数据丢失。</li>
</ol>
<p>这套方案全部用开源工具和对象存储搭建，月成本一杯咖啡钱，换的是任何单点事故都不致命的底气。</p>
<h2>延伸阅读</h2>
<ul>
<li><a href="https://restic.readthedocs.io/en/latest/">restic 官方文档</a></li>
<li><a href="https://download.samba.org/pub/rsync/rsync.1">rsync 手册页</a></li>
<li><a href="https://www.postgresql.org/docs/current/backup.html">PostgreSQL 官方文档：Backup and Restore</a></li>
</ul>
]]></content>
    <author><name>もゆちゃん</name></author>
    <category term="运维实践"/>
  </entry>
  <entry>
    <title>云服务器初始化安全清单：安全组、SSH 加固与 fail2ban</title>
    <link href="https://meow.us.kg/posts/cloud-server-initial-hardening/" rel="alternate" type="text/html"/>
    <id>https://meow.us.kg/posts/cloud-server-initial-hardening/</id>
    <published>2026-10-19T02:30:00.000Z</published>
    <updated>2026-10-19T02:30:00.000Z</updated>
    <summary>新购云服务器上架后的 30 分钟加固流程：安全组最小开放、创建普通用户、SSH 密钥登录与端口策略、fail2ban 防爆破、自动安全更新。</summary>
    <content type="html"><![CDATA[<p>一台新云服务器从拿到公网 IP 那一刻起，就开始被全互联网的扫描器光顾——SSH 爆破尝试通常在几分钟内出现。默认配置的服务器在公网上等于裸奔。这套清单按「先挡住外面的，再管好里面的」排序，30 分钟内可以全部完成。</p>
<h2>第 1 步：安全组只开必要端口（云厂商层面）</h2>
<p>安全组是云平台的虚拟防火墙，规则在系统之外生效——就算系统内防火墙没配，安全组也能兜底。原则：<strong>默认全拒，按需放行</strong>。</p>
<p>一台 Web 服务器的典型放行清单：</p>
<table>
<thead>
<tr>
<th>端口</th>
<th>来源</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>22（或自定义 SSH 端口）</td>
<td>你的办公/家庭 IP 段</td>
<td>SSH 管理</td>
</tr>
<tr>
<td>80</td>
<td>0.0.0.0/0</td>
<td>HTTP（跳转 HTTPS 及证书验证）</td>
</tr>
<tr>
<td>443</td>
<td>0.0.0.0/0</td>
<td>HTTPS</td>
</tr>
</tbody>
</table>
<p>数据库 3306、Redis 6379、管理面板端口<strong>永远不要</strong>对公网开放，内部访问走 VPC 内网。SSH 来源限制为固定 IP 段是性价比最高的一条规则，直接砍掉 99% 的爆破流量。</p>
<h2>第 2 步：创建普通用户，告别 root 日常操作</h2>
<pre><code>adduser deploy
usermod -aG sudo deploy        # Debian/Ubuntu；RHEL 系是 wheel 组
</code></pre>
<p>此后所有日常操作用 <code>deploy</code> 登录，需要特权时 <code>sudo</code>。直接以 root 工作，一个手滑的 <code>rm -rf</code> 就没有任何挽回余地。</p>
<h2>第 3 步：SSH 密钥登录，关掉密码</h2>
<p>在<strong>本地</strong>生成密钥（如果还没有）：</p>
<pre><code>ssh-keygen -t ed25519 -C "deploy@myserver"
ssh-copy-id deploy@服务器IP
</code></pre>
<p>验证密钥能登录后，修改服务器 <code>/etc/ssh/sshd_config</code>：</p>
<pre><code>PasswordAuthentication no       # 禁用密码登录
PermitRootLogin no              # 禁止 root 直接登录
PubkeyAuthentication yes
</code></pre>
<p>改完务必<strong>新开一个终端验证能登录，再关掉旧会话</strong>——配置写错又断开会话，就只能靠云厂商的 VNC 控制台救场了。</p>
<pre><code>sudo sshd -t                    # 语法检查
sudo systemctl reload ssh
</code></pre>
<p>关于改 SSH 端口（如 2222）：它能减少日志噪音，但对有目的的扫描者作用有限（全端口扫描照样找到）。可做可不做；如果改，记得同步更新安全组。</p>
<h2>第 4 步：fail2ban 封禁爆破 IP</h2>
<p>即使有密钥登录，爆破流量仍会灌满日志。fail2ban 监控日志，把反复失败的 IP 自动拉进防火墙黑名单：</p>
<pre><code>sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
</code></pre>
<p><code>jail.local</code> 中调整 sshd 段：</p>
<pre><code>[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
</code></pre>
<p>含义：10 分钟内失败 5 次，封禁 1 小时。启动并查看状态：</p>
<pre><code>sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
</code></pre>
<h2>第 5 步：系统防火墙（ufw）作为第二道门</h2>
<p>安全组在云侧，ufw 在系统侧，两层独立互备：</p>
<pre><code>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
</code></pre>
<p>:::warning
<code>ufw enable</code> 前确认 SSH 端口已放行，否则会把自己锁在门外。云服务器至少还有安全组和 VNC 可以补救，裸金属机器上这条错误可能意味着一趟机房。
:::</p>
<h2>第 6 步：开启自动安全更新</h2>
<p>系统漏洞补丁不及时打，前面的加固都是沙上筑塔。Debian/Ubuntu 用 <code>unattended-upgrades</code> 自动安装安全更新：</p>
<pre><code>sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades   # 交互式确认开启
</code></pre>
<p>默认只自动更新安全源（security），不动功能更新——这个平衡对服务器正合适。</p>
<h2>第 7 步：时间同步与基础可观测</h2>
<ul>
<li>确认 <code>timedatectl</code> 显示 NTP 同步正常——证书验证、日志时间线、排障对齐全依赖准确时间。</li>
<li>顺手装上排障工具：<code>apt install htop iotop ncdu mtr-tiny</code>。</li>
<li>在云平台控制台配置<strong>监控告警</strong>（CPU、磁盘、带宽），这是系统外的一层眼睛。</li>
</ul>
<h2>加固完成后的自查</h2>
<pre><code>ss -tlnp                         # 监听端口是否与预期一致，没有多余服务
grep "PasswordAuthentication" /etc/ssh/sshd_config
sudo fail2ban-client status      # jail 是否生效
sudo ufw status
</code></pre>
<p>对照清单逐项打钩。安全不是一次性动作——定期检查登录日志（<code>journalctl -u ssh</code>）、关注所用软件的安全公告，才是常态。</p>
<h2>延伸阅读</h2>
<ul>
<li><a href="https://documentation.ubuntu.com/server/how-to/security/openssh-server/">Ubuntu 官方文档：OpenSSH Server</a></li>
<li><a href="https://fail2ban.readthedocs.io/en/latest/">fail2ban 官方文档</a></li>
<li><a href="https://www.cisecurity.org/cis-benchmarks">CIS Benchmarks（系统加固基线）</a></li>
</ul>
]]></content>
    <author><name>もゆちゃん</name></author>
    <category term="运维实践"/>
  </entry>
  <entry>
    <title>用 GitHub Actions 搭建静态博客自动部署流水线</title>
    <link href="https://meow.us.kg/posts/github-actions-auto-deploy-blog/" rel="alternate" type="text/html"/>
    <id>https://meow.us.kg/posts/github-actions-auto-deploy-blog/</id>
    <published>2026-10-18T02:30:00.000Z</published>
    <updated>2026-10-18T02:30:00.000Z</updated>
    <summary>从 push 触发、构建、部署到通知的完整链路：为 Astro/Hugo 类静态博客编写 GitHub Actions 工作流，实现提交即上线。</summary>
    <content type="html"><![CDATA[<p>静态博客的发布流程本应是「写完、push、完事」，但很多人还在手动 <code>npm run build</code>、手动 <code>rsync</code> 上传——每次发文都多一步出错的机会。GitHub Actions 可以把这条链路完全自动化：push 到主分支后，构建、部署、通知全部自动完成。本文以一个 Node 系静态站（Astro/Hugo 同理）为例走通全流程。</p>
<h2>工作流全貌</h2>
<pre><code>push 到 main
   │
   ▼
GitHub Actions Runner 启动
   ├─ 1. 拉代码
   ├─ 2. 装依赖、构建静态产物
   ├─ 3. 部署产物（rsync 到服务器 / 推到 Pages）
   └─ 4. 失败时通知
</code></pre>
<p>工作流文件放在仓库的 <code>.github/workflows/deploy.yml</code>：</p>
<pre><code>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 }}
</code></pre>
<h2>逐段拆解</h2>
<p><strong>触发器 <code>on</code></strong>：<code>push: branches: [main]</code> 表示推送到 main 分支时触发；<code>workflow_dispatch</code> 加一个手动按钮，改完配置想立刻验证时不用特意造一个 commit。</p>
<p><strong><code>npm ci</code> 而不是 <code>npm install</code></strong>：CI 环境的铁律。<code>ci</code> 严格按 <code>package-lock.json</code> 安装，保证每次构建依赖版本完全一致；<code>install</code> 可能按 semver 范围悄悄升级某个包，让「本地能过、CI 挂掉」变成悬案。搭配 <code>setup-node</code> 的 <code>cache: npm</code>，依赖下载在多次运行间缓存，构建快很多。</p>
<p><strong>部署环节的安全</strong>：服务器地址、用户名、SSH 私钥全部放仓库的 Settings → Secrets and variables → Actions 里，以 <code>${{ secrets.XXX }}</code> 引用。私钥内容<strong>绝不能</strong>写进 YAML——这个文件在仓库里是公开的。</p>
<p><code>--delete</code> 参数让 rsync 删除服务器上多余的旧文件，保持目录与构建产物一致；它对部署目录是安全的，但务必确认 <code>remote_path</code> 没填错——填成 <code>/var/www/</code> 会把兄弟站点的目录清掉。</p>
<h2>为部署准备专用 SSH 密钥</h2>
<p>不要在多个地方复用同一把私钥，给 Actions 单独生成一对：</p>
<pre><code>ssh-keygen -t ed25519 -C "github-actions-blog" -f ~/.ssh/gh-actions-deploy
</code></pre>
<p>公钥追加到服务器部署用户的 <code>~/.ssh/authorized_keys</code>，私钥全文粘贴到仓库 Secret <code>DEPLOY_KEY</code>。这样即便 CI 侧泄露，也只暴露这一把受限密钥，可以随时吊销更换。更严格的做法是在服务器上给该用户配置 <code>command=</code> 前缀限制它只能执行 rsync。</p>
<h2>不想维护服务器？推到 GitHub Pages</h2>
<p>如果博客就托管在 GitHub Pages，部署步骤换成官方 Action，连服务器和密钥都不用管：</p>
<pre><code>      - 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
</code></pre>
<p>（需要在仓库 Settings → Pages 里把来源选为 GitHub Actions。）</p>
<h2>让失败可见</h2>
<p>流水线最大的谎言是「绿钩即一切正常」，第二大的谎言是「失败了会有人发现」。加一步失败通知，比如用 curl 调用任意 webhook（Server 酱、Telegram Bot、企业微信均可）：</p>
<pre><code>      - name: 失败通知
        if: failure()
        run: |
          curl -s "https://api.day.app/${{ secrets.BARK_KEY }}/博客部署失败/${{ github.sha }}?level=critical"
</code></pre>
<p><code>if: failure()</code> 表示仅当前面步骤失败才执行。同理 <code>if: success()</code> 可以发成功确认——初期建议两种都开，跑稳定后只留失败通知，避免消息疲劳。</p>
<h2>值得加的两个增强</h2>
<ol>
<li><strong>构建前内容校验</strong>：在 build 步骤前加 <code>npm run check</code>（如果项目提供），让 Front Matter 缺字段这类问题在 CI 阶段就拦住，而不是部署后才发现页面异常。</li>
<li><strong>并发控制</strong>：连续 push 两个 commit 时不希望两个部署任务打架：</li>
</ol>
<pre><code>concurrency:
  group: deploy
  cancel-in-progress: true
</code></pre>
<p>同一时刻只允许一个部署在跑，新的 push 会取消未完成的旧任务——反正部署的目标是「最新状态」，中间的半成品没有意义。</p>
<h2>延伸阅读</h2>
<ul>
<li><a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions">GitHub Actions 官方文档：Workflow syntax</a></li>
<li><a href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub Actions 官方文档：Using secrets</a></li>
<li><a href="https://github.com/actions">actions/checkout 等官方 Action 索引</a></li>
</ul>
]]></content>
    <author><name>もゆちゃん</name></author>
    <category term="运维实践"/>
  </entry>
  <entry>
    <title>Kubernetes 核心对象：Pod、Deployment 与 Service 的关系</title>
    <link href="https://meow.us.kg/posts/kubernetes-pod-deployment-service/" rel="alternate" type="text/html"/>
    <id>https://meow.us.kg/posts/kubernetes-pod-deployment-service/</id>
    <published>2026-10-17T02:30:00.000Z</published>
    <updated>2026-10-17T02:30:00.000Z</updated>
    <summary>用「声明式」心智模型理解 K8s：Pod 为什么是最小单位、Deployment 如何管副本与滚动更新、Service 怎样提供稳定访问入口。</summary>
    <content type="html"><![CDATA[<p>Kubernetes 的学习曲线陡峭，很大程度是因为一上来就被几十个对象类型淹没。其实日常 80% 的工作只围绕三个对象：<strong>Pod 跑应用，Deployment 管 Pod，Service 暴露 Pod</strong>。理清这三者的关系，K8s 的大门就打开了。</p>
<h2>先换脑子：声明式，不是命令式</h2>
<p>传统运维是命令式的：「启动 3 个容器」「这台挂了，重启它」。K8s 是声明式的：你只提交<strong>期望状态</strong>（"这个应用要有 3 个副本"），K8s 的控制循环持续观察实际状态，发现偏差就自动纠正——副本少了就补，节点挂了就迁。</p>
<p>你提交的每一份 YAML 都是一张「愿望清单」，而不是操作手册。这个模型是理解一切 K8s 行为的前提。</p>
<h2>Pod：最小调度单位</h2>
<p>Pod 是一个或多个<strong>共享网络与存储</strong>的容器组，是 K8s 调度的最小单位。同一 Pod 内的容器共享 IP 和端口空间，通过 <code>localhost</code> 互访。</p>
<p>为什么不是直接调度容器？因为存在「主容器 + 紧密耦合的辅助容器」场景：比如应用容器写日志到共享目录，边车（sidecar）容器负责把日志转发出去——它们必须同生共死、共享存储，打包成一个 Pod 正合适。</p>
<p>实践中很少手写 Pod 定义，但要知道它的两个特性：<strong>IP 是临时的</strong>（Pod 重建就换）、<strong>Pod 是不可变的</strong>（想改配置？删掉重建）。</p>
<h2>Deployment：让 Pod 保持「想要的样子」</h2>
<p>直接创建 Pod 是自虐：它挂了就挂了，没人管。Deployment 在 Pod 之上加了副本管理和更新策略：</p>
<pre><code>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
</code></pre>
<pre><code>kubectl apply -f deployment.yaml
kubectl get pods                  # 看到 3 个 web-xxx 的 Pod
</code></pre>
<p><strong>滚动更新</strong>：改 <code>image</code> 版本后再 <code>apply</code>，Deployment 默认用 RollingUpdate 策略——先起一个新版本 Pod，健康了再灭一个旧的，如此滚动，全程服务不中断。</p>
<p><strong>回滚</strong>：历史版本被自动记录，新版出问题时一键回到上一个：</p>
<pre><code>kubectl rollout status deployment/web      # 观察更新进度
kubectl rollout undo deployment/web        # 回滚
</code></pre>
<p><strong>自愈</strong>：手动 <code>kubectl delete pod web-xxx</code> 试试——Deployment 立刻补一个新 Pod 维持 <code>replicas: 3</code>。这就是声明式控制循环的直观体现。</p>
<h2>Service：给会漂移的 Pod 一个稳定地址</h2>
<p>Pod IP 会变、副本会扩缩，客户端该连哪个地址？Service 用<strong>标签选择器</strong>圈定一组 Pod，提供一个稳定的虚拟 IP 和 DNS 名，并把流量转发过去。</p>
<pre><code>apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web              # 选中所有带 app=web 标签的 Pod
  ports:
    - port: 80            # Service 自己的端口
      targetPort: 8080    # 转发到 Pod 的端口
</code></pre>
<p>集群内其他应用现在可以通过 <code>http://web</code>（同命名空间下）访问你的服务——不用关心后面是 3 个还是 30 个 Pod、IP 换了几次。</p>
<p>Service 的三种常用类型：</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ClusterIP</code>（默认）</td>
<td>仅集群内访问，内部服务间调用的标准方式</td>
</tr>
<tr>
<td><code>NodePort</code></td>
<td>在每个节点开一个端口（30000-32767），外部可经节点 IP 访问</td>
</tr>
<tr>
<td><code>LoadBalancer</code></td>
<td>向云厂商申请一个外部负载均衡器，生产暴露服务的常用方式</td>
</tr>
</tbody>
</table>
<p>对外提供 HTTP 服务时，更常见的组合是 Service + Ingress（统一域名、路径路由和 TLS 终结），Ingress 可以认为是「HTTP 层的 Service」。</p>
<h2>三者关系一图流</h2>
<pre><code>                 管理副本与更新
   Deployment ──────────────────► Pod × 3（标签 app=web）
                                      ▲
                                      │ 标签选择器
   Service ───────────────────────────┘
   （稳定 IP/DNS，负载均衡转发）
</code></pre>
<p>客户端访问 Service 的固定地址；Service 按标签把请求分发给当前存活的 Pod；Deployment 保证 Pod 的数量和版本符合期望。三层各司其职，任何一层都不需要知道其他层的实现细节。</p>
<h2>动手前的建议</h2>
<p>本地学习用 minikube 或 kind（Kubernetes in Docker）起单节点集群；云厂商的托管 K8s（ACK、TKE、EKS）省掉控制面运维，适合生产起步。学会 <code>kubectl get/describe/logs/apply</code> 四件套，就足以完成日常 80% 的操作——<code>describe</code> 尤其重要，任何对象「不正常」，先看它末尾的 Events。</p>
<h2>延伸阅读</h2>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/workloads/">Kubernetes 官方文档：Workloads</a></li>
<li><a href="https://kubernetes.io/docs/concepts/services-networking/service/">Kubernetes 官方文档：Service</a></li>
<li><a href="https://kubernetes.io/docs/reference/kubectl/quick-reference/">Kubernetes 官方文档：kubectl 快速参考</a></li>
</ul>
]]></content>
    <author><name>もゆちゃん</name></author>
    <category term="云原生"/>
  </entry>
  <entry>
    <title>Docker Compose 实战：一键编排多容器应用</title>
    <link href="https://meow.us.kg/posts/docker-compose-multi-container-app/" rel="alternate" type="text/html"/>
    <id>https://meow.us.kg/posts/docker-compose-multi-container-app/</id>
    <published>2026-10-16T02:30:00.000Z</published>
    <updated>2026-10-16T02:30:00.000Z</updated>
    <summary>以「Web 应用 + PostgreSQL + Redis」为例编写 docker-compose.yml，讲清服务定义、依赖启动顺序、环境变量与数据卷管理的完整实践。</summary>
    <content type="html"><![CDATA[<p>一个真实应用很少是单个容器：Web 服务要连数据库，数据库要挂数据卷，也许还有缓存和任务队列。用一连串 <code>docker run</code> 维护这些，命令越攒越长、参数越记越乱。Docker Compose 把整套应用写进一个 YAML 文件，<code>docker compose up -d</code> 一条命令完整拉起。</p>
<h2>示例场景：Web + PostgreSQL + Redis</h2>
<p>目标架构：一个 Web 应用，连接 PostgreSQL 存数据、Redis 做缓存；数据库数据要持久化；Redis 不对外暴露端口。</p>
<pre><code>services:
  web:
    image: registry.example.com/myapp:1.4.2
    build: .
    ports:
      - "8080:8080"
    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: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped
    networks:
      - appnet

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

volumes:
  pgdata:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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