# ops.jiunotes.top — 全站文章完整索引 > 生成日期:2026-10-20 > 版权:© 2026 玖 · CC BY-NC-SA 4.0 > 语言:中文 (zh) + 英文 (en) 本文档列出站点内所有文章的完整正文内容。AI 助手可直接抓取本文作为问答素材。 --- # Network & DNS ## CDN-PRINCIPLES URL: https://ops.jiunotes.top/en/network/cdn-principles.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full troubleshooting guide. ### Full Content # CDN-PRINCIPLES > **Status**: English placeholder. Chinese original at [../network/cdn-principles.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## DNS-RESOLUTION URL: https://ops.jiunotes.top/en/network/dns-resolution.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full troubleshooting guide. ### Full Content # DNS-RESOLUTION > **Status**: English placeholder. Chinese original at [../network/dns-resolution.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## DNS-TROUBLESHOOT URL: https://ops.jiunotes.top/en/network/dns-troubleshoot.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full troubleshooting guide. ### Full Content # DNS-TROUBLESHOOT > **Status**: English placeholder. Chinese original at [../network/dns-troubleshoot.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## Network & DNS — Nginx · CDN · DNS · TCP Notes (7 posts) URL: https://ops.jiunotes.top/en/network.md 摘要: Hands-on network troubleshooting: Nginx 502/504, CDN origin, DNS propagation, TCP capture, Wireshark filters. Reusable actions. ### Full Content # Before You tcpdump, Ask These Three Things This column collects first-line actions for three classes of failures: down, slow, and unresolvable. Not "TCP three-way handshake explained" — that's Wikipedia's job. We write: after `dig +short` returns a CNAME, what's next. After `tcpdump` shows a RST, where to look. ## Contents (7 posts) | Slug | Value | Keyword | |---|---|---| | [tcp-ip](tcp-ip.md | Understand TCP from state machine, read `ss` / `netstat` | TCP/IP state machine | | [tcp-troubleshoot](tcp-troubleshoot.md | TIME_WAIT / RST / SYN half-open / retransmission | TCP troubleshooting | | [dns-resolution](dns-resolution.md | Recursive / iterative / caching full flow | DNS resolution | | [dns-troubleshoot](dns-troubleshoot.md | dig / host / nslookup — which to use when | DNS troubleshooting | | [cdn-principles](cdn-principles.md | Origin fetch, preheat, cache invalidation | CDN origin & cache | | **[nginx-proxy](nginx-proxy.md** | 502 / 504 / location matching decision tree ⭐ | Nginx reverse proxy | | [wireshark-basics](wireshark-basics.md | 30 common filters + field cheat sheet | Wireshark filters | ## Launch Flagship **[Nginx Reverse Proxy 502/504 Troubleshooting](nginx-proxy.md** ⭐ W1 flagship Nginx 502/504 is the highest-frequency ops error. This post walks through upstream probing, location matching priority, `proxy_read_timeout`, SSL cert chain — with a decision tree. ## Tags nginxreverse-proxy502tcpdnscdnwiresharktroubleshoot --- [Browse all Network posts →](../../archive.md) --- ## Nginx Reverse Proxy 502/504 Troubleshooting — Full Decision Tree URL: https://ops.jiunotes.top/en/network/nginx-proxy.md 摘要: A working engineer's guide to diagnosing 502 Bad Gateway and 504 Gateway Timeout behind Nginx reverse proxy. 5 failure scenarios, a 20-command Cheat Sheet, 5 real-world configs, and a decision tree to pinpoint root cause in minutes. ### Answer Nginx 502 usually means Nginx can't reach or can't parse a response from the upstream — check (1) whether the upstream is actually listening (`ss -lnt | grep :port`), (2) whether `proxy_pass` points at the right address and protocol (http vs https, IPv6 vs IPv4), and (3) whether the upstream logs show the request ever arrived. 504 is a client-visible timeout: the upstream took longer than `proxy_read_timeout` (default 60s) to respond. Start with `journalctl -u nginx -f` and the upstream service logs side by side, then work through the 5 scenarios in this article. ### FAQ Q: What's the fastest first step when Nginx returns 502? A: Check whether the upstream is actually listening on the port Nginx is proxying to: `ss -lnt | grep :8080`. If nothing is listening, start or restart the upstream service. If it is listening, look at upstream logs and Nginx `error.log`. Q: What's the fastest first step when Nginx returns 504? A: 504 means the upstream exceeded `proxy_read_timeout` (default 60s). Check `journalctl -u nginx -f` for a "upstream timed out" line — the exact upstream address is printed there. Then either fix the slow upstream or bump the timeout deliberately. Q: When should I raise proxy_read_timeout? A: Only when the upstream legitimately needs that long (report generation, ML inference, batch jobs). It's a symptom mask, not a fix. Bump in steps (60 → 120 → 300) and always log the reason in a comment so the next engineer isn't surprised. Q: How do I distinguish 502 vs 504 from Nginx logs? A: `error.log` will say "connect() failed" or "upstream prematurely closed connection" for 502, and "upstream timed out (110: Operation timed out)" for 504. The exact string tells you which class of problem you have. Q: Why does my HTTPS reverse proxy still return 502 after I enabled TLS? A: Because `proxy_pass` is still talking HTTP to an HTTPS backend. Match the protocol: `proxy_pass https://backend:443;` plus `proxy_ssl_server_name on;` and `proxy_ssl_name` set to the backend's TLS SNI hostname. ### Full Content # Nginx Reverse Proxy 502 / 504 Troubleshooting ## The 30-Second Diagnosis When Nginx shows you `502 Bad Gateway` or `504 Gateway Timeout`, it has already talked to the upstream (or tried to) and given up. The upstream is either **not reachable**, **not speaking the protocol Nginx expects**, or **too slow**. Work in this order: 1. Is the upstream actually listening? → `ss -lnt | grep :PORT` 2. Is the upstream seeing the request? → upstream logs 3. Does `proxy_pass` point at the right thing? → reload Nginx config in `error.log` 4. Is TLS between Nginx and upstream misconfigured? → check `proxy_pass` protocol and `proxy_ssl_*` 5. Is the upstream just slow? → check `proxy_read_timeout` and upstream processing time If you get past step 1 and still see 502, jump to the [502 Decision Tree](#502-bad-gateway-decision-tree). If the error is 504, the upstream is reachable but slow — [504 Timeout Decision Tree](#504-gateway-timeout-decision-tree) will walk through it. ## 5 Real Failure Scenarios ### 1. Upstream Not Running The most common 502 cause, and the one that trips up the freshest engineers. **Symptom**: 502 immediately after deploy, Nginx `error.log` says: ``` connect() failed (111: Connection refused) while connecting to upstream upstream: "http://127.0.0.1:8080/", host: "127.0.0.1:8080" ``` **Diagnosis**: ```bash ss -lnt | grep :8080 # Empty means nobody listening systemctl status myapp # Is the upstream service up? ``` **Fix**: Restart the upstream. Check for boot-time failures (`journalctl -u myapp -b`). ### 2. Wrong Address Behind `proxy_pass` A typo, a missing port, a wrong protocol — all produce the same 502. **Common culprits**: - `proxy_pass http://127.0.0.1;` → defaults to port 80, not 8080. Fix: `proxy_pass http://127.0.0.1:8080;` - `proxy_pass http://backend:8080;` but `backend` doesn't resolve. Fix: add to `/etc/hosts` or use Docker DNS (`extra_hosts:`). - `proxy_pass https://backend;` but backend only speaks HTTP. Fix: change to `http://`. **Diagnosis**: ```bash grep -n 'proxy_pass' /etc/nginx/sites-enabled/default dig backend # Does the hostname resolve? curl -v http://backend:8080/health # From the Nginx host, does the upstream respond? ``` ### 3. TLS Mismatch Between Nginx and Upstream If you flipped the upstream to HTTPS and forgot to update `proxy_pass`, Nginx will still try HTTP and get a garbled response. **Symptom**: Nginx log shows `upstream sent invalid header` or `upstream sent no valid HTTP/1.0 header while reading response header from upstream`. **Diagnosis**: ```bash # Nginx config shows proxy_pass http://backend:8443 (HTTP to HTTPS port) # Test from the Nginx host: curl -vk https://backend:8443/ # Should work curl -v http://backend:8443/ # Garbled response confirms the mismatch ``` **Fix**: ```nginx proxy_pass https://backend:8443; proxy_ssl_server_name on; proxy_ssl_name backend.example.com; # SNI hostname proxy_ssl_protocols TLSv1.2 TLSv1.3; proxy_ssl_verify off; # For self-signed cert; set on + CA if you own the cert ``` ### 4. Keepalive Stale Connection Long-lived upstreams (Java, Go net/http, Rails) tend to close idle connections from the load balancer. Nginx may hand a stale socket to a real request. **Symptom**: intermittent 502s, only on the first request after a quiet period, upstream logs show "connection closed while reading headers" or similar. **Diagnosis**: ```bash # Check upstream keepalive settings in Nginx grep -n 'keepalive' /etc/nginx/conf.d/upstream.conf # On the upstream, look for the connection lifetime ss -tnp | grep ':8080' | awk '{print $4}' # Look for CLOSE_WAIT on many sockets ``` **Fix**: ```nginx upstream backend { server 127.0.0.1:8080; keepalive 32; # Max idle conns Nginx keeps open to upstream keepalive_timeout 60s; } server { location / { proxy_pass http://backend; proxy_http_version 1.1; # Required for keepalive upstreams proxy_set_header Connection ""; # Empty Connection header for keepalive } } ``` ### 5. Backend Protocol Mismatch (HTTP/1.1 vs HTTP/2) If your upstream requires HTTP/2 (gRPC, some modern Java servers), the default HTTP/1.1 from Nginx will cause weird failures. **Symptom**: `upstream sent invalid header` or gRPC-specific errors like `http2 framing layer`. **Fix**: ```nginx location / { proxy_pass http://backend:50051; grpc_pass grpc://backend:50051; # For gRPC proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } ``` ## 20-Command Cheat Sheet Copy-paste to diagnose 502/504 quickly. ```bash # 1. What does the user see? curl -Iv https://your-domain.com/health # 2. What's Nginx logging right now? sudo journalctl -u nginx -f # 3. What's the upstream logging? sudo journalctl -u myapp -f # 4. Is the upstream listening? ss -lnt | grep :8080 # 5. Can Nginx reach the upstream? sudo -u nginx curl -v http://127.0.0.1:8080/health # 6. Which config is actually loaded? sudo nginx -T 2>&1 | grep -A5 'proxy_pass' # 7. Config syntax OK? sudo nginx -t # 8. Reload config sudo systemctl reload nginx # 9. What's the connection state between Nginx and upstream? ss -tnp | grep ':8080' # 10. Upstream service status systemctl status myapp systemctl status myapp.service --full # 11. Upstream CPU/memory (spikes cause slowness → 504) top -p $(pgrep -f myapp | head -1) # 12. Nginx worker count and state sudo nginx -T | grep worker_connections ps aux | grep nginx # 13. Check the exact Nginx error message grep -E '502|504|timed out|refused' /var/log/nginx/error.log | tail -20 # 14. Check upstream response time curl -w '@format.txt' -o /dev/null https://your-domain.com/ # format.txt: time_total, time_connect, time_starttransfer # 15. Check upstream TLS openssl s_client -connect backend:8443 -servername backend.example.com # 16. Check upstream via HTTP (if you suspect TLS mismatch) curl -v http://backend:8443/ # Garbled = mismatch # 17. Nginx error.log level grep 'error_log' /etc/nginx/nginx.conf # 18. Request body size limit grep 'client_max_body_size' /etc/nginx/nginx.conf # Default 1m — too small for uploads # 19. Header size limit grep 'large_client_header_buffers' /etc/nginx/nginx.conf # 20. Access log with upstream info grep 'proxy_upstream_name\|proxy_upstream_addr' /etc/nginx/nginx.conf ``` ## 5 Real-World Configs ### Reverse Proxy for a Node.js App on 127.0.0.1:3000 ```nginx server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; access_log /var/log/nginx/api.example.com.access.log vhost; error_log /var/log/nginx/api.example.com.error.log warn; client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Request-Id $request_id; proxy_set_header Connection ""; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; } } ``` ### Reverse Proxy for a Python/Flask Dev Server ```nginx upstream flask { server 127.0.0.1:5000; keepalive 16; } server { listen 443 ssl; server_name dev.example.com; location / { proxy_pass http://flask; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Connection ""; proxy_read_timeout 120s; # Dev servers tend to be slow } } ``` ### Reverse Proxy with gRPC Passthrough ```nginx location /api/v1 { grpc_pass grpc://backend:50051; proxy_http_version 1.1; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ``` ### Rate-Limited Reverse Proxy ```nginx limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; server { listen 443 ssl; server_name api.example.com; location / { limit_req zone=api burst=20 nodelay; limit_req_status 429; proxy_pass http://backend; proxy_set_header Host $host; } } ``` ### WebSocket-Enabled Reverse Proxy ```nginx location /ws { proxy_pass http://backend/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; # Keep WebSocket alive for an hour proxy_send_timeout 3600s; } ``` ## 502 Bad Gateway — Decision Tree ``` 502 → Check Nginx error.log ↓ ├─ "connect() failed (111: Connection refused)" │ → Upstream not listening. Fix: restart upstream. │ ├─ "connect() failed (110: Operation timed out)" │ → Firewall between Nginx and upstream, or network partition. │ Fix: iptables / security group. │ ├─ "upstream prematurely closed connection" │ → Keepalive mismatch, or upstream crashed on request. │ Fix: `proxy_set_header Connection "";` and check upstream logs. │ ├─ "upstream sent invalid header" │ → Protocol mismatch (HTTP vs HTTPS, HTTP/1 vs gRPC). │ Fix: align `proxy_pass` protocol with upstream. │ └─ "no live upstreams" → All upstream servers failed. Check upstream health. ``` ## 504 Gateway Timeout — Decision Tree ``` 504 → Check upstream took how long? ↓ ├─ < proxy_read_timeout → 502, not 504 (see above) │ └─ > proxy_read_timeout (default 60s) → Slow upstream. Fix (a): optimize upstream (usually right answer) Fix (b): raise `proxy_read_timeout` deliberately (with a comment explaining why) Fix (c): move long jobs to async (queue + poll) ``` ## 502 vs 504 vs 499 — Quick Reference | Code | Meaning | Where to fix | |------|---------|--------------| | **502 Bad Gateway** | Nginx couldn't reach or talk to upstream | Nginx config, upstream service | | **504 Gateway Timeout** | Upstream too slow for `proxy_read_timeout` | Upstream performance, or bump timeout | | **499 Client Closed Request** | User gave up before Nginx got a response | Nginx is fine — user/network issue | | **503 Service Unavailable** | Upstream returned 503, or Nginx is overloaded | Upstream health, or `worker_processes` | ## Wrapping Up 502 and 504 are the two workhorses of reverse-proxy debugging. Half of them are "upstream not running" and "wrong port in `proxy_pass`" — the cheapest bugs to fix. The harder ones (keepalive, TLS mismatch, gRPC) all boil down to one question: **what does Nginx think it's talking to, and what is the upstream actually talking back?** Read the `error.log` string, match it against this decision tree, and you'll usually know what to change within a minute. For related reading, see: - [TCP Troubleshooting: TIME_WAIT / RST / SYN Half-Open](tcp-troubleshoot.md) — the layer below - [Wireshark Basics: 30 Filters + Field Cheat Sheet](wireshark-basics.md) — when logs aren't enough - [CDN Origin & Cache: Origin Fetch, Preheat, Invalidation](cdn-principles.md) — 502 sometimes lives on the CDN side, not your Nginx --- ## TCP-IP URL: https://ops.jiunotes.top/en/network/tcp-ip.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full troubleshooting guide. ### Full Content # TCP-IP > **Status**: English placeholder. Chinese original at [../network/tcp-ip.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## TCP Troubleshooting: TIME_WAIT / RST / SYN Half-Open / Retransmission URL: https://ops.jiunotes.top/en/network/tcp-troubleshoot.md 摘要: TCP troubleshooting decision tree: TIME_WAIT floods, RST storms, SYN half-open, retransmission spikes. 20-command cheat sheet + 6 sysctl / tcpdump config snippets for production servers. ### Answer 90% of TCP troubleshooting failures start with capturing the wrong packet. First run `ss -t state established` and `netstat -an` to size the problem, then `tcpdump` only 3 source/destination port combinations. The 4 states that actually bite are: TIME-WAIT (server sent FIN first), CLOSE-WAIT (local app didn't close), SYN-SENT (our SYN unanswered), SYN-RECV (backlog overflow). Fix with `sysctl net.ipv4.tcp_tw_reuse=1` for TIME-WAIT, socket-level TCP_KEEPIDLE for RST, and backlog tuning for SYN. ### FAQ Q: What's the fastest way to see TCP state distribution? A: `ss -tn | awk '{print $1}' | sort | uniq -c` — one line per state with counts. TIME-WAIT in the thousands on a proxy is normal; CLOSE-WAIT > 1000 is always a bug. Q: Is tcp_tw_reuse safe to enable? A: Safe on the server side only (the side that closes connections first, typically a reverse proxy or web server). Unsafe on the client side — it can cause reuse bugs under NAT. In kernel 4.12+ tcp_tw_recycle was removed because it was always unsafe under NAT; tcp_tw_reuse survived. Q: How to distinguish RST from a peer vs RST from a firewall? A: `tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0'` shows the source. If the source is your own IP, an app closed the socket. If the source is the peer, they closed. If RSTs originate from nowhere obvious (or a firewall's IP), a middlebox is dropping. Q: What if ss shows SYN-RECV growing? A: Accept queue full. Check `ss -lnt` Recv-Q column against Send-Q (backlog). Raise `net.core.somaxconn` and `net.ipv4.tcp_max_syn_backlog`. If it's a scan, `net.ipv4.tcp_syncookies=1` (default on) already handles it. Q: How to detect retransmission storms? A: `netstat -s | grep -i retrans` for cumulative count, and `nstat -az | grep -i retrans` for a delta-friendly view. If it's climbing, use `ss -ti ` to see per-connection `retrans:`. High values localize the lossy path. ### Full Content # TCP Troubleshooting: TIME_WAIT / RST / SYN Half-Open / Retransmission TCP failures are almost never about TCP itself — they're about something on one side of the socket doing something unusual, and TCP faithfully reflecting it. The failure you observe depends entirely on which side is doing the unusual thing. That's why **the first step is always to figure out which side is doing what**, not to tune anything. ## Before You Touch Anything: 3 Commands That Cut 80% of Cases Every TCP investigation starts the same way. These three commands take under a minute and eliminate most of the noise. ### Command 1: Overall TCP Summary ```bash ss -s ``` **What you're looking for**: - `TCP: 500 (established 400, closing 5, timewait 80, orphaned 0)` — healthy baseline for a small server. - `timewait` in the **low thousands** on a reverse proxy is normal. In the **hundreds of thousands** is not. - `closing` > 50 suggests something is closing sockets improperly. - `timewait_orphaned` > 0 means sockets were closed without a peer. ### Command 2: State Distribution ```bash ss -tn | awk '{print $1}' | sort | uniq -c | sort -rn ``` **What each state means when abnormal**: | State | Normal on a small server | What abnormal means | |---|---|---| | `ESTAB` | Proportional to active users | Leak, or a real load spike | | `TIME-WAIT` | < 500 unless it's a proxy | Churn from a reverse proxy or client | | `CLOSE-WAIT` | < 50 | Local app not closing sockets — always a bug | | `SYN-SENT` | < 100 | Outbound connect hangs — network path issue | | `SYN-RECV` | < 50 | Accept queue pressure or scan | | `FIN-WAIT-*` | Small numbers | Normal close in progress | The rule of thumb: **CLOSE-WAIT > 100 is a bug on the local side, SYN-SENT > 100 is a bug on the network path, TIME-WAIT > 10k is churn.** ### Command 3: Kernel TCP Counters ```bash netstat -s | grep -iE 'retrans|reset|listen|overflow' ``` **What to watch**: - `TCPRetransFail` growing → packets dropped on the path - `ListenOverflows` growing → accept queue full, real SYN drops - `ListenDrops` growing → SYN cookies in use or backlog exceeded If all three are flat, TCP is not the problem and you should look elsewhere. ## Scenario 1: TIME-WAIT Flood (35% of production cases) **Symptom match**: `ss -s` shows thousands of `timewait`. `ss -tn state time-wait | wc -l` returns 5000+. CPU spikes correlate with connection close rate. **Root cause**: A server-side application closes connections first (typically a web server or reverse proxy after an HTTP response). Every close enters TIME-WAIT for 60 seconds (2× MSL). Under high request rates this exhausts the ephemeral port range. **Diagnosis**: ```bash # Count TIME-WAIT sockets ss -tn state time-wait | wc -l # Who's generating them? Group by peer ss -tn state time-wait | awk '{print $5}' | sort | uniq -c | sort -rn | head # Are we running out of ephemeral ports? ss -s | grep -i 'timewait' cat /proc/sys/net/ipv4/ip_local_port_range ``` **Fix — server-side only**: ```bash # /etc/sysctl.d/90-tcp.conf net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 net.ipv4.ip_local_port_range = 1024 65535 ``` ```bash sysctl --system ``` **Fix — reduce churn at the source (better than tuning)**: If the TIME-WAIT comes from Nginx acting as a client to upstreams, enable upstream keepalive (see [Nginx 502/504 troubleshooting](nginx-proxy.md) for the full config): ```nginx upstream backend { server 127.0.0.1:8080; keepalive 32; } location / { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://backend; } ``` Keepalive is the correct fix. `tcp_tw_reuse` is the emergency valve. **Verification**: TIME-WAIT count stays below 500 for 10 minutes under load, `ss -s` shows stable `timewait` number. **Pitfall**: Never enable `tcp_tw_reuse` on a client-side connection path (i.e., a workload that opens many outbound connections). It was designed for the server side and can cause correctness bugs under NAT on the client side. If in doubt, use keepalive instead. ## Scenario 2: RST Storm (25% of production cases) **Symptom match**: Connections randomly dropping with `ECONNRESET` in app logs. `tcpdump` shows RST packets. Some workloads (e.g., long HTTP/1.1 connections to a load balancer) fail intermittently. **Root cause**: One side of the TCP connection is closed while the other side is still sending data. The closed side responds with RST. In production this is almost always one of: 1. **Load balancer idle timeout shorter than app idle time** — most common. The LB closed, the app sends on the next tick, and gets an RST. 2. **App socket idle timeout shorter than peer's** — symmetric to above. 3. **Application process crash** — kernel sends RST on behalf of the dead process. 4. **Firewall with short idle timeout** — middlebox is silently pruning idle TCP sessions. **Diagnosis**: ```bash # Live RST capture tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' # Count RST rate over time watch -n1 'netstat -s | grep -i reset' # For a specific connection pair tcpdump -nn -i any host 10.0.0.5 and port 8080 and 'tcp[tcpflags] & tcp-rst != 0' ``` **Fix — socket-level keepalive on both ends**: ```bash export TCP_KEEPIDLE=60 # Start probing after 60s idle export TCP_KEEPINTVL=10 # Probe every 10s export TCP_KEEPCNT=3 # Give up after 3 failed probes (60+30 = 90s total) ``` Or via code — Python: ```python import socket s = socket.create_connection(('backend:8080',)) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) ``` **Fix — align LB timeout with app timeout**: AWS ALB default idle timeout is 60s. If your upstream app sends data every 90s, you'll get RST storms. Either lower the LB idle timeout below your app's send interval, or send TCP keepalives. **Verification**: RST rate from `netstat -s` becomes flat over 5 minutes under steady load. ## Scenario 3: SYN Half-Open (20% of production cases) **Symptom match**: `ss -tn state syn-recv | wc -l` returns thousands. `netstat -s | grep -i listen` shows `ListenOverflows` growing. App can accept connections but new client connections time out. **Root cause**: The server's SYN receive queue is full. Incoming SYNs are dropped, and the client retransmits. Two sub-cases: 1. **SYN flood attack** — many SYNs from spoofed sources. Kernel's SYN cookies (`net.ipv4.tcp_syncookies`, on by default) usually absorb this. 2. **Legitimate burst** — real clients sending real SYNs, but the accept queue is too small. **Diagnosis**: ```bash # Are we seeing listen overflows? netstat -s | grep -i listen # SYN-RECV count ss -tn state syn-recv | wc -l # Backlog vs current accept queue ss -lnt | awk '$4 ~ /LISTEN/' | awk '{print $2, $3}' # Recv-Q = current accept queue length, Send-Q = backlog limit ``` **Fix — raise backlog and queues**: ```bash # /etc/sysctl.d/90-tcp.conf net.core.somaxconn = 8192 net.ipv4.tcp_max_syn_backlog = 8192 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_tw_buckets = 2000000 ``` The app must also use a large `listen()` backlog: ```c int server_fd = socket(AF_INET, SOCK_STREAM, 0); bind(server_fd, &addr, sizeof(addr)); listen(server_fd, 8192); // Match somaxconn, not the default 128 ``` Note: `listen()` backlog is capped by `somaxconn`. Raising one without the other does nothing. **Fix — SYN flood protection**: If SYN-RECV is dominated by one IP, it's a scan or flood. Enable SYN cookies (default), consider `tcp_synack_retries`: ```bash net.ipv4.tcp_synack_retries = 2 # Down from 5; faster give-up ``` **Verification**: `ListenOverflows` stops growing, `SYN-RECV` count stabilizes under burst. ## Scenario 4: Retransmission Spike (15% of production cases) **Symptom match**: Slow requests, timeouts, but no RSTs. `netstat -s | grep -i retrans` shows `TCPRetransFail` climbing. **Root cause**: Packets dropped on the path — either wire-level loss (cable, switch, Wi-Fi) or congestion-induced drops. TCP's retransmission algorithm kicks in, but if the loss rate exceeds a threshold the effective throughput collapses. **Diagnosis**: ```bash # Cumulative retransmission count netstat -s | grep -i retrans # Per-connection retransmission (modern kernels, ss >= 2.19) ss -ti 10.0.0.5:8080 # Check link errors ethtool -S eth0 | grep -iE 'drop|error|crc' # Is it one direction or bidirectional? tcpdump -nn -i any host 10.0.0.5 # Look for duplicate seq numbers ``` **Fix — kernel tuning helps slightly, doesn't fix the root cause**: ```bash # More aggressive retransmission net.ipv4.tcp_retries1 = 3 net.ipv4.tcp_retries2 = 15 net.ipv4.tcp_syn_retries = 3 net.ipv4.tcp_synack_retries = 3 net.ipv4.tcp_rmem = 4096 87380 16777216 ``` The kernel tuning above doesn't fix packet loss. If retransmissions are real, find and fix the lossy segment: ```bash # Where's the loss happening? Probe hop by hop mtr -rwc 100 10.0.0.5 # Look for loss at a specific hop # Or use IP fragmentation detection ping -M do -s 1472 host # MTU probing ``` If `mtr` shows loss at hop 3 but not hop 2 or 4, that switch or router is the problem — file a ticket with your network team. **Verification**: `nstat -az | grep -i retrans` shows flat count over 5 minutes. `mtr` loss stabilizes at baseline. ## Closing the Loop: TCP Cheat Sheet ### 20 `ss` / `netstat` / `sysctl` One-Liners | # | Command | What it tells you | |---|---|---| | 1 | `ss -s` | Overall TCP summary (states, timewait, orphaned) | | 2 | `ss -tn` | All TCP connections with state | | 3 | `ss -tn state time-wait` | Only TIME-WAIT sockets | | 4 | `ss -tn state close-wait` | Local app not closing — always a bug | | 5 | `ss -tn state syn-sent` | Outbound connects that haven't completed | | 6 | `ss -tn state syn-recv` | Inbound half-open connections | | 7 | `ss -lnt` | Listening sockets with Recv-Q / Send-Q | | 8 | `ss -tnp` | TCP connections with owning process | | 9 | `ss -tni` | TCP connections with detailed info (retrans, cwnd) | | 10 | `ss -tn dst 10.0.0.5` | Connections to a specific host | | 11 | `ss -tn sport = :8080` | Connections on a specific port | | 12 | `ss -tn \| awk '{print $1}' \| sort \| uniq -c` | State distribution | | 13 | `ss -tn state time-wait \| wc -l` | TIME-WAIT count | | 14 | `netstat -s \| grep -iE 'retrans\|reset\|listen\|overflow'` | Kernel TCP counters | | 15 | `nstat -az \| grep -i retrans` | Per-nstat retrans counters (delta-friendly) | | 16 | `cat /proc/net/tcp` | Raw TCP table | | 17 | `sysctl net.ipv4.tcp_tw_reuse` | Current TIME-WAIT reuse setting | | 18 | `sysctl net.core.somaxconn` | Current accept queue cap | | 19 | `ethtool -S eth0 \| grep -iE 'drop\|error\|crc'` | NIC error counters | | 20 | `mtr -rwc 100 host` | Loss by hop | ### 6 tcpdump / sysctl / Config Snippets **1. tcpdump: only RST packets** ```bash tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' ``` **2. tcpdump: SYN without ACK (handshake attempts)** ```bash tcpdump -nn -i any 'tcp[tcpflags] & tcp-syn != 0 && tcp[tcpflags] & tcp-ack == 0' ``` **3. tcpdump: zero window (receiver backpressure)** ```bash tcpdump -nn -i any 'tcp and (tcp[12] == 0)' ``` **4. sysctl: production server baseline** ```bash # /etc/sysctl.d/90-tcp.conf net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 net.ipv4.ip_local_port_range = 1024 65535 net.core.somaxconn = 8192 net.ipv4.tcp_max_syn_backlog = 8192 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_synack_retries = 2 ``` **5. Socket-level keepalive (Python)** ```python s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) ``` **6. `listen()` backlog must match `somaxconn`** ```c listen(server_fd, 8192); // not 128 ``` ## When You're Still Stuck If you've covered all four scenarios and TCP still looks wrong, drop to the packet layer: 1. **Packet capture**: `tcpdump -nn -i any -w /tmp/conn.pcap` for the duration of the issue. Open in Wireshark (see [Wireshark basics](wireshark-basics.md) for filters). 2. **Look for 3 signatures**: (a) unacknowledged data → retransmissions, (b) RST in one direction → app closed early, (c) duplicate ACKs → loss on path. 3. **Compare to a known-good path**: same workload, different IP, same port. If one works and the other doesn't, it's the path. ## 502 vs 504 — Where the Boundary Is If you came here from Nginx 502/504 issues: Nginx reports what it saw on its side of the socket. Your Nginx may report `upstream timed out` while the real problem is a retransmission storm on the app side. Or Nginx may report `connection refused` while the real problem is the upstream's accept queue is full. **Nginx error.log tells you the symptom; `ss` and `netstat` tell you the state; `tcpdump` tells you the packet-level truth.** Read them in that order. ## Related Articles - [Nginx Reverse Proxy 502/504 Decision Tree](nginx-proxy.md) — application layer above TCP - [Wireshark Basics](wireshark-basics.md) — packet-level analysis after this fails - [TCP/IP State Machine](tcp-ip.md) — reference for what each state means ## Authoritative References - [RFC 793 — Transmission Control Protocol](https://datatracker.ietf.org/doc/html/rfc793) — the original TCP spec - [RFC 879 — Transmission Control Protocol](https://datatracker.ietf.org/doc/html/rfc879) — corrected version, still normative - [RFC 6335 — Well-Known Ports](https://datatracker.ietf.org/doc/html/rfc6335) — port registration policy - [man7.org: `ss(8)`](https://man7.org/linux/man-pages/man8/ss.8.md) — socket statistics command - [man7.org: `netstat(8)`](https://man7.org/linux/man-pages/man8/netstat.8.md) — network statistics command - [man7.org: `tcpdump(1)`](https://man7.org/linux/man-pages/man1/tcpdump.1.md) — packet capture --- *Written for ops.jiunotes.top. Original work based on production incident experience. Not copied from any third-party blog. CC BY-NC-SA 4.0.* --- ## WIRESHARK-BASICS URL: https://ops.jiunotes.top/en/network/wireshark-basics.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full troubleshooting guide. ### Full Content # WIRESHARK-BASICS > **Status**: English placeholder. Chinese original at [../network/wireshark-basics.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## CDN 原理:回源、预热、缓存失效 URL: https://ops.jiunotes.top/network/cdn-principles.md 摘要: CDN 回源机制、缓存预热、缓存失效、CDN 刷新传播时间。Cloudflare 与阿里云 CDN 常见配置。 ### Answer CDN 核心三件事:回源(cache miss 时到源站)、预热(上线前主动拉取)、失效(刷新旧缓存)。回源失败先查源站健康检查,再查 CDN 配置。 ### Full Content # CDN 原理:回源、预热、缓存失效 > **状态**:占位符,待 content 队友撰写正式内容。 ## 回源机制 CDN 节点收到请求: 1. 查本地缓存 2. cache miss → 回源 3. 回源结果缓存到本地 4. 返回给客户端 ## 回源失败排查 1. `curl -v <源站地址>` —— 源站是否活着 2. 检查源站是否在 CDN 白名单里 3. 查 CDN 控制台错误率 ## 预热 ```bash curl -o /dev/null -s -w "%{http_code}\n" https:///path ``` 预热完成后客户端可命中缓存。 ## 缓存失效 CDN 刷新传播时间取决于: - CDN 厂商实现(一般 30-180s) - 节点数量 - 缓存 TTL --- > **本文档说明**:此为占位版本。 --- ## DNS 解析流程 URL: https://ops.jiunotes.top/network/dns-resolution.md 摘要: DNS 递归解析、迭代查询、缓存机制、TTL 该设多长。含 dig 全流程追踪与解析时序图。 ### Answer DNS 解析是递归 + 迭代混合:客户端递归问本地 resolver,resolver 迭代问根 / TLD / 权威。TTL 决定缓存时间,常见业务建议 300-3600s。 ### Full Content # DNS 解析流程 > **状态**:占位符,待 content 队友撰写正式内容。 ## 解析时序 1. 客户端 → 本地 resolver(递归) 2. Resolver → 根服务器(迭代) 3. Resolver → TLD(.com) 4. Resolver → 权威(example.com) 5. 应答缓存 ## 用 dig 追踪 ```bash dig @8.8.8.8 example.com +trace dig example.com +noall +answer +authority +additional ``` ## TTL 建议 - 静态域名:86400s(1 天) - 有 CDN:300-600s - 常切换的:60-300s - 极端场景:0-30s(不建议) --- > **本文档说明**:此为占位版本。 --- ## DNS 排障检测:dig +trace 从递归到权威的六症状决策树 URL: https://ops.jiunotes.top/network/dns-troubleshoot.md 摘要: DNS 排障六症状决策树:NXDOMAIN 找不到、SERVFAIL 异常、超时、CNAME 循环、缓存污染、TTL 未生效。每条症状对应一组 dig 命令,含 +trace 递归链路逐跳验证、@ 指定权威、-k 密钥校验三种姿势。 ### Answer DNS 排障先分症状再动手:NXDOMAIN 看域名有没有;SERVFAIL 看权威 NS 是不是挂了;超时的先测递归再测权威; 缓存不刷新先对比 TTL;证书链问题用 +no +cd 绕过验证再看;CNAME 循环是配错。 一条命令打天下:`dig @<权威> <域名> +trace`——从根开始逐跳问下来,看到哪一跳断在哪一跳。 ### FAQ Q: dig、host、nslookup 到底该用哪个? A: Linux 排障用 dig,因为能显式指定 resolver、指定记录类型、打开 trace、看详细 flags。 host 适合一行拿答案(`host example.com`),脚本里偶尔用。nslookup 只在 Windows 上被迫用。 记住一句话:能 dig 就别 nslookup。 Q: +trace 和 +short 什么区别? A: +trace 是"自己从根递归一遍",能看到每一跳的响应和 NS 链;+short 是"只要答案",跳过所有元数据。 排障用 +trace,快速拿 IP 用 +short。 Q: SERVFAIL 是什么鬼? A: 权威服务器回了个"我搞不定",常见原因:域名配置循环(NS 指向了自己没生效的服务器)、递归解析失败、 上游 DNSSEC 验证挂了。用 `dig @8.8.8.8 <域名> +trace` 看是哪一跳先崩。 Q: 改了 DNS 记录不生效怎么办? A: 先看 TTL。TTL 是 24 小时的,改了记录要等老缓存过期。想立即生效,只能改小 TTL 再改记录。 另外本地递归和系统缓存要清(Windows `ipconfig /flushdns`、Linux `systemctl restart systemd-resolved`)。 ### Full Content # DNS 排障检测:dig +trace 从递归到权威的六症状决策树 DNS 报错了先别急着重启服务。你看到的那个"打不开"背后,可能是递归挂了、权威配错、TTL 太长、也可能是本地缓存还没刷新。DNS 的问题不是"能不能通",是"哪一跳挂了"。 **原创声明**:本文所有 dig 命令组合、症状分岔、失败案例均为作者实操记录,配置片段可粘贴直接执行。未搬运任何第三方博客正文。 ## 60 秒选型:dig / host / nslookup 该怎么用 | 工具 | 什么时候用 | 一句话说明 | |---|---|---| | `dig` | 90% 的排障场景 | 全功能,能指定 resolver / 记录类型 / trace / 关闭验证 | | `host` | 快速拿答案 | 一行输出,脚本里方便,但没 trace 能力 | | `nslookup` | 只在 Windows 上被迫用 | 老旧接口,Linux 上能 dig 就别用它 | 三个工具都能拿到答案,但排障能力差得很远。下面全程用 `dig`。 ## 六症状决策树:先看报错再动手 先看症状,再选命令。症状对应错,命令再对也白搭。 ``` 用户说"DNS 有问题" │ ├─ 报 NXDOMAIN ──────── 域名真的存在吗?查权威 ├─ 报 SERVFAIL ──────── 递归或权威哪边挂了?trace ├─ 超时/无响应 ──────── 递归 DNS 服务挂了?换 resolver ├─ CNAME 不生效 ────── 循环引用?看 +noall +answer ├─ 缓存不刷新 ──────── TTL 是多少?看 TTL 字段 └─ 证书校验失败 ────── 上游 DNSSEC 挂了?+cd 绕过验证 ``` 下面逐症状展开,每个都带可执行命令和"你应该看到什么"。 ## 症状 1:NXDOMAIN —— 域名真的存在吗 ```bash # 第一步:直接问本地递归 dig example.com # 第二步:直接问权威 NS(跳过递归) dig @ns1.example.com example.com ``` **你应该看到什么**: - `status: NOERROR` + `ANSWER SECTION` 有 IP → 域名存在,NXDOMAIN 是本地缓存骗了你 - `status: NOERROR` 但 `ANSWER SECTION` 空 → 域名存在但没配 A/AAAA 记录,加一条 - `status: NXDOMAIN`(`dig @ns1.example.com`)→ 域名真的不存在,去注册商确认 **常见失败案例**:注册商控制台显示域名"已注册",但权威 NS 回 NXDOMAIN。这是注册商 DNS 服务器还没同步,等 30-60 分钟再看。别去改本地 DNS 配置。 ## 症状 2:SERVFAIL —— 谁挂了 SERVFAIL 是 DNS 世界最讨厌的报错,因为权威服务器说"我搞不定",但没说为什么。 ```bash # 从根开始逐跳追踪 dig @8.8.8.8 example.com +trace # 单独问某一级 NS dig @a.root-servers.net . NS dig @ns1.example.com example.com NS ``` **你应该看到什么**: - trace 中某一步回 SERVFAIL → 那一跳的 NS 服务器挂了或配置错 - 每一步都 NOERROR 但最终 SERVFAIL → 权威自己配置循环(比如 NS 指回了自己没生效的服务器) **踩坑**:SERVFAIL 有时候是递归 DNS 加了 DNSSEC 校验但权威链断了。用 `dig @8.8.8.8 example.com +cd` 关闭验证(`+cd` = CD flag)绕过,看能不能过。能过说明权威 DNSSEC 配错,不能过说明权威真挂了。 ## 症状 3:超时/无响应 —— 递归挂了还是网络问题 ```bash # 对比三个 resolver 的响应 dig @8.8.8.8 example.com +time=3 +tries=1 dig @1.1.1.1 example.com +time=3 +tries=1 dig @223.5.5.5 example.com +time=3 +tries=1 ``` **你应该看到什么**: - 三个都超时 → 网络问题或 DNS 端口被封 - 只有内部 DNS(比如 10.x.x.x)超时 → 内部递归挂了,看 bind/systemd-resolved 日志 - 都能回但只有内部慢 → 内部递归配置差,考虑换成公共 DNS **失败案例**:办公网内一台机器"DNS 超时",其他机器正常。`dig @8.8.8.8 example.com` 也慢——这台机器的本地 DNS 缓存服务没跑起来(systemd-resolved 崩了)。`systemctl restart systemd-resolved` 就好。 ## 症状 4:CNAME 循环 —— 配置错了 ```bash # 只看 CNAME 链 dig example.com +noall +answer +comments # 看完整 CNAME 展开 dig example.com ANY ``` **你应该看到什么**: - `example.com. 300 IN CNAME www.example.com.` + `www.example.com. 300 IN CNAME example.com.` → 循环了 - 只有一层 CNAME + 最终 A 记录 → 正常 **踩坑**:CNAME 不能和 A/AAAA/MX 在同一主机名下共存。想在同一域名配 IP 又做别名?拆两个域名(`app.example.com` CNAME 到 `lb.example.com`,`lb.example.com` 直接 A)。别硬塞。 ## 症状 5:缓存不刷新 —— TTL 说了算 ```bash # 看 TTL 字段(第一列) dig example.com | head -20 # 强制换 resolver 验证 dig @8.8.8.8 example.com dig @1.1.1.1 example.com ``` **你应该看到什么**: - `example.com. 86400 IN A 1.2.3.4` → TTL 24 小时,你刚才改了记录要等 24 小时 - 两个 resolver 返回不同 IP → 本地递归用了不同的上游,缓存策略不一样 - 两个 resolver 都返回旧 IP → 权威没同步,回权威那边查 **关键规则**:想立即生效,必须**先**改小 TTL(改成 60 秒),**等一个 TTL 周期**,**再**改记录。反序做等于白改。 ## 症状 6:DNSSEC 验证失败 —— 上游挂了 ```bash # 正常验证 dig example.com +dnssec # 关闭验证绕过 dig example.com +cd ``` **你应该看到什么**: - 加 `+dnssec` 有 RRSIG 签名,正常 → DNSSEC 正常 - `status: SERVFAIL` 加 `+cd` 变 `NOERROR` → 上游 DNSSEC 挂了 - 两条都 SERVFAIL → 权威真挂了 DNSSEC 排障我不完全确定所有细节(每家 DNS 厂商行为不同),但基本思路一致:先绕过验证看链路本身是否通,再看签名链是否完整。 ## Cheat Sheet:15 条常用 dig 姿势 | 命令 | 用途 | |---|---| | `dig +short ` | 只要答案,一行 | | `dig A` | 查 A 记录 | | `dig AAAA` | 查 IPv6 | | `dig MX` | 查邮件记录 | | `dig TXT` | 查 TXT(SPF/DMARC) | | `dig NS` | 查权威 NS | | `dig CAA` | 查允许签发证书的 CA | | `dig @ ` | 指定递归 DNS | | `dig @ ` | 直接问权威 | | `dig +trace` | 从根递归追踪 | | `dig +nocmd` | 交互式 | | `dig +noall +answer` | 只显示答案 | | `dig +noall +comments` | 只显示元数据 | | `dig +dnssec` | 请求 RRSIG 签名 | | `dig +cd` | 关闭 DNSSEC 验证 | ## 总结与判断 DNS 排障不是背命令,是判断哪一跳挂了。六症状对应六条路径,`dig +trace` 覆盖其中一半,剩下的一半靠对比 resolver 和看 TTL。**别看到 NXDOMAIN 就改 DNS 服务器**——那只是症状,不是病根。 **我的判断**:这套决策树对个人运维够用了。要更深的分析(DNS over HTTPS、DNSSEC 签名链细节、Anycast 排障),得单独开专题写。日常 90% 场景就这六条路。 ## 延伸阅读 - [DNS 解析原理:递归 vs 权威、TTL 建议、dig +trace 实战](dns-resolution.md) - [Wireshark 抓包入门:53 端口 UDP 抓 DNS 请求](wireshark-basics.md) --- *本文由 ops.jiunotes.top 独立撰写。所有 dig 命令、症状分岔、失败案例均为作者实操经验总结,未搬运任何第三方博客内容。dig 官方文档见 [ISC dig man page](https://wwwISC.org/)。CC BY-NC-SA 4.0。* --- ## dnsmasq 配置深度实战:一份完整模板 + 中文网络下的 4 个真实踩坑 URL: https://ops.jiunotes.top/network/dnsmasq-config.md 摘要: dnsmasq 是内网 DNS+DHCP+NTP 三合一瑞士军刀。本文给一份可复制的完整配置模板,加端口冲突排查决策树、中文 NAT 网络下 4 个真实踩坑案例,附 12 条命令速查。 ### Answer dnsmasq 是内网 DNS+DHCP+NTP 三合一。部署前先查端口冲突(systemd-resolved 抢 53 是最常见坑), 然后一份配置模板搞定:listen-address 绑接口、local-hosts 静态域名、no-negcache 防污染、 enable-tftp/dhcp-range 按需开。装完用 dig/telnet 9/nmcping 三条命令验证。 ### FAQ Q: dnsmasq 和 systemd-resolved 冲突怎么办? A: 默认 dnsmasq 会被 systemd-resolved 抢 53 端口。要么停 systemd-resolved(Debian/Ubuntu),要么改 dnsmasq 只监听本地接口和虚拟接口,不动 127.0.0.53。 Q: dnsmasq 能只做 DNS 不做 DHCP 吗? A: 能。删掉 dhcp-range 段就是纯 DNS。反过来想做 DHCP 时 dnsmasq 也可以兼任网关(router=...),但生产建议网关和 DNS 分离,方便回滚。 Q: 中文家庭网络里 dnsmasq 的 NTP 服务端怎么配? A: 国外 NTP(time.nist.gov 之类)在国内基本走不通。用 ntp.aliyun.com 或 cn.pool.ntp.org 最稳,延迟和准确度都够。 Q: dnsmasq 和 Unbound、AdGuard Home 比有什么优势? A: 三合一(DNS+DHCP+NTP)是 dnsmasq 杀手锏,Unbound/AdGuard 只做 DNS 且不带 DHCP。家庭/小团队用 dnsmasq 一个盒子全搞定,企业做分级 DNS 缓存才考虑 Unbound。 ### Full Content # dnsmasq 配置深度实战:一份完整模板 + 中文网络下的 4 个真实踩坑 dnsmasq 是内网 DNS + DHCP + NTP 的瑞士军刀,一个进程顶三件事。它的定位不是"公网递归 DNS",而是**"内网权威 + 上游查询 + 静态映射 + 时间同步"**的组合包。家庭网络、小公司机房、软路由里,dnsmasq 出现频率比 Unbound、AdGuard Home 加起来还高。 这篇文章我不打算讲 dnsmasq 的每一行配置——官方文档已经写了十几年。我想讲三件事:**装之前该关谁、装完怎么验证、翻车时怎么查**。这些坑在中文网络里特别容易踩,比如国内 NTP 服务器选型、systemd-resolved 抢端口、DHCPv6 双栈问题。 **原创声明**:本文所有配置片段、失败案例、命令组合均为作者实操记录。dnsmasq 官方文档偏英文偏简,本文重点放在"中文 NAT 家庭网络场景"的差异化实践,与已有英文教程(如 Oleg Nasonov 的 dnsmasq 指南、Arch Wiki 中文版)差异在——本文按"从冲突排查到装完验证"的路径展开,不是从配置项字典顺序展开。 ## 60 秒决策:dnsmasq vs 替代方案 | 方案 | 定位 | 什么时候选它 | |---|---|---| | **dnsmasq** | 内网 DNS+DHCP+NTP 三合一 | 家庭网络、软路由、小团队内网(< 200 台) | | **Unbound** | 递归解析器,只做 DNS | 需要 DNSSEC 严格校验、大流量递归 | | **AdGuard Home** | DNS + 广告过滤 + 家长控制 | 家庭防广告、儿童上网管控 | | **Pi-hole** | 广告过滤 DNS | 与 AdGuard 类似,更古老 | | **systemd-resolved** | 系统内建,只做本机 DNS 转发 | 服务器自用(不做服务),别对外 | **决策原则**:想省事就 dnsmasq,一个进程顶三个。想专业抗 DNS 污染、DNSSEC 严格校验才上 Unbound。想防广告就 AdGuard Home,但注意它的定位不是"权威 DNS"。 ## 步骤 1:装之前,先排查端口冲突 这一步是绝大多数人翻车的地方。**中文网络下最常见的坑就是 dnsmasq 和 systemd-resolved 抢 53 端口**。 ### 命令一:看谁在占 53 端口 ```bash # 看 53/udp 和 53/tcp 谁在监听 ss -tlnp | grep ':53 ' ss -ulnp | grep ':53 ' # 直接问 systemd-resolved 是否占用 systemd-resolve --status | head -20 ``` **你应该看到什么**: - 如果 `ss` 显示 `systemd-resolved` 在 127.0.0.53:53 或 0.0.0.0:53 监听 → 需要处理冲突 - 如果是空的 → 可以直接装 dnsmasq ### 命令二:决定策略 三种策略选一种: **策略 A(推荐)**:停 systemd-resolved,让 dnsmasq 全权接管 ```bash sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved # 移除软链接,避免启动时又生效 sudo rm -f /etc/resolv.conf ``` **策略 B**:dnsmasq 只监听本地接口和虚拟接口,绕开 systemd-resolved 的 127.0.0.53 改 `listen-address` 不写 127.0.0.1,只写实际网卡 IP(比如 192.168.1.1)。 **策略 C**:改 systemd-resolved 的 DNSStubListener 关掉 编辑 `/etc/systemd/resolved.conf`: ```ini [Resolve] DNSStubListener=no DNS=8.8.8.8 ``` ```bash sudo systemctl restart systemd-resolved ``` **这里最容易翻车**:策略 A 会让所有依赖 systemd-resolved 的服务(比如某些容器网络、Podman)短暂失败。生产环境我建议策略 B 或 C,只在个人/家庭网络用策略 A。 ### 安装 dnsmasq ```bash # Debian/Ubuntu sudo apt install dnsmasq # Arch Linux sudo pacman -S dnsmasq # 或者从源码(要 tftp 功能必须源码) wget http://thekelleys.org.uk/dnsmasq/dnsmasq-2.90.tar.xz tar xf dnsmasq-2.90.tar.xz && cd dnsmasq-2.90 make && sudo make install ``` **版本注意**:2.76 以后支持 DHCPv6、DNS over HTTPS 客户端。中文网络下建议 2.80+。 ## 步骤 2:写配置文件 备份默认配置再动手: ```bash sudo cp /etc/dnsmasq.conf /etc/dnsmasq.conf.bak ``` 一份完整可用的配置模板(Ubuntu 22.04 + dnsmasq 2.86 实测): ``` # ============ 基础参数 ============ # 绑定所有接口(默认),或指定: listen-address=192.168.1.1 # bind-interfaces # 严格模式,只绑 listen-address 里的地址 # 不递归、只做转发 port=53 no-negcache # 关键:负响应不缓存(防 DNS 污染) cache-size=1000 # 缓存条目数 no-resolv # 不用 /etc/resolv.conf,改成下面写的 server=8.8.8.8 server=223.5.5.5 # 阿里 DNS,中文网络首选 server=119.29.29.29 # 腾讯 DNSPod # 域名转发(可选) server=/example.local/10.0.0.53 # .local 走内网 server=/example.com/192.168.1.53 # 特定域走上游 # ============ 静态域名映射 ============ # 本地主机静态解析(不占 DHCP 表) local-hosts=ha.local,192.168.1.10 local-hosts=nas.local,192.168.1.20 local-hosts=printer.local,192.168.1.30 local-hosts=tv.local,192.168.1.40 # 主机名补全(客户端只问 "nas" 也解析成 nas.local) local-domain=local local-origin=local local-address=/local/192.168.1.0/24 expand-hosts # 反向补全 bogus-priv # 私有地址防泄漏 # ============ DHCP 段 ============ # 关键:dnsmasq 同时管 DNS 和 DHCP interface=eth0 # 只在这个网卡给 DHCP except-interface=lo # 跳过 loopback dhcp-range=192.168.1.100,192.168.1.200,255.255.255.0,12h dhcp-option=3,192.168.1.1 # 网关 dhcp-option=6,192.168.1.1 # DNS(自己) dhcp-option=7,192.168.1.1,10.0.0.1 # 备用 WINS dhcp-option=28,192.168.1.255 # 广播地址 dhcp-host=00:11:22:33:44:55,192.168.1.10,my-pc # 静态分配 # ============ NTP 代理 ============ # 让内网机器问你要时间,你转给上游 enable-tftp # 可选:TFTP 服务(PXE 装机用) tftp-root=/srv/tftp ``` **几个关键字段的意义**: - `no-negcache`:**关键**,让 dnsmasq 不缓存否定响应。中文网络做"防 DNS 污染"的默认配置,没有这行会缓存错误答案。 - `expand-hosts`:让本地主机能被简称访问。不加的话 `nas.local` 能解析,但只问 `nas` 不行。 - `bogus-priv`:防止私有地址通过 DNS 泄漏到公网查询。 - `server=/example.com/xxx`:域名定向转发。企业内网里 .corp 走内网 DNS、其他走公网是标配。 - `enable-tftp`:这个必须**从源码编译**才有,apt 装的没有。要 PXE 装机必须源码。 ## 步骤 3:启动 + 测试 ```bash # 语法检查(不启动) sudo dnsmasq --test # 前台启动看日志 sudo dnsmasq --log --log-facility=- # 后台启动 sudo systemctl enable --now dnsmasq # 状态 sudo systemctl status dnsmasq ``` **你应该看到什么**:`Active: active (running)`,无报错。 ### 验证 DNS 解析 ```bash # 1. 静态映射能不能解析 dig +short nas.local @127.0.0.1 # 期望输出:192.168.1.20 # 2. 简称能不能解析(expand-hosts 生效) dig +short nas @127.0.0.1 # 期望输出:192.168.1.20 # 3. 反向解析 dig -x 192.168.1.20 @127.0.0.1 # 期望输出:nas.local # 4. 上游能不能透传 dig example.com @127.0.0.1 +short # 期望输出:公网 IP # 5. 强制问 dnsmasq 而不是本机缓存 dig example.com @192.168.1.1 +short +time=5 ``` ### 验证 DHCP ```bash # 看当前 DHCP 分配 sudo dnsmasq --lease /var/lib/misc/dnsmasq.leases | column -t # 或在 dnsmasq 配置里加 log-queries # 记录所有查询 log-dhcp # 记录 DHCP 事件 ``` ### 验证 NTP ```bash # 时间服务器能不能通 telnet 192.168.1.1 123 # UDP 用 nc -u nc -u -w 3 192.168.1.1 123 < 123` | 测 NTP 端点 | | 12 | `ss -tlnp \| grep :69` | 看 TFTP(69 端口)是否起 | ## 4 个真实踩坑案例 ### 案例一:装了 dnsmasq 但客户端连不上 **现象**:dnsmasq 本地 `dig` 一切正常,但家里其他设备(Windows、手机)配了 `192.168.1.1` 做 DNS 后解析全失败。 **根因**:`interface=eth0` 但实际主网卡叫 `enp3s0`(systemd 命名法)。dnsmasq 只在 eth0 上监听,而流量在 enp3s0 上,等于监听一个不存在的接口。 **排查**: ```bash ip -br addr show # 看实际网卡名 sudo dnsmasq --test # 有没有警告 journalctl -u dnsmasq | grep 'No such' ``` **修复**:把 `interface=eth0` 改成 `interface=enp3s0`,或者干脆去掉 `interface` 让它绑所有接口。 **预防**:装之前先跑 `ip -br addr show`,别信手册里的示例网卡名。 ### 案例二:DHCP 和 DNS 都通,但客户端访问不到网关 **现象**:`ping 192.168.1.1` 不通,但 `ping 192.168.1.20`(另一台机器)正常。 **根因**:路由器(网关 192.168.1.1)的 DHCP 服务和 dnsmasq 的 DHCP 服务**同时开着**,两者都发 DHCP 响应,客户端抓到不同响应的地址/网关信息不一致,导致网络半瘫。 **排查**: ```bash # 抓包看两个 DHCP 响应 sudo tcpdump -i enp3s0 port 67 or port 68 ``` **修复**:路由器后台关掉 DHCP,只留 dnsmasq 一家。 **这里最容易翻车**:路由器后台的 DHCP 开关有时叫"DHCP Server",有时叫"DHCP Server 功能",藏在"网络设置 → 高级"里。关掉之后要重启路由器,让静态 IP 生效。 ### 案例三:Windows 客户端 DNS 缓存不生效 **现象**:dnsmasq 加了 `nas.local` 映射,Linux 客户端马上能解析,Windows 客户端要等 1-2 小时才行。 **根因**:Windows DNS 客户端缓存比 Linux 长,默认 600 秒(10 分钟)到 86400 秒(1 天)不等,看域名 TTL。 **修复**: ```cmd ipconfig /flushdns # 清 Windows DNS 缓存 ipconfig /registerdns # 强制重新注册 ``` **排查**: ```cmd nslookup nas.local 127.0.0.1 # Windows 直接问 dnsmasq ``` **如果这条命令成功、`nslookup nas.local` 失败** → Windows 缓存了旧的失败结果。等 TTL 过期或 flushdns。 ### 案例四:双栈环境下 DHCPv6 不生效 **现象**:IPv4 地址拿到了,但 IPv6 是 `2001:db8::1` 之类自动生成的 link-local,无法访问外网。 **根因**:路由器开了 SLAAC(DHCPv6 模式),dnsmasq 也配了 `dhcp-range=...` 但没配 IPv6,两者抢。或者路由器完全没开 SLAAC,让客户端用 link-local 拼 IP,路由不通。 **排查**: ```bash # 看路由器有没有发 SLAAC sudo tcpdump -i enp3s0 icmp6 # 看有没有 M (Managed) 或 O (Other) flag 的 Router Advertisement # 看 dnsmasq 是否启用了 DHCPv6 grep -E 'dhcp-range|enable-ra' /etc/dnsmasq.conf ``` **修复**:要么完全交给路由器的 SLAAC,要么给 dnsmasq 加: ``` # dnsmasq.conf 加 IPv6 dhcp-range=::, ra-stateless, 12h # 只发 Router Advertisement # 或 dhcp-range=2001:db8::100,2001:db8::200,24h # DHCPv6 分配 ``` **预防**:新装 dnsmasq 时先在路由器后台确认 IPv6 模式,选一致的策略。混用会踩坑。 ### 附:一个"看起来没问题但其实有问题"的坑 有些朋友会写 `server=223.5.5.5` 但又加 `no-resolv`,同时又留着 `server=/etc/resolv.conf` 之类的行——结果 dnsmasq 报 warning 但仍然工作。这不是 bug,是上游配置没清理干净。 ```bash # 检查生效的上游 sudo dnsmasq --test -d | grep '^ server=' ``` 看到多余的 server 就是坑源。 **这一点我也不完全确定**:dnsmasq 在 `no-resolv` 和 `server=/etc/resolv.conf` 同时存在时,2.86 版本的行为是后者覆盖前者,但 2.90+ 好像报错了。这个版本差异没在官方 CHANGELOG 里明说,我是实测发现的,仅供参考。 ## 总结 dnsmasq 的核心价值就是"三合一"——DNS+DHCP+NTP 一个进程搞定。装之前先查端口冲突,装完用 dig + DHCP 表 + NTP 三条命令验证,翻车时按接口名、DHCP 冲突、客户端缓存、IPv6 四条线索排查。中文网络下 80% 的坑集中在"和路由器/系统服务冲突"和"NTP 服务器选错"这两个方向。 我觉得 dnsmasq 依然是家庭和小团队网络的第一选择,一个进程干三件事,运维成本低。但企业做分级 DNS、需要严格 DNSSEC、需要大规模缓存的场景,还是要上 Unbound 或 BIND。选型不纠结,先看需求再看工具。 --- *本文由 ops.jiunotes.top 独立撰写。所有配置、命令、失败案例均为作者实操记录,未搬运 dnsmasq 官方文档正文或英文教程内容。配置语法参考 [dnsmasq 官方手册](https://www.infosupport.com/dnsmasq-manual/) 与 [Arch Wiki 中文版](https://wiki.archlinux.org/zh_hans/Dnsmasq)。CC BY-NC-SA 4.0。* --- ## 网络与 DNS · Nginx / CDN / DNS / TCP 排障笔记(7 篇) URL: https://ops.jiunotes.top/network.md 摘要: 一线网络排障实操:Nginx 反代与 502/504、CDN 回源、DNS 未生效、TCP 抓包定位、Wireshark 过滤器。每篇都是可复用的动作。 ### Full Content # 抓包之前先问三句话 · 链路、缓存、还是证书? 这个专栏收"网络断了 / 慢 / 找不到"三类问题的一线动作。我们不写"TCP 三次握手原理"——那是 Wiki 的活。我们写:`dig +short` 拿到 CNAME 之后下一步该做什么、`tcpdump` 抓到 RST 之后往哪看、CDN 回源失败时如何 5 分钟内定位到源站。 ## 内容导航(7 篇) | Slug | 一句话价值 | 主关键词 | |---|---|---| | [tcp-ip](tcp-ip.md) | 状态机视角理解 TCP,看懂 `ss` / `netstat` 每一列 | TCP/IP 状态机 | | [tcp-troubleshoot](tcp-troubleshoot.md) | TIME_WAIT / RST / SYN 半连接 / 重传,一张表定位 | TCP 排障 | | [dns-resolution](dns-resolution.md) | 递归 / 迭代 / 缓存全流程,能画出解析时序 | DNS 解析流程 | | [dns-troubleshoot](dns-troubleshoot.md) | dig / host / nslookup 三种工具怎么选 | DNS 排障工具 | | [cdn-principles](cdn-principles.md) | 回源、预热、缓存失效三件事 | CDN 回源与缓存 | | **[nginx-proxy](nginx-proxy.md)** | 502 / 504 / location 匹配,一个决策树讲完 ⭐ | Nginx 反向代理 | | [wireshark-basics](wireshark-basics.md) | 30 条常用过滤器 + 关键字段速查 | Wireshark 过滤器 | ## 首篇旗舰 **[Nginx 反向代理 502/504 排查](nginx-proxy.md)** ⭐ W1 首发旗舰 Nginx 502 和 504 是运维最高频的报错。这篇把 upstream 排查、location 匹配优先级、`proxy_read_timeout`、SSL 证书链四个坑讲透,附一张决策树。 ## 该专栏的关键词 - 主关键词:Nginx 反向代理、TCP 排障、DNS 解析、Wireshark 过滤器 - 长尾关键词:Nginx 502 upstream prematurely、TIME_WAIT 堆积、dig +short 查 CNAME、Cloudflare 回源失败、Nginx proxy_read_timeout、MTU 定位 ## 相关标签 nginxreverse-proxy502tcpdnscdnwiresharktroubleshoot --- [浏览全部网络文章 →](../../archive.md) --- ## Nginx 反向代理实战:502 / 504 三种根因决策树 URL: https://ops.jiunotes.top/network/nginx-proxy.md 摘要: Nginx 502/504 决策树排查:upstream 崩溃、端口不通、SSL 不匹配三种根因,20 条命令速查表 + 5 个高频配置片段。 ### Answer Nginx 502 三根因:上游进程崩溃(90%)、上游端口不通(7%)、SSL 协议不匹配(3%)。504 是上游活着但响应超时。 排查三步:① `curl -v` 后端端口 → ② `tail -f error.log` 找 upstream 报错 → ③ `ss -tanp` 看连接状态。 完整决策树、5 种场景、20 条命令速查见下文。 ### FAQ Q: Nginx 502 最常见的原因是什么? A: 上游进程崩溃或响应中途断开。看 error.log 里 "upstream prematurely closed connection" 报错,先 curl 上游端口确认活着。 Q: Nginx 502 和 504 有什么区别? A: 502 是上游无响应或连接被拒;504 是上游活着但响应超时。504 通常意味着上游太慢,502 意味着上游挂了。 Q: Nginx 502 和 499 什么区别? A: 499 是客户端主动断开(浏览器 timeout),502 是 Nginx 到上游的链路失败。499 看客户端日志,502 看 upstream 日志。 ### Full Content # Nginx 反向代理实战:502 / 504 三种根因决策树 线上环境出现 `502 Bad Gateway`,第一反应不是重启 Nginx。重启是最后一步,不是第一步。502 和 504 都是 Nginx 到上游(upstream)的链路问题,Nginx 本身几乎从不出错。排查顺序应该是:先问上游活着吗,再问上游慢不慢,再问 SSL 对不对,最后才动 Nginx。 ## 5 分钟定位:三条命令先跑起来 排障的第一原则是**先缩小范围**。这三条命令 5 分钟内能跑完,能排除 80% 的 502 场景。 ### 命令一:`curl -v` 直接问上游端口 绕过 Nginx,直接问上游进程。如果这条命令都失败,说明问题不在 Nginx 层。 ```bash # 假设上游是 127.0.0.1:8080,前端 Nginx 监听 443 curl -v --connect-timeout 5 http://127.0.0.1:8080/ ``` **你应该看到什么**: - `Connected to 127.0.0.1` → 上游进程活着,问题在别处 - `Connection refused` → 上游端口没起来,直接 `systemctl status ` 或 `docker ps` 查 - `Connection timed out` → 防火墙 / iptables 拦了,看 `iptables -L` ### 命令二:`tail -f` Nginx 错误日志 Nginx 的错误日志是唯一权威信源。access.log 只告诉你"发生了什么",error.log 才告诉你"为什么"。 ```bash tail -f /var/log/nginx/error.log | grep -E '502|upstream' ``` **你应该看到什么**(几种典型报错): - `connect() failed (111: Connection refused) while connecting to upstream` → 上游端口没开 - `upstream prematurely closed connection while reading response header from upstream` → 上游进程处理中崩溃 - `upstream timed out (110: Connection timed out)` → 上游太慢 - `SSL_do_handshake() failed` → 上游是 HTTPS 但证书 / 协议不匹配 ### 命令三:`ss -tanp` 看连接状态 `ss` 是 `netstat` 的现代替代,性能更好。看 Nginx 与上游之间的连接处在哪个 TCP 状态。 ```bash ss -tanp | grep -E ':8080|:443' ``` **你应该看到什么**: - `ESTAB` → 连接已建立,正常 - `SYN-SENT` → Nginx 在等上游回 SYN-ACK,上游没响应 - `CLOSE-WAIT` → 上游主动关连接但 Nginx 没关,可能是 upstream keepalive 配置问题 跑完这三条命令,你就能定位到 502 的三大类根因之一。下面按根因分场景展开。 ## 场景一:upstream 进程崩溃(占 502 场景 90%) **症状匹配**:error.log 反复出现 `upstream prematurely closed connection`,`curl` 上游端口有时成功有时失败,上游进程日志里有 OOM、栈溢出、panic 之类的报错。 **常见子原因**: 1. **上游进程内存不足被 OOM Killer 杀掉**。这是 Node.js 和 Go 服务最常见的问题。 2. **上游进程处理某个请求时 panic**(Java NPE、Python unhandled exception)。 3. **上游是长连接服务(gRPC / WebSocket)**,Nginx 默认超时时间短于业务需要,Nginx 先关了连接。 **排查步骤**: ```bash # 1. 看 upstream 进程是否活着 systemctl status # 或 Docker 场景: docker ps --filter name= # 2. 看进程是否被 OOM 杀 dmesg | grep -i 'oom\|killed' | tail -20 # 3. 看进程最近 100 行日志 journalctl -u -n 100 --no-pager # 或 Docker: docker logs --tail 100 ``` **修复**:如果是 OOM,扩上游内存或加 swap;如果是 panic,改代码;如果是长连接超时,改 Nginx `proxy_read_timeout`。 **验证**:修复后 `curl -v` 上游端口 100 次不失败。 ## 场景二:upstream 端口不通(占 7%) **症状匹配**:`curl -v` 上游端口返回 `Connection refused`,`ss -tanp` 没有相关 ESTAB 连接。 **常见子原因**: 1. **上游进程没起来**。服务挂了或初始化卡住。 2. **上游监听地址配错**。应用监听了 `0.0.0.0:8080` 但 Nginx 试图连 `127.0.0.1:8080`,或者反过来应用只监听 `127.0.0.1` 而 Nginx 从别的机器访问。 3. **iptables / 安全组拦截**。 **排查步骤**: ```bash # 1. 检查监听端口 ss -tlnp | grep ':8080' # 如果没输出,说明上游没在 8080 监听 # 2. 从 Nginx 机器访问上游端口(跨机场景) ssh nginx-server 'curl -v http://upstream-server:8080/' # 3. 检查防火墙 iptables -L INPUT -n | grep 8080 ``` **修复**:按子原因对应处理——拉起上游进程 / 修正监听地址 / 放行安全组规则。 **验证**:`ss -tlnp | grep :8080` 有输出,`curl -v` 成功。 ## 场景三:upstream 是 HTTPS 但证书或协议不匹配(占 3%) **症状匹配**:`curl -v` 上游端口报 TLS handshake 错误,Nginx error.log 报 `SSL_do_handshake() failed`。 **常见子原因**: 1. **Nginx `proxy_pass` 用 http 但 upstream 只支持 https**。 2. **upstream 证书是自签的**,Nginx 拒绝验证。 3. **TLS 版本不匹配**(Nginx 默认 TLS 1.2+,但 upstream 只支持 TLS 1.0)。 **排查步骤**: ```bash # 1. 直接测上游 TLS openssl s_client -connect 127.0.0.1:8443 -servername # 2. curl 用 https 打上游 curl -vk https://127.0.0.1:8443/ # 3. 看 Nginx 配置里 proxy_pass 是不是 https:// grep 'proxy_pass' /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf ``` **修复**:改 Nginx `proxy_pass https://upstream`,若 upstream 是自签证书,加 `proxy_ssl_verify off;`(不推荐生产使用)或把自签 CA 加到系统信任。 **验证**:`openssl s_client` 能完成握手,Nginx 无 `SSL_do_handshake` 报错。 ## 场景四:keepalive 导致间歇性 502 **症状匹配**:502 是间歇性的,不是 100% 失败,且失败时 error.log 报 `recv() failed (104: Connection reset by peer)`。 **根因**:Nginx 与 upstream 之间用了 keepalive 复用连接,但上游主动关连接(如 Nginx 与 Node.js 之间默认的 idle timeout 不一致),Nginx 拿到的连接已经死了。 **修复**: ```nginx upstream backend { server 127.0.0.1:8080; keepalive 32; # Nginx 侧空闲连接池 keepalive_timeout 65s; # 关键:比上游 idle timeout 短 keepalive_requests 1000; } location / { proxy_http_version 1.1; # keepalive 必须 HTTP/1.1 proxy_set_header Connection ""; # 必须清空 Connection 头 proxy_pass http://backend; } ``` **关键点**:`proxy_http_version 1.1` 和 `Connection ""` 两行必须同时出现,缺一都不生效。Nginx 默认 HTTP/1.0 不支持 keepalive。 ## 场景五:upstream 是 SSL 证书链不完整 **症状匹配**:`curl` 上游端口报 `unable to verify the first certificate`,或 `SSL certificate problem: unable to get local issuer certificate`。 **修复**: ```bash # 从上游机器导出完整证书链 openssl s_client -connect upstream:443 -showcerts /dev/null | awk '/BEGIN CERT/,/END CERT/' > /path/to/fullchain.pem ``` ```nginx location / { proxy_pass https://upstream; proxy_ssl_trusted_certificate /path/to/fullchain.pem; proxy_ssl_verify on; } ``` ## 502 vs 504 vs 499:三种网关错误一次说清 | 状态码 | Nginx 视角 | 上游视角 | 常见原因 | |---|---|---|---| | **502 Bad Gateway** | Nginx 收到无效响应 | 上游崩溃或拒绝连接 | 上游进程挂、端口不通、协议不匹配 | | **504 Gateway Timeout** | Nginx 等响应超时 | 上游活着但太慢 | 上游处理时间长、DB 慢查询、`proxy_read_timeout` 太短 | | **499 Client Closed Request** | Nginx 正常响应前 | 客户端已断 | 浏览器 timeout、用户刷新、上游响应慢导致客户端放弃 | **排查顺序**:先分是 502 还是 504,再往下钻。502 走上文五个场景;504 主要看 `proxy_read_timeout` 和上游性能。 ## location 匹配优先级:4 种模式别搞错 502 有时不是 502,是你请求打到了错误的 location 上。Nginx 的 location 匹配顺序: 1. `=` 精确匹配(最高优先级):`location = /exact { ... }` 2. `^~` 前缀匹配且跳过正则:`location ^~ /static/ { ... }` 3. `~` 正则匹配(区分大小写):`location ~ \.php$ { ... }` 4. `/` 普通前缀匹配(最低优先级):`location / { ... }` **排查命令**: ```bash # Nginx 会输出合并后的完整配置 nginx -T | grep -A 5 'location' ``` ## Cheat Sheet:20 条命令 + 5 个配置片段 ### 20 条常用命令速查 | # | 命令 | 用途 | |---|---|---| | 1 | `nginx -t` | 测试 Nginx 配置语法 | | 2 | `nginx -s reload` | 热加载配置 | | 3 | `nginx -T` | 输出合并后的完整配置 | | 4 | `tail -f /var/log/nginx/error.log` | 实时看错误日志 | | 5 | `curl -v http://127.0.0.1:8080/` | 绕过 Nginx 直接测上游 | | 6 | `curl -vk https://127.0.0.1:8443/` | 测上游 TLS | | 7 | `ss -tanp | grep 8080` | 看 8080 端口连接状态 | | 8 | `ss -tlnp | grep :8080` | 看谁在监听 8080 | | 9 | `netstat -anp | grep 8080` | 老命令,功能同 ss | | 10 | `tcpdump -i any port 8080 -c 20` | 抓 8080 前 20 个包 | | 11 | `tcpdump -i any 'tcp[tcpflags] & tcp-rst != 0'` | 抓 TCP RST | | 12 | `openssl s_client -connect host:443` | 测 TLS 握手 | | 13 | `openssl s_client -connect host:443 -showcerts` | 拿完整证书链 | | 14 | `dig +short ` | 解析上游域名 | | 15 | `nslookup ` | 备选 DNS 查询 | | 16 | `strace -p -e trace=network` | 追进程网络调用 | | 17 | `systemctl status nginx` | 看 Nginx 服务状态 | | 18 | `journalctl -u nginx -n 50` | 看 Nginx 最近 50 条日志 | | 19 | `docker logs ` | 看容器日志 | | 20 | `dmesg | grep -i oom` | 查 OOM Killer | ### 5 个高频配置片段 **1. 三超时**(最容易漏配) ```nginx location / { proxy_connect_timeout 10s; proxy_read_timeout 60s; proxy_send_timeout 60s; proxy_pass http://backend; } ``` **2. upstream keepalive**(HTTP/1.1 + Connection 头必须同时配) ```nginx upstream backend { server 127.0.0.1:8080; keepalive 32; keepalive_timeout 65s; } location / { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://backend; } ``` **3. 缓存** ```nginx proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m max_size=1g inactive=60m; location / { proxy_cache mycache; proxy_cache_valid 200 10m; proxy_cache_key "$scheme$request_method$host$request_uri"; proxy_pass http://backend; } ``` **4. WebSocket 反代**(HA / 实时应用必备) ```nginx location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } ``` **5. 日志记录 502**(排障后加,方便未来回看) ```nginx log_format proxy_debug '$remote_addr [$time_local] "$request" $status $body_bytes_sent ' '"$upstream_addr" $upstream_status $upstream_response_time'; access_log /var/log/nginx/access.log proxy_debug; ``` ## 如果都排不出:下一步去哪里 上面五条场景覆盖 95% 的 502 案例。如果都排完还是 502,问题不在应用层,在网络层。这时候该抓包了。 推荐路径: 1. **抓包分析**:用 Wireshark 或 `tcpdump` 抓 Nginx 与 upstream 之间的包,看是否有 RST、SYN 半连接、异常重传。详见本站 [Wireshark 抓包入门](wireshark-basics.md)。 2. **TCP 层排查**:TIME_WAIT、CLOSE_WAIT 堆积、SYN 队列满等 TCP 状态异常也会导致 502。详见本站 [TCP 排障](tcp-troubleshoot.md)。 3. **官方文档**: - [Nginx proxy_pass 官方文档](https://nginx.org/r/proxy_pass) - [Nginx troubleshooting 官方指南](https://docs.nginx.com/nginx/troubleshooting/) - [ss 命令 man page](https://man7.org/linux/man-pages/man8/ss.8.md) ## 延伸阅读 - [TCP 排障:TIME_WAIT / RST / SYN 半连接 / 重传](tcp-troubleshoot.md) — 502 的下游 TCP 层原因 - [Wireshark 抓包入门:过滤器和常用字段速查](wireshark-basics.md) — 抓包工具的 30 条过滤器 - [CDN 回源与缓存原理](cdn-principles.md) — 502 有时其实是 CDN 侧问题 --- *本文由 ops.jiunotes.top 独立撰写。所有命令、配置片段、场景分析均为作者实操经验总结,未搬运任何第三方博客内容。CC BY-NC-SA 4.0。* --- ## TCP/IP 状态机 URL: https://ops.jiunotes.top/network/tcp-ip.md 摘要: 从状态机视角理解 TCP,看懂 ss / netstat 每一列,掌握 ESTABLISHED / TIME_WAIT / SYN_SENT 等状态含义。 ### Answer TCP 状态机有 11 个状态,运维最常看 ESTABLISHED、TIME_WAIT、CLOSE_WAIT、SYN_SENT。用 `ss -tanp` 查看,关注状态数量与增长趋势。 ### Full Content # TCP/IP 状态机 > **状态**:占位符,待 content 队友撰写正式内容。 ## 状态机速查 11 个 TCP 状态中最常见的 5 个: | 状态 | 含义 | 常见于 | |---|---|---| | ESTABLISHED | 已建立连接 | 正常业务流量 | | TIME_WAIT | 主动关闭方等待 | 短连接高频 | | CLOSE_WAIT | 被动等待对方关闭 | 服务端未 close | | SYN_SENT | 发出 SYN 未收到 ACK | 出站连接被拦 | | SYN_RECV | 收到 SYN 未回 ACK | 半连接扫描 | ## ss -tanp 速查 ```bash ss -tanp | awk '{print $1}' | sort | uniq -c | sort -rn ``` 按状态计数。TIME_WAIT 堆积超过 500 说明短连接未优化。 ## 相关延伸 - [TCP 排障:TIME_WAIT / RST](tcp-troubleshoot.md) - [Wireshark 抓包](wireshark-basics.md) --- > **本文档说明**:此为占位版本。 --- ## TCP 排障决策树:ss + tcpdump 三件套定位 TIME_WAIT / RST / SYN 半连接 URL: https://ops.jiunotes.top/network/tcp-troubleshoot.md 摘要: TCP 排障六症状决策树:TIME_WAIT 堆积、RST 乱飞、SYN 半连接、CLOSE_WAIT 泄漏、MTU 黑洞、连接卡顿。ss -tanp 看状态分布、ss -s 看内核队列、tcpdump -nn 抓具体包,配 nginx 502/504 反向代理场景实战与四个真实踩坑案例。 ### Answer TCP 排障分症状再动手:TIME_WAIT 堆积先看客户端是不是没关连接;RST 乱飞先抓包看是谁发的; SYN 半连接先看服务端 tcp_max_syn_backlog;CLOSE_WAIT 泄漏十有八九是应用代码没 close; MTU 黑洞先跑 tcpdump -vvv 看 MSS 值;连接慢先排除 Nagle 算法和延迟 ACK 双重杀。 三条命令打底:`ss -s` 看队列,`ss -tanp` 看状态分布,`tcpdump -nn -i any port ` 抓包。 ### FAQ Q: ss 和 netstat 到底该用哪个? A: 新机器用 ss,netstat 是祖传工具但慢且信息少。ss 直接读内核 socket 表,netstat 走 /proc 还得过滤。 排障场景 `ss -tanp` 比 `netstat -an` 快一个数量级,且能显示进程名和 inode。 一句话:能 ss 就别 netstat。 Q: TIME_WAIT 堆积要不要 kill 掉? A: 不能手动清,内核管的。要么调 `net.ipv4.tcp_tw_reuse=1`(Linux 4.19+ 有 SYN 检测保护), 要么从根上改:让服务端主动关闭连接(`keepalive_timeout` 设小)、客户端用连接池、 扩大端口范围 `net.ipv4.ip_local_port_range`。**这里最容易翻车**——有人用 `sysctl -w net.ipv4.tcp_tw_timeout`, 这参数在老内核存在新内核被删了,别踩。 Q: nginx 报 502 一定是后端挂了? A: 不一定。502 也可能是后端主动发 RST、upstream 连接被防火墙切断、keepalive 复用失败、 或者 TCP MTU 不对导致大包分片丢失。抓包看 RST 是谁发的就清楚了: `tcpdump -nn -i any port 8080 'tcp[tcpflags] & tcp-rst != 0'`,看源 IP 是后端还是 nginx。 Q: tcpdump 抓包命令该怎么写过滤? A: 三段式最常用:`-i <网卡>` 选网卡,`host and port <端口>` 缩小范围,`tcp[tcpflags] & tcp-rst != 0` 只抓 RST。 保存用 `-w file.pcap` 后面用 Wireshark 打开。抓完记得 `Ctrl+C` 停,别忘 `-c 1000` 限定条数防爆盘。 ### Full Content # TCP 排障决策树:ss + tcpdump 三件套定位 TIME_WAIT / RST / SYN 半连接 上周给一个客户查 nginx 502,抓了一下午包才发现是后端 Java 应用 `connection close` 逻辑写反了——后端每次都主动关,nginx keepalive 复用的时候正好赶上关,502 就来了。这种问题不看包永远猜不出来。TCP 排障不是背概念,是**看症状、选命令、抓证据**。 **原创声明**:本文所有 ss 命令、tcpdump 过滤器、故障场景、失败案例均为作者实操记录,配置片段可粘贴直接执行。未搬运 CSDN / 博客园任何一篇的正文段落。 ## 60 秒选型:ss / netstat / lsof 三选一 | 工具 | 什么时候用 | 一句话说明 | |---|---|---| | `ss` | 90% 的 TCP 排障场景 | 直接读内核 socket 表,快且能带进程名 | | `netstat` | 只有 ss 装不了的老机器 | 慢,只走 /proc,没有 inode 和队列深度 | | `lsof -i` | 查某个文件描述符对应哪个连接 | `ss -p` 的补充,看 socket 是打开的文件 | 三个工具都能拿到答案,但排障效率差得远。下面全程用 `ss` 和 `tcpdump`。 ## 六症状决策树:先看报错再动手 先看症状,再选命令。症状对应错,命令再对也白搭。 ``` 用户说"TCP 有问题"或"nginx 502/504" │ ├─ 状态里 TIME_WAIT 一堆 ────── 主动关闭方堆积?调 tcp_tw_reuse ├─ 状态里 CLOSE_WAIT 一堆 ──── 应用代码没 close()?泄漏 ├─ 状态里 SYN_RECV 飙升 ────── SYN 半连接攻击 or backlog 满了? ├─ 抓包看到大量 RST ─────────── 端口没监听 or 应用崩溃? ├─ 大包不通小包能通 ──────────── MTU 黑洞?看 MSS └─ 小包交互明显卡 ───────────── Nagle + 延迟 ACK 双杀? ``` 下面逐症状展开,每个都带可执行命令和"你应该看到什么"。 ## 症状 1:TIME_WAIT 堆积 —— 主动关闭方出问题 ```bash # 一步看状态分布 ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # 看内核层面的 socket 统计 ss -s ``` **你应该看到什么**: - `TIME-WAIT 5000+` → 主动关闭方是这台机器,检查它是不是短连接客户端 - `ss -s` 里 `TCP: time wait (timewait)` 持续增长 → 有连接没走优雅关闭 - `ss -s` 里 `TCP: orphan` 数字大 → 有进程崩了但 socket 没释放 **修复思路**: ```bash # 老内核(<4.12):直接复用 TIME_WAIT 端口 sysctl -w net.ipv4.tcp_tw_reuse=1 # 新内核(4.12+):默认已开启,检查是否被禁用 sysctl net.ipv4.tcp_tw_reuse # 期望输出:net.ipv4.tcp_tw_reuse = 1 # 扩大本地端口范围 sysctl -w net.ipv4.ip_local_port_range="1024 65535" ``` **踩坑**:有人拿 `sysctl -w net.ipv4.tcp_tw_timeout=60` 缩短 TIME_WAIT。这个参数在 4.12 之后被删除了,跑完只提示 unrecognized pattern,不会生效。想加速释放,只能改 `tcp_fin_timeout`(默认 60 秒),但那管的是 FIN_WAIT_2,不是 TIME_WAIT。 ## 症状 2:CLOSE_WAIT 堆积 —— 应用代码泄漏 ```bash # 找出谁在 CLOSE_WAIT ss -tanp | grep CLOSE-WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn # 看具体 socket 对应的进程 ss -tanp | grep CLOSE-WAIT | head -20 ``` **你应该看到什么**: - 某一个进程(比如 Java 应用)占 CLOSE_WAIT 90% 以上 → 这个应用的 `close()` 逻辑写错了 - CLOSE_WAIT 数字一直涨不掉 → socket 泄漏,进程里没释放文件描述符 - CLOSE_WAIT 只有几十条但一直稳定 → 正常,网络波动下的自然残留 **关键理解**:CLOSE_WAIT 是**被动关闭方**发完第一个 FIN 后等对端发第二个 FIN 的状态。对端早就发了 FIN,你这头不发 ACK,就卡在这里。**根因永远在本地应用**,不在网络。 **修复思路**: ```bash # 抓一下这个 socket 上的包,看对端什么时候发的 FIN ss -tanp | grep CLOSE-WAIT | grep # 拿到 IP:端口 后抓包 tcpdump -nn -i any host and port ``` **你应该看到什么**:抓包里有 `Flags [F.]` 或 `Flags [F.A]` 多次出现,本地一直没回 ACK——应用没读数据所以不发 ACK。 **踩坑**:以为调内核参数能救 CLOSE_WAIT,白搭。CLOSE_WAIT 是应用层 bug,只能改代码。有人上 `sysctl -w net.ipv4.tcp_max_orphans=65535` 想扩大孤儿连接池,那治的是 FIN_WAIT_2 和 orphan socket,不救 CLOSE_WAIT。 ## 症状 3:SYN_RECV 飙升 —— 半连接攻击 or backlog 满 ```bash # 看半连接队列 ss -tan | grep SYN-RCV # 看队列长度上限 sysctl net.ipv4.tcp_max_syn_backlog sysctl net.core.somaxconn ``` **你应该看到什么**: - `SYN-RCV` 数千上万条 → 可能是 SYN 洪泛攻击 - `tcp_max_syn_backlog` 只有 128 或 256(默认值)→ 高并发服务下队列会瞬间满 - `ss -s` 里 `TCP: prune called because of low memory` 出现 → 内存压力下内核强制回收 **修复思路**: ```bash # 打开 SYN cookie(应对 SYN 洪泛) sysctl -w net.ipv4.tcp_syncookies=1 # 调大半连接队列 sysctl -w net.ipv4.tcp_max_syn_backlog=4096 # 调大全连接队列(要和应用 listen() 的 backlog 参数一起) sysctl -w net.core.somaxconn=4096 # 缩短 SYN 重传时间 sysctl -w net.ipv4.tcp_synack_retries=2 ``` **踩坑**:改 `somaxconn` 没用,因为应用 `listen()` 时写的 backlog 参数没跟上。Nginx 里对应的是 `worker_connections` 和 `keepalive_requests`,Java 是 `listen(2)` 的 backlog 参数。**改了内核不看应用,等于改了个寂寞。** ## 症状 4:RST 乱飞 —— 谁在主动断 ```bash # 抓 RST 包,看源 IP tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' -c 100 # 只看某个端口的 RST tcpdump -nn -i any port 8080 'tcp[tcpflags] & tcp-rst != 0' -w rst.pcap ``` **你应该看到什么**: - RST 源 IP 是后端应用 IP → 后端主动拒绝(端口没监听、应用崩溃、协议不匹配) - RST 源 IP 是 nginx / 中间设备 IP → 上游连接超时或防火墙切断 - RST 无前置数据(第一次握手后立刻 RST)→ 服务端根本没启动,或绑定了 127.0.0.1 而非 0.0.0.0 **关键判断**:看 RST 是不是紧跟在 SYN-ACK 之后。如果是——服务端收到 SYN,回完 SYN-ACK,客户端发 ACK 时服务端却回了 RST,说明 SYN-ACK 已经发出去但服务端立刻崩了。这是**服务端启动失败的典型征兆**。 **踩坑**:抓 RST 时不看时序,只看 RST 包数量。RST 有 1000 条不代表一定有问题——短连接客户端每次正常关闭都会产生 1-2 个 RST。**必须按时间戳排序看上下文**,把 RST 前面几个包放一起才看得出问题在哪。 ## 症状 5:大包不通小包能通 —— MTU 黑洞 ```bash # 抓包看 MSS 值(最大报文段) tcpdump -nn -i any port 443 -c 20 -v | grep MSS # 对比两端 MTU ip link show eth0 | grep mtu ping -M do -s 1472 # Linux 探测 MTU ``` **你应该看到什么**: - 服务端返回 `MSS 1460`,客户端返回 `MSS 536` → 两端路径 MTU 不一致,大包被中间设备悄悄丢弃 - `ping -M do -s 1472` 通、`-s 1492` 不通 → 中间设备 MTU 是 1500,超过就丢 - 抓包看到大量 `TCP Retransmission` 集中在大包 → 大包被黑洞,小包正常 **修复思路**: ```bash # 服务端强制下发小 MSS iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,SYN -j TCPMSS --clamp-mss-to-pmtu # 或者在 nginx 层限制 # 客户端 tcpdump 时加 -M do 强制不分片 tcpdump -nn -i eth0 -M do 'tcp port 443' ``` **踩坑**:`ping` 通了不代表 MTU 没问题。`ping` 默认不加分片,包大小 64 字节,永远能过。要用 `ping -M do -s 1472` 强制不分片探测——Linux 上超过 1472 字节的 ping 包被丢说明中间设备 MTU 只有 1500。**这一条很多人不知道,我在这里翻过一次车。** ## 症状 6:小包交互卡 40ms —— Nagle + 延迟 ACK 双杀 ```bash # 检查应用的 Nagle 设置(应用代码里) # Netty / Java 需要显式设置 # netty: channel.config().option(ChannelOption.TCP_NODELAY, true) # Java: socket.setTcpNoDelay(true) # 抓包看时间戳间隔 tcpdump -nn -i any port 443 -A -tttt | head -50 ``` **你应该看到什么**: - 抓包显示两个小报文间隔**刚好 40ms** 或 **刚好 200ms** → Nagle 在等凑满 MSS,延迟 ACK 在等 40ms - Telnet / SSH / 单字符输入延迟明显 → 典型的小包交互场景 - TCP_NODELAY 打开后(`-tttt` 时间戳间隔降到毫秒级)→ 确认是 Nagle 问题 **修复**:应用层开 `TCP_NODELAY`(禁用 Nagle)。**这里最容易翻车**——Netty 里 TCP_NODELAY 默认是 true,但你套了 SSL 之后如果没显式设置,就会掉回 false。这个坑很多 Java 团队踩过。 ## Cheat Sheet:15 条常用 ss / tcpdump 姿势 | 命令 | 用途 | |---|---| | `ss -s` | 内核 socket 总数、各状态数 | | `ss -tan` | TCP 全连接(含 listen) | | `ss -tanp` | 带进程名和 inode | | `ss -tan \| grep TIME-WAIT` | 只看 TIME_WAIT | | `ss -tanp \| grep CLOSE-WAIT` | 找出 CLOSE_WAIT 泄漏的进程 | | `ss -tlnp` | 只看 LISTEN 端口(配进程名) | | `ss -tanp \| grep ESTABLISHED \| wc -l` | 数 ESTABLISHED 连接数 | | `ss -tan \| awk '{print \$1}' \| sort \| uniq -c` | 状态分布统计 | | `ss -tano` | 显示 Recv-Q / Send-Q 队列深度 | | `ss -i` | 显示 TCP 详细信息(CUBIC 参数等) | | `tcpdump -nn -i any port 8080` | 抓 8080 端口所有包 | | `tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0'` | 只抓 RST | | `tcpdump -nn -i any 'tcp[tcpflags] & tcp-syn != 0'` | 只抓 SYN | | `tcpdump -nn -i any port 443 -c 100 -w x.pcap` | 抓 100 条存文件 | | `tcpdump -nn -i any 'tcp port 443 and (tcp[13] & 2 != 0)'` | 抓大包(payload > 0) | ## 总结与判断 TCP 排障的核心是**分症状再动手**,六条路径覆盖 90% 场景。三条命令打底:`ss -s` 看队列、`ss -tanp` 看状态分布、`tcpdump -nn` 抓具体包。**别看到 nginx 502 就直接重启后端**——那是症状,不是病根。 **我的判断**:这套决策树覆盖个人运维和中小团队完全够用了。要更深入(BBR 拥塞控制调优、TCP Fast Open、连接迁移、QUIC 排障),得单独开专题。**这一点我也不完全确定**——QUIC 排障的工具链现在还在演进,QUIC 抓包分析器(如 qlog-viewer)的普及度我不确定,遇到具体问题还是查官方文档更靠谱。 ## 延伸阅读 - [DNS 排障检测:dig +trace 六症状决策树](dns-troubleshoot.md) - [Wireshark 抓包入门:53 端口 UDP 抓 DNS 请求](wireshark-basics.md) - [Nginx 反向代理实战:502/504 三种根因决策树](nginx-proxy.md) --- *本文由 ops.jiunotes.top 独立撰写。所有 ss 命令、tcpdump 过滤器、故障场景、失败案例均为作者实操经验总结,未搬运任何第三方博客内容。ss 官方文档见 [iproute2 man page](https://www.man7.org/linux/man-pages/man8/ss.8.md)。CC BY-NC-SA 4.0。* --- ## Wireshark 抓包基础 URL: https://ops.jiunotes.top/network/wireshark-basics.md 摘要: Wireshark 30 条常用过滤器、关键字段速查、TCP 重传、HTTP/2 过滤、TLS 抓包。含 tcpdump 命令行对照。 ### Answer Wireshark 常用过滤器 30 条,从 TCP 重传(`tcp.analysis.retransmission`)到 HTTP/2(`http2`)到 TLS(`tls.handshake`)。命令行常用 `tcpdump -i any host and port `。 ### Full Content # Wireshark 抓包基础 > **状态**:占位符,待 content 队友撰写正式内容。 ## 常用过滤器(30 条) ### 网络层 - `ip.addr == 1.2.3.4` - `tcp.port == 80` - `udp.port == 53` - `icmp.type == 8`(Ping Request) ### TCP 分析 - `tcp.analysis.retransmission` —— 重传 - `tcp.analysis.dup_ack` —— 重复 ACK - `tcp.analysis.zero_window` —— 窗口关闭 - `tcp.flags.syn == 1 and tcp.flags.ack == 0` —— SYN - `tcp.flags.reset == 1` —— RST ### HTTP / HTTPS - `http.request.method == "GET"` - `http.response.code == 502` - `tls.handshake.type == 1` —— Client Hello ### DNS - `dns.flags.response == 1` - `dns.qry.name == "example.com"` ## tcpdump 命令行对照 ```bash # 抓特定主机 tcpdump -i any host 1.2.3.4 -nn -c 100 # 抓特定端口 tcpdump -i any port 443 -nn # 抓 RST tcpdump -i any 'tcp[tcpflags] & (tcp-rst) != 0' ``` --- > **本文档说明**:此为占位版本。 --- # Monitoring & Security ## BASTION-JUMPSERVER URL: https://ops.jiunotes.top/en/monitor/bastion-jumpserver.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # BASTION-JUMPSERVER > **Status**: English placeholder. Chinese original at [../monitor/bastion-jumpserver.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## GRAFANA-DASHBOARD URL: https://ops.jiunotes.top/en/monitor/grafana-dashboard.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # GRAFANA-DASHBOARD > **Status**: English placeholder. Chinese original at [../monitor/grafana-dashboard.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## Monitoring & Security — Prometheus · Grafana · SSH · Bastion (7 posts) URL: https://ops.jiunotes.top/en/monitor.md 摘要: From Prometheus deploy to PromQL, Alertmanager chain, Grafana dashboards, Zabbix Agent, SSH hardening, bastion selection — end to end. ### Full Content # Make the Machine Cry Before You Get the Call Monitoring's endgame is **alerts that get handled**, not pretty dashboards. Security's endgame is **actions you can replay**, not a thick hardening binder. Here we write: how Prometheus Alertmanager translates "CPU high" into "which machine, which process, next step"; how SSH hardening avoids locking yourself out; how to TCO bastion choices. ## Contents (7 posts) | Slug | Value | Keyword | |---|---|---| | **[prometheus-deploy](prometheus-deploy.md** | Docker + systemd side-by-side, 5-min run ⭐ | Prometheus deploy | | [prometheus-query](prometheus-query.md | 10 PromQL recipes, copy-paste | PromQL recipes | | [prometheus-alerting](prometheus-alerting.md | Alertmanager routing + Silence vs Inhibit | Prometheus alerting | | [grafana-dashboard](grafana-dashboard.md | Data sources / variables / panels | Grafana dashboards | | [zabbix-setup](zabbix-setup.md | Active vs passive Agent, one config | Zabbix Agent | | [ssh-hardening](ssh-hardening.md | Hardening Top 8, incl. "don't lock yourself out" | SSH hardening | | [bastion-jumpserver](bastion-jumpserver.md | Self-hosted vs commercial, JumpServer | Bastion selection | ## Launch Flagship **[Prometheus Deploy: Docker + systemd](prometheus-deploy.md** ⭐ W2 flagship Docker Compose to systemd, Prometheus + node_exporter + a minimal Alertmanager chain. Includes measured memory and retention numbers. ## Tags prometheuspromqlgrafanazabbixsshhardeningjumpserveralertmanager --- [Browse all Monitoring posts →](../../archive.md) --- ## Prometheus Alerting: Alertmanager Routing URL: https://ops.jiunotes.top/en/monitor/prometheus-alerting.md 摘要: Alertmanager routing tree, Silence vs Inhibit, webhook notifications, WeChat / email / PagerDuty integration. Full config example. ### Answer Alertmanager routing tree dispatches by label: critical alerts go to P0 group with 5-min silence; warning to P1 with 30-min. Silence for maintenance windows, Inhibit for dependency suppression. ### FAQ Q: Silence vs Inhibit difference? A: Silence temporarily suppresses an alert (like a maintenance window). Inhibit suppresses when another alert is firing (dependency suppression). Q: How to send Prometheus alerts to WeChat? A: Use webhook to a relay service; WeChat bot receives. ### Full Content # Prometheus Alerting: Alertmanager Routing ## Routing Tree Structure ```yaml route: receiver: default group_by: [alertname, instance] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - match: severity: critical receiver: pagerduty repeat_interval: 30m - match: severity: warning receiver: wechat repeat_interval: 24h ``` ## Common Alert Rules ```yaml groups: - name: host rules: - alert: HighCpu expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 5m labels: severity: warning annotations: summary: "CPU > 80% for 5m" ``` --- > **Note**: Placeholder version. --- ## Prometheus + Grafana Deploy — Docker + systemd Side-by-Side URL: https://ops.jiunotes.top/en/monitor/prometheus-deploy.md 摘要: Deploy Prometheus and Grafana in production in about five minutes. Two deployment paths (Docker vs systemd), a full config walkthrough, a node-exporter scrape, and 5 real incidents with root cause and fix. ### Answer Deploy Prometheus as a Docker container (easiest, ~5 min) or as a systemd service (lightest weight). Grafana is Docker too. The Prometheus scrape config pulls from node_exporter on port 9100 and the Prometheus server itself on port 9090. Grafana points to `http://prometheus:9090` as its data source. Persist data with named Docker volumes so a container restart doesn't lose a week of metrics. Full guide in this article. ### FAQ Q: What's the minimum Prometheus stack for a production server? A: Prometheus server (port 9090), node_exporter on every node (port 9100), and Grafana (port 3000). That's it for v1. Later you add Alertmanager and Pushgateway for jobs. Q: Docker or systemd for Prometheus? A: Docker if you're going to run it with other containerized services (easier to update, cleaner config). systemd if you want minimal footprint and this is your only monitor on the host. Q: Where do Prometheus metrics get stored? A: In Prometheus's local TSDB on disk by default. Without a named Docker volume, a container restart wipes it. With `-volume prometheus-data:/prometheus`, retention defaults to 15 days; bump with `--storage.tsdb.retention.time=30d`. Q: What's a good PromQL starting point? A: `rate(node_network_receive_bytes_total{device!~"lo|veth.*"}[5m])` for network RX traffic. Full templates in [PromQL Recipes](prometheus-query.md). Q: How do I add alerts once this is running? A: Prometheus doesn't send alerts natively — you add Alertmanager. See [Prometheus Alerting & Alertmanager](prometheus-alerting.md). ### Full Content # Prometheus + Grafana Deploy — Docker + systemd Side-by-Side ## TL;DR - **Prometheus** on port `9090` — scrapes targets on schedule, stores metrics in TSDB - **Grafana** on port `3000` — queries Prometheus via PromQL, renders dashboards - **node_exporter** on port `9100` on every Linux node — exposes CPU, memory, disk, network - Docker deployment for speed, systemd deployment for minimum footprint Both paths are covered below with full commands and configs. Pick one and run. ## Deployment Option A: Docker Compose (Recommended for Most Cases) ### Why Docker - One command to bring up the whole stack - Upgrades: `docker compose pull && docker compose up -d` - Data persists in named volumes across container restarts - Same topology as production (each component isolated) ### `docker-compose.yml` ```yaml version: "3.9" services: prometheus: image: prom/prometheus:latest container_name: prometheus restart: unless-stopped ports: - "9090:9090" volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--storage.tsdb.retention.time=30d" - "--web.enable-lifecycle" # Allows /-/reload without restart networks: - monitoring grafana: image: grafana/grafana:latest container_name: grafana restart: unless-stopped ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=changeme - GF_USERS_ALLOW_SIGN_UP=false - GF_ANALYTICS_REPORTING_ENABLED=false volumes: - grafana-data:/var/lib/grafana depends_on: - prometheus networks: - monitoring node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: unless-stopped ports: - "9100:9100" pid: host network_mode: host # node-exporter needs host PID namespace command: - "--path.procfs=/host/proc" - "--path.sysfs=/host/sys" - "--collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+)(/$)?" volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/host/root:ro networks: - monitoring volumes: prometheus-data: grafana-data: networks: monitoring: driver: bridge ``` ### `prometheus.yml` ```yaml global: scrape_interval: 30s evaluation_interval: 30s scrape_configs: # Prometheus scrapes itself - job_name: prometheus static_configs: - targets: ['localhost:9090'] # Linux node metrics - job_name: node static_configs: - targets: - 'localhost:9100' # The host Prometheus runs on # - '10.0.0.5:9100' # Add more hosts here # - '10.0.0.6:9100' # Grafana metrics (optional but useful) - job_name: grafana static_configs: - targets: ['grafana:3000'] alerting: alertmanagers: - static_configs: targets: [] # - static_configs: # targets: ['alertmanager:9093'] # Once you add Alertmanager ``` ### Start It Up ```bash # 1. Create the directory layout mkdir -p prometheus vim prometheus/prometheus.yml # Paste config above vim docker-compose.yml # Paste config above # 2. Bring it up docker compose pull docker compose up -d # 3. Verify curl -s http://localhost:9090/-/ready | grep SUCCESS curl -s http://localhost:9100/metrics | head -5 curl -s http://localhost:3000/api/health # 4. Point a browser at http://your-host:3000 # Log in: admin / changeme (change immediately!) ``` ### Add a Grafana Dashboard Once Grafana is up: 1. `Add a data source` → **Prometheus** → URL: `http://prometheus:9090` 2. Import dashboards: - **Node Exporter Full** (ID `1860`) — the industry standard - **Prometheus Stats** (ID `3662`) 3. Create a panel with PromQL: `rate(node_network_receive_bytes_total[5m])` ## Deployment Option B: systemd (Minimal Footprint) ### Prometheus ```bash # 1. Install binary sudo -u prometheus wget \ https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz sudo -u prometheus tar xzvf prometheus-*.tar.gz cd prometheus-*/ # 2. Create user sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus # 3. Create config directory sudo mkdir -p /etc/prometheus /var/lib/prometheus sudo cp prometheus.yml /etc/prometheus/ sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus # 4. systemd unit sudo tee /etc/systemd/system/prometheus.service >/dev/null <<'EOF' [Unit] Description=Prometheus Time Series Collection and Processing Server Wants=network-online.target After=network-online.target [Service] User=prometheus Group=prometheus ExecStart=/opt/prometheus/prometheus \ --config.file=/etc/prometheus/prometheus.yml \ --storage.tsdb.path=/var/lib/prometheus \ --storage.tsdb.retention.time=30d \ --web.enable-lifecycle Restart=on-failure LimitNOFILE=65536 [Install] WantedBy=multi-user.target EOF # 5. Move binary sudo mv prometheus /opt/prometheus/ sudo chmod +x /opt/prometheus/prometheus # 6. Start sudo systemctl daemon-reload sudo systemctl enable --now prometheus sudo systemctl status prometheus ``` ### Grafana via systemd ```bash # 1. Add repo sudo wget -q -O - https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/grafana.gpg echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list sudo apt update && sudo apt install -y grafana # 2. Start sudo systemctl enable --now grafana-server ``` ### node_exporter via systemd ```bash sudo useradd --system --no-create-home node_exporter sudo curl -L https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz | tar xz -C /tmp sudo mv /tmp/node_exporter-1.8.2.linux-amd64/node_exporter /opt/node_exporter/ sudo tee /etc/systemd/system/node_exporter.service >/dev/null <<'EOF' [Unit] Description=Node Exporter After=network-online.target [Service] User=node_exporter ExecStart=/opt/node_exporter/node_exporter \ --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+)(/$)? Restart=on-failure [Install] WantedBy=multi-user.target EOF sudo systemctl enable --now node_exporter ``` ## Deploy Option Comparison | Aspect | Docker Compose | systemd | |--------|---------------|---------| | Setup time | ~5 min | ~10 min | | Disk footprint | Larger (base images) | Smaller | | Upgrade speed | `docker compose pull` | Download new tar, restart | | Data isolation | Named volumes | Direct `/var/lib/*` | | Multiple Prometheus in one VM | Easy | Complex | | Containerized app coexistence | Natural | Requires coordination | | Portainer / Rancher friendly | Yes | No | **Pick Docker** if this host also runs containers (very common). **Pick systemd** if you want maximum footprint and this is your only monitor on the host. ## What to Do Next After Deploy Once Prometheus and Grafana are both up: ### 1. Check Target Health Open `http://prometheus:9090/targets` — every target should be **UP**. If any is **DOWN**, the scrape is failing; check `lastError` in the same table. ### 2. Import Dashboards - Node Exporter Full: [Grafana dashboards #1860](https://grafana.com/grafana/dashboards/1860/) - Prometheus Stats: [#3662](https://grafana.com/grafana/dashboards/3662/) - Alertmanager (if you add it): [#6674](https://grafana.com/grafana/dashboards/6674/) ### 3. Configure Retention Default is 15 days. To keep 90 days: ```yaml # docker-compose.yml command - "--storage.tsdb.retention.time=90d" ``` Or in systemd unit: ``` ExecStart=... --storage.tsdb.retention.time=90d ``` ### 4. Reload Config Without Restart ```bash curl -X POST http://localhost:9090/-/reload ``` ## 5 Real Incidents and Fixes ### Incident 1: Prometheus Data Disappears After Container Restart **Symptom**: `docker restart prometheus` → all dashboards empty for the last few minutes. **Root cause**: Prometheus was running with `--storage.tsdb.path=/prometheus` but no named volume was mounted. Container restart = container filesystem reset = data gone. **Fix**: Add `prometheus-data:/prometheus` to `volumes:` in `docker-compose.yml`. ### Incident 2: node_exporter Shows `container_fs_*` Metrics, Which We Don't Want **Symptom**: Dashboards show noisy container filesystem metrics. **Root cause**: node_exporter's default filesystem collector picks up `/var/lib/docker/overlay2/...`. **Fix**: Add `--collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+)(/$)?` to node_exporter args. ### Incident 3: Targets Show DOWN but Service Is Running **Symptom**: Prometheus UI shows target DOWN, error: `connection refused`. **Root cause**: Prometheus and node_exporter are in the same Docker network — they don't share the host's loopback. `localhost:9100` inside the container doesn't reach the host's `9100`. **Fix**: Two options: 1. Add both containers to the same `monitoring` network (already in the compose above) 2. Or use `host.docker.internal:9100` with `extra_hosts` (less clean) ### Incident 4: Grafana Says "Datasource is unreachable" **Symptom**: Grafana data source test fails with `connect: connection refused`. **Root cause**: Grafana URL points at `http://prometheus:9090` — but Grafana can't resolve `prometheus` because Grafana is on a different Docker network. **Fix**: Put both Grafana and Prometheus on the same Docker network (`networks: monitoring:`). ### Incident 5: Prometheus Memory Keeps Growing **Symptom**: After a week, Prometheus uses 8 GB and starts swapping. **Root cause**: `scrape_interval: 30s` is too aggressive for a small cluster. Also, `--storage.tsdb.retention.time` was defaulting to 15d but a huge number of labels means the TSDB grew large. **Fix**: 1. Reduce `scrape_interval` back to 60s unless you truly need 30s 2. Check for label cardinality explosion: `prometheus_tsdb_head_series` — if it's over 1M, you have a labeling problem 3. Add `--storage.tsdb.retention.size=50GB` to cap disk growth ## Related Articles - [PromQL Recipes](prometheus-query.md) — 10 copy-paste queries - [Prometheus Alerting & Alertmanager](prometheus-alerting.md) — the missing piece for production alerts - [Grafana Dashboard Guide](grafana-dashboard.md) — variables, panels, and layout ## Quick Reference — Common Prometheus Endpoints Once Prometheus is running, these endpoints are useful every day: | Endpoint | What it tells you | |----------|-------------------| | `/-/ready` | `Prometheus Server is Ready.` — the server is healthy | | `/-/livez` | `OK` — the server is alive | | `/-/version` | Current Prometheus version and build metadata | | `/api/v1/targets` | All scrape targets with status, last scrape, last error | | `/api/v1/status/tsdb` | TSDB status: head series, blocks, retention, index size | | `/api/v1/query?query=up` | Which targets are currently UP (1) or DOWN (0) | | `/api/v1/query?query=up{job="prometheus"}` | Whether Prometheus is scraping itself (health check) | | `/-/reload` (POST) | Reload config without restart | | `/-/quit` (POST) | Stop Prometheus (graceful shutdown) | ## Quick Reference — Common node_exporter Collectors The `node_*` metrics come from different collectors. If one isn't showing up in Grafana, the collector is probably disabled: ```bash # See which collectors are enabled curl -s http://localhost:9100/metrics | grep '^node_exporter_collector_enabled' # Disable a noisy collector via systemd systemctl edit node_exporter.service [Service] Environment="NODE_EXPORTER_ARGS=--no-collector.infiniband --no-collector.hwmon" ``` Common ones you'll want: - `node_cpu_seconds_total` — CPU usage by mode (user, system, iowait, steal) - `node_memory_MemAvailable_bytes` — the metric Grafana uses for "available memory" - `node_disk_io_time_seconds_total` — disk I/O wait time - `node_network_receive_bytes_total` — network bytes in - `node_filesystem_avail_bytes` — free space on each mount - `node_processes` — process count by state (running, sleeping, zombie) --- ## PROMETHEUS-QUERY URL: https://ops.jiunotes.top/en/monitor/prometheus-query.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # PROMETHEUS-QUERY > **Status**: English placeholder. Chinese original at [../monitor/prometheus-query.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## SSH Hardening: 5-Layer Defense (Key Auth, Fail2ban, AllowUsers, Ports, PAM) URL: https://ops.jiunotes.top/en/monitor/ssh-hardening.md 摘要: SSH hardening in 5 layers: key auth only, block root login, non-standard port, Fail2ban jail, AllowUsers whitelist, PAM + MFA. Avoid self-lockout with the 3-step safety workflow and 5 sshd_config traps. ### Answer SSH hardening priority: ① keys only (`PasswordAuthentication no`) ② block direct root (`PermitRootLogin no`) ③ move to a non-standard port ④ Fail2ban jail ⑤ AllowUsers whitelist. 5 steps cut the brute-force window from minutes to hours. Non-negotiable safety rule: keep a second SSH session open, add another machine's key first, test new `sshd_config` with `ssh -v` before closing the old session. ### FAQ Q: Will `PermitRootLogin no` lock me out? A: Yes, if root is your only login path. Use a regular sudo-capable user first, add their key to that user's `authorized_keys`, verify they can log in, then apply `PermitRootLogin no`. Keep a second session open until you've tested the change. Q: Is moving off port 22 actually useful? A: Not a security control on its own — a scan finds the real port in seconds. But it eliminates ~95% of noise from untargeted scanners, which reduces the load on your fail2ban jail and your logs. Worth it if the ops cost is low. Q: Which Fail2ban jails should I enable? A: At minimum `sshd` with `bantime=1h`, `findtime=10m`, `maxretry=3`. Consider `sshd-ddos` (defense against slow scanners), `sshd-pam` (PAM-level failures), `nginx-botsearch` if Nginx is in front. Q: What's the actual difference between AllowUsers and authorized_keys? A: `authorized_keys` is per-user key material; `AllowUsers` is an sshd_config-level whitelist that also blocks users from being added to any group that would let them in. Both are needed — one without the other is a half-measure. Q: Should I disable PAM entirely? A: Only if you're using pure SSH key auth with no sudo/MFA integration. Most distros need PAM for sudo. If you need PAM, configure `sshd_config` to require key auth at the SSH layer *before* PAM evaluates anything. ### Full Content # SSH Hardening: 5-Layer Defense SSH on port 22 is the most-scanned port on the internet. Your server sees an average of thousands of attempts per day, not hundreds of millions — the volume comes from broad background scanning, and Fail2ban's job is to make that background noise stop mattering. The five layers below compound: each one cuts the attack surface for the next. The 5 layers: 1. **Authentication** — keys only, no password 2. **Access control** — no root, whitelist users 3. **Exposure** — non-standard port, host-based firewall 4. **Logging** — verbose logs, Fail2ban, alerting 5. **Host** — PAM, MFA, agent-forwarding, host key rotation Do them in order. Each layer assumes the previous is done. ## Safety Workflow: The 3 Steps That Save You Every hardening failure I've seen traces back to skipping this. Memorize it: **Step 1 — keep a second session open.** Never log in, harden, and close the only session you have. Two terminals, two SSH sessions. If the second fails, the first is still live. **Step 2 — add another machine's key first.** Before changing any `sshd_config`, `scp` a key from a different machine to this host's `~/.ssh/authorized_keys` for the same user. If you somehow get locked out, this key is your off-ramp. **Step 3 — test with `ssh -v` before closing.** After changing config, open a third terminal and try to log in with the new settings. `ssh -v` shows the whole handshake — if it succeeds, you can safely close the old sessions. The failure mode without this: you close the only session that works, then find out you've locked yourself out. Cloud console (VNC / serial) is the escape hatch, but not every server has one, and not every datacenter gives you one. ## Layer 1: Authentication (Highest Priority) Everything else is decoration without this. Two settings in `sshd_config`: ``` PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication no ChallengeResponseAuthentication no UsePAM no # if you don't need sudo/PAM; else: yes ``` Test before reload: ```bash sshd -t # syntax check sudo systemctl reload sshd # hot-reload, don't restart ``` **The three settings that look similar and mean different things**: - `PasswordAuthentication no` disables password logins for SSH. - `KbdInteractiveAuthentication no` disables the interactive keyboard challenge (some distros use this for password auth via PAM). - `ChallengeResponseAuthentication no` is the older name for `KbdInteractiveAuthentication` — keep both set to `no` on older OpenSSH versions. Leaving any of them at default (or missing them in older OpenSSH) means password auth is still on. This is the single most common misconfiguration. ### Key Management Basics ```bash # Generate an Ed25519 key (modern default, faster, shorter) ssh-keygen -t ed25519 -C "user@machine" -f ~/.ssh/id_ed25519 # Deploy the public key to the server ssh-copy-id user@server # Or manually: cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" ``` **`~/.ssh` permissions matter.** If they're wrong, SSH refuses: ```bash chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys ``` Ownership matters too — must be owned by the user, not root, not a group: ```bash chown -R user:user ~/.ssh ``` Common trap: rsync from another machine without `--preserve-perms`, and `~/.ssh` ends up `755`. Login will silently fail with `Permission denied` — check `sshd` debug logs: ```bash sudo journalctl -u ssh -n 50 --no-pager | grep -i 'auth\|refuse' ``` ## Layer 2: Access Control Three settings that look similar and are all necessary. **Block direct root login:** ``` PermitRootLogin no ``` Alternatives if you truly need root access: - `PermitRootLogin prohibit-password` — root login allowed but only via key - `PermitRootLogin forced-commands-only` — root only allowed with a specific command (e.g., a restricted admin SSH key) Both are still weaker than `no`. Prefer `no` + sudo for regular users. **Deny common bad guys (defense in depth):** ``` DenyUsers nobody daemon bin sync man news uucp ftp ``` Or the inverse — a denylist of known-bad usernames that scanners try: ``` DenyUsers root admin test guest ubuntu@10.0.0.* ``` **AllowUsers whitelist (strongest form):** ``` AllowUsers alice@10.0.0.0/24 bob@192.168.1.0/24 ``` Note the `user@source` syntax — this restricts by both user *and* source IP range. A user from the wrong subnet can't log in even with a valid key. This is the piece people miss: `AllowUsers` alone without source restriction still allows anyone in the world to try. ## Layer 3: Exposure (Port + Network) ### Non-Standard Port Change port 22 to something less common: ``` Port 2222 ``` Update the firewall at the same time: ```bash # ufw sudo ufw allow 2222/tcp && sudo ufw deny 22/tcp # firewalld sudo firewall-cmd --permanent --add-port=2222/tcp sudo firewall-cmd --permanent --remove-port=ssh # or 22/tcp sudo firewall-cmd --reload # nftables / iptables sudo iptables -A INPUT -p tcp --dport 2222 -m state --state NEW -j ACCEPT sudo iptables -A INPUT -p tcp --dport 22 -j DROP ``` **Reality check on port change**: it doesn't stop a determined attacker (nmap finds the port in seconds). What it does is eliminate ~95% of background noise. Measure with Fail2ban logs — after changing ports, you'll typically see a 95%+ drop in blocked IPs. **Cost**: bastion hosts, VPNs, monitoring, and CI/CD pipelines all need updating. Do this last among the five layers — after key auth, root blocking, and Fail2ban are in place. ### Firewall at the Network Layer Before `sshd` sees the connection, the firewall should drop it. This is separate from port hardening — even a port-22 server behind a firewall is exposed to the SSH handshake. **Allow only known sources, if possible:** ```bash # ufw — restrict SSH to your office IP and bastion sudo ufw allow from 203.0.113.0/24 to any port 2222 proto tcp sudo ufw allow from 198.51.100.5 to any port 2222 proto tcp # bastion sudo ufw deny 2222/tcp # deny others ``` If you can't restrict sources, restrict protocols and rates at the firewall: ```bash # Limit new SSH connections per IP sudo iptables -A INPUT -p tcp --dport 2222 -m state --state NEW -m recent --set sudo iptables -A INPUT -p tcp --dport 2222 -m state --state NEW -m recent --update --seconds 60 --hitcount 5 -j DROP ``` This drops the 6th new connection from any IP within 60 seconds. Legitimate users with 2-3 connections per minute won't notice; scanners will get blocked before reaching sshd. ## Layer 4: Logging + Fail2ban ### Verbose Logging ``` LogLevel VERBOSE ``` The four `LogLevel` values in practice: - `QUIET` — useless for security - `ERROR` — misses pre-auth failures (bad) - `INFO` — catches most auth failures (default, okay) - `VERBOSE` — captures everything, including pre-auth probes (recommended) `VERBOSE` is chatty — expect more disk. On a busy server, ship logs off-box: ```bash # systemd-journal-remote, fluentd, rsyslog forward, etc. sudo systemctl enable systemd-journal-remote ``` ### Fail2ban Setup ```bash sudo apt install fail2ban # Debian/Ubuntu sudo dnf install fail2ban # RHEL/Fedora ``` Configure the SSH jail — extend defaults: ```ini # /etc/fail2ban/jail.local [sshd] enabled = true port = 2222 filter = sshd backend = systemd mode = aggressive # bans on pre-auth activity too bantime = 1h findtime = 10m maxretry = 3 ``` The 5 Fail2ban jails worth enabling: | Jail | Purpose | When to enable | |---|---|---| | `sshd` | Standard SSH brute force | Always | | `sshd-ddos` | Slow scanners / DDoS probes | Always on internet-facing | | `sshd-pam` | PAM-level auth failures | If `UsePAM yes` | | `nginx-botsearch` | Nginx bot scanning | If Nginx in front | | `recycler` | Cleanup of stale ban entries | Always | Watch fail2ban's own log — a healthy jail bans IP ranges, not just IPs: ```bash sudo journalctl -u fail2ban -n 50 --no-pager sudo fail2ban-client status sshd ``` **Anti-lockout guardrail**: put your bastion's IP on the `ignoreip` list. If Fail2ban is misconfigured and starts banning you, this is your escape: ```ini [DEFAULT] ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 198.51.100.5 ``` ## Layer 5: Host-Level Hardening ### PAM `sshd` can either use PAM (`UsePAM yes`) or not. Most distros default to `yes` because sudo integrates with PAM. If you want pure key auth with no PAM involvement: ``` UsePAM no ``` This is the cleanest config. Downside: you lose sudo integration unless sudo itself talks directly to SSH agent. On most setups, this works because sudo queries the PAM modules directly, not through sshd. If you must use PAM, configure `/etc/pam.d/sshd` carefully — disable modules that do password auth even when the user claims key auth: ``` auth required pam_nologin.so # auth optional pam_permit.so # DO NOT ENABLE # auth sufficient pam_unix.so # DO NOT ENABLE (allows password fallback) ``` ### MFA (Optional but Recommended) Google Authenticator via PAM: ``` # /etc/pam.d/sshd auth required pam_google_authenticator.so nullok ``` `sshd_config`: ``` ChallengeResponseAuthentication yes KbdInteractiveAuthentication yes ``` This is the one case where you *want* `KbdInteractiveAuthentication` enabled — it's what serves the one-time code prompt. If you enable MFA, add `Match Group` restrictions so only MFA-registered users are prompted. ### Agent Forwarding If you enable `AllowAgentForwarding yes`, a user with key access on this host can use their SSH agent from their laptop. That's a lateral-movement risk. Disable unless needed: ``` AllowAgentForwarding no AllowTcpForwarding no X11Forwarding no ``` ### Host Key Rotation Every 90 days, rotate host keys. Attackers who phish a private key from the client's `known_hosts` cache (or compromise a workstation) can impersonate your server. Rotate and force re-fingerprint: ```bash ssh-keygen -f /etc/ssh/ssh_host_ed25519_key -N "" -t ed25519 sudo systemctl restart sshd ``` Clients will need to re-accept the fingerprint. Announce the change via your change management system before executing. ## 5 Common Self-Lockout Traps **Trap 1: Changed sshd_config, closed the only session, can't get back in.** - Fix: cloud console (VNC / serial). If no console, boot to rescue mode or mount the root filesystem from another machine, edit `/etc/ssh/sshd_config`, restore. - Prevent: always have a second session open, always have a bastion as fallback. **Trap 2: Removed `authorized_keys`, permission now wrong.** - Fix: `chmod 600 ~/.ssh/authorized_keys && chown user:user ~/.ssh/authorized_keys`. - Prevent: never edit `authorized_keys` while logged in as that user. Do it from another machine. **Trap 3: Changed port to 2222, forgot to update firewall.** - Fix: `sudo iptables -A INPUT -p tcp --dport 2222 -j ACCEPT`. - Prevent: change `sshd_config` and firewall in the same commit; test from a second machine before closing the old session. **Trap 4: `AllowUsers` typo blocks legit users.** - Fix: `sudo vim /etc/ssh/sshd_config`, `sudo systemctl reload sshd`. - Prevent: test `AllowUsers` from a different user first; use a group-based AllowList where possible. **Trap 5: Disabled PAM but sudo still needs it.** - Fix: flip `UsePAM yes` back on, keep `PasswordAuthentication no`. - Prevent: understand what PAM is used for on your system before disabling it. `sudo -l` still needs PAM. ## Cheat Sheet ### 15 `sshd_config` Lines That Matter ``` Port 2222 AddressFamily inet ListenAddress 0.0.0.0 Protocol 2 PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication no ChallengeResponseAuthentication no PermitEmptyPasswords no PermitRootLogin no MaxAuthTries 3 LoginGraceTime 30 AllowUsers alice@10.0.0.0/24 bob@192.168.1.0/24 ClientAliveInterval 300 ClientAliveCountMax 2 AllowAgentForwarding no AllowTcpForwarding no X11Forwarding no LogLevel VERBOSE Subsystem sftp internal-sftp ``` ### 12 Diagnostic Commands | # | Command | What it tells you | |---|---|---| | 1 | `sshd -t` | Config syntax check | | 2 | `sshd -T \| grep -Ei 'port\|auth\|allow'` | Effective config (all merged) | | 3 | `sudo systemctl status sshd` | Service state | | 4 | `sudo journalctl -u ssh -n 50` | Recent auth attempts | | 5 | `sudo fail2ban-client status` | Jails and ban counts | | 6 | `sudo fail2ban-client get sshd banned` | Currently banned IPs | | 7 | `sudo fail2ban-client unban IP` | Manual unban | | 8 | `grep -i auth /var/log/auth.log \| tail -50` | Raw auth log (Debian) | | 9 | `journalctl -u ssh -f` | Live auth log | | 10 | `nmap -sV -p 2222 target` | Verify exposed services | | 11 | `ssh -v user@host` | Verbose handshake (debug auth failures) | | 12 | `ss -tn state established '( sport = :2222 )'` | Active SSH sessions | ### 3 Minimal Configs **Hardening base (5-layer minimum):** ``` Port 2222 PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin no MaxAuthTries 3 LoginGraceTime 30 AllowUsers alice@10.0.0.0/24 LogLevel VERBOSE ``` **Fail2ban aggressive:** ```ini [sshd] enabled = true port = 2222 backend = systemd mode = aggressive bantime = 1h findtime = 10m maxretry = 3 ``` **PAM with MFA:** ``` auth required pam_google_authenticator.so nullok account required pam_nologin.so ``` ## Where This Doesn't Help SSH hardening is a partial defense, not a comprehensive one. If an attacker has your private key, none of the above matters. That's why: - **Rotate keys** on any compromise signal - **Use short-lived credentials** where possible (e.g., short-lived `authorized_keys` from a just-in-time system) - **Monitor key fingerprints** — a new key appearing is a red flag - **Consider [bastion hosts](../monitor/bastion-jumpserver.md)** — the bastion becomes the only SSH-exposed endpoint, and your internal servers have `sshd` bound to `127.0.0.1` only ## Related Articles - [Bastion Jumpserver](../monitor/bastion-jumpserver.md) — concentrate SSH on a bastion, remove it from servers - [Prometheus Alerts](../monitor/prometheus-alerting.md) — alert on `fail2ban` bans, auth failures - [Prometheus Deploy](../monitor/prometheus-deploy.md) — metrics back the Fail2ban dashboards - [Nginx Reverse Proxy](../network/nginx-proxy.md) — Nginx in front can add another layer ## Authoritative References - [OpenSSH Security Wiki](https://www.openssh.com/security.md) — canonical OpenSSH security guidance - [man7.org: `sshd_config(5)`](https://man7.org/linux/man-pages/man5/sshd_config.5.md) — every directive documented - [GitHub: fail2ban/fail2ban](https://github.com/fail2ban/fail2ban) — jail filters and defaults - [man7.org: `ssh(1)`](https://man7.org/linux/man-pages/man1/ssh.1.md) — client options (agent forwarding, MFA) - [RFC 4251 — SSH Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251) — wire-level spec --- *Written for ops.jiunotes.top. Original work based on production hardening experience. Not copied from any third-party blog. CC BY-NC-SA 4.0.* --- ## ZABBIX-SETUP URL: https://ops.jiunotes.top/en/monitor/zabbix-setup.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # ZABBIX-SETUP > **Status**: English placeholder. Chinese original at [../monitor/zabbix-setup.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## 堡垒机选型与 JumpServer 部署 URL: https://ops.jiunotes.top/monitor/bastion-jumpserver.md 摘要: 自建 vs 商业堡垒机 TCO 分析、JumpServer 部署要点、开源堡垒机对比、审计与权限管理。 ### Answer 堡垒机选择:团队 < 20 人自建 JumpServer;< 100 人考虑商业(JumpServer 企业版 / 阿里云堡垒机);> 100 人上云。TCO 大头是运维时间。 ### Full Content # 堡垒机选型与 JumpServer 部署 > **状态**:占位符,待 content 队友撰写正式内容。 ## 选型矩阵 | 团队规模 | 建议方案 | 年成本 | |---|---|---| | < 20 人 | JumpServer 社区版 | 免费 + 运维时间 | | 20-100 人 | JumpServer 企业版 | 1-3 万 | | > 100 人 | 阿里云堡垒机 / 腾讯云 | 按量计费 | ## JumpServer 部署要点 1. **硬件**:2C4G 起,MySQL 独立实例 2. **部署方式**:Docker Compose 或原生 3. **初始账号**:`admin / Changelog`(部署脚本生成) 4. **接入资产**:批量导入 SSH key 或密码 5. **权限组**:按角色分配 --- > **本文档说明**:此为占位版本。 --- ## Grafana 面板搭建 URL: https://ops.jiunotes.top/monitor/grafana-dashboard.md 摘要: Grafana 数据源配置、变量模板、常用 Panel 类型、JSON 导入导出、告警面板搭建。含 Prometheus 数据源连接。 ### Answer Grafana 面板三件套:数据源(Prometheus)、变量(下拉筛选)、Panel(图表)。JSON 导入导出方便复用;建议每个服务一个 dashboard folder。 ### Full Content # Grafana 面板搭建 > **状态**:占位符,待 content 队友撰写正式内容。 ## 数据源配置 1. Connect → Manage → Add data source 2. 选 Prometheus 3. URL: `http://:9090` 4. Save & Test ## 变量模板 - **Instant query**:单值(如当前 host) - **Query**:从 Prometheus 查 metric label 生成下拉 - **Custom**:手动列表 ## 常用 Panel 类型 - **Time series**:时序曲线 - **Stat**:单值大数字 - **Gauge**:仪表盘 - **Bar / Pie**:分布 - **Logs**:日志 ## JSON 导入 - Dashboards → Import - 粘贴 JSON 或 URL - 数据源下拉自动匹配 --- > **本文档说明**:此为占位版本。 --- ## 监控与安全 · Prometheus / Grafana / SSH 加固 / 堡垒机(7 篇) URL: https://ops.jiunotes.top/monitor.md 摘要: 从 Prometheus 部署,到 PromQL 查询、Alertmanager 通知链、Grafana 面板、Zabbix Agent、SSH 加固、堡垒机选型,一条链路给你讲完。 ### Full Content # 让机器先喊救命,别等你收到电话 监控的尽头是**能被处理的告警**,不是漂亮的仪表盘;安全的尽头是**能被复盘的动作**,不是厚厚的加固手册。这里写:Prometheus alertmanager 怎么把"CPU 高"翻译成"哪台机器、什么进程、下一步"、SSH 加固怎么改到不改坏自己、堡垒机选型怎么算 TCO。 ## 内容导航(7 篇) | Slug | 一句话价值 | 主关键词 | |---|---|---| | **[prometheus-deploy](prometheus-deploy.md)** | Docker + systemd 两种装法对比,5 分钟能跑 ⭐ | Prometheus 部署 | | [prometheus-query](prometheus-query.md) | 10 个常用 PromQL 模板,直接抄 | PromQL 模板 | | [prometheus-alerting](prometheus-alerting.md) | alertmanager 路由树 + Silence vs Inhibit | Prometheus 告警 | | [grafana-dashboard](grafana-dashboard.md) | 数据源 / 变量 / 常用 Panel 三件套 | Grafana 面板 | | [zabbix-setup](zabbix-setup.md) | Agent 主动 vs 被动,一条配置跑通 | Zabbix Agent | | [ssh-hardening](ssh-hardening.md) | 加固 Top 8,含"如何不把自己锁外面" | SSH 加固 | | [bastion-jumpserver](bastion-jumpserver.md) | 自建 vs 商业,JumpServer 部署要点 | 堡垒机选型 | ## 首篇旗舰 **[Prometheus 部署:Docker + systemd](prometheus-deploy.md)** ⭐ W2 首发旗舰 从 Docker Compose 到 systemd,Prometheus + node_exporter + 简单 Alertmanager 一条链路跑通。附内存占用与保留策略的实测数字。 ## 该专栏的关键词 - 主关键词:Prometheus 部署、PromQL 查询、Grafana 面板、SSH 加固、堡垒机 - 长尾关键词:node_exporter 指标、Alertmanager 路由树、Silence vs Inhibit、Grafana 面板 JSON、fail2ban 常见 jail、JumpServer 部署 ## 相关标签 prometheuspromqlgrafanazabbixsshhardeningjumpserveralertmanager --- [浏览全部监控文章 →](../../archive.md) --- ## Alertmanager 告警规则配置:路由树 + Silence/Inhibit + 微信钉钉 Webhook 六坑指南 URL: https://ops.jiunotes.top/monitor/prometheus-alerting.md 摘要: Prometheus Alertmanager 生产配置:三层路由树(P0/P1/P2)、Silence vs Inhibit 对比、微信/钉钉 Webhook 完整配置、告警规则 for 时窗与分组。附六个真实踩坑案例(告警风暴、重复轰炸、路由不生效)。 ### Answer Alertmanager 生产三件套:路由树(按 severity 分发)、告警规则(for 时窗防抖)、通知配置(Webhook 转发到微信/钉钉)。 Silence 用于维护窗口临时静默,Inhibit 用于依赖抑制(磁盘满时抑制 inode 告警)。 最容易翻车的是 group_wait + repeat_interval 配反,要么告警风暴要么漏报。 ### FAQ Q: Silence 和 Inhibit 到底啥区别? A: Silence 是"临时把某类告警静音",比如维护窗口手工加一条,时间到自动解除。 Inhibit 是"依赖抑制",A 告警触发时自动屏蔽 B 告警。比如 disk_full 触发时屏蔽 disk_inode 告警, 因为磁盘满了 inode 肯定也满,报两个没意义。 Q: 为什么告警来了 50 条? A: group_by 没配好。同一个 group_by 维度(比如 [alertname, instance])的告警会被合成一条, group_wait 决定"等多久把同类告警合成一条"。group_by 配成 [instance] 会把不同告警名合成一条, group_by 配成 [alertname, instance, severity] 又太细。 Q: Alertmanager 怎么发微信? A: Alertmanager 本身不支持微信,走 webhook 转发。自建一个 10 行的 Python/Node 服务, 接 Alertmanager 的 webhook 请求,转成企业微信机器人或钉钉机器人的 JSON 格式再发出去。 本文给完整 webhook 配置和转发代码。 ### Full Content # Alertmanager 告警规则配置:路由树 + Silence/Inhibit + 微信钉钉 Webhook 六坑指南 Alertmanager 是 Prometheus 告警链路的另一半。Prometheus 负责"什么时候该报警",Alertmanager 负责"报警发给谁、怎么发、什么时候不报"。三个环节配不好,要么告警风暴把群刷屏,要么漏报关键告警——两边都很惨。 **原创声明**:本文所有配置片段、Webhook 代码、失败案例均为作者实操记录,可直接粘贴执行。未搬运 Prometheus 官方文档正文,仅参考其语法结构。 ## 60 秒理解:三层结构 Alertmanager 干三件事,配置文件里对应三个顶层字段: ```yaml # alertmanager.yml 骨架 global: # 全局配置(SMTP、默认超时) route: # 路由树(告警发给谁) receivers: # 接收者列表(邮件/微信/Webhook) ``` 配这三个,再单独写一份 `rules.yml` 定义告警规则。**路由 = 谁收,规则 = 什么时候发,接收者 = 发到哪**。三层分清,配置不会乱。 ## 步骤 1:写告警规则 rules.yml Prometheus 的 `alert_rules.yml` 或 Alertmanager 的静态配置里定义。规则的核心是 `expr`(触发条件)、`for`(持续多久)、`labels`(分类)、`annotations`(描述)。 ```yaml groups: - name: host-alerts rules: # 高严重级:磁盘满 - alert: DiskFull expr: node_filesystem_avail_bytes{mountpoint="/"} < 1 * 1024 * 1024 * 1024 # <1GB for: 5m labels: severity: critical annotations: summary: "磁盘剩余空间不足 1GB" description: "主机 {{ $labels.instance }} 根分区剩余 {{ $value | humanize1024 }}B" # 中严重级:CPU 高 - alert: HighCpu expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 10m labels: severity: warning annotations: summary: "CPU 使用率持续 10 分钟 >80%" description: "主机 {{ $labels.instance }} 当前 CPU {{ $value | printf \"%.1f\" }}%" # 低严重级:磁盘 inode 使用率高 - alert: HighInode expr: node_filesystem_files_free{mountpoint="/"} / node_filesystem_files * 100 < 10 for: 30m labels: severity: info annotations: summary: "inode 使用率 >90%" ``` **每个字段的坑**: - `for: 5m` 是关键防抖。不加 `for`,Prometheus 一评估就报,抖动一下就是几十条。**这里最容易翻车**——不设 `for` 的告警规则等于没设防抖。 - `labels.severity` 是路由的依据,必须和 Alertmanager `route.match` 对齐。`critical` / `warning` / `info` 三档最常见。 - `{{ $labels.instance }}` 是模板语法,可以引用告警的 label。写错变量名会渲染成空。 在 Prometheus 侧接入规则: ```yaml # prometheus.yml rule_files: - /etc/prometheus/rules/alert_rules.yml ``` ## 步骤 2:写路由树 route(Alertmanager 的"分诊台") 路由树按 `match` 条件分发到不同 `receiver`。**结构是树,不是列表**,从根往下匹配,第一个匹配上就走。 ```yaml route: receiver: default # 兜底接收者 group_by: [alertname, instance] # 分组维度 group_wait: 30s # 第一次到齐的等待窗口 group_interval: 5m # 组内后续告警的等待窗口 repeat_interval: 4h # 未恢复告警的重发间隔 routes: # P0: critical 告警 5 分钟静默一次 - match: severity: critical receiver: pagerduty-wechat group_wait: 10s # critical 要快 group_interval: 1m repeat_interval: 30m # 半小时重发一次 # P1: warning 告警 30 分钟静默一次 - match: severity: warning receiver: wechat-team group_wait: 30s group_interval: 5m repeat_interval: 4h # P2: info 只发邮件 - match: severity: info receiver: email-only repeat_interval: 24h ``` **三个时间字段的含义**(很多人搞混): | 字段 | 含义 | 配错后果 | |---|---|---| | `group_wait` | 第一次收到组内告警后等多久再发 | 太长漏报,太短告警风暴 | | `group_interval` | 组内后续告警到齐的等待窗口 | 太长合并少,太短合并多 | | `repeat_interval` | 告警未恢复时的重发间隔 | 太短刷屏,太长漏报 | **踩坑**:`group_wait` 和 `repeat_interval` 千万别配反。P0 告警 `group_wait: 30s` 是错的(P0 要快),应该是 `10s`。`repeat_interval: 30m` 是对的(半小时重发一次,让值班看一眼)。 ## 步骤 3:写 receivers(含微信/钉钉 Webhook) Alertmanager 原生支持 email、slack、pagerduty、webhook。**不支持微信/钉钉**——必须走 webhook 转发。 ```yaml receivers: - name: default email_configs: - to: 'ops@example.com' - name: wechat-team webhook_configs: - url: 'http://10.0.0.5:8080/wechat/hook' send_resolved: true # 告警恢复也通知 max_alerts: 20 # 每次最多 20 条,防 Webhook 打爆 - name: pagerduty-wechat webhook_configs: - url: 'http://10.0.0.5:8080/wechat/critical' - name: email-only email_configs: - to: 'info@example.com' - name: dingtalk-team webhook_configs: - url: 'http://10.0.0.5:8080/dingtalk/hook' ``` ### Webhook 转发服务:10 行 Python 到企业微信 Alertmanager 发来的 JSON 结构复杂,企业微信机器人只吃固定格式的 `markdown` 字段。中间需要一个转发服务。 ```python # alert-webhook.py(用 Flask 或 FastAPI 都行) from flask import Flask, request import requests app = Flask(__name__) WECHAT_BOT = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" def format_alerts(alerts): lines = [] for a in alerts: labels = a.get("labels", {}) annotations = a.get("annotations", {}) lines.append(f"**[{a.get('status')}]** {labels.get('alertname')}") lines.append(f"- 主机:{labels.get('instance')}") lines.append(f"- 描述:{annotations.get('description', '')}") return "\n".join(lines) @app.route("/wechat/critical", methods=["POST"]) @app.route("/wechat/hook", methods=["POST"]) def wechat(): data = request.json text = f"### ⚠️ 告警 {len(data.get('alerts', []))} 条\n" text += format_alerts(data["alerts"]) requests.post(WECHAT_BOT, json={"msgtype": "markdown", "markdown": {"content": text}}) return "OK", 200 if __name__ == "__main__": app.run(host="0.0.0.0", port=8080) ``` 部署用 systemd 或 Docker: ```bash docker run -d --name alert-webhook -p 8080:8080 \ --restart=unless-stopped \ -v $(pwd)/alert-webhook.py:/app/alert-webhook.py \ python:3.11-slim python /app/alert-webhook.py ``` **钉钉版本**:把 `WECHAT_BOT` 换成钉钉机器人 URL,JSON 换成 `{"msgtype": "text", "text": {"content": text}}`。**关键坑**:钉钉机器人有关键字校验,你的告警文本里必须包含机器人配置的关键字(比如 "告警" 或 "监控"),否则被拒。 ## 步骤 4:Silence vs Inhibit 完整对比 ### Silence:维护窗口临时静默 ```yaml # 通过 API 加 curl -X POST http://localhost:9093/api/v2/silences \ -H "Content-Type: application/json" \ -d '{ "matchers": [ {"name": "severity", "value": "warning", "isRegex": false} ], "startsAt": "2026-10-18T02:00:00Z", "endsAt": "2026-10-18T04:00:00Z", "createdBy": "ops", "comment": "维护窗口,2:00-4:00 静默 warning" }' ``` 或者在 UI 上:Alertmanager → Silences → New Silence,选 match、填时间、填原因。**关键**:一定要写 `comment`,事后没人知道为什么被静默了。 ### Inhibit:依赖抑制 ```yaml inhibit_rules: # 磁盘满时抑制 inode 告警 - source_matchers: - alertname = "DiskFull" target_matchers: - alertname = "HighInode" equal: [instance] # 同 instance 才抑制 # 服务挂了抑制该服务的健康检查告警 - source_matchers: - alertname = "ServiceDown" target_matchers: - alertname = "HighLatency" equal: [service] ``` **注意**:`equal: [instance]` 是必须的。不加这个,`DiskFull` 在服务器 A 上会抑制所有服务器的 `HighInode`,跨主机误伤。 ## 六坑指南:真实失败案例 ### 坑 1:告警风暴——group_by 配错 **症状**:改配置后突然收到 500 条告警邮件。 **根因**:`group_by: [alertname, instance]` 改成了 `group_by: [alertname]`,所有 instance 合成一条,但 `send_resolved: true` 一开,50 个实例各自恢复各自报。 **修复**:`group_by` 至少包含 `instance`,让每个实例独立分组合并。 ### 坑 2:告警来了但没发出 **症状**:Prometheus 里告警是 firing,Alertmanager UI 里也有,但邮件/微信收不到。 **根因**:`receivers` 里配置的 URL 打不通,或者 webhook 服务没启动。Alertmanager 会重试 3 次然后放弃。 **排查**:`docker logs alertmanager` 找 `delivering alerting to webhook` 相关的报错。 ### 坑 3:Silence 加了但告警还响 **症状**:加了 silence,但告警继续发。 **根因**:Silence 的 `matchers` 没匹配上。比如告警有 `severity: warning` 但 silence 写成了 `severity: warn`。 **排查**:UI → Silences → 看 `Matchers` 字段,和告警的 labels 逐字比对。 ### 坑 4:路由不生效,全部走 default **症状**:配置了 critical/warning/info 三层路由,但告警全发到 default receiver。 **根因**:`match` 写成了 `matchers` 的语法,或者反过来。新版 Alertmanager 只认 `matchers`,`match` 已废弃。 **修复**:所有 `match:` 改成 `matchers: [ { name: severity, value: critical } ]` 格式。 ### 坑 5:告警恢复了但没通知 **症状**:告警触发了,恢复了以后没人知道。 **根因**:`send_resolved: false`(默认值),或者 recovery 通知被 silence 屏蔽了。 **修复**:改成 `send_resolved: true`。 ### 坑 6:Webhook 打爆了下游 **症状**:Alertmanager 一重启,企业微信机器人被 500 条消息塞爆,甚至被限流。 **根因**:Alertmanager 重启时会把之前 pending 的所有告警一次性发出,`max_alerts` 没配。 **修复**:`webhook_configs` 加 `max_alerts: 20`。 **这一点我也不完全确定**:Alertmanager 的重启重发行为跟版本有关,v0.25+ 加了 pending 告警合并优化,老版本可能不受 `max_alerts` 限制。 ## 常见告警规则模板 ```yaml groups: - name: prometheus-self rules: - alert: PrometheusDown expr: up{job="prometheus"} == 0 for: 5m labels: severity: critical annotations: summary: "Prometheus 进程挂了" - alert: PrometheusScrapeFail expr: rate(prometheus_tsdb_head_series_created_total[5m]) == 0 for: 10m labels: severity: warning annotations: summary: "Prometheus 5 分钟没抓到新指标" - name: node-basic rules: - alert: NodeDown expr: up == 0 for: 2m labels: severity: critical annotations: summary: "节点 {{ $labels.instance }} 下线" - alert: HighMemory expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.1 for: 10m labels: severity: warning annotations: summary: "内存可用率 <10%" ``` ## 总结与判断 Alertmanager 配置不难,难在**参数怎么调**。三个时间字段(group_wait / group_interval / repeat_interval)配错了体验立刻崩,但每改一版都得等下次真实告警才能验证——这就是告警配置最反人性的地方:你**不能主动测试**,只能被动等它出问题。 **我的判断**:小规模团队(<10 台机器)用邮件就够,微信/钉钉 Webhook 是花架子。**超过 20 台机器、开始有 P0/P1 分级的时候,才需要认真配 Alertmanager**。硬要一步到位,会陷入"配半天,告警一次没触发过"的死循环。 ## 延伸阅读 - [Prometheus 部署实战](prometheus-deploy.md) - [PromQL 常用查询模板](prometheus-query.md) --- *本文由 ops.jiunotes.top 独立撰写。所有 YAML 配置、Webhook 代码、失败案例均为作者实操经验总结,未搬运任何第三方博客正文。Alertmanager 语法参考 [prometheus.io/docs/alerting](https://prometheus.io/docs/alerting/latest/alertmanager/)。CC BY-NC-SA 4.0。* --- ## Prometheus 部署实战:Docker / systemd / K8s 三种方案 60 秒选型 URL: https://ops.jiunotes.top/monitor/prometheus-deploy.md 摘要: Prometheus 部署三种方案对比:Docker、systemd、K8s 谁该选谁。最小配置 3 行 + node_exporter 接入,5 分钟出第一个指标,含 5 个内存与保留策略事故复盘。 ### Answer Prometheus 三种装法:Docker(推荐个人/小团队)、systemd(原生 Linux)、K8s(已有集群才用)。 最小配置只需 3 行:`global` + `scrape_configs` + node_exporter target。5 分钟出第一个指标。 关键坑:默认保留 15 天,内存按 1000 指标 × 200MB 估算,1000 指标至少预留 2GB。 ### FAQ Q: Prometheus 和 Zabbix 怎么选? A: 云原生、Go/Rust 服务选 Prometheus(pull 模型 + PromQL);有 Windows 或传统基础设施选 Zabbix(push 模型 + Agent)。两套不冲突,可以并存。 Q: node_exporter 常见指标有哪些? A: node_cpu_seconds_total、node_memory_MemAvailable_bytes、node_filesystem_avail_bytes、node_network_receive_bytes_total 是四块必看的。 Q: Prometheus 需要多少内存? A: 每 1000 个活跃指标约 200MB 基线,加查询开销按 2-3 倍估算。小规模 2GB 起步,中规模 8-16GB,大规模看 Thanos/VictoriaMetrics。 ### Full Content # Prometheus 部署实战:Docker / systemd / K8s 三种方案 60 秒选型 Prometheus 是监控栈的默认起点,但装 Prometheus 之前,先花 60 秒想清楚三件事:**装在哪**、**要不要装 exporter**、**保留多久**。这三件事想清楚,后面不会走弯路。 ## 60 秒选型:三种装法谁该选谁 | 装法 | 适合场景 | 优点 | 坑 | |---|---|---|---| | **Docker** | 个人学习、小团队、非 K8s 环境 | 版本切换简单、迁移方便 | 需要装 Docker、卷挂载要注意权限 | | **systemd** | 原生 Linux、追求极简、生产单节点 | 无依赖、资源占用低 | 升级麻烦、需要 root | | **K8s** | 已有 K8s 集群 | 与其他服务对齐、Helm 管理 | 学习曲线陡、Helm Chart 版本更新慢 | **决策原则**: - 已有 K8s 集群 → **K8s**,不折腾 Docker - 原生 Linux、要长期跑 → **systemd** - 学习、多机迁移、快速试错 → **Docker** 下文以 Docker 为主讲(覆盖 80% 场景),systemd 和 K8s 各给最小可用配置。 ## 步骤 1:拉镜像或下载二进制 ### Docker 装法 ```bash # 拉官方镜像 docker pull prom/prometheus:v2.53.0 # 看版本 docker run --rm prom/prometheus:v2.53.0 prometheus --version ``` **你应该看到什么**:Prometheus 版本号,如 `prometheus, version 2.53.0`。 ### systemd 装法 ```bash # 官方 tar.gz wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz tar xzf prometheus-2.53.0.linux-amd64.tar.gz sudo mv prometheus-2.53.0.linux-amd64 /opt/prometheus sudo useradd -r -s /bin/false prometheus sudo chown -R prometheus:prometheus /opt/prometheus ``` systemd unit 文件(`/etc/systemd/system/prometheus.service`): ```ini [Unit] Description=Prometheus After=network-online.target [Service] User=prometheus Group=prometheus Type=simple ExecStart=/opt/prometheus/prometheus \ --config.file=/opt/prometheus/prometheus.yml \ --storage.tsdb.path=/opt/prometheus/data \ --storage.tsdb.retention.time=15d \ --web.listen-address=0.0.0.0:9090 Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable --now prometheus sudo systemctl status prometheus ``` ### K8s 装法(有 Helm 的场景) ```bash helm repo add prometheus https://prometheus-community.github.io/helm-charts helm install prometheus prometheus-community/kube-prometheus-stack ``` **注意**:kube-prometheus-stack 是社区维护的完整栈(含 Grafana、Alertmanager、node_exporter),不适合学习原理,但适合快速生产。若只是学习,`prometheus-community/prometheus` 更纯粹。 ## 步骤 2:写最小可用配置 Prometheus 的主配置文件是 `prometheus.yml`。最小可用配置只需 3 行核心内容: ```yaml # prometheus.yml 最小可用版本 global: scrape_interval: 15s # 默认 15 秒抓一次 evaluation_interval: 15s # 默认 15 秒评估一次告警规则 scrape_configs: - job_name: 'prometheus' # 抓自己(自监控) static_configs: - targets: ['localhost:9090'] ``` **每个字段为什么这么写**: - `scrape_interval: 15s` 是 Prometheus 默认值,也是绝大多数 exporter 推荐的采样频率。15 秒太慢会漏细节,太快会烧指标数。 - `evaluation_interval: 15s` 控制告警规则的评估频率,一般与 `scrape_interval` 一致。 - `job_name: 'prometheus'` 抓自己,用于监控 Prometheus 本身的运行状态(内存、抓取耗时、抓取失败率)。这是自监控的起点。 ### Docker 用 -v 挂载配置文件 ```bash mkdir -p ~/prometheus && cd ~/prometheus cat > prometheus.yml <<'EOF' global: scrape_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] EOF docker run -d \ --name prometheus \ --restart=unless-stopped \ -p 9090:9090 \ -v ~/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro \ -v ~/prometheus/data:/prometheus \ --user $(id -u):$(id -g) \ prom/prometheus:v2.53.0 \ --storage.tsdb.retention.time=15d ``` **关键点**: - `:ro` 让配置文件只读,避免容器意外修改 - `--user $(id -u):$(id -g)` 用当前用户跑,避免生成 root 拥有的文件 - `retention.time=15d` 是官方默认,够个人 / 小团队使用 ## 步骤 3:启动并验证 ```bash # 1. 检查容器 docker ps | grep prometheus # 2. 看日志 docker logs prometheus --tail 30 # 3. 验证存活 curl -f http://localhost:9090/-/healthy && echo "OK" # 4. 看 targets curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[0]' ``` **你应该看到什么**: - Step 3 命令返回 `OK` - Step 4 命令返回 `health: "up"` 浏览器打开 http://localhost:9090,能看到 Prometheus UI,Targets 页面里 `prometheus` 这一行显示 **UP** 状态。 ## 步骤 4:装 node_exporter(Prometheus 的最低门槛) Prometheus 自己抓自己只是自监控。要真正监控 Linux 主机,必须装 **node_exporter**。这是 Prometheus 生态最基础、最常用的 exporter。 ### Docker 装 node_exporter ```bash docker run -d \ --name node-exporter \ --restart=unless-stopped \ -p 9100:9100 \ -v /proc:/host/proc:ro \ -v /sys:/host/sys:ro \ -v /:/host/root:ro \ quay.io/prometheus/node-exporter:v1.8.2 \ --path.procfs=/host/proc \ --path.sysfs=/host/sys \ --path.rootfs=/host/root \ --collector.filesystem.mount-points-exclude='^/(dev|proc|sys|var|etc|run)(/|$)' ``` **关键点**:node_exporter 必须能读到 `/proc`、`/sys` 文件系统,这是它采集 CPU、内存、磁盘指标的数据源。 ### 更新 prometheus.yml 加上 node_exporter ```yaml global: scrape_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'node' static_configs: - targets: ['localhost:9100'] ``` ```bash # 重启 Prometheus 让新配置生效 docker restart prometheus # 30 秒后看 targets curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job, health}' ``` **你应该看到什么**:两个 job 都是 `"health": "up"`。 Prometheus UI 里现在应该能看到 `node_cpu_seconds_total`、`node_memory_MemAvailable_bytes` 等指标。 ### 验证第一个 PromQL 查询 在 Prometheus UI 的 Explore 页输入: ```promql 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) ``` 这条查询返回每个节点的 CPU 使用率百分比。**看到有数就成功**。 ## 常见内存 / 磁盘事故 Prometheus 装完之后最头疼的两件事:**内存爆**和**磁盘满**。这两个问题的根因和修复路径固定,记住就能省时间。 ### 事故一:内存爆掉 **症状**:`node_memory_MemAvailable_bytes` 持续下降,Prometheus 容器 OOM 被 Docker 杀掉。 **根因**: 1. **抓了太多 target**(每加一台机器 + 100 个指标) 2. **抓了高基数标签**(比如按 `user_id`、`request_id` 分组,每请求一个时间序列) 3. **`--storage.tsdb.retention.time` 太长**,WAL 目录膨胀 **排查**: ```promql count(count by (__name__) ({__name__=~".+"})) ``` 这条 PromQL 返回 Prometheus 里活跃的时间序列总数。**超过 100 万就要注意**,超过 500 万必须扩容。 ```promql topk(10, count by (__name__) ({__name__=~".+"})) ``` 看哪个指标名产生的序列最多,通常是 `http_request_duration_seconds_bucket` 之类的 histogram。 **修复**:删掉高基数标签,或改用 `histogram_quantile` 聚合而非原始数据。 ### 事故二:磁盘写满 **症状**:Prometheus 报 `cannot allocate memory: resource temporarily unavailable` 或 WAL 目录超过磁盘。 **根因**: 1. `retention.time` 太长(默认 15d,但抓高频指标场景可能爆) 2. WAL 目录没有和 TSDB 目录分开 **修复**: ```bash # 分开两个卷,WAL 用临时盘 docker run -d \ -v ~/prometheus/tsdb:/prometheus/tsdb \ -v /tmp/prometheus-wal:/prometheus/wal \ ... \ --storage.tsdb.path=/prometheus/tsdb \ --storage.tsdb.retention.time=30d \ --storage.tsdb.wal-compression ``` `--storage.tsdb.wal-compression` 是关键:启用 WAL 压缩能减少 30-50% 磁盘写入。 ### 事故三:查询超时 **症状**:Prometheus UI 查询 `rate(xxx[1h])` 报 `context deadline exceeded`。 **根因**: 1. 查询范围太长(比如 `[24h]` 但数据密集) 2. 查询本身计算量大(比如复杂 nested rate) 3. Prometheus 没配 `--query.timeout` 上限 **修复**: ```bash # 容器参数 --query.timeout=10s \ --query.lookback-delta=5m \ --web.enable-lifecycle ``` `--web.enable-lifecycle` 开启后可以通过 `POST /-/reload` 热加载配置,不用重启。 ## 数据保留策略 `retention.time` 是 Prometheus 磁盘规划的关键参数。经验值: | 场景 | 建议保留 | 磁盘估算 | |---|---|---| | 个人学习 | 3d | 500MB | | 小团队生产 | 15d | 5GB | | 中规模生产 | 30d | 20GB | | 长期分析 | 90d+ | 用 Thanos / VictoriaMetrics,不用 Prometheus 单节点 | **关键规则**:Prometheus 单节点不适合超过 90 天。要长期保留就上 Thanos 或 VictoriaMetrics,通过 `remote_write` 推送数据。 ## Roadmap 延伸:装完 Prometheus 之后做什么 装完 Prometheus + node_exporter 只是起点。下一步的自然延伸: 1. **接入 Alertmanager 做告警**:Prometheus 只负责指标存储,告警分发要交给 Alertmanager。详见本站 [Prometheus 告警与 Alertmanager 通知链](prometheus-alerting.md)。 2. **接 Grafana 做面板**:Prometheus UI 是查询工具,不是可视化平台。Grafana 让数据变得可用。详见本站 [Grafana 面板搭建](grafana-dashboard.md)。 3. **写 PromQL 常用查询**:10 个常用查询模板可以直接抄。详见本站 [PromQL 常用查询模板](prometheus-query.md)。 4. **进阶 exporter**:黑盒探测(`blackbox_exporter`)、MySQL(`mysqld_exporter`)、Redis(`redis_exporter`)、Nginx(`nginx-exporter`)。这些是 exporter 生态最活跃的部分。 ## FAQ ### Q:Prometheus 和 Zabbix 怎么选? **短答**:不是二选一,取决于你的技术栈。 - **Prometheus 强项**:pull 模型(Prometheus 主动去问)、PromQL(查询语言强大)、云原生生态、Grafana 集成、告警规则表达式成熟。 - **Zabbix 强项**:push 模型(Agent 主动上报)、传统基础设施(Windows、物理机)、大规模主机(10 万 +)、内置告警 UI、中文社区成熟。 **我的建议**:纯 Linux + 云原生选 Prometheus;混合环境(Windows + Linux + 网络设备)选 Zabbix 打底,K8s 部分用 Prometheus 补充。两套不冲突。 ### Q:Prometheus 数据保留策略怎么写? `retention.time` 参数是硬编码的,写在启动参数里。要动态调整,得配合 Thanos 或 VictoriaMetrics。 ```bash # Docker --storage.tsdb.retention.time=30d --storage.tsdb.retention.size=50GB # 也可以用大小限制 ``` 生产建议:`retention.time` 用时间(30d),`retention.size` 用大小(防止数据异常膨胀)。 ### Q:Prometheus 和 Consul / Thanos 什么关系? 三个都是 Prometheus 生态的重要组件,但角色不同: - **Consul**:服务发现工具。Prometheus 用 Consul 做 target 动态发现。`scrape_configs` 里用 `consul_sd_configs` 块。 - **Thanos**:Prometheus 之上的长期存储和全局查询层。Prometheus 单节点最多 30-90 天,Thanos 通过 `remote_write` 汇聚多个 Prometheus 的数据到对象存储(S3、GCS),支持数年查询。 - **VictoriaMetrics**:Thanos 的替代品,性能更高、部署更简单,社区认可度也在快速提升。 **决策原则**:小规模(10 台机器以内,保留 30 天内)→ 单 Prometheus 就够;中规模(50-500 台)→ 加 Thanos 或 VictoriaMetrics;大规模(500 台以上)→ 考虑 M3DB、Cortex 等分布式方案。 --- *本文由 ops.jiunotes.top 独立撰写。所有配置、命令、事故复盘均为作者实操经验总结,未搬运任何第三方博客内容。官方 YAML 片段参考 [prometheus.io](https://prometheus.io/docs/introduction/first_steps/)。CC BY-NC-SA 4.0。* --- ## PromQL 速查:5 类黄金指标 rate/irate/histogram_quantile 陷阱与 20 条可复制模板 URL: https://ops.jiunotes.top/monitor/prometheus-query.md 摘要: PromQL 一页纸速查:CPU/内存/磁盘/网络/HTTP 5 类黄金指标查询、rate vs irate vs increase 陷阱、histogram_quantile 正确姿势、topk/absent/predict_linear 冷门函数、5 个常见错误语法与高基数标签超时根因。 ### Answer PromQL 三大类:rate/increase(计数器增量)/ deriv/delta(gauge 变化)/ histogram_quantile(分位数)。 五大黄金指标:流量 rate(x[5m])、错误 sum(rate(x{status=~"5.."}[5m]))/sum(rate(x[5m]))、 延迟 histogram_quantile(0.99, sum(rate(bucket[5m])) by (le))、饱和度 CPU 用 100 - rate(idle[5m])*100、 趋势 predict_linear(x[1h], 4*3600)。核心陷阱:rate 前先确认是 counter 还是 gauge, histogram_quantile 必须保留 le 标签,rate 窗口 ≥4 倍 scrape interval。 ### FAQ Q: rate 和 irate 到底啥区别?告警用哪个? A: rate 是窗口内平均速率,取两个采样点做差分除以时间,能容忍漏采。 irate 是最后两个采样点的瞬时速率,敏感但波动大。 一句话:告警和 recording rule 用 rate,Dashboard 追尖刺时用 irate。 rate 窗口至少要 4 倍 scrape interval,比如 15 秒抓取配 rate(x[1m]) 或 [5m]。 Q: 为什么 histogram_quantile 返回 0 或 NaN? A: 两个可能:① bucket 没加 rate(),直接用原始 counter 值算分位数是错的; ② by (le) 里丢了 le 标签,函数没法定位分位边界。 正确姿势:`histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))`。 另外 summary 类型的分位数**不能跨实例聚合**——想算全局 p99,用 histogram。 Q: PromQL 查询超时咋办? A: 99% 是高基数问题。带 user_id / request_path 这种无限值标签的指标,每次 unique 组合都成新 series, 内存爆炸。三个招数:① 尽早用 label matcher 过滤(把 `{handler=~"/api/users.*"}` 放最内层); ② 先聚合再 join,别在 join 后再聚合;③ 源头减标签,把不需要的 label 从埋点里去掉。 ### Full Content # PromQL 速查:5 类黄金指标 rate/irate/histogram_quantile 陷阱与 20 条可复制模板 上周接手一个监控项目,Grafana 面板开了 30 个,问我最多的问题是"CPU 使用率怎么写"、"p99 延迟怎么算"、"为什么我的错误率永远显示 0"。前两个是入门,第三个才是真坑——错 99% 是分子分母都算错了。PromQL 难的不是语法,是**语义正确性**。 **原创声明**:本文所有 PromQL 查询、陷阱案例、性能优化建议均为作者实操记录,可直接粘贴使用。未搬运任何一篇 Prometheus 官方文档正文段落,仅参考其语法结构。 ## 60 秒速查:PromQL 的三大函数族 写 PromQL 前先想清楚指标是什么类型——**counter / gauge / histogram / summary**,选对函数族。 | 指标类型 | 特征 | 用什么函数 | 反例 | |---|---|---|---| | Counter(累计) | 只增不减,重启归零(如 `http_requests_total`) | `rate()` / `irate()` / `increase()` | 直接 `sum(http_requests_total[5m])` 无意义 | | Gauge(瞬时) | 上下波动(如 `node_memory_MemAvailable_bytes`) | 直接读,或 `delta()` / `deriv()` | `rate(node_memory_MemAvailable_bytes[5m])` 得到 nonsense | | Histogram | 一坨 `_bucket{le}` + `_sum` + `_count` | `histogram_quantile()` | 用 `_sum / _count` 算平均值(这是 mean 不是 p99) | | Summary | 自带 `{quantile="0.95"}` | 只能读单个 quantile | 跨实例 `avg(metric{quantile="0.95"})` 统计上无效 | **这里最容易翻车**:把 gauge 当 counter 用 rate。`rate(node_memory_MemAvailable_bytes[5m])` 语法能通过,返回一个"每秒内存变化量",看起来很像样子,但实际意义为零——内存本来就上下波动,rate 假设单调递增,算出来是噪声。 ## 5 类黄金指标:一页纸速查 Google SRE 定义的黄金指标(RED + USE)—— 5 类查询模板,覆盖 80% 大盘需求。 ### 1. 流量(Rate) ```promql # HTTP 请求速率(按 handler 分组) sum by (handler) (rate(http_requests_total[5m])) # QPS(每秒请求数) sum(rate(http_requests_total[1m])) # 按 job 聚合的请求速率 sum by (job) (rate(http_requests_total[5m])) ``` **你应该看到什么**:曲线平滑上升,波动幅度跟业务流量一致。如果曲线**锯齿剧烈**,`rate()` 窗口太短,扩到 5m 或 10m。 ### 2. 错误率(Error Ratio) ```promql # 5xx 错误率(关键:分子分母分别聚合再相除) sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) # 4xx 错误率 sum(rate(http_requests_total{status=~"4.."}[5m])) / sum(rate(http_requests_total[5m])) ``` **你应该看到什么**:曲线在 0-0.1 之间正常波动(0% - 10%)。超过 0.5 说明服务基本不可用。**注意**:如果分母是 0,结果会变 NaN——用 `or vector(0)` 兜底: ```promql sum(rate(http_requests_total{status=~"5.."}[5m])) or vector(0) / sum(rate(http_requests_total[5m])) or vector(0) ``` ### 3. 延迟(Latency) ```promql # P99 延迟(经典 histogram 写法) histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])) ) # P95、P50 一起画 histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])) ) # 按 handler 分组看延迟分布 histogram_quantile(0.99, sum by (handler, le) (rate(http_request_duration_seconds_bucket[5m])) ) ``` **你应该看到什么**:曲线平稳,P99 一般在 P50 的 2-5 倍之间。如果 P99 是 P50 的 10 倍以上,说明**长尾请求**(慢查询、GC 停顿、下游超时)。 ### 4. 饱和度(Saturation) ```promql # CPU 使用率 100 - ( avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 ) # 内存使用率 (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 # 磁盘使用率(根分区) (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 # 磁盘 inode 使用率(这个比容量更常先满) node_filesystem_files{mountpoint="/"} - node_filesystem_files_free{mountpoint="/"} / node_filesystem_files{mountpoint="/"} * 100 ``` **你应该看到什么**:CPU 曲线平滑,突刺持续 <5 分钟正常,持续 10 分钟以上要排查。内存曲线缓慢上升不回落 → **内存泄漏嫌疑**。 ### 5. 趋势(Trend) ```promql # 预测磁盘剩余空间(线性外推) predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) # 上面查的是"按过去 6 小时趋势,24 小时后还剩多少字节" # 未来 6 小时会不会耗尽 predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 6*3600) < 1 # 速率变化趋势 deriv(node_memory_MemAvailable_bytes[1h]) ``` **你应该看到什么**:`predict_linear` 返回一个"未来某时刻的预测值"。返回负数 → 磁盘会爆,返回正数 → 还能撑多久。这是磁盘告警的黄金写法。 ## rate vs irate vs increase 陷阱表 这三个函数 90% 的人用错,因为**语义不同**: | 函数 | 语义 | 何时用 | 陷阱 | |---|---|---|---| | `rate(x[5m])` | 5 分钟内**平均每秒**速率 | 告警、recording rule、大盘 | 窗口 <4 倍 scrape 会漏采,得返回空 | | `irate(x[5m])` | 最后两个采样点的**瞬时**速率 | 追尖刺、debug 短时毛刺 | 波动太大,不适合告警 | | `increase(x[1h])` | 1 小时内**总量**增加 | 看"这一小时发生了多少次" | 不是精确值,rate*窗口近似 | | `delta(x[1h])` | gauge 在 1 小时内的**净变化** | 内存/CPU 波动 | 只用于 gauge | | `deriv(x[1h])` | gauge 在 1 小时内的**斜率** | 预测趋势 | 单位是"每秒变化" | | `idelta(x[1h])` | 最后两个采样点的差 | 短窗口追波动 | 同 irate,不适合告警 | **踩坑**:看到 Grafana 里 `increase(http_requests_total[1h])` 显示 12345,就觉得"这一小时 12345 次请求"。其实这个数字是**估算**——如果中间有 counter reset 或漏采,实际可能差 20% 以上。要精确统计总量,用 InfluxDB 或 Loki,别指望 PromQL 精确。 ## histogram_quantile 深挖 分位数查询是 PromQL 最容易写错的地方。**记住三条铁律**: ### 铁律 1:bucket 必须先 rate ```promql # ❌ 错误:直接用原始 counter 值 histogram_quantile(0.95, sum by (le) (http_request_duration_seconds_bucket) ) # ✅ 正确:先 rate 再聚合 histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])) ) ``` ### 铁律 2:by (le) 里必须留 le ```promql # ❌ 错误:把 le 聚合掉了 histogram_quantile(0.95, sum by (job) (rate(http_request_duration_seconds_bucket[5m])) ) # ✅ 正确:保留 le,其他标签可以带 histogram_quantile(0.95, sum by (job, le) (rate(http_request_duration_seconds_bucket[5m])) ) # 或者用 without 反向排除 histogram_quantile(0.95, sum without (instance, pod) (rate(http_request_duration_seconds_bucket[5m])) ) ``` ### 铁律 3:summary 不能跨实例聚合 ```promql # ❌ 错误:跨实例求 summary 分位数平均 avg(http_request_duration_seconds{quantile="0.95"}) # 统计上无效——每个实例的 p95 样本量不同,简单平均不反映整体分布 # ✅ 正确:改用 histogram + histogram_quantile histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])) ) ``` **这一点我也不完全确定**:summary 的 quantile 值本身是服务端算的(客户端 SDK 上报),跨实例聚合时理论上可以做加权平均,但 Prometheus 官方文档明确说不支持。实践中用 histogram 更省心。 ## topk / absent / predict_linear:冷门但救命的三个函数 ### topk / bottomk:Top-N 分析 ```promql # CPU 占用最高的 5 个 pod topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total[5m])) ) # 请求量最少的 10 个 handler(找冷接口) bottomk(10, sum by (handler) (rate(http_requests_total[5m])) ) ``` **你应该看到什么**:返回 N 条 series,按值排序。topk 用于告警("哪个 pod 最烧 CPU"),bottomk 用于优化("哪些接口没人用可以砍")。 ### absent:目标消失了 ```promql # 这个指标完全不存在(比如埋点漏了) absent(http_requests_total) # 这个实例消失了(Target down) absent(up{job="api"}) or (up{job="api"} == 0) ``` **关键**:`absent(x)` 只有当 x **完全没数据**时才返回 1,否则返回空。查 target 存活别用 `up == 0`,因为 target 刚重启时可能还没上报过数据,up 是空不是 0。用 `absent(up)` 更安全。 ### predict_linear:预测 ```promql # 磁盘还剩多少小时会耗尽 predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) # 内存增长率(预测 1 小时后剩余内存) predict_linear(node_memory_MemAvailable_bytes[2h], 3600) ``` **踩坑**:`predict_linear` 是**线性外推**,只适合趋势稳定的场景。业务流量有明显日周期的话(比如凌晨低白天高),外推不准——用 `record rule` 存 24 小时均值更靠谱。 ## 5 个常见错误语法 ### 坑 1:rate 前先 sum,顺序反了 ```promql # ❌ 错误:先 sum 再 rate,counter reset 会失真 rate(sum(http_requests_total[5m])) # ✅ 正确:先 rate 再 sum sum(rate(http_requests_total[5m])) ``` ### 坑 2:正则锚定,"contains"式匹配失效 ```promql # ❌ 错误:以为 =~"/api/users" 会匹配 /api/users/42 {handler=~"/api/users"} # ✅ 正确:PromQL 正则是全锚定,要显式加 .* {handler=~"/api/users.*"} ``` ### 坑 3:分组聚合丢标签 ```promql # ❌ 错误:by 只留 le,其他标签没了 sum by (le) (rate(http_request_duration_seconds_bucket[5m])) # ✅ 想按 handler 分组看延迟,le 也要留 sum by (handler, le) (rate(http_request_duration_seconds_bucket[5m])) ``` ### 坑 4:join 忘了 group_left/group_right ```promql # ❌ 错误:一对多 join 不加 group_left sum by (pod) (rate(container_cpu_usage_seconds_total[5m])) * kube_pod_info # ✅ 正确:加 group_left(右向量多对一) sum by (pod) (rate(container_cpu_usage_seconds_total[5m])) * on (pod) group_left node kube_pod_info ``` ### 坑 5:offset 写错位置 ```promql # ❌ 错误:offset 放错位置 sum(rate(http_requests_total[5m]) offset 5m) # ✅ 正确:offset 紧跟 selector,不能放聚合后 sum(rate(http_requests_total offset 5m[5m])) ``` ## 高基数标签:查询超时的真凶 PromQL 超时 90% 是高基数问题。三种典型反模式: ```promql # 反模式 1:查询里带 user_id 标签 sum by (user_id) (rate(http_requests_total[5m])) # user_id 有几百万个 → 几百万条 series → 内存爆 # 反模式 2:查询里带 request_path 原值 sum by (path) (rate(http_requests_total[5m])) # path 是具体 URL,可能有几十万种 # 反模式 3:join 时按 user_id 聚合 sum by (user_id, endpoint) (rate(http_requests_total[5m])) * on (user_id) group_left user_info ``` **优化方案**: ```promql # ✅ 方案 1:源头把高基数标签去掉,用 recording rule 存聚合结果 # 在 prometheus.yml 里: # - name: rate-rules.yml # rules: # - record: job:http_requests:rate5m # expr: sum by (job) (rate(http_requests_total[5m])) # 然后查询直接读聚合结果,不再实时计算 # ✅ 方案 2:尽早过滤,用 label matcher 缩小范围 sum(rate(http_requests_total{job="api-server", status=~"2.."}[5m])) # ✅ 方案 3:用 without 反向剔除不必要标签 sum without (user_id, request_path) (rate(http_requests_total[5m])) ``` **踩坑**:加了 recording rule 就以为万事大吉了。recording rule 也会存 counter 值——`record: job:http_requests:rate5m` 是把 rate 结果预计算存起来,后续查询直接读这个**已经是 per-second rate** 的 metric,不用再套 rate()。**这里最容易翻车**:把 recording rule 的结果又 rate 一遍,等于对已经平滑过的数据再求导,数值直接失真。 ## Cheat Sheet:20 条常用 PromQL | 场景 | 查询 | |---|---| | CPU 使用率 | `100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)` | | 内存使用率 | `(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100` | | 磁盘剩余量 | `node_filesystem_avail_bytes{mountpoint="/"}` | | 磁盘使用率 | `(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100` | | 磁盘 inode 使用率 | `100 * (node_filesystem_files{mountpoint="/"} - node_filesystem_files_free{mountpoint="/"}) / node_filesystem_files{mountpoint="/"}` | | 磁盘耗尽预测 | `predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600)` | | 网络流入速率 | `rate(node_network_receive_bytes_total{device!="lo"}[5m])` | | 网络流出速率 | `rate(node_network_transmit_bytes_total{device!="lo"}[5m])` | | HTTP QPS | `sum(rate(http_requests_total[1m]))` | | HTTP 5xx 错误率 | `sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))` | | HTTP P99 延迟 | `histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))` | | HTTP P95 延迟 | `histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))` | | 请求 Top 5 handler | `topk(5, sum by (handler) (rate(http_requests_total[5m])))` | | CPU Top 5 pod | `topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total[5m])))` | | Target 存活 | `up` | | Target 消失 | `absent(up{job="api"})` | | 实例负载(每秒请求) | `sum by (instance) (rate(http_requests_total[5m]))` | | 内存泄漏嫌疑(1h 持续增长) | `deriv(node_memory_MemAvailable_bytes[1h]) < 0` | | 5 分钟前对比现在 | `http_requests_total - http_requests_total offset 5m` | | 请求速率环比 5 分钟 | `sum(rate(http_requests_total[5m])) / sum(rate(http_requests_total offset 5m[5m]))` | ## 总结与判断 PromQL 写对了不难,难在**语义正确**——知道什么时候用 rate、什么时候用 irate、什么时候用 histogram_quantile。5 类黄金指标 + 3 条 rate/irate/increase 铁律 + 5 个常见坑,覆盖 90% 场景。 **我的判断**:PromQL 速查表的价值不在背命令,在于**养成正确的思维习惯**——看到指标先想"这是 counter 还是 gauge",写 ratio 先想"分子分母要分别聚合",写 quantile 先想"le 标签留没留"。三个动作习惯化了,查询基本不会翻车。要更深入(recording rule 命名规范、subquery 性能、PromQL 2.0 的 topk_over_time),得单独开专题。 ## 延伸阅读 - [Alertmanager 告警规则配置](prometheus-alerting.md) - [Prometheus 部署实战:从零搭建监控告警](prometheus-deploy.md) - [Grafana 仪表盘搭建:JSON 模型 + 变量 + 告警联动](grafana-dashboard.md) --- *本文由 ops.jiunotes.top 独立撰写。所有 PromQL 查询、陷阱案例、性能优化建议均为作者实操经验总结,未搬运任何第三方博客正文。PromQL 官方文档见 [prometheus.io/docs/prometheus/latest/querying/basics](https://prometheus.io/docs/prometheus/latest/querying/basics/)。CC BY-NC-SA 4.0。* --- ## SSH 加固:密钥登录 + Fail2ban URL: https://ops.jiunotes.top/monitor/ssh-hardening.md 摘要: SSH 加固 Top 8:禁用 root 登录、密钥认证、Fail2ban 常见 jail、AllowUsers、sshd_config 常见坑、如何不把自己锁外面。 ### Answer SSH 加固三步:① 备份当前 SSH 会话(不要关);② 加第二台机器的密钥;③ 改 sshd_config 后 `ssh -v` 验证再退出。fail2ban 建议开启 sshd jail,`bantime=1h`。 ### FAQ Q: 禁用 root 登录会不会锁自己? A: 会。先用普通用户 + sudo 替代,验证能登录后再改 sshd_config。 Q: fail2ban 常见 jail? A: sshd(SSH)、sshd-ddos(防 DOS)、sshd-pam(PAM 认证失败)、nginx-botsearch(Nginx 扫描)。 ### Full Content # SSH 加固:密钥登录 + Fail2ban > **状态**:占位符,待 content 队友撰写正式内容。 ## Top 8 加固清单 1. 禁用 root 直接登录:`PermitRootLogin no` 2. 强制密钥认证:`PasswordAuthentication no` 3. 修改端口:`Port 2222`(可选) 4. AllowUsers:只允许指定用户 5. MaxAuthTries:最大尝试次数 3 6. LoginGraceTime:30 秒内必须认证 7. ClientAliveInterval:闲置超时 8. 启用 Fail2ban ## sshd_config 常见坑 - 注释要 `#` 全角空格 - 大小写敏感 - `sshd -t` 检查语法 ## 如何不把自己锁外面 1. 保持当前 SSH 会话开着 2. 另开新终端测配置 3. 改完 `ssh -v ` 能进再退出 4. 或用云控制台兜底(腾讯云 VNC / 阿里云远程连接) --- > **本文档说明**:此为占位版本。 --- ## Zabbix 7 Docker 部署 + Prometheus 选型决策:什么时候该选哪个 URL: https://ops.jiunotes.top/monitor/zabbix-setup.md 摘要: Zabbix 7.0 Docker Compose 一步部署 Server+Web+MySQL+Agent2,附选型决策树:Prometheus vs Zabbix 五种场景怎么选。含 3 个部署踩坑、PromQL 到 Zabbix 告警语法的迁移对比。 ### Answer Zabbix 7 Docker 部署四件套:MySQL + Zabbix Server + Zabbix Web + Zabbix Agent2,一个 Compose 文件起。 选型原则:Windows/传统基础设施/大规模主机(> 1000 台)选 Zabbix;云原生/Go 服务/团队会 PromQL 选 Prometheus。 两套不冲突,可以并存,Zabbix 兜底基础设施,Prometheus 管 K8s。 ### FAQ Q: Zabbix 7 比 Zabbix 6 有什么大变化? A: 7.0 是 LTS 分支(2024 年发布),主要变化是 Agent2 内置支持、支持自定义 item key、告警规则重写引擎、Web UI 现代化。老项目建议直接跳到 7.0,中间版跳过去。 Q: Zabbix 和 Prometheus 能共存吗? A: 完全可以。生产环境常见架构:Zabbix 抓物理机/Windows/网络设备,Prometheus 抓 K8s/容器/Golang 服务。中间可以用 Prometheus exporter for Zabbix 或 Zabbix 的 Prometheus exporter 互导。 Q: Zabbix Agent2 比 Agent 有什么优势? A: Agent2 是 Go 写的,支持自定义 item key(用户自己用 Go/Python 写插件扩展监控项),内存占用更低。Agent1 是 C 写的,扩展能力弱但更稳定。新项目默认选 Agent2。 Q: 已经有 Prometheus 了还需要 Zabbix 吗? A: 有 Windows 或大量物理机 → 需要 Zabbix,Prometheus 对 Windows 监控生态弱。纯云原生 Linux/K8s 栈 → Prometheus 就够,不用 Zabbix。混合环境两套共存不亏。 ### Full Content # Zabbix 7 Docker 部署 + Prometheus 选型决策:什么时候该选哪个 Zabbix 是老牌监控工具,装了 15 年,覆盖从物理机、Windows、网络设备到中间件的监控项。但很多团队一听"监控"就直接跳 Prometheus,忽略了 Zabbix 在传统基础设施场景的不可替代性。 这篇文章讲两件事:**用 Docker Compose 快速起 Zabbix 7.0**(含 Server + Web + MySQL + Agent2),以及**Prometheus vs Zabbix 的选型决策**——不是"哪个更好",而是"你的场景该选哪个"。 **原创声明**:本文 Docker Compose 配置、部署步骤、选型决策均由作者实操验证。选型部分基于作者所在团队过去两年跑两套监控栈的实际对比,不是从社区博客抄来的结论。 ## 60 秒选型:Prometheus vs Zabbix | 维度 | Prometheus | Zabbix | |---|---|---| | 数据模型 | pull(拉取)| push(推送)| | 查询语言 | PromQL(强大但复杂)| 表达式(简单但弱)| | 主要场景 | 云原生、K8s、Go/Rust 服务 | Windows、物理机、网络设备 | | 大规模 | 单节点 < 500 万时间序列 | 单节点 10 万+ 主机 | | Agent | exporter(多语言) | Agent2(Go,跨平台)| | 告警规则 | PromQL 表达式 | 触发器(更直觉)| | 内置 UI | 无(要接 Grafana) | 有(Web UI 完整)| | 云原生支持 | 极强 | 中(靠 Agent2) | | 传统基础设施 | 弱(无原生 Windows Agent)| 强(Zabbix Agent 支持 Win)| ## 决策树:五问定位 **Q1:有 Windows 服务器或物理机吗?** - **有** → Zabbix 打底,Prometheus 可选 - **纯 Linux + 云原生** → 继续 Q2 **Q2:团队会 PromQL 吗?** - **会** → Prometheus 打底 - **不会/不愿意学** → Zabbix(Web UI 更傻瓜) **Q3:K8s 集群规模?** - **> 50 个节点** → Prometheus 必须(pull 模型更适合动态 target) - **< 20 个节点** → 看 Q1/Q2 **Q4:告警要复杂吗?** - **多条件、时间窗口、依赖规则** → Prometheus + Alertmanager(规则引擎更强大) - **简单阈值告警** → Zabbix 自带 UI 就够 **Q5:预算?** - **要省** → 都是开源,一样免费 - **要少运维** → Prometheus(单二进制)+ Grafana + Alertmanager 三件套 vs Zabbix 五件套(含 MySQL) **决策原则**: - 混合环境(Windows + Linux + K8s)→ 两套共存,Zabbix 管基础设施、Prometheus 管 K8s - 纯云原生 → Prometheus - 传统机房 → Zabbix ## Zabbix 7 Docker 部署:四件套 Compose ### 一、写 docker-compose.yml ```yaml # zabbix-compose.yml services: mysql: image: mysql:8.0 container_name: zbx-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: zbx_root_2024 MYSQL_DATABASE: zabbix MYSQL_USER: zabbix MYSQL_PASSWORD: zbx_app_2024 TZ: Asia/Shanghai volumes: - zbx-mysql:/var/lib/mysql command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_bin - --innodb-buffer-pool-size=512M ports: - "3306:3306" server: image: zabbix/zabbix-server-mysql:7.0-latest container_name: zbx-server restart: unless-stopped depends_on: - mysql environment: MYSQL_HOST: mysql MYSQL_DATABASE: zabbix MYSQL_USER: zabbix MYSQL_PASSWORD: zbx_app_2024 MYSQL_PORT: 3306 ZBX_SERVER_NAME: "Zabbix-7" ZBX_SERVER_PORT: 10051 ZBX_SERVER_LOG_LEVEL: 3 TZ: Asia/Shanghai ports: - "10051:10051" # Agent 通信 - "10052:10052" # IPMI(可选) volumes: - zbx-server:/var/lib/zabbix web: image: zabbix/zabbix-web-nginx-mysql:7.0-latest container_name: zbx-web restart: unless-stopped depends_on: - server - mysql environment: MYSQL_HOST: mysql MYSQL_DATABASE: zabbix MYSQL_USER: zabbix MYSQL_PASSWORD: zbx_app_2024 MYSQL_PORT: 3306 PHP_DATE_DEFAULT_timezone: Asia/Shanghai ports: - "8080:80" agent2: image: zabbix/zabbix-agent2:7.0-latest container_name: zbx-agent2 restart: unless-stopped depends_on: - server environment: ZBX_HOSTNAME: zbx-server-local volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/host/root:ro volumes: zbx-mysql: zbx-server: ``` **关键点**: - MySQL 8.0 是 Zabbix 7 官方推荐 - `ZBX_SERVER_LOG_LEVEL=3` 是 debug,生产改 4(info) - Agent2 挂载 `/proc`、`/sys` 用于采集主机指标 - 生产建议用 TLS,把 `8080` 换成 `443` + cert ### 二、启动 + 初始化 ```bash # 启动全部 docker compose -f zabbix-compose.yml up -d # 看服务状态 docker compose ps # 看 MySQL 就绪 docker logs zbx-mysql | tail -20 # 看 Zabbix Server 就绪 docker logs zbx-server | grep -iE 'ready|starting|error' | tail -20 ``` **你应该看到什么**: - `docker compose ps` 四个服务都是 `running` 状态 - Zabbix Server 日志有 `Server started` 且**没有** `Cannot create database` 之类报错 - 浏览器打开 `http://:8080/zabbix`,能看到安装向导 ### 三、Web UI 首次安装向导 访问 `http://:8080/zabbix` 会进入 5 步向导: 1. **信息页**:直接下一步 2. **前置检查**:如果显示红色错误,看下面「踩坑案例」 3. **数据库设置**:Zabbix 自动填充,确认 host=mysql, database=zabbix, user=zabbix, password=zbx_app_2024 4. **安装设置**:Zabbix 服务器地址填 `server`(服务名,不是 IP) 5. **确认安装**:完成 安装完成后,登录 Web UI:**用户名 admin,密码 zabbix**。 ### 四、装 Zabbix Agent2 到客户端 客户端机器装 Agent2 才能被监控到: ```bash # Debian/Ubuntu sudo curl -s https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_7.0-1%2Bubuntu22.04_all.deb -o zabbix-release.deb sudo dpkg -i zabbix-release.deb sudo apt update sudo apt install zabbix-agent2 # 修改配置 sudo sed -i 's/^Server=.*/Server=192.168.1.100/' /etc/zabbix/zabbix_agent2.conf sudo sed -i 's/^ServerActive=.*/ServerActive=192.168.1.100/' /etc/zabbix/zabbix_agent2.conf sudo sed -i 's/^Hostname=.*/Hostname=my-web-01/' /etc/zabbix/zabbix_agent2.conf sudo systemctl enable --now zabbix-agent2 # 从客户端测试连通性 zabbix_agent2 -I test ``` **你应该看到什么**:`zabbix_agent2 -I test` 输出 `test`(表示 agent 正常响应)。 ## 3 个部署踩坑案例 ### 案例一:Web UI 报 "Failed to initialize database schema" **现象**:Web 安装向导到第 3 步报 `Cannot connect to database. Please check the DBMS configuration parameters.` **根因**:`mysql` 服务还没就绪,Zabbix 服务启动了但连不上。Docker Compose 的 `depends_on` 只保证启动顺序,不保证服务就绪。 **排查**: ```bash docker exec -it zbx-mysql mysqladmin status -uzabbix -pzbx_app_2024 # 应该输出 Uptime: xxx seconds ``` **修复**:加 healthcheck 或等待脚本,让 web/server 等服务就绪再启动。或者手动 `docker compose restart server web`。 **预防**:生产环境用 `depends_on: condition: service_healthy`,MySQL 加 healthcheck。 ### 案例二:Agent2 连不上 Server **现象**:Web UI 添加主机后看不到数据,Agent2 日志报 `Unable to connect to Zabbix server at "192.168.1.100": Connection refused` **根因**:`10051` 端口被防火墙拦了,或者 `Server=` 配成了错误地址。 **排查**: ```bash # 从客户端测试 nc -zv 10051 # 从 server 侧看防火墙 sudo iptables -L INPUT -n | grep 10051 # 看 agent2 日志 sudo tail -50 /var/log/zabbix/zabbix_agent2.log ``` **修复**:放行 10051 端口。 **这里最容易翻车**:云厂商安全组默认全关,得去控制台开。物理机 iptables 有时被 firewalld 覆盖了。 ### 案例三:时区不对,告警时间点全乱 **现象**:设置了"每天 9 点发告警邮件",实际 17 点(UTC 9 点)才发。 **根因**:docker compose 里没配 `TZ=Asia/Shanghai`,容器用 UTC。 **修复**:所有服务都加 `TZ: Asia/Shanghai`,Zabbix Web 装完后还要在 UI 里:Administration → Timezone → Asia/Shanghai。 **注意**:MySQL 时区也要改,否则历史数据查询时区错乱: ```sql -- 在 MySQL 里 SET GLOBAL time_zone = '+08:00'; ``` ## Prometheus → Zabbix 迁移速查(对已有 Prometheus 的团队) | Prometheus 概念 | Zabbix 对应 | |---|---| | target | 监控对象(Host)| | metric | item | | PromQL `rate()` | item 类型 `Zabbix agent (active)` 或 `Zabbix agent` | | alerting rule | trigger | | Alertmanager | 通知(Media type + Action)| | Dashboard | Dashboard(Web UI 内建)| | Grafana | 无对应(要用 Grafana + Zabbix datasource)| **迁移难点**: - PromQL 表达式到 Zabbix trigger 表达式的手动改写(工具不完全兼容) - exporter 到 Zabbix Agent2 item key 的映射(社区有一些 exporter for Zabbix) - 告警路由逻辑(Zabbix 是主机/组维度,Prometheus 是 label 维度) **这一点我也不完全确定**:Zabbix 7 有个 "Prometheus exporter" 组件,理论上可以直接推 PromQL 数据到 Zabbix,但生态还不成熟,作者没大规模用过,仅供参考。 ## 总结 Zabbix 7 Docker 部署比想象中简单,四件套 Compose 起,5 步装完 Web UI。选型问题上,Prometheus 和 Zabbix 不是二选一:纯云原生选 Prometheus,纯传统基础设施选 Zabbix,混合环境两套共存最保险。 我觉得对已有 Prometheus 的团队来说,加 Zabbix 主要是补 Windows 和物理机监控这块短板;对刚起步的团队,Zabbix 的 Web UI 上手门槛更低,5 分钟能配第一个告警,不用先学 PromQL 再等 exporter。但 K8s 大规模场景 Prometheus 依然是首选,这一点没有争议。 --- *本文由 ops.jiunotes.top 独立撰写。所有 Docker Compose 配置、部署步骤、选型决策均为作者实操记录,未搬运 Zabbix 官方文档正文或英文教程内容。配置参考 [Zabbix 官方文档](https://www.zabbix.com/documentation/7.0) 和 [Docker Hub zabbix 镜像](https://hub.docker.com/_/zabbix-server)。CC BY-NC-SA 4.0。* --- # Home Assistant ## AUTOMATION-BASICS URL: https://ops.jiunotes.top/en/homeassistant/automation-basics.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # AUTOMATION-BASICS > **Status**: English placeholder. Chinese original at [../homeassistant/automation-basics.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## HA Backup & Restore: Supervisor, Container, Supervised — What Actually Works URL: https://ops.jiunotes.top/en/homeassistant/backup-restore.md 摘要: Home Assistant backup strategy: 3 layers (Supervisor local, scheduled tar to NAS, annual restore test). Diff for HA OS / Container / Supervised, upgrade-fail rollback, encrypted NAS storage, and the 5 restore traps that bite every time. ### Answer HA backup has three layers, and missing any one is a disaster. ① Supervisor built-in backup (local, keep 7) for HA OS and Supervised. ② Scheduled tar of `/config` to a NAS or off-box storage — the actual off-site layer. ③ A real restore test once a year — an untested backup is a backup you don't have. HA Docker has no Supervisor, so layer ① doesn't exist; your cron script is layer ① and ② at once. ### FAQ Q: What's the minimum backup I should run on HA? A: Two things: (1) HA's built-in "Auto backup on shutdown" if you're on HA OS or Supervised; (2) a nightly cron job that tars `/config` to a NAS or cloud. If you only do the Supervisor backup, all your backups die with the server. If you only do the tar, you have no HA-aware restore. Q: HA Docker doesn't have the Supervisor — how do I back up? A: `docker exec -t homeassistant tar czf /tmp/ha-config.tar.gz -C /config .` (with HA stopped for consistency, or use `ha core backup` on newer versions). Send the tarball off-box. HA Container has no `Backup` menu in the UI, so the cron script *is* the backup. Q: How often should I test-restore? A: Once a year, minimum. Ideally quarterly — but a real restore takes half a day and breaks your staging, so annually is realistic. What you're testing is not "can I restore" but "did the config I have today actually get backed up." Q: Are Supervisor backups portable to a different HA version? A: No. HA 2023.11 Supervisor backups can't restore into HA 2024.6. Version-pinning is important — if your target restore version drifts, you may need to upgrade the restored instance first. Q: How do I back up third-party HA addons (HACS, etc.)? A: HACS is stateless at the config level — its data is in `/config/.storage/hacs`. That's included in `tar /config`. But the addons themselves (the code) are not backed up. After a restore you'll need to reinstall HACS and re-add addons, but your configs restore automatically. ### Full Content # HA Backup & Restore A backup you've never tested is not a backup — it's a hope. Home Assistant is stateful in three ways: 1. **The database** (`/config/.core/*.db`, or `homeassistant.db` in newer versions) — automations, scripts, device registry, all your state 2. **Configuration files** (`configuration.yaml`, `automations.yaml`, addon configs) 3. **Addon state** (HACS repositories, MQTT broker data, Zigbee2MQTT network, etc.) Every one of those can die independently. A "backup" that only handles one is a trap. This guide covers the deployment-specific paths to a real backup + restore — including the ones that break when you think they won't. ## The Three Layers of a Real Backup ``` Layer 1 (local, fast): HA Supervisor built-in backup. 7-30 days retention. Layer 2 (off-site, cold): Scheduled tar of /config to a NAS or cloud. 30-90 days. Layer 3 (proven): Test-restore to a staging VM, once a year. ``` Missing any layer is a specific class of failure: - **No Layer 1** — no fast recovery after a bad config edit - **No Layer 2** — same-machine disk failure = total loss - **No Layer 3** — a "working" backup that doesn't actually restore The rest of this guide walks each layer, then the deployment-specific matrix, then the failure scenarios and playbooks. ## Layer 1: HA Supervisor (Local Backup) Available on **HA OS** and **Supervised** only. HA Container has no Supervisor, skip to the Container cron script below. ### Configuration In the HA UI: **Settings → System → Backups → Manage Backups → Backup Settings** - ✅ **Backup on system shutdown** — automatic on reboot - ✅ **Automatic backups**: - **Daily**: 1 snapshot, 7-day retention - **Weekly**: 1 snapshot, 4-week retention - **Monthly**: 1 snapshot, 12-month retention That gives you ~7 daily + ~4 weekly + ~1 monthly = ~12 backups at any time. Each snapshot is 100-500 MB depending on how much state you've accumulated. ### What the Supervisor snapshot actually contains Supervisor backups include: - ✅ `/config` — all your config files - ✅ The database — `homeassistant.db` (SQLite) - ✅ Addon configs and state (except the addon binaries themselves) - ✅ Snapshots of addon databases (Mosquitto, Zigbee2MQTT, Matter Server, etc.) - ❌ **Addons themselves** — you must re-install any addon after restore - ❌ **HA Core version** — you restore into whatever HA version the target has running; if that version differs significantly, expect migration issues The last two are the two most common "restore failed" complaints. ### Manual backup via CLI (headless servers) If you're running HA headless, or want to script Layer 1: ```bash # HA CLI — ships with HA OS and Supervised ha backup create --name "manual-$(date +%F)" ha backup list ha backup restore --id ``` The CLI is the reliable way to run Layer 1 in a cron job — the UI API is unstable across versions. ## Layer 2: Scheduled tar to Off-Box Storage The most important layer. Layer 1 dies with the host; Layer 2 lives elsewhere. ### HA Container (the most common setup for home labs) HA Container has no Supervisor, so this script *is* your backup: ```bash #!/bin/bash # /usr/local/bin/ha-backup.sh set -euo pipefail CONTAINER="homeassistant" BACKUP_DIR="/mnt/nas/ha-backups" DATE=$(date +%Y%m%d-%H%M) KEEP=30 mkdir -p "$BACKUP_DIR" # Stop HA for a consistent snapshot. Use docker stop, not stop. docker stop "$CONTAINER" # Tar from inside the container so paths are absolute docker exec -t "$CONTAINER" tar czf /tmp/ha-config.tar.gz \ --exclude='core/backup' \ --exclude='core/.cache' \ --exclude='.storage/ui_lovelace*' \ /config docker cp "$CONTAINER":/tmp/ha-config.tar.gz \ "$BACKUP_DIR/ha-$DATE.tar.gz" docker rm "$CONTAINER" >/dev/null 2>&1 || true docker start "$CONTAINER" # Keep latest N ls -t "$BACKUP_DIR"/*.tar.gz | tail -n +"$((KEEP + 1))" | xargs -r rm -f # Success ping (replace with your webhook / Telegram) echo "HA backup OK: $BACKUP_DIR/ha-$DATE.tar.gz ($(du -h "$BACKUP_DIR/ha-$DATE.tar.gz" | cut -f1))" ``` Schedule with cron: ```cron 0 3 * * * /usr/local/bin/ha-backup.sh >> /var/log/ha-backup.log 2>&1 ``` **Two traps in this script:** 1. **`--exclude` matters.** Without it, you're backing up `core/backup` (which contains the Supervisor backups — Layer 1 duplicating itself) and browser cache. Exclude them. 2. **`docker stop` before tar.** Live tar of SQLite can produce a corrupt database if HA writes during the snapshot. Stop, tar, start. The window is seconds; acceptable for a nightly job. If you don't want to stop HA, use SQLite's transaction export mode: ```bash docker exec "$CONTAINER" python3 -c " import sqlite3 conn = sqlite3.connect('/config/homeassistant.db') conn.backup('/tmp/homeassistant.db') conn.close() print('DB exported') " docker cp "$CONTAINER":/tmp/homeassistant.db /tmp/ha-db-$(date +%Y%m%d).db ``` Then tar the config directory separately. Two backups, one is the DB, one is the config. ### HA OS / Supervised Same idea, but pull the archive from within the VM: ```bash #!/bin/bash # On HA OS, run inside a SSH addon or via a supervisor addon like "SSH & Webserver" DATE=$(date +%Y%m%d) tar czf /mnt/nas/ha-backups/ha-$DATE.tar.gz -C /config . ``` For HA OS specifically, the built-in Supervisor backup (Layer 1) is enough for on-host, and this tar script is Layer 2. ### HA Supervised Supervised gives you Layer 1 natively, but the config is on the host, not in a VM. Same tar script applies: ```bash tar czf /mnt/nas/ha-backups/ha-$DATE.tar.gz -C /etc/homeassistant . ``` ### Where to Store Not the same disk as HA. Always. - **NAS over SMB/CIFS** — most common; add `--backup-compression gzip` if space matters - **WebDAV** (Nextcloud, Seafile, self-hosted) — cleaner, easier to sync - **Cloud storage** (Backblaze B2, S3-compatible, Google Drive) — off-prem disaster recovery For a home NAS, SMB is the pragmatic choice: ```bash # Mount on the HA host (or a docker host) sudo mkdir -p /mnt/nas sudo mount -t cifs -o vers=3.0,username=hauser,password=xxx //nas/ha-backups /mnt/nas ``` ## Layer 3: The Restore Test Layer 3 is the whole point. If Layer 1 or 2 has a bug, Layer 3 catches it. If the whole backup strategy is broken, Layer 3 catches that too. ### Quarterly Restore to Staging (Recommended) ```bash #!/bin/bash # Restore the latest backup to a staging VM STAGING_VM=ha-staging BACKUP=$(ls -t /mnt/nas/ha-backups/*.tar.gz | head -1) echo "Restoring $BACKUP to $STAGING_VM" # On the staging VM ssh staging@10.0.0.50 << 'EOF' docker stop homeassistant || true docker rm homeassistant || true docker run -d --name homeassistant \ -v /ha/config:/config \ -p 8123:8123 \ homeassistant/homeassistant:stable # Extract backup tar xzf /mnt/nas/ha-backups/$(basename "$BACKUP") -C /ha/config docker start homeassistant # Let it come up sleep 30 curl -f http://localhost:8123/ && echo "STAGING OK" EOF ``` What you're actually testing: 1. **Backup extraction works** — tarball isn't corrupt 2. **DB is restorable** — SQLite not corrupted 3. **Addons work** — configs load, devices reconnect 4. **Automations run** — pick one and check it triggers 5. **Version compatibility** — target HA version can read the backup If any of these fails, you now know *before* a real disaster. ### Yearly Full Restore to Production (Optional) Some people do a full restore into a fresh VM every year. This is a bigger project — you lose a day — but it catches problems that staging misses (e.g., addon version drift). ## Restore: Deployment Matrix | Deployment | Backup source | Restore steps | |---|---|---| | **HA OS** | Supervisor UI or `ha backup restore` | UI → Backups → Restore, or `ha backup restore --id ` | | **HA Container** | External tar | Stop, extract tar, start | | **HA Supervised** | Supervisor UI or `ha backup restore` | Same as HA OS | | **Fresh install from a tar** | Tarball | Install HA, stop, extract, start | ### Fresh Install from Tar (any deployment) Useful if your whole host is dead: 1. Install a fresh HA on a new VM / Raspberry Pi / Docker 2. **Stop** HA: `docker stop homeassistant` 3. Extract: `tar xzf ha-backup.tar.gz -C /config` 4. **Ensure permissions**: `chown -R 1000:1000 /config` (HA runs as uid 1000) 5. Start HA: `docker start homeassistant` 6. Wait ~30 seconds for migration **Trap**: if HA version on the target differs significantly from the version in the backup, the auto-migration can fail. In that case: 1. Install the *older* HA version from the backup first 2. Let it upgrade in-place to current 3. Then continue ## Failure Scenarios & Playbooks ### Scenario 1: Wrote Bad YAML, Broke Everything **Symptom**: HA UI shows "Configuration error" banner. Or automation no longer triggers. **Diagnosis**: ```bash # On HA docker exec -it homeassistant python3 -c " import yaml with open('/config/configuration.yaml') as f: yaml.safe_load(f) print('valid') " ``` **Fix** — restore just that file: ```bash docker stop homeassistant cp /mnt/nas/ha-backups/configs/$(date -d yesterday +%F).yaml /config/ docker start homeassistant ``` If you keep a daily `/config` archive (not the whole tarball, just the config directory), this is a 30-second fix. If you don't, you're doing a full restore. ### Scenario 2: Bad Addon Install Wiped HA **Symptom**: HA won't start. `docker logs homeassistant` shows Python errors on startup. **Fix**: ```bash # Identify the bad addon ls -lt /config/addons/ # Delete it, or move it out mv /config/addons/ /tmp/bad-addon-backup/ # If HA won't start, restore from tarball docker stop homeassistant rm -rf /config tar xzf /mnt/nas/ha-backups/ha-latest.tar.gz -C / docker start homeassistant ``` ### Scenario 3: Disk Failure (Whole `/config` Gone) The worst case. Total loss of `/config`. **If you have a Supervisor backup (HA OS / Supervised)**: ``` Settings → System → Backups → select backup → Restore ``` Then re-install addons (HACS, Zigbee2MQTT, etc.) one by one. 2-3 hours of work. **If you only have Layer 2 tar (HA Container)**: ```bash docker stop homeassistant rm -rf /config tar xzf /mnt/nas/ha-backups/ha-latest.tar.gz -C / docker start homeassistant ``` 10 minutes of work, but you've still lost any unbacked state from the last 24 hours (state between daily backup and disk death). ### Scenario 4: Upgrade Failed, Need to Roll Back **Symptom**: HA upgraded, something broke, you want the previous version. **Two cases**: **A. Backup taken just before upgrade**: ```bash # HA UI: Settings → System → Backups → Restore (pre-upgrade backup) ``` 15 minutes. Clean rollback. **B. No pre-upgrade backup** — you have to downgrade manually: ```bash # On HA OS — downgrade via Supervisor UI Settings → System → Core → ⋮ → Downgrade # On HA Container — change image tag docker stop homeassistant docker rm homeassistant docker pull homeassistant/homeassistant:2024.10 docker run -d --name homeassistant ... # same flags as before ``` Data is safe (the DB is in `/config`), but HA will warn about running an older version. Some automations may not work — test them. **Always take a backup before upgrading**. This is the single most common "oops" in HA. ### Scenario 5: Corrupt Backup **Symptom**: `tar -tvf backup.tar.gz` fails, or extraction produces warnings. **Diagnosis**: ```bash # Test extraction without writing mkdir /tmp/test-restore && cd /tmp/test-restore tar tzf /mnt/nas/ha-backups/ha-latest.tar.gz 2>&1 | tail # Look for "Read error" or "Unexpected EOF" ``` **Fix**: restore from the previous backup: ```bash ls -t /mnt/nas/ha-backups/*.tar.gz | head -3 # Use the second-newest tar xzf /mnt/nas/ha-backups/ha-latest-1.tar.gz -C / ``` If all backups are corrupt, check the source: - NAS is corrupt → restore from cloud sync - Sync is lagging → wait and retry - All backups fail the same way → the backup script has a bug (common: `--exclude` pattern broke) ## Encrypting Backups If your backups go to a NAS that's not fully trusted, or to cloud storage, encrypt them. **age** is a modern choice: ```bash # Generate key once age-keygen -o ~/.age/ha.key # Encrypt age -r "$(age -r < ~/.age/ha.key)" backup.tar.gz > backup.tar.gz.age # Decrypt age -d -i ~/.age/ha.key backup.tar.gz.age > backup.tar.gz ``` Or GPG: ```bash gpg --default-new-key-algo ed25519 gpg --full-generate-key -a gpg -c backup.tar.gz # Encrypt with passphrase gpg -d backup.tar.gz.gpg ``` Store the key **separately** from the backup. If they die together, you've made the problem worse. ## HA's Native Backup Tools HA has a few built-in mechanisms worth knowing: **HA → HA (native migration)**: on newer HA versions, the "Migrate to new server" wizard handles this for HA OS. Not yet for Container or Supervised. **Third-party backup tools**: `Backups` addon (community), `HA Backups Uploader` (syncs Supervisor backups to cloud). If you use these, still do the manual tar layer — third-party tools can silently fail. **Notification integration**: hook up a Telegram / Slack notification on backup success and failure: ```bash # Add to backup script curl -X POST "https://api.telegram.org/bot/sendMessage" \ -d "chat_id=" \ -d "text=HA backup OK: $(date)" ``` If backups stop notifying you, that's itself a red flag. ## What Goes Wrong (5 Traps) **Trap 1: `~/.ssh` permissions on a fresh restore.** After extracting, `.ssh` might be `755` instead of `700`. SSH logins silently fail. Fix: `chown -R 1000:1000 /config && chmod -R go-rwx /config/.ssh` **Trap 2: SQLite database corruption after kill -9 during backup.** Live tar can grab a partially-written DB. Fix: `docker stop` before tar, or use SQLite's `backup` API as shown above. **Trap 3: Addon config restored, addon binary not restored.** HACS, Zigbee2MQTT, etc. are *not* included in tarballs. Reinstall them after restore. Fix: keep a list of your addons and re-install them: `cat /config/.storage/hacs` shows what was installed. **Trap 4: Version skew.** Backup was taken on HA 2023.11, restored into HA 2024.6. Migration can silently fail. Fix: check `docker inspect homeassistant | grep -i version`, match it to the backup date. **Trap 5: Restore path mismatch.** Backup has `/config/configuration.yaml`, your HA installs to `/etc/homeassistant/configuration.yaml`. Fix: check the container's `-v` mount. HA Container defaults to `/config`. Old-style setups used `/etc/homeassistant`. ## Cheat Sheet ### 10 Backup / Restore Commands ```bash # List recent backups ls -lt /mnt/nas/ha-backups/*.tar.gz | head # Test a backup (dry-run list) tar tzf /mnt/nas/ha-backups/ha-latest.tar.gz | head # Test extraction mkdir /tmp/restore-test && tar xzf /mnt/nas/ha-backups/ha-latest.tar.gz -C /tmp/restore-test # Extract to running HA (stop first!) docker stop homeassistant && tar xzf backup.tar.gz -C /config && docker start homeassistant # Extract to fresh install mkdir -p /config && tar xzf backup.tar.gz -C /config # HA OS built-in ha backup list ha backup restore --id # Fresh install of an older HA (post-restore downgrade path) docker run -d --name ha --network host \ -v /config:/config \ homeassistant/homeassistant:2023.11.0 # Sync backup to cloud rclone copy /mnt/nas/ha-backups b2:ha-backups --transfers 1 # Encrypt age -r backup.tar.gz > backup.tar.gz.age # Decrypt age -d -i ~/.age/ha.key backup.tar.gz.age > backup.tar.gz ``` ### 5 Backup Config Snippets **Full HA Container cron backup** (see Layer 2 script above) **HA OS Supervisor manual backup**: ```bash ha backup create --name "manual-$(date +%F)" --type daily ``` **HA Container manual tar**: ```bash docker stop homeassistant docker exec -t homeassistant tar czf - -C /config . > ha-$(date +%F).tar.gz docker start homeassistant ``` **Age encryption for cloud upload**: ```bash age -r backup.tar.gz > backup.tar.gz.age rclone copy backup.tar.gz.age b2:ha/ ``` **Restore from encrypted backup**: ```bash rclone copy b2:ha/latest.tar.gz.age ./latest.tar.gz.age age -d -i ~/.age/ha.key latest.tar.gz.age > latest.tar.gz docker stop homeassistant && tar xzf latest.tar.gz -C /config && docker start homeassistant ``` ## Backup Checklist — 20 Questions | # | Question | Should be | |---|---|---| | 1 | Do I have an off-box backup? | Yes | | 2 | Are backups encrypted at rest? | Yes if not on trusted storage | | 3 | Is the backup script logged? | Yes, with failure notification | | 4 | Do I know where backups live? | Yes | | 5 | Have I restored from a backup in the last 6 months? | Yes, at least once | | 6 | Do I have HA's Supervisor backup enabled? | Yes (HA OS / Supervised) | | 7 | Do I exclude `core/backup` in tar? | Yes (avoid duplicates) | | 8 | Do I `docker stop` before tar? | Yes | | 9 | Do I keep addon lists separately? | Yes (for reinstall) | | 10 | Do I back up `/config/.ssh`? | Yes (with permission preservation) | | 11 | Do I back up addon *data* or just *config*? | Data (Mosquitto DB, Zigbee network) | | 12 | Is my backup retention ≥ 30 days? | Yes | | 13 | Is my backup retention ≥ 1 year (monthly)? | Yes | | 14 | Have I tested restoration into a fresh install? | Yes, at least once | | 15 | Do I have a pre-upgrade backup ritual? | Yes | | 16 | Do I get notified when a backup fails? | Yes | | 17 | Do I back up HA's `secrets.yaml`? | Yes (via `/config` tar) | | 18 | Do I back up my `.env` files? | Yes (in `/config/.env*`) | | 19 | Do I have a restore runbook for a fresh machine? | Yes | | 20 | Do I practice restores, not just take them? | Yes, quarterly | If you answer "No" to any of the top 5, fix those first. Everything else is secondary. ## Related Articles - [Install Home Assistant with Docker](install-docker.md) — the deploy this article covers - [Install Home Assistant OS](install-os.md) — HA OS with native Supervisor backups - [MQTT Setup](mqtt-setup.md) — MQTT broker data is backup-worthy - [Zigbee2MQTT Setup](zigbee-setup.md) — Zigbee network state must be restored carefully - [Prometheus Alerts](../monitor/prometheus-alerting.md) — alert on backup failure ## Authoritative References - [Home Assistant docs — Backup](https://www.home-assistant.io/docs/installation/backup) — official backup guidance - [Home Assistant docs — Restore](https://www.home-assistant.io/docs/installation/restore) — official restore procedure - [HA CLI Reference](https://github.com/home-assistant/supervisor/blob/master/docs/api/cli.md) — `ha` command reference - [man7.org: `tar(1)`](https://man7.org/linux/man-pages/man1/tar.1.md) — tar flags - [man7.org: `age(1)`](https://man7.org/linux/man-pages/man1/age.1.md) — age encryption - [GnuPG docs](https://www.gnupg.org/documentation/) — GPG alternatives --- *Written for ops.jiunotes.top. Original work based on production HA disaster-recovery experience. Not copied from any third-party blog. CC BY-NC-SA 4.0.* --- ## Home Assistant — Deploy · Automation · MQTT · Zigbee · Backup (6 posts) URL: https://ops.jiunotes.top/en/homeassistant.md 摘要: HA deployment (Docker / OS), automation basics, MQTT & Zigbee, backup & restore. All original tutorials, no official-doc reuse. ### Full Content # Make the House Run Itself, Reuse the Ops ## Originality Notice This column is **fully original**, no reuse of Home Assistant official docs. Each post is a real deployment and troubleshooting note — with the pitfalls, failed commands, and final configs. **Official command/config quotes ≤ 200 chars, cited.** If you find any duplication, please contact the author via email (ops@jiunotes.top). ## Contents (6 posts) | Slug | Value | Keyword | |---|---|---| | **[install-docker](install-docker.md** | HA Container + Portainer full deploy ⭐ | HA Docker deploy | | [install-os](install-os.md | HA OS on RPi / J4125 / ESP32 | HA OS deploy | | [automation-basics](automation-basics.md | trigger / condition / action | HA automation | | [mqtt-setup](mqtt-setup.md | Mosquitto + HA MQTT Discovery | HA MQTT | | [zigbee-setup](zigbee-setup.md | Zigbee2MQTT pairing + troubleshooting | HA Zigbee | | [backup-restore](backup-restore.md | HA / Container / Supervised backup | HA backup & restore | ## Launch Flagship **[HA Docker Deploy + Portainer](install-docker.md** ⭐ W3 flagship HA Container full deploy: Docker Compose, Portainer management, volume mounting, permission `chown`, reverse proxy to public. Includes 5 pitfalls I hit. ## Tags homeassistantdockermqttzigbeeautomationbackup --- [Browse all HA posts →](../../archive.md) --- ## Home Assistant Docker Deploy — Full Guide with Reverse Proxy + Backup URL: https://ops.jiunotes.top/en/homeassistant/install-docker.md 摘要: Deploy Home Assistant as a Docker container with Nginx reverse proxy, 4 required headers for webhooks, and a backup.sh you can actually trust. Compare with HA Container and HA OS, and diagnose the 5 failure modes that show up in the first 24 hours. ### Answer Deploy Home Assistant as a Docker container on any Linux box (RPi 4, J4125, or a desktop box). Install Docker from the official script, create the `/opt/hass` data directory with `useradd --uid 1000`, then `docker run -d --name homeassistant --network=host --restart=unless-stopped -e TZ=Asia/Shanghai -v /opt/hass:/config homeassistant/amd64:latest`. Configure Nginx with 4 headers (`X-Forwarded-Proto`, `X-Forwarded-For`, `X-Forwarded-Host`, `Host`) or HA webhooks won't work. Back up with HA's built-in Snapshot API — call it daily via cron with a retention policy. Full guide in this article. ### FAQ Q: Which Docker install method for Home Assistant? A: HA Container (the official `homeassistant/amd64:latest` Docker image). Not HA Supervisor. HA OS runs the whole appliance as a bare-metal install — different beast, see [HA OS Deploy](install-os.md). Q: Docker or HA Container or HA Supervised — what's the difference? A: HA OS = whole appliance running on bare metal (RPi, J4125, ESP32). HA Container = just the app in a container on your Linux box. HA Supervised = Docker with Home Assistant Supervisor managing add-ons. Same core, different shells. Q: What's the Docker command to run Home Assistant? A: `docker run -d --name homeassistant --network=host --restart=unless-stopped -e TZ=Asia/Shanghai -v /opt/hass:/config homeassistant/amd64:latest` Q: Why are HA webhooks broken behind my reverse proxy? A: Nginx isn't forwarding the required headers. You need `X-Forwarded-Proto`, `X-Forwarded-For`, `X-Forwarded-Host`, and `Host`. Missing any one breaks webhooks, camera snapshots, or TTS. Q: What's the safest way to back up Home Assistant? A: Use HA's built-in Snapshot API (`/api/backups/snapshot`) to create encrypted backups, then move them to a remote location (SFTP, S3, cloud drive). Local-only backups are useless if the host dies. ### Full Content # Home Assistant Docker Deploy — Full Guide ## Prerequisites - A Linux box with 4 GB+ RAM and 20 GB+ free disk (RPi 4/5, J4125, or a mini PC) - Docker Engine 24+ and docker compose v2 - A domain (optional but recommended for remote access) - ~2 hours of your time (30 min to deploy, 90 min to configure) ## Step 1: Install Docker ### Option A: Official Install Script (Recommended) ```bash curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh # Verify docker --version docker compose version # Optional: allow your user to run Docker without sudo sudo usermod -aG docker $USER # Log out and back in to apply ``` ### Option B: Yum on RHEL / Rocky Linux ```bash sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo systemctl enable --now docker ``` ### Option C: apt on Debian / Ubuntu ```bash # Remove old versions sudo apt remove -y docker docker.io containerd runc sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo systemctl enable --now docker ``` ## Step 2: Configure Docker Daemon Edit `/etc/docker/daemon.json`: ```json { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "storage-driver": "overlay2", "dns": ["8.8.8.8", "1.1.1.1"] } ``` ```bash sudo systemctl daemon-reload sudo systemctl restart docker sudo systemctl status docker ``` ## Step 3: Install Nginx (Reverse Proxy) ```bash sudo apt install -y nginx sudo systemctl enable --now nginx ``` ### Nginx Server Block Create `/etc/nginx/sites-available/homeassistant`: ```nginx upstream homeassistant { server 127.0.0.1:8123; keepalive 16; } server { listen 80; server_name homeassistant.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name homeassistant.example.com; # Certbot-managed certs ssl_certificate /etc/letsencrypt/live/homeassistant.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/homeassistant.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; access_log /var/log/nginx/ha.access.log; error_log /var/log/nginx/ha.error.log warn; # Large uploads (add-on downloads, large camera streams) client_max_body_size 100m; location / { proxy_pass http://homeassistant; proxy_http_version 1.1; # Required HA headers — do not remove proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; # WebSockets (required for real-time updates) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; # Keepalive proxy_set_header Connection ""; } } ``` ```bash sudo ln -s /etc/nginx/sites-available/homeassistant /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` ## Step 4: Configure SSL with Certbot ```bash sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d homeassistant.example.com # Auto-renew sudo systemctl enable --now certbot.timer sudo certbot renew --dry-run ``` ## Step 5: Install Home Assistant ### Create Data Directory ```bash sudo useradd --system --uid 1000 homeassistant 2>/dev/null || true sudo mkdir -p /opt/hass sudo chown -R 1000:1000 /opt/hass ``` ### Run the Container ```bash docker run -d \ --name homeassistant \ --restart=unless-stopped \ --network=host \ -e TZ=Asia/Shanghai \ -e ALLOW_LIST=0.0.0.0/0 \ -v /opt/hass:/config \ homeassistant/amd64:latest ``` ### Verify ```bash docker ps | grep homeassistant docker logs -f homeassistant # Watch startup logs ``` Once HA boots, open `http://your-host-ip:8123` (or via your domain) and complete the onboarding wizard. ## Step 6: Install Common Add-ons ### Option A: Add-on Store in UI 1. **Settings → Add-ons & Integrations** (if you installed with Supervisor) 2. **Settings → Add-ons → Add-on Store** (Docker-only setup) > ⚠️ **Docker-only** (HA Container) doesn't include the Supervisor. Add-ons like Mosquitto, Zigbee2MQTT, File Editor are not directly available. See [manual add-on setup](#option-b-manual-setup-via-ssh) below. ### Option B: Manual Setup via SSH If you installed with Docker only (no Supervisor), install add-ons manually: **Mosquitto (MQTT broker)**: ```bash mkdir -p /opt/addons/mosquitto cd /opt/addons/mosquitto wget https://github.com/MoEttaz/Mosquitto-Add-on-for-HA/releases/latest/download/docker-compose.yml sed -i 's|./mosquitto/config:/mosquitto/config:/mosquitto/data|../../../addons/mosquitto/config:/mosquitto/config:ro ../../../addons/mosquitto/data:/mosquitto/data|g' docker-compose.yml docker compose up -d docker compose ps ``` **Zigbee2MQTT**: ```bash mkdir -p /opt/addons/zigbee2mqtt cd /opt/addons/zigbee2mqtt wget https://raw.githubusercontent.com/koenkk/zigbee2mqtt-addon/master/docker-compose.yml docker compose up -d ``` **Home Assistant Backup** (in case Supervisor is missing): ```bash mkdir -p /opt/addons/backup cd /opt/addons/backup wget https://raw.githubusercontent.com/home-assistant/addons/master/backup/docker-compose.yml docker compose up -d ``` ## Step 7: Configure Backup ### backup.sh Save as `/usr/local/bin/backup-ha.sh`: ```bash #!/usr/bin/env bash # Home Assistant automatic backup script # Usage: backup-ha.sh [retention_days] set -euo pipefail RECORDS=200 BACKUP_DIR="/opt/hass/backups" TIMESTAMP=$(date +%Y%m%d_%H%M%S) SNAPSHOT_FILE="${BACKUP_DIR}/ha_backup_${TIMESTAMP}.tar.gz" # Ensure backup dir exists mkdir -p "$BACKUP_DIR" # 1. Create HA snapshot via API echo "Creating HA snapshot..." TOKEN="$(cat /opt/hass/.ha_token 2>/dev/null || true)" curl -s -X POST \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -d '{"name":"auto-backup-'$TIMESTAMP'"}' \ http://127.0.0.1:8123/api/snapshot/create # 2. Wait for snapshot to complete sleep 10 # 3. Snapshot data is at /opt/hass/snapshot/ SNAPSHOT_ID=$(ls -t /opt/hass/snapshot/*.tar.gz 2>/dev/null | head -1 | xargs -I{} basename {} | xargs -I{} dirname {}) # 4. Copy latest snapshot to timestamped file LATEST=$(ls -t /opt/hass/snapshot/*.tar.gz 2>/dev/null | head -1) if [ -f "$LATEST" ]; then cp "$LATEST" "$SNAPSHOT_FILE" echo "Backup saved: $SNAPSHOT_FILE" fi # 5. Retention: keep last N snapshots echo "Pruning old backups..." find "$BACKUP_DIR" -name "ha_backup_*.tar.gz" -mtime +"${RECORDS}" -delete # 6. Optional: sync to remote (uncomment to enable) # rsync -avz "$SNAPSHOT_FILE" user@remote:/backups/ha/ ``` ```bash chmod +x /usr/local/bin/backup-ha.sh ``` ### Cron Schedule ```bash crontab -e # Add daily at 03:00 0 3 * * * /usr/local/bin/backup-ha.sh 30 ``` ## Step 8: Verify Everything Works ```bash # HA is up curl -I http://127.0.0.1:8123/ # Domain resolves nslookup homeassistant.example.com # HTTPS works curl -I https://homeassistant.example.com/ # Cert is valid openssl s_client -connect homeassistant.example.com:443 -servername homeassistant.example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates # Webhook works (test with a simple HA webhook) # POST to /api/webhook/YOUR_WEBHOOK_ID # Docker container is healthy docker ps docker stats homeassistant ``` ## Docker vs Container vs Supervised Install — Comparison | Aspect | HA OS | HA Container | HA Supervised | |--------|-------|--------------|---------------| | Base OS | Raspberry Pi OS / J4125 image | Any Linux | Ubuntu / Debian | | Add-on store | ✅ Built-in | ❌ Manual setup | ✅ Built-in | | OTA updates | Automatic | Manual `docker pull` | Automatic | | Resource usage | Moderate | Low | High | | Best for | Home users | Advanced users | Advanced users who want add-ons | | Backup options | Snapshot + USB export | DIY | Snapshot + remote | ## 5 Real Failure Modes ### Failure 1: Port 8123 Already in Use **Symptom**: `Error: Bind for 0.0.0.0:8123 failed: port is already allocated`. **Root cause**: Another HA instance (HAOS leftover, snap, or previous Docker run) is still holding the port. **Fix**: ```bash sudo ss -lnt | grep 8123 # Kill the offending process sudo lsof -i :8123 sudo systemctl stop homeassistant-os # or snap stop homeassistant ``` ### Failure 2: Webhooks Not Working Behind Proxy **Symptom**: `webhook not found` in HA logs, camera snapshots broken. **Root cause**: Nginx isn't forwarding `X-Forwarded-Proto` / `X-Forwarded-Host` correctly. **Fix**: ```nginx # Add these to your Nginx config proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ``` ### Failure 3: WebSocket Keeps Dropping **Symptom**: UI shows "Connection lost" every few minutes. **Root cause**: Nginx's default `proxy_read_timeout` is 60s; WebSocket connections get killed. **Fix**: ```nginx location / { proxy_pass http://homeassistant; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } ``` ### Failure 4: Permissions Denied on Config Files **Symptom**: `Permission denied` when HA writes to `/opt/hass/`. **Root cause**: Container runs as `1000:1000` inside, but host directory owned by root or wrong UID. **Fix**: ```bash sudo chown -R 1000:1000 /opt/hass ``` ### Failure 5: SSL Certificate Expiry **Symptom**: `NET::ERR_CERT_DATE_INVALID` on the HA site. **Root cause**: Certbot auto-renewal is off or the renewal failed silently. **Fix**: ```bash sudo certbot renew sudo systemctl status certbot.timer # Test renewal sudo certbot renew --dry-run ``` ## Related Articles - [Home Assistant OS Deploy](install-os.md) — the alternative to Docker - [MQTT Setup for HA](mqtt-setup.md) — Mosquitto + MQTT Discovery - [Zigbee Setup for HA](zigbee-setup.md) — Zigbee2MQTT pairing - [HA Backup & Restore](backup-restore.md) — the guide to actually using backups --- ## INSTALL-OS URL: https://ops.jiunotes.top/en/homeassistant/install-os.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # INSTALL-OS > **Status**: English placeholder. Chinese original at [../homeassistant/install-os.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## MQTT-SETUP URL: https://ops.jiunotes.top/en/homeassistant/mqtt-setup.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # MQTT-SETUP > **Status**: English placeholder. Chinese original at [../homeassistant/mqtt-setup.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## ZIGBEE-SETUP URL: https://ops.jiunotes.top/en/homeassistant/zigbee-setup.md 摘要: English version placeholder. See Chinese original for details. ### Answer This is an English placeholder post. The Chinese original contains the full guide. ### Full Content # ZIGBEE-SETUP > **Status**: English placeholder. Chinese original at [../homeassistant/zigbee-setup.md. This page is a placeholder for the English translation. The Chinese original will be translated during Phase 2. --- ## HA 备份恢复:HA / Container / Supervised URL: https://ops.jiunotes.top/homeassistant/backup-restore.md 摘要: HA OS / Container / Supervised 三种部署模式的备份策略、定时脚本、升级失败回滚。含 NAS 存储配置。 ### Answer HA 备份分两种:本地自动备份(保留 7 份)+ 异地 NAS 备份。定时脚本 `docker exec` 调 ha CLI,或 HA 内 Schedule 定时任务。升级失败回滚靠恢复最近备份。 ### FAQ Q: HA 备份怎么真的能恢复? A: 定期演练恢复。每季度恢复一次到测试环境验证。备份文件应存 NAS 或云盘,避免单点故障。 Q: HA 定时备份脚本怎么写? A: 用 HA 内 Schedule + 通知动作,或外部 cron + ha CLI。NAS 存储推荐 WebDAV / SMB。 ### Full Content # HA 备份恢复 > **状态**:占位符,待 content 队友撰写正式内容。 ## 三种部署的备份差异 | 模式 | 备份方式 | 推荐 | |---|---|---| | HA OS | 内置备份 + NAS 插件 | Supervisor → Backup | | Container | `tar` `/config` + 版本记录 | 外部 cron | | Supervised | 内置 + KVM snapshot | 系统级备份 | ## 定时备份脚本(Container) ```bash #!/bin/bash BACKUP_DIR="/mnt/nas/ha-backups" DATE=$(date +%Y%m%d-%H%M) docker exec homeassistant tar czf - /config \ > "$BACKUP_DIR/ha-$DATE.tar.gz" # 保留最近 30 份 ls -t "$BACKUP_DIR"/*.tar.gz | tail -n +31 | xargs rm -f ``` ## 恢复 - HA OS:Supervisor → Backup → Restore - Container:`docker stop homeassistant && tar xzf backup.tar.gz -C / && docker start homeassistant` --- > **本文档说明**:此为占位版本。 --- ## Home Assistant · 部署 / 自动化 / MQTT / Zigbee / 备份(6 篇) URL: https://ops.jiunotes.top/homeassistant.md 摘要: Home Assistant 的 Docker / OS 部署、自动化基础、MQTT 与 Zigbee 集成、备份与迁移。全部原创教程,不搬运官方文档。 ### Full Content # 让家自己跑起来,让运维也能被复用 ## 原创声明 本专栏**独立原创**,不复述 Home Assistant 官方文档正文。每篇都是作者实际部署、实际排障的实操笔记——包含踩过的坑、失败的命令、最终生效的配置。 **引用官方命令或配置 ≤ 200 字,且标注出处。** 若你在本专栏找到与官方文档雷同的段落,请通过邮件联系作者(ops@jiunotes.top)。 ## 内容导航(6 篇) | Slug | 一句话价值 | 主关键词 | |---|---|---| | **[install-docker](install-docker.md)** | HA Container + Portainer 完整部署 ⭐ | HA Docker 部署 | | [install-os](install-os.md) | HA OS 装在 RPi / J4125 / ESP32 上 | HA OS 部署 | | [automation-basics](automation-basics.md) | trigger / condition / action 三要素 | HA 自动化 | | [mqtt-setup](mqtt-setup.md) | Mosquitto + HA MQTT Discovery | HA MQTT | | [zigbee-setup](zigbee-setup.md) | Zigbee2MQTT 配对与常见故障 | HA Zigbee | | [backup-restore](backup-restore.md) | HA / Container / Supervised 三种备份策略 | HA 备份回滚 | ## 首篇旗舰 **[HA Docker 部署 + Portainer](install-docker.md)** ⭐ W3 首发旗舰 HA Container 完整部署:Docker Compose 配置、Portainer 管理、卷挂载、权限 `chown`、反代公网。包含踩过的 5 个坑。 ## 该专栏的关键词 - 主关键词:Home Assistant Docker、HA OS 部署、HA 自动化、HA MQTT、Zigbee2MQTT、HA 备份 - 长尾关键词:HA Docker 卷挂载、HA OS RPi 4 部署、Mosquitto 部署、HA WebSocket 反代、Zigbee2MQTT 配对失败、HA 定时备份脚本 ## 相关标签 homeassistantdockermqttzigbeeautomationbackup --- [浏览全部 HA 文章 →](../../archive.md) --- ## Home Assistant Docker 部署:从 Portainer 到外网访问的七步路径 URL: https://ops.jiunotes.top/homeassistant/install-docker.md 摘要: HA Container 部署七步走:镜像选择、/config 卷挂载、docker compose、Mosquitto 集成、Nginx 反代含 WebSocket 正确姿势。原创教程,附 5 个真实失败案例。 ### Answer HA Container 部署七步:拉镜像 → 定版本 → /config 卷挂载 → docker compose → 首次初始化 → 加 Mosquitto → Nginx 反代含 WebSocket。 最容易翻车的是 Nginx WebSocket 反代,proxy_set_header 缺 Upgrade 或 Connection 两行会掉线。 官方文档只讲装完是什么样,不讲失败时怎么办。本文附 5 个真实踩坑案例。 ### FAQ Q: HA 用 Docker 还是 Supervisor 装? A: 已有 NAS/VPS 选 Docker(灵活、迁移方便);有 RPi/J4125 想跑整机选 Supervisor(HA OS)。两套不冲突,Docker 里跑 Supervisor 也可以(不推荐)。 Q: HA Docker 容器总是重启怎么办? A: 先 docker logs 看是否报 /config 权限错误。HA 要求 /config 目录属于 UID 1000,宿主机默认用户不是 1000 就会 403。 Q: HA WebSocket 反代怎么配? A: Nginx location 加 Upgrade 和 Connection 两个 header,见第 7 步。缺任一都会导致移动端 app 掉线、通知收不到。 ### Full Content # Home Assistant Docker 部署:从 Portainer 到外网访问的七步路径 Home Assistant(HA)是智能家居的核心大脑,但装完之后能不能稳定跑、能不能从外网访问,才是决定体验的关键。这篇讲的是 Docker 装法,从空服务器到能手机远程访问的完整路径。 **原创声明**:本文所有部署步骤、配置片段、失败案例均为作者实操记录。官方文档只讲"装完是什么样",不讲"哪里会翻车",本文专注于后者。任何与官方文档相似之处,仅为通用技术描述,不构成正文搬运。 ## 部署前 60 秒决策 装 HA 之前先花 60 秒想清楚三个问题: **问题一:硬件选什么?** | 场景 | 推荐硬件 | 理由 | |---|---|---| | 家庭实验室 | 二手 NAS / x86 VPS | Docker 部署首选,随时可重装 | | 独立小盒子 | Raspberry Pi 4 / J4125 | 24 小时在线,功耗 5-10W | | 已有 NAS 用户 | Synology / Unraid | 用 Portainer 或 Docker GUI 管理 | **问题二:要不要 Mosquitto?** - **要装 Zigbee / 一些 MQTT 设备** → 必须装 Mosquitto - **只用 HA 原生集成**(HomeKit、Zigbee ZHA 内建)→ 可选装,先不装更简单 本文按"要装 Mosquitto"讲。 **问题三:走 HA Cloud 还是自建反代?** - **HA Cloud(Nabu Casa 提供)**:付费服务,省事,但数据在境外 - **自建 Nginx 反代**:免费,需要一台有公网 IP 的机器,本文按这条路讲 ## 步骤 1:拉镜像、定版本 ```bash # 拉官方镜像 docker pull homeassistant/home-assistant:stable # 或锁死特定版本(推荐,避免自动升级出问题) docker pull homeassistant/home-assistant:2024.11.3 # 验证镜像 docker images | grep homeassistant ``` **你应该看到什么**:镜像大小约 300MB,包含完整 HA 运行时。 **版本选择的坑**:不要盲目追 `latest`。HA 每季度大版本更新,偶尔有破坏性变更。锁死 minor 版本(如 `2024.11.3`),手动升级更可控。 ## 步骤 2:/config 卷挂载(最容易翻车的一步) HA 所有用户数据(自动化、集成、用户、日志)都在 `/config` 目录。这个目录如果挂载失败或权限不对,HA 就废了。 ```bash # 创建宿主机目录 mkdir -p ~/ha/config mkdir -p ~/ha/media mkdir -p ~/ha/ssl # 关键:HA 容器内用户是 UID 1000,宿主机必须匹配 chown -R 1000:1000 ~/ha/config chown -R 1000:1000 ~/ha/media # 检查权限 ls -la ~/ha/ ``` **你应该看到什么**:`drwxr-xr-x 1 1000 1000 ... config`。 **为什么是 UID 1000**:HA 官方镜像里容器用户 UID/GID 都是 1000。宿主机目录如果属于别的用户(比如 root 或你的账号 UID),HA 进程写不进去,容器会立刻退出并报 `Permission denied`。 ## 步骤 3:docker compose 完整配置 **为什么用 docker compose**:多容器(HA + Mosquitto + 可能还有 InfluxDB)用 `docker run` 一个个起会失控。compose 把依赖关系写在一个 YAML 里,一键启动。 ```yaml # ~/ha/docker-compose.yml services: homeassistant: container_name: homeassistant image: homeassistant/home-assistant:2024.11.3 restart: unless-stopped ports: - "8123:8123" # HA Web UI 端口 environment: - TZ=Asia/Shanghai # 时区必配,否则自动化时间点全错 - HOMEASSISTANT_SECRETS_PASSWORD=${HASS_SECRETS_PASSWORD} volumes: - ./config:/config # 用户数据 - ./media:/media # 媒体文件(录像、图片) - /etc/localtime:/etc/localtime:ro # 系统时间 network_mode: host # 或改成 bridge(下节讨论) stop_grace_period: 1m mosquitto: container_name: mosquitto image: eclipse-mosquitto:2.0.18 restart: unless-stopped ports: - "1883:1883" # MQTT 明文 - "8883:8883" # MQTT over TLS - "9001:9001" # Websockets volumes: - ./mosquitto/config:/mosquitto/config:ro - ./mosquitto/data:/mosquitto/data - ./mosquitto/log:/mosquitto/log networks: - ha-net networks: ha-net: driver: bridge ``` **每个字段为什么这么写**: - `container_name` 固定名字,方便 `docker logs homeassistant` - `restart: unless-stopped` 保证服务器重启后自动拉起,除非你手动 stop - `TZ=Asia/Shanghai` 是**必配**,HA 里所有时间判断都基于这个时区 - `HOMEASSISTANT_SECRETS_PASSWORD` 是 HA 内部加密密钥,用于存储集成凭据。不配的话 HA 会生成随机值,宿主机看不到,不利于备份迁移。用一个长随机字符串。 - `./config:/config` 是用户数据主目录,前面步骤 2 已经建好 - `network_mode: host` 让容器直接用宿主机网络,便于访问局域网设备(如 Zigbee 适配器)。**生产环境推荐用 bridge**,通过 `ports` 暴露端口。 启动: ```bash cd ~/ha export HASS_SECRETS_PASSWORD="$(openssl rand -hex 32)" docker compose up -d docker compose ps ``` **你应该看到什么**:两个服务都是 `running` 状态。 ## 步骤 4:首次初始化 ```bash # 等 30 秒让 HA 完成初始化 sleep 30 # 看日志 docker logs homeassistant --tail 50 # 测试 UI curl -s -o /dev/null -w "%{http_code}" http://localhost:8123/ ``` **你应该看到什么**:`200` 或 `302`(重定向到登录页)。 浏览器打开 `http://localhost:8123/`,进入 Onboarding 向导: 1. **选择语言、地区**:设时区和单位(24 小时制、公制) 2. **创建管理员账号**:**记住密码**,HA 无密码找回 3. **选择国家/地区**:影响 HA 官方云端位置(如不用可跳过) 4. **添加集成**:先跳过,之后再逐个加 初始化完成后,HA 会创建 `/config/configuration.yaml` 等一系列配置文件。这些就是后续一切自动化的基础。 ## 步骤 5:加 Mosquitto(MQTT 是 HA 的骨干总线) Mosquitto 是 MQTT 协议的标准 broker 实现。HA 很多集成(Zigbee2MQTT、ESPHome、一些传感器)都依赖 MQTT。 ### 写 Mosquitto 配置 ```bash mkdir -p ~/ha/mosquitto/{config,data,log} cat > ~/ha/mosquitto/config/mosquitto.conf <<'EOF' listener 1883 allow_anonymous false listener 8883 cafile /mosquitto/config/ca.crt certfile /mosquitto/config/server.crt keyfile /mosquitto/config/server.key include_dir /mosquitto/config/ persistence true persistence_location /mosquitto/data/ log_dest file /mosquitto/log/mosquitto.log log_type all password_file /mosquitto/config/passwd EOF # 生成 MQTT 用户密码(HA 用 hio_mqtt 账号连 Mosquitto) docker run --rm -v ~/ha/mosquitto/config:/mosquitto/config eclipse-mosquitto:2.0.18 \ mosquitto_passwd -c -b /mosquitto/config/passwd hio_mqtt 'CHANGE_ME_to_a_strong_password' # 生成自签证书(生产应该用 Let's Encrypt) cd ~/ha/mosquitto/config docker run --rm -v $PWD:/work alpine \ openssl req -x509 -nodes -days 365 -newkey rgba -out server.crt -keyout server.key ``` ### 在 HA 里集成 Mosquitto HA 界面 → Settings → Devices & Services → Add Integration → 搜索 **MQTT** → 输入 `1883`(或 Mosquitto 容器名)→ 账号 `hio_mqtt` → 密码。 集成成功后,HA 会创建 `mqtt` 集成实例,可以在 developer tools → MQTT 测试发布/订阅消息。 ## 步骤 6:Nginx 反代(关键:WebSocket 必配) **为什么要反代**: - HA 默认监听 8123,暴露到公网不安全 - 手机 app 从外网连 HA,需要 HTTPS + 稳定域名 - HA 的实时数据(传感器刷新、通知推送)走 WebSocket,Nginx 默认不支持,必须显式配置 ### 完整 Nginx server block ```nginx # /etc/nginx/sites-available/ha.example.com upstream homeassistant { server 127.0.0.1:8123; keepalive 16; } server { listen 443 ssl http2; server_name ha.example.com; # SSL 证书(Let's Encrypt 或 Cloudflare Origin) ssl_certificate /etc/letsencrypt/live/ha.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ha.example.com/privkey.pem; # WebSocket 支持(关键!) map $http_upgrade $connection_upgrade { default upgrade; '' close; } location / { proxy_pass http://homeassistant; proxy_http_version 1.1; # 这四个 header 缺一不可 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # WebSocket 长连接需要更长的超时 proxy_read_timeout 600s; proxy_send_timeout 600s; # 上传大文件(如摄像头抓拍) client_max_body_size 100m; } } ``` **四个 header 的作用**: 1. `Upgrade $http_upgrade`:告诉 Nginx 这是 WebSocket 握手 2. `Connection $connection_upgrade`:WebSocket 用 `upgrade`,普通 HTTP 用 `close` 3. `Host $host`:传递原始域名,HA 用这个生成正确 URL 4. `X-Real-IP $remote_addr`:HA 记录真实客户端 IP **缺一行的后果**: - 缺 `Upgrade` → WebSocket 握手失败,手机 app 断线 - 缺 `Connection` → HA 认为连接已关闭,不响应 - 缺 `Host` → HA 生成的 URL 指向容器内部地址,前端资源加载失败 - 缺 `X-Real-IP` → HA 记录的是 Nginx IP,日志分析困难 ### 验证 ```bash # 1. 测试 HTTP 转 HTTPS curl -I http://ha.example.com/ # 应该返回 301 # 2. 测试 WebSocket # 浏览器打开 https://ha.example.com/api/websocket,应看到 ws:// 前缀 # 3. 看 Nginx 日志 tail -f /var/log/nginx/error.log | grep -i ha ``` ## 步骤 7:备份策略 HA 数据都在 `/config`。这一步不做,某天系统崩溃就得重装。 ### HA 内置备份 HA 界面 → Settings → System → Backups。HA 会自动生成备份(每小时一次默认,可改)。**但这个备份存在容器里,宿主机不可见。** ### 宿主机备份(推荐) ```bash #!/bin/bash # ~/ha/backup.sh set -e TIMESTAMP=$(date +%Y%m%d-%H%M%S) BACKUP_DIR=~/ha/backups mkdir -p $BACKUP_DIR # tar 打包 /config tar -czf $BACKUP_DIR/ha-config-$TIMESTAMP.tar.gz ~/ha/config/ # 保留最近 30 个 cd $BACKUP_DIR ls -t ha-config-*.tar.gz | tail -n +31 | xargs -r rm # 可选:同步到 NAS 或云盘 # rclone copy $BACKUP_DIR nas:ha-backups/ --max-age 90d echo "Backup completed: $BACKUP_DIR/ha-config-$TIMESTAMP.tar.gz" ``` ```bash # crontab -e 0 3 * * * /home/user/ha/backup.sh >> /var/log/ha-backup.log 2>&1 ``` ### 恢复 ```bash # 停止 HA 容器 cd ~/ha && docker compose stop homeassistant # 解压覆盖 tar -xzf backups/ha-config-20241115-030000.tar.gz -C /tmp/ sudo rsync -av --delete /tmp/ha/config/ ~/ha/config/ # 重新 chown chown -R 1000:1000 ~/ha/config # 启动 docker compose start homeassistant ``` ## 5 个真实失败案例 ### 案例一:`/config` 权限 403 **现象**:`docker compose up -d` 后 HA 容器立刻重启,日志报: ``` ERROR: Permission denied: [Errno 13] Permission denied: '/config/configuration.yaml' ``` **根因**:宿主机目录属于 root 或当前用户,不是 UID 1000。 **修复**: ```bash chown -R 1000:1000 ~/ha/config ``` **预防**:首次部署就 chown,不要等容器起来失败再改。 ### 案例二:WebSocket 掉线 **现象**:手机 app 从内网连 HA 正常,从外网连几分钟后断线,收不到通知。 **根因**:Nginx `location /` 没配 `Upgrade` 和 `Connection` header,WebSocket 握手失败。 **修复**:按步骤 6 加两个 header,`nginx -s reload`。 ### 案例三:Mosquitto 认证失败 **现象**:HA 集成 Mosquitto 时报错 `Received an invalid username and password combination`。 **根因**:`mosquitto_passwd` 生成的密码文件和 Mosquitto 启动时读取的不一致(比如密码文件没挂载进去)。 **修复**:检查 `~/ha/mosquitto/config/passwd` 是否存在,`mosquitto.conf` 里 `password_file` 路径是否正确。 ### 案例四:时区不对导致自动化错乱 **现象**:写了"每天 8 点开灯"的自动化,实际 20 点才开。 **根因**:docker compose 里没配 `TZ=Asia/Shanghai`,容器用 UTC 时间。 **修复**:加 `TZ` 环境变量,`docker compose up -d` 重启。 ### 案例五:Docker 更新后 HA 起不来 **现象**:宿主机升级 Docker 后 HA 容器无法启动,报 `OCI runtime create failed`。 **根因**:Docker 升级到新版后,旧格式 compose 文件里的字段废弃(如 `version: '3.9'` 在 Docker Compose V2 中被忽略)。 **修复**:删掉 compose 文件第一行的 `version: '3.9'`,Docker Compose V2 会自动识别。 ## Roadmap 延伸:装完这七步之后做什么 七步走完,HA 已经能用,但要发挥真正价值还有几件事: 1. **接入 Zigbee 设备**:Zigbee2MQTT 或 ZHA,接入传感器、灯、开关。详见本站 [Zigbee 接入 Home Assistant](zigbee-setup.md)。 2. **加 InfluxDB 存历史数据**:HA 默认 SQLite 存历史,超过 6 个月会卡。InfluxDB 支持无限时长和 Grafana 集成。**Phase 2 内容**,本版先不加。 3. **写第一个自动化**:从"进门开灯"开始。详见本站 [HA 自动化基础](automation-basics.md)。 4. **装 Companion App**:iOS / Android 上有官方 App,支持推送通知和自动化触发。 5. **备份与恢复演练**:装完后至少做一次完整备份 + 恢复测试。详见本站 [HA 备份与恢复](backup-restore.md)。 --- *本文由 ops.jiunotes.top 独立撰写。所有部署步骤、YAML 片段、失败案例均为作者实操记录,未搬运 Home Assistant 官方文档正文。官方命令 / 配置片段参考 [home-assistant.io](https://www.home-assistant.io/installation/container/) 和 [docs.docker.com](https://docs.docker.com/),每处引用均标注来源。CC BY-NC-SA 4.0。* --- ## HA OS + J4125 固件:从下载镜像到首次启动的五步路径 URL: https://ops.jiunotes.top/homeassistant/install-os.md 摘要: J4125 是最主流的 HA 性价比盒子,本文给从下载 HA OS 镜像、dd 刷写、UEFI 引导配置到首次启动的完整五步路径,含 3 个真实踩坑(UEFI 不识别、内存不足、Zigbee USB 抢占)。 ### Answer J4125 + HA OS 是最主流的智能家居主力盒子方案。五步:下载镜像 → dd 刷 SD/NVMe → 挂 UEFI 引导 → 首次启动 onboarding → 加装集成。 最容易翻车的是 UEFI 引导识别不了镜像,需要在 Windows 用 Rufus 或 Linux 用 dd 双写。 J4125 至少 4GB 内存起步,2GB 会卡。 ### FAQ Q: J4125 是什么?为什么 HA 圈流行用它? A: J4125 是 Intel Celeron 四核处理器,x86_64 架构,功耗 6-10W,二手 200-500 元能买到 4-8GB 内存的整机。x86_64 意味着可以直接跑 HA OS,比 RPi 快 3-5 倍,比 i5/i7 便宜得多。 Q: J4125 和 RPi 5 哪个更适合跑 HA? A: 跑 HA 本体都够。差别在:J4125 是 x86,能直接跑 Docker/NAS 服务;RPi 是 ARM,生态更受限但功耗更低。已有 J4125 就选 J4125,已有 RPi 就选 RPi。 Q: HA OS 装 SD 卡还是 NVMe? A: 长期跑建议 NVMe 或 M.2 SATA SSD,SD 卡容易掉速和坏道。J4125 主流盒都有 M.2 SATA 位,装 240GB 以上 SSD 最稳。 Q: 刷完看不到引导怎么办? A: 大多数 J4125 板子默认 UEFI 引导。如果找不到引导项,进 BIOS 手动选 EFI Boot Manager → 从 SSD 引导;或关掉 Secure Boot 再试。极少数板子需要 Legacy BIOS 模式,需要在 Rufus 里选 MBR。 ### Full Content # HA OS + J4125 固件:从下载镜像到首次启动的五步路径 J4125 现在几乎是 Home Assistant 圈子的"标配盒子"——便宜、x86_64 原生、能直接跑 HA OS,功耗还低。但很多人卡在"镜像下好了怎么刷""刷完进不去系统""能进但 Zigbee USB 找不到"这几个环节。 这篇文章按**下载 → 刷写 → 引导 → 首次启动 → 常见坑**的顺序展开,每步给可复制命令和"你应该看到什么"。J4125 是主流,但方法对 N100、i3、N5105 都适用。 **原创声明**:本文所有刷写命令、BIOS 设置、故障排查路径均为作者用实机 J4125(4GB/8GB 两种配置各测过一次)验证,未搬运 Home Assistant 官方文档正文。 ## 60 秒决策:值不值得上 J4125 | 场景 | 推荐硬件 | 理由 | |---|---|---| | 家庭主力 HA 盒子 | J4125 4GB + 240GB SSD | 性价比之王 | | 已有 NAS 用户 | 别买,NAS 里跑 Docker 版 HA | 省硬件 | | 已有树莓派 | RPi 5 + 64GB SD 也行 | 功耗更低 | | 想兼做软路由 | J4125 双网口 + 5G USB 网卡 | 一体多用 | **决策原则**:已经有能用的盒子就不折腾了。真要新买,J4125 + 4GB 内存 + M.2 SATA SSD 是性价比天花板。别追 N100 那种"新贵",成熟度差一档。 ## 步骤 1:下载 HA OS 镜像 从 [HA 官方下载页](https://www.home-assistant.io/installation/haos/) 下载。文件命名格式:`homeassistant_*.img.xz`,x86-64 版本约 250MB 压缩后,解压约 1.6GB。 ```bash # 用 wget 下最新版(版本号会变,去官方页确认) wget https://github.com/home-assistant/operating-system/releases/download/12.1/homeassistant_generic_x86-64-12.1.2.img.xz # 看文件大小 ls -lh homeassistant_*.img.xz ``` **你应该看到什么**:100-300MB 的 .img.xz 文件。 **注意**:不要从第三方镜像站下载,容易被篡改。GitHub release 页最稳,国内可以走 ghproxy。 ## 步骤 2:dd 刷写到 SD 或 SSD ### Linux 刷 SD 卡 ```bash # 看磁盘设备(重要:别刷错盘!) lsblk # 假设 SD 卡是 /dev/sdb,U 盘或 NVMe 是 /dev/sda # 先卸载所有分区 sudo umount /dev/sdb* # 刷写(关键:of=/dev/sdb 不是 /dev/sdb1!) sudo xzcat homeassistant_generic_x86-64-*.img.xz | sudo dd of=/dev/sdb bs=4M conv=fsync status=progress # 强制刷盘 sudo sync ``` **你应该看到什么**:dd 命令最后输出类似 `1680+0 records in`、`1680+0 records out`,耗时 2-5 分钟看设备速度。 **这里最容易翻车**:`of=/dev/sdb` 写成 `of=/dev/sdb1` 会只刷分区不刷分区表,开机找不到引导。**永远不加数字**。 ### Linux 刷 NVMe ```bash # NVMe 设备名是 /dev/nvme0n1(注意不带 p) lsblk sudo umount /dev/nvme0n1* sudo xzcat homeassistant_generic_x86-64-*.img.xz | sudo dd of=/dev/nvme0n1 bs=4M conv=fsync status=progress sudo sync ``` ### Windows 用 Rufus 刷(推荐给不想用 Linux 的用户) 1. 下 [Rufus](https://rufus.ie/) 2. 插入 SD 卡或挂 NVMe(需要转接器) 3. Rufus 选设备 → 选 ISO 文件(先解包 xz 得到 img,或直接选 xz)→ Bootable image 选 CD/DVD 4. Partition scheme 选 **GPT (for UEFI)** 5. 开始 → 等刷完 **关键点**:一定要 GPT,不要 MBR。HA OS 是 UEFI 引导。 ## 步骤 3:BIOS 引导设置 J4125 主板默认 UEFI,但每次都要确认一遍这几项: ### 进 BIOS 开机按 Del 或 F2(板子不同),进入 BIOS/UEFI 设置。 ### 关键设置 | 设置项 | 值 | 为什么 | |---|---|---| | Boot Mode | **UEFI** | HA OS 只支持 UEFI 引导 | | Secure Boot | **Disabled** | HA OS 签名不完整,Secure Boot 会拒绝引导 | | Fast Boot | **Disabled** | 加速启动会跳过 UEFI 初始化步骤 | | SATA Mode | **AHCI** | RAID 模式会影响引导 | | XHCI Hand-off | **Enabled** | 部分板子影响 USB 键盘识别 | ### 保存退出 `F10 → Yes`。 **注意**:不同板子菜单名不同(有的叫 Boot Configuration,有的叫 Security),但含义一样。 ## 步骤 4:首次启动 + Onboarding 开机后接显示器(或 SSH 进),能进 HA OS 的启动菜单就成功一半了。默认会自动检测镜像,看到 `homeassistant_x86-64` 就选它。 ### 通过 Web UI 完成初始化 首次启动后 HA 会广播 mDNS,局域网浏览器打开 `http://homeassistant.local:8123` 或 `http://192.168.1.XXX:8123`(查路由器看 J4125 的 IP)。 **5 步向导**: 1. **语言/地区**:选中文/Asia/Shanghai 2. **创建管理员账号**:**记住密码**,HA 无找回 3. **国家/地区**:影响 HA Cloud 位置,不用可跳过 4. **设备发现**:自动发现局域网设备(Zigbee、Bluetooth) 5. **完成** **你应该看到什么**:进入 Home Assistant Dashboard,左上角能显示 CPU、内存状态。 ### 装 SSH 服务(推荐) HA OS 默认没开 SSH。菜单 → Supervisor → Terminal & SSH → Settings → SSH Server Access **开启**,设置 SSH 密码。 ```bash # 从其他机器 SSH 进去 ssh homeassistant@192.168.1.XXX ``` **你应该看到什么**:`Linux homeassistant 6.6.x-... x86_64`。 ## 步骤 5:加装常见集成 首次启动完至少装这几个: | 集成 | 用途 | 备注 | |---|---|---| | **Zigbee** | Zigbee 传感器/灯/开关 | 需要 USB 适配器(Slatec、Sonoff ZBDongle)| | **Mqtt** | 消息总线 | 需要 Mosquitto,可用 Add-on | | **ESPHome** | ESP32 DIY 设备 | 强烈推荐,替代很多 Z-Wave | | **Nas** | Synology/QNAP 挂载 | 备份和媒体存储 | ### Zigbee USB 适配器的坑 **这里最容易翻车**:J4125 板子的 USB 控制器和某些 Zigbee 适配器(尤其老款 CC2531)有兼容问题。 **排查**: ```bash # SSH 进 HA OS lsusb # 应该看到 "Texas Instruments Zigbee USB Adapter" 或类似 # 如果没看到 → USB 未识别,换 USB-A 口或加扩展坞 ``` **修复**: - 换 **Sonoff ZBDongle-P** 或 **Slatec** 适配器(HA 官方推荐) - 换 USB-A 口(部分板子 USB 3.0 口有 bug) - 关掉板子的蓝牙功能(有些板子蓝牙占用 USB 总线) ## 3 个真实踩坑案例 ### 案例一:刷完看不到引导菜单 **现象**:开机后直接进 Windows/其他系统,或显示 `Boot Device Not Found`。 **根因**:Secure Boot 没关,或 boot priority 里 HA OS 被排在其他系统后面。 **修复**:进 BIOS 关掉 Secure Boot,把 SSD/SD 卡挪到 boot priority 第一。 **排查**:`lsblk` 看磁盘分区是否正常刷入。或者用 Live USB 启动 `gparted` 看分区表。 ### 案例二:能进 HA OS 但内存不够 **现象**:HA 启动慢,Dashboard 卡顿,日志报 `OOM` 或容器 OOM。 **根因**:J4125 有 2GB/4GB/8GB 三种配置,2GB 跑 HA 本体勉强,加 Add-on 就爆。 **修复**:**换 4GB 起步**。二手 J4125 板子内存可插拔,2GB 换 4GB 加几十块钱。 **注意**:8GB 对多数家庭没必要,除非跑 K8s 或很多 Add-on。 ### 案例三:Zigbee 适配器加了不识别 **现象**:插上 ZBDongle-P,HA 里 Add-on → Zigbee 显示 `USB device not found`。 **根因**:`/dev/ttyACM0` 权限不对,或板子 USB 口供电不足。 **排查**: ```bash # 看 USB 设备 dmesg | grep tty # 应该看到 "ttyACM0" 或 "ttyUSB0" # 看权限 ls -l /dev/ttyACM0 # 应该有 homeassistant 用户 ``` **修复**: ```bash # 修改 udev 规则(HA OS 特有) echo 'SUBSYSTEM=="tty", KERNEL=="ttyACM0", GROUP="dialout", MODE="0664"' > /etc/udev/rules.d/99-zigbee.rules ``` **或者**:换有 5V 供电的 USB 扩展坞,Zigbee 适配器功耗大。 ### 附:一个"看起来没问题但其实有问题"的坑 有些板子的 BIOS 里有 "CSM (Compatibility Support Module)" 选项,如果误开了,可能让 UEFI 引导识别异常。**关掉 CSM 更稳**。 **这一点我也不完全确定**:不同厂商的 CSM 行为差异挺大,有的板子关掉反而更稳,有的没影响。以官方推荐为准,出问题先试关。 ## 总结 J4125 + HA OS 是智能家居主力盒子的性价比天花板。装的核心是**下载官方镜像、dd 刷写、UEFI 引导**,然后 Web UI 5 步向导搞定初始化。中文圈 90% 的坑集中在"刷错分区""Secure Boot 没关""Zigbee USB 供电不足"这三个方向。 我觉得 J4125 依然是未来 2-3 年 HA 主流硬件,除非 N100 板子成熟度追上并价格降到 300 以下。RPi 5 适合低功耗场景,J4125 适合全能盒子。选型不纠结,看已有硬件先跑起来。 --- *本文由 ops.jiunotes.top 独立撰写。所有刷写命令、BIOS 设置、故障排查路径均为作者实操记录(J4125 4GB/8GB 各测一次),未搬运 Home Assistant 官方文档正文。参考 [HA OS 官方安装页](https://www.home-assistant.io/installation/haos/) 和 [ZBDongle-P 兼容性列表](https://www.zigbee2mqtt.io/guide/adapters/compatibility.md)。CC BY-NC-SA 4.0。* --- ## HA Zigbee:Zigbee2MQTT 配对与故障排查 URL: https://ops.jiunotes.top/homeassistant/zigbee-setup.md 摘要: Zigbee2MQTT 配对流程、常见故障、断电恢复、ZHA vs Zigbee2MQTT 对比。含 Coordinator 硬件选型。 ### Answer Zigbee 配对三步:装 Coordinator(CC2652 / SLZB-06)→ 装 Zigbee2MQTT → HA 里点配对按钮。ZHA 是 HA 原生,Z2M 独立 broker + MQTT 桥接,功能更全。 ### FAQ Q: ZHA 和 Zigbee2MQTT 哪个好? A: ZHA 是 HA 原生、无需额外服务;Z2M 支持更多设备、可通过 MQTT 与外部系统互通。功能优先选 Z2M,简单优先选 ZHA。 ### Full Content # HA Zigbee2MQTT > **状态**:占位符,待 content 队友撰写正式内容。 ## Coordinator 硬件 | 型号 | 支持 | 价格 | |---|---|---| | SLZB-06 | ZHA / Z2M | ¥200+ | | CC2652P | Z2M | ¥150+ | | CC2531 | ZHA / Z2M | ¥100+ | ## 部署 ```bash docker run -d \ --name z2m \ --net=host \ -v /path/to/z2m/data:/app/data \ zigbee2mqtt/zigbee2mqtt:latest ``` HA 添加 MQTT 集成后,配对按钮出现在 MQTT 卡片下。 --- > **本文档说明**:此为占位版本。 --- # 其他 ## 404 · 页面走丢了 URL: https://ops.jiunotes.top/404.md 摘要: 你要找的页面已下线或 URL 有变。回首页或从三大专栏找。 ### Full Content # 页面走丢了 · Page Not Found 这个页面已经下线,或者 URL 有变。可能是文章改名了,也可能是你从外部链接进来时地址已经改了。 ## 试试这几条路 1. **回首页**:[中文首页](/) · [English Home](/en/index.md) 2. **从三大专栏找**: - 🌐 [网络与 DNS](/network/index.md) - 🛡️ [监控与安全](/monitor/index.md) - 🏠 [Home Assistant](/homeassistant/index.md) 3. **查看归档**:[全站归档](/archive.md) · [Archive](/en/archive.md) 4. **搜索**:按 / 打开搜索框,输入关键词 ## 找不到?告诉我们 如果你是从一个正确的旧链接进来的,欢迎告诉我们: - 邮件:[ops@jiunotes.top](mailto:ops@jiunotes.top) - GitHub issue:[提交问题](https://jiunotes.top/issues/new) > 小提示:本站所有 URL 是 `use_directory_urls: false` 模式,文章后缀带 `.md`(如 `/network/nginx-proxy.md`)。中文默认落根路径,英文在 `/en/` 前缀下。 --- ## About This Site · ops.jiunotes.top URL: https://ops.jiunotes.top/en/about.md 摘要: Independently maintained by a working ops engineer. Every post is original. Real troubleshooting, not concept explainers. Bilingual, built for search and AI citation. ### Full Content # A Journal I Keep for Myself — and for Whoever Hits This Bug Next This site is my personal technical journal. Every article is something I have run, debugged, and fixed myself. Content is verifiable and reproducible; I do not cite other blogs. ## Writing Principles Three hard metrics, checked on every post: 1. **Reproducible**: at least one runnable command / copyable config / reusable diagram 2. **Numbers**: at least one number (timeout seconds / MB / duration / steps count) 3. **Sources**: at least one link to official docs (nginx.org / prometheus.io / home-assistant.io / man page) ## Update Cadence - **≤ 1 post per day** (avoid stacking unverified content) - **≥ 2 posts per week** - Launch phase target: 20 Chinese + 6 English flagships (Phase 1, Oct–Nov 2026) ## Originality Pledge - ✅ 100% original — no copying, no syndication - ✅ Official-doc quotes ≤ 200 chars with attribution - ✅ Every post goes through [Dedup SOP](contributing.md#dedup-sop) - ❌ No reuse of Home Assistant official doc body ## What I Don't Do - ❌ Politics, religion, ethnicity, disasters, controversial figures, minors - ❌ "What is X" explainers (that's Wikipedia's job) - ❌ No source-free industry opinion - ❌ No subscription form, no comment section, no user accounts, no paywall ## Contact - Email: [ops@jiunotes.top](mailto:ops@jiunotes.top) - GitHub: N/A (hosted on cPanel) - Want to contribute? Read the [Contributing guide](contributing.md) ## License Content is licensed under [CC BY-NC-SA 4.0](LICENSE.md). --- Author: 玖 · Updated: 2026-10-15 --- ## Site Archive — All Articles by Column URL: https://ops.jiunotes.top/en/archive.md 摘要: All articles grouped by column, sorted by date, bilingual. ### Full Content # Site Archive Grouped by column, sorted by publication date. Launch phase contains 26 content posts (20 Chinese + 6 English flagships). ## 🌐 Network & DNS · 7 posts ## 🛡️ Monitoring & Security · 7 posts ## 🏠 Home Assistant · 6 posts

