知识广场
按学科筛选:计算机科学
«计算机科学» 分类下共 256 篇帖子
Web 请求里同步发邮件 / 调外部 API / 跑重计算就是把响应时间往沟里推。 异步丢任务队列,Web 立刻返回,后台 worker 慢慢干。Celery + Redis 是 Python 生态最常用的组合。 ## 依赖 ```bash uv add 'celery[redis]' ``` `[redis]` extra 装上 `redis-py`,作为 broker + result backend。 ## 项目结构 ``` myapp/ ├── celery_app.py ├── tasks.py └── ... (Django/Flask/FastAPI) ``` ## celery_app.py ```python from celery import Celery app = Celery( 'myapp', broker='redis://localhost:6379/0', backend='redis://localhost:6379/1', include=['myapp.tasks'], ) app.conf.update( task_serializer='json', accept_content=['json'], result_serializer='json', timezone='Asia/Shanghai', enable_utc=True, task_acks_late=True, # worker 处理完才 ack;崩了任务会被重新派发 task_reject_on_worker_lost=True, worker_prefetch_multiplier=1, # 长任务必须设 1,否则一个 worker 卡住所有 prefetch task_time_limit=300, # 硬超时 5 分钟(SIGKILL) task_soft_time_limit=270, # 软超时(抛 SoftTimeLimitExceeded) ) ``` `prefetch_multiplier=1` 是长任务必须改的——默认 4,意思是每个 worker 预取 4 个任务,第一个任务慢的话另外 3 个被卡住等。 ## tasks.py ```python from celery import shared_task from celery.exceptions import SoftTimeLimitExceeded import smtplib @shared_task( bind=True, autoretry_for=(ConnectionError, smtplib.SMTPException), retry_backoff=True, # 1s, 2s, 4s, 8s ... retry_backoff_max=600, max_retries=5, ) def send_welcome_email(self, user_id, email): try: # ... 真的发邮件 ... with smtplib.SMTP('smtp.example.com', 587) as s: s.login(...) s.sendmail('from@', email, 'Welcome!') except SoftTimeLimitExceeded: # 清理资源 raise @shared_task def heavy_calc(rows): return sum(complex_fn(r) for r in rows) ``` `autoretry_for` + `retry_backoff` 是失败自动指数退避重试的现代写法。 比手写 `self.retry(countdown=...)` 简洁。 ## Web 端触发 ```python # Django / Flask / FastAPI 都一样 from myapp.tasks import send_welcome_email def signup_view(request): user = User.objects.create(...) send_welcome_email.delay(user.id, user.email) return Response({'ok': True}) # .delay() 是 .apply_async() 的简写 # 想要更细控制: send_welcome_email.apply_async( args=(user.id, user.email), countdown=60, # 60 秒后再执行 queue='email', # 路由到指定队列 expires=3600, # 1 小时后没执行就放弃 ) ``` ## 启动 worker ```bash uv run celery -A myapp.celery_app worker -l info -c 4 # -c 4: 并发数;CPU 密集任务建议 = 物理核心数;IO 密集可以更高 ``` 多队列分开: ```bash # 一个 worker 只跑邮件队列 uv run celery -A myapp.celery_app worker -l info -Q email -c 2 # 另一个 worker 跑默认 + 计算队列 uv run celery -A myapp.celery_app worker -l info -Q celery,heavy -c 8 ``` ## 周期任务(Celery Beat) ```python # celery_app.py app.conf.beat_schedule = { 'cleanup-every-hour': { 'task': 'myapp.tasks.cleanup_temp_files', 'schedule': 3600.0, # 秒;或 crontab(hour=3, minute=0) }, 'send-digest-monday-9am': { 'task': 'myapp.tasks.send_weekly_digest', 'schedule': crontab(hour=9, minute=0, day_of_week='mon'), }, } ``` 启动 beat(单独进程,单实例): ```bash uv run celery -A myapp.celery_app beat -l info ``` beat 是单实例的。多机部署只起一个 beat 进程,否则任务被触发多次。 用 `redbeat` 把 schedule 存 Redis 解决高可用。 ## 监控:Flower ```bash uv add flower uv run celery -A myapp.celery_app flower --port=5555 # 浏览器打开 http://localhost:5555 ``` 可以看到所有 worker、队列长度、任务历史、失败重试。 ## 生产部署清单 1. broker / backend 用 **不同 Redis DB**(broker 流量大、result 流量小) 2. 长时间不取的 result 自动过期:`result_expires=3600` 3. worker 用 systemd unit 管,开 `Restart=on-failure` 4. 监控队列长度(Prometheus celery exporter):超过阈值就扩 worker 5. 别在 task 里用 Django ORM 然后忘记 `connection.close()`: 长跑 worker 会泄连接 ## systemd 单元 ```ini # /etc/systemd/system/celery-worker.service [Unit] Description=Celery worker After=network.target redis.service [Service] Type=simple User=app Group=app WorkingDirectory=/srv/myapp EnvironmentFile=/srv/myapp/.env ExecStart=/srv/myapp/.venv/bin/celery -A myapp.celery_app worker -l info -c 4 Restart=on-failure RestartSec=5s KillMode=mixed [Install] WantedBy=multi-user.target ``` ## 踩过的坑 - task 改了签名(加 / 删参数)后还有老格式的任务在队列里,新 worker 处理时 会爆 TypeError。新增字段加默认值,删字段做兼容 wrapper。 - `task_serializer='pickle'` —— 不要!pickle 允许执行任意代码,broker 一被攻破 全完。一律 JSON。 - worker 跑 Django ORM 任务时,每个任务后忘记 `transaction.commit()`, 数据库连接长期持有,下一个任务可能看到老数据。Django 4+ 默认 ATOMIC_REQUESTS + 显式 `connection.close()` 能解决。 - Result backend 用 Redis 时,每个 task 结果默认占一个 key 直到 expire。 几十万任务的 backlog 能把 Redis 撑爆。要么 `task_ignore_result=True`, 要么 `result_expires` 设短。
## 起因 团队主要写 React,但接手一个 Vue 2 老项目。Vue 2 的 Options API(data/methods/ computed 分块)跟 React hooks 风格完全不一样,写起来不顺手。 升级到 Vue 3 + Composition API 后语义跟 React 接近多了,迁移 成本下降。下面记录我的"React 视角看 Vue 3"。 ## script setup 语法 Vue 3.2+ 的 `<script setup>` 是 Composition API 的语法糖: ```vue <script setup lang="ts"> import { ref, computed, watch, onMounted } from 'vue' const count = ref(0) const doubled = computed(() => count.value * 2) function increment() { count.value++ } watch(count, (newVal, oldVal) => { console.log(`count ${oldVal} → ${newVal}`) }) onMounted(() => { console.log('mounted') }) </script> <template> <button @click="increment">{{ count }} (doubled: {{ doubled }})</button> </template> ``` 对比 React: ```tsx const [count, setCount] = useState(0) const doubled = useMemo(() => count * 2, [count]) useEffect(() => { console.log(`count: ${count}`) }, [count]) useEffect(() => { console.log('mounted') }, []) return <button onClick={() => setCount(c => c + 1)}>{count}</button> ``` Vue 的 `ref(0)` ≈ React 的 `useState`。访问值要 `.value`(template 里 自动 unwrap)。`computed` ≈ `useMemo`。`watch` ≈ `useEffect` with deps。 ## ref vs reactive ```ts import { ref, reactive } from 'vue' const counter = ref(0) console.log(counter.value) counter.value = 10 const state = reactive({ count: 0, name: '' }) console.log(state.count) state.count = 10 ``` `ref` 适合基础类型;`reactive` 适合对象。 **重要**:reactive 不能解构(会失去响应性): ```ts const state = reactive({ count: 0 }) const { count } = state // ❌ count 不再响应 ``` 要解构必须 `toRefs`: ```ts import { toRefs } from 'vue' const { count } = toRefs(state) // ✅ ``` 为了避免这个坑,我直接全用 ref,复杂状态用 `ref({ count: 0, name: '' })`。 ## 组合函数(custom hook 等价物) 抽象逻辑成 `useXxx`: ```ts // composables/useCounter.ts import { ref, computed } from 'vue' export function useCounter(initial = 0) { const count = ref(initial) const doubled = computed(() => count.value * 2) const inc = () => count.value++ const dec = () => count.value-- return { count, doubled, inc, dec } } ``` 用: ```vue <script setup> import { useCounter } from '@/composables/useCounter' const { count, doubled, inc } = useCounter(10) </script> ``` 完全等价于 React custom hook。 ## defineProps / defineEmits(类型化 props / events) ```vue <script setup lang="ts"> const props = defineProps<{ user: { id: string; name: string } count?: number }>() const emit = defineEmits<{ (e: 'increment', value: number): void (e: 'reset'): void }>() function bump() { emit('increment', (props.count ?? 0) + 1) } </script> <template> <button @click="bump">{{ user.name }}: {{ count }}</button> </template> ``` 父组件: ```vue <MyButton :user="me" :count="5" @increment="handleInc" @reset="handleReset" /> ``` 类型完全跟着传。 ## v-model 双向绑定 React 强迫你手写 `value={x} onChange={...}`。Vue 的 v-model 是糖: ```vue <!-- 父 --> <MyInput v-model="email" /> <!-- 子 --> <script setup> const props = defineProps<{ modelValue: string }>() const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>() </script> <template> <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)"> </template> ``` 简化版用 `defineModel`(Vue 3.4+): ```vue <script setup> const email = defineModel<string>() </script> <template> <input v-model="email"> </template> ``` 3 行干完。React 里至少 8-10 行。 ## Pinia:Vue 的状态管理 ```bash npm i pinia ``` ```ts // stores/auth.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const isLoggedIn = computed(() => user.value !== null) async function login(email: string, pw: string) { const r = await fetch('/api/login', { method: 'POST', body: ... }) user.value = await r.json() } function logout() { user.value = null } return { user, isLoggedIn, login, logout } }) ``` ```vue <script setup> import { useAuthStore } from '@/stores/auth' const auth = useAuthStore() function onSubmit() { auth.login(email.value, password.value) } </script> ``` 跟 Zustand 类似(但写法是 composition function 而非 set/get)。 ## 路由 ```bash npm i vue-router@4 ``` ```ts // router.ts import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('./views/Home.vue') }, { path: '/posts/:id', component: () => import('./views/Post.vue') }, ], }) export default router ``` ```vue <script setup> import { useRoute, useRouter } from 'vue-router' const route = useRoute() // /posts/42 → route.params.id = '42' const router = useRouter() function goHome() { router.push('/') } </script> ``` 完全镜像 React Router 的 useParams / useNavigate。 ## SSR / SSG: Nuxt 类似 React 的 Next.js,Vue 的元框架是 Nuxt: ```bash npx nuxi@latest init my-app ``` 文件系统路由、自动 import、SSR、API routes 等都是 Nuxt 的事,开箱即用。 ## Vue 比 React 强的几点 1. **template + script + style 三段式**:scoped CSS 内置, `<style scoped>` 自动加 hash,组件样式不互相污染 2. **响应式不需要写依赖数组**:`watch` 自动追踪。React `useEffect` 写错 deps 是常见 bug。 3. **v-for, v-if, v-show**:模板里写就够,不像 JSX 强 `.map()` / `&&` 4. **defineModel 简化双向绑定** ## React 比 Vue 强的几点 1. **更大生态**:Next.js、Remix、库选择更多 2. **TypeScript 更原生**:Vue 也支持但偶尔有些边角不丝滑 3. **更广泛的工作机会** ## 效果 我个人混着写下来: - Vue 项目代码量比同等 React 少 20-30% - 模板比 JSX 更适合"以视图为中心"的页面(marketing / 表单密集) - 复杂状态 / 复杂交互 React 生态成熟度高 - 个人选择:marketing / 后台管理 → Vue + Nuxt; 复杂 web app → React ## 踩过的坑 1. **reactive 解构丢响应性**:上面说过。新人最容易踩。 2. **template 里 ref 自动 unwrap**: ```vue <script setup> const count = ref(0) </script> <template>{{ count }}</template> <!-- 自动 .value --> ``` 习惯之后没事,刚转过来时反复 `{{ count.value }}` 多写 `.value`。 3. **`v-html` 直接 innerHTML**:XSS 隐患。任何用户输入永远不用 v-html。 4. **Vue 3 跟 Vue 2 写法不通用**:老 Vue 2 项目升 3 是 breaking change, 不是 React 16 → 17 那种平滑升级。 5. **Pinia store 在 setup() 外用要 createPinia 先注入**: `main.ts` 必须 `app.use(createPinia())`,否则报 "no active pinia"。
## 起因 老项目 JS(或松散 TS)想拧紧 strict mode: - `noImplicitAny` - `strictNullChecks` - `strictFunctionTypes` - `noImplicitThis` - `strictPropertyInitialization` - `alwaysStrict` 一次性全开 → 几千 type error → 团队崩。 渐进 rollout 是正解。 ## strict 全开 ```json { "compilerOptions": { "strict": true, // 等价于全开上面 6 个 } } ``` 新项目直接 strict。 ## 老项目分步骤 ```json { "compilerOptions": { "strict": false, "noImplicitAny": true, "strictNullChecks": false, ... } } ``` 一个一个加,每个加完跑全 ts check + 修。 推荐顺序: 1. `noImplicitAny`(多数 type 自动推或 explicit) 2. `strictNullChecks`(最大改动,但最值) 3. `strictFunctionTypes` 4. 其它 ## strictNullChecks 影响 ```ts // 关 strictNullChecks function getName(user) { // user: any return user.name; // 没事 } getName(null); // 没报,但运行时崩 // 开 function getName(user: User | null) { return user.name; // ❌ Object is possibly 'null' } function getNameSafe(user: User | null) { return user?.name; // ✅ } ``` 每函数 explicit handle null。 最大价值:编译期 catch null pointer 问题。 ## 渐进:per-file strict `@ts-strict` 风格 comment / 工具: ```ts // @ts-check // @ts-strict ``` 或者用 `ts-strict-plugin`、单 file `// @ts-expect-error` 标技术债。 ## TypeScript per-file 更激进:拆 tsconfig 多个 project: ```json // tsconfig.strict.json { "extends": "./tsconfig.json", "compilerOptions": { "strict": true }, "include": ["src/strict/**"] } ``` `src/strict/` 下严格,其余宽。 新代码进 strict,老代码慢慢迁。 ## 工具帮迁移 - `ts-migrate`(Airbnb 出):批量 JS → TS + 加 `// @ts-expect-error` - `typescript-strict-plugin`:渐进 strict per-file ## 渐进示例 ``` Week 1: 装 TS, allowJs: true, 跑 baseline Week 2: noImplicitAny on, 修 (100 error) Week 3: 把 critical paths 改 strict (per-file) Week 4: strictNullChecks on, 修 (500 error,多数小) Month 2: 全 strict ``` ## 常见迁移 pattern ```ts // 之前 function process(data) { if (data.user.address.city) { ... } } // 改 1: 类型 interface User { address?: Address } interface Address { city?: string } function process(data: { user: User }) { if (data.user.address?.city) { ... } // optional chaining } // 改 2: 早返 function process(data: { user: User }) { const city = data.user.address?.city; if (!city) return; // city 类型 narrowed string } ``` optional chaining + early return 是最常用 strict-friendly pattern。 ## 第三方 lib 没 type ```ts // 装 @types npm install -D @types/lodash // 没 @types 的 declare module 'weird-lib' { export function doStuff(x: any): any; } ``` 最差情况 cast: ```ts const lib = require('weird-lib') as any; ``` 但 `as any` 会传染 → 限定到边界。 ## any vs unknown ```ts function parse(input: any) { // 危险 return input.value; // 不报,但运行时崩 } function parse(input: unknown) { // 强制处理 if (typeof input === 'object' && input && 'value' in input) { return input.value; // narrowed } } ``` `unknown` 是 type-safe 版 `any`。强制 narrow 后用。 新代码用 unknown 替代 any。 ## strictNullChecks 后的常见错 ```ts // 1. array access const arr: string[] = []; const first: string = arr[0]; // ❌ undefined if empty // noUncheckedIndexedAccess: true const first: string | undefined = arr[0]; // 2. delete const obj: { x?: number } = { x: 1 }; delete obj.x; // 必须先 optional // 3. JSON.parse const data: unknown = JSON.parse(s); // 不能直接 data.x → Zod / type guard ``` ## CI gate ```json // package.json "scripts": { "typecheck": "tsc --noEmit", "typecheck:strict": "tsc --project tsconfig.strict.json --noEmit" } ``` ```yaml # CI - run: pnpm typecheck - run: pnpm typecheck:strict # 只检查 strict files ``` PR 改动 strict file → 必须过 strict check。 慢慢"扩 strict 边界"。 ## 真实迁移 某老项目 (50k LOC JS): - 全开 strict 一次:3000 error,回归测试一周 - 改渐进:4 个月 - 月 1:装 TS + allowJs,0 改 → 跑通 - 月 2:noImplicitAny + 主要路径手加 type → 50% 文件 typed - 月 3:strictNullChecks,整修 → 1500 changes - 月 4:全 strict + tslint → strict - 期间也持续开发新 feature 迁完收益: - 生产 NullPointer error 降 80% - IDE auto-complete 准 - 重构信心大增 - 新人 onboarding 快(看类型就懂 API) ## skipLibCheck ```json { "skipLibCheck": true // 第三方 .d.ts 不 check } ``` 大型项目几乎必开。第三方 type 偶有小问题不卡你。 ## 不要追求 100% `any` 不是恶魔。 边界(unknown API / migration code / quick prototype)用 `any` 标 `// FIXME: type later`。 内部核心 type-tight 90% 已经收益大头。 ## 与 jsdoc TS 对比 ```js // JS file + JSDoc /** * @param {string} name * @returns {string} */ function greet(name) { return `Hi ${name}`; } ``` `// @ts-check` 让 TS check JSDoc 类型。 适合:不想转 .ts 但想要 type-check(如 lib / 小项目)。 中大型 → 直接 .ts。 ## 踩过的坑 1. **`Object.keys` 返回 string[]**:`Object.keys({a:1, b:2}).forEach((k) => obj[k])` 报 string can't index Foo。`(Object.keys(obj) as (keyof Foo)[])` cast。 2. **DOM null**:`document.querySelector('.x')` 返 Element | null。 `!` non-null assert 慎用。 3. **callback this**:strict 模式下 method as callback 失 `this`。 bind / arrow。 4. **enum 类型**:`enum X { A, B }` 编译有运行时 object。 `const X = { A: 'a', B: 'b' } as const; type X = (typeof X)[keyof typeof X]` 更轻。 5. **复杂 generic**:递归 / mapped type 编译慢。简化或拆。
## 起因 K8s 全套 (kubeadm) 在小机器(2-4 vCPU / 4 GB)跑得费力: - etcd + apiserver + controller-manager + scheduler 几 GB RAM - 启动 5 分钟 - 升级痛苦 但有时只想: - 边缘设备(树莓派 / IoT gateway)跑 K8s 编排 - 笔记本本地 dev - 单租户产品打包"K8s on appliance" - 小公司 1-2 server prod **k3s**(Rancher,2019+):单 binary(~50 MB),裁剪 K8s 到 ~512 MB RAM。 **microk8s**(Canonical):类似目标,更 batteries-included。 ## k3s 装 ```bash # 一键装(server) curl -sfL https://get.k3s.io | sh - # kubectl 立刻可用 sudo k3s kubectl get nodes # 或者复制 kubeconfig 用普通 kubectl sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $USER ~/.kube/config ``` 完事。1 分钟起一个 K8s。 ## 加 worker node ```bash # server 拿 token sudo cat /var/lib/rancher/k3s/server/node-token # worker 上 curl -sfL https://get.k3s.io | K3S_URL=https://server:6443 K3S_TOKEN=xxx sh - ``` ## k3s 是什么 K8s - **完整 K8s API**(不是 mini K8s) - 替代 etcd 用 SQLite(也支持 etcd / MySQL / PG external) - 替代 docker 用 containerd - 内置 traefik ingress(可关) - 内置 helm controller / local storage provisioner / metrics-server - 删了 in-tree cloud provider / 实验性 feature 90% workload 跟 vanilla K8s 一样跑。 ## 资源占用 | | k3s | kubeadm K8s | docker swarm | |---|---|---|---| | RAM | 512 MB | 2 GB+ | 50 MB | | 启动 | 30s | 5 min | 5s | | Binary | 50 MB | 几百 MB | 50 MB | 树莓派 4 (4 GB RAM) 跑 k3s 余 3+ GB 给应用 → 完全实用。 ## HA k3s ```bash # server 1 curl -sfL https://get.k3s.io | sh -s - server --cluster-init # server 2 / 3 curl -sfL https://get.k3s.io | sh -s - server --server https://server1:6443 --token xxx ``` 3 个 server 自动 embedded etcd HA。 worker 同前面加。 ## microk8s Ubuntu 系强推: ```bash sudo snap install microk8s --classic sudo usermod -aG microk8s $USER microk8s status microk8s kubectl get nodes # 启用 addon microk8s enable dns ingress storage cert-manager ``` addon 系统比 k3s 友好: ``` dns / ingress / cert-manager / metallb / observability / istio / linkerd / hostpath-storage / openebs / cilium / dashboard ``` 一行启用,省手动 helm。 ## k3s vs microk8s | | k3s | microk8s | |---|---|---| | 母公司 | Rancher / SUSE | Canonical | | 分发 | shell script | snap | | 多 distro | 是 | snap 限制(Ubuntu / 部分) | | HA | embedded etcd | 自动 (3+ nodes) | | addon | 内置少,手动 helm | addon 丰富 | | 升级 | 手动 | snap 自动 | | 默认 ingress | traefik | nginx | | 体量 | 50 MB | 200 MB+ | 我倾向 k3s(更轻 + 跨 distro),但 ubuntu 系统 microk8s 顺手。 ## 适合场景 - **edge / IoT**:低资源 + 远程管理 - **CI / 测试**:临时拉一个 k3s 测 manifest - **个人 / 小 团队 prod**:1-3 server 足够 - **embedded product**:appliance 内置 K8s 不适合: - 千节点集群(k3s SQLite 不行;要 PostgreSQL 后端) - 重 K8s feature(webhooks 多 / CRD 多) 仍 OK 但优势减 - 合规要求企业级 K8s 发行版 ## k3d (k3s in docker) ```bash brew install k3d k3d cluster create mycluster --servers 3 --agents 2 kubectl get nodes ``` 3 master + 2 worker 在 5 个 docker container 起来 → laptop 测多 节点行为。 比 kind 轻量。 ## 真实部署 case 某 IoT 客户: - 100 个边缘 gateway(4 vCPU / 8 GB) - 每 gateway 跑 k3s + 几个微服务 + 本地 DB - 中心 cluster 用 Rancher 管 100 个 k3s 集群 - 应用更新通过 Fleet / ArgoCD 推 效果: - 单 gateway 装 K8s 5 分钟(自动化 script) - 中心可视化所有 edge 状态 - 应用迭代独立各 gateway vs 老方案(docker compose + ansible):管理统一 + K8s API 标准化 + 故障恢复更智能。 ## 本地 dev: k3s vs kind vs minikube ```bash # kind (K8s in docker) kind create cluster # k3d (k3s in docker) k3d cluster create # minikube (VM) minikube start ``` | | 启动 | 资源 | 多节点 | |---|---|---|---| | kind | 30s | 中 | ✅ | | k3d | 15s | 低 | ✅ | | minikube | 1 min | 高(VM) | 慢 | 我本地 dev 用 k3d。 ## 升级 k3s ```bash # 同样 curl 命令 + INSTALL_K3S_VERSION curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.30.0+k3s1 sh - ``` 或者用 Rancher / system-upgrade-controller 自动滚动升级 多 node。 ## monitoring ```bash helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \ -n monitoring --create-namespace ``` 跟 vanilla K8s 一样 helm install。 metrics-server 已内置 (k3s)。 ## 与 Talos Linux 对比 Talos Linux:Linux distro for K8s。OS 本身是 K8s。 极简 + 安全 + immutable。 | | k3s | Talos | |---|---|---| | Host OS | 任意 | Talos only | | 管理 | systemd | API only(无 ssh) | | 升级 | 手动 / 工具 | 强 immutable | | 学习 | 易 | 中 | Talos 是新潮选择,适合"K8s as appliance"。 ## 踩过的坑 1. **SQLite 单 server 限制**:k3s 默认 SQLite 不支持多 server HA。 3+ server 要 embedded etcd(默认 `--cluster-init`)。 2. **storage 默认 hostpath**:pod restart 跨 node 数据丢。生产用 longhorn / NFS / cloud volume。 3. **traefik 内置版本旧**:k3s 自带 traefik 跟最新版本可能差几个版本。 `--disable traefik` + 自装最新。 4. **resource 限制**:CPU 紧张时 etcd / apiserver 慢 → pod schedule 失败。监控 system pod CPU。 5. **firewall blocking 6443**:worker 加入失败常见原因。`ufw allow 6443` / 防火墙规则。
## 起因 模型训完了 `.pt` 文件躺在硬盘上。要让前端 / 移动端 / 别的服务能用它, 需要包成 REST API。手写 Flask / FastAPI 包一遍是 100 行 boilerplate (加载模型 + parse 输入 + tensor 转 numpy + 错误处理 + batching)。 做几个模型这种工作就极乏味。 BentoML 把 ML 模型 → 生产 service 的过程标准化:写一个 service 文件, 自动生成 HTTP / gRPC / OpenAPI / Docker。 ## 解决方案 ### 装 ```bash uv add bentoml torch torchvision pillow ``` ### 模型仓库(model store) ```python # save_model.py import bentoml import torch from torchvision.models import resnet50, ResNet50_Weights model = resnet50(weights=ResNet50_Weights.DEFAULT).eval() bento_model = bentoml.pytorch.save_model('resnet50', model) print(bento_model) # Model(tag="resnet50:abc123...") ``` 模型存到本地 `~/bentoml/models/`,分 tag 版本化。 团队共享用 `bentoml push / pull` 配 BentoCloud 或自建 S3。 ### service.py(核心) ```python import bentoml from bentoml.io import Image, JSON from PIL import Image as PILImage import torch from torchvision import transforms resnet = bentoml.pytorch.get('resnet50:latest').to_runner() svc = bentoml.Service('image_classifier', runners=[resnet]) preprocess = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) with open('imagenet_classes.txt') as f: LABELS = [line.strip() for line in f] @svc.api(input=Image(), output=JSON()) async def predict(img: PILImage.Image) -> dict: x = preprocess(img).unsqueeze(0) logits = await resnet.async_run(x) probs = torch.softmax(logits, dim=1)[0] top5 = torch.topk(probs, k=5) return [ {'label': LABELS[idx], 'prob': prob.item()} for idx, prob in zip(top5.indices.tolist(), top5.values.tolist()) ] ``` 要点: - `to_runner()`:让 BentoML 管理 model 生命周期 + 并发 batch - `@svc.api(input=Image(), output=JSON())`:声明输入输出,自动 生成 OpenAPI doc + 验证 - `async run`:BentoML 在 worker 进程里跑 model,业务进程 async 协程 ### 本地启动 ```bash bentoml serve service:svc --reload # uvicorn 起来 http://localhost:3000 # http://localhost:3000/docs 自动 Swagger ``` 调用: ```bash curl -X POST http://localhost:3000/predict \ -H 'Content-Type: image/jpeg' \ --data-binary @cat.jpg # [{"label": "Egyptian cat", "prob": 0.84}, ...] ``` ### Adaptive batching(自动批处理) `runner` 默认开启 batching:单条请求进来时 hold ~10ms 等更多请求, 合并 batch 一次 forward,吞吐量直接翻几倍。 ```python runner = bentoml.pytorch.get('resnet50:latest').to_runner( method_configs={'__call__': { 'max_batch_size': 32, 'max_latency_ms': 100, }}, ) ``` 业务代码无感知。 ### 打包成 Bento + Docker ```bash # bentofile.yaml service: 'service:svc' include: - 'service.py' - 'imagenet_classes.txt' python: packages: - torch - torchvision - pillow models: - resnet50:latest ``` ```bash bentoml build # 生成 ~/bentoml/bentos/image_classifier/<tag> bentoml containerize image_classifier:latest # 生成 docker image image_classifier:latest docker run -p 3000:3000 image_classifier:latest ``` 镜像里包含:Python + 依赖 + service code + 模型权重。直接部署。 ### K8s 部署(BentoML Yatai) ```bash bentoml deployment create my-deploy \ --bento image_classifier:latest \ --cluster prod ``` 或者用 Yatai operator,K8s 原生 CRD 管理 Bento。 ## 效果 - 训完 model → 上生产 API 从 2 天 → 2 小时 - 自动 batch 让单 GPU 吞吐量翻 4 倍 - OpenAPI 文档自动生成,前端不再追着问 schema - 多版本管理 / canary deploy 都是 framework 原生支持 - 监控 metrics 自动暴露 /metrics endpoint 给 Prometheus ## 与替代品对比 | | BentoML | TorchServe | Triton | 自己写 FastAPI | |---|---|---|---|---| | 学习曲线 | 中 | 中 | 高 | 低 | | 多框架 | ✅ | 主 PyTorch | ✅ | N/A | | 自动 batching | ✅ | ✅ | ✅ | 需自写 | | Docker / K8s | ✅ | ✅ | ✅ | 需自写 | | 模型仓库 | ✅ | ✅ | ✅ | 需自建 | | 简单 API | 中 | 中 | 复杂 | 极简 | 复杂 ML 系统选 BentoML / Triton;单模型 < 100 QPS 自己写 FastAPI 更轻量。 ## 踩过的坑 1. **runner 进程模型加载慢**:cold start 几秒-几十秒。生产用 `bentoml serve --production --workers 4 --runners 2 ...` 提前 warmup。 2. **adaptive batching 引入延迟**:单条请求 P99 可能 > 100ms(等其它 请求凑 batch)。低 QPS 场景关 batching:`max_batch_size=1`。 3. **input/output schema 不严格**:默认 JSON 接受任意结构。生产用 pydantic `JSON(pydantic_model=MyInput)` 强校验。 4. **模型权重打包到镜像里**:镜像几 GB 推送慢。模型放 OSS / 启动时 下载更轻量;trade-off 是冷启动慢。 5. **GPU 不释放**:bentoml runner 进程退出时 GPU 偶尔被 PyTorch leaked。systemd 重启 service 时确保 `KillMode=control-group` 杀 所有子进程。
Neovim 0.10+ 已经是一流的 IDE 平台:内置 LSP 客户端、Treesitter 语法、 Lua 配置。Lazy.nvim 是事实标准的插件管理器,懒加载 + 锁版本。 下面给一个 < 100 行 Lua、能开箱即用的最小配置。 ## 1. 装 Neovim 0.10+ ```bash # Ubuntu 24.04+ apt 自带;老版本用 ppa / appimage / brew sudo apt install -y neovim nvim --version # 确认 >= 0.10 ``` ## 2. 目录结构 ``` ~/.config/nvim/ ├── init.lua └── lua/ ├── options.lua ├── keymaps.lua ├── plugins.lua └── lsp.lua ``` ## 3. `init.lua` ```lua -- 一键 bootstrap lazy.nvim local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' if not vim.uv.fs_stat(lazypath) then vim.fn.system({ 'git', 'clone', '--filter=blob:none', 'https://github.com/folke/lazy.nvim.git', '--branch=stable', lazypath, }) end vim.opt.rtp:prepend(lazypath) require('options') require('keymaps') require('lazy').setup(require('plugins')) require('lsp') ``` ## 4. `lua/options.lua` ```lua local o = vim.opt o.number = true o.relativenumber = true o.signcolumn = 'yes' -- 给 LSP / git 留位置 o.expandtab = true o.shiftwidth = 2 o.tabstop = 2 o.smartindent = true o.wrap = false o.ignorecase = true o.smartcase = true o.termguicolors = true o.cursorline = true o.scrolloff = 8 o.updatetime = 250 -- 让 hover / diag 更快 o.clipboard = 'unnamedplus' -- 跟系统剪贴板互通 o.splitright = true o.splitbelow = true o.undofile = true -- 跨 session undo o.mouse = 'a' ``` ## 5. `lua/keymaps.lua` ```lua vim.g.mapleader = ' ' vim.g.maplocalleader = ' ' local map = vim.keymap.set map('n', '<leader>w', '<cmd>w<cr>', { desc = 'Save' }) map('n', '<leader>q', '<cmd>q<cr>', { desc = 'Quit' }) map('n', '<esc>', '<cmd>nohlsearch<cr>') map('n', '<C-h>', '<C-w>h') map('n', '<C-j>', '<C-w>j') map('n', '<C-k>', '<C-w>k') map('n', '<C-l>', '<C-w>l') map('v', '<', '<gv') -- 缩进后保持选中 map('v', '>', '>gv') ``` ## 6. `lua/plugins.lua` ```lua return { -- 配色 { 'catppuccin/nvim', name = 'catppuccin', config = function() vim.cmd.colorscheme('catppuccin-mocha') end }, -- 文件树 { 'nvim-tree/nvim-tree.lua', dependencies = { 'nvim-tree/nvim-web-devicons' }, keys = { { '<leader>e', '<cmd>NvimTreeToggle<cr>', desc = 'File tree' } }, config = function() require('nvim-tree').setup() end, }, -- 模糊搜索 { 'nvim-telescope/telescope.nvim', dependencies = { 'nvim-lua/plenary.nvim' }, keys = { { '<leader>f', '<cmd>Telescope find_files<cr>', desc = 'Find files' }, { '<leader>g', '<cmd>Telescope live_grep<cr>', desc = 'Live grep' }, { '<leader>b', '<cmd>Telescope buffers<cr>', desc = 'Buffers' }, }, }, -- 语法高亮(Treesitter) { 'nvim-treesitter/nvim-treesitter', build = ':TSUpdate', config = function() require('nvim-treesitter.configs').setup({ ensure_installed = { 'lua', 'python', 'javascript', 'typescript', 'tsx', 'go', 'rust', 'bash', 'markdown', 'json', 'yaml', 'html', 'css' }, highlight = { enable = true }, indent = { enable = true }, }) end, }, -- Git 集成 { 'lewis6991/gitsigns.nvim', config = function() require('gitsigns').setup() end }, -- LSP 安装管理 { 'williamboman/mason.nvim', config = true }, { 'williamboman/mason-lspconfig.nvim', dependencies = { 'mason.nvim', 'neovim/nvim-lspconfig' }, config = function() require('mason-lspconfig').setup({ ensure_installed = { 'pyright', 'ts_ls', 'lua_ls', 'gopls', 'rust_analyzer' }, }) end, }, -- 补全 { 'hrsh7th/nvim-cmp', dependencies = { 'hrsh7th/cmp-nvim-lsp', 'hrsh7th/cmp-buffer', 'hrsh7th/cmp-path', 'L3MON4D3/LuaSnip', 'saadparwaiz1/cmp_luasnip', }, config = function() local cmp = require('cmp') cmp.setup({ snippet = { expand = function(args) require('luasnip').lsp_expand(args.body) end }, mapping = cmp.mapping.preset.insert({ ['<C-Space>'] = cmp.mapping.complete(), ['<CR>'] = cmp.mapping.confirm({ select = true }), ['<Tab>'] = cmp.mapping.select_next_item(), ['<S-Tab>'] = cmp.mapping.select_prev_item(), }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'luasnip' }, { name = 'buffer' }, { name = 'path' }, }), }) end, }, -- 状态栏 { 'nvim-lualine/lualine.nvim', config = function() require('lualine').setup() end }, } ``` ## 7. `lua/lsp.lua` ```lua local lspconfig = require('lspconfig') local caps = require('cmp_nvim_lsp').default_capabilities() local on_attach = function(_, buf) local map = function(keys, fn, desc) vim.keymap.set('n', keys, fn, { buffer = buf, desc = desc }) end map('gd', vim.lsp.buf.definition, 'Go to definition') map('gr', vim.lsp.buf.references, 'References') map('K', vim.lsp.buf.hover, 'Hover') map('<leader>rn', vim.lsp.buf.rename, 'Rename') map('<leader>ca', vim.lsp.buf.code_action, 'Code action') map('<leader>d', vim.diagnostic.open_float, 'Show diagnostic') end for _, server in ipairs({ 'pyright', 'ts_ls', 'gopls', 'rust_analyzer', 'lua_ls' }) do lspconfig[server].setup({ on_attach = on_attach, capabilities = caps, }) end -- 保存时自动格式化(如果 LSP 支持) vim.api.nvim_create_autocmd('BufWritePre', { callback = function() vim.lsp.buf.format({ async = false }) end, }) ``` ## 8. 第一次启动 ```bash nvim # Lazy 自动 clone 插件,等几秒 # :Mason ← 进去能看到 LSP 安装状态 ``` ## 9. 升级 / 锁版本 ```vim :Lazy update " 升级所有 :Lazy sync " 装新加的、删旧的 :Lazy log " 看更新历史 ``` Lazy 会在 `~/.config/nvim/lazy-lock.json` 记录每个插件的 commit hash。 进 git 让别的机器同步到完全一样的版本。 ## 10. 排错 ```vim :checkhealth " 列出每个组件的健康状态 " 缺什么(python3 / npm / node / fd / ripgrep)一目了然 :LspInfo " 看当前 buffer 的 LSP 状态 ``` ## 踩过的坑 - 不装 `ripgrep` → Telescope live_grep 不能用。`sudo apt install ripgrep`。 - LSP server 装了但没启动:通常因为 root marker 找不到(pyright 找 `pyproject.toml` / `setup.py`)。在项目根目录打开 Neovim。 - 自动 format on save 把你刚写的中文注释格式化乱:把不可信的 formatter 关掉,或者 `:noa w` 跳过 autocmd。 - 升级 Treesitter parser 时 build 失败:缺 gcc / make。装编译工具链 `sudo apt install build-essential`。
测试工具几年一换,2024 后的主流: - **单元测试 / 组件测试**:Vitest(替代 Jest,与 Vite 一体) - **E2E 测试**:Playwright(替代 Cypress,多浏览器 + 并发) - **可视化组件库 / VRT**:Storybook + Chromatic(可选) ## Vitest 安装 ```bash npm i -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event ``` `vitest.config.ts`: ```ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'html'], }, }, }) ``` `src/test/setup.ts`: ```ts import '@testing-library/jest-dom/vitest' ``` `package.json`: ```json { "scripts": { "test": "vitest", "test:run": "vitest run", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage" } } ``` ## 第一个组件测试 ```tsx // Counter.tsx import { useState } from 'react' export function Counter() { const [n, setN] = useState(0) return <button onClick={() => setN(n + 1)}>count: {n}</button> } // Counter.test.tsx import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from './Counter' test('increments on click', async () => { render(<Counter />) expect(screen.getByRole('button')).toHaveTextContent('count: 0') await userEvent.click(screen.getByRole('button')) expect(screen.getByRole('button')).toHaveTextContent('count: 1') }) ``` 注意: - `screen.getByRole` 优先于 `getByTestId`(更符合用户视角) - `userEvent` 比 `fireEvent` 更真实(模拟键盘 / 焦点) ## query 优先级 按 testing-library 官方推荐: 1. `getByRole` (button, link, heading) 2. `getByLabelText` (form fields) 3. `getByText` (text content) 4. `getByDisplayValue` (input current value) 5. `getByAltText` / `getByTitle` 6. `getByTestId` (最后选项) ## mock 模块 / 函数 ```ts import { vi } from 'vitest' vi.mock('./api', () => ({ fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }), })) // 或单个函数 const spy = vi.spyOn(console, 'log').mockImplementation(() => {}) ``` ## 测异步 ```tsx import { render, screen, waitFor } from '@testing-library/react' test('loads user', async () => { render(<UserCard id="1" />) expect(screen.getByText(/loading/i)).toBeInTheDocument() await waitFor(() => { expect(screen.getByText('Alice')).toBeInTheDocument() }) }) // 或者用 findBy (自动 await) const el = await screen.findByText('Alice') ``` ## MSW:mock 网络请求 ```bash npm i -D msw ``` ```ts // src/mocks/handlers.ts import { http, HttpResponse } from 'msw' export const handlers = [ http.get('/api/user/:id', () => HttpResponse.json({ id: '1', name: 'Alice' })), http.post('/api/login', async () => HttpResponse.json({ token: 'x' })), ] ``` ```ts // setup.ts import { setupServer } from 'msw/node' import { handlers } from '../mocks/handlers' const server = setupServer(...handlers) beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) ``` 测试代码里直接调真实 fetch,MSW 在网络层拦截返回 mock 数据。 比 mock `fetch` 函数干净得多。 ## Playwright 安装 ```bash npm init playwright@latest # 选项:TypeScript / 是否要 GitHub Actions / 是否装浏览器 ``` 生成的目录: ``` tests/ example.spec.ts playwright.config.ts ``` ## 第一个 E2E ```ts // tests/login.spec.ts import { test, expect } from '@playwright/test' test('user can log in', async ({ page }) => { await page.goto('/login') await page.getByLabel('Email').fill('[email protected]') await page.getByLabel('Password').fill('secret') await page.getByRole('button', { name: 'Sign in' }).click() await expect(page).toHaveURL(/\/dashboard/) await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible() }) ``` 跑: ```bash npx playwright test npx playwright test --ui # 交互模式(推荐开发用) npx playwright test --debug # 单步调试 ``` ## 多浏览器 `playwright.config.ts`: ```ts projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'mobile', use: { ...devices['iPhone 13'] } }, ] ``` `npx playwright test --project=mobile` 只跑特定 project。 ## 自动等待 / locator Playwright 的 locator 自带 auto-wait: ```ts await page.getByText('Save').click() // 等元素出现 + 可点击 + 视口内 → click ``` 不需要写 `waitForSelector` / sleep。 ## 截图 / 视频 / trace(调试神器) ```ts // playwright.config.ts use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', } ``` 测试失败时: ```bash npx playwright show-report # 浏览器打开 HTML 报告,看到失败时的截图 / 视频 / 一帧帧的 DOM 快照 ``` trace viewer 让你"时间穿越"看测试每一步页面状态——比 console.log 强 100 倍。 ## CI 集成 ```yaml # .github/workflows/test.yml - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npx playwright install --with-deps chromium - run: npm run test:unit - run: npx playwright test - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ ``` ## 性能基准 - 单元测试:每个 ~10ms,1000 个测试几秒跑完 - Playwright:每个 1-5s(浏览器启动 + 实际操作),用 `workers: 4` 并发 ## Vitest vs Jest Vitest 优势: - 与 Vite 共享配置(plugins / alias) - 启动快得多(< 1s 冷启 vs Jest 几秒) - ESM-native - Watch 模式更快(Vite 增量 build) 迁移:API 几乎与 Jest 兼容,`jest.fn()` → `vi.fn()`。 ## 踩过的坑 - `import.meta.env` 在 Vitest 里要在 setup 里 mock:测试时 env 是 test。 - Playwright 测真 API 时数据库被污染:用 `test.beforeEach` 清表,或单独 test environment + Docker 起独立 DB。 - userEvent 漏 `await`:操作没真发生测试通过,假绿。`await userEvent.click(...)` 必须 await。 - Playwright 第一次跑要下载浏览器(~300MB),CI 用 `--with-deps` 装系统 lib 否则跑不起来。
## 起因 代码搜索 / log 分析 / 文本处理,老朋友 grep 一直在。但实际开发场景: - 项目目录 grep:"grep -r 'foo' ." 默认搜 node_modules / .git → 慢 + 噪音 - 多文件类型:"grep --include='*.py'" 语法不顺 - regex 不一致(POSIX vs PCRE vs Perl) `ripgrep`(`rg`)是 Rust 写的,**默认尊重 .gitignore + 多线程 + PCRE2**。 工程师日常用比 grep 强百倍。 ## 装 ```bash brew install ripgrep apt install ripgrep cargo install ripgrep ``` ## 基础 ```bash # 当前目录搜 "foo" rg foo # 指定目录 rg foo src/ # 只搜某文件类型 rg foo -t py rg foo -tpy # 排除某类型 rg foo -T js # 列出支持的类型 rg --type-list ``` ## 默认行为对比 grep | | grep -r | rg | |---|---|---| | 默认递归 | 否(要 -r) | 是 | | 尊重 .gitignore | 不 | 是 | | 二进制文件 | 默认搜 | 默认跳过 | | 隐藏文件 | 默认搜 | 默认跳过(要 --hidden) | | 多线程 | 否 | 是 | | Unicode | 不完美 | 完美 | | 文件名颜色 | 看 alias | 默认彩 | | 速度 | 1x | 5-100x(看场景) | 在一个 100w 行 / 1 GB / 15k 文件的 monorepo 搜某 function: ```bash $ time grep -r "func MakeWidget" . real 0m12.345s $ time rg "func MakeWidget" real 0m0.143s ``` 差不多 100x(grep 还搜了 node_modules / dist 噪音输出)。 ## 常用 flag ```bash # 上下文 rg foo -A 3 # 后 3 行 rg foo -B 2 # 前 2 行 rg foo -C 5 # 前后各 5 行 # 不区分大小写 rg -i foo # 完整词 rg -w foo # 不匹配 foobar # 替换(预览,不写入) rg foo --replace bar # 写入:用 sd 或者 rg + xargs # 列文件名,不显内容 rg foo -l # 不显文件名(pipeline 用) rg foo --no-filename # count rg foo -c # 每文件 match 数 rg foo --count-matches # 总 match 数 ``` ## 玩 regex PCRE2 模式: ```bash rg -P 'func\s+(\w+)\s*\(' src/ # 复杂 PCRE # 多 line(跨行 match) rg --multiline 'class\s+\w+\s*\{[^}]*\}' # JSON path-like rg -P '"name":\s*"\w+"' ``` PCRE2 (-P) 支持 lookbehind / lookahead / named group。POSIX regex (-e) 简单但不够。 ## 在 vim / VS Code 用 vim: ```vim " .vimrc set grepprg=rg\ --vimgrep\ --no-heading :grep "TODO" " 用 rg 而不是 grep ``` VS Code:搜索框默认就是 ripgrep(VSCode 内置)。配置可以加 `search.useRipgrep` 之类(其实默认开)。 ## 配合其它工具 ```bash # fzf 实时搜 rg foo | fzf # fzf preview 找文件 + 高亮内容 rg foo -l | fzf --preview 'rg --color always -n foo {}' # xargs 批改 rg -l 'oldFunc' | xargs sed -i 's/oldFunc/newFunc/g' # 或者用 sd(rust sed) rg -l 'oldFunc' | xargs sd 'oldFunc' 'newFunc' ``` ## .ignore + .rgignore `.gitignore` ✓ 默认尊重。 项目里加 `.ignore` 或 `.rgignore` 文件,让 rg 额外排除: ``` # .ignore(rg / fd 都用) generated/ dist/ build/ *.lock ``` git 不用排但搜索不想看的。 ## 类型自定义 ```bash # 注册自定义类型 rg --type-add 'config:*.{yaml,yml,toml,json}' rg 'redis' -t config ``` 放 alias / config file: ```bash # ~/.config/ripgrep/config --type-add=config:*.{yaml,yml,toml,json} --smart-case --max-columns=200 --max-columns-preview ``` `export RIPGREP_CONFIG_PATH=~/.config/ripgrep/config` 让 rg 自动加载。 ## 性能数据 在 Linux kernel source(约 70k 文件,约 1.3 GB): ```bash $ time rg PM_RESUME 0.12s $ time grep -r PM_RESUME 3.42s $ time ag PM_RESUME # the_silver_searcher 0.45s ``` rg 是当前最快的代码搜索工具。 ## ag / ack 怎么样 - `ag`(the silver searcher):5 年前是黄金标准,今天 rg 全面超越(速度 + 功能) - `ack`:老 perl 写的,速度慢一档 - `rg` 是事实标准 ## 一个真实工作流 debug 时找"为啥某变量是这值": ```bash # 1. 哪写过这变量 rg "user_score" --type py # 2. 哪写这值 rg "user_score\s*=" --type py # 3. 函数调链 rg "calc_user_score|update_user_score" --type py -A 5 # 4. 用 vimgrep 格式给 vim rg --vimgrep "user_score" --type py > /tmp/grep.out # 然后 vim -q /tmp/grep.out 跳 quickfix list ``` 10 秒解决"找全部相关代码"。grep 同样要分多次 + 慢 + 噪音。 ## 跟 git grep 对比 `git grep` 也很快(multi-thread + 知道哪些是 tracked file): ```bash git grep "foo" # 只搜 tracked git grep -W "foo" # 显示完整函数 ``` 优势:知道 git 跟踪状态。 劣势:只搜 tracked,不能搜 untracked / ignored;项目外目录用不了。 我通常 `rg` 90% + `git grep` 10%(review 提交时)。 ## 踩过的坑 1. **隐藏文件不搜**:搜 `.env` 找不到 → `rg --hidden`。 2. **.gitignore 太严**:项目里 ignore 了 generated code → rg 也不搜。 `rg -uu` 跳过 ignore;`--no-ignore` 完全不读 .gitignore。 3. **PCRE2 没编译**:自己编译的 rg 可能没 PCRE2 support → `-P` 报错。 `rg --pcre2-version` 检查;用 prebuild。 4. **大 binary file 误判**:rg 用 NUL byte 检测 binary,某些 utf-16 文件被判 binary 跳过。`-a` 强制当文本搜。 5. **smart-case 反直觉**:默认 `--smart-case`:全小写 = 不区分大小写; 有大写 = 区分。我有时想要小写也区分大小写 → `-s`。
## 起因 第一次用 MongoDB 是从关系数据库转过来:把 PG 里的 users / posts / comments 三张表照搬成三个 collection,每篇 post 存 `user_id` 引用。 查询时 application 端做 3 次 query 拼起来。 最后发现这就是把 MongoDB 当 NoSQL "假 PG" 用,丧失了文档数据库的优势 (也没拿到关系数据库的事务)。 MongoDB 的正确姿势是设计 schema 时考虑**访问模式**(access pattern): 经常一起读的数据 embed 在一起。 ## 决策框架 按几个维度选 embed vs reference: ### 1. 一对一 / 一对多 / 多对多 - 1:1 或 1:few(< 几十)→ embed - 1:many(百级 +)→ reference(避免文档太大) - many:many → reference ### 2. 是否一起读 / 一起写 - 经常一起读 → embed - 独立访问、生命周期不同 → reference ### 3. 是否会增长 - 有界(如 user.address 通常 1-3 条)→ embed - 无界(comments 可能千万级)→ reference ### 4. 写入频率 - 子数据频繁更新(counter / status) → reference 避免文档反复重写 - 子数据基本不变 → embed ## 实战例子:博客系统 ### 模式 A:扁平 reference(关系数据库 mindset) ```js // users { _id: ObjectId("..."), name: "Alice", email: "..." } // posts { _id: ObjectId("..."), title: "...", body: "...", author_id: ObjectId("...") } // comments { _id: ObjectId("..."), post_id: ObjectId("..."), author_id: ObjectId("..."), body: "..." } ``` 读一篇 post + 它的评论 + 作者:3 次 query + application 拼接。 不是 MongoDB 的优势用法。 ### 模式 B:embed comments 进 post ```js { _id: ObjectId("..."), title: "...", body: "...", author: { _id: ObjectId("..."), name: "Alice" }, // embed 作者基本信息 comments: [ { _id: ObjectId("..."), author: { _id: ObjectId("..."), name: "Bob" }, body: "...", created_at: ISODate(), }, // ... ], created_at: ISODate(), } ``` 一次 `db.posts.findOne({_id})` 拿全部数据。 适合: - 每篇 post 评论数有限(< 几百) - 评论展示几乎总是跟 post 一起读 - 评论不会被独立查询(如"列出某用户所有评论"少见) 不适合: - 评论可能上千(文档 > 16MB 限制) - 评论独立修改频繁(每次更新整个 post 文档) - 需要按评论独立查询 ### 模式 C:分桶(bucket pattern) 如果评论会增长但仍想 embed-like 局部性: ```js // post 主体 { _id, title, body, comment_count: 42 } // comments 分桶 { _id: ObjectId(), post_id: ObjectId("..."), bucket_index: 0, // 第 0 桶 comments: [ { author, body, created_at }, // 桶内最多 100 条 // ... ] } ``` 按时间 / id 范围把评论分批。读最近评论 = 读最后一个 bucket(小文档)。 全量评论 = 多次 query 但每次都是几 KB 而非几 MB。 适合 IoT 时序数据、聊天历史、活动 feed 等。 ### 模式 D:用 sub-collection + 反规范化字段 ```js // posts (主信息 + 反规范化字段) { _id, title, body, author_id, author_name, // 作者名嵌进来避免 join comment_count: 42, last_comment_at: ISODate(), } // comments(独立 collection) { _id, post_id, author_id, author_name, body, created_at } ``` `author_name` 在 post 里冗余存——读 post 列表不需要 join users。 用户改名时要更新所有引用(cost 低频)。 `last_comment_at` / `comment_count` 在 post 上维护,列表页排序方便。 **这是大多数博客 / 社交 app 的 sweet spot**。 ## 实战例子:电商订单 ```js // orders(embed product 当时的 snapshot) { _id, user_id: ObjectId(), items: [ { product_id: ObjectId(), // 下面是下单时 snapshot,product 涨价也不变 sku: "ABC-123", name: "Widget", price: 99.50, quantity: 2, } ], total: 199.00, status: "shipped", shipping_address: { ... }, // embed payment: { method, txn_id }, // embed created_at, updated_at, } ``` 为什么 product 信息要 embed snapshot?产品涨价 / 改名 / 下架后, 历史订单还能显示下单时的价格 / 描述。这是文档数据库优势:把"事实" 冻结在事件发生时。 ## 索引 ```js // 复合索引:常按 user 查最新订单 db.orders.createIndex({ user_id: 1, created_at: -1 }) // 部分索引:只索引未完成订单 db.orders.createIndex( { user_id: 1, created_at: -1 }, { partialFilterExpression: { status: { $in: ["pending", "processing"] } } } ) // 全文搜索 db.posts.createIndex({ title: "text", body: "text" }) db.posts.find({ $text: { $search: "mongodb schema" } }) ``` `explain('executionStats')` 看 query 走没走索引: ```js db.orders.find({user_id: ObjectId("...")}).sort({created_at: -1}).limit(20).explain('executionStats') // IXSCAN + totalKeysExamined ~= nReturned = 健康 // COLLSCAN = 缺索引 ``` ## 事务 MongoDB 4.0+ 支持多文档事务: ```js const session = client.startSession() try { session.startTransaction() await db.collection('orders').insertOne(order, { session }) await db.collection('inventory').updateOne( { _id: productId }, { $inc: { stock: -quantity }}, { session } ) await session.commitTransaction() } catch (e) { await session.abortTransaction() throw e } finally { session.endSession() } ``` 但好的 schema 设计应该让大多数操作**不需要跨文档事务**。 embed 把"原子操作"装进一个文档里,单文档写自然原子。 ## Aggregation pipeline 复杂查询用 aggregation: ```js db.orders.aggregate([ { $match: { status: "shipped" } }, { $unwind: "$items" }, { $group: { _id: "$items.sku", revenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] }}, count: { $sum: "$items.quantity" } }}, { $sort: { revenue: -1 } }, { $limit: 10 }, ]) ``` 像 SQL GROUP BY 但 stages 串成 pipeline。比 application 端聚合快 10-100x (DB 内执行,结果集小)。 ## 何时不用 MongoDB - 强事务 + 多表 join + 复杂报表 → PostgreSQL - 关系紧密 + schema 严格 → 关系库 - 极大写吞吐(百万 ops/s)+ key-value → DynamoDB / Cassandra MongoDB 适合:用户 profile / 内容 / 半结构化 / 易变 schema / fast-iterating 产品。 ## 效果 我们的内容系统迁移到合理设计: - 首页列表 query:3 次 SQL JOIN(PG)→ 1 次 find(Mongo),延迟从 80ms → 12ms - 文档存 markdown 源 + render 后 HTML + metadata 一起,全 cache 友好 - schema 增加新字段无需 migration(直接写新 doc) ## 踩过的坑 1. **文档 > 16MB**:MongoDB 单文档硬上限。无限增长的 array 早晚踩。 2. **数组上的 `$push` + `$slice`**:要限大小: ```js db.posts.updateOne( { _id: postId }, { $push: { comments: { $each: [newComment], $slice: -100 } } } ) ``` 保留最近 100 条评论,老的滚出。 3. **null 和缺字段语义混淆**: ```js { foo: null } // 字段存在,值为 null { /* no foo */ } // 字段缺失 ``` `find({foo: null})` 同时匹配两种。要区分用 `{foo: {$exists: true}}`。 4. **ObjectId vs string**:把 string `_id` 传给 `find({_id: id})` 匹配不到 ObjectId。永远 `new ObjectId(idStr)` 转换。 5. **schema 后期变更**:MongoDB 不强制 schema,但 application 假设 字段存在。改 schema 需要 migrate 旧文档(`updateMany` 加默认值)。 建议生产用 schema validation: ```js db.createCollection("orders", { validator: { $jsonSchema: { ... } } }) ```
## 起因 老办法做 responsive: ```css .card { padding: 1rem; } @media (min-width: 768px) { .card { padding: 2rem; display: flex; } } ``` 依据**viewport** 调整。 但很多组件不关心 viewport,它关心**自己父容器多大**。 例:card 组件放 sidebar(300px)时纵向布局,放主区(800px)时横向。 media query 帮不上忙 → 同 viewport 不同位置要不同样式。 **Container queries**(CSS 2023+ 主流支持)解决:组件根据**容器 自己大小**调样式。 ## 基本用法 父元素声明 containment: ```css .sidebar { container-type: inline-size; container-name: sidebar; } .main { container-type: inline-size; } ``` 子元素 query 容器: ```css .card { padding: 1rem; display: block; } @container (min-width: 500px) { .card { padding: 2rem; display: flex; gap: 1rem; } } ``` card 在 sidebar (300px) 内 → block。 同 card 在 main (800px) 内 → flex。 ## containment type - `inline-size`:监听宽(最常用) - `size`:宽 + 高 - `normal`:不监听(默认) ```css .parent { container-type: inline-size; } ``` 注意:`container-type` 让元素成为 containment context,影响 layout 计算(不一定能在所有元素用)。 ## named container ```css .sidebar { container-type: inline-size; container-name: sidebar; } .main { container-type: inline-size; container-name: main; } @container sidebar (min-width: 400px) { ... } @container main (max-width: 600px) { ... } ``` 按 container 名 query,避免 ambiguity。 ## query unit (cqw / cqh) ```css .card { font-size: 5cqi; /* container inline-size 的 5% */ padding: 2cqi; } ``` - `cqi`:container inline-size 1% - `cqb`:container block-size 1% - `cqw` / `cqh`:absolute width/height (less common) 字号跟容器大小成正比,缩放友好。 ## 实战 example:可复用 card ```css .card { container-type: inline-size; border: 1px solid #ddd; border-radius: 8px; } .card-inner { padding: 1rem; } .card-thumb { width: 100%; aspect-ratio: 16/9; } @container (min-width: 400px) { .card-inner { display: grid; grid-template-columns: 150px 1fr; gap: 1rem; } .card-thumb { width: 150px; aspect-ratio: 1; } } @container (min-width: 700px) { .card-inner { grid-template-columns: 200px 1fr; } .card-thumb { width: 200px; } } ``` 同一个 `.card` 在任何容器里自适应,不需要 media query 协调。 ## media query 仍有用 container query 不是 100% 替代 media query: - **页面级布局**(sidebar/main 切换)→ media query - **组件内适应** → container query - **基于设备特性(hover / touch)** → media query - **prefers-color-scheme** → media query 混用: ```css @media (prefers-color-scheme: dark) { .card { background: #222; } } @container (min-width: 500px) { .card { display: flex; } } ``` ## style query (实验) ```css @container style(--theme: dark) { .card { background: black; } } ``` 依据自定义 prop 值变样式。Chrome 111+,Safari 18+。 还在 stabilizing。 ## 浏览器支持 Chrome 105+ / Safari 16+ / Firefox 110+ → 主流浏览器从 2022 末 / 2023 全支持。 2026 视角:可以默认用,老 browser 用 `@supports` fallback: ```css @supports not (container-type: inline-size) { /* fallback to media query */ } ``` ## 与 CSS-in-JS 对比 CSS-in-JS(styled-components / Emotion)经常通过 prop 控制样式: ```jsx <Card variant={width > 500 ? 'wide' : 'narrow'} /> ``` JS 测宽 → re-render → 改样式。复杂 + JS 阻塞。 container query 纯 CSS,浏览器原生计算 → 更高效 + 简洁。 ## 实际项目效果 我重构一个 dashboard,从 media query / JS measure 改 container query: - 删 200+ 行 JS layout logic - CSS 简化(不需要 `at-768`, `at-1024` 等命名) - 同组件在 modal / sidebar / 主区 都能 work - bundle 小 5KB 最大改善:组件**真正可复用** —— 不需要为不同上下文写变体。 ## 与 grid auto-fit / minmax 对比 ```css /* grid 自适应 N 列 */ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } ``` grid `auto-fit` 已经覆盖一些 container query 用例。 但 grid 是 layout 内自适应;container query 是组件内部样式。 ## 踩过的坑 1. **`container-type: inline-size` 副作用**:让元素 layout 隔离, 某些 height 计算变化。布局突变 → 检查父子。 2. **嵌套 container**:子 container 的 query 默认查最近的 named container,不指定容易混乱。明确 `container-name`。 3. **inline element 不能 container-type**:必须 block / inline-block 或者 display 至少能 contain。 4. **`@container` 在 nested rule 内**:CSS nesting 里写时注意顺序。 5. **devtool 难调**:Chrome devtools 显示 container query 没 media query 那么直观。仔细看 box model。
## 起因 某些场景"在边缘节点跑代码" 比"回源" 更优: - 全球用户都需要的轻请求(API rate limit / auth check / A/B redirect) - 静态站定制(个性化 header / cookie 处理) - 流量塑形(特定 user 转到 staging) - 短 latency API(< 50ms 全球) `Cloudflare Workers`:V8 isolate 跑在 250+ 城市边缘节点。 冷启动 < 5ms(不是 Lambda 那种),按请求计费便宜。 ## hello world ```js // worker.js export default { async fetch(request, env, ctx) { return new Response('Hello from the edge!'); }, }; ``` 部署: ```bash npm install -g wrangler wrangler init my-worker cd my-worker wrangler deploy ``` 5 分钟得到 `my-worker.user.workers.dev` URL,全球 250+ 城市同时跑。 ## 完整示例:geo redirect ```js export default { async fetch(request) { const country = request.cf.country; // CF 自动加 country header const url = new URL(request.url); if (country === 'CN' && url.hostname === 'example.com') { return Response.redirect('https://cn.example.com' + url.pathname, 302); } // 其它转到 origin return fetch(request); }, }; ``` 中国用户自动重定向到 cn 子域,其它人正常回源。 ## A/B 测试 ```js export default { async fetch(request) { const cookie = request.headers.get('cookie') || ''; let variant = parseCookie(cookie, 'variant'); if (!variant) { variant = Math.random() < 0.5 ? 'A' : 'B'; } const response = await fetch( variant === 'B' ? request.url.replace('example.com', 'beta.example.com') : request, ); const newResponse = new Response(response.body, response); newResponse.headers.set('set-cookie', `variant=${variant}; Path=/; Max-Age=86400`); return newResponse; }, }; ``` 50/50 分流,cookie 粘性。无需改后端。 ## 鉴权 / API gateway ```js export default { async fetch(request, env) { const auth = request.headers.get('authorization'); if (!auth || !auth.startsWith('Bearer ')) { return new Response('Unauthorized', { status: 401 }); } const token = auth.slice(7); // 验 JWT(用 Web Crypto API) const valid = await verifyJWT(token, env.JWT_PUBLIC_KEY); if (!valid) return new Response('Invalid token', { status: 401 }); // 加 user info 转到 origin const newRequest = new Request(request); newRequest.headers.set('x-user-id', valid.sub); return fetch(newRequest); }, }; ``` 边缘 JWT 验证 → 无效请求不打到 origin → 省 origin 带宽 / CPU。 ## KV / D1 / R2 存储 Workers 配套存储: - **KV**:edge K/V,最终一致,读快写慢 - **D1**:SQLite at edge(每个 region 副本) - **R2**:S3 兼容对象存储,无 egress 费 ```js // 读 KV const value = await env.MY_KV.get('user:42'); // D1 query const result = await env.MY_DB.prepare( 'SELECT * FROM users WHERE id = ?').bind(42).first(); // R2 上传 await env.MY_BUCKET.put('file.bin', request.body); ``` Workers + KV / D1 / R2 全栈在边缘 → 静态站 + API + 数据全套。 ## Durable Objects 需要强一致 + stateful → DO: ```js export class Counter { constructor(state, env) { this.state = state; } async fetch(request) { let count = (await this.state.storage.get('count')) || 0; count++; await this.state.storage.put('count', count); return new Response(count.toString()); } } // Worker export default { async fetch(request, env) { const id = env.COUNTER.idFromName('global'); const obj = env.COUNTER.get(id); return obj.fetch(request); }, }; ``` DO 是全球唯一 instance(按 name),适合:counter / chat room / collaborative state。WebSocket 跨用户的协作 app 杀手锏。 ## limits - 单请求 CPU: 50ms (free) / 30s (paid) - memory: 128 MB - subrequest: 50 (free) / 1000 (paid) - script size: 1 MB compressed (10 MB paid) 不像 Lambda 可以重计算几分钟。 Workers 是"轻量边缘 hook",不是通用 compute。 ## 价格 - free tier: 100k req/day - $5/月: 10M req - $5 per additional 1M req KV / R2 / D1 各自计费但都便宜。 对比 Lambda(cold start + GB-second + egress):高 QPS edge 场景 Workers 便宜 5-10x。 ## 本地 dev ```bash wrangler dev # 本地起 worker(用 V8 模拟) wrangler dev --remote # 直接在 CF 边缘 dev ``` `wrangler.toml`: ```toml name = "my-worker" main = "src/index.js" compatibility_date = "2024-05-25" [vars] ENVIRONMENT = "production" [[kv_namespaces]] binding = "MY_KV" id = "..." [[d1_databases]] binding = "MY_DB" database_name = "myapp" database_id = "..." ``` ## TypeScript / Hono ```ts // 用 Hono framework import { Hono } from 'hono'; const app = new Hono(); app.get('/api/users/:id', async (c) => { const id = c.req.param('id'); const user = await c.env.MY_DB.prepare( 'SELECT * FROM users WHERE id = ?').bind(id).first(); return c.json(user); }); export default app; ``` Hono 是 Workers 上的 Express 替代,类型 + 路由友好。 ## 跟 Lambda@Edge 对比 | | CF Workers | AWS Lambda@Edge | |---|---|---| | 启动 | < 5ms (V8 isolate) | 100-500ms (cold) | | 语言 | JS / Wasm / Python | Node / Python | | 限制 | 50ms CPU | 5s | | 存储 | KV / D1 / R2 / DO | 需调外部 | | 价格 | $0.50 / M req | $0.60 / M req(更贵) | | Region | 250+ | CF 100+ | Workers 现在边缘 compute 几乎事实标准。 AWS 也在推 Lambda@Edge 但慢。 ## 真实 case:API rate limit 我们 API 想全球分布式 rate limit(不只是单 region)。 ```js export default { async fetch(request, env) { const ip = request.headers.get('cf-connecting-ip'); const key = `rl:${ip}`; const count = (await env.RL_KV.get(key)) || 0; if (count >= 100) { return new Response('Too Many Requests', { status: 429 }); } await env.RL_KV.put(key, (parseInt(count) + 1).toString(), { expirationTtl: 60 }); return fetch(request); }, }; ``` 每 IP 每分钟 100 req,跨 CF region 累计(KV 最终一致,5-10s 同步 → 某 region 用户可能 over limit 一点,可接受)。 强一致 → 用 DO + sliding window。 ## 不适合的场景 - 长跑 CPU(视频转码 / ML inference):用 Workers AI / cloud function - 大文件处理(> 100 MB body):直接 R2 - 复杂 stateful 流(DB transaction 多):传统 API server ## 踩过的坑 1. **request body 不能多次读**:`request.body` 是 stream,读一次。 需多用:`const body = await request.text()` 先 buffer。 2. **fetch subrequest 计数**:每 `fetch()` 算 1 subrequest,超 50 (free)报错。复杂 worker 谨慎。 3. **KV 写 eventual consistency**:刚写完读可能旧值。读 critical 用 D1 或 DO。 4. **environment binding 没配**:本地 dev 跑通,部署后 `env.MY_KV` undefined → 看 wrangler.toml 是否 deploy。 5. **package size 1MB**:依赖大(如 lodash 全部 import)→ size 超。 tree-shake + 小依赖(zod / arktype 替代)。
## 起因 K8s 默认 CNI(flannel / calico iptables 模式)问题: - 服务多了 iptables 规则数万条 → packet 经过 N rules 慢 - network policy 通过 iptables 模拟 → 性能差 + 难调试 - 没原生 L7 (HTTP) policy - 跨节点流量 encap (VXLAN) 开销 **Cilium** 用 eBPF 在内核态做: - pod 间通信(直接路由 / VXLAN / WireGuard) - network policy(L3/4/7) - service LB 替代 kube-proxy - observability(hubble) - mTLS eBPF 不走 iptables 链 → 性能高 + 灵活。 ## 装 (kind 本地) ```bash # kind cluster 不带默认 CNI kind create cluster --config kind-config.yaml cat > kind-config.yaml <<EOF kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 networking: disableDefaultCNI: true kubeProxyMode: none # cilium 替代 EOF # 装 cilium cilium install --version 1.16.0 cilium status ``` ## 网络 policy(L3/L4) ```yaml apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: api-policy namespace: app spec: endpointSelector: matchLabels: app: api ingress: - fromEndpoints: - matchLabels: app: frontend toPorts: - ports: - port: "8080" protocol: TCP ``` `api` pod 只接受 `frontend` pod 的 8080 流量。 其它源(包括同 namespace 别的 pod)阻断。 ## L7 policy (HTTP) ```yaml spec: endpointSelector: matchLabels: app: api ingress: - fromEndpoints: - matchLabels: app: frontend toPorts: - ports: - port: "8080" rules: http: - method: "GET" path: "/api/users/.*" - method: "POST" path: "/api/login" ``` frontend 只能 GET `/api/users/*` 和 POST `/api/login`,其它 404。 传统 iptables 做不到。 ## hubble (observability) ```bash cilium hubble enable # CLI hubble observe --namespace=app TIMESTAMP SOURCE DESTINATION TYPE VERDICT 12:34:56 frontend-xxx:34521 api-yyy:8080 L7 ALLOWED (GET /api/users/42) 12:34:57 frontend-xxx:34522 api-yyy:8080 L7 DROPPED (POST /admin/delete) ``` 每 packet 看 source / dest / verdict。 debug network 神器。 hubble UI 浏览器看: ```bash cilium hubble ui ``` 实时 service map + flow log。 ## kube-proxy 替代 ```bash # install 时 --set kubeProxyReplacement=true helm install cilium ... --set kubeProxyReplacement=true ``` Cilium 用 eBPF 实现 service routing → 删 kube-proxy → 删几万条 iptables。 性能: | | iptables | Cilium eBPF | |---|---|---| | service connect 延迟 | 50 μs | 5 μs | | pod-to-pod throughput | 7 Gbps | 9.5 Gbps | | iptables rule 数 | 几万 | 0 | 大 cluster 显著。 ## 跨节点流量 模式选: - **VXLAN**:兼容性最好(默认) - **Geneve**:VXLAN 替代 - **直接路由**:节点同 L2 网(性能最好) - **WireGuard**:跨 region 加密 - **IPsec**:类似 我们 prod 用直接路由(节点同 VPC)+ WireGuard 用于跨 region。 ## bandwidth manager ```yaml apiVersion: v1 kind: Pod metadata: annotations: kubernetes.io/egress-bandwidth: "10M" ``` cilium 用 eBPF 限 pod 出口带宽 → 防 noisy neighbor。 ## clustermesh (多 cluster) ```bash cilium clustermesh enable --context cluster1 cilium clustermesh connect --context cluster1 --destination-context cluster2 ``` 两个 cluster 互通 service: ```yaml # Service on cluster2 metadata: annotations: service.cilium.io/global: "true" ``` cluster1 的 pod 访问该 service → cilium 跨 cluster 路由。 无需 service mesh / API gateway。 ## mTLS (cilium 1.14+) ```yaml spec: endpointSelector: matchLabels: { app: api } ingress: - fromEndpoints: - matchLabels: { app: frontend } authentication: mode: required # 强制 mTLS ``` cilium 用 SPIFFE 标识自动 mTLS。 比 Istio 简单(不需要 sidecar)。 ## 性能 CNI throughput benchmark(10 Gbps 网络): | | Pod-to-Pod | |---|---| | flannel VXLAN | 6 Gbps | | calico iptables | 7 Gbps | | calico eBPF | 9 Gbps | | Cilium (native routing) | 9.5 Gbps | cilium 几乎跑满。 ## 与 calico eBPF 对比 | | Cilium | Calico (eBPF mode) | |---|---|---| | L7 policy | ✅ | 弱 | | Hubble observability | ✅ 强 | 基本 | | clustermesh | ✅ | 弱 | | mTLS | ✅ | 计划中 | | 复杂度 | 高 | 中 | | 生态 | 大(CNCF) | 大 | 两者都好。cilium 是更现代 / 功能多。calico 更老更广。 ## 与 Istio 对比 | | Cilium | Istio | |---|---|---| | 层次 | CNI + L7 policy | service mesh (L7+) | | sidecar | 不需要 | envoy sidecar | | 资源开销 | 低 (eBPF kernel) | 高 (sidecar 每 pod) | | mTLS | ✅ | ✅ | | traffic policy | 中 | 强 (canary / mirror 等) | | 复杂度 | 中 | 高 | Cilium Service Mesh(cilium 1.12+)能部分替代 Istio。 重 traffic management 还是 Istio。 ## 实战 case:从 calico 迁 cilium 我们 prod cluster 用 calico 几年,问题: - network policy 调试痛苦 - 没 L7 policy - 缺 observability 迁 cilium: 1. 装 cilium + 关 calico(drain 一台一台测) 2. policy 翻译(calico NetworkPolicy → CiliumNetworkPolicy,多数语法兼容) 3. 启用 hubble + kube-proxy replacement 4. 启用 mTLS(替代 Istio mTLS) 效果: - service-to-service latency 平均 -30%(无 iptables) - 一次 debug "为啥 A 连不上 B" 从一上午 → 5 分钟(hubble 直接看 verdict) - 节点 CPU -10%(kube-proxy 不跑了) 迁移挑战: - L7 policy 需要 sidecar (envoy embed in cilium),资源 +20% - WireGuard 跨 region 需要内核 5.6+ ## 监控 prometheus metrics 几百个: - `cilium_drop_count_total` - `cilium_policy_l7_total` - `cilium_endpoint_state` Grafana 官方 dashboard 拉来直接看。 ## 踩过的坑 1. **内核版本**:很多 eBPF feature 要内核 5.10+。Ubuntu 20.04 默认 5.4 → 升级。 2. **kube-proxy replacement + HostNetwork**:HostNetwork pod 跟 cilium service 不交互 → 部分场景 fall back iptables。 3. **policy 默认 allow**:没 CiliumNetworkPolicy 时 default 允许所有 流量。要 default-deny → 加 catch-all policy。 4. **hubble retention**:默认内存只存 4096 flow → 大 cluster 看不到 历史。配 hubble-export 推 ELK。 5. **multi-cluster identity 冲突**:clustermesh 时不同 cluster pod labels 同 → 路由错。namespace / label 命名规范。
云厂商的 K8s(EKS / GKE / AKS)按月几十美元。自己买几台便宜 VPS 也能跑 K8s——一台 control plane + 2 台 worker,每月几美元。 适合自学 / 个人项目 / 小规模生产。 下面用 kubeadm 起 v1.30 集群。 ## 准备(每台机器都做) ```bash # 关 swap(K8s 要求) sudo swapoff -a sudo sed -i '/swap/d' /etc/fstab # 内核模块 cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay br_netfilter # sysctl cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system ``` ## 装 containerd ```bash sudo apt update sudo apt install -y containerd sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml # 启 SystemdCgroup(K8s 要求) sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml sudo systemctl restart containerd ``` ## 装 kubeadm / kubelet / kubectl ```bash sudo apt install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \ | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' \ | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt update sudo apt install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl ``` ## 初始化 control plane(只在 master 上) ```bash sudo kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=<MASTER_IP> ``` 完成后会打印一段 `kubeadm join ...` 命令——保存下来,下面 worker 用。 让普通用户用 kubectl: ```bash mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config kubectl get nodes # 状态会是 NotReady —— 因为没装 CNI ``` ## 装 CNI(这里用 Flannel) ```bash kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml # 等 1-2 分钟 kubectl get pods -A kubectl get nodes # Ready 了 ``` 其它 CNI 选择: - **Calico**:网络策略 + BGP,生产更常用 - **Cilium**:eBPF,性能 + 观测最好 - **Flannel**:最简单,仅 overlay 网络 ## Worker 加入 每台 worker 上: ```bash sudo kubeadm join <MASTER_IP>:6443 \ --token <TOKEN> \ --discovery-token-ca-cert-hash sha256:<HASH> ``` Master 上: ```bash kubectl get nodes # NAME STATUS ROLES AGE VERSION # master Ready control-plane 5m v1.30.0 # worker-1 Ready <none> 1m v1.30.0 # worker-2 Ready <none> 1m v1.30.0 ``` ## 部署个 demo ```yaml # nginx-demo.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: { app: nginx } template: metadata: { labels: { app: nginx } } spec: containers: - name: nginx image: nginx:alpine ports: [{ containerPort: 80 }] --- apiVersion: v1 kind: Service metadata: name: nginx spec: type: NodePort selector: { app: nginx } ports: - port: 80 nodePort: 30080 ``` ```bash kubectl apply -f nginx-demo.yaml kubectl get pods -o wide curl http://<任意-Node-IP>:30080 ``` ## Ingress(暴露 80/443 到公网) NodePort 30000+ 端口不友好,用 Ingress: ```bash kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/baremetal/deploy.yaml ``` ```yaml # ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: { name: nginx, port: { number: 80 } } ``` 把 `<MASTER_IP>` 或 worker IP DNS 指到 `app.example.com`, 80/443 流量进 ingress-nginx,再分发到 service。 ## cert-manager 自动 HTTPS ```bash kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml ``` ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: { name: letsencrypt-prod } spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: { name: letsencrypt-prod } solvers: - http01: ingress: { class: nginx } ``` Ingress 加 annotation `cert-manager.io/cluster-issuer: letsencrypt-prod`, 自动签发 + 续期证书。 ## 备份 etcd ```bash # 在 master 上 sudo ETCDCTL_API=3 etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /tmp/etcd-$(date +%F).db ``` 定时备份这个文件到对象存储是单 master K8s 集群唯一的 DR 手段。 ## 升级 ```bash sudo apt-mark unhold kubeadm sudo apt install -y kubeadm=1.30.5-* sudo apt-mark hold kubeadm sudo kubeadm upgrade plan sudo kubeadm upgrade apply v1.30.5 # 然后升 kubelet / kubectl ``` ## 踩过的坑 - swap 没关 kubelet 启动失败:v1.22 之前默认要求;v1.22+ 可以保留 swap 但要在 kubelet 配置里显式 `failSwapOn: false`。 - 防火墙:control plane 需要开 6443、10250、2379-2380,CNI 用的端口 各异(Flannel UDP 8472)。最简单:把 master / worker 之间的网络 完全开放(内网或 VPC)。 - Token 24 小时过期:worker 加入时 `kubeadm token create --print-join-command` 生成新 token。 - 单 master 没冗余:宕机就完蛋。生产多 master + HA load balancer + 外部 etcd cluster。kubeadm 也支持。
## 起因 我在 macOS 用 zsh + oh-my-zsh、Linux 服务器用 bash、Windows 偶尔用 PowerShell。每个 shell 一套提示符配置:颜色、git branch 显示、Python venv 提示、kubectl context… 三套配置一改全乱。 `starship` 是 Rust 写的跨 shell 提示符生成器:一份 TOML 配置文件, 所有 shell 都能用同一份。 ## 安装 ```bash # 一行装 curl -sS https://starship.rs/install.sh | sh # 或包管理器 brew install starship sudo apt install starship # Debian 12+ / Ubuntu 24.04+ scoop install starship # Windows starship --version ``` ## 接到 shell ```bash # bash: ~/.bashrc 末尾 eval "$(starship init bash)" # zsh: ~/.zshrc 末尾 eval "$(starship init zsh)" # fish: ~/.config/fish/config.fish 末尾 starship init fish | source # PowerShell: $PROFILE Invoke-Expression (&starship init powershell) ``` 重启 shell,提示符立刻变好看。 ## 默认行为 启动后默认显示: - 当前目录(智能截断 home / 项目根) - Git 分支 + 状态(修改 / 新文件 / 待 push) - 当前语言版本(在该项目目录自动显示 Python/Node/Go/Rust 版本) - exit code(非 0 时红色显示) - 命令耗时(> 2s 自动显示) 不用任何配置就比 default 强 10 倍。 ## 个性化 ~/.config/starship.toml 我用的(基于官方 nerd-font symbol preset 简化): ```toml format = """ $directory\ $git_branch\ $git_status\ $python\ $nodejs\ $golang\ $rust\ $kubernetes\ $cmd_duration\ $line_break\ $character\ """ [character] success_symbol = "[➜](bold green)" error_symbol = "[✗](bold red)" vimcmd_symbol = "[V](bold yellow)" [directory] truncation_length = 3 # 只显示最后 3 段 truncate_to_repo = true # 进 repo 后只显示从 repo 根的相对路径 style = "bold cyan" [git_branch] symbol = " " style = "purple" [git_status] ahead = "⇡${count}" behind = "⇣${count}" diverged = "⇕⇡${ahead_count}⇣${behind_count}" untracked = "?${count}" modified = "✱${count}" staged = "+${count}" deleted = "✘${count}" style = "yellow" [cmd_duration] min_time = 2_000 format = "took [$duration]($style) " style = "yellow" [python] symbol = " " format = '[${symbol}${pyenv_prefix}(${version} )(\($virtualenv\) )]($style)' style = "bold yellow" [nodejs] symbol = " " format = "[${symbol}${version}]($style) " style = "bold green" [kubernetes] disabled = false format = '[$symbol$context( \($namespace\))]($style) ' style = "bold blue" # 关一些少用的模块加速 [aws] disabled = true [gcloud] disabled = true [package] disabled = true ``` ## 看效果 ``` ~/projects/myapp on feature/auth ✱2 ?1 3.12.0 (.venv) prod default took 4.5s ➜ ``` ## 让符号显示完整 starship 用了大量 nerd-font 符号(、、、)。终端必须装 Nerd Font 字体才能正常显示: ```bash # macOS brew tap homebrew/cask-fonts brew install --cask font-jetbrains-mono-nerd-font # Linux mkdir -p ~/.local/share/fonts cd ~/.local/share/fonts curl -fLO https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip unzip JetBrainsMono.zip fc-cache -fv # 然后终端 settings 字体改成 'JetBrainsMono Nerd Font' ``` 不装 Nerd Font 也能用——把 `symbol` 改成普通字符或留空。 ## 跨机器同步配置 把 `~/.config/starship.toml` 放进 dotfiles repo: ```bash cd ~/dotfiles ln -sf $(pwd)/starship.toml ~/.config/starship.toml git add starship.toml git commit -m 'starship config' ``` 新机器: ```bash git clone https://github.com/me/dotfiles ln -sf $(pwd)/dotfiles/starship.toml ~/.config/starship.toml ``` 一次配置,所有机器一致。 ## 性能 starship 单次 prompt 渲染 < 30ms(Rust + 并行模块)。 慢命令后 prompt 也立即响应。 ```bash # 看哪个模块拖时间 STARSHIP_LOG=debug starship prompt 2>&1 | grep -i 'time' ``` 发现慢模块 → 关掉或加 `disabled = true`。 ## 与 oh-my-zsh / Spaceship 等对比 | 工具 | 速度 | 跨 shell | 配置语法 | |---|---|---|---| | **starship** | 极快(Rust) | ✅ 全支持 | TOML | | oh-my-zsh themes | 慢(zsh 脚本) | 仅 zsh | shell | | Spaceship | 中(zsh script) | 仅 zsh | zsh vars | | pure | 极快 | 仅 zsh | minimal | | p10k | 极快(C) | 仅 zsh | wizard | 如果你只用 zsh,p10k 配置体验更好;多 shell 选 starship。 ## 效果 - 三台机器 + 两个 shell 同步配置,零差异 - 提示符里直接看到 git branch / venv / Node 版本,少跑 N 个 `git status` - 命令耗时显示让我知道哪些命令该 background 跑 - 同事看见后被安利的成功率约 50% ## 踩过的坑 1. **服务器 SSH 上没 Nerd Font** → 各种"豆腐块"。`starship preset plain-text-symbols` 切到纯文本符号 preset。 2. **WSL 启动慢**:默认每次 prompt 都查 git。`scan_timeout = 10` 限制超时;大仓库 disable git 模块只保留 branch。 3. **tmux 里颜色错**:tmux 设 `set -g default-terminal "tmux-256color"` + `set -ga terminal-overrides ',xterm-256color:Tc'` 启用真彩色。 4. **PowerShell 上 modulator 不显示**:Windows Terminal 字体必须设为 `CaskaydiaCove NF` 之类的 Nerd Font 变体。系统全局字体不算。 5. **zoxide / direnv 等其它工具的钩子顺序**:starship init 应在最后, 否则它的 hook 被覆盖。
## 起因 我们的备份服务器收 rsync from 多台业务机器。问题: - rsync 跑到一半,业务机继续写新文件 → 备份目录里出现"part-A from 12:00, part-B from 12:15" 时间错乱的版本 - 备份过程中如果客户端机器挂了 → 备份目录是半新半旧 - 想"备份这一刻的整盘快照" 而不是"几小时内分散的快照" 如果备份目标是 ZFS 文件系统,可以利用 ZFS snapshot 做"事务性"备份: 1. rsync 完整跑完 2. 跑完后立即 zfs snapshot 3. 之后业务继续 rsync 上新数据 4. snapshot 是这一刻完整一致的副本 ## 完整流程 ### setup 备份服务器 NAS 上: ```bash # 创建一个 ZFS dataset 收备份 sudo zfs create backuptank/clients/server-foo sudo zfs set compression=zstd backuptank/clients/server-foo sudo zfs set atime=off backuptank/clients/server-foo # 给 rsync 用户写权限 sudo zfs allow rsync-user create,destroy,snapshot,mount backuptank/clients/server-foo ``` ### 业务机推送 + snapshot 脚本 ```bash #!/usr/bin/env bash # /usr/local/sbin/snap-rsync.sh set -euo pipefail REMOTE_USER=rsync-user REMOTE_HOST=nas.local REMOTE_DATASET=backuptank/clients/server-foo REMOTE_MOUNT=/backuptank/clients/server-foo SRC="/etc /home /srv" HOSTNAME=$(hostname -s) TS=$(date +%Y-%m-%dT%H-%M-%S) # 1. 同步过去(增量;--delete 保镜像) rsync -aAXH --numeric-ids --delete --info=stats2,progress2 \ -e ssh \ $SRC "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_MOUNT}/${HOSTNAME}/" # 2. 完成后远程 zfs snapshot 标记这一刻 ssh "${REMOTE_USER}@${REMOTE_HOST}" \ "sudo zfs snapshot ${REMOTE_DATASET}@${HOSTNAME}-${TS}" echo "snapshot: ${REMOTE_DATASET}@${HOSTNAME}-${TS}" # 3. retention:保留最近 30 个 snapshot ssh "${REMOTE_USER}@${REMOTE_HOST}" " sudo zfs list -H -o name -t snapshot ${REMOTE_DATASET} \ | grep '@${HOSTNAME}-' \ | sort -r \ | tail -n +31 \ | xargs -r -n 1 sudo zfs destroy " ``` 每天 cron / timer 跑这个。 ### 浏览 / 还原历史快照 NAS 上 ZFS snapshot 默认可访问: ```bash ls /backuptank/clients/server-foo/.zfs/snapshot/ # server-foo-2024-05-24T03-00-00 # server-foo-2024-05-23T03-00-00 # ... # 还原某个文件 cp /backuptank/clients/server-foo/.zfs/snapshot/server-foo-2024-05-24T03-00-00/etc/nginx/nginx.conf /tmp/ ``` 或者 `zfs clone` 把 snapshot 挂成可写副本: ```bash sudo zfs clone backuptank/clients/server-foo@server-foo-2024-05-24T03-00-00 \ backuptank/restore/server-foo-2024-05-24 mount | grep restore ``` 任意时间点的完整目录树都能挂载浏览。 ## ZFS dedup(可选) 如果多机器备份内容重叠多(OS 文件大体相同): ```bash sudo zfs set dedup=on backuptank/clients ``` 代价:每 1 TB dedup 数据约需 5 GB RAM 维护 dedup table。 所以只在高 dedup ratio + 充裕 RAM 时开。 测: ```bash sudo zdb -S backuptank # 输出 estimated dedup ratio:1.5x 以上才值得开 ``` ## ZFS send:异地同步备份 ```bash # 第一次全量 → 异地 NAS sudo zfs send backuptank/clients/server-foo@latest \ | ssh remote-nas "sudo zfs receive backuptank-mirror/server-foo" # 后续增量 sudo zfs send -i @prev-snap backuptank/clients/server-foo@new-snap \ | ssh remote-nas "sudo zfs receive backuptank-mirror/server-foo" ``` 或者 syncoid 自动: ```bash syncoid backuptank/clients/server-foo remote-nas:backuptank-mirror/server-foo ``` 每天本地 rsync + snapshot;每周 syncoid 异地。**两层保护**。 ## 与"直接备到 ZFS snapshot" 的对比 替代方案:客户端不 rsync,业务机自己 ZFS snapshot + zfs send 给 NAS。 ```bash # 业务机 sudo zfs snapshot tank/data@$(date +%F) sudo zfs send -i @prev tank/data@new \ | ssh nas "sudo zfs receive backuptank/clients/foo" ``` 优点: - 原生 ZFS 一致性快照(毫秒级 frozen) - 增量 send 只传变化的 block(比 rsync 比 file 快很多) - 文件系统级,不漏 metadata / xattr / hardlink 要求:业务机文件系统是 ZFS。 如果业务机是 ext4 / xfs → rsync + 备份目标 ZFS snapshot 是 fallback。 ## monitoring ```bash # 看 snapshot 数 + 大小 zfs list -t snapshot backuptank/clients/server-foo # Prometheus exporter (node_exporter zfs collector) # 暴露:node_zfs_zpool_state{pool="backuptank"} = 1 (ONLINE) # node_zfs_zfs_dataset_used_bytes # node_zfs_zfs_dataset_available_bytes ``` 仪表盘看: - snapshot 数量是否正常增长 - 各客户端的备份目录大小变化 - 最新 snapshot 时间戳(确认 cron 跑了) 告警:snapshot 超过 26 小时没新 = 备份链断了。 ## retention 策略:sanoid 风格 手写 `tail -n +31` 简单但只按计数。sanoid 风格按时间精细: ```bash # 保留: # - 最近 7 个 hourly # - 最近 30 个 daily # - 最近 12 个 monthly snapshots=$(ssh nas "sudo zfs list -H -o name -t snapshot backuptank/clients/foo") # 分组按时间删 # (实际写起来复杂;建议用 sanoid 现成工具) ``` sanoid 配 retention policy + 跨多 dataset 统一管。装好后写一个 `sanoid.conf` 完事。 ## 实战效果 我们 20 台业务机每天备份到一台 NAS: | | rsync only | rsync + ZFS snapshot | |---|---|---| | 单次备份耗时 | 30 min | 30 min | | 一致性 | 半天的飘移 | 时刻精确 | | 历史版本 | 没有 | 30 天日级 | | 占用空间 | 增量 | 增量 + dedup 后接近 | | 还原历史文件 | 困难 | `cp /.zfs/...` | ZFS snapshot 几乎免费给增量备份"加时间维度"。 ## 与商业方案对比 | | rsync + ZFS | Duplicati | restic to S3 | Veeam | |---|---|---|---|---| | 价格 | 免费 | 免费 | 免费 + 存储费 | 商业 | | 客户端加密 | 否(依靠传输 ssh) | ✅ | ✅ | ✅ | | 时间点恢复 | ✅(snapshot) | ✅ | ✅ | ✅ | | Web UI | 无 | 有 | 第三方 | ✅ | | 学习曲线 | 中 | 低 | 低 | 中 | 家用 / 小公司 + 有 ZFS NAS → 这套最便宜 + 最快。 ## 踩过的坑 1. **`sudo zfs snapshot` 没权限**:rsync 用户没 zfs allow。 `sudo zfs allow rsync-user create,destroy,snapshot ...`。 2. **snapshot 名字有冲突**:同一秒两次备份生成同名 snapshot 失败。 用毫秒级或随机 suffix。 3. **`--delete` 删了重要文件**:业务机误删 → rsync 同步删 → 但 ZFS snapshot 保住前一刻状态。**snapshot 是真正救命**。 4. **未控制保留 → snapshot 爆**:1 年 = 365 个 daily snapshot, 每 snapshot 元数据几 MB → 几 GB 元数据。 sanoid 策略限制数量。 5. **NAS 空间满 → 客户端 rsync 失败**:监控 ZFS pool 利用率, 超过 80% 告警。