知识广场
按学科筛选:计算机科学
«计算机科学» 分类下共 256 篇帖子
## 起因 第一次看到 React Server Components(RSC)的介绍很懵: "在服务端渲染的组件"——这不就是 SSR 吗?为什么搞个新东西? 直到我真的写了一个 RSC 项目(Next.js 14 app router)才明白它解的 是另一个问题:**让组件能直接访问后端资源(DB / 文件 / API), 零 JS bundle 开销**。 ## 传统 SSR 的局限 传统 SSR(getServerSideProps / loader): ```jsx // pages/posts/[id].js (传统 Next.js pages router) export async function getServerSideProps({ params }) { const post = await db.posts.findById(params.id) const author = await db.users.findById(post.author_id) const comments = await db.comments.findByPost(params.id) return { props: { post, author, comments } } } export default function Page({ post, author, comments }) { return ( <article> <h1>{post.title}</h1> <AuthorBlock user={author} /> <CommentList items={comments} /> </article> ) } ``` 问题: - 数据获取集中在一个函数里,组件需要数据时要 prop drill 传 - 整个组件树都 hydrate(即使大部分不需要交互) - 所有数据 JSON serialize 进 HTML,包大 - `<AuthorBlock>` 和 `<CommentList>` 各自需要的数据要 page 这层先取 好再传 ## RSC 的做法 ```tsx // app/posts/[id]/page.tsx (Next.js 14 app router) // 默认就是 Server Component! import { db } from '@/db' import AuthorBlock from './AuthorBlock' import CommentList from './CommentList' export default async function Page({ params }: { params: { id: string }}) { const post = await db.posts.findById(params.id) return ( <article> <h1>{post.title}</h1> <AuthorBlock userId={post.author_id} /> <CommentList postId={params.id} /> </article> ) } ``` ```tsx // app/posts/[id]/AuthorBlock.tsx // 也是 Server Component,自己取数据 import { db } from '@/db' export default async function AuthorBlock({ userId }: { userId: string }) { const user = await db.users.findById(userId) return ( <div> <img src={user.avatar} alt="" /> <strong>{user.name}</strong> </div> ) } ``` ```tsx // app/posts/[id]/CommentList.tsx import { db } from '@/db' export default async function CommentList({ postId }: { postId: string }) { const comments = await db.comments.findByPost(postId) return ( <ul> {comments.map(c => <li key={c.id}>{c.body}</li>)} </ul> ) } ``` 变化: - 组件**就是 async 函数**,直接 await DB / API - 每个组件管自己的数据获取 - 没有 prop drilling - 这些组件的 JS 完全不进客户端 bundle(因为它们只在服务端跑) ## 客户端组件交互("use client") 需要 useState / 事件 / 浏览器 API 的组件加 `"use client"`: ```tsx // app/posts/[id]/LikeButton.tsx "use client" import { useState } from 'react' export default function LikeButton({ postId, initialCount }: ...) { const [count, setCount] = useState(initialCount) return ( <button onClick={() => { setCount(c => c + 1) fetch(`/api/posts/${postId}/like`, { method: 'POST' }) }}> ❤️ {count} </button> ) } ``` ```tsx // app/posts/[id]/page.tsx (server component) import LikeButton from './LikeButton' export default async function Page({ params }: ...) { const post = await db.posts.findById(params.id) return ( <> <h1>{post.title}</h1> <LikeButton postId={post.id} initialCount={post.likes} /> </> ) } ``` `LikeButton` 是 client component,JS 进 bundle、可以 useState。 其它 server component 完全不进 bundle。 ## 实际收益 我们的项目(中等规模博客): | | 传统 SSR | RSC | |---|---|---| | 首页 JS bundle | 280 KB | 60 KB | | First Contentful Paint | 1.8s | 0.9s | | LCP | 2.4s | 1.4s | | 代码可读性 | 数据 / UI 分离明显 | 数据在用它的组件里 | bundle 显著小(因为大部分组件不进客户端),首屏快得多。 ## 什么时候用 client component `"use client"` 的场景: - `useState` / `useEffect` 等 React hooks - onClick / onChange 等事件 - 浏览器 API(localStorage / window / etc) - 用了 client-only 库(如 framer-motion / chart.js) - Context provider(虽然 server component 也能 consume) 其它一律 server component。原则:"静态展示 → server;交互 → client"。 ## 数据获取最佳实践 ### 1. 直接 DB / ORM ```tsx import { prisma } from '@/lib/db' export default async function Page() { const posts = await prisma.post.findMany({ take: 20 }) return <PostList posts={posts} /> } ``` 不再需要 REST / GraphQL 层。component → ORM → DB。 ### 2. 并行获取 ```tsx export default async function Page({ params }: ...) { const [post, author] = await Promise.all([ db.posts.findById(params.id), db.users.findById(params.userId), ]) return ... } ``` 避免 await 串行让请求慢。 ### 3. Streaming + Suspense ```tsx import { Suspense } from 'react' export default function Page({ params }: ...) { return ( <> <h1>Article</h1> <Suspense fallback={<div>loading post...</div>}> <PostBody id={params.id} /> </Suspense> <Suspense fallback={<div>loading comments...</div>}> <CommentList postId={params.id} /> </Suspense> </> ) } ``` server 先 stream 出 `<h1>` + fallback,post / comments 各自异步加载完 就 stream 出真实 DOM 替换 fallback。用户看到 "渐进显示" 而不是 "全部等好再显示"。 ## Server Actions:从 client 调 server 函数 ```tsx // app/posts/[id]/page.tsx async function deletePost(postId: string) { 'use server' await db.posts.delete(postId) revalidatePath('/posts') } export default function Page({ params }: ...) { return ( <form action={async () => { 'use server'; deletePost(params.id) }}> <button>删除</button> </form> ) } ``` 不需要写 `/api/posts/:id/delete` endpoint。直接调函数,Next.js 自动 hook 成 RPC。 ## 与传统 SSR / SPA 共存 不是非黑即白。常见混合: - 营销页 / 文章 / 列表 → server component - 仪表盘 / 复杂表单 / 实时聊天 → client component - Next.js app router 默认 server,按需 `"use client"` ## 效果 - 首屏体验大幅改善(bundle 减少) - 数据 / UI 不再分离,组件代码更聚合 - 不需要专门维护 REST API 给前端用(直接调 ORM) - 但要熟悉"哪些代码跑服务端 / 哪些跑客户端" 心智模型 ## 踩过的坑 1. **在 server component 里用 useState** → 编译报错。新人最常见的 错。要么加 "use client",要么把状态下移到 client 子组件。 2. **import 服务端库进 client component**:bundle 暴涨 + 可能泄漏 secret。`import { db } from '@/db'` 在 client component 里就是 重大失误。lint 规则强制检查。 3. **server / client 边界传 props 必须 serializable**:函数 / Map / class 实例传不过去。只能传 plain object / 数组 / 基础类型。 4. **revalidate / cache 复杂**:Next.js 默认激进 cache,dev / prod 行为差异大。明确用 `cache: 'no-store'` / `revalidate: 60` / `cookies()`。 5. **Vercel / 自托管差异**:Server Actions 等功能在自托管 Next.js 还要配 standalone build。Vercel 上很丝滑,自部署要折腾。
## 起因 公司不想全部 LLM 调用都走 OpenAI / Anthropic API: - 成本:长上下文 + 高 QPS → API 月几万刀 - 数据敏感:医疗 / 金融数据不送 cloud - 自由度:fine-tune 自己 model 开源 LLM(Llama 3 / Qwen 2 / DeepSeek 等)质量足够替代 GPT-4 一些场景。 推理框架选 `vLLM`(Berkeley 出,paged attention,**事实标准**)。 ## 装 ```bash pip install vllm ``` 需要 NVIDIA GPU + CUDA。RTX 4090 / A100 / H100 / L40。 ## 跑 ```bash python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Meta-Llama-3-8B-Instruct \ --port 8000 ``` 启动后兼容 OpenAI API 格式: ```python from openai import OpenAI client = OpenAI(base_url='http://localhost:8000/v1', api_key='x') res = client.chat.completions.create( model='meta-llama/Meta-Llama-3-8B-Instruct', messages=[{'role': 'user', 'content': '你好'}], ) print(res.choices[0].message.content) ``` 现有 OpenAI client 一行改 base_url 就能换成自己的 vLLM。 ## 性能 vs HF transformers 跑 Llama 3 8B / 单 A100: | | 推理速度 | |---|---| | HuggingFace transformers | 30 tok/s | | vLLM | 800 tok/s(continuous batching) | | TGI (huggingface) | 600 tok/s | vLLM 的 paged-attention + continuous batching 让 GPU 利用率拉满 → 20x HF transformers。 ## 跑大 model 70B 模型需 80GB+ VRAM。单 A100 80GB 跑 fp16 紧。 量化 + tensor parallel: ```bash # 4-bit AWQ 量化 → 40GB 装得下 single A100 python -m vllm.entrypoints.openai.api_server \ --model TheBloke/Llama-3-70B-Instruct-AWQ \ --quantization awq # 多 GPU tensor parallel python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Meta-Llama-3-70B-Instruct \ --tensor-parallel-size 4 ``` 4 张 A100 跑 70B fp16 → 充足空间 + 速度更快。 ## paged attention 是什么 LLM 推理时每个 sequence 有 KV cache(每 token 的 attention key/value 存住)。 传统:每 sequence 预留最大 length 的 contiguous block → 浪费(多数 sequence 短)。 paged:把 KV cache 切成固定大小 page(类似 OS virtual memory)→ 按需分配 / 共享相同 prefix。 效果: - 内存利用率 90%+ vs 传统 30-50% - 同 GPU 能跑更多 concurrent request - prefix caching:相同 system prompt 的多个请求共享 prefix cache ## continuous batching 老办法 static batching:等够 N 个 request → 一起跑 → 等最慢的 → 返回。 有 request 早完成也要等。 continuous batching:每生成 1 token 就检查能不能塞新 request。 不浪费 GPU cycle。 ``` Time GPU 0 [A:gen, B:gen, C:gen, D:gen] # A B C D 同 batch 1 [A:gen, B:gen, C:gen, D:gen, E:start] # E 加入 2 [A:done, B:gen, C:gen, D:gen, E:gen] # A 完成腾出 slot 3 [F:start, B:gen, C:gen, D:gen, E:gen] # F 加入 ``` 吞吐量是传统 batching 2-5x。 ## prefix caching system prompt 经常一样: ``` You are a helpful assistant. Today is 2026-05-25. Answer concisely. [user message: ...] ``` vLLM 0.4+ 默认开 prefix cache:相同 prefix 的 KV cache 共享 → system prompt 不重新算 → 加速。 特别适合 chatbot / agent 多 turn 对话。 ## structured output GPT-4 / Claude 都支持 JSON mode / function calling。vLLM 0.4+ 也有: ```python res = client.chat.completions.create( model=..., messages=[...], extra_body={ 'guided_json': { 'type': 'object', 'properties': { 'name': {'type': 'string'}, 'age': {'type': 'integer'}, }, 'required': ['name', 'age'], }, }, ) ``` 底层用 outlines / xgrammar 强制 token 选择符合 schema → 100% 合规 JSON。 ## LoRA 多租户 部署一个 base model + 多个 LoRA fine-tune 一起 serve: ```bash python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Meta-Llama-3-8B \ --enable-lora \ --lora-modules sql=./loras/sql-lora medical=./loras/medical-lora ``` ```python client.chat.completions.create(model='sql', ...) client.chat.completions.create(model='medical', ...) ``` 多个特化 model 共享同 base → 显存省 + 切换零开销。 ## 监控 vLLM 暴露 Prometheus metrics: ```bash --enable-metrics # /metrics endpoint ``` 关键指标: - `vllm:num_requests_running` - `vllm:gpu_cache_usage_perc` - `vllm:time_to_first_token_seconds` - `vllm:e2e_request_latency_seconds` Grafana dashboard 看吞吐 / latency / cache 利用。 ## 与替代方案对比 | | vLLM | TGI | llama.cpp | ollama | |---|---|---|---|---| | 目标 | 高吞吐 GPU server | 同 | CPU/GPU 单机 | 单用户简单 | | 性能 | 最高 | 中高 | 中(CPU 优秀) | 中 | | API | OpenAI-compat | 自家 + OAI | 自家 + OAI | 自家 | | 量化 | AWQ / GPTQ / FP8 | 同 | GGUF | GGUF | | 适合 | 生产 server | 生产 server | 笔记本 | 个人 | 生产用 vLLM。个人 / 笔记本用 ollama。 ## 实战 cost 对比 跑 Llama 3 70B 处理 100M tokens/月: | 方案 | 月成本 | |---|---| | OpenAI GPT-4 API | ~$8000 | | Anthropic Claude Sonnet | ~$6000 | | Together / Fireworks API(70B) | ~$900 | | 自托管 vLLM + 2× A100 cloud | ~$2500(GPU 租金) | | 自托管 + own H100 | ~$0(一次性买卡) | 自托管 break-even 大约 200M tokens/月(vs cloud API)。 量大 + 控制需求 → 自托管。量小 → API。 ## 容器化部署 ```dockerfile FROM vllm/vllm-openai:latest CMD ["--model", "meta-llama/Meta-Llama-3-8B-Instruct", "--port", "8000"] ``` ```yaml # k8s deployment resources: limits: nvidia.com/gpu: 1 ``` prod 跑 NVIDIA GPU operator 配 GPU node + vLLM container。 ## 踩过的坑 1. **OOM**:模型 + KV cache 超 VRAM → OOM。`--gpu-memory-utilization 0.9` 留 buffer;或者降 `--max-model-len`。 2. **量化精度损失**:AWQ 4-bit 比 fp16 任务上有 1-3% 精度损失。 测过再上。 3. **prefix cache 没用上**:system prompt 略有不同(如时间戳) → prefix 不匹配。把动态部分放后面。 4. **OpenAI client 兼容性**:某些参数(如 `tools`)vLLM 还不支持 → 用基础 chat completions。 5. **生产 reload model**:vLLM 不支持热重载。换 model 必须重启容器。 blue-green deploy。
任何挂公网的 SSH 端口每天会收到几千到几万次扫描尝试。 真正的防线是禁用密码登录 + 只用密钥,但 fail2ban 作为第二道防线 能进一步降低日志噪音、屏蔽明显的恶意 IP 段。 ## 安装 ```bash sudo apt install -y fail2ban sudo systemctl enable --now fail2ban ``` ## 配置(重点:local 覆盖,不动 .conf) **永远不要直接改 `/etc/fail2ban/jail.conf`**——升级会被覆盖。改 `.local`: `/etc/fail2ban/jail.local`: ```ini [DEFAULT] # 屏蔽时长:从 10 分钟开始指数退避,最多 1 周 bantime = 10m bantime.increment = true bantime.factor = 2 bantime.maxtime = 1w # 检测窗口:10 分钟内 findtime = 10m maxretry = 5 # 不要 ban 自己 ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8 # 后端:systemd journal(无 /var/log/auth.log 时必选) backend = systemd # 用 nftables 而不是默认的 iptables banaction = nftables-multiport [sshd] enabled = true port = ssh logpath = %(sshd_log)s maxretry = 3 ``` `bantime.increment = true` 让累犯越关越久:第一次 10 分钟,第二次 20, 然后 40、80...... 最长 1 周。绝大多数扫描脚本几次就放弃换下一个 IP。 ## 启用 + 校验 ```bash sudo systemctl restart fail2ban sudo fail2ban-client status # Number of jail: 1 # Jail list: sshd sudo fail2ban-client status sshd # Currently failed: ... # Currently banned: ... # Banned IP list: 1.2.3.4 5.6.7.8 ... ``` ## 实时观察 ```bash sudo tail -f /var/log/fail2ban.log # 或者 journal: sudo journalctl -u fail2ban -f ``` ## 手动操作 IP ```bash # 立即解封 sudo fail2ban-client unban 1.2.3.4 # 永久 ban sudo fail2ban-client set sshd banip 1.2.3.4 ``` ## 验证 nftables 规则 ```bash sudo nft list set inet f2b-table addr-set-sshd # table inet f2b-table { # set addr-set-sshd { # elements = { 1.2.3.4, 5.6.7.8 } # } # } ``` ## 配合其它服务 fail2ban 自带几十个 jail(nginx-botsearch、postfix、recidive 等)。 常见组合: ```ini [nginx-botsearch] enabled = true filter = nginx-botsearch logpath = /var/log/nginx/access.log maxretry = 3 findtime = 2m [recidive] # 反复在多个 jail 触发的 IP,全局长期 ban enabled = true bantime = 1w findtime = 1d maxretry = 5 ``` `recidive` 是元 jail:观察 fail2ban 自己的日志,把"在多个 jail 都被 ban 过"的 IP 长期屏蔽,效果非常好。 ## 踩过的坑 - 把自己 ban 了:从 Console / out-of-band 连进去,`fail2ban-client unban <你的IP>`, 然后把 ignoreip 加上你常用的 IP 段。 - `backend = systemd` 必须在 `[DEFAULT]` 段,写错位置会被忽略,filter 看 不到任何日志,但状态看着一切正常 —— 一定要看 `failed` 数字是不是在增长。 - IPv6 时代,单 IP ban 几乎没意义;建议改 `banaction = nftables-multiport[blocktype=drop]` 按整段 ASN ban,或者干脆把 SSH 端口改成非 22,噪音少 90%+。
## 起因 个人 / 小项目用 SQLite: - 单文件,零运维 - 性能足够(WAL 模式撑万 QPS 读 + 千 QPS 写) - 备份 = cp 文件 缺点:单服务器挂了 → 服务停 + 上次备份后的数据丢。 `Litestream`(Ben Johnson):实时把 SQLite WAL 流式复制到 S3 / SFTP / 其它。无应用改动 + 几乎实时(秒级 RPO)。 ## 装 ```bash # binary wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz tar -xf litestream-*.tar.gz sudo mv litestream /usr/local/bin/ ``` ## 配 `/etc/litestream.yml`: ```yaml dbs: - path: /srv/myapp/db.sqlite3 replicas: - type: s3 bucket: my-backups path: myapp/db region: us-east-1 access-key-id: ${AWS_ACCESS_KEY_ID} secret-access-key: ${AWS_SECRET_ACCESS_KEY} retention: 720h # 30 day ``` 启动: ```bash litestream replicate -config /etc/litestream.yml ``` ## systemd service ```ini # /etc/systemd/system/litestream.service [Unit] Description=Litestream After=network.target [Service] ExecStart=/usr/local/bin/litestream replicate -config /etc/litestream.yml Restart=always EnvironmentFile=/etc/litestream.env [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable --now litestream ``` ## 怎么工作 SQLite WAL 模式下,写操作先到 -wal 文件,定期 checkpoint 到主 db。 Litestream: 1. open snapshot baseline 上传 S3 2. tail WAL → 流式上传增量 (每秒) 3. WAL 自动 checkpoint 时 → 新 snapshot S3 存: ``` myapp/db/ snapshots/ 20250314T100000.db.lz4 20250320T100000.db.lz4 wal/ 20250314T100000_000001.wal.lz4 ... ``` 可以 restore 到任意时间点(snapshot + replay WAL)。 ## restore ```bash # 最新 litestream restore -o /tmp/restored.db -config /etc/litestream.yml /srv/myapp/db.sqlite3 # 时间点 litestream restore -o /tmp/restored.db \ -timestamp '2025-03-14T10:00:00Z' \ -config /etc/litestream.yml \ /srv/myapp/db.sqlite3 ``` 5 分钟级别 RTO(取决于 DB size)。 ## 应用透明 litestream 不改 SQLite 行为。应用照常 read/write `db.sqlite3`。 litestream 是 sidecar 进程,监 WAL。 应用不知有 litestream 存在 → 0 风险。 ## 性能影响 litestream 跟 SQLite 共享 disk IO。 小 DB(< 1 GB)+ 适度写(< 100 wps):几乎无感。 高写 throughput → litestream 上传 bandwidth 跟得上吗? 我们 production: - 1 GB SQLite - 50 wps - litestream + S3:CPU < 1%,network 几 KB/s 平均 - WAL upload latency P99 < 2s ## 不替代 HA litestream 是 **disaster recovery**(备份 + restore),不是 HA。 HA 需要: - 多 server / 多 region 同时活 - failover 自动 SQLite + litestream 是"主备"模式:主挂了,备份机 restore + 起来 = 5 分钟 downtime。 真要 HA → Postgres + replication / managed DB。 ## read replica litestream 0.4+ 实验性 read replica: ```yaml dbs: - path: /srv/myapp/db.sqlite3 replicas: - type: s3 bucket: ... ``` ```bash # 在别 server 上 litestream replicate \ -read-only \ -config replica.yml ``` 从 S3 拉 + apply WAL → 本地 read-only SQLite。 读扩展可行(写仍主单机)。 ## 价格 S3 storage:1 GB DB + 1 month WAL = 几 GB → $0.10/月。 S3 PUT:每秒一次 WAL upload → 86400 PUT/day × $0.005/1k = $0.40/day。 总:几刀/月。比 RDS db.t3.micro($15/月)便宜很多。 ## SFTP / GCS / Azure 不一定 S3: ```yaml replicas: - type: sftp host: backup.example.com user: backup path: /backups/myapp key-path: ~/.ssh/backup_key ``` 或 GCS: ```yaml replicas: - type: gcs bucket: my-bucket path: myapp/db ``` ## 与 PG / MySQL 对比 | | SQLite + Litestream | Postgres | |---|---|---| | 写 QPS | 千级 | 万级 | | 多 reader | 是 | 是 | | 多 writer | 单 | 是 | | HA | 主备(5min downtime) | streaming replica | | 运维 | 极简 | 中 | | 备份 | litestream | pg_dump / WAL-G | 简单 app + 单 server → SQLite + litestream。 多 server / 高 throughput / HA 必须 → Postgres。 ## 真实部署 我个人项目 / 小 SaaS(< 1k DAU): - VPS $5/月(Hetzner) - Django + SQLite + litestream → S3 - nginx reverse proxy - Cloudflare CDN free 总成本 < $10/月。 db.sqlite3 + WAL 上 S3 自动 5 分钟内一份 (snapshot interval)。 服务器爆炸 → 新 VPS + restore + 部署,半小时上线。 SLA 不是 99.99% 但 99% 易达。 ## 与 cron + cp 对比 ```bash cp db.sqlite3 /backup/db-$(date +%F).sqlite3 ``` 简单但: - 间隔大(每日)→ RPO 一天 - 没 PITR - cp 时 WAL 可能不一致 litestream RPO 秒级 + PITR + WAL consistent。 ## 监控 litestream 暴露 prometheus metrics: ```yaml addr: ":9090" ``` - `litestream_replica_position_bytes` - `litestream_replica_last_sync_seconds` 报警:> 60s 没 sync。 ## 踩过的坑 1. **WAL 没启用**:`PRAGMA journal_mode=WAL;` 必须。不是 WAL litestream 不能 tail。 2. **multi-process write 麻烦**:SQLite 多进程写有限制。 只让一个进程写 → litestream tail 那 WAL。 3. **DB 删了**:手动 `rm db.sqlite3` → litestream 看到删,但 S3 上仍 有数据。restore 即可。但小心 `--full-resync` 误操作覆盖 backup。 4. **快速 restore 慢**:大 DB(10 GB+)restore 几分钟(下 snapshot + replay WAL)。RTO 不是 instant。 5. **monitor 缺失**:litestream 进程死了,应用照常跑没人知道 → 备份悄无声息断。systemd Restart=always + prometheus monitor。
## 起因 之前文章分别介绍过 borg 和 restic。一个朋友问"我应该选哪个?" 干脆做一个对比实测:同样数据集 + 同样备份策略,跑出真实数字。 三个候选: - **borg**:Python 写的,2014 起,老牌 - **restic**:Go 写的,2015 起,现代 - **kopia**:Go 写的,2018 起,最年轻,UI 最强 三者都支持:客户端加密、去重、增量、压缩、多 backend。 ## 测试 setup - 数据:500 GB(混合:1M 个文件 + 几个大 VM disk) - 机器:8 core / 32 GB / NVMe SSD - 后端:本地磁盘(避免网络变量) - 跑 5 次取均值 ## 1. 首次全量备份 | | 时间 | 备份大小 | CPU 峰值 | RAM 峰值 | |---|---|---|---|---| | borg | 38 min | 195 GB | 800% (8 core) | 1.2 GB | | restic | 31 min | 198 GB | 700% | 1.8 GB | | kopia | 24 min | 180 GB | 850% | 2.4 GB | kopia 最快 + 最小(zstd 默认压缩级别更激进)。 borg / restic 接近。 ## 2. 增量备份(改动 5GB) | | 时间 | 新写入 | |---|---|---| | borg | 1m 50s | 1.8 GB | | restic | 1m 20s | 2.0 GB | | kopia | 0m 55s | 1.5 GB | kopia 增量也最快。 ## 3. 还原性能(取 10 个文件) | | 时间 | |---|---| | borg | 4s | | restic | 2s | | kopia | 2s | borg 单文件还原稍慢(Python 启动开销)。 ## 4. 还原全量 | | 时间 | |---|---| | borg | 32 min | | restic | 25 min | | kopia | 22 min | 跟备份时间类似的趋势。 ## 5. 仓库 check(校验完整性) | | 时间 | |---|---| | borg check | 8 min | | restic check --read-data | 18 min | | kopia maintenance | 6 min | borg / kopia 校验快。restic 默认 check 不读 data;带 `--read-data` 慢。 ## 6. 大量小文件场景(100 万个 1KB 文件) | | 备份时间 | |---|---| | borg | 14 min | | restic | 12 min | | kopia | 9 min | kopia 处理 small files 最好。 ## 7. UI / 易用性 ### borg:CLI only ```bash borg init -e repokey /backup/repo borg create /backup/repo::$(date +%F) /home /etc borg list /backup/repo borg extract /backup/repo::2024-05-24 home/me/important.txt ``` `borgmatic` 是它的 YAML wrapper。无原生 GUI。 ### restic:CLI + fuse mount ```bash restic init -r /backup/repo restic backup -r /backup/repo /home /etc restic snapshots restic mount /tmp/r # FUSE 挂载浏览 ``` 无 GUI 但 fuse mount 让"浏览历史快照" 很方便。 ### kopia:CLI + Web UI + 跨平台 GUI ```bash kopia repository create filesystem --path=/backup/repo kopia snapshot create /home /etc kopia snapshot list kopia server start --address=0.0.0.0:51515 --insecure # Web UI ``` Web UI 浏览快照 / 还原 / 管理 policy 都可视化。 还有 Windows / Mac 客户端 GUI。 ## 8. 多客户端共享仓库 多机器备份到同一仓库(共享 dedup 池): | | 支持 | 体验 | |---|---|---| | borg | ❌ 单写者(一次一客户端) | 复杂:需 borg serve + lock | | restic | ✅ 多客户端 | 简单 | | kopia | ✅ 多客户端 | 简单(甚至有 P2P 模式) | 家庭多设备 / 公司多服务器场景 restic / kopia 友好。 ## 9. 远程后端支持 | | 本地 | SSH | S3 | B2 | GCS | Azure | rclone | |---|---|---|---|---|---|---|---| | borg | ✅ | ✅ | rclone 代理 | rclone | rclone | rclone | ✅ | | restic | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | kopia | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | webdav | restic / kopia 原生支持云存储更全。 ## 10. 安全 / 加密 三者都 AES + 客户端加密 + 服务端只看到加密块。 | | 加密算法 | 密钥管理 | |---|---|---| | borg | AES-256-CTR + HMAC-SHA256 | keyfile / repokey | | restic | AES-256-CTR + Poly1305-AES | 内置 config | | kopia | AES-256-GCM | 多 key 支持 | 都安全。kopia 用 AEAD 更现代。 ## 11. 资源占用对比 长期运行的 inactive 仓库: | | 元数据大小 | 看快照速度 | |---|---|---| | borg | 中(chunks index) | 中 | | restic | 中 | 慢(大 repo 时) | | kopia | 小 | 快 | restic 在 TB 级仓库的"列出快照"操作会慢,是个已知问题。 ## 选择决策 | 场景 | 推荐 | |---|---| | 单机 / Linux 老手 / 已经在用 | borg(稳定 + 成熟) | | 多机器 + 云后端 + 团队 | restic(生态最广) | | 喜欢 GUI + 跨平台 + 多设备共享 | **kopia**(最现代) | | 不想折腾 | restic | | 极致性能 | kopia | 我个人现在新项目首选 kopia,遗留项目继续 restic / borg。 ## 共同 best practice 不管选哪个: 1. **测试还原**:每季度真的还原一次到不同机器,验证流程能通 2. **3-2-1 规则**:3 份数据,2 种媒介,1 份异地 3. **加密 key 单独备份**:repo 备份了但 key 丢了 = 数据死透了 4. **自动 prune 策略**:保留 7d / 4w / 12m 之类 5. **monitor 告警**:备份失败要立刻知道(systemd OnFailure / healthchecks.io) ## 配置示例:kopia + B2 ```bash # 第一次 kopia repository create b2 \ --bucket=my-backups \ --key-id=00abc... \ --key=K00... \ --password=very-strong-password # 定 policy kopia policy set --global \ --keep-latest 10 \ --keep-hourly 24 \ --keep-daily 30 \ --keep-weekly 12 \ --keep-monthly 24 \ --keep-annual 5 \ --compression=zstd # 备份 kopia snapshot create /etc /home/me/important /srv # 定时 # systemd timer 每小时 ``` ## 踩过的坑 1. **borg 单写锁卡死**:多机器同时备份 → 第二个等待 / 失败。 解决:每机一独立 repo,或者错峰备份。 2. **restic prune 慢**:TB 级仓库 prune 几小时。改 `forget --keep-* --prune-max-unused 5%` 增量 prune。 3. **kopia metadata 损坏**:仓库被 unsafe shutdown → 偶尔 corruption。 `kopia maintenance --safety=full` 修复。 4. **三者都 case-sensitive**:Windows / Mac 用户备份大小写不敏感 文件系统,恢复到 Linux 时可能冲突。 5. **加密密码丢了**:**没有 recovery**。所有这类工具都设计成"你忘 密码 = 数据死透了"。一定要写在 password manager + 离线纸质备份。
不想把数据发到 OpenAI 但想用大模型?Ollama 让你本地跑 Llama 3 / Qwen / DeepSeek / Mistral 等开源模型,LangChain 提供统一封装做 RAG / agent。 整套零成本(GPU 电费除外),数据完全在本地。 ## 1. 装 Ollama ```bash curl -fsSL https://ollama.com/install.sh | sh # macOS: brew install ollama # Windows: 从 ollama.com 下安装包 ollama --version ``` Ollama 作为后台服务跑(端口 11434): ```bash systemctl start ollama # Linux brew services start ollama # macOS ``` ## 2. 拉个模型 ```bash ollama pull qwen2.5:7b # 或更小的:qwen2.5:3b (4 GB 显存就能跑) # 或更大的:qwen2.5:14b、llama3.1:70b(需要 24+ GB / 80 GB) ollama list ``` ## 3. 命令行直接聊 ```bash ollama run qwen2.5:7b >>> 用 Python 写一个二分查找 ``` `Ctrl-D` 退出。 ## 4. HTTP API(OpenAI 兼容) Ollama 默认在 `:11434` 暴露 OpenAI 兼容 endpoint: ```bash curl http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "你好"}] }' ``` 任何 OpenAI 库直接换 base URL 就能用: ```python from openai import OpenAI client = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama') resp = client.chat.completions.create( model='qwen2.5:7b', messages=[{'role': 'user', 'content': '你好'}], ) print(resp.choices[0].message.content) ``` ## 5. LangChain 集成 ```bash uv add langchain langchain-community langchain-ollama ``` ```python from langchain_ollama import ChatOllama, OllamaEmbeddings llm = ChatOllama(model='qwen2.5:7b', temperature=0.3) emb = OllamaEmbeddings(model='nomic-embed-text') # 嵌入模型,需另拉 ``` `ollama pull nomic-embed-text` 拉一个 274MB 的嵌入模型。 ## 6. RAG:用本地 LLM + 本地知识库回答 ```python from langchain_chroma import Chroma from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain.chains import RetrievalQA from langchain_community.document_loaders import TextLoader # 1. 加载文档(这里举例一个 txt,实际可能是 md / pdf / html) loader = TextLoader('knowledge.md', encoding='utf-8') docs = loader.load() # 2. 切块 splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) chunks = splitter.split_documents(docs) # 3. 向量化 + 入库 vectorstore = Chroma.from_documents(chunks, embedding=emb, persist_directory='./chroma_db') # 4. 检索 + LLM 回答 qa = RetrievalQA.from_chain_type( llm=llm, chain_type='stuff', retriever=vectorstore.as_retriever(search_kwargs={'k': 4}), return_source_documents=True, ) result = qa.invoke({'query': '本文如何解释 X 概念?'}) print(result['result']) print('---sources---') for d in result['source_documents']: print(d.page_content[:80]) ``` 整个 pipeline 完全在本地。 ## 7. 持久化向量库 ```python # 写入后端 vectorstore.persist() # Chroma 自动 persist 到 directory # 之后加载已有库 vectorstore = Chroma(persist_directory='./chroma_db', embedding_function=emb) ``` Chroma 是文件型向量数据库,适合 < 100 万 chunk 的小规模 RAG。 大规模用 Qdrant / Milvus / Weaviate。 ## 8. Agent / tools ```python from langchain.agents import create_react_agent, AgentExecutor from langchain.tools import tool from langchain import hub @tool def get_weather(city: str) -> str: """获取指定城市的当前天气。""" # 假装调 API return f'{city} 今天晴,22°C' @tool def calculator(expr: str) -> str: """计算数学表达式,例如 '2 * (3 + 4)'。""" try: return str(eval(expr, {'__builtins__': {}})) except Exception as e: return f'error: {e}' prompt = hub.pull('hwchase17/react') agent = create_react_agent(llm, [get_weather, calculator], prompt) executor = AgentExecutor(agent=agent, tools=[get_weather, calculator], verbose=True, max_iterations=4) executor.invoke({'input': '北京天气怎么样?顺便算一下 47 * 12'}) ``` `verbose=True` 输出 agent 的思考过程(很好玩,但生产关掉)。 ## 9. 流式输出 ```python for chunk in llm.stream('用 100 字介绍 RAG'): print(chunk.content, end='', flush=True) ``` 或者在 FastAPI 里返回 SSE 流。 ## 10. 性能 tip - **量化**:`ollama pull qwen2.5:7b-instruct-q4_K_M` 4-bit 量化, 4 GB 显存 / 内存就能跑。精度降几个点 - **多模型切换**:`ollama list` + `ollama run`,自动 load / unload - **并发**:Ollama 默认串行处理,需要并发的话调 `OLLAMA_NUM_PARALLEL=4` - **GPU**:自动检测 CUDA / Metal;CPU 也能跑但慢 5-10x - **保持模型在内存**:`ollama keep-alive` 控制;默认 5 分钟没请求会卸载 ## 11. 模型选择 | 用途 | 推荐 | 显存 | |---|---|---| | 通用对话 / RAG | qwen2.5:7b / 14b | 8 / 16 GB | | 编程 | qwen2.5-coder:7b / deepseek-coder:6.7b | 8 GB | | 中文为主 | qwen2.5、yi、deepseek | - | | 极轻量 | qwen2.5:3b、phi3:mini | 4 GB | | 顶配 | llama3.1:70b、qwen2.5:72b | 48-80 GB | | 嵌入 | nomic-embed-text、bge-m3 | < 2 GB | ## 12. 数据安全 - Ollama 默认监听 `127.0.0.1:11434`,不暴露公网 - LangChain 调用的所有 API 都走本地 - 向量库(Chroma)默认本地文件 - 整个 pipeline 不发任何数据到云端 完美的 enterprise / 隐私敏感场景。 ## 踩过的坑 - 第一次拉大模型很慢(GB 级),可以预先 `ollama pull` 而不是等首次调用 超时。 - 7B 模型在 CPU 上跑非常慢(每 token ~1 秒),交互式不可用;GPU / Apple Silicon 必备。 - 上下文窗口默认很小(2048)。Ollama Modelfile 可以加大: ``` FROM qwen2.5:7b PARAMETER num_ctx 8192 ``` `ollama create my-qwen -f Modelfile`。 - LangChain 升级特别快,API 经常变。在 pyproject.toml lock 版本, 不要随便升。
## 起因 distributed trace 工具: - **Jaeger**:经典 + 自托管 + 存 Cassandra / ES - **Zipkin**:更老 - **Tempo**(Grafana Labs):新选择,存 S3 trace 数据量大(每 request 多 span,TB / day),存 Cassandra / ES 贵。 Tempo 设计为"object storage native trace store",类似 Loki for log。 ## Tempo 特点 - 存 trace blob 到 S3 / GCS(极便宜) - 只索引 trace ID(不索引 attribute)→ 不能按 service / tag 全文搜 - query 模式:先用 metric 找到时段 → 拿 trace ID → 查 trace 对应 metric/log/trace 思路: ``` metric (Prometheus / Mimir) ↓ 发现 spike 时段 log (Loki) ↓ 找 trace_id trace (Tempo) ↓ 详细看 trace ``` ## 装 ```yaml # docker-compose services: tempo: image: grafana/tempo:2.5.0 command: ['-config.file=/etc/tempo.yaml'] ports: - 3200:3200 # HTTP - 4317:4317 # OTLP gRPC volumes: - ./tempo.yaml:/etc/tempo.yaml - tempo-data:/tmp/tempo ``` `tempo.yaml`: ```yaml server: http_listen_port: 3200 distributor: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 storage: trace: backend: s3 s3: bucket: my-traces endpoint: s3.amazonaws.com region: us-east-1 compactor: compaction: block_retention: 720h # 30 day ``` ## ingest 应用 OTEL SDK 发 trace 到 Tempo (or otel-collector → Tempo): ```python from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter exporter = OTLPSpanExporter(endpoint='tempo:4317') ``` 或通过 otel-collector 中转(推荐生产)。 ## Grafana 查 Grafana 加 Tempo data source: ``` URL: http://tempo:3200 ``` Explore → Tempo → "Search" tab: - search by trace ID(直接 lookup) - search by service name + tag(TraceQL) - duration / status filter ```traceql {service.name="api" && duration > 500ms && status = error} ``` 返回 trace 列表 → 点开看 span tree。 ## TraceQL (Tempo query language) 类 PromQL for trace: ```traceql # 时间窗口内 service=api 的 trace {service.name="api"} # 慢 trace {duration > 1s} # 错误 trace {status = error} # 跨 span 关系 { name="GET /api/users" } >> { name="db.query" && duration > 100ms } # 父 span 是 "GET /api/users" + 子 span 含慢 DB query ``` 强大 + 不需要 fulltext index。 ## 跟 metric 关联 Grafana panel: ``` metric: rate(http_requests{status="500"}) ↓ click data point trace search: status=error around timestamp ↓ list of error trace IDs ``` 一键从 metric spike 跳到具体 trace。 debug 神器。 ## 跟 log 关联 log 里写 trace_id: ```python import logging from opentelemetry import trace handler = logging.StreamHandler() formatter = logging.Formatter( '%(asctime)s [%(levelname)s] [trace_id=%(otelTraceID)s] %(message)s' ) handler.setFormatter(formatter) ``` Loki 查 log: ```logql {service="api"} |= "error" # 看到 trace_id=abc123 ``` Grafana 自动识别 trace_id → 点击跳 Tempo 拉 trace。 ## 存储成本对比 | | Jaeger (ES) | Tempo (S3) | |---|---|---| | 1 TB trace/day | ~$2000/月(ES cluster) | ~$50/月(S3)| | query latency | < 100ms | ~1s(拉 S3) | | index 灵活 | 任 attr | trace ID + 部分 attr | Tempo cost 1-2 个量级低。 trade-off:query 慢(拉 S3 + scan)+ 索引弱。 ## sampling trace 量大 → 不全存。 两种 sampling: - **head sampling**:应用端决定(如 1%) - **tail sampling**:collector buffer 全 trace + 决定(如 100% error + 1% normal) tail 更智能但需 collector 资源(otel-collector tail_sampling processor)。 ## 与 Jaeger 对比 | | Jaeger | Tempo | |---|---|---| | 存储 | Cassandra / ES / Memory | S3 / GCS | | query 强 | 强(全索引) | 中(trace ID) | | 成本 | 高 | 低 | | 集成 | Jaeger UI | Grafana | | 部署 | 多组件 | 单 binary | Jaeger 适合:低 volume + 重 ad-hoc query。 Tempo 适合:高 volume + 接受 metric/log-driven trace lookup。 ## OpenTelemetry → 后端无关 只要应用用 OTEL SDK,后端可换: - Jaeger - Tempo - Honeycomb (SaaS) - Datadog APM (SaaS) - Splunk 代码不改。 ## 真实部署 我们 prod: - 100 微服务 + 10w QPS - 50 GB trace/day(10% sampling) - Tempo + S3 backend - 30 day retention - Grafana 主入口 成本: - S3:50 GB × 30 day × $0.023/GB = $35/月 - Tempo compute (2 small instance):$30/月 - 总:< $100/月 Jaeger + ES 等价:> $1000/月。 体验 trade-off: - query 1-3s(vs Jaeger < 200ms) - 必须知道 trace_id 或者跨服务 search(不能全文 search trace 内容) 可接受。 ## metrics generator (Tempo extra) Tempo 能从 trace 生成 metric: ```yaml metrics_generator: registry: external_labels: source: tempo processor: service_graphs: span_metrics: ``` 自动生成: - `traces_spanmetrics_calls_total{service, operation}` (call rate) - `traces_spanmetrics_latency_*` (latency histogram) - service graph (which service calls which) 替代 RED metric exporter,从 trace 推导。 ## 与 Datadog APM 对比 | | Tempo | Datadog APM | |---|---|---| | 成本 | $100/月 | $1000-10000/月 | | 部署 | self-host | SaaS | | UX | Grafana 中 | 极好 | | 集成 | OTEL + 自己 stack | Datadog 全生态 | 预算大 + 想 batteries-included → Datadog。 预算敏感 / 已有 Grafana → Tempo。 ## 踩过的坑 1. **S3 cost spike**:put request 计费,频繁小 trace 飞速 → 加 compactor 合并 block。 2. **query 超时**:跨大时间窗口 search → S3 拉量大 → timeout。 缩窗口或者 use trace ID lookup。 3. **OTEL 版本兼容**:Tempo 升级时 OTLP schema 变 → 接收旧 SDK trace 失败。pin SDK + Tempo 版本。 4. **head sampling 漏关键 trace**:随机 1% 漏掉了 error trace → 没 trace 可看。tail sampling 解决。 5. **trace too large**:单 trace 几千 span → UI 慢。控制 cardinality。
Flask 的微框架哲学意味着每个项目都要自己组装数据层。下面是一套 经过几个生产项目验证的最小骨架:Flask 3.x + SQLAlchemy 2.0 + Alembic 迁移 + 应用工厂模式。 ## 项目结构 ``` myapp/ ├── pyproject.toml ├── alembic.ini ├── migrations/ │ ├── env.py │ └── versions/ └── src/myapp/ ├── __init__.py # create_app() ├── extensions.py # db, ... ├── models.py ├── routes.py └── config.py ``` ## 1. 依赖 ```bash uv add flask 'sqlalchemy>=2.0' alembic psycopg[binary] python-dotenv ``` ## 2. extensions.py ```python from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass db = SQLAlchemy(model_class=Base) ``` SQLAlchemy 2.0 推荐用 `DeclarativeBase` 替代 `declarative_base()`, 类型提示更友好。 ## 3. models.py ```python from datetime import datetime from sqlalchemy import String, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from .extensions import db, Base class User(Base): __tablename__ = 'users' id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(String(120), unique=True) nickname: Mapped[str] = mapped_column(String(60)) created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) posts: Mapped[list['Post']] = relationship(back_populates='author') class Post(Base): __tablename__ = 'posts' id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(200)) body: Mapped[str] author_id: Mapped[int] = mapped_column(ForeignKey('users.id')) author: Mapped[User] = relationship(back_populates='posts') ``` `Mapped[...]` + `mapped_column(...)` 是 SA 2.0 的新风格, 完全类型化,mypy / pyright 直接懂。 ## 4. config.py ```python import os from dotenv import load_dotenv load_dotenv() class Config: SQLALCHEMY_DATABASE_URI = os.environ.get( 'DATABASE_URL', 'postgresql+psycopg://localhost/myapp') SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = os.environ['SECRET_KEY'] ``` ## 5. __init__.py(应用工厂) ```python from flask import Flask from .config import Config from .extensions import db def create_app(config_object=Config): app = Flask(__name__) app.config.from_object(config_object) db.init_app(app) from . import routes app.register_blueprint(routes.bp) return app ``` ## 6. routes.py ```python from flask import Blueprint, jsonify, request from sqlalchemy import select from .extensions import db from .models import User, Post bp = Blueprint('main', __name__) @bp.get('/users/<int:uid>/posts') def user_posts(uid): stmt = select(Post).where(Post.author_id == uid) posts = db.session.scalars(stmt).all() return jsonify([{'id': p.id, 'title': p.title} for p in posts]) @bp.post('/users') def create_user(): data = request.get_json() u = User(email=data['email'], nickname=data['nickname']) db.session.add(u) db.session.commit() return jsonify({'id': u.id}), 201 ``` SA 2.0 用 `select()` + `db.session.scalars()`;老的 `Model.query.filter_by(...)` 仍可用但官方建议迁移。 ## 7. Alembic 初始化 ```bash uv run alembic init -t async migrations # 或同步:uv run alembic init migrations ``` 改 `alembic.ini`: ```ini sqlalchemy.url = postgresql+psycopg://localhost/myapp ``` 改 `migrations/env.py`,让它能找到 metadata: ```python from myapp import create_app from myapp.extensions import db, Base # ... target_metadata = Base.metadata app = create_app() with app.app_context(): ... ``` ## 8. 第一次迁移 ```bash uv run alembic revision --autogenerate -m 'init users + posts' # 检查生成的 migrations/versions/*.py uv run alembic upgrade head ``` ## 9. 运行 ```bash uv run flask --app src.myapp run --debug # 或: uv run gunicorn 'src.myapp:create_app()' -b 0.0.0.0:8000 ``` ## 10. 测试 ```python # tests/conftest.py import pytest from src.myapp import create_app from src.myapp.extensions import db class TestConfig: SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' SECRET_KEY = 'test' @pytest.fixture def app(): app = create_app(TestConfig) with app.app_context(): db.create_all() yield app @pytest.fixture def client(app): return app.test_client() ``` ## 踩过的坑 - Alembic autogenerate 看不到 model:检查 `env.py` 是否真的 import 了 所有 model 模块(光 `import models` 子模块不自动加载)。我们一般写 `from myapp import models # noqa`。 - 加字段时 default 是 Python 端的,DB 端不会自动加 default。 改 model 后跑 autogenerate 会生成 `op.add_column` 但 nullable 字段 没初值时升级会失败。手动给 server_default 或者分两步迁移。 - SA 2.0 的 lazy loading 默认严格:N+1 查询会触发警告。生产建议 `select(...).options(selectinload(Post.author))` 显式预取。 - Flask-SQLAlchemy 3.x 配 SA 2.0 时,model 必须用 `db.Model` 或自己定义 的 `Base`,不能混用。
## 起因 数据团队常见需求: - 给 PM / 业务做个交互式 dashboard - 给一个 ML model 一个 demo UI - 内部工具(查 user / 跑 cohort 分析) 完整前端(React + API)几天工作量。 **streamlit** / **gradio**:纯 Python 描述 UI,5 分钟出能用 app。 ## streamlit ```python # app.py import streamlit as st import pandas as pd st.title('Sales Dashboard') uploaded = st.file_uploader('上传 CSV', type='csv') if uploaded: df = pd.read_csv(uploaded) st.dataframe(df) country = st.selectbox('国家', df.country.unique()) filtered = df[df.country == country] st.bar_chart(filtered.groupby('product').amount.sum()) st.metric('总销售', f"${filtered.amount.sum():,.0f}") ``` ```bash streamlit run app.py # 浏览器自动开 localhost:8501 ``` 整个 app 一个 .py,自顶向下 imperative。 每次 widget 交互 → script 从头重跑(reactive)。 ## gradio ```python # app.py import gradio as gr def predict(image): # 跑 model return label, score demo = gr.Interface( fn=predict, inputs=gr.Image(), outputs=[gr.Label(), gr.Number()], title='Image Classifier', ) demo.launch() ``` gradio 更 function-centric:定义 input/output → 自动 wrap UI。 ## 两个的定位 - **streamlit**:通用 dashboard / 内部工具(多个 widget 交互) - **gradio**:ML model demo(input → 跑 → output) streamlit 是 layout + state-aware app;gradio 是 function demo wrapper。 ## streamlit 细节 ### session state ```python if 'count' not in st.session_state: st.session_state.count = 0 if st.button('+1'): st.session_state.count += 1 st.write(f'count: {st.session_state.count}') ``` 每次重跑 script,session_state 持久(同浏览器 session)。 ### cache ```python @st.cache_data def load_data(path): return pd.read_csv(path) # 慢操作 df = load_data('big.csv') # 重 invocation cached ``` `@st.cache_data` 内容缓存;`@st.cache_resource` 资源(model)缓存。 不 cache 的话每次按 widget 都重读 CSV → 慢。 ### 多页面 ``` my_app/ ├── app.py # 主页 ├── pages/ │ ├── 1_📊_Dashboard.py │ ├── 2_🔍_Search.py │ └── 3_⚙️_Settings.py ``` `streamlit run app.py` 左侧自动有 page 切换。 ### chart 库 streamlit 内置: - `st.line_chart`, `st.bar_chart`, `st.area_chart`(轻量) - `st.pyplot()` matplotlib - `st.plotly_chart()` plotly - `st.altair_chart()` altair - `st.vega_lite_chart()` 任意 chart 都能塞。 ## gradio 细节 ### 复合 input/output ```python demo = gr.Interface( fn=lambda txt, slider: txt * slider, inputs=[ gr.Textbox(label='文本'), gr.Slider(1, 10, step=1), ], outputs=gr.Textbox(), ) ``` ### Blocks (复杂布局) ```python with gr.Blocks() as demo: gr.Markdown('# My App') with gr.Row(): with gr.Column(): inp = gr.Textbox() btn = gr.Button('Run') with gr.Column(): out = gr.Textbox() btn.click(fn=process, inputs=inp, outputs=out) demo.launch() ``` Blocks 更接近 streamlit 灵活度。 ### chat interface ```python def respond(message, history): return f"echo: {message}" gr.ChatInterface(respond).launch() ``` 3 行起 LLM chat UI。huggingface space 上 90% chat demo 用 gradio。 ## 性能 / scale - streamlit:每个 user 连接独立 session,但跑同一进程;重 compute 会卡其它 user - gradio:queue 系统,多个 request 排队跑 LLM demo 用 gradio(queue 默认);多 user dashboard 用 streamlit。 ## 部署 ### streamlit cloud / hugging face streamlit cloud free tier: - 连 GitHub repo - 推 → 自动部署到 streamlit.app domain hugging face space: - 同样思路,免费 CPU - gradio / streamlit 都支持 ### 自托管 ```dockerfile FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt EXPOSE 8501 CMD streamlit run app.py --server.address 0.0.0.0 ``` 放 nginx 反代 + auth → 内部工具。 ## 鉴权 两者都没原生用户系统。 方案: - nginx + basic auth - Cloudflare Access(zero-trust) - streamlit-authenticator package - OAuth proxy (oauth2-proxy) 内部工具我用 Cloudflare Access:5 分钟配,免维护。 ## 与 dash / panel 对比 - **Dash**(Plotly):基于 Flask + React,更灵活但写得多 - **Panel**(HoloViz):科学计算友好,多 backend - **Reflex**(前 Pynecone):写 Python 编 React,UI 强 streamlit / gradio 简单优先;dash / reflex 复杂应用。 ## 我的选择 - **数据 dashboard** → streamlit - **ML 模型 demo** → gradio - **内部 admin tool** → streamlit - **需要复杂前端** → 接 SPA + FastAPI ## case:客户演示工具 要给客户演示一个文本摘要 LLM: ```python import gradio as gr from transformers import pipeline summarizer = pipeline('summarization') def summarize(text): return summarizer(text, max_length=100)[0]['summary_text'] gr.Interface( fn=summarize, inputs=gr.Textbox(lines=10, placeholder='粘贴长文'), outputs=gr.Textbox(label='摘要'), title='LLM 摘要 Demo', examples=[['Long text 1...'], ['Long text 2...']], ).launch(share=True) # share=True 给临时公网 URL(gradio.live) ``` 30 秒部署 + URL 发给客户 + 客户能直接试。比 PPT 强 100 倍。 `share=True` 临时 tunnel 72 小时有效。 ## 内部 case:cohort 分析工具 ```python import streamlit as st import duckdb st.title('User Cohort Analysis') date_range = st.date_input('日期范围', value=(start, end)) group_by = st.selectbox('Group by', ['country', 'plan', 'source']) @st.cache_data def query(date_range, group_by): return duckdb.sql(f""" SELECT {group_by}, DATE_TRUNC('week', signup_date) AS cohort, COUNT(*) AS users FROM read_parquet('s3://.../users/*.parquet') WHERE signup_date BETWEEN '{date_range[0]}' AND '{date_range[1]}' GROUP BY 1, 2 """).df() df = query(date_range, group_by) st.plotly_chart(px.line(df, x='cohort', y='users', color=group_by)) st.dataframe(df) ``` 业务自己改 dropdown 看不同维度。 原本 BA 找数据团队跑 → 改成业务自助。 ## 踩过的坑 1. **state 重置**:streamlit 每次交互重跑 script。耗时 op 没 cache → 卡。 2. **gradio queue 默认关**:高 concurrent 时阻塞。`demo.queue()` 打开。 3. **streamlit 多 tab**:同 user 多 tab → state 不共享。 `st.session_state` 是单 session 单 tab。 4. **share=True 安全**:gradio share 链接公网,没 auth。给 demo 用, 不要放 secret data。 5. **upload size 限制**:streamlit 默认 200 MB;要更大改 `--server.maxUploadSize=1000`。
## 起因 第一次做 RAG 时按"每 500 字符一刀"切了 1 万份文档进向量库, 召回出来的 chunk 经常是半句话开头、半句话结尾。LLM 看着这种残缺片段 回答经常张冠李戴:"根据文档 ...(被截断)"。 解决回答质量问题,70% 在切块上下功夫,30% 在检索算法。 ## 三种切块策略 ### A. 固定长度(最简单) ```python def chunk_fixed(text, size=500, overlap=50): chunks = [] for i in range(0, len(text), size - overlap): chunks.append(text[i:i+size]) return chunks ``` `overlap` 让相邻 chunk 有 50 字符重叠,避免句子被切两半时只有半句进 任一 chunk。优点:实现简单,长度均匀;缺点:仍可能在标点 / 段落中间截断。 ### B. 语义边界(推荐) 按段落、句子、Markdown 标题切。LangChain 的 `RecursiveCharacterTextSplitter` 最常用: ```python from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ". ", " ", ""], ) chunks = splitter.split_text(text) ``` `separators` 顺序很重要:先按双换行切(段落),不够再按单换行(行), 还不够按句号,最后才硬切。中文要加 `"。"`、`"!"`、`"?"`, 英文版本默认有 `"."`。 代码 / 表格 / Markdown 文档: ```python from langchain_text_splitters import MarkdownHeaderTextSplitter splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")], ) chunks = splitter.split_text(md_text) # 每 chunk 自动带 metadata { h1: "...", h2: "..." } ``` 按 Markdown 标题切的好处:chunk 自带"它属于哪一节"的元数据,可以在 prompt 里附带给 LLM 当上下文。 ### C. 父子层级(parent-child retrieval) 切两份: - **小 chunk**(200-300 字符)用于 embedding + 检索(细颗粒,语义匹配准) - **大 chunk**(1000-2000 字符)用于喂给 LLM(提供完整上下文) 存储时小 chunk 通过 metadata 指向所属大 chunk: ```python from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000) child_splitter = RecursiveCharacterTextSplitter(chunk_size=400) retriever = ParentDocumentRetriever( vectorstore=Chroma(embedding=emb), docstore=InMemoryStore(), child_splitter=child_splitter, parent_splitter=parent_splitter, ) retriever.add_documents(docs) # 查询时:小 chunk 匹配 → 返回对应大 chunk chunks = retriever.invoke('问题') ``` ## 效果对比 我在一个内部知识库(5000 篇文档)做过 A/B: | 策略 | 检索准确率 | LLM 回答完整度 | 实现复杂度 | |---|---|---|---| | 固定长度 | 62% | 70%(多被截断) | 极简 | | 语义边界 | 78% | 88% | 简单 | | 父子层级 | 81% | 94% | 中 | **结论**:起步用语义边界(RecursiveCharacterTextSplitter),效果不够好 再升级到父子。固定长度只在快速 POC 时用。 ## 其它要诀 ### 1. chunk size 不是越小越好 300 字符的 chunk 召回时上下文太少;2000 字符的 chunk embedding 时 语义被"稀释",匹配不准。**400-800 字符是甜点**(英文)/ **200-400 中文字符** (中文每字承载语义比英文 token 多)。 ### 2. metadata 一起存 ```python chunk = { 'text': '...', 'metadata': { 'source': 'manual.md', 'section': 'Installation', 'date': '2024-01-15', } } ``` 检索时可以 filter(只查"最近 30 天的内容"),rerank 时可以加权。 ### 3. 长文档加 summary chunk 文档开头加一个"全文摘要" chunk(用 LLM 生成)作为 top-level entry。 检索时 summary chunk 匹配 → 暗示整篇文档相关 → 扩展取相邻 chunk。 ## 踩过的坑 1. **Tokenizer 不匹配**:embedding model 是按 token 计长,但我按字符切 500,可能实际是 800-1200 token,超过 model 上限被截。改成 `from_huggingface_tokenizer` 按真实 token 切: ```python splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer( tokenizer, chunk_size=200, chunk_overlap=20) ``` 2. **PDF 提取文字时表格变乱**:纯文本提取(pdfplumber)保留不了表格结构。 表格密集的 PDF 用 `unstructured` 库专门处理表格,或者 PDF → HTML → 切。 3. **重复内容污染检索**:每篇文档都有相同的 "本文档版权所有…" 页脚 → embedding 集中在这个 footer,检索结果偏向有 footer 的文档。 切之前先 strip 这些 boilerplate。 4. **多语言文档**:中英文混排时 RecursiveCharacterTextSplitter 默认 separators 不包含中文标点。手动加 `"。"`、`"!"`、`"?"`、`";"`。
## 起因 团队 / 个人需要访问内部服务: - 内网 dev server / Postgres / 监控仪表盘 - 多机 ssh 不暴露公网 - 跨地点协作(家 / 办公室 / 出差) 老方案: - 开 22 端口公网 + fail2ban:被扫被打 - OpenVPN:配置复杂、性能一般 - ssh tunnel + jump host:能用但碎 **WireGuard**:现代 VPN 协议,几百行 C 代码,内核态,10x OpenVPN 性能。 **Tailscale**:WireGuard 之上的 mesh VPN,自动 NAT 穿透 + 身份认证。 我家里 / 公司 / 客户机器 30+ 节点全 Tailscale,5 分钟搭好。 ## Tailscale 用法 ```bash # Mac brew install tailscale # Linux curl -fsSL https://tailscale.com/install.sh | sh # 启动 + 登录(浏览器 OAuth) sudo tailscale up # 看节点 tailscale status ``` 每台机器装好登录同账号 → 自动加入私有 mesh。 每节点拿个 `100.x.y.z` 内网 IP(CGNAT range)。 跨机器: ```bash ssh [email protected] # 用 tailscale IP ssh user@my-laptop # 或者用 hostname (MagicDNS) ``` ## MagicDNS ```bash tailscale set --accept-dns # 启用 MagicDNS ``` 每节点名(hostname)自动解析: ```bash ssh my-laptop # 等价 100.64.1.2 curl http://my-server:8080 ``` 不用记 IP。 ## 节点 expose ```bash # 在内网 server 上 tailscale up --advertise-routes=10.0.0.0/24 ``` 让 tailscale network 能访问那台机器背后的整个子网(subnet router)。 家里 NAS / 路由器 / 旧设备一并访问。 ## ACL Tailscale admin 控制台配 ACL(json): ```json { "acls": [ { "action": "accept", "src": ["group:admin"], "dst": ["*:*"] }, { "action": "accept", "src": ["group:dev"], "dst": ["tag:dev-server:*"] }, ], "groups": { "group:admin": ["[email protected]"], "group:dev": ["[email protected]", "[email protected]"], }, "tagOwners": { "tag:dev-server": ["group:admin"], }, } ``` dev 组只能访问 dev-server tag 节点,admin 能访问全部。 基于身份的 zero-trust 网络。 ## SSH ```bash # 启用 Tailscale SSH(替代 OpenSSH) sudo tailscale up --ssh ``` `tailscale ssh user@host` 用 tailscale 身份做 SSH key 替代。 忘记管 SSH key + ACL 一处控制。 ## funnel(公网暴露) ```bash tailscale serve --https=443 localhost:3000 # 内部用 tailscale funnel --https=443 localhost:3000 # 公网暴露(HTTPS 自动) ``` `funnel` 让任意 tailscale URL 公网可达,自动 HTTPS(Let's Encrypt)。 不开 router 端口 + 不要 cloudflare 中转就把本地服务对公网。 ## WireGuard 原生(不用 Tailscale) 更轻量但要自己 NAT 穿透 / key 分发。 ```bash sudo apt install wireguard # 生成 key wg genkey | tee privatekey | wg pubkey > publickey ``` server `wg0.conf`: ```ini [Interface] PrivateKey = <server-priv> Address = 10.0.0.1/24 ListenPort = 51820 [Peer] PublicKey = <client-pub> AllowedIPs = 10.0.0.2/32 ``` client: ```ini [Interface] PrivateKey = <client-priv> Address = 10.0.0.2/24 [Peer] PublicKey = <server-pub> Endpoint = vpn.example.com:51820 AllowedIPs = 10.0.0.0/24 PersistentKeepalive = 25 ``` 启动: ```bash sudo wg-quick up wg0 ``` 简单可控但**没 NAT 穿透**(client 直连 server 端口必须可达)。 mesh 不行(每节点连每节点 → key 矩阵)。 ## Tailscale 是 WireGuard 之上的什么 - key 分发自动化 - DERP 中继服务(双 NAT 时 fallback) - 身份系统(OAuth + ACL) - MagicDNS - admin UI WireGuard 协议是 Tailscale 的 data plane。 ## 性能 iperf3 测试,两台 1 Gbps 机器: - 直连:940 Mbps - WireGuard:920 Mbps (~2% overhead) - Tailscale:900 Mbps (NAT 穿透成功) - Tailscale (DERP 中继):100-300 Mbps NAT 友好时性能几乎无损。复杂 NAT fallback DERP 会慢。 ## Headscale(开源 Tailscale 控制面) Tailscale 客户端开源 + 协议公开,但控制面是 SaaS。 担心 vendor lock-in / 完全自托管 → Headscale: ```bash docker run -d -p 8080:8080 \ -v ./config.yaml:/etc/headscale/config.yaml \ headscale/headscale serve ``` 自己跑控制面,Tailscale 客户端连。 免费 + 完全控制。 ## 与 NetMaker / ZeroTier 对比 - **ZeroTier**:跟 Tailscale 类似 mesh VPN,更老,OG - **NetMaker**:开源,self-host,WireGuard 之上 - **Nebula**(Slack 出):CA 模型,性能极好 我用 Tailscale: - 易用性最好 - 个人免费 tier 100 device - ACL / SSH / funnel 集成 ## 实战 case:跨地点开发 我家、办公室、客户公司、3 个云服务器(DigitalOcean / Hetzner / AWS) 全 Tailscale: - ssh 任意机器:`ssh alice@office-server` - 内网 DB 直连:`psql -h 100.x.y.z` - 客户给我个 server 加入 group → 我能 ssh,3 个月后他 revoke - 出差咖啡店 wifi → 一样工作 - 公网 0 开放(除了 80/443 给 web) 安全 + 方便 + 0 复杂网络配置。 ## 与 Cloudflare Zero Trust 对比 Cloudflare Access (Zero Trust): - 主要给 HTTP 服务(不是 IP-level mesh) - 优势:浏览器原生 + SSO 集成 - 劣势:每协议要单独配 / 不能 ssh Tailscale 是 network layer(all IP traffic)。 Cloudflare Access 是 application layer。 我两个都用: - ssh / 内网服务 → Tailscale - 内部 web app → Cloudflare Access (OIDC) ## 踩过的坑 1. **subnet router 不工作**:`--advertise-routes` 后 admin console 要 approve route。 2. **double NAT 性能慢**:`tailscale netcheck` 看 NAT 类型。CGNAT 背后 fallback DERP,慢。 3. **MagicDNS 冲突**:本机 /etc/hosts 已经有 hostname → 解析错。 `tailscale set --accept-dns=false` 或者删 /etc/hosts。 4. **WireGuard MTU**:跨 VPN 隧道 packet MTU 减小,应用大 packet 慢。 `MTU = 1280` 或者调。 5. **退订机器忘删**:员工离职后 device 还在 admin console。定期清理 + ACL 跟着员工 group 自动。
Redux Toolkit 适合大型项目,对小项目却有大量样板。Zustand 是 Pmndrs(与 Three.js / Jotai 同作者)的极简方案,2024 后越来越流行。 ## 安装 ```bash npm i zustand ``` ## 最小 store ```ts // stores/counter.ts import { create } from 'zustand' interface State { count: number inc: () => void reset: () => void } export const useCounter = create<State>((set) => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })), reset: () => set({ count: 0 }), })) ``` ```tsx function Counter() { const count = useCounter(s => s.count) const inc = useCounter(s => s.inc) return <button onClick={inc}>{count}</button> } ``` 无 Provider、无 reducer、无 action type。selector 写法天然做了 memoization (只有 selector 返回值变了才重渲染)。 ## persist middleware ```ts import { persist, createJSONStorage } from 'zustand/middleware' export const useSettings = create( persist<Settings>( (set) => ({ theme: 'light', setTheme: (t) => set({ theme: t }), }), { name: 'settings', storage: createJSONStorage(() => localStorage), version: 1, } ) ) ``` 刷新页面状态自动还原。 ## immer middleware(写嵌套结构方便) ```ts import { immer } from 'zustand/middleware/immer' const useTodos = create<TodoState>()( immer((set) => ({ todos: [], addTodo: (text) => set(s => { s.todos.push({ id: Date.now(), text }) }), toggle: (id) => set(s => { const t = s.todos.find(t => t.id === id) if (t) t.done = !t.done }), })) ) ``` 直接 mutate(看起来),immer 内部生成 immutable 更新。 ## 多 store vs 单 store Zustand 鼓励"多个小 store",每个独立: ```ts useAuth() // 登录状态 useTheme() // 主题 useTodos() // 待办列表 ``` 而不是 Redux 那种"一个 root reducer"。 小 store 更容易代码分割 / 测试 / 删除。 ## 异步 action ```ts const useUser = create<UserState>((set) => ({ user: null, loading: false, async fetchUser(id: string) { set({ loading: true }) try { const r = await fetch(`/api/users/${id}`) set({ user: await r.json(), loading: false }) } catch (e) { set({ loading: false }) } }, })) ``` 异步逻辑就是普通 async 函数,无需 thunk / saga。 ## 选择多个字段时避免 re-render ```tsx // ❌ 每次新对象 → 总是重渲 const { name, email } = useUser(s => ({ name: s.name, email: s.email })) // ✅ 用 shallow 比较 import { shallow } from 'zustand/shallow' const { name, email } = useUser(s => ({ name: s.name, email: s.email }), shallow) // 或者分别 select const name = useUser(s => s.name) const email = useUser(s => s.email) ``` ## getState / setState(store 之外用) ```ts useAuth.getState().logout() useAuth.setState({ user: null }) // 订阅外部 const unsub = useAuth.subscribe( (state) => state.user, (user) => console.log('user changed', user) ) ``` 适合在路由 hook、worker 等非 React 上下文里用。 ## Devtools ```ts import { devtools } from 'zustand/middleware' const useStore = create(devtools<State>( (set) => ({ ... }), { name: 'MyStore' } )) ``` Redux DevTools 扩展能看到时间线 + state diff。 ## 与 React Server Components Zustand 是 client-side 状态。RSC 里直接用 fetch;只在交互组件("use client") 里用 store。 ## 测试 ```ts import { act } from 'react' it('inc works', () => { const { inc, count } = useCounter.getState() expect(count).toBe(0) act(() => inc()) expect(useCounter.getState().count).toBe(1) }) // 测后重置: beforeEach(() => useCounter.setState({ count: 0 })) ``` ## vs Jotai / Recoil - **Zustand**:单值存储 + selector,类似 mini Redux - **Jotai**:atomic state,类似 SolidJS signal - **Recoil**:Facebook 出的 atom 风格,已停止维护 中型项目用 Zustand 最实用;大量原子化派生状态用 Jotai。 ## 踩过的坑 - selector 返回新对象 → 总是重渲。要么 shallow 要么拆字段 select。 - store 闭包了旧值:set 用函数形式 `set(s => ...)` 而不是 `set({})`。 - store 不要循环依赖(A store 里 select B store)。把跨 store 派生 搬到组件层。 - persist 跨大版本:手动写 migrate 函数;不写则旧 state 直接覆盖 默认值有可能丢字段。
## 起因 装了 Prometheus + Grafana 后能看图了,但"半夜 3 点磁盘满了"还得 有人盯仪表盘才发现。要把"触发某指标条件"自动转成 push 告警。 Alertmanager 是 Prometheus 生态官方告警分发组件。 ## 解决方案 ### 1. 写告警规则(Prometheus 端) `/etc/prometheus/rules/node.yml`: ```yaml groups: - name: node-alerts interval: 30s rules: - alert: NodeDown expr: up{job="node"} == 0 for: 2m labels: severity: critical team: ops annotations: summary: '节点 {{ $labels.instance }} 离线' description: 'Prometheus 已经 2 分钟无法 scrape {{ $labels.instance }}' - alert: HighCPU expr: 100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 > 85 for: 10m labels: severity: warning annotations: summary: '{{ $labels.instance }} CPU > 85% 持续 10 分钟' - alert: DiskAlmostFull expr: 100 - node_filesystem_avail_bytes{mountpoint="/"} * 100 / node_filesystem_size_bytes{mountpoint="/"} > 90 for: 5m labels: severity: critical annotations: summary: '{{ $labels.instance }} 根分区 > 90%' description: '目前 {{ $value | humanizePercentage }}' - alert: MemoryPressure expr: 100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 92 for: 15m labels: severity: warning ``` 关键点: - `expr`:PromQL 表达式,结果非空时触发 - `for`:持续多久才真触发(避免瞬间抖动告警) - `labels`:可以路由用(severity / team) - `annotations`:人读的内容,模板支持 `{{ $labels.x }}` 和 `{{ $value }}` `prometheus.yml` 引入: ```yaml rule_files: - /etc/prometheus/rules/*.yml alerting: alertmanagers: - static_configs: - targets: ['localhost:9093'] ``` reload Prometheus: ```bash curl -X POST http://localhost:9090/-/reload ``` UI 看告警状态:http://prom:9090/alerts,三种状态:Inactive / Pending / Firing。 ### 2. 装 Alertmanager ```bash curl -fsSL https://github.com/prometheus/alertmanager/releases/latest/download/alertmanager-0.27.0.linux-amd64.tar.gz \ | sudo tar xz -C /opt/ sudo ln -s /opt/alertmanager-0.27.0.linux-amd64 /opt/alertmanager sudo useradd -rs /bin/false alertmanager sudo mkdir -p /var/lib/alertmanager sudo chown -R alertmanager:alertmanager /var/lib/alertmanager ``` ### 3. 配置 Alertmanager `/etc/alertmanager/alertmanager.yml`: ```yaml global: resolve_timeout: 5m smtp_smarthost: 'smtp.example.com:587' smtp_from: '[email protected]' smtp_auth_username: 'notifier' smtp_auth_password: '...' # 路由树 route: receiver: default group_by: ['alertname', 'instance'] group_wait: 30s # 第一条告警等 30s 看有没有同组的 group_interval: 5m # 同组下一批告警最少间隔 repeat_interval: 4h # 同告警重复发的最短间隔 routes: - matchers: - severity = critical receiver: pagerduty group_wait: 10s repeat_interval: 1h - matchers: - severity = warning - team = ops receiver: slack-ops inhibit_rules: - source_matchers: [severity="critical"] target_matchers: [severity="warning"] equal: ['instance'] # 同一台机器既有 critical 又有 warning,warning 被抑制(避免噪音) receivers: - name: default email_configs: - to: '[email protected]' - name: slack-ops slack_configs: - api_url: 'https://hooks.slack.com/services/...' channel: '#alerts-ops' title: '{{ .CommonAnnotations.summary }}' text: | {{ range .Alerts }} *{{ .Annotations.summary }}* {{ .Annotations.description }} severity: {{ .Labels.severity }} {{ end }} - name: pagerduty pagerduty_configs: - service_key: '<PD key>' - name: dingtalk webhook_configs: - url: 'https://oapi.dingtalk.com/robot/send?access_token=...' send_resolved: true ``` 钉钉 webhook 需要[特定 JSON 格式](https://open.dingtalk.com/document/orgapp-server/custom-robot-access), 通常需要中间 adapter(`prometheus-webhook-dingtalk`)转换。 systemd unit: ```ini [Service] User=alertmanager ExecStart=/opt/alertmanager/alertmanager \ --config.file=/etc/alertmanager/alertmanager.yml \ --storage.path=/var/lib/alertmanager Restart=on-failure ``` ```bash sudo systemctl enable --now alertmanager ``` ### 4. 测试 手动触发一条假告警: ```bash curl -XPOST http://alertmanager:9093/api/v2/alerts -d ' [{ "labels": {"alertname": "TestAlert", "severity": "critical", "instance": "test"}, "annotations": {"summary": "测试告警"}, "startsAt": "2026-05-24T10:00:00Z" }]' ``` 应该几秒内收到 Slack / 邮件。 ### 5. 抑制 + 静默 ```bash # 系统维护期间静默 amtool silence add alertname=NodeDown instance=app1.example.com \ --duration=2h --comment 'planned maintenance' amtool silence query amtool silence expire <id> ``` 或 Alertmanager UI(端口 9093)有 silence 表单。 ## 效果 - 告警从"刷仪表盘" → "手机 push" - 通过 group_by + group_interval 把"集群批量告警"合并成几条消息, 不再被刷屏 - inhibit 让"机器挂了之后机器上每个服务都告警"自动只剩一条 NodeDown - on-call 工程师 MTTR 从 30 分钟(发现 + 上线诊断)降到 5 分钟 ## 几个最佳实践 1. **`for: 5m` 不要省**:磁盘 90% 持续 5 分钟才真告警,避免 cron 任务 瞬间冲高引起假警 2. **每条 alert 配 runbook 链接**:annotation 加 `runbook_url`,告警 消息里点击直达"出现 X 怎么处理"文档 3. **不要告警一切**:CPU 80% 不需要立刻人工干预,写到 daily report 就好。半夜 page 应当是"现在不处理业务挂"级别 4. **`repeat_interval: 4h` 平衡**:太短刷屏;太长重要告警睡过 5. **定期 review alert noise**:跑 `amtool alert query` 看哪些告警 反复 firing 没人理 → 要么调阈值要么删 ## 踩过的坑 1. **rule reload 没生效**:Prometheus reload `/-/reload` 端点默认禁用, 要 `--web.enable-lifecycle` 启动。 2. **告警时间戳错乱**:Prometheus / Alertmanager 时区不一致 → UI 显示 告警是几年前的。两端都 UTC。 3. **expr 写错没语法报错**:Prometheus 接受语法对但语义错的表达式 (如 `metric > 100` 而 metric 单位是 GB),跑出来永远空。在 Prometheus UI Graph 选项卡先跑 expr 看结果再写规则。 4. **Slack webhook URL 进 git**:泄露了被人乱发消息。放 env / secret 或 alertmanager 的 `file:` 引用: ```yaml api_url_file: /etc/alertmanager/slack_url ``` 5. **inhibit_rules 的 `equal` 字段不一致**:source 和 target 没共同 label 时 inhibit 不生效。仔细 check label 列表。
`rg` 是 ripgrep 的命令行工具。比 `grep -r` 快一个数量级(Rust + SIMD), 默认行为更符合代码库搜索的预期:跳过 `.git`、尊重 `.gitignore`、 跳过二进制文件、自动多线程。 ## 安装 ```bash sudo apt install -y ripgrep # Debian 11+ / Ubuntu 20.04+ brew install ripgrep # macOS choco install ripgrep # Windows rg --version ``` ## 最基础 ```bash rg pattern # 在当前目录递归搜 pattern rg pattern src/ # 在 src/ 里搜 rg -i pattern # 大小写不敏感 rg -w word # 整词匹配 rg -v 'never' # 反向(不含的行) rg -F 'a.b.c' # 字面字符串(不当 regex) ``` 输出格式: ``` src/utils/string.py 12: def normalize(s: str) -> str: 14: return s.strip().lower() ``` 文件名 + 行号 + 高亮。 ## 文件类型筛选 ```bash rg pattern -t py # 只在 Python 文件搜 rg pattern -T md # 排除 Markdown rg pattern -g '*.tsx' # glob 匹配 rg pattern -g '!**/node_modules/**' # glob 排除 rg --type-list # 列所有已知文件类型 ``` `-t`、`-T` 让命令行干净得多——`rg -t py error` vs `grep -r --include='*.py' error .`。 ## 显示前后行(context) ```bash rg pattern -A 3 # after 3 行 rg pattern -B 3 # before 3 行 rg pattern -C 3 # context 前后各 3 行 ``` 跟 grep 完全一样的参数。 ## 只列文件名 ```bash rg -l pattern # 只列匹配的文件名 rg --files-without-match pattern # 反过来,列不匹配的文件 ``` `-l` 配合 fzf / xargs 极常用: ```bash # 编辑所有匹配的文件 vim $(rg -l 'TODO') ``` ## 替换(dry-run + 实际改) ```bash # dry-run:只显示替换效果,不写入 rg 'old_name' --replace 'new_name' # 真的批量替换:rg 找出 + sed 改 rg -l 'old_name' | xargs sed -i 's/old_name/new_name/g' # macOS 上 sed -i '' '...' ``` 或者用 `sd`(rust 写的 sed 替代): ```bash rg -l 'old_name' | xargs sd 'old_name' 'new_name' ``` ## 输出 JSON(pipeline 友好) ```bash rg --json pattern src/ | head # {"type":"begin","data":{"path":{"text":"src/foo.py"}}} # {"type":"match","data":{"path":...,"lines":...,"line_number":12,...}} # {"type":"end",...} ``` 让别的工具处理结构化输出。 ## 通用预设:~/.ripgreprc ``` # ~/.ripgreprc --smart-case --max-columns=200 --colors=line:fg:yellow --colors=path:fg:blue --type-add=web:*.{html,css,js,jsx,ts,tsx,vue} --type-add=md:*.{md,markdown,mdx} ``` ```bash echo 'export RIPGREP_CONFIG_PATH=~/.ripgreprc' >> ~/.bashrc ``` 之后 `rg pattern` 自动用这套默认。 ## 与 grep 对比 | 任务 | grep | rg | |---|---|---| | 在仓库找 `TODO` | `grep -r --include='*.py' TODO .` | `rg -t py TODO` | | 跳过 .git / node_modules | 要手动 `--exclude-dir` | 自动 | | 二进制文件 | 默认搜(输出乱码) | 自动跳过 | | 速度(100k LOC) | 几秒 | < 0.5s | | 默认大小写 | 区分 | smart-case (有大写=区分) | `grep` 仍然是 POSIX 工具,远程小机器没 rg 时用。日常本地 / CI 用 rg。 ## 与 fzf 联用 ```bash # 模糊搜代码内容 rg . --line-number --no-heading --color=always \ | fzf --ansi --delimiter=':' \ --preview 'bat --color=always {1} --highlight-line {2}' ``` 写成函数(前面 fzf 那篇有提到)。 ## VSCode 内部用了它 VSCode 的全局搜索(Ctrl-Shift-F)底层就是 ripgrep。任何用 rg 直接命令行的 都和 IDE 搜索一致体验。 ## 性能:为什么这么快 - 用 Rust 写 + Aho-Corasick / RE2 / SIMD - 多线程:每个文件并行搜 - mmap:大文件 zero-copy - gitignore 解析:跳过整个子树而不是访问每个文件 - 二进制检测:发现 NUL 字节立即跳过整个文件 ## 边界 / 别用 rg 的场合 - 单文件的复杂正则:`grep -P` 的 PCRE 比 rg 默认的 Rust regex 更全 (lookbehind 等)。rg 加 `-P` 也能用 PCRE2,但需要 build 时打开特性。 - 流式(stdin)+ 极简:`grep` 在 docker shell / 嵌入式环境永远可用。 - 颜色输出走 pipeline:rg 默认看到 stdout 不是 tty 就关闭颜色, 和别的工具兼容。 ## 踩过的坑 - 想搜 `.git` 里的东西时 rg 默认跳过 → 加 `-uu`("unrestricted" 二次: 搜隐藏 + 不尊重 .gitignore + 二进制)。 - `.gitignore` 有 `*.log` 时 rg 不会搜日志文件 —— 通常正确,但调试时 忘了为什么没结果。`rg -u pattern` 一次性绕过 ignore 规则。 - 重定向到文件后颜色没了:`rg --color=always pattern | less -R` 强制保留颜色。 - 中文 / 非 ASCII 搜索:rg 默认 UTF-8 兼容;老 BOM / GBK 文件需要 `--encoding gbk`。
## 起因 每天调几十次 curl 测 API,命令越写越长: ```bash curl -X POST https://api.example.com/v1/users \ -H 'Authorization: Bearer xxx' \ -H 'Content-Type: application/json' \ -d '{"email":"[email protected]","name":"Alice","age":30}' \ -i -v -s \ | jq . ``` curl 强大但 verbose。下面几个现代工具针对"开发期 API 调试"做了大幅 简化。 ## httpie:人友好的 curl 替代 ```bash brew install httpie # 或 apt install httpie / pipx install httpie http --version ``` ### 用法 ```bash # GET http https://api.example.com/users/1 # 自动 pretty-print JSON + 语法高亮 # POST + JSON body http POST https://api.example.com/users \ [email protected] name=Alice age:=30 # - key=value 是字符串 # - key:=value 是 number/bool/null 等 (JSON 类型) # - 自动加 Content-Type: application/json # Authorization http https://api.example.com/me Authorization:'Bearer xxx' # 或: http -A bearer -a xxx https://api.example.com/me # 上传文件 http POST https://api.example.com/upload file@./photo.jpg # 看 request / response 头(默认显示 response 头) http -v POST ... # -v 把 request 也打印 http -h POST ... # 只 print headers http -p Hb POST ... # 控制哪些部分打印 (H=header, b=body) # session(保存 cookie + auth) http --session=alice POST https://api.example.com/login [email protected] http --session=alice https://api.example.com/me # 复用 cookies ``` 输出: ``` HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 24 May 2026 10:00:00 GMT Server: nginx { "id": 42, "name": "Alice", "email": "[email protected]" } ``` JSON 自动格式化 + 高亮,headers 区分颜色,整体一眼看清。 ## xh:Rust 重写的 httpie,启动更快 ```bash brew install xh # 或 cargo install xh xh --version ``` API 几乎完全跟 httpie 兼容: ```bash xh POST https://api.example.com/users [email protected] name=Alice ``` httpie 启动 ~150ms(Python),xh ~10ms(Rust)。频繁使用差异明显。 我个人现在用 xh > httpie > curl 这个偏好。 ## hurl:把 API 测试写成可重放脚本 ```bash brew install hurl ``` `.hurl` 文件: ``` # login.hurl POST https://api.example.com/login { "email": "[email protected]", "password": "secret" } HTTP 200 [Asserts] jsonpath "$.token" exists jsonpath "$.user.email" == "[email protected]" [Captures] token: jsonpath "$.token" GET https://api.example.com/me Authorization: Bearer {{token}} HTTP 200 [Asserts] jsonpath "$.email" == "[email protected]" POST https://api.example.com/posts Authorization: Bearer {{token}} { "title": "Hello", "body": "..." } HTTP 201 [Asserts] jsonpath "$.title" == "Hello" [Captures] post_id: jsonpath "$.id" GET https://api.example.com/posts/{{post_id}} HTTP 200 ``` 跑: ```bash hurl login.hurl # 全部 assertion 通过 = exit 0 # 否则 exit 非 0 # 输出更详细 hurl --verbose --variables-file vars.env login.hurl # 测试 mode hurl --test login.hurl ``` 适用: - E2E API 测试 + CI - API 回归验证(每次部署后自动跑) - 复制粘贴前端测试用的 request 序列 完整功能:query params、文件上传、cookies、多文件 chain、报告输出(JSON/JUnit/HTML)。 ## curl 仍然适合什么 curl 不会消失: - 服务器 minimal 环境(绝对默认安装) - 调试 TLS / HTTP 协议级(`-v --trace`) - script / Dockerfile / CI 里(无依赖、跨平台) - 二进制下载 / 长文件传输 httpie / xh 是"开发期人调用",curl 是"机器调用"。 ## 与 Postman / Insomnia 对比 GUI 工具优势: - 历史记录 + 文档化 - 团队共享 collection - 自动 OAuth 流程 CLI 工具优势: - 嵌入 shell 脚本 - 版本控制(.hurl 进 git) - 远程服务器跑(无 GUI 环境) 我个人:探索 / 团队共享用 [Bruno](https://www.usebruno.com/)(GUI 但 本地文件存储 + git 友好);shell 直接调用 xh / hurl。 ## bonus: jq / jaq 处理 JSON 响应 ```bash xh https://api.example.com/users | jq '.[] | select(.age > 30) | .name' # jaq(jq 的 Rust 重写,10x 快) brew install jaq xh https://api.example.com/users | jaq -r '.[] | .email' ``` `-r` raw 输出(无引号),适合 pipe 进 xargs。 ## 实战 workflow 测一个新 API: ```bash # 1. 探索 - GET xh https://api.example.com/users/1 # 2. 试 POST xh POST https://api.example.com/users name=Bob [email protected] # 3. 满意后写成 hurl 脚本 cat > tests/create-user.hurl <<'EOF' POST https://api.example.com/users { "name": "{{name}}", "email": "{{email}}" } HTTP 201 [Asserts] jsonpath "$.id" exists EOF # 4. CI 里跑 hurl --test --variables-file ci-vars.env tests/*.hurl ``` xh 探索 → hurl 固化。一条 pipeline。 ## 效果 迁移后: - 日常 API 调试输入字符 -70%(不用 `-X` `-H` `-d`) - API 回归测试用 hurl 脚本进 git,新员工看脚本就懂用法 - ci 端 hurl 测试 10 个 endpoint 用 5 秒 - 团队同 hurl 文件分享,跨 OS / 跨编辑器一致 ## 踩过的坑 1. **shell 解析 `:` 和 `=`**: ```bash http POST url 'description=hello, world' # 逗号正常 http POST url 'tags:=["a","b"]' # JSON 数组要 quote ``` 特殊字符记得 quote。 2. **httpie 默认 redirect 不跟**:`-F` / `--follow` 才跟 redirect。 xh 默认跟。 3. **session 文件路径**:`~/.config/httpie/sessions/`。换机器要带, 或 `--session-read-only` 临时用。 4. **hurl asserts 严格类型**:`jsonpath "$.count" == 5` 和 `== "5"` 不同(数字 vs 字符串)。看 API 返回类型对照写。 5. **不支持 WebSocket / gRPC**:纯 HTTP。WebSocket 用 wscat / websocat; gRPC 用 grpcurl。