知识广场

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

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

WireGuard 部署一条点对点 VPN(含 IPv4/IPv6 双栈)

WireGuard 是当前最容易理解 + 最快的 VPN 协议:4000 行内核代码, 配置就一个 `.conf` 文件。比 OpenVPN / IPsec 维护成本低一个数量级。 下面把两台公网服务器 `gate.example.com`(公网 IP `203.0.113.10`) 和家里的 NAT 后机器接到同一虚拟网段 `10.7.0.0/24`。 ## 1. 安装 ```bash # Debian 11+ / Ubuntu 20.04+ sudo apt install -y wireguard wireguard-tools ``` ## 2. 生成密钥(两端各一份) ```bash cd /etc/wireguard umask 077 wg genkey | tee privatekey | wg pubkey > publickey cat privatekey publickey ``` 下面用占位符: - `SERVER_PRIV` / `SERVER_PUB` - `PEER_PRIV` / `PEER_PUB` ## 3. 服务端 `gate` 上 `/etc/wireguard/wg0.conf` ```ini [Interface] Address = 10.7.0.1/24, fd00:7::1/64 ListenPort = 51820 PrivateKey = SERVER_PRIV # NAT 出口:让 peer 通过本机访问公网 PostUp = nft add table inet wg-nat; \ nft add chain inet wg-nat postrouting { type nat hook postrouting priority 100\; }; \ nft add rule inet wg-nat postrouting ip saddr 10.7.0.0/24 oifname "eth0" masquerade PostDown = nft delete table inet wg-nat [Peer] PublicKey = PEER_PUB AllowedIPs = 10.7.0.2/32, fd00:7::2/128 PersistentKeepalive = 25 ``` ## 4. 客户端 `/etc/wireguard/wg0.conf` ```ini [Interface] Address = 10.7.0.2/24, fd00:7::2/64 PrivateKey = PEER_PRIV DNS = 1.1.1.1, 2606:4700:4700::1111 [Peer] PublicKey = SERVER_PUB Endpoint = 203.0.113.10:51820 AllowedIPs = 0.0.0.0/0, ::/0 # 全量流量走 VPN;只走内网就改 10.7.0.0/24 PersistentKeepalive = 25 ``` `PersistentKeepalive = 25` 是 NAT 穿透的关键:每 25 秒发一个空包让上游 路由器 keep mapping。漏写 NAT 后端 5 分钟就掉。 ## 5. 启用 两端都: ```bash # 开 IP 转发(服务端必须) echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-wg.conf echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-wg.conf sudo sysctl --system sudo systemctl enable --now wg-quick@wg0 sudo wg show ``` ## 6. 校验 ```bash # 在 peer 上 ping 10.7.0.1 ping fd00:7::1 curl ifconfig.me # 应当返回 gate 的公网 IP(说明全量走 VPN 了) # 在 gate 上 sudo wg show # peer: ... # latest handshake: 5 seconds ago # transfer: 10 KiB received, 12 KiB sent ``` `latest handshake` 没出现说明握手没成功;先排查防火墙是否放行 UDP/51820。 ## 7. 加更多 peer 服务端 wg0.conf 追加 `[Peer]` 段;不需要 restart,热加: ```bash sudo wg set wg0 peer NEW_PEER_PUB allowed-ips 10.7.0.3/32 persistent-keepalive 25 # 然后改 conf 保存让重启后还在 ``` ## 踩过的坑 - `AllowedIPs` 不是 "允许这个 peer 访问哪里",而是 **路由表**——告诉 内核"目标是这些 IP 的包应该走这个 peer"。客户端写 `0.0.0.0/0` 就把 默认路由抢了。 - 服务端 `AllowedIPs = 10.7.0.2/32` 不能写 `/24`,否则只能挂一个 peer —— 后加的 peer 路由会被前者抢掉。 - `wg-quick` 的 `PostUp` / `PostDown` 行如果失败,接口照常起来但 NAT 没工作,表现为"能 ping 服务端但 ping 不到外网"。用 `sudo journalctl -u wg-quick@wg0` 看清楚 nft 报错。 - IPv6 加进来后 DNS resolver 必须双栈,否则会出现 IPv4 通、IPv6 通但 解析慢的"假掉线"现象。

OpenTelemetry 追踪一个请求经过的所有微服务(含 propagation)

## 起因 微服务架构里一个用户请求穿过:API gateway → auth service → product service → inventory service → recommendation service → DB / cache 各种。 某个请求慢了或失败,看不出是哪一跳。 日志里有 request id 但要手工去多个服务的 log 里搜,痛苦。 OpenTelemetry (OTel) 是 CNCF 的分布式追踪标准,让一个请求在所有服务里的 执行被串成"瀑布图",秒级定位慢 / 错的环节。 ## 解决方案 ### 1. 整体架构 ``` service A → service B → service C | | | +-----------+-----------+ ↓ OTel Collector ↓ Jaeger / Tempo ↓ Grafana UI ``` 每个 service 用 OTel SDK 生成 span(一段工作)。span 通过 HTTP header 跨服务传递(context propagation)。所有 span 发到 Collector, 后端(Jaeger / Tempo / DataDog)存储 + 显示。 ### 2. Python 服务集成(FastAPI) ```bash uv add opentelemetry-distro opentelemetry-exporter-otlp \ opentelemetry-instrumentation-fastapi \ opentelemetry-instrumentation-requests \ opentelemetry-instrumentation-psycopg ``` 启动时打开: ```bash OTEL_SERVICE_NAME=my-api \ OTEL_TRACES_EXPORTER=otlp \ OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 \ opentelemetry-instrument uvicorn app.main:app ``` `opentelemetry-instrument` 自动 patch FastAPI / requests / psycopg / SQLAlchemy / Redis / Kafka / 几十种库。零代码改动。 每个 HTTP 请求自动开 trace span,DB query / outgoing HTTP 自动是 child span。 ### 3. 手动加 span(业务关键路径) ```python from opentelemetry import trace tracer = trace.get_tracer(__name__) def process_order(order_id: str): with tracer.start_as_current_span('process_order') as span: span.set_attribute('order.id', order_id) with tracer.start_as_current_span('validate'): validate(order_id) with tracer.start_as_current_span('charge_payment'): charge(order_id) with tracer.start_as_current_span('ship'): ship(order_id) ``` UI 里看到 process_order 总耗时 350ms,其中 validate 50ms / charge 280ms / ship 20ms。一眼定位 charge 是瓶颈。 ### 4. Go 服务集成 ```go import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) func setupTracing(ctx context.Context) (*sdktrace.TracerProvider, error) { exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint("collector:4317"), otlptracegrpc.WithInsecure(), ) if err != nil { return nil, err } res := resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceName("my-go-svc"), ) tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp), sdktrace.WithResource(res), ) otel.SetTracerProvider(tp) return tp, nil } // HTTP server 加 middleware mux := http.NewServeMux() mux.Handle("/api/", otelhttp.NewHandler(yourHandler, "api")) // 业务里 tracer := otel.Tracer("my-go-svc") ctx, span := tracer.Start(ctx, "fetch_user") defer span.End() span.SetAttributes(attribute.String("user.id", uid)) ``` ### 5. 跨服务 propagation 服务间 HTTP 调用时自动透传 trace context(W3C `traceparent` header): ``` GET /products HTTP/1.1 Host: products-svc traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ``` 接收方解析 → 在同 trace 下创建 child span → 追踪连续。 Python / Go / Java / Node SDK 都自动处理。 ### 6. Collector 部署 `otel-collector-config.yaml`: ```yaml receivers: otlp: protocols: grpc: { endpoint: 0.0.0.0:4317 } http: { endpoint: 0.0.0.0:4318 } processors: batch: timeout: 5s memory_limiter: limit_mib: 1024 exporters: otlp/jaeger: endpoint: jaeger:4317 tls: { insecure: true } logging: { loglevel: warn } service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, batch] exporters: [otlp/jaeger, logging] ``` ```bash docker run -p 4317:4317 -p 4318:4318 \ -v $(pwd)/otel-collector-config.yaml:/etc/otelcol/config.yaml \ otel/opentelemetry-collector-contrib ``` ### 7. 后端:Jaeger / Tempo ```bash # Jaeger(all-in-one,开发用) docker run -p 16686:16686 -p 4317:4317 jaegertracing/all-in-one:latest # 浏览器:http://localhost:16686 # 选 service → 查 traces → 点开看瀑布图 ``` 生产用 Tempo(与 Grafana / Loki 同生态)+ S3 后端。 ### 8. 在 trace 里看到日志(trace + log correlation) ```python import logging from opentelemetry.instrumentation.logging import LoggingInstrumentor LoggingInstrumentor().instrument(set_logging_format=True) logging.info('processing order %s', order_id) # log 自动带上 trace_id / span_id ``` ``` 2026-05-24 10:00:01 [INFO] [trace_id=abc... span_id=def...] processing order o-123 ``` Loki 配置识别 trace_id → 在 Grafana 里点 log 直接跳到对应 trace。 跨数据源关联无缝。 ### 9. 采样 100% 采样在高 QPS 时数据爆炸。生产建议: ```yaml processors: probabilistic_sampler: sampling_percentage: 5 # 5% 采样 ``` 或更智能 tail sampling:保留所有 error trace + 慢 trace + 5% 普通 trace。 ### 10. metrics + logs(统一 OTel) OTel 不止 trace,还有 metric 和 log。一份 SDK 配置三种信号都收。 逐步替代 Prometheus client / 各种 logger,到 OTel 标准化。 ## 效果 我们 5 微服务架构接 OTel 后: - "用户报反应慢" 类 issue 调查时间从 30min → 3min - 发现一个 N+1 query(隐藏在 lib 里)日浪费 500ms × 万次请求 - 知道哪个下游服务最不稳定(看 trace span 错误率) - DBA / SRE 不再需要"装 5 个服务的 log 拼接" ## 与 Sentry / DataDog 等对比 | | OTel + Jaeger/Tempo | DataDog APM | Sentry Performance | New Relic | |---|---|---|---|---| | 开源 / 自托管 | ✅ | ❌ | 部分 | ❌ | | 学习曲线 | 中 | 低 | 低 | 低 | | 价格 | 几乎免费 | 贵 | 中 | 贵 | | 标准化 | ✅ 行业标准 | 私有 | 私有 | 私有 | OTel 让你的代码 vendor-neutral:今天 Jaeger,明天换 DataDog 切 exporter 就行。 ## 踩过的坑 1. **collector 没起 → SDK 重试堆积 RAM**:SDK 默认会缓存 batch 失败 重试。collector 早死 → 应用内存涨。配 `max_export_batch_size` 和 超时 drop。 2. **trace context 跨异步任务丢**:Celery / async task 默认 trace 断开。 要手动用 OTel context inject / extract: ```python ctx = trace.set_span_in_context(current_span) carrier = {} inject(carrier) # carrier 里有 traceparent celery_task.delay(payload, ctx=carrier) ``` 3. **span 太多**:每个 SQL query 自动一个 span,1 个请求几百个 span, 存储成本飙。SQL instrumentation 配 `enable_commenter=False` 或 只 trace 慢 query。 4. **PII 泄漏**:默认 instrument 把 HTTP query string / body 记进 span attribute → trace 里包含密码 / token。配 `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` 或自定义 sanitizer。 5. **service 间时钟漂移**:trace 时间戳来自各服务本机时钟。两机器 差 100ms → 瀑布图错位。所有服务配 chrony 同 NTP。

ZFS vs btrfs:家用 NAS 选哪个文件系统

