<?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 's Blog</title>
        <link>https://blog.cheverjohn.me</link>
        <description> Chever John 's blog and personal website. Software engineer, Photographer</description>
        <lastBuildDate>Wed, 02 Sep 2026 08:32:56 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for Node.js</generator>
        <language>en</language>
        <ttl>60</ttl>
        <image>
            <title> Chever John 's Blog</title>
            <url>https://blog.cheverjohn.me/assets/avatar.jpg</url>
            <link>https://blog.cheverjohn.me</link>
        </image>
        <copyright>All rights reserved 2026, Chenwei Jiang</copyright>
        <item>
            <title><![CDATA[Temporary Egress with SSH Reverse SOCKS and KubeVirt]]></title>
            <link>https://blog.cheverjohn.me/temporary-egress-with-ssh-reverse-socks-and-kubevirt</link>
            <guid>https://blog.cheverjohn.me/temporary-egress-with-ssh-reverse-socks-and-kubevirt</guid>
            <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I needed to lend a Singapore server a temporary network egress for one weekend. The egress had to be my Ubuntu VM inside a local K3s cluster, not the Singapore server itself. It also had to survive terminal disconnects and transient network failures, stop after exactly four days, and leave no reusab...]]></description>
            <content:encoded><![CDATA[<article><div><p>I needed to lend a Singapore server a temporary network egress for one weekend. The egress had to be my Ubuntu VM inside a local K3s cluster, not the Singapore server itself. It also had to survive terminal disconnects and transient network failures, stop after exactly four days, and leave no reusable credential behind.</p><p>The final design was small:</p><ul><li>OpenSSH remote dynamic forwarding exposed a SOCKS5 listener only on the Singapore server&#x27;s loopback interface.</li><li>A systemd service on the Ubuntu VM kept the SSH connection alive and restarted it after failures.</li><li>An absolute systemd timer stopped and disabled the service at the deadline.</li><li>Live socket inspection showed destination IPs and ports without pretending that HTTPS payloads were readable.</li><li>Cleanup revoked the public key first, then removed the local private key and every tunnel artifact.</li></ul><p>This post documents the complete path, including the mistakes. All public addresses, usernames, and key material below are sanitized examples.</p><h2 id="tldr">TL;DR</h2><p>The command that made the Ubuntu VM the egress was:</p><pre><code class="lang-bash">ssh -N -T \
  -i ~/.ssh/sg-egress-ed25519 \
  -o BatchMode=yes \
  -o IdentitiesOnly=yes \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=~/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:1080 \
  sg-egress@203.0.113.10
</code></pre>
<p>The important detail is that <code>-R 127.0.0.1:1080</code> has <strong>no fixed destination</strong>. With modern OpenSSH, that creates a remote dynamic SOCKS listener. Applications on the Singapore server connect to <code>127.0.0.1:1080</code>; the Ubuntu-side SSH client then opens the destination connections, so the Internet sees the Ubuntu VM&#x27;s upstream public address.</p><p>Verification from the Singapore server:</p><pre><code class="lang-bash">curl --proxy socks5h://127.0.0.1:1080 https://ifconfig.me/ip
</code></pre>
<p>If the result matches the Ubuntu VM&#x27;s direct egress IP and differs from the Singapore server&#x27;s direct IP, the data path is correct.</p><h2 id="requirement-and-safety-boundaries">Requirement and safety boundaries</h2><p>This was intentionally a narrow, temporary capability:</p><ol start="1"><li>Only processes on the Singapore server could use the proxy.</li><li>The SOCKS port was never exposed on <code>0.0.0.0</code>.</li><li>The SSH key belonged to an unprivileged account that could only open the required remote listener.</li><li>The tunnel recovered automatically, but the deadline could not slide after a reboot.</li><li>The key expired server-side at the same deadline and was revoked before its private half was deleted.</li></ol><p>Binding the proxy to <code>127.0.0.1</code> matters. Using this instead:</p><pre><code class="lang-text">-R 0.0.0.0:1080
</code></pre>
<p>would request a public listener. The client address alone is not a sufficient guard: <code>GatewayPorts yes</code> on the SSH server forces remote forwards onto wildcard addresses even when the client requests <code>127.0.0.1</code>. I therefore enforced <code>GatewayPorts no</code> and <code>PermitListen 127.0.0.1:1080</code> for the tunnel account, then verified the live listener.</p><h2 id="topology-and-data-path">Topology and data path</h2><p>The Ubuntu VM ran in KubeVirt on K3s. Its SSH service was published through a NodePort on the worker node.</p><pre><code class="lang-mermaid">flowchart LR
    Mac[&quot;Operator Mac&quot;]
    Node[&quot;K3s worker&lt;br/&gt;192.168.22.31:30022&quot;]
    VM[&quot;KubeVirt Ubuntu VM&lt;br/&gt;SSH :22&quot;]
    SG[&quot;Singapore server&lt;br/&gt;127.0.0.1:1080&quot;]
    Internet[&quot;Internet destination&quot;]

    Mac --&gt;|&quot;SSH administration&quot;| Node
    Node --&gt;|&quot;NodePort to VM :22&quot;| VM
    VM --&gt;|&quot;SSH control connection :22&lt;br/&gt;remote dynamic forwarding&quot;| SG
    SG --&gt;|&quot;SOCKS5 request inside tunnel&quot;| VM
    VM --&gt;|&quot;new outbound connection&quot;| Internet
</code></pre>
<p>The direction is easy to misread. The SSH control connection starts on the Ubuntu VM and goes to Singapore, but proxied application traffic starts on Singapore and exits from Ubuntu.</p><h2 id="confirm-that-i-was-entering-the-guest-not-virt-launcher">Confirm that I was entering the guest, not virt-launcher</h2><p>The KubeVirt pod contained the QEMU process. Running <code>kubectl exec</code> into <code>virt-launcher</code> would enter the compute container, not the Ubuntu guest OS.</p><p>I first confirmed the VMI and its SSH Service:</p><pre><code class="lang-bash">kubectl -n kubevirt-vms get vmi ubuntu-vm -o wide
kubectl -n kubevirt-vms get service ubuntu-vm-ssh -o wide
</code></pre>
<p>The Service mapped NodePort <code>30022</code> to guest port <code>22</code>, so guest access was:</p><pre><code class="lang-bash">ssh -p 30022 vmuser@192.168.22.31
</code></pre>
<p>For one-off administration I also used a local port-forward:</p><pre><code class="lang-bash">kubectl -n kubevirt-vms port-forward service/ubuntu-vm-ssh 2222:22
ssh -p 2222 vmuser@127.0.0.1
</code></pre>
<p>These ports have different meanings:</p><ul><li><code>30022</code> is the persistent K3s NodePort.</li><li><code>2222</code> was a temporary port on my Mac created by <code>kubectl port-forward</code>.</li><li><code>1080</code> was the SOCKS listener on the Singapore server.</li></ul><p>Also note the CLI detail: <code>ssh</code> uses lowercase <code>-p</code> for a port; <code>scp</code> uses uppercase <code>-P</code>.</p><h2 id="prepare-a-dedicated-ssh-credential-and-deadline">Prepare a dedicated SSH credential and deadline</h2><p>The private key lived only on the Ubuntu VM and was mode <code>0600</code>:</p><pre><code class="lang-bash">install -d -m 700 ~/.ssh
install -m 600 /secure/input/sg-egress-ed25519 ~/.ssh/sg-egress-ed25519
ssh-keygen -lf ~/.ssh/sg-egress-ed25519
</code></pre>
<p>I calculated the deadline once and derived the OpenSSH key-expiry format from the same value:</p><pre><code class="lang-bash">deadline=$(date -u -d &#x27;+96 hours&#x27; &#x27;+%Y-%m-%d %H:%M:%S UTC&#x27;)
key_expiry=$(date -u -d &quot;$deadline&quot; &#x27;+%Y%m%d%H%M%SZ&#x27;)
public_key=$(ssh-keygen -y -f ~/.ssh/sg-egress-ed25519)
printf &#x27;deadline=%s\nkey_expiry=%s\n&#x27; &quot;$deadline&quot; &quot;$key_expiry&quot;
printf &#x27;expiry-time=&quot;%s&quot; %s sg-egress-four-day\n&#x27; &quot;$key_expiry&quot; &quot;$public_key&quot;
</code></pre>
<p>I transferred only the final public-key line through an authenticated administrative channel. On the Singapore server, a separate administrator created a dedicated account and installed that line as <code>/home/sg-egress/.ssh/authorized_keys</code>:</p><pre><code class="lang-bash">sudo useradd --create-home --user-group --shell /usr/sbin/nologin sg-egress
sudo install -d -o sg-egress -g sg-egress -m 700 /home/sg-egress/.ssh
sudo install -o sg-egress -g sg-egress -m 600 \
  /secure/input/sg-egress-authorized-key \
  /home/sg-egress/.ssh/authorized_keys
</code></pre>
<p>I also installed <code>/etc/ssh/sshd_config.d/sg-egress.conf</code>:</p><pre><code class="lang-text">Match User sg-egress
    AuthenticationMethods publickey
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    AllowAgentForwarding no
    AllowStreamLocalForwarding no
    AllowTcpForwarding remote
    GatewayPorts no
    PermitListen 127.0.0.1:1080
    PermitTTY no
    PermitTunnel no
    PermitUserRC no
    X11Forwarding no
    MaxSessions 0

Match all
</code></pre>
<p><code>MaxSessions 0</code> denies shell, command, and subsystem sessions while still permitting forwarding. <code>AllowTcpForwarding remote</code> rejects local forwards, and <code>PermitListen</code> limits the only allowed remote listener. I validated and reloaded the server configuration before using the key:</p><pre><code class="lang-bash">sudo sshd -t
sudo systemctl reload ssh
sudo sshd -T -C user=sg-egress,host=localhost,addr=127.0.0.1 | \
  grep -E &#x27;^(allowagentforwarding|allowstreamlocalforwarding|allowtcpforwarding|gatewayports|maxsessions|permitlisten|permittty|permittunnel|permituserrc|x11forwarding) &#x27;
</code></pre>
<p>Strict host-key checking also needs an explicit trust bootstrap. From an already authenticated administrative console on Singapore, I recorded the ED25519 host-key fingerprint:</p><pre><code class="lang-bash">sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
</code></pre>
<p>On the Ubuntu VM, running as <code>vmuser</code>, I scanned the public host key into a dedicated file but refused to install it until its fingerprint exactly matched the value obtained through that trusted channel. The tunnel never reads or modifies the shared <code>known_hosts</code> file:</p><pre><code class="lang-bash">pinned_hosts=$(mktemp ~/.ssh/sg-egress-known_hosts.XXXXXX)
trap &#x27;test ! -e &quot;$pinned_hosts&quot; || unlink &quot;$pinned_hosts&quot;&#x27; EXIT

ssh-keyscan -t ed25519 203.0.113.10 &gt; &quot;$pinned_hosts&quot;
scanned_fingerprint=$(ssh-keygen -lf &quot;$pinned_hosts&quot; | awk &#x27;NR == 1 { print $2 }&#x27;)
read -r -p &#x27;Trusted ED25519 SHA256 fingerprint: &#x27; trusted_fingerprint

if [ -z &quot;$scanned_fingerprint&quot; ] || \
   [ -z &quot;$trusted_fingerprint&quot; ] || \
   [ &quot;$scanned_fingerprint&quot; != &quot;$trusted_fingerprint&quot; ]; then
  printf &#x27;host-key fingerprint mismatch\n&#x27; &gt;&amp;2
  exit 1
fi

chmod 600 &quot;$pinned_hosts&quot;
mv &quot;$pinned_hosts&quot; ~/.ssh/sg-egress-known_hosts
trap - EXIT

ssh-keygen -F 203.0.113.10 -f ~/.ssh/sg-egress-known_hosts
ssh-keygen -lf ~/.ssh/sg-egress-known_hosts
</code></pre>
<p><code>ssh-keyscan</code> only collected the presented key; it was not the trust source. The independently obtained fingerprint was the trust source. Every tunnel command explicitly uses this file and ignores global host-key files, so exactly the verified key is trusted without changing unrelated SSH state.</p><p>Do not paste a production private key into a ticket, chat, shell history, or repository. Use a dedicated short-lived key and an authenticated transfer mechanism. If a private key is ever disclosed, revocation is mandatory; deleting one local copy is not enough.</p><h2 id="why-the-first--r-idea-was-wrong">Why the first <code>-R</code> idea was wrong</h2><p>The first mental model was a reverse SSH login port:</p><pre><code class="lang-bash">ssh -N -R 127.0.0.1:10022:127.0.0.1:22 sg-egress@203.0.113.10
</code></pre>
<p>That makes Singapore&#x27;s <code>127.0.0.1:10022</code> forward to the Ubuntu VM&#x27;s SSH port. It lets the Singapore server log back into the VM, but it does <strong>not</strong> make Ubuntu a general Internet egress.</p><p>The corrected command removed the fixed destination:</p><pre><code class="lang-bash">ssh -N -T \
  -i ~/.ssh/sg-egress-ed25519 \
  -o BatchMode=yes \
  -o IdentitiesOnly=yes \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=~/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -o TCPKeepAlive=yes \
  -o ConnectTimeout=15 \
  -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:1080 \
  sg-egress@203.0.113.10
</code></pre>
<p>Now <code>1080</code> is a SOCKS5 endpoint, and destinations are selected per client request.</p><h2 id="make-it-survive-a-weekend-with-systemd">Make it survive a weekend with systemd</h2><p>A foreground terminal is not a reliability mechanism. I installed this unit as <code>/etc/systemd/system/sg-egress-socks.service</code>:</p><pre><code class="lang-ini">[Unit]
Description=Reverse SOCKS5 tunnel using Ubuntu VM as SG egress
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=vmuser
Environment=LANG=C
ExecStart=/usr/bin/ssh -N -T \
  -i /home/vmuser/.ssh/sg-egress-ed25519 \
  -o BatchMode=yes \
  -o IdentitiesOnly=yes \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=/home/vmuser/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -o TCPKeepAlive=yes \
  -o ConnectTimeout=15 \
  -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:1080 \
  sg-egress@203.0.113.10
Restart=always
RestartSec=5s
TimeoutStopSec=15s
NoNewPrivileges=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
</code></pre>
<p><code>ExitOnForwardFailure=yes</code> is essential. Without it, SSH could stay connected even when it failed to bind the remote port, leaving systemd with a green process and a broken service.</p><p>Before enabling the unit, I validated it:</p><pre><code class="lang-bash">systemd-analyze verify /etc/systemd/system/sg-egress-socks.service
</code></pre>
<p>Then:</p><pre><code class="lang-bash">sudo systemctl daemon-reload
sudo systemctl enable --now sg-egress-socks.service
systemctl is-enabled sg-egress-socks.service
systemctl is-active sg-egress-socks.service
</code></pre>
<h2 id="stop-after-exactly-96-hours">Stop after exactly 96 hours</h2><p><code>RuntimeMaxSec=4d</code> looks attractive, but combining a runtime limit with an automatic restart policy creates ambiguous deadline behavior. I wanted a deadline that survived reboots and never moved. The <code>deadline</code> and <code>key_expiry</code> values were already calculated from the same instant when the key was prepared; the timer had to consume that exact <code>deadline</code> rather than a copied example timestamp.</p><p>The stop action was <code>/etc/systemd/system/sg-egress-socks-stop.service</code>:</p><pre><code class="lang-ini">[Unit]
Description=Stop and disable the SG egress SOCKS tunnel

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl disable sg-egress-socks.service
ExecStart=/usr/bin/systemctl stop sg-egress-socks.service
</code></pre>
<p>I rejected an empty or expired value, checked how systemd parsed it, and generated the timer directly from the shell variable:</p><pre><code class="lang-bash">test -n &quot;${deadline:-}&quot; || {
  printf &#x27;deadline is not set\n&#x27; &gt;&amp;2
  exit 1
}
deadline_epoch=$(date -u -d &quot;$deadline&quot; &#x27;+%s&#x27;)
test &quot;$deadline_epoch&quot; -gt &quot;$(date -u &#x27;+%s&#x27;)&quot; || {
  printf &#x27;deadline is not in the future: %s\n&#x27; &quot;$deadline&quot; &gt;&amp;2
  exit 1
}
systemd-analyze calendar &quot;$deadline&quot;

sudo tee /etc/systemd/system/sg-egress-socks-stop.timer &gt;/dev/null &lt;&lt;EOF
[Unit]
Description=Stop the SG egress SOCKS tunnel after the four-day window

[Timer]
OnCalendar=$deadline
AccuracySec=1s
Persistent=yes
Unit=sg-egress-socks-stop.service

[Install]
WantedBy=timers.target
EOF
</code></pre>
<p><code>Persistent=yes</code> ensures that a missed deadline fires after the VM returns. The stop action also disables the tunnel unit, so a later reboot cannot reopen it. The matching <code>expiry-time</code> on the Singapore server prevents a new SSH authentication after the same deadline even if the client-side timer fails.</p><pre><code class="lang-bash">sudo systemctl daemon-reload
sudo systemctl enable --now sg-egress-socks-stop.timer
systemctl list-timers --all sg-egress-socks-stop.timer
</code></pre>
<h2 id="verify-the-actual-egress">Verify the actual egress</h2><p>I compared three observations:</p><pre><code class="lang-bash"># On the Ubuntu VM
curl -4fsS https://ifconfig.me/ip

# Directly on the Singapore server
curl -4fsS https://ifconfig.me/ip

# On Singapore, through the reverse SOCKS tunnel
curl -4fsS \
  --proxy socks5h://127.0.0.1:1080 \
  https://ifconfig.me/ip
</code></pre>
<p>The expected relationship is:</p><pre><code class="lang-text">SG via SOCKS IP == Ubuntu direct IP
SG direct IP     != Ubuntu direct IP
</code></pre>
<p>I also checked the listener on Singapore:</p><pre><code class="lang-bash">ss -lntp &#x27;sport = :1080&#x27;
</code></pre>
<p>It had to show <code>127.0.0.1:1080</code>, not <code>0.0.0.0:1080</code>.</p><p>Finally, I killed the SSH main process once and watched systemd recover. The PID changed, <code>NRestarts</code> increased, and the SOCKS test worked again seven seconds later. This tested the failure mode I actually cared about instead of merely checking that the initial start succeeded.</p><h2 id="observe-live-traffic-without-inventing-visibility">Observe live traffic without inventing visibility</h2><p>The SSH client process opens destination sockets on the Ubuntu VM. Its live connections can be inspected with:</p><pre><code class="lang-bash">tunnel_pid=$(systemctl show sg-egress-socks.service -p MainPID --value)
sudo lsof -nP -a -p &quot;$tunnel_pid&quot; -i
</code></pre>
<p>For a persistent terminal view:</p><pre><code class="lang-bash">tmux new-session -A -s sg-egress-monitor
watch -n 1 &#x27;pid=$(systemctl show sg-egress-socks.service -p MainPID --value); printf &quot;tunnel_pid=%s\n&quot; &quot;$pid&quot;; if [ &quot;$pid&quot; != 0 ]; then sudo -n lsof -nP -a -p &quot;$pid&quot; -i; else printf &quot;service is not running\n&quot;; fi&#x27;
</code></pre>
<p>Detach with <code>Ctrl-b</code>, then <code>d</code>.</p><p>This view has strict limits:</p><ul><li>It shows active destination IPs and ports.</li><li>It may miss very short connections between one-second samples.</li><li>It does not reconstruct historical flows.</li><li>HTTPS payloads, credentials, and full URLs remain encrypted.</li><li>DNS names may be visible only when ordinary DNS or additional auditing captures them.</li></ul><p>If historical accounting is a real requirement, enable consented flow logging before the window starts. A live <code>lsof</code> screen is not an audit log.</p><h2 id="failures-encountered">Failures encountered</h2><h3 id="1-one-ip-check-endpoint-failed">1. One IP-check endpoint failed</h3><p><code>api.ipify.org:443</code> returned <code>Connection refused</code>, and a Cloudflare endpoint timed out from the Ubuntu egress. Other independent endpoints returned the same valid public address.</p><p>The lesson was not to change the tunnel because one measurement target failed. Test DNS, TCP reachability, and at least two unrelated endpoints before blaming the data path.</p><h3 id="2-remote-port-1080-was-occupied">2. Remote port <code>1080</code> was occupied</h3><p>The service later accumulated 1,428 failures over roughly two hours:</p><pre><code class="lang-text">Error: remote port forwarding failed for listen port 1080
</code></pre>
<p><code>ExitOnForwardFailure=yes</code> made each failed bind visible to systemd, which retried every five seconds. The evidence proved a port conflict on Singapore; it did not prove whether the owner was a stale SSH session or another manually started tunnel.</p><p>The correct investigation was:</p><pre><code class="lang-bash">ss -lntp &#x27;sport = :1080&#x27;
ps -fp &lt;listener-pid&gt;
</code></pre>
<p>Do not kill a listener based only on a port number. Identify the owning process and connection first. Once the conflicting listener disappeared, the managed service acquired <code>1080</code> and stayed active.</p><h3 id="3-a-multiline-paste-broke-the-tmux-monitor">3. A multiline paste broke the tmux monitor</h3><p>The shell received actual newlines inside the service name and options:</p><pre><code class="lang-text">sg-egress-
socks.service
-p
MainPID
</code></pre>
<p>That produced <code>socks.service: not found</code>, <code>-p: not found</code>, and an empty PID that made <code>lsof -a</code> invalid. Terminal visual wrapping is harmless; embedded newline characters are not. Replacing the stored tmux command with one literal shell line fixed the monitor.</p><h3 id="4-a-green-control-connection-does-not-imply-proxy-traffic">4. A green control connection does not imply proxy traffic</h3><p>The monitor always showed one established socket from Ubuntu to Singapore port <code>22</code>. That was the SSH control connection. Additional destination sockets appeared only while a client actually used the SOCKS proxy.</p><p>During a ten-second sample, Singapore had no established client to <code>127.0.0.1:1080</code> and no speed-test process. The correct conclusion was simply “the proxy is idle now,” not “the monitoring is broken.”</p><h2 id="complete-revocation-and-cleanup">Complete revocation and cleanup</h2><p>The order matters. Stop the active tunnel first, revoke the public key through a separate administrative account, verify that the old key cannot create a new tunnel, and only then delete the private key.</p><p>The remote scripts below use stdin for their bodies, so they cannot also use stdin for a sudo password. Before changing any state, I verified that the separate administrator had pre-existing non-interactive sudo access:</p><pre><code class="lang-bash">ssh \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=~/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  admin@203.0.113.10 \
  sudo -n true
</code></pre>
<p>If that check fails, stop here and run the privileged command bodies directly from an already authenticated Singapore management console. Do not send a sudo password through the script stream.</p><h3 id="1-stop-the-active-tunnel-but-keep-the-private-key">1. Stop the active tunnel but keep the private key</h3><pre><code class="lang-bash">sudo systemctl disable --now sg-egress-socks-stop.timer
sudo systemctl disable --now sg-egress-socks.service
sudo systemctl stop sg-egress-socks-stop.service
</code></pre>
<p>Stopping first closes the already-authenticated SSH connection. Removing an authorized key does not terminate sessions that are already established.</p><h3 id="2-remove-only-the-matching-public-key-on-singapore">2. Remove only the matching public key on Singapore</h3><p>From the Ubuntu VM, derive the public blob without printing private material. Use a separate administrative account on Singapore to pass it as one controlled argument, require exactly one match, and replace <code>authorized_keys</code> atomically. The restricted tunnel key cannot and must not execute this cleanup script:</p><pre><code class="lang-bash">key_blob=$(ssh-keygen -y -f ~/.ssh/sg-egress-ed25519 | awk &#x27;{print $2}&#x27;)

ssh \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=~/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  admin@203.0.113.10 \
  sudo -n bash -s -- &quot;$key_blob&quot; &lt;&lt;&#x27;REMOTE&#x27;
set -eu
key_blob=$1
auth_file=/home/sg-egress/.ssh/authorized_keys
matches=$(awk -v key_blob=&quot;$key_blob&quot; \
  &#x27;index($0, key_blob) { count++ } END { print count+0 }&#x27; \
  &quot;$auth_file&quot;)

if [ &quot;$matches&quot; -ne 1 ]; then
  printf &#x27;refusing to edit: expected 1 key, found %s\n&#x27; &quot;$matches&quot; &gt;&amp;2
  exit 1
fi

temp_auth=$(mktemp /home/sg-egress/.ssh/authorized_keys.cleanup.XXXXXX)
trap &#x27;test ! -e &quot;$temp_auth&quot; || unlink &quot;$temp_auth&quot;&#x27; EXIT

awk -v key_blob=&quot;$key_blob&quot; \
  &#x27;index($0, key_blob) == 0&#x27; \
  &quot;$auth_file&quot; &gt; &quot;$temp_auth&quot;

chmod --reference=&quot;$auth_file&quot; &quot;$temp_auth&quot;
chown --reference=&quot;$auth_file&quot; &quot;$temp_auth&quot;
mv &quot;$temp_auth&quot; &quot;$auth_file&quot;
REMOTE
</code></pre>
<p>An OpenSSH public-key blob contains a restricted base64 alphabet, but the match-count guard remains important: cleanup must not silently remove zero keys or multiple keys.</p><p>Then verify that the old key cannot authenticate a new forwarding connection. Because the managed service is already stopped, port <code>1080</code> is free; require an explicit <code>Permission denied (publickey)</code> result rather than treating a timeout or network error as successful revocation:</p><pre><code class="lang-bash">timeout 10s ssh -N -T \
  -i ~/.ssh/sg-egress-ed25519 \
  -o BatchMode=yes \
  -o IdentitiesOnly=yes \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=~/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  -o ConnectTimeout=5 \
  -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:1080 \
  sg-egress@203.0.113.10
</code></pre>
<p>The expected result is <code>Permission denied (publickey)</code>.</p><p>The administrator can now remove the dedicated account and its scoped SSH configuration:</p><pre><code class="lang-bash">ssh \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile=~/.ssh/sg-egress-known_hosts \
  -o GlobalKnownHostsFile=/dev/null \
  admin@203.0.113.10 \
  sudo -n bash -s &lt;&lt;&#x27;REMOTE&#x27;
set -eu
userdel -r sg-egress
unlink /etc/ssh/sshd_config.d/sg-egress.conf
sshd -t
systemctl reload ssh
REMOTE
</code></pre>
<h3 id="3-remove-the-ubuntu-side-runtime-artifacts">3. Remove the Ubuntu-side runtime artifacts</h3><pre><code class="lang-bash">tmux kill-session -t sg-egress-monitor

sudo unlink /etc/systemd/system/sg-egress-socks.service
sudo unlink /etc/systemd/system/sg-egress-socks-stop.service
sudo unlink /etc/systemd/system/sg-egress-socks-stop.timer
sudo systemctl daemon-reload

unlink ~/.ssh/sg-egress-ed25519
unlink ~/.ssh/sg-egress-known_hosts
</code></pre>
<p>Remove any explicitly known temporary copies as well. Avoid broad globs when credentials are involved.</p><p>System journals were retained as an audit trail. Selectively erasing one unit&#x27;s records is not a normal journal operation, while vacuuming the whole journal would destroy unrelated operational evidence.</p><h3 id="4-final-checks">4. Final checks</h3><pre><code class="lang-bash">systemctl show sg-egress-socks.service -p LoadState --value
systemctl list-timers --all &#x27;sg-egress-socks*&#x27;
pgrep -af &#x27;[s]sh.*-R.*1080&#x27;
tmux has-session -t sg-egress-monitor
</code></pre>
<p>The final state was:</p><ul><li>all three units: <code>not-found</code></li><li>no matching timer</li><li>no <code>ssh -R</code> process</li><li>no monitor tmux session</li><li>no private key, dedicated host-key file, or temporary unit copies on Ubuntu</li><li>no dedicated <code>sg-egress</code> account or scoped SSH configuration on Singapore</li><li>the old key rejected by Singapore</li><li>no listener on Singapore <code>127.0.0.1:1080</code></li></ul><h2 id="what-i-would-keep-for-the-next-temporary-egress">What I would keep for the next temporary egress</h2><ol start="1"><li>Treat reverse login forwarding and reverse dynamic forwarding as different tools.</li><li>Bind temporary proxies to loopback unless broader exposure is explicitly required.</li><li>Combine SSH keepalives with systemd restart and <code>ExitOnForwardFailure=yes</code>.</li><li>Use an absolute persistent deadline, then disable the service at expiry.</li><li>Verify the egress by comparing direct and proxied public addresses.</li><li>Decide before launch whether live observation or historical auditing is required.</li><li>Revoke the server-side public key before deleting the local private key.</li><li>Preserve logs, but remove credentials and runtime artifacts.</li></ol><p>The tunnel itself was one line. Making it reliable, bounded, observable, and revocable was the actual engineering work.</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>homelab</category>
            <category>ssh</category>
            <category>networking</category>
            <category>kubevirt</category>
            <category>systemd</category>
            <category>security</category>
            <category>troubleshooting</category>
        </item>
        <item>
            <title><![CDATA[Microsoft Store and Clash Verge on Windows: Fix AppContainer Loopback Access]]></title>
            <link>https://blog.cheverjohn.me/troubleshoot-microsoft-store-with-clash-verge-on-windows</link>
            <guid>https://blog.cheverjohn.me/troubleshoot-microsoft-store-with-clash-verge-on-windows</guid>
            <pubDate>Wed, 26 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Microsoft Store failed to initialize while browsers and other desktop applications continued to work. The obvious reactions—reinstalling Store, resetting the whole network stack, or replacing Clash—would all have been too destructive. Nothing was broken in the Store package, the relevant Windows ser...]]></description>
            <content:encoded><![CDATA[<article><div><p>Microsoft Store failed to initialize while browsers and other desktop applications continued to work. The obvious reactions—reinstalling Store, resetting the whole network stack, or replacing Clash—would all have been too destructive.</p><p>Nothing was broken in the Store package, the relevant Windows services, or the Clash core. The failure was on the first edge of the data path: <strong>Microsoft Store runs inside an AppContainer and lacked permission to reach the Clash proxy through the local loopback interface.</strong></p><p>The minimal fix was to grant a loopback exemption to Microsoft Store. Clash Verge kept both TUN mode and the Windows system proxy enabled.</p><h2 id="tldr">TL;DR</h2><ul><li><strong>Symptom</strong>: Microsoft Store failed to initialize when Clash Verge used both TUN mode and the Windows system proxy.</li><li><strong>Proxy state</strong>: The system proxy was <code>127.0.0.1:7897</code>, and <code>verge-mihomo</code> was listening on that port.</li><li><strong>Eliminated causes</strong>: The Store package and its related Windows services were healthy.</li><li><strong>Root cause</strong>: Microsoft Store was missing from the AppContainer loopback exemption list and could not reach the local Clash proxy.</li><li><strong>Fix</strong>: Run this command as Administrator:</li></ul><pre><code class="lang-powershell">CheckNetIsolation.exe LoopbackExempt -a -n=Microsoft.WindowsStore_8wekyb3d8bbwe
</code></pre>
<h2 id="incident-context">Incident context</h2><p>The failure occurred with both Clash Verge entry points enabled:</p><ul><li>TUN mode: enabled</li><li>Windows system proxy: enabled</li><li>System proxy address: <code>127.0.0.1:7897</code></li><li>Clash core: <code>verge-mihomo</code></li></ul><p>Microsoft Store displayed this message:</p><blockquote><p>Sorry about that. Something went wrong and Microsoft Store failed to initialize. Try refreshing or come back later.</p></blockquote>
<p>The important signal was that regular desktop applications still had network access. A machine-wide outage or a stopped Clash core was therefore unlikely.</p><h2 id="start-with-the-data-path">Start with the data path</h2><p>With the system proxy enabled, the expected request path was:</p><pre><code class="lang-text">Microsoft Store (AppContainer)
        │
        │ connect to 127.0.0.1:7897
        ▼
Clash / verge-mihomo
        │
        ▼
Microsoft services
</code></pre>
<p>The broken edge was the first one, not the connection from Clash to the Internet.</p><p>Windows isolates AppContainer applications from the local loopback interface by default. A Win32 application reaching <code>127.0.0.1:7897</code> does not prove that Microsoft Store can do the same. Pointing the system proxy at a local listener exposes that difference.</p><p>Clash Fake-IP records only prove that a request entered Clash&#x27;s DNS or proxy pipeline. Seeing an address from <code>198.18.0.0/15</code> is <strong>not enough to blame Fake-IP itself</strong>. Changing the DNS mode would have treated a symptom without proving the cause.</p><h2 id="evidence-eliminate-corruption-before-changing-anything">Evidence: eliminate corruption before changing anything</h2><h3 id="1-verify-that-clash-is-listening">1. Verify that Clash is listening</h3><p>Check port <code>7897</code> in PowerShell:</p><pre><code class="lang-powershell">Get-NetTCPConnection -LocalPort 7897 -State Listen
</code></pre>
<p>In this incident, <code>verge-mihomo</code> was listening normally. The proxy process and its local entry point were healthy.</p><h3 id="2-check-the-microsoft-store-package">2. Check the Microsoft Store package</h3><pre><code class="lang-powershell">Get-AppxPackage -Name Microsoft.WindowsStore
</code></pre>
<p>The package status was <code>Ok</code>. With no evidence of package corruption, uninstalling or re-registering Store would have added risk without addressing the data path.</p><h3 id="3-check-related-windows-services">3. Check related Windows services</h3><pre><code class="lang-powershell">Get-Service AppXSvc, BITS, InstallService, wuauserv
</code></pre>
<p>No service failure was found. Package corruption and stopped update services were now low-priority hypotheses.</p><h3 id="4-inspect-the-loopback-exemption-list">4. Inspect the loopback exemption list</h3><pre><code class="lang-powershell">CheckNetIsolation.exe LoopbackExempt -s
</code></pre>
<p>The list did not contain:</p><pre><code class="lang-text">microsoft.windowsstore_8wekyb3d8bbwe
</code></pre>
<p>The evidence now formed a complete chain: Store had to reach <code>127.0.0.1:7897</code>, the proxy was listening, but the AppContainer did not have loopback access.</p><h2 id="minimal-fix-exempt-microsoft-store-only">Minimal fix: exempt Microsoft Store only</h2><p>Open PowerShell or Windows Terminal as Administrator and run:</p><pre><code class="lang-powershell">CheckNetIsolation.exe LoopbackExempt -a -n=Microsoft.WindowsStore_8wekyb3d8bbwe
</code></pre>
<p>Verify the result:</p><pre><code class="lang-powershell">CheckNetIsolation.exe LoopbackExempt -s
</code></pre>
<p>The output should now include:</p><pre><code class="lang-text">microsoft.windowsstore_8wekyb3d8bbwe
</code></pre>
<p>This grants loopback access to Microsoft Store only. It does not require disabling Clash TUN or the Windows system proxy, and it is narrower than relaxing isolation for unrelated applications.</p><p>If the local proxy is no longer used, remove the exemption with:</p><pre><code class="lang-powershell">CheckNetIsolation.exe LoopbackExempt -d -n=Microsoft.WindowsStore_8wekyb3d8bbwe
</code></pre>
<h2 id="clear-stale-state">Clear stale state</h2><p>After fixing the permission, clear state left behind by the failed requests.</p><h3 id="1-flush-the-dns-cache">1. Flush the DNS cache</h3><pre><code class="lang-powershell">ipconfig /flushdns
</code></pre>
<h3 id="2-reset-the-microsoft-store-cache">2. Reset the Microsoft Store cache</h3><p>Close Microsoft Store completely, then run:</p><pre><code class="lang-powershell">wsreset.exe
</code></pre>
<p><code>wsreset.exe</code> clears Store state and restarts the application. It does not grant loopback access, so running it before fixing the permission may only reproduce the same failure after a clean restart.</p><h2 id="why-0x80073d02-was-not-the-root-cause">Why <code>0x80073D02</code> was not the root cause</h2><p>The event log also contained <code>0x80073D02</code>. It indicates that Windows could not update Microsoft Store while the application was still using resources required by the update.</p><p>That was a separate issue:</p><ul><li>The loopback restriction explained why Store could not reach the local proxy.</li><li><code>0x80073D02</code> explained why a Store update could not finish while Store was running.</li></ul><p>The correct response to the error code was to close Store and allow the update to finish. It was not evidence that the Store package was corrupted.</p><h2 id="state-after-the-fix">State after the fix</h2><ul><li>Microsoft Store package status remained <code>Ok</code></li><li>Microsoft Store opened and refreshed normally</li><li>The AppContainer loopback exemption was present</li><li>Clash Verge TUN mode remained enabled</li><li>The Windows system proxy remained <code>127.0.0.1:7897</code></li><li><code>verge-mihomo</code> continued running</li></ul><h2 id="if-the-problem-returns">If the problem returns</h2><p>Use this order instead of reinstalling Store immediately:</p><ol start="1"><li>Close Microsoft Store completely.</li><li>Confirm that Clash Verge and <code>verge-mihomo</code> are running.</li><li>Confirm that <code>127.0.0.1:7897</code> is listening.</li><li>Run <code>CheckNetIsolation.exe LoopbackExempt -s</code> and verify the Store exemption.</li><li>Run <code>wsreset.exe</code>, then reopen Store.</li><li>If it still fails, inspect Clash connection logs for Microsoft domain rules and test the selected proxy node.</li></ol><p>A major Windows upgrade or system reset is a good reason to check the exemption again. Normal Clash configuration updates should not alter Windows AppContainer loopback permissions.</p><h2 id="takeaway">Takeaway</h2><p>The misleading shortcut was to equate “Store cannot open” with “Store is broken.” A better troubleshooting sequence is:</p><ol start="1"><li>Draw the request path.</li><li>Verify every node.</li><li>Identify the broken edge.</li><li>Change only that edge.</li></ol><p>Here, the broken edge was <code>Microsoft Store → 127.0.0.1:7897</code>. A single-application loopback exemption fixed it. Reinstalling Store, disabling TUN, or changing Clash DNS mode was unnecessary.</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>windows</category>
            <category>clash</category>
            <category>networking</category>
            <category>troubleshooting</category>
        </item>
        <item>
            <title><![CDATA[A NixOS Major Upgrade Without Relying on Luck: 25.11 to 26.05]]></title>
            <link>https://blog.cheverjohn.me/nixos-major-upgrade-without-losing-sleep</link>
            <guid>https://blog.cheverjohn.me/nixos-major-upgrade-without-losing-sleep</guid>
            <pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The machine I upgraded was not a disposable test box. It is my daily GNOME desktop and development environment, but it also runs PostgreSQL, Redis, Docker, Ollama, Traefik, Open WebUI, and several other services. Moving from NixOS 25.11 to 26.05 changes systemd, D-Bus, Docker, GNOME, NetworkManager,...]]></description>
            <content:encoded><![CDATA[<article><div><p>The machine I upgraded was not a disposable test box.</p><p>It is my daily GNOME desktop and development environment, but it also runs PostgreSQL, Redis, Docker, Ollama, Traefik, Open WebUI, and several other services. Moving from NixOS 25.11 to 26.05 changes systemd, D-Bus, Docker, GNOME, NetworkManager, glibc, and more. If I had simply run <code>nixos-rebuild switch</code>, a failure would have left me wondering whether the operating system, a database, a container image, or the live desktop session was responsible.</p><p>The most valuable result of this upgrade was therefore not a list of renamed NixOS options. It was a safer upgrade process: <strong>reduce the number of moving parts, build everything in advance, prepare the rollback before deployment, and validate the new system with real services and real data.</strong></p><h2 id="tldr">TL;DR</h2><p>This was the final upgrade sequence:</p><pre><code class="lang-mermaid">flowchart TB
    A[&quot;Phase 1: Reduce the variables&lt;br/&gt;Capture the baseline · Freeze application versions&quot;]
    B[&quot;Phase 2: Produce the candidate system&lt;br/&gt;Update the Flake · Migrate configuration · Build everything · Diff closures&quot;]
    C[&quot;Phase 3: Deploy with a way back&lt;br/&gt;Back up stateful data · Install only the boot entry · Reboot&quot;]
    D[&quot;Phase 4: Validate the real system&lt;br/&gt;Test in layers · Observe delayed jobs and logs&quot;]

    A --&gt; B --&gt; C --&gt; D
</code></pre>
<p>Six principles are worth carrying into future upgrades:</p><ol start="1"><li><code>system.stateVersion</code> is not the currently installed NixOS release. Do not bump it automatically.</li><li>Keep the operating-system upgrade separate from application upgrades. Temporarily pin container images to exact digests.</li><li>A successful <code>nix eval</code> does not prove that the system can be built. Build the complete system closure.</li><li>When several foundational components change at once, prefer a <code>boot</code> deployment over a live <code>switch</code>.</li><li>System generations and data backups recover from different classes of failure. You need both.</li><li>Validation must outlive the first few minutes after boot. Timers and delayed jobs may fail later.</li></ol><h2 id="why-i-did-not-run-nixos-rebuild-switch">Why I Did Not Run <code>nixos-rebuild switch</code></h2><p>NixOS has excellent declarative configuration and generation rollback, but neither eliminates every runtime risk.</p><p>The candidate system included all of these changes at once:</p><ul><li>D-Bus moved from <code>dbus-daemon</code> to <code>dbus-broker</code>.</li><li>The initrd switched to systemd.</li><li>systemd, Docker, GNOME, and NetworkManager crossed release boundaries.</li><li>The glibc update affected PostgreSQL collation metadata.</li><li>Home Manager required several option migrations.</li><li>Containers still using <code>latest</code> or <code>main</code> could have upgraded their applications during the reboot.</li></ul><p><code>switch</code> activates services inside the currently running system. That is convenient for a small configuration change. It is less attractive when foundational services, the desktop session, and stateful applications all change together. The result can be an awkward intermediate state: the target system on disk is new, some processes still belong to the old environment, and other services have already restarted.</p><p>I used the prebuilt result instead:</p><pre><code class="lang-bash">sudo ./result-26.05/bin/switch-to-configuration boot
</code></pre>
<p>This installs the new generation as the next boot target without hot-switching every service in the current session. The old system stays intact until the backups and checks are complete, and the reboot creates a clean boundary: a complete old system before it, and a complete new system after it.</p><h2 id="step-1-capture-a-baseline-before-changing-versions">Step 1: Capture a Baseline Before Changing Versions</h2><p>Before touching the Flake, I recorded:</p><ul><li>the output of <code>nixos-version</code>, plus the kernel, Nix, and systemd versions;</li><li>available space on <code>/</code> and <code>/boot</code>;</li><li>failed systemd units;</li><li>versions and health of PostgreSQL, Redis, Docker, and other stateful services;</li><li>running containers and their resolved image digests;</li><li>the current system generation and at least one older bootable generation.</li></ul><p>This may not feel like progress, but it determines whether two important questions can be answered later: <strong>What actually changed, and did this problem already exist before the upgrade?</strong></p><h3 id="do-not-bump-stateversion">Do not bump <code>stateVersion</code></h3><p>I deliberately kept the existing values:</p><pre><code class="lang-nix">system.stateVersion = &quot;25.05&quot;;
home.stateVersion = &quot;24.05&quot;;
</code></pre>
<p><code>stateVersion</code> is not a display field for the installed release. It declares which historical compatibility behavior the system should preserve, and it can affect service data directories, defaults, and data formats.</p><p>Updating <code>nixpkgs</code> to 26.05 does not mean that <code>system.stateVersion</code> should also become 26.05. Unless you intend to review and perform the associated state migrations, leaving it unchanged is usually the safe choice.</p><h3 id="protect-the-rollback-path-first">Protect the rollback path first</h3><p>My maintenance job previously ran this every week:</p><pre><code class="lang-bash">nix-collect-garbage -d
</code></pre>
<p>The <code>-d</code> flag deletes every non-current generation, which directly conflicts with the goal of retaining the old system after an upgrade. I changed it to:</p><pre><code class="lang-bash">nix-collect-garbage --delete-older-than 30d
</code></pre>
<p>NixOS can create rollback generations, but that does not mean your garbage-collection policy will preserve them. <strong>Rollback capability is itself something that must be maintained.</strong></p><h2 id="step-2-freeze-applications-to-reduce-the-number-of-variables">Step 2: Freeze Applications to Reduce the Number of Variables</h2><p>Before the upgrade, Open WebUI, Dockhand, and Traefik used <code>main</code>, <code>latest</code>, or ordinary version tags. If Docker restarted after those tags had moved in the registry, an operating-system upgrade could silently become three application upgrades as well, potentially including application database migrations.</p><p>I resolved the image used by each running container and pinned that exact digest in the configuration:</p><pre><code class="lang-nix">image = &quot;ghcr.io/example/app@sha256:&lt;digest&gt;&quot;;
</code></pre>
<p>After reboot, the applications would therefore run the same images that had already been working on the old system.</p><p>The lesson is not that tags should never be used. It is that <strong>one change window should deal with one layer of the stack</strong>. Finish and validate the operating-system upgrade first, then upgrade the applications as separate, reviewable changes. Otherwise, the troubleshooting space grows exponentially.</p><h2 id="step-3-migrate-the-configuration-not-just-the-option-names">Step 3: Migrate the Configuration, Not Just the Option Names</h2><p>The direct part of the 25.11-to-26.05 migration was updating the Flake releases:</p><pre><code class="lang-nix">nixpkgs.url = &quot;github:NixOS/nixpkgs/nixos-26.05&quot;;

home-manager = {
  url = &quot;github:nix-community/home-manager/release-26.05&quot;;
  inputs.nixpkgs.follows = &quot;nixpkgs&quot;;
};
</code></pre>
<p>I then followed evaluation errors and deprecation warnings through the configuration. A few cases were particularly instructive.</p><h3 id="move-systemd-resolved-to-structured-settings">Move <code>systemd-resolved</code> to structured settings</h3><p>The old <code>extraConfig</code>, <code>dnssec</code>, and <code>fallbackDns</code> options became structured settings:</p><pre><code class="lang-nix">services.resolved.settings.Resolve = {
  DNSSEC = false;
  DNSOverTLS = &quot;no&quot;;
  MulticastDNS = &quot;no&quot;;
  LLMNR = &quot;yes&quot;;
  Cache = &quot;yes&quot;;
  FallbackDNS = [
    &quot;1.1.1.1&quot;
    &quot;8.8.8.8&quot;
  ];
};
</code></pre>
<p>This looks like a syntax-only migration, but it still requires runtime verification. After reboot, I used <code>resolvectl status</code> to inspect the effective resolver, per-link DNS configuration, and <code>resolv.conf</code> mode.</p><h3 id="select-the-ollama-cpu-package-explicitly">Select the Ollama CPU package explicitly</h3><p>The previous configuration used a Boolean acceleration setting:</p><pre><code class="lang-nix">services.ollama.acceleration = false;
</code></pre>
<p>The new release expresses that choice through the package:</p><pre><code class="lang-nix">services.ollama.package = pkgs.ollama-cpu;
</code></pre>
<p>When migrating configuration, preserve the original intent instead of merely searching for a new option with a similar name. The behavior I needed to retain here was “do not enable GPU acceleration.”</p><h3 id="pin-the-postgresql-server-and-client-to-the-same-major-version">Pin the PostgreSQL server and client to the same major version</h3><p>The PostgreSQL service was already pinned to version 15:</p><pre><code class="lang-nix">services.postgresql.package = pkgs.postgresql_15;
</code></pre>
<p>The system package list, however, still contained the unversioned <code>postgresql</code> package. Under the new nixpkgs release, that resolved to PostgreSQL 17. The full system build then found file collisions between the PostgreSQL 15 and 17 outputs in <code>system-path</code>.</p><p>The fix was to pin the CLI tools as well:</p><pre><code class="lang-nix">environment.systemPackages = with pkgs; [
  postgresql_15
];
</code></pre>
<p>Pinning a service does not necessarily pin every related tool in the system. For databases, compilers, and language runtimes with major-version compatibility boundaries, inspect the closure for accidental second versions.</p><h3 id="remove-flake-outputs-that-are-no-longer-delivered">Remove Flake outputs that are no longer delivered</h3><p>The ThinkPad configuration and Home Manager package both built successfully, but <code>nix flake check --no-build</code> still found an obsolete NUC/Proxmox configuration that no longer evaluated under 26.05.</p><p>It could not stop the ThinkPad from booting, but it proved that the Flake was no longer internally consistent. After confirming that the machine had been retired, I removed its output, input, and host configuration.</p><p>An output with no owner and no continuous validation is not a harmless archive for possible future use. It is hidden maintenance cost waiting for the next upgrade.</p><h2 id="step-4-build-everythingdo-not-treat-eval-as-acceptance">Step 4: Build Everything—Do Not Treat <code>eval</code> as Acceptance</h2><p>I first confirmed that the target system evaluated:</p><pre><code class="lang-bash">nix eval --raw \
  .#nixosConfigurations.nixos.config.system.build.toplevel.drvPath
</code></pre>
<p>Then I performed a real build of the complete system:</p><pre><code class="lang-bash">nix build \
  .#nixosConfigurations.nixos.config.system.build.toplevel \
  --print-build-logs \
  -o result-26.05
</code></pre>
<p>I also built the standalone Home Manager activation package:</p><pre><code class="lang-bash">nix build \
  .#homeConfigurations.cheverjohn.activationPackage \
  --no-link
</code></pre>
<p>An evaluation proves that module merging and expression evaluation succeed. It does not catch every problem, including:</p><ul><li>two packages installing the same file into <code>system-path</code>;</li><li>an unavailable upstream download for proprietary software;</li><li>a Home Manager activation package that fails to build;</li><li>an error while generating a service script;</li><li>a NAR missing from a regional binary cache with no fallback substituter.</li></ul><p>This system required more than a thousand derivations, and its closure grew from roughly 31.6 GiB to 34.0 GiB. A full build costs more than an evaluation, but that cost is best paid while the old system is still healthy—not after rebooting into the new one.</p><h3 id="diff-the-closures-not-just-the-source-code">Diff the closures, not just the source code</h3><p>Once the build completed, I ran:</p><pre><code class="lang-bash">nix store diff-closures /run/current-system ./result-26.05
</code></pre>
<p>A Git diff tells me what I wrote in the configuration. A closure diff tells me what will actually run. It quickly exposed:</p><ul><li>major-version changes in the kernel, systemd, and Docker;</li><li>whether PostgreSQL had moved unexpectedly;</li><li>replacements of runtimes such as Node.js;</li><li>the actual changes to D-Bus, the desktop, and firmware.</li></ul><p>In a declarative system, the final closure—not the source diff—is the deployment artifact.</p><h2 id="step-5-back-up-the-data-before-installing-the-boot-entry">Step 5: Back Up the Data Before Installing the Boot Entry</h2><p>An old generation can roll back system configuration and software. It cannot undo database writes or an application data migration. Before deployment, I therefore backed up:</p><ul><li>PostgreSQL with <code>pg_dumpall</code>;</li><li>Redis after checking persistence and saving an RDB snapshot;</li><li>bind-mounted state for file-based applications such as Open WebUI, with the relevant containers stopped;</li><li>the current configuration, lock file, and important state inventories.</li></ul><p>There is an important boundary here:</p><ul><li><strong>generation rollback</strong> handles a system that does not boot or has broken configuration;</li><li><strong>data backup</strong> handles a new service that has already modified persistent state.</li></ul><p>Preparing only one of them is not a complete recovery plan.</p><p>Only after the backups were complete did I install the prebuilt result into systemd-boot. I also verified that the boot menu still contained a NixOS 25.11 generation.</p><h2 id="step-6-validate-the-rebooted-system-in-layers">Step 6: Validate the Rebooted System in Layers</h2><p>Reaching the desktop only proves that the machine booted. It does not prove that the upgrade is finished. I divided acceptance testing into several layers.</p><h3 id="system-identity">System identity</h3><pre><code class="lang-bash">nixos-version
uname -r
readlink -f /run/current-system
systemctl is-system-running
systemctl --failed
</code></pre>
<p>The first task is to confirm that the running system is the intended store path, rather than an old generation selected by mistake.</p><h3 id="stateful-services">Stateful services</h3><pre><code class="lang-bash">pg_isready -U postgres
redis-cli ping
curl http://127.0.0.1:11434/api/version
</code></pre>
<p>An active process is not enough. I checked versions, existing data, persistence status, and real API responses.</p><h3 id="containers">Containers</h3><p>For each container, I checked:</p><ul><li>health status;</li><li>whether <code>.Config.Image</code> still contained the pinned digest;</li><li>restart count;</li><li>the external HTTP endpoint;</li><li>application logs for migration failures.</li></ul><h3 id="desktop-and-network">Desktop and network</h3><p>This layer covered the GNOME session, Home Manager user services, Wi-Fi, DNS, Bluetooth, and external connectivity. A desktop system cannot be accepted by testing system services alone: user units and desktop portals can also break across releases.</p><h2 id="two-problems-that-appeared-only-after-reboot">Two Problems That Appeared Only After Reboot</h2><h3 id="postgresql-collation-warnings-after-the-glibc-update">PostgreSQL collation warnings after the glibc update</h3><p>The new system uses glibc 2.42, while the existing PostgreSQL cluster recorded collation version 2.40. Connections produced a collation version mismatch warning.</p><p>I did not react to the warning by immediately changing metadata. First, I performed read-only checks of the recorded and actual versions for each database and confirmed that there were no user tables or indexes. With a pre-upgrade backup already available, I refreshed the metadata:</p><pre><code class="lang-sql">ALTER DATABASE postgres REFRESH COLLATION VERSION;
ALTER DATABASE template1 REFRESH COLLATION VERSION;
</code></pre>
<p>If a database contains indexes that depend on locale-aware ordering, merely refreshing the version number is not enough. The affected objects must first be assessed and rebuilt. <code>REFRESH COLLATION VERSION</code> does not rebuild indexes for you.</p><h3 id="execstart-is-not-a-shell-command-line"><code>ExecStart</code> is not a shell command line</h3><p>The system initially looked healthy after reboot. At midnight, however, a timer activated <code>drop-caches.service</code>; the service failed and changed the overall system state to <code>degraded</code>. Its existing configuration was:</p><pre><code class="lang-nix">serviceConfig.ExecStart =
  &quot;${pkgs.coreutils}/bin/sync; echo 3 &gt; /proc/sys/vm/drop_caches&quot;;
</code></pre>
<p>systemd does not pass <code>ExecStart=</code> through a shell by default. The <code>;</code> and <code>&gt;</code> characters do not mean command sequencing and redirection. systemd treated <code>sync;</code> as part of the executable name and returned <code>203/EXEC</code>.</p><p>If shell behavior is genuinely required, use a NixOS service script:</p><pre><code class="lang-nix">systemd.services.drop-caches = {
  serviceConfig.Type = &quot;oneshot&quot;;
  script = &#x27;&#x27;
    ${pkgs.coreutils}/bin/sync
    echo 3 &gt; /proc/sys/vm/drop_caches
  &#x27;&#x27;;
};
</code></pre>
<p>The better solution is probably to remove this timer entirely. Linux already manages the page cache, and regularly forcing it to be dropped usually does little beyond reducing cache hit rates.</p><p>NixOS 26.05 did not introduce this mistake. The broken configuration had existed for some time; the real timer execution simply fell outside my earlier observation windows. The lesson is that <strong>upgrade acceptance must cover delayed timers, not just the system state immediately after boot.</strong></p><p>At the time of writing, this service remains a known follow-up item. I did not turn “the core services work” into the less honest claim that “the system has no problems.”</p><h2 id="do-not-let-every-red-log-line-drive-the-upgrade">Do Not Let Every Red Log Line Drive the Upgrade</h2><p>The new boot also logged duplicate D-Bus service names from dbus-broker, an Fcitx/PAM warning, and several BIOS, ACPI, and PMC messages.</p><p>They were worth recording, but the word <code>error</code> alone was not enough to classify them as blocking. I used five questions instead:</p><ol start="1"><li>Does the message correspond to a failed unit?</li><li>Does an independent functional check fail?</li><li>Does the error repeat or trigger retries?</li><li>Was it introduced by the upgrade, or did it already exist?</li><li>Would fixing it expand the scope of the current change?</li></ol><p>For example, dbus-broker printed many duplicate-name warnings, but GNOME, portals, and the keyring all worked. I recorded the messages as non-blocking noise instead of restructuring the entire desktop package set during the upgrade window.</p><p><strong>Being rigorous does not mean changing configuration whenever a log line turns red. It means supporting every conclusion with state and functional evidence.</strong></p><h2 id="a-reusable-upgrade-checklist">A Reusable Upgrade Checklist</h2><h3 id="before-the-upgrade">Before the upgrade</h3><ul><li><input readonly="" type="checkbox"/> Create a dedicated upgrade branch.</li><li><input readonly="" type="checkbox"/> Record the system, kernel, Nix, systemd, and database versions.</li><li><input readonly="" type="checkbox"/> Check <code>/</code>, <code>/boot</code>, failed units, and current generations.</li><li><input readonly="" type="checkbox"/> Inventory every stateful service and its data directory.</li><li><input readonly="" type="checkbox"/> Record the exact digest of every running container image.</li><li><input readonly="" type="checkbox"/> Keep <code>system.stateVersion</code> and <code>home.stateVersion</code> unchanged.</li><li><input readonly="" type="checkbox"/> Confirm that garbage collection will not remove the rollback generation too soon.</li></ul><h3 id="configuration-and-build">Configuration and build</h3><ul><li><input readonly="" type="checkbox"/> Match the nixpkgs and Home Manager release branches.</li><li><input readonly="" type="checkbox"/> Handle removed, renamed, and semantically changed options.</li><li><input readonly="" type="checkbox"/> Pin database server and CLI tools to the intended major version.</li><li><input readonly="" type="checkbox"/> Freeze container images so applications do not upgrade at the same time.</li><li><input readonly="" type="checkbox"/> Build the complete NixOS system closure.</li><li><input readonly="" type="checkbox"/> Build the standalone Home Manager activation package.</li><li><input readonly="" type="checkbox"/> Run the formatter, <code>git diff --check</code>, and Flake checks.</li><li><input readonly="" type="checkbox"/> Review the effective version changes with <code>nix store diff-closures</code>.</li></ul><h3 id="deployment-and-recovery">Deployment and recovery</h3><ul><li><input readonly="" type="checkbox"/> Back up PostgreSQL, Redis, and file-based application state.</li><li><input readonly="" type="checkbox"/> Verify that backups are readable and record which services must stop before restoration.</li><li><input readonly="" type="checkbox"/> Deploy with <code>boot</code> to avoid a mixed live transition of foundational services.</li><li><input readonly="" type="checkbox"/> Confirm that an old generation remains in the boot menu.</li><li><input readonly="" type="checkbox"/> Define the conditions for system rollback and data restoration.</li></ul><h3 id="after-reboot">After reboot</h3><ul><li><input readonly="" type="checkbox"/> Confirm that <code>/run/current-system</code> points to the target closure.</li><li><input readonly="" type="checkbox"/> Check system and user units.</li><li><input readonly="" type="checkbox"/> Verify database connectivity, versions, data, and persistence.</li><li><input readonly="" type="checkbox"/> Verify container digests, health, restart counts, and HTTP endpoints.</li><li><input readonly="" type="checkbox"/> Test the desktop, Wi-Fi, DNS, Bluetooth, and external connectivity.</li><li><input readonly="" type="checkbox"/> Check the glibc/PostgreSQL collation version.</li><li><input readonly="" type="checkbox"/> Check failed units again after timers have had time to run.</li><li><input readonly="" type="checkbox"/> Keep backups and the old generation until the observation period ends.</li></ul><h2 id="closing-thoughts">Closing Thoughts</h2><p>The NixOS 25.11-to-26.05 upgrade ultimately succeeded. The new kernel, systemd, dbus-broker, GNOME, Docker, PostgreSQL, Redis, Ollama, and Home Manager all work, while the old generation and pre-upgrade backups remain available.</p><p>What gives me confidence is not merely that this particular upgrade did not fail. It is that the process did not depend on luck:</p><ul><li>the build happened while the old system was still usable;</li><li>application upgrades were moved out of this change window;</li><li>both system rollback and data recovery existed before deployment;</li><li>every post-reboot conclusion was supported by a command, a state check, or a real request;</li><li>the one failed unit remained visible instead of being ignored for the sake of a clean conclusion.</li></ul><p>The strength of NixOS is not just that configuration can be written as code. More importantly, it lets us decompose a risky upgrade into a sequence of reviewable, buildable, verifiable, and reversible state transitions. The tooling makes that possible; whether the upgrade is truly safe still depends on how we design the process.</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>nixos</category>
            <category>nix</category>
            <category>linux</category>
            <category>ops</category>
        </item>
        <item>
            <title><![CDATA[TLS Basics and a Real Traefik “Not secure” Incident]]></title>
            <link>https://blog.cheverjohn.me/traefik-certificate-troubleshooting</link>
            <guid>https://blog.cheverjohn.me/traefik-certificate-troubleshooting</guid>
            <pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[TL;DR - Symptom: https://traefik.example.local/dashboard/ shows Not secure in Chrome/Safari; Firefox works fine. - Root cause: macOS Trust validator rejects wildcard cert .example.local for subdomain traefik.example.local (OpenSSL accepts it, Apple Trust does not). - Fix: Issue explicit hostname cer...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="tldr">TL;DR</h2><ul><li><strong>Symptom</strong>: <code>https://traefik.example.local/dashboard/</code> shows <strong>Not secure</strong> in Chrome/Safari; Firefox works fine.</li><li><strong>Root cause</strong>: macOS Trust validator rejects wildcard cert <code>*.example.local</code> for subdomain <code>traefik.example.local</code> (OpenSSL accepts it, Apple Trust does not).</li><li><strong>Fix</strong>: Issue explicit hostname certificate for <code>traefik.example.local</code> instead of betting on wildcard.</li><li><strong>In one sentence</strong>: Validator rules determine everything, not &quot;the cert is broken.&quot;</li></ul><hr/><p>This was not &quot;the certificate is broken.&quot; It was &quot;the certificate is validated by a different engine.&quot;
OpenSSL said OK. Apple Trust said no. Chrome/Safari follow Apple Trust.</p><p><strong>This post has two parts: TLS basics + the real troubleshooting chain.</strong></p><h2 id="tls-basics-only-the-essentials">TLS basics (only the essentials)</h2><p>Let’s keep the core pieces:</p><ul><li><strong>Certificate chain</strong>: leaf → intermediate (optional) → root CA. Browsers trust roots, not leaves.</li><li><strong>SAN (Subject Alternative Name)</strong>: hostname matching is done here; CN is legacy.</li><li><strong>Trust store</strong>: macOS uses System Keychain; Firefox has its own store; Chrome/Safari use system trust.</li><li><strong>Validation</strong>: the same cert can be accepted by one validator and rejected by another.</li></ul><p>One sentence: <strong>“Valid” does not mean “accepted by your current validator.”</strong></p><h2 id="problem-symptoms-and-impact-scope">Problem Symptoms and Impact Scope</h2><p>Accessing <code>https://traefik.example.local/dashboard/#/</code> showed <strong>Not secure</strong> in Chrome/Safari.</p><p><strong>Impact scope:</strong></p><ul><li>❌ Chrome (relies on macOS System Keychain)</li><li>❌ Safari (relies on macOS System Keychain)</li><li>✅ Firefox (uses its own cert store, unaffected)</li></ul><p><strong>Reproduction prerequisites:</strong></p><ul><li>macOS version: Sonoma 14.x+ (other versions may behave differently)</li><li>CA already installed in macOS Keychain and set to &quot;Always Trust&quot;</li><li>Certificate issued by mkcert (version 1.4.4)</li><li>Service running on <code>10.0.0.100</code></li></ul><p><strong>I needed three answers:</strong></p><ol start="1"><li>Does DNS point to the right server?</li><li>Is the cert chain complete and SAN correct?</li><li>Does the system trust engine accept the hostname?</li></ol><h2 id="hypothesis-checklist-in-elimination-order">Hypothesis Checklist (in elimination order)</h2><p>Before troubleshooting, list possible root causes:</p><ul><li><p><strong>H1: CA not properly installed in system trust store</strong></p><ul><li>Validation: Check macOS Keychain, confirm CA cert exists and is &quot;Always Trust&quot;</li><li>Expected: If CA missing, all browsers would fail (but Firefox works, so low probability)</li></ul></li><li><p><strong>H2: Incomplete certificate chain (missing intermediate cert)</strong></p><ul><li>Validation: <code>openssl s_client -showcerts</code> to check chain depth</li><li>Expected: Self-signed CA usually has only two levels (leaf + root), mkcert default is complete</li></ul></li><li><p><strong>H3: SAN does not include target domain</strong></p><ul><li>Validation: <code>openssl x509 -text</code> to check Subject Alternative Name field</li><li>Expected: If SAN missing or mismatched, OpenSSL would also fail (but curl passed, so low probability)</li></ul></li><li><p><strong>H4: Validator rule differences (OpenSSL vs Apple Trust)</strong></p><ul><li>Validation: Use <code>security verify-cert</code> to simulate macOS system validation</li><li>Expected: <strong>This is the most likely root cause</strong> — different validators have different hostname matching rules for wildcards</li></ul></li></ul><p>Validate each hypothesis in order.</p><h2 id="validation-flow-visualization">Validation Flow Visualization</h2><p>This diagram shows the 4-layer validation progression (no execution, conceptual only):</p><pre><code class="lang-bash">#!/usr/bin/env bash
# Validation flow: 4-layer check to identify which layer rejects

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                          │
└─────────────────────────────────────────────────────────┘

Key finding: Layer 3 ✅ but Layer 4 ❌
→ Different validators have different hostname matching rules for wildcards
→ Chrome/Safari rely on Apple Trust (Layer 4)

EOF
</code></pre>
<h2 id="the-troubleshooting-chain-in-order">The troubleshooting chain (in order)</h2><h3 id="1-dns-check">1) DNS check</h3><pre><code class="lang-bash">dig traefik.example.local +short
</code></pre>
<p><strong>Expected output:</strong></p><pre><code class="lang-text">10.0.0.100
</code></pre>
<p><strong>Observation</strong>: DNS points correctly, network layer is fine.</p><p><strong>Conclusion</strong>: Network connectivity ruled out, proceed to certificate layer.</p><h3 id="2-inspect-the-cert-chain-and-san">2) Inspect the cert chain and 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>Expected output (key part):</strong></p><pre><code class="lang-text">X509v3 Subject Alternative Name:
    DNS:*.example.local, DNS:example.local
</code></pre>
<p><strong>Observation</strong>: SAN contains wildcard <code>*.example.local</code>, which should theoretically match <code>traefik.example.local</code> per RFC 6125.</p><p><strong>Conclusion</strong>: Certificate fields are correct, <strong>H3 (SAN missing) eliminated</strong>.</p><h3 id="3-openssl-validation-passes">3) OpenSSL validation (passes)</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>Expected output:</strong></p><pre><code class="lang-text">* SSL certificate verify ok.
* subject: CN=*.example.local
</code></pre>
<p><strong>Observation</strong>: OpenSSL validator considers cert valid, hostname matching passed.</p><p><strong>Conclusion</strong>: <strong>OpenSSL finds no issues</strong>, <strong>H2 (incomplete chain) eliminated</strong>.</p><h3 id="4-macos-system-validation-fails">4) macOS system validation (fails)</h3><pre><code class="lang-bash"># Export cert to temp file first (if not already done)
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 system-level validation
security verify-cert -c /tmp/traefik-leaf.pem -p ssl -n traefik.example.local -L -v
</code></pre>
<p><strong>Expected output (key error):</strong></p><pre><code class="lang-text">...certificate verification failed
...Host name mismatch
</code></pre>
<p><strong>Observation</strong>: Apple Trust explicitly rejects this wildcard match for <code>traefik.example.local</code>.</p><p><strong>Conclusion</strong>: <strong>Apple Trust considers this SAN unable to match the target domain</strong>. This is the pivotal point: OpenSSL OK, Apple Trust not OK. Chrome/Safari rely on Apple Trust. <strong>H4 (validator rule differences) confirmed</strong>.</p><h2 id="root-cause">Root cause</h2><p>Not a missing CA, not a broken chain. The real cause:</p><p><strong>macOS hostname validation did not accept the wildcard cert for <code>traefik.example.local</code>.</strong></p><p>This is not theory; it is the system validator&#x27;s explicit result.</p><h2 id="minimal-reliable-fix">Minimal, reliable fix</h2><p>Stop relying on the wildcard. Issue an explicit hostname certificate.</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>If you want to keep wildcard + apex too:</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>Then replace the Traefik TLS cert and restart the service.</p><h2 id="a-reusable-checklist">A reusable checklist</h2><ul><li>DNS resolution is correct (<code>dig</code>)</li><li>SAN includes the target hostname (<code>openssl x509 -text</code>)</li><li>OpenSSL validation (<code>curl -Iv</code>)</li><li>macOS validation (<code>security verify-cert</code>)</li><li>Browser trust store alignment</li></ul><h2 id="retrospective-and-prevention">Retrospective and Prevention</h2><h3 id="one-sentence-lesson">One-Sentence Lesson</h3><p><strong>The most valuable evidence:</strong>
The <code>Host name mismatch</code> output from <code>security verify-cert</code> — it directly proved the issue was not &quot;CA trust&quot; but &quot;hostname validation rules.&quot;</p><p><strong>If you remember one thing:</strong>
Don&#x27;t judge cert correctness by &quot;it opens on my machine.&quot; Different validators (OpenSSL / Apple Trust / Firefox NSS) can reach different conclusions on the same cert.</p><hr/><h3 id="prevention-checklist-for-next-time-you-issue-certs">Prevention Checklist (for next time you issue certs)</h3><p><strong>For internal self-signed CA (mkcert/self-built CA):</strong></p><ul><li><input readonly="" type="checkbox"/> Prefer explicit hostnames over betting on wildcard cross-platform compatibility</li><li><input readonly="" type="checkbox"/> If you must use wildcards, pre-validate with <code>security verify-cert</code> on macOS</li><li><input readonly="" type="checkbox"/> Keep issuance command records (with complete SAN list) for easy re-issuance</li></ul><p><strong>For public certs (Let&#x27;s Encrypt, etc.):</strong></p><ul><li><input readonly="" type="checkbox"/> Request both <code>example.com</code> and <code>*.example.com</code> if you need to cover apex and subdomains</li><li><input readonly="" type="checkbox"/> Validate on at least 3 platforms: Linux (OpenSSL) / macOS (Apple Trust) / Windows (Schannel)</li></ul><p><strong>Validation template (reusable script):</strong></p><pre><code class="lang-bash">#!/usr/bin/env bash
# Quick validation of cert behavior across validators

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

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

echo &quot;==&gt; macOS validation (must run on macOS)&quot;
# Export cert first
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2&gt;/dev/null | \
  openssl x509 &gt; /tmp/cert-to-verify.pem
# System validation
security verify-cert -c /tmp/cert-to-verify.pem -p ssl -n ${DOMAIN} -L -v

echo &quot;==&gt; Check 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="final-conclusion">Final Conclusion</h3><p>&quot;I already installed the CA&quot; is not the end of the story.
What matters is <strong>which validator you are actually using</strong>.</p><p>This time the fix is simple and clean:
<strong>issue an explicit cert for the host and the warning disappears.</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[Self-Hosting Deployment Notes: Traefik + Docker Compose + Domain-Only Access + Shared Middleware]]></title>
            <link>https://blog.cheverjohn.me/self-hosting-deployment-notes</link>
            <guid>https://blog.cheverjohn.me/self-hosting-deployment-notes</guid>
            <pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This post summarizes what worked (and what hurt) when I deployed multiple self-hosted services in this repo (e.g. dockhand, vaultwarden, Checkmate, Miniflux). It focuses on repeatable defaults, pitfalls, and a practical debugging workflow. Goals and non-negotiables - Domain-only access: Do not expos...]]></description>
            <content:encoded><![CDATA[<article><div><p>This post summarizes what worked (and what hurt) when I deployed multiple self-hosted services in this repo (e.g. <code>dockhand</code>, <code>vaultwarden</code>, <code>Checkmate</code>, <code>Miniflux</code>). It focuses on repeatable defaults, pitfalls, and a practical debugging workflow.</p><h2 id="goals-and-non-negotiables">Goals and non-negotiables</h2><ul><li><strong>Domain-only access</strong>: Do not expose host ports for business services. Avoid <code>IP:port</code> bypassing auth or leaking admin panels.</li><li><strong>Single entrypoint</strong>: Only open <code>80/443</code> externally. Let <strong>Traefik</strong> handle reverse proxying, TLS, and security headers.</li><li><strong>Shared middleware</strong>: Run shared MongoDB / Redis / PostgreSQL once and reuse them across apps to reduce waste.</li><li><strong>Never break userspace</strong>: Any change must not break existing online services (routing/port/network/auth conflicts).</li></ul><h2 id="repo-layout-and-my-deployment-defaults">Repo layout and my deployment defaults</h2><ul><li><strong>One app per directory</strong>: e.g. <code>dockhand/</code>, <code>vaultwarden/</code>, <code>Checkmate/</code>, <code>v2/</code> (Miniflux).</li><li><strong>Each app has a runnable <code>docker-compose.yml</code></strong>: avoid scattered upstream examples under <code>contrib/</code>.</li><li><strong>Shared middleware is a separate project</strong>: <code>middlewares/</code> runs <code>postgres</code> / <code>mongodb</code> / <code>redis</code>.
<ul><li>Shared middleware joins internal networks only and exposes <strong>no public ports</strong>.</li><li>Apps join shared networks via <code>external</code> networks.</li></ul></li><li><strong>One Traefik network</strong>: all services that Traefik should reach join an external network like <code>traefik_proxy</code>.</li><li><strong>Prefer Docker labels</strong> (docker provider): use file provider only for a few non-container targets or special cases.</li></ul><h2 id="standard-deployment-playbook-the-core-tricks">Standard deployment playbook (the core tricks)</h2><h3 id="1-do-not-publish-host-ports">1) Do not publish host ports</h3><p>For public-facing services:</p><ul><li>Use <code>expose:</code> (container-network visible only) instead of <code>ports:</code> (host visible).</li><li>Route traffic via Traefik using labels.</li></ul><p>This immediately reduces:</p><ul><li><strong>Port conflicts</strong> (e.g. host <code>3000</code> already in use).</li><li><strong><code>IP:port</code> bypass</strong> risks.</li></ul><h3 id="2-traefik-labels-domain-routing--tls--security-headers">2) Traefik labels (domain routing + TLS + security headers)</h3><p>Typical label set (example):</p><ul><li><code>traefik.enable=true</code></li><li><code>traefik.http.routers.&lt;name&gt;.rule=Host(`example.com`)</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-multi-network-containers-must-pin-the-traefik-network">3) Multi-network containers must pin the Traefik network</h3><p>When a container joins both:</p><ul><li><code>traefik_proxy</code> (for Traefik)</li><li><code>infra_backend</code> (for databases/caches)</li></ul><p>Traefik may pick the wrong container IP (from a network Traefik cannot reach), resulting in <strong>504 Gateway Timeout</strong>.</p><p>Fix: add this label to the service:</p><ul><li><code>traefik.docker.network=traefik_proxy</code></li></ul><p>This was the typical cause for Checkmate: adding it removed the 504 immediately.</p><h2 id="shared-middleware-middlewares">Shared middleware (<code>middlewares/</code>)</h2><h3 id="design-principles">Design principles</h3><ul><li><strong>High cohesion, low coupling</strong>: middleware provides network services only.</li><li><strong>Least privilege</strong>: no public ports by default; internal networks only.</li><li><strong>Centralized persistence</strong>: volumes managed in <code>middlewares/</code> for easier backup/migration.</li></ul><h3 id="practical-notes">Practical notes</h3><ul><li>Use a stable shared network name (e.g. <code>infra_backend</code>), and let apps join with <code>external: true</code>.</li><li>Use service names in connection strings (e.g. <code>postgres</code> / <code>mongodb</code>).</li><li>Prefer separate DB/schema/user per app to avoid data pollution.</li></ul><h2 id="app-specific-pitfalls-i-hit-this-time">App-specific pitfalls I hit this time</h2><h3 id="dockhand-high-risk-admin-surface">Dockhand (high-risk admin surface)</h3><ul><li><strong>Do not publish host ports</strong>, domain + Traefik only.</li><li><strong>Always add auth</strong>: protect it with Traefik <code>basicAuth</code>.</li><li>Keep it simple; complexity is how incidents are born.</li></ul><h3 id="vaultwarden-bitwarden-compatible">Vaultwarden (Bitwarden-compatible)</h3><p>This is where it is easy to “be clever” and break clients:</p><ul><li><strong>Do not put BasicAuth on the entire site</strong>: official clients and browser extensions will fail.</li><li>Correct approach: protect <code>/admin</code> only.
<ul><li>Client paths like <code>/</code>, <code>/api</code>, <code>/identity</code> must not be blocked by BasicAuth.</li></ul></li><li>If “the UI loads but login fails”, check:
<ul><li>whether Cloudflare is challenging API requests (see below)</li><li>whether <code>/api</code> is treated as ServerConfig (may require rewriting to <code>/api/config</code> in Traefik)</li></ul></li></ul><h3 id="checkmate-multi-network--mongodb">Checkmate (multi-network + MongoDB)</h3><ul><li>Sharing MongoDB + multiple networks often triggers the Traefik wrong-network issue:
<ul><li>symptoms: 504</li><li>fix: <code>traefik.docker.network=traefik_proxy</code></li></ul></li></ul><h3 id="miniflux-rss-reader">Miniflux (rss reader)</h3><ul><li>Miniflux requires <strong>PostgreSQL</strong>. No MongoDB/Redis is needed (unless you add RSSHub/Browserless, etc.).</li><li><code>HEAD /</code> returning <code>405</code> can be normal as long as <code>GET</code> works; do not misdiagnose it as downtime.</li></ul><h2 id="cloudflare-pitfall-clientsextensions-cannot-log-in">Cloudflare pitfall: clients/extensions cannot log in</h2><p>I hit the same issue twice:</p><ul><li>Vaultwarden browser extension: Cloudflare challenge → 403</li><li>Reeder (Google Reader API): Cloudflare challenge → 403</li></ul><p>Common signals:</p><ul><li>response headers contain <code>server: cloudflare</code> + <code>cf-mitigated: challenge</code></li><li>requests never reach Traefik/app logs</li></ul><p>Mitigation options (recommended order):</p><ul><li><strong>A: set the subdomain to DNS-only (grey cloud)</strong>: the most robust, no client-side hacks needed.</li><li><strong>B: keep proxy (orange cloud), but skip challenge for this host/path</strong>:
<ul><li>at minimum allow <code>/api</code>, <code>/identity</code>, <code>/reader/api/0</code>, <code>/accounts/ClientLogin</code>, etc.</li></ul></li></ul><p>Key takeaway:</p><ul><li><strong>“It works in a browser” does not mean “it works in a client”</strong>. Browsers can pass challenges; clients usually cannot.</li></ul><h2 id="debug-playbook-fast-to-slow">Debug playbook (fast to slow)</h2><h3 id="1-decide-whether-the-request-reaches-your-server">1) Decide whether the request reaches your server</h3><ul><li>Cloudflare 403 challenge: request never reaches your server. Fix Cloudflare/WAF first.</li><li>Traefik 404/504: request reaches Traefik, but routing/network/backend is wrong.</li><li>Backend 4xx/5xx: request reaches the app. Inspect app logs/config.</li></ul><h3 id="2-traefik-side-debugging">2) Traefik-side debugging</h3><ul><li>Check access logs: which router/service handled the request and what status code it returned.</li><li>Multi-network containers: suspect missing <code>traefik.docker.network</code> first.</li><li>Same host defined twice: suspect file-provider vs docker-provider conflicts (delete legacy file routes).</li></ul><h3 id="3-app-side-debugging">3) App-side debugging</h3><ul><li><code>docker logs &lt;container&gt;</code>: verify startup, listening port, DB connectivity.</li><li>Verify you did not accidentally use <code>ports:</code> and expose/conflict host ports.</li></ul><h2 id="final-security-checklist-before-going-public">Final security checklist (before going public)</h2><ul><li><strong>No business ports published on the host</strong> (only keep <code>80/443</code>)</li><li><strong>Admin surfaces must be authenticated</strong> (dockhand, vaultwarden <code>/admin</code>, etc.)</li><li><strong>Databases/caches are never exposed to the public internet</strong></li><li><strong>No Traefik routing conflicts</strong> (avoid defining the same host in both file-provider and docker-provider)</li><li><strong>Cloudflare must not challenge API paths</strong> (clients/extensions/third-party integrations will break)</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[Daily Debugging: Minisforum N100 Random Disk Drop / System Freeze (DNS, 1+ Day Uptime)]]></title>
            <link>https://blog.cheverjohn.me/minisforum-n100-dns-random-freeze</link>
            <guid>https://blog.cheverjohn.me/minisforum-n100-dns-random-freeze</guid>
            <pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[This is a troubleshooting log template. Facts first, evidence second, conclusions last. TL;DR - Symptoms: The DNS service becomes unresponsive; the whole machine stutters/freezes; sometimes it looks like storage I/O issues or a “disk drop”. - Pattern: Random, usually after 1+ day of continuous runni...]]></description>
            <content:encoded><![CDATA[<article><div><blockquote><p>This is a troubleshooting log template. Facts first, evidence second, conclusions last.</p></blockquote>
<h2 id="tldr">TL;DR</h2><ul><li><strong>Symptoms</strong>: The DNS service becomes unresponsive; the whole machine stutters/freezes; sometimes it looks like storage I/O issues or a “disk drop”.</li><li><strong>Pattern</strong>: Random, usually after <strong>1+ day</strong> of continuous running.</li><li><strong>Recovery</strong>: <strong>Power off and let it cool down</strong> for a while, then it works again.</li><li><strong>Current conclusion (probabilistic)</strong>: ____ (e.g., thermals / power / storage-controller reset).</li></ul><h2 id="impact">Impact</h2><ul><li><strong>Direct impact</strong>: ____ (e.g., LAN-wide DNS timeouts).</li><li><strong>Indirect impact</strong>: ____ (services that depend on name resolution).</li><li><strong>Time window</strong>: ____ (start time / duration / auto-recovery?).</li></ul><h2 id="environment-reproducible">Environment (reproducible)</h2><h3 id="hardware">Hardware</h3><ul><li><strong>CPU</strong>: Intel® N100 (4C/4T, 6MB cache, up to 3.4GHz)</li><li><strong>GPU</strong>: Intel® UHD Graphics</li><li><strong>RAM</strong>: 8GB LPDDR5 (single channel, onboard, 4800MHz)</li><li><strong>Storage</strong>: UFS 2.1 256G</li><li><strong>Wireless</strong>: Intel AX200/201 (Wi‑Fi 6 / Bluetooth 5.2)</li><li><strong>Ethernet</strong>: 2.5G RJ45 ×1 (PoE IEEE 802.3at)</li><li><strong>Ports</strong>: USB 3.2 Gen2 Type‑A ×2; USB 3.2 Gen2 Type‑C ×1 (Alt DP/PD); HDMI ×1</li><li><strong>Power</strong>: 65W USB‑C Power Delivery adapter</li></ul><h3 id="software">Software</h3><ul><li><strong>Actual OS</strong>: Linux (current kernel: <code>6.8.12-5-pve</code>; factory OS: Windows 11 Home)</li><li><strong>Kernel (if Linux)</strong>: <code>6.8.12-5-pve</code></li><li><strong>Bootloader (GRUB)</strong>: <code>grub2 2.06-13+pmx2</code></li><li><strong>DNS stack</strong>: ____ (dnsmasq / unbound / AdGuard Home / Pi‑hole / other)</li><li><strong>Deployment</strong>: ____ (systemd / Docker / other)</li><li><strong>Other services</strong>: ____</li><li><strong>Logging policy</strong>: ____ (write frequency / rotation / same disk?).</li></ul><h2 id="problem-statement-facts-only">Problem statement (facts only)</h2><ul><li><strong>First seen</strong>: ____</li><li><strong>Frequency</strong>: about every ____ hours/days (random).</li><li><strong>Observable symptoms during failure</strong>:
<ul><li><strong>DNS</strong>: ____ (timeouts / refusal / high latency)</li><li><strong>System</strong>: ____ (SSH/RDP reachable? CPU/memory spike?)</li><li><strong>Storage</strong>: ____ (mount disappears / read-only remount / I/O error / device reset)</li><li><strong>Network</strong>: ____ (packet loss? NIC reset?)</li></ul></li><li><strong>Recovery</strong>: ____ (power off for ____ minutes; does reboot also work: ____)</li><li><strong>Load correlation</strong>: ____ (QPS / logging / other tasks).</li></ul><h2 id="immediate-mitigation-timeline">Immediate mitigation (timeline)</h2><ul><li><strong>T+0</strong>: ____ (e.g., switch upstream DNS)</li><li><strong>T+5m</strong>: ____ (stop DNS or high-IO workloads)</li><li><strong>T+10m</strong>: ____ (collect logs and metrics snapshots)</li><li><strong>Result</strong>: ____</li></ul><h2 id="evidence-model-align-everything-by-timestamp">Evidence model (align everything by timestamp)</h2><ul><li><strong>One timeline</strong>: failure start time, temperature peak, I/O errors, service timeouts.</li><li><strong>One core question</strong>: is DNS dead, or is the storage/system dead and DNS is just the first visible symptom?</li></ul><h2 id="hypotheses-prioritized">Hypotheses (prioritized)</h2><ul><li><strong>H1: Thermals / overheating → throttling or protection → instability</strong>
<ul><li>Evidence: ____</li></ul></li><li><strong>H2: Power instability (PD/PoE/adapter) → controller reset</strong>
<ul><li>Evidence: ____</li></ul></li><li><strong>H3: Storage media/controller issue → I/O stall or device drop</strong>
<ul><li>Evidence: ____</li></ul></li><li><strong>H4: Software resource exhaustion (FD/memory/log amplification/IO saturation)</strong>
<ul><li>Evidence: ____</li></ul></li><li><strong>H5: Kernel/driver bug (power management, device PM policy)</strong>
<ul><li>Evidence: ____</li></ul></li></ul><h2 id="data-to-capture-during-failure">Data to capture (during failure)</h2><ul><li><strong>Temps/fans</strong>: ____ (<code>sensors</code>, <code>/sys/class/thermal/</code>, Windows tools)</li><li><strong>System logs</strong>: ____ (<code>journalctl</code>, Windows Event Viewer)</li><li><strong>Storage logs</strong>: ____ (I/O error, timeout, reset, read-only remount)</li><li><strong>Resource metrics</strong>: ____ (CPU/load/iowait/memory/disk throughput)</li><li><strong>DNS metrics</strong>: ____ (QPS, cache hit ratio, upstream latency, error rate)</li><li><strong>Client checks</strong>: ____ (<code>dig/nslookup</code> outputs)</li></ul><h2 id="key-evidence-35-snippets">Key evidence (3–5 snippets)</h2><pre><code class="lang-text">[time] ____
</code></pre>
<pre><code class="lang-text">[time] ____
</code></pre>
<pre><code class="lang-text">[time] ____
</code></pre>
<h2 id="investigation-attempt--observation--conclusion">Investigation (Attempt → Observation → Conclusion)</h2><h3 id="attempt-a-">Attempt A: ____</h3><ul><li>Change: ____</li><li>Observation: ____</li><li>Conclusion: ____</li></ul><h3 id="attempt-b-">Attempt B: ____</h3><ul><li>Change: ____</li><li>Observation: ____</li><li>Conclusion: ____</li></ul><h3 id="attempt-c-">Attempt C: ____</h3><ul><li>Change: ____</li><li>Observation: ____</li><li>Conclusion: ____</li></ul><h2 id="final-conclusion-probability-not-absolute-claims">Final conclusion (probability, not absolute claims)</h2><ul><li><strong>Most likely root cause</strong>: ____</li><li><strong>Secondary factors</strong>: ____</li><li><strong>Ruled out</strong>: ____ (with evidence)</li></ul><h2 id="fix-plan-short-term-vs-long-term">Fix plan (short-term vs long-term)</h2><h3 id="short-term-mitigation">Short-term mitigation</h3><ul><li>____ (e.g., improve cooling)</li><li>____ (e.g., cap power / disable aggressive power saving)</li><li>____ (e.g., reduce disk writes / configure log rotation)</li></ul><h3 id="long-term-fix">Long-term fix</h3><ul><li>____ (e.g., replace power adapter / replace storage / BIOS update)</li><li>____ (e.g., airflow redesign / thermal pads)</li><li>____ (e.g., kernel/OS change)</li></ul><h3 id="validation">Validation</h3><ul><li>Uptime: ____ days without recurrence</li><li>Temperature: CPU ≤ ____°C; storage ≤ ____°C</li><li>Logs: no I/O errors / resets / read-only remount</li></ul><h2 id="retrospective">Retrospective</h2><ul><li>Biggest learning: ____</li><li>What I will do earlier next time: ____ (monitoring, alerting, auto log capture)</li><li>Open questions: ____</li></ul><h2 id="attempt-plan-monitor-1921682218-minisforum-via-a-monitoring-lxc-1921682219-on-pve-1921682212">Attempt plan: Monitor 192.168.22.18 (Minisforum) via a monitoring LXC (192.168.22.19) on PVE 192.168.22.12</h2><blockquote><p>Goal: turn “it feels like overheating / disk drop / DNS died” into an evidence-aligned timeline: <strong>temps + kernel/storage errors + real DNS query success/latency + host reachability</strong>.</p><p>Note: <strong>never put passwords into configs or the repo</strong>. Any credentials should live in root-only local files on the monitoring host.</p></blockquote>
<h3 id="extra-pitfall-port-22-is-reachable-but-ssh-still-hangs--drops-more-common-across-subnets">Extra pitfall: port <code>22</code> is reachable, but <code>ssh</code> still hangs / drops (more common across subnets)</h3><p>This one is misleading: <code>nc</code> says <code>22/tcp open</code>, so people assume “network is fine”. But <code>ssh</code> can still stall during handshake and eventually time out (or die with <code>Broken pipe</code>).</p><h4 id="topology-what-happened-in-this-incident">Topology (what happened in this incident)</h4><ul><li><strong>Client subnet</strong>: <code>192.168.11.0/24</code></li><li><strong>Target host</strong>: <code>192.168.22.12</code> (PVE)</li><li><strong>Same-subnet control point</strong>: <code>192.168.22.18</code> (SSH from here to <code>.12</code> is stable)</li><li><strong>Correlated signal</strong>: DNS server (e.g. <code>192.168.22.53:53</code>) experienced timeouts — reverse lookups can amplify SSH handshake latency.</li></ul><h4 id="symptoms-typical">Symptoms (typical)</h4><ul><li><code>nc -vz -G 2 192.168.22.12 22</code> reports <strong>succeeded</strong> (TCP handshake is OK)</li><li><code>ssh root@192.168.22.12</code> hangs and later shows:
<ul><li><code>ssh_dispatch_run_fatal: Connection to 192.168.22.12 port 22: Operation timed out</code></li><li>or <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="quick-triage-split-port-is-open-into-smaller-truths">Quick triage (split “port is open” into smaller truths)</h4><ol start="1"><li><strong>Check whether the SSH banner is returned immediately</strong> (protocol only, no auth):</li></ol><pre><code class="lang-bash">nc -v 192.168.22.12 22
</code></pre>
<p>You should quickly see <code>SSH-2.0-OpenSSH_...</code>. If the TCP connect succeeds but the banner is <strong>slow/missing</strong>, the issue is likely in <strong>sshd/system path</strong> (DNS reverse lookup stalls, high load, policy/ACL causing unstable data path), not “port blocked”.</p><ol start="2"><li><strong>Capture where SSH stalls</strong> (useful evidence):</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="minimal-fix-eliminate-dnsgssapi-special-cases-first">Minimal fix (eliminate DNS/GSSAPI special cases first)</h4><p>If you can SSH from a same-subnet host (e.g. <code>.22.18</code>) into <code>.22.12</code>, prefer the <strong>server-side</strong> fix:</p><ol start="1"><li>On <code>.22.12</code>, edit <code>/etc/ssh/sshd_config</code> and make sure you have:</li></ol><ul><li><code>UseDNS no</code></li><li><code>GSSAPIAuthentication no</code></li></ul><ol start="2"><li>Validate and restart:</li></ol><pre><code class="lang-bash">sshd -t &amp;&amp; systemctl restart ssh
systemctl status ssh --no-pager
</code></pre>
<blockquote><p>Practical rationale: when DNS/reverse lookup is flaky, <code>UseDNS yes</code> can block parts of the handshake/audit path. Disabling it removes an entire class of “it depends on DNS today” failures.</p></blockquote>
<h4 id="if-it-is-still-flaky-continue-with-the-cross-subnet-triad">If it is still flaky: continue with the “cross-subnet triad”</h4><ul><li><strong>Return path routing</strong> (the target must know how to route back to <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>Firewall/ACL/state tracking</strong>: allow not only <code>22/tcp</code> inbound but also the correct return direction and conntrack behavior (especially with gateways/NAT/policy routing).</li><li><strong>MTU/fragmentation</strong>: tunnels/PPPoE/VLAN paths can yield “SYN works but data is unstable”; validate with DF-sized pings on Linux and temporarily lower MSS/MTU to confirm.</li></ul><h3 id="architecture-minimal-but-sufficient">Architecture (minimal, but sufficient)</h3><ul><li><strong>192.168.22.19 (monitoring LXC on PVE 192.168.22.12)</strong>: Prometheus + Grafana + blackbox_exporter (optional Pushgateway)</li><li><strong>192.168.22.18 (target host)</strong>: node<em>exporter (host metrics) + (optional) smartctl</em>exporter + custom textfile metrics (kernel/I/O error counter)</li></ul><p>Data model:</p><ul><li><strong>Time series (Prometheus)</strong>: scrape every 15s, keep at least 48h around failures.</li><li><strong>Active probing (blackbox_exporter)</strong>: probe <code>.18/.54</code> from <code>.19</code> to separate “host down” from “service down”.</li><li><strong>Alerting</strong>: pin the incident timestamp immediately.</li></ul><hr/><h3 id="attempt-d-bring-up-the-monitoring-stack-prometheus--grafana--blackboxexporter">Attempt D: Bring up the monitoring stack (Prometheus + Grafana + blackbox_exporter)</h3><h4 id="d0-pre-checks-network--ports">D0. Pre-checks (network &amp; ports)</h4><p>From <code>192.168.22.19</code>, verify reachability to <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>Ports used (defaults):</p><ul><li><code>.18:9100</code> node_exporter</li><li><code>.18:9633</code> smartctl_exporter (optional)</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 (optional)</li></ul><h4 id="d1-deploy-nodeexporter-on-1921682218-systemd-stable">D1. Deploy node_exporter on 192.168.22.18 (systemd, stable)</h4><p>Run on <code>.18</code>:</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>Verify (from <code>.12</code> or any LAN host):</p><pre><code class="lang-bash">curl -fsS http://192.168.22.18:9100/metrics | head
</code></pre>
<h4 id="d2-recommended-add-a-kernelstorage-error-counter-as-a-custom-metric">D2. (Recommended) Add a kernel/storage error counter as a custom metric</h4><p>The keywords that matter:
<code>I/O error|timeout|reset|remount read-only|EXT4-fs error|BTRFS error|blk_update_request</code></p><p>Use node_exporter <strong>textfile collector</strong> and a timer:</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=&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
</code></pre>
<p>Verify:</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-optional-smartctlexporter-for-disk-health">D3. (Optional) smartctl_exporter for disk health</h4><p>If the storage device does not expose SMART/health, this may not work. Still worth trying.</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>
<h4 id="d4-deploy-prometheus--grafana--blackboxexporter-on-1921682212-pve-via-lxc-recommended">D4. Deploy Prometheus + Grafana + blackbox_exporter on 192.168.22.12 (PVE) via LXC (recommended)</h4><p>Since <code>192.168.22.12</code> is also a PVE host, avoid piling Docker onto the host OS. The simplest operationally is: <strong>one Debian LXC container running the monitoring stack with systemd</strong>.</p><h5 id="d41-create-a-debian-lxc-container-on-pve-12">D4.1 Create a Debian LXC container on PVE (.12)</h5><p>You can do it in the PVE Web UI, or via CLI like this (replace <code>&lt;CTID&gt;</code>, <code>&lt;GW&gt;</code>; LXC IP is pinned to <code>192.168.22.19</code>):</p><pre><code class="lang-bash"># Run on the PVE host 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

pct set &lt;CTID&gt; -features keyctl=1,nesting=1
pct start &lt;CTID&gt;
</code></pre>
<h5 id="d42-install-prometheus-in-the-container-systemd">D4.2 Install Prometheus in the container (systemd)</h5><p>Enter the container:</p><pre><code class="lang-bash">pct exec &lt;CTID&gt; -- bash
</code></pre>
<p>Inside the container:</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>Create <code>/etc/prometheus/prometheus.yml</code> (scrape <code>.18</code> + active probes; DNS is the AdGuardDNS LXC at <code>.54</code>):</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>Create the systemd service:</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
</code></pre>
<h5 id="d43-install-blackboxexporter-in-the-container-systemd">D4.3 Install blackbox_exporter in the container (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
</code></pre>
<h5 id="d44-install-grafana-in-the-container-systemd">D4.4 Install Grafana in the container (systemd)</h5><p>Do not commit any Grafana admin password. Install it, then set the password manually.</p><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
</code></pre>
<p>Reset admin password (run in the container):</p><pre><code class="lang-bash">grafana-cli admin reset-admin-password
</code></pre>
<hr/><h3 id="attempt-e-deploy-adguarddns-on-1921682218-via-lxc-fixed-ip-1921682254--real-dns-query-probing">Attempt E: Deploy AdGuardDNS on 192.168.22.18 via LXC (fixed IP: 192.168.22.54) + real DNS query probing</h3><p>Goal: isolate DNS service from the host and <strong>pin the DNS IP to <code>192.168.22.54</code></strong> so monitoring targets never drift.</p><h4 id="e0-create-the-adguarddns-lxc-container-on-pve-18-with-ip-1921682254">E0. Create the AdGuardDNS LXC container on PVE (.18) with IP 192.168.22.54</h4><p>Example (replace <code>&lt;CTID&gt;</code>, <code>&lt;GW&gt;</code>):</p><pre><code class="lang-bash"># Run on the PVE host 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-install-adguard-home-inside-the-container-as-adguarddns">E1. Install AdGuard Home inside the container (as AdGuardDNS)</h4><p>Enter the container:</p><pre><code class="lang-bash">pct exec &lt;CTID&gt; -- bash
</code></pre>
<p>Inside the container:</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>First-time setup:</p><ul><li>Admin UI: <code>http://192.168.22.54:3000</code></li><li>DNS: <code>192.168.22.54:53</code> (UDP/TCP)</li></ul><h4 id="e1-real-dns-query-probe-using-dig--pushgateway-credential-free">E1. Real DNS query probe using <code>dig</code> + Pushgateway (credential-free)</h4><p>Use <code>.12</code> to run <code>dig</code> periodically and push two metrics: success and latency.</p><ol start="1"><li>Optionally install Pushgateway on the monitoring side (the <code>.12</code> monitoring LXC) via 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>Add a Prometheus scrape job (use <code>127.0.0.1</code> since Pushgateway runs on the same container):</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>Create a probe script (DNS IP is pinned to <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;

if echo &quot;${OUT}&quot; | grep -qE &#x27;^;; ANSWER SECTION:&#x27;; then
  SUCCESS=1
else
  SUCCESS=0
fi

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>Schedule it (during investigation, 15s is useful):</li></ol><pre><code class="lang-bash">crontab -e
*/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>
<h4 id="e2-optional-adguard-home-internal-stats-requires-admin-api-creds-never-commit-them">E2. (Optional) AdGuard Home internal stats (requires admin API creds; never commit them)</h4><p>If you need AdGuard-specific stats (blocked/allowed/qps/upstream latency), run an exporter on <code>.12</code> that talks to AdGuard’s admin API.
Store credentials in a root-only local file (e.g. <code>/opt/monitoring/secrets/adguard.env</code>, <code>chmod 600</code>) and keep it out of Git.</p></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[Daily Debugging: PVE Cross-VLAN Ping Fails (Wrong Default Gateway + OpenWrt LAN NAT)]]></title>
            <link>https://blog.cheverjohn.me/pve-openwrt-icmp-debug</link>
            <guid>https://blog.cheverjohn.me/pve-openwrt-icmp-debug</guid>
            <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[This is a practical troubleshooting post. Facts first, evidence second, conclusions last. TL;DR - Symptom: Cross-VLAN ping times out. The target host shows echo reply sent, yet the client never receives it. - Root cause 1 (decisive): PVE intentionally kept OpenWrt (192.168.22.13) as the default gate...]]></description>
            <content:encoded><![CDATA[<article><div><blockquote><p>This is a practical troubleshooting post. Facts first, evidence second, conclusions last.</p></blockquote>
<h2 id="tldr">TL;DR</h2><ul><li><strong>Symptom</strong>: Cross-VLAN <code>ping</code> times out. The target host shows <code>echo reply</code> sent, yet the client never receives it.</li><li><strong>Root cause #1 (decisive)</strong>: PVE <strong>intentionally</strong> kept OpenWrt (<code>192.168.22.13</code>) as the default gateway for QoS/traffic handling, but had <strong>no explicit routes for other internal VLAN subnets</strong>, so return traffic to internal networks followed the default route into OpenWrt and never traversed the primary L3 gateway (classic asymmetric path).</li><li><strong>Root cause #2 (amplifier)</strong>: OpenWrt had <code>masq=1</code> on the <strong>LAN zone</strong>, applying <strong>SNAT</strong> to internal east-west traffic and making paths/sessions unpredictable.</li><li><strong>Fix</strong>:
<ul><li><strong>Keep</strong> PVE default gateway = OpenWrt (<code>192.168.22.13</code>) for QoS, but add explicit routes for internal subnets (e.g., <code>192.168.11.0/24</code>, <code>192.168.183.0/24</code>) via the primary gateway (<code>192.168.22.1</code>)</li><li>Disable masquerade on OpenWrt <code>lan</code> zone</li></ul></li><li><strong>Prevention</strong>: Route internally, NAT only on real WAN egress; if a side gateway must be the default route (QoS/proxy), explicitly split internal routing vs Internet default; use <code>tcpdump</code> + <code>ip route get</code> as a standard SOP.</li></ul><hr/><h2 id="background-topology-and-roles">Background (Topology and Roles)</h2><p>Multi-VLAN home network:</p><ul><li><strong>Cloud Gateway Fiber (primary L3 gateway)</strong>
<ul><li>Inter-VLAN routing</li><li>Example subnets:
<ul><li>VLAN11: <code>192.168.11.0/24</code> (GW <code>192.168.11.1</code>)</li><li>VLAN22 (PVE/Lab): <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></ul></li><li><strong>OpenWrt (secondary / side gateway)</strong>
<ul><li><code>192.168.22.13/24</code> on <code>br-lan</code></li><li><code>fw3 + iptables</code>, plus proxy tooling (OpenClash/Passwall)</li></ul></li><li><strong>PVE node</strong>: <code>192.168.22.12/24</code> on <code>vmbr0</code></li><li><strong>Test hosts</strong>:
<ul><li><code>192.168.183.235</code> (VLAN183)</li><li><code>192.168.11.29</code> (VLAN11)</li></ul></li></ul><p>The intended design is simple:</p><ul><li>Inter-VLAN traffic should be routed by the primary gateway.</li><li>NAT should only happen on a true WAN egress, not on internal VLAN-to-VLAN paths.</li></ul><hr/><h2 id="ascii-topology-printable-via-shell">ASCII topology (printable via shell)</h2><p>This script does not change any config. It prints the topology and the two key paths (bad vs fixed) so readers can visualize the data flow:</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="incidents-and-impact">Incidents and Impact</h2><h3 id="incident-1-vlan183-could-not-ping-pve">Incident #1: VLAN183 could not ping PVE</h3><ul><li>Client <code>192.168.183.235</code> → <code>ping 192.168.22.12</code> timed out</li><li>On PVE, captures showed:
<ul><li><code>ICMP echo request</code> arriving</li><li><code>ICMP echo reply</code> being generated</li></ul></li><li>On the primary gateway (Fiber), captures on <code>br22</code>/<code>br0</code> showed requests only, no replies</li></ul><p><strong>Meaning</strong>: the reply never traversed the primary gateway.</p><h3 id="incident-2-after-fixing-1-vlan11-still-could-not-ping-pve">Incident #2: After fixing #1, VLAN11 still could not ping PVE</h3><ul><li><code>192.168.11.29</code> could not ping <code>192.168.22.12</code></li><li>But it could ping OpenWrt <code>192.168.22.13</code></li></ul><p>This pointed to OpenWrt NAT/firewall behavior affecting internal reachability.</p><hr/><h2 id="investigation-evidence-driven">Investigation (Evidence-driven)</h2><h3 id="1-confirm-the-primary-gateway-is-fine">1) Confirm the primary gateway is fine</h3><p>On Fiber:</p><ul><li>It could ping both <code>192.168.183.235</code> and <code>192.168.22.12</code>.</li><li><code>ip route</code> showed both subnets as directly connected.</li></ul><p>Conclusion: core L3 routing on Fiber was not broken.</p><h3 id="2-hop-by-hop-capture-find-where-the-reply-disappears">2) Hop-by-hop capture: find where the reply disappears</h3><ul><li>On PVE: request/reply both exist → PVE is replying.</li><li>On Fiber (<code>br22</code>/<code>br0</code>): request only → reply never reached Fiber.</li></ul><p>Conclusion: the return path is not going through the primary gateway.</p><h3 id="3-let-ip-route-get-tell-the-truth">3) Let <code>ip route get</code> tell the truth</h3><p>On PVE:</p><ul><li><code>ip route</code> showed: <code>default via 192.168.22.13 dev vmbr0</code></li><li><code>ip route get 192.168.183.235</code> showed: <code>via 192.168.22.13</code></li></ul><p><strong>Root cause #1</strong>: PVE had <strong>no explicit route</strong> to internal VLAN subnets, so return traffic to <code>192.168.183.0/24</code> naturally followed the default route into OpenWrt. Once that happens, any NAT/proxy/misroute on OpenWrt can prevent the primary gateway from ever seeing the reply.</p><p>That creates asymmetric routing:</p><ul><li>Forward: <code>183 → Fiber → 22 → PVE</code> (OK)</li><li>Return: <code>PVE → OpenWrt → (NAT/proxy/unknown)</code> (uncontrolled)</li></ul><h3 id="4-validate-openwrt-firewallnat-configuration">4) Validate OpenWrt firewall/NAT configuration</h3><p>After confirming OpenWrt uses <code>fw3 + iptables</code> (no nftables), the key finding was:</p><ul><li><code>lan</code> zone had <code>masq=&#x27;1&#x27;</code></li></ul><p><strong>Root cause #2</strong>: OpenWrt applied SNAT to internal traffic, rewriting sources to <code>192.168.22.13</code> and making internal routing/session behavior unstable, especially with proxy redirection.</p><hr/><h2 id="fix-minimal-changes">Fix (Minimal changes)</h2><h3 id="fix-1-keep-default-gateway--openwrt-but-route-internal-subnets-via-fiber">Fix #1: Keep default gateway = OpenWrt, but route internal subnets via Fiber</h3><p>Constraint: PVE default route must remain <code>192.168.22.13</code> (OpenWrt) for QoS/traffic handling. In that case, do not touch the default route. Instead, explicitly route internal VLAN subnets via the primary L3 gateway (<code>192.168.22.1</code>).</p><p>Temporary test (examples for VLAN11/VLAN183; add others as needed):</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>Verification tips:</p><ul><li><code>ip route get 192.168.183.235</code> should show <code>via 192.168.22.1</code></li><li>ICMP replies should be visible on Fiber interfaces (e.g., <code>br22/br0</code>)</li></ul><p>Persist it in Proxmox networking (commonly <code>/etc/network/interfaces</code> for <code>vmbr0</code>) with <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>Verification:</p><ul><li>Fiber sees ICMP replies on <code>br22/br0</code>.</li><li><code>192.168.183.235</code> can ping <code>192.168.22.12</code> again.</li></ul><h3 id="fix-2-disable-masquerade-on-openwrt-lan-zone">Fix #2: Disable masquerade on OpenWrt LAN zone</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>Verification:</p><ul><li><code>192.168.11.29</code> can ping <code>192.168.22.12</code>.</li><li>Internal traffic keeps real source IPs, making debugging and ACLs sane.</li></ul><hr/><h2 id="prevention">Prevention</h2><ul><li><strong>Route internally; NAT only on real WAN</strong>
<ul><li><code>lan</code>/internal zones: <code>masq=0</code></li><li><code>wan</code> egress: <code>masq=1</code></li></ul></li><li><strong>If a side gateway must be the default route (QoS/proxy), split routing explicitly</strong>
<ul><li>Default route can stay on OpenWrt, but internal subnets must have explicit routes via the primary L3 gateway (or use policy routing by destination).</li><li>Keep east-west traffic on predictable L3 routing. Do not let it fall into NAT/proxy black boxes.</li></ul></li><li><strong>Standard SOP (Standard Operating Procedure)</strong>
<ul><li>Meaning: for “cross-VLAN ping fails”, do not guess. Follow the same evidence-driven checklist every time.</li><li><strong>Step 1: packet captures answer 3 questions</strong>: did the request arrive, did the target reply, and at which hop did the reply disappear?
<ul><li>On PVE: <code>tcpdump -ni vmbr0 icmp</code></li><li>On the primary gateway (both VLAN interfaces): <code>tcpdump -ni br22 icmp</code> and <code>tcpdump -ni br0 icmp</code></li></ul></li><li><strong>Step 2: verify routing decisions (no imagination)</strong>
<ul><li>On PVE: <code>ip route</code> + <code>ip route get 192.168.183.235</code></li><li>On OpenWrt/Fiber: <code>ip route get 192.168.183.235</code> (confirm the actual next hop)</li></ul></li><li><strong>Step 3: simplify the data path first</strong>
<ul><li>Restore pure routing with real source IPs and predictable return paths before layering QoS/proxy/policy routing.</li></ul></li></ul></li><li><strong>Make proxy behavior explicit</strong>
<ul><li>Exclude internal subnets from transparent proxy/tunnel redirection.</li></ul></li></ul><hr/><h2 id="one-line-lesson">One-line lesson</h2><p>If ICMP looks like “ghost packets”, you most likely built a black box with the wrong default gateway and the wrong NAT — not a broken protocol stack.</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[How to Properly Use Claude as an Individual]]></title>
            <link>https://blog.cheverjohn.me/how-to-properly-use-claude-as-an-individual</link>
            <guid>https://blog.cheverjohn.me/how-to-properly-use-claude-as-an-individual</guid>
            <pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[As AI assistants become increasingly integrated into our daily workflows, understanding how to properly leverage tools like Claude can significantly enhance your productivity and decision-making. This comprehensive guide will help you maximize the value you get from Claude while maintaining good pra...]]></description>
            <content:encoded><![CDATA[<article><div><p>As AI assistants become increasingly integrated into our daily workflows, understanding how to properly leverage tools like Claude can significantly enhance your productivity and decision-making. This comprehensive guide will help you maximize the value you get from Claude while maintaining good practices and realistic expectations.</p><h2 id="understanding-claudes-core-strengths">Understanding Claude&#x27;s Core Strengths</h2><p>Before diving into specific use cases, it&#x27;s crucial to understand what Claude excels at:</p><h3 id="1-text-analysis-and-processing">1. Text Analysis and Processing</h3><p>Claude can quickly analyze, summarize, and extract insights from large volumes of text. Whether you&#x27;re dealing with research papers, meeting notes, or technical documentation, Claude can help you identify key points and patterns.</p><h3 id="2-creative-and-technical-writing">2. Creative and Technical Writing</h3><p>From drafting emails to writing code, Claude can assist with various forms of content creation while maintaining your personal voice and style preferences.</p><h3 id="3-problem-solving-and-research">3. Problem-Solving and Research</h3><p>Claude can help break down complex problems into manageable components and provide structured approaches to finding solutions.</p><h3 id="4-learning-and-explanation">4. Learning and Explanation</h3><p>Need something explained? Claude can adapt explanations to your level of expertise and learning style.</p><h2 id="essential-best-practices">Essential Best Practices</h2><h3 id="start-with-clear-specific-prompts">Start with Clear, Specific Prompts</h3><p><strong>Good:</strong> &quot;Help me write a professional email declining a meeting request for Thursday afternoon, suggesting alternative times next week, and maintaining a collaborative tone.&quot;</p><p><strong>Poor:</strong> &quot;Write an email for me.&quot;</p><p>The more context and specificity you provide, the better Claude can tailor its response to your needs.</p><h3 id="iterate-and-refine">Iterate and Refine</h3><p>Don&#x27;t expect perfect results on the first try. Use follow-up prompts to:</p><ul><li>Ask for clarification or expansion on specific points</li><li>Request different approaches or styles</li><li>Provide feedback on what works and what doesn&#x27;t</li></ul><h3 id="verify-important-information">Verify Important Information</h3><p>While Claude is knowledgeable, always verify critical facts, especially for:</p><ul><li>Medical or legal advice</li><li>Financial decisions</li><li>Current events or rapidly changing information</li><li>Technical specifications or requirements</li></ul><h2 id="practical-use-cases-for-individuals">Practical Use Cases for Individuals</h2><h3 id="professional-development">Professional Development</h3><p><strong>Email Communication:</strong></p><ul><li>Draft professional emails with appropriate tone</li><li>Summarize long email threads</li><li>Translate complex technical concepts for non-technical audiences</li></ul><p><strong>Document Creation:</strong></p><ul><li>Create structured reports and presentations</li><li>Write job application materials</li><li>Develop project plans and timelines</li></ul><p><strong>Learning and Skill Development:</strong></p><ul><li>Get explanations of complex topics</li><li>Practice interview questions</li><li>Learn new programming languages or frameworks</li></ul><h3 id="personal-productivity">Personal Productivity</h3><p><strong>Research and Planning:</strong></p><ul><li>Compare options for major purchases</li><li>Plan travel itineraries</li><li>Research topics of personal interest</li></ul><p><strong>Creative Projects:</strong></p><ul><li>Brainstorm ideas for hobbies or side projects</li><li>Get feedback on creative writing</li><li>Plan events or celebrations</li></ul><p><strong>Daily Life Management:</strong></p><ul><li>Create meal plans and shopping lists</li><li>Organize and prioritize tasks</li><li>Draft important personal correspondence</li></ul><h2 id="advanced-techniques">Advanced Techniques</h2><h3 id="context-management">Context Management</h3><p>Provide relevant background information at the start of conversations:</p><ul><li>Your role and expertise level</li><li>The specific context or constraints you&#x27;re working within</li><li>Your goals and success criteria</li></ul><h3 id="chain-of-thought-prompting">Chain of Thought Prompting</h3><p>For complex problems, ask Claude to &quot;think step by step&quot; or &quot;explain your reasoning.&quot; This often leads to more thorough and accurate responses.</p><h3 id="role-playing-scenarios">Role-Playing Scenarios</h3><p>Ask Claude to take on specific roles or perspectives:</p><ul><li>&quot;Act as a senior software engineer reviewing my code&quot;</li><li>&quot;Respond as someone unfamiliar with this topic would&quot;</li><li>&quot;Take the perspective of a potential customer&quot;</li></ul><h3 id="template-creation">Template Creation</h3><p>Develop reusable prompts for recurring tasks:</p><ul><li>Meeting summary templates</li><li>Email response frameworks</li><li>Problem-solving methodologies</li></ul><h2 id="what-claude-cannot-do">What Claude Cannot Do</h2><p>Understanding limitations is crucial for effective use:</p><h3 id="real-time-information">Real-Time Information</h3><p>Claude&#x27;s training has a knowledge cutoff and cannot access current web information or real-time data.</p><h3 id="personal-data-access">Personal Data Access</h3><p>Claude cannot access your personal files, emails, or private information unless you explicitly share it in the conversation.</p><h3 id="execute-actions">Execute Actions</h3><p>Claude cannot directly perform actions in external systems, send emails, or make purchases on your behalf.</p><h3 id="replace-human-judgment">Replace Human Judgment</h3><p>While Claude can provide insights and suggestions, important decisions should always involve human judgment and expertise.</p><h2 id="maintaining-privacy-and-security">Maintaining Privacy and Security</h2><h3 id="information-sharing">Information Sharing</h3><ul><li>Avoid sharing sensitive personal information (SSNs, passwords, financial details)</li><li>Be cautious with proprietary or confidential business information</li><li>Consider anonymizing data when seeking help with specific scenarios</li></ul><h3 id="data-retention">Data Retention</h3><ul><li>Understand the platform&#x27;s data retention policies</li><li>Don&#x27;t rely on Claude to remember information between separate conversations</li><li>Keep your own records of important insights or decisions</li></ul><h2 id="building-an-effective-workflow">Building an Effective Workflow</h2><h3 id="1-define-your-use-cases">1. Define Your Use Cases</h3><p>Identify specific, recurring tasks where Claude can add value to your routine.</p><h3 id="2-develop-standard-prompts">2. Develop Standard Prompts</h3><p>Create and refine prompt templates for your most common use cases.</p><h3 id="3-set-realistic-expectations">3. Set Realistic Expectations</h3><p>Understand that Claude is a tool to augment your capabilities, not replace your thinking and judgment.</p><h3 id="4-measure-impact">4. Measure Impact</h3><p>Track how Claude usage affects your productivity and the quality of your work.</p><h3 id="5-stay-updated">5. Stay Updated</h3><p>AI capabilities evolve rapidly. Stay informed about new features and best practices.</p><h2 id="common-pitfalls-to-avoid">Common Pitfalls to Avoid</h2><h3 id="over-reliance">Over-Reliance</h3><p>Don&#x27;t become dependent on Claude for basic thinking or decision-making. Use it to enhance your capabilities, not replace them.</p><h3 id="prompt-laziness">Prompt Laziness</h3><p>Avoid vague or minimal prompts. Investing time in clear communication yields better results.</p><h3 id="ignoring-context">Ignoring Context</h3><p>Remember to provide sufficient context for complex or specialized topics.</p><h3 id="assuming-perfection">Assuming Perfection</h3><p>Always review and validate Claude&#x27;s outputs, especially for important tasks.</p><h2 id="conclusion">Conclusion</h2><p>Claude can be a powerful ally in both professional and personal contexts when used thoughtfully and strategically. The key to success lies in understanding its strengths and limitations, developing clear communication patterns, and maintaining appropriate skepticism and verification practices.</p><p>Start with simple use cases, build your skills gradually, and always remember that Claude is most effective when it augments your human intelligence rather than attempting to replace it. With practice and the right approach, you&#x27;ll find Claude becomes an invaluable tool in your personal and professional toolkit.</p><p>Remember: the goal isn&#x27;t to use Claude for everything, but to use it for the right things in the right way.</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 Is Coming, Can You Really Code?]]></title>
            <link>https://blog.cheverjohn.me/can-you-really-code</link>
            <guid>https://blog.cheverjohn.me/can-you-really-code</guid>
            <pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[AI has been moving at a breakneck pace. Many people are getting hooked on barking orders at AI and then discovering piles of error‑ridden code it produced (and in truth, you may not fully understand the code the AI wrote either). If you do understand AI, sure—you know how to use prompt engineering....]]></description>
            <content:encoded><![CDATA[<article><div><p>AI has been moving at a breakneck pace. Many people are getting hooked on barking orders at AI and then discovering piles of error‑ridden code it produced (and in truth, you may not fully understand the code the AI wrote either).</p><p>If you do understand AI, sure—you know how to use prompt engineering. Still, I want to reinforce the fundamentals of software engineering.</p><p>Object‑oriented programming, design principles, design patterns, coding conventions, and refactoring.</p><h2 id="programming-paradigmsstyles">Programming paradigms/styles</h2><p>The mainstream programming paradigms/styles are:</p><ul><li>Procedural programming</li><li>Object‑oriented programming (OOP)</li><li>Functional programming</li></ul><p>Among them, OOP remains the most widely adopted paradigm. Most popular programming languages today are object‑oriented or provide strong OO support.</p><h2 id="design-principles">Design principles</h2><p>Definition: distilled practical experience for code design.</p><p>These principles often sound abstract and are described vaguely.</p><p>The hard part: understand the original intent, the specific problems they solve, and the scenarios where they apply.</p><p>Common principles include:</p><ul><li><p>SOLID principles</p><blockquote><p>You might wonder why SOLID uniquely comes with five additional “sub‑principles.”</p><p>I had the same question, so I looked it up.</p><p>SOLID is called the cornerstone of object‑oriented design because it offers a concrete, actionable toolkit—rather than vague ideas—for solving real problems in software development.</p><p>Each sub‑principle targets a different layer of design pitfalls. It stands out precisely because it is not a single fuzzy rule, but a collection of five specific, operable design principles.</p></blockquote>
<ul><li>Single Responsibility Principle (SRP)</li><li>Open/Closed Principle (OCP)</li><li>Liskov Substitution Principle (LSP)</li><li>Interface Segregation Principle (ISP)</li><li>Dependency Inversion Principle (DIP)</li></ul></li><li><p>KISS principle (Keep It Simple, Stupid)</p></li><li><p>DRY principle (Don’t Repeat Yourself)</p></li><li><p>YAGNI principle (You Aren’t Gonna Need It)</p></li><li><p>Law of Demeter (LOD)</p></li></ul><h2 id="design-patterns">Design patterns</h2><p>Definition: recurring solutions or design approaches distilled from frequently encountered problems in software development.</p><p>Most design patterns are fundamentally about extensibility.</p><p>The hard part: knowing which problem each pattern addresses, recognizing typical application scenarios, and—importantly—avoiding overuse.</p><p>What’s included: there are 23 classic design patterns.</p><p>As languages evolve:</p><ul><li>Some patterns (e.g., Singleton) have aged poorly and even become anti‑patterns.</li><li>Some are now built into languages (e.g., Iterator).</li><li>New patterns keep emerging (e.g., Monostate).</li></ul><p>The 23 classic patterns are grouped into three categories: creational, structural, and behavioral.</p><h3 id="creational">Creational</h3><p>Common: Singleton, Factory (Factory Method and Abstract Factory), Builder.</p><p>Less common: Prototype.</p><h3 id="structural">Structural</h3><p>Common: Proxy, Bridge, Decorator, Adapter.</p><p>Less common: Facade, Composite, Flyweight.</p><h3 id="behavioral">Behavioral</h3><p>Common: Observer, Template Method, Strategy, Chain of Responsibility, Iterator, State.</p><p>Less common: Visitor, Memento, Command, Interpreter, Mediator.</p><h2 id="coding-conventions">Coding conventions</h2><p>Definition: primarily concerned with readability. Compared with design principles and design patterns, conventions are more concrete and closer to code‑level details. Even if you’re unfamiliar with design principles or patterns, at minimum you should master basic coding conventions.</p><p>For example: how to name variables, classes, and functions; how to write comments; keep functions short; avoid too many parameters; and so on.</p><p>There are plenty of classic books to learn from—“Refactoring,” “Code Complete,” and “Clean Code,” to name a few.</p><p>Each convention is straightforward and specific. Just remember them and follow them. Unlike design principles, they require less personal interpretation.</p><h2 id="refactoring">Refactoring</h2><p>As long as a project is alive and people keep working on it, the software will evolve. New features will inevitably push older code to be refactored. Refactoring is the practical way to keep code quality from decaying to an unrecoverable state.</p><p>The toolkit for refactoring includes everything mentioned earlier: paradigms, design principles, design patterns, and coding conventions.</p><p>While design patterns can improve extensibility, overuse or misuse increases complexity and harms readability.</p><p>In early development, unless absolutely necessary, don’t over‑design or apply complex patterns.</p><p>Instead, when code shows concrete issues, refactor to address those issues using the appropriate principles and patterns.</p><p>This effectively prevents premature over‑engineering.</p><h3 id="mustknow-points">Must‑know points</h3><p>As follows:</p><ul><li>The purpose (why), target (what), timing (when), and methods (how) of refactoring</li><li>Techniques for safe refactoring: unit tests and code testability</li><li>Two scales of refactoring: large‑scale/high‑level and small‑scale/low‑level</li></ul><h2 id="how-the-five-pieces-fit-together">How the five pieces fit together</h2><p>The relationship among OOP, design principles, design patterns, coding conventions, and refactoring is as follows:</p><ul><li>OOP, with its rich features (encapsulation, abstraction, inheritance, polymorphism), enables sophisticated design ideas and is the foundation for implementing many principles and patterns.</li><li>Design principles guide design decisions and help determine whether a particular pattern should be applied in a given scenario. For example, the Open/Closed Principle underpins patterns such as Strategy and Template Method.</li><li>Design patterns are recurring solutions to common design problems in software development. Their main goal is to improve extensibility. In terms of abstraction, principles are more abstract; patterns are more concrete and executable.</li><li>Coding conventions primarily address readability. Compared with principles and patterns, conventions are more specific, detail‑oriented, and directly actionable. Continuous small‑scale refactoring relies heavily on conventions as its theoretical backbone.</li></ul><p>Ultimately, this article is about one thing: writing high‑quality code. Once you trace it back to that, how to do many things—and how to implement them in code—becomes clear.</p><p><img alt="code-knowledge-graph_en.png" src="code-knowledge-graph_en.png"/></p><pre><code class="lang-mermaid">mindmap
  root((Writing High-Quality Code))
    Object-Oriented
      Encapsulation, Abstraction, Inheritance, Polymorphism
      OOP vs Procedural Programming
      OO Analysis, Design, Programming
      Interface vs Abstract Class
      Program to an Interface, Not an Implementation
      Favor Composition Over Inheritance
      Anemic vs Rich Domain Model
    Design Principles
      SOLID Principles
        SRP Single Responsibility
        OCP Open/Closed
        LSP Liskov Substitution
        ISP Interface Segregation
        DIP Dependency Inversion
      Others
        DRY Principle
        KISS Principle
        YAGNI Principle
        Law of Demeter (LOD)
    Coding Conventions
      20 Quick Rules to Improve Code Quality
    Refactoring
      Purpose, Target, Timing, Methods
      Unit Tests and Testability
      Large-Scale (High-Level)
      Small-Scale (Low-Level)
    Design Patterns
      Creational
        Common
          Singleton
          Factory (Factory Method &amp; Abstract Factory)
          Builder
        Less Common
          Prototype
      Structural
        Common
          Proxy
          Bridge
          Decorator
          Adapter
        Less Common
          Facade
          Composite
          Flyweight
      Behavioral
        Common
          Observer
          Template Method
          Strategy
          Chain of Responsibility
          Iterator
          State
        Less Common
          Visitor
          Memento
          Command
          Interpreter
          Mediator
</code></pre>
<pre><code class="lang-mermaid">mindmap
  root((Writing High-Quality Code))
    Object-Oriented
      Encapsulation, Abstraction, Inheritance, Polymorphism
      OOP vs Procedural Programming
      OO Analysis, Design, Programming
      Interface vs Abstract Class
      Program to an Interface, Not an Implementation
      Favor Composition Over Inheritance
      Anemic vs Rich Domain Model
    Design Principles
      SOLID Principles
        SRP Single Responsibility
        OCP Open/Closed
        LSP Liskov Substitution
        ISP Interface Segregation
        DIP Dependency Inversion
      Others
        DRY Principle
        KISS Principle
        YAGNI Principle
        Law of Demeter (LOD)
    Coding Conventions
      20 Quick Rules to Improve Code Quality
    Refactoring
      Purpose, Target, Timing, Methods
      Unit Tests and Testability
      Large-Scale (High-Level)
      Small-Scale (Low-Level)
    Design Patterns
      Creational
        Common
          Singleton
          Factory (Factory Method &amp; Abstract Factory)
          Builder
        Less Common
          Prototype
      Structural
        Common
          Proxy
          Bridge
          Decorator
          Adapter
        Less Common
          Facade
          Composite
          Flyweight
      Behavioral
        Common
          Observer
          Template Method
          Strategy
          Chain of Responsibility
          Iterator
          State
        Less Common
          Visitor
          Memento
          Command
          Interpreter
          Mediator
</code></pre>
<p>PS: A bunch of keywords for further exploration:</p><p>System architecture principles
High cohesion and low coupling
Law of Demeter (principle of least knowledge)
Composition over inheritance
Separation of concerns
Design by contract
Cloud‑native design principles
Resilience design principles
Observability principles
Immutable infrastructure
Service autonomy
Declarative configuration
Security by design
Defense in depth
Principle of least privilege
Secure by default
Separation of duties
Fail‑safe</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>code</category>
        </item>
        <item>
            <title><![CDATA[Performance Optimization of Legacy Systems]]></title>
            <link>https://blog.cheverjohn.me/performance-optimization-of-old-systems</link>
            <guid>https://blog.cheverjohn.me/performance-optimization-of-old-systems</guid>
            <pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[When legacy systems are initially created and coded, the final system performance and maintainability depend entirely on the developer's expertise. When you inherit a system with the following issues, this article might help you: 1. No monitoring 2. No alerting 3. No business success rate metrics 4....]]></description>
            <content:encoded><![CDATA[<article><div>
<p>When legacy systems are initially created and coded, the final system performance and maintainability depend entirely on the developer&#x27;s expertise.</p><p>When you inherit a system with the following issues, this article might help you:</p><ol start="1"><li>No monitoring</li><li>No alerting</li><li>No business success rate metrics</li><li>Frequent optimistic locking failures</li><li>Black box system response time (yes, your business users only vaguely tell you the system is slow, but don&#x27;t know specifically where it&#x27;s slow)</li><li>Complex log troubleshooting</li></ol><p>Yes, these are the headache-inducing problems I&#x27;ve encountered over the past month.</p><p>Now that the month is up, I&#x27;m starting to take action and perform a series of performance optimizations on this system.</p><p>First, you need to understand the overall situation of the system. For optimization, you should at least know what you&#x27;re optimizing. Business users should also know where exactly their system is slow.</p><p>So what you need to do is <strong>establish system baselines</strong>.</p><h2 id="establishing-system-baselines">Establishing System Baselines</h2><p>System baselines are quantitative descriptions of key performance indicators when the system is running normally.</p><p>They are the starting point for system performance optimization and the standard reference for measuring optimization effectiveness. Simply put, system baselines are your system&#x27;s &quot;health checkup report,&quot; recording system performance under various conditions. (<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="system-baselines-include-the-following">System Baselines Include the Following</h3><p>A complete system baseline should include the following aspects:</p><h4 id="performance-metrics">Performance Metrics</h4><ul><li>Response time (average, P95, P99)</li><li>Throughput (QPS/TPS)</li><li>Latency</li><li>Concurrent processing capability</li></ul><h4 id="resource-usage"><strong>Resource Usage</strong></h4><ul><li><strong>CPU utilization</strong></li><li><strong>Memory usage and allocation</strong></li><li><strong>Disk I/O</strong></li><li><strong>Network I/O</strong></li><li><strong>Connection pool usage status</strong></li></ul><h4 id="business-metrics">Business Metrics</h4><ul><li>Business success rate</li><li>Error rate</li><li>Key business process completion time</li></ul><h4 id="system-stability-metrics">System Stability Metrics</h4><ul><li>Mean Time Between Failures (MTBF)</li><li>Mean Time To Recovery (MTTR)</li><li>Lock contention situations (such as the optimistic lock failure rate you mentioned)</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: Simplified Data Processing on Large Clusters]]></title>
            <link>https://blog.cheverjohn.me/google-map-reduce</link>
            <guid>https://blog.cheverjohn.me/google-map-reduce</guid>
            <pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Taking this opportunity during the May Day holiday to thoroughly analyze one of the most famous papers: "MapReduce: Simplified Data Processing on Large Clusters." Of course, this article is deeply influenced by the article from "Muniao's Notes," so my article is purely a combination of two articles...]]></description>
            <content:encoded><![CDATA[<article><div><p>Taking this opportunity during the May Day holiday to thoroughly analyze one of the most famous papers: &quot;MapReduce: Simplified Data Processing on Large Clusters.&quot;</p><p>Of course, this article is deeply influenced by the <a href="https://www.qtmuniao.com/2019/04/30/map-reduce/">article</a> from &quot;Muniao&#x27;s Notes,&quot; so my article is purely a combination of two articles plus my own thoughts.</p><p><img alt="MR Execution Flow" src="The-Whole-Arch.png"/></p><p>I&#x27;ve always heard senior colleagues talk about how powerful MapReduce distributed computing is, so today&#x27;s the opportunity to dive in!</p><h2 id="introduction">Introduction</h2><p>MapReduce in the paper is actually a concept. But in another context, it can also be a programming model, as well as a distributed system implementation supporting that model. I found an article that explains this concept better:</p><blockquote><p>MapReduce is a concept proposed in Google&#x27;s 2004 paper (Google internally wrote the first version starting in 2003).</p><p>In Google&#x27;s context, MapReduce is both a programming model and a distributed system implementation supporting that model. Its introduction enables developers without distributed systems background to leverage large-scale clusters for high-throughput processing of massive data with relative ease.</p></blockquote>
<p>This article also has a sentence explaining the problem-solving approach for applying this technology: <strong>Finding pain points in requirements (such as how to maintain, update, and rank massive indexes), performing high-level abstractions of key processing flows (sharding Map, on-demand Reduce), for efficient system implementation (so-called tailored solutions).</strong></p><p><em>Among these, finding an appropriate computational abstraction is the most difficult part, requiring both intuitive understanding of requirements and extremely high computer science literacy.</em></p><p>The quote above is from the referenced article from &quot;Muniao&#x27;s Notes.&quot;</p><p>Returning to the paper, we can see that on the first page, Google&#x27;s experts clearly explained what this is.</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>This means we abstracted something to express a computational method that can hide many conceptual details (parallelization, fault tolerance, data distribution, and load balancing).</p><p>This abstraction is inspired by the map and reduce primitives present in Lisp and many other functional languages.</p><p>We realized that most of our computations involved applying a map operation to each logical &quot;record&quot; 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>Most of our computations involve applying a map operation to each logical record in the input to compute a set of intermediate key/value pairs, then applying a reduce operation to all values sharing the same key to appropriately combine the derived data.</p><p>Our use of a functional model with user-specified 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>We use a functional model with user-specified map and reduce operations, enabling easy parallelization of large computations.</p></blockquote>
<p>I find the last sentence very interesting:</p><blockquote><p>use re-execution as the primary mechanism for fault tolerance.</p></blockquote>
<p>Using &quot;re-execution&quot; as the primary mechanism for fault tolerance.</p><p>The paper&#x27;s Abstract outlines the content:</p><ol start="1"><li>Section 1 is the Introduction above</li><li>Section 2 describes the basic programming model and gives several examples, <a href="#programming-model">Programming Model</a></li><li>Section 3 describes an implementation of the MapReduce interface tailored towards our cluster-based computing environment, <a href="#implementation">Implementation</a><ul><li>Implementation of MapReduce interface customized for cluster computing environments</li></ul></li><li>Section 4 describes several refinements of the programming model that we have found useful - Several programming model improvements</li><li>Section 5 has performance measurements of our implementation for a variety of tasks - Performance measurements of implementing various 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 applications within Google</li><li>Section 7 discusses related and future work</li></ol><h2 id="programming-model">Programming Model</h2><p>The Map key is a normal key, and the value can be thought of as a string array.</p><p>MapReduce, simply put, consists of two functions: the map function and the reduce function.</p><p>The Map function receives an input pair and generates a set of intermediate key/value pairs, then the MapReduce library combines all intermediate values associated with the same key.</p><h3 id="example">Example</h3><p>Below is pseudo-code, grandfather-level, from the original paper:</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>This is a classic MapReduce word count implementation, one of the most common examples in the MapReduce programming model.</p><ul><li>key: document name</li><li>value: complete document content (text string)</li></ul><p>EmitIntermediate represents the function provided by the MapReduce framework for outputting intermediate key/value pairs. Each call to this function produces a key-value pair: (word, &quot;1&quot;), indicating that the word appeared once.</p><p>For example, processing document content &quot;hello world hello&quot;:</p><ul><li>First word &quot;hello&quot; → EmitIntermediate(&quot;hello&quot;, &quot;1&quot;)</li><li>Second word &quot;world&quot; → EmitIntermediate(&quot;world&quot;, &quot;1&quot;)</li><li>Third word &quot;hello&quot; → EmitIntermediate(&quot;hello&quot;, &quot;1&quot;)</li></ul><p>Map phase intermediate results:</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-automatically-completed-by-framework">Shuffle Stage (Automatically Completed by Framework)</h3><p>Between Map and Reduce, the MapReduce framework automatically performs the Shuffle operation:</p><ol start="1"><li>Collect all mapper outputs</li><li>Sort by key (word)</li><li>Group all values with the same key together</li></ol><p>So the above example after 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-function">Reduce Function</h3><p>After Shuffle comes the Reduce function&#x27;s work. The pseudo-code above essentially performs accumulation, which needs no further explanation.</p><h3 id="complete-execution-flow">Complete Execution Flow</h3><p>Here&#x27;s a larger example showing the entire MapReduce execution flow:</p><p><strong>Assume three documents</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 Phase (Parallel Execution)</strong></p><p>Mapper 1 processes 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 processes 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 processes 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 Phase (Automatically Completed by Framework)</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 Phase (Parallel Execution)</strong>:</p><p>Reducer processes &quot;hello&quot;:</p><pre><code class="lang-shell">result = 0
result += 1 = 1
result += 1 = 2
Emit(&quot;2&quot;)  # Output (&quot;hello&quot;, &quot;2&quot;)
</code></pre>
<p>Similarly for other words...</p><p><strong>Final Results</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-frameworks-role">MapReduce Framework&#x27;s Role</h4><p>In this process, the MapReduce framework is responsible for:</p><ol start="1"><li>Splitting input data into multiple shards, assigning them to different Mappers</li><li>Executing multiple Map tasks in parallel</li><li>Performing Shuffle operations, reorganizing and sorting intermediate results</li><li>Executing multiple Reduce tasks in parallel</li><li>Collecting and integrating Reduce outputs</li><li>Handling task failures and retries</li><li>Optimizing data locality, processing data on nodes where it resides when possible</li></ol><p>This pattern allows developers to focus on business logic (Map and Reduce functions) without worrying about complex issues like parallelization, distributed computing, and fault tolerance.</p><h3 id="more-examples">More Examples</h3><p>Here are more examples:</p><p><strong>Distributed Grep</strong></p><p><strong>Working Principle</strong></p><p>In this example:</p><ul><li>Map function checks each line of input text, emitting the line if it matches a specified pattern</li><li>Reduce function is a simple identity function that directly copies intermediate results to output</li></ul><p><strong>Application Value</strong></p><p>This pattern is very suitable for quickly finding text lines with specific patterns in large-scale distributed file systems. It fully utilizes MapReduce&#x27;s parallel processing capabilities and is extremely efficient when searching for specific error information in TB or even PB-level log files.</p><p><strong>Count of URL Access Frequency</strong></p><p><strong>Working Principle</strong></p><ul><li><strong>Map function</strong>: Processes web request logs, emitting <code>&lt;URL, 1&gt;</code> key-value pairs for each URL</li><li><strong>Reduce function</strong>: Sums all counts for the same URL, outputting <code>&lt;URL, total count&gt;</code></li></ul><p><strong>Application Value</strong></p><p>This is a fundamental operation in web analytics, crucial for understanding website traffic distribution, identifying popular content, and detecting abnormal access patterns. In large websites, daily log data can reach several TB, and MapReduce can effectively handle this scale of data.</p><p><strong>Reverse Web-Link Graph</strong></p><p><strong>Working Principle</strong></p><ul><li><strong>Map function</strong>: Analyzes web content, outputting <code>&lt;target URL, source URL&gt;</code> for each discovered link</li><li><strong>Reduce function</strong>: Collects all source URLs for a target URL, outputting <code>&lt;target URL, source URL list&gt;</code></li></ul><p><strong>Application Value</strong></p><p>Reverse link graphs are one of the core data structures for modern search engines, used in the following scenarios:</p><ul><li>Foundation data for PageRank and other web importance algorithms</li><li>Analyzing citation relationships between websites</li><li>Discovering influential content creators</li><li>Providing backlink analysis tools for webmasters</li></ul><p>Building a complete web reverse link graph is a computation-intensive task, and the MapReduce model is very suitable for this naturally parallelizable problem.</p><p><strong>Term-Vector per Host</strong></p><p><strong>Working Principle</strong></p><ul><li><strong>Map function</strong>: Analyzes document content, extracts hostname from URL, outputs <code>&lt;hostname, document term vector&gt;</code></li><li><strong>Reduce function</strong>: Merges all term vectors for the same host, filters low-frequency terms, outputs <code>&lt;hostname, aggregated term vector&gt;</code></li></ul><p><strong>Application Value</strong></p><p>This analysis is very valuable for understanding website content characteristics:</p><ul><li>Can be used for website topic classification</li><li>Helps with search engine optimization</li><li>Content similarity comparison</li><li>Competitor website content analysis</li><li>Foundation data for content recommendation systems</li></ul><p><strong>Inverted Index</strong></p><p><strong>Working Principle</strong></p><ul><li><strong>Map function</strong>: Parses each document, outputs <code>&lt;word, document ID&gt;</code> key-value pairs</li><li><strong>Reduce function</strong>: Receives all document IDs for a given word, sorts them and outputs <code>&lt;word, document ID list&gt;</code> key-value pairs</li></ul><p><strong>Application Value</strong></p><p>Inverted indexes are the fundamental data structure for modern search engines, used for:</p><ol start="1"><li><strong>Full-text search</strong>: Quickly find all documents containing query terms</li><li><strong>Phrase search</strong>: Achieve precise phrase search through positional information</li><li><strong>TF-IDF calculation</strong>: Provide term frequency statistics for information retrieval systems</li><li><strong>Keyword highlighting</strong>: Help frontend display matched text fragments</li><li><strong>Relevance ranking</strong>: Provide foundation data for search result ranking</li></ol><p>MapReduce is particularly suitable for building inverted indexes because it can efficiently process large numbers of documents in parallel and naturally implement index merging in the Reduce phase.</p><p><strong>Distributed Sort</strong></p><p><strong>Working Principle</strong></p><ul><li><strong>Map function</strong>: Extracts the key of each record, outputs <code>&lt;key, record&gt;</code> key-value pairs</li><li><strong>Reduce function</strong>: Directly outputs all received key-value pairs without modification</li></ul><p>This seemingly simple example actually cleverly utilizes two core features of the MapReduce framework:</p><ol start="1"><li><strong>Partitioning mechanism</strong>: Ensures records with keys in the same range are sent to the same Reducer</li><li><strong>Sorting property</strong>: Ensures keys received by Reducers are in order</li></ol><p><strong>MapReduce Framework&#x27;s Special Contribution</strong>:</p><p>In distributed sorting, the MapReduce framework does most of the important work:</p><ol start="1"><li><p><strong>Custom Partitioner</strong>:</p><pre><code class="lang-go">// Example: Range partitioner
func RangePartitioner(key string, numReducers int) int {
    // Determine which reducer to send to based on key range
    // This ensures global sorting
    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>Sort Comparator</strong>:</p><pre><code class="lang-go">// Define natural sorting order for keys
func KeyComparator(key1, key2 string) int {
    return strings.Compare(key1, key2)
}
</code></pre>
</li></ol><p><strong>Application Value</strong></p><p>Distributed sorting is a fundamental operation for many big data processing workflows:</p><ol start="1"><li><strong>Data preprocessing</strong>: Preparing large datasets for further analysis</li><li><strong>Log analysis</strong>: Sorting massive log records by timestamp</li><li><strong>Building indexes</strong>: Creating sorted indexes for databases or search engines</li><li><strong>Merging sorted data</strong>: Combining multiple sorted datasets into one</li><li><strong>TopN queries</strong>: Quickly finding top N records for a given metric</li></ol><p><strong>Overall Examples Analysis and Comparison</strong></p><p>The examples demonstrate the versatility and adaptability of the MapReduce model:</p><ol start="1"><li><strong>Distributed Grep</strong>: Simplest application, basically only uses Map functionality, suitable for simple filtering operations</li><li><strong>URL Access Frequency Count</strong>: Classic word count variant, demonstrates MapReduce advantages in statistical aggregation</li><li><strong>Reverse Web-Link Graph</strong>: Shows how to use MapReduce to build complex relationship graphs and index structures</li><li><strong>Term-Vector per Host</strong>: Combines text analysis and aggregation functions, suitable for advanced content analysis</li></ol><h2 id="implementation">Implementation</h2><p>Multiple different implementations of the MapReduce interface are possible.</p><p><strong>The right choice depends on the specific environment (meaning specific problems require specific analysis).</strong></p><p>For example, one implementation might be suitable for small shared-memory machines, another for large NUMA multiprocessors, and yet another for even larger networked machine clusters.</p><p>Below is the computing environment widely used at 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.</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 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) Machines typically feature dual-processor x86 architecture, running Linux systems, with 2-4 GB of memory per machine.</p><p>(2) Commodity networking hardware is used — typically 100 megabits/second or 1 gigabit/second at the machine level, but the average overall bisection bandwidth is significantly lower.</p><p>(3) A cluster consists of hundreds or thousands of machines, so machine failures are common.</p><p>(4) Storage is provided by inexpensive IDE disks directly connected to individual machines. An internally developed distributed file system is used to manage data stored on these disks. The file system provides availability and reliability on top of unreliable hardware through data replication.</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 the cluster.</p><h3 id="execution-process">Execution Process</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 Execution Flow" src="The-Whole-Arch.png"/></p><ol start="1"><li><p>The MapReduce Library in the user program first splits the files into M pieces, each piece typically 16-64 MB in size;</p></li><li><p>The program copy on the Master is special, while other workers are assigned tasks by the master. There are typically M map tasks and R reduce tasks to assign.</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>Here, the word &quot;idle&quot; refers to idle workers - in Google&#x27;s MapReduce architecture, the entire computation task is executed distributedly, including:</p><ol start="1"><li><strong>Master (master node)</strong>: A special program copy responsible for task scheduling and coordinating the entire computation process</li><li><strong>Workers (worker nodes)</strong>: Other program copies responsible for executing actual computation tasks</li><li><strong>Idle workers</strong>: Worker nodes currently not executing any tasks and in a waiting state</li></ol><p>I hope this clarifies what idle workers are.</p><p>So generally speaking, the entire MapReduce workflow related to idle worker nodes:</p><ol start="1"><li>When computation tasks begin, the system starts multiple program copies, with one as the master node and others as worker nodes</li><li>The master node maintains the state of the entire cluster, including whether each worker node is idle</li><li>When the master node detects that a worker node is &quot;idle&quot; (not executing tasks), it selects one from the pending M Map tasks or R Reduce tasks to assign to that worker node</li></ol></li><li><p>A worker assigned a map task reads the contents of the corresponding input split, parses key/value pairs from the input data, and passes each pair to the user-defined map function.</p><p>These intermediate key/value pairs produced by map are buffered in memory.</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>In fact, Hadoop does exactly this.</p></li><li><p>Buffered intermediate result pairs are periodically written to local disk, then partitioned into R regions by the partitioning function.</p><p>These buffered pairs on local disk are passed back to the master, so the master can inform reduce workers of these pair locations.</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>When a reduce worker receives the buffered pair locations mentioned above, it uses RPC to read the corresponding partition data.</p><p>When a reduce worker has read all the data, it sorts by key so that all data with the same key is sorted together.</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>If the intermediate data is too large, external sorting programs are needed. This is where performance optimization points come in.</p></li></ol><p><strong>Steps 4 and 5 together are called shuffle</strong></p><ol start="6"><li><p>The Reduce Worker then iterates through this sorted intermediate data and passes this data along with its key-related data to the user&#x27;s reduce function.</p><p>The Reduce function&#x27;s output is appended to the final output file.</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&#x27;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>When all map tasks and reduce tasks are completed, the master wakes up the user program.</p><p>At this point, the user program receives a final computation result (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>Understanding the key data structures maintained by the Master node in the MapReduce framework and its core responsibilities in task coordination.</p><p>The Master node is actually the &quot;brain&quot; of the entire MapReduce execution process, maintaining the following important data:</p><ol start="1"><li><p><strong>Task State Records</strong>: For each map and reduce task, the master node records its current state:</p><ul><li>idle: pending tasks</li><li>in-progress: tasks assigned to workers but not yet completed</li><li>completed: finished tasks</li></ul></li><li><p><strong>Worker Machine Identity</strong>: For non-idle state tasks, the master node records the identity of the worker machine executing that task, used for tracking task execution and handling failures</p></li><li><p><strong>Intermediate File Metadata</strong>: For completed map tasks, the master stores location and size information of intermediate result files produced by that task</p></li></ol><p><strong>Master as Information Transfer Channel</strong></p><p>The Master node plays the role of an information transfer channel for intermediate result location information. When a map task completes, it informs the master node of which intermediate files were produced, along with their location and size information.</p><p>Additionally, Google&#x27;s MapReduce implementation has job-level encapsulation, where each job contains a series of tasks, namely Map Tasks and Reduce Tasks.</p><p>To maintain metadata for a running job, it&#x27;s necessary to save the state of all executing tasks and their machine IDs.</p><p>This information is crucial for reduce tasks, as reduce tasks need to know where to obtain the data they need to process.</p><p>Moreover, the Master also serves as an information channel from Map Task output to Reduce Task. The master node incrementally pushes this information to worker nodes executing reduce tasks.</p><p>When each Map Task ends, it notifies the Master of the location information of its output intermediate results, the Master then forwards this to the corresponding Reduce Task, and the Reduce Task fetches the corresponding size of data from the corresponding location.</p><p><strong>Note: Since Map Task completion times are not uniform, this notification → forwarding → fetching process is incremental. It&#x27;s reasonable to infer that the sorting of intermediate data on the reduce side should be a continuous merge process, unlikely to be global sorting after all data is in place.</strong> — from Muniao</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>
<h3 id="fault-tolerance">Fault Tolerance</h3><p>Distributed systems must gracefully handle various errors on distributed machines when processing large amounts of data. The paper roughly divides failures into three types:</p><ol start="1"><li><a href="#worker-failure">Worker Failure</a></li><li><a href="#master-failure">Master Failure</a></li><li><a href="#semantics-in-the-presence-of-failures">Semantics in the Presence of Failures</a></li></ol><h4 id="worker-failure">Worker Failure</h4><p>The master periodically pings each worker node, and if there&#x27;s no response, the master marks it as a failed node.</p><p>At this time, both completed and uncompleted map tasks are marked back to the initial idle state, then wait to be scheduled to other normal workers.</p><p>These map tasks are then re-executed and re-stored to local disk, and the master continues to report this information to Reduce workers.</p><p>If a Reducer worker has already processed one of the map tasks, it doesn&#x27;t need to fetch processing data again from the master&#x27;s provided information; if not processed, it continues to fetch.</p><p>On the Reducer worker side, when it discovers that the map worker processing a certain segment of map tasks has failed, for example, this Reduce program is R5, processing map tasks (41-51) on Worker-37, when R5 processes M47 and discovers a failure, its countermeasures are as follows:</p><ul><li><strong>Transmission Interruption Handling</strong>: If R5&#x27;s connection is interrupted while pulling data from Worker-37, it triggers exception handling</li></ul><pre><code class="lang-java">// Simplified pseudo-code
try {
    fetchMapOutput(worker37, mapTaskId47);
} catch (FetchFailureException e) {
    // Wait for Master notification of new data location
    waitForNotification(mapTaskId47);
    // Retry with new data location
    fetchMapOutput(worker51, mapTaskId47);
}
</code></pre>
<ul><li><strong>Data Consistency</strong>: R5 discards incomplete data partially pulled from Worker-37</li></ul><pre><code class="lang-java">if (partialData &amp;&amp; dataSource != currentSourceForTask) {
    discardPartialData();
    fetchFromNewSource();
}
</code></pre>
<ul><li><strong>Notification Mechanism</strong>: Master notifies Reduce tasks through the following ways</li></ul><pre><code class="lang-shell">1. Heartbeat responses include updated Map output location information
2. RPC calls notify state changes
3. Reduce tasks periodically poll Master for latest mapping information
</code></pre>
<h4 id="master-failure">Master Failure</h4><p>Google&#x27;s paper handles Master failure relatively simply: saving state through checkpoint mechanisms, but terminating the entire job when Master actually fails.</p><p>This design is based on two considerations:</p><ol start="1"><li><strong>Single Point Characteristic</strong>: There&#x27;s typically only one Master node in the system, with relatively low failure probability</li><li><strong>Simplified Design</strong>: Simple failure handling mechanisms reduce system complexity</li></ol><p>However, in critical production environments, this simple handling approach obviously cannot meet high availability requirements.</p><p>With the development of distributed systems, more robust Master failure handling mechanisms should be seriously considered.</p><h4 id="semantics-in-the-presence-of-failures">Semantics in the Presence of Failures</h4><p>MapReduce provides a key promise: <strong>In deterministic operations, the results of distributed parallel execution are completely consistent with sequential execution</strong>.</p><p>This feature greatly simplifies the complexity of distributed programs.</p><p>How is this &quot;computational result consistency&quot; achieved?</p><p><strong>Key Mechanism: Atomic Commits</strong></p><p>MapReduce achieves result consistency through carefully designed atomic commit mechanisms:</p><ol start="1"><li>Temporary file strategy</li><li>Task completion flow</li><li>Redundant execution handling</li></ol><p><strong>Core Role of Atomic Operations</strong></p><p>Atomic operations are the foundation of the entire fault tolerance mechanism, mainly reflected in two levels:</p><ol start="1"><li>Master data structure updates</li><li>File system atomic renaming - for example, atomic renaming operations when Reduce tasks complete; only one execution instance can successfully rename, meaning if there&#x27;s a successfully named file, it indicates one instance has been successfully executed</li></ol><h3 id="locality">Locality</h3><p>A commonly used principle in computer science is called <strong>locality of reference</strong> (specifically spatial locality), which states that when a program executes sequentially, after accessing a block of data, it&#x27;s highly likely to access data physically adjacent to that data next. This simple assertion is the foundation for all cache effectiveness, leading to the formation of a storage hierarchy system from slow to fast, cheap to expensive, large to small (hard disk → memory → cache → registers).</p><p>In distributed environments, this hierarchy system requires at least one more layer — network I/O. This is also the first sentence in the paper: &quot;Network bandwidth is a relatively scarce resource in our computing environment.&quot;</p><p>In MapReduce systems, we also fully utilize input data locality. However, this time, instead of loading data over, we <strong>schedule</strong> the program to go there (<strong>Moving Computation is Cheaper than Moving Data</strong>). If input exists on GFS, it manifests as a series of logical blocks, each of which may have several (typically three) physical replicas. For each logical block of input, we can run Map Tasks on machines where one of its physical replicas resides (if it fails, try another replica), thereby minimizing network data transmission and reducing latency while saving bandwidth.</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) 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&#x27;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>Deep analysis of the core design principles, influencing factors, and best practices of Task Granularity in the MapReduce framework.</p><p>The paper has an important viewpoint: &quot;<strong>M and R should be much larger than the number of worker machines</strong>.&quot;</p><h4 id="m-and-r-should-be-much-larger-than-worker-machine-count">M and R Should Be Much Larger Than Worker Machine Count</h4><ol start="1"><li><p><strong>Dynamic Load Balancing</strong></p><pre><code class="lang-shell">// Simplified load balancing scenario
100 Map tasks, 10 machines
- Machines 1-9: Each processes 10 tasks, uniform load
- Machine 10: Slower hardware, only completes 5 tasks
- Task allocator automatically redistributes remaining 5 tasks to machines that completed tasks
</code></pre>
<p>When each machine processes multiple small tasks rather than single large tasks, fast machines can process more tasks while slow machines process fewer, naturally forming performance-based work distribution.</p></li><li><p><strong>Accelerated Failure Recovery</strong></p><pre><code>Assumption:
- 2000 machines, each executing about 100 Map tasks
- Single Worker-37 fails, has completed 92 Map tasks

Impact:
- Traditional design (1 large task per machine): Lose entire Worker-37 computation results
- Fine-grained design: Only need to re-execute 92 small tasks, distributed to other 1999 machines
- Recovery speed: About 1/20 of traditional method (average less than 1 additional task per machine)
</code></pre>
<p>When a worker fails, its completed multiple small tasks can be quickly redistributed to other machines in the cluster for re-execution, significantly accelerating recovery speed.</p></li></ol><h3 id="backup-tasks">Backup Tasks</h3><p>This mechanism was designed by Google to solve the &quot;straggler&quot; problem.</p><blockquote><p>One of the common causes that lengthens the total time taken for a MapReduce operation is a &quot;straggler&quot;: 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>An important optimization introduced to solve slow task problems.</p><h4 id="straggler-problem-key-challenge-in-distributed-systems">Straggler Problem: Key Challenge in Distributed Systems</h4><p>&quot;Stragglers&quot; refer to machines that complete tasks abnormally slowly, severely slowing down the completion time of entire MapReduce jobs. This problem is particularly prominent in large-scale distributed environments.</p><h5 id="main-causes">Main Causes</h5><ol start="1"><li><p><strong>Hardware Issues</strong>:</p><pre><code>- Disk errors: Correctable errors reduce read speed from 30MB/s to 1MB/s
- Network problems: Network card failures cause bandwidth reduction
- CPU or memory failures: Significantly reduced processing capability
</code></pre>
</li><li><p><strong>Resource Competition</strong>:</p><pre><code>- Multi-task scheduling conflicts: Other jobs occupy CPU, memory resources
- I/O contention: Multiple processes compete for disk or network I/O
- Memory pressure: Insufficient memory causes frequent page swapping
</code></pre>
</li><li><p><strong>Software Issues</strong>:</p><pre><code>- Configuration errors: Such as Google&#x27;s processor cache disabled bug (100x performance degradation)
- GC pauses: Long pauses caused by garbage collection
- System updates: Background services or updates consuming resources
</code></pre>
</li></ol><h5 id="impact-of-stragglers">Impact of Stragglers</h5><p>In MapReduce jobs, job completion time is limited by the last completing task. When 99% of tasks complete quickly but a few tasks are abnormally slow, the entire job&#x27;s completion time will be dominated by these slow tasks.</p><p>Google designed the backup task mechanism as a simple yet effective strategy, using moderate resource redundancy to reduce overall execution time.</p><h2 id="refinements">Refinements</h2><p>The paper provides several extensions beyond the basic Mapper and Reducer primitives that have become recognized as standard components: Partitioner, Combiner, and Reader/Writer.</p><h3 id="partitioning-function">Partitioning Function</h3><p>The Partitioning Function in MapReduce is an important extension mechanism connecting the Map and Reduce phases.</p><h4 id="core-concept">Core Concept</h4><p>The partitioning function is a key component in MapReduce that connects the Map and Reduce phases, determining which intermediate key-value pairs are sent to which Reduce task.</p><pre><code class="lang-go">// Basic definition of partitioning function
type PartitionFunc func(key interface{}, numPartitions int) int
</code></pre>
<p>Core functions:</p><ul><li>Determines which Reduce task processes Map output intermediate key-value pairs, ultimately affecting output file organization</li><li>Controls the number and content organization of final output files</li><li>Affects data distribution and load balancing across the cluster</li></ul><h4 id="default-hash-partitioning-mechanism">Default Hash Partitioning Mechanism</h4><p>MapReduce provides a simple and efficient default partitioning strategy:</p><pre><code class="lang-java">// Default hash partitioning implementation
public int getPartition(K key, V value, int numReduceTasks) {
    return (key.hashCode() &amp; Integer.MAX_VALUE) % numReduceTasks;
}
</code></pre>
<p>Characteristics:</p><ul><li><strong>Simple and efficient</strong>: Low computational overhead, suitable for most scenarios</li><li><strong>Relatively balanced</strong>: Uses hash function to evenly distribute keys across partitions</li><li><strong>Deterministic</strong>: Same keys always map to the same partition, ensuring correct aggregation</li></ul><p>Data flow diagram:</p><pre><code class="lang-shell">    Map Output          Partition Function       Reduce Tasks
 [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="custom-partitioning-function-use-cases">Custom Partitioning Function Use Cases</h4><p>The paper points out that certain scenarios require specific partitioning logic, such as when processing URL data where you want all URLs from the same host to go into the same output file.</p><h3 id="combiner-function">Combiner Function</h3><h4 id="core-concept-and-working-principle">Core Concept and Working Principle</h4><p>Combiner is a key optimization component in the MapReduce framework that <strong>performs partial data aggregation on the Map side</strong>, reducing network transmission volume.</p><pre><code>Workflow:
Map output → Combiner local aggregation → Network transmission → Reducer final aggregation
</code></pre>
<p>Applicable conditions:</p><ul><li>Map output intermediate keys have <strong>lots of duplicates</strong></li><li>Reduce function has <strong>commutativity and associativity</strong> (such as sum, max)</li></ul><h4 id="performance-advantage-example">Performance Advantage Example</h4><p>Using word count as an example:</p><pre><code>Without Combiner:
Map1 output: &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 records transmitted
Map2 output: &lt;&quot;hello&quot;,1&gt;, &lt;&quot;hadoop&quot;,1&gt;, &lt;&quot;hello&quot;,1&gt; // 3 records transmitted
Total network transmission: 7 records

With Combiner:
Map1 output: &lt;&quot;hello&quot;,3&gt;, &lt;&quot;world&quot;,1&gt; // 2 records transmitted
Map2 output: &lt;&quot;hello&quot;,2&gt;, &lt;&quot;hadoop&quot;,1&gt; // 2 records transmitted
Total network transmission: 4 records (43% reduction)
</code></pre>
<p>For data following Zipf distribution (like word frequency), Combiner can significantly reduce network transmission and improve performance.</p><h4 id="difference-from-reduce">Difference from Reduce</h4><ul><li>Reducer output writes to final result files</li><li>Combiner output writes to intermediate files, then transmitted to Reducer</li></ul><p>Combiner is an important optimization in the MapReduce framework for improving data processing efficiency through &quot;pre-aggregation,&quot; significantly reducing data transmission and processing time, especially effective for aggregation operations.</p><h3 id="input-and-output-types">Input and Output Types</h3><p>Support for different input data formats:</p><pre><code>1. TextInputFormat (default)
   - Each line as one record
   - Key: line offset (LongWritable)
   - Value: line content (Text)
   - Smart splitting: ensures splitting at line boundaries

2. KeyValueTextInputFormat
   - Split each line into key-value by delimiter (default Tab)
   - Suitable for: simple structured text data

3. SequenceFileInputFormat
   - Read binary sequence files (key-value pairs)
   - Support compression, efficient random access
   - Commonly used for passing data between MapReduce jobs

4. DBInputFormat
   - Read records from relational databases
   - Support SQL queries as data source
</code></pre>
<p>This demonstrates the <strong>flexibility and extensibility</strong> of the framework.</p><p>MapReduce&#x27;s input/output interface design enables it to handle diverse data sources:</p><pre><code>- HDFS files
- Local file systems
- S3, Azure Blob and other cloud storage
- HBase, MongoDB and other NoSQL databases
- Kafka streaming data
</code></pre>
<p>By implementing appropriate InputFormat/OutputFormat, developers can integrate MapReduce with almost any data source/target, demonstrating the framework&#x27;s powerful extensibility and making it suitable for various big data processing scenarios.</p><h2 id="performance">Performance</h2><p>The paper mentions two performance tests representing MapReduce framework&#x27;s capability to handle two typical big data processing scenarios:</p><p><strong>Test Scenario Analysis</strong></p><ol start="1"><li><p><strong>Pattern Search Test</strong>: Search for specific patterns in about 1TB of data</p><ul><li>Represents the &quot;extract small amounts of valuable information from large datasets&quot; computation pattern</li><li>Typical applications include log analysis, anomaly detection, specific record finding, etc.</li></ul></li><li><p><strong>Large Data Sorting Test</strong>: Sort about 1TB of data</p><ul><li>Represents the &quot;transform data from one representation to another&quot; computation pattern</li><li>Typical applications include ETL processes, data preprocessing, data reorganization, etc.</li></ul></li></ol><p>Below, the entire MapReduce distributed framework will be analyzed from five angles:</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-analysis">Cluster Configuration Analysis</h3><p>First, a cluster configuration is given:</p><pre><code class="lang-go">// Cluster configuration overview
type ClusterConfig struct {
    Nodes           int     // About 1800 machines
    CpuPerNode      int     // 2 × 2GHz Intel Xeon per node (with hyperthreading)
    MemoryPerNode   string  // 4GB per node (actual usable 2.5-3GB)
    DisksPerNode    int     // 2 × 160GB IDE disks per node
    NetworkBandwidth string  // Gigabit Ethernet
    NetworkTopology string   // Two-level tree switching network
    RootBandwidth   string   // 100-200Gbps aggregate bandwidth
    Latency         string   // &lt;1ms latency between nodes
}
</code></pre>
<p><strong>Analysis</strong>:</p><ul><li>This is a balanced cluster design for both computation and I/O, particularly suitable for MapReduce&#x27;s divide-and-conquer model</li><li>Storage-wise, each node has over 300GB total storage space, providing sufficient local storage for TB-level data processing</li><li>Network-wise, tree topology is simple but may form bottlenecks during shuffle phase</li><li>Low inter-node latency (&lt;1ms) is extremely beneficial for data transmission in the reduce phase</li><li>Cluster scale (1800 nodes) enables effective parallelization when processing TB-level data</li></ul><h3 id="grep">Grep</h3><p><img alt="Figure2 Data Transfer Rate Over Time" src="Figure2-Data-transfer-rate-over-time.png"/></p><p>This is a typical &quot;extract small amounts of information from massive data&quot; scenario:</p><pre><code class="lang-go">// Grep task configuration
type GrepJobConfig struct {
    InputSize       string  // About 1TB (10^10 100-byte records)
    Pattern         string  // Three-character pattern (matches 92,337 records)
    InputSplits     int     // M=15000 (about 64MB per block)
    ReduceTasks     int     // R=1 (single output file)
    PeakScanRate    string  // &gt;30GB/s (with 1764 workers)
    TotalTime       int     // About 150 seconds (including 60 seconds startup overhead)
}
</code></pre>
<p><strong>Performance Analysis</strong>:</p><ol start="1"><li><strong>Scalability Performance</strong>: From Figure 2, as worker count increases, scan rate linearly improves to 30GB/s, showing excellent horizontal scaling capability for map-intensive tasks</li><li><strong>I/O Bound Characteristics</strong>: Grep is essentially I/O intensive work; the test achieving 30GB/s throughput approaches the theoretical upper limit of total disk I/O for 1764 nodes</li><li><strong>Optimization Opportunities</strong>: About 60 seconds startup overhead (40% of total time) shows an optimization point - GFS metadata operations and task distribution can be further optimized</li><li><strong>R=1 Design</strong>: Single reduce design suits this &quot;filtering&quot; scenario, but also means final result collection could become a bottleneck (not manifested in this example due to small data volume)</li></ol><h3 id="sort">Sort</h3><p><img alt="Figure3 Data Transfer Rates Over Time for Different Executions" src="Figure3-Data-transfer-rates-over-time-for-diff-exec-of-sort-program.png"/></p><p>Comprehensive test of the entire MapReduce framework capability:</p><pre><code class="lang-golang">// Sort task characteristic analysis
type SortJobAnalysis struct {
    InputSize       string  // About 1TB (10^10 100-byte records)
    InputRate       string  // Peak 13GB/s (lower than Grep due to writing intermediate data)
    ShufflePattern  string  // Two-phase pattern, related to reduce task batching
    OutputRate      string  // 2-4GB/s (dual replica writes, actual physical writes 4-8GB/s)
    MapTasks        int     // M=15000 (about 64MB per block)
    ReduceTasks     int     // R=4000 (partitioning strategy leverages key distribution knowledge)
    TotalTime       int     // 891 seconds (close to TeraSort benchmark 1057 seconds)
}
</code></pre>
<p><strong>Technical Analysis</strong>:</p><ol start="1"><li><strong>Data Pipeline</strong>: Test clearly shows MapReduce three-phase pipeline - map phase (0-200 seconds), shuffle phase (200-600 seconds), and reduce phase (600-850 seconds)</li><li><strong>Resource Bottleneck Shifting</strong>:
<ul><li>0-200 seconds: Bottleneck in disk I/O and CPU (parsing data)</li><li>200-600 seconds: Bottleneck shifts to network bandwidth (shuffle)</li><li>600-850 seconds: Bottleneck in sorting computation and output disk I/O</li></ul></li><li><strong>Locality Optimization Effect</strong>: Input rate (13GB/s) higher than shuffle rate mainly due to data locality optimization, with most reads going through local disk rather than network</li><li><strong>Replication Overhead</strong>: Output rate (2-4GB/s) relatively low mainly due to GFS dual replica strategy, actual physical writes are twice this rate</li></ol><h4 id="effect-of-backup-tasks">Effect of Backup Tasks</h4><p>Figure 3 also shows:</p><pre><code class="lang-go">// Backup task impact analysis
type BackupTaskAnalysis struct {
    WithBackup      int     // Normal execution, total time 891 seconds
    WithoutBackup   int     // 1283 seconds, 44% increase
    StragglerDelay  int     // Last 5 reduce tasks took additional 300 seconds
    EfficiencyGain  string  // Backup task mechanism improves performance by 44%
}
</code></pre>
<p><strong>Professional Interpretation</strong>:</p><ol start="1"><li><strong>Severity of Straggler Problem</strong>: Data clearly demonstrates the severity of the &quot;straggler problem&quot; in distributed systems - just 5 slow tasks increased total time by 44%</li><li><strong>Root Cause Analysis</strong>: Stragglers typically stem from:
<ul><li>Hardware anomalies (like disk performance degradation, memory errors)</li><li>Resource competition (like interference from other processes)</li><li>Data skew (some reduce tasks process significantly more data than others)</li></ul></li><li><strong>Cloud-Native Environment Significance</strong>: In shared resource cloud environments, straggler problems are more prevalent; backup task mechanisms are key to ensuring performance predictability</li></ol><h4 id="machine-failures">Machine Failures</h4><p>Figure 3 also shows:</p><pre><code class="lang-go">// Fault tolerance capability analysis
type FaultToleranceAnalysis struct {
    NodesKilled     int     // 200 nodes (about 11.5% of nodes)
    RecoveryPattern string  // Brief negative input rate, then quick recovery
    TotalTime       int     // 933 seconds, only 5% increase
    KeyMechanism    string  // Automatically detect failures and re-execute tasks
}
</code></pre>
<p><strong>Analysis</strong>:</p><ol start="1"><li><strong>Failure Impact Visualization</strong>: Negative input rate in the graph intuitively shows how node failures cause loss of completed work and re-execution requirements</li><li><strong>Quick Recovery Principle</strong>:
<ul><li>Task state tracking: master node continuously tracks each task&#x27;s state</li><li>Heartbeat detection: detect worker failures through periodic heartbeats</li><li>Task rescheduling: reassign failed node tasks to healthy nodes</li><li>Redundant execution: key is MapReduce design allowing any node to handle any task</li></ul></li><li><strong>Comparison with Traditional Systems</strong>: Traditional MPP databases typically fail completely or have 50%+ performance degradation with 11% node failure; MapReduce&#x27;s 5% performance loss highlights its excellent fault tolerance capability</li></ol><h2 id="experience">Experience</h2><p>This content is excerpted from Jeff Dean and Sanjay Ghemawat&#x27;s MapReduce paper, detailing MapReduce&#x27;s early development history and application within Google.</p><p><img alt="Table1 MapReduce Jobs Run in August 2004" src="Table1-MapReduce-jobs-run-in-august-2004.png"/></p><h3 id="technical-development-history">Technical Development History</h3><p>The first version of the MapReduce library was developed in February 2003, with major enhancements in August of the same year, including:</p><ul><li>Locality optimization</li><li>Dynamic load balancing of task execution across worker nodes</li><li>Other performance optimizations</li></ul><h3 id="wide-application-scope">Wide Application Scope</h3><p>MapReduce gained widespread application within Google, covering multiple domains:</p><ol start="1"><li>Large-scale machine learning problems</li><li>Clustering problems for Google News and Froogle (early Google Shopping) products</li><li>Popular query report data extraction (such as Google Zeitgeist)</li><li>Attribute extraction from large-scale web corpora (such as geographical locations for localized search)</li><li>Large-scale graph computations</li></ol><h3 id="explosive-growth">Explosive Growth</h3><p>From the graph, MapReduce usage within Google showed exponential growth:</p><ul><li>Early 2003: Near 0 instances</li><li>End of September 2004: Nearly 900 instances</li></ul><p>This rapid growth indicates MapReduce gained extremely high recognition and application value within Google.</p><h3 id="success-factor-analysis">Success Factor Analysis</h3><p>Key factors for MapReduce success:</p><ol start="1"><li><strong>Simplified Distributed Computing</strong>: Enables developers to write simple programs that run efficiently on thousands of machines</li><li><strong>Accelerated Development Cycles</strong>: Significantly shortened development and prototyping cycles</li><li><strong>Lowered Technical Barriers</strong>: Allows programmers without distributed/parallel systems experience to easily leverage large-scale computing resources</li></ol><h3 id="scale-and-efficiency-analysis-august-2004-data">Scale and Efficiency Analysis (August 2004 Data)</h3><p>From table data, the following insights emerge:</p><ol start="1"><li><strong>Wide Usage</strong>: Executed 29,423 MapReduce jobs in a single month</li><li><strong>High Processing Efficiency</strong>: Average job completion time was 634 seconds (about 10.5 minutes)</li><li><strong>Large Computation Scale</strong>:
<ul><li>Used equivalent of 79,186 machine-days of computation time</li><li>Processed 3,288 TB of input data</li><li>Generated 758 TB of intermediate data</li><li>Output 193 TB of results</li></ul></li><li><strong>Task Distribution Characteristics</strong>:
<ul><li>Each job used average of 157 worker machines</li><li>Average 1.2 worker node failures per job (indicating good fault tolerance)</li><li>Average 3,351 map tasks and 55 reduce tasks per job</li></ul></li><li><strong>Code Reusability</strong>:
<ul><li>395 unique map implementations</li><li>269 unique reduce implementations</li><li>426 unique map/reduce combinations</li></ul></li></ol><h2 id="conclusions">Conclusions</h2><p>The paper&#x27;s conclusions explain why this paper is so famous:</p><h3 id="three-key-success-factors-of-mapreduce">Three Key Success Factors of MapReduce</h3><h4 id="1-simple-and-easy-to-use-programming-model">1. Simple and Easy-to-Use Programming Model</h4><p>MapReduce&#x27;s primary success factor is its concise programming interface:</p><pre><code class="lang-go">// Users only need to define these two functions, without worrying about distributed system complexity
func Map(key, value string) []KeyValue { /* User-defined mapping logic */ }
func Reduce(key string, values []string) string { /* User-defined reduction logic */ }
</code></pre>
<p>This design is extremely developer-friendly because it:</p><ul><li>Hides the complex details of parallelization</li><li>Automatically handles fault tolerance mechanisms</li><li>Built-in locality optimization</li><li>Provides transparent load balancing</li></ul><p>This enables even programmers without distributed systems experience to easily write efficient distributed programs.</p><h4 id="2-powerful-expressiveness">2. Powerful Expressiveness</h4><p>The MapReduce model can easily express various types of computational problems, widely applied within Google for:</p><ul><li>Web search service data generation</li><li>Large-scale sorting</li><li>Data mining</li><li>Machine learning</li><li>Many other systems</li></ul><p>This versatility makes MapReduce a fundamental computing framework within Google.</p><h4 id="3-excellent-scalability">3. Excellent Scalability</h4><p>MapReduce implementation can scale to large clusters containing thousands of machines:</p><pre><code class="lang-go">// Pseudo-code: MapReduce scheduling process
func Schedule(input []string, mappers int, reducers int) Result {
    // Automatically handles:
    // 1. Task allocation and parallelization
    // 2. Machine failure detection and recovery
    // 3. Data locality optimization
    // 4. Intermediate result management
}
</code></pre>
<p>This enables efficient handling of large-scale computational problems encountered by Google, laying the foundation for big data processing.</p><h3 id="three-key-insights-from-research-team">Three Key Insights from Research Team</h3><h4 id="1-value-of-restricted-programming-models">1. Value of Restricted Programming Models</h4><p>Research shows that by consciously restricting the programming model, enormous system advantages can be gained:</p><ul><li>Easy parallelization and distributed computing</li><li>Natural implementation of fault tolerance mechanisms</li><li>Reduced development and maintenance costs</li></ul><p>This &quot;less is more&quot; philosophy contrasts sharply with other systems attempting to provide completely general parallel programming environments.</p><h4 id="2-network-bandwidth-is-a-scarce-resource">2. Network Bandwidth is a Scarce Resource</h4><p>The research team found network bandwidth is a precious resource in distributed systems, so many optimizations target reducing network transmission:</p><ul><li><strong>Locality optimization</strong>: Prioritize reading data from local disks, reducing cross-network data transmission</li><li><strong>Local intermediate data storage</strong>: Write intermediate results to local disks rather than distributed storage, saving network bandwidth</li></ul><p>These designs are particularly important in large-scale clusters, where data transmission can become system bottlenecks.</p><h4 id="3-importance-of-redundant-execution">3. Importance of Redundant Execution</h4><p>Redundant execution is a key innovation in MapReduce, used for:</p><ul><li>Reducing impact of slow machines (stragglers)</li><li>Gracefully handling machine failures</li><li>Preventing data loss</li></ul><pre><code class="lang-go">// Pseudo-code: Redundant task scheduling in MapReduce
func scheduleBackupTasks(slowTasks []Task) {
    for _, task := range slowTasks {
        if time.Now() - task.StartTime &gt; slowThreshold {
            // Launch backup copy of same task on another machine
            launchDuplicateTask(task)
        }
    }
}
</code></pre>
<p>This mechanism significantly improves reliability and performance consistency of large distributed systems.</p><h3 id="technical-legacy-of-mapreduce">Technical Legacy of MapReduce</h3><p>The paper&#x27;s conclusions reveal that MapReduce is not just a technical innovation, but a new paradigm for large-scale data processing:</p><ol start="1"><li><strong>Architectural Influence</strong>: MapReduce design philosophy influenced later big data processing frameworks like Hadoop, Spark</li><li><strong>Programming Model Innovation</strong>: Proved that simplified programming models can solve complex distributed computing problems</li><li><strong>Engineering Practice Revolution</strong>: Changed methods for building large-scale data processing systems, from expert systems to general frameworks</li><li><strong>Commercial Value Creation</strong>: Laid foundation for later big data ecosystems, creating enormous commercial value</li></ol><p>In summary, MapReduce successfully solved core challenges of large-scale distributed data processing through simple yet powerful abstractions, making massive data processing accessible. This is why it achieved tremendous success both within Google and across the industry.</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Distributed System</category>
            <category>MapReduce</category>
        </item>
        <item>
            <title><![CDATA[Gateway Architecture Design for High-Concurrency API Management Scenarios]]></title>
            <link>https://blog.cheverjohn.me/gateway-high-concurrency-api-management</link>
            <guid>https://blog.cheverjohn.me/gateway-high-concurrency-api-management</guid>
            <pubDate>Thu, 27 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Overall Architectural Design Principles In high-concurrency API management scenarios, gateway architecture must adhere to the following core design principles: 1. Layered Architecture Design Adopt a strictly layered architecture with separation of concerns at each layer, making the system more scala...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="overall-architectural-design-principles">Overall Architectural Design Principles</h2><p>In high-concurrency API management scenarios, gateway architecture must adhere to the following core design principles:</p><ol start="1"><li><p><strong>Layered Architecture Design</strong></p><pre><code class="lang-shell">Traffic Ingress Layer → Processing Layer → Routing Layer → Backend Service Layer
</code></pre>
<p>Adopt a strictly layered architecture with separation of concerns at each layer, making the system more scalable horizontally and optimizable vertically.</p></li><li><p><strong>Stateless Design</strong></p><p>Design stateless gateway nodes to ensure any gateway instance can handle any request - this is fundamental to supporting high concurrency. Session state and user information should be stored in distributed caches or dedicated state storage systems.</p></li></ol><h2 id="high-performance-technology-stack-selection">High-Performance Technology Stack Selection</h2><ol start="1"><li><p><strong>Core Technology Selection</strong></p><ol start="1"><li><strong>Data Plane</strong>: Based on high-performance proxies like Envoy, NGINX, or custom components built with Go/Rust</li><li><strong>Control Plane</strong>: Employ efficient configuration management and service discovery mechanisms</li></ol></li><li><p><strong>Asynchronous I/O Model</strong></p><ol start="1"><li>Adopt non-blocking I/O models (such as Go&#x27;s goroutine+channel, Rust&#x27;s tokio, Node.js event loop)</li><li>Avoid traditional thread pool models to reduce context switching overhead</li></ol></li></ol><h2 id="multi-level-caching-architecture">Multi-Level Caching Architecture</h2><ol start="1"><li><p><strong>Global Distributed Cache Layer</strong></p><pre><code class="lang-shell">Client → CDN → Edge Cache → API Gateway Local Cache → Service Cache
</code></pre>
</li><li><p><strong>Multi-Dimensional Caching Strategy</strong></p><ol start="1"><li><strong>Routing Information Cache</strong>: Local high-speed cache with periodic updates</li><li><strong>Authentication Information Cache</strong>: Distributed token validation result caching</li><li><strong>Response Data Cache</strong>: Intelligent caching strategy based on content characteristics</li></ol></li></ol><h2 id="dynamic-scaling-design">Dynamic Scaling Design</h2><ol start="1"><li><p><strong>Flexible Deployment Architecture</strong></p><pre><code class="lang-shell">Multi-Region → Multi-AZ → Multi-Cluster → Multi-Instance
</code></pre>
</li><li><p><strong>Elastic Scaling Strategy</strong></p><ol start="1"><li><strong>Predictive Scaling</strong>: Predict scaling needs based on historical traffic patterns</li><li><strong>Reactive Scaling</strong>: Trigger scaling based on real-time metrics (CPU, memory, request queue depth)</li><li><strong>Graceful Scale-Down</strong>: Ensure graceful connection closure and request completion processing</li></ol></li></ol><h2 id="efficient-traffic-control-mechanisms">Efficient Traffic Control Mechanisms</h2><ol start="1"><li><p><strong>Multi-Level Rate Limiting Design</strong></p><pre><code class="lang-mermaid">flowchart LR
    Client --&gt; GlobalLimit[&quot;Global Rate Limiting Layer&quot;]
    GlobalLimit --&gt; ServiceLimit[&quot;Service-Level Rate Limiting&quot;]
    ServiceLimit --&gt; APILimit[&quot;API-Level Rate Limiting&quot;]
    APILimit --&gt; UserLimit[&quot;User-Level Rate Limiting&quot;]
    UserLimit --&gt; Backend[&quot;Backend Services&quot;]
</code></pre>
</li><li><p><strong>Adaptive Flow Control Algorithms</strong></p><ol start="1"><li><strong>Token Bucket + Leaky Bucket Combination</strong>: Balance burst traffic and steady traffic</li><li><strong>Priority-Based Differential Processing</strong>: Prioritize core API protection</li><li><strong>Adaptive Rate Limiting</strong>: Dynamically adjust rate limiting thresholds based on backend service health</li></ol></li></ol><h2 id="gateway-cluster-high-availability-design">Gateway Cluster High Availability Design</h2><ol start="1"><li><p><strong>Multi-Region Deployment Architecture</strong></p><ol start="1"><li><strong>Geographic-Level Redundancy</strong>: Cross-region deployment ensures regional-level fault isolation</li><li><strong>Proximity Access</strong>: Intelligent DNS or global load balancing for traffic proximity routing</li></ol></li><li><p><strong>Fault Isolation Strategy</strong></p><pre><code class="lang-shell">Client Grouping → Gateway Instance Grouping → Backend Service Grouping
</code></pre>
<ol start="1"><li><strong>Bulkhead Pattern</strong>: Isolate client requests to different gateway instance groups</li><li><strong>Circuit Breaker Mechanism</strong>: Intelligent circuit breaker design based on multi-dimensional metrics like error rates and latency</li><li><strong>Degradation Strategy</strong>: Define clear service degradation paths and fallback mechanisms</li></ol></li></ol><h2 id="request-processing-optimization">Request Processing Optimization</h2><ol start="1"><li><p><strong>Request Processing Pipeline</strong></p><pre><code class="lang-shell">Request Reception → Authentication &amp; Authorization → Request Transformation → 
Routing Decision → Load Balancing → Backend Invocation → Response Processing
</code></pre>
</li><li><p><strong>Performance Optimization Techniques</strong></p><ol start="1"><li><strong>Batch Processing</strong>: Merge fragmented requests to reduce network round trips</li><li><strong>Request Collapsing</strong>: Merge concurrent requests for the same resource</li><li><strong>Parallel Processing</strong>: Parallelize cross-service request processing</li><li><strong>Streaming Response Processing</strong>: Stream transmission for large responses</li><li><strong>Zero-Copy Technology</strong>: Reduce data copying stages</li></ol></li></ol><h2 id="efficient-communication-protocols">Efficient Communication Protocols</h2><ol start="1"><li><p><strong>Protocol Support and Optimization</strong></p><ol start="1"><li><strong>HTTP/2 Multiplexing</strong>: Reduce connection establishment overhead</li><li><strong>gRPC Support</strong>: Efficient binary transmission and stream processing</li><li><strong>WebSocket Optimization</strong>: Long connection management and heartbeat mechanisms</li></ol></li><li><p><strong>Connection Pool Management</strong></p><ol start="1"><li>Dynamically adjustable backend connection pools</li><li>Long connection reuse and keep-alive strategies</li><li>Connection warm-up mechanisms to avoid cold start latency</li></ol></li></ol><h2 id="end-to-end-observability">End-to-End Observability</h2><ol start="1"><li><p><strong>Multi-Dimensional Monitoring System</strong></p><pre><code class="lang-shell">Infrastructure Metrics → Gateway Performance Metrics → API Call Metrics → Business Metrics
</code></pre>
</li><li><p><strong>Real-Time Monitoring and Alerting</strong></p><ol start="1"><li><strong>Health Checks</strong>: Combination of active and passive health detection</li><li><strong>Performance Analysis</strong>: Key metrics like request latency distribution and queue depth</li><li><strong>Anomaly Detection</strong>: Machine learning-based abnormal behavior identification</li></ol></li></ol><h2 id="dynamic-configuration-update-mechanism">Dynamic Configuration Update Mechanism</h2><ol start="1"><li><p><strong>Dynamic Configuration Architecture</strong></p><ol start="1"><li>Distributed configuration center + local cache</li><li>Configuration change event notification mechanism</li><li>Incremental configuration updates to reduce resource consumption</li></ol></li><li><p><strong>Canary Release Capabilities</strong></p><ol start="1"><li>Canary deployment for configuration changes</li><li>Smooth traffic migration switching</li><li>Emergency rollback mechanisms</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[Challenges in Microservices Architecture: The Redemption Path of API Gateway]]></title>
            <link>https://blog.cheverjohn.me/gateway-api-management-in-multi-scenories</link>
            <guid>https://blog.cheverjohn.me/gateway-api-management-in-multi-scenories</guid>
            <pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[While browsing through technical articles, I came across this insightful piece on microservices architecture, which resonated with our company's actual business scenarios. Challenges at Scale The article discusses how client applications access microservices in a distributed architecture. The main c...]]></description>
            <content:encoded><![CDATA[<article><div><p>While browsing through technical articles, I came across this insightful <a href="https://microservices.io/patterns/apigateway.html">piece</a> on microservices architecture, which resonated with our company&#x27;s actual business scenarios.</p><h2 id="challenges-at-scale">Challenges at Scale</h2><p>The article discusses how client applications access microservices in a distributed architecture. The main challenges include:</p><ol start="1"><li><strong>API Granularity Mismatch</strong>: Microservices typically expose fine-grained APIs, while clients need aggregated data, forcing multiple service interactions</li><li><strong>Client Requirement Diversity</strong>: Different clients (desktop browsers, mobile apps, third-party applications) require different data structures and payloads</li><li><strong>Network Performance Variations</strong>: Mobile networks are typically slower with higher latency compared to internal networks, impacting user experience</li><li><strong>Dynamic Service Instance Changes</strong>: Service instances and their locations (host + port) change dynamically</li><li><strong>Protocol Heterogeneity</strong>: Backend services may use different protocols, some of which are not web-friendly</li></ol><p>I&#x27;ll skip the detailed explanations here - feel free to reach out for deeper technical discussions.</p><h2 id="solution-architecture">Solution Architecture</h2><p>The author proposes implementing the API Gateway pattern as the solution:</p><ol start="1"><li><strong>Unified Entry Point</strong>: Design an API gateway as the single entry point for all clients</li><li><strong>Request Processing Strategies</strong>:
<ul><li>Simple proxy/routing to corresponding services</li><li>Fan-out requests to multiple services with result aggregation</li></ul></li><li><strong>Client-Specific APIs</strong>: Provide customized APIs for different clients rather than one-size-fits-all solutions</li><li><strong>Cross-Cutting Concerns</strong>: Implement authentication, authorization, SSL termination, caching, and other shared functionalities</li></ol><p><strong>The article also introduces a pattern variant: Backends for Frontends (BFF), which involves designing dedicated API gateways for each client type (web applications, mobile apps, third-party applications).</strong></p><h2 id="personal-analysis">Personal Analysis</h2><h3 id="advantages"><strong>Advantages</strong></h3><p><strong>Centralized Management</strong>: All API traffic flows through a single entry point, enabling unified monitoring, tracing, and governance</p><pre><code class="lang-shell">A financial institution reduced security incident response time from hours to minutes 
after implementation, as all suspicious traffic could be quickly identified at the gateway layer.
</code></pre>
<p><strong>Unified Cross-Cutting Concerns</strong>: Authentication, authorization, rate limiting, and other functionalities need to be implemented only once at the gateway layer</p><p><strong>Protocol Translation &amp; Aggregation</strong>: Convert internal protocols to unified external interfaces, reducing client request counts. An e-commerce platform reduced mobile app loading time by 68% by aggregating 7 independent calls into a single API</p><p><strong>Decoupling Implementation</strong>: Clients are decoupled from internal microservice structure, allowing services to evolve independently without affecting clients</p><p><strong>Traffic Control</strong>: Provides intelligent routing, load balancing, and circuit breaker capabilities, enhancing system resilience</p><p>These benefits are well-established in the industry.</p><h3 id="disadvantages"><strong>Disadvantages</strong></h3><p><strong>Potential Single Point of Failure</strong>: All traffic depends on a single component, which could become a system bottleneck if not properly designed - though this depends on the distributed architecture team&#x27;s capabilities</p><p><strong>Increased Latency</strong>: Additional network hops may increase response times (typically 5-10ms)</p><p><strong>Complexity Escalation</strong>: As scale increases, the gateway may become bloated and difficult to maintain</p><blockquote><p>A large enterprise&#x27;s API gateway accumulated over 200 custom processing rules, 
eventually reaching a point where no one dared to modify the codebase.</p></blockquote>
<p><strong>Team Collaboration Challenges</strong>: Gateway changes require cross-team coordination, potentially creating development bottlenecks</p><h3 id="applicable-scenarios">Applicable Scenarios</h3><ol start="1"><li><strong>Small to Medium Microservices Architecture</strong>: Moderate service count (10-50 services) with limited client types</li><li><strong>High-Security Systems</strong>: Financial, healthcare, and other domains requiring unified security policies</li><li><strong>Mixed Protocol Environments</strong>: Internal services using different protocols (REST, gRPC, AMQP, etc.)</li><li><strong>Centralized API Governance</strong>: Enterprise applications requiring centralized API governance strategies</li></ol><h2 id="core-functionality--implementation-effects">Core Functionality &amp; Implementation Effects</h2><p>The author outlines three core functions provided by API gateways:</p><ol start="1"><li><strong>Reverse Proxy/Gateway Routing</strong>: Using Layer 7 routing to redirect HTTP requests</li><li><strong>Request Aggregation</strong>: Aggregating multiple internal microservice requests into single client requests</li><li><strong>Cross-Cutting Concerns &amp; Gateway Offloading</strong>: Centralized implementation of common functionalities</li></ol><h3 id="benefits">Benefits</h3><ol start="1"><li><strong>Client-Microservice Decoupling</strong>: Isolates clients from microservice architecture details</li><li><strong>Optimal API Provision</strong>: Provides customized APIs for various client types</li><li><strong>Reduced Request Count</strong>: Single requests retrieve multi-service data, reducing network overhead</li><li><strong>Simplified Client Logic</strong>: Complex call logic migrates from client to API gateway</li><li><strong>Protocol Translation</strong>: Converts public web-friendly API protocols to internal protocols</li></ol><h3 id="drawbacks">Drawbacks</h3><ol start="1"><li><strong>Increased Complexity</strong>: Introduces a new component requiring development, deployment, and management</li><li><strong>Increased Response Time</strong>: Additional network hops may increase latency (minimal impact for most applications)</li><li><strong>Single Point of Failure Risk</strong>: A single API gateway may become a bottleneck or single point of failure</li></ol><p>These align with my initial assessment.</p><h2 id="production-deployment-strategies">Production Deployment Strategies</h2><pre><code class="lang-shell">┌───────────┐  ┌───────────┐  ┌─────────────┐
│  Web BFF  │  │ Mobile BFF│  │ Third-party │
│           │  │           │  │   BFF       │
└─────┬─────┘  └─────┬─────┘  └─────┬───────┘
      │              │              │
      ▼              ▼              ▼
┌─────────────────────────────────────┐
│        Core API Gateway             │
│ (Auth, Monitoring, Rate Limiting,   │
│      Basic Routing)                 │
└─────────────────────────────────────┘
      │              │              │
      ▼              ▼              ▼
┌─────────┐     ┌───────────┐     ┌───────────┐
│Service  │     │Service    │     │Service    │
│Cluster 1│     │Cluster 2  │     │Cluster 3  │
└─────────┘     └───────────┘     └───────────┘
</code></pre>
<h3 id="scale-considerations">Scale Considerations</h3><ol start="1"><li><strong>Small Applications (5-15 microservices)</strong>:
<ul><li>Recommendation: Single API gateway</li><li>Rationale: Simple and efficient, avoiding over-engineering</li></ul></li><li><strong>Medium Applications (15-50 microservices)</strong>:
<ul><li>Recommendation: Basic API gateway + key BFFs</li><li>Rationale: Balances complexity and client optimization needs</li></ul></li><li><strong>Large Applications (50+ microservices)</strong>:
<ul><li>Recommendation: Complete three-tier architecture (Edge Gateway + BFF Layer + Internal Gateway)</li><li>Rationale: Supports organizational scaling and complex business domains</li></ul></li></ol><h3 id="decision-factors">Decision Factors</h3><p>When selecting API gateway strategies, consider:</p><ol start="1"><li><strong>Organizational Structure</strong>: Conway&#x27;s Law suggests system design often reflects organizational structure</li><li><strong>Scaling Expectations</strong>: Consider business growth and diversification over 3-5 years</li><li><strong>Operational Capabilities</strong>: Assess team capacity for managing multiple gateways</li><li><strong>Consistency Requirements</strong>: Business requirements for API behavior consistency</li><li><strong>Performance Budget</strong>: Evaluate latency impact of additional network hops</li></ol><p><strong>Regardless of the chosen pattern, maintain lightweight gateway layers, avoid excessive business logic drift leading to &quot;smart pipes&quot; anti-patterns, and establish robust monitoring systems to ensure gateway layer performance and stability.</strong></p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Gateway</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Gateway Performance Bottleneck Analysis and Optimization Techniques (Private Collection)]]></title>
            <link>https://blog.cheverjohn.me/performance-bottleneck-analysis-and-optimization-tech</link>
            <guid>https://blog.cheverjohn.me/performance-bottleneck-analysis-and-optimization-tech</guid>
            <pubDate>Thu, 18 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[2025-03-07 Update: Recently, Xiaohongshu's gateway practices follow many of the principles below. —— 《Xiaohongshu Launches Self-Developed Rust High-Performance Layer 7 Gateway ROFF》 This article is divided into two parts: 1. Performance Bottleneck Analysis Methods 2. Hands-on Analysis of APISIX, Kon...]]></description>
            <content:encoded><![CDATA[<article><div><p>2025-03-07 Update: Recently, Xiaohongshu&#x27;s gateway practices follow many of the principles below. —— <a href="https://mp.weixin.qq.com/s/wnkYr4qKIFmh9E9H_XcwTA">《Xiaohongshu Launches Self-Developed Rust High-Performance Layer 7 Gateway ROFF》</a></p><p>This article is divided into two parts:</p><ol start="1"><li><a href="#performance-bottleneck-analysis-methods">Performance Bottleneck Analysis Methods</a></li><li><a href="#hands-on-analysis">Hands-on Analysis of APISIX, Kong, and Nginx</a></li></ol><h2 id="performance-bottleneck-analysis-methods">Performance Bottleneck Analysis Methods</h2><h3 id="performance-bottleneck-analysis-methods-the-real-deal">Performance Bottleneck Analysis Methods (The Real Deal)</h3><p>This section covers the actual methods.</p><h4 id="monitoring-metrics-analysis"><strong>Monitoring Metrics Analysis</strong></h4><ul><li>Latency Metrics: Monitor request latency and integration latency</li><li>Error Rate Metrics: Monitor 4XX and 5XX error rates to identify system failure points</li><li>Throughput Metrics: Request count and requests per second (RPS)</li><li>Cache Hit Metrics: Monitor CacheHitCount and CacheMissCount ratios</li></ul><h4 id="system-resource-monitoring"><strong>System Resource Monitoring</strong></h4><ul><li>CPU utilization analysis</li><li>Memory consumption analysis</li><li>Network I/O monitoring</li><li>Disk I/O monitoring (especially for logs and cache)</li></ul><h4 id="request-flow-analysis"><strong>Request Flow Analysis</strong></h4><ul><li>Request processing chain tracing</li><li>Hot path identification</li><li>Serial processing bottleneck discovery</li></ul><h3 id="infrastructure-level-optimization">Infrastructure-Level Optimization</h3><h4 id="hardware-resource-optimization">Hardware Resource Optimization</h4><ul><li>Increase CPU cores and memory capacity</li><li>Use SSD storage to improve I/O speed</li><li>Deploy high-performance network cards to reduce network latency</li><li>Properly evaluate and adjust container resource limits (in Kubernetes environments)</li></ul><h4 id="network-optimization">Network Optimization</h4><ul><li>Optimize network topology structure</li><li>Reduce network hops</li><li>Use CDN to accelerate static content</li><li>Implement DNS optimization</li><li>Deploy gateways and backend services in the same network zone</li></ul><h4 id="operating-system-tuning">Operating System Tuning</h4><ul><li>Adjust TCP/IP stack parameters (such as increasing connection queues)</li><li>Optimize file descriptor limits</li><li>Adjust kernel parameters (such as somaxconn, tcp<em>fin</em>timeout)</li><li>Optimize client thread count in Linux environments</li></ul><h3 id="gateway-configuration-level-optimization">Gateway Configuration-Level Optimization</h3><h4 id="connection-pool-optimization">Connection Pool Optimization</h4><ul><li>Configure appropriate database connection pool sizes (recommended to match expected client count) —— <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">Oracle Database Administrator Essentials</a></li><li>Adjust backend service connection pool parameters</li><li>Set connection timeout and retry strategies</li><li>Implement connection keep-alive mechanisms</li></ul><h4 id="http-optimization">HTTP Optimization</h4><ul><li>Enable HTTP keep-alive to reuse TCP connections —— <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">Oracle Performance Practices</a></li><li>Configure appropriate chunked encoding strategies</li><li>Adjust HTTP header size limits</li><li>Set reasonable request/response timeout values</li></ul><h4 id="caching-strategy-optimization">Caching Strategy Optimization</h4><ul><li>Implement response caching to reduce backend calls —— <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-optimize.html">Amazon API Optimization Practices</a></li><li>Configure cache key strategies and TTL (Time To Live)</li><li>Implement multi-level caching strategies</li><li>Optimize cache validation and invalidation strategies</li></ul><h3 id="request-processing-optimization">Request Processing Optimization</h3><h4 id="concurrent-processing-optimization">Concurrent Processing Optimization</h4><ul><li>Increase worker thread count</li><li>Implement asynchronous processing patterns</li><li>Optimize thread pool configuration</li><li>Use event-driven architecture for high-concurrency request handling</li></ul><h4 id="load-balancing-strategies">Load Balancing Strategies</h4><ul><li>Implement intelligent load balancing algorithms (weighted round-robin, least connections, etc.)</li><li>Configure dynamic load balancing strategies</li><li>Implement service health checks and automatic failover</li><li>Adjust weights based on backend service capacity</li></ul><h4 id="routing-optimization">Routing Optimization</h4><ul><li>Optimize routing lookup algorithms</li><li>Implement routing caching</li><li>Configure routing warm-up strategies</li><li>Implement intelligent routing based on traffic characteristics</li></ul><h3 id="data-processing-optimization">Data Processing Optimization</h3><h4 id="message-processing-optimization">Message Processing Optimization</h4><ul><li>Configure &quot;spill to disk&quot; strategies for large messages (e.g., set &gt;4MB messages to write to disk) <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>Implement request/response compression <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-optimize.html">1</a></li><li>Optimize serialization/deserialization processes</li><li>Reduce unnecessary data transformations</li></ul><h4 id="protocol-optimization">Protocol Optimization</h4><ul><li>Use HTTP/2 to reduce latency and improve parallel processing capability</li><li>Use WebSocket and other long-connection protocols in appropriate scenarios</li><li>Consider using gRPC to improve microservice communication efficiency</li><li>Implement protocol upgrade strategies</li></ul><h3 id="monitoring-and-logging-optimization">Monitoring and Logging Optimization</h3><h4 id="log-optimization">Log Optimization</h4><ul><li>Reduce unnecessary trace information (set production environment to ERROR or FATAL level) <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>Disable or reduce access log recording <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>Disable transaction logs to reduce disk I/O burden <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>Implement asynchronous log writing strategies</li></ul><h4 id="monitoring-optimization">Monitoring Optimization</h4><ul><li>Disable real-time monitoring to reduce overhead <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>Disable or reduce traffic monitoring <a href="https://docs.oracle.com/cd/E55956_01/doc.11123/administrator_guide/content/admin_performance.html">3</a></li><li>Configure reasonable monitoring sampling rates</li><li>Implement intelligent alerting thresholds to avoid excessive monitoring system load</li></ul><h3 id="security-performance-optimization">Security Performance Optimization</h3><h4 id="authentication-optimization">Authentication Optimization</h4><ul><li>Cache authentication results</li><li>Use lightweight token validation</li><li>Implement tiered authentication strategies</li><li>Optimize JWT processing workflows</li></ul><h4 id="ssltls-optimization">SSL/TLS Optimization</h4><ul><li>Use session reuse to reduce handshake overhead</li><li>Configure OCSP stapling</li><li>Implement TLS connection pools</li><li>Select efficient cipher suites</li></ul><h3 id="advanced-optimization-strategies">Advanced Optimization Strategies</h3><h4 id="circuit-breaking-and-rate-limiting">Circuit Breaking and Rate Limiting</h4><ul><li>Implement request rate limiting to protect backend systems</li><li>Configure circuit breakers to prevent system overload</li><li>Implement backoff algorithms for retry handling</li><li>Traffic shaping to optimize request distribution</li></ul><h4 id="service-mesh-integration">Service Mesh Integration</h4><ul><li>Integrate with service meshes like Istio/Envoy</li><li>Delegate some gateway functions to sidecar proxies</li><li>Implement collaborative strategies between gateways and service meshes</li><li>Leverage traffic management capabilities provided by service meshes</li></ul><h4 id="architectural-optimization">Architectural Optimization</h4><ul><li>Consider multi-tier gateway architecture (edge gateway + internal gateway)</li><li>Implement edge computing to reduce latency</li><li>Domain-driven API gateway design</li><li>Gateway sharding based on traffic characteristics</li></ul><p>By systematically analyzing performance bottlenecks in the above aspects and applying corresponding optimization techniques, gateway performance, throughput, and reliability can be significantly improved. The key is to select the most suitable combination of optimization strategies based on actual system characteristics and business requirements, and maintain high system performance through continuous monitoring and tuning.</p><h2 id="hands-on-analysis">Hands-on Analysis</h2><h3 id="nginx-gateway">Nginx Gateway</h3><h4 id="main-performance-bottlenecks">Main Performance Bottlenecks</h4><ol start="1"><li><p><strong>Connection Handling Capacity Bottlenecks</strong></p><ul><li>Default worker process configuration may not match server CPU cores</li><li>Connection pool size limits cause connection rejection under high concurrency</li><li>File descriptor limits cause &quot;too many open files&quot; errors</li></ul></li><li><p><strong>Configuration Complexity Bottlenecks</strong></p><ul><li>Static configuration files require manual modification and reloading</li><li>Large-scale routing rules make configuration maintenance difficult</li><li>Configuration changes require reloading, potentially causing request interruptions</li></ul></li><li><p><strong>SSL Processing Bottlenecks</strong></p><ul><li>High SSL handshake overhead, CPU usage spikes under high concurrency</li><li>Inefficient key exchange algorithms</li><li>Improper session cache configuration causes repeated handshakes</li></ul></li></ol><h4 id="optimization-methods">Optimization Methods</h4><ol start="1"><li><p><strong>Worker Process and Connection Optimization</strong></p><ul><li>Set <code>worker_processes</code> to match CPU core count</li><li>Increase <code>worker_connections</code> value (typically set to 4096 or higher)</li><li>Use <code>worker_cpu_affinity</code> to bind worker processes to specific CPU cores</li><li>Adjust system file descriptor limits (ulimit -n)</li></ul></li><li><p><strong>Event Processing Optimization</strong></p><ul><li>Enable <code>multi_accept</code> and <code>accept_mutex</code></li><li>Use <code>epoll</code> event handling model (on Linux systems)</li><li>Adjust <code>worker_aio_requests</code> to improve async I/O performance</li></ul></li><li><p><strong>HTTP Optimization</strong></p><ul><li>Configure <code>keepalive_timeout</code> and <code>keepalive_requests</code> parameters</li><li>Enable <code>sendfile</code>, <code>tcp_nopush</code>, and <code>tcp_nodelay</code> options</li><li>Implement <code>gzip</code> compression to reduce transmitted data</li><li>Set <code>client_body_buffer_size</code> and <code>client_max_body_size</code> to limit request size</li></ul></li><li><p><strong>SSL Performance Optimization</strong></p><ul><li>Enable <code>ssl_session_cache shared</code> to improve session reuse rate</li><li>Configure OCSP stapling to reduce handshake latency</li><li>Use ECC certificates to reduce computational overhead</li><li>Prioritize high-performance cipher suites (like AES-GCM)</li></ul></li></ol><h3 id="kong-gateway">Kong Gateway</h3><p>A reliable and well-established gateway.</p><h4 id="main-performance-bottlenecks">Main Performance Bottlenecks</h4><ol start="1"><li><p><strong>Database Dependency Bottlenecks</strong></p><ul><li>PostgreSQL/Cassandra database queries become performance bottlenecks</li><li>Database pressure increases during configuration changes</li><li>Database consistency challenges in distributed deployments</li></ul></li><li><p><strong>Lua Script Processing Bottlenecks</strong></p><ul><li>Overly long plugin execution chains increase request latency <a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">1</a></li><li>Improper Lua VM memory usage causes performance degradation</li><li>JIT compilation limitations affect dynamic script performance</li></ul></li><li><p><strong>JWT Validation Bottlenecks</strong></p><ul><li>High JWT validation processing overhead, noticeable in high-percentile latency <a href="https://www.f5.com/company/blog/nginx/benchmarking-api-management-solutions-nginx-kong-amazon-real-time-apis">2</a></li><li>Kong&#x27;s 99.99 percentile latency can reach 3 times that of NGINX <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="optimization-methods">Optimization Methods</h4><ol start="1"><li><p><strong>Database Optimization</strong></p><ul><li>Use DB-less mode to reduce database dependencies</li><li>Increase database connection pool size (pg_pool parameter in kong.conf)</li><li>Implement database read-write separation (master-slave architecture)</li><li>Consider using declarative configuration instead of database storage</li></ul></li><li><p><strong>Plugin Chain Optimization</strong></p><ul><li>Enable only necessary plugins to reduce processing chain length</li><li>Adjust plugin execution order (lightweight, high-frequency plugins first)</li><li>Configure independent caches for plugins (like Redis cache for rate-limiting plugin)</li><li>Monitor and optimize long-running plugins</li></ul></li><li><p><strong>Cache Optimization</strong></p><ul><li>Configure <code>lua_shared_dict</code> cache size</li><li>Adjust plugin-level cache TTL</li><li>Use external Redis cache to improve hit rates</li><li>Configure entity cache to reduce database queries</li></ul></li><li><p><strong>Connection Pool Optimization</strong></p><ul><li>Adjust upstream_keepalive parameter (typically 100-200)</li><li>Increase nginx<em>upstream</em>keepalive_timeout value</li><li>Set reasonable nginx<em>upstream</em>keepalive_requests value</li><li>Increase nginx<em>http</em>client<em>body</em>buffer_size for large request bodies</li></ul></li></ol><h3 id="apisix-gateway">APISIX Gateway</h3><h4 id="main-performance-bottlenecks">Main Performance Bottlenecks</h4><ol start="1"><li><p><strong>etcd Dependency Bottlenecks</strong></p><ul><li>etcd cluster stability affects gateway configuration propagation</li><li>Frequent configuration changes increase etcd pressure</li><li>etcd read-write latency affects dynamic routing updates</li></ul></li><li><p><strong>Route Matching Bottlenecks</strong></p><ul><li>Large numbers of fine-grained routes increase matching latency</li><li>Complex regex routes reduce matching efficiency</li><li>Untimely route cache updates cause routing errors</li></ul></li></ol><h4 id="optimization-methods">Optimization Methods</h4><ol start="1"><li><p><strong>etcd Optimization</strong></p><ul><li>Build highly available etcd clusters</li><li>Optimize etcd configuration (like setting reasonable heartbeat intervals)</li><li>Implement etcd sharding to reduce single-node pressure</li><li>Increase config_center.timeout parameter value (default 30 seconds)</li></ul></li><li><p><strong>Route Optimization</strong></p><ul><li>Use prefix matching instead of full regex</li><li>Increase route cache TTL</li><li>Reduce route rule complexity, split overly complex rules</li><li>Use domain or hostname pre-filtering</li></ul></li></ol><h3 id="envoy-gateway">Envoy Gateway</h3><h4 id="main-performance-bottlenecks">Main Performance Bottlenecks</h4><ol start="1"><li><p><strong>xDS Configuration Update Bottlenecks</strong></p><ul><li>Dynamic configuration updates cause resource reallocation overhead</li><li>Control Plane communication latency affects configuration delivery</li><li>Large numbers of listener and cluster configurations cause high memory usage</li></ul></li><li><p><strong>Filter Chain Processing Bottlenecks</strong></p><ul><li>Long HTTP filter chains increase processing latency</li><li>Complex filter logic causes high CPU usage</li><li>Lua filters are less efficient than native filters</li></ul></li></ol><h4 id="optimization-methods">Optimization Methods</h4><ol start="1"><li><p><strong>xDS Configuration Optimization</strong></p><ul><li>Implement incremental xDS to reduce configuration update overhead</li><li>Optimize Control Plane communication (use gRPC streams instead of polling)</li><li>Set reasonable configuration cache TTL</li><li>Use Aggregated Discovery Service (ADS) to ensure configuration consistency</li></ul></li><li><p><strong>Filter Optimization</strong></p><ul><li>Reduce filter chain length, keep only necessary filters</li><li>Prioritize native C++ filters over Lua or WASM</li><li>Adjust filter execution order (high-frequency filters first)</li><li>Enable statistical monitoring for critical filters</li></ul></li></ol><h3 id="performance-comparison-and-selection-recommendations-for-different-gateways">Performance Comparison and Selection Recommendations for Different Gateways</h3><h4 id="performance-comparison">Performance Comparison</h4><ul><li>In standard API call tests, NGINX API Management Module performance can be 2x better than Kong <a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">Data Support</a></li><li>In terms of latency, NGINX adds 20-30% lower latency than Kong <a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">Data Support</a></li><li>In CPU efficiency, NGINX is about 40% more efficient than Kong <a href="https://www.f5.com/company/blog/nginx/nginx-controller-api-management-module-vs-kong-performance-comparison">Data Support</a></li><li>In JWT validation scenarios, NGINX can handle 2x more API calls than Kong <a href="https://www.f5.com/company/blog/nginx/benchmarking-api-management-solutions-nginx-kong-amazon-real-time-apis">Data Support</a></li></ul><h4 id="selection-recommendations">Selection Recommendations</h4><ol start="1"><li><p><strong>NGINX Suitable Scenarios</strong>:</p><ul><li>Stable API gateway needs with static routing configuration</li><li>Scenarios prioritizing performance and low latency</li><li>Lightweight gateway needs in resource-constrained environments</li><li>Primarily providing reverse proxy and load balancing functions</li></ul></li><li><p><strong>Kong Suitable Scenarios</strong>:</p><ul><li>Need rich API management features (authentication, rate limiting, transformation, etc.)</li><li>Teams pursuing development convenience (RESTful API configuration)</li><li>Microservice architectures with dynamic routing needs</li><li>Can accept some performance loss for feature richness</li></ul></li><li><p><strong>APISIX Suitable Scenarios</strong>:</p><ul><li>Teams pursuing balance between dynamic routing and high performance</li><li>Cloud-native architectures with service discovery integration needs</li><li>Scenarios requiring fine-grained traffic control</li></ul></li><li><p><strong>Envoy Suitable Scenarios</strong>:</p><ul><li>Kubernetes/Istio service mesh infrastructure</li><li>Modern cloud architectures requiring advanced observability</li><li>DevOps teams pursuing programmability and extensibility</li></ul></li></ol><p>By understanding the performance bottleneck characteristics and optimization methods of different gateways, you can choose the most suitable gateway type based on your business characteristics and technology stack, and implement targeted performance optimization to achieve the best balance of gateway performance and functionality.</p><h2 id="summary">Summary</h2><p>Gateway performance optimization is systematic work that requires analysis and optimization from multiple levels. Through the analysis methods, optimization techniques, and code examples provided above, you can implement targeted optimization for different performance bottlenecks. The key points are:</p><ol start="1"><li>Establish comprehensive performance monitoring systems to detect performance bottlenecks promptly <a href="https://learn.microsoft.com/en-us/data-integration/gateway/service-gateway-performance">1</a></li><li>Adopt multi-level caching strategies to reduce redundant calculations and network requests</li><li>Optimize connection pool management to improve connection reuse efficiency</li><li>Implement efficient load balancing algorithms for intelligent request distribution</li><li>Use regular benchmark testing to continuously evaluate and optimize performance</li></ol><p>For gateways in cloud-native environments, you can also consider leveraging Kubernetes&#x27; auto-scaling capabilities to dynamically adjust gateway instance counts in response to traffic changes.</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Gateway</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[How the Kernel Receives Network Packets]]></title>
            <link>https://blog.cheverjohn.me/network-stack</link>
            <guid>https://blog.cheverjohn.me/network-stack</guid>
            <pubDate>Tue, 27 Feb 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Overall Architecture Overview Thanks to Zhang Yanfei for the diagram. After you basically understand what network card drivers, hardware interrupts, software interrupts, and ksoftirqd threads are, you can provide a kernel packet reception path diagram as shown above. The general process is as follow...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="overall-architecture-overview">Overall Architecture Overview</h2><p><img alt="image-20250310194742752" src="theWholeLand.png"/></p><blockquote><p>Thanks to Zhang Yanfei for the diagram.</p></blockquote>
<p>After you basically understand what network card drivers, hardware interrupts, software interrupts, and ksoftirqd threads are, you can provide a kernel packet reception path diagram as shown above.</p><p>The general process is as follows:</p><ol start="1"><li>When the network card receives data, it writes the received frames to memory using DMA, then sends an interrupt to the CPU to notify that data has arrived.</li><li>When the CPU receives the interrupt request, it calls the interrupt handler registered by the network device driver.</li><li>The network card&#x27;s interrupt handler doesn&#x27;t do much work - it issues a software interrupt request and quickly releases CPU resources.</li><li>The ksoftirqd kernel thread detects the software interrupt request, calls poll to start polling for packet reception, and forwards received packets to various protocol stack layers for processing. For TCP packets, they are placed in the user socket&#x27;s receive queue.</li></ol><h2 id="foundation-work-before-everything-else">Foundation Work Before Everything Else</h2><p>Before Linux drivers, kernel protocol stacks, and other modules can receive network card data packets, extensive preparation work must be done:</p><ol start="1"><li>Pre-create ksoftirqd kernel threads;</li><li>Register processing functions for various protocols;</li><li>Pre-initialize the network device subsystem;</li><li>Start up the network card.</li></ol><h3 id="initialization-work">Initialization Work</h3><h4 id="creating-ksoftirqd-kernel-threads">Creating ksoftirqd Kernel Threads</h4><p>All Linux software interrupts are handled in dedicated kernel threads (ksoftirqd), so it&#x27;s essential to understand how these threads are initialized.</p><p>First, there isn&#x27;t just one thread, but N threads, where N equals the number of cores on your machine.</p><p>During system initialization, the <code>smpboot_register_percpu_thread</code> function is called in <code>kernel/smpboot.c</code>, which further executes <code>spawn_ksoftirqd</code> (located in <code>kernel/softirq.c</code>) to create softirqd threads, as shown below:</p><p>Related code:</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>After ksoftirqd is created, it enters its thread loop functions ksoftirqd<em>should</em>run and run_ksoftirqd, then checks if there are any software interrupts to process.</p><p>Software interrupts include not only network software interrupts but other types as well. The Linux kernel defines all software interrupt types in 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="network-subsystem-initialization">Network Subsystem Initialization</h4><p>During network subsystem initialization, softnet<em>data is initialized for each CPU, and processing functions are registered for RX</em>SOFTIRQ and TX_SOFTIRQ, as shown in Figure 2.4.</p><p>The Linux kernel initializes various subsystems by calling subsys_initcall.</p><p><strong>Key Point!!! The network subsystem initialization mentioned here executes the net<em>dev</em>init function.</strong></p><p>This is the net<em>dev</em>init function in subsys<em>initcall(net</em>dev_init). Code as follows:</p><pre><code class="lang-c">static int __init net_dev_init(void)
{
    ......

    /*
     *    Initialise the packet receive queues.
     */

  /*
   * Allocate a softnet_data structure for each CPU. The poll_list in this structure
   * is used to wait for driver programs to register their poll functions, which can
   * be seen later when network card drivers initialize.
   */
        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 registers a processing function for each type of software interrupt.
     * NET_TX_SOFTIRQ processing function is net_tx_action;
     * NET_RX_SOFTIRQ processing function is 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>Following open<em>softirq further reveals that this registration method is recorded in the softirq</em>vec variable. Later, when the softirqd thread receives software interrupts, it will use this variable to find the corresponding processing function for each type of software interrupt.</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="protocol-stack-registration">Protocol Stack Registration</h4><h4 id="network-card-driver-initialization">Network Card Driver Initialization</h4><p>Each driver uses <code>module_init</code> to register an initialization function with the kernel. When the driver is loaded, the kernel calls this function.</p><p>After completion, the Linux kernel knows the driver&#x27;s relevant information, such as the igb network card driver&#x27;s igb<em>driver</em>name and igb_probe function address.</p><p>When a network card device is identified, the kernel calls its driver&#x27;s probe method. (Continuing with the igb network card driver example), the igb<em>driver&#x27;s probe method is igb</em>probe.</p><p>The purpose of the igb_probe method is to get the device into a ready state as quickly as possible.</p><p>Additionally, there&#x27;s a critical step: registering the poll function required by the NAPI mechanism, which for the igb network card driver is igb_poll.</p><h3 id="after-initialization-completion">After Initialization Completion</h3><h4 id="starting-the-network-card">Starting the Network Card</h4><p>After all the above initialization is complete, the network card can be started. The general startup sequence is similar, as shown below:</p><p>igb_open code:</p><pre><code class="lang-c">static int __igb_open(struct net_device *netdev, bool resuming)
{
  // Allocate transmit descriptor arrays
  err = igb_setup_all_tx_resources(adapter);
  // Allocate receive descriptor arrays
    err = igb_setup_all_rx_resources(adapter);

  // Register interrupt handler
  err = igb_request_irq(adapter);
    if (err)
        goto err_req_irq;

  // Enable 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>The igb<em>open function calls igb</em>setup<em>all</em>tx<em>resources and igb</em>setup<em>all</em>rx<em>resources. In the igb</em>setup<em>all</em>rx_resources step, RingBuffer is allocated and the mapping relationship between memory and Rx queues is established.</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>Using a for loop with the igb<em>setup</em>rx<em>resources function, several queues are created. The igb</em>setup<em>rx</em>resources function:</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. Allocate igb_rx_buffer array memory
    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. Allocate e1000_adv_rx_desc DMA array memory
    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. Initialize queue members
    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>From the above source code, you can see that internally, a RingBuffer doesn&#x27;t have just one circular queue array, but two:</p><ol start="1"><li>igb<em>rx</em>buffer array: This array is used by the kernel, allocated via vzalloc;</li><li>e1000<em>adv</em>rx<em>desc array: This array is used by network card hardware, allocated via dma</em>alloc_coherent.</li></ol><p>Then there&#x27;s the final step of interrupt function registration, which can be seen in igb<em>request</em>irq.</p><p>OK, that&#x27;s all the preparation work! Next comes receiving data packets.</p><h2 id="starting-to-receive-data-packets">Starting to Receive Data Packets</h2><p>This section includes hardware interrupt processing.</p><h3 id="hardware-interrupt-processing">Hardware Interrupt Processing</h3><p>First, when data frames arrive at the network card from the network cable, the first stop is the network card&#x27;s receive queue. The network card searches for available memory locations in its allocated RingBuffer, and when found, the DMA engine will DMA the data to the memory previously associated with the network card. At this point, the CPU is unaware.</p><p>When the DMA operation completes, the network card sends a hardware interrupt to the CPU, notifying it that data has arrived. The hardware interrupt processing is as follows:</p><p>In the &quot;Starting the Network Card&quot; section mentioned earlier, the network card&#x27;s hardware interrupt registered processing function is 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>The igb<em>write</em>itr only records hardware interrupt frequency. Following the napi<em>schedule call, you&#x27;ll discover that Linux only completes simple necessary work in hardware interrupts, leaving most processing to software interrupts. From the above code, you can see that the hardware interrupt processing is really very short - it just records a register, modifies the CPU&#x27;s poll</em>list, and then issues a software interrupt. That&#x27;s it, the hardware interrupt work is complete.</p><h3 id="ksoftirqd-kernel-thread-processing-software-interrupts">ksoftirqd Kernel Thread Processing Software Interrupts</h3><p>Network packet reception processing mainly occurs in the ksoftirqd kernel thread, where all software interrupts are processed, as shown below:</p><h3 id="network-protocol-stack-processing">Network Protocol Stack Processing</h3><p>The netif<em>receive</em>skb function processes packets according to their protocol. For UDP packets, packets are sent sequentially to protocol processing functions like ip<em>rcv, udp</em>rcv, etc., as shown below:</p><h3 id="ip-layer-processing">IP Layer Processing</h3><p>Linux IP layer operations are in the code file <code>net/ipv4/ip_input.c</code>.</p><h2 id="summary">Summary</h2><p>The network module is the most complex module in the Linux kernel. The entire process involves interactions between many kernel components, such as network card drivers, protocol stacks, kernel ksoftirqd threads, etc. It looks complex, but the overall picture is actually quite clear. Simple summary as follows.</p><p>After a user executes a recvfrom call, the user process enters kernel mode through the system call. If the receive queue has no data, the process enters sleep state and is suspended by the operating system. This part is relatively simple. Next comes the work between various Linux kernel components.</p><p>First, before starting packet reception, Linux must do extensive preparation work:</p><ul><li>Create ksoftirqd kernel threads, set up their thread functions, and rely on them to handle software interrupts later;</li><li>Protocol stack registration: Linux implements many protocols like ARP, ICMP, IP, UDP, and TCP. Each protocol registers its processing function, making it convenient to quickly find the corresponding processing function when packets arrive;</li><li>Network card driver initialization: Each driver has an initialization function that the kernel will initialize. During this initialization process, prepare DMA and tell the kernel the NAPI poll function address;</li><li>Start the network card: Allocate RX and TX queues, register interrupt corresponding processing functions.</li></ul><p>After preparation work is complete, data arrives. The first to greet it is the network card:</p><ul><li>The network card DMAs data frames to memory&#x27;s RingBuffer, then sends an interrupt notification to the CPU;</li><li>CPU responds to the interrupt request, calls the interrupt processing function registered when the network card started;</li><li>The interrupt processing function only issues a software interrupt request and does nothing else;</li><li>Kernel thread ksoftirqd discovers a software interrupt request has arrived, first disables hardware interrupts;</li><li>ksoftirqd thread starts calling the driver&#x27;s poll function to receive packets;</li><li>The poll function sends received packets to the protocol stack&#x27;s registered ip_rcv function;</li><li>The ip<em>rcv function sends packets to the udp</em>rcv function (for TCP packets, sent to tcp<em>rcv</em>v4).</li></ul><h2 id="some-conclusions">Some Conclusions</h2><h3 id="question-1-what-exactly-is-ringbuffer-and-why-does-ringbuffer-drop-packets">Question 1: What exactly is RingBuffer, and why does RingBuffer drop packets?</h3><p>RingBuffer is a special area in memory, a circular queue array. In fact, this data structure includes igb<em>rx</em>buffer circular queue arrays, e1000<em>adv</em>rx_desc circular queue arrays, and numerous skbs.</p><p>If RingBuffer represents pointer arrays, they are pre-allocated. If they are skbs, they are dynamically allocated during the packet reception process.</p><h3 id="question-2-what-are-software-interrupts-and-hardware-interrupts-respectively">Question 2: What are software interrupts and hardware interrupts respectively?</h3><p>Key flow of data packet reception in Linux network stack:</p><ol start="1"><li><strong>Hardware Stage</strong>: Network card places received data packets into RingBuffer</li><li><strong>Hardware Interrupt Trigger</strong>: Network card generates hardware interrupt to notify CPU</li><li><strong>Hardware Interrupt Processing</strong>: Add network card device to <code>poll_list</code> doubly linked list in <code>softnet_data</code> structure</li><li><strong>Software Interrupt Trigger</strong>: Trigger <code>NET_RX_SOFTIRQ</code> software interrupt</li><li><strong>Software Interrupt Processing</strong>: Traverse <code>poll_list</code>, execute network card driver&#x27;s <code>poll</code> function to collect network packets</li><li><strong>Protocol Stack Processing</strong>: Forward data packets to protocol processing functions like <code>ip_rcv</code>, <code>udp_rcv</code>, <code>tcp_rcv_v4</code></li></ol><p>This describes the Linux NAPI (New API) mechanism, an efficient method for handling network data packets.</p><h4 id="actual-application-of-ringbuffer-in-network-stack">Actual Application of RingBuffer in Network Stack</h4><h5 id="network-card-rxtx-ring-buffers">Network Card RX/TX Ring Buffers</h5><pre><code class="lang-c">/* Simplified network card RX Ring structure */
struct e1000_rx_desc {
    __le64 buffer_addr;    /* Data buffer address */
    __le16 length;         /* Packet length */
    __le16 checksum;       /* Checksum */
    __u8  status;          /* Descriptor status */
    __u8  errors;          /* Error code */
    __le16 special;
};
</code></pre>
<p>In practice, an Intel network card&#x27;s RX Ring might contain 256 such descriptors, forming a circular structure.</p><h5 id="actual-example-of-hardware-and-software-interrupt-cooperation">Actual Example of Hardware and Software Interrupt Cooperation</h5><p>In Intel 82599 network card driver:</p><pre><code class="lang-c">/* Hardware interrupt handler */
static irqreturn_t ixgbe_msix_lsc(int irq, void *data)
{
    struct net_device *netdev = data;

    /* Disable network card interrupt */
    ixgbe_disable_interrupt();

    /* Add device to poll_list */
    napi_schedule(&amp;adapter-&gt;q_vector[vector]-&gt;napi);

    return IRQ_HANDLED;
}

/* NAPI poll function */
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;

    /* Batch receive packets from RingBuffer, process at most budget packets */
    work_done = ixgbe_clean_rx_irq(q_vector, budget);

    /* If work is incomplete, stay in 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[Shell Script for Pushing Large Git Repositories]]></title>
            <link>https://blog.cheverjohn.me/shell-to-push-large-git-repo</link>
            <guid>https://blog.cheverjohn.me/shell-to-push-large-git-repo</guid>
            <pubDate>Wed, 27 Dec 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Check Out My Results My requirement: I want to read Linux source code, manage my Linux source code reading with git, and save my source code reading with my self-built GitLab. Then I discovered that simply using git push for a 2GB Linux source code file would cause problems. So I wrote a script to s...]]></description>
            <content:encoded><![CDATA[<article><div><h2 id="check-out-my-results">Check Out My Results</h2><p><img alt="image-20250310233801870" src="beforeShow.png"/></p><p>My requirement: I want to read Linux source code, manage my Linux source code reading with git, and save my source code reading with my self-built GitLab. Then I discovered that simply using <code>git push</code> for a 2GB Linux source code file would cause problems.</p><p>So I wrote a script to solve this problem.</p><p>The script content is as follows:</p><pre><code class="lang-shell">#!/bin/bash

# Configuration parameters (modify as needed)
REMOTE_NAME=&quot;origin&quot;              # Remote repository name
BRANCH_NAME=&quot;main&quot;                # Target branch name
REMOTE_URL=&quot;git@gitlab.cheverjohn.me:CheverJohn/linux.git&quot;  # Remote repository URL
BATCH_SIZE=500                    # Number of files per batch
COMMIT_MESSAGE=&quot;Batch file upload&quot;      # Commit message

# Color configuration
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; # No color

# Check if we&#x27;re inside a git repository
if ! git rev-parse --is-inside-work-tree &gt; /dev/null 2&gt;&amp;1; then
    echo -e &quot;${YELLOW}Current directory is not a git repository, initializing...${NC}&quot;
    git init
    echo -e &quot;${GREEN}Git repository initialized${NC}&quot;
else
    echo -e &quot;${GREEN}Git repository already exists${NC}&quot;
fi

# Check if remote repository is configured
if ! git remote | grep -q &quot;$REMOTE_NAME&quot;; then
    echo -e &quot;${YELLOW}Adding remote repository $REMOTE_NAME: $REMOTE_URL${NC}&quot;
    git remote add $REMOTE_NAME $REMOTE_URL
    echo -e &quot;${GREEN}Remote repository added${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}Updating remote repository URL: $REMOTE_URL${NC}&quot;
        git remote set-url $REMOTE_NAME $REMOTE_URL
        echo -e &quot;${GREEN}Remote repository URL updated${NC}&quot;
    else
        echo -e &quot;${GREEN}Remote repository correctly configured${NC}&quot;
    fi
fi

# Ensure main branch exists
if ! git show-ref --quiet refs/heads/$BRANCH_NAME; then
    echo -e &quot;${YELLOW}Creating branch $BRANCH_NAME...${NC}&quot;
    git checkout -b $BRANCH_NAME
    echo -e &quot;${GREEN}Branch $BRANCH_NAME created${NC}&quot;
else
    echo -e &quot;${GREEN}Branch $BRANCH_NAME already exists${NC}&quot;
    git checkout $BRANCH_NAME
fi

# Get all untracked and modified files
echo -e &quot;${BLUE}Getting list of files to upload...${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}No files found to add, trying to add all files...${NC}&quot;
    git add -A
    FILES=($(git diff --name-only --cached))
    TOTAL_FILES=${#FILES[@]}

    if [ $TOTAL_FILES -eq 0 ]; then
        echo -e &quot;${RED}Error: No files found to push${NC}&quot;
        exit 1
    fi
else
    # Add all files to staging area
    echo -e &quot;${BLUE}Adding all files to staging area...${NC}&quot;
    git add -A
fi

echo -e &quot;${GREEN}Found $TOTAL_FILES files to upload${NC}&quot;

# Calculate number of batches
BATCH_COUNT=$(( ($TOTAL_FILES + $BATCH_SIZE - 1) / $BATCH_SIZE ))
echo -e &quot;${GREEN}Will upload in $BATCH_COUNT batches${NC}&quot;

# First commit all files
echo -e &quot;${BLUE}Committing all files...${NC}&quot;
git commit -m &quot;$COMMIT_MESSAGE&quot;

# Push in batches
echo -e &quot;${BLUE}Starting batch push...${NC}&quot;
git push -u $REMOTE_NAME $BRANCH_NAME

if [ $? -eq 0 ]; then
    echo -e &quot;${GREEN}All files successfully pushed to remote repository${NC}&quot;
else
    echo -e &quot;${YELLOW}Regular push failed, trying batch push method...${NC}&quot;
    # Using git batch push method (referenced from search results examples)
    # Based on git_batch_push.sh concept, but simplified implementation

    # Use git rev-list to get all commits
    COMMITS=($(git rev-list --reverse HEAD))
    TOTAL_COMMITS=${#COMMITS[@]}

    if [ $TOTAL_COMMITS -eq 0 ]; then
        echo -e &quot;${RED}Error: No commits found${NC}&quot;
        exit 1
    fi

    echo -e &quot;${GREEN}Found $TOTAL_COMMITS commits, will push in batches${NC}&quot;

    # Calculate number of batches (500 commits per batch)
    COMMIT_BATCH_SIZE=500
    COMMIT_BATCH_COUNT=$(( ($TOTAL_COMMITS + $COMMIT_BATCH_SIZE - 1) / $COMMIT_BATCH_SIZE ))

    echo -e &quot;${GREEN}Will push commits in $COMMIT_BATCH_COUNT batches${NC}&quot;

    # Push commits in batches
    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}Pushing batch $((i+1))/${COMMIT_BATCH_COUNT} (commits $((START+1))-$END)...${NC}&quot;

        if git push $REMOTE_NAME $TARGET_COMMIT:refs/heads/$BRANCH_NAME; then
            echo -e &quot;${GREEN}Batch $((i+1)) successfully pushed${NC}&quot;
        else
            echo -e &quot;${RED}Batch $((i+1)) push failed${NC}&quot;
            echo -e &quot;${YELLOW}Trying alternative push method...${NC}&quot;

            # If above method fails, try another batch push method
            if [ $i -eq 0 ]; then
                # First batch, create new branch
                git push $REMOTE_NAME $BRANCH_NAME
            else
                # Get the end commit of previous batch
                PREV_END=$(($START - 1))
                PREV_COMMIT=${COMMITS[$PREV_END]}
                CURR_COMMIT=${COMMITS[$BATCH_END_INDEX]}

                echo -e &quot;${BLUE}Using range push $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}Batch upload process completed!${NC}&quot;
</code></pre>
<p>The usage method is simple.</p><pre><code class="lang-shell">chmod +x git_batch_push.sh
</code></pre>
<p>Then run the script in the current folder.</p><h2 id="final-results">Final Results</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[Applied Cryptography: GPG File Encryption (Practical Guide)]]></title>
            <link>https://blog.cheverjohn.me/cryptography-gpg</link>
            <guid>https://blog.cheverjohn.me/cryptography-gpg</guid>
            <pubDate>Tue, 26 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[This article focuses on practical implementation with a brief introduction to the underlying principles. Throughout this article, I'll first cover the basic principles, then walk through three main sections: how to create GPG keys, how to manage your keys (key management commands), and hands-on prac...]]></description>
            <content:encoded><![CDATA[<article><div><p>This article focuses on practical implementation with a brief introduction to the underlying principles.</p><p>Throughout this article, I&#x27;ll first cover the <strong>basic principles</strong>, then walk through three main sections: <strong>how to create GPG keys</strong>, <strong>how to manage your keys (key management commands)</strong>, and <strong>hands-on practice: using self-created GPG keys to decrypt transmitted encrypted files</strong>.</p><h2 id="principle-overview">Principle Overview</h2><p>The principle behind using GPG for encrypting and decrypting documents is straightforward, as described in the documentation:</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>The entire process is: I encrypt the file I want to send you with my private key. Then you use my public key to decrypt the file I want to send you, which has been encrypted by me.</p><p>This is the basic process.</p><p>This article provides a quick overview of how GPG encrypts document content.</p><p>This article is divided into two parts, starting with creating a usable GPG key.</p><h2 id="how-to-create-gpg-keys">How to Create GPG Keys</h2><pre><code class="lang-shell">[Se] gpg --list-keys
gpg: checking the trustdb
gpg: no ultimately trusted keys found
</code></pre>
<p>First, let&#x27;s list whether my machine currently has any GPG keys. As we can see, there are none. This mainly refers to whether there are GPG public keys. Let&#x27;s also check for private keys.</p><pre><code class="lang-shell">[Se] gpg --list-secret-keys
[Se]
</code></pre>
<p>Nothing there either.</p><h3 id="one-command-to-rule-them-all">One Command to Rule Them All</h3><p>Let&#x27;s start creating.</p><p>The detailed steps begin with <code>gpg --full-generate-key</code>, as follows:</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="detailed-stage-by-stage-command-explanation">Detailed Stage-by-Stage Command Explanation</h3><p>Let me break down what happens at each stage.</p><h4 id="stage-1-select-encryption-and-signing-algorithm">Stage 1: Select Encryption and Signing Algorithm</h4><p>In the first stage, you need to select the encryption algorithm. I chose option 1, which means both encryption and signing use the RSA algorithm.</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>Oh, and there&#x27;s also a copyright notice.</p><h4 id="stage-2-select-key-length">Stage 2: Select Key Length</h4><p>Longer keys are more secure. I chose 4096 here.</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="stage-3-set-key-validity-period">Stage 3: Set Key Validity Period</h4><p>Set the validity period.</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>For this demonstration, I configured <code>Key does not expire at all</code>.</p><h4 id="stage-4-personal-information">Stage 4: Personal Information</h4><p>This section will ultimately be used to generate your 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>Here I set my <code>Real name</code> to &quot;Chenwei Jiang&quot;, my <code>Email address</code> to &quot;cheverjonathan@gmail.com&quot;, and added a <code>comment</code>. These are some basic information I commonly use, primarily for demonstration purposes.</p><p>The result is a USER-ID: &quot;Chenwei Jiang (Used for learning) <a href="mailto:cheverjonathan@gmail.com">cheverjonathan@gmail.com</a>&quot;.</p><h2 id="how-to-manage-your-keys-key-management-commands">How to Manage Your Keys (Key Management Commands)</h2><p>This section covers how to manage multiple keys on a host machine.</p><h3 id="listing-keys">Listing Keys</h3><p>There are two types of key listings: public keys and private keys.</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>The above command shows the details.</p><p>The key section:</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>The first line shows the public key filename.</p><table><thead><tr><th> Value </th><th> Explanation </th></tr></thead><tbody><tr><td> /home/cheverjohn/.gnupg/pubring.kbx </td><td> First line shows the public key filename (pubring.kbx) </td></tr><tr><td> pub rsa4096 2023-09-19 [SC]<br/>D1DED33E8FE9CA4B315A488968CC99424BF9EF81 </td><td> Second line shows public key characteristics (4096 bits, hash string, and creation time) </td></tr><tr><td> uid [ultimate] Chenwei Jiang (Used for learning) <a href="mailto:cheverjonathan@gmail.com">cheverjonathan@gmail.com</a> </td><td> Third line shows the &quot;user ID&quot; </td></tr><tr><td> sub rsa4096 2023-09-19 [E] </td><td> Fourth line shows private key characteristics </td></tr></tbody></table><h3 id="exporting-keys">Exporting Keys</h3><p>The public key file is located at <code>~/.gnupg/pubring.kbx</code> and stored in binary format. Using the armor parameter converts it to ASCII display.</p><p>Using commands to export as <code>public-key.txt</code> and <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>As shown above, this demonstrates the steps to export <code>public-key.txt</code>. The detailed command is:</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>Below are the steps to export <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>If you previously set a password, this operation requires the password. The command requiring password is:</p><pre><code class="lang-shell">gpg --armor --output private-key.txt --export-secret-keys
</code></pre>
<h3 id="uploading-public-keys">Uploading Public Keys</h3><p>Public key servers are network servers specifically for storing user public keys. The <code>--send-keys</code> subcommand can achieve this.</p><p>Public key servers have no verification mechanism - anyone can upload public keys in your name, so there&#x27;s no guarantee of reliability for public keys on servers.</p><p>Generally, we publish a public key fingerprint on our own website for others to verify that the downloaded public key is authentic. The <code>--fingerprint</code> subcommand can generate a public key fingerprint.</p><h2 id="hands-on-practice--practice-makes-perfect">Hands-On Practice / Practice Makes Perfect</h2><p>In this section, I&#x27;ll demonstrate exporting public keys, encrypting with private keys, and decrypting with public keys on another device.</p><p>I&#x27;ll show how to encrypt a file containing &quot;hello world&quot; and then decrypt it remotely.</p><p>The steps are divided into encryption and decryption.</p><p>Detailed steps:</p><ol start="1"><li><p>Create file with the following commands:</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>The file creation commands are:</p><pre><code class="lang-shell">touch demo.txt
echo &quot;hello world&quot; &gt; demo.txt
</code></pre>
<p>Now we have a demo.txt file containing &quot;hello world&quot;.</p></li><li><p>Encrypt the file:</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>Encryption command:</p><pre><code class="lang-shell">gpg --recipient &#x27;cheverjonathan@gmail.com&#x27; --output demo.gpg --encrypt demo.txt
</code></pre>
<p>You can see that demo.gpg is the encrypted file.</p></li><li><p>Decrypt the file:</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>Decryption command:</p><pre><code class="lang-shell">gpg --output demo --decrypt demo.gpg
</code></pre>
<p>After entering this command, you need to input the password set earlier, then you can get the file.</p><p>The above demonstrates the process of encrypting files using local keys and decrypting them locally.</p><p>Next, let&#x27;s begin...</p><h3 id="obtaining-files-on-other-computers-encrypting-them-then-having-the-original-host-decrypt-using-private-keys">Obtaining Files on Other Computers, Encrypting Them, Then Having the Original Host Decrypt Using Private Keys</h3><p>First, we transfer public-key.txt to another host. You can use GitHub&#x27;s functionality to upload/download files. For convenience, I&#x27;ll upload the following file tree from the above process:</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>After removing private-key.txt, I&#x27;ll upload the files to GitHub.</p><p>You can see I&#x27;ve downloaded the public-key.txt file on another host, as shown in the file tree:</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>Next, I need to use this public-key.txt to encrypt a file, then transfer this file back to the host with the private key for decryption.</p><p>First, we need to import this public-key.txt locally:</p><pre><code class="lang-shell">gpg --import public-key.txt 
</code></pre>
<p>Command execution result:</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>Now we need to use this public key to encrypt text.</p><p>First, let&#x27;s create a file 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>We need to encrypt this file and then send it to the host with the private key for decryption.</p><p>The entire encryption process:</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>Encryption command:</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>Then we need to upload the encrypted file hello-encrypted.gpg to the machine with the private key.</p><p>Again, I&#x27;ll use GitHub as the file transfer tool.</p><p>You can see I&#x27;ve obtained the file on the host machine (the machine with the keys) and started decryption:</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>Next, we need to decrypt 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>Decryption command:</p><pre><code class="lang-shell">gpg --output hello-decrypted.txt --decrypt hello-encrypted.gpg
</code></pre>
</li></ol><h2 id="related-articles">Related Articles</h2><ul><li><a href="https://www.gnupg.org/gph/en/manual/x110.html">gnupg.org Official Manual</a></li></ul></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>Cryptography</category>
            <category>GPG</category>
        </item>
        <item>
            <title><![CDATA[Hello, I am Chever John]]></title>
            <link>https://blog.cheverjohn.me/hello-world</link>
            <guid>https://blog.cheverjohn.me/hello-world</guid>
            <pubDate>Tue, 27 Dec 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Hello! I am Chever John, a software engineer.]]></description>
            <content:encoded><![CDATA[<article><div><p>Hello!</p><p>I am Chever John, a software engineer.</p></div></article>]]></content:encoded>
            <author>cheverjonathan@gmail.com (Chenwei Jiang)</author>
            <category>hello</category>
            <category>world</category>
        </item>
        <item>
            <title><![CDATA[How to run go plugin runner with Apisix-Ingress-controller]]></title>
            <link>https://blog.cheverjohn.me/how-to-run-go-plugin-runner-in-apisix-ingress</link>
            <guid>https://blog.cheverjohn.me/how-to-run-go-plugin-runner-in-apisix-ingress</guid>
            <pubDate>Fri, 29 Apr 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Just as title says. Background Description While wandering around the community, I found a user confused about "how to use multilingual plugins in APISIX Ingress environment". I happen to be a user of go-plugin-runner and have a little knowledge of the APISIX Ingress project, so this document was bo...]]></description>
            <content:encoded><![CDATA[<article><div><p>Just as title says.</p><h2 id="background-description">Background Description</h2><p>While wandering around the community, I found a user confused about &quot;how to use multilingual plugins in APISIX Ingress environment&quot;. I happen to be a user of go-plugin-runner and have a little knowledge of the APISIX Ingress project, so this document was born.</p>
<h2 id="proposal-description">Proposal Description</h2><p>Based on version 0.3 of the go-plugin-runner plugin and version 1.4.0 of APISIX Ingress, this article goes through building the cluster, building the image, customizing the helm chart package, and finally, deploying the resources. It is guaranteed that the final result can be derived in full based on this documentation.</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="begin">Begin</h2><h3 id="build-a-cluster-environment">Build a cluster environment</h3><p>Select <code>kind</code> to build a local cluster environment. The command is as follows:</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="build-the-go-plugin-runner-executable">Build the go-plugin-runner executable</h3><p>If you have finished writing the plugin, you can start compiling the executable to run with APISIX.</p><p>This article recommends two packaging build options.</p><ol start="1"><li>put the packaging process into the Dockerfile and finish the compilation process when you build the docker image later.</li><li>You can also follow the scheme used in this document, building the executable first and then copying the packaged executable to the image.</li></ol><p>How you choose the option should depend on your local hardware considerations. The reason for selecting the second option here is that I want to rely on my powerful local hardware to increase the building speed and speed up the process.</p><h3 id="go-to-the-go-plugin-runner-directory">Go to the go-plugin-runner directory</h3><p>Choose a folder address <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code> and place our <code>apisix-go-plugin-runner</code> project in this folder.</p><p>After successful placement, the file tree is shown below:</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>Then you need to go to the <code>apisix-go-plugin-runner/cmd/go-runner/plugins</code> directory and write the plugins you need in that directory. This article will use the default plugin <code>say</code> for demonstration purposes.</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>After writing the plugins, start compiling the executable formally, and note here that you should build static executables, not dynamic ones.</p><p>The package compile command is as follows.</p><pre><code class="lang-bash">CGO_ENABLED=0 go build -a -ldflags &#x27;-extldflags &quot;-static&quot;&#x27; .
</code></pre>
<p>This successfully packages a statically compiled <code>go-runner</code> executable.</p><p>In the <code>apisix-go-plugin-runner/cmd/go-runner/</code> directory, you can see that the current file tree looks like this:</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>Please remember the path <code>apisix-go-plugin-runner/cmd/go-runner/go-runner</code>, we will use it later.</p><h3 id="build-docker-image">Build Docker Image</h3><p>The image is built here in preparation for installing APISIX later using <code>helm</code>.</p><h4 id="write-dockerfile">Write Dockerfile</h4><p>Return to the path <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code> and create a Dockerfile in that directory, a demonstration of which is given here.</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; ]; };  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>Package all the <code>/apisix-go-plugin-runner</code> files in the <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code> directory into a Docker image. Note down the location of the executable <code>apisix-go-plugin-runner/cmd/go-runner/go-runner</code> and the location of the <code>/usr/local/apisix-go-plugin-runner</code> directory in the Dockerfile above to get the final location of the executable in the Docker image is located as follows.</p><pre><code class="lang-bash">/usr/local/apisix-go-plugin-runner/cmd/go-runner/go-runner
</code></pre>
<p>Please make a note of this address. We will use it in the rest of the configuration.</p><h4 id="begin-to-build-docker-image">Begin to build Docker Image</h4><p>Start building a Docker image based on the Dockerfile. The command is executed in the <code>/home/chever/api7/cloud_native/tasks/plugin-runner</code> directory. The command is as follows:</p><pre><code class="lang-bash">docker build -t apisix/forrunner:0.1 .
</code></pre>
<p>Command Explanation: Build an image with the name <code>apisix/forrunner</code> and mark it as version 0.1.</p><h4 id="load-the-image-to-the-cluster-environment">Load the image to the cluster environment</h4><pre><code class="lang-bash">kind  load docker-image apisix/forrunner:0.1 
</code></pre>
<p>Load the image into the kind cluster environment to pull the custom local image for installation during the helm installation.</p><h3 id="install-apisix-ingress">Install APISIX Ingress</h3><h4 id="customize-the-helm-chart">Customize the helm chart</h4><p>This section focuses on modifying the <code>values.yaml</code> file in the official helm package so that it can install locally packaged images and run the <code>go-plugin-runner</code> executable properly.</p><h5 id="fetch-official-helm-chart">Fetch Official helm chart</h5><p>First, fetch the latest apisix helm chart package with the following command:</p><pre><code class="lang-bash">helm fetch apisix/apisix
</code></pre>
<p>The file tree is as follows:</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="unzip">Unzip</h5><p>Unzip the <code>apisix-0.9.1.tgz</code> file and prepare to rewrite the configuration. The unzip command is as follows.</p><pre><code class="lang-bash">tar zxvf apisix-0.9.1.tgz
</code></pre>
<p>The file tree is as follows:</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="change-valuesyaml">Change <code>values.yaml</code></h5><p>Go to the <code>apisix</code> folder and modify the <code>values.yaml</code> file. The two changes are as follows:</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>The first change sets the image for the helm installation to be a locally packaged image of your own.</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>The second change sets the position of go-runner in the container after running the container.</p><h5 id="compress-the-modified-helm-chart">Compress the modified helm chart</h5><p>Once configured, compress the <code>apisix</code> file. The compression command is as follows:</p><pre><code class="lang-bash">tar zcvf apisix.tgz apisix/
</code></pre>
<p>The compressed file is obtained, at which point the file tree is as follows:</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="execute-the-helm-install-command">Execute the helm install command</h4><h5 id="create-namespace">Create namespace</h5><p>Before installation, create namespaces with the following command:</p><pre><code class="lang-bash">kubectl create ns ingress-apisix
</code></pre>
<p>Then install APISIX using helm with the following command:</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="create-httpbin-service-and-apisixroute-resources">Create httpbin service and ApisixRoute resources</h3><p>Create an httpbin backend resource to run with the deployed ApisixRoute resource to test that the functionality is working correctly.</p><h4 id="create-httpbin-service">Create httpbin service</h4><p>Create an httpbin service with the following command:</p><pre><code class="lang-bash">kubectl run httpbin --image kennethreitz/httpbin --port 80
</code></pre>
<p>Expose the port with the following command:</p><pre><code class="lang-bash">kubectl expose pod httpbin --port 80
</code></pre>
<h4 id="create-apisixroute-resource">Create ApisixRoute Resource</h4><p>Create the <code>go-plugin-runner-route.yaml</code> file to enable the ApisixRoute resource, with the following configuration file:</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>The create resource command is as follows:</p><pre><code class="lang-bash">kubectl apply -f go-plugin-runner-route.yaml
</code></pre>
<h3 id="test">Test</h3><p>The command is as follows to test if the plugin written in Golang is working correctly:</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>Here I derived from the <code>kubectl get pods --all-namespaces</code> command that the <code>${namespace of Apache APISIX}</code> and <code>${Pod name of Apache APISIX}</code> parameters here are <code>ingress-apisix</code> and <code>apisix- 55d476c64-s5lzw</code>, execute the command as follows:</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>The expected response obtained is:</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>