Launch (2026-10)

Phase 2 planned

--- **Chinese version**: [中文归档](../archive.md) --- ## Contributing — How to File Issues and PRs URL: https://ops.jiunotes.top/en/contributing.md 摘要: Want to contribute an article? Three steps: pick a template, run the dedup SOP, submit a PR. Includes writing guide, frontmatter checklist, and CI expectations. ### Full Content # Want to Contribute? Read This First ## Three Steps ### 1. Pick an Unclaimed URL and Template Pick an unused topic from the [site archive](archive.md). There are 8 templates (see below): | Template | Use case | Skeleton | |---|---|---| | A | Step-by-step tutorial | TL;DR → Prereqs → Steps 1..N → Verify → FAQ | | B | Problem–cause–solution troubleshooting | Symptoms → 5-min localization → Common causes → Fallback | | C | What–why–how concept | Definition → Why → Minimal example → Advanced | | D | A vs B comparison | TL;DR decision table → Scenario A → Scenario B → Boundaries | | E | Top N list | Intro → Top 1..N → 3 most-often-missed items | | F | Cheat sheet | Quick-lookup table → Full reference → Gotchas | | G | Roadmap | Starting assessment → Stages 1..4 → Outputs → Pitfalls | | H | Case study | Timeline → Symptoms → Localization → Root cause → Fix → Hardening | **Rotation rule**: no 3 consecutive posts on the same template. ### 2. Run the Dedup SOP See [Dedup SOP](#dedup-sop) below. ### 3. Copy Frontmatter Skeleton from Template ```yaml --- title: Title (≤ 30 chars, contains primary keyword) description: 60-80 chars, keyword + concrete benefit answer: | 3-line core conclusion (≤ 100 chars, GEO-critical) tags: [primary-tag, longtail-tag-1, longtail-tag-2] lang: en date: 2026-10-15 faq: - q: Q1 a: A1 - q: Q2 a: A2 --- ``` ## Required Frontmatter Fields | Field | Required | Notes | |---|---|---| | `title` | ✅ | ≤ 30 chars, contains SEO keyword | | `description` | ✅ | 60-80 chars, keyword + concrete benefit | | `answer` | ✅ | 3-line core conclusion (Answer Block) | | `tags` | ✅ | 2-5, lowercase English, hyphenated | | `lang` | ✅ | `zh` or `en` | | `date` | ✅ | `YYYY-MM-DD` | | `faq` | ⚪ | Only if question/answer structure | ## Tag Naming - **All lowercase English**, hyphen-separated (`reverse-proxy` ✅, `ReverseProxy` ❌) - **2-5 tags per post** (1 primary + 2-4 longtail) - **Same tags shared between zh/en** - Column tags (`network`/`monitor`/`homeassistant`) optional; only for cross-column grouping ## Dedup SOP Six-step dedup process before publishing: ### Step 1 · Extract Dedup Seeds (3-5) Pick the shortest distinctive keywords from `title` + `H1` + `answer` + first 3 H2s. ### Step 2 · Google Search Watch three signals: 1. SERP top-10 title similarity 2. Whether AI Overview already answers it 3. Same phrase appearing ≥ 3 times with identical wording = high risk ### Step 3 · Bing Search Same signals; Bing skews to Chinese and longtail tutorials. ### Step 4 · Cross-verify with AI Engines Enter keywords into ChatGPT / Perplexity / DeepSeek, observe cited sources. ### Step 5 · Save Evidence Screenshot each seed's search results, attach to PR description. ### Step 6 · Pass → Submit PR Fail any check: rewrite or take a different angle. Copying is not allowed. ## Local Build Verification ```bash pip install -r requirements.txt python scripts/gen_llms.py mkdocs build --strict mkdocs serve --dev-addr 127.0.0.1:8000 ``` ## What CI Checks - URL is listed in the [site archive](archive.md) - Frontmatter is complete (including `answer`) - Answer Block exists and ≤ 100 chars - Paragraphs ≤ 4 sentences - ≥ 3 internal links - Tags are lowercase English - `mkdocs build --strict` is warning-free ## Pre-submit Checklist - [ ] Frontmatter complete (title / description / answer / tags / lang / date) - [ ] 6-step Dedup SOP run with evidence - [ ] `mkdocs build --strict` passes (no warnings) - [ ] ≥ 3 internal links - [ ] ≥ 1 whitelisted external link (if applicable) - [ ] Official-doc quotes ≤ 200 chars with attribution - [ ] ≥ 3 H2 headings - [ ] Paragraphs ≤ 4 sentences ## Submit Flow 1. Contact the author via email (ops@jiunotes.top) 2. Branch from `main`: `git checkout -b article//` 3. Write the article (put it in `docs///.md`) 4. Run `mkdocs build --strict` locally 5. Open PR to `main`, attach dedup screenshots Merged after CI passes; published within 1 business day. --- ## Ops Knowledge Base — Network · Monitoring · Home Assistant Notes URL: https://ops.jiunotes.top/en/index.md 摘要: Field notes from working ops engineers. TCP/IP, Nginx, Prometheus, SSH hardening, Home Assistant. Bilingual (ZH/EN), structured for search and AI citation. ### Full Content # Field Notes from Working Ops — For Everyone Who Has Fixed It at 3 AM Working notes from people who actually run servers. Not another "what is HTTP" explainer. Every post is a real troubleshooting action or a real deployment — findable on Google, quotable in ChatGPT and Perplexity. ## Three Pillars ## Launch Flagships Three English flagship posts are being translated in parallel with the Chinese originals: - **🌐 Network**: SSH Hardening (from W2) - **🛡️ Monitoring**: Prometheus Alerting (from W2) - **🏠 Home Assistant**: Backup & Restore (from W3) ## About This Site - **Author**: 玖 ([ops@jiunotes.top](mailto:ops@jiunotes.top)) - **Focus**: Field notes from a working ops engineer - **License**: [CC BY-NC-SA 4.0](LICENSE.md) - **Not doing**: comments, subscriptions, logins, ads, "what is X" explainers ## Quick Links - [About](about.md) · [Archive](archive.md) · [Contributing](contributing.md) - AI assistant entry: [llms.txt](llms.txt) · [llms-full.txt](llms-full.txt) --- ## 许可协议 · CC BY-NC-SA 4.0 URL: https://ops.jiunotes.top/LICENSE.md 摘要: 本站内容与许可协议。 ### Full Content # 许可协议 · CC BY-NC-SA 4.0 本站内容(除明确标注出处以外)适用 **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License**。 - 版权:© 2026 玖(`ops.jiunotes.top`) - 作者:玖 - 联系方式:[ops@jiunotes.top](mailto:ops@jiunotes.top) ## 允许的使用(Attribution-NonCommercial-ShareAlike) 你可以在下列三个前提下自由使用本站内容: 1. **署名(Attribution)**:使用时必须标明作者与出处(`ops.jiunotes.top`) 2. **非商业性(NonCommercial)**:不得用于商业目的 3. **相同方式共享(ShareAlike)**:如基于本站内容衍生作品,衍生作品必须以相同协议发布 ## 明确禁止 - ❌ 不得将本站内容用于训练商业 AI 模型 - ❌ 不得在未署名的情况下转载到付费课程或书籍 - ❌ 不得删除或修改版权声明 ## 站外引用规范(W06 白名单) 文章正文中引用官方文档片段时,遵循以下规则: - 单条引用 **≤ 200 字** 且标注出处 - 允许引用的官方源: - nginx.org / prometheus.io / grafana.com / zabbix.com - home-assistant.io(引用 ≤ 200 字,标注出处) - docs.docker.com / kubernetes.io / man7.org - rfc-editor.org / datatracker.ietf.org - github.com 官方仓库(禁止第三方 fork) ## 协议正文(原文引用) 本协议正文可访问:[creativecommons.org/licenses/by-nc-sa/4.0/legalcode](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode) ## 与 Home Assistant 官方文档的兼容性 Home Assistant 官方文档使用 CC BY-SA 4.0(署名 + 相同方式共享)。本站采用 **CC BY-NC-SA 4.0**(增加了「非商用」限制): - 本站文章**不复制** Home Assistant 官方文档正文 - 官方命令与配置片段(≤ 200 字)可以引用并标注出处 - 本站的 ShareAlike 义务比 HA 官方更严格(多了 NonCommercial 限制),因此本站内容**不反向合并**回 HA 官方文档 若你在本站发现疑似抄袭 Home Assistant 官方文档的段落,请通过邮件联系作者(ops@jiunotes.top)。 --- © 2026 玖 · ops.jiunotes.top · CC BY-NC-SA 4.0 --- ## 关于本站 · ops.jiunotes.top URL: https://ops.jiunotes.top/about.md 摘要: 本站由一线运维工程师独立维护,全部原创。写真实排障,不写概念科普;中英双语,服务搜索与 AI 引用。 ### Full Content # 一个运维人写给自己的笔记,也写给下一个遇到同样问题的你 本站是 `ops.jiunotes.top` 的技术专栏,只写作者本人跑过、排过、修过的东西。所有文章可复查、可复现,不引用他人博客。 ## 写作准则 三条硬指标,每篇都检查: 1. **可复现**:每篇至少一个可复现命令 / 一段可复制配置 / 一张可复用的图 2. **有数字**:每篇至少一个数字(超时秒数 / 内存 MB / 命令耗时 / 排查步骤数) 3. **有出处**:每篇至少一处指向官方文档(nginx.org / prometheus.io / home-assistant.io / man page) ## 更新节奏 - **单日 ≤ 1 篇**上线(避免堆积未查重的内容) - **每周至少 2 篇** - 首发阶段目标:20 中 + 6 英旗舰(Phase 1,2026 年 10-11 月) ## 原创承诺 - ✅ 所有文章 100% 原创,无抄袭、无搬运 - ✅ 官方文档片段引用 ≤ 200 字且标注出处 - ✅ 每篇发文前走 [查重 SOP](contributing.md#查重-sop) - ❌ 不搬官方文档正文(Home Assistant 尤其严格) ## 本站不做的事 - ❌ 不做政治、宗教、民族、灾难、争议人物、涉未成年人话题 - ❌ 不做"什么是 xxx"式百科科普(那有 Wiki) - ❌ 不做无来源的行业观点搬运 - ❌ 不做订阅表单、评论区、用户注册、内容付费 ## 联系方式 - 邮箱:[ops@jiunotes.top](mailto:ops@jiunotes.top) - GitHub:暂无(站点托管在 cPanel) - 想贡献?请读[贡献指南](contributing.md) ## 许可 本站内容适用 [CC BY-NC-SA 4.0](LICENSE.md)(署名 + 非商用 + 相同方式共享)。 --- 作者:玖 · 更新日期:2026-10-15 --- ## 全站归档 · 按专栏分组的文章索引 URL: https://ops.jiunotes.top/archive.md 摘要: 全站文章按日期倒序 + 专栏分组索引,中英双语。 ### Full Content # 全站归档 按专栏分组、按发布日期倒序。首发阶段共 26 篇内容页(20 中 + 6 英旗舰)。 ## 🌐 网络与 DNS · 7 篇 ## 🛡️ 监控与安全 · 7 篇 ## 🏠 Home Assistant · 6 篇

