<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Chever John 的博客</title>
        <link>https://blog.cheverjohn.me/zh</link>
        <description>Chever John 的博客和个人网站。软件工程师，摄影师</description>
        <lastBuildDate>Fri, 30 Jan 2026 07:47:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for Node.js</generator>
        <language>zh-CN</language>
        <ttl>60</ttl>
        <image>
            <title>Chever John 的博客</title>
            <url>https://blog.cheverjohn.me/assets/avatar.jpg</url>
            <link>https://blog.cheverjohn.me/zh</link>
        </image>
        <copyright>All rights reserved 2026, Chenwei Jiang</copyright>
        <item>
            <title><![CDATA[证书基础与一次 Traefik “Not secure” 排查实录]]></title>
            <link>https://blog.cheverjohn.me/zh/traefik-certificate-troubleshooting</link>
            <guid>https://blog.cheverjohn.me/zh/traefik-certificate-troubleshooting</guid>
            <pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[TL;DR - 现象：访问 https://traefik.example.local/dashboard/，Chrome/Safari 提示 Not secure；Firefox 正常。 - 根因：macOS Trust 验证器不接受通配符证书 .example.local 对子域名 traefik.example.local 的匹配（OpenSSL 接受，Apple Trust 不接受）。 - 修复：签发显式域名证书 traefik.example.local，替代通配符赌博。 - 一句话：验证器规则决定一切，不是"证书坏了"。 预防性清单（下次签发证书时直接照做） 对于内网自签 CA（m...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="tldr">TL;DR</h2><ul><li><strong>现象</strong>：访问 <code>https://traefik.example.local/dashboard/</code>，Chrome/Safari 提示 <strong>Not secure</strong>；Firefox 正常。</li><li><strong>根因</strong>：macOS Trust 验证器不接受通配符证书 <code>*.example.local</code> 对子域名 <code>traefik.example.local</code> 的匹配（OpenSSL 接受，Apple Trust 不接受）。</li><li><strong>修复</strong>：签发显式域名证书 <code>traefik.example.local</code>，替代通配符赌博。</li><li><strong>一句话</strong>：验证器规则决定一切，不是&quot;证书坏了&quot;。</li></ul><hr/><p>这不是&quot;证书坏了&quot;的问题，而是&quot;证书被验证的方式不同&quot;。在 Linux/OpenSSL 眼里没问题，在 macOS/Apple Trust 眼里却不合格。</p><p><strong>本文分两部分：证书的基础知识 + 今天这起排查的完整链路。</strong></p><h2 id="">证书基础（够用就好）</h2><p>先把最关键的概念拎出来：</p><ul><li><strong>证书链</strong>：叶子证书 → 中间证书（可选）→ 根 CA。浏览器只信任它认识的根 CA。</li><li><strong>SAN（Subject Alternative Name）</strong>：域名匹配完全看这里，CN 已经退居二线。</li><li><strong>信任库</strong>：macOS 有系统 Keychain；Firefox 有自己一套；Chrome/Safari 走系统信任。</li><li><strong>验证结果</strong>：同一张证书，在不同验证器里可能得出不同结论。</li></ul><p>一句话总结：<strong>证书“正确”不等于“被当前验证器接受”。</strong></p><h2 id="">问题现象与影响范围</h2><p>访问 <code>https://traefik.example.local/dashboard/#/</code>，Chrome/Safari 都提示 <strong>Not secure</strong>。</p><p><strong>影响范围：</strong></p><ul><li>❌ Chrome（依赖 macOS System Keychain）</li><li>❌ Safari（依赖 macOS System Keychain）</li><li>✅ Firefox（使用自己的证书存储，不受影响）</li></ul><p><strong>可复现前提：</strong></p><ul><li>macOS 版本：Sonoma 14.x+（其他版本可能表现不同）</li><li>CA 已导入 macOS Keychain 并设置为&quot;始终信任&quot;</li><li>证书由 mkcert 签发（版本 1.4.4）</li><li>服务端在 <code>10.0.0.100</code></li></ul><p><strong>我希望先回答三个问题：</strong></p><ol start="1"><li>域名是否解析到了正确的服务器？</li><li>证书链是否完整、域名是否匹配？</li><li>浏览器信任库是否真的认可这张证书？</li></ol><h2 id="">假设清单（按排除顺序）</h2><p>在开始排查前，先列出可能的根因假设：</p><ul><li><p><strong>H1：CA 未正确导入系统信任库</strong></p><ul><li>验证方法：检查 macOS Keychain，确认 CA 证书存在且为&quot;始终信任&quot;</li><li>预期：如果 CA 不在信任库，所有浏览器都会报错（但 Firefox 正常，所以可能性低）</li></ul></li><li><p><strong>H2：证书链不完整（缺少中间证书）</strong></p><ul><li>验证方法：<code>openssl s_client -showcerts</code> 查看证书链深度</li><li>预期：自签 CA 通常只有两层（leaf + root），mkcert 默认就是完整的</li></ul></li><li><p><strong>H3：SAN 不包含目标域名</strong></p><ul><li>验证方法：<code>openssl x509 -text</code> 查看 Subject Alternative Name 字段</li><li>预期：如果 SAN 缺失或不匹配，OpenSSL 也会报错（但 curl 通过了，所以可能性低）</li></ul></li><li><p><strong>H4：验证器规则差异（OpenSSL vs Apple Trust）</strong></p><ul><li>验证方法：用 <code>security verify-cert</code> 模拟 macOS 系统验证</li><li>预期：<strong>这是最可能的根因</strong> —— 不同验证器对通配符的 hostname matching 规则不同</li></ul></li></ul><p>接下来逐条验证。</p><h2 id="">验证流程可视化</h2><p>下面这个图展示了4层验证的递进关系（不执行任何操作，仅用于理解）：</p><pre><code class="lang-bash">#!/usr/bin/env bash
# 验证流程：4层检查，定位到底是哪一层不认

cat &lt;&lt;&#x27;EOF&#x27;

┌─────────────────────────────────────────────────────────┐
│  Layer 1: DNS Resolution                                │
│  dig traefik.example.local +short                       │
│  ✅ Result: 10.0.0.100                                   │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Layer 2: Certificate Content (SAN)                     │
│  openssl s_client + grep &quot;Subject Alternative Name&quot;     │
│  ✅ Result: DNS:*.example.local, DNS:example.local      │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Layer 3: OpenSSL Validator (Linux/curl)                │
│  curl -Iv https://traefik.example.local/                │
│  ✅ Result: SSL certificate verify ok                   │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Layer 4: Apple Trust Validator (macOS)                 │
│  security verify-cert -n traefik.example.local          │
│  ❌ Result: Host name mismatch                          │
└─────────────────────────────────────────────────────────┘

关键发现：Layer 3 ✅ but Layer 4 ❌
→ 不同验证器对通配符的 hostname matching 规则不同
→ Chrome/Safari 依赖 Apple Trust (Layer 4)

EOF
</code></pre>
<h2 id="">排查链路（按顺序）</h2><h3 id="1-dns-">1) DNS 是否正确</h3><pre><code class="lang-bash">dig traefik.example.local +short
</code></pre>
<p><strong>预期输出：</strong></p><pre><code class="lang-text">10.0.0.100
</code></pre>
<p><strong>观察</strong>：DNS 指向正确，网络层没问题。</p><p><strong>结论</strong>：排除网络连通性问题，继续验证证书层。</p><h3 id="2--san-">2) 证书链与 SAN 是否正确</h3><pre><code class="lang-bash">openssl s_client -connect traefik.example.local:443 -servername traefik.example.local -showcerts 2&gt;/dev/null | openssl x509 -text -noout | grep -A2 &quot;Subject Alternative Name&quot;
</code></pre>
<p><strong>预期输出（关键部分）：</strong></p><pre><code class="lang-text">X509v3 Subject Alternative Name:
    DNS:*.example.local, DNS:example.local
</code></pre>
<p><strong>观察</strong>：SAN 包含通配符 <code>*.example.local</code>，按 RFC 6125 理论应该匹配 <code>traefik.example.local</code>。</p><p><strong>结论</strong>：证书字段本身没问题，排除 <strong>H3（SAN 缺失）</strong>。</p><h3 id="3--openssl-">3) 用 OpenSSL 验证（通过）</h3><pre><code class="lang-bash">curl -Iv https://traefik.example.local/ 2&gt;&amp;1 | grep -E &quot;SSL certificate verify|subject:&quot;
</code></pre>
<p><strong>预期输出：</strong></p><pre><code class="lang-text">* SSL certificate verify ok.
* subject: CN=*.example.local
</code></pre>
<p><strong>观察</strong>：OpenSSL 验证器认为证书有效，hostname 匹配通过。</p><p><strong>结论</strong>：<strong>OpenSSL 认为没问题</strong>，排除 <strong>H2（证书链不完整）</strong>。</p><h3 id="4--macos-">4) 用 macOS 系统验证（失败）</h3><pre><code class="lang-bash"># 先导出证书到临时文件（如果还没导出）
echo | openssl s_client -connect traefik.example.local:443 -servername traefik.example.local 2&gt;/dev/null | \
  openssl x509 &gt; /tmp/traefik-leaf.pem

# macOS 系统级验证
security verify-cert -c /tmp/traefik-leaf.pem -p ssl -n traefik.example.local -L -v
</code></pre>
<p><strong>预期输出（关键错误）：</strong></p><pre><code class="lang-text">...certificate verification failed
...Host name mismatch
</code></pre>
<p><strong>观察</strong>：Apple Trust 明确拒绝了这个通配符对 <code>traefik.example.local</code> 的匹配。</p><p><strong>结论</strong>：<strong>Apple Trust 认为这个 SAN 不能匹配目标域名</strong>。这是关键转折：OpenSSL OK，但 Apple Trust 不 OK。Chrome/Safari 都依赖后者。命中 <strong>H4（验证器规则差异）</strong>。</p><h2 id="">根因判断</h2><p>根因不是&quot;CA 没安装&quot;，也不是&quot;证书链缺失&quot;，而是：</p><p><strong>macOS 的主机名校验没有接受通配符证书对 <code>traefik.example.local</code> 的匹配。</strong></p><p>这不是理论推测，是系统验证器的实际结果。</p><h2 id="">最小可行修复</h2><p>不要赌通配符，直接签发显式域名证书。</p><pre><code class="lang-bash">mkcert -cert-file traefik.example.local.pem -key-file traefik.example.local-key.pem &quot;traefik.example.local&quot;
</code></pre>
<p>如果你希望保留通配符与根域：</p><pre><code class="lang-bash">mkcert -cert-file traefik.example.local.pem -key-file traefik.example.local-key.pem \
  &quot;traefik.example.local&quot; &quot;*.example.local&quot; &quot;example.local&quot;
</code></pre>
<p>然后在 Traefik TLS 配置里替换证书，重启服务即可。</p><h2 id="">一张排查清单（以后直接复用）</h2><ul><li>DNS 指向是否正确（<code>dig</code>）</li><li>SAN 是否包含目标域名（<code>openssl x509 -text</code>）</li><li>OpenSSL 验证结果（<code>curl -Iv</code>）</li><li>macOS 验证结果（<code>security verify-cert</code>）</li><li>浏览器是否使用系统信任库</li></ul><h2 id="">复盘与预防</h2><h3 id="">一句话教训</h3><p><strong>这次最值钱的证据：</strong>
<code>security verify-cert</code> 的 <code>Host name mismatch</code> 输出 —— 它直接证明了问题不在&quot;CA 信任&quot;而在&quot;hostname 验证规则&quot;。</p><p><strong>如果只能记住一件事：</strong>
不要用&quot;在我电脑上能打开&quot;来判断证书是否正确。不同验证器（OpenSSL / Apple Trust / Firefox NSS）对同一张证书可能得出不同结论。</p><hr/><h3 id="">预防性清单（下次签发证书时直接照做）</h3><p><strong>对于内网自签 CA（mkcert/自建 CA）：</strong></p><ul><li><input readonly="" type="checkbox"/> 优先签发显式域名，不要赌通配符的跨平台兼容性</li><li><input readonly="" type="checkbox"/> 如果必须用通配符，用 <code>security verify-cert</code> 在 macOS 上提前验证</li><li><input readonly="" type="checkbox"/> 保留签发命令记录（包含完整 SAN 列表），方便后续重新签发</li></ul><p><strong>对于公网证书（Let&#x27;s Encrypt 等）：</strong></p><ul><li><input readonly="" type="checkbox"/> 申请时同时包含 <code>example.com</code> 和 <code>*.example.com</code>（如果需要覆盖根域和子域）</li><li><input readonly="" type="checkbox"/> 验证时至少在 3 个平台测试：Linux (OpenSSL) / macOS (Apple Trust) / Windows (Schannel)</li></ul><p><strong>验证模板（可复用脚本）：</strong></p><pre><code class="lang-bash">#!/usr/bin/env bash
# 快速验证证书在多个验证器下的表现

DOMAIN=&quot;traefik.example.local&quot;

echo &quot;==&gt; OpenSSL 验证&quot;
curl -Iv https://${DOMAIN}/ 2&gt;&amp;1 | grep -E &quot;SSL certificate verify|subject:&quot;

echo &quot;==&gt; macOS 验证 (需要在 macOS 上运行)&quot;
# 先导出证书
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2&gt;/dev/null | \
  openssl x509 &gt; /tmp/cert-to-verify.pem
# 系统验证
security verify-cert -c /tmp/cert-to-verify.pem -p ssl -n ${DOMAIN} -L -v

echo &quot;==&gt; 检查 SAN&quot;
openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} &lt;/dev/null 2&gt;/dev/null | \
  openssl x509 -text -noout | grep -A2 &quot;Subject Alternative Name&quot;
