知识广场

按学科筛选:计算机科学
清除筛选

«计算机科学» 分类下共 256 篇帖子

写 incident runbook:让凌晨 3 点的 oncall 能跟着做

## 起因 服务挂了。监控告警发到 oncall 手机。 理想:oncall 知道怎么处理。 现实:oncall 是新人 / 不熟这服务 / 凌晨 3 点头脑不清 → 慌。 `runbook`(操作手册):每个服务 / 每类 alert 一份明确步骤文档。 不需要 oncall 思考根因,跟着步骤恢复 → 后续 owner 来 root cause。 ## 一份 runbook 包含什么 ```markdown # Runbook: API service down ## TL;DR 1. Check status: `kubectl get pods -n api` 2. If pod CrashLoopBackOff → restart: `kubectl rollout restart deploy api -n api` 3. If still bad → check dashboard: <url> 4. If beyond 15 min → page tech lead: <name> ## 告警含义 什么触发的:API 5xx > 5% 持续 5 min 影响:用户 login / 主要功能不可用 ## 快速 mitigation [5 个具体命令 / 步骤] ## diagnose [查日志 / metric 怎么看] ## 升级路径 mitigation 没用 → 找谁 + 怎么联系 ``` 关键:**第一行就告诉怎么做**,不是先 5 段背景介绍。 ## 模板 ```markdown # Runbook: <Alert Name> ## 紧急 mitigation (< 5 分钟) 具体命令 / 步骤,复制粘贴能跑。 ## 影响 - 用户感知:xxx - 受影响服务:xxx ## 何时升级 - 5 分钟内未解决 → 找 oncall #2 - 15 分钟内未解决 → page tech lead - 影响数据完整性 → 拉 incident commander ## diagnose 1. Grafana: <link> 2. 日志查询:<link Loki / ELK> 3. trace:<link Jaeger> ## 已知场景 - 场景 A:症状 → 处理 - 场景 B:症状 → 处理 ## 相关链接 - 架构图:<link> - 设计文档:<link> - post-mortem:<link to history> ``` ## 实例:DB connection pool 满 ```markdown # Runbook: PG connection pool exhausted ## 5 分钟 mitigation ```bash # 1. 看 pool 状态 psql -h pgbouncer -U admin pgbouncer -c "SHOW POOLS;" # 如果 cl_waiting > 0 → 有客户端排队 # 2. 重启 app(释放可能 leak 的连接) kubectl rollout restart deploy/api -n api kubectl rollout restart deploy/worker -n api # 3. 临时扩 pool kubectl edit configmap pgbouncer-config -n db # default_pool_size: 30 → 50 kubectl rollout restart deploy/pgbouncer -n db # 4. 5 分钟内未恢复 → 升级 ``` ## 影响 - 所有 API 请求 timeout / 慢 - 后台 worker 失败 ## diagnose - Grafana "PG conn pool": <link> - log: `{service="api"} |= "connection refused"` - trace 看慢 query:<link> ## 已知场景 - 长 transaction leak(应用 bug):找 long-running query: ```sql SELECT pid, now() - xact_start, query FROM pg_stat_activity WHERE state != 'idle' ORDER BY 2 DESC; ``` kill 长跑:`SELECT pg_terminate_backend(pid);` - 流量 spike:HPA 没跟上 → 手动扩 `kubectl scale deploy/api --replicas=20 -n api` ## 升级 - 第 5 分钟 → @oncall-secondary - 第 15 分钟 → @platform-lead - 数据损坏迹象 → @dba @cto ## 历史 - 2025-02-12 incident: long tx leak in checkout flow - 2024-11-03 incident: traffic spike from marketing ``` ## 让 runbook 持续有用 死的 runbook = 没 runbook。 每 incident 后 update: - 这步骤管用 → 强化 - 不管用 → 删 / 改 - 新 case → 加"已知场景" post-mortem 写完必须更新 runbook(强制 process)。 ## alert 跟 runbook 关联 ```yaml # Prometheus alert rule - alert: ApiHighErrorRate expr: rate(api_errors[5m]) > 0.05 annotations: summary: "API error rate > 5%" runbook_url: "https://wiki.example.com/runbook/api-error-rate" ``` PagerDuty / Slack alert 自动带 runbook 链接 → oncall 一键点开。 ## 命名 - `api-5xx-spike`(不是"api-bad") - `pg-replica-lag` - `cert-expiring-soon` 具体 + 可搜。每个 alert name 对应一个 runbook URL slug。 ## 存哪 - Confluence / Notion:好搜索 + 版本 - GitHub repo: ops/runbooks/<name>.md:跟 code 一起 review - 内置 Grafana annotation 我们用 git repo + 工具自动 publish 到 web view。 PR review 强制:alert 新增必须有 runbook PR。 ## decision tree 模式 复杂场景画决策树: ``` alert: api-down start │ ▼ pod 是否 running? │ ├─ 否 → kubectl describe pod → 是否 ImagePullBackOff? │ ├─ 是 → registry 故障,看 <link> │ └─ 否 → 看 events,可能 OOMKilled → 扩 memory │ └─ 是 → 是否 5xx? ├─ 是 → 看 dependency (DB / Redis),看 <link> └─ 否 → ingress 问题,看 <link> ``` mermaid 图直接渲染。复杂场景帮助新人按图索骥。 ## 演练 (game day) 每季度模拟 incident: 1. SRE 故意触发"假 incident"(kill pod / 切 DB / 灌错配) 2. oncall 跟 runbook 处理 3. 后 retro:runbook 哪步含糊 / 缺步骤 新人参与多 → 暴露 runbook 不足。 不演练 = runbook 锈了。 ## 没 runbook 的 alert 怎么办 不应该。 原则:"no runbook, no alert"。 如果没人知道收到这 alert 该干啥 → 这 alert 没意义,删了或者写 runbook。 避免"alert 海":每天 100 个 alert 没人看 → 真出事漏。 ## 与 chaos engineering chaos engineering 主动制造小故障验证系统韧性。 runbook 是被动响应文档。 两者互补:chaos 找出 weakness → 加 runbook + 修系统。 ## 工具 - **PagerDuty**:alert → oncall + runbook - **Opsgenie**:类似 - **Atlassian Statuspage**:状态页给客户 - **Linear / Jira**:incident ticket 跟踪 ## 真实 case 某 prod incident 凌晨 2 点: - API 5xx 飙 - oncall (新人,3 个月)收到 alert - runbook 链接:5 步 mitigation - 第 2 步 (rollout restart) 没好 - 第 4 步 (扩 pool) 缓解 - 5 分钟用户感知恢复 - 早上 team 看到 → root cause: third-party API timeout,加更长 timeout 完整 incident 30 分钟解决,影响 < 5 分钟。 没 runbook 的话新人可能就先 page 一堆人,影响时间几倍。 ## 反模式 - "看监控就知道了":监控不告诉你怎么修 - "去问 X 老员工":X 出差 / 离职 - "看代码就知道":凌晨 3 点没人看 code - "再 alert 一次看看":用户 already 受影响 runbook 是为最坏情况写的,假设阅读者一无所知。 ## 踩过的坑 1. **runbook 跟实际不符**:服务改了配置 runbook 没更新 → 命令报错。 review 加"runbook 改了吗"。 2. **太长**:5 页 runbook 凌晨没人读完。TL;DR + 第一屏放 mitigation。 3. **依赖外部知识**:"找 platform team Slack 沟通" 但 Slack 名变了。 绝对路径 / 永久 link。 4. **不演练**:以为 runbook 写完就够 → 真出事步骤不通。季度演练。 5. **没人 own**:runbook 谁更新?每服务 owner 负责。新 alert 设 "DRI"。

lazygit:终端里把 git 用得比 IDE 快的 TUI

## 起因 我每天 `git status` / `git add -p` / `git commit -m` / `git push` / `git log --oneline` / `git diff HEAD` 重复几十次。打字成本不算高但 每次都要看一眼输出再决定下一步,rhythm 容易卡。IDE 的 git 面板鼠标 操作又比键盘慢。 `lazygit` 是 Go 写的 git TUI:一屏看到 status / branches / log / stash / commits / files,全键盘操作。 ## 安装 ```bash # Debian / Ubuntu (PPA) LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" \ | grep -oE '"tag_name":\s*"v[^"]+"' | sed 's/.*"v\([^"]*\)"/\1/') curl -Lo lazygit.tar.gz \ "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" tar xf lazygit.tar.gz lazygit sudo install lazygit /usr/local/bin rm lazygit.tar.gz lazygit # macOS brew install lazygit # Windows scoop install lazygit ``` 在任意 git 仓库里跑: ```bash lazygit # 或者起个 alias echo 'alias lg=lazygit' >> ~/.bashrc ``` ## 关键键位 主屏分 5 个 panel:files / branches / commits / stash / status。 `Tab` / `[` `]` 切 panel。 ### Files panel - `<space>` stage / unstage(按整文件) - `<enter>` 进入文件 → 选行 / hunk → `<space>` 部分 stage - `d` discard(清除改动) - `c` commit(弹起编辑器写 message) - `C` commit without verifying (--no-verify) - `a` stage all - `i` add to .gitignore ### Branches panel - `<space>` checkout - `n` 新建分支 - `d` 删除 - `r` rebase 选中分支到当前 - `f` fast-forward 拉这条分支(不切过去) - `M` merge 进当前分支 ### Commits panel - `<space>` checkout 这个 commit - `c` cherry-pick - `r` reword commit message - `e` interactive edit (相当于 rebase -i + edit) - `f` fixup 上一条 - `s` squash 进上一条 - `d` drop - `<enter>` 看 commit diff,再选文件 / hunk 看具体改动 ### Stash panel - `s` stash 当前改动 - `<space>` apply - `g` pop(apply + drop) - `d` drop ### Status panel (左上) - `,` switch to recent repos - 看 ahead/behind 远端的数量 ## 一些日常操作的"光速" gesture ### 局部 stage(hunk-level) ``` 进 files → enter 进文件 → ↓↑ 选 hunk → <space> stage 这块 → q 退出 → c commit ``` 比 `git add -p` 流畅 5x。 ### Interactive rebase 一气呵成 ``` commits panel → e(edit)→ 选要 reword 的 → r → 改 message → 选要 fixup 的 → f → m menu → 'continue rebase' ``` 不用记 `git rebase -i HEAD~5` 的所有 squash/reword/edit/drop 单字母。 ### 解 conflict merge / rebase 冲突时 lazygit 自动跳到 conflict 面板: ``` ↓↑ 选要保留的版本 (ours / theirs / both) → <space> 选 → 下一处 ``` 复杂 conflict 仍要进编辑器手解,但简单的可以 lazygit 内完成。 ### 自动 push / fetch ``` P push p pull f fetch ``` `shift+P` 强制推(force push with lease)。 ## 自定义命令 `~/.config/lazygit/config.yml`: ```yaml gui: showFileTree: true # 文件树视图 showRandomTip: false language: 'auto' scrollHeight: 2 customCommands: - key: 'C' context: 'files' command: "git commit -m '{{.Form.Message}}' --signoff" prompts: - type: 'input' title: 'commit message (with --signoff)' key: 'Message' - key: 'b' context: 'commits' description: 'git bisect from this commit' command: "git bisect start && git bisect bad HEAD && git bisect good {{.SelectedLocalCommit.Sha}}" ``` ## 与 nvim / vscode 集成 ```bash # 在 vim 里直接打开 lazygit nnoremap <leader>g :LazyGit<CR> # 需要装 kdheepak/lazygit.nvim ``` VSCode 装 `lazygit-vscode` 扩展,`Cmd+Shift+P → LazyGit` 启动。 ## 效果 - daily 流程从 "切窗口 → 打命令 → 看输出 → 想下一步" 变成 "lg → 几个 字母 → 完成" - 复杂的 rebase / cherry-pick / branch 切换不再让我犹豫"忘了命令" - 同事被我安利后我推荐过的 5 个人有 4 个换成了 lazygit - 老 git 命令照样会用(脚本里、CI 里),lazygit 是 interactive 场景的 覆盖 ## 踩过的坑 1. **大仓库(10w+ files)启动慢**:lazygit 会扫整个 status。在 monorepo 里设 `git config core.untrackedCache true` + `core.fsmonitor true` 提速。 2. **conflict UI 不显示 conflict marker 颜色**:终端不支持 truecolor。 换 iTerm2 / Alacritty / WezTerm 解决。 3. **快捷键和 vim 冲突**:把 `:` 作为 lazygit menu 的话和 vim 命令模式 冲突。`x` `:` 改 keybinding。 4. **跨平台行尾问题**:Windows + Linux 协作时 lazygit 显示一堆 "modified no diff",是 CRLF 自动转换。`git config core.autocrlf` 团队统一。 5. **git LFS**:lazygit 不知道 LFS 文件,stage 大文件可能不走 LFS pointer。 先 `git lfs track` 配好 .gitattributes。

