知识广场

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

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

用 Restic 加密备份到 S3 兼容存储(含还原演练)

Restic 是单文件 Go 二进制,做客户端加密 + 内容寻址 dedup + 增量备份。 对比 rsync 优势: - 加密在客户端完成,存储端只能看到加密块 - 强 dedup:跨快照 / 跨主机 / 跨文件位置都生效 - 任何 S3 兼容存储(AWS S3 / Backblaze B2 / Wasabi / minio / Cloudflare R2)都支持 - 单二进制零运行时依赖 ## 安装 ```bash # Debian / Ubuntu 自带的版本一般偏旧,建议直接装官方二进制 RESTIC_VER=0.17.3 ARCH=$(dpkg --print-architecture) # amd64 / arm64 curl -fsSL "https://github.com/restic/restic/releases/download/v${RESTIC_VER}/restic_${RESTIC_VER}_linux_${ARCH}.bz2" \ | bunzip2 > /usr/local/bin/restic sudo chmod +x /usr/local/bin/restic restic version ``` ## 初始化仓库(一次) 以 Backblaze B2 为例(最便宜的 S3-API 兼容方案之一): ```bash export B2_ACCOUNT_ID=... export B2_ACCOUNT_KEY=... export RESTIC_REPOSITORY=b2:my-bucket:/host-foo export RESTIC_PASSWORD=$(openssl rand -base64 32 | tr -d '\n') echo "$RESTIC_PASSWORD" | sudo tee /etc/restic.pw && sudo chmod 600 /etc/restic.pw restic init ``` **`RESTIC_PASSWORD` 丢了仓库就回不来了**,没有任何官方 recover —— 一定要 把它存到一个独立的 password manager / 离线纸质备份。 ## 备份脚本 ```bash #!/usr/bin/env bash # /usr/local/sbin/restic-backup.sh set -euo pipefail export B2_ACCOUNT_ID=... export B2_ACCOUNT_KEY=... export RESTIC_REPOSITORY=b2:my-bucket:/host-$(hostname -s) export RESTIC_PASSWORD_FILE=/etc/restic.pw restic backup \ --tag daily \ --exclude-file /etc/restic.exclude \ /etc /srv /home/yourname/projects # 保留策略:每日 7、周 4、月 12、年 5 restic forget --prune \ --keep-daily 7 --keep-weekly 4 \ --keep-monthly 12 --keep-yearly 5 # 完整性校验(采样 10%,全量太慢) restic check --read-data-subset=10% ``` `/etc/restic.exclude` 例: ``` node_modules __pycache__ *.pyc .cache .venv venv target build dist ``` ## systemd timer ```ini # /etc/systemd/system/restic-backup.timer [Timer] OnCalendar=*-*-* 02:30:00 RandomizedDelaySec=1h Persistent=true [Install] WantedBy=timers.target ``` ## 还原演练(重要——做一次) ```bash # 列出所有快照 restic snapshots # 列某个快照的文件 restic ls latest /etc # 还原某文件到临时目录 restic restore latest --include /etc/nginx --target /tmp/restore-test # 直接挂载为只读 FUSE mkdir /tmp/rmount restic mount /tmp/rmount & ls /tmp/rmount/snapshots/ fusermount -u /tmp/rmount ``` **至少每季度做一次完整还原演练**到一台不同的机器,确认凭据和流程都对。 未经演练的备份等于没有备份。 ## 监控 加在脚本末尾: ```bash # 通知 healthchecks.io 跑完了 curl -fsSL --retry 3 "https://hc-ping.com/<uuid>" # 或者推 Prometheus pushgateway cat <<EOF | curl --data-binary @- http://pushgw:9091/metrics/job/restic/host/$(hostname) restic_backup_last_success_time $(date +%s) restic_backup_last_size_bytes $(restic stats latest --mode raw-data --json | jq .total_size) EOF ``` ## 踩过的坑 - 第一次备份会很慢(全量上传),后续增量秒级;不要担心。 - `restic prune` 在大仓库(TB 级)上非常慢且 I/O 密集;改成 `restic forget --keep-* --prune-max-unused 5%` 限制每次 prune 范围。 - 仓库锁残留:客户端被 kill 后会留 `lock-*` 文件,下次报"repo is locked"。 确认没有其他进程后 `restic unlock`。 - B2 / R2 的 API 调用收费在小文件场景容易超出预期,配 `restic backup --pack-size 32` 增加 pack 大小(默认 16 MB)能减少调用次数。

本地跑 Stable Diffusion:ComfyUI + 模型管理 + 工作流复用

## 起因 想生成产品配图,又不想把数据传 Midjourney / DALL-E。Stable Diffusion 是 开源文生图模型,本地一张 8GB+ 显存的卡能跑。 WebUI 老牌但 UI 笨重;ComfyUI 用 node-based 工作流(像 Blender 的节点编辑器), 更适合复杂 pipeline + 跨实验复用。 ## 解决方案 ### 装 ComfyUI ```bash git clone https://github.com/comfyanonymous/ComfyUI cd ComfyUI uv venv --python 3.12 source .venv/bin/activate # CUDA 12.x uv pip install torch torchvision --index https://download.pytorch.org/whl/cu124 uv pip install -r requirements.txt # 起服务 python main.py --listen 0.0.0.0 --port 8188 # 浏览器打开 http://localhost:8188 ``` 第一次进去是空白画布。 ### 装模型 ComfyUI 的目录结构: ``` ComfyUI/models/ ├── checkpoints/ # 基础模型 (.safetensors) ├── loras/ # LoRA 微调(风格 / 角色) ├── vae/ # VAE 解码器 ├── controlnet/ # ControlNet 模型 ├── upscale_models/ # 超分模型 └── embeddings/ # textual inversion ``` 从 [civitai.com](https://civitai.com) 或 HuggingFace 下: ```bash cd models/checkpoints # SDXL 1.0 base(6.5 GB) wget https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors # 流行的写实风:Juggernaut XL(7 GB) # 流行的二次元:Pony Diffusion XL(7 GB) # 流行的快出图:SDXL Turbo(lightning 版,4 步出图) ``` ### 第一个工作流 ComfyUI 默认工作流:Load Checkpoint → CLIP Text Encode (Prompt) → CLIP Text Encode (Negative) → Empty Latent Image → KSampler → VAE Decode → Save Image. 右键画布 → Add Node → 选模块拖入。Connect 各节点 output → input。 prompt 例: ``` positive: a serene Japanese garden with cherry blossoms, golden hour, ultra detailed, 8k negative: ugly, blurry, watermark, low quality ``` KSampler 参数: - `steps`: 20-30(普通模型)/ 4-8(Turbo / Lightning) - `cfg`: 7(普通) / 1-2(Turbo) - `sampler_name`: dpmpp_2m_sde / euler_ancestral - `scheduler`: karras / normal `Queue Prompt` 跑一次。 ### 工作流保存 / 分享 `Save (JSON)` 把整个 graph 存成 `.json`。 团队里"哪个工作流出图最好" → JSON 文件共享,对方 `Load` 即可复现。 ComfyUI 还把生成的 PNG 嵌入了 metadata:图片本身包含生成它的完整 workflow。 拖任意 PNG 到 ComfyUI 画布 → 自动还原工作流。神奇。 ### LoRA 风格切换 ``` Load Checkpoint → Load LoRA → CLIP Text Encode → ... ``` `Load LoRA` 节点选 `pony_anime_v3.safetensors`,strength 0.7, 风格立刻切到该 LoRA 训练的风格。多个 LoRA 可以串联。 ### ControlNet(按草图 / 边缘 / 姿态约束) ``` Load Image → Canny Edge → Apply ControlNet (Advanced) ↓ Load Checkpoint → ... → KSampler (with controlnet conditioning) → ... ``` 可以从一张参考图提取边缘 / 姿态 / 深度,让生成图保持同样构图。 广告 / 产品设计常用:用线稿生成 100 种渲染风格。 ### 批量生成 `KSampler` 的 `batch_size` 设 4 → 一次跑出 4 张。 `Save Image` 节点自动命名 `00001.png`、`00002.png`。 `Queue Prompt` 多次连续跑: ``` Queue → 队列 5 → 跑完 5 批 = 20 张图 ``` 跑久后图片在 `ComfyUI/output/`。 ## 效果 - 本地 RTX 4090:SDXL 1024×1024 单图 ~5s(30 steps),batch 4 ~15s - SDXL Lightning:~1.5s 单图 - 月生成量 1k+ 张零成本 - 工作流文件版本化,"上次客户喜欢的那个风格" 一键复现 - 同组设计师共享 lora + 工作流 json,统一风格 ## 性能 tips - **--lowvram** flag 启动:8GB 卡能跑 SDXL(变慢但能跑) - **--use-pytorch-cross-attention**:xformers 不兼容时的替代 - **VAE FP16**:`--fp16-vae` 减半解码显存 - **TAESD**:`models/vae_approx/` 预览快但质量低,调试用 ## 与 WebUI / API 对比 | | A1111 WebUI | ComfyUI | 商业 API | |---|---|---|---| | UI | 表单 | Node graph | Prompt only | | 工作流复用 | 弱 | 极强 | 无 | | ControlNet / Inpaint | ✅ | ✅ | 部分 | | 复杂 pipeline | 难 | 极易 | 不行 | | 学习曲线 | 低 | 中 | 极低 | | 性能 | 好 | 更好(无 Gradio overhead) | N/A | 简单文生图选 WebUI;任何"图 → 处理 → 再处理 → 多阶段"的工作流选 ComfyUI。 ## 踩过的坑 1. **CUDA OOM 但 nvidia-smi 显示有显存**:PyTorch caching allocator 碎片。`--gpu-only` 全部组件放 GPU,或者 `--cpu-vae` 把 VAE 移 CPU (VAE decode 慢但省 2GB)。 2. **首次加载某 model 极慢**:safetensors 文件几 GB,第一次读盘 + 装入 GPU。后续 cache 命中秒级。 3. **不同 model 的最佳 prompt 不一样**:SDXL 跟 SD 1.5 prompt 风格完全 不同。每个 model 看 civitai 上的"showcase prompt"参考。 4. **negative prompt 误用**:写过多 negative(比如 "bad anatomy, ugly, blurry, low quality, worst quality, ...")反而效果差。3-5 个核心 negative 就够。 5. **生成的图有水印 / artifact**:训练数据里有 watermark 的 model 常产出"假水印"。换 model 或在 prompt 加 `(watermark:1.5)` 进 negative。

PostgreSQL EXPLAIN ANALYZE 读法 + 找慢查询的根因

