知识广场
按学科筛选:计算机科学
«计算机科学» 分类下共 256 篇帖子
## 起因 SPA 路由切换 instant 但生硬: - click link → 内容直接换 - 没有 native app 那种"页面滑入"感觉 要做平滑过渡过去要用 framer-motion / GSAP / react-transition-group 等, JS 重 + 配置 + 难做"shared element transition"(同一元素跨页面渐变到 新位置)。 `View Transitions API`(Chrome 111+,Safari 18+)让浏览器原生做这事: ```js document.startViewTransition(() => { updateDOM(); // 你只管 update DOM }); // 浏览器自动 fade(默认) ``` 3 行启用,CSS 控制效果。 ## 基本 fade ```js function navigate(url) { if (!document.startViewTransition) { return loadPage(url); // fallback } document.startViewTransition(() => loadPage(url)); } ``` 浏览器拍 snapshot → DOM 改 → 自动 crossfade。 ## 自定义动画 CSS: ```css ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 300ms; } ::view-transition-old(root) { animation: slide-out 300ms; } ::view-transition-new(root) { animation: slide-in 300ms; } @keyframes slide-out { to { transform: translateX(-100%); } } @keyframes slide-in { from { transform: translateX(100%); } } ``` 浏览器把"老" 和"新" 都当独立的 pseudo-element animate。 slide 效果。 ## shared element transition 同一元素从 page A 滑到 page B 新位置: ```css /* page A */ .hero-image { view-transition-name: hero; } /* page B */ .detail-image { view-transition-name: hero; } ``` `view-transition-name` 相同的两个元素 → 浏览器自动 morph 过去(位置 + 大小)。 效果:list 页 thumb 点击后展开到 detail 页大图的位置 → 流畅滑动。 native app 这是常见效果,web 历来很难做。 ## 跨文档 (MPA) 也行 ```css /* old behavior: SPA only */ @view-transition { navigation: auto; } ``` 加这个 → 普通 `<a href>` 跳转也自动有 view transition。 MPA / 服务端渲染网站直接得到 SPA 般体验。 Chrome 126+ 支持,Safari 18+。 ## 实战 example:image gallery ```html <!-- list page --> <ul> <li><a href="/photo/1"><img src="thumb1.jpg" class="thumb-1"></a></li> <li><a href="/photo/2"><img src="thumb2.jpg" class="thumb-2"></a></li> </ul> <!-- detail page /photo/1 --> <img src="full1.jpg" class="thumb-1"> ``` ```css .thumb-1 { view-transition-name: photo-1; } .thumb-2 { view-transition-name: photo-2; } @view-transition { navigation: auto; } ``` 点 list 缩略图 → 滑到 detail 页大图位置(同 `view-transition-name` morph)。 50 行 CSS / 0 行 JS framework 出 native app 体验。 ## SPA framework 集成 SvelteKit: ```js // app.html / hook import { onNavigate } from '$app/navigation'; onNavigate((navigation) => { if (!document.startViewTransition) return; return new Promise((resolve) => { document.startViewTransition(async () => { resolve(); await navigation.complete; }); }); }); ``` Next.js (with App Router 14+) 计划 next-view-transitions package 集成。 Astro 4 内置 view transitions(最早接入的 SPA framework)。 ## 自定义动画 token ```css ::view-transition-group(*) { animation-duration: 200ms; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } ::view-transition-old(*) { animation-name: fade-out; } ::view-transition-new(*) { animation-name: fade-in; } @keyframes fade-out { to { opacity: 0; } } @keyframes fade-in { from { opacity: 0; } } ``` `*` 是所有 view-transition-name,统一应用。 ## reduce motion ```css @media (prefers-reduced-motion: reduce) { ::view-transition-old(*), ::view-transition-new(*) { animation: none; } } ``` 尊重无障碍偏好,关闭动画。 ## fallback ```js async function navigate(url) { const update = () => loadPage(url); if (document.startViewTransition) { document.startViewTransition(update); } else { update(); } } ``` 老浏览器(Firefox 现在还不支持)→ 直接换不动画。 不需要 polyfill,平滑降级。 ## debug Chrome DevTools 有 view transition debugger: - Animation panel 看 frames - Performance panel 看 transition cost 慢的话查: - snapshot 渲染贵 - 动画太多并行 ## 性能 view transition 是 GPU 加速(compositor 层),60 fps 容易。 比 React state 切换 + CSS transition 效率高。 但 snapshot 大 element(整页)也有成本。 > 500ms 不算 view transition 适合场景。 ## 与 framer-motion 对比 | | View Transitions API | framer-motion | |---|---|---| | 配置 | CSS + 几行 JS | JS 重 | | 大小 | 0 (原生) | ~30 KB | | shared element | 简单 | 复杂 (AnimatePresence) | | 兼容 | Chrome / Safari (Firefox no) | 全部 | | 灵活度 | 中 | 极高 | simple fade / slide / shared element → View Transitions API。 复杂交互(drag / gesture / spring physics)→ framer-motion。 ## 真实 case:内容站 我们一个博客 + 文档站,加了 5 行 CSS(`@view-transition: navigation: auto` + hero 图 view-transition-name)。 效果: - 点击文章卡片 → 卡片 morph 到文章详情 header - 切换文档章节 → 内容 fade - 滑动手感像 native app - bundle 增加 0(纯 CSS / 浏览器原生) - code 改动几十行 用户反馈:"站点感觉变快了"(实际加载没变,但平滑过渡心理 perceived performance 提升)。 ## 不适合的场景 - 复杂物理动画(spring / momentum)→ 用 framer / GSAP - 需要 user gesture(drag / pinch)→ 用 framer - 极致性能场景(动画 60fps + 5+ 大元素并发) ## 踩过的坑 1. **`view-transition-name` 必须 unique**:两个元素同名同时存在 → 只 transition 一个。dynamic list 用 unique id 后缀。 2. **width/height 变化 morph 怪**:position absolute 大小变化时 transform 计算偏差。试 `width: auto` 与 `aspect-ratio` 配合。 3. **dark mode 切换不平滑**:toggle dark mode 时 view transition → crossfade 整页。慢设备卡。考虑 disable。 4. **图片 loading**:transition 时新图还没加载 → 闪。preload 关键 img。 5. **嵌套 transition**:transition 期间 trigger 另一 transition → undefined behavior。debounce / sequence。
## 起因 监控装好了,Prometheus + Loki + Grafana 都在跑,仪表盘有 30 多个。 出问题时点开仪表盘要翻几屏才看到关键指标。on-call 的同事中午吃饭 被叫起来排查,越急越慌越找不到。 "作战仪表盘"就是把"凌晨被叫醒后要看的 8 个图"压缩到一屏内。 ## 解决方案 ### 设计原则 1. **一屏装下,不滚屏**:1920×1080 上 8-12 个 panel 是甜点 2. **金字塔布局**:最上面是"系统健康分",下方是各组件细节 3. **时间窗口默认 1h**:足够看到趋势又不太久 4. **每个 panel 自带告警阈值线**:红色 horizontal line 标 SLO 5. **stats vs graph**:当前值用 stat panel(巨大数字),趋势用 timeseries ### 顶部:4 个 stat panel(系统状态) ```promql # Panel: 在线节点数 / 总节点数 sum(up{job="node"}) / count(up{job="node"}) # Panel: 当前 RPS(应用总) sum(rate(http_requests_total[1m])) # Panel: 当前 P95 延迟 histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) # Panel: 当前 5xx 错误率 sum(rate(http_requests_total{status=~"5.."}[1m])) / sum(rate(http_requests_total[1m])) ``` 每个 stat panel 配 thresholds: - 绿色:正常 - 黄色:警戒 - 红色:异常 例如 P95 延迟 < 100ms 绿、100-500ms 黄、> 500ms 红。 ### 中部:时间序列趋势(2x2) 1. **RPS by endpoint**: ``` sum by (path) (rate(http_requests_total[1m])) ``` 2. **延迟 P50/P95/P99**: ``` histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) histogram_quantile(0.95, ...) histogram_quantile(0.99, ...) ``` 3. **错误率 by status code**: ``` sum by (status) (rate(http_requests_total[1m])) ``` 4. **数据库 query 时间 P95**: ``` histogram_quantile(0.95, sum by (le) (rate(db_query_duration_seconds_bucket[5m]))) ``` ### 下部:基础设施(2x2) 5. **每节点 CPU**: ``` 100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 ``` 6. **每节点 RAM**: ``` 100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) ``` 7. **磁盘使用率**: ``` 100 - node_filesystem_avail_bytes{mountpoint="/"} * 100 / node_filesystem_size_bytes{mountpoint="/"} ``` 8. **网络入/出**: ``` sum by (instance) (rate(node_network_receive_bytes_total[1m])) sum by (instance) (rate(node_network_transmit_bytes_total[1m])) ``` ### 底部:log 错误日志(Loki) Logs panel,query: ```logql {job="myapp"} |~ "ERROR|FATAL|panic" | json ``` 直接看到最近的错误日志,配合上面的指标曲线看时间关联。 ### 配置 tip #### 单位 每个 panel 设正确的 unit(seconds / percent / bytes/sec), Grafana 自动用 K/M/G 缩写。`{r}/s` 表示每秒请求数。 #### 颜色一致 所有"错误"用红、"延迟"用紫、"流量"用蓝、"资源"用橙。 形成视觉默契,眼睛快速分辨。 #### 阈值线 ``` Thresholds: - color: green, value: 0 - color: yellow, value: 100 - color: red, value: 500 ``` panel 上自动画水平线。 #### Variables(下拉切环境 / 服务) 仪表盘顶部: ``` Variable: env Type: query Query: label_values(up, env) Variable: service Type: query Query: label_values(up{env="$env"}, job) ``` 用户切 env=prod / service=api → 所有 panel 自动 filter。 ### 自动化:dashboard as code 不要在 UI 里手动建。用 [grafonnet](https://github.com/grafana/grafonnet) 或者 [grizzly](https://github.com/grafana/grizzly) 把 dashboard 写成 Jsonnet / YAML 进 git: ```jsonnet local g = import 'g.libsonnet'; g.dashboard.new('My App SLI Overview') + g.dashboard.withRefresh('30s') + g.dashboard.withPanels([ g.panel.stat.new('Active nodes') + g.panel.stat.queryOptions.withTargets([ g.query.prometheus.new('default', 'sum(up)') ]), // ... 更多 panel ]) ``` `grr` apply 一键部署到 Grafana。改动走 PR review。 ### Provisioning 放 dashboard JSON 到 `/etc/grafana/provisioning/dashboards/`, Grafana 启动自动加载。这样部署 / 还原灾备时 dashboard 不需要手动重建。 ## 效果 - 凌晨 on-call 收到告警 → 打开仪表盘 → 5 秒看清"哪个指标红了" - mean time to diagnose 从 ~15 分钟降到 ~3 分钟 - 团队新人 onboarding 时一份仪表盘 = 一份系统全貌速成课 - "感觉慢" 类玄学反馈被替换为"看这个 panel 上 P95 涨了 3 倍" ## 一些进阶 panel ### Service Level Objective (SLO) burn rate ``` 1 - sum(rate(http_requests_total{status!~"5.."}[1h])) / sum(rate(http_requests_total[1h])) ``` 定义 SLO(如"99.5% 成功率"),算每小时实际 vs 目标的 burn rate。 > 1 = 这小时消耗了超过本月预算的 1/30。 ### Apdex ``` (sum(rate(http_request_duration_seconds_bucket{le="0.1"}[5m])) + sum(rate(http_request_duration_seconds_bucket{le="0.4"}[5m])) / 2) / sum(rate(http_request_duration_seconds_count[5m])) ``` 100ms 满意 + 400ms 容忍 → 0-1 分数。一个数字概括"用户满意度"。 ### USE method (Utilization / Saturation / Errors) 每资源(CPU / RAM / disk / network)三栏:当前用量 / 队列长度 / 错误数。 Brendan Gregg 的经典系统排查框架,dashboard 上也用得上。 ## 踩过的坑 1. **panel 用 sum 不加 by**:所有节点 RPS 加一起看不出哪个节点出问题。 关键指标都按 `by (instance)` 或 `by (job)` 分组。 2. **时间窗口太长**:default 6h 仪表盘载入慢 + 看不清。设 1h 默认, 按需调长。 3. **stat panel 数字闪烁**:默认 refresh 5s 时每次刷新都重算 → 视觉 抖。配 `min_step: 30s` 平滑。 4. **告警从 dashboard 配(Grafana alerting)vs Alertmanager rule**: 两套别混。生产建议规则进 Prometheus rule files(与 Grafana 解耦), Grafana 只展示。 5. **dashboard 太多**:超过 30 个仪表盘后没人知道用哪个。归类 + 命名 规范 + folder 组织,每月 review 删用不到的。
## 起因 每天跑一遍数据仓库 SQL transform: ```sql -- daily_user_metrics SELECT user_id, DATE(created_at) AS day, COUNT(*) AS events, SUM(value) AS total FROM events GROUP BY 1, 2; ``` 全量跑:扫几亿行 events → 20 min + Snowflake credit 几百刀。 但实际上**昨天之前的天数据是冻结的**,每天只需算今天的新增。 dbt 的 incremental model 模式解决这问题。 ## 全量 model ```sql -- models/daily_user_metrics.sql {{ config(materialized='table') }} SELECT user_id, DATE(created_at) AS day, COUNT(*) AS events FROM {{ ref('events') }} GROUP BY 1, 2 ``` `materialized='table'`:每次 dbt run 都 DROP + CREATE TABLE AS。 小数据 OK;大数据浪费。 ## incremental model ```sql -- models/daily_user_metrics.sql {{ config( materialized='incremental', unique_key=['user_id', 'day'], on_schema_change='append_new_columns' ) }} SELECT user_id, DATE(created_at) AS day, COUNT(*) AS events, SUM(value) AS total FROM {{ ref('events') }} {% if is_incremental() %} WHERE created_at >= (SELECT MAX(day) FROM {{ this }}) - INTERVAL '1 day' {% endif %} GROUP BY 1, 2 ``` 关键: - `{{ this }}` 引用当前 model 自己 - `is_incremental()` 在"已存在且不是 --full-refresh"时为 true - WHERE 只取新数据 - `unique_key` 让 dbt 知道按啥 merge 第一次 run:建 table + 全量。 之后每次 run:只算新数据 → MERGE 进现有 table。 ## 效果 我们一个 metric model: - 事件表 5 亿行 - 全量跑:22 分钟,$3 credit - incremental:1.5 分钟,$0.2 credit 12x 时间节省,15x 成本节省。 ## 几种 strategy dbt 支持几种 incremental 策略(按 warehouse): | strategy | 行为 | |---|---| | `append` | 直接 INSERT 新数据(不去重) | | `merge` | 默认。MERGE INTO with unique_key | | `delete+insert` | 删 unique_key 匹配的行 + INSERT | | `insert_overwrite` | partition-level overwrite(BigQuery / Spark) | ```sql {{ config( materialized='incremental', incremental_strategy='merge', unique_key='id', ) }} ``` PG / Snowflake / Redshift 用 merge。BigQuery 大 partition 用 insert_overwrite。 ## late-arriving data 事件 24 小时后才到(移动 SDK 离线缓存)。 window 要更宽: ```sql {% if is_incremental() %} WHERE created_at >= (SELECT MAX(day) FROM {{ this }}) - INTERVAL '7 days' {% endif %} ``` 回看 7 天,覆盖晚到的事件。`unique_key` 保证 merge 不重复。 trade-off:window 越宽 → 重算越多 → 增量收益减。 ## 全量 backfill 老数据要重算(如 metric 公式改了): ```bash dbt run --select daily_user_metrics --full-refresh ``` `--full-refresh` 让 incremental 当 table 处理 → DROP + 重建。 或者部分重算: ```bash # 重算最近 30 天 dbt run --select daily_user_metrics --vars '{"start_date": "2025-04-01"}' ``` model 里读 var: ```sql {% if var('start_date', false) %} WHERE created_at >= '{{ var("start_date") }}' {% endif %} ``` ## 跟 partitioned table 配 BigQuery / Snowflake partition: ```sql {{ config( materialized='incremental', incremental_strategy='insert_overwrite', partition_by={'field': 'day', 'data_type': 'date'}, cluster_by=['user_id'], ) }} ``` incremental insert overwrite 比 merge 更高效(partition 整块替换 vs row-level merge)。 ## test incremental dbt test 默认在 model run 后跑: ```yaml # models/schema.yml models: - name: daily_user_metrics columns: - name: user_id tests: - not_null - name: day tests: - not_null tests: - dbt_utils.unique_combination_of_columns: combination_of_columns: [user_id, day] ``` increment 后唯一性测试**关键**:merge 配错 → 重复数据。 ## 监控 `dbt run` 完看 timing: ``` 1 of 5 START incremental model daily_user_metrics ........ [RUN] 1 of 5 OK created incremental model daily_user_metrics ... [SUCCESS in 89.42s] ``` 如果 incremental 跑越来越慢(不应该): - WHERE 条件 partition / cluster key 利用不充分 - merge 行数变大(window 太宽) - new column 加了导致 `on_schema_change` 触发 ## dbt 1.6+ microbatch dbt 1.6 引入 `microbatch` incremental_strategy(更明确的 partition-by-time): ```sql {{ config( materialized='incremental', incremental_strategy='microbatch', event_time='created_at', batch_size='day', lookback=3, ) }} SELECT ... FROM {{ ref('events') }} ``` dbt 自动按 day 分批跑 → backfill 时按天 chunk → 单个 chunk 失败不 影响别天。比手写 `is_incremental()` 干净。 ## 我们的 pipeline ``` events (raw, 5 亿行) ↓ [hourly] events_hourly (incremental, 24h window) ↓ [daily] daily_metrics (incremental, 7d window) ↓ monthly_summary (incremental, 1m window) ↓ dashboard ``` 每层 incremental,各自 window 覆盖延迟。 全量 backfill 用 `--full-refresh` 串行跑全部。 ## 真实 vs 全量 我们一个 200 个 model 的 dbt 项目,把 80% 大 model 改 incremental 后: - 总跑时间从 4h → 35min - Snowflake 月成本从 $8000 → $1200 - 失败重试代价低很多 incremental 是 dbt 最重要的 production pattern。 ## 踩过的坑 1. **unique_key 不对**:merge 进重复 → 数据涨。每次改 unique_key 必须 `--full-refresh`。 2. **`is_incremental()` 没用**:写在 CTE 里 / 没读 `{{ this }}` → 每次都全量。看生成的 SQL(`compile`)确认。 3. **schema change**:表加列 → 默认 incremental 报错(schema mismatch)。 `on_schema_change='append_new_columns'` 或 `'sync_all_columns'`。 4. **late-arriving 漏数**:window 太窄 → 晚到事件没进表。监控 `events vs metrics` 一致性。 5. **partition 没 prune**:BigQuery 大 partition 表 WHERE 写 timestamp 比较,但 model 没 partition by → full scan。 `partition_by` 配 + WHERE 用 partition 列。
SSH 密钥是日常运维 / git 协作的基础。新人常踩的坑: - 一个 key 用到所有地方 - 私钥没加密 - 失去机器后忘了删 key 的服务端授权 - 多个机器多个 key 没区分 下面是一套实用的 SSH 密钥管理流程。 ## 1. 生成密钥 ```bash ssh-keygen -t ed25519 -C 'name@laptop' -f ~/.ssh/id_ed25519 # 提示 passphrase:建议设一个强密码 ``` `-t ed25519` 推荐;`rsa 4096` 兼容性最广但比 ed25519 慢。 `-C` 是注释,写"哪个用户在哪台机器",方便服务端管理识别。 `-f` 指定文件名。我习惯: - `~/.ssh/id_ed25519` 个人主密钥(用得最多) - `~/.ssh/id_ed25519_work` 工作账号 - `~/.ssh/id_ed25519_github` GitHub 专用 ## 2. 一个 key 不要到处用 理由: - 一处泄露 → 所有地方都泄露 - 不同身份混淆(个人 git / 公司 git / VPS) - 撤销时全部更新很麻烦 按维度分: - 个人 vs 公司 - 永久机器(笔记本)vs 临时机器(VPS / 开发容器) - GitHub / GitLab 各一把(很多 git host 拒绝重复 key) ## 3. ssh-agent:缓存解锁的私钥 ```bash # 启 agent(GNOME / KDE 默认已经起好) eval $(ssh-agent) # 加 key(要输 passphrase 一次) ssh-add ~/.ssh/id_ed25519 ssh-add ~/.ssh/id_ed25519_work # 看已加载的 key ssh-add -l # 256 SHA256:xxx... name@laptop (ED25519) # 删某个 ssh-add -d ~/.ssh/id_ed25519_work # 全删(退出前清干净) ssh-add -D ``` 之后所有 ssh / scp / git 不再要密码。 ## 4. agent 自动启动 + key 自动加载 `~/.zshrc` 或 `~/.bashrc`: ```bash # 启 ssh-agent 如果没运行 if [ -z "$SSH_AUTH_SOCK" ]; then eval $(ssh-agent -s) >/dev/null fi # 自动加载尚未加载的 key if ! ssh-add -l 2>/dev/null | grep -q "$HOME/.ssh/id_ed25519"; then ssh-add ~/.ssh/id_ed25519 2>/dev/null fi ``` macOS 用 keychain 集成更优雅: ```bash ssh-add --apple-use-keychain ~/.ssh/id_ed25519 ``` 密码存系统 keychain,重启后自动加载。 ## 5. authorized_keys 服务端 每台机器的 `~/.ssh/authorized_keys` 是允许登录的公钥列表。 推送公钥的标准方式: ```bash ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server # 自动追加到 ~user/.ssh/authorized_keys + 设权限 ``` 手动方式(如果 ssh-copy-id 不可用): ```bash cat ~/.ssh/id_ed25519.pub | ssh user@server \ 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys' ``` ## 6. 给授权 key 加限制 服务端 `authorized_keys` 每行前面可以加 options: ``` # 只允许从某 IP from="203.0.113.5" ssh-ed25519 AAAA... user@laptop # 不允许 shell / 端口转发 / agent forward;只能跑特定命令 command="/usr/local/bin/backup.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... backup-runner # 过期时间 expiry-time="20251231" ssh-ed25519 AAAA... contractor ``` `command` 限制是给 backup / 监控这种"只跑一个脚本"的服务账号用的。 登录的人开 shell 也只跑这个命令,比开 shell 安全得多。 ## 7. 定期审计 服务端列所有授权 key: ```bash for u in $(cut -d: -f1 /etc/passwd); do if [ -f /home/$u/.ssh/authorized_keys ] || [ -f /root/.ssh/authorized_keys ]; then echo "=== $u ===" cat /home/$u/.ssh/authorized_keys 2>/dev/null [ "$u" = "root" ] && cat /root/.ssh/authorized_keys 2>/dev/null fi done ``` 发现陌生 key / 离职同事 key → 立刻删。 ## 8. 服务端 sshd 加固 `/etc/ssh/sshd_config`: ``` # 禁密码登录(强制只用密钥) PasswordAuthentication no PubkeyAuthentication yes ChallengeResponseAuthentication no KbdInteractiveAuthentication no # 禁 root 直接登录 PermitRootLogin prohibit-password # 或 no(彻底禁) # 限制允许登录的用户 AllowUsers alice bob deploy # 或:AllowGroups sshusers # 减少协议噪音 ListenAddress 0.0.0.0 Port 22 # 客户端不活动时断开 ClientAliveInterval 300 ClientAliveCountMax 2 ``` ```bash sudo sshd -t # 语法检查 sudo systemctl reload ssh ``` ## 9. 硬件 key(YubiKey / 谷歌 Titan) 最强方案:私钥永远不在磁盘 / 内存上,存硬件芯片: ```bash # 生成 FIDO2 / U2F 密钥(保存在 YubiKey) ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_yubi # resident key(可以从其它机器恢复) ssh-keygen -t ed25519-sk -O resident -f ~/.ssh/id_ed25519_yubi ``` 之后每次 ssh 都需要触摸 YubiKey 金属片。 恢复 resident key 到新机器: ```bash ssh-keygen -K # 把 YubiKey 上所有 resident key 提取到当前目录 ``` 私钥本质上是在 YubiKey 里,磁盘文件只是"指针"。丢硬件 = 数字身份废掉 (但攻击者拿不到密钥)。 ## 10. SSH CA(企业规模时) 机器多了,每次给新人加 key 改几百台机器 authorized_keys 不现实。 SSH CA 系统: - 一个 CA 私钥签发用户证书(包含 username + 有效期 + 权限) - 每台机器 `TrustedUserCAKeys = /etc/ssh/ca.pub` - 用户拿到带签名的临时证书登录,到期失效 ```bash # 签发 ssh-keygen -s ca-key -I alice@2026-05 -n alice -V +1d \ alice_id_ed25519.pub # 输出 alice_id_ed25519-cert.pub # 登录时自动用 cert ssh server # ssh 会读 cert + key ``` 工具:Vault SSH backend / Smallstep / Teleport / sssd。 ## 踩过的坑 - 公钥 vs 私钥:`*.pub` 是公钥可以随便发;没后缀那个是私钥 **永远不发**。 新手把 `id_ed25519` 而不是 `id_ed25519.pub` 贴 GitHub 是经典错误。 - 私钥权限:必须 600;775 / 644 sshd 会拒绝使用。`chmod 600 ~/.ssh/id_*`。 - `.ssh/authorized_keys` 权限:必须 600;目录必须 700。否则 sshd 静默 忽略整个文件,登录失败查不到原因(auth log 才会有提示)。 - ssh-agent forwarding(`ForwardAgent yes`):跳板机 root 用户能拿你的 key 用,相当于把信任传递给跳板机。除非完全可信,否则用 ProxyJump 代替 agent forward。
某次合并后线上崩了,可疑提交几十上百个,一个个 checkout 太慢。 `git bisect` 做二分搜索:log N 次 checkout 就找出元凶。 ## 基本流程 ```bash git bisect start git bisect bad HEAD # 现在的状态是坏的 git bisect good v1.5.0 # 这个 tag 是好的 # Git checkout 中间提交,让你测: # Bisecting: 38 revisions left to test after this (roughly 6 steps) # [a1b2c3d] Some commit ``` 你测一下(手动或脚本): ```bash # 如果坏: git bisect bad # 如果好: git bisect good # 测不了(编译失败 / 不相关): git bisect skip ``` Git 自动 checkout 下一个二分点。重复直到: ``` # a1b2c3d is the first bad commit # commit a1b2c3d # Author: ... # Date: ... # feat: refactor user permission check ``` 完事: ```bash git bisect reset # 回到原本的 HEAD ``` ## 自动化(最好用的方式) 写一个返回 0 / 1 的检查脚本: ```bash # check.sh #!/usr/bin/env bash set -e make build > /dev/null 2>&1 || exit 125 # 编译失败 → skip ./run_test.sh > /dev/null 2>&1 # exit 0 = good, !=0 = bad ``` 退出码约定: - `0`:good - `1` ~ `124`、`126` ~ `127`:bad - `125`:skip - `128+`:abort bisect 然后: ```bash git bisect start HEAD v1.5.0 git bisect run ./check.sh ``` Git 自动跑完所有二分步骤,结束时打印 first bad commit。 20 个提交需要约 5 次 checkout + 测试。 ## 真实例子:找性能回归 线上某 endpoint 从 50ms 涨到 200ms: ```bash # benchmark.sh #!/usr/bin/env bash make build ./serve & PID=$! sleep 2 LAT=$(curl -s -o /dev/null -w '%{time_total}' http://localhost:8000/api/x) kill $PID # 50ms 以下算好 awk "BEGIN { exit ($LAT < 0.10) ? 0 : 1 }" ``` ```bash git bisect start HEAD v1.5.0 git bisect run ./benchmark.sh ``` 20 分钟后告诉你是哪个提交把性能拖坏的。 ## 一次同时盯多个文件 bisect 时如果你只想看某子目录: ```bash git bisect start --term-good=fast --term-bad=slow -- src/api/ ``` `--` 后面限定路径范围;只把改了 `src/api/` 的提交纳入二分。 搜索空间小很多。 ## skip 多个提交 某段提交都 build 失败(比如有人合错了破坏分支),全 skip: ```bash git bisect skip $(git rev-list bad-broken^..good-broken) ``` bisect 算法会跳过这段继续。 ## 可视化 bisect 当前状态 ```bash git bisect log # git bisect start # # bad: [abcdef] description # git bisect bad abcdef # # good: [123456] description # git bisect good 123456 # # skip: [xyzxyz] ... # git bisect skip xyzxyz ``` 把 log 输出存文件,回头能重放 bisect: ```bash git bisect log > /tmp/bisect.txt git bisect reset # ... 时间过去 ... git bisect replay /tmp/bisect.txt # 还原到之前的进度 ``` ## 与 git worktree 配合 bisect 期间不能在主目录干别的。开个 worktree 让 bisect 单独跑: ```bash git worktree add ../bisect-tmp HEAD cd ../bisect-tmp git bisect start HEAD v1.5.0 git bisect run ./check.sh # 完事 cd ../myapp git worktree remove ../bisect-tmp ``` ## 不止 commit —— bisect tag / branch ```bash git bisect start v2.0 v1.0 # 在两个 tag 之间二分 git bisect start origin/main origin/main~100 # 最近 100 个 commit ``` ## 几个手动 bisect 的小 trick ### 中途调整范围 ```bash git bisect bad <some-commit> # 把 "bad" 边界从 HEAD 改到更早 git bisect good <other> # 把 "good" 边界改到更晚 ``` bisect 重新计算二分。 ### 临时 fix forward 后继续 某中间 commit build 失败需要小补丁才能跑你的 check: ```bash # 在二分到的 commit 上 git cherry-pick <fix-commit> # 临时打补丁 ./check.sh git bisect good/bad # 根据结果 git reset --hard HEAD~ # 撤销 cherry-pick git bisect <next> ``` ## 踩过的坑 - bisect 用的 check 脚本不稳定(偶尔 false bad / false good)→ 二分到 错误结论。check 脚本必须 deterministic。 - `git bisect bad` 漏写 commit 参数 → 自动用 HEAD(当前 bisect 选的) 当 bad。手抖容易标错。 - bisect 跨越了 merge commit,结果指到一个 merge commit。看 `git log --first-parent` 复审;真正 bug 在 merge 的某个 parent 分支里, 对那个分支再做二级 bisect。 - 整套流程用 `git bisect run` 自动化才省力。手动 100 次每次手测, 错一次就要 reset 重来。
React 新手最常踩的"过度优化"就是给所有函数 / 计算包 `useMemo` `useCallback`。 官方文档 2024 之后已经明确:**默认不要用,先 profile**。 但是有几个场景必须用。下面分清楚。 ## 默认不要用的理由 `useMemo` 本身要执行: 1. 调用 `useMemo` 创建 hook entry 2. 比较依赖数组(浅比较) 3. 决定是否复用 这些开销 > 大多数同步函数的执行成本。所以对一个"加两个数字"包 useMemo, 反而更慢。 ## 真正需要 useMemo 的场景 ### 场景 1:计算确实贵(毫秒级以上) ```tsx function ChartView({ rawData }: { rawData: Row[] }) { // 大量数据点排序 + 重计算 const processed = useMemo(() => { return rawData .map(row => ({ ...row, score: complexCalc(row) })) .sort((a, b) => b.score - a.score) .slice(0, 100) }, [rawData]) return <Chart data={processed} /> } ``` 判断标准:profile 看到这块 > 16ms(一帧)就有必要。 ### 场景 2:作为 `useEffect` / `useMemo` / `useCallback` 的依赖 ```tsx function Form({ schema }: { schema: Schema }) { // 不 memo 的话每次渲染 validator 都是新对象,下面 useEffect 每次都跑 const validator = useMemo(() => createValidator(schema), [schema]) useEffect(() => { validator.subscribe(handleValid) return () => validator.unsubscribe(handleValid) }, [validator]) } ``` 这是 useMemo 最常见的合理用途——稳定依赖。 ### 场景 3:传给 memoized 子组件 ```tsx const SearchResults = React.memo(function SearchResults({ items }: ...) { // ... }) function Page() { const [query, setQuery] = useState('') // items 每次都是新数组 → React.memo 浅比较失败 → 子组件每次都重渲染 // 加 useMemo 之后只有 query 变了才重新生成 items const items = useMemo(() => filterItems(allItems, query), [query, allItems]) return <SearchResults items={items} /> } ``` 如果子组件没 `React.memo` 包裹,这个 useMemo 完全无效——白干。 ## useCallback 类似 ```tsx function Parent() { const [count, setCount] = useState(0) // ❌ 没必要:onClick 每次都是新引用但 Button 没 memo const onClick = useCallback(() => setCount(c => c + 1), []) return <Button onClick={onClick} /> } ``` `useCallback` 仅在以下两种情况有意义: 1. 函数作为 `useEffect` / `useMemo` 的依赖 2. 函数传给 `React.memo` 的子组件 ## 反模式集合 ### ❌ 给基础类型包 useMemo ```tsx const sum = useMemo(() => a + b, [a, b]) // 直接: const sum = a + b // 一样快 ``` ### ❌ 包内联对象 / 数组(除非传给 memo 子) ```tsx const style = useMemo(() => ({ color: 'red' }), []) // 直接: const style = { color: 'red' } // 多创建一个对象,但 GC 几乎免费 ``` ### ❌ "为了未来扩展" 提前 useMemo 写代码时不要"也许将来子组件会 React.memo 所以现在先包"。等真有需要再包。 ## 测量工具 React DevTools → Profiler → Record。看哪个组件渲染 > 5ms 或者 "unnecessarily" 标红。 Chrome DevTools → Performance → Record。看主线程任务,> 50ms 就是 long task,需要优化。 ## React 19 的 React Compiler React 19+ 带的 React Compiler 会在编译期自动决定哪些值需要 memo, **届时手写 useMemo / useCallback 几乎可以全删**。 提前为这个未来准备: 1. 不要为了 memo 而引入"复杂的依赖追踪"。代码清晰更重要,性能用编译器 2. 现有的 useMemo / useCallback 可以保留,编译器会忽略它们 ## 一个完整反例 ```tsx // 全是反优化 function UserCard({ user }: { user: User }) { const name = useMemo(() => user.name, [user]) // ❌ const onClick = useCallback(() => alert(user.id), [user]) // ❌(没 memo 子) const style = useMemo(() => ({ padding: 12 }), []) // ❌ return ( <div style={style} onClick={onClick}> <strong>{name}</strong> </div> ) } ``` 清版: ```tsx function UserCard({ user }: { user: User }) { return ( <div style={{ padding: 12 }} onClick={() => alert(user.id)}> <strong>{user.name}</strong> </div> ) } ``` 后者更短、更快、更易读。 ## 踩过的坑 - 给 useMemo 的依赖数组写错(漏依赖 / 多依赖)是 React 最常见 bug, 装 `eslint-plugin-react-hooks` 让 `exhaustive-deps` 规则帮你查。 - 复杂表单的 onChange 用 useCallback 包并传给 memoized Input 子组件—— 看起来对,但 input 上的 onChange 引用是否稳定通常不是性能瓶颈。 先 profile 再优化。 - 把 `useState` 的 setter 写进 useCallback 的依赖数组:setter 永远稳定, 写了不影响但说明你没理解 setter 的行为。可以省略。
## 起因 ML model train 在 PyTorch / TF / sklearn,部署面对: - 想跑在 CPU 而不是 GPU - 想 deploy 到 mobile / web / 嵌入式 - 想要更小 / 更快的 runtime(PyTorch ~ 1 GB;ONNX Runtime ~ 50 MB) - 不想生产环境扛 PyTorch 依赖 **ONNX (Open Neural Network Exchange)** 是模型表示标准。 **ONNX Runtime** 是跑 ONNX 模型的 runtime(C++ 写,多 backend)。 train 用任何框架 → export ONNX → 用 ONNX Runtime 跑。 ## export PyTorch: ```python import torch model = MyModel() model.load_state_dict(torch.load('model.pt')) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, 'model.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}}, opset_version=18, ) ``` `dynamic_axes` 让 batch dim 运行时可变。 sklearn: ```python from skl2onnx import to_onnx onnx_model = to_onnx(sk_model, X_sample[:1]) with open('model.onnx', 'wb') as f: f.write(onnx_model.SerializeToString()) ``` HuggingFace transformers: ```bash optimum-cli export onnx --model bert-base-uncased ./onnx_out/ ``` ## 验证 ```python import onnx onnx.checker.check_model(onnx.load('model.onnx')) import onnxruntime as ort sess = ort.InferenceSession('model.onnx', providers=['CPUExecutionProvider']) input_name = sess.get_inputs()[0].name result = sess.run(None, {input_name: dummy_input.numpy()}) print(result[0].shape) ``` 跑通 → ONNX 模型 ready。 跟 PyTorch 原 model 输出做 numerical 比较(tolerance 1e-5): ```python torch_out = model(dummy_input).detach().numpy() onnx_out = sess.run(None, {input_name: dummy_input.numpy()})[0] np.testing.assert_allclose(torch_out, onnx_out, rtol=1e-3, atol=1e-5) ``` ## 跑 ONNX Runtime CPU: ```python sess = ort.InferenceSession('model.onnx', providers=['CPUExecutionProvider']) ``` GPU (CUDA): ```python sess = ort.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) ``` CoreML (mac M 系列): ```python providers=['CoreMLExecutionProvider', 'CPUExecutionProvider'] ``` WebAssembly (浏览器): ```js // onnxruntime-web import * as ort from 'onnxruntime-web'; const session = await ort.InferenceSession.create('model.onnx'); const results = await session.run({ input: tensor }); ``` 同一 .onnx 文件,多 platform 复用。 ## 性能 vs PyTorch ResNet50 inference / batch=1 / single thread: | Runtime | latency | |---|---| | PyTorch CPU | 80 ms | | ONNX Runtime CPU | 45 ms | | PyTorch (TorchScript) | 60 ms | | ONNX + OpenVINO backend | 28 ms | ONNX Runtime 普遍比 PyTorch CPU 快 1.5-2x(graph 优化 + inference 专用)。 GPU 上差距小,PyTorch 也很快。 ## 量化 ```python from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic('model.onnx', 'model_int8.onnx', weight_type=QuantType.QInt8) ``` INT8 量化 → model size 4x 小 + CPU 2x 快(精度损 1-3%)。 mobile / edge 部署关键。 ## 部署在 server ```dockerfile FROM python:3.12-slim RUN pip install onnxruntime fastapi uvicorn numpy COPY model.onnx app.py /app/ WORKDIR /app CMD ["uvicorn", "app:app", "--host", "0.0.0.0"] ``` ```python # app.py from fastapi import FastAPI import onnxruntime as ort import numpy as np app = FastAPI() sess = ort.InferenceSession('model.onnx') @app.post('/predict') async def predict(data: dict): x = np.array(data['input'], dtype=np.float32) result = sess.run(None, {'input': x})[0] return {'output': result.tolist()} ``` image 大小: - PyTorch image: ~3 GB - ONNX Runtime image: ~150 MB container 启动快 + 部署成本低。 ## 浏览器跑 model(onnxruntime-web) ```html <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script> <script> const session = await ort.InferenceSession.create('/static/model.onnx'); const feeds = { input: new ort.Tensor('float32', data, [1, 3, 224, 224]) }; const results = await session.run(feeds); console.log(results.output.data); </script> ``` 不需要 server,model 跑在用户浏览器。 - 数据不出端(隐私) - 0 server cost - 适合:图片分类 / 文本嵌入 / 小型模型 mobile 浏览器 4-bit 量化后跑 MobileNet 50 ms。 ## 与 TFLite / CoreML 对比 | | ONNX Runtime | TFLite | CoreML | TorchScript | |---|---|---|---|---| | 跨平台 | 强 | 强 | iOS only | 中 | | Train 框架 | 全 | TF | 全 | PyTorch | | 性能 | 高 | 高(mobile) | 极高(apple) | 中高 | | 工具链 | 复杂但灵 | 简单 | 简单 | PyTorch 内置 | iOS 跑:CoreML 最快。 Android:TFLite 或 ONNX Runtime。 跨平台 / server:ONNX。 ## transformers 适配 HuggingFace optimum 让 transformers 一键转 ONNX: ```python from optimum.onnxruntime import ORTModelForSequenceClassification model = ORTModelForSequenceClassification.from_pretrained( 'distilbert-base-uncased-finetuned-sst-2-english', export=True, ) # 内部自动 export ONNX + 用 ORT 跑 ``` API 跟 transformers 一样,performance 是 ORT 加成。 ## 真实 case:BERT 文本分类部署 train: HF transformers + GPU。 原计划: PyTorch serve 在 t3.medium (2vCPU, 4GB)。 ``` PyTorch model: 440 MB 推理 latency: 350 ms / request RAM: 1.2 GB ``` 转 ONNX + quantize INT8: ``` ONNX INT8 model: 110 MB 推理 latency: 80 ms / request RAM: 350 MB ``` 成本 / 性能都改善。同 instance 能跑 4x QPS。 ## 不适合 ONNX 的场景 - **dynamic graph 复杂**(控制流多):ONNX op 不全 cover,export 失败 - **custom op**:必须写 ONNX custom op(C++) - **需要 train**:ONNX Runtime 主要 inference(有 training 但弱) - **frequent model update**:ORT runtime load 慢,热更新麻烦 train 阶段不动 PyTorch;deploy 阶段转 ONNX。 ## 踩过的坑 1. **opset version**:老 export 用 opset 11,新 ORT 默认要 17+。 不匹配 unsupported op。统一 opset 18+。 2. **dynamic shape**:忘 `dynamic_axes` → batch=1 hardcoded。生产 variable batch 报错。 3. **数值不一致**:FP16 / FP32 mix 后 train 和 ONNX 差 1%。生产 numerical 严格场景小心。 4. **custom op**:用了 PyTorch 特有 op (如 grid_sample 某些 mode) → ONNX export 报错。改 model 或者手写 ONNX op。 5. **runtime version mismatch**:onnx 库版本 vs onnxruntime 版本不 匹配 → load model 报错。pip 同一时间装。
## 起因 React 组件里调 API 老方法: ```jsx useEffect(() => { setLoading(true); fetch('/api/posts') .then(r => r.json()) .then(setPosts) .finally(() => setLoading(false)); }, []); ``` - 没 cache,组件 mount 都重 fetch - 没 retry / refetch - 没共享:两个组件用同 endpoint 各 fetch 一次 - 没 stale-while-revalidate - 错误处理累 `TanStack Query` (前 React Query) / `SWR` 解决这些。 ## SWR (Vercel) ```jsx import useSWR from 'swr'; const fetcher = (url) => fetch(url).then(r => r.json()); function Posts() { const { data, error, isLoading } = useSWR('/api/posts', fetcher); if (isLoading) return <Spinner />; if (error) return <Error />; return <List items={data} />; } ``` 简单 API:URL = cache key,fetcher 函数任意。 ### 优势 - API 极简 - bundle 小(4 KB) - Vercel 友好(Next.js 推荐) - focus revalidation 默认(窗口聚焦自动 refetch) ### 劣势 - mutation API 较弱 - 复杂场景能力上限低 - 文档比 TanStack Query 少 ## TanStack Query ```jsx import { useQuery } from '@tanstack/react-query'; function Posts() { const { data, error, isLoading } = useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()), }); if (isLoading) return <Spinner />; if (error) return <Error />; return <List items={data} />; } ``` API 类似但更"框架化"。 ### 优势 - 功能极完整(pagination / infinite / mutation / optimistic update) - devtools 强(专门 query inspector) - 框架无关(react / vue / solid / svelte 都有版本) - 大型项目首选 ### 劣势 - bundle 大(13 KB gzip) - 概念多(queryClient / mutation / invalidation) - 学习曲线高于 SWR ## 共享 cache 两者都按 key cache: ```jsx // 组件 A useSWR('/api/posts'); // 组件 B(同 URL) useSWR('/api/posts'); // → 只 fetch 一次,共享 data ``` TanStack Query 同理(按 queryKey)。 跨组件状态共享解决了,不需要 Redux 把 server state 塞 store。 ## stale-while-revalidate ``` 1. mount → 显 cached data (instant) 2. 后台 refetch 3. 新 data 到 → 静默 update ``` 用户感觉"立刻有数据" + 后台保持最新。 两者都默认 SWR 行为。 可配 `staleTime`:< staleTime 不 refetch(节省请求): ```jsx useQuery({ queryKey: ['posts'], queryFn, staleTime: 5 * 60 * 1000, // 5 min 内不重新 fetch }); ``` ## mutation (TanStack) ```jsx const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (newPost) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(newPost) }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }); }, }); function NewPostForm() { return <button onClick={() => mutation.mutate({ title: 'hi' })}>Save</button>; } ``` mutation 完后 invalidate `['posts']` query → 自动 refetch。 ## optimistic update ```jsx useMutation({ mutationFn: deletePost, onMutate: async (postId) => { await queryClient.cancelQueries({ queryKey: ['posts'] }); const prev = queryClient.getQueryData(['posts']); queryClient.setQueryData(['posts'], (old) => old.filter(p => p.id !== postId)); return { prev }; }, onError: (err, postId, ctx) => { queryClient.setQueryData(['posts'], ctx.prev); // rollback }, }); ``` UI 立刻显示删除 → API 失败 → 自动 rollback。 专业级 UX,TanStack Query 内置 helper。 SWR 也能做但要手动。 ## pagination ```jsx const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam = 1 }) => fetch(`/api/posts?page=${pageParam}`).then(r => r.json()), getNextPageParam: (lastPage) => lastPage.next_page, initialPageParam: 1, }); ``` `fetchNextPage` 加载下一页,data 累积。 infinite scroll 一行代码。 SWR 的 `useSWRInfinite` 类似但 API 略别扭。 ## prefetching ```jsx // hover 时预拉数据 function PostLink({ postId }) { const queryClient = useQueryClient(); return ( <a onMouseEnter={() => queryClient.prefetchQuery({ queryKey: ['post', postId], queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json()), })} > ... </a> ); } ``` 用户点链接前已经预 fetch → 点击 = 立刻有数据。 ## devtools TanStack Query devtools: ```jsx import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; <> <App /> <ReactQueryDevtools /> </> ``` floating 浮窗看所有 active query / state / cache / refetch。 debug 神器。 SWR devtools 弱很多。 ## 选择 - **简单项目 / 几个 query** → SWR - **复杂应用 / 多 mutation / 需 devtools** → TanStack Query - **Next.js + SWR 风格** → SWR - **跨框架** → TanStack Query(React/Vue/Svelte/Solid 都行) 我的默认是 TanStack Query(功能完整 + 不会后悔)。 ## 与 RTK Query 对比 RTK Query (Redux Toolkit): - Redux 圈子的查询库 - 跟 Redux store 集成深 - 学习曲线更陡 已经用 Redux → RTK Query 自然。 没用 Redux → TanStack Query / SWR 更轻。 ## Server Components 时代 React 19 + Next.js App Router 倾向 Server Components 直接 fetch: ```tsx // Server Component async function Posts() { const posts = await fetch('https://...').then(r => r.json()); return <List items={posts} />; } ``` server-side 数据获取不需要 query 库。 但 client-side mutation / interactive 数据还是需要 TanStack Query / SWR。 混用:server fetch 初始 → client query 后续更新。 ## 真实 case 我们一个 dashboard 用 TanStack Query: - 20+ query (各种 metric / list) - 10+ mutation - 大量 prefetch (hover 看 detail) - 自动 refetch on window focus(用户回来看最新) 效果: - delete useEffect / useState 50% - 用户体验显著(看不到 loading spinner,always cached) - bundle +13 KB but worth 之前手写 useEffect + Context:500 行 boilerplate。 TanStack Query:100 行 query / mutation definition。 ## 踩过的坑 1. **queryKey 写错**:写 string 而非 array → cache 不 share 或者错 share。永远 array:`['posts', filter, page]`。 2. **fetchOptions vs queryFn**:query 库不强制 fetch impl。 传 axios / ofetch 都行,但要在 queryFn 返 promise<data>。 3. **invalidation 过粗**:mutation 后 `invalidateQueries({ queryKey: ['posts'] })` invalidate 所有 posts*。可以更精确 `exact: true`。 4. **stale closure in mutation**:onSuccess 用 useState 值过时 → 用 functional update 或 ref。 5. **SSR hydration mismatch**:SSR data 跟 client first query 不一 致 → flicker。两者都有 hydration helper(HydrationBoundary 等)。
## 起因 K8s pod 用 PV: - 云上:EBS / GCE PD / Azure Disk - 自托管:hostPath(坏到没救)/ NFS(慢 + 没 snapshot)/ Ceph(运维炸) 需要"PVC 跑 stateful 应用"但又不在云上: - 边缘 cluster - 自建机房 - 本地 dev / 小 prod **Longhorn**(Rancher / SUSE,CNCF):K8s-native distributed block storage。 轻量 + 部署简单 + UI 友好。 ## 装 ```bash helm install longhorn longhorn/longhorn \ -n longhorn-system --create-namespace ``` 每 node 自动跑 longhorn-manager pod + 用本地 disk 做 storage。 `StorageClass` 自动创建: ```bash kubectl get sc longhorn (default) driver.longhorn.io Delete Immediate ``` ## 用 ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: data spec: storageClassName: longhorn accessModes: [ReadWriteOnce] resources: requests: storage: 10Gi ``` ```yaml spec: containers: - volumeMounts: - mountPath: /data name: data volumes: - name: data persistentVolumeClaim: claimName: data ``` 挂载即用。Longhorn 在背后: - 找有空间的 node 创建 volume - replica 跨 3 node 同步(默认) - pod schedule 到任意 node,volume 自动跟随 ## replica 跨 node 3 replica = 3 个 node 各存一份。 任一 node 挂 → 还有 2 副本,pod 在别 node 重启接着用。 ## snapshot + backup ```yaml # 创建 snapshot apiVersion: longhorn.io/v1beta2 kind: Snapshot metadata: name: snap-1 namespace: longhorn-system spec: volume: pvc-xxx ``` snapshot 是 volume 状态 point-in-time copy(本地,秒级)。 backup 是把 snapshot 上传到 S3 / NFS: ```bash # Longhorn UI 配 S3 backup target # 或者 CR ``` ```yaml apiVersion: longhorn.io/v1beta2 kind: Backup metadata: name: backup-1 spec: snapshotName: snap-1 ``` 恢复:从 backup 创建新 volume → PVC → pod 挂载。 跨集群迁移:cluster A backup → cluster B restore。 ## recurring job ```yaml apiVersion: longhorn.io/v1beta2 kind: RecurringJob metadata: name: daily-backup spec: cron: "0 2 * * *" task: backup retain: 7 groups: [default] ``` 每天 2:00 自动 snapshot + backup,保留 7 份。 ## UI ```bash kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80 # http://localhost:8080 ``` UI 看 volume / replica 状态 / backup / settings。 比 yaml 直观,运维友好。 ## 性能 3-node cluster + NVMe disk + Longhorn 3 replica: - random read 4k: ~50k IOPS - random write 4k: ~15k IOPS - sequential write: ~600 MB/s 跟单盘比有 30-50% overhead(network + replication)。 跟 EBS gp3 接近。够多数 workload。 ## 与 Ceph (Rook) 对比 | | Longhorn | Rook-Ceph | |---|---|---| | 复杂度 | 低 | 极高 | | 性能 | 中 | 高 | | scale | 中(几十 node) | 巨(几百+) | | object storage | ❌ | ✅ | | file storage (NFS-like) | RWX 实验 | ✅ | | block storage | ✅ | ✅ | 中小 cluster + 主要 block storage → Longhorn。 大 scale + 需 object / file → Ceph。 ## RWX (多 pod 同时读写) ```yaml accessModes: [ReadWriteMany] # RWX ``` Longhorn RWX 用内置 NFS share volume 之上。 性能比 RWO 弱,但能用。 ReadWriteOncePod (K8s 1.27+) 是更严格 RWO。 ## 与 hostPath ```yaml volumes: - name: data hostPath: path: /mnt/data ``` 简单粗暴 但: - pod schedule 必须在那 node(pin) - 没备份 / 副本 - 移走 node 数据丢 dev 还行,prod 别用。 ## 与 NFS NFS provisioner:装个 NFS server + provisioner,PVC 在 NFS 上分 volume。 | | NFS | Longhorn | |---|---|---| | RWX | ✅ | ✅(弱) | | 性能 | 中 | 高 | | HA | NFS server 单点 | replica HA | | 部署 | 简 | 中 | 小数据 / RWX 重 → NFS 简单。 HA / 性能 → Longhorn。 ## 真实 case 某客户私有云 cluster: - 5 node bare metal - 每 node 1 TB NVMe - Longhorn 3 replica - PG / Redis / app data 全 Longhorn 效果: - Postgres 跑得跟单盘差 30%(可接受) - 任一 node down → pod 自动迁移 + volume 跟随 - 每天自动 backup 到 S3 - UI 让 ops 直观看 storage 状态 挑战: - 网络 IO 飙(replica sync 占带宽)→ 10 GbE 网卡 - node 重启时 replica rebuild 几小时 ## 监控 Prometheus metrics: - `longhorn_volume_actual_size_bytes` - `longhorn_volume_state` - `longhorn_node_status` Grafana dashboard 官方提供。 alert 关键: - replica 不足(应 3 实际 < 3) - volume detached - node down - backup 失败 ## 与 cloud volume 对比 | | Longhorn | EBS / PD | |---|---|---| | 部署 | 自管 | 托管 | | 跨 AZ | 自配 | 内置 | | 性能 | 看本地 disk | 一致 | | 成本 | hardware 一次性 | 按月 | | 适合 | 自托管 / 边缘 | 云 | 在云上没必要 Longhorn(用 EBS)。 自托管 / 混合云 → Longhorn 填补"K8s storage" 空白。 ## 踩过的坑 1. **每 node 一份 replica = 数据膨胀**:3 replica 占 3x 空间。 规划存储 capacity * 3。 2. **kernel module**:Longhorn 用 iSCSI 协议。某些 minimal OS 没装 `open-iscsi` → 启不来。 3. **disk fill 90%**:默认 reserve 30% 给系统。改 `Storage Minimal Available Percentage`。 4. **node drain 慢**:drain 时 volume detach + reattach 慢(几十秒)。 maintenance window 留时间。 5. **backup target 配错**:S3 endpoint / credential 错 → 备份失败但 UI 显示成功初看。定期手 restore 验证。
## 起因 单 PG 写多读多,CPU 经常 80%+。临时方案是加内存 + SSD,但读 query 还是抢主库 CPU。 PG 自带的 streaming replication 几乎免费——配一个 standby 把读流量 分过去,写仍走主。 ## 整体架构 ``` 应用 (写) 应用 (读) ↓ ↓ 主库 (primary) ----→ 从库 (standby) WAL 流 ``` - 主库正常处理读写 - 从库实时 replay 主库 WAL,几乎实时同步(毫秒级延迟) - 从库只读,可以服务 SELECT 查询 - 主库挂了 → 提升从库为新主 ## 解决方案 ### 1. 主库配置 `/etc/postgresql/16/main/postgresql.conf`: ``` wal_level = replica max_wal_senders = 5 wal_keep_size = 1GB # 保留多少 WAL 给落后的 standby 追 hot_standby = on ``` `/etc/postgresql/16/main/pg_hba.conf` 允许复制连接: ``` host replication replicator <standby_ip>/32 scram-sha-256 ``` 创建复制用户: ```sql CREATE USER replicator REPLICATION LOGIN PASSWORD 'strong-pass'; ``` `sudo systemctl restart postgresql`。 ### 2. 从库初始化(pg_basebackup) 从库**全空状态**(删 data 目录或新机器): ```bash sudo systemctl stop postgresql sudo rm -rf /var/lib/postgresql/16/main/* sudo -u postgres pg_basebackup \ -h <primary_ip> -U replicator -p 5432 \ -D /var/lib/postgresql/16/main \ -Fp -Xs -P -R ``` `-R` 自动生成 `postgresql.auto.conf` + `standby.signal`, 让这个 instance 启动后就是 standby。 ```bash sudo systemctl start postgresql # 校验 sudo -u postgres psql -c "SELECT pg_is_in_recovery();" # t (true,是 standby) ``` ### 3. 在主库上看复制状态 ```sql SELECT client_addr, state, sent_lsn, write_lsn, replay_lsn, sync_state, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes FROM pg_stat_replication; -- client_addr | state | ... | lag_bytes -- 10.0.0.2 | streaming | ... | 0 ``` `state=streaming` + `lag_bytes` 接近 0 = 健康。 ### 4. 应用读写分离 最简单:用两个连接池。 ```python # Django settings DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'HOST': 'primary.db.local', 'NAME': 'myapp', 'USER': 'app', 'PASSWORD': '...', }, 'replica': { 'ENGINE': 'django.db.backends.postgresql', 'HOST': 'standby.db.local', 'NAME': 'myapp', 'USER': 'app', 'PASSWORD': '...', }, } DATABASE_ROUTERS = ['myapp.routers.PrimaryReplicaRouter'] ``` ```python # routers.py class PrimaryReplicaRouter: def db_for_read(self, model, **hints): return 'replica' def db_for_write(self, model, **hints): return 'default' def allow_relation(self, obj1, obj2, **hints): return True def allow_migrate(self, db, app_label, **hints): return db == 'default' ``` 读 query 自动走 replica,写走 primary。 Node / Python 其它框架同理:分两个连接池,业务代码按操作类型选。 ### 5. 同步 vs 异步复制 默认**异步**:主库 commit 立刻返回,WAL 后台 stream 到 standby。 代价:主挂时 standby 可能差几秒数据。 切**同步**(commit 等 standby 确认): ``` # postgresql.conf synchronous_standby_names = 'standby1' synchronous_commit = on ``` 代价:standby 慢 / 挂时主库写阻塞。生产建议 quorum: ``` synchronous_standby_names = 'ANY 1 (standby1, standby2, standby3)' ``` 3 个 standby 任一确认即可——既保证 RPO=0 又有容错。 ### 6. 自动 failover:repmgr / Patroni PG 自带不做"主挂了自动提升 standby"。需要外部工具: - **repmgr**:简单成熟 - **Patroni**:基于 etcd / Consul,K8s 友好 - **pg_auto_failover**:Citus 出品 最简 repmgr: ```bash sudo apt install postgresql-16-repmgr # 注册主库 sudo -u postgres repmgr -f /etc/repmgr.conf primary register # 注册 standby sudo -u postgres repmgr -f /etc/repmgr.conf standby register # 启动 daemon(监控 + 自动 failover) sudo systemctl enable --now repmgrd ``` 主挂后 repmgrd 30 秒内提升某 standby 为新主,更新所有节点配置。 应用层用 PgBouncer + 监听 repmgr 事件改 backend 指向新主。 或者用 HAProxy 在前面做 health check: ``` backend pg_primary option pgsql-check user healthcheck server primary primary.db:5432 check server standby1 standby1.db:5432 check backup ``` `backup` 表示 standby 在 primary down 时才接流量。 ### 7. 监控复制延迟 ```sql -- 主库看每个 standby 的延迟(bytes) SELECT application_name, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) / 1024 / 1024 AS lag_mb FROM pg_stat_replication; -- standby 上看延迟(秒) SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS lag_seconds; ``` Prometheus postgres_exporter 自动暴露这些指标。 告警阈值:lag > 30 秒 → warning,> 5 分钟 → critical。 ### 8. logical replication(不同 schema 选择性复制) streaming replication 是物理(整库 / 所有表 / 同版本)。 logical replication(PG 10+)按表选择性复制,可跨版本: ```sql -- 主库 CREATE PUBLICATION mypub FOR TABLE users, posts; -- 订阅端(可以是另一台 PG,不要求版本一致) CREATE SUBSCRIPTION mysub CONNECTION 'host=primary user=replicator dbname=myapp password=...' PUBLICATION mypub; ``` 用于:跨版本升级(旧版做 publication,新版做 subscription,同步后切流量)、 ETL(把 production 部分表 logical 复制到分析库)。 ## 效果 我们配主从后: - 主库 CPU 80% → 35%(读流量去 standby) - 报表 query(重 read)不再影响业务写性能 - 主库挂过一次,repmgr 28 秒 failover,业务无感知 - 监控显示复制延迟稳定 < 100ms ## 与其它扩容方案对比 | | 物理复制(流) | 逻辑复制 | 读写分离中间件 | Citus / 分库 | |---|---|---|---|---| | 复杂度 | 低 | 中 | 中 | 高 | | 适合 | 读扩容 / HA | 跨版本 / 部分表 | 多主 | 横向扩容 PB 级 | | 跨主版本 | ❌ | ✅ | N/A | ✅ | | 自动 failover | 需 repmgr/Patroni | 难 | ✅ | ✅ | ## 踩过的坑 1. **standby IP 防火墙**:5432 端口必须从主库 → standby、standby → 主库 双向通(standby 需要拉 WAL)。 2. **wal_keep_size 太小**:standby 落后超过这个大小后 WAL 被回收, standby 无法追上 → 必须 full re-base。生产至少 1-10 GB。或者用 replication slot(slot 让 PG 保留 WAL 直到 slot 消费完): ```sql SELECT pg_create_physical_replication_slot('slot_standby1'); ``` standby 配 `primary_slot_name='slot_standby1'`。 3. **standby 上跑长 query 阻塞复制**:standby 默认会 cancel 长 query 让复制优先。要避免就调 `max_standby_streaming_delay = 30s`(query 能跑多久)。 4. **switchover 后没清旧主**:旧主重启后会变 "split brain"(同时两个 主)。一定先 demote 旧主或者关掉 PG service。 5. **同步复制 standby 全挂主库 hang**:synchronous + 没 standby 时主库 write 阻塞等。设 `synchronous_commit = local` 或者 quorum `ANY 1` 避免单点。
## 起因 每个项目都有一堆"开发命令": ```bash pnpm dev docker compose up -d uv run pytest ruff check . && mypy . docker compose exec api python manage.py shell ``` 放哪? - `package.json` scripts:JS 项目 OK,但 Python / Rust 项目不自然 - `Makefile`:tab vs space / 老语法 / shell 转义恶心 / 不跨平台 - README 里 copy-paste:散 - shell alias:项目特定的不该污染全局 `just` 是 Rust 写的命令记事本 / task runner。`justfile` 取代 Makefile, 专门为"跑命令" 设计(不为编译 build artifact)。 ## 装 ```bash brew install just # macOS cargo install just # 通用 sudo apt install just # ubuntu 22.04+ ``` ## 基础 justfile ```just # justfile(项目根) # 默认 task:列出所有 default: @just --list # 装依赖 install: uv sync pnpm install # 启动 dev dev: docker compose up -d db redis uv run python manage.py runserver # 跑测试 test: uv run pytest -x # 检查代码质量 lint: ruff check . mypy . pnpm tsc --noEmit ``` 跑: ```bash $ just # 默认 → just --list $ just install $ just test $ just lint ``` ## vs Makefile ```make # Makefile 等价 .PHONY: install test install: uv sync pnpm install test: uv run pytest -x ``` - Makefile 要 **tab** 缩进(编辑器配错就崩) - Makefile 假设是"build artifact"(filename dependency),shell 当 side effect - `$(VAR)` `$$VAR` 转义恶心 - 跨平台不一致(GNU make vs BSD make) `just` 直接 shell 命令 / 空格缩进 / 跨平台一致 / 默认无 dependency 跟踪(这是 feature,不是 bug,跑命令不需要)。 ## 参数 ```just # 带参数 greet name='world': echo "hello {{name}}" # 多 arg deploy env target: ./deploy.sh {{env}} {{target}} ``` ```bash $ just greet hello world $ just greet Alice hello Alice $ just deploy prod web ``` 参数变量用 `{{name}}` 引用(Mustache 风格)。 ## recipe 调 recipe ```just build: pnpm build deploy: build ./deploy.sh # 或者显式 release: just lint just test just build ./release.sh ``` ## 用 python / node 写 recipe ```just # bash(默认) hello: echo "hello" # python analyze: #!/usr/bin/env python3 import json with open('data.json') as f: data = json.load(f) print(f"items: {len(data)}") # node gen: #!/usr/bin/env node console.log('hi from node'); ``` `#!` 第一行指定 shebang → 整个 recipe 当一个 script 跑(不是逐行)。 ## 环境变量 ```just # justfile 顶部 set set dotenv-load # 自动加载 .env # 默认变量 NAME := "myapp" PORT := env_var_or_default("PORT", "8000") run: PORT={{PORT}} ./{{NAME}} # Linux specific [linux] clean: rm -rf build/ [macos] clean: rm -rf build/ .DS_Store ``` `[linux]` `[macos]` recipe attribute 让同 recipe 在不同平台用不同命令。 ## 列出 + help ```bash $ just --list Available recipes: default # 列出所有 install # 装依赖 test # 跑测试 # recipe 上面的注释自动当 docstring ``` 每个 recipe 顶部加注释 → 自动是描述。新人 clone 项目 `just` 一下立刻 知道有什么任务。 ## 我的常用模板 ```just # justfile set dotenv-load set positional-arguments PYTHON := "uv run" default: @just --list # 初始化(一键 onboarding) init: uv sync pnpm install docker compose up -d db {{PYTHON}} python manage.py migrate {{PYTHON}} python manage.py createsuperuser --noinput || true # 开发 dev: docker compose up -d db redis {{PYTHON}} python manage.py runserver 0.0.0.0:8000 # 测试 + 覆盖率 test *args='': {{PYTHON}} pytest -x --cov=. {{args}} # 代码检查(CI 同样跑) lint: {{PYTHON}} ruff check . {{PYTHON}} ruff format --check . {{PYTHON}} mypy . # 自动 fix fix: {{PYTHON}} ruff check . --fix {{PYTHON}} ruff format . # DB migrate / shell / etc migrate: {{PYTHON}} python manage.py migrate shell: {{PYTHON}} python manage.py shell makemigrations *args='': {{PYTHON}} python manage.py makemigrations {{args}} # 部署相关 deploy env: @echo "deploying to {{env}}..." ./scripts/deploy.sh {{env}} ``` `*args=''` 收尾通过位置传给 recipe(如 `just test forum/tests.py`)。 ## 与 npm scripts 对比 `package.json`: ```json { "scripts": { "dev": "next dev", "build": "next build", "test": "vitest" } } ``` - JSON 单行字符串 → 复杂命令难看 - 跨平台 (Windows) 转义不一致 - 只能 cd 到 package.json 所在目录跑 `just` 更适合多 layer / 多语言项目。npm script 适合纯 JS 单项目。 很多项目我两个都用:`package.json` 给 JS 的 thin wrapper,`justfile` 给跨语言 orchestration(justfile 调 npm script)。 ## CI 集成 GitHub Actions: ```yaml - uses: extractions/setup-just@v2 - run: just lint - run: just test ``` CI = 本地的 `just`,绝对一致。 ## 与 mage / task / make 对比 | | just | make | task (taskfile.dev) | mage | |---|---|---|---|---| | 语言 | rust | C | go | go | | 配置 | justfile | Makefile | Taskfile.yml | magefile.go | | 跨平台 | ✅ | 弱 | ✅ | ✅ | | 学习曲线 | 低 | 中 | 低 | 中 | | 适合 | 通用任务跑 | C/C++ build | 现代替代 make | go 项目 | just 是"现代 Make 替代品 for 命令"。如果你在做实际编译依赖跟踪(如 C/C++ build),用 make 或 cmake。 ## 踩过的坑 1. **PATH 不一致**:CI 跑 just → recipe 里 `npx` 找不到。`set shell := ["bash", "-cu"]` + ensure tool 装好。 2. **dotenv 自动加载敏感变量到子进程**:`set dotenv-load` 让 `.env` 变量进所有 recipe shell。生产 secrets 不要放项目根 `.env`,分开。 3. **recipe shell 默认 sh 不是 bash**:`pipefail` 等 bash-only。 `set shell := ["bash", "-cu"]` 改。 4. **变量插值与 shell 冲突**:`{{VAR}}` 是 just 模板,shell 里用 `${VAR}` 是 shell 变量。混着用要小心。 5. **递归 just 调 just**:可以但易嵌套深 → 跑得乱。一般保持扁平结构。
## 起因 我的服务器上 Docker daemon 用 root 跑。任何能跟 docker.sock 通信的用户 都等于 root(passing socket 到容器 = 容器逃逸)。多用户 / 共享开发机 上风险大。 `podman` 是 Red Hat 主导的 OCI 兼容容器引擎,几个区别: - **no daemon**:每个容器一个进程 fork 出来,systemd 直接 supervise - **rootless first**:默认非 root 用户跑 - **Docker CLI 兼容**:`alias docker=podman` 大多数命令直接 work - **支持 pod**(K8s 那种多容器组) ## 装 ```bash # Debian 12+ / Ubuntu 22.04+ sudo apt install -y podman # 给当前用户 subuid / subgid(rootless 必须) # /etc/subuid + /etc/subgid 应当各有: # me:100000:65536 # 默认装好就有;没有就: sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 me # 配置 storage(rootless 在 ~/.local/share/containers/) podman info | grep -A 3 storage ``` ## 基础用法(跟 Docker 几乎一样) ```bash podman pull nginx:alpine podman run -d --name web -p 8080:80 nginx:alpine podman ps podman logs -f web podman exec -it web sh podman stop web podman rm web ``` `alias docker=podman` 后大部分 docker 命令直接用。 ## rootless 网络 Rootless 容器不能 bind 80/443(< 1024 需 root)。 解决: ```bash # 方案 A:bind 8080,前置 nginx 反代 podman run -d -p 8080:80 nginx # 方案 B:开 unprivileged port range sudo sysctl net.ipv4.ip_unprivileged_port_start=80 # 之后 rootless 也能 bind 80 ``` 方案 B 安全风险:任何用户能 bind 80。多人共享机器不建议。 ## podman generate systemd 杀手锏:把 container 转成 systemd unit: ```bash podman run -d --name web -p 8080:80 nginx:alpine # 生成 unit 文件 podman generate systemd --name web --files --new # 输出:container-web.service mkdir -p ~/.config/systemd/user/ mv container-web.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now container-web.service # 开机自启(必须 enable linger) loginctl enable-linger $USER ``` 之后 systemctl 全程管理 container: ```bash systemctl --user status container-web systemctl --user restart container-web journalctl --user -u container-web -f ``` 容器 = systemd service。一致性极佳。 ## Quadlet(podman 4.4+ 推荐) 更现代的方式:`.container` 文件直接被 systemd 当 unit: `~/.config/containers/systemd/web.container`: ```ini [Unit] Description=Web server [Container] Image=docker.io/library/nginx:alpine PublishPort=8080:80 Volume=/home/me/www:/usr/share/nginx/html:Z AutoUpdate=registry [Service] Restart=always [Install] WantedBy=default.target ``` ```bash systemctl --user daemon-reload systemctl --user start web # 注意是 web 不是 web.container ``` 比 generate systemd 简洁得多。每改 image / volume / port 直接改 .container 文件 + daemon-reload。 ## Pod(多容器组) K8s 风格的 Pod(共享 network + IPC): ```bash podman pod create --name myapp -p 8080:80 podman run -d --pod myapp --name api myapi:latest podman run -d --pod myapp --name worker myworker:latest podman pod ls ``` api 和 worker 共享网络(同 localhost),共享 IPC namespace。 适合"sidecar" 模式。 K8s YAML 直接转 podman pod: ```bash podman play kube k8s-pod.yaml ``` 支持基础 K8s YAML。 ## auto-update ```bash # 容器加 label podman run -d --label io.containers.autoupdate=registry nginx:alpine # 系统级 timer 自动检查 + 升级 systemctl --user enable --now podman-auto-update.timer ``` 每天检查 registry 上 image 新版,有就 pull + restart 容器。Watchtower 等价。 ## docker-compose 兼容 ```bash sudo apt install -y podman-compose # 或:pip install podman-compose podman-compose up -d # 大部分 docker-compose.yml 直接 work ``` 或者用 `podman compose`(subcommand,跟 Docker Compose v2 接近)。 少数 docker-only feature(如 `network_mode: bridge` 的特定行为) 偶尔不兼容,看具体场景。 ## 与 Docker 对比 | | Docker | podman | |---|---|---| | daemon | dockerd (root) | 无 daemon(每容器自己进程) | | rootless | 实验性 + 有限 | 默认 + 一等公民 | | systemd 集成 | 弱 | 极强(Quadlet) | | Docker CLI 兼容 | 原生 | 极高 | | Compose | docker compose | podman-compose / podman compose | | K8s 友好 | Pod 概念无 | 原生 Pod | | 性能 | 略好(daemon overhead 摊销) | 接近 | | 生态成熟度 | 极高 | 高 + 增长中 | | RHEL / Fedora 默认 | ❌ | ✅ | Red Hat 系(RHEL / CentOS Stream / Fedora)官方推 podman。 其它 distro 都装得上。 ## 适用场景 ✅ **podman 适合**: - 单机部署 + 喜欢 systemd 集成 - 多用户开发机(rootless 安全) - 不需要 docker swarm - RHEL / Fedora 用户 - K8s 学习(pod 概念友好) ❌ **保 Docker 适合**: - 重度用 docker swarm / docker compose 高级 feature - 团队工具链都基于 docker - 公司 SaaS 工具明确支持 Docker(如 GitHub Actions docker-container action) ## 我的实际迁移 家用服务器(10+ 容器:nginx / db / 监控 / 个人 app)从 docker 迁 podman + Quadlet: ```ini # ~/.config/containers/systemd/postgres.container [Container] Image=postgres:16-alpine PublishPort=127.0.0.1:5432:5432 Environment=POSTGRES_PASSWORD=... Volume=postgres-data.volume:/var/lib/postgresql/data HealthCmd=pg_isready -U postgres HealthInterval=10s [Install] WantedBy=default.target ``` 每个容器一个 .container 文件,git 管。 重启服务器后 systemd 自动按顺序起所有容器。 ### 效果 - 总内存:少 200 MB(无 dockerd 常驻) - 部署 / 重启容器跟 systemctl service 一致 - 容器崩溃 / OOM / 异常退出:journal 里完整 log + restart 历史 - rootless 让"容器漏洞 → 拿 host root" 这条路被堵 - 一致 backup:备份 ~/.local/share/containers + ~/.config = 完整状态 ## 踩过的坑 1. **rootless 网络默认 slirp4netns** 慢:吞吐量大幅低于 root docker。 高流量场景配 `pasta` (新版默认) 或者 root pod。 2. **rootless 不能跨用户访问 image**:每个用户独立 storage。 `podman --root=/path/to/system-storage` 可以共享但复杂。 3. **某些 docker-only env var**:如 `DOCKER_HOST` socket 路径。 podman 用 unix socket 在 `$XDG_RUNTIME_DIR/podman/podman.sock`。 4. **selinux 标签**:volume mount 缺 `:Z` / `:z` 后缀容器内访问权限错。 `-v /path:/path:Z` 让 SELinux 自动 relabel。 5. **GPU passthrough**:rootless + GPU 不直接 work。`--device /dev/nvidia*` + udev rule 配。生产推荐 root podman 跑 GPU 容器。
## 起因 新 landing page 在 Lighthouse Mobile 上跑只有 45 分(红色)。 boss 看到 PageSpeed Insights 截图骂街。 "哪些指标拉低分数?怎么改?" 系统化地走一遍。 ## 6 个关键指标 Lighthouse 综合分基于: | 指标 | 解释 | 目标 | |---|---|---| | **LCP** (Largest Contentful Paint) | 首屏最大元素显示时间 | < 2.5s | | **INP** (Interaction to Next Paint) | 用户交互响应延迟 | < 200ms | | **CLS** (Cumulative Layout Shift) | 累计布局偏移 | < 0.1 | | FCP (First Contentful Paint) | 任何内容首次显示 | < 1.8s | | Speed Index | 视觉填充速度 | < 3.4s | | TBT (Total Blocking Time) | 主线程被阻塞总时长 | < 200ms | LCP / INP / CLS 是 "Core Web Vitals",Google 排名因子。 ## 我做的 7 个改动 ### 1. 给关键图片加 `fetchpriority="high"` 和 preload ```html <head> <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high"> </head> <body> <img src="/hero.jpg" fetchpriority="high" loading="eager" width="1200" height="600" alt="..."> </body> ``` 之前 hero 图被各种 CSS / JS 排到后面,LCP 4s。 preload + high priority 后 1.2s。 ### 2. 字体 preload + font-display: swap ```html <link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin> <style> @font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: swap; /* 字体没下来用 fallback,不阻塞渲染 */ } </style> ``` 之前自定义字体阻塞文字渲染 800ms。改后字体没到先用 system fallback。 ### 3. 关闭未使用的第三方脚本 PageSpeed 的 "Reduce unused JavaScript" 报告告诉我 Google Analytics + Hotjar + Intercom 注入了 280KB JS。 - Hotjar 临时关(运营暂时不需要) - Intercom 改 "页面停留 > 10s 才加载" - GA 切到 `gtag.js`(最小版) JS bundle 从 320 KB → 80 KB。 ### 4. CSS critical path:inline above-the-fold + defer 其余 ```html <head> <style> /* 首屏关键 CSS inline 进 HTML */ body{font-family:system-ui;...} .hero{...} .nav{...} </style> <link rel="preload" href="/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="/main.css"></noscript> </head> ``` 之前 main.css 阻塞 render 600ms。inline critical CSS 后渲染立刻开始。 工具:`critters` / `critical` npm 包能自动提取 critical CSS。 ### 5. 图片用 AVIF / WebP + responsive srcset ```html <picture> <source srcset="/hero-800.avif 800w, /hero-1600.avif 1600w" sizes="100vw" type="image/avif"> <source srcset="/hero-800.webp 800w, /hero-1600.webp 1600w" sizes="100vw" type="image/webp"> <img src="/hero-800.jpg" alt="" width="1600" height="800" loading="eager" fetchpriority="high"> </picture> ``` AVIF 比 JPEG 小 50%+。手机用 800w 版本,平板用 1600w。 图片总下载量从 1.2 MB → 300 KB。 ### 6. 移除阻塞渲染的 JS(defer / async) ```html <!-- 之前 --> <script src="/analytics.js"></script> <!-- 阻塞 --> <script src="/vendor.js"></script> <!-- 阻塞 --> <!-- 现在 --> <script src="/main.js" defer></script> <!-- 等 HTML parse 完才执行 --> <script src="/analytics.js" async></script><!-- 不阻塞,下载完就跑 --> ``` - `defer`:HTML parse 完 + DOMContentLoaded 前按顺序执行 - `async`:下载完立刻执行,不保顺序 - 不带:阻塞 parser(最差) ### 7. CLS:所有 image / iframe 写 width/height ```html <!-- 错 --> <img src="/photo.jpg" alt=""> <!-- 加载完撑高页面 → 跳动 --> <!-- 对 --> <img src="/photo.jpg" width="800" height="600" alt=""> ``` 让浏览器在图片下载完前知道占位空间。CSS 用 `max-width:100%; height:auto` 保持响应式。 CLS 从 0.34 → 0.02。 ## 跑 Lighthouse Chrome DevTools → Lighthouse → 选 Mobile + Performance → Generate report. 或者命令行 CI 跑: ```bash npm i -D lighthouse @lhci/cli lhci autorun --upload.target=temporary-public-storage ``` `@lhci/cli` 能跑多次取中位数 + 上传报告 + PR 评论分数变化。 ## 结果 | | 改前 | 改后 | |---|---|---| | Performance | 45 | 95 | | LCP | 4.2s | 1.4s | | INP | 380ms | 120ms | | CLS | 0.34 | 0.02 | | TBT | 850ms | 110ms | | JS bundle | 320 KB | 80 KB | | 首屏图片 | 1.2 MB | 300 KB | ## 持续优化 ### CI 限红线 ```yaml # lighthouse.config.js module.exports = { ci: { assert: { assertions: { 'categories:performance': ['error', { minScore: 0.9 }], 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], }, }, }, } ``` LCP > 2.5s 的 PR 直接 fail。防止性能慢慢退化。 ### 真实用户数据:web-vitals + analytics ```js import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals' const send = (metric) => { navigator.sendBeacon('/analytics/vitals', JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, })) } onCLS(send) onINP(send) onLCP(send) onFCP(send) onTTFB(send) ``` 收集真实用户的 web vitals 上报到自己的分析后端。Lighthouse 是合成测试, 真实用户在各种设备 / 网络下的体验才是终极目标。 ## 踩过的坑 1. **测试环境 vs 生产**:dev server 没 gzip / 没缓存 → 跑 Lighthouse 分数极低。永远在 staging / prod-like build 上跑。 2. **`fetchpriority="high"` 滥用**:所有 img 都设 high → 优先级失效。 只首屏关键 1-2 个图设。 3. **inline critical CSS 太多**:> 14 KB(一个 TCP 窗口)反而慢,因为 阻塞了 HTML 后续 stream。生产建议 < 10 KB。 4. **Hydration 慢拉低 INP**:React app 大 bundle hydrate 时主线程被 占。RSC + 把交互组件拆细减少 hydration scope。 5. **PSI mobile 分数比 Desktop 低很多**:移动用 throttled CPU + 慢 4G。 永远以 mobile 为准(用户主要在手机)。
## 起因 我们的 RAG 系统每天调 OpenAI `text-embedding-3-small` 几十万次,账单 $300+/月。而且内部知识库不能出公网。bge-m3 是智源开源的多语言 embedding 模型,质量在 MTEB 中文榜上经常排前 3,比 OpenAI 3-small 还好。 本地跑一张 4090 就够。 ## 解决方案:FastAPI 包一层 OpenAI 兼容接口 让现有用 OpenAI Python SDK 的代码改一行 base_url 就能切。 ### 装 ```bash uv add fastapi 'uvicorn[standard]' sentence-transformers # 第一次启动会下载 bge-m3 模型 (~1.2 GB) ``` ### service.py ```python import logging from contextlib import asynccontextmanager from typing import List import torch from FlagEmbedding import BGEM3FlagModel from fastapi import FastAPI, HTTPException from pydantic import BaseModel log = logging.getLogger('embedding') class EmbeddingRequest(BaseModel): input: str | List[str] model: str = 'bge-m3' encoding_format: str | None = 'float' class EmbeddingData(BaseModel): object: str = 'embedding' embedding: List[float] index: int class EmbeddingResponse(BaseModel): object: str = 'list' data: List[EmbeddingData] model: str usage: dict model: BGEM3FlagModel | None = None @asynccontextmanager async def lifespan(app: FastAPI): global model log.info('loading bge-m3 ...') model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=torch.cuda.is_available()) log.info('ready') yield app = FastAPI(lifespan=lifespan) @app.post('/v1/embeddings', response_model=EmbeddingResponse) def embed(req: EmbeddingRequest): if model is None: raise HTTPException(503, 'model loading') texts = [req.input] if isinstance(req.input, str) else req.input if not texts: raise HTTPException(400, 'empty input') if len(texts) > 256: raise HTTPException(400, 'batch too large') out = model.encode(texts, batch_size=32, max_length=8192, return_dense=True)['dense_vecs'] data = [EmbeddingData(embedding=vec.tolist(), index=i) for i, vec in enumerate(out)] total_tokens = sum(len(t) for t in texts) // 4 # 粗估 return EmbeddingResponse( data=data, model='bge-m3', usage={'prompt_tokens': total_tokens, 'total_tokens': total_tokens}, ) @app.get('/health') def health(): return {'ok': model is not None} ``` ### 起服务 ```bash uv run uvicorn service:app --host 0.0.0.0 --port 8001 # 或生产: uv run gunicorn -k uvicorn.workers.UvicornWorker \ -w 1 -b 0.0.0.0:8001 service:app ``` 注意 `-w 1`:embedding 是 GPU 密集,多 worker 抢一张卡反而慢。 水平扩容用多机或者把模型放多张卡。 ### 客户端:OpenAI SDK 直接用 ```python from openai import OpenAI client = OpenAI( base_url='http://localhost:8001/v1', api_key='local', # 不校验,随便填 ) resp = client.embeddings.create( input=['你好世界', 'Hello world', '今日天气不错'], model='bge-m3', ) for i, d in enumerate(resp.data): print(f'{i}: dim={len(d.embedding)} first 5={d.embedding[:5]}') ``` ## 性能 我的 RTX 4090 上: - batch=32 文本(每条 ~200 字):~80ms(≈ 400 texts/s 持续) - batch=1:~20ms P95 延迟 < 100ms 在企业 RAG 场景完全够。OpenAI 3-small 平均 200-500ms (网络),自托管反而更快。 ## 效果 - 月度 embedding 账单 $300 → $0 - P95 延迟 320ms → 80ms(无公网往返) - 内部敏感文档不出公网 - bge-m3 中文 MTEB 评测比 text-embedding-3-small 高 5-8 个点 ## 踩过的坑 1. **第一次冷启动慢**:bge-m3 模型 1.2 GB 从 HuggingFace 拉。在国内 设 `HF_ENDPOINT=https://hf-mirror.com`。 2. **OpenAI SDK 严格校验维度**:bge-m3 输出 1024 维,OpenAI 3-small 是 1536。如果客户端代码硬编码 1536 校验,要么改客户端要么改 model 多输出 padding。 3. **`use_fp16=True` 在某些老 GPU 数值溢出**:T4 / 老 CUDA 上偶尔 NaN。 降到 `use_fp16=False` 慢 30% 但稳。 4. **batch 太大显存爆**:bge-m3 max_length=8192,batch=64 × 8192 token 可能 OOM。生产 `batch_size=32` + `max_length=512`(多数 chunk 这个 长度够用)。 5. **multi-process 共享 GPU 模型不共享内存**:每个 worker 各自 load 一份 1.2GB。`-w 1` 单 worker 是正确选择;要并发用 async + 内部 batch 合并请求。
## 起因 新机器 VSCode 装上后大家第一件事是装一堆插件:bracket pair colorizer / icon theme / 各种 snippet。其实 VSCode 内置功能已经很强,不知道反而错过。 下面是我每天用、新人常不知道的 12 个原生功能。 ## 1. 多光标编辑 ``` Alt+Click 加一个光标 Ctrl+D 选中下一个相同 word(继续按 Ctrl+D 选更多) Ctrl+Shift+L 选中所有相同 word Alt+Shift+I 多行末尾各加一个光标(先选多行) Ctrl+Alt+↓/↑ 垂直加光标(多行同列编辑) ``` 例:选中变量名 `old` → 几次 Ctrl+D → 同时改 5 处。 比 find/replace 快 10 倍(避免改到不该改的)。 ## 2. 命令面板(Command Palette) ``` Ctrl+Shift+P 命令面板 ``` VSCode 所有功能都在这里。打 "format" 找格式化命令;打 ">git" 找 git 操作;打 "Reload" 重启 window。 派生: ``` Ctrl+P 快速打开文件(fuzzy) Ctrl+Shift+O 当前文件 symbol 跳转 Ctrl+T workspace 全局 symbol 跳转 Ctrl+G 跳行号 Ctrl+@ 查看 outline ``` 记 `Ctrl+P / Ctrl+Shift+P / Ctrl+Shift+O` 三个就够日常。 ## 3. 集成 terminal ``` Ctrl+` 打开 / 切换 terminal Ctrl+Shift+` 新 terminal ``` terminal 支持 split / multiple shell / link 点击文件路径直接跳过去。 不用切窗口看输出。 ## 4. Source Control(git 集成) ``` Ctrl+Shift+G Source Control 视图 ``` - 看 diff(点 modified 文件) - 提交(输入 message + Ctrl+Enter) - stage hunk(左侧 ± 按钮) - branch 切换(左下角 status bar) - 推送 / 拉取(status bar 同步图标) 复杂的 rebase / 解 conflict 也有原生 UI。 GitLens 插件加强(line blame / file history 直观),但基础够用。 ## 5. 多文件 search & replace ``` Ctrl+Shift+F workspace 搜索 Ctrl+Shift+H workspace 替换 ``` - 支持 regex(小图标 .*) - preserve case(保留大小写:oldVar → newName,OldVar → NewName) - include / exclude 文件 glob - 全选预览后 replace all 替代很多自己写 sed 脚本的场景。 ## 6. snippets ``` Ctrl+Space 补全(如果 LSP 没自动弹) Tab 接受补全 ``` 自带 snippets:`for`、`if`、`function` 等输入触发模板。 自定义: ``` File → Preferences → Configure User Snippets → typescript.json ``` ```json { "Console Log": { "prefix": "clog", "body": ["console.log('${1:label}:', ${2:value})"], "description": "log with label" } } ``` 输入 `clog` + Tab → `console.log('label:', value)`,光标在 label。 ## 7. emmet(HTML / CSS / JSX) ``` div.container>ul>li*5>a[href="#"]{Item $}+Tab ``` → ```html <div class="container"> <ul> <li><a href="#">Item 1</a></li> <li><a href="#">Item 2</a></li> <li><a href="#">Item 3</a></li> <li><a href="#">Item 4</a></li> <li><a href="#">Item 5</a></li> </ul> </div> ``` CSS:`m10p` → `margin: 10px;`。HTML 模板秒生成。 ## 8. zen 模式 / 双 editor ``` Ctrl+K Z zen mode(隐藏所有 UI) Ctrl+\ split editor 左右 Ctrl+1 / Ctrl+2 切到第 1 / 2 个 editor Ctrl+W 关当前 editor ``` 复杂 review 时左 test 右 code 并排。 ## 9. 自定义 keymap `File → Preferences → Keyboard Shortcuts`: - 搜命令名(如"toggle terminal")→ 改 binding - 写 keybindings.json: ```json [ { "key": "ctrl+j", "command": "workbench.action.terminal.toggleTerminal" }, { "key": "ctrl+shift+j", "command": "editor.action.joinLines" } ] ``` Vim 用户装 vscode-neovim(不是 vim 插件)能用真 nvim 引擎,几乎 100% vim 体验。 ## 10. workspace 设置 `.vscode/settings.json` 进 git,团队成员共享: ```json { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit", "source.fixAll.eslint": "explicit" }, "editor.rulers": [80, 100], "files.exclude": { "**/__pycache__": true, "**/.pytest_cache": true, "**/node_modules": true }, "search.exclude": { "**/dist": true, "**/.venv": true }, "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } } ``` 新人 clone 后 IDE 自动应用这些设置:format on save、自动 organize imports、文件树过滤 __pycache__ 等。 ## 11. tasks(不离开 IDE 跑命令) `.vscode/tasks.json`: ```json { "version": "2.0.0", "tasks": [ { "label": "test", "type": "shell", "command": "pytest -xvs", "group": { "kind": "test", "isDefault": true } }, { "label": "dev", "type": "shell", "command": "npm run dev", "isBackground": true, "presentation": { "reveal": "always", "panel": "dedicated" } } ] } ``` `Ctrl+Shift+P → Tasks: Run Task → test` 跑。 test failures 自动跳到对应行 + 显示 error。 ## 12. launch.json (debugger) `.vscode/launch.json`: ```json { "version": "0.2.0", "configurations": [ { "name": "Python: Current File", "type": "debugpy", "request": "launch", "program": "${file}" }, { "name": "Django", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/manage.py", "args": ["runserver", "--noreload"], "django": true }, { "name": "FastAPI", "type": "debugpy", "request": "launch", "module": "uvicorn", "args": ["app.main:app", "--reload"] }, { "name": "Node: Attach", "type": "node", "request": "attach", "port": 9229 } ] } ``` F5 启动 debugger,breakpoint / watch / call stack 全 GUI。 对 Python / Node 项目调试效率秒杀 print。 ## 几个值得装的插件 虽然主题是"原生功能",但有几个插件确实显著提升: 1. **GitLens**:行 blame / file history / interactive rebase 2. **Error Lens**:errors / warnings inline 显示 3. **REST Client**:写 `.http` 文件直接调 API 4. **Code Spell Checker**:拼写检查(注释 / 字符串) 5. **Better Comments**:`// TODO:` `// FIXME:` 颜色区分 不是 30+ 个的"插件汤",5 个就够了。 ## 性能 tip ```json { "files.watcherExclude": { "**/node_modules/**": true, "**/dist/**": true }, "search.followSymlinks": false, "telemetry.telemetryLevel": "off" } ``` 大项目(monorepo / 10万+ 文件)watcher 吃 CPU,excluded 加快。 ## 远程开发:Remote-SSH 服务器代码不想 mount / sync 到本地? ``` Remote-SSH: Connect to Host → server.example.com ``` VSCode 在远程跑 server 端,本地 UI 显示,编辑 / debugger / terminal 都像在本地。开发服务器 / 大型 GPU 机器场景神器。 ## 效果 教会团队新人这 12 个: - 多光标改名场景 -80% find/replace 使用 - "我代码格式不对了" 类 PR 评论消失(save 自动 format) - 不再"打开第二个窗口跑 terminal"(内置 terminal 够用) - workspace settings 进 git 让团队体验一致 ## 踩过的坑 1. **settings.json 全局 vs workspace 冲突**:workspace > user > default。 优先级搞清楚。 2. **format on save 改了你不想改的**:prettier 把字符串单引号改成双 引号之类。`.editorconfig` + `.prettierrc` 配项目偏好。 3. **多个 formatter for same language**:`editor.defaultFormatter` 强 指定,否则每次问你用哪个。 4. **Ctrl+Shift+P 命令找不到**:扩展没装就没那个命令。装对应 extension 后 reload window。 5. **远程 SSH 在 ARM mac 连 x86 server**:第一次连慢(下 server 端 binary)。挂代理设 `remote.SSH.useExecServer: false` 减少 hop。