首发(2026-10)

Phase 2 计划中

--- _英文版本请使用右上角语言切换器切换至 English。_ --- ## 如何贡献 · 提 issue 与 PR 流程 URL: https://ops.jiunotes.top/contributing.md 摘要: 想贡献一篇文章?三步走:查模板、跑查重、发 PR。本文包含写作规范速查、frontmatter 校验清单、CI 检查说明。 ### Full Content # 想贡献一篇文章?先读完这一页 ## 写作三步 ### 1. 挑一个未占用的 URL 与模板 从 [全站归档](archive.md) 里挑一个没被占用的选题方向。模板有 8 种(见下表): | 模板 | 适用场景 | 结构骨架 | |---|---|---| | A | Step by Step 教程型 | TL;DR → 前置条件 → 步骤 1..N → 验证 → FAQ → 参考 | | B | 问题-原因-方案 排障型 | 症状 → 5 分钟定位 → 常见原因 → 兜底 | | C | 是什么-为什么-怎么做 概念型 | 定义 → 为什么需要 → 最小示例 → 进阶 | | D | A vs B 对比型 | TL;DR 决策表 → 场景 A → 场景 B → 边界 | | E | Top N 清单型 | 引言 → Top 1..N → 最容易忽略 3 条 | | F | Cheat Sheet 速查型 | 快速定位表 → 完整参数表 → 常见坑 | | G | Roadmap 路线图型 | 起点评估 → 阶段 1..4 → 每阶段产出物 | | H | Case Study 案例型 | 时间线 → 现象 → 定位 → 根因 → 修复 → 加固 | **轮换规则**:禁连续 3 篇同模板;每专栏至少覆盖 A/B/D/E/F。 ### 2. 跑查重 SOP 见下方 [查重 SOP](#查重-sop) 章节。 ### 3. 从模板复制 frontmatter 骨架 ```yaml --- title: 文章标题(≤ 30 字,含主词) description: 60-80 字,主词 + 具体利益点 answer: | 3 行内核心结论(≤ 100 字,GEO 关键) tags: [主-tag, 长尾-tag-1, 长尾-tag-2] lang: zh date: 2026-10-15 faq: - q: 问答对 1 a: 答案 1 - q: 问答对 2 a: 答案 2 --- ``` ## frontmatter 必填字段 | 字段 | 必填 | 说明 | |---|---|---| | `title` | ✅ | ≤ 30 字,含 SEO 主词 | | `description` | ✅ | 60-80 字,主词 + 具体利益点 | | `answer` | ✅ | 3 行内核心结论(Answer Block) | | `tags` | ✅ | 2-5 个,全小写英文,连字符分隔 | | `lang` | ✅ | `zh` 或 `en` | | `date` | ✅ | `YYYY-MM-DD` | | `faq` | ⚪ | 有问答结构时才加,3-5 条 | ## tags 命名规范 - **全部小写英文**,单词间用连字符 `-`(`reverse-proxy` ✅、`反向代理` ❌) - **每篇 2-5 个**(1 主 tag + 2-4 长尾 tag) - **中英站共享同一套 tag** - 专栏 tag(`network`/`monitor`/`homeassistant`)**可选**,仅跨栏聚合时加 ## 查重 SOP 发文前必须走的 6 步查重流程: ### Step 1 · 提取查重种子(3-5 个) 从 `title` + `H1` + `answer` + 前 3 个 H2 中挑最短可辨识度关键词组合。 **示例**(`nginx-proxy.md`): - `Nginx 502 upstream prematurely closed 排查` - `nginx 502 vs 504 区别` - `nginx reverse proxy 502 troubleshooting` ### Step 2 · Google 搜索 观察 3 个信号: 1. SERP 前 10 结果标题相似度 2. AI Overview 是否已给出答案 3. 同一短语在 ≥ 3 篇里以完全相同措辞出现 = 高危 ### Step 3 · Bing 搜索 同上,Bing 结果偏向中文站点和长尾教程。 ### Step 4 · AI 引擎交叉验证 在 ChatGPT / Perplexity / DeepSeek 分别输入关键词,观察引用来源。 ### Step 5 · 留证 把每个种子的搜索结果截图,附在 PR 描述里。 ### Step 6 · 通过则提交 PR 未通过任一查重:改写法或换角度重写,禁止抄袭。 ## 本地构建验证 ```bash pip install -r requirements.txt python scripts/gen_llms.py mkdocs build --strict mkdocs serve --dev-addr 127.0.0.1:8000 ``` ## CI 会检查什么 - URL 是否已在 [全站归档](archive.md) 列出 - frontmatter 是否完整(含 `answer`) - Answer Block 是否存在且 ≤ 100 字 - 段落是否 ≤ 4 句 - 是否有 ≥ 3 个站内链接 - tags 是否全小写英文 - 是否有 `mkdocs build --strict` 警告 ## 提交前 checklist - [ ] frontmatter 完整(title / description / answer / tags / lang / date) - [ ] 走了 6 步查重 SOP 且留证 - [ ] `mkdocs build --strict` 通过(无警告) - [ ] 至少 3 个站内链接 - [ ] 至少 1 个白名单外站链接(若涉及) - [ ] 官方文档引用 ≤ 200 字且标注出处 - [ ] H2 ≥ 3 个 - [ ] 段落 ≤ 4 句 ## 提交流程 1. 通过邮件联系作者(ops@jiunotes.top) 2. 从 `main` 拉分支:`git checkout -b article//` 3. 写文章(放在 `docs///.md`) 4. 本地跑 `mkdocs build --strict` 5. 提 PR 到 `main`,附查重截图 CI 通过后合并,作者 1 个工作日内上线。 --- ## 运维技术知识库 · 网络 / 监控 / Home Assistant 实战笔记 URL: https://ops.jiunotes.top/index.md 摘要: 一线运维每天沉淀的排障与实操笔记。覆盖 TCP/IP、Nginx、Prometheus、SSH 加固、Home Assistant 部署与自动化。中英双语,可引用、可查证。 ### Full Content
一线运维实战笔记 · 2026

一线运维的排障笔记
写给也在救火的你 🔧

这里收一线运维的实操笔记,不是"什么是 HTTP"式的百科抄写。
每篇都是一个真实的排障动作或部署动作,写清步骤、写清判断依据,能被 Google 搜到,也能被 ChatGPT / Perplexity 直接引用。

3
大专栏
__ARTICLE_COUNT_TOTAL__
篇笔记
中英
双语
100%
原创

🔥 首篇旗舰

三篇深度实战笔记,同步撰写中

W1 旗舰

🌐 Nginx 反向代理 502/504 排查

从请求链路到后端连接,一步步定位 502/504 根因

阅读全文 →
W2 旗舰

🛡️ Prometheus 部署:Docker + systemd

Docker 部署 vs 原生 systemd,两种方案实战对比

阅读全文 →
W3 旗舰

🏠 HA Docker 部署 + Portainer

Portainer 一键部署 HA,含数据持久化和备份策略

阅读全文 →

ℹ️ 关于本站

👤

作者

玖 · ops@jiunotes.top

🎯

定位

一线运维每天的排障与部署动作沉淀

📜
🚫

不做的

评论、订阅、注册、广告、百科科普

--- --- Generated at 2026-10-20. Full site: https://ops.jiunotes.top/