应用慢 90% 是 DB 查询慢。能读 EXPLAIN ANALYZE 输出是 SQL 调优的前置技能。 ## 1. 一个最简单的例子 ```sql EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]'; ``` 输出: ``` Index Scan using users_email_idx on users (cost=0.43..8.45 rows=1 width=84) (actual time=0.025..0.026 rows=1 loops=1) Index Cond: ((email)::text = '[email protected]'::text) Planning Time: 0.123 ms Execution Time: 0.045 ms ``` 读懂这几个字段: - `Index Scan using users_email_idx`:用了哪个索引(好) - `cost=0.43..8.45`:估算的相对开销(启动..总) - `rows=1`:估算返回行数 - `actual time=0.025..0.026`:**实际**启动时间..完成时间(毫秒) - `rows=1 loops=1`:实际行数 + 循环次数 `Execution Time` 是最终关注点。 ## 2. 看到 Seq Scan 警觉 ``` Seq Scan on orders (cost=0.00..18334.00 rows=987 width=58) (actual time=0.123..123.456 rows=982 loops=1) Filter: (status = 'pending') Rows Removed by Filter: 999018 ``` Seq Scan = 全表扫描。1M 行表里筛 1k 行,扫了 1M 行。 解决:给 status 建索引。 ```sql CREATE INDEX idx_orders_status ON orders (status); ``` 但 PG 优化器有时仍选 Seq Scan(如果 status='pending' 占行数比例 > 5-10% PG 会觉得全扫更快——是对的,索引扫 + 回表的成本可能更高)。 ## 3. estimated vs actual 行数差异 ``` Bitmap Heap Scan on events (cost=... rows=1) (actual time=... rows=12345 loops=1) ``` `rows=1` 估算 vs `rows=12345` 实际,差 4 个数量级。 说明统计信息陈旧或不准。 ```sql ANALYZE events; -- 更激进 ALTER TABLE events ALTER COLUMN type SET STATISTICS 1000; ANALYZE events; ``` `STATISTICS` 默认 100,加大让统计直方图更细。 PG 自动 ANALYZE 大量更新后触发,但 batch ETL 后建议显式 ANALYZE。 ## 4. Nested Loop / Hash Join / Merge Join ``` Hash Join (cost=... ) Hash Cond: (o.user_id = u.id) -> Seq Scan on orders o (...) -> Hash (...) -> Seq Scan on users u (...) ``` 三种 JOIN 策略: - **Nested Loop**:外层循环 + 内层索引查找。适合小集合 + 内表有索引 - **Hash Join**:内表建 hash 表,外表 lookup。适合两边都大的等值连接 - **Merge Join**:两边排序后归并。适合已排序数据 PG 自动选;策略错(如 nested loop 跑 1M × 1M)会爆。 慢的话试着调 `work_mem`(影响 Hash 是否能放内存): ```sql SET work_mem = '256MB'; ``` ## 5. Bitmap Index Scan ``` Bitmap Heap Scan on logs (cost=... rows=10000) -> Bitmap Index Scan on logs_date_idx (cost=...) Index Cond: (date >= '2024-01-01' AND date < '2024-02-01') ``` Bitmap scan 是"读索引拿到所有匹配的 row position 后批量去表里取"。 适合中等选择度(1-30% 行匹配)。Index Scan 适合更小(< 1%), Seq Scan 适合大量(> 30%)。 ## 6. EXPLAIN ANALYZE BUFFERS ```sql EXPLAIN (ANALYZE, BUFFERS) SELECT ... ``` 输出加上: ``` Buffers: shared hit=128 read=4520 written=12 ``` - `hit`:命中 shared buffer(内存里的 PG cache) - `read`:从磁盘读 - `dirtied` / `written`:脏页 / 写盘 `read` 数字大 = 慢查询是 I/O 受限的。如果同一查询第二次跑 read 变 hit, 就是缓存效应,单次测试不准。 ```sql -- 重置缓存 + 多次跑取平均 EXPLAIN (ANALYZE, BUFFERS) SELECT ...; -- 跑 3-5 次 ``` ## 7. EXPLAIN (ANALYZE, VERBOSE, FORMAT JSON) ```sql EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON) SELECT ...; ``` JSON 格式适合丢进可视化工具,如: - [explain.dalibo.com](https://explain.dalibo.com/) - [explain.depesz.com](https://explain.depesz.com/) - pgMustard 把 EXPLAIN 输出粘进去,图形化展示树 + 高亮慢节点。强烈推荐。 ## 8. 慢查询日志 `postgresql.conf`: ``` log_min_duration_statement = 100 # ms,超过就记录 log_duration = on log_statement_stats = off ``` ```bash sudo systemctl reload postgresql sudo tail -f /var/log/postgresql/postgresql-*.log ``` 或用 pg_stat_statements 扩展看 Top N 慢查询: ```sql CREATE EXTENSION pg_stat_statements; SELECT query, calls, total_exec_time, mean_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20; ``` `pg_stat_statements` 是生产 PG 的必备扩展之一。 ## 9. 几个常见慢查询根因 ### A. 缺索引 ```sql EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42; -- Seq Scan + Rows Removed by Filter: 多 -- 解:CREATE INDEX idx_orders_user_id ON orders (user_id); ``` ### B. 索引顺序错了 ```sql -- 索引 (a, b, c) SELECT ... WHERE b = ? -- ❌ 跳过 a,索引用不上 SELECT ... WHERE a = ? AND b = ? -- ✅ 用上索引前缀 ``` ### C. 不能用 SARGable 表达式 ```sql -- ❌ 函数应用在列上,索引无效 SELECT ... WHERE lower(email) = '[email protected]' -- ✅ 用函数索引 CREATE INDEX idx_users_email_lower ON users (lower(email)); ``` ### D. JOIN 时 row estimate 严重偏差 → ANALYZE 更新统计;或者改写 query 让 PG 估算更准。 ### E. 大表 DELETE/UPDATE 没回收空间 ```sql VACUUM ANALYZE big_table; VACUUM FULL big_table; -- 会锁表,谨慎 ``` ## 10. 索引大概念 | 索引类型 | 适合 | |---|---| | btree | 默认;等值、范围、ORDER BY | | hash | 仅等值(PG 10+ 才 WAL safe) | | gin | 数组、jsonb、全文搜索 | | gist | 几何、范围类型 | | brin | 大表 + 数据有物理顺序(如 append-only 日志) | 99% 用 btree。 ## 11. 部分索引(partial index) ```sql -- 只索引 active 的订单 CREATE INDEX idx_orders_pending_user ON orders (user_id) WHERE status = 'pending'; ``` 索引体积小、维护便宜,但只在查询条件包含 `status='pending'` 时生效。 ## 12. 表达式索引 ```sql CREATE INDEX idx_users_created_month ON users ((date_trunc('month', created_at))); -- 查询用同样表达式才命中 SELECT count(*) FROM users WHERE date_trunc('month', created_at) = '2024-01-01'; ``` ## 踩过的坑 - 用 EXPLAIN(不带 ANALYZE)只看估算,不真跑 → 估算和实际差很多时 误导。诊断必带 ANALYZE。 - EXPLAIN ANALYZE 在 INSERT/UPDATE/DELETE 真的执行!要 dry-run 包 事务:`BEGIN; EXPLAIN ANALYZE ...; ROLLBACK;` - "我加了索引为啥还 Seq Scan":PG 觉得 Seq Scan 更快(数据量 / 选择度 问题);ANALYZE 表 + 看 actual stats。 - VACUUM FULL 锁表,生产慎用。日常依赖 autovacuum;不够用 pg_repack 在线 repack。

direnv:进目录自动 load env(再也不 source 错文件)

## 起因 每个项目不同 env: - 项目 A:DATABASE_URL=postgres://localhost/a, API_KEY=xxx - 项目 B:DATABASE_URL=postgres://localhost/b, AWS_PROFILE=client 老办法: - `source .env`(每次开新 shell 要重 source) - `.envrc` shell script `. ./envrc`(容易忘) - `dotenv` lib 在应用里读(CLI 工具读不到) `direnv` 解决:进目录自动 export env,出目录自动 unset。 ## 装 ```bash brew install direnv apt install direnv # shell 集成 eval "$(direnv hook zsh)" # 加到 .zshrc ``` ## 用 ```bash cd ~/projects/myapp echo 'export DATABASE_URL=postgres://localhost/myapp' > .envrc direnv allow # 首次要批准 cd .. # DATABASE_URL unset cd ~/projects/myapp # DATABASE_URL 自动 set ``` `.envrc` 是 bash script,可以做任何事。 ## 安全 `.envrc` 是任意 shell code → 安全风险。 direnv 强制 `direnv allow` 才执行 → 你 review 后启用。 文件改了 → 重新 allow。 ```bash cd ~/some-random-project # direnv: error .envrc is blocked. Run `direnv allow` to approve its content ``` ## 加载 .env ```bash # .envrc dotenv # 加载同目录 .env 文件 ``` 或者: ```bash dotenv .env.local ``` 支持标准 .env 格式: ``` DATABASE_URL=postgres://... SECRET_KEY=abc ``` ## venv 自动 activate ```bash # .envrc layout python python3.12 # 自动建 venv + activate ``` 或者用现有 venv: ```bash # .envrc source .venv/bin/activate ``` 进项目目录 → venv 自动激活 + env vars 加载。 不用记 `source .venv/bin/activate`。 ## Node 版本 ```bash # .envrc use node 20 # mise / nvm 集成 ``` 或者: ```bash # .envrc PATH_add ./node_modules/.bin # 加到 PATH,能调 local tool ``` `PATH_add` 在前 → 项目 local 工具优先。 ## 多层 .envrc ``` ~/.envrc # 全局(如 EDITOR=nvim) ~/projects/.envrc # 所有 project 共享(如 PNPM_HOME=...) ~/projects/myapp/.envrc # 项目特定 ``` 进 myapp → 3 个 .envrc 合并(深层覆盖浅层)。 ## 与 dotenv-cli / 应用层 dotenv 应用层 `dotenv` lib 只对应用进程有效。 direnv 对 **shell 内任何命令** 有效(如手动 `psql`, `aws cli` 等)。 混用:direnv 给 shell + 应用 dotenv 给 production deploy(不用 direnv 在 server)。 ## 与 mise mise 也支持 dotenv-load: ```toml # .mise.toml [env] _.file = ".env" ``` 两个工具能 overlap。我用:mise 管 runtime 版本,direnv 管 env vars。 都装 fine(启动 hook 不冲突)。 ## 安全 - secret in git `.env` 通常 gitignore,`.envrc` 也 gitignore 较好(避免 secret 进 git)。 template: ```bash # .envrc.example (commit 这个) export DATABASE_URL=postgres://localhost/myapp export API_KEY=<your-key> # .envrc (gitignore,每人自己 cp 自己改) ``` 或者用 1Password CLI 拉 secret: ```bash # .envrc export API_KEY="$(op read 'op://Personal/myapp/api_key')" ``` 执行 direnv 时去 1Password 拉,不 commit 实际值。 ## CI CI 通常不用 direnv(用 env var injection)。但 direnv 的 .envrc 可以 被 CI 间接利用: ```bash # CI step source .envrc ``` 或者把 .envrc 改成 plain .env 用 dotenv CLI 跑。 ## 真实工作流 我每个项目 .envrc: ```bash dotenv .env.local # local 配置 layout python # venv PATH_add ./bin # 项目 script PATH_add ./node_modules/.bin export PYTHONUNBUFFERED=1 export AWS_PROFILE=$(basename "$PWD") # AWS profile = 项目名 ``` 进项目 → venv + env + AWS profile + PATH 一键就绪。 1 秒切换项目 context。 ## 与 docker compose env_file 对比 ```yaml # docker-compose.yml services: app: env_file: .env ``` compose 自动 load .env。但只对 compose 启动的容器有效。 shell 里手动 psql / debug 仍要 direnv。 ## 性能 direnv hook 在 shell prompt 前跑(PROMPT_COMMAND)。 单次 < 5ms(cache)。 首次进新目录加载 .envrc 慢点(看 .envrc 复杂度)。 ## 踩过的坑 1. **没装 hook**:source 了 hook 但没重启 shell → direnv 不生效。 `exec zsh` 或开新 terminal。 2. **`.envrc` 改了忘 allow**:env 用旧值。每次改后 `direnv allow`。 工具会提示。 3. **export 漏**:bash 习惯 `KEY=value` 而非 `export KEY=value` → 子 进程拿不到。`.envrc` 内必须 export。 4. **dotenv 顺序**:`.envrc` 内 `dotenv` 后又 `export` 覆盖。后者 生效。 5. **cd 进子目录 env 不变**:direnv 按 .envrc 文件位置加载。子目录 没 .envrc 沿用父。OK 但有时困惑。

MLflow:experiment 追踪 + model registry 自己跑