systemd WatchdogSec:进程卡死自动重启(不只是崩溃)

## 起因 服务挂了 systemd `Restart=on-failure` 能自动重启。但 "进程没崩,却卡死 不响应" 这种情况 systemd 看不出来——TCP 连接还在,进程还活,只是不 处理请求了。 `WatchdogSec` 让服务定期向 systemd 发"我还活"信号;超时不发就被 systemd 当死了重启。 ## 工作原理 ``` service 进程 ↓ 每 N 秒 sd_notify(WATCHDOG=1) systemd watchdog timer ↓ 收到 → reset timer ↓ 没收到 → SIGTERM + restart ``` 时间预算(WatchdogSec=30s)= 服务必须在 30s 内"喂狗"一次。 ## 配置 systemd 单元 `/etc/systemd/system/myapp.service`: ```ini [Service] Type=notify WatchdogSec=30 NotifyAccess=main ExecStart=/usr/local/bin/myapp Restart=on-watchdog RestartSec=5 ``` - `Type=notify`:systemd 期待服务在启动好后发 `READY=1` - `WatchdogSec=30`:30 秒内没"喂狗"就重启 - `Restart=on-watchdog`:watchdog 超时也重启(默认 on-failure 不含) - `NotifyAccess=main`:只允许主进程发 notify ## 应用代码:发 WATCHDOG ### Python ```python import os import socket import threading import time def notify(message): """向 systemd notify socket 发消息""" sock_path = os.environ.get('NOTIFY_SOCKET') if not sock_path: return s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) if sock_path.startswith('@'): sock_path = '\0' + sock_path[1:] try: s.sendto(message.encode(), sock_path) finally: s.close() def watchdog_loop(): interval = int(os.environ.get('WATCHDOG_USEC', '30000000')) // 1_000_000 // 2 while True: # 在这里 check 应用真"活"——不只是循环跑 if app_is_healthy(): notify('WATCHDOG=1') time.sleep(interval) # 启动时 notify('READY=1') # 后台喂狗 threading.Thread(target=watchdog_loop, daemon=True).start() # 业务主循环 serve_forever() ``` `WATCHDOG_USEC` 是 systemd 自动设的环境变量(微秒)。喂狗间隔通常 设为 WatchdogSec 的 1/2 给容错。 `app_is_healthy()` 是你的健康检查: ```python def app_is_healthy(): # check DB / Redis / 关键资源 try: db.ping(timeout=2) # 检查事件循环没死锁 / queue 不爆 return queue.qsize() < 10000 except Exception: return False ``` 如果 health check 失败,不要喂狗 → systemd 杀进程重启。 ### Go ```go import ( "os" "time" "github.com/coreos/go-systemd/v22/daemon" ) func main() { // ... 初始化 daemon.SdNotify(false, daemon.SdNotifyReady) // 启动 watchdog goroutine interval, err := daemon.SdWatchdogEnabled(false) if err == nil && interval > 0 { go func() { tick := time.NewTicker(interval / 2) for range tick.C { if appHealthy() { daemon.SdNotify(false, daemon.SdNotifyWatchdog) } } }() } serve() } ``` ### Rust ```rust use systemd::daemon; use std::thread; use std::time::Duration; fn main() { daemon::notify(false, [(daemon::STATE_READY, "1")].iter()).unwrap(); thread::spawn(|| { let interval = Duration::from_secs(15); loop { if app_healthy() { daemon::notify(false, [(daemon::STATE_WATCHDOG, "1")].iter()).unwrap(); } thread::sleep(interval); } }); serve(); } ``` ### 不会修改源码的服务 跑现成 binary 没 sd_notify 支持?用 `systemd-notify` wrapper: ```ini [Service] ExecStart=/path/to/wrapper.sh ``` ```bash #!/bin/bash # wrapper.sh your-app & APP_PID=$! while kill -0 $APP_PID 2>/dev/null; do if curl -sf http://localhost:8080/health > /dev/null; then systemd-notify WATCHDOG=1 fi sleep 15 done ``` 但这种方式增加复杂度;建议优先改应用代码。 ## 实测 ```bash sudo systemctl start myapp sudo systemctl status myapp # 启动后看 status: active (running) # 状态行有 "Watchdog: 30s" # 模拟"应用卡死" —— 让 app_is_healthy 返 False # 30 秒后 systemd 会: # 1. 发 SIGTERM # 2. 等待终止 # 3. 按 Restart=on-watchdog 重启 journalctl -u myapp -f # 看到 "Watchdog timeout (limit 30s)!" + restart ``` ## 与"被动监控 + 外部重启" 对比 外部脚本检测 + 重启: ```bash # cron */1 * * * * if ! curl -sf http://localhost:8080/health; then systemctl restart myapp fi ``` 优点:简单,不改应用代码。 缺点: - 间隔最小 1 分钟(cron) - 健康检查走 HTTP(增加耦合) - restart 流程慢(systemctl 命令 + 进程退出 + 重启) WatchdogSec 优点: - 毫秒级触发 - 应用内部直接判断"我健康吗" - systemd 一站式管 生产推荐 WatchdogSec。 ## 几个调参 ### WatchdogSec 太短 `WatchdogSec=5`:网络抖动 / GC 暂停 / 大事务 commit → 误杀健康进程。 通常 30-120 秒是合理范围。 ### 启动期不喂狗 启动加载大模型几分钟: ```ini TimeoutStartSec=10min WatchdogSec=30 ``` 启动期内由 `TimeoutStartSec` 控制;READY 发出后才进入 WatchdogSec 监管。 应用代码:模型 load 完才 `notify('READY=1')` + 启动 watchdog 线程。 ### RuntimeMaxSec:周期性重启 ```ini WatchdogSec=30 RuntimeMaxSec=24h # 跑超过 24h 强制重启 ``` 防内存泄漏类问题:每天自动重启一次,泄漏不可能跑到 OOM。 ## 实战:FastAPI + uvicorn + WatchdogSec uvicorn 不原生支持 sd_notify。 解决方案 1:用 hypercorn 或 gunicorn (`UvicornWorker`),自己写 hook。 或者用 ASGI lifespan event: ```python # app/main.py import asyncio, socket, os from fastapi import FastAPI from contextlib import asynccontextmanager def notify(msg): sock_path = os.environ.get('NOTIFY_SOCKET') if not sock_path: return s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) try: s.sendto(msg.encode(), sock_path) finally: s.close() async def watchdog(): interval = int(os.environ.get('WATCHDOG_USEC', '30000000')) / 2_000_000 while True: if await check_health_async(): notify('WATCHDOG=1') await asyncio.sleep(interval) @asynccontextmanager async def lifespan(app: FastAPI): notify('READY=1') task = asyncio.create_task(watchdog()) yield task.cancel() app = FastAPI(lifespan=lifespan) # health check 是真检查事件循环 + DB async def check_health_async(): try: await asyncio.wait_for(db.execute('SELECT 1'), timeout=2) return True except Exception: return False ``` systemd unit: ```ini [Service] Type=notify WatchdogSec=60 ExecStart=/srv/app/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=on-watchdog ``` 效果:DB 卡死 → health check 不通过 → 不喂狗 → 60s 后 systemd 杀重启。 ## 监控 systemd journal 里 watchdog 触发显示: ``` systemd[1]: myapp.service: Watchdog timeout (limit 30s)! systemd[1]: myapp.service: Killing process 12345 with signal SIGABRT. ``` Prometheus node_exporter 自动收 systemd `*_restart_count` 指标。 告警:`rate(systemd_unit_restart_total[1h]) > 5` → 服务持续不健康。 ## 踩过的坑 1. **`Type=simple` 不 work**:必须 `Type=notify` watchdog 才生效。 2. **fork 后子进程喂狗** `NotifyAccess` 默认 main:子进程发的被忽略。 要么改成 `NotifyAccess=all`,要么主进程负责。 3. **测试期间设置太短**:`WatchdogSec=5` 测试是因为快,生产忘改回 常态 → 高负载时 GC 一下被误杀。 4. **app 用 thread + GIL**:watchdog 线程被 GIL 卡住 → 整个 Python 进程都不喂狗 → 误杀。用 async 或 multiprocessing 把 watchdog 独立。 5. **WATCHDOG_USEC 没读到**:环境变量是 systemd 启动子进程时注入。 如果你的 service 用 shell wrapper 启动,要 `exec` 替换或显式 `env` 传过去。

Partytown:把 GA / GTM 等第三方 script 丢 web worker