## 起因 新装 NAS 4 块 4TB 硬盘。选 file system 时纠结 ZFS vs btrfs。 都支持:snapshot / compression / dedup / RAID / checksum。 但底层设计 + 成熟度 + 稳定性差异不小。下面是我做完功课的对比。 ## 共同特性 - 文件系统级 checksum(侦测 bit rot) - transparent compression(zstd / lz4 / gzip) - snapshot + send/receive 增量 - pool / volume / dataset 抽象 - 多盘 RAID-like 配置 - copy-on-write 设计 ## 主要差异 ### 1. RAID **ZFS**: - mirror / raidz (raid5) / raidz2 (raid6) / raidz3 - pool 一旦创建后**不能扩容 vdev 内的盘**(v2024 改了,但有限制) - 加更多盘要新 vdev(stripe over vdev) **btrfs**: - single / dup / raid0 / raid1 / raid10 / raid5 / raid6 - 极灵活:随时加盘 / 移盘 / 改 RAID profile(balance 操作) - **raid5/6 仍然 NOT production ready**(这是 btrfs 最大坑) 家用 NAS 常见配置: - 4 块盘 + ZFS RAID-Z2:可挂 2 块(最佳容错) - 4 块盘 + btrfs RAID1:只挂 1 块;btrfs RAID1 是"每文件 2 副本" 而非"两组镜像",新颖但适配差 ### 2. 稳定性 / 数据安全 **ZFS**: - 2005 起 production,全球大企业用,**经受 20 年实战** - 数据完整性是设计第一原则 - bug 极少(且大多在 cutting-edge 功能) **btrfs**: - 2007 起,主线内核 2009+ - 单盘 / RAID0/1/10 稳,**RAID5/6 多年文档警告 not production** - 2024 ext4 maintainer 写过 "I still don't recommend btrfs raid5/6" - 多盘环境历史 bug 多于 ZFS 数据安全敏感 → ZFS。 ### 3. 内存需求 **ZFS**: - ARC (Adaptive Replacement Cache) 吃内存:1GB / 1TB rule of thumb - 16GB NAS + 16TB 数据:ARC 占 10GB + - 启用 dedup 更吃(每 1TB ~5GB RAM) - 可调整 `zfs_arc_max` **btrfs**: - 内存占用低很多 - 4GB RAM 也能稳定跑 8TB btrfs 老 NAS / 低 RAM 设备 → btrfs。 ### 4. 性能 随机读写:ZFS(with ARC)通常更快。 顺序读写:相近。 压缩:ZFS lz4 / zstd 性能略好。 但实际差距不大(除非 RAM 极充裕时 ZFS ARC 效果显著)。 ### 5. snapshot + send/receive 两者都有,用法相似: ```bash # ZFS zfs snapshot tank/data@daily-20240524 zfs send tank/data@yesterday tank/data@daily-20240524 | ssh remote 'zfs receive ...' # btrfs btrfs subvolume snapshot /mnt/data /mnt/data/.snapshots/daily-20240524 -r btrfs send -p /mnt/data/.snapshots/yesterday /mnt/data/.snapshots/daily-20240524 | ssh remote 'btrfs receive ...' ``` ZFS send 更成熟稳定。btrfs send/receive 有过历史 bug。 ### 6. 加密 **ZFS** 2.0+:内置 native encryption **btrfs**:依赖 LUKS 下层加密(不是 fs 级 native) ZFS 加密更灵活(per-dataset key)。 ### 7. 文件系统级 quota **ZFS**:quota 是基础特性,per-dataset / per-user **btrfs**:qgroup 复杂 + 历史 bug + 性能影响 需要 quota → ZFS。 ### 8. Linux kernel 集成 **ZFS**:不在 mainline kernel(CDDL vs GPL license 冲突)。 - 装 OpenZFS 模块(apt install zfsutils-linux) - 每次 kernel 升级需要 DKMS 重 build - Ubuntu 官方支持;其它 distro 看情况 **btrfs**:mainline kernel 内置,开箱即用 部署简单 → btrfs。 但 ZFS 装好后稳定,DKMS 自动重 build,不算大坑。 ### 9. 工具生态 | 任务 | ZFS | btrfs | |---|---|---| | GUI | Cockpit ZFS / TrueNAS | Cockpit btrfs / Snapper | | 备份 sync | syncoid + sanoid(极成熟) | btrbk | | 自动 snapshot | sanoid | snapper / btrbk | | pool 监控 | zpool status + zed | btrfs scrub status | ZFS 工具链更成熟(成熟得益于 Solaris 时代积累)。 ### 10. 修复 / 恢复 **ZFS**: - `zpool scrub` 自动修复有 redundancy 的损坏 - 严重时 `zpool import -F` 强制恢复 - 数据恢复工具:zdb(专家用) **btrfs**: - `btrfs scrub` 类似 - 严重时 `btrfs restore` 拉文件出来 - 但 RAID5/6 + 多盘损坏场景历史上有人 lose 数据 ZFS recover 工具更可靠。 ## 选择决策 | 场景 | 推荐 | |---|---| | 4+ 盘 + RAID5/6 + 关键数据 | **ZFS** (RAID-Z2) | | 2 盘 mirror | 都行(btrfs 略简单) | | 单盘大容量 | btrfs(CoW + snapshot) | | 内存紧张(< 4GB) | btrfs | | 内存充裕(> 16GB) | ZFS | | 笔记本 + Fedora | btrfs(mainline) | | 严格 quota / multi-tenant | ZFS | | 想随时改 RAID profile | btrfs | | 远程异地复制 | ZFS(syncoid 成熟) | ## 我的实际选择 家用 NAS(4 × 4TB):**ZFS RAID-Z2 + zstd 压缩** - 容忍 2 盘挂 - 16GB RAM 给 ARC 用 - 用 sanoid + syncoid 自动 snapshot + 远程异地 笔记本 Linux 根分区:**btrfs** - mainline,无 DKMS 折腾 - snapshot 频繁,Fedora 系自动 snapper ## 安装 ### ZFS on Ubuntu 22.04+ ```bash sudo apt install -y zfsutils-linux # 4 盘 RAID-Z2 + 4K aligned + lz4 默认 sudo zpool create -o ashift=12 \ -O compression=lz4 -O atime=off -O xattr=sa -O dnodesize=auto \ tank raidz2 \ /dev/disk/by-id/scsi-... \ /dev/disk/by-id/scsi-... \ /dev/disk/by-id/scsi-... \ /dev/disk/by-id/scsi-... ``` ### btrfs on Ubuntu / Fedora ```bash sudo apt install -y btrfs-progs # 4 盘 RAID10 sudo mkfs.btrfs -L data -m raid10 -d raid10 /dev/sdb /dev/sdc /dev/sdd /dev/sde sudo mkdir /mnt/data sudo mount -o compress=zstd:3,space_cache=v2 /dev/sdb /mnt/data ``` ## 常见操作对照 ```bash # 看状态 zpool status -v # ZFS btrfs filesystem usage /mnt/data # btrfs # 校验所有数据 zpool scrub tank btrfs scrub start /mnt/data # 加盘扩容 zpool add tank /dev/new-disk # 新 vdev (stripe) btrfs device add /dev/new-disk /mnt/data && btrfs balance start /mnt/data # snapshot zfs snapshot tank/data@snap btrfs subvolume snapshot /mnt/data /mnt/data/.snap/snap # 删除 snapshot zfs destroy tank/data@snap btrfs subvolume delete /mnt/data/.snap/snap # 看压缩比 zfs get compressratio tank compsize /mnt/data ``` ## 踩过的坑 ### ZFS 1. **ARC 吃光内存** → 容器 OOM。`zfs_arc_max` 调小。 2. **DKMS 升级 kernel 慢** → reboot 后 zpool 找不到。等 DKMS build 完 再 reboot。 3. **pool import 失败** → 用 disk by-id 不要 /dev/sdX(顺序会变)。 4. **dedup 慎用** → 5GB RAM / 1TB 数据,开了多数情况后悔。 ### btrfs 1. **RAID5/6 不要上生产**——文档明确警告。 2. **subvolume 嵌套乱** → snapshot 时易踩坑。清晰目录结构。 3. **balance 时机器抖** → 大量 IO 影响业务。低峰跑 + 限速。 4. **qgroup 启用后慢 5-10 倍** → 不需要 quota 就别开。 ## 多年使用感受 我个人 5+ 年家用 NAS 跑 ZFS: - 多次硬盘故障,RAID-Z2 透明处理 + 在线替换 - 月度 scrub 偶尔修复 bit rot(每次几 KB;硬盘老化的标志) - snapshot 救命过若干次:误删文件 / VM 改坏直接回滚 btrfs 笔记本 3 年: - snapshot 救过几次升级失败 - 偶尔 scrub 报错(无 redundancy 时无法 fix),重要数据靠云备份 - 性能 / 稳定够日常用 两个都好。选符合你 use case 的不犯错。

试一周 Svelte 5:runes 模式 + 编译时优化的"无 vDOM"哲学

## 起因 React 写多了开始审美疲劳:useState 一个数字、useEffect 一个 fetch、 useMemo 一堆 callback...... 想试试别的范式。Svelte 5 的卖点是 "编译时把响应式翻译成命令式代码"——bundle 极小,写法更接近原生 JS。 跟一个小项目(个人博客 + 评论系统)试了一周。 ## 装 ```bash npm create vite@latest myblog -- --template svelte-ts cd myblog npm i npm run dev ``` 或者 SvelteKit(带 SSR / routing / API routes): ```bash npx sv create myblog ``` ## 第一个组件 ```svelte <!-- Counter.svelte --> <script lang="ts"> let count = $state(0) let doubled = $derived(count * 2) function increment() { count++ } </script> <button onclick={increment}> {count} (doubled: {doubled}) </button> <style> button { padding: 8px 16px; border-radius: 4px; } </style> ``` `$state` 和 `$derived` 是 Svelte 5 引入的 "runes"——显式标记响应式 变量。 跟 React 对比: ```tsx const [count, setCount] = useState(0) const doubled = useMemo(() => count * 2, [count]) return ( <button onClick={() => setCount(c => c + 1)}> {count} (doubled: {doubled}) </button> ) ``` Svelte 优势: - `count++` 直接改,无需 setter - `doubled` 自动追踪依赖,无需 deps 数组 - `<style>` scoped 内置 - 模板更接近 HTML,无 className 之类 bundle 大小:上面 Svelte 组件编译后约 1KB;React 等价组件需要 React + ReactDOM ~45 KB。 ## $effect: 副作用 ```svelte <script> let count = $state(0) $effect(() => { console.log(`count changed to ${count}`) document.title = `Count: ${count}` }) </script> ``` `$effect` 类似 React useEffect 但**自动追踪**用到的响应式变量, 不需要 deps 数组。`count` 一变就重跑。 ## 父子通信:$props / $bindable ```svelte <!-- Child.svelte --> <script lang="ts"> let { name, count = $bindable(0) } = $props<{ name: string count: number }>() </script> <input bind:value={count} /> <p>{name}: {count}</p> ``` ```svelte <!-- Parent.svelte --> <script> let n = $state(0) </script> <Child name="counter" bind:count={n} /> <p>Parent sees: {n}</p> ``` `bind:` 是 Vue v-model 的等价物。`$bindable` 让 prop 双向。 ## 列表渲染 ```svelte <script> let items = $state([1, 2, 3]) function add() { items.push(items.length + 1) // ✅ 直接 push,Svelte 5 能追踪 } </script> {#each items as item, i (item)} <div>{i}: {item}</div> {/each} <button onclick={add}>add</button> ``` `(item)` 是 key(类似 React key)。 `{#each}` / `{#if}` / `{#await}` 是 Svelte 模板语法。 ## 异步:{#await} ```svelte <script> let promise = fetch('/api/users/1').then(r => r.json()) </script> {#await promise} <p>loading...</p> {:then user} <p>{user.name}</p> {:catch err} <p>error: {err.message}</p> {/await} ``` 直接在模板里处理 promise,不需要 useState + useEffect。 ## SvelteKit:File-based routing + SSR ``` src/routes/ ├── +page.svelte # / ├── about/+page.svelte # /about ├── posts/ │ ├── +page.svelte # /posts │ ├── +page.server.ts # 数据预取(server-only) │ └── [id]/+page.svelte # /posts/:id ``` ```ts // src/routes/posts/+page.server.ts export async function load() { const posts = await db.posts.findMany() return { posts } } ``` ```svelte <!-- src/routes/posts/+page.svelte --> <script> let { data } = $props() </script> {#each data.posts as post} <a href="/posts/{post.id}">{post.title}</a> {/each} ``` `load` 在服务端跑,结果通过 `data` prop 传给页面。 类似 Next.js getServerSideProps 但更轻量。 ## API routes ```ts // src/routes/api/posts/+server.ts import { json } from '@sveltejs/kit' export async function GET() { const posts = await db.posts.findMany() return json(posts) } export async function POST({ request }) { const data = await request.json() const post = await db.posts.create({ data }) return json(post, { status: 201 }) } ``` 文件即 API endpoint。 ## 状态管理 不需要 Redux / Zustand。直接 `$state` 在共享模块里: ```ts // src/lib/auth.svelte.ts export const auth = $state({ user: null as User | null, }) export async function login(email, pw) { const r = await fetch('/api/login', ...) auth.user = await r.json() } ``` 任何组件 import 后修改 `auth.user`,所有用到的地方自动更新。 ## bundle 大小对比 同一个 todo app: | | bundle (gzipped) | |---|---| | React + Redux | 78 KB | | React + Zustand | 52 KB | | Vue 3 | 41 KB | | **Svelte 5** | 11 KB | Svelte 编译模式让框架本身的运行时极小。 ## 性能 benchmark [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/) 显示 Svelte 在多数操作上比 React 快 1.5-3 倍(虽然 React 19 + compiler 之后差距缩小)。 ## 何时选 Svelte - 个人项目 / 小团队 / 喜欢简洁 - bundle size 关键场景(嵌入式 widget / PWA) - 已有 web 基础,不想被框架抽象绑死 何时选 React: - 大生态需求(组件库 / 招聘 / 大公司支持) - 已有 React 团队 - React Native 跨端 ## 缺点 1. **生态相对小**:组件库 / 工具链 / 教程都比 React 少 2. **招聘难**:Svelte 工程师比 React 少很多 3. **Svelte 5 runes 模式刚出**:4 → 5 迁移有小痛 4. **大型应用案例少**:Apple Music 等用了但还不算主流 ## 一周体验感受 - 写起来明显比 React 顺手(无 useState 仪式、无 deps 数组焦虑) - 编译后产物小到惊喜 - 但生态查个稍复杂的库选择少 / 文档少 - 对于个人博客 / 小工具站推荐;上生产前评估团队 / 长期维护成本 ## 踩过的坑 1. **Svelte 4 教程 / 文档不适用 Svelte 5**:`export let foo` → `let { foo } = $props()`, `$:` → `$derived`/`$effect`。教程要看 5.x。 2. **`$state` 必须在 script setup 顶层**:不能在条件 / 函数里。 3. **数组 / 对象的响应性**:`items.push(...)` Svelte 5 能追踪, 但深层嵌套 `items[0].nested.value = ...` 仍要小心。深层用 store。 4. **SSR + 浏览器 API**:`window.localStorage` 在 SSR 时报错。 `if (browser) { ... }` 包起来或 `onMount` 里访问。 5. **VSCode 插件**:要装官方 Svelte for VSCode + 语言服务器。 IntelliSense / 跳定义都依赖。