## 起因 训练 ML model 时: - 改 hyperparameter / feature → 跑一次 - 比较哪次效果好?凭记忆?csv 抄结果? - model artifact 存哪?git LFS?S3 哪个 path? - 模型 deploy 时哪个版本? `MLflow` 是 Databricks 出的 open source,解决 ML 实验管理 4 件事: 1. **Tracking**:每次 run 记 params / metrics / artifact 2. **Projects**:reproducible run(conda env / docker) 3. **Models**:标准化打包 / 注册 / 部署 4. **Registry**:model version 管理 (staging / production) ## 装 ```bash pip install mlflow ``` ```bash mlflow server --host 0.0.0.0 --port 5000 \ --backend-store-uri sqlite:///mlflow.db \ --default-artifact-root file:./mlruns ``` UI:`http://localhost:5000`。 生产用 PG + S3 替代 SQLite + local file。 ## 追踪 run ```python import mlflow import mlflow.sklearn from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, f1_score mlflow.set_tracking_uri('http://localhost:5000') mlflow.set_experiment('user-churn-prediction') with mlflow.start_run(run_name='rf-baseline'): # log hyperparameter n_estimators = 100 max_depth = 10 mlflow.log_param('n_estimators', n_estimators) mlflow.log_param('max_depth', max_depth) # 训练 model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth) model.fit(X_train, y_train) # log metric pred = model.predict(X_val) mlflow.log_metric('val_accuracy', accuracy_score(y_val, pred)) mlflow.log_metric('val_f1', f1_score(y_val, pred)) # log model artifact mlflow.sklearn.log_model(model, 'model') # log 任意 artifact mlflow.log_artifact('feature_importance.png') ``` UI 里看:每次 run 一行,columns 是 params + metrics,能排序 / 过滤。 ## auto-log 不想手动 log 每个 param? ```python mlflow.sklearn.autolog() # sklearn 自动 log mlflow.pytorch.autolog() # pytorch mlflow.xgboost.autolog() mlflow.tensorflow.autolog() model = LogisticRegression(C=0.1) model.fit(X, y) # 自动 log C / penalty / accuracy / model ``` 90% 用例 autolog 够。 ## 比较 run UI 选多个 run → Compare → 表格 + parallel coordinates 看 hyperparam ↔ metric。 Programmatic: ```python from mlflow.tracking import MlflowClient client = MlflowClient() exp = client.get_experiment_by_name('user-churn-prediction') # 找最佳 run runs = client.search_runs( experiment_ids=[exp.experiment_id], order_by=['metrics.val_f1 DESC'], max_results=1, ) best = runs[0] print(best.data.params, best.data.metrics) ``` ## model registry train 完想正式 deploy: ```python result = mlflow.register_model( f"runs:/{best.info.run_id}/model", "user-churn-model", # 注册名 ) # version 自动 +1 ``` UI 里看 model registry → "user-churn-model" v1, v2, v3 ... mark stage: ```python client.transition_model_version_stage( name='user-churn-model', version=3, stage='Production', ) ``` production code 加载: ```python model = mlflow.pyfunc.load_model('models:/user-churn-model/Production') pred = model.predict(X_test) ``` 切换版本只改 stage 标记,code 不动。 ## hyperparameter sweep ```python from itertools import product for n, d in product([100, 200, 500], [5, 10, 20]): with mlflow.start_run(): mlflow.log_params({'n_estimators': n, 'max_depth': d}) model = RandomForestClassifier(n_estimators=n, max_depth=d) model.fit(X_train, y_train) score = model.score(X_val, y_val) mlflow.log_metric('val_score', score) ``` 9 run 自动跑 + 全部对比。 配 Optuna / Hyperopt 自动化 search 更强。 ## 跟 git 集成 mlflow 自动 log: - git commit hash - branch name - diff(未 commit 改动) 每个 run 知道是哪个 code 版本产生的 → 复现性。 ## 与替代品对比 | | MLflow | Weights & Biases | Neptune | TensorBoard | |---|---|---|---|---| | 自托管 | ✅ | ❌(cloud only OSS有限) | ❌ | ✅(无 server) | | metric tracking | ✅ | ✅+ | ✅+ | ✅ | | model registry | ✅ | ✅ | ✅ | ❌ | | collab | 弱 | 强 | 中 | 弱 | | 成本 | 0 | 团队收费 | 团队收费 | 0 | | 生态 | 大 | 大 | 中 | 大 | 我个人 / 小团队 → MLflow(自托管 + 免费 + 标准)。 大团队 / 跨公司 collab → W&B。 ## 部署:自托管设置 ```yaml # docker-compose.yml services: mlflow: image: ghcr.io/mlflow/mlflow:v2.13 ports: - 5000:5000 environment: - AWS_ACCESS_KEY_ID=... - AWS_SECRET_ACCESS_KEY=... command: > mlflow server --host 0.0.0.0 --backend-store-uri postgresql://user:pass@db/mlflow --default-artifact-root s3://my-bucket/mlflow depends_on: - db db: image: postgres:16 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mlflow volumes: - mlflow-pg:/var/lib/postgresql/data ``` PG 存 metadata + S3 存 artifact → 可 scale。 ## 实战 lessons 我们一个客户 churn 项目用 MLflow: - 200+ experiment run(不同 feature set / model type / hyperparam) - 8 个最终 model 在 registry - Production model 每月更新(registry stage transition) - 任何时候能 diff 当前 production 跟 candidate 少了 mlflow 之前: - experiment 结果存团队 Notion / Slack message - model artifact 各种 S3 path 散 - 谁也不知道 production 现在跑的是 train_v3_final_FINAL2.pkl 还是 v4 接入 mlflow 后: - experiment 透明 / 可追溯 - model 版本明确 - 切回老版本 1 行命令 ## 跟 Airflow / Prefect 集成 ML pipeline DAG 每天跑: ```python @task def train_and_register(): with mlflow.start_run(): model = train(...) mlflow.sklearn.log_model(model, 'model') if score > threshold: mlflow.register_model(...) ``` 定期 retrain → log → 满足条件 promote 到 staging → 人手批准 → production。CD for ML。 ## 踩过的坑 1. **artifact path 写错**:local file mode 测试 OK,部署 server 后 `./mlruns` 在 server 找不到 → 必须 S3 / blob storage。 2. **大 model artifact**:log 大 model 几 GB 慢。考虑只 log 必要部分。 3. **run 不 close**:`mlflow.start_run()` 不在 with block + 没 `end_run()` → status="RUNNING" 一直挂。`with` 习惯保命。 4. **registry 名字冲突**:team 多人用同一 mlflow server,注册名建议 带 prefix 或 namespace。 5. **MLflow autolog 与 keras 不全兼容**:某些 callback / model subclassing 不 capture。complex case 手动 log。

下载大文件断点续传:curl -C / wget / aria2c 三种用法

## 起因 跨大洋下载 50 GB 数据集,到一半网断了。重新跑 = 重新下完整 50GB。 HTTP 标准里有 Range header(断点续传),客户端工具不全自动用。 ## curl:`-C -` 自动续传 ```bash curl -C - -O https://example.com/big.tar.gz # -C - 让 curl 检测本地文件大小 + Range header 续传 # -O 用 server 提供的文件名保存 ``` 如果服务端不支持 Range: ``` curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume. ``` 绝大多数现代 HTTP server / CDN 都支持。 ### 加速 / 重试 ```bash curl -C - -O \ --retry 5 \ --retry-delay 10 \ --retry-max-time 3600 \ --limit-rate 10M \ -L \ https://example.com/big.tar.gz ``` - `--retry 5`:网络错误自动重试 5 次 - `--retry-delay 10`:间 10 秒 - `--retry-max-time 3600`:1 小时内重试 - `--limit-rate 10M`:限速 10MB/s(晚上无人时占满带宽,白天礼貌点) - `-L`:跟 redirect ### 校验 ```bash curl -O https://example.com/big.tar.gz curl -O https://example.com/big.tar.gz.sha256 sha256sum -c big.tar.gz.sha256 ``` 下完一定要校验,网络可能让文件损坏。 ## wget:默认更友好 ```bash wget -c https://example.com/big.tar.gz # -c 续传 ``` 加重试 / 限速: ```bash wget -c \ --tries=10 \ --retry-connrefused \ --waitretry=30 \ --limit-rate=10m \ --user-agent='Mozilla/5.0' \ https://example.com/big.tar.gz ``` 镜像整个目录: ```bash wget -r -np -nH --cut-dirs=2 -R 'index.html*' \ https://example.com/files/2024/ ``` - `-r` 递归 - `-np` 不向上跳目录 - `-nH` 不创建 host name 目录 - `--cut-dirs=2` 砍掉前 2 级路径 - `-R 'index.html*'` 排除目录列表 HTML ## aria2c:多连接 + magnet + 元数据 vacuum ```bash sudo apt install aria2 brew install aria2 aria2c -x 16 -s 16 https://example.com/big.tar.gz # -x 16: 同一服务器最多 16 个连接 # -s 16: 同一文件用 16 个 segment 并行下 ``` 如果服务端支持 Range,多个 TCP 连接并行下不同字节范围, 吃满带宽(单 TCP 流由于 congestion control 经常吃不满)。 速度通常 3-5x 单连接。 更激进: ```bash aria2c -x 16 -s 16 -j 4 -k 1M --file-allocation=falloc \ --max-tries=10 --retry-wait=30 \ https://example.com/big.tar.gz ``` - `-j 4`: 同时下 4 个文件 - `-k 1M`: 每 segment 至少 1MB - `--file-allocation=falloc`: ext4/xfs 上预分配文件(防碎片) ### aria2c 支持的协议 ```bash # HTTPS aria2c https://... # FTP aria2c ftp://user:pass@host/path # BitTorrent aria2c some.torrent # Magnet link aria2c 'magnet:?xt=urn:btih:...' # metalink (含多个 mirror) aria2c file.metalink ``` 下大文件 magnet 时 aria2c 远比传统 BT 客户端轻量。 ### 多 mirror ```bash aria2c \ https://us.mirror.example.com/big.tar.gz \ https://eu.mirror.example.com/big.tar.gz \ https://asia.mirror.example.com/big.tar.gz ``` 3 个 mirror 同时下,每个用不同 segment。 带宽利用最大化。 ### 续传 aria2 默认自动续传: ```bash aria2c https://example.com/big.tar.gz # 中断 → 再跑同命令,从断点继续 ``` 断点信息在 `.aria2` 控制文件里。 ## rsync:对增量传输最优 ```bash rsync -avz --progress --partial \ user@server:/path/to/big.tar.gz \ ./big.tar.gz ``` - `--partial`:保留部分传输的文件供下次续传 - `--progress`:显示进度条 - rsync 还能传整个目录树 + 只传差异,比 scp 强 10x 更激进的限速 / 加密: ```bash rsync -avz --bwlimit=10000 \ -e 'ssh -c [email protected]' \ src/ dst:/path/ ``` `--bwlimit=10000` = 10 MB/s。aes128-gcm 加密快(用现代 CPU 的 AES-NI), 比默认 ChaCha20 快 2-3 倍。 ## 边下边校验:sha256 + tee ```bash curl -L https://example.com/big.tar.gz | tee big.tar.gz | sha256sum - # 边下载 + 边写文件 + 边算 hash # 输出 hash 后对比 expected ``` 适合一次性下 + 校验场景。 ## 限速 + 后台 + 续传完整组合 ```bash # 启动一个后台下载 nohup aria2c -x 8 -s 8 --max-overall-download-limit=20M \ --enable-rpc=true --rpc-listen-all=true --rpc-secret=mysecret \ -d ~/Downloads -o big.tar.gz \ https://example.com/big.tar.gz \ > ~/Downloads/aria2.log 2>&1 & # 通过 RPC 控制(暂停 / 看进度) curl -s -d '{"jsonrpc":"2.0","method":"aria2.tellActive","id":"x","params":["token:mysecret"]}' \ http://localhost:6800/jsonrpc | jq ``` aria2 RPC 让你用 web UI(aria2-webui)控制。Synology / OpenMediaVault 等 NAS 都有 aria2 GUI 包。 ## 服务端:让你的文件支持 Range nginx: ```nginx location /downloads/ { alias /var/www/downloads/; # 默认 nginx 静态文件就支持 Range,不需要配 } ``` 应用程序自己 stream 大文件时(如 Django/FastAPI/Flask),要手动处理 Range header。或者直接让 nginx 服务静态文件(最快)。 ## 反过来:让别人能续传我的下载 ```python # FastAPI 例 from fastapi import FastAPI, Request, Response from fastapi.responses import StreamingResponse, FileResponse import os @app.get('/download/{filename}') def download(filename: str, request: Request): path = f'/var/files/{filename}' # FileResponse 自动处理 Range return FileResponse(path, filename=filename) ``` `FileResponse` / nginx X-Sendfile 都支持 Range。 自己写 stream generator 要: - 看 `Range: bytes=100-200` header - 返回 206 Partial Content + `Content-Range` ## 效果 跨太平洋下 50GB(实测): | 工具 | 时间 | 带宽利用 | 注意 | |---|---|---|---| | curl | 6h | 25% | 单 TCP | | wget -c | 6h | 25% | 同上 | | aria2c -x 8 | 1.5h | 95% | 8 并行 | | aria2c + 3 mirror | 45min | 100% | 3 mirror × 8 段 | | rsync | 7h | 22% | SSH 加密开销 | 中断续传都 OK,但 aria2c 显著快。 ## 踩过的坑 1. **服务端不支持 Range** → curl `-C -` 直接报错。 测试:`curl -I -H "Range: bytes=0-100" https://...`, 返回 `206 Partial Content` = 支持。 2. **proxy / CDN 不传 Range** → 一些反代默认 buffer,吞 Range header。 nginx 加 `proxy_set_header Range $http_range;`。 3. **aria2c segment 上限**:每个 segment 一个 TCP 连接,过多反而 慢(拥塞 + 服务端 ratelimit)。8-16 是甜点。 4. **文件名含特殊字符 / 编码**:URL encode 不完整时下错文件。 `curl -o "myname.tar.gz" -L url` 显式指定本地名。 5. **磁盘满 silent fail**:下载到 99% 磁盘满 → 文件损坏。 `df -h` 提前确认空间。