## 起因 你的网站有: - Google Analytics - Google Tag Manager - Facebook Pixel - HubSpot / Intercom chat - Segment 第三方 script 通常 200-500 KB 不少,跑在 main thread → 占 CPU + 拖慢 首屏(hydration / interaction)。 Lighthouse 性能跌 20-30 分常因这些。 `Partytown`(Builder.io):把第三方 script 丢到 web worker 跑。 main thread 专心 render,性能不被第三方坑。 ## 原理 ``` Main thread: - 你的 React / Vue - DOM 操作 - 用户交互 Web Worker (Partytown): - GA / GTM / 等 - 通过 proxy 间接访问 DOM (synchronous via SharedArrayBuffer) ``` 第三方 script 调 `document.cookie` / `window.dataLayer` → Partytown 代理 到 main thread → script 以为自己在 main thread 跑。 ## 装 (Next.js) ```bash npm install @builder.io/partytown ``` ```tsx // next.config.js module.exports = { experimental: { nextScriptWorkers: true }, }; // _document.tsx 或 app/layout.tsx import Script from 'next/script'; <Script strategy="worker" // 关键 src="https://www.googletagmanager.com/gtag/js?id=G-XXX" /> <Script id="gtag-init" strategy="worker"> {` window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-XXX'); `} </Script> ``` 跑起来 → GA script 在 worker。 GA 看到的数据跟正常一样。 ## 装 (vanilla / 任意 framework) ```html <head> <script> window.partytown = { forward: ['dataLayer.push'], // 这些 method 转 worker }; </script> <script src="/~partytown/partytown.js"></script> <script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=G-XXX"></script> <script type="text/partytown"> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-XXX'); </script> </head> ``` `type="text/partytown"` 是关键 → Partytown 拦截 + 在 worker 跑。 ## 性能效果 我们一个 marketing 站: | metric | before | after | |---|---|---| | Lighthouse Performance | 62 | 92 | | TBT (Total Blocking Time) | 850ms | 80ms | | TTI (Time to Interactive) | 5.2s | 2.1s | | LCP | 3.8s | 2.5s | 主要 GA + GTM + HubSpot 三个第三方加起来约 600 KB JS。 丢 worker 后 main thread 几乎不卡。 ## 副作用 / 限制 1. **不能用 `document.write`**:worker 没 DOM 写。多数现代 script OK。 2. **synchronous DOM access 延迟**:worker → main 调用有几 ms 开销。 GA 类 batched analytics 无感;动画类 script 不行。 3. **某些 script 不兼容**:检测自己环境会发现"不在 main thread" 报错。少数老 script。 4. **需要 `Cross-Origin-*` header**:用 SharedArrayBuffer 优化要 COOP/COEP。 不配也能跑(fallback 慢 IPC)。 ## 哪些第三方适合 适合: - analytics (GA, Mixpanel, Segment, Amplitude) - tag manager (GTM) - chat widget (Intercom, HubSpot) - ad pixel (Facebook, TikTok) - A/B testing (Optimizely) 不适合: - 必须 sync DOM 操作(如 Stripe Elements 表单内嵌) - 视频 / 动画 SDK(需 60fps) - 关键功能 script(auth / payment 核心) ## debug Chrome devtools 看: - main thread:你的 app - worker thread:partytown + 第三方 network tab 看 partytown 跑的 request(带 `partytown` referer)。 ## 与 GTM server-side 对比 GTM server-side container:把 GTM 处理移到自己 server,client 只发原始 event。 | | partytown | GTM SS | |---|---|---| | 复杂度 | 低 | 高 | | client perf | 好 | 极好 | | 成本 | 0 | server cost | | 控制 | 中 | 强 | 大网站常两者结合:partytown for 简单 script + GTM SS for 核心 tracking。 ## 与 facade pattern YouTube embed / chat widget 等:先显示假封面,用户 hover/click 才加载真 iframe。 ```jsx const [loaded, setLoaded] = useState(false); return loaded ? <YouTubeEmbed /> : <FakePoster onClick={() => setLoaded(true)} />; ``` partytown 移走 main thread 加载;facade 直接延迟加载。 互补。 ## 实际接入步骤 1. Lighthouse 跑当前 → 找 main thread blocking script 2. 列出第三方 script 清单 3. 一个一个 wrap partytown → 测 GA / etc 仍正常报数据 4. 重 Lighthouse → 看分提升 通常 1 天内能上线 + 显著效果。 ## 静态站特别合适 Astro / Gatsby / 11ty 等静态站性能"就差第三方拖" → partytown 神药。 Astro 内置 partytown integration: ```js // astro.config.mjs import partytown from '@astrojs/partytown'; export default { integrations: [partytown()] }; ``` ```html <script type="text/partytown" src="..."></script> ``` ## 跟 Next.js Script 对比 Next.js 自己也有 `Script strategy`: - `beforeInteractive`:阻塞,head 内 - `afterInteractive`:default,body 末 - `lazyOnload`:idle 时 - `worker`:partytown(实验) `worker` 内部就是 partytown 集成。 ## 真实 case 某客户 marketing 站 SEO / 性能死活上不去: - GTM 200 KB - HubSpot 300 KB - GA + 5 个 pixel LCP 5s,Google 算"slow" → SEO 罚。 接 partytown 一周: - LCP 2.3s - Core Web Vitals 全绿 - 自然流量 +15%(SEO 改善) 第三方需求没变 → 性能彻底改善。 ## 不要乱开 不是所有 script 都该丢 worker。 仔细想:这 script 真的不需要 sync main thread? 比如 Stripe element 在 form 里要 sync 渲染 → 不行。 GA fire-and-forget event → 完美。 ## 踩过的坑 1. **第三方更新挂掉**:GA 新 SDK 跟 partytown 不兼容(罕见)。 monitor 数据连续性,发现异常 fallback 普通 script。 2. **COOP/COEP header 跟 OAuth iframe 冲突**:需要 cross-origin iframe 的功能可能挂。试个 staging。 3. **dev mode performance 反慢**:partytown 在 dev 调试模式 IPC 开 销大。生产无问题。 4. **dataLayer race condition**:用 partytown 后 dataLayer 是异步 推。需要 sync read 的 code 会拿空。 5. **CSP 配 worker-src**:CSP 严格时 worker 加载被 block。 `worker-src 'self'` 加。

lazygit:终端里的 git GUI,告别一长串 git 命令

## 起因 git 命令多 + 难记。常见操作经常要 4-5 个步骤: - 看 diff → stage 部分 → unstage 错的 → commit → push - 找某个 commit 看做了啥 → cherry-pick → push - rebase 时改某个 commit message - 一个文件 history 看每行 blame 老办法:`git status` + `git diff` + `git add -p`(交互 hunk)+ `git commit` ... 一连串。 GUI 方案(GitKraken / Tower / Sourcetree)功能强但要离开终端 + 收费 + 启动慢。 `lazygit` 是终端 TUI,5 秒启动 + 键盘操作 + 覆盖 90% git 工作流。 ## 装 ```bash brew install lazygit # 或 apt install lazygit # ubuntu 22.04+ ppa ``` ## 启动 仓库目录里: ```bash lazygit ``` 或者 alias `lg='lazygit'`。 UI 5 个 panel: ``` ┌─Status─────┬─Files──────────────────────────────────┐ │ master │ M app/views.py │ │ │ ?? new_file.md │ ├─Branches───┤ │ │ master ├─Diff/Output──────────────────────────┤ │ feature │ + new line │ │ │ - old line │ ├─Commits────┤ │ │ abc Update │ │ │ def Fix │ │ └────────────┴──────────────────────────────────────┘ ``` `Tab` 切 panel,`?` 看快捷键。 ## 常用操作 ### stage / commit / push ``` 1. j/k 在 Files panel 选文件 2. space 切换 stage / unstage 3. enter 进 hunk view,space stage 单个 hunk 4. c 写 commit message 5. P push ``` vs `git add -p` 交互的痛苦:lazygit 可视化 + 按键即操作。 ### 看 diff 选文件 → 右边 panel 自动显示 diff。 高亮 / 上下滚(PgUp/PgDn) / 同步两窗口(vimdiff like 用 i 触发)。 ### 切换分支 Branches panel: ``` 1. Tab 到 Branches 2. j/k 选分支 3. space checkout 4. n 新建分支 5. d 删除 ``` ### rebase / cherry-pick Commits panel: ``` 1. Tab 到 Commits 2. r 选中 commit 改 message 3. s squash 进上一个 commit 4. f fixup(squash + 用上一个 message) 5. e edit commit(修改文件后 continue) 6. d drop commit ``` interactive rebase 不用记 `git rebase -i HEAD~5` + vim 编辑 todo list。 lazygit 里上下选 + 按键。 ### stash 操作 ``` S # stash all g # 弹 stash 列表 space # pop ``` ### log + blame ``` y # show log graph b # blame current file ``` ### remote 操作 ``` P # push(可选 force / 不同 remote) p # pull f # fetch ``` ## 配置 `~/.config/lazygit/config.yml`: ```yaml gui: theme: lightTheme: false showCommandLog: false nerdFontsVersion: "3" # 用 NerdFont 图标 git: autoFetch: false # 默认每分钟 fetch,关掉省网络 paging: colorArg: always pager: delta --paging=never # 用 delta 渲染 diff keybinding: files: commitChanges: "c" customCommands: - key: "C" description: "Commit conventional" command: "git commit -m '{{.Form.Type}}({{.Form.Scope}}): {{.Form.Msg}}'" context: "files" prompts: - type: "menu" title: "Type" options: - { name: "feat", value: "feat" } - { name: "fix", value: "fix" } - { name: "chore", value: "chore" } - type: "input" title: "Scope" key: "Scope" - type: "input" title: "Message" key: "Msg" ``` customCommands 强大:自定义快捷工作流。`Shift-C` 启动 conventional commit prompt。 ## 与 git 命令对比 | 操作 | git 命令 | lazygit | |---|---|---| | stage 部分 | `git add -p` + y/n | space space space | | commit | `git commit -m "..."` | c → 输入 | | 切分支 | `git checkout xxx` | Tab → 选 → space | | rebase 改 message | `git rebase -i HEAD~3` + edit | r | | stash pop | `git stash pop` | g → space | | 看历史 | `git log --oneline` | 主屏右下自动 | 熟练后 lazygit **快 2-3x**。新人上手时间从 git CLI 几周 → lazygit 几小时。 ## 与 GitUI / tig / gitk 对比 - **GitUI**:Rust 写,比 lazygit 启动还快,但功能略少(无 customCommands) - **tig**:老牌,只读 history 浏览强,操作交互弱 - **gitk**:Tcl/Tk GUI,老土但 history graph 经典 我用 lazygit 做日常 + tig 看复杂 history graph。 ## 与 IDE 内置 git 工具对比 VS Code / JetBrains 自带 git GUI 也好用。 lazygit 优势: - 在终端里(vim / tmux 用户友好) - 跨编辑器一致体验 - 远程 ssh 服务器也能用(GUI 没法用) 我同时用:IDE 看 diff / cherry-pick UI;lazygit 做 rebase / stage。 ## tmux popup 启动 `~/.tmux.conf`: ``` bind g display-popup -E -w 90% -h 90% lazygit ``` 任意窗口 `prefix+g` 弹 lazygit popup → 操作完关掉。完美。 ## 踩过的坑 1. **大仓库慢**:几十万 commit 的 repo(如 chromium)lazygit 启动几秒 load。`logCmd` config 可以限制。 2. **submodule 不直观**:lazygit 对 submodule 支持有限。 submodule 切换要 enter 进 sub view。 3. **conflict 解决**:merge conflict 时 lazygit 提示要手动编辑文件再回来。 有简单的 file marker view 但复杂 conflict 还是开编辑器。 4. **commit 模板没用上**:git `commit.template` 配的模板 lazygit commit 命令不自动用。要在 lazygit config 里另外配 `commitPrefix`。 5. **远程 / fetch 不显**:拉新分支后 lazygit 没自动刷新。R refresh 一下。

JWT:HS256 vs RS256 vs EdDSA,啥时候哪个