mise(前 rtx):单工具管理 Node / Python / Go / Ruby 版本

## 起因 每个项目用不同版本的 runtime / SDK: - 项目 A:Node 18 + Python 3.11 - 项目 B:Node 20 + Python 3.12 - 项目 C:Go 1.22 + Bun 1.1 老办法各种 version manager: - nvm(Node) - pyenv(Python) - rbenv(Ruby) - gvm / goenv(Go) 每个工具一套 shell 集成 / shim 机制 → 启动慢 + 配置碎。 `mise`(Rust 写的,前身 rtx,前前身 asdf 的现代 fork)是**一个工具 管理所有版本**。`.mise.toml` 文件描述项目所需版本,进目录自动切。 ## 装 ```bash # macOS brew install mise # Linux curl https://mise.run | sh # 集成 shell echo 'eval "$(mise activate zsh)"' >> ~/.zshrc ``` ## 项目用 ```bash cd ~/projects/myapp # 给项目指定 Node 20 + Python 3.12 mise use node@20 [email protected] # 自动写到 .mise.toml cat .mise.toml # [tools] # node = "20" # python = "3.12" # 装这俩 mise install ``` 进入这目录 → mise 自动激活对应版本: ```bash $ cd ~/projects/myapp $ node --version v20.18.0 $ python --version Python 3.12.4 $ cd ~/projects/other $ node --version v18.20.0 # 自动切换 ``` ## .mise.toml 完整例 ```toml # .mise.toml(项目根,commit 进 git) [tools] node = "20.18.0" # 精确版本 python = "3.12" # 主版本 go = "1.22" bun = "latest" # 最新 "npm:pnpm" = "9" # 通过 npm 装 pnpm # 环境变量 [env] NODE_ENV = "development" PYTHONUNBUFFERED = "1" _.file = ".env" # 加载 .env 文件 # 项目 task(取代 justfile / Makefile,简单场景) [tasks.dev] run = "node server.js" [tasks.test] run = "pnpm test" ``` `mise install` 装全部 → 进目录自动用。 ## 比 asdf 快多少 mise 是 asdf 的 Rust rewrite + 改进: | | asdf | mise | |---|---|---| | 语言 | Bash | Rust | | shell hook 延迟 | 50-200ms | < 5ms | | 工具机制 | shim | env-based + shim | | 配置 | .tool-versions | .mise.toml | | 任务系统 | 无 | 有(取代 just) | | 装速度 | 慢 | 快 | cd 到项目目录,asdf 的 shell hook 拖 100ms+。mise 几乎无感。 ## 多 runtime + 同时 ```toml [tools] node = ["20", "22"] # 两个版本都装 # 默认用第一个 → node 是 20 # 调 22:mise exec node@22 -- node --version ``` ## 共享 mise.toml + 个人 override ```toml # .mise.toml(commit) [tools] node = "20" python = "3.12" # .mise.local.toml(gitignore,个人覆盖) [tools] python = "3.13" # 我本地试 3.13 ``` `.mise.local.toml` 覆盖 `.mise.toml`,团队不受影响。 ## 全局默认 ```bash mise use -g node@22 mise use -g [email protected] ``` 写到 `~/.config/mise/config.toml`。没有 `.mise.toml` 的目录用全局 版本。 ## 工具源 mise 用 asdf 插件 + 内置后端: ```bash # 列出可装 mise ls-remote node mise ls-remote python # 通过 npm / pip / cargo 后端装 mise use "npm:typescript@5" mise use "pipx:black@24" mise use "cargo:ripgrep@13" ``` 也支持 GitHub release / ubi / pre-compiled binary 等。 ## task runner ```toml [tasks.lint] description = "Run linters" run = """ ruff check . pnpm tsc --noEmit """ [tasks.test] depends = ["lint"] run = "pytest" ``` ```bash $ mise tasks # 列出 $ mise run test # 跑(先跑 lint) ``` 替代 justfile 的简单场景。复杂的还是 just 强。 ## 环境变量 + secrets ```toml [env] DATABASE_URL = "postgres://localhost/myapp" _.file = ".env.local" # 也加载这文件 _.path = ["./bin", "./node_modules/.bin"] # 加 PATH ``` 进目录自动 export → 写脚本不用 source。 `.env.local` 在 .gitignore,本地 secrets。 ## CI 用 mise ```yaml # GitHub Actions - uses: jdx/mise-action@v2 with: install: true - run: mise run test ``` CI 装的版本 = `.mise.toml` 里的版本 = 本地版本。一致性保证。 ## 与 Docker dev container 对比 | | mise | Docker dev container | |---|---|---| | 隔离 | 进程级 | 完全隔离 | | 启动 | 即时 | 启动 container 几秒 | | IDE | 原生(无远程) | VS Code Remote | | 文件 IO | 原生 | bind mount 可能慢(mac) | | 生产一致性 | 中(runtime 一致 OS 可能不同) | 高 | 小项目 / 单人 → mise 够用 + 更快。 大项目 + 多 OS 团队 → devcontainer 更靠谱。 我自己 95% 项目 mise,少数客户项目 devcontainer。 ## 从 nvm 迁移 ```bash # 卸 nvm rm -rf ~/.nvm # 从 .zshrc 删 nvm source # 装 mise + 集成 # 老的 .nvmrc 自动识别(兼容) echo "v20" > .nvmrc mise install ``` `.nvmrc` / `.python-version` / `.tool-versions` mise 全识别。 不强制迁到 `.mise.toml`,但建议(功能更强)。 ## 踩过的坑 1. **shell 没集成**:`node --version` 返回老版本。mise 必须 shell hook 才能切。`mise activate` 加 rc 文件 + 重开终端。 2. **CI 装很慢**:mise 第一次装 Python 3.12 编译几分钟。 `MISE_PYTHON_COMPILE=0` 用预编译 binary(uv 同样做法)。或者 cache `~/.local/share/mise/installs`。 3. **跟 asdf 冲突**:装了 asdf 又装 mise → PATH 混。卸一个。 4. **某些工具没插件**:罕见工具 mise 没现成插件。可以 `mise plugin install <git-url>` 加 asdf 插件用。 5. **`mise use` 不写版本会写 latest 到 .mise.toml**:commit 进 git 后别人装的可能是更新版本。建议明确版本号。

Helix 编辑器:开箱即用的 modal editor 替代 vim/neovim

## 起因 我用 Neovim 几年,写过 ~200 行 Lua 配置。换电脑要折腾环境;同事抄 config 也要解释半天为什么这个插件做这个事。 听说 Helix 是 "Vim 改进版" 而且**零配置**就有 LSP / Treesitter / fuzzy 搜索,试一周。 ## 与 Vim 的区别 Helix 的设计借鉴 Kakoune:**先选后操**(selection-first)。 Vim 是动词→名词:`d w` = delete word(先 delete 后选)。 Helix 是名词→动词:`w d` = select word(看到 selection 高亮)然后 delete。 效果差异: - Vim:你想象 → 输命令 → 看结果(有时不对要 undo) - Helix:每一步看到 selection 高亮 → 确认后再操作 对于复杂选择(`d a (` vs `d i {`),Helix 的"先选"模式让人少出错。 ## 装 + 跑 ```bash # macOS brew install helix # Debian / Ubuntu sudo add-apt-repository ppa:maveonair/helix-editor sudo apt install helix # Arch sudo pacman -S helix hx --version ``` ```bash hx myfile.py # 或: hx . # 打开文件夹(文件浏览器) ``` ## 默认能力(零配置) 打开任何文件就有: - 语法高亮(Treesitter,所有主流语言) - LSP(自动找 pyright / rust-analyzer / gopls / tsserver 如果系统装了) - 自动补全 - 跳转定义 / 引用 - diagnostics 显示 - 模糊文件搜索(Space-f) - 全局 grep(Space-/) - 多文件 buffer - multiple cursor 多光标编辑 - 主题(30+ 内置) 不需要装插件,不需要写 config。 ## 我用了一周记下来的常用键位 normal 模式(默认): ``` hjkl 移动 w / b 下/上一词 W / B 下/上 WORD (含标点) gh / gl 行首 / 行尾 gg / ge 文件首 / 末 G 指定行号 % 配对括号 # 选择 v 进 selection extend 模式 x 选整行 ) 扩选下一句 m m i 选当前 ( ... ) 内(match mode) m m a 选当前 ( ... ) 含括号 # 多光标 C 下一行加光标 , collapse 多光标到主光标 A-C 上一行加光标 * 把当前 selection 作搜索 word # 操作(先选后改) d delete c change (delete + insert mode) y yank p paste ~ 切大小写 u undo U redo # LSP gd 去定义 gr 引用 gi 实现 K hover info SPC a code actions SPC r rename # 文件 / buffer SPC f 模糊找文件 SPC b buffer 列表 SPC / grep 全工程 SPC s symbol(当前文件) SPC S workspace symbol # 命令模式 : : write / : help / : config-reload / 等 ``` 按 Space 进入 picker 是 Helix 一大亮点:fuzzy 文件 / buffer / symbol / grep / diagnostic 都在 Space-x 系列下。 ## 配置:可选但很少需要 `~/.config/helix/config.toml`: ```toml theme = "catppuccin_mocha" [editor] line-number = "relative" mouse = false cursorline = true auto-format = true bufferline = "always" [editor.indent-guides] render = true character = "│" [editor.lsp] display-messages = true display-inlay-hints = true [editor.cursor-shape] insert = "bar" normal = "block" select = "underline" ``` 跟 Neovim 几百行 Lua 比,30 行 TOML 解决全部。 ### 自定义 keymap ```toml [keys.normal] "C-s" = ":w" "C-q" = ":q" "esc" = ["collapse_selection", "keep_primary_selection"] [keys.normal.space] "e" = "file_picker" # 改 SPC e 为文件搜索(个人偏好) ``` ## LSP / formatter 装系统 Helix 不管 LSP server 安装。系统装好后自动用: ```bash # Python pipx install pyright ruff # 或 npm i -g pyright # TypeScript / JS npm i -g typescript typescript-language-server # Go go install golang.org/x/tools/gopls@latest # Rust(rustup 自带) rustup component add rust-analyzer ``` `hx --health python` 看 python LSP / formatter / debugger 检测状态: ``` Configured language servers: ✓ pyright: /home/me/.local/bin/pyright-langserver ✓ ruff: /home/me/.local/bin/ruff Configured debug adapter: ✓ debugpy Configured formatter: ✓ ruff Treesitter parser: ✓ Highlight queries: ✓ ``` 哪一项不通 / 哪个 binary 缺都明确告诉你。 ## 跟 Neovim / VSCode 对比 | | Helix | Neovim | VSCode | |---|---|---|---| | 启动速度 | ~30ms | ~50-300ms(看配置) | 1-3s | | 配置量(默认能用) | 0 行 | 200-500 行 Lua | 0 行 + extension | | LSP 集成 | 内置 | nvim-lspconfig | 内置 | | 插件生态 | 极少(按 plugin 不支持) | 极丰富 | 极丰富 | | modal editing | ✅(kakoune 风) | ✅(vi 风) | extension | | 内置 fuzzy / grep | ✅ | 装 telescope 等 | ✅ | | debugger | DAP 集成(粗糙) | DAP(telescope-dap) | 极强 | | 鼠标 / GUI | terminal only | terminal / neovide | full GUI | **Helix 适合**:想要 modal editing 但不想花周末写 config 的人。 **Neovim 适合**:极致定制 / 已经投入了配置 / 需要某些插件。 **VSCode 适合**:debugger 重度 / 需要图形 / 团队混用。 ## 缺点 / 注意 - **插件系统**:Helix 不支持 plugin(design 选择)。Neovim 上的 copilot / lazygit 集成 / Treesitter playground 等都没有 Helix 等价物。 Plugin support 在 roadmap 上但还没。 - **debugger 弱**:内置 DAP 客户端粗糙,debug 体验不如 VSCode / nvim-dap。 - **Vim 用户初期不适**:动词名词顺序反 + 一些常用键改了。1-2 周适应。 ## 一周体验 - 启动从 Neovim ~150ms → Helix 30ms(lazyloading 自然瞬时) - 写代码体验差不多(LSP + Treesitter 一样齐) - 不需要每年大改 config 跟 plugin breaking change - 同事看我屏幕能立刻上手(key 高亮 + status bar 提示下一步) 正在评估全量切到 Helix。对深度 plugin 依赖(如 Org-mode)的同事保留 Neovim。 ## 踩过的坑 1. **`y` / `p` 不进系统剪贴板**:默认是 Helix 内部 register。 要系统:`"+y` / `"+p`(用 `+` register)。可以 keymap 覆盖让 `y` 默认走系统。 2. **Treesitter parser 缺**:第一次开某语言文件 `hx --health <lang>` 看哪些缺。`:treesitter-build` 重 build。 3. **LSP 自动启但不响应**:`:log-open` 看 LSP 日志。常见原因是 project root 找不到(缺 `pyproject.toml` / `package.json`)。 4. **Multi-cursor 撤销不一致**:`u` 一次 undo 所有 cursor 的最近改动。 习惯就好。 5. **没 file tree**:`SPC f` fuzzy 找文件代替(实际更快)。强烈想要 file tree → 暂时只能等 plugin support。