密码 hash 用 argon2id:替代 bcrypt / scrypt 的现代选择

## 起因 老项目用 bcrypt(rounds=10) 存密码。安全审计建议升级到 argon2id: > bcrypt 是 1999 年算法,对 GPU 暴力破解抗性已弱。 > argon2id 是 2015 年 Password Hashing Competition 冠军, > memory-hard,专门抗 GPU/ASIC 攻击。 OWASP 2024 推荐:新项目用 argon2id;老项目升级时 graceful 迁移。 ## 解决方案 ### 1. Python: argon2-cffi ```bash uv add argon2-cffi ``` ```python from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError, InvalidHashError # 单例 hasher ph = PasswordHasher( time_cost=3, # 迭代次数 memory_cost=65536, # 64 MB parallelism=4, # 并行度 hash_len=32, salt_len=16, ) # 注册时 def register(email: str, password: str): h = ph.hash(password) # 自动 salt db.execute('INSERT INTO users (email, password_hash) VALUES (%s, %s)', (email, h)) # 登录时 def verify(email: str, password: str) -> bool: h = db.fetchone('SELECT password_hash FROM users WHERE email=%s', (email,)) if not h: return False try: ph.verify(h[0], password) # 如果参数升级了,自动 rehash if ph.check_needs_rehash(h[0]): new_h = ph.hash(password) db.execute('UPDATE users SET password_hash=%s WHERE email=%s', (new_h, email)) return True except VerifyMismatchError: return False ``` `hash()` 输出形如: ``` $argon2id$v=19$m=65536,t=3,p=4$<salt-b64>$<hash-b64> ``` 参数(m / t / p)编码在 hash 里,verify 时不需要单独存。 ### 2. 推荐参数 OWASP 2024 推荐(适合典型 web 服务器): ```python PasswordHasher( time_cost=3, memory_cost=12288, # 12 MB(低规模服务器) parallelism=1, ) ``` memory_cost 单位 KiB。生产服务器 RAM 充足建议: - 64-128 MB / hash - time_cost 3-5 - parallelism 1(避免 thread 争抢) 调参思路:在你的服务器上测 `ph.hash('test')` 耗时,目标 250-500 ms。 太快 → 攻击者 GPU 一秒破百万;太慢 → 用户登录卡。 ### 3. 渐进迁移:bcrypt → argon2id 用户表里 hash 共存: ```python from argon2 import PasswordHasher import bcrypt ph = PasswordHasher() def verify(email, password): h = db.get_user(email).password_hash if h.startswith('$argon2'): try: ph.verify(h, password); ok = True except: ok = False elif h.startswith('$2'): # bcrypt ok = bcrypt.checkpw(password.encode(), h.encode()) else: ok = False if ok and not h.startswith('$argon2'): # 用户登录成功 + 老 hash → 趁机升级 new_h = ph.hash(password) db.update_user_hash(email, new_h) return ok ``` 不需要"全部用户重设密码",每次登录顺便升级。3-6 个月几乎全部用户 迁完。 ### 4. Go: argon2id ```bash go get github.com/alexedwards/argon2id ``` ```go import "github.com/alexedwards/argon2id" params := &argon2id.Params{ Memory: 64 * 1024, Iterations: 3, Parallelism: 1, SaltLength: 16, KeyLength: 32, } hash, err := argon2id.CreateHash(password, params) // 存 hash 进 DB // 验证 match, err := argon2id.ComparePasswordAndHash(password, hash) ``` ### 5. Node.js: argon2 ```bash npm i argon2 ``` ```js import argon2 from 'argon2' const hash = await argon2.hash(password, { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 1, }) const ok = await argon2.verify(hash, password) ``` ### 6. 用 passlib(Python 老项目) Django 也用 passlib 兼容多算法: ```python # Django settings PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.Argon2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', # 兼容老 hash 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', ] ``` Django 验密时按顺序尝试匹配;用户登录成功后自动 rehash 到第一个 hasher(argon2id)。零代码迁移。 ## 选择对比 | | bcrypt | scrypt | PBKDF2 | argon2id | |---|---|---|---|---| | 年代 | 1999 | 2009 | 2000 | 2015 | | GPU 抗性 | 弱 | 中 | 弱 | 强(memory-hard) | | 算法标准化 | 事实标准 | RFC 7914 | NIST | RFC 9106 | | 推荐参数 | rounds=12+ | N=2^15+ | iters=600k+ | t=3,m=64MB | | 库支持 | 极广 | 广 | 广 | 良好(增长中) | 2024 后新项目首选 argon2id;老项目迁移路径如上。 ## 配套安全实践 ### 1. 永远 hash + salt,不要明文 ```python # ❌ db.execute('INSERT ... VALUES (%s, %s)', (email, password)) # ❌ 简单 hash(无 salt 易 rainbow table 攻击) db.execute('INSERT ... VALUES (%s, %s)', (email, hashlib.sha256(password.encode()).hexdigest())) # ✅ db.execute('INSERT ... VALUES (%s, %s)', (email, ph.hash(password))) ``` ### 2. 等时比较 `ph.verify()` 内部用 constant-time compare。手写 `hash == expected` 有时序泄漏。 ### 3. 不要 log 密码 / hash ```python # ❌ log.info('login attempt: user=%s pw=%s', email, password) # ✅ log.info('login attempt: user=%s', email) ``` ### 4. 限制登录失败 按 IP / 按 email 限速: - 5 分钟内 5 次失败 → 拒绝 15 分钟 - 配合 captcha ### 5. 强密码策略 不强求复杂度,但建议: - 最少 12 字符 - 用 zxcvbn / haveibeenpwned API 检查"是否已泄漏" ```python import requests def is_pwned(password: str) -> bool: sha = hashlib.sha1(password.encode()).hexdigest().upper() prefix, suffix = sha[:5], sha[5:] r = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}') return suffix in r.text ``` 只发 prefix 5 位(k-anonymity),不泄漏完整 hash。 ### 6. 不要"密码找回 = 显示原密码" 如果你能给用户发"你的密码是 xxx",说明密码明文存。删库跑路风险。 正确做法:reset 链接让用户重设新密码。 ## 效果 我们升级后: - 5000 用户在 4 个月内 95% 自然 rehash 到 argon2id - 安全审计 pass - 单次登录验证延迟从 50ms → 350ms(增加但仍可接受) - 服务器 RAM 占用增加 ~50MB / 并发 login(PasswordHasher 实例) ## 踩过的坑 1. **memory_cost 单位是 KiB**:写 65536 是 64 MB,不是 64 KB。 写错值参数提示模糊。 2. **每个 worker 都创建 hasher**:PasswordHasher() 实例化贵。 全局单例。 3. **rehash 升级时不告诉用户**:用户体验无感(好事)。但要 log "user X migrated to argon2id" 供审计。 4. **password 长度无限**:Argon2 自身能处理任意长度,但 DB column 设 VARCHAR(255) 限制 hash 输出(~100 字符)+ 限输入密码 ≤ 128 字符 防 DoS(超长密码 hash 时间长)。 5. **跨实例参数不一致**:你的 web server 用 64MB,别人在 mobile API 用 12MB,同密码 verify 应该都 work(参数编码在 hash 里)。 但 hash 操作如果 mobile API 太慢,统一参数到最小公倍数。

pandas 内存优化:dtype 收缩 / categorical / sparse 让 5GB → 800MB

## 起因 加载一份"用户行为日志" CSV,10M 行 × 30 列, `pd.read_csv('events.csv')` → 内存 5 GB 后 OOM。 机器只有 8 GB RAM。 查 `df.info(memory_usage='deep')`: ``` RangeIndex: 10000000 entries, 0 to 9999999 Data columns (total 30 columns): # Column Dtype MemUsage (MB) --- ------ ----- ------------- 0 event_type object 620 1 user_id int64 76 2 country object 580 3 device_type object 610 4 amount float64 76 ... dtypes: float64(8), int64(7), object(15) Memory: 5102 MB ``` 绝大多数内存被 string column 占用(pandas object dtype = Python str 对象数组,每个 string 40+ bytes 开销)。 5 个优化让同样数据降到 800 MB: ## 1. categorical:低基数字符串 ```python df['event_type'] = df['event_type'].astype('category') df['country'] = df['country'].astype('category') df['device_type'] = df['device_type'].astype('category') ``` categorical 把 string 映射到 int8/int16 + 一份 unique values 字典。 event_type 5 个值 + 10M 行 → 5 bytes / row 而非 60 bytes / row。 效果: ``` 0 event_type category 10 (从 620 MB) 2 country category 11 (从 580 MB) 3 device_type category 12 (从 610 MB) ``` 3 个 column 从 1.8 GB → 33 MB。 判断"该不该 categorical": ```python # 列的 unique 数 / 总行数 比例 df['country'].nunique() / len(df) # 0.00002 = 200 / 10M → 强烈 categorical # 0.5 = 5M unique / 10M → 不该 categorical(开销可能更大) # 一般 ratio < 0.05 时 categorical 受益 ``` `> 50%` 基数的 string column(如 URL / session_id)反而保 object 或 string[pyarrow]。 ## 2. 数值列收缩 dtype ```python df['user_id'].max() # 200_000_000 → int32 装得下 (max 2.1B) df['amount'].max() # 9999.99 → float32 够(默认 float64) df['hour_of_day'].max() # 23 → int8 (max 127) df = df.astype({ 'user_id': 'int32', 'amount': 'float32', 'hour_of_day': 'int8', 'minute_of_day': 'int8', }) ``` int64 → int32 减半;int8 减 8 倍。 float64 → float32 减半;某些数据(ML 特征)甚至 float16 也 OK。 辅助: ```python def shrink_ints(df): for col in df.select_dtypes(include=['int64']).columns: c = df[col] mx, mn = c.max(), c.min() if mn >= 0: if mx < 256: df[col] = c.astype('uint8') elif mx < 65536: df[col] = c.astype('uint16') elif mx < 4294967296: df[col] = c.astype('uint32') else: if -128 <= mn and mx < 128: df[col] = c.astype('int8') elif -32768 <= mn and mx < 32768: df[col] = c.astype('int16') elif -2147483648 <= mn and mx < 2147483648: df[col] = c.astype('int32') return df ``` 跑一次自动 downcast 所有 int。 ## 3. sparse for mostly-zero columns ```python # 90% 是 0 的 dummy variable column df['premium'] = df['premium'].astype('Sparse[int8, 0]') ``` 只存 non-zero 值 + 索引。 0.9M 个 0 + 0.1M 个 1 → 之前 10MB → 1MB。 适合 one-hot encoding 后的稀疏矩阵。 ## 4. parquet 替代 CSV 读 / 写 / 存都快得多: ```python df.to_parquet('events.parquet', compression='zstd') # 5GB CSV → 800MB Parquet(列存 + 压缩) df = pd.read_parquet('events.parquet') # 比 read_csv 快 5-10 倍 # 自动保留 dtype(包括 categorical) ``` read_csv 时也指定 dtype 避免 pandas 猜: ```python df = pd.read_csv('events.csv', dtype={ 'event_type': 'category', 'country': 'category', 'amount': 'float32', 'user_id': 'int32', }) ``` 避免 pandas 默认 int64 / float64 / object 后再转换的中间峰值。 ## 5. chunked read:分块处理 如果转完仍装不下,分块流式处理: ```python chunks = [] for chunk in pd.read_csv('events.csv', chunksize=100_000): chunk = optimize_dtypes(chunk) # 业务处理 / 聚合 result = chunk.groupby('user_id').agg(...) chunks.append(result) # 最后合并 final = pd.concat(chunks).groupby(...).sum() ``` 或者直接用 polars(前面有文章),原生支持流式 + 多核。 ## 6. PyArrow string 替代 object pandas 2.0+ 加了 `string[pyarrow]` 类型,比 object 省内存 + 快: ```python df['session_id'] = df['session_id'].astype('string[pyarrow]') ``` 适合"高基数 string" —— categorical 不划算的场景。 比 object 省 40-60% 内存 + groupby / sort 快 2-3x。 `pd.options.future.infer_string = True` 让 read_csv 默认用 string[pyarrow](pandas 3.0 将成默认)。 ## 实战脚本 ```python import pandas as pd def shrink(df): """通用 dtype 收缩 + categorical 自动检测""" df = df.copy() # 1. integer downcast for col in df.select_dtypes(include=['int64']).columns: df[col] = pd.to_numeric(df[col], downcast='integer') # 2. float downcast for col in df.select_dtypes(include=['float64']).columns: df[col] = pd.to_numeric(df[col], downcast='float') # 3. low-cardinality string → category for col in df.select_dtypes(include=['object']).columns: if df[col].nunique() / len(df) < 0.05: df[col] = df[col].astype('category') return df df = pd.read_csv('events.csv') print('before:', df.memory_usage(deep=True).sum() / 1e9, 'GB') df = shrink(df) print('after: ', df.memory_usage(deep=True).sum() / 1e9, 'GB') ``` 典型场景 5x 内存压缩。 ## 效果 我的 10M × 30 dataset: | | 内存 | |---|---| | 默认 read_csv | 5.1 GB | | + int / float downcast | 3.2 GB | | + categorical (3 cols) | 1.4 GB | | + string[pyarrow] (1 col) | 1.1 GB | | + sparse (2 cols) | 800 MB | 整套操作 < 30 秒 + 完全保留语义。后续 groupby / merge 也快得多 (小数据 = 快 cache 命中)。 ## 何时考虑换工具 pandas 优化到底后还是不够 → 切: - **polars**:原生流式 + 多核,比 pandas 快 5-30x - **DuckDB**:SQL 跑 Parquet / CSV,省内存 - **Dask**:pandas 类似 API + out-of-core + 集群 - **Spark**:超大数据集群 但单机 100GB 内 pandas + 这些技巧通常够。 ## 踩过的坑 1. **categorical 后 join 慢**:两个 df join 的 categorical 列要 "相同 categories" 否则 pandas 转回 object。 `df['x'].cat.set_categories(combined_cats)`。 2. **`pd.read_csv` 不带 dtype**:pandas 先全读 object / int64 / float64 占大量内存,read 完才转。**指定 dtype**:内存峰值降 2-3x。 3. **categorical 不能做某些操作**:`.str` accessor 在 categorical 上慢 / 不工作。需要时 `.astype('object').str.lower()`。 4. **sparse 与 numpy/sklearn 兼容性差**:很多 sklearn estimator 不 接受 SparseArray,要 `.to_dense()`。trade-off。 5. **string[pyarrow] 仍在演进**:pandas 2.x 部分功能(如 groupby) 仍回退到 object。看 changelog 跟进。