## 起因 JWT 签名算法常见三个: - **HS256**:HMAC-SHA256,对称密钥 - **RS256**:RSA + SHA256,非对称 - **EdDSA (Ed25519)**:现代椭圆曲线,非对称 选哪个?默认 HS256 行不行?为啥很多场景非 RS256 不可? ## HS256 (HMAC) ```python import jwt SECRET = 'shared-secret-key' # 签 token = jwt.encode({'user_id': 42}, SECRET, algorithm='HS256') # 验 payload = jwt.decode(token, SECRET, algorithms=['HS256']) ``` 对称:**签和验用同一 secret**。 ### 优势 - 简单(一个 secret) - 性能极好(HMAC 比 RSA 快 100x) - token 短 ### 劣势 / 适用边界 - 任何能验 token 的服务都能签 token → **不能给第三方验** - 单 monolith 内部用 OK;微服务要严格区分签发方与验证方 适合:单一服务 + 用同一 secret 的自家系统。 ## RS256 (RSA 非对称) ```python # 服务端:private key 签 with open('private.pem', 'rb') as f: private_key = f.read() token = jwt.encode({'user_id': 42}, private_key, algorithm='RS256') # 任何方:public key 验 with open('public.pem', 'rb') as f: public_key = f.read() payload = jwt.decode(token, public_key, algorithms=['RS256']) ``` 非对称:**private key 签,public key 验**。 public key 公开(其它服务 / 客户端能拿到),但拿到也不能伪造 token。 ### 优势 - 安全分离:auth server 持 private key,resource server 只持 public key - public key 可发布(JWKS endpoint) - 大组织 / OAuth / SSO 标配 ### 劣势 - 性能:RSA 签验比 HMAC 慢 100-1000x - token 大(RSA 签名 256 字节起) - key 管理更复杂 适合:微服务 / OAuth / 第三方需验 token。 ## EdDSA / Ed25519 ```python token = jwt.encode({'user_id': 42}, ed25519_private_key, algorithm='EdDSA') ``` 非对称,基于 Ed25519 椭圆曲线。 RS256 的现代替代: | | RS256 (2048 bit) | EdDSA (Ed25519) | |---|---|---| | 签名速度 | 慢 | 快(10x+) | | 验证速度 | 较快 | 极快 | | key size | 2048 bit | 256 bit | | 签名 size | 256 byte | 64 byte | | 安全性 | 强 | 强(现代设计) | 新项目能用 EdDSA 就用,性能 + size 全面优于 RS256。 RS256 仍是 OAuth 兼容性最广(IdP / library 都支持)。 ## 决策 ``` 监控你的 token 谁签 / 谁验 单 monolith / 自家服务全套: → HS256 跨服务 / 公开 API / 给第三方 / SSO: → RS256(兼容性)或 EdDSA(性能) OAuth provider / IdP: → RS256(事实标准) ``` ## 错配灾难(algorithm confusion attack) ```python # 危险:alg 不锁定 payload = jwt.decode(token, public_key) # 不指定 algorithms ``` 攻击者把 token 的 alg header 改成 `HS256`,用 public key 当 secret 签 (因为 RS256 → HS256 confusion)→ 假 token 通过验证。 **正确**: ```python payload = jwt.decode(token, public_key, algorithms=['RS256']) # 指定接受的算法,不让 attacker 选 ``` 库默认大多防御此攻击,但**永远显式指定 algorithms**。 ## key rotation JWT 没内置 rotation。常见做法: - token 包含 `kid` (key ID) header - JWKS endpoint 暴露多个 active public key - 验证时根据 kid 选 key ```python # 假设 JWKS 已 cache def verify(token): header = jwt.get_unverified_header(token) kid = header['kid'] key = jwks_cache[kid] return jwt.decode(token, key, algorithms=['RS256']) ``` 旧 key 留 grace period 后删 → 老 token 平滑过渡。 ## token 内容设计 ```python { "iss": "https://auth.example.com", # issuer "sub": "user-12345", # subject (user) "aud": "https://api.example.com", # audience (intended verifier) "iat": 1716000000, # issued at "exp": 1716003600, # expiry "nbf": 1716000000, # not before "jti": "unique-id", # JWT ID(防重放) // custom "roles": ["admin", "editor"], "tenant": "acme" } ``` 验证时检查: - `exp` 没过 - `aud` 是自己 - `iss` 是受信任 issuer 很多 lib 自动。 ## refresh token JWT 短 expiry(15min - 1h)+ refresh token(长期,不透明 random string)。 access token 过期 → 用 refresh token 换新。 refresh token 存数据库(能撤销);access token 是 stateless JWT。 ## stateless vs session JWT 优势:服务端不存 session → 横向扩展无 sticky session 问题。 劣势:撤销难(除非短 expiry + refresh token 模式)。 中小项目其实 session cookie + Redis store 简单 + 够用。 微服务 / 多 IdP / OAuth 才必要 JWT。 ## 不要存敏感数据 ```python # bad jwt.encode({'password': 'secret123', ...}, ...) ``` JWT body **未加密**只签名。`base64decode` 立刻看到内容。 不要放:password / API secret / PII。 需要加密 → JWE (JSON Web Encryption),但复杂得多,少用。 ## 实战 case:迁移 HS256 → RS256 老项目 monolith HS256。 拆出 mobile API + 计划开放第三方 API → 需要 RS256(合作伙伴验 token 不该知 secret)。 迁移: 1. 生成 RSA key pair 2. JWKS endpoint 暴露 public key 3. token issue 支持双 alg:老 token HS256 + 新 token RS256 4. token verify 接受 HS256(kid="legacy")+ RS256 5. 等所有老 token expire(30 day) 6. 删 HS256 支持 平滑过渡,无服务中断。 ## 性能数据 签 / 验 100k token / single core: | | sign | verify | |---|---|---| | HS256 | 0.5s | 0.5s | | RS256 (2048) | 80s | 5s | | EdDSA | 3s | 1s | 签贵很多,验也比 HS256 慢。 高 QPS 验 token 时考虑 cache verified token 或者用 EdDSA。 ## 库 - Python: `PyJWT` / `python-jose` / `authlib` - Node: `jsonwebtoken` - Go: `github.com/golang-jwt/jwt` - Rust: `jsonwebtoken` PyJWT 简单 + 维护好。authlib 大全(含 OAuth)。 ## 踩过的坑 1. **algorithms 没指定**:confusion attack。永远 `algorithms=['XX']`。 2. **exp 时区**:服务器时间错 → 立刻过期 / 永不过期。NTP sync 必备。 3. **public key 写错**:copy 时 \n 丢 → 验失败。pem 用 file 加载, 不要在 env var 里塞 multi-line。 4. **JWT 当 session**:放 admin_panel=True 类敏感权限 → token leak 后 攻击者直接获取。敏感操作必须 DB 二次验证。 5. **长 expiry + 无 revoke**:30 day expiry JWT 用户改密码后老 token 仍有效。要么短 expiry + refresh,要么 blacklist 表(牺牲 stateless)。

LLM prompt engineering 实战 6 个 pattern(少花钱 + 多对题)

## 起因 接了一个"自动分类客服工单" 的任务。最 naive prompt: ``` 分类下面的客服请求到一个类别: {ticket} ``` 效果:60% 正确率 + 经常输出无关解释 + 偶尔编造类别。 调了几轮 prompt 后到 92%,token 使用减半。下面是几个真正提分的 pattern,不是"魔法咒语"。 ## Pattern 1: 明确角色 + 任务 + 输出格式 ``` 你是客服工单分类专家。 任务:把客户请求归到下面一个类别。 类别(严格选其中一个): - billing (付款 / 发票 / 退款) - bug (产品故障 / 异常报错) - feature_request (希望新增功能) - account (账号登录 / 权限 / 密码) - other (上面都不对) 输出格式: {"category": "<类别名>", "confidence": <0-1>} 不要输出任何额外解释。 客户请求: {ticket} ``` 为什么有效: - **角色**:把模型从"通用助手" 引导到"分类专家"语境 - **限制类别**:明确告诉它可选范围,减少幻觉 - **JSON 输出**:好解析 + 没散文废话 - **"不要输出额外解释"**:明确禁止前/后缀闲话 立刻从 60% → 80%。 ## Pattern 2: Few-shot 例子 光描述类别不够,给具体例子: ``` [上面的 prompt...] 例子: 输入:「我的卡被扣了两次,请退一次」 输出:{"category": "billing", "confidence": 0.95} 输入:「点击保存按钮后页面卡死」 输出:{"category": "bug", "confidence": 0.9} 输入:「能不能加个深色模式?」 输出:{"category": "feature_request", "confidence": 0.95} 输入:{ticket} 输出: ``` 3-5 个例子覆盖"边界 / 容易混淆" 的情况效果最好。 "为什么这个分到 billing 而不是 account" 类的边界 case 用例子讲清楚。 → 80% → 90%。 ## Pattern 3: Chain of thought(思考过程) 对推理类任务(不是简单分类)让模型先"想"再答: ``` 你是数学题解题专家。 题目:一个商店周一卖了 23 件商品,周二是周一的 1.5 倍, 周三是周一周二之和的一半。三天总共卖了多少? 要求: 1. 先逐步推理(标 [思考]) 2. 再给最终答案(标 [答案]) ``` 模型输出: ``` [思考] - 周一:23 件 - 周二:23 × 1.5 = 34.5 件 → 取整 35(实际 35 才合理) - 周三:(23 + 35) / 2 = 29 件 - 总:23 + 35 + 29 = 87 件 [答案] 87 ``` 直接问"答案是?"很多时候算错。让它写推理过程后准确率显著上升。 适合:数学 / 推理 / 多步逻辑。 不适合:简单 lookup / 分类(CoT 浪费 token)。 ## Pattern 4: Structured output (JSON mode / function call) 不要让 LLM 用 markdown 包 JSON 后让你正则提取——浪费 token + 不稳定。 直接用 API 的 structured output: ```python from openai import OpenAI import json client = OpenAI() resp = client.chat.completions.create( model='gpt-4o', messages=[{'role': 'user', 'content': prompt}], response_format={ 'type': 'json_schema', 'json_schema': { 'name': 'ticket_classification', 'schema': { 'type': 'object', 'properties': { 'category': { 'type': 'string', 'enum': ['billing', 'bug', 'feature_request', 'account', 'other'], }, 'confidence': {'type': 'number', 'minimum': 0, 'maximum': 1}, }, 'required': ['category', 'confidence'], 'additionalProperties': False, }, 'strict': True, } } ) result = json.loads(resp.choices[0].message.content) ``` `strict: true` 让模型在 decoding 时只产生 schema 合法 token。 **100% 合规 JSON**,从此不需要 try/except parse。 Anthropic 用 tool use / Gemini 用 response_schema 同理。 ## Pattern 5: 减少 token = 省钱 + 快 token 不仅是钱,还是延迟。每个 token 大模型 50-200ms。 技巧: 1. **删冗余形容词**:"请帮忙仔细认真地分析下面..." → "分析:" 2. **缩短例子**:3 个例子够时不放 10 个 3. **不重复 system + user**:system 里写完类别后 user 不重复 4. **truncate 长输入**:保留 ticket 前 1000 字符(多数信号在开头) 5. **batch 处理**:5 个工单一起喂("分析下面 5 个 ticket")省 system token ```python # 单条 vs batch single_cost = (system_tokens + ticket_tokens + output) * N batch_cost = (system_tokens + N * ticket_tokens + N * output) # batch 省的是 N 份 system tokens ``` 我们 batch=10 后 token 量降 30%。注意 batch 太大模型 attention 分散 精度反而降,5-15 是甜点。 ## Pattern 6: 自检 / verifier loop 对关键任务,跑两次让另一个 prompt 验证: ```python # Pass 1: 主分类 result = classify(ticket) # Pass 2: 验证 verify_prompt = f""" 有人把这个工单分类为 '{result['category']}'。是否合理? 工单:{ticket} 输出 JSON: {{"agree": true|false, "reason": "..."}} """ verify = llm(verify_prompt) if not verify['agree']: # 退到人工 review 队列 flag_for_human(ticket, result, verify['reason']) ``` cost 翻倍但低置信度 ticket 被人工接管,整体准确率上 96%+。 ## 一些反 pattern ### ❌ 写一堆"必须 / 不要 / 绝对" ``` 你必须按下面规则。 你绝对不能输出 X。 你一定要返回 JSON。 你不允许加任何解释。 你必须用中文。 ``` 模型对负面指令响应一般。改成正面: ``` 输出只包含 JSON。返回的语言是中文。 ``` ### ❌ 提供模糊定义 ``` 分类为: - 重要的问题 - 不重要的问题 ``` "重要" 没定义 → 模型自己猜 → 不稳定。 给具体标准 + 例子。 ### ❌ "请尽量准确" 这种废话 模型不会因为你 polite 就更努力。直接指令。 ### ❌ 给"超过 100k token" 的 context 大 context 时模型有"middle lost in the haystack" 效应——中间的信息 被忽略。能 chunk + RAG 的就别一次塞。 ## 调试 / 评估 写个 eval set(30-100 个 ground truth 例子): ```python test_cases = [ {'ticket': '...', 'expected': 'billing'}, {'ticket': '...', 'expected': 'bug'}, ... ] correct = 0 for tc in test_cases: pred = classify(tc['ticket']) if pred['category'] == tc['expected']: correct += 1 print(f'accuracy: {correct/len(test_cases):.2%}') ``` 每改 prompt 跑一遍 eval。比"感觉好像准了" 客观。 用 LangSmith / Promptfoo / weave 等工具系统化跑 A/B prompt 对比。 ## 模型选择 不同任务用不同模型: | 任务 | 推荐 | |---|---| | 简单分类 / 提取 | gpt-4o-mini / claude-haiku(便宜 + 快) | | 推理 / 代码 | gpt-4o / claude-sonnet-4-5 | | 极致难推理 | o1 / claude opus-4-7 / DeepSeek-R1 | | 本地隐私 | qwen2.5:14b / llama3.1:8b(Ollama) | 强行用大模型做简单分类 = 浪费钱。 ## 效果 工单分类项目最终: - 准确率 60% → 92% (+ verifier 后 96%) - 单 ticket 成本 $0.012 → $0.003(batch + 改 mini) - P95 延迟 5s → 1.2s(mini + structured output) - 总 month cost 从 $800 → $150 ## 踩过的坑 1. **改 prompt 不跑 eval**:"感觉变好了"是偏见。每次改后跑测试集 验证。 2. **prompt 越改越长**:加 patch 修 case → 长 prompt → 模型 confused。 定期 refactor 简化。 3. **生产环境模型版本变**:OpenAI / Anthropic 偶尔 silent 升级模型, prompt 行为变化。pin model name 包括 date suffix (`gpt-4o-2024-08-06`) + 监控 metrics。 4. **temperature**:分类 / 提取类 task 设 0 (deterministic)。 创意写作设 0.7-1.0。 5. **隐私数据**:不要把客户 PII 直接发 OpenAI。先 mask 邮箱 / 手机 / 信用卡再调 API。

PostgreSQL jsonb + GIN 索引:把日志 / 配置 / 半结构化数据放进 SQL

