知识广场
按学科筛选:计算机科学
«计算机科学» 分类下共 256 篇帖子
## 起因 要给 React 项目快速搭出"看着像样"的 UI。选择: - Material UI / Chakra:大、深度定制难、bundle 大 - Ant Design:大且企业风、设计语言强 - Headless UI / Radix:无样式、要自己写 CSS shadcn/ui 是 2023 后流行的新范式:**不是 npm 包**,而是 "复制粘贴 组件源码进你的项目"。底层用 Radix Primitives 做 a11y / 行为, Tailwind 做样式。你拥有源码,随便改。 ## 解决方案 ### 装 前提:React + Tailwind CSS 项目。 ```bash npx shadcn-ui@latest init # 交互式问几个问题: # Style: default / new-york # Base color: slate / gray / zinc / ... # CSS variables: yes ``` 生成 `components.json` 配置。 ### 加组件 ```bash npx shadcn-ui@latest add button npx shadcn-ui@latest add dialog npx shadcn-ui@latest add toast ``` 每个组件作为 `.tsx` 文件被复制到 `src/components/ui/`: ``` src/components/ui/ ├── button.tsx ├── dialog.tsx ├── toast.tsx └── ... ``` 打开 `button.tsx`: ```tsx import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium ...", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent", ghost: "hover:bg-accent", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default" }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ) } ) Button.displayName = "Button" export { Button, buttonVariants } ``` 这是**你**的代码。要加一个 `success` variant 直接改就行: ```tsx variant: { default: "...", success: "bg-green-600 text-white hover:bg-green-700", ... }, ``` 不像传统组件库要 fork 整个 repo 或者写 wrap。 ### 用 ```tsx import { Button } from '@/components/ui/button' <Button>Click me</Button> <Button variant="outline" size="lg">Outline</Button> <Button variant="success">Saved</Button> ``` ### Dialog 例 ```tsx import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' <Dialog> <DialogTrigger asChild> <Button variant="destructive">删除</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>确认删除</DialogTitle> </DialogHeader> <p>真的要删除这条记录吗?</p> <DialogFooter> <Button variant="outline">取消</Button> <Button variant="destructive">确认</Button> </DialogFooter> </DialogContent> </Dialog> ``` 底层 Radix Dialog 已经处理好:focus trap、ESC 关闭、aria-modal、 overlay 点击关闭、滚动锁等所有 a11y 细节。 ### 配色:CSS variables `globals.css`: ```css @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; /* ... */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... */ } } ``` 改这些变量 → 整个组件库换色。dark mode `<html class="dark">` 自动切。 ### theming:从 [ui.shadcn.com/themes](https://ui.shadcn.com/themes) 抄 shadcn 官网有 themes 页面,挑色调 + 复制 CSS 变量 → 替换你的 globals.css。 3 秒换主题。 ### 组件清单 shadcn 现有 50+ 组件: - Button / Input / Label / Textarea / Select / Switch / Checkbox / Radio - Dialog / Sheet / Popover / Tooltip / HoverCard / ContextMenu / DropdownMenu / Menubar / NavigationMenu / Command (cmd-k palette) - Card / Badge / Avatar / Separator / Skeleton / Progress - Toast / Alert / AlertDialog - Table / DataTable - Tabs / Accordion / Collapsible - Calendar / DatePicker - Form (react-hook-form 集成) - Carousel / Resizable / Drawer 涵盖 95% web app 需求。 ### 数据表 + sorting + filter(DataTable) ```tsx import { useReactTable } from '@tanstack/react-table' // shadcn 用 TanStack Table 做底层 const table = useReactTable({ data: users, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), }) // 渲染部分 shadcn 提供的 Table 组件包 ``` 复杂表格 < 100 行 + 完全可定制。 ## 与传统组件库对比 | | shadcn/ui | MUI v5 | Ant Design | |---|---|---|---| | 引入 | 复制源码 | npm i | npm i | | 改样式 | 直接改 .tsx | sx prop / theme override | 复杂 token | | Bundle | 仅用到的组件 | 大(tree-shaking 不彻底) | 极大 | | Tailwind | ✅ 原生 | 需适配 | 需适配 | | 升级 | 自己控制 | 升级有 breaking risk | 升级有 breaking risk | | 设计风格 | 默认 minimal | Material | 企业风 | shadcn 的"复制源码"哲学是关键差异: - **优点**:完全掌控、零依赖、bundle 最小、修改简单 - **缺点**:组件库更新不会自动应用(要重新 `add` 覆盖) ## 效果 我们一个内部工具用 shadcn 重做,对比之前的 Material UI 版: - bundle 从 850 KB → 280 KB - 设计师对样式调整一句话就能改(不需要"懂 MUI theme") - 暗色模式切换 0 配置 - 新人 onboarding 时间减半(组件就在自己代码里,IDE 跳定义直接看) ## 与同生态组合 - **next-themes**:dark mode 切换 - **react-hook-form** + **zod**:表单(shadcn Form 组件直接集成) - **TanStack Query**:数据 - **TanStack Table**:表格 - **lucide-react**:icon - **sonner**:toast 替代品(更现代) 一套组合下来基本 cover 现代 React app 全部需求。 ## 踩过的坑 1. **第一次 init 必须有 Tailwind**:shadcn 强依赖 Tailwind utility classes。 非 Tailwind 项目要么先装 Tailwind,要么用 [tremor](https://tremor.so/) 等替代。 2. **`npx shadcn-ui add` 覆盖修改**:再次 add 同名组件会覆盖你改过的 版本。要么不再 add(自己维护),要么 git 改动检查。 3. **`asChild` 模式坑**:`<Button asChild><a href="/">link</a></Button>` 把样式应用到 child 上。新人不知道这个 pattern 时疑惑。 Radix Slot 的设计,多看几次就熟。 4. **CSS 变量值是 HSL 三段值不是颜色字符串**:`--primary: 220 90% 56%` 不是 `--primary: hsl(220, 90%, 56%)`。tailwind config 用 `hsl(var(--primary) / <alpha-value>)` 拼。 5. **monorepo 共享 components/ui**:复制到每个 app 都改 = 重复劳动。 抽到 `packages/ui` 共享,但 shadcn 自动 add 路径不知道。 手动维护 import 路径或写自己的 CLI wrapper。
## 起因 要做一个"自然语言查数据库"的功能。用户问"上周日北京下单的用户有几个?" → LLM 生成 SQL → 后端执行 → 返结果。 最原始做法是让 LLM 生成 SQL 字符串然后 regex 提取。痛点: - 模型有时输出 ```sql ... ``` markdown 包裹 - 有时多输出一段"分析这个 SQL..." 散文 - 有时 SQL 语法错(缺逗号、wrong table) - parse 失败要 try/except 重试 `function calling`(OpenAI 起的名,Anthropic 叫 tool use)让模型 **直接结构化输出"调用什么函数 + 什么参数"**,零解析。 ## 解决方案:tool calling ### 定义工具 ```python from openai import OpenAI client = OpenAI() tools = [ { 'type': 'function', 'function': { 'name': 'run_sql', 'description': '在分析数据库上执行 SELECT SQL,返回最多 50 行。' '只允许 SELECT;DELETE/UPDATE/INSERT 会被拒绝。', 'parameters': { 'type': 'object', 'properties': { 'sql': { 'type': 'string', 'description': 'PostgreSQL 标准 SQL,必须以 SELECT 开头', }, 'explanation': { 'type': 'string', 'description': '一句话解释这个 SQL 在做什么', }, }, 'required': ['sql', 'explanation'], }, }, }, { 'type': 'function', 'function': { 'name': 'list_tables', 'description': '列出可用表名', 'parameters': {'type': 'object', 'properties': {}, 'required': []}, }, }, ] system_prompt = """ 你是一个数据分析助手。回答用户问题前,可能需要: 1. 用 list_tables 看有哪些表 2. 用 run_sql 查数据 数据 schema: - users(id, email, country, created_at, plan) - orders(id, user_id, amount, city, ordered_at, status) - products(id, name, category, price) 回答用户用中文。 """ def chat(messages): return client.chat.completions.create( model='gpt-4o', messages=messages, tools=tools, ) ``` ### 调用循环 ```python def run(user_question: str): messages = [ {'role': 'system', 'content': system_prompt}, {'role': 'user', 'content': user_question}, ] while True: resp = chat(messages) msg = resp.choices[0].message # 模型决定调函数 if msg.tool_calls: messages.append(msg) # assistant turn for tc in msg.tool_calls: result = dispatch(tc.function.name, json.loads(tc.function.arguments)) messages.append({ 'role': 'tool', 'tool_call_id': tc.id, 'content': json.dumps(result), }) # 继续下一轮,让模型看 tool 结果再决定 continue # 模型给最终答案 return msg.content ``` ### 实现 dispatch(真的执行 SQL) ```python import psycopg def dispatch(name, args): if name == 'list_tables': return list_tables() if name == 'run_sql': return run_sql(args['sql']) return {'error': f'unknown function: {name}'} def list_tables(): with psycopg.connect(DB_URL) as conn: cur = conn.execute(""" SELECT table_name, obj_description(...) FROM information_schema.tables WHERE table_schema='public' """) return [{'table': r[0], 'desc': r[1]} for r in cur.fetchall()] def run_sql(sql: str): if not sql.strip().upper().startswith('SELECT'): return {'error': 'only SELECT allowed'} try: with psycopg.connect(DB_URL, autocommit=False) as conn: conn.execute('SET statement_timeout=10000') # 10s 限时 cur = conn.execute(sql) rows = [dict(zip([c[0] for c in cur.description], r)) for r in cur.fetchmany(50)] return {'rows': rows, 'count': len(rows)} except Exception as e: return {'error': str(e)} ``` ### 跑一下 ```python print(run('上周日北京下单的用户有几个?')) # 输出: # 上周日(2024-05-19)在北京下单的用户共 47 个。 ``` 模型自动: 1. 调 `list_tables()` 看有哪些 2. 调 `run_sql('SELECT COUNT(DISTINCT user_id) FROM orders WHERE city=...')` 3. 拿到结果后用自然语言回答 整套流程**无字符串解析**——arguments 已经是 typed JSON。 ## 几个重要细节 ### 1. 多 tool 同时调 ```python if msg.tool_calls: # 可能一次调 2-3 个函数 for tc in msg.tool_calls: ... ``` 模型可能并行调 list_tables + run_sql。要 loop 处理所有。 ### 2. 防恶意 SQL 工具签名再严格也挡不住"DROP TABLE users; --" 写在 SQL 字符串里。 在 dispatch 层做实际验证: - 只允许 SELECT 开头 - 用只读 DB 用户(无 DDL / DML 权限) - `SET statement_timeout=N` 限时长 - ACL 限 schema / table 访问 - 用 SQLAlchemy `text(sql)` + 参数化(更难做) 或者更激进:sandbox 跑(DuckDB on read-only data copy)。 ### 3. 限制循环次数 模型可能死循环调工具。限步数: ```python for step in range(10): resp = chat(messages) if not msg.tool_calls: return msg.content # ... raise RuntimeError('exceeded 10 tool-use steps') ``` ### 4. parallel tool call OpenAI 默认开 parallel;要禁用: ```python resp = client.chat.completions.create(..., parallel_tool_calls=False) ``` 复杂任务有 dependency 时禁用更稳。 ### 5. tool_choice 强制 ```python client.chat.completions.create( ..., tool_choice={'type': 'function', 'function': {'name': 'run_sql'}}, ) ``` 强制本轮调某个函数(不让模型 freestyle 直接答)。 ## Anthropic / Gemini / Ollama 也支持 API 风格略不同但概念一致。 ```python # Anthropic import anthropic client = anthropic.Anthropic() resp = client.messages.create( model='claude-sonnet-4-5', max_tokens=1024, tools=[{'name': 'run_sql', 'description': '...', 'input_schema': {...}}], messages=[{'role': 'user', 'content': '...'}], ) # stop_reason='tool_use' 时遍历 content blocks ``` ```python # Ollama (qwen2.5 等支持 function calling) import ollama resp = ollama.chat( model='qwen2.5:7b', messages=[...], tools=[{'type': 'function', 'function': {...}}], ) ``` 跨家 LLM 工具调用接口已经形成事实标准(OpenAI 格式被大部分模仿)。 ## 实际应用场景 1. **数据库 query agent**(上面例子) 2. **代码 review bot**:tool 是 read_file / list_files / run_tests 3. **客服 agent**:lookup_order / refund / escalate 4. **DevOps agent**:check_deployment / rollback / fetch_logs 5. **RAG with citations**:search_docs / fetch_doc tool 任何"模型需要查外部信息再回答" 都适合。 ## 与 LangChain / LlamaIndex 的关系 LangChain `create_react_agent` / `create_tool_calling_agent` 是上面 循环的封装: ```python from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_openai import ChatOpenAI from langchain.tools import tool @tool def run_sql(sql: str) -> str: """Execute SELECT SQL, return rows.""" return run_sql_impl(sql) llm = ChatOpenAI(model='gpt-4o') agent = create_tool_calling_agent(llm, [run_sql], prompt) executor = AgentExecutor(agent=agent, tools=[run_sql], max_iterations=10) executor.invoke({'input': '上周日北京...'}) ``` 封装方便但藏了细节。简单场景手写循环更可控;复杂 agent 用 LangChain 省事。 ## 效果 我们的 SQL agent 上线后: - 业务团队不再用 Metabase 拖拽,直接问中文 - "为什么这个客户流失了" 类自由查询能 90% 准确给出 SQL + 结果 - function calling 解析失败率:0%(结构化 output) - vs 之前用 regex 提取 SQL 字符串:~12% 失败要重试 ## 踩过的坑 1. **参数 schema 错**:模型按 schema 生成参数,schema 不严会乱传。 `required` / `enum` / `type` 都明确写。 2. **大 tool 数 → 模型 confused**:超过 ~10 个 tool 后模型选错率 上升。分层:top-level "router" → 选 sub-agent → sub-agent 有 3-5 个 tool。 3. **tool 实现 crash 抛异常**:返 `{'error': str(e)}` 让模型看见, 模型会 retry / 换策略。直接 raise 就只能上层 catch。 4. **token 成本**:tool description + schema 占 system prompt 不少 token。每次请求都付。优化:精简 description / 用 short tool name。 5. **流式(streaming)+ tool**:streaming response 中 tool_call chunks 是分段的,要 buffer 后再 parse。复杂场景非流式更稳。
TypeScript 的标准库带了几十个 utility type。熟练用能少写一半重复定义。 下面只挑实战频率最高的 20+ 个 + 5 个我自己常造的。 ## 内置常用 ### `Partial<T>` / `Required<T>` ```ts interface User { id: string; name: string; email: string } // 所有字段可选(PATCH 接口最常用) function update(id: string, patch: Partial<User>) {} update('1', { name: 'Alice' }) // OK // 所有字段必选 type FullUser = Required<Partial<User>> ``` ### `Pick<T, K>` / `Omit<T, K>` ```ts type UserPublic = Pick<User, 'id' | 'name'> // 只要这些字段 type UserPrivate = Omit<User, 'email' | 'password'> // 去掉这些字段 ``` ### `Record<K, V>` ```ts type StatusMap = Record<'pending' | 'done' | 'failed', number> // { pending: number; done: number; failed: number } type Cache<T> = Record<string, T> // 字典 ``` ### `Readonly<T>` / `ReadonlyArray<T>` ```ts function process(items: ReadonlyArray<string>) { items.push('x') // ❌ 编译错 } ``` ### `ReturnType<F>` / `Parameters<F>` ```ts function fetchUser(id: string): Promise<User> { ... } type UserResp = Awaited<ReturnType<typeof fetchUser>> // User type Args = Parameters<typeof fetchUser> // [string] ``` `Awaited<T>` 解 Promise 包装。 ### `Exclude<T, U>` / `Extract<T, U>` ```ts type Color = 'red' | 'green' | 'blue' | 'transparent' type Opaque = Exclude<Color, 'transparent'> // 'red' | 'green' | 'blue' type Warm = Extract<Color, 'red' | 'orange'> // 'red' ``` ### `NonNullable<T>` ```ts type StringOrNull = string | null | undefined type DefinitelyString = NonNullable<StringOrNull> // string ``` ### `Awaited<T>` ```ts type A = Awaited<Promise<Promise<number>>> // number ``` 递归解包 Promise。 ## 自定义常用 ### `DeepPartial<T>` — 递归 Partial ```ts type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T type Config = { a: { b: { c: number } } } type PartialConfig = DeepPartial<Config> // { a?: { b?: { c?: number } } } ``` 适合 patch 嵌套对象。 ### `DeepReadonly<T>` ```ts type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T ``` ### `PickRequired<T, K>` — 指定字段必选,其它保持 ```ts type PickRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] } interface Opts { name?: string; age?: number; email?: string } type WithName = PickRequired<Opts, 'name'> // { name: string; age?: number; email?: string } ``` `-?` 是 "去掉 optional 修饰"。 ### `Branded<T, B>` — 名义类型(防混淆) ```ts type Branded<T, B extends string> = T & { __brand: B } type UserId = Branded<string, 'UserId'> type PostId = Branded<string, 'PostId'> function getUser(id: UserId): User { ... } const uid = 'u1' as UserId const pid = 'p1' as PostId getUser(uid) // OK getUser(pid) // ❌ Type 'PostId' is not assignable to 'UserId' ``` 让基础类型(string / number)按业务含义区分,编译期阻止误用。 ### `ValueOf<T>` ```ts type ValueOf<T> = T[keyof T] const Status = { Active: 'active', Inactive: 'inactive' } as const type StatusVal = ValueOf<typeof Status> // 'active' | 'inactive' ``` ### `Mutable<T>` — 反向 Readonly ```ts type Mutable<T> = { -readonly [K in keyof T]: T[K] } ``` ### `XOR<T, U>` — 两选一不能同时 ```ts type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never } type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T) type Login = XOR<{ email: string }, { phone: string }> // 必须传 email 或 phone,不能两个都传 ``` ## 高级:条件类型 + infer ```ts // 提取数组元素类型 type ArrayElement<T> = T extends (infer E)[] ? E : never type Item = ArrayElement<string[]> // string // 提取函数返回类型(重新实现 ReturnType) type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never // 提取 Promise 解析后类型(重新实现 Awaited) type Unpromise<T> = T extends Promise<infer V> ? V : T ``` `infer` 让你在条件类型里"声明并捕获"一个类型变量。 ## 字符串字面量类型 ```ts type CSSVar<S extends string> = `--${S}` type X = CSSVar<'primary'> // "--primary" type ToCamelCase<S extends string> = S extends `${infer A}-${infer B}${infer Rest}` ? `${A}${Uppercase<B>}${ToCamelCase<Rest>}` : S type C = ToCamelCase<'foo-bar-baz'> // "fooBarBaz" ``` 模板字面量类型 + infer 让 TS 能做一些"字符串编程"。但别滥用—— 复杂的会让编辑器卡 + 错误信息难懂。 ## 类型守卫 ```ts function isError(x: unknown): x is Error { return x instanceof Error } try { ... } catch (e) { if (isError(e)) { console.error(e.message) // TS 知道 e 是 Error } } ``` `x is Type` 让 TS 在 if 分支里 narrow 类型。 ## type vs interface 何时选 - 简单对象 / 可被扩展(库提供的):interface - 联合 / 交叉 / 字面量 / 复杂条件:type - 不知道选哪个:type(更通用) 性能上几乎无差。 ## 工具:type-fest [type-fest](https://github.com/sindresorhus/type-fest) 收集了 100+ 实用 utility type: ```bash npm i type-fest ``` ```ts import type { SetRequired, JsonValue, CamelCase } from 'type-fest' type User = { name?: string; age?: number } type WithName = SetRequired<User, 'name'> ``` 不要每个项目都自己实现 `DeepPartial`,引这个库直接用。 ## 踩过的坑 - `Partial<DeepFoo>` 不递归。要 `DeepPartial<...>`。 - `Exclude<T, U>` 看似"减去",但 U 必须是 T 的子集;不是的话什么都不去。 - `as Type` 强制断言不安全(编译器信你的)。能用类型守卫就用。 - 复杂 conditional type 报错时 TS 错误信息可达几十行。把复杂 type 拆 成中间命名 type,错误位置更清楚。
## 起因 新部署的 nginx 默认配置在中等流量下出现: - P99 偶尔 200ms+ 突刺 - `connection reset` 客户端错误 - 上游 PHP-FPM / gunicorn `connection timeout` 不调优 → 撑 1k QPS 就开始 wobble。调几个 key 参数后 5k QPS 稳。 下面是我每个新部署都套的 baseline。 ## worker_processes / worker_connections ```nginx worker_processes auto; # = CPU 核心数 worker_rlimit_nofile 65535; # 文件描述符上限 events { worker_connections 8192; # 单 worker 最大连接 use epoll; # Linux 用 epoll multi_accept on; # 一次接多 connection } ``` `worker_processes auto` 让 nginx 自动用所有核。 `worker_connections × worker_processes` = 理论最大并发连接。 8 核 × 8192 = 65536 并发,远超 1k QPS 需求。 ## upstream keepalive(关键) 默认 nginx 反代每请求开新 TCP 连接到 upstream → TCP/TLS 握手成本。 ```nginx upstream backend { server 127.0.0.1:8000; server 127.0.0.1:8001; keepalive 64; # 与 upstream 保持 64 个空闲连接 keepalive_timeout 60s; keepalive_requests 1000; # 每连接最多 1000 请求后重建 } server { location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; # 必须!不传 Connection close } } ``` `proxy_set_header Connection ""` 这行最容易漏。 缺它 → upstream 默认 HTTP/1.0 不复用 → keepalive 不生效。 加上后 latency 降 30-50%(省 TCP 握手 + 不必要的 connection setup)。 ## buffer / timeout ```nginx # 请求 buffer client_max_body_size 20m; client_body_buffer_size 16k; client_header_buffer_size 4k; large_client_header_buffers 4 16k; # upstream buffer proxy_buffering on; proxy_buffer_size 8k; proxy_buffers 8 16k; proxy_busy_buffers_size 16k; # timeout proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; send_timeout 60s; ``` `proxy_buffering on`(默认):nginx 先收完 response 再发给 client → 慢 client 不拖累 upstream。 特殊场景关闭(如 SSE / streaming): ```nginx location /sse/ { proxy_pass http://backend; proxy_buffering off; # streaming 不缓冲 proxy_cache off; } ``` ## gzip ```nginx gzip on; gzip_vary on; gzip_min_length 1024; gzip_proxied any; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; ``` JSON / HTML 压缩 70-90%。 `gzip_comp_level 5` 在 CPU 和压缩比间平衡(1 - 9 范围)。 ## brotli (更好压缩) ```nginx brotli on; brotli_comp_level 6; brotli_types text/plain text/css application/json application/javascript; ``` 需要 brotli module 装(一些 distro 默认带 `nginx-extras` package)。 比 gzip 压缩比好 15-20%,CPU 占用稍高。 ## 静态文件 sendfile / tcp_nopush ```nginx sendfile on; # 内核态文件直接 send tcp_nopush on; # 凑满 packet 再发(搭配 sendfile) tcp_nodelay on; # 长连接 small payload 不延迟 # 静态文件 cache location /static/ { expires 30d; add_header Cache-Control "public, immutable"; access_log off; } ``` `sendfile` 是 zero-copy → 大文件传输 CPU 占用 1/4。 ## TLS 优化 ```nginx ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; # 10 MB session cache,约 40000 session ssl_session_timeout 1d; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; # HTTP/2 listen 443 ssl http2; # HTTP/3 (nginx 1.25+) listen 443 quic reuseport; add_header Alt-Svc 'h3=":443"; ma=86400'; ``` session cache 让 TLS handshake 从 2 RTT → 0 RTT(resumption)。 高 QPS 时显著省 CPU。 ## rate limit 防 abuse: ```nginx http { limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s; limit_conn_zone $binary_remote_addr zone=conn:10m; } server { location /api/ { limit_req zone=api burst=200 nodelay; limit_conn conn 20; proxy_pass http://backend; } } ``` 每 IP 100 req/s + burst 200 + 同时 ≤ 20 connection。 基础防护,挡 script kiddie。 ## log 格式 + sampling ```nginx log_format main_json escape=json '{' '"time":"$time_iso8601",' '"remote":"$remote_addr",' '"method":"$request_method",' '"uri":"$request_uri",' '"status":$status,' '"bytes":$body_bytes_sent,' '"rt":$request_time,' '"upstream_rt":"$upstream_response_time",' '"ua":"$http_user_agent"' '}'; # 大量流量时只 log 一部分(health check / static) map $request_uri $loggable { default 1; /health 0; ~*\.ico 0; } access_log /var/log/nginx/access.log main_json if=$loggable; ``` JSON 格式 → ELK / Loki / Datadog 解析友好。 ## 实战调优 case 某 Django app,单 server,4 vCPU / 8 GB: 调前: - 1000 QPS 撑得住但 P99 300ms - upstream connection 数 4000+ 频繁 TIME_WAIT - 偶发 502 调后(上面 baseline): - 3000 QPS 稳定,P99 80ms - upstream connection 200(keepalive 复用) - 502 几乎绝迹 关键改动: 1. `upstream keepalive 64` + `Connection ""` header 2. worker_processes auto + worker_connections 8192 3. gzip on + brotli on ## 监控 `/nginx_status` (stub_status module): ```nginx location /nginx_status { stub_status; allow 127.0.0.1; deny all; } ``` prometheus exporter(nginx-vts / nginx-prometheus-exporter)抓 → Grafana panel: - active connections - accepted / handled / requests rate - per-status code rate - upstream response time 发现 spike / leak 第一时间。 ## 高级:worker_cpu_affinity ```nginx worker_processes 4; worker_cpu_affinity 0001 0010 0100 1000; ``` 绑 worker 到特定 CPU → cache locality 提升 5-10%。 8 核以下机器一般不必要;多核高压才显著。 ## 踩过的坑 1. **`Connection ""` 漏了**:upstream keepalive 无效 → 性能没提升却 以为配了。`tcpdump` / netstat 看 connection 数确认。 2. **worker_connections 高但 ulimit 低**:worker_rlimit_nofile 也要 配,否则 syscall fail。`ulimit -n` 检查。 3. **proxy_buffer_size 小**:上游 response header 大(cookie 多)→ `upstream sent too big header` 错误。调大到 16k+。 4. **server_tokens on**:默认显 nginx 版本在 header / error page → 信息泄。`server_tokens off`。 5. **reload 不等于 restart**:`nginx -s reload` 优雅 reload 是好习惯; `restart` 断现有连接。CI deploy 用 reload。
## 起因 Django 4.0+ 支持 async views,文档里说"并发 IO 性能提升"。 直接把所有 view 改 `async def`?踩了几个坑: - ORM 默认是同步的,async view 里 `User.objects.get()` 会 block event loop - middleware 不全 async,会有性能 penalty - 不是所有场景都受益 理解什么时候用、什么时候别用,才有价值。 ## 解决方案:分场景 ### 场景 A:view 里发 N 个外部 HTTP 请求(async 真的快) ```python # 同步版(用 requests) def aggregate_view(request): weather = requests.get('https://api.weather/...').json() news = requests.get('https://api.news/...').json() stocks = requests.get('https://api.stocks/...').json() return JsonResponse({'weather': weather, 'news': news, 'stocks': stocks}) ``` 3 个串行 IO,每个 300ms → 总 900ms。 ```python # async 版 import httpx async def aggregate_view(request): async with httpx.AsyncClient() as client: weather, news, stocks = await asyncio.gather( client.get('https://api.weather/...'), client.get('https://api.news/...'), client.get('https://api.stocks/...'), ) return JsonResponse({ 'weather': weather.json(), 'news': news.json(), 'stocks': stocks.json(), }) ``` 3 个并行,总 300ms(max)。3x 加速。 ### 场景 B:view 主要查 ORM(async 没用) ```python async def post_list(request): posts = Post.objects.filter(visibility='public')[:20] # ❌ return ... ``` Django ORM 默认同步。在 async view 里调用同步 ORM → 内部跑 `sync_to_async` 用线程池执行 → 比纯同步 view 还慢一点(多一次 context switch)。 Django 4.1+ 加了 async ORM: ```python async def post_list(request): posts = [p async for p in Post.objects.filter(visibility='public')[:20]] # 或: posts = await Post.objects.filter(...).a_in_bulk([1, 2, 3]) post = await Post.objects.aget(pk=1) await post.adelete() return ... ``` `a*` 系列方法是 async 版。但要点: - 这不让你"并发查多条"——还是单连接顺序查 - 唯一收益:不 block event loop(同进程能服务其它 async 请求) - 高 QPS + 复杂查询:Django ORM 仍是性能瓶颈(不是 async 能解的) ### 场景 C:streaming response(async 是必须的) ```python async def chat_stream(request): async def generator(): async for chunk in llm.stream(prompt): yield f'data: {json.dumps(chunk)}\n\n' return StreamingHttpResponse( generator(), content_type='text/event-stream', ) ``` LLM streaming / 长 polling / SSE → 必须 async 才能正常工作。 同步 view 在 stream 完成前 block 一个 worker。 ### 场景 D:mixed sync + async 需要在 async view 里调同步代码(如某个老 lib): ```python from asgiref.sync import sync_to_async @sync_to_async def heavy_sync_work(x): return cpu_bound_calc(x) async def view(request): result = await heavy_sync_work(42) return JsonResponse({'result': result}) ``` `sync_to_async` 把同步函数包成 awaitable,在线程池跑。 反向 `async_to_sync` 让 sync 调 async。 ## 启动方式 async views 需要 ASGI server(不是 WSGI): ```bash # 之前:gunicorn (WSGI) gunicorn myapp.wsgi # 现在:uvicorn (ASGI) uvicorn myapp.asgi:application --workers 4 # 或 gunicorn + uvicorn worker gunicorn -k uvicorn.workers.UvicornWorker -w 4 myapp.asgi:application ``` `myapp/asgi.py` 默认 Django 已生成。 WSGI server 跑 async view 也能跑(Django 自动用 sync_to_async 适配), 但性能不如 ASGI。 ## 性能测试(实际数据) 我对一个 endpoint 测试:3 个外部 API + 1 个 DB 查询。 | | wrk -t8 -c100 -d30s RPS | P95 latency | |---|---|---| | 同步 gunicorn(4 worker) | 35 | 2.9s | | async uvicorn(4 worker) | 320 | 380ms | 外部 IO 密集场景 async 收益巨大。 但纯 DB 查询 endpoint: | | RPS | P95 | |---|---|---| | 同步 gunicorn | 1200 | 80ms | | async uvicorn | 1100 | 90ms | async 反而略慢(额外的协程开销 + ORM 仍同步)。 ## 什么场景该用 async ✅ 适合: - 外部 API / webhook 调用密集 - SSE / streaming response - WebSocket (Django Channels) - 长 polling - AI / LLM 调用 ❌ 不适合: - 纯 CRUD(ORM 同步主导) - CPU 密集(GIL,async 没用) - 老 lib 没 async 版本 ## 实战:把现有 view 改 async 的流程 1. 评估:这 view 主要 IO 类型是什么?外部 HTTP > DB 查询 > 其它 → 值得改 2. 安装 ASGI server (uvicorn) 3. 把 view 函数 `def` → `async def` 4. 把 `requests` 换 `httpx` / `aiohttp` 5. 把 ORM 调用换 `aget` / `acreate` / async iter(如果用得到) 6. middleware:检查是否 async-aware,老 middleware 加 `async_capable = True` 7. 测试 + 性能 benchmark 对比 ## Channels(WebSocket) 如果要 WebSocket / SSE 大量并发,Django Channels 是标准: ```bash pip install channels channels-redis ``` ```python # routing.py from channels.routing import ProtocolTypeRouter, URLRouter from chat.consumers import ChatConsumer application = ProtocolTypeRouter({ 'websocket': URLRouter([ path('ws/chat/', ChatConsumer.as_asgi()), ]), }) ``` ```python # consumers.py from channels.generic.websocket import AsyncWebsocketConsumer class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() async def receive(self, text_data): await self.send(text_data=f'echo: {text_data}') ``` Channels 让 Django 处理 WebSocket / SSE / HTTP / Channel layer (跨 worker 消息)全面 async。 ## 效果 我们一个 API gateway 类服务(聚合多个内部 service 数据)改 async: - P95 延迟从 1.2s → 230ms - 同硬件下 RPS 从 200 → 1500 - worker 数 from 16 减到 4(每个 worker 并发服务) - 内存占用减半 而我们的 CRUD 类 admin backend 改了一半发现没什么用,回滚保持同步。 ## 踩过的坑 1. **在 async view 里调同步 view function**:直接调会 block。 要 `await sync_to_async(other_view)(request)`。 2. **middleware 不 async**:所有 middleware 必须 async-compatible, 否则 Django 退化到 sync 模式。第三方 middleware 检查文档支持 async。 3. **DB connection pool**:async views 处理并发更高 → 同时打开的 DB connection 多 → DB 连接耗尽。配 PgBouncer 在前面。 4. **`@login_required` 等装饰器**:检查是否 async-compatible。 Django 4.1+ 内置装饰器都改了;第三方需要确认。 5. **测试**:`AsyncClient` 替代 `Client`: ```python async def test_view(): client = AsyncClient() response = await client.get('/api/...') ```
## 起因 打开项目 X 的工作流总是:开 tmux session → split 3 panel → 各跑 `vim .` / `npm run dev` / `tail -f log`。 切到项目 Y 又是另一套布局。每次手动布几次烦。 `tmuxinator`(YAML 定义 tmux layout)和 `zellij`(自带 layout 系统) 让"项目 → 预设布局" 一行命令搞定。 ## tmuxinator(tmux 上层) ```bash gem install tmuxinator # 或:brew install tmuxinator tmuxinator new myapp # 打开 YAML 编辑器 ``` ```yaml # ~/.config/tmuxinator/myapp.yml name: myapp root: ~/projects/myapp windows: - editor: layout: main-vertical panes: - nvim . - git status - tail -f log/dev.log - server: panes: - npm run dev - npm run watch:css - db: panes: - psql -d myapp - shell: panes: - clear ``` 启动: ```bash tmuxinator start myapp # 或 mux start myapp(短 alias) ``` 自动: 1. 开新 tmux session `myapp` 2. 第 1 个 window 'editor':左侧 nvim,右侧上 git status / 下 log tail 3. 第 2 个 window 'server':上下分 panel 跑 dev + watch 4. 第 3 个 window 'db':psql 5. 第 4 个 window 'shell':空 shell 切 window:tmux prefix + 数字键。整个项目立刻 ready。 ### 高级:pre/post hook ```yaml pre: docker compose -f docker-compose.dev.yml up -d post_window_create: sleep 1 windows: - editor: panes: - nvim . ``` `pre` 在启动 tmux 前跑(启 DB / 装依赖)。`post_window_create` 每个 window 创建后 sleep。 ### 多 session 同时 ```bash tmuxinator start myapp # tmux session "myapp" 起来 tmuxinator start client-x # 另一个项目 # tmux session "client-x" 同时存在 ``` `tmux ls` 看所有 session;`tmux a -t myapp` 进任意一个。 ## zellij(tmux 替代品,layout 内置) ```bash brew install zellij cargo install zellij ``` ```bash zellij --layout default # 默认 layout 启动 ``` zellij 跟 tmux 比: - modern UI(status bar + tips 自动显示) - 鼠标支持开箱即用 - layout 是 KDL 格式(更结构化) - 内置插件系统(Wasm) - 性能(Rust) ### KDL layout `~/.config/zellij/layouts/myapp.kdl`: ``` layout { cwd "/Users/me/projects/myapp" tab name="editor" focus=true { pane command="nvim" { args "." } pane split_direction="vertical" size="40%" { pane command="git" { args "status" } pane command="bash" { args "-c" "tail -f log/dev.log" } } } tab name="server" { pane command="npm" { args "run" "dev" } pane command="npm" { args "run" "watch:css" } } tab name="db" { pane command="psql" { args "-d" "myapp" } } } ``` 启动: ```bash zellij --layout myapp # 或: zellij -s myapp -l myapp ``` `-s myapp` session 名;`-l myapp` 用这个 layout。 ### resurrect (持久化) ```bash zellij list-sessions zellij attach myapp # attach 重连,状态保留 zellij kill-session myapp ``` 机器重启后 session 消失(跟 tmux 一样)。 需要持久化用 tmux + tmux-resurrect 插件。 ## 我用的 workflow 混用: - daily 默认 shell 用 tmux + tmuxinator(成熟、稳) - 新项目尝试 zellij(更好看 / 鼠标好) 每个项目根目录有 `.tmuxinator.yml`: ```bash cd myapp mux . # 在当前目录跑 tmuxinator 配置 ``` 或者 cd 时自动启 tmux: ```bash # ~/.zshrc function cd() { builtin cd "$@" if [[ -f .tmuxinator.yml && -z "$TMUX" ]]; then local name=$(basename "$PWD") tmuxinator start "$name" -f .tmuxinator.yml -n "$name" 2>/dev/null fi } ``` cd 进项目自动开预设布局。 ## 与 IDE 的关系 VSCode 已经能 multi-panel + integrated terminal。 为什么还要 tmux/zellij? - **远程开发**:SSH 上 tmux session 断线不丢 - **CLI 主导**:vim/neovim/helix 用户原生体验 - **多 session**:可以跑跑 backend / frontend / docs 各一个 tmux 互不干扰 - **持久 background**:长跑任务用 tmux 后台跑,不占 IDE IDE 用户也常用 tmux 跑 background server,VSCode 只 edit + git。 ## 团队共享 layout `.tmuxinator.yml` 进 git → 团队成员 `mux .` 起来同样 layout。 对 onboarding 价值极大:新人不需要"我应该开哪几个 terminal"。 注意:layout 文件里硬编码用户 path(`/home/me/...`)要避免, 用 `${PWD}` 或 `.` 相对路径。 ## 效果 - 项目切换从"开 5 个 terminal 各布置 1 分钟" → 1 秒 - 远程 SSH 断线后 reattach 看到所有状态保留 - 团队新人 onboarding 直接 `mux .` 起来全套 - 一台机器 daily 同时挂 4-5 个项目 session,按需 attach 切换 ## 踩过的坑 1. **tmuxinator 不更新某个 pane 命令**:YAML 改了但 session 已经 起来 → 改动不生效。`tmuxinator kill myapp` 再 `start`。 2. **windows 排序**:YAML 列出顺序就是 tab 顺序。习惯把 editor 放第 1(默认 attach 进的)。 3. **session 同名只能一个**:第二次 start 提示已存在。`mux start myapp` 会 attach 已有的(feature);要重启加 `--no-attach` + kill 旧的。 4. **layout 文件 path 跨机器不一致**:用 `~/projects/...` 不要硬 `/home/specific-user/`。 5. **tmux 自动 attach 把别人踢出去**:多人共享 server 上同一 tmux 不友好。建议 `tmux a -t myapp -d` (`-d` detach 别人) 或者 read-only attach(`-r`)。
## 起因 每开一台新服务器都重复同样的"装好后第一件事"。下面这 14 项是我每次 都做的,从攻击者最常用入口 → 防御。 ## checklist ### 1. 关闭 root SSH ```bash sudo sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config sudo sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config sudo systemctl reload ssh ``` 只允许密钥 + 非 root 用户登录。改之前**确认**你有正常用户 + 密钥能登: ```bash # 新终端测试 ssh me@server # 必须能进 sudo whoami # 必须能 sudo ``` 否则改完会把自己锁外面。 ### 2. 创建非 root 用户 + 加 sudo ```bash sudo adduser me sudo usermod -aG sudo me # 拷贝你的 public key sudo mkdir -p /home/me/.ssh sudo cp ~/.ssh/authorized_keys /home/me/.ssh/ sudo chown -R me:me /home/me/.ssh sudo chmod 700 /home/me/.ssh sudo chmod 600 /home/me/.ssh/authorized_keys ``` ### 3. 启用 ufw 防火墙 ```bash sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow ssh # 22 或自定义端口 sudo ufw allow 80 sudo ufw allow 443 sudo ufw enable ``` ### 4. 启用自动安全更新 ```bash sudo apt install -y unattended-upgrades apt-listchanges sudo dpkg-reconfigure -priority=low unattended-upgrades ``` `/etc/apt/apt.conf.d/50unattended-upgrades` 改成只装 security: ``` Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; }; ``` ```bash sudo systemctl enable --now unattended-upgrades ``` ### 5. fail2ban 防暴力破解 ```bash sudo apt install -y fail2ban sudo systemctl enable --now fail2ban ``` `/etc/fail2ban/jail.local`: ```ini [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 ignoreip = 127.0.0.1/8 ::1 <你的家庭 IP> [sshd] enabled = true ``` ### 6. 改 SSH 端口(可选但有效) ```bash sudo sed -i 's/^#\?Port .*/Port 2200/' /etc/ssh/sshd_config sudo systemctl reload ssh sudo ufw allow 2200/tcp sudo ufw delete allow ssh ``` 不是真"安全提升",但 SSH 攻击 log 减少 99%(攻击脚本只扫 22)。 副作用:很多自动化工具默认 22,加 `~/.ssh/config` 写好端口。 ### 7. 装 SSL 证书(Let's Encrypt) ```bash sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com # 自动跑 cert + 续期 cron ``` 或用 Caddy(前面有篇)自动 HTTPS。 ### 8. systemd hardening 关键 service 主要的应用 service unit 加: ```ini [Service] User=appuser Group=appuser ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/log/myapp /var/lib/myapp PrivateTmp=true NoNewPrivileges=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX MemoryMax=2G TasksMax=128 ``` `systemd-analyze security myapp.service` 评分 + 提示。 ### 9. 限制 sudo 命令 `/etc/sudoers.d/me`: ``` me ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myapp ``` 只允许特定命令免密。其它 sudo 仍要密码。CI / deploy 服务账号 强烈推荐。 ### 10. 配置 fail-fast 日志清理 ```bash # /etc/systemd/journald.conf SystemMaxUse=2G SystemMaxFileSize=200M MaxRetentionSec=1month ``` ```bash sudo systemctl restart systemd-journald ``` 防止日志失控撑爆磁盘。 ### 11. 装 logwatch / 邮件总结 ```bash sudo apt install -y logwatch echo 'Mailto: [email protected]' | sudo tee -a /etc/logwatch/conf/logwatch.conf ``` 每天自动邮件汇总:sudo 用法 / SSH 登录 / cron 跑了什么 / 错误 log。 看一眼就知道异常。 ### 12. 内核加固 sysctl `/etc/sysctl.d/99-security.conf`: ``` # 防止 IP spoofing net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 # Ignore ICMP broadcast net.ipv4.icmp_echo_ignore_broadcasts = 1 # 防 SYN flood net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 8192 # 不接 IPv4 / v6 source routing net.ipv4.conf.all.accept_source_route = 0 net.ipv6.conf.all.accept_source_route = 0 # 不发 ICMP redirect net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 # kernel hardening kernel.dmesg_restrict = 1 kernel.kptr_restrict = 2 fs.protected_hardlinks = 1 fs.protected_symlinks = 1 fs.protected_fifos = 2 ``` ```bash sudo sysctl --system ``` ### 13. 监控登录 + 命令历史 ```bash # /etc/profile.d/audit-history.sh export HISTTIMEFORMAT="%F %T " export HISTSIZE=10000 export HISTFILESIZE=20000 shopt -s histappend PROMPT_COMMAND='history -a' ``` 让所有 user shell history 带时间戳 + 跨 session 共享。 事后审计时知道"谁在什么时候跑了什么"。 进阶:装 auditd 记录 syscall 级别。 ### 14. 备份 + 远程 按前面 borg / restic / kopia 配,备份到远程。 被勒索 → 还能拉回来。 ## 一键脚本 把上面集成成一个 init 脚本: ```bash #!/bin/bash # /usr/local/sbin/server-init.sh set -euo pipefail # 1. 包更新 apt update && apt upgrade -y # 2. 装基础工具 apt install -y \ ufw fail2ban unattended-upgrades apt-listchanges \ logwatch htop vim tmux git curl jq ncdu \ chrony # 3. ufw ufw default deny incoming ufw default allow outgoing ufw allow 22 ufw allow 80 ufw allow 443 ufw --force enable # 4. ssh 加固 sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl reload ssh # 5. fail2ban cat > /etc/fail2ban/jail.local <<'EOF' [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 [sshd] enabled = true EOF systemctl enable --now fail2ban # 6. sysctl 加固 cat > /etc/sysctl.d/99-security.conf <<'EOF' net.ipv4.tcp_syncookies = 1 kernel.dmesg_restrict = 1 fs.protected_symlinks = 1 EOF sysctl --system # 7. unattended-upgrades systemctl enable --now unattended-upgrades echo "Done. Reboot recommended." ``` 10 分钟跑完一台新机器的基础加固。 ## 不要做的反例 1. **不要装一堆"安全套件"**:lynis / OSSEC / OpenSCAP 类工具好但 不是装就安全。要懂它们报的是什么 + 真去修。 2. **不要禁所有 ICMP**:v6 的 PMTU 依赖 ICMPv6;v4 ping 关了排错痛苦。 按需禁 (echo-request 限速即可)。 3. **不要开 root sudo passwordless**:你 deploy script 方便了, 攻击者拿到 sudo 权限也方便。 4. **不要给所有人 wheel / sudo group**:每个用户单独 sudoers.d 文件 细粒度授权。 5. **不要忘了 host key 备份**:服务器迁移时 host key 变 → 用户连接 报"key changed (MITM?)" 警告。备份 `/etc/ssh/ssh_host_*` 提前换。 ## 监督性持续工作 加固不是一次性。还有: - 定期跑 lynis / debsecan 看 CVE - nginx / nginx-mod-* 等关键包额外关注 CVE - 日志 anomaly detection(异常登录时间 / IP) - 重要服务的 systemd-analyze security 评分跟踪 ## 效果 我们一批 ~20 台服务器跑这套 checklist 后: - SSH brute force log 从每天 5000 条 → 几条 - 自动 security update 一周修 30+ CVE 不要人管 - 误操作 destroy 风险通过 systemd hardening 大幅降低 - 半年内一次"红蓝对抗演练",渗透测试团队 7 天没能从外网拿 shell
Go 标准库的 `net/http` + `http.FileServer` 五行就能起一个静态文件服务, 但缺生产里几个关键能力:ETag、Range(断点续传 / 视频拖动)、压缩、 正确的 Cache-Control。下面写一个完整版。 ## 五行起步 ```go package main import "net/http" func main() { http.Handle("/", http.FileServer(http.Dir("./public"))) http.ListenAndServe(":8080", nil) } ``` 这就行了——`http.FileServer` 已经支持 Range、Last-Modified、 If-Modified-Since、自动 Content-Type 推断。 但 ETag / 压缩 / 自定义 Cache-Control 要自己加。 ## 完整版 ```go package main import ( "compress/gzip" "crypto/sha256" "encoding/hex" "io" "log" "mime" "net/http" "os" "path/filepath" "strings" "time" ) const root = "./public" func main() { h := http.HandlerFunc(serve) log.Fatal(http.ListenAndServe(":8080", h)) } func serve(w http.ResponseWriter, r *http.Request) { // 1. 安全:阻止 ../ 跳出 root clean := filepath.Clean(r.URL.Path) if strings.HasPrefix(clean, "..") { http.Error(w, "forbidden", http.StatusForbidden) return } full := filepath.Join(root, clean) // 2. 默认页:目录请求映射到 index.html info, err := os.Stat(full) if err != nil { http.NotFound(w, r) return } if info.IsDir() { full = filepath.Join(full, "index.html") info, err = os.Stat(full) if err != nil { http.NotFound(w, r) return } } // 3. ETag(用 mtime + size 计算,足够稳) etag := computeETag(info) if match := r.Header.Get("If-None-Match"); match == etag { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("ETag", etag) // 4. Cache-Control if isAsset(full) { // 带 hash 的资源:长期缓存 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } else { w.Header().Set("Cache-Control", "public, max-age=300") } // 5. Content-Type ct := mime.TypeByExtension(filepath.Ext(full)) if ct != "" { w.Header().Set("Content-Type", ct) } // 6. 真正发文件 —— ServeFile 已包含 Range 支持 if acceptsGzip(r) && isCompressible(full) { w.Header().Set("Content-Encoding", "gzip") w.Header().Add("Vary", "Accept-Encoding") // 注意:开 gzip 后 Range 不再好用(gzip stream 不能任意定位) // 静态文件如果要支持 Range(音视频),别 gzip 它 gz := gzip.NewWriter(w) defer gz.Close() f, _ := os.Open(full) defer f.Close() io.Copy(gz, f) return } http.ServeFile(w, r, full) } func computeETag(info os.FileInfo) string { h := sha256.New() h.Write([]byte(info.Name())) h.Write([]byte(info.ModTime().UTC().Format(time.RFC3339Nano))) h.Write([]byte(string(rune(info.Size())))) return `"` + hex.EncodeToString(h.Sum(nil))[:16] + `"` } func acceptsGzip(r *http.Request) bool { return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") } func isCompressible(p string) bool { switch strings.ToLower(filepath.Ext(p)) { case ".html", ".css", ".js", ".json", ".svg", ".txt", ".xml": return true } return false } func isAsset(p string) bool { // 文件名含 8+ 位 hex 视为带 hash 的资源 base := filepath.Base(p) for _, part := range strings.Split(base, ".") { if len(part) >= 8 && isHex(part) { return true } } return false } func isHex(s string) bool { for _, c := range s { if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { return false } } return true } ``` ## Range / 视频拖动 `http.ServeFile` 已经处理 Range 请求(206 Partial Content)。 不需要自己写。 校验: ```bash curl -I -H 'Range: bytes=0-100' http://localhost:8080/video.mp4 # HTTP/1.1 206 Partial Content # Content-Range: bytes 0-100/1234567 # Content-Length: 101 ``` 如果你不用 ServeFile 而自己 io.Copy,Range 就失效了。 ## ETag 的边界 我用 mtime + size 算 ETag —— 简单但有边界: - 同内容不同文件(不同 mtime)会返回不同 ETag - 改完没改大小且 mtime 精度不够(FAT32 只到 2 秒)会被认为没变 更稳的方案:第一次访问时算文件 SHA256,写到 sidecar 文件 `foo.css.etag` 作为缓存。代价是写文件 / 维护成本。 ## 测试 ```bash # ETag ETAG=$(curl -sI http://localhost:8080/main.css | grep -i etag | cut -d' ' -f2 | tr -d '\r\n') curl -I -H "If-None-Match: $ETAG" http://localhost:8080/main.css # 应该返回 304 # gzip curl -I -H 'Accept-Encoding: gzip' http://localhost:8080/main.css # Content-Encoding: gzip ``` ## 为什么不用 nginx nginx 这事做得更好(C 写的 + sendfile + zero-copy)。这个 Go 实现的 价值是: - 单二进制可执行,跨平台 - 嵌入到现有 Go 后端里 - 配合 `embed.FS` 直接把静态资源打包到二进制(go embed) ```go //go:embed public var staticFS embed.FS http.Handle("/", http.FileServer(http.FS(staticFS))) ``` 整个网站打包成一个 `myapp` 二进制丢服务器跑。CD 流程极简。 ## 踩过的坑 - 没做 `filepath.Clean` + 阻止 `..` → 经典目录穿越漏洞,攻击者请求 `/../../../etc/passwd` 把文件读走。 - `http.ServeFile` 在 path 含编码的 `%2E%2E` 时可能仍接受(早期 Go 版本 有 CVE)。升级到 Go 1.22+ 已修。 - 写自己的 io.Copy 而不用 ServeFile:失去 Range、失去 Last-Modified 比较、效率也差。能用 ServeFile 就用。 - 跨平台 mtime 精度差异:Windows 是 100ns、macOS 是 1ns、Linux 通常 ns。 Build 时给 docker COPY 文件 mtime 都被改为 build 时间,结果所有文件 ETag 一样。CI 里用 `touch -d` 还原 mtime 或换 SHA-based ETag。
## 起因 我们的 Node + Python 微服务 CI 每次 build: 1. `apt install` 系统依赖:1 分钟 2. `npm ci` 装 node_modules:2.5 分钟 3. `pip install` 装 Python 依赖:1.5 分钟 4. webpack 编译:2 分钟 5. push image:1 分钟 总 8 分钟。改一行代码 → 等 8 分钟。每天 30 次 build = 4 小时浪费。 BuildKit 是 Docker 的新 builder(默认开启),支持 cache mount / secret mount / 并行 stage 构建。配合 CI 端 layer 缓存, 重复 build 大部分步骤跳过。 ## 解决方案 ### 1. 启用 BuildKit ```bash # 现代 Docker Desktop / docker 23+ 默认开 # 如果没开: export DOCKER_BUILDKIT=1 docker build . # Compose 用 buildx: docker compose build --progress plain ``` ### 2. cache mount for package managers 旧 Dockerfile: ```dockerfile FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci # 每次都从零下载 + 装 COPY . . RUN npm run build ``` 每改一行代码 → COPY 之后所有 layer 失效 → `npm ci` 重新下整套依赖。 加 cache mount: ```dockerfile # syntax=docker/dockerfile:1.7 FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm,sharing=locked \ npm ci --prefer-offline COPY . . RUN --mount=type=cache,target=/root/.npm,sharing=locked \ npm run build ``` `--mount=type=cache` 创建一个跨 build 持久的 cache 目录(在 buildkit 之内,不进最终 image)。npm 下载的包缓存到那里,下次 build 直接命中。 第一行 `# syntax=docker/dockerfile:1.7` 必须,开启高级 Dockerfile 语法。 效果:第一次 build 2.5 分钟;后续 npm install 5-10 秒。 ### 3. apt cache mount ```dockerfile FROM ubuntu:24.04 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt update \ && apt install -y --no-install-recommends \ build-essential libpq-dev \ && rm -rf /var/lib/apt/lists/* ``` 第一次跑 apt-get update 30 秒;后续每次 build 这一步 < 5 秒。 ### 4. pip / uv cache ```dockerfile FROM python:3.12-slim # 用 uv 极快 COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app COPY pyproject.toml uv.lock ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-install-project COPY . . RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen CMD ["uv", "run", "gunicorn", "myapp:app"] ``` uv 本身就快,加 cache mount 几乎是"第一次外,永远秒级"。 ### 5. multi-stage build:让产物 image 不带 build dep ```dockerfile # === build stage === FROM node:20-slim AS builder WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci COPY . . RUN --mount=type=cache,target=/root/.npm \ npm run build # === runtime stage === FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 ``` 最终 image 只有 nginx + 静态文件,不含 node_modules / 源码。 小、安全、启动快。 ### 6. 让 CI 跨 job 缓存 layer GitHub Actions: ```yaml - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v6 with: context: . push: true tags: ghcr.io/myorg/myapp:latest cache-from: type=gha cache-to: type=gha,mode=max ``` `type=gha` 用 GitHub Actions 自带 cache 后端。 重复 build 跨 job 命中 cache。 GitLab CI: ```yaml build: image: docker:cli services: [docker:dind] script: - docker buildx create --use - docker buildx build --cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache --cache-to type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max --push -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . ``` ### 7. secret mount(不让 secret 进 image layer) ```dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci ``` ```bash docker build --secret id=npmrc,src=$HOME/.npmrc . ``` `.npmrc` 在 build 时挂入,build 完不留在 image 任何 layer。 比 ARG 安全(ARG 进 history 可被 `docker history` 看到)。 ### 8. 并行 stage ```dockerfile FROM base AS deps RUN install_deps FROM base AS assets RUN compile_assets FROM runtime COPY --from=deps /opt/deps /opt/deps COPY --from=assets /opt/assets /opt/assets ``` BuildKit 自动并行无依赖的 stage(`deps` 和 `assets` 同时跑)。 ## 效果 按上面优化后我们的 build: | 步骤 | 之前 | 之后 | |---|---|---| | apt install | 60s | 5s | | npm ci | 150s | 8s | | pip install | 90s | 5s | | webpack | 120s | 60s(业务代码改了仍要 build) | | push | 60s | 10s(只 push 改了的 layer) | | **总** | **8m** | **~90s** | CI iteration 速度提升 5x,开发体验改善明显。 ## 调试 BuildKit ```bash # 详细输出 docker build --progress plain -t myapp . # 看 build cache docker buildx du # 清 cache(如果 cache 损坏 / 太大) docker buildx prune docker buildx prune --all ``` ## 踩过的坑 1. **没写 `# syntax=...` 注释**:cache mount 等高级语法不识别, 报 "unknown flag --mount"。第一行必加。 2. **`sharing=locked` 重要**:多个 build 并发跑同一 cache → race condition 损坏。`locked` 让 buildkit 串行访问。 3. **CI cache 太大被 evict**:GitHub Actions cache 上限 10 GB / repo, `mode=max` 缓存所有 layer 容易超。改 `mode=min` 只缓存最终层, 或定期清理。 4. **multi-arch build 慢**:`--platform linux/amd64,linux/arm64` 时 QEMU 模拟另一架构很慢。改用 native runner(GitHub 提供 arm64 runner, 或 self-host)。 5. **layer 顺序错**:把变化频繁的 `COPY . .` 放在装依赖前 → 每次代码 改都让依赖层失效。永远先 COPY package files 装依赖,再 COPY 代码。
预训练 + 微调是 NLP 的标准范式。Hugging Face `transformers` 把 模型 / tokenizer / 训练循环都封装好,从原始数据到训练好的分类器 只需 < 50 行代码。 下面用 IMDB 影评数据微调 BERT 做二分类(正面 / 负面)。 ## 装 ```bash uv add 'transformers[torch]' datasets accelerate evaluate ``` ## 数据 ```python from datasets import load_dataset ds = load_dataset('imdb') print(ds) # DatasetDict({ # train: Dataset({ features: ['text', 'label'], num_rows: 25000 }) # test: Dataset({ features: ['text', 'label'], num_rows: 25000 }) # unsupervised: ... # }) # label: 0 = neg, 1 = pos print(ds['train'][0]) ``` ## tokenizer ```python from transformers import AutoTokenizer MODEL = 'distilbert-base-uncased' # 比 bert-base 小 40%,效果差几个点但快 tok = AutoTokenizer.from_pretrained(MODEL) def tokenize(batch): return tok(batch['text'], truncation=True, max_length=256, padding='max_length') ds_tok = ds.map(tokenize, batched=True) ds_tok = ds_tok.remove_columns(['text']) ds_tok = ds_tok.rename_column('label', 'labels') ds_tok.set_format('torch') ``` `truncation=True` 截到 256 token;BERT 最大 512,但短点训练快得多。 ## 模型 ```python from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=2) ``` `from_pretrained` 自动下载预训练权重 + 加上一个新的 classification head。 ## Trainer:30 行训练循环 ```python from transformers import TrainingArguments, Trainer import evaluate import numpy as np metric = evaluate.load('accuracy') def compute_metrics(eval_pred): logits, labels = eval_pred preds = np.argmax(logits, axis=1) return metric.compute(predictions=preds, references=labels) args = TrainingArguments( output_dir='./out', num_train_epochs=2, per_device_train_batch_size=16, per_device_eval_batch_size=32, learning_rate=2e-5, weight_decay=0.01, warmup_steps=500, logging_steps=50, eval_strategy='epoch', save_strategy='epoch', load_best_model_at_end=True, metric_for_best_model='accuracy', fp16=True, # GPU 时开混合精度 report_to='wandb', # 可选:wandb 跟踪 ) trainer = Trainer( model=model, args=args, train_dataset=ds_tok['train'].shuffle(seed=42).select(range(10000)), eval_dataset=ds_tok['test'].select(range(2000)), tokenizer=tok, compute_metrics=compute_metrics, ) trainer.train() trainer.evaluate() ``` 10k 训练样本 / 2k 验证样本,DistilBERT,单个 RTX 3090 上 ~5 分钟训完, 准确率约 91-92%。 全量 25k 训 3 个 epoch 能到 93-94%。 ## 保存 + 加载 + 推理 ```python trainer.save_model('./imdb-distilbert') tok.save_pretrained('./imdb-distilbert') # 加载推理 from transformers import pipeline classifier = pipeline('sentiment-analysis', model='./imdb-distilbert', device=0) # device=0 = cuda:0; -1 = cpu print(classifier('This movie was absolutely fantastic')) # [{'label': 'LABEL_1', 'score': 0.998}] print(classifier('What a complete waste of time and money')) # [{'label': 'LABEL_0', 'score': 0.997}] ``` `label` 是模型内部的,可以在训练时设标签名: ```python model = AutoModelForSequenceClassification.from_pretrained( MODEL, num_labels=2, id2label={0: 'NEGATIVE', 1: 'POSITIVE'}, label2id={'NEGATIVE': 0, 'POSITIVE': 1}, ) ``` 之后 pipeline 直接返回 'POSITIVE' / 'NEGATIVE'。 ## 中文文本? ```python MODEL = 'bert-base-chinese' # 谷歌中文 BERT # 或: MODEL = 'hfl/chinese-roberta-wwm-ext' # 哈工大 RoBERTa 全词 mask # 或: MODEL = 'IDEA-CCNL/Erlangshen-Roberta-110M-Sentiment' # 已经是情感分类微调过的 ``` 中文 tokenizer 是字级 BPE(不像英文按 subword),输出结构一样。 ## 推到 Hugging Face Hub(可选) ```bash huggingface-cli login ``` ```python trainer.push_to_hub('your-username/imdb-distilbert') # 之后任何人能: # AutoModelForSequenceClassification.from_pretrained('your-username/imdb-distilbert') ``` ## 减小内存:LoRA 完整微调 DistilBERT 已经够轻;微调 7B+ 模型时显存装不下,用 LoRA: ```bash uv add peft ``` ```python from peft import LoraConfig, get_peft_model, TaskType lora_config = LoraConfig( task_type=TaskType.SEQ_CLS, r=8, lora_alpha=16, lora_dropout=0.1, target_modules=['q_lin', 'v_lin'], ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # trainable params: 0.6M / 67M = 0.9% ``` 之后照常 `Trainer`。LoRA 让显存 / 速度大幅降低,性能差距通常 < 1%。 ## 推理优化(生产) - **量化**:`bitsandbytes` int8 / int4,2-4x 速度,半精度差几个点 - **ONNX 导出 + ONNX Runtime**:CPU 推理 2-3x 加速 - **TGI** (Text Generation Inference) / **vLLM**:高并发 LLM 推理 - **TorchServe**:通用 PyTorch 推理服务 简单分类任务直接用 `pipeline`;高并发用 TGI / TorchServe。 ## 评估更精细 ```python import evaluate metrics = evaluate.combine(['accuracy', 'f1', 'precision', 'recall']) def compute_metrics(eval_pred): logits, labels = eval_pred preds = np.argmax(logits, axis=1) return metrics.compute(predictions=preds, references=labels) ``` ## 踩过的坑 - `padding='max_length'` 把所有样本 pad 到 256 → 浪费计算。改成 `padding=True`(动态 pad 到 batch 内最长),训练快 2-3x。 - learning rate 太大:BERT 微调通常 2e-5 ~ 5e-5。1e-4 已经偏大, 常导致 loss NaN。 - 数据集 label 顺序:HuggingFace `load_dataset('imdb')` 是按 label 排序的,不 shuffle 训练会先看完所有 negative 再看 positive, loss 曲线奇形怪状。`shuffle(seed=42)` 必加。 - `fp16=True` 在 BF16 友好的硬件(A100 / H100)改 `bf16=True` 更稳。 老 V100 / GTX 用 fp16。
## 起因 公司项目演化: - 1 个 web app → 加 mobile app(React Native) - 加 admin dashboard - 加 marketing site - 加 design system 组件库 - 加 shared TS types / API client 多 repo 痛点: - shared code 要 publish package - 跨 repo PR 难 coordinate - 升级 dep 各 repo 跟不齐 monorepo 解决但 build / test / cache 是新挑战。 **Turborepo** / **Nx** 解决"哪些 task 要跑 + 缓存什么"。 ## Turborepo Vercel 收购,2022+。 ``` monorepo/ ├── apps/ │ ├── web/ # Next.js │ ├── mobile/ # React Native │ └── admin/ ├── packages/ │ ├── ui/ # shared component │ ├── api-client/ │ └── tsconfig/ ├── package.json ├── turbo.json └── pnpm-workspace.yaml ``` `pnpm-workspace.yaml`: ```yaml packages: - 'apps/*' - 'packages/*' ``` `turbo.json`: ```json { "tasks": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] }, "test": { "dependsOn": ["build"] }, "lint": {}, "dev": { "cache": false, "persistent": true } } } ``` `^build` = 先跑依赖 package 的 build。 ## 用法 ```bash pnpm turbo build # build 所有 package(按 dep order) pnpm turbo test # 同上 pnpm turbo dev # 跑所有 dev server pnpm turbo build --filter=web # 只 build web app + 它依赖 ``` 第一次跑 build:跑全部 task。 第二次跑 build(无改动):**全部 cache hit,零执行**。 ## cache 怎么工作 Turbo 把每 task 的: - input file content hash - env var - dependency hash → cache key。命中 cache:直接复用 output(dist/)。 ``` ✔ ui:build cached (3.2s saved) ✔ api-client:build cached ○ web:build finished (cached) 4.5s ``` 改 1 个 package 的 source → 它跟它的 dependent 重 build;其它仍 cache hit。 ## 远程 cache Turbo cache 默认本地。多人开发 / CI 同一计算重复跑。 remote cache: ```bash npx turbo login npx turbo link ``` 把 cache 推 Vercel cloud / 自托管 → 同事 pull 后 cache 命中(同 commit 他不必再跑 build)。 我们 6 人团队 CI 时间从 12 分钟 → 2 分钟(90% cache hit)。 ## Nx Nrwl 出,2017+。比 turbo 更老更全。 ```bash npx create-nx-workspace@latest myorg ``` Nx 提供: - task scheduling + cache(跟 Turbo 类似) - generator(一键 scaffold app / lib) - 依赖图可视化 - 多语言(不只 JS,也支持 .NET / Go via plugin) - consistent project structure 强制 `nx.json`: ```json { "targetDefaults": { "build": { "cache": true, "inputs": ["default", "^production"] } }, "tasksRunnerOptions": { "default": { "runner": "nx/tasks-runners/default", "options": { "cacheableOperations": ["build", "test", "lint"] } } } } ``` ```bash nx build web nx affected -t test # 只跑受影响 package nx graph # 浏览器看 dependency 图 ``` `nx affected` 是杀器:基于 git diff 判定哪 package 改了,只跑相关。 ## Turbo vs Nx | | Turbo | Nx | |---|---|---| | 设计 | 轻量 task runner | 全套 monorepo solution | | 学习曲线 | 低 | 中高 | | generator | 无 | 强(scaffold app / lib) | | 依赖图 UI | 无 | 强 | | 多语言 | JS only | JS + 插件 | | 远程 cache | Vercel / 自托管 | Nx Cloud / 自托管 | | 强约束 | 少 | 多("Nx way") | **Turbo**:你已有 monorepo + 想加 cache → 5 分钟接入。 **Nx**:从 0 开始 + 想 batteries-included → Nx。 我个人 Turbo 多(不喜欢被框架强约束)。 ## 内部 package 引用 ```json // apps/web/package.json { "dependencies": { "@myorg/ui": "workspace:*", "@myorg/api-client": "workspace:*" } } ``` `workspace:*` 让 pnpm 链接到本地 package(不发 npm)。 ```tsx // apps/web/app/page.tsx import { Button } from '@myorg/ui'; import { fetchPosts } from '@myorg/api-client'; ``` 跟普通 npm package 一样 import。 ## shared tsconfig ```json // packages/tsconfig/base.json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "strict": true, "moduleResolution": "Bundler" } } ``` ```json // apps/web/tsconfig.json { "extends": "@myorg/tsconfig/base", "compilerOptions": { ... } } ``` config 一处改 全 repo 受用。 ## CI 优化 ```yaml # GitHub Actions - uses: pnpm/action-setup@v2 - run: pnpm install - run: pnpm turbo build test lint --concurrency=4 ``` Turbo 跟 GH cache action 配合 → cache 远程拉。 PR 时间从 15 分钟 → 3 分钟(动几个 package 重 build,其余 cache)。 ## 真实 case 我们 monorepo 演化: ``` 最初: 单 Next.js 项目 ↓ 加 admin dashboard (Next.js) → 共享 component 想抽 ↓ 建 monorepo + pnpm workspace + Turbo ↓ packages/ui (shared component) packages/api-types (类型 + zod schema) packages/tsconfig apps/web, apps/admin, apps/marketing ↓ 后来加 packages/api-client (axios + types) ↓ team grow 到 8 人 → 加 remote cache ``` 效果: - 改 ui component → web + admin 一起测 - 共享 type 跨 frontend / backend(Node 后端也 import api-types) - CI 快 5x - 新成员 onboarding:clone 1 repo 全 setup ## 不要过早 monorepo 单 app + < 3 人 → 不必。 N app + shared code → monorepo + Turbo。 ## 与 yarn workspaces / npm workspaces pnpm 是 monorepo 最强: - 严格 dependency 隔离 - 链接速度快 - workspaces 原生 npm workspaces / yarn workspaces 也行但 pnpm 主流。 ## 踩过的坑 1. **依赖循环**:`@myorg/ui` 依赖 `@myorg/utils` → utils 依赖 ui → build 报错。`nx graph` / `turbo` 看依赖图理清。 2. **build output 没指定**:turbo 不知道 cache 啥 → cache miss 一直。 `"outputs": [".next/**", "dist/**"]` 必填。 3. **env 变量影响 cache**:env 改了应该 invalidate cache。 `"env": ["NODE_ENV"]` 配置。 4. **monorepo 大 后 IDE 慢**:100+ package 后 TS server 慢。 project references + 限 indexing scope。 5. **publish 仍麻烦**:内部 package 不 publish OK。但要 publish shared design system → changesets 工具帮 version + changelog。
Docker 适合"一进程 + 镜像分发"。LXC(system container)适合"我要一个 完整的 Linux,但不想多装一台 VM"——它共享宿主内核但提供完整的 init / cron / ssh / multiple processes,像轻量 VM。 适合场景:本地开发环境隔离(一个项目一个 LXC,不脏宿主)、 跑老版本 Ubuntu 试旧软件、做 CI 隔离。 ## 安装 LXD(LXC 的现代封装) ```bash sudo snap install lxd sudo lxd init # 一路 Enter 用默认值:dir storage、新的 lxdbr0 网桥、不加 cluster sudo usermod -aG lxd $USER # 重新登录让 group 生效 ``` ## 启动一个容器 ```bash # 列可用镜像 lxc image list images: ubuntu/22.04 | head # 启动 lxc launch images:ubuntu/22.04 devbox lxc list # +--------+---------+----------------------+------+-----------+-----------+ # | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | # +--------+---------+----------------------+------+-----------+-----------+ # | devbox | RUNNING | 10.10.20.42 (eth0) | | CONTAINER | 0 | # +--------+---------+----------------------+------+-----------+-----------+ ``` ## 进去 / 跑命令 ```bash # 交互 shell lxc exec devbox -- bash # 一次性命令 lxc exec devbox -- apt update lxc exec devbox -- apt install -y python3-pip git # 以普通用户身份进 lxc exec devbox -- su - ubuntu ``` ## 挂载宿主目录 ```bash # 把宿主 /home/yourname/code/myproj 挂到容器 /opt/myproj lxc config device add devbox myproj disk \ source=/home/yourname/code/myproj \ path=/opt/myproj ``` ID 映射的小坑:默认 LXD 用 unprivileged container(UID 映射), 宿主的 `1000:1000` 在容器内是 `1000:1000`,权限正确。但如果宿主用了 NFS、 overlay 等,映射可能不直观,遇到再 `lxc config show devbox` 看。 ## 端口映射 / 反代 容器有 IP 但 NAT 在宿主桥后。要把容器的 8080 暴露到宿主: ```bash lxc config device add devbox webproxy proxy \ listen=tcp:0.0.0.0:18080 \ connect=tcp:127.0.0.1:8080 ``` 之后 `curl http://<宿主-IP>:18080/` 转到容器内 8080。 ## 拍快照 / 还原 ```bash lxc snapshot devbox before-upgrade lxc restore devbox before-upgrade lxc info devbox | grep -A 5 Snapshots ``` 非常适合"我要装一个可能搞坏环境的东西,万一不行就回去"。 ## 限制资源 ```bash lxc config set devbox limits.cpu 2 lxc config set devbox limits.memory 2GB lxc config set devbox limits.memory.swap false lxc restart devbox ``` ## 模板化 ```bash # 把 devbox 当模板 lxc snapshot devbox base lxc publish devbox/base --alias my-dev-base # 之后随时拉一份新的出来 lxc launch my-dev-base devbox2 ``` ## 自动启动 ```bash lxc config set devbox boot.autostart true lxc config set devbox boot.autostart.priority 10 ``` ## 与 Docker 对比 | 维度 | LXC (system container) | Docker (app container) | |---|---|---| | 进程数 | 多进程,有 init | 默认单进程(PID 1) | | 文件系统 | 完整 distro | 极简 image | | 启动速度 | 1-2 秒 | < 1 秒 | | 生命周期 | 长(像 VM) | 短(每次启动是新实例) | | 应用分发 | 不方便 | 主流(image registry) | | 编排生态 | LXD cluster | Kubernetes、Swarm、Compose | ## 踩过的坑 - 默认 storage backend 是 `dir`(裸目录),性能差且不支持快照高效复制。 生产 / 重度使用换 `btrfs` 或 `zfs`:`sudo lxd init` 时选。 - 容器内 dmesg / mount 等命令可能权限不够 —— 这是 unprivileged container 的预期行为,不是 bug。需要的话 `lxc config set devbox security.privileged true`, 但安全等同 root。 - LXD vs LXC:`apt install lxc` 装的是底层工具;建议直接 `snap install lxd` 用更现代的 API。两者底层兼容但命令完全不同。 - Snap 版 LXD 升级时容器会随之重启,业务跑在 LXD 上的要避开升级窗口。
## 起因 PG 慢。CPU 跑满。但具体是哪条 query / 哪个业务模块? slow query log 能记慢的,但漏掉"单次快、调用极多次"的;也不容易聚合 看"Top N 总耗时 query"。 `pg_stat_statements` 是 PG 自带 extension,按"query 模板" 累计每条 SQL 的总耗时 / 调用次数 / 平均时间。性能分析的瑞士军刀。 ## 启用 `postgresql.conf`: ``` shared_preload_libraries = 'pg_stat_statements' pg_stat_statements.max = 10000 pg_stat_statements.track = all # all / top / none pg_stat_statements.track_utility = on ``` 需要重启 PG: ```bash sudo systemctl restart postgresql ``` 加载 extension(一次性): ```sql CREATE EXTENSION pg_stat_statements; ``` ## 经典 Top 查询 ### Top 10 总耗时 query ```sql SELECT substring(query, 1, 100) AS query_short, calls, round(total_exec_time::numeric, 2) AS total_ms, round(mean_exec_time::numeric, 2) AS mean_ms, round((total_exec_time / sum(total_exec_time) OVER ()) * 100, 1) AS pct FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10; ``` 输出: ``` query_short | calls | total_ms | mean_ms | pct SELECT * FROM orders WHERE user_id = $1 | 1234567 | 245678.12 | 0.20 | 32.5 SELECT * FROM products WHERE category = $1 | 234567 | 123456.78 | 0.53 | 16.3 UPDATE sessions SET ... WHERE id = $1 | 5678901 | 89012.34 | 0.016 | 11.8 ``` `pct` 列告诉你"这条 query 占总 DB 时间 32.5%"。**显著大头一目了然**。 ### Top 10 平均最慢 ```sql SELECT substring(query, 1, 100), calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(stddev_exec_time::numeric, 2) AS stddev_ms FROM pg_stat_statements WHERE calls > 100 -- 调用 < 100 次的忽略(统计意义弱) ORDER BY mean_exec_time DESC LIMIT 10; ``` 找"单次很慢但调用不多"的 query。 ### Top IO 消耗 ```sql SELECT substring(query, 1, 100), calls, shared_blks_hit, shared_blks_read, round((shared_blks_hit::numeric * 100 / (shared_blks_hit + shared_blks_read + 1)), 1) AS hit_pct, round(total_exec_time::numeric, 2) AS total_ms FROM pg_stat_statements WHERE shared_blks_read > 0 ORDER BY shared_blks_read DESC LIMIT 10; ``` `shared_blks_read` 高 = 经常从磁盘读(cache miss)。 `hit_pct` 低 = working set 不在 shared_buffers 内。 ### Top temp 文件用量 ```sql SELECT substring(query, 1, 100), calls, temp_blks_read, temp_blks_written FROM pg_stat_statements WHERE temp_blks_written > 0 ORDER BY temp_blks_written DESC LIMIT 10; ``` `temp_blks_written > 0` = `work_mem` 不够大,PG 用磁盘做 sort / hash。 调大 work_mem 或者改 query 让 sort 集少点。 ## 让 query 文本可读 默认 `query` 字段把字面值替换成 `$1` `$2` 等: ``` SELECT * FROM orders WHERE user_id = $1 AND status = $2 ``` 同一 query 模板不同参数算同一条。 分析 perspective 想看完整 SQL: ```sql ALTER SYSTEM SET pg_stat_statements.track = 'all'; ALTER SYSTEM SET pg_stat_statements.save = on; SELECT pg_reload_conf(); ``` 不过 `$1`/`$2` 模板化是 feature,便于聚合分析。 ## reset stats ```sql SELECT pg_stat_statements_reset(); -- 清零,重新开始统计 ``` 定期 reset 让数据反映最近的负载。 跑性能优化前 reset → 跑业务 1 小时 → 看 stats。 ## 跟 EXPLAIN ANALYZE 配合 pg_stat_statements 告诉你"哪条 query 是瓶颈"。 EXPLAIN ANALYZE 告诉你"这条 query 为什么慢"。 组合用: ```sql -- 1. pg_stat_statements 找 Top 1 -- 2. 拿完整 SQL(替换 $1 等占位符) -- 3. EXPLAIN ANALYZE 跑一遍 EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM orders WHERE user_id = 12345 AND status = 'pending'; -- 4. 看 plan: -- Seq Scan? → 加索引 -- 估算 rows 偏差大? → ANALYZE 表 -- 大 Hash join? → work_mem 不够 ``` ## 生产监控仪表盘 Grafana + postgres_exporter 自动暴露 pg_stat_statements 数据: ```yaml # postgres_exporter 配置 queries: - pg_stat_statements_top: query: | SELECT queryid::text, query, total_exec_time, calls FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20 metrics: - queryid: { usage: LABEL } - query: { usage: LABEL } - total_exec_time: { usage: GAUGE } - calls: { usage: GAUGE } ``` Grafana 仪表盘 panel: - Top 10 query by total time(pie chart) - Top 10 query by mean time - query 数 / 慢 query 数趋势线 ## 真实 case:减少 90% DB 时间 我们一个 web app 用 pg_stat_statements 跑一周后看 Top 5: ``` 1. SELECT COUNT(*) FROM users WHERE deleted_at IS NULL | 38% pct 2. SELECT * FROM products JOIN ... | 24% pct 3. SELECT * FROM sessions WHERE expires > now() | 18% pct 4. SELECT * FROM logs WHERE ... | 8% pct 5. ... ``` 行动: 1. `SELECT COUNT(*) FROM users WHERE deleted_at IS NULL`:被首页用了 N 次。改成 cache 5 分钟(Redis),从 38% → 0.5%。 2. `SELECT * FROM products JOIN ...`:N+1 query。改成 prefetch + 单次 JOIN。 3. `SELECT * FROM sessions ...`:缺索引。`CREATE INDEX ON sessions(expires)`。 reset stats + 跑一周后 Top 5 完全变化,DB CPU 从 70% → 12%。 ## 限制 + 注意 ### 1. plan 不存 pg_stat_statements 只记 query 文本 + 计数 / 时间,不存 query plan。 要看 plan 仍要 EXPLAIN。 ### 2. session 级变量影响 `SET LOCAL work_mem = '256MB'` 等 session 设置影响 query 但 stats 不区分。 ### 3. 安全(敏感数据) extension 默认 query 文本会被截短(参数 `track_activity_query_size`)。 完整 SQL 可能含 schema 名 / 表名 / 业务逻辑细节。访问 pg_stat_statements 要 superuser 权限。 ### 4. 性能 overhead 收集统计本身极轻(< 1% CPU)。生产开它是标准做法。 ## 几个查询模板我经常用 ```sql -- 找"慢但调用少" 可优化但可能 ROI 低 SELECT substring(query, 1, 80), calls, round(mean_exec_time::numeric, 2) FROM pg_stat_statements WHERE mean_exec_time > 1000 AND calls < 100 ORDER BY total_exec_time DESC LIMIT 20; -- 找"非常频繁的快 query"(可能 cache 化) SELECT substring(query, 1, 80), calls, round(mean_exec_time::numeric, 3) FROM pg_stat_statements WHERE calls > 10000 AND mean_exec_time < 5 ORDER BY calls DESC LIMIT 20; -- 找写操作 SELECT substring(query, 1, 80), calls, round(total_exec_time::numeric, 2) FROM pg_stat_statements WHERE query ILIKE 'UPDATE%' OR query ILIKE 'INSERT%' OR query ILIKE 'DELETE%' ORDER BY total_exec_time DESC LIMIT 20; ``` ## 与 auto_explain `auto_explain` 是另一个 extension,自动对慢 query 跑 EXPLAIN 并 log: ``` shared_preload_libraries = 'pg_stat_statements, auto_explain' auto_explain.log_min_duration = 1000 # > 1s 自动 EXPLAIN auto_explain.log_analyze = true ``` 慢 query 自动留下 plan,不需要事后复现。 但 log_analyze 让 query 跑两次(一次正常 + 一次 ANALYZE),有性能开销。 ## 踩过的坑 1. **shared_preload_libraries 改了忘重启**:extension 不加载。 `CREATE EXTENSION` 也会报错 `library not loaded`。 2. **pg_stat_statements.max 太小**:超过后老 query 被 evict。 显著负载多种 query 的应用建议 10000+。 3. **reset 频繁** → 失去历史趋势。生产建议每月 reset 一次 + 之前 dump 数据到分析 DB。 4. **生产 query 含动态 IN** :`WHERE id IN ($1, $2, $3)` 跟 `IN ($1, $2)` 被算作不同 query(IN 元素数不一样模板就不同)。考虑改成 `= ANY($1)` 传数组。 5. **跨 DB**:pg_stat_statements 是 DB 级别。每个 DB 都要单独装 extension + 查看。
## 起因 部署一个 Python / Node / Go 应用,需要: - 后台跑(不 attach 终端) - 开机自启 - crash 自动重启 - 集中查 log - restart / stop 标准命令 老办法: - nohup + & + 写 pid 文件(朴素 + 不重启) - supervisord(Python,需装) - pm2(Node 圈,需装) - forever(更老的 Node 工具) systemd 是现代 Linux 默认(Ubuntu 16+ / RHEL 7+ / Debian 8+), **无需装第三方工具**。下面写一份 service 文件就够了。 ## 最小 service file ```ini # /etc/systemd/system/myapp.service [Unit] Description=My Web App After=network.target [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/srv/myapp ExecStart=/srv/myapp/.venv/bin/gunicorn myapp.wsgi -b 127.0.0.1:8000 -w 4 Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ``` 启用 + 启动: ```bash sudo systemctl daemon-reload sudo systemctl enable --now myapp ``` 完事。开机自启 + 后台 + crash 自动 5 秒重启 + log 到 journald。 ## 常用命令 ```bash systemctl status myapp # 状态 systemctl start/stop/restart myapp systemctl reload myapp # 发 SIGHUP(如果应用支持 reload) systemctl enable / disable myapp # 开机启动开关 journalctl -u myapp # 看 log journalctl -u myapp -f # tail -f journalctl -u myapp --since '1 hour ago' journalctl -u myapp -p err # 只看 error 级别 ``` ## 环境变量 ```ini [Service] EnvironmentFile=/etc/myapp/env Environment="DJANGO_SETTINGS_MODULE=myapp.settings.production" Environment="PYTHONUNBUFFERED=1" ``` `/etc/myapp/env`: ``` DATABASE_URL=postgres://... SECRET_KEY=... ``` 权限 600 + owned by root → secret 安全。 ## 自动重启策略 ```ini [Service] Restart=on-failure # always / on-failure / on-abnormal RestartSec=5s StartLimitIntervalSec=300 StartLimitBurst=10 # 5 分钟内重启 > 10 次就放弃 ``` `Restart=always`:包括正常退出也重启(适合 worker)。 `Restart=on-failure`:只 exit code ≠ 0 才重启(适合 server)。 ## 资源限制 ```ini [Service] MemoryMax=2G # 超过被 OOM killed CPUQuota=200% # 最多用 2 核 TasksMax=512 # 子进程上限 LimitNOFILE=65535 # 文件描述符 ``` 防 runaway 进程吃光资源。容器化等价但 systemd 也能做。 ## 安全 hardening ```ini [Service] NoNewPrivileges=true # 不能 setuid PrivateTmp=true # 独立 /tmp ProtectSystem=strict # /usr / /boot 等只读 ProtectHome=true # /home 不可见 ReadWritePaths=/var/log/myapp /var/lib/myapp ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictSUIDSGID=true ``` systemd-analyze security myapp 评分(0-10,越低越严)。 不必全开但默认加几个稳妥。 ## socket activation ```ini # /etc/systemd/system/myapp.socket [Unit] Description=myapp socket [Socket] ListenStream=127.0.0.1:8000 Accept=no [Install] WantedBy=sockets.target ``` ```ini # myapp.service [Service] ExecStart=/srv/myapp/server StandardInput=socket # 接收 socket fd ``` systemd 监听 port,第一个请求来才启动 service → 节省资源 + 启动期间 请求 buffered。 对 web app 较 niche,inetd 风格。 ## timer (取代 cron) ```ini # /etc/systemd/system/myapp-cleanup.service [Service] Type=oneshot ExecStart=/srv/myapp/.venv/bin/python /srv/myapp/cleanup.py ``` ```ini # /etc/systemd/system/myapp-cleanup.timer [Timer] OnCalendar=daily # 每天午夜 OnCalendar=*-*-* 03:30:00 # 每天 3:30 Persistent=true # boot 后补跑漏的 [Install] WantedBy=timers.target ``` ```bash systemctl enable --now myapp-cleanup.timer systemctl list-timers # 看下次跑时间 journalctl -u myapp-cleanup # 历史 log ``` cron 优势: - log 集成 journald - 重试 / failure 处理 systemd 标准 - random delay(多机错峰) - 可重用 service 单独执行 我现在新部署 0 cron,全 timer。 ## graceful shutdown ```ini [Service] ExecStart=/srv/myapp/server TimeoutStopSec=30s # SIGTERM 后等 30s KillSignal=SIGTERM ExecReload=/bin/kill -HUP $MAINPID ``` `systemctl stop myapp` → 发 SIGTERM → 应用清理 → 30s 内不退就 SIGKILL。 应用要 catch SIGTERM 完成 in-flight 请求再退。 ## 多实例 `@` template: ```ini # /etc/systemd/system/[email protected] [Service] ExecStart=/srv/myapp/server --port=%i ``` ```bash systemctl start myapp@8001 myapp@8002 myapp@8003 ``` 3 个 instance 不同 port,共享 service 模板。 ## user service 不需要 sudo: ```bash # 写到 ~/.config/systemd/user/myapp.service systemctl --user daemon-reload systemctl --user enable --now myapp loginctl enable-linger $USER # 退出 shell 后仍跑 ``` 适合:个人项目 / 不能 root 的服务器。 ## 与 supervisord 对比 | | systemd | supervisord | pm2 | |---|---|---|---| | 默认装 | ✅(modern Linux) | ❌(pip install) | ❌(npm install) | | 配置语法 | INI(详细) | INI(简) | JS/JSON | | 资源限制 | ✅ | ❌ | ❌ | | timer | ✅ | ❌ | ❌(用 cron) | | log | journald | 文件 | 文件 | | 跨平台 | Linux only | 多平台 | 多平台 | | 开机启动 | ✅ | 要 init 配 | pm2 startup | Linux 服务器 systemd 完胜(已经在那)。 非 Linux / 容器(无 systemd)→ supervisord / pm2。 ## docker 里要 systemd? 容器内一般 PID 1 跑应用,不需要 systemd。 特殊场景(多进程 in container)用 `tini` / `supervisord` / `s6-overlay`。 systemd in container:复杂,不推荐。如果非要,Podman 比 Docker 友好。 ## journald log 持久化 默认 journald 内存 + 临时存储。重启丢。 持久化: ```bash sudo mkdir -p /var/log/journal sudo systemctl restart systemd-journald ``` 或者 forward 到 rsyslog / Loki: ```ini # /etc/systemd/journald.conf [Journal] ForwardToSyslog=yes ``` ## 真实部署 case 部署 Django + gunicorn + celery worker + celery beat: ```ini # myapp.service (gunicorn) [Service] ExecStart=/srv/myapp/.venv/bin/gunicorn myapp.wsgi -b 127.0.0.1:8000 # myapp-worker.service [Service] ExecStart=/srv/myapp/.venv/bin/celery -A myapp worker --loglevel=info # myapp-beat.service [Service] ExecStart=/srv/myapp/.venv/bin/celery -A myapp beat --loglevel=info ``` 3 个 service,`systemctl status myapp myapp-worker myapp-beat` 全状态。 部署 deploy script: ```bash git pull .venv/bin/pip install -r requirements.txt .venv/bin/python manage.py migrate sudo systemctl restart myapp myapp-worker myapp-beat ``` 简单 + 工业级稳定。 ## 踩过的坑 1. **改了 service file 没 daemon-reload**:systemctl 用老版本。 `daemon-reload` 必须。 2. **WorkingDirectory 不存在**:service 启不来报错 213。 `journalctl -u myapp` 看具体原因。 3. **env 变量空格转义**:`Environment="K=v with space"` 双引号必须。 4. **Type=forking 错用**:应用 fork 后 systemd 跟丢主进程。多数 web server 用 `Type=simple` / `notify`。 5. **PID 文件错**:traditional daemon 写 PID 文件,systemd 不靠它。 `PIDFile=` 配置是给 forking type 用。
## 起因 数据分析常见情境: - 收到一堆 CSV / Parquet(几 GB - 几十 GB) - 想跑 SQL JOIN / 聚合 / 窗口函数分析 - 没 Snowflake / BigQuery(个人项目 / 本地探索) - pandas 慢 + groupby 写得难看 `DuckDB`:嵌入式 OLAP 数据库("SQLite for analytics"),单文件 binary,跑分析 SQL 跟 columnar 仓库一样快,**在你笔记本上**。 ## 装 ```bash pip install duckdb # Python brew install duckdb # CLI ``` ## CLI ```bash $ duckdb my.db D SELECT * FROM 'data.csv' LIMIT 5; -- 直接读 csv,无需 import D SELECT COUNT(*) FROM 'data.parquet'; D SELECT a.x, b.y FROM 'a.csv' a JOIN 'b.parquet' b ON a.id = b.id; ``` CSV / Parquet / JSON 直接当 table 查,无 import 步骤。 ## Python ```python import duckdb # in-memory con = duckdb.connect() # 直接查 CSV df = con.execute(""" SELECT country, SUM(amount) AS total FROM 'orders.csv' WHERE qty > 5 GROUP BY country ORDER BY total DESC """).df() # 返回 pandas df # 持久化 con = duckdb.connect('analysis.db') con.execute("CREATE TABLE orders AS SELECT * FROM 'orders.csv'") ``` ## 跟 pandas / DataFrame 互通 ```python import pandas as pd import duckdb # pandas df 直接当 table 用(DuckDB zero-copy 引用) df = pd.read_csv('big.csv') result = duckdb.sql(""" SELECT col1, AVG(col2) FROM df -- 直接引用 pandas df GROUP BY col1 """).df() ``` polars 同样: ```python import polars as pl pl_df = pl.read_csv('big.csv') result = duckdb.sql("SELECT * FROM pl_df WHERE col1 > 100").pl() ``` DuckDB 跟 pandas / polars / Arrow 数据**zero-copy 互转**(都用 Arrow columnar 内存格式)。 ## 性能 8 核 16 GB 笔记本,10 GB Parquet 文件: ```sql SELECT country, SUM(amount), COUNT(*) FROM 'orders.parquet' GROUP BY country ORDER BY 2 DESC LIMIT 10; ``` | 工具 | 时间 | |---|---| | pandas | 35s | | polars (eager) | 8s | | polars (lazy) | 4s | | DuckDB | 2.5s | DuckDB 列存 + vector 执行 + 多核 + 全局优化器,把分析查询打得很快。 ## 直接查远程 Parquet ```python duckdb.sql(""" SELECT * FROM read_parquet('s3://my-bucket/orders/*.parquet') WHERE date = '2025-03-01' """) ``` DuckDB 支持 S3 / GCS / Azure / HTTP 直读。 配合 partition + Parquet column pruning → 只读必要的 column 和 partition。 ## 数据湖直接查 跟 Iceberg / Delta lake 集成: ```sql INSTALL iceberg; LOAD iceberg; SELECT * FROM iceberg_scan('s3://bucket/table/'); ``` 不用 Spark 也能查 Iceberg。 ## window 函数 / 复杂 SQL ```sql SELECT user_id, date, amount, SUM(amount) OVER (PARTITION BY user_id ORDER BY date) AS cum_sum, RANK() OVER (PARTITION BY DATE_TRUNC('month', date) ORDER BY amount DESC) AS rank_in_month FROM orders; ``` 全 SQL 标准 + Postgres 兼容大量扩展 + DuckDB 特有的 ANTI/SEMI JOIN / QUALIFY 等。 ## EXPORT / IMPORT ```sql COPY (SELECT * FROM big_table) TO 'out.parquet' (FORMAT PARQUET); COPY (SELECT * FROM big_table) TO 'out.csv' (HEADER, DELIMITER ','); ``` 数据格式互转的瑞士军刀。 ## 真实 case:替代 pandas EDA 数据探索,原本: ```python df = pd.read_csv('events.csv') df_filtered = df[df.user_age > 18] grouped = df_filtered.groupby(['country', 'product']).agg({ 'amount': ['sum', 'mean'], 'qty': 'count', }).reset_index() sorted = grouped.sort_values(('amount', 'sum'), ascending=False) sorted.head(20) ``` DuckDB 等价: ```python duckdb.sql(""" SELECT country, product, SUM(amount) AS total, AVG(amount) AS avg_amount, COUNT(*) AS n FROM 'events.csv' WHERE user_age > 18 GROUP BY country, product ORDER BY total DESC LIMIT 20 """).df() ``` SQL 更直白 + 跑得快 + 不需要 import 完整 csv。 ## 跟 Snowflake / BigQuery 对比 | | DuckDB | Snowflake | BigQuery | |---|---|---|---| | 部署 | 单 binary | SaaS | SaaS | | 数据规模 | < 1 TB(单机) | PB | PB | | 成本 | 0 | 按 credit | 按扫描 GB | | 启动 | < 100ms | < 1s | < 5s | | SQL | Postgres-like | ANSI++ | ANSI+ | | 并发用户 | 单 | 多 | 多 | DuckDB 不替代 Snowflake(不是 multi-user / 不是无限 scale)。 但 90% 个人 / 团队分析(< 1 TB)DuckDB 够 + 免费 + 快。 ## motherduck(DuckDB cloud) DuckDB 团队也做了 motherduck.com → DuckDB + cloud sync: - 本地查 + 云端永久存 - 共享数据集 - 跨设备一致 按需用,但 DuckDB 本身完全离线可用。 ## 嵌入应用 ```python # Django / FastAPI 里嵌 DuckDB 做 analytics endpoint import duckdb @app.get('/analytics/top-products') def top_products(): return duckdb.sql(""" SELECT product, SUM(amount) AS total FROM read_parquet('s3://.../orders/*.parquet') WHERE date > current_date - 7 GROUP BY product ORDER BY total DESC LIMIT 10 """).df().to_dict('records') ``` 不用 separate analytics DB / 全部嵌进应用。 ## extension 生态 ```sql INSTALL httpfs; -- HTTP / S3 INSTALL spatial; -- 地理空间 INSTALL fts; -- 全文搜索 INSTALL postgres; -- 查 Postgres 表 INSTALL excel; -- 读写 .xlsx INSTALL sqlite; -- 读写 SQLite 文件 ``` `INSTALL postgres; LOAD postgres;` 后: ```sql ATTACH 'host=pg.example.com dbname=app user=...' AS pg (TYPE postgres); SELECT * FROM pg.public.orders LIMIT 10; ``` 把 Postgres 表当本地表查 + JOIN 本地 CSV → 异构数据查询。 ## 踩过的坑 1. **大数据 > RAM 时**:DuckDB 用 disk spilling 但仍可能慢。`SET memory_limit='10GB'`,留剩余给 OS。 2. **column type 自动推断错**:CSV 列 sometime "N/A",DuckDB 推断 string。`read_csv_auto(..., types={'col': 'INTEGER'})` 显式。 3. **更新慢**:DuckDB 是 OLAP,不适合频繁 UPDATE。点查 / 单行更新 → 用 SQLite。 4. **并发写不行**:单写者,多 reader。Web app 多 worker 同时写 DuckDB 会锁。 5. **extension 版本不匹配**:DuckDB 升级后 extension cache 旧版本 报错。`FORCE INSTALL <ext>` 强制更新。