gRPC vs REST:Go 服务间通信怎么选 + grpc-gateway 兼容方案

## 起因 10 个 Go 微服务之间互通: - REST + JSON:易调试 / 浏览器友好 / 工具多 - gRPC:类型严格 / 性能好 / 双向 streaming 之前都用 REST。后来内部 service 间改 gRPC,对外仍 REST, 靠 grpc-gateway 一份 proto 自动生成两套。 ## gRPC 优势(service 间) ### 1. 强类型 ```protobuf // user.proto syntax = "proto3"; service UserService { rpc GetUser(GetUserRequest) returns (User); rpc CreateUser(CreateUserRequest) returns (User); rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); } message User { int64 id = 1; string email = 2; string name = 3; int64 created_at = 4; } message GetUserRequest { int64 id = 1; } ``` `protoc` 生成 Go / Python / TypeScript / Java client + server stub。 **改字段类型 → 客户端编译报错**。 vs REST 改 JSON schema → 客户端运行时炸。 ### 2. 性能 - HTTP/2 + protobuf binary:3-5x 比 JSON 快 + 小 - 长连接复用:避免每请求 TCP / TLS 握手 - 服务端 streaming / 双向 streaming 原生支持 我们一个高 QPS 服务从 REST + JSON 改 gRPC: | | REST | gRPC | |---|---|---| | P50 延迟 | 5ms | 1.5ms | | 单连接 RPS | 500 | 5000 | | CPU 占用 | 35% | 12% | 3-10x 提升。 ### 3. 双向 streaming ```protobuf service ChatService { rpc Chat(stream Message) returns (stream Message); } ``` 客户端 / 服务端都能持续发消息。WebSocket-like 但带类型。 适合:实时通知、聊天、log tail、bidirectional sync。 ## REST 优势(对外 API) - 浏览器直接调用(不需要特殊 client) - curl / Postman / 任意 HTTP tool 调试 - HTTP cache friendly(GET / If-Modified-Since 等) - OpenAPI / Swagger 文档丰富 - 简单 SDK 自动生成(openapi-generator 覆盖语言广) 对外暴露给"未知客户端" → 必须 REST 或 GraphQL。 ## 两者结合:grpc-gateway `grpc-gateway` 让一份 .proto 同时生成 gRPC server + REST proxy: ```protobuf import "google/api/annotations.proto"; service UserService { rpc GetUser(GetUserRequest) returns (User) { option (google.api.http) = { get: "/v1/users/{id}" }; } rpc CreateUser(CreateUserRequest) returns (User) { option (google.api.http) = { post: "/v1/users" body: "*" }; } } ``` `protoc` + `grpc-gateway` 插件生成: - `UserServiceServer` interface(你实现 gRPC server) - `RegisterUserServiceHandlerServer`(注册 REST → gRPC proxy) ```go // main.go func main() { // gRPC server grpcServer := grpc.NewServer() userpb.RegisterUserServiceServer(grpcServer, &userServer{}) // 启 gRPC :9090 lis, _ := net.Listen("tcp", ":9090") go grpcServer.Serve(lis) // REST gateway :8080 ctx := context.Background() mux := runtime.NewServeMux() err := userpb.RegisterUserServiceHandlerServer(ctx, mux, &userServer{}) http.ListenAndServe(":8080", mux) } ``` 客户端两种方式调: ```bash # REST curl http://localhost:8080/v1/users/42 # gRPC grpcurl -plaintext localhost:9090 user.UserService/GetUser -d '{"id": 42}' ``` 服务端逻辑写一遍,两套 API 自动并存。 **对外 REST,对内 gRPC**。 ## connect-rpc:现代替代 `buf.build` 出的 `connect-go`: - 兼容 gRPC 协议 - 同时支持 REST + JSON + gRPC-Web(无需 grpc-gateway) - 浏览器直接调 - 比 grpc-gateway 简洁 ```go type UserService struct{} func (s *UserService) GetUser(ctx context.Context, req *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.User], error) { return connect.NewResponse(&userv1.User{Id: req.Msg.Id, Name: "..."}), nil } mux := http.NewServeMux() mux.Handle(userv1connect.NewUserServiceHandler(&UserService{})) http.ListenAndServe(":8080", mux) ``` ```bash curl -X POST http://localhost:8080/user.v1.UserService/GetUser \ -H 'Content-Type: application/json' \ -d '{"id": 42}' # {"id": 42, "name": "..."} ``` JSON over HTTP/1 / HTTP/2 / gRPC 同 endpoint 自动适配。 **2024 后新项目推荐**。 ## gRPC client(Go) ```go import "google.golang.org/grpc" conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials())) defer conn.Close() client := userpb.NewUserServiceClient(conn) resp, err := client.GetUser(ctx, &userpb.GetUserRequest{Id: 42}) fmt.Println(resp.Name) ``` 调 client 跟调本地函数一样。 ### 配置 keep-alive ```go conn, _ := grpc.Dial(addr, grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: 10 * time.Second, Timeout: 3 * time.Second, PermitWithoutStream: true, }), ) ``` NAT 后面长连接 ping 保活。 ### 连接池 gRPC 单 connection 默认多路复用(HTTP/2 stream)→ 一般不需要 pool。 高 QPS 时 `grpc.WithDefaultServiceConfig('{"loadBalancingPolicy":"round_robin"}')` + DNS 解析多 IP 自动负载均衡。 ## 错误处理 ```go // 服务端 return nil, status.Errorf(codes.NotFound, "user %d not found", id) // 客户端 resp, err := client.GetUser(...) if err != nil { if status.Code(err) == codes.NotFound { // handle 404 equivalent } } ``` gRPC 错误码标准化(NotFound / Unauthenticated / PermissionDenied / etc)。 比 REST 的"HTTP status + 自定义 body" 类型严格。 ## interceptor(middleware) ```go func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() resp, err := handler(ctx, req) log.Printf("%s took %v err=%v", info.FullMethod, time.Since(start), err) return resp, err } server := grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor)) ``` 或者用现成 middleware 库: - `github.com/grpc-ecosystem/go-grpc-middleware/v2` - prometheus / opentelemetry / auth / recovery / rate-limit ## 实战 case:微服务架构 我们的架构: ``` [mobile app / web] [外部 partner API] ↓ ↓ └─────── REST + JSON ───┘ ↓ [API Gateway (Go)] ↓ ┌─────────────┼─────────────┐ ↓ ↓ ↓ [User svc] [Order svc] [Payment svc] ↑ ↑ └─── gRPC ────┘ ``` - 对外 REST + OpenAPI 文档 - 内部 service 间 gRPC(性能 + 类型) - gateway 翻译 ## proto 仓库化 Monorepo / 单独 repo 存所有 .proto: ``` proto-repo/ ├── user/v1/user.proto ├── order/v1/order.proto └── buf.yaml ``` `buf` 工具 lint + 兼容性检查: ```bash buf lint buf breaking --against '.git#branch=main' # PR 不能破坏兼容 buf generate # 生成 Go / TS / Python ``` 防止"改 proto 删字段把客户端搞挂"。 ## 何时不用 gRPC - 客户端是浏览器 + 没 backend proxy → 用 REST / GraphQL(gRPC-Web 也是 选项但复杂) - 极简内部 tool → REST 更友好 - 团队不愿学 protobuf → 强推有阻力 ## 踩过的坑 1. **proto 字段 reserved**:删字段要 `reserved 5` 占位防 future wire-incompatible。 2. **enum 默认 0 值**:proto3 必须有 ENUM_UNSPECIFIED = 0;不写 迁移问题大。 3. **timestamp / duration 用 google.protobuf.Timestamp / Duration**: 不要用 int64 自己定义。Well-known types 跨语言兼容。 4. **streaming RPC 错误处理**:mid-stream error 客户端要正确退出。 ctx cancel + 关 stream。 5. **gRPC client 不复用 connection**:每次 NewClient 新 connection → DoS PG / DB。client 在应用启动时建一次 + 全局共享。

bat / eza / fd / dust / btop:把 6 个常用命令一次性现代化