`jsonb` 是 PostgreSQL 9.4+ 的内部二进制 JSON 类型。比 `json`(纯文本存) 快得多,且可以建 GIN 索引做高速字段查询。 典型场景:审计日志、用户配置、设备上报数据、事件流。 ## 建表 ```sql CREATE TABLE events ( id BIGSERIAL PRIMARY KEY, type TEXT NOT NULL, occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), payload JSONB NOT NULL ); ``` ## 插入 ```sql INSERT INTO events (type, payload) VALUES ('login', '{"user_id": 42, "ip": "1.2.3.4", "ua": "Mozilla/5.0..."}'), ('purchase', '{"user_id": 42, "amount": 99.5, "items": ["a", "b"]}'); ``` ## 常用查询操作符 ```sql -- 取字段 SELECT payload->>'user_id' AS uid FROM events; -- 文本 SELECT (payload->>'amount')::numeric FROM events; -- 转型 SELECT payload->'items'->0 FROM events; -- 数组取第 0 个 -- 包含查询(最常用) SELECT * FROM events WHERE payload @> '{"user_id": 42}'; -- 键存在 SELECT * FROM events WHERE payload ? 'amount'; SELECT * FROM events WHERE payload ?& array['user_id', 'ip']; -- 全部存在 SELECT * FROM events WHERE payload ?| array['user_id', 'guest_id']; -- 任一存在 ``` ## GIN 索引:让上面查询变快 ```sql -- 全字段 GIN:所有 key/value 路径都索引(大) CREATE INDEX events_payload_gin ON events USING GIN (payload); -- jsonb_path_ops GIN:只索引 @>,体积小 30-50% CREATE INDEX events_payload_pgin ON events USING GIN (payload jsonb_path_ops); -- 部分索引:只有特定 type 的事件才索引 CREATE INDEX events_login_payload ON events USING GIN (payload) WHERE type = 'login'; ``` `jsonb_path_ops` 是大多数 @> 查询的最优选择,体积小但只支持 `@>`。 ## 函数式索引(针对单字段) 如果你只查 payload 里某一个字段: ```sql -- 在 (payload->>'user_id') 上建普通 btree CREATE INDEX events_user_id ON events (((payload->>'user_id')::bigint)); -- 查询 EXPLAIN ANALYZE SELECT * FROM events WHERE (payload->>'user_id')::bigint = 42; ``` 比 GIN 还快,但只能用于这一种查询。 ## 更新 ```sql -- 替换整个 payload UPDATE events SET payload = '{"...":...}' WHERE id = 1; -- 修改单字段 UPDATE events SET payload = payload || '{"status": "done"}' WHERE id = 1; -- 删字段 UPDATE events SET payload = payload - 'status' WHERE id = 1; -- 嵌套路径设置 UPDATE events SET payload = jsonb_set(payload, '{user, name}', '"alice"', true) WHERE id = 1; ``` ## 聚合 / 展开 ```sql -- 取所有不同的 type 值 SELECT DISTINCT payload->>'level' FROM events; -- 展开 JSON 数组为多行 SELECT id, jsonb_array_elements_text(payload->'items') AS item FROM events; -- 按 JSON 字段聚合 SELECT payload->>'country' AS country, count(*) FROM events GROUP BY payload->>'country' ORDER BY count(*) DESC; ``` ## 校验 / Schema 约束(jsonb_schema) PG 17 之前没原生 JSON Schema 校验。可以用 CHECK 约束做基础校验: ```sql ALTER TABLE events ADD CONSTRAINT payload_has_user_id CHECK (payload ? 'user_id' AND jsonb_typeof(payload->'user_id') = 'number'); ``` 更复杂的校验建议在应用层做(Pydantic / JSON Schema 库)。 ## 性能边界 - 行级 `payload` 字段 > 8KB 会被 TOAST(外部存储),读取需要解压 - 单值超过 1MB 性能急剧下降 - 经常修改的字段不要塞 jsonb:每次 UPDATE 整列重写 - 数组 push 没有"in-place",每次 append 都是整列重写 如果数据本质就是结构化的,建关系表,别用 jsonb 偷懒。 ## EXPLAIN 验证索引被用上 ```sql EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM events WHERE payload @> '{"user_id": 42}'; -- Bitmap Index Scan on events_payload_pgin -- Recheck Cond: (payload @> '{"user_id": 42}') -- ... ``` `Seq Scan` 出现说明索引没生效 —— 检查统计信息是否够新(`ANALYZE events`) 或者索引类型是否匹配查询。 ## Django ORM 用法 ```python from django.db import models from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.fields import JSONField # PG-specific, 旧 # 现代 Django 用通用的 models.JSONField,PG 后端自动 jsonb class Event(models.Model): type = models.CharField(max_length=50) occurred_at = models.DateTimeField(auto_now_add=True) payload = models.JSONField() class Meta: indexes = [ GinIndex(fields=['payload'], opclasses=['jsonb_path_ops'], name='events_payload_pgin'), ] # 查询: Event.objects.filter(payload__contains={'user_id': 42}) Event.objects.filter(payload__user_id=42) # 自动 ->> Event.objects.filter(payload__has_key='ip') ``` ## 踩过的坑 - 字段路径里 `->` 是 jsonb,`->>` 是 text。`->>` 后才能比较字符串; `(payload->>'amount')::numeric` 才能数值比较。 - GIN 索引建索引非常慢且占用空间大(可能比表本身还大)。生产建议 `CREATE INDEX CONCURRENTLY` 在线建,不锁表。 - `jsonb_path_ops` 不支持 `?`、`?|`、`?&` 操作符;如果要用这几个, 必须用普通 GIN。 - 在 PG 16 之前 `jsonb_set` 修改不存在的路径 + `create_missing=true` 时行为有 quirk;升 PG 16+ 之后行为更一致。

fzf 在终端里模糊搜索任意东西(文件、历史、分支、进程)

`fzf` 是命令行模糊搜索工具,几乎所有 "我要从一堆东西里挑一个" 的场景 都能用。一旦养成习惯,效率提升明显。 ## 安装 ```bash # Debian/Ubuntu (22.04+) sudo apt install -y fzf # 或最新版(推荐) git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` `install` 脚本会问你要不要装 shell key binding + completion,都选 y。 ## 基本 shell 集成 装完后,bash / zsh 里有这几个快捷键: - **Ctrl-T**:把"文件路径"插到光标处(fuzzy 选择当前目录下的文件) - **Ctrl-R**:搜索 shell 命令历史 - **Alt-C**:fuzzy cd 到子目录 - **`** `Tab`**:通用 completion(命令、文件名、kill PID 等) 最常用的是 **Ctrl-R** —— 一旦用上就忘不掉。 ## 常用模式 ### 选文件打开 ```bash vim "$(fzf)" ``` ### 选分支 checkout ```bash git checkout "$(git branch --all | fzf | sed 's/^..//; s/remotes\/origin\///')" ``` ### kill 进程 ```bash ps -ef | fzf -m | awk '{print $2}' | xargs kill # -m 多选 ``` ### 选 docker 容器进入 ```bash docker exec -it "$(docker ps | fzf | awk '{print $1}')" bash ``` ## 写成函数 / alias ```bash # 模糊 cd fcd() { cd "$(fd -t d --hidden --exclude .git . "${1:-.}" | fzf)" } # fzf-grep:模糊搜文件内容 fg() { local file file=$(rg --line-number --no-heading --color=always "${1:-.}" \ | fzf --ansi --delimiter=':' \ --preview 'bat --color=always {1} --highlight-line {2}' \ | cut -d':' -f1) [[ -n "$file" ]] && ${EDITOR:-vim} "$file" } # fzf-git-log:浏览 git log fgl() { git log --oneline --color=always | fzf --ansi \ --preview 'git show --color=always {1}' \ --bind 'enter:execute(git show {1} | less -R)' } ``` ## 与其他工具组合 ### 配合 ripgrep + bat ```bash export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!.git"' export FZF_DEFAULT_OPTS=' --height 40% --layout=reverse --border --preview "bat --color=always --style=numbers --line-range=:500 {}" ' ``` 放 `~/.bashrc`,之后 Ctrl-T 直接带预览。 ### fzf + Neovim 最快的"在 vim 里 fuzzy 找文件": ```vim " init.lua / vimrc nnoremap <leader>f :Telescope find_files<cr> " telescope 已经是 fzf-like ``` 或装 `fzf.vim`: ```vim nnoremap <leader>f :FZF<cr> ``` ## 多选 + 操作 ```bash # 多选文件批量删除 rm $(fzf -m) # 多选 git commit 做 cherry-pick git cherry-pick $(git log --oneline | fzf -m | awk '{print $1}') ``` `-m` 启用 multi-select;用 Tab 选中 / 取消选中。 ## 自定义 preview ```bash # 选目录,右边显示 ls find . -type d | fzf --preview 'ls -la {}' # 选 PR 号,右边显示 PR 详情 gh pr list | fzf --preview 'gh pr view {1}' | awk '{print $1}' ``` ## 模糊语法 - 默认 fzf 用 "smart case + fuzzy":你打 `usrlist` 能匹配 `User_List_View.tsx` - `'word`:精确匹配 - `^prefix`:开头 - `suffix$`:结尾 - `!noword`:取反 - `word1 | word2`:或 ## 自定义颜色 ```bash export FZF_DEFAULT_OPTS=' --color=bg+:#313244,bg:#1e1e2e,spinner:#f5e0dc,hl:#f38ba8 --color=fg:#cdd6f4,header:#f38ba8,info:#cba6f7,pointer:#f5e0dc --color=marker:#f5e0dc,fg+:#cdd6f4,prompt:#cba6f7,hl+:#f38ba8 ' ``` Catppuccin Mocha 主题。其他主题:fzf 的 wiki 一大堆。 ## 把 fzf 放进生产脚本 ```bash #!/usr/bin/env bash # deploy.sh - 选要部署的版本 TAG=$(git tag | sort -rV | fzf --header='选要部署的版本') || exit 1 echo "Deploying $TAG..." ssh prod-server "cd /srv/app && git fetch && git checkout $TAG && systemctl restart app" ``` CI 里 fzf 没意义(无 tty),但本地交互式脚本用 fzf 比一堆 case 语句 干净得多。 ## 踩过的坑 - 嵌套 fzf(fzf 选完结果当 fzf 参数)容易卡住——子 fzf 进程被父 fzf 抢 tty。中间存个变量再调用。 - `FZF_DEFAULT_COMMAND` 设了 `rg` 但 rg 没装:所有 fzf 调用都报错。 确保 rg / fd 都装了。 - 大目录(node_modules、.cache)拖慢 fzf:默认 command 加 `--glob '!**/node_modules'` 之类排除。fd 比 find 快 5-10 倍且默认尊重 .gitignore。 - WSL 里 fzf 偶尔光标错位,是终端 escape sequence 问题;升级 Windows Terminal + 用最新 fzf 解决。

GitHub Actions:matrix build / cache / reusable workflow 实战