用 Service Worker 实现离线缓存(最小可用 PWA)

Service Worker 是 PWA 的核心:拦截网络请求,按策略返回缓存 / 网络 / 兜底页面, 让网页在无网时也能用。下面写一个最简单但完整的离线缓存层。 ## 1. 注册 ```js // main.js if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js', { scope: '/' }) .then(reg => console.log('SW registered:', reg.scope)) .catch(err => console.error('SW failed:', err)) }) } ``` `scope: '/'` 让 SW 拦截整个站点。SW 文件必须从 **同源 root 路径** 提供 (不能放 CDN)。 ## 2. SW 主体 ```js // public/sw.js const VERSION = 'v3-2026-05-23' const PRECACHE = `precache-${VERSION}` const RUNTIME = `runtime-${VERSION}` const PRECACHE_URLS = [ '/', '/index.html', '/assets/main.css', '/assets/main.js', '/offline.html', ] // 安装:预缓存 shell 资源 self.addEventListener('install', event => { event.waitUntil( caches.open(PRECACHE) .then(cache => cache.addAll(PRECACHE_URLS)) .then(() => self.skipWaiting()) // 立刻激活,不等老 SW 退出 ) }) // 激活:清理老版本缓存 self.addEventListener('activate', event => { event.waitUntil( caches.keys() .then(keys => Promise.all( keys.filter(k => k !== PRECACHE && k !== RUNTIME) .map(k => caches.delete(k)) )) .then(() => self.clients.claim()) ) }) // fetch:按策略响应 self.addEventListener('fetch', event => { const { request } = event if (request.method !== 'GET') return // 只缓存 GET const url = new URL(request.url) if (url.origin !== self.location.origin) return // 不管跨域 // 1. 导航请求 → 网络优先,失败回退到 offline.html if (request.mode === 'navigate') { event.respondWith( fetch(request).catch(() => caches.match('/offline.html')) ) return } // 2. 静态资源(hash 文件名)→ 缓存优先(永久) if (url.pathname.match(/\.(css|js|woff2|png|svg|ico)$/)) { event.respondWith( caches.match(request).then(cached => { if (cached) return cached return fetch(request).then(response => { if (response.ok) { const clone = response.clone() caches.open(RUNTIME).then(c => c.put(request, clone)) } return response }) }) ) return } // 3. API 请求 → 网络优先,失败用缓存 if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request).then(response => { if (response.ok) { const clone = response.clone() caches.open(RUNTIME).then(c => c.put(request, clone)) } return response }).catch(() => caches.match(request)) ) return } }) ``` ## 3. offline 页 ```html <!-- public/offline.html --> <!DOCTYPE html> <html> <head><meta charset="utf-8"><title>离线</title></head> <body> <h1>当前离线</h1> <p>请检查网络后重试。已缓存的页面仍然可以打开。</p> </body> </html> ``` ## 4. 三种缓存策略速查 | 策略 | 适用 | 实现 | |---|---|---| | Cache-first | 静态资源(hash 文件名)、字体 | match → 否则 fetch + cache | | Network-first | HTML / API(要新鲜数据) | fetch → 失败用 cache | | Stale-while-revalidate | 头像、缩略图(旧的也能用) | match 立刻返回 + 异步 fetch 更新 | 实现 stale-while-revalidate: ```js caches.match(request).then(cached => { const fetched = fetch(request).then(response => { if (response.ok) { caches.open(RUNTIME).then(c => c.put(request, response.clone())) } return response }) return cached || fetched // 有 cache 立刻给,同时后台更新 }) ``` ## 5. Manifest(让浏览器认你是 PWA) ```json // public/manifest.json { "name": "My App", "short_name": "MyApp", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#2563eb", "icons": [ { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } ] } ``` ```html <link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#2563eb"> ``` 满足 PWA 安装条件(SW + manifest + HTTPS + 图标),Chrome 地址栏会出现 "安装应用"图标。 ## 6. 版本更新与"重启提示" ```js // main.js navigator.serviceWorker.register('/sw.js').then(reg => { reg.addEventListener('updatefound', () => { const nw = reg.installing nw.addEventListener('statechange', () => { if (nw.state === 'installed' && navigator.serviceWorker.controller) { // 新版本可用 if (confirm('有新版本,立即刷新?')) { window.location.reload() } } }) }) }) ``` ## 7. 调试 Chrome DevTools → Application → Service Workers: - "Update on reload" 勾上,每次 F5 强制 SW 重新安装 - "Bypass for network" 临时禁用 SW - "Unregister" 卸载(开发期常用) Cache 内容在同面板的 Cache Storage 看。 ## 8. Workbox 手写 SW 没问题,但生态成熟后用 Google 的 Workbox: ```bash npm i -D vite-plugin-pwa workbox-window ``` `vite-plugin-pwa` 配几行就生成 SW + manifest + precache,并处理"新版本提示" 的边界。生产推荐。 ## 踩过的坑 - SW 必须 HTTPS(localhost 例外)。HTTP 网站永远 register 不上。 - 改了 SW 文件浏览器要等"老 SW 没有 client" 时才激活新版;测试期间 关掉整个 tab 才生效,反复测试很烦——用 `self.skipWaiting()` + `self.clients.claim()` 强制立刻接管。 - SW 缓存了一个 redirect 响应后,整个站点行为奇怪。`Cache.put()` 不要存 `response.redirected` 的 response。 - 缓存条目 LRU 上限是浏览器决定的(Chrome 总配额几百 MB-几 GB); 超额时按时间删除最老条目,缓存可能突然失效。

Playwright vs Cypress:E2E 测试选型