## 起因 `cat` / `ls` / `find` / `du` / `top` / `df` 是 1970 年代的设计, 在终端不支持彩色 + 不假设大字符集时的产物。2020 后有一批 Rust / Go 写 的现代替代,UX 显著好。下面是我桌面 + 服务器都装的 6 个。 ## 1. bat 替代 cat ```bash sudo apt install bat # Debian/Ubuntu (有时命令叫 batcat) brew install bat # macOS ``` ```bash bat README.md # 显示行号 + 语法高亮 + git 改动 marker + 自动 less 分页 ``` 更进一步: ```bash bat src/*.py # 多文件 bat --diff README.md # 只显示有 git 改动的行 bat -A weird-file # 显示控制字符 / Unicode 不可见 bat --plain | less # 关掉装饰,纯 cat 行为 ``` 集成给其它工具用: ```bash export PAGER='bat --plain' # man 命令用 bat 当 pager alias cat='bat --plain --paging=never' # 替代 cat 但不分页 # 或保留 cat 原行为,bat 命令独立 ``` ## 2. eza(前 exa)替代 ls ```bash brew install eza cargo install eza # Debian: 二进制 release ``` ```bash eza -lah # 长格式 + 隐藏 + 友好大小 eza --tree -L 2 # 树状显示 eza -la --git # 加 git 状态列(M / A / ! / ?) eza -l --sort=size --reverse # 按大小排 eza --icons # 文件类型 icon(需 Nerd Font) ``` 我的 alias: ```bash alias ls='eza --icons --group-directories-first' alias ll='eza -lah --icons --group-directories-first --git' alias lt='eza --tree -L 3 --icons' ``` `--group-directories-first` 让目录排在前面,比标准 ls 更可读。 ## 3. fd 替代 find ```bash sudo apt install fd-find # 命令叫 fdfind brew install fd cargo install fd-find alias fd=fdfind # Debian 上 ``` ```bash # 找文件(自动 fuzzy + 默认尊重 .gitignore + 默认彩色) fd config # 在特定目录 fd config ~/projects # 按扩展名 fd -e py -e pyx # 找目录 fd -t d build # 找后执行 fd -e log -x rm # 删所有 .log fd -e py -x ruff check ``` 比 `find` 简洁太多: ```bash find . -name '*.py' -not -path './node_modules/*' # vs fd -e py # 自动跳过 .gitignore 里的 node_modules ``` fd 比 find 也快得多(并行)。 ## 4. dust 替代 du ```bash cargo install du-dust brew install dust ``` ```bash dust # 当前目录大小,树状显示 dust -d 3 # 限制深度 dust ~/projects # 指定目录 ``` 输出例: ``` 0B ┌── empty.txt 12M ├── data.json 512M ├─┬ logs │ └── 2024 1.2G ├─┬ node_modules │ ├── react ... 2.8G ┌── . (current) ``` 带 ASCII 条形图 + 自动按大小排,秒级看出"哪个目录吃磁盘"。 比 `du -h | sort -h` 直观 100 倍。 ## 5. duf 替代 df ```bash sudo apt install duf brew install duf ``` ```bash duf ``` 输出: ``` ╭─────────────────────────────────────────────────────────────────╮ │ 4 local devices │ ├─────────────────────────────────────────────────────────────────┤ │ MOUNTED ON SIZE USED AVAIL USE% TYPE FILESYSTEM │ │ / 456.5G 234.2G 198.3G 51% ext4 /dev/nvme0n1p2 │ │ /boot 1.0G 245M 755M 24% ext4 /dev/nvme0n1p1 │ │ /home 912.0G 423.1G 442.1G 46% ext4 /dev/sda2 │ ╰─────────────────────────────────────────────────────────────────╯ ``` 颜色 + 表格 + 进度条,一眼看出哪个分区危险。 ## 6. btop 替代 top ```bash sudo apt install btop brew install btop ``` ```bash btop ``` 全屏 TUI,比 top / htop 又好看又信息量大: - 上方 CPU 每核 + 占用进程 - 中部内存 + swap - 下方进程列表 + 网络 + 磁盘 IO - 鼠标 + 键盘都能交互 - 跨平台(Linux / macOS / FreeBSD) 按 `m` 切换显示模式,`+/-` 缩放面板,`q` 退出。 ## 一次性都装上 我的服务器初始化脚本: ```bash sudo apt update sudo apt install -y bat eza fd-find duf btop ripgrep zoxide fzf # Debian 上 fd 叫 fdfind,bat 叫 batcat echo 'alias fd=fdfind' >> ~/.bashrc echo 'alias bat=batcat' >> ~/.bashrc # 或一个 cargo 大餐: cargo install bat eza fd-find du-dust ripgrep starship zoxide ``` ## 工具对照表 | 老 | 新 | 主要优点 | |---|---|---| | `cat` | `bat` | 语法高亮、行号、git 改动 | | `ls` | `eza` | 颜色、git、icon、tree | | `find` | `fd` | 简洁、快、尊重 .gitignore | | `grep` | `rg` | 快 10x、跳过 .git、smart-case | | `du` | `dust` | 直观、条形图 | | `df` | `duf` | 表格、颜色 | | `top` | `btop` | 信息更全、好看 | | `cd` | `z` | 模糊跳 | | `man` | `tldr` | 例子优先而非全文档 | | `make` | `just` | 简洁、跨平台 | ## tldr 顺便提一下 ```bash brew install tldr 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 # ... ``` 不需要看 200 行 man,直接给你常用的几条示例。 ## 效果 - 日常 ls/cat 命令视觉信息量翻倍,找东西更快 - "哪个目录占空间" 类问题秒回(dust) - 服务器 troubleshoot 三件套:`btop` + `dust` + `duf` 一屏诊断 - 新机器 5 分钟初始化完所有工具 - 同事被安利后没人愿意回去用原生 ls ## 踩过的坑 1. **Debian 命令名前缀 fd-find / batcat**:跟其它工具冲突历史原因。 alias 一行解决,但脚本里调用要小心,别在脚本里依赖 alias (脚本默认 non-interactive 不读 alias)。 2. **服务器没有 Nerd Font 装不了 icon**:`eza --icons` 显示豆腐块。 服务器上去掉 `--icons` 即可(仍有颜色)。 3. **bat 默认 pager 是 less**:`less -R` 才能正确显示 ANSI。 `export BAT_PAGER='less -R'` 显式设。 4. **fd 不显示隐藏文件**:默认尊重 `.gitignore` + 隐藏文件。 `fd -H` 显示隐藏;`fd -I` 忽略 ignore 规则;`fd -HI` 全开。 5. **btop CPU 占用高**:默认刷新 2Hz,加 `--update 5` 改 5Hz 不影响判断 但更稳。

Envoy 当 API gateway:替代 nginx 的现代选择

## 起因 微服务架构需要 API gateway 做: - 路由(path → service) - LB - 限流 / 熔断 - TLS termination - 鉴权(OIDC / API key) - observability (metrics / traces) 老办法 nginx + Lua / openresty,扩展靠 module / 第三方 script。 **Envoy**(Lyft 出,CNCF)是云原生 API gateway 标准: - xDS API 动态配置(控制面 push 配置) - 内置 metric / trace - HTTP/2 / HTTP/3 / gRPC 一等公民 - Istio / Consul Service Mesh 底层用它 ## 装 / 跑 ```bash docker run -d -p 10000:10000 \ -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml \ envoyproxy/envoy:v1.30.0 ``` ## 最小配置 ```yaml # envoy.yaml static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 10000 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http route_config: virtual_hosts: - name: backend domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: backend_service } http_filters: - name: envoy.filters.http.router clusters: - name: backend_service type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: backend_service endpoints: - lb_endpoints: - endpoint: address: socket_address: { address: backend, port_value: 8080 } ``` 监听 10000,转发到 backend:8080。 ## 路由 ```yaml routes: - match: { prefix: "/api/users" } route: { cluster: user_service } - match: { prefix: "/api/orders" } route: { cluster: order_service } - match: prefix: "/admin" headers: - name: x-internal-tool string_match: { exact: "true" } route: { cluster: admin_service } - match: { prefix: "/" } route: { cluster: web_frontend } ``` prefix / path / regex / header / query 匹配,路由到不同 cluster。 ## LB 策略 ```yaml clusters: - name: api_service lb_policy: LEAST_REQUEST # ROUND_ROBIN / LEAST_REQUEST / RANDOM / RING_HASH load_assignment: endpoints: - lb_endpoints: - endpoint: { address: { socket_address: { address: api-1, port_value: 8080 }}} - endpoint: { address: { socket_address: { address: api-2, port_value: 8080 }}} ``` `LEAST_REQUEST` 比 round-robin 更智能(少请求的 instance 接新请求)。 ## 限流 ```yaml http_filters: - name: envoy.filters.http.local_ratelimit typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit stat_prefix: http_local_rate_limiter token_bucket: max_tokens: 100 tokens_per_fill: 100 fill_interval: 1s filter_enabled: runtime_key: local_rate_limit_enabled default_value: { numerator: 100 } filter_enforced: runtime_key: local_rate_limit_enforced default_value: { numerator: 100 } ``` 每秒 100 req,超出 429。 或者全局限流(多 envoy 共享)→ 外接 rate limit service(如 lyft/ratelimit)。 ## 熔断 ```yaml clusters: - name: backend circuit_breakers: thresholds: - max_connections: 100 - max_pending_requests: 50 - max_requests: 100 - max_retries: 3 ``` backend 慢 / 死 → envoy 主动拒新请求(避免雪崩)。 ## retry ```yaml routes: - match: { prefix: "/" } route: cluster: backend retry_policy: retry_on: "5xx,reset,connect-failure" num_retries: 3 per_try_timeout: 5s ``` 5xx / connection reset 自动重试 3 次。 对 idempotent 请求安全;POST / 写要小心。 ## TLS ```yaml listeners: - address: socket_address: { address: 0.0.0.0, port_value: 443 } filter_chains: - transport_socket: name: envoy.transport_sockets.tls typed_config: common_tls_context: tls_certificates: - certificate_chain: { filename: /etc/certs/cert.pem } private_key: { filename: /etc/certs/key.pem } ``` ALPN / SNI 自动。HTTP/2 自动协商。 HTTP/3 / QUIC 也支持(额外配 `udp_listener_config`)。 ## 鉴权:OAuth2 / JWT ```yaml http_filters: - name: envoy.filters.http.jwt_authn typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication providers: my_provider: issuer: https://auth.example.com remote_jwks: http_uri: uri: https://auth.example.com/.well-known/jwks.json cluster: jwks_cluster timeout: 5s cache_duration: 600s forward: true rules: - match: { prefix: "/api" } requires: { provider_name: my_provider } ``` envoy 自动验 JWT,无效 401。验过的 claim 转发给 backend。 backend 不必再写 JWT 验逻辑。 ## ext_authz(外部鉴权 service) 复杂鉴权(动态 ACL / per-resource permission)→ envoy 调外部 service: ```yaml http_filters: - name: envoy.filters.http.ext_authz typed_config: grpc_service: envoy_grpc: cluster_name: ext_authz_cluster ``` 每请求 envoy → gRPC 调 authz service → allow/deny。 集中 policy decision,service 不写 authz。 ## observability ```yaml # admin endpoint admin: address: socket_address: { address: 0.0.0.0, port_value: 9901 } ``` `http://localhost:9901/stats` → 几百个 metric。 `http://localhost:9901/clusters` → 当前 cluster 状态。 Prometheus scrape `/stats/prometheus`。 ## tracing ```yaml tracing: http: name: envoy.tracers.opentelemetry typed_config: grpc_service: envoy_grpc: cluster_name: otel_collector service_name: my-gateway ``` 每请求自动产生 span,发 OTEL collector → Jaeger / Tempo。 backend trace 跟 gateway 串联。 ## 动态配置(xDS) 静态 envoy.yaml 改配置要重启。 xDS API 让 envoy 从 control plane(Istio / Consul / 自家)pull 配置: ```yaml dynamic_resources: ads_config: api_type: GRPC grpc_services: - envoy_grpc: cluster_name: xds_cluster ``` router / cluster / listener 全运行时改,无中断。 大规模 / 多 envoy 必备。 ## vs nginx | | nginx | Envoy | |---|---|---| | 配置 | nginx.conf(声明式) | yaml + xDS | | 动态更新 | reload(断老连接) | xDS 真 hot reload | | HTTP/2 / 3 | 支持但 retrofit | first-class | | gRPC | 透传 | 原生(gRPC routing) | | observability | 第三方 | 内置 metric + trace | | 学习曲线 | 中 | 高(yaml 长) | | 资源 | 小 | 中 | 简单场景 nginx 够。微服务 + mesh / 动态路由 / gRPC → Envoy。 ## 与 API gateway product 对比 - **Kong**:基于 nginx + 插件 - **Tyk**:Go 写,open source - **APISIX**:基于 nginx + etcd - **Envoy + Istio**:service mesh 体系 Envoy 是底层 + 通用。Kong/APISIX/Tyk 是产品(envoy 上层封装)。 要 admin UI / 插件市场 → API gateway 产品。 完全自控 → envoy 自配。 ## 真实部署 我们一个 k8s 集群,envoy 当 ingress(替代 nginx-ingress): - 200 微服务 - 10w QPS 峰值 - envoy 2 replica(HPA 到 8) - xDS 控制面 = Istio + 自家 control plane 效果: - 配置变更 1 秒 propagate(vs nginx reload 几秒中断) - gRPC 路由原生(vs nginx 需要插件) - per-service mTLS 自动(mesh 模式) ## 踩过的坑 1. **yaml 配置长**:1000+ 行常见。用 helm / kustomize 模板化。 2. **memory 使用**:默认 envoy 几百 MB;大 route 表几 GB。监控 + adjust。 3. **JWKS cache miss**:JWT auth 频繁拉 JWKS endpoint → IdP rate limit。 `cache_duration: 600s` 加大。 4. **route order**:从上到下匹配第一个。`/` prefix 放最前 → 后面 都不生效。常见错。 5. **TLS cert reload**:file 改了 envoy 不自动 reload。SDS (Secret Discovery Service) 动态推或者用 cert-manager + restart envoy pod。

SQLite 启用 WAL 模式 + 调几个 pragma 让并发写不再串行