## 起因 库需要在 Python 3.10/3.11/3.12 × Linux/Mac/Windows × 9 个组合上测试。 之前老 CI 写 9 个 job 重复代码。GitHub Actions 的 matrix 一行配齐。 ## 1. matrix build ```yaml # .github/workflows/test.yml name: test on: push: branches: [main] pull_request: jobs: test: strategy: fail-fast: false matrix: python: ['3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: windows-latest python: '3.10' # 跳某些组合 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: 'pip' - run: pip install -e .[dev] - run: pytest -xvs ``` `fail-fast: false`:一个 job 失败不取消其它(看完所有再 fail)。 8 个 job 并行跑(GitHub 免费 plan 公开 repo 20 并发)。 ## 2. cache 加速 ```yaml - uses: actions/cache@v4 with: path: | ~/.cache/pip ~/.cache/pypoetry .venv key: ${{ runner.os }}-py${{ matrix.python }}-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-py${{ matrix.python }}- ``` `key` 包含 lockfile hash → lockfile 没变 → cache 命中。 `restore-keys` fallback:完全不命中时拿前缀匹配最新的(部分有比没有强)。 cache 命中跳过 pip install → CI 时间从 2 分钟 → 20 秒。 Node: ```yaml - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci ``` setup-node action 内置 npm/yarn/pnpm cache,不需要单独 actions/cache。 ## 3. matrix + variables ```yaml strategy: matrix: include: - { name: 'python 3.10 / no extras', python: '3.10', extras: '' } - { name: 'python 3.12 / all extras', python: '3.12', extras: 'all' } - { name: 'python 3.12 / pgsql', python: '3.12', extras: 'postgres' } runs-on: ubuntu-latest steps: - run: pip install -e .[${{ matrix.extras }}] - run: pytest ``` `include` 比 `python: × extras:` 笛卡尔积更精细,只跑你 list 的组合。 ## 4. reusable workflows 公共逻辑抽到一个 yml 给多 repo 用: ```yaml # .github/workflows/python-test.yml (reusable) name: python-test on: workflow_call: inputs: python-version: type: string default: '3.12' coverage: type: boolean default: false secrets: CODECOV_TOKEN: required: false jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - run: pip install -e .[dev] - run: pytest ${{ inputs.coverage && '--cov' || '' }} - if: inputs.coverage uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} ``` 调用方: ```yaml # 某 repo .github/workflows/ci.yml jobs: test: uses: my-org/.github/.github/workflows/python-test.yml@main with: python-version: '3.12' coverage: true secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ``` 更新 reusable yml → 所有调用方下次 run 自动用新版(无需逐个 repo 改)。 ## 5. composite actions(细粒度复用) 复用几个 step 而不是整个 workflow: ```yaml # my-action/action.yml name: 'Setup project' description: 'Install uv + Python + sync deps' inputs: python-version: default: '3.12' runs: using: composite steps: - uses: astral-sh/setup-uv@v3 with: version: 'latest' enable-cache: true - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - run: uv sync --frozen shell: bash ``` ```yaml # 调用 - uses: ./my-action with: python-version: '3.12' - run: uv run pytest ``` 放在仓库本地 (`./.github/actions/setup`) 或独立 repo (`my-org/setup-action@v1`)。 ## 6. 条件运行 ```yaml - if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: ./scripts/deploy.sh - if: matrix.os == 'ubuntu-latest' run: ./scripts/coverage.sh - if: failure() run: ./scripts/notify-slack.sh ``` `success() / failure() / always() / cancelled()` 是 step 级条件。 ## 7. 路径 / 分支过滤 ```yaml on: push: paths: - 'backend/**' - '.github/workflows/backend.yml' branches: - main - 'release/**' ``` monorepo 里只改前端时不跑后端 CI。 ## 8. concurrency 防同一分支同时多 run ```yaml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true ``` PR 连续 push 时,老的 in-progress run 被 cancel,只跑最新。 省 CI 资源。 ## 9. secrets / env ```yaml env: NODE_ENV: production API_BASE_URL: https://api.example.com jobs: deploy: env: EXTRA: value steps: - env: STEP_LOCAL: x run: echo "$EXTRA $STEP_LOCAL ${{ secrets.AWS_KEY }}" ``` secrets 在 repo settings → Secrets and variables → Actions 配。 环境(environment)配的可加 approval gate: ```yaml jobs: deploy-prod: environment: production # 需要审批员点击 approve 才跑 steps: ... ``` ## 10. 自托管 runner(cost / hardware 控制) ```yaml runs-on: [self-hosted, linux, gpu] ``` 自己机器装 GitHub Actions runner agent → label 标记 → workflow 用 label 选择。免费层时间紧 / 需 GPU / 需大内存时省钱。 ## 11. 经验:调试 workflow ```bash # 本地跑 workflow(用 act) brew install act act -j test # 跑 test job act pull_request # 模拟 pr 事件 ``` act 用 Docker 模拟 GitHub Actions 环境。不 100% 一致(缺一些 GitHub service),但能本地快速迭代。 或者用 `tmate` 在 GitHub runner 里开 SSH 会话调试: ```yaml - name: SSH into runner if test fails if: failure() uses: mxschmitt/action-tmate@v3 with: detached: true limit-access-to-actor: true ``` step 失败时 GitHub 给你 SSH URL,进 runner 看现场。强烈推荐紧急调试用。 ## 12. cost 控制 - GitHub 免费层公开 repo 无限 minutes;私有 repo 每月 2000 minutes - 用 `concurrency` cancel 老 run - 选小机器(默认 ubuntu-latest 2-core;linux 私有可以选 `ubuntu-24.04-arm` 64 cores 但贵 16x) - 把 release / nightly build 放 self-hosted ## 效果 我们一个开源库 CI: - matrix build 9 组合并行,总时长 4 分钟(顺序跑要 30 分钟+) - cache 让 build 时间 80% 来自实际跑测试,不是装依赖 - reusable workflow 让 6 个相关 repo 共享配置,改一处生效全部 - composite action 把"setup uv + sync"封装,每 yml 少 10 行 ## 踩过的坑 1. **cache key 不带 lockfile hash**:依赖变了 cache 还用老的, bug 隐蔽。永远 `hashFiles('package-lock.json')` 之类。 2. **secrets 在 PR 不可见**:跨 fork PR 默认不传 secrets(防泄露)。 要 dependabot / fork PR 跑需要 deploy → 用 `workflow_run` triggered workflow 在 base repo 跑。 3. **windows runner 路径分隔**:脚本里 hardcode `/` 在 Windows 上 失败。用 cross-platform shell(bash 在 Windows runner 也有)+ `\` `/` 都用 `path.join`。 4. **timeout 默认 6 小时**:偶尔 hang 的 job 把 minutes 烧光。 `timeout-minutes: 30` 给每个 job 设上限。 5. **actions 升级 v3 → v4 breaking**:node 16 退役大批 actions 强制 升 node 20。看 GitHub deprecation notice + dependabot for actions 自动 PR 升级。

MSW (Mock Service Worker):前端开发 / 测试时拦截 HTTP 返回 mock

## 起因 后端 API 还没好,前端要先做 UI;或者测试时不想真打后端(速度 + 隔离)。 传统做法:在 fetch 调用处 `if (mocked) return mockData`,丑陋且要清理。 `MSW` 在 service worker / Node 层拦截 HTTP 请求,让"前端代码无修改 + 真的发请求 + 拦截返 mock 数据"。同一套 mock 给开发 / 测试 / Storybook 共用。 ## 解决方案 ### 装 ```bash npm i -D msw npx msw init public/ --save # 给浏览器生成 service worker 文件 ``` ### 写 handler ```ts // src/mocks/handlers.ts import { http, HttpResponse, delay } from 'msw' export const handlers = [ http.get('/api/users/:id', ({ params }) => { return HttpResponse.json({ id: params.id, name: 'Alice', email: '[email protected]', }) }), http.get('/api/users', async ({ request }) => { const url = new URL(request.url) const page = Number(url.searchParams.get('page') ?? 1) await delay(300) // 模拟网络延迟 return HttpResponse.json({ items: [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ], page, hasMore: page < 5, }) }), http.post('/api/login', async ({ request }) => { const body = await request.json() as { email: string; pw: string } if (body.email === '[email protected]' && body.pw === 'secret') { return HttpResponse.json({ token: 'fake-jwt-token' }) } return new HttpResponse(null, { status: 401 }) }), http.delete('/api/posts/:id', async () => { await delay(200) return new HttpResponse(null, { status: 204 }) }), ] ``` ### 浏览器集成(dev) ```ts // src/mocks/browser.ts import { setupWorker } from 'msw/browser' import { handlers } from './handlers' export const worker = setupWorker(...handlers) ``` ```ts // src/main.ts 入口 async function enableMocking() { if (import.meta.env.MODE !== 'development') return const { worker } = await import('./mocks/browser') return worker.start({ onUnhandledRequest: 'bypass', // 未 mock 的请求放行 }) } enableMocking().then(() => { // 启动你的 React / Vue app ReactDOM.createRoot(...).render(<App />) }) ``` `npm run dev`,所有 fetch / axios 调用被 MSW 拦截返 mock。 Console 显示: ``` [MSW] GET /api/users/1 (200 OK) [MSW] GET /api/users?page=1 (200 OK) ``` DevTools Network 也能看到这些请求(带 [from service worker] 标记)。 ### Node 集成(Jest / Vitest 测试) ```ts // src/mocks/server.ts import { setupServer } from 'msw/node' import { handlers } from './handlers' export const server = setupServer(...handlers) ``` ```ts // vitest.setup.ts (或 jest.setup.ts) import { server } from './src/mocks/server' beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) ``` 测试代码不需要 mock fetch,直接调真函数: ```ts test('login flow', async () => { const { getByLabelText, getByRole } = render(<LoginForm />) await userEvent.type(getByLabelText('Email'), '[email protected]') await userEvent.type(getByLabelText('Password'), 'secret') await userEvent.click(getByRole('button', { name: 'Sign in' })) await waitFor(() => { expect(localStorage.getItem('token')).toBe('fake-jwt-token') }) }) ``` MSW 在 Node 端拦截 fetch(Node 18+ 内置 fetch)/ axios,返 mock 数据。 ### 单测里 override handler ```ts test('shows error on 500', async () => { server.use( http.get('/api/users', () => { return new HttpResponse(null, { status: 500 }) }), ) const { findByText } = render(<UserList />) expect(await findByText(/something went wrong/i)).toBeInTheDocument() }) ``` `server.use(...)` 临时覆盖;每个测试 `resetHandlers()` 清回基准。 ### Storybook 集成 ```bash npm i -D msw-storybook-addon ``` `.storybook/preview.ts`: ```ts import { initialize, mswLoader } from 'msw-storybook-addon' initialize() export default { loaders: [mswLoader], } ``` 每个 story 设特定 handler: ```ts export const ErrorState: Story = { parameters: { msw: { handlers: [ http.get('/api/users', () => new HttpResponse(null, { status: 500 })), ], }, }, } ``` Storybook 里展示"网络错误"状态而不需要真实坏后端。 ### E2E (Playwright) ```ts import { test, expect } from '@playwright/test' test('login', async ({ page }) => { await page.route('/api/login', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ token: 'fake' }), }) }) await page.goto('/login') await page.fill('[name=email]', '[email protected]') await page.fill('[name=password]', 'secret') await page.click('button[type=submit]') await expect(page).toHaveURL('/dashboard') }) ``` Playwright 自己有 `page.route` 类似机制;用 MSW 时直接复用 handlers 更统一。 ### 动态响应(基于状态) mock 用户数据库 + 增删改: ```ts let users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] export const handlers = [ http.get('/api/users', () => HttpResponse.json(users)), http.post('/api/users', async ({ request }) => { const data = await request.json() as { name: string } const u = { id: Date.now(), name: data.name } users.push(u) return HttpResponse.json(u, { status: 201 }) }), http.delete('/api/users/:id', ({ params }) => { users = users.filter(u => String(u.id) !== params.id) return new HttpResponse(null, { status: 204 }) }), ] ``` 前端做 CRUD 流,mock 像真的在工作。 ### 与 OpenAPI 集成 ```bash npm i -D openapi-msw ``` ```ts import { createOpenApiMock } from 'openapi-msw' import type { paths } from './generated-api-types' const http = createOpenApiMock<paths>() export const handlers = [ http.get('/api/users/{id}', () => HttpResponse.json({ id: 1, name: 'Alice', // 类型完全跟着 OpenAPI schema,缺字段编译报错 })), ] ``` 后端给 OpenAPI 文档 → 前端类型 + mock 一起自动有,零手写。 ## 效果 - 后端 delay 一周时前端可独立开发 - 单测从"mock fetch 函数"升到"在网络层拦截",更接近真实场景 - Storybook 演示"loading / error / empty / 满数据"四种状态都有 - E2E 测试稳定(不依赖真后端 / 不污染数据库) - Mock data 文件被 dev / test / Storybook 共用,单一来源 ## 与替代品对比 | | MSW | json-server | nock | jest.mock('axios') | |---|---|---|---|---| | 拦截层 | network (sw / node) | 真后端 | node only | 模块 mock | | 浏览器支持 | ✅ | ✅ | ❌ | ❌ | | 真发 HTTP | ✅ | ✅ | ❌(模拟) | ❌ | | 灵活程度 | 极高 | 中(REST 模板) | 中 | 高 | | Storybook 集成 | ✅ | 难 | 无 | 无 | MSW 全面胜出,是 2024 后的事实标准。 ## 踩过的坑 1. **Service Worker 没注册成功**:`npx msw init` 没把 mockServiceWorker.js 放对位置(应在 publicly served root)。生产 build 后路径变也会失效。 2. **MSW 拦截了真实生产请求**:忘了 `if (MODE === 'development')` 包裹 `worker.start()`。线上版本不能注入 MSW。 3. **Node fetch vs cross-fetch**:MSW 在 Node 18+ 拦截 global fetch; 老 Node 用 `node-fetch` 不被拦截,要装 `@mswjs/interceptors`。 4. **测试间 handler 泄漏**:忘 `afterEach(() => server.resetHandlers())`, 上个测试 override 的 handler 影响下个测试。 5. **WebSocket 不支持**:MSW 主要拦截 HTTP;WebSocket 用 [mock-socket](https://github.com/thoov/mock-socket) 或专门的 ws mock 库。