## 起因 E2E 测试两个主流: - **Cypress**:先发优势,DX 好,2017+ - **Playwright**(Microsoft,2020+):更快 / 更全 / 现代 新项目都选 Playwright,下面对比。 ## 写法 ```ts // Playwright import { test, expect } from '@playwright/test'; test('login', async ({ page }) => { await page.goto('/login'); await page.fill('[name=email]', '[email protected]'); await page.fill('[name=password]', 'pass'); await page.click('button[type=submit]'); await expect(page.locator('h1')).toHaveText('Dashboard'); }); ``` ```js // Cypress describe('login', () => { it('logs in', () => { cy.visit('/login'); cy.get('[name=email]').type('[email protected]'); cy.get('[name=password]').type('pass'); cy.get('button[type=submit]').click(); cy.contains('h1', 'Dashboard'); }); }); ``` API 风格差异: - Playwright:async/await 标准 JS - Cypress:chained command,自定义 promise-like Cypress chain 直观但 debug 复杂。Playwright async 更标准。 ## 浏览器支持 | | Playwright | Cypress | |---|---|---| | Chromium | ✅ | ✅ | | Firefox | ✅ | ✅ | | WebKit (Safari) | ✅ | ❌(实验) | | Mobile emulation | ✅ | 部分 | | 并发跨浏览器 | ✅ | 部分 | Playwright **WebKit 支持** 是杀手:Safari bug 能在 CI 测出(不能跑 真 Safari 但 WebKit engine 同一个)。 ## 性能 我们一个项目 200 E2E 测试: | | Cypress | Playwright | |---|---|---| | 全跑 | 12 min | 4 min | | parallel (4 worker) | 5 min | 1.5 min | | 启动 (first test) | 8s | 2s | Playwright 3x 快主要因为: - 真正并行(多 worker,单 process 多 context) - WebSocket 协议直连(vs Cypress iframe-based) - 默认 headless 优化 Cypress 并行要付费云服务 (Cypress Cloud) 或者多 CI runner。 Playwright 单进程并行免费。 ## auto-wait ```ts // Playwright await page.click('button'); // 自动等元素可点 await expect(page.locator('h1')).toHaveText('done'); // 自动 retry until match ``` ```js // Cypress cy.get('button').click(); // 自动 retry cy.contains('h1', 'done'); // 自动 retry ``` 两者都有 auto-wait,行为相似。 Playwright 的 `expect().toBeVisible()` 等 matcher 配合 retry 内置。 ## test isolation Cypress:每 test 在新 iframe 跑(同 browser)。 Playwright:每 test 在新 BrowserContext(独立 cookie / storage)。 Playwright context 隔离更彻底,并行更安全。 ## fixture / setup ```ts // Playwright test.beforeEach(async ({ page }) => { await page.goto('/login'); }); // 共享 page state (storageState) test.use({ storageState: 'auth.json' }); ``` ```js // Cypress beforeEach(() => cy.visit('/login')); // 共享 login Cypress.Commands.add('login', () => { ... }); cy.login(); ``` Playwright `storageState` 让"登录一次" 然后多 test 复用 session → 速度 +30%。Cypress 类似 `cy.session()`。 ## debug Playwright: - VS Code 扩展:UI mode 一键 step through - trace viewer:自动录制每步 DOM + network + screenshot - `--debug` 启动 Inspector ```bash npx playwright test --ui # 交互 UI npx playwright show-trace ... # 看失败 trace ``` Cypress: - GUI mode 一直是核心(time-travel debugger) - 录像 / 截图 两者 debug 体验都好,Playwright trace viewer 后来居上更详细。 ## codegen ```bash npx playwright codegen example.com ``` 打开浏览器 → 你点 / 输 → Playwright 自动生成代码。 新人写第一个测试快。 Cypress 也有 Studio 但不如 Playwright codegen 成熟。 ## visual regression Playwright 内置 snapshot: ```ts await expect(page).toHaveScreenshot('home.png'); ``` Cypress 要装插件(cypress-image-snapshot)。 ## API testing Playwright 也能测 API: ```ts const response = await request.post('/api/users', { data: {...} }); expect(response.status()).toBe(201); ``` Cypress 也有 `cy.request()`。 两者都行,API 测试不是主战场。 ## component testing Cypress component testing 较早(2021+)。 Playwright 1.30+ 也有 component testing(experimental)。 实际:我们用 vitest + Testing Library 做 component test, Playwright 只跑 full E2E。 ## CI 集成 ```yaml # Playwright GitHub Actions - run: npx playwright install --with-deps - run: npx playwright test - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/ ``` 失败自动上传 HTML report + trace + screenshot → review 方便。 Cypress: ```yaml - run: npx cypress run ``` 跟 Cypress Cloud 集成更好(recorded video / dashboard),但要付费。 ## flake 处理 E2E 测试 flake(不稳定)是普遍问题。 Playwright retries: ```ts // playwright.config.ts export default { retries: process.env.CI ? 2 : 0, }; ``` Cypress 也支持 retry。 flake 治本: - locator 用 stable selector(test-id 而不是 CSS class) - 等明确条件(不是 sleep) - isolate test(不依赖前一 test state) ## locator best practice ```ts // bad: brittle page.locator('.btn-primary:nth-child(3)'); // good page.getByRole('button', { name: 'Submit' }); page.getByTestId('submit-button'); ``` Playwright `getByRole` / `getByText` / `getByLabel` 跟 RTL 一致。 accessibility-friendly + 稳定。 ## 选择决策 - **新项目** → Playwright(性能 + 跨浏览器 + 免费并行) - **老 Cypress 项目 + 测试稳** → 不必迁 - **macOS Safari 是 target** → Playwright(WebKit) - **团队偏好 GUI debug** → Cypress 仍胜 我新项目 100% Playwright。 ## 真实迁移 case 某客户项目 Cypress 200 测试,CI 跑 15 分钟,flaky。 迁 Playwright: - 1 周转换 + 调优 - CI 时间 → 4 分钟(4 worker parallel) - flake rate 从 5% → < 1% - 没付 Cypress Cloud 钱(每月 $75) 主要工作: - API 对应(cy.get → page.locator) - custom command → fixture / helper - 调整等待条件(用 expect with auto-retry) ## 踩过的坑 1. **CI 没装 browser dependencies**:`npx playwright install` 也要 `--with-deps`(Linux 系统依赖)。 2. **fixture 滥用**:把太多 setup 塞 fixture → test 慢。balance。 3. **`waitForLoadState('networkidle')`**:永远不到 idle(持续 polling) → 超时。改 `domcontentloaded` 或 specific selector。 4. **trace 文件大**:每 test 一个 zip 几 MB → CI 100 test 100 MB artifact。只 retain failure trace(`trace: 'retain-on-failure'`)。 5. **headed mode 与 headless 行为差**:极少数 case 一致性问题(如 focus / window size)。CI 主要 headless,dev 偶尔 headed debug。

fzf:把模糊查找塞进 shell 的每个角落

## 起因 终端里"找东西"老操作: - `cd ../../../some/path` —— 路径长 + 容易记错 - `Ctrl-R` 反向搜历史 —— 一次只能展示一条 - `git checkout <Tab>` —— 分支多了 tab 全列出来 - `kill <pid>` —— 先 `ps` 查 PID 再 kill `fzf` 给所有这些场景加了模糊搜索 + 实时筛选 + 多选。装一次,所有地方 都香。 ## 装 ```bash # macOS brew install fzf $(brew --prefix)/opt/fzf/install # 装 shell 集成 # Ubuntu/Debian apt install fzf # 或者源码(最新版) git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` `install` 脚本问你装不装 `Ctrl-T`(粘文件)/`Ctrl-R`(搜历史)/`Alt-C` (cd 目录)键绑定 → 全选 yes。 ## 基础体验 shell 里随便敲: ```bash $ fzf ``` 打开一个全屏列表(默认是当前目录所有文件),上下选 / 输入字符模糊 过滤 / 回车确认。 ```bash $ vim $(fzf) # 模糊找文件然后 vim 打开 $ cat $(fzf) ``` ## 必装的 shell 绑定 ```bash Ctrl-T # 触发"插入文件路径"模式(任何 cmd 后都能用) Ctrl-R # 反向搜命令历史,模糊匹配 Alt-C # cd 到模糊选中的子目录 ``` 例: ```bash $ git diff <Ctrl-T> # 弹列表选文件 → 插入路径 $ vim <Ctrl-T> # 同上 ``` `Ctrl-R` 把内建反搜替换成多行筛选,找老命令快 10 倍。 ## 与 ripgrep / fd 配合 `fd`(rust find)+ `rg`(ripgrep)+ `fzf` 三件套: ```bash # ~/.zshrc export FZF_DEFAULT_COMMAND='fd --type f --hidden --exclude .git' export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_ALT_C_COMMAND='fd --type d --hidden --exclude .git' ``` `Ctrl-T` 只列文件 + 隐藏文件 + 排除 .git,速度起飞(fd 比 find 10x 快)。 ## 实战 alias ```bash # kill 进程 with fzf alias fkill='ps -ef | fzf --multi | awk "{print \$2}" | xargs kill' # git checkout 分支模糊选 gco() { local branch=$(git branch --all | grep -v HEAD | sed 's/^..//' | fzf) git checkout "$branch" } # cd 到最近用过的目录 cdh() { local dir=$(dirs -p | uniq | fzf) cd "$dir" } # kubectl pod fzf kpod() { kubectl get pods --all-namespaces | fzf } # tmux session 切换 tms() { local session=$(tmux ls -F '#{session_name}' | fzf) tmux switch-client -t "$session" } ``` 每条都是日常高频,5 个字符内启动 + 模糊选 + 操作。 ## preview 窗口 最强 feature:右侧实时显示选中项预览。 ```bash fzf --preview 'cat {}' # 文件用 bat(高亮) fzf --preview 'bat --color=always {}' --preview-window right:60% # git log fzf git log --oneline | fzf --preview 'git show --color {1}' # JSON file 用 jq fzf --preview 'jq -C . {}' ``` `{}` 是 placeholder 选中项。`{1}` 是第一字段(空格分割)。 ## 多选 `--multi` / `-m` 模式,`Tab` 选 / `Shift-Tab` 反选。 ```bash # 选多个文件 git add git add $(git status --short | awk '{print $2}' | fzf -m) # 选多个 PR 关闭 gh pr list | fzf -m | awk '{print $1}' | xargs -I{} gh pr close {} ``` ## fzf 嵌入工具 很多 CLI 把 fzf 当 picker: - `forgit`:git 操作 + fzf 选 hunk / commit - `fz`:autojump 替代,fzf 选历史目录 - `peco`:fzf 的另一选择(go 写) - `sk`(skim):rust 写的 fzf 兼容 ## fzf as filter(programmable) 不只是交互工具,也是 Unix filter: ```bash echo -e "apple\nbanana\norange" | fzf # 交互选 echo -e "apple\nbanana\norange" | fzf -f "ap" # 非交互过滤 echo -e "apple\nbanana\norange" | fzf -f "ap" --print-query ``` `--filter`/`-f` 非交互模式,可以塞 pipeline。 ## tmux 集成 ```bash fzf-tmux -p 70%,70% # 在 tmux popup 里跑 fzf ``` 不会污染当前窗口。tmux 3.2+ 才有 popup 支持。 ## 性能 fzf 是 Go 写的,单线程能扫几百万行(typical < 100ms)。 benchmark: | 文件数 | 启动时间 | |---|---| | 10k | 30ms | | 100k | 100ms | | 1M | 800ms | 100w 文件项目(monorepo)也 sub-second。 ## 与替代品对比 - **peco**(go):同代,UI 略不同,更新少 - **sk / skim**(rust):兼容 fzf,速度更快,preview 略弱 - **selecta**(ruby):老 + 慢,过时 fzf 仍是事实标准。 ## 我的 .zshrc 完整片段 ```bash # fzf [ -f ~/.fzf.zsh ] && source ~/.fzf.zsh export FZF_DEFAULT_COMMAND='fd --type f --hidden --exclude .git' export FZF_DEFAULT_OPTS='--height 50% --layout=reverse --border --preview-window right:50%' export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range :100 {}'" export FZF_ALT_C_OPTS="--preview 'eza --tree --color=always {} | head -50'" export FZF_CTRL_R_OPTS="--preview 'echo {}' --preview-window down:3:wrap" # alias alias gco='git_checkout_fzf' alias gst='git_status_fzf' git_checkout_fzf() { git branch --all --color=always | grep -v HEAD | \ fzf --ansi --preview 'git log --color {-1} -10 --oneline' | \ sed 's/.* //' | xargs git checkout } ``` ## 踩过的坑 1. **shell 启动慢**:fzf 装多个集成 → zsh 启动 +200ms。 `time zsh -i -c exit` 测一下,找 culprit。 2. **`Ctrl-R` 跟其它工具冲突**:tmux / vim / readline 都用 Ctrl-R。 fzf 集成默认覆盖 bash/zsh 的内建,要小心副作用。 3. **`fd` 在 Debian 名字是 `fdfind`**:装时 `apt install fd-find` → 命令叫 `fdfind`。alias 一下 `alias fd=fdfind`。 4. **preview 卡**:preview 命令慢(如 git show 大 commit)→ fzf 看着卡。 `--preview-window noborder` + 优化 preview 命令。 5. **macOS 与 zsh 5.9 兼容**:老 zsh 可能 fzf widget 报错。升 zsh / brew upgrade zsh。

日志收集后端:Loki vs Elasticsearch(成本 vs 功能)

