知识广场
按学科筛选:计算机科学
«计算机科学» 分类下共 256 篇帖子
每个项目都有自己的环境变量(API key / DB URL / venv 路径 / Node 版本)。 手动 `source .env` 麻烦且容易跨项目污染。direnv 让 shell 在 cd 进项目 目录时自动加载 `.envrc`,离开时自动卸载。 ## 安装 ```bash sudo apt install -y direnv # 或 brew install direnv direnv version ``` ## 集成 shell ```bash # bash echo 'eval "$(direnv hook bash)"' >> ~/.bashrc # zsh echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc # fish echo 'direnv hook fish | source' >> ~/.config/fish/config.fish ``` 重新加载 shell。 ## 第一次用 ```bash cd ~/projects/myapp echo 'export DATABASE_URL=postgresql://localhost/myapp' > .envrc echo 'export STRIPE_KEY=sk_test_xxxxx' >> .envrc echo 'export FLASK_DEBUG=1' >> .envrc # direnv 提示: # direnv: error /home/me/projects/myapp/.envrc is blocked. # Run `direnv allow` to approve its content direnv allow # 之后每次 cd 进来: # direnv: loading ~/projects/myapp/.envrc # direnv: export +DATABASE_URL +STRIPE_KEY +FLASK_DEBUG env | grep DATABASE_URL # DATABASE_URL=postgresql://localhost/myapp ``` cd 离开目录: ```bash cd .. # direnv: unloading env | grep DATABASE_URL # 没了 ``` ## allow 机制 direnv 不会自动加载未授权的 `.envrc`(防止 git clone 别人仓库就执行 恶意代码)。每次 `.envrc` 内容变了都要重新 `direnv allow`。 ```bash direnv allow . # 授权 direnv deny . # 撤销 direnv reload # 强制重新加载 ``` ## stdlib:常用 helper direnv 自带一组 helper 函数: ```bash # .envrc use python 3.12 # 自动用 pyenv 切到 3.12 layout python python3.12 # 创建 .direnv/python-3.12 venv 并激活 dotenv # 自动读 .env 文件里的 KEY=VAL PATH_add bin # 把项目 bin/ 加进 PATH(自动相对路径) source_up # 也加载上一层目录的 .envrc ``` 完整:`direnv stdlib | less`。 ## 真实例子:Python 项目 ```bash # .envrc layout python python3.12 dotenv # 加 project 本地 bin 到 PATH PATH_add bin PATH_add scripts # Django settings export DJANGO_SETTINGS_MODULE=myapp.settings.dev export DJANGO_SECRET_KEY=$(cat .secret-key 2>/dev/null || echo 'dev-key') ``` cd 进去时: 1. 自动创建 / 激活 `.direnv/python-3.12/` venv 2. 读 `.env` 注入 KEY=VAL 3. 把 bin/ 加进 PATH `pip install ...` 装的依赖就在这个 venv 里,不污染全局。 ## Node 项目 ```bash # .envrc use node 20 # 配合 nvm / fnm / asdf 切版本 PATH_add node_modules/.bin # 让 npx 命令直接 in PATH ``` ## Rust 项目 ```bash # .envrc PATH_add target/debug PATH_add target/release ``` ## 多版本工具切换(asdf / mise) ```bash # .envrc use mise # 让 direnv 触发 mise 的环境 ``` mise 会按 `.tool-versions` 自动切 Node/Python/Go/etc 版本, direnv 让这套在 cd 时自动应用。 ## .env vs .envrc | 文件 | 作用 | |---|---| | `.envrc` | direnv 配置(bash 脚本,可写逻辑) | | `.env` | 简单 `KEY=VAL` 列表,被 `dotenv` 读 | | `.envrc.local` | 个人覆盖(建议加 `.gitignore`) | ```bash # .envrc dotenv .env dotenv_if_exists .env.local # 个人覆盖 source_env_if_exists .envrc.local ``` ## 与 IDE 集成 VSCode 不会自动用 direnv。装插件 `mkhl.direnv` 让 VSCode 在打开项目 时执行 `.envrc` 并把变量塞给 terminal / debugger。 JetBrains 系也有 direnv 插件。 ## 安全注意 `.envrc` 是 bash 脚本,能执行任意命令。git clone 后 direnv 不会自动 load, 必须 `direnv allow`——这是 feature。 但养成习惯:clone 完之后 **先 cat .envrc 看一眼** 再 allow。 某些恶意 `.envrc` 在你 allow 时执行 `rm -rf /` 你没法找回来。 ## 调试 ```bash direnv status # 当前目录的 direnv 状态 direnv exec . env # 看 direnv 实际注入了哪些变量 DIRENV_LOG_FORMAT=... # 让加载日志更详细 ``` ## 踩过的坑 - 在 docker / SSH session 里没生效:`direnv hook` 没运行,rc 文件不被 source。`bash -ic '...'` 可以强制 interactive。 - `.envrc` 里 `cd ..` —— 别这么写,direnv 钩到 cd,会无限循环。 - 在 git 钩子 / cron 里 cd 进项目目录,direnv 不会触发(cron 没 shell hook), 环境变量不会注入。要么显式 `source .envrc`,要么 `direnv exec . command`。 - venv 在 macOS 上启动慢:每次 cd 进出都重新激活。如果不需要 venv 隔离, 用 `dotenv` 而非 `layout python` 跳过创建 venv。
## 起因 要给 `*.example.com` 申通配符证书(一个证书管所有子域名)。 普通 `--nginx` / `--apache` certbot 用 HTTP-01 challenge, **不支持通配符** —— LE 强制通配符必须用 DNS-01 challenge。 DNS-01:在 DNS 加一条 `_acme-challenge.example.com TXT` 记录, LE 来查这条 → 验证你控制这个域。 手动加 TXT 记录烦 + 续期时还要再加 → 自动化 DNS API 是必经之路。 ## 用 certbot + Cloudflare API ### 1. 拿 Cloudflare API token Cloudflare 控制台 → 我的个人资料 → API 令牌 → 创建令牌: 权限: - Zone:Zone:Read - Zone:DNS:Edit 资源:包括 → 特定区域 → example.com 复制 token。 ### 2. 装 certbot + cloudflare 插件 ```bash sudo apt install -y certbot python3-certbot-dns-cloudflare ``` ### 3. 配置 token 文件 ```bash sudo mkdir -p /etc/letsencrypt sudo nano /etc/letsencrypt/cloudflare.ini ``` ``` dns_cloudflare_api_token = your-token-here ``` ```bash sudo chmod 600 /etc/letsencrypt/cloudflare.ini ``` ### 4. 申请通配符证书 ```bash sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ --dns-cloudflare-propagation-seconds 30 \ -d '*.example.com' \ -d 'example.com' \ --agree-tos --no-eff-email \ -m [email protected] ``` 要点: - `-d '*.example.com' -d 'example.com'`:通配符 + 裸域名要分别 list (通配符不 cover 裸域名 + apex domain) - `--dns-cloudflare-propagation-seconds 30`:让 LE 查询前等 30 秒 让 DNS 传播 成功输出: ``` Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem ``` ### 5. nginx 配置 ```nginx server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name app.example.com api.example.com blog.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # 加强配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_dhparam /etc/ssl/dhparam.pem; # openssl dhparam -out ... 4096 # OCSP stapling ssl_stapling on; ssl_stapling_verify on; location / { proxy_pass http://localhost:8080; } } ``` ```bash sudo nginx -t && sudo systemctl reload nginx ``` ### 6. 自动续期 certbot 装好后默认 systemd timer: ```bash sudo systemctl list-timers certbot.timer # certbot.timer active next run in ~12h ``` 每 12 小时检查证书;< 30 天到期时续。 确认续期能跑通: ```bash sudo certbot renew --dry-run ``` 成功输出说明真续期会 work。 ### 7. 续期后 reload nginx certbot hook: ```bash sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy sudo tee /etc/letsencrypt/renewal-hooks/deploy/nginx-reload <<'EOF' #!/bin/bash systemctl reload nginx EOF sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload ``` 证书续期成功后自动 reload nginx,新证书生效。 ## 其它 DNS provider `certbot-dns-*` 插件覆盖大多数: ```bash sudo apt install -y \ python3-certbot-dns-cloudflare \ python3-certbot-dns-route53 \ python3-certbot-dns-google \ python3-certbot-dns-digitalocean \ python3-certbot-dns-rfc2136 # 用于 bind 等支持 RFC2136 的服务器 ``` 完整列表 see certbot docs。 ### Route53 / AWS ```bash sudo certbot certonly \ --dns-route53 \ -d '*.example.com' -d 'example.com' ``` 凭据从 `~/.aws/credentials` 或 instance role。 ### Google Cloud DNS ```bash sudo certbot certonly \ --dns-google \ --dns-google-credentials /etc/letsencrypt/gcloud-svc.json \ -d '*.example.com' ``` 需要 service account JSON。 ## 用 acme.sh(轻量替代) ```bash curl https://get.acme.sh | sh export CF_Token="..." export CF_Zone_ID="..." acme.sh --issue --dns dns_cf -d '*.example.com' -d 'example.com' ``` acme.sh 是 shell 写的,比 certbot 轻量 + 不依赖 Python。 内置上百个 DNS provider。功能等价。 ## Caddy 一行搞定 ``` *.example.com { tls { dns cloudflare {env.CF_API_TOKEN} } reverse_proxy localhost:8080 } ``` `sudo caddy add-package github.com/caddy-dns/cloudflare` 装好插件后 这就够了。Caddy 自动 ACME + DNS-01 + 续期,零额外配置。 如果选 Caddy 作反代,完全不用 certbot。 ## DNS-01 vs HTTP-01 | | DNS-01 | HTTP-01 | |---|---|---| | 通配符 | ✅ | ❌ | | 需要 80 端口 | ❌ | ✅ | | DNS provider API | ✅ | ❌ | | 域名 ACME 验证 | 通过 DNS | 通过 HTTP | | 内网 / 防火墙后服务器 | ✅(DNS 在云端) | ❌(需公网 80) | | 实施复杂度 | 中 | 简单 | 总结: - 普通单域名 + 公网 → HTTP-01 - 通配符 / 内网 / 多服务器 → DNS-01 ## Let's Encrypt 限制 - 每周每证书 5 次重复申请(同样 SAN 列表) - 每周 50 个不同 SAN 组合 - 失败计数:连续 5 次 fail 后 1 小时锁 - 全球每周每注册域名 300 张新证书 正常使用碰不到。**测试时用 staging server** 避免被限: ```bash certbot ... --staging ``` 或者用 ZeroSSL / BuyPass / Google CA 等其它 ACME provider 分散。 ## 监控证书到期 ```bash # 看到期时间 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate # notAfter=Jul 15 12:00:00 2024 GMT ``` Prometheus blackbox_exporter: ```yaml - name: tls_cert_not_after url: https://example.com threshold_days: 14 # < 14 天告警 ``` 监控让你看 certbot 是否真的在续。一次因 cron 没跑而过期 → 客户全报错 红色警告,痛。 ## 踩过的坑 1. **通配符不 cover 二级**:`*.example.com` 不包含 `example.com` 本身(apex), 也不包含 `*.sub.example.com`。第二级通配符要单独申 `*.api.example.com`。 2. **API token 权限不够**:缺 DNS:Edit → 加 TXT 失败 → certbot 报 "rate limit" 误导。先 `dig _acme-challenge.example.com TXT` 验证。 3. **DNS propagation 时间**:cloudflare 一般 30s 内全球同步。 AWS / DigitalOcean 偶尔慢,调 `--propagation-seconds 60`。 4. **renew dry-run 成功 → 真 renew 失败**:dry-run 用 staging API, 生产可能 rate limit。看 `journalctl -u certbot.service` 真实日志。 5. **fullchain vs cert**:nginx 用 `fullchain.pem`(含中间证书)。 写成 `cert.pem` 浏览器报"untrusted intermediate"。
## 起因 经常遇到这场景: - 当前 feature 分支开发到一半 - 紧急 hotfix 需要在 main 改一行 - 又要 review 同事的 PR 在 third-branch 老办法:`git stash` + `git checkout main` + 改 + commit + `checkout feature` + `stash pop`。每次切换几分钟(编辑器要重 index、deps 不一样还要重装、 本地服务要重启)。 `git worktree` 解决:**一个仓库,多个工作目录,分别 checkout 不同分支**。 ## 基本用法 ```bash cd ~/projects/myapp # 主 worktree(main 分支) # 创建新 worktree 在 ../myapp-hotfix,checkout hotfix-x 分支 git worktree add ../myapp-hotfix -b hotfix-x # 创建在 ../myapp-review,checkout 现有 PR 分支 git worktree add ../myapp-review feature/pr-42 # 列出所有 git worktree list # /home/u/projects/myapp abcd1234 [main] # /home/u/projects/myapp-hotfix ef567890 [hotfix-x] # /home/u/projects/myapp-review 12345abc [feature/pr-42] # 完成后删 git worktree remove ../myapp-hotfix ``` 每个 worktree 是**独立目录**:自己的 working tree、index、stash、 hooks 状态。但共享 `.git/`(共享对象库、refs、config)。 磁盘只增加 working tree 文件大小,不重复存历史。 ## 我的目录约定 ``` ~/projects/myapp/ # 主 worktree(main) ~/projects/myapp-wt/ # 所有附加 worktree 都进这里 ├── feature-search/ ├── hotfix-401/ └── review-bob-pr/ ``` `-wt/` 在 `.gitignore` 里加(实际上 git 不会管它),也加到编辑器的 排除列表,避免 indexing 重复。 ## 效果 切换分支从"清理 + checkout + 重装 deps + 重启服务" → "cd 到另一个目录"。 - 编辑器在每个目录独立打开 - 每个 worktree 跑自己的 dev server(端口不同) - venv / node_modules 独立(cargo / go 缓存可以共享) 实测:从 hotfix 切回主 feature 从平均 5 分钟 → 5 秒。一周省 1 小时。 ## 自动化 每次 review 同事 PR 都建 worktree,写个 alias: ```bash # ~/.zshrc function wt-pr() { local pr=$1 gh pr checkout $pr # 拉到本地分支 local branch=$(git branch --show-current) cd .. git -C myapp worktree add "myapp-pr-$pr" "$branch" cd "myapp-pr-$pr" } ``` `wt-pr 42` → 自动 fetch + 建 worktree + cd 过去。 ## venv / node_modules 怎么办 每个 worktree 独立装: ```bash cd ../myapp-hotfix uv sync # 装到本地 .venv/ pnpm install # 装到本地 node_modules/ ``` uv / pnpm 用 hard link / 全局 cache → 实际硬盘开销小。 n × 同 deps 不会真的 n 倍磁盘。 ## 共享 hooks `.git/hooks/` 是共享的(git 内部目录共用)。 每个 worktree 都跑同样的 pre-commit。一般是好事。 个别 worktree 想跳过 hook → `--no-verify`。 ## 配 IDE VS Code:每个 worktree 当成独立 workspace 打开。 WebStorm / IntelliJ:同样。 Cursor:可以在多个窗口打开不同 worktree 同时操作。 `.vscode/settings.json` 通过 git ignore 的方式各自配置。 ## 与 stash / checkout 对比 | 场景 | stash + checkout | worktree | |---|---|---| | 切换速度 | 慢(要重 index / restart) | 快(cd 即可) | | 心智模型 | 一个目录多状态 | 多目录多状态(更直觉) | | 编辑器 indexing | 重做 | 不需要 | | 磁盘 | 少 | 多 working tree(但对象库共享) | | 临时草稿 | stash 容易丢 | 文件实存 | 90% 场景 worktree 胜。 ## 与 git clone 对比 git clone 多次克隆同 repo: - 优点:完全独立,无干扰 - 缺点:每个独立 `.git/`(重复存对象库,几百 MB → 几 GB)+ 不同 clone 之间分支不互通 worktree 共享 `.git/`,省空间 + 一处 fetch 全部能看到新 remote refs。 ## main worktree 不能删 ```bash git worktree remove . # error: cannot remove main working tree ``` 主 worktree 是创建仓库时所在的目录。要换主 worktree 要 `git worktree move`(少用)。 ## 踩过的坑 1. **同分支不能两个 worktree checkout**:保险机制防止冲突。 要看同一分支就建一个 detached HEAD worktree: `git worktree add ../tmp HEAD`。 2. **worktree 删除后 git 不会清理元数据**:`git worktree prune`。 或者用 `remove` 命令而不是手动 `rm -rf`。 3. **submodule + worktree**:早期 git submodule 在多 worktree 不太稳定。git 2.40+ 改善很多但仍偶有奇怪行为。 4. **CI 不支持 worktree**:CI clone 出来是单 worktree。worktree 是本地开发优化,不影响 CI。 5. **跨平台 path 差异**:worktree path 存在 `.git/worktrees/*/gitdir` 绝对路径。把仓库目录改名 / 移动 → worktree 失效。 用 `git worktree repair` 修。
ufw(Uncomplicated Firewall)是 iptables/nftables 的人话封装。 新服务器装好后第一件事就该把它配上 —— 比起裸奔,门槛降到 5 分钟, 但能挡住 99% 的扫描流量噪音。 ## 安装 + 默认策略 ```bash sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing ``` `deny incoming` 是关键 —— 默认拒绝所有进来的连接,靠白名单放行。 ## 放行你需要的 ```bash sudo ufw allow ssh # 22/tcp sudo ufw allow http # 80/tcp sudo ufw allow https # 443/tcp # 或者指定端口 sudo ufw allow 8080/tcp comment 'app dev server' # 限制来源 IP sudo ufw allow from 192.168.1.0/24 to any port 5432 comment 'pg from LAN' # 限制速率(防 SSH 暴力破解) sudo ufw limit ssh comment 'rate-limit SSH' ``` `ufw limit` 等价于 "同 IP 30 秒内 6 次连接就 deny",比 fail2ban 轻量但 粒度粗。生产服务器两个一起用最稳。 ## 启用 + 校验 ```bash # !!! 启用前确认 22 已经 allow !!! sudo ufw enable sudo ufw status verbose # Status: active # Default: deny (incoming), allow (outgoing), deny (routed) # To Action From # -- ------ ---- # 22/tcp LIMIT IN Anywhere # 80/tcp ALLOW IN Anywhere # ... ``` 如果你是 SSH 连进去配的,开 ufw 之前 **务必先 allow ssh**,否则当场断开。 ## 实际生产建议 ```bash # 改 SSH 端口(不是安全提升但能省 99% 噪音日志) sudo sed -i 's/^#\?Port .*/Port 2200/' /etc/ssh/sshd_config sudo systemctl reload ssh sudo ufw delete allow ssh sudo ufw allow 2200/tcp comment 'ssh on non-standard port' sudo ufw limit 2200/tcp # Docker 装上后会绕过 ufw(默认改 iptables 但 ufw 看不到) # 解决:用 DOCKER-USER 链 + ufw-docker(搜这个项目) ``` ## 日志 ```bash sudo ufw logging medium # off / low / medium / high / full sudo tail -f /var/log/ufw.log ``` `medium` 已经够分析;`full` 会刷爆磁盘。 ## 应用 profiles ufw 自带常见应用的端口配置: ```bash sudo ufw app list # Available applications: # Nginx Full # Nginx HTTP # Nginx HTTPS # OpenSSH sudo ufw allow 'Nginx Full' ``` 自定义 profile 放在 `/etc/ufw/applications.d/`: ``` [my-app] title=My App description=Backend API server ports=8000,8443/tcp ``` ```bash sudo ufw app update my-app sudo ufw allow my-app ``` ## 备份 / 恢复 ```bash sudo cp /etc/ufw /etc/ufw.bak -r # /etc/ufw/user.rules 和 user6.rules 是规则文件 ``` ## 踩过的坑 - 远程改防火墙最危险。规范流程: 1. 准备 rollback 脚本:`(sleep 300 && ufw --force reset) &`,5 分钟内 新规则有问题没人手动取消,自动 reset 2. 在 screen / tmux 里操作 3. 改完后开第二个 SSH 测试,确认连得上再 kill rollback - Docker 镜像启动后默认会 publish 端口到 `0.0.0.0`,**完全绕过 ufw**。 生产里所有 Docker port mapping 都写 `127.0.0.1:8080:8080` 这种显式 bind。 - KVM / LXC 桥接网络可能让 ufw 看不到流量,需要在 `/etc/ufw/sysctl.conf` 里开 `net.bridge.bridge-nf-call-iptables=1`。
Python web 应用部署最常见的问题:worker 数怎么定?sync 还是 async? gunicorn vs uvicorn 选哪个?下面讲清楚。 ## 1. WSGI vs ASGI - **WSGI**:传统同步接口(Flask / Django 默认) - **ASGI**:异步接口(FastAPI / Starlette / Django 4+ async) WSGI 接口里每个请求一个线程同步处理;ASGI 一个 event loop 协程并发。 ## 2. 几种部署组合 ### A. gunicorn + sync workers(WSGI 经典) ```bash gunicorn -w 4 myapp:app ``` `-w 4` 起 4 个 worker 进程,每个进程同步处理一个请求。 WSGI app(Flask / Django)的默认。 并发上限 = workers 数。worker 数建议 `2 × CPU + 1`。 ### B. gunicorn + gthread ```bash gunicorn -w 4 --threads 8 myapp:app ``` 每个 worker 起 8 个线程,并发 = 4 × 8 = 32。 合适 IO 密集应用(Python 线程 GIL 不阻塞 IO)。 ### C. gunicorn + gevent / eventlet ```bash gunicorn -w 4 -k gevent --worker-connections 1000 myapp:app ``` gevent monkey-patch socket → 每个 worker 处理 1000 并发协程。 极高并发但代码必须 monkey-patch 友好(少用 C 扩展)。 适用:Django / Flask 想要异步表现但不能完全重写。 注意:psycopg2 等 C 扩展跟 gevent 不友好,要用 psycogreen 包。 ### D. uvicorn(ASGI 推荐) ```bash uvicorn myapp:app --workers 4 --host 0.0.0.0 --port 8000 ``` uvicorn 起 4 个 worker,每个跑独立 event loop。 FastAPI / Starlette 项目默认。 ### E. gunicorn + uvicorn workers(生产 ASGI 推荐) ```bash gunicorn myapp:app -w 4 -k uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --timeout 60 --keep-alive 5 ``` gunicorn 管理 worker 生命周期(restart 优雅、信号处理稳),uvicorn 处理 ASGI 协议。FastAPI 生产推荐这个。 为什么不直接 uvicorn?gunicorn 的进程管理更成熟(worker 数、reload 信号、 preload、failover)。uvicorn 单进程模式 dev 用就好。 ## 3. worker 数选择 CPU 密集(每个请求大量计算):`workers = CPU 核数` IO 密集 + 同步代码(Flask):`workers = 2 × CPU + 1`(部分 worker 阻塞时 另一个能接客) IO 密集 + async(FastAPI):`workers = CPU 核数`,依靠 event loop 内并发 具体数字看 profiling,不要拍脑袋。 ## 4. 内存 每个 worker 独立内存。Django / 大型 app 一个 worker 可能 200-500 MB。 8 worker × 300 MB = 2.4 GB 起步。 `--preload`:在 fork 前加载 app 代码,worker 共享只读 page → 节省内存: ```bash gunicorn --preload -w 4 myapp:app ``` 副作用:app 启动慢,且某些资源(DB connection / cache)不能在 fork 前 初始化。生产开 preload 时常踩这个坑。 ## 5. graceful restart ```bash # Reload code without dropping requests kill -HUP $(cat gunicorn.pid) ``` gunicorn 收到 HUP 重读代码,启动新 worker,等老 worker 处理完旧请求再退。 **代码部署的标准操作**。 uvicorn 单进程没有这个能力(要重启),所以生产基本是 gunicorn + uvicorn workers。 ## 6. timeout ```bash gunicorn --timeout 60 ... ``` 请求处理超过 60s worker 被 kill。默认 30s。慢 endpoint 设大一点; 但太大会让卡死的 worker 长期占资源。 uvicorn worker 类型支持 `--timeout-keep-alive`、`--limit-concurrency` 等。 ## 7. keepalive ```bash gunicorn --keep-alive 5 ... ``` 每条 TCP 连接保持 5s 等下一个请求。CDN / 反代后面建议 30-60s 减少 TCP 握手开销。 ## 8. systemd unit ```ini # /etc/systemd/system/myapp.service [Unit] Description=My FastAPI app After=network.target [Service] Type=notify User=app Group=app WorkingDirectory=/srv/myapp EnvironmentFile=/srv/myapp/.env # gunicorn + uvicorn worker ExecStart=/srv/myapp/.venv/bin/gunicorn myapp.main:app \ -w 4 -k uvicorn.workers.UvicornWorker \ --bind 127.0.0.1:8001 \ --timeout 60 --keep-alive 30 \ --access-logfile - --error-logfile - ExecReload=/bin/kill -s HUP $MAINPID KillMode=mixed TimeoutStopSec=30 Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ``` 注意: - `Type=notify`:gunicorn 启动好之后 notify systemd - `--access-logfile -`:日志到 stdout → systemd journal - `KillMode=mixed`:先 SIGTERM 父进程,超时再 KILL 子进程 ## 9. 前面反代(nginx) ```nginx upstream myapp { server 127.0.0.1:8001; # keepalive 减少 TCP 握手 keepalive 32; } server { location / { proxy_pass http://myapp; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; proxy_connect_timeout 5s; } } ``` `proxy_http_version 1.1 + Connection ""` 让 nginx → gunicorn 用长连接。 ## 10. uvicorn 单独的场景 dev:`uvicorn --reload`(自动热加载 + 单进程) 小测试 / sidecar:`uvicorn --workers 2 ...` 生产不直接 uvicorn(信号处理 / preload / 优雅退出都不如 gunicorn)。 ## 11. Hypercorn / Daphne 等其它 ASGI server - **Hypercorn**:纯 Python,支持 HTTP/2 + HTTP/3 - **Daphne**:Django Channels 的官方推荐 - **Granian**:Rust 写的 ASGI,号称更快 uvicorn 是事实标准,其它有特定需求才考虑。 ## 12. asgi 与 wsgi 共存 老 Django 项目想加 async 路由:用 ASGI server + django.core.asgi.get_asgi_application(), sync 视图 / async 视图都能跑。 ## 踩过的坑 - 数据库连接没在 fork 后重建:preload + gunicorn → 所有 worker 共享一个 DB connection 导致游标错乱。`postworkerfork` hook 里重置 DB pool。 - worker 数太多:CPU 都被 context switch 吃了。8 核机器开 64 worker 是反优化。 - timeout 太短:报告 / 导出大数据的 endpoint 几十秒被 kill。慢 endpoint 设单独 timeout 或异步化(Celery)。 - 异步框架里 await 一个同步 IO(pandas / requests / boto3)→ 阻塞 event loop,其它请求全等。用 `run_in_executor` 或者换 async 库。
## 起因 `man tar` 输出 300 行,我只想知道"怎么解压一个 .tar.gz"。 man 是参考手册,新人 / 偶尔用户需要的是"3 个常用例子"。 `tldr` 项目("Too Long; Didn't Read")是社区维护的 CLI 命令速查, 每个命令给 5-10 个常见用法。 ## tldr ```bash # 装 brew install tldr # macOS sudo apt install tldr # Debian/Ubuntu npm i -g tldr # 跨平台 # 或者 Rust 实现(快得多) cargo install tealdeur alias tldr=tldr # rust 二进制叫 tldr ``` ```bash tldr tar ``` 输出: ``` tar Archiving utility. - Create an archive from files: tar cf {{target.tar}} {{file1 file2 file3}} - Create a gzipped archive: tar czf {{target.tar.gz}} {{file1 file2 file3}} - Extract a gzipped archive in the current directory: tar xzf {{source.tar.gz}} - Extract an archive into a target directory: tar xf {{source.tar}} -C {{directory}} - List the contents of an archive: tar tvf {{source.tar}} ``` 5 个例子覆盖 90% 用法。`man tar` 几百行的实际有用部分浓缩成 30 秒 能读完。 ### 更新 cache ```bash tldr --update ``` 定期跑更新到最新版社区贡献。 ### 离线工作 第一次跑 `tldr --update` 后所有 cheat sheet 缓存到本地(~30 MB), 之后无网络也能查。 ### 不同 OS 的命令变体 tldr 自动检测你的平台显示对应版本: ```bash tldr -p linux ls tldr -p osx ls tldr -p windows dir ``` Linux 上 `find` vs macOS BSD 版 `find` 语法不同,tldr 给你正确的。 ## cheat 类似 tldr 但更"自己加"友好: ```bash # 装(Go 二进制) go install github.com/cheat/cheat/cmd/cheat@latest cheat tar # 显示 tar 的 cheatsheet cheat -e tar # 用编辑器修改本地 tar cheatsheet(写自己常用的) cheat -l # 列所有 cheatsheets ``` `~/.config/cheat/cheatsheets/community/` 是社区版本; `~/.config/cheat/cheatsheets/personal/` 是你自己加的(优先显示)。 适合"团队内部的命令速查"——给项目特定的命令写 cheat。 ## navi:interactive cheatsheet + 直接执行 ```bash brew install navi cargo install navi ``` ```bash navi # TUI 弹出,浏览所有 cheats,选中后回车直接执行 ``` cheat 文件格式: ``` % docker # stop all containers docker stop $(docker ps -aq) # remove all stopped containers docker rm $(docker ps -aq) # list images sorted by size docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | sort -k 2 -h ``` navi 会让你 fuzzy 搜("docker stop all"),找到后直接执行。 对"我知道有这条命令但记不清完整"特别适合。 参数化命令: ``` % find # find files by name (case insensitive) find <dir> -iname "*<pattern>*" ``` navi 选中后弹小框让你填 `<dir>` 和 `<pattern>`,然后执行。 ### 同步团队 cheat ```bash navi --finder fzf --query 'docker' navi --tldr 'tar' # 直接调用 tldr navi repo browse # 浏览社区 cheat 仓库 navi repo add <git-url> # 加自定义 repo ``` 公司内部 cheat 仓库 + `navi repo add [email protected]/cheats.git` → 全员共享团队最佳实践。 ## bro pages(命令请求帮助 + 评分) ```bash # 装(不再积极维护,但仍可用) gem install bropages bro tar # 显示社区贡献的 examples,按点赞排序 bro thanks # 给当前显示的 example 点赞 bro add tar # 自己贡献一个 example ``` 社区驱动 vs tldr 的"官方维护"差异: bro 更草根,tldr 更整洁。 ## 自己装"代码 snippet" 我个人的 `~/notes/cli.md`: ```markdown ## ssh 隧道 # Local port -> remote service through bastion ssh -L 8080:internal-host:80 -N bastion # Reverse tunnel: expose local port to remote ssh -R 9000:localhost:9000 -N remote ## ffmpeg # extract audio ffmpeg -i in.mp4 -vn -acodec copy out.aac # convert to mp3 ffmpeg -i in.wav -codec:a libmp3lame -b:a 192k out.mp3 # trim by time ffmpeg -i in.mp4 -ss 00:01:30 -t 00:00:30 -c copy out.mp4 # add subtitle ffmpeg -i in.mp4 -vf subtitles=in.srt out.mp4 ``` 配合 fzf 让自己的笔记可搜: ```bash mynote() { grep -E '^#' ~/notes/cli.md | fzf | xargs -I {} grep -A 5 '^{}' ~/notes/cli.md } ``` 打 `mynote` → fzf 出来所有标题 → 选一个看内容。 ## 与 ChatGPT / Claude 的关系 LLM 也能"我想做 X 命令怎么写"。优劣: - **LLM**:自由表达、不限范围;可能编造(小心 hallucination) - **tldr / cheat**:人工验证、可靠;范围限于已收录 我现在混用: - 先 `tldr cmd` → 如果想要的功能在 5 个例子里直接用 - 否则问 LLM → 验证后加进自己的 `~/notes/cli.md` ## 效果 - 不再 `man tar` / `man find` 滚屏几分钟找 example - 团队"上次那个 ffmpeg 命令是啥来着" 类问题 → 自己 cheat 速查 - 新人 onboarding 速查内部命令大幅简化(navi + 团队 cheat repo) ## 踩过的坑 1. **tldr 例子里 `<placeholder>` 别照抄**:`{{file}}` 是占位符, 替换成真值再跑。 2. **cheat 跨用户同步**:`~/.config/cheat/cheatsheets/personal/` 进自己 dotfiles repo + symlink,多机器同步。 3. **navi 选 cheat 后误执行**:危险命令(rm -rf)navi 默认会先填参数 但仍会执行。一行 cheat 写法注意防滑指。 4. **bropages 维护停了**:不推荐新装,迁 tldr / navi。 5. **`tldr` 命令冲突**:有 Node / Python / Rust / shell 多个实现。 PATH 里只留一个,按需 alias。
## 起因 新做的 React SPA + Django 后端,cookie 鉴权(HttpOnly session cookie)。 QA 测试时报:用户在浏览器 A 登录后,访问恶意网站 evil.com,evil 里 有个表单: ```html <form action="https://myapp.com/api/transfer" method="POST"> <input name="to" value="attacker"> <input name="amount" value="1000000"> </form> <script>document.forms[0].submit()</script> ``` 用户访问 evil → 表单自动提交 → 浏览器把 myapp.com 的 cookie 带上 → 后端以为是合法用户操作 → 转走 100 万。 这就是 CSRF (Cross-Site Request Forgery)。任何 cookie-based auth 的 系统都要防。 ## 解决方案 ### 第一道:SameSite cookie 现代浏览器 cookie 默认 `SameSite=Lax`(Chrome 80+),意思是: 跨站请求自动**不发** cookie(除了 top-level GET navigation)。 确保你的 session cookie 设置: ```python # Django settings SESSION_COOKIE_SAMESITE = 'Lax' # 或 'Strict' SESSION_COOKIE_SECURE = True # 仅 HTTPS SESSION_COOKIE_HTTPONLY = True # JS 不能读 CSRF_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_SECURE = True ``` ```javascript // FastAPI / Starlette response.set_cookie( 'session', value=token, secure=True, httponly=True, samesite='lax' ) ``` `SameSite=Lax` 防护: - ✅ form POST / XHR / fetch 跨站不带 cookie - ✅ 主要 CSRF 路径被堵 - ⚠️ 老浏览器(IE / Safari < 13)不支持 `SameSite=Strict` 更严:连同站 navigation 都不带 cookie。会影响"从外部 链接打开本站还要重新登录"的体验。 ### 第二道:CSRF token(仍推荐配合 SameSite) 即使 SameSite 防住 99%,建议加 token 双保险: 1. 服务端 set cookie `csrftoken=abc...`(非 HttpOnly,让 JS 能读) 2. JS 每个 POST/PUT/DELETE 请求 header 加 `X-CSRFToken: <token>` 3. 服务端校验 header token vs cookie token 一致 Django 自带: ```python # settings.py CSRF_COOKIE_NAME = 'csrftoken' # middleware (默认已有) MIDDLEWARE = [ ... 'django.middleware.csrf.CsrfViewMiddleware', ] ``` 前端(fetch / axios): ```js function getCookie(name) { return document.cookie .split('; ').find(c => c.startsWith(name + '=')) ?.split('=')[1] } const csrfToken = getCookie('csrftoken') await fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, credentials: 'include', // 带 cookie body: JSON.stringify({ to, amount }), }) ``` 恶意 evil.com 的 JS **不能读 myapp.com 的 cookie**(跨域), 所以拿不到 csrftoken → 攻击失败。 axios 全局配: ```js axios.defaults.xsrfCookieName = 'csrftoken' axios.defaults.xsrfHeaderName = 'X-CSRFToken' axios.defaults.withCredentials = true ``` ### Double Submit Cookie 模式(无需 server-side state) 如果你想避免在服务端存 token: ``` 1. 用户登录时 server set cookie csrf=<random> 2. JS 读 cookie 把同值放进 header X-CSRF-Token 3. server 校验 cookie 和 header 一致 ``` 不需要 server 记住 token(只验两边相等)。简单 + 无状态。 Django 默认就是这种模式。 ### Stateless API + JWT 用户 JWT 放 cookie:同 cookie session,按上面处理。 JWT 放 localStorage: - 没 CSRF(请求带 cookie 才有 CSRF;JWT 走 Authorization header 不自动带) - 但容易 XSS(任何注入的 JS 都能读 localStorage) **OWASP 推荐 JWT 走 HttpOnly cookie + CSRF token**: - HttpOnly 防 XSS 偷 token - CSRF token 防跨站请求 ### 后端校验 ```python # Django CsrfViewMiddleware 自动校验 # 不需要 CSRF 的 endpoint 加装饰器 from django.views.decorators.csrf import csrf_exempt @csrf_exempt def public_webhook(request): # 不需要 CSRF 校验的 endpoint(如外部 webhook) ... ``` ```python # FastAPI 没有内置 CSRF,第三方:starlette-csrf from starlette_csrf import CSRFMiddleware app.add_middleware( CSRFMiddleware, secret='your-secret', cookie_name='csrftoken', header_name='X-CSRFToken', safe_methods={'GET', 'HEAD', 'OPTIONS', 'TRACE'}, ) ``` ### Origin / Referer 校验(额外加固) ```python def check_origin(request): origin = request.headers.get('Origin') or request.headers.get('Referer') if not origin: return False parsed = urlparse(origin) return parsed.netloc in ALLOWED_HOSTS ``` CSRF 攻击的请求 Origin / Referer 来自 evil.com(浏览器自动加,无法 伪造)。检查就能挡。 注意: - 直接打开链接 (top-level navigation GET) 可能没 Referer - 一些隐私插件去掉 Referer - 只用 Origin/Referer 不够(建议组合 token) ### 实战:完整流程 ``` 登录: 1. POST /api/login (email + password) 2. server 验证 + set HttpOnly cookie 'session' + non-HttpOnly cookie 'csrftoken' 3. JS 拿到 csrftoken(读 cookie) 后续 API 调用: 4. fetch('/api/x', { headers: {'X-CSRFToken': csrftoken}, credentials: 'include' }) 5. server 校验: - cookie session 合法 - header X-CSRFToken == cookie csrftoken - Origin 在白名单 6. 通过 → 处理业务 恶意网站攻击: 7. evil.com 提交表单到 myapp.com/api/x 8. 浏览器带上 session cookie (SameSite=Lax 已经会过滤大部分) 9. evil 不知道 csrftoken(跨域读不到)→ 请求失败 ``` ### CSRF 不防什么 - **XSS(脚本注入)**:CSRF 是"跨站请求";XSS 是"在你站里执行代码", 完全不同的攻击面。CSP / 输入校验防 XSS。 - **网络中间人**:HTTPS 防的。 - **服务端逻辑漏洞**:业务校验是另一层(如 transfer 应该让用户确认 密码 / 2FA)。 ### 双重提交的危险变种 ``` 1. attacker 在 evil.com 上设 cookie .yourapp.com csrftoken=evil_value (subdomain takeover 或 set-cookie via XSS on other subdomain) 2. attacker JS fetch myapp.com 时浏览器带这个 cookie 3. attacker 用同 evil_value 当 header X-CSRFToken 4. server 校验 cookie == header → pass ``` 防: - 用 SameSite cookie(attacker JS 跨域也不能 set 受害者域的 cookie) - 不要用 `*.yourapp.com` 通配域 cookie - 子域共享时 csrf token 加 hmac 包 user id 防 forge ## 效果 - pen test 报告 CSRF 漏洞清零 - 用户无感(自动带 token) - 攻击者从其它站 POST 直接失败 - 老浏览器(占比 < 1%)能 fallback 到 CSRF token-only ## 何时不需要 CSRF 防护 - 纯 token-bearer API(Authorization header)+ token 不存 cookie - 公开 endpoint(不修改状态 / 不查私有数据) - 同域单页全 GET / 状态在 URL 里 但**只要用 cookie 鉴权 + 有改状态 API,必须 CSRF 防护**。 ## 踩过的坑 1. **`fetch` 默认不带 cookie**:要 `credentials: 'include'` (同源时 `'same-origin'`)。漏写 cookie 不发,登录态丢, 误以为是"鉴权坏了"。 2. **CSRF 中间件 + 微服务架构**:A 服务 set cookie,B 服务校验, B 不知道 secret → fail。共享 SECRET_KEY 或 token-issuing 服务集中。 3. **SameSite=Lax 不防 GET CSRF**:GET 在 Lax 下会带 cookie。 永远不要让 GET 改状态(如 `/api/delete?id=1` → 邮件里图片 src 触发执行)。GET 必须 idempotent。 4. **WebSocket / SSE 不被 SameSite 保护**:跨站打开 WebSocket 仍带 cookie。WebSocket 鉴权要在握手时校验 Origin。 5. **CDN / proxy 把 cookie 丢了**:proxy 不带 cookie 头 → 后端看不到 session。检查 `proxy_set_header Cookie $http_cookie`。
## 起因 应用要异步处理 / 解耦 / 削峰,需要消息中间件。 候选: - **Kafka**:日志型,高吞吐,持久化 - **RabbitMQ**:经典 broker,AMQP,灵活路由 - **NATS / NATS JetStream**:现代轻量,Go 写 - **SQS / Pub/Sub**:云托管 下面对比关键差异 + 各自适合的场景。 ## Kafka ``` [Producer] → topic (partition × N) → [Consumer Group] ↓ 持久化到 disk (broker × N,replicated) ``` - 消息是 append-only log,consumer 维护 offset - partition 是并行度(一个 partition 同时一个 consumer instance) - replication 跨 broker → HA - 默认保留消息几天到几周(retention 配置) ### 用法 ```python # Producer (confluent-kafka) from confluent_kafka import Producer p = Producer({'bootstrap.servers': 'kafka:9092'}) p.produce('orders', key='user-1', value='{"action": "buy"}') p.flush() # Consumer from confluent_kafka import Consumer c = Consumer({ 'bootstrap.servers': 'kafka:9092', 'group.id': 'order-processors', 'auto.offset.reset': 'earliest', }) c.subscribe(['orders']) while True: msg = c.poll(1.0) if msg: print(msg.value()) c.commit(msg) ``` ### 优势 - 极高吞吐(百万 msg/s/broker) - 重放历史(offset 任意 reset) - 分布式天生 HA - 流处理生态强(Kafka Streams / Flink / ksqlDB) ### 劣势 - 操作复杂(broker + zookeeper / KRaft + partition 设计) - 资源消耗大(broker JVM 4 GB+) - 不擅长 priority / per-message TTL / 复杂路由 - 消费速度受 partition 数限制 ## RabbitMQ ``` [Producer] → [Exchange] → routing → [Queue × N] → [Consumer] ``` - 经典 broker 模型(push-based) - AMQP 协议,丰富 routing(direct / fanout / topic / headers) - 消息消费后默认删除(除非 stream queue) - 单 broker 几万 msg/s ### 用法 ```python import pika connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq')) channel = connection.channel() channel.queue_declare(queue='orders', durable=True) # Producer channel.basic_publish(exchange='', routing_key='orders', body='hello', properties=pika.BasicProperties(delivery_mode=2)) # 持久化 # Consumer def callback(ch, method, properties, body): print(body) ch.basic_ack(delivery_tag=method.delivery_tag) channel.basic_consume(queue='orders', on_message_callback=callback) channel.start_consuming() ``` ### 优势 - 灵活路由(topic exchange `order.*.created`) - per-message TTL / priority - 死信队列原生 - 部署简单(单 Erlang 进程) - 消费 ack/nack / retry 模型完善 ### 劣势 - 吞吐 < Kafka - 不擅长 replay(消费后消息没了) - HA cluster 配置棘手(mirror / quorum queue) - 老版本运维痛点多 ## NATS / JetStream ``` [Publisher] → subject → [Subscriber] (JetStream 模式可持久化) ``` - Go 写,极轻量(单 binary 30 MB) - subject-based(类似 Kafka topic 但更灵活) - core NATS:fire-and-forget(极快) - JetStream:持久化 + replication(Kafka 类似 semantics) ### 用法 ```python import asyncio import nats async def main(): nc = await nats.connect('nats://localhost:4222') js = nc.jetstream() await js.add_stream(name='ORDERS', subjects=['orders.>']) # publish await js.publish('orders.created', b'hello') # consume sub = await js.subscribe('orders.>', durable='processor') async for msg in sub.messages: print(msg.data) await msg.ack() asyncio.run(main()) ``` ### 优势 - 极轻量 + 易部署(单 binary,跨平台) - 性能好(百万 msg/s core,几十万 jetstream) - subject wildcard 路由 - 现代设计(gRPC 风格) ### 劣势 - 生态比 Kafka / RMQ 小 - 工具链 / 监控比较新 - 跨 cluster 复制弱(不像 Kafka MirrorMaker 成熟) ## 横向对比 | | Kafka | RabbitMQ | NATS JetStream | SQS | |---|---|---|---|---| | 吞吐 | 极高 | 中 | 高 | 中 | | 持久化 | 默认 | 可选 | 可选 | 默认 | | 路由 | partition+key | 丰富 (AMQP) | subject wildcard | queue | | 顺序 | partition 内 | queue 内 | stream 内 | FIFO queue 内 | | Replay | ✅ | 弱 | ✅ | ❌ | | HA | 强(replication) | 中(cluster) | 强 | 托管 | | 部署 | 复杂 | 简单 | 极简 | 0 | | 资源 | 大 | 中 | 小 | 0 | | 适合 | 日志 / 流 / 大规模 | 任务队列 / RPC | 现代云 / 服务网格 | 简单异步 | ## 推荐 - **任务队列(celery 类)** → RabbitMQ 或 Redis Streams - **日志聚合 / event sourcing** → Kafka - **微服务消息总线 + 现代云原生** → NATS - **简单异步 + 不想运维** → SQS / Pub/Sub - **数据 pipeline ETL** → Kafka - **极致吞吐(百万 QPS)** → Kafka ## 我的实际选择 中小项目(< 100k msg/s): - **Celery + Redis**:Django 任务队列首选,0 引入 - **NATS JetStream**:微服务通信,轻量 大项目 / 数据 pipeline: - **Kafka**:必备 ## 真实 case 1:Celery → Kafka 迁移 老 Celery + Redis 跑 task queue。规模大了: - 任务 50k/s 起 Redis 撑不住 - 失败任务重放难 - 监控弱 改 Kafka: - 任务塞 topic - consumer group 处理 - 失败 → 进 retry topic(带 delay)→ DLQ - 重放 / 重处理一行命令 挑战: - partition 设计(保证某 user 的 task 顺序) - consumer rebalance 期间 short downtime 但 throughput + observability 大改善。 ## 真实 case 2:RabbitMQ 路由 某客户系统:"订单创建" event 要通知: - 库存(扣库存) - 财务(开发票) - 物流(准备发货) - analytics(统计) RabbitMQ topic exchange: ```python # producer channel.basic_publish(exchange='order_events', routing_key='order.created.us', body=...) # 4 consumer 各自 bind channel.queue_bind('inventory_queue', 'order_events', 'order.created.*') channel.queue_bind('finance_queue', 'order_events', 'order.created.*') channel.queue_bind('logistics_queue', 'order_events', 'order.created.us') channel.queue_bind('analytics_queue', 'order_events', 'order.*') ``` 灵活订阅,新 consumer 加入无需 producer 改动。 Kafka 也能做但要 topic 设计多副本 + 自管 routing。 ## 与 outbox pattern 配 应用先写 DB + outbox 表 → background job 读 outbox → 发到 MQ。 保证 DB 写成功 + MQ 发出一致(事务安全)。 debezium 等 CDC 工具自动监听 PG WAL → Kafka,省手写。 ## 不要为了用而用 很多场景: - 单 DB + 简单异步:cron job + DB queue 表 - 微服务通信:HTTP / gRPC(不必每次 async) - 通知:直接 webhook MQ 增加运维 + 复杂度。确认真的需要 async / decouple / 高吞吐 / replay 再上。 ## 踩过的坑 1. **Kafka partition 数选错**:partition 是并行度上限。设 4,consumer 增到 8 后 4 个 idle。partition 加容易减难,初始就设大些(如 32+)。 2. **RabbitMQ 队列 unbounded**:consumer 慢 + producer 快 → 队列长几 亿条 → broker OOM。设 `x-max-length` 或 `x-message-ttl` 限。 3. **NATS JetStream stream 配错**:retention 设短 / max bytes 小 → 消息被驱逐。production 前 stress test。 4. **at-least-once 重复处理**:所有 MQ 都是 at-least-once。consumer 必须幂等(用 message id 去重)。 5. **monitoring 缺**:MQ 死了应用一段时间没察觉。最少监控 lag / queue size / consumer alive。
时间不准的服务器会引发各种诡异 bug: - TLS 证书"还没生效"或"已过期" - Kerberos 认证失败 - 分布式日志时间错乱,没法 trace - 数据库主从复制时间戳混乱 `systemd-timesyncd` 是 Ubuntu 默认的 SNTP 客户端,能用但功能很基础 (只读 NTP,不会被别人查询,调整精度一般)。生产用 chrony, 小内存占用 + 快收敛 + 网络中断后的快速恢复。 ## 安装 + 切换 ```bash sudo systemctl disable --now systemd-timesyncd sudo apt install -y chrony sudo systemctl enable --now chrony # 校验当前用的是哪个 timedatectl # System clock synchronized: yes # NTP service: active ``` ## 配置 `/etc/chrony/chrony.conf` 主流 distro 自带的默认就能跑。 生产建议改: ```conf # 优先用 pool 而不是 server —— 自动负载均衡到多台 pool 2.debian.pool.ntp.org iburst maxsources 4 # 国内服务器换国内源 # pool cn.pool.ntp.org iburst maxsources 4 # server ntp.aliyun.com iburst # server ntp.tencent.com iburst # 启动后的前 10 个样本只要测量到偏差就立即跳调 makestep 1.0 10 # 系统时钟同步到 RTC 硬件时钟(关机后保留) rtcsync # 允许哪些客户端查询(如果本机也是 NTP server) # allow 192.168.0.0/16 # 默认拒绝,注释掉它就 client-only # 数据存储 driftfile /var/lib/chrony/chrony.drift makestep 1 3 keyfile /etc/chrony/chrony.keys ntsdumpdir /var/lib/chrony # 日志 logdir /var/log/chrony log measurements statistics tracking ``` `iburst` 是关键:启动时连发 8 个查询,秒级完成初始同步(默认每 64 秒一次 会慢得让人无奈)。 ## 校验 ```bash # 当前选用的服务器 + 偏差 chronyc tracking # Reference ID : C0A87B0F (ntp1.aliyun.com) # Stratum : 3 # Ref time (UTC) : Sat May 23 09:00:12 2026 # System time : 0.000001234 seconds slow of NTP time # Last offset : +0.000045678 seconds # RMS offset : 0.000123456 seconds # Frequency : 12.345 ppm slow # ... # 看所有 source 状态 chronyc sources # MS Name/IP address Stratum Poll Reach LastRx Last sample # =============================================================== # ^* 100.100.5.1 2 7 377 45 +12us[+15us] +/- 1567us # ^* 是当前在用;^+ 是候选;^? 是不可达;^x 不一致;^- 被算法排除 # 各 source 的详细测量 chronyc sourcestats ``` `System time` 在毫秒级即合格;几十微秒到几百微秒是 Internet NTP 的正常范围。 ## NTS(NTP over TLS) 新硬件 / 新版 chrony 支持 NTS,给 NTP 流量加密 + 鉴权(防中间人篡改时间): ```conf server time.cloudflare.com iburst nts server nts.netnod.se iburst nts ``` 需要 `nts` 关键字 + chrony >= 4.0 + 客户端能解析 nts 服务器的证书链。 ## 给本机当 NTP server ```conf # /etc/chrony/chrony.conf allow 192.168.0.0/16 # 内网客户端 allow 10.0.0.0/8 # 监听 IPv4 / IPv6(默认开) # bindaddress 0.0.0.0 ``` ```bash sudo systemctl restart chrony sudo ufw allow 123/udp comment 'NTP' ``` 客户端 chrony.conf 写 `server <你这台>.example.com iburst`。 ## 强制立即同步 ```bash sudo chronyc -a 'burst 4/4' sudo chronyc -a makestep # 之前都不行的话直接: sudo chronyc -a 'manual on' && sudo chronyc settime ... ``` ## 监控 ```bash # Prometheus node_exporter 自带 chrony collector # node_chrony_system_time_offset_seconds 是要报警的指标 # 阈值 > 0.01 秒就告警 # 或直接定时脚本检查偏移 chronyc tracking | awk '/System time/ {print $4}' | xargs -I {} \ python3 -c "import sys; v=abs(float('{}')); sys.exit(0 if v<0.01 else 1)" ``` ## 踩过的坑 - 在云上(AWS / GCP / Azure)建议用 **云供应商提供的内网 NTP** 而不是公网 pool:低延迟 + 不出 VPC + 通常更稳定(AWS: `169.254.169.123`)。 - 容器里运行的程序看到的时间是宿主的;容器里跑 chrony 多此一举,反而可能 和宿主冲突。 - 虚拟机长时间挂起后 wakeup,时间漂移可能很大,触发 `makestep` 跳变。 如果业务对时间不能跳(金融 / log 单调),考虑把 makestep 关掉接受 slewing(缓慢调整)。 - 同步上游用 IP 而不用域名时,AWS / 云上 169.254.169.123 这类 link-local 地址不要经过 DNS(永远查不到)。
ZFS 的两个杀手特性: 1. **快照即时 + 几乎零成本**:copy-on-write,秒级、不占额外空间 2. **zfs send / receive**:增量传输到另一台机器,做异地备份 下面在 Ubuntu 上装 ZFS、建池、配快照轮转、做远程增量备份。 ## 1. 装 ZFS(Ubuntu 已带) ```bash sudo apt install -y zfsutils-linux zfs version ``` CentOS / RHEL:用 `zfs-fuse` 不行,要装 native(zfsonlinux 仓库)。 ## 2. 建池 假设有两块裸盘 `/dev/sdb` `/dev/sdc`: ```bash # 镜像(mirror = RAID1) sudo zpool create tank mirror /dev/sdb /dev/sdc # 单盘 # sudo zpool create tank /dev/sdb # RAIDZ1 (RAID5 类似,需 ≥ 3 盘) # sudo zpool create tank raidz /dev/sdb /dev/sdc /dev/sdd sudo zpool status tank sudo zpool list sudo zfs list ``` `tank` 是池名。建议用块设备的 `/dev/disk/by-id/...` 而不是 `/dev/sdb`(重启后顺序变)。 ## 3. 创建文件系统(dataset) ```bash sudo zfs create tank/data sudo zfs create tank/data/users sudo zfs create tank/data/projects sudo zfs list # tank/data 96K 3.5T ... # tank/data/users 96K 3.5T ... # tank/data/projects 96K 3.5T ... ``` dataset 像目录但每个都是独立挂载 + 独立 properties。 ## 4. 重要 properties ```bash sudo zfs set compression=lz4 tank # 透明压缩,几乎免费 sudo zfs set atime=off tank # 关 atime,性能 + sudo zfs set xattr=sa tank # 高效 xattr 存储 sudo zfs set recordsize=1M tank/data # 大文件场景调大 sudo zfs set quota=500G tank/data/users # 限制使用空间 sudo zfs set reservation=100G tank/data/projects # 保留 100G ``` `compression=lz4` 是 ZFS 最值得开的:CPU 开销几乎为零,能压缩出 30-50% 的额外空间。 ## 5. 快照 ```bash sudo zfs snapshot tank/data@$(date +%F-%H%M%S) sudo zfs list -t snapshot # tank/data@2026-05-23-090000 0B ... ``` `@` 后面是快照名。 恢复某个文件: ```bash ls /tank/data/.zfs/snapshot/2026-05-23-090000/ # 像普通目录,cp 出来即可 ``` 整个 dataset 回滚到某快照: ```bash sudo zfs rollback tank/data@2026-05-23-090000 # 注意:会丢掉快照之后的所有改动! ``` 删快照: ```bash sudo zfs destroy tank/data@2026-05-23-090000 ``` ## 6. 自动快照轮转:zfs-auto-snapshot ```bash sudo apt install -y zfs-auto-snapshot ``` 自动每 15 分钟 / 小时 / 天 / 周 / 月做快照并轮转: ```bash ls /etc/cron.*/zfs-auto-snapshot # /etc/cron.d/zfs-auto-snapshot (15min) # /etc/cron.hourly/zfs-auto-snapshot # /etc/cron.daily/... # ... # 给特定 dataset 关掉某频率 sudo zfs set com.sun:auto-snapshot:frequent=false tank/data/projects sudo zfs set com.sun:auto-snapshot:hourly=true tank/data/projects ``` 默认保留:96 frequent / 24 hourly / 31 daily / 8 weekly / 12 monthly。 ## 7. zfs send:远程增量备份 第一次全量: ```bash # 源机 sudo zfs snapshot tank/data@base sudo zfs send tank/data@base | ssh backup@remote 'zfs receive -F tank-backup/data' # 远端 zfs list # tank-backup/data ``` 之后增量: ```bash # 源机:在前一个快照基础上做新快照 sudo zfs snapshot tank/data@2026-05-23 # 增量 send:只传 base → 2026-05-23 的差异 sudo zfs send -i tank/data@base tank/data@2026-05-23 \ | ssh backup@remote 'zfs receive tank-backup/data' # 完成后 base 可以删掉(远端也跟着删),用新快照做下一次的 base ``` 实际生产用 [syncoid](https://github.com/jimsalterjrs/sanoid) 或 [zrepl](https://zrepl.github.io/) 包装,自动管 base 和 retention: ```bash sudo apt install -y sanoid # /etc/sanoid/sanoid.conf 配置 dataset + retention # /etc/sanoid/syncoid.conf 配置 replication syncoid tank/data backup@remote:tank-backup/data ``` ## 8. 加密 ```bash # 建 dataset 时启用加密 sudo zfs create -o encryption=on -o keyformat=passphrase \ tank/data/sensitive # 之后挂载要解锁 sudo zfs load-key tank/data/sensitive # 输入密码 sudo zfs mount tank/data/sensitive ``` zfs send / receive 加 `-w` 直接发加密 stream(远端不需要密码, 压缩 + dedup 在加密状态下做)。 ## 9. 校验 + scrub ```bash sudo zpool scrub tank sudo zpool status tank # scan: scrub in progress since ... # 100M scanned out of 50G at 200M/s ``` scrub 读全部数据 + 校验 checksum,发现损坏自动从镜像 / parity 修复。 推荐每月跑一次: ```cron 0 3 1 * * /sbin/zpool scrub tank ``` ## 10. 容量监控 ```bash sudo zpool list -v tank # NAME SIZE ALLOC FREE ... # tank 4T 1.2T 2.8T ... # mirror 4T 1.2T 2.8T # sdb - - - # sdc - - - sudo zfs list -o name,used,avail,refer,mountpoint ``` ZFS 超过 80% 使用率性能急剧下降;保持在 80% 以下。 ## 踩过的坑 - 用 `/dev/sdb` 直接建池,机器重启 sdb 变成 sdc → 池找不到。永远用 `/dev/disk/by-id/` 路径。 - ZFS 内存吃得多:1GB / 1TB 数据是经验值。8GB 机器跑 32TB 池可能要 调 `zfs_arc_max` 限制 ARC 缓存。 - `zfs destroy` 没确认就执行,整个 dataset + 所有快照消失。养成 `-n` 模拟先看的习惯。 - snapshot 是 read-only,但占空间 = 这个快照后所有被修改的数据。 长期保留快照 + 高频改动 = 池快速胀满。retention 不要太长。
## 起因 要把一个英文站点国际化到中文 + 日文。 "naive"做法:`if (lang === 'zh') return '你有 ' + n + ' 条消息'`。 但碰到: - 复数:英文 "1 message" vs "5 messages";俄文 4 种复数形式 - 日期:1/2/2024 vs 2024-01-02 vs 2024年1月2日 - 货币:$1,234.56 vs ¥12,345.6 - 嵌入组件:`'<b>张三</b> 发了 <a href=...>3 条评论</a>'` 怎么拆? ICU MessageFormat 是 Unicode 联盟标准,处理这些场景。`react-intl` (FormatJS)是 React 的标准 ICU 实现。 ## 解决方案 ### 装 ```bash npm i react-intl ``` ### 顶层 Provider ```tsx import { IntlProvider } from 'react-intl' const messages = { en: { 'msg.greeting': 'Hello, {name}!' }, zh: { 'msg.greeting': '你好,{name}!' }, ja: { 'msg.greeting': '{name}さん、こんにちは!' }, } function App() { const [locale, setLocale] = useState('en') return ( <IntlProvider locale={locale} messages={messages[locale]}> <MyApp /> </IntlProvider> ) } ``` ### 用 `<FormattedMessage>` 或 `intl.formatMessage` ```tsx import { FormattedMessage, useIntl } from 'react-intl' function Greeting({ user }) { return ( <h1> <FormattedMessage id="msg.greeting" values={{ name: user.name }} /> </h1> ) } // 或 hook function Title({ name }) { const intl = useIntl() const text = intl.formatMessage({ id: 'msg.greeting' }, { name }) return <title>{text}</title> } ``` ### ICU MessageFormat 复数 ```ts const messages = { en: { 'comments.count': '{count, plural, =0 {No comments} one {1 comment} other {# comments}}' }, zh: { 'comments.count': '{count, plural, =0 {暂无评论} other {# 条评论}}' }, ru: { 'comments.count': '{count, plural, =0 {Нет комментариев} one {# комментарий} few {# комментария} many {# комментариев} other {# комментариев}}' }, } <FormattedMessage id="comments.count" values={{ count: 5 }} /> ``` `#` 自动替换成数字。 不同语言的复数规则: - 英文:one (1) / other (其它) - 中文:other - 俄文:one / few / many / other - 阿拉伯:zero / one / two / few / many / other 库里有 CLDR 数据自动应用。 ### Select(按值选不同文案) ```ts 'user.role': '{role, select, admin {管理员} editor {编辑} viewer {访客} other {未知}}' ``` ### 嵌入 React 组件 ```tsx const messages = { zh: { 'notif.commented': '<b>{user}</b> 评论了你的 <a>帖子</a>', }, } <FormattedMessage id="notif.commented" values={{ user: 'Alice', b: chunks => <strong>{chunks}</strong>, a: chunks => <a href={`/posts/${id}`}>{chunks}</a>, }} /> // 输出:<strong>Alice</strong> 评论了你的 <a href="/posts/1">帖子</a> ``` 翻译里的标签会调对应 function。完全保留 React 组件 + 事件 + 链接。 ### 日期 / 时间 / 货币 ```tsx import { FormattedDate, FormattedTime, FormattedNumber } from 'react-intl' <FormattedDate value={new Date()} year="numeric" month="long" day="numeric" /> // en: January 15, 2024 // zh: 2024年1月15日 // ja: 2024年1月15日 <FormattedTime value={new Date()} hour="numeric" minute="numeric" /> <FormattedNumber value={1234.5} style="currency" currency="USD" /> // $1,234.50 <FormattedNumber value={1234.5} style="currency" currency="CNY" /> // ¥1,234.50 <FormattedNumber value={0.85} style="percent" /> // 85% ``` 底层 `Intl.DateTimeFormat` / `Intl.NumberFormat`,浏览器原生。 ### 相对时间 ```tsx import { FormattedRelativeTime } from 'react-intl' <FormattedRelativeTime value={-2} unit="hour" /> // en: 2 hours ago // zh: 2 小时前 ``` ### 翻译文件管理 每个 locale 一个 JSON: ``` src/locales/ ├── en.json ├── zh.json └── ja.json ``` ```json // en.json { "msg.greeting": "Hello, {name}!", "comments.count": "{count, plural, =0 {No comments} one {1 comment} other {# comments}}" } ``` 按需加载: ```tsx async function loadMessages(locale: string) { return (await import(`./locales/${locale}.json`)).default } function App() { const [locale, setLocale] = useState('en') const [messages, setMessages] = useState({}) useEffect(() => { loadMessages(locale).then(setMessages) }, [locale]) return <IntlProvider locale={locale} messages={messages}>...</IntlProvider> } ``` 不同语言不打进 main bundle,按需 lazy load。 ### 提取翻译 key ```bash npx @formatjs/cli extract 'src/**/*.{ts,tsx}' --out-file lang/en.json --format simple ``` 扫描代码里所有 `FormattedMessage` / `formatMessage` 调用,提取 key + defaultMessage。翻译团队基于这个文件翻译。 ### 用 Crowdin / Lokalise / POEditor 把 en.json 上传 → 译员翻译 → 下载 zh.json / ja.json。 专业翻译工具有翻译记忆 / 协作 / 校对功能。 ### TypeScript 类型安全 ```bash npm i -D @formatjs/cli ``` ```ts // auto-generated.ts type Messages = | 'msg.greeting' | 'comments.count' // ... declare module 'react-intl' { interface FormattedMessageProps { id: Messages } } ``` 写错 id 编译报错。 ## 实战 tip ### 1. defaultMessage ```tsx <FormattedMessage id="msg.greeting" defaultMessage="Hello, {name}!" values={{ name }} /> ``` `defaultMessage` 在 dev 时显示(避免 key 缺失看到 `[msg.greeting]`), 在 extract 时被收进 source 文件。 ### 2. 不要 concat 字符串 ```tsx // ❌ 永远不要 <>{intl.formatMessage({ id: 'a' })} {intl.formatMessage({ id: 'b' })}</> // ✅ 一条 message 一句话 'msg.fullName': '{first} {last}' // 在 message 里组合 ``` 某些语言(日文)语序不一样,concat 永远不能正确翻译。 ### 3. 上下文 (context) 同样英文 "Open" 可能是按钮 / 状态 / 动词,翻译不同: ```json "button.open": "Open", "status.open": "Open", ``` 加注释让译员理解: ```tsx <FormattedMessage id="button.open" description="Button to open a file picker" defaultMessage="Open" /> ``` extract 出来 description 给译员看。 ## 效果 - 3 种语言(en/zh/ja)全覆盖 + 复数 + 日期 + 货币正确 - 翻译团队用 Crowdin 协作,开发不参与翻译细节 - bundle 按 locale 分割,每个 locale 增加 ~20KB - 加新语言只需添 JSON + locale 列表 + 设置 default 即可 ## 替代品 - **i18next**(react-i18next):更老牌、生态大、社区资源多 - **lingui**:Babel macro 转译,运行时几乎无开销 - **next-intl**:Next.js App Router 原生集成 i18next 与 react-intl 哪个好争论不断。我选 react-intl 因为 ICU 标准 + FormatJS 团队(Intl 提案推动者)维护。 ## 踩过的坑 1. **复数规则用错语法**:英文写 `{count} comments` 而不是 ICU `{count, plural, ...}`。后者才能让翻译灵活。 2. **直接 hard-code 日期格式**:`new Date().toLocaleDateString('en')` 只能英文。永远 `FormattedDate` 才跟随当前 locale。 3. **RTL 语言(阿拉伯 / 希伯来)布局**:仅翻字符串不够,CSS 也要 适配。`dir="rtl"` + `logical properties`(margin-inline-start 而非 margin-left)。 4. **lazy load 时第一次渲染缺译文**:fallback 显示 `[msg.greeting]` 是糟糕体验。加载完成前用 default locale 兜底,或显示骨架。 5. **翻译里有 HTML 注入风险**:译员误打了 `<script>` 进去会被解析。 ICU `<tag>` 必须在代码里映射;不在映射里的会被原样输出(HTML 实体 escape)。
## 起因 公网服务器 access log 每天几千条扫描尝试:SSH 暴力破解、WordPress 路径 扫描、各种 CVE 探测。一台 VPS 偶尔被低强度 SYN flood 打到延迟飙升。 fail2ban + ufw 能挡掉大部分,但更基础的内核层防护让攻击者连 TCP 握手 都建立不了,更省 CPU。 ## 解决方案:三道防线 ### 1. SYN cookies 启用(内核级 SYN flood 防护) ```bash # 临时 sudo sysctl -w net.ipv4.tcp_syncookies=1 sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192 sudo sysctl -w net.ipv4.tcp_synack_retries=2 # 持久 /etc/sysctl.d/99-syn-protect.conf cat <<EOF | sudo tee /etc/sysctl.d/99-syn-protect.conf net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 8192 net.ipv4.tcp_synack_retries = 2 net.ipv4.tcp_fin_timeout = 15 net.core.somaxconn = 4096 net.ipv4.tcp_tw_reuse = 1 EOF sudo sysctl --system ``` `tcp_syncookies=1` 让内核在 SYN backlog 满时改用 cookie 验证客户端, 不分配真实 socket,让 SYN flood 失效(攻击者要回 ACK 才占资源)。 ### 2. iptables/nftables 限速 SYN `iptables` 版(老但通用): ```bash # 每秒最多 25 个新连接,超过的丢弃 sudo iptables -N SYN_FLOOD sudo iptables -A INPUT -p tcp --syn -j SYN_FLOOD sudo iptables -A SYN_FLOOD -m limit --limit 25/s --limit-burst 50 -j RETURN sudo iptables -A SYN_FLOOD -j LOG --log-prefix "SYN_FLOOD: " sudo iptables -A SYN_FLOOD -j DROP # 持久化 sudo apt install iptables-persistent sudo netfilter-persistent save ``` `nftables` 版(推荐,新机器都用这个): ```nft table inet filter { # 限速:每秒 25 个新 SYN,突发 50 chain input { type filter hook input priority filter; policy drop; ct state established,related accept ct state invalid drop iif lo accept ip protocol icmp accept meta nfproto ipv6 icmpv6 accept # 新 SSH 连接限速 tcp dport 22 ct state new \ limit rate over 6/minute \ counter drop tcp dport 22 accept # HTTP/S 限速 SYN(防 layer-4 DDoS) tcp dport { 80, 443 } ct state new \ limit rate over 50/second burst 100 packets \ counter drop tcp dport { 80, 443 } accept # 默认 log + drop limit rate 5/minute log prefix "nft drop: " counter drop } } ``` ### 3. 用 ipset / nft set 屏蔽已知恶意 IP 段 ```bash sudo apt install ipset # 创建 set sudo ipset create blacklist hash:net # 加 IP 段(来自公开威胁情报源) curl -s https://lists.blocklist.de/lists/ssh.txt \ | xargs -I {} sudo ipset add blacklist {} 2>/dev/null # 接到 iptables sudo iptables -I INPUT -m set --match-set blacklist src -j DROP ``` 或 nftables 的 set(更优雅): ```nft table inet filter { set blacklist { type ipv4_addr flags interval elements = { 45.95.169.0/24, 193.32.162.0/24, # ... 几千条 } } chain input { ip saddr @blacklist drop # ... 其它规则 } } ``` 更新 set 用 `nft add element`,无需重启。 ### 4. conntrack 调优 ```bash # /etc/sysctl.d/99-conntrack.conf # 默认 65536 在高并发被吃满("nf_conntrack: table full" log) net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_tcp_timeout_established = 7200 net.netfilter.nf_conntrack_buckets = 262144 ``` 观察当前用量: ```bash sudo sysctl net.netfilter.nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max ``` 接近 max 时 conntrack 表满 → 新连接被 drop。 ### 5. 校验 / 监控 ```bash # 看 SYN 状态 ss -tan state syn-recv | wc -l ss -tan state established | wc -l # 看 conntrack 表 sudo conntrack -L | wc -l sudo conntrack -S # 统计 # iptables / nft 命中数 sudo nft list ruleset | grep counter ``` 实时看被 drop 的: ```bash sudo journalctl -k -f | grep -E 'nft drop|SYN_FLOOD' ``` ## 效果 - 之前一次 SYN flood 把 CPU 打到 60% → 现在内核层 cookie + 限速消化, CPU 几乎不动 - SSH brute force 日志从每天 5000+ 条 → < 50 条(被 nft limit 提前丢) - 已知恶意 IP 段(blocklist.de 等)直接 drop,访问日志几乎清净 - nginx 不再被无效请求打扰,正常用户体验改善 ## 性能成本 - iptables/nftables 处理一个包 nanosecond 级 - conntrack 每连接 ~300 bytes,1M 连接 ~300 MB(VPS 4GB+ 没压力) - 内核 SYN cookie 极少 CPU 公网服务器开这些防护是"几乎免费的保险"。 ## 与 Cloudflare / DDoS 服务对比 应用 / VPS 自己防只能挡到 L3-L4 中等流量(< 1 Gbps)。 大规模 DDoS(10 Gbps+)流量已经把上游带宽打满,再防火墙也无济于事。 正经防 DDoS 要前置 Cloudflare / AWS Shield 等大网络服务商, 利用它们的全球抗 D 能力。 但这些没必要 24x7 开(成本);做好本地防护 + 紧急时切到 Cloudflare "under attack" 模式是合理 trade-off。 ## 踩过的坑 1. **限速规则放错位置**:`limit rate` 必须在 `accept` 之前。我曾把 accept 写前面 → 所有连接都 accept,limit 形同虚设。 2. **`ct state new` 漏写**:限速规则没 `ct state new` 就把 established 连接的每个包都算 → 正常流量也被限速。 3. **blacklist 来源选择**:随便加几万条 IP 进 ipset 可能误封 CDN / 公有云 IP(共享网段)。用 abuseipdb / blocklist.de 这种带置信度 评分的源,confidence > 90 才加。 4. **iptables vs nftables 共存**:新 Debian/Ubuntu 用 `iptables-nft` 兼容层,老 iptables 命令实际写 nftables。混改 iptables 命令 + nft 命令导致规则错乱。统一用 nft。 5. **重启后规则丢**:iptables 改了忘 `netfilter-persistent save`; nftables 改了忘写 `/etc/nftables.conf`。养成"改完测试 →立刻 持久化"习惯。
媒体查询(@media)按视口尺寸响应式。但同一个组件可能放在不同宽度的 容器里(侧栏窄 / 主区宽 / 仪表盘卡片各种尺寸),媒体查询不知道 组件实际可用宽度。 Container Queries(容器查询)让组件按 **父容器** 尺寸响应式。 2023 后全 evergreen 浏览器支持,可以放心用。 ## 1. 启用容器 ```css .card-container { container-type: inline-size; /* 或者 size(同时观察宽 + 高) */ container-name: card; /* 可选,命名容器便于精确引用 */ } ``` `container-type: inline-size` 让浏览器观察这个元素的宽度变化, 开销小(不需要观察高度)。 ## 2. 容器查询 ```css @container card (min-width: 400px) { .card-title { font-size: 1.5rem; } .card-image { display: block; } } @container card (min-width: 600px) { .card { display: grid; grid-template-columns: 200px 1fr; } } ``` `@container card` 引用前面命名为 "card" 的容器。 不写名字也行:`@container (min-width: 400px)` 用最近的祖先容器。 ## 3. 完整例子:自适应卡片 ```html <div class="grid"> <div class="card-wrap"> <article class="card"> <img src="thumb.jpg" alt=""> <div> <h3>标题</h3> <p>描述...</p> </div> </article> </div> <div class="card-wrap"> <article class="card">...</article> </div> </div> ``` ```css .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; } .card-wrap { container-type: inline-size; } .card { display: flex; flex-direction: column; gap: 8px; } .card img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } /* 容器够宽时改横向布局 */ @container (min-width: 360px) { .card { flex-direction: row; } .card img { width: 120px; aspect-ratio: 1; flex-shrink: 0; } } ``` 视口窄时卡片竖排;视口宽时容器宽度 > 360px,自动横排。 **同一组件 + 同一 CSS 在不同上下文行为不同**。 ## 4. 容器查询单位 CSS 出了 `cqw` / `cqh` / `cqi` / `cqb` 等单位,相对于容器尺寸: ```css .card-title { font-size: clamp(1rem, 4cqi, 2rem); } ``` `4cqi` = 4% 容器 inline 尺寸。容器越宽字体越大,但限制在 1-2rem 之间。 ## 5. style queries(实验性) 按容器的某个 CSS 自定义属性查询: ```css .theme-dark { --mode: dark; container-type: normal; } @container style(--mode: dark) { .card { background: #1e1e1e; color: #fff; } } ``` 适合切主题不影响组件 HTML 结构。仍是实验阶段,部分浏览器支持。 ## 6. 与媒体查询配合 媒体查询管页面布局(侧栏开关、导航形态),容器查询管组件内部。 分工清晰: ```css /* 媒体查询:响应视口 */ @media (max-width: 800px) { .layout { grid-template-columns: 1fr; } .sidebar { display: none; } } /* 容器查询:响应组件可用空间 */ @container card (min-width: 400px) { .card { ... } } ``` ## 7. 命名 vs 匿名容器 匿名(不写 container-name)查询最近的祖先 container: ```css .parent { container-type: inline-size; } @container (min-width: 500px) { .child { ... } } ``` 命名让你跨层级精确引用: ```css .page { container: page / inline-size; } .card { container: card / inline-size; } @container page (min-width: 1000px) { /* 引用 page 容器 */ } @container card (min-width: 400px) { /* 引用 card 容器 */ } ``` ## 8. polyfill / 回退 老浏览器不支持时降级: ```css .card { /* 默认(窄屏 / 不支持时的样子) */ flex-direction: column; } @container (min-width: 360px) { .card { flex-direction: row; } } /* 或者 @supports 兜底 */ @supports not (container-type: inline-size) { /* 不支持容器查询的浏览器用媒体查询近似 */ @media (min-width: 600px) { .card { flex-direction: row; } } } ``` ## 9. 性能 `container-type: inline-size` 让浏览器为这个元素建立 containment context。 开销很小(不重排不重绘),但避免无脑给所有元素加。 通常每个独立组件根加一个就好。 `container-type: size`(同时观察宽 + 高)更贵些,因为元素的高度 通常由内容决定,会创建一个潜在的"无限循环"风险。 ## 10. 实际收益 之前没容器查询时,常见 hack: - 给容器加 class(`.card--wide` / `.card--narrow`)→ 业务代码要知道布局 - ResizeObserver + JS 控制 → 跨框架不一致 + 性能差 - 多套 CSS 类按 prop 切换 → 难维护 容器查询是这些痛点的官方解。组件 truly self-contained。 ## 11. 工具支持 Tailwind CSS v3.4+ 有 container queries plugin: ```html <div class="@container"> <div class="@md:flex @lg:grid">...</div> </div> ``` UI 库(shadcn / Mantine)渐渐采纳。 ## 踩过的坑 - 自己引用自己:`.card { container-type: inline-size; }` 然后 `@container (min-width: ...) .card { width: ... }` —— 改 width 会 触发容器尺寸变化 → 触发条件再判断 → 死循环。浏览器有保护但视觉上抖。 - 父子嵌套容器名相同:第二个 container-name 覆盖第一个,意外查询。 跨层 query 务必命名清楚。 - height container query 慎用:必须 `container-type: size` 且容器有 确定高度(不能完全由内容撑开)。 - 把 container-type 加到 body 上 → 全局影响,性能可能下降。粒度 控制在组件根元素。
## 起因 ssh 远程开发 + 本地多窗口工作流,需要: - 多终端面板(split / 切换) - detach 重连(断网不丢 session) - 持久化布局 `tmux` 是事实标准 20 年。`zellij`(Rust,2021+)是现代挑战者。 最近重度用 zellij 一个月。下面对比。 ## tmux 经典 + 普遍可用。 ```bash brew install tmux apt install tmux ``` `~/.tmux.conf` 配色 / 键位 / 插件。 ### 操作 ``` prefix = Ctrl-b(默认) prefix + c 新 window prefix + n 下个 window prefix + % 横分 pane prefix + " 竖分 pane prefix + d detach session prefix + [ 进 copy mode(scroll) ``` `tmux ls` 列 session,`tmux attach -t <name>` 接回。 ### 优势 - **最广泛可用**:所有 Linux distro / macOS / *BSD 包管理都有 - 极成熟(2007 至今) - 插件生态丰富(tpm + 几千插件) - 资源占用小(< 5 MB) - session 持久化 + tmux-resurrect 保存恢复 ### 劣势 - 默认配色 / 键位丑陋复古,要花时间配 - 学习曲线陡(前 1 小时挫败感强) - 配置文件语法奇特 - 无内置浮窗(3.2+ popup 比较基础) ## zellij ```bash brew install zellij cargo install zellij ``` 启动直接 `zellij`。 ### 操作 底部有状态栏显示当前 mode 和键位 → **不用记**! ``` Ctrl + p pane mode(split / resize) Ctrl + t tab mode(多 tab) Ctrl + s scroll mode Ctrl + o session mode(detach 等) Ctrl + n resize mode Ctrl + h move mode ``` 进 mode 后状态栏显示可用键,按数字 / 字母执行。 ### 优势 - **新手友好**:状态栏永远显示能做什么 - 默认配色现代 + 美观 - 内置 layout(KDL config) - 内置 floating pane / sticky pane - WASM 插件(Rust / Go 写插件) - 性能极好(Rust) ### 劣势 - 还年轻(兼容性 / 生态比 tmux 弱) - 老服务器没有(apt 包要等 24.04) - 远程 ssh 渲染偶有小问题 - 内存占用更大(30-80 MB vs tmux 5 MB) ## 配色 / layout 对比 **tmux 默认**: ``` [0] 0:bash- 1:vim* "myhost" 14:23 25-Apr-24 ``` 朴素绿色 bar,要配 powerline / status-utf8 才好看。 **zellij 默认**: ``` zellij_session TAB: 1 Bash 2 Vim ───────────────────────────────────── (panes) ───────────────────────────────────── Ctrl+p PANE Ctrl+t TAB Ctrl+s SCROLL Ctrl+o SESSION ``` 底部 hint bar 直接告诉你下一步。 ## 配置文件 **tmux** `.tmux.conf`: ```tmux set -g prefix C-a unbind C-b bind C-a send-prefix set -g mouse on set -g history-limit 50000 # split with intuitive keys bind | split-window -h bind - split-window -v # vim-style pane nav bind h select-pane -L bind j select-pane -D bind k select-pane -U bind l select-pane -R # plugin manager set -g @plugin 'tmux-plugins/tpm' set -g @plugin 'tmux-plugins/tmux-resurrect' run '~/.tmux/plugins/tpm/tpm' ``` **zellij** `~/.config/zellij/config.kdl`: ```kdl keybinds { normal { bind "Ctrl g" { SwitchToMode "Locked"; } } } theme "nord" simplified_ui false default_layout "compact" mouse_mode true ``` KDL 比 tmux 自定义语法人类友好得多。 ## layout file(强项) zellij: ```kdl // dev.kdl layout { tab name="editor" { pane command="nvim" } tab name="server" { pane split_direction="vertical" { pane command="pnpm" { args "dev"; } pane command="docker" { args "compose" "logs" "-f"; } } } tab name="test" { pane command="pnpm" { args "test" "--watch"; } } } ``` 启动: ```bash zellij --layout dev.kdl ``` 一键开 3 tab / 多 pane 各跑特定命令。 tmux 等价是 shell script + tmux send-keys → 又长又脆。 ## 远程 ssh 注意 tmux:远程跑 tmux + 本地 ssh 进去 attach。断网 → 进程不丢。 zellij:同样可以,但 zellij 用更多 ANSI escape → 慢网络上重绘 更明显。 我远程**仍用 tmux**。本地 zellij。 ## 我现在的工作流 - 远程服务器:tmux(兼容性 / 速度 + 几乎所有服务器都装好) - 本地 macOS:zellij(视觉好 + layout file 项目自动布局) - 跨 ssh 项目:zellij 本地嵌套(zellij outer + tmux inner on remote) ## 内存 / 启动 ```bash # tmux: ~3 MB resident # zellij: ~30 MB resident # zellij 启动 ~ 100ms vs tmux ~ 20ms ``` 500 倍 memory 差距听起来吓人,绝对数都很小。除非 1 GB 小机器 (树莓派 / 老 VPS)不是问题。 ## 共享 session(远程 pairing) tmux:`tmux a -t name`(多终端 attach 同 session,看到同样画面)。 zellij:`zellij attach <session>` 同样支持。 zellij 还有 `zellij --new-session-with-layout`,新人加入直接套布局。 ## 替代品快览 - **screen**:祖宗,纯 80 年代风格,今天没人新装 - **wezterm**:终端 emulator + 内置 multiplexer(可替代 zellij/tmux) - **kitty**:终端 emulator,有窗口管理但不是 multiplexer 如果你想"一个工具搞定终端 + multiplex" → wezterm。 传统派 → tmux/zellij 跟终端解耦。 ## 决策 - **新人 / 个人** → zellij(学习曲线低) - **远程 ssh / 老 server** → tmux(兼容) - **重 layout 配置** → zellij(KDL layout file) - **重 plugin** → tmux(生态成熟) - **极简资源** → tmux ## 踩过的坑 1. **zellij + 24-bit color 终端报错**:老终端不支持 truecolor。 `TERM=xterm-256color` 或者用 alacritty / kitty / wezterm。 2. **zellij 复制到系统剪贴板**:默认 OSC 52 escape。某些终端不支持 → 要配 `copy_command "pbcopy"` (mac) / `xclip`。 3. **tmux 2.x vs 3.x**:服务器装的 tmux 2.x 没 popup / 某些新 feature。 ssh 远程时小心。 4. **嵌套 multiplex 键冲突**:本地 zellij + 远程 tmux → prefix 键冲突。 把 inner 的 prefix 改成不同(`C-a` vs `C-b`)。 5. **`.tmux.conf` 编辑要 source**:改完不会自动重载。 `tmux source ~/.tmux.conf`,或者 `prefix + r` bind 一下。
## 起因 老板让"预测下季度营收"。专业的 forecasting 上 ARIMA / SARIMA / state space model 调一周;我们要的是"现在马上有个基础数字 + 不太离谱"。 Facebook (Meta) 开源的 Prophet 是 GAM (Generalized Additive Model) 风格的 time series 工具,对"商业数据特征 (趋势 + 周期 + 节假日 + 异常点)" 极友好,几乎零调参就有不错效果。 ## 装 ```bash uv add prophet # Mac M 系列 / Win 装 prophet 偶尔遇 cmdstanpy 编译问题 # 解决:先 conda install cmdstanpy 再 pip install prophet ``` ## 5 分钟 demo ```python import pandas as pd from prophet import Prophet # 数据格式:必须两列 ds (datetime) + y (value) df = pd.read_csv('daily_revenue.csv') # date,revenue # 2023-01-01,1234.5 # 2023-01-02,1289.1 # ... df.columns = ['ds', 'y'] # 模型 m = Prophet() m.fit(df) # 预测未来 90 天 future = m.make_future_dataframe(periods=90) forecast = m.predict(future) # yhat = 预测值;yhat_lower/upper = 80% 置信区间 print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail()) # 可视化 m.plot(forecast).savefig('forecast.png') m.plot_components(forecast).savefig('components.png') ``` 输出: - `forecast.png`:历史 + 预测曲线 + 置信带 - `components.png`:拆解成 trend / weekly / yearly 三个成分 调参为 0。 ## 加节假日 中国春节 / 双 11 等是营业额异常点,加进 model 让预测更准: ```python holidays = pd.DataFrame({ 'holiday': 'major_sale', 'ds': pd.to_datetime([ '2023-11-11', '2024-11-11', # 双 11 '2023-06-18', '2024-06-18', # 618 ]), 'lower_window': -1, # 节前 1 天也算 'upper_window': 2, # 节后 2 天也算 }) m = Prophet(holidays=holidays) m.fit(df) ``` 或者内置节假日: ```python m.add_country_holidays(country_name='CN') # 自动加入春节 / 国庆 / 五一等 ``` ## 多季节性 默认 yearly + weekly + daily。要月度 / 自定义周期: ```python m = Prophet() m.add_seasonality(name='monthly', period=30.5, fourier_order=5) m.fit(df) ``` `fourier_order` 是季节性的"灵活度"(越大越能拟合复杂周期;过高 overfit)。 ## 外部回归量(exogenous) 业务量受外部因素影响: ```python df['ads_spend'] = ... # 广告投入 df['is_promotion'] = ... # 0/1 促销标志 m = Prophet() m.add_regressor('ads_spend') m.add_regressor('is_promotion') m.fit(df) # 预测时也要提供未来值 future['ads_spend'] = expected_ads_for_next_90d future['is_promotion'] = expected_promo_flag forecast = m.predict(future) ``` 广告 / 促销作为协变量进 model,预测更贴合业务现实。 ## 异常点 Prophet 对异常点(COVID 期间 / 单日大促)相对鲁棒(用 GAM + 平滑)。 不需要手工剔除。要进一步控制可以: ```python # 把已知异常段 mark 为 NaN 让 Prophet 忽略 df.loc[(df['ds'] >= '2020-02-01') & (df['ds'] <= '2020-05-01'), 'y'] = None m.fit(df) ``` 或者用 `add_seasonality` 处理"上下界" 让 trend 不被极端值影响。 ## cross-validation 评估 ```python from prophet.diagnostics import cross_validation, performance_metrics # initial 365 天训练,每 30 天滚动一次,预测 horizon=30 天 df_cv = cross_validation(m, initial='365 days', period='30 days', horizon='30 days') metrics = performance_metrics(df_cv) print(metrics[['horizon', 'mape', 'rmse']].head()) # horizon mape rmse # 0 1 days 0.08 102.3 # 1 2 days 0.09 105.1 # ... ``` MAPE (Mean Absolute Percentage Error) < 10% 在大多数商业数据是 "还行",5% 算优秀。 ## 与替代品对比 | | Prophet | ARIMA / SARIMA | statsmodels | NeuralProphet | NHITS / TimeGPT | |---|---|---|---|---|---| | 学习曲线 | 极低 | 高(要懂 ACF/PACF) | 高 | 中 | 低 | | 调参量 | 几乎 0 | 多 | 多 | 中 | 几乎 0 | | 自动季节性 | ✅ | 手动 | 手动 | ✅ | ✅ | | 节假日 | ✅ | 手动 | 手动 | ✅ | ✅ | | 多变量 | regressor | VAR | VAR | ✅ | ✅ | | 高频数据 | 中(小时级) | ✅ | ✅ | ✅ | ✅ | | 长期趋势 | 强 | 中 | 中 | 强 | 强 | 简单业务时间序列 → Prophet。 学术 / 严谨需求 → ARIMA / state space。 现代深度方法 → NeuralProphet / Darts / Nixtla statsforecast。 ## 真实业务案例 我们用 Prophet 做月度营收预测: - 训练数据:2 年日级营收 - 加节假日:春节 + 双 11 + 618 - 外部回归:广告预算 + 促销日 - horizon:90 天 vs 之前的"人拍" 预测: | | 人拍 | Prophet | Prophet + 节假日 + regressor | |---|---|---|---| | MAPE | 25% | 12% | 7% | | 工时 | 4 hour/月 | 10 min/月 | 30 min/月 | 不仅准、还省时。 ## 输出多 model + ensemble ```python # 跑 3 个不同 changepoint_prior_scale 取均值 forecasts = [] for cps in [0.001, 0.05, 0.5]: m = Prophet(changepoint_prior_scale=cps) m.fit(df) forecasts.append(m.predict(future)['yhat']) ensemble = pd.concat(forecasts, axis=1).mean(axis=1) ``` 不同超参对不同部分敏感;平均通常更稳。 ## 何时不该用 Prophet - **特别短**的时间序列(< 2 季):信号不够,model 拟合差 - **复杂多变量交互**:要用 LightGBM / 神经网络 - **极高频(毫秒级)实时**:Prophet 慢,用 specialized 工具 - **概率性预测要严格 calibrated**:bootstrap interval 仅近似 ## 踩过的坑 1. **列名必须 ds / y**:用了别的名 Prophet 不识别。`df.rename(...)` 后再 fit。 2. **datetime 时区**:Prophet 内部假设 naive datetime(无时区)。 带 tz 会报错。`df['ds'] = df['ds'].dt.tz_localize(None)`。 3. **`make_future_dataframe(periods=90, freq='D')`**:默认日级; 月级数据要 `freq='M'`。漏指定就把月数据当日数据预测。 4. **logistic growth 需要 cap**:用 `growth='logistic'` 时必须给 `cap` 列(上界),否则报错。 5. **changepoint 自动检测不靠谱**:业务有大转折(疫情 / 公司战略 变化)时,手动指定: ```python m = Prophet(changepoints=['2020-02-01', '2022-06-15']) ```