OpenTelemetry Collector:统一收集 trace / metric / log

## 起因 可观测性 3 大 pillar: - **metric**:Prometheus + node_exporter / app exporter - **trace**:Jaeger / Tempo / Zipkin - **log**:Loki / ELK 每个数据类型一套 collector:promtail / vector / fluentd / filebeat / otel-trace 等。 应用要装 N 个 SDK,运维要管 N 套 agent。 **OpenTelemetry Collector** 统一:一个 binary 收 3 类数据 + 转发给后端。 应用用一套 OTEL SDK,agent 收一套 protocol。 ## 装 ```bash docker run -d -p 4317:4317 -p 4318:4318 \ -v $(pwd)/config.yaml:/etc/otelcol/config.yaml \ otel/opentelemetry-collector-contrib:latest ``` `config.yaml`: ```yaml receivers: otlp: protocols: grpc: { endpoint: 0.0.0.0:4317 } http: { endpoint: 0.0.0.0:4318 } prometheus: config: scrape_configs: - job_name: 'apps' static_configs: - targets: ['app:8080'] processors: batch: timeout: 10s exporters: otlphttp/jaeger: endpoint: http://jaeger:4318 prometheusremotewrite: endpoint: http://mimir:9009/api/v1/push loki: endpoint: http://loki:3100/loki/api/v1/push service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlphttp/jaeger] metrics: receivers: [otlp, prometheus] processors: [batch] exporters: [prometheusremotewrite] logs: receivers: [otlp] processors: [batch] exporters: [loki] ``` receiver → processor → exporter 流水线。 ## 应用接入 (Python) ```bash pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-bootstrap -a install # 自动装 instrumentation ``` ```bash # 启动应用 opentelemetry-instrument \ --traces_exporter otlp \ --metrics_exporter otlp \ --logs_exporter otlp \ --service_name myapp \ --exporter_otlp_endpoint http://otel-collector:4317 \ python app.py ``` 或者代码内: ```python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter provider = TracerProvider() provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpoint='otel-collector:4317')) ) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span('process_order'): # ... pass ``` auto-instrumentation 自动 trace Django / requests / SQLAlchemy 等。 ## 跨语言 JS / Java / Go / Ruby / .NET 都有 OTEL SDK。 全用 OTLP 发到同 collector → 后端汇总。 ## 处理器 (processor) ```yaml processors: attributes: actions: - key: env value: production action: insert - key: password # 敏感字段 action: delete filter: traces: span: - 'attributes["http.url"] == "/health"' # 过滤健康检查 trace tail_sampling: decision_wait: 10s policies: - name: errors type: status_code status_code: { status_codes: [ERROR] } - name: slow type: latency latency: { threshold_ms: 1000 } - name: sample type: probabilistic probabilistic: { sampling_percentage: 1 } ``` - attributes 加 / 删 tag - filter 丢弃噪音 span - tail sampling: 1% 抽样 + 100% 错误 + 100% 慢请求 → 节省后端存储 ## tail vs head sampling head sampling:trace 开始时决定要不要采集(应用端)。 tail sampling:trace 完成后看完整决定(collector 端)。 tail 优势:基于结果决定(错误 / 慢的全采,正常的 1%)。 缺点:collector 要 buffer 所有 trace 几秒。 ## deployment 模式 ``` agent (DaemonSet, 每 node) → gateway (Deployment, 集群级) → 后端 ``` - agent:每 node 一个,应用本地连,减少网络 - gateway:中央处理(采样 / batch / 多后端 fanout) 或者单层:应用 → collector → 后端(小集群)。 ## k8s 部署 (operator) ```bash helm install opentelemetry-operator open-telemetry/opentelemetry-operator ``` ```yaml apiVersion: opentelemetry.io/v1beta1 kind: OpenTelemetryCollector metadata: name: gateway spec: mode: deployment replicas: 3 config: | receivers: ... processors: ... exporters: ... ``` operator 管 deployment + 配置 reload。 ## 自动 instrumentation (k8s) ```yaml apiVersion: opentelemetry.io/v1alpha1 kind: Instrumentation metadata: name: python-instr spec: exporter: endpoint: http://otel-gateway:4317 python: image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python ``` ```yaml # pod annotation metadata: annotations: instrumentation.opentelemetry.io/inject-python: "true" ``` operator 自动 inject sidecar / init container → 应用零改动获得 trace / metric。 ## metric processing ```yaml processors: metricstransform: transforms: - include: http.server.duration action: update new_name: http_request_duration_seconds ``` OTEL metric 名换成 Prometheus 风格 → 兼容 Grafana 老 dashboard。 ## 与 vector 对比 vector (Datadog OSS) 是另一通用 telemetry pipeline: | | OTEL Collector | Vector | |---|---|---| | 标准 | OTEL(CNCF) | Datadog 自家 | | log | ✅ | ✅ 强 | | metric | ✅ | ✅ | | trace | ✅ 强 | 弱 | | 配置 | YAML | TOML | | 性能 | 中 | 极高(Rust) | trace 重 → OTEL。 log / metric pipeline + 极致性能 → Vector。 我用 OTEL 因为标准化优势 + 跨多后端。 ## 与 fluentd / fluent-bit fluent-bit / fluentd 主要 log shipper,metric / trace 弱。 新项目用 OTEL 一栈。 老 ELK 项目可能仍 fluent。 ## 真实 case 新项目从 0 设计 observability: ``` 应用(Python / Go / TS) + OTEL SDK auto-instrument ↓ OTLP gRPC otel-collector (DaemonSet, 每 node) ↓ OTLP otel-collector (gateway, 3 replica) ↓ ├─ trace → Tempo ├─ metric → Mimir └─ log → Loki ↓ Grafana 统一查看 ``` 一套 SDK + 一套 collector → 3 类数据 → Grafana 一处看(trace ID 关联 log 和 metric)。 trace ID 关联是杀手:error 看 trace → 同 trace ID 拉 log → 看 metric spike 时间窗口。debug 速度极大提升。 ## 踩过的坑 1. **OTLP gRPC vs HTTP**:默认 4317 是 gRPC,4318 HTTP。client 配错 端口报错。 2. **batch processor 太大**:batch 太大 latency 高 + OOM 风险。 `send_batch_size: 8192` 调。 3. **tail sampling 内存**:高 QPS 时 buffer 几秒 trace → 几 GB RAM。 gateway 单独 deployment,分配大内存。 4. **auto-instrument 性能**:某些 framework 全部 instrument 后 P99 涨。disable 不重要的(health check / metrics endpoint)。 5. **多 env tag 漏**:dev / staging / prod 数据混 → 难区分。 `resource_attributes: env=prod` 强制加。

PostgreSQL + pgvector 存 OpenAI / 本地 embeddings 做向量检索

RAG / 语义搜索的标准做法:把文档切成 chunk → 用 embedding model 转向量 → 存向量库 → 查询时 embedding 后 ANN 搜索。 向量库选项: - 专用:Qdrant / Milvus / Weaviate / Chroma - 通用 + 向量扩展:**PostgreSQL + pgvector** 如果你已经在用 PG,pgvector 是最省事的——一套数据库管业务数据 + 向量, 不引入新系统。下面是完整流程。 ## 1. 装扩展 ```bash # Debian / Ubuntu sudo apt install postgresql-16-pgvector # 或编译: # git clone https://github.com/pgvector/pgvector # cd pgvector && make && sudo make install ``` ```sql -- 在目标数据库里执行 CREATE EXTENSION IF NOT EXISTS vector; ``` ## 2. 建表 ```sql CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, source TEXT NOT NULL, chunk TEXT NOT NULL, embedding vector(1536), -- OpenAI text-embedding-3-small 维度 metadata JSONB, created_at TIMESTAMPTZ DEFAULT now() ); ``` `vector(N)` 是定长向量类型,N 必须匹配你的 embedding model 输出维度。 常见: - OpenAI `text-embedding-3-small`: 1536 - OpenAI `text-embedding-3-large`: 3072 - Cohere `embed-multilingual-v3`: 1024 - `bge-large-zh-v1.5`: 1024 - `bge-m3`: 1024 - `nomic-embed-text`: 768 ## 3. 插入 embedding ```python import psycopg from openai import OpenAI client = OpenAI() def embed(text): resp = client.embeddings.create(input=text, model='text-embedding-3-small') return resp.data[0].embedding # List[float] con = psycopg.connect('postgresql://localhost/mydb') text = 'PostgreSQL 是开源关系数据库...' emb = embed(text) con.execute( 'INSERT INTO documents (source, chunk, embedding) VALUES (%s, %s, %s)', ('manual.md', text, emb) ) con.commit() ``` psycopg3 + pgvector-python: ```bash uv add psycopg pgvector ``` ```python from pgvector.psycopg import register_vector register_vector(con) # 现在能直接传 numpy array / list 给 vector 字段 ``` ## 4. ANN 搜索 ```sql -- 找最相似的 5 条(距离最小) SELECT id, source, chunk, embedding <=> $1::vector AS distance FROM documents ORDER BY embedding <=> $1::vector LIMIT 5; ``` `<=>` 是余弦距离运算符。pgvector 还支持: - `<->`:欧氏距离 (L2) - `<#>`:内积负值(dot product 越大越相似,所以取负) 最常用的是 `<=>` 余弦距离,对长度归一化的 embedding 等价于内积。 ## 5. 索引:HNSW 或 IVFFlat 无索引时是 brute-force 扫表,100k 行还能用,百万级就慢。 建索引: ```sql -- HNSW(推荐,召回高) CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- IVFFlat(建索引快,召回略低) CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); ``` 索引必须匹配你的距离操作符: - `vector_cosine_ops` ↔ `<=>` - `vector_l2_ops` ↔ `<->` - `vector_ip_ops` ↔ `<#>` `lists` 推荐 `sqrt(rows)`,`ef_construction` 越大召回越好但建索引越慢。 ## 6. 查询时调召回 / 速度 ```sql -- HNSW SET hnsw.ef_search = 100; -- 默认 40;越大召回越好越慢 SELECT ... FROM documents ORDER BY embedding <=> $1 LIMIT 10; -- IVFFlat SET ivfflat.probes = 10; -- 默认 1;越大召回越好越慢 SELECT ... FROM documents ORDER BY embedding <=> $1 LIMIT 10; ``` 通常 `ef_search` 64-256 之间是甜点。 ## 7. 混合搜索(向量 + 全文 + 元数据过滤) 向量搜索的弱点:精确关键词容易丢。最佳实践是混合: ```sql -- 假设 chunk 上建了 to_tsvector('simple', chunk) 的 GIN 索引 WITH vector_results AS ( SELECT id, chunk, embedding <=> $1::vector AS dist FROM documents WHERE metadata->>'project' = $2 ORDER BY embedding <=> $1::vector LIMIT 50 ), fts_results AS ( SELECT id, chunk, ts_rank(to_tsvector('simple', chunk), plainto_tsquery('simple', $3)) AS rank FROM documents WHERE metadata->>'project' = $2 AND to_tsvector('simple', chunk) @@ plainto_tsquery('simple', $3) LIMIT 50 ) SELECT * FROM ( SELECT id, chunk, 1 - dist AS score FROM vector_results UNION ALL SELECT id, chunk, rank * 5 AS score FROM fts_results ) GROUP BY id, chunk ORDER BY MAX(score) DESC LIMIT 10; ``` 或者更精致用 RRF (Reciprocal Rank Fusion) 算法。 ## 8. 性能数据(参考) | 量级 | brute force | HNSW | |---|---|---| | 10k 行 | 10-50ms | < 5ms | | 100k | 100-500ms | 10ms | | 1M | 几秒 | 20-50ms | | 10M | 60s+ | 100ms | 1000 万向量是 PostgreSQL + pgvector 大致甜点。再大上 Qdrant / Milvus。 ## 9. 批量插入 ```python from pgvector.psycopg import register_vector register_vector(con) # 批量 with con.cursor() as cur: cur.executemany( 'INSERT INTO documents (source, chunk, embedding) VALUES (%s, %s, %s)', [(s, c, e) for s, c, e in zip(sources, chunks, embeddings)] ) con.commit() ``` 千条以上用 `COPY ... FROM STDIN`,10x 速度。 ## 10. 用 Django ```python # settings: 装 'pgvector.django' 应用 from pgvector.django import VectorField, HnswIndex class Document(models.Model): source = models.CharField(max_length=200) chunk = models.TextField() embedding = VectorField(dimensions=1536) class Meta: indexes = [ HnswIndex( name='doc_emb_hnsw', fields=['embedding'], m=16, ef_construction=64, opclasses=['vector_cosine_ops'], ), ] # 查询 from pgvector.django import CosineDistance Document.objects.alias(d=CosineDistance('embedding', query_emb)).order_by('d')[:10] ``` ## 踩过的坑 - 维度不匹配:插 1024 维向量到 `vector(1536)` 字段会报错。 embedding model 一定要固定,换 model 必须重建索引。 - HNSW 索引构建非常慢且耗内存(10M 行可能要几小时 + 10GB+ 内存)。 生产建议在低峰期 `CREATE INDEX CONCURRENTLY`。 - pgvector 不存储原文,只存向量:要返回相关文档需要把原文也存表里。 - 别在 vector 列上做 `WHERE` 条件而不带 ORDER BY ... LIMIT: 全表扫的 vector 距离计算极慢。索引只在 ORDER BY 配 LIMIT 时生效。