## 起因 应用日志几个 GB / day 起,需要: - 集中查询(多机器日志一个地方搜) - 告警(错误率 spike) - 长期保留(compliance / debug 历史) 主流方案: - **ELK / OpenSearch**:Elasticsearch + Logstash + Kibana - **Loki + Grafana**:Grafana Labs 出,"日志的 Prometheus" - **Datadog / NewRelic**:SaaS 全套,贵但省事 Loki 跟 ES 的核心差异:**Loki 只索引 metadata,不索引日志正文** → 存储成本 10-100x 低,但全文搜索弱。 ## ES 方式 ``` [app] → filebeat → [Logstash] → [Elasticsearch (集群)] → [Kibana] ``` 每条日志 parse + 索引每个 token: ``` ts: "2025-03-14T10:23:00Z" level: "ERROR" service: "api" msg: "Failed to connect to db at host pg-1, error: timeout" trace_id: "abc123" ``` 索引后: - 任意 token 模糊搜索 < 100ms - 字段聚合 / 统计快 - Kibana 仪表盘 / 告警丰富 代价: - 存储 5-10x 原日志 size(倒排索引膨胀) - ES 集群 RAM 重(每 node 8-32 GB) - 索引 CPU 高 - 1 TB/day 日志 → 10-100 TB 索引存储 ## Loki 方式 ``` [app] → Promtail / Fluent Bit → [Loki] → [Grafana] ``` 日志按 stream 存(compressed chunk),只索引 stream label: ``` labels: {service="api", level="ERROR", instance="api-1"} log line: "2025-03-14T10:23:00Z Failed to connect..." ``` 查: ```logql {service="api", level="ERROR"} |= "timeout" ``` - 先按 label 过滤 → 找到匹配的 chunk - 全文 grep chunk 内日志 结果: - 存储 1-2x 原 size(gzip 压缩,无倒排) - 单实例几 GB RAM 撑大量数据 - 全文搜限定 label 范围内 grep(不是全局倒排) ## 实测对比 我们一个项目 100 GB/day 日志: | | ELK | Loki | |---|---|---| | 存储 (30 day retention) | 30 TB | 3 TB | | 节点数 | 5 × 16GB ES + 3 logstash | 2 × 8GB Loki | | 月成本 (AWS) | ~$3500 | ~$400 | | 简单 query | < 100ms | 1-3s | | 复杂搜索 | 强 | 中(label-bound) | | 仪表盘 | Kibana 强 | Grafana 强 | | 告警 | watcher / ElastAlert | Loki alert rule | Loki **十分之一存储成本** + **十分之一计算成本**。 搜索体验稍弱但够用。 ## label 设计是关键 Loki 的 label 不能 high-cardinality(如 user_id / trace_id 不行)。 原则: - low cardinality(< 100k unique values):service / instance / level / env - 把 high cardinality 数据放 log line 里(grep 找) ``` ✅ {service="api", level="ERROR"} # 几十个 service × 4 level ❌ {user_id="12345"} # 百万 user → label 爆炸 ``` high cardinality label → Loki 索引膨胀 → 慢 + 内存 OOM。 ## LogQL 类 PromQL: ```logql # 简单 {service="api"} |= "error" # 多条件 {service=~"api|worker", level="ERROR"} |~ "timeout|connection refused" # rate (metric from logs) rate({service="api", level="ERROR"}[5m]) # parse json + filter {service="api"} | json | status >= 500 ``` `|=` 包含, `|~` regex, `!=` / `!~` 排除。 ## 部署 ### Loki ```yaml # docker-compose.yml services: loki: image: grafana/loki:3.0.0 ports: - 3100:3100 volumes: - ./loki-config.yml:/etc/loki/local-config.yaml - loki-data:/loki promtail: image: grafana/promtail:3.0.0 volumes: - /var/log:/var/log - ./promtail-config.yml:/etc/promtail/config.yml ``` ```yaml # promtail-config.yml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name: docker docker_sd_configs: - host: unix:///var/run/docker.sock relabel_configs: - source_labels: ['__meta_docker_container_name'] target_label: 'container' ``` ### Grafana 连 Loki Grafana 加 Loki data source `http://loki:3100`。Explore 直接查。 ## chunked storage Loki 支持 S3 / GCS / 本地: ```yaml storage_config: aws: s3: s3://my-bucket/loki region: us-east-1 ``` 老数据存 S3(几 $0.02/GB/月)→ 长期保留极便宜。 查询时 Loki 拉对应 chunk 解压扫。 ## alert rule ```yaml groups: - name: api-alerts rules: - alert: HighErrorRate expr: sum(rate({service="api", level="ERROR"}[5m])) > 10 for: 5m labels: severity: warning annotations: summary: "API error rate > 10/s for 5min" ``` Loki ruler 跑 alert,发 alertmanager → PagerDuty / Slack。 ## 与 SaaS 对比 - Datadog logs:1 GB/day 跑 $30/月 起,100 GB/day = $3000+/月 - New Relic:类似 - Self-host Loki:100 GB/day = $400/月 scale 上 SaaS 贵 10x+。中小 scale 时间成本 vs 现金成本 trade-off。 ## 何时仍选 ES - **全文搜索是核心**(不只是 grep):日志做"搜索引擎",ES 倒排是杀手锏 - **复杂聚合 / OLAP-like 分析**:ES aggregation 比 LogQL 强 - **业务已有 Kibana dashboard 多**:迁移成本高 - **多种数据 type 混搜**(log + APM trace + metric 一处) ## 与 Quickwit 对比 Quickwit 是另一选项:log 优化 + S3-native + 全文搜索(用 tantivy / Rust)。 比 ES 便宜 + 比 Loki 搜索强。 | | ES | Loki | Quickwit | |---|---|---|---| | 索引 | full | label only | full + S3-native | | 存储成本 | 高 | 极低 | 低 | | 全文搜 | 极强 | 弱 | 强 | | 部署 | 复杂 | 简单 | 中 | 新项目 Quickwit 在评估,但 Loki 仍是 mainstream 简单选择。 ## 我的选择 - **新项目,云原生** → Loki(成本 + Grafana 一体化) - **既有 ES 团队** → 保留 ES - **极致全文搜需求** → ES - **超大 scale + 成本敏感** → Quickwit ## 踩过的坑 1. **label cardinality 爆**:把 trace_id 放 label → 几百万 stream → Loki OOM。`max_streams_per_user` 限制。 2. **chunk size 配置**:默认 1.5 MB chunk,业务量大改 5 MB 减 IO。 3. **regex 慢**:`|~ "complex.*regex"` 全 chunk 扫。先 label filter 再 `|=` 关键词 prefilter,最后 regex。 4. **retention 策略**:默认 chunks 不删。配 `compactor` + retention policy。 5. **promtail 漏 log**:log file rotate 时 inode 变 → promtail 没 follow。`stat_config` 调 polling。

React Hooks 几个少有人讲清楚的规则 + 反模式

## 起因 新人 React 开发普遍熟"useState / useEffect / useMemo / useCallback" 名字 但用法常踩坑。下面是我帮 review code 时反复指出的 6 个点。 ## 1. Hooks 只能在 React 函数 top-level 调 ```tsx // ❌ function MyComp({ user }) { if (user) { const [name, setName] = useState(user.name) // 错!hook 在 if 里 } return ... } // ✅ function MyComp({ user }) { const [name, setName] = useState(user?.name ?? '') // hook 在 top level,逻辑在 hook 之后 ... } ``` 为什么:React 用 hook 调用**顺序** 来匹配 state。条件调用 → 顺序变 → state 错位。 eslint-plugin-react-hooks 的 `rules-of-hooks` 规则自动检查。 **必须开**。 ## 2. useEffect 不是 "componentDidMount" 新人常用法: ```tsx useEffect(() => { fetchData().then(setData) }, []) // 类似 componentDidMount ``` 问题: - React Strict Mode 在 dev 双调用 → effect 跑两次 - 切 prop 不重 fetch - cleanup 经常漏写 更深的问题:**useEffect 是同步外部系统**,不是 lifecycle hook。 ```tsx // useEffect 真正的用途 useEffect(() => { // 同步外部:subscribe DOM event / WebSocket / 第三方库 const sub = thirdPartyLib.subscribe(handler) return () => sub.unsubscribe() // cleanup 必须 }, [handler]) ``` 数据获取应该用 React Query / SWR / RSC,不是 useEffect。 事件订阅 / 第三方库初始化才用 useEffect。 ## 3. dependency array 不能撒谎 ```tsx function MyComp({ userId }) { useEffect(() => { fetchUser(userId).then(setUser) }, []) // ❌ 用到 userId 但没写依赖 → 切 userId 不 refetch } ``` eslint `exhaustive-deps` 规则强制: ```tsx useEffect(() => { fetchUser(userId).then(setUser) }, [userId]) // ✅ ``` 不要为了"骗" linter 写 `// eslint-disable-next-line`。 真要省 refetch 改其它策略(debounce / 单独 ref)。 ## 4. 不要在 useEffect 里 setState 然后依赖那个 state ```tsx function MyComp() { const [count, setCount] = useState(0) useEffect(() => { setCount(c => c + 1) // ❌ 触发 effect 重跑 → setState → 无限循环 }, [count]) } ``` 修正取决于意图: ```tsx // 只初始化一次 useEffect(() => { setCount(initialCount) }, []) // 但更好的是用 useState 初值 // 派生 state 用 useMemo 或直接计算 const doubled = count * 2 // 不需要 state ``` 90% "我有个 state 依赖另一个 state" 都该用 useMemo / 直接计算。 ## 5. useMemo / useCallback:默认不要用 ```tsx function MyComp({ items }) { const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]) return <span>{total}</span> } ``` `reduce` 100 个 item 极快(毫秒级)。 `useMemo` 本身的 hook 开销 + 依赖比较 > 计算成本。 **实际更慢**。 useMemo 真的有用的时候: - 计算确实贵(profile 显示 > 16ms) - 结果作为 React.memo 子组件的 prop - 结果作为另一个 useEffect 的依赖(保持稳定) ```tsx // ✅ 派生值传给 memoized 子组件 const filtered = useMemo(() => items.filter(complex), [items, filter]) return <MemoedList items={filtered} /> // ✅ 给 useEffect 稳定依赖 const config = useMemo(() => ({ url, timeout }), [url, timeout]) useEffect(() => { subscribe(config) }, [config]) ``` 不传 React.memo / 不进 useEffect deps,useMemo 多余。 React 19 + React Compiler 会自动决定哪些 memo,写代码时不用关心。 ## 6. setState 是异步的,但批量也异步 ```tsx const [count, setCount] = useState(0) function handleClick() { setCount(count + 1) setCount(count + 1) setCount(count + 1) // count 只 +1,不是 +3 console.log(count) // 仍是 0(这次 render 的 count) } ``` 原因: 1. React 同一 event 内 batch setState(多次合并成一次 render) 2. `count` 是闭包捕获的旧值 修正:函数式 setState ```tsx function handleClick() { setCount(c => c + 1) setCount(c => c + 1) setCount(c => c + 1) // count +3 } ``` `c => c + 1` 每次拿到最新 state。 读最新值(罕见用例): ```tsx function handleClick() { setCount(c => c + 1) // 这一行后 count 仍是旧值(要等下次 render) // 要立刻拿新值用 ref } ``` 要立刻执行更新后效果用 `flushSync`(React 18+): ```tsx import { flushSync } from 'react-dom' flushSync(() => { setCount(c => c + 1) }) // 这里 DOM 已经更新到新 count ``` 性能差,少用。 ## 7. ref vs state ```tsx // state:用作 UI 输出 const [count, setCount] = useState(0) // ref:用作"persistence 但不触发 render" const timerRef = useRef<NodeJS.Timer | null>(null) const renderCountRef = useRef(0) ``` ```tsx useEffect(() => { timerRef.current = setInterval(...) return () => clearInterval(timerRef.current) }, []) ``` 何时用 ref: - DOM ref(input focus) - 长生命周期 mutable(timer / WebSocket / observer) - 计数器 / debounce token(不需要 re-render) 何时用 state: - 视图反映的数据 经验:**只要 UI 不看这个值就用 ref**。 ## 8. custom hook:纯函数 + 命名 use 开头 ```tsx function useDebounce<T>(value: T, delay: number): T { const [debounced, setDebounced] = useState(value) useEffect(() => { const t = setTimeout(() => setDebounced(value), delay) return () => clearTimeout(t) }, [value, delay]) return debounced } // 用 function Search() { const [input, setInput] = useState('') const debouncedInput = useDebounce(input, 500) useEffect(() => { search(debouncedInput) }, [debouncedInput]) } ``` custom hook 内部调其它 hook → 受 hook rules 约束。 命名必须 `use` 开头 linter 才识别 + 启用规则检查。 ## 9. 控制 vs 非控制 input ```tsx // 控制(推荐):state 是真相 const [val, setVal] = useState('') <input value={val} onChange={e => setVal(e.target.value)} /> // 非控制:DOM 是真相 const ref = useRef<HTMLInputElement>(null) <input defaultValue="hello" ref={ref} /> const value = ref.current?.value ``` 控制:每次 input 触发 re-render(小开销,大表单累积)。 非控制:性能好但 React 不知道 value,复杂表单要混用。 react-hook-form 用非控制 + ref 拿值,避开 re-render,是高性能表单 首选。 ## 10. 经典反模式:把 hook 当 class state ```tsx // ❌ 一堆 useState const [name, setName] = useState('') const [email, setEmail] = useState('') const [phone, setPhone] = useState('') const [age, setAge] = useState(0) ``` 或者: ```tsx // ❌ 一个 object state(更新很麻烦) const [form, setForm] = useState({ name: '', email: '', phone: '', age: 0 }) setForm({ ...form, name: 'Alice' }) // 容易漏字段 ``` 用 useReducer 或外部 state(Zustand / Jotai / react-hook-form): ```tsx const [form, dispatch] = useReducer(formReducer, initialForm) ``` 复杂状态用专门工具。 ## 调试 hooks React DevTools → Components → 选组件 → Hooks 一栏。 能看到每个 hook 当前值。注意 useReducer / useMemo 显示有限。 ## 总结 | 反模式 | 该做的 | |---|---| | useEffect 做 fetch | React Query / SWR / RSC | | useEffect 依赖 state 触发更新 | 直接派生 / useMemo | | 无脑 useMemo / useCallback | 默认不用,profile 后按需 | | dependency array 写 [] 撒谎 | 实事求是 + lint | | 闭包 setState 用旧值 | `setX(prev => ...)` | | 一堆 useState 平铺 | useReducer / 第三方 store | | 在 if / loop 调 hook | 顶层调用,逻辑在内部分支 | 熟悉这些后写 React 体感顺很多 + bug 少很多。 ## 踩过的坑 1. **strict mode 双调 effect**:dev 模式 effect 跑两次。cleanup 必须 做对,否则 subscribe / setTimeout 等"两个跑半个" 状态。 2. **useEffect 跑两次拉两次 API**:dev 干扰开发体验。 `<StrictMode>` 内层包不要去 / 或者用 React Query 等自动 dedup。 3. **dep 数组的 object / function 每次新引用**: ```tsx useEffect(..., [{ x: 1 }]) // 每次都"新对象" ``` 触发 effect 每次都跑。用 stable ref 或 useMemo。 4. **custom hook 名没 use 开头**:lint 不识别 → 规则不生效 → 隐藏 bug。 5. **React 19 / Compiler 期望写法**:未来 useMemo / useCallback 大多 不需要。养成"先简洁后优化" 的习惯。