SQLite 默认 journal_mode 是 `delete`,写操作要拿整库锁,多写者 直接互相 block。改 WAL(Write-Ahead Logging)后: - 读不阻塞写 - 写不阻塞读 - 多个读者并发 - 单写者(这个仍是 SQLite 的硬限制) 对中小型应用(< 10k QPS)足够把它当生产数据库用。 ## 启用 WAL ```python import sqlite3 conn = sqlite3.connect('app.db') conn.execute('PRAGMA journal_mode = WAL;') conn.execute('PRAGMA synchronous = NORMAL;') conn.execute('PRAGMA busy_timeout = 5000;') conn.execute('PRAGMA cache_size = -64000;') # 64MB cache conn.execute('PRAGMA foreign_keys = ON;') conn.execute('PRAGMA temp_store = MEMORY;') conn.execute('PRAGMA mmap_size = 134217728;') # 128 MB mmap ``` 各 pragma 含义: | pragma | 作用 | 建议值 | |---|---|---| | `journal_mode = WAL` | 改为 Write-Ahead Logging | WAL | | `synchronous = NORMAL` | WAL 同步级(默认 FULL 更安全但慢) | NORMAL | | `busy_timeout` | 等锁的毫秒数(超时返回 SQLITE_BUSY) | 5000 | | `cache_size` | 负数表示 KB,正数表示页数;建议负数 | -64000 (64MB) | | `foreign_keys` | 默认 **关闭** —— 必须显式开 | ON | | `temp_store` | 临时表放内存还是磁盘 | MEMORY | | `mmap_size` | 用 mmap 而不是 read() 读 | 128MB | 每次新连接都要重设这些 pragma(除了 journal_mode 是持久化的)。 ## WAL 后会出现的文件 ``` app.db app.db-wal # WAL log app.db-shm # 共享内存映射 ``` 备份这 3 个文件都要在一致快照里,最好用 `VACUUM INTO` 或 SQLite Online Backup API。 不要简单 `cp app.db backup.db`,可能拿到不一致状态。 ```python # 一致备份: src = sqlite3.connect('app.db') dst = sqlite3.connect('backup.db') with dst: src.backup(dst) ``` ## checkpoint:把 WAL 合并回主库 WAL 不断追加,会越来越大。SQLite 默认 1000 页时自动 checkpoint, 但繁忙系统可能赶不上。手动控制: ```python conn.execute('PRAGMA wal_autocheckpoint = 1000;') # 默认值 conn.execute('PRAGMA wal_checkpoint(PASSIVE);') # 立刻做一次(不阻塞读写) conn.execute('PRAGMA wal_checkpoint(TRUNCATE);') # 完全清空 WAL(要短暂独占) ``` 低峰期跑一次 `TRUNCATE` 比较好。 ## 真实并发测试 ```python # 5 个线程并发读 + 1 个线程写 import threading, sqlite3, time DB = 'test.db' def setup(): c = sqlite3.connect(DB) c.execute('PRAGMA journal_mode = WAL') c.execute('CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v TEXT)') for i in range(10000): c.execute('INSERT INTO t (v) VALUES (?)', (f'val{i}',)) c.commit(); c.close() setup() def reader(): c = sqlite3.connect(DB) c.execute('PRAGMA busy_timeout = 5000') for _ in range(1000): c.execute('SELECT count(*) FROM t').fetchone() c.close() def writer(): c = sqlite3.connect(DB) c.execute('PRAGMA busy_timeout = 5000') for i in range(1000): c.execute('INSERT INTO t (v) VALUES (?)', (f'w{i}',)) c.commit() c.close() threads = [threading.Thread(target=reader) for _ in range(5)] + [threading.Thread(target=writer)] t0 = time.time() for t in threads: t.start() for t in threads: t.join() print(f'done in {time.time()-t0:.2f}s') ``` WAL 模式比 delete 模式通常快 3-10 倍(取决于 IO)。 ## SQLITE_BUSY 的根因 + 处理 WAL 模式下还可能 BUSY: 1. **两个写者同时持有写锁**:第二个收到 BUSY。`busy_timeout` 让 SQLite 自动重试 5 秒 2. **DDL 时被读者卡住**:CREATE / ALTER 需要独占,正在 SELECT 的连接会阻塞 DDL 3. **WAL checkpoint(TRUNCATE) 时被读者卡住**:同上 应用层正确做法:捕获 `sqlite3.OperationalError` 重试,或者用 `busy_timeout` 让驱动层重试。 ## Django 配置 ```python # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;', 'timeout': 20, # busy_timeout (秒) }, } } ``` 或用一个 connection signal 设更多 pragma: ```python from django.db.backends.signals import connection_created def _set_pragmas(sender, connection, **kwargs): if connection.vendor == 'sqlite': with connection.cursor() as cur: cur.execute('PRAGMA journal_mode=WAL;') cur.execute('PRAGMA synchronous=NORMAL;') cur.execute('PRAGMA temp_store=MEMORY;') cur.execute('PRAGMA cache_size=-64000;') cur.execute('PRAGMA mmap_size=134217728;') connection_created.connect(_set_pragmas) ``` ## 什么时候放弃 SQLite - 写 QPS > 1000:单写者是瓶颈 - 多机器需要共享 DB:SQLite 是单机的 - 字段需要 jsonb 索引等高级 PG 特性 - 复杂查询性能:SQLite 优化器较 PG 简单 否则 SQLite 比 PG/MySQL 部署简单一个数量级,单文件备份,绝对值得。 ## 踩过的坑 - WAL 文件不被压缩;WAL 不 checkpoint 时膨胀到几 GB 还在变大。监控 `app.db-wal` 大小,超过 100MB 跑一次 `PRAGMA wal_checkpoint(TRUNCATE)`。 - NFS / SMB 上跑 SQLite **绝对不行**——锁机制基于 fcntl,网络文件系统的 fcntl 不可靠,会有静默数据损坏。 - `PRAGMA foreign_keys = ON` 每个连接都要单独设。Django 默认设了; 自己 sqlite3.connect() 时容易漏。 - 多进程(如 gunicorn 多 worker)共享 SQLite 没问题,但每个 worker 自己的连接都要设 pragma。

Radix UI Primitives:a11y 严谨的"无样式"组件库

## 起因 要做一个 a11y 合规的 dropdown menu:键盘 navigation / focus trap / ARIA roles / Escape 关闭 / 点外面关闭 / 上下方向键循环 / Home/End 键 跳第一项末项 / 输入字符直接跳到对应项 / ... 每个细节单独写都 30-50 行 JS。完全无 bug 实施一个 dropdown 要 1 天。 不抽象怎么办——5 个组件就要 1 周。 Radix UI Primitives 把这些"a11y 行为正确" 的组件做成了 React component, **不带样式**,你用 Tailwind / CSS / 任何 styling 方案套外观。 ## 装 ```bash npm i @radix-ui/react-dropdown-menu npm i @radix-ui/react-dialog npm i @radix-ui/react-tooltip npm i @radix-ui/react-popover # 每个组件独立 package,按需装 ``` 或者一次性装常用的: ```bash npm i @radix-ui/themes # 带默认主题的全套 ``` ## Dropdown Menu 例子 ```tsx import * as DropdownMenu from '@radix-ui/react-dropdown-menu' function UserMenu() { return ( <DropdownMenu.Root> <DropdownMenu.Trigger asChild> <button>菜单 ↓</button> </DropdownMenu.Trigger> <DropdownMenu.Portal> <DropdownMenu.Content className="bg-white shadow-lg rounded-md p-1 min-w-[180px]" sideOffset={4} > <DropdownMenu.Item className="px-3 py-2 rounded hover:bg-gray-100 outline-none cursor-pointer" onSelect={() => navigate('/profile')} > 个人主页 </DropdownMenu.Item> <DropdownMenu.Item className="px-3 py-2 rounded hover:bg-gray-100" onSelect={() => navigate('/settings')} > 设置 </DropdownMenu.Item> <DropdownMenu.Separator className="h-px bg-gray-200 my-1" /> <DropdownMenu.Item className="px-3 py-2 rounded hover:bg-red-50 text-red-600" onSelect={logout} > 退出登录 </DropdownMenu.Item> </DropdownMenu.Content> </DropdownMenu.Portal> </DropdownMenu.Root> ) } ``` **没写任何 keyboard handler / focus trap / ARIA**,但 Radix 内部 全做好了: - ↑↓ 切换项目 - Home / End 跳首末 - Enter 选中 - Escape 关闭 - 点外面关闭 - 输入字符跳到对应项 - aria-expanded / aria-haspopup / aria-orientation 等 ARIA 全配 - focus 进入 / 离开管理 - screen reader 报"opened menu, 3 items, item 1 of 3" 业务代码只关心"我有哪些菜单项 + 点击做什么"。 ## Dialog 例子(合规 modal) ```tsx import * as Dialog from '@radix-ui/react-dialog' function DeleteConfirm({ onDelete }) { return ( <Dialog.Root> <Dialog.Trigger asChild> <button className="btn-danger">删除</button> </Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-black/50" /> <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg max-w-md"> <Dialog.Title className="text-lg font-bold">确认删除</Dialog.Title> <Dialog.Description className="text-sm text-gray-500 mt-1"> 此操作不可撤销。 </Dialog.Description> <div className="mt-4 flex gap-2 justify-end"> <Dialog.Close asChild> <button>取消</button> </Dialog.Close> <button onClick={onDelete} className="btn-danger">删除</button> </div> </Dialog.Content> </Dialog.Portal> </Dialog.Root> ) } ``` 内置: - focus trap(Tab 不出对话框) - ESC 关闭 - 点 overlay 关闭 - 自动 focus 第一可聚焦元素 - 关闭后焦点回到 trigger - 背景 inert / aria-hidden 屏蔽 - title / description 自动关联到对话框 aria-labelledby / aria-describedby 合规模态 5 行配置。 ## Tooltip / Popover ```tsx import * as Tooltip from '@radix-ui/react-tooltip' <Tooltip.Provider delayDuration={200}> <Tooltip.Root> <Tooltip.Trigger asChild> <button>?</button> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content className="bg-black text-white p-2 rounded text-sm" sideOffset={4}> 这是一段提示 <Tooltip.Arrow className="fill-black" /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> ``` Tooltip 之类需要全局 Provider 控制 hover delay 一致。 ## Accordion ```tsx import * as Accordion from '@radix-ui/react-accordion' <Accordion.Root type="single" collapsible className="w-[400px]"> <Accordion.Item value="item-1"> <Accordion.Header> <Accordion.Trigger className="w-full text-left"> 什么是 React Hooks? </Accordion.Trigger> </Accordion.Header> <Accordion.Content> Hooks 是 React 16.8 引入的特性... </Accordion.Content> </Accordion.Item> <Accordion.Item value="item-2"> ... </Accordion.Item> </Accordion.Root> ``` `type='single'` 同时只展开一个;`type='multiple'` 多个。 键盘 Tab / Space / Enter 展开收起,箭头切项。 ## Switch / Checkbox / RadioGroup ```tsx import * as Switch from '@radix-ui/react-switch' <Switch.Root className="w-11 h-6 bg-gray-300 rounded-full relative data-[state=checked]:bg-blue-500" checked={isDark} onCheckedChange={setIsDark} > <Switch.Thumb className="block w-5 h-5 bg-white rounded-full shadow translate-x-0.5 data-[state=checked]:translate-x-5 transition" /> </Switch.Root> ``` 完整 keyboard support + ARIA + 适合 screen reader 的 label 关联。 ## asChild 模式 很多 Radix 组件支持 `asChild`: ```tsx <DropdownMenu.Trigger asChild> <button>Click</button> {/* 把 trigger 的行为应用到这个 button */} </DropdownMenu.Trigger> // vs 默认(包一层 button) <DropdownMenu.Trigger>Click</DropdownMenu.Trigger> ``` `asChild` 让你用自己的元素(甚至自定义 styled component)作为 trigger, 不强制额外 DOM。React Aria / Headless UI 也类似。 ## 跟 shadcn/ui 关系 shadcn/ui = "Radix Primitives + Tailwind 样式 + 复制粘贴" 的预设。 直接: ```bash npx shadcn-ui@latest add dropdown-menu dialog tooltip ``` 代码进你的 `components/ui/`,底层就是 Radix。 省了"自己包样式" 这一步。 ## 跟 Headless UI / React Aria 对比 | | Radix UI | Headless UI | React Aria (Adobe) | |---|---|---|---| | 风格 | 较细粒度 primitive | 简洁 | 极完整(也最重) | | 组件数 | ~30 | 10+ | ~50 | | Tailwind 友好 | ✅ | ✅(同公司) | 中性 | | 学习曲线 | 中 | 低 | 中-高 | | a11y 严谨度 | 高 | 高 | 极高 | | bundle | 中(按需) | 小 | 中-大 | shadcn 默认用 Radix;个人偏好用 Headless UI(Tailwind 同公司)也很 好。 ## 实际效果 我们一个 React app 把所有 dialog / dropdown / popover / tooltip 改 Radix: - 之前自己实现 + 半成品 → bug 多 + 不一致 - 改 Radix 后 a11y 自动达标(axe-core lint 0 violation) - bundle 增加 ~30KB(5-6 个 primitive) - 团队 review focus 从"键盘怎么操作" → 只看业务逻辑 ## 几个建议 1. **不要从 Radix 自己包 100 个 wrapper**:用 shadcn 把 Radix + Tailwind 集成代码丢进自己 repo 直接改更灵活 2. **状态管理交给 Radix**:除非需要受控 (controlled mode),让 Radix 内部管 `open` state 3. **portal 默认开**:菜单 / dialog 进 portal 避免 overflow / z-index 父元素影响 4. **className 接受**:所有 primitive 都接 className + style, 尽情用 Tailwind / CSS Modules ## 踩过的坑 1. **DropdownMenu 不能 nested**:嵌套菜单要用 `DropdownMenu.Sub`, 不是再嵌一个 Root。 2. **Dialog 内嵌 Tooltip 不显示**:z-index / portal 父子关系问题。 Tooltip 也 portal 出去。 3. **`asChild` + Link**:React Router `<Link>` 已经是 `<a>`, 配 `asChild` 时不要重复包 `<a>`。 4. **server side rendering**:Radix 用 useId / 假 portal,SSR 时 小心 hydration mismatch。包 dynamic import / suppress hydration warning。 5. **bundle 一个 component 拉一堆 dep**:每个 Radix component 拉 `@radix-ui/react-primitive` 等 shared util。多组件用同样 base tree-shake 后实际不重复,但单独看 size 比想象大。