React Hook Form + Zod:性能 + 类型安全的表单方案

React 表单的痛点: - 每次输入触发整页 re-render(useState 模式) - 校验逻辑分散 - 类型推导 / 错误提示不一致 - 异步校验 / 提交状态难管 `react-hook-form` (RHF) + `zod` 组合是 2024 业界共识: RHF 用 ref 而非 state 避免重渲,zod 提供 schema-first 校验 + 类型推导。 ## 安装 ```bash npm i react-hook-form zod @hookform/resolvers ``` ## 5 行版 ```tsx import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' const schema = z.object({ email: z.string().email('邮箱格式不对'), password: z.string().min(8, '密码至少 8 位'), age: z.coerce.number().int().min(0).max(150), }) type FormData = z.infer<typeof schema> function LoginForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm<FormData>({ resolver: zodResolver(schema), defaultValues: { email: '', password: '', age: 18 }, }) const onSubmit = async (data: FormData) => { await fetch('/api/login', { method: 'POST', body: JSON.stringify(data) }) } return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('email')} placeholder="邮箱" /> {errors.email && <p>{errors.email.message}</p>} <input type="password" {...register('password')} /> {errors.password && <p>{errors.password.message}</p>} <input type="number" {...register('age')} /> {errors.age && <p>{errors.age.message}</p>} <button disabled={isSubmitting}>登录</button> </form> ) } ``` 注意: - `z.infer<typeof schema>` 自动推 FormData 类型,schema 改字段自动同步 - `register('email')` 把 input 注册到表单系统,不用 useState - 提交时 zod 校验,错的字段进 `errors` ## 性能:只渲染必要部分 ```tsx import { useFormState, useWatch } from 'react-hook-form' function EmailPreview({ control }) { // 只在 email 字段变了时重渲,整个表单不动 const email = useWatch({ control, name: 'email' }) return <div>预览: {email}</div> } ``` ## 嵌套 / 数组字段 ```ts const schema = z.object({ name: z.string(), emails: z.array(z.object({ address: z.string().email(), primary: z.boolean(), })).min(1), }) ``` ```tsx import { useFieldArray } from 'react-hook-form' const { fields, append, remove } = useFieldArray({ control, name: 'emails' }) return ( <> {fields.map((f, i) => ( <div key={f.id}> <input {...register(`emails.${i}.address`)} /> <input type="checkbox" {...register(`emails.${i}.primary`)} /> <button onClick={() => remove(i)}>×</button> </div> ))} <button onClick={() => append({ address: '', primary: false })}>加</button> </> ) ``` `useFieldArray` 优化动态列表场景,比手动 `useState<Email[]>` 性能好。 ## 复杂校验 ### 跨字段 ```ts const schema = z.object({ password: z.string().min(8), confirm: z.string(), }).refine(d => d.password === d.confirm, { message: '两次密码不一致', path: ['confirm'], }) ``` ### 异步(如检查用户名是否已存在) ```ts const schema = z.object({ username: z.string().min(3).refine(async (val) => { const r = await fetch(`/api/check-username?u=${val}`) return (await r.json()).available }, { message: '用户名已被占用' }), }) ``` zod 支持 async refine,RHF 自动 await。 ### transform / coerce ```ts const schema = z.object({ // <input type="number" /> 实际是字符串,z 转 number age: z.coerce.number().int(), // 字符串前后去空格 name: z.string().trim().min(1), // 把 "yes"/"no" 字符串变 boolean consent: z.string().transform(v => v === 'yes'), }) ``` ## 异步提交状态 ```tsx const { handleSubmit, formState: { isSubmitting, isSubmitSuccessful } } = useForm(...) return ( <form onSubmit={handleSubmit(onSubmit)}> ... <button disabled={isSubmitting}> {isSubmitting ? '提交中...' : '提交'} </button> {isSubmitSuccessful && <p>成功!</p>} </form> ) ``` ## 服务端错误回填 ```tsx const { setError } = useForm(...) const onSubmit = async (data) => { try { await api.create(data) } catch (e) { if (e.code === 'EMAIL_TAKEN') { setError('email', { type: 'manual', message: '邮箱已注册' }) } } } ``` ## 与 UI 组件库(Radix UI / shadcn / Mantine)配合 ```tsx import { Controller } from 'react-hook-form' <Controller control={control} name="country" render={({ field }) => <Select {...field} options={countries} />} /> ``` `Controller` 把不支持 ref 的受控组件包起来。 ## 共用 schema 到后端 zod schema 是纯 TypeScript,可以在 FastAPI / Express / Hono / tRPC 后端 共用。前后端用同一份校验,类型一致: ```ts // shared/schema.ts export const userSchema = z.object({ ... }) // frontend useForm({ resolver: zodResolver(userSchema) }) // backend (hono) import { zValidator } from '@hono/zod-validator' app.post('/api/user', zValidator('json', userSchema), (c) => { ... }) ``` ## 何时不用 RHF 简单 1-2 字段的表单直接 `useState` 就够,引入 RHF 反而麻烦。 3+ 字段 + 校验 + 异步提交时 RHF 收益最大。 ## 踩过的坑 - 忘了 `defaultValues`:第一次渲染 input value 是 undefined,React 报 "uncontrolled to controlled" 警告。永远显式给 defaultValues。 - `mode: 'onChange'` 让每次输入都校验 → 慢且打扰用户。默认 'onSubmit' 最稳;onBlur 是折中。 - 跨大版本升 RHF:v6 → v7 API 变化大。lockfile + 一次性升级。 - zod refine 异步:会让 onChange 校验变慢;尽量 onBlur 异步校验。

用 tcpdump 抓包 + Wireshark 复现一个 TCP RST 问题

应用日志里出现 "connection reset by peer",应用端代码看着没问题, 对端代码看着也没问题。这类问题 70% 是中间链路(LB / 防火墙 / 反代) 干的。抓包是唯一直接定位的办法。 ## 1. 收集足够的现场信息 before 开抓之前,先列清楚四元组: ```bash # 客户端 ss -tan state established '( dport = :8080 or sport = :8080 )' # 服务端 ss -tan state established '( sport = :8080 )' ``` 记下 src / dst IP + port。 ## 2. tcpdump 命令 服务端: ```bash sudo tcpdump -i any \ -s 0 \ -w /tmp/svr.pcap \ 'tcp port 8080' ``` 客户端(如果可以同时抓): ```bash sudo tcpdump -i eth0 -s 0 -w /tmp/cli.pcap 'host <server-ip> and tcp port 8080' ``` - `-s 0` 不截断 payload(默认 96 字节,分析 HTTP / TLS 不够看) - `-i any` 在 Linux 上抓所有接口(不知道走哪个就用这个) - 写文件比直接终端打印好 100 倍 —— 终端文本格式会丢字段、丢顺序 跑出问题后 Ctrl-C 停。 ## 3. 快速浏览 ```bash # 文本看一眼 tcpdump -r /tmp/svr.pcap -n -nn -c 20 # 只看 RST 包 tcpdump -r /tmp/svr.pcap -n 'tcp[tcpflags] & tcp-rst != 0' # 看哪些会话有 RST tshark -r /tmp/svr.pcap -Y 'tcp.flags.reset == 1' \ -T fields -e tcp.stream -e ip.src -e ip.dst | sort -u ``` `tcp.stream` 是 Wireshark 给每个 TCP 会话的整数 ID。找到出问题的 stream 号 (比如 7): ```bash tshark -r /tmp/svr.pcap -Y 'tcp.stream == 7' -V | less ``` ## 4. 用 Wireshark 看 ```bash scp server:/tmp/svr.pcap . wireshark svr.pcap ``` 经验流程: 1. **Statistics → Conversations → TCP**,看哪些会话异常短或有 RST 2. 右键问题会话 **Follow → TCP Stream**:看应用层 payload,能判断 RST 是发生在哪条请求 / 哪个字节边界 3. **Expert Information**(左下角的⚠图标):Wireshark 自动标注的 异常(previous segment lost, dup ACK, RST 等) ## 5. RST 的常见根因 | 现象 | 根因 | |---|---| | 服务端发的 RST,紧跟在 FIN 之后 | 应用没读完 socket 缓冲就 close(HTTP body 没消费) | | 客户端发的 RST,应用层没异常 | 客户端 OS RST:socket 被强制回收(fd 泄露后被 GC) | | 中间设备发的 RST,TTL 远小于真实端 | 防火墙 / 流量清洗设备主动断 | | 握手后立刻 RST | TCP wrappers / hostsdeny;或对端只接受 IPv4 而你发的 IPv6 | | 大块上传中途 RST | LB 的 buffer overflow / 限流;或 MTU 不匹配(Path MTU Black Hole) | ## 6. MTU 问题的快速验证 如果怀疑 MTU: ```bash # 强制最大 1500 - 28 = 1472 字节 payload,不允许分片 ping -M do -s 1472 <server> # 二分缩小直到能通 ping -M do -s 1400 <server> ``` `ping ... fragmentation needed` 就是被某段链路要求分片但你设了 DF(不分片)。 通常出现在 PPPoE / VPN / 部分隧道场景。 ## 7. 持续抓包(环形缓冲) 排查间歇性问题时,连续抓不停但不要把磁盘塞满: ```bash sudo tcpdump -i any -s 0 \ -w /tmp/sniff-%Y%m%d-%H%M%S.pcap \ -G 600 \ -W 24 \ 'tcp port 8080' ``` `-G 600` 每 10 分钟切新文件;`-W 24` 最多保留 24 个,循环覆盖。 ## 踩过的坑 - 抓包文件别忘了清理 —— `-s 0` 在繁忙服务上一小时能写 GB 级。 - tshark `-Y` 是显示过滤器(capture 后筛选),`-f` 是 capture 过滤器 (抓的时候用)。语法不一样,别混。`-Y 'tcp.port == 8080'` vs `-f 'tcp port 8080'`。 - TLS 流量看不到明文,配合 SSLKEYLOGFILE 让浏览器/curl 把会话 key 落地, Wireshark 用这个 key 文件就能解密(Edit → Preferences → Protocols → TLS)。 - 抓本机环回流量在 Linux 上需要 `-i lo`,`-i any` 偶尔会漏。