</code></pre>
<hr/><h3 id="">最终结论</h3><p>证书问题最容易被&quot;我已经导入 CA 了&quot;这句话带偏。
真正决定一切的，是<strong>验证器的规则</strong>，而不是你的主观印象。</p><p>这一次的结论非常清晰：
<strong>用显式域名证书替代通配符，Chrome/Safari 立刻恢复安全状态。</strong></p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>tls</category>
            <category>https</category>
            <category>traefik</category>
            <category>ops</category>
        </item>
        <item>
            <title><![CDATA[自建服务部署与运维经验：一些踩坑与总结（模板）]]></title>
            <link>https://blog.cheverjohn.me/zh/self-hosting-deployment-notes</link>
            <guid>https://blog.cheverjohn.me/zh/self-hosting-deployment-notes</guid>
            <pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[部署经验总结（Traefik + Docker Compose + 域名访问 + 共享中间件） 这篇文章总结了本仓库这次连续部署多个服务（dockhand、vaultwarden、Checkmate、Miniflux(rss)）的经验、踩坑点、排查手法，以及我个人的部署习惯（也是本仓库后续建议遵循的“默认做法”）。 目标与底线 - 只允许域名访问：服务不对外暴露宿主机端口，避免 IP:端口 绕过认证/暴露管理面。 - 统一入口：对外只开 80/443，由 Traefik 统一反代、签发证书、加安全头。 - 共享基础中间件：MongoDB / Redis / PostgreSQL 这类通用组件集...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="traefik--docker-compose----">部署经验总结（Traefik + Docker Compose + 域名访问 + 共享中间件）</h2><p>这篇文章总结了本仓库这次连续部署多个服务（<code>dockhand</code>、<code>vaultwarden</code>、<code>Checkmate</code>、<code>Miniflux(rss)</code>）的经验、踩坑点、排查手法，以及我个人的部署习惯（也是本仓库后续建议遵循的“默认做法”）。</p><h2 id="">目标与底线</h2><ul><li><strong>只允许域名访问</strong>：服务不对外暴露宿主机端口，避免 <code>IP:端口</code> 绕过认证/暴露管理面。</li><li><strong>统一入口</strong>：对外只开 <code>80/443</code>，由 <code>Traefik</code> 统一反代、签发证书、加安全头。</li><li><strong>共享基础中间件</strong>：MongoDB / Redis / PostgreSQL 这类通用组件集中维护、统一复用，避免每个应用重复起一套浪费资源。</li><li><strong>Never break userspace（不破坏现有服务）</strong>：所有变更优先考虑是否会影响已在线服务（路由冲突、端口冲突、网络冲突、认证方式冲突）。</li></ul><h2 id="">目录组织与“我的部署习惯”</h2><ul><li><strong>每个应用一个目录</strong>：例如 <code>dockhand/</code>、<code>vaultwarden/</code>、<code>Checkmate/</code>、<code>v2/</code>（Miniflux）。</li><li><strong>每个应用目录下有一个可直接运行的 <code>docker-compose.yml</code></strong>：避免依赖上游仓库示例分散在 <code>contrib/</code> 里不好维护。</li><li><strong>共享中间件单独一个项目</strong>：新增 <code>middlewares/</code>，集中跑 <code>postgres</code>/<code>mongodb</code>/<code>redis</code>。
<ul><li>共享中间件只加入内部网络：对外<strong>不暴露任何端口</strong>。</li><li>应用通过 <code>external network</code> 方式加入共享网络获取 DB/缓存能力。</li></ul></li><li><strong>Traefik 统一网络</strong>：所有需要被 Traefik 访问的业务容器都加入 <code>traefik_proxy</code> 外部网络。</li><li><strong>不滥用文件路由</strong>：优先使用 Docker labels（docker provider）。file provider 只用于少数“非容器化目标”或特殊转发。</li></ul><h2 id="">标准部署模板（核心套路）</h2><h3 id="1">1）禁止宿主机端口暴露</h3><p>对外服务统一做法：</p><ul><li>用 <code>expose:</code>（仅容器网络可见）替代 <code>ports:</code>（宿主机可见）。</li><li>由 Traefik 通过 label 路由到容器端口。</li></ul><p>这能一次性解决：</p><ul><li><strong>端口冲突</strong>（例如宿主机 3000 已被别的服务占用）</li><li><strong>IP:端口绕过域名</strong>（安全风险）</li></ul><h3 id="2traefik-labels--tls--">2）Traefik labels（域名路由 + TLS + 安全头）</h3><p>常规标签集合（示意）：</p><ul><li><code>traefik.enable=true</code></li><li><code>traefik.http.routers.&lt;name&gt;.rule=Host(`miniflux.example.me`)</code></li><li><code>traefik.http.routers.&lt;name&gt;.entrypoints=websecure</code></li><li><code>traefik.http.routers.&lt;name&gt;.tls.certresolver=letsencrypt</code></li><li><code>traefik.http.routers.&lt;name&gt;.middlewares=security-headers@file</code></li><li><code>traefik.http.services.&lt;name&gt;.loadbalancer.server.port=&lt;container_port&gt;</code></li></ul><h3 id="3-traefik-">3）多网络容器必须指定 Traefik 网络</h3><p>当容器同时加入：</p><ul><li><code>traefik_proxy</code>（给 Traefik 用）</li><li><code>infra_backend</code>（给数据库/缓存用）</li></ul><p>Traefik 可能选错 IP（选到它不在的网络），导致 <strong>504 Gateway Timeout</strong>。</p><p>解决：在该容器 labels 增加：</p><ul><li><code>traefik.docker.network=traefik_proxy</code></li></ul><p>本次 Checkmate 就是典型案例：加了这条后，504 立即消失。</p><h2 id="middlewares-">共享中间件（middlewares 项目）</h2><h3 id="">设计原则</h3><ul><li><strong>高内聚低耦合</strong>：中间件只提供网络服务，不掺杂业务逻辑。</li><li><strong>最小权限</strong>：默认不对外暴露端口，只在 Docker 内部网络给应用用。</li><li><strong>数据持久化统一管理</strong>：卷在 <code>middlewares</code> 项目里集中维护（便于备份/迁移）。</li></ul><h3 id="">经验要点</h3><ul><li>共享网络使用固定名字（例如 <code>infra_backend</code>），各应用 <code>external: true</code> 加入。</li><li>应用连接字符串里使用共享网络的服务名（例如 <code>postgres</code>/<code>mongodb</code>）。</li><li>不同应用之间尽量使用不同的 DB / schema / user，避免互相污染数据。</li></ul><h2 id="">应用级注意点（本次踩坑记录）</h2><h3 id="dockhand">Dockhand（容器管理面，风险极高）</h3><ul><li><strong>必须禁用宿主机端口映射</strong>，只允许域名 + Traefik 访问。</li><li><strong>必须加认证</strong>：用 Traefik <code>basicAuth</code> 中间件保护。</li><li>不建议再叠加复杂逻辑，越简单越不容易出事故。</li></ul><h3 id="vaultwardenbitwarden-">Vaultwarden（Bitwarden 兼容服务）</h3><p>这是最容易“自作聪明搞坏客户端”的服务：</p><ul><li><strong>不要对整个站点加 BasicAuth</strong>：官方客户端/浏览器插件会直接不可用。</li><li>正确做法：仅保护 <code>/admin</code>（管理后台）：
<ul><li><code>/</code>、<code>/api</code>、<code>/identity</code> 等客户端路径必须无 BasicAuth 阻挡。</li></ul></li><li>遇到“插件能打开但登录失败”，先看：
<ul><li>Cloudflare 是否给 API 下发 challenge（见下文）</li><li><code>/api</code> 是否被客户端当作 ServerConfig（必要时在 Traefik 做精确重写到 <code>/api/config</code>）</li></ul></li></ul><h3 id="checkmate--mongo">Checkmate（多网络 + Mongo）</h3><ul><li>共享 Mongo 后，容器加入多个网络会触发 Traefik 选错网络：
<ul><li>直接导致 504</li><li>用 <code>traefik.docker.network=traefik_proxy</code> 一把解决</li></ul></li></ul><h3 id="minifluxminifluxexampleme">Miniflux（miniflux.example.me）</h3><ul><li>Miniflux 只支持 <strong>PostgreSQL</strong>，不需要 Mongo/Redis（除非你额外跑 RSSHub、Browserless 等）。</li><li><code>HEAD /</code> 返回 <code>405</code> 是可接受的（GET 正常即可），不要把它误判成服务挂了。</li></ul><h2 id="cloudflare-">Cloudflare 的坑：客户端/插件集体“无法登录”</h2><p>这次出现过两次同源问题：</p><ul><li>Vaultwarden 浏览器插件：请求被 Cloudflare challenge 拦截 → 返回 403</li><li>Reeder（Google Reader API）：请求被 Cloudflare challenge 拦截 → 返回 403</li></ul><p>共同特征：</p><ul><li>响应头出现：<code>server: cloudflare</code> + <code>cf-mitigated: challenge</code></li><li>请求根本到不了 Traefik/应用日志</li></ul><p>解决策略（推荐优先级）：</p><ul><li><strong>A：该子域名切 DNS only（灰云）</strong>：最稳，不需要客户端做任何配合。</li><li><strong>B：保留橙云，但对该 Host/路径跳过挑战</strong>：
<ul><li>至少放行：<code>/api</code>、<code>/identity</code>、<code>/reader/api/0</code>、<code>/accounts/ClientLogin</code> 等 API 路径</li></ul></li></ul><p>经验结论：</p><ul><li><strong>“能在浏览器里打开 ≠ 客户端能用”</strong>：浏览器能做人机验证，客户端做不了。</li></ul><h2 id="">排错手册（从快到慢）</h2><h3 id="1">1）先判定“请求到没到服务器”</h3><ul><li>如果 Cloudflare 403 challenge：请求没到服务器，先处理 Cloudflare/WAF。</li><li>如果 Traefik 404/504：请求到 Traefik，但路由/网络/后端不可达。</li><li>如果后端 4xx/5xx：请求到应用，查应用日志与配置。</li></ul><h3 id="2traefik-">2）Traefik 侧排查</h3><ul><li>看 access log：该 Host 的请求落到了哪个 router/service、返回码是多少。</li><li>多网络容器：优先怀疑 <code>traefik.docker.network</code> 缺失。</li><li>同域名多路由来源：优先怀疑 file-provider 与 docker-provider 冲突（删掉旧 file 路由）。</li></ul><h3 id="3">3）应用侧排查</h3><ul><li><code>docker logs &lt;container&gt;</code> 看启动是否成功、是否监听端口、是否连上 DB。</li><li>检查是否误用了 <code>ports:</code> 导致端口冲突/暴露。</li></ul><h2 id="">最终安全清单（上线前必做）</h2><ul><li><strong>宿主机无业务端口暴露</strong>（只保留 80/443）</li><li><strong>管理面必须有认证</strong>（dockhand、vaultwarden /admin 等）</li><li><strong>数据库/缓存不对公网暴露</strong></li><li><strong>Traefik 路由无冲突</strong>（同 Host 不重复定义 file-provider + docker-provider）</li><li><strong>Cloudflare 不对 API 下发 challenge</strong>（客户端/插件/第三方集成会挂）</li></ul></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>homelab</category>
            <category>self-hosting</category>
            <category>deployment</category>
            <category>devops</category>
            <category>security</category>
            <category>observability</category>
        </item>
        <item>
            <title><![CDATA[日常排查：Minisforum N100 小主机随机掉盘/卡顿失去响应（DNS 服务运行 1 天+）]]></title>
            <link>https://blog.cheverjohn.me/zh/minisforum-n100-dns-random-freeze</link>
            <guid>https://blog.cheverjohn.me/zh/minisforum-n100-dns-random-freeze</guid>
            <pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[这是一篇“日常排查记录”模板文章。我会尽量只写事实 + 证据链，避免凭感觉下结论。 TL;DR - 问题现象：DNS 服务跑着跑着失去响应；整机出现明显顿卡；偶发（疑似）掉盘/IO 异常。 - 触发特征：随机出现，通常在连续运行 1 天+ 后发生。 - 恢复方式：关机断电并静置一段时间后恢复正常。 - 当前结论（概率表达）：\\\\（例如：高概率与散热/供电/存储控制器复位相关）。 影响范围 - 直接影响：\\\\（例如：内网所有设备 DNS 解析超时/失败）。 - 间接影响：\\\\（例如：依赖域名解析的服务/容器全部异常）。 - 影响时段：\\\\（开始时间/持续多久/是否自动恢复）。 环...]]></description>
            <content:encoded><![CDATA[<article><div><blockquote><p>这是一篇“日常排查记录”模板文章。我会尽量只写事实 + 证据链，避免凭感觉下结论。</p></blockquote>
<h2 id="tldr">TL;DR</h2><ul><li><strong>问题现象</strong>：DNS 服务跑着跑着失去响应；整机出现明显顿卡；偶发（疑似）掉盘/IO 异常。</li><li><strong>触发特征</strong>：随机出现，通常在连续运行 <strong>1 天+</strong> 后发生。</li><li><strong>恢复方式</strong>：<strong>关机断电并静置一段时间</strong>后恢复正常。</li><li><strong>当前结论（概率表达）</strong>：____（例如：高概率与散热/供电/存储控制器复位相关）。</li></ul><h2 id="">影响范围</h2><ul><li><strong>直接影响</strong>：____（例如：内网所有设备 DNS 解析超时/失败）。</li><li><strong>间接影响</strong>：____（例如：依赖域名解析的服务/容器全部异常）。</li><li><strong>影响时段</strong>：____（开始时间/持续多久/是否自动恢复）。</li></ul><h2 id="">环境信息（可复现前提）</h2><h3 id="">硬件</h3><ul><li><strong>CPU</strong>：Intel® N100（4C/4T，6MB Cache，最高 3.4GHz）</li><li><strong>GPU</strong>：Intel® UHD Graphics</li><li><strong>内存</strong>：8GB LPDDR5（单通道，板载，4800MHz）</li><li><strong>存储</strong>：UFS 2.1 256G</li><li><strong>无线</strong>：Intel AX200/201（Wi‑Fi 6 / Bluetooth 5.2）</li><li><strong>有线</strong>：2.5G RJ45 ×1（支持 PoE IEEE 802.3at）</li><li><strong>接口</strong>：USB 3.2 Gen2 Type‑A ×2；USB 3.2 Gen2 Type‑C ×1（Alt DP/PD）；HDMI ×1</li><li><strong>电源</strong>：65W USB‑C Power Delivery 适配器</li></ul><h3 id="">软件</h3><ul><li><strong>实际运行系统</strong>：Linux（当前内核版本为 <code>6.8.12-5-pve</code>；出厂为 Windows 11 Home）</li><li><strong>内核版本（如 Linux）</strong>：<code>6.8.12-5-pve</code></li><li><strong>引导加载器（GRUB）</strong>：<code>grub2 2.06-13+pmx2</code></li><li><strong>DNS 软件</strong>：____（dnsmasq / unbound / AdGuard Home / Pi‑hole / 其它）</li><li><strong>部署方式</strong>：____（systemd / Docker / 其它）</li><li><strong>其它常驻服务</strong>：____</li><li><strong>日志策略</strong>：____（落盘频率/轮转策略/是否写入同一磁盘）</li></ul><h2 id="">问题描述（只写事实）</h2><ul><li><strong>首次出现时间</strong>：____</li><li><strong>复现频率</strong>：大约每 ____ 小时/天一次（随机）。</li><li><strong>故障时可观测症状</strong>：
<ul><li><strong>DNS</strong>：____（超时/拒绝/解析慢）</li><li><strong>系统</strong>：____（SSH/远程桌面是否可达？CPU/内存是否飙升？）</li><li><strong>存储</strong>：____（挂载点消失/只读/IO error/设备重置/掉盘）</li><li><strong>网络</strong>：____（ping 是否丢包？网卡是否 reset？）</li></ul></li><li><strong>故障后的恢复动作</strong>：____（关机静置 ____ 分钟后恢复；“重启”是否也能恢复：____）</li><li><strong>与负载的关系</strong>：____（高 QPS/日志写入/其它任务是否更容易触发）</li></ul><h2 id="">当场止血（按时间线）</h2><blockquote><p>目的：先恢复核心服务，避免数据损坏，再谈定位。</p></blockquote>
<ul><li><strong>T+0</strong>：____（例如：将上游 DNS 临时切换到路由器/公网 DNS）</li><li><strong>T+5m</strong>：____（例如：停止 DNS 服务/停止高 IO 服务）</li><li><strong>T+10m</strong>：____（例如：采集日志/抓取指标快照）</li><li><strong>结果</strong>：____（是否恢复？持续多久？）</li></ul><h2 id="">我关心的“数据结构”（证据链要对齐时间戳）</h2><ul><li><strong>一个时间线</strong>：故障开始时间、温度峰值、IO 错误、服务超时，必须能对齐。</li><li><strong>一个核心问题</strong>：是“DNS 服务死了”，还是“机器/存储子系统死了导致 DNS 表现为死”。</li></ul><h2 id="">假设清单（按优先级）</h2><ul><li><strong>H1：散热/过热导致保护或降频，继而触发系统不稳定</strong>
<ul><li>证据：____（温度曲线/thermal throttle log）</li></ul></li><li><strong>H2：供电不稳（PD/PoE/适配器）导致存储控制器或系统 reset</strong>
<ul><li>证据：____（kernel reset/电源事件/规律性）</li></ul></li><li><strong>H3：存储介质或控制器问题（UFS/NVMe/SATA）导致 IO 卡死或设备消失</strong>
<ul><li>证据：____（I/O error、timeout、device reset、SMART/health）</li></ul></li><li><strong>H4：软件层资源耗尽（FD/内存泄漏/日志写爆/IO 打满）</strong>
<ul><li>证据：____（ulimit、内存曲线、iowait、磁盘写入量）</li></ul></li><li><strong>H5：驱动/内核 bug（省电策略/设备电源管理）</strong>
<ul><li>证据：____（特定模块报错、升级/降级内核后变化）</li></ul></li></ul><h2 id="">数据采集（故障发生时我必须抓到什么）</h2><blockquote><p>不要靠“感觉像过热/像掉盘”，用日志说话。</p></blockquote>
<ul><li><strong>温度/风扇</strong>：____（例如 Linux：<code>sensors</code>、<code>/sys/class/thermal/</code>；Windows：HWiNFO）</li><li><strong>系统日志</strong>：____（Linux：<code>journalctl</code>；Windows：事件查看器）</li><li><strong>存储相关日志</strong>：____（I/O error、timeout、reset、file system remount read-only）</li><li><strong>资源指标</strong>：____（CPU、load、iowait、内存、swap、磁盘吞吐）</li><li><strong>DNS 指标</strong>：____（QPS、缓存命中、上游延迟、失败率）</li><li><strong>客户端验证</strong>：____（<code>dig/nslookup</code> 的失败表现）</li></ul><h2 id="-35-">关键证据（粘 3~5 段就够）</h2><ul><li><strong>证据 1（定性）</strong>：</li></ul><pre><code class="lang-text">[time] ____
</code></pre>
<ul><li><strong>证据 2（支持链路）</strong>：</li></ul><pre><code class="lang-text">[time] ____
</code></pre>
<ul><li><strong>证据 3（排除项）</strong>：</li></ul><pre><code class="lang-text">[time] ____
</code></pre>
<h2 id="----">排查过程（尝试 → 观察 → 结论）</h2><h3 id="-a">尝试 A：____</h3><ul><li><strong>改动</strong>：____</li><li><strong>观察</strong>：____</li><li><strong>结论</strong>：____（支持/否定哪条假设）</li></ul><h3 id="-b">尝试 B：____</h3><ul><li><strong>改动</strong>：____</li><li><strong>观察</strong>：____</li><li><strong>结论</strong>：____</li></ul><h3 id="-c">尝试 C：____</h3><ul><li><strong>改动</strong>：____</li><li><strong>观察</strong>：____</li><li><strong>结论</strong>：____</li></ul><h2 id="">结论（概率表达，不装确定）</h2><ul><li><strong>高概率主因</strong>：____</li><li><strong>次要因素</strong>：____</li><li><strong>我排除的方向</strong>：____（必须给证据）</li></ul><h2 id="-vs-">解决方案（短期止血 vs 长期修复）</h2><h3 id="">短期止血（马上能做）</h3><ul><li>____（例如：加强散热/开盖/外接风扇）</li><li>____（例如：限制功耗/关闭省电策略）</li><li>____（例如：降低日志落盘/开启轮转/把日志写到另一块盘）</li></ul><h3 id="">长期修复（一次到位）</h3><ul><li>____（例如：更换电源/更换存储/更新 BIOS/固件）</li><li>____（例如：调整机箱风道/加导热垫/改善安装位置）</li><li>____（例如：升级/更换系统与内核版本）</li></ul><h3 id="">验证标准</h3><ul><li><strong>连续运行</strong>：____ 天无复现</li><li><strong>温度上限</strong>：CPU ≤ ____°C；存储 ≤ ____°C</li><li><strong>日志</strong>：无 I/O error / reset / remount read-only</li></ul><h2 id="">复盘</h2><ul><li><strong>这次最值钱的证据</strong>：____</li><li><strong>下次更早做的事</strong>：____（例如：加监控、告警、故障自动抓日志）</li><li><strong>仍未解决的问题</strong>：____</li></ul><h2 id="-1921682212pve--lxc1921682219-1921682218minisforum">尝试方案：依赖 192.168.22.12（PVE 宿主）上的监控 LXC（192.168.22.19）持续监测 192.168.22.18（Minisforum）</h2><blockquote><p>目标：把“感觉像过热/像掉盘/像 DNS 挂了”变成一条可对齐时间戳的证据链：<strong>温度曲线 + IO/内核报错计数 + DNS 可用性探测 + 主机可达性</strong>。</p><p>注意：<strong>不要把账号密码写进任何配置文件/仓库</strong>。下面所有登录步骤都默认你手动输入密码或（更推荐）使用 SSH key。</p></blockquote>
<h3 id="22--ssh-">额外坑：<code>22</code> 端口通，但 <code>ssh</code> 仍然卡住/断线（跨网段更常见）</h3><p>这个坑很容易误导人：你看到 <code>nc</code> 显示 <code>22/tcp open</code>，就以为网络没问题；但 <code>ssh</code> 仍然会在握手阶段卡住，最后报超时或 <code>Broken pipe</code>。</p><h4 id="">场景（这次真实遇到的拓扑）</h4><ul><li><strong>客户端网段</strong>：<code>192.168.11.0/24</code></li><li><strong>目标主机</strong>：<code>192.168.22.12</code>（PVE）</li><li><strong>同网段跳板/对照</strong>：<code>192.168.22.18</code>（从这里 <code>ssh</code> 到 <code>.12</code> 通畅）</li><li><strong>同时发生的信号</strong>：DNS 服务（例如 <code>192.168.22.53:53</code>）出现超时（反查/解析失败会放大 SSH 的握手耗时）</li></ul><h4 id="">现象（典型表现）</h4><ul><li><code>nc -vz -G 2 192.168.22.12 22</code> 显示 <strong>succeeded</strong>（TCP 三次握手没问题）</li><li><code>ssh root@192.168.22.12</code> 卡住，随后出现：
<ul><li><code>ssh_dispatch_run_fatal: Connection to 192.168.22.12 port 22: Operation timed out</code></li><li>或 <code>Read from remote host 192.168.22.12: Operation timed out</code> / <code>client_loop: send disconnect: Broken pipe</code></li></ul></li></ul><h4 id="">快速判定（先把“端口通”细分）</h4><ol start="1"><li><strong>看 SSH banner 是否能立即返回</strong>（只测协议，不涉及认证）：</li></ol><pre><code class="lang-bash">nc -v 192.168.22.12 22
</code></pre>
<p>正常情况会快速看到类似 <code>SSH-2.0-OpenSSH_...</code>。如果<strong>连上但 banner 很久不出/不出</strong>，说明问题更像在 <strong>sshd/系统层</strong>（例如 DNS 反查阻塞、系统负载、网络策略导致后续数据不稳定），不是“单纯端口被挡”。</p><ol start="2"><li><strong>抓 SSH 卡在哪一步</strong>（留证据用）：</li></ol><pre><code class="lang-bash">ssh -vvv -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 root@192.168.22.12
</code></pre>
<h4 id="-dnsgssapi-">最小修复（优先消灭 DNS/GSSAPI 造成的特殊情况）</h4><p>如果你能从同网段主机（例如 <code>.22.18</code>）登录到 <code>.22.12</code>，优先在 <strong>服务端</strong>做这两个改动（对内网环境通常更稳、更“少坑”）：</p><ol start="1"><li>编辑 <code>.22.12</code> 的 <code>/etc/ssh/sshd_config</code>，确保存在：</li></ol><ul><li><code>UseDNS no</code></li><li><code>GSSAPIAuthentication no</code></li></ul><ol start="2"><li>校验并重启：</li></ol><pre><code class="lang-bash">sshd -t &amp;&amp; systemctl restart ssh
systemctl status ssh --no-pager
</code></pre>
<blockquote><p>直觉解释：当 DNS/反查不稳定时，<code>UseDNS yes</code> 可能让 SSH 在握手/审计阶段等待解析结果；把它关掉能让“特殊情况”消失。</p></blockquote>
<h4 id="">如果仍然不稳：按“跨网段三件套”继续排除</h4><ul><li><strong>回程路由是否正确</strong>（目标机必须知道怎么回 <code>192.168.11.0/24</code>）：</li></ul><pre><code class="lang-bash">ip route get 192.168.11.12
</code></pre>
<ul><li><strong>防火墙/ACL</strong>：除了放行 <code>22/tcp</code>，还要确保回程方向与状态跟踪规则一致（尤其是网关/NAT/策略路由）。</li><li><strong>MTU/分片问题</strong>：跨网段、隧道、PPPoE 场景容易出现“能握手但数据阶段不稳”；建议在 Linux 上用带 DF 的大包 <code>ping</code> 做一次验证（必要时先临时把 MSS/MTU 调小验证）。</li></ul><h3 id="">总体架构（最少但够用）</h3><ul><li><strong>192.168.22.19（监控端 LXC，运行在 192.168.22.12 这台 PVE 上）</strong>：Prometheus + Grafana + blackbox_exporter（可选 Pushgateway）</li><li><strong>192.168.22.18（被监控端）</strong>：node<em>exporter（主机指标）+（可选）smartctl</em>exporter（磁盘健康）+ 自定义 textfile 指标（内核/IO error 计数）</li></ul><p>这套东西的“数据结构”很简单：</p><ul><li><strong>时序指标（Prometheus）</strong>：每 15s 抓一次，故障前后至少覆盖 48 小时。</li><li><strong>主动探测（blackbox_exporter）</strong>：从 <code>.19</code> 主动 ping / tcp 探测 <code>.18/.54</code>，区分“机器死了”和“服务死了”。</li><li><strong>告警（可选）</strong>：故障发生时把时间戳钉死（邮件/Telegram/飞书随你，先把规则跑起来）。</li></ul><hr/><h3 id="-dprometheus--grafana--blackboxexporter">尝试 D：监控栈落地（Prometheus + Grafana + blackbox_exporter）</h3><h4 id="d0-">D0. 前置检查（网络与端口）</h4><p>在 <code>192.168.22.19</code> 上确认能到达 <code>192.168.22.18</code>：</p><pre><code class="lang-bash">ping 192.168.22.18
Test-NetConnection 192.168.22.18 -Port 22
</code></pre>
<p>后续会用到的端口（按默认值）：</p><ul><li><code>.18:9100</code> node_exporter</li><li><code>.18:9633</code> smartctl_exporter（可选）</li><li><code>.19:9090</code> Prometheus</li><li><code>.19:3000</code> Grafana</li><li><code>.19:9115</code> blackbox_exporter</li><li><code>.19:9091</code> Pushgateway（可选）</li></ul><blockquote><p>如果你有防火墙/安全组，先放行 <strong><code>.19 -&gt; .18</code> 的 9100/9633/22</strong>，以及 <strong><code>.19 -&gt; .54</code> 的 53/3000</strong>（只在内网开放就行）。</p></blockquote>
<h4 id="d1--1921682218--nodeexportersystemd-">D1. 在 192.168.22.18 部署 node_exporter（systemd 方式，最稳）</h4><p>在 <code>.18</code> 执行（版本号你可以换新，但别用“latest”，排查时要可追溯）：</p><pre><code class="lang-bash">export VER=&quot;1.7.0&quot;
curl -fsSL -o /tmp/node_exporter.tar.gz &quot;https://github.com/prometheus/node_exporter/releases/download/v${VER}/node_exporter-${VER}.linux-amd64.tar.gz&quot;
tar -C /tmp -xzf /tmp/node_exporter.tar.gz
install -m 0755 /tmp/node_exporter-${VER}.linux-amd64/node_exporter /usr/local/bin/node_exporter

useradd --system --no-create-home --shell /usr/sbin/nologin nodeexp || true
mkdir -p /var/lib/node_exporter/textfile_collector
chown -R nodeexp:nodeexp /var/lib/node_exporter

cat &gt;/etc/systemd/system/node_exporter.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Prometheus Node Exporter
After=network-online.target
Wants=network-online.target

[Service]
User=nodeexp
Group=nodeexp
ExecStart=/usr/local/bin/node_exporter \
  --web.listen-address=:9100 \
  --collector.textfile.directory=/var/lib/node_exporter/textfile_collector
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now node_exporter
systemctl status node_exporter --no-pager
</code></pre>
<p>验证（在 <code>.12</code> 或任意内网机器）：</p><pre><code class="lang-bash">curl -fsS http://192.168.22.18:9100/metrics | head
</code></pre>
<h4 id="d2-1921682218-io-">D2.（可选但强烈建议）在 192.168.22.18 增加“内核/IO 错误计数”自定义指标</h4><p>你要抓的是这种关键字：<code>I/O error|timeout|reset|remount read-only|EXT4-fs error|BTRFS error|blk_update_request</code></p><p>做法：用 node_exporter 的 <strong>textfile collector</strong> 每分钟吐一个计数指标（避免你事后翻日志翻到吐）。</p><pre><code class="lang-bash">cat &gt;/usr/local/bin/collect_kernel_error_metrics.sh &lt;&lt;&#x27;EOF&#x27;
#!/usr/bin/env bash
set -euo pipefail

OUT=&quot;/var/lib/node_exporter/textfile_collector/kernel_errors.prom&quot;
TMP=&quot;$(mktemp)&quot;

# Count kernel messages in last 10 minutes (adjust if needed)
COUNT=&quot;$(journalctl -k --since &quot;10 min ago&quot; --no-pager 2&gt;/dev/null | \
  grep -Eic &#x27;I/O error|timeout|reset|remount read-only|EXT4-fs error|BTRFS error|blk_update_request|nvme|ufs&#x27; || true)&quot;

NOW=&quot;$(date +%s)&quot;

{
  echo &quot;# HELP kernel_error_events_10m Number of kernel error-like events in last 10 minutes&quot;
  echo &quot;# TYPE kernel_error_events_10m gauge&quot;
  echo &quot;kernel_error_events_10m ${COUNT}&quot;
  echo &quot;# HELP kernel_error_scrape_time_seconds Last collect time&quot;
  echo &quot;# TYPE kernel_error_scrape_time_seconds gauge&quot;
  echo &quot;kernel_error_scrape_time_seconds ${NOW}&quot;
} &gt;&quot;${TMP}&quot;

mv &quot;${TMP}&quot; &quot;${OUT}&quot;
EOF

chmod +x /usr/local/bin/collect_kernel_error_metrics.sh

cat &gt;/etc/systemd/system/kernel-error-metrics.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Collect kernel error metrics for node_exporter textfile collector

[Service]
Type=oneshot
ExecStart=/usr/local/bin/collect_kernel_error_metrics.sh
EOF

cat &gt;/etc/systemd/system/kernel-error-metrics.timer &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Run kernel error metrics collector every 60s

[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
AccuracySec=1s

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now kernel-error-metrics.timer
systemctl list-timers --no-pager | grep kernel-error-metrics || true
</code></pre>
<p>验证：</p><pre><code class="lang-bash">curl -fsS http://192.168.22.18:9100/metrics | grep -E &#x27;^kernel_error_events_10m|^kernel_error_scrape_time_seconds&#x27; || true
</code></pre>
<h4 id="d3-1921682218--smartctlexporter">D3.（可选）在 192.168.22.18 部署 smartctl_exporter（磁盘健康）</h4><p>说明：如果你的“UFS 2.1”设备不支持 SMART/health，这一步可能拿不到有效数据；但做了不亏（能拿到就直接中大奖）。</p><pre><code class="lang-bash">apt-get update
apt-get install -y smartmontools

export VER=&quot;0.12.0&quot;
curl -fsSL -o /tmp/smartctl_exporter.tar.gz &quot;https://github.com/prometheus-community/smartctl_exporter/releases/download/v${VER}/smartctl_exporter-${VER}.linux-amd64.tar.gz&quot;
tar -C /tmp -xzf /tmp/smartctl_exporter.tar.gz
install -m 0755 /tmp/smartctl_exporter-${VER}.linux-amd64/smartctl_exporter /usr/local/bin/smartctl_exporter

useradd --system --no-create-home --shell /usr/sbin/nologin smartctl-exp || true

cat &gt;/etc/systemd/system/smartctl_exporter.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Prometheus Smartctl Exporter
After=network-online.target
Wants=network-online.target

[Service]
User=smartctl-exp
Group=smartctl-exp
ExecStart=/usr/local/bin/smartctl_exporter --web.listen-address=:9633
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now smartctl_exporter
</code></pre>
<p>验证：</p><pre><code class="lang-bash">curl -fsS http://192.168.22.18:9633/metrics | head
</code></pre>
<hr/><h4 id="d4--1921682212pve-lxc--prometheus--grafana--blackboxexporter">D4. 在 192.168.22.12（PVE）用 LXC 部署 Prometheus + Grafana + blackbox_exporter（推荐）</h4><blockquote><p>你说 <code>.12</code> 也是 PVE 主机，那就别在宿主机上堆 Docker 了：<strong>直接建一个 LXC 容器跑监控栈</strong>，数据/配置独立、可迁移、也更符合“排查期可控变更”的原则。</p></blockquote>
<h5 id="d41--pve12-debian-lxc-">D4.1 在 PVE（.12）创建一个 Debian LXC 容器</h5><p>你可以用 PVE Web UI 创建，也可以用命令行。下面是命令行示例（按你的环境替换 <code>&lt;CTID&gt;</code>、<code>&lt;GW&gt;</code>；监控 LXC IP 固定为 <code>192.168.22.19</code>）：</p><pre><code class="lang-bash"># 在 PVE 宿主机 192.168.22.12 执行
pct create &lt;CTID&gt; local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname monitoring \
  --cores 2 --memory 2048 --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.22.19/24,gw=&lt;GW&gt; \
  --unprivileged 1

# 让容器内 systemd 工作更顺滑
pct set &lt;CTID&gt; -features keyctl=1,nesting=1

pct start &lt;CTID&gt;
</code></pre>
<blockquote><p>这篇文章里我们把监控 LXC 的 IP 固定为：<code>192.168.22.19</code>。</p></blockquote>
<h5 id="d42--prometheussystemd">D4.2 在容器内安装 Prometheus（systemd）</h5><p>进入容器：</p><pre><code class="lang-bash">pct exec &lt;CTID&gt; -- bash
</code></pre>
<p>在容器内执行：</p><pre><code class="lang-bash">apt-get update
apt-get install -y ca-certificates curl tar

useradd --system --no-create-home --shell /usr/sbin/nologin prometheus || true
mkdir -p /etc/prometheus /var/lib/prometheus
chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus

export VER=&quot;2.54.1&quot;
curl -fsSL -o /tmp/prometheus.tar.gz &quot;https://github.com/prometheus/prometheus/releases/download/v${VER}/prometheus-${VER}.linux-amd64.tar.gz&quot;
tar -C /tmp -xzf /tmp/prometheus.tar.gz
install -m 0755 /tmp/prometheus-${VER}.linux-amd64/prometheus /usr/local/bin/prometheus
install -m 0755 /tmp/prometheus-${VER}.linux-amd64/promtool /usr/local/bin/promtool
cp -r /tmp/prometheus-${VER}.linux-amd64/consoles /etc/prometheus/
cp -r /tmp/prometheus-${VER}.linux-amd64/console_libraries /etc/prometheus/
chown -R prometheus:prometheus /etc/prometheus
</code></pre>
<p>创建 <code>/etc/prometheus/prometheus.yml</code>（抓 <code>.18</code> 的 exporter + 主动探测；DNS 用 <code>.54</code> 的 AdGuardDNS）：</p><pre><code class="lang-yaml">global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: &quot;minisforum-node&quot;
    static_configs:
      - targets: [&quot;192.168.22.18:9100&quot;]

  - job_name: &quot;minisforum-smartctl&quot;
    static_configs:
      - targets: [&quot;192.168.22.18:9633&quot;]

  - job_name: &quot;blackbox-icmp&quot;
    metrics_path: /probe
    params:
      module: [icmp]
    static_configs:
      - targets:
          - 192.168.22.18
          - 192.168.22.54
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 127.0.0.1:9115

  - job_name: &quot;blackbox-tcp&quot;
    metrics_path: /probe
    params:
      module: [tcp_connect]
    static_configs:
      - targets:
          - 192.168.22.18:22
          - 192.168.22.18:9100
          - 192.168.22.54:53
          - 192.168.22.54:3000
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 127.0.0.1:9115
</code></pre>
<p>创建 systemd 服务：</p><pre><code class="lang-bash">cat &gt;/etc/systemd/system/prometheus.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Prometheus
After=network-online.target
Wants=network-online.target

[Service]
User=prometheus
Group=prometheus
ExecStart=/usr/local/bin/prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --storage.tsdb.path=/var/lib/prometheus \
  --storage.tsdb.retention.time=30d
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now prometheus
systemctl status prometheus --no-pager
</code></pre>
<h5 id="d43--blackboxexportersystemd">D4.3 在容器内安装 blackbox_exporter（systemd）</h5><pre><code class="lang-bash">useradd --system --no-create-home --shell /usr/sbin/nologin blackbox || true
mkdir -p /etc/blackbox_exporter
chown -R blackbox:blackbox /etc/blackbox_exporter

export VER=&quot;0.25.0&quot;
curl -fsSL -o /tmp/blackbox.tar.gz &quot;https://github.com/prometheus/blackbox_exporter/releases/download/v${VER}/blackbox_exporter-${VER}.linux-amd64.tar.gz&quot;
tar -C /tmp -xzf /tmp/blackbox.tar.gz
install -m 0755 /tmp/blackbox_exporter-${VER}.linux-amd64/blackbox_exporter /usr/local/bin/blackbox_exporter

cat &gt;/etc/blackbox_exporter/blackbox.yml &lt;&lt;&#x27;EOF&#x27;
modules:
  icmp:
    prober: icmp
    timeout: 5s

  tcp_connect:
    prober: tcp
    timeout: 5s
EOF

chown -R blackbox:blackbox /etc/blackbox_exporter

cat &gt;/etc/systemd/system/blackbox_exporter.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Prometheus Blackbox Exporter
After=network-online.target
Wants=network-online.target

[Service]
User=blackbox
Group=blackbox
ExecStart=/usr/local/bin/blackbox_exporter \
  --config.file=/etc/blackbox_exporter/blackbox.yml \
  --web.listen-address=:9115
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now blackbox_exporter
systemctl status blackbox_exporter --no-pager
</code></pre>
<h5 id="d44--grafanasystemd">D4.4 在容器内安装 Grafana（systemd）</h5><blockquote><p><strong>不要</strong>把 Grafana 的 admin 密码写进任何配置文件/仓库。建议安装完后手动设置。</p></blockquote>
<pre><code class="lang-bash">apt-get install -y apt-transport-https software-properties-common wget gpg
mkdir -p /etc/apt/keyrings
wget -qO- https://apt.grafana.com/gpg.key | gpg --dearmor &gt;/etc/apt/keyrings/grafana.gpg
echo &quot;deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main&quot; &gt;/etc/apt/sources.list.d/grafana.list
apt-get update
apt-get install -y grafana

systemctl enable --now grafana-server
systemctl status grafana-server --no-pager
</code></pre>
<p>设置 admin 密码（在容器内执行）：</p><pre><code class="lang-bash">grafana-cli admin reset-admin-password
</code></pre>
<h5 id="d45-">D4.5 访问地址与最小验证</h5><ul><li>Prometheus：<code>http://192.168.22.19:9090</code></li><li>Grafana：<code>http://192.168.22.19:3000</code></li><li>blackbox_exporter：<code>http://192.168.22.19:9115</code></li></ul><p>最小验证：</p><pre><code class="lang-bash">curl -fsS &quot;http://192.168.22.19:9090/-/ready&quot;
curl -fsS &quot;http://192.168.22.19:9115/probe?module=icmp&amp;target=192.168.22.18&quot; | head
</code></pre>
<hr/><h4 id="d5-grafana--h1h5">D5. Grafana 面板与关键观察点（对应 H1~H5）</h4><p>你不需要“花里胡哨的大盘”。就盯下面这些就够了：</p><ul><li><strong>H1 过热/降频</strong>：
<ul><li>CPU 温度（如果可见）：<code>node_thermal_zone_temp</code> 或 <code>node_hwmon_temp_celsius</code></li><li>CPU load/iowait：<code>node_load1</code>、<code>rate(node_cpu_seconds_total{mode=&quot;iowait&quot;}[5m])</code></li></ul></li><li><strong>H2 供电/复位</strong>：
<ul><li>uptime/重启点：<code>node_boot_time_seconds</code>（突然变化 = reboot）</li><li>blackbox 探测：<code>probe_success{job=~&quot;blackbox-.*&quot;}</code></li></ul></li><li><strong>H3 存储/控制器异常</strong>：
<ul><li>关键：<code>kernel_error_events_10m</code>（你自己定义的）</li><li>IO time：<code>rate(node_disk_io_time_seconds_total[5m])</code></li><li>文件系统只读（如果发生）：<code>node_filesystem_readonly</code></li></ul></li><li><strong>H4 资源耗尽</strong>：
<ul><li>内存：<code>node_memory_MemAvailable_bytes</code></li><li>FD（如果启用）：<code>node_filefd_allocated</code>/<code>node_filefd_maximum</code>（视 exporter 版本）</li></ul></li><li><strong>H5 驱动/省电策略/网卡 reset</strong>：
<ul><li>网络错误：<code>rate(node_network_receive_errs_total[5m])</code>、<code>rate(node_network_transmit_errs_total[5m])</code></li><li>同样看 <code>kernel_error_events_10m</code> 的变化点</li></ul></li></ul><hr/><h4 id="d6-">D6. 告警规则（把时间戳钉死）</h4><p>你至少要有三条告警（其余都是锦上添花）：</p><ul><li><strong>机器不可达</strong>（icmp probe 失败）</li><li><strong>DNS 端口不可达</strong>（tcp probe 到 53 失败）</li><li><strong>内核/IO 错误突增</strong>（<code>kernel_error_events_10m</code> 超过阈值）</li></ul><blockquote><p>告警接收渠道先别纠结，先让 Prometheus “能触发告警”这件事成立；否则你复现那一刻还是会错过。</p></blockquote>
<hr/><h3 id="-e-1921682218--lxc--adguarddnsip--1921682254-dns-">尝试 E：在 192.168.22.18 用 LXC 部署 AdGuardDNS（IP 固定为 192.168.22.54）+ DNS 真实查询探测</h3><blockquote><p>目标：把 DNS 业务从宿主机里隔离出来，并且<strong>把 DNS 的 IP 固定到 <code>192.168.22.54</code></strong>，这样监控/告警的 target 不会因为容器漂移而失效。</p><p>重点：排查“宿主机是否先死”，所以 <strong>node<em>exporter / 自定义 IO 错误计数 / smartctl</em>exporter 仍然部署在宿主 <code>.18</code></strong>；DNS 服务放到容器里只是为了更可控地观测“业务侧”。</p></blockquote>
<h4 id="e0--pve18-adguarddns--lxc-1921682254">E0. 在 PVE（.18）创建 AdGuardDNS 的 LXC 容器（192.168.22.54）</h4><p>示例（按你的环境替换 <code>&lt;CTID&gt;</code>、<code>&lt;GW&gt;</code>）：</p><pre><code class="lang-bash"># 在 PVE 宿主机 192.168.22.18 执行
pct create &lt;CTID&gt; local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname adguarddns \
  --cores 1 --memory 1024 --swap 256 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.22.54/24,gw=&lt;GW&gt; \
  --unprivileged 1

pct set &lt;CTID&gt; -features keyctl=1,nesting=1
pct start &lt;CTID&gt;
</code></pre>
<h4 id="e1--adguard-home-adguarddns">E1. 在容器内安装 AdGuard Home（作为 AdGuardDNS）</h4><p>进入容器：</p><pre><code class="lang-bash">pct exec &lt;CTID&gt; -- bash
</code></pre>
<p>在容器内执行（固定版本，排查期可追溯）：</p><pre><code class="lang-bash">apt-get update
apt-get install -y ca-certificates curl tar

export VER=&quot;0.107.57&quot;
curl -fsSL -o /tmp/adguardhome.tar.gz &quot;https://github.com/AdguardTeam/AdGuardHome/releases/download/v${VER}/AdGuardHome_linux_amd64.tar.gz&quot;
tar -C /opt -xzf /tmp/adguardhome.tar.gz

/opt/AdGuardHome/AdGuardHome -s install
systemctl status AdGuardHome --no-pager
</code></pre>
<p>访问初始化页面（首次配置）：</p><ul><li>管理口：<code>http://192.168.22.54:3000</code></li><li>DNS：<code>192.168.22.54:53</code>（UDP/TCP）</li></ul><blockquote><p>你要把内网客户端 DNS 指向 <code>192.168.22.54</code>。如果你有 DHCP/路由器下发 DNS，就统一从那里改，别一个个设备手动改。</p></blockquote>
<h4 id="e1-dns--blackbox--dig-">E1. DNS 真实查询探测（不靠 blackbox 的“端口可达”，直接用 <code>dig</code> 验证解析链路）</h4><p>做法：在 <code>192.168.22.19</code>（监控端 LXC）定时执行 <code>dig</code>，把“成功/失败、延迟”推送到 Pushgateway。
这一步<strong>不依赖 AdGuard 管理口账号密码</strong>，可观测性更干净。</p><ol start="1"><li>在监控端（<code>.12</code> 的 monitoring LXC）里<strong>可选安装</strong> Pushgateway（systemd）：</li></ol><pre><code class="lang-bash">apt-get update
apt-get install -y ca-certificates curl tar

useradd --system --no-create-home --shell /usr/sbin/nologin pushgateway || true
mkdir -p /etc/pushgateway /var/lib/pushgateway
chown -R pushgateway:pushgateway /etc/pushgateway /var/lib/pushgateway

export VER=&quot;1.10.0&quot;
curl -fsSL -o /tmp/pushgateway.tar.gz &quot;https://github.com/prometheus/pushgateway/releases/download/v${VER}/pushgateway-${VER}.linux-amd64.tar.gz&quot;
tar -C /tmp -xzf /tmp/pushgateway.tar.gz
install -m 0755 /tmp/pushgateway-${VER}.linux-amd64/pushgateway /usr/local/bin/pushgateway

cat &gt;/etc/systemd/system/pushgateway.service &lt;&lt;&#x27;EOF&#x27;
[Unit]
Description=Prometheus Pushgateway
After=network-online.target
Wants=network-online.target

[Service]
User=pushgateway
Group=pushgateway
ExecStart=/usr/local/bin/pushgateway --web.listen-address=:9091
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now pushgateway
systemctl status pushgateway --no-pager
</code></pre>
<ol start="2"><li>Prometheus 增加一个 scrape（在同一台监控容器里就用 <code>127.0.0.1</code>）：</li></ol><pre><code class="lang-yaml">- job_name: &quot;pushgateway&quot;
  honor_labels: true
  static_configs:
    - targets: [&quot;127.0.0.1:9091&quot;]
</code></pre>
<ol start="3"><li>在监控端新增探测脚本（把 <code>&lt;DNS_IP&gt;</code> 固定为 <code>192.168.22.54</code>）：</li></ol><pre><code class="lang-bash">cat &gt;/opt/monitoring/dns_probe_push.sh &lt;&lt;&#x27;EOF&#x27;
#!/usr/bin/env bash
set -euo pipefail

DNS_IP=&quot;192.168.22.54&quot;
NAME=&quot;www.baidu.com&quot;
PUSH_URL=&quot;http://127.0.0.1:9091/metrics/job/dns_probe/instance/${DNS_IP}&quot;

OUT=&quot;$(dig +tries=1 +time=2 +stats @&quot;${DNS_IP}&quot; &quot;${NAME}&quot; A 2&gt;/dev/null || true)&quot;

# success=1 if we got an ANSWER section with at least one A record
if echo &quot;${OUT}&quot; | grep -qE &#x27;^;; ANSWER SECTION:&#x27;; then
  SUCCESS=1
else
  SUCCESS=0
fi

# Parse query time in ms from &quot;Query time: 12 msec&quot;
LAT_MS=&quot;$(echo &quot;${OUT}&quot; | awk -F&#x27;: &#x27; &#x27;/^;; Query time:/{print $2}&#x27; | awk &#x27;{print $1}&#x27; || true)&quot;
LAT_MS=&quot;${LAT_MS:-0}&quot;

cat &lt;&lt;METRICS | curl -fsS --data-binary @- &quot;${PUSH_URL}&quot; &gt;/dev/null
# TYPE dns_probe_success gauge
dns_probe_success ${SUCCESS}
# TYPE dns_probe_latency_ms gauge
dns_probe_latency_ms ${LAT_MS}
METRICS
EOF

chmod +x /opt/monitoring/dns_probe_push.sh
</code></pre>
<ol start="4"><li>用 cron/systemd timer 每 15s~60s 跑一次（排查期建议 15s）：</li></ol><pre><code class="lang-bash">crontab -e
# 每分钟 4 次
*/1 * * * * /opt/monitoring/dns_probe_push.sh
*/1 * * * * sleep 15; /opt/monitoring/dns_probe_push.sh
*/1 * * * * sleep 30; /opt/monitoring/dns_probe_push.sh
*/1 * * * * sleep 45; /opt/monitoring/dns_probe_push.sh
</code></pre>
<blockquote><p>这一步的意义：当你复现“DNS 不通”时，你能立刻看到是 <strong>解析延迟逐步升高</strong>、还是 <strong>直接成功率掉到 0</strong>，并且与宿主机 <code>iowait/错误计数</code> 对齐。</p></blockquote>
<h4 id="e2adguard-home-">E2.（可选）AdGuard Home 业务指标（需要管理口凭据，务必不要进仓库）</h4><p>如果你确实想看 AdGuard 自身的统计（blocked/allowed/qps/upstream latency），可以在 <code>.12</code> 上跑一个 AdGuard exporter，读取 AdGuard 的管理 API 再暴露成 Prometheus 指标。</p><p>关键原则：</p><ul><li><strong>凭据只放在 <code>.12</code> 本机的 root-only 文件里</strong>（例如 <code>/opt/monitoring/secrets/adguard.env</code>，<code>chmod 600</code>）</li><li><strong>不要写进本文仓库、更不要写进 compose 文件</strong></li></ul><p>（这里不强制指定 exporter，你用哪个就以其文档为准；排查阶段我更看重 E1 的 DNS 真实探测 + 宿主机指标。）</p><h3 id="">验证标准（这套监控是否“抓得住问题”）</h3><ul><li><strong>连续运行</strong>：至少 7 天（你说通常 1 天+ 触发，7 天才有统计意义）</li><li><strong>当复现发生时必须能回答</strong>：
<ul><li>先断的是 <strong>icmp</strong> 还是 <strong>tcp/53</strong>？</li><li><code>kernel_error_events_10m</code> 是否在故障前 0~10 分钟上升？</li><li>iowait 是否先飙升（<code>cpu iowait</code>/<code>disk io_time</code>）？</li><li><code>node_boot_time_seconds</code> 是否变化（是否实际重启）？</li><li><code>dns_probe_success/dns_probe_latency_ms</code> 在故障前是“逐步变坏”还是“瞬断”？</li></ul></li></ul></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>homelab</category>
            <category>dns</category>
            <category>troubleshooting</category>
            <category>linux</category>
            <category>hardware</category>
        </item>
        <item>
            <title><![CDATA[日常排查：PVE 跨 VLAN Ping 不通（默认网关指错 + OpenWrt LAN 区误开 NAT）]]></title>
            <link>https://blog.cheverjohn.me/zh/pve-openwrt-icmp-debug</link>
            <guid>https://blog.cheverjohn.me/zh/pve-openwrt-icmp-debug</guid>
            <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[这是一篇“日常排查记录”。只写可验证的事实、证据链和最小修复，不写玄学。 TL;DR - 现象：跨 VLAN ping 超时；目标机本地抓包却能看到 echo reply 已经发出。 - 根因 1（决定性）：PVE 刻意以 OpenWrt（192.168.22.13）作为默认网关做 QoS/流量处理，但 没有为其它内网 VLAN 配置显式路由，导致回程流量走默认路由“拐进” OpenWrt，主网关看不到 reply（典型非对称路径）。 - 根因 2（放大器）：OpenWrt 把 lan zone 开了 masq=1，把内网互访流量也 SNAT 改源成 192.168.22.13，让路径与会话变...]]></description>
            <content:encoded><![CDATA[<article><div><blockquote><p>这是一篇“日常排查记录”。只写可验证的事实、证据链和最小修复，不写玄学。</p></blockquote>
<h2 id="tldr">TL;DR</h2><ul><li><strong>现象</strong>：跨 VLAN <code>ping</code> 超时；目标机本地抓包却能看到 <code>echo reply</code> 已经发出。</li><li><strong>根因 1（决定性）</strong>：PVE <strong>刻意</strong>以 OpenWrt（<code>192.168.22.13</code>）作为默认网关做 QoS/流量处理，但 <strong>没有为其它内网 VLAN 配置显式路由</strong>，导致回程流量走默认路由“拐进” OpenWrt，主网关看不到 reply（典型非对称路径）。</li><li><strong>根因 2（放大器）</strong>：OpenWrt 把 <code>lan</code> zone 开了 <code>masq=1</code>，把内网互访流量也 SNAT 改源成 <code>192.168.22.13</code>，让路径与会话变得不可预期。</li><li><strong>修复</strong>：
<ul><li><strong>保留</strong> PVE 默认网关为 OpenWrt（<code>192.168.22.13</code>），但为内网网段（如 <code>192.168.11.0/24</code>、<code>192.168.183.0/24</code>）添加静态路由指向主网关（<code>192.168.22.1</code>）</li><li>OpenWrt 关闭 <code>lan</code> zone 的 <code>masq</code></li></ul></li><li><strong>预防</strong>：内网只路由不 NAT；如果默认网关必须指向旁路（QoS/代理），就把“内网路由”和“公网默认路由”明确拆开；用 <code>tcpdump</code> + <code>ip route get</code> 走 SOP。</li></ul><hr/><h2 id="">背景（拓扑与角色）</h2><p>家庭网络是多 VLAN：</p><ul><li><strong>Cloud Gateway Fiber（主网关）</strong>：负责三层路由与 VLAN 间转发
<ul><li>VLAN11：<code>192.168.11.0/24</code>（GW <code>192.168.11.1</code>）</li><li>VLAN22（PVE/实验）：<code>192.168.22.0/24</code>（GW <code>192.168.22.1</code>）</li><li>VLAN183：<code>192.168.183.0/24</code>（GW <code>192.168.183.1</code>）</li></ul></li><li><strong>OpenWrt（旁路/二级网关）</strong>：<code>192.168.22.13/24</code>（<code>br-lan</code>，<code>fw3 + iptables</code>，带旁路代理脚本）</li><li><strong>PVE</strong>：<code>192.168.22.12/24</code>（<code>vmbr0</code>）</li><li><strong>测试机</strong>：
<ul><li><code>192.168.183.235</code>（VLAN183）</li><li><code>192.168.11.29</code>（VLAN11）</li></ul></li></ul><p>简化数据路径（正常设计）应该是：</p><ul><li>VLAN183 ↔ VLAN22：都由 Fiber 做三层转发</li><li>OpenWrt 如果要做“上网/代理出口”，应该只在真正的 <code>wan</code> 出口做 NAT/代理，而不是改写内网互访流量</li></ul><hr/><h2 id="-shell-ascii">用 shell 画出拓扑与数据路径（ASCII）</h2><p>下面这个脚本不会改任何配置，只是把拓扑和两条关键路径打印出来，方便读者“看见数据怎么走”：</p><pre><code class="lang-bash">#!/usr/bin/env bash
set -euo pipefail

# Print the topology and the two paths (bad vs fixed).
cat &lt;&lt;&#x27;EOF&#x27;

                    +------------------------------+
                    |   Cloud Gateway Fiber (L3)   |
                    |   VLAN11: 192.168.11.1       |
                    |   VLAN22: 192.168.22.1       |
                    |   VLAN183: 192.168.183.1     |
                    +---------------+--------------+
                                    |
                                  VLAN22
                                    |
                   +----------------+----------------+
                   |                                 |
        +----------v-----------+          +----------v-----------+
        |  OpenWrt (side GW)   |          |        PVE           |
        |  192.168.22.13       |          |  192.168.22.12       |
        |  QoS / traffic mgmt  |          |  vmbr0               |
        +----------------------+          +----------------------+

Clients:
  VLAN183 host: 192.168.183.235
  VLAN11  host: 192.168.11.29

Problem #1 (before):
  192.168.183.235 -&gt; Fiber -&gt; PVE
  PVE reply -&gt; (default gw) OpenWrt -&gt; [NAT/proxy/unknown] -&gt; ???   (Fiber never sees the reply)

Fix idea:
  Keep PVE default gw = OpenWrt (for QoS),
  but route internal subnets via Fiber.

Problem #2 (amplifier):
  OpenWrt LAN masquerade (SNAT) rewrites east-west traffic,
  making internal routing/session behavior unpredictable.

EOF
</code></pre>
<h2 id="">事件与影响</h2><h3 id="-1vlan183-ping--pve">事件 1：VLAN183 ping 不通 PVE</h3><ul><li>发起端：<code>192.168.183.235</code> 执行 <code>ping 192.168.22.12</code> 一直超时</li><li>PVE 本机抓包能看到：
<ul><li><code>ICMP echo request</code> 到达</li><li><code>ICMP echo reply</code> 发出</li></ul></li><li>但在主网关 Fiber（<code>br22</code>/<code>br0</code>）抓包只看到 request，看不到 reply</li></ul><p><strong>这意味着</strong>：PVE 的 reply 根本没走回 Fiber —— 问题不是“主网关丢包”，而是“回程路径压根没经过主网关”。</p><h3 id="-2vlan11--ping--pve">事件 2：修复后，VLAN11 仍 ping 不通 PVE</h3><ul><li><code>192.168.11.29</code> ping 不通 <code>192.168.22.12</code></li><li>但可以 ping 通 <code>192.168.22.13</code>（OpenWrt）</li></ul><p>这暴露出第二个问题：OpenWrt 的 NAT 行为在破坏内网互访的可预期性。</p><hr/><h2 id="">排查过程（证据链）</h2><h3 id="1">1）先证明主网关本身可用</h3><p>在 Fiber 上验证：</p><ul><li>Fiber 能 ping 通 <code>192.168.183.235</code> 与 <code>192.168.22.12</code></li><li><code>ip route</code> 显示 <code>192.168.22.0/24</code> 与 <code>192.168.183.0/24</code> 为直连</li></ul><p>结论：Fiber 的基础三层与直连网段没问题。</p><h3 id="2reply-">2）逐跳抓包：定位“reply 消失在哪一跳”</h3><p>抓包观察到：</p><ul><li>PVE 本机：request/reply 都存在（说明 PVE 会回包）</li><li>Fiber 的 <code>br22/br0</code>：只有 request，没有 reply</li></ul><p>结论：reply 在到达 Fiber 之前就“拐走了”。</p><h3 id="3-ip-route-get-">3）用 <code>ip route get</code> 让路由决策自己说话</h3><p>在 PVE 上：</p><ul><li><code>ip route</code> 显示默认路由：<code>default via 192.168.22.13 dev vmbr0</code></li><li><code>ip route get 192.168.183.235</code> 显示：<code>via 192.168.22.13</code></li></ul><p><strong>关键结论（问题 1 根因）</strong>：PVE <strong>没有到其它内网 VLAN 的显式路由</strong>，因此对 <code>192.168.183.0/24</code> 的回包自然走默认路由交给 OpenWrt。只要 OpenWrt 这边出现 NAT/代理/错误路由中的任意一种，主网关就可能完全看不到 reply。</p><p>这就是典型的非对称路由：</p><ul><li>正向：<code>183 → Fiber → 22 → PVE</code>（正常）</li><li>回程：<code>PVE → OpenWrt → （NAT/代理/错误路由/黑盒）</code>（不可控）</li></ul><h3 id="4-openwrt--nat-">4）验证 OpenWrt 的防火墙实现与 NAT 配置</h3><p>为了排除“背后还有一套规则系统”的不确定性，确认 OpenWrt 是 <code>fw3 + iptables</code>（无 nftables），然后检查到：</p><ul><li><code>lan</code> zone 开启了 <code>masq=&#x27;1&#x27;</code></li></ul><p><strong>关键结论（问题 2 根因）</strong>：OpenWrt 把内网互访也做了 SNAT，源地址被改成 <code>192.168.22.13</code>，会让跨 VLAN 的会话与回程路径变得混乱（尤其叠加旁路代理/策略导流时）。</p><hr/><h2 id="">解决方法（最小改动）</h2><h3 id="-1-openwrt-fiber">修复 1：保留默认网关为 OpenWrt，但把“内网路由”显式指回主网关 Fiber</h3><p>需求前提：PVE 默认网关 <strong>必须</strong>是 <code>192.168.22.13</code>（OpenWrt），因为你要在 OpenWrt 上做 QoS/流量处理；那就别动默认路由，改成“内网网段走 Fiber，其他流量仍走 OpenWrt”。最简单且最可控的做法是给 PVE 加静态路由。</p><p>临时验证（示例只覆盖本次出现问题的 VLAN11/VLAN183，可按你的实际 VLAN 增加）：</p><pre><code class="lang-bash"># Keep default via OpenWrt for QoS / traffic management
# default via 192.168.22.13

# Route internal VLANs via the primary L3 gateway (Fiber)
ip route replace 192.168.183.0/24 via 192.168.22.1 dev vmbr0
ip route replace 192.168.11.0/24  via 192.168.22.1 dev vmbr0
</code></pre>
<p>验证思路：</p><ul><li><code>ip route get 192.168.183.235</code> 应该显示 <code>via 192.168.22.1</code></li><li>在 Fiber 的 <code>br22/br0</code> 上应该能抓到 <code>echo reply</code></li></ul><p>持久化（Proxmox 常见在 <code>/etc/network/interfaces</code> 的 <code>vmbr0</code>，用 <code>post-up</code> 写死最直接）：</p><pre><code class="lang-bash">post-up ip route replace 192.168.183.0/24 via 192.168.22.1 dev vmbr0
post-up ip route replace 192.168.11.0/24  via 192.168.22.1 dev vmbr0
post-down ip route del 192.168.183.0/24 via 192.168.22.1 dev vmbr0 || true
post-down ip route del 192.168.11.0/24  via 192.168.22.1 dev vmbr0 || true
</code></pre>
<p>验证：</p><ul><li>Fiber 的 <code>br22/br0</code> 能抓到 <code>echo reply</code></li><li><code>192.168.183.235</code> → <code>ping 192.168.22.12</code> 恢复</li></ul><h3 id="-2-openwrt--lan-zone-nat">修复 2：关闭 OpenWrt 的 <code>lan</code> zone NAT</h3><pre><code class="lang-bash">uci set firewall.@zone[0].masq=&#x27;0&#x27;
uci commit firewall
/etc/init.d/firewall restart
</code></pre>
<p>验证：</p><ul><li><code>192.168.11.29</code> → <code>ping 192.168.22.12</code> 恢复</li><li>内网互访源 IP 保持真实，后续排障和访问控制都更简单</li></ul><hr/><h2 id="">预防措施（别再制造“网络玄学”）</h2><ul><li><strong>内网之间只路由，不做 NAT</strong>
<ul><li><code>lan</code> / 内网 zones：<code>masq=0</code></li><li>只有真正的公网出口（<code>wan</code>）才 <code>masq=1</code></li></ul></li><li><strong>如果默认网关必须指向旁路（QoS/代理），就把路由拆清楚</strong>
<ul><li>默认路由可以指向 OpenWrt，但 <strong>所有内网网段必须有显式静态路由</strong> 指回主网关（或用策略路由按目的网段分流）</li><li>原则很简单：内网东西向流量走“可预期的三层”，不要让它掉进 NAT/代理黑盒</li></ul></li><li><strong>固定排障 SOP（Standard Operating Procedure：标准排查流程）</strong>
<ul><li>这句话的意思是：遇到类似“跨 VLAN ping 不通”的问题，<strong>不要靠猜</strong>，按同一套步骤用证据把问题钉死。</li><li><strong>第一步：先用抓包回答 3 个问题</strong>：request 到没到？目标回没回？reply 消失在哪一跳？
<ul><li>在目标机上：<code>tcpdump -ni vmbr0 icmp</code></li><li>在主网关上（目标 VLAN 口 / 发起 VLAN 口）：<code>tcpdump -ni br22 icmp</code>、<code>tcpdump -ni br0 icmp</code></li></ul></li><li><strong>第二步：用路由决策输出替代“想当然”</strong>
<ul><li>在 PVE 上：<code>ip route</code> + <code>ip route get 192.168.183.235</code></li><li>在 OpenWrt/Fiber 上：<code>ip route get 192.168.183.235</code>（确认下一跳到底是谁）</li></ul></li><li><strong>第三步：优先把数据路径变简单</strong>
<ul><li>内网互访先恢复为“纯路由、真实源地址、可预期回程”，再去叠加 QoS/代理/策略导流</li></ul></li></ul></li><li><strong>旁路代理要“明确排除内网网段”</strong>
<ul><li>内网网段不应被透明导入 VPN/隧道</li><li>先保证纯路由可用，再谈代理与加速</li></ul></li></ul><hr/><h2 id="">一句话教训</h2><p>当你觉得“ICMP 在搞幽灵”的时候，大概率不是协议栈坏了，而是你把默认网关/NAT 配成了黑盒。</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>homelab</category>
            <category>networking</category>
            <category>openwrt</category>
            <category>proxmox</category>
            <category>troubleshooting</category>
            <category>vlan</category>
        </item>
        <item>
            <title><![CDATA[个人如何正确使用 Claude]]></title>
            <link>https://blog.cheverjohn.me/zh/how-to-properly-use-claude-as-an-individual</link>
            <guid>https://blog.cheverjohn.me/zh/how-to-properly-use-claude-as-an-individual</guid>
            <pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[随着 AI 助手越来越深入地融入我们的日常工作流程，理解如何正确利用 Claude 这样的工具可以显著提升你的生产力和决策质量。这份全面指南将帮助你最大化从 Claude 获得的价值，同时保持良好的使用习惯和现实的期望。 理解 Claude 的核心优势 在深入具体用例之前，了解 Claude 擅长什么是至关重要的： 1. 文本分析和处理 Claude 可以快速分析、总结和从大量文本中提取洞察。无论你在处理研究论文、会议记录还是技术文档，Claude 都能帮助你识别关键点和模式。 2. 创意和技术写作 从起草邮件到编写代码，Claude 可以协助各种形式的内容创作，同时保持你的个人声音和风格偏好...]]></description>
            <content:encoded><![CDATA[<article><div><p>随着 AI 助手越来越深入地融入我们的日常工作流程，理解如何正确利用 Claude 这样的工具可以显著提升你的生产力和决策质量。这份全面指南将帮助你最大化从 Claude 获得的价值，同时保持良好的使用习惯和现实的期望。</p><h2 id="-claude-">理解 Claude 的核心优势</h2><p>在深入具体用例之前，了解 Claude 擅长什么是至关重要的：</p><h3 id="1-">1. 文本分析和处理</h3><p>Claude 可以快速分析、总结和从大量文本中提取洞察。无论你在处理研究论文、会议记录还是技术文档，Claude 都能帮助你识别关键点和模式。</p><h3 id="2-">2. 创意和技术写作</h3><p>从起草邮件到编写代码，Claude 可以协助各种形式的内容创作，同时保持你的个人声音和风格偏好。</p><h3 id="3-">3. 问题解决和研究</h3><p>Claude 可以帮助将复杂问题分解为可管理的组件，并提供寻找解决方案的结构化方法。</p><h3 id="4-">4. 学习和解释</h3><p>需要解释什么吗？Claude 可以根据你的专业水平和学习风格调整解释方式。</p><h2 id="">基本最佳实践</h2><h3 id="">从清晰、具体的提示开始</h3><p><strong>好的示例：</strong> &quot;帮我写一封专业邮件，礼貌地拒绝周四下午的会议请求，建议下周的替代时间，并保持合作的语调。&quot;</p><p><strong>糟糕示例：</strong> &quot;帮我写封邮件。&quot;</p><p>你提供的上下文和具体性越多，Claude 就越能针对你的需求定制回应。</p><h3 id="">迭代和完善</h3><p>不要期望第一次就得到完美的结果。使用后续提示来：</p><ul><li>要求澄清或扩展特定要点</li><li>请求不同的方法或风格</li><li>提供对什么有效、什么无效的反馈</li></ul><h3 id="">验证重要信息</h3><p>虽然 Claude 知识渊博，但总是要验证关键事实，特别是涉及：</p><ul><li>医疗或法律建议</li><li>财务决策</li><li>时事或快速变化的信息</li><li>技术规格或要求</li></ul><h2 id="">个人实用案例</h2><h3 id="">职业发展</h3><p><strong>邮件沟通：</strong></p><ul><li>起草适当语调的专业邮件</li><li>总结冗长的邮件链</li><li>为非技术受众翻译复杂的技术概念</li></ul><p><strong>文档创建：</strong></p><ul><li>创建结构化报告和演示文稿</li><li>撰写求职申请材料</li><li>制定项目计划和时间表</li></ul><p><strong>学习和技能发展：</strong></p><ul><li>获得复杂主题的解释</li><li>练习面试问题</li><li>学习新的编程语言或框架</li></ul><h3 id="">个人生产力</h3><p><strong>研究和规划：</strong></p><ul><li>比较重大购买的选项</li><li>规划旅行行程</li><li>研究个人感兴趣的话题</li></ul><p><strong>创意项目：</strong></p><ul><li>为兴趣爱好或副业项目头脑风暴</li><li>获得创意写作的反馈</li><li>规划活动或庆祝活动</li></ul><p><strong>日常生活管理：</strong></p><ul><li>创建膳食计划和购物清单</li><li>组织和优先处理任务</li><li>起草重要的个人信件</li></ul><h2 id="">高级技巧</h2><h3 id="">上下文管理</h3><p>在对话开始时提供相关背景信息：</p><ul><li>你的角色和专业水平</li><li>你工作的具体环境或约束</li><li>你的目标和成功标准</li></ul><h3 id="">思维链提示</h3><p>对于复杂问题，要求 Claude &quot;逐步思考&quot;或&quot;解释你的推理&quot;。这通常会产生更全面和准确的回应。</p><h3 id="">角色扮演场景</h3><p>要求 Claude 承担特定角色或观点：</p><ul><li>&quot;作为高级软件工程师审查我的代码&quot;</li><li>&quot;像不熟悉这个话题的人那样回应&quot;</li><li>&quot;从潜在客户的角度看&quot;</li></ul><h3 id="">模板创建</h3><p>为重复性任务开发可重用的提示：</p><ul><li>会议总结模板</li><li>邮件回复框架</li><li>问题解决方法论</li></ul><h2 id="claude-">Claude 不能做什么</h2><p>理解局限性对有效使用至关重要：</p><h3 id="">实时信息</h3><p>Claude 的训练有知识截止时间，无法访问当前网络信息或实时数据。</p><h3 id="">个人数据访问</h3><p>Claude 无法访问你的个人文件、邮件或私人信息，除非你在对话中明确分享。</p><h3 id="">执行操作</h3><p>Claude 无法直接在外部系统中执行操作、发送邮件或代表你进行购买。</p><h3 id="">替代人类判断</h3><p>虽然 Claude 可以提供见解和建议，但重要决策应始终涉及人类判断和专业知识。</p><h2 id="">维护隐私和安全</h2><h3 id="">信息分享</h3><ul><li>避免分享敏感个人信息（社保号、密码、财务详情）</li><li>谨慎处理专有或机密商业信息</li><li>在寻求特定场景帮助时考虑匿名化数据</li></ul><h3 id="">数据保留</h3><ul><li>了解平台的数据保留政策</li><li>不要依赖 Claude 在单独对话之间记住信息</li><li>保留重要见解或决策的自己记录</li></ul><h2 id="">建立有效工作流程</h2><h3 id="1-">1. 定义你的用例</h3><p>识别 Claude 可以为你的日常工作增加价值的具体、重复性任务。</p><h3 id="2-">2. 开发标准提示</h3><p>为你最常见的用例创建和完善提示模板。</p><h3 id="3-">3. 设定现实期望</h3><p>理解 Claude 是增强你能力的工具，而不是替代你的思考和判断。</p><h3 id="4-">4. 衡量影响</h3><p>跟踪 Claude 的使用如何影响你的生产力和工作质量。</p><h3 id="5-">5. 保持更新</h3><p>AI 能力发展迅速。及时了解新功能和最佳实践。</p><h2 id="">避免常见陷阱</h2><h3 id="">过度依赖</h3><p>不要在基本思考或决策制定上变得依赖 Claude。用它来增强你的能力，而不是替代它们。</p><h3 id="">提示懒惰</h3><p>避免模糊或最小化的提示。在清晰沟通上投入时间会产生更好的结果。</p><h3 id="">忽略上下文</h3><p>记住为复杂或专业主题提供充分的上下文。</p><h3 id="">假设完美</h3><p>总是审查和验证 Claude 的输出，特别是对于重要任务。</p><h2 id="">结论</h2><p>当明智和战略性地使用时，Claude 可以成为专业和个人环境中的强大盟友。成功的关键在于理解其优势和局限性，发展清晰的沟通模式，并保持适当的怀疑和验证实践。</p><p>从简单的用例开始，逐步建立你的技能，并始终记住 Claude 在增强你的人类智能而非试图替代它时最为有效。通过实践和正确的方法，你会发现 Claude 成为你个人和专业工具包中的宝贵工具。</p><p>记住：目标不是将 Claude 用于一切，而是以正确的方式将其用于正确的事情。</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>AI</category>
            <category>Claude</category>
            <category>productivity</category>
            <category>guide</category>
        </item>
        <item>
            <title><![CDATA[在这个 AI 时代，你真的会敲代码嘛？！！！]]></title>
            <link>https://blog.cheverjohn.me/zh/can-you-really-code</link>
            <guid>https://blog.cheverjohn.me/zh/can-you-really-code</guid>
            <pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[最近 AI 实在是太凶了，大家都沉迷于对 AI 发号施令，然后发现一大堆报错的代码被写出来。（事实上你可能自己也看不懂 AI 写的代码）。 当然懂 AI 的人，晓得使用 Prompt Engineering 嘛。不过我还是想巩固一下软件工程的基本功。 面向对象、设计原则、设计模式、编程规范、重构。 编程范式/编程风格 主流的编程范式/编程风格有三种： - 面向过程 - 面向对象 - 函数式编程 其中，面向对象这种编程风格又是其中最主流的。（现在比较流行的编程语言大部分都是面向对象编程语言）。 设计原则 解释定义：代码设计的经验总结。 原则一般听起来很抽象，定义描述比较模糊。 难点：需要掌握设计...]]></description>
            <content:encoded><![CDATA[<article><div><p>最近 AI 实在是太凶了，大家都沉迷于对 AI 发号施令，然后发现一大堆报错的代码被写出来。（事实上你可能自己也看不懂 AI 写的代码）。</p><p>当然懂 AI 的人，晓得使用 Prompt Engineering 嘛。不过我还是想巩固一下软件工程的基本功。</p><p><strong>面向对象、设计原则、设计模式、编程规范、重构。</strong></p><h2 id="">编程范式/编程风格</h2><p>主流的编程范式/编程风格有三种：</p><ul><li>面向过程</li><li>面向对象</li><li>函数式编程</li></ul><p>其中，面向对象这种编程风格又是其中最主流的。（现在比较流行的编程语言大部分都是面向对象编程语言）。</p><h2 id="">设计原则</h2><p>解释定义：代码设计的经验总结。</p><p>原则一般听起来很抽象，定义描述比较模糊。</p><p>难点：需要掌握<strong>设计初衷</strong>、能够<strong>解决哪些编程问题</strong>，有哪些<strong>应用场景</strong>。</p><p>常用的设计原则如下：</p><ul><li><p>SOLID 原则</p><blockquote><p>也许你会有疑问，为什么就只有 SOLID 原则会有五个额外的类似于子原则的东西。</p><p>咳咳，我也有这个问题，所以我查了，查到了。</p><p>SOLID 被称为面向对象设计的基石，是因为它提供了一套<strong>具体</strong>而非抽象的工具，用于解决软件开发中的实际问题。</p><p>每个子原则针对的是不同层面的设计缺陷。它之所以特殊，恰恰是因为它不是一个模糊的单一规则，而是五个具体、可操作的设计原则的组合</p></blockquote>
<ul><li>单一职责原则 (Single Responsibility Principle)</li><li>开闭原则 (Open/Closed Principle)</li><li>里氏替换原则 (Liskov Substitution Principle)</li><li>接口隔离原则 (Interface Segregation Principle)</li><li>依赖反转原则 (Dependency Inversion Principle)</li><li>简洁设计原则</li></ul></li><li><p>KISS原则 (Keep It Simple, Stupid)</p></li><li><p>DRY原则 (Don&#x27;t Repeat Yourself)</p></li><li><p>YAGNI原则 (You Aren&#x27;t Gonna Need It)</p></li><li><p>LOD 法则</p></li></ul><h2 id="">设计模式</h2><p>解释定义：针对于软件开发中经常遇到的一些设计问题，总结出来的一套解决方案或者设计思路。</p><p><strong>大部分设计模式要解决的都是代码的可扩展性问题。</strong></p><p>难点：了解它们都能解决哪些问题，掌握典型的应用场景，并且懂得不过度应用。</p><p>有哪些：经典的设计模式有 23 种。</p><p>随着编程语言的演进，</p><p>一些设计模式（比如 Singleton）也随之过时，甚至成了反模式；</p><p>一些则被内置在编程语言中（比如 Iterator）；</p><p>还有一些新的设计模式诞生（比如 Monostate）。</p><p>23 种经典的设计模式，可以分为三大类：创建型、结构型、行为型。</p><h3 id="">创建型</h3><p>常用的有：单例模式、工厂模式（工厂方法和抽象工厂）、建造者模式。</p><p>不常用的有：原型模式。</p><h3 id="">结构型</h3><p>常用的有：代理模式、桥接模式、装饰者模式、适配器模式。</p><p>不常用的有：门面模式、组合模式、享元模式。</p><h3 id="">行为型</h3><p>常用的有：观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式。</p><p>不常用的有：访问者模式、备忘录模式、命令模式、解释器模式、中介模式。</p><h2 id="">编程规范</h2><p>定义解释：主要解决的是代码的可读性问题。相对于设计原则、设计模式，<strong>更加具体、更加偏重代码细节</strong>。即便你可能对设计原则不熟悉、对设计原则不了解，最起码掌握基本的编码规范。</p><p>比如，如何给变量、类、函数命名，如何写代码注释，函数不宜过长、参数不能过多等。</p><p>这一块有很多经典的书可以去看就完事了，《重构》、《代码大全》、《代码整洁之道》等。</p><p>这一块每条编码规范都很简单、明确，记一下就行了，只需要照着来就可以。不像设计原则，需要融入很多个人的理解和思考。</p><h2 id="">重构</h2><p>只要工程一直在进行，这个项目一直有人，那么软件就会需要持续迭代，那么新的功能需求一定会推动着之前的需求进行代码重构，这是保证代码质量不下降的有效手段。有效避免代码腐化到无可救药的地步。</p><p>而重构的工具，就是之前提到的所有的那些，编程范式、设计原则、设计模式、编程规范。</p><p>虽然使用设计模式可以提高代码的可扩展性，但过度不恰当地使用，也会增加代码的复杂度，影响代码的可读性。</p><p>在开发初期，除非特别必须，我们一定不要过度设计，应用复杂的设计模式。</p><p><strong>而是当代码出现问题的时候，我们再针对问题，应用原则和模式进行重构。</strong></p><p>这样就能有效避免前期的过度设计。</p><h3 id="">务必掌握的知识点</h3><p>如下：</p><ul><li>重构的目的（why）、对象（what）、时机（when）、方法（how）；</li><li>保证重构不出错的技术手段：单元测试和代码的可测试性；</li><li>两种不同规模的重构：大重构（大规模高层次）和小重构（小规模低层次）。</li></ul><h2 id="">五者之间的联系</h2><p>关于面向对象、设计原则、设计模式、编程规范和代码重构，这五者的关系如下：</p><ul><li>面向对象编程因为其丰富的特性（封装、抽象、继承、多态），可以实现很多复杂的设计思路，是很多设计原则、设计模式等<strong>编码实现的基础</strong>。</li><li>设计原则是指导我们代码设计的一些经验总结，<strong>对于某些场景下，是否应该应用某种设计模式，具有指导意义</strong>。比如，“开闭原则” 是很多设计模式（策略、模版等）的<strong>指导原则</strong>。</li><li>设计模式是针对软件<strong>开发中经常遇到的一些设计问题，总结出来的一套解决方案或者设计思路</strong>。应用设计模式的主要目的是提高代码的可扩展性。从抽象程度上来讲，设计原则比设计模式更抽象。设计模式更加具体、更加可执行。</li><li>编程规范主要解决的是代码的<strong>可读性问题</strong>。编程规范相对于设计原则、设计模式，<strong>更加具体、更加偏重代码细节、更加能落地</strong>。持续的小重构依赖的理论基础主要就是编程规范。</li></ul><p>事实上这篇文章主要的作用就是，为了编写高质量代码这一件事。当追本溯源，之后很多事情怎么做，代码怎么实现，就清楚了。</p><pre><code class="lang-mermaid">mindmap
  root((编写高质量代码))
    面向对象
      封装、抽象、继承、多态
      面向对象编程 VS 面向过程编程
      面向对象分析、设计、编程
      接口 VS 抽象类
      基于接口而非实现编程
      多用组合少用继承
      贫血模型和充血模型
    设计原则
      SOLID 原则
        SRP 单一职责
        OCP 开闭
        LSP 里氏替换
        ISP 接口隔离
        DIP 依赖倒置
      其他
        DRY 原则
        KISS 原则
        YAGNI 原则
        LOD 法则
    编程规范
      20 条最快速改善代码质量的编程规范
    代码重构
      目的、对象、时机、方法
      单元测试与代码可测试性
      大重构（大规模高层次）
      小重构（小规模低层次）
    设计模式
      创建型
        常用
          单例模式
          工厂模式（工厂方法与抽象工厂）
          建造者模式
        不常用
          原型模式
      结构型
        常用
          代理模式
          桥接模式
          装饰者模式
          适配器模式
        不常用
          门面模式
          组合模式
          享元模式
      行为型
        常用
          观察者模式
          模板模式
          策略模式
          责任链模式
          迭代器模式
          状态模式
        不常用
          访问者模式
          备忘录模式
          命令模式
          解释器模式
          中介者模式
</code></pre>
<p><img alt="code-knowledge-graph_zh.png" src="code-knowledge-graph_zh.png"/></p><pre><code class="lang-mermaid">mindmap
  root((编写高质量代码))
    面向对象
      封装、抽象、继承、多态
      面向对象编程 VS 面向过程编程
      面向对象分析、设计、编程
      接口 VS 抽象类
      基于接口而非实现编程
      多用组合少用继承
      贫血模型和充血模型
    设计原则
      SOLID 原则
        SRP 单一职责
        OCP 开闭
        LSP 里氏替换
        ISP 接口隔离
        DIP 依赖倒置
      其他
        DRY 原则
        KISS 原则
        YAGNI 原则
        LOD 法则
    编程规范
      20 条最快速改善代码质量的编程规范
    代码重构
      目的、对象、时机、方法
      单元测试与代码可测试性
      大重构（大规模高层次）
      小重构（小规模低层次）
    设计模式
      创建型
        常用
          单例模式
          工厂模式（工厂方法与抽象工厂）
          建造者模式
        不常用
          原型模式
      结构型
        常用
          代理模式
          桥接模式
          装饰者模式
          适配器模式
        不常用
          门面模式
          组合模式
          享元模式
      行为型
        常用
          观察者模式
          模板模式
          策略模式
          责任链模式
          迭代器模式
          状态模式
        不常用
          访问者模式
          备忘录模式
          命令模式
          解释器模式
          中介者模式
</code></pre>
<p>ps：再附上一大堆关键词，可以慢慢看。</p><p>系统架构原则
高内聚低耦合原则
最小知识原则（迪米特法则）
组合优于继承原则
关注点分离原则
契约式设计原则
云原生设计原则
弹性设计原则
可观测性原则
不可变基础设施原则
服务自治原则
声明式配置原则
安全设计原则
纵深防御原则
最小权限原则
安全默认配置原则
职责分离原则
失效安全原则</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>code</category>
        </item>
        <item>
            <title><![CDATA[老系统的性能优化改造]]></title>
            <link>https://blog.cheverjohn.me/zh/performance-optimization-of-old-systems</link>
            <guid>https://blog.cheverjohn.me/zh/performance-optimization-of-old-systems</guid>
            <pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[老系统的在最开始创建、编写代码创造出来的时候，最终系统的性能和可维护性，完全取决于编码者的水平。 当你接手的系统，遇到下面这些情况的时候，这篇文章或许可以给你一点帮助： 1. 没有监控 2. 没有告警 3. 没有业务成功率 4. 乐观锁频繁失败 5. 系统响应时间黑盒（是的，你的业务方，使用者只是一味地跟你讲，系统很慢，但是具体不知道满在哪里） 6. 日志查问题很复杂 是的，上面就是我一个月以来遇到的备儿头疼的问题。 一个月之期已到，所以我开始操刀，对这套系统进行一系列的性能优化了。 首先，要对系统的整体情况摸清除，所谓优化，你至少知道你自己优化了个啥。业务方也应该知道自己使用的系统到底慢在了...]]></description>
            <content:encoded><![CDATA[<article><div>
<p>老系统的在最开始创建、编写代码创造出来的时候，最终系统的性能和可维护性，完全取决于编码者的水平。</p><p>当你接手的系统，遇到下面这些情况的时候，这篇文章或许可以给你一点帮助：</p><ol start="1"><li>没有监控</li><li>没有告警</li><li>没有业务成功率</li><li>乐观锁频繁失败</li><li>系统响应时间黑盒（是的，你的业务方，使用者只是一味地跟你讲，系统很慢，但是具体不知道满在哪里）</li><li>日志查问题很复杂</li></ol><p>是的，上面就是我一个月以来遇到的备儿头疼的问题。</p><p>一个月之期已到，所以我开始操刀，对这套系统进行一系列的性能优化了。</p><p>首先，要对系统的整体情况摸清除，所谓优化，你至少知道你自己优化了个啥。业务方也应该知道自己使用的系统到底慢在了哪里。</p><p>所以你需要做的事情就是，<strong>制定系统的基线</strong>。</p><h2 id="">制定系统的基线</h2><p>系统的基线，是对系统在正常运行状态下各项关键性能指标的量化描述。</p><p>它是系统性能优化的起点，也是衡量优化效果的标准参照。简单来说，系统基线就是你的系统的&quot;健康体检报告&quot;，记录了系统在各种条件下的性能表现。（<a href="https://zuplo.com/blog/2025/02/28/improving-api-performance-in-legacy-systems">1</a>，<a href="https://dzone.com/articles/optimizing-legacy-systems-through-scalable-architectures">2</a>）</p><h3 id="">系统基线包含以下这些</h3><p>一个完整的系统基线应该包括以下方面：</p><h4 id="">性能指标</h4><ul><li>响应时间（平均、P95、P99）</li><li>吞吐量（QPS/TPS）</li><li>延迟（Latency）</li><li>并发处理能力</li></ul><h4 id=""><strong>资源使用情况</strong></h4><ul><li><strong>CPU 使用率</strong></li><li><strong>内存占用及分配情况</strong></li><li><strong>磁盘 I/O</strong></li><li><strong>网络 I/O</strong></li><li><strong>连接池使用状况</strong></li></ul><h4 id="">业务指标</h4><ul><li>业务成功率</li><li>错误率</li><li>关键业务流程的完成时间</li></ul><h4 id="">系统稳定性指标</h4><ul><li><p>系统平均无故障时间（MTBF）</p></li><li>系统崩溃恢复时间（MTTR）</li><li>锁竞争情况（如你提到的乐观锁失败率）</li></ul></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Performance Optimization</category>
            <category>System Design</category>
        </item>
        <item>
            <title><![CDATA[Google MapReduce]]></title>
            <link>https://blog.cheverjohn.me/zh/google-map-reduce</link>
            <guid>https://blog.cheverjohn.me/zh/google-map-reduce</guid>
            <pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[这不就机会来了，乘着五一假期，好好解读一下这个最著名的论文之一，《MapReduce: Simplified Data Processing on Large Clusters》。 当然这篇文章我还受 “木鸟杂记” 的 文章 影响很深，所以我的这篇文章纯粹是两篇文章的结合 + 我自己的一些思想。 之前一直听组里的老大哥说，MapReduce 分布式有多么厉害，那今天机会就来了～ Introduction MapReduce 在论文中其实是一个概念。但是在另外一种情况下，它也可以是一种编程模型，也可以是支持该模型的一种分布式系统实现。当然我找到一篇文章[1]把这个概念解释的更好，如下： MapR...]]></description>
            <content:encoded><![CDATA[<article><div><p>这不就机会来了，乘着五一假期，好好解读一下这个最著名的论文之一，《MapReduce: Simplified Data Processing on Large Clusters》。</p><p>当然这篇文章我还受 “木鸟杂记” 的 <a href="https://www.qtmuniao.com/2019/04/30/map-reduce/">文章</a> <strong>影响很深</strong>，所以我的这篇文章纯粹是两篇文章的结合 + 我自己的一些思想。</p><p><img alt="MR执行流" src="The-Whole-Arch.png"/></p><p>之前一直听组里的老大哥说，MapReduce 分布式有多么厉害，那今天机会就来了～</p><h2 id="introduction">Introduction</h2><p>MapReduce 在论文中其实是一个概念。但是在另外一种情况下，它也可以是一种编程模型，也可以是支持该模型的一种分布式系统实现。当然我找到一篇文章<sup>[1]</sup>把这个概念解释的更好，如下：</p><blockquote><p>MapReduce 是谷歌 2004 年（Google 内部是从 03 年写出第一个版本）发表的论文里提出的一个概念。</p><p>在 Google 的语境里，MapReduce 既是一种编程模型，也是支持该模型的一种分布式系统实现。它的提出，让没有分布式系统背景的开发者，也能较轻松的利用大规模集群以高吞吐量的方式来处理海量数据。</p></blockquote>
<p>这篇文章<sup>[1]</sup>还有一句话解释了应用这项技术的解决问题思路：<strong>找到需求的痛点（如海量索引如何维护，更新和排名），对处理关键流程进行高阶抽象（分片 Map，按需 Reduce），以进行高效的系统实现（所谓量体裁衣）。</strong></p><p><em>而在这其中，如何找到一个合适的计算抽象，是最难的部分，既要对需求有直觉般的了解，又要具有极高的计算机科学素养</em></p><p>上面 👆 这句话还是出自于引用 “木鸟杂记” 的<a href="https://www.qtmuniao.com/2019/04/30/map-reduce/">文章</a>。</p><p>我们回到论文，其实可以发现，在论文的第一页纸，Google 大佬就说清楚了这是个啥。</p><blockquote><p>As a reaction to this complexity, we designed a new abstraction that allows us to express the simple computations we were trying to perform but hides the messy details of parallelization, fault-tolerance, data distribution and load balancing in a library.</p><p>意思是我们抽象了一个东西用来表达一种计算方式。这可以隐藏很多概念性的东西（并行化、容错性、数据分布和负载均衡）。</p><p>这种东西就是起源于 Lisp 和许多其他函数式语言中的 map 和 reduce 原语（primitives）。</p><p>We realized that most of our computations involved applying a map operation to each logical “record” in our input in order to compute a set of intermediate key/value pairs, and then applying a reduce operation to all the values that shared the same key, in order to combine the derived data appropriately.</p><p>我们的大部分计算，基本上都涉及到对输入中的每个逻辑 Record 应用 map 操作，以计算其中一组中间 key/val pair，然后对拥有相同的 key 的所有值应用 reduce 操作，以一种适当地组合导出数据。</p><p>Our use of a functional model with userspecified map and reduce operations allows us to parallelize large computations easily and to use re-execution as the primary mechanism for fault tolerance.</p><p>我们使用用户指定的 map 和 reduce 操作的 func module。这样就可以实现并行化大型计算。</p></blockquote>
<p>我发现最后一句话很有意思，</p><blockquote><p>use re-execution as the primary mechanism for fault tolerance.</p></blockquote>
<p>使用 “重新执行/re-execution” 作为容错的主要机制。</p><p>OK，这篇论文的 Abstract 内容来咯：</p><ol start="1"><li>Section 1 就是上面的这个 Introduction, <a href="#Introduction">Introduction</a>;</li><li>Section 2 describes the basic programming model and gives several examples, [Programming Model](#Programming Model);</li><li>Section 3 describes an implementation of the MapReduce interface tailored towards our cluster-based computing environment, <a href="#Implementation">Implementation</a>;
基于集群计算环境定制的 MapReduce 接口的实现。</li><li>Section 4 describes several refinements of the programming model that we have found useful;
几个编程模型的改进。</li><li>Section 5 has performance measurements of our implementation for a variety of tasks;
实现各种任务的性能测量。</li><li>Section 6 explores the use of MapReduce within Google including our experiences in using it as the basis for a rewrite of our production indexing system;
MapReduce 在 Google 中的应用。</li><li>Section 7 discusses related and future work;</li></ol><h2 id="programming-model">Programming Model</h2><p>Map 的 Key 是正常的 Key，value 这边就假想为一个字符串数组吧。</p><p>这个 MapReduce，通俗来讲，就是两个函数，map 函数和 reduce 函数。</p><p>Map 函数接收一个输入对，并生成一组 intermediate key/value，然后 MapReduce library 将所有与同一 key 关联 intermediate value 组合在一起。</p><h3 id="example">Example</h3><p>下面是一段伪代码，祖父级别，来自于原论文：</p><pre><code class="lang-java">map(String key, String value):
    // key: document name
    // value: document contents
    for each word w in value:
      EmitIntermediate(w, &quot;1&quot;);

reduce(String key, Iterator values):
    // key: a word
    // values: a list of counts
    int result = 0;
    for each v in values:
      result += ParseInt(v);
    Emit(AsString(result));
</code></pre>
<p>上面这是一个经典的 MapReduce 单词计数（Word Count）实现，这是 MapReduce 编程模型中最常见的示例之一。</p><ul><li>key：文档名称</li><li>value：文档的完整内容（文本字符串）</li></ul><p>其中的 EmitIntermediate 表示的是 MapReduce 框架提供的用于输出中间键值对（intermediate key/val）。每次调用这个函数，就会产生一个键值对：（单词，“1”），表示该单词出现了一次。</p><p>举个例子，处理文档内容 &quot;hello world hello&quot;：</p><ul><li>第一个单词 &quot;hello&quot; → EmitIntermediate(&quot;hello&quot;, &quot;1&quot;)</li><li>第二个单词 &quot;world&quot; → EmitIntermediate(&quot;world&quot;, &quot;1&quot;)</li><li>第三个单词 &quot;hello&quot; → EmitIntermediate(&quot;hello&quot;, &quot;1&quot;)</li></ul><p>Map 阶段输出的中间结果：</p><pre><code class="lang-shell">(&quot;hello&quot;, &quot;1&quot;)
(&quot;world&quot;, &quot;1&quot;)
(&quot;hello&quot;, &quot;1&quot;)
</code></pre>
<h3 id="shuffle-stage">Shuffle Stage（框架自动完成）</h3><p>在 Map 和 Reduce 之间，MapReduce 框架自动执行 Shuffle 操作：</p><ol start="1"><li>收集所有的 mapper 的输出；</li><li>按键（单词）排序；</li><li>将具有相同 键 的所有值分组在一起；</li></ol><p>所以上面的示例经过 Shuffle 之后：</p><pre><code class="lang-shell">(&quot;hello&quot;, [&quot;1&quot;, &quot;1&quot;])
(&quot;world&quot;, [&quot;1&quot;])
</code></pre>
<h3 id="reduce-">Reduce 函数</h3><p>Shuffle 之后就是 Reduce 函数的工作了，上面的伪代码的作用其实就是累加，不多做解释。</p><h3 id="">完整执行流程</h3><p>下面是一个更大的示例，展示整个 MapReduce 执行流程：</p><p><strong>假设有三个文档</strong>：</p><ul><li>document1.txt: &quot;hello world&quot;</li><li>document2.txt: &quot;hello mapreduce&quot;</li><li>document3.txt: &quot;mapreduce world example&quot;</li></ul><p><strong>Map</strong> 阶段（并行执行）</p><p>Mapper 1 处理 document1.txt:</p><pre><code class="lang-shell">EmitIntermediate(&quot;hello&quot;, &quot;1&quot;)
EmitIntermediate(&quot;world&quot;, &quot;1&quot;)
</code></pre>
<p>Mapper 2 处理 document2.txt:</p><pre><code class="lang-shell">EmitIntermediate(&quot;hello&quot;, &quot;1&quot;)
EmitIntermediate(&quot;mapreduce&quot;, &quot;1&quot;)
</code></pre>
<p>Mapper 3 处理 document3.txt:</p><pre><code class="lang-shell">EmitIntermediate(&quot;mapreduce&quot;, &quot;1&quot;)
EmitIntermediate(&quot;world&quot;, &quot;1&quot;)
EmitIntermediate(&quot;example&quot;, &quot;1&quot;)
</code></pre>
<p><strong>Shuffle</strong> 阶段（框架自动完成）：</p><pre><code class="lang-shell">(&quot;hello&quot;, [&quot;1&quot;, &quot;1&quot;])
(&quot;world&quot;, [&quot;1&quot;, &quot;1&quot;])
(&quot;mapreduce&quot;, [&quot;1&quot;, &quot;1&quot;])
(&quot;example&quot;, [&quot;1&quot;])
</code></pre>
<p><strong>Reduce</strong> 阶段（并行执行）：</p><p>Reducer 处理 &quot;hello&quot;:</p><pre><code class="lang-shell">result = 0
result += 1 = 1
result += 1 = 2
Emit(&quot;2&quot;)  # 输出 (&quot;hello&quot;, &quot;2&quot;)
</code></pre>
<p>以此类推处理其他单词...</p><p><strong>最终得出</strong></p><pre><code class="lang-shell">(&quot;hello&quot;, &quot;2&quot;)
(&quot;world&quot;, &quot;2&quot;)
(&quot;mapreduce&quot;, &quot;2&quot;)
(&quot;example&quot;, &quot;1&quot;)
</code></pre>
<h4 id="mapreduce-">MapReduce 框架的作用</h4><p>在这个过程中，MapReduce 框架负责：</p><ol start="1"><li>将输入数据分割成多个分片，分配给不同的 Mapper</li><li>并行执行多个 Map 任务</li><li>执行 Shuffle 操作，重组和排序中间结果</li><li>并行执行多个 Reduce 任务</li><li>收集和整合 Reduce 输出</li><li>处理任务失败和重试</li><li>优化数据本地性，尽量在数据所在节点处理数据</li></ol><p>这种模式使开发者能够专注于业务逻辑（Map 和 Reduce 函数），而无需关心并行化、分布式计算和容错等复杂问题。</p><h3 id="more-examples">More Examples</h3><p>当然这里还有更多的样例。</p><p><strong>Distributed Grep</strong></p><p><strong>工作原理</strong></p><p>在这个示范 example 中，</p><ul><li>Map 函数检查输入文本的每一行，如果匹配指定模式，则发出该行。</li><li>Reduce 函数是一种简单的恒等函数（identity function），直接将中间结果复制到输出。</li></ul><p><strong>应用价值</strong></p><p>这种模式非常适合在大规模分布式文件系统中快速查找特定模式的文本行。它充分利用了 MapReduce 的并行处理能力，在数 TB 甚至 PB 级别的日志文件中查找特定错误信息时效率极高。</p><p><strong>Count of URL Access Frequency（URL 访问频率统计）</strong></p><p><strong>工作原理</strong></p><ul><li><strong>Map 函数</strong>: 处理网页请求日志，对每个 URL 发出 <code>&lt;URL, 1&gt;</code> 键值对</li><li><strong>Reduce 函数</strong>: 将同一 URL 的所有计数相加，输出 <code>&lt;URL, 总计数&gt;</code></li></ul><p><strong>应用价值</strong></p><p>这是网站分析中的基础操作，对于了解网站流量分布、识别热门内容和检测异常访问模式至关重要。在大型网站中，日志数据量可能达到每天数 TB，使用 MapReduce 可以有效处理这种规模的数据。</p><p><strong>Reverse Web-Link Graph（反向网络链接图）</strong></p><p><strong>工作原理</strong></p><ul><li><strong>Map 函数</strong>: 分析网页内容，对每个发现的链接，输出 <code>&lt;目标URL, 源URL&gt;</code></li><li><strong>Reduce 函数</strong>: 收集目标 URL 的所有源 URL，输出 <code>&lt;目标URL, 源URL列表&gt;</code></li></ul><p><strong>应用价值</strong></p><p>反向链接图是现代搜索引擎的核心数据结构之一，用于以下场景：</p><ul><li>PageRank 等网页重要性算法的基础数据</li><li>分析网站间的引用关系</li><li>发现影响力较大的内容创作者</li><li>为网站管理员提供反向链接分析工具</li></ul><p>构建完整的网络反向链接图是一项计算密集型任务，MapReduce 模型非常适合这种自然可并行化的问题。</p><p><strong>Term-Vector per Host（每主机词向量统计）</strong></p><p><strong>工作原理</strong></p><ul><li><strong>Map 函数</strong>: 分析文档内容，从 URL 提取主机名，输出 <code>&lt;主机名, 文档词向量&gt;</code></li><li><strong>Reduce 函数</strong>: 合并同一主机的所有词向量，过滤低频词，输出 <code>&lt;主机名, 汇总词向量&gt;</code></li></ul><p><strong>应用价值</strong></p><p>这种分析对于理解网站内容特征非常有价值：</p><ul><li>可以用于网站的主题分类</li><li>帮助搜索引擎优化</li><li>内容相似性比较</li><li>竞争对手网站内容分析</li><li>内容推荐系统的基础数据</li></ul><p><strong>Inverted Index</strong></p><p><strong>工作原理</strong></p><ul><li><strong>Map 函数</strong>: 解析每个文档，输出 <code>&lt;单词, 文档ID&gt;</code> 键值对</li><li><strong>Reduce 函数</strong>: 接收给定单词的所有文档 ID，排序后输出 <code>&lt;单词, 文档ID列表&gt;</code> 键值对</li></ul><p><strong>应用价值</strong></p><p>倒排索引是现代搜索引擎的基础数据结构，用于：</p><ol start="1"><li><strong>全文搜索</strong>: 快速找到包含查询词的所有文档</li><li><strong>短语搜索</strong>: 通过位置信息实现精确短语搜索</li><li><strong>TF-IDF 计算</strong>: 为信息检索系统提供词频统计</li><li><strong>关键词高亮</strong>: 帮助前端展示匹配的文本片段</li><li><strong>相关性排序</strong>: 为搜索结果提供基础数据</li></ol><p>MapReduce 特别适合构建倒排索引，因为它可以高效地并行处理大量文档，并在 Reduce 阶段自然地实现索引合并。</p><p><strong>Distributed Sort</strong></p><p><strong>工作原理</strong></p><ul><li><strong>Map 函数</strong>: 提取每条记录的键，输出 <code>&lt;键, 记录&gt;</code> 键值对</li><li><strong>Reduce 函数</strong>: 直接输出接收到的所有键值对，不做任何修改</li></ul><p>这个看似简单的例子实际上巧妙利用了 MapReduce 框架的两个核心特性：</p><ol start="1"><li><strong>分区机制</strong> (Partitioning): 确保具有相同范围键的记录被发送到同一个 Reducer</li><li><strong>排序属性</strong> (Sorting): 确保 Reducer 接收到的键按顺序排列</li></ol><p><strong>MapReduce 框架的特殊贡献:</strong></p><p>在分布式排序中，MapReduce 框架做了大部分重要工作：</p><ol start="1"><li><p><strong>自定义分区器</strong> (Custom Partitioner):</p><pre><code class="lang-go">// 示例：范围分区器
func RangePartitioner(key string, numReducers int) int {
    // 根据键的范围确定应该发送到哪个reducer
    // 这确保了全局排序
    if key &lt; &quot;D&quot; {
        return 0
    } else if key &lt; &quot;N&quot; {
        return 1
    } else {
        return 2
    }
}
</code></pre>
</li><li><p><strong>排序比较器</strong> (Sort Comparator):</p><pre><code class="lang-go">// 定义键的自然排序顺序
func KeyComparator(key1, key2 string) int {
    return strings.Compare(key1, key2)
}
</code></pre>
</li></ol><p><strong>应用价值</strong></p><p>分布式排序是许多大数据处理工作流的基础操作：</p><ol start="1"><li><strong>数据预处理</strong>: 准备大规模数据集进行进一步分析</li><li><strong>日志分析</strong>: 按时间戳排序大量日志记录</li><li><strong>构建索引</strong>: 为数据库或搜索引擎创建排序索引</li><li><strong>合并已排序数据</strong>: 将多个已排序的数据集合并为一个</li><li><strong>TopN 查询</strong>: 快速找出某个指标的前 N 条记录</li></ol><p><strong>总体 Examples 分析与比较</strong></p><p>示例展示了 MapReduce 模型的多功能性和适应性：</p><ol start="1"><li><strong>分布式 Grep</strong>: 最简单的一种应用，基本上只用到了 Map 功能，适合简单的过滤操作</li><li><strong>URL 访问频率统计</strong>: 经典的单词计数变种，体现了 MapReduce 在统计聚合上的优势</li><li><strong>反向网络链接图</strong>: 展示了如何使用 MapReduce 构建复杂的关系图和索引结构</li><li><strong>每主机词向量统计</strong>: 结合了文本分析和聚合功能，适用于高级内容分析</li></ol><h2 id="implementation">Implementation（实现）</h2><p>MapReduce 接口的多种不同实现方式都是可能的。</p><p><strong>正确的选择取决于具体环境（具体问题具体分析的意思）。</strong></p><p>例如，一种实现可能 适用于小型共享内存机器，另一种适用于大 型 NUMA 多处理器，还有一种则适用于更庞大的 联网机器集群。</p><p>下面是 Google 广泛使用的一种计算环境。</p><blockquote><p>This section describes an implementation targeted to the computing environment in wide use at Google: large clusters of commodity PCs connected together with switched Ethernet [4].</p><p>In our environment:</p><p>(1) Machines are typically dual-processor x86 processors running Linux, with 2-4 GB of memory per machine.</p><p>(2) Commodity networking hardware is used – typically either 100 megabits/second or 1 gigabit/second at the machine level, but averaging considerably less in overall bisection bandwidth.</p><p>(3) A cluster consists of hundreds or thousands of machines, and therefore machine failures are common.</p><p>(4) Storage is provided by inexpensive IDE disks attached directly to individual machines. A distributed file system [8] developed in-house is used to manage the data stored on these disks. The file system uses replication to provide availability and reliability on top of unreliable hardware.</p><p>(5) Users submit jobs to a scheduling system. Each job consists of a set of tasks, and is mapped by the scheduler to a set of available machines within a cluster.</p></blockquote>
<p>(1) 机器通常配备双处理器 x86 架构，运行 Linux 系统，每台机器内存为 2‑4 GB。</p><p>(2) 采用商用网络硬件 —— 通常在机器层面为 100 兆比特 / 秒或 1 千兆比特 / 秒，但整体二分带宽的 平均值显著较低。</p><p>(3) 一个集群由数百或数千台机器组成，因此机器故障 是常有的事。</p><p>(4) 存储由廉价的 IDE 磁盘提供，这些磁盘直接连 接到各台机器上。一个内部开发的分布式文件系统 [8] 用于管理存储在这些磁盘上的数据。该文件系统 通过数据复制在不可靠的硬件基础上提供可用性和 可靠性。</p><p>(5) 用户向调度系统提交作业。每个作业由一组任务 构成，并由调度器映射至集群内一组可用机器上。</p><h3 id="">执行过程</h3><blockquote><p>The Map invocations are distributed across multiple machines by automatically partitioning the input data into a set of M splits. The input splits can be processed in parallel by different machines. Reduce invocations are distributed by partitioning the intermediate key space into R pieces using a partitioning function (e.g., hash(key) mod R). The number of partitions (R) and the partitioning function are specified by the user.</p><p>Following figure shows the overall flow of a MapReduce operation in our implementation. When the user program calls the MapReduce function, the following sequence of actions occurs (the numbered labels in Figure correspond to the numbers in the list below):</p></blockquote>
<p><img alt="MR执行流" src="The-Whole-Arch.png"/></p><ol start="1"><li><p>这个在 用户 Program 中的 MapReduce Library 首先将文件分成 M 个 pieces，每个 piece 大小通常是 16 ～ 64 MB；</p></li><li><p>Master 上的程序 Copy 是特殊的，其他的 workers 会由 master 派活。这通常有 M 个 map tasks 和 R 个 reduce tasks 来分配。</p><blockquote><p>There are M map tasks and R reduce tasks to assign. The master picks idle workers and assigns each one a map task or a reduce task.</p></blockquote>
<p>这里有一个 idle 单词，idle workers 是指空闲工作节点，在 Google 的 MapReduce 架构中，整个计算任务是分布式执行的，包括：</p><ol start="1"><li><strong>Master（主节点）</strong>：一个特殊的程序副本，负责任务调度和协调整个计算过程</li><li><strong>Workers（工作节点）</strong>：其余的程序副本，负责执行实际的计算任务</li><li><strong>Idle workers（空闲工作节点）</strong>：指当前没有在执行任何任务、处于等待状态的 worker 节点 <a href="https://people.cs.rutgers.edu/~pxk/417/notes/content/mapreduce.html">1</a></li></ol><p>我希望这能够解释清楚什么是 Idle workers。</p><p>所以这一块通俗来讲，整个 MapReduce 的工作流程与空闲工作节点相关：</p><ol start="1"><li>当计算任务开始时，系统会启动多个程序副本，其中一个作为主节点，其余作为工作节点</li><li>主节点维护整个集群的状态，包括每个工作节点是否处于空闲状态 <a href="https://www.junaideffendi.com/p/everything-you-need-to-know-about">2</a></li><li>当主节点检测到某个工作节点是&quot;空闲&quot;的（即没有正在执行任务）时，会从待处理的 M 个 Map 任务或 R 个 Reduce 任务中选择一个分配给该工作节点</li></ol></li><li><p>执行 map 任务的 Worker，会读取被分配到的输入切片，从输入切片中解析出键值对，然后将这个 pair 传递给用户定义的 map 函数。</p><p>这些中间键值对，由 map 生产并 buffer 在内存中。</p><blockquote><p>A worker who is assigned a map task reads the contents of the corresponding input split. It parses key/value pairs out of the input data and passes each pair to the user-defined Map function. The intermediate key/value pairs produced by the Map function are buffered in memory</p></blockquote>
<p>事实上 Hadoop 就是这么干的。</p></li><li><p>buffered 的中间结果 pairs 会被定期写入到本地 disk，然后被 partitioning 函数分片成 R 个 regions。</p><p>这些位于本地 disk 的 bufferd 的 pairs 又会被传递回到 master，这是为了让 master 可以将这些 pairs 的 locations 告知到 reduce workers。</p><blockquote><p>Periodically, the buffered pairs are written to local disk, partitioned into R regions by the partitioning function.</p><p>The locations of these buffered pairs on the local disk are passed back to the master, who is responsible for forwarding these locations to the reduce workers.</p></blockquote>
</li><li><p>当一个 reduce worker 收到上面的 buffered 的 pairs 的 locations 的时候，会通过 RPC 来读取这些对应的 partition 的数据。</p><p>当一个 reduce worker 已经读取完所有的数据之后，会按照 key 进行排序。这样就可以将所有拥有同样 key 的数据排序到一块去了。</p><blockquote><p>When a reduce worker is notified by the master about these locations, it uses remote procedure calls to read the buffered data from the local disks of the map workers.</p><p>When a reduce worker has read all intermediate data, it sorts it by the intermediate keys so that all occurrences of the same key are grouped together.</p><p>The sorting is needed because typically many different keys map to the same reduce task.</p><p>If the amount of intermediate data is too large to fit in memory, an external sort is used.</p></blockquote>
<p>如果 intermediate data（中间数据键值对）too large 的话，那么就会需要外部排序程序了。这里面就是一些性能优化的点了。</p></li></ol><p><strong>第 4 步和第 5 步骤合在一块就叫做 shuffle</strong></p><ol start="6"><li><p>之后，Reduce Worker 会遍历这些排好序的 intermediate data 数据，然后将这些数据以及其 key 相关的 data 传递到用户的 reduce 函数。</p><p>Reduce 函数的输出会被追加到最终的输出文件</p><blockquote><p>The reduce worker iterates over the sorted intermediate data and for each unique intermediate key encountered, it passes the key and the corresponding set of intermediate values to the user’s Reduce function.</p><p>The output of the Reduce function is appended to a final output file for this reduce partition.</p></blockquote>
</li><li><p>当所有的 map 任务和 reduce 任务都完成之后，master 会唤醒用户程序。</p><p>在这个角度，用户程序就会被返回一个最终的计算结果（MapReduce call）。</p><blockquote><p>When all map tasks and reduce tasks have been completed, the master wakes up the user program. At this point, the MapReduce call in the user program returns back to the user code.</p></blockquote>
</li></ol><h3 id="master-data-structures">Master Data Structures</h3><p>理解一下 MapReduce 框架中的 Master 节点所维护的关键数据结构以及其在任务调协中的核心职责。</p><p>Master 节点实际上是整个 MapReduce 执行过程的&quot;大脑&quot;，它维护以下重要数据：</p><ol start="1"><li><strong>任务状态记录</strong>：对每个 map 和 reduce 任务，master 节点都会记录其当前状态 <a href="https://hadoop.apache.org/docs/r1.2.1/mapred_tutorial.html">5</a>：
<ul><li>idle（空闲）：待分配的任务</li><li>in-progress（进行中）：已分配给 worker 但尚未完成的任务</li><li>completed（已完成）：执行完毕的任务</li></ul></li><li><strong>Worker 机器标识</strong>：对于非空闲状态的任务，master 节点会记录执行该任务的 worker 机器的身份标识，用于跟踪任务执行情况和处理故障 <a href="https://hadoop.apache.org/docs/r1.2.1/mapred_tutorial.html">5</a></li><li><strong>中间文件元数据</strong>：对于已完成的 map 任务，master 会存储该任务产生的中间结果文件的位置和大小信息</li></ol><p><strong>Master 作为信息传递的渠道</strong></p><p>Master 节点扮演着中间结果定位信息的传递渠道角色。当一个 map 任务完成后，它会告知 master 节点产生了哪些中间文件，以及这些文件的位置和大小信息 <a href="https://en.wikipedia.org/wiki/MapReduce">2</a>。</p><p>此外额外补充信息，Google 的 MapReduce 实现是有作业（Job）级别的封装，每一个 Job 包含一系列任务（Task），即 Map Task 和 Reduce Task。</p><p>那么如果要维护一个正在运行的 Job 的元信息，就势必要保存所有正在执行的 Task 的状态，以及其所在的机器 ID 等等信息。</p><p>这些信息对 reduce 任务至关重要，因为 reduce 任务需要知道从哪里获取它需要处理的数据。</p><p>而且，Master 也充当了一种从 Map Task 输出到 Reduce Task 的信息 Channel。master 节点会增量地将这些信息推送给正在执行 reduce 任务的 worker 节点 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">1</a>。</p><p>每一个 Map Task 结束时，会将其输出的中间结果的位置信息通知 Master，Master 再将其转给对应的 Reduce Task，Reduce Task 再去对应位置拉取对应 size 的数据。</p><p><strong>注意，由于 Map Task 的结束时间不统一，这个<em>通知 -&gt; 转发 -&gt; 拉取</em> 的过程是增量的。那么不难推测出，reduce 侧对中间数据排序的应该是一个不断 merge 的过程，不大可能是等所有数据就位了再全局排序。</strong> —— from 木鸟</p><blockquote><p>The master keeps several data structures. For each map task and reduce task, it stores the state (idle, in-progress, or completed), and the identity of the worker machine (for non-idle tasks).</p><p>The master is the conduit through which the location of intermediate file regions is propagated from map tasks to reduce tasks. Therefore, for each completed map task, the master stores the locations and sizes of the R intermediate file regions produced by the map task. Updates to this location and size information are received as map tasks are completed. The information is pushed incrementally to workers that have in-progress reduce tasks.</p></blockquote>
<h4 id="-master-data-structure-">例子解释一下 Master Data Structure 的作用</h4><p>拿一个日志分析系统来说明 Master 在 MapReduce 过程中的作用。</p><h5 id="">工作原理</h5><p><strong>场景描述</strong></p><ul><li>有一个分布式系统生成了大量日志文件（数 TB）</li><li>需要分析每小时内各服务的错误率</li><li>日志文件分散存储在 100 个服务器上</li></ul><p><strong>MapReduce 处理流程</strong></p><ol start="1"><li><p>任务初始化：</p><ul><li>将输入分为 1000 个 splits，创建 1000 个 Map 任务；</li><li>设置 10 个 Reduce 任务（按小时分组）；</li><li>Master 将所有任务状态初始化为 idle；</li></ul></li><li><p>Map 阶段：</p><ul><li>Master 将 idle 的 Map 任务分配给可用的 Worker，优先分配给存储数据的本地节点；</li><li>假设有 50 个 Worker 可用，每个 Worker 可同时处理 2 个任务；</li><li>Map 任务处理日志 entries，按小时分组，输出 &lt;小时，错误信息&gt; 对；</li></ul></li><li><p>Map 输出处理：</p><ul><li><p>某 Worker 完成 Map-42 任务，生成 10 个中间文件（对应 10 个 Reduce 任务）</p></li><li><p>Worker 向 Master 报告：</p><pre><code class="lang-shell">完成 Map-42，生成文件：
- worker3:/tmp/job7/map_42_reduce_0.out (2.3 MB)
- worker3:/tmp/job7/map_42_reduce_1.out (1.7 MB)
- ...其他8个文件
</code></pre>
<ul><li>Master 记录这些文件位置和大小信息</li><li>Master 更新 Map-42 状态为 completed</li></ul></li></ul></li><li><p>信息传递：</p><ul><li><p>如果 Worker3 在完成 Map-42 后崩溃</p></li><li>Master 检测到心跳丢失，将 Worker3 上的所有 in-progress 任务重置为 idle</li><li>已完成的 Map-42 不需重做，但其中间文件不可访问</li><li>Master 将重新调度 Map-42 给其他 Worker 执行</li></ul></li></ol><h5 id="">机制优缺点</h5><p>优点：</p><ol start="1"><li><strong>简单高效的协调机制</strong>：Master 集中管理任务状态和中间文件位置，简化了分布式系统设计</li><li><strong>增量数据传输</strong>：中间结果信息的增量推送允许 Reduce 任务尽早开始获取和处理数据</li><li><strong>良好的容错性</strong>：Master 可以检测 Worker 故障并重新调度任务，确保计算的可靠性</li><li><strong>数据本地性优化</strong>：Master 可以根据数据位置分配任务，减少网络传输</li></ol><p>缺点：</p><ol start="1"><li><strong>单点故障问题</strong>：Master 本身成为单点故障源，如果 Master 崩溃，整个作业将失败 <a href="https://medium.com/@mmoshikoo/mapreduce-an-introduction-to-distributed-computing-for-beginners-1f718a7bf546">5</a></li><li><strong>可扩展性瓶颈</strong>：
<ul><li>当任务数量极大时，Master 需要维护大量状态信息</li><li>频繁的状态更新会给 Master 带来高负载</li></ul></li><li><strong>网络瓶颈</strong>：所有中间文件位置信息都经过 Master 传递，可能导致网络拥塞</li><li><strong>复杂的故障恢复</strong>：如果 Map 任务已完成但中间文件丢失（如 Worker 存储故障），需要重新执行 Map 任务</li></ol><h4 id="-master-">针对 Master 设计缺陷的常见解决方案</h4><p>在分布式系统中，最忌讳的就是单点。因为往往这样的分布式架构，如果 Master 节点出现了故障，那么整套系统的可用性就为 0 了。所以我们做的很多事情，就是在强化 Master 节点。</p><p>我们从 “<strong>强化 Master 节点</strong>” 这个角度去思考解决方案：</p><ol start="1"><li>Master 高可用设计
<ol start="1"><li>实现 Master 的主从机制，可以类似 etcd、consul 这样进行分布式协调服务选举主 Master，利用租约一类的设计完成；</li><li>实现 Master 节点的热备份和故障转移；</li></ol></li><li>分层架构
<ol start="1"><li>引入二级 Master 或者区域 Master 分担负载；</li><li>YARN 之类的现代框架将资源管理与任务调度分离，增强了可扩展性，一定程度上隔离了风险故障；</li></ol></li></ol><p>从 ”<strong>强化存储中间文件信息存储</strong>“ 这个角度出发，思路可以有，分布式存储元数据：</p><ol start="1"><li>分布式元数据存储：
<ul><li>将任务状态和中间文件信息存储在分布式系统中</li><li>支持快照和持久化，便于故障恢复</li></ul></li></ol><p>从 ”<strong>增强 Worker</strong>“ 角度出发：</p><ol start="1"><li>直接 Worker 通信：
<ul><li>允许 Worker 之间直接通信交换中间结果位置</li><li>减轻 Master 负担，但增加系统复杂性</li></ul></li></ol><p>从 ”<strong>任务粒度调整</strong>“ 角度出发：</p><ol start="1"><li>动态任务粒度调整：
<ul><li>根据任务规模和集群状态动态调整任务大小</li><li>小型作业使用较粗粒度任务减少管理开销，大型任务使用细粒度任务提高并行度</li></ul></li></ol><h3 id="failure-tolerance">Failure Tolerance</h3><p>分布式系统，在处理大量的数据的时候，同时也一定面临着分布式机器上的各种错误，如何优雅地处理这些错误，也是必学的一门课。</p><p>这里的论文大概分为三种 Failure：</p><ol start="1"><li>[Worker Failure](#Worker Failure)</li><li>[Master Failure](#Master Failure)</li><li>[Semantics in the Presence of Failures](#Semantics in the Presence of Failures)</li></ol><h4 id="worker-failure">Worker Failure</h4><p>master 会定期去 ping 各个 worker 节点，如果没有响应的话，那么 master 就会将其标记为 failed 节点。</p><p>这个时候不管是已经完成还是未完成的 map tasks 都会被标记为最初的状态 idle state。然后等待被调度到其他正常的 workers 上去。</p><p>然后这一段 map tasks 就会被重新执行，重新存储到本地 disk 上去，然后 master 会将这些信息继续通报给 Reduce workers。</p><p>这个时候呢，如果 Reducer worker 已经处理了 map tasks 其中的某个一个单独 task，那么它不用再从 master 提供的信息中继续去拿处理数据，如果没处理，就继续拿。</p><p>在 Reducer worker 侧，当发现自己处理的某一段 map tasks 上的 map worker 出现了故障，举个例子，这个 Reduce 程序是 R5，他正在 处理 Worker-37 上的 map 任务（41 ～ 51），当 R5 处理到 M47 的时候，发现出现了故障，那么它的应对措施（伪代码）如下：</p><ul><li><strong>传输中断处理</strong>：如果 R5 正在从 Worker-37 拉取数据时连接中断，会触发异常处理流程</li></ul><pre><code class="lang-java">// 简化的伪代码
try {
    fetchMapOutput(worker37, mapTaskId47);
} catch (FetchFailureException e) {
    // 等待Master通知新的数据位置
    waitForNotification(mapTaskId47);
    // 获取新的数据位置后重试
    fetchMapOutput(worker51, mapTaskId47);
}
</code></pre>
<ul><li><strong>数据一致性</strong>：R5 会丢弃从 Worker-37 已部分拉取的不完整数据</li></ul><pre><code class="lang-java">if (partialData &amp;&amp; dataSource != currentSourceForTask) {
    discardPartialData();
    fetchFromNewSource();
}
</code></pre>
<ul><li><strong>通知机制</strong>：Master 通过以下方式通知 Reduce 任务</li></ul><pre><code class="lang-shell">1. 心跳响应中包含更新的Map输出位置信息
2. RPC调用通知状态变更
3. Reduce任务定期轮询Master获取最新映射信息
</code></pre>
<p>我再额外引申一些联想，上面的一系列操作，体现了实际的分布式架构设计模型中常用的一些技术点下：</p><p><strong>关键技术点</strong></p><ol start="1"><li><strong>乐观并发模型</strong>：MapReduce 采用乐观策略，不阻止多个 Worker 同时处理同一数据，而是在需要时重新计算</li><li><strong>幂等性保证</strong>：Map 和 Reduce 函数被设计为幂等操作，确保重复执行不会影响最终结果</li><li><strong>增量通知机制</strong>：Master 增量通知 Reduce 任务，减少不必要的数据传输和重新计算</li><li><strong>数据验证</strong>：通常使用校验和验证数据完整性，确保即使使用已获取的数据也能保证计算正确性</li></ol><h4 id="master-failure">Master Failure</h4><p>Google 的论文对于 Master 故障的处理相对来说很简单：通过检查点机制保存状态，但在 Master 实际故障时会终止整个作业。</p><p>这种设计基于两个考虑：</p><ol start="1"><li><strong>单点特性</strong>：系统中通常只有一个 Master 节点，故障概率相对较低；</li><li><strong>简化设计</strong>：简单的故障处理机制减少了系统复杂度；</li></ol><p>然而，在关键生产环境中，这种简单的处理方式显然难以满足高可用需求。</p><p>随着分布式系统的发展，更健壮的 Master 故障处理机制应该被严肃认真滴考虑一下。考虑方案如下：</p><ol start="1"><li><p>主备高可用架构</p><pre><code class="lang-shell">+---------------+   复制状态   +---------------+
|  Active Master |-------------&gt;| Standby Master|
+---------------+              +---------------+
        ^                              |
        | 心跳                         | 故障转移
        |                              v
+--------------------------------------------+
|              ZooKeeper集群                  |
+--------------------------------------------+
        ^                              ^
        |                              |
+---------------+              +---------------+
|   Worker-1    |              |   Worker-N    |
+---------------+              +---------------+
</code></pre>
<p><strong>实现</strong>：</p><ul><li>使用 ZooKeeper 等进行 Leader 选举</li><li>通过同步复制或共享存储保持状态一致</li><li>心跳监测机制检测故障</li><li>平滑接管避免作业中断</li></ul></li><li><p>分布式状态管理</p><pre><code class="lang-go">// 使用分布式存储系统保存状态
func (m *Master) updateTaskState(taskID string, state TaskState) {
    // 更新内存状态
    m.taskStates[taskID] = state

    // 同步更新到分布式存储
    etcdClient.Put(context.Background(),
                  fmt.Sprintf(&quot;/mapreduce/tasks/%s/state&quot;, taskID),
                  string(state))
}

// 从分布式存储恢复状态
func recoverMasterState() *Master {
    master := newMaster()

    // 从etcd读取所有任务状态
    resp, _ := etcdClient.Get(context.Background(),
                             &quot;/mapreduce/tasks/&quot;, clientv3.WithPrefix())

    for _, kv := range resp.Kvs {
        // 解析键值并恢复状态
        taskID, field := parseKey(string(kv.Key))
        if field == &quot;state&quot; {
            master.taskStates[taskID] = TaskState(kv.Value)
        }
        // 其他字段类似处理...
    }

    return master
}
</code></pre>
</li><li><p>应用级故障恢复</p><p>现代系统如 YARN 将 MapReduce 的 Master 分为两个角色：</p><ol start="1"><li><strong>ResourceManager</strong>：集群资源管理（全局角色）</li><li><strong>ApplicationMaster</strong>：单个作业协调（每个作业一个）</li></ol><p>这种设计带来两个优势：</p><ul><li>ResourceManager 故障不会影响正在运行的作业</li><li>ApplicationMaster 故障只影响单个作业，可以独立恢复</li></ul></li><li><p>客户端弹性恢复</p><p>为支持 Master 故障后的客户端重试，需要确保 MapReduce 操作的幂等性：</p><pre><code class="lang-go">// 客户端重试逻辑
func executeMapReduceWithRetry(job *MapReduceJob, maxRetries int) Result {
    // 为每个作业生成唯一ID，支持幂等执行
    if job.JobID == &quot;&quot; {
        job.JobID = generateUniqueID()
    }

    var lastError error
    for i := 0; i &lt; maxRetries; i++ {
        result, err := submitMapReduceJob(job)
        if err == nil {
            return result
        }

        lastError = err
        if !isMasterFailureError(err) {
            // 非Master故障错误，直接返回
            return nil, err
        }

        log.Printf(&quot;Master failure detected, retrying job %s (%d/%d)...&quot;,
                  job.JobID, i+1, maxRetries)
        time.Sleep(retryBackoff(i))
    }

    return nil, fmt.Errorf(&quot;all retries failed: %v&quot;, lastError)
}
</code></pre>
</li></ol><h5 id="">示范用例：金融交易数据分析系统</h5><p>下面通过一个金融交易数据分析系统的例子来说明不同 Master 故障处理策略的效果：</p><p><strong>场景描述</strong></p><ul><li><strong>任务</strong>：处理全球金融市场一天的交易数据(10TB)并计算风险指标</li><li><strong>时间要求</strong>：必须在市场开盘前完成(有严格的时间窗口)</li><li><strong>可靠性要求</strong>：结果必须 100%准确，不允许数据丢失</li></ul><h6 id="">对比不同故障处理策略</h6><ol start="1"><li><p>原始 MapReduce 方案(简单重启)</p><pre><code class="lang-txt">晚上10:00 - 作业开始，预计4小时完成
凌晨01:30 - Master故障，计算被中断(已完成约70%)
凌晨01:35 - 运维人员接到告警，手动重启作业
凌晨01:40 - 新作业从头开始计算
凌晨05:40 - 作业完成，但已超过市场开盘时间
结果：业务影响严重，无法按时提供风险分析
</code></pre>
</li><li><p>改进方案(高可用 Master)</p><pre><code class="lang-txt">晚上10:00 - 作业开始，预计4小时完成
凌晨01:30 - 主Master故障
凌晨01:30.5 - 备Master自动接管(500毫秒故障转移)
            - 备Master从共享存储恢复状态
            - 重置进行中任务状态为idle
凌晨01:35 - 系统重新调度中断的任务
凌晨04:15 - 作业正常完成
结果：系统自动恢复，业务正常进行
</code></pre>
</li></ol><h4 id="semantics-in-the-presence-of-failures">Semantics（语义） in the Presence of Failures</h4><p>MapReduce 提供了一个关键的承诺：<strong>在确定性操作时，分布式并行执行的结果与顺序执行完全一致</strong>。</p><p>这个特性极大简化了分布式程序的复杂性。</p><p>那么是如何实现这个“<strong>计算结果一致性</strong>” 的呢？</p><p><strong>关键机制：原子提交</strong></p><p>MapReduce 通过精心设计的原子提交机制实现结果一致性：</p><ol start="1"><li>临时文件策略</li><li>任务完成流程</li><li>冗余执行处理</li></ol><p><strong>原子操作的核心作用</strong></p><p>原子操作时整个容错机制的基础，主要体现在两个层面：</p><ol start="1"><li>Master 数据结构更新</li><li>文件系统原子重命名，举个例子，Reduce 任务完成时的原子重命名操作，只有一个执行实例能成功命名，换言之，如果有一个命名好的文件，那就意味着已经成功执行了一个实例了</li></ol><h5 id="">确定性与非确定性操作的语义区别</h5><p>Map Reduce 框架对不同类型的操作提供不同级别的语义保证：</p><h6 id="">确定性操作的强语义保证</h6><p>当 map 和 reduce 函数是<strong>确定性</strong>的（相同输入总是产生相同输出）时：</p><ul><li><strong>全局一致性</strong>：整个计算的结果与单机顺序执行完全相同</li><li><strong>重复执行不变性</strong>：任务执行多次，结果不变</li><li><strong>故障透明性</strong>：用户无需关心故障处理细节</li></ul><pre><code class="lang-python"># 确定性Map函数示例
def map_word_count(doc_id, document):
    for word in document.split():
        emit(word, 1)  # 相同输入总是产生相同输出

# 确定性Reduce函数示例
def reduce_word_count(word, counts):
    emit(word, sum(counts))  # 相同输入总是产生相同的和
</code></pre>
<h6 id="">非确定性操作的弱语义保证</h6><p>当 map 或 reduce 函数是<strong>非确定性</strong>的（相同输入可能产生不同输出）时：</p><ul><li><strong>部分一致性</strong>：单个 reduce 任务的输出等同于某次顺序执行的相应输出</li><li><strong>分片间不一致性</strong>：不同 reduce 任务可能对应不同的顺序执行结果</li><li><strong>&quot;部分&quot;顺序执行等价</strong>：结果不等同于任何单次顺序执行的完整结果</li></ul><pre><code class="lang-python"># 非确定性Map函数示例
def map_with_random(doc_id, document):
    for word in document.split():
        # 添加随机噪声，使得相同输入产生不同输出
        random_value = random.random()
        emit(word, random_value)
</code></pre>
<h5 id="">实例解析</h5><p>这边那两个实例来解释和理解“故障情况下的语义保证”：</p><p>两个场景：</p><ol start="1"><li>分析网站用户行为程序，计算每个 URL 的独特访问者数量；</li><li>广告点击分析系统；</li></ol><h6 id="-url-">计算每个 URL 的独特访问者数量</h6><p>考虑一个具体场景：分析网站用户行为数据，计算每个 URL 的独特访问者数量。</p><p><strong>分布式系统配置</strong></p><ul><li>100 个 Map 任务(M0-M99)：每个处理一个日志分片</li><li>10 个 Reduce 任务(R0-R9)：按 URL 哈希分区</li><li>Map 输出：&lt;URL, UserID&gt;对</li><li>Reduce 操作：去重计数用户 ID</li></ul><p><strong>确定性操作场景</strong></p><p>假设所有操作都是确定性的：</p><ol start="1"><li>M42 任务在 Worker-A 上执行，生成输出</li><li>Worker-A 故障，M42 输出不可访问</li><li>M42 在 Worker-B 上重新执行，生成相同的输出</li><li>R3 读取 M42(Worker-B 执行)的输出</li><li>R7 读取 M42(Worker-B 执行)的输出</li></ol><p>结果：R3 和 R7 都处理了相同的 M42 输出，最终整体结果等价于顺序执行</p><p><strong>非确定性操作场景</strong></p><p>假设 Map 函数使用随机采样来降低数据量：</p><ol start="1"><li>M42 首次在 Worker-A 上执行，随机采样生成输出 X</li><li>R3 开始执行并读取了 M42 的输出 X</li><li>Worker-A 故障，M42 输出 X 不可访问</li><li>M42 在 Worker-B 重新执行，随机采样生成不同的输出 Y</li><li>R7 执行并读取 M42 的新输出 Y</li></ol><p>结果：</p><ul><li>R3 处理了基于样本 X 的数据</li><li>R7 处理了基于不同样本 Y 的数据</li><li>最终结果不等同于任何单次顺序执行的结果</li></ul><h6 id="">实时广告点击分析系统</h6><p><strong>系统需求</strong></p><ul><li>分析广告点击流数据，计算每小时广告转化率</li><li>处理数据量：每小时数十亿点击事件</li><li>需要输出：每个广告 ID 的点击次数和转化次数</li></ul><p><strong>数据流设计</strong></p><p>输入：点击事件流(ad<em>id, event</em>type, timestamp, user<em>id, ...)
Map 函数：提取(ad</em>id, event_info)键值对
Reduce 函数：按广告 ID 聚合统计点击和转化</p><p><strong>确定性实现</strong></p><pre><code class="lang-python"># 确定性Map函数
def map_ad_events(_, event):
    ad_id = event[&#x27;ad_id&#x27;]
    event_type = event[&#x27;event_type&#x27;]
    info = {
        &#x27;clicks&#x27;: 1 if event_type == &#x27;click&#x27; else 0,
        &#x27;conversions&#x27;: 1 if event_type == &#x27;conversion&#x27; else 0
    }
    emit(ad_id, info)

# 确定性Reduce函数
def reduce_ad_stats(ad_id, event_infos):
    total_clicks = sum(info[&#x27;clicks&#x27;] for info in event_infos)
    total_conversions = sum(info[&#x27;conversions&#x27;] for info in event_infos)

    result = {
        &#x27;ad_id&#x27;: ad_id,
        &#x27;clicks&#x27;: total_clicks,
        &#x27;conversions&#x27;: total_conversions,
        &#x27;conversion_rate&#x27;: total_conversions / total_clicks if total_clicks &gt; 0 else 0
    }
    emit(ad_id, result)
</code></pre>
<p><strong>故障情况下的一致性</strong>：即使某些 Worker 故障并导致任务重新执行，最终计算的广告统计结果仍然准确，因为操作是确定性的。</p><p><strong>非确定性实现（采样）</strong></p><pre><code class="lang-python"># 非确定性Map函数(使用随机采样)
def map_ad_events_sampled(_, event):
    # 随机采样10%的事件
    if random.random() &lt;= 0.1:  # 非确定性!
        ad_id = event[&#x27;ad_id&#x27;]
        event_type = event[&#x27;event_type&#x27;]
        # 因为是10%采样，权重需要乘以10
        info = {
            &#x27;clicks&#x27;: 10 if event_type == &#x27;click&#x27; else 0,
            &#x27;conversions&#x27;: 10 if event_type == &#x27;conversion&#x27; else 0
        }
        emit(ad_id, info)
</code></pre>
<p><strong>故障情况下的不一致性</strong>：</p><p>如果 Map 任务 M25 处理的是美国地区的点击数据，并且：</p><ol start="1"><li>M25 首次执行时采样了一组事件 X</li><li>Reduce 任务 R3（处理广告 ID 1000-1999）读取了这个输出</li><li>M25 执行的 Worker 故障，导致 M25 重新执行</li><li>第二次执行采样了不同的事件集合 Y</li><li>Reduce 任务 R7（处理广告 ID 6000-6999）读取了新输出</li></ol><p>结果：</p><ul><li>广告 ID 1500 的统计数据基于样本 X</li><li>广告 ID 6500 的统计数据基于样本 Y</li><li>不同广告之间的相对性能比较可能不一致</li></ul><h5 id="">实现要点与最佳实践</h5><ol start="1"><li><strong>优先使用确定性操作</strong>：
<ul><li>尽可能设计确定性的 Map 和 Reduce 函数</li><li>将不确定性因素（如随机性）封装在预处理或后处理阶段</li></ul></li><li><strong>处理非确定性需求</strong>：
<ul><li>使用伪随机数生成器并提供固定种子</li><li>将随机状态作为输入参数而非函数内生成</li></ul></li><li><strong>确保幂等性</strong>：
<ul><li>设计能多次安全执行的操作</li><li>输出命名使用任务 ID 而非时间戳</li></ul></li><li><strong>原子性保证</strong>：
<ul><li>利用底层文件系统或数据库的事务能力</li><li>实现&quot;先写临时，后原子重命名&quot;的模式</li></ul></li></ol><p>MapReduce 的这种语义保证设计平衡了系统的一致性需求与工程实现复杂度，为分布式计算提供了实用而强大的模型。确定性操作的强一致性保证特别有价值，它让程序员可以像编写顺序程序一样思考分布式计算，极大降低了分布式编程的复杂性。</p><h3 id="locality">Locality（局部性）</h3><p>计算机科学中常用的一个原理，叫做<strong>局部性原理</strong> （locality reference，这里特指空间局部性），说的是程序在顺序执行时，访问了一块数据，接下来大概率会访问该数据（物理位置上）旁边的一块数据。很朴素的断言，却是一切 cache 发挥作用的基础，计算机存储因此也形成了由慢到快，由贱到贵，由大到小的存储层次体系（硬盘 -&gt; 内存 -&gt; 缓存 -&gt; 寄存器）。</p><p>在分布式环境中，这个层次体系至少还要再罩上一层 —— 网络 IO。这也就是论文中的第一句 “Network bandwidth is a relatively scarce resource in our computing environment”。</p><p>在 MapReduce 系统中，我们也会充分利用输入数据的 locality。只不过这次，不是将数据加载过来，而是将程序<strong>调度</strong>过去（<strong>Moving Computation is Cheaper than Moving Data</strong>）。如果输入存在 GFS 上，表现形式将为一系列的逻辑 Block，每个 Block 可能会有几个（一般是三个）物理副本。对于输入每个逻辑 Block，我们可以在其某个物理副本所在机器上运行 Map Task（如果失败，就再换一个副本），由此来尽量减小网络数据传输。从而降低了延迟，节约了带宽。</p><blockquote><p>Network bandwidth is a relatively scarce resource in our computing environment. We conserve network bandwidth by taking advantage of the fact that the input data (managed by GFS [8]) is stored on the local disks of the machines that make up our cluster. GFS divides each file into 64 MB blocks, and stores several copies of each block (typically 3 copies) on different machines. The MapReduce master takes the location information of the input files into account and attempts to schedule a map task on a machine that contains a replica of the corresponding input data. Failing that, it attempts to schedule a map task near a replica of that task’s input data (e.g., on a worker machine that is on the same network switch as the machine containing the data). When running large MapReduce operations on a significant fraction of the workers in a cluster, most input data is read locally and consumes no network bandwidth.</p></blockquote>
<h3 id="task-granularity">Task Granularity（任务粒度）</h3><p>深入分析一下 MapReduce 框架中任务粒度（Task Granularity）的核心设计原则、影响因素和最佳实践。</p><h4 id="">任务粒度的基本概念</h4><p>MapReduce 将计算任务分为两个阶段：</p><ul><li><strong>M</strong>: Map 任务的数量（输入数据被分割成 M 个片段）</li><li><strong>R</strong>: Reduce 任务的数量（中间键空间被分成 R 个分区）</li></ul><p>任务粒度指的是每个单独任务处理的数据量大小，它是 MapReduce 框架中一个关键的设计参数。</p><p>论文中有一个很重要的观点，“<strong>M and R should be much larger than the number of worker machines</strong>“。</p><h4 id="m--r--worker-">M 和 R 应该远大于 Worker 机器数量</h4><ol start="1"><li><p>动态负载均衡</p><pre><code class="lang-shell">// 简化的负载均衡场景
100个Map任务, 10台机器
- 机器1-9: 每台处理10个任务，均匀负载
- 机器10: 硬件较慢，只完成5个任务
- 任务分配器自动将剩余5个任务重新分配给已完成任务的机器
</code></pre>
<p>当每台机器处理多个小任务而非单个大任务时，快速的机器可以处理更多任务，慢速机器处理较少，自然形成了基于性能的工作分配。</p></li><li><p>加速故障恢复</p><pre><code>假设:
- 2000台机器，每台执行约100个Map任务
- 单台Worker-37故障，已完成92个Map任务

影响:
- 传统设计(每台机器1个大任务): 丢失整个Worker-37的计算结果
- 细粒度设计: 只需重新执行92个小任务，分散到其他1999台机器
- 恢复速度: 约为传统方法的1/20(平均每台机器分担不到1个额外任务)
</code></pre>
<p>当一个 worker 故障时，它已完成的多个小任务可以迅速分散到集群中的其他机器上重新执行，显著加快恢复速度</p></li></ol><h4 id="">任务粒度的实际限制因素</h4><p>尽管细粒度任务有明显优势，但不能无限增加 M 和 R 的值。主要限制包括：</p><ol start="1"><li><p>Master 节点的调度与<strong>存储开销</strong></p><pre><code>// Master需要维护的状态数据量
存储空间 ≈ O(M + R) + O(M * R)
   - O(M + R): 任务状态信息
   - O(M * R): Map输出位置信息(每个Map任务为每个Reduce生成一个分区)
</code></pre>
<p>论文指出，Master 节点需要做 O(M + R)次调度决策，并且在内存中维护 O(M * R)的状态信息。虽然每个 Map/Reduce 任务对的状态只占约 1 字节，但数量庞大时仍会造成显著开销。</p></li><li><p>输出文件的管理限制</p><p>Reduce 任务的数量 R 通常受到用户需求的限制，因为每个 Reduce 任务会生成一个独立的输出文件。如果应用需要生成固定数量的输出文件（如按地区分组的报告），这会直接约束 R 的选择</p></li><li><p>任务启动开销</p><pre><code>// 任务启动开销累积
总启动开销 ≈ (任务启动时间 * (M + R))
</code></pre>
<p>每个任务启动都有固定开销（进程创建、JVM 启动、资源分配等）。</p><p>任务过小会导致大量时间浪费在非计算性工作上。</p></li></ol><h4 id="google-">Google 实践中的参数选择</h4><p>论文提供了 Google 实际使用的参数作为参考：</p><ol start="1"><li><p><strong>Map 任务大小</strong>：通常选择 16MB-64MB 的输入数据量，这个范围有利于数据本地性优化</p></li><li><p><strong>Reduce 任务数量</strong>：通常设为预期使用的 worker 机器数量的小倍数</p></li><li><p><strong>实际应用规模</strong>：</p><pre><code>典型参数:
- M = 200,000 (Map任务数)
- R = 5,000 (Reduce任务数)
- Worker = 2,000 (机器数)

平均每台机器:
- 执行约100个Map任务
- 执行约2-3个Reduce任务
</code></pre>
</li></ol><p>这种配置充分体现了&quot;任务数远多于机器数&quot;的设计理念</p><h4 id="">任务粒度优化的多维度考量</h4><h5 id="">数据本地性与任务粒度</h5><p>数据本地性，是 MapReduce 框架的核心优化策略，指的是“将计算移动到数据所在位置”，而非通过网络传输大量数据。这一概念在分布式系统中极为重要，因为网络传输通常是主要瓶颈。</p><h5 id="">粒度太大为何减少并行度</h5><p>当 Map 任务粒度过大时：</p><ol start="1"><li>集群节点利用不充分</li><li>资源绑定时间长</li><li>调度灵活性降低</li></ol><h5 id="">粒度太小为何增加非本地执行概率</h5><p>数据本地性在 MapReduce 中分为三个层级：</p><ol start="1"><li><strong>节点本地性(Node Locality)</strong>: 数据与执行任务的节点位于同一服务器</li><li><strong>机架本地性(Rack Locality)</strong>: 数据与执行节点在同一机架但不同服务器</li><li><strong>跨机架(Off-Rack)</strong>: 数据需要从其他机架传输</li></ol><p>当任务粒度过小时：</p><ol start="1"><li><p><strong>调度竞争激烈</strong></p><pre><code>假设：集群有1000个节点，每个节点数据块分布均匀

- 小粒度(1MB/任务)：产生1,000,000个任务
- 每个节点平均存储1000个数据块
- 同时有多个任务竞争相同节点的执行槽位
- 当节点A的槽位都被占用，即使还有本地数据块未处理
- 调度器被迫将这些任务分配给非本地节点B
</code></pre>
</li><li><p><strong>调度复杂度提高</strong></p><pre><code>大量小任务导致调度过载:
- Master需要做出更多调度决策(O(M)复杂度)
- 调度延迟增加，最优本地性决策可能错失
- 调度器可能退化为贪心策略，优先满足可用性而非最佳本地性
</code></pre>
</li><li><p><strong>资源碎片化</strong></p><pre><code>每个任务都有固定开销:
- JVM启动: ~1秒
- 资源分配: ~0.5秒
- 状态报告: 持续占用少量资源

过多小任务导致:
- 资源大量用于管理开销而非实际计算
- 节点资源碎片化，难以高效分配
</code></pre>
</li></ol><h5 id="16-64mb-">16-64MB 为何是理想任务粒度</h5><p>这个范围并非随机选择，而是基于多种技术因素的平衡点：</p><ol start="1"><li><p><strong>分布式文件系统块大小</strong></p><pre><code>HDFS默认块大小: 64MB-128MB(早期版本)
GFS块大小: 64MB(Google文件系统，MapReduce最初设计环境)
</code></pre>
<p>当 Map 任务粒度与文件系统块大小相近时，可以实现最优的数据本地性 <a href="https://techvidvan.com/tutorials/data-locality-in-hadoop-mapreduce/">5</a>。一个 Map 任务处理一个或少数几个块是最理想的情况。</p></li><li><p><strong>网络与磁盘性能比率分析</strong></p><p>MapReduce 设计之初(2003-2004 年)，集群环境下：</p><pre><code>数据中心网络带宽: ~1Gbps(共享)
实际可用节点带宽: ~100Mbps
磁盘顺序读取速度: ~50-100MB/s

传输64MB数据:
- 本地读取时间: &lt;1秒
- 网络传输时间: ~5秒

性能差距: 5倍以上
</code></pre>
<p>根据这个性能差距，选择 16-64MB 的粒度可以在数据需要通过网络传输时，将额外开销控制在合理范围内。</p></li><li><p><strong>任务启动开销与执行时间比例</strong></p><pre><code>假设任务固定开销:
- JVM启动: ~1秒
- 资源分配: ~0.5秒
- 状态报告: ~0.2秒
总固定开销: ~1.7秒

处理不同大小数据所需时间(假设100MB/s处理速度):
- 1MB: 0.01秒 (开销比: 99%)
- 10MB: 0.1秒 (开销比: 94%)
- 16MB: 0.16秒 (开销比: 91%)
- 64MB: 0.64秒 (开销比: 73%)
- 128MB: 1.28秒 (开销比: 57%)
- 1GB: 10秒 (开销比: 15%)
</code></pre>
<p>16-64MB 范围在固定开销与实际计算时间之间取得了合理平衡</p></li><li><p><strong>故障恢复粒度考量</strong></p><pre><code>场景：1000节点集群，处理10TB数据

节点故障影响:
- 1GB粒度: 重新计算~10个任务，每个~10秒，串行~100秒
- 64MB粒度: 重新计算~160个任务，每个~0.64秒，并行~几秒
</code></pre>
<p>适中的 16-64MB 粒度使得失败任务可以迅速并行重新执行，而不会造成大量计算浪费</p></li><li><p><strong>内存与排序效率</strong></p><p>Map 和 Reduce 任务都需要在内存中进行数据操作：</p><pre><code>早期节点内存: 4-8GB
系统和框架开销: ~1-2GB
可用应用内存: ~3-6GB

考虑多任务并行执行:
- 每节点10个并行任务
- 每任务可用内存: ~300-600MB
- 安全工作内存: ~100-200MB

16-64MB的输入通常能在这个内存限制内高效处理
</code></pre>
</li><li><p><strong>实验验证的经验值</strong></p><p>Google 在论文中提到这个范围，很可能是基于大量实际实验和生产工作负载分析得出的最佳实践 <a href="https://stackoverflow.com/questions/58272650/what-exactly-does-data-locality-mean-in-hadoop">2</a>。随着硬件演进，现代系统可能会调整这个范围，但基本原理依然适用。</p></li></ol><p><strong>图解</strong></p><pre><code class="lang-shell">数据本地性随任务粒度变化的趋势:

      ^
      |                    最优范围
本地  |             ******
执行  |         ****      *****
比例  |      ***               ***
      |    **                     **
      |  **                         **
      |**                             **
      +----------------------------------&gt;
        小                               大
                  任务粒度
</code></pre>
<p>这个曲线说明：</p><ul><li>粒度太小时，调度竞争导致本地执行比例下降</li><li>粒度太大时，并行度降低，导致整体效率下降</li><li>16-64MB 范围位于曲线顶点附近，是实际应用中的最佳平衡点</li></ul><p>虽然 16-64MB 是一个很好的起点，但最佳粒度应根据具体应用场景调整：</p><ol start="1"><li><strong>计算密集型任务</strong>: 可以使用较大粒度(如 64-128MB)</li><li><strong>IO 密集型任务</strong>: 应使用较小粒度(如 16-32MB)</li><li><strong>异构集群</strong>: 可考虑动态调整粒度，适应不同节点能力</li><li><strong>高内存需求</strong>: 如果单任务内存需求大，应相应减少粒度</li></ol><p>通过合理设置任务粒度，可以实现数据本地性、并行度和系统开销的最佳平衡，从而获得 MapReduce 框架的最优性能。</p><h3 id="backup-tasks">Backup Tasks（备份任务）</h3><p>这个东西，是 Google 为了解决“掉队者”（stragglers）而设计的。</p><blockquote><p>One of the common causes that lengthens the total time taken for a MapReduce operation is a “straggler”: a machine that takes an unusually long time to complete one of the last few map or reduce tasks in the computation.</p></blockquote>
<p>为了解决慢任务问题引入的重要优化。</p><h4 id="straggler-">Straggler 问题：分布式系统中的关键挑战</h4><p>&quot;掉队者&quot;(Stragglers)指那些异常缓慢完成任务的机器，它们会严重拖慢整个 MapReduce 作业的完成时间。在大规模分布式环境中，这个问题尤为突出。</p><h5 id="">主要原因</h5><ol start="1"><li><p><strong>硬件问题</strong>：</p><pre><code>- 磁盘错误：可纠正错误将读取速度从30MB/s降至1MB/s
- 网络问题：网卡故障导致带宽下降
- CPU或内存故障：处理能力显著下降
</code></pre>
</li><li><p><strong>资源竞争</strong>：</p><pre><code>- 多任务调度冲突：其他作业占用CPU、内存资源
- I/O争用：多进程争夺磁盘或网络I/O
- 内存压力：内存不足导致频繁页面交换
</code></pre>
</li><li><p><strong>软件问题</strong>：</p><pre><code>- 配置错误：如Google遇到的处理器缓存被禁用bug(性能降低100倍)
- GC暂停：垃圾回收引起的长时间暂停
- 系统更新：后台服务或更新占用资源
</code></pre>
</li></ol><h5 id="stragglers-">Stragglers 的影响</h5><p>在 MapReduce 作业中，作业完成时间受限于最后一个完成的任务。当 99%的任务都快速完成，而少数几个任务异常缓慢时，整个作业的完成时间将被这些慢任务所主导 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">4</a>。</p><p>典型情景：</p><ul><li>10,000 个 Map 任务，预期 1 小时内完成</li><li>9,990 个任务在 58 分钟内完成</li><li>10 个 stragglers 可能需要额外 2-3 小时</li><li>结果：整个作业耗时超过 3 小时而非 1 小时</li></ul><h4 id="">备份任务机制：优雅解决方案</h4><p>Google 设计的备份任务机制是一种简单而有效的策略，通过适度的资源冗余来减少总体执行时间。</p><h4 id="">工作原理详解</h4><pre><code class="lang-go">// 备份任务调度的伪代码实现
func (m *Master) scheduleBackupTasks() {
    // 当作业接近完成时触发
    if m.progressRate() &gt; 0.95 { // 95%任务已完成
        // 查找所有运行时间超过平均值的进行中任务
        inProgressTasks := m.getInProgressTasks()
        for _, task := range inProgressTasks {
            if task.runningTime() &gt; m.averageTaskTime() * 1.5 {
                // 为可能的straggler创建备份任务
                m.scheduleBackupExecution(task)
            }
        }
    }
}

// 任务完成处理
func (m *Master) markTaskCompleted(taskID string, workerID string) {
    task := m.tasks[taskID]

    if task.State == TaskCompleted {
        // 任务已被另一个执行实例(主执行或备份)完成
        return
    }

    // 标记任务完成
    task.State = TaskCompleted

    // 取消该任务的其他执行实例
    m.cancelOtherExecutions(taskID, workerID)
}
</code></pre>
<h4 id="">核心设计要点</h4><ol start="1"><li><p><strong>触发时机</strong>：</p><ul><li>仅在 MapReduce 作业接近完成时启动</li><li>通常是在 95%以上的任务完成后</li><li>针对的是最后剩余的几个运行中任务</li></ul></li><li><p><strong>判定标准</strong>：</p><pre><code>Master维护任务执行统计信息:
- 平均任务完成时间
- 各任务已运行时间
- 执行速度(如已处理数据量/已运行时间)

当某任务运行时间显著高于平均值时,被判定为潜在straggler
</code></pre>
</li><li><p><strong>执行策略</strong>：</p><ul><li>不取消原任务，而是并行启动备份执行</li><li>在不同机器上调度备份任务</li><li>首个完成(主执行或备份)的结果被采用</li><li>另一个执行实例被取消</li></ul></li><li><p><strong>资源管理</strong>：</p><ul><li>经过调优，通常只增加几个百分点(1-5%)的资源使用</li><li>只为少数任务创建备份，避免资源浪费</li><li>优先使用空闲资源进行备份执行</li></ul></li></ol><h4 id="">备份任务的实际效果</h4><p>论文给出了一个具体的性能改进案例：</p><pre><code>排序程序性能对比:
- 启用备份任务: 基准时间T
- 禁用备份任务: 1.44×T (多44%的执行时间)

资源使用增加: &lt;5%
时间减少: ~30%
整体效率提升: 显著
</code></pre>
<p>这表明备份任务机制是一个高投入产出比的优化：用少量额外计算资源换取显著的速度提升</p><h4 id="">深入剖析：备份任务调度策略</h4><p>备份任务不是简单地为所有慢任务创建副本，而是采用智能调度策略：</p><p><strong>1. 任务选择机制</strong></p><pre><code>// 简化的备份任务选择算法
function selectTasksForBackup() {
    candidateTasks = []

    // 每种判断慢任务的方法
    metrics = [
        {name: &quot;绝对运行时间&quot;, threshold: avg * 1.5},
        {name: &quot;进度速率&quot;, threshold: avgRate * 0.5},
        {name: &quot;预估剩余时间&quot;, threshold: avgRemaining * 2}
    ]

    // 使用多种指标识别慢任务
    for each task in runningTasks:
        for each metric in metrics:
            if task.value(metric) &gt; metric.threshold:
                candidateTasks.add(task)
                break

    // 排序并限制备份数量
    return prioritize(candidateTasks).limit(maxBackups)
}
</code></pre>
<p><strong>2. 机器选择策略</strong></p><p>备份任务的机器选择也是关键因素：</p><pre><code>优选条件:
1. 无故障历史的机器
2. 数据本地性好的机器
3. 当前负载较低的机器
4. 硬件配置较好的机器
</code></pre>
<p><strong>3. 动态调整机制</strong></p><pre><code>// 动态调整备份任务数量
function adjustBackupThreshold() {
    // 监控集群资源使用率
    clusterUtilization = getCurrentClusterUtilization()

    if clusterUtilization &gt; 0.9 { // 高负载
        // 减少备份任务，只处理极端慢的任务
        increaseBackupThreshold(0.2)
    } else if clusterUtilization &lt; 0.7 { // 低负载
        // 增加备份任务，更积极预防慢任务
        decreaseBackupThreshold(0.1)
    }
}
</code></pre>
<h4 id="">备份任务的演进与现代实现</h4><p>原始 MapReduce 的备份任务策略在现代系统中得到了进一步改进：</p><p><strong>1. Hadoop 的推测执行(Speculative Execution)</strong></p><p>Hadoop 实现了类似的机制，但增加了更多配置选项：</p><pre><code class="lang-xml">&lt;!-- Hadoop推测执行配置 --&gt;
&lt;property&gt;
  &lt;name&gt;mapreduce.map.speculative&lt;/name&gt;
  &lt;value&gt;true&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
  &lt;name&gt;mapreduce.reduce.speculative&lt;/name&gt;
  &lt;value&gt;true&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
  &lt;!-- 慢任务判定阈值 --&gt;
  &lt;name&gt;mapreduce.job.speculative.slownodethreshold&lt;/name&gt;
  &lt;value&gt;1.0&lt;/value&gt;
&lt;/property&gt;
</code></pre>
<p><strong>2. LATE 调度器</strong></p><p>UC Berkeley 提出的 LATE(Longest Approximate Time to End)调度器改进了备份任务机制：</p><pre><code>LATE调度器优化:
1. 基于估计剩余时间而非已运行时间判断stragglers
2. 考虑节点异构性，针对性能不均集群优化
3. 设置推测任务上限，避免资源浪费
4. 优先在快速节点上执行备份任务
</code></pre>
<p>这种改进使得备份任务机制在异构环境中表现更佳</p><p><strong>3. Spark 的推测执行</strong></p><p>Spark 在继承 MapReduce 理念的同时，对备份任务机制做了进一步优化：</p><pre><code>// Spark推测执行配置
spark.speculation                     true
spark.speculation.interval            100ms
spark.speculation.multiplier          1.5
spark.speculation.quantile            0.75
</code></pre>
<p>Spark 引入了任务持续时间分布的概念，使用分位数而非简单平均值来判断异常情况，进一步提高了识别准确率。</p><h4 id="">关键实施挑战与解决方法</h4><p><strong>1. 误判问题</strong></p><p>备份任务机制可能会误判正常但处理数据复杂的任务为 stragglers：</p><pre><code>解决方案:
1. 结合数据特征(如输入大小、复杂度)评估预期执行时间
2. 使用机器学习模型预测任务执行时间
3. 引入任务进度报告机制，评估实际完成百分比
</code></pre>
<p><strong>2. 资源调度冲突</strong></p><p>备份任务可能与其他作业竞争资源：</p><pre><code>解决方案:
1. 资源池隔离，为备份任务预留特定资源
2. 优先级机制，根据集群负载动态调整备份任务优先级
3. 公平调度器集成，考虑整体资源分配策略
</code></pre>
<p><strong>3. 网络拥塞</strong></p><p>备份任务可能增加网络流量：</p><pre><code>解决方案:
1. 数据本地性优先，尽量在数据本地节点执行备份
2. 差异化传输，只传输必要的数据子集
3. 网络感知调度，避免在网络瓶颈区域增加负载
</code></pre>
<h4 id="">总结</h4><p>备份任务机制是 MapReduce 框架中的一个关键创新，它通过少量资源冗余换取显著的性能提升。其核心思想是：</p><ol start="1"><li><strong>专注于关键路径</strong>：只优化影响总体完成时间的任务</li><li><strong>资源效率权衡</strong>：用 1-5%的额外资源换取 30%+的速度提升</li><li><strong>概率对抗策略</strong>：不试图预测具体哪个任务会慢，而是为所有可能的慢任务准备备份</li></ol><p>这一机制充分体现了分布式系统设计的精髓：接受部分失败是不可避免的，并通过冗余和并行执行来优雅地应对它。现代大数据系统普遍采用了这一核心思想，进一步证明了其在大规模分布式计算中的价值。</p><h2 id="refinements">Refinements（改进）</h2><p>这篇论文除了 Mapper 和 Reducer 这两个基本的原语，该系统还提供了一些后面事实上也成为了公认的标配的扩展原语：Partitioner、Combiner 和 Reader/Writter。</p><h3 id="partitioning-function">Partitioning Function</h3><p>MapReduce 中的分区函数(Partitioning Function)，这是 MapReduce 框架中的重要扩展机制。</p><h4 id="">核心概念</h4><p>分区函数是 MapReduce 中连接 Map 和 Reduce 阶段的关键组件，它决定了哪些中间键值对被发送到哪个 Reduce 任务。</p><pre><code class="lang-go">// 分区函数的基本定义
type PartitionFunc func(key interface{}, numPartitions int) int
</code></pre>
<p>核心作用:</p><ul><li>确定 Map 输出的中间键值对分配给哪个 Reduce 任务处理，最终影响输出文件组织</li><li>控制最终输出文件的数量和内容组织方式</li><li>影响数据在集群中的分布和负载均衡</li></ul><h4 id="">默认哈希分区机制</h4><p>MapReduce 提供了简单高效的默认分区策略：</p><pre><code class="lang-java">// 默认哈希分区实现
public int getPartition(K key, V value, int numReduceTasks) {
    return (key.hashCode() &amp; Integer.MAX_VALUE) % numReduceTasks;
}
</code></pre>
<p>特点:</p><ul><li><strong>简单高效</strong>: 计算开销小，适用于大多数场景</li><li><strong>相对均衡</strong>: 通过哈希函数将键均匀分散到各分区</li><li><strong>确定性</strong>: 相同键总是映射到相同分区，保证聚合正确性</li></ul><p>数据流程示意图:</p><pre><code class="lang-shell">    Map输出          分区函数            Reduce任务
 [K1:V1, K2:V2]    hash(K) % R    -&gt;    Reduce-0
 [K3:V3, K4:V4]                   -&gt;    Reduce-1
 [K5:V5, K6:V6]                   -&gt;    Reduce-2
       ...                               ...
</code></pre>
<h4 id="">自定义分区函数的使用场景</h4><p>论文指出，某些场景下需要特定的分区逻辑，例如处理 URL 数据时希望同一主机的 URL 都进入同一个输出文件 <a href="https://medium.com/towards-data-engineering/mapreduce-how-it-powers-scalable-data-processing-68e68c5e7172">6</a>。</p><p>当然不止这个，还可以根据地理位置分区。</p><h4 id="-mapreduce-">现代 MapReduce 框架中的分区扩展</h4><p>现代分布式计算框架在 Google 原始 MapReduce 的分区函数基础上进行了多种扩展：</p><p><strong>Hadoop 中的分区器实现</strong></p><pre><code class="lang-java">// Hadoop TotalOrderPartitioner示例
// 用于全局排序的分区器
public class TotalOrderPartitioner&lt;K extends WritableComparable&lt;?&gt;, V&gt;
    extends Partitioner&lt;K, V&gt; {

    private TrieNode&lt;K&gt; trie;  // 采用Trie树存储分割点
    private K[] splitPoints;   // 数据分割点

    @Override
    public int getPartition(K key, V value, int numPartitions) {
        return trie.findPartition(key);
    }

    // 使用采样数据初始化分区分割点
    public void setConf(Configuration conf) {
        // 从分布式缓存加载采样数据
        Path partFile = new Path(TotalOrderPartitioner.getPartitionFile(conf));
        // 初始化trie和分割点
        // ...
    }
}
</code></pre>
<p><strong>范围分区</strong></p><pre><code class="lang-go">// 范围分区器伪代码
func RangePartitioner(key interface{}, boundaries []interface{},
                     numPartitions int) int {
    // 使用二分查找确定键值落在哪个范围
    index := sort.Search(len(boundaries), func(i int) bool {
        return compareKeys(key, boundaries[i]) &lt; 0
    })

    return index
}

// 初始化时进行数据采样确定分区边界
func determinePartitionBoundaries(sampleSize int, numPartitions int) []interface{} {
    samples := collectRandomSamples(sampleSize)
    sort.Sort(samples)

    // 选择均匀分布的分割点
    boundaries := make([]interface{}, numPartitions-1)
    for i := 0; i &lt; numPartitions-1; i++ {
        boundaries[i] = samples[(i+1)*sampleSize/numPartitions]
    }

    return boundaries
}
</code></pre>
<p>范围分区的优势:</p><ul><li>保留顺序关系，便于范围查询</li><li>适用于有序数据集的处理</li><li>支持数据倾斜优化</li></ul><h4 id="">分区策略与数据倾斜</h4><p>分区函数的选择直接影响数据分布均衡性，不合理的分区可能导致严重的数据倾斜问题，比如下面这种情况：</p><pre><code>// 数据倾斜示例
数据集: 1000万用户行为记录
键分布: 90%的记录来自10%的热门用户

使用用户ID直接哈希:
- Reducer-1: 处理2,000万条记录 (热门用户集中)
- Reducer-2: 处理200万条记录
- ...
- Reducer-10: 处理100万条记录
</code></pre>
<p>那么我可以应对的分区策略如下：</p><pre><code class="lang-java">// 组合键分区策略
public class BalancedPartitioner extends Partitioner&lt;Text, Text&gt; {
    @Override
    public int getPartition(Text key, Text value, int numReduceTasks) {
        String originalKey = key.toString();

        // 检测热点键
        if (isHotKey(originalKey)) {
            // 为热点键添加随机前缀以分散负载
            int randomSuffix = ThreadLocalRandom.current().nextInt(numReduceTasks);
            return randomSuffix;
        } else {
            // 非热点键使用正常哈希
            return (originalKey.hashCode() &amp; Integer.MAX_VALUE) % numReduceTasks;
        }
    }

    private boolean isHotKey(String key) {
        // 根据预先统计或采样识别热点键
        // 实际应用中可能使用布隆过滤器等数据结构
        return HOT_KEYS_SET.contains(key);
    }
}
</code></pre>
<h4 id="-mapreduce-">分区函数在 MapReduce 执行流程中的位置角色</h4><p>分区函数在 MapReduce 执行过程中的位置和作用：</p><pre><code>Map阶段      →     分区阶段     →     Shuffle阶段    →    Reduce阶段
(数据处理)        (分区函数)        (网络传输)         (聚合处理)

Map输出        分区键值对         分组排序           Reduce处理
&lt;K1,V1&gt;    →  Partition(K1)=0  →  传送到Reducer-0  →  所有相同键
&lt;K2,V2&gt;       Partition(K2)=1     传送到Reducer-1     在同一Reducer处理
...           ...                 ...
</code></pre>
<p>执行细节:</p><ol start="1"><li>Map 任务完成键值对处理后，调用分区函数确定每个键值对的目标分区</li><li>按分区将键值对写入本地磁盘（为每个分区生成一个临时文件）</li><li>Reduce 任务从多个 Map 任务获取属于其分区的所有数据</li><li>分区函数保证相同键的所有值都被发送到同一个 Reduce 任务</li></ol><h4 id="">高级分区技术和最佳实践</h4><p>实际上，我还发现了一些新的分区技术。</p><h5 id="">自适应分区</h5><p>核心思想：根据实际数据分布动态调整分区决策，是处理不均匀数据的有效方法。</p><p><strong>工作原理</strong></p><p>分为两个阶段，</p><p>第一阶段：数据分析阶段，此阶段主要功能：</p><ol start="1"><li><strong>收集键频率统计</strong>：记录每个键出现的次数</li><li><strong>数据分布分析</strong>：识别热点键和数据倾斜情况</li><li><strong>生成分区策略</strong>：根据分析结果，计算最优分区策略</li></ol><p>第二阶段：优化执行阶段，此阶段主要功能：</p><ol start="1"><li><strong>应用优化策略</strong>：基于第一阶段统计结果的分区决策</li><li><strong>动态负载均衡</strong>：跟踪分区负载，动态调整热点键分配</li><li><strong>实际数据处理</strong>：执行业务逻辑处理</li></ol><p><strong>热点键识别与处理策略</strong></p><p>热点键识别是自适应分区的关键环节：</p><p>热点键处理策略：</p><ol start="1"><li><strong>散列扩展</strong>：将单个热点键扩展为多个逻辑键</li><li><strong>动态负载均衡</strong>：实时监控并平衡各分区的数据量</li><li><strong>键重组合并</strong>：将多个低频键合并处理</li></ol><p>这一块还有太多太多可以讲的东西了，一些实际情况中要遇到的挑战。</p><h5 id="">混合分区策略</h5><p>比如可以使用 url、timestamp 时间戳、geopoint 地理网格分区。现代流处理系统（如 Kafka、Flink）中的分区概念与 MapReduce 密切相关；</p><h3 id="ordering-guaranteesmapreduce-">Ordering Guarantees（MapReduce 排序保证）</h3><p><strong>排序保证的核心</strong></p><p>MapReduce 框架确保<strong>同一分区内的中间键值对按键递增顺序处理</strong>。这是框架提供的重要保证，不需要开发者额外编码实现。</p><h4 id="">技术实现与优势</h4><p>实现机制：</p><pre><code>Map阶段 → 分区 → 排序 → 归并 → Reduce处理
               ↑       ↑
             框架自动完成
</code></pre>
<p>这种排序保证带来两大关键优势：</p><ol start="1"><li><p><strong>高效随机访问</strong>：生成的文件可支持二分查找等快速检索</p><pre><code>// 有序文件随机查找示例
position = binarySearch(file, targetKey)
record = file.seek(position)
</code></pre>
</li><li><p><strong>连续键处理</strong>：便于流式处理、时间序列分析等场景</p><pre><code>// 连续键处理示例
currentKey = null
for (key, value) in sortedData:
    if key != currentKey:
        // 处理键边界
    // 处理当前记录
</code></pre>
</li></ol><h4 id="">实际应用示例</h4><ol start="1"><li><p><strong>构建索引系统</strong>：搜索引擎倒排索引生成</p></li><li><p><strong>时间序列数据处理</strong>：有序事件日志分析</p><pre><code>// 时间序列事件检测
T1: 用户登录
T2: 查看商品
T3: 添加购物车
T4: 购买
// 有序数据使模式识别更简单
</code></pre>
</li><li><p><strong>增量导出和更新</strong>：按时间戳排序的变更记录</p></li></ol><p>MapReduce 的这一排序保证，加上分区机制，为大规模数据处理提供了强大而灵活的框架基础，使各种复杂数据处理变得简单高效。</p><h3 id="combiner-function">Combiner Function</h3><h4 id="">核心概念与工作原理</h4><p>Combiner 是 MapReduce 框架的关键优化组件，<strong>在 Map 端执行部分数据聚合</strong>，减少网络传输量。</p><pre><code>工作流程:
Map输出 → Combiner本地聚合 → 网络传输 → Reducer最终聚合
</code></pre>
<p>适用条件：</p><ul><li>Map 输出的中间键有<strong>大量重复</strong></li><li>Reduce 函数具有<strong>可交换和可结合性</strong>（如求和、求最大值）</li></ul><h4 id="">性能优势示例</h4><p>以单词计数为例：</p><pre><code>无Combiner时:
Map1输出: &lt;&quot;hello&quot;,1&gt;, &lt;&quot;world&quot;,1&gt;, &lt;&quot;hello&quot;,1&gt;, &lt;&quot;hello&quot;,1&gt; // 4条记录传输
Map2输出: &lt;&quot;hello&quot;,1&gt;, &lt;&quot;hadoop&quot;,1&gt;, &lt;&quot;hello&quot;,1&gt; // 3条记录传输
总网络传输: 7条记录

使用Combiner后:
Map1输出: &lt;&quot;hello&quot;,3&gt;, &lt;&quot;world&quot;,1&gt; // 2条记录传输
Map2输出: &lt;&quot;hello&quot;,2&gt;, &lt;&quot;hadoop&quot;,1&gt; // 2条记录传输
总网络传输: 4条记录 (减少43%)
</code></pre>
<p>对于遵循 Zipf 分布的数据(如单词频率)，Combiner 可显著减少网络传输，提升性能。</p><h4 id="-reduce-">与 Reduce 的区别</h4><ul><li>Reducer 输出写入最终结果文件</li><li>Combiner 输出写入中间文件，随后传输给 Reducer</li></ul><p>Combiner 是 MapReduce 框架提高数据处理效率的重要优化手段，通过&quot;预聚合&quot;显著减少数据传输量和处理时间，对于聚合类操作尤为有效。</p><h3 id="input-and-output-types">Input and Output Types</h3><p>支持不同的输入数据的格式。如下所示：</p><pre><code>1. TextInputFormat（默认）
   - 每行作为一个记录
   - 键: 行偏移量(LongWritable)
   - 值: 行内容(Text)
   - 智能分片: 确保在行边界分割

2. KeyValueTextInputFormat
   - 按分隔符(默认Tab)将每行分为键值
   - 适用: 简单结构化文本数据

3. SequenceFileInputFormat
   - 读取二进制序列文件(键值对)
   - 支持压缩、高效随机访问
   - 常用于MapReduce作业间传递数据

4. DBInputFormat
   - 从关系数据库读取记录
   - 支持SQL查询作为数据源
</code></pre>
<p>这体现了这套框架的<strong>灵活性和扩展性</strong>。</p><p>MapReduce 的输入输出接口设计使其能处理多样化数据源：</p><pre><code>- HDFS文件
- 本地文件系统
- S3、Azure Blob等云存储
- HBase、MongoDB等NoSQL数据库
- Kafka流数据
</code></pre>
<p>通过实现适当的 InputFormat/OutputFormat，开发者可以将 MapReduce 与几乎任何数据源/目标集成，体现了框架的强大扩展性，使其适用于各种大数据处理场景。</p><h3 id="side-effects">Side-effects</h3><p>在某些情况下，MapReduce 的用户会发现，一些在 map 和 reduce operator 中生成的辅助文件作为额外输出是很便利的。</p><p>我们可以依靠写代码，将这种 side-effects 具有原子性和幂等性。</p><p>通常情况下，应用程序会写入一个临时文件，并在该文件完全生成后对其进行原子重命名，重命名该文件。</p><p>对副作用有两个基本要求：</p><ol start="1"><li><strong>原子性(Atomic)</strong>：生成过程必须是原子的，通常通过临时文件+重命名实现</li><li><strong>幂等性(Idempotent)</strong>：操作可重复执行，对任务重试很重要</li></ol><p><strong>一些常见的应用</strong></p><p>实践中常见应用：</p><ul><li>生成调试日志文件</li><li>创建优化的索引结构</li><li>输出特殊格式数据(如模型文件)</li><li>写入监控指标数据</li></ul><h3 id="skipping-bad-records">Skipping Bad Records</h3><p>如其名，跳过错误的记录。</p><p>MapReduce 中，<strong>确定性崩溃记录</strong>是常见挑战：</p><ul><li>特定记录导致 Map/Reduce 任务必然失败</li><li>无法修复 Bug（如第三方闭源库问题）</li><li>少量记录丢失在大规模统计分析中可接受</li></ul><pre><code>工作流程：
1. 检测 → 2. 报告 → 3. 识别 → 4. 跳过
</code></pre>
<p><strong>应用场景</strong></p><p>此机制特别适用于：</p><ul><li><strong>大规模数据清洗</strong>：个别格式异常记录不阻塞整体处理</li><li><strong>第三方库集成</strong>：处理外部组件对特定输入的脆弱性</li><li><strong>容忍数据不完整</strong>：统计分析允许小比例数据丢失</li></ul><p>通过跳过机制，MapReduce 提升了框架容错性，确保作业能够在面对局部数据问题时继续完成，这对生产环境中的大规模数据处理至关重要 <a href="https://research.google.com/archive/mapreduce-osdi04-slides/index-auto-0024.html">1</a><a href="https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapred/SkipBadRecords.html">2</a>.</p><h3 id="status-information">Status Information</h3><p>MapReduce Master 节点提供内置 HTTP 服务器，展示作业执行状态和各项指标。</p><p><strong>核心监控指标</strong></p><p>状态页面展示的关键信息：</p><pre><code>- 任务计数：已完成/进行中/等待任务数
- 数据量度：输入字节/中间数据字节/输出字节
- 处理速率：字节/秒、记录/秒
- 日志链接：各任务的标准输出和标准错误
- 失败信息：失败节点及其处理的任务
</code></pre>
<h3 id="counters">Counters</h3><p>MapReduce 计数器是一种轻量级分布式统计机制，用于跟踪作业执行过程中的各类事件和指标。</p><p><strong>系统实现机制</strong></p><p>计数器值的收集与聚合流程：</p><pre><code>Worker节点计数 → 定期汇报(ping) → Master聚合 → 最终结果
</code></pre>
<p>关键技术点：</p><ol start="1"><li><strong>分布式收集</strong>：计数器值通过心跳消息(ping)附带传输，避免额外通信开销</li><li><strong>全局聚合</strong>：Master 节点汇总所有成功任务的计数器值</li><li><strong>重复消除</strong>：消除重复执行(备份任务、失败重试)产生的重复计数</li><li><strong>实时展示</strong>：当前计数值在 Master 状态页面实时更新</li></ol><h2 id="performance">Performance（性能）</h2><p>论文中提到的两个性能测试代表了 MapReduce 框架应对两种典型大数据处理场景的能力：</p><p><strong>测试场景分析</strong></p><ol start="1"><li><p>模式搜索测试</p><p>：在约 1TB 数据中搜索特定模式</p><ul><li>这代表了&quot;从大数据集提取少量有价值信息&quot;的计算模式</li><li>典型应用如日志分析、异常检测、特定记录查找等</li></ul></li><li><p>大数据排序测试</p><p>：对约 1TB 数据进行排序</p><ul><li>这代表了&quot;将数据从一种表示转换为另一种表示&quot;的计算模式</li><li>典型应用如 ETL 过程、数据预处理、重组数据等</li></ul></li></ol><p>下面将会从五个角度对整套 mapreduce 分布式框架，进行性能分析：</p><ol start="1"><li>Cluster Configuration</li><li>Grep</li><li>Sort</li><li>Effect of Backup Tasks</li><li>Machine Failures</li></ol><h3 id="cluster-configuration">Cluster Configuration（集群配置分析）</h3><p>先是给定了一个集群配置：</p><pre><code class="lang-go">// 集群配置概要
type ClusterConfig struct {
    Nodes           int     // 约1800台机器
    CpuPerNode      int     // 每节点2个2GHz Intel Xeon (支持超线程)
    MemoryPerNode   string  // 每节点4GB (实际可用2.5-3GB)
    DisksPerNode    int     // 每节点2个160GB IDE磁盘
    NetworkBandwidth string  // 千兆以太网
    NetworkTopology string   // 两级树状交换网络
    RootBandwidth   string   // 100-200Gbps聚合带宽
    Latency         string   // 节点间&lt;1ms延迟
}
</code></pre>
<p><strong>分析</strong>：</p><ul><li>这是一个计算和 I/O 能力均衡的集群设计，特别适合 MapReduce 的分治模型</li><li>存储层面，每节点有超过 300GB 的总存储空间，对于 TB 级数据处理提供了足够的本地存储</li><li>网络层面采用树状拓扑，虽然简单但可能在 shuffle 阶段形成瓶颈</li><li>节点间低延迟(&lt;1ms)对 reduce 阶段的数据传输极为有利</li><li>集群规模(1800 节点)使得处理 TB 级数据时能够有效并行化</li></ul><h3 id="grep">Grep</h3><p><img alt="image-20250505175829225" src="Figure2-Data-transfer-rate-over-time.png"/></p><p>这是典型的&quot;从海量数据中提取少量信息&quot;场景：</p><pre><code class="lang-go">// Grep任务配置
type GrepJobConfig struct {
    InputSize       string  // 约1TB (10^10条100字节记录)
    Pattern         string  // 三字符模式(匹配92,337条记录)
    InputSplits     int     // M=15000(每块约64MB)
    ReduceTasks     int     // R=1(单一输出文件)
    PeakScanRate    string  // &gt;30GB/s(1764个workers时)
    TotalTime       int     // 约150秒(包括60秒启动开销)
}
</code></pre>
<p><strong>性能分析</strong>：</p><ol start="1"><li><strong>扩展性表现</strong>：从图表 2 看出，随着 worker 数量增加，扫描率线性提升至 30GB/s，表明 MapReduce 在 map 密集型任务上有优秀的水平扩展能力</li><li><strong>I/O 绑定特性</strong>：Grep 本质上是 I/O 密集型工作，测试达到 30GB/s 的吞吐率接近理论上 1764 个节点的磁盘 I/O 总和上限</li><li><strong>优化机会</strong>：约 60 秒的启动开销(占总时间 40%)显示了一个优化点 - GFS 元数据操作和任务分发可进一步优化</li><li><strong>R=1 设计</strong>：单 reduce 设计适合这种&quot;过滤&quot;场景，但也意味着最终结果汇集可能成为瓶颈(本例数据量小，未表现出来)</li></ol><h3 id="sort">Sort</h3><p><img alt="image-20250505180025136" src="Figure3-Data-transfer-rates-over-time-for-diff-exec-of-sort-program.png"/></p><p>整个 MapReduce 框架能力的综合测试：</p><pre><code class="lang-golang">// Sort任务特征分析
type SortJobAnalysis struct {
    InputSize       string  // 约1TB (10^10条100字节记录)
    InputRate       string  // 峰值13GB/s(低于Grep因需写中间数据)
    ShufflePattern  string  // 两阶段模式，与reduce任务分批相关
    OutputRate      string  // 2-4GB/s(双副本写入，实际物理写入4-8GB/s)
    MapTasks        int     // M=15000(每块约64MB)
    ReduceTasks     int     // R=4000(分区策略利用键分布知识)
    TotalTime       int     // 891秒(接近TeraSort基准1057秒)
}
</code></pre>
<p><strong>技术分析</strong>：</p><ol start="1"><li><strong>数据流水线</strong>：测试清晰展示 MapReduce 三阶段流水线 - map 阶段(0-200 秒)、shuffle 阶段(200-600 秒)和 reduce 阶段(600-850 秒)</li><li>资源瓶颈转移：
<ul><li>0-200 秒：瓶颈在磁盘 I/O 和 CPU(解析数据)</li><li>200-600 秒：瓶颈转为网络带宽(shuffle)</li><li>600-850 秒：瓶颈为排序计算和输出磁盘 I/O</li></ul></li><li><strong>局部性优化效果</strong>：输入率(13GB/s)高于 shuffle 率的主因是数据局部性优化，大部分读取走本地磁盘而非网络</li><li><strong>复制开销</strong>：输出率(2-4GB/s)较低主要因为 GFS 双副本策略，实际物理写入是这个速率的两倍</li></ol><h4 id="effect-of-backup-tasks">Effect of Backup Tasks</h4><p>图 3 还能看到一些东西：</p><pre><code class="lang-go">// 备份任务影响分析
type BackupTaskAnalysis struct {
    WithBackup      int     // 正常执行，总时间891秒
    WithoutBackup   int     // 1283秒，增加44%
    StragglerDelay  int     // 最后5个reduce任务额外花费300秒
    EfficiencyGain  string  // 备份任务机制提高44%性能
}
</code></pre>
<p><strong>专业解读</strong>：</p><ol start="1"><li><strong>掉队者问题严重性</strong>：数据清晰展示了分布式系统中&quot;掉队者问题&quot;(straggler problem)的严重性 - 仅 5 个慢任务就使总时间增加 44%</li><li><strong>根本原因分析：</strong>掉队者通常源于：
<ul><li>硬件异常(如磁盘性能下降、内存错误)</li><li>资源竞争(如其他进程干扰)</li><li>数据倾斜(某些 reduce 任务处理数据量显著多于其他)</li></ul></li><li><strong>云原生环境意义</strong>：在共享资源的云环境中，掉队者问题更加普遍，备份任务机制是确保性能可预测性的关键</li></ol><h4 id="machine-failures">Machine Failures</h4><p>图 3 还能看到</p><pre><code class="lang-go">// 故障恢复能力分析
type FaultToleranceAnalysis struct {
    NodesKilled     int     // 200个(约11.5%的节点)
    RecoveryPattern string  // 短暂的负输入率，然后快速恢复
    TotalTime       int     // 933秒，仅增加5%
    KeyMechanism    string  // 自动检测失败并重新执行任务
}
</code></pre>
<p><strong>分析</strong>：</p><ol start="1"><li><strong>失败影响可视化</strong>：图表中的负输入率直观地展示了节点故障如何导致已完成工作的丢失和重做需求</li><li><strong>快速恢复原理</strong>：
<ul><li>任务状态追踪：master 节点持续追踪每个任务的状态</li><li>心跳检测：通过周期性心跳检测 worker 失败</li><li>任务重新调度：将失效节点的任务重新分配给健康节点</li><li>冗余执行：关键是 MapReduce 设计让任何节点都能处理任何任务</li></ul></li><li><strong>对比传统系统</strong>：传统 MPP 数据库在 11%节点故障时通常会完全失败或性能下降 50%以上，MapReduce 的 5%性能损失凸显其卓越的容错能力</li></ol><h2 id="experience">Experience</h2><p>这段内容摘自 Jeff Dean 和 Sanjay Ghemawat 的 MapReduce 论文，详细描述了 MapReduce 在 Google 内部的早期发展历程和应用情况。</p><p><img alt="image-20250505180550625" src="Table1-MapReduce-jobs-run-in-august-2004.png"/></p><h3 id="">技术发展历程</h3><p>MapReduce 库的首个版本开发于 2003 年 2 月，并在同年 8 月进行了重大增强 <a href="https://research.google.com/archive/mapreduce.html">1</a>，包括：</p><ul><li>局部性优化（locality optimization）</li><li>跨工作节点的任务执行动态负载平衡</li><li>其他性能优化</li></ul><h3 id="">应用领域广泛性</h3><p>MapReduce 在 Google 内部得到了广泛应用，涵盖多个领域：</p><ol start="1"><li>大规模机器学习问题</li><li>Google News 和 Froogle（早期的 Google Shopping）产品的聚类问题</li><li>流行查询报告数据提取（如 Google Zeitgeist）</li><li>从大规模网页语料库中提取属性（如用于本地化搜索的地理位置）</li><li>大规模图计算</li></ol><h3 id="">爆发式增长</h3><p>从图表可以看出，MapReduce 在 Google 内部的使用呈现指数级增长：</p><ul><li>2003 年初：接近 0 个实例</li><li>2004 年 9 月底：接近 900 个实例</li></ul><p>这种快速增长表明 MapReduce 在 Google 内部获得了极高的认可和应用价值。</p><h3 id="">成功原因分析</h3><p>MapReduce 取得成功的关键因素：</p><ol start="1"><li><strong>简化分布式计算</strong>：使开发者能够编写简单程序并在数千台机器上高效运行</li><li><strong>加速开发周期</strong>：大幅缩短开发和原型设计周期</li><li><strong>降低技术门槛</strong>：让没有分布式/并行系统经验的程序员也能轻松利用大规模计算资源</li></ol><h3 id="2004--8-">规模与效率分析（2004 年 8 月数据）</h3><p>从表格数据可以得出以下见解：</p><ol start="1"><li><strong>使用广泛</strong>：单月内执行了 29,423 个 MapReduce 作业</li><li><strong>处理效率高</strong>：平均作业完成时间为 634 秒（约 10.5 分钟）</li><li><strong>计算规模大</strong>：
<ul><li>使用了相当于 79,186 天的机器计算时间</li><li>处理了 3,288 TB 的输入数据</li><li>产生了 758 TB 的中间数据</li><li>输出了 193 TB 的结果数据</li></ul></li><li><strong>任务分布特征</strong>：
<ul><li>每个作业平均使用 157 台工作机器</li><li>平均每个作业有 1.2 次工作节点失效（表明系统具有良好的容错能力）</li><li>平均每个作业有 3,351 个 map 任务和 55 个 reduce 任务</li></ul></li><li><strong>代码复用性</strong>：
<ul><li>395 个独特的 map 实现</li><li>269 个独特的 reduce 实现</li><li>426 个独特的 map/reduce 组合</li></ul></li></ol><h3 id="large-scale-indexing">Large-Scale Indexing</h3><p>MapReduce 在 Google 网络搜索索引系统中的应用，这是 MapReduce 最重要的应用案例之一。</p><h4 id="google-">Google 搜索索引系统概述</h4><p>Google 使用 MapReduce 重写了其整个生产索引系统，该系统负责生成 Google 网络搜索服务所需的数据结构 <a href="https://research.google/pubs/mapreduce-simplified-data-processing-on-large-clusters/">1</a>。这个索引系统具有以下特点：</p><ul><li><strong>输入数据</strong>：来自爬虫系统抓取的大量网页文档，存储在 GFS (Google File System) 文件中</li><li><strong>数据规模</strong>：原始内容超过 20TB</li><li><strong>处理流程</strong>：索引过程由 5-10 个 MapReduce 操作序列组成</li></ul><p>有机会我再去看一下这套系统的设计，再进一步做一下分析。</p><h2 id="mapreduce-">MapReduce 和一些其他的并行计算系统的对比分析</h2><p>MapReduce 的关键优势在于：</p><ol start="1"><li><strong>受限但强大的编程模型</strong>：通过限制编程模型，实现了自动并行化和透明的容错机制</li><li><strong>大规模扩展能力</strong>：能扩展到数千个处理器的规模</li><li><strong>自动处理机器故障</strong>：对比其他系统将故障处理细节留给程序员</li></ol><h3 id="">特性设计</h3><ol start="1"><li><p>局部性优化 (Locality Optimization)</p><p>MapReduce 的局部性优化借鉴了主动磁盘 (Active Disks) 技术：</p><ul><li><p><strong>核心思想</strong>：将计算推送到靠近本地磁盘的处理元素，减少通过 I/O 子系统或网络发送的数据量</p></li><li><p><strong>实现差异</strong>：MapReduce 在直接连接少量磁盘的普通处理器上运行，而非直接在磁盘控制器处理器上运行</p></li><li><p><strong>优势</strong>：显著减少网络传输，提高大规模数据处理效率</p></li></ul></li><li><p>备份任务机制 (Backup Tasks)</p><p>这一机制类似于 Charlotte 系统中的积极调度机制 (eager scheduling)：</p><ul><li><p><strong>创新点</strong>：MapReduce 增加了跳过错误记录的机制，解决了简单积极调度中反复失败导致整个计算无法完成的问题</p></li><li><p><strong>实现方式</strong>：当任务接近完成时，调度冗余执行的任务，大大减少了非均匀性（如慢速或卡住的工作节点）对完成时间的影响</p></li></ul></li><li><p>集群管理系统</p><p>MapReduce 实现依赖于内部集群管理系统：</p><ul><li><p><strong>功能</strong>：负责在大量共享机器上分发和运行用户任务</p></li><li><p><strong>类似系统</strong>：Condor 等工作负载管理系统</p></li></ul></li></ol><h3 id="">与其他系统的技术比较</h3><ol start="1"><li><p>MapReduce vs. NOW-Sort</p><p>MapReduce 的排序功能在操作上类似于 NOW-Sort：</p><ul><li><p><strong>相似点</strong>：源机器（map worker）对数据进行分区并将其发送给 R 个 reduce worker，每个 reduce worker 在本地排序</p></li><li><p><strong>差异点</strong>：MapReduce 具有用户可定义的 Map 和 Reduce 函数，使其应用范围更广</p></li></ul></li><li><p>MapReduce vs. River</p><p>River 提供了一种进程通过分布式队列发送数据进行通信的编程模型：</p><pre><code>- **共同目标**：在异构硬件或系统扰动引入的不均匀性存在的情况下，提供良好的平均情况性能</code></pre><ul><li><p>实现方法差异：</p><ul><li><p>River：通过谨慎调度磁盘和网络传输来实现平衡的完成时间</p></li><li><p>MapReduce：通过限制编程模型，将问题划分为大量细粒度任务，并在可用工作节点上动态调度这些任务，使得更快的工作节点处理更多任务</p></li></ul></li></ul></li><li><p>MapReduce vs. BAD-FS</p><p>尽管编程模型完全不同，且 BAD-FS 针对广域网执行作业，两者仍有根本相似之处：</p><ol start="1"><li>都使用冗余执行来从故障引起的数据丢失中恢复</li><li>都使用位置感知调度来减少通过拥塞网络链路发送的数据量</li></ol></li><li><p>MapReduce vs. TACC</p><p>TACC 是一个旨在简化高可用网络服务构建的系统：</p><ul><li><strong>共同点</strong>：都依赖重新执行作为实现容错的机制</li></ul></li></ol><h3 id="">技术创新总结</h3><p>MapReduce 的主要技术创新可归纳为以下几点：</p><ol start="1"><li><strong>简化的并行编程模型</strong>：通过限制编程模型，使框架能够自动处理并行化和容错</li><li><strong>大规模容错实现</strong>：扩展到数千处理器规模，自动处理机器故障</li><li><strong>细粒度任务分解</strong>：将问题分解为大量细粒度任务，实现更好的负载均衡和故障恢复</li><li><strong>动态任务调度</strong>：根据工作节点的速度动态分配任务，优化整体性能</li><li><strong>冗余执行优化</strong>：在作业结束时调度冗余任务，显著减少在存在非均匀性情况下的完成时间</li></ol><p>这些创新和设计选择使 MapReduce 成为一个独特而强大的分布式计算框架 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">1</a>，它不仅借鉴了以前系统的优点，还通过简化的编程模型和自动化的故障处理解决了大规模分布式计算的关键挑战。MapReduce 的设计思想后来极大地影响了 Hadoop 等开源大数据处理框架的发展 <a href="https://cs162.org/static/hw/hw-map-reduce-rs/docs/fault-tolerance/">5</a>，成为现代大数据处理的基础。</p><h2 id="conclusions">Conclusions（结论）</h2><p>嗯是的，这篇论文为什么这么有名，论文的 conclusions 也讲清楚了。如下：</p><h3 id="mapreduce-">MapReduce 成功的三大关键因素</h3><h4 id="1-">1. 简单易用的编程模型</h4><p>MapReduce 的首要成功因素是其简洁的编程接口：</p><pre><code class="lang-go">// 用户只需定义这两个函数，无需关心分布式系统复杂性
func Map(key, value string) []KeyValue { /* 用户定义的映射逻辑 */ }
func Reduce(key string, values []string) string { /* 用户定义的归约逻辑 */ }
</code></pre>
<p>这种设计对开发者极为友好，因为它：</p><ul><li>隐藏了并行化的复杂细节</li><li>自动处理容错机制</li><li>内置了局部性优化</li><li>提供了透明的负载均衡</li></ul><p>这使得即使没有分布式系统经验的程序员也能轻松编写高效的分布式程序 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">1</a>。</p><h4 id="2-">2. 强大的表达能力</h4><p>MapReduce 模型能够轻松表达各种不同类型的计算问题，在 Google 内广泛应用于：</p><ul><li>网络搜索服务数据生成</li><li>大规模排序</li><li>数据挖掘</li><li>机器学习</li><li>其他众多系统</li></ul><p>这种通用性使 MapReduce 成为 Google 内部的基础计算框架 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">1</a>。</p><h4 id="3-">3. 出色的扩展性</h4><p>MapReduce 实现能够扩展到包含数千台机器的大型集群：</p><pre><code class="lang-go">// 伪代码：MapReduce调度过程
func Schedule(input []string, mappers int, reducers int) Result {
    // 自动处理:
    // 1. 任务分配与并行化
    // 2. 机器故障检测与恢复
    // 3. 数据本地性优化
    // 4. 中间结果管理
}
</code></pre>
<p>这使其能够高效处理 Google 遇到的大规模计算问题，为大数据处理奠定了基础 <a href="https://www.talend.com/resources/what-is-mapreduce/">3</a>。</p><h3 id="">研究团队的三大关键经验</h3><h4 id="1-">1. 受限编程模型的价值</h4><p>研究表明，通过有意识地限制编程模型，可以获得巨大的系统优势：</p><ul><li>易于并行化和分布计算</li><li>自然地实现容错机制</li><li>降低开发和维护成本</li></ul><p>这种&quot;少即是多&quot;的理念，与其他尝试提供完全通用并行编程环境的系统形成鲜明对比 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">1</a>。</p><h4 id="2-">2. 网络带宽是稀缺资源</h4><p>研究团队发现网络带宽是分布式系统中的宝贵资源，因此许多优化都针对减少网络传输：</p><ul><li><strong>局部性优化</strong>：优先从本地磁盘读取数据，减少跨网络数据传输</li><li><strong>本地中间数据存储</strong>：将中间结果写入本地磁盘而非分布式存储，节省网络带宽</li></ul><p>这些设计在大规模集群中特别重要，因为数据传输可能成为系统瓶颈 <a href="https://research.google.com/archive/mapreduce-osdi04.pdf">1</a><a href="https://www.talend.com/resources/what-is-mapreduce/">3</a>。</p><h4 id="3-">3. 冗余执行的重要性</h4><p>冗余执行是 MapReduce 的一项关键创新，用于：</p><ul><li>减少慢速机器（stragglers）的影响</li><li>优雅处理机器故障</li><li>防止数据丢失</li></ul><pre><code class="lang-go">// 伪代码：MapReduce中的冗余任务调度
func scheduleBackupTasks(slowTasks []Task) {
    for _, task := range slowTasks {
        if time.Now() - task.StartTime &gt; slowThreshold {
            // 在另一台机器上启动相同任务的备份副本
            launchDuplicateTask(task)
        }
    }
}
</code></pre>
<p>这种机制显著提高了大型分布式系统的可靠性和性能一致性 <a href="https://www.talend.com/resources/what-is-mapreduce/">3</a><a href="https://www.tutorialspoint.com/advantages-of-hadoop-mapreduce-programming">5</a>。</p><h3 id="mapreduce-">MapReduce 的技术遗产</h3><p>MapReduce 论文的结论揭示了它不仅仅是一个技术创新，更是一种全新的大规模数据处理范式：</p><ol start="1"><li><strong>架构思想影响</strong>：MapReduce 的设计理念影响了后来的 Hadoop、Spark 等众多大数据处理框架</li><li><strong>编程模型革新</strong>：证明了简化的编程模型可以解决复杂的分布式计算问题</li><li><strong>工程实践变革</strong>：改变了构建大规模数据处理系统的方法，从专家系统转变为通用框架</li><li><strong>商业价值创造</strong>：为后来的大数据生态系统奠定了基础，创造了巨大的商业价值 <a href="https://www.dremio.com/wiki/mapreduce-programming-model/">4</a><a href="https://www.tutorialspoint.com/advantages-of-hadoop-mapreduce-programming">5</a></li></ol><p>总的来说，MapReduce 通过简单而强大的抽象，成功解决了大规模分布式数据处理的核心挑战，使得处理海量数据变得触手可及，这也是为什么它在 Google 内部和整个行业中都取得了巨大成功的根本原因。</p><h2 id="references">References</h2><p><span>[1] [MapReduce: Simplified Data Processing on Large Clusters]</span>(<a href="https://www.qtmuniao.com/2019/04/30/map-reduce/">https://www.qtmuniao.com/2019/04/30/map-reduce/</a>)</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Distributed System</category>
            <category>MapReduce</category>
        </item>
        <item>
            <title><![CDATA[高并发API管理场景下的网关架构设计]]></title>
            <link>https://blog.cheverjohn.me/zh/gateway-high-concurrency-api-management</link>
            <guid>https://blog.cheverjohn.me/zh/gateway-high-concurrency-api-management</guid>
            <pubDate>Thu, 27 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[整体架构设计原则 在高并发 API 管理场景下，网关架构需要遵循以下核心设计原则： 1. 分层架构设计 采用严格分层的架构，每一层关注点分离，使系统更易于横向扩展和纵向优化 2. 无状态设计 设计无状态的网关节点，确保任何网关实例都能处理任何请求，这是支撑高并发的基础。会话状态与用户信息应存储在分布式缓存或专门的状态存储系统中。 高性能技术栈选择 1. 底层技术选型 1. 数据面：基于高性能代理如 Envoy、NGINX 或基于 Go/Rust 自研组件 2. 控制面：采用高效的配置管理和服务发现机制 2. 异步 IO 模型 1. 采用非阻塞 IO 模型(如 Go 的 goroutine+ch...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="">整体架构设计原则</h2><p>在高并发 API 管理场景下，网关架构需要遵循以下核心设计原则：</p><ol start="1"><li><p>分层架构设计</p><pre><code class="lang-shell">流量入口层 → 处理层 → 路由层 → 后端服务层
</code></pre>
<p>采用严格分层的架构，每一层关注点分离，使系统更易于横向扩展和纵向优化</p></li><li><p>无状态设计</p><p>设计无状态的网关节点，确保任何网关实例都能处理任何请求，这是支撑高并发的基础。会话状态与用户信息应存储在分布式缓存或专门的状态存储系统中。</p></li></ol><h2 id="">高性能技术栈选择</h2><ol start="1"><li>底层技术选型
<ol start="1"><li>数据面：基于高性能代理如 Envoy、NGINX 或基于 Go/Rust 自研组件</li><li>控制面：采用高效的配置管理和服务发现机制</li></ol></li><li>异步 IO 模型
<ol start="1"><li>采用非阻塞 IO 模型(如 Go 的 goroutine+channel、Rust 的 tokio、Node.js 的事件循环)</li><li>避免使用传统的线程池模型，减少上下文切换开销</li></ol></li></ol><h2 id="">多级缓存架构</h2><ol start="1"><li><p>全局分布式缓存层</p><pre><code class="lang-shell">客户端 → CDN → 边缘缓存 → API网关本地缓存 → 服务缓存
</code></pre>
</li><li><p>多维度缓存策略</p><ol start="1"><li><strong>路由信息缓存</strong>：本地高速缓存+定期更新</li><li><strong>认证信息缓存</strong>：分布式令牌验证结果缓存</li><li><strong>响应数据缓存</strong>：基于内容特性的智能缓存策略</li></ol></li></ol><h2 id="">动态扩缩容设计</h2><ol start="1"><li><p>灵活的部署架构</p><pre><code class="lang-shell">多区域 → 多可用区 → 多集群 → 多实例
</code></pre>
</li><li><p>弹性伸缩策略</p><ol start="1"><li><strong>预测式扩容</strong>：基于历史流量模式预测扩容需求</li><li><strong>反应式扩容</strong>：根据实时指标(CPU、内存、请求队列深度)触发扩容</li><li><strong>平滑缩容</strong>：确保连接优雅关闭和请求完成处理</li></ol></li></ol><h2 id="">高效流量控制机制</h2><ol start="1"><li><p>多级限流设计</p><pre><code class="lang-mermaid">flowchart LR
    Client --&gt; GlobalLimit[全局限流层]
    GlobalLimit --&gt; ServiceLimit[服务级限流]
    ServiceLimit --&gt; APILimit[API级限流]
    APILimit --&gt; UserLimit[用户级限流]
    UserLimit --&gt; Backend[后端服务]
</code></pre>
</li><li><p>自适应流控算法</p><ol start="1"><li><strong>令牌桶+漏桶组合</strong>：解决突发流量与稳定流量的平衡</li><li><strong>基于请求优先级的差异化处理</strong>：核心 API 优先保障</li><li><strong>自适应限流</strong>：基于后端服务健康度动态调整限流阈值</li></ol></li></ol><h2 id="">网关集群高可用设计</h2><ol start="1"><li><p>多区域部署架构</p><ol start="1"><li>地理级别冗余：跨区域部署确保区域级故障隔离</li><li>就近接入：智能 DNS 或全局负载均衡实现流量就近接入<a href="https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway">4</a></li></ol></li><li><p>故障隔离策略</p><pre><code class="lang-shell">客户端分组 → 网关实例分组 → 后端服务分组
</code></pre>
<ol start="1"><li><strong>舱壁模式</strong>：将客户端请求隔离到不同的网关实例组</li><li><strong>熔断机制</strong>：智能熔断设计，基于错误率、延迟等多维度指标</li><li><strong>降级策略</strong>：定义清晰的服务降级路径和回退机制</li></ol></li></ol><h2 id="">请求处理优化</h2><ol start="1"><li><p>请求处理流水线</p><pre><code class="lang-shell">接收请求 → 认证授权 → 请求转换 → 路由决策 → 负载均衡 → 后端调用 → 响应处理
</code></pre>
</li><li><p>性能优化技术</p><ol start="1"><li><strong>批处理</strong>：合并碎片化请求减少网络往返</li><li><strong>请求折叠</strong>：合并对相同资源的并发请求<a href="https://microservices.io/patterns/apigateway.html">1</a></li><li><strong>并行处理</strong>：跨服务请求并行化处理</li><li><strong>响应流式处理</strong>：大型响应的流式传输</li><li><strong>零拷贝技术</strong>：减少数据复制环节</li></ol></li></ol><h2 id="">高效通信协议</h2><ol start="1"><li>协议支持和优化
<ol start="1"><li><strong>HTTP/2 多路复用</strong>：减少连接建立开销</li><li><strong>gRPC 支持</strong>：高效二进制传输与流处理</li><li><strong>WebSocket 优化</strong>：长连接管理与心跳机制</li></ol></li><li>连接池管理
<ol start="1"><li>动态调整的后端连接池</li><li>长连接复用与保活策略</li><li>连接预热机制避免冷启动延迟</li></ol></li></ol><h2 id="">全链路可观测性</h2><ol start="1"><li><p>多维度监控体系</p><pre><code class="lang-shell">基础设施指标 → 网关性能指标 → API调用指标 → 业务指标
</code></pre>
</li><li><p>实时监控与预警</p><ol start="1"><li><strong>健康检查</strong>：主动和被动健康检测结合</li><li><strong>性能分析</strong>：请求延迟分布、队列深度等关键指标</li><li><strong>异常检测</strong>：基于机器学习的异常行为识别</li></ol></li></ol><h2 id="">配置热更新机制</h2><ol start="1"><li>动态配置架构
<ol start="1"><li>分布式配置中心+本地缓存</li><li>配置变更事件通知机制</li><li>增量配置更新减少资源消耗</li></ol></li><li>灰度发布能力
<ol start="1"><li>配置变更的金丝雀发布</li><li>流量迁移的平滑切换</li><li>紧急回滚机制</li></ol></li></ol></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Gateway</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[微服务架构下的挑战：API网关的救赎之路]]></title>
            <link>https://blog.cheverjohn.me/zh/gateway-api-management-in-multi-scenories</link>
            <guid>https://blog.cheverjohn.me/zh/gateway-api-management-in-multi-scenories</guid>
            <pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[在日常上网冲浪的时候看到一篇文章，结合公司实际业务有感。 面临挑战 这篇文章大概讲述了在微服务架构下客户端应用如何访问微服务的问题。主要面临以下挑战： 1. API 粒度不匹配：微服务通常提供细粒度 API，而客户端需要综合数据，导致客户端需要与多个服务交互 2. 客户端需求差异：不同客户端（桌面浏览器、移动端、第三方应用）需要不同的数据结构和量 3. 网络性能差异：移动网络通常比内网慢且延迟高，影响用户体验 4. 服务实例动态变化：服务实例数量和位置（主机+端口）会不断变化 5. 协议多样性：后端服务可能使用不同协议，有些对 Web 不友好 具体细节就不补充了。可以私聊 hhhhhh 交流。...]]></description>
            <content:encoded><![CDATA[<article><div><p>在日常上网冲浪的时候看到一篇<a href="https://microservices.io/patterns/apigateway.html">文章</a>，结合公司实际业务有感。</p><h2 id="">面临挑战</h2><p>这篇文章大概讲述了在微服务架构下客户端应用如何访问微服务的问题。主要面临以下挑战：</p><ol start="1"><li><strong>API 粒度不匹配</strong>：微服务通常提供细粒度 API，而客户端需要综合数据，导致客户端需要与多个服务交互</li><li><strong>客户端需求差异</strong>：不同客户端（桌面浏览器、移动端、第三方应用）需要不同的数据结构和量</li><li><strong>网络性能差异</strong>：移动网络通常比内网慢且延迟高，影响用户体验</li><li><strong>服务实例动态变化</strong>：服务实例数量和位置（主机+端口）会不断变化</li><li><strong>协议多样性</strong>：后端服务可能使用不同协议，有些对 Web 不友好</li></ol><p>具体细节就不补充了。可以私聊 hhhhhh 交流。</p><h2 id="">解决方案</h2><p>然后作者给出了他的解决方案，就是实现 API Gateway 模式：</p><ol start="1"><li><strong>实现统一入口</strong>：设计一个 API 网关作为所有客户端的单一入口点</li><li><strong>请求处理方式</strong>：
<ul><li>简单代理/路由请求到对应服务</li><li>扇出请求到多个服务并聚合结果</li></ul></li><li><strong>客户端特定 API</strong>：为不同客户端提供定制化 API，而非一刀切方案</li><li><strong>实现横切关注点</strong>：加入认证、授权、SSL 终止、缓存等功能</li></ol><p><strong>文章还介绍了一个变种：Backends for frontends(BFF) 模式，即为每种客户端类型（Web 应用、移动应用、第三方应用）设计专用 API 网关。</strong></p><p>个人分析：</p><h3 id=""><strong>优点</strong></h3><p><strong>集中化管理</strong>：所有 API 流量通过单一入口，便于统一监控、追踪和治理</p><pre><code class="lang-shell">在某金融机构实施后，安全事件响应时间从小时级降至分钟级，因为所有可疑流量都能在网关层快速识别。
</code></pre>
<p><strong>横切关注点统一实现</strong>：认证、授权、限流等功能只需在网关层实现一次</p><p><strong>协议转换与聚合</strong>：将内部多种协议转换为统一外部接口，减少客户端请求次数，某电商将 7 次独立调用合并为 1 次聚合 API，移动端加载时间减少 68%。</p><p><strong>解耦实现</strong>：客户端与微服务内部结构解耦，服务可独立演进而不影响客户端</p><p><strong>流量控制</strong>：具备智能路由、负载均衡、熔断降级能力，提高系统弹性</p><p>上面这些内容都是老生常谈的了。</p><h3 id=""><strong>缺点</strong></h3><p><strong>潜在单点故障</strong>：所有流量依赖单一组件，若设计不当可能成为系统瓶颈，当燃这个就得看高可用分布式架构师的能力啦～</p><p><strong>延迟增加</strong>：增加额外网络跳转，可能增加响应时间（通常为 5-10ms）</p><p><strong>复杂度陡增</strong>：随着规模扩大，网关可能变得臃肿且难以维护</p><blockquote><p>一家大型企业的 API 网关积累了超过 200 个定制化处理逻辑，最终导致无人敢修改代码。</p></blockquote>
<p><strong>团队协作挑战</strong>：网关变更需要跨团队协调，可能形成开发瓶颈</p><h3 id="">适用场景</h3><ol start="1"><li><strong>中小型微服务架构</strong>：服务数量适中（10-50 个），客户端类型有限</li><li><strong>安全要求高的系统</strong>：需要统一安全策略的金融、医疗等领域</li><li><strong>混合协议环境</strong>：内部服务使用不同协议（REST、gRPC、AMQP 等）</li><li><strong>需要统一网关治理</strong>：企业级应用需要集中化 API 治理策略</li></ol><h2 id="">核心功能与实现效果</h2><p>然后他还给出了他的 API 网关提供三大核心功能：</p><ol start="1"><li><strong>反向代理/网关路由</strong>：使用第 7 层路由重定向 HTTP 请求</li><li><strong>请求聚合</strong>：将多个内部微服务请求聚合为单个客户端请求</li><li><strong>横切关注点与网关卸载</strong>：集中实现通用功能</li></ol><h3 id="">优势</h3><ol start="1"><li><strong>客户端与微服务解耦</strong>：隔离客户端与微服务架构细节</li><li><strong>提供最优 API</strong>：为各类客户端提供定制化 API</li><li><strong>减少请求次数</strong>：单次请求获取多服务数据，降低网络开销</li><li><strong>简化客户端逻辑</strong>：复杂调用逻辑从客户端迁移到 API 网关</li><li><strong>协议转换</strong>：将公共 Web 友好 API 协议转换为内部协议</li></ol><h3 id="">缺点</h3><ol start="1"><li><strong>增加复杂性</strong>：新增一个需要开发、部署和管理的组件</li><li><strong>增加响应时间</strong>：额外网络跳转可能增加延迟（对大多数应用影响不大）</li><li><strong>单点故障风险</strong>：单一 API 网关可能成为瓶颈或单点故障</li></ol><p>果然和我所想的是一样的。</p><h2 id="">实际部署策略</h2><pre><code class="lang-shell">┌───────────┐  ┌───────────┐  ┌─────────────┐
│  Web BFF  │  │ Mobile BFF│  │ Third-party │
│           │  │           │  │   BFF       │
└─────┬─────┘  └─────┬─────┘  └─────┬───────┘
      │              │              │
      ▼              ▼              ▼
┌─────────────────────────────────────┐
│        Core API Gateway             │
│    (认证、监控、限流、基础路由)         │
└─────────────────────────────────────┘
      │              │              │
      ▼              ▼              ▼
┌─────────┐     ┌───────────┐     ┌───────────┐
│ 服务集群1│     │ 服务集群2   │     │ 服务集群3  │
└─────────┘     └───────────┘     └───────────┘
</code></pre>
<h3 id="">规模化考量</h3><ol start="1"><li><strong>小型应用（5-15 个微服务）</strong>：
<ul><li>推荐：单一 API 网关</li><li>理由：简单高效，避免过度设计</li></ul></li><li><strong>中型应用（15-50 个微服务）</strong>：
<ul><li>推荐：基础 API 网关+少量关键 BFF</li><li>理由：平衡复杂性和客户端优化需求</li></ul></li><li><strong>大型应用（50+微服务）</strong>：
<ul><li>推荐：完整三层架构（边缘网关+BFF 层+内部网关）</li><li>理由：支持组织扩展和复杂业务领域</li></ul></li></ol><h3 id="">决策要点</h3><p>选择 API 网关策略时需考虑：</p><ol start="1"><li><strong>组织结构</strong>：Conway 法则表明系统设计往往反映组织结构</li><li><strong>扩展预期</strong>：考虑 3-5 年内的业务增长和多样化</li><li><strong>运维能力</strong>：评估团队管理多个网关的能力</li><li><strong>一致性需求</strong>：业务对 API 行为一致性的要求程度</li><li><strong>性能预算</strong>：额外网络跳转的延迟影响评估</li></ol><p><strong>无论选择何种模式，都应保持网关层轻量化，避免业务逻辑下沉过多导致&quot;智能管道&quot;反模式，并建立良好的监控体系，确保网关层性能与稳定性。</strong></p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Gateway</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[网关性能瓶颈分析与优化技术（私藏）]]></title>
            <link>https://blog.cheverjohn.me/zh/performance-bottleneck-analysis-and-optimization-tech</link>
            <guid>https://blog.cheverjohn.me/zh/performance-bottleneck-analysis-and-optimization-tech</guid>
            <pubDate>Thu, 18 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[2025-03-07 更新：最近小红书的网关实践，我看他们就遵守了下面的好多条。—— 《小红书推出自研 Rust 高性能七层网关 ROFF》 这篇文章分两部分： 1. 性能瓶颈分析方法 2. 上手实际来分析一下 APISIX、Kong、Nginx 性能瓶颈分析方法 性能瓶颈分析方法（真） 这一部分真讲方法。 监控指标分析 - 延迟指标：监控请求延迟（Latency）和集成延迟（Integration Latency） - 错误率指标：监控 4XX 和 5XX 错误率，找出系统故障点 - 吞吐量指标：请求计数（Count）和每秒请求数（RPS） - 缓存命中指标：监控 CacheHitCount...]]></description>
            <content:encoded><![CDATA[<article><div><p>2025-03-07 更新：最近小红书的网关实践，我看他们就遵守了下面的好多条。—— <a href="https://mp.weixin.qq.com/s/wnkYr4qKIFmh9E9H_XcwTA">《小红书推出自研 Rust 高性能七层网关 ROFF》</a></p><p>这篇文章分两部分：</p><ol start="1"><li><a href="#性能瓶颈分析方法">性能瓶颈分析方法</a></li><li><a href="#上手实际来分析一下">上手实际来分析一下 APISIX、Kong、Nginx</a></li></ol><h2 id="">性能瓶颈分析方法</h2><h3 id="">性能瓶颈分析方法（真）</h3><p>这一部分真讲方法。</p><h4 id=""><strong>监控指标分析</strong></h4><ul><li>延迟指标：监控请求延迟（Latency）和集成延迟（Integration Latency）</li><li>错误率指标：监控 4XX 和 5XX 错误率，找出系统故障点</li><li>吞吐量指标：请求计数（Count）和每秒请求数（RPS）</li><li>缓存命中指标：监控 CacheHitCount 和 CacheMissCount 比率</li></ul><h4 id=""><strong>系统资源监控</strong></h4><ul><li>CPU 使用率分析</li><li>内存消耗分析</li><li>网络 IO 监控</li><li>磁盘 IO 监控（尤其是日志和缓存）</li></ul><h4 id=""><strong>请求流分析</strong></h4><ul><li>请求处理链路追踪</li><li>热点路径识别</li><li>串行处理瓶颈发现</li></ul><h3 id="">基础架构层面优化</h3><h4 id="">硬件资源优化</h4><ul><li>增加 CPU 核心数和内存容量</li><li>使用 SSD 存储提高 IO 速度</li><li>采用高性能网卡降低网络延迟</li><li>合理评估和调整容器资源限制（在 Kubernetes 环境中）</li></ul><h4 id="">网络优化</h4><ul><li>优化网络拓扑结构</li><li>减少网络跳数</li><li>使用 CDN 加速静态内容</li><li>实施 DNS 优化</li><li>网关与后端服务部署在同一网络区域</li></ul><h4 id="">操作系统调优</h4><ul><li>调整 TCP/IP 栈参数（如增大连接队列）</li><li>优化文件描述符限制</li><li>调整内核参数（如 somaxconn、tcp<em>fin</em>timeout）</li><li>在 Linux 环境下优化客户端线程数</li></ul><h3 id="">网关配置层面优化</h3><h4 id="">连接池优化</h4><ul><li>配置适当的数据库连接池大小（建议至少与预期客户端数量相当）—— <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">Oracle 的数据库管理员必知</a></li><li>调整后端服务连接池参数</li><li>设置连接超时和重试策略</li><li>实施连接保活机制</li></ul><h4 id="http-">HTTP 优化</h4><ul><li>启用 HTTP keep-alive 重用 TCP 连接 —— <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">oracle 的性能实践</a></li><li>配置合适的 chunked encoding 策略</li><li>调整 HTTP 标头大小限制</li><li>设置合理的请求/响应超时时间</li></ul><h4 id="">缓存策略优化</h4><ul><li>实施响应缓存减少后端调用 —— <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-optimize.html">Amazon 的 api 优化实践</a></li><li>配置缓存键策略和 TTL（存活时间）</li><li>实施多级缓存策略</li><li>缓存验证和失效策略优化</li></ul><h3 id="">请求处理优化</h3><h4 id="">并发处理优化</h4><ul><li>增加工作线程数量</li><li>实施异步处理模式</li><li>优化线程池配置</li><li>使用事件驱动架构处理高并发请求</li></ul><h4 id="">负载均衡策略</h4><ul><li>实施智能负载均衡算法（加权轮询、最少连接等）</li><li>配置动态负载均衡策略</li><li>实施服务健康检查和自动故障转移</li><li>根据后端服务能力调整权重</li></ul><h4 id="">路由优化</h4><ul><li>优化路由查找算法</li><li>实施路由缓存</li><li>配置路由预热策略</li><li>基于流量特征的智能路由</li></ul><h3 id="">数据处理优化</h3><h4 id="">消息处理优化</h4><ul><li>配置&quot;溢出到磁盘&quot;的策略处理大型消息（如设置为&gt;4MB 的消息写入磁盘）<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>实施请求/响应压缩<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-optimize.html">1</a></li><li>优化序列化/反序列化过程</li><li>减少不必要的数据转换</li></ul><h4 id="">协议优化</h4><ul><li>使用 HTTP/2 降低延迟并提高并行处理能力</li><li>在适当场景下使用 WebSocket 等长连接协议</li><li>考虑使用 gRPC 提高微服务间通信效率</li><li>实施协议升级策略</li></ul><h3 id="">监控与日志优化</h3><h4 id="">日志优化</h4><ul><li>减少不必要的追踪信息（将生产环境设置为 ERROR 或 FATAL 级别）<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>禁用或减少访问日志记录<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>禁用事务日志以减轻磁盘 IO 负担<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>实施异步日志写入策略</li></ul><h4 id="">监控优化</h4><ul><li>禁用实时监控减少开销<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>禁用或减少流量监控<a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>配置合理的监控采样率</li><li>实施智能告警阈值，避免监控系统负载过高</li></ul><h3 id="">安全性能优化</h3><h4 id="">身份验证优化</h4><ul><li>缓存认证结果</li><li>使用轻量级 Token 验证</li><li>实施分级认证策略</li><li>优化 JWT 处理流程</li></ul><h4 id="ssltls-">SSL/TLS 优化</h4><ul><li>使用 Session 复用减少握手开销</li><li>配置 OCSP 装订（Stapling）</li><li>实施 TLS 连接池</li><li>选择高效加密套件</li></ul><h3 id="">高级优化策略</h3><h4 id="">熔断与限流</h4><ul><li>实施请求限流保护后端系统</li><li>配置熔断器防止系统过载</li><li>实施退避算法处理重试</li><li>流量整形优化请求分布</li></ul><h4 id="">服务网格集成</h4><ul><li>与 Istio/Envoy 等服务网格集成</li><li>下放部分网关功能至边车代理</li><li>实施网关与服务网格的协同策略</li><li>利用服务网格提供的流量管理能力</li></ul><h4 id="">架构优化</h4><ul><li>考虑多级网关架构（边缘网关+内部网关）</li><li>实施边缘计算减少延迟</li><li>领域驱动的 API 网关设计</li><li>基于流量特征的网关分片</li></ul><p>通过系统地分析上述各方面的性能瓶颈，并应用相应的优化技术，可以显著提升网关的性能、吞吐量和可靠性。最关键的是要根据实际系统特点和业务需求，选择最合适的优化策略组合，并通过持续监控和调优来保持系统的高性能状态。</p><h2 id="">上手实际来分析一下</h2><h3 id="nginx-">Nginx 网关</h3><h4 id="">主要性能瓶颈</h4><ol start="1"><li><strong>连接处理能力瓶颈</strong>
<ul><li>默认工作进程配置可能不匹配服务器 CPU 核心数</li><li>连接池大小限制导致高并发下连接拒绝</li><li>文件描述符限制引起&quot;too many open files&quot;错误</li></ul></li><li><strong>配置复杂度瓶颈</strong>
<ul><li>静态配置文件需要手动修改和重新加载</li><li>大规模路由规则导致配置维护困难</li><li>配置变更需要重新加载，可能导致请求中断</li></ul></li><li><strong>SSL 处理瓶颈</strong>
<ul><li>SSL 握手开销大，高并发下 CPU 使用率飙升</li><li>密钥交换算法效率低下</li><li>会话缓存配置不当导致重复握手</li></ul></li></ol><h4 id="">优化方法</h4><ol start="1"><li><strong>工作进程和连接优化</strong>
<ul><li>将<code>worker_processes</code>设置为与 CPU 核心数匹配</li><li>增加<code>worker_connections</code>值（通常可设为 4096 或更高）</li><li>使用<code>worker_cpu_affinity</code>绑定工作进程到特定 CPU 核心</li><li>调整系统文件描述符限制（ulimit -n）</li></ul></li><li><strong>事件处理优化</strong>
<ul><li>启用<code>multi_accept</code>和<code>accept_mutex</code></li><li>使用<code>epoll</code>事件处理模型（在 Linux 系统上）</li><li>调整<code>worker_aio_requests</code>提高异步 IO 性能</li></ul></li><li><strong>HTTP 优化</strong>
<ul><li>配置<code>keepalive_timeout</code>和<code>keepalive_requests</code>参数</li><li>开启<code>sendfile</code>、<code>tcp_nopush</code>和<code>tcp_nodelay</code>选项</li><li>实施<code>gzip</code>压缩减少传输数据量</li><li>设置<code>client_body_buffer_size</code>和<code>client_max_body_size</code>限制请求大小</li></ul></li><li><strong>SSL 性能优化</strong>
<ul><li>启用<code>ssl_session_cache shared</code>提高会话重用率</li><li>配置 OCSP stapling 减少握手延迟</li><li>使用 ECC 证书减少计算开销</li><li>优先选择高性能加密套件（如 AES-GCM）</li></ul></li></ol><h3 id="kong-">Kong 网关</h3><p>老牌好用网关。</p><h4 id="">主要性能瓶颈</h4><ol start="1"><li><strong>数据库依赖瓶颈</strong>
<ul><li>PostgreSQL/Cassandra 数据库查询成为性能瓶颈</li><li>配置变更时数据库压力增大</li><li>分布式部署下数据库一致性挑战</li></ul></li><li><strong>Lua 脚本处理瓶颈</strong>
<ul><li>插件执行链过长导致请求延迟增加<a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">1</a></li><li>Lua VM 内存使用不当导致性能下降</li><li>JIT 编译限制影响动态脚本性能</li></ul></li><li><strong>JWT 验证瓶颈</strong>
<ul><li>JWT 验证处理开销大，在高百分位延迟明显<a href="https://www.f5.com/company/blog/nginx/benchmarking-api-management-solutions-nginx-kong-amazon-real-time-apis">2</a></li><li>在 99.99 百分位延迟方面，Kong 的延迟可达到 NGINX 的 3 倍<a href="https://www.f5.com/company/blog/nginx/benchmarking-api-management-solutions-nginx-kong-amazon-real-time-apis">2</a></li></ul></li></ol><h4 id="">优化方法</h4><ol start="1"><li><strong>数据库优化</strong>
<ul><li>使用 DB-less 模式减少数据库依赖</li><li>增加数据库连接池大小（kong.conf 中的 pg_pool 参数）</li><li>实施数据库读写分离（主从架构）</li><li>考虑使用声明式配置而非数据库存储</li></ul></li><li><strong>插件链优化</strong>
<ul><li>只启用必要的插件，减少处理链长度</li><li>调整插件执行顺序（高频使用的轻量插件前置）</li><li>为插件配置独立缓存（如 rate-limiting 插件的 Redis 缓存）</li><li>监控并优化长耗时插件</li></ul></li><li><strong>缓存优化</strong>
<ul><li>配置<code>lua_shared_dict</code>缓存大小</li><li>调整插件级别缓存 TTL</li><li>使用外部 Redis 缓存提高命中率</li><li>配置实体缓存减少数据库查询</li></ul></li><li><strong>连接池优化</strong>
<ul><li>调整 upstream_keepalive 参数（通常设置为 100-200）</li><li>增加 nginx<em>upstream</em>keepalive_timeout 值</li><li>设置合理的 nginx<em>upstream</em>keepalive_requests 值</li><li>增加 nginx<em>http</em>client<em>body</em>buffer_size 处理大请求体</li></ul></li></ol><h3 id="apisix-">APISIX 网关</h3><h4 id="">主要性能瓶颈</h4><ol start="1"><li><strong>etcd 依赖瓶颈</strong>
<ul><li>etcd 集群稳定性影响网关配置传播</li><li>配置变更频繁导致 etcd 压力增大</li><li>etcd 读写延迟影响动态路由更新</li></ul></li><li><strong>路由匹配瓶颈</strong>
<ul><li>大量精细化路由导致匹配延迟增加</li><li>复杂正则表达式路由降低匹配效率</li><li>路由缓存更新不及时导致路由错误</li></ul></li></ol><h4 id="">优化方法</h4><ol start="1"><li><strong>etcd 优化</strong>
<ul><li>构建高可用 etcd 集群</li><li>优化 etcd 配置（如设置合理的心跳间隔）</li><li>实施 etcd 分片减轻单节点压力</li><li>增加 config_center.timeout 参数值（默认 30 秒）</li></ul></li><li><strong>路由优化</strong>
<ul><li>使用前缀匹配代替完全正则表达式</li><li>增加路由缓存 TTL</li><li>减少路由规则复杂度，拆分过于复杂的规则</li><li>使用域名或主机名前置过滤</li></ul></li></ol><h3 id="envoy-">Envoy 网关</h3><h4 id="">主要性能瓶颈</h4><ol start="1"><li><strong>xDS 配置更新瓶颈</strong>
<ul><li>动态配置更新导致资源再分配开销</li><li>Control Plane 通信延迟影响配置下发</li><li>大量监听器和集群配置导致内存占用高</li></ul></li><li><strong>过滤器链处理瓶颈</strong>
<ul><li>HTTP 过滤器链过长导致处理延迟增加</li><li>复杂过滤逻辑导致 CPU 使用率高</li><li>Lua 过滤器执行效率低于原生过滤器</li></ul></li></ol><h4 id="">优化方法</h4><ol start="1"><li><strong>xDS 配置优化</strong>
<ul><li>实施增量 xDS 减少配置更新开销</li><li>优化 Control Plane 通信（如使用 gRPC 流而非轮询）</li><li>设置合理的配置缓存 TTL</li><li>使用聚合发现服务(ADS)确保配置一致性</li></ul></li><li><strong>过滤器优化</strong>
<ul><li>减少过滤器链长度，仅保留必要过滤器</li><li>优先使用原生 C++过滤器而非 Lua 或 WASM</li><li>调整过滤器执行顺序（高频执行的前置）</li><li>为关键过滤器启用统计监控</li></ul></li></ol><h3 id="">不同网关性能对比与选择建议</h3><h4 id="">性能对比</h4><ul><li>在标准 API 调用测试中，NGINX API 管理模块的性能可达到 Kong 的 2 倍以上<a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">数据支撑</a></li><li>在延迟方面，NGINX 添加的延迟比 Kong 低 20-30%<a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">数据支撑</a></li><li>在 CPU 效率方面，NGINX 比 Kong 高效 40%左右<a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">数据支撑</a></li><li>在 JWT 验证场景下，NGINX 可处理的 API 调用数是 Kong 的 2 倍以上<a href="https://www.f5.com/company/blog/nginx/benchmarking-api-management-solutions-nginx-kong-amazon-real-time-apis">数据支撑</a></li></ul><h4 id="">选择建议</h4><ol start="1"><li><strong>NGINX 适合场景</strong>：
<ul><li>静态路由配置的稳定 API 网关需求</li><li>以性能和低延迟为首要考量的场景</li><li>资源受限环境下的轻量级网关需求</li><li>主要提供反向代理和负载均衡功能</li></ul></li><li><strong>Kong 适合场景</strong>：
<ul><li>需要丰富 API 管理功能（身份验证、限流、转换等）</li><li>追求开发便利性的团队（RESTful API 配置）</li><li>具有动态路由需求的微服务架构</li><li>可以承受一定性能损失换取功能丰富性</li></ul></li><li><strong>APISIX 适合场景</strong>：
<ul><li>追求动态路由与高性能平衡的团队</li><li>有服务发现集成需求的云原生架构</li><li>需要细粒度流量控制的场景</li></ul></li><li><strong>Envoy 适合场景</strong>：
<ul><li>Kubernetes/Istio 服务网格基础架构</li><li>需要高级可观测性的现代云架构</li><li>追求可编程性和扩展性的 DevOps 团队</li></ul></li></ol><p>通过理解不同网关的性能瓶颈特点和优化方法，您可以根据自身业务特点和技术栈选择最适合的网关类型，并实施有针对性的性能优化，以获得最佳的网关性能与功能平衡。</p><h2 id="">总结</h2><p>网关性能优化是一个系统性的工作，需要从多个层面进行分析和优化。通过以上提供的分析方法、优化技术和代码示例，您可以针对不同的性能瓶颈进行有针对性的优化。关键在于:</p><ol start="1"><li>建立完善的性能监控系统，及时发现性能瓶颈<a href="https://learn.microsoft.com/en-us/data-integration/gateway/service-gateway-performance">1</a></li><li>采用多级缓存策略减少重复计算和网络请求</li><li>优化连接池管理，提高连接复用效率</li><li>实现高效的负载均衡算法，智能分发请求</li><li>使用定期基准测试，持续评估和优化性能</li></ol><p>对于云原生环境中的网关，还可以考虑利用Kubernetes的自动扩缩容能力，动态调整网关实例数量以应对流量变化。</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Gateway</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[内核是如何接受网络包的]]></title>
            <link>https://blog.cheverjohn.me/zh/network-stack</link>
            <guid>https://blog.cheverjohn.me/zh/network-stack</guid>
            <pubDate>Tue, 27 Feb 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[整体一览图 感谢张彦飞大佬的图。 在你基本上了解了什么是网卡驱动、硬中断、软中断和 ksoftirqd 线程之后，可以给出一个如上图所示的内核收包的路径示意图。 大致过程如下： 1. 当网卡收到数据之后，以 DMA 的方式把网卡收到的帧写到内存里，再向 CPU 发起一个中断，以通知 CPU 有数据到达。 2. 当 CPU 收到中断请求之后，会去调用网络设备驱动注册的中断处理函数。 3. 网卡的中断处理函数并不做过多工作，发出软中断请求，然后尽快释放 CPU 资源。 4. ksoftirqd 内核线程检测到有软中断请求到达，调用 poll 开始轮询收包，收到后交由各级协议栈处理。对于 TCP 包...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="">整体一览图</h2><p><img alt="image-20250310194742752" src="theWholeLand.png"/></p><blockquote><p>感谢张彦飞大佬的图。</p></blockquote>
<p>在你基本上了解了什么是网卡驱动、硬中断、软中断和 ksoftirqd 线程之后，可以给出一个如上图所示的内核收包的路径示意图。</p><p>大致过程如下：</p><ol start="1"><li>当网卡收到数据之后，以 DMA 的方式把网卡收到的帧写到内存里，再向 CPU 发起一个中断，以通知 CPU 有数据到达。</li><li>当 CPU 收到中断请求之后，会去调用网络设备驱动注册的中断处理函数。</li><li>网卡的中断处理函数并不做过多工作，发出软中断请求，然后尽快释放 CPU 资源。</li><li>ksoftirqd 内核线程检测到有软中断请求到达，调用 poll 开始轮询收包，收到后交由各级协议栈处理。对于 TCP 包来说，会被放到用户 socket 的接收队列中。</li></ol><h2 id="">做一切之前的基础准备工作</h2><p>Linux 驱动、内核协议栈等模块在能够接收网卡数据包之前，要做很多的准备工作才行。如下：</p><ol start="1"><li>提前创建好 ksoftirqd 内核线程；</li><li>要注册好各个协议对应的处理函数；</li><li>网卡设备子系统要提前初始化好；</li><li>网卡要启动好。</li></ol><h3 id="">初始化工作</h3><h4 id="-ksoftirqd-">创建 ksoftirqd 内核线程</h4><p>Linux 的软中断都是在专门的内核线程（ksoftirqd）中进行的，因此我们非常有必要看一下这些线程是怎么初始化的。</p><p>首先，这个线程的数量不是 1 个，而是 N 个，其中 N 等于你的机器的核数。</p><p>系统初始化的时候在 <code>kernel/smpboot.c</code> 中调用了 <code>smpboot_register_percpu_thread</code> 这个函数，该函数进一步执行到 <code>spawn_ksoftirqd</code>（位于 <code>kernel/softirq.c</code>）来创建出 softirqd 线程，执行过程如下图所示：</p><p>相关代码如下：</p><pre><code class="lang-c">static struct smp_hotplug_thread softirq_threads = {
    .store            = &amp;ksoftirqd,
    .thread_should_run    = ksoftirqd_should_run,
    .thread_fn        = run_ksoftirqd,
    .thread_comm        = &quot;ksoftirqd/%u&quot;,
};

static __init int spawn_ksoftirqd(void)
{
    cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, &quot;softirq:dead&quot;, NULL,
                  takeover_tasklets);
    BUG_ON(smpboot_register_percpu_thread(&amp;softirq_threads));

    return 0;
}
early_initcall(spawn_ksoftirqd);
</code></pre>
<p>当 ksoftirqd 被创建出来以后，它就会进入自己的线程循环函数 ksoftirqd<em>should</em>run 和 run_ksoftirqd 了。接下来判断有没有软中断需要处理。</p><p>软中断不仅有网络软中断，还有其他类型。Linux 内核在 interrupt.h 中定义了所有的软中断类型，如下所示：</p><pre><code class="lang-c">// file: include/linux/interrupt.h
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};
</code></pre>
<h4 id="">网络子系统初始化</h4><p>在网络子系统的初始化过程中，会为每个 CPU 初始化 softnet<em>data，也会为 RX</em>SOFTIRQ 和 TX_SOFTIRQ 注册处理函数，流程如图 2.4 所示。</p><p>Linux 内核通过调用 subsys_initcall 来初始化各个子系统。</p><p><strong>重点！！！这里说的网络子系统的初始化，会执行 net<em>dev</em>init 函数。</strong></p><p>就是这里的 subsys<em>initcall(net</em>dev<em>init) 中的 net</em>dev_init 函数。代码如下：</p><pre><code class="lang-c">static int __init net_dev_init(void)
{
    ......

    /*
     *    Initialise the packet receive queues.
     */

  /*
   * 为每个 CPU 都申请一个 softnet_data 数据结构，这个数据结构里的 poll_list 用于等待驱动程序将其 poll 函数注册进来，稍后网卡驱动程序初始化的时候就可以看到这一过程了。
   */
        for_each_possible_cpu(i) {
        struct softnet_data *sd = &amp;per_cpu(softnet_data, i);

        memset(sd, 0, sizeof(*sd));
        skb_queue_head_init(&amp;sd-&gt;input_pkt_queue);
        skb_queue_head_init(&amp;sd-&gt;process_queue);
        sd-&gt;completion_queue = NULL;
        INIT_LIST_HEAD(&amp;sd-&gt;poll_list);
    ......
  }
  ......
    /*
     * open_softirq 为每一种软中断都注册了一个处理函数。
     * NET_TX_SOFTIRQ 的处理函数为 net_tx_action；
     * NET_RX_SOFTIRQ 的处理函数为 net_rx_action；
     */
  open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
}

subsys_initcall(net_dev_init);

</code></pre>
<p>继续跟踪 open<em>softirq 后发现这个注册的方式是记录在 softirq</em>vec 变量里的。后面 softirqd 线程收到软中断的时候，也会使用这个变量来找到每一种软中断对应的处理函数。</p><pre><code class="lang-c">void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}
</code></pre>
<h4 id="">协议栈注册</h4><h4 id="">网卡驱动初始化</h4><p>每个驱动程序都会使用 <code>module_init</code> 向内核注册一个初始化函数，当驱动程序加载的时候，内核会调用这个函数。</p><p>调用完成后，linux 内核就会知道这个驱动的相关信息，比如 igb 网卡驱动的 igb<em>driver</em>name 和 igb_probe 函数地址 等。</p><p>当网卡设备被识别之后，内核会调用其驱动的 probe 方法，（继续拿 igb 网卡驱动举例子），igb<em>driver 的 probe 方法是 igb</em>probe。</p><p>igb_probe 方法的作用就是，尽快让设备处于 ready 状态。</p><p>此外，还有一步比较关键，注册了 NAPI 机制必需的 poll 函数，这个对于 igb 网卡驱动来说，就是 igb_poll。</p><h3 id="">初始化完成之后</h3><h4 id="">启动网卡</h4><p>上面所有的初始化都完成以后，就可以启动网卡了。一般启动网卡的顺序都差不多，如下图所示：</p><p>igb_open 代码如下：</p><pre><code class="lang-c">static int __igb_open(struct net_device *netdev, bool resuming)
{
  // 分配传输描述符数组
  err = igb_setup_all_tx_resources(adapter);
  // 分配接收描述符数组
    err = igb_setup_all_rx_resources(adapter);

  // 注册中断处理函数
  err = igb_request_irq(adapter);
    if (err)
        goto err_req_irq;

  // 启用 NAPI
      for (i = 0; i &lt; adapter-&gt;num_q_vectors; i++)
        napi_enable(&amp;(adapter-&gt;q_vector[i]-&gt;napi));
    ......
}
</code></pre>
<p>igb<em>open 函数又调用了 igb</em>setup<em>all</em>tx<em>resources 和 igb</em>setup<em>all</em>rx<em>resources。在 igb</em>setup<em>all</em>rx_resources 这一步操作中，分配了 RingBuffer，并建立了内存和 Rx 队列的映射关系。</p><pre><code class="lang-c">static int igb_setup_all_rx_resources(struct igb_adapter *adapter)
{
    ......

    for (i = 0; i &lt; adapter-&gt;num_rx_queues; i++) {
        err = igb_setup_rx_resources(adapter-&gt;rx_ring[i]);
        ...
    }

    return err;
}
</code></pre>
<p>使用 for 循环，然后搭配 igb<em>setup</em>rx<em>resources 函数，创建了若干个队列。igb</em>setup<em>rx</em>resources 函数如下：</p><pre><code class="lang-c">int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
    struct device *dev = rx_ring-&gt;dev;
    int size;

    // 1. 申请 igb_rx_buffer 数组内存
    size = sizeof(struct igb_rx_buffer) * rx_ring-&gt;count;

    rx_ring-&gt;rx_buffer_info = vzalloc(size);
    if (!rx_ring-&gt;rx_buffer_info)
        goto err;

    /* Round up to nearest 4K */
    // 2. 申请 e1000_adv_rx_desc DMA 数组内存
    rx_ring-&gt;size = rx_ring-&gt;count * sizeof(union e1000_adv_rx_desc);
    rx_ring-&gt;size = ALIGN(rx_ring-&gt;size, 4096);

    rx_ring-&gt;desc = dma_alloc_coherent(dev, rx_ring-&gt;size,
                       &amp;rx_ring-&gt;dma, GFP_KERNEL);
    if (!rx_ring-&gt;desc)
        goto err;

    // 3. 初始化队列成员
    rx_ring-&gt;next_to_alloc = 0;
    rx_ring-&gt;next_to_clean = 0;
    rx_ring-&gt;next_to_use = 0;

    return 0;

err:
    vfree(rx_ring-&gt;rx_buffer_info);
    rx_ring-&gt;rx_buffer_info = NULL;
    dev_err(dev, &quot;Unable to allocate memory for the Rx descriptor ring\n&quot;);
    return -ENOMEM;
}
</code></pre>
<p>上述源码可见，实际上一个 RingBuffer 的内部不是仅有一个环形队列数组，而是有两个：</p><ol start="1"><li>igb<em>rx</em>buffer 数组：这个数组是内核使用的，通过 vzalloc 申请的；</li><li>e1000<em>adv</em>rx<em>desc 数组：这个数组是网卡硬件使用的，通过 dma</em>alloc_coherent 分配。</li></ol><p>然后其实还有最后一步中断函数的注册，注册过程看 igb<em>request</em>irq。</p><p>OK，上面就是所有的准备工作了～接下里啊就是接受数据包了。</p><h2 id="">开始接受数据包</h2><p>这一部分包括了，硬中断处理</p><h3 id="">硬中断处理</h3><p>首先，当数据帧从网线抵达网卡的时候，第一站是网卡的接收队列。网卡在分配给自己的 RingBuffer 中寻找可用的内存位置，找到后 DMA 引擎会将数据 DMA 到网卡之前关联的内存里，到这个时候 CPU 都是无感的。</p><p>当 DMA 操作完成后，网卡会向 CPU 发起一个硬中断，通知 CPU 有数据到达。硬中断的处理过程如下图：</p><p>在之前的“启动网卡”这一部分中，讲到网卡的硬中断注册的处理函数是 igb<em>msix</em>ring。</p><pre><code class="lang-c">// file: drivers/net/ethernet/intel/igb/igb_main.c
static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    /* Write the ITR value calculated from the previous interrupt. */
    igb_write_itr(q_vector);

    napi_schedule(&amp;q_vector-&gt;napi);

    return IRQ_HANDLED;
}
</code></pre>
<p>其中的 igb<em>write</em>itr 只记录硬件中断频率。顺着 napi<em>schedule 调用一路跟踪下去，你就会发现，Linux 在硬中断里只完成简单必要的工作，剩下的大部分的处理都是转交给软中断的。通过以上代码可以看到，硬中断处理过程真的非常短，只是记录了一个寄存器，修改了一下 CPU 的 poll</em>list，然后发出一个软中断，就这样，硬中断的工作就算是完成了。</p><h3 id="ksoftirqd-">ksoftirqd 内核线程处理软中断</h3><p>网络包的接收处理过程主要都在 ksoftirqd 内核线程中完成，软中断都是在这里处理的，流程如下所示：</p><h3 id="">网络协议栈处理</h3><p>netif<em>receive</em>skb 函数会根据包的协议进行处理，假如是 UDP 包，将包依次送到 ip<em>rcv、udp</em>rcv 等协议处理函数中进行处理。如下图：</p><h3 id="ip-">IP 层处理</h3><p>Linux 在 IP 层做的操作，在代码 <code>net/ipv4/ip_input.c</code> 这个代码文件中。</p><h2 id="">总结</h2><p>网络模块是 Linux 内核中最复杂的模块了。整个过程，涉及到了许多内核组件之间的交互，如网卡驱动、协议栈、内核 ksoftirqd 线程等。看起来很复杂，但实际整体大概还是很清晰的。简单总结如下。</p><p>当用户执行完 recvfrom 调用之后，用户进程就通过系统调用进行到内核态工作了。如果接收队列没有数据，进程就进入睡眠状态被操作系统挂起。这块相对简单，接下来就是 LInux 各个内核组件之间的工作了。</p><p>首先在开始收包之前，Linux 要做许多的准备工作：</p><ul><li>创建 ksoftirqd 内核线程，为它设置好它自己的线程函数，后面指望着它来处理软中断；</li><li>协议栈注册，Linux 要实现许多协议，比如 ARP、ICMP、IP、UDP 和 TCP，每一个协议都会将自己的处理函数注册一下，这样会方便包来了之后迅速找到对应的处理函数；</li><li>网卡驱动初始化，每个驱动都有一个初始化函数，内核会让驱动也初始化一下。在这个初始化过程中，准备好自己的 DMA，并且把 NAPI 的 poll 函数地址告诉内核；</li><li>启动网卡，分配 RX、TX 队列，注册中断对应的处理函数。</li></ul><p>准备工作完成之后，接下来就是数据到来。第一个迎接它的是网卡：</p><ul><li>网卡将数据帧 DMA 到内存的 RingBUffer 中，然后向 CPU 发起中断通知；</li><li>CPU 响应中断请求，调用网卡启动时注册的中断处理函数；</li><li>中断处理函数只是发起了软中断请求，其他的什么也没有干；</li><li>内核线程 ksoftirqd 发现有软中断请求到来，先关闭硬中断；</li><li>ksoftirqd 线程开始调用驱动的 poll 函数收包；</li><li>poll 函数将收到的包送到协议栈注册的 ip_rcv 函数中；</li><li>ip<em>rcv 函数将包送到 udp</em>rcv 函数中（对于 TCP 包是送到 tcp<em>rcv</em>v4）。</li></ul><h2 id="">一些总结</h2><h3 id="ringbuffer--ringbuffer-">问题一：RingBuffer 究竟是什么，为什么 RingBuffer 会丢包？</h3><p>RingBuffer 是内存中特殊的一块区域，是一种环形队列数组，事实上这个数据结构包括了 igb<em>rx</em>buffer 环形队列数组、e1000<em>adv</em>rx_desc 环形队列数组及众多的 skb。</p><p>如果 RingBuffer 代表的是指针数组，那么是预先分配好的，如果是 skb，那么是随着收包过程而动态申请的。</p><h3 id="">问题二：软中断和硬中断分别是什么？</h3><p>Linux 网络栈中数据包接收的关键流程：</p><ol start="1"><li><strong>硬件阶段</strong>：网卡将接收到的数据包放入 RingBuffer</li><li><strong>硬中断触发</strong>：网卡产生硬中断通知 CPU</li><li><strong>硬中断处理</strong>：添加网卡设备到 <code>softnet_data</code> 结构的 <code>poll_list</code> 双向链表</li><li><strong>软中断触发</strong>：触发 <code>NET_RX_SOFTIRQ</code> 软中断</li><li><strong>软中断处理</strong>：遍历 <code>poll_list</code> 列表，执行网卡驱动的 <code>poll</code> 函数收取网络包</li><li><strong>协议栈处理</strong>：将数据包转发到 <code>ip_rcv</code>、<code>udp_rcv</code>、<code>tcp_rcv_v4</code> 等协议处理函数</li></ol><p>这描述的是 Linux NAPI (New API) 机制，一种高效处理网络数据包的方法。</p><h4 id="ringbuffer-">RingBuffer 在网络栈中的实际应用</h4><h5 id="-rxtx-">网卡 RX/TX 环形缓冲区</h5><pre><code class="lang-c">/* 简化的网卡 RX Ring 结构 */
struct e1000_rx_desc {
    __le64 buffer_addr;    /* 数据缓冲区地址 */
    __le16 length;         /* 数据包长度 */
    __le16 checksum;       /* 校验和 */
    __u8  status;          /* 描述符状态 */
    __u8  errors;          /* 错误码 */
    __le16 special;
};
</code></pre>
<p>实际上，一个 Intel 网卡的 RX Ring 可能包含 256 个这样的描述符，形成一个环形结构。</p><h5 id="">硬中断与软中断协作的实际例子</h5><p>在 Intel 82599 网卡的驱动中：</p><pre><code class="lang-c">/* 硬中断处理程序 */
static irqreturn_t ixgbe_msix_lsc(int irq, void *data)
{
    struct net_device *netdev = data;

    /* 禁用网卡中断 */
    ixgbe_disable_interrupt();

    /* 将设备添加到 poll_list */
    napi_schedule(&amp;adapter-&gt;q_vector[vector]-&gt;napi);

    return IRQ_HANDLED;
}

/* NAPI poll 函数 */
static int ixgbe_poll(struct napi_struct *napi, int budget)
{
    struct ixgbe_q_vector *q_vector = container_of(napi, struct ixgbe_q_vector, napi);
    struct ixgbe_adapter *adapter = q_vector-&gt;adapter;
    int work_done = 0;

    /* 从 RingBuffer 中批量收包，最多处理 budget 个 */
    work_done = ixgbe_clean_rx_irq(q_vector, budget);

    /* 如果工作未完成，保持在 poll_list 中 */
    if (work_done &lt; budget) {
        napi_complete(napi);
        ixgbe_enable_interrupt();
    }

    return work_done;
}
</code></pre></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Network</category>
            <category>Tech</category>
        </item>
        <item>
            <title><![CDATA[git 推送大文件 repo]]></title>
            <link>https://blog.cheverjohn.me/zh/shell-to-push-large-git-repo</link>
            <guid>https://blog.cheverjohn.me/zh/shell-to-push-large-git-repo</guid>
            <pubDate>Wed, 27 Dec 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[看一下我的效果 我的需求：我想要阅读 Linux 源码，使用 git 管理我阅读的 Linux 源码，使用我自建的 gitlab 保存我阅读的源码。然后发现简单的 git push 一个 2GB 的 linux 源码文件会出现问题。 所以我就写了个脚本以期来解决这个问题。 脚本内容如下： 运行方法很简单。 然后在当前文件夹运行脚本即可。 最终效果]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="">看一下我的效果</h2><p><img alt="image-20250310233801870" src="beforeShow.png"/></p><p>我的需求：我想要阅读 Linux 源码，使用 git 管理我阅读的 Linux 源码，使用我自建的 gitlab 保存我阅读的源码。然后发现简单的 git push 一个 2GB 的 linux 源码文件会出现问题。</p><p>所以我就写了个脚本以期来解决这个问题。</p><p>脚本内容如下：</p><pre><code class="lang-shell">#!/bin/bash

# 配置参数（请根据需要修改）
REMOTE_NAME=&quot;origin&quot;              # 远程仓库名称
BRANCH_NAME=&quot;main&quot;                # 目标分支名
REMOTE_URL=&quot;git@gitlab.cheverjohn.me:CheverJohn/linux.git&quot;  # 远程仓库地址
BATCH_SIZE=500                    # 每批文件数量
COMMIT_MESSAGE=&quot;批量上传文件&quot;      # 提交信息

# 颜色配置
RED=&#x27;\033[0;31m&#x27;
GREEN=&#x27;\033[0;32m&#x27;
YELLOW=&#x27;\033[0;33m&#x27;
BLUE=&#x27;\033[0;34m&#x27;
NC=&#x27;\033[0m&#x27; # 无颜色

# 检查是否在git仓库中
if ! git rev-parse --is-inside-work-tree &gt; /dev/null 2&gt;&amp;1; then
    echo -e &quot;${YELLOW}当前目录不是git仓库，正在初始化...${NC}&quot;
    git init
    echo -e &quot;${GREEN}Git仓库已初始化${NC}&quot;
else
    echo -e &quot;${GREEN}Git仓库已存在${NC}&quot;
fi

# 检查远程仓库是否已配置
if ! git remote | grep -q &quot;$REMOTE_NAME&quot;; then
    echo -e &quot;${YELLOW}添加远程仓库 $REMOTE_NAME: $REMOTE_URL${NC}&quot;
    git remote add $REMOTE_NAME $REMOTE_URL
    echo -e &quot;${GREEN}远程仓库已添加${NC}&quot;
else
    CURRENT_URL=$(git remote get-url $REMOTE_NAME 2&gt;/dev/null || echo &quot;&quot;)
    if [ &quot;$CURRENT_URL&quot; != &quot;$REMOTE_URL&quot; ]; then
        echo -e &quot;${YELLOW}更新远程仓库URL: $REMOTE_URL${NC}&quot;
        git remote set-url $REMOTE_NAME $REMOTE_URL
        echo -e &quot;${GREEN}远程仓库URL已更新${NC}&quot;
    else
        echo -e &quot;${GREEN}远程仓库已正确配置${NC}&quot;
    fi
fi

# 确保主分支存在
if ! git show-ref --quiet refs/heads/$BRANCH_NAME; then
    echo -e &quot;${YELLOW}创建分支 $BRANCH_NAME...${NC}&quot;
    git checkout -b $BRANCH_NAME
    echo -e &quot;${GREEN}分支 $BRANCH_NAME 已创建${NC}&quot;
else
    echo -e &quot;${GREEN}分支 $BRANCH_NAME 已存在${NC}&quot;
    git checkout $BRANCH_NAME
fi

# 获取所有未跟踪和已修改的文件
echo -e &quot;${BLUE}获取待上传的文件列表...${NC}&quot;
FILES=($(git ls-files --others --exclude-standard) $(git diff --name-only))
TOTAL_FILES=${#FILES[@]}

if [ $TOTAL_FILES -eq 0 ]; then
    echo -e &quot;${YELLOW}没有找到需要添加的文件，尝试添加所有文件...${NC}&quot;
    git add -A
    FILES=($(git diff --name-only --cached))
    TOTAL_FILES=${#FILES[@]}

    if [ $TOTAL_FILES -eq 0 ]; then
        echo -e &quot;${RED}错误: 没有找到要推送的文件${NC}&quot;
        exit 1
    fi
else
    # 添加所有文件到暂存区
    echo -e &quot;${BLUE}添加所有文件到暂存区...${NC}&quot;
    git add -A
fi

echo -e &quot;${GREEN}找到 $TOTAL_FILES 个文件需要上传${NC}&quot;

# 计算批次数
BATCH_COUNT=$(( ($TOTAL_FILES + $BATCH_SIZE - 1) / $BATCH_SIZE ))
echo -e &quot;${GREEN}将分为 $BATCH_COUNT 批次上传${NC}&quot;

# 首先提交所有文件
echo -e &quot;${BLUE}提交所有文件...${NC}&quot;
git commit -m &quot;$COMMIT_MESSAGE&quot;

# 分批推送
echo -e &quot;${BLUE}开始分批推送...${NC}&quot;
git push -u $REMOTE_NAME $BRANCH_NAME

if [ $? -eq 0 ]; then
    echo -e &quot;${GREEN}所有文件已成功推送到远程仓库${NC}&quot;
else
    echo -e &quot;${YELLOW}常规推送失败，尝试使用批量方式推送...${NC}&quot;
    # 使用git batch push方式（参考了搜索结果中的示例）
    # 基于 git_batch_push.sh 的思路，但简化了实现

    # 使用git rev-list获取所有提交
    COMMITS=($(git rev-list --reverse HEAD))
    TOTAL_COMMITS=${#COMMITS[@]}

    if [ $TOTAL_COMMITS -eq 0 ]; then
        echo -e &quot;${RED}错误: 没有找到提交记录${NC}&quot;
        exit 1
    fi

    echo -e &quot;${GREEN}找到 $TOTAL_COMMITS 个提交，将分批推送${NC}&quot;

    # 计算批次数（每批500个提交）
    COMMIT_BATCH_SIZE=500
    COMMIT_BATCH_COUNT=$(( ($TOTAL_COMMITS + $COMMIT_BATCH_SIZE - 1) / $COMMIT_BATCH_SIZE ))

    echo -e &quot;${GREEN}将分为 $COMMIT_BATCH_COUNT 批次推送提交${NC}&quot;

    # 分批推送提交
    for ((i=0; i&lt;$COMMIT_BATCH_COUNT; i++)); do
        START=$(($i * $COMMIT_BATCH_SIZE))
        END=$((($i + 1) * $COMMIT_BATCH_SIZE))

        if [ $END -gt $TOTAL_COMMITS ]; then
            END=$TOTAL_COMMITS
        fi

        BATCH_END_INDEX=$((END - 1))
        TARGET_COMMIT=${COMMITS[$BATCH_END_INDEX]}

        echo -e &quot;${BLUE}推送批次 $((i+1))/${COMMIT_BATCH_COUNT} (提交 $((START+1))-$END)...${NC}&quot;

        if git push $REMOTE_NAME $TARGET_COMMIT:refs/heads/$BRANCH_NAME; then
            echo -e &quot;${GREEN}批次 $((i+1)) 成功推送${NC}&quot;
        else
            echo -e &quot;${RED}批次 $((i+1)) 推送失败${NC}&quot;
            echo -e &quot;${YELLOW}尝试另一种推送方法...${NC}&quot;

            # 如果上面的方法失败，尝试另一种批量推送方法
            if [ $i -eq 0 ]; then
                # 第一批次，创建新分支
                git push $REMOTE_NAME $BRANCH_NAME
            else
                # 获取上一批次的末尾提交
                PREV_END=$(($START - 1))
                PREV_COMMIT=${COMMITS[$PREV_END]}
                CURR_COMMIT=${COMMITS[$BATCH_END_INDEX]}

                echo -e &quot;${BLUE}使用范围推送 $PREV_COMMIT..$CURR_COMMIT${NC}&quot;
                git push $REMOTE_NAME $PREV_COMMIT:refs/heads/$BRANCH_NAME $CURR_COMMIT:refs/heads/$BRANCH_NAME
            fi
        fi

        echo
    done
fi

echo -e &quot;${GREEN}分批上传过程完成!${NC}&quot;
</code></pre>
<p>运行方法很简单。</p><pre><code class="lang-shell">chmod +x git_batch_push.sh
</code></pre>
<p>然后在当前文件夹运行脚本即可。</p><h2 id="">最终效果</h2><p><img alt="image-20250310234253015" src="afterShow.png"/></p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Shell</category>
            <category>Chore</category>
        </item>
        <item>
            <title><![CDATA[常用密码学：GPG 加密文件（实践向）]]></title>
            <link>https://blog.cheverjohn.me/zh/cryptography-gpg</link>
            <guid>https://blog.cheverjohn.me/zh/cryptography-gpg</guid>
            <pubDate>Tue, 26 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[本篇文章主要是在实践，然后会简单介绍一下原理。 整篇文章，前期会讲一个简单原理，然后会从如何创建 gpg 密钥、如何管理自己的密钥（管理密钥相关命令）、实战：使用自己创建的 gpg 密钥去解密传输的加密文件三个部分开始讲完整个文章。 原理简介 使用 GPG Encrypting and decrypting doc 的原理其实就像文档中的一段话，如下 The procedure for encrypting and decrypting documents is straightforward with this mental model. If you want to encrypt a m...]]></description>
            <content:encoded><![CDATA[<article><div><p>本篇文章主要是在实践，然后会简单介绍一下原理。</p><p>整篇文章，前期会讲一个<strong>简单原理</strong>，然后会从<strong>如何创建 gpg 密钥</strong>、<strong>如何管理自己的密钥（管理密钥相关命令）</strong>、<strong>实战：使用自己创建的 gpg 密钥去解密传输的加密文件</strong>三个部分开始讲完整个文章。</p><h2 id="">原理简介</h2><p>使用 GPG Encrypting and decrypting doc 的原理其实就像文档中的一段话，如下</p><blockquote><p>The procedure for encrypting and decrypting documents is straightforward with this mental model. If you want to encrypt a message to Alice, you encrypt it using Alice&#x27;s public key, and she decrypts it with her private key. If Alice wants to send you a message, she encrypts it using your public key, and you decrypt it with your key.</p></blockquote>
<p>整个过程就是，我拿我的私钥加密我想要发给你的文件。然后你拿我的公钥，解密我想要发给你的，但是被我加密的文件。</p><p>这么一个过程。</p><p>这篇文章主要就是，简单快速过一下 gpg 是如何加密文档内容的。</p><p>本篇文章分为两部分，首先是创建一个可用的 gpg 密钥。</p><h2 id="-gpg-">如何创建 gpg 密钥</h2><pre><code class="lang-shell">[Se] gpg --list-keys
gpg: checking the trustdb
gpg: no ultimately trusted keys found
</code></pre>
<p>首先 list 一下我的机器当前是否有个 gpg key。可以看到是没有的，此处主要是指有没有 gpg public key。同样看看 private key。</p><pre><code class="lang-shell">[Se] gpg --list-secret-keys
[Se]
</code></pre>
<p>啥也没有。</p><h3 id="">一个命令搞定一切</h3><p>开始创建。</p><p>详细步骤从一个 <code>gpg --full-generate-key</code> 开始，命令如下：</p><pre><code class="lang-shell">[Se] gpg --full-generate-key
gpg (GnuPG) 2.4.0; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
         0 = key does not expire
      &lt;n&gt;  = key expires in n days
      &lt;n&gt;w = key expires in n weeks
      &lt;n&gt;m = key expires in n months
      &lt;n&gt;y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: Chenwei Jiang
Email address: cheverjonathan@gmail.com
Comment: Used for learning
You selected this USER-ID:
    &quot;Chenwei Jiang (Used for learning) &lt;cheverjonathan@gmail.com&gt;&quot;

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

gpg: revocation certificate stored as &#x27;/home/cheverjohn/.gnupg/openpgp-revocs.d/D1DED33E8FE9CA4B315A488968CC99424BF9EF81.rev&#x27;
public and secret key created and signed.

pub   rsa4096 2023-09-19 [SC]
      D1DED33E8FE9CA4B315A488968CC99424BF9EF81
uid                      Chenwei Jiang (Used for learning) &lt;cheverjonathan@gmail.com&gt;
sub   rsa4096 2023-09-19 [E]
</code></pre>
<h3 id="">详细分阶段解释一下命令</h3><p>想了想，还是需要简单分全阶段讲一下发生了什么。</p><h4 id="">第一阶段：选择加密签名用算法</h4><p>第一阶段，可以看到需要你选择加密算法。而我选择了 1，这表示加密和签名都适用了 RSA 算法。</p><pre><code class="lang-shell">[Se] gpg --full-generate-key
gpg (GnuPG) 2.4.0; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
Your selection? 1
</code></pre>
<p>哦对了，还有一段版权声明。</p><h4 id="">第二阶段：选择密钥的长度</h4><p>密钥越长越安全，我这边选择了 4096。</p><pre><code class="lang-shell">RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits
</code></pre>
<h4 id="">第三阶段：设定密钥的有效期</h4><p>设定有效期。</p><pre><code class="lang-shell">Please specify how long the key should be valid.
         0 = key does not expire
      &lt;n&gt;  = key expires in n days
      &lt;n&gt;w = key expires in n weeks
      &lt;n&gt;m = key expires in n months
      &lt;n&gt;y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
</code></pre>
<p>这边演示的话，配置了 <code>Key does not expire at all</code>。</p><h4 id="">第四阶段：个人信息</h4><p>这一部分会最终用来生成你的 user id。</p><pre><code class="lang-shell">GnuPG needs to construct a user ID to identify your key.

Real name: Chenwei Jiang
Email address: cheverjonathan@gmail.com
Comment: Used for learning
You selected this USER-ID:
    &quot;Chenwei Jiang (Used for learning) &lt;cheverjonathan@gmail.com&gt;&quot;

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
</code></pre>
<p>这边我设置了我的 <code>Real name</code> 为 “Chenwei Jiang”，我的 <code>Email address</code> 为 “cheverjonathan@gmail.com“。以及一个 <code>comment</code>。这些都是我常用的一些基本信息，此处主要就是用来演示。</p><p>得到的结果 —— 一个 USER-ID 为     &quot;Chenwei Jiang (Used for learning) <a href="mailto:cheverjonathan@gmail.com">cheverjonathan@gmail.com</a>&quot;。</p><h2 id="">如何管理自己的密钥（管理密钥相关命令）</h2><p>这边讲了我该如何在一台宿主机上，管理我的很多密钥呢。</p><h3 id="">列出密钥</h3><p>列出密钥分两种，一种是公钥，一种是私钥。</p><pre><code class="lang-shell">[Se] gpg --list-keys
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
/home/cheverjohn/.gnupg/pubring.kbx
-----------------------------------
pub   rsa4096 2023-09-19 [SC]
      D1DED33E8FE9CA4B315A488968CC99424BF9EF81
uid           [ultimate] Chenwei Jiang (Used for learning) &lt;cheverjonathan@gmail.com&gt;
sub   rsa4096 2023-09-19 [E]
</code></pre>
<p>上面的命令显示了。</p><p>其中仔细的部分。</p><pre><code class="lang-shell">/home/cheverjohn/.gnupg/pubring.kbx
-----------------------------------
pub   rsa4096 2023-09-19 [SC]
      D1DED33E8FE9CA4B315A488968CC99424BF9EF81
uid           [ultimate] Chenwei Jiang (Used for learning) &lt;cheverjonathan@gmail.com&gt;
sub   rsa4096 2023-09-19 [E]
</code></pre>
<p>其中第一行，显示公钥文件名。</p><table><thead><tr><th> 值                                                           </th><th> 解释                                                     </th><th></th></tr></thead><tbody><tr><td> /home/cheverjohn/.gnupg/pubring.kbx                          </td><td> 第一行显示公钥文件名（pubring.kbx）                      </td><td></td></tr><tr><td> pub   rsa4096 2023-09-19 [SC]<br/>      D1DED33E8FE9CA4B315A488968CC99424BF9EF81 </td><td> 第二行显示公钥特性（4096 位，Hash 字符串以及生成的时间。 </td><td></td></tr><tr><td> uid           [ultimate] Chenwei Jiang (Used for learning) <a href="mailto:cheverjonathan@gmail.com">cheverjonathan@gmail.com</a> </td><td> 第三行显示“用户 ID”。                                    </td><td></td></tr><tr><td> sub   rsa4096 2023-09-19 [E]                                 </td><td> 第四行显示私钥特征。                                     </td><td></td></tr></tbody></table><h3 id="">输出密钥</h3><p>公钥文件位于 <code>~/.gnupg/pubring.kbx</code> 以二进制形式存储，使用 armor 参数可以将其转换为 ASCII 码显示。</p><p>使用命令，展示输出为 <code>public-key.txt</code> 和 <code>private-key.txt</code>，命令如下：</p><pre><code class="lang-shell">[gpg] pwd
/home/cheverjohn/Se/gpg
[gpg] ls
[gpg] ls -la
total 0
drwxr-xr-x. 1 cheverjohn cheverjohn  0 Sep 19 23:36 .
drwxr-xr-x. 1 cheverjohn cheverjohn 34 Sep 19 23:36 ..
[gpg] gpg --armor --output public-key.txt --export &#x27;Chenwei Jiang (used for learning) &lt;cheverjonathan@gmail.com&gt;&#x27;
[gpg] ls
public-key.txt
[gpg] cat public-key.txt
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBGUJt+QBEADAzoLPAdK8GfJ/5Ouxh2rOrMsClMmoOznMm2GOBcqSaQsdmP4G
................................................................
oM3YFFujtMxK/cQ/KkbmwAtlMkWx5x8RT/dJ
=NMtR
-----END PGP PUBLIC KEY BLOCK-----
[gpg]
</code></pre>
<p>如上图所示，这是展示 <code>public-key.txt</code> 的步骤，详细命令如下：</p><pre><code class="lang-shell">gpg --armor --output public-key.txt --export &#x27;Chenwei Jiang (used for learning) &lt;cheverjonathan@gmail.com&gt;&#x27;
</code></pre>
<p>下面是展示 <code>private-key.txt</code> 的步骤，详细步骤如下：</p><pre><code class="lang-shell">[gpg] ls
public-key.txt
[gpg] gpg --armor --output private-key.txt --export-secret-keys
[gpg] ls
private-key.txt  public-key.txt
[gpg]
</code></pre>
<p>其中如果之前设置了密码的话，这个操作是需要密码的，需要密码的命令如下：</p><pre><code class="lang-shell">gpg --armor --output private-key.txt --export-secret-keys
</code></pre>
<h3 id="">上传公钥</h3><p>公钥服务器是网络上专门存储用户公钥的服务器。<code>--send-keys</code> 子命令可以实现。</p><p>公钥服务器没有检查机制，任何人都可以用你的名义上传公钥，所以没法保证服务器上的公钥的可靠性。</p><p>一般我们在自己的网站上公布一个公钥指纹，用来让别人核对下载的公钥是否为真。<code>--fingerprint</code> 子命令可以生成公钥指纹。</p><h2 id="">实战/实践出真知</h2><p>这一部分我将按照导出公钥，使用私钥加密，在另外一台设备上使用公钥进行解密。</p><p>此处我将展示如何加密一个内容为 “hello world” 的文件，然后在异地进行解密。</p><p>步骤，将分为加密、解密</p><p>详细步骤如下</p><ol start="1"><li><p>创建文件，命令如下：</p><pre><code class="lang-shell">[gpg] touch demo.txt
echo &quot;hello world&quot; &gt; demo.txt
[gpg] ls
demo.txt  private-key.txt  public-key.txt
[gpg] cat demo.txt
hello world
[gpg]
</code></pre>
<p>其中，创建文件的命令如下，可复制直接使用：</p><pre><code class="lang-shell">touch demo.txt
echo &quot;hello world&quot; &gt; demo.txt
</code></pre>
<p>然后我们就得到了一个 demo.txt 文件，里边的内容就是 hello world。</p></li><li><p>对该文件加密，加密过程如下：</p><pre><code class="lang-shell">[gpg] ls
demo.txt  private-key.txt  public-key.txt
[gpg] gpg --recipient &#x27;cheverjonathan@gmail.com&#x27; --output demo.gpg --encrypt demo.txt
[gpg] ls
demo.gpg  demo.txt  private-key.txt  public-key.txt
[gpg] cat demo.gpg
�
..............
�&amp;5p�.�)I�槹iQ%
[gpg]
</code></pre>
<p>加密命令如下：</p><pre><code class="lang-shell">gpg --recipient &#x27;cheverjonathan@gmail.com&#x27; --output demo.gpg --encrypt demo.txt
</code></pre>
<p>这边可以看到 demo.gpg 文件就是已经加密好之后的文件。</p></li><li><p>对该文件解密，解密过程如下：</p><pre><code class="lang-shell">[gpg] gpg --output demo --decrypt demo.gpg
gpg: encrypted with rsa4096 key, ID 13C117D5FEC4F051, created 2023-09-19
      &quot;Chenwei Jiang (Used for learning) &lt;cheverjonathan@gmail.com&gt;&quot;
[gpg] ls
demo  demo.gpg  demo.txt  private-key.txt  public-key.txt
[gpg] cat demo
hello world
[gpg]
</code></pre>
<p>解密命令如下：</p><pre><code class="lang-shell">gpg --output demo --decrypt demo.gpg
</code></pre>
<p>输入这行命令之后，需要输入之前的密码，然后就能得到文件。</p><p>上面主要是在本机上使用本地的密钥加密文件，并在本地解密的过程。</p><p>接下来开始，开始......</p><h3 id="">在其他电脑上获取到文件并加密，然后由原主机使用私钥进行解密</h3><p>首先我们将 public-key.txt 传到其他主机，这边你可以通过 GitHub 的功能将文件上下传，我这边省事，将上面过程得出的如下文件树：</p><pre><code class="lang-Shell">cheverjohn@Dell-G33579 git:(doc/test-gpg*)% tree ~/workspace/Opensource/github.com/Chever-John/cheverjohn.me/docs/wait-for-publish/gpg
.
├── demo
├── demo.gpg
├── demo.txt
├── private-key.txt
└── public-key.txt
</code></pre>
<p>中的 private-key.txt 删除后，将文件上传到 GitHub 上。</p><p>可以看到我在另外一台主机上下载了 public-key.txt 文件，如下文件树所示：</p><pre><code class="lang-shell">cheverjohn:wait-for-publish/ git:(doc/test-gpg*)$ tree gpg                                                                   [0:54:06]
gpg
├── demo
├── demo.gpg
├── demo.txt
└── public-key.txt

1 directory, 4 files
</code></pre>
<p>然后我接下来要做的就是，使用这个 public-key.txt 去加密一个文件，然后将这个文件传回最初拥有私钥的主机上，并交由它进行解密得到内容。</p><p>首先我们需要将这个 public-key.txt 导入到本地。命令如下：</p><pre><code class="lang-shell">gpg --import public-key.txt 
</code></pre>
<p>命令执行结果如下：</p><pre><code class="lang-shell">cheverjohn:gpg/ git:(doc/test-gpg*)$ gpg --import public-key.txt                                                             [1:20:11]
gpg: key 3BE465D20064251C: public key &quot;Chenwei Jiang (Learning second) &lt;cheverjonathan@gmail.com&gt;&quot; imported
gpg: Total number processed: 1
gpg:               imported: 1
</code></pre>
<p>然后我们需要使用这个 public-key 对文本进行加密。</p><p>首先我们需要创建一个文件 hello-no-encrypt.txt，一系列操作如下：</p><pre><code class="lang-shell">cheverjohn:gpg/ git:(doc/test-gpg*)$ touch hello-no-encrypt.txt                                                              [1:16:07]
cheverjohn:gpg/ git:(doc/test-gpg*)$ nvim hello-no-encrypt.txt                                                               [1:16:15]
cheverjohn:gpg/ git:(doc/test-gpg*)$ cat hello-no-encrypt.txt                                                                [1:16:24]
Hello Sasa!
cheverjohn:gpg/ git:(doc/test-gpg*)$ tree                                                                                    [1:16:28]
.
├── demo
├── demo.gpg
├── demo.txt
├── hello-no-encrypt.txt
└── public-key.txt

1 directory, 5 files
</code></pre>
<p>我们需要对这个文件进行加密，然后传给拥有私钥的宿主机进行解密。</p><p>加密文件整个流程如下：</p><pre><code class="lang-shell">cheverjohn:gpg/ git:(doc/test-gpg*)$ gpg --output hello-encrypted.gpg --encrypt --recipient &#x27;Chenwei Jiang (Learning second) &lt;cheverjonathan@gmail.com&gt;&#x27; hello-no-encrypt.txt
gpg: 617C5542800731C1: There is no assurance this key belongs to the named user

sub  rsa4096/617C5542800731C1 2023-09-21 Chenwei Jiang (Learning second) &lt;cheverjonathan@gmail.com&gt;
 Primary key fingerprint: 5588 D37D AF51 50FD 9186  47E7 3BE4 65D2 0064 251C
      Subkey fingerprint: A038 0DA1 D481 62C7 517C  D6C6 617C 5542 8007 31C1

It is NOT certain that the key belongs to the person named
in the user ID.  If you *really* know what you are doing,
you may answer the next question with yes.

Use this key anyway? (y/N) y
</code></pre>
<p>加密命令如下：</p><pre><code class="lang-shell">gpg --output hello-encrypted.gpg --encrypt --recipient &#x27;Chenwei Jiang (Learning second) &lt;cheverjonathan@gmail.com&gt;&#x27; hello-no-encrypt.txt
</code></pre>
<p>然后我们需要将这个加密之后的文件 hello-decrypted.gpg 上传到拥有私钥的机器上。</p><p>这边还是选择 GitHub 来作为文件传输工具～</p><p>这边可以看到我在宿主机（拥有密钥的机器）上拿到了文件，开始进行解密，看到如下：</p><pre><code class="lang-shell">[gpg] tree                                                                                                         git:(doc/test-gpg)
.
├── demo
├── demo.gpg
├── demo.txt
├── hello-encrypted.gpg
├── hello-no-encrypt.txt
└── public-key.txt

1 directory, 6 files
</code></pre>
<p>接下来需要对 hello-encrypted.gpg 进行解密。解密过程如下：</p><pre><code class="lang-shell">[gpg] gpg --output hello-decrypted.txt --decrypt hello-encrypted.gpg                                               git:(doc/test-gpg)
gpg: encrypted with rsa4096 key, ID 617C5542800731C1, created 2023-09-21
      &quot;Chenwei Jiang (Learning second) &lt;cheverjonathan@gmail.com&gt;&quot;
[gpg] ls                                                                                                        git:(doc/test-gpg*)  ✱
demo  demo.gpg  demo.txt  hello-decrypted.txt  hello-encrypted.gpg  hello-no-encrypt.txt  public-key.txt
[gpg] cat hello-decrypted.txt                                                                                   git:(doc/test-gpg*)  ✱
Hello Sasa!
</code></pre>
<p>解密的命令如下：</p><pre><code class="lang-shell">gpg --output hello-decrypted.txt --decrypt hello-encrypted.gpg
</code></pre>
<h2 id="">相关文章</h2><ul><li><a href="https://www.gnupg.org/gph/en/manual/x110.html">gnupg.org 官方手册</a></li></ul></li></ol></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Cryptography</category>
            <category>GPG</category>
        </item>
        <item>
            <title><![CDATA[你好，我是 Chever John]]></title>
            <link>https://blog.cheverjohn.me/zh/hello-world</link>
            <guid>https://blog.cheverjohn.me/zh/hello-world</guid>
            <pubDate>Tue, 27 Dec 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[你好！ 我是 Chever John，一名软件工程师。]]></description>
            <content:encoded><![CDATA[<article><div><p>你好！</p><p>我是 Chever John，一名软件工程师。</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>hello</category>
            <category>world</category>
        </item>
        <item>
            <title><![CDATA[如何在 APISIX Ingress Controller 中运行 Go 插件运行器]]></title>
            <link>https://blog.cheverjohn.me/zh/how-to-run-go-plugin-runner-in-apisix-ingress</link>
            <guid>https://blog.cheverjohn.me/zh/how-to-run-go-plugin-runner-in-apisix-ingress</guid>
            <pubDate>Fri, 29 Apr 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[如标题所说。 背景描述 在社区中闲逛时，我发现有用户对"如何在 APISIX Ingress 环境中使用多语言插件"感到困惑。我恰好是 go-plugin-runner 的用户，对 APISIX Ingress 项目也有一些了解，于是就有了这份文档。 方案描述 基于 go-plugin-runner 插件的 0.3 版本和 APISIX Ingress 的 1.4.0 版本，本文通过构建集群、构建镜像、自定义 helm chart 包，最后部署资源的步骤。保证完全基于本文档可以推导出最终结果。 开始 构建集群环境 选择 kind 来构建本地集群环境。命令如下： 构建 go-plugin-run...]]></description>
            <content:encoded><![CDATA[<article><div><p>如标题所说。</p><h2 id="">背景描述</h2><p>在社区中闲逛时，我发现有用户对&quot;如何在 APISIX Ingress 环境中使用多语言插件&quot;感到困惑。我恰好是 go-plugin-runner 的用户，对 APISIX Ingress 项目也有一些了解，于是就有了这份文档。</p>
<h2 id="">方案描述</h2><p>基于 go-plugin-runner 插件的 0.3 版本和 APISIX Ingress 的 1.4.0 版本，本文通过构建集群、构建镜像、自定义 helm chart 包，最后部署资源的步骤。保证完全基于本文档可以推导出最终结果。</p><pre><code class="lang-bash">go-plugin-runner: 0.3
APISIX Ingress: 1.4.0

kind: kind v0.12.0 go1.17.8 linux/amd64
kubectl version: Client Version: v1.23.5/Server Version: v1.23.4
golang: go1.18 linux/amd64
</code></pre>
<h2 id="">开始</h2><h3 id="">构建集群环境</h3><p>选择 <code>kind</code> 来构建本地集群环境。命令如下：</p><pre><code class="lang-bash">cat &lt;&lt;EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: &quot;ingress-ready=true&quot;
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
EOF
</code></pre>
<h3 id="-go-plugin-runner-">构建 go-plugin-runner 可执行文件</h3><p>如果你已经完成了插件的编写，就可以开始编译可执行文件来与 APISIX 一起运行。</p><p>本文推荐两种打包构建选项。</p><ol start="1"><li>将打包过程放入 Dockerfile 中，在后续构建 docker 镜像时完成编译过程。</li><li>你也可以按照本文档使用的方案，先构建可执行文件，然后将打包的可执行文件复制到镜像中。</li></ol><p>如何选择方案应该根据你的本地硬件考虑。这里选择第二种方案的原因是，我想依托我强大的本地硬件来提高构建速度，加快流程。</p><h3 id="-go-plugin-runner-">进入 go-plugin-runner 目录</h3><p>选择一个文件夹地址 <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code>，将我们的 <code>apisix-go-plugin-runner</code> 项目放置在这个文件夹中。</p><p>成功放置后，文件树如下所示：</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner$ tree -L 1
.
└── apisix-go-plugin-runner

1 directory, 0 files
</code></pre>
<p>然后你需要进入 <code>apisix-go-plugin-runner/cmd/go-runner/plugins</code> 目录，在该目录中编写你需要的插件。本文将使用默认插件 <code>say</code> 进行演示。</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner/apisix-go-plugin-runner$ tree cmd
cmd
└── go-runner
    ├── main.go
    ├── main_test.go
    ├── plugins
    │   ├── fault_injection.go
    │   ├── fault_injection_test.go
    │   ├── limit_req.go
    │   ├── limit_req_test.go
    │   ├── say.go
    │   └── say_test.go
    └── version.go
    
2 directories, 10 files
</code></pre>
<p>编写完插件后，正式开始编译可执行文件，这里注意应该构建静态可执行文件，而不是动态的。</p><p>包编译命令如下。</p><pre><code class="lang-bash">CGO_ENABLED=0 go build -a -ldflags &#x27;-extldflags &quot;-static&quot;&#x27; .
</code></pre>
<p>这样就成功打包了一个静态编译的 <code>go-runner</code> 可执行文件。</p><p>在 <code>apisix-go-plugin-runner/cmd/go-runner/</code> 目录中，你可以看到当前文件树如下所示：</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner/apisix-go-plugin-runner/cmd/go-runner$ tree -L 1
.
├── go-runner
├── main.go
├── main_test.go
├── plugins
└── version.go

1 directory, 4 files
</code></pre>
<p>请记住路径 <code>apisix-go-plugin-runner/cmd/go-runner/go-runner</code>，我们稍后会用到它。</p><h3 id="-docker-">构建 Docker 镜像</h3><p>这里构建镜像是为了后续使用 <code>helm</code> 安装 APISIX 做准备。</p><h4 id="-dockerfile">编写 Dockerfile</h4><p>回到路径 <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code>，在该目录中创建一个 Dockerfile，这里给出一个演示。</p><pre><code class="lang-dockerfile">ARG ENABLE_PROXY=false

# Build Apache APISIX
FROM api7/apisix-base:1.19.9.1.5

ADD ./apisix-go-plugin-runner /usr/local/apisix-go-plugin-runner

ARG APISIX_VERSION=2.13.1
LABEL apisix_version=&quot;${APISIX_VERSION}&quot;

ARG ENABLE_PROXY
RUN set -x \
    &amp;&amp; (test &quot;${ENABLE_PROXY}&quot; != &quot;true&quot; || /bin/sed -i &#x27;s,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g&#x27; /etc/apk/repositories) \
    &amp;&amp; apk add --no-cache --virtual .builddeps \
        build-base \
        automake \
        autoconf \
        make \
        libtool \
        pkgconfig \
        cmake \
        unzip \
        curl \
        openssl \
        git \
        openldap-dev \
    &amp;&amp; luarocks install https://github.com/apache/apisix/raw/master/rockspec/apisix-${APISIX_VERSION}-0.rockspec --tree=/usr/local/apisix/deps PCRE_DIR=/usr/local/openresty/pcre \
    &amp;&amp; cp -v /usr/local/apisix/deps/lib/luarocks/rocks-5.1/apisix/${APISIX_VERSION}-0/bin/apisix /usr/bin/ \
    &amp;&amp; (function ver_lt { [ &quot;$1&quot; = &quot;$2&quot; ] &amp;&amp; return 1 || [ &quot;$1&quot; = &quot;`echo -e &quot;$1\n$2&quot; | sort -V | head -n1&quot;`&quot; ]; };  if [ &quot;$APISIX_VERSION&quot; = &quot;master&quot; ] || ver_lt 2.2.0 $APISIX_VERSION; then echo &#x27;use shell &#x27;;else bin=&#x27;#! /usr/local/openresty/luajit/bin/luajit\npackage.path = &quot;/usr/local/apisix/?.lua;&quot; .. package.path&#x27;; sed -i &quot;1s@.*@$bin@&quot; /usr/bin/apisix ; fi;) \
    &amp;&amp; mv /usr/local/apisix/deps/share/lua/5.1/apisix /usr/local/apisix \
    &amp;&amp; apk del .builddeps \
    &amp;&amp; apk add --no-cache \
        bash \
        curl \
        libstdc++ \
        openldap \
        tzdata \
    # forward request and error logs to docker log collector
    &amp;&amp; ln -sf /dev/stdout /usr/local/apisix/logs/access.log \
    &amp;&amp; ln -sf /dev/stderr /usr/local/apisix/logs/error.log

WORKDIR /usr/local/apisix

ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin

EXPOSE 9080 9443

CMD [&quot;sh&quot;, &quot;-c&quot;, &quot;/usr/bin/apisix init &amp;&amp; /usr/bin/apisix init_etcd &amp;&amp; /usr/local/openresty/bin/openresty -p /usr/local/apisix -g &#x27;daemon off;&#x27;&quot;]

STOPSIGNAL SIGQUIT
</code></pre>
<p>这份 Dockerfile 配置文档，来源于这个<a href="https://github.com/apache/apisix-docker/blob/master/alpine/Dockerfile">链接</a>。我做的唯一修改如下：</p><pre><code class="lang-bash">ARG ENABLE_PROXY=false

# Build Apache APISIX
FROM api7/apisix-base:1.19.9.1.5

ADD ./apisix-go-plugin-runner /usr/local/apisix-go-plugin-runner

ARG APISIX_VERSION=2.13.1
LABEL apisix_version=&quot;${APISIX_VERSION}&quot;

ARG ENABLE_PROXY

</code></pre>
<p>将 <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code> 目录中的所有 <code>/apisix-go-plugin-runner</code> 文件打包到 Docker 镜像中。注意可执行文件 <code>apisix-go-plugin-runner/cmd/go-runner/go-runner</code> 的位置和上面 Dockerfile 中 <code>/usr/local/apisix-go-plugin-runner</code> 目录的位置，得出可执行文件在 Docker 镜像中的最终位置如下。</p><pre><code class="lang-bash">/usr/local/apisix-go-plugin-runner/cmd/go-runner/go-runner
</code></pre>
<p>请记住这个地址。我们将在其余配置中使用它。</p><h4 id="-docker-">开始构建 Docker 镜像</h4><p>基于 Dockerfile 开始构建 Docker 镜像。命令在 <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code> 目录中执行。命令如下：</p><pre><code class="lang-bash">docker build -t apisix/forrunner:0.1 .
</code></pre>
<p>命令解释：构建一个名为 <code>apisix/forrunner</code> 的镜像，并标记为 0.1 版本。</p><h4 id="">将镜像加载到集群环境</h4><pre><code class="lang-bash">kind  load docker-image apisix/forrunner:0.1 
</code></pre>
<p>将镜像加载到 kind 集群环境中，以便在 helm 安装过程中拉取自定义本地镜像进行安装。</p><h3 id="-apisix-ingress">安装 APISIX Ingress</h3><h4 id="-helm-chart">自定义 helm chart</h4><p>这一部分重点是修改官方 helm 包中的 <code>values.yaml</code> 文件，使其能够安装本地打包的镜像，并正确运行 <code>go-plugin-runner</code> 可执行文件。</p><h5 id="-helm-chart">获取官方 helm chart</h5><p>首先，用以下命令获取最新的 apisix helm chart 包：</p><pre><code class="lang-bash">helm fetch apisix/apisix
</code></pre>
<p>文件树如下：</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner$ tree -L 1
.
├── apisix-0.9.1.tgz
└── apisix-go-plugin-runner

1 directory, 1 file
</code></pre>
<h5 id="">解压</h5><p>解压 <code>apisix-0.9.1.tgz</code> 文件，准备重写配置。解压命令如下。</p><pre><code class="lang-bash">tar zxvf apisix-0.9.1.tgz
</code></pre>
<p>文件树如下：</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner$ tree -L 1
.
├── apisix
├── apisix-0.9.1.tgz
└── apisix-go-plugin-runner

2 directories, 1 file
</code></pre>
<h5 id="-valuesyaml">更改 <code>values.yaml</code></h5><p>进入 <code>apisix</code> 文件夹，修改 <code>values.yaml</code> 文件。两处更改如下：</p><pre><code class="lang-yaml">image:
  repository: apisix/forrunner
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: 0.1
</code></pre>
<p>第一处更改将 helm 安装的镜像设置为你自己本地打包的镜像。</p><pre><code class="lang-yaml">extPlugin:
  enabled: true
  cmd: [&quot;/usr/local/apisix-go-plugin-runner/cmd/go-runner/go-runner&quot;, &quot;run&quot;]
</code></pre>
<p>第二处更改设置了运行容器后 go-runner 在容器中的位置。</p><h5 id="-helm-chart">压缩修改后的 helm chart</h5><p>配置完成后，压缩 <code>apisix</code> 文件。压缩命令如下：</p><pre><code class="lang-bash">tar zcvf apisix.tgz apisix/
</code></pre>
<p>获得压缩文件，此时文件树如下：</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner$ tree -L 1
.
├── apisix
├── apisix-0.9.1.tgz
├── apisix-go-plugin-runner
└── apisix.tgz

2 directories, 2 files
</code></pre>
<h4 id="-helm-">执行 helm 安装命令</h4><h5 id="">创建命名空间</h5><p>安装前，先用以下命令创建命名空间：</p><pre><code class="lang-bash">kubectl create ns ingress-apisix
</code></pre>
<p>然后使用 helm 安装 APISIX，命令如下：</p><pre><code class="lang-bash">helm install apisix ./apisix.tgz --set gateway.type=NodePort --set ingress-controller.enabled=true --namespace ingress-apisix --set ingress-controller.config.apisix.serviceNamespace=ingress-apisix
</code></pre>
<h3 id="-httpbin--apisixroute-">创建 httpbin 服务和 ApisixRoute 资源</h3><p>创建一个 httpbin 后端资源，与部署的 ApisixRoute 资源一起运行，以测试功能是否正常工作。</p><h4 id="-httpbin-">创建 httpbin 服务</h4><p>用以下命令创建 httpbin 服务：</p><pre><code class="lang-bash">kubectl run httpbin --image kennethreitz/httpbin --port 80
</code></pre>
<p>用以下命令暴露端口：</p><pre><code class="lang-bash">kubectl expose pod httpbin --port 80
</code></pre>
<h4 id="-apisixroute-">创建 ApisixRoute 资源</h4><p>创建 <code>go-plugin-runner-route.yaml</code> 文件来启用 ApisixRoute 资源，配置文件如下：</p><pre><code class="lang-yaml">apiVersion: apisix.apache.org/v2beta3
kind: ApisixRoute
metadata:
  name: plugin-runner-demo
spec:
  http:
  - name: rule1
    match:
      hosts:
      - local.httpbin.org
      paths:
      - /get
    backends:
    - serviceName: httpbin
      servicePort: 80
    plugins:
    - name: ext-plugin-pre-req
      enable: true
      config:
        conf:
        - name: &quot;say&quot;
          value: &quot;{\&quot;body\&quot;: \&quot;hello\&quot;}&quot;
</code></pre>
<p>创建资源的命令如下：</p><pre><code class="lang-bash">kubectl apply -f go-plugin-runner-route.yaml
</code></pre>
<h3 id="">测试</h3><p>用以下命令测试用 Golang 编写的插件是否正常工作：</p><pre><code class="lang-bash">kubectl exec -it -n ${namespace of Apache APISIX} ${Pod name of Apache APISIX} -- curl http://127.0.0.1:9080/get -H &#x27;Host: local.httpbin.org&#x27;
</code></pre>
<p>这里我从 <code>kubectl get pods --all-namespaces</code> 命令推导出 <code>${namespace of Apache APISIX}</code> 和 <code>${Pod name of Apache APISIX}</code> 参数分别是 <code>ingress-apisix</code> 和 <code>apisix-55d476c64-s5lzw</code>，执行命令如下：</p><pre><code class="lang-bash">kubectl exec -it -n ingress-apisix apisix-55d476c64-s5lzw -- curl http://127.0.0.1:9080/get -H &#x27;Host: local.httpbin.org&#x27;
</code></pre>
<p>期望得到的响应是：</p><pre><code class="lang-bash">chever@cloud-native-01:~/api7/cloud_native/tasks/plugin-runner$ kubectl exec -it -n ingress-apisix apisix-55d476c64-s5lzw -- curl http://127.0.0.1:9080/get -H &#x27;Host: local.httpbin.org&#x27;
Defaulted container &quot;apisix&quot; out of: apisix, wait-etcd (init)
hello
</code></pre></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>go</category>
        </item>
    </channel>
</rss>