Ray:把 Python 函数变分布式(不学 Spark)

## 起因 需要分布式跑 Python: - ML 训练(多 GPU 多机) - 批 inference(百万张图过 model) - hyperparameter sweep(试 1000 个组合) - 数据预处理(pandas 不够大) 老办法: - spark:scala-friendly,PySpark 写得别扭,启动重 - multiprocessing:单机 - celery:任务队列,不为 compute 设计 `Ray`(UC Berkeley 出,2018+)让 Python 函数加 `@ray.remote` 即分布式。 轻量 + Python 原生。 ## 装 ```bash pip install ray ``` ## 本地用 ```python import ray @ray.remote def square(x): return x * x ray.init() futures = [square.remote(i) for i in range(100)] results = ray.get(futures) # 等结果 print(results) ``` `square.remote(i)` 异步丢 task,返回 future。 `ray.get()` 阻塞拿结果。 本地多核就用多核(自动)。 ## actor (持久 state) ```python @ray.remote class Counter: def __init__(self): self.count = 0 def inc(self): self.count += 1 return self.count c = Counter.remote() c.inc.remote() # 异步调 c.inc.remote() print(ray.get(c.inc.remote())) # 3 ``` actor 跑在某 worker 上,state 持久。多 caller 共享。 适合:load 一个大 model 在 actor 里,多个 request 调它(避免重复加载)。 ## 多机集群 启动 head node: ```bash ray start --head --port=6379 ``` worker node 加入: ```bash ray start --address='head_node:6379' ``` 代码不变: ```python ray.init(address='auto') # 连接 cluster ``` `square.remote` 自动 schedule 到任意 worker。 ## 资源 / GPU 请求 ```python @ray.remote(num_cpus=2, num_gpus=1) def train(data): # 跑 GPU 训练 ... ``` Ray scheduler 把 task 放到有 GPU + 2 CPU 的 worker 上。 不够资源 → 排队等。 ## 数据:Ray Data ```python import ray ds = ray.data.read_parquet('s3://bucket/data/*.parquet') # 分布式 map processed = ds.map(lambda r: {**r, 'doubled': r['x'] * 2}) # 分布式 group + agg result = processed.groupby('country').sum() result.write_parquet('s3://bucket/out/') ``` 类似 spark RDD / dataframe,但 Python 原生。 ## ML:Ray Train ```python from ray.train.torch import TorchTrainer from ray.train import ScalingConfig def train_func(config): model = ... for epoch in range(config['epochs']): ... trainer = TorchTrainer( train_func, scaling_config=ScalingConfig(num_workers=4, use_gpu=True), train_loop_config={'lr': 1e-3, 'epochs': 10}, ) result = trainer.fit() ``` 4 GPU 分布式 train,PyTorch DDP 自动配置。 ## ML:Ray Tune (hyperparameter) ```python from ray import tune def trainable(config): score = train_model(config['lr'], config['dropout']) tune.report(score=score) tuner = tune.Tuner( trainable, param_space={ 'lr': tune.loguniform(1e-5, 1e-2), 'dropout': tune.uniform(0, 0.5), }, tune_config=tune.TuneConfig(num_samples=100), ) results = tuner.fit() ``` 跑 100 个 trial 在集群 → 自动 schedule。 内置 HyperBand / Population-Based Training 等算法。 ## ML:Ray Serve (推理 server) ```python from ray import serve @serve.deployment(num_replicas=4, ray_actor_options={'num_gpus': 1}) class Predictor: def __init__(self): self.model = load_model() async def __call__(self, request): data = await request.json() return self.model.predict(data) serve.run(Predictor.bind()) ``` 4 replica 在 4 个 GPU,前面 router 自动 LB。 比手写 FastAPI + load model worker 简单。 ## 与 spark 对比 | | Ray | Spark | |---|---|---| | 语言 | Python first | Scala / Java first | | 数据 size | < 10 TB | PB | | ML 集成 | 强(Ray Train/Tune/Serve) | 中(MLlib) | | 启动 | < 5s | 30s+ | | 心智模型 | Python actor / task | RDD / dataframe | | 生态 | ML / RL / serving | 通用大数据 | - 数据 < 10 TB + Python 重 → Ray - 数据 > 10 TB + Java/Scala 团队 → Spark - ML + 分布式训练 → Ray - ETL 跑数仓 → Spark / dbt ## 与 dask 对比 dask 也是 Python 分布式。 | | Ray | dask | |---|---|---| | API | actor + task | array / dataframe / bag | | ML | Ray Train/Tune/Serve | dask-ml(弱) | | 资源 model | 强(GPU / 自定义) | 中 | | 通用 compute | 是 | 是 | dask 更"分布式 pandas/numpy",Ray 更"分布式 Python compute + ML"。 ML 选 Ray;纯数据 ETL 选 dask 或 polars streaming。 ## 一个 case:分布式批 inference 需求:1 亿张图过 vision model。 ```python import ray import torch ray.init(address='auto') # cluster 8 GPU node @ray.remote(num_gpus=1) class Inferencer: def __init__(self): self.model = torch.load('model.pt').cuda().eval() @torch.no_grad() def predict(self, batch): return self.model(batch.cuda()).cpu() # 4 actor 4 GPU predictors = [Inferencer.remote() for _ in range(4)] # stream 数据 ds = ray.data.read_images('s3://bucket/images/').map_batches(preprocess, batch_size=64) # 分布式调 results = ds.map_batches( lambda batch: ray.get(predictors[0].predict.remote(batch)), # 简化 compute=ray.data.ActorPoolStrategy(size=4), ) results.write_parquet('s3://bucket/predictions/') ``` 8 GPU 跑满 → 1 亿张图几小时跑完。 ## 监控 ray dashboard 8265 端口默认开: - 集群资源使用 - task / actor 状态 - log 聚合 - profiler 调试比 spark UI 友好。 ## 部署 - **KubeRay**:k8s operator 部署 Ray cluster - **Anyscale**:Ray 公司的托管服务 - **手动**:ssh 到每 node `ray start` prod 用 KubeRay 多。 ## 踩过的坑 1. **object size 大**:ray 传 object 用 plasma store,几 GB object 序列化贵。共享大 dataset 用 `ray.put(data)` 一次 + 引用。 2. **import 依赖差**:worker node 没装跟 head 一样的 Python package → import error。runtime_env 指定 deps,或者 docker image 统一。 3. **GPU 假资源**:`num_gpus=1` 只是 Ray scheduler 视角,task 内部 仍要 `cuda()` 实际用。配错 → 多 task 抢同 GPU。 4. **schedule 不平衡**:data skew → 某 worker 一直累。 `ray.data.repartition()` 重新均衡。 5. **Ray 版本兼容**:head + worker Ray 版本必须一致。docker image pin 版本。

RabbitMQ vs Kafka 选型 + 各自最小可用部署

消息队列两个最主流:RabbitMQ(AMQP)和 Kafka(log-based)。设计哲学 完全不同,选错会很疼。 ## 一句话区分 - **RabbitMQ**:消息**任务**,消费后即删,适合"事件触发处理" - **Kafka**:消息**日志**,消费后保留,适合"事件流回放 / 多消费者" ## 详细对比 | 维度 | RabbitMQ | Kafka | |---|---|---| | 数据模型 | queue + exchange | partitioned log | | 消息 retention | 消费后删 | 时间 / 大小(默认 7 天) | | 顺序保证 | 单 queue | 单 partition | | 多消费者 | competing consumers(争抢) | consumer group(offset 独立) | | 持久化 | 可选 | 总是 | | 单 broker 吞吐 | 1k-50k msg/s | 100k-1M msg/s | | 重试 / 死信 | 内置 DLX | 自己处理 | | 路由 | 灵活(topic / fanout / direct / headers) | 简单(topic + partition key) | | 协议 | AMQP / STOMP / MQTT | 自家二进制 | | 复杂度 | 中 | 高 | ## 何时选 RabbitMQ - "用户注册了,发欢迎邮件"——单次任务,一个消费者处理 - 任务队列(Celery 用的就是 RabbitMQ / Redis) - RPC over message - 中等吞吐(< 50k msg/s) ## 何时选 Kafka - "用户做了 N 个动作,多个下游各自处理"——同一消息多消费者 - 事件流 / 实时分析(订单流 → 多个团队订阅) - 日志聚合管道(应用日志 → Kafka → Logstash / ELK) - 大吞吐(> 100k msg/s) ## RabbitMQ 最小部署 ```bash docker run -d --hostname rabbit \ --name rabbit -p 5672:5672 -p 15672:15672 \ rabbitmq:4-management ``` Web 管理界面 http://localhost:15672 默认 guest/guest。 ### 发 / 收(Python pika) ```bash uv add pika ``` ```python # producer.py import pika conn = pika.BlockingConnection(pika.ConnectionParameters('localhost')) ch = conn.channel() ch.queue_declare(queue='tasks', durable=True) for i in range(100): ch.basic_publish( exchange='', routing_key='tasks', body=f'task {i}'.encode(), properties=pika.BasicProperties(delivery_mode=2), # persistent ) conn.close() ``` ```python # consumer.py import pika def callback(ch, method, props, body): print(f'got: {body!r}') # 假装处理 ch.basic_ack(delivery_tag=method.delivery_tag) conn = pika.BlockingConnection(pika.ConnectionParameters('localhost')) ch = conn.channel() ch.queue_declare(queue='tasks', durable=True) ch.basic_qos(prefetch_count=1) # 一次只抓一条,处理完才下一条 ch.basic_consume(queue='tasks', on_message_callback=callback) ch.start_consuming() ``` 启动多个 consumer:消息在它们之间分配(competing consumers)。 ### 死信队列(DLX) 任务失败重试几次后丢进 DLQ 人工检查: ```python ch.queue_declare( queue='tasks', durable=True, arguments={ 'x-dead-letter-exchange': '', 'x-dead-letter-routing-key': 'tasks-dlq', 'x-message-ttl': 30000, # 30 秒处理超时 'x-max-length': 100000, # 队列上限 }, ) ch.queue_declare(queue='tasks-dlq', durable=True) ``` ## Kafka 最小部署 Kafka 4.x KRaft 模式(不再需要 ZooKeeper): ```yaml # docker-compose.yml services: kafka: image: apache/kafka:3.8.0 ports: [ "9092:9092", "9093:9093" ] environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' ``` ```bash docker compose up -d ``` ### 发 / 收(Python kafka-python) ```bash uv add kafka-python ``` ```python # producer.py from kafka import KafkaProducer import json p = KafkaProducer( bootstrap_servers='localhost:9092', value_serializer=lambda v: json.dumps(v).encode(), acks='all', # 等所有副本确认(最安全) ) for i in range(100): p.send('events', value={'id': i, 'type': 'click'}) p.flush() ``` ```python # consumer.py from kafka import KafkaConsumer c = KafkaConsumer( 'events', bootstrap_servers='localhost:9092', group_id='analytics', auto_offset_reset='earliest', # 从头开始(如果是新 group) value_deserializer=lambda v: json.loads(v.decode()), enable_auto_commit=False, ) for msg in c: print(f'partition={msg.partition} offset={msg.offset} value={msg.value}') # 处理... c.commit() # 手动 commit offset,处理失败下次重读 ``` ### 多消费组 ```python KafkaConsumer('events', group_id='analytics', ...) KafkaConsumer('events', group_id='billing', ...) KafkaConsumer('events', group_id='email', ...) ``` 三个 group 独立从头消费同一 topic —— 这是 Kafka 的核心能力。 ### partition + 顺序 ```python p.send('events', key=b'user-42', value=...) # 同一 key 总是去同一 partition → 同一 user 的事件保证顺序 ``` partition 数决定并行度上限:一个 partition 同时只能被同 group 内一个 consumer 消费。 ## 运维差异 **RabbitMQ**:装在一台机器开心用,复杂集群 erlang 配很麻烦 **Kafka**:单机能跑但生产至少 3 broker;K8s 用 Strimzi operator 简化 ## 监控 **RabbitMQ**:内置 Web UI + Prometheus exporter **Kafka**:Kafka Manager / Conduktor / Kowl / kafka_exporter ## 替代 / 衍生 - **NATS / JetStream**:超轻量 message + streaming,Go 写的 - **Pulsar**:Kafka + RabbitMQ 优点合一,复杂度也合一 - **Redis Streams**:Redis 内置 stream,5.x+,比 Kafka 简单 50x - **AWS SQS / Google PubSub**:托管,省运维 ## 踩过的坑 - RabbitMQ 没设 `prefetch_count` → 一个 consumer 抓走全部消息, 其它 consumer 闲着。设 prefetch=1 或较小数。 - Kafka offset 自动 commit + 处理失败:消息已经"算消费过了",重启后丢失。 生产用手动 commit。 - Kafka 一开始 partition 数定得太少:扩 partition 复杂(顺序 / rebalance)。 规划时按峰值并发 × 2-3 倍。 - 把 RabbitMQ 当 log 用(永久保留消息):会胀死 broker。换 Kafka。

用 Web Worker 把 CPU 密集计算移出主线程(不卡 UI)

任何在主线程跑超过 50ms 的 JS 都会让交互掉帧。常见 culprit: - 大 JSON parse / stringify - markdown 渲染、syntax highlight - 客户端排序 / 过滤万条数据 - 图像 / 视频 / WASM 解码 正确做法:放 Web Worker。下面是从零到 production 的最小路径。 ## 1. 经典 Worker(自己管字符串) ```js // main.js const worker = new Worker('/worker.js') worker.onmessage = e => console.log('got:', e.data) worker.postMessage({ type: 'sort', payload: bigArray }) ``` ```js // worker.js self.onmessage = e => { if (e.data.type === 'sort') { const sorted = e.data.payload.sort((a, b) => a - b) self.postMessage(sorted) } } ``` 毛病:字符串 type、回调地狱、worker 文件路径在打包工具里难管。 ## 2. Vite + 模块 Worker(推荐) Vite 原生支持 `?worker` 后缀: ```ts // worker.ts self.onmessage = (e: MessageEvent<{ items: number[] }>) => { const sorted = e.data.items.sort((a, b) => a - b) self.postMessage(sorted) } export {} // 标记为 module ``` ```ts // main.ts import MyWorker from './worker.ts?worker' const worker = new MyWorker() worker.postMessage({ items: [3, 1, 2] }) worker.onmessage = e => console.log(e.data) ``` Vite 会把 worker.ts 单独打包,部署后是独立 .js 文件。 ## 3. Comlink:把 worker 当普通对象用 每次 postMessage / onmessage 写起来很烦。Comlink 把消息通信封装成 "调远程方法": ```bash npm i comlink ``` ```ts // worker.ts import { expose } from 'comlink' const api = { sort(items: number[]) { return items.sort((a, b) => a - b) }, async process(data: Row[]) { // 任意复杂同步 / 异步逻辑 return data.map(transform).filter(predicate) }, } export type Api = typeof api expose(api) ``` ```ts // main.ts import { wrap, Remote } from 'comlink' import Worker from './worker.ts?worker' import type { Api } from './worker' const worker = new Worker() const api: Remote<Api> = wrap<Api>(worker) // 用起来像普通对象 const sorted = await api.sort([3, 1, 2]) const processed = await api.process(rows) ``` 代码主线非常清晰,类型完整。Comlink 内部还是 postMessage,但你不需要管。 ## 4. Transferable:避免大数据拷贝 `postMessage` 默认深拷贝传过去。对 ArrayBuffer / ImageBitmap / OffscreenCanvas 可以用 transfer,把所有权直接交给 worker(O(1) 操作): ```ts // main const buf = new Float32Array(10_000_000).buffer worker.postMessage({ buf }, [buf]) // buf 这边变成 length=0,不能再用 // worker self.onmessage = e => { const arr = new Float32Array(e.data.buf) // ... 处理 self.postMessage({ result: arr.buffer }, [arr.buffer]) } ``` 对于几十 MB 以上的数据,transfer vs 拷贝差距可以从几秒到 0。 ## 5. SharedArrayBuffer:多 worker 共享同一块内存 需要服务端发 COOP / COEP header: ``` Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` 否则现代浏览器禁用 `SharedArrayBuffer`(Spectre 缓解)。 ```ts const shared = new SharedArrayBuffer(1024 * 1024) const view = new Int32Array(shared) worker1.postMessage({ buf: shared }) worker2.postMessage({ buf: shared }) // 三方都看到同一块内存;用 Atomics.* 同步 ``` ## 6. Worker Pool CPU 任务很多时一个 worker 不够,开 N 个: ```ts class Pool<T extends object> { private workers: Remote<T>[] private next = 0 constructor(n: number, factory: () => Remote<T>) { this.workers = Array.from({ length: n }, factory) } call<K extends keyof T>(method: K, ...args: any[]) { const w = this.workers[this.next++ % this.workers.length] return (w[method] as any)(...args) } } const pool = new Pool( navigator.hardwareConcurrency || 4, () => wrap<Api>(new Worker()) ) await pool.call('sort', items) ``` navigator.hardwareConcurrency 是浏览器报的核心数,通常等于物理核数。 ## 7. 不要把 React 状态搬进 Worker worker 没有 DOM,没法 React。它的工作是 **纯计算 + 返回数据**。 返回数据后主线程拿到才能 setState / 渲染。 ## 8. 调试 Chrome DevTools 的 Sources 面板能看到 worker 上下文(左侧 frames 列表), 断点、step、查变量都正常。 ## 踩过的坑 - worker 内不能用 `window`、`document`、`localStorage`(IndexedDB 可以); 共享代码模块要避免引用这些。 - Worker 启动有 50-100ms 开销。一次性短任务(< 10ms)放主线程更快。 - worker.terminate() 立即杀死 worker(不让它清理)。优雅退出应让 worker 接收一个 "shutdown" 消息后 close。 - Safari 对 module worker 支持比较晚(15.4+),需要确认目标用户。 Vite 编译目标设 `es2017` 时可能自动 fallback 到 classic worker,注意区分。

precision / recall / F1 / ROC-AUC:分类指标什么时候用谁

## 起因 老板问我"模型准确率 95% 是不是很厉害"。我一翻数据集: 99.5% 是负类("用户不会购买"),全猜负 trivial baseline 也 99.5% 准确率。我们的 95% 反而是垃圾。这是 accuracy 在不平衡数据上的经典 陷阱。 下面把几个常见分类指标用一个具体例子串起来。 ## 例子:邮件垃圾识别 - 测试集 1000 封邮件 - 真实:900 normal + 100 spam - 模型预测出 80 封 spam,其中 70 真是 spam(10 假阳性 FP)+ 30 真 spam 被漏(30 假阴性 FN) 混淆矩阵: | | 预测 spam | 预测 normal | |---|---|---| | 真实 spam | TP=70 | FN=30 | | 真实 normal | FP=10 | TN=890 | ## 指标对照 ### Accuracy $$\text{acc} = \frac{TP + TN}{TP + TN + FP + FN} = \frac{960}{1000} = 96\%$$ 不平衡数据下骗人。这个例子 96% 看着很好。 ### Precision(精确率) $$\text{precision} = \frac{TP}{TP + FP} = \frac{70}{80} = 87.5\%$$ "被预测为 spam 的邮件里,多少真是 spam"。**FP 代价高时关注**(误判 正常邮件为垃圾很烦)。 ### Recall(召回率) / Sensitivity / TPR $$\text{recall} = \frac{TP}{TP + FN} = \frac{70}{100} = 70\%$$ "所有真 spam 里,我抓到多少"。**FN 代价高时关注**(漏报 spam)。 医疗筛查、欺诈检测、安全告警都更看重 recall——宁可多报点 false positive 让人复查,也不能漏 true positive。 ### F1 $$F1 = 2 \cdot \frac{precision \cdot recall}{precision + recall} = 0.778$$ precision 和 recall 的调和平均。两者都重要、又只能优化一个数时用 F1。 ### F-beta `beta` 控制 recall 相对 precision 的权重: - F0.5:precision 加权(更看重不误报) - F1:均衡 - F2:recall 加权(更看重不漏报) ### ROC-AUC 不依赖阈值,看模型"把正样本排在负样本前"的能力。1.0 = 完美, 0.5 = 瞎猜。 在 sklearn: ```python from sklearn.metrics import roc_auc_score, precision_recall_fscore_support y_proba = model.predict_proba(X_test)[:, 1] auc = roc_auc_score(y_test, y_proba) # 在某个阈值上 y_pred = (y_proba > 0.5).astype(int) p, r, f, _ = precision_recall_fscore_support(y_test, y_pred, average='binary') ``` ROC-AUC 在极不平衡数据上**会过于乐观**。建议同时看 PR-AUC(precision-recall 曲线下面积)。 ### PR-AUC ```python from sklearn.metrics import average_precision_score ap = average_precision_score(y_test, y_proba) ``` 不平衡数据(正类占比 < 10%)的标准评估。比 ROC-AUC 更敏感地反映模型 区分能力。 ## 选择指南 | 场景 | 主指标 | |---|---| | 类别平衡 + FP/FN 等代价 | accuracy / F1 | | 不平衡 + 关注少数类 | PR-AUC、F1、recall | | 排序质量(不定阈值) | ROC-AUC、PR-AUC | | FP 代价高(误报扰民) | precision、F0.5 | | FN 代价高(漏报致命) | recall、F2 | | 多分类 | macro-F1(每类均权)/ weighted-F1(按支持度) | ## confusion matrix 可视化 ```python from sklearn.metrics import ConfusionMatrixDisplay ConfusionMatrixDisplay.from_predictions(y_test, y_pred, normalize='true') # normalize='true' 按真实类归一化,看每类的召回率 ``` 行内归一化能直接读"真 spam 里有多少被分对"。 ## classification_report ```python from sklearn.metrics import classification_report print(classification_report(y_test, y_pred, digits=3, target_names=['normal', 'spam'])) ``` ``` precision recall f1-score support normal 0.967 0.989 0.978 900 spam 0.875 0.700 0.778 100 accuracy 0.960 1000 macro avg 0.921 0.844 0.878 1000 weighted avg 0.958 0.960 0.958 1000 ``` 一眼看到每类的三个指标 + 宏 / 加权平均。 ## 阈值调整 预测概率 > 0.5 是默认。但业务上可以调: - 关注 recall:降阈值(0.3)→ 更多预测为 spam → recall 升、precision 降 - 关注 precision:升阈值(0.7)→ 反之 用 PR 曲线选最优 trade-off: ```python from sklearn.metrics import precision_recall_curve prec, rec, thr = precision_recall_curve(y_test, y_proba) # 找 precision >= 0.95 的最高 recall 对应阈值 idx = (prec[:-1] >= 0.95).nonzero()[0] best_thr = thr[idx[rec[idx].argmax()]] ``` ## 效果(一个 churn 模型的真实改造) 我们的客户流失预测原本看 accuracy,0.91。但 base rate 流失率 8%—— 全猜不流失也 92%。换成 PR-AUC 评估: - 全猜不流失:PR-AUC = 0.08 - 我们的模型:PR-AUC = 0.34 明显能看出模型有价值。再按 PR 曲线选阈值:保留 recall >= 0.7 的最高 precision 点,把 marketing 干预投到这批"高风险"用户上。CAC 下降 30%。 ## 踩过的坑 1. **`average='binary'` 在多分类时报错**:多分类要 `'macro'`/`'micro'`/ `'weighted'`。`'micro'` 在不平衡时等于 accuracy,意义有限。 2. **正类标号搞反**:sklearn 默认 `pos_label=1`。如果你的"想检测的类" 是 0,得 `pos_label=0` 否则 precision/recall 算的是另一类。 3. **不看 support**:F1 macro = 0.55 看着不行,但其中 90% 是稀有类 的支撑,整体 weighted F1 0.88 还可以。两个都要看。 4. **直接信任 cross-validation 的 ROC-AUC**:不平衡数据 + KFold(不 stratify)→ 某些 fold 几乎没正样本 → AUC 失真。永远用 `StratifiedKFold`。 5. **阈值在训练集上选**:必须用验证集 / OOF 选阈值;测试集只用于最终 报告。