vLLM 部署一个高吞吐量 LLM 推理服务(PagedAttention)

直接用 HuggingFace transformers 跑 LLM 推理性能很差: batch 1 时 GPU 利用率 30-50%,多并发请求时显存碎片化 OOM。 vLLM 是伯克利出的高性能 LLM 推理引擎,核心技术是 **PagedAttention** (像 OS 分页一样管理 KV cache),加上 continuous batching, 比 transformers 直接推理快 5-24 倍。 ## 安装 ```bash uv add vllm # 需要 CUDA 11.8+ 或 12.x,PyTorch 2.x ``` ## 命令行起服务 ```bash uv run vllm serve Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --port 8000 ``` 第一次启动会从 HuggingFace 下载模型(~15GB)。 启动后默认 OpenAI 兼容 API。 ## 调用 ```bash curl http://localhost:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{ "model": "Qwen/Qwen2.5-7B-Instruct", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 200 }' ``` 或 Python: ```python from openai import OpenAI client = OpenAI(base_url='http://localhost:8000/v1', api_key='dummy') resp = client.chat.completions.create( model='Qwen/Qwen2.5-7B-Instruct', messages=[{'role': 'user', 'content': '你好'}], max_tokens=200, ) print(resp.choices[0].message.content) ``` ## 关键参数 - `--max-model-len`:上下文最大长度(影响 KV cache 大小) - `--gpu-memory-utilization`:用多少显存(0-1,默认 0.9) - `--tensor-parallel-size`:多 GPU 时拆 tensor 并行(4 卡设 4) - `--quantization awq` / `gptq` / `fp8`:量化加速 - `--enable-prefix-caching`:相同前缀的请求复用 KV cache(系统 prompt 共享场景大幅加速) ## Python 直接调用(不走 HTTP) ```python from vllm import LLM, SamplingParams llm = LLM(model='Qwen/Qwen2.5-7B-Instruct', gpu_memory_utilization=0.85) prompts = [ '介绍一下 RAG', '解释 PagedAttention', '写一个 Python 二分查找', ] params = SamplingParams(temperature=0.3, max_tokens=300) outputs = llm.generate(prompts, params) for out in outputs: print('---') print(out.outputs[0].text) ``` vLLM 自动 batch 这 3 个 prompt 一起跑,单次 forward 处理多个序列。 ## continuous batching 的含义 传统推理: ``` batch 1: [seq A 100 tokens, seq B 80 tokens, seq C 60 tokens] 等三个序列都完成才能下一个 batch ``` continuous batching: ``` 任意时刻一个请求结束就立刻让出位置给新请求 GPU 持续吃满,无 idle ``` 这是 vLLM 高吞吐的核心,比"动态 batch"更激进。 ## benchmark:vs 直接 transformers ```bash # 100 个并发请求,每个生成 200 token # vLLM ab -n 100 -c 16 -p body.json -T application/json \ http://localhost:8000/v1/chat/completions # 通常:3000-8000 tokens/s 吞吐 # transformers + 简单 FastAPI 包装 # 通常:300-800 tokens/s ``` 10x 量级的吞吐差距。 ## 多卡:tensor parallelism 70B 模型单卡装不下,4 张 A100 拆开: ```bash uv run vllm serve meta-llama/Llama-3.1-70B-Instruct \ --tensor-parallel-size 4 \ --max-model-len 8192 ``` vLLM 自动用 NCCL 在 4 卡间分配 attention head / FFN 矩阵。 ## 量化:让更大模型跑在更小显卡 ```bash # AWQ 4-bit uv run vllm serve TheBloke/Llama-3.1-70B-AWQ \ --quantization awq # 4 bit 量化的 70B 大约 40GB 显存(不量化要 140GB) ``` ```bash # FP8 (需要 H100) uv run vllm serve meta-llama/Llama-3.1-70B-Instruct \ --quantization fp8 ``` ## 长上下文 ```bash # 32k 上下文 uv run vllm serve Qwen/Qwen2.5-7B-Instruct \ --max-model-len 32768 ``` 但 KV cache 占显存 = batch_size × max_seq_len × 每层 KV size。 32k context + 100 batch ≈ 显存吃 50%+,要 trade off。 ## 与 Hugging Face 模型生态 vLLM 支持的 model:Llama / Mistral / Qwen / Mixtral / Gemma / Yi / DeepSeek / Phi / Baichuan / ChatGLM 等几乎全部主流开源 LLM。 官方维护清单看 vLLM docs。 ## 与 sglang / lmdeploy 对比 | 引擎 | 优势 | 劣势 | |---|---|---| | vLLM | 生态最大、模型最多 | 长上下文性能一般 | | TGI (HF) | HF 官方、生产稳 | 吞吐略低于 vLLM | | sglang | 结构化生成(JSON / regex)极快 | 模型支持稍少 | | lmdeploy | 国内(商汤)、TurboMind 后端快 | 文档不全 | 通用选 vLLM;要求 JSON 严格输出选 sglang。 ## prefix caching:相同系统 prompt 复用 ```bash uv run vllm serve qwen2.5:7b --enable-prefix-caching ``` 所有请求都用 "You are a helpful assistant..." 起头的话, prefix 这部分的 KV cache 只算一次,10k token 系统 prompt 几乎免费。 ## 生产部署清单 1. 用 systemd 起 vLLM service 2. 前面套 nginx 反代(限流 + auth) 3. Prometheus 抓 vLLM 内置的 `/metrics` 4. health check:`/health` 5. 多模型用多个 vLLM 进程,每个绑不同 GPU ## 踩过的坑 - 启动时 "out of memory":`--gpu-memory-utilization` 调小, 或减 `--max-model-len`。 - 模型权重 download 慢:用 HuggingFace mirror 或预先下载, `HF_HUB_OFFLINE=1` 让 vLLM 不再尝试下载。 - TP > 1 时 NCCL 卡死:检查机器内 GPU 互联(PCIe / NVLink); 设 `NCCL_P2P_DISABLE=1` 排查。 - vLLM 0.5+ 跟 PyTorch 2.4+ 紧耦合,旧 PyTorch 装不上。`uv` 自动解析 依赖一般没问题,手动 pip 时容易翻车。

给 sudoers 加一条 NOPASSWD 规则(最小授权 + 别动 visudo 之外的)

经常需要让某用户 / 自动化脚本运行少数特权命令,但又不想 sudo 让对方做任何事, 也不想每次输密码。正确做法是写一条 **最小授权** 的 NOPASSWD 规则。 ## 三条原则 1. **永远用 `visudo`** —— 改坏 `/etc/sudoers` 没法回滚,全机 sudo 失效 2. **放到 `/etc/sudoers.d/*` 而不是主文件** —— 单文件出错只影响一个 drop-in 3. **绝对路径 + 精确命令** —— 别用 wildcard,更别 `ALL` ## 例子:让 deploy 用户重启某个特定服务 ```bash sudo visudo -f /etc/sudoers.d/deploy ``` 内容: ``` # 让 deploy 用户不需要密码就能重启 app.service,且只能这个 deploy ALL=(root) NOPASSWD: /bin/systemctl restart app.service, \ /bin/systemctl status app.service, \ /bin/systemctl reload nginx.service ``` 注意三件事: - 第一个 `ALL` 是允许在 **哪些 host** 上有效(sudoers 是网络共享时有用) - `(root)` 是允许切换为哪个用户身份 - 命令必须是 **绝对路径**,并且每个参数都精确写出 如果只想允许重启 `app-*` 系列服务: ``` deploy ALL=(root) NOPASSWD: /bin/systemctl restart app-*.service ``` 注意 `*` 在 sudoers 里是 glob,能匹配 `app-frontend.service` 但也能匹配 `app-evil.service` —— 评估清楚再用。 ## 验证 + 测试 写完保存。`visudo` 在退出时做语法检查;有错会让你重新编辑,不会 落盘错配置。 ```bash # 用 deploy 身份测试 sudo -u deploy -i sudo -l # 列出该用户被允许的命令 sudo systemctl restart app.service # 应当不要密码就成功 sudo systemctl restart sshd.service # 应当拒绝 ``` ## 常见反模式(千万别) ``` # 错: 把整个 root 让出去 deploy ALL=(ALL) NOPASSWD: ALL # 错: 用 sudo 跑 shell deploy ALL=(root) NOPASSWD: /bin/bash # 完全等价于 NOPASSWD: ALL # 错: 允许跑文本编辑器 deploy ALL=(root) NOPASSWD: /usr/bin/vim # 在 vim 里 :!bash 就提权了 # 错: 用相对路径 deploy ALL=(root) NOPASSWD: systemctl restart app.service # 攻击者改 PATH 就能换 systemctl 为他自己的脚本 ``` `sudoedit`(即 `sudo -e`)是安全的编辑器封装,它把文件 copy 到临时位置 让用户编辑,结束后再 mv 回去。要让用户编辑 `/etc/foo.conf`: ``` deploy ALL=(root) NOPASSWD: sudoedit /etc/foo.conf ``` 调用:`sudo -e /etc/foo.conf`,用 `$EDITOR` 打开编辑。 ## CI / 自动化的特殊建议 CI runner 上的 NOPASSWD 列表是攻击面,越短越好。如果可能: 1. 用 `systemctl --user` + lingering,不需要 root 2. 用 systemd 套接字激活,CI 只 push 文件,service 自动 reload 3. 把命令写成"无参数的"脚本:CI 只能跑 `/usr/local/bin/redeploy-app`, 脚本内部硬编码 systemctl,攻击面收敛到一个文件 ## 强制日志 `/etc/sudoers.d/00-logging`: ``` Defaults log_input Defaults log_output Defaults iolog_dir="/var/log/sudo-io/%{user}" Defaults !syslog Defaults logfile="/var/log/sudo.log" ``` `log_output` 会把每次 sudo 会话的输入输出记下来,需要时可以重放: ```bash sudo cat /var/log/sudo.log sudo sudoreplay -d /var/log/sudo-io deploy/00/00/01 ``` ## 踩过的坑 - 不在 `/etc/sudoers.d/` 而直接改 `/etc/sudoers`:升级时 dpkg 可能问你 "保留本地修改 / 用新版",不留神就丢配置。 - drop-in 文件名 **不能** 有 `.`(包括 `.bak`);sudo 默认忽略带点的。 我们一律 .conf 后缀也不要,比如 `01-deploy`。 - `NOPASSWD` 的命令列表里如果有逗号但漏写空格,会被解析成单个长命令名, 匹配不上。每个命令前后都加空格保险。 - 给同一用户配多条规则时,**最后一条生效**。所以放更宽松的规则在前面、 更严格的在后面要小心。最佳实践:每个用户一份 drop-in 文件,规则集中。