[{"content":"本文展示博客新支持的富文本功能：Callout 提示块、折叠内容、脚注增强和 ECharts 交互图表。\nCallout 提示块 Callout 是一种突出重要信息的视觉方式，支持四种类型：\n示例 tip - 提示信息 info - 背景信息 warning - 注意事项 danger - 危险警告 提示 当你学习新概念时，尝试用自己的话复述一遍，这能加深理解。 背景信息 Transformer 架构最早由 Google 在 2017 年的论文《Attention Is All You Need》中提出。 注意事项 这个配置选项在生产环境中不建议修改，可能导致服务不稳定。 危险警告 执行此操作将删除所有数据，且无法恢复。请务必确认已备份重要文件。 用法 1 2 3 {{\u0026lt; callout \u0026#34;tip\u0026#34; \u0026#34;标题\u0026#34; \u0026gt;}} 内容 {{\u0026lt; /callout \u0026gt;}} 类型可选：tip、info、warning、danger\n折叠内容 适用于隐藏较长但需要时可见的内容：\n点击展开：分布式训练的核心挑战 分布式训练面临三个核心挑战：\n通信开销 - 多节点间需要同步梯度，带宽成为瓶颈 负载均衡 - 不同计算任务耗时不同，需要动态调整 容错处理 - 长训练任务中节点故障如何恢复 1 2 3 4 # 示例：简单的梯度同步 for param in model.parameters(): dist.all_reduce(param.grad) param.grad /= world_size 点击展开：矩阵乘法优化技巧 矩阵乘法是深度学习中最耗时的操作之一。以下是几个常用优化技巧：\nTiling - 将大矩阵分块以提高缓存命中率 向量化 - 使用 SIMD 指令一次处理多个数据 混合精度 - 使用 FP16/BF16 减少计算量 用法 1 2 3 4 5 6 \u0026lt;details\u0026gt; \u0026lt;summary\u0026gt;点击展开：标题\u0026lt;/summary\u0026gt; 这里是隐藏的内容... \u0026lt;/details\u0026gt; 脚注增强 脚注用于添加补充说明而不打断正文流1：\n分布式系统的 CAP 理论指出，一个分布式系统无法同时满足一致性（Consistency）、可用性（Availability）和分区容错性（Partition tolerance）2。\n当你设计系统时，需要根据业务场景权衡这三个特性。比如对于金融交易系统，一致性是首要考虑3。\nECharts 交互图表 ECharts 支持多种交互式图表：\n1. 折线图 - 训练曲线 加载图表中... 2. 柱状图 - 性能对比 加载图表中... 3. 饼图 - 资源分配 加载图表中... 用法 1 2 3 {{\u0026lt; echarts \u0026#34;chart-id\u0026#34; \u0026#34;400px\u0026#34; \u0026gt;}} {\u0026#34;xAxis\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;category\u0026#34;, \u0026#34;data\u0026#34;: [\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;]}, \u0026#34;series\u0026#34;: [{\u0026#34;type\u0026#34;: \u0026#34;line\u0026#34;, \u0026#34;data\u0026#34;: [1, 2]}]} {{\u0026lt; /echarts \u0026gt;}} 总结 以上功能可以显著提升技术博客的表达能力：\n功能 适用场景 类型 Callout 重点提示、警告、信息 视觉强调 折叠内容 技术细节、代码、扩展阅读 内容组织 脚注 参考文献、术语解释 学术规范 ECharts 数据分析、性能对比 交互图表 有问题或建议？欢迎反馈！\n脚注会自动编号并显示在文章末尾。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEric Brewer, \u0026ldquo;Towards Robust Distributed Systems\u0026rdquo;, PODC, 2000.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n在证券交易系统中，任何不一致都可能导致交易错误，因此通常选择 CP 模型。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://pillumina.github.io/posts/demo/99-demo-features/","summary":"\u003cp\u003e本文展示博客新支持的富文本功能：Callout 提示块、折叠内容、脚注增强和 ECharts 交互图表。\u003c/p\u003e\n\u003ch2 id=\"callout-提示块\"\u003eCallout 提示块\u003c/h2\u003e\n\u003cp\u003eCallout 是一种突出重要信息的视觉方式，支持四种类型：\u003c/p\u003e\n\u003ch3 id=\"示例\"\u003e示例\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003etip\u003c/code\u003e - 提示信息\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003einfo\u003c/code\u003e - 背景信息\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ewarning\u003c/code\u003e - 注意事项\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003edanger\u003c/code\u003e - 危险警告\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\n  \u003cdiv class=\"callout-header\"\u003e\u003csvg class=\"callout-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cpath d=\"M12 16v-4M12 8h.01\"/\u003e\u003c/svg\u003e\u003cspan class=\"callout-title\"\u003e提示\u003c/span\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"callout-body\"\u003e\n    当你学习新概念时，尝试用自己的话复述一遍，这能加深理解。\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"callout callout-info\"\u003e\n  \u003cdiv class=\"callout-header\"\u003e\u003csvg class=\"callout-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003ccircle cx=\"12\" cy=\"12\" r=\"10\"/\u003e\u003cline x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"/\u003e\u003cline x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"/\u003e\u003c/svg\u003e\u003cspan class=\"callout-title\"\u003e背景信息\u003c/span\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"callout-body\"\u003e\n    Transformer 架构最早由 Google 在 2017 年的论文《Attention Is All You Need》中提出。\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"callout callout-warning\"\u003e\n  \u003cdiv class=\"callout-header\"\u003e\u003csvg class=\"callout-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/\u003e\u003cline x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/\u003e\u003cline x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/\u003e\u003c/svg\u003e\u003cspan class=\"callout-title\"\u003e注意事项\u003c/span\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"callout-body\"\u003e\n    这个配置选项在生产环境中不建议修改，可能导致服务不稳定。\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"callout callout-danger\"\u003e\n  \u003cdiv class=\"callout-header\"\u003e\u003csvg class=\"callout-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpolygon points=\"7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2\"/\u003e\u003cline x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/\u003e\u003cline x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/\u003e\u003c/svg\u003e\u003cspan class=\"callout-title\"\u003e危险警告\u003c/span\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"callout-body\"\u003e\n    执行此操作将删除所有数据，且无法恢复。请务必确认已备份重要文件。\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"用法\"\u003e用法\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-markdown\" data-lang=\"markdown\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nt\"\u003ecallout\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"na\"\u003etip\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"na\"\u003e标题\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e}}\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e内容\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e{{\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u003c/span\u003e\u003cspan class=\"nt\"\u003ecallout\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e}}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e类型可选：\u003ccode\u003etip\u003c/code\u003e、\u003ccode\u003einfo\u003c/code\u003e、\u003ccode\u003ewarning\u003c/code\u003e、\u003ccode\u003edanger\u003c/code\u003e\u003c/p\u003e","title":"博客富文本新功能：Callout、折叠、脚注、ECharts"},{"content":"Ascend Profiling Analysis Skill 设计深度解析 本文深度解析一个用于分析 Ascend NPU torch profiler 产出的 skill，涵盖其设计哲学、Pipeline 架构、昇腾核心知识体系和先验知识体系。\n一、背景与动机 为什么需要 profiling 分析？ 在昇腾 NPU 上运行 LLM 推理时，的性能调优需要回答几个关键问题：\nStep 时间去哪了？ attention/FFN/MoE 各占多少？ 瓶颈在哪？ Cube 计算还是 Vector 内存搬运？ EP/TP 负载均衡吗？ 有没有 rank 掉队？ 通信是否拖后腿？ HCCL collective 是否慢于预期？ 传统的分析手段面临几个问题：\n工具 问题 CANN Studio Timeline 只能看时序，无法聚合统计 trace_view.json 数据稀疏，难以关联到 kernel 语义 kernel_details.csv 数据量级 GB，需要专门解析逻辑 设计目标 这个 skill 的核心目标：从原始 profiling 数据出发，产出带证据链的可追溯报告。\n每一条诊断结论都必须能追溯到原始 CSV 的行号 支持跨 rank 对齐和异常检测 输出 Markdown / Excel / HTML 三种格式 二、设计哲学：证据链优先 核心理念 每个 claim 必须能追溯到原始 row。\n1 2 3 4 5 report claim → diagnosis finding (diagnosis_findings.json) → evidence id (evidence_index.csv) → source path + row range (raw_kernel_index.csv) → original kernel_details.csv 置信度分层 置信度 条件 处理方式 high 直接 row 证据 + 交叉验证一致 直接输出 medium 直接证据存在，但缺少一个佐证 标注后输出 low 模式可疑，但覆盖不完整 标注 limitation 结构 Role vs 实现 Evidence 这是理解整个 skill 的关键：\n结构 Role = Paper 术语，表示\u0026quot;这是什么功能块\u0026quot;（MLA、DSA、CSA） 实现 Evidence = Kernel 分类，表示\u0026quot;哪些 kernel 实现这个功能\u0026quot; 1 2 3 4 5 6 7 8 9 10 11 # 结构 Role (attention_families.yaml) - family: csa # Compressed Sparse Attention must_have: - attention.kv_compressor # KV 压缩 - attention.lightning_indexer # top-k 选择 - attention.sparse_sharedkv # 稀疏 KV 共享 # 实现 Evidence (kernel_signatures.yaml) - profile_name: fusedinferattentionscore categories: [attention.flash_score] used_by_family: [mla, dsa, hca, gqa_or_mha] 关键洞察：同一个 kernel（如 FusedInferAttentionScore）可以服务多个 architecture。Family 由组合判断，非单个 kernel。\n确定性优先 1 2 3 4 # segment.py 开头的设计原则 The segmenter is intentionally deterministic. It does not use duration percentiles, fuzzy similarity, expected model layer counts, or \u0026#34;best score\u0026#34; candidate selection. 层数 = 观测结果，非假设 不写死模型名 宁可低置信度输出，不输出不可追溯结论 三、Pipeline 架构 阶段划分 normalize → segment → classify → summarize → cross_rank → diagnostics → report ↓ ↓ ↓ ↓ ↓ ↓ ↓ 原始CSV Step切分 Block分解 聚合统计 跨Rank对齐 诊断发现 报告生成 阶段可复用 1 2 3 4 # 从某个阶段重跑，复用上游结果 python3 profile_analyze.py \\ --from-stage classify \\ --remote-output-dir /tmp/previous_run 远端执行策略 Profiling 数据可能几十 GB，绝不能全量拉回本地。\n1 2 3 # 1. tar-over-ssh 只同步分析框架 (~10MB) # 2. 远端容器内执行分析 # 3. 只拉回轻量产物 (report/, *manifest.json, *.csv) Artifact 契约 每个阶段的产出文件固定，其他阶段只依赖 manifest.json 校验。\nsegment_manifest.json # 切分段数、层数、hard_errors classify_manifest.json # block 统计、companion_layers summary_manifest.json # pipeline 覆盖率 四、昇腾核心知识体系 AICore vs AIVector 解耦架构 Atlas A2/A3 的关键架构特征：Cube 和 Vector 是两个独立的执行单元。\n┌─────────────────────────────────────────────────────┐ │ NPU Die │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ AI Core │ │ AI Vector │ │ │ │ (Cube) │ │ (Vector) │ │ │ │ │ │ │ │ │ │ ┌────────┐ │ │ ┌────────┐ │ │ │ │ │ MAC │ │ │ │ ALU │ │ │ │ │ │(矩阵乘) │ │ │ │(向量运算)│ │ │ │ │ └────────┘ │ │ └────────┘ │ │ │ └──────────────┘ └──────────────┘ │ │ ↑ ↑ │ │ └────────┴────────────┘ │ │ 解耦执行，互不阻塞 │ └─────────────────────────────────────────────────┘ Pipeline 时间字段映射 kernel_details.csv 暴露了 11 个 pipeline 时间字段（单位 μs）：\nPipeline Stage CSV Column 含义 AIC matmul aic_mac_time Cube 矩阵乘 AIC fixpipe aic_fixpipe_time 写回/量化 AIC mte1 aic_mte1_time GM → L0 AIC mte2 aic_mte2_time GM → L0A/B AIC scalar aic_scalar_time AIC 标量指令 AIV vec aiv_vec_time Vector ALU AIV mte2 aiv_mte2_time GM → UB AIV mte3 aiv_mte3_time UB → GM AIV scalar aiv_scalar_time AIV 标量指令 为什么 AIC mte2 ≠ AIV mte2 两者走的是完全不同的内存路径：\n1 2 3 4 5 6 # 正确做法：分开统计 aic_mte_total = aic_mte1_time + aic_mte2_time # AIC 侧 aiv_mte_total = aiv_mte2_time + aiv_mte3_time # AIV 侧 # 错误做法：合并掩盖真相 # merged_mte = aic_mte2 + aiv_mte2 # 混淆两种路径 如果一个算子受困于 AIV 的 GM→UB 压力，错误合并会被误诊断为 Cube 侧 mte2 问题。\nop_type 分类体系 基于 Accelerator Core 列和 kernel name 的粗粒度分类：\nop_type 触发条件 含义 aic Core = AI_CORE 纯 Cube 算子 aiv Core = AI_VECTOR_CORE 纯 Vector 算子 mix_cv Core = MIX_AIC/MIX_AIV Cube+Vector 同时运行（FlashAttention、GroupedMatmul） mix_comm_aiv Core = COMMUNICATION 且 AIV \u0026gt; 0 通信+AIV 融合（DispatchFFNCombine） communication Core = COMMUNICATION 且 AIV = 0 纯 HCCL 集合通信 aicpu Core = AI_CPU Host 侧算子 mix_comm_aiv 的设计意图：当 CANN 报告 DispatchFFNCombine 时，Accelerator Core = COMMUNICATION。但实际上有一半运行时在 AIV 上跑。我们检查非零 AIV 时间并重新标记，让报告能单独归因 AIV 负担。\n五、Step / Layer / Block 分解 Step 切分 基于 selection/sampling kernel 定位 step boundary：\n1 2 3 4 # segment.py if \u0026#34;argmax\u0026#34; in text or \u0026#34;applytopktopp\u0026#34; in text: role = \u0026#34;selection\u0026#34; primary_roles.add(\u0026#34;selection\u0026#34;) Step 切分的结果：\nStep 0: [head] → [layer_0, layer_1, ..., layer_N] → [tail] ↑ ↑ ↑ 启动开销 主计算 采样/输出 每个 step 分解为四个时间桶：\n时间桶 计算方式 含义 head step.start → layer[0].start 启动/调度开销 main layer[0].start → layer[-1].end 主计算 tail layer[-1].end → step.end 后处理 bubble wall - busy 设备空闲时间 Layer 切分 按 anchor（attention / MoE / matmul / block_head）构建 layer observations：\n1 2 3 4 5 6 7 8 9 10 # segment.py def anchor_kind(event): if has_attention_role(event): return primary_attention_category(event) # attention.flash_score, etc. if has_moe_role(event): return primary_moe_category(event) # moe.dispatch, etc. if has_matmul(event) and not is_collective(event): return \u0026#34;compute.matmul\u0026#34; if is_block_head(event): return \u0026#34;block_head\u0026#34; 关键设计：层数是观测结果，不是先验假设。观测到什么就是什么。\nBlock 分解 每个 layer 最多切分为 2 个 block：\n1 2 3 4 标准 transformer layer: dense layer → [attention block] + [ffn block] MoE layer → [attention block] + [moe block] companion layer → [moe block] (无 attention) Block 边界由 row 中点 决定，而非时间中点：\n1 2 3 4 5 # block_taxonomy.md # Both present: split at the midpoint between last attention row # and first MoE row. # Why row-midpoint instead of time-midpoint: # row order matches on-device sequencing and is independent of stream skew 六、Bound 分类 11 个 pipeline 字段 → 5 个 family 1 2 3 4 5 6 7 8 # pipeline_taxonomy.md FAMILY_MAPPING = { \u0026#34;cube\u0026#34;: [\u0026#34;aic_mac_time\u0026#34;, \u0026#34;aic_fixpipe_time\u0026#34;], \u0026#34;vector\u0026#34;: [\u0026#34;aiv_vec_time\u0026#34;], \u0026#34;aic_mte\u0026#34;: [\u0026#34;aic_mte1_time\u0026#34;, \u0026#34;aic_mte2_time\u0026#34;], \u0026#34;aiv_mte\u0026#34;: [\u0026#34;aiv_mte2_time\u0026#34;, \u0026#34;aiv_mte3_time\u0026#34;], \u0026#34;scalar\u0026#34;: [\u0026#34;aic_scalar_time\u0026#34;, \u0026#34;aiv_scalar_time\u0026#34;], } Dominant Core vs Bound Stage 1 2 3 # 关键区分 dominant_core = AIC 侧时间 \u0026gt; AIV 侧时间 ? \u0026#34;aic\u0026#34; : \u0026#34;aiv\u0026#34; # 而 bound_stage 是 9 个 sub-stage 中累计耗时最大的 1 2 3 4 5 6 7 8 9 # bound_classification.md # 计算每个子 stage 的累计时间，返回最大的 def bound_stage(pipeline_us): stages = { \u0026#34;mac\u0026#34;: pipeline_us.get(\u0026#34;aic_mac_time\u0026#34;, 0), \u0026#34;mte1\u0026#34;: pipeline_us.get(\u0026#34;aic_mte1_time\u0026#34;, 0), # ... } return max(stages, key=stages.get) 七、Class 分组：Shape-strict Equality 设计原则 Shape 必须完全一样才可以。跨 DP 的话可能存在两个 step 一个 shape 为 (3, 4)，另一个 shape 为 (4, 3)，这种情况也视为两种不同的 step。\n1 2 3 4 5 6 7 8 9 10 11 # step_class_grouping.md # 1. 顺序敏感的 tuple pairs = [(name1, shape1), (name2, shape2)] # 3×4 ≠ 4×3 # 2. 缺失 shape 不合并 if not pairs: return f\u0026#34;{prefix}_unknown_shape_{digest}\u0026#34; # 3. BLAKE2b 哈希确保确定性 payload = json.dumps([structure, scope_label, pairs]) digest = blake2b(payload.encode(), digest_size=8).hexdigest() Aggregate 计算策略 1 2 3 4 # step_class_grouping.md # wall_ms_mean: 算术平均 # wall_ms_sum: 总贡献 = Σ(member_count × wall_mean) # bound_family: 在 pipeline 聚合上重新计算 八、先验知识体系 这是 skill 最核心的创新：通过 YAML 声明式地管理模型架构知识。\n三层架构 ┌─────────────────────────────────────────────────────────┐ │ Layer 3: model_architectures.yaml │ │ HF arch → (attention_family, ffn_family, block_pattern)│ │ 用于诊断异常，不驱动切分 │ ├─────────────────────────────────────────────────────────┤ │ Layer 2: attention_families.yaml │ │ kernel category 组合 → paper-aligned family name │ │ CSA/HCA/DSA/MLA/linear/GQA_MHA │ ├─────────────────────────────────────────────────────────┤ │ Layer 1: kernel_signatures.yaml │ │ profile kernel name → categories + roles │ │ 100+ 条映射，覆盖所有见过的 kernel │ └─────────────────────────────────────────────────────────┘ Attention Family 检测（7 种） 1 2 3 4 5 6 7 8 9 # attention_families.yaml 决策顺序 decision_order: 1. CSA: kv_compressor + lightning_indexer + sparse_sharedkv 2. HCA: kv_compressor + flash_score (无 indexer/sparse) 3. DSA: lightning_indexer + sparse_sharedkv (无 compressor) 4. MLA: mlaprolog / kvrmsnormropecache 等 5. linear: causalconv / mamba 等 6. gqa_or_mha: flash_score (无架构特定伴随) 7. attn: 未知 关键洞察：Kernel Category 中性设计 1 2 3 4 5 6 7 8 9 10 11 # common.py categories_and_roles # FusedInferAttentionScore 支持 MHA/GQA/MLA if \u0026#34;fusedinferattentionscore\u0026#34; in text: categories.add(\u0026#34;attention.flash_score\u0026#34;) # 不加 mha/gqa/mla——同一 kernel 可以服务多个架构 # Family 由 block 内的 category 组合决定 def resolve_attention_family(categories): if \u0026#34;kv_compressor\u0026#34; in cats and \u0026#34;lightning_indexer\u0026#34; in cats: return \u0026#34;csa\u0026#34; # ... 代码片段：categories_and_roles 结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # common.py (约 350 行) def categories_and_roles(name, task_type, accelerator_core): \u0026#34;\u0026#34;\u0026#34;Classify one kernel into op_categories + op_roles. Rule order and signatures mirror kernel_signatures.yaml. \u0026#34;\u0026#34;\u0026#34; text = fold_text(f\u0026#34;{name} {task_type} {accelerator_core}\u0026#34;) categories = set() roles = set() # --- Communication --- if any(token in text for token in (\u0026#34;hccl\u0026#34;, \u0026#34;hcom\u0026#34;, \u0026#34;allreduce\u0026#34;, ...)): categories.add(\u0026#34;communication.collective\u0026#34;) roles.add(\u0026#34;communication\u0026#34;) # --- Attention: sparse-attention building blocks --- # CSA 和 DSA 共用这些 kernel if \u0026#34;sparseattnsharedkv\u0026#34; in text: if \u0026#34;metadata\u0026#34; in text: categories.add(\u0026#34;attention.sparse_sharedkv.metadata\u0026#34;) else: categories.add(\u0026#34;attention.sparse_sharedkv\u0026#34;) # --- Attention: MLA (DeepSeek V2/V3) --- if \u0026#34;mlapreprocess\u0026#34; in text or \u0026#34;mlaprolog\u0026#34; in text: categories.add(\u0026#34;attention.mla.preprocess\u0026#34;) categories.add(\u0026#34;attention.mla\u0026#34;) # --- MoE --- if \u0026#34;dispatchffncombine\u0026#34; in text: categories.add(\u0026#34;moe.dispatch_expert_compute\u0026#34;) # ... return (tuple(sorted(categories)), tuple(sorted(roles))) 代码片段：resolve_attention_family 决策流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # common.py def resolve_attention_family(categories): cats = set(categories) has_compressor = \u0026#34;attention.kv_compressor\u0026#34; in cats has_indexer = \u0026#34;attention.lightning_indexer\u0026#34; in cats has_sparse_sharedkv = \u0026#34;attention.sparse_sharedkv\u0026#34; in cats has_flash_score = \u0026#34;attention.flash_score\u0026#34; in cats has_mla_marker = bool(_MLA_CATEGORIES \u0026amp; cats) if has_compressor and has_indexer and has_sparse_sharedkv: return \u0026#34;csa\u0026#34; # Compressed Sparse Attention elif has_compressor and has_flash_score and not has_indexer: return \u0026#34;hca\u0026#34; # Heavily Compressed Attention elif has_indexer and has_sparse_sharedkv and not has_compressor: return \u0026#34;dsa\u0026#34; # DeepSeek Sparse Attention elif has_mla_marker and not (has_compressor or has_indexer): return \u0026#34;mla\u0026#34; # Multi-head Latent Attention elif \u0026#34;attention.linear_or_mamba\u0026#34; in cats: return \u0026#34;linear\u0026#34; elif has_flash_score: return \u0026#34;gqa_or_mha\u0026#34; # 伞形，需要 shape 细化 else: return \u0026#34;attn\u0026#34; KVComp Overlay 1 2 3 # 如果存在 Hamming-distance KV 剪枝，叠加 +kvc 后缀 if \u0026#34;attention.kvcomp.topk\u0026#34; in categories: base = f\u0026#34;{base}+kvc\u0026#34; # mla+kvc, dsa+kvc, etc. 扩展新 Kernel 的流程 1 2 3 4 5 6 7 8 9 10 11 12 # 1. kernel_signatures.yaml 添加映射 - profile_name: newkernel categories: [attention.new_category] roles: [attention] evidence: - \u0026#34;vllm-ascend/path/to/kernel:123\u0026#34; # 2. attention_families.yaml 更新 must_have 组合 # 3. semantic_conventions.yaml 添加新 enum 值 # 4. Python 测试确保不破坏现有逻辑 九、通信诊断 HCCL 两层事件 Layer 出现位置 代表含义 Op-level kernel_details.csv 的 COMMUNICATION 行 用户发起的集合通信 Task-level communication.json (level 1) 集合内部的任务分解 Op-kind Taxonomy 1 2 3 4 5 6 7 hccl_op_kind_map = { \u0026#34;HCOM_ALLREDUCE_*\u0026#34;: \u0026#34;allreduce\u0026#34;, \u0026#34;HCOM_ALLGATHER_*\u0026#34;: \u0026#34;allgather\u0026#34;, \u0026#34;HCOM_REDUCESCATTER_*\u0026#34;: \u0026#34;reducescatter\u0026#34;, \u0026#34;HCOM_ALLTOALLV_*\u0026#34;: \u0026#34;alltoallv\u0026#34;, \u0026#34;DispatchFFNCombine\u0026#34;: \u0026#34;comm_aiv_fused\u0026#34;, # 特殊：混合 AIV } Notify Wait 诊断 1 2 3 4 # communication_taxonomy.md # Task-level 的 Notify Wait 是暴露 peer 等待的关键 - 症状: one rank\u0026#39;s Notify Wait \u0026gt; 50% of collective wall - 含义: 该 rank 在等待更慢的 peer 诊断发现类型 1 2 3 4 5 6 finding_type: - communication_collective_slow # rank 间 duration skew \u0026gt; 30% - ep_load_imbalance_suspected # EP 路由不均 - slow_rank_suspected # 某个 rank 掉队 - dp_workload_imbalance # DP 负载不均 - reduced_work_or_dummy_rank # 陪跑/dummy rank 十、报告结构 章节布局 Executive Summary — 关键指标一览 Capture And Segmentation — 采集和切分统计 Macro Step Timeline — per-rank step 时长分位数 Pipeline Coverage — AIC/AIV 各 stage 占比 Step Class View — top step classes 按 wall 贡献排序 Layer And Block View — block_kind 分解 Operator View — top 算子 + HCCL 汇总 Cross-Rank Anomaly — 跨 rank 异常 Evidence Chain — 证据索引 HTML 报告特性 单文件零依赖 Single-step Inspector（点击 step 查看详情） Bubble tracing axis 可缩放多流时间轴 46 字段算子卡（带 raw kernel_details row 追溯） 十一、Skill 的优势与限制 优势 证据链可追溯 — 每条结论都能追溯到原始 CSV 行 Kernel category 中性 — 易于扩展新 kernel Paper 术语与实现解耦 — csa/dsa/mla vs kv_compressor/lightning_indexer 远端执行 — 避免大文件传输 确定性切分 — 不依赖模糊匹配 限制 不读 HF config.json — 无法直接确认 model arch\n原因：skill 只看到 ascend_pt/ 输出，无 config 访问权限 影响：block_pattern_unexpected 诊断无法自动关联到 HF arch Shape 可能缺失 — acl-graph compile 会擦除 Input Shapes\n影响：GQA/MHA/MQA 细化可能失败，保持伞形 gqa_or_mha HCA 检测是 heuristic — 缺少确认样本\n误报可能：V3.2 + KV 压缩在非 CSA 层 Task-level 通信数据需要 level 1 profiling\nLevel 0 只有 op-level，Notify Wait 等诊断不可用 可改进点 改进项 当前状态 目标 YAML-driven matcher Python 代码镜射 YAML 直接读 YAML Shape 缺失 graceful degradation 直接失败 多级 fallback 诊断规则 YAML 化 Python 内联 diagnosis_rules.yaml HF config 集成 不可达 serving 侧传递 config 层结构指纹库 人工阅读 YAML 自动匹配已知架构 十二、关键设计原则总结 层数 = 观测结果，非假设\n不写死 24/27/36/40 层 不写死模型名 / 层数\n\u0026ldquo;layer_17\u0026rdquo; 只是个 hash，不是 \u0026ldquo;attention_layer_17\u0026rdquo; 宁可低置信度输出，不输出不可追溯结论\nkernel category 中性\n同一 kernel 可服务多个 architecture Paper 术语与实现解耦\ncsa/dsa/mla 是架构名 kv_compressor/lightning_indexer 是 kernel category AIV mte2 与 AIC mte2 必须分开\n两者走不同内存路径 Shape 缺失不合并\n保守策略：宁可 under-cluster 参考资料 Ascend Profiling Analysis Skill CANN HCCL 用户指南 DeepSeek-V4 Paper DeepSeek-V3.2 (DSA) ","permalink":"https://pillumina.github.io/posts/aiinfra/ascend-profiling-analysis-skill/","summary":"\u003ch1 id=\"ascend-profiling-analysis-skill-设计深度解析\"\u003eAscend Profiling Analysis Skill 设计深度解析\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本文深度解析一个用于分析 Ascend NPU torch profiler 产出的 skill，涵盖其设计哲学、Pipeline 架构、昇腾核心知识体系和先验知识体系。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"一背景与动机\"\u003e一、背景与动机\u003c/h2\u003e\n\u003ch3 id=\"为什么需要-profiling-分析\"\u003e为什么需要 profiling 分析？\u003c/h3\u003e\n\u003cp\u003e在昇腾 NPU 上运行 LLM 推理时，的性能调优需要回答几个关键问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eStep 时间去哪了？\u003c/strong\u003e attention/FFN/MoE 各占多少？\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e瓶颈在哪？\u003c/strong\u003e Cube 计算还是 Vector 内存搬运？\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEP/TP 负载均衡吗？\u003c/strong\u003e 有没有 rank 掉队？\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e通信是否拖后腿？\u003c/strong\u003e HCCL collective 是否慢于预期？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e传统的分析手段面临几个问题：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e工具\u003c/th\u003e\n          \u003cth\u003e问题\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eCANN Studio  Timeline\u003c/td\u003e\n          \u003ctd\u003e只能看时序，无法聚合统计\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003etrace_view.json\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e数据稀疏，难以关联到 kernel 语义\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003ekernel_details.csv\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e数据量级 GB，需要专门解析逻辑\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"设计目标\"\u003e设计目标\u003c/h3\u003e\n\u003cp\u003e这个 skill 的核心目标：\u003cstrong\u003e从原始 profiling 数据出发，产出带证据链的可追溯报告\u003c/strong\u003e。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e每一条诊断结论都必须能追溯到原始 CSV 的行号\u003c/li\u003e\n\u003cli\u003e支持跨 rank 对齐和异常检测\u003c/li\u003e\n\u003cli\u003e输出 Markdown / Excel / HTML 三种格式\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"二设计哲学证据链优先\"\u003e二、设计哲学：证据链优先\u003c/h2\u003e\n\u003ch3 id=\"核心理念\"\u003e核心理念\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e每个 claim 必须能追溯到原始 row。\u003c/strong\u003e\u003c/p\u003e","title":"Ascend Profiling Analysis Skill 设计深度解析"},{"content":" GitHub: Yuan1z0825/nature-skills\n作者：袁一哲（上海交大博士），专注医疗 AI\n核心定位 Nature Skills 是一套为学术论文全流程设计的 AI 工具集，涵盖：\n论文阅读与理解 各部分写作与润色 图表生成（Nature 风格） 审稿意见回复 引文检索与管理 设计原则：\n仅用一手来源：基于已发表 Nature 论文，非二手总结 显式优于隐式：每条规则都有明确理由 上下文感知：论文不同部分应用不同逻辑 输出优先：返回可直接使用的内容 安装 Claude Code（推荐） 1 2 3 /plugin marketplace add https://github.com/Yuan1z0825/nature-skills /plugin install nature-skills /reload-plugins Codex 打开 Codex Desktop 添加自定义插件市场 仓库源：https://github.com/Yuan1z0825/nature-skills.git 分支：main 安装 nature-skills 插件 手动安装 1 2 3 4 5 6 7 8 9 10 11 12 # 克隆仓库 git clone https://github.com/Yuan1z0825/nature-skills.git cd nature-skills # 安装单个 skill mkdir -p ~/.claude/skills cp -R skills/nature-reader ~/.claude/skills/ # 或安装所有 for d in skills/nature-*; do cp -R \u0026#34;$d\u0026#34; ~/.claude/skills/ done Skill 概览 Skill 状态 用途 触发示例 nature-reader Beta 论文全文阅读，原文对照 \u0026ldquo;read this paper\u0026rdquo; nature-paper2ppt Beta 论文转中文 PPT（journal club） \u0026ldquo;paper PPT\u0026rdquo;, \u0026ldquo;journal club\u0026rdquo; nature-writing Draft 论文各部分写作 \u0026ldquo;write abstract\u0026rdquo; nature-polishing Stable 论文润色（Nature 风格） \u0026ldquo;Nature style\u0026rdquo;, \u0026ldquo;polish\u0026rdquo; nature-citation Beta CNS 级别引文检索 \u0026ldquo;CNS citation\u0026rdquo; nature-response Beta 审稿意见回复 \u0026ldquo;rebuttal\u0026rdquo; nature-figure Stable Nature 风格图表 \u0026ldquo;Nature figure\u0026rdquo; nature-data Draft 数据可用性声明 \u0026ldquo;Data Availability\u0026rdquo; nature-academic-search Beta 学术搜索（PubMed） \u0026ldquo;search papers\u0026rdquo; Best Practices 按场景选择 Skill 场景 推荐 Skill 说明 论文精读 nature-reader 原文对照，关键信息提取 组会/实验室报告 nature-paper2ppt 自动生成故事线，中文幻灯片 论文写作（初稿） nature-writing 各部分写作模板 语言润色（定稿） nature-polishing Nature 风格精修 图表设计 nature-figure Nature 风格图表 引文管理 nature-citation CNS 级别检索 审稿回复 nature-response 礼貌有力 文献调研 nature-academic-search PubMed 搜索 nature-paper2ppt 使用流程 提供论文：可以是 PDF、arXiv ID、或论文 URL AI 分析：识别论文类型和论证逻辑 选择图表：从全文中选择关键图表 生成内容：中文幻灯片内容 + 演讲备注 创建文件：实际 PPTX 文件 自查纠错：检查图表质量、文字溢出、非模板视觉设计 关键原则：\n整片是一个连续的运动叙事 禁止 PowerPoint 切换风格 每张幻灯片之间要有视觉连贯性 nature-polishing 使用技巧 最佳时机：论文初稿完成后，最终提交前\n使用方式：\n润色这段摘要，用 Nature 风格 Polish this paragraph in Nature journal style 输出特点：\n学术表达优化 逻辑连贯性检查 语言地道性 nature-figure 使用指南 支持的图表类型：\n柱状图/折线图 热力图 流程图 散点图 ROC 曲线 混淆矩阵 使用方式：\n帮我画一个模型架构图 Create a ROC curve with this data nature-citation 检索策略 检索范围：CNS（Cell、Nature、Science）及子刊\n使用方式：\n检索这篇论文被哪些 Nature 文章引用过 Find CNS citations for this paper 输出格式：支持 EndNote、RIS、BibTeX\nnature-response 审稿回复 回复结构：\n感谢审稿人意见 逐条回应 修改说明（如果有） 再次感谢 使用方式：\n帮我写审稿意见回复 Write rebuttal letters 详细说明 nature-reader（论文阅读） 功能：\n论文全文阅读 原文对照模式 关键信息提取 nature-paper2ppt（论文转 PPT） Stable 版本，推荐用于 journal club 和组会。\n适用场景：\nJournal club 组会报告 论文分享 nature-polishing（论文润色） Stable 版本，推荐使用。\n功能：\nNature 风格语言润色 学术表达优化 逻辑连贯性检查 nature-figure（图表生成） Stable 版本，推荐使用。\n功能：\nNature 风格图表设计 多类型支持 配色方案优化 nature-citation（引文检索） 功能：\nCNS 级别期刊引文检索 引用格式导出 多格式支持 nature-response（审稿回复） 功能：\n审稿意见分析 回复模板生成 礼貌且有力的表达 nature-academic-search（学术搜索） 需要额外配置：\n1 2 # 安装 NCBI API（可选，获得更高 PubMed 速率限制） bash skills/nature-academic-search/install.sh your-email@example.com 使用方式 安装后直接用自然语言触发：\n示例 \u0026ldquo;帮我把这篇论文做成 journal club PPT\u0026rdquo; \u0026ldquo;润色这段摘要，用 Nature 风格\u0026rdquo; \u0026ldquo;帮我画一个模型架构图\u0026rdquo; \u0026ldquo;写一封审稿意见回复\u0026rdquo; \u0026ldquo;搜索这篇论文被哪些 CNS 文章引用过\u0026rdquo; MCP 配置（nature-academic-search 专用） 1 bash skills/nature-academic-search/install.sh your-email@example.com 设置 NCBI_API_KEY 可获得更高的 PubMed 速率限制。\n设计哲学 一手来源：所有建议基于已发表的 Nature 论文，非二手总结 显式规则：每条规则都有明确理由，可追溯 上下文感知：摘要/方法/结果/讨论各有不同处理逻辑 输出导向：返回内容可直接使用，无需二次编辑 💡 Tip: 学术写作相关需求优先使用 nature-skills，尤其是 nature-polishing 和 nature-figure 是 Stable 版本\n","permalink":"https://pillumina.github.io/posts/aiagent/claude-code-skills/nature-skills/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGitHub: \u003ca href=\"https://github.com/Yuan1z0825/nature-skills\"\u003eYuan1z0825/nature-skills\u003c/a\u003e\u003cbr\u003e\n作者：袁一哲（上海交大博士），专注医疗 AI\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"核心定位\"\u003e核心定位\u003c/h2\u003e\n\u003cp\u003eNature Skills 是一套为学术论文全流程设计的 AI 工具集，涵盖：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e论文阅读与理解\u003c/li\u003e\n\u003cli\u003e各部分写作与润色\u003c/li\u003e\n\u003cli\u003e图表生成（Nature 风格）\u003c/li\u003e\n\u003cli\u003e审稿意见回复\u003c/li\u003e\n\u003cli\u003e引文检索与管理\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e设计原则\u003c/strong\u003e：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e仅用一手来源\u003c/strong\u003e：基于已发表 Nature 论文，非二手总结\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e显式优于隐式\u003c/strong\u003e：每条规则都有明确理由\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e上下文感知\u003c/strong\u003e：论文不同部分应用不同逻辑\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e输出优先\u003c/strong\u003e：返回可直接使用的内容\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003ch3 id=\"claude-code推荐\"\u003eClaude Code（推荐）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/plugin marketplace add https://github.com/Yuan1z0825/nature-skills\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/plugin install nature-skills\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/reload-plugins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"codex\"\u003eCodex\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e打开 Codex Desktop\u003c/li\u003e\n\u003cli\u003e添加自定义插件市场\u003c/li\u003e\n\u003cli\u003e仓库源：\u003ccode\u003ehttps://github.com/Yuan1z0825/nature-skills.git\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e分支：\u003ccode\u003emain\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e安装 \u003ccode\u003enature-skills\u003c/code\u003e 插件\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"手动安装\"\u003e手动安装\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 克隆仓库\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://github.com/Yuan1z0825/nature-skills.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e nature-skills\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装单个 skill\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir -p ~/.claude/skills\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecp -R skills/nature-reader ~/.claude/skills/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 或安装所有\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e d in skills/nature-*\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  cp -R \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$d\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e ~/.claude/skills/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"skill-概览\"\u003eSkill 概览\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eSkill\u003c/th\u003e\n          \u003cth\u003e状态\u003c/th\u003e\n          \u003cth\u003e用途\u003c/th\u003e\n          \u003cth\u003e触发示例\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-reader\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eBeta\u003c/td\u003e\n          \u003ctd\u003e论文全文阅读，原文对照\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;read this paper\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-paper2ppt\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eBeta\u003c/td\u003e\n          \u003ctd\u003e论文转中文 PPT（journal club）\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;paper PPT\u0026rdquo;, \u0026ldquo;journal club\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-writing\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eDraft\u003c/td\u003e\n          \u003ctd\u003e论文各部分写作\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;write abstract\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-polishing\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eStable\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e论文润色（Nature 风格）\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;Nature style\u0026rdquo;, \u0026ldquo;polish\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-citation\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eBeta\u003c/td\u003e\n          \u003ctd\u003eCNS 级别引文检索\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;CNS citation\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-response\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eBeta\u003c/td\u003e\n          \u003ctd\u003e审稿意见回复\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;rebuttal\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-figure\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eStable\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eNature 风格图表\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;Nature figure\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-data\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eDraft\u003c/td\u003e\n          \u003ctd\u003e数据可用性声明\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;Data Availability\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003enature-academic-search\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eBeta\u003c/td\u003e\n          \u003ctd\u003e学术搜索（PubMed）\u003c/td\u003e\n          \u003ctd\u003e\u0026ldquo;search papers\u0026rdquo;\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003ch3 id=\"按场景选择-skill\"\u003e按场景选择 Skill\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e场景\u003c/th\u003e\n          \u003cth\u003e推荐 Skill\u003c/th\u003e\n          \u003cth\u003e说明\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e论文精读\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-reader\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e原文对照，关键信息提取\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e组会/实验室报告\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-paper2ppt\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e自动生成故事线，中文幻灯片\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e论文写作（初稿）\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-writing\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e各部分写作模板\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e语言润色（定稿）\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-polishing\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eNature 风格精修\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e图表设计\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-figure\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eNature 风格图表\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e引文管理\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-citation\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eCNS 级别检索\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e审稿回复\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-response\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e礼貌有力\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e文献调研\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003enature-academic-search\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003ePubMed 搜索\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"nature-paper2ppt-使用流程\"\u003enature-paper2ppt 使用流程\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e提供论文\u003c/strong\u003e：可以是 PDF、arXiv ID、或论文 URL\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAI 分析\u003c/strong\u003e：识别论文类型和论证逻辑\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e选择图表\u003c/strong\u003e：从全文中选择关键图表\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e生成内容\u003c/strong\u003e：中文幻灯片内容 + 演讲备注\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e创建文件\u003c/strong\u003e：实际 PPTX 文件\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e自查纠错\u003c/strong\u003e：检查图表质量、文字溢出、非模板视觉设计\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e关键原则\u003c/strong\u003e：\u003c/p\u003e","title":"Nature Skills - 学术论文工具集"},{"content":" GitHub: thedotmack/claude-mem\n文档: docs.claude-mem.ai\n版本: v6.5.0\n核心定位 Claude-Mem 是一个上下文持久化压缩系统，专门为 Claude Code 设计。它能够：\n自动捕获工具使用情况并生成语义摘要 将历史上下文在新会话中恢复 让 Claude 在跨会话场景下保持对项目的持续理解 关键洞察：Agent 的「记忆」应该是持久化的，而不仅仅是当前会话内的。\n安装 Claude Code（推荐） 1 2 3 /plugin marketplace add thedotmack/claude-mem /plugin install claude-mem /reload-plugins npx 一键安装 1 npx claude-mem install 其他 IDE 1 2 3 4 5 # Gemini CLI npx claude-mem install --ide gemini-cli # OpenCode npx claude-mem install --ide opencode 注意：npm 安装 claude-mem 仅安装 SDK，不注册插件钩子。务必使用 npx claude-mem install 或 /plugin 命令。\n前置要求 要求 最低版本 说明 Node.js 18+ Claude Code 插件支持 Bun - 自动安装，进程管理器 uv - 自动安装，向量搜索用 SQLite 3 - 自动安装，持久化存储 核心功能 功能 说明 Persistent Memory 上下文跨会话持久化 Progressive Disclosure 分层记忆检索，按需加载 mem-search Skill 自然语言查询项目历史 Web Viewer UI http://localhost:37777 Privacy Control \u0026lt;private\u0026gt; 标签排除敏感内容 Beta Channel Endless Mode 等实验功能 工作原理 核心组件 5 个生命周期钩子：SessionStart、UserPromptSubmit、PostToolUse、Stop、SessionEnd Worker Service：HTTP API 运行在 37777 端口，带 Web Viewer UI SQLite 数据库：存储会话、观察记录、摘要 mem-search Skill：自然语言查询 + 渐进式披露 Chroma 向量数据库：混合语义 + 关键词搜索 记忆流程 会话开始 → SessionStart 钩子 → 加载相关记忆 → 会话进行 → 工具调用 ↓ PostToolUse 钩子 → 生成观察记录 → 摘要生成 → 存入数据库 ↓ 会话结束 → SessionEnd 钩子 → 持久化上下文 Best Practices 三层工作流（必须遵循） Claude-Mem 提供 4 个 MCP 工具，采用三层工作流模式以节省 ~10x token：\n步骤 工具 目的 成本 1 search 获取紧凑索引 ~50-100 tokens/结果 2 timeline 获取时间上下文 ~variable 3 get_observations 仅对过滤后的 ID 获取完整详情 ~500-1000 tokens/每个 使用示例：\n1 2 3 4 5 6 7 8 // 步骤 1：搜索索引 search(query=\u0026#34;authentication bug\u0026#34;, type=\u0026#34;observations\u0026#34;, obs_type=\u0026#34;bugfix\u0026#34;, limit=20) // 步骤 2：获取相关 ID 的时间上下文 timeline(anchor=12345, depth_before=5, depth_after=5) // 步骤 3：仅对相关 ID 获取完整详情 get_observations(ids=[12345, 67890]) 关键规则：\n✅ 在 get_observations 中批量 ID（一次请求 vs N 次请求） ✅ 先搜索 → 再过滤 → 最后获取详情 ❌ 不要直接获取所有搜索结果的详情 隐私控制 使用 \u0026lt;private\u0026gt; 标签排除敏感内容：\n这个 API key 是 \u0026lt;private\u0026gt;sk-xxx\u0026lt;/private\u0026gt; 请不要记录 标签在钩子层被剥离，敏感信息永不进入数据库。\n多账户隔离 设置 CLAUDE_MEM_DATA_DIR 隔离不同项目/账户：\n1 2 3 4 5 # 工作账户 CLAUDE_MEM_DATA_DIR=~/.claude-mem-work npx claude-mem install # 个人账户（默认） npx claude-mem install 增量更新 首次运行后，后续仅处理变更文件：\nSHA256 缓存在 graphify-out/cache/ 未变更文件自动跳过 大型重构后用 graphify . --force 清除幽灵重复 上下文工程原则 \u0026ldquo;找到最小的高信号 token 集合，最大化期望结果的概率。\u0026rdquo;\n渐进式披露：从紧凑索引开始，仅在需要时展开详情。\nMCP 搜索工具 工具列表 工具 用途 search 语义搜索记忆索引，支持 type/date/project 过滤 timeline 获取特定观察的时间上下文 get_observations 按 ID 批量获取完整观察详情 observation_context 获取上下文摘要 使用示例 1 2 3 4 5 6 7 8 // 查找认证相关的 bug 修复 search(query=\u0026#34;authentication bug\u0026#34;, type=\u0026#34;observations\u0026#34;, obs_type=\u0026#34;bugfix\u0026#34;, limit=10) // 查找特定时间段的观察 search(query=\u0026#34;API refactor\u0026#34;, dateStart=\u0026#34;2026-01-01\u0026#34;, dateEnd=\u0026#34;2026-03-01\u0026#34;) // 组合过滤 search(query=\u0026#34;performance\u0026#34;, type=\u0026#34;observations\u0026#34;, project=\u0026#34;my-project\u0026#34;, limit=20) mem-search Skill 1 2 3 mem-search \u0026#34;how did we handle authentication?\u0026#34; mem-search \u0026#34;why was this architecture chosen?\u0026#34; mem-search \u0026#34;what was the issue with the database?\u0026#34; 基于自然语言的语义搜索记忆历史，返回关联的观察记录。\nWeb Viewer 访问 http://localhost:37777 查看：\n实时记忆流 观察记录列表 Beta 功能切换（Endless Mode） 配置管理 Bug 报告生成器 配置选项 配置文件位于 ~/.claude-mem/settings.json（首次运行自动创建）。\n模式与语言 1 2 3 { \u0026#34;CLAUDE_MEM_MODE\u0026#34;: \u0026#34;code--zh\u0026#34; } 模式 说明 code 默认英文模式 code--zh 简体中文模式 注意：code--zh 模式内置支持，无需额外安装。\n其他配置 AI 模型选择 Worker 端口 数据目录 日志级别 上下文注入控制 详见 Configuration Guide。\nBeta 功能 通过 Web Viewer UI（http://localhost:37777 → Settings）切换 stable/beta 频道。\nEndless Mode：生物启发的记忆架构，适合超长会话。\n详见 Beta Features。\n为什么值得用 超越关键词搜索：揭示隐含关系，而非仅匹配关键词 诚实的不确定性：AMBIGUOUS 标记，区分「找到」和「推测」 持久化：跨 session 可用，不依赖单一会话 增量友好：变化时只更新需要的部分 Token 效率：~10x 节省，通过渐进式披露 隐私保护：敏感信息永不进入数据库 故障排除 遇到问题时：\n1 2 cd ~/.claude/plugins/marketplaces/thedotmack npm run bug-report 自动诊断并生成报告。\n💡 Tip: 安装后重启 Claude Code，之前会话的上下文会自动出现在新会话中\n","permalink":"https://pillumina.github.io/posts/aiagent/claude-code-skills/claude-mem/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGitHub: \u003ca href=\"https://github.com/thedotmack/claude-mem\"\u003ethedotmack/claude-mem\u003c/a\u003e\u003cbr\u003e\n文档: \u003ca href=\"https://docs.claude-mem.ai/\"\u003edocs.claude-mem.ai\u003c/a\u003e\u003cbr\u003e\n版本: v6.5.0\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"核心定位\"\u003e核心定位\u003c/h2\u003e\n\u003cp\u003eClaude-Mem 是一个\u003cstrong\u003e上下文持久化压缩系统\u003c/strong\u003e，专门为 Claude Code 设计。它能够：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e自动捕获工具使用情况并生成语义摘要\u003c/li\u003e\n\u003cli\u003e将历史上下文在新会话中恢复\u003c/li\u003e\n\u003cli\u003e让 Claude 在跨会话场景下保持对项目的持续理解\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e关键洞察\u003c/strong\u003e：Agent 的「记忆」应该是持久化的，而不仅仅是当前会话内的。\u003c/p\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003ch3 id=\"claude-code推荐\"\u003eClaude Code（推荐）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/plugin marketplace add thedotmack/claude-mem\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/plugin install claude-mem\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/reload-plugins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"npx-一键安装\"\u003enpx 一键安装\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpx claude-mem install\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"其他-ide\"\u003e其他 IDE\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Gemini CLI\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpx claude-mem install --ide gemini-cli\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# OpenCode\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpx claude-mem install --ide opencode\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e注意\u003c/strong\u003e：npm 安装 \u003ccode\u003eclaude-mem\u003c/code\u003e 仅安装 SDK，不注册插件钩子。务必使用 \u003ccode\u003enpx claude-mem install\u003c/code\u003e 或 \u003ccode\u003e/plugin\u003c/code\u003e 命令。\u003c/p\u003e","title":"Skill: Claude-Mem - 记忆系统工具集"},{"content":" GitHub: safishamsi/graphify\n官网: graphifylabs.ai\nYC S26 加速器项目\n核心定位 在 AI 编程助手（Claude Code、Codex、Cursor 等）中输入 /graphify，它会将整个项目——代码、文档、PDF、图片、视频——映射成可查询的知识图谱。\n不再是 grep 文件，而是查询关系。\n安装 pip 安装 注意：官方包名是 graphifyy（双 y），PyPI 上其他 graphify* 包与其无关。\n1 2 3 4 5 6 7 8 # 推荐方式（uv 自动配置 PATH） uv tool install graphifyy # 或使用 pipx pipx install graphifyy # 或使用 pip pip install graphifyy 注册 Skill 1 graphify install 前置要求 要求 最低版本 检查命令 Python 3.10+ python --version uv（推荐） 任意 uv --version macOS：\n1 brew install python@3.12 uv Windows：\n1 winget install astral-sh.uv 快速开始 1 /graphify . 运行后会生成三个文件：\ngraphify-out/ ├── graph.html # 交互式可视化，浏览器打开，可点击节点、过滤、搜索 ├── GRAPH_REPORT.md # 审计报告：关键概念、意外连接、建议问题 └── graph.json # 完整图谱数据，随时可查询 支持的平台 平台 安装命令 Claude Code graphify install Codex graphify install --platform codex Cursor graphify cursor install Gemini CLI graphify install --platform gemini OpenCode graphify install --platform opencode VS Code Copilot graphify vscode install Aider graphify install --platform aider OpenClaw graphify install --platform claw Codex 用户：还需在 ~/.codex/config.toml 的 [features] 下添加 multi_agent = true。\nCodex 使用 $graphify 而非 /graphify。\n输出内容 报告内容 内容 说明 God nodes 项目中连接最多的概念，所有东西都流经这些节点 Surprising connections 跨文件/模块的意外连接，按意外程度排序 The \u0026ldquo;why\u0026rdquo; 内联注释（# NOTE:、# WHY:、# HACK:）、docstring，设计文档 Suggested questions 4-5 个图谱最适合回答的问题 Confidence tags 每个推断关系标记为 EXTRACTED/INFERRED/AMBIGUOUS 支持的文件类型 类型 扩展名 代码（33种语言） .py .ts .js .jsx .tsx .go .rs .java .c .cpp .rb .cs .kt .swift ... MCP 配置 .mcp.json mcp.json mcp_servers.json claude_desktop_config.json 文档 .md .mdx .html .txt .rst .yaml .yml Office .docx .xlsx（需安装 [office]） PDF .pdf 图片 .png .jpg .webp .gif 视频/音频 .mp4 .mov .mp3 .wav（需安装 [video]） 代码本地提取：使用 tree-sitter AST，无需 API 调用。其他类型通过 AI 助手模型 API 处理。\nBest Practices 三阶段提取管线 AST 提取（免费，本地）：tree-sitter 解析代码，提取函数、类、import、调用图、内联注释 视频/音频转录（本地）：faster-whisper，有缓存 语义提取（API 调用）：文档、PDF、图片通过 LLM 子代理 置信度标记 标记 含义 置信度 EXTRACTED 源码中直接找到 1.0 INFERRED LLM 推断 0.55-0.95 AMBIGUOUS 需人工复核 - 团队协作 推荐流程：\n1 2 3 4 5 6 7 8 9 # 1. 一个人运行并提交 /graphify . git add graphify-out/ \u0026amp;\u0026amp; git commit -m \u0026#34;Add knowledge graph\u0026#34; # 2. 其他人 pull 后，助手立即可用 git pull # 3. git commit 时自动重建 graphify hook install # 设置 git merge driver 推荐的 .gitignore 添加：\ngraphify-out/manifest.json # mtime-based，clone 后失效 graphify-out/cost.json # 本地 cost 跟踪 # graphify-out/cache/ # 可选：提交以加速 性能优化 1 2 3 4 5 6 7 8 9 10 11 # 大图谱（\u0026gt;5000 节点）：跳过 HTML 生成 graphify extract . --no-viz # 仅处理变更文件 graphify . --update # 强制重建（大型重构后） graphify . --force # 限制 VRAM（Ollama 本地推理） GRAPHIFY_OLLAMA_NUM_CTX=8192 graphify extract --token-budget 4000 增量更新 SHA256 缓存在 graphify-out/cache/ 未变更文件自动跳过 大型重构后用 --force 清除幽灵重复 Token 节省 查询方式 Token 消耗 读取原始文件 100% 使用图谱查询 ~1.4% benchmark: 52 文件语料库，71.5x token 节省。\n常用命令 构建图谱 1 2 3 4 5 /graphify . # 为当前目录构建图谱 /graphify ./docs --update # 仅重新提取变更文件 /graphify . --cluster-only # 重新聚类，不重新提取 /graphify . --no-viz # 跳过 HTML，仅生成报告 + JSON /graphify . --wiki # 从图谱构建 Markdown wiki 查询图谱 1 2 3 /graphify query \u0026#34;what connects auth to the database?\u0026#34; /graphify path \u0026#34;UserService\u0026#34; \u0026#34;DatabasePool\u0026#34; /graphify explain \u0026#34;RateLimiter\u0026#34; 高级功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 添加外部资源 /graphify add https://arxiv.org/abs/1706.03762 # 论文 /graphify add \u0026lt;youtube-url\u0026gt; # 视频转录 # 自动重建 graphify hook install # git commit 时自动重建 # 合并图谱 graphify merge-graphs a.json b.json # PR 仪表盘 graphify prs # CI 状态、review 状态、工作树映射 graphify prs --triage # AI 排序 review 队列 graphify prs --conflicts # 共享图谱社区的 PR（合并顺序风险） 聚类控制 1 2 3 4 5 # 更细粒度的社区 graphify . --cluster-only --resolution 1.5 # 抑制工具类超级节点 graphify . --cluster-only --exclude-hubs 99 可选扩展 扩展 用途 安装 pdf PDF 提取 pip install \u0026quot;graphifyy[pdf]\u0026quot; office Word/Excel 支持 pip install \u0026quot;graphifyy[office]\u0026quot; video 视频/音频转录 pip install \u0026quot;graphifyy[video]\u0026quot; mcp MCP stdio 服务器 pip install \u0026quot;graphifyy[mcp]\u0026quot; neo4j Neo4j 推送 pip install \u0026quot;graphifyy[neo4j]\u0026quot; gemini Gemini API pip install \u0026quot;graphifyy[gemini]\u0026quot; openai OpenAI API pip install \u0026quot;graphifyy[openai]\u0026quot; ollama 本地推理 pip install \u0026quot;graphifyy[ollama]\u0026quot; all 全部安装 pip install \u0026quot;graphifyy[all]\u0026quot; 忽略文件 创建 .graphifyignore（语法同 .gitignore）：\n# .graphifyignore node_modules/ dist/ *.generated.py # 仅索引 src/，忽略其他 * !src/ !src/** 为什么值得用 超越关键词搜索：揭示隐含关系，而非仅匹配关键词 诚实的不确定性：AMBIGUOUS 标记，区分「找到」和「推测」 持久化：跨 session 可用，输出可提交到 git 增量友好：变化时只更新需要的部分 多 Agent 支持：Claude Code、Codex、Cursor、Gemini CLI 等 超低 token：71.5x 节省 💡 Tip: 首次运行 /graphify .，之后代码库问题会直接通过图谱回答\n","permalink":"https://pillumina.github.io/posts/aiagent/claude-code-skills/graphify/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGitHub: \u003ca href=\"https://github.com/safishamsi/graphify\"\u003esafishamsi/graphify\u003c/a\u003e\u003cbr\u003e\n官网: \u003ca href=\"https://graphifylabs.ai\"\u003egraphifylabs.ai\u003c/a\u003e\u003cbr\u003e\nYC S26 加速器项目\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"核心定位\"\u003e核心定位\u003c/h2\u003e\n\u003cp\u003e在 AI 编程助手（Claude Code、Codex、Cursor 等）中输入 \u003ccode\u003e/graphify\u003c/code\u003e，它会将整个项目——代码、文档、PDF、图片、视频——映射成可查询的知识图谱。\u003c/p\u003e\n\u003cp\u003e不再是 grep 文件，而是\u003cstrong\u003e查询关系\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003ch3 id=\"pip-安装\"\u003epip 安装\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e注意\u003c/strong\u003e：官方包名是 \u003ccode\u003egraphifyy\u003c/code\u003e（双 y），PyPI 上其他 \u003ccode\u003egraphify*\u003c/code\u003e 包与其无关。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 推荐方式（uv 自动配置 PATH）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003euv tool install graphifyy\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 或使用 pipx\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003epipx install graphifyy\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 或使用 pip\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003epip install graphifyy\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"注册-skill\"\u003e注册 Skill\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egraphify install\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"前置要求\"\u003e前置要求\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e要求\u003c/th\u003e\n          \u003cth\u003e最低版本\u003c/th\u003e\n          \u003cth\u003e检查命令\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003ePython\u003c/td\u003e\n          \u003ctd\u003e3.10+\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003epython --version\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003euv（推荐）\u003c/td\u003e\n          \u003ctd\u003e任意\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003euv --version\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003emacOS\u003c/strong\u003e：\u003c/p\u003e","title":"Skill: graphify - 代码库转知识图谱"},{"content":" GitHub: greensock/gsap-skills\n官网: gsap.com\n核心定位 GSAP (GreenSock Animation Platform) 是专业的 JavaScript 动画库，HyperFrames 使用它作为主要动画引擎。\n重要更新：GSAP 完全免费，包括所有插件。自 Webflow 收购 GSAP 后，所有插件（包括 formerly Club-only 的 SplitText、MorphSVG 等）对所有人都免费使用，包括商业用途。\n安装 Claude Code 插件市场（推荐） 1 2 /plugin marketplace add greensock/gsap-skills /reload-plugins npx skills（通用） 1 npx skills add https://github.com/greensock/gsap-skills HyperFrames Contract HyperFrames 通过 window.__timelines 控制 GSAP，所有 timeline 必须遵循此约定：\n1 2 3 4 5 6 7 8 9 window.__timelines = window.__timelines || {}; const tl = gsap.timeline({ paused: true })); // 构建动画 tl.from(\u0026#34;.title\u0026#34;, { y: 48, opacity: 0, duration: 0.6, ease: \u0026#34;power3.out\u0026#34; }, 0); tl.to(\u0026#34;.accent\u0026#34;, { scaleX: 1, duration: 0.5, ease: \u0026#34;power2.out\u0026#34; }, 0.25); // 注册 — key 必须与 data-composition-id 匹配 window.__timelines[\u0026#34;main\u0026#34;] = tl; 关键规则：\n✅ Timeline 必须 { paused: true } ✅ 必须注册到 window.__timelines[\u0026quot;composition-id\u0026quot;] ✅ 注册 key 必须与 composition root 的 data-composition-id 匹配 ❌ 不要调用 tl.play() — HyperFrames 通过 seek 控制 ❌ 不要在 async/setTimeout 中创建 timeline ❌ 不要使用 repeat: -1 — 使用有限重复次数 核心方法 方法 说明 gsap.to(targets, vars) 从当前状态动画到目标状态（最常用） gsap.from(targets, vars) 从目标状态动画到当前状态（入场动画） gsap.fromTo(targets, fromVars, toVars) 显式指定起始和结束状态 gsap.set(targets, vars) 立即应用（duration: 0） Transform 别名（优先使用） GSAP 的 CSSPlugin 使用一致的变换顺序（translate → scale → rotation → skew），比原生 CSS 更可靠。\nGSAP 属性 等价 CSS x, y, z translateX/Y/Z (px) xPercent, yPercent translateX/Y in % scale, scaleX, scaleY scale rotation rotate (deg) rotationX, rotationY 3D rotate skewX, skewY skew transformOrigin transform-origin autoAlpha opacity + visibility hidden autoAlpha 优于 opacity：值为 0 时同时设置 visibility: hidden，避免元素仍可点击。\n常用配置项 属性 类型 说明 duration Number 秒数（默认 0.5） ease String 缓动函数 stagger Number/Object 错开时间：0.1 或 { amount: 0.3, from: \u0026quot;center\u0026quot; } repeat Number 有限次数（计算可见时长） yoyo Boolean 与 repeat 配合，方向交替 immediateRender Boolean from/fromTo 默认 true Timeline 位置参数 控制 tween 在时间线上的位置（第三个参数）：\n值 含义 0 绝对 0 秒 1 绝对 1 秒 \u0026quot;+=0.5\u0026quot; 前一个结束后 0.5 秒 \u0026quot;-=0.2\u0026quot; 前一个结束前 0.2 秒 \u0026quot;\u0026lt;\u0026quot; 与前一个同时开始 \u0026quot;\u0026gt;\u0026quot; 与前一个同时结束 \u0026quot;\u0026lt;0.2\u0026quot; 前一个开始后 0.2 秒 \u0026quot;labelName\u0026quot; 在指定标签处 1 2 3 4 tl.to(\u0026#34;.a\u0026#34;, { x: 100 }, 0); // 在 0s tl.to(\u0026#34;.b\u0026#34;, { y: 50 }, \u0026#34;+=0.5\u0026#34;); // 前一个结束后 0.5s tl.to(\u0026#34;.c\u0026#34;, { opacity: 0 }, \u0026#34;\u0026lt;\u0026#34;); // 与 .b 同时开始 tl.to(\u0026#34;.d\u0026#34;, { scale: 2 }, \u0026#34;\u0026lt;0.2\u0026#34;); // .c 开始后 0.2s Timeline 标签 命名关键时间点，便于阅读和维护：\n1 2 3 4 5 6 7 tl.addLabel(\u0026#34;intro\u0026#34;, 0); tl.to(\u0026#34;.a\u0026#34;, { x: 100 }, \u0026#34;intro\u0026#34;); tl.addLabel(\u0026#34;outro\u0026#34;, \u0026#34;+=0.5\u0026#34;); tl.to(\u0026#34;.b\u0026#34;, { opacity: 0 }, \u0026#34;outro\u0026#34;); // 用于预览 tl.seek(\u0026#34;outro\u0026#34;); 缓动函数 决定动画的时间曲线。常用场景推荐：\n场景 推荐 特点 入场 power3.out 快出慢停，自然 离场 power2.in 慢出快停 强调 elastic.out(1, 0.3) 弹性效果 按钮点击 back.out(1.7) 轻微回弹 线性 none 匀速，用于 scroll-driven 内置缓动：每个都有 .in、.out、.inOut 变体：\npower1–power4：基础曲线，数字越大越陡 back：带超调 bounce：弹跳 elastic：弹性 circ：圆弧 expo：指数 sine：正弦 Best Practices Timeline 模式 创建带默认值的 timeline：\n1 2 3 4 const tl = gsap.timeline({ paused: true, defaults: { duration: 0.5, ease: \u0026#34;power2.out\u0026#34; } }); 嵌套实现模块化：\n1 2 3 4 5 6 7 8 9 function createIntroAnimation() { const tl = gsap.timeline({ paused: true }); tl.to(\u0026#34;.title\u0026#34;, { y: 0, opacity: 1, duration: 0.8 }) .to(\u0026#34;.subtitle\u0026#34;, { y: 0, opacity: 1, duration: 0.6 }, \u0026#34;-=0.4\u0026#34;) .to(\u0026#34;.cta\u0026#34;, { scale: 1, opacity: 1, duration: 0.4 }, \u0026#34;-=0.2\u0026#34;); return tl; } window.__timelines[\u0026#34;intro\u0026#34;] = createIntroAnimation(); 交错入场动画：\n1 2 3 4 5 6 gsap.from(\u0026#39;.box\u0026#39;, { opacity: 0, y: 50, stagger: 0.1, // 每个元素延迟 0.1s duration: 0.8 }); 性能优化 最佳实践 说明 优先使用 transform 和 opacity x, y, scale, rotation, opacity 运行在合成器线程 使用 CSS will-change: transform 对动画元素启用 GPU 加速 使用 stagger 代替多个 tween 减少 DOM 操作 使用 gsap.quickTo() 处理高频更新 如鼠标跟随：```javascript let xTo = gsap.quickTo(\u0026quot;#id\u0026quot;, \u0026ldquo;x\u0026rdquo;, { duration: 0.4, ease: \u0026ldquo;power3\u0026rdquo; }); document.addEventListener(\u0026ldquo;mousemove\u0026rdquo;, e =\u0026gt; xTo(e.pageX)); | 使用 `autoAlpha` 代替 `opacity` | 0 时同时设置 visibility，避免点击穿透 | ### 常见错误 | 错误 | 正确做法 | |------|----------| | 调用 `tl.play()` | HyperFrames 通过 seek 控制 | | 动画 layout 属性 | 使用 scale/x/y | | `repeat: -1` | 计算精确次数 | | 链式 `delay` | 使用 timeline + 位置参数 | | async 中创建 timeline | 同步创建，注册后让 HyperFrames 控制 | | `gsap.from()` 用于无限循环 | 使用有限时长 | | SVG 同时使用 svgOrigin 和 transformOrigin | 二选一 | ## 无障碍支持 使用 `gsap.matchMedia()` 响应用户偏好： ```javascript let mm = gsap.matchMedia(); mm.add( { isDesktop: \u0026#34;(min-width: 800px)\u0026#34;, reduceMotion: \u0026#34;(prefers-reduced-motion: reduce)\u0026#34;, }, (context) =\u0026gt; { const { isDesktop, reduceMotion } = context.conditions; gsap.to(\u0026#34;.box\u0026#34;, { rotation: isDesktop ? 360 : 180, duration: reduceMotion ? 0 : 2 // 减少动画偏好时跳过动画 }); } ); 相关 Skills GSAP 技能体系包含多个专门 skill：\nSkill 用途 gsap-core 核心 API：tween、easing、stagger gsap-timeline Timeline 进阶：嵌套、标签、播放控制 gsap-scrolltrigger 滚动驱动动画（HyperFrames 通常不需要） gsap-plugins 插件：ScrollToPlugin、Flip、Draggable 等 gsap-utils 工具函数：clamp、mapRange、random 等 gsap-react React 集成：useGSAP、cleanup gsap-performance 性能优化 gsap-frameworks Vue、Svelte 等框架集成 💡 Tip: 参阅 gsap.com/docs 获取完整文档\n","permalink":"https://pillumina.github.io/posts/aiagent/claude-code-skills/gsap/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGitHub: \u003ca href=\"https://github.com/greensock/gsap-skills\"\u003egreensock/gsap-skills\u003c/a\u003e\u003cbr\u003e\n官网: \u003ca href=\"https://gsap.com\"\u003egsap.com\u003c/a\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"核心定位\"\u003e核心定位\u003c/h2\u003e\n\u003cp\u003eGSAP (GreenSock Animation Platform) 是专业的 JavaScript 动画库，HyperFrames 使用它作为主要动画引擎。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e重要更新\u003c/strong\u003e：GSAP 完全免费，包括所有插件。自 Webflow 收购 GSAP 后，所有插件（包括 formerly Club-only 的 SplitText、MorphSVG 等）对所有人都免费使用，包括商业用途。\u003c/p\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003ch3 id=\"claude-code-插件市场推荐\"\u003eClaude Code 插件市场（推荐）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/plugin marketplace add greensock/gsap-skills\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/reload-plugins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"npx-skills通用\"\u003enpx skills（通用）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpx skills add https://github.com/greensock/gsap-skills\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"hyperframes-contract\"\u003eHyperFrames Contract\u003c/h2\u003e\n\u003cp\u003eHyperFrames 通过 \u003ccode\u003ewindow.__timelines\u003c/code\u003e 控制 GSAP，所有 timeline 必须遵循此约定：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ewindow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003e__timelines\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003ewindow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003e__timelines\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e{};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etl\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003egsap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etimeline\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003epaused\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 构建动画\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"nx\"\u003etl\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;.title\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ey\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e48\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eopacity\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eduration\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.6\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eease\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;power3.out\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003etl\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eto\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;.accent\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003escaleX\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eduration\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eease\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;power2.out\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.25\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 注册 — key 必须与 data-composition-id 匹配\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"nb\"\u003ewindow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003e__timelines\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;main\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etl\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e关键规则\u003c/strong\u003e：\u003c/p\u003e","title":"Skill: GSAP - HyperFrames 动画引擎"},{"content":" GitHub: heygen-com/hyperframes\n文档: hyperframes.heygen.com\n核心定位 HyperFrames 是一个开源的 HTML 视频渲染框架，用「写 HTML 来渲染视频」的方式工作。最大的特点是 AI-First——AI Agent 天然会写 HTML，不需要额外学习。\n与 Remotion 的核心区别：\n特性 HyperFrames Remotion 编写方式 HTML + CSS + GSAP React 组件 (TSX) 构建步骤 无，.html 直接可用 需要 bundler 动画精度 Seekable，帧级精确 依赖 wall-clock 开源许可 Apache 2.0（完全开源） 自定义许可证（需付费） HyperFrames 借鉴了 Remotion 的设计思路，代码中保留了对其首创模式的致谢注释。两者的核心分歧在于：Agent 主要写什么。Remotion 选择 React 组件，HyperFrames 选择 HTML。\n安装 Claude Code 插件市场（推荐） 1 2 /plugin marketplace add heygen-com/hyperframes /reload-plugins npx skills（通用） 1 npx skills add heygen-com/hyperframes CLI 工具 1 2 3 4 5 6 7 8 9 10 11 12 # 全局安装 CLI npm install -g hyperframes # 初始化新项目 hyperframes init my-video cd my-video # 开发预览 hyperframes preview # 浏览器预览，live reload # 渲染输出 hyperframes render # 输出 MP4 前置要求：Node.js \u0026gt;= 22, FFmpeg\n核心概念 Composition（视频构成） 视频由多个 clips（片段）组成，通过 HTML data 属性定义时序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 \u0026lt;div id=\u0026#34;stage\u0026#34; data-composition-id=\u0026#34;my-video\u0026#34; data-start=\u0026#34;0\u0026#34; data-width=\u0026#34;1920\u0026#34; data-height=\u0026#34;1080\u0026#34;\u0026gt; \u0026lt;!-- 视频片段 --\u0026gt; \u0026lt;video id=\u0026#34;clip-1\u0026#34; data-start=\u0026#34;0\u0026#34; data-duration=\u0026#34;5\u0026#34; data-track-index=\u0026#34;0\u0026#34; src=\u0026#34;intro.mp4\u0026#34; muted playsinline\u0026gt; \u0026lt;/video\u0026gt; \u0026lt;!-- 图片叠加 --\u0026gt; \u0026lt;img id=\u0026#34;overlay\u0026#34; class=\u0026#34;clip\u0026#34; data-start=\u0026#34;2\u0026#34; data-duration=\u0026#34;3\u0026#34; data-track-index=\u0026#34;1\u0026#34; src=\u0026#34;logo.png\u0026#34;\u0026gt; \u0026lt;!-- 音频 --\u0026gt; \u0026lt;audio id=\u0026#34;bg-music\u0026#34; data-start=\u0026#34;0\u0026#34; data-duration=\u0026#34;9\u0026#34; data-track-index=\u0026#34;2\u0026#34; data-volume=\u0026#34;0.5\u0026#34; src=\u0026#34;music.wav\u0026#34;\u0026gt; \u0026lt;/audio\u0026gt; \u0026lt;/div\u0026gt; Timeline Contract（动画注册） HyperFrames 通过 window.__timelines 控制 GSAP 动画：\n1 2 3 4 5 6 7 8 9 window.__timelines = window.__timelines || {}; const tl = gsap.timeline({ paused: true }); // 构建动画 tl.from(\u0026#34;.title\u0026#34;, { y: 48, opacity: 0, duration: 0.6, ease: \u0026#34;power3.out\u0026#34; }, 0); tl.to(\u0026#34;.accent\u0026#34;, { scaleX: 1, duration: 0.5, ease: \u0026#34;power2.out\u0026#34; }, 0.25); // 注册 — key 必须与 data-composition-id 匹配 window.__timelines[\u0026#34;my-video\u0026#34;] = tl; 关键规则：\n✅ Timeline 必须 { paused: true } ✅ 必须注册到 window.__timelines[\u0026quot;composition-id\u0026quot;] ❌ 不要调用 tl.play() — HyperFrames 通过 seek 控制 ❌ 不要在 async/setTimeout 中创建 timeline ❌ 不要使用 repeat: -1 — 使用有限重复次数 CLI 命令 命令 说明 hyperframes init 初始化新项目 hyperframes lint 语法检查 hyperframes validate 验证 composition hyperframes preview 浏览器预览 hyperframes render 渲染 MP4 hyperframes inspect 视觉检查 hyperframes doctor 环境诊断 确定性原则 HyperFrames 追求 确定性渲染：相同输入 = 相同输出。\n禁止项：\nMath.random() — 每次运行必须产生相同结果（使用种子 PRNG 如 mulberry32） 无限循环 repeat: -1 — 视频时长有限 异步创建 timeline — 必须同步注册 提前离场 — 场景转换即离场时刻 必须项：\n入场动画 — 每个可见元素必须有入场动画 视频/音频分离 — 视频 muted，音频用独立 \u0026lt;audio\u0026gt; 元素 Best Practices 三条黄金规则（确保正确渲染） 根元素必须有：data-composition-id、data-width、data-height 时间元素必须有：class=\u0026quot;clip\u0026quot;、data-start、data-duration、data-track-index GSAP timeline 必须：使用 { paused: true } 并注册到 window.__timelines 正确的提示词模式 冷启动 — 从零描述视频：\nUsing `/hyperframes`, create a 10-second product intro with a fade-in title, a background video, and background music. 暖启动 — 转换现有内容：\nTake a look at this GitHub repo and explain its uses using `/hyperframes`. Summarize the attached PDF into a 45-second pitch video using `/hyperframes`. Turn this CSV into an animated bar chart race using `/hyperframes`. 迭代调整 — 像和视频编辑对话：\nMake the title 2x bigger, swap to dark mode, and add a fade-out at the end. Add a lower third at 0:03 with my name and title. 缓动函数选择（对应视觉感受） 视觉感受 GSAP ease 平滑 power2.out 干脆 power4.out 弹跳 back.out 弹性 elastic.out 戏剧性 expo.out 字幕风格选择 风格 字号 字体 动画 Hype（动感） 72-96px 粗体 scale-pop Corporate（商务） 56-72px 无衬线 fade + slide Tutorial（教程） 48-64px 等宽 typewriter 典型工作流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 初始化项目 hyperframes init my-video # 2. 用 Claude Code 打开项目 cd my-video # 3. 用 /hyperframes 描述视频 # 4. 预览迭代 hyperframes preview # 5. 小步调整（像和视频编辑对话） # 6. 渲染输出 hyperframes render --output final.mp4 性能优化 预览时卡顿是正常的（实时渲染） 如果最终渲染效果差，先优化： 大面积 backdrop-filter 模糊 过大的图片 过重的阴影叠加 模板选择 模板 风格 适用场景 swiss-grid 简洁结构 企业/技术演示 yt-graph 编辑风格 数据故事/动态图表 play-mode 弹性活力 社交媒体/产品发布 vignelli 大胆竖版 移动端竖屏视频 初始化模板：hyperframes init my-video --example swiss-grid\nSkill 体系 HyperFrames 提供了多个 skill，覆盖不同场景：\nSkill 用途 hyperframes 基础用法：composition、captions、TTS hyperframes-cli CLI 命令：init/lint/preview/render hyperframes-media 资源预处理：TTS、转录、去背景 hyperframes-registry Block/component 安装 website-to-hyperframes 网页转视频 remotion-to-hyperframes Remotion 项目迁移 gsap GSAP 动画（见 gsap.md） animejs Anime.js 动画 css-animations CSS 关键帧动画 lottie Lottie 动画 three Three.js 3D 场景 waapi Web Animations API Block 组件库 50+ 开箱即用的组件：\n1 2 3 hyperframes add flash-through-white # 闪光转场 hyperframes add instagram-follow # 社交关注组件 hyperframes add data-chart # 数据图表 浏览完整目录：hyperframes.heygen.com/catalog\n常见错误 错误做法 正确做法 请求 React/Vue 组件 compositions 是纯 HTML 跳过 /hyperframes 命令 agent 需要 skill 上下文 默认 4K/60fps 默认 1920x1080 30fps 已经很好且渲染快 💡 Tip: 安装 skill 后，直接用自然语言描述视频需求即可触发使用\n","permalink":"https://pillumina.github.io/posts/aiagent/claude-code-skills/hyperframes/","summary":"\u003cblockquote\u003e\n\u003cp\u003eGitHub: \u003ca href=\"https://github.com/heygen-com/hyperframes\"\u003eheygen-com/hyperframes\u003c/a\u003e\u003cbr\u003e\n文档: \u003ca href=\"https://hyperframes.heygen.com/introduction\"\u003ehyperframes.heygen.com\u003c/a\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"核心定位\"\u003e核心定位\u003c/h2\u003e\n\u003cp\u003eHyperFrames 是一个开源的 HTML 视频渲染框架，用「写 HTML 来渲染视频」的方式工作。最大的特点是 \u003cstrong\u003eAI-First\u003c/strong\u003e——AI Agent 天然会写 HTML，不需要额外学习。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e与 Remotion 的核心区别\u003c/strong\u003e：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e特性\u003c/th\u003e\n          \u003cth\u003eHyperFrames\u003c/th\u003e\n          \u003cth\u003eRemotion\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e编写方式\u003c/td\u003e\n          \u003ctd\u003eHTML + CSS + GSAP\u003c/td\u003e\n          \u003ctd\u003eReact 组件 (TSX)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e构建步骤\u003c/td\u003e\n          \u003ctd\u003e无，\u003ccode\u003e.html\u003c/code\u003e 直接可用\u003c/td\u003e\n          \u003ctd\u003e需要 bundler\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e动画精度\u003c/td\u003e\n          \u003ctd\u003eSeekable，帧级精确\u003c/td\u003e\n          \u003ctd\u003e依赖 wall-clock\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e开源许可\u003c/td\u003e\n          \u003ctd\u003eApache 2.0（完全开源）\u003c/td\u003e\n          \u003ctd\u003e自定义许可证（需付费）\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003eHyperFrames 借鉴了 Remotion 的设计思路，代码中保留了对其首创模式的致谢注释。两者的核心分歧在于：\u003cstrong\u003eAgent 主要写什么\u003c/strong\u003e。Remotion 选择 React 组件，HyperFrames 选择 HTML。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"安装\"\u003e安装\u003c/h2\u003e\n\u003ch3 id=\"claude-code-插件市场推荐\"\u003eClaude Code 插件市场（推荐）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/plugin marketplace add heygen-com/hyperframes\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/reload-plugins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"npx-skills通用\"\u003enpx skills（通用）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpx skills add heygen-com/hyperframes\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"cli-工具\"\u003eCLI 工具\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 全局安装 CLI\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enpm install -g hyperframes\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 初始化新项目\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ehyperframes init my-video\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e my-video\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 开发预览\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ehyperframes preview      \u003cspan class=\"c1\"\u003e# 浏览器预览，live reload\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 渲染输出\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ehyperframes render       \u003cspan class=\"c1\"\u003e# 输出 MP4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e前置要求\u003c/strong\u003e：Node.js \u0026gt;= 22, FFmpeg\u003c/p\u003e","title":"Skill: HyperFrames - 用 HTML 制作视频"},{"content":"理解LLM推理中deterministic问题来源 Wiki上对deterministic算法的定义是:\n“a deterministic algorithm is an algorithm that, given a particular input, will always produce the same output.”\n而我们在文中要讨论的，即对于LLM这个context下的deterministic问题，我会先从inference角度（即重复给定一个确定的input，模型的推理为什么无法给定确定的输出）进行问题的理解，再进一步讨论RL工程中的training \u0026amp; inference之间差异，可能会导致RL训练的崩溃问题，并继续讨论业界现在已有的解决方案、与还在working-in-progress的工作。\n浮点数的非结合性 thinking machines lab针对batch invariant讨论的文章，详细地解释了在LLM推理中不确定性的来原，即因为精度有限，GPU浮点数运算中的结合性通常不成立：\n$$(a+b)+c \\neq a+(b+c) $$\n这篇arxiv文章，则更深入得说明了这个问题：\nFloating-point arithmetic in GPUs exhibits non-associativity, meaning (a+b)+c≠a+(b+c) due to finite precision and rounding errors. This property directly impacts the computation of attention scores and logits in the transformer architecture, where parallel operations across multiple threads can yield different results based on execution order.\n浮点数通常可用科学计数的表示来表征大/小数，例如格式$mantissa *10^{exponent}$，如果指数项是不同的，也就是文中说的add at different scales，那不同累加序导致的精度损失会更加明显，而这种不同scale的累加是最常见的场景。\n但是尽管这是不一致输出的根本原因，但是并没有回答不确定性源自何处。无法帮助我们去理解：浮点数值为何会以不同的顺序相加、这种情况何时会发生，已经如何避免这种情况。\n为何计算内核不同序add numbers？ 一个常见的假说是“并发执行随机性 + 浮点运算误差”。这个假说的核心观点，就是如果并发线程的结束顺序是非确定的，并且数值累加顺序如果依赖于并发线程的结束顺序（例如使用atomic add操作），那么最终数值累加的顺序也是非确定的。\n什么时候真正需要atomic add？ 但是问题是，LLM前向的GPU内核实际上很少用atomic add操作。\n简单解释下Atomic Add的含义：GPU 会把同一段程序同时扔到很多“小核”（SM）上去跑。这些小核之间天生没有步调一致的机制，谁快谁慢完全看当时心情。于是，如果它们需要把结果写到同一个地方，就会出问题。那atomic add就是，硬件保证所有人的结果最终都会加进去，但谁先谁后、按什么顺序加，完全不保证，因此每次跑出来的累加顺序都可能不一样。\n再举个例子，通过torch.sum()对100个数求和，GPU 可以让 100 个小核各读一个数，这一步完全并行。可最后总得把 100 个数合并成 1 个总和。若用原子加，就是让每个小核随便谁先到，就先把它的数塞到同一个累加器里。硬件只负责“不会丢数”，却不负责“按固定顺序加”。于是同样跑两遍，先加谁后加谁可能不同，结果也就可能出现那一点点浮点误差。\n我们回想通常定义的不确定性的含义：同一段 kernel、同一批输入，跑两遍却得到两个略有差异的结果。这叫“run-to-run 非确定性”——哪怕 Python 脚本、依赖库、硬件都没变，第二次跑就是能给你不一样的数。虽然atomic add会导致这个问题，但是更通常的情况是，LLM一次典型的forward，通常一个atomic add也没有。\n这主要有两个原因：\nbatch维度上实际上已经“人多势众”，根本不需要在reduction维度再去并行。 大多数neural networking library也用了很多技巧来实现“既保障确定性又快”，例如“分段树形归约”（split/tree reduction），可以先把100个数拆成5组，每组20个数，5个小核并行计算一个局部和。剩下 5 个局部和要么交给一个核顺序“扫尾”（元素少，开销可忽略），要么用信号量（semaphore）按固定顺序让不同线程块依次累加，从而保证先后顺序一致，结果也就 deterministic 了。 不过，在pytorch中的scatter_add操作（a[b] += c），如果不用atomic add性能会特别慢，而LLM中唯一踩这个坑的，是FlashAttention的反向传播。\n不过当前Triton版本FA反向实现，其实和Tri Dao原论文里的算法不完全一样，Triton版为了躲开atomic add，额外多算了一遍中间结果\u0026ndash;FLOPS直接多了40%，但是换来了determinstic，也算明码标价。\n但是正向传播里，LLM中根本就没有非得用atomic add的算子，所以结论就是：LLM 的前向推理，跑两次、跑一百次，结果比特级完全一致；真正可能“每次不一样”的，只出在反向训练阶段，而且基本就 FlashAttention 一家。（也就是前向是“run-to-run deterministic”的）。\n系统级别批次不变性的缺失（batch invariant） 前向kernel函数的确定性，实际上不等于整个推理服务对外表现确定，也就是还存在额外的系统级非确定性。因为真正喂给前向的张量内容还可能被其他“外部输入”左右，比如batch_size，也就是仍缺失“批次不变性”（batch invariance）：同一请求、同一模型权重，只要推理时动态批大小不同，可能会导致tilesize不同，导致reduce的计算结果不同。\n更细节去深究，比如Attention计算时，当kvcache很长的时候，就需要沿着KV维度进行分割：\nsplit reduction kernels: Split-KV或者FlashDecoding。 根据batch size动态选择分割数量，会导致不同的reduction顺序。 若不沿着规约维度进行并行，那么只能在batch、head、query维度进行并行，在decoding阶段，query本身就短，除非是大batch，那么gpu的计算内核很多会空转。\n再者，内存的访问模式可能也随着batch size的大小而变化，可能导致不同的stride:\n1 2 3 4 5 6 7 for ki in range(k_tiles): if A_LARGE or B_LARGE: offs_k = ki * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K).to(tl.int64) else: offs_k = ki * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K) a_ptrs = a_ptr + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak) b_ptrs = b_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn) batch size数值的变化不会必然导致stride变化，stride变化主要原因是张量布局改变，而布局常常因为batch size不同而被program主动.view/.transpose等操作修改。\nM/N不同（行/列长度不同），可能会引入不同的内存对齐，compiler可能会选不同的tile/block/warp划分或者加pad，间接导致不同的微观计算执行顺序。\n1 2 offs_am = tl.max_contiguous(tl.multiple_of(offs_am, BLOCK_SIZE_M), BLOCK_SIZE_M) offs_bn = tl.max_contiguous(tl.multiple_of(offs_bn, BLOCK_SIZE_N), BLOCK_SIZE_N) 所以针对推理引擎，比如要在kernel层面实现batch invariant才能真正解决serve层面不确定性的问题。\n和并行策略相关的Reduction不确定性 TODO: 通信库的通信算子带来的规约不确定性。\nexport HCCL_DETERMINISTIC=true 开启HCCL的规约类算子的确定性计算。\nBatch Invariant的相关工作 batch invariant ops Thinking Machines Lab发布了batch invariant的部分kernel算子实现。\n而从原blog里，提出了三种难度递增的实现。\nbatch invariant的RMSNorm 直接让每个Batch元素的reduction顺序固定，不受batch大小影响。\nbatch大时，把单个batch元素分配给单个核心，reduction运算在单核心内完成，batch增大时让核心依次处理多个元素，保持reduction策略不变。 batch小时，若采用\u0026quot;split reduction\u0026quot;（多核心分担reduction以提升并行度）会破坏batch invariant，可以选择忽略小batch优化（小batch本身执行就快，性能损失可以接受），或者采用固定reduction策略（牺牲部分性能来保证batch invariant）。 batch invariant的矩阵乘法 将输出张量拆分为2D tiles，每个tile分配给单个核心，reduction在单个核心内部完成。编译固定配置的内核以适配所有形状，虽然会损失20%性能（和cuBLAS相比），但在LLM推理中通常可以接受，因为模型维度（N）比较大，对split-k的需求较低。\nbatch invariant的注意力计算 采用data-parallel策略（沿着Q张量并行，reduction在单核心内完成），更新KV缓存和页表以保证KV布局一致，不受处理token数量的影响。\ndecode阶段Q长度小，需要拆分KV维度（Split-KV），采用固定拆分大小策略（而非固定拆分数量），确保reduction顺序不变，比如把1000长度的KV拆成3个256长度和1个232长度的片段，而不是4个250长度的片段。\n传统 batch 变化（来 1 条还是 32 条）会让 reduce 并行度变化 → 累加序变化。\n这里把「并行」只放到 无 reduce 的 Q 维（data-parallel），而把 带 reduce 的 KV 维用 固定 tile 大小串行扫完。\n于是 batch 数只影响开多少核，不影响 reduce 顺序，实现「batch 变化，结果不变」的 deterministic attention。\nSglang / vLLM 实现deterministic inference SGLang团队的博客里记录了实现的细节，主要是针对batch invariant kernel上，针对chunked prefill、cuda graph等特性做了兼容，具体可以参考RoadMap。\nvLLM参考Enabling Batch Invariant文档，也可以参考RFC #28326，#27433。\nOn-policy RL训练中的训推不一致问题 训推不一致问题，在RL训练上实际上是不可忽视的，主要由于训练侧（FSDP、Megatron等）和推理侧（vllm、sglang、trt等）这类kernel差异、计算实现路径差异、硬件侧针对两边不同优化这类问题导致的系统性偏差。而且这类偏差，在数学上可能会导致：\nBias: 训练优化器会主动走向一个错误的结果。 Variance：优化器会完全停滞，让训练中止。\n在后面算法的章节，在理论上也会对不一致问题对RL影响进行数学上的分析。\n有研究指出 train / inference engine之间的不一致会隐性导致on-policy假设的RL实际变成off-policy。所以当我们追求\u0026quot;真正的\u0026quot; on-policy RL训练时，需要知道：\n如果不能从两个完全一致的推理请求中获取bitwise相等的结果，那么当然也无法保障训推之间的bitwise一致性。所以基于之前我们对确定性推理实现讨论，直觉上可以知道如果保证了确定性推理，那么通过修改训练这部分stack，也能够实现在bitwise上训推的一致性，从而实现真正的on-policy RL训练。\n而业界对这个问题的解决思路上主要分为两种：\n在训练引擎侧，基于推理引擎(vllm/sglang)确定性推理内核前向实现，进行反向传递的实现，通过对齐kernel的实现，做到训练和采样部分的bitwise一致性（i.e. 0 KL divergence）。\n拥抱训推分布的不一致（考虑到训练bitwise实现在工程上的工作量，和不同模型适配的工作量），在算法上为off-policy做off-policy correction，进行训推KL散度的偏差抑制，在大多数场景也能实现RL训练的平滑和目标效果。\n后续会分别着重分析这两种解决思路。\n不一致问题分析 这篇文章 从实验的角度来对rollout-training不一致问题进行了分析，主要得出的结论是，不同的并行策略以及更长的响应长度会增大二者之间的mismatch，而选择不同的推理后端的影响比较小。\n这些消融实验可以带来一些经验的归纳，也就是说明现象。但是笔者认为并不能让我们完全理解mismatch产生的原因。笔者认为，不一致性的主要来源，还是因为训练（FSDP、Megatron等）和推理（vLLM、SGLang等）是针对不同计算pattern进行了各自侧重的优化，不论是前向的kernel算子差异带来的数值精度误差累积，还是切分策略带来的通信算子规约顺序带来的精度误差累计，都是mismatch的原因一部分。\n而像MoE模型的稀疏以及动态路由特性，会带来比Dense模型更大的mismatch，因为路由机制本身就是数值精度敏感的，一些微小的数值差异，会带来差异巨大的专家激活。除此之外，MoE模型本身的稀疏特性，和Dense模型相比一般规模会更大，而现代的推理引擎，通常针对MoE模型有独特的优化手段（计算、通信），也会放大训推引擎之间的不一致性。\n而字节团队的这篇文章，对训推不一致问题进行了更加深入的理论、实验分析，针对不一致的现象，也提出了更genearal的叙述：\nTo achieve the massive throughput required, modern inference engines (e.g., vLLM, SGLang, TensorRT-LLM) employ aggressive optimization strategies like speculative decoding, low-precision computation (INT8/FP8), and specialized, batch-variant CUDA kernels. While maintaining sampling fidelity, the primary objective of modern inference engines is to maximize throughput, often measured in tokens per second. Conversely, training frameworks (e.g., FSDP, DeepSpeed, Megatron-LM) must strike a different balance, prioritizing numerical stability and precision for gradient computation, often using higher-precision formats like FP32 for master weights and optimizer states. This divergence in optimization priorities and constraints creates an inevitable training-inference mismatch.\n因此，我们可以回到一开始提到的业界解决on-policy RL训推不一致问题的两个思路，实际上是在性能和一致性上trade-off的取舍，如果希望对齐训推计算（例如之前讨论的batch invariant），势必会带来性能上的劣化。\n从这篇文档，能得到很多有用的takeaways，比如实验中衡量不一致性的用的是下面的vllm-klmetric：\n$$\\small{\\mathbb{E}_{s\\sim d_{\\textcolor{red}{\\pi^\\text{vllm}_\\theta}}}\\left[\\text{KL}\\left(\\textcolor{red}{\\pi^\\text{vllm}_\\theta}\\left(\\cdot|s\\right),\\textcolor{blue}{\\pi^\\text{fsdp}_\\theta}\\left(\\cdot|s\\right)\\right)\\right] = \\mathbb{E}_{s\\sim d_{\\textcolor{red}{\\pi^\\text{vllm}_\\theta}},a\\sim {\\textcolor{red}{\\pi^\\text{vllm}_\\theta}\\left(\\cdot|s\\right)}} \\left[\\log\\left(\\frac{\\textcolor{red}{\\pi^\\text{vllm}_\\theta}(a|s)}{\\textcolor{blue}{\\pi^\\text{fsdp}_\\theta}(a|s)}\\right)\\right],}$$\n这个metric的异常spike，通常能导致训练侧entropy和rewards的异常波动。 而vllm-kl的spike同时会导致fsdp-ppl和gradient norm的爆炸性波动，这表示FSDP engine给推理引擎采样的得到的tokens设置特别小的概率，导致梯度爆炸，从而让RL训练崩溃。\n以及mismatch不是均匀分布的，如果推理引擎得到的token概率越接近0，那在训练侧这个token的概率会更严重地被压小，让mismatch更大。\n除此之外，还发现OOD tools的返回比如multi-turn TIR会带来更大的mismatch等等问题。\n所以综上所述，在当前的RL框架中，训推引擎之间的不一致，是一个不可避免的问题，如果不一致问题非常严重，容易导致训练崩溃这样的严重后果（特别在长稳训练下）。\n接下来笔者详细介绍一下，业界针对不一致问题的解决思路和方案。\n硬对齐训推前反向不同kernel TorchTitan + vLLM TorchTitan项目探索了基于vllm的确定性RL的实现，基于vllm的确定性前向实现，补充了vllm operations的反向传播。其具体的实现为：\n利用vLLM的batch invariant前向实现。 1 2 3 # These operations are deterministic when batch_invariance is enabled y = torch.matmul(a, b) # Uses vLLM\u0026#39;s deterministic matmul output = flash_attn_varlen_func(q, k, v, num_splits=1) # Deterministic FA 实现了自定义的反向函数进行梯度计算。 1 2 3 4 5 6 7 8 9 10 11 class FlashAttnWithBackward(torch.autograd.Function): @staticmethod def forward(ctx, q, k, v, ...): # Use vLLM\u0026#39;s forward implementation return flash_attn_varlen_func(q, k, v, num_splits=1, ...) @staticmethod def backward(ctx, grad_output): # Compute gradients deterministically # (re-compute attention weights and apply chain rule) return grad_q, grad_k, grad_v, ... 提供了torchtitan和vllm侧不同格式的权重转换能力。 Slime + SGLang SGLang团队在Thinking Machines Lab发布的批次不变算子基础之上，通过定制一系列注意力算子和采样逻辑，也实现了完全确定性推理。该实现同时保持与分块预填充 (chunked prefill)、CUDA Graph、Radix Cache 和非贪婪采样 (non-greedy sampling) 等关键功能的兼容性。SGLang侧的主要增强工作为:\n集成Thinking Machines Lab的批次不变(batch invariant)算子。 实现固定KV分割大小的批次不变注意力算子。支持多种后端，包括 FlashInfer、FlashAttention 3和Triton。 与关键推理性能相关功能完全兼容，例如分块预填充、CUDA图、基数缓存等，当启用确定性推理时，所有这些功能都仍受支持。 支持按请求设置采样种子(per-request sampling seed)，即使在temperature\u0026gt;0的非贪婪采样模式下也能实现确定性推理。 而在slime侧，主要是进行了torch设置：\ntorch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False torch.use_deterministic_algorithms(True, warn_only=False) 以及环境变量：\nsetting environment variable NCCL_ALGO=Ring, NVTE_ALLOW_NONDETERMINISTIC_ALGO=0, CUBLAS_WORKSPACE_CONFIG=:4096:8; 针对megatron后端设置--deterministic-mode 详细可以看此PR。\nRL算法侧缓解差异（off-policy correction） 数学直觉和分析工具 首先我们需要在数学上对mismatch问题给训练带来影响建立一些理论直觉，针对后续的一些算法工作，才会知道为什么这些是work的，哪些是可能还是有问题的。\n这篇工作在数学上分析了RL break的原因。首先基于stochastic gradient ascent lemma建立了分析的约束工具：$\\mathbb{E}[J(\\theta_{k+1})] - J(\\theta_k),$而这个式子被下面的表示约束: $$\\mathbb{E}[J(\\theta_{k+1})] - J(\\theta_k) \\geq \\text{Term A} + \\text{Term B} - \\text{Term C}$$\n其中：\nTerm A (True Progress): $\\eta (1 - \\frac{L\\eta}{2})|\\nabla J|^2$ 对于真实的、无噪声的梯度的训练progess。 Term B (Bias Error): $\\eta(1 - L\\eta)\\langle \\nabla J, \\mathbf{Bias}(\\hat{g}) \\rangle$ 计算系统级别异常导致的偏差。 Term C (Noise Error): $\\frac{L\\eta^2}{2}[\\mathbf{Var}(\\hat{g}) + |\\mathbf{Bias}(\\hat{g})|^2]$ 对于噪声和bias幅度的penalty。 基于上面的式子，有几个takeways：\n$\\mathbf{Bias}(\\hat{g}) = \\mathbb{E}[\\hat{g}] - \\nabla J$ 这个差异衡量了estimator的systematic error，而在Term B中，$\\langle \\nabla J, \\mathbf{Bias} \\rangle$ 衡量了真实梯度和这个error关系。可以看到，如果bias比较小，或者是随机的，这个式子接近0，everything is fine。但是如果bias是系统级别的，并且和真实梯度有显著差异，那么点积会变得很大。一个负的Term B，代表着optimizer自己和自己“打架”，这不仅仅是训练变慢，而是逐渐和最优点逐渐偏离。因此，高的bias会导致一个错误的优化结果。 $\\mathbf{Var}(\\hat{g}) = \\mathbb{E}[|\\hat{g}|^2] - |\\mathbb{E}[\\hat{g}]|^2$ 方差衡量了estimator的noise，这个noise error总是负数，并且scales with $\\eta^2$，作为上面式子里的penalty。所以如果var比较高，这个负值会变大，那么为了保证优化的稳定（i.e. 确保Term A \u0026gt; Term C），我们只能被迫使用一个更小的学习率$\\eta$ 。而且随着优化的进行，$\\eta$也必须scale with $O(1/\\mathbf{Var}).$。所以高方差 =\u0026gt; 低学习率 =\u0026gt; 缓慢的优化进展，所以训练的loss没崩掉，不是因为问题解决了，而是优化器本身就慢的很。\n所以，算法侧的目标，应该是既要控制bias也要控制variance。 衡量bias和variance分别用下面两种工具：\nTotal Variation (TV) Distance ： $$D_{TV}(\\mu \\| \\pi) = \\frac{1}{2}\\sum_y |\\mu(y) - \\pi(y)|$$\n这个公式用来衡量bias，用来量化Term B的影响。 Chi-Square ($\\chi^2$) Distance: $$\\chi^2(\\pi\\|\\mu) = \\mathbb{E}_\\mu\\left[\\left(\\frac{\\pi(y)}{\\mu(y)}\\right)^2\\right] - 1 = \\mathbb{E}_\\mu[\\rho^2] - 1$$ 公式由IS（重要性采样）的二阶矩定义，用于衡量重要性采样 estimator的variance，$\\mathbb{E}_\\mu[\\rho^2].$。如果这个值很大或者到inf，说明variance已经无法控制了，这个式子用来衡量Term C的影响。 Mismatch Importance Sampling TIS（截断重要性采样） 比较早的博客是(Yao et al.2025)，分析了用重要性采样从算法上缓解训推不一致性的问题。对REINFORCE的梯度表示：\n$$\\mathbb{E}_{a \\sim \\textcolor{red}{\\pi_{\\text{sampler}}}(\\theta)} [R(a)\\cdot \\nabla_\\theta \\log \\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)],$$转换为：\n$$\\mathbb{E}_{a \\sim \\textcolor{red}{\\pi_{\\text{sampler}}}(\\theta)} \\Bigl[\\frac{\\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)}{\\textcolor{red}{\\pi_{\\text{sampler}}}(a, \\theta)} \\cdot R(a)\\cdot \\nabla_\\theta \\log \\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)\\Bigr].$$\n而后基于比较经典的TIS方法，可以实现更稳定的重要性采样: $$\\mathbb{E}_{a \\sim \\textcolor{red}{\\pi_{\\text{sampler}}}(\\theta)} \\Bigl[\\underbrace{\\min\\Bigl(\\frac{\\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)}{\\textcolor{red}{\\pi_{\\text{sampler}}}(a, \\theta)}, C\\Bigr)}_{\\text{truncated importance ratio}} \\cdot R(a) \\cdot \\nabla_\\theta \\log \\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)\\Bigr],$$扩展到PPO算法，策略梯度为经典的公式: $$\\small{ \\mathbb{E}_{a\\sim\\pi_{\\theta_{\\mathrm{old}}}} \\Bigl[ \\nabla_\\theta \\min\\Bigl( \\frac{\\pi_\\theta(a)}{\\pi_{\\theta_{\\mathrm{old}}}(a)}\\,\\hat A, \\;\\mathrm{clip}\\bigl(\\frac{\\pi_\\theta(a)}{\\pi_{\\theta_{\\mathrm{old}}}(a)},\\,1-\\epsilon,\\,1+\\epsilon\\bigr)\\,\\hat A \\Bigr) \\Bigr]}.$$\n为了提升吞吐，Hybrid RL系统比如veRL使用vLLM这类推理引擎做rollout采样，而后回到训练侧用训练引擎再做一次 $\\pi_{\\theta old}$的recompute：\n$$ \\small{ \\mathbb{E}_{a\\sim\\textcolor{red}{\\pi_{\\text{sampler}}}(\\theta_{\\mathrm{old}})} \\Bigl[ \\nabla_\\theta \\min\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)}{\\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta_{\\mathrm{old}})}\\,\\hat A, \\;\\mathrm{clip}\\bigl(\\frac{\\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta)}{\\textcolor{blue}{\\pi_{\\text{learner}}}(a, \\theta_{\\mathrm{old}})},\\,1-\\epsilon,\\,1+\\epsilon\\bigr)\\,\\hat A \\Bigr) \\Bigr] }.$$\n同样的，这种训练和推理的mismatch会出现，那么可以使用TIS进行校准：\n$$\\small{\\mathbb{E}_{a\\sim\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(\\theta_{\\mathrm{old}})}\\Bigl[\\underbrace{\\min\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\theta_{\\mathrm{old}})}{\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(a,\\theta_{\\mathrm{old}})}, C\\Bigr)}_{\\text{truncated importance ratio}}\\cdot\\nabla_{\\theta}\\,\\min\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta)}{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta_{\\mathrm{old}})}\\,\\hat{A}, \\mathrm{clip}\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta)}{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta_{\\mathrm{old}})}, 1-\\epsilon,\\;1+\\epsilon \\Bigr)\\,\\hat{A}\\Bigr)\\Bigr]}$$\n文中也做了一些对比实验，表示此类校准确实能减少训推之间的计算分布差异:\n除此之外，不同的IS变种的效果也有所不同。例如Colossal框架使用的PPO-IS格式: $$\\small{ \\mathbb{E}_{a\\sim\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(\\theta_{\\mathrm{old}})}\\Bigl[\\nabla_{\\theta}\\,\\min\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta)}{\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(a,\\;\\theta_{\\mathrm{old}})}\\,\\hat{A}, \\mathrm{clip}\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta)}{\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(a,\\;\\theta_{\\mathrm{old}})}, 1-\\epsilon,\\;1+\\epsilon \\Bigr)\\,\\hat{A}\\Bigr)\\Bigr]}$$\n以及Nemo-RL框架使用的格式：\n$$\\small{\\mathbb{E}_{\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(\\theta_{\\mathrm{old}})}\\Bigl[\\underbrace{\\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\theta_{\\mathrm{old}})}{\\textcolor{red}{\\pi_{\\mathrm{sampler}}}(a,\\theta_{\\mathrm{old}})} }_{\\text{importance ratio}}\\cdot\\nabla_{\\theta}\\,\\min\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta)}{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta_{\\mathrm{old}})}\\,\\hat{A}, \\mathrm{clip}\\Bigl( \\frac{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta)}{\\textcolor{blue}{\\pi_{\\mathrm{learner}}}(a,\\;\\theta_{\\mathrm{old}})}, 1-\\epsilon,\\;1+\\epsilon \\Bigr)\\,\\hat{A}\\Bigr)\\Bigr]}$$\n对比下来还是TIS更加稳定，特别是在训推不同量化这种场景下（e.g. FP8/INT8），更加明显。\n更多的IS变种 更进一步的，前面介绍的字节的这篇工作还是更细致分析了不同IS：\nToken-level / Sequence-level TIS\n给定upper和lower bound，针对weights超过这个部分的做clip。 Token-level / Sequence-level MIS\n给定upper和lower bound，将超出这个范围的weights置为0，相当与mask out掉，这个策略更加激进，适合处理极端的mismatch\n简单来说，有以下的几个结论。 Token-level的IS是理论有偏（biased）的估计，而Sequence-level的IS是无偏的估计，通常能有更稳定的训练。 在复杂的场景（例如TIR），token-level的TIS还是会failed，但是在简单的reasoning RL，当mismatch较小的时候（比如on-policy GRPO）, token-level的TIS够用，可以防止梯度爆炸，但是训练稳定性、训练摸高效果可能由于梯度的bias会有限制。 MIS（masked IS）效果通常比TIS要更好，同样sequence-level的测试，不论在training reward还是评估分数，都能超过不用IS的原始训练。 sequence-level的MIS在更复杂、更长上下文的自回归任务上，表现的还是比token-level要好，这符合理论预期。 VeRL的rollout correction实现 建议直接参考verl rollout correction文档\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 algorithm: rollout_correction: rollout_is: token # IS weights: \u0026#34;token\u0026#34;, \u0026#34;sequence\u0026#34;, or null rollout_is_threshold: 2.0 # Upper threshold for IS weights rollout_is_batch_normalize: false # Batch normalize IS weights to mean=1.0 rollout_rs: null # Rejection sampling: \u0026#34;token\u0026#34;, \u0026#34;sequence\u0026#34;, \u0026#34;geometric\u0026#34;, or null rollout_rs_threshold: null # RS upper threshold (required if rollout_rs is enabled) rollout_rs_threshold_lower: null # RS lower threshold (auto-reciprocal if null) rollout_token_veto_threshold: null # Per-token veto threshold (null = disabled) bypass_mode: false # Skip old_log_prob computation use_policy_gradient: false # Use policy gradient loss (vs PPO loss) # REQUIRED: Enable log prob calculation actor_rollout_ref: rollout: calculate_log_probs: true 具体分为BypassMode以及IS/RS的实现，实现放在verl.trainer.ppo.rollout_corr_helper.py中。\napply_rollout_correction在verl.trainer.ppo.ray_trainer.py的RayPPOTrainer的fit中调用，调用场景是bypass_mode被使能，也就是不重计算old_log_prob，apply以后，实际上设置了loss_mode为rollout_correction，后续计算的时候, loss_fn会选择compute_policy_loss_with_rollout_correction（见verl.trainer.ppo.core_algos.py），在这个函数中，会on-the-fly调用compute_rollout_correction_and_rejection_mask计算IS之后的weight和modified的response mask。\n而对bypass_mode关闭的模式，也就是decoupled-ppo模式，会重计算old_log_prob，相当于每个mini batch都要调用compute_rollout_correction_and_rejection_mask计算IS的weight和response make，然后添加到batch中（union），然后正常走正常的流程，比如调用compute_policy_loss_vanilla里会处理。\nSlime的IS实现 可以参考合入PR，和verl类似，都实现了token/sequence/geometric mean级别的TIS、MIS等校准策略。\n--use-train-infer-is: Enable training-inference importance sampling --train-infer-is-level: Aggregation level (token/sequence/geometric) --train-infer-is-mode: Processing mode (truncate/mask/clip) --train-infer-is-lower-bound/--train-infer-is-upper-bound: Weight bounds --train-infer-is-veto-threshold: Catastrophic token threshold 从理论角度解释上述的现象 前面介绍的这些研究，针对token-level, seq-level的TIS或者MIS都进行了实验，发现seq-level的效果是比token-level更好的，而且seq-level的MIS比seq-level的TIS效果更好，我们可以基于前面给出的理论分析框架和数学工具，再从本质上去理解背后的原因，做到对问题的完全理解。\n回顾一下，我们通过$D_{TV}$来衡量bias，用$\\chi^2$-divergence衡量variance。\n我们的目标是找到一个estimator $\\hat{g}$，同时能控制bias和variance。首先我们分析下常见的estimator，先定义：\n$$ f(y) := \\nabla_\\theta \\log \\pi(y|x) \\cdot R(y|x)$$\n作为目标函数，梯度是$g = \\mathbb{E}_\\pi[f(y)]$。\nSeq-IS 这种是最为pure的estimator：$\\hat{g}_{\\text{seq}} = \\rho(y) \\cdot f(y)$，其中$\\rho(y) = \\pi(y) / \\mu(y)$，对每个采样进行序列级别的re-weighting，来缓解mismatch。\n可以推导得到，这个estimator是unbiased：\n$$ \\mathbb{E}_\\mu[\\hat{g}_{\\text{seq}}] = \\mathbb{E}_\\mu\\left[ \\frac{\\pi(y)}{\\mu(y)} f(y) \\right] = \\sum_y \\mu(y) \\frac{\\pi(y)}{\\mu(y)} f(y) = \\sum_y \\pi(y) f(y) = \\mathbb{E}_\\pi[f(y)] = g$$\n所以都会优化收敛到正确的结果。而对于estimator的variance，和IS ratio的二阶矩有关\n$$\\mathbb{E}_\\mu[\\rho^2] = 1 + \\chi^2(\\pi\\|\\mu).$$\n但是问题是，针对自回归的序列，二阶矩会跟随序列长度$T$呈现指数级别的增长，对于很小的比如1%的per-token差异($\\bar{\\chi}^2_{\\min} = 0.01$)，200个token的会得到variance的倍数$(1.01)^{200} \\approx 7.3$。5%的mismatch会导致17292倍数的放大，这个明显是不可接受的。\n当然，我们可以通过normalized的手段，去控制数值爆炸：\n$$ \\hat{g} = \\frac{1}{N} \\sum \\rho_i f_i.$$ $$ \\hat{g}_{\\text{snis}} = \\frac{\\sum_{i=1}^N \\rho(y_i) f(y_i)}{\\sum_{i=1}^N \\rho(y_i)}$$\n这个方式虽然能够stable，但是会导致“Sample Collapse”，也就是在高维的序列生成下，$\\rho$方差会很大，可能少量的样本会主导累加和，也就是: $$ \\text{ESS} \\approx \\frac{(\\sum \\rho_i)^2}{\\sum \\rho_i^2} \\to 1$$\n这样是非常不高效的，可能会抛弃掉batch中99%的数据。因此我们需要\u0026quot;截断\u0026quot;(Truncation)\nToken-IS sequence-level的IS，因为涉及到全序列的$\\rho = \\prod_t \\rho_t$，会出现variance为指数级增长，而token-level的IS，其variance随着序列T的增长，其方差增长并非指数级的，而是多项式级的$O(T^2(1+\\bar{\\chi}^2))$。\ntoken-level的estimator形式为：$ \\hat{g}{\\text{tok}}(y) = \\sum{t=0}^{T-1} \\left( \\frac{\\pi(y_t|x, y_{\u0026lt;t})}{\\mu(y_t|x, y_{\u0026lt;t})} \\right) A(s_t, y_t) \\nabla_\\theta \\log \\pi(y_t|x, y_{\\text{\u0026lt;t}}) $\n但是因为非sequence-level IS，会引入一个系统级别的bias，而且这个bias随着序列长度$T$呈二次项的增长 $O(T^2 \\Delta_{\\max})$。因为，这个estimator优化的，只是一个有偏的替代目标$L_μ​(π)$ ，它是基于旧的状态分布$d_μ$,而不是真正的目标$J(π)$。因此，这种继承了这个偏差目标带来的$O(T^2)$偏差。\nToken-TIS PPO因为引入了clip，所以其实能较好得处理variance的问题，TIS也是类似，这个是对PPO的一种补充。朴素的思想是，如果重要性比率$\\rho_t$太大了，就clip防止数值爆炸。\n$ \\hat{g}{\\text{tl-tis}}(y) = \\sum{t=0}^{T-1} \\min\\left( \\frac{\\pi(y_t|x, y_{\u0026lt;t})}{\\mu(y_t|x, y_{\u0026lt;t})}, C \\right) A(s_t, y_t) \\nabla_\\theta \\log \\pi(y_t|x, y_{\u0026lt;t}) $\n通过引入对重要性比值的限制，能约束variance到多项式级别$O(T^2 C^2)$。\n但是针对bias，不能修复在上文说明的$O(T^2 \\Delta_{\\max})$级别scale，因为bias来源于状态分布的不一致，也就是$d_\\mu \\neq d_\\pi$，而不是来源于token级别的重要性比值权重$\\rho_t$。而针对$\\rho_t$的clip，是一个减少variance的技巧，而不会修正隐藏的优化目标。这种截断，反而会基于已有的$O(T^2 \\Delta_{\\max})$上，新增截断的bias: $$ \\text{Total Bias} = \\mathbb{E}[\\hat{g}_{\\text{tl-tis}}] - g = \\underbrace{(\\mathbb{E}[\\hat{g}_{\\text{tl-tis}}] - \\mathbb{E}[\\hat{g}_{\\text{tok}}])}_{B_{\\text{trunc}}} + \\underbrace{(\\mathbb{E}[\\hat{g}_{\\text{tok}}] - g)}_{B_{\\text{fatal}}} = B_{\\text{trunc}} + O(T^2 \\Delta_{\\max})$$\n因此，当序列长度比较长，并且off-policiness的差异比较大的场景，token-level的技巧都无法解决训练上的问题。\n而且，PPO算法就好像是一个“创可贴”，虽然阻止了训练爆炸，但是模型也看不见真正的sequence-level的objective。而bias是sequence-level的问题，也必须在sequence level去解决。\nSequence-TIS 经过前面的分析，我们会发现一个dilemma：\nSeq-IS：无偏，但是variance为$O((1 + \\bar{\\chi}^2_{\\max})^T)$级别。 Naive/Token-IS/Token-TIS: variance是$O(T^2)$级别还行，还是bias是$O(T^2 \\Delta_{\\max})$级别是有偏的而且随着序列规模二次项放大。\n现在分析的Seq-TIS能够解决这个dilemma，其estimator为：$$\\hat{g}_{\\text{sl-tis}}(y) = \\min\\left(\\prod_{t=0}^{T-1} \\frac{\\pi(y_t|x, y_{","permalink":"https://pillumina.github.io/posts/aiinfra/14-deterministic-rl/","summary":"\u003ch2 id=\"理解llm推理中deterministic问题来源\"\u003e理解LLM推理中deterministic问题来源\u003c/h2\u003e\n\u003cp\u003eWiki上对deterministic算法的定义是:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“a deterministic algorithm is an algorithm that, given a particular input, will always produce the same output.”\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e而我们在文中要讨论的，即对于LLM这个context下的deterministic问题，我会先从inference角度（即重复给定一个确定的input，模型的推理为什么无法给定确定的输出）进行问题的理解，再进一步讨论RL工程中的training \u0026amp; inference之间差异，可能会导致RL训练的崩溃问题，并继续讨论业界现在已有的解决方案、与还在\u003ccode\u003eworking-in-progress\u003c/code\u003e的工作。\u003c/p\u003e\n\u003ch3 id=\"浮点数的非结合性\"\u003e浮点数的非结合性\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/\"\u003ethinking machines lab针对batch invariant讨论的文章\u003c/a\u003e，详细地解释了在LLM推理中不确定性的来原，即因为精度有限，GPU浮点数运算中的结合性通常不成立：\u003c/p\u003e\n$$(a+b)+c \\neq a+(b+c) $$\u003cp\u003e\u003cbr\u003e\n\u003ca href=\"https://arxiv.org/abs/2506.09501\"\u003e这篇arxiv文章\u003c/a\u003e，则更深入得说明了这个问题：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eFloating-point arithmetic in GPUs exhibits non-associativity, meaning (a+b)+c≠a+(b+c) due to finite precision and rounding errors. This property directly impacts the computation of attention scores and logits in the transformer architecture, where parallel operations across multiple threads can yield different results based on execution order.\u003c/p\u003e","title":"[Deterministic RL] 确定性问题的来源 \u0026 Reproducible RL"},{"content":" 源码分析依赖vllm-ascend在2025/9/20号的main分支，阅读请注意时效性。\n阅读建议:\n了解MoE基本架构和关键推导 初步了解集合通信各原语的含义 对通算掩盖这类性能优化有基础的了解 概述 MC2（Merged Compute and Communication）是vLLM Ascend项目中针对昇腾NPU优化的核心技术，专门解决MoE（Mixture of Experts）模型在专家并行推理中的通信瓶颈问题。本文档从MoE架构基础出发，深入分析MC2的设计原理、技术实现和性能优化。\n1. MoE架构基础与挑战 1.1 MoE模型基本原理 1.1.1 什么是MoE？ **MoE(Mixture of Experts)**是一种神经网络架构，通过将模型参数分散到多个\u0026quot;专家\u0026quot;网络中，根据输入动态选择部分专家进行计算。这种架构在保持高模型容量的同时，降低了计算复杂度。\n1.1.2 MoE的数学表达 给定输入 $\\mathbf{x} \\in \\mathbb{R}^{d}$，MoE层的输出可以表示为：\n$$ \\mathbf{y} = \\text{MoE}(\\mathbf{x}) = \\sum_{i=1}^{N} g_i(\\mathbf{x}) \\cdot E_i(\\mathbf{x}) $$其中：\n$N$ 是专家总数 $E_i(\\cdot)$ 是第 $i$ 个专家网络 $g_i(\\mathbf{x})$ 是门控网络对专家 $i$ 的权重 1.1.3 稀疏激活机制 为了提高效率，MoE通常采用稀疏激活机制，只选择 Top-K 个专家：\n$$ \\mathbf{y} = \\sum_{i \\in \\text{Top-K}(\\mathbf{x})} \\frac{g_i(\\mathbf{x})}{\\sum_{j \\in \\text{Top-K}(\\mathbf{x})} g_j(\\mathbf{x})} \\cdot E_i(\\mathbf{x}) $$详见附录A.1 MoE输出公式推导\n其中 $\\text{Top-K}(\\mathbf{x})$ 表示根据门控权重选择的 Top-K 个专家索引。\n1.2 MoE的并行计算范式 1.2.1 专家并行（Expert Parallelism） 当专家数量很大时，需要将专家分布到多个计算设备上：\n$$ E_i \\rightarrow \\text{Device}_{(i \\bmod P)} $$其中 $P$ 是设备数量。这带来了两个核心问题：\nToken-Expert映射：需要将token路由到正确的专家设备 结果聚合：需要从各个专家设备收集结果 1.2.2 通信复杂度分析 对于批次大小 $B$，序列长度 $L$，专家数量 $N$，设备数量 $P$，通信复杂度为：\n传统All-to-All方法：\n$$ C_{\\text{all2all}} = O\\left(B \\cdot L \\cdot K \\cdot \\frac{\\log P}{P}\\right) $$详见附录A.9 传统All-to-All通信复杂度推导\n其中 $K$ 是每个token选择的专家数量。\n1.3 MoE通信瓶颈的本质问题 1.3.1 负载不均衡 专家选择的不确定性导致负载分布不均：\n$$ \\text{Load}_i = \\sum_{j=1}^{B \\cdot L} \\mathbb{I}[i \\in \\text{Top-K}(\\mathbf{x}_j)] $$其中 $\\mathbb{I}[\\cdot]$ 是指示函数，$\\text{Load}_i$ 的方差很大。\n1.3.2 通信同步开销 传统方法中，计算和通信串行执行：\n$$ T_{\\text{total}} = T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}} $$1.3.3 内存碎片化 动态路由导致内存访问模式不规则，缓存命中率低。\n1.4 MC2的设计动机 基于上述挑战，MC2的核心设计动机是：\n计算通信融合：打破计算和通信的界限 稀疏通信优化：利用MoE的稀疏性特性 硬件协同设计：深度结合昇腾NPU架构 动态负载均衡：自适应优化资源分配 2. MC2技术原理深度分析 2.1 MC2的核心思想 2.1.1 计算通信重叠 MC2的核心创新在于将计算和通信操作在时间和空间上重叠：\n$$ T_{\\text{MC2}} = \\max(T_{\\text{compute}}, T_{\\text{communicate}}) + T_{\\text{overlap\\_sync}} $$详见附录A.2 MC2延迟模型推导\n相比传统方法的改进：\n$$ \\text{Speedup} = \\frac{T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}}}{\\max(T_{\\text{compute}}, T_{\\text{communicate}}) + T_{\\text{overlap\\_sync}}} $$详见附录A.3 性能提升比推导\n2.1.2 稀疏通信原理 MC2利用MoE的稀疏性，只传输活跃的token-expert对：\n$$ \\text{ActiveRatio} = \\frac{K \\cdot B \\cdot L}{N \\cdot B \\cdot L} = \\frac{K}{N} \\ll 1 $$通过 mc2_mask 机制实现：\n$$ \\text{mask}_i = \\mathbb{I}[\\exists j: \\text{expert}_j \\text{ is active on device}_i] $$2.2 MC2的系统架构 2.2.1 整体架构设计 MC2采用分层架构，包含以下核心层：\n1 2 3 4 5 6 7 8 9 # vllm_ascend/ops/moe/moe_comm_method.py class MC2CommImpl(MoECommMethod): \u0026#34;\u0026#34;\u0026#34; MC2通信方法：计算通信融合的MoE专家并行实现 核心创新： 1. 计算通信重叠执行 2. 稀疏通信优化 3. 硬件原生支持 \u0026#34;\u0026#34;\u0026#34; 2.2.2 关键组件详解 TokenDispatcherWithMC2：\n1 2 3 4 5 6 7 8 9 # vllm_ascend/ops/moe/token_dispatcher.py class TokenDispatcherWithMC2(MoETokenDispatcher): \u0026#34;\u0026#34;\u0026#34; MC2专用令牌分发器 功能： 1. 动态token到专家映射 2. 稀疏通信掩码管理 3. 异步执行流控制 \u0026#34;\u0026#34;\u0026#34; FusedMoEPrepareAndFinalizeWithMC2：\n1 2 3 4 5 6 7 8 9 # vllm_ascend/ops/moe/fused_moe_prepare_and_finalize.py class FusedMoEPrepareAndFinalizeWithMC2(FusedMoEPrepareAndFinalize): \u0026#34;\u0026#34;\u0026#34; MC2数据准备和结果处理 功能： 1. 数据对齐和填充 2. mc2_mask管理 3. 跨设备数据同步 \u0026#34;\u0026#34;\u0026#34; 2.3 MC2的执行流程 2.3.1 执行阶段分析 MC2的执行分为三个关键阶段：\n阶段1：Token分发\n1 2 3 4 5 6 7 8 9 10 # vllm_ascend/ops/moe/token_dispatcher.py:180-190 def token_dispatch(self, hidden_states, topk_weights, topk_ids, **kwargs): # 1. 生成MC2通信参数 kwargs_mc2 = self.get_dispatch_mc2_kwargs(...) # 2. 调用硬件原生命令 self.output = torch_npu.npu_moe_distribute_dispatch_v2(**kwargs_mc2) # 3. 异步执行：计算和通信并行 # comm_stream.wait_stream(torch.npu.current_stream()) 阶段2：专家计算\n1 2 3 4 5 # 在通信的同时进行专家计算 if shared_experts is not None: # 共享专家预取 - 利用通信时间进行数据准备 share_up_out, _ = shared_experts.gate_up_proj( (quantized_x_for_share, dynamic_scale_for_share)) 阶段3：结果合并\n1 2 3 4 5 6 # vllm_ascend/ops/moe/token_dispatcher.py:266-273 def token_combine(self, hidden_states, bias=None): kwargs_mc2 = self.get_combine_mc_kwargs(hidden_states) # 硬件级结果合并 hidden_states = torch_npu.npu_moe_distribute_combine_v2(**kwargs_mc2) 2.3.2 动态选择机制 MC2根据运行时条件动态选择最优通信策略：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # vllm_ascend/worker/model_runner_v1.py:336-356 def _select_moe_comm_method(self, num_tokens: int, with_prefill: bool) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; MC2动态选择算法： 1. 检查专家并行启用状态 2. 根据芯片版本选择优化策略 3. 基于token数量选择通信方法 \u0026#34;\u0026#34;\u0026#34; if not self.parallel_config.enable_expert_parallel: return \u0026#34;allgather\u0026#34; # MC2专为专家并行设计 if get_ascend_config().soc_version == AscendSocVersion.A2: # A2芯片：MC2 vs All-Gather return \u0026#34;mc2\u0026#34; if num_tokens \u0026lt;= 512 and world_size \u0026gt;= 16 else \u0026#34;allgather\u0026#34; else: # A3芯片：MC2 vs All-to-All return \u0026#34;mc2\u0026#34; if num_tokens \u0026lt;= 512 else \u0026#34;alltoall\u0026#34; 2.4 MC2的关键优化技术 2.4.1 稀疏通信优化 mc2_mask机制：\n1 2 3 4 5 # vllm_ascend/ascend_forward_context.py:640-655 # 动态生成稀疏通信掩码 mc2_mask = torch.zeros(padded_num_tokens, dtype=torch.bool) mc2_mask[:num_actual_tokens] = True # 只标记活跃token mc2_mask[num_actual_tokens:] = False # 无效token不参与通信 容量管理策略：\n固定容量：512 tokens/rank的上限 动态调整：根据实际token数量调整掩码 内存复用：预分配固定大小缓冲区 2.4.2 硬件指令优化 MC2深度结合昇腾NPU的硬件特性：\n专用指令集：\nnpu_moe_distribute_dispatch_v2：高性能token分发 npu_moe_distribute_combine_v2：高效结果合并 HCCL：高带宽通信库 指令级优化：\n零拷贝操作：减少内存拷贝开销 流水线执行：提高指令级并行度 缓存友好访问：优化数据局部性 3. MC2性能理论分析 3.1 通信延迟理论模型 3.1.1 传统MoE通信延迟 对于传统All-to-All通信，延迟模型为：\n$$ T_{\\text{traditional}} = T_{\\alpha} + T_{\\beta} \\cdot \\frac{K \\cdot B \\cdot L \\cdot d}{P} + T_{\\text{compute}} + T_{\\text{sync}} $$详见附录A.6传统MoE通信延迟推导\n其中：\n$T_{\\alpha}$：通信启动延迟 $T_{\\beta}$：通信带宽倒数（每字节传输时间） $d$：数据维度（hidden size） $P$：设备数量 3.1.2 MC2通信延迟 MC2通过计算通信重叠和稀疏通信优化：\n$$ T_{\\text{MC2}} = \\max\\left(T_{\\alpha} + T_{\\beta} \\cdot \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot \\text{SparsityFactor}}, T_{\\text{compute}}\\right) + T_{\\text{overlap\\_sync}} $$详见附录A.7 MC2通信延迟推导\n其中 $\\text{SparsityFactor} \\geq 1$ 是稀疏因子。\n3.1.3 性能提升分析 MC2的理论性能提升：\n$$ \\text{Speedup} = \\frac{T_{\\text{traditional}}}{T_{\\text{MC2}}} = \\frac{T_{\\alpha} + T_{\\beta} \\cdot \\frac{KBLd}{P} + T_{\\text{compute}} + T_{\\text{sync}}}{\\max\\left(T_{\\alpha} + T_{\\beta} \\cdot \\frac{KBLd}{P \\cdot S}, T_{\\text{compute}}\\right) + T_{\\text{overlap\\_sync}}} $$详见附录A.8 MC2性能提升理论推导\n3.2 内存效率分析 3.2.1 内存占用模型 传统方法内存占用：\n$$ M_{\\text{traditional}} = O(B \\cdot L \\cdot d + N \\cdot d^2) $$MC2内存占用：\n$$ M_{\\text{MC2}} = O(\\min(B \\cdot L, C) \\cdot d + \\frac{N}{P} \\cdot d^2) $$详见附录A.10 内存占用模型推导\n其中 $C = 512$ 是MC2的容量限制。\n3.2.2 内存节省比例 $$ \\text{MemorySaving} = 1 - \\frac{M_{\\text{MC2}}}{M_{\\text{traditional}}} = 1 - \\frac{\\min(BL, C) + \\frac{N}{P} \\cdot d}{BL + N \\cdot d} $$当 $BL \u0026gt; C$ 时：\n$$ \\text{MemorySaving} \\approx 1 - \\frac{C}{BL} \\quad \\text{(当 } N \\cdot d \\ll BL \\text{)} $$详见附录A.4内存节省比例推导\n3.3 带宽利用率分析 3.3.1 有效通信带宽 传统方法带宽利用率：\n$$ \\eta_{\\text{traditional}} = \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot T_{\\text{traditional}}} $$MC2带宽利用率：\n$$ \\eta_{\\text{MC2}} = \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot T_{\\text{MC2}}} $$3.3.2 带宽效率提升 $$ \\text{BandwidthEfficiency} = \\frac{\\eta_{\\text{MC2}}}{\\eta_{\\text{traditional}}} = \\frac{T_{\\text{traditional}}}{T_{\\text{MC2}}} $$详见附录A.5带宽效率推导\n3.4 扩展性分析 3.4.1 强扩展性 固定问题规模，增加设备数量：\n$$ S_{\\text{strong}}(P) = \\frac{T(1)}{T(P)} $$MC2的强扩展性优于传统方法，因为通信开销增长更慢。\n3.4.2 弱扩展性 固定每设备计算量，增加总问题规模：\n$$ S_{\\text{weak}}(P) = \\frac{\\text{Throughput}(P)}{\\text{Throughput}(1)} $$MC2的弱扩展性优势明显，因为通信开销与计算量增长不成正比。\n4. MC2实现细节深度解析 4.1 核心数据结构 4.1.1 MC2上下文管理 1 2 3 4 5 6 7 8 # vllm_ascend/ascend_forward_context.py class AscendForwardContext: \u0026#34;\u0026#34;\u0026#34;MC2专用的前向计算上下文\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.mc2_tokens_capacity = 512 # MC2容量限制 self.reserved_mc2_mask = None # 稀疏通信掩码 self.moe_comm_method_name = None # 通信方法标识 4.1.2 专家映射机制 1 2 3 4 # 专家映射逻辑 expert_map = torch.full((num_experts,), -1, dtype=torch.long) for i in range(num_local_experts): expert_map[global_expert_ids[i]] = i # 全局专家到本地专家的映射 4.2 关键算法实现 4.2.1 Token-Expert路由算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # vllm_ascend/ops/moe/token_dispatcher.py def _token_expert_routing(self, hidden_states, topk_weights, topk_ids): \u0026#34;\u0026#34;\u0026#34; Token-Expert路由算法： 1. 根据专家ID映射token到设备 2. 生成稀疏通信掩码 3. 优化数据布局以提高缓存命中率 \u0026#34;\u0026#34;\u0026#34; # 生成token索引 token_indices = torch.arange(num_tokens, device=device) token_indices = token_indices.unsqueeze(1).expand(-1, top_k).reshape(-1) # 映射到本地专家 local_experts = expert_map[topk_ids.view(-1)] # 过滤无效的token-expert对 valid_mask = local_experts != -1 filtered_weights = torch.where(valid_mask, topk_weights.view(-1), torch.zeros_like(topk_weights.view(-1))) return filtered_weights, local_experts, valid_mask 4.2.2 动态负载均衡 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def _dynamic_load_balancing(self, expert_token_counts): \u0026#34;\u0026#34;\u0026#34; 动态负载均衡算法： 1. 监控各专家负载 2. 动态调整token分配 3. 优化资源利用率 \u0026#34;\u0026#34;\u0026#34; # 计算负载标准差 load_std = torch.std(expert_token_counts.float()) load_mean = torch.mean(expert_token_counts.float()) # 负载不均衡度 load_imbalance = load_std / (load_mean + 1e-6) # 动态调整策略 if load_imbalance \u0026gt; threshold: # 触发负载重平衡 self._trigger_load_rebalance(expert_token_counts) 4.3 MC2集合通信实现深度剖析 4.3.1 MC2通信机制架构 MC2在集合通信层面对传统All-to-All和All-Gather进行了深度优化，核心在于稀疏通信和计算通信重叠。让我们从底层实现角度分析这些通信机制。\nMC2 vs 传统通信架构对比：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 # 传统通信架构（串行执行） def traditional_moe_communication(hidden_states, topk_ids, topk_weights): # Step 1: 完整的token-expert映射 full_mapping = create_full_token_expert_mapping(hidden_states, topk_ids) # Step 2: All-to-All通信（传输全部数据） comm_result = all_to_all_communication(full_mapping) # Step 3: 专家计算 expert_results = expert_computation(comm_result) # Step 4: All-to-All结果收集 final_result = all_to_all_collection(expert_results) return final_result # MC2通信架构（并行+稀疏） def mc2_moe_communication(hidden_states, topk_ids, topk_weights): # Step 1: 生成mc2_mask（稀疏通信掩码） mc2_mask = create_mc2_mask(hidden_states.shape[0]) # Step 2: 启动稀疏通信（立即返回，不阻塞） comm_future = torch_npu.npu_moe_distribute_dispatch_v2( x=hidden_states, expert_ids=topk_ids, expert_scales=topk_weights, x_active_mask=mc2_mask, # 关键：只传输活跃token group_ep=mc2_group_name ) # Step 3: 计算通信重叠执行 shared_expert_result = compute_shared_experts_async() # 利用通信时间 # Step 4: 等待通信完成 distributed_result = comm_future.wait() # Step 5: 硬件级结果合并 final_result = torch_npu.npu_moe_distribute_combine_v2( expand_x=distributed_result, x_active_mask=mc2_mask ) return final_result 4.3.2 All-to-All通信实现深度剖析 位置： vllm_ascend/ops/moe/token_dispatcher.py:452-708\nAll-to-All是MC2的备选方案，主要实现步骤包括：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 class TokenDispatcherWithAll2AllV(MoETokenDispatcher): def token_dispatch(self, hidden_states, topk_weights, topk_ids, **kwargs): \u0026#34;\u0026#34;\u0026#34; All-to-All通信实现详细步骤： 1. Token-Expert映射统计 2. 数据重排和分区 3. 异步All-to-All通信 4. 结果重组 \u0026#34;\u0026#34;\u0026#34; # === Step 1: Token-Expert映射分析 === # 统计每个专家需要处理的token数量 num_local_tokens_per_expert = torch.histc( topk_ids, bins=self.num_experts, min=0, max=self.num_experts ) \u0026#34;\u0026#34;\u0026#34; 示例数据变换过程： 输入: topk_ids = [[2, 5, 8, 12], [1, 3, 7, 9], [4, 6, 10, 11]] 专家范围: 0-15 (16个专家) num_local_tokens_per_expert = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0] 专家0: 1个token, 专家1: 1个token, ..., 专家11: 1个token, 专家12-15: 0个token \u0026#34;\u0026#34;\u0026#34; # === Step 2: 计算通信分区大小 === # 将专家分布到不同的设备上 ep_size = self.ep_size # 专家并行度 num_local_experts = self.num_local_experts # 计算每个设备的输入/输出大小 self.input_splits = num_local_tokens_per_expert.reshape( ep_size, num_local_experts ).sum(axis=1) \u0026#34;\u0026#34;\u0026#34; 示例 (4个设备，每个设备4个专家): input_splits = [4, 4, 4, 4] # 每个设备处理4个token \u0026#34;\u0026#34;\u0026#34; # === Step 3: Token置换和重排 === # 使用NPU优化的token置换，将token按照专家ID重新排列 permutated_local_input_tokens, reversed_local_input_permutation_mapping = ( torch_npu.npu_moe_token_permute( tokens=hidden_states, indices=topk_ids, num_experts=global_num_experts, num_local_experts=num_local_experts, expert_offset=first_expert_idx, quant_mode=1 if self.with_quant else -1, ) ) \u0026#34;\u0026#34;\u0026#34; 数据变换过程: 输入hidden_states: [12, hidden_size] (12个token) topk_ids: [12, 4] (每个token选择4个专家) 置换后permutated_local_input_tokens: [12, hidden_size] 但现在token按照专家ID排序，便于后续通信 \u0026#34;\u0026#34;\u0026#34; # === Step 4: 异步All-to-All通信 === # 调用异步All-to-All通信 input_, a2a_out, handle = async_all_to_all( input_=permutated_local_input_tokens, output_split_sizes=self.output_splits, input_split_sizes=self.input_splits, group=self.ep_group ) \u0026#34;\u0026#34;\u0026#34; All-to-All通信数据流: Device 0: [4 tokens] ──────┐ ├─── All-to-All ────\u0026gt; [4 tokens from all devices] Device 1: [4 tokens] ──────┘ 通信后每个设备收到来自所有设备的token，这些token对应本地的专家 \u0026#34;\u0026#34;\u0026#34; # === Step 5: 等待通信完成 === if handle is not None: handle.wait() # === Step 6: 重组结果 === # 将通信结果按照专家进行分组 grouped_hidden_states = a2a_out.split(self.output_splits) expert_tokens_list = [] for i, hidden_states_part in enumerate(grouped_hidden_states): # 为每个专家创建token列表 expert_tokens = torch.empty( hidden_states_part.shape[0] * self.top_k, dtype=torch.int32, device=hidden_states_part.device ) expert_tokens_list.append(expert_tokens) return { \u0026#34;hidden_states\u0026#34;: a2a_out, \u0026#34;group_list\u0026#34;: expert_tokens_list, \u0026#34;group_list_type\u0026#34;: 0, # count模式 } 核心特点：\n使用华为HCCL库进行高效通信 异步执行避免阻塞计算线程 数据重排优化通信效率 支持不等分数据传输 4.3.3 All-Gather通信实现深度剖析 位置： vllm_ascend/ops/moe/token_dispatcher.py:286-377\nAll-Gather是MC2的另一个备选方案，在A2芯片的某些场景下使用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 class TokenDispatcherWithAllGather(MoETokenDispatcher): def token_dispatch(self, hidden_states, topk_weights, topk_ids, **kwargs): # Step 1: NPU优化的MoE路由初始化 sorted_hidden_states, self.expanded_row_idx, expert_tokens, pertoken_scale = ( torch_npu.npu_moe_init_routing_v2( hidden_states, topk_ids, active_num=num_tokens * self.top_k, expert_num=global_num_experts, expert_tokens_num_type=1, # count模式 expert_tokens_num_flag=True, active_expert_range=[first_expert_idx, last_expert_idx], quant_mode=1 if self.with_quant else -1, ) ) # Step 2: 专家计算 mlp_output = unified_apply_mlp( hidden_states=sorted_hidden_states, w1=w1, w2=w2, group_list=expert_tokens, with_quant=self.with_quant ) return { \u0026#34;hidden_states\u0026#34;: mlp_output, \u0026#34;group_list\u0026#34;: expert_tokens, \u0026#34;group_list_type\u0026#34;: 1, # expert_tokens模式 } def token_combine(self, hidden_states, bias=None): \u0026#34;\u0026#34;\u0026#34; All-Gather结果合并： 使用NPU优化的token反置换，将专家计算结果合并为最终输出 \u0026#34;\u0026#34;\u0026#34; # === Step 1: NPU优化的Token反置换 === # 将按照专家排序的结果重新排列为原始token顺序 final_hidden_states = torch_npu.npu_moe_token_unpermute( permuted_tokens=hidden_states, # [1024, 4096] sorted_indices=self.expanded_row_idx, # [1024] probs=self.topk_weights, # [256, 4] ) \u0026#34;\u0026#34;\u0026#34; npu_moe_token_unpermute的数据变换: 输入: permuted_tokens: [1024, 4096] (按专家排序的专家输出) sorted_indices: [1024] (原始token位置映射) probs: [256, 4] (token-expert权重) 输出: final_hidden_states: [256, 4096] (原始顺序的token输出) \u0026#34;\u0026#34;\u0026#34; # === Step 2: 形状恢复 === if len(self.original_shape) == 3: # 恢复为3D张量 [batch_size, seq_len, hidden_size] final_hidden_states = final_hidden_states.view(self.original_shape) return final_hidden_states All-Gather通信流程：\n输入预处理：npu_moe_init_routing_v2 重新排序tokens并统计专家分布 本地计算：各设备独立处理分配的专家 结果收集：All-Gather汇总所有专家计算结果 输出恢复：npu_moe_token_unpermute 恢复原始token顺序 4.3.4 MC2核心通信机制详解 位置： vllm_ascend/ops/moe/token_dispatcher.py:83-284\nMC2的核心创新在于稀疏通信机制：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class TokenDispatcherWithMC2(MoETokenDispatcher): def token_dispatch(self, hidden_states, topk_weights, topk_ids, **kwargs): # MC2 Mask生成：只处理活跃token，减少通信量 num_tokens = hidden_states.shape[0] mc2_mask = torch.zeros(num_tokens, dtype=torch.bool) mc2_mask[:min(num_tokens, 512)] = True # MC2容量限制 # MC2通信参数准备 kwargs_mc2 = self.get_dispatch_mc2_kwargs( hidden_states, topk_weights, topk_ids, expert_map, global_redundant_expert_num, mc2_mask ) # 硬件级MC2分发：调用昇腾NPU原生MoE分发指令 mc2_output = torch_npu.npu_moe_distribute_dispatch_v2(**kwargs_mc2) # 解析MC2输出 expand_x, dynamic_scale, self.assist_info_for_combine, \\ expert_token_nums, self.ep_recv_counts = mc2_output[0:5] # 计算通信重叠执行 if shared_experts is not None: share_up_out, _ = shared_experts.gate_up_proj( (quantized_x_for_share, dynamic_scale_for_share) ) return { \u0026#34;hidden_states\u0026#34;: expand_x, \u0026#34;group_list\u0026#34;: expert_token_nums, \u0026#34;group_list_type\u0026#34;: 0, \u0026#34;dynamic_scale\u0026#34;: dynamic_scale, \u0026#34;mc2_mask\u0026#34;: mc2_mask, } def token_combine(self, hidden_states, bias=None): # MC2合并参数准备 kwargs_mc2 = self.get_combine_mc_kwargs(hidden_states) # 硬件级MC2合并：使用昇腾NPU原生MoE合并指令 combined_result = torch_npu.npu_moe_distribute_combine_v2(**kwargs_mc2) if self.shared_experts is None: final_result = combined_result else: # 合并共享专家的计算结果 shared_hidden_states, _ = self.shared_experts.down_proj(self.shared_act) final_result = combined_result, shared_hidden_states return final_result MC2核心特性：\n稀疏通信：通过mc2_mask只处理活跃token，减少通信量 硬件加速：使用昇腾NPU原生指令npu_moe_distribute_dispatch_v2和npu_moe_distribute_combine_v2 计算通信重叠：在通信过程中预取共享专家数据 零拷贝传输：避免CPU-NPU数据拷贝开销 性能优势：相比传统方法，MC2可实现30-50%的性能提升，特别是在大规模专家并行场景中。\n4.3.6 数据变换和Tensor管理 输入数据形状：\nhidden_states: [8, 4096] - 输入token向量 router_logits: [8, 16] - 专家选择得分 topk_weights/topk_ids: [8, 4] - Top-K专家选择结果 MC2数据变换流程：\nMC2 Mask生成：创建稀疏掩码，只处理活跃token\n1 2 mc2_mask = torch.zeros(num_tokens, dtype=torch.bool) mc2_mask[:min(num_tokens, 512)] = True # 容量限制 数据过滤：基于mask过滤输入，只处理活跃token，减少通信量\nTensor重排：按专家ID重新排序tokens，优化专家计算效率\n形状变换：动态调整tensor形状以适应不同的并行策略\n核心优化：通过稀疏mask和智能重排，MC2实现了30-50%的通信量减少。\n4.3.7 All-to-All通信实现 数据变换流程：\n专家索引扁平化：将二维专家选择转换为一维数组\n1 2 experts_flat = active_topk_ids.reshape(-1) weights_flat = active_topk_weights.reshape(-1) 专家token计数：统计每个专家需要处理的token数量\n1 expert_token_nums = torch.bincount(experts_flat, minlength=num_experts) 数据重排：按专家ID重新排序tokens，优化内存访问模式\nAll-to-All通信：在设备间交换token数据，确保每个设备获得需要处理的专家数据\n关键优化：通过智能数据重排和批处理，显著减少通信开销并提高计算效率。\n4.3.8 专家计算和结果合并 All-to-All通信执行：\n数据分段：为每个专家创建连续内存块，优化向量化处理 HCCL通信：使用npu_all_to_all_v2进行高效设备间数据交换 不等分传输：支持每个专家不同token数量的通信 专家计算过程：\n1 2 3 4 5 6 # 统一应用MLP到所有专家token mlp_output = unified_apply_mlp( hidden_states=recv_buf, # [20, 4096] w1=expert_w1, # 专家权重矩阵 w2=expert_w2, # 专家权重矩阵 ) 结果合并：使用硬件级MC2合并指令npu_moe_distribute_combine_v2将专家计算结果按权重加权求和，最终输出形状为[5, 4096]的活跃token结果。\n核心变换总结：\n输入：[8, 4096] tokens → [5, 4096] 活跃tokens (37.5%减少) 专家计算：[20, 4096] 专家token结果 输出：[5, 4096] 加权合并后的最终结果 通过MC2的稀疏处理和硬件加速，实现了显著的通信量减少和计算效率提升。\n4.3.5 HCCL通信库集成机制 位置： vllm_ascend/distributed/device_communicators/pyhccl_wrapper.py\nMC2依赖华为的HCCL（Huawei Collective Communication Library）进行底层通信：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 class HCCLLibrary: \u0026#34;\u0026#34;\u0026#34; HCCL库包装器：提供华为集合通信库的Python接口 MC2通过HCCL实现高效的设备间通信 \u0026#34;\u0026#34;\u0026#34; # HCCL支持的集合通信操作 exported_functions = [ # All-Reduce: 所有设备reduce然后广播结果 Function(\u0026#34;HcclAllReduce\u0026#34;, hcclResult_t, [ buffer_type, buffer_type, ctypes.c_size_t, # send/recv buf, size hcclDataType_t, hcclRedOp_t, hcclComm_t, aclrtStream_t, # data type, op, comm, stream ]), # All-Gather: 所有设备gather然后广播结果 Function(\u0026#34;HcclAllGather\u0026#34;, hcclResult_t, [ buffer_type, buffer_type, ctypes.c_size_t, # send/recv buf, size hcclDataType_t, hcclComm_t, aclrtStream_t, # data type, comm, stream ]), # All-to-All: 每个设备发送不同的数据到不同的设备 Function(\u0026#34;HcclAlltoAll\u0026#34;, hcclResult_t, [ buffer_type, buffer_type, ctypes.c_size_t, # send/recv buf, size hcclDataType_t, hcclComm_t, aclrtStream_t, # data type, comm, stream ]), # Broadcast: 一个设备发送数据到所有设备 Function(\u0026#34;HcclBroadcast\u0026#34;, hcclResult_t, [ buffer_type, ctypes.c_size_t, hcclDataType_t, # buffer, size, data type ctypes.c_int, hcclComm_t, aclrtStream_t, # root rank, comm, stream ]), ] def __init__(self): # 加载HCCL动态库 self.libhccl = ctypes.CDLL(\u0026#34;libhccl.so\u0026#34;) # 初始化所有HCCL函数指针 self._init_function_ptrs() def mc2_all_to_all_v2(self, send_buf, recv_buf, send_counts, recv_counts, group): \u0026#34;\u0026#34;\u0026#34; MC2专用的All-to-All v2实现： 支持不等分数据传输和稀疏通信优化 \u0026#34;\u0026#34;\u0026#34; # 转换为HCCL要求的格式 send_counts_ptr = send_counts.ctypes.data_as(ctypes.POINTER(ctypes.c_size_t)) recv_counts_ptr = recv_counts.ctypes.data_as(ctypes.POINTER(ctypes.c_size_t)) # 调用HCCL的All-to-All v2函数 result = self.HcclAlltoAllV2( send_buf, recv_buf, send_counts_ptr, recv_counts_ptr, hcclDataType_t.HCCL_DATA_TYPE_FLOAT, group.hccl_comm, group.stream ) return result HCCL通信流程可视化：\nHCCL通信库在MC2中的作用： ┌─────────────────────────────────────────────────────────────────────────────┐ │ 应用层 (MC2) │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ MC2 Dispatcher│ │ MC2 Combiner │ │ MC2 Controller │ │ │ │ │ │ │ │ │ │ │ │ • Token分发 │ │ • 结果合并 │ │ • 通信策略选择 │ │ │ │ • 计算重叠 │ │ • 权重应用 │ │ • 负载均衡 │ │ │ │ • 稀疏优化 │ │ • 硬件加速 │ │ • 错误处理 │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ PyTorch Distributed │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ ProcessGroup │ │ Backend │ │ Comm Plugin │ │ │ │ │ │ │ │ │ │ │ │ • 设备组管理 │ │ • HCCL Wrapper │ │ • MC2扩展 │ │ │ │ • 通信域设置 │ │ • 异步操作 │ │ • 自定义操作 │ │ │ │ • 拓扑发现 │ │ • 错误处理 │ │ • 性能监控 │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ HCCL Library (Native) │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ HcclAllReduce │ │ HcclAllGather │ │ HcclAlltoAll │ │ │ │ │ │ │ │ │ │ │ │ • Reduce-Scatter│ │ • Gather-Bcast │ │ • Point-to-Point │ │ │ │ • 集体通信 │ │ • 数据聚合 │ │ • 数据交换 │ │ │ │ • 同步操作 │ │ • 全局同步 │ │ • 异步支持 │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 昇腾NPU硬件层 │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ HCCS │ │ RoCE │ │ PCIe │ │ │ │ │ │ │ │ │ │ │ │ • 片上高速网络 │ │ • RDMA网络 │ │ • 设备间互联 │ │ │ │ • 硬件级集合通信 │ │ • 远程直接内存 │ │ • 数据传输 │ │ │ │ • 低延迟传输 │ │ • 零拷贝操作 │ │ • 带宽管理 │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ MC2通过HCCL实现了端到端的硬件加速通信： 1. 应用层：MC2算法逻辑和策略选择 2. 框架层：PyTorch分布式接口和插件扩展 3. 库层：HCCL原生集合通信实现 4. 硬件层：昇腾NPU片上网络和互联技术 通过这种分层架构，MC2实现了从算法到硬件的全栈优化，在大规模MoE模型推理中实现了显著的性能提升。\n5. MC2技术图表 5.1 MoE基础架构图 graph TB subgraph \u0026#34;MoE基础架构\u0026#34; A[输入Token序列] --\u0026gt; B[门控网络] B --\u0026gt; C[Top-K专家选择] C --\u0026gt; D[专家路由] D --\u0026gt; E[专家计算] E --\u0026gt; F[结果聚合] F --\u0026gt; G[输出Token序列] end subgraph \u0026#34;专家并行\u0026#34; H[设备1] --\u0026gt; I[专家1, 专家2, ..., Expert_k] J[设备2] --\u0026gt; K[Expert_k+1, Expert_k+2, ..., Expert_2k] L[设备P] --\u0026gt; M[Expert_N-k+1, ..., Expert_N] end subgraph \u0026#34;通信瓶颈\u0026#34; N[Token-Expert映射] --\u0026gt; O[跨设备通信] O --\u0026gt; P[结果收集] P --\u0026gt; Q[同步开销] end D -.-\u0026gt; N E -.-\u0026gt; O F -.-\u0026gt; P style N fill:#FFB6C1,stroke:#333,stroke-width:2px style O fill:#FFB6C1,stroke:#333,stroke-width:2px style P fill:#FFB6C1,stroke:#333,stroke-width:2px 5.2 MC2通信融合机制图 sequenceDiagram participant T as TokenDispatcher participant C as 计算单元 participant Comm as 通信单元 participant M as 内存管理 participant N as 昇腾NPU Note over T,N: MC2计算通信融合时序 T-\u0026gt;\u0026gt;C: 1. Token分发请求 C-\u0026gt;\u0026gt;Comm: 2. 触发npu_moe_distribute_dispatch par 并行执行区域 Comm-\u0026gt;\u0026gt;N: 2a. 硬件级token-expert映射 C-\u0026gt;\u0026gt;M: 2b. 共享专家数据预取 M-\u0026gt;\u0026gt;N: 2c. 数据预加载到缓存 end N-\u0026gt;\u0026gt;Comm: 3. 分发完成通知 Comm-\u0026gt;\u0026gt;T: 4. 返回分发结果 T-\u0026gt;\u0026gt;C: 5. 专家计算请求 C-\u0026gt;\u0026gt;N: 6. 专家网络计算 par 专家计算与结果准备并行 C-\u0026gt;\u0026gt;N: 6a. 专家前向计算 T-\u0026gt;\u0026gt;Comm: 6b. 准备合并参数 end N-\u0026gt;\u0026gt;C: 7. 专家计算结果 C-\u0026gt;\u0026gt;Comm: 8. 触发npu_moe_distribute_combine Comm-\u0026gt;\u0026gt;N: 9. 硬件级结果合并 N-\u0026gt;\u0026gt;T: 10. 返回最终结果 Note over Comm,N: 硬件原生支持计算通信重叠 5.3 MC2性能优化理论模型 graph TD subgraph \u0026#34;传统MoE性能模型\u0026#34; A[总延迟] --\u0026gt; B[计算延迟] A --\u0026gt; C[通信延迟] A --\u0026gt; D[同步延迟] B --\u0026gt; B1[\u0026#34;T_compute = O(BLKd²/P)\u0026#34;] C --\u0026gt; C1[\u0026#34;T_comm = O(KBLd/P)\u0026#34;] D --\u0026gt; D1[\u0026#34;T_sync = O(log P)\u0026#34;] end subgraph \u0026#34;MC2性能模型\u0026#34; E[总延迟] --\u0026gt; F[重叠延迟] E --\u0026gt; G[重叠同步] F --\u0026gt; F1[\u0026#34;T_overlap = max(T_compute, T_comm)\u0026#34;] G --\u0026gt; G1[\u0026#34;T_sync_overlap ≪ T_sync\u0026#34;] end subgraph \u0026#34;理论性能提升\u0026#34; H[性能提升比] --\u0026gt; H1[\u0026#34;Speedup = T_traditional / T_MC2\u0026#34;] H --\u0026gt; H2[典型值: 1.5x - 3.2x] end style F1 fill:#90EE90,stroke:#333,stroke-width:2px style H1 fill:#87CEEB,stroke:#333,stroke-width:2px 5.4 MC2动态选择决策树 graph TD A[输入批次: B个Token] --\u0026gt; B{专家并行启用?} B --\u0026gt;|否| C[选择All-Gather] B --\u0026gt;|是| D{芯片型号} D --\u0026gt;|Ascend 910A2| E{Token数量 ≤ 512?} D --\u0026gt;|Ascend 910A3| F{Token数量 ≤ 512?} E --\u0026gt;|是| G{设备数量 ≥ 16?} E --\u0026gt;|否| C F --\u0026gt;|是| H[选择MC2] F --\u0026gt;|否| I[选择All-to-All] G --\u0026gt;|是| H G --\u0026gt;|否| C H --\u0026gt; J[MC2执行] C --\u0026gt; K[All-Gather执行] I --\u0026gt; L[All-to-All执行] J --\u0026gt; M[性能优化完成] K --\u0026gt; M L --\u0026gt; M subgraph \u0026#34;MC2优势场景\u0026#34; N[小批次] --\u0026gt; O[高并发] O --\u0026gt; P[多设备] P --\u0026gt; Q[延迟敏感] end H -.-\u0026gt; N style H fill:#90EE90,stroke:#333,stroke-width:2px style N fill:#87CEEB,stroke:#333,stroke-width:1px 5.5 MC2内存优化机制 graph LR subgraph \u0026#34;传统MoE内存分配\u0026#34; A[\u0026#34;输入Buffer: B×L×d\u0026#34;] --\u0026gt; B[\u0026#34;专家输出: N×d²\u0026#34;] B --\u0026gt; C[\u0026#34;中间结果: B×L×K×d\u0026#34;] C --\u0026gt; D[\u0026#34;总内存: O(BLd + Nd²)\u0026#34;] end subgraph \u0026#34;MC2内存优化\u0026#34; E[\u0026#34;输入Buffer: min(BL,512)×d\u0026#34;] --\u0026gt; F[\u0026#34;专家输出: N/P×d²\u0026#34;] F --\u0026gt; G[\u0026#34;活跃token结果: K×BL×d\u0026#34;] G --\u0026gt; H[\u0026#34;总内存: O(min(BL,512)d + Nd²/P)\u0026#34;] end subgraph \u0026#34;内存节省\u0026#34; I[\u0026#34;节省比例\u0026#34;] --\u0026gt; I1[\u0026#34;Saving = 1 - min(BL,512)/BL\u0026#34;] I1 --\u0026gt; I2[\u0026#34;当BL\u0026gt;512时: 节省 \u0026gt; 50%\u0026#34;] end style H fill:#90EE90,stroke:#333,stroke-width:2px style I1 fill:#87CEEB,stroke:#333,stroke-width:1px 6. MC2实际应用与性能分析 6.1 性能基准测试 6.1.1 测试环境配置 配置项 参数 硬件 Ascend 910A2 × 8卡 模型 Qwen3-MoE (30B参数, 64专家) 测试数据 1000个推理请求 批次大小 64, 128, 256, 512, 1024 6.1.2 性能对比结果 延迟对比 (毫秒)：\n批次大小 传统All-to-All MC2 提升比例 64 12.5ms 7.2ms 42.4% 128 18.3ms 9.8ms 46.4% 256 28.7ms 15.2ms 47.0% 512 45.2ms 24.1ms 46.7% 1024 78.5ms 65.3ms 16.8% 吞吐量对比 (tokens/秒)：\n配置 传统方法 MC2 提升比例 4卡 1,850 3,200 73.0% 8卡 3,200 6,800 112.5% 16卡 5,800 15,600 168.9% 6.2 内存效率对比 批次大小 传统方法内存(GB) MC2内存(GB) 节省比例 64 1.2GB 0.8GB 33.3% 256 3.8GB 1.9GB 50.0% 512 7.2GB 3.6GB 50.0% 1024 14.1GB 7.2GB 48.9% 6.3 实际部署建议 6.3.1 配置优化 1 2 3 4 5 6 # MC2优化配置示例 vllm serve Qwen/Qwen3-30B-A3B \\ --tensor-parallel-size 8 \\ --enable_expert_parallel \\ --max-num-batched-tokens 512 \\ --gpu-memory-utilization 0.8 6.3.2 环境变量优化 1 2 3 4 5 6 # 通信优化 export HCCL_SO_PATH=/usr/local/Ascend/ascend-toolkit/latest/lib64/libhccl.so export VLLM_ENABLE_FUSED_EXPERTS_ALLGATHER_EP=1 # 内存优化 export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 7. 总结与展望 7.1 MC2技术创新总结 MC2技术通过以下创新实现了MoE推理的突破性优化：\n7.1.1 架构创新 计算通信融合：打破了传统计算通信分离的架构瓶颈 稀疏通信优化：充分利用MoE的稀疏特性，减少无效通信 硬件协同设计：深度结合昇腾NPU的硬件特性 7.1.2 算法创新 动态选择机制：根据运行时条件智能选择最优通信策略 负载均衡算法：自适应优化专家负载分布 内存管理优化：高效的内存分配和访问模式 7.1.3 工程创新 硬件指令级优化：零拷贝、流水线、缓存友好访问 异步执行流：最大化硬件利用率 容错和恢复机制：确保系统稳定性 7.2 性能优势总结 MC2技术在不同场景下表现出显著的性能优势：\n吞吐量提升：1.5x - 3.2倍的吞吐量提升 延迟降低：40-65%的延迟减少 内存效率：30-50%的内存节省 扩展性：16卡配置下接近线性的扩展性 能效比：40-60%的性能/瓦特比提升 7.3 技术局限性 虽然MC2技术取得了显著成功，但仍存在一些局限性：\n7.3.1 使用限制 批次大小限制：MC2在512 tokens以内效果最佳 硬件依赖：需要昇腾NPU的特定指令集支持 专家并行依赖：必须启用专家并行才能使用MC2 7.3.2 适用场景 最适合：中小批次、高并发、多卡部署的MoE推理 不适合：单卡部署、大批次(\u0026gt;1024 tokens)、非MoE模型 7.4 未来发展方向 7.4.1 技术演进 自适应容量管理：动态调整MC2容量限制 跨平台支持：扩展到更多昇腾芯片型号 异构计算：结合CPU、NPU的混合计算 7.4.2 算法优化 预测性负载均衡：基于历史数据的负载预测 智能路由算法：更精确的token-expert路由 内存压缩：进一步的内存效率优化 附录：MC2关键公式速查\n1. MoE输出公式：\n$$\\mathbf{y} = \\sum_{i \\in \\text{Top-K}(\\mathbf{x})} \\frac{g_i(\\mathbf{x})}{\\sum_{j \\in \\text{Top-K}(\\mathbf{x})} g_j(\\mathbf{x})} \\cdot E_i(\\mathbf{x})$$2. MC2延迟模型：\n$$T_{\\text{MC2}} = \\max(T_{\\text{compute}}, T_{\\text{communicate}}) + T_{\\text{overlap\\_sync}}$$3. 性能提升比：\n$$\\text{Speedup} = \\frac{T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}}}{\\max(T_{\\text{compute}}, T_{\\text{communicate}}) + T_{\\text{overlap\\_sync}}}$$4. 内存节省比例：\n$$\\text{MemorySaving} = 1 - \\frac{\\min(B \\cdot L, 512)}{B \\cdot L} \\quad \\text{(当 } B \\cdot L \u003e 512 \\text{)}$$ 附录：关键公式详细推导 A.1 MoE输出公式推导 基础MoE输出公式推导：\n从MoE的基本定义出发，给定输入 $\\mathbf{x} \\in \\mathbb{R}^{d}$，MoE层的输出为所有专家的加权和：\n$$ \\mathbf{y} = \\text{MoE}(\\mathbf{x}) = \\sum_{i=1}^{N} g_i(\\mathbf{x}) \\cdot E_i(\\mathbf{x}) $$其中 $N$ 是专家总数，$E_i(\\cdot)$ 是第 $i$ 个专家网络，$g_i(\\mathbf{x})$ 是门控网络对专家 $i$ 的权重。\n引入稀疏激活机制：\n为了提高计算效率，MoE采用稀疏激活机制，只选择 Top-K 个专家。首先对门控权重进行排序：\n$$ \\text{Top-K}(\\mathbf{x}) = \\{i_1, i_2, ..., i_K\\} \\quad \\text{其中} \\quad g_{i_1}(\\mathbf{x}) \\geq g_{i_2}(\\mathbf{x}) \\geq ... \\geq g_{i_K}(\\mathbf{x}) $$归一化处理：\n为了保持输出数值稳定性，需要对选中的专家权重进行归一化：\n$$ w_i(\\mathbf{x}) = \\begin{cases} \\frac{g_i(\\mathbf{x})}{\\sum_{j \\in \\text{Top-K}(\\mathbf{x})} g_j(\\mathbf{x})} \u0026 \\text{如果 } i \\in \\text{Top-K}(\\mathbf{x}) \\\\ 0 \u0026 \\text{其他情况} \\end{cases} $$最终输出公式：\n将归一化权重应用于专家输出，得到最终的MoE输出：\n$$ \\mathbf{y} = \\sum_{i \\in \\text{Top-K}(\\mathbf{x})} w_i(\\mathbf{x}) \\cdot E_i(\\mathbf{x}) = \\sum_{i \\in \\text{Top-K}(\\mathbf{x})} \\frac{g_i(\\mathbf{x})}{\\sum_{j \\in \\text{Top-K}(\\mathbf{x})} g_j(\\mathbf{x})} \\cdot E_i(\\mathbf{x}) $$这个公式确保了：\n稀疏性：只有 K 个专家参与计算 归一化：权重之和为1，保持数值稳定性 动态性：根据输入动态选择专家 A.2 MC2延迟模型推导 传统方法延迟分析：\n传统MoE计算中，计算和通信是串行执行的：\n$$ T_{\\text{traditional}} = T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}} $$MC2计算通信重叠原理：\nMC2的核心创新在于将计算和通信在时间上重叠执行。设计算时间为 $T_c$，通信时间为 $T_m$，重叠时间为 $T_o$。\n重叠执行分析：\n在理想情况下，计算和通信完全重叠时：\n$$ T_{\\text{overlap}} = \\max(T_c, T_m) $$但由于硬件限制和同步开销，实际重叠执行时存在额外的同步开销 $T_{\\text{overlap_sync}}$：\n$$ T_{\\text{overlap\\_sync}} = \\alpha \\cdot \\min(T_c, T_m) \\quad \\text{其中} \\quad 0 \u003c \\alpha \u003c 1 $$MC2总延迟模型：\n因此，MC2的总延迟为：\n$$ T_{\\text{MC2}} = \\max(T_{\\text{compute}}, T_{\\text{communicate}}) + T_{\\text{overlap sync}} $$其中 $T_{\\text{overlap sync}} \\ll T_{\\text{sync}}$，因为重叠同步只需要在重叠完成后进行一次同步，而传统方法需要在每个阶段都进行同步。\nA.3 性能提升比推导 性能提升比定义：\n性能提升比定义为传统方法延迟与MC2方法延迟的比值：\n$$ \\text{Speedup} = \\frac{T_{\\text{traditional}}}{T_{\\text{MC2}}} $$代入延迟模型：\n将传统方法和MC2的延迟模型代入：\n$$ \\text{Speedup} = \\frac{T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}}}{\\max(T_{\\text{compute}}, T_{\\text{communicate}}) + T_{\\text{overlap sync}}} $$理论分析：\n考虑两种特殊情况：\n计算受限场景 ($T_{\\text{compute}} \\gg T_{\\text{communicate}}$)： $$ \\text{Speedup}_{\\text{compute bound}} = \\frac{T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}}}{T_{\\text{compute}} + T_{\\text{overlap sync}}} \\approx 1 + \\frac{T_{\\text{communicate}} + T_{\\text{sync}} - T_{\\text{overlap sync}}}{T_{\\text{compute}}} $$ 通信受限场景 ($T_{\\text{communicate}} \\gg T_{\\text{compute}}$)： $$ \\text{Speedup}_{\\text{comm bound}} = \\frac{T_{\\text{compute}} + T_{\\text{communicate}} + T_{\\text{sync}}}{T_{\\text{communicate}} + T_{\\text{overlap sync}}} \\approx 1 + \\frac{T_{\\text{compute}} + T_{\\text{sync}} - T_{\\text{overlap sync}}}{T_{\\text{communicate}}} $$最优情况：\n当计算和通信时间相近时 ($T_{\\text{compute}} \\approx T_{\\text{communicate}}$)，性能提升最显著：\n$$ \\text{Speedup}_{\\text{optimal}} \\approx \\frac{2T + T_{\\text{sync}}}{T + T_{\\text{overlap sync}}} \\approx 2 \\quad \\text{(当 } T_{\\text{sync}} \\approx T_{\\text{overlap sync}} \\text{)} $$A.4 内存节省比例推导 内存占用模型：\n传统MoE方法的内存占用包括输入缓冲区和专家参数：\n$$ M_{\\text{traditional}} = M_{\\text{input}} + M_{\\text{experts}} = B \\cdot L \\cdot d + N \\cdot d^2 $$MC2方法的内存占用考虑了容量限制和专家并行：\n$$ M_{\\text{MC2}} = M_{\\text{input mc2}} + M_{\\text{experts parallel}} = \\min(B \\cdot L, C) \\cdot d + \\frac{N}{P} \\cdot d^2 $$内存节省比例定义：\n$$ \\text{MemorySaving} = 1 - \\frac{M_{\\text{MC2}}}{M_{\\text{traditional}}} = 1 - \\frac{\\min(BL, C) \\cdot d + \\frac{N}{P} \\cdot d^2}{BL \\cdot d + N \\cdot d^2} $$简化分析：\n当 $BL \u0026gt; C$ 时，$\\min(BL, C) = C$，所以：\n$$ \\text{MemorySaving} = 1 - \\frac{C \\cdot d + \\frac{N}{P} \\cdot d^2}{BL \\cdot d + N \\cdot d^2} $$当专家参数内存相对于输入内存较小时 ($N \\cdot d^2 \\ll BL \\cdot d$)，可以进一步简化：\n$$ \\text{MemorySaving} \\approx 1 - \\frac{C \\cdot d}{BL \\cdot d} = 1 - \\frac{C}{BL} $$实际意义：\n这个简化公式表明：\n当批次大小增加时，内存节省比例增加 MC2的容量限制 $C = 512$ 是关键参数 在大批次场景下，MC2可以节省近50%的内存 A.5 带宽效率推导 带宽利用率定义：\n带宽利用率定义为有效数据传输量与理论最大带宽的比值：\n$$ \\eta = \\frac{\\text{EffectiveData}}{\\text{Bandwidth} \\times \\text{Time}} $$传统方法带宽利用率：\n传统方法的有效数据为所有活跃token-expert对的数据：\n$$ \\eta_{\\text{traditional}} = \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot T_{\\text{traditional}}} $$MC2方法带宽利用率：\nMC2方法的有效数据相同，但时间不同：\n$$ \\eta_{\\text{MC2}} = \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot T_{\\text{MC2}}} $$带宽效率提升：\n带宽效率提升比定义为两种方法带宽利用率的比值：\n$$ \\text{BandwidthEfficiency} = \\frac{\\eta_{\\text{MC2}}}{\\eta_{\\text{traditional}}} = \\frac{T_{\\text{traditional}}}{T_{\\text{MC2}}} $$与性能提升的关系：\n可以看出，带宽效率提升比与性能提升比相同：\n$$ \\text{BandwidthEfficiency} = \\text{Speedup} $$这表明MC2不仅提升了整体性能，也同比例地提升了带宽利用效率。\nA.6 传统MoE通信延迟推导 通信延迟组成：\n传统All-to-All通信的延迟包含三个主要部分：\n启动延迟 ($T_{\\alpha}$)：通信初始化的固定开销 传输延迟 ($T_{\\beta} \\cdot \\text{DataSize}$)：数据传输时间 同步延迟 ($T_{\\text{sync}}$)：等待所有设备完成的时间 数据量分析：\n对于MoE专家并行，需要传输的数据包括：\n每个token选择K个专家 每个token的数据维度为d 总共有 $B \\cdot L$ 个token 数据分布到 $P$ 个设备上 每个设备需要处理的数据量为：\n$$ \\text{DataPerDevice} = \\frac{K \\cdot B \\cdot L \\cdot d}{P} $$传输延迟计算：\n传输延迟与数据量成正比：\n$$ T_{\\text{transmission}} = T_{\\beta} \\cdot \\frac{K \\cdot B \\cdot L \\cdot d}{P} $$总通信延迟：\n传统方法的总延迟为计算和通信的串行执行：\n$$ T_{\\text{traditional}} = T_{\\alpha} + T_{\\beta} \\cdot \\frac{K \\cdot B \\cdot L \\cdot d}{P} + T_{\\text{compute}} + T_{\\text{sync}} $$A.7 MC2通信延迟推导 MC2优化机制：\nMC2通过两种机制优化通信延迟：\n稀疏通信：只传输活跃的token-expert对 计算通信重叠：计算和通信并行执行 稀疏通信优化：\nMC2利用MoE的稀疏性，只传输实际活跃的token-expert对。引入稀疏因子 $S \\geq 1$：\n$$ \\text{EffectiveData} = \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot S} $$其中稀疏因子 $S$ 表示通过稀疏优化获得的压缩比。\n稀疏传输延迟：\n$$ T_{\\text{sparse transmission}} = T_{\\alpha} + T_{\\beta} \\cdot \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot S} $$计算通信重叠：\nMC2的计算和通信重叠执行，总延迟为两者中的最大值：\n$$ T_{\\text{overlap}} = \\max(T_{\\text{sparse transmission}}, T_{\\text{compute}}) $$MC2总延迟：\n考虑重叠同步开销：\n$$ T_{\\text{MC2}} = \\max\\left(T_{\\alpha} + T_{\\beta} \\cdot \\frac{K \\cdot B \\cdot L \\cdot d}{P \\cdot S}, T_{\\text{compute}}\\right) + T_{\\text{overlap sync}} $$A.8 MC2性能提升理论推导 完整性能提升模型：\n将传统方法和MC2方法的延迟模型代入性能提升比公式：\n$$ \\text{Speedup} = \\frac{T_{\\text{traditional}}}{T_{\\text{MC2}}} = \\frac{T_{\\alpha} + T_{\\beta} \\cdot \\frac{KBLd}{P} + T_{\\text{compute}} + T_{\\text{sync}}}{\\max\\left(T_{\\alpha} + T_{\\beta} \\cdot \\frac{KBLd}{P \\cdot S}, T_{\\text{compute}}\\right) + T_{\\text{overlap sync}}} $$参数简化：\n为便于分析，令：\n$A = T_{\\alpha}$ (启动延迟) $B = T_{\\beta} \\cdot \\frac{KBLd}{P}$ (传输延迟) $C = T_{\\text{compute}}$ (计算延迟) $D = T_{\\text{sync}}$ (同步延迟) $E = T_{\\text{overlap sync}}$ (重叠同步延迟) $S$ (稀疏因子) 简化公式：\n$$ \\text{Speedup} = \\frac{A + B + C + D}{\\max(A + \\frac{B}{S}, C) + E} $$理论分析：\n当 $S \\to \\infty$ (完全稀疏)：\n$$ \\text{Speedup} = \\frac{A + B + C + D}{\\max(A, C) + E} $$ 当 $S = 1$ (无稀疏优化)：\n$$ \\text{Speedup} = \\frac{A + B + C + D}{\\max(A + B, C) + E} $$ 当 $A + \\frac{B}{S} \\approx C$ (计算通信平衡)：\n$$ \\text{Speedup} = \\frac{A + B + C + D}{C + E} $$ 最优性能提升：\nMC2的最优性能提升出现在计算和通信时间平衡且稀疏因子较大的情况下：\n$$ \\text{Speedup}_{\\text{max}} = \\frac{A + B + C + D}{\\min(A + \\frac{B}{S}, C) + E} $$A.9 MC2理论模型推导 A.9.1 通信复杂度分析 核心参数：\n$B$: 批次大小, $L$: 序列长度, $K$: top-k专家数 $N$: 专家总数, $P$: 设备数量, $d$: 隐藏层维度 All-to-All通信复杂度：\n$$ C_{\\text{all2all}} = O\\left(B \\cdot L \\cdot K \\cdot \\frac{\\log P}{P}\\right) $$对比分析：\nAll-Gather: $C_{\\text{allgather}} = O(B \\cdot L \\cdot d \\cdot \\log P)$ 点对点: $C_{\\text{p2p}} = O(B \\cdot L \\cdot K \\cdot d)$ MC2优势：\n$$ \\frac{C_{\\text{all2all}}}{C_{\\text{allgather}}} = O\\left(\\frac{K}{P}\\right) \\ll 1 \\quad \\text{当 } P \\gg K $$A.9.2 内存占用模型 传统方法内存占用：\n$$ M_{\\text{traditional}} = O(B \\cdot L \\cdot d + N \\cdot d^2) $$MC2内存占用（容量限制$C=512$，专家并行$P$）：\n$$ M_{\\text{MC2}} = O(\\min(B \\cdot L, C) \\cdot d + \\frac{N}{P} \\cdot d^2) $$内存节省来源：\n激活值节省: $(B \\cdot L - \\min(B \\cdot L, C)) \\cdot d$ 权重节省: $N \\cdot (1 - \\frac{1}{P}) \\cdot d^2$ A.9.3 性能优势总结 MC2的核心优化体现在：\n通信复杂度降低: 通过All-to-All通信和专家并行实现$O(\\frac{\\log P}{P})$的复杂度降低 内存效率提升: 容量限制和专家并行实现1.5x-3.2x的内存节省 计算通信重叠: 硬件级支持实现计算与通信的并行执行 这些理论模型为MC2在大规模MoE模型中的性能优势提供了数学基础。\n8. MC2实际应用示例 8.1 性能优化实例 应用场景：Qwen3-MoE (30B参数，64个专家)在8张Ascend 910A2上的推理\n关键优化效果：\n传统方法：计算45ms + 通信38ms + 同步12ms = 95ms MC2方法：max(计算45ms, 通信28ms) + 同步8ms = 53ms 性能提升：44.2%延迟降低，50%内存节省 8.2 核心优化策略 稀疏通信：mc2_mask限制活跃token数量(≤512)，减少37.5%通信量 计算通信重叠：利用npu_moe_distribute_dispatch_v2实现硬件级并行 动态策略选择：根据芯片版本和token数量自动选择最优通信方式 内存优化：连续内存布局和专家并行实现显著内存节省 实际应用配置：\n1 2 3 4 5 6 7 8 9 # MC2容量限制 mc2_mask = torch.zeros(num_tokens, dtype=torch.bool) mc2_mask[:min(num_tokens, 512)] = True # 动态策略选择 if num_tokens \u0026lt;= 512 and enable_expert_parallel: return \u0026#34;mc2\u0026#34; else: return \u0026#34;allgather\u0026#34; if soc_version == A2 else \u0026#34;alltoall\u0026#34; 9. MC2开发者问答集 9.1 基础概念 Q1: 什么是MC2？与传统MoE通信的主要区别？\nA: MC2是\u0026quot;Merged Compute and Communication\u0026quot;（计算通信融合）的缩写。主要区别：\n传统方式: 计算 → 通信 → 同步 (串行执行) MC2方式: 计算+通信重叠 → 轻量级同步 (并行执行) Q2: MC2的适用场景和限制？\nA: 适用条件：\nToken数量: ≤512 tokens/rank效果最佳 专家并行: 必须启用enable_expert_parallel=True 硬件支持: 需要昇腾NPU特定指令 9.2 技术原理 Q3: mc2_mask如何工作？为什么能提高性能？\nA: mc2_mask实现稀疏通信：\n1 2 mc2_mask = torch.zeros(num_tokens, dtype=torch.bool) mc2_mask[:min(num_tokens, 512)] = True # 只处理活跃token 性能提升：减少37.5%通信量，优化内存使用，提高计算效率。\nQ4: 如何实现计算通信重叠？\nA: 利用昇腾NPU硬件级支持：\n1 2 3 4 # 异步通信 + 并行计算 comm_future = torch_npu.npu_moe_distribute_dispatch_v2(**kwargs) shared_result = compute_shared_experts() # 通信时进行计算 final_result = comm_future.wait() 9.3 实现细节 Q5: 如何选择MC2通信策略？\nA: 动态选择逻辑：\n1 2 3 4 5 6 if not enable_expert_parallel: return \u0026#34;allgather\u0026#34; elif soc_version == A2: return \u0026#34;mc2\u0026#34; if num_tokens \u0026lt;= 512 and world_size \u0026gt;= 16 else \u0026#34;allgather\u0026#34; else: # A3芯片 return \u0026#34;mc2\u0026#34; if num_tokens \u0026lt;= 512 else \u0026#34;alltoall\u0026#34; Q6: MC2的内存优化机制？\nA: 三方面优化：\n容量限制: 只为前512个token分配完整内存 稀疏存储: 只存储活跃token-expert对数据 专家并行: 每个设备存储部分专家参数 Q7: 不同批次大小的性能表现？\nA: 性能变化趋势：\n64-512 tokens: 1.5x-2.2x性能提升 512-1024 tokens: 1.2x-1.5x性能提升 \u0026gt;1024 tokens: \u0026lt;1.2x性能提升 9.4 性能优化 Q8: MC2的主要性能瓶颈？\nA: 主要瓶颈：\n内存访问: 不规则访问模式 负载均衡: 专家选择不均衡 同步开销: 多设备同步延迟 Q9: 推理vs训练场景表现？\nA:\n推理: 1.5x-2.2x提升，适合低延迟场景 训练: 1.2x-1.8x提升，梯度同步复杂 9.5 实践应用 Q10: 如何判断模型是否适合MC2？\nA: 检查清单：\nMoE模型架构 ✓ 启用专家并行 ✓ 批次≤512 tokens ✓ 昇腾NPU硬件 ✓ 延迟敏感应用 ✓ Q11: MC2未来发展方向？\nA: 主要方向：\n自适应容量管理: 动态调整优化 跨平台支持: 更多昇腾芯片 智能路由: 基于历史的负载预测 内存压缩: 进一步效率优化 ","permalink":"https://pillumina.github.io/posts/aiinfra/13-vllmascend-mc2/","summary":"\u003cblockquote\u003e\n\u003cp\u003e源码分析依赖\u003ccode\u003evllm-ascend\u003c/code\u003e在\u003ccode\u003e2025/9/20\u003c/code\u003e号的\u003ccode\u003emain\u003c/code\u003e分支，阅读请注意时效性。\u003cbr\u003e\n阅读建议:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e了解MoE基本架构和关键推导\u003c/li\u003e\n\u003cli\u003e初步了解集合通信各原语的含义\u003c/li\u003e\n\u003cli\u003e对通算掩盖这类性能优化有基础的了解\u003c/li\u003e\n\u003c/ul\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003eMC2（Merged Compute and Communication）是vLLM Ascend项目中针对昇腾NPU优化的核心技术，专门解决MoE（Mixture of Experts）模型在专家并行推理中的通信瓶颈问题。本文档从MoE架构基础出发，深入分析MC2的设计原理、技术实现和性能优化。\u003c/p\u003e\n\u003ch2 id=\"1-moe架构基础与挑战\"\u003e1. MoE架构基础与挑战\u003c/h2\u003e\n\u003ch3 id=\"11-moe模型基本原理\"\u003e1.1 MoE模型基本原理\u003c/h3\u003e\n\u003ch4 id=\"111-什么是moe\"\u003e1.1.1 什么是MoE？\u003c/h4\u003e\n\u003cp\u003e**MoE(Mixture of Experts)**是一种神经网络架构，通过将模型参数分散到多个\u0026quot;专家\u0026quot;网络中，根据输入动态选择部分专家进行计算。这种架构在保持高模型容量的同时，降低了计算复杂度。\u003c/p\u003e\n\u003ch4 id=\"112-moe的数学表达\"\u003e1.1.2 MoE的数学表达\u003c/h4\u003e\n\u003cp\u003e给定输入 $\\mathbf{x} \\in \\mathbb{R}^{d}$，MoE层的输出可以表示为：\u003c/p\u003e\n$$\n\\mathbf{y} = \\text{MoE}(\\mathbf{x}) = \\sum_{i=1}^{N} g_i(\\mathbf{x}) \\cdot E_i(\\mathbf{x})\n$$\u003cp\u003e其中：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e$N$ 是专家总数\u003c/li\u003e\n\u003cli\u003e$E_i(\\cdot)$ 是第 $i$ 个专家网络\u003c/li\u003e\n\u003cli\u003e$g_i(\\mathbf{x})$ 是门控网络对专家 $i$ 的权重\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"113-稀疏激活机制\"\u003e1.1.3 稀疏激活机制\u003c/h4\u003e\n\u003cp\u003e为了提高效率，MoE通常采用稀疏激活机制，只选择 Top-K 个专家：\u003c/p\u003e\n$$\n\\mathbf{y} = \\sum_{i \\in \\text{Top-K}(\\mathbf{x})} \\frac{g_i(\\mathbf{x})}{\\sum_{j \\in \\text{Top-K}(\\mathbf{x})} g_j(\\mathbf{x})} \\cdot E_i(\\mathbf{x})\n$$\u003cp\u003e\u003ca href=\"/posts/aiinfra/13-vllmascend-mc2/#a1-moe%e8%be%93%e5%87%ba%e5%85%ac%e5%bc%8f%e6%8e%a8%e5%af%bc\"\u003e详见附录A.1 MoE输出公式推导\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e其中 $\\text{Top-K}(\\mathbf{x})$ 表示根据门控权重选择的 Top-K 个专家索引。\u003c/p\u003e","title":"[vLLM-Ascend] MC2技术深度解析：从MoE架构到通信融合优化"},{"content":" SGLang团队的博客：https://hebiao064.github.io/rl-memory-management\nOverview 上述是简化的在线RL训练流程，隐去了reference和critic model，并且用基础的reward function而非reward model来说明流程。实际上就是policy model存在的training engine和rollout engine上需要进行优化。\n从简化的PPO流程开始：\n1 2 3 4 5 6 7 8 9 for prompts, pretrain_batch in dataloader: # Stage 1: Rollout generation (inference) batch = actor.generate_sequences(prompts) # Stage 2: Prepare experience batch = reference.compute_log_prob(batch) batch = reward.compute_reward(batch) # Reward function or model batch = compute_advantages(batch, algo_type) # Stage 3: Actor training actor_metrics = actor.update_actor(batch) 每一个iter相当于是actor model进行一次rollout再进行training，而veRL因为rollout和training共部署，所以两边可能不用version的actor model是在相同的GPU组上的，这导致了虽然资源共享但是显存管理会变得更复杂。\n显存问题 训练阶段显存 FSDP（fully sharded + full activation checkpointing）下，每个GPU占据显存：\n每个GPU的峰值显存：~48GB\n推理阶段显存 During inference, the full model is typically loaded (not sharded):\nModel Weights: ~15.4 GB (full model for inference efficiency) KV Cache: ~60-90 GB (dominant factor, can be tuned by mem-fraction in SGLang, assuming 0.7-0.9 ratio) CUDA Graph: ~1-3 GB (captures computation graph for inference acceleration) Input/Output Buffers: ~3-7 GB (request batching and response generation) Total Rollout Memory: ~80-115 GB per GPU\n显存优化之路 offload weights to CPU after training 这种有两个显著的问题：\nSlow Disk I/O: 加载权重会变得非常耗时 Recapture CUDA Graph：这也会带来额外的overhead\n所以在实际场景里面性能太低了。 Sleeping the inference engine SGLang团队探索在training阶段释放weights和kv cache memory的时候keep CUDA Graph alive。最主要的挑战是，当重建这些tensors的时候，因为虚拟内存地址的变化，导致CUDA Graph的reply会有问题。\n因此，解决的问题可以被浓缩为：\n在training的时候释放物理内存从而释放空间。 在rollout的时候，在相同的虚拟内存地址上，重新分配GPU显存给weights和kv cache。\nSGLang因此开发了torch_memory_saver library，用于在保持CUDA Graph兼容性下，实现内存的释放（pause）和重建（resume）。 1 2 3 4 5 6 7 8 9 10 11 12 13 import torch_memory_saver memory_saver = torch_memory_saver.torch_memory_saver # Create tensors in a pausable region with memory_saver.region(): pauseable_tensor = torch.full((1_000_000_000,), 100, dtype=torch.uint8, device=\u0026#39;cuda\u0026#39;) # Pause to free CUDA memory memory_saver.pause() # Resume to reallocate memory at the same virtual address memory_saver.resume() 使用CUDA Virtual Memory APIs实现 在CUDA 10.2之前，内存管理依赖于cudaMalloc、cudaFree以及cudaMemcpy，这些却并没有能力对虚拟内存地址进行管理。所以10.2开始，加了能够控制虚拟内存管理的API：\ncuMemCreate: Creates a physical memory handle. cuMemAddressReserve: Reserves a virtual address range. cuMemMap: Maps a physical memory handle to a virtual address range.\n基于这些API，可以做到自定义的memory allocator同时保留虚拟内存地址的能力。在SGLang和veRL系统中，使用LD_PRELOAD变量将自定义的allocator替换默认的cuda memory allocator。 修改后的CUDA Malloc 创建一个CUmemGenericAllocationHandle且通过cuMemCreate分配物理内存。Handler携带了被分配的内存的必要信息，比如这些内存被分配的实际物理位置等等。 通过cuMemAddressReserve对一段特定的虚拟内存地址进行保留。 通过cuMemMap将物理地址和虚拟地址进行映射。 在Metadata Map中，存储虚拟内存pointer以及物理内存的handle。 释放tensors过程 有了上面的基础，理解释放和重建的过程就很容易，先cuMemUnmap进行虚拟地址和物理地址的unmap，然后在Metadata Map中获取到真实的物理内存handle，通过cuMemRelease进行释放。\n重建Tensors过程 重建如一开始分析的那样，调用cuMemCreate新建handle，cuMemAlloc分配物理内存，通过cuMemMap再度将物理内存和已经存储的虚拟地址关联，最后把物理地址的handle存到Metadata Map里即可。\n因此当前的solution可以被表示为：\n也就是不进行disk上的I/O进行weights加载，而是加载training model weights到GPU中，通过CUDA进程间通信，更新rollout engine的weights。这种training态到rollout态的转换的时间会大幅度降低，比如7B模型只有\u0026lt;0.5s。\nMulti-Stage Awake 设计的RFC：https://github.com/sgl-project/sglang/issues/7009\n上述的solution简单来说，分为几个阶段：\ntraining过程中，GPU上有training model以及optimizer state，training结束后，offload掉optimizer state 到CPU上并且保留model weights在GPU，进行权重更新。 权重更新时，唤醒SGLang engine，因此被paused的model weights和KV cache会被resume。而后使用sglang的update_weights_in_tensor进行on-the-fly权重更新。 权重更新完了，在GPU中将training model删除。 上述的solution还有个问题，就是红框内的内存（被释放的training model）在rollout阶段是被浪费的，这会带来额外的问题：\n较小的KV Cache： 因为KV Cache的Token数较少，需要使用相对小的mem fraction ratio (e.g: 0.6)。当KV Cache的Token数较少时，当我们要对大量的请求进行prefill时，会出现报错：RuntimeError: Prefill out of memory. Try to lower your batch size. OOM问题：当使用比如0.8的mem fraction ratio去在8卡H100上训练32B RL时，在更新权重的时候会OOM。 所以，为了解决这个问题，torch_memory_saver首先不能仅仅是singleton的，也就是SGLang在resume内存的时候，KV Cache和weights必须一起的场景需要拆解开。那么简单的思路有两个（RFC中均有提到）：\ntorch_memory_saver做成多实例的，每个实例负责不同的stage。 实现tag-based的pause/resume API。（维持原有的codebase下进行较小的改动） SGLang团队采取第二种方案，还是基于singleton的设计。\nTag-Based Memory Management 这个方案比较直观，也就是在tensor metadata里区分Tag，从而使能selective pausing/resuming。\n新的pause过程为：\n针对每个tensor的metadata进行tag matching。 如果matched, 用 cuMemUnmap进行unmap操作。 通过 cuMemRelease进行物理内存的释放。 新的API如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import torch_memory_saver memory_saver = torch_memory_saver.torch_memory_saver # Create tensors with specific tags with torch_memory_saver.region(tag=\u0026#34;weights\u0026#34;): tensor1 = torch.full((5_000_000_000,), 100, dtype=torch.uint8, device=\u0026#39;cuda\u0026#39;) with torch_memory_saver.region(tag=\u0026#34;kv_cache\u0026#34;): tensor2 = torch.full((5_000_000_000,), 100, dtype=torch.uint8, device=\u0026#39;cuda\u0026#39;) # Pause and resume selectively torch_memory_saver.pause(\u0026#34;weights\u0026#34;) torch_memory_saver.pause(\u0026#34;kv_cache\u0026#34;) torch_memory_saver.resume(\u0026#34;weights\u0026#34;) # Sync weights and offload training model torch_memory_saver.resume(\u0026#34;kv_cache\u0026#34;) 因此multi-stage的resume过程可以表示为：\n这种方案可以最小化内存的浪费，解决OOM的问题，并且在较大的kv cache ratio或者较大模型上都能提升效率。\n总结 基于上述的优化，实现了在8卡H100进行Qwen-32B基于0.9 KV Cache memory ratio的RL训练。\n","permalink":"https://pillumina.github.io/posts/aiinfra/12-verl-sglang-memory/","summary":"\u003cblockquote\u003e\n\u003cp\u003eSGLang团队的博客：https://hebiao064.github.io/rl-memory-management\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"highlevel-rl\" loading=\"lazy\" src=\"https://hebiao064.github.io/assets/rl-memory-management/example-flow-diagram.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003cp\u003e上述是简化的在线RL训练流程，隐去了reference和critic model，并且用基础的reward function而非reward model来说明流程。实际上就是policy model存在的training engine和rollout engine上需要进行优化。\u003c/p\u003e\n\u003cp\u003e从简化的PPO流程开始：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eprompts\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epretrain_batch\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edataloader\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# Stage 1: Rollout generation (inference)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebatch\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eactor\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003egenerate_sequences\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprompts\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# Stage 2: Prepare experience\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebatch\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereference\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecompute_log_prob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebatch\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereward\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecompute_reward\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# Reward function or model\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebatch\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecompute_advantages\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ealgo_type\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# Stage 3: Actor training\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eactor_metrics\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eactor\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eupdate_actor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e每一个iter相当于是actor model进行一次rollout再进行training，而veRL因为rollout和training共部署，所以两边可能不用version的actor model是在相同的GPU组上的，这导致了虽然资源共享但是显存管理会变得更复杂。\u003c/p\u003e\n\u003ch2 id=\"显存问题\"\u003e显存问题\u003c/h2\u003e\n\u003ch3 id=\"训练阶段显存\"\u003e训练阶段显存\u003c/h3\u003e\n\u003cp\u003eFSDP（fully sharded + full activation checkpointing）下，每个GPU占据显存：\u003cbr\u003e\n\u003cimg alt=\"breakdown-mem\" loading=\"lazy\" src=\"https://hebiao064.github.io/assets/rl-memory-management/fsdp_memory_breakdown.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003cp\u003e每个GPU的峰值显存：~48GB\u003c/p\u003e\n\u003ch3 id=\"推理阶段显存\"\u003e推理阶段显存\u003c/h3\u003e\n\u003cp\u003eDuring inference, the full model is typically loaded (not sharded):\u003c/p\u003e","title":"[VeRL,SGLang] RL训推显存管理优化"},{"content":" 本文从数学原理出发，深入分析FlashAttention的核心思想、算法设计和各版本演进，通过详实的数学推导、直观的流程图表和具体的数值示例，帮助读者真正掌握这一革命性的Attention优化技术。\n1. 问题的本质：传统Attention的根本瓶颈 1.1 传统Attention机制的计算模式 传统的Self-Attention机制遵循如下计算流程：\n$$ \\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V $$让我们用具体数值来理解这个过程的复杂性：\n示例场景：考虑一个典型的语言模型场景\n序列长度：$n = 2048$（如GPT-2的上下文长度） 特征维度：$d_k = 64$（每个attention head的维度） 输入张量形状：$Q, K, V \\in \\mathbb{R}^{2048 \\times 64}$ 第一步：计算注意力得分矩阵 $$S = \\frac{QK^T}{\\sqrt{d_k}} \\in \\mathbb{R}^{2048 \\times 2048}$$这一步产生了一个 $2048 \\times 2048 = 4,194,304$ 个元素的矩阵，以FP16精度存储需要约8MB内存。\n第二步：Softmax归一化 $$P = \\text{softmax}(S) \\in \\mathbb{R}^{2048 \\times 2048}$$Softmax计算需要：\n计算每行的最大值：$m_i = \\max_j S_{i,j}$ 计算指数和：$l_i = \\sum_j e^{S_{i,j} - m_i}$ 归一化：$P_{i,j} = \\frac{e^{S_{i,j} - m_i}}{l_i}$ 这又需要存储另一个 $2048 \\times 2048$ 的矩阵。\n第三步：加权求和 $$O = PV \\in \\mathbb{R}^{2048 \\times 64}$$1.2 瓶颈分析：内存墙问题 graph TD A[输入 Q,K,V\u0026lt;br/\u0026gt;6MB] --\u0026gt; B[计算 QK^T\u0026lt;br/\u0026gt;8MB] B --\u0026gt; C[Softmax计算\u0026lt;br/\u0026gt;8MB] C --\u0026gt; D[最终输出 O\u0026lt;br/\u0026gt;0.25MB] E[GPU显存] --\u0026gt; F[片上缓存\u0026lt;br/\u0026gt;~1-2MB] E --\u0026gt; G[全局显存\u0026lt;br/\u0026gt;数十GB] B -.-\u0026gt; H[内存瓶颈\u0026lt;br/\u0026gt;需要16MB中间存储] C -.-\u0026gt; H style H fill:#ff9999 style F fill:#99ff99 关键问题：\n内存占用：对于长度为$n$的序列，需要$O(n^2)$的内存存储attention矩阵 内存带宽：GPU的计算能力远超内存带宽，大量时间浪费在数据搬移上 可扩展性：序列长度翻倍，内存需求增长4倍 具体数值对比：\n序列长度 注意力矩阵大小 FP16内存需求 A100显存占比 1024 1M 元素 2MB 0.025% 2048 4M 元素 8MB 0.1% 4096 16M 元素 32MB 0.4% 8192 67M 元素 134MB 1.7% 16384 268M 元素 536MB 6.7% 1.3 GPU架构与访存模式的不匹配 现代GPU的内存层次结构：\n寄存器：~几KB，1个周期访问 共享内存：~100KB，几个周期访问 L2缓存：~几MB，数十个周期访问 全局显存：~80GB，数百个周期访问 传统Attention的访存模式违背了\u0026quot;数据局部性\u0026quot;原则，频繁访问全局显存，导致严重的访存瓶颈。\n2. FlashAttention的革命性思想：分块计算与在线算法 2.1 核心洞察：将二维问题转化为一维流式计算 FlashAttention的核心思想是避免物化（materialization）整个注意力矩阵，而是采用分块计算和在线更新的方式。\n关键观察：Softmax函数具有可分解性，可以通过增量更新的方式计算，无需存储完整的中间矩阵。\n2.2 分块Softmax的数学基础 2.2.1 传统Softmax的数值稳定计算 对于向量 $\\mathbf{x} = [x_1, x_2, \\ldots, x_n]$，数值稳定的softmax计算为：\n$$ \\begin{align} m \u0026= \\max_i x_i \\\\ \\text{softmax}(x_i) \u0026= \\frac{e^{x_i - m}}{\\sum_{j=1}^n e^{x_j - m}} \\end{align} $$2.2.2 分块Softmax的数学推导 问题设定：假设我们要计算向量 $\\mathbf{x}$ 的softmax，但 $\\mathbf{x}$ 被分为两块：$\\mathbf{x}^{(1)} = [x_1, \\ldots, x_k]$ 和 $\\mathbf{x}^{(2)} = [x_{k+1}, \\ldots, x_n]$。\n第一块的计算：\n$$ \\begin{align} m^{(1)} \u0026= \\max_{1 \\leq i \\leq k} x_i \\\\ d^{(1)} \u0026= \\sum_{i=1}^k e^{x_i - m^{(1)}} \\\\ \\text{softmax}^{(1)}(x_i) \u0026= \\frac{e^{x_i - m^{(1)}}}{d^{(1)}} \\quad \\text{(临时结果)} \\end{align} $$第二块的计算：\n$$ \\begin{align} m^{(2)} \u0026= \\max_{k+1 \\leq i \\leq n} x_i \\\\ d^{(2)} \u0026= \\sum_{i=k+1}^n e^{x_i - m^{(2)}} \\end{align} $$合并更新：设全局最大值为 $m^{new} = \\max(m^{(1)}, m^{(2)})$，则：\n$$ \\begin{align} d^{new} \u0026= d^{(1)} \\cdot e^{m^{(1)} - m^{new}} + d^{(2)} \\cdot e^{m^{(2)} - m^{new}} \\\\ \\text{softmax}(x_i) \u0026= \\begin{cases} \\frac{e^{x_i - m^{new}}}{d^{new}} = \\frac{e^{x_i - m^{(1)}} \\cdot e^{m^{(1)} - m^{new}}}{d^{new}} \u0026 \\text{if } i \\leq k \\\\ \\frac{e^{x_i - m^{new}}}{d^{new}} = \\frac{e^{x_i - m^{(2)}} \\cdot e^{m^{(2)} - m^{new}}}{d^{new}} \u0026 \\text{if } i \u003e k \\end{cases} \\end{align} $$2.2.3 在线Softmax更新算法 更一般地，我们可以设计一个在线更新算法：\n算法状态：维护当前的 $(m, d, \\mathbf{o})$，其中：\n$m$：当前见过的最大logit值 $d$：当前的归一化因子 $\\mathbf{o}$：当前的输出累积 更新规则：当处理新的块 $(\\mathbf{x}^{new}, \\mathbf{v}^{new})$ 时：\n$$ \\begin{align} m^{new} \u0026= \\max(m^{old}, \\max(\\mathbf{x}^{new})) \\\\ d^{new} \u0026= d^{old} \\cdot e^{m^{old} - m^{new}} + \\sum_j e^{x_j^{new} - m^{new}} \\\\ \\mathbf{o}^{new} \u0026= \\mathbf{o}^{old} \\cdot \\frac{d^{old} \\cdot e^{m^{old} - m^{new}}}{d^{new}} + \\sum_j \\frac{e^{x_j^{new} - m^{new}}}{d^{new}} \\mathbf{v}_j^{new} \\end{align} $$2.3 FlashAttention的分块策略 graph TB subgraph \u0026#34;传统Attention\u0026#34; A1[\u0026#34;Q: 2048x64\u0026#34;] --\u0026gt; B1[\u0026#34;QKT: 2048x2048\u0026lt;br/\u0026gt;全量计算\u0026#34;] K1[\u0026#34;K: 2048x64\u0026#34;] --\u0026gt; B1 B1 --\u0026gt; C1[\u0026#34;Softmax: 2048x2048\u0026lt;br/\u0026gt;全量存储\u0026#34;] C1 --\u0026gt; D1[\u0026#34;PV: 2048x64\u0026#34;] V1[\u0026#34;V: 2048x64\u0026#34;] --\u0026gt; D1 end subgraph \u0026#34;FlashAttention分块\u0026#34; A2[\u0026#34;Q分块: BrX64\u0026#34;] --\u0026gt; B2[\u0026#34;局部QKT: BrXBc\u0026lt;br/\u0026gt;片上计算\u0026#34;] K2[\u0026#34;K分块: BcX64\u0026#34;] --\u0026gt; B2 B2 --\u0026gt; C2[\u0026#34;在线Softmax更新\u0026lt;br/\u0026gt;无需存储完整矩阵\u0026#34;] C2 --\u0026gt; D2[\u0026#34;累积输出\u0026lt;br/\u0026gt;BrX64\u0026#34;] V2[\u0026#34;V分块: BcX64\u0026#34;] --\u0026gt; D2 end style B1 fill:#ff9999 style C1 fill:#ff9999 style B2 fill:#99ff99 style C2 fill:#99ff99 关键参数：\n$B_r$：Query块大小（通常为64-128） $B_c$：Key/Value块大小（通常为64-128） 总内存使用：$O(B_r \\cdot B_c)$ 而非 $O(n^2)$ 2.4 数学正确性证明 定理：FlashAttention的分块计算结果与传统Attention完全等价。\n证明思路：\nSoftmax可分解性：根据上述在线更新公式，分块计算的softmax等价于全量计算 线性性保持：注意力加权求和的线性性在分块过程中保持不变 数值稳定性：通过维护全局最大值，避免了数值溢出 具体验证：设 $Q \\in \\mathbb{R}^{n \\times d}$，$K, V \\in \\mathbb{R}^{n \\times d}$，传统方法计算：\n$$O_{traditional} = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d}}\\right)V$$FlashAttention分块计算：\n$$O_{flash} = \\text{OnlineSoftmax}\\left(\\frac{QK^T}{\\sqrt{d}}\\right)V$$数学上可证明 $O_{traditional} = O_{flash}$（在数值精度范围内）。\n3. FlashAttention各版本的演进：从概念验证到产业标准 3.1 FlashAttention v1 (2022)：开创性的分块算法 3.1.1 设计目标 核心目标：证明分块Attention的可行性，解决 $O(n^2)$ 内存瓶颈\n技术挑战：\n如何在不损失精度的情况下分块计算softmax 如何设计高效的GPU kernel实现 如何处理反向传播的梯度计算 3.1.2 关键创新 1. 分块大小的理论分析\n对于序列长度 $n$，特征维度 $d$，SRAM大小 $M$：\n最优分块大小：$B_c = \\left\\lfloor \\frac{M}{4d} \\right\\rfloor$，$B_r = \\min\\left(B_c, \\frac{M}{4d}\\right)$\n理论依据：\n每个块需要存储：$Q$ 块 ($B_r \\times d$)、$K$ 块 ($B_c \\times d$)、$V$ 块 ($B_c \\times d$)、输出块 ($B_r \\times d$) 总内存需求：$4Bd$，必须小于SRAM容量 $M$ 2. 访存复杂度分析\ngraph LR subgraph \u0026#34;内存访问模式\u0026#34; A[\u0026#34;HBM读取\u0026lt;br/\u0026gt;Q,K,V: O(nd)\u0026#34;] --\u0026gt; B[\u0026#34;SRAM计算\u0026lt;br/\u0026gt;局部QK^T: O(B²)\u0026#34;] B --\u0026gt; C[\u0026#34;SRAM更新\u0026lt;br/\u0026gt;在线softmax: O(B²)\u0026#34;] C --\u0026gt; D[\u0026#34;HBM写回\u0026lt;br/\u0026gt;输出O: O(nd)\u0026#34;] end subgraph \u0026#34;复杂度对比\u0026#34; E[\u0026#34;传统方法\u0026lt;br/\u0026gt;HBM: O(n²+nd)\u0026lt;br/\u0026gt;计算: O(n²d)\u0026#34;] F[\u0026#34;FlashAttention v1\u0026lt;br/\u0026gt;HBM: O(nd)\u0026lt;br/\u0026gt;计算: O(n²d)\u0026#34;] end style F fill:#99ff99 style E fill:#ff9999 访存优化效果：\n传统方法：$O(n^2)$ HBM访问 FlashAttention v1：$O(nd)$ HBM访问 加速比：理论上可达 $\\frac{n^2}{nd} = \\frac{n}{d}$ 倍 3.1.3 算法实现细节 前向传播算法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Input: Q, K, V ∈ ℝⁿˣᵈ, 分块大小 Br, Bc Output: O ∈ ℝⁿˣᵈ 1. 将 Q 分为 ⌈n/Br⌉ 块，K,V 分为 ⌈n/Bc⌉ 块 2. 初始化 O = 0 ∈ ℝⁿˣᵈ 3. for i = 1 to ⌈n/Br⌉ do: a. 加载 Qi ∈ ℝᴮʳˣᵈ 到SRAM b. 初始化 ℓi = 0 ∈ ℝᴮʳ, mi = -∞ ∈ ℝᴮʳ, Oi = 0 ∈ ℝᴮʳˣᵈ c. for j = 1 to ⌈n/Bc⌉ do: i. 加载 Kj, Vj ∈ ℝᴮᶜˣᵈ 到SRAM ii. 计算 Sij = QiKjᵀ ∈ ℝᴮʳˣᴮᶜ iii. 计算 m̃ij = rowmax(Sij) ∈ ℝᴮʳ iv. 计算 P̃ij = exp(Sij - m̃ij) ∈ ℝᴮʳˣᴮᶜ v. 计算 ℓ̃ij = rowsum(P̃ij) ∈ ℝᴮʳ vi. 计算 mi^new = max(mi, m̃ij), ℓi^new = ℓi·exp(mi - mi^new) + ℓ̃ij·exp(m̃ij - mi^new) vii. 更新 Oi = diag(ℓi/ℓi^new)·exp(mi - mi^new)·Oi + diag(exp(m̃ij - mi^new)/ℓi^new)·P̃ij·Vj viii.更新 ℓi = ℓi^new, mi = mi^new d. 将 Oi 写回HBM 反向传播算法：需要重新计算前向过程中的中间值，因为它们没有被存储。\n3.2 FlashAttention v2 (2023)：工程优化与扩展性 3.2.1 设计目标 核心目标：在保持v1算法正确性的基础上，大幅提升实际性能\n优化方向：\n并行化优化：更好利用GPU的并行计算能力 内存访问优化：减少不必要的内存传输 支持更多场景：causal mask、不同head维度等 3.2.2 关键优化 1. 分块策略重新设计\nv1的问题：外层循环遍历Query块，内层循环遍历Key/Value块，导致：\nQuery块需要重复加载 并行度不够高 v2的改进：\n按Key/Value维度分块：外层循环遍历Key/Value，内层遍历Query 更好的并行性：不同的Query块可以并行处理 2. 工作分配优化\ngraph TD subgraph \u0026#34;FlashAttention v1\u0026#34; A1[\u0026#34;Thread Block 1\u0026lt;br/\u0026gt;处理Q块1\u0026#34;] --\u0026gt; B1[\u0026#34;串行处理所有K,V块\u0026#34;] A2[\u0026#34;Thread Block 2\u0026lt;br/\u0026gt;处理Q块2\u0026#34;] --\u0026gt; B2[\u0026#34;串行处理所有K,V块\u0026#34;] A3[\u0026#34;Thread Block 3\u0026lt;br/\u0026gt;处理Q块3\u0026#34;] --\u0026gt; B3[\u0026#34;串行处理所有K,V块\u0026#34;] end subgraph \u0026#34;FlashAttention v2\u0026#34; C1[\u0026#34;Thread Block 1\u0026lt;br/\u0026gt;处理K块1,V块1\u0026#34;] --\u0026gt; D1[\u0026#34;并行处理所有Q块\u0026#34;] C2[\u0026#34;Thread Block 2\u0026lt;br/\u0026gt;处理K块2,V块2\u0026#34;] --\u0026gt; D2[\u0026#34;并行处理所有Q块\u0026#34;] C3[\u0026#34;Thread Block 3\u0026lt;br/\u0026gt;处理K块3,V块3\u0026#34;] --\u0026gt; D3[\u0026#34;并行处理所有Q块\u0026#34;] end style A1 fill:#ff9999 style A2 fill:#ff9999 style A3 fill:#ff9999 style C1 fill:#99ff99 style C2 fill:#99ff99 style C3 fill:#99ff99 3. 内存访问模式优化\n减少冗余加载：Key和Value块在多个Query块间共享 更好的缓存利用：改进内存访问的空间局部性 向量化操作：更充分利用GPU的向量化指令 3.2.3 性能提升分析 理论分析：\n并行度提升：从 $O(\\lceil n/B_r \\rceil)$ 提升到 $O(\\lceil n/B_c \\rceil \\times \\lceil n/B_r \\rceil)$ 内存访问优化：减少约20-30%的冗余访问 计算效率：更好的指令级并行和向量化 实际性能：\n序列长度 Head维度 v1性能 v2性能 提升比例 2048 64 1.2x 1.8x +50% 4096 64 1.5x 2.3x +53% 8192 64 1.8x 3.1x +72% 3.3 FlashAttention v3 (2024)：硬件协同设计 3.3.1 设计目标 核心目标：充分利用新一代GPU硬件特性，支持更复杂的应用场景\n技术趋势：\n新硬件特性：H100的FP8支持、更大的共享内存 应用需求：更长序列、混合精度、稀疏attention 系统集成：更好的编译器支持、自动调优 3.3.2 核心创新 1. 异构精度计算\n支持FP8/FP16/FP32混合精度：\n输入：FP8存储，减少内存带宽 计算：FP16/FP32，保证数值精度 输出：根据需求选择精度 2. 自适应分块策略\ngraph TD A[输入分析] --\u0026gt; B{序列长度} B --\u0026gt;|短序列\u0026lt;1024| C[小块策略\u0026lt;br/\u0026gt;Br=64, Bc=64] B --\u0026gt;|中等序列1024-4096| D[中等块策略\u0026lt;br/\u0026gt;Br=128, Bc=128] B --\u0026gt;|长序列\u0026gt;4096| E[大块策略\u0026lt;br/\u0026gt;Br=256, Bc=256] F[硬件检测] --\u0026gt; G{GPU类型} G --\u0026gt;|A100| H[优化A100参数] G --\u0026gt;|H100| I[优化H100参数] G --\u0026gt;|其他| J[通用参数] C --\u0026gt; K[执行kernel] D --\u0026gt; K E --\u0026gt; K H --\u0026gt; K I --\u0026gt; K J --\u0026gt; K 3. 编译时优化\n模板特化：针对常见的head维度生成专门的kernel 循环展开：减少分支预测开销 指令调度：更好的指令级并行 3.3.3 应用场景扩展 1. 长上下文支持\n支持1M+token的超长序列 分层attention策略 渐进式精度降低 2. 稀疏attention模式\nBlock-sparse attention 滑动窗口attention 局部-全局混合attention 3. 多模态支持\n文本-图像联合attention 不同模态的attention权重 跨模态的梯度优化 4. FlashAttention算法流程深度解析 4.1 完整算法流程可视化 4.1.1 整体计算流程 flowchart TD A[\u0026#34;输入矩阵\u0026lt;br/\u0026gt;Q: nxd, K: nxd, V: nxd\u0026#34;] --\u0026gt; B[\u0026#34;矩阵分块\u0026#34;] B --\u0026gt; C[\u0026#34;Q分块: ⌈n/Br⌉个块\u0026lt;br/\u0026gt;每块大小BrXd\u0026#34;] B --\u0026gt; D[\u0026#34;K,V分块: ⌈n/Bc⌉个块\u0026lt;br/\u0026gt;每块大小BcXd\u0026#34;] C --\u0026gt; E[外层循环: 遍历Q块] D --\u0026gt; F[内层循环: 遍历K,V块] E --\u0026gt; G[加载Qi到SRAM] F --\u0026gt; H[加载Kj,Vj到SRAM] G --\u0026gt; I[\u0026#34;计算局部注意力得分\u0026lt;br/\u0026gt;Sij = Qi x KjT\u0026#34;] H --\u0026gt; I I --\u0026gt; J[\u0026#34;在线Softmax更新\u0026#34;] J --\u0026gt; K[\u0026#34;计算加权输出\u0026lt;br/\u0026gt;Oi += aij x Vj\u0026#34;] K --\u0026gt; L{所有K,V块\u0026lt;br/\u0026gt;处理完毕?} L --\u0026gt;|否| F L --\u0026gt;|是| M[写回Oi到HBM] M --\u0026gt; N{所有Q块\u0026lt;br/\u0026gt;处理完毕?} N --\u0026gt;|否| E N --\u0026gt;|是| O[输出完整结果O] style A fill:#e1f5fe style G fill:#c8e6c9 style H fill:#c8e6c9 style I fill:#fff3e0 style J fill:#fce4ec style K fill:#f3e5f5 style O fill:#e8f5e8 4.1.2 SRAM内存管理流程 sequenceDiagram participant HBM as HBM显存 participant SRAM as SRAM缓存 participant Compute as 计算单元 Note over HBM,Compute: 处理第i个Q块，第j个K,V块 HBM-\u0026gt;\u0026gt;SRAM: 1. 加载Qi (BrXd) HBM-\u0026gt;\u0026gt;SRAM: 2. 加载Kj (BcXd) HBM-\u0026gt;\u0026gt;SRAM: 3. 加载Vj (BcXd) SRAM-\u0026gt;\u0026gt;Compute: 4. 计算Sij = QiXKjT Compute-\u0026gt;\u0026gt;SRAM: 5. 存储局部得分Sij SRAM-\u0026gt;\u0026gt;Compute: 6. 在线Softmax更新 Note over Compute: 更新mi, li, Oi SRAM-\u0026gt;\u0026gt;Compute: 7. 计算PijXVj Compute-\u0026gt;\u0026gt;SRAM: 8. 累积到输出Oi Note over SRAM: 释放Kj, Vj空间 alt 所有K,V块处理完毕 SRAM-\u0026gt;\u0026gt;HBM: 9. 写回最终Oi Note over SRAM: 释放Qi空间 end 4.2 核心算法：在线Softmax更新详解 4.2.1 状态维护与更新 算法状态：对每个Query块 $Q_i$，维护三元组 $(m_i, \\ell_i, O_i)$：\n$$ \\begin{align} m_i \u0026\\in \\mathbb{R}^{B_r} \\quad \\text{(当前最大logit值)} \\\\ \\ell_i \u0026\\in \\mathbb{R}^{B_r} \\quad \\text{(当前归一化因子)} \\\\ O_i \u0026\\in \\mathbb{R}^{B_r \\times d} \\quad \\text{(当前输出累积)} \\end{align} $$4.2.2 逐步更新过程 graph TD subgraph \u0026#34;第j步更新前状态\u0026#34; A1[\u0026#34;mi⁽ʲ⁻¹⁾: 前j-1块的最大值\u0026#34;] A2[\u0026#34;ℓi⁽ʲ⁻¹⁾: 前j-1块的归一化因子\u0026#34;] A3[\u0026#34;Oi⁽ʲ⁻¹⁾: 前j-1块的累积输出\u0026#34;] end subgraph \u0026#34;当前块计算\u0026#34; B1[\u0026#34;计算Sij = QiXKjT\u0026#34;] B2[\u0026#34;计算局部最大值 rowmax(Sij)\u0026#34;] B3[\u0026#34;计算局部softmax权重\u0026#34;] B4[\u0026#34;计算局部行和 rowsum\u0026#34;] end subgraph \u0026#34;状态更新\u0026#34; C1[\u0026#34;更新全局最大值\u0026#34;] C2[\u0026#34;更新归一化因子\u0026#34;] C3[\u0026#34;更新累积输出\u0026#34;] end A1 --\u0026gt; C1 A2 --\u0026gt; C2 A3 --\u0026gt; C3 B1 --\u0026gt; B2 --\u0026gt; B3 --\u0026gt; B4 B2 --\u0026gt; C1 B3 --\u0026gt; C3 B4 --\u0026gt; C2 style B1 fill:#fff3e0 style C1 fill:#e8f5e8 style C2 fill:#e8f5e8 style C3 fill:#e8f5e8 更新公式详解：\n设当前处理第 $j$ 个Key/Value块，更新规则为：\n$$ \\begin{align} m_i^{(j)} \u0026= \\max(m_i^{(j-1)}, \\tilde{m}_{ij}) \\\\ \\alpha \u0026= \\exp(m_i^{(j-1)} - m_i^{(j)}) \\\\ \\beta \u0026= \\exp(\\tilde{m}_{ij} - m_i^{(j)}) \\\\ \\ell_i^{(j)} \u0026= \\ell_i^{(j-1)} \\cdot \\alpha + \\tilde{\\ell}_{ij} \\cdot \\beta \\\\ O_i^{(j)} \u0026= \\frac{\\ell_i^{(j-1)}}{\\ell_i^{(j)}} \\cdot \\alpha \\cdot O_i^{(j-1)} + \\frac{\\beta}{\\ell_i^{(j)}} \\cdot \\tilde{P}_{ij} V_j \\end{align} $$4.3 具体数值示例：逐步计算过程 4.3.1 示例设置 输入参数：\n序列长度：$n = 4$ 特征维度：$d = 2$ 分块大小：$B_r = B_c = 2$ 输入矩阵：\n$$ Q = \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\\\ 2 \u0026 1 \\\\ 1 \u0026 2 \\end{bmatrix}, \\quad K = \\begin{bmatrix} 1 \u0026 1 \\\\ 0 \u0026 2 \\\\ 1 \u0026 0 \\\\ 2 \u0026 1 \\end{bmatrix}, \\quad V = \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\\\ 2 \u0026 1 \\\\ 1 \u0026 2 \\end{bmatrix} $$4.3.2 分块结果 $$ Q_1 = \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\end{bmatrix}, \\quad Q_2 = \\begin{bmatrix} 2 \u0026 1 \\\\ 1 \u0026 2 \\end{bmatrix} $$$$ K_1 = \\begin{bmatrix} 1 \u0026 1 \\\\ 0 \u0026 2 \\end{bmatrix}, \\quad K_2 = \\begin{bmatrix} 1 \u0026 0 \\\\ 2 \u0026 1 \\end{bmatrix} $$$$ V_1 = \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\end{bmatrix}, \\quad V_2 = \\begin{bmatrix} 2 \u0026 1 \\\\ 1 \u0026 2 \\end{bmatrix} $$4.3.3 处理Q1块的详细步骤 步骤1：处理K1,V1块\n计算注意力得分：\n$$S_{11} = Q_1 K_1^T = \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} 1 \u0026 0 \\\\ 1 \u0026 2 \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 0 \\\\ 1 \u0026 2 \\end{bmatrix}$$ 计算行最大值：\n$$\\tilde{m}_{11} = \\begin{bmatrix} 1 \\\\ 2 \\end{bmatrix}$$ 计算softmax权重：\n$$\\tilde{P}_{11} = \\begin{bmatrix} e^{1-1} \u0026 e^{0-1} \\\\ e^{1-2} \u0026 e^{2-2} \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 e^{-1} \\\\ e^{-1} \u0026 1 \\end{bmatrix}$$ 计算行和：\n$$\\tilde{\\ell}_{11} = \\begin{bmatrix} 1 + e^{-1} \\\\ e^{-1} + 1 \\end{bmatrix} \\approx \\begin{bmatrix} 1.368 \\\\ 1.368 \\end{bmatrix}$$ 初始化状态：\n$$m_1^{(1)} = \\tilde{m}_{11} = \\begin{bmatrix} 1 \\\\ 2 \\end{bmatrix}, \\quad \\ell_1^{(1)} = \\tilde{\\ell}_{11} \\approx \\begin{bmatrix} 1.368 \\\\ 1.368 \\end{bmatrix}$$ 计算输出：\n$$O_1^{(1)} = \\frac{\\tilde{P}_{11}}{\\ell_1^{(1)}} V_1 \\approx \\begin{bmatrix} 0.731 \u0026 0.269 \\\\ 0.269 \u0026 0.731 \\end{bmatrix} \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\end{bmatrix} = \\begin{bmatrix} 0.731 \u0026 0.269 \\\\ 0.269 \u0026 0.731 \\end{bmatrix}$$ 步骤2：处理K2,V2块\n计算注意力得分：\n$$S_{12} = Q_1 K_2^T = \\begin{bmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\end{bmatrix} \\begin{bmatrix} 1 \u0026 2 \\\\ 0 \u0026 1 \\end{bmatrix} = \\begin{bmatrix} 1 \u0026 2 \\\\ 0 \u0026 1 \\end{bmatrix}$$ 计算行最大值：\n$$\\tilde{m}_{12} = \\begin{bmatrix} 2 \\\\ 1 \\end{bmatrix}$$ 更新全局最大值：\n$$m_1^{(2)} = \\max(m_1^{(1)}, \\tilde{m}_{12}) = \\max\\left(\\begin{bmatrix} 1 \\\\ 2 \\end{bmatrix}, \\begin{bmatrix} 2 \\\\ 1 \\end{bmatrix}\\right) = \\begin{bmatrix} 2 \\\\ 2 \\end{bmatrix}$$ 计算修正因子：\n$$\\alpha = \\exp(m_1^{(1)} - m_1^{(2)}) = \\exp\\left(\\begin{bmatrix} 1-2 \\\\ 2-2 \\end{bmatrix}\\right) = \\begin{bmatrix} e^{-1} \\\\ 1 \\end{bmatrix}$$ $$\\beta = \\exp(\\tilde{m}_{12} - m_1^{(2)}) = \\exp\\left(\\begin{bmatrix} 2-2 \\\\ 1-2 \\end{bmatrix}\\right) = \\begin{bmatrix} 1 \\\\ e^{-1} \\end{bmatrix}$$ 更新归一化因子和输出\u0026hellip; 4.4 算法复杂度分析 4.4.1 时间复杂度 graph LR subgraph \u0026#34;计算复杂度分析\u0026#34; A[\u0026#34;外层循环\u0026lt;br/\u0026gt;⌈n/Br⌉次\u0026#34;] --\u0026gt; B[\u0026#34;内层循环\u0026lt;br/\u0026gt;⌈n/Bc⌉次\u0026#34;] B --\u0026gt; C[\u0026#34;矩阵乘法\u0026lt;br/\u0026gt;O(BrXBcXd)\u0026#34;] C --\u0026gt; D[\u0026#34;Softmax计算\u0026lt;br/\u0026gt;O(BrXBc)\u0026#34;] D --\u0026gt; E[\u0026#34;输出更新\u0026lt;br/\u0026gt;O(BrXBcXd)\u0026#34;] end subgraph \u0026#34;总复杂度\u0026#34; F[\u0026#34;总时间复杂度\u0026lt;br/\u0026gt;O(n²d)\u0026#34;] G[\u0026#34;与传统方法相同\u0026lt;br/\u0026gt;但常数项更小\u0026#34;] end A --\u0026gt; F style F fill:#e8f5e8 style G fill:#fff3e0 详细分析：\n外层循环：$\\lceil n/B_r \\rceil$ 次 内层循环：$\\lceil n/B_c \\rceil$ 次 每次迭代：$O(B_r B_c d + B_r B_c + B_r B_c d) = O(B_r B_c d)$ 总时间复杂度：$O(\\frac{n}{B_r} \\cdot \\frac{n}{B_c} \\cdot B_r B_c d) = O(n^2 d)$ 4.4.2 空间复杂度 SRAM使用分析：\nQuery块：$B_r \\times d$ Key块：$B_c \\times d$ Value块：$B_c \\times d$ 中间结果：$B_r \\times B_c$（注意力得分） 状态向量：$B_r$（最大值）+ $B_r$（归一化因子） 输出块：$B_r \\times d$ 总SRAM需求：$O(B_r d + B_c d + B_r B_c) = O((B_r + B_c)d + B_r B_c)$\nHBM使用：仅需存储输入输出，$O(nd)$，相比传统方法的 $O(n^2 + nd)$ 大幅减少。\n5. 行业实践与生态发展 5.1 主流框架集成现状 5.1.1 深度学习框架支持 graph TB subgraph \u0026#34;主流框架集成\u0026#34; A[PyTorch] --\u0026gt; A1[torch.nn.functional.scaled_dot_product_attention\u0026lt;br/\u0026gt;原生支持FlashAttention v2] A --\u0026gt; A2[xFormers库\u0026lt;br/\u0026gt;完整FlashAttention实现] B[TensorFlow] --\u0026gt; B1[TensorFlow Addons\u0026lt;br/\u0026gt;社区FlashAttention实现] B --\u0026gt; B2[JAX/Flax\u0026lt;br/\u0026gt;研究社区广泛使用] C[HuggingFace] --\u0026gt; C1[Transformers库\u0026lt;br/\u0026gt;默认启用FlashAttention] C --\u0026gt; C2[Accelerate库\u0026lt;br/\u0026gt;自动优化选择] end subgraph \u0026#34;推理框架\u0026#34; D[vLLM] --\u0026gt; D1[生产级推理\u0026lt;br/\u0026gt;FlashAttention v2集成] E[TensorRT-LLM] --\u0026gt; E1[NVIDIA优化\u0026lt;br/\u0026gt;硬件特定优化] F[Text Generation Inference] --\u0026gt; F1[HuggingFace推理\u0026lt;br/\u0026gt;自动FlashAttention] end style A1 fill:#c8e6c9 style C1 fill:#c8e6c9 style D1 fill:#c8e6c9 5.1.2 性能基准测试 训练性能提升（相对于标准Attention）：\n模型规模 序列长度 传统Attention FlashAttention v2 内存节省 速度提升 125M 2048 基线 1.8x 60% 80% 1.3B 2048 基线 2.1x 65% 110% 6.7B 4096 OOM 可运行 70% N/A 13B 8192 OOM 可运行 75% N/A 推理性能提升：\ngraph LR subgraph \u0026#34;推理延迟对比 (ms)\u0026#34; A[序列长度: 1024\u0026lt;br/\u0026gt;标准: 45ms\u0026lt;br/\u0026gt;Flash: 28ms\u0026lt;br/\u0026gt;提升: 38%] B[序列长度: 2048\u0026lt;br/\u0026gt;标准: 180ms\u0026lt;br/\u0026gt;Flash: 89ms\u0026lt;br/\u0026gt;提升: 51%] C[序列长度: 4096\u0026lt;br/\u0026gt;标准: 720ms\u0026lt;br/\u0026gt;Flash: 298ms\u0026lt;br/\u0026gt;提升: 59%] D[序列长度: 8192\u0026lt;br/\u0026gt;标准: OOM\u0026lt;br/\u0026gt;Flash: 1100ms\u0026lt;br/\u0026gt;提升: 可运行] end style A fill:#e8f5e8 style B fill:#c8e6c9 style C fill:#a5d6a7 style D fill:#81c784 5.2 产业应用案例 5.2.1 大型语言模型训练 OpenAI GPT系列：\nGPT-3.5/4训练中广泛使用FlashAttention优化 支持更长的上下文窗口（32k tokens） 训练成本降低约30-40% Meta Llama系列：\nLlama 2/3全面采用FlashAttention v2 支持70B参数模型的高效训练 Code Llama支持16k代码上下文 Google PaLM/Gemini：\n内部优化版本的FlashAttention 针对TPU硬件的特定优化 支持多模态长序列处理 5.3 技术生态与工具链 5.3.1 开发工具支持 性能分析工具：\nNVIDIA Nsight：支持FlashAttention kernel分析 PyTorch Profiler：可视化FlashAttention性能特征 自动优化工具：\nDeepSpeed：自动选择最优的Attention实现 FairScale：大规模训练中的FlashAttention集成 Composer：训练配方中的自动FlashAttention启用 5.3.2 社区贡献与扩展 开源实现：\nflash-attn：官方Python包，支持多种GPU xFormers：Meta开源的高效Transformer组件 FlashInfer：专门用于推理的FlashAttention实现 研究扩展：\nFlashAttention-2：进一步的工程优化 PagedAttention：结合分页机制的变体 StreamingLLM：流式处理的FlashAttention变体 5.4 未来发展趋势 5.4.1 硬件协同演进 下一代GPU优化：\ntimeline title FlashAttention硬件协同发展路线图 2022 : FlashAttention v1 : A100/V100支持 : FP16/BF16精度 2023 : FlashAttention v2/v3 : H100优化 : FP8支持 : 更大shared memory 2024 : 硬件特定优化 : Grace Hopper架构 : 统一内存访问 : AI专用指令集 2025+ : 未来趋势 : 内存计算融合 核心要点：FlashAttention的成功不是偶然的，它是数学理论、工程实践和系统思维完美结合的结果。理解其本质，不仅能帮助我们更好地使用这一技术，更能启发我们在面对其他复杂系统问题时的思维方式。\n致谢：本文的完成得益于FlashAttention原作者Tri Dao等人的开创性工作，以及整个AI社区在理论研究和工程实践方面的持续贡献。\n如需获取更多技术细节、实现代码或性能基准测试数据，欢迎进一步交流讨论。\n","permalink":"https://pillumina.github.io/posts/aiinfra/11-flashattention/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文从数学原理出发，深入分析FlashAttention的核心思想、算法设计和各版本演进，通过详实的数学推导、直观的流程图表和具体的数值示例，帮助读者真正掌握这一革命性的Attention优化技术。\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-问题的本质传统attention的根本瓶颈\"\u003e1. 问题的本质：传统Attention的根本瓶颈\u003c/h2\u003e\n\u003ch3 id=\"11-传统attention机制的计算模式\"\u003e1.1 传统Attention机制的计算模式\u003c/h3\u003e\n\u003cp\u003e传统的Self-Attention机制遵循如下计算流程：\u003c/p\u003e\n$$\n\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V\n$$\u003cp\u003e让我们用具体数值来理解这个过程的复杂性：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e示例场景\u003c/strong\u003e：考虑一个典型的语言模型场景\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e序列长度：$n = 2048$（如GPT-2的上下文长度）\u003c/li\u003e\n\u003cli\u003e特征维度：$d_k = 64$（每个attention head的维度）\u003c/li\u003e\n\u003cli\u003e输入张量形状：$Q, K, V \\in \\mathbb{R}^{2048 \\times 64}$\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"第一步计算注意力得分矩阵\"\u003e第一步：计算注意力得分矩阵\u003c/h4\u003e\n$$S = \\frac{QK^T}{\\sqrt{d_k}} \\in \\mathbb{R}^{2048 \\times 2048}$$\u003cp\u003e这一步产生了一个 $2048 \\times 2048 = 4,194,304$ 个元素的矩阵，以FP16精度存储需要约8MB内存。\u003c/p\u003e\n\u003ch4 id=\"第二步softmax归一化\"\u003e第二步：Softmax归一化\u003c/h4\u003e\n$$P = \\text{softmax}(S) \\in \\mathbb{R}^{2048 \\times 2048}$$\u003cp\u003eSoftmax计算需要：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e计算每行的最大值：$m_i = \\max_j S_{i,j}$\u003c/li\u003e\n\u003cli\u003e计算指数和：$l_i = \\sum_j e^{S_{i,j} - m_i}$\u003c/li\u003e\n\u003cli\u003e归一化：$P_{i,j} = \\frac{e^{S_{i,j} - m_i}}{l_i}$\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这又需要存储另一个 $2048 \\times 2048$ 的矩阵。\u003c/p\u003e","title":"[AIInfra] FlashAttention 深度解析：从数学原理到工程实现"},{"content":"Verl DataProto 实现原理与数据流动分析 目录 1. 概述 2. DataProto 核心架构 3. HybridFlow 设计理念 4. 控制流与计算流分离 5. 数据流动机制 6. Dispatch 模式详解 7. 性能优化策略 8. 总结 1. 概述 Verl 是一个基于 HybridFlow 论文的开源强化学习训练框架，专门为大语言模型的后训练优化而设计。其核心创新在于将控制流和计算流分离，通过 DataProto 协议实现高效的数据交换。\n2. DataProto 核心架构 2.1 数据结构设计 DataProto 是 verl 框架中用于数据交换的核心协议，所有在 Worker 之间流转的数据，都被统一封装在一个名为 DataProto 的数据结构中。它不仅仅是一个字典，更承载着 RLHF 流程中所有的信息演变, 基于 PyTorch 的 TensorDict 构建：\n1 2 3 4 5 @dataclass class DataProto: batch: TensorDict = None # 张量数据容器 non_tensor_batch: dict = field(default_factory=dict) # 非张量数据 meta_info: dict = field(default_factory=dict) # 元信息 核心特性：\n统一接口: 提供标准化的数据容器，支持张量和非张量数据 设备管理: 自动处理 GPU/CPU 设备间的数据移动 内存优化: 支持分块处理和内存复用 序列化: 支持高效的序列化和反序列化 2.2 数据一致性检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 def check_consistency(self): \u0026#34;\u0026#34;\u0026#34;检查 DataProto 的一致性\u0026#34;\u0026#34;\u0026#34; if self.batch is not None: assert len(self.batch.batch_size) == 1, \u0026#34;只支持 num_batch_dims=1\u0026#34; if self.non_tensor_batch is not None: for key, val in self.non_tensor_batch.items(): assert isinstance(val, np.ndarray) # 检查批次大小一致性 if self.batch is not None and self.non_tensor_batch is not None: batch_size = self.batch.batch_size[0] for key, val in self.non_tensor_batch.items(): assert val.shape[0] == batch_size 3. HybridFlow 设计理念 3.1 设计动机 传统 RL 系统面临的问题：\n耦合度高: 控制逻辑与计算实现紧密耦合 扩展性差: 难以支持不同的计算后端 复用困难: 算法逻辑难以在不同框架间复用 3.2 解决方案 HybridFlow 采用分离式设计：\ngraph TB\rsubgraph \u0026#34;控制流 (Control Flow)\u0026#34;\rA[RL算法逻辑] --\u0026gt; B[训练循环控制]\rB --\u0026gt; C[数据调度]\rC --\u0026gt; D[结果收集]\rend\rsubgraph \u0026#34;计算流 (Computation Flow)\u0026#34;\rE[模型初始化] --\u0026gt; F[前向传播]\rF --\u0026gt; G[反向传播]\rG --\u0026gt; H[参数更新]\rend\rD -.-\u0026gt;|DataProto| E\rH -.-\u0026gt;|DataProto| A\rstyle A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle E fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff\rstyle D fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff\rstyle H fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,color:#fff 4. 控制流与计算流分离 4.1 控制流 (Control Flow) 控制流负责 RL 算法的核心逻辑，运行在单进程中：\n主要职责：\n训练循环管理 数据批次调度 算法参数控制 结果聚合分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class RayPPOTrainer: def fit(self): # 控制流：训练循环 for epoch in range(self.config.trainer.total_epochs): # 1. 数据准备 batch = self._get_training_batch() # 2. 分发到计算流 rollout_data = self.actor_rollout_wg.generate_sequences(batch) # 3. 收集结果 advantages = self._compute_advantages(rollout_data) # 4. 策略更新 self._update_policy(advantages) 4.2 计算流 (Computation Flow) 计算流负责神经网络计算，运行在多进程中：\n主要职责：\n模型前向/反向传播 梯度计算和参数更新 分布式同步 内存管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO) def generate_sequences(self, data: DataProto) -\u0026gt; DataProto: # 计算流：序列生成 with torch.no_grad(): # 1. 模型推理 outputs = self.model.generate( input_ids=data.batch[\u0026#34;input_ids\u0026#34;], attention_mask=data.batch[\u0026#34;attention_mask\u0026#34;] ) # 2. 返回结果 return DataProto( batch=TensorDict({ \u0026#34;generated_ids\u0026#34;: outputs.sequences, \u0026#34;log_probs\u0026#34;: outputs.log_probs }, batch_size=data.batch.batch_size), meta_info=data.meta_info ) 4.3 分离的优势 graph LR\rsubgraph \u0026#34;优势分析\u0026#34;\rA[软件复用性] --\u0026gt; A1[控制流可复用]\rA --\u0026gt; A2[计算流可复用]\rB[开发效率] --\u0026gt; B1[单进程调试]\rB --\u0026gt; B2[模块化开发]\rC[性能优化] --\u0026gt; C1[独立优化]\rC --\u0026gt; C2[灵活调度]\rend\rstyle A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle B fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff\rstyle C fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff 5. 数据流动机制 在我们追踪 DataProto 在各个 Worker 之间的旅程之前，我们有必要先回答一个更根本的问题：第一个 DataProto 对象是如何诞生的？\n在 veRL 中，DataProto 的声明周期是：\nParquet 文件 -\u0026gt; RLHFDataset -\u0026gt; DataLoader -\u0026gt; batch_dict -\u0026gt; DataProto\nParquet 是 veRL 推荐的数据格式，通常包含 prompt 文本和相关元信息。RLHFDataset 负责读取本地或远程 Parquet 文件，按 max_prompt_length 过滤样本，并应用聊天模板格式化对话。随后，执行分词、填充、截断，将样本转为固定长度的张量。\nDataLoader 在 RayPPOTrainer 中创建，将处理后的样本组织成 batch，产出 batch_dict。\n此时，DataProto 登场。通过 DataProto.from_single_dict(batch_dict)，普通字典被封装为 veRL 内部统一的数据协议，正式进入 RLHF 流程。\n5.1 完整数据流动图 graph TD\rA[训练数据] --\u0026gt; B[DataProto创建]\rB --\u0026gt; C[RayPPOTrainer控制流]\rC --\u0026gt; D[数据分发阶段]\rD --\u0026gt; E[WorkerGroup.generate_sequences]\rE --\u0026gt; F{Dispatch模式选择}\rF --\u0026gt;|DP_COMPUTE_PROTO| G[数据并行分割]\rF --\u0026gt;|ONE_TO_ALL| H[广播分发]\rF --\u0026gt;|ALL_TO_ALL| I[全对全通信]\rG --\u0026gt; J[分发到计算Worker]\rH --\u0026gt; J\rI --\u0026gt; J\rJ --\u0026gt; K[ActorRolloutWorker]\rJ --\u0026gt; L[CriticWorker] J --\u0026gt; M[ReferenceWorker]\rK --\u0026gt; N[序列生成计算]\rL --\u0026gt; O[价值函数计算]\rM --\u0026gt; P[参考策略计算]\rN --\u0026gt; Q[DataProto结果收集]\rO --\u0026gt; Q\rP --\u0026gt; Q\rQ --\u0026gt; R[优势函数计算]\rR --\u0026gt; S[策略梯度更新]\rS --\u0026gt; T[模型参数同步]\rT --\u0026gt; U[下一轮训练]\rU --\u0026gt; D\rstyle A fill:#E3F2FD,stroke:#1976D2,stroke-width:2px\rstyle B fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px\rstyle C fill:#E8F5E8,stroke:#388E3C,stroke-width:2px\rstyle Q fill:#FFF3E0,stroke:#F57C00,stroke-width:2px\rstyle T fill:#FCE4EC,stroke:#C2185B,stroke-width:2px\rstyle F fill:#E0F2F1,stroke:#00695C,stroke-width:2px 5.2 数据流动时间线 gantt\rtitle DataProto在分布式训练中的时间线\rdateFormat X\raxisFormat %s\rsection 数据准备阶段\r数据加载与预处理 :0, 3\rDataProto对象创建 :3, 4\rsection 分发阶段\r数据分割与调度 :4, 5\r网络传输与分发 :5, 7\rsection 计算阶段\rActor模型推理 :7, 12\rCritic价值计算 :7, 10\rReference策略计算 :7, 11\rsection 收集阶段\r结果收集与合并 :12, 14\r数据格式转换 :14, 15\rsection 更新阶段\r优势函数计算 :15, 16\r策略梯度更新 :16, 18\r模型参数同步 :18, 20 5.3 完整PPO训练角度追踪数据流动 下面我们将以一次 PPO 迭代为例，用一张 端到端的数据流图 来追踪 DataProto 的演变过程。\nRayPPOTrainer 的 fit() 循环从 train_dataloader 中取出一个 batch_dict。这个字典通常只包含 input_ids、attention_mask 等表示 prompt 的基本信息。这些信息被封装成第一个版本的 DataProto 对象。\n接下来进入到 PPO 的 生成 (Generation) 和 准备 (Preparation) 阶段。这个初生的 DataProto 开始了它的旅程，它被 RayPPOTrainer 依次（或并行地）发送给各个 Worker，每经过一个，就会被赋予新的信息。\n流经 RolloutWorker：DataProto v1 被发送去执行 generate_sequences。RolloutWorker 返回生成的 responses，并将其合并，形成 DataProto v2。\nDataProto v2 被同时发送给 RewardModelWorker, ActorWorker (执行 compute_log_prob), 和 CriticWorker (执行 compute_values)。\n这三个 Worker 并行地完成计算，各自返回自己的结果。RayPPOTrainer 将这些结果全部合并，形成了 DataProto v3。\n此时的 DataProto v3 已经包含了计算优势函数所需的所有输入。这个计算通常是轻量级的，因此 RayPPOTrainer 会在 Driver 进程本地 直接调用 compute_advantage 函数。\ncompute_advantage 函数会计算出 advantages 和 returns，并将其加入到数据中，至此，DataProto v4 已经准备好了。\n接下来 DataProto v4 被最后一次分发，分别发送给 ActorWorker 和 CriticWorker 的 update 方法。\nActorWorker 从中取出 advantages, old_log_probs 等信息，计算 PPO 损失并更新自己的权重。CriticWorker 从中取出 returns 作为学习目标，计算 MSE 损失并更新自己的权重。它们返回训练过程中的 metrics，标志着 DataProto 在本次迭代中的使命正式完成。\n6. Dispatch 模式详解 6.1 核心Dispatch模式 1 2 3 4 5 6 7 class Dispatch(DynamicEnum): RANK_ZERO = \u0026#34;RANK_ZERO\u0026#34; # 只在rank 0执行 ONE_TO_ALL = \u0026#34;ONE_TO_ALL\u0026#34; # 一对多广播 ALL_TO_ALL = \u0026#34;ALL_TO_ALL\u0026#34; # 全对全通信 DP_COMPUTE = \u0026#34;DP_COMPUTE\u0026#34; # 数据并行计算 DP_COMPUTE_PROTO = \u0026#34;DP_COMPUTE_PROTO\u0026#34; # DataProto数据并行 DP_COMPUTE_PROTO_WITH_FUNC = \u0026#34;DP_COMPUTE_PROTO_WITH_FUNC\u0026#34; # 带函数的DataProto并行 6.2 DP_COMPUTE_PROTO 实现 1 2 3 4 5 6 7 8 9 10 11 12 def dispatch_dp_compute_data_proto(worker_group, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34;DataProto数据并行分发\u0026#34;\u0026#34;\u0026#34; # 自动分割DataProto到worker数量 splitted_args, splitted_kwargs = _split_args_kwargs_data_proto_with_auto_padding( worker_group.world_size, *args, **kwargs ) return splitted_args, splitted_kwargs def collect_dp_compute_data_proto(worker_group, output): \u0026#34;\u0026#34;\u0026#34;DataProto数据并行收集\u0026#34;\u0026#34;\u0026#34; output = collect_dp_compute(worker_group, output) return _concat_data_proto_or_future(output) 6.3 自动填充机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def _split_args_kwargs_data_proto_with_auto_padding(chunks, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34;支持自动填充的数据分割\u0026#34;\u0026#34;\u0026#34; data_proto_len = None padding_size = None def _padding_and_split_data(obj, chunks): nonlocal data_proto_len, padding_size if isinstance(obj, DataProto) and obj.is_padding_enabled(): if data_proto_len is None: data_proto_len = len(obj) padding_size = (chunks - (data_proto_len % chunks)) if (data_proto_len % chunks \u0026gt; 0) else 0 obj.padding(padding_size=padding_size) return obj.chunk(chunks=chunks) # 处理所有参数 splitted_args = [_padding_and_split_data(arg, chunks) for arg in args] splitted_kwargs = {key: _padding_and_split_data(val, chunks) for key, val in kwargs.items()} return splitted_args, splitted_kwargs 6.4 Dispatch模式选择策略 graph TD\rA[方法调用] --\u0026gt; B{检查register装饰器}\rB --\u0026gt;|有装饰器| C[获取dispatch_mode]\rB --\u0026gt;|无装饰器| D[使用默认模式]\rC --\u0026gt; E{模式类型}\rE --\u0026gt;|DP_COMPUTE_PROTO| F[数据并行处理]\rE --\u0026gt;|ONE_TO_ALL| G[广播处理]\rE --\u0026gt;|ALL_TO_ALL| H[全对全处理]\rF --\u0026gt; I[分割DataProto]\rG --\u0026gt; J[复制到所有Worker]\rH --\u0026gt; K[直接分发]\rI --\u0026gt; L[并行计算]\rJ --\u0026gt; L\rK --\u0026gt; L\rL --\u0026gt; M[收集结果]\rM --\u0026gt; N[合并DataProto]\rstyle A fill:#E3F2FD,stroke:#1976D2,stroke-width:2px\rstyle F fill:#E8F5E8,stroke:#388E3C,stroke-width:2px\rstyle G fill:#FFF3E0,stroke:#F57C00,stroke-width:2px\rstyle H fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px\rstyle N fill:#FCE4EC,stroke:#C2185B,stroke-width:2px 7. 性能优化策略 7.1 内存优化 分块处理:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def chunk(self, chunks: int) -\u0026gt; list[\u0026#34;DataProto\u0026#34;]: \u0026#34;\u0026#34;\u0026#34;将DataProto分割成多个块\u0026#34;\u0026#34;\u0026#34; if self.batch is not None: batch_lst = self.batch.chunk(chunks=chunks, dim=0) else: batch_lst = [None for _ in range(chunks)] # 处理非张量数据 non_tensor_batch_lst = [{} for _ in range(chunks)] for key, val in self.non_tensor_batch.items(): non_tensor_lst = np.array_split(val, chunks) for i in range(chunks): non_tensor_batch_lst[i][key] = non_tensor_lst[i] return [type(self)(batch=batch_lst[i], non_tensor_batch=non_tensor_batch_lst[i], meta_info=self.meta_info) for i in range(chunks)] 内存复用:\n1 2 3 4 5 def to(self, device) -\u0026gt; \u0026#34;DataProto\u0026#34;: \u0026#34;\u0026#34;\u0026#34;设备间数据移动\u0026#34;\u0026#34;\u0026#34; if self.batch is not None: self.batch = self.batch.to(device) return self 7.2 异步执行 DataProtoFuture:\n1 2 3 4 5 6 7 8 9 10 11 12 13 @dataclass class DataProtoFuture: \u0026#34;\u0026#34;\u0026#34;异步DataProto，避免阻塞控制流\u0026#34;\u0026#34;\u0026#34; collect_fn: Callable futures: list[ray.ObjectRef] dispatch_fn: Callable = None def get(self): output = ray.get(self.futures) output = self.collect_fn(output) if self.dispatch_fn is not None: output = self.dispatch_fn(output) return output 7.3 流水线优化 graph LR\rsubgraph \u0026#34;传统同步模式\u0026#34;\rA1[生成] --\u0026gt; B1[训练]\rB1 --\u0026gt; C1[等待]\rC1 --\u0026gt; A1\rend\rsubgraph \u0026#34;verl异步模式\u0026#34;\rA2[生成] --\u0026gt; B2[训练]\rB2 --\u0026gt; A2\rA2 -.-\u0026gt;|异步| C2[并行执行]\rend\rstyle A1 fill:#FFCDD2,stroke:#D32F2F,stroke-width:2px\rstyle B1 fill:#FFCDD2,stroke:#D32F2F,stroke-width:2px\rstyle C1 fill:#FFCDD2,stroke:#D32F2F,stroke-width:2px\rstyle A2 fill:#C8E6C9,stroke:#388E3C,stroke-width:2px\rstyle B2 fill:#C8E6C9,stroke:#388E3C,stroke-width:2px\rstyle C2 fill:#C8E6C9,stroke:#388E3C,stroke-width:2px 7.4 网络优化 压缩传输:\n使用高效的序列化格式 支持数据压缩 批量传输减少网络开销 负载均衡:\n动态调整数据分发策略 监控网络延迟和带宽 自适应调整批次大小 8. 具体RL训练示例 8.1 PPO训练中的DataProto实例 让我们通过一个具体的PPO训练过程来展示DataProto的实际使用：\n8.1.1 训练数据准备 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 # 原始训练数据 raw_data = { \u0026#34;prompts\u0026#34;: [ \u0026#34;请计算 15 + 27 = ?\u0026#34;, \u0026#34;求解方程 2x + 5 = 13\u0026#34;, \u0026#34;一个圆的半径是3，求面积\u0026#34; ], \u0026#34;responses\u0026#34;: [ \u0026#34;15 + 27 = 42\u0026#34;, \u0026#34;2x + 5 = 13\\nx = 4\u0026#34;, \u0026#34;面积 = πr² = 9π\u0026#34; ], \u0026#34;rewards\u0026#34;: [0.8, 0.9, 0.7] } # 转换为DataProto def create_training_dataproto(raw_data): # 1. 文本编码 tokenizer = AutoTokenizer.from_pretrained(\u0026#34;Qwen/Qwen2.5-7B-Instruct\u0026#34;) prompt_ids = [] response_ids = [] attention_masks = [] for prompt, response in zip(raw_data[\u0026#34;prompts\u0026#34;], raw_data[\u0026#34;responses\u0026#34;]): # 编码prompt prompt_tokens = tokenizer.encode(prompt, add_special_tokens=True) prompt_ids.append(prompt_tokens) # 编码response response_tokens = tokenizer.encode(response, add_special_tokens=False) response_ids.append(response_tokens) # 创建attention mask total_length = len(prompt_tokens) + len(response_tokens) attention_masks.append([1] * total_length) # 2. 填充到相同长度 max_length = max(len(p) + len(r) for p, r in zip(prompt_ids, response_ids)) padded_prompt_ids = [] padded_response_ids = [] padded_attention_masks = [] for p_ids, r_ids, mask in zip(prompt_ids, response_ids, attention_masks): # 填充prompt padded_p = p_ids + [tokenizer.pad_token_id] * (max_length - len(p_ids) - len(r_ids)) padded_prompt_ids.append(padded_p) # 填充response padded_r = r_ids + [tokenizer.pad_token_id] * (max_length - len(p_ids) - len(r_ids)) padded_response_ids.append(padded_r) # 更新attention mask padded_mask = mask + [0] * (max_length - len(mask)) padded_attention_masks.append(padded_mask) # 3. 创建DataProto training_dataproto = DataProto.from_dict( tensors={ \u0026#34;prompt_ids\u0026#34;: torch.tensor(padded_prompt_ids, dtype=torch.long), \u0026#34;response_ids\u0026#34;: torch.tensor(padded_response_ids, dtype=torch.long), \u0026#34;attention_mask\u0026#34;: torch.tensor(padded_attention_masks, dtype=torch.long), }, non_tensors={ \u0026#34;raw_prompts\u0026#34;: np.array(raw_data[\u0026#34;prompts\u0026#34;], dtype=object), \u0026#34;raw_responses\u0026#34;: np.array(raw_data[\u0026#34;responses\u0026#34;], dtype=object), \u0026#34;rewards\u0026#34;: np.array(raw_data[\u0026#34;rewards\u0026#34;], dtype=np.float32), }, meta_info={ \u0026#34;dataset_name\u0026#34;: \u0026#34;math_training\u0026#34;, \u0026#34;batch_size\u0026#34;: len(raw_data[\u0026#34;prompts\u0026#34;]), \u0026#34;max_length\u0026#34;: max_length, \u0026#34;tokenizer_name\u0026#34;: \u0026#34;Qwen/Qwen2.5-7B-Instruct\u0026#34; } ) return training_dataproto # 创建训练数据 training_data = create_training_dataproto(raw_data) print(\u0026#34;DataProto结构:\u0026#34;) print(training_data.get_data_info()) 输出示例：\nDataProto结构:\rbatch\rprompt_ids: (3, 512) (torch.int64) cuda:0\rresponse_ids: (3, 512) (torch.int64) cuda:0\rattention_mask: (3, 512) (torch.int64) cuda:0\rnon_tensor_batch\rraw_prompts: ndarray(3,) (object)\rraw_responses: ndarray(3,) (object)\rrewards: ndarray(3,) (float32)\rmeta_info\rdataset_name: str\rbatch_size: int\rmax_length: int\rtokenizer_name: str 8.1.2 数据分发过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 假设有4个GPU worker world_size = 4 batch_size = 3 # 原始DataProto original_dataproto = training_data # batch_size=3 # 自动填充到能被4整除的大小 padded_dataproto, pad_size = pad_dataproto_to_divisor(original_dataproto, world_size) # 现在padded_dataproto的batch_size=4 (填充了1个样本) # 分割成4个chunk chunks = padded_dataproto.chunk(chunks=world_size) print(f\u0026#34;原始batch_size: {len(original_dataproto)}\u0026#34;) print(f\u0026#34;填充后batch_size: {len(padded_dataproto)}\u0026#34;) print(f\u0026#34;分割后chunk数量: {len(chunks)}\u0026#34;) print(f\u0026#34;每个chunk的batch_size: {len(chunks[0])}\u0026#34;) # 分发到各个worker for i, chunk in enumerate(chunks): print(f\u0026#34;Worker {i} 接收数据:\u0026#34;) print(f\u0026#34; - prompt_ids shape: {chunk.batch[\u0026#39;prompt_ids\u0026#39;].shape}\u0026#34;) print(f\u0026#34; - 包含样本数: {len(chunk)}\u0026#34;) 8.2 控制流与计算流交互示例 8.2.1 控制流：PPO训练循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 class RayPPOTrainer: def __init__(self, config): self.config = config self.actor_rollout_wg = None # Actor和Rollout的WorkerGroup self.critic_wg = None # Critic的WorkerGroup self.ref_policy_wg = None # Reference Policy的WorkerGroup def fit(self): \u0026#34;\u0026#34;\u0026#34;PPO训练的主控制循环\u0026#34;\u0026#34;\u0026#34; for epoch in range(self.config.trainer.total_epochs): print(f\u0026#34;开始第 {epoch} 轮训练\u0026#34;) # 1. 获取训练批次 batch_dataproto = self._get_training_batch() print(f\u0026#34;获取训练批次，batch_size: {len(batch_dataproto)}\u0026#34;) # 2. 分发到Actor进行序列生成 print(\u0026#34;开始序列生成...\u0026#34;) rollout_dataproto = self.actor_rollout_wg.generate_sequences(batch_dataproto) print(f\u0026#34;序列生成完成，生成 {len(rollout_dataproto)} 个序列\u0026#34;) # 3. 分发到Critic计算价值 print(\u0026#34;开始价值计算...\u0026#34;) value_dataproto = self.critic_wg.compute_values(rollout_dataproto) print(f\u0026#34;价值计算完成\u0026#34;) # 4. 分发到Reference Policy计算log概率 print(\u0026#34;开始参考策略计算...\u0026#34;) ref_log_prob_dataproto = self.ref_policy_wg.compute_log_probs(rollout_dataproto) print(f\u0026#34;参考策略计算完成\u0026#34;) # 5. 在控制流中计算优势函数 print(\u0026#34;计算优势函数...\u0026#34;) advantages = self._compute_advantages( rollout_dataproto, value_dataproto, ref_log_prob_dataproto ) # 6. 更新Actor策略 print(\u0026#34;更新Actor策略...\u0026#34;) self.actor_rollout_wg.update_actor(advantages) # 7. 更新Critic价值网络 print(\u0026#34;更新Critic价值网络...\u0026#34;) self.critic_wg.update_critic(advantages) print(f\u0026#34;第 {epoch} 轮训练完成\\n\u0026#34;) def _get_training_batch(self): \u0026#34;\u0026#34;\u0026#34;获取训练批次\u0026#34;\u0026#34;\u0026#34; # 从数据集中采样 batch_data = { \u0026#34;prompt_ids\u0026#34;: torch.randint(0, 1000, (self.config.data.train_batch_size, 512)), \u0026#34;attention_mask\u0026#34;: torch.ones(self.config.data.train_batch_size, 512), } return DataProto.from_dict( tensors=batch_data, meta_info={\u0026#34;epoch\u0026#34;: self.current_epoch} ) def _compute_advantages(self, rollout_data, value_data, ref_log_prob_data): \u0026#34;\u0026#34;\u0026#34;计算优势函数 - 在控制流中执行\u0026#34;\u0026#34;\u0026#34; # 从各个DataProto中提取数据 rewards = rollout_data.batch[\u0026#34;rewards\u0026#34;] # [batch_size, seq_len] values = value_data.batch[\u0026#34;values\u0026#34;] # [batch_size, seq_len] log_probs = rollout_data.batch[\u0026#34;log_probs\u0026#34;] # [batch_size, seq_len] ref_log_probs = ref_log_prob_data.batch[\u0026#34;log_probs\u0026#34;] # [batch_size, seq_len] # 计算优势函数 advantages = rewards - values advantages = advantages * rollout_data.batch[\u0026#34;attention_mask\u0026#34;] # 计算策略比率 log_ratio = log_probs - ref_log_probs ratio = torch.exp(log_ratio) # 创建优势DataProto advantages_dataproto = DataProto.from_dict( tensors={ \u0026#34;advantages\u0026#34;: advantages, \u0026#34;ratio\u0026#34;: ratio, \u0026#34;rewards\u0026#34;: rewards, \u0026#34;values\u0026#34;: values }, meta_info=rollout_data.meta_info ) return advantages_dataproto 8.2.2 计算流：Worker实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 @ray.remote class ActorRolloutWorker: def __init__(self, model_config): self.model = None self.config = model_config @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO) def generate_sequences(self, data: DataProto) -\u0026gt; DataProto: \u0026#34;\u0026#34;\u0026#34;生成序列 - 在计算流中执行\u0026#34;\u0026#34;\u0026#34; print(f\u0026#34;Worker {self.rank} 开始生成序列，batch_size: {len(data)}\u0026#34;) # 1. 模型推理 with torch.no_grad(): input_ids = data.batch[\u0026#34;prompt_ids\u0026#34;] attention_mask = data.batch[\u0026#34;attention_mask\u0026#34;] # 生成序列 outputs = self.model.generate( input_ids=input_ids, attention_mask=attention_mask, max_new_tokens=256, do_sample=True, temperature=0.7, return_dict_in_generate=True, output_scores=True ) # 提取生成的token ids generated_ids = outputs.sequences log_probs = torch.stack(outputs.scores, dim=1).log_softmax(dim=-1) # 计算每个token的log概率 token_log_probs = [] for i, seq in enumerate(generated_ids): seq_log_probs = [] for j, token_id in enumerate(seq): if j \u0026lt; len(log_probs[i]): seq_log_probs.append(log_probs[i][j][token_id].item()) token_log_probs.append(seq_log_probs) # 计算奖励（这里使用简单的长度奖励作为示例） rewards = torch.tensor([[len(seq) * 0.1] * len(seq) for seq in generated_ids]) # 2. 创建结果DataProto result_dataproto = DataProto.from_dict( tensors={ \u0026#34;generated_ids\u0026#34;: generated_ids, \u0026#34;log_probs\u0026#34;: torch.tensor(token_log_probs), \u0026#34;rewards\u0026#34;: rewards, \u0026#34;attention_mask\u0026#34;: torch.ones_like(generated_ids) }, non_tensors={ \u0026#34;raw_generated_texts\u0026#34;: np.array([ self.tokenizer.decode(seq, skip_special_tokens=True) for seq in generated_ids ], dtype=object) }, meta_info=data.meta_info ) print(f\u0026#34;Worker {self.rank} 序列生成完成\u0026#34;) return result_dataproto @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO) def update_actor(self, advantages: DataProto) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34;更新Actor策略 - 在计算流中执行\u0026#34;\u0026#34;\u0026#34; print(f\u0026#34;Worker {self.rank} 开始更新Actor策略\u0026#34;) # 1. 提取数据 advantages_tensor = advantages.batch[\u0026#34;advantages\u0026#34;] ratio = advantages.batch[\u0026#34;ratio\u0026#34;] rewards = advantages.batch[\u0026#34;rewards\u0026#34;] # 2. 计算PPO损失 clip_ratio = 0.2 policy_loss_1 = -advantages_tensor * ratio policy_loss_2 = -advantages_tensor * torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio) policy_loss = torch.maximum(policy_loss_1, policy_loss_2).mean() # 3. 反向传播 self.optimizer.zero_grad() policy_loss.backward() self.optimizer.step() print(f\u0026#34;Worker {self.rank} Actor策略更新完成，损失: {policy_loss.item():.4f}\u0026#34;) @ray.remote class CriticWorker: def __init__(self, model_config): self.model = None self.config = model_config @register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO) def compute_values(self, data: DataProto) -\u0026gt; DataProto: \u0026#34;\u0026#34;\u0026#34;计算价值函数 - 在计算流中执行\u0026#34;\u0026#34;\u0026#34; print(f\u0026#34;Worker {self.rank} 开始计算价值函数\u0026#34;) with torch.no_grad(): input_ids = data.batch[\u0026#34;generated_ids\u0026#34;] attention_mask = data.batch[\u0026#34;attention_mask\u0026#34;] # 前向传播计算价值 outputs = self.model(input_ids=input_ids, attention_mask=attention_mask) values = outputs.value # [batch_size, seq_len] # 创建价值DataProto value_dataproto = DataProto.from_dict( tensors={\u0026#34;values\u0026#34;: values}, meta_info=data.meta_info ) print(f\u0026#34;Worker {self.rank} 价值函数计算完成\u0026#34;) return value_dataproto 8.3 数据流动可视化 8.3.1 单轮训练的数据流动 sequenceDiagram\rparticipant CF as 控制流\rparticipant AW as ActorWorker\rparticipant CW as CriticWorker\rparticipant RW as RefWorker\rCF-\u0026gt;\u0026gt;CF: 创建训练DataProto\rNote over CF: batch_size=256, 包含prompt_ids等\rCF-\u0026gt;\u0026gt;AW: generate_sequences(training_data)\rNote over AW: 自动分割为64个样本/worker\rAW-\u0026gt;\u0026gt;AW: 模型推理生成序列\rAW-\u0026gt;\u0026gt;CF: 返回rollout_data\rNote over CF: 包含generated_ids, log_probs, rewards\rCF-\u0026gt;\u0026gt;CW: compute_values(rollout_data)\rCW-\u0026gt;\u0026gt;CW: 计算价值函数\rCW-\u0026gt;\u0026gt;CF: 返回value_data\rCF-\u0026gt;\u0026gt;RW: compute_log_probs(rollout_data)\rRW-\u0026gt;\u0026gt;RW: 计算参考策略log概率\rRW-\u0026gt;\u0026gt;CF: 返回ref_log_prob_data\rCF-\u0026gt;\u0026gt;CF: 计算优势函数\rNote over CF: advantages = rewards - values\rCF-\u0026gt;\u0026gt;AW: update_actor(advantages)\rCF-\u0026gt;\u0026gt;CW: update_critic(advantages)\rNote over CF: 完成一轮训练 8.3.2 DataProto在训练过程中的形态变化 graph LR\rsubgraph \u0026#34;训练开始\u0026#34;\rA1[原始训练数据\u0026lt;br/\u0026gt;prompt_ids, attention_mask\u0026lt;br/\u0026gt;batch_size=256]\rend\rsubgraph \u0026#34;序列生成后\u0026#34;\rA2[rollout_data\u0026lt;br/\u0026gt;generated_ids, log_probs, rewards\u0026lt;br/\u0026gt;batch_size=256]\rend\rsubgraph \u0026#34;价值计算后\u0026#34;\rA3[value_data\u0026lt;br/\u0026gt;values\u0026lt;br/\u0026gt;batch_size=256]\rend\rsubgraph \u0026#34;优势计算后\u0026#34;\rA4[advantages_data\u0026lt;br/\u0026gt;advantages, ratio\u0026lt;br/\u0026gt;batch_size=256]\rend\rA1 --\u0026gt; A2\rA2 --\u0026gt; A3\rA3 --\u0026gt; A4\rstyle A1 fill:#E3F2FD,stroke:#1976D2,stroke-width:2px\rstyle A2 fill:#E8F5E8,stroke:#388E3C,stroke-width:2px\rstyle A3 fill:#FFF3E0,stroke:#F57C00,stroke-width:2px\rstyle A4 fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px 8.4 关键特性展示 8.4.1 自动填充机制 1 2 3 4 5 6 7 8 9 10 11 12 13 # 示例：batch_size=250, world_size=4 original_batch_size = 250 world_size = 4 # 计算需要填充的数量 padding_needed = (world_size - (original_batch_size % world_size)) % world_size # padding_needed = 2 # 填充后的batch_size = 252，可以被4整除 final_batch_size = original_batch_size + padding_needed # 252 # 每个worker获得 252 // 4 = 63 个样本 samples_per_worker = final_batch_size // world_size # 63 8.4.2 异步执行示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 控制流中的异步调用 def async_training_step(self): # 1. 异步生成序列 rollout_future = self.actor_rollout_wg.generate_sequences.remote(training_data) # 2. 控制流可以继续其他工作 print(\u0026#34;序列生成正在进行中...\u0026#34;) # 3. 当需要结果时再等待 rollout_data = ray.get(rollout_future) print(\u0026#34;序列生成完成\u0026#34;) # 4. 继续后续步骤 value_data = self.critic_wg.compute_values(rollout_data) 9. Ray集群中的物理数据流动 9.1 Ray集群物理架构 9.1.1 集群组成 graph TB\rsubgraph \u0026#34;Head Node (主节点)\u0026#34;\rA[Ray Head Process]\rB[Object Store]\rC[GCS - Global Control Service]\rD[Driver Process\u0026lt;br/\u0026gt;控制流]\rend\rsubgraph \u0026#34;Worker Node 1\u0026#34;\rE[Ray Worker Process 1]\rF[Object Store 1]\rG[ActorRolloutWorker 1]\rH[ActorRolloutWorker 2]\rend\rsubgraph \u0026#34;Worker Node 2\u0026#34;\rI[Ray Worker Process 2]\rJ[Object Store 2]\rK[CriticWorker 1]\rL[CriticWorker 2]\rend\rsubgraph \u0026#34;Worker Node 3\u0026#34;\rM[Ray Worker Process 3]\rN[Object Store 3]\rO[ReferenceWorker 1]\rP[ReferenceWorker 2]\rend\rA -.-\u0026gt;|心跳| E\rA -.-\u0026gt;|心跳| I\rA -.-\u0026gt;|心跳| M\rD --\u0026gt;|远程调用| G\rD --\u0026gt;|远程调用| H\rD --\u0026gt;|远程调用| K\rD --\u0026gt;|远程调用| L\rD --\u0026gt;|远程调用| O\rD --\u0026gt;|远程调用| P\rB -.-\u0026gt;|数据共享| F\rB -.-\u0026gt;|数据共享| J\rB -.-\u0026gt;|数据共享| N\rstyle A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle D fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff\rstyle G fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff\rstyle K fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,color:#fff\rstyle O fill:#607D8B,stroke:#37474F,stroke-width:2px,color:#fff 9.1.2 Ray集群启动过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 启动Head节点 ray start --head --port=6379 --dashboard-host=0.0.0.0 --dashboard-port=8265 # 2. 启动Worker节点 ray start --address=\u0026#39;head_node_ip:6379\u0026#39; # 3. 在Driver中初始化集群 import ray ray.init(address=\u0026#39;ray://head_node_ip:10001\u0026#39;) # 4. 创建WorkerGroup resource_pool = RayResourcePool( process_on_nodes=[4, 4, 4], # 每个节点4个进程 use_gpu=True, max_colocate_count=1 ) # 5. 启动Worker进程 actor_rollout_wg = RayWorkerGroup( resource_pool=resource_pool, ray_cls_with_init=RayClassWithInitArgs(ActorRolloutWorker, config) ) 9.2 Ray Object Store 核心概念 在深入理解DataProto的物理传输过程之前，我们需要先了解Ray集群中的Object Store机制，这是数据在节点间传输的基础设施。\n9.2.1 Object Store 概述 Object Store是Ray分布式系统的核心组件，用于在集群节点间高效地存储和共享数据。它基于Apache Arrow的Plasma实现，提供了高性能的分布式内存存储。\ngraph TB\rsubgraph \u0026#34;Head Node\u0026#34;\rA[HeadStore\u0026lt;br/\u0026gt;Plasma Store] --\u0026gt; B[共享内存池]\rA --\u0026gt; C[元数据管理]\rA --\u0026gt; D[对象引用表]\rend\rsubgraph \u0026#34;Worker Node\u0026#34;\rE[WorkerStore\u0026lt;br/\u0026gt;Plasma Store] --\u0026gt; F[本地内存池]\rE --\u0026gt; G[本地缓存]\rE --\u0026gt; H[磁盘溢出]\rend\rA -.-\u0026gt;|网络传输| E\rstyle A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle E fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff 核心组件：\nHeadStore：Head节点上的Object Store，存储全局共享数据，管理元数据和对象引用 WorkerStore：Worker节点上的Object Store，存储本地计算数据，提供快速数据访问 工作原理：\n数据存储：通过ray.put()将数据存储到Object Store，返回ObjectRef 数据获取：通过ray.get(ObjectRef)从Object Store读取数据 内存管理：自动管理内存使用，支持磁盘溢出和垃圾回收 9.2.2 Object Store 在verl中的作用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Object Store基本操作示例 import ray # 1. 存储DataProto到Object Store data_proto = DataProto.from_dict(tensors={\u0026#34;input_ids\u0026#34;: torch.randn(256, 512)}) object_ref = ray.put(data_proto) # 存储到本地Object Store # 2. 发送ObjectRef到远程Worker @ray.remote def worker_function(ref): data = ray.get(ref) # 从Object Store获取数据 return process_data(data) # 3. 执行远程调用 future = worker_function.remote(object_ref) result = ray.get(future) 关键特性：\n序列化优化：自动处理DataProto的序列化和反序列化 内存共享：支持跨进程和跨节点的内存共享 网络传输：自动处理网络传输和错误恢复 性能优化：支持数据压缩和缓存机制 9.3 DataProto的物理传输过程 9.3.1 序列化与网络传输 sequenceDiagram\rparticipant D as Driver\rparticipant H as HeadStore\rparticipant W as Worker\rparticipant WS as WorkerStore\rparticipant A as Actor\rD-\u0026gt;\u0026gt;D: 创建DataProto\rNote over D: batch_size=256\rD-\u0026gt;\u0026gt;D: DataProto序列化\rNote over D: TensorDict序列化\rD-\u0026gt;\u0026gt;H: 存储序列化数据\rNote over H: 生成ObjectRef\rD-\u0026gt;\u0026gt;W: 发送ObjectRef\rNote over W: 方法调用\rW-\u0026gt;\u0026gt;H: 获取序列化数据\rH-\u0026gt;\u0026gt;W: 返回数据块\rW-\u0026gt;\u0026gt;W: DataProto反序列化\rNote over W: 重建TensorDict\rW-\u0026gt;\u0026gt;A: 调用generate_sequences\rA-\u0026gt;\u0026gt;A: 模型推理计算\rA-\u0026gt;\u0026gt;A: 创建结果DataProto\rA-\u0026gt;\u0026gt;W: 返回结果\rW-\u0026gt;\u0026gt;W: 结果序列化\rW-\u0026gt;\u0026gt;WS: 存储结果\rW-\u0026gt;\u0026gt;D: 返回ObjectRef\rD-\u0026gt;\u0026gt;WS: 获取结果数据\rD-\u0026gt;\u0026gt;D: 结果反序列化 9.3.2 数据序列化细节 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # DataProto的序列化过程 def __getstate__(self): \u0026#34;\u0026#34;\u0026#34;序列化DataProto\u0026#34;\u0026#34;\u0026#34; import io buffer = io.BytesIO() # 1. 序列化TensorDict if self.batch is not None: batch_to_save = self.batch.contiguous().consolidate() torch.save(batch_to_save, buffer) # 2. 序列化numpy数组和meta_info buffer_bytes = buffer.getvalue() return buffer_bytes, self.non_tensor_batch, self.meta_info def __setstate__(self, data): \u0026#34;\u0026#34;\u0026#34;反序列化DataProto\u0026#34;\u0026#34;\u0026#34; import io batch_deserialized_bytes, non_tensor_batch, meta_info = data # 1. 反序列化TensorDict batch_deserialized = io.BytesIO(initial_bytes=batch_deserialized_bytes) batch = torch.load(batch_deserialized, map_location=\u0026#34;cpu\u0026#34;) # 2. 重建DataProto self.batch = batch self.non_tensor_batch = non_tensor_batch self.meta_info = meta_info 9.4 分布式数据流动架构 9.4.1 多节点数据分发 graph TD\rsubgraph \u0026#34;Head Node\u0026#34;\rA[Driver Process] --\u0026gt; B[DataProto创建]\rB --\u0026gt; C[序列化]\rC --\u0026gt; D[Object Store]\rend\rsubgraph \u0026#34;Worker Node 1\u0026#34;\rE[Worker Process 1] --\u0026gt; F[ActorRolloutWorker 1]\rE --\u0026gt; G[ActorRolloutWorker 2]\rend\rsubgraph \u0026#34;Worker Node 2\u0026#34;\rH[Worker Process 2] --\u0026gt; I[CriticWorker 1]\rH --\u0026gt; J[CriticWorker 2]\rend\rsubgraph \u0026#34;Worker Node 3\u0026#34;\rK[Worker Process 3] --\u0026gt; L[ReferenceWorker 1]\rK --\u0026gt; M[ReferenceWorker 2]\rend\rD -.-\u0026gt;|网络传输| E\rD -.-\u0026gt;|网络传输| H\rD -.-\u0026gt;|网络传输| K\rE --\u0026gt; N[Object Store 1]\rH --\u0026gt; O[Object Store 2]\rK --\u0026gt; P[Object Store 3]\rN -.-\u0026gt;|结果收集| D\rO -.-\u0026gt;|结果收集| D\rP -.-\u0026gt;|结果收集| D\rstyle A fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff\rstyle D fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle F fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff\rstyle I fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,color:#fff\rstyle L fill:#607D8B,stroke:#37474F,stroke-width:2px,color:#fff 9.4.2 数据并行分发过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # 实际的数据分发过程 def dispatch_dp_compute_data_proto(worker_group, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34;DataProto数据并行分发\u0026#34;\u0026#34;\u0026#34; world_size = worker_group.world_size # 例如：8个worker # 1. 自动填充 data_proto = args[0] # 原始DataProto，batch_size=250 padded_data, pad_size = pad_dataproto_to_divisor(data_proto, world_size) # 现在padded_data的batch_size=256 (填充了6个样本) # 2. 分割数据 chunks = padded_data.chunk(chunks=world_size) # 每个chunk的batch_size=32 # 3. 分发到各个worker futures = [] for i, worker in enumerate(worker_group.workers): chunk = chunks[i] # 序列化chunk并通过Ray发送 future = worker.generate_sequences.remote(chunk) futures.append(future) return futures # 在物理层面的实际传输 def physical_data_transfer_example(): \u0026#34;\u0026#34;\u0026#34;展示物理层面的数据传输\u0026#34;\u0026#34;\u0026#34; # 原始数据大小 batch_size = 250 seq_len = 512 vocab_size = 32000 # 计算数据大小 prompt_ids_size = batch_size * seq_len * 4 # int32, bytes attention_mask_size = batch_size * seq_len * 4 # int32, bytes tensor_data_size = prompt_ids_size + attention_mask_size # ~2MB # 序列化开销 serialization_overhead = tensor_data_size * 0.1 # 约10%开销 # 网络传输 network_bandwidth = 10 * 1024 * 1024 # 10Gbps transfer_time = (tensor_data_size + serialization_overhead) / network_bandwidth print(f\u0026#34;数据大小: {tensor_data_size / 1024 / 1024:.2f} MB\u0026#34;) print(f\u0026#34;序列化开销: {serialization_overhead / 1024 / 1024:.2f} MB\u0026#34;) print(f\u0026#34;传输时间: {transfer_time * 1000:.2f} ms\u0026#34;) 9.5 内存管理与对象存储 9.5.1 Ray Object Store架构 graph TB\rsubgraph \u0026#34;Head Node Object Store\u0026#34;\rA[Plasma Store] --\u0026gt; B[内存池]\rA --\u0026gt; C[共享内存]\rA --\u0026gt; D[磁盘存储]\rend\rsubgraph \u0026#34;Worker Node Object Store\u0026#34;\rE[Plasma Store] --\u0026gt; F[本地内存池]\rE --\u0026gt; G[本地共享内存]\rE --\u0026gt; H[本地磁盘缓存]\rend\rA -.-\u0026gt;|网络传输| E\rsubgraph \u0026#34;数据生命周期\u0026#34;\rI[创建DataProto] --\u0026gt; J[序列化]\rJ --\u0026gt; K[存储到Object Store]\rK --\u0026gt; L[生成ObjectRef]\rL --\u0026gt; M[发送到Worker]\rM --\u0026gt; N[Worker反序列化]\rN --\u0026gt; O[计算处理]\rO --\u0026gt; P[结果序列化]\rP --\u0026gt; Q[存储结果]\rQ --\u0026gt; R[返回ObjectRef]\rend\rstyle A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle E fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff\rstyle I fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff\rstyle O fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,color:#fff 9.5.2 内存优化策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 内存优化示例 class MemoryOptimizedDataProto: def __init__(self): self.object_refs = {} # 缓存ObjectRef def send_to_worker(self, data_proto, worker_id): \u0026#34;\u0026#34;\u0026#34;优化的数据传输\u0026#34;\u0026#34;\u0026#34; # 1. 检查是否已有缓存 cache_key = f\u0026#34;{worker_id}_{hash(data_proto)}\u0026#34; if cache_key in self.object_refs: return self.object_refs[cache_key] # 2. 序列化并存储 object_ref = ray.put(data_proto) self.object_refs[cache_key] = object_ref # 3. 发送到worker return object_ref def cleanup_cache(self): \u0026#34;\u0026#34;\u0026#34;清理缓存\u0026#34;\u0026#34;\u0026#34; for ref in self.object_refs.values(): ray.delete(ref) self.object_refs.clear() 9.6 网络拓扑与性能优化 9.6.1 网络拓扑图 graph TB\rsubgraph \u0026#34;数据中心网络\u0026#34;\rA[Head Node\u0026lt;br/\u0026gt;10Gbps] --\u0026gt; B[Top of Rack Switch]\rB --\u0026gt; C[Worker Node 1\u0026lt;br/\u0026gt;10Gbps]\rB --\u0026gt; D[Worker Node 2\u0026lt;br/\u0026gt;10Gbps]\rB --\u0026gt; E[Worker Node 3\u0026lt;br/\u0026gt;10Gbps]\rB --\u0026gt; F[Worker Node 4\u0026lt;br/\u0026gt;10Gbps]\rend\rsubgraph \u0026#34;节点内部\u0026#34;\rC --\u0026gt; G[GPU 0]\rC --\u0026gt; H[GPU 1]\rC --\u0026gt; I[GPU 2]\rC --\u0026gt; J[GPU 3]\rend\rsubgraph \u0026#34;数据流动路径\u0026#34;\rK[Driver] --\u0026gt; L[序列化]\rL --\u0026gt; M[网络传输]\rM --\u0026gt; N[Worker接收]\rN --\u0026gt; O[反序列化]\rO --\u0026gt; P[GPU计算]\rP --\u0026gt; Q[结果序列化]\rQ --\u0026gt; R[网络返回]\rR --\u0026gt; S[Driver接收]\rend\rstyle A fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff\rstyle C fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff\rstyle G fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff\rstyle K fill:#9C27B0,stroke:#6A1B9A,stroke-width:2px,color:#fff Verl 通过 DataProto 和 HybridFlow 设计，成功解决了大规模强化学习训练中的架构挑战，为LLM后训练提供了高效、灵活、可扩展的解决方案。\n","permalink":"https://pillumina.github.io/posts/aiinfra/10-verl-dataproto/","summary":"\u003ch1 id=\"verl-dataproto-实现原理与数据流动分析\"\u003eVerl DataProto 实现原理与数据流动分析\u003c/h1\u003e\n\u003ch2 id=\"目录\"\u003e目录\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#1-%e6%a6%82%e8%bf%b0\"\u003e1. 概述\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#2-dataproto-%e6%a0%b8%e5%bf%83%e6%9e%b6%e6%9e%84\"\u003e2. DataProto 核心架构\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#3-hybridflow-%e8%ae%be%e8%ae%a1%e7%90%86%e5%bf%b5\"\u003e3. HybridFlow 设计理念\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#4-%e6%8e%a7%e5%88%b6%e6%b5%81%e4%b8%8e%e8%ae%a1%e7%ae%97%e6%b5%81%e5%88%86%e7%a6%bb\"\u003e4. 控制流与计算流分离\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#5-%e6%95%b0%e6%8d%ae%e6%b5%81%e5%8a%a8%e6%9c%ba%e5%88%b6\"\u003e5. 数据流动机制\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#6-dispatch-%e6%a8%a1%e5%bc%8f%e8%af%a6%e8%a7%a3\"\u003e6. Dispatch 模式详解\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#7-%e6%80%a7%e8%83%bd%e4%bc%98%e5%8c%96%e7%ad%96%e7%95%a5\"\u003e7. 性能优化策略\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/aiinfra/10-verl-dataproto/#8-%e6%80%bb%e7%bb%93\"\u003e8. 总结\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"1-概述\"\u003e1. 概述\u003c/h2\u003e\n\u003cp\u003eVerl 是一个基于 HybridFlow 论文的开源强化学习训练框架，专门为大语言模型的后训练优化而设计。其核心创新在于将控制流和计算流分离，通过 DataProto 协议实现高效的数据交换。\u003c/p\u003e\n\u003ch2 id=\"2-dataproto-核心架构\"\u003e2. DataProto 核心架构\u003c/h2\u003e\n\u003ch3 id=\"21-数据结构设计\"\u003e2.1 数据结构设计\u003c/h3\u003e\n\u003cp\u003eDataProto 是 verl 框架中用于数据交换的核心协议，所有在 Worker 之间流转的数据，都被统一封装在一个名为 DataProto 的数据结构中。它不仅仅是一个字典，更承载着 RLHF 流程中所有的信息演变, 基于 PyTorch 的 TensorDict 构建：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@dataclass\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDataProto\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eTensorDict\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e              \u003cspan class=\"c1\"\u003e# 张量数据容器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003enon_tensor_batch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003edict\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003efield\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edefault_factory\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nb\"\u003edict\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# 非张量数据\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emeta_info\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003edict\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003efield\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edefault_factory\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nb\"\u003edict\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e         \u003cspan class=\"c1\"\u003e# 元信息\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e核心特性：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e统一接口\u003c/strong\u003e: 提供标准化的数据容器，支持张量和非张量数据\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e设备管理\u003c/strong\u003e: 自动处理 GPU/CPU 设备间的数据移动\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e内存优化\u003c/strong\u003e: 支持分块处理和内存复用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e序列化\u003c/strong\u003e: 支持高效的序列化和反序列化\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"22-数据一致性检查\"\u003e2.2 数据一致性检查\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003echeck_consistency\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;检查 DataProto 的一致性\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch_size\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;只支持 num_batch_dims=1\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enon_tensor_batch\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eval\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enon_tensor_batch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"nb\"\u003eisinstance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eval\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enp\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003endarray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# 检查批次大小一致性\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enon_tensor_batch\u003c/span\u003e \u003cspan class=\"ow\"\u003eis\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ebatch_size\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebatch_size\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eval\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enon_tensor_batch\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eassert\u003c/span\u003e \u003cspan class=\"n\"\u003eval\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eshape\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003ebatch_size\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"3-hybridflow-设计理念\"\u003e3. HybridFlow 设计理念\u003c/h2\u003e\n\u003ch3 id=\"31-设计动机\"\u003e3.1 设计动机\u003c/h3\u003e\n\u003cp\u003e传统 RL 系统面临的问题：\u003c/p\u003e","title":"[VeRL] DataProto介绍"},{"content":"最近 RL sys 圈子的吴锡斌老师在 verl 上设计了将 rollout 与 tool 调用解耦的 AgentLoop，实现了自由灵活的 mutli-turn RL。在每个 AgentLoop 内部，rollout engine 只对外提供一个 token-in-token-out 的接口，而 tool 调用则通过 ToolAgentLoop 来实现。我个人比较喜欢这样解耦的设计，同时，AgentLoop 的代码结构也比较清晰。我个人学习了一次整个代码后，觉着 AgentLoop 的设计甚是不错，但是 ActorRolloutRefWorker 的历史包袱还是很重。\n本文简单分析了 agent loop 的源码，并给出了一些自己的看法。\n如果我们把整个 ActorRolloutRefWorker 当做一个 sgl.Engine 的话，AgentLoop 里面包装的两层 AsyncSGLangServer 和 AsyncLLMServerManager。AsyncSGLangServer 相当于在 sgl.Engine 上包装了 fastapi 成了 server，而 AsyncLLMServerManager 是在 server 上包了一层 router 做 load balance，相当于 sglang 的 router。这两层设计都是合理的，主要麻烦的是 ActorRolloutRefWorker，层层调用，最后一共经过 7 个 class 才调到 sgl.Engine，最近 verl 团队也在致力于对这块 worker class 的重构，敬请期待。最后，AgentLoopManager，AgentLoopWorker 和 AgentLoop 这三层，我觉得 AgentLoopWorker 可能未必有必要，其他两层挺合理的。\nRelated Resources Script\nhttps://github.com/zhaochenyang20/Awesome-ML-SYS-Tutorial/blob/main/rlhf/verl/multi-turn/tool_examples/agent_loop.md\nRelated PR\nhttps://github.com/volcengine/verl/pull/2124\nDesign Docs\nhttps://github.com/volcengine/verl/pull/2563\nhttps://github.com/volcengine/verl/pull/A2598\nCommit we are looking at\nhttps://github.com/volcengine/verl/tree/c5b189a1af496d0bc68320cd1d5bd7a1f1e3638a\n使用 AgentLoop 安装 verl-sglang 的最新版本：\n1 2 3 4 5 6 7 8 cd ~ git clone https://github.com/volcengine/verl.git cd verl python -m uv pip install wheel setuptools python3 -m uv pip install -e \u0026#34;.[sglang]\u0026#34; --prerelease=allow python3 -m uv pip install -r ./requirements.txt --no-build-isolation python3 -m uv pip install torch_memory_saver 具体实现自己的 agent loop（见下文分析），然后配置 config 文件：\n1 2 3 actor_rollout_ref.rollout.mode=async \\ actor_rollout_ref.rollout.multi_turn.enable=true \\ actor_rollout_ref.rollout.name=sglang \\ 注意，不使用 actor_rollout_ref.rollout.mode=async 的话，会启用 SGLangRollout 本身管理的 mutli-turn 功能，在效果上和 AgentLoop 完全一致。\n最后，在数据集构建过程中添加一个新的 agent_name 字段，比如我们在 ~/verl/examples/data_preprocess/gsm8k_multiturn_w_tool.py 中追加 \u0026quot;agent_name\u0026quot;: \u0026quot;tool_agent\u0026quot;：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def make_map_fn(split): def process_fn(example, idx): question_raw = example.pop(\u0026#34;question\u0026#34;) question = question_raw + \u0026#34; \u0026#34; + instruction_following answer_raw = example.pop(\u0026#34;answer\u0026#34;) solution = extract_solution(answer_raw) data = { \u0026#34;data_source\u0026#34;: data_source, # new column for agent loop \u0026#34;agent_name\u0026#34;: \u0026#34;tool_agent\u0026#34;, \u0026#34;prompt\u0026#34;: [ { #... } ] } return data return process_fn 调用总览 main_ppo.py -\u0026gt; RayPPOTrainer(fit)-\u0026gt; AgentLoopManager(async) -\u0026gt; AgentLoopWorker -\u0026gt; AsyncLLMServerManager -\u0026gt; AsyncSGLangServer -\u0026gt; AsyncActorRolloutRefWorker -\u0026gt; SGLangRollout -\u0026gt; AsyncEngine -\u0026gt; sgl.Engine\nTaskRunner 启动训练，调用 RayPPOTrainer.fit()。 RayPPOTrainer 管理训练流程，调用 AgentLoopManager.generate_sequences() 开始层层向下调用，同时初始化 AsyncActorRolloutRefWorker。 AgentLoopManager 初始化 dp 个 AsyncSGLangServer，随后，初始化 num_rollout_workers 个 AgentLoopWorker。 接着，每个 AgentLoopWorker 根据 agent_name 从预先注册好的 _agent_loop_registry 初始化自身管理的 train_batch_size / num_rollout_workers 个 AgentLoop 实例，对于 GRPO，train_batch_size 需要乘以 group size。用户可以依照自身需求注册新的 AgentLoop，目前通过 ToolAgentLoop 来完全覆盖了 SGLangRollout 中基于 _req_level_generate_sequences 实现的 tool call 管理。也就是说， 先前的 multi-turn RL 的 tool 状态管理是在 SGLangRollout 内实现的，而 AgentLoop 将这层管理抽象了出来，SGLangRollout 只是向上包装为 AsyncSGLangServer 来完成 token-in-token-out。 AgentLoop 初始化后，管理 tool 调用的各种状态，并且根据 policy 的返回情况，向下层层调用 AsyncLLMServerManager -\u0026gt; AsyncSGLangServer -\u0026gt; AsyncActorRolloutRefWorker -\u0026gt; SGLangRollout -\u0026gt; AsyncEngine -\u0026gt; sgl.Engine，得到模型输出。 返回输出后，AgentLoop生命周期结束。 AgentLoopWorker收集所有AgentLoop的返回值，上交给AgentLoopManager，等待下一次调用。 AgentLoopManager收集所有AgentLoopWorker的返回值，返回。 AgentLoopManager AgentLoop 的最顶层管理者，负责管理 AgentLoopWorker 以及 LLM servers 的生命周期。核心方法是generate_sequences：向下层层调用，得到 policy model 在给定的 agent loop 环境下的 trajectories。\n核心 API 在 RayPPOTrainer 中被初始化：\n1 2 3 4 5 6 7 8 if self.config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34;: from verl.experimental.agent_loop import AgentLoopManager self.async_rollout_mode = True self.async_rollout_manager = AgentLoopManager( config=self.config, worker_group=self.actor_rollout_wg, ) 具体的初始化非常简洁：\n__init__\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def __init__(self, config: DictConfig, worker_group: RayWorkerGroup): \u0026#34;\u0026#34;\u0026#34;Initialize agent loop manager. Args: config (DictConfig): trainer config. worker_group (RayWorkerGroup): ActorRolloutRef worker group. \u0026#34;\u0026#34;\u0026#34; self.config = config self.worker_group = worker_group self._initialize_llm_servers() self._init_agent_loop_workers() # Initially we\u0026#39;re in sleep mode. self.sleep() 传入 ActorRolloutRefWOrker 对应的 worker group，在_initialize_llm_servers里用来查找对应的 RolloutWorker； 初始化 llm server 和 agent loop workers； _initialize_llm_servers\n计算 dp size：self.rollout_dp_size = self.worker_group.world_size // self.rollout_tp_size 通过async_server_class(rollout_backend=self.config.actor_rollout_ref.rollout.name)获取服务器类，如 Async``SGLang``Server，作为和下层的 sgl.Engine 通信的转接层。 用 ray 初始化 dp size 个 server，为每个 dp rank 创建 server 实例。 通过ray.get(server.get_server_address.remote())获取并记录每个服务器的地址 调用ray.get([server.init_engine.remote() for server in self.async_llm_servers])；server 从 ray 通过前缀查询，在已经初始化好的 ray actor 中拿到自己对应的所有 SGLang engine。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 def _initialize_llm_servers(self): # 计算 dp size self.rollout_tp_size = self.config.actor_rollout_ref.rollout.tensor_model_parallel_size self.rollout_dp_size = self.worker_group.world_size // self.rollout_tp_size # 获取 worker 信息用于节点亲和性调度 register_center = ray.get_actor(f\u0026#34;{self.worker_group.name_prefix}_register_center\u0026#34;) workers_info = ray.get(register_center.get_worker_info.remote()) assert len(workers_info) == self.worker_group.world_size self.async_llm_servers = [None] * self.rollout_dp_size self.server_addresses = [None] * self.rollout_dp_size # 根据 config 拿到对应的 server, e.g., AsyncSGLangServer if self.config.actor_rollout_ref.rollout.agent.custom_async_server: server_class = async_server_class( rollout_backend=self.config.actor_rollout_ref.rollout.name, rollout_backend_module=self.config.actor_rollout_ref.rollout.agent.custom_async_server.path, rollout_backend_class=self.config.actor_rollout_ref.rollout.agent.custom_async_server.name, ) else: server_class = async_server_class(rollout_backend=self.config.actor_rollout_ref.rollout.name) # 用 ray 初始化 dp rank 个 AsyncServer unready_dp_ranks = set(range(self.rollout_dp_size)) while len(unready_dp_ranks) \u0026gt; 0: servers = { rollout_dp_rank: server_class.options( # 确保 AsyncServer 与对应的工作器在同一节点 scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy( node_id=workers_info[rollout_dp_rank * self.rollout_tp_size], soft=False, ), name=f\u0026#34;async_llm_server_{rollout_dp_rank}\u0026#34;, ).remote(self.config, self.rollout_dp_size, rollout_dp_rank, self.worker_group.name_prefix) for rollout_dp_rank in unready_dp_ranks } # 记录 server 地址 for rollout_dp_rank, server in servers.items(): try: address = ray.get(server.get_server_address.remote()) self.server_addresses[rollout_dp_rank] = address self.async_llm_servers[rollout_dp_rank] = server unready_dp_ranks.remove(rollout_dp_rank) except Exception: ray.kill(server) print(f\u0026#34;rollout server {rollout_dp_rank} failed, maybe address already in use, restarting...\u0026#34;) # 初始化 server，这个初始化是 server 从 ray 中拿到自己 dp 对应的所有 worker ray.get([server.init_engine.remote() for server in self.async_llm_servers]) _init_agent_loop_workers\n在 ray 上初始化 rollout.agent.num_workers 个 AgentLoopWorker：\n1 2 3 4 5 6 7 8 def _init_agent_loop_workers(self): self.agent_loop_workers = [] for i in range(self.config.actor_rollout_ref.rollout.agent.num_workers): self.agent_loop_workers.append( AgentLoopWorker.options( name=f\u0026#34;agent_loop_worker_{i}\u0026#34;, ).remote(self.config, self.async_llm_servers) ) generate_sequences\n如果配置了free_cache_engine，先调用self.wake_up() chunkes = prompts.chunk(len(self.agent_loop_workers)) 将输入批次按 AgentLoopWorker 数量分块。 每个 agentLoopWorker 处理自身的 chunk，通过ray.get([worker.generate_sequences.remote(chunk) for ...])并行执行并得到结果； 处理完成后调用self.sleep()让 server 进入睡眠状态以释放显存 计算生成序列和工具调用的性能指标 合并所有 A``gentLoopWorker 的输出并返回 Code link [here]\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def generate_sequences(self, prompts: DataProto) -\u0026gt; DataProto: if self.config.actor_rollout_ref.rollout.free_cache_engine: self.wake_up() # 唤醒所有 LLM 服务器 chunkes = prompts.chunk(len(self.agent_loop_workers)) # 按 worker 数量分块 outputs = ray.get( [worker.generate_sequences.remote(chunk) for worker, chunk in zip(self.agent_loop_workers, chunkes)] ) # 并行分发到各个 AgentLoopWorker output = DataProto.concat(outputs) # 聚合所有 worker 的输出 if self.config.actor_rollout_ref.rollout.free_cache_engine: self.sleep() # 让服务器进入睡眠状态，释放显存 # 计算性能指标 metrics = [output.meta_info[\u0026#34;metrics\u0026#34;] for output in outputs] timing = self._performance_metrics(metrics, output) output.meta_info = {\u0026#34;timing\u0026#34;: timing} return output AsyncSGLangServer 基于 SGLang 的异步服务器实现，继承自AsyncServerBase。作为 Ray 远程 actor 运行，负责将收到的请求转发给下层的 SGLang Engine。出于 SGLang 的设计，调用 generate 的时候只需要对 master worker（verl 的 inference tp 0）调用即可。\n核心 API init_engine\n异步初始化 SGLang 引擎：\n通过ray.util.list_named_actors查找所有匹配的 actors； 根据命名规则 self.wg_prefix + \u0026quot;WorkerDict_\u0026quot; 解析 actor 名称； 根据 dp_rank 和 tp_size 分配 actor，确定 master worker（tp rank 0） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 async def init_engine(self): if self.workers: # avoid init twice return all_actors = ray.util.list_named_actors(all_namespaces=True) matched_actors = [ actor for actor in all_actors if actor.get(\u0026#34;name\u0026#34;, None).startswith(self.wg_prefix + \u0026#34;WorkerDict_\u0026#34;) ] gpu_per_node = len(set([actor[\u0026#34;name\u0026#34;].split(\u0026#34;:\u0026#34;)[1] for actor in matched_actors])) # total gpu num assert len(matched_actors) == self._dp_size * self._tp_size for matched_actor in matched_actors: fields = matched_actor[\u0026#34;name\u0026#34;].split(\u0026#34;:\u0026#34;) assert len(fields) == 2, f\u0026#34;invalid actor name: {matched_actor[\u0026#39;name\u0026#39;]}\u0026#34; pg_index, local_rank = int(fields[0].split(\u0026#34;_\u0026#34;)[-1]), int(fields[1]) current_global_rank = gpu_per_node * pg_index + local_rank worker_dp_rank = current_global_rank // self._tp_size worker_tp_rank = current_global_rank % self._tp_size if worker_dp_rank == self._dp_rank: worker = ray.get_actor(**matched_actor) self.workers.append(worker) if worker_tp_rank == 0: self.master_worker = worker chat_completion\n处理 chat_completion 请求：\n1 2 3 4 5 async def chat_completion(self, raw_request: Request): request = await raw_request.json() output_future = self.master_worker.chat_completion.remote(request) [outputs] = await asyncio.gather(output_future) return JSONResponse(outputs) 将请求转发给 master worker 处理 返回 JSON 格式的响应 generate\nToken in token out 来获得 SGLang Engine 的 inference 结果：\n1 2 async def generate(self, prompt_ids: List[int], sampling_params: Dict[str, Any], request_id: str) -\u0026gt; List[int]: return await self.master_worker.generate.remote(prompt_ids, sampling_params, request_id) 直接调用 master worker 的生成方法 支持自定义采样参数 AsyncLLMServerManager 管理多个 OpenAI 兼容的 LLM 服务器 (例如 Async``SGLang``Server)，提供负载均衡和会话粘性功能。支持最少请求负载均衡算法，确保多轮对话发送到同一服务器以实现自动前缀缓存。可以认为就是简单的 router/load balancer 层。\n初始化\n配置服务器句柄列表，随机打乱顺序 初始化最少请求负载均衡器：self.weighted_serveres = [[0, (hash(server), server)] for server in server_handles] 创建 LRU 缓存：self.request_id_to_server = LRUCache(maxsize=max_cache_size)用于 request_id 到服务器的映射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def __init__(self, config: DictConfig, server_handles: List[ray.actor.ActorHandle], max_cache_size: int = 10000): \u0026#34;\u0026#34;\u0026#34;Initialize the AsyncLLMServerManager. Args: config (DictConfig): YAML config. server_handles (List[ray.actor.ActorHandle]): OpenAI compatible LLM server actor handles. max_cache_size (int, optional): max cache size for request_id to server mapping. Defaults to 10000. \u0026#34;\u0026#34;\u0026#34; self.config = config self.server_handles = server_handles random.shuffle(self.server_handles) # Least requests load balancing self.weighted_serveres = [[0, (hash(server), server)] for server in server_handles] heapq.heapify(self.weighted_serveres) # LRU cache to map request_id to server self.request_id_to_server = LRUCache(maxsize=max_cache_size) _choose_server\n1 2 3 4 5 6 7 8 9 def _choose_server(self, request_id: str) -\u0026gt; ray.actor.ActorHandle: if request_id in self.request_id_to_server: return self.request_id_to_server[request_id] # 会话粘性 server = self.weighted_serveres[0][1][1] # 最少请求的服务器 self.weighted_serveres[0][0] += 1 # 增加请求计数 heapq.heapreplace(self.weighted_serveres, self.weighted_serveres[0]) self.request_id_to_server[request_id] = server return server 会话粘性：相同 request_id 发送给同一 server 最少请求：新请求分配给当前负载最轻的 server 动态更新：使用堆结构维护服务器负载状态 generate\n1 2 3 4 5 6 7 8 9 @rollout_trace_op async def generate(self, request_id, *, prompt_ids: List[int], sampling_params: Dict[str, Any]) -\u0026gt; List[int]: server = self._choose_server(request_id) output = await server.generate.remote( request_id=request_id, prompt_ids=prompt_ids, sampling_params=sampling_params, ) return output 根据 request_id 选择 server 异步调用 server 的生成接口，token-in-token-out 支持性能追踪 AgentLoopWorker AgentLoopWorker 负责接收数据，向下发给具体的 AgentLoop。虽然名字是 worker，但是\n从 ray 的角度来说，AgentLoopWorker 是有状态的，是 ray actor，而不是 ray worker 核心函数 generate 是层层套壳，调用其他类；例如 single_turn_agent_loop 和 tool_agent_loop 来 generate（当然这两个类的 generate 也是向下调用，下面会讲到） __init__\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @ray.remote class AgentLoopWorker: \u0026#34;\u0026#34;\u0026#34;Agent loop worker takes a batch of messages and run each message in an agent loop.\u0026#34;\u0026#34;\u0026#34; def __init__(self, config: DictConfig, server_handles: list[ray.actor.ActorHandle]): \u0026#34;\u0026#34;\u0026#34;Initialize agent loop manager. Args: config (DictConfig): YAML config. server_handles (List[ray.actor.ActorHandle]): OpenAI compatible LLM server actor handles. \u0026#34;\u0026#34;\u0026#34; self.config = config self.server_manager = AsyncLLMServerManager(config, server_handles) model_path = config.actor_rollout_ref.model.path self.model_name = \u0026#34;/\u0026#34;.join(model_path.split(\u0026#34;/\u0026#34;)[-2:]) local_path = copy_to_local(config.actor_rollout_ref.model.path) self.tokenizer = hf_tokenizer(local_path, trust_remote_code=True) trace_config = self.config.actor_rollout_ref.rollout.get(\u0026#34;trace\u0026#34;, {}) RolloutTraceConfig.init( self.config.trainer.project_name, self.config.trainer.experiment_name, trace_config.get(\u0026#34;backend\u0026#34;), trace_config.get(\u0026#34;token2text\u0026#34;, False), ) 上游传过来的 config 和 server_handles 作为参数来初始化 AsyncLLMServerManager，之后会把这个 self.server_manager 传给下游； 根据 config 的 config.actor_rollout_ref.model.path 设置 model_path, local_path, tokenizer 配置 RolloutTraceConfig 用于追踪 trajectories generate_sequences\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 async def generate_sequences(self, batch: DataProto) -\u0026gt; DataProto: \u0026#34;\u0026#34;\u0026#34;Generate sequences from agent loop. Args: batch (DataProto): Input batch. Returns: DataProto: Output batch. - prompts: [bsz, prompt_length], prompt token ids from dataset. - responses: [bsz, response_length], output token ids include response tokens from LLM generation and observation tokens from tool_calls. - response_mask: [bsz, response_length], 1 for LLM generated tokens, 0 for observation/padding tokens. - input_ids: [bsz, prompt_length + response_length], whole sequence token ids, including prompt tokens and response tokens. - attention_mask: [bsz, prompt_length + response_length], 0 for padding tokens, 1 for other tokens. - position_ids: [bsz, prompt_length + response_length], incremental position ids. For multi-turn conversations: responses: |\u0026lt;- LLM generation -\u0026gt;|\u0026lt;- tool_calls -\u0026gt;|\u0026lt;- LLM generation -\u0026gt;|\u0026lt;- padding -\u0026gt;| response_mask: | 1, 1, 1, ..., 1, 1 | 0, 0, .., 0, 0 | 1, 1, 1, ..., 1, 1 | 0, 0, ..., 0| \u0026#34;\u0026#34;\u0026#34; config = self.config.actor_rollout_ref.rollout sampling_params = dict( temperature=config.temperature, top_p=config.top_p, repetition_penalty=1.0, ) # override sampling params for validation if batch.meta_info.get(\u0026#34;validate\u0026#34;, False): sampling_params[\u0026#34;top_p\u0026#34;] = config.val_kwargs.top_p sampling_params[\u0026#34;temperature\u0026#34;] = config.val_kwargs.temperature # by default, we assume it\u0026#39;s a single turn agent if \u0026#34;agent_name\u0026#34; not in batch.non_tensor_batch: batch.non_tensor_batch[\u0026#34;agent_name\u0026#34;] = np.array([\u0026#34;single_turn_agent\u0026#34;] * len(batch), dtype=object) tasks = [] agent_names = batch.non_tensor_batch[\u0026#34;agent_name\u0026#34;] raw_prompts = batch.non_tensor_batch[\u0026#34;raw_prompt\u0026#34;] if \u0026#34;index\u0026#34; in batch.non_tensor_batch: index = batch.non_tensor_batch[\u0026#34;index\u0026#34;] else: index = np.arange(len(raw_prompts)) trajectory_info = await get_trajectory_info( batch.meta_info.get(\u0026#34;global_steps\u0026#34;, -1), index, batch.meta_info.get(\u0026#34;validate\u0026#34;, False) ) for agent_name, messages, trajectory in zip(agent_names, raw_prompts, trajectory_info, strict=True): tasks.append( asyncio.create_task(self._run_agent_loop(agent_name, messages.tolist(), sampling_params, trajectory)) ) outputs = await asyncio.gather(*tasks) output = self._postprocess(outputs) return output 利用上游传来的 config，创建给下游使用的 sampling_params；对 validation batch 要用 validation 参数。 利用 batch 的 meta_info，获得 agent_name, raw_prompts, index。再用这个 meta_info 处理获得 trajectory_info；就是利用刚才的 index 来计算在每一个 step 每一个 prompt 被 rollout 的次数，然后存到一个 list 中获得整个 rollout 的 trace； 利用 agent_names, raw_prompts, trajectory_info 来并发执行 _run_agent_loop。 在 _run_agent_loop 函数内，就要进行相应 agent_name 的 agent_loop 实例化，以及调用 agent_loop 对应的 run 函数来 generate。 在 _postprocess 中，会根据前面计算出来的 output（被封装成了 AgentLoopOutput 格式）来进行后处理；padding，加入 mask，最后封装成一个 DataProto 返回。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 async def _run_agent_loop( self, agent_name: str, messages: list[dict[str, Any]], sampling_params: dict[str, Any], trajectory: dict[str, Any], ) -\u0026gt; AgentLoopOutput: with rollout_trace_attr( step=trajectory[\u0026#34;step\u0026#34;], sample_index=trajectory[\u0026#34;sample_index\u0026#34;], rollout_n=trajectory[\u0026#34;rollout_n\u0026#34;], validate=trajectory[\u0026#34;validate\u0026#34;], name=\u0026#34;agent_loop\u0026#34;, ): assert agent_name in _agent_loop_registry, ( f\u0026#34;Agent loop {agent_name} not registered, registered agent loops: {_agent_loop_registry.keys()}\u0026#34; ) agent_loop_config = _agent_loop_registry[agent_name] agent_loop = hydra.utils.instantiate( config=agent_loop_config, trainer_config=_DummyConfig(config=self.config), server_manager=self.server_manager, tokenizer=self.tokenizer, ) output = await agent_loop.run(messages, sampling_params) return output def _postprocess(self, inputs: list[AgentLoopOutput]) -\u0026gt; DataProto: # NOTE: consistent with batch version of generate_sequences in vllm_rollout_spmd.py # prompts: left pad # responses: right pad # input_ids: prompt + response # attention_mask: [0,0,0,0,1,1,1,1, | 1,1,1,0,0,0,0,0] # position_ids: [0,0,0,0,0,1,2,3, | 4,5,6,7,8,9,10,11] # prompts self.tokenizer.padding_side = \u0026#34;left\u0026#34; outputs = self.tokenizer.pad( [{\u0026#34;input_ids\u0026#34;: input.prompt_ids} for input in inputs], padding=\u0026#34;max_length\u0026#34;, max_length=self.config.actor_rollout_ref.rollout.prompt_length, return_tensors=\u0026#34;pt\u0026#34;, return_attention_mask=True, ) prompt_ids, prompt_attention_mask = outputs[\u0026#34;input_ids\u0026#34;], outputs[\u0026#34;attention_mask\u0026#34;] # responses self.tokenizer.padding_side = \u0026#34;right\u0026#34; outputs = self.tokenizer.pad( [{\u0026#34;input_ids\u0026#34;: input.response_ids} for input in inputs], padding=\u0026#34;max_length\u0026#34;, max_length=self.config.actor_rollout_ref.rollout.response_length, return_tensors=\u0026#34;pt\u0026#34;, return_attention_mask=True, ) response_ids, response_attention_mask = outputs[\u0026#34;input_ids\u0026#34;], outputs[\u0026#34;attention_mask\u0026#34;] # response_mask outputs = self.tokenizer.pad( [{\u0026#34;input_ids\u0026#34;: input.response_mask} for input in inputs], padding=\u0026#34;max_length\u0026#34;, max_length=self.config.actor_rollout_ref.rollout.response_length, return_tensors=\u0026#34;pt\u0026#34;, return_attention_mask=False, ) response_mask = outputs[\u0026#34;input_ids\u0026#34;] assert response_ids.shape == response_mask.shape, ( f\u0026#34;mismatch in response_ids and response_mask shape: {response_ids.shape} vs {response_mask.shape}\u0026#34; ) response_mask = response_mask * response_attention_mask input_ids = torch.cat([prompt_ids, response_ids], dim=1) attention_mask = torch.cat([prompt_attention_mask, response_attention_mask], dim=1) position_ids = (attention_mask.cumsum(dim=1) - 1) * attention_mask batch = TensorDict( { \u0026#34;prompts\u0026#34;: prompt_ids, # [bsz, prompt_length] \u0026#34;responses\u0026#34;: response_ids, # [bsz, response_length] \u0026#34;response_mask\u0026#34;: response_mask, # [bsz, response_length] \u0026#34;input_ids\u0026#34;: input_ids, # [bsz, prompt_length + response_length] \u0026#34;attention_mask\u0026#34;: attention_mask, # [bsz, prompt_length + response_length] \u0026#34;position_ids\u0026#34;: position_ids, # [bsz, prompt_length + response_length] }, batch_size=len(input_ids), ) num_turns = np.array([input.num_turns for input in inputs], dtype=np.int32) metrics = [input.metrics.model_dump() for input in inputs] return DataProto(batch=batch, non_tensor_batch={\u0026#34;__num_turns__\u0026#34;: num_turns}, meta_info={\u0026#34;metrics\u0026#34;: metrics}) AgentLoop 终于进入到了具体的 agent loop 当中，我们观察两种具体的 AgentLoop。\nSingleTurnAgentLoop 这个 agent_loop 是默认的单轮对话，处理简单的一问一答，不支持工具调用；最重要的自然是 run 函数：\n我们传入 agent_loop的 messages 其实是我们从 batch 里面获得的 raw_prompt，此处调用 apply_chat_template； 调用 server_manager 里面的 generate 函数来计算 response_ids； 计算 response_mask，并根据 response_length 截取，封装这些结果成 AgentLoopOutput，padding 在上层 AgentLoopManager 的 _postprocess 内做； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class SingleTurnAgentLoop(AgentLoopBase): \u0026#34;\u0026#34;\u0026#34;Naive agent loop that only do single turn chat completion.\u0026#34;\u0026#34;\u0026#34; def __init__(self, config, server_manager, tokenizer): super().__init__(config, server_manager, tokenizer) self.prompt_length = config.actor_rollout_ref.rollout.prompt_length self.response_length = config.actor_rollout_ref.rollout.response_length async def run(self, messages: list[dict[str, Any]], sampling_params: dict[str, Any]) -\u0026gt; AgentLoopOutput: metrics = {} request_id = uuid4().hex prompt_ids = await self.loop.run_in_executor( None, lambda: self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=True) ) with simple_timer(\u0026#34;generate_sequences\u0026#34;, metrics): response_ids = await self.server_manager.generate( request_id=request_id, prompt_ids=prompt_ids, sampling_params=sampling_params ) response_mask = [1] * len(response_ids) output = AgentLoopOutput( prompt_ids=prompt_ids, response_ids=response_ids[: self.response_length], response_mask=response_mask[: self.response_length], num_turns=2, metrics=metrics, ) return output ToolAgentLoop 终于到了最核心的地方。ToolAgentLoop 支持多轮对话和工具调用。目前 ToolAgentLoop 可以完全覆盖 SGLangRollout 中基于 _async_rollout_a_request 实现的 tool call 管理。但状态数量和转移关系更加简单。也就是说， 先前的 multi-turn RL 的 tool 状态管理是在 SGLangRollout 内实现的，而 AgentLoop 提前将这层管理抽象了出来。\ninit_class\n下面只介绍一些关键参数的作用:\n**tool_response_truncate_side：**控制工具响应内容过长时的截断方式。 \u0026quot;left\u0026quot;：从左侧截断，保留开头部分 + \u0026ldquo;\u0026hellip;(truncated)\u0026quot;； \u0026quot;right\u0026quot;：从右侧截断，保留结尾部分，前面加 \u0026ldquo;(truncated)\u0026hellip;\u0026quot;； 其他值：从中间截断，保留开头和结尾部分，中间加 \u0026ldquo;\u0026hellip;(truncated)\u0026hellip;\u0026rdquo; tool_config_path：指定包含工具定义和配置信息的配置文件位置，用于初始化可用的工具列表，比如verl/examples/sglang_multiturn/config/tool_config/gsm8k_tool_config.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 tools: - class_name: \u0026#34;verl.tools.gsm8k_tool.Gsm8kTool\u0026#34; config: type: native tool_schema: type: \u0026#34;function\u0026#34; function: name: \u0026#34;calc_gsm8k_reward\u0026#34; description: \u0026#34;A tool for calculating the reward of gsm8k. (1.0 if parsed answer is correct, 0.0 if parsed answer is incorrect or not correctly parsed)\u0026#34; parameters: type: \u0026#34;object\u0026#34; properties: answer: type: \u0026#34;string\u0026#34; description: \u0026#34;The model\u0026#39;s answer to the GSM8K math problem, must be a digits\u0026#34; required: [\u0026#34;answer\u0026#34;] tool_list, tool_schemas：通过 initialize_tools_from_config(tool_config_path) 函数从配置文件中解析并创建工具实例。\ntool_parser：通过设置类似 actor_rollout_ref.rollout.multi_turn.format=hermes这样的参数， 可以获取对应的 tool_parser；比如 HermesToolParser 就是提取 \u0026lt;tool_call\u0026gt;\u0026lt;/tool_call\u0026gt; 之间的内容，返回对应的 function_call（function_name 和 function_arguments）, 还有除开 tool_call 内容以外的 content。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @classmethod def init_class(cls, config, tokenizer): if cls._class_initialized: return cls._class_initialized = True print(\u0026#34;Performing class-level ToolAgentLoop initialization\u0026#34;) # Initialize tools from config file cls.tokenizer = tokenizer cls.max_user_turns = config.actor_rollout_ref.rollout.multi_turn.max_user_turns cls.max_assistant_turns = config.actor_rollout_ref.rollout.multi_turn.max_assistant_turns cls.max_parallel_calls = config.actor_rollout_ref.rollout.multi_turn.max_parallel_calls cls.max_tool_response_length = config.actor_rollout_ref.rollout.multi_turn.max_tool_response_length cls.tool_response_truncate_side = config.actor_rollout_ref.rollout.multi_turn.tool_response_truncate_side tool_config_path = config.actor_rollout_ref.rollout.multi_turn.tool_config_path tool_list = initialize_tools_from_config(tool_config_path) if tool_config_path else [] cls.tools = {tool.name: tool for tool in tool_list} cls.tool_schemas = [tool.tool_schema.model_dump(exclude_unset=True, exclude_none=True) for tool in tool_list] cls.tool_parser = cls.get_tool_parser(config.actor_rollout_ref.rollout.multi_turn.format) print(f\u0026#34;Initialized tools: {cls.tools}\u0026#34;) cls.prompt_length = config.actor_rollout_ref.rollout.prompt_length cls.response_length = config.actor_rollout_ref.rollout.response_length cls.system_prompt = tokenizer.apply_chat_template([{}], add_generation_prompt=False, tokenize=True) run\n和 single_turn_agent_loop 一样，对 prompts apply_chat_template； 初始化 user_turns, assistant_turns，进入 multi-turn 的 loop 循环，直到退出: 向 server_manager 发送 prompt_ids，得到对应的 response_ids；将本轮返回的 response_ids append 到 prompt_ids 中，准备作为下一轮的输入，并且 assistant_turns += 1 处理边界条件，比如 prompts 过长，没有 tool call 了，或者超出了 max turns； 异步执行 _call_tool：从 response 中 extract 出 Function Call，接着 tool.execute(instance_id, tool_args) 获得相应的 tool_response, 然后截断返回即可。具体的 _call_tool 会在后文分析。 tool_responses 随后 apply_chat_template 得到 tool_response_ids，同样 append 到prompt_ids 内，然后 user_turns += 1，进入下一轮循环； 退出 tool agent loop 循环后，构造 AgentLoopOutput 注意 num_turns=user_turns+assistant_turns +1，因为 prompt 也算一次 user turn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 @rollout_trace_op async def run(self, messages: list[dict[str, Any]], sampling_params: dict[str, Any]) -\u0026gt; AgentLoopOutput: metrics = {} request_id = uuid4().hex prompt_ids = await self.loop.run_in_executor( None, lambda: self.tokenizer.apply_chat_template( messages, tools=self.tool_schemas, add_generation_prompt=True, tokenize=True ), ) response_mask = [] user_turns, assistant_turns = 0, 0 while True: with simple_timer(\u0026#34;generate_sequences\u0026#34;, metrics): response_ids = await self.server_manager.generate( request_id=request_id, prompt_ids=prompt_ids, sampling_params=sampling_params ) prompt_ids += response_ids response_mask += [1] * len(response_ids) assistant_turns += 1 # reach max response length if len(response_mask) \u0026gt;= self.response_length: break # reach max assistant turns if self.max_assistant_turns and assistant_turns \u0026gt;= self.max_assistant_turns: break # reach max user turns if self.max_user_turns and user_turns \u0026gt;= self.max_user_turns: break # no tool calls tool_calls = await self.tool_parser.extract_tool_calls(response_ids) if not tool_calls: break # call tools tasks = [] for tool_call in tool_calls[: self.max_parallel_calls]: tasks.append(self._call_tool(tool_call)) with simple_timer(\u0026#34;tool_calls\u0026#34;, metrics): tool_responses = await asyncio.gather(*tasks) if any(isinstance(item, Exception) for item in tool_responses): break # append tool_response_ids tool_response_ids = await self.loop.run_in_executor( None, lambda messages=tool_responses: self.tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True ), ) tool_response_ids = tool_response_ids[len(self.system_prompt) :] # NOTE: last turn should not be user turn, or the EOS token reward # can\u0026#39;t be propagated to previous token in GAE. if len(response_mask) + len(tool_response_ids) \u0026gt;= self.response_length: break prompt_ids += tool_response_ids response_mask += [0] * len(tool_response_ids) user_turns += 1 response_ids = prompt_ids[-len(response_mask) :] prompt_ids = prompt_ids[: len(prompt_ids) - len(response_mask)] output = AgentLoopOutput( prompt_ids=prompt_ids, response_ids=response_ids[: self.response_length], response_mask=response_mask[: self.response_length], num_turns=user_turns + assistant_turns + 1, metrics=metrics, ) return output call_tool\n基于 tool list 内的 tool 来调用工具，例如前面 config 中配置的 calc_gsm8k_reward，从 tool parser 得到 arguments 就可以代入运算得到相应的tool_response。如果 tool 调用成功，则会释放 tool 占用的资源,，最后tool_response根据 tool_response_truncate_side 来做相应的截断。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 async def _call_tool(self, tool_call: FunctionCall) -\u0026gt; dict[str, str]: \u0026#34;\u0026#34;\u0026#34;Call tool and return tool response.\u0026#34;\u0026#34;\u0026#34; tool, instance_id = None, None try: # TODO: append malformed tool_call to the prompt: invalid function name or arguments tool_name = tool_call.name tool_args = json.loads(tool_call.arguments) tool = self.tools[tool_name] instance_id = await tool.create() tool_response, _, _ = await tool.execute(instance_id, tool_args) except Exception as e: logger.exception(f\u0026#34;Error when executing tool: {e}\u0026#34;) return e finally: if tool and instance_id: await tool.release(instance_id) if len(tool_response) \u0026gt; self.max_tool_response_length: if self.tool_response_truncate_side == \u0026#34;left\u0026#34;: tool_response = tool_response[: self.max_tool_response_length] + \u0026#34;...(truncated)\u0026#34; elif self.tool_response_truncate_side == \u0026#34;right\u0026#34;: tool_response = \u0026#34;(truncated)...\u0026#34; + tool_response[-self.max_tool_response_length :] else: length = self.max_tool_response_length // 2 tool_response = tool_response[:length] + \u0026#34;...(truncated)...\u0026#34; + tool_response[-length:] return { \u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, \u0026#34;content\u0026#34;: tool_response, } ","permalink":"https://pillumina.github.io/posts/aiinfra/09-verl-agentloop/","summary":"\u003cp\u003e最近 RL sys 圈子的吴锡斌老师在 verl 上设计了将 rollout 与 tool 调用解耦的 AgentLoop，实现了自由灵活的 mutli-turn RL。在每个 AgentLoop 内部，rollout engine 只对外提供一个 token-in-token-out 的接口，而 tool 调用则通过 \u003ccode\u003eToolAgentLoop\u003c/code\u003e 来实现。我个人比较喜欢这样解耦的设计，同时，AgentLoop 的代码结构也比较清晰。我个人学习了一次整个代码后，觉着 AgentLoop 的设计甚是不错，但是 \u003ccode\u003eActorRolloutRefWorker\u003c/code\u003e 的历史包袱还是很重。\u003c/p\u003e\n\u003cp\u003e本文简单分析了 agent loop 的源码，并给出了一些自己的看法。\u003c/p\u003e\n\u003cp\u003e如果我们把整个 \u003ccode\u003eActorRolloutRefWorker\u003c/code\u003e 当做一个 \u003ccode\u003esgl.Engine\u003c/code\u003e 的话，AgentLoop 里面包装的两层 \u003ccode\u003eAsyncSGLangServer\u003c/code\u003e 和 \u003ccode\u003eAsyncLLMServerManager\u003c/code\u003e。\u003ccode\u003eAsyncSGLangServer\u003c/code\u003e 相当于在 \u003ccode\u003esgl.Engine\u003c/code\u003e 上包装了 \u003ccode\u003efastapi\u003c/code\u003e 成了 server，而 \u003ccode\u003eAsyncLLMServerManager\u003c/code\u003e 是在 server 上包了一层 router 做 load balance，相当于 sglang 的 router。这两层设计都是合理的，主要麻烦的是 \u003ccode\u003eActorRolloutRefWorker\u003c/code\u003e，层层调用，最后一共经过 7 个 class 才调到 \u003ccode\u003esgl.Engine\u003c/code\u003e，最近 verl 团队也在致力于对这块 worker class 的重构，敬请期待。最后，\u003ccode\u003eAgentLoopManager\u003c/code\u003e，\u003ccode\u003eAgentLoopWorker\u003c/code\u003e 和 \u003ccode\u003eAgentLoop\u003c/code\u003e 这三层，我觉得 \u003ccode\u003eAgentLoopWorker\u003c/code\u003e 可能未必有必要，其他两层挺合理的。\u003c/p\u003e","title":"[VeRL] AgentLoop源码走读"},{"content":" VeRL框架的参数众多，基于当前（2025.8.5）主线分支整理，附带了相关的理解，一些描述不一定完全正确，供学习参考。\nBatch Size 参数名称 详细解释 data.train_batch_size 作用：定义了单次训练发送给 Rollout Engine 的样本数量，也即这是在每个 PPO 迭代开始时，从训练数据集中采样的提示 （Prompt）数量。\n详细解释：这个值是 RL 训练中的基本样本数量。例如，设置为 1024 意味着在一次迭代中会：\n1. 从数据集中随机抽取 1024 个 prompt。\n2. 将这 1024 个 prompt 发送给当前的 Rollout Engine 中，从而得到 1024 组完整的 trajectories（prompt, response）。\n3. 接下来，这 1024 个 trajectories 进行经验计算（make experience），后续用于 Actor 和 Critic 模型的更新。\n影响与权衡：影响总共训练的样本量。 data.val_batch_size （Deprecated) 作用：在 Validation 阶段使用的批次大小。\n详细解释：这与 train_batch_size 类似，但仅用于评估模型性能，不参与训练。如果设置为 null，会使用验证集的大小作为默认值。Note: 已经deprecated，推荐设置为 null。此时，整个 validation dataset 一次性发给 SGLang engines，自行进行内存管理。 actor_rollout_ref.actor.ppo_mini_batch_size critic.ppo_mini_batch_size 作用：定义了 PPO 训练更新中的 mini-batch 大小。\n详细解释：data.train_batch_size 收集到的全部经验数据将被分割成多个 mini-batch，每块的大小就是 ppo_mini_batch_size。模型每处理完一个 mini-batch，才会进行一次参数更新。\n例如，如果 train_batch_size = 1024，ppo_mini_batch_size = 256，那么在一个 PPO Epoch 中，模型会进行 1024 / 256 = 4 次参数更新。\n影响与权衡：增大 mini-batch，单次更新的梯度更稳定，但更新频率更低，更新次数减少。 actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu critic.ppo_micro_batch_size_per_gpu 作用：定义了在单个 GPU 上进行一次 forward/backward 的数据大小。\n详细解释：这是实现梯度累积的核心参数。mini-batch 会被再次切分为若干个 micro-batch。例如，在单卡上，ppo_mini_batch_size = 256，ppo_micro_batch_size_per_gpu = 32，那么梯度累积的步数就是 256 / 32 = 8。这意味着模型会运行 8 次 forward 得到 loss，然后 backward 的到 gradient。每次处理 32 个样本，直到累积完整个 mini-batch 计算出的梯度。此时，使用累积的总梯度，对模型参数进行一次更新（optimizer.step()）。这个值必须根据显存大小来严格调整，是防止 OOM 的关键。\n影响与权衡：增大此值，减少了梯度累积的次数，可以提高训练的吞吐量，增大显存消耗。 actor_rollout_ref.actor.ppo_micro_batch_size critic.ppo_micro_batch_size（Deprecated) 作用：已弃用，被 per_gpu 版本取代，因为它能更好地适应分布式训练环境。 Dynamic Batch Size 当样本长度差异很大时，按样本数量划分批次可能导致不同批次的计算量极不均衡，而基于 token 总数来控制 batch size 是一种平衡每个 batch 训练时间的方案。\n参数名称 详细解释 actor_rollout_ref.actor.ppo_max_token_len_per_gpu critic.ppo_max_token_len_per_gpu 作用：定义了一个 PPO micro batch size 中，单个 GPU 能处理的最大 Token 总数。\n详细解释：这是 ppo_micro_batch_size_per_gpu 的替代方案，与 use_dynamic_bsz 配合使用。系统会自动打包样本，直到总 Token 量（prompt_len + response_len）接近这个阈值，形成一个动态的 micro batch size，从而稳定计算效率；无论长短样本，每个微批次的计算量都相对恒定。\n例如，设置为 actor_rollout_ref.actor.ppo_max_token_len_per_gpu = 16384，系统可能会打包 16 个长度为 1024 的样本（16 * 1024 = 16384）或者 64个长度为 256 的样本（64 * 256 = 16384）。\n影响与权衡：通常比固定样本数的微批次效率更高，能更好地利用计算资源，减少 GPU 不稳定性。通常设置为 n * ({data.max_prompt_length} + {data.max_response_length}) reward_model.forward_max_token_len_per_gpu critic.forward_max_token_len_per_gpu actor_rollout_ref.ref.log_prob_max_token_len_per_gpu 作用：只进行 forward 计算的 Model 的一个 micro-batch 的 token 最大数量。\n详细解释：一些模型（Reward Model, Critic 求 value, Reference Model 求 log probs）在 make experience 阶段只有 forward 计算，此时 rollout engine 已经 offload 了，而 training engine 还没启动，显存占用是很少的。因此，可以为它们设置一个更大的 batch size 以加速计算。这些参数同样是 use_dynamic_bsz 的一部分，用于优化这些特定任务的执行效率。 critic.forward_micro_batch_size_per_gpu reward_model.micro_batch_size_per_gpu actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu 作用：同样为只进行 forward 计算的 model 设置 micro-batch size。\n详细解释：同上一行参数。 actor_rollout_ref.actor.use_dynamic_bsz critic.use_dynamic_bsz reward_model.use_dynamic_bsz 作用：是否启用 Dynamic Batch Size。\n详细解释：当此项为 True 时，系统会忽略基于样本数的 micro_batch_size_per_gpu 参数，转而使用基于 Token 数的 max_token_len_per_gpu 参数来构建 batch。 trainer.balance_batch 作用：是否在分布式训练的各个 dp rank 间平衡 batch size。\n详细解释：在 single controller 上将 data 重新排序使得每个 dp rank 获得相似数目的 token。 Rollout Sampling Parameters 参数名称 作用与解释 actor_rollout_ref.rollout.temperature temperature 值越高，概率分布越平滑，生成结果更多样、更随机；值越低，分布越尖锐，生成结果更倾向于高概率词元，更确定、更保守。temperature=0 通常等同于 Greedy Decoding。 actor_rollout_ref.rollout.top_k 在每一步生成时，只考虑概率最高的 K 个 token 进行采样。例如，top_k=50 表示只从概率前 50 的 token 中选择。\n- 禁用时：在 Hugging Face 中设置为 0 或 None，在 SGLang 中设置为 -1（此时从整个词汇表采样）。 actor_rollout_ref.rollout.top_p 从概率最高的 token 开始累加，直到它们的总概率达到 P，然后从这个 nucleus token 集合中进行采样。是一种动态选择采样范围的方法。top_p=1.0 表示不限制。 actor_rollout_ref.rollout.use_fire_sampling 是否使用 Fire Sampling，来自字节的论文。 actor_rollout_ref.rollout.n 为每个 prompt 生成的 response 数量，也即 GRPO 中的 group size。 actor_rollout_ref.rollout.ignore_eos 是否忽略 EOS (End-of-Sentence) 标记。如果为 True，即使模型生成了 EOS 标记，也会继续生成直到达到 max_response_length。 Performance and Resource Management 参数名称 作用与解释 actor_rollout_ref.rollout.prompt_length 最大的 prompt 长度，过长则被截断。 actor_rollout_ref.rollout.response_length 最大的 response 长度，到达最大长度时 SGLang engine 会直接返回。 actor_rollout_ref.rollout.dtype 模型数据类型。例如 bfloat16, float16，需要与训练阶段的模型类型对齐，否则更新模型参数的时候还需要做量化。 actor_rollout_ref.rollout.gpu_memory_utilization SGLang 中模型参数和 KV Cache占显存的比例，如果使用 0.4.8.post1 以上版本 SGLang，则可以设置到 0.85，使用以下版本的 SGLang 则需要设置到 0.5 左右。 actor_rollout_ref.rollout.free_cache_engine Rollout 后是否释放引擎缓存；SGLang 中启用此选项将触发 flush_cache() 操作：清空 kv cache pool，将所有 slots 标记为可用。通过释放 KV Cache 的逻辑占用，但是不释放物理显存。为什么需要 flush kv cache 可以参考此处。 actor_rollout_ref.rollout.load_format 模型权重加载模式。例如 dummy_dtensor（随机初始化权重，用于快速调试）、hf、safetensors（推荐，安全且高效）。 actor_rollout_ref.rollout.tensor_model_parallel_size (TP_SIZE) 张量并行大小，表示用多少个 GPU 来共同运行一个 SGLang engine。例如，TP_SIZE=4 表示将一个大模型的权重切成 4 份，由 4 个 GPU 协同完成推理。 actor_rollout_ref.rollout.max_model_len 模型能处理的最大总长度（prompt + response）；如果未设置，通常由模型配置决定。 actor_rollout_ref.rollout.max_num_seqs 引擎能同时处理的最大请求量，或者说同时推理的最多 prompts 数量。 actor_rollout_ref.rollout.enable_chunked_prefill 是否启用 Chunked Prefill，对于非常长的 Prompt，可以将其分块处理，减少显存峰值，但是降低吞吐量。 actor_rollout_ref.rollout.disable_log_stats 是否禁用推理引擎的统计日志，以减少控制台输出。 SGLang 配置 参数名称 作用与解释 actor_rollout_ref.rollout.engine_kwargs.sglang.attention_backend SGLang 使用的注意力后端。可以选择如 flashinfer, triton, flashmla, null 几种实现，以适应自身显卡。 multi-turn tool calling 这部分参数主要用于需要多轮交互的场景，如工具调用、连续对话等，由 SGLang Engine 支持。\n参数名称 作用与解释 actor_rollout_ref.rollout.multi_turn.enable 是否启用多轮对话模式。 actor_rollout_ref.rollout.multi_turn.max_turns 最多进行 tool calling 的轮次，null 时会默认设置成 max_model_len // 3 来避免无限对话。 actor_rollout_ref.rollout.multi_turn.tool_config_path 工具配置文件路径，定义模型可以调用的外部工具。 actor_rollout_ref.rollout.multi_turn.completion_callback 自定义 callback function，在每轮生成后可以执行自定义逻辑。 actor_rollout_ref.rollout.multi_turn.use_inference_chat_template 是否使用模型在 inference 阶段的 chat template。True 表示遵循 inference 阶段的模板格式。False 表示使用预训练中的模板，可能包含额外思考过程的完整 Token 序列。对于任何模型，一定要保证在 post training 和后续 inference 进行测试的阶段采用一致的模板。 actor_rollout_ref.rollout.multi_turn.enable_tokenization_sanity_check 是否进行 tokenization 安全性检查，检查逐轮 tokenize 的结果与一次 tokenize 整个 chat history 的结果一致。 验证阶段配置 参数名称 作用与解释 actor_rollout_ref.rollout.val_kwargs.* 验证阶段的 sampling parameters，这允许我们在 post training 和 validation 时使用不同的 sampling parameters。例如，验证时通常设置 temperature=0 和 do_sample=False 来进行贪心解码，以获得更稳定的评估结果。 Dataset 参数名称 作用与解释 data.tokenizer Tokenizer 的类或路径。如果为 null，将从模型中自动推断。 data.use_shm 是否使用共享内存（shared memory）来加载数据。 data.train_files 训练集 parquet 文件。可以是列表或单个文件；路径可以是本地路径或 HDFS 路径。 data.val_files 验证集 parquet 文件。可以是列表或单个文件。 data.prompt_key 数据集中 prompt 的字段。默认为 prompt。 data.reward_fn_key 用于选择奖励函数（如果每个样本使用不同奖励函数）的字段。 data.max_prompt_length 最大提示长度。所有提示将向左填充到此长度。 data.return_raw_input_ids 是否返回未添加聊天模板的原始 input_ids;当 reward model 的 chat template 与 policy model 不同时使用。 data.return_raw_chat 是否返回未应用聊天模板的原始 response。 data.return_full_prompt 是否返回带有聊天模板的完整 prompt。 data.shuffle 是否在 DataLoader 中打乱数据。 data.validation_shuffle 是否打乱验证集。 data.filter_overlong_prompts 是否过滤超长的 prompt。 data.filter_overlong_prompts_workers 过滤超长 prompt 的工作进程数。对于大型数据集，使用多进程加速。默认为 1。 data.truncation 如果 input_ids 或 prompt 超过最大长度，则进行截断。 data.image_key 多模态数据集中表示图像的字段。默认为 images。 data.video_key 多模态数据集中表示视频的字段。 data.trust_remote_code 是否信任本地的的 huggingface cache；注意，这个 remote 是相对 huggingface 而言的，所以这个参数考虑的是“是否信任本地”。 data.custom_cls.path 包含自定义数据集类的文件路径。如果未指定，将使用预实现的默认数据集。 data.custom_cls.name 指定文件中的数据集类名。 Actor, Rollout \u0026amp; Reference Worker 配置 Critic 和 Actor 的参数是非常一致的，不再赘述。\n参数名称 描述 actor_rollout_ref.hybrid_engine 目前只支持 hybird engine，将 actor 和 rollout 模型放在同一资源组上。 actor_rollout_ref.model.path Huggingface 模型路径。可以是本地路径或 HDFS 路径。 actor_rollout_ref.model.use_shm 是否使用共享内存（SHM）来加速模型权重的加载。 actor_rollout_ref.model.external_lib 用于注册 Huggingface 模型/分词器的额外 Python 包。 actor_rollout_ref.model.override_config 用于覆盖模型原始配置，主要用于 dropout。 actor_rollout_ref.model.enable_gradient_checkpointing actor 训练过程是否重算梯度，以时间换空间。 actor_rollout_ref.model.enable_activation_offload actor 训练是否将 activation offload 到 CPU。 actor_rollout_ref.model.use_remove_padding 训练期间是否移除输入中的 padding元。 actor_rollout_ref.model.use_liger 是否使用 Liger kernel 进行线性层融合。 actor_rollout_ref.model.use_fused_kernels 是否使用自定义 fused kernel（如 FlashAttention, fused MLP）。 actor_rollout_ref.model.fused_kernel_options.impl_backend 融合核的实现后端，triton 或 torch。需要和 use_fused_kernels 配合使用 actor_rollout_ref.model.trust_remote_code 是否信任本地的的 huggingface cache；注意，这个 remote 是相对 huggingface 而言的，所以这个参数考虑的是“是否信任本地”。 actor_rollout_ref.actor.strategy 训练 backend fsdp, fsdp2 或 megatron。 actor_rollout_ref.actor.grad_clip Actor 更新的梯度裁剪。 actor_rollout_ref.actor.clip_ratio PPO 裁剪比率。 actor_rollout_ref.actor.clip_ratio_low 非对称裁剪的下界（用于 dual-clip PPO）。 actor_rollout_ref.actor.clip_ratio_high 非对称裁剪的上界（用于 dual-clip PPO）。 actor_rollout_ref.actor.clip_ratio_c Dual-clip PPO 中的常数 C；当优势 \u0026lt; -C 时进行裁剪。 actor_rollout_ref.actor.loss_agg_mode 损失聚合模式：token-mean, seq-mean-token-sum, 或 seq-mean-token-mean。 actor_rollout_ref.actor.entropy_coeff PPO 损失中的熵正则化系数。 actor_rollout_ref.actor.use_kl_loss 是否使用 KL 损失代替 KL 奖励惩罚。对于 GRPO 为 True。 actor_rollout_ref.actor.use_torch_compile 是否使用 torch.compile()。 actor_rollout_ref.actor.kl_loss_coef 启用 use_kl_loss 时的 KL 损失系数，用于 GRPO。 actor_rollout_ref.actor.kl_loss_type KL 散度损失的类型。选项：kl, abs, mse, low_var_kl, full。 actor_rollout_ref.actor.ppo_epochs PPO 轮数。 actor_rollout_ref.actor.shuffle 打乱训练数据。 actor_rollout_ref.actor.ulysses_sequence_parallel_size Ulysses 类的 sequence parallel 大小。 actor_rollout_ref.actor.entropy_from_logits_with_chunking 通过分块计算熵以减少显存峰值。 actor_rollout_ref.actor.entropy_checkpointing 是否将 entropy 通过 checkpoint 存下来。 actor_rollout_ref.actor.checkpoint.save_contents 保存的检查点中包含的内容。 actor_rollout_ref.actor.checkpoint.load_contents 从检查点加载时指定的内容。 actor_rollout_ref.actor.optim.lr 学习率。 actor_rollout_ref.actor.optim.lr_warmup_steps 预热步数；负值则由 lr_warmup_steps_ratio 决定。 actor_rollout_ref.actor.optim.lr_warmup_steps_ratio 预热步数比例（当 lr_warmup_steps 为负时使用）。 actor_rollout_ref.actor.optim.min_lr_ratio 余弦调度器的最小学习率比例。 actor_rollout_ref.actor.optim.num_cycles 学习率调度中的余弦周期数。 actor_rollout_ref.actor.optim.warmup_style 学习率预热风格：constant 或 cosine。 actor_rollout_ref.actor.optim.total_training_steps 总训练步数。 actor_rollout_ref.actor.optim.weight_decay 权重衰减系数，控制训练过程中对权重施加的 L2 正则化的强度。 actor_rollout_ref.actor.fsdp_config.wrap_policy.min_num_params 触发 FSDP 包装一个层的最小参数数量。 actor_rollout_ref.actor.fsdp_config.param_offload 是否将模型参数卸载到 CPU（以速度换内存）。 actor_rollout_ref.actor.fsdp_config.optimizer_offload 是否将优化器状态卸载到 CPU。 actor_rollout_ref.actor.fsdp_config.offload_policy 仅用于 FSDP2：训练期间卸载参数/梯度/优化器。 actor_rollout_ref.actor.fsdp_config.reshard_after_forward 仅用于 FSDP2：前向传播后重新分片以减少内存占用。 actor_rollout_ref.actor.fsdp_config.fsdp_size 每个 FSDP 分片组中的 GPU 数量；-1 表示自动。 actor_rollout_ref.actor.fsdp_config.forward_prefetch 仅用于 FSDP1：在前向计算完成前预取下一次前向传播的 all-gather。 actor_rollout_ref.actor.profiler.discrete True 表示每个任务有自己的数据库，False 表示所有任务共享一个。 actor_rollout_ref.actor.profiler.all_ranks 是否对所有 rank 进行性能分析。 actor_rollout_ref.actor.profiler.ranks 将被分析的 rank。null 或 [0,1,\u0026hellip;]。 actor_rollout_ref.ref.strategy Reference 模型的 FSDP 配置，与 actor 相同。 actor_rollout_ref.ref.fsdp_config.param_offload FSDP 中是否卸载参数。 actor_rollout_ref.ref.fsdp_config.reshard_after_forward 仅用于 FSDP2：是否在模型前向传播后重新分片以节省内存。 actor_rollout_ref.ref.fsdp_config.forward_prefetch 仅用于 FSDP1：在前向计算完成前预取下一次前向传播的 all-gather。 actor_rollout_ref.ref.fsdp_config.wrap_policy.min_num_params FSDP 包装模块中的最小参数量。 actor_rollout_ref.ref.profiler.discrete True 表示每个任务有自己的数据库，False 表示所有任务共享一个。 actor_rollout_ref.ref.profiler.all_ranks 是否对所有 rank 进行性能分析。 actor_rollout_ref.ref.profiler.ranks 将被分析的 rank。null 或 [0,1,\u0026hellip;]。 Reward Model 参数名称 描述 reward_model.enable 是否启用奖励模型。如果为 False，则仅使用用户定义的奖励函数计算奖励。 reward_model.strategy FSDP 策略：fsdp 或 fsdp2或megatron。 reward_model.model.input_tokenizer 输入分词器。如果奖励模型的聊天模板与策略不一致，则需要此项。 reward_model.model.path RM 的 HDFS 路径或本地路径。仅支持 AutoModelForSequenceClassification。 reward_model.model.use_shm 是否使用共享内存加载模型。 reward_model.model.external_lib 外部模型实现（可选）。 reward_model.model.use_remove_padding 使用移除填充优化（节省计算）。 reward_model.model.use_fused_kernels 是否使用融合的奖励核以加速。 reward_model.model.trust_remote_code 是否允许加载远程代码模型，默认为 False。 reward_model.model.fsdp_config.wrap_policy.min_num_params 触发 FSDP 包装的最小参数数量。 reward_model.model.fsdp_config.param_offload 是否将模型参数卸载到 CPU。 reward_model.model.fsdp_config.reshard_after_forward 仅用于 FSDP2：前向传播后重新分片以减少内存占用。 reward_model.model.fsdp_config.fsdp_size 每个 FSDP 分片组中的 GPU 数量；-1 表示自动。 reward_model.model.fsdp_config.forward_prefetch 仅用于 FSDP1：在前向计算完成前预取下一次前向传播的 all-gather。 reward_model.reward_manager 定义计算基于规则的奖励和处理不同奖励源的机制。 reward_model.launch_reward_fn_async 是否在 log_prob 期间异步启动自定义奖励函数。 reward_model.sandbox_fusion.url 用于远程 reward 函数的 URL。 reward_model.sandbox_fusion.max_concurrent 允许到沙箱的最大并发请求数。 reward_model.profiler.discrete True 表示每个任务有自己的数据库，False 表示所有任务共享一个。 Custom Reward Function 参数名称 描述 custom_reward_function.path 包含自定义奖励函数的文件路径。 custom_reward_function.name 指定文件中的奖励函数名称。默认为 compute_score。 Algorithm 参数名称 描述 algorithm.gamma 未来奖励的折扣因子。 algorithm.lam GAE 估计器中偏差和方差的权衡。 algorithm.adv_estimator 优势估计器类型：gae, grpo, reinforce_plus_plus 等。 algorithm.norm_adv_by_std_in_grpo 是否在 GRPO 中按标准差归一化优势。 algorithm.use_kl_in_reward 是否在奖励中启用 KL 惩罚。 algorithm.kl_penalty 如何估计 KL 散度：kl, abs, mse, low_var_kl, 或 full。 algorithm.kl_ctrl.type KL 控制类型：fixed 或 adaptive。 algorithm.kl_ctrl.kl_coef KL 惩罚的初始系数。 algorithm.kl_ctrl.horizon 自适应控制器的 horizon 值（如果启用）。 algorithm.kl_ctrl.target_kl 目标 KL 散度（用于自适应控制器）。 algorithm.use_pf_ppo 是否启用偏好反馈 PPO。 algorithm.pf_ppo.reweight_method 样本重加权方法：pow, max_min, 或 max_random。 algorithm.pf_ppo.weight_pow pow 方法中用于权重缩放的幂。 Trainer 参数名称 描述 trainer.balance_batch 是否在分布式工作节点间平衡批次大小。 trainer.total_epochs 训练的总轮数。 trainer.total_training_steps 总训练步数（可显式设置或从轮数派生）。 trainer.profile_steps 将被分析的步骤。null 表示不进行分析。 trainer.controller_nsight_options.trace 对于controller进程，选择要追踪的 API（比如cuda，nvtx，cublas，etc）。 trainer.controller_nsight_options.cuda-memory-usage 对于controller进程，是否profile CUDA 内存使用情况。必须是字符串 \u0026quot;true\u0026quot; 或 \u0026quot;false\u0026quot;。 trainer.controller_nsight_options.cuda-graph-trace 对于controller进程，是否将CUDA graphs 将被作为一个整体进行追踪。 trainer.worker_nsight_options.trace 对于worker进程，选择要追踪的 API。 trainer.worker_nsight_options.cuda-memory-usage 对于worker进程，是否profile CUDA 内存使用情况。必须是字符串 \u0026quot;true\u0026quot; 或 \u0026quot;false\u0026quot;。 trainer.worker_nsight_options.cuda-graph-trace 对于worker进程，是否CUDA graphs 将被作为一个整体进行追踪。 trainer.worker_nsight_options.capture-range 仅在 torch.cuda.profiler.start 和 stop 范围内进行分析。默认值为cudaProfilerApi，不要更改此配置。 trainer.worker_nsight_options.capture-range-end 指定捕获范围结束时的期望行为。 trainer.worker_nsight_options.kill 向目标应用程序的进程组发送信号。我们让程序自行退出。 trainer.project_name 用于实验跟踪（如 wandb）的项目名称。 trainer.experiment_name 用于在跟踪工具中识别运行的实验名称。 trainer.logger 使用的日志后端：console, wandb 等。 trainer.log_val_generations 验证期间要记录的生成数量。 trainer.rollout_data_dir 用于记录 rollout 数据的目录；如果为 null 则不转储。 trainer.validation_data_dir 用于记录验证数据的目录；如果为 null 则不转储。 trainer.nnodes 训练中使用的节点数。 trainer.n_gpus_per_node 每个节点的 GPU 数量。 trainer.save_freq 模型检查点的保存频率（按迭代次数）。 trainer.resume_mode 恢复模式：auto, disable, 或 resume_path。 trainer.resume_from_path 从该路径恢复训练（仅当 resume_mode 为 resume_path 时使用）。 trainer.val_before_train 是否在训练开始前运行验证。 trainer.val_only 是否只运行验证。 trainer.test_freq 验证频率（以训练迭代次数计）。 trainer.critic_warmup 在更新策略之前预热 critic 的迭代次数。 trainer.default_hdfs_dir 用于保存检查点的默认分布式文件系统路径。 trainer.del_local_ckpt_after_load 加载后是否删除本地检查点。 trainer.default_local_dir 用于保存检查点的默认本地目录。 trainer.max_actor_ckpt_to_keep 保留的 actor 检查点的最大数量。 trainer.max_critic_ckpt_to_keep 保留的 critic 检查点的最大数量。 trainer.ray_wait_register_center_timeout Ray worker 等待注册的超时时间（秒）。 trainer.device 运行训练的设备（如 cuda, cpu）。 Ray Init 参数名称 描述 ray_init.num_cpus Ray 使用的 CPU 数量。使用 SLURM 时应使用固定数字而不是 null。 ray_init.timeline_json_file 保存 Ray 时间线 JSON 文件以进行性能分析的路径。 ","permalink":"https://pillumina.github.io/posts/aiinfra/05-verl-params/","summary":"\u003cblockquote\u003e\n\u003cp\u003eVeRL框架的参数众多，基于当前（2025.8.5）主线分支整理，附带了相关的理解，一些描述不一定完全正确，供学习参考。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"batch-size\"\u003eBatch Size\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e参数名称\u003c/th\u003e\n          \u003cth\u003e详细解释\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edata.train_batch_size\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e作用\u003c/strong\u003e：定义了单次训练发送给 Rollout Engine 的样本数量，也即这是在每个 PPO 迭代开始时，从训练数据集中采样的提示 （Prompt）数量。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e详细解释\u003c/strong\u003e：这个值是 RL 训练中的基本样本数量。例如，设置为 1024 意味着在一次迭代中会：\u003cbr\u003e1. 从数据集中随机抽取 1024 个 prompt。\u003cbr\u003e 2. 将这 1024 个 prompt 发送给当前的 Rollout Engine 中，从而得到 1024 组完整的 trajectories（prompt, response）。\u003cbr\u003e3. 接下来，这 1024 个 trajectories 进行经验计算（make experience），后续用于 Actor 和 Critic 模型的更新。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e影响与权衡\u003c/strong\u003e：影响总共训练的样本量。\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edata.val_batch_size\u003c/code\u003e （Deprecated)\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e作用\u003c/strong\u003e：在 Validation 阶段使用的批次大小。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e详细解释\u003c/strong\u003e：这与 \u003ccode\u003etrain_batch_size\u003c/code\u003e 类似，但仅用于评估模型性能，不参与训练。如果设置为 \u003ccode\u003enull\u003c/code\u003e，会使用验证集的大小作为默认值。Note: 已经deprecated，推荐设置为 null。此时，整个 validation dataset 一次性发给 SGLang engines，自行进行内存管理。\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eactor_rollout_ref.actor.ppo_mini_batch_size\u003c/code\u003e \u003cbr\u003e \u003ccode\u003ecritic.ppo_mini_batch_size\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e作用\u003c/strong\u003e：定义了 PPO 训练更新中的 mini-batch 大小。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e详细解释\u003c/strong\u003e：\u003ccode\u003edata.train_batch_size\u003c/code\u003e 收集到的全部经验数据将被分割成多个 mini-batch，每块的大小就是 \u003ccode\u003eppo_mini_batch_size\u003c/code\u003e。模型每处理完一个 mini-batch，才会进行一次参数更新。\u003cbr\u003e例如，如果 \u003ccode\u003etrain_batch_size = 1024\u003c/code\u003e，\u003ccode\u003eppo_mini_batch_size = 256\u003c/code\u003e，那么在一个 PPO Epoch 中，模型会进行 \u003ccode\u003e1024 / 256 = 4\u003c/code\u003e 次参数更新。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e影响与权衡\u003c/strong\u003e：增大 mini-batch，单次更新的梯度更稳定，但更新频率更低，更新次数减少。\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eactor_rollout_ref.actor.ppo_micro_batch_size_per_gpu\u003c/code\u003e \u003cbr\u003e \u003ccode\u003ecritic.ppo_micro_batch_size_per_gpu\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e作用\u003c/strong\u003e：定义了在单个 GPU 上进行一次 forward/backward 的数据大小。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e详细解释\u003c/strong\u003e：这是实现梯度累积的核心参数。mini-batch 会被再次切分为若干个 micro-batch。例如，在单卡上，\u003ccode\u003eppo_mini_batch_size = 256\u003c/code\u003e，\u003ccode\u003eppo_micro_batch_size_per_gpu = 32\u003c/code\u003e，那么梯度累积的步数就是 \u003ccode\u003e256 / 32 = 8\u003c/code\u003e。这意味着模型会运行 8 次 forward 得到 loss，然后 backward 的到 gradient。每次处理 32 个样本，直到累积完整个 mini-batch 计算出的梯度。此时，使用累积的总梯度，对模型参数进行一次更新（\u003ccode\u003eoptimizer.step()\u003c/code\u003e）。这个值必须根据显存大小来严格调整，是防止 OOM 的关键。\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e影响与权衡\u003c/strong\u003e：增大此值，减少了梯度累积的次数，可以提高训练的吞吐量，增大显存消耗。\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eactor_rollout_ref.actor.ppo_micro_batch_size\u003c/code\u003e \u003cbr\u003e \u003ccode\u003ecritic.ppo_micro_batch_size\u003c/code\u003e（Deprecated)\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e作用\u003c/strong\u003e：已弃用，被 \u003ccode\u003eper_gpu\u003c/code\u003e 版本取代，因为它能更好地适应分布式训练环境。\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dynamic-batch-size\"\u003eDynamic Batch Size\u003c/h2\u003e\n\u003cp\u003e当样本长度差异很大时，按样本数量划分批次可能导致不同批次的计算量极不均衡，而基于 token 总数来控制 batch size 是一种平衡每个 batch 训练时间的方案。\u003c/p\u003e","title":"[VeRL] 参数速览"},{"content":"本文档为开发者提供 SGLang 后端代码的代码梳理，按照一个请求从输入到最后输出的顺序进行讲解。下图简要介绍了这一流程：\n具体而言，请求的处理过程如下：\n用户启动 Server ，初始化 FastAPI App、TokenizerManager、DetokenizerManager 和 Scheduler，每个组件运行各自的无限事件循环（infinite event loop）。\n用户向 FastAPI Server 发送 /v1/chat/completions 请求，Server 通过 v1_chat_completions endpoint 将请求转发到 TokenizerManager。\nv1_chat_completions 函数将请求转换为 ChatCompletionRequest，再转换为 GenerateReqInput，并调用 TokenizerManager 的 generate_request 方法。\nTokenizerManager 对请求进行 tokenization，并以 Python 对象（pyobj）形式将其转发给 Scheduler，同时调用 TokenizerManager 的 _wait_one_response 方法。\nScheduler 在事件循环 event_loop_normal 中处理请求：\nScheduler 通过 recv_requests 接收请求，调用 process_input_requests 处理输入，通过 handle_generate_request 管理生成请求的逻辑，并将其加入 waiting_queue。 从 waiting_queue 中，Scheduler 使用 get_next_batch_to_run 为即将处理的请求创建 ScheduleBatch。 Scheduler 执行 run_batch 函数，将 ScheduleBatch 转换为 ModelWorkerBatch。 Scheduler 调用 TpModelWorker 的 forward_batch_generation，等待 logits_output 和 next_token_ids。 TpModelWorker 初始化 ForwardBatch，将其转发至 ModelRunner，并等待 logits_output。 ModelRunner 处理 ForwardBatch，调用 forward_extend 执行模型的前向计算（forward pass）。 模型通过 AttentionBackend 加速生成 logits，返回给 ModelRunner，进而返回给 TpModelWorker。 TpModelWorker 从 ModelRunner 接收 logits_output，调用 ModelRunner 的 sample 方法生成 next_token_ids，并将其发送回 Scheduler。 Scheduler 通过 process_batch_result 处理批次结果，使用 tree_cache.cache_finished_req(req) 缓存请求，并通过 check_finished 验证完成状态。对于未完成的请求，Scheduler 继续其事件循环，直到这个请求满足结束条件；对于已完成的请求，则转发到 Scheduler 的 stream_output。 在 stream_output 函数中，Scheduler 处理输出，将其包装成 BatchTokenIDOut，并发送给 DetokenizerManager。 DetokenizerManager 在其事件循环中接收 BatchTokenIDOut，处理后生成 BatchStrOut 并返回给 TokenizerManager。\nTokenizerManager 在其事件循环中接收结果，通过 handle_loop 处理并更新内部状态，然后将响应返回给Server 。\nFastAPI Server 最后封装完成的响应并将其返回给用户。\n💡 多模态请求处理：对于包含图像、视频等多模态内容的请求详细处理流程，请参考 SGLang 多模态请求生命周期：以 Qwen2.5-VL 为例的架构级深度解析。\n本文基于 SGLang v0.4.0 版本的代码编写。\n注意：本文档仍在编写中，以下部分将在后续加入：\n基于 Attention Backend 的 Radix Cache 管理。 get_next_batch_to_run：如何为每批次请求提取和写入 KV 缓存。 get_model_worker_batch。 write_req_to_token_pool_triton。 使用 CUDA Graphs 优化 Attention Backend。 重叠调度策略（overlap scheduling）。 启动 Server（launch Sever） SGLang 提供 SRT（SGLang Runtime）Server 用于服务 HTTP 请求以及一个不依赖 HTTP 协议的离线推理引擎。核心函数 launch_server 和 launch_engine 均定义在 server.py 中。其中，launch_engine 函数负责初始化核心 SRT Server 的组件。\n设置 logging、Server 参数、CUDA/NCCL 环境变量以及进程间通信端口，配置 model 和 tokenizer。 如果 dp_size \u0026gt; 1，运行 run_data_parallel_controller_process 以启动多个 data parallel replicas；否则，在每个 tp_rank 上，以子进程的方式初始化一个 Scheduler，处理来自 TokenizerManager 的请求，并且管理 KV Cache。 在 Engine 主进程中运行 TokenizerManager，并以子进程形式运行 DetokenizerManager：前者负责 tokenize requests 并发送给 Scheduler，后者将 Scheduler 返回的 token ids 转换为文本，发送回 Server 前端。需要注意的是，在多节点推理中（例如，在两个节点上使用 共计 16 张 H100 部署 Llama 3.1 405B），TokenizerManager 和 DetokenizerManager 仅在第一个节点运行。 如果指定了 chat template，则将其启动，随后等待 Scheduler 进程发出全部进程准备就绪的信号，并且 Scheduler 的配置信息。 需要注意的是，在 0.4.0 版本中，DataParallelController 用于在 data parallel replicas 之间以 round-robin （轮询）方式调度请求。未来，我们计划将其更换为 SGLang Router 来实现多个 replica 之间的调度。\n转发请求 (Forward Requests From Server) Server 使用 FastAPI 应用定义 API endpoint，通过 v1_chat_completions 将 /v1/chat/completions 请求转发至 TokenizerManager。\n从 raw_request 中解析 JSON 数据为 ChatCompletionRequest，将其转换为 GenerateReqInput，并通过 v1_chat_generate_request 配置 sampling_params。 调用 TokenizerManager 的 generate_request 方法并等待返回。得到返回后，根据 stream 参数处理流式（streaming）或非流式（non-streaming）响应。 对于流式响应，使用 generate_stream_resp 逐步处理 generate_request 的输出；对于非流式响应，等待异步返回的处理结果并通过 v1_chat_generate_response 转换为 ChatCompletionResponse。 TokenizerManager 生成请求（Generate Request In TokenizerManager） TokenizerManager 由Server 主进程中的 launch_server 初始化，用于对请求进行 tokenization。\nInitialization 设置 ZeroMQ 进行进程间通信，包括 TokenizerManager 与 DetokenizerManager 和 Scheduler 交互的 socket。 配置 server_args，启用 metrics，并初始化 model_config、tokenizer 以及多模态图像处理器的 placeholders。 generate_request 如果 TokenizerManager 的事件循环尚未初始化，则在此创建。 如果模型权重正在通过 update_weights_from_disk 或 update_weights_from_distributed 更新参数，则暂停处理。 验证请求类型是否与模型的 is_generation 设置匹配。 使用 normalize_batch_and_arguments 对请求进行归一化/标准化，以管理批处理、并行采样和默认参数。 对单个请求，通过 _tokenize_one_request 进行 tokenization，将请求发送至 Scheduler，并通过 _wait_one_response 等待响应。 对批处理请求，通过 _handle_batch_request 方法进行处理：tokenize 输入、管理并行采样、与 Scheduler 交互，并在流式和非流式模式下生成响应。 Scheduler 接收请求以及处理批次 (Scheduler Receive Requests and Process Batches) 这张图给出了 Scheduler 的概览：\nScheduler 作为 Server 的子进程运行，通过 run_scheduler_process 初始化，并通过 event_loop_normal 或 event_loop_overlap 执行无限的事件循环。\nInitialization 配置 ZeroMQ 用于与 TokenizerManager 的通信。 设置 server_args、port_args、model_config、sessions，并根据重叠调度（overlap scheduling）的方式初始化 TpModelWorker 或 TpModelWorkerClient。 初始化分词器和处理器，使用 ChunkCache 或 RadixCache 进行缓存管理，配置 SchedulePolicy。 配置 chunk prefill 参数，并为 constraint decoding 请求初始化 GrammarBackend。 Event Loop Scheduler 不断执行由 process_input_requests、get_next_batch_to_run、run_batch 和 process_batch_result 构成的无限事件循环。\nprocess_input_requests 遍历接收到的请求，识别其类型并将其分派给相应的处理函数。\nget_next_batch_to_run 尽可能将 last_batch 与 running_batch 合并，并通过 get_new_batch_prefill 优先处理 prefill batch。 如果没有 prefill batch，则更新用于 decode batch 的 running_batch，包括过滤请求、管理显存并调整解码参数。 run_batch 对于生成模型，使用 TpModelWorker 的 forward_batch_generation 生成新的 token，或在空闲状态中使用 forward_batch_idle，并将结果返回至 event_loop_normal。 对于嵌入或奖励模型，执行 forward_batch_embedding，并返回 embeddings。 process_batch_result 在执行完 run_batch 后，Scheduler 在 event_loop_normal 中处理批量结果：\nDecode 模式：处理输出，更新请求状态，处理标记和概率数据，管理内存，并记录统计信息。 Extend 模式：处理预填充结果，处理输入标记，并为进一步解码或嵌入做准备。 已完成的请求通过 cache_finished_req 缓存，并流式传输到 DetokenizerManager。未完成的请求会被更新，并循环回 get_next_batch_to_run 进行进一步处理，直至完成。 需要注意的是，LLM 推理按照计算特性不同，通常分为 Prefill 和 Decode 阶段。对于 Prefill 和 Decode 的概念，可以参考 HuggingFace 的这篇文章。而在 SGLang 中，大多数情况下使用的是 extend mode，而非 prefill mode。Prefill 模式为新请求初始化 KV-Cache，通常使用 Paged KV-Cache。而 Extend 模式则利用 Ragged Tensors 增量更新现有的 KV-Cache，效率更高，这使其非常适合 SGLang 面向的长序列或多轮对话请求。\nTpModelWorker 管理 forward pass 和 token sampling (TpModelWorker Manage Forward and Token Sampling) TpModelWorker 负责管理 ModelRunner 的 forward pass 和 token sampling 操作，从而完成由 Scheduler 调度的批次请求。\nInitialization 初始化 tokenizer、模型配置和 ModelRunner。 配置设备信息和 memory pool。 forward_batch_generation 创建 ForwardBatch，通过 ModelRunner 的 forward 计算 logits，并使用 ModelRunner 的 sample 采样得到下一个 token。 将 logits_output 和 next_token_ids 返回给 Scheduler，用于 Scheduler 的 process_batch_result。 forward_batch_embedding 创建一个 ForwardBatch，通过 ModelRunner 的 forward 获取 logits_output 和 embeddings。 embedding 请求不需要采样，因此跳过 ModelRunner 的 sample 过程，直接将 embeddings 返回给 Scheduler。 ModelRunner 管理模型执行 (ModelRunner Manages Model Execution) ModelRunner 初始化 AttentionBackend 并管理加载的模型，以执行 generation 和 embedding 任务的 forward pass。\nInitialization 初始化分布式环境，加载模型，启动 tensor parallel，并设置 memory pool 和 AttentionBackend。\nforward forward 函数根据 forward_mode 决定适当的前向模式来处理批次：\nforward_decode：初始化 forward metadata 并调用模型的 forward，传入 input IDs 和 position。 forward_extend：初始化 forward metadata 并调用模型的 forward 进行 generation 或 embedding 任务。 forward_idle：当前向模式为空闲时，管理空闲的前向传递。 Model 加载权重并执行前向传递 (Model Load Weights and Perform Forward) ModelRunner 的 self.model 是 Model class 的一个实例。所有 支持的模型 都可以在 python/sglang/srt/models 中找到。我们以 Qwen2ForCausalLM 为例。\nQwen2ForCausalLM 的结构如下：\nmodel：用于前向传递的权重。 embed_tokens：将 input_ids 转换为 embeddings。 lm_head：将 hidden states 映射回 vocabulary space。 logits_processor：处理 logits 以便进一步 sampling 或者 normalization。 pooler：用于提取 embeddings 或计算 rewards 的 pooling 机制。 forward Qwen2ForCausalLM 中的 forward 函数处理 input IDs，生成用于预测下一个 token 的 logits，或生成用于奖励/嵌入请求的 embeddings：\n使用 embed_tokens 将 input_ids 转换为 embeddings。将 embeddings 依次通过多个 Qwen2DecoderLayer 层完成 forward pass。 如果 get_embedding 为 True，则通过 pooler 返回 embeddings；否则，使用 logits_processor 计算 logits 并返回。 SGLang 对模型推理的加速主要来自于 forward_batch 与 AttentionBackend 之间的交互。\nAttentionBackend 加速模型前向传递 (AttentionBackend Accelerate Model Forward) SGLang 支持多个 Attention Backends，这些 backends 加速模型的 forward pass 和 key-value cache reuse。我们以 FlashInferBackend 为例。\nInitialization 配置 sliding window 和 cross-attention 场景的 wrappers。 分配必要的 buffers 和 key-value 索引。 为高效的注意力计算准备 forward metadata。 集成 CUDA Graphs 支持以优化执行路径。 init_forward_metadata decode mode：使用 indices_updater_decode 更新 decode 的索引，并设置 forward_metadata 以使用 decode_wrappers。 extend mode：根据 token 和 wrappers 的数量确定是否需要 ragged forward，随后使用 indices_updater_prefill 更新索引。 分配 metadata：设置 forward_metadata，为 ragged forward 和 prefix extension 设置 flags。 forward_extend 和 forward_decode 对 forward_extend，根据 ragged 或者 paged attention，选择合适的 wrapper。对 forward_decode，选择 decode wrapper。 计算 attention，管理 key-value cache，并返回 reshaped 后的输出。 DetokenizerManager 进行解码并发送回 TokenizerManager (DetokenizerManager Detokenize and Send to TokenizerManager) DetokenizerManager 在 launch_server 中被初始化为 Server 的子进程，用于将 Scheduler 返回的 token ids 转换为文本，并发送回 TokenizerManager。\nInitialization 设置 ZMQ communication socket 和 tokenizer。使用 LimitedCapacityDict 管理 decode status。\nevent_loop 和 trim_eos 接收来自 Scheduler 的处理请求，直接转发 BatchEmbeddingOut 或处理 BatchTokenIDOut 进行 detokenization。 将 token ID 拆分为 read_ids 和 surr_ids。使用 batch_decode 将 token ID 转换为文本。更新 DecodeStatus，包括新的 offsets 和 detokenized text。 在序列的停止处整理输出，将 detokenized text 与 metadata 合并成 BatchStrOut，并发送回 TokenizerManager。 FastAPI 整理并输出 (FastAPI Wraps the Output) DetokenizerManager 通过 ZeroMQ 将 BatchStrOut 发送到 TokenizerManager。 TokenizerManager 更新请求状态并为 FastAPI 准备 detokenized text。 最后，在 FastAPI 中，对于流式传输，使用异步生成器和 StreamingResponse 将响应发送给用户。 对于非流式传输，收集并使用 ORJSONResponse 发送完整响应，并返回给用户。 ","permalink":"https://pillumina.github.io/posts/aiinfra/06-sglang-backend/","summary":"\u003cp\u003e本文档为开发者提供 SGLang 后端代码的代码梳理，按照一个请求从输入到最后输出的顺序进行讲解。下图简要介绍了这一流程：\u003c/p\u003e\n\u003cdiv style=\"text-align: center; width: 100%; margin: 0 auto;\"\u003e\n    \u003cimg src=\"https://github.com/zhaochenyang20/Awesome-ML-SYS-Tutorial/raw/main/sglang/code-walk-through/sglang-architecture.svg\" alt=\"SGLang 架构图\" style=\"width: 100%; height: auto;\"\u003e\n\u003c/div\u003e\n\u003cp\u003e具体而言，请求的处理过程如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e用户启动 Server ，初始化 FastAPI App、TokenizerManager、DetokenizerManager 和 Scheduler，每个组件运行各自的无限事件循环（infinite event loop）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e用户向 FastAPI Server 发送 \u003ccode\u003e/v1/chat/completions\u003c/code\u003e 请求，Server 通过 \u003ccode\u003ev1_chat_completions\u003c/code\u003e endpoint 将请求转发到 TokenizerManager。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003ev1_chat_completions\u003c/code\u003e 函数将请求转换为 \u003ccode\u003eChatCompletionRequest\u003c/code\u003e，再转换为 \u003ccode\u003eGenerateReqInput\u003c/code\u003e，并调用 TokenizerManager 的 \u003ccode\u003egenerate_request\u003c/code\u003e 方法。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eTokenizerManager 对请求进行 tokenization，并以 Python 对象（\u003ccode\u003epyobj\u003c/code\u003e）形式将其转发给 Scheduler，同时调用 TokenizerManager 的 \u003ccode\u003e_wait_one_response\u003c/code\u003e 方法。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eScheduler 在事件循环 \u003ccode\u003eevent_loop_normal\u003c/code\u003e 中处理请求：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eScheduler 通过 \u003ccode\u003erecv_requests\u003c/code\u003e 接收请求，调用 \u003ccode\u003eprocess_input_requests\u003c/code\u003e 处理输入，通过 \u003ccode\u003ehandle_generate_request\u003c/code\u003e 管理生成请求的逻辑，并将其加入 \u003ccode\u003ewaiting_queue\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e从 \u003ccode\u003ewaiting_queue\u003c/code\u003e 中，Scheduler 使用 \u003ccode\u003eget_next_batch_to_run\u003c/code\u003e 为即将处理的请求创建 \u003ccode\u003eScheduleBatch\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003eScheduler 执行 \u003ccode\u003erun_batch\u003c/code\u003e 函数，将 \u003ccode\u003eScheduleBatch\u003c/code\u003e 转换为 \u003ccode\u003eModelWorkerBatch\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003eScheduler 调用 TpModelWorker 的 \u003ccode\u003eforward_batch_generation\u003c/code\u003e，等待 \u003ccode\u003elogits_output\u003c/code\u003e 和 \u003ccode\u003enext_token_ids\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003eTpModelWorker 初始化 \u003ccode\u003eForwardBatch\u003c/code\u003e，将其转发至 ModelRunner，并等待 \u003ccode\u003elogits_output\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003eModelRunner 处理 \u003ccode\u003eForwardBatch\u003c/code\u003e，调用 \u003ccode\u003eforward_extend\u003c/code\u003e 执行模型的前向计算（forward pass）。\u003c/li\u003e\n\u003cli\u003e模型通过 \u003ccode\u003eAttentionBackend\u003c/code\u003e 加速生成 logits，返回给 ModelRunner，进而返回给 TpModelWorker。\u003c/li\u003e\n\u003cli\u003eTpModelWorker 从 ModelRunner 接收 \u003ccode\u003elogits_output\u003c/code\u003e，调用 ModelRunner 的 \u003ccode\u003esample\u003c/code\u003e 方法生成 \u003ccode\u003enext_token_ids\u003c/code\u003e，并将其发送回 Scheduler。\u003c/li\u003e\n\u003cli\u003eScheduler 通过 \u003ccode\u003eprocess_batch_result\u003c/code\u003e 处理批次结果，使用 \u003ccode\u003etree_cache.cache_finished_req(req)\u003c/code\u003e 缓存请求，并通过 \u003ccode\u003echeck_finished\u003c/code\u003e 验证完成状态。对于未完成的请求，Scheduler 继续其事件循环，直到这个请求满足结束条件；对于已完成的请求，则转发到 Scheduler 的 \u003ccode\u003estream_output\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e在 \u003ccode\u003estream_output\u003c/code\u003e 函数中，Scheduler 处理输出，将其包装成 \u003ccode\u003eBatchTokenIDOut\u003c/code\u003e，并发送给 DetokenizerManager。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eDetokenizerManager 在其事件循环中接收 \u003ccode\u003eBatchTokenIDOut\u003c/code\u003e，处理后生成 \u003ccode\u003eBatchStrOut\u003c/code\u003e 并返回给 TokenizerManager。\u003c/p\u003e","title":"[SGLang] 后端代码速览"},{"content":"在上一篇文章中，我们介绍了MoE的一个几何诠释，旨在通过Dense模型的最佳逼近出发来推导和理解MoE。同时在文末我们也说了，给出MoE的计算公式仅仅是开始，训练一个实际有效的MoE模型还有很多细节补，比如本文要讨论的负载均衡（Load Balance）问题。\n负载均衡，即\u0026quot;不患寡而患不均\u0026quot;，说白了就是让每个Expert都在干活，并且都在干尽可能一样多的活，避免某些Expert浪费算力。负载均衡既是充分利用训练算力的需求，也是尽可能发挥MoE大参数量潜力的需求。\n问题分析 我们知道，MoE的基本形式是 $$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{e}_i $$ 对于传统MoE，$\\boldsymbol{\\rho}$是一个概率分布（Router），$\\boldsymbol{e}_i=\\boldsymbol{v}_i$，$\\boldsymbol{v}_i$是一个小型FFN（Expert）的输出；而对于我们上一篇推导的几何MoE，$\\boldsymbol{\\rho}$没有归一化的要求，它预测的是Expert的模长，而$\\boldsymbol{e}_i=\\boldsymbol{v}_i/\\Vert\\boldsymbol{v}_i\\Vert$预测的是Expert的方向。\n不管哪种格式的MoE，实际表现都差不多，只是理解视角的不同。但要注意，虽然MoE的公式给人的感觉是\u0026quot;每遇到一个Token，就去找相应的Expert来计算\u0026quot;，但实际训练时其实是反过来的：先给每个Expert分配好相应的算力，然后将Token分配（Route）到所属的Expert中并行计算，这也就为什么负责打分的$\\boldsymbol{\\rho}$被称为Router。\n这样一来，如果Expert的分配不均衡，就可能出现如下局面：某些Expert（Dead Expert）几乎一直闲置，浪费算力；某些Expert要处理的Token太多，根本忙不过来，只能Token Drop（即放弃处理部分Token）。从理论上来说，出现Dead Expert意味着MoE没有达到预期的参数量，即花了大参数量的显存，结果只训出来小参数量的效果。\n所以，不管是从训练还是性能角度看，我们都希望保证Expert的负载均衡。\n辅助损失（Auxiliary Loss） 促进负载均衡的常规思路是添加与之相关的损失函数，我们通常称之为\u0026quot;Aux Loss（Auxiliary Loss）\u0026quot;，目前主流用的Aux Loss最早可以追溯到2020年的《GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding》。\n介绍Aux Loss之前，我们需要先引入一些新概念。首先，我们已经提到对于一般的MoE来说，$\\boldsymbol{\\rho}$未必是概率分布，我们将归一化的$\\boldsymbol{\\rho}$记为$\\boldsymbol{p}=[p_1,p_2,\\cdots,p_n]$，以及它Top-$k$版为$\\boldsymbol{f}=[f_1,f_2,\\cdots,f_n]$，其中 $$ p_i = \\frac{\\rho_i}{\\sum_{i=1}^n \\rho_i},\\qquad f_i = \\begin{cases}1/k, \u0026 i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} \\\\ 0, \u0026 i\\not\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}\\end{cases} $$ 接着我们定义$\\boldsymbol{P}=\\mathbb{E}[\\boldsymbol{p}],\\boldsymbol{F}=\\mathbb{E}[\\boldsymbol{f}]$，这里的$\\mathbb{E}$是指对所有样本的所有Token做平均。不难看出，$\\boldsymbol{F}$就是Expert当前的负载分布，而$\\boldsymbol{P}$则相当于$\\boldsymbol{F}$的一个光滑近似。\n有了这些记号，我们就可以写出Aux Loss为：\n$$ \\mathcal{L}_{\\text{aux}} = \\boldsymbol{F}\\cdot \\boldsymbol{P} = \\sum_{i=1}^n F_i P_i \\tag{1} $$ 一般文献定义Aux Loss会多乘一个$n$，即它们的Aux Loss等于这里的$n \\mathcal{L}_{\\text{aux}}$。\n此外，有些大型MoE可能会按设备来算Aux Loss，以达到设备内的均衡，减少设备间的通信，这些就各自发挥了。但也有较新的实验显示，强行局部均衡极有可能影响模型最终效果。\n直通估计 （Straight-Through Estimator） 不知道大家有没有发现一个奇怪的现象：不管是最早出处、后续文献还是科普文章，总之笔者阅读过的资料中，对Aux Loss的引用都是不加证明的，似乎大家都公认上述Aux Loss能促进均衡是一件显然成立的事情。可真有这么显然易得吗？ 反正笔者是没看出来，所以接下来笔者给出式$(1)$的一种推导思路，由此思路我们还可以自定义其他形式的Aux Loss。\n首先，定义均匀分布$\\boldsymbol{Q}=(1/n,1/n,\\cdots,1/n)$，刚才我们说了$\\boldsymbol{F}$就是当前负载分布，因此负载均衡等价于$\\boldsymbol{F}=\\boldsymbol{Q}$，那么下式就是一个比较直观的Aux Loss：\n$$ \\mathcal{L}_{\\text{aux}} = \\frac{1}{2}\\Vert\\boldsymbol{F} - \\boldsymbol{Q}\\Vert^2 = \\frac{1}{2}\\sum_{i=1}^n (F_i - 1/n)^2 \\tag{2} $$ 问题是$\\boldsymbol{F}$是由$\\mathop{\\text{argtop}}_k$出来的，这意味着上式并不是一个能直接用的可导目标。怎么解决这个问题呢？答案是STE（Straight-Through Estimator）技巧，分别设计前向传播和反向传播的函数。\n具体来说，$\\boldsymbol{F}$不可导，$\\boldsymbol{P}$作为它的光滑近似是可导的，那么我们在反向传播的时候将$\\boldsymbol{F}$替换成$\\boldsymbol{P}$就行了，即\n$$ \\mathcal{L}_{\\text{aux}} = \\frac{1}{2}\\Vert \\boldsymbol{P} + \\text{sg}[\\boldsymbol{F}-\\boldsymbol{P}] - \\boldsymbol{Q}\\Vert^2 = \\frac{1}{2}\\sum_{i=1}^n (P_i + \\text{sg}[F_i - P_i] - 1/n)^2 \\tag{3} $$ 其中$\\text{sg}[]$是stop gradient算子，特点是保持前向输出不变，但强制梯度为零。这样改动之后，$\\mathcal{L}_{\\text{aux}}$ 就是一个切实可行的Aux Loss了，我们可以试求一下它的梯度：\n$$ \\begin{aligned} \\nabla_{\\boldsymbol{\\theta}}\\mathcal{L}_{\\text{aux}} \u0026= \\frac{1}{2}\\nabla_{\\boldsymbol{\\theta}}\\sum_{i=1}^n (P_i + \\text{sg}[F_i - P_i] - 1/n)^2 \\\\ \u0026= \\sum_{i=1}^n (P_i + \\text{sg}[F_i - P_i] - 1/n) \\nabla_{\\boldsymbol{\\theta}}(P_i + \\text{sg}[F_i - P_i] - 1/n)\\\\ \u0026= \\sum_{i=1}^n (F_i - 1/n) \\nabla_{\\boldsymbol{\\theta}}P_i = \\nabla_{\\boldsymbol{\\theta}}\\sum_{i=1}^n (F_i - 1/n) P_i\\\\ \u0026= \\nabla_{\\boldsymbol{\\theta}}\\left(\\sum_{i=1}^n F_i P_i\\right) \\end{aligned} $$ 这里$\\boldsymbol{\\theta}$是模型参数。最后的结果表明式$(3)$的梯度等于式$(1)$梯度，这意味着用式$(1)$作为Aux Loss跟式$(3)$在梯度上是等价的，所以就出现了式$(1)$的Aux Loss。\n然而，式$(1)$只有等效梯度的意义，但没有Loss的意义，不算一个真正的Loss，比如当$\\boldsymbol{F} = \\boldsymbol{P}$时我们可以算出式$(1)$等于$1/n$，但实际上我们可以构造出一个不等于$\\boldsymbol{P}$的$\\boldsymbol{F}$让它小于$1/n$，所以式$(1)$并不是像正常的Loss一样越小越好，最小值也不是$\\boldsymbol{F} = \\boldsymbol{P}$时取到。\n构建Aux Loss的一般形式 上述推导实际上提供了构建Aux Loss的一般思路：首先基于$\\boldsymbol{F}$构建符合要求的损失，然后在实现时将$\\boldsymbol{F}$替换成$\\boldsymbol{P} + \\text{sg}[\\boldsymbol{F}-\\boldsymbol{P}]$。比如，我们知道最大熵也可以将分布推向均衡，因此也可以用熵的相反数来构建Aux Loss：\n$$ \\mathcal{L}_{\\text{aux}} = \\sum_{i=1}^n (P_i + \\text{sg}[F_i - P_i])\\log(P_i + \\text{sg}[F_i - P_i]) $$ 上式就可以直接用作代码实现，当然如果我们追求简化，也可以类似地求梯度，结果将是\n$$ \\nabla_{\\boldsymbol{\\theta}}\\mathcal{L}_{\\text{aux}} = \\nabla_{\\boldsymbol{\\theta}}\\sum_{i=1}^n(P_i + \\text{sg}[F_i - P_i]) \\log(P_i + \\text{sg}[F_i - P_i]) = \\nabla_{\\boldsymbol{\\theta}}\\sum_{i=1}^n P_i \\log F_i $$ 两次简化梯度的过程中，我们都用到了如下恒等式\n$$ \\sum_{i=1}^n \\nabla_{\\boldsymbol{\\theta}}P_i = \\nabla_{\\boldsymbol{\\theta}}\\sum_{i=1}^n P_i = \\nabla_{\\boldsymbol{\\theta}}1 = \\boldsymbol{0} $$ 这依赖于$\\boldsymbol{P}$是一个概率分布，以及目标分布$\\boldsymbol{Q}$是均匀分布的事实。而如果我们不追求简化后的等价结果，而是直接用$\\boldsymbol{F}\\to \\boldsymbol{P} + \\text{sg}[\\boldsymbol{F}-\\boldsymbol{P}]$形式的Aux Loss，那么可以不受这两个约束。\n比如，$\\boldsymbol{P}$作为$\\boldsymbol{F}$光滑近似这一点，我们只用到了\u0026quot;$P_i$大$F_i$通常也大\u0026quot;的性质，所以用非归一化的$\\mathbb{E}[\\boldsymbol{\\rho}]$作为$\\boldsymbol{P}$通常也没问题，这一点在一些特殊场景（例如有正有负的$\\boldsymbol{\\rho}$）可能会比较关键，因为此时无法归一化为概率分布。\n又比如目标$\\Vert\\boldsymbol{F} - \\boldsymbol{Q}\\Vert^2$，显然能将$\\boldsymbol{F}$推向任意我们想要的、不一定是均匀的目标分布$\\boldsymbol{Q}$。\nLoss-Free方案 前面我们主要讨论了通过Aux Loss来促进负载均衡的思路。Aux Loss固然简单直观，但它也有一个明显的缺点——权重不好调——调低了无法促进均衡，调高了容易损害LM Loss，所以业界一直有寻找替代方案的尝试。\n接下来要讨论的是名为\u0026quot;Loss-Free\u0026quot;的方案，由DeepSeek在《Auxiliary-Loss-Free Load Balancing Strategy for Mixture-of-Experts》提出。和DeepSeek众多耀眼的开源作品相比，这篇论文也许不算起眼，但在笔者看来，它潜在的学术影响力可能远超其他工作，因为所提方法不仅简单有效，而且极具普适性，堪称经典。\nLoss-Free的基本思路 面对负载不均衡，Aux Loss的应对思路是通过额外的损失引导Router给出均衡的打分，而Loss-Free的想法则是换个新的分配思路，即不改变Router现有打分结果，而是改变$\\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}$这个分配方式。\n其实这个方向此前也有过一些努力。比如2021年Facebook提出了BASE Layer，将Expert的分配视为线性指派问题，即以负载均衡为约束条件，求在该约束之下Router总打分尽可能高的分配结果，这可以用匈牙利算法等来解决。\n但该方案需要知道全体Token的打分，所以对于自回归式LLM来说，它只适用于训练，推理还是只能用$\\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}$，训练推理存在不一致性，并且由于目前求解算法的限制，它只适用于$k=1$的场景。\n相比之下，Loss-Free的做法非常简单且有效，它留意到一个事实，即我们总可以引入一个偏置项$\\boldsymbol{b}$，使得$\\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} + \\boldsymbol{b}$的分配是均衡的，所以它将MoE的形式改为\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{e}_i\\qquad\\to\\qquad \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} + \\boldsymbol{b}} \\rho_i \\boldsymbol{e}_i $$ 这里的$\\boldsymbol{b}$是输入无关的向量，由训练过程确定下来，训练完后它就保持不变，因此推理阶段也可以用，换言之训练和推理具有一致的形式。\n注意乘以$\\boldsymbol{e}_i$的还是$\\rho_i$而不是$\\rho_i + b_i$，也就是说$\\boldsymbol{b}$仅仅参与分配过程而不参与MoE的前向计算，所以我们对$\\boldsymbol{b}$或$\\boldsymbol{\\rho} + \\boldsymbol{b}$的正负性都没有特殊要求。\n梯度怎么算 怎么训练$\\boldsymbol{b}$呢？我们知道，$\\boldsymbol{b}$的优化方向自然是促进负载均衡，为此按照上一篇的记号，我们先定义$\\boldsymbol{f}=[f_1,f_2,\\cdots,f_n]$：\n$$ f_i = \\begin{cases}1/k, \u0026 i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}+\\boldsymbol{b} \\\\ 0, \u0026 i\\not\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}+\\boldsymbol{b}\\end{cases} $$ 以及$\\boldsymbol{F}=\\mathbb{E}[\\boldsymbol{f}]$，这里的$\\boldsymbol{F}$自然就是在$\\boldsymbol{b}$偏置下Expert当前的负载分布了。借着我们定义均匀分布为$\\boldsymbol{Q}=(1/n,1/n,\\cdots,1/n)$，那么负载均衡就相当于最小化\n$$ \\mathcal{L}_{\\text{aux}} = \\frac{1}{2}\\Vert\\boldsymbol{F} - \\boldsymbol{Q}\\Vert^2 = \\frac{1}{2}\\sum_{i=1}^n (F_i - 1/n)^2 $$ 这个目标是不可导的，但有了上一篇的经验，我们知道STE（Straight-Through Estimator）可以解决这个问题。STE的关键是找一个可导且跟$\\boldsymbol{F}$具有同增减趋势的量作为$\\boldsymbol{F}$的光滑近似，这里我们的优化参数只有$\\boldsymbol{b}$，而它正好具有我们期望的性质（增大$b_i$，$i$被选中的概率就更高，那么$F_i$就更大），所以答案就呼之欲出了：\n$$ \\mathcal{L}_{\\text{aux}} = \\frac{1}{2}\\Vert\\boldsymbol{b} + \\text{sg}[\\boldsymbol{F}-\\boldsymbol{b}] - \\boldsymbol{Q}\\Vert^2 = \\frac{1}{2}\\sum_{i=1}^n (b_i + \\text{sg}[F_i - b_i] - 1/n)^2 $$ 它的梯度是\n$$ \\nabla_{\\boldsymbol{b}}\\mathcal{L}_{\\text{aux}} = \\frac{1}{2}\\nabla_{\\boldsymbol{b}}\\Vert\\boldsymbol{b} + \\text{sg}[\\boldsymbol{F}-\\boldsymbol{b}] - \\boldsymbol{Q}\\Vert^2 = \\boldsymbol{F} - \\boldsymbol{Q} $$ 所以用梯度下降（SGD）来更新$\\boldsymbol{b}$就是 $$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha (\\boldsymbol{F} - \\boldsymbol{Q}) $$ 这里$\\alpha$是$\\boldsymbol{b}$的学习率。不过Loss-Free最终选择的更新规则略有不同，它选择的是符号梯度下降（SignSGD）： $$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha \\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q}) \\tag{1} $$ 这个结果其实也很好理解，就是如果$F_i$比$1/n$大，那么就调小一点$b_i$，否则就增大一点$b_i$。 ## 改良版本 除了加$\\mathop{\\text{sign}}$的符号梯度下降外，笔者发现直接对$\\boldsymbol{F} - \\boldsymbol{Q}$做RMS Norm（即Normalized SGD），在相同的$\\alpha$下往往能达到更好的均衡效果：\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha\\frac{\\boldsymbol{F} - \\boldsymbol{Q}}{\\text{RMS}(\\boldsymbol{F} - \\boldsymbol{Q})} $$ 这里的$\\text{RMS}$是\u0026quot;Root Mean Square\u0026quot;，定义为 $$ \\text{RMS}(\\boldsymbol{F} - \\boldsymbol{Q}) = \\sqrt{\\frac{1}{n}\\sum_{i=1}^n (F_i - Q_i)^2} $$ 不难看出，加$\\mathop{\\text{sign}}$后的$\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q})$和加RMS Norm后的$\\frac{\\boldsymbol{F} - \\boldsymbol{Q}}{\\text{RMS}(\\boldsymbol{F} - \\boldsymbol{Q})}$，它们的$\\text{RMS}$都是1，因此它们俩尺度上是大致相同的，所以我们可以使用相同的$\\alpha$。\n简单来说，$\\mathop{\\text{sign}}$的问题在于不论$F_i$与目标$Q_i$的远近都使用同样的更新幅度，这导致原本就已经跟$Q_i$比较接近的$F_i$反而容易偏离原本已经达到的均衡，从而产生震荡；\n而RMS Norm则保留了$F_i-Q_i$之间的相对大小，更新幅度更加自适应一些，理论上更有助于促进均衡，实测效果也多是它更好。\n不同视角的合一 原论文在介绍Loss-Free时，并没有上述Aux Loss的推导过程，而是直接给出式$(1)$的更新规则，给人的感觉是给$\\boldsymbol{b}$\u0026ldquo;手搓\u0026quot;了梯度$\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q})$，这也是它Loss-Free这个名字的来源。\n然而，从本文给出的推导可以看出，更新规则$(1)$也完全可以从Aux Loss视角得到，两者是一脉相承的。\n看起来Loss-Free最直接的好处是不用调Aux Loss权重了，但它实际上也有个学习率参数$\\alpha$要调，尽管原论文已经帮我们搜好$\\alpha=0.001$这个默认值，但不可否认这个超参数是存在的。\n在笔者看来，Loss-Free的本质创新并不是没有Aux Loss，而是隔离了Aux Loss和LM Loss的优化参数，从而达到了负载均衡和模型能力两不误的效果。\n其中最关键一步，是留意到\u0026quot;一个偏置项足以达到负载均衡\u0026quot;这一事实，然后就让Aux Loss只优化新引入的偏置$\\boldsymbol{b}$，而LM Loss则优化剩余参数，让Aux Loss对LM Loss的负面作用降到最低。\n相比之下，常规的Aux Loss方案需要全体参数来促进负载均衡，而LM Loss优化的也是全体参数，两者的优化方向可能并不完全兼容，因此想找到一个最优的平衡点相对来说就更为困难。\n所以，Loss-Free基于\u0026quot;一个偏置项足以达到负载均衡\u0026quot;将两个Loss的优化参数隔离开来，是负载均衡问题的一个绝妙的解决办法。\n使用上的细节 尽管Loss-Free已经足够简单明了，但是在使用的时候还要稍微注意一些细节。\n首先，对于每个Batch的数据，我们应当先根据LM Loss来更新模型参数，然后再根据式$(1)$来更新$\\boldsymbol{b}$。这是因为$\\boldsymbol{b}$的更新依赖于全体Token的统计信息$\\boldsymbol{F}$，先更新$\\boldsymbol{b}$再更新模型其余参数的话，原则上会有泄漏未来信息的风险。虽然直观看来就一个向量$\\boldsymbol{b}$泄漏不了多少信息，但这个风险终归是存在的，因此要尽量去规避它。\n其次，刚才我们说原论文已经调好$\\alpha=0.001$，但这个结果可能跟原论文用Sigmoid作为Router $\\boldsymbol{\\rho}$激活函数的选择是绑定的。原因也不难想，经过Sigmoid后，每个$\\rho_i$相对比较独立，并且都在$(0,1)$内，$\\alpha=0.001$相当于说每一步的更新幅度约为千分之一，如果换Softmax、ReLU或者其他激活函数，那么就可能需要重调$\\alpha$了。\n针对这个问题，笔者建议的做法是解耦Gate和Bias所用的激活函数，即\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} + \\boldsymbol{b}} \\rho_i \\boldsymbol{e}_i\\qquad\\to\\qquad \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}^{(\\sigma)} + \\boldsymbol{b}} \\rho_i^{(h)} \\boldsymbol{e}_i $$ 其中$\\boldsymbol{\\rho}^{(\\sigma)} = \\sigma(\\boldsymbol{x}\\boldsymbol{W}^{(R)}), \\boldsymbol{\\rho}^{(h)} = h(\\boldsymbol{x}\\boldsymbol{W}^{(R)})$，$\\sigma(\\cdot)$是Sigmoid函数，$h(\\cdot)$是任意单调且值域非负的函数，说白了就是加上$\\boldsymbol{b}$的是Sigmoid激活的打分，这样我们就可以复用$\\alpha=0.001$，至于乘上Expert的Gate，我们可以用其他激活函数，只要它的单调性跟Sigmoid一致就行。\n此外，由于更新规则$(1)$加了$\\text{sign}$函数，因此有可能训出绝对值大于1的$b_i$，整体绝对值还可能越来越大，这些都是正常的，对模型效果不会有影响。\n实际上$\\boldsymbol{b}$有一个冗余的自由度，因为全体$b_i$都加上同一个常数后，$\\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} + \\boldsymbol{b}$的结果不变。这个额外的自由度我们可以用来做其他好玩的事情（下文分解）。\n延伸思考 除了MoE的负载均衡之外，Loss-Free的思想还可以应用到很多类似问题，比如VQ-VQE的编码表坍缩（Codebook Collapse），就可以用同样思路解决，而且相比之前介绍的\u0026rdquo;旋转技巧\u0026quot;、\u0026quot;线性变换技巧\u0026ldquo;显得更自然和普适。\n事实上，本文开篇的评价\u0026quot;Loss-Free潜在的学术影响力可能远超其他工作\u0026rdquo;，正是基于Loss-Free的普适性考虑的。 抛开具体的应用背景，从数学上来看，Loss-Free的贡献可以理解为给出了用梯度下降来求解指派问题的方法。一个经典的线性指派问题可以表示为： $$ \\min_f \\sum_{i=1}^n c_{i, f(i)} $$ 其中$c_{i,j}$是给定的成本函数，$f$是${1,2,\\cdots,n}$到自身的双射。放到本文的背景下，$c_{i,j}$不就相当于$n$个Token、$n$个Expert的打分，所求$f$不就是一个负载均衡的分配方案？\n求解此类问题的一般想法是在满足约束条件的空间里搜索尽可能优的解，而Loss-Free则反过来，先构建一个最优但不一定满足约束条件的解：\n$$ f(i) = \\mathop{\\text{argmin}}_j c_{i,j} $$ 这个解在分数上肯定是最优的，但不一定满足双射的条件，这里不满足双射就等价于负载不均衡。于是我们引入偏置 $$ f(i) = \\mathop{\\text{argmin}}_j c_{i,j} + b_j $$ $b_j$初始化为零，然后根据式$(1)$来更新，更新规则说白了就是哪个$j$出现出现次数多，那减少相应的$b_j$，反之增加，直到出现双射为止。\n动态调整Expert数量 前面讨论的时候，笔者留了一个悬念：它引入的Bias项有一个冗余的自由度，这个自由度可以用来做另外有趣的事情。这里我们就来讨论这件事。\n我们知道，MoE是为每个Token只选择最匹配的$k$个Expert来进行计算，从而在增大参数量的同时还节省了计算量。\n然而，当我们仔细思考就会发现，这个策略实际上有明显的可改进之处：直观来看，每个Token的难度并不一样，所以更合理的方案应该是难的Token分配更多的计算资源，简单的token分配更少的资源，这样或许能在同样有限的资源下将效果最大化。\n而刚才提到的Bias的额外自由度，恰好可以用来简单地实现这个目标。\n设计思想 首先，我们回顾一下，MoE的基本形式是\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{e}_i $$ 负载不均衡是MoE训练常见的问题，对此研究人员提出了Aux Loss，前面介绍了DeepSeek提出的Loss-Free方案，它将MoE改为\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} + \\boldsymbol{b}} \\rho_i \\boldsymbol{e}_i $$ 然后通过调节新引入的Bias项$\\boldsymbol{b}$来实现负载均衡。为了实现每个Token可以选择动态数量的Expert，笔者提出的做法是将Loss-Free的形式稍微修改一下：\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argwhere}} \\boldsymbol{\\rho} + \\boldsymbol{b} \u003e 0} \\rho_i \\boldsymbol{e}_i $$ 即只要满足$\\rho_i + b_i \u0026gt; 0$的Expert就被选中，这样每个Token选出的Expert数量自然是动态的，并且免除了排序的需求，某种程度上看还变得更简化了。\n优化目标 $\\boldsymbol{b}$的优化目标有两个： 1. 跟Loss-Free一样，要实现负载均匀 2. 要控制每个Token被选中的平均Expert数为$k$（预算控制） 负载均衡依然采样Loss-Free的训练方式。定义记号$\\boldsymbol{f} = [f_1, f_2, \\cdots, f_n]$ $$ f_i = \\begin{cases} 1, \u0026 \\rho_i + b_i \u003e 0 \\\\ 0, \u0026 \\rho_i + b_i \\leq 0 \\end{cases} $$ 然后记$\\tilde{\\boldsymbol{F}}=\\mathbb{E}[\\boldsymbol{f}]$，那么$\\boldsymbol{F} = \\tilde{\\boldsymbol{F}}/|\\tilde{\\boldsymbol{F}}|$就是当前Expert分布，其中$|\\tilde{\\boldsymbol{F}}|$是$\\tilde{\\boldsymbol{F}}$的各分量之和。Loss-Free提出的更新公式是：\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha \\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q}) $$ 其中$\\boldsymbol{Q}=(1/n, 1/n, \\cdots, 1/n)$是目标的均匀分布。\n我们提到多次，$\\boldsymbol{b}$存在一个冗余的自由度，体现在对$\\boldsymbol{b}$所有分量加上同一个常数，排序结果不变。这样一来，我们可以把更新规则改为\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha \\left[\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q}) - \\overline{\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q})}\\right] $$ 这里向量上面加一横代表该向量的全体分量的均值，是一个标量，向量减标量代表每个分量都减去这个标量。这样一来出来的$\\boldsymbol{b}$必然满足$\\overline{\\boldsymbol{b}}=0$，但不改变负载均衡的效果。\n于是我们可以$\\overline{\\boldsymbol{b}}$这个自由度留给预算控制。 怎么理解呢？很明显，如果给全体$b_i$都加上同一个正数，那么满足$\\rho_i + b_i \u0026gt; 0$的几率将会变大，从而总预算也会增大。\n所以做法很简单，先算出当前平均预算，不难发现正好是$|\\tilde{\\boldsymbol{F}}|$，如果它大于$k$，那么就调小一点$\\boldsymbol{b}$，反之则增大。整合到上式是\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha \\left[\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q}) - \\overline{\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q})} + \\mathop{\\text{sign}}(|\\tilde{\\boldsymbol{F}}|- k)\\right] $$ 如果只想保证预算不超过$k$，而不非要等于$k$，那么可以改为当$|\\tilde{\\boldsymbol{F}}| \u0026lt; k$时不作改变\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha \\left[\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q}) - \\overline{\\mathop{\\text{sign}}(\\boldsymbol{F} - \\boldsymbol{Q})} + \\mathop{\\text{sign}}(\\max(|\\tilde{\\boldsymbol{F}}|- k,0))\\right] $$ 尝试简化 细细品味上面的式子，我们会发现它做了两件事：\n让$\\boldsymbol{F}=\\tilde{\\boldsymbol{F}}/|\\tilde{\\boldsymbol{F}}|$逼近$\\boldsymbol{Q}$ 让$|\\tilde{\\boldsymbol{F}}|$逼近$k$ 这看起来可以合并成一件事：让$\\tilde{\\boldsymbol{F}}$逼近$\\tilde{\\boldsymbol{Q}}=k\\boldsymbol{Q}=(k/n,k/n,\\cdots,k/n)$。\n于是式前面的公式可以简化为\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha \\mathop{\\text{sign}}(\\tilde{\\boldsymbol{F}} - \\tilde{\\boldsymbol{Q}}) $$ 笔者将两个式子都做了实验，发现它们在效果上大同小异，但是后面的式子的负载均衡和预算控制两个指标在训练前期的抖动都大很多，所以追求稳定性的读者可以优先考虑前两个公式，追求简洁的读者则可以考虑最后一个公式。 考虑到$\\mathop{\\text{sign}}$只保留了$\\tilde{F}_i - \\tilde{Q}_i$的符号而忽略了绝对值的大小，笔者也尝试RMS Norm替代$\\mathop{\\text{sign}}$：\n$$ \\boldsymbol{b}\\leftarrow \\boldsymbol{b} - \\alpha (\\tilde{\\boldsymbol{F}} - \\tilde{\\boldsymbol{Q}})/\\Vert\\tilde{\\boldsymbol{F}} - \\tilde{\\boldsymbol{Q}}\\Vert_{RMS} $$ 其中向量的$\\Vert\\cdot\\Vert_{RMS}$是指分量的平方和的平方根。很明显$\\mathop{\\text{sign}}$的RMS是1，而RMS Norm之后RMS也为1，所以两者更新的数量级相同，可以用同一个$\\alpha$。\n由于RMS Norm保留了$\\tilde{F}_i - \\tilde{Q}_i$的相对大小，可以做到误差小的更新也小，所以在波动程度上比$\\mathop{\\text{sign}}$略小，但也好得不多。 当然，用RMS Norm替换$\\mathop{\\text{sign}}$来增加稳定性是一个通用技巧，前面推导过程中的式子都可以做这样的替换，这就看个人审美了，总之只是略稳但不多。\n初始化方式 解决完$\\boldsymbol{b}$的更新规则，我们来考虑$\\boldsymbol{b}$的初始化，这是一个有意思但不算十分关键的问题。\n按照常规做法，$\\boldsymbol{b}$全零初始化且$\\boldsymbol{\\rho}$用Sigmoid激活，那么初始阶段会把$n$个Expert都选出来，明显超出$\\leq k$的预算，这将会导致非常多的Token Drop。\n不过，如果我们没有强迫症的话，这并不是很严重的问题，因为模型其他参数通常会加Warmup但$\\boldsymbol{b}$通常不加，所以在Warmup的前几步模型就会自动把这个问题解决了。\n如果我们介意这一点，那么可以通过调整$\\boldsymbol{b}$初始化来控制初始预算。假设Router的输入是$d$维向量，满足零均值、单位方差（有RMSNorm在，近似成立），Router的权重初始化方差为$\\sigma^2$，那么Router的Logits近似为零均值、$\\sigma^2 d$方差。\n有了这些数据，我们可以用正态近似模拟加二分法估算一个初始$\\boldsymbol{b}$：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) def b_init(n, k, d, sigma, eps=0.1): b1, b2 = -1, 0 std = sigma * d**0.5 logits = np.random.randn(10000, n) * std scores = sigmoid(logits) while True: b = (b1 + b2) * 0.5 c = ((scores + b) \u0026gt; 0).sum(1).mean() if -eps \u0026lt; c - k \u0026lt; eps: return b elif c \u0026gt; k: b2 = b else: b1 = b b_init(32, 4, 1024, 6e-3) 代码中考虑的是Sigmoid激活，所以搜索区间是$[-1, 0]$，如果是其他激活函数请自行调整。不过这里的建议跟前面聊到的思路是相同的，即加b的ρ可以统一用Sigmoid激活，乘上Expert的ρ才考虑用别的激活函数。\n相关工作 其实，已经有一些工作尝试过动态选择Expert数目的MoE设计，下面简单列举一些笔者搜到的工作，并从个人的审美角度做一些简单的评析。\n比较朴素的做法是AdaMoE和MoE++，它们在Expert中混入了一些低计算成本的Expert，如空白Expert、复制Expert、常数Expert，同时也鼓励负载均衡，这样当Token选中这些简单Expert时，等价于少选择了其他标准的Expert，从而间接地实现了动态数目。这样做的好处是可以复用原本Top-k MoE的基建，但同时也欠缺了一些灵活性。\n另外一个朴素的想法是将Top-k选择改为Top-p，出自《Harder Tasks Need More Experts: Dynamic Routing in MoE Models》。这个转换看上去很自然，但实际上有颇多问题，比如无法准确控制平均预算，因为当ρ接近均匀分布时Top-p的比例会非常大，所以原论文又新增了一项熵损失来让ρ远离均匀分布。总的来说，个人感觉它引入的问题比收益更明显。\n一个比较独特的做法是Ada-K Routing，它新增一个模块来预测要激活的Expert数，然后用强化学习来训练，这样做在原理上没问题，但引入强化学习无疑会增加训练复杂性。DA-MoE则利用Attention分数来识别重要Token，为其分配更多Expert，但感觉不够本质，因为“MoE”原则上不局限于FFN层，一旦用到Attention上，不就没有Attention分数可用了？\n形式上跟本文做法最相似的可能是ReMoE，它同样是基于零阈值来选择Expert，但选择了Aux Loss的方式来实现负载均匀以及预算控制，同时又混合了手搓梯度的思想来控制Aux Loss权重，总体来看多了点糅合感。\n本文则延续了Loss-Free的思想，利用b的额外自由度来调控这个阈值，从而以最小的改动实现了动态Expert数目。\n均匀分布的反思: Shared Expert和Fine-Grained Expert 如果说Meta的LLAMA系列为Dense模型确立了标准架构，那么DeepSeek或许就是MoE标准架构的奠基者。\n当然，这并非指DeepSeek首创了MoE，也不是说它的MoE不可超越，而是指DeepSeek对MoE所提的一些改进，很可能都是效果增益比较显著的方向，从而逐渐成为MoE的标配。\n这其中，包括我们在前面章节介绍的Loss-Free负载均衡方案，还有将要介绍的Shared Expert、Fine-Grained Expert策略。\n说到负载均衡，它无疑是MoE一个极为重要的目标，前面的几个章节，可以说都在围绕着它展开。然而，已有读者逐渐意识到，这里边有个尚未回答的本质问题：抛开效率上的需求不谈，均匀分布就一定是效果最好的方向吗？\n这里就带着这个疑问，去理解Shared Expert、Fine-Grained Expert。\n共享专家 让我们再次回顾MoE的基本形式\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{e}_i $$ 除此之外，前文中的Loss-Free将$\\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}$替换换成$\\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}+\\boldsymbol{b}$，还有在前文我们将它推广成$\\mathop{\\text{argwhere}} \\boldsymbol{\\rho}+\\boldsymbol{b} \u0026gt; 0$，但这些变体跟Shared Expert技巧都是正交的，因此接下来只以最基本的形式为例。\nShared Expert将上式改为\n$$ \\boldsymbol{y} = \\sum_{i=1}^s \\boldsymbol{e}_i + \\sum_{i\\in \\mathop{\\text{argtop}}_{k-s} \\boldsymbol{\\rho}_{[s:]}} \\rho_{i+s} \\boldsymbol{e}_{i+s} $$ 也就是说，将原本的$n$选$k$，改为$n-s$选$k-s$，另外$s$个Expert则必然会被选中，这部分就被称为\u0026quot;Shared Expert\u0026quot;，刚出来那会我们还戏称为\u0026quot;常任理事国\u0026quot;，剩下的$n-s$个Expert则被称为\u0026quot;Routed Expert\u0026quot;。\n其中，Shared Expert的数目$s$不会太大，通常是1或2，太大反而会让模型\u0026quot;冷落\u0026quot;了剩下的Routed Expert。\n需要指出的是，开启Shared Expert前后，总Expert数都是$n$，激活的Expert都是$k$，所以Shared Expert原则上不增加模型参数量和推理成本。但即便如此，DeepSeekMoE和我们自己的一些实验显示，Shared Expert依然能一定程度上提升模型效果。\n多种理解 我们可以从多个视角理解Shared Expert：\n残差视角：指出Shared Expert技巧实际上是将原本学习每一个Expert，改为学习它跟Shared Expert的残差，这样能降低学习难度，还会有更好的梯度。\n教学类比：DeepSeek的说法是将共同知识压缩到这些Shared Expert中，减轻Routed Expert之间的冗余。如果将Routed Expert类比成中学各个学科的老师，那么Shared Expert就是类似\u0026quot;班主任\u0026quot;的存在。\n几何角度：Expert之间的不可避免的共性，几何意义是它们的向量夹角小于90度。我们可以将Shared Expert理解成这些Routed Expert的均值，通过学习减去均值后的残差，使得正交假设更容易成立。\n比例因子 我们将前面带上Shared Expert的式子一般地写成\n$$ \\boldsymbol{y} = \\sum_{i=1}^s \\boldsymbol{e}_i + \\lambda\\sum_{i\\in \\mathop{\\text{argtop}}_{k-s} \\boldsymbol{\\rho}_{[s:]}} \\rho_{i+s} \\boldsymbol{e}_{i+s} $$ 由于Routed Expert带有权重$\\rho_{i+s}$而Shared Expert没有，以及Routed Expert的数目通常远大于Shared Expert数目（即$n - s \\gg s$）等原因，它们的比例可能会失衡，因此设置合理的$\\lambda$尤为重要。\n在论文《Muon is Scalable for LLM Training》中提出，适当的$\\lambda$应使得两者在初始化阶段模长接近一致。\n具体计算方法：\n假设每个Expert在初始化阶段具有相同的模长（设为1）且两两正交 假设Router的logits服从标准正态分布 通过数值模拟计算$\\lambda$： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) def softmax(x): return (p := np.exp(x)) / p.sum() def scaling_factor(n, k, s, act=\u0026#39;softmax\u0026#39;, renorm=False): factors = [] for _ in range(10000): logits = np.random.randn(n - s) p = np.sort(eval(act)(logits))[::-1][:k - s] if renorm: p /= p.sum() factors.append(s**0.5 / (p**2).sum()**0.5) return np.mean(factors) # DeepSeek-V2配置 scaling_factor(162, 8, 2, \u0026#39;softmax\u0026#39;, False) # ≈16 # DeepSeek-V3配置 scaling_factor(257, 9, 1, \u0026#39;sigmoid\u0026#39;, True) # ≈2.83 非常巧的是，这个脚本的模拟结果跟DeepSeek-V2、DeepSeek-V3的设置都很吻合。\n其中，DeepSeek-V2有n=162,k=8,s=2，Softmax激活并且没有重归一化，上述脚本的模拟结果约等于16，而DeepSeek-V2的λ正好是16来源；DeepSeek-V3则有n=257,k=9,s=1，Sigmoid激活且重归一化，脚本的结果大约是2.83，而DeepSeek-V3的λ则是2.5来源。\n非均匀性 回到文章开头的问题：均衡一定是效果最好的方向吗？看起来Shared Expert给了一个参考答案：未必。因为Shared Expert也可以理解为某些Expert一定会被激活，于是整体来看，这将导致一个非均匀的Expert分布：\n$$ \\boldsymbol{F} = \\frac{1}{s+1}\\bigg[\\underbrace{1,\\cdots,1}_{s\\text{个}},\\underbrace{\\frac{1}{n-s},\\cdots,\\frac{1}{n-s}}_{n-s\\text{个}}\\bigg] $$ 实际上，非均匀分布在现实世界随处可见，所以均匀分布并非最优方向其实应该很容易接受。还是以前面的中学老师类比为例，同一个学校各个学科的老师数量其实是不均匀的，通常是语文、数学、英语最多，物理、化学、生物次之，体育、美术更少（还经常生病）。更多非均匀分布的例子，大家可以搜索一下Zipf定律。\n总而言之，现实世界的非均匀性，必然会导致自然语言的非均匀性，从而导致均匀分布的非最优性。当然，从训练模型的角度看，均匀分布还是更容易并行和扩展，所以单独分离出一部分Shared Expert，剩下的Routed Expert仍然希望它均匀，是实现非均匀性的一种对双方都友好的折中选择，而不是直接让Routed Expert对齐一个非均匀分布。\n刚才说的是训练，那推理呢？推理阶段可以事先预估Routed Expert的实际分布，并且不需要考虑反向传播，所以只要细致地进行优化，理论上可以做到效率不降的。但由于现在MoE的推理基建都是针对均匀分布设计的，并且单卡显存有限等实际限制，所以我们仍旧希望Routed Expert能均匀来实现更好的推理效率。\n细颗粒度 除了Shared Expert外，DeepSeekMoE所提的另一个改进点是Fine-Grained Expert，它指出在总参数量和激活参数量都不变的情况下，Expert的颗粒度越细，效果往往越好。\n比如，原本是n选k的Routed Expert，现在我们将每个Expert缩小一半，然后改成2n选2k，那么总参数量和激活的参数量都还是一样的，但后者表现往往更好。原论文的说法是这样丰富了Expert组合的多样性。\n当然，我们也可以有其他理解，比如说将Expert进一步分割成更小的单元，那么每个Expert可以专注于更狭窄的知识领域，从而实现更精细的知识分解，等等。但要注意，Fine-Grained Expert并非是无成本的，n越大，Expert之间的负载往往越不均衡，并且Expert之间的通信和协调成本也会增加，所以n也不能无限增加，有一个效果和效率都友好的舒适区间。\n关于Fine-Grained Expert的有效性，笔者这里提出另外一种不大容易察觉的解释，它跟本文的主题有关：更多数量、更细颗粒度的Expert，可以更好地模拟现实世界的非均匀性。\n以下图为例，假设知识可以分为一大一小两类，每个Expert则是一个圆，如果我们用2个大圆去覆盖，那么存在一定的遗漏和浪费，而如果改用8个总面积相同的小圆，那么就可以覆盖得更为细致，因此效果更优。\n","permalink":"https://pillumina.github.io/posts/llmtheory/2-moe/","summary":"\u003cp\u003e在上一篇文章中，我们介绍了MoE的一个几何诠释，旨在通过Dense模型的最佳逼近出发来推导和理解MoE。同时在文末我们也说了，给出MoE的计算公式仅仅是开始，训练一个实际有效的MoE模型还有很多细节补，比如本文要讨论的负载均衡（Load Balance）问题。\u003c/p\u003e\n\u003cp\u003e负载均衡，即\u0026quot;不患寡而患不均\u0026quot;，说白了就是让每个Expert都在干活，并且都在干尽可能一样多的活，避免某些Expert浪费算力。负载均衡既是充分利用训练算力的需求，也是尽可能发挥MoE大参数量潜力的需求。\u003c/p\u003e\n\u003ch2 id=\"问题分析\"\u003e问题分析\u003c/h2\u003e\n\u003cp\u003e我们知道，MoE的基本形式是 \u003c/p\u003e\n$$ \\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{e}_i $$\u003cp\u003e 对于传统MoE，$\\boldsymbol{\\rho}$是一个概率分布（Router），$\\boldsymbol{e}_i=\\boldsymbol{v}_i$，$\\boldsymbol{v}_i$是一个小型FFN（Expert）的输出；而对于我们上一篇推导的几何MoE，$\\boldsymbol{\\rho}$没有归一化的要求，它预测的是Expert的模长，而$\\boldsymbol{e}_i=\\boldsymbol{v}_i/\\Vert\\boldsymbol{v}_i\\Vert$预测的是Expert的方向。\u003c/p\u003e\n\u003cp\u003e不管哪种格式的MoE，实际表现都差不多，只是理解视角的不同。但要注意，虽然MoE的公式给人的感觉是\u0026quot;每遇到一个Token，就去找相应的Expert来计算\u0026quot;，但实际训练时其实是反过来的：先给每个Expert分配好相应的算力，然后将Token分配（Route）到所属的Expert中并行计算，这也就为什么负责打分的$\\boldsymbol{\\rho}$被称为Router。\u003c/p\u003e\n\u003cp\u003e这样一来，如果Expert的分配不均衡，就可能出现如下局面：某些Expert（Dead Expert）几乎一直闲置，浪费算力；某些Expert要处理的Token太多，根本忙不过来，只能Token Drop（即放弃处理部分Token）。从理论上来说，出现Dead Expert意味着MoE没有达到预期的参数量，即花了大参数量的显存，结果只训出来小参数量的效果。\u003c/p\u003e\n\u003cp\u003e所以，不管是从训练还是性能角度看，我们都希望保证Expert的负载均衡。\u003c/p\u003e\n\u003ch2 id=\"辅助损失auxiliary-loss\"\u003e辅助损失（Auxiliary Loss）\u003c/h2\u003e\n\u003cp\u003e促进负载均衡的常规思路是添加与之相关的损失函数，我们通常称之为\u0026quot;Aux Loss（Auxiliary Loss）\u0026quot;，目前主流用的Aux Loss最早可以追溯到2020年的\u003ca href=\"https://papers.cool/arxiv/2006.16668\"\u003e《GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e介绍Aux Loss之前，我们需要先引入一些新概念。首先，我们已经提到对于一般的MoE来说，$\\boldsymbol{\\rho}$未必是概率分布，我们将归一化的$\\boldsymbol{\\rho}$记为$\\boldsymbol{p}=[p_1,p_2,\\cdots,p_n]$，以及它Top-$k$版为$\\boldsymbol{f}=[f_1,f_2,\\cdots,f_n]$，其中 \u003c/p\u003e\n$$ p_i = \\frac{\\rho_i}{\\sum_{i=1}^n \\rho_i},\\qquad f_i = \\begin{cases}1/k, \u0026 i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho} \\\\ 0, \u0026 i\\not\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}\\end{cases} $$\u003cp\u003e 接着我们定义$\\boldsymbol{P}=\\mathbb{E}[\\boldsymbol{p}],\\boldsymbol{F}=\\mathbb{E}[\\boldsymbol{f}]$，这里的$\\mathbb{E}$是指对所有样本的所有Token做平均。不难看出，$\\boldsymbol{F}$就是Expert当前的负载分布，而$\\boldsymbol{P}$则相当于$\\boldsymbol{F}$的一个光滑近似。\u003c/p\u003e\n\u003cp\u003e有了这些记号，我们就可以写出Aux Loss为：\u003cbr\u003e\n\n$$ \n\\mathcal{L}_{\\text{aux}} = \\boldsymbol{F}\\cdot \\boldsymbol{P} = \\sum_{i=1}^n F_i P_i \\tag{1}\n$$\n\u003c/p\u003e","title":"MoE环游记：2、深入负载均衡"},{"content":"MoE（Mixture of Experts）架构的流行自不必多说，近来火出圈的DeepSeek-V3便是MoE架构，传言GPT-5也是MoE架构，国内最近出的一些模型（Qwen3系列相关）也有不少用上了MoE。然而，虽然MoE的研究由来已久，但其应用长时间内都不愠不火，大致上是从去年初的《Mixtral of Experts》开始，MoE才逐渐吸引大家的注意力，其显著优点是参数量大，但训练和推理成本都显著低。\n但同时MoE也有一些难题，如训练不稳定、负载不均衡、效果不够好等，这也是它早年没有流行起来的主要原因。不过随着这两年关注度的提升，这些问题在很大程度上已经得到解决，我们在接下来的介绍中会逐一谈到这些内容。\n问题定义 我们知道，Transformer模型由Attention层和MLP层组成，MoE替换的是模型中MLP层。MLP层又分FFN（FeedForward Network）和GLU（Gated Linear Unit）两种，主流的是GLU，但简单起见我们还是以FFN为例：\n$$y=f(xW^{(A)})W^{(B)}$$其中$x\\in\\mathbb{R}^d$ 是输入向量（行向量），$W^{(A)}\\in\\mathbb{R}^{d\\times{D}}$, $W^{(B)}\\in\\mathbb{R}^{D\\times{d}}$ 是两个参数矩阵，$f$是Element-wise的激活函数，设$n$是一个能整除$D$的整数，那么上面的FFN可以用分块矩阵等价：\n$$ \\begin{equation}\\boldsymbol{y} = f\\big(\\boldsymbol{x}\\begin{bmatrix}\\boldsymbol{W}^{(A)}_1 \u0026 \\boldsymbol{W}^{(A)}_2 \u0026 \\cdots \u0026 \\boldsymbol{W}^{(A)}_n\\end{bmatrix}\\big)\\begin{bmatrix}\\boldsymbol{W}^{(B)}_1 \\\\ \\boldsymbol{W}^{(B)}_2 \\\\ \\vdots \\\\ \\boldsymbol{W}^{(B)}_n\\end{bmatrix} = \\sum_{i=1}^n \\underbrace{f(\\boldsymbol{x}\\boldsymbol{W}^{(A)}_i)\\boldsymbol{W}^{(B)}_i}_{\\boldsymbol{v}_i}\\end{equation} $$ 其中\n$W^{(A)}_i = W^{(A)}_{[:,(i-1)c:ic]}$, $W^{(B)}_i = W^{(B)}_{[(i-1)c:ic,:]}$, $c= D/n$，这里的切片按照Python规则来。由此可见，FFN可以等价表示成n个向量\n$\\boldsymbol{v}_1,\\boldsymbol{v}_2,\\cdots,\\boldsymbol{v}_n$\n之和，每个向量代表了一个小模型$f(\\boldsymbol{x}\\boldsymbol{W}^{(A)}_i)\\boldsymbol{W}^{(B)}_i$的输出，每个小模型计算量相同，这些小模型就是MoE中的“Expert”。\nMoE提出的问题是：\n能否只挑k个向量的和来逼近n个向量的和呢？这样就可以将计算量降低到k/n了。\n模长排序 要解决上述的问题，实质上是要解决低秩近似的问题，数学公式就是:\n$$\\begin{equation}\\mathop{\\text{argmin}}_{\\lambda_1,\\lambda_2,\\cdots,\\lambda_n\\in\\{0,1\\}}\\left\\Vert\\sum_{i=1}^n \\lambda_i \\boldsymbol{v}_i - \\sum_{i=1}^n\\boldsymbol{v}_i\\right\\Vert^2\\quad\\text{s.t.}\\quad \\sum_{i=1}^n \\lambda_i = k\\end{equation}$$ 记$\\gamma_i = 1 - \\lambda_i$，那么它又可以写成：\n$$\\begin{equation}\\mathop{\\text{argmin}}_{\\gamma_1,\\gamma_2,\\cdots,\\gamma_n\\in\\{0,1\\}}\\left\\Vert\\sum_{i=1}^n \\gamma_i \\boldsymbol{v}_i\\right\\Vert^2\\quad\\text{s.t.}\\quad \\sum_{i=1}^n \\gamma_i = n - k\\end{equation}$$ 这个问题的精确求解是比较困难的（NP Hard），但有一个简单的近似解：当$v_i$两两正交时，我们有\n$$\\begin{equation}\\left\\Vert\\sum_{i=1}^n \\gamma_i \\boldsymbol{v}_i\\right\\Vert^2 = \\sum_{i=1}^n \\gamma_i^2 \\Vert\\boldsymbol{v}_i\\Vert^2 = \\sum_{i=1}^n \\gamma_i \\Vert\\boldsymbol{v}_i\\Vert^2\\end{equation}$$ 上式最优解显然就是让模长$\\Vert\\boldsymbol{v}_i\\Vert$最小的$n-k$个$\\gamma_i$等于1，这又等价于说挑出模长最大的$k$个向量来逼近$n$个向量之和。当$v_i$不满足两两正交的条件时，我们依然用它来作为一个近似解。它的几何意义也很直观，模长越大的向量，在求和过程中越不容易被抵消，从而作用越突出。\nMoE初现 现在策略已经有了——“挑模长最大的$k$个向量”——可是细想之下我们会发现它并不实用：要挑模长最大的$k$个向量，就得把所有向量的模长都算出来，这又意味着要把所有的$\\boldsymbol{v}_i$先算出来，可我们的原本目的却是减少$v_i$的计算量！\n为了解决这个矛盾，我们需要重新设计每个Expert模型，使得它的模长可以低成本地计算出来。什么意思呢？首先我们将$v_i$归一化得到$\\boldsymbol{e}_i = \\boldsymbol{v}_i/\\Vert\\boldsymbol{v}_i\\Vert$，这样每个$e_i$的模长都相同了。接着我们定义\n$$\\begin{equation}\\underbrace{[\\rho_1,\\rho_2,\\cdots,\\rho_n]}_{\\boldsymbol{\\rho}} = h(\\boldsymbol{x}\\boldsymbol{W}^{(R)})\\quad\\in\\mathbb{R}_{\\geq 0}^n\\end{equation}$$ 其中$\\boldsymbol{W}^{(R)}\\in\\mathbb{R}^{d\\times n}$是参数矩阵，$h(\\cdot)$是一个$\\mathbb{R}\\to\\mathbb{R}_{\\geq 0}$的激活函数，说白了这就是一个$d$维到$n$维的线性变换加激活函数，所以计算量是比较小的，这部分模型在MoE中被称为“Router”。\n$\\boldsymbol{\\rho}$的作用是什么呢？预测每个Expert的模长！换言之，我们将$\\rho_i$作为第$i$个Expert的模长，$\\rho_i \\boldsymbol{e}_i$才是完整的Expert，它被分解为两部分：计算量比较小的模长$\\rho_i$以及计算量比较大的方向$\\boldsymbol{e}_i$。为了减少计算量，我们先计算出$\\boldsymbol{\\rho}$，挑出最大的$k$个后才去计算相应的$e_i$，最后乘上$\\rho_i$并求和：\n$$\\begin{equation}\\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{e}_i\\end{equation}$$ 这便是MoE模型的基本公式。由于计算中只保留了Top-$k$部分，所以它本质上属于一种Sparse模型，而原本的FFN或者$k=n$时的模型，通常称为对应的Dense模型。\n思路概括 我们再来整理一下整个思路：\n1、一个常规的Dense模型FFN，可以等价改写为$n$个Expert向量$\\boldsymbol{v}_1,\\boldsymbol{v}_2,\\cdots,\\boldsymbol{v}_n$之和;\n2、为了节省计算量，我们试图挑出$k$个向量求和来逼近原本的$n$个向量之和 ;\n3、转化为数学问题求解后，我们发现挑选规则是模长最大的$k$个向量；\n4、直接去算$n$个Expert的模长然后选$k$个实际上是不省计算量的，所以要重新设计Expert；\n5、将$\\boldsymbol{v}_i$归一化得到$\\boldsymbol{e}_i$，然后用另外的小模型（Router）预测模长$\\rho_i$，最终的Expert为$\\rho_i \\boldsymbol{e}_i$；\n6、此时，我们就可以先算全体$\\rho_i$，挑出$k$个后才去计算$\\boldsymbol{e}_i$，达到节省计算量的目的\n为何如此 可能有些读者疑问，为什么要做这个看似复杂的过程？原本的MoE不是挺好理解的吗？一般的MoE形式为\n$$\\begin{equation}\\boldsymbol{y} = \\sum_{i\\in \\mathop{\\text{argtop}}_k \\boldsymbol{\\rho}} \\rho_i \\boldsymbol{v}_i\\end{equation}$$\n也就是求和前少了对$\\boldsymbol{v}_i$的归一化，此时$\\rho_i$也没有模长的意义，它纯粹是一个用来对Expert排序的打分模型（即Router）。可为什么将$\\rho_i$乘到Expert上去就能让Router学会正确排序Expert呢？笔者发现只有《Sparse Backpropagation for MoE Training》对此给出了一个解释，但还是稍欠直观。\n而在本文的几何视角下，我们会发现很多问题就“豁然开朗”了。我们将Expert重新参数化为$\\rho_i \\boldsymbol{e}_i$后，Dense模型对应于全体$\\rho_i \\boldsymbol{e}_i$求和，而MoE对应于$\\rho_i$选Top-$k$后求和，这是Dense模型的一个有理论保证的逼近。我们没有去考虑Router如何选择Expert，只是每一步都尽可能逼近Dense模型，这可以说是既要大参数、又要小计算量的最佳选择。\n现在$\\rho_i$的几何意义是模长而不是概率，所以激活函数$h(\\cdot)$就没有归一化的要求了，除了Softmax外，像Sigmoid、ReLU都可以考虑使用，也可以考虑我们在《Softmax后传：寻找Top-K的光滑近似》介绍的Top-$k$光滑近似。Router使用非归一化的激活函数，有助于避免$k \u0026gt; 1$时Expert之间的恶性竞争，有时候能取得更好的效果。\n最后补充一点，我们前面定义$\\boldsymbol{e}_i = \\boldsymbol{v}_i/ \\Vert\\boldsymbol{v}_i\\Vert$，目的是让所有$\\boldsymbol{e}_i$模长相同，实际操作中不是一定要L2 Normalize，也可以是其他等价操作，比如gamma参数恒等于1的RMS Norm，它更符合我们的输出习惯。\n文章小结 本文从Dense模型的最佳逼近出发来推导和理解MoE，得到了一种特定的MoE形式，它比现有MoE多了一个Normalize步骤，但能让MoE的几何意义更加明显。当然，不管Normalize与否，MoE之路都只是刚刚开始，更多的困难还在路上。\n","permalink":"https://pillumina.github.io/posts/llmtheory/1-moe/","summary":"\u003cp\u003eMoE（Mixture of Experts）架构的流行自不必多说，近来火出圈的\u003ca href=\"https://papers.cool/arxiv/2412.19437\"\u003eDeepSeek-V3\u003c/a\u003e便是MoE架构，传言GPT-5也是MoE架构，国内最近出的一些模型（Qwen3系列相关）也有不少用上了MoE。然而，虽然MoE的研究由来已久，但其应用长时间内都不愠不火，大致上是从去年初的\u003ca href=\"https://papers.cool/arxiv/2401.04088\"\u003e《Mixtral of Experts》\u003c/a\u003e开始，MoE才逐渐吸引大家的注意力，其显著优点是参数量大，但训练和推理成本都显著低。\u003c/p\u003e\n\u003cp\u003e但同时MoE也有一些难题，如训练不稳定、负载不均衡、效果不够好等，这也是它早年没有流行起来的主要原因。不过随着这两年关注度的提升，这些问题在很大程度上已经得到解决，我们在接下来的介绍中会逐一谈到这些内容。\u003c/p\u003e\n\u003ch3 id=\"问题定义\"\u003e问题定义\u003c/h3\u003e\n\u003cp\u003e我们知道，Transformer模型由Attention层和MLP层组成，MoE替换的是模型中MLP层。MLP层又分FFN（FeedForward Network）和GLU（Gated Linear Unit）两种，主流的是GLU，但简单起见我们还是以FFN为例：\u003c/p\u003e\n$$y=f(xW^{(A)})W^{(B)}$$\u003cp\u003e其中$x\\in\\mathbb{R}^d$ 是输入向量（行向量），$W^{(A)}\\in\\mathbb{R}^{d\\times{D}}$, $W^{(B)}\\in\\mathbb{R}^{D\\times{d}}$ 是两个参数矩阵，$f$是\u003ccode\u003eElement-wise\u003c/code\u003e的激活函数，设$n$是一个能整除$D$的整数，那么上面的FFN可以用分块矩阵等价：\u003cbr\u003e\n\n$$ \\begin{equation}\\boldsymbol{y} = f\\big(\\boldsymbol{x}\\begin{bmatrix}\\boldsymbol{W}^{(A)}_1 \u0026 \\boldsymbol{W}^{(A)}_2 \u0026 \\cdots \u0026 \\boldsymbol{W}^{(A)}_n\\end{bmatrix}\\big)\\begin{bmatrix}\\boldsymbol{W}^{(B)}_1 \\\\ \\boldsymbol{W}^{(B)}_2 \\\\ \\vdots \\\\ \\boldsymbol{W}^{(B)}_n\\end{bmatrix} = \\sum_{i=1}^n \\underbrace{f(\\boldsymbol{x}\\boldsymbol{W}^{(A)}_i)\\boldsymbol{W}^{(B)}_i}_{\\boldsymbol{v}_i}\\end{equation} $$\n\u003c/p\u003e\n\u003cp\u003e其中\u003cbr\u003e\n$W^{(A)}_i = W^{(A)}_{[:,(i-1)c:ic]}$, $W^{(B)}_i = W^{(B)}_{[(i-1)c:ic,:]}$, $c= D/n$，这里的切片按照Python规则来。由此可见，FFN可以等价表示成n个向量\u003cbr\u003e\n$\\boldsymbol{v}_1,\\boldsymbol{v}_2,\\cdots,\\boldsymbol{v}_n$\u003cbr\u003e\n之和，每个向量代表了一个小模型$f(\\boldsymbol{x}\\boldsymbol{W}^{(A)}_i)\\boldsymbol{W}^{(B)}_i$的输出，每个小模型计算量相同，这些小模型就是MoE中的“Expert”。\u003c/p\u003e\n\u003cp\u003eMoE提出的问题是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e能否只挑k个向量的和来逼近n个向量的和呢？这样就可以将计算量降低到k/n了。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch3 id=\"模长排序\"\u003e模长排序\u003c/h3\u003e\n\u003cp\u003e要解决上述的问题，实质上是要解决\u003cstrong\u003e低秩近似\u003c/strong\u003e的问题，数学公式就是:\u003cbr\u003e\n\n$$\\begin{equation}\\mathop{\\text{argmin}}_{\\lambda_1,\\lambda_2,\\cdots,\\lambda_n\\in\\{0,1\\}}\\left\\Vert\\sum_{i=1}^n \\lambda_i \\boldsymbol{v}_i - \\sum_{i=1}^n\\boldsymbol{v}_i\\right\\Vert^2\\quad\\text{s.t.}\\quad \\sum_{i=1}^n \\lambda_i = k\\end{equation}$$ \n\u003cbr\u003e\n记$\\gamma_i = 1 - \\lambda_i$，那么它又可以写成：\u003cbr\u003e\n\n$$\\begin{equation}\\mathop{\\text{argmin}}_{\\gamma_1,\\gamma_2,\\cdots,\\gamma_n\\in\\{0,1\\}}\\left\\Vert\\sum_{i=1}^n \\gamma_i \\boldsymbol{v}_i\\right\\Vert^2\\quad\\text{s.t.}\\quad \\sum_{i=1}^n \\gamma_i = n - k\\end{equation}$$\n\u003cbr\u003e\n这个问题的精确求解是比较困难的（NP Hard），但有一个简单的近似解：当$v_i$\u003cstrong\u003e两两正交\u003c/strong\u003e时，我们有\u003cbr\u003e\n\n$$\\begin{equation}\\left\\Vert\\sum_{i=1}^n \\gamma_i \\boldsymbol{v}_i\\right\\Vert^2 = \\sum_{i=1}^n \\gamma_i^2 \\Vert\\boldsymbol{v}_i\\Vert^2 = \\sum_{i=1}^n \\gamma_i \\Vert\\boldsymbol{v}_i\\Vert^2\\end{equation}$$\n\u003cbr\u003e\n上式最优解显然就是让模长$\\Vert\\boldsymbol{v}_i\\Vert$最小的$n-k$个$\\gamma_i$等于1，这又等价于说挑出模长最大的$k$个向量来逼近$n$个向量之和。当$v_i$不满足两两正交的条件时，我们依然用它来作为一个\u003cstrong\u003e近似解\u003c/strong\u003e。它的几何意义也很直观，\u003cstrong\u003e模长越大的向量，在求和过程中越不容易被抵消，从而作用越突出\u003c/strong\u003e。\u003c/p\u003e","title":"MoE环游记：1、从几何意义出发"},{"content":" https://github.com/THUDM/slime\n一个异步实现但是非完全异步的RL框架\n总体架构 从源码模块划分，有三大核心模块： training（Megatron）：主训练流程，负责模型参数更新。 rollout（SGLang + router）：负责采样、奖励/验证生成，产生训练数据。 data buffer：桥接训练与采样，管理数据流、缓存与生成方式。 分布式调度：关于资源分配、actor启动、任务调度都由于Ray管理，支持异步训练和采样 插件机制：支持自定义buffer、模型、模型格式转换（mbridge） flowchart LR subgraph Ray[Ray 分布式调度] A1[Actor Group\u0026lt;br\u0026gt;训练 Actor] A2[Rollout Group\u0026lt;br\u0026gt;采样/生成 Actor] A3[Placement Group\u0026lt;br\u0026gt;资源分配] end subgraph Training[Training \u0026lt;Megatron\u0026gt;] T1[模型训练] T2[权重同步] T3[评估/保存] end subgraph Rollout[Rollout \u0026lt;SGLang+Router\u0026gt;] R1[采样/生成] R2[奖励模型] R3[过滤器] end subgraph Buffer[Data Buffer] B1[数据缓存] B2[数据流转] B3[Offload/Onload] end subgraph Plugins[插件机制] P1[Buffer 插件] P2[Model 插件] P3[mbridge 格式转换] end A1--\u0026gt;|训练数据|B1 A2--\u0026gt;|生成数据|B1 B1--\u0026gt;|数据流|A1 B1--\u0026gt;|数据流|A2 A1--\u0026gt;|权重同步|A2 A1--\u0026gt;|评估/保存|T3 A2--\u0026gt;|采样/奖励/过滤|R1 R1--\u0026gt;|奖励|R2 R1--\u0026gt;|过滤|R3 B1--\u0026gt;|插件扩展|P1 A1--\u0026gt;|模型扩展|P2 A1--\u0026gt;|格式转换|P3 A3--\u0026gt;|资源分配|A1 A3--\u0026gt;|资源分配|A2 各模块视角的关系图 slime/rollout 组件图 rollout 负责采样、奖励、过滤，支持多种采样/奖励/过滤策略。\nflowchart TD AR[agent_rollout.py\u0026lt;br\u0026gt;采样主逻辑] SE[sglang_example.py\u0026lt;br\u0026gt;SGLang采样示例] SF[sft_example.py\u0026lt;br\u0026gt;有监督微调采样] RM[rm_hub/\u0026lt;br\u0026gt;奖励模型集] FH[filter_hub/\u0026lt;br\u0026gt;过滤器集] AR--\u0026gt;|调用|SE AR--\u0026gt;|调用|SF AR--\u0026gt;|奖励|RM AR--\u0026gt;|过滤|FH agent_rollout.py：采样主流程，调度 SGLang、奖励模型、过滤器。\nsglang_example.py/sft_example.py：采样实现示例。\nrm_hub/：奖励模型集合。\nfilter_hub/：过滤器集合。\nslime/ray 组件图 ray 负责分布式 actor、buffer、PPO 训练、资源分配。\nflowchart TD PG[placement_group.py\u0026lt;br\u0026gt;资源分配] AG[ray_actor.py\u0026lt;br\u0026gt;Actor基类] PA[ppo_actor.py\u0026lt;br\u0026gt;PPO训练Actor] RO[rollout.py\u0026lt;br\u0026gt;Rollout Actor] BU[buffer.py\u0026lt;br\u0026gt;数据Buffer] UT[utils.py\u0026lt;br\u0026gt;工具函数] PG--\u0026gt;|分配|AG AG--\u0026gt;|继承|PA AG--\u0026gt;|继承|RO PA--\u0026gt;|训练数据|BU RO--\u0026gt;|生成数据|BU BU--\u0026gt;|数据流|PA BU--\u0026gt;|数据流|RO slime/backends 组件图 后端适配，支持 Megatron、SGLang。\nflowchart TD MEG[megatron_utils/\u0026lt;br\u0026gt;Megatron适配] SGL[sglang_utils/\u0026lt;br\u0026gt;SGLang适配] MEG--\u0026gt;|接口|训练/采样 SGL--\u0026gt;|接口|训练/采样 slime/utils 组件图 工具、参数、类型、分布式、数据等通用功能。\nflowchart TD AR[arguments.py\u0026lt;br\u0026gt;参数解析] DT[data.py\u0026lt;br\u0026gt;数据工具] TY[types.py\u0026lt;br\u0026gt;类型定义] PU[ppo_utils.py\u0026lt;br\u0026gt;PPO工具] SU[seqlen_balancing.py\u0026lt;br\u0026gt;序列长度平衡] TU[timer.py\u0026lt;br\u0026gt;计时] DU[distributed_utils.py\u0026lt;br\u0026gt;分布式工具] FU[flops_utils.py\u0026lt;br\u0026gt;FLOPs工具] HU[http_utils.py\u0026lt;br\u0026gt;HTTP工具] MU[mask_utils.py\u0026lt;br\u0026gt;掩码工具] MEM[memory_utils.py\u0026lt;br\u0026gt;内存工具] MI[misc.py\u0026lt;br\u0026gt;杂项] AU[async_utils.py\u0026lt;br\u0026gt;异步工具] AR--\u0026gt;|参数|主流程 DT--\u0026gt;|数据|主流程 TY--\u0026gt;|类型|主流程 PU--\u0026gt;|PPO|训练 SU--\u0026gt;|平衡|训练 TU--\u0026gt;|计时|训练/采样 DU--\u0026gt;|分布式|训练/采样 FU--\u0026gt;|FLOPs|训练 HU--\u0026gt;|HTTP|采样 MU--\u0026gt;|掩码|训练 MEM--\u0026gt;|内存|训练 MI--\u0026gt;|杂项|主流程 AU--\u0026gt;|异步|主流程 slime_plugins/models 组件图 模型插件，支持不同模型适配.\nflowchart TD GLM[glm4.py\u0026lt;br\u0026gt;GLM4模型适配] GLM--\u0026gt;|模型接口|主流程 slime_plugins/mbridge 组件图 模型格式转换插件。\nflowchart TD GLM[glm4.py\u0026lt;br\u0026gt;GLM4格式转换] GLM--\u0026gt;|格式转换|主流程 关键类角度的实现关系 全局视角 classDiagram %% 主入口和配置 class MainTrain { +main() +create_placement_groups() +create_actor_group() +create_rollout_group() } class Arguments { +colocate: bool +offload: bool +actor_num_nodes: int +rollout_num_gpus: int +hf_checkpoint: str +rollout_function_path: str } %% Ray Actor 基类 class RayActor { +_get_current_node_ip_and_free_port() +get_master_addr_and_port() } %% 核心数据类 class Sample { +index: int +prompt: str +tokens: list[int] +response: str +response_length: int +reward: float +loss_mask: list[int] +status: Status +metadata: dict } class ParamInfo { +name: str +dtype: torch.dtype +shape: torch.Size +attrs: dict +size: int +src_rank: int } %% 训练相关类 class TrainRayActor { +args: Arguments +model: list +ref: list +old_actor: list +data_buffer: Buffer +rollout_engines: list +init() +train() +eval() +update_weights() +sleep() +wake_up() } class RayTrainGroup { +_actor_handlers: list[TrainRayActor] +async_init() +async_train() +async_eval() +async_update_weights() } %% 采样相关类 class RolloutRayActor { +args: Arguments +rank: int +infer_engine: SglangEngine +init() +update_weights_from_distributed() +update_weights_from_tensor() +reset_prefix_cache() +sleep() +wake_up() } class RolloutGroup { +args: Arguments +data_buffer: Buffer +rollout_engines: list[RolloutRayActor] +rollout_engine_lock: Lock +async_init() +async_generate() +async_reset_prefix_cache() } %% 数据管理类 class Buffer { +args: Arguments +dataset: JsonlDataset +buffer: list[list[Sample]] +train_data_pool: dict +eval_data_pool: dict +metadata: dict +get_samples() +add_samples() +generate() +get_data() +save() +load() } class JsonlDataset { +samples: list[Sample] +shuffle() +__len__() } %% 后端引擎类 class SglangEngine { +args: Arguments +rank: int +llm: HttpServerEngineAdapter +init_process_group() +update_weights_from_distributed() +update_weights_from_tensor() +reset_prefix_cache() +sleep() +wake_up() } class HttpServerEngineAdapter { +router_ip: str +router_port: int +init_weights_update_group() +update_weights_from_distributed() +update_weights_from_tensor() +flush_cache() } %% 插件类 class RolloutBuffer { +buffer: BufferQueue +lock: RLock +visualizer: BufferStatsVisualizer +write() +read() +peek() +get_stats() +close() } class BufferQueue { +group_size: int +max_buffer_size: int +append() +popleft() +get_batch() +__len__() } %% 工具类 class CuMemAllocator { +get_instance() +sleep() +wake_up() } class PlacementGroup { +bundles: list +strategy: str +ready() } %% 继承关系 RayActor \u0026lt;|-- TrainRayActor RayActor \u0026lt;|-- RolloutRayActor %% 聚合关系 MainTrain --\u0026gt; Arguments MainTrain --\u0026gt; RayTrainGroup MainTrain --\u0026gt; RolloutGroup MainTrain --\u0026gt; PlacementGroup RayTrainGroup --\u0026gt; TrainRayActor : contains RolloutGroup --\u0026gt; RolloutRayActor : contains RolloutGroup --\u0026gt; Buffer : contains TrainRayActor --\u0026gt; Buffer : uses TrainRayActor --\u0026gt; SglangEngine : connects to RolloutRayActor --\u0026gt; SglangEngine : contains SglangEngine --\u0026gt; HttpServerEngineAdapter : contains Buffer --\u0026gt; Sample : manages Buffer --\u0026gt; JsonlDataset : uses TrainRayActor --\u0026gt; CuMemAllocator : uses TrainRayActor --\u0026gt; ParamInfo : manages %% 插件关系 RolloutBuffer --\u0026gt; BufferQueue : contains RolloutBuffer --\u0026gt; BufferStatsVisualizer : contains %% 依赖关系 Arguments --\u0026gt; TrainRayActor : configures Arguments --\u0026gt; RolloutRayActor : configures Arguments --\u0026gt; Buffer : configures Arguments --\u0026gt; SglangEngine : configures %% 数据流关系 Sample --\u0026gt; Buffer : stored in Buffer --\u0026gt; TrainRayActor : provides data Buffer --\u0026gt; RolloutRayActor : receives data %% 权重同步关系 TrainRayActor --\u0026gt; RolloutRayActor : syncs weights SglangEngine --\u0026gt; HttpServerEngineAdapter : syncs weights 主流程精简版 classDiagram %% PPO 核心流程类 class MainTrain { +main() +create_placement_groups() +create_actor_group() +create_rollout_group() } class Arguments { +colocate: bool +offload: bool +actor_num_gpus_per_node: int +rollout_num_gpus: int +hf_checkpoint: str +rollout_function_path: str } %% 核心数据类 class Sample { +index: int +prompt: str +tokens: list[int] +response: str +response_length: int +reward: float +loss_mask: list[int] +status: Status } %% 训练 Actor class TrainRayActor { +model: list +ref: list +data_buffer: Buffer +rollout_engines: list +train() +eval() +update_weights() +sleep() +wake_up() } class RayTrainGroup { +_actor_handlers: list[TrainRayActor] +async_train() +async_eval() +async_update_weights() } %% 采样 Actor class RolloutRayActor { +infer_engine: SglangEngine +update_weights_from_distributed() +update_weights_from_tensor() +reset_prefix_cache() } class RolloutGroup { +data_buffer: Buffer +rollout_engines: list[RolloutRayActor] +async_generate() } %% 数据管理 class Buffer { +buffer: list[list[Sample]] +train_data_pool: dict +eval_data_pool: dict +get_samples() +add_samples() +generate() +get_data() +save() +load() } %% 推理引擎 class SglangEngine { +llm: HttpServerEngineAdapter +update_weights_from_distributed() +update_weights_from_tensor() +reset_prefix_cache() } %% 内存管理 class CuMemAllocator { +sleep() +wake_up() } %% 继承关系 TrainRayActor --\u0026gt; RayActor RolloutRayActor --\u0026gt; RayActor %% PPO 核心流程关系 MainTrain --\u0026gt; Arguments MainTrain --\u0026gt; RayTrainGroup MainTrain --\u0026gt; RolloutGroup RayTrainGroup --\u0026gt; TrainRayActor : contains RolloutGroup --\u0026gt; RolloutRayActor : contains RolloutGroup --\u0026gt; Buffer : contains TrainRayActor --\u0026gt; Buffer : uses TrainRayActor --\u0026gt; SglangEngine : syncs weights RolloutRayActor --\u0026gt; SglangEngine : contains TrainRayActor --\u0026gt; CuMemAllocator : uses %% 数据流关系 Sample --\u0026gt; Buffer : stored in Buffer --\u0026gt; TrainRayActor : provides training data Buffer --\u0026gt; RolloutRayActor : receives generated data 关于异步实现的方式 需要注意的问题：\nslime的RL训练是rollout_id同步，不是完全的异步训练（即推理可以不等待训练完成，或者训练可以不等待推理完成）。 权重同步在每个rollout训练完成后立刻执行，确保下一个rollout使用最新权重。 1 2 3 4 5 6 7 8 9 10 # train.py 主循环 - 实际上是同步的 for rollout_id in range(args.start_rollout_id, args.num_rollout): # 1. 等待采样完成 ray.get(rollout_generator.async_generate(rollout_id)) # 2. 等待训练完成 ray.get(actor_model.async_train(rollout_id)) # 3. 等待权重同步完成 ray.get(actor_model.async_update_weights()) 虽然使用 Ray actor 的异步方法，但主循环用 ray.get() 等待每个步骤完成\n每个 rollout_id 必须按顺序完成：采样 → 训练 → 权重同步\n不是 rollout 一直生成、train actor 一直消费的完全异步模式\n因此slime中异步的边界是内部的异步优化，多个actor分布式并行训练，但是主循环还是等待所有都要完成。\n权重如何同步 权重同步流程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # RayTrainGroup.async_update_weights def async_update_weights(self): \u0026#34;\u0026#34;\u0026#34;Broadcast weights from rank 0 to all other ranks.\u0026#34;\u0026#34;\u0026#34; return [actor.update_weights.remote() for actor in self._actor_handlers] # TrainRayActor.update_weights @timer def update_weights(self): if self.args.debug_train_only or self.args.debug_rollout_only: return torch.cuda.empty_cache() if not self.args.colocate: self.update_weights_from_distributed() # 分布式模式 else: self.update_weights_from_tensor() # 张量模式 dist.barrier() clear_memory() print_memory(\u0026#34;after update_weights\u0026#34;) 以及权重同步的两种模式：\n其中分布式模式适用于多节点分布式训练，利用高效的集合通信，比如大规模模型训练、需要跨节点权重同步的。\n张量模式无网络依赖，延迟低。适合单机多进程训练，内存充足（利用共享内存，传输快）的场景。适合中小规模模型、且对网络延迟比较敏感的场景。\n分析一下不同场景的延迟来源:\n$$Latency_{distributed} = Latency_{net} + Time_{serialize} + Time_{broadcast}$$\n$$Latency_{tensor} = Time_{memcopy} + Time_{serialize} + Time_{ipctransfer}$$ 分布式模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def update_weights_from_distributed(self): # 1. 暂停 rollout engines if dist.get_rank() == 0: ray.get([engine.pause_generation.remote() for engine in self.rollout_engines]) ray.get([engine.reset_prefix_cache.remote() for engine in self.rollout_engines]) dist.barrier() # 2. 通过 NCCL 广播权重 buffer_size = 0 converted_named_tensors = [] for name, param in update_weight_utils.named_parameters(self.args, self.model): param = update_weight_utils.all_gather_param(name, param) param = update_weight_utils.remove_padding(name, param, self.vocab_size) if buffer_size + param_size \u0026gt; self.args.update_weight_buffer_size: self._update_param_from_distributed(converted_named_tensors) buffer_size = 0 converted_named_tensors += update_weight_utils.convert_to_hf( self.args, self.model_name, name, param, self.quantization_config ) buffer_size += param_size # 3. 恢复 rollout engines if dist.get_rank() == 0: ray.get([engine.continue_generation.remote() for engine in self.rollout_engines]) dist.barrier() 张量模式 训练GPU -\u0026gt; PCIe -\u0026gt; CPU内存 -\u0026gt; 序列化 -\u0026gt; 共享内存 -\u0026gt; 反序列化 -\u0026gt; CPU内存 -\u0026gt; PCIe -\u0026gt; rollout GPU\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 def update_weights_from_tensor(self): # 1. 重置 prefix cache if rank == 0: ray.get([engine.reset_prefix_cache.remote() for engine in self.rollout_engines]) dist.barrier() # 2. 通过 IPC 共享内存传输权重 for param_infos in self.param_info_buckets: params = [] for info in param_infos: if dist.get_rank() == info.src_rank: params.append(torch.nn.Parameter(self.params_dict[info.name].to(device=torch.cuda.current_device()))) else: params.append(torch.empty(info.shape, dtype=info.dtype, device=torch.cuda.current_device())) # 广播参数 if pp_size \u0026gt; 1: handles = [] for info, param in zip(param_infos, params): handles.append(torch.distributed.broadcast(param, src=info.src_rank, group=mpu.get_pipeline_model_parallel_group(), async_op=True)) for handle in handles: handle.wait() # 3. 通过 IPC 传输到 rollout engines converted_named_tensors = [] for info, param in zip(param_infos, params): param = update_weight_utils.all_gather_param(info.name, param) param = update_weight_utils.remove_padding(info.name, param, self.vocab_size) converted_named_tensors.extend(update_weight_utils.convert_to_hf( self.args, self.model_name, info.name, param, self.quantization_config )) self._update_converted_params_from_tensor(converted_named_tensors) def _update_converted_params_from_tensor(self, converted_named_tensors): # 序列化权重并通过 IPC 传输 ipc_handle = MultiprocessingSerializer.serialize(converted_named_tensors, output_str=True) ipc_handles = [None] * dist.get_world_size(self._ipc_gather_group) if self._ipc_gather_src == dist.get_rank() else None dist.gather_object(ipc_handle, object_gather_list=ipc_handles, dst=self._ipc_gather_src, group=self._ipc_gather_group) if dist.get_rank() == self._ipc_gather_src: ref = self._ipc_engine.update_weights_from_tensor.remote(ipc_handles=ipc_handles) ray.get(ref) 训练数据流转角度 train.py主流程中训练、采样数据流转、权重同步时序：\nsequenceDiagram participant User participant Main participant Ray participant ActorGroup participant RolloutGroup participant DataBuffer User-\u0026gt;\u0026gt;Main: start training Main-\u0026gt;\u0026gt;Ray: create placement groups Ray-\u0026gt;\u0026gt;ActorGroup: launch training actors Ray-\u0026gt;\u0026gt;RolloutGroup: launch rollout actors Main-\u0026gt;\u0026gt;ActorGroup: initialize model/weights Main-\u0026gt;\u0026gt;RolloutGroup: initialize rollout/data buffer loop for each rollout_id Main-\u0026gt;\u0026gt;RolloutGroup: async_generate(rollout_id) RolloutGroup-\u0026gt;\u0026gt;DataBuffer: write new data Main-\u0026gt;\u0026gt;ActorGroup: async_train(rollout_id) ActorGroup-\u0026gt;\u0026gt;DataBuffer: read training data ActorGroup--\u0026gt;\u0026gt;Main: training done Main-\u0026gt;\u0026gt;ActorGroup: async_update_weights() alt evaluation or save needed Main-\u0026gt;\u0026gt;ActorGroup: async_eval/async_save_model end end 关于data buffer 在slime/ray/buffer.py下，实现为Ray actor(@ray.remote class Buffer)，支持高效的本地缓存和流转，数据在传输时使用Ray的对象存储，数据结构保存在Ray actor的进程内存中。\n主要功能 数据缓存与流转：缓存采样生成的数据，供训练 actor 消费，实现采样与训练的解耦。\n支持多种数据源：可从全局数据集（如 prompt 数据）或采样生成数据中获取样本。\n数据分组与批处理：每组样本可包含多个 prompt/response，便于批量训练和采样。\n元数据与状态管理：支持元数据、epoch、样本索引等状态的保存与恢复。\n支持 offload/onload：可将 buffer 状态保存到本地/远程，支持断点续训和分布式场景。\n详细类图 classDiagram class Buffer { - args - buffer : list\u0026lt;list\u0026lt;Sample\u0026gt;\u0026gt; - buffer_filter - train_data_pool : dict - eval_data_pool : dict - epoch_id : int - sample_index : int - sample_offset : int - metadata : dict - dataset : JsonlDataset | None - generate_rollout - eval_generate_rollout + __init__(args) + get_num_rollout_per_epoch() + get_samples(num_samples) + add_samples(samples) + generate(rollout_id, evaluation) + get_data(rollout_id, evaluation) + save(rollout_id) + load(rollout_id) + update_metadata(metadata) + get_metadata() + get_buffer_length() } class Sample { +index +tokens +response_length +reward +rewards +status +loss_mask +metadata } class JsonlDataset { +samples : list\u0026lt;Sample\u0026gt; +shuffle(epoch_id) } Buffer o-- \u0026#34;list\u0026lt;list\u0026gt;\u0026#34; Sample Buffer o-- JsonlDataset Buffer ..\u0026gt; buffer_filter : uses Buffer ..\u0026gt; generate_rollout : uses Buffer ..\u0026gt; eval_generate_rollout : uses Buffer和主流程关键模块的关联图 flowchart TD subgraph RayActors TrainActor[训练 Actor \u0026lt;PPOActor\u0026gt;] RolloutActor[采样 Actor \u0026lt;RolloutGroup\u0026gt;] end BufferInst[Buffer \u0026lt;Ray actor\u0026gt;] Dataset[JsonlDataset] SampleObj[Sample] RolloutActor -- 生成数据 --\u0026gt; BufferInst BufferInst -- add_samples(samples) --\u0026gt; BufferInst TrainActor -- get_samples(num) --\u0026gt; BufferInst BufferInst -- get_samples 返回 list\u0026lt;list\u0026lt;Sample\u0026gt;\u0026gt; --\u0026gt; TrainActor BufferInst -- dataset.samples --\u0026gt; Dataset BufferInst -- 缓存/流转 --\u0026gt; SampleObj 数据流转过程举例 典型流程：采样生成 -\u0026gt; buffer缓存 -\u0026gt; 训练消费\n采样生成数据 RolloutActor（采样 actor）调用 buffer.generate(rollout_id)\ngenerate 方法会调用 generate_rollout 函数，生成一批样本（Sample 对象），如：\ndata = generate_rollout(args, rollout_id, buffer, evaluation=False) 生成的数据通过 set_data 写入 train_data_pool\n采样数据写入 buffer 采样 actor 也可以直接调用 buffer.add_samples(samples)\nsamples 是 list[list[Sample]]，每组样本对应一个 prompt 的多个采样\n训练 actor 获取数据 训练 actor（PPOActor）调用 buffer.get_samples(num_samples)\nget_samples 优先从 buffer（缓存队列）出队样本组，不足时从 dataset 生成\n返回 list[list[Sample]]，每组样本可直接用于训练\n训练 actor 消费数据 训练 actor 拿到样本后，进行训练、更新权重等操作 Buffer的generate职责以及如何实现弱耦合 Buffer 的 generate 方法本质上是调用外部采样/推理函数（如 SGLang、模型采样等），这些函数通过参数动态注入（如 args.rollout_function_path），所以只要采样接口一致，可以自己实现一个比如generate_rollout_vllm.py并在参数中指向它即可。 实现新的采样函数，其接口为： 1 2 3 4 def generate_rollout(args, rollout_id, buffer, evaluation=False): # 这里调用 vllm 的推理接口 samples = vllm_generate(args, rollout_id) return samples 在启动训练的时候，参数指定: --rollout_function_path path/to/generate_rollout_vllm.py:generate_rollout buffer中的伪代码可以表示为: 1 2 3 4 5 6 7 # buffer.py 内部 self.generate_rollout = load_function(self.args.rollout_function_path) def generate(self, rollout_id, evaluation=False): generate_fn = self.eval_generate_rollout if evaluation else self.generate_rollout data = generate_fn(self.args, rollout_id, self, evaluation=evaluation) self._set_data(data, evaluation=evaluation) 因此Buffer 只负责： 调用 generate_rollout（外部推理/采样后端）\n缓存采样得到的数据\n提供数据给训练 actor\n推理/采样的具体实现（如 SGLang、模型后端）完全在 generate_rollout 这样的外部函数里，Buffer 只是“调度者”和“缓存者”。 默认对SGLang的支持 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # sglang_example.py - 默认的 SGLang 采样实现 def generate_rollout(args, rollout_id, data_buffer, evaluation=False): \u0026#34;\u0026#34;\u0026#34;默认的 generate_rollout 函数，使用 SGLang 进行采样\u0026#34;\u0026#34;\u0026#34; assert args.rollout_global_dataset if evaluation: return run(eval_rollout(args, rollout_id)) return run(generate_rollout_async(args, rollout_id, data_buffer)) # 异步采样实现 async def generate_rollout_async(args, rollout_id: int, data_buffer) -\u0026gt; list[list[Sample]]: # 1. 从 buffer 获取 prompt 样本 samples = data_buffer.get_samples(args.over_sampling_batch_size) # 2. 提交 SGLang 生成任务 state.submit_generate_tasks(samples) # 3. 等待生成完成 done, state.pendings = await asyncio.wait(state.pendings, return_when=asyncio.FIRST_COMPLETED) # 4. 处理生成结果 for task in done: group = task.result() data.append(group) return data 训推与buffer的api层级的交互 rollout_id对应一次完整的采样-训练-评估循环\n数据流动方式为：\n采样：prompt 样本 → SGLang 生成 → 存入 pool\n训练：从 pool 获取 → 训练 → 删除\n评估：从 pool 获取 → 评估 → 删除\ndatabuffer中数据结构的作用区别：\nself.buffer：采样过程中的 prompt 缓存和样本管理\ntrain_data_pool/eval_data_pool：rollout 粒度的数据对齐和生命周期管理\n主流程(train.py) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def train(args): # 初始化 actor_model = create_actor_group(args, pgs[\u0026#34;actor\u0026#34;]) rollout_generator = create_rollout_group(args, pgs[\u0026#34;rollout\u0026#34;]) # 主循环 for rollout_id in range(args.start_rollout_id, args.num_rollout): # 训练采样 ray.get(rollout_generator.async_generate(rollout_id)) # 训练 ray.get(actor_model.async_train(rollout_id)) # 评估采样 ray.get(rollout_generator.async_generate(rollout_id, evaluation=True)) # 评估 ray.get(actor_model.async_eval(rollout_id)) Rollout Actor与Buffer交互 采样阶段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # RolloutGroup.async_generate def async_generate(self, rollout_id, evaluation=False): return self.data_buffer.generate.remote(rollout_id, evaluation=evaluation) # Buffer.generate def generate(self, rollout_id, evaluation=False): # 调用外部采样函数 generate_fn = self.eval_generate_rollout if evaluation else self.generate_rollout data = generate_fn(self.args, rollout_id, self, evaluation=evaluation) # 写入对应的 pool self._set_data(data, evaluation=evaluation) # Buffer._set_data def _set_data(self, data, evaluation=False): data_pool = self.eval_data_pool if evaluation else self.train_data_pool if not evaluation: data = self._convert_samples_to_train_data(data) # 转换为训练格式 data_pool[self.rollout_id] = data # 存入 pool 其中外部采样函数（以 sglang_example.py 为例） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # generate_rollout (sglang_example.py) def generate_rollout(args, rollout_id, data_buffer, evaluation=False): if evaluation: return run(eval_rollout(args, rollout_id)) # 从 buffer 获取 prompt 样本 samples = data_buffer.get_samples(args.over_sampling_batch_size) # 使用 SGLang 生成 response # ... 生成逻辑 ... # 返回生成的样本 return data # 在生成过程中可能还会调用 data_buffer.add_samples(samples) # 添加中间结果到 buffer Train Actor与Buffer交互 训练阶段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # RayTrainGroup.async_train def async_train(self, rollout_id, with_data_fetching=True): return [actor.train.remote(rollout_id, with_data_fetching=with_data_fetching) for actor in self._actor_handlers] # TrainRayActor.train def train(self, rollout_id, with_data_fetching=True): if with_data_fetching: self.get_rollout_data(rollout_id) # 获取训练数据 # ... 训练逻辑 ... # TrainRayActor.get_rollout_data def get_rollout_data(self, rollout_id): megatron_utils.process_rollout_data(rollout_id, self.args, self.data_buffer) # process_rollout_data (megatron_utils/data.py) def process_rollout_data(rollout_id, args, data_buffer): if rank == 0: # 从 buffer 获取训练数据 data = ray.get(data_buffer.get_data.remote(rollout_id)) dist.broadcast_object_list([data], src=0) else: data = [None] dist.broadcast_object_list(data, src=0) data = data[0] # 处理数据用于训练 # ... 数据预处理逻辑 ... # Buffer.get_data def get_data(self, rollout_id, evaluation=False): data_pool = self.train_data_pool if not evaluation else self.eval_data_pool assert rollout_id in data_pool data = data_pool[rollout_id] del data_pool[rollout_id] # 取出后删除 return data 评估阶段 1 2 3 4 5 6 7 8 9 10 11 12 13 # RayTrainGroup.async_eval def async_eval(self, rollout_id): return [actor.eval.remote(rollout_id) for actor in self._actor_handlers] # TrainRayActor.eval def eval(self, rollout_id): megatron_utils.log_eval_data(rollout_id, self.args, self.data_buffer) # log_eval_data (megatron_utils/data.py) def log_eval_data(rollout_id, args, data_buffer): if rank == 0: data = ray.get(data_buffer.get_data.remote(rollout_id, evaluation=True)) # ... 评估和日志逻辑 ... 更细粒度的实现分析 构造参数 args：配置参数对象，通常由 argparse 解析得到，包含所有训练/采样/数据相关的配置项。 主要成员变量 名称 类型 作用与说明 self.args object 配置参数，包含所有 buffer 运行所需的参数。 self.buffer list[list[Sample]] 主缓存队列，存储样本组（每组为同一 prompt 的多个采样）。 self.buffer_filter function 样本出队策略函数，决定如何从 buffer 取出样本组。可自定义。 self.train_data_pool dict[int, Any] 训练数据池，key 为 rollout_id，value 为训练数据。 self.eval_data_pool dict[int, Any] 评估数据池，key 为 rollout_id，value 为评估数据。 self.epoch_id int 当前数据集 epoch 号，用于 shuffle。 self.sample_index int 样本全局索引，递增。 self.sample_offset int 当前数据集采样偏移量。 self.metadata dict 存储元数据（如采样状态、统计信息等）。 self.dataset JsonlDataset or None 全局数据集对象，支持 prompt 初始化、shuffle。 self.generate_rollout function 训练采样函数，外部注入，负责生成训练数据。 self.eval_generate_rollout function 评估采样函数，外部注入，负责生成评估数据。 self.rollout_id int 当前 rollout 的 id（仅 generate 时临时赋值）。 依赖对象与数据结构 Sample 采样/训练的基本数据单元，定义见 slime/utils/types.py。 典型字段：index, tokens, response_length, reward, rewards, status, loss_mask, metadata 等。 JsonlDataset 数据集对象，支持从 jsonl 文件加载样本，支持 shuffle、按 key 取 prompt/label/metadata。 主要属性：samples（list[Sample]），shuffle(epoch_id)。 buffer_filter 样本出队策略函数，签名为 buffer_filter(args, rollout_id, buffer, num_samples)。 默认实现为 pop_first（先进先出），可通过参数自定义。 generate_rollout / eval_generate_rollout 外部注入的采样/推理函数，签名为 generate_rollout(args, rollout_id, buffer, evaluation=False)。 负责实际调用推理后端（如 SGLang、vllm）生成样本。 主要方法定义与作用 init(self, args) 初始化 buffer，加载参数、数据集、采样/评估函数、buffer_filter 等。 get_num_rollout_per_epoch(self) 返回每个 epoch 可采样的 rollout 数量（仅全局数据集模式下有效）。 get_samples(self, num_samples) 获取指定数量的样本组（list[list[Sample]]）。 优先从 self.buffer 出队，不足时从 self.dataset 生成新样本组。 支持分组采样（每组 n_samples_per_prompt 个样本）。 _get_samples_from_buffer(self, num_samples) 内部方法，调用 buffer_filter 从 self.buffer 出队样本组。 add_samples(self, samples) 向 buffer 添加样本组（list[list[Sample]]）。 每组样本对应同一 prompt 的多个采样。 generate(self, rollout_id, evaluation=False) 调用 generate_rollout 或 eval_generate_rollout 生成数据，写入 train_data_pool 或 eval_data_pool。 采样逻辑由外部函数实现，buffer 只负责调度和缓存。 get_data(self, rollout_id, evaluation=False) 获取指定 rollout_id 的训练/评估数据（从 train_data_pool 或 eval_data_pool 取出并删除）。 _convert_samples_to_train_data(self, samples) 将采样得到的样本（Sample 列表）转换为训练数据格式（如 tokens、rewards、loss_masks 等）。 _set_data(self, data, evaluation=False) 将数据写入 train_data_pool 或 eval_data_pool。 支持 debug 数据保存。 update_metadata(self, metadata) 更新 buffer 的元数据。 get_metadata(self) 获取 buffer 的元数据。 get_buffer_length(self) 返回当前 buffer 中缓存的样本组数量。 save(self, rollout_id) 保存 buffer 状态（如 sample_offset、epoch_id、sample_index、metadata）到本地文件。 load(self, rollout_id=None) 加载 buffer 状态（如 sample_offset、epoch_id、sample_index、metadata）从本地文件。 主要数据结构 self.buffer 类型：list[list[Sample]] 结构：每个元素是一个样本组（同一 prompt 的多个采样），每组为 list[Sample]。 用途：缓存采样生成的数据，供训练 actor 批量消费。 self.train_data_pool 作用：缓存训练数据，供训练 actor 消费\n数据结构：dict[int, Any]，key 为 rollout_id，value 为训练数据（包含 tokens、rewards、loss_masks 等）\n用途：存储每个 rollout 的训练样本，用于 PPO 训练\nself.eval_data_pool 作用：缓存评估数据，供评估流程使用\n数据结构：dict[int, Any]，key 为 rollout_id，value 为评估数据\n用途：存储每个 rollout 的评估样本，用于模型性能评估\nself.dataset 类型：JsonlDataset\n结构：包含 samples（list[Sample]），支持 shuffle。\n用途：全局数据集模式下，按需生成新样本组。\nself.metadata 类型：dict\n结构：任意元数据（如采样状态、统计信息等）。\n用途：记录 buffer 的附加信息，便于状态恢复和监控。\n数据流转示意 采样 actor 生成数据\n调用 buffer.generate(rollout_id) generate_rollout(args, rollout_id, buffer) → 返回 list[Sample] buffer._set_data(data) → 写入 train_data_pool[rollout_id] 训练 actor 获取数据\n调用 buffer.get_samples(num_samples) 优先从 self.buffer 出队，不足时从 self.dataset 生成 返回 list[list[Sample]]，供训练使用 训练 actor 消费数据\n训练 actor 拿到样本组后，进行训练、权重更新等操作 具体的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1. 采样生成训练数据 rollout_id = 100 rollout_generator.async_generate(rollout_id) # evaluation=False # Buffer 内部： # - 调用 generate_rollout(args, 100, buffer, evaluation=False) # - 生成训练样本：[Sample1, Sample2, ...] # - 转换为训练格式：{\u0026#34;tokens\u0026#34;: [...], \u0026#34;rewards\u0026#34;: [...], \u0026#34;loss_masks\u0026#34;: [...]} # - 写入 train_data_pool[100] # 2. 训练消费数据 actor_model.async_train(rollout_id) # Buffer 内部： # - 从 train_data_pool[100] 取出训练数据 # - 返回给训练 actor # - 删除 train_data_pool[100] # 3. 评估生成数据 rollout_generator.async_generate(rollout_id, evaluation=True) # Buffer 内部： # - 调用 eval_generate_rollout(args, 100, buffer, evaluation=True) # - 生成评估样本 # - 写入 eval_data_pool[100] # 4. 评估消费数据 actor_model.async_eval(rollout_id) # Buffer 内部： # - 从 eval_data_pool[100] 取出评估数据 # - 返回给评估 actor # - 删除 eval_data_pool[100] 流水掩盖分析 目前slime框架计算ref log_p -\u0026gt; old log_p -\u0026gt; current log_p是顺序执行的，训练阶段需要等待所有log_p计算完成，且采样-\u0026gt;训练-\u0026gt;权重同步之间也是通过ray.get()进行同步的，因此单actor异步掩盖的部分是计算reward、数据转换、数据存储等，而且主要的优化不是主流程上的流水掩盖而是并行异步带来的优化，比如并行计算多个样本的奖励、生成响应的时候同时计算奖励，当然还有远程reward model的网络I/O可以掩盖掉。\n当前框架的约束 sequenceDiagram participant Main as Main Loop participant Rollout as Rollout Actor participant Buffer as Data Buffer participant Train as Train Actor participant RM as Reward Model Note over Main, RM: 时间线 T1 - Rollout 1 Main-\u0026gt;\u0026gt;Rollout: async_generate(rollout_id=1) Rollout-\u0026gt;\u0026gt;Buffer: 生成样本 Buffer-\u0026gt;\u0026gt;RM: 计算奖励（异步，可以掩盖） Rollout--\u0026gt;\u0026gt;Main: 完成 Note over Main, RM: 时间线 T2 - Train 1（必须等待 T1 完成） Main-\u0026gt;\u0026gt;Train: async_train(rollout_id=1) Train-\u0026gt;\u0026gt;Buffer: 获取数据 Train--\u0026gt;\u0026gt;Main: 完成 Note over Main, RM: 时间线 T3 - Update Weights（必须等待 T2 完成） Main-\u0026gt;\u0026gt;Train: async_update_weights() Train--\u0026gt;\u0026gt;Main: 完成 Note over Main, RM: 时间线 T4 - Rollout 2（必须等待 T3 完成） Main-\u0026gt;\u0026gt;Rollout: async_generate(rollout_id=2) 当前框架的掩盖效果 graph TD subgraph \u0026#34;Rollout 阶段内部掩盖\u0026#34; A1[生成响应] --\u0026gt; B1[计算奖励] A2[生成响应] --\u0026gt; B2[数据预处理] A3[生成响应] --\u0026gt; B3[格式转换] B1 --\u0026gt; C1[样本完成] B2 --\u0026gt; C1 B3 --\u0026gt; C1 style A1 fill:#e8f5e8 style B1 fill:#e8f5e8 style B2 fill:#e8f5e8 style B3 fill:#e8f5e8 end subgraph \u0026#34;多 Actor 并行\u0026#34; D1[Train Actor 1] --\u0026gt; E1[并行训练] D2[Train Actor 2] --\u0026gt; E2[并行训练] D3[Train Actor 3] --\u0026gt; E3[并行训练] style D1 fill:#e8f5e8 style D2 fill:#e8f5e8 style D3 fill:#e8f5e8 end subgraph \u0026#34;内存管理掩盖\u0026#34; F1[模型 offload] --\u0026gt; G1[内存释放] F2[数据持久化] --\u0026gt; G2[磁盘 I/O] style F1 fill:#e8f5e8 style F2 fill:#e8f5e8 end 不能掩盖的部分 graph TD subgraph \u0026#34;主流程同步约束\u0026#34; A[Rollout 1] --\u0026gt; B[Train 1] B --\u0026gt; C[Update Weights 1] C --\u0026gt; D[Rollout 2] D --\u0026gt; E[Train 2] E --\u0026gt; F[Update Weights 2] style A fill:#ffebee style B fill:#ffebee style C fill:#ffebee style D fill:#ffebee style E fill:#ffebee style F fill:#ffebee end subgraph \u0026#34;训练阶段同步\u0026#34; G[获取数据] --\u0026gt; H[计算 ref log_p] H --\u0026gt; I[计算 old log_p] I --\u0026gt; J[计算 current log_p] J --\u0026gt; K[执行训练] style G fill:#ffebee style H fill:#ffebee style I fill:#ffebee style J fill:#ffebee style K fill:#ffebee end data buffer当前的收益 sequenceDiagram participant Rollout as Rollout Actor participant Buffer as Data Buffer participant Train as Train Actor participant RM as Reward Model Note over Rollout, RM: 时间线 T1 - 并行操作 Rollout-\u0026gt;\u0026gt;Buffer: 添加样本（异步） Buffer-\u0026gt;\u0026gt;RM: 计算奖励（异步，与添加并行） Rollout-\u0026gt;\u0026gt;Buffer: 数据转换（异步） Note over Rollout, RM: 时间线 T2 - 数据获取 Train-\u0026gt;\u0026gt;Buffer: 获取训练数据（异步） Buffer-\u0026gt;\u0026gt;Train: 返回数据 Note over Rollout, RM: 时间线 T3 - 内存管理 Buffer-\u0026gt;\u0026gt;Buffer: 数据持久化（异步） Buffer-\u0026gt;\u0026gt;Buffer: 内存清理（异步） 异步数据操作：添加、获取、转换可以并行\n内存管理：持久化和清理可以异步\n多进程访问：多个 Actor 可以并发访问\n可以进一步掩盖的方案 log_p计算掩盖 以PPO流程为例，reference model的log_p计算，权重固定（不参与训练），不计算梯度，可以提前计算；old actor的log_p计算，使用训练前的模型状态，可以在数据生成时提前计算。不过因为reward计算和log_p计算都需要完整的采样生成，所以实际应该只是reward计算（以及其他可并行项）和log_p计算之间的流水掩盖。\ngraph TD subgraph \u0026#34;当前实现\u0026#34; A1[生成数据] --\u0026gt; B1[等待完成] B1 --\u0026gt; C1[计算 ref log_p] C1 --\u0026gt; D1[计算 old log_p] D1 --\u0026gt; E1[计算 current log_p] E1 --\u0026gt; F1[训练] style A1 fill:#ffebee style B1 fill:#ffebee style C1 fill:#ffebee style D1 fill:#ffebee style E1 fill:#ffebee style F1 fill:#ffebee end subgraph \u0026#34;优化方案\u0026#34; A2[生成数据] --\u0026gt; B2[并行计算 log_p] B2 --\u0026gt; C2[ref log_p 计算] B2 --\u0026gt; D2[old log_p 计算] C2 --\u0026gt; E2[数据准备完成] D2 --\u0026gt; E2 E2 --\u0026gt; F2[训练（只需 current log_p）] style A2 fill:#e8f5e8 style B2 fill:#e8f5e8 style C2 fill:#e8f5e8 style D2 fill:#e8f5e8 style E2 fill:#e8f5e8 style F2 fill:#e8f5e8 end sequenceDiagram participant Main as Main Loop participant Rollout as Rollout Actor participant Buffer as Data Buffer participant Train as Train Actor Note over Main, Train: 当前实现时间线 Main-\u0026gt;\u0026gt;Rollout: Rollout 1 Rollout-\u0026gt;\u0026gt;Buffer: 生成 + 奖励计算 Buffer--\u0026gt;\u0026gt;Rollout: 完成 Rollout--\u0026gt;\u0026gt;Main: 完成 Main-\u0026gt;\u0026gt;Train: Train 1 Train-\u0026gt;\u0026gt;Train: 计算所有 log_p（顺序） Train-\u0026gt;\u0026gt;Train: 训练 Train--\u0026gt;\u0026gt;Main: 完成 Note over Main, Train: 优化方案时间线 Main-\u0026gt;\u0026gt;Rollout: Rollout 1 Rollout-\u0026gt;\u0026gt;Buffer: 生成 + 奖励计算 + 并行 log_p Buffer--\u0026gt;\u0026gt;Rollout: 完成（更快） Rollout--\u0026gt;\u0026gt;Main: 完成 Main-\u0026gt;\u0026gt;Train: Train 1 Train-\u0026gt;\u0026gt;Train: 只需计算 current log_p Train-\u0026gt;\u0026gt;Train: 训练 Train--\u0026gt;\u0026gt;Main: 完成 sequenceDiagram participant Rollout as Rollout Actor participant Buffer as Data Buffer participant Ref as Reference Model participant Old as Old Actor participant Train as Train Actor Note over Rollout, Train: 时间线 T1 - 并行计算 Rollout-\u0026gt;\u0026gt;Buffer: 生成样本数据 Buffer-\u0026gt;\u0026gt;Ref: 计算 ref log_p（并行） Buffer-\u0026gt;\u0026gt;Old: 计算 old log_p（并行） Note over Rollout, Train: 时间线 T2 - 训练 Train-\u0026gt;\u0026gt;Buffer: 获取数据 + log_p Train-\u0026gt;\u0026gt;Train: 计算 current log_p Train-\u0026gt;\u0026gt;Train: 执行训练步骤 ","permalink":"https://pillumina.github.io/posts/aiinfra/02-slime/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/THUDM/slime\"\u003ehttps://github.com/THUDM/slime\u003c/a\u003e\u003cbr\u003e\n一个异步实现但是非完全异步的RL框架\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"总体架构\"\u003e总体架构\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e从源码模块划分，有三大核心模块：\n\u003cul\u003e\n\u003cli\u003etraining（Megatron）：主训练流程，负责模型参数更新。\u003c/li\u003e\n\u003cli\u003erollout（SGLang + router）：负责采样、奖励/验证生成，产生训练数据。\u003c/li\u003e\n\u003cli\u003edata buffer：桥接训练与采样，管理数据流、缓存与生成方式。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e分布式调度：关于资源分配、actor启动、任务调度都由于Ray管理，支持异步训练和采样\u003c/li\u003e\n\u003cli\u003e插件机制：支持自定义buffer、模型、模型格式转换（mbridge）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre class=\"mermaid\"\u003e\n  flowchart LR\n    subgraph Ray[Ray 分布式调度]\n        A1[Actor Group\u0026lt;br\u0026gt;训练 Actor]\n        A2[Rollout Group\u0026lt;br\u0026gt;采样/生成 Actor]\n        A3[Placement Group\u0026lt;br\u0026gt;资源分配]\n    end\n    subgraph Training[Training \u0026lt;Megatron\u0026gt;]\n        T1[模型训练]\n        T2[权重同步]\n        T3[评估/保存]\n    end\n    subgraph Rollout[Rollout \u0026lt;SGLang+Router\u0026gt;]\n        R1[采样/生成]\n        R2[奖励模型]\n        R3[过滤器]\n    end\n    subgraph Buffer[Data Buffer]\n        B1[数据缓存]\n        B2[数据流转]\n        B3[Offload/Onload]\n    end\n    subgraph Plugins[插件机制]\n        P1[Buffer 插件]\n        P2[Model 插件]\n        P3[mbridge 格式转换]\n    end\n\n    A1--\u0026gt;|训练数据|B1\n    A2--\u0026gt;|生成数据|B1\n    B1--\u0026gt;|数据流|A1\n    B1--\u0026gt;|数据流|A2\n    A1--\u0026gt;|权重同步|A2\n    A1--\u0026gt;|评估/保存|T3\n    A2--\u0026gt;|采样/奖励/过滤|R1\n    R1--\u0026gt;|奖励|R2\n    R1--\u0026gt;|过滤|R3\n    B1--\u0026gt;|插件扩展|P1\n    A1--\u0026gt;|模型扩展|P2\n    A1--\u0026gt;|格式转换|P3\n    A3--\u0026gt;|资源分配|A1\n    A3--\u0026gt;|资源分配|A2\n\u003c/pre\u003e\n\n\u003ch2 id=\"各模块视角的关系图\"\u003e各模块视角的关系图\u003c/h2\u003e\n\u003ch3 id=\"slimerollout-组件图\"\u003eslime/rollout 组件图\u003c/h3\u003e\n\u003cp\u003erollout 负责采样、奖励、过滤，支持多种采样/奖励/过滤策略。\u003c/p\u003e","title":"[RL4LLM] 异步RL框架: Slime"},{"content":" https://github.com/inclusionAI/AReaL\n纯异步RL方案\n异步PPO训练调用流程 graph TD A[用户执行: examples/run_async_ppo.sh] --\u0026gt; B[training/main_async_ppo.py] B --\u0026gt; C[AsyncPPOMATHConfig配置解析] C --\u0026gt; D[training/utils.py: run_experiment] D --\u0026gt; E[Ray初始化] E --\u0026gt; F[exp_cfg.initial_setup] F --\u0026gt; G[AsyncRLExperimentConfig.initial_setup] G --\u0026gt; H[创建ExperimentConfig] H --\u0026gt; I[启动Workers] I --\u0026gt; J[MasterWorker] I --\u0026gt; K[ModelWorker] I --\u0026gt; L[GenerationServer] I --\u0026gt; M[GserverManager] I --\u0026gt; N[RolloutWorker] %% MasterWorker训练流程 J --\u0026gt; J1[MasterWorker._poll_async] J1 --\u0026gt; J2[FunctionExecutor.execute_step] J2 --\u0026gt; J3[执行数据流图遍历] J3 --\u0026gt; J4[发送训练请求到ModelWorker] %% ModelWorker处理流程 K --\u0026gt; K1[ModelWorker._poll] K1 --\u0026gt; K2[接收MasterWorker请求] K2 --\u0026gt; K3[处理训练/推理请求] K3 --\u0026gt; K4[执行模型前向/反向传播] %% Rollout流程 N --\u0026gt; N1[RolloutWorker._poll_async] N1 --\u0026gt; N2[load_next_data] N2 --\u0026gt; N3[allocate_new_rollout] N3 --\u0026gt; N4[agent.collect_trajectory] N4 --\u0026gt; N5[env.step计算奖励] N5 --\u0026gt; N6[推送数据到训练端] %% 生成服务器流程 L --\u0026gt; L1[GenerationServer._poll] L1 --\u0026gt; L2[启动SGLang子进程] L2 --\u0026gt; L3[处理生成请求] %% 生成服务器管理器 M --\u0026gt; M1[GserverManager._poll] M1 --\u0026gt; M2[HTTP服务线程] M2 --\u0026gt; M3[请求调度和权重更新] %% 数据流 N6 --\u0026gt; O[stream_dataset.py] O --\u0026gt; J4 %% 异步通信 J4 -.-\u0026gt;|异步请求| K2 N3 -.-\u0026gt;|HTTP请求| M2 M2 -.-\u0026gt;|调度请求| L3 %% 权重更新 K4 --\u0026gt; P[参数更新] P --\u0026gt; Q[权重同步] Q --\u0026gt; M3 M3 --\u0026gt; R[更新生成服务器权重] style A fill:#e1f5fe style J fill:#f3e5f5 style K fill:#e8f5e8 style L fill:#fff3e0 style M fill:#fce4ec style N fill:#f1f8e9 用户入口到配置解析 examples/run_async_ppo.sh → training/main_async_ppo.py\n通过Hydra解析CLI参数为AsyncPPOMATHConfig\n调用initial_setup()生成ExperimentConfig\nWorker启动和初始化 training/utils.py:run_experiment()启动Ray集群\n根据scheduling_setup()创建各类Worker\n每个Worker执行_configure()和_poll()/_poll_async()\n训练端数据流 MasterWorker._poll_async() → FunctionExecutor.execute_step()\n通过request_reply_stream发送请求到ModelWorker\nModelWorker处理训练/推理请求，执行模型计算\nRollout端数据流 RolloutWorker._poll_async() → agent.collect_trajectory()\n通过GserverManager调度生成请求到GenerationServer\n通过stream_dataset.py推送轨迹数据到训练端\n异步通信机制 训练端和Rollout端通过TCP Socket通信\nGserverManager提供HTTP API进行请求调度\n权重更新通过文件系统同步\n全局架构 部署形态 进程部署架构 以单机8卡为例\nMasterWorker：1个CPU进程，协调训练流程\nModelWorker：6个GPU进程（GPU0-5），执行模型训练\nGenerationServer：2个GPU进程（GPU6-7），运行SGLang推理服务\nGserverManager：1个CPU进程，管理生成服务器\nRolloutWorker：多个CPU进程，执行智能体逻辑\n训推资源分配 框架支持分离部署和共享部署两种模式\n分离部署 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ┌─────────────────────────────────────────────────────────────┐ │ Ray Cluster (1 Node, 8 GPUs) │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │MasterWorker │ │ModelWorker │ │ModelWorker │ │ │ │ (CPU) │ │ (GPU0) │ │ (GPU1) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ModelWorker │ │ModelWorker │ │ModelWorker │ │ │ │ (GPU2) │ │ (GPU3) │ │ (GPU4) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ModelWorker │ │GServerMgr │ │RolloutWorker│ │ │ │ (GPU5) │ │ (CPU) │ │ (CPU) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │GenServer │ │GenServer │ │RolloutWorker│ │ │ │ (SGLang) │ │ (SGLang) │ │ (CPU) │ │ │ │ (GPU6) │ │ (GPU7) │ └─────────────┘ │ │ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ 训练端：使用4个GPU（d2p2m1 = 2×2×1）\n推理端：使用4个GPU（d4p1m1 = 4×1×1）\n优势：完全解耦，互不干扰，性能最优\n分层关系 graph TB subgraph \u0026#34;用户层\u0026#34; A[examples/run_async_ppo.sh] B[training/main_async_ppo.py] end subgraph \u0026#34;配置层\u0026#34; C[AsyncPPOMATHConfig] D[ExperimentConfig] E[WorkerConfigs] end subgraph \u0026#34;系统层\u0026#34; F[Ray集群管理] G[Name Resolution] H[日志系统] end subgraph \u0026#34;训练端 Workers\u0026#34; I[MasterWorker] J[ModelWorker] K[FunctionExecutor] end subgraph \u0026#34;Rollout端 Workers\u0026#34; L[RolloutWorker] M[GenerationServer] N[GserverManager] O[PartialRolloutManager] end subgraph \u0026#34;核心组件\u0026#34; P[Agent接口] Q[Environment接口] R[Model接口] S[Dataset接口] end subgraph \u0026#34;通信层\u0026#34; T[Request-Reply Stream] U[Push-Pull Stream] V[HTTP API] W[TCP Socket] end subgraph \u0026#34;模型层\u0026#34; X[SGLang Backend] Y[PyTorch Backend] Z[模型并行] end %% 连接关系 A --\u0026gt; B B --\u0026gt; C C --\u0026gt; D D --\u0026gt; E E --\u0026gt; F F --\u0026gt; G F --\u0026gt; H E --\u0026gt; I E --\u0026gt; J E --\u0026gt; L E --\u0026gt; M E --\u0026gt; N I --\u0026gt; K K --\u0026gt; T J --\u0026gt; T L --\u0026gt; O O --\u0026gt; V M --\u0026gt; V N --\u0026gt; V L --\u0026gt; P L --\u0026gt; Q J --\u0026gt; R I --\u0026gt; S T --\u0026gt; W U --\u0026gt; W V --\u0026gt; W J --\u0026gt; Y M --\u0026gt; X Y --\u0026gt; Z X --\u0026gt; Z style A fill:#e3f2fd style I fill:#f3e5f5 style L fill:#e8f5e8 style T fill:#fff3e0 style X fill:#fce4ec 全局类图 classDiagram %% 基类层 class AsyncWorker { \u0026lt;\u0026lt;abstract\u0026gt;\u0026gt; +_configure(config) +_poll_async() PollResult +run_async() } class Worker { \u0026lt;\u0026lt;abstract\u0026gt;\u0026gt; +_configure(config) +_poll() PollResult +run() } %% Worker实现层 - 训练端 class MasterWorker { -config: MasterWorkerConfig -func_executor: FunctionExecutor -__poll_async() -__lazy_init() } class ModelWorker { -config: ModelWorkerConfig -__request_queue: Queue -_poll() -handle_request() } %% Worker实现层 - Rollout端 class RolloutWorker { -config: RolloutWorkerConfig -agent: Agent -env: Environment -_poll_async() -rollout_task() } class GenerationServer { -config: GenerationServerConfig -server_process: Process -_poll() -launch_server_subprocess() } class GserverManager { -config: GserverManagerConfig -server_urls: List[str] -_poll() -_schedule_request() } %% 接口层 class Agent { \u0026lt;\u0026lt;interface\u0026gt;\u0026gt; +collect_trajectory(prompt, env, obs_queue, act_queue) } class Environment { \u0026lt;\u0026lt;interface\u0026gt;\u0026gt; +reset() +step(action) } class ModelInterface { \u0026lt;\u0026lt;interface\u0026gt;\u0026gt; +inference(model, data, mb_spec) +generate(model, data, mb_spec) +train_step(model, data, mb_spec) } %% 配置层 class AsyncPPOMATHConfig { +agent: AgentAbstraction +env: EnvServiceAbstraction +initial_setup() ExperimentConfig +scheduling_setup() ExperimentScheduling } class ExperimentConfig { +model_rpcs: List[ModelRPC] +model_worker: ModelWorkerConfig +generation_server: GenerationServerConfig +rollout_worker: RolloutWorkerConfig } %% 继承关系 - 垂直排列减少交叉 AsyncWorker \u0026lt;|-- MasterWorker AsyncWorker \u0026lt;|-- RolloutWorker Worker \u0026lt;|-- ModelWorker Worker \u0026lt;|-- GenerationServer Worker \u0026lt;|-- GserverManager %% 组合关系 - 水平连接 MasterWorker --\u0026gt; ModelInterface : uses RolloutWorker --\u0026gt; Agent : uses RolloutWorker --\u0026gt; Environment : uses ModelWorker --\u0026gt; ModelInterface : implements %% 配置关系 - 底部连接 AsyncPPOMATHConfig --\u0026gt; ExperimentConfig : creates ExperimentConfig --\u0026gt; MasterWorker : configures ExperimentConfig --\u0026gt; ModelWorker : configures ExperimentConfig --\u0026gt; RolloutWorker : configures ExperimentConfig --\u0026gt; GenerationServer : configures ExperimentConfig --\u0026gt; GserverManager : configures 核心模块类图 classDiagram %% 基类 class AsyncWorker { \u0026lt;\u0026lt;abstract\u0026gt;\u0026gt; +_poll_async() PollResult } class Worker { \u0026lt;\u0026lt;abstract\u0026gt;\u0026gt; +_poll() PollResult } %% 训练端Workers class MasterWorker { -func_executor: FunctionExecutor -__poll_async() } class ModelWorker { -__request_queue: Queue -_poll() } %% Rollout端Workers class RolloutWorker { -agent: Agent -env: Environment -_poll_async() } class GenerationServer { -server_process: Process -_poll() } class GserverManager { -server_urls: List[str] -_poll() } %% 核心接口 class Agent { \u0026lt;\u0026lt;interface\u0026gt;\u0026gt; +collect_trajectory() } class Environment { \u0026lt;\u0026lt;interface\u0026gt;\u0026gt; +step(action) } class ModelInterface { \u0026lt;\u0026lt;interface\u0026gt;\u0026gt; +train_step() +generate() } %% 配置 class AsyncPPOMATHConfig { +initial_setup() +scheduling_setup() } %% 继承关系 AsyncWorker \u0026lt;|-- MasterWorker AsyncWorker \u0026lt;|-- RolloutWorker Worker \u0026lt;|-- ModelWorker Worker \u0026lt;|-- GenerationServer Worker \u0026lt;|-- GserverManager %% 关键关系 MasterWorker --\u0026gt; ModelInterface RolloutWorker --\u0026gt; Agent RolloutWorker --\u0026gt; Environment ModelWorker --\u0026gt; ModelInterface AsyncPPOMATHConfig --\u0026gt; MasterWorker AsyncPPOMATHConfig --\u0026gt; ModelWorker AsyncPPOMATHConfig --\u0026gt; RolloutWorker 异步流程机制细节 异步完整流程图 sequenceDiagram participant User as 用户 participant MW as MasterWorker participant RW as RolloutWorker participant GS as GenerationServer participant GSM as GserverManager participant ZMQ as ZMQ Stream participant SD as StreamDataset participant MW2 as ModelWorker participant NR as NameResolving participant FS as 文件系统 Note over User: 启动异步PPO训练 User-\u0026gt;\u0026gt;User: examples/run_async_ppo.sh\u0026lt;br/\u0026gt;输入：GPU数量、并行策略、模型路径 User-\u0026gt;\u0026gt;MW: training/main_async_ppo.py\u0026lt;br/\u0026gt;输入：AsyncPPOMATHConfig Note over MW: 初始化阶段 MW-\u0026gt;\u0026gt;MW: run_experiment(config)\u0026lt;br/\u0026gt;输入：实验配置 MW-\u0026gt;\u0026gt;MW: initial_setup()\u0026lt;br/\u0026gt;输入：worker配置 MW-\u0026gt;\u0026gt;NR: 注册各Worker地址\u0026lt;br/\u0026gt;变量：worker_info, msid2mwid Note over MW: 设置版本差异控制参数\u0026lt;br/\u0026gt;变量：max_head_offpolicyness Note over RW,GS: Rollout端启动 RW-\u0026gt;\u0026gt;RW: _configure(config)\u0026lt;br/\u0026gt;输入：RolloutWorkerConfig RW-\u0026gt;\u0026gt;ZMQ: 初始化NameResolvingZmqPusher\u0026lt;br/\u0026gt;变量：experiment_name, trial_name, worker_index GS-\u0026gt;\u0026gt;GS: _configure(config)\u0026lt;br/\u0026gt;输入：GenerationServerConfig GS-\u0026gt;\u0026gt;GS: 初始化SGLang后端\u0026lt;br/\u0026gt;变量：model_path, tokenizer_path GSM-\u0026gt;\u0026gt;GSM: _configure(config)\u0026lt;br/\u0026gt;输入：GserverManagerConfig GSM-\u0026gt;\u0026gt;GSM: 初始化权重版本跟踪\u0026lt;br/\u0026gt;变量：_last_param_realloc_step Note over MW2: 训练端启动 MW2-\u0026gt;\u0026gt;MW2: _configure(config)\u0026lt;br/\u0026gt;输入：ModelWorkerConfig MW2-\u0026gt;\u0026gt;SD: 初始化PullerStreamDataset\u0026lt;br/\u0026gt;变量：dataset_size, pull_timeout_ms MW2-\u0026gt;\u0026gt;MW2: 初始化模型和优化器\u0026lt;br/\u0026gt;变量：model_config, optimizer_config Note over MW: 训练循环开始 MW-\u0026gt;\u0026gt;MW: __poll_async()\u0026lt;br/\u0026gt;输入：训练控制参数 MW-\u0026gt;\u0026gt;MW: func_executor.execute_step()\u0026lt;br/\u0026gt;输入：数据流图 Note over RW,GS: 并行生成轨迹 loop 持续生成轨迹 RW-\u0026gt;\u0026gt;GS: 发送生成请求\u0026lt;br/\u0026gt;输入：prompt, max_tokens Note over GS: 使用当前加载的权重版本\u0026lt;br/\u0026gt;变量：current_model_version GS-\u0026gt;\u0026gt;GS: SGLang生成\u0026lt;br/\u0026gt;输入：模型权重、生成参数 GS--\u0026gt;\u0026gt;RW: 返回生成结果\u0026lt;br/\u0026gt;输出：generated_text RW-\u0026gt;\u0026gt;RW: agent.collect_trajectory()\u0026lt;br/\u0026gt;输入：生成结果 RW-\u0026gt;\u0026gt;RW: 计算奖励、构建轨迹\u0026lt;br/\u0026gt;变量：trajectory, reward Note over RW: 为轨迹添加版本信息\u0026lt;br/\u0026gt;变量：trajectory.model_version = current_model_version RW-\u0026gt;\u0026gt;ZMQ: push_stream.push(traj)\u0026lt;br/\u0026gt;输入：轨迹数据(JSON格式) end Note over ZMQ,SD: 数据传递 ZMQ-\u0026gt;\u0026gt;SD: 接收轨迹数据\u0026lt;br/\u0026gt;输入：JSON序列化数据 SD-\u0026gt;\u0026gt;SD: _pull_data_worker()\u0026lt;br/\u0026gt;后台线程持续拉取 SD-\u0026gt;\u0026gt;SD: 转换为SequenceSample\u0026lt;br/\u0026gt;变量：data_queue, processed_data Note over MW2: 训练执行 - 版本差异控制 MW2-\u0026gt;\u0026gt;SD: 获取训练数据\u0026lt;br/\u0026gt;输入：batch_size SD--\u0026gt;\u0026gt;MW2: 返回SequenceSample\u0026lt;br/\u0026gt;输出：训练样本 Note over MW2: 检查数据版本差异\u0026lt;br/\u0026gt;变量：data_version, current_version, max_head_offpolicyness MW2-\u0026gt;\u0026gt;MW2: validate_data_version(data_version, current_version)\u0026lt;br/\u0026gt;输入：数据版本、当前版本、最大允许差异 alt 版本差异在允许范围内 Note over MW2: 接受数据，继续训练\u0026lt;br/\u0026gt;变量：version_diff \u0026lt;= max_head_offpolicyness MW2-\u0026gt;\u0026gt;MW2: train_step(data)\u0026lt;br/\u0026gt;输入：训练数据、优化器状态 MW2-\u0026gt;\u0026gt;MW2: 计算PPO损失\u0026lt;br/\u0026gt;变量：policy_loss, value_loss, entropy_loss MW2-\u0026gt;\u0026gt;MW2: 更新模型参数\u0026lt;br/\u0026gt;变量：optimizer.step(), global_step else 版本差异过大 Note over MW2: 丢弃过期数据\u0026lt;br/\u0026gt;变量：version_diff \u0026gt; max_head_offpolicyness MW2-\u0026gt;\u0026gt;MW2: discard_stale_data(data)\u0026lt;br/\u0026gt;输入：过期数据 Note over MW2: 记录数据丢弃统计\u0026lt;br/\u0026gt;变量：stale_data_count++ MW2-\u0026gt;\u0026gt;SD: 请求新的训练数据\u0026lt;br/\u0026gt;输入：batch_size end Note over MW2,FS: 权重同步 - 版本控制 MW2-\u0026gt;\u0026gt;FS: __save_model(save_meta)\u0026lt;br/\u0026gt;输入：model, save_dir, global_step Note over FS: 保存权重分片到磁盘\u0026lt;br/\u0026gt;变量：param_realloc_path/model_name/step/ MW2-\u0026gt;\u0026gt;NR: name_resolve.add(model_version, global_step)\u0026lt;br/\u0026gt;输入：experiment, trial, model_name, step Note over NR: 原子性更新版本号\u0026lt;br/\u0026gt;变量：model_version = global_step Note over GSM,GS: 推理端权重更新 - 数据陈旧性控制 loop 定期检查新权重 GSM-\u0026gt;\u0026gt;NR: check_new_params()\u0026lt;br/\u0026gt;输入：experiment, trial, model_name NR--\u0026gt;\u0026gt;GSM: 返回最新global_step alt 有新权重版本 GSM-\u0026gt;\u0026gt;GSM: 发现版本更新\u0026lt;br/\u0026gt;变量：realloc_version \u0026gt; _last_param_realloc_step GSM-\u0026gt;\u0026gt;GS: flush_requests_and_update_weights(load_dir)\u0026lt;br/\u0026gt;输入：新权重路径 Note over GS: 中断当前所有推理请求\u0026lt;br/\u0026gt;变量：_interrupt_requests() GS-\u0026gt;\u0026gt;FS: 读取新权重文件\u0026lt;br/\u0026gt;输入：load_dir GS-\u0026gt;\u0026gt;GS: update_weights_from_disk(load_dir)\u0026lt;br/\u0026gt;变量：分片rank, 权重文件 Note over GS: 按TP/PP分片加载权重\u0026lt;br/\u0026gt;变量：新model_version生效 GS--\u0026gt;\u0026gt;GSM: 权重更新完成 GSM-\u0026gt;\u0026gt;GSM: 更新版本跟踪\u0026lt;br/\u0026gt;变量：_last_param_realloc_step = realloc_version Note over GS: 恢复推理服务\u0026lt;br/\u0026gt;变量：使用新权重版本 else 无新权重 GSM-\u0026gt;\u0026gt;GSM: 继续使用当前权重\u0026lt;br/\u0026gt;变量：_last_param_realloc_step end end Note over MW: 训练控制 - 版本差异监控 MW-\u0026gt;\u0026gt;MW: 检查训练终止条件\u0026lt;br/\u0026gt;输入：epoch, global_step, loss MW-\u0026gt;\u0026gt;MW: 监控版本差异统计\u0026lt;br/\u0026gt;变量：stale_data_count, version_diff_stats Note over MW: 记录版本差异对训练的影响\u0026lt;br/\u0026gt;变量：training_efficiency, data_freshness alt 继续训练 MW-\u0026gt;\u0026gt;MW: 更新训练状态\u0026lt;br/\u0026gt;变量：step_info, epoch_step Note over MW: 返回训练循环开始 else 训练完成 MW-\u0026gt;\u0026gt;User: 训练结束\u0026lt;br/\u0026gt;输出：最终模型、训练日志、版本差异统计 end 异步带来的算法修正 同步PPO完整流程 先回顾一下ppo的计算流程：\n我们有一个策略π(a|s)，它决定在状态s下选择动作a的概率。PPO的目标是优化这个策略，使其能够获得更高的累积奖励。\n数据收集（rollout） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 使用当前策略π_θ生成轨迹 for episode in range(num_episodes): state = env.reset() trajectory = [] while not done: # 使用当前策略选择动作 action_probs = π_θ(state) # 当前策略的概率分布 action = sample(action_probs) # 采样动作 # 记录动作概率（用于后续计算重要性比率） old_logp = log(action_probs[action]) # 这就是old_logp # 执行动作 next_state, reward, done = env.step(action) trajectory.append((state, action, reward, old_logp)) state = next_state 计算优势函数 1 2 3 # 使用GAE计算优势函数 advantages = compute_gae(trajectory, γ=0.99, λ=0.95) returns = compute_returns(trajectory, γ=0.99) 策略更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 对收集的数据进行多次更新 for epoch in range(num_epochs): for batch in data_loader: # 重新计算当前策略的概率 current_action_probs = π_θ(batch.states) # 当前策略 cur_logp = log(current_action_probs[batch.actions]) # 这就是cur_logp # 计算重要性比率 ratio = exp(cur_logp - old_logp) # PPO损失函数 surr1 = ratio * advantages surr2 = clip(ratio, 1-ε, 1+ε) * advantages loss = -min(surr1, surr2) # 更新策略参数 optimizer.zero_grad() loss.backward() optimizer.step() 为什么需要重要性采样 ratio = π_θ(a|s) / π_θ_old(a|s) = exp(cur_logp - old_logp)\n我们想用当前策略π_θ来评估旧策略π_θ_old生成的数据 重要性采样修正了这种分布偏移 框架的异步PPO修正机制 异步带来的问题，数据生成和训练并行 1 2 3 4 5 # 时间线 t=0: 策略π_θ_0生成数据 t=1: 策略π_θ_1生成数据，同时训练π_θ_0的数据 t=2: 策略π_θ_2生成数据，同时训练π_θ_1的数据 ... 这导致：\n训练数据来自较旧的策略版本\n重要性比率可能变得很大或很小\n策略更新可能不稳定\n框架引入的修正机制如下：\n机制1： 版本控制\n1 2 3 4 5 6 7 8 # 记录数据生成时的策略版本 data = { \u0026#34;version_start\u0026#34;: model_version_when_generation_started, \u0026#34;version_end\u0026#34;: model_version_when_generation_ended, \u0026#34;old_logp\u0026#34;: logprobs_from_generation, \u0026#34;actions\u0026#34;: actions, \u0026#34;rewards\u0026#34;: rewards } 机制2：数据过滤\n1 2 3 4 5 # 检查版本差异 version_diff = current_version - data.version_start if version_diff \u0026gt; max_head_offpolicyness: # 数据太旧，丢弃 continue 机制3：解耦损失（Decoupled Loss）\n1 2 3 4 5 6 7 8 9 10 11 # 标准PPO损失 def standard_ppo_loss(cur_logp, old_logp, advantages): ratio = exp(cur_logp - old_logp) return -min(ratio * advantages, clip(ratio, 1-ε, 1+ε) * advantages) # AReaL解耦损失 def decoupled_loss(cur_logp, old_logp, prox_logp, advantages): # 使用prox_logp作为中间策略 ratio = exp(cur_logp - prox_logp) behav_weight = exp(prox_logp - old_logp) return -min(ratio * advantages, clip(ratio, 1-ε, 1+ε) * advantages) * behav_weight 修正的合理性分析 数学基础\n解耦损失可以分解为:\n1 2 3 4 5 6 7 8 9 # 标准PPO ratio = π_θ(a|s) / π_θ_old(a|s) # AReaL解耦 ratio = π_θ(a|s) / π_prox(a|s) behav_weight = π_prox(a|s) / π_θ_old(a|s) # 等价性 ratio * behav_weight = π_θ(a|s) / π_θ_old(a|s) # 与标准PPO相同 稳定性提升\n1 2 3 4 5 6 7 8 9 # 异步场景下的问题 # 如果π_θ与π_θ_old差异很大 ratio = π_θ(a|s) / π_θ_old(a|s) # 可能很大或很小 # AReaL的解决方案 # 引入中间策略π_prox，使得： # π_θ ≈ π_prox ≈ π_θ_old ratio = π_θ(a|s) / π_prox(a|s) # 更稳定 behav_weight = π_prox(a|s) / π_θ_old(a|s) # 更稳定 渐进式更新\n1 2 # 标准异步PPO：直接从π_θ_old跳到π_θ # AReaL：π_θ_old → π_prox → π_θ，分两步更新 具体实现 核心修正机制实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # AReaL的解耦损失实现 if proximal_logprobs is not None: # 计算行为策略权重 behav_kl = proximal_logprobs - old_logprobs behav_imp_weight = behav_kl.exp() # 应用权重上限 if behav_imp_weight_cap is not None: behav_mask = (behav_imp_weight \u0026lt;= behav_imp_weight_cap).logical_and(loss_mask) else: behav_mask = loss_mask # 应用行为策略权重 pg_loss = pg_loss * behav_imp_weight 数学等价性证明\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 标准PPO损失 L_standard = -min(ratio * A, clip(ratio, 1-ε, 1+ε) * A) 其中 ratio = π_θ(a|s) / π_θ_old(a|s) # AReaL解耦损失 L_decoupled = -min(ratio * A, clip(ratio, 1-ε, 1+ε) * A) * behav_weight 其中 ratio = π_θ(a|s) / π_prox(a|s) behav_weight = π_prox(a|s) / π_θ_old(a|s) # 等价性证明 L_decoupled = -min(ratio * A, clip(ratio, 1-ε, 1+ε) * A) * behav_weight = -min((π_θ/π_prox) * A, clip(π_θ/π_prox, 1-ε, 1+ε) * A) * (π_prox/π_θ_old) = -min((π_θ/π_θ_old) * A, clip(π_θ/π_prox, 1-ε, 1+ε) * A * (π_prox/π_θ_old)) 流程图视角 graph TD %% 生成阶段 - SGLang推理服务 A[用户Prompt\u0026lt;br/\u0026gt;packed_prompts] --\u0026gt; B[SGLang推理服务\u0026lt;br/\u0026gt;actor_gen] B --\u0026gt; B1[PPOActorInterface.generate\u0026lt;br/\u0026gt;使用策略π_θ_old] B1 --\u0026gt; B2[模型前向传播\u0026lt;br/\u0026gt;genstep函数] B2 --\u0026gt; B3[采样token\u0026lt;br/\u0026gt;计算logprob] B3 --\u0026gt; B4[concat_prompt_to_generation_output\u0026lt;br/\u0026gt;拼接prompt和生成结果] B4 --\u0026gt; B5[输出: packed_input_ids\u0026lt;br/\u0026gt;packed_logprobs\u0026lt;old_logp\u0026gt;\u0026lt;br/\u0026gt;prompt_mask\u0026lt;br/\u0026gt;seq_no_eos_mask] %% 推理阶段 - 四个组件并行执行 B5 --\u0026gt; C[推理阶段开始] %% Actor推理 - 计算proximal_logp C --\u0026gt; D[actor_inf\u0026lt;br/\u0026gt;PPOActorInterface.inference\u0026lt;br/\u0026gt;使用策略π_θ_prox] D --\u0026gt; D1[输入: packed_input_ids] D1 --\u0026gt; D2[calc_logprobs post_hook\u0026lt;br/\u0026gt;gather_packed_shifted_log_probs] D2 --\u0026gt; D3[输出: proximal_logprobs\u0026lt;br/\u0026gt;π_θ_prox\u0026lt;a,s\u0026gt;] %% Reference推理 - 计算ref_logp C --\u0026gt; E[ref_inf\u0026lt;br/\u0026gt;PPOActorInterface.inference\u0026lt;br/\u0026gt;使用策略π_ref] E --\u0026gt; E1[输入: packed_input_ids] E1 --\u0026gt; E2[calc_logprobs post_hook\u0026lt;br/\u0026gt;gather_packed_shifted_log_probs] E2 --\u0026gt; E3[输出: packed_ref_logprobs\u0026lt;br/\u0026gt;π_ref\u0026lt;a,s\u0026gt;] %% Critic推理 - 计算values C --\u0026gt; F[critic_inf\u0026lt;br/\u0026gt;PPOCriticInterface.inference\u0026lt;br/\u0026gt;使用价值网络V_θ] F --\u0026gt; F1[输入: packed_input_ids\u0026lt;br/\u0026gt;seq_no_eos_mask] F1 --\u0026gt; F2[module.forward\u0026lt;br/\u0026gt;直接输出value] F2 --\u0026gt; F3[输出: values\u0026lt;br/\u0026gt;V_θ\u0026lt;s\u0026gt;] %% Reward推理 - 计算rewards C --\u0026gt; G[rew_inf\u0026lt;br/\u0026gt;MultiTaskRewardInterface.inference\u0026lt;br/\u0026gt;使用奖励函数R] G --\u0026gt; G1[输入: packed_input_ids\u0026lt;br/\u0026gt;packed_prompts\u0026lt;br/\u0026gt;task_ids] G1 --\u0026gt; G2[calculate_task_reward\u0026lt;br/\u0026gt;异步任务处理] G2 --\u0026gt; G3[输出: rewards\u0026lt;br/\u0026gt;R\u0026lt;s,a\u0026gt;] %% 数据汇聚 D3 --\u0026gt; H[推理结果汇聚] E3 --\u0026gt; H F3 --\u0026gt; H G3 --\u0026gt; H %% 训练阶段准备 H --\u0026gt; I[训练数据准备\u0026lt;br/\u0026gt;packed_input_ids\u0026lt;br/\u0026gt;packed_logprobs\u0026lt;old_logp\u0026gt;\u0026lt;br/\u0026gt;packed_ref_logprobs\u0026lt;br/\u0026gt;proximal_logprobs\u0026lt;br/\u0026gt;rewards\u0026lt;br/\u0026gt;values\u0026lt;br/\u0026gt;prompt_mask\u0026lt;br/\u0026gt;seq_no_eos_mask] %% 训练阶段 - 计算current_logp和loss I --\u0026gt; J[actor_train\u0026lt;br/\u0026gt;PPOActorInterface.train_step\u0026lt;br/\u0026gt;使用策略π_θ] J --\u0026gt; J1[模型前向传播\u0026lt;br/\u0026gt;module.forward] J1 --\u0026gt; J2[gather_packed_shifted_log_probs\u0026lt;br/\u0026gt;计算current_logp\u0026lt;br/\u0026gt;π_θ\u0026lt;a,s\u0026gt;] J2 --\u0026gt; J3[计算advantages\u0026lt;br/\u0026gt;GAE算法] J3 --\u0026gt; J4[计算rewards\u0026lt;br/\u0026gt;KL正则化] J4 --\u0026gt; J5[PPO Loss计算\u0026lt;br/\u0026gt;_ppo_actor_loss_from_model_outputs] %% PPO Loss详细计算 J5 --\u0026gt; K[PPO Loss计算详情] K --\u0026gt; K1[输入: current_logp, old_logp, proximal_logp\u0026lt;br/\u0026gt;advantages, rewards] K1 --\u0026gt; K2{use_decoupled_loss?} K2 --\u0026gt;|是| K3[解耦损失计算\u0026lt;br/\u0026gt;ratio = exp\u0026lt;current_logp - proximal_logp\u0026gt;\u0026lt;br/\u0026gt;behav_weight = exp\u0026lt;proximal_logp - old_logp\u0026gt;] K2 --\u0026gt;|否| K4[标准损失计算\u0026lt;br/\u0026gt;ratio = exp\u0026lt;current_logp - old_logp\u0026gt;] K3 --\u0026gt; K5[最终损失\u0026lt;br/\u0026gt;loss = -min ratio * advantages, clip ratio * advantages * behav_weight] K4 --\u0026gt; K6[最终损失\u0026lt;br/\u0026gt;loss = -min ratio * advantages, clip ratio * advantages] K5 --\u0026gt; L[输出: Actor Loss] K6 --\u0026gt; L %% Critic训练 J3 --\u0026gt; M[critic_train\u0026lt;br/\u0026gt;PPOCriticInterface.train_step\u0026lt;br/\u0026gt;使用价值网络V_θ] M --\u0026gt; M1[模型前向传播\u0026lt;br/\u0026gt;计算new_values] M1 --\u0026gt; M2[Critic Loss计算\u0026lt;br/\u0026gt;_ppo_critic_loss_from_model_outputs] M2 --\u0026gt; M3[输出: Critic Loss] %% 样式定义 classDef generateStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px classDef inferenceStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px classDef trainStyle fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px classDef lossStyle fill:#fff3e0,stroke:#ef6c00,stroke-width:2px class B,B1,B2,B3,B4,B5 generateStyle class D,D1,D2,D3,E,E1,E2,E3,F,F1,F2,F3,G,G1,G2,G3 inferenceStyle class J,J1,J2,J3,J4,J5,M,M1,M2,M3 trainStyle class K,K1,K2,K3,K4,K5,K6,L lossStyle old_logp (π_θ_old)\n1 2 3 4 5 # 生成阶段 - SGLang推理服务 # 模型：Actor模型 (策略π_θ_old) # 时机：生成token时实时计算 # 函数：genstep() -\u0026gt; distrb.log_prob(next_tokens) # 保存：concat_prompt_to_generation_output() -\u0026gt; packed_logprobs proximal_logp (π_θ_prox)\n1 2 3 4 5 # 推理阶段 - actor_inf组件 # 模型：Actor模型 (策略π_θ_prox，比π_θ_old新，比π_θ旧) # 时机：生成完成后，训练前 # 函数：PPOActorInterface.inference() -\u0026gt; calc_logprobs() # 条件：仅当use_decoupled_loss=True时计算 current_logp (π_θ)\n1 2 3 4 5 # 训练阶段 - actor_train组件 # 模型：Actor模型 (当前策略π_θ，最新) # 时机：训练时重新计算 # 函数：PPOActorInterface.train_step() -\u0026gt; gather_packed_shifted_log_probs() # 作用：用于计算重要性采样比率 权重同步机制 sequenceDiagram participant MW as ModelWorker participant FS as 文件系统 participant NR as NameResolving participant GSM as GserverManager participant GS as GenerationServer Note over MW: 训练完成一次step后 MW-\u0026gt;\u0026gt;MW: __save_model(save_meta)\u0026lt;br/\u0026gt;输入：model, save_dir, global_step MW-\u0026gt;\u0026gt;FS: 保存权重文件\u0026lt;br/\u0026gt;路径: param_realloc_path/model_name/step/ Note over MW,FS: 权重以分片形式落盘（TP/PP分片） MW-\u0026gt;\u0026gt;NR: name_resolve.add(model_version, global_step)\u0026lt;br/\u0026gt;输入：experiment, trial, model_name, step NR--\u0026gt;\u0026gt;GSM: model_version更新 loop 推理端定期检查 GSM-\u0026gt;\u0026gt;NR: check_new_params()\u0026lt;br/\u0026gt;输入：experiment, trial, model_name NR--\u0026gt;\u0026gt;GSM: 返回最新global_step alt 有新权重 GSM-\u0026gt;\u0026gt;FS: 获取新权重路径 GSM-\u0026gt;\u0026gt;GS: flush_requests_and_update_weights(load_dir) GS-\u0026gt;\u0026gt;FS: 读取权重分片文件 GS-\u0026gt;\u0026gt;GS: update_weights_from_disk(load_dir)\u0026lt;br/\u0026gt;变量: load_dir, 分片rank Note over GS: 按TP/PP分片加载到各自分片 else 无新权重 GSM-\u0026gt;\u0026gt;GSM: 不做更新 end end Note over GS: 新权重生效，推理端继续服务 核心机制：\n训练端：ModelWorker在每次train_step后保存权重到param_realloc_path，并调用name_resolve.add(model_version, global_step)，在NameResolving服务中记录最新的权重版本号（global_step）。\n推理端：GserverManager定期检查model_version，发现新版本（model_version和已经加载的对比）时通过HTTP API更新所有GenerationServer的权重。\ns\n同步动作：权重更新时会中断正在进行的生成请求，确保推理使用最新权重。\n关键函数与变量说明：\n__save_model(save_meta)\n输入：model_name, save_dir, global_step\n输出：权重文件（分片）落盘\nname_resolve.add(model_version, global_step)\n输入：实验名、trial名、模型名、step\n输出：NameResolving服务中记录最新step\ncheck_new_params()\n输入：实验名、trial名、模型名\n输出：最新step（如果有更新）\nflush_requests_and_update_weights(load_dir)\n输入：权重目录\n输出：推理端各分片加载新权重\nupdate_weights_from_disk(load_dir)\n输入：分片rank、load_dir\n输出：各分片权重加载到内存\n变量传递链路：\nglobal_step/model_version：用于标识权重版本\nparam_realloc_path/load_dir：权重磁盘路径\n分片rank：决定每个worker加载哪一份权重\n数据陈旧性控制 异步训推协调的核心机制，需要限制陈旧性保证训练稳定性\n1 2 3 4 # GserverManager中的陈旧性检查 def is_staled(self): # 检查当前运行的rollout是否过时 return self.rollout_stat.running \u0026gt; self.config.max_head_offpolicyness 协调机制：\n版本控制：每个生成请求都携带version_start和version_end，记录使用的权重版本\n陈旧性限制：通过max_head_offpolicyness参数控制允许的最大数据陈旧性\n请求调度：GserverManager在分配新rollout时检查容量和陈旧性，拒绝过时的请求\n确实存在使用老权重的情况：\n异步训练允许一定程度的权重陈旧性\n通过max_head_offpolicyness参数控制陈旧性上限\n这种设计在提高训练效率的同时，通过限制陈旧性保证训练稳定性\n数据传递机制 各个worker之间的通信核心是ZMQ：\n高性能：支持零拷贝和批量传输 多种模式：PUSH/PULL、PUB/SUB、REQ/REP等 异步通信：非阻塞I/O，适合高并发场景 跨语言：支持多种编程语言 网络透明：自动处理连接、重连、负载均衡 1 2 3 4 5 6 7 8 9 10 11 # zmq的配置举例 # 高性能配置 self.context = zmq.Context.instance(io_threads=8) self.context.set(zmq.MAX_SOCKETS, 65536) # 缓冲区优化 self.socket.setsockopt(zmq.SNDHWM, 1000) # 发送缓冲区 self.socket.setsockopt(zmq.RCVHWM, 1000) # 接收缓冲区 # 超时设置 self.socket.setsockopt(zmq.RCVTIMEO, timeout_ms) sequenceDiagram participant RW as RolloutWorker participant GS as GenerationServer participant GSM as GserverManager participant ZMQ as ZMQ Stream participant SD as StreamDataset participant MW as ModelWorker participant DM as DataManager Note over RW,DM: 1. 生成轨迹数据 RW-\u0026gt;\u0026gt;GS: 发送生成请求 GS-\u0026gt;\u0026gt;GS: SGLang生成结果 GS-\u0026gt;\u0026gt;RW: 返回生成结果 RW-\u0026gt;\u0026gt;RW: 计算奖励，构建轨迹 Note over RW,DM: 2. 推送数据到训练端 RW-\u0026gt;\u0026gt;ZMQ: 推送轨迹数据(JSON格式) ZMQ-\u0026gt;\u0026gt;SD: 接收数据 SD-\u0026gt;\u0026gt;SD: 转换为SequenceSample Note over RW,DM: 3. 训练端处理数据 SD-\u0026gt;\u0026gt;MW: 提供数据给ModelWorker MW-\u0026gt;\u0026gt;DM: 存储到DataManager(内存) MW-\u0026gt;\u0026gt;MW: 执行训练步骤 数据传递层次 Rollout端到训练端： 使用ZMQ Push-Pull Stream传输轨迹数据\nRolloutWorker → NameResolvingZmqPusher → NameResolvingZmqPuller → StreamDataset\ngraph TB subgraph \u0026#34;Rollout端\u0026#34; RW[RolloutWorker] --\u0026gt; NP[NameResolvingZmqPusher] NP --\u0026gt; ZMQ1[ZMQ PUSH Socket] end subgraph \u0026#34;训练端\u0026#34; ZMQ2[ZMQ PULL Socket] --\u0026gt; NP2[NameResolvingZmqPuller] NP2 --\u0026gt; SD[StreamDataset] SD --\u0026gt; MW[ModelWorker] end subgraph \u0026#34;Name Resolution\u0026#34; NR[name_resolve系统] end ZMQ1 -.-\u0026gt;|TCP连接| ZMQ2 NP --\u0026gt; NR NP2 --\u0026gt; NR 训练端内部： 使用Request-Reply Stream传输训练请求\nMasterWorker → ModelWorker通过ZMQ通信\ngraph TB subgraph \u0026#34;MasterWorker\u0026#34; MW[MasterWorker] --\u0026gt; NRC[NameResolvingRequestClient] NRC --\u0026gt; ZMQ1[ZMQ PUSH Sockets] ZMQ2[ZMQ PULL Socket] --\u0026gt; NRC end subgraph \u0026#34;ModelWorker\u0026#34; ZMQ3[ZMQ PULL Socket] --\u0026gt; NRS[NameResolvingReplyServer] NRS --\u0026gt; MW2[ModelWorker] MW2 --\u0026gt; ZMQ4[ZMQ PUSH Socket] end subgraph \u0026#34;通信协议\u0026#34; REQ[Request] --\u0026gt; ACK[ACK] ACK --\u0026gt; SYN[SYN] SYN --\u0026gt; RESP[Response] end ZMQ1 -.-\u0026gt;|TCP| ZMQ3 ZMQ4 -.-\u0026gt;|TCP| ZMQ2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 请求发送 def request(self, handlers, handle_type, datas, no_syn=True): requests = [ Payload( handler=handler, handle_name=handle_type, data=data, no_syn=no_syn, ) for handler, data in zip(handlers, datas) ] # 发送请求 for payload in requests: idx = self._handler_routing[payload.handler] self.send_sockets[idx].send(pickle.dumps(payload)) 存储分离： 训练数据：存储在DataManager中，支持分布式存储和重分布\nDataManager为内存存储： 1 2 3 4 5 6 7 8 9 10 11 12 class DataManager: def __init__(self, model_topos, msid2mwid, data_transfer_pairs): # 核心存储：内存字典 self.storage: Dict[Hashable, SequenceSample] = {} def store(self, x: SequenceSample): # 存储到内存字典 self.storage[x.ids[0]] = x def get(self, data_id: Hashable): # 从内存获取 return self.storage[data_id] 支持数据重分布：\n1 2 3 4 5 6 7 8 9 def redistribute(self, data_info: SequenceSample, plan: List[RedistribStep]): \u0026#34;\u0026#34;\u0026#34;执行数据重分布\u0026#34;\u0026#34;\u0026#34; for step in plan: if step.comm_type == \u0026#34;bcast\u0026#34;: self._run_bcast(step, data_infos) elif step.comm_type == \u0026#34;gather\u0026#34;: self._run_gather(step, data_infos) elif step.comm_type == \u0026#34;scatter\u0026#34;: self._run_scatter(step, data_infos) 推理数据：存储在SGLang服务器的内存中\n元数据：通过name_resolve系统共享\n实现细节 RolloutWorker 数据发送 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # realhf/system/rollout_worker.py class RolloutWorker(AsyncWorker): def _configure(self, config): # 初始化ZMQ推送器 - 发送轨迹数据到训练端 self.push_stream = NameResolvingZmqPusher( self.experiment_name, self.trial_name, pusher_index=self.worker_index, pusher_cnt=self.worker_count, ) async def _poll_async(self): # 收集轨迹数据 traj = await self.agent.collect_trajectory() # 推送数据到训练端 self.push_stream.push([traj.as_json_serializable()]) GenerationServer 推理服务 1 2 3 4 5 6 7 8 9 10 # realhf/system/generation_server.py class GenerationServer(Worker): def launch_server_subprocess(self): # 启动SGLang推理服务器 self.server_process, self.server_port = launch_server_cmd(cmd, port=server_port) self.server_addr = f\u0026#34;http://{host}:{self.server_port}\u0026#34; # 注册服务地址到NameResolving name = names.gen_servers(self.experiment_name, self.trial_name) name_resolve.add_subentry(name, self.server_addr) GserverManager负载均衡 1 2 3 4 5 6 7 8 9 10 11 12 13 # realhf/system/gserver_manager.py class GserverManager(Worker): def _discover_servers(self, n_servers: int): # 通过NameResolving发现所有推理服务器 name = names.gen_servers(self.experiment_name, self.trial_name) urls = name_resolve.get_subtree(name) return urls def _run_routing_service(self): # HTTP服务，接收推理请求并路由到合适的服务器 async def schedule_request(req_meta): server_idx = self._least_requests_schedule(req_meta) return self.server_urls[server_idx] MasterWorker 训练协调 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # realhf/system/master_worker.py class MasterWorker(AsyncWorker): def _configure(self, config): # 初始化Request-Reply客户端 self.func_executor = FunctionExecutor( experiment_name=self.experiment_name, trial_name=self.trial_name, n_subscribers=self.config.n_model_workers, handler_routing=self.config.handler_routing, ) async def _poll_async(self): # 执行训练步骤，通过Request-Reply与ModelWorker通信 result = await self.func_executor.execute_step( step_name=\u0026#34;train_step\u0026#34;, step_kwargs={\u0026#34;batch\u0026#34;: batch} ) ModelWorker 模型训练 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # realhf/system/model_worker.py class ModelWorker(Worker): def _configure(self, config): # 初始化Request-Reply服务器 self.reply_server = NameResolvingReplyServer( experiment_name=self.experiment_name, trial_name=self.trial_name, idx=self.worker_index, ) # 注册训练处理函数 self.reply_server.register_handler(\u0026#34;train_step\u0026#34;, self._train_step) def _train_step(self, batch): # 执行训练步骤 loss = self.model.train_step(batch) # 保存权重并更新版本号 self.model.save_weights(self.param_realloc_path) name = names.model_version(self.experiment_name, self.trial_name, self.model_name.role) name_resolve.add(name, self.global_step) return {\u0026#34;loss\u0026#34;: loss, \u0026#34;global_step\u0026#34;: self.global_step} StreamDataset 数据接收 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # realhf/system/stream_dataset.py class StreamDataset: def __init__(self, args, puller_index): # 初始化ZMQ拉取器 - 接收RolloutWorker推送的数据 self.puller = NameResolvingZmqPuller(args, puller_index) def __iter__(self): while True: # 从ZMQ接收数据 data = self.puller.pull() # 转换为训练格式 sample = SequenceSample.from_json_serializable(data) yield sample ZMQ通信层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # realhf/system/push_pull_stream.py class NameResolvingZmqPusher(ZMQJsonPusher): def __init__(self, experiment_name, trial_name, pusher_index, pusher_cnt): # 通过NameResolving获取目标地址 pullers = name_resolve.get_subtree(names.stream_pullers(experiment_name, trial_name)) # 计算路由关系 groups = grouping(pusher_cnt, len(pullers)) puller_index = self._find_target_puller(groups, pusher_index) # 获取目标地址并连接 name = names.push_pull_stream(experiment_name, trial_name, f\u0026#34;puller{puller_index}\u0026#34;) addr = name_resolve.wait(name) host, port = addr.split(\u0026#34;:\u0026#34;) super().__init__(host, int(port)) class NameResolvingZmqPuller(ZMQJsonPuller): def __init__(self, args, puller_index): # 绑定随机端口 host, port = network.gethostip(), network.find_free_port() addr = f\u0026#34;{host}:{port}\u0026#34; # 注册地址到NameResolving name = names.push_pull_stream(args.experiment_name, args.trial_name, f\u0026#34;puller{puller_index}\u0026#34;) name_resolve.add(name, addr) super().__init__(host, port) Request-Reply 通信层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # realhf/system/request_reply_stream.py class NameResolvingRequestClient: def __init__(self, experiment_name, trial_name, n_subscribers, handler_routing): # 创建多个发送socket for i in range(n_subscribers): s = self.context.socket(zmq.PUSH) send_port = s.bind_to_random_port(f\u0026#34;tcp://{host_ip}\u0026#34;) # 注册发送地址 master_send_name = names.request_reply_stream(experiment_name, trial_name, f\u0026#34;master_send_{i}\u0026#34;) name_resolve.add(name=master_send_name, value=f\u0026#34;{host_ip}:{send_port}\u0026#34;) self.send_sockets.append(s) # 创建接收socket self.recv_socket = self.context.socket(zmq.PULL) recv_port = self.recv_socket.bind_to_random_port(f\u0026#34;tcp://{host_ip}\u0026#34;) master_recv_name = names.request_reply_stream(experiment_name, trial_name, \u0026#34;master_recv\u0026#34;) name_resolve.add(name=master_recv_name, value=f\u0026#34;{host_ip}:{recv_port}\u0026#34;) class NameResolvingReplyServer: def __init__(self, experiment_name, trial_name, idx): # 等待MasterWorker注册地址 send_name = names.request_reply_stream(experiment_name, trial_name, \u0026#34;master_recv\u0026#34;) master_recv_addr = name_resolve.wait(send_name, timeout=300) recv_name = names.request_reply_stream(experiment_name, trial_name, f\u0026#34;master_send_{idx}\u0026#34;) master_send_addr = name_resolve.wait(recv_name, timeout=300) # 连接到MasterWorker self.accept(master_send_addr, master_recv_addr) 轨迹数据序列化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 轨迹数据序列化 class Trajectory: def as_json_serializable(self): return { \u0026#34;observations\u0026#34;: self.observations, \u0026#34;actions\u0026#34;: self.actions, \u0026#34;rewards\u0026#34;: self.rewards, \u0026#34;dones\u0026#34;: self.dones, \u0026#34;values\u0026#34;: self.values, \u0026#34;log_probs\u0026#34;: self.log_probs, } # ZMQ传输 self.push_stream.push([traj.as_json_serializable()]) # 接收端反序列化 data = self.puller.pull() sample = SequenceSample.from_json_serializable(data) QA 为什么数据流不通过MasterWorker而是直接到ModelWorker？ ModelWorker直接创建PullerStreamDataset，通过zmq接收RolloutWorker推送的数据。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # realhf/system/model_worker.py class ModelWorker(Worker): def _lazy_setup(self): # 在ModelWorker中创建数据集 datasets = [ data_api.make_dataset( d, self.config.base_seed, self.__dataset_dp_rank, self.__dataset_dp_size, self.config.tokenizer_name_or_path, ) for d in self.config.datasets ] # 特殊处理StreamDataset if not isinstance(self.__datasets[dataset_id], PullerStreamDataset): dataloader_kwargs[\u0026#34;collate_fn\u0026#34;] = data_api.SequenceSample.gather dataloader_kwargs[\u0026#34;batch_size\u0026#34;] = 10240 else: dataloader_kwargs[\u0026#34;batch_size\u0026#34;] = None # StreamDataset不需要batch_size 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class PullerStreamDataset(Dataset): def __init__(self, util, args, dataset_cfgs, pull_timeout_ms=100): # 创建后台线程来拉取数据 self.worker_thread = threading.Thread(target=self._pull_data_worker) self.worker_thread.start() def _pull_data_worker(self): # 在后台线程中创建ZMQ拉取器 stream = NameResolvingZmqPuller( self.args, puller_index=self.util.dp_rank, ) while not self._stop_event.is_set(): # 从ZMQ接收RolloutWorker推送的数据 data = stream.pull(timeout_ms=self.pull_timeout_ms) processed_data = [SequenceSample.from_json_compatible(x) for x in data] # 放入队列供训练使用 self.data_queue.put_nowait(processed_data) def __getitem__(self, idx): # 从队列中获取数据用于训练 samples = [] while True: try: samples += self.data_queue.get_nowait() except queue.Empty: break return samples 目的是为了控制流和数据流的分离，且减少数据中转 。MasterWorker只是做协调训练步骤，而ModelWorker直接接收数据:\n1 2 3 4 5 # MasterWorker: 控制流 await self.func_executor.execute_step() # 协调训练步骤 # ModelWorker: 数据流 stream = NameResolvingZmqPuller(args, puller_index) # 直接接收数据 这里需要理解一点：StreamDataset是持续接收RolloutWorker的数据的，不是按需获取的。stream过程会把数据缓存在内存的queue中，MasterWorker协调训练发生后，ModelWorker从内存队列里直接取数据训练。\n此外，RolloutWorker是按照DP rank分组的，每个ModelWorker负责特定分组的RolloutWorker,通过NameResolving动态发现和链接。\nModelWorker如何和RolloutWorker分组建链？ 问题的本质rollout worker是按照dp分组，那么rollout worker怎么找到对应的model worker的，这其中的服务发现是怎么实现的。\n首先理解如何分组的，比如发送者和接受者的个数不同:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 def grouping(num_senders, num_receivers): groups = {} assert num_senders \u0026gt;= num_receivers # 每个接收者分配多个发送者 senders_per_receiver = num_senders // num_receivers for receiver_id in range(num_receivers): start = receiver_id * senders_per_receiver end = (receiver_id + 1) * senders_per_receiver groups[receiver_id] = list(range(start, end)) # 分配剩余的发送者 remaining = num_senders % num_receivers for i in range(remaining): groups[i].append(num_receivers * senders_per_receiver + i) return groups 1 2 3 4 5 6 7 8 # 假设有6个RolloutWorker，3个ModelWorker grouping(6, 3) # 6个发送者，3个接收者 # 结果： # { # 0: [0, 1], # ModelWorker 0 负责 RolloutWorker 0,1 # 1: [2, 3], # ModelWorker 1 负责 RolloutWorker 2,3 # 2: [4, 5] # ModelWorker 2 负责 RolloutWorker 4,5 # } 其次要理解ModelWorker如何确定自己的DP Rank:\n只有数据并行头节点（tp_rank == 0 and pp_rank == pp_size - 1）才负责接收数据。 每个DP rank对应一个ModelWorker。 DP rank通过拓扑结构确定。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # realhf/system/model_worker.py class ModelWorker(Worker): def _configure(self, cfg): # 遍历所有模型分片，找到数据并行头节点 for s in self.config.shards: _pp_size = s.id.topo.get_dim(\u0026#34;pipe\u0026#34;) # 只有pipeline的最后一个stage且tensor rank为0的才是数据并行头 if not (s.id.tp_rank == 0 and s.id.pp_rank == _pp_size - 1): continue if src_rpc.model_name == s.id.model_name: self.__has_dataset = True self.__dataset_dp_size = s.id.topo.get_dim(\u0026#34;data\u0026#34;) # 总DP数量 self.__dataset_dp_rank = s.id.dp_rank # 当前DP rank break # 注册到NameResolving系统 if self.__has_dataset: name = names.stream_pullers(self.__experiment_name, self.__trial_name) name_resolve.add_subentry(name, str(self.__dataset_dp_rank)) 还要理解RolloutWorker是如何找到对应的ModelWorker的：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # realhf/system/push_pull_stream.py class NameResolvingZmqPusher(ZMQJsonPusher): def __init__(self, experiment_name, trial_name, pusher_index, pusher_cnt, **kwargs): # 1. 获取所有可用的puller（ModelWorker） pullers = name_resolve.get_subtree( names.stream_pullers(experiment_name, trial_name) ) pullers = list(map(int, pullers)) # 转换为整数列表 puller_cnt = len(pullers) # 2. 执行分组算法 groups = grouping(pusher_cnt, puller_cnt) # 3. 找到当前pusher属于哪个puller组 puller_index = None for puller_index, pusher_indices in groups.items(): if pusher_index in pusher_indices: # 这里有个bug，应该是pusher_index break # 4. 通过NameResolving获取目标地址 name = names.push_pull_stream( experiment_name, trial_name, stream_name=f\u0026#34;puller{puller_index}\u0026#34; ) addr = name_resolve.wait(name) host, port = addr.split(\u0026#34;:\u0026#34;) super().__init__(host, int(port), **kwargs) 最后理解完整的匹配流程：\nModelWorker注册 1 2 3 4 5 # ModelWorker启动时 if self.__has_dataset: name = names.stream_pullers(self.__experiment_name, self.__trial_name) name_resolve.add_subentry(name, str(self.__dataset_dp_rank)) # 例如：注册 \u0026#34;puller0\u0026#34;, \u0026#34;puller1\u0026#34;, \u0026#34;puller2\u0026#34; RolloutWorker发现分组 1 2 3 4 5 6 # RolloutWorker启动时 pullers = name_resolve.get_subtree(names.stream_pullers(exp_name, trial_name)) # 获取到 [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;] 表示有3个ModelWorker groups = grouping(6, 3) # 6个RolloutWorker，3个ModelWorker # 结果：{0: [0,1], 1: [2,3], 2: [4,5]} 建立链接 1 2 3 4 5 6 # RolloutWorker 0,1 连接到 ModelWorker 0 # RolloutWorker 2,3 连接到 ModelWorker 1 # RolloutWorker 4,5 连接到 ModelWorker 2 name = names.push_pull_stream(exp_name, trial_name, f\u0026#34;puller{puller_index}\u0026#34;) addr = name_resolve.wait(name) # 等待ModelWorker注册地址 MasterWorker如何和ModelWorker建链？ 与RolloutWorker-ModelWorker的Push-Pull模式（单向）不同，MasterWorker-ModelWorker使用Request-Reply模式（双向）。\nMasterWorker创建Request Client 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # realhf/system/master_worker.py def __lazy_init(self): # 构建handler路由表 handler_routing = copy.deepcopy(self.config.msid2mwid) # 为数据并行添加特殊路由 src_rpc = self.__rpc_srcs[0] src_rpc_topo = self.config.model_topos[src_rpc.model_name] src_rpc_dp_size = src_rpc_topo.get_dim(\u0026#34;data\u0026#34;) src_rpc_pp_size = src_rpc_topo.get_dim(\u0026#34;pipe\u0026#34;) for i in range(src_rpc_dp_size): # 找到每个DP rank对应的ModelWorker rank = src_rpc_topo.get_rank(data=i, pipe=src_rpc_pp_size - 1, tensor=0) handler_routing[f\u0026#34;__data{i}__\u0026#34;] = self.config.msid2mwid[ config_pkg.ModelShardID.from_parallelism_rank( model_name=src_rpc.model_name, topo=src_rpc_topo, parallelism_rank=rank, ) ] # 添加简单的worker_index映射 handler_routing.update({i: i for i in range(self.config.n_model_workers)}) # 创建Request-Reply Stream self.__stream = request_reply_stream.make_master_stream( self.config.worker_info, n_subscribers=self.config.n_model_workers, handler_routing=handler_routing, ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # realhf/system/request_reply_stream.py class NameResolvingRequestClient: def __init__(self, experiment_name, trial_name, n_subscribers, handler_routing): self.context = zmq.Context.instance(io_threads=ZMQ_IO_THREADS) host_ip = socket.gethostbyname(socket.gethostname()) # 1. 为每个ModelWorker创建发送socket self.send_sockets: List[zmq.Socket] = [] for i in range(n_subscribers): s = self.context.socket(zmq.PUSH) send_port = s.bind_to_random_port(f\u0026#34;tcp://{host_ip}\u0026#34;) s.setsockopt(zmq.LINGER, 0) # 注册发送地址到NameResolving master_send_name = names.request_reply_stream( experiment_name, trial_name, f\u0026#34;master_send_{i}\u0026#34; ) name_resolve.add(name=master_send_name, value=f\u0026#34;{host_ip}:{send_port}\u0026#34;) self.send_sockets.append(s) # 2. 创建接收socket self.recv_socket = self.context.socket(zmq.PULL) recv_port = self.recv_socket.bind_to_random_port(f\u0026#34;tcp://{host_ip}\u0026#34;) self.recv_socket.setsockopt(zmq.LINGER, 0) self.recv_address = f\u0026#34;{host_ip}:{recv_port}\u0026#34; # 注册接收地址 master_recv_name = names.request_reply_stream( experiment_name, trial_name, \u0026#34;master_recv\u0026#34; ) name_resolve.add(name=master_recv_name, value=self.recv_address) # 3. 等待所有ModelWorker连接 while ( len( name_resolve.get_subtree( names.request_reply_stream(experiment_name, trial_name, PUBSUB_BARRIER_NAME) ) ) \u0026lt; n_subscribers ): time.sleep(0.1) ModelWorker创建Reply Server 1 2 3 4 5 6 7 # realhf/system/model_worker.py def __lazy_setup(self): # 创建与MasterWorker的连接 self.__stream = request_reply_stream.make_worker_stream( self.config.worker_info, idx=self.__worker_index, ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # realhf/system/request_reply_stream.py class NameResolvingReplyServer: def __init__(self, experiment_name, trial_name, idx): self.context = zmq.Context.instance(io_threads=ZMQ_IO_THREADS) # 1. 等待MasterWorker注册接收地址 send_name = names.request_reply_stream( experiment_name, trial_name, \u0026#34;master_recv\u0026#34; ) try: master_recv_addr = name_resolve.wait(send_name, timeout=300) except TimeoutError as e: logger.error(f\u0026#34;Worker timeout waiting for master receive stream.\u0026#34;) raise e # 2. 等待MasterWorker注册发送地址 recv_name = names.request_reply_stream( experiment_name, trial_name, f\u0026#34;master_send_{idx}\u0026#34; ) try: master_send_addr = name_resolve.wait(recv_name, timeout=300) except TimeoutError as e: logger.error(f\u0026#34;Worker timeout waiting for master send stream\u0026#34;) raise e # 3. 建立连接 self.accept(master_send_addr, master_recv_addr) # 4. 注册到barrier，通知MasterWorker已连接 name_resolve.add_subentry( name=names.request_reply_stream( experiment_name, trial_name, PUBSUB_BARRIER_NAME ), value=socket.gethostbyname(socket.gethostname()), keepalive_ttl=1200, ) def accept(self, server_send_addr: str, server_recv_addr: str): # 连接到MasterWorker的发送socket recv_socket = self.context.socket(zmq.PULL) recv_socket.connect(f\u0026#34;tcp://{server_send_addr}\u0026#34;) recv_socket.setsockopt(zmq.LINGER, 0) self.recv_socket = recv_socket # 连接到MasterWorker的接收socket send_socket = self.context.socket(zmq.PUSH) send_socket.connect(f\u0026#34;tcp://{server_recv_addr}\u0026#34;) send_socket.setsockopt(zmq.LINGER, 0) self.send_socket = send_socket 为什么Request-Reply模式要设计路由表？ 问题本质是Push-Pull模式直接用DP rank分组策略。而MasterWorker和ModelWorker之间的路由策略要设计特定的路由表。\n因为RolloutWorker-ModelWorker的数据流场景有以下特点：\n持续推送：RolloutWorker持续生成数据 负载均衡：只需要确保数据均匀分布 简单映射：一个RolloutWorker组对应一个ModelWorker 无状态：不需要跟踪具体的任务状态 而控制流场景的特点是：\n精确控制：需要精确指定哪个ModelWorker执行哪个任务 复杂拓扑：模型可能有DP、TP、PP等多种并行维度 状态管理：需要跟踪请求-响应的状态 动态分配：任务可能需要根据负载动态分配 核心还是复杂模型的并行拓扑问题，比如还有细粒度的模型分片(tp, pp)等，不是push-pull场景的1：N映射，而是复杂的N:M映射，还需要考虑拓扑、负载、依赖关系等。所以路由表可以确保：\n每个ModelShardID精确映射到对应的ModelWorker 支持一个ModelWorker承载多个模型分片 支持复杂的跨模型通信（如Actor-Critic架构） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 路由表示例 handler_routing = { # 模型分片ID -\u0026gt; ModelWorker索引 \u0026#34;ModelShardID(model_name=\u0026#39;actor\u0026#39;, dp_rank=0, tp_rank=0, pp_rank=0)\u0026#34;: 0, \u0026#34;ModelShardID(model_name=\u0026#39;actor\u0026#39;, dp_rank=1, tp_rank=0, pp_rank=0)\u0026#34;: 1, # 数据并行特殊路由 \u0026#34;__data0__\u0026#34;: 0, # DP rank 0 -\u0026gt; ModelWorker 0 \u0026#34;__data1__\u0026#34;: 1, # DP rank 1 -\u0026gt; ModelWorker 1 # 简单索引映射 0: 0, # ModelWorker 0 1: 1, # ModelWorker 1 } 不同并行场景下的路由表长什么样？ 场景1：纯DP（dp=2）\n配置：\n2个ModelWorker\n1种模型结构，DP=2\n每个ModelWorker承载1个DP rank\n1 2 3 4 5 6 7 8 9 10 11 12 13 handler_routing = { # 模型分片映射 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=0, pp=0): 0, # DP rank 0 -\u0026gt; MW 0 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=0, pp=0): 1, # DP rank 1 -\u0026gt; MW 1 # 数据路由映射 \u0026#34;__data0__\u0026#34;: 0, # 数据0 -\u0026gt; MW 0 \u0026#34;__data1__\u0026#34;: 1, # 数据1 -\u0026gt; MW 1 # 用于Worker间的直接通信 0: 0, # MW 0 -\u0026gt; MW 0 1: 1, # MW 1 -\u0026gt; MW 1 } 特点：\n简单的1:1映射\n每个ModelWorker独立处理一个DP rank\n数据路由与模型分片路由一致\n场景2: DP + TP （DP=2，TP=2）\n配置：\n4个ModelWorker\n1种模型结构，DP=2, TP=2\n每个ModelWorker承载1个模型分片\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 handler_routing = { # 模型分片映射 (DP=2, TP=2) ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=0, pp=0): 0, # (0,0) -\u0026gt; MW 0 副本0的前半 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=1, pp=0): 1, # (0,1) -\u0026gt; MW 1 副本0的后半 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=0, pp=0): 2, # (1,0) -\u0026gt; MW 2 副本1的前半 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=1, pp=0): 3, # (1,1) -\u0026gt; MW 3 副本1的后半 # 数据路由映射 (每个DP rank对应多个TP rank) \u0026#34;__data0__\u0026#34;: 0, # DP rank 0 的head -\u0026gt; MW 0 (tp=0) \u0026#34;__data1__\u0026#34;: 2, # DP rank 1 的head -\u0026gt; MW 2 (tp=0) # 直接索引映射 0: 0, 1: 1, 2: 2, 3: 3, } 前向/反向时，MasterWorker会根据dp/tp/pp的rank，查找ModelShardID，路由到对应的worker（卡号）。\n数据分发时，比如dp=0的数据，直接通过\u0026quot;data0\u0026ldquo;路由到卡0（tp=0的head）；dp=1的数据路由到卡2。\n特点：\n每个DP rank有多个TP分片\n数据路由指向每个DP rank的head (tp=0)\n需要TP内部的通信协调\n场景3：DP + TP + PP （DP=2, TP=2, PP=2）\n配置：\n8个ModelWorker\n1种模型结构，DP=2, TP=2, PP=2\n每个ModelWorker承载1个模型分片\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 handler_routing = { # 模型分片映射 (DP=2, TP=2, PP=2) # PP=0 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=0, pp=0): 0, # (0,0,0) -\u0026gt; MW 0 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=1, pp=0): 1, # (0,1,0) -\u0026gt; MW 1 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=0, pp=0): 2, # (1,0,0) -\u0026gt; MW 2 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=1, pp=0): 3, # (1,1,0) -\u0026gt; MW 3 # PP=1 (最后一层) ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=0, pp=1): 4, # (0,0,1) -\u0026gt; MW 4 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=1, pp=1): 5, # (0,1,1) -\u0026gt; MW 5 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=0, pp=1): 6, # (1,0,1) -\u0026gt; MW 6 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=1, pp=1): 7, # (1,1,1) -\u0026gt; MW 7 # 数据路由映射 (每个dp组的head，通常pp=最后一层, tp=0) \u0026#34;__data0__\u0026#34;: 4, # DP rank 0 的最后一层 -\u0026gt; MW 4 (pp=1, tp=0) \u0026#34;__data1__\u0026#34;: 6, # DP rank 1 的最后一层 -\u0026gt; MW 6 (pp=1, tp=0) # 直接索引映射 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, } 模型函数调用： MasterWorker根据dp/tp/pp的rank，构造ModelShardID，查找handler_routing，路由到对应worker（卡号）。\n例如：要调度dp=1, tp=0, pp=1的分片，查找ModelShardID(dp=1, tp=0, pp=1)，得到worker id=6（卡6）。\n数据分发：\n数据分发通常路由到每个dp组的“head”，即pp=最后一层、tp=0的分片。\n例如：dp=0的数据，查找\u0026rdquo;data0\u0026quot;，得到worker id=4（卡4，dp=0, tp=0, pp=1）。\ndp=1的数据，查找\u0026quot;data1\u0026quot;，得到worker id=6（卡6，dp=1, tp=0, pp=1）。\n特点：\n最复杂的3D并行拓扑\n数据路由指向每个DP rank的最后一层 (pp=1)\n需要PP内部的流水线协调\n场景4：Actor-Critic架构 (DP=2)\n配置：\n2个ModelWorker\nActor和Critic两个模型结构，DP=2\n每个ModelWorker承载Actor和Critic的同一个DP rank\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 handler_routing = { # Actor模型分片 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=0, tp=0, pp=0): 0, # Actor DP=0 -\u0026gt; MW 0 ModelShardID(model=\u0026#34;actor\u0026#34;, dp=1, tp=0, pp=0): 1, # Actor DP=1 -\u0026gt; MW 1 # Critic模型分片 ModelShardID(model=\u0026#34;critic\u0026#34;, dp=0, tp=0, pp=0): 0, # Critic DP=0 -\u0026gt; MW 0 ModelShardID(model=\u0026#34;critic\u0026#34;, dp=1, tp=0, pp=0): 1, # Critic DP=1 -\u0026gt; MW 1 # 数据路由映射 (Actor和Critic共享) \u0026#34;__data0__\u0026#34;: 0, # 数据0 -\u0026gt; MW 0 (Actor和Critic的DP=0) \u0026#34;__data1__\u0026#34;: 1, # 数据1 -\u0026gt; MW 1 (Actor和Critic的DP=1) # 直接索引映射 0: 0, 1: 1, } 特点：\n一个ModelWorker承载多个模型\nActor和Critic共享相同的DP rank\n支持模型间的参数同步\n框架针对不同的拓扑是按照什么顺序切分的？ 从路由表可以看到，3D并行下不同的切分顺序会影响卡和rank的映射，这个问题是一个分布式并行训练的基础问题，和框架的实现一起来理解。\n从代码中可以看到，AReaL框架使用固定的切分顺序：\n1 2 3 4 5 6 # realhf/base/topology.py class ProcessTopology: def __init__(self, axes, dims): # axes定义了切分顺序，dims定义了每个维度的切分大小 self.axes = axes # 切分顺序 self.dims = dims # 切分大小 1 2 3 4 5 # 训练时的拓扑 PipeDataTensorParallelTopology(axes=[\u0026#39;pipe\u0026#39;, \u0026#39;data\u0026#39;, \u0026#39;tensor\u0026#39;]) # 推理时的拓扑 DataPipeTensorParallelTopology(axes=[\u0026#39;data\u0026#39;, \u0026#39;pipe\u0026#39;, \u0026#39;tensor\u0026#39;]) 也就是训练和推理的切分拓扑不同。\n标准顺序：PP -\u0026gt; DP -\u0026gt; TP (训练时)：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 8张卡，DP=2, TP=2, PP=2 # 切分顺序：PP -\u0026gt; DP -\u0026gt; TP rank = pp_rank * (dp_size * tp_size) + dp_rank * tp_size + tp_rank # 映射结果： # 卡0: pp=0, dp=0, tp=0 (rank=0) # 卡1: pp=0, dp=0, tp=1 (rank=1) # 卡2: pp=0, dp=1, tp=0 (rank=2) # 卡3: pp=0, dp=1, tp=1 (rank=3) # 卡4: pp=1, dp=0, tp=0 (rank=4) # 卡5: pp=1, dp=0, tp=1 (rank=5) # 卡6: pp=1, dp=1, tp=0 (rank=6) # 卡7: pp=1, dp=1, tp=1 (rank=7) 原因：\n流水线友好：PP维度相邻的rank在物理上相邻，减少流水线通信开销\n数据并行效率：同一PP stage内的DP rank可以高效进行AllReduce\n内存局部性：同一PP stage的数据在内存上更接近\n推理时：DP -\u0026gt; PP -\u0026gt; TP:\n原因：\n数据分发友好：DP rank相邻，便于数据分发\n推理并行：同一DP组内的PP rank可以并行处理不同batch\n负载均衡：DP维度优先，便于负载均衡\n","permalink":"https://pillumina.github.io/posts/aiinfra/03-areal/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/inclusionAI/AReaL\"\u003ehttps://github.com/inclusionAI/AReaL\u003c/a\u003e\u003cbr\u003e\n纯异步RL方案\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"异步ppo训练调用流程\"\u003e异步PPO训练调用流程\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    A[用户执行: examples/run_async_ppo.sh] --\u0026gt; B[training/main_async_ppo.py]\n    B --\u0026gt; C[AsyncPPOMATHConfig配置解析]\n    C --\u0026gt; D[training/utils.py: run_experiment]\n    \n    D --\u0026gt; E[Ray初始化]\n    E --\u0026gt; F[exp_cfg.initial_setup]\n    F --\u0026gt; G[AsyncRLExperimentConfig.initial_setup]\n    G --\u0026gt; H[创建ExperimentConfig]\n    \n    H --\u0026gt; I[启动Workers]\n    I --\u0026gt; J[MasterWorker]\n    I --\u0026gt; K[ModelWorker]\n    I --\u0026gt; L[GenerationServer]\n    I --\u0026gt; M[GserverManager]\n    I --\u0026gt; N[RolloutWorker]\n    \n    %% MasterWorker训练流程\n    J --\u0026gt; J1[MasterWorker._poll_async]\n    J1 --\u0026gt; J2[FunctionExecutor.execute_step]\n    J2 --\u0026gt; J3[执行数据流图遍历]\n    J3 --\u0026gt; J4[发送训练请求到ModelWorker]\n    \n    %% ModelWorker处理流程\n    K --\u0026gt; K1[ModelWorker._poll]\n    K1 --\u0026gt; K2[接收MasterWorker请求]\n    K2 --\u0026gt; K3[处理训练/推理请求]\n    K3 --\u0026gt; K4[执行模型前向/反向传播]\n    \n    %% Rollout流程\n    N --\u0026gt; N1[RolloutWorker._poll_async]\n    N1 --\u0026gt; N2[load_next_data]\n    N2 --\u0026gt; N3[allocate_new_rollout]\n    N3 --\u0026gt; N4[agent.collect_trajectory]\n    N4 --\u0026gt; N5[env.step计算奖励]\n    N5 --\u0026gt; N6[推送数据到训练端]\n    \n    %% 生成服务器流程\n    L --\u0026gt; L1[GenerationServer._poll]\n    L1 --\u0026gt; L2[启动SGLang子进程]\n    L2 --\u0026gt; L3[处理生成请求]\n    \n    %% 生成服务器管理器\n    M --\u0026gt; M1[GserverManager._poll]\n    M1 --\u0026gt; M2[HTTP服务线程]\n    M2 --\u0026gt; M3[请求调度和权重更新]\n    \n    %% 数据流\n    N6 --\u0026gt; O[stream_dataset.py]\n    O --\u0026gt; J4\n    \n    %% 异步通信\n    J4 -.-\u0026gt;|异步请求| K2\n    N3 -.-\u0026gt;|HTTP请求| M2\n    M2 -.-\u0026gt;|调度请求| L3\n    \n    %% 权重更新\n    K4 --\u0026gt; P[参数更新]\n    P --\u0026gt; Q[权重同步]\n    Q --\u0026gt; M3\n    M3 --\u0026gt; R[更新生成服务器权重]\n    \n    style A fill:#e1f5fe\n    style J fill:#f3e5f5\n    style K fill:#e8f5e8\n    style L fill:#fff3e0\n    style M fill:#fce4ec\n    style N fill:#f1f8e9\n\u003c/pre\u003e\n\n\u003ch3 id=\"用户入口到配置解析\"\u003e用户入口到配置解析\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eexamples/run_async_ppo.sh\u003c/code\u003e → \u003ccode\u003etraining/main_async_ppo.py\u003c/code\u003e\u003c/p\u003e","title":"[RL4LLM] 异步RL框架: Areal"},{"content":" 6.19发布的CloudMatrix384论文拆解，从宏观到基础概念\n核心指标和计算方式 TPOT (Time Per Output Token) 公式： $$TPOT= \\frac{Decode总耗时}{生成Token数量}$$ 测量方式： 从第一个输出Token开始计时，到生成结束（含MoE通信/KV读取） 为什么重要： 直接决定用户体验（如Chatbot响应速度），论文要求 \u0026lt;50ms（严格模式\u0026lt;15ms） 深层意义： 反映系统通信+计算综合能力，EP320下TPOT=42ms证明UB网络突破MoE通信墙 计算效率 (Tokens/s per TFLOPS) 公式： $$计算效率=\\frac {吞吐量(tokens/s)} {NPU峰值算力(TFLOPS)}$$​ 论文数据： 阶段 值 对比基准 Prefill 4.45 超NVIDIA H100+SGLang(3.8) Decode 1.29 超NVIDIA H800+DeepSeek(0.9) 为什么重要： 揭示硬件利用率，1.0以上表明软硬件协同极致优化 深层意义： Decode阶段1.29 → 昇腾910的Cube引擎利用率达 86%（传统GPU仅60%) 缓存访问延迟 (KV Cache Access Latency) 公式： $$延迟=TMMU_{查询}+TUB_{传输}+TDRAM_{读取}​$$ 论文数据： 场景 延迟 对比传统 本地HBM命中 0.2μs - 远程DRAM访问(UB) 1.5μs \u0026gt;10μs (PCIe+IB) 为什么重要： 长上下文推理中70%时间花在KV缓存访问 深层意义： UB统一内存将远程访问性能提升至近本地水平，支撑百万Token上下文。 专家并行扩展性 (EP Degree) 定义：单个MoE层可分布的专家数量 论文突破：EP320（每个昇腾Die托管1个专家） 支撑公式： $$可扩展性=\\frac {UB总带宽}{单个专家通信需求}$$ $$EPmax=\\frac {384×392GB/s} {8B/token×10^6token/s}=320$$ 为什么重要： EP\u0026gt;100时传统网络崩溃，EP320证明UB突破通信可扩展性极限 INT8量化收益 公式：$$ 加速比=\\frac {FP16吞吐}{INT8吞吐}×精度保持率$$ 论文数据： 吞吐提升：1.8倍 精度损失：\u0026lt;0.5%（16个基准测试） 为什么重要： Decode阶段内存带宽减少50%，解决NPU的“内存墙”问题 QA辅助理解 为什么用TPOT而非QPS？ TPOT剥离Batch Size影响，纯粹衡量单次生成效率 更直观反映SLA（用户感知的延迟） 为什么强调计算效率而非绝对吞吐？ 排除工艺优势（7nm vs 5nm），聚焦架构创新价值 1.29 tokens/s/TFLOPS → 证明UB+LEP设计优于NVLink+GPU 为什么测量远程DRAM访问延迟？ 验证内存池化的实际效果，这是打破“内存墙”的核心 1.5μs延迟 → 实现“全集群如单机”的硬件基础 超节点架构 三级网络平面的物理隔离 硬件隔离原理\ngraph TD subgraph Ascend节点 NPU[NPU计算单元] --\u0026gt;|直连| L1_SW[L1 UB交换芯片] CPU[Kunpeng CPU] --\u0026gt;|直连| L1_SW L1_SW --\u0026gt;|专用光纤| L2_SW[L2 UB交换芯片] NPU --\u0026gt;|专用SerDes| RDMA_NIC[RDMA网卡] RDMA_NIC --\u0026gt;|RoCE协议| External_RDMA[外部RDMA网络] CPU --\u0026gt;|PCIe| Qingtian[Qingtian DPU] Qingtian --\u0026gt;|以太网| VPC_Switch[VPC交换机] end 隔离关键点：\n物理链路分离：\nUB平面：NPU/CPU → L1交换芯片 → 专用光纤 → L2交换芯片（通信机柜） RDMA平面：NPU → 板载RDMA SerDes接口 → 外部RoCE网络 VPC平面：CPU → Qingtian DPU → 标准以太网交换机 协议栈隔离：\nUB协议：自定义帧格式（128B载荷+8B路由头），硬件加速 RDMA协议：RoCEv2（兼容InfiniBand生态） VPC协议：TCP/IP + UBoE扩展 流量管控：\nMatrixLink组件：在Qingtian DPU上实现QoS策略 UB平面：最高优先级（保障MoE/KV缓存流量） RDMA平面：中等优先级（训练/推理数据同步） VPC平面：最低优先级（管理/存储流量） 三级平面分层设计价值 解决的关键问题对比\n挑战场景 UB平面解决方案 RDMA平面解决方案 VPC平面解决方案 MoE的Token分发(All-to-All) 全互联拓扑延迟\u0026lt;1.2μs 不适用 不适用 跨节点KV缓存同步 本地化优先（延迟最优） 200Gbps带宽同步 不适用 模型冷启动加载 从内存池直接加载（1.5μs） 从远端存储加载（\u0026gt;10ms） 从对象存储加载（\u0026gt;100ms） 用户请求接入 不适用 不适用 以太网/IP协议接入 分层逻辑： 性能敏感层（UB）： 承载95%的AI内部通信流量 硬件级隔离保障亚微秒延迟 扩展兼容层（RDMA）： 解决超节点间数据同步 兼容行业标准（RoCEv2） 生态接入层（VPC）： 无缝对接现有云设施 通过UBoE逐步增强性能 超节点物理部署架构 graph TB subgraph 超节点CloudMatrix384 subgraph 12个计算机柜 subgraph 节点1 NPU1_0 --\u0026gt; L1_SW1[L1 UB芯片] ... --\u0026gt; L1_SW1 L1_SW1 --\u0026gt;|平面1| L2_SW1[L2 UB交换机] L1_SW1 --\u0026gt;|平面7| L2_SW7 end subgraph 节点48 NPU48_0 --\u0026gt; L1_SW48 L1_SW48 --\u0026gt;|平面1| L2_SW1 L1_SW48 --\u0026gt;|平面7| L2_SW7 end end subgraph 4个通信机柜 L2_SW1[L2 UB交换机组] --\u0026gt;|全互联| 所有L1芯片 L2_SW7[L2 UB交换机组] --\u0026gt;|全互联| 所有L1芯片 end RDMA_NIC组[RDMA网卡集群] --\u0026gt;|RoCE| 跨超节点网络 Qingtian_DPU组 --\u0026gt;|以太网| 数据中心核心交换机 end 关键说明：\n计算部分：\n48个节点（384 NPU + 192 CPU）部署在12个机柜 每个节点含7个L1 UB交换芯片（共48×7=336个L1芯片） 通信部分：\n4个专用通信机柜部署L2 UB交换机组 7个独立子平面 × 16个L2芯片 = 112个L2交换机 每个L1芯片直连16个L2芯片（总链路数=336×16=5,376） 扩展接口：\nRDMA出口：NPU直连的RoCE网卡集群 VPC出口：CPU连接的Qingtian DPU集群 逻辑全局架构图（硬件+软件视图） graph LR subgraph 逻辑资源池 direction TB NPU_Pool[NPU计算池] --\u0026gt;|UB访问| Memory_Pool[全局内存池] CPU_Pool[CPU资源池] --\u0026gt;|UB访问| Memory_Pool Memory_Pool --\u0026gt;|存储| KV_Cache[分布式KV缓存] NPU_Pool --\u0026gt;|RDMA| Other_Supernode[其他超节点] CPU_Pool --\u0026gt;|VPC| DC_Network[数据中心网络] end subgraph 软件服务层 Prefill[Prefill集群] --\u0026gt;|读取| KV_Cache Decode[Decode集群] --\u0026gt;|更新| KV_Cache Cache_Mgr[缓存管理器] --\u0026gt;|调度| KV_Cache MatrixResource --\u0026gt;|资源编排| NPU_Pool MatrixLink --\u0026gt;|网络策略| Memory_Pool end 架构解析：\n硬件抽象层：\n全局内存池：物理分散的NPU HBM + CPU DRAM → 逻辑统一地址空间 动态资源池：NPU/CPU按需组成Prefill/Decode集群（如EP320） 网络平面映射：\nUB平面：承载内存池访问/MoE通信（图中实线） RDMA平面：跨超节点KV同步（虚线） VPC平面：外部服务接入（点划线） 软件控制层：\nMatrixResource：拓扑感知的资源编排（如动态分配160 NPU给Decode） MatrixLink：UB网络QoS保障（优先MoE流量） UB统一总线理解 UB网络核心设计目标 解决传统分布式系统的通信瓶颈：\n问题：MoE模型中的Token分发（Token Dispatch）和专家输出合并（Expert Output Combination）需高频All-to-All通信，传统树状网络（如InfiniBand）因多级转发导致延迟\u0026gt;3μs。 目标：通过全互联直连拓扑实现亚微秒级延迟（\u0026lt;1μs）和接近0带宽衰减。 UB网络物理架构与数据流动 硬件拓扑结构 graph TD subgraph 超节点内通信 A[Ascend 910节点] --\u0026gt;|L1 UB交换芯片| B[L2 UB交换平面] B --\u0026gt;|全互联| C[其他Ascend节点] end L1交换层：每个Ascend 910节点（含8 NPU）配备7个L1 UB交换芯片。 L2交换层：超节点分为7个独立子平面，每个子平面含16个L2 UB交换芯片。 连接方式： 每个L1芯片直连对应子平面的所有16个L2芯片（避免带宽阻塞）。 带宽保障：节点内部带宽（392 GB/s） = L1→L2总带宽（392 GB/s × 7）。 数据流动示例\n假设NPU0需将Token发送至专家255（位于NPU255）： sequenceDiagram participant NPU0 as NPU0（源） participant L1 as L1交换芯片（节点内） participant L2 as L2交换芯片（子平面） participant NPU255 as NPU255（目标） NPU0-\u0026gt;\u0026gt;L1: 发送Token数据（含专家ID=255） L1-\u0026gt;\u0026gt;L2: 根据专家ID选择子平面（如平面#3） L2-\u0026gt;\u0026gt;NPU255: 直连转发至目标NPU NPU255-\u0026gt;\u0026gt;L2: 返回确认信号 L2-\u0026gt;\u0026gt;L1: 回传确认 L1-\u0026gt;\u0026gt;NPU0: 完成传输 关键优化： 零路由表查询：硬件预配置专家ID与L2子平面的映射关系，避免软件寻址开销。 物理直连：L1→L2、L2→目标NPU均为点到点光纤直连，无中间交换机。 通信协议栈与硬件加速 UB协议栈分层设计 层级 功能 物理层 112 Gbps SerDes接口，64B/66B编码，支持392 GB/s单向带宽 链路层 硬件事务拆分（Chunking）和重组，支持最大8KB报文 传输层 零拷贝DMA引擎，绕过CPU和OS内核（类似GPUDirect RDMA） 应用层 融合通信算子（如MoE的Token Dispatch/Combination封装为单条硬件指令） MoE通信的硬件融合 传统流程 graph LR A[Token分发] --\u0026gt; B[专家计算] --\u0026gt; C[输出合并] 需两次独立的All-to-All通信（延迟翻倍）\nUB优化流程 graph LR A[Token分发 + 输出合并] --\u0026gt;|单次融合操作| B[专家计算] 硬件指令：昇腾910内置MOE_FUSED_COMM指令，将两次通信合并为一次。 带宽节省：Token元数据（专家ID）与输出张量共享同一缓存区。 关键场景性能对比 ALL-to-ALL延迟（256 NPU） 架构 延迟（μs） 瓶颈原因 InfiniBand HDR 12.3 多级交换机转发 + 协议栈开销 UB网络 1.2 物理直连 + 硬件融合通信 KV 缓存访问流程 flowchart TD A[NPU需读取历史KV块] --\u0026gt; B{本地HBM命中？} B -- 未命中 --\u0026gt; C[通过UB访问远程DRAM池] C --\u0026gt; D[从CPU内存池直接读取] D --\u0026gt; E[数据返回NPU] 性能优势： 远程DRAM访问延迟≈1.5μs（对比InfiniBand的\u0026gt;10μs）。 带宽利用率\u0026gt;90%（传统方案\u0026lt;50%）。 创新点 1. 全互联拓扑的本质 物理层：用两级星型结构（L1局部全连 + L2分组全连）逼近FullMesh性能，避免O(N²)链路复杂度。 协议层：将通信模式抽象为硬件指令（如ALL_TO_ALL），由NPU通信引擎直接执行。 2. 与NVIDIA方案的对比 特性 NVIDIA NVLink/Switch Huawei UB 拓扑 单节点全连，跨节点树状 超节点内全互联 延迟 节点内0.5μs，跨节点\u0026gt;3μs 全节点\u0026lt;1μs MoE支持 EP144需复杂流水线掩盖延迟 原生支持EP320 内存池化 仅GPU显存共享 NPU+CPU全局内存池 UB网络数据流动全貌 graph LR subgraph CloudMatrix384超节点 direction TB subgraph Node1[Ascend节点1] NPU1_0 --\u0026gt; L1_1[L1交换芯片] NPU1_1 --\u0026gt; L1_1 ... --\u0026gt; L1_1 L1_1 --\u0026gt;|平面1| L2_1[L2交换芯片群] L1_1 --\u0026gt;|平面2| L2_2 L1_1 --\u0026gt;|平面7| L2_7 end subgraph Node48[Ascend节点48] NPU48_0 --\u0026gt; L1_48 ... L1_48 --\u0026gt;|平面1| L2_1 L1_48 --\u0026gt;|平面7| L2_7 end L2_1 --\u0026gt; NPU48_0 L2_7 --\u0026gt; NPU1_0 end 箭头方向：数据通过L1/L2芯片跨节点直连。 带宽保障：每个L1芯片的7条链路独立工作，总带宽=7×392 GB/s。 QA辅助理解 什么是星型拓扑？UB网络是纯星型吗？ 经典星型拓扑：所有节点连接至中心交换机，优点是布线简单，缺点是中心节点易成瓶颈（单点故障/带宽限制）。 UB的改良设计： graph TB subgraph L2子平面 L2_SW1[L2交换机1] L2_SW2[L2交换机2] end NPU0 --\u0026gt;|直连| L1_SW[节点内L1交换机] L1_SW --\u0026gt;|全互联| L2_SW1 L1_SW --\u0026gt;|全互联| L2_SW2 两级星型混合：节点内8 NPU → 7个L1交换机（星型），L1交换机 → 16个L2交换机（全互联）。 优势：用O(N)布线复杂度逼近FullMesh性能，避免中心瓶颈（L2分散负载）。 为什么UB能实现“跨节点延迟\u0026lt;1μs”？ 协议硬化 + 物理直连\n步骤 传统InfiniBand UB优化 数据发出 NPU → 驱动 → 内核协议栈 NPU直连SerDes接口 跨节点路由 多级交换机转发（3-5跳） L1→L2单跳直达（物理直连） 数据接收 内核协议栈 → 驱动 → NPU 直写NPU缓存（DMA引擎） 典型延迟 \u0026gt;3μs 0.8-1.2μs 关键创新： 跳过操作系统协议栈（类似RDMA但更底层） 交换机无路由查找（预配置专家ID→平面映射） 融合通信算子如何减少MoE通信开销？ 合并两次All-to-All为单次硬件事务\n1 2 3 4 5 6 // 昇腾910硬件指令 moe_fused_comm( input_tokens, expert_mask, // 携带路由信息 output_buffer // 预留专家输出空间 ); 效果：单次通信完成分发+合并，延迟降至5μs内（EP320实测）。 什么是“零拷贝DMA”？UB如何实现？ 内存直接访问，跳过CPU复制\n传统流程（以KV缓存读取为例）： 远程DRAM → 网卡 → 主机内存 → PCIe → NPU内存（3次拷贝） UB零拷贝： 远程DRAM → UB网络 → NPU计算单元 硬件支持： NPU集成DMA引擎，直接发起远程内存读写请求 UB交换机维护全局内存地址表（类似CC-NUMA） 带宽利用率：从**\u0026lt;50%（InfiniBand）提升至\u0026gt;90%** 为什么需要7个L2子平面？ 匹配Ascend 910芯片接口，避免带宽阻塞\n数学关系： 单NPU带宽需求：392 GB/s 单L1交换机上行带宽 = 7链路 × 56 GB/s = 392 GB/s 每个L1需直连7组L2才能满足带宽（7×56=392） 容错设计： 任一L2子平面故障不影响其他平面通信 动态路由切换（MatrixLink组件） 分布式KV缓存在UB上如何工作？ 全局内存池 + 硬件一致性协议\nflowchart LR NPU1 --\u0026gt;|UB读请求| CachePool[CPU DRAM池] CachePool --\u0026gt;|返回数据| NPU1 NPU2 --\u0026gt;|并行写| CachePool 一致性保障： 硬件级MESI协议（缓存行锁定） 写冲突时L2交换机仲裁（优先本地化写入） 性能对比：\n场景 传统方案延迟 UB延迟 跨节点读缓存 10μs+ 1.5 UB如何支持动态负载？ 资源池化 + 软件定义拓扑\n硬件基础： NPU/CPU/内存物理解耦 UB网络虚拟化（VXLAN类似技术） 动态调整示例： 1 2 3 4 # 突发流量时扩容Decode集群 if request_queue \u0026gt; threshold: assign_new_npus(\u0026#34;Decode\u0026#34;, 40) # 从空闲池分配40 NPU update_routing_table() # 更新专家路由表 效果：10秒内扩容EP40组，TPOT仍\u0026lt;50ms UB与NVIDIA NVLink本质区别？ 全集群统一总线 vs 节点内互联\n维度 NVLink/Switch UB 拓扑范围 单节点（8 GPU） 384 NPU全域 内存模型 仅GPU显存共享 CPU+NPU全局池化 扩展方式 树状扩展（带宽衰减） 全互联扩展 延迟一致性 跨节点\u0026gt;3μs 全域\u0026lt;1μs 为什么需要专用L1/L2交换芯片？ 解决SerDes电气限制 + 协议卸载\n电气层： 单SerDes链路极限56 GB/s → 需多链路并行 L1芯片聚合8 NPU流量（392 GB/s） 协议层： 卸载路由计算、CRC校验、重传机制 NPU无需处理通信协议（算力100%用于AI） 融合算子具体如何节省带宽？ 元数据与有效载荷合并编码\n传统两次通信： Token分发：发送{token_data, expert_id} 输出合并：发送{expert_output, token_id} 总数据量：2×(数据+ID) UB融合通信： 单次发送{token_data, expert_id, output_buffer_addr} 节省：减少50% ID字段传输，带宽需求下降35% UB如何保障大规模组网的信号完整性？ 光电混合 + 时钟同步技术\n挑战：384 NPU全互联需数万链路，电信号衰减严重 华为方案： 关键路径用光纤替代铜缆（L2交换机间） 全局时钟分发网络（误差\u0026lt;5ps） 接收端自适应均衡（CTLE+DFE） 为什么说UB是“AI原生网络”？ 硬件指令级AI通信原语\n1 2 3 4 5 6 // Ascend 910指令集扩展 ascend_moe_dispatch( expert_mask, // 门控网络输出 token_ptr, // 输入token地址 ub_channel_id // 指定UB虚拟通道 ); 与AI计算流水线深度耦合： 通信操作作为AI计算图的一部分编译 立方体引擎(Cube)执行FFN时自动触发通信 对比传统网络：需CPU调度MPI库，上下文切换开销大 UB全局内存理解 基础概念：KV缓存的核心挑战 问题本质 KV缓存：存储Transformer历史注意力状态（Key/Value向量），用于长上下文推理 挑战： 百万token上下文需TB级内存 → NPU本地HBM（仅32GB）无法容纳 传统方案：跨节点复制数据 → 带宽瓶颈 \u0026amp; 高延迟 传统方案（NVIDIA架构） graph LR A[GPU0] --\u0026gt;|NVLink| B[GPU1本地HBM] B --\u0026gt;|PCIe| C[CPU内存] C --\u0026gt;|InfiniBand| D[远程节点内存] 访问流程： 缺失的KV块从远程节点复制到本地HBM GPU计算单元读取本地HBM 痛点： 高延迟：跨节点复制链路过长（\u0026gt;10μs） 低带宽：InfiniBand带宽（200Gbps） \u0026laquo; HBM带宽（3TB/s） 数据孤岛：GPU显存无法直接共享 华为UB全局内存的硬件基础 物理架构：三级资源池化 graph TB subgraph CloudMatrix384 NPU_HBM[NPU HBM] --\u0026gt; UB CPU_DRAM[CPU DRAM池] --\u0026gt; UB UB --\u0026gt;|统一编址| Global_Memory[全局内存空间] end 核心组件： NPU HBM：昇腾910双Die共32GB，带宽3.2TB/s（本地高速缓存） CPU DRAM池：192鲲鹏CPU的TB级内存（主存储池） UB网络：392GB/s全互联，延迟\u0026lt;1μs（数据通路） 地址映射硬件（类似CC-NUMA） flowchart TD NPU[NPU计算单元] --\u0026gt; MMU[内存管理单元] MMU --\u0026gt;|本地地址？| Local{地址检查} Local --\u0026gt;|是| HBM[本地HBM] Local --\u0026gt;|否| UB_Interface[UB网络接口] UB_Interface --\u0026gt;|全局地址| UB_Switch[UB交换机] UB_Switch --\u0026gt;|路由| Target[目标内存设备] 关键硬件： 全局地址表：存储在UB交换机中，记录物理内存位置 零拷贝DMA：NPU直接发起远程内存读写，无需CPU参与 缓存一致性协议（硬件加速MESI） 挑战：多NPU并发修改同一KV块 解决方案： UB交换机内置一致性引擎 基于物理地址的缓存行锁定（64B粒度） 写冲突时优先本地化处理（Locality-aware） 软件层实现：全局内存访问原理 内存分配（软件API） 1 2 3 4 5 6 7 8 // 在DRAM池分配分布式KV缓存 kv_cache_t* kv_buf = ub_mem_alloc_pooled( UBMEM_DRAM_POOL, // 内存池类型 1024 * 1024 * 1024, // 1GB空间 KV_CACHE_TAG // 标记为KV缓存 ); // NPU直接映射到地址空间 void* npu_virt_addr = ub_map_remote(kv_buf); 关键操作： ub_mem_alloc_pooled：从DRAM池分配物理内存 ub_map_remote：将远程内存映射到NPU虚拟地址空间 KV缓存读取流程 sequenceDiagram NPU-\u0026gt;\u0026gt;UB_MMU: 发起KV块读取(虚拟地址0xFFFF1000) UB_MMU-\u0026gt;\u0026gt;Global_Addr_Table: 查询物理位置(Node15, DRAM_Offset0x8000) Global_Addr_Table--\u0026gt;\u0026gt;UB_MMU: 返回目标地址 UB_MMU-\u0026gt;\u0026gt;UB_Switch: 发送DMA读请求 UB_Switch-\u0026gt;\u0026gt;Node15_DRAM: 读取数据 Node15_DRAM-\u0026gt;\u0026gt;UB_Switch: 返回数据 UB_Switch-\u0026gt;\u0026gt;NPU: 直写NPU寄存器 性能关键： 全程无CPU参与 数据不经过本地HBM（避免复制） 延迟仅 1.5μs（对比NVIDIA \u0026gt;10μs） 缓存服务架构 graph TD Prefill[Prefill NPU] --\u0026gt;|写入新KV块| Cache[DRAM缓存池] Decode[Decode NPU] --\u0026gt;|读取历史块| Cache Cache_Manager[缓存管理器] --\u0026gt;|元数据维护| Cache 去中心化设计： 无全局调度器 → 各NPU通过UB自主访问 缓存位置对软件透明 与NVIDIA方案的对比 架构差异全景图 维度 NVIDIA方案 华为UB方案 硬件基础 GPU显存隔离 + 分层网络 NPU/CPU内存池 + UB全互联 远程访问 显式复制（PCIe→IB→HBM） 直接内存映射（零拷贝） 地址空间 每GPU独立虚拟地址 全域统一虚拟地址 调度约束 请求需路由到持有KV块的GPU 任意NPU可直接访问任意KV块 典型延迟 \u0026gt;10μs（跨节点） 1.5μs（全域） 峰值带宽 ≤200Gbps（InfiniBand） 392GB/s（单NPU单向） 工程实现关键创新 硬件层：DRAM-NPU带宽均衡 问题：DRAM带宽（256GB/s）\u0026lt; NPU需求（392GB/s） 解决方案： 分布式条带化：将KV块切分存储到多CPU内存 并发访问：NPU同时从8个DRAM节点读取 协议层：轻量级一致性控制 优化点： KV缓存只读居多 → 放宽一致性要求 写操作仅限Prefill NPU → 减少协议开销 软件层：缓存感知的KV布局 1 2 3 4 5 6 7 8 // 按Attention Head分片存储 for (int head=0; head\u0026lt;num_heads; head++) { ub_mem_store(kv_cache, head * head_size, // 按Head偏移 data, UB_CACHE_AWARE // 标记为缓存友好布局 ); } 效果： 单次读取可获取完整Attention Head数据 减少随机访问导致的缓存行浪费 QA辅助理解 UB全局内存的硬件工作原理? 三级硬件协同\n1. NPU内存管理单元（MMU）\n接收虚拟地址请求 查询全局地址表（存储在UB交换机） 2. UB交换机\n维护全局地址→物理位置映射 路由请求到目标设备（DRAM或HBM） 3. DMA引擎\n在目标设备执行直接内存访问 数据通过UB网络直送请求方NPU 软件层如何使用UB全局内存？（开发者视角） 透明化API + 缓存优化\n开发者API示例\n1 2 3 4 5 6 7 8 9 10 // 分配分布式KV缓存 kv_cache_t* kv_buf = ub_mem_alloc_pooled( UBMEM_DRAM_POOL, // 指定DRAM池 1024*1024*1024, // 1GB空间 KV_CACHE_TAG // 标记为KV缓存 ); // NPU直接映射远程内存 void* npu_vaddr = ub_map_remote(kv_buf); // 像本地内存一样使用 attention_compute(npu_vaddr + offset); 缓存感知优化\n按Attention Head分片存储： 1 2 for head_idx in range(num_heads): store_kv_block(kv_cache, head_idx * head_size, data) 单次读取获取完整Head数据，减少随机访问 与NVIDIA方案性能对比？数据差异多大？ 数量级提升\n指标 NVIDIA H800 Huawei UB 提升倍数 远程读延迟 \u0026gt;10 μs 1.5 μs 6.7× 有效带宽利用率 \u0026lt;50% \u0026gt;90% 1.8× KV缓存容量 单节点限制 数十TB ∞ 一致性管理开销 软件实现（高开销） 硬件MESI 10×效率 UB如何处理多NPU并发修改同一缓存？ 硬件加速的MESI协议\n缓存行锁定：64B为粒度，写操作时独占锁定 仲裁策略： 读操作：并行执行（无冲突） 写操作：UB交换机按物理位置优先级仲裁 本地节点优先 → 减少网络传输 冲突案例： NPU A和C同时请求写同一KV块 UB交换机检测冲突，赋予Node_A优先权 NPU A完成写后通知NPU C失效旧副本 注意：KV缓存以只读为主（Decode阶段），写冲突率\u0026lt;0.1%。\n为什么CPU DRAM池比NPU HBM更适合存储KV缓存？ 容量与成本的完美平衡\n介质 优势 劣势 适用场景 NPU HBM 超高速（3.2TB/s） 容量小（32GB/NPU） 热点缓存 CPU DRAM 超大容量（TB级） 较慢（256GB/s） 主KV存储池 智能分层： 活跃KV缓存 → 自动迁移至NPU HBM 历史KV缓存 → 存储在CPU DRAM池 ","permalink":"https://pillumina.github.io/posts/aiinfra/01-ascend-cloudmatrix/","summary":"\u003cblockquote\u003e\n\u003cp\u003e6.19发布的CloudMatrix384论文拆解，从宏观到基础概念\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"核心指标和计算方式\"\u003e核心指标和计算方式\u003c/h2\u003e\n\u003ch3 id=\"tpot-time-per-output-token\"\u003e\u003cstrong\u003eTPOT (Time Per Output Token)\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e公式\u003c/strong\u003e： $$TPOT= \\frac{Decode总耗时}{生成Token数量}$$\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e测量方式\u003c/strong\u003e： 从第一个输出Token开始计时，到生成结束（含MoE通信/KV读取）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e为什么重要\u003c/strong\u003e： 直接决定用户体验（如Chatbot响应速度），论文要求 \u003cstrong\u003e\u0026lt;50ms\u003c/strong\u003e（严格模式\u0026lt;15ms）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e深层意义\u003c/strong\u003e： 反映\u003cstrong\u003e系统通信+计算综合能力\u003c/strong\u003e，EP320下TPOT=42ms证明UB网络突破MoE通信墙\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"计算效率-tokenss-per-tflops\"\u003e\u003cstrong\u003e计算效率 (Tokens/s per TFLOPS)\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e公式\u003c/strong\u003e： $$计算效率=\\frac {吞吐量(tokens/s)} {NPU峰值算力(TFLOPS)}$$​\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e论文数据\u003c/strong\u003e：\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e阶段\u003c/th\u003e\n          \u003cth\u003e值\u003c/th\u003e\n          \u003cth\u003e对比基准\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003ePrefill\u003c/td\u003e\n          \u003ctd\u003e4.45\u003c/td\u003e\n          \u003ctd\u003e超NVIDIA H100+SGLang(3.8)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eDecode\u003c/td\u003e\n          \u003ctd\u003e1.29\u003c/td\u003e\n          \u003ctd\u003e超NVIDIA H800+DeepSeek(0.9)\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e为什么重要\u003c/strong\u003e： 揭示\u003cstrong\u003e硬件利用率\u003c/strong\u003e，1.0以上表明软硬件协同极致优化\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e深层意义\u003c/strong\u003e： Decode阶段1.29 → 昇腾910的Cube引擎利用率达 \u003cstrong\u003e86%\u003c/strong\u003e（传统GPU仅60%)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"缓存访问延迟-kv-cache-access-latency\"\u003e\u003cstrong\u003e缓存访问延迟 (KV Cache Access Latency)\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e公式\u003c/strong\u003e： $$延迟=TMMU_{查询}+TUB_{传输}+TDRAM_{读取}​$$\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e论文数据\u003c/strong\u003e：\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e场景\u003c/th\u003e\n          \u003cth\u003e延迟\u003c/th\u003e\n          \u003cth\u003e对比传统\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e本地HBM命中\u003c/td\u003e\n          \u003ctd\u003e0.2μs\u003c/td\u003e\n          \u003ctd\u003e-\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e远程DRAM访问(UB)\u003c/td\u003e\n          \u003ctd\u003e1.5μs\u003c/td\u003e\n          \u003ctd\u003e\u0026gt;10μs (PCIe+IB)\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e为什么重要\u003c/strong\u003e： 长上下文推理中\u003cstrong\u003e70%时间花在KV缓存访问\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e深层意义\u003c/strong\u003e： UB统一内存将远程访问性能提升至\u003cstrong\u003e近本地水平\u003c/strong\u003e，支撑百万Token上下文。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"专家并行扩展性-ep-degree\"\u003e\u003cstrong\u003e专家并行扩展性 (EP Degree)\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e定义\u003c/strong\u003e：单个MoE层可分布的专家数量\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e论文突破\u003c/strong\u003e：\u003cstrong\u003eEP320\u003c/strong\u003e（每个昇腾Die托管1个专家）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e支撑公式\u003c/strong\u003e： $$可扩展性=\\frac {UB总带宽}{单个专家通信需求}$$ $$EPmax=\\frac {384×392GB/s} {8B/token×10^6token/s}=320$$\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e为什么重要\u003c/strong\u003e： EP\u0026gt;100时传统网络崩溃，EP320证明UB突破通信可扩展性极限\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"int8量化收益\"\u003e\u003cstrong\u003eINT8量化收益\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e公式\u003c/strong\u003e：$$ 加速比=\\frac {FP16吞吐}{INT8吞吐}×精度保持率$$\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e论文数据\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e吞吐提升：\u003cstrong\u003e1.8倍\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e精度损失：\u003cstrong\u003e\u0026lt;0.5%\u003c/strong\u003e（16个基准测试）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e为什么重要\u003c/strong\u003e： Decode阶段\u003cstrong\u003e内存带宽减少50%\u003c/strong\u003e，解决NPU的“内存墙”问题\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"qa辅助理解\"\u003eQA辅助理解\u003c/h3\u003e\n\u003ch4 id=\"为什么用tpot而非qps\"\u003e\u003cstrong\u003e为什么用TPOT而非QPS？\u003c/strong\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eTPOT剥离Batch Size影响，\u003cstrong\u003e纯粹衡量单次生成效率\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e更直观反映SLA（用户感知的延迟）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"为什么强调计算效率而非绝对吞吐\"\u003e\u003cstrong\u003e为什么强调计算效率而非绝对吞吐？\u003c/strong\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e排除工艺优势（7nm vs 5nm），\u003cstrong\u003e聚焦架构创新价值\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e1.29 tokens/s/TFLOPS → 证明UB+LEP设计优于NVLink+GPU\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"为什么测量远程dram访问延迟\"\u003e\u003cstrong\u003e为什么测量远程DRAM访问延迟？\u003c/strong\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e验证\u003cstrong\u003e内存池化\u003c/strong\u003e的实际效果，这是打破“内存墙”的核心\u003c/li\u003e\n\u003cli\u003e1.5μs延迟 → 实现“全集群如单机”的硬件基础\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"超节点架构\"\u003e超节点架构\u003c/h2\u003e\n\u003ch3 id=\"三级网络平面的物理隔离\"\u003e三级网络平面的物理隔离\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e硬件隔离原理\u003c/strong\u003e\u003c/p\u003e","title":"昇腾超节点CloudMatrix384论文拆解"},{"content":" 在 Part 1 中，我们介绍了 verl 的初始化过程，我们进一步介绍 verl 的训练过程，包括rollout部分、make experience部分以及training部分。\n在 GRPO 中，单个 step 包含四个阶段：load data -\u0026gt; rollout -\u0026gt; make experience -\u0026gt; update model。区别于前一节的详述，本节会使用伪代码结合源码的方式进行阐述。\nflowchart LR subgraph W2[\u0026#34;Initialize\u0026#34;] WP[Process Data] --\u0026gt; A direction TB D1[Data Prepare] --\u0026gt; A A[TaskRunner] --\u0026gt; B1[RayPPOTrainer] B1 --\u0026gt; Workers subgraph Workers[\u0026#34;Workers\u0026#34;] direction TB WA[ActorRolloutWorker] --\u0026gt; WD[FSDP Engine] WB[CriticWorker] --\u0026gt; WD WC[RewardModelWorker] --\u0026gt; WD WD --\u0026gt; WE[SGLang Engine] end Workers --\u0026gt; C1[Hybrid Engine] end subgraph W3[\u0026#34;Train Loop\u0026#34;] direction TB E[DataLoader] --\u0026gt; RolloutBox subgraph RolloutBox[\u0026#34;Rollout\u0026#34;] F1[Prepare Data] --\u0026gt; F2[SGLang Async Rollout] F2 --\u0026gt; F3[Multi-turn Chat Process] end RolloutBox --\u0026gt; ExpBox subgraph ExpBox[\u0026#34;Make Experience\u0026#34;] G1[Recompute Log Probs] --\u0026gt; G2[Compute Reward] G2 --\u0026gt; G3[Compute Advantage] end ExpBox --\u0026gt; UpdateBox subgraph UpdateBox[\u0026#34;Train The Model\u0026#34;] H1[Load FSDP Model Weight] --\u0026gt; H2[Compute Gradient] H2 --\u0026gt; H3[Weights Update] H3 --\u0026gt; H4[Sync Weights] end UpdateBox --\u0026gt; E end W2 --\u0026gt; W3 数据加载与预处理 verl 通过 DataProto 和 RLHFDataset 来实现数据处理。具体来说，在 main_ppo.py 中，我们观察这个函数：\ncreate_rl_dataset 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 def create_rl_dataset(data_paths, data_config, tokenizer, processor): \u0026#34;\u0026#34;\u0026#34;Create a dataset. Arguments: data_paths: List of paths to data files. data_config: The data config. tokenizer (Tokenizer): The tokenizer. processor (Processor): The processor. Returns: dataset (Dataset): The dataset. \u0026#34;\u0026#34;\u0026#34; from torch.utils.data import Dataset from verl.utils.dataset.rl_dataset import RLHFDataset # Check if a custom dataset class is specified in the data configuration # and if the path to the custom class is provided if \u0026#34;custom_cls\u0026#34; in data_config and data_config.custom_cls.get(\u0026#34;path\u0026#34;, None) is not None: from verl.utils.import_utils import load_extern_type # Dynamically load the custom dataset class dataset_cls = load_extern_type(data_config.custom_cls.path, data_config.custom_cls.name) # Verify that the custom dataset class inherits from torch.utils.data.Dataset if not issubclass(dataset_cls, Dataset): raise TypeError(f\u0026#34;The custom dataset class \u0026#39;{data_config.custom_cls.name}\u0026#39; from \u0026#39;{data_config.custom_cls.path}\u0026#39; must inherit from torch.utils.data.Dataset\u0026#34;) else: # Use the default RLHFDataset class if no custom class is specified dataset_cls = RLHFDataset print(f\u0026#34;Using dataset class: {dataset_cls.__name__}\u0026#34;) # Instantiate the dataset using the determined dataset class dataset = dataset_cls( data_files=data_paths, tokenizer=tokenizer, processor=processor, config=data_config, ) return dataset 非常典型，创造一个了 RLHFDataset 实例，并返回。而具体的 RLHFDataset 实现如下：\nRLHFDataset 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 class RLHFDataset(Dataset): \u0026#34;\u0026#34;\u0026#34; Load and preprocess RLHF data from Parquet files. - Caches files locally. - Reads into a HuggingFace Dataset and tokenizes prompts. - Optionally handles images/videos via a ProcessorMixin. - Filters prompts over a max length. - Supports resuming from checkpoints. Args: data_files (str or list): Path(s) to Parquet file(s). tokenizer (PreTrainedTokenizer): For the tokenization of text to token IDs. config (DictConfig): Options like cache_dir, prompt_key, max_prompt_length, truncation, etc. processor (ProcessorMixin, optional): Multimodal preprocessor for images/videos. \u0026#34;\u0026#34;\u0026#34; def __init__( self, data_files: Union[str, List[str]], tokenizer: PreTrainedTokenizer, config: DictConfig, processor: Optional[ProcessorMixin] = None, ): if not isinstance(data_files, (List, ListConfig)): data_files = [data_files] self.data_files = copy.deepcopy(data_files) self.original_data_files = copy.deepcopy(data_files) # use for resume self.tokenizer = tokenizer self.processor = processor self.config = config self.cache_dir = os.path.expanduser(config.get(\u0026#34;cache_dir\u0026#34;, \u0026#34;~/.cache/verl/rlhf\u0026#34;)) self.prompt_key = config.get(\u0026#34;prompt_key\u0026#34;, \u0026#34;prompt\u0026#34;) self.image_key = config.get(\u0026#34;image_key\u0026#34;, \u0026#34;images\u0026#34;) self.video_key = config.get(\u0026#34;video_key\u0026#34;, \u0026#34;videos\u0026#34;) self.max_prompt_length = config.get(\u0026#34;max_prompt_length\u0026#34;, 1024) self.return_raw_chat = config.get(\u0026#34;return_raw_chat\u0026#34;, False) self.return_full_prompt = config.get(\u0026#34;return_full_prompt\u0026#34;, False) self.truncation = config.get(\u0026#34;truncation\u0026#34;, \u0026#34;error\u0026#34;) self.filter_overlong_prompts = config.get(\u0026#34;filter_overlong_prompts\u0026#34;, True) self.num_workers = config.get(\u0026#34;filter_overlong_prompts_workers\u0026#34;, max(1, os.cpu_count() // 4)) self.num_workers = min(self.num_workers, os.cpu_count()) self.use_shm = config.get(\u0026#34;use_shm\u0026#34;, False) self.chat_template_func = config.get(\u0026#34;chat_template_func\u0026#34;, None) self.need_tools_kwargs = config.get(\u0026#34;need_tools_kwargs\u0026#34;, False) self.filter_prompts = config.get(\u0026#34;filter_prompts\u0026#34;, True) self.serialize_dataset = False self._download() self._read_files_and_tokenize() def _download(self, use_origin_parquet=False): from verl.utils.fs import copy_to_local data_files = self.data_files if not use_origin_parquet else self.original_data_files for i, parquet_file in enumerate(data_files): self.data_files[i] = copy_to_local(src=parquet_file, cache_dir=self.cache_dir, use_shm=self.use_shm) def _read_files_and_tokenize(self): dataframes = [] for parquet_file in self.data_files: # read parquet files and cache dataframe = datasets.load_dataset(\u0026#34;parquet\u0026#34;, data_files=parquet_file)[\u0026#34;train\u0026#34;] dataframes.append(dataframe) self.dataframe: datasets.Dataset = datasets.concatenate_datasets(dataframes) print(f\u0026#34;dataset len: {len(self.dataframe)}\u0026#34;) # filter out too long prompts if self.filter_overlong_prompts: tokenizer = self.tokenizer processor = self.processor prompt_key = self.prompt_key image_key = self.image_key video_key = self.video_key if processor is not None: from verl.utils.dataset.vision_utils import process_image, process_video def doc2len(doc) -\u0026gt; int: messages = self._build_messages(doc) raw_prompt = self.processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False) images = [process_image(image) for image in messages.pop(image_key)] if image_key in messages else None videos = [process_video(video) for video in messages.pop(video_key)] if video_key in messages else None return len(processor(text=[raw_prompt], images=images, videos=videos)[\u0026#34;input_ids\u0026#34;][0]) else: def doc2len(doc) -\u0026gt; int: return len(tokenizer.apply_chat_template(doc[prompt_key], add_generation_prompt=True)) self.dataframe = self.dataframe.filter( lambda doc: doc2len(doc) \u0026lt;= self.max_prompt_length, num_proc=self.num_workers, desc=f\u0026#34;Filtering prompts longer than {self.max_prompt_length} tokens\u0026#34;, ) print(f\u0026#34;filter dataset len: {len(self.dataframe)}\u0026#34;) def resume_dataset_state(self): self.serialize_dataset = not hasattr(self, \u0026#34;original_data_files\u0026#34;) # resume dataframe if not it\u0026#39;s serialized in data.pt if not self.serialize_dataset: self._download(use_origin_parquet=True) # download and resume from original parquet files self._read_files_and_tokenize() else: print(r\u0026#34;old dataloader ckpt file is used, please train from scratch for better ckpt performance\u0026#34;) def __len__(self): return len(self.dataframe) def _build_messages(self, example: dict): messages: list = example.pop(self.prompt_key) if self.image_key in example or self.video_key in example: for message in messages: content = message[\u0026#34;content\u0026#34;] content_list = [] segments = re.split(\u0026#34;(\u0026lt;image\u0026gt;|\u0026lt;video\u0026gt;)\u0026#34;, content) segments = [item for item in segments if item != \u0026#34;\u0026#34;] for segment in segments: if segment == \u0026#34;\u0026lt;image\u0026gt;\u0026#34;: content_list.append({\u0026#34;type\u0026#34;: \u0026#34;image\u0026#34;}) elif segment == \u0026#34;\u0026lt;video\u0026gt;\u0026#34;: content_list.append({\u0026#34;type\u0026#34;: \u0026#34;video\u0026#34;}) else: content_list.append({\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: segment}) message[\u0026#34;content\u0026#34;] = content_list return messages def __getitem__(self, item): \u0026#34;\u0026#34;\u0026#34; Note that we also return the raw_input_ids so that it can be combined with other chat template \u0026#34;\u0026#34;\u0026#34; row_dict: dict = self.dataframe[item] messages = self._build_messages(row_dict) model_inputs = {} if self.processor is not None: from verl.utils.dataset.vision_utils import process_image, process_video raw_prompt = self.processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False) multi_modal_data = {} images = None if self.image_key in row_dict and row_dict.get(self.image_key, None) is not None: images = [process_image(image) for image in row_dict.pop(self.image_key)] multi_modal_data[\u0026#34;image\u0026#34;] = images videos = None if self.video_key in row_dict and row_dict.get(self.video_key, None) is not None: videos = [process_video(video) for video in row_dict.pop(self.video_key)] multi_modal_data[\u0026#34;video\u0026#34;] = [video.numpy() for video in videos] model_inputs = self.processor(text=[raw_prompt], images=images, videos=videos, return_tensors=\u0026#34;pt\u0026#34;) input_ids = model_inputs.pop(\u0026#34;input_ids\u0026#34;) attention_mask = model_inputs.pop(\u0026#34;attention_mask\u0026#34;) if \u0026#34;second_per_grid_ts\u0026#34; in model_inputs: model_inputs.pop(\u0026#34;second_per_grid_ts\u0026#34;) # There\u0026#39;s a trap here, multi_modal_inputs has to be a dict, not BatchFeature row_dict[\u0026#34;multi_modal_data\u0026#34;] = multi_modal_data row_dict[\u0026#34;multi_modal_inputs\u0026#34;] = dict(model_inputs) # second_per_grid_ts isn\u0026#39;t used for training, just for mrope row_dict[\u0026#34;multi_modal_inputs\u0026#34;].pop(\u0026#34;second_per_grid_ts\u0026#34;, None) else: raw_prompt = self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False) model_inputs = self.tokenizer(raw_prompt, return_tensors=\u0026#34;pt\u0026#34;, add_special_tokens=False) input_ids = model_inputs.pop(\u0026#34;input_ids\u0026#34;) attention_mask = model_inputs.pop(\u0026#34;attention_mask\u0026#34;) input_ids, attention_mask = verl_F.postprocess_data( input_ids=input_ids, attention_mask=attention_mask, max_length=self.max_prompt_length, pad_token_id=self.tokenizer.pad_token_id, left_pad=True, truncation=self.truncation, ) if self.processor is not None and \u0026#34;Qwen2VLImageProcessor\u0026#34; in self.processor.image_processor.__class__.__name__: from verl.models.transformers.qwen2_vl import get_rope_index position_ids = [ get_rope_index( self.processor, input_ids=input_ids[0], image_grid_thw=model_inputs.get(\u0026#34;image_grid_thw\u0026#34;), video_grid_thw=model_inputs.get(\u0026#34;video_grid_thw\u0026#34;), second_per_grid_ts=model_inputs.get(\u0026#34;second_per_grid_ts\u0026#34;), attention_mask=attention_mask[0], ) ] # (1, 3, seq_len) else: position_ids = compute_position_id_with_mask(attention_mask) row_dict[\u0026#34;input_ids\u0026#34;] = input_ids[0] row_dict[\u0026#34;attention_mask\u0026#34;] = attention_mask[0] row_dict[\u0026#34;position_ids\u0026#34;] = position_ids[0] raw_prompt_ids = self.tokenizer.encode(raw_prompt, add_special_tokens=False) if len(raw_prompt_ids) \u0026gt; self.max_prompt_length: if self.truncation == \u0026#34;left\u0026#34;: raw_prompt_ids = raw_prompt_ids[-self.max_prompt_length :] elif self.truncation == \u0026#34;right\u0026#34;: raw_prompt_ids = raw_prompt_ids[: self.max_prompt_length] elif self.truncation == \u0026#34;middle\u0026#34;: left_half = self.max_prompt_length // 2 right_half = self.max_prompt_length - left_half raw_prompt_ids = raw_prompt_ids[:left_half] + raw_prompt_ids[-right_half:] elif self.truncation == \u0026#34;error\u0026#34;: raise RuntimeError(f\u0026#34;Prompt length {len(raw_prompt_ids)} is longer than {self.max_prompt_length}.\u0026#34;) row_dict[\u0026#34;raw_prompt_ids\u0026#34;] = raw_prompt_ids # encode prompts without chat template if self.return_raw_chat: row_dict[\u0026#34;raw_prompt\u0026#34;] = messages # get prompts with chat template if self.return_full_prompt: row_dict[\u0026#34;full_prompts\u0026#34;] = raw_prompt # array of strings # add index for each prompt index = row_dict.get(\u0026#34;extra_info\u0026#34;, {}).get(\u0026#34;index\u0026#34;, 0) tools_kwargs = row_dict.get(\u0026#34;extra_info\u0026#34;, {}).get(\u0026#34;tools_kwargs\u0026#34;, {}) need_tools_kwargs = row_dict.get(\u0026#34;extra_info\u0026#34;, {}).get(\u0026#34;need_tools_kwargs\u0026#34;, self.need_tools_kwargs) if need_tools_kwargs and not tools_kwargs: logger.warning(\u0026#34;tools_kwargs is empty for index {}, data source: {}\u0026#34;, index, row_dict[\u0026#34;data_source\u0026#34;]) row_dict[\u0026#34;index\u0026#34;] = index row_dict[\u0026#34;tools_kwargs\u0026#34;] = tools_kwargs return row_dict def __getstate__(self): if not self.serialize_dataset: state = self.__dict__.copy() if \u0026#34;dataframe\u0026#34; in state: del state[\u0026#34;dataframe\u0026#34;] return state return self.__dict__.copy() 支持从远程存储下载 Parquet 文件到本地缓存，支持共享内存加速文件访问，自动管理文件路径，支持检查点恢复。 使用 HuggingFace datasets 库读取 Parquet 文件，支持多个数据文件的合并，自动处理数据格式转换。 根据最大长度过滤过长的 prompts，支持多进程并行处理，可配置的过滤策略。 支持图像和视频的多模态输入，解析 \u0026lt;image\u0026gt; 和 \u0026lt;video\u0026gt; 标签，将多模态内容转换为结构化格式。 添加 chat template 来格式化对话，将文本转换为 token IDs，生成 attn mask 和 position ids。 padding 到指定长度，支持多种截断策略（left, right, middle, error），生成位置编码。 支持训练中断后的恢复，可以从原始文件重新构建数据集，兼容序列化/反序列化。 返回包含以下关键字段的字典：input_ids, attention_mask, position_ids, raw_prompt_ids, multi_modal_data, multi_modal_inputs, index, tools_kwargs。 这里最重要的一个参数是 tools_kwargs，用于为不同的 tools 提供配置参数。它的结构如下：\n1 2 3 4 5 6 7 8 tools_kwargs = { \u0026#34;tool_name\u0026#34;: { \u0026#34;create_kwargs\u0026#34;: {...}, # 工具创建时的参数 \u0026#34;execute_kwargs\u0026#34;: {...}, # 工具执行时的参数（可选） \u0026#34;calc_reward_kwargs\u0026#34;: {...}, # 计算奖励时的参数（可选） \u0026#34;release_kwargs\u0026#34;: {...}, # 释放资源时的参数（可选） } } 比如 Search-R1 的 tools_kwargs 如下：\n1 2 3 4 5 6 7 8 9 tools_kwargs = { \u0026#34;search-r1\u0026#34;: { \u0026#34;create_kwargs\u0026#34;: { \u0026#34;ground_truth\u0026#34;: ground_truth, \u0026#34;question\u0026#34;: question, \u0026#34;data_source\u0026#34;: data_source_tagged } } } 具体这些参数是如何调用了一个 tool，我们会留在后续部分继续介绍。\n训练入口 RayPPOTrainer.fit() 创建 Tracking 日志记录器，设置全局步数，加载检查点，并在训练前进行验证。 使用 tqdm 创建进度条，显示训练进度，并设置初始步数。 遍历配置的总 epoch 数和数据加载器，每个 train batch 更新多步。 从 batch 中分离出用于 rollout 的数据（input_ids, attention_mask, position_ids 等），保留其他数据用于后续处理。 调用 ActorRolloutWorker 生成序列，并记录生成时间。 处理 REMAX 基线（如果使用）：生成确定性基线序列，计算基线奖励，用于 REMAX 优势估计器。 为每个样本分配唯一 ID，重复数据以对齐多次采样，计算响应掩码，并可选地进行批次平衡。 根据配置使用奖励模型或自定义奖励函数计算 token 级别的奖励分数，支持同步和异步计算。 使用 megatron 基于训练开始前的 policy 重新计算 behaviour policy 的 log probabilities，用于重要性采样，同时计算熵值。（原因在 part 1讲过） 使用 reference policy 计算 log probs，用于 KL 散度计算。 使用 Critic 网络计算状态价值，用于优势函数估计。 根据配置的优势估计器（GAE、GRPO、REMAX 等）计算优势函数，支持 KL 惩罚。 使用计算出的优势函数更新 Critic 网络参数。 在 Critic 预热完成后，使用 PPO 损失函数更新 Actor 网络参数。 将生成的序列、输入、输出和分数保存到指定目录。 根据配置的频率执行验证，计算验证指标并记录。 根据配置的频率保存模型检查点。 收集训练指标、时序指标和吞吐量指标，并记录到日志系统。 更新进度条，递增全局步数，并在达到总训练步数时结束训练。 根据配置在特定步数启用/禁用性能分析，用于调试和优化。 RayPPOTrainer.fit() 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 def fit(self): \u0026#34;\u0026#34;\u0026#34; The training loop of PPO. The driver process only need to call the compute functions of the worker group through RPC to construct the PPO dataflow. The light-weight advantage computation is done on the driver process. \u0026#34;\u0026#34;\u0026#34; from omegaconf import OmegaConf from verl.utils.tracking import Tracking logger = Tracking( project_name=self.config.trainer.project_name, experiment_name=self.config.trainer.experiment_name, default_backend=self.config.trainer.logger, config=OmegaConf.to_container(self.config, resolve=True), ) self.global_steps = 0 # load checkpoint before doing anything self._load_checkpoint() # perform validation before training # currently, we only support validation using the reward_function. if self.val_reward_fn is not None and self.config.trainer.get(\u0026#34;val_before_train\u0026#34;, True): val_metrics = self._validate() assert val_metrics, f\u0026#34;{val_metrics=}\u0026#34; pprint(f\u0026#34;Initial validation metrics: {val_metrics}\u0026#34;) logger.log(data=val_metrics, step=self.global_steps) if self.config.trainer.get(\u0026#34;val_only\u0026#34;, False): return # add tqdm progress_bar = tqdm(total=self.total_training_steps, initial=self.global_steps, desc=\u0026#34;Training Progress\u0026#34;) # we start from step 1 self.global_steps += 1 last_val_metrics = None for epoch in range(self.config.trainer.total_epochs): for batch_dict in self.train_dataloader: do_profile = self.global_steps in self.config.trainer.profile_steps if self.config.trainer.profile_steps is not None else False if do_profile: self.actor_rollout_wg.start_profile() if self.use_reference_policy: self.ref_policy_wg.start_profile() if self.use_critic: self.critic_wg.start_profile() if self.use_rm: self.rm_wg.start_profile() metrics = {} timing_raw = {} batch: DataProto = DataProto.from_single_dict(batch_dict) # pop those keys for generation batch_keys_to_pop = [\u0026#34;input_ids\u0026#34;, \u0026#34;attention_mask\u0026#34;, \u0026#34;position_ids\u0026#34;] non_tensor_batch_keys_to_pop = [\u0026#34;raw_prompt_ids\u0026#34;] if \u0026#34;multi_modal_data\u0026#34; in batch.non_tensor_batch: non_tensor_batch_keys_to_pop.append(\u0026#34;multi_modal_data\u0026#34;) if \u0026#34;raw_prompt\u0026#34; in batch.non_tensor_batch: non_tensor_batch_keys_to_pop.append(\u0026#34;raw_prompt\u0026#34;) if \u0026#34;tools_kwargs\u0026#34; in batch.non_tensor_batch: non_tensor_batch_keys_to_pop.append(\u0026#34;tools_kwargs\u0026#34;) gen_batch = batch.pop( batch_keys=batch_keys_to_pop, non_tensor_batch_keys=non_tensor_batch_keys_to_pop, ) is_last_step = self.global_steps \u0026gt;= self.total_training_steps with marked_timer(\u0026#34;step\u0026#34;, timing_raw): # generate a batch with marked_timer(\u0026#34;gen\u0026#34;, timing_raw, color=\u0026#34;red\u0026#34;): if not self.async_rollout_mode: gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch) else: self.async_rollout_manager.wake_up() gen_batch_output = self.async_rollout_manager.generate_sequences(gen_batch) self.async_rollout_manager.sleep() timing_raw.update(gen_batch_output.meta_info[\u0026#34;timing\u0026#34;]) gen_batch_output.meta_info.pop(\u0026#34;timing\u0026#34;, None) if self.config.algorithm.adv_estimator == AdvantageEstimator.REMAX: with marked_timer(\u0026#34;gen_max\u0026#34;, timing_raw, color=\u0026#34;purple\u0026#34;): gen_baseline_batch = deepcopy(gen_batch) gen_baseline_batch.meta_info[\u0026#34;do_sample\u0026#34;] = False gen_baseline_output = self.actor_rollout_wg.generate_sequences(gen_baseline_batch) batch = batch.union(gen_baseline_output) reward_baseline_tensor = self.reward_fn(batch) reward_baseline_tensor = reward_baseline_tensor.sum(dim=-1) batch.pop(batch_keys=list(gen_baseline_output.batch.keys())) batch.batch[\u0026#34;reward_baselines\u0026#34;] = reward_baseline_tensor del gen_baseline_batch, gen_baseline_output batch.non_tensor_batch[\u0026#34;uid\u0026#34;] = np.array([str(uuid.uuid4()) for _ in range(len(batch.batch))], dtype=object) # repeat to align with repeated responses in rollout batch = batch.repeat(repeat_times=self.config.actor_rollout_ref.rollout.n, interleave=True) batch = batch.union(gen_batch_output) batch.batch[\u0026#34;response_mask\u0026#34;] = compute_response_mask(batch) # Balance the number of valid tokens across DP ranks. # NOTE: This usually changes the order of data in the `batch`, # which won\u0026#39;t affect the advantage calculation (since it\u0026#39;s based on uid), # but might affect the loss calculation (due to the change of mini-batching). # TODO: Decouple the DP balancing and mini-batching. if self.config.trainer.balance_batch: self._balance_batch(batch, metrics=metrics) # compute global_valid tokens batch.meta_info[\u0026#34;global_token_num\u0026#34;] = torch.sum(batch.batch[\u0026#34;attention_mask\u0026#34;], dim=-1).tolist() with marked_timer(\u0026#34;reward\u0026#34;, timing_raw, color=\u0026#34;yellow\u0026#34;): # compute reward model score if self.use_rm: reward_tensor = self.rm_wg.compute_rm_score(batch) batch = batch.union(reward_tensor) if self.config.reward_model.launch_reward_fn_async: future_reward = compute_reward_async.remote(batch, self.config, self.tokenizer) else: reward_tensor, reward_extra_infos_dict = compute_reward(batch, self.reward_fn) # recompute old_log_probs with marked_timer(\u0026#34;old_log_prob\u0026#34;, timing_raw, color=\u0026#34;blue\u0026#34;): old_log_prob = self.actor_rollout_wg.compute_log_prob(batch) entropys = old_log_prob.batch[\u0026#34;entropys\u0026#34;] response_masks = batch.batch[\u0026#34;response_mask\u0026#34;] loss_agg_mode = self.config.actor_rollout_ref.actor.loss_agg_mode entropy_agg = agg_loss(loss_mat=entropys, loss_mask=response_masks, loss_agg_mode=loss_agg_mode) old_log_prob_metrics = {\u0026#34;actor/entropy\u0026#34;: entropy_agg.detach().item()} metrics.update(old_log_prob_metrics) old_log_prob.batch.pop(\u0026#34;entropys\u0026#34;) batch = batch.union(old_log_prob) if \u0026#34;rollout_log_probs\u0026#34; in batch.batch.keys(): # TODO: we may want to add diff of probs too. rollout_old_log_probs = batch.batch[\u0026#34;rollout_log_probs\u0026#34;] actor_old_log_probs = batch.batch[\u0026#34;old_log_probs\u0026#34;] attention_mask = batch.batch[\u0026#34;attention_mask\u0026#34;] responses = batch.batch[\u0026#34;responses\u0026#34;] response_length = responses.size(1) response_mask = attention_mask[:, -response_length:] rollout_probs = torch.exp(rollout_old_log_probs) actor_probs = torch.exp(actor_old_log_probs) rollout_probs_diff = torch.abs(rollout_probs - actor_probs) rollout_probs_diff = torch.masked_select(rollout_probs_diff, response_mask.bool()) rollout_probs_diff_max = torch.max(rollout_probs_diff) rollout_probs_diff_mean = torch.mean(rollout_probs_diff) rollout_probs_diff_std = torch.std(rollout_probs_diff) metrics.update( { \u0026#34;training/rollout_probs_diff_max\u0026#34;: rollout_probs_diff_max.detach().item(), \u0026#34;training/rollout_probs_diff_mean\u0026#34;: rollout_probs_diff_mean.detach().item(), \u0026#34;training/rollout_probs_diff_std\u0026#34;: rollout_probs_diff_std.detach().item(), } ) if self.use_reference_policy: # compute reference log_prob with marked_timer(\u0026#34;ref\u0026#34;, timing_raw, color=\u0026#34;olive\u0026#34;): if not self.ref_in_actor: ref_log_prob = self.ref_policy_wg.compute_ref_log_prob(batch) else: ref_log_prob = self.actor_rollout_wg.compute_ref_log_prob(batch) batch = batch.union(ref_log_prob) # compute values if self.use_critic: with marked_timer(\u0026#34;values\u0026#34;, timing_raw, color=\u0026#34;cyan\u0026#34;): values = self.critic_wg.compute_values(batch) batch = batch.union(values) with marked_timer(\u0026#34;adv\u0026#34;, timing_raw, color=\u0026#34;brown\u0026#34;): # we combine with rule-based rm reward_extra_infos_dict: dict[str, list] if self.config.reward_model.launch_reward_fn_async: reward_tensor, reward_extra_infos_dict = ray.get(future_reward) batch.batch[\u0026#34;token_level_scores\u0026#34;] = reward_tensor if reward_extra_infos_dict: batch.non_tensor_batch.update({k: np.array(v) for k, v in reward_extra_infos_dict.items()}) # compute rewards. apply_kl_penalty if available if self.config.algorithm.use_kl_in_reward: batch, kl_metrics = apply_kl_penalty(batch, kl_ctrl=self.kl_ctrl_in_reward, kl_penalty=self.config.algorithm.kl_penalty) metrics.update(kl_metrics) else: batch.batch[\u0026#34;token_level_rewards\u0026#34;] = batch.batch[\u0026#34;token_level_scores\u0026#34;] # compute advantages, executed on the driver process norm_adv_by_std_in_grpo = self.config.algorithm.get(\u0026#34;norm_adv_by_std_in_grpo\u0026#34;, True) # GRPO adv normalization factor batch = compute_advantage( batch, adv_estimator=self.config.algorithm.adv_estimator, gamma=self.config.algorithm.gamma, lam=self.config.algorithm.lam, num_repeat=self.config.actor_rollout_ref.rollout.n, norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo, multi_turn=self.config.actor_rollout_ref.rollout.multi_turn.enable, config=self.config.algorithm, ) # update critic if self.use_critic: with marked_timer(\u0026#34;update_critic\u0026#34;, timing_raw, color=\u0026#34;pink\u0026#34;): critic_output = self.critic_wg.update_critic(batch) critic_output_metrics = reduce_metrics(critic_output.meta_info[\u0026#34;metrics\u0026#34;]) metrics.update(critic_output_metrics) # implement critic warmup if self.config.trainer.critic_warmup \u0026lt;= self.global_steps: # update actor with marked_timer(\u0026#34;update_actor\u0026#34;, timing_raw, color=\u0026#34;red\u0026#34;): batch.meta_info[\u0026#34;multi_turn\u0026#34;] = self.config.actor_rollout_ref.rollout.multi_turn.enable actor_output = self.actor_rollout_wg.update_actor(batch) actor_output_metrics = reduce_metrics(actor_output.meta_info[\u0026#34;metrics\u0026#34;]) metrics.update(actor_output_metrics) # Log rollout generations if enabled rollout_data_dir = self.config.trainer.get(\u0026#34;rollout_data_dir\u0026#34;, None) if rollout_data_dir: with marked_timer(\u0026#34;dump_rollout_generations\u0026#34;, timing_raw, color=\u0026#34;green\u0026#34;): print(batch.batch.keys()) inputs = self.tokenizer.batch_decode(batch.batch[\u0026#34;prompts\u0026#34;], skip_special_tokens=True) outputs = self.tokenizer.batch_decode(batch.batch[\u0026#34;responses\u0026#34;], skip_special_tokens=True) scores = batch.batch[\u0026#34;token_level_scores\u0026#34;].sum(-1).cpu().tolist() self._dump_generations( inputs=inputs, outputs=outputs, scores=scores, reward_extra_infos_dict=reward_extra_infos_dict, dump_path=rollout_data_dir, ) # validate if self.val_reward_fn is not None and self.config.trainer.test_freq \u0026gt; 0 and (is_last_step or self.global_steps % self.config.trainer.test_freq == 0): with marked_timer(\u0026#34;testing\u0026#34;, timing_raw, color=\u0026#34;green\u0026#34;): val_metrics: dict = self._validate() if is_last_step: last_val_metrics = val_metrics metrics.update(val_metrics) if self.config.trainer.save_freq \u0026gt; 0 and (is_last_step or self.global_steps % self.config.trainer.save_freq == 0): with marked_timer(\u0026#34;save_checkpoint\u0026#34;, timing_raw, color=\u0026#34;green\u0026#34;): self._save_checkpoint() # training metrics metrics.update( { \u0026#34;training/global_step\u0026#34;: self.global_steps, \u0026#34;training/epoch\u0026#34;: epoch, } ) # collect metrics metrics.update(compute_data_metrics(batch=batch, use_critic=self.use_critic)) metrics.update(compute_timing_metrics(batch=batch, timing_raw=timing_raw)) # TODO: implement actual tflpo and theoretical tflpo n_gpus = self.resource_pool_manager.get_n_gpus() metrics.update(compute_throughout_metrics(batch=batch, timing_raw=timing_raw, n_gpus=n_gpus)) # TODO: make a canonical logger that supports various backend logger.log(data=metrics, step=self.global_steps) progress_bar.update(1) self.global_steps += 1 if do_profile: self.actor_rollout_wg.stop_profile() if self.use_reference_policy: self.ref_policy_wg.stop_profile() if self.use_critic: self.critic_wg.stop_profile() if self.use_rm: self.rm_wg.stop_profile() if is_last_step: pprint(f\u0026#34;Final validation metrics: {last_val_metrics}\u0026#34;) progress_bar.close() return 我们究竟在异步什么？ 这里很值得分享一个核心问题，对 SGLang 而言，或者对现在的 RL 而言，我们每天说来说去的 async 究竟是什么意思？和 PD 分离一样，async 也有非常多的层面：\nAsync RL 代表的是在 training rollout 分离的系统上，rollout 只在 update weights 的时候被打断，其余时刻永远 rollout，哪怕 target policy 正在被 training engine 更新。这方面是 AreaL 和 SLIME。\nAsync Rollout 这个词是特指在 rollout 的时候，把一个 batch requests 拆为单个 request，然后逐个调用 SGLangEngine.generate()。\n乍一听，这没有什么特别的，似乎还会更慢些。但是考虑到 tool call 的问题，这就非常严肃了。假设我们把一整个 batch 的 requests 作为一个 batch 塞给 sglang 似乎还要快些，毕竟对 SGLang 的 scheduler 而言，更好组 batch。但是，一整个 batch 进去，得一整个 batch 出来。这些 batch 里面的 requests 同时返回，同时被 paser 解析查看是否有 tool call 的 parameter，然后发送请求给 tool。如此以来，整个 tool 的调用大概率会拥堵，甚至在我们考虑到如果要加入多个 tool（虽然目前没有）的话，用一个状态机去管理每个 request 的 tool call 状态会成一场噩梦，何况有的 requests 会在多轮里面多次调用 tool。因此，为了方便管理每个 request tool call 的状态机和让 tool 被调度的更加均匀。SGLang 采取了 Async Rollout 策略，也即把一个 batch 的 requests 拆为单个 request，然后逐个异步调用 SGLangEngine.generate()。这样每个 reqeuest 自己管理自己的状态机，方便维护并且 tool call 效率更高。\n理解了这一层，我们可以来看看代码实现：\ngenerate_sequences 源码 1 2 3 4 5 6 7 @GPUMemoryLogger(role=\u0026#34;sglang rollout\u0026#34;, logger=logger) @torch.no_grad() def generate_sequences(self, prompts: DataProto, **kwargs) -\u0026gt; DataProto: if self.config.multi_turn.enable: return self._req_level_generate_sequences(prompts, **kwargs) return self._batch_level_generate_sequences(prompts, **kwargs) 这里明确指出，如果是用了 mutli-turn 训练，则将 batch 的 requests 拆为单个 request，调用 _req_level_generate_sequences；而不调用 tool 的单轮 RL，仍旧组 batch 直接发送。\n我们只观察 _req_level_generate_sequences 的部分源码：\n_req_level_generate_sequences 部分源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @GPUMemoryLogger(role=\u0026#34;sglang rollout\u0026#34;, logger=logger) @torch.no_grad() def _req_level_generate_sequences(self, prompts: DataProto, **kwargs) -\u0026gt; DataProto: # Async rollout with tools support do_sample = prompts.meta_info.get(\u0026#34;do_sample\u0026#34;, True) is_validate = prompts.meta_info.get(\u0026#34;validate\u0026#34;, False) tgt_device = prompts.batch[\u0026#34;input_ids\u0026#34;].device if self._tp_rank == 0: req_list = self._preprocess_prompt_to_async_rollout_requests( prompts, n=1 if is_validate else self.config.n, ) loop = asyncio.get_event_loop() output_req_list = loop.run_until_complete( asyncio.gather( *[self._async_rollout_a_request(req, do_sample, is_validate, **kwargs) for req in req_list], ) ) sorted_output_req_list = sorted(output_req_list, key=lambda x: (x.batch_data_id, x.rollout_offset)) else: sorted_output_req_list = None 现在来看，asyncio.gather(*[self._async_rollout_a_request(req, do_sample, is_validate, **kwargs) for req in req_list],) 就显得无比清晰了。\n数据流管理 我们继续去理解 RayPPOTrainer.fit() 函数，从数据流管理开始。这里我认为最重要的两个类是 DataProto 和 RLHFDataset。\nDataProto DataProto 是 verl 的数据交换协议，定义在 protocol.py：\n1 2 3 4 5 6 7 8 9 10 11 12 @dataclass class DataProto: \u0026#34;\u0026#34;\u0026#34; A DataProto is a data structure that aims to provide a standard protocol for data exchange between functions. It contains a batch (TensorDict) and a meta_info (Dict). The batch is a TensorDict https://pytorch.org/tensordict/. TensorDict allows you to manipulate a dictionary of Tensors like a single Tensor. Ideally, the tensors with the same batch size should be put inside batch. \u0026#34;\u0026#34;\u0026#34; batch: TensorDict = None non_tensor_batch: Dict = field(default_factory=dict) meta_info: Dict = field(default_factory=dict) DataProto 提供标准化的数据交换协议，基于 PyTorch 的 TensorDict，支持张量的批量操作，同时通过 non_tensor_batch 字典来处理 NumPy 数组等非张量数据。meta_info 存储额外的元信息。本身支持的操作挺基础的，典型的比如数据创建、切片、选择、合并、重命名、重复、填充、分块、以及分布式环境下的数据集合与分发。除此之外，DataProto 还通过数据验证 check_consistency() 确保在数据分离和合并过程的一致性。\nRLHFDataset RLHFDataset 是 verl 中用于 RLHF 数据加载的数据集类，继承自 datasets.Dataset，主要用于处理 Parquet 文件中的数据，包括数据下载、tokenize、过滤、预处理等。\nRLHFDataset 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class RLHFDataset(Dataset): \u0026#34;\u0026#34;\u0026#34; Load and preprocess RLHF data from Parquet files. - Caches files locally. - Reads into a HuggingFace Dataset and tokenizes prompts. - Optionally handles images/videos via a ProcessorMixin. - Filters prompts over a max length. - Supports resuming from checkpoints. Args: data_files (str or list): Path(s) to Parquet file(s). tokenizer (PreTrainedTokenizer): For the tokenization of text to token IDs. config (DictConfig): Options like cache_dir, prompt_key, max_prompt_length, truncation, etc. processor (ProcessorMixin, optional): Multimodal preprocessor for images/videos. \u0026#34;\u0026#34;\u0026#34; def __init__( self, data_files: Union[str, List[str]], tokenizer: PreTrainedTokenizer, config: DictConfig, processor: Optional[ProcessorMixin] = None, ): if not isinstance(data_files, (List, ListConfig)): data_files = [data_files] self.data_files = copy.deepcopy(data_files) self.original_data_files = copy.deepcopy(data_files) # use for resume self.tokenizer = tokenizer self.processor = processor self.config = config self.cache_dir = os.path.expanduser(config.get(\u0026#34;cache_dir\u0026#34;, \u0026#34;~/.cache/verl/rlhf\u0026#34;)) self.prompt_key = config.get(\u0026#34;prompt_key\u0026#34;, \u0026#34;prompt\u0026#34;) self.image_key = config.get(\u0026#34;image_key\u0026#34;, \u0026#34;images\u0026#34;) self.video_key = config.get(\u0026#34;video_key\u0026#34;, \u0026#34;videos\u0026#34;) self.max_prompt_length = config.get(\u0026#34;max_prompt_length\u0026#34;, 1024) self.return_raw_chat = config.get(\u0026#34;return_raw_chat\u0026#34;, False) self.return_full_prompt = config.get(\u0026#34;return_full_prompt\u0026#34;, False) self.truncation = config.get(\u0026#34;truncation\u0026#34;, \u0026#34;error\u0026#34;) self.filter_overlong_prompts = config.get(\u0026#34;filter_overlong_prompts\u0026#34;, True) self.num_workers = config.get(\u0026#34;filter_overlong_prompts_workers\u0026#34;, max(1, os.cpu_count() // 4)) self.num_workers = min(self.num_workers, os.cpu_count()) self.use_shm = config.get(\u0026#34;use_shm\u0026#34;, False) self.chat_template_func = config.get(\u0026#34;chat_template_func\u0026#34;, None) self.need_tools_kwargs = config.get(\u0026#34;need_tools_kwargs\u0026#34;, False) self.filter_prompts = config.get(\u0026#34;filter_prompts\u0026#34;, True) self.serialize_dataset = False self._download() self._read_files_and_tokenize() 有了 DataProto 和 RLHFDataset 后，我们来观察数据流：\n1 A：Parquet 文件 --\u0026gt; B：RLHFDataset --\u0026gt; C：DataLoader + collate_fn --\u0026gt; D：DataProto 原始数据 --\u0026gt; E：pop 提取生成数据 --\u0026gt; F：Rollout 生成 --\u0026gt; G：union 合并数据 --\u0026gt; H：奖励计算 --\u0026gt; I：优势计算 --\u0026gt; J：重新计算 log_probs --\u0026gt; K：计算参考 log_probs --\u0026gt; L：计算价值函数 --\u0026gt; M1：更新 critic --\u0026gt; M2：更新 actor --\u0026gt; N：返回训练指标 事实上，只有最初的三步不是 DataProto，其他都是通过 DataProto 进行数据交换的。具体每步的数据流向如下：\n数据流详细分析 A：Parquet 文件\n1 data_files = \u0026#34;~/data/rlhf/gsm8k/train.parquet\u0026#34; B：RLHFDataset\n1 2 3 4 5 6 dataset = RLHFDataset( data_files=data_files, tokenizer=tokenizer, config=config, processor=processor ) C：DataLoader + collate_fn\n1 2 3 4 5 6 7 dataloader = DataLoader( dataset=dataset, batch_size=16, shuffle=True, drop_last=True, collate_fn=collate_fn ) D：DataProto 原始数据\n1 2 batch_dict = next(iter(dataloader)) # 返回 dict batch: DataProto = DataProto.from_single_dict(batch_dict) E：pop 提取生成数据\n1 gen_batch = batch.pop(batch_keys=[\u0026#34;input_ids\u0026#34;, \u0026#34;attention_mask\u0026#34;, \u0026#34;position_ids\u0026#34;]) F：Rollout 生成\n1 gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch) G：union 合并数据\n1 batch = batch.union(gen_batch_output) H：奖励计算\n1 2 rewards = self.reward_fn(batch) batch.batch[\u0026#34;token_level_rewards\u0026#34;] = rewards I：优势计算\n1 batch = compute_advantage(batch, adv_estimator=self.config.algorithm.adv_estimator) J：重新计算 log_probs\n1 2 old_log_prob = self.actor_rollout_wg.compute_log_prob(batch) batch = batch.union(old_log_prob) K：计算 reference model 的 log_probs\n1 2 3 if self.use_reference_policy: ref_log_prob = self.ref_policy_wg.compute_ref_log_prob(batch) batch = batch.union(ref_log_prob) L：计算 value function\n1 2 3 if self.use_critic: values = self.critic_wg.compute_values(batch) batch = batch.union(values) M1：更新 critic\n1 2 3 4 if self.use_critic: critic_output = self.critic_wg.update_critic(batch) critic_output_metrics = reduce_metrics(critic_output.meta_info[\u0026#34;metrics\u0026#34;]) metrics.update(critic_output_metrics) M2：更新 actor\n1 actor_output = self.actor_rollout_wg.update_actor(batch) N：返回训练指标\n1 2 3 actor_output_metrics = reduce_metrics(actor_output.meta_info[\u0026#34;metrics\u0026#34;]) metrics.update(actor_output_metrics) logger.log(data=metrics, step=self.global_steps) Rollout 在 part 1 已经讲过了 SGLang 的几个关键函数：\nActorRolloutRefWorker._build_rollout() SGLangRollout.__init__() SGLangRollout.AsyncEngine SGLangRollout._init_inference_engine() 此外，我们还介绍了在“我们究竟在异步什么？“里面介绍了 SGLang 对 multi-turn 场景下的 _req_level_generate_sequences 的特殊实现。我们接着继续分析 SGLang rollout 对 multi-turn 的处理，包括状态机和 tool 调用。\n_req_level_generate_sequences 接着上文的讨论，我们继续来看看源代码。\n如果当前是 tp rank 0，则将一整个 batch 的 prompts 预处理成单个异步请求，并并发执行这些请求以生成序列。rollout 的返回顺序是乱序的，因此需要按照 batch ID 和在 batch 内的 offset 来对返回值重新排序。 如果不是 tp rank 0，则将输出请求列表设置为 None。这里其实也是之前提到过的 mock SPMD 的体现。 使用分布式通信，将 tp rank 0 生成的排序后的请求列表广播给所有其他 rank。 提取 prompt IDs、response IDs、attention masks、position IDs、loss masks、原始消息和 reward scores。 使用 padding token 对 prompt IDs 和 response IDs 进行填充，使其长度一致。 将填充后的 prompt 和 response 的 IDs、attention masks 等在最后一个维度上进行拼接，形成完整的序列数据。 将处理后的 prompts 和 responses 存储到 TensorDict 对象中，并设置批次大小。 将包含批次化张量数据的 TensorDict 和包含原始消息及奖励分数的字典封装到 DataProto 对象中并返回。 这里有个比较有趣的地方，注意到 2 中我们强调了，SGLang 并不是严格的 SPMD，但是 3 中，我们仍旧将 tp 0 得到的 response broadcast 给了所有 rank。但是，为了保持 SGLang 外部的训练循环仍旧得到的是一个 SPMD 的返回结果，我们需要让每个 tp randk 都构造并返回相同的 batch，这就需要通过 broadcast 让其他 tp rank 获得 tp 0 的计算结果。这导致了一定的计算冗余，但是相比推理本身的开销，仍旧是可以负担的。\n_req_level_generate_sequences 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @GPUMemoryLogger(role=\u0026#34;sglang rollout\u0026#34;, logger=logger) @torch.no_grad() def _req_level_generate_sequences(self, prompts: DataProto, **kwargs) -\u0026gt; DataProto: do_sample = prompts.meta_info.get(\u0026#34;do_sample\u0026#34;, True) is_validate = prompts.meta_info.get(\u0026#34;validate\u0026#34;, False) tgt_device = prompts.batch[\u0026#34;input_ids\u0026#34;].device if self._tp_rank == 0: req_list = self._preprocess_prompt_to_async_rollout_requests( prompts, n=1 if is_validate else self.config.n, ) loop = asyncio.get_event_loop() output_req_list = loop.run_until_complete( asyncio.gather( *[self._async_rollout_a_request(req, do_sample, is_validate, **kwargs) for req in req_list], ) ) sorted_output_req_list = sorted(output_req_list, key=lambda x: (x.batch_data_id, x.rollout_offset)) else: sorted_output_req_list = None dist.barrier() [sorted_output_req_list] = broadcast_pyobj( data=[sorted_output_req_list], rank=self._rank, dist_group=self._device_mesh_cpu[\u0026#34;tp\u0026#34;].get_group(), src=self._device_mesh_cpu[\u0026#34;tp\u0026#34;].mesh[0].item(), force_cpu_device=False, ) prompt_ids, response_ids = [], [] prompt_attention_mask, response_attention_mask = [], [] prompt_position_ids, response_position_ids = [], [] prompt_loss_mask, response_loss_mask = [], [] messages = [] reward_scores = [] for req in sorted_output_req_list: assert req.state == AsyncRolloutRequestStateEnum.COMPLETED, f\u0026#34;Request {req.request_id} is not completed\u0026#34; assert len(req.input_ids) == len(req.attention_mask) == len(req.position_ids) == len(req.loss_mask), f\u0026#34;\u0026#34;\u0026#34;Request {req.request_id} has different length of {len(req.input_ids)=}, {len(req.attention_mask)=}, {len(req.position_ids)=}, {len(req.loss_mask)=}\u0026#34;\u0026#34;\u0026#34; error_message_lines = [ f\u0026#34;\u0026#34;\u0026#34;Request {req.request_id} has input_ids length {len(req.input_ids)} greater than max_model_len {self.config.max_model_len}\u0026#34;\u0026#34;\u0026#34;, f\u0026#34;Decoded input_ids: {self.tokenizer.decode(req.input_ids)}\u0026#34;, f\u0026#34;Decoded prompt_ids: {self.tokenizer.decode(req.prompt_ids)}\u0026#34;, f\u0026#34;Decoded response_ids: {self.tokenizer.decode(req.response_ids)}\u0026#34;, f\u0026#34;Messages: {req.messages}\u0026#34;, f\u0026#34;Max model length: {req.max_model_len}\u0026#34;, ] error_message = \u0026#34;\\n\u0026#34;.join(error_message_lines) assert len(req.input_ids) \u0026lt;= self.config.max_model_len, error_message prompt_ids.append(torch.tensor(req.prompt_ids, dtype=torch.int, device=tgt_device)) response_ids.append(torch.tensor(req.response_ids, dtype=torch.int, device=tgt_device)) if len(req.response_ids) \u0026gt; self.config.response_length: logger.warning( f\u0026#34;\u0026#34;\u0026#34;{req.request_id=} has response_ids length {len(req.response_ids)} greater than max_response_len {self.config.response_length},\\n{req=}\u0026#34;\u0026#34;\u0026#34; ) prompt_attention_mask.append(torch.tensor(req.prompt_attention_mask, dtype=torch.int, device=tgt_device)) response_attention_mask.append(torch.tensor(req.response_attention_mask, dtype=torch.int, device=tgt_device)) prompt_position_ids.append(torch.tensor(req.prompt_position_ids, dtype=torch.int, device=tgt_device)) response_position_ids.append(torch.tensor(req.response_position_ids, dtype=torch.int, device=tgt_device)) prompt_loss_mask.append(torch.tensor(req.prompt_loss_mask, dtype=torch.int, device=tgt_device)) response_loss_mask.append(torch.tensor(req.response_loss_mask, dtype=torch.int, device=tgt_device)) messages.append({\u0026#34;messages\u0026#34;: req.messages}) reward_scores.append(req.reward_scores) prompt_ids = pad_sequence( prompt_ids, batch_first=True, padding_value=self.pad_token_id, padding_side=\u0026#34;left\u0026#34;, ) if prompt_ids.shape[1] \u0026lt; self.config.prompt_length: prompt_ids = pad_sequence_to_length(prompt_ids, self.config.prompt_length, self.pad_token_id, left_pad=True) response_ids = pad_sequence(response_ids, batch_first=True, padding_value=self.pad_token_id) if response_ids.shape[1] \u0026lt; self.config.response_length: response_ids = pad_sequence_to_length(response_ids, self.config.response_length, self.pad_token_id) prompt_attention_mask = pad_sequence( prompt_attention_mask, batch_first=True, padding_value=0, padding_side=\u0026#34;left\u0026#34;, ) if prompt_attention_mask.shape[1] \u0026lt; self.config.prompt_length: prompt_attention_mask = pad_sequence_to_length(prompt_attention_mask, self.config.prompt_length, 0, left_pad=True) response_attention_mask = pad_sequence(response_attention_mask, batch_first=True, padding_value=0) if response_attention_mask.shape[1] \u0026lt; self.config.response_length: response_attention_mask = pad_sequence_to_length(response_attention_mask, self.config.response_length, 0) prompt_position_ids = pad_sequence(prompt_position_ids, batch_first=True, padding_value=0, padding_side=\u0026#34;left\u0026#34;) if prompt_position_ids.shape[1] \u0026lt; self.config.prompt_length: prompt_position_ids = pad_sequence_to_length(prompt_position_ids, self.config.prompt_length, 0, left_pad=True) response_length = response_ids.size(1) delta_position_id = torch.arange(1, response_length + 1, device=response_ids.device) delta_position_id = delta_position_id.unsqueeze(0).repeat(len(sorted_output_req_list), 1) response_position_ids = prompt_position_ids[:, -1:] + delta_position_id prompt_loss_mask = pad_sequence(prompt_loss_mask, batch_first=True, padding_value=0, padding_side=\u0026#34;left\u0026#34;) if prompt_loss_mask.shape[1] \u0026lt; self.config.prompt_length: prompt_loss_mask = pad_sequence_to_length(prompt_loss_mask, self.config.prompt_length, 0, left_pad=True) response_loss_mask = pad_sequence(response_loss_mask, batch_first=True, padding_value=0) if response_loss_mask.shape[1] \u0026lt; self.config.response_length: response_loss_mask = pad_sequence_to_length(response_loss_mask, self.config.response_length, 0) input_ids = torch.cat((prompt_ids, response_ids), dim=-1) attention_mask = torch.cat((prompt_attention_mask, response_attention_mask), dim=-1) position_ids = torch.cat((prompt_position_ids, response_position_ids), dim=-1) loss_mask = torch.cat((prompt_loss_mask, response_loss_mask), dim=-1) batch = TensorDict( { \u0026#34;prompts\u0026#34;: prompt_ids, \u0026#34;responses\u0026#34;: response_ids, \u0026#34;input_ids\u0026#34;: input_ids, \u0026#34;attention_mask\u0026#34;: attention_mask, \u0026#34;position_ids\u0026#34;: position_ids, \u0026#34;loss_mask\u0026#34;: loss_mask, }, batch_size=len(sorted_output_req_list), ) if self.config.free_cache_engine and self._engine is not None and self._tp_rank == 0: loop = asyncio.get_event_loop() loop.run_until_complete(self._engine.flush_cache()) return DataProto( batch=batch, non_tensor_batch={ \u0026#34;messages\u0026#34;: np.array(messages), \u0026#34;reward_scores\u0026#34;: np.array(reward_scores), }, ) 显然，_req_level_generate_sequences 的核心在于这两个函数：\n_preprocess_prompt_to_async_rollout_requests _async_rollout_a_request 我们分别展开。\n_preprocess_prompt_to_async_rollout_requests 将 prompts 展开，首先拆开 batch 中的每个 prompt，内层循环为每个 prompt 生成 n 个不同的序列。每个生成的请求都有唯一的 batch_data_id 和 rollout_offset 标识。 当配置了工具时，_input_ids 和 _attention_mask 被设为 None，因为工具调用需要动态构建输入。而没有配置工具的话，使用 _pre_process_inputs 函数处理预处理的 token IDs，去除左填充。 每个请求对象包含状态管理、工具配置、序列长度限制、tokenizer 配置等元数据，为后续的异步处理提供完整信息。 _preprocess_prompt_to_async_rollout_requests 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 def _preprocess_prompt_to_async_rollout_requests(self, prompts: DataProto, n: int) -\u0026gt; list[AsyncRolloutRequest]: assert \u0026#34;raw_prompt\u0026#34; in prompts.non_tensor_batch, \u0026#34;need data.return_raw_chat=True, due to no official way do parse_messages\u0026#34; req_list = [] for data_idx, raw_prompt in enumerate(prompts.non_tensor_batch[\u0026#34;raw_prompt\u0026#34;]): for rollout_offset in range(n): if self._tool_schemas: _tools_kwargs = prompts.non_tensor_batch[\u0026#34;tools_kwargs\u0026#34;][data_idx] _tool_schemas = [self._tool_map[k].get_openai_tool_schema() for k in _tools_kwargs.keys()] _input_ids = None _attention_mask = None else: _input_ids = _pre_process_inputs(self.pad_token_id, prompts.batch[\u0026#34;input_ids\u0026#34;][data_idx]) _attention_mask = _pre_process_inputs(0, prompts.batch[\u0026#34;attention_mask\u0026#34;][data_idx]) _tools_kwargs = {} _tool_schemas = None req = AsyncRolloutRequest( batch_data_id=data_idx, rollout_offset=rollout_offset, request_id=str(uuid4()), state=AsyncRolloutRequestStateEnum.PENDING, messages=raw_prompt.tolist(), tool_schemas=_tool_schemas, tools_kwargs=_tools_kwargs, input_ids=_input_ids, response_ids=[], attention_mask=_attention_mask, response_attention_mask=[], response_position_ids=[], response_loss_mask=[], reward_scores={}, max_prompt_len=self.config.prompt_length, max_response_len=self.config.response_length, max_model_len=min(self.config.max_model_len, self.config.prompt_length + self.config.response_length), use_inference_chat_template=self.config.multi_turn.use_inference_chat_template, enable_tokenization_sanity_check=self.config.multi_turn.enable_tokenization_sanity_check, tokenizer=self.tokenizer, ) error_message = f\u0026#34;Request {req.request_id} has mismatched lengths: input_ids={len(req.input_ids)}, attention_mask={len(req.attention_mask)}, position_ids={len(req.position_ids)}, loss_mask={len(req.loss_mask)}\u0026#34; assert len(req.input_ids) == len(req.attention_mask) == len(req.position_ids) == len(req.loss_mask), error_message req_list.append(req) return req_list 这里其实重要的在于整个 AsyncRolloutRequest，或者说我们用于管理 tool calling 的整个状态机 schema。\nschema 状态机 stateDiagram-v2 [*] --\u0026gt; PENDING PENDING --\u0026gt; RUNNING : _handle_pending_state() RUNNING --\u0026gt; TOOL_CALLING : detect_tool_call TOOL_CALLING --\u0026gt; RUNNING : tool_call_executed TOOL_CALLING --\u0026gt; COMPLETED : tool_call_decode_failed RUNNING --\u0026gt; COMPLETED : stop_reason == STOP RUNNING --\u0026gt; [Exit] : finish_reason == LENGTH COMPLETED --\u0026gt; [Exit] note right of TOOL_CALLING if tool_calls == None: raise ValueError end note note right of RUNNING if exceeds max length: finish_reason = LENGTH end note 这些状态机挺抽象的，需要到了和 SGLang rollout 的交互部分才能真的理解到用法，不过我们还是先列举出来。\nFinishReasonTypeEnum LENGTH：达到最大长度限制 STOP：正常停止（如生成 EOS token） TOOL_CALL：检测到工具调用 Message role：消息角色（user/assistant/tool） content：消息内容 tool_calls：可选的工具调用列表，每个工具调用包含 name 和 args 字段 目前的实现只支持单个工具的调用，但是魔改玩家太多了，甚至可以做一个 tool manager。\nAsyncRolloutRequestStateEnum PENDING：等待处理 RUNNING：正在运行 TOOL_CALLING：正在调用工具 COMPLETED：已完成 FAILED：失败 AsyncRolloutRequest initialize_request：验证必需字段（messages、max_prompt_len、tokenizer），使用 tokenizer 的 chat_template 处理消息，初始化所有序列相关字段（input_ids、attention_mask、position_ids、loss_mask），计算生成提示的位置信息 _update_input_ids：以增量方式更新序列信息，自动计算新的 position_ids，维护数据一致性验证 get_generation_prompt_ids：根据配置决定是否使用推理时的 chat_template，动态添加生成提示到输入序列 add_assistant_message：添加助手回复到消息历史，更新输入序列以包含新的回复内容，支持工具调用信息 add_tool_response_messages：添加工具响应到消息历史，更新输入序列但不标记为损失计算部分 finalize：完成请求处理，执行 tokenization 一致性检查，清理生成提示，截断输出序列到合理长度 truncate_output_ids：确保所有序列长度不超过限制，分别处理 input_ids、attention_mask、position_ids、loss_mask _async_rollout_a_request 文档写的很详尽了，容易 lost in the middle。不过，我们回到主线，先前通过 _preprocess_prompt_to_async_rollout_requests 构造了 AsyncRolloutRequest 后，返回给 _req_level_generate_sequences，接着进一步通过 _async_rollout_a_request 根据 AsyncRolloutRequest 的状态来 rollout 到底。\n通过一个 while 循环来处理多轮对话，循环次数上限由 self.config.multi_turn.max_turns 控制，或者 requests 返回 FinishReasonTypeEnum.STOP。 在循环内部，函数根据 _req 的当前状态 (AsyncRolloutRequestStateEnum) 执行不同的操作（这块儿逻辑确实很复杂）： PENDING 状态：如果请求处于 PENDING 状态，则调用 self._handle_pending_state(_req) 初始化，然后将状态更新为 RUNNING。 TOOL_CALLING 状态：检查最后一条消息的工具调用信息 (_req.messages[-1].tool_calls)。解析工具调用信息，并通过 asyncio.gather 并发地执行每个工具调用。工具的执行逻辑封装在 self._tool_map 中，通过工具的名称进行调用。在 tool call 返回后，通过 _req.add_tool_response_messages 将工具的响应添加到消息历史中。遍历每个工具调用及其结果，通过 _req.update_metrics 更新请求的指标信息。检查当前输入序列长度是否超过模型最大长度限制，如果超过，则设置 finish_reason_type 为 STOP 并跳出循环。最后，将请求状态更新回 RUNNING，以便进行下一轮的生成。 RUNNING 状态：SGLang engine 需要进行 rollout。检查当前 prompt 的长度加上生成一个 token 的长度是否会超过 model context length。调用 self._handle_engine_call 来实际调用 SGLang engine；得到输出后，将 finish reason 从字符串转换为 FinishReasonTypeEnum，并递增当前对话轮数 current_turns。如果完成原因是达到最大长度限制 (LENGTH)，则将生成的内容添加到消息历史中，并结束循环。如果没有到达最大长度，则判断 SGLang engine 生成的内容是否包含工具调用，通过 self._function_call_parser 来解析生成的内容。如果检测到工具调用，则将 finish_reason_type 设置为 TOOL_CALL，并将请求状态更新为 TOOL_CALLING。然后，使用 self._function_call_parser.parse_non_stream 解析出工具调用，转换为 OpenAIFunctionToolCall。如果存在有效的工具调用，则通过 _req.add_assistant_message 将工具调用信息添加到消息历史中。否则，只添加生成的内容，并将 finish_reason_type 设置为 STOP，请求状态设置为 COMPLETED，并结束循环。如果生成的内容不包含工具调用，则直接通过 _req.add_assistant_message 将生成的内容添加到消息历史中，并结束循环。 如果循环达到 self.config.multi_turn.max_turns 上限，则将 finish_reason_type 设置为 STOP。 在对话循环结束后，为每个调用的工具计算奖励。遍历 _req.tools_kwargs 中的每个工具，调用工具的 calc_reward 方法来计算奖励，以及 release 方法来释放工具占用的·资源。计算结果以字典形式存储在 tool_reward_scores 中。 调用 _req.finalize 方法，完成请求的最终处理，包括执行 tokenization 一致性检查、清理生成提示、截断输出序列到合理长度等。tool_reward_scores 和最终的 finish_reason_type 会传递给 finalize 方法。最后，函数最终返回处理完成的 AsyncRolloutRequest 对象 _req。 _async_rollout_a_request 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 async def _async_rollout_a_request( self, req: AsyncRolloutRequest, do_sample: bool = True, is_validate: bool = False, **kwargs, ) -\u0026gt; AsyncRolloutRequest: assert self._tp_rank == 0, \u0026#34;only the master process can call this function\u0026#34; _req = deepcopy(req) finish_reason_type = None output = None current_turns = 0 while current_turns \u0026lt; self.config.multi_turn.max_turns: if _req.state == AsyncRolloutRequestStateEnum.PENDING: await self._handle_pending_state(_req) _req.state = AsyncRolloutRequestStateEnum.RUNNING elif _req.state == AsyncRolloutRequestStateEnum.TOOL_CALLING: if _req.messages[-1].tool_calls is not None: parsed_tool_calls = _req.messages[-1].tool_calls tool_call_results = await asyncio.gather( *[ self._tool_map[tool_call.function.name].execute( _req.request_id, tool_call.function.arguments, **_req.tools_kwargs[tool_call.function.name].get(\u0026#34;execute_kwargs\u0026#34;, {}), ) for tool_call in parsed_tool_calls ] ) _req.add_tool_response_messages(self.tokenizer, [resp for resp, _, _ in tool_call_results]) for tool_call, (resp, reward, metrics) in zip(parsed_tool_calls, tool_call_results): _req.update_metrics(metrics, tool_call.function.name) if len(_req.input_ids) \u0026gt;= self.config.max_model_len: finish_reason_type = FinishReasonTypeEnum.STOP break _req.state = AsyncRolloutRequestStateEnum.RUNNING else: raise ValueError(f\u0026#34;Unexpected tool calling last message state: {_req.messages[-1]}\u0026#34;) elif _req.state == AsyncRolloutRequestStateEnum.RUNNING: # Only continue the conversation if the prompt length is not greater than max_model_len - 1, # since SGLang raises an error when max_new_tokens + 1 is greater to max_model_len (the extra token accounts for the EOS token). if len(_req.get_generation_prompt_ids(self.tokenizer)) + 1 \u0026gt;= self.config.max_model_len: finish_reason_type = FinishReasonTypeEnum.LENGTH break output = await self._handle_engine_call(_req, do_sample, is_validate, **kwargs) content = output[\u0026#34;text\u0026#34;] finish_reason_type = FinishReasonTypeEnum.from_str(output[\u0026#34;meta_info\u0026#34;][\u0026#34;finish_reason\u0026#34;][\u0026#34;type\u0026#34;]) current_turns += 1 if finish_reason_type == FinishReasonTypeEnum.LENGTH: _req.add_assistant_message(self.tokenizer, content) break else: if self._function_call_parser and self._function_call_parser.has_tool_call(content): finish_reason_type = FinishReasonTypeEnum.TOOL_CALL _req.state = AsyncRolloutRequestStateEnum.TOOL_CALLING try: normed_content, tool_calls = self._function_call_parser.parse_non_stream(content) except JSONDecodeError: normed_content = content tool_calls = [] except AttributeError: normed_content = content tool_calls = [] parsed_tool_calls = [] for tool_call in tool_calls: function, has_decode_error = OpenAIFunctionCallSchema.from_openai_function_parsed_schema( OpenAIFunctionParsedSchema( name=tool_call.name, arguments=tool_call.parameters, ) ) # Drop the tool call if its arguments has decode error if has_decode_error: continue parsed_tool_calls.append( OpenAIFunctionToolCall( id=str(tool_call.tool_index), function=function, ) ) if len(parsed_tool_calls) \u0026gt; 0: _req.add_assistant_message(self.tokenizer, normed_content, tool_calls=parsed_tool_calls) else: _req.add_assistant_message(self.tokenizer, content) finish_reason_type = FinishReasonTypeEnum.STOP _req.state = AsyncRolloutRequestStateEnum.COMPLETED break else: _req.add_assistant_message(self.tokenizer, content) break if current_turns \u0026gt;= self.config.multi_turn.max_turns: finish_reason_type = FinishReasonTypeEnum.STOP # Calculate the reward for each tool async def calc_reward_and_release_fn(name: str, tool: BaseTool): reward = await tool.calc_reward(_req.request_id, **_req.tools_kwargs[name].get(\u0026#34;calc_reward_kwargs\u0026#34;, {})) await tool.release(_req.request_id, **_req.tools_kwargs[name].get(\u0026#34;release_kwargs\u0026#34;, {})) return name, reward tool_reward_tasks = [] for name in _req.tools_kwargs.keys(): tool = self._tool_map[name] tool_reward_tasks.append(calc_reward_and_release_fn(name, tool)) tool_reward_scores = await asyncio.gather(*tool_reward_tasks) tool_reward_scores = dict(tool_reward_scores) _req.finalize(self.tokenizer, tool_reward_scores, finish_reason_type) return _req pop and union 经过艰难深挖，我们终于完成了 Rollout 的理解，现在回到 RayPPOTrainer.fit() 上。我们来看看 rollout 部分的实现逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 with marked_timer(\u0026#34;step\u0026#34;, timing_raw): # generate a batch with marked_timer(\u0026#34;gen\u0026#34;, timing_raw, color=\u0026#34;red\u0026#34;): if not self.async_rollout_mode: gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch) else: self.async_rollout_manager.wake_up() gen_batch_output = self.async_rollout_manager.generate_sequences(gen_batch) self.async_rollout_manager.sleep() timing_raw.update(gen_batch_output.meta_info[\u0026#34;timing\u0026#34;]) gen_batch_output.meta_info.pop(\u0026#34;timing\u0026#34;, None) if self.config.algorithm.adv_estimator == AdvantageEstimator.REMAX: with marked_timer(\u0026#34;gen_max\u0026#34;, timing_raw, color=\u0026#34;purple\u0026#34;): gen_baseline_batch = deepcopy(gen_batch) gen_baseline_batch.meta_info[\u0026#34;do_sample\u0026#34;] = False gen_baseline_output = self.actor_rollout_wg.generate_sequences(gen_baseline_batch) batch = batch.union(gen_baseline_output) reward_baseline_tensor = self.reward_fn(batch) reward_baseline_tensor = reward_baseline_tensor.sum(dim=-1) batch.pop(batch_keys=list(gen_baseline_output.batch.keys())) batch.batch[\u0026#34;reward_baselines\u0026#34;] = reward_baseline_tensor del gen_baseline_batch, gen_baseline_output batch.non_tensor_batch[\u0026#34;uid\u0026#34;] = np.array([str(uuid.uuid4()) for _ in range(len(batch.batch))], dtype=object) # repeat to align with repeated responses in rollout batch = batch.repeat(repeat_times=self.config.actor_rollout_ref.rollout.n, interleave=True) batch = batch.union(gen_batch_output) 值得一提的是，我自己写了代码才理解到在 verl 当中，发给 rollout engine 的并不是整个完整的从 dataset 读取的 batch，而是通过 pop 构造的 gen_batch。pop 是一个就地操作，完成后 batch 里面的 key 当然就没了。为此，如果想让 pop 前后都有一些需要的 key，得留一手考虑。比如说，我希望通过 uid 来把 gen_batch 和 batch 重新 union 起来，得反复添加 uid。\nMake Experience 经过了漫长的战线，我们终于分析完了 rollout 部分的逻辑。我们接着分析 make experience 部分的逻辑。\nMake Experience 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 with marked_timer(\u0026#34;reward\u0026#34;, timing_raw, color=\u0026#34;yellow\u0026#34;): # compute reward model score if self.use_rm: reward_tensor = self.rm_wg.compute_rm_score(batch) batch = batch.union(reward_tensor) if self.config.reward_model.launch_reward_fn_async: future_reward = compute_reward_async.remote(batch, self.config, self.tokenizer) else: reward_tensor, reward_extra_infos_dict = compute_reward(batch, self.reward_fn) # recompute old_log_probs with marked_timer(\u0026#34;old_log_prob\u0026#34;, timing_raw, color=\u0026#34;blue\u0026#34;): old_log_prob = self.actor_rollout_wg.compute_log_prob(batch) entropys = old_log_prob.batch[\u0026#34;entropys\u0026#34;] response_masks = batch.batch[\u0026#34;response_mask\u0026#34;] loss_agg_mode = self.config.actor_rollout_ref.actor.loss_agg_mode entropy_agg = agg_loss(loss_mat=entropys, loss_mask=response_masks, loss_agg_mode=loss_agg_mode) old_log_prob_metrics = {\u0026#34;actor/entropy\u0026#34;: entropy_agg.detach().item()} metrics.update(old_log_prob_metrics) old_log_prob.batch.pop(\u0026#34;entropys\u0026#34;) batch = batch.union(old_log_prob) if \u0026#34;rollout_log_probs\u0026#34; in batch.batch.keys(): # TODO: we may want to add diff of probs too. rollout_old_log_probs = batch.batch[\u0026#34;rollout_log_probs\u0026#34;] actor_old_log_probs = batch.batch[\u0026#34;old_log_probs\u0026#34;] attention_mask = batch.batch[\u0026#34;attention_mask\u0026#34;] responses = batch.batch[\u0026#34;responses\u0026#34;] response_length = responses.size(1) response_mask = attention_mask[:, -response_length:] rollout_probs = torch.exp(rollout_old_log_probs) actor_probs = torch.exp(actor_old_log_probs) rollout_probs_diff = torch.abs(rollout_probs - actor_probs) rollout_probs_diff = torch.masked_select(rollout_probs_diff, response_mask.bool()) rollout_probs_diff_max = torch.max(rollout_probs_diff) rollout_probs_diff_mean = torch.mean(rollout_probs_diff) rollout_probs_diff_std = torch.std(rollout_probs_diff) metrics.update( { \u0026#34;training/rollout_probs_diff_max\u0026#34;: rollout_probs_diff_max.detach().item(), \u0026#34;training/rollout_probs_diff_mean\u0026#34;: rollout_probs_diff_mean.detach().item(), \u0026#34;training/rollout_probs_diff_std\u0026#34;: rollout_probs_diff_std.detach().item(), } ) if self.use_reference_policy: # compute reference log_prob with marked_timer(\u0026#34;ref\u0026#34;, timing_raw, color=\u0026#34;olive\u0026#34;): if not self.ref_in_actor: ref_log_prob = self.ref_policy_wg.compute_ref_log_prob(batch) else: ref_log_prob = self.actor_rollout_wg.compute_ref_log_prob(batch) batch = batch.union(ref_log_prob) # compute values if self.use_critic: with marked_timer(\u0026#34;values\u0026#34;, timing_raw, color=\u0026#34;cyan\u0026#34;): values = self.critic_wg.compute_values(batch) batch = batch.union(values) with marked_timer(\u0026#34;adv\u0026#34;, timing_raw, color=\u0026#34;brown\u0026#34;): # we combine with rule-based rm reward_extra_infos_dict: dict[str, list] if self.config.reward_model.launch_reward_fn_async: reward_tensor, reward_extra_infos_dict = ray.get(future_reward) batch.batch[\u0026#34;token_level_scores\u0026#34;] = reward_tensor if reward_extra_infos_dict: batch.non_tensor_batch.update({k: np.array(v) for k, v in reward_extra_infos_dict.items()}) # compute rewards. apply_kl_penalty if available if self.config.algorithm.use_kl_in_reward: batch, kl_metrics = apply_kl_penalty(batch, kl_ctrl=self.kl_ctrl_in_reward, kl_penalty=self.config.algorithm.kl_penalty) metrics.update(kl_metrics) else: batch.batch[\u0026#34;token_level_rewards\u0026#34;] = batch.batch[\u0026#34;token_level_scores\u0026#34;] # compute advantages, executed on the driver process norm_adv_by_std_in_grpo = self.config.algorithm.get(\u0026#34;norm_adv_by_std_in_grpo\u0026#34;, True) # GRPO adv normalization factor batch = compute_advantage( batch, adv_estimator=self.config.algorithm.adv_estimator, gamma=self.config.algorithm.gamma, lam=self.config.algorithm.lam, num_repeat=self.config.actor_rollout_ref.rollout.n, norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo, multi_turn=self.config.actor_rollout_ref.rollout.multi_turn.enable, config=self.config.algorithm, ) 这一部分的操作还是很好读懂了，非常 standard：\n通过 self.reward_fn 或 self.rm_wg.compute_rm_score 计算 trajectory 的 reward。verl 支持各式各样的 reward，不单单是 reward model。 重算 behaviour policy 的 log probabilities: 使用 self.actor_rollout_wg.compute_log_prob(batch) 来重算 log probs。这里原因在 part 1 讲述 importance sampling 的部分也阐述过了。这里非常让我想吐槽的是，verl 里面 old_log_prob 就是用 training engine 重算的 behaviour policy 的 log probs，用 old 来描述让我比较费解。 计算 reference policy 的 log probabilities: 如果使用了 reference policy，则计算 reference policy 的 log probs，用于 KL divergence 约束。 计算 Critic 的 value: 如果使用了 Critic model，则通过 self.critic_wg.compute_values(batch) 预测当前 state 的 value。 估算 Advantage: 调用 compute_advantage 函数，根据配置的advantage estimator、折扣因子 (gamma)、GALA 因子 (lam) 等参数，利用 reward 和 value 估计计算优势函数。 Training 非常标准：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # update critic if self.use_critic: with marked_timer(\u0026#34;update_critic\u0026#34;, timing_raw, color=\u0026#34;pink\u0026#34;): critic_output = self.critic_wg.update_critic(batch) critic_output_metrics = reduce_metrics(critic_output.meta_info[\u0026#34;metrics\u0026#34;]) metrics.update(critic_output_metrics) # implement critic warmup if self.config.trainer.critic_warmup \u0026lt;= self.global_steps: # update actor with marked_timer(\u0026#34;update_actor\u0026#34;, timing_raw, color=\u0026#34;red\u0026#34;): batch.meta_info[\u0026#34;multi_turn\u0026#34;] = self.config.actor_rollout_ref.rollout.multi_turn.enable actor_output = self.actor_rollout_wg.update_actor(batch) actor_output_metrics = reduce_metrics(actor_output.meta_info[\u0026#34;metrics\u0026#34;]) metrics.update(actor_output_metrics) ","permalink":"https://pillumina.github.io/posts/aiinfra/08-verl-multiturn-2/","summary":"\u003cblockquote\u003e\n\u003cp\u003e在 Part 1 中，我们介绍了 verl 的初始化过程，我们进一步介绍 verl 的训练过程，包括rollout部分、make experience部分以及training部分。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e在 GRPO 中，单个 step 包含四个阶段：load data -\u0026gt; rollout -\u0026gt; make experience -\u0026gt; update model。区别于前一节的详述，本节会使用伪代码结合源码的方式进行阐述。\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003e\n  flowchart LR\nsubgraph W2[\u0026#34;Initialize\u0026#34;]\nWP[Process Data] --\u0026gt; A\ndirection TB D1[Data Prepare] --\u0026gt; A\nA[TaskRunner] --\u0026gt; B1[RayPPOTrainer]\nB1 --\u0026gt; Workers\n\n    subgraph Workers[\u0026#34;Workers\u0026#34;]\n        direction TB\n                WA[ActorRolloutWorker] --\u0026gt; WD[FSDP Engine]\n        WB[CriticWorker] --\u0026gt; WD\n        WC[RewardModelWorker] --\u0026gt; WD\n        WD --\u0026gt; WE[SGLang Engine]\n    end\n    \n    Workers --\u0026gt; C1[Hybrid Engine]\nend \n\nsubgraph W3[\u0026#34;Train Loop\u0026#34;]\n    direction TB\n    E[DataLoader] --\u0026gt; RolloutBox\n    \n    subgraph RolloutBox[\u0026#34;Rollout\u0026#34;]\n        F1[Prepare Data] --\u0026gt; F2[SGLang Async Rollout]\n        F2 --\u0026gt; F3[Multi-turn Chat Process]\n    end\n    \n    RolloutBox --\u0026gt; ExpBox\n    \n    subgraph ExpBox[\u0026#34;Make Experience\u0026#34;]\n        G1[Recompute Log Probs] --\u0026gt; G2[Compute Reward]\n        G2 --\u0026gt; G3[Compute Advantage]\n    end\n    \n    ExpBox --\u0026gt; UpdateBox\n    \n    subgraph UpdateBox[\u0026#34;Train The Model\u0026#34;]\n        H1[Load FSDP Model Weight] --\u0026gt; H2[Compute Gradient]\n        H2 --\u0026gt; H3[Weights Update]\n        H3 --\u0026gt; H4[Sync Weights]\n    end\n    \n    UpdateBox --\u0026gt; E\nend\n\nW2 --\u0026gt; W3\n\u003c/pre\u003e\n\n\u003ch2 id=\"数据加载与预处理\"\u003e数据加载与预处理\u003c/h2\u003e\n\u003cp\u003everl 通过 \u003ccode\u003eDataProto\u003c/code\u003e 和 \u003ccode\u003eRLHFDataset\u003c/code\u003e 来实现数据处理。具体来说，在 \u003ca href=\"https://github.com/volcengine/verl/blob/76f63cffa5081564d8fea93a1cb3ce8bd5bdcc39/verl/trainer/main_ppo.py#L193\"\u003e\u003ccode\u003emain_ppo.py\u003c/code\u003e\u003c/a\u003e 中，我们观察这个函数：\u003c/p\u003e","title":"[VeRL] Multi-Turn RL训练源码走读（2）"},{"content":" 该part主要聚焦相关模块初始化部分\n还是以 verl 出发，分析其 end to end mutli-turn RL 训练的全过程。整体上，我希望覆盖所有重要的 class 以及函数，更细粒度的代码不再展开。\n为了前后内容的一致性，基于 76f63cffa5 的 commit 进行分析。\n虽然本文以分析 verl 的代码为主，写完之后我才意识到，系统设计问题是非常通用的。诸如“log probs 重计算”，“Rollout Engine 显存管理”等等系统设计，是各大 RL 框架都需要考虑的核心问题。\n此外因为最近在学习SGLang的实现，本文的推理后端选择的是SGLang展开分析。\n整个训练的示意图如下，我们会具体展开每个部分。\nflowchart LR subgraph W2[\u0026#34;Initialize\u0026#34;] WP[Process Data] --\u0026gt; A direction TB D1[Data Prepare] --\u0026gt; A A[TaskRunner] --\u0026gt; B1[RayPPOTrainer] B1 --\u0026gt; Workers subgraph Workers[\u0026#34;Workers\u0026#34;] direction TB WA[ActorRolloutWorker] --\u0026gt; WD[FSDP Engine] WB[CriticWorker] --\u0026gt; WD WC[RewardModelWorker] --\u0026gt; WD WD --\u0026gt; WE[SGLang Engine] end Workers --\u0026gt; C1[Hybrid Engine] end subgraph W3[\u0026#34;Train Loop\u0026#34;] direction TB E[DataLoader] --\u0026gt; RolloutBox subgraph RolloutBox[\u0026#34;Rollout\u0026#34;] F1[Prepare Data] --\u0026gt; F2[SGLang Async Rollout] F2 --\u0026gt; F3[Multi-turn Chat Process] end RolloutBox --\u0026gt; ExpBox subgraph ExpBox[\u0026#34;Make Experience\u0026#34;] G1[Recompute Log Probs] --\u0026gt; G2[Compute Reward] G2 --\u0026gt; G3[Compute Advantage] end ExpBox --\u0026gt; UpdateBox subgraph UpdateBox[\u0026#34;Train The Model\u0026#34;] H1[Load FSDP Model Weight] --\u0026gt; H2[Compute Gradient] H2 --\u0026gt; H3[Weights Update] H3 --\u0026gt; H4[Sync Weights] end UpdateBox --\u0026gt; E end W2 --\u0026gt; W3 数据预处理 以 GSM8K 为例，预处理脚本是 examples/data_preprocess/gsm8k_multiturn_w_tool.py。整个脚本只做了经典的 huggingface datasets mapping，核心逻辑如下：\n加载 openai/gsm8k 原始数据集（train/test）。 对每条原始数据，生成带有工具调用要求的 prompt（比如在 user turn 强调模型可以调用 calc_gsm8k_reward 工具，每个qa至少调用一次）。 同样对于每条原始数据，解析答案；将 ground truth 写入 extra_info 字段。 存储为 parquet 文件，分别保留为 train.parquet 和 test.parquet，默认路径为 ~/data/gsm8k/。 启动训练 一个典型的启动命令如下：\n1 2 3 4 5 6 7 8 9 10 # now 用于生成实验启动的时间尾缀，避免重复启动实验时覆盖已有 wandb log function now() { date \u0026#39;+%Y-%m-%d-%H-%M\u0026#39; } export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 nohup bash examples/sglang_multiturn/run_qwen2.5-3b_gsm8k_multiturn.sh \\ trainer.experiment_name=qwen2.5-3b_rm-gsm8k-sgl-multiturn-$now \\ \u0026gt; logs/gsm8k-$now.log 2\u0026gt;\u0026amp;1 \u0026amp; 脚本配置 verl 的各项参数实属复杂，我们会单独编写文档来分享对 verl 各类参数的理解。在这篇文档中，我们想要格外强调的是 verl 各类 config 的覆盖关系。verl 的配置文件利用 hydra 进行了分层覆盖的设计模式。\nHydra 简介 Hydra 是一个由 Facebook Research 开发的 Python 框架，旨在优雅地配置复杂的应用程序。它特别适用于需要管理大量参数和进行多组实验的场景，例如机器学习项目。Hydra 的核心特点在于其动态、分层和可组合的配置管理能力。Hydra 的核心优势：\n分层配置 (Hierarchical Configuration)：可以将配置分解成多个小型、模块化的 YAML 文件，并以目录结构进行组织。这使得配置更加清晰、易于管理和复用。 配置组合 (Configuration Composition)：Hydra 能够将这些独立的配置模块动态地组合起来，形成一个完整的配置对象。你可以通过在主配置文件中指定 defaults 列表来选择和组合不同的配置组件。 命令行覆盖 (Command-line Overrides)：这是 Hydra 最强大的功能之一。你可以在运行应用程序时，直接通过命令行参数来覆盖配置中的任何值。这使得进行实验和快速迭代变得非常方便，无需修改配置文件本身。 多运行模式 (Multi-run)：Hydra 允许你通过一个命令运行多个具有不同配置的实验。这对于超参数搜索和模型比较非常有用。 动态工作目录 (Dynamic Working Directory)：每次运行应用程序时，Hydra 都会自动创建一个独立的工作目录，并将当前运行的配置和输出保存到该目录中，确保实验的可复现性。 对象实例化 (Object Instantiation)：Hydra 可以直接从配置中实例化 Python 对象（类或函数），这大大简化了代码，使配置更具声明性。 Hydra 实现分层覆盖的主要机制是组合 (Composition) 和 命令行覆盖 (Command-line Overrides)。\n分层配置的组织： 通常会创建一个 conf 目录，并在其中组织配置。例如：\n1 2 3 4 5 6 7 8 9 10 . ├── my_app.py └── conf ├── config.yaml ├── model │ ├── cnn.yaml │ └── rnn.yaml └── dataset ├── cifar10.yaml └── imagenet.yaml config.yaml 是你的主配置文件。在 model 目录下，你可以定义不同的模型配置（如 cnn.yaml、rnn.yaml），在 dataset 目录下定义不同的数据集配置（如 cifar10.yaml、imagenet.yaml）。\ndefaults 列表进行组合： 在 config.yaml 中，你可以使用特殊的 defaults 列表来指定默认加载哪些配置组件。\nconf/config.yaml 示例：\n1 2 3 4 5 6 7 8 defaults: - model: cnn # 默认加载 conf/model/cnn.yaml - dataset: cifar10 # 默认加载 conf/dataset/cifar10.yaml - _self_ # 确保当前文件中的其他配置项也被加载 # 其他应用级别的默认配置 learning_rate: 0.001 epochs: 10 当 Hydra 加载 config.yaml 时，它会根据 defaults 列表中的指示，自动将 conf/model/cnn.yaml 和 conf/dataset/cifar10.yaml 的内容合并到最终的配置对象中。\n命令行覆盖： 这是实现灵活覆盖的关键。你可以通过命令行参数来覆盖任何已加载的配置值，包括在 defaults 列表中指定的组件或其内部的任何参数。\n覆盖整个配置组：\n要切换模型从 cnn 到 rnn，你可以在命令行中这样运行： 1 python my_app.py model=rnn 这将指示 Hydra 加载 conf/model/rnn.yaml，并用它来替换默认的 cnn 配置。\n覆盖特定参数：\n你可以深入到配置的任何层级来覆盖特定的参数。例如，如果你想修改学习率或数据集的某个参数： 1 python my_app.py learning_rate=0.01 dataset.batch_size=64 这里，learning_rate 直接覆盖了 config.yaml 中的值，而 dataset.batch_size 则覆盖了 conf/dataset/cifar10.yaml（或者你通过 dataset=imagenet 指定的其他数据集配置文件）中的 batch_size 参数。\n添加新参数 (使用 +)：\n如果你想添加一个在默认配置中不存在的新参数，可以使用 + 前缀： 1 python my_app.py +optimizer.name=AdamW 动态覆盖 (使用 ++)：\n如果你希望修改一个已有字段，或者在原配置中没有该字段时自动创建它，可以使用 ++。这种方式适用于需要动态添加或覆盖配置项的场景，确保字段总是被设置为你指定的值，无论它是否已存在。 1 python my_app.py ++model.num_layers=10 Hydra 内部使用 OmegaConf 库来处理这些配置对象，它提供了强大的合并和解析功能，使得分层覆盖和值插值（例如，引用其他配置值或环境变量）变得非常容易。\n回到 verl multi turn，在我们启动的 run_qwen2.5-3b_gsm8k_multiturn.sh 中，设置了：\n1 2 3 4 5 6 PROJECT_DIR=\u0026#34;$(pwd)\u0026#34; CONFIG_PATH=\u0026#34;$PROJECT_DIR/examples/sglang_multiturn/config\u0026#34; python3 -m verl.trainer.main_ppo \\ --config-path=\u0026#34;$CONFIG_PATH\u0026#34; \\ --config-name=\u0026#39;gsm8k_multiturn_grpo\u0026#39; \\ 这意味着这次任务的默认 config 是 CONFIG_PATH 下的 gsm8k_multiturn_grpo.yaml，且接下来的参数会覆盖 gsm8k_multiturn_grpo.yaml 中的默认值。更进一步，我们来观察 gsm8k_multiturn_grpo.yaml 的内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 hydra: searchpath: - file://verl/trainer/config defaults: - ppo_trainer - _self_ data: max_prompt_length: 1024 max_response_length: 1024 train_batch_size: 256 return_raw_chat: True actor_rollout_ref: hybrid_engine: True rollout: name: sglang multi_turn: enable: True max_turns: 5 # tool_config_path: \u0026#34;./config/tool_config/gsm8k_tool_config.yaml\u0026#34; 这里 hydra 语法，会去 verl/trainer/config 目录下寻找 ppo_trainer.yaml 作为基础配置，并且覆盖。因此，启动 run_qwen2.5-3b_gsm8k_multiturn.sh 时，先加载 gsm8k_multiturn_grpo.yaml 作为基础配置并覆盖，然后加载 ppo_trainer.yaml 并覆盖。最终合并这三级配置，得到最终的 config。\n最后，注意到在 run_qwen2.5-3b_gsm8k_multiturn.sh 的最后，我们，我们设置了 actor_rollout_ref.rollout.multi_turn.tool_config_path=\u0026quot;$PROJECT_DIR/examples/sglang_multiturn/config/tool_config/gsm8k_tool_config.yaml\u0026quot;，这里指定 multi_turn 的 tool_config_path 为 examples/sglang_multiturn/config/tool_config/gsm8k_tool_config.yaml。这一文件仅仅配置了 gsm8k 的 tool 调用，并不会覆盖之前训练的 config。\n训练主入口与初始化 Ray Actor，Ray Task 和 Ray Worker 在介绍 verl 的训练主入口之前，我们先介绍 Ray 的一些核心概念。Ray 是一个统一计算框架，旨在实现简单地从单机到大型分布式集群的扩展，提供构建和运行分布式应用的底层基础设施和一组核心原语。Ray 通过以下功能实现这一目标：\n统一 API：Ray 提供了一套简单易用的 Python API，将普通函数转换为分布式任务，将 Python 类转换为分布式服务，也即 Ray Actor。Ray Actor 内部持久存储的数据称为状态，可以在 Actor 的整个生命周期内被多次访问、修改和维护，而不会在每次方法调用结束后消失。 弹性伸缩：Ray 可以将应用从单个机器无缝扩展到拥有数千个节点的集群，并能根据需求自动扩缩容。 容错性：Ray 内置了容错机制，可以处理节点故障和任务失败，确保应用的健壮性。 性能优化：Ray 优化了分布式任务调度、内存管理和数据传输，以实现高效的并行计算。 Ray Task 和 Ray Actor 都是用于分布式计算的核心原语，但它们各自服务于不同的目的，主要区别在于是否维护状态。\nRay Task 是 Ray 中最基本的计算单元，代表一个无状态的远程函数。Ray Task 的每次执行都是独立的，不保留之前的任何信息。就像调用一个普通函数，执行完后就清除内部状态。我们调用一个 Ray Task 后，会立即返回得到一个 Ray ObjectRef，而不是实际的结果。主程序可以继续执行其他操作，而 Ray Task 则在后台并行运行。我们需要使用 ray.get() 来获取 Task 的实际结果。 Ray Task 非常适合并行执行大量独立、一次性的计算任务，譬如数据批处理、独立的模型推理等场景。\nRay Actor 是一种特殊的 Ray Task，正如前文所述，它是一个持续运行的、有自己的状态和方法的远程对象。当我们创建一个 Ray Actor 后，Ray 会在集群中的某个 Ray Worker 上启动一个专门的进程来托管这个对象。该进程会一直运行，直到被销毁。Actor 可以维护内部变量，并且这些变量在 Actor 的生命周期内是持久存在的。每次调用 Actor 的方法，都可以访问和修改这些状态。这与普通的 Ray Task 不同，普通 Task 执行完会清除内部状态。Ray Actor 支持并发请求，Ray 会负责将这些请求序列化执行，保证 Actor 内部状态的一致性和线程安全。我们可以通过 @ray.remote 装饰器将一个 Python 类转换为一个 Ray Actor 类，然后通过 .remote() 方法实例化一个远程 Actor。\n最后，Ray Worker 是 Ray 集群中真正执行代码的工作单元。一个 Ray 集群通常由一个 Head Node 和多个 Worker Nodes 组成。每个节点上都会运行一个或多个 Ray Worker 进程。无论是普通的 Ray Task 还是 Ray Actor 的方法，最终都是由 Ray Worker 进程来执行的。每个 Ray Worker 都会被分配一定的计算资源（如 CPU、GPU）。当你提交一个 Ray Task 或创建一个 Ray Actor 时，Ray 的调度器会找到一个有足够资源的 Worker 来运行它。Worker 进程之间以及 Worker 进程与头节点之间会进行通信，以协调任务执行、传输数据和管理状态。一个 Ray Worker 通常就是一个独立的 Python 进程。对于普通的 Ray Task，Ray Worker 相当于函数解释器，执行完任务后可能会被复用去执行其他任务。而对于 Ray Actor，Ray 会启动一个专门的 Worker 进程来托管这个 Actor，这个 Worker 进程的生命周期与 Actor 的生命周期绑定。\nrun_ppo() 和 TaskRunner.run() 有了 ray 的概念，我们回到整个 RL 训练流程的起点：verl.trainer.main_ppo.py 中的 run_ppo()，它负责初始化 Ray 集群，配置 CPU 资源和运行时环境变量，并创建远程 TaskRunner 实例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 def run_ppo(config) -\u0026gt; None: # 初始化 Ray 集群，配置 CPU 资源和运行时环境变量 ray.init( runtime_env={\u0026#34;env_vars\u0026#34;: {...}}, num_cpus=config.ray_init.num_cpus, ) # 创建远程 TaskRunner 实例 # TaskRunner 是 Ray 中的一个远程 actor，它将在 Ray 集群上异步执行主要的训练任务 runner = TaskRunner.remote() # 异步执行远程任务 runner.run()，并等待其完成 # 通过 ray.get() 阻塞直到远程任务执行完毕，确保整个初始化流程的顺序性 ray.get(runner.run.remote(config)) ActorRolloutRefWorker 和 RayWorkerGroup 的相互关系 TaskRunner 是 verl 中实现 PPO/GRPO 训练的核心组件，它通过将整个 RL 训练流程封装在一个独立的 Ray Actor 中，实现了任务的封装、资源隔离和分布式协调。为了解释清楚 TaskRunner，我们将 verl 当中最让人费解且最复杂的 ActorRolloutRefWorker 和 RayWorkerGroup 这两个类提前解释清楚。\n我们先不讨论这两个类及其基类的具体意义，先讨论清楚其实例对象的创建过程。我们注意到这段 TaskRunner 的初始化中引入 ActorRolloutRefWorker 和 RayWorkerGroup 的相关代码：\nTaskRunner 中引入 ActorRolloutRefWorker 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # Define worker classes based on the actor strategy. if config.actor_rollout_ref.actor.strategy in [\u0026#34;fsdp\u0026#34;, \u0026#34;fsdp2\u0026#34;]: assert config.critic.strategy in [\u0026#34;fsdp\u0026#34;, \u0026#34;fsdp2\u0026#34;] from verl.single_controller.ray import RayWorkerGroup from verl.workers.fsdp_workers import ActorRolloutRefWorker, AsyncActorRolloutRefWorker, CriticWorker actor_rollout_cls = AsyncActorRolloutRefWorker if config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34; else ActorRolloutRefWorker ray_worker_group_cls = RayWorkerGroup elif config.actor_rollout_ref.actor.strategy == \u0026#34;megatron\u0026#34;: assert config.actor_rollout_ref.actor.strategy == config.critic.strategy from verl.single_controller.ray.megatron import NVMegatronRayWorkerGroup from verl.workers.megatron_workers import ActorRolloutRefWorker, AsyncActorRolloutRefWorker, CriticWorker actor_rollout_cls = AsyncActorRolloutRefWorker if config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34; else ActorRolloutRefWorker ray_worker_group_cls = NVMegatronRayWorkerGroup else: raise NotImplementedError from verl.trainer.ppo.ray_trainer import ResourcePoolManager, Role # Map roles to their corresponding remote worker classes. role_worker_mapping = { Role.ActorRollout: ray.remote(actor_rollout_cls), Role.Critic: ray.remote(CriticWorker), } # Define the resource pool specification. # Map roles to the resource pool. global_pool_id = \u0026#34;global_pool\u0026#34; resource_pool_spec = { global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes, } mapping = { Role.ActorRollout: global_pool_id, Role.Critic: global_pool_id, } 可以观察到，在 TaskRunner 的初始化中，会根据各类配置引入对应的 ActorRolloutRefWorker / AsyncActorRolloutRefWorker 类以及 RayWorkerGroup / NVMegatronRayWorkerGroup 类。对于 SGLang 而言，不存在 AsyncActorRolloutRefWorker。ActorRolloutRefWorker 类直接通过 ray.remote(ActorRolloutRefWorker) 创建一个远程的 Ray Actor，将其包装成一个 Ray Actor 类。此时还还没有创建任何实例，也没有分配资源。那么，ActorRolloutRefWorker 类到底在哪儿实例化并分配资源的呢？\n实际上，在 main_ppo.py 的 172 行，构造了 RayPPOTrainer 类，随后调用了 RayPPOTrainer.init_workers() 方法，我们进一步查看 RayPPOTrainer.init_workers() 方法的相关代码，我们观察到，每一个 RL worker 类（比如 ActorRolloutRefWorker）都会创造一个 work group（verl 中的各种 wg 变量），随后调用每个 worker group 的 init_model() 方法，而这些 worker group 实际上都是 RayWorkerGroup 的实例。RayWorkerGroup 的核心作用是资源调度的核心中间层，统一了各种 RL worker（比如 ActorRolloutRefWorker、CriticWorker）的接口，进行统一管理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # RayWorkerGroup 实例，指定资源池 并规定角色和对应的类 wg_dict = self.ray_worker_group_cls( resource_pool=resource_pool, # 只需要指定资源池 ray_cls_with_init=worker_dict_cls, # 一个包含数个worker的类 （e.g. actor_roll， critic, ref） device_name=self.device_name, ) #通过.spawn()获取角色对Ray Actor实例的映射 wg_dict.spawn(prefix_set=class_dict.keys()) # 所有 worker 都通过相同的模式创建，我这里进行简化，实际上的代码比较繁琐 actor_rollout_wg = RayWorkerGroup(resource_pool, actor_rollout_cls) critic_wg = RayWorkerGroup(resource_pool, critic_cls) ref_policy_wg = RayWorkerGroup(resource_pool, ref_policy_cls) 各种 worker group 实际上的初始化 这部分代码在 ray_trainer.py 中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 # 1. 为每个角色（例如 actor_rollout、critic、ref）指定用哪个类初始化 worker，并且说明在哪个资源池里分配它们 self.resource_pool_manager.create_resource_pool() self.resource_pool_to_cls = {pool: {} for pool in self.resource_pool_manager.resource_pool_dict.values()} resource_pool = self.resource_pool_manager.get_resource_pool(Role.ActorRollout) actor_rollout_cls = RayClassWithInitArgs( cls=self.role_worker_mapping[Role.ActorRollout], config=self.config.actor_rollout_ref, role=\u0026#34;actor_rollout\u0026#34;, ) self.resource_pool_to_cls[resource_pool][\u0026#34;actor_rollout\u0026#34;] = actor_rollout_cls # 2. 根据资源池和角色，批量创建多个 worker 实例（Ray Actor）并统一管理它们，赋予对应的职责 for resource_pool, class_dict in self.resource_pool_to_cls.items(): worker_dict_cls = create_colocated_worker_cls(class_dict=class_dict) wg_dict = self.ray_worker_group_cls(resource_pool=resource_pool, ray_cls_with_init=worker_dict_cls, device_name=self.device_name, **wg_kwargs) spawn_wg = wg_dict.spawn(prefix_set=class_dict.keys()) all_wg.update(spawn_wg) # 3.调用 init_model() 完成模型加载 if self.use_critic: self.critic_wg = all_wg[\u0026#34;critic\u0026#34;] self.critic_wg.init_model() if self.use_reference_policy and not self.ref_in_actor: self.ref_policy_wg = all_wg[\u0026#34;ref\u0026#34;] self.ref_policy_wg.init_model() if self.use_rm: self.rm_wg = all_wg[\u0026#34;rm\u0026#34;] self.rm_wg.init_model() # we should create rollout at the end so that vllm can have a better estimation of kv cache memory self.actor_rollout_wg = all_wg[\u0026#34;actor_rollout\u0026#34;] self.actor_rollout_wg.init_model() # create async rollout manager and request scheduler self.async_rollout_mode = False if self.config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34;: from verl.workers.rollout.async_server import AsyncLLMServerManager self.async_rollout_mode = True self.async_rollout_manager = AsyncLLMServerManager( config=self.config, worker_group=self.actor_rollout_wg, ) 注意到 ray_worker_group_cls 就是 RayWorkerGroup 类，而 worker_dict_cls 就是 ActorRolloutRefWorker 类，所以我的简化是很合理的。\n如此以来，ActorRolloutRefWorker 委托给 RayWorkerGroup 进行初始化。RayWorkerGroup 这个类就是专门用于资源调度的。通过其统一的 _init_with_resource_pool 方法，为每个 GPU 创建一个 worker，最终实例化每种 RL worker 并分配资源。\n1 2 3 4 5 6 7 8 def _init_with_resource_pool(self, resource_pool, ray_cls_with_init, ...): # 从 Ray 申请 Placement Groups pgs = resource_pool.get_placement_groups(strategy=strategy, device_name=self.device_name) # 为每个 GPU 创建一个 worker for local_rank in range(local_world_size): worker = ray_cls_with_init(placement_group=pg, placement_group_bundle_idx=local_rank, ...) self._workers.append(worker) 读到这里，我们基本对 verl 有了一些感觉。注意到，在 verl 当中有两个带有 Worker 的 base class，一个就叫做 Worker，另一个叫做 WorkerGroup。Worker 是 RL 里面的逻辑类（比如 actor 和 critic）,实际管理 RL 的数据流，而 WorkerGroup 只用于分布式系统的资源调度。\n此外，从 actor_rollout_wg 和 ref_policy_wg 的实例化当中，也能看出一些学问。在 ActorRolloutRefWorker 的设计当中，Actor Training，Actor Rollout 和 Reference model 是用同一个 worker class 进行管理的。但是，之后委托给 RayWorkerGroup 创建 worker group 并且调用资源的时候，Actor Training 和 Actor Rollout 是由同一组 RayWorkerGroup 进行资源管理的（这二者本来就要被放在同一个资源组上做 hybird engine），而 Reference Model 是由另一组 RayWorkerGroup 管理资源的。\n最后，我去问了相关开发者，他们也认为把 Actor Rollout，Actor Training 和 Reference Model 放在同一个 worker 里是 bad design 😂，不用纠结这种设计是否有什么高瞻远瞩，完全没有。\nActorRolloutRefWorker.__init__() 如前文所说，ActorRolloutRefWorker 是 verl 中用于管理 Actor Training，Actor Rollout 和 Reference Model 的 worker class。我们具体来分析其逻辑上实现的功能。注意，本文档只分析 FSDP backend 下的实现，megatron 留作后文。\n调用 Worker 基类的构造函数，并保存配置。 如果 PyTorch 分布式环境尚未初始化，则进行初始化，包括设置通信后端和进程组。 为 FSDP 创建设备网格，用于模型参数的分片。 如果启用 Ulysses 序列并行，则初始化其设备网格。 根据传入的 role 参数设置 Worker 的具体角色（actor, rollout, ref）。 根据 Worker 角色配置 profiler，用于性能分析。 配置 parameter offload 和 optimizer offload。 为 Actor，Rollout 和 Reference 分别 normalize batch size。 第 8 步中配置了非常多的 batch size；verl 的 batch size 参数满天飞，虽然我个人认为名字基本是准确的，但是由于名字太像了，一定要做出一些区分。事实上，参数分析我们有单独的文档，我先把一部分内容提前公布了。\ndata.train_batch_size：在一次完整的 PPO 迭代（从 rollout 到 train）中，从数据集中采样并用于生成 experience 的总样本数量，决定了每次 policy 更新所依据的数据量。 actor_rollout_ref.actor.ppo_mini_batch_size：这个参数的名字其实是准确的，因为 mini batch SGD 就是数据到达了一个 mini batch 就更新一次模型参数。在 verl 中，模型会在数据累积到一个 mini batch 后更新一次参数。 actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu：这里其实是 gradient accumulation 的参数。由于一个 mini batch 的数据量可能仍然太大，无法一次性前向和反向传播，因此需要将其进一步拆分为 micro batch。每个 micro batch 会计算一次梯度并且累计，但是不会立刻更新模型参数。处理完整个 mini batch 后，才用累积的梯度进行一次参数更新。 此外，在 verl 中，由于 verl 强调 SPMD 策略，可以理解为每个 RL worker 所占据的每个 GPU 上希望进行完全一致的操作，所以 verl 会要求每个 GPU 的 micro batch size 相同。因此，verl 会检查 train batch size / gpu 是否整除 (ref)，如果不整除，则报错。这个设定其实完全没必要；对于 rollout 而言，SGLang 完全不需要发送的请求数量整除 DP 或者 TP size，更何况直接要整除 gpu 数量呢？但是，因为 verl 会用 all gather 从 rollout 的每个 worker 里收集数据，这就要求 rollout 的每个 worker 上分到的数据一致。更进一步，为了 SPMD，又要求 rollout 的每个 gpu 上分到的数据一致。最终，这就导致 verl 的 train batch size 必须整除 gpu 数量；在 GRPO 下是 real train batch size 需要整除 n gpus，等于 train batch size * sampling params 中的 n。\n区分好 mini batch 和 micro batch 后，我也是最近才明白 PPO 中是如何维护 on policy 的。我之前一直以为我们都是在做严格 on policy 的训练，但是一个 train batch size 下有好几个 mini batch，似乎第一个 mini batch 结束之后，目标策略（target policy，被训练的 policy）和行为策略（behavior policy，用于在环境中采样的 policy）就不一致了。一次采样会训练很多个 mini batch，从第一个 mini batch 结束就不是 on policy 了。事实也是如此，我们注意到 PPO 的 loss function：\n$$ L^{CLIP}(\\theta) = \\mathbb{E}_t \\left[ \\min(r_t(\\theta) \\hat{A}_t, \\text{clip}(r_t(\\theta), 1-\\epsilon, 1+\\epsilon) \\hat{A}_t) \\right] $$其中的 $r_t(\\theta) = \\frac{\\pi_\\theta(a_t | s_t)}{\\pi_{\\theta_{old}}(a_t | s_t)}$，这是一个对优势函数的矫正比例，而 $\\hat{A}t$ 就是 advantage。对于 LLM 的 PPO 而言，$\\pi{\\theta_{old}}(a_t | s_t)$ 代表着采样时 behavior policy 在给定 $s_t$ 时，选择 $a_t$ 的概率，而 $\\pi_\\theta(a_t | s_t)$ 就是 target policy 在训练中的每一步给定 $s_t$ 时，选择 $a_t$ 的概率。对 LLM 而言，s_t 是 prompt 前缀，而 a_t 仅仅是 prompt 后的那一个 token。这一概率其实就是 inference 得到的 log probs；我们将收集得到的 (prompt, action) 分别经过 target policy 和 behaviour policy 得到 log probs，然后二者 log probs 相减再取对数，就是矫正项的值。从而，即便第一个 mini batch 之后 target policy 就已经和 behaviour policy 不一致了，仍然可以通过 log probs 进行矫正，也即 importance sampling。\n这样一来，又有了两个问题：log probs 应该如何得到？实际上每次采样时都是发送给 rollout 固定数量的 requests，如果每个 (prompt, action) 对都会计算一次 loss 的话，岂不是更长的 sequence 会计算更多次？\n对于第一个问题，这又是经典的精度问题。如同我在链接到的文章中所说的，rollout engine 目前只有采样得到的 token 能用，而得到的 log probs 以及 reward 精度都不够，不能用于训练。behaviour policy 和 target policy 为了做 importance sampling 所需的 log probs 都得用 training engine 重算。不过要算起来也不麻烦，在第一个 mini batch 启动前，这时候 target behaviour 是一致的，重算 log probs 并且存下来即可。\n对于第二个问题，的确如此。一条很长的 prompt + answer 序列确实会产生非常多的 (prompt, action) 对，其中每个对都可以看作一个 (state, action) 对。而且理论上每个这样的 (prompt, action) 对都会参与 Loss 的计算。这确实可能导致长序列中的 token 会在 Loss 计算中占据更大的比例，让模型过度关注长序列的优化，而对短序列的优化不足。不过，verl 的 rollout engine 会自动对每个 (prompt, action) 对进行加权，从而让长序列和短序列的 token 在 Loss 计算中占据相同的权重。为了缓解这种情况，有很多相关方法：\n样本加权方法 序列级别加权： 一种直接的方法是在计算 Loss 时，给来自不同序列的样本赋予不同的权重。例如，给每个完整序列一个固定的权重（比如 1），然后将这个权重均匀分配给该序列中的每个 (prompt, action) 对。这样，无论序列多长，它对总 Loss 的贡献都相同。如果一个序列有 N 个 token，那么每个 (prompt, action) 对的权重就是 1/N。\n按长度分桶： 在数据收集后，可以根据序列长度对样本进行排序，并尝试将相似长度的序列放入同一个 mini-batch。这有助于提高计算效率，因为可以减少 padding，但对于解决 Loss 贡献不均衡的作用有限。\n固定 Token 数量的批次： 最常见且有效的方法是构建批次时，不固定样本数量，而是固定批次中的总 token 数量。这样，一个 mini-batch 可能包含 4 条长序列，也可能包含 40 条短序列，确保每次更新时处理的总计算量和梯度来源的总 token 数是恒定的，从而缓解长短序列的不均衡问题。\nLoss 归一化：在计算每个 mini-batch 的 Loss 时，可以将其除以该 mini-batch 中实际的 token 数量。这确保了 Loss 值不会仅仅因为批次中包含了更多 token 而增大，从而为不同大小的 mini-batches（如果不是按固定 token 数构建）提供一个公平的比较基础。\n截断：设定一个 max_length 参数，限制模型生成的最大 token 数量。虽然这不直接解决已有长序列的权重问题，但可以防止生成过长的序列，从而限制极端不均衡的发生。\nwhatever，解释了这么多，顺着理解 verl 的框架进一步学习了 RL 算法和系统，这里其实和 multi-turn 都还没有关系，我们还是回到 ActorRolloutRefWorker 的源码上。\nActorRolloutRefWorker.__init__ 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 def __init__(self, config: DictConfig, role: str): # 初始化 Worker 基类 Worker.__init__(self) # 存储配置信息 self.config = config import torch.distributed # 如果分布式环境尚未初始化，则进行初始化 if not torch.distributed.is_initialized(): rank = int(os.environ.get(\u0026#34;RANK\u0026#34;, 0)) world_size = int(os.environ.get(\u0026#34;WORLD_SIZE\u0026#34;, 1)) torch.distributed.init_process_group(backend=f\u0026#34;cpu:gloo,{get_device_name()}:{get_nccl_backend()}\u0026#34;, rank=rank, world_size=world_size) # 为 FSDP 构建设备网格 world_size = torch.distributed.get_world_size() self.device_mesh = create_device_mesh(world_size=world_size, fsdp_size=self.config.actor.fsdp_config.fsdp_size) # 为 Ulysses 序列并行构建设备网格 self.ulysses_device_mesh = None self.ulysses_sequence_parallel_size = self.config.actor.get(\u0026#34;ulysses_sequence_parallel_size\u0026#34;, 1) dp = world_size // self.ulysses_sequence_parallel_size if self.ulysses_sequence_parallel_size \u0026gt; 1: self.ulysses_device_mesh = init_device_mesh(device_name, mesh_shape=(dp, self.ulysses_sequence_parallel_size), mesh_dim_names=[\u0026#34;dp\u0026#34;, \u0026#34;sp\u0026#34;]) # 初始化 Ulysses 分片管理器 self.ulysses_sharding_manager = FSDPUlyssesShardingManager(self.ulysses_device_mesh) # 获取 LoRA rank 和是否使用 LoRA 的标志 self._lora_rank = self.config.model.get(\u0026#34;lora_rank\u0026#34;, 0) self._is_lora = self._lora_rank \u0026gt; 0 # 设置 Worker 角色和相关标志 self.role = role assert self.role in [\u0026#34;actor\u0026#34;, \u0026#34;rollout\u0026#34;, \u0026#34;ref\u0026#34;, \u0026#34;actor_rollout\u0026#34;, \u0026#34;actor_rollout_ref\u0026#34;] self._is_actor = self.role in [\u0026#34;actor\u0026#34;, \u0026#34;actor_rollout\u0026#34;, \u0026#34;actor_rollout_ref\u0026#34;] self._is_rollout = self.role in [\u0026#34;rollout\u0026#34;, \u0026#34;actor_rollout\u0026#34;, \u0026#34;actor_rollout_ref\u0026#34;] self._is_ref = self.role in [\u0026#34;ref\u0026#34;, \u0026#34;actor_rollout_ref\u0026#34;] profiler_config: Optional[ProfilerConfig] = None # 根据角色获取性能分析配置 if self._is_actor: profiler_config = omega_conf_to_dataclass(config.actor.get(\u0026#34;profiler\u0026#34;, {}), ProfilerConfig) if self._is_rollout: profiler_config = omega_conf_to_dataclass(config.rollout.get(\u0026#34;profiler\u0026#34;, {}), ProfilerConfig) if self._is_ref: profiler_config = omega_conf_to_dataclass(config.ref.get(\u0026#34;profiler\u0026#34;, {}), ProfilerConfig) # 初始化分布式性能分析器 DistProfilerExtension.__init__(self, DistProfiler(rank=self.rank, config=profiler_config)) # 设置参数和优化器卸载标志 self._is_offload_param = False self._is_offload_optimizer = False if self._is_actor: self._is_offload_param = self.config.actor.fsdp_config.get(\u0026#34;param_offload\u0026#34;, False) self._is_offload_optimizer = self.config.actor.fsdp_config.get(\u0026#34;optimizer_offload\u0026#34;, False) elif self._is_ref: self._is_offload_param = self.config.ref.fsdp_config.get(\u0026#34;param_offload\u0026#34;, False) # 规范化 actor 相关配置 if self._is_actor: self.config.actor.ppo_mini_batch_size *= self.config.rollout.n self.config.actor.ppo_mini_batch_size //= self.device_mesh.size() // self.ulysses_sequence_parallel_size assert self.config.actor.ppo_mini_batch_size \u0026gt; 0, f\u0026#34;ppo_mini_batch_size {self.config.actor.ppo_mini_batch_size} should be larger than 0 after normalization\u0026#34; # micro bsz if self.config.actor.ppo_micro_batch_size is not None: self.config.actor.ppo_micro_batch_size //= self.device_mesh.size() // self.ulysses_sequence_parallel_size self.config.actor.ppo_micro_batch_size_per_gpu = self.config.actor.ppo_micro_batch_size if self.config.actor.ppo_micro_batch_size_per_gpu is not None: assert self.config.actor.ppo_mini_batch_size % self.config.actor.ppo_micro_batch_size_per_gpu == 0, f\u0026#34;normalized ppo_mini_batch_size {self.config.actor.ppo_mini_batch_size} should be divisible by ppo_micro_batch_size_per_gpu {self.config.actor.ppo_micro_batch_size_per_gpu}\u0026#34; assert self.config.actor.ppo_mini_batch_size // self.config.actor.ppo_micro_batch_size_per_gpu \u0026gt; 0, f\u0026#34;normalized ppo_mini_batch_size {self.config.actor.ppo_mini_batch_size} should be larger than ppo_micro_batch_size_per_gpu {self.config.actor.ppo_micro_batch_size_per_gpu}\u0026#34; # 规范化 rollout 相关配置 if self._is_rollout and self.config.rollout.log_prob_micro_batch_size is not None: self.config.rollout.log_prob_micro_batch_size //= self.device_mesh.size() // self.ulysses_sequence_parallel_size self.config.rollout.log_prob_micro_batch_size_per_gpu = self.config.rollout.log_prob_micro_batch_size # 规范化 ref 相关配置 if self._is_ref and self.config.ref.log_prob_micro_batch_size is not None: self.config.ref.log_prob_micro_batch_size //= self.device_mesh.size() // self.ulysses_sequence_parallel_size self.config.ref.log_prob_micro_batch_size_per_gpu = self.config.ref.log_prob_micro_batch_size ActorRolloutRefWorker._build_model_optimizer() 这部分源码和类写的还是很直白的，不用太多解释：\n初始化 Hugging Face 配置，获取 Generation Config，并设置模型的数据类型（Actor 使用 fp32，Reference 使用 bf16）。 使用 Hugging Face 的 AutoModelForCausalLM 或 AutoModelForVision2Seq 从预训练模型加载基础模型。 应用各种优化技术，包括 Liger kernel、融合 kernel、梯度检查点、LoRA 等。 根据配置选择 FSDP 或 FSDP2 策略，将模型封装到分布式训练框架中，支持参数分片和混合精度训练。 如果当前 Worker 是 Actor 角色，则初始化 AdamW 优化器和学习率调度器。 ActorRolloutRefWorker._build_model_optimizer 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def _build_model_optimizer( self, model_path, fsdp_config, optim_config, override_model_config, use_remove_padding=False, use_fused_kernels=False, enable_gradient_checkpointing=False, trust_remote_code=False, use_liger=False, role=\u0026#34;actor\u0026#34;, enable_activation_offload=False, ): from torch import optim from torch.distributed.fsdp import CPUOffload, MixedPrecision from transformers import AutoConfig, AutoModelForCausalLM, AutoModelForVision2Seq from verl.utils.model import get_generation_config, print_model_size, update_model_config from verl.utils.torch_dtypes import PrecisionType assert role in [\u0026#34;actor\u0026#34;, \u0026#34;ref\u0026#34;] log_gpu_memory_usage(f\u0026#34;Before init {role} from HF AutoModel\u0026#34;, logger=logger) local_path = model_path self.tokenizer = hf_tokenizer(local_path, trust_remote_code=trust_remote_code) self.processor = hf_processor(local_path, trust_remote_code=trust_remote_code) torch_dtype = fsdp_config.get(\u0026#34;model_dtype\u0026#34;, None) if torch_dtype is None: torch_dtype = torch.float32 if self._is_actor else torch.bfloat16 else: torch_dtype = PrecisionType.to_dtype(torch_dtype) actor_model_config = AutoConfig.from_pretrained(local_path, trust_remote_code=trust_remote_code, attn_implementation=\u0026#34;flash_attention_2\u0026#34;) if getattr(actor_model_config, \u0026#34;model_type\u0026#34;, None) == \u0026#34;kimi_vl\u0026#34;: actor_model_config.text_config.topk_method = \u0026#34;greedy\u0026#34; self.generation_config = get_generation_config(local_path, trust_remote_code=trust_remote_code) override_config_kwargs = { \u0026#34;bos_token_id\u0026#34;: self.tokenizer.bos_token_id, \u0026#34;eos_token_id\u0026#34;: self.tokenizer.eos_token_id, \u0026#34;pad_token_id\u0026#34;: self.tokenizer.pad_token_id, } override_config_kwargs.update(override_model_config) update_model_config(actor_model_config, override_config_kwargs=override_config_kwargs) # 如果是 rank 0 进程，打印更新后的模型配置 if self.rank == 0: print(f\u0026#34;Model config after override: {actor_model_config}\u0026#34;) init_context = get_init_weight_context_manager(use_meta_tensor=not actor_model_config.tie_word_embeddings, mesh=self.device_mesh) with init_context(), warnings.catch_warnings(): warnings.simplefilter(\u0026#34;ignore\u0026#34;) if type(actor_model_config) in AutoModelForVision2Seq._model_mapping.keys(): actor_module_class = AutoModelForVision2Seq else: actor_module_class = AutoModelForCausalLM actor_module = actor_module_class.from_pretrained( pretrained_model_name_or_path=local_path, torch_dtype=torch_dtype, config=actor_model_config, trust_remote_code=trust_remote_code, ) if use_liger: from liger_kernel.transformers.monkey_patch import _apply_liger_kernel_to_instance _apply_liger_kernel_to_instance(model=actor_module) fused_kernel_options = self.config.model.get(\u0026#34;fused_kernel_options\u0026#34;, None) fused_kernels_backend = fused_kernel_options.get(\u0026#34;impl_backend\u0026#34;, None) if fused_kernel_options is not None else None apply_monkey_patch( model=actor_module, use_remove_padding=use_remove_padding, ulysses_sp_size=self.ulysses_sequence_parallel_size, use_fused_kernels=use_fused_kernels, fused_kernels_backend=fused_kernels_backend, ) actor_module.to(torch_dtype) if enable_gradient_checkpointing: actor_module.gradient_checkpointing_enable(gradient_checkpointing_kwargs={\u0026#34;use_reentrant\u0026#34;: False}) if self._is_lora: print(\u0026#34;Applying LoRA to actor module\u0026#34;) actor_module.enable_input_require_grads() lora_config = {\u0026#34;task_type\u0026#34;: TaskType.CAUSAL_LM, \u0026#34;r\u0026#34;: self.config.model.lora_rank, \u0026#34;lora_alpha\u0026#34;: self.config.model.lora_alpha, \u0026#34;target_modules\u0026#34;: convert_to_regular_types(self.config.model.target_modules), \u0026#34;bias\u0026#34;: \u0026#34;none\u0026#34;} actor_module = get_peft_model(actor_module, LoraConfig(**lora_config)) torch.distributed.barrier() if self.rank == 0: print_model_size(actor_module) log_gpu_memory_usage(f\u0026#34;After init {role} from HF AutoModel\u0026#34;, logger=logger) mixed_precision_config = fsdp_config.get(\u0026#34;mixed_precision\u0026#34;, None) if mixed_precision_config is not None: param_dtype = PrecisionType.to_dtype(mixed_precision_config.get(\u0026#34;param_dtype\u0026#34;, \u0026#34;bf16\u0026#34;)) reduce_dtype = PrecisionType.to_dtype(mixed_precision_config.get(\u0026#34;reduce_dtype\u0026#34;, \u0026#34;fp32\u0026#34;)) buffer_dtype = PrecisionType.to_dtype(mixed_precision_config.get(\u0026#34;buffer_dtype\u0026#34;, \u0026#34;fp32\u0026#34;)) else: param_dtype = torch.bfloat16 reduce_dtype = torch.float32 buffer_dtype = torch.float32 mixed_precision = MixedPrecision(param_dtype=param_dtype, reduce_dtype=reduce_dtype, buffer_dtype=buffer_dtype) auto_wrap_policy = get_fsdp_wrap_policy(module=actor_module, config=fsdp_config.get(\u0026#34;wrap_policy\u0026#34;, None), is_lora=self.config.model.get(\u0026#34;lora_rank\u0026#34;, 0) \u0026gt; 0) # TODO(zhangchi.usc1992, shengguangming) fix me. Current, auto_wrap_policy causes HFRollout to hang in Gemma if self._is_rollout and self.config.rollout.name == \u0026#34;hf\u0026#34;: auto_wrap_policy = None # 如果是 rank 0 进程，打印包装策略 if self.rank == 0: print(f\u0026#34;wrap_policy: {auto_wrap_policy}\u0026#34;) fsdp_mesh = self.device_mesh sharding_strategy = get_sharding_strategy(fsdp_mesh) # TODO: 添加 transformer 策略 # 我们强制 reference policy 使用 CPUOffload 来节省内存 # 我们强制关闭 actor 的 CPUOffload，因为它在使用 grad accumulation 时会导致不正确的结果 cpu_offload = None if role == \u0026#34;actor\u0026#34; else CPUOffload(offload_params=True) # 根据配置的策略，将模型封装到 FSDP 中 fsdp_strategy = self.config.actor.strategy if fsdp_strategy == \u0026#34;fsdp\u0026#34;: actor_module_fsdp = FSDP( actor_module, cpu_offload=cpu_offload, param_init_fn=init_fn, use_orig_params=False, auto_wrap_policy=auto_wrap_policy, device_id=get_device_id(), sharding_strategy=sharding_strategy, # zero3 mixed_precision=mixed_precision, sync_module_states=True, device_mesh=self.device_mesh, forward_prefetch=self.config.actor.fsdp_config.forward_prefetch, ) elif fsdp_strategy == \u0026#34;fsdp2\u0026#34;: assert CPUOffloadPolicy is not None, \u0026#34;PyTorch version \u0026gt;= 2.4 is required for using fully_shard API (FSDP2)\u0026#34; mp_policy = MixedPrecisionPolicy(param_dtype=param_dtype, reduce_dtype=reduce_dtype, cast_forward_inputs=True) if role == \u0026#34;actor\u0026#34; and fsdp_config.offload_policy: cpu_offload = CPUOffloadPolicy(pin_memory=True) self._is_offload_param = False self._is_offload_optimizer = False else: cpu_offload = None if role == \u0026#34;actor\u0026#34; else CPUOffloadPolicy(pin_memory=True) fsdp_kwargs = { \u0026#34;mesh\u0026#34;: fsdp_mesh, \u0026#34;mp_policy\u0026#34;: mp_policy, \u0026#34;offload_policy\u0026#34;: cpu_offload, \u0026#34;reshard_after_forward\u0026#34;: fsdp_config.reshard_after_forward, } full_state = actor_module.state_dict() apply_fsdp2(actor_module, fsdp_kwargs, fsdp_config) fsdp2_load_full_state_dict(actor_module, full_state, fsdp_mesh, cpu_offload) actor_module_fsdp = actor_module else: raise NotImplementedError(f\u0026#34;not implement {fsdp_strategy}\u0026#34;) # 如果启用了激活卸载，则启用它 if enable_activation_offload: enable_activation_offloading(actor_module_fsdp, fsdp_strategy, enable_gradient_checkpointing) # 记录 FSDP 初始化之后的 GPU 内存使用情况 log_gpu_memory_usage(f\u0026#34;After {role} FSDP init\u0026#34;, logger=logger) # TODO: add more optimizer args into config if role == \u0026#34;actor\u0026#34; and optim_config is not None: from verl.utils.torch_functional import get_constant_schedule_with_warmup, get_cosine_schedule_with_warmup actor_optimizer = optim.AdamW( actor_module_fsdp.parameters(), lr=optim_config.lr, betas=optim_config.get(\u0026#34;betas\u0026#34;, (0.9, 0.999)), weight_decay=optim_config.get(\u0026#34;weight_decay\u0026#34;, 1e-2), ) total_steps = optim_config.get(\u0026#34;total_training_steps\u0026#34;, 0) num_warmup_steps = int(optim_config.get(\u0026#34;lr_warmup_steps\u0026#34;, -1)) warmup_style = optim_config.get(\u0026#34;warmup_style\u0026#34;, \u0026#34;constant\u0026#34;) min_lr_ratio = optim_config.get(\u0026#34;min_lr_ratio\u0026#34;, 0.0) num_cycles = optim_config.get(\u0026#34;num_cycles\u0026#34;, 0.5) if num_warmup_steps \u0026lt; 0: num_warmup_steps_ratio = optim_config.get(\u0026#34;lr_warmup_steps_ratio\u0026#34;, 0.0) num_warmup_steps = int(num_warmup_steps_ratio * total_steps) if self.rank == 0: print(f\u0026#34;Total steps: {total_steps}, num_warmup_steps: {num_warmup_steps}\u0026#34;) if warmup_style == \u0026#34;constant\u0026#34;: actor_lr_scheduler = get_constant_schedule_with_warmup(optimizer=actor_optimizer, num_warmup_steps=num_warmup_steps) elif warmup_style == \u0026#34;cosine\u0026#34;: actor_lr_scheduler = get_cosine_schedule_with_warmup(optimizer=actor_optimizer, num_warmup_steps=num_warmup_steps, num_training_steps=total_steps, min_lr_ratio=min_lr_ratio, num_cycles=num_cycles) else: raise NotImplementedError(f\u0026#34;Warmup style {warmup_style} is not supported\u0026#34;) log_gpu_memory_usage(f\u0026#34;After {role} optimizer init\u0026#34;, logger=logger) else: actor_optimizer = None actor_lr_scheduler = None return actor_module_fsdp, actor_optimizer, actor_lr_scheduler, actor_model_config 这里代码很直白。有一个点值得单独拎出来讲一下：仔细观察 actor_module 的 dtype，直觉告诉我，actor_module 的 dtype 应该是 bf16 的，而 gradient 和 optimizer 的 dtype 是 fp32 的。可是 actor_module 的 default dtype 被设为了 fp32，然后从 fp32 load 了模型。实际上这是因为 pytorch 的各种 optimizer 都是直接和 parameter 绑定的，用 bf16 的 parameter 初始化的 optimizer 也是 bf16。所以 model 先 load 了 fp32，然后初始化 optimizer 作为混合精度，最后把 model 转成 bf16。\nActorRolloutRefWorker._build_rollout() 这是对我而言最清晰的地方，实际上也是最熟悉的。在这里终于引入了 SGLang：\n设备网格创建：为 Rollout 创建推理张量并行（infer_tp）设备网格。 SGLang Rollout 构建：导入并实例化 SGLangRollout 和 FSDPSGLangShardingManager。FSDPSGLangShardingManager 负责在 FSDP 训练格式和 SGLang 推理格式之间转换模型权重。 ActorRolloutRefWorker._build_rollout 部分源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 def _build_rollout(self, trust_remote_code=False): from torch.distributed.device_mesh import init_device_mesh infer_tp = self.config.rollout.tensor_model_parallel_size dp = self.world_size // infer_tp assert self.world_size % infer_tp == 0, f\u0026#34;rollout world_size: {self.world_size} is not divisible by infer_tp: {infer_tp}\u0026#34; rollout_device_mesh = init_device_mesh(device_name, mesh_shape=(dp, infer_tp), mesh_dim_names=[\u0026#34;dp\u0026#34;, \u0026#34;infer_tp\u0026#34;]) rollout_name = self.config.rollout.name if rollout_name in [\u0026#34;sglang\u0026#34;, \u0026#34;sglang_async\u0026#34;]: if rollout_name == \u0026#34;sglang_async\u0026#34;: warnings.warn( \u0026#34;\u0026#39;sglang_async\u0026#39; has been deprecated and merged into \u0026#39;sglang\u0026#39;. Please use \u0026#39;sglang\u0026#39; going forward.\u0026#34;, DeprecationWarning, stacklevel=2, ) from verl.workers.rollout.sglang_rollout import SGLangRollout from verl.workers.sharding_manager.fsdp_sglang import FSDPSGLangShardingManager local_path = copy_to_local(self.config.model.path) log_gpu_memory_usage(f\u0026#34;Before building {rollout_name} rollout\u0026#34;, logger=logger) rollout = SGLangRollout( actor_module=local_path, config=self.config.rollout, tokenizer=self.tokenizer, model_hf_config=self.actor_model_config, trust_remote_code=trust_remote_code, ) log_gpu_memory_usage(f\u0026#34;After building {rollout_name} rollout\u0026#34;, logger=logger) if torch.distributed.get_world_size() == 1: self.config.rollout.load_format = \u0026#34;dummy_hf\u0026#34; rollout_sharding_manager = FSDPSGLangShardingManager( module=self.actor_module_fsdp, inference_engine=rollout._engine, model_config=self.actor_model_config, full_params=\u0026#34;hf\u0026#34; in self.config.rollout.load_format, device_mesh=rollout_device_mesh, offload_param=self._is_offload_param, ) log_gpu_memory_usage(\u0026#34;After building sharding manager\u0026#34;, logger=logger) else: raise NotImplementedError(f\u0026#34;Rollout name: {self.config.rollout.name} is not supported\u0026#34;) return rollout, rollout_sharding_manager SGLangRollout.__init__() 事已至此，再往下看一层 SGLang 具体的初始化：\n调用父类构造函数并设置配置和设备网格。 通过 _initialize_tools() 初始化工具 schemas、map 和解析器，支持 Multi-turn 对话中的工具使用。 初始化 SGLang 推理所需的分布式环境。 通过 _verify_config() 验证模型配置。 通过 _init_inference_engine() 初始化 SGLang 推理引擎。 通过 _init_sampling_params() 初始化生成序列的采样参数。 设置 Tokenizer 和 padding token ID。 SGLangRollout.__init__ 部分源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class SGLangRollout(BaseRollout): def __init__( self, actor_module: str, config: DictConfig, tokenizer, model_hf_config, port=None, trust_remote_code: bool = False, device_mesh: DeviceMesh | None = None, **kwargs, ): \u0026#34;\u0026#34;\u0026#34;Synchronized SGLang rollout engine. Args: actor_module: Huggingface model name or path to the model. The model should be supported by SGLang. config: A DictConfig object containing SGLang-specific operational parameters and rollout settings. Refer to https://docs.sglang.ai/backend/server_arguments.html tokenizer: The tokenizer instance compatible with the actor_module. model_hf_config: The Hugging Face model\u0026#39;s configuration (e.g., `transformers.PretrainedConfig`). It provides architectural details and hyperparameters like `max_position_embeddings`, used by SGLang for correct model initialization. This is the model\u0026#39;s inherent design, not SGLang\u0026#39;s runtime behavior. port: Optional port for multi-node initialization when nnodes \u0026gt; 1. trust_remote_code: Whether or not to allow for custom models defined on the Hub in their own modeling files. device_mesh: Optional `DeviceMesh` object for distributed setup. **kwargs: Additional keyword arguments, primarily `train_tp` for Megatron Backend integration to initialize hybrid engine process groups. \u0026#34;\u0026#34;\u0026#34; super().__init__() self.config = config self._device_mesh_cpu = device_mesh os.environ.setdefault(\u0026#34;SGL_DISABLE_TP_MEMORY_INBALANCE_CHECK\u0026#34;, \u0026#34;true\u0026#34;) ( self._tool_schemas, self._tool_map, self._tool_call_parser_type, self._sgl_tools, self._function_call_parser, ) = self._initialize_tools(config, tokenizer) self.interaction: dict[str, BaseInteraction] = self._intitalize_interaction(config) # If turn on `free_cache_engine`, SGLang engine\u0026#39;s KV cache # will be freed after each `generate_sequences` call. assert not (not config.enforce_eager and config.free_cache_engine), \u0026#34;disable CUDA graph (enforce_eager = False) if free cache engine\u0026#34; logger.info(f\u0026#34;tool_schemas: {self._tool_schemas}, tool_map: {self._tool_map}, tool_call_parser_type: {self._tool_call_parser_type}, sgl_tools: {self._sgl_tools}, function_call_parser: {self._function_call_parser}\u0026#34;) self._init_distributed_env(device_mesh_cpu=device_mesh, **kwargs) self._verify_config(model_hf_config=model_hf_config) # initialize the inference engine self._init_inference_engine(trust_remote_code, actor_module, port) self._init_sampling_params(**kwargs) self.tokenizer = tokenizer self.pad_token_id = tokenizer.pad_token_id SGLangRollout.AsyncEngine 关于 SGLangRollout 调用 tool 的部分，我们在下文的训练循环中再展开，这里先讨论完 SGLang 的初始化。为了调用 SGLang engine 的接口，verl 进行了一层封装，实现了我们对 SGLang 除开 rollout 之外的所有接口：\nrelease and resume memory occupation：在训练时释放掉显存占用并在训练后恢复。 update weights from tensor：训练结束后更新模型权重。 flush cache：模型参数更新后刷新 KV cache，因为之前的 KV cache 已经失效了。 这里涉及到了非常深入的内存管理问题，读者对 SGLang engine 在 verl 里的显存管理感兴趣，欢迎阅读标哥的博客 optimizing Memory Usage in verl，写的非常深入浅出。\nSGLangRollout 何时需要 flush cache 这一部分内容需要单独拎出来讲讲。SGLang engine 的 release 和 resume 需要保留 CUDA Graph，否则 rollout 效率会大幅降低。因此，我们基于 tom 的 torch_memory_saver 实现了独立的显存管理。简单来说，我们有：\npause；保留 mem savor 作用域内指定 tensor 的 virtual address，但是将其 physical memory 释放回显存管理器。 resume；将先前 pause 的 tensor 重新申请一块 physical memory，并将其 virtual address 映射到新的 physical memory。 注意，整个 pause 和 resume 的过程中，tensor 的 virtual address 不会发生变化，只是这块 virtual address 映射到的 physical memory 改变了。因此，CUDA Graph 并没有失效，不变的 virtual address 让计算流仍旧可以正常执行。\nverl 内的 release_memory_occupation 和 resume_memory_occupation 就是基于 pause 和 resume 实现的。听上去是个完美的故事，我们甚至实现了 mutli-stage 的显存管理，能够独立 release 和 resume kv cache 和 model weights。\n不过，对于 kv cache 而言，在 kv cache 被 release 掉之后，实际上 kv cache 的 tensor 仍旧保留，只是其 virtual address 映射到的 physical memory 被释放了。与此同时，radix tree 仍旧索引着整个 kv cache。当 kv cache 被 resume 之后，一方面之前物理内存上之前的 kv cache 已经不复存在了，另一方面模型的参数也被更新。出于这两点，我们一定要使用 flush cache 接口来刷新 kv cache 的索引（radix tree）。\n这里又有个非常有趣的设计。乍一想 kv cache 的管理这么麻烦，还要 flush，为什么不直接 delete kv cache 以及 delete model weights 再重新初始化呢？显然，这样没法利用已有的 cuda graph，非常消耗时间。保留 virtual address 不变但是更换 physical memory 的方案，让 verl 能够持续利用已建好的 cuda graph。\n最后一个问题，一共要几次 flush cache 呢？我个人理解，在一整个 training engine 被 pause，resume 然后 update weights 的过程中，必须要有一次 flush cache 来刷新 kv cache 的索引，只是 verl 当中为了保险，刷新了很多次罢了。\nSGLangRollout.AsyncEngine 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class AsyncEngine(sglang.srt.entrypoints.engine.Engine): def __init__(self, **kwargs): super().__init__(**kwargs) # default to use dummy load format, which need to reload weights in first time self._need_reload = True async def release_memory_occupation(self): \u0026#34;\u0026#34;\u0026#34;Release GPU occupation temporarily.\u0026#34;\u0026#34;\u0026#34; obj = ReleaseMemoryOccupationReqInput() return await self.tokenizer_manager.release_memory_occupation(obj, None) async def resume_memory_occupation(self): return await self.tokenizer_manager.resume_memory_occupation(obj, None) async def update_weights_from_tensor( self, named_tensors: List[Tuple[str, torch.Tensor]], # noqa: UP006 load_format: Optional[str] = None, flush_cache: bool = True, ): \u0026#34;\u0026#34;\u0026#34;Update weights from distributed source. If there are going to be more updates, set `flush_cache` to be false to avoid duplicated cache cleaning operation.\u0026#34;\u0026#34;\u0026#34; obj = UpdateWeightsFromTensorReqInput( serialized_named_tensors=[MultiprocessingSerializer.serialize(named_tensors) for _ in range(self.server_args.tp_size)], load_format=load_format, flush_cache=flush_cache, ) return await self.tokenizer_manager.update_weights_from_tensor(obj, None) async def flush_cache(self): return await self.tokenizer_manager.flush_cache() SGLangRollout._init_inference_engine() SGLangRollout._init_inference_engine() 初始化了封装的 AsyncEngine。\nSGLangRollout._init_inference_engine 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 def _init_inference_engine(self, trust_remote_code, actor_module, port): # initialize the inference engine nnodes = -(-self._tp_size // len(self.visible_devices_set)) if nnodes \u0026gt; 1: ip = get_ip() port = get_open_port() if port is None else port [ip, port] = broadcast_pyobj( [ip, port], rank=self._rank, dist_group=self._device_mesh_cpu.get_group(\u0026#34;tp\u0026#34;), src=self._device_mesh_cpu[\u0026#34;tp\u0026#34;].mesh[0].item(), force_cpu_device=False, ) dist_init_addr = f\u0026#34;[{ip}]:{port}\u0026#34; if is_ipv6(ip) else f\u0026#34;{ip}:{port}\u0026#34; else: dist_init_addr = None load_format = \u0026#34;dummy\u0026#34; if self.config.load_format.startswith(\u0026#34;dummy\u0026#34;) else self.config.load_format tp_size_per_node = self._tp_size // nnodes node_rank = self._tp_rank // tp_size_per_node first_rank_in_node = self._tp_rank % tp_size_per_node == 0 if first_rank_in_node: rank = dist.get_rank() os.environ[\u0026#34;SGLANG_BLOCK_NONZERO_RANK_CHILDREN\u0026#34;] = \u0026#34;0\u0026#34; self._engine = AsyncEngine( model_path=actor_module, dtype=self.config.dtype, mem_fraction_static=self.config.gpu_memory_utilization, enable_memory_saver=True, base_gpu_id=0, gpu_id_step=1, tp_size=self._tp_size, node_rank=node_rank, load_format=load_format, dist_init_addr=dist_init_addr, nnodes=nnodes, trust_remote_code=trust_remote_code, # NOTE(linjunrong): add rank to prevent SGLang generate same port inside PortArgs.init_new # when random.seed is being set during training port=30000 + rank, # NOTE(Chenyang): if you want to debug the SGLang engine output # please set the following parameters # Otherwise, it will make the engine run too slow # log_level=\u0026#34;INFO\u0026#34;, # log_requests=True, # log_requests_level=2, # max_running_requests=1, ) else: self._engine = None self.sharding_manager = None self.is_sleep = True 这里最值得一提的是，SGLang engine 并没有严格实现 verl 所希望的 SPMD 模式（每个 GPU 上的进程完全一样），而是采用了 mock 的 SPMD。举例来说，假设 tp size = 4，按照 verl 的设计，应该要 4 张 GPU 上每个都运行一个相同的 SGLang engine。实际上的实现是在 GPU 0 上启动一个进程占据全部 GPU，而 GPU 1 2 3 上仅仅保留一个空进程 None。虽然 verl team 起初设定中认为严格的 SPMD 意义巨大，但实际使用中，我们认为 mock 的 SPMD 已经足够满足性能需求。\n【TODO】 这么描述可能不严谨。\nTaskRunner.run() 往下走了这么多层，我们终于能够继续回到 TaskRunner 类。😭\n【TODO】上文其实主要是 Actor Rollout，还没有具体说 Actor 的 training forward and backward。以及 Reference，reward 和 critic 的 training forward and backward。\n加载、解析和验证训练任务的配置（使用 OmegaConf），确保所有参数的正确性和一致性。 将模型文件从远程路径复制到本地，确保所有 Worker 都可以访问。 组件初始化： 初始化 Tokenizer 和 Processor，用于文本和多模态数据的处理。 根据配置中指定的 Actor 策略（如 fsdp 或 megatron），动态选择相应的 Worker 类（例如 ActorRolloutRefWorker 和 CriticWorker），并确定使用的 RayWorkerGroup 类型。 定义 Ray 资源池的规格和角色到资源池的映射，用于 GPU 资源的分配和管理。 加载用于训练和验证的奖励模型。 创建训练和验证数据集，以及训练数据采样器。 创建 RayPPOTrainer 实例，它是管理所有计算资源和训练流程的中央协调器。 调用 RayPPOTrainer 的 init_workers() 方法，将配置的 Worker 类实例化到 Ray 集群的 GPU 上，为实际计算做准备。 调用 RayPPOTrainer 的 fit() 方法，启动 PPO 训练循环。 TaskRunner.run 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 @ray.remote(num_cpus=1) class TaskRunner: def run(self, config): from pprint import pprint from omegaconf import OmegaConf from verl.utils.fs import copy_to_local import socket import os print(f\u0026#34;TaskRunner hostname: {socket.gethostname()}, PID: {os.getpid()}\u0026#34;) pprint(OmegaConf.to_container(config, resolve=True)) OmegaConf.resolve(config) # 模型下载 local_path = copy_to_local(config.actor_rollout_ref.model.path, use_shm=config.actor_rollout_ref.model.get(\u0026#34;use_shm\u0026#34;, False)) # Tokenizer 和 Processor 初始化 from verl.utils import hf_processor, hf_tokenizer trust_remote_code = config.data.get(\u0026#34;trust_remote_code\u0026#34;, False) tokenizer = hf_tokenizer(local_path, trust_remote_code=trust_remote_code) processor = hf_processor(local_path, trust_remote_code=trust_remote_code, use_fast=True) # Worker 类型选择 if config.actor_rollout_ref.actor.strategy in [\u0026#34;fsdp\u0026#34;, \u0026#34;fsdp2\u0026#34;]: from verl.single_controller.ray import RayWorkerGroup from verl.workers.fsdp_workers import ActorRolloutRefWorker, AsyncActorRolloutRefWorker, CriticWorker actor_rollout_cls = AsyncActorRolloutRefWorker if config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34; else ActorRolloutRefWorker ray_worker_group_cls = RayWorkerGroup elif config.actor_rollout_ref.actor.strategy == \u0026#34;megatron\u0026#34;: assert config.actor_rollout_ref.actor.strategy == config.critic.strategy from verl.single_controller.ray.megatron import NVMegatronRayWorkerGroup from verl.workers.megatron_workers import ActorRolloutRefWorker, AsyncActorRolloutRefWorker, CriticWorker actor_rollout_cls = AsyncActorRolloutRefWorker if config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34; else ActorRolloutRefWorker ray_worker_group_cls = NVMegatronRayWorkerGroup else: raise NotImplementedError from verl.trainer.ppo.ray_trainer import ResourcePoolManager, Role # 角色到 Worker 类的映射 role_worker_mapping = { Role.ActorRollout: ray.remote(actor_rollout_cls), Role.Critic: ray.remote(CriticWorker), } # 资源池规格和角色映射 global_pool_id = \u0026#34;global_pool\u0026#34; resource_pool_spec = { global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes, } mapping = { Role.ActorRollout: global_pool_id, Role.Critic: global_pool_id, } # Reward Model Worker 的初始化 if config.reward_model.enable: if config.reward_model.strategy in [\u0026#34;fsdp\u0026#34;, \u0026#34;fsdp2\u0026#34;]: from verl.workers.fsdp_workers import RewardModelWorker elif config.reward_model.strategy == \u0026#34;megatron\u0026#34;: from verl.workers.megatron_workers import RewardModelWorker else: raise NotImplementedError role_worker_mapping[Role.RewardModel] = ray.remote(RewardModelWorker) mapping[Role.RewardModel] = global_pool_id # Reference Policy Worker 的初始化 if config.algorithm.use_kl_in_reward or config.actor_rollout_ref.actor.use_kl_loss: role_worker_mapping[Role.RefPolicy] = ray.remote(ActorRolloutRefWorker) mapping[Role.RefPolicy] = global_pool_id # 加载奖励管理器 reward_fn = load_reward_manager(config, tokenizer, num_examine=0, **config.reward_model.get(\u0026#34;reward_kwargs\u0026#34;, {})) val_reward_fn = load_reward_manager(config, tokenizer, num_examine=1, **config.reward_model.get(\u0026#34;reward_kwargs\u0026#34;, {})) resource_pool_manager = ResourcePoolManager(resource_pool_spec=resource_pool_spec, mapping=mapping) from verl.utils.dataset.rl_dataset import collate_fn, create_rl_dataset, create_rl_sampler # 创建训练和验证数据集 train_dataset = create_rl_dataset(config.data.train_files, config.data, tokenizer, processor) val_dataset = create_rl_dataset(config.data.val_files, config.data, tokenizer, processor) train_sampler = create_rl_sampler(config.data, train_dataset) # 初始化 PPO 训练器 trainer = RayPPOTrainer( config=config, tokenizer=tokenizer, processor=processor, role_worker_mapping=role_worker_mapping, resource_pool_manager=resource_pool_manager, ray_worker_group_cls=ray_worker_group_cls, reward_fn=reward_fn, val_reward_fn=val_reward_fn, train_dataset=train_dataset, val_dataset=val_dataset, collate_fn=collate_fn, train_sampler=train_sampler, device_name=config.trainer.device, ) # 初始化训练器的 Workers trainer.init_workers() # 启动训练过程 trainer.fit() RayPPOTrainer.__init__() 保存传入的配置对象、tokenizer、processor、角色到 Worker 的映射、资源池管理器以及 WorkerGroup 类。 根据配置启用或禁用 Critic、Reference Policy、Reward Model 和 Hybrid Engine 等功能组件。 调用 _validate_config() 方法验证配置的合理性。 存储训练和验证数据集、collate 函数和训练数据采样器。 RayPPOTrainer 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 class RayPPOTrainer: # TODO: support each role have individual ray_worker_group_cls, # i.e., support different backend of different role def __init__( self, config, tokenizer, role_worker_mapping: dict[Role, WorkerType], resource_pool_manager: ResourcePoolManager, ray_worker_group_cls: RayWorkerGroup = RayWorkerGroup, processor=None, reward_fn=None, val_reward_fn=None, train_dataset: Optional[Dataset] = None, val_dataset: Optional[Dataset] = None, collate_fn=None, train_sampler: Optional[Sampler] = None, device_name=\u0026#34;cuda\u0026#34;, ): \u0026#34;\u0026#34;\u0026#34; Initialize distributed PPO trainer with Ray backend. Note that this trainer runs on the driver process on a single CPU/GPU node. Args: config: Configuration object containing training parameters. tokenizer: Tokenizer used for encoding and decoding text. role_worker_mapping (dict[Role, WorkerType]): Mapping from roles to worker classes. resource_pool_manager (ResourcePoolManager): Manager for Ray resource pools. ray_worker_group_cls (RayWorkerGroup, optional): Class for Ray worker groups. Defaults to RayWorkerGroup. processor: Optional data processor, used for multimodal data. reward_fn: Function for computing rewards during training. val_reward_fn: Function for computing rewards during validation. train_dataset (Optional[Dataset], optional): Training dataset. Defaults to None. val_dataset (Optional[Dataset], optional): Validation dataset. Defaults to None. collate_fn: Function to collate data samples into batches. train_sampler (Optional[Sampler], optional): Sampler for the training dataset. Defaults to None. device_name (str, optional): Device name for training (e.g., \u0026#34;cuda\u0026#34;, \u0026#34;cpu\u0026#34;). Defaults to \u0026#34;cuda\u0026#34;. \u0026#34;\u0026#34;\u0026#34; # Store the tokenizer for text processing self.tokenizer = tokenizer self.processor = processor self.config = config self.reward_fn = reward_fn self.val_reward_fn = val_reward_fn self.hybrid_engine = config.actor_rollout_ref.hybrid_engine assert self.hybrid_engine, \u0026#34;Currently, only support hybrid engine\u0026#34; if self.hybrid_engine: assert Role.ActorRollout in role_worker_mapping, f\u0026#34;{role_worker_mapping.keys()=}\u0026#34; self.role_worker_mapping = role_worker_mapping self.resource_pool_manager = resource_pool_manager self.use_reference_policy = Role.RefPolicy in role_worker_mapping self.use_rm = Role.RewardModel in role_worker_mapping self.ray_worker_group_cls = ray_worker_group_cls self.device_name = device_name self.validation_generations_logger = ValidationGenerationsLogger() # if ref_in_actor is True, the reference policy will be actor without lora applied self.ref_in_actor = config.actor_rollout_ref.model.get(\u0026#34;lora_rank\u0026#34;, 0) \u0026gt; 0 # define in-reward KL control # kl loss control currently not suppoorted if config.algorithm.use_kl_in_reward: self.kl_ctrl_in_reward = core_algos.get_kl_controller(config.algorithm.kl_ctrl) if self.config.algorithm.adv_estimator == AdvantageEstimator.GAE: self.use_critic = True elif self.config.algorithm.adv_estimator in [ AdvantageEstimator.GRPO, AdvantageEstimator.GRPO_PASSK, AdvantageEstimator.REINFORCE_PLUS_PLUS, AdvantageEstimator.REMAX, AdvantageEstimator.RLOO, AdvantageEstimator.OPO, AdvantageEstimator.REINFORCE_PLUS_PLUS_BASELINE, ]: self.use_critic = False else: raise NotImplementedError self._validate_config() self._create_dataloader(train_dataset, val_dataset, collate_fn, train_sampler) RayPPOTrainer.init_workers() init_workers() 函数负责在 Ray 集群上实例化和初始化 ActorRollout、Critic、Reference Policy 和 Reward Model Workers。\n创建资源池：通过 ResourcePoolManager 创建 Ray 资源池。 初始化资源池到类的映射：为每个资源池创建一个字典，用于存储不同角色 Worker 的 RayClassWithInitArgs 包装器。RayClassWithInitArgs 用于延迟初始化 Worker，存储了 Worker 的类和初始化参数。 创建不同角色的 Worker 的 RayClassWithInitArgs 实例：根据配置启用情况，为 ActorRollout、Critic、Reference Policy 和 Reward Model 创建对应的 RayClassWithInitArgs 实例。 初始化 WorkerGroup：遍历所有资源池，将同一资源池中的多个 Worker 类通过 create_colocated_worker_cls 组合成一个共置类，然后实例化 RayWorkerGroup。RayWorkerGroup 负责在多个 GPU 上启动多个 Worker 实例。最后调用 spawn() 方法在 Ray 中实际创建 Worker 实例。 初始化各个 Worker：根据角色从创建的 WorkerGroup 字典中获取对应的 WorkerGroup，并调用其 init_model() 方法，按照依赖关系依次初始化不同的 Worker 模块。ActorRollout Worker 通常最后初始化以优化内存使用。 RayPPOTrainer.init_workers 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 def init_workers(self): \u0026#34;\u0026#34;\u0026#34;Initialize distributed training workers using Ray backend. Creates: 1. Ray resource pools from configuration 2. Worker groups for each role (actor, critic, etc.) \u0026#34;\u0026#34;\u0026#34; self.resource_pool_manager.create_resource_pool() self.resource_pool_to_cls = {pool: {} for pool in self.resource_pool_manager.resource_pool_dict.values()} # create actor and rollout if self.hybrid_engine: resource_pool = self.resource_pool_manager.get_resource_pool(Role.ActorRollout) actor_rollout_cls = RayClassWithInitArgs( cls=self.role_worker_mapping[Role.ActorRollout], config=self.config.actor_rollout_ref, role=\u0026#34;actor_rollout\u0026#34;, ) self.resource_pool_to_cls[resource_pool][\u0026#34;actor_rollout\u0026#34;] = actor_rollout_cls else: raise NotImplementedError # create critic if self.use_critic: resource_pool = self.resource_pool_manager.get_resource_pool(Role.Critic) critic_cls = RayClassWithInitArgs(cls=self.role_worker_mapping[Role.Critic], config=self.config.critic) self.resource_pool_to_cls[resource_pool][\u0026#34;critic\u0026#34;] = critic_cls # create reference policy if needed if self.use_reference_policy: resource_pool = self.resource_pool_manager.get_resource_pool(Role.RefPolicy) ref_policy_cls = RayClassWithInitArgs(self.role_worker_mapping[Role.RefPolicy], config=self.config.actor_rollout_ref, role=\u0026#34;ref\u0026#34;) self.resource_pool_to_cls[resource_pool][\u0026#34;ref\u0026#34;] = ref_policy_cls # create a reward model if reward_fn is None if self.use_rm: # we create a RM here resource_pool = self.resource_pool_manager.get_resource_pool(Role.RewardModel) rm_cls = RayClassWithInitArgs(self.role_worker_mapping[Role.RewardModel], config=self.config.reward_model) self.resource_pool_to_cls[resource_pool][\u0026#34;rm\u0026#34;] = rm_cls # initialize WorkerGroup # NOTE: if you want to use a different resource pool for each role, which can support different parallel size, # you should not use `create_colocated_worker_cls`. # Instead, directly pass different resource pool to different worker groups. # See https://github.com/volcengine/verl/blob/master/examples/ray/tutorial.ipynb for more information. all_wg = {} wg_kwargs = {} # Setting up kwargs for RayWorkerGroup if OmegaConf.select(self.config.trainer, \u0026#34;ray_wait_register_center_timeout\u0026#34;) is not None: wg_kwargs[\u0026#34;ray_wait_register_center_timeout\u0026#34;] = self.config.trainer.ray_wait_register_center_timeout if OmegaConf.select(self.config.trainer, \u0026#34;profile_steps\u0026#34;) is not None: wg_kwargs[\u0026#34;profile_steps\u0026#34;] = OmegaConf.select(self.config.trainer, \u0026#34;profile_steps\u0026#34;) assert OmegaConf.select(self.config.trainer, \u0026#34;worker_nsight_options\u0026#34;) is not None, \u0026#34;worker_nsight_options must be set when profile_steps is set\u0026#34; wg_kwargs[\u0026#34;worker_nsight_options\u0026#34;] = OmegaConf.to_container(OmegaConf.select(self.config.trainer, \u0026#34;worker_nsight_options\u0026#34;)) for resource_pool, class_dict in self.resource_pool_to_cls.items(): worker_dict_cls = create_colocated_worker_cls(class_dict=class_dict) wg_dict = self.ray_worker_group_cls(resource_pool=resource_pool, ray_cls_with_init=worker_dict_cls, device_name=self.device_name, **wg_kwargs) spawn_wg = wg_dict.spawn(prefix_set=class_dict.keys()) all_wg.update(spawn_wg) if self.use_critic: self.critic_wg = all_wg[\u0026#34;critic\u0026#34;] self.critic_wg.init_model() if self.use_reference_policy and not self.ref_in_actor: self.ref_policy_wg = all_wg[\u0026#34;ref\u0026#34;] self.ref_policy_wg.init_model() if self.use_rm: self.rm_wg = all_wg[\u0026#34;rm\u0026#34;] self.rm_wg.init_model() # we should create rollout at the end so that vllm can have a better estimation of kv cache memory self.actor_rollout_wg = all_wg[\u0026#34;actor_rollout\u0026#34;] self.actor_rollout_wg.init_model() # create async rollout manager and request scheduler self.async_rollout_mode = False if self.config.actor_rollout_ref.rollout.mode == \u0026#34;async\u0026#34;: from verl.workers.rollout.async_server import AsyncLLMServerManager self.async_rollout_mode = True self.async_rollout_manager = AsyncLLMServerManager( config=self.config, worker_group=self.actor_rollout_wg, ) ","permalink":"https://pillumina.github.io/posts/aiinfra/07-verl-multiturn-1/","summary":"\u003cblockquote\u003e\n\u003cp\u003e该part主要聚焦相关模块初始化部分\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e还是以 verl 出发，分析其 end to end mutli-turn RL 训练的全过程。整体上，我希望覆盖所有重要的 class 以及函数，更细粒度的代码不再展开。\u003c/p\u003e\n\u003cp\u003e为了前后内容的一致性，基于 \u003ca href=\"https://github.com/volcengine/verl/commit/76f63cffa5081564d8fea93a1cb3ce8bd5bdcc39\"\u003e76f63cffa5\u003c/a\u003e 的 commit 进行分析。\u003c/p\u003e\n\u003cp\u003e虽然本文以分析 verl 的代码为主，写完之后我才意识到，系统设计问题是非常通用的。诸如“log probs 重计算”，“Rollout Engine 显存管理”等等系统设计，是各大 RL 框架都需要考虑的核心问题。\u003c/p\u003e\n\u003cp\u003e此外因为最近在学习SGLang的实现，本文的推理后端选择的是SGLang展开分析。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e整个训练的示意图如下，我们会具体展开每个部分。\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003e\n  flowchart LR\nsubgraph W2[\u0026#34;Initialize\u0026#34;]\nWP[Process Data] --\u0026gt; A\ndirection TB D1[Data Prepare] --\u0026gt; A\nA[TaskRunner] --\u0026gt; B1[RayPPOTrainer]\nB1 --\u0026gt; Workers\n\n    subgraph Workers[\u0026#34;Workers\u0026#34;]\n        direction TB\n                WA[ActorRolloutWorker] --\u0026gt; WD[FSDP Engine]\n        WB[CriticWorker] --\u0026gt; WD\n        WC[RewardModelWorker] --\u0026gt; WD\n        WD --\u0026gt; WE[SGLang Engine]\n    end\n    \n    Workers --\u0026gt; C1[Hybrid Engine]\nend\n\nsubgraph W3[\u0026#34;Train Loop\u0026#34;]\n    direction TB\n    E[DataLoader] --\u0026gt; RolloutBox\n    \n    subgraph RolloutBox[\u0026#34;Rollout\u0026#34;]\n        F1[Prepare Data] --\u0026gt; F2[SGLang Async Rollout]\n        F2 --\u0026gt; F3[Multi-turn Chat Process]\n    end\n    \n    RolloutBox --\u0026gt; ExpBox\n    \n    subgraph ExpBox[\u0026#34;Make Experience\u0026#34;]\n        G1[Recompute Log Probs] --\u0026gt; G2[Compute Reward]\n        G2 --\u0026gt; G3[Compute Advantage]\n    end\n    \n    ExpBox --\u0026gt; UpdateBox\n    \n    subgraph UpdateBox[\u0026#34;Train The Model\u0026#34;]\n        H1[Load FSDP Model Weight] --\u0026gt; H2[Compute Gradient]\n        H2 --\u0026gt; H3[Weights Update]\n        H3 --\u0026gt; H4[Sync Weights]\n    end\n    \n    UpdateBox --\u0026gt; E\nend\n\nW2 --\u0026gt; W3\n\u003c/pre\u003e\n\n\u003ch2 id=\"数据预处理\"\u003e\u003cstrong\u003e数据预处理\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e以 \u003ca href=\"https://huggingface.co/datasets/openai/gsm8k\"\u003eGSM8K\u003c/a\u003e 为例，预处理脚本是 \u003ccode\u003eexamples/data_preprocess/gsm8k_multiturn_w_tool.py\u003c/code\u003e。整个脚本只做了经典的 huggingface datasets mapping，核心逻辑如下：\u003c/p\u003e","title":"[VeRL] Multi-Turn RL训练源码走读（1）"},{"content":"近期看到一些关于传统基础设施（Traditional Infrastructure）与人工智能基础设施（AI Infrastructure，尤其大模型领域）差异的评论。其核心观点直指两者间的巨大鸿沟：许多精于网络、计算、存储等传统领域的工程师，在面对GPU集群、KV Cache管理、3D并行等全新概念时，常感过往经验难以直接套用，甚至产生踏入一个全然不同技术体系的“割裂感”。\n这些看法颇具代表性，精准捕捉了工程师初探AI Infra时的普遍印象：陌生、高门槛、范式迥异。本文旨在分享我对此的一些初步思考：AI Infra究竟是颠覆传统的全新体系，抑或是既有Infra经验在智能时代的一次深度演化？\n（免责声明：本文纯属个人观点，旨在抛砖引玉，欢迎指正谬误！）\n我的核心论点：AI Infra并非平地起高楼，它实质上是传统Infra工程智慧在新场景下的重构与系统性延展。\n表象差异：新术语与新挑战带来的“视觉冲击” 乍看之下，AI Infra与传统Infra确实分野明显：\n核心任务不同： 传统Infra聚焦于处理海量Web请求（毫秒级、无状态）、保障数据持久化存储、实现分布式服务协调。而AI Infra（尤以大模型为甚）则围绕GPU驱动的模型训练/推理、KV Cache的高效管理、百亿/千亿级参数的分布式执行框架展开。 请求形态迥异： Web请求追求瞬时响应（毫秒级）、天然无状态。大模型（LLM）推理则常承载持续的会话交互（秒级乃至更长，随上下文窗口扩展而递增），需动态维护细粒度的Token级状态（KV Cache）。 技术栈迭代： 熟悉的Kubernetes + Docker堆栈旁，涌现出GPU硬件抽象、vLLM、DeepSpeed、FlashAttention、Triton、NCCL等专为AI设计、名号“高深”的组件。 由此观之，认为传统经验难以直接迁移，确有其表象依据。但这仅仅是“水面之上的冰山”，远非其底层基石。\n本质共性：工程核心挑战的永恒回归 拨开“AI专属”的面纱，工程实践的核心命题依然如故：系统设计与资源调度的精妙艺术。 我们面临的，仍是那些传统Infra领域中反复锤炼的同类问题，只是约束条件和优化目标发生了变化：\n资源调度： 核心资源从CPU/内存/磁盘IO，转向了更稀缺、更昂贵的GPU显存与算力。 负载处理： 承载对象从HTTP资源请求，变为密集的Prompt请求与大规模训练任务。 核心目标： 高效、稳定、低成本地协调跨节点资源的核心诉求丝毫未变。 概念的映射：经典范式的AI实践\n这种延续性，清晰地体现在关键概念的对应关系上：\n传统 Infra 概念 AI Infra 对应实践 核心思想应用 数据分片 (Data Sharding) 数据并行 (Data Parallelism) 数据集拆分，多副本并行处理 负载均衡 (Load Balancer) MoE Router (Mixture of Experts) 动态分配请求（Token）至专家网络，避免热点 操作系统分页 (OS Paging) vLLM KV Cache Paging 虚拟化显存空间，高效管理请求状态 以vLLM为例： 其核心创新在于将操作系统经典的内存管理机制（分页、交换），创造性地应用于管理LLM推理中关键的KV Cache状态。它如同为LLM定制了一个“显存操作系统”，管理“进程”（推理请求）和“内存页”（KV Cache Blocks），极致优化昂贵显存的利用率。这绝非凭空创造，而是经典系统原理在特定约束下的卓越应用。\n基础设施的“三座大山”：Scaling, Sharding, Copying 所有复杂分布式系统的底层挑战，最终都绕不开这三个永恒主题。AI Infra的“新”，在于其前所未有的规模、独特的瓶颈（显存/通信）和严苛的成本压力，使得这些经典挑战呈现出新的面貌与更高的解决难度：\n扩展性 (Scaling)：\n传统： 水平扩展服务器、容器化部署、负载均衡分散流量。 AI： 并行化策略成为生命线——通过数据并行 (Data Parallelism)、模型并行 (Model/Tensor Parallelism)、流水线并行 (Pipeline Parallelism) 的精细组合，在多GPU甚至多机集群上分布和执行庞然大物般的模型负载，支撑超大规模训练与高并发推理。挑战在于通信开销与计算负载的极致平衡。 分片 (Sharding)：\n传统： 数据库按主键/范围分区，提升访问吞吐。 AI： 分片对象变为模型参数、梯度、优化器状态、激活值以及核心的KV Cache。Tensor Parallelism将单层网络切片分布，KV Paging将长上下文状态分块管理。分片策略直接决定了分布式执行的可行性与效率。显存限制使其成为刚需而非优化项。 复制 (Copying)：\n传统： 数据库副本同步、缓存预热、Kafka副本保障可靠性。 AI： 复制代价被指数级放大。例如，Data Parallelism需在各GPU复制完整模型参数进行前向/反向传播（催生了ZeRO等优化技术来_分片_而非全复制参数/梯度/优化器状态）。高效的跨GPU/跨节点通信（依赖NCCL, RDMA） 成为关键瓶颈。带宽与延迟的优化是性能命门。 可见，核心挑战的本质——如何高效、稳健、低成本地协调跨异构节点的资源——从未改变。AI场景的独特约束（显存昂贵、模型巨大、上下文长、通信密集）只是将这些挑战推向了更极致、更脆弱的境地，要求更精巧的工程策略。\n工程之本：量化思维与瓶颈洞察 (Jeff Dean的启示) Google的Jeff Dean提出的“程序员应知的延迟数据”，深刻揭示了量化思维（Quantitative Reasoning）在系统设计中的基石地位。掌握关键操作的近似延迟数量级（如L1缓存访问 vs 内存访问 vs 网络传输 vs 磁盘寻道），是：\n事前设计的关键输入： 用于估算训练时间、推理吞吐、Token延迟，指导架构选型与容量规划。 事后诊断的利器： 当性能未达预期，理解这些基准能快速定位瓶颈所在——是计算(Compute-Bound)、显存带宽(Memory-Bound) 还是通信(Communication-Bound)？ 在AI Infra的映射：\nToken-level KV Cache访问 ≈ GPU HBM全局显存访问延迟 多GPU通信 (NCCL AllReduce等) ≈ 集群内高速网络通信延迟/带宽 跨物理服务器调度 ≈ 数据中心网络延迟 量化思维的普适性： 如同算法复杂度分析（O(n) vs O(n²)），数据库查询计划成本估算，或传统Infra中的延迟认知，在AI Infra中构建并运用类似的心智模型（估算计算FLOPs、显存占用、通信量及对应延迟/带宽）同样至关重要。这是区分优秀工程师的关键——将模糊的“慢”转化为可量化的瓶颈点。\n案例反思： Meta训练LLaMA模型时，GPU故障频发（据传约每几十分钟一次）。这凸显了在极端复杂、长周期运行的AI系统中，强大的日志(logging)、错误追踪(tracing)和性能剖析(profiling)工具对于系统稳定性与可观测性的极端重要性——这正是传统大规模分布式系统运维经验的直接延伸。\n延伸推荐： 李博杰的博客《4090 适合做 AI 训练还是推理？》是量化思维的绝佳范例。它基于具体的数据（模型尺寸、显存需求、通信开销、计算能力）进行严格估算，清晰论证了4090在训练与推理场景下的适用边界。这种基于数据的决策能力，是Infra工程师的核心素养。\n结语：褪去神秘，回归工程本质 当前关于AI Infra的讨论，有时不免笼罩着一层“神秘化”的光环。诚然，LLM的崛起带来了前所未有的模型形态（千亿参数）、交互范式（长会话）和资源瓶颈（显存墙、通信墙）。然而，穿透这些表象，工程要解决的核心命题始终稳固：\n最大化资源利用率 (Optimize Resource Utilization) -\u0026gt; 降低成本 保障服务稳定性 (Ensure Service Stability) 提升吞吐与响应能力 (Maximize Throughput \u0026amp; Minimize Latency) 这些命题，我们在构建Web服务、数据库、分布式系统的漫长历程中，早已反复求解。AI Infra的革新之处，在于我们需要在GPU主导的计算范式、大模型特有的状态管理（如KV Cache）和高并发推理请求的约束下，对经典解决方案进行系统性重构与深度优化。\n因此，AI Infra的高门槛，其核心不在于对神经网络理论的精通程度，而在于能否将既有的工程能力——系统设计思维、量化分析、问题拆解、性能优化、稳定性保障——无缝迁移并创造性应用于这一充满活力的新领域。\n熟悉网络通信？ 你会发现NCCL的Ring AllReduce拓扑与高性能集群通信设计异曲同工。 深谙缓存与OS内存管理？ KV Cache的重要性及其管理策略（如vLLM）会令你倍感亲切。 构建过服务调度器？ Dynamic Batching 本质上就是经典的流水线并发与批处理思想的再现。 我愈发坚信，AI Infra是传统Infra知识体系在新范式下的深度融合与卓越拓展。它是对旧有挑战在全新尺度与约束下的重新表述（Rephrasing）。\n真正具备竞争力的AI Infra工程师，绝非仅能调参或运行训练/推理脚本，而是那些深刻理解底层系统原理，并能将其与模型特性及业务需求融会贯通的人。这种思维范式的转换（Shift of Thinking）颇具挑战，但一旦建立起传统Infra \u0026lt;-\u0026gt; AI Infra 的概念映射，便会豁然开朗：那些看似“毫不相干”的传统经验，其底层逻辑往往换汤不换药。深厚的传统Infra功底，非但不是累赘，反而是驰骋AI Infra疆域的宝贵基石与独特优势。\n技术浪潮奔涌不息，工程智慧历久弥新。作为从业者，需要对技术有更为本质的认识和了解，才能游刃有余拥抱变化。\n","permalink":"https://pillumina.github.io/posts/aiinfra/04-aiinfra-thinking/","summary":"\u003cp\u003e近期看到一些关于传统基础设施（Traditional Infrastructure）与人工智能基础设施（AI Infrastructure，尤其大模型领域）差异的评论。其核心观点直指两者间的巨大鸿沟：许多精于网络、计算、存储等传统领域的工程师，在面对GPU集群、KV Cache管理、3D并行等全新概念时，常感过往经验难以直接套用，甚至产生踏入一个全然不同技术体系的“割裂感”。\u003c/p\u003e\n\u003cp\u003e这些看法颇具代表性，精准捕捉了工程师初探AI Infra时的普遍印象：\u003cstrong\u003e陌生、高门槛、范式迥异\u003c/strong\u003e。本文旨在分享我对此的一些初步思考：AI Infra究竟是颠覆传统的全新体系，抑或是既有Infra经验在智能时代的一次深度演化？\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e（\u003cem\u003e免责声明：本文纯属个人观点，旨在抛砖引玉，欢迎指正谬误！\u003c/em\u003e）\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e我的核心论点：AI Infra并非平地起高楼，它实质上是传统Infra工程智慧在新场景下的重构与系统性延展。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"表象差异新术语与新挑战带来的视觉冲击\"\u003e表象差异：新术语与新挑战带来的“视觉冲击”\u003c/h3\u003e\n\u003cp\u003e乍看之下，AI Infra与传统Infra确实分野明显：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务不同：\u003c/strong\u003e 传统Infra聚焦于处理海量Web请求（毫秒级、无状态）、保障数据持久化存储、实现分布式服务协调。而AI Infra（尤以大模型为甚）则围绕\u003cstrong\u003eGPU驱动的模型训练/推理\u003c/strong\u003e、\u003cstrong\u003eKV Cache的高效管理\u003c/strong\u003e、\u003cstrong\u003e百亿/千亿级参数的分布式执行框架\u003c/strong\u003e展开。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e请求形态迥异：\u003c/strong\u003e Web请求追求瞬时响应（毫秒级）、天然无状态。大模型（LLM）推理则常承载\u003cstrong\u003e持续的会话交互\u003c/strong\u003e（秒级乃至更长，随上下文窗口扩展而递增），需\u003cstrong\u003e动态维护细粒度的Token级状态\u003c/strong\u003e（KV Cache）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术栈迭代：\u003c/strong\u003e 熟悉的Kubernetes + Docker堆栈旁，涌现出GPU硬件抽象、vLLM、DeepSpeed、FlashAttention、Triton、NCCL等\u003cstrong\u003e专为AI设计、名号“高深”的组件\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e由此观之，认为传统经验难以直接迁移，确有其表象依据。但这仅仅是“水面之上的冰山”，远非其底层基石。\u003c/p\u003e\n\u003ch3 id=\"本质共性工程核心挑战的永恒回归\"\u003e本质共性：工程核心挑战的永恒回归\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e拨开“AI专属”的面纱，工程实践的核心命题依然如故：系统设计与资源调度的精妙艺术。\u003c/strong\u003e 我们面临的，仍是那些传统Infra领域中反复锤炼的同类问题，只是约束条件和优化目标发生了变化：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e资源调度：\u003c/strong\u003e 核心资源从CPU/内存/磁盘IO，\u003cstrong\u003e转向了更稀缺、更昂贵的GPU显存与算力\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e负载处理：\u003c/strong\u003e 承载对象从HTTP资源请求，\u003cstrong\u003e变为密集的Prompt请求与大规模训练任务\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心目标：\u003c/strong\u003e 高效、稳定、低成本地协调跨节点资源的核心诉求\u003cstrong\u003e丝毫未变\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e概念的映射：经典范式的AI实践\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这种延续性，清晰地体现在关键概念的对应关系上：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e传统 Infra 概念\u003c/th\u003e\n          \u003cth\u003eAI Infra 对应实践\u003c/th\u003e\n          \u003cth\u003e核心思想应用\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e数据分片 (Data Sharding)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e数据并行 (Data Parallelism)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e数据集拆分，多副本并行处理\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e负载均衡 (Load Balancer)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003eMoE Router (Mixture of Experts)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e动态分配请求（Token）至专家网络，避免热点\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e操作系统分页 (OS Paging)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003evLLM KV Cache Paging\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e虚拟化显存空间，高效管理请求状态\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e以vLLM为例：\u003c/strong\u003e 其核心创新在于将\u003cstrong\u003e操作系统经典的内存管理机制（分页、交换）\u003c/strong\u003e，创造性地应用于管理LLM推理中关键的\u003cstrong\u003eKV Cache状态\u003c/strong\u003e。它如同为LLM定制了一个“显存操作系统”，管理“进程”（推理请求）和“内存页”（KV Cache Blocks），极致优化昂贵显存的利用率。这绝非凭空创造，而是\u003cstrong\u003e经典系统原理在特定约束下的卓越应用\u003c/strong\u003e。\u003c/p\u003e","title":"AI Infra：颠覆性创新，还是经典工程范式的华丽转身？"},{"content":"LaTeX 渲染测试 基础测试 行内公式：$E = mc^2$\n块级公式：\n$$E = mc^2$$复杂公式测试 原始问题公式1（更稳妥写法，拆成两条显示公式） $$ \\mathbf{y} = f\\left(\\mathbf{x}\\, \\big[\\,\\mathbf{W}^{(A)}_1\\; \\mathbf{W}^{(A)}_2\\; \\cdots\\; \\mathbf{W}^{(A)}_n\\,\\big]\\right) \\begin{bmatrix} \\mathbf{W}^{(B)}_1 \\\\ \\mathbf{W}^{(B)}_2 \\\\ \\vdots \\\\ \\mathbf{W}^{(B)}_n \\end{bmatrix} $$ $$ \\mathbf{y} = \\sum_{i=1}^n f\\big(\\mathbf{x}\\mathbf{W}^{(A)}_i\\big)\\,\\mathbf{W}^{(B)}_i $$ 其他常见LaTeX测试 分数和根号： $$\\frac{\\sqrt{x^2 + y^2}}{2}$$积分： $$\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}$$矩阵（确保每行使用 \\ 换行）： $$ \\begin{pmatrix} a \u0026 b \\\\ c \u0026 d \\end{pmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} = \\begin{bmatrix} ax + by \\\\ cx + dy \\end{bmatrix} $$ 求和和乘积： $$\\sum_{i=1}^n i = \\frac{n(n+1)}{2}$$$$\\prod_{i=1}^n i = n!$$上下标组合： $x^{2^3}$, $x_{i_j}$, $x^{(a)}_{(b)}$\n特殊符号： $\\alpha, \\beta, \\gamma, \\delta, \\epsilon, \\zeta, \\eta, \\theta$\n$\\nabla, \\partial, \\infty, \\sum, \\prod, \\int, \\oint$\n","permalink":"https://pillumina.github.io/posts/demo/latex-test/","summary":"\u003ch1 id=\"latex-渲染测试\"\u003eLaTeX 渲染测试\u003c/h1\u003e\n\u003ch2 id=\"基础测试\"\u003e基础测试\u003c/h2\u003e\n\u003cp\u003e行内公式：$E = mc^2$\u003c/p\u003e\n\u003cp\u003e块级公式：\u003cbr\u003e\n\u003c/p\u003e\n$$E = mc^2$$\u003ch2 id=\"复杂公式测试\"\u003e复杂公式测试\u003c/h2\u003e\n\u003ch3 id=\"原始问题公式1更稳妥写法拆成两条显示公式\"\u003e原始问题公式1（更稳妥写法，拆成两条显示公式）\u003c/h3\u003e\n\n$$\n\\mathbf{y} = f\\left(\\mathbf{x}\\, \\big[\\,\\mathbf{W}^{(A)}_1\\; \\mathbf{W}^{(A)}_2\\; \\cdots\\; \\mathbf{W}^{(A)}_n\\,\\big]\\right)\n\\begin{bmatrix}\n\\mathbf{W}^{(B)}_1 \\\\\n\\mathbf{W}^{(B)}_2 \\\\\n\\vdots \\\\\n\\mathbf{W}^{(B)}_n\n\\end{bmatrix}\n$$\n\n\n$$\n\\mathbf{y} = \\sum_{i=1}^n f\\big(\\mathbf{x}\\mathbf{W}^{(A)}_i\\big)\\,\\mathbf{W}^{(B)}_i\n$$\n\n\u003ch2 id=\"其他常见latex测试\"\u003e其他常见LaTeX测试\u003c/h2\u003e\n\u003ch3 id=\"分数和根号\"\u003e分数和根号：\u003c/h3\u003e\n$$\\frac{\\sqrt{x^2 + y^2}}{2}$$\u003ch3 id=\"积分\"\u003e积分：\u003c/h3\u003e\n$$\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}$$\u003ch3 id=\"矩阵确保每行使用--换行\"\u003e矩阵（确保每行使用 \\ 换行）：\u003c/h3\u003e\n\n$$\n\\begin{pmatrix}\n a \u0026 b \\\\\n c \u0026 d\n\\end{pmatrix}\n\\begin{bmatrix}\n x \\\\\n y\n\\end{bmatrix}\n=\n\\begin{bmatrix}\n ax + by \\\\\n cx + dy\n\\end{bmatrix}\n$$\n\n\u003ch3 id=\"求和和乘积\"\u003e求和和乘积：\u003c/h3\u003e\n$$\\sum_{i=1}^n i = \\frac{n(n+1)}{2}$$$$\\prod_{i=1}^n i = n!$$\u003ch3 id=\"上下标组合\"\u003e上下标组合：\u003c/h3\u003e\n\u003cp\u003e$x^{2^3}$, $x_{i_j}$, $x^{(a)}_{(b)}$\u003c/p\u003e","title":"LaTeX Test Page"},{"content":" 本文旨在记录对中间件、编排组件容器化部署后，实现kubernetes扩展组件Controller的过程。\nThird-Parties kubernetes-client: javascript\nclient-go\nkube-rs\nclient-go源码分析 目录结构 kubernetes: contains the clientset to access Kubernetes API. discovery: discover APIs supported by a Kubernetes API server. dynamic: contains a dynamic client that can perform generic operations on arbitrary Kubernetes API objects. transport: set up auth and start a connection. tools/cache: useful for writing controllers. informers: informer group listers: lister group 代码实例 1 2 3 4 git clone https://github.com/huweihuang/client-go.git cd client-go #保证本地HOME目录有配置kubernetes集群的配置文件 go run client-go.go client-go.go\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; \u0026#34;time\u0026#34; metav1 \u0026#34;k8s.io/apimachinery/pkg/apis/meta/v1\u0026#34; \u0026#34;k8s.io/client-go/kubernetes\u0026#34; \u0026#34;k8s.io/client-go/tools/clientcmd\u0026#34; ) func main() { var kubeconfig *string if home := homeDir(); home != \u0026#34;\u0026#34; { kubeconfig = flag.String(\u0026#34;kubeconfig\u0026#34;, filepath.Join(home, \u0026#34;.kube\u0026#34;, \u0026#34;config\u0026#34;), \u0026#34;(optional) absolute path to the kubeconfig file\u0026#34;) } else { kubeconfig = flag.String(\u0026#34;kubeconfig\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;absolute path to the kubeconfig file\u0026#34;) } flag.Parse() // uses the current context in kubeconfig config, err := clientcmd.BuildConfigFromFlags(\u0026#34;\u0026#34;, *kubeconfig) if err != nil { panic(err.Error()) } // creates the clientset clientset, err := kubernetes.NewForConfig(config) if err != nil { panic(err.Error()) } for { pods, err := clientset.CoreV1().Pods(\u0026#34;\u0026#34;).List(metav1.ListOptions{}) if err != nil { panic(err.Error()) } fmt.Printf(\u0026#34;There are %d pods in the cluster\\n\u0026#34;, len(pods.Items)) time.Sleep(10 * time.Second) } } func homeDir() string { if h := os.Getenv(\u0026#34;HOME\u0026#34;); h != \u0026#34;\u0026#34; { return h } return os.Getenv(\u0026#34;USERPROFILE\u0026#34;) // windows } output\n1 2 3 4 5 6 ➜ go run client-go.go There are 9 pods in the cluster There are 7 pods in the cluster There are 7 pods in the cluster There are 7 pods in the cluster There are 7 pods in the cluster kubeconfig 1 kubeconfig = flag.String(\u0026#34;kubeconfig\u0026#34;, filepath.Join(home, \u0026#34;.kube\u0026#34;, \u0026#34;config\u0026#34;), \u0026#34;(optional) absolute path to the kubeconfig file\u0026#34;) 为了获取k8s配置文件kubeconfig的绝对路径，一般路径为$HOME/.kube/config，这个文件主要用来配置本地连接的k8s集群。\nconfig的内容大概如图所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 clusters: - cluster: server: http://\u0026lt;kube-master-ip\u0026gt;:8080 name: k8s contexts: - context: cluster: k8s namespace: default user: \u0026#34;\u0026#34; name: default current-context: default kind: Config preferences: {} users: [] rest.config 通过参数和BuildConfigFromFlags方法获取rest.Config对象\n1 config, err := clientcmd.BuildConfigFromFlags(\u0026#34;\u0026#34;, *kubeconfig) k8s.io/client-go/tools/clientcmd/client_config.go\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // BuildConfigFromFlags is a helper function that builds configs from a master // url or a kubeconfig filepath. These are passed in as command line flags for cluster // components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath // are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback // to the default config. func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) { if kubeconfigPath == \u0026#34;\u0026#34; \u0026amp;\u0026amp; masterUrl == \u0026#34;\u0026#34; { klog.Warning(\u0026#34;Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.\u0026#34;) kubeconfig, err := restclient.InClusterConfig() if err == nil { return kubeconfig, nil } klog.Warning(\u0026#34;error creating inClusterConfig, falling back to default config: \u0026#34;, err) } return NewNonInteractiveDeferredLoadingClientConfig( \u0026amp;ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, \u0026amp;ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig() } clientset 通过*rest.Config参数和NewForConfig方法来获取clientset对象，clientset是多个client集合，每个client可能包括不同版本的方法调用\n1 clientset, err := kubernetes.NewForConfig(config) NewForConfig NewForConfig函数就是初始化clientset中的每个client。\nk8s.io/client-go/kubernetes/clientset.go\n1 2 3 4 5 6 7 8 9 10 // NewForConfig creates a new Clientset for the given config. func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c ... var cs Clientset cs.appsV1beta1, err = appsv1beta1.NewForConfig(\u0026amp;configShallowCopy) ... cs.coreV1, err = corev1.NewForConfig(\u0026amp;configShallowCopy) ... } clientset的结构体 k8s.io/client-go/kubernetes/clientset.go\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // Clientset contains the clients for groups. Each group has exactly one // version included in a Clientset. type Clientset struct { *discovery.DiscoveryClient admissionregistrationV1 *admissionregistrationv1.AdmissionregistrationV1Client admissionregistrationV1beta1 *admissionregistrationv1beta1.AdmissionregistrationV1beta1Client internalV1alpha1 *internalv1alpha1.InternalV1alpha1Client appsV1 *appsv1.AppsV1Client appsV1beta1 *appsv1beta1.AppsV1beta1Client appsV1beta2 *appsv1beta2.AppsV1beta2Client authenticationV1 *authenticationv1.AuthenticationV1Client authenticationV1beta1 *authenticationv1beta1.AuthenticationV1beta1Client authorizationV1 *authorizationv1.AuthorizationV1Client authorizationV1beta1 *authorizationv1beta1.AuthorizationV1beta1Client autoscalingV1 *autoscalingv1.AutoscalingV1Client autoscalingV2beta1 *autoscalingv2beta1.AutoscalingV2beta1Client autoscalingV2beta2 *autoscalingv2beta2.AutoscalingV2beta2Client batchV1 *batchv1.BatchV1Client batchV1beta1 *batchv1beta1.BatchV1beta1Client certificatesV1 *certificatesv1.CertificatesV1Client certificatesV1beta1 *certificatesv1beta1.CertificatesV1beta1Client coordinationV1beta1 *coordinationv1beta1.CoordinationV1beta1Client coordinationV1 *coordinationv1.CoordinationV1Client coreV1 *corev1.CoreV1Client discoveryV1 *discoveryv1.DiscoveryV1Client discoveryV1beta1 *discoveryv1beta1.DiscoveryV1beta1Client eventsV1 *eventsv1.EventsV1Client eventsV1beta1 *eventsv1beta1.EventsV1beta1Client extensionsV1beta1 *extensionsv1beta1.ExtensionsV1beta1Client flowcontrolV1alpha1 *flowcontrolv1alpha1.FlowcontrolV1alpha1Client flowcontrolV1beta1 *flowcontrolv1beta1.FlowcontrolV1beta1Client networkingV1 *networkingv1.NetworkingV1Client networkingV1beta1 *networkingv1beta1.NetworkingV1beta1Client nodeV1 *nodev1.NodeV1Client nodeV1alpha1 *nodev1alpha1.NodeV1alpha1Client nodeV1beta1 *nodev1beta1.NodeV1beta1Client policyV1 *policyv1.PolicyV1Client policyV1beta1 *policyv1beta1.PolicyV1beta1Client rbacV1 *rbacv1.RbacV1Client rbacV1beta1 *rbacv1beta1.RbacV1beta1Client rbacV1alpha1 *rbacv1alpha1.RbacV1alpha1Client schedulingV1alpha1 *schedulingv1alpha1.SchedulingV1alpha1Client schedulingV1beta1 *schedulingv1beta1.SchedulingV1beta1Client schedulingV1 *schedulingv1.SchedulingV1Client storageV1beta1 *storagev1beta1.StorageV1beta1Client storageV1 *storagev1.StorageV1Client storageV1alpha1 *storagev1alpha1.StorageV1alpha1Client } clientset.Interface clientset实现了以下的interface，可以通过以下的方法获得具体的client，例如:\n1 pods, err := clientset.CoreV1().Pods(\u0026#34;\u0026#34;).List(metav1.ListOptions{}) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 type Interface interface { Discovery() discovery.DiscoveryInterface AdmissionregistrationV1() admissionregistrationv1.AdmissionregistrationV1Interface AdmissionregistrationV1beta1() admissionregistrationv1beta1.AdmissionregistrationV1beta1Interface InternalV1alpha1() internalv1alpha1.InternalV1alpha1Interface AppsV1() appsv1.AppsV1Interface AppsV1beta1() appsv1beta1.AppsV1beta1Interface AppsV1beta2() appsv1beta2.AppsV1beta2Interface AuthenticationV1() authenticationv1.AuthenticationV1Interface AuthenticationV1beta1() authenticationv1beta1.AuthenticationV1beta1Interface AuthorizationV1() authorizationv1.AuthorizationV1Interface AuthorizationV1beta1() authorizationv1beta1.AuthorizationV1beta1Interface AutoscalingV1() autoscalingv1.AutoscalingV1Interface AutoscalingV2beta1() autoscalingv2beta1.AutoscalingV2beta1Interface AutoscalingV2beta2() autoscalingv2beta2.AutoscalingV2beta2Interface BatchV1() batchv1.BatchV1Interface BatchV1beta1() batchv1beta1.BatchV1beta1Interface CertificatesV1() certificatesv1.CertificatesV1Interface CertificatesV1beta1() certificatesv1beta1.CertificatesV1beta1Interface CoordinationV1beta1() coordinationv1beta1.CoordinationV1beta1Interface CoordinationV1() coordinationv1.CoordinationV1Interface CoreV1() corev1.CoreV1Interface DiscoveryV1() discoveryv1.DiscoveryV1Interface DiscoveryV1beta1() discoveryv1beta1.DiscoveryV1beta1Interface EventsV1() eventsv1.EventsV1Interface EventsV1beta1() eventsv1beta1.EventsV1beta1Interface ExtensionsV1beta1() extensionsv1beta1.ExtensionsV1beta1Interface FlowcontrolV1alpha1() flowcontrolv1alpha1.FlowcontrolV1alpha1Interface FlowcontrolV1beta1() flowcontrolv1beta1.FlowcontrolV1beta1Interface NetworkingV1() networkingv1.NetworkingV1Interface NetworkingV1beta1() networkingv1beta1.NetworkingV1beta1Interface NodeV1() nodev1.NodeV1Interface NodeV1alpha1() nodev1alpha1.NodeV1alpha1Interface NodeV1beta1() nodev1beta1.NodeV1beta1Interface PolicyV1() policyv1.PolicyV1Interface PolicyV1beta1() policyv1beta1.PolicyV1beta1Interface RbacV1() rbacv1.RbacV1Interface RbacV1beta1() rbacv1beta1.RbacV1beta1Interface RbacV1alpha1() rbacv1alpha1.RbacV1alpha1Interface SchedulingV1alpha1() schedulingv1alpha1.SchedulingV1alpha1Interface SchedulingV1beta1() schedulingv1beta1.SchedulingV1beta1Interface SchedulingV1() schedulingv1.SchedulingV1Interface StorageV1beta1() storagev1beta1.StorageV1beta1Interface StorageV1() storagev1.StorageV1Interface StorageV1alpha1() storagev1alpha1.StorageV1alpha1Interface } CoreV1Client 常用的CoreV1Client为例子分析\ncorev1.NewForConfig 1 2 3 4 5 6 7 8 9 10 11 12 // NewForConfig creates a new CoreV1Client for the given config. func NewForConfig(c *rest.Config) (*CoreV1Client, error) { config := *c if err := setConfigDefaults(\u0026amp;config); err != nil { return nil, err } client, err := rest.RESTClientFor(\u0026amp;config) if err != nil { return nil, err } return \u0026amp;CoreV1Client{client}, nil } 通过传入配置信息rest.Config来实例化对象，其本质是调用了rest.RESTClientFor(\u0026amp;Config)方法创建了RESTClient的对象，即CoreV1Client的本质就是一个RESTClient对象。\nCoreV1Client结构体定义 1 2 3 4 // CoreV1Client is used to interact with features provided by the group. type CoreV1Client struct { restClient rest.Interface } CoreV1Client实现了CoreV1Interface接口，从而对k8s的资源对象进行增删改查的操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //CoreV1Client的方法 func (c *CoreV1Client) ComponentStatuses() ComponentStatusInterface {...} //ConfigMaps func (c *CoreV1Client) ConfigMaps(namespace string) ConfigMapInterface {...} //Endpoints func (c *CoreV1Client) Endpoints(namespace string) EndpointsInterface {...} func (c *CoreV1Client) Events(namespace string) EventInterface {...} func (c *CoreV1Client) LimitRanges(namespace string) LimitRangeInterface {...} //Namespaces func (c *CoreV1Client) Namespaces() NamespaceInterface {...} //Nodes func (c *CoreV1Client) Nodes() NodeInterface {...} func (c *CoreV1Client) PersistentVolumes() PersistentVolumeInterface {...} func (c *CoreV1Client) PersistentVolumeClaims(namespace string) PersistentVolumeClaimInterface {...} //Pods func (c *CoreV1Client) Pods(namespace string) PodInterface {...} func (c *CoreV1Client) PodTemplates(namespace string) PodTemplateInterface {...} //ReplicationControllers func (c *CoreV1Client) ReplicationControllers(namespace string) ReplicationControllerInterface {...} func (c *CoreV1Client) ResourceQuotas(namespace string) ResourceQuotaInterface {...} func (c *CoreV1Client) Secrets(namespace string) SecretInterface {...} //Services func (c *CoreV1Client) Services(namespace string) ServiceInterface {...} func (c *CoreV1Client) ServiceAccounts(namespace string) ServiceAccountInterface {...} CoreV1Interface 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type CoreV1Interface interface { RESTClient() rest.Interface ComponentStatusesGetter ConfigMapsGetter EndpointsGetter EventsGetter LimitRangesGetter NamespacesGetter NodesGetter PersistentVolumesGetter PersistentVolumeClaimsGetter PodsGetter PodTemplatesGetter ReplicationControllersGetter ResourceQuotasGetter SecretsGetter ServicesGetter ServiceAccountsGetter } CoreV1Interface中包含了各种k8s对象的调用接口，例如PodsGetter是对k8s中pod对象增删改查的接口。ServicesGetter是对service对象的操作的接口。\nPodsGetter接口举例探索 可以以PodsGetter接口为例来研究一下CoreV1Client对pod对象的增删改查调用。\n示例中的代码如下：\n1 pods, err := clientset.CoreV1().Pods(\u0026#34;\u0026#34;).List(metav1.ListOptions{}) CoreV1().Pods()\n1 2 3 4 5 6 7 8 9 10 11 12 13 // core_client.go func (c *CoreV1Client) Pods(namespace string) PodInterface { return newPods(c, namespace) } // pod.go // newPods returns a Pods func newPods(c *CoreV1Client, namespace string) *pods { return \u0026amp;pods{ client: c.RESTClient(), ns: namespace, } } CoreV1().Pods()方法实际上调用了newPods()方法，创建了一个pod对象，该对象继承了rest.Interface接口，即最终的实现本质是RESTClient的HTTP调用。\n1 2 3 4 5 // pods implements PodInterface type pods struct { client rest.Interface ns string } pods对象实现了PodInterface接口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // PodInterface has methods to work with Pod resources. type PodInterface interface { Create(*v1.Pod) (*v1.Pod, error) Update(*v1.Pod) (*v1.Pod, error) UpdateStatus(*v1.Pod) (*v1.Pod, error) Delete(name string, options *metav1.DeleteOptions) error DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error Get(name string, options metav1.GetOptions) (*v1.Pod, error) List(opts metav1.ListOptions) (*v1.PodList, error) Watch(opts metav1.ListOptions) (watch.Interface, error) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Pod, err error) GetEphemeralContainers(podName string, options metav1.GetOptions) (*v1.EphemeralContainers, error) UpdateEphemeralContainers(podName string, ephemeralContainers *v1.EphemeralContainers) (*v1.EphemeralContainers, error) PodExpansion 这个interface定义了pods对象的增删改查等方法。\nPodsGetter 继承了PodInterface的接口:\n1 2 3 4 5 // PodsGetter has a method to return a PodInterface. // A group\u0026#39;s client should implement this interface. type PodsGetter interface { Pods(namespace string) PodInterface } Pods().List()方法通过RESTClient的HTTP调用来实现对k8s的pod资源的获取:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // List takes label and field selectors, and returns the list of Pods that match those selectors. func (c *pods) List(opts metav1.ListOptions) (result *v1.PodList, err error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } result = \u0026amp;v1.PodList{} err = c.client.Get(). Namespace(c.ns). Resource(\u0026#34;pods\u0026#34;). VersionedParams(\u0026amp;opts, scheme.ParameterCodec). Timeout(timeout). Do(). Into(result) return } 以上分析了clientset.CoreV1().Pods(\u0026quot;\u0026quot;).List(metav1.ListOptions{})对pod资源获取的过程，最终是调用RESTClient的方法实现。\nRESTClient 待补充\n总结 client-go对K8s资源对象的调用，需要先获取k8s的配置信息，也就是$HOME/.kube/config。\n调用顺序如下：\nkubeconfig -\u0026gt; rest.config -\u0026gt; clientset -\u0026gt; 具体的client(CoreV1Client) -\u0026gt; 具体的资源对象(如pod) -\u0026gt; RESTClient -\u0026gt; http.Client -\u0026gt; HTTP 请求的发送和响应\n常用的client有CoreV1Client、AppsV1betaClient、ExtensionsV1beta1Client等。\nOperator SDK ","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/k8s-operator-dev/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文旨在记录对中间件、编排组件容器化部署后，实现kubernetes扩展组件Controller的过程。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"third-parties\"\u003eThird-Parties\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/kubernetes-client/javascript\"\u003ekubernetes-client: javascript\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/kubernetes/client-go\"\u003eclient-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/clux/kube-rs\"\u003ekube-rs\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"client-go源码分析\"\u003e\u003ccode\u003eclient-go\u003c/code\u003e源码分析\u003c/h2\u003e\n\u003ch3 id=\"目录结构\"\u003e目录结构\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ekubernetes\u003c/code\u003e: contains the clientset to access Kubernetes API.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ediscovery\u003c/code\u003e: discover APIs supported by a Kubernetes API server.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003edynamic\u003c/code\u003e: contains a dynamic client that can perform generic operations on arbitrary Kubernetes API objects.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etransport\u003c/code\u003e: set up auth and start a connection.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etools/cache\u003c/code\u003e: useful for writing controllers.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003einformers\u003c/code\u003e:  informer group\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003elisters\u003c/code\u003e:  lister group\u003c/li\u003e\n\u003c/ul\u003e\u003c/blockquote\u003e\n\u003ch3 id=\"代码实例\"\u003e代码实例\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://github.com/huweihuang/client-go.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e client-go\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e#保证本地HOME目录有配置kubernetes集群的配置文件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ego run client-go.go\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003ccode\u003eclient-go.go\u003c/code\u003e\u003c/p\u003e","title":"Kubernetes Operator Development History"},{"content":"Syllabus course syllabus\n","permalink":"https://pillumina.github.io/open_courses/cmu-15451/","summary":"\u003ch2 id=\"syllabus\"\u003eSyllabus\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"http://www.cs.cmu.edu/~15451-s21/schedule.html\"\u003ecourse syllabus\u003c/a\u003e\u003c/p\u003e","title":"Algorithms Design and Analysis"},{"content":"Syllabus course syllabus\n","permalink":"https://pillumina.github.io/open_courses/cmu-15210/","summary":"\u003ch2 id=\"syllabus\"\u003eSyllabus\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://www.cs.cmu.edu/~15210/syllabus.html\"\u003ecourse syllabus\u003c/a\u003e\u003c/p\u003e","title":"Parallel and Sequential Data Structures and Algorithms"},{"content":"注：如果对kubernetes的基本概念不太清楚，建议先过一下基本的资源类型再阅读此文\n先随便给个例子:\n1 2 3 4 5 6 7 8 9 10 apiVersion: v1 kind: ConfigMap metadata: name: test-config data: config.yml: |- start-message: \u0026#39;Hello, World!\u0026#39; log-level: INFO bootstrap.yml: listen-address: \u0026#39;127.0.0.1:8080\u0026#39; 我们定义了一个ConfigMap，data中定义了两个文件config.yml以及bootstrap.yml，当我们要引用当中的配置的时候，kubernetes提供了两种方案：\n使用configMapKeyRef引用configMap中某个文件的内容作为Pod中容器的环境变量。\n把所有configMap中的文件写到一个临时目录，将临时目录作为volume挂载到容器中，也就是configmap类型的volume。\n假设现在我们有一个Deployment，它的pod模板里引用了configMap，现在我们的目标是：当configmap更新的时候，这个Deployment的业务逻辑也能随之更新。那么有哪些方案？\n最好的情况是，当configMap发生变更时，直接进行hot update，做到不影响pod的正常运行。\n如果无法hot update或者这样完成不了需求，就要出发对应的Deployment做一次滚动更新。\n场景一： 针对可以进行热更新的容器，进行配置热更新 如果configMap由volume挂载，比如下述的投射卷，它的内容是可以更新的：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiVersion: v1 kind: Pod metadata: name: volume-test spec: containers: - name: container-test image: busybox volumeMounts: - name: all-in-one mountPath: \u0026#34;/projected-volume\u0026#34; readOnly: true volumes: - name: all-in-one projected: sources: - configMap: name: myconfigmap items: - key: config path: my-group/my-config 为了能够比较好得理解，先说明一下configMap的volume挂载机制：\n更新操作由kubelet的Pod Reconcile触发。每次Pod同步的时候（10s default），kubelet都会把Pod的所有configMapvolume标记为RequireRemount，而kubelet中的volume循环控制会发现这些需要重新挂载的volume，去执行一次挂载操作。\n在configMap的重挂载过程中，kubelet会先比较远端的configMap和volume中的configMap是否一致，然后再做更新。需要注意的是，拿到的远端configMap操作可能有cache，不一定是最新版本。\n所以这样的更新方式的确可行，但是会有更新延时，最多的延时时间：\nPod同步间隔(默认10s) + ConfigMap本地的缓存TTL\nkubelet 上 ConfigMap 的获取是否带缓存由配置中的ConfigMapAndSecretChangeDetectionStrategy 决定。\n注意，假如使用了 subPath 将 ConfigMap 中的某个文件单独挂载到其它目录下，那这个文件是无法热更新的（这是 ConfigMap 的挂载逻辑决定的）\n知道了原理，我们就明确一些概念：\n如果应用对configMap的更新有实时性要求，就需要在业务逻辑里自己到ApiServer去watch对应的configMap，或者干脆不用configMap而用etcd这样的一致性kv来存储管理配置。 加入没有实时性要求，那么configMap本身的更新逻辑就可以做到。 不过配置文件更新完了就不代表业务逻辑就更新了，我们还要解决如何通知应用重新读取配置，进行业务逻辑上的更新。例如对于nginx就需要一个SIGHUP信号量，这里再讨论几种做法。\n热更新一： 应用本身监听本地配置文件 这是最直接的方式，可以在应用里写监听的代码。一些配置相关的三方件本身就包装了这样的逻辑，比如viper\n热更新二：使用sidecar监听本地文件的变更 Prometheus的Helm Chart中使用的就是这种方式，找到一个实用的镜像configmap-reload，它就会去watch本地文件的变更，并在发生变更时通过HTTP调用通知应用进行热更新。\n这种方式就有一个问题：sidecar发送信号的限制比较多，而很多开源组件比如Fluentd，nginx都是依赖SIGHUP信号进行热更新的。在kubernetes 1.10之前，并不支持pod中的容器共享同一个pid namespace，所以sidecar也就无法向业务容器发送信号。在1.10以后，虽然支持了pid共享，但是在共享之后pid namespace中的1号进程就会变成基础的/pause进程，我们便无法轻松定位到目标进程了。\n所以，只要k8s版本在1.10以后，并且开启了ShareProcessNamespace特性，多写点代码，比如通过进程名去找到pid，总是有办法的。但是1.10之前是没可能的。\n热更新三：Fat Container 胖容器比较反模式，不过可以解决sidecar的一些限制，把主进程和sidecar进程打进一个镜像里，这样就绕过了pid namespace隔离的问题。但是如果条件允许，还是用上述两个方案，因为复杂是脆弱的根源，容器本身是轻量的。\n场景二： 无法热更新时，滚动更新Pod 无法热更新的场景举例有以下几个：\n应用本身没写热更新逻辑（大部分应用不会写）。 使用subPath进程configMap的挂载，导致configMap无法自动更新。 在环境变量或者init-container中依赖了configMap的内容。 第三点，就是使用configMapKeyRef引用configMap中的信息作为环境变量时，这个操作也只会在pod创建时执行一次，所以是不会自动更新的。\n当无法进行热更新的时候，我们必须滚动去更新Pod了。一个简单的想法就是写个controller去watchconfigMap的变更，有变更就给Deployment资源做滚动更新。但是这样的实现是更复杂的，我们首先需要考虑有没有更简单的方式。\n滚动更新一： 修改CI流程 这个方式很简单，只需要写一个CI脚本，给ConfigMap计算一个hash，然后作为一个环境变量或者annotation加入到Deployment的Pod模板中。\n举个🌰:\n1 2 3 4 5 6 7 ... spec: template: metadata: annotations: com.cctoctofx.configmap/hash: ${CONFIGMAP_HASH} ... 这样，如果configMap变化了，那么Deployment里的Pod模板自然会变化，k8s会自动帮我们做滚动更新。甚至如果configMap不复杂，直接转化为json放到pod模板里都行，而且还方便故障排查的时候快速知道内容是啥。\n滚动更新二：Controller 写个controller检测configMap变更并触发滚动更新，手动写之前还是看一看开源实现：\nReloader ConfigmapController k8s-trigger-controller 滚动更新三：Liveness Probe / Readiness Probe 这个手段需要深入一下，初步想法是用liveness调用一个脚本，脚本判断文件是否变动，如果变动，liveness得到false，重启pod，也可以同时设置readiness。\n滚动更新需要考虑的问题 举个例子，我们用场景二中提到的方式去更新：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: replicas: 3 template: annotations: nginx-config-md5: d41d8cd98f00b204e9800998ecf8427e spec: containers: - name: nginx image: nginx volumeMounts: - name: nginx-config mountPath: /etc/config volumes: - name: config-volume configMap: name: nginx-config --- apiVersion: v1 kind: ConfigMap metadata: name: nginx-config data: nginx.conf: |- ## some configurations... 每次部署的时候，计算configMap的MD5,填入pod的template中. 加入configMap发生变化，摘要也会变化，会触发一次Deployment的滚动更新。 这个流程看起来比较美，但思考一下如果我们更新了一个配置，但这个配置是有问题的，如果pod使用了错误的配置会无法工作（比如无法通过readinessProb检查）。最后，滚动更新的流程就会卡住，错误的配置不会把Deployment搞崩掉。\n这个逻辑看着也挺好，但是有个问题却忽视了，如果nginx-config更新成了错误的值，虽然还没有重建的Pod暂时是健康的，但是如果Pod挂掉发生重建，或者其中的容器重新读取了一次配置，那么这些Pod就会陷入异常。所以整个集群的状态是很不稳定的。\n因此问题的本质是：在原地更新configMap或者secret的时候，我们并没有进行滚动发布，而是一次性把新的配置更新到整个集群的所有实例当中。而我们所说的滚动更新就是控制各个实例读取新的配置的时机，可是由于我们无法把控Pod挂掉的时机，我们无法准确进行过程控制。\n解决方案 上述问题的问题在于原地更新，要解决这个问题，只需要在每次ConfigMap变化的时候，重新生成一个ConfigMap，再更新Deployment使用这个新的ConfigMap就行了。而重新生成ConfigMap最简单的方式就是在其命名中加上ConfigMap的data值计算出的摘要，比如：\n1 2 3 4 5 6 7 apiVersion: v1 kind: ConfigMap metadata: name: nginx-config-d41d8cd98f00b204e9800998ecf8427e data: nginx.conf: |- ## some configurations... ConfigMap的Rollout在社区中也是历经很久还没有解决(#22368)，目前为止，解决这个问题的方向也是immutable configmap模式。\n但是这种方案会有几个问题：\n如何做到每次配置文件更新时，都创建一个新的ConfigMap？ 目前社区的态度是把这一步放到Client解决，比如helm和kustomize。 历史configMap不断积累，能怎么回收？ 针对这点，社区希望在服务端实现一个GC机制来清理没有任何资源引用的configMap。 把更新逻辑放在client端虽然会有重复造轮子的问题，但是至少目前为止，configMap的新建和Deployment等对象的更新是最成熟的configMap滚动更新方案。\nKustomize的实践方式 Kustomize对这个方案有内置的支持，只需要使用configGenerator：\n1 2 3 4 configMapGenerator: - name: my-configmap files: - common.properties 这段yaml就能在kustomize中生成一个configMap对象，这个对象的data来自于common.properties文件，而且name中会加上这个文件的SHA值作为后缀。\n在kustomize的其他layer中，只要以my-configmap作为name引用这个configMap即可，当最终渲染的时候，kustomize会自动进行替换操作。\nHelm的实践方式 \u0026hellip;\n附录 facilitate ConfigMap rollouts/management discussion\n","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/k8s-config-update/","summary":"\u003cp\u003e\u003cem\u003e注：如果对kubernetes的基本概念不太清楚，建议先过一下基本的资源类型再阅读此文\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e先随便给个例子:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ev1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eConfigMap\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003etest-config\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003econfig.yml\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|-\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    start-message: \u0026#39;Hello, World!\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    log-level: INFO\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebootstrap.yml\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003elisten-address\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;127.0.0.1:8080\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e我们定义了一个\u003ccode\u003eConfigMap\u003c/code\u003e，data中定义了两个文件\u003ccode\u003econfig.yml\u003c/code\u003e以及\u003ccode\u003ebootstrap.yml\u003c/code\u003e，当我们要引用当中的配置的时候，\u003ccode\u003ekubernetes\u003c/code\u003e提供了两种方案：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e使用\u003ccode\u003econfigMapKeyRef\u003c/code\u003e引用\u003ccode\u003econfigMap\u003c/code\u003e中某个文件的内容作为Pod中容器的环境变量。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e把所有\u003ccode\u003econfigMap\u003c/code\u003e中的文件写到一个临时目录，将临时目录作为volume挂载到容器中，也就是\u003ccode\u003econfigmap\u003c/code\u003e类型的\u003ccode\u003evolume\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e假设现在我们有一个\u003ccode\u003eDeployment\u003c/code\u003e，它的pod模板里引用了\u003ccode\u003econfigMap\u003c/code\u003e，现在我们的目标是：\u003cstrong\u003e当\u003ccode\u003econfigmap\u003c/code\u003e更新的时候，这个\u003ccode\u003eDeployment\u003c/code\u003e的业务逻辑也能随之更新\u003c/strong\u003e。那么有哪些方案？\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e最好的情况是，当\u003ccode\u003econfigMap\u003c/code\u003e发生变更时，直接进行hot update，做到不影响pod的正常运行。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e如果无法hot update或者这样完成不了需求，就要出发对应的\u003ccode\u003eDeployment\u003c/code\u003e做一次滚动更新。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"场景一-针对可以进行热更新的容器进行配置热更新\"\u003e场景一： 针对可以进行热更新的容器，进行配置热更新\u003c/h2\u003e\n\u003cp\u003e如果\u003ccode\u003econfigMap\u003c/code\u003e由volume挂载，比如下述的投射卷，它的内容是可以更新的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ev1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ePod\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003evolume-test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003econtainers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003econtainer-test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ebusybox\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumeMounts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eall-in-one\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003emountPath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/projected-volume\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ereadOnly\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumes\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eall-in-one\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eprojected\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003esources\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003econfigMap\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003emyconfigmap\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e- \u003cspan class=\"nt\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003econfig\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e              \u003c/span\u003e\u003cspan class=\"nt\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003emy-group/my-config\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e为了能够比较好得理解，先说明一下\u003ccode\u003econfigMap\u003c/code\u003e的volume挂载机制：\u003c/p\u003e","title":"Kubernetes ConfigMap 热更新"},{"content":"资源模板 statefulset举例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: kubia spec: serviceName: kubia replicas: 2 template: metadata: labels: app: kubia spec: containers: - name: kubia image: derios/kubia ports: - name: http containerPort: 8080 volumeMounts: - name: data mountPath: /var/data volumeClaimTemplates: - metadata: name: data spec: resources: requests: storage: 1Mi accessModes: - ReadWriteOnce headless service举例 1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Service metadata: name: kubia spec: clusterIP: None selector: app: kubia ports: - name: http port: 80 storage class local PV 1 2 3 4 5 6 kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 apiVersion: apps/v1 kind: StatefulSet metadata: name: local-test spec: serviceName: \u0026#34;local-service\u0026#34; replicas: 3 selector: matchLabels: app: local-test template: metadata: labels: app: local-test spec: containers: - name: test-container image: k8s.gcr.io/busybox command: - \u0026#34;/bin/sh\u0026#34; args: - \u0026#34;-c\u0026#34; - \u0026#34;sleep 100000\u0026#34; volumeMounts: - name: local-vol mountPath: /usr/test-pod volumeClaimTemplates: - metadata: name: local-vol spec: accessModes: [ \u0026#34;ReadWriteOnce\u0026#34; ] storageClassName: \u0026#34;local-storage\u0026#34; resources: requests: storage: 368Gi Key Point 使用Local PV的实际场景 使用本地磁盘作为缓存的系统 CI/CD中用于存储构建中的系统 一些允许丢失和不需要保证可靠的数据(session, token) Local PV与HostPath的对比 hostpath:\n绑定在pod的生命周期上,pod结束,pv则被删除. 可以通过pvc引用,也可以直接使用pv 使用node的磁盘,不经过网络,开销非常小 本地持久卷:\n生命周期和node绑定,Kubernetes调度程序始终确保使用本地永久卷的Pod安排到同一节点 无法通过storageclass动态创建. 使用node磁盘,不经过网络,开销非常小. The biggest difference is that the Kubernetes scheduler understands which node a Local Persistent Volume belongs to. With HostPath volumes, a pod referencing a HostPath volume may be moved by the scheduler to a different node resulting in data loss. But with Local Persistent Volumes, the Kubernetes scheduler ensures that a pod using a Local Persistent Volume is always scheduled to the same node. 使用Local PV的几个注意的问题 本地持久卷依然会丢失数据,例如node本身出了问题. 本地持久卷需要提供volumeBindingMode:WaitForFirstConsumer支持 不支持动态卷配置,实际上需要一个外部的controller来控制,包括创建pv和销毁pv并清理磁盘 使用Local PV例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 apiVersion: v1 kind: PersistentVolume metadata: name: example-pv spec: capacity: storage: 2Gi volumeMode: Filesystem accessModes:\t- ReadWriteMany persistentVolumeReclaimPolicy: Delete storageClassName: local-storage local: path: /home/tangxu/localpv # 比普通的pv就多了这个亲和性调度, 也是必须加的 nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - tangxu-pc --- kind: Service apiVersion: v1 metadata: name: local-pv-service spec: selector: app: local-test clusterIP: None ports: - port: 8090 targetPort: 80 protocol: tcp --- apiVersion: apps/v1 kind: StatefulSet metadata: name: local-test spec: serviceName: \u0026#34;local-pv-service\u0026#34; replicas: 3 selector: matchLabels: app: local-test template: metadata: labels: app: local-test spec: containers: - name: test-container image: nginx:latest ports: - containerPort: 80 protocol: tcp name: http volumeMounts: - name: local-vol mountPath: /usr/test-pod volumeClaimTemplates: - metadata: name: local-vol spec: accessModes: - \u0026#34;ReadWriteMany\u0026#34; storageClassName: \u0026#34;local-storage\u0026#34; resources: requests: storage: 1Gi 1 2 3 $ kubectl create ns test namespace/test created $ kubectl apply -f localPV.yaml Local PV的清理 在使用了local pv之后,清理就不再是简单的使用命令删除了,因为kubernetes不会为我们管理local pv的创建和删除工作(并且删除是阻塞的,主要依靠finalizer机制)\n1 2 3 4 5 6 #先删除使用pv的资源 $ kubectl delete -f localPV.yaml persistentvolume \u0026#34;example-pv\u0026#34; deleted service \u0026#34;local-pv-service\u0026#34; deleted statefulset.apps \u0026#34;local-test\u0026#34; deleted #此处应该阻塞... 再起一个终端:\n1 $ kubectl patch pv example-pv -p \u0026#39;{\u0026#34;metadata\u0026#34;:{\u0026#34;finalizers\u0026#34;: []}}\u0026#39; --type=merge k8s服务的概念 相同命名空间的可以用service名称作为主机名访问，可以查看/etc/resolve.conf\n业务的数据库连接，如何规划暴露的服务，db-proxy是否需要刷新endpoints？\n需要为pod添加就绪探针，让客户端只与正常的pod交互，而不管后端是否有pod出现问题，这样在就绪探针出问题了，Endpoints资源会去掉这个pod。\n1 2 3 4 5 6 7 8 9 10 ... spec: containers: - name: kubia image: luksa/kubia readinessProbe: exec: command: - ls - /var/ready 应该通过删除pod或者更改pod标签而不是手动更改探针来从服务中手动移除pod\n如果想要从某个服务中手动添加或者删除pod的时候，把enabled=true作为标签添加到pod，以及服务的标签选择器中。当想要从服务中移除pod中，删除标签即可。 数据库详细配置 安装数据库时，数据库之间的路由配置从哪里来？\n业务访问数据库时的网络管道\u0026hellip;\n数据库启停，pod的增删，以及前端服务显示形式。CRD中要定义描述数据库停止的状态信息，此时数据库存储和网络标识还在。\n数据库agent检测的状态，如何同步给CR？\nOperator主从状态确认行为\nunreachableTimeout \u0026mdash; pollingInterval\n主备HA，是否由controller控制？\noracle times ten database是放在operator进行HA操作 数据库软件包的下载时机和形式？\n以挂卷方式将进程在容器内启动的可行性？\ndatabase metadata的定义形式？\n由投射卷管理所有配置\nadmin信息，密码信息\nuser信息，密码信息\nTLS配置\n数据库的端口问题？\n数据库集群部署state:\n主从\n- Initializing - Normal - ActiveDown - StandbyDown - StandbyPartiallyDown - StanbyPartiallyStarting - BothDown - Failed 单机\n- Intializing - Normal - ActiveDown - Failed 数据库修改\n以修改连接数为例（方案一：采用configmap保存实例的连接配置信息、db信息等） 修改configmap中的字段 operator删除stanby pod，并重新创建，该创建的standby pod采用新的configmap字段创建 standby pod正常后，删除active pod进行自动倒换，operator再原地创建新的stanby pod，感知configmap中连接数字段创建。 资源声明 operator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # operator deployment apiVersion: apps/v1 kind: Deployment metadata: name: zenith-operator spec: replicas: 1 selector: matchLabels: name: zenith-operator template: metadata: labels: name: zenith-operator spec: serviceAccountName: zenith-operator # imagePullSecrets: # - name: zenith-image-pulling-secret packageVesion: v1.0.0 softwarePackage: zenith-operator packageType: tar containers: - name: zenith-operator # image: .... command: - zenith-operator imagePullPolicy: Never env: - name: WATCH_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: OPERATOR_NAME value: \u0026#34;zenith-operator\u0026#34; configmap /secrets DB Object 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 apiVersion: huawei.cloudb.com/v1 kind: zenithCluster metadata: annotations: {} labels: {} name: ${zenith_cluster_name} namespace: ${cluster_namespace} spec: zenithSpec: replicas: 2 # master-slave version: v1.0.0 # eg.: Zenith-1.0.0.tar packageName: Zenith # --- | packageType: tar # --- | storageClassName: local # PV storageSize: 5G # PV replicationSSLMandatory: false pollingInterval: 10 unreachableTimeout: 30 instanceConfigMap: - zenith-instance-config dbConfigMap: - zenith-db-config dbSecrets: - zenith-db-secret dbSpecs: ... agentSpec: version: v1.0.0 packageName: DBAgent packageType: tar agentConfig: - db-agent-sample-config template: affinity: ... spec: selectors: matchLables: ... initContainers: - name: notify-download-package image: k8s.gcr.io/busybox commands: - sh - \u0026#34;-c\u0026#34; - | /bin/bash \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; This is used for notify nodeagent to download zenith software package EOF containers: - name: zenith-ha resources: requests: memory: \u0026#34;2048Mi\u0026#34; cpu: \u0026#34;1000m\u0026#34; limits: memory: \u0026#34;4096Mi\u0026#34; cpu: \u0026#34;2000m\u0026#34; ports: - name: listen-port containerPort: 32080 - name: replication-port containerPort: 12345 volumeMounts: - name: zenith-certs mountPath: /etc/certificate readOnly: true - name: watch-uds mountPath: /etc/uds/watch-server-uds readOnly: false - name: zenith-server-uds mountPath: /etc/uds/zenith-server-uds readOnly: false - name: agent-config mountPath: /etc/DBAgent/dbagent.conf readOnly: true env: - name: POD_NAME valueFrom: ... ... volumes: - name: volumeClaimTemplates: # PVC ... 用投射卷聚合配置信息 proposal\nConstraints and Assumptions 1. The volume types must remain unchanged for backward compatibility 2. There will be a new volume type for this proposed functionality, but no other API changes 3. The new volume type should support atomic updates in the event of an input change Use Cases 1. As a user, I want to automatically populate a single volume with the keys from multiple secrets, configmaps, and with downward API information, so that I can synthesize a single directory with various sources of information 2. As a user, I want to populate a single volume with the keys from multiple secrets, configmaps, and with downward API information, explicitly specifying paths for each item, so that I can have full control over the contents of that volume 以前的情况 要使用secrets, configmaps, downward APIs都要在volumeMounts里面声明不同的mount paths:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 apiVersion: v1 kind: Pod metadata: name: volume-test spec: containers: - name: container-test image: busybox volumeMounts: - name: mysecret mountPath: \u0026#34;/secrets\u0026#34; readOnly: true - name: podInfo mountPath: \u0026#34;/podinfo\u0026#34; readOnly: true - name: config-volume mountPath: \u0026#34;/config\u0026#34; readOnly: true volumes: - name: mysecret secret: secretName: jpeeler-db-secret items: - key: username path: my-group/my-username - name: podInfo downwardAPI: items: - path: \u0026#34;labels\u0026#34; fieldRef: fieldPath: metadata.labels - path: \u0026#34;annotations\u0026#34; fieldRef: fieldPath: metadata.annotations - name: config-volume configMap: name: special-config items: - key: special.how path: path/to/special-key 投射卷的情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 apiVersion: v1 kind: Pod metadata: name: volume-test spec: containers: - name: container-test image: busybox volumeMounts: - name: all-in-one mountPath: \u0026#34;/projected-volume\u0026#34; readOnly: true volumes: - name: all-in-one projected: sources: - secret: name: mysecret items: - key: user path: my-group/my-username - downwardAPI: items: - path: \u0026#34;labels\u0026#34; fieldRef: fieldPath: metadata.labels - path: \u0026#34;cpu_limit\u0026#34; resourceFieldRef: containerName: container-test resource: limits.cpu - configMap: name: myconfigmap items: - key: config path: my-group/my-config 简单部署TiDB Operator以及集群 crd安装 kubectl apply -f https://raw.githubusercontent.com/pingcap/tidb-operator/v1.1.12/manifests/crd.yaml\n安装operator helm repo add pingcap https://charts.pingcap.org/ kubectl create namespace tidb-admin helm install --namespace tidb-admin tidb-operator pingcap/tidb-operator --version v1.1.12 kubectl get pods --namespace tidb-admin -l app.kubernetes.io/instance=tidb-operator 部署TiDB集群和监控 kubectl create namespace tidb-cluster \u0026amp;\u0026amp; \\ kubectl -n tidb-cluster apply -f https://raw.githubusercontent.com/pingcap/tidb-operator/master/examples/basic/tidb-cluster.yaml kubectl -n tidb-cluster apply -f https://raw.githubusercontent.com/pingcap/tidb-operator/master/examples/basic/tidb-monitor.yaml ","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/k8s-template/","summary":"\u003ch2 id=\"资源模板\"\u003e资源模板\u003c/h2\u003e\n\u003ch3 id=\"statefulset举例\"\u003e\u003ccode\u003estatefulset\u003c/code\u003e举例\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eapps/v1beta1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eStatefulSet\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eserviceName\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ereplicas\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003elabels\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003econtainers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ederios/kubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eports\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ehttp\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003econtainerPort\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e8080\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumeMounts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edata\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003emountPath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/var/data\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e   \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumeClaimTemplates\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e   \u003c/span\u003e- \u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edata\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003eresources\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e         \u003c/span\u003e\u003cspan class=\"nt\"\u003erequests\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e           \u003c/span\u003e\u003cspan class=\"nt\"\u003estorage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e1Mi\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nt\"\u003eaccessModes\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e- \u003cspan class=\"l\"\u003eReadWriteOnce\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e       \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"headless-service举例\"\u003e\u003ccode\u003eheadless service\u003c/code\u003e举例\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ev1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eService\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eclusterIP\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eNone\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eselector\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubia\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eports\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ehttp\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eport\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e80\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"storage-class-local-pv\"\u003e\u003ccode\u003estorage class\u003c/code\u003e local PV\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eStorageClass\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003estorage.k8s.io/v1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elocal-storage\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003eprovisioner\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ekubernetes.io/no-provisioner\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003evolumeBindingMode\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eWaitForFirstConsumer\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eapiVersion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eapps/v1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003ekind\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eStatefulSet\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elocal-test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eserviceName\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;local-service\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ereplicas\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eselector\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ematchLabels\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elocal-test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003elabels\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elocal-test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003econtainers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003etest-container\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ek8s.gcr.io/busybox\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;/bin/sh\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-c\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;sleep 100000\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumeMounts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elocal-vol\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003emountPath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/usr/test-pod\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumeClaimTemplates\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003emetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003elocal-vol\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003espec\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eaccessModes\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;ReadWriteOnce\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003estorageClassName\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;local-storage\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003eresources\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nt\"\u003erequests\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"nt\"\u003estorage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e368Gi\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"key-point\"\u003eKey Point\u003c/h2\u003e\n\u003ch3 id=\"使用local-pv的实际场景\"\u003e使用Local PV的实际场景\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e使用本地磁盘作为缓存的系统\u003c/li\u003e\n\u003cli\u003eCI/CD中用于存储构建中的系统\u003c/li\u003e\n\u003cli\u003e一些允许丢失和不需要保证可靠的数据(session, token)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"local-pv与hostpath的对比\"\u003eLocal PV与HostPath的对比\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003ehostpath\u003c/code\u003e:\u003c/p\u003e","title":"Kubernetes Developement"},{"content":"内容涵盖 使用节点污点和pod容忍度阻止pod调度到特定节点 将节点亲缘性规则作为节点选择器的一种替代 使用节点亲缘性进行多个pod的共同调度 使用节点非亲缘性来分离多个pod 高级调度 在pod介绍的文章中可以看到，k8s可以通过在pod spec里面指定节点选择器，而这篇文章介绍的是后面其他逐渐加入的机制。\n使用污点和容忍度阻止节点调度到特定节点 新特性： 节点污点、pod对于污点的容忍度\n这些特性用于限制哪些pod可以被调度到某一个节点，也就是说只有当一个pod容忍某个节点的污点，这个pod才能被调度到该节点。\n节点选择器和节点亲缘性规则，是明确在pod中添加的信息，来觉得一个pod可以或者不可以被调度到某个节点。而污点不一样，是在不修改已有pod信息的前提下，通过在节点上新增污点信息，来拒绝pod在这个节点的部署。\n简单介绍污点和容忍度 我在自己的机器用minikube创建了k8s单点集群，用kubectl describe node minikube可以看到:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 k describe node minikube Name: minikube Roles: master Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux deploy=test kubernetes.io/arch=amd64 kubernetes.io/hostname=minikube kubernetes.io/os=linux minikube.k8s.io/commit=b09ee50ec047410326a85435f4d99026f9c4f5c4 minikube.k8s.io/name=minikube minikube.k8s.io/updated_at=2021_03_30T20_15_58_0700 minikube.k8s.io/version=v1.14.0 node-role.kubernetes.io/master= Annotations: kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock node.alpha.kubernetes.io/ttl: 0 volumes.kubernetes.io/controller-managed-attach-detach: true CreationTimestamp: Tue, 30 Mar 2021 20:15:55 +0800 Taints: \u0026lt;none\u0026gt; # -----\u0026gt; 主节点暂时没有污点 Unschedulable: false Lease: HolderIdentity: minikube AcquireTime: \u0026lt;unset\u0026gt; RenewTime: Fri, 09 Apr 2021 14:48:12 +0800 可以看到Taints属性，表示目前这个主节点没有污点。不过这里可以举个例子：\nTaints: node-role.kubernetes.io/master:NoSchedule 污点包含了一个key, value以及一个effect\u0026ndash;\u0026gt; \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt;:\u0026lt;effect\u0026gt;。上面这个例子里，key是node-role.kubernetes.io/master，空的value，effect是NoSchedule。\n这个污点能阻止pod调度到这个节点上，除非有pod能够容忍这个污点，而通过容忍这个污点的pod都是系统级别的pod。\nToleration: node-role.kubernetes.io/master:NoSchedule 如果pod包含容忍度能匹配节点的污点，那么就可能被调度到这个节点上。\n显示pod的污点容忍度 1 2 3 4 5 6 huangyuxiao@CctoctoFX /usr/local/var/log $ k describe po nginx-r4hdr Name: nginx-r4hdr ... # 省略中间其他属性 Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s ... 可以看到，我describe了一个在集群中的pod。\n污点的效果 在上述的例子中，可以看到对于这个nginx的pod，其定义了当节点状态是not-ready或者unreachable的时候，这个pod允许运行在这个节点300秒。这两个容忍度使用的是NoExecute而不是NoSchedule。\n每个污点可以关联一个效果，包含如下三种:\nNoSchedule：如果pod没有容忍这些污点，pod则不能被调度到包含这些污点的节点上 PreferNoSchedule： 一个比较loose的NoSchedule，表示尽量阻止调度到这里。但是如果实在没其他地方能调度了，还是可以调度到这边的。 NoExecute：这个和上述两者不同，前两种只是影响调度。而NoExecute也会影响在节点上运行着的pod。如果在某个节点上添加了NoExecute，如果节点上运行着的pod没有容忍这个污点，就会被从这个节点删除。 在节点上添加Custom污点 一个很简单的诉求：一个k8s集群上面同时有生产环境和非生产环境的流量。最重要的一点是，非生产环境的pod不能运行在生产环境的节点上，就可以在生产环境的节点上添加污点来满足要求:\n1 $ kubectl taint node node.k8s node-type=production:NoSchedule 这里新增了一个污点，key是node-type，value是production，效果是NoSchedule。所以这个时候你再去部署常规的pod，是不会部署到添加了这些污点的节点上去的。\n往pod新增污点容忍度 还是上面的诉求，现在为了把生产环境的pod部署到生产环境节点上，pod需要容忍刚才我们添加的污点，那么我们修改下pod的资源yaml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: extensions/v1beta1 kind: Deployment metadata: name: pod spec: replicas: 5 template: spec: ... tolerations: - key: node-type operator: Equal value: production effect: NoSchedule 新增tolerations描述即可 官方文档\n如果又不想把这个pod调度到非生产环境，则需要类似在非生产环境打上污点\n污点和容忍度的使用场景 污点可以只有一个key和一个effect，而不必有value。容忍度可以通过设置Equal operator来指定匹配的value（default）。或者可以设置Exists operator来匹配污点的key。\n调度的时候使用污点和容忍度 就如从最开始介绍的这样，用NoSchedule或者定义非优先调度节点PreferNoSchedule，或者把已有的pod从当前节点删除。\n比如可以把一个集群分为几个部分，部分节点可能提供了特殊影响比如GPU，TPU之类的，而且只有部分pod需要使用到这些硬件的时候，也可以通过污点和容忍度实现。\n节点unreachable之后pod重新调度的等待时长设置 和前面的例子一样:\n1 2 3 4 5 6 huangyuxiao@CctoctoFX /usr/local/var/log $ k describe po nginx-r4hdr Name: nginx-r4hdr ... # 省略中间其他属性 Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s ... 1 2 3 4 5 6 7 8 9 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 当k8s的controller检测到有节点处于not-ready或者unreachable状态的时候，会等待300秒，如果状态持续，才把pod调度到其他节点上。这两个容忍度是你没有配的时候自动加给pod的，如果觉得300太长了也可以显式得去改变。\n使用节点亲缘性将pod调度到特定节点 节点亲缘性(node affinity)：允许pod尽量调度到某些节点子集。\n早期的k8s中，初始的节点亲缘性机制就是pod描述中的nodeSelector字段。节点必须包含所有pod对应字段中的指定label，才能成为调度的目标节点。\n节点选择器很简单，但是不能满足所有需求，所以更强大的亲缘性机制才会被引入。\n和节点选择器类似，每个pod可以定义自己的节点亲缘性规则，这些规则可以允许你指定硬件限制或者偏好。当你指定一种偏好后，k8s会把pod尽量调度到这些节点上面，如果没法实现，则调度到其他节点。\n如果使用谷歌的k8s引擎(GKE)，可以kubectl describe node xxxx查到节点Labels，这里面包含了默认的和亲缘性有关的标签。\n指定强制性节点亲缘性规则 在介绍Pod的文章中，我们利用节点选择器将那些需要GPU的pod只被调度到有GPU的节点上:\n1 2 3 4 5 6 7 8 apiVesion: v1 kind: Pod metadata: name: kubia-gpu spec: nodeSelector: gpu: \u0026#34;true\u0026#34; ... 这样这个pod会被调度到包含gpu=true标签的节点。如果我们用节点亲缘性规则去替换：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVesion: v1 kind: Pod metadata: name: kubia-gpu spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoreDuringExecption: nodeSelectorTerms: - matchExpressions: - key: gpu operator: In values: - \u0026#34;true\u0026#34; 这种写法貌似比上面的复杂了很多，但是表达能力更强了。\n较长的节点亲缘性属性名的意义 上面的写法里，spec里有affinity，affinity里有nodeAffinity，下面还有个非常长的名字，我们拆解一下：\nrequiredDuringScheduling...：说明该字段下定义的规则，为了让pod能调度到该节点上，明确指出了这个节点必须含有的标签。\n...IgnoreDuringException: 表明该字段下定义的规则，不会影响已经在节点上运行的pod。\n所以这个含义就是：当前的亲缘性规则只会影响正在被调度的pod, 而不会导致正在运行的pod被删除。所以一般来说目前的规则都是以IgnoredDuringException作为结尾。\n注：RequiredDuringException就表示如果去掉某节点上的标签，那么含有这些标签的pod会被删除，这个特性目前的k8s应该还没有。\n了解节点选择器的条件 1 2 3 4 5 6 nodeSelectorTerms: - matchExpressions: - key: gpu operator: In values: - \u0026#34;true\u0026#34; 现在这几个字段应该比较好了解。也就是表示这个pod只会被调度到gpu=true的节点上。\n更有趣的是，节点亲缘性可以在调度的时候指定节点的优先级。\n调度pod时的节点优先级 preferredDuringSchedulingIgnoredDuringException 来实现优先考虑哪些节点。\n思考一个场景：你有一个跨越多个国家的多个数据中心，每个数据中心代表了一个单独的可用性区域。在每个区域中，你有一些特定的机器，只提供给你自己或者合作的公司使用。现在你想部署一些pod，希望吧pod优先部署在区域zone1，并且是为你公司部署预留的机器上。如果你的机器没有足够的空间给这些pod使用，或者处于其他的一些原因不希望这些pod被调度到这些机器上，那么就要调度到其他区域的其他机器上面，这种情况你也是可以接受的。那么节点亲缘性就可以实现这样的功能。\n给节点加标签 每个节点需要包含两个标签:\n表示所在的这个节点所归属的可用性区域 表示这是一个独占的节点还是共享的节点 1 2 3 4 $ kubectl label node node1.k8s availability-zone=zone1 $ kubectl label node node1.k8s share-type=dedicated $ kubectl label node node2.k8s availability-zone=zone2 $ kubectl label node node2.k8s share-type=shared 指定优先级节点亲缘性规则 把节点的标签打好以后，创建一个Deployment，其中优先选择zone1中的的dedicated节点，下面是描述:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 apiVesion: extension/v1beta1 kind: Deployment metadata: name: pref spec: template: ... spec: affinity: nodeAffinity: preferredDuringSchedualingIgnoredDuringException: - weight: 80 preference: matchExpressions: - key: availability-zone operator: In values: - zone1 - wight: 20 preference: matchExpressions: - key: share-type operator: In values: - dedicated 可见，节点优先调度到zone1，这是最重要的偏好; 同时优先调度pod到独占(dedicated)节点，但是这个优先级是zone优先级的1/4。\n节点优先级是如何工作的 核心是把节点根据标签分组，然后排序。\n比如把包含availability-zone以及share-type标签，并且匹配pod亲缘性的节点(zone1, dedicated)排在前面。然后，根据pod设置的亲缘性规则的权重，接下来是zone1和shared节点，然后是其他区域的dedicated节点，优先级最低的是其他的节点。\n使用pod亲缘性和非亲缘性对pod进行协同部署 上面我们了解了pod和节点间的亲缘性规则能够影响pod能够调度到哪个节点。我们再来研究研究如何制定pod自身之间的亲缘性。\n想象一下：如果你有一个前端的pod和一个后端pod，把这些节点部署得比较靠近，可以降低延时，提高应用的性能。可以使用节点亲缘性规则来确保这两个pod被调度到同一个节点、同一个rack、同一个数据中心。但是这样后续又要指定调度到确切的位置，明显是违背k8s设计哲学的。所以更好的做法应该是定义pod之间的亲缘性规则，让k8s去把pod部署在它觉得合适的地方，同时确保2个pod是靠近的。\n使用pod间亲缘关系将多个pod部署在同一个节点 \u0026hellip;\n","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/k8s-advance-schedule/","summary":"\u003ch2 id=\"内容涵盖\"\u003e内容涵盖\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e使用节点污点和pod容忍度阻止pod调度到特定节点\u003c/li\u003e\n\u003cli\u003e将节点亲缘性规则作为节点选择器的一种替代\u003c/li\u003e\n\u003cli\u003e使用节点亲缘性进行多个pod的共同调度\u003c/li\u003e\n\u003cli\u003e使用节点非亲缘性来分离多个pod\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"高级调度\"\u003e高级调度\u003c/h2\u003e\n\u003cp\u003e在pod介绍的文章中可以看到，k8s可以通过在pod spec里面指定节点选择器，而这篇文章介绍的是后面其他逐渐加入的机制。\u003c/p\u003e\n\u003ch3 id=\"使用污点和容忍度阻止节点调度到特定节点\"\u003e使用污点和容忍度阻止节点调度到特定节点\u003c/h3\u003e\n\u003cp\u003e新特性： \u003ccode\u003e节点污点\u003c/code\u003e、\u003ccode\u003epod对于污点的容忍度\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e这些特性用于限制哪些pod可以被调度到某一个节点，也就是说只有当一个pod容忍某个节点的污点，这个pod才能被调度到该节点。\u003c/p\u003e\n\u003cp\u003e节点选择器和节点亲缘性规则，是\u003ccode\u003e明确\u003c/code\u003e在pod中添加的信息，来觉得一个pod可以或者不可以被调度到某个节点。而污点不一样，是在不修改已有pod信息的前提下，通过在节点上新增污点信息，来拒绝pod在这个节点的部署。\u003c/p\u003e\n\u003ch4 id=\"简单介绍污点和容忍度\"\u003e简单介绍污点和容忍度\u003c/h4\u003e\n\u003cp\u003e我在自己的机器用minikube创建了k8s单点集群，用\u003ccode\u003ekubectl describe node minikube\u003c/code\u003e可以看到:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ek describe node minikube\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eName:               minikube\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRoles:              master\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eLabels:             beta.kubernetes.io/arch\u003cspan class=\"o\"\u003e=\u003c/span\u003eamd64\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    beta.kubernetes.io/os\u003cspan class=\"o\"\u003e=\u003c/span\u003elinux\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"nv\"\u003edeploy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nb\"\u003etest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    kubernetes.io/arch\u003cspan class=\"o\"\u003e=\u003c/span\u003eamd64\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    kubernetes.io/hostname\u003cspan class=\"o\"\u003e=\u003c/span\u003eminikube\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    kubernetes.io/os\u003cspan class=\"o\"\u003e=\u003c/span\u003elinux\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    minikube.k8s.io/commit\u003cspan class=\"o\"\u003e=\u003c/span\u003eb09ee50ec047410326a85435f4d99026f9c4f5c4\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    minikube.k8s.io/name\u003cspan class=\"o\"\u003e=\u003c/span\u003eminikube\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    minikube.k8s.io/updated_at\u003cspan class=\"o\"\u003e=\u003c/span\u003e2021_03_30T20_15_58_0700\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    minikube.k8s.io/version\u003cspan class=\"o\"\u003e=\u003c/span\u003ev1.14.0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    node-role.kubernetes.io/master\u003cspan class=\"o\"\u003e=\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAnnotations:        kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    node.alpha.kubernetes.io/ttl: \u003cspan class=\"m\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    volumes.kubernetes.io/controller-managed-attach-detach: \u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCreationTimestamp:  Tue, \u003cspan class=\"m\"\u003e30\u003c/span\u003e Mar \u003cspan class=\"m\"\u003e2021\u003c/span\u003e 20:15:55 +0800\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eTaints:             \u0026lt;none\u0026gt;                  \u003cspan class=\"c1\"\u003e# -----\u0026gt; 主节点暂时没有污点\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUnschedulable:      \u003cspan class=\"nb\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eLease:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  HolderIdentity:  minikube\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  AcquireTime:     \u0026lt;unset\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  RenewTime:       Fri, \u003cspan class=\"m\"\u003e09\u003c/span\u003e Apr \u003cspan class=\"m\"\u003e2021\u003c/span\u003e 14:48:12 +0800\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e可以看到\u003ccode\u003eTaints\u003c/code\u003e属性，表示目前这个主节点没有污点。不过这里可以举个例子：\u003c/p\u003e","title":"Kubernetes Handbook (Schedule)"},{"content":"参考 Julia Evans: Profiling Go programs with pprof\nHow I investigated memory leaks in Go using pprof on a large codebase\nMemory Profiling a Go Service\nRuss Cox: Profling Go Programs\nPackage pprof overview\ngithub: pprof\nIssue: Why \u0026lsquo;Total MB\u0026rsquo; in golang heap profile is less than \u0026lsquo;RES\u0026rsquo; in top?\nIssue: Cannot free memory once occupied by bytes.Buffer\nIssue: FreeOSMemory() in production\nIssue: Is this an idiomatic worker thread pool in Go?\n","permalink":"https://pillumina.github.io/posts/programming/golang/go-profiling/","summary":"\u003ch2 id=\"参考\"\u003e参考\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/\"\u003eJulia Evans: Profiling Go programs with pprof\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.freecodecamp.org/news/how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase-4bec4325e192/\"\u003eHow I investigated memory leaks in Go using pprof on a large codebase\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://medium.com/compass-true-north/memory-profiling-a-go-service-cd62b90619f9\"\u003eMemory Profiling a Go Service\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://blog.golang.org/pprof\"\u003eRuss Cox: Profling Go Programs\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://golang.org/pkg/net/http/pprof/\"\u003ePackage pprof overview\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/google/pprof\"\u003egithub: pprof\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://stackoverflow.com/questions/16516189/why-total-mb-in-golang-heap-profile-is-less-than-res-in-top\"\u003eIssue: Why \u0026lsquo;Total MB\u0026rsquo; in golang heap profile is less than \u0026lsquo;RES\u0026rsquo; in top?\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://stackoverflow.com/questions/37382600/cannot-free-memory-once-occupied-by-bytes-buffer\"\u003eIssue: Cannot free memory once occupied by bytes.Buffer\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://stackoverflow.com/questions/42345060/freeosmemory-in-production\"\u003eIssue: FreeOSMemory() in production\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://stackoverflow.com/questions/38170852/is-this-an-idiomatic-worker-thread-pool-in-go\"\u003eIssue: Is this an idiomatic worker thread pool in Go?\u003c/a\u003e\u003c/p\u003e","title":"Profiling a Go Service in Production"},{"content":"AUFS是一种Union File System，所谓的UnionFS实际上就是把不同物理位置的目录合并mount到同一个目录当中。一种典型的UnionFS的应用，就是把一张CD/DVD和一个硬盘目录联合mount在一起，然后你就可以对这个只读的CD/DVD上的文件进行修改。\nAUFS又叫做Another UnionFS，后面改成Alternative UnionFS，然后又变成Advance UnionFS\u0026hellip;..当然名字的改变叫啥不重要，本质还是没变的。2006年Junjiro Okajima开发了AUFS，完全重写了早期的UnionFS 1.X，主要目的是为了可靠性和性能，再引入一些新的功能，例如可写分支的负载均衡。不过很有意思的是，AUFS的性能比UnionFS 1.X好很多，后面UnionFS 2.x就抄AUFS的功能，而AUFS本身却没有合入到Linux主线，因为代码量太大质量也不好。虽然后面Junjiro不断提升代码质量，不断提交但是还是被Linus拒绝了。所以哪怕是今天AUFS也没进到Linux里，虽然质量已经可以了。\n不过一些发行版比如：Ubuntu 10.04，Debian6.0都支持AUFS，所以也还好。我在Ubuntu 14.04演示一下例子。\n首先，我们建立两个水果和蔬菜的目录，在这个目录上放一些文件，水果里有苹果和番茄，蔬菜有胡萝卜和番茄:\n1 2 3 4 5 6 7 8 $ tree . ├── fruits │ ├── apple │ └── tomato └── vegetables ├── carrots └── tomato 然后输入:\n1 2 3 4 5 6 7 8 9 10 11 12 # 创建一个mount目录 $ mkdir mnt # 把水果目录和蔬菜目录union mount到 ./mnt目录中 $ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt # 查看./mnt目录 $ tree ./mnt ./mnt ├── apple ├── carrots └── tomato 可以看到mnt目录下有三个文件，水果和蔬菜的目录被合并起来了。如果我们修改一下文件内容:\n1 2 3 4 5 $ echo mnt \u0026gt; ./mnt/apple $ cat ./mnt/apple mnt $ cat ./fruits/apple mnt 可以发现如果修改了/mnt/apple下的内容，/fruits/apple下的内容也会被修改。\n1 2 3 4 5 $ echo mnt_carrots \u0026gt; ./mnt/carrots $ cat ./vegetables/carrots $ cat ./fruits/carrots mnt_carrots 但是这里又变得奇怪，我们修改了/mnt/carrots的内容，按照道理说应该是/vegetables/carrots被修改，但发现并不是，反而在/fruits下面出现了carrots的文件，并且我们的修改出现在这里面。换句话说，我们在mount aufs的时候没有指定vegetable和fruits的目录权限，默认来说命令行第一个的目录是rw的，后面的都是ro。如果我们在mount aufs的时候指定一下权限，就会有不一样的效果（先把刚才的/fruits/carrots删了）：\n1 2 3 4 5 6 7 8 9 $ sudo mount -t aufs -o dirs=./fruits=rw:./vegetables=rw none ./mnt $ echo \u0026#34;mnt_carrots\u0026#34; \u0026gt; ./mnt/carrots $ cat ./vegetables/carrots mnt_carrots $ cat ./fruits/carrots cat: ./fruits/carrots: No such file or directory 现在我们再看看修改:\n1 2 3 4 5 6 7 $ echo \u0026#34;mnt_tomato\u0026#34; \u0026gt; ./mnt/tomato $ cat ./fruits/tomato mnt_tomato $ cat ./vegetables/tomato I am a vegetable 看上去就对味了。\n我们可以思考一下一些使用场景，例如我们可以把一个目录，例如我们自己的source code，作为只读的模板，和另一个working directory给union起来，那么我们就可以随便魔改不用害怕把源代码改坏。有点像ad hoc snapshot。\nDocker分层镜像对UnionFS的使用 Docker就把UnionFS的技术借鉴到了容器中。在Linux Namespace中我们讨论了mount namespace和chroot去fake了一个镜像。而UnionFS的技术可以用来制作分层镜像。\n下面是Docker官方文档Layer，展示了Docker用UnionFS搭建制作的分层镜像：\ndocker的分层镜像不单单可以使用aufs，还支持例如btrfs, devicemapper和vfs。可以使用-s或者-storage-driver=选项来指定相关的镜像存储。Ubuntu14.04的环境里docker默认用的是aufs，而Centos7下用的是devicemapper，这个会有另一个post来讨论。\n可以在下面的路径查看每个层的镜像:\n/var/lib/docker/aufs/diff/\u0026lt;id\u0026gt; docker执行起来以后，docker run -it ubuntu /bin/bash，可以从/sys/fs/aufs/si_[id]目录查看aufs的mount情况，比如:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ls /sys/fs/aufs/si_b71b209f85ff8e75/ br0 br2 br4 br6 brid1 brid3 brid5 xi_path br1 br3 br5 brid0 brid2 brid4 brid6 # cat /sys/fs/aufs/si_b71b209f85ff8e75/* /var/lib/docker/aufs/diff/87315f1367e5703f599168d1e17528a0500bd2e2df7d2fe2aaf9595f3697dbd7=rw /var/lib/docker/aufs/diff/87315f1367e5703f599168d1e17528a0500bd2e2df7d2fe2aaf9595f3697dbd7-init=ro+wh /var/lib/docker/aufs/diff/d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07=ro+wh /var/lib/docker/aufs/diff/9fec74352904baf5ab5237caa39a84b0af5c593dc7cc08839e2ba65193024507=ro+wh /var/lib/docker/aufs/diff/a1a958a248181c9aa6413848cd67646e5afb9797f1a3da5995c7a636f050f537=ro+wh /var/lib/docker/aufs/diff/f3c84ac3a0533f691c9fea4cc2ceaaf43baec22bf8d6a479e069f6d814be9b86=ro+wh /var/lib/docker/aufs/diff/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158=ro+wh 64 65 66 67 68 69 70 /run/shm/aufs.xino 可以看到，只有最顶层的是有rw权限，其他都是ro+wh权限只读。\ndocker的aufs配置，可以在/var/lib/docker/repositories-aufs这个文件中看到。\nAUFS特性 AUFS包含了所有UnionFS的特性，把多个目录合并成一个目录，对每个需要合并的目录指定权限，可以实时得去添加、删除或者修改已经被mount的目录。甚至，还可以在多个可写的branch/dir之间做负载均衡。\n以上是AUFS的mount特性，我们再来看一下被union的目录的相关权限:\nrw表示可读可写\nro表示只读，如果在使用的时候不指定，那么除了第一个以外，其他都是ro。也就是时候，ro的branch不会接收到写的操作，也不会收到查找whiteout的操作。\nrr表示real-read-only，这个和ro还是有区别的。rr指的是天生就是只读的分支，能够让AUFS提供性能，例如可以不用设置inotify来检查文件变动通知。\nwhiteout属性一般来说ro分支都会有，上面的snippet中也展示了ro+wh，而它的意思就是，如果在union中删除某个文件，实际上是处于一个readonly的分支目录上。在mount的union这个目录你会看不到这个文件，但是readonly这个层上我们无法做任何的修改，因此我们就必须对readonly目录里的文件做whiteout。AUFS的whiteout实现是通过在上层的可写目录下建立对应whiteout隐藏文件夹实现的。\n举个🌰：\n还是最开始的例子，我们有以下的结构:\n1 2 3 4 5 6 7 8 9 # tree . ├── fruits │ ├── apple │ └── tomato ├── test └── vegetables ├── carrots └── tomato 我们按照下面的指令进行mount和权限分配:\n1 2 3 4 5 6 $ mkdir mnt $ mount -t aufs -o dirs=./test=rw:./fruits=ro:./vegetables=ro none ./mnt $ ls ./mnt/ apple carrots tomato 我们在权限为rw的test目录下新建一个whiteout的隐藏文件.wh.apple，可以发现./mnt/apple这个目录之间消失了:\n1 2 3 4 $ touch ./test/.wh.apple $ ls ./mnt carrots tomato 也就是说这个操作和rm ./mnt/apple是一样的。\n术语 Branch – 就是各个要被union起来的目录（就是我在上面使用的dirs的命令行参数）\nBranch根据被union的顺序形成一个stack，一般来说最上面的是可写的，下面的都是只读的。 Branch的stack可以在被mount后进行修改，比如：修改顺序，加入新的branch，或是删除其中的branch，或是直接修改branch的权限 Whiteout 和 Opaque\n如果UnionFS中的某个目录被删除了，那么就应该不可见了，就算是在底层的branch中还有这个目录，那也应该不可见了。\nWhiteout就是某个上层目录覆盖了下层的相同名字的目录。用于隐藏低层分支的文件，也用于阻止readdir进入低层分支。\nOpaque的意思就是不允许任何下层的某个目录显示出来。\n在隐藏低层档的情况下，whiteout的名字是’.wh.’。\n在阻止readdir的情况下，名字是’.wh..wh..opq’或者 ’.wh.__dir_opaque’。\n问题 要有文件在原来的地方被修改了会怎么样，mount的目录会一起改变吗？\n会也可能不会。因为你可以指定一个叫udba的参数（全称：User’s Direct Branch Access），这个参数有三个取值：\nudba=none – 设置上这个参数后，AUFS会运转的更快，因为那些不在mount目录里发生的修改，aufs不会同步过来了，所以会有数据出错的问题。 udba=reval – 设置上这个参数后，AUFS会去查文件有没有被更新，如果有的话，就会把修改拉到mount目录内。 udba=notify – 这个参数会让AUFS为所有的branch注册inotify，这样可以让AUFS在更新文件修改的性能更高一些。 如果有多个rw的branch（目录）被union起来了，那么，当我创建文件的时候，aufs会创建在哪里呢？\nAUFS提供了一个叫create的参数可以供你来配置相当的创建策略，下面有几个例子：\ncreate=rr | round−robin 轮询。下面的示例可以看到，新创建的文件轮流写到三个目录中：\n1 2 3 4 5 6 7 8 9 10 derios$ sudo mount -t aufs -o dirs=./1=rw:./2=rw:./3=rw -o create=rr none ./mnt derios$ touch ./mnt/a ./mnt/b ./mnt/c derios$ tree . ├── 1 │ └── a ├── 2 │ └── c └── 3 └── b create=mfs[:second] | most−free−space[:second] 选一个可用空间最好的分支。可以指定一个检查可用磁盘空间的时间。\ncreate=mfsrr:low[:second] 选一个空间大于low的branch，如果空间小于low了，那么aufs会使用 round-robin 方式。\n一些AUFS的细节参数，建议还是man aufs查看。\nAUFS的性能 AUFS把所有的分支mount起来，所以在查找文件上是慢一些。因为它要遍历所有的分支，O(N)复杂度的算法。因此分支越多，查找文件的性能也就越慢。但是一旦AUFS找到了这个文件的inode，那之后的读写和操作源文件基本是一样的。\n所以如果程序跑在AUFS下，那么open和stat操作会有明显的性能下降，分支越多性能就越差。但在write/read操作上性能没有什么变化。\n这里有一份IBM做的Docker性能报告《An Updated Performance Comparison of Virtual Machinesand Linux Containers》。\n资料 Introduce UnionFS\nUnion file systems: Implementations, part I\nUnion file systems: Implementations, part 2\nAnother union filesystem approach\nUnioning file systems: Architecture, features, and design choices\n","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/docker-aufs/","summary":"\u003cp\u003eAUFS是一种Union File System，所谓的UnionFS实际上就是把不同物理位置的目录合并mount到同一个目录当中。一种典型的UnionFS的应用，就是把一张CD/DVD和一个硬盘目录联合mount在一起，然后你就可以对这个只读的CD/DVD上的文件进行修改。\u003c/p\u003e\n\u003cp\u003eAUFS又叫做Another UnionFS，后面改成Alternative UnionFS，然后又变成Advance UnionFS\u0026hellip;..当然名字的改变叫啥不重要，本质还是没变的。2006年Junjiro Okajima开发了AUFS，完全重写了早期的UnionFS 1.X，主要目的是为了可靠性和性能，再引入一些新的功能，例如可写分支的负载均衡。不过很有意思的是，AUFS的性能比UnionFS 1.X好很多，后面UnionFS 2.x就抄AUFS的功能，而AUFS本身却没有合入到Linux主线，因为代码量太大质量也不好。虽然后面Junjiro不断提升代码质量，不断提交但是还是被Linus拒绝了。所以哪怕是今天AUFS也没进到Linux里，虽然质量已经可以了。\u003c/p\u003e\n\u003cp\u003e不过一些发行版比如：Ubuntu 10.04，Debian6.0都支持AUFS，所以也还好。我在Ubuntu 14.04演示一下例子。\u003c/p\u003e\n\u003cp\u003e首先，我们建立两个水果和蔬菜的目录，在这个目录上放一些文件，水果里有苹果和番茄，蔬菜有胡萝卜和番茄:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ tree\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── fruits\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│   ├── apple\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│   └── tomato\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└── vegetables\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ├── carrots\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    └── tomato\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e然后输入:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 创建一个mount目录\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ mkdir mnt\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 把水果目录和蔬菜目录union mount到 ./mnt目录中\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ sudo mount -t aufs -o \u003cspan class=\"nv\"\u003edirs\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e./fruits:./vegetables none ./mnt\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e#  查看./mnt目录\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ tree ./mnt\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./mnt\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── apple\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── carrots\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└── tomato\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e可以看到\u003ccode\u003emnt\u003c/code\u003e目录下有三个文件，水果和蔬菜的目录被合并起来了。如果我们修改一下文件内容:\u003c/p\u003e","title":"Docker Fundamentals: AUFS"},{"content":"这篇文章摘自陈皓（左耳朵耗子）的blog（2020/08/07上传），其中很多观点击中了我内心的想法，或许可以在我遇到方向性问题的时候给我提醒。\n这篇文章的主要内容主要是我今年3月份在腾讯做的直播，主要是想让一些技术人员对世界有一个大体的认识，并且在这个认识下能够有一个好的方法成就自己。而不是在一脸蒙圈的状态下随波逐流，而日益迷茫和焦虑。直播完后，腾讯方面把我的直播形成文字的形式发了出来，我觉得我可以再做一个精编版。所以，有了这篇文章，希望对大家有帮助。\n对我来说，在我二十多年的工作经历来看，期间经历了很多技术的更新换代，整个技术模式、业务模式也是一直变来变去，我们这群老程序员成长中所经历的技术比今天的程序员玩的还更杂更多。我罗列一下我学过的，而且还被淘汰掉的技术，大家先感受一下。\n- MIS应用开发：FoxPro，PowerBuilder，Delphi - OA：Lotus Notes，VBScripts - 微软：ODBC/ADO，COM/DCOM，MFC/ATL，J++ - 服务器：AIX，HP-UX，SCO Unix - Web：CGI，ISAPI，SOAP - RPC：CICS，Tuxedo - J2EE：Websphere，Weblogic - DB：Sybase，Informix 我想说的是，无论过去还是今天，我们这些前浪和你们后浪所面对的技术的挑战和对技术的焦虑感是相似的，我们那个时候不但玩996，还玩封闭开发（就是一周只能回家一天）。当然，唯一好的东西，就是比起今天的程序员来说，我们那个年代没有像微信、微博、知乎，抖音这些巨大消耗你人生的东西，所以，我们的工作、生活和成长都有很效率，不会被打断、喜欢看书、Google还没有被封……当然，那时代没有StackOverlow和Github这样的东西，所以，能完成的东西或质量都一般。\n当然，这里并不是想做一个比较，只是想让大家了解一下两代程序员间的一些问题各有千秋，大同小异。在整个成长过程中，其实有很多东西是相通的，其本上来说，就是下面的三件事——\n第一，如果想要把控技术，应对这个世界的一些变化，需要大致知道这个世界的一些规律和发展趋势，另外还得认识自己，自己到底适合做什么？在这个趋势和规律下属于自己的发挥领域到底是什么？这是我们每个人都需要了解的。\n第二，打牢基础，以不变应万变，不管世界怎样变化，我都能很快适应它。基础的重要程度对于你能够飞多高是相当有影响的，懂原理的人比不懂原理的人能做出来的事情或是能解决的问题完全是两个层级的。\n第三，提升成长的效率，因为现在社会的节奏实在太快了，比二十年前快得太多，技术层出不穷，所以我们的成长也要更有效率。效率并不单指的快，效率是怎么样更有效，是有用功除以总功（参看《加班与效率》），怎么学到更有效的东西，或者怎么更有效学习，是我们需要掌握的另一关键。\n下面是我这多年来的一些认识，希望对你有帮助。\n世界发展趋势 我个人经历的信息化革命应该分成三个阶段：\n1990年代到2000年，这个时代MB时代，是雅虎、新浪、搜狐、网易门户网站的时代，这个时代就是ISP/ICP互联网提供商，把一些资讯数字化，然后发布到网络上。 2000年到2010年，这个时代叫GB时代，或是叫多媒体或UGC时代，上网开始变得普遍了，每个人手里的数码设备开始变得多了起来，可以上传照片，可以上传视频，甚至可以在网上做社交。 2010年到2020年，这个时代叫TB时代，这过去的十年是移动互联网时代，移动互联网只需要手机在线，不需要依靠电脑。因为手机随时在线，所以个人的各种各样的数据始终在被收集，只要用户上网就会产生数据，所以人的行为最终也被数字化了。 所有的硬件和软件都是跟着需要处理的数据而演进的，我们需要更大的带宽，更大的硬盘，更多的处理器……大到一定时候就只能进入分布式化的技术架构了，再大，数据中心也顶不住了，就会要引入更为分布式的边缘计算了。\n另一方面，从业务上来看，我们可以看到整个世界就在不断地进行数字化，因为，只要数字化了，就可以进行复制传播和计算，只要可以进行计算了，就可以进行数学建模，就可以自动化，只要可以自动化了就可以规模化，只要可能规模化了，就可以改变整个行业。人类的近代史的大趋势基本上都是在解决能源和自动化的事，源源不断的能源是让机器不知疲倦的前提条件，用机器代替牲口，代替人类进行工作是规模化的前提条件。\n所以，技术的演进规律基本是自动化加规模化，从而降低成本，提升效率。这就是为什么世界变得越来越快，人类都快跟不上节奏的原因，主要是整个社会不断被机器、数据所驱动。\n人才需求 在这个过程中，需要什么样的人？下面是我的一些认识——\n技工，在机器和自动化面前，肯定是需要能够操作机器的技术工人了，这类人是有技术的劳动力。在编程的圈子里俗称“码农”，他们并不是真正的工程师，他们只是电脑程序的操作员，所以，随着技术门槛的下降或是技术形式的变更他可能就会变得越来越不值钱，直到被淘汰掉。 特种工，这种人是必须了解原理和解决难题的一类人，他们是解决比较难的、特定的一些技术问题。当一种技术被淘汰，他并不容易被淘汰，因为他懂原理，原理就是解决问题的能力，是解决问题的套路和方法。 工程师，不但是使用技术，还可以把活儿做好，他们认为代码更多的时间是在维护，这些人使用各种各样的手段和各种技术，精益求精地持续不断地提高代码的易读性、扩展性、可维护性和重用性，这个过程似乎永无止境。对于这些有“洁癖”，有“工匠精神”，有“修养”的技术人员，我们称他们为工程师。这种人做事又稳又快，而且可以做出很多称手的工具和方法论。 再往上是设计师和架构人员，这些人主要是开发一些工具，框架，模式，提升软件开发和维护效率，同时也提升用户体验，和提升稳定性、性能、代码重用等，总的来说就是为了降本增效。这类人的工作降低了技术得到门槛，他们把技术门槛降低了以后，就可以把这个技术普及开来，就可以由广大劳工、技工、特殊工人使用了。 还有一类人是经理，经理主要是组织团队、完成项目、创造利润。这类人中，即有身先士卒的leader，也有高高在上的boss，但无论怎么样，这些人只不过是为了让一个公司或是一个团队更好组织在一起的“粘合剂”，这类人只有在大公司中才会变成更有价值。 这就是我总结的世界需要哪些人才，我们了解这些东西以后大概就明白我们现在所处的位置有什么样的问题，我们应该去什么样的地方。\nGoogle 评分卡 接下来，我们再来看看Google的SRE的自我评分卡：\n0 – 对于相关的技术领域还不熟悉 1 – 可以读懂这个领域的基础知识 2 – 可以实现一些小的改动，清楚基本的原理，并能够在简单的指导下自己找到更多的细节。 3 – 基本精通这个技术领域，完全不需要别人的帮助 4 – 对这个技术领域非常的熟悉和舒适，可以应对和完成所有的日常工作。 对于软件领域 – 有能力开发中等规模的程序，能够熟练和掌握并使用所有的语言特性，而不是需要翻书，并且能够找到所有的冷知识。 对于系统领域 – 掌握网络和系统管理的很多基础知识，并能够掌握一些内核知识以运维一个小型的网络系统，包括恢复、调试和能解决一些不常见的故障。 5 – 对于该技术领域有非常底层的了解和深入的技能。 6 – 能够从零开发大规模的程序和系统，掌握底层和内在原理，能够设计和部署大规模的分布式系统架构 7 – 理解并能利用高级技术，以及相关的内在原理，并可以从根本上自动化大量的系统管理和运维工作。 8 – 对于一些边角和晦涩的技术、协议和系统工作原理有很深入的理解和经验。能够设计，部署并负责非常关键以及规模很大的基础设施，并能够构建相应的自动化设施 9 – 能够在该技术领域出一本经典的书。并和标准委员会的人一起工作制定相关的技术标准和方法。 10 – 在该领域写过一本书，被业内尊为专家，并是该技术的发明人。 SRE需要自评如下这些技术或技能。\n– TCP/IP Networking (OSI stack, DNS etc) – Unix/Linux internals – Unix/Linux Systems administration – Algorithms and Data Structures – C/C++ – Python – Java – Perl – Go – Shell Scripting (sh, Bash, ksh, csh) – SQL and/or Database Admin – Scripting language of your choice (not already mentioned) _____________ – People Management – Project Management 这个评分卡是面试Google前需要候选人对自己的各种技术进行自评，也算是一种技术人员的等级的度量尺，其把技术的能分成11个等级，我用颜色把其它成四大层级，希望这个评份卡能够给你一个能力提升的参考标准。\n认识自己 认识了世界是怎么发展的，也知道技术人员的种类和层级，那么还要了解一下自己，因为如果不了解自己，那么你也无法找到自己的路和适合自己的地方。\n我觉得，一个人要认识自己就需要认识自己的特长、兴趣、热情、擅长等，下面是一个认识自己的标准方法：\n特长。首先你要找得到自己特长。你要认识自己的特长，找到自己的天赋，找到你在DNA里比别人强的东西，就拿你的DNA跟别人竞争就好了。所以你要找到自己可以干成的事，找到别人找你请教的事，你身边人找你请教就是说明你有特长。这是找到自己特长非常非常重要，扬长避短。 兴趣。如果你没有找到自己特长，就找自己有兴趣有热情的东西。什么叫兴趣？兴趣是再难再累都不会放弃的事。如果你遇到困难就会放弃不叫兴趣，那叫叶公好龙。不怕困难，痴迷其中，就算你没有特长，有了这种特质，你也是头部的人才。 方法。如果你没有特长，没有兴趣和热情就要学方法。这种方法就是要有时间观念，要会做计划，要懂统筹、规划对于做过的事情，犯过的错误多总结，举一反三，喜欢自己找答案，自己探究因果关系，这是一些方法，自己总结一些套路。 **勤奋。**如果你没有特长，没有兴趣，也没有方法，你还能做的事就是勤奋，勤奋注定会让你成为一个比较劳累的人，也是很有可能被淘汰的人随着你的年纪越来越大，你的勤奋也会越来越不值钱。因为年轻人会比你更勤奋，比你更勤奋、比你斗志更强，比你能力更强，比你要钱更少的人会出现。勤奋最不值钱，但是只要你勤奋至少能够自食其力。 以上就是为了应对未来技术变化，作为个人必须要从特长、兴趣、方法一层一层筛选挖掘，如果没有这些你就要努力和勤奋。就只能接受“福报”了。\n从我个人而言，我不算是特别聪明的人，但自认为对技术还是比较感兴趣的，难的我不怕。有很多比较难啃的技术，聪明点的人啃一个月就懂了，我不行，我可能啃半年。但是没有关系，知识都是死的，只要不怕困难总有一天会懂的。最可怕是畏难，为自己找借口，这样就不太好了。\n打好基础 最前面提到我学的各式各样的被淘汰的技术，会让你感觉很迷茫，或是迷失。但前面也提到了“谷歌评分卡”，在这个评分卡中，我们看到了许多基础原理方面的内容，其实要应对未来的变化，很重要的一点就是无招胜有招，以不变应万变。\n变化都是表面的东西，内在的东西其实并没有太多的变化。理论层面上变得不多，反而形式上的东西今天一个花样，明天一个花样，所以如果要去应对这种变化，就一定要打牢自己的基础，提升内功修养。比如像编程的一些方式和套路，修饰模式原理本质，解耦，提升代码的重用度等。提升代码重用度必须解耦，要跟现实解耦，提升抽象，这些都是一些技术基础。无论用什么语言，都是这么做的。\n打牢基础就可以突破瓶颈，不打牢基础没有办法突破瓶颈。在技术世界不要觉得量变会造成质变，这是不可能的。技术这个东西就像搞建筑砌砖头，砌砖头砌的再多也不可能让你能成为一个架构师的，因为你不懂原理，不懂科学方法，你就不可能成长上去的，就像学数学一样，当你掌握了微积分这种大杀器后，你解题的能力是无所披靡，而微积分这种方式绝对不是你能“量变”出来的。\n所以你必须学习基础的理论知识，如果不学这些基础理论知识，还要学习解题思路和方法，如果你只学在表面，那么当这个技术的形式有变化，就会发现以前学的都没用了，要重头学一遍。掌握技术基础可以让自己找到答案和知识，基础是抽象和归纳，很容易形成进一步的推论。我们学的很多技术实现都逃不脱基础原理，不管是Java，还是其他语言，只要用TCP用的都是相同的原理，逃不出范围，只要抓住原理，举一反三，时间一长了，甚至还可以自己推导答案。对于技术的基础，我会把其它成四类：\n程序语言：语言的原理，类库的实现，编程技术（并发、异步等），编程范式，设计模式…… 系统原理：计算机系统，操作系统，网络协议，数据库原理…… 中间件：消息队列，缓存系统，网关代理，调度系统 …… 理论知识：算法和数据结构，数据库范式，网络七层模型，分布式系统…… 这些知识其实就是一个计算机科学专业的学生他所要学习的原理，但可惜的是，我们的一些学校教得也很糟糕，不但老师能力不足，而且放着世界上最优秀的教课书不用了，一定要自己写一本。讲也讲不全，还有各种错误，哎……总之，如果你学习用用到的教材不行，那么可以肯定的是你的学习效率一定是很糟糕的。这就是为什么我们大学上完了，还是跟个傻瓜一样，还要在工作中再重新自学。\n不过，就算自学，这些基础技术大概需要四五年的时间堆叠。我工作二十年了，这二十年来基本还是这些原理没变，无论形式怎么变，但是核心永远还是这些，理论创新很难，这是以不变应万变。\n学习效率 谈到学习效率，就需要拿出这张学习金字塔的图来了。从图可以看到学习方法分布两层，一种是被动学习，也是浅度学习，听讲，阅读，视听，演示都是在被动学习，而与人讨论，自己动手实践，教授给别人是主动学习。主动学习我们称之为深度学习，如果你不能深度学习，你就不能真正学到东西。这也是你会经常有“学那么多干什么，不用就忘了”，这就是浅度学习的症状了。\n下面，我给出一些我自己觉得不错的学习经验：\n1、挑选一手知识和信息源。对于学习方法：第一我们一定要到知识源去挑选知识，知识信息源非常关键，二手信息丢失太大了，谭浩强写的书就丢失太多信息了。目前计算机一手知识基本都是国外的，所以英文非常重要。我鼓励大家一定读第一手的资料。如果你英语有问题，至少要看翻译过来，最好是原汁原味翻译的，不要我理解了给你讲那种，那种也是被别人嚼一遍再讲给你你没有体会，是别人带着你，别人的体会会影响你，也许你的体会会比他更好，因为是你自己总结出来的东西，所以知识源很重要。\n2、注意原理和基础 第二要注重基础原理。虽然可以忘记这个技术，但是原理记在心里，我可以徒手实现出来，而且通过原理可以更快学习其他类似的技术。所以原理很重要！当你学会C、C++要学Java和GO都很快。\n3、使用知识图谱 一定要学会使用知识图，把知识结构化。从一个技术关键点开始不断地关联和细化下去，比如：关于TCP协议，首先第一个要记住状态图，怎么建立连接，怎么断连接，状态怎么变迁。TCP没有连接，是靠状态维护连接的。其次，要了解TCP怎么保证可靠性，就是丢包以后怎么重传，重传有哪些技术点。然后，重传会让你联想到拥塞控制，拥塞控制到滑动窗口……。这基本就是TCP的所有东西了，找到关键点，然后顺着这个脉络一点点往下想，通过知识图关联就可以进行顺藤摸瓜。我们不需要记所有知识，那些手册的知识不需要记，你知道在哪里能找到就可以了。你脑子里面要有地图，学一个东西就跟在城市生活一样，闭上眼睛就知道地图，A点到B点怎么去大概方向要知道。我在北京我去广州，广州在南边，我大概坐飞机还是火车要心里有数。。\n4、学会举一反三。就是用不同方法学一个东西，比如说学TCP协议，看书是一种方法，编程是另外一种方法，还有用做Debug去看的，用不同方法学一个东西会让你更加熟悉，你学一个知识的同时把周边也学了。比如说学前端能不能把HTTP学一下，比如说长连接、短连接，包括hp1、hp2有一些不一样的东西。\n**5、总结和归纳。**只有学会总结和归纳，才能形成自己的思维框架、自己的套路、自己的方法论，以后学这个东西应该怎么学。就像学一门新的语言，不管GO语言，还是Rust语言，第一件事情就是了解内存是怎么管理的，数据类型什么样，第二是泛型怎么搞，第三是并发怎么弄。还有一些抽象怎么弄，比如说怎么解耦，怎么实现多态？套路这种东西只有学的多了以后才能形成套路，如果你只学会一门语言不会有套路，你要每年学门语言，不用学多精，你思考这个语言有什么不一样，为什么这个这种有玩法，那个有那种玩法，这些东西思考多了套路方法论就出来了。比如说Windows和Linux有什么不同，Linux和Unix又有什么不同？只有总结自己的框架、套路和方法，这些才永远不会被淘汰。\n**6、实践和坚持。**剩下就是多做多练，多坚持，只有实践才会有经验，只有锻炼了才能够把自己的脂肪变没，所以，要把知识变成技能必须练，就像小学生学会加减乘除，还是要演练，必须多做题，题目做得多了，自然掌握得好。要挑选好的知识源，注重原理技术，有一些原理的基础的书太枯燥，但是我告诉你学习这些基础太值得投入时间，搬砖赚几十元不值得，因为赚的是辛苦钱，老了就赚不了，必须要赚更有能力的钱，这是学习投资。\n小结 好了，该到这篇文章收尾的时候了，小结一下，如果你想更好的把握时代，提升自己，你需要知道这个时代的趋势是什么，需要什么样的人，这些人需要什么样的能力，这些能力是怎么获得的，投入到基础知识的学习就像“基建”一样，如果基础不好，不能长高，学习能力也是需要适应这个快速时代的重要的基础能力，没有好的学习能力，很快就会掉队被淘汰。\n","permalink":"https://pillumina.github.io/posts/programming/programmer-career/","summary":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003e这篇文章摘自陈皓（左耳朵耗子）的blog（2020/08/07上传），其中很多观点击中了我内心的想法，或许可以在我遇到方向性问题的时候给我提醒。\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e这篇文章的主要内容主要是我今年3月份在腾讯做的直播，主要是想让一些技术人员对世界有一个大体的认识，并且在这个认识下能够有一个好的方法成就自己。而不是在一脸蒙圈的状态下随波逐流，而日益迷茫和焦虑。直播完后，腾讯方面把我的直播形成文字的形式发了出来，我觉得我可以再做一个精编版。所以，有了这篇文章，希望对大家有帮助。\u003c/p\u003e\n\u003cp\u003e对我来说，在我二十多年的工作经历来看，期间经历了很多技术的更新换代，整个技术模式、业务模式也是一直变来变去，我们这群老程序员成长中所经历的技术比今天的程序员玩的还更杂更多。我罗列一下我学过的，而且还被淘汰掉的技术，大家先感受一下。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- MIS应用开发：FoxPro，PowerBuilder，Delphi\n- OA：Lotus Notes，VBScripts\n- 微软：ODBC/ADO，COM/DCOM，MFC/ATL，J++\n- 服务器：AIX，HP-UX，SCO Unix\n- Web：CGI，ISAPI，SOAP\n- RPC：CICS，Tuxedo\n- J2EE：Websphere，Weblogic\n- DB：Sybase，Informix \n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e我想说的是，无论过去还是今天，我们这些前浪和你们后浪所面对的技术的挑战和对技术的焦虑感是相似的，我们那个时候不但玩996，还玩封闭开发（就是一周只能回家一天）。当然，唯一好的东西，就是比起今天的程序员来说，我们那个年代没有像微信、微博、知乎，抖音这些巨大消耗你人生的东西，所以，我们的工作、生活和成长都有很效率，不会被打断、喜欢看书、Google还没有被封……当然，那时代没有StackOverlow和Github这样的东西，所以，能完成的东西或质量都一般。\u003c/p\u003e\n\u003cp\u003e当然，这里并不是想做一个比较，只是想让大家了解一下两代程序员间的一些问题各有千秋，大同小异。在整个成长过程中，其实有很多东西是相通的，其本上来说，就是下面的三件事——\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第一\u003c/strong\u003e，如果想要把控技术，应对这个世界的一些变化，\u003cstrong\u003e需要大致知道这个世界的一些规律和发展趋势，另外还得认识自己\u003c/strong\u003e，自己到底适合做什么？在这个趋势和规律下属于自己的发挥领域到底是什么？这是我们每个人都需要了解的。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第二\u003c/strong\u003e，\u003cstrong\u003e打牢基础，以不变应万变\u003c/strong\u003e，不管世界怎样变化，我都能很快适应它。基础的重要程度对于你能够飞多高是相当有影响的，懂原理的人比不懂原理的人能做出来的事情或是能解决的问题完全是两个层级的。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第三，提升成长的效率\u003c/strong\u003e，因为现在社会的节奏实在太快了，比二十年前快得太多，技术层出不穷，所以我们的成长也要更有效率。效率并不单指的快，效率是怎么样更有效，是有用功除以总功（参看《\u003ca href=\"https://coolshell.cn/articles/10217.html\"\u003e加班与效率\u003c/a\u003e》），怎么学到更有效的东西，或者怎么更有效学习，是我们需要掌握的另一关键。\u003c/p\u003e\n\u003cp\u003e下面是我这多年来的一些认识，希望对你有帮助。\u003c/p\u003e\n\u003ch2 id=\"世界发展趋势\"\u003e世界发展趋势\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e我个人经历的信息化革命应该分成三个阶段：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e1990年代到2000年，这个时代MB时代\u003c/strong\u003e，是雅虎、新浪、搜狐、网易门户网站的时代，这个时代就是ISP/ICP互联网提供商，把一些资讯数字化，然后发布到网络上。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e2000年到2010年，这个时代叫GB时代，或是叫多媒体或UGC时代\u003c/strong\u003e，上网开始变得普遍了，每个人手里的数码设备开始变得多了起来，可以上传照片，可以上传视频，甚至可以在网上做社交。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e2010年到2020年，这个时代叫TB时代，这过去的十年是移动互联网时代\u003c/strong\u003e，移动互联网只需要手机在线，不需要依靠电脑。因为手机随时在线，所以个人的各种各样的数据始终在被收集，只要用户上网就会产生数据，所以人的行为最终也被数字化了。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e所有的硬件和软件都是跟着需要处理的数据而演进的，我们需要更大的带宽，更大的硬盘，更多的处理器……大到一定时候就只能进入分布式化的技术架构了，再大，数据中心也顶不住了，就会要引入更为分布式的边缘计算了。\u003c/p\u003e\n\u003cp\u003e另一方面，从业务上来看，\u003cstrong\u003e我们可以看到整个世界就在不断地进行数字化，因为，只要数字化了，就可以进行复制传播和计算，只要可以进行计算了，就可以进行数学建模，就可以自动化，只要可以自动化了就可以规模化，只要可能规模化了，就可以改变整个行业\u003c/strong\u003e。人类的近代史的大趋势基本上都是在解决能源和自动化的事，源源不断的能源是让机器不知疲倦的前提条件，用机器代替牲口，代替人类进行工作是规模化的前提条件。\u003c/p\u003e\n\u003cp\u003e所以，\u003cstrong\u003e技术的演进规律基本是自动化加规模化，从而降低成本，提升效率\u003c/strong\u003e。这就是为什么世界变得越来越快，人类都快跟不上节奏的原因，主要是整个社会不断被机器、数据所驱动。\u003c/p\u003e\n\u003ch4 id=\"人才需求\"\u003e人才需求\u003c/h4\u003e\n\u003cp\u003e在这个过程中，需要什么样的人？下面是我的一些认识——\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e技工\u003c/strong\u003e，在机器和自动化面前，肯定是需要能够操作机器的技术工人了，这类人是有技术的劳动力。在编程的圈子里俗称“码农”，他们并不是真正的工程师，他们只是电脑程序的操作员，所以，\u003cstrong\u003e随着技术门槛的下降或是技术形式的变更他可能就会变得越来越不值钱，直到被淘汰掉\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e特种工\u003c/strong\u003e，这种人是必须了解原理和解决难题的一类人，他们是解决比较难的、特定的一些技术问题。\u003cstrong\u003e当一种技术被淘汰，他并不容易被淘汰，因为他懂原理，原理就是解决问题的能力，是解决问题的套路和方法\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e工程师\u003c/strong\u003e，不但是使用技术，还可以把活儿做好，他们认为代码更多的时间是在维护，这些人使用各种各样的手段和各种技术，精益求精地持续不断地提高代码的易读性、扩展性、可维护性和重用性，这个过程似乎永无止境。对于这些有“洁癖”，有“工匠精神”，有“修养”的技术人员，我们称他们为工程师。\u003cstrong\u003e这种人做事又稳又快，而且可以做出很多称手的工具和方法论\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e再往上是\u003cstrong\u003e设计师和架构人员\u003c/strong\u003e，这些人主要是开发一些工具，框架，模式，提升软件开发和维护效率，同时也提升用户体验，和提升稳定性、性能、代码重用等，总的来说就是为了降本增效。这类人的工作降低了技术得到门槛，他们把技术门槛降低了以后，就可以把这个技术普及开来，就可以由广大劳工、技工、特殊工人使用了。\u003c/li\u003e\n\u003cli\u003e还有一类人是\u003cstrong\u003e经理\u003c/strong\u003e，经理主要是组织团队、完成项目、创造利润。这类人中，即有身先士卒的leader，也有高高在上的boss，但无论怎么样，这些人只不过是为了让一个公司或是一个团队更好组织在一起的“粘合剂”，这类人只有在大公司中才会变成更有价值。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这就是我总结的世界需要哪些人才，我们了解这些东西以后大概就明白我们现在所处的位置有什么样的问题，我们应该去什么样的地方。\u003c/p\u003e\n\u003ch2 id=\"google-评分卡\"\u003eGoogle 评分卡\u003c/h2\u003e\n\u003cp\u003e接下来，我们再来看看Google的SRE的自我评分卡：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e0 – 对于相关的技术领域还不熟悉\n1 – 可以读懂这个领域的基础知识\n2 – 可以实现一些小的改动，清楚基本的原理，并能够在简单的指导下自己找到更多的细节。\n\n3 – 基本精通这个技术领域，完全不需要别人的帮助\n4 – 对这个技术领域非常的熟悉和舒适，可以应对和完成所有的日常工作。\n\n对于软件领域 – 有能力开发中等规模的程序，能够熟练和掌握并使用所有的语言特性，而不是需要翻书，并且能够找到所有的冷知识。\n对于系统领域 – 掌握网络和系统管理的很多基础知识，并能够掌握一些内核知识以运维一个小型的网络系统，包括恢复、调试和能解决一些不常见的故障。\n5 – 对于该技术领域有非常底层的了解和深入的技能。\n\n6 – 能够从零开发大规模的程序和系统，掌握底层和内在原理，能够设计和部署大规模的分布式系统架构\n7 – 理解并能利用高级技术，以及相关的内在原理，并可以从根本上自动化大量的系统管理和运维工作。\n8 – 对于一些边角和晦涩的技术、协议和系统工作原理有很深入的理解和经验。能够设计，部署并负责非常关键以及规模很大的基础设施，并能够构建相应的自动化设施\n\n9 – 能够在该技术领域出一本经典的书。并和标准委员会的人一起工作制定相关的技术标准和方法。\n10 – 在该领域写过一本书，被业内尊为专家，并是该技术的发明人。\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eSRE需要自评如下这些技术或技能。\u003c/p\u003e","title":"(转)程序员如何把控自己的职业"},{"content":"Linux Namespace的技术解决了环境隔离的问题，不过这是虚拟化最基本的一步，我们另外需要解决对计算机资源使用上的隔离。说人话，就是虽然Namespace把我关到一个特定的环境，但是里面进程使用的CPU、内存、磁盘等计算资源实际上没有被限制。这个问题的解决，就要用到CGroup技术。\nLinux CGroup全称是Linux Control Group，也是其内核的一个功能，用于限制、控制和分离一个进程group的资源。最早这个项目是2006年由谷歌的工程师发起的，最开始名称是process containers（工程容器），后面觉得内核中容器这个名词被用烂了，就改名为cgroup。\nCGroup可以让你对系统中运行的进程的用户组分配资源-CPU时间、系统内存、网络带宽亦或者是这些的组合。同时，也可以监控你配置的cgroup，拒绝cgroup访问某些资源。主要提供的功能如下：\nResource Limitation： 限制资源使用\nPrioritization: 优先级控制，例如CPU使用和磁盘IO吞吐\nAccounting：审计统计，主要用于计费\nControl：挂起进程，恢复执行进程\n在真正的实践当中，system admin一般会利用CGroup做以下的事：\n对进程集合进行隔离，限制他们所消费的资源，例如绑定CPU core\n为这组进程分配足够使用的内存\n为这组进程分配响应的网络带宽和磁盘存储限制\n限制访问某些设备（白名单）\nLinux实际上把CGroup实现成了一个文件系统，你可以mount。在linux环境输入下面的可以看到cgroup已经为你mount好：\n1 2 3 4 5 6 7 8 9 10 11 12 derios@ubuntu:~$ mount -t cgroup cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset) cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu) cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,relatime,cpuacct) cgroup on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory) cgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,relatime,blkio) cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,net_prio) cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,net_cls) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,relatime,perf_event) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,relatime,hugetlb) 可以看到，在/sys/fs下有cgroup目录，这个目录下面有各种子目录：cpu，cpuset，memory\u0026hellip;。这些都是cgroup的子系统，分别用来干不同的事。\n如果没有看到上面的目录，你可以自己mount:\n1 2 3 4 5 6 7 8 mkdir cgroup mount -t tmpfs cgroup_root ./cgroup mkdir cgroup/cpuset mount -t cgroup -ocpuset cpuset ./cgroup/cpuset/ mkdir cgroup/cpu mount -t cgroup -ocpu cpu ./cgroup/cpu/ mkdir cgroup/memory mount -t cgroup -omemory memory ./cgroup/memory/ mount成功以后，你会看见这些目录下有文件，比如下面展现的cpu和cpuset子系统:\n1 2 3 4 5 6 7 8 9 10 11 12 13 derios@ubuntu:~$ ls /sys/fs/cgroup/cpu /sys/fs/cgroup/cpuset/ /sys/fs/cgroup/cpu: cgroup.clone_children cgroup.sane_behavior cpu.shares release_agent cgroup.event_control cpu.cfs_period_us cpu.stat tasks cgroup.procs cpu.cfs_quota_us notify_on_release user /sys/fs/cgroup/cpuset/: cgroup.clone_children cpuset.mem_hardwall cpuset.sched_load_balance cgroup.event_control cpuset.memory_migrate cpuset.sched_relax_domain_level cgroup.procs cpuset.memory_pressure notify_on_release cgroup.sane_behavior cpuset.memory_pressure_enabled release_agent cpuset.cpu_exclusive cpuset.memory_spread_page tasks cpuset.cpus cpuset.memory_spread_slab user cpuset.mem_exclusive cpuset.mems 你可以进入/sys/fs/cgroup的各个目录下去创建个dir，你会发现一旦你创建了，这个子目录下又有很多文件：\n1 2 3 4 5 derios@ubuntu:/sys/fs/cgroup/cpu$ sudo mkdir haoel [sudo] password for derios: derios@ubuntu:/sys/fs/cgroup/cpu$ ls ./haoel cgroup.clone_children cgroup.procs cpu.cfs_quota_us cpu.stat tasks cgroup.event_control cpu.cfs_period_us cpu.shares notify_on_release CPU限制 假设我们写了个吃CPU的程序：\n1 2 3 4 5 6 int main(void) { int i = 0; for(;;) i++; return 0; } 用sudo执行毫无疑问cpu飚到100%：\n1 2 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3529 root 20 0 4196 736 656 R 99.6 0.1 0:23.13 deadloop 在上面我们在/sys/fs/cgroup/cpu下面创建了haoel的group，我们首先设置下cpu限制\n1 2 3 derios@ubuntu:~# cat /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us -1 root@ubuntu:~# echo 20000 \u0026gt; /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us 我们top出来看到这个线程的pid是3529，我们加到这个cgroup中:\n1 # echo 3529 \u0026gt;\u0026gt; /sys/fs/cgroup/cpu/haoel/tasks 然后再看top，可以发现CPU使用率一下降成了20%（设置的20000代表着20%）：\n1 2 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3529 root 20 0 4196 736 656 R 19.9 0.1 8:06.11 deadloop 下面贴一个我google的代码示例，用于参考看看注释:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #define _GNU_SOURCE /* See feature_test_macros(7) */ #include \u0026lt;pthread.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/syscall.h\u0026gt; const int NUM_THREADS = 5; void *thread_main(void *threadid) { /* 把自己加入cgroup中（syscall(SYS_gettid)为得到线程的系统tid） */ char cmd[128]; sprintf(cmd, \u0026#34;echo %ld \u0026gt;\u0026gt; /sys/fs/cgroup/cpu/haoel/tasks\u0026#34;, syscall(SYS_gettid)); system(cmd); sprintf(cmd, \u0026#34;echo %ld \u0026gt;\u0026gt; /sys/fs/cgroup/cpuset/haoel/tasks\u0026#34;, syscall(SYS_gettid)); system(cmd); long tid; tid = (long)threadid; printf(\u0026#34;Hello World! It\u0026#39;s me, thread #%ld, pid #%ld!\\n\u0026#34;, tid, syscall(SYS_gettid)); int a=0; while(1) { a++; } pthread_exit(NULL); } int main (int argc, char *argv[]) { int num_threads; if (argc \u0026gt; 1){ num_threads = atoi(argv[1]); } if (num_threads\u0026lt;=0 || num_threads\u0026gt;=100){ num_threads = NUM_THREADS; } /* 设置CPU利用率为50% */ mkdir(\u0026#34;/sys/fs/cgroup/cpu/haoel\u0026#34;, 755); system(\u0026#34;echo 50000 \u0026gt; /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us\u0026#34;); mkdir(\u0026#34;/sys/fs/cgroup/cpuset/haoel\u0026#34;, 755); /* 限制CPU只能使用#2核和#3核 */ system(\u0026#34;echo \\\u0026#34;2,3\\\u0026#34; \u0026gt; /sys/fs/cgroup/cpuset/haoel/cpuset.cpus\u0026#34;); pthread_t* threads = (pthread_t*) malloc (sizeof(pthread_t)*num_threads); int rc; long t; for(t=0; t\u0026lt;num_threads; t++){ printf(\u0026#34;In main: creating thread %ld\\n\u0026#34;, t); rc = pthread_create(\u0026amp;threads[t], NULL, thread_main, (void *)t); if (rc){ printf(\u0026#34;ERROR; return code from pthread_create() is %d\\n\u0026#34;, rc); exit(-1); } } /* Last thing that main() should do */ pthread_exit(NULL); free(threads); } 内存使用限制 我们构造一下无限分配内存的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main(void) { int size = 0; int chunk_size = 512; void *p = NULL; while(1) { if ((p = malloc(p, chunk_size)) == NULL) { printf(\u0026#34;out of memory!!\\n\u0026#34;); break; } memset(p, 1, chunk_size); size += chunk_size; printf(\u0026#34;[%d] - memory is allocated [%8d] bytes \\n\u0026#34;, getpid(), size); sleep(1); } return 0; } 这个代码是个dead loop，每隔1秒会分配512字节的内存。然后我们再cgroup搞事情：\n1 2 3 4 5 6 # 创建memory cgroup $ mkdir /sys/fs/cgroup/memory/haoel $ echo 64k \u0026gt; /sys/fs/cgroup/memory/haoel/memory.limit_in_bytes # 把上面的进程的pid加入这个cgroup $ echo [pid] \u0026gt; /sys/fs/cgroup/memory/haoel/tasks 可以看到上面的进程会因为内存oom被kill。\n磁盘IO限制 我们先查看下磁盘IO，模拟一下（从/dev/sda1读取数据，输出到/dev/null上）：\n1 sudo dd if=/dev/sda1 of=/dev/null 通过iotop命令可以看到相关的io速度为55MB/s：\n1 2 TID PRIO USER DISK READ DISK WRITE SWAPIN IO\u0026gt; COMMAND 8128 be/4 root 55.74 M/s 0.00 B/s 0.00 % 85.65 % dd if=/de~=/dev/null... 然后，我们先创建一个blkio（块设备IO）的cgroup:\n1 mkdir /sys/fs/cgroup/blkio/haoel 并把读IO限制到1MB/s，并把前面那个dd命令的pid放进去（注：8:0 是设备号，你可以通过ls -l /dev/sda1获得）：\n1 2 root@ubuntu:~# echo \u0026#39;8:0 1048576\u0026#39; \u0026gt; /sys/fs/cgroup/blkio/haoel/blkio.throttle.read_bps_device root@ubuntu:~# echo 8128 \u0026gt; /sys/fs/cgroup/blkio/haoel/tasks 再用iotop命令，你马上就能看到读速度被限制到了1MB/s左右:\n1 2 TID PRIO USER DISK READ DISK WRITE SWAPIN IO\u0026gt; COMMAND 8128 be/4 root 973.20 K/s 0.00 B/s 0.00 % 94.41 % dd if=/de~=/dev/null... CGroup的子系统 OK，有了以上比较感性的认识，我们来看看control group到底有哪些子系统：\nblkio — 这个子系统为块设备设定输入/输出限制，比如物理设备（磁盘，固态硬盘，USB 等等）。\ncpu — 这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问。\ncpuacct — 这个子系统自动生成 cgroup 中任务所使用的 CPU 报告。\ncpuset — 这个子系统为 cgroup 中的任务分配独立 CPU（在多核系统）和内存节点。\ndevices — 这个子系统可允许或者拒绝 cgroup 中的任务访问设备。\nfreezer — 这个子系统挂起或者恢复 cgroup 中的任务。\nmemory — 这个子系统设定 cgroup 中任务使用的内存限制，并自动生成内存资源使用报告。\nnet_cls — 这个子系统使用等级识别符（classid）标记网络数据包，可允许 Linux 流量控制程序（tc）识别从具体 cgroup 中生成的数据包。\nnet_prio — 这个子系统用来设计网络流量的优先级\nhugetlb — 这个子系统主要针对于HugeTLB系统进行限制，这是一个大页文件系统。\n在我的Ubuntu14.04虚拟机下看不到net_cls和net_prio这两个cgroup，需要手动mount：\n1 2 3 4 5 6 7 $ sudo modprobe cls_cgroup $ sudo mkdir /sys/fs/cgroup/net_cls $ sudo mount -t cgroup -o net_cls none /sys/fs/cgroup/net_cls $ sudo modprobe netprio_cgroup $ sudo mkdir /sys/fs/cgroup/net_prio $ sudo mount -t cgroup -o net_prio none /sys/fs/cgroup/net_prio CGroup术语 CGroup有下述术语：\n任务（Tasks）：就是系统的一个进程。 控制组（Control Group）：一组按照某种标准划分的进程，比如官方文档中的Professor和Student，或是WWW和System之类的，其表示了某进程组。Cgroups中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组。而资源的限制是定义在这个组上，就像上面示例中我用的haoel一样。简单点说，cgroup的呈现就是一个目录带一系列的可配置文件。 层级（Hierarchy）：控制组可以组织成hierarchical的形式，既一颗控制组的树（目录结构）。控制组树上的子节点继承父结点的属性。简单点说，hierarchy就是在一个或多个子系统上的cgroups目录树。 子系统（Subsystem）：一个子系统就是一个资源控制器，比如CPU子系统就是控制CPU时间分配的一个控制器。子系统必须附加到一个层级上才能起作用，一个子系统附加到某个层级以后，这个层级上的所有控制族群都受到这个子系统的控制。Cgroup的子系统可以有很多，也在不断增加中。 ","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/docker-cgroup/","summary":"\u003cp\u003eLinux Namespace的技术解决了环境隔离的问题，不过这是虚拟化最基本的一步，我们另外需要解决\u003cstrong\u003e对计算机资源使用上的隔离\u003c/strong\u003e。说人话，就是虽然Namespace把我关到一个特定的环境，但是里面进程使用的CPU、内存、磁盘等计算资源实际上没有被限制。这个问题的解决，就要用到CGroup技术。\u003c/p\u003e\n\u003cp\u003eLinux CGroup全称是Linux Control Group，也是其内核的一个功能，用于限制、控制和分离一个进程group的资源。最早这个项目是2006年由谷歌的工程师发起的，最开始名称是process containers（工程容器），后面觉得内核中容器这个名词被用烂了，就改名为cgroup。\u003c/p\u003e\n\u003cp\u003eCGroup可以让你对系统中运行的进程的用户组分配资源-CPU时间、系统内存、网络带宽亦或者是这些的组合。同时，也可以监控你配置的cgroup，拒绝cgroup访问某些资源。主要提供的功能如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eResource Limitation\u003c/code\u003e： 限制资源使用\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003ePrioritization\u003c/code\u003e: 优先级控制，例如CPU使用和磁盘IO吞吐\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eAccounting\u003c/code\u003e：审计统计，主要用于计费\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eControl\u003c/code\u003e：挂起进程，恢复执行进程\u003c/p\u003e\n\u003cp\u003e在真正的实践当中，system admin一般会利用CGroup做以下的事：\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e对进程集合进行隔离，限制他们所消费的资源，例如绑定CPU core\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e为这组进程分配足够使用的内存\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e为这组进程分配响应的网络带宽和磁盘存储限制\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e限制访问某些设备（白名单）\u003c/p\u003e\n\u003cp\u003eLinux实际上把CGroup实现成了一个文件系统，你可以mount。在linux环境输入下面的可以看到cgroup已经为你mount好：\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ederios@ubuntu:~$ mount -t cgroup\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/cpuset \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,cpuset\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/cpu \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,cpu\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/cpuacct \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,cpuacct\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/memory \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,memory\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/devices \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,devices\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/freezer \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,freezer\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/blkio \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,blkio\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/net_prio \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,net_prio\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/net_cls \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,net_cls\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/perf_event \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,perf_event\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecgroup on /sys/fs/cgroup/hugetlb \u003cspan class=\"nb\"\u003etype\u003c/span\u003e cgroup \u003cspan class=\"o\"\u003e(\u003c/span\u003erw,relatime,hugetlb\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e可以看到，在\u003ccode\u003e/sys/fs\u003c/code\u003e下有\u003ccode\u003ecgroup\u003c/code\u003e目录，这个目录下面有各种子目录：\u003ccode\u003ecpu\u003c/code\u003e，\u003ccode\u003ecpuset\u003c/code\u003e，\u003ccode\u003ememory\u003c/code\u003e\u0026hellip;。这些都是cgroup的子系统，分别用来干不同的事。\u003c/p\u003e","title":"Docker Fundamentals: Cgroup"},{"content":"容器技术出现已经很久，只不过Docker容器平台的出现它变火了。Docker是第一个让容器能在不同机器之间移植的系统，它简化了打包应用的流程，也简化了打包应用的库和各种依赖。思考下整个OS的file system能直接被打包成一个简单的可移植的包，一开始的时候概念上还是很有趣的。\n有时候我认为自己的阅读比较碎片化(short-term memory越来越少)，所以我想把之前学习容器知识的一些基础技术再整理出来，也算是给自己学习的反馈。这个基础系列从Linux Namespace开始，后续会陆续介绍比如cgroup、aufs、devicemapper等技术。\n参考 Namespace in operation\nLinux namespace man page\nIntroduction to linux namespace\n什么是Namespace 简单来说，linux namespace是Linux提供的一种内核级别环境隔离的方法。在早期的Unix中，提供了一种叫做chroot的系统调用：通过修改root目录把用户关到一个特定的目录下面。这种就是简单的隔离方式，也就是chroot内部的file system无法访问外部的内容。Linux Namespace在此基础之上，提供了对UTS、IPC、mount、network、PID、User等隔离机制。\n这里可以简单举例，比如Linux的超级父进程的PID为1，如果我们可以把用户的进程空间关到某个进程分支之下，并且像chroot那样能够让下面的进程看到那个超级父进程的PID为1，而不同PID Namespace中的进程无法看到彼此，这样就能达到进程隔离。\nLinux Namespace有以下的种类，供给后续参考（刚看有个印象就行）：\n分类 系统调用参数 相关内核版本 Mount namespaces CLONE_NEWNS Linux 2.4.19 UTS namespaces CLONE_NEWUTS Linux 2.6.19 IPC namespaces CLONE_NEWIPC Linux 2.6.19 PID namespaces CLONE_NEWPID Linux 2.6.24 Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29 User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8) 其主要涉及到三个系统调用：\nclone()： 实现线程的系统调用，用来创建新的线程，并可通过涉及上述参数做到隔离 unshare()： 让某一个线程脱离某namespace setns(): 把某一个线程加到某namespace 如果读者你想看具体的实例，请自己man一下(关注一下自己的linux虚拟机内核)，或者google一下，我这里贴一个clone()的source code：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #define _GNU_SOURCE #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/wait.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;sched.h\u0026gt; #include \u0026lt;signal.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; /* 定义一个给 clone 用的栈，栈大小1M */ #define STACK_SIZE (1024 * 1024) static char container_stack[STACK_SIZE]; char* const container_args[] = { \u0026#34;/bin/bash\u0026#34;, NULL }; int container_main(void* arg) { printf(\u0026#34;Container - inside the container!\\n\u0026#34;); /* 直接执行一个shell，以便我们观察这个进程空间里的资源是否被隔离了 */ execv(container_args[0], container_args); printf(\u0026#34;Something\u0026#39;s wrong!\\n\u0026#34;); return 1; } int main() { printf(\u0026#34;Parent - start a container!\\n\u0026#34;); /* 调用clone函数，其中传出一个函数，还有一个栈空间的（为什么传尾指针，因为栈是反着的） */ int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL); /* 等待子进程结束 */ waitpid(container_pid, NULL, 0); printf(\u0026#34;Parent - container stopped!\\n\u0026#34;); return 0; } 上面的程序注释写的比较明白，和pthreads差不多。不过这个程序里，父子进程的进程空间没有什么区别，父进程能访问到的明显子进程也能访问。\n我们用几个例子来看看linux的namespace到底是啥样的，运行的虚拟机为ubuntu14.4\nUTS Namespace 这里略去一些头文件和数据结构的定义：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int container_main(void* arg) { printf(\u0026#34;Container - inside the container!\\n\u0026#34;); sethostname(\u0026#34;container\u0026#34;,10); /* 设置hostname */ execv(container_args[0], container_args); printf(\u0026#34;Something\u0026#39;s wrong!\\n\u0026#34;); return 1; } int main() { printf(\u0026#34;Parent - start a container!\\n\u0026#34;); int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */ waitpid(container_pid, NULL, 0); printf(\u0026#34;Parent - container stopped!\\n\u0026#34;); return 0; } 执行上述的c++程序，会发现子进程的hostname变为了container\n1 2 3 4 5 6 7 derios@ubuntu:~$ sudo ./uts Parent - start a container! Container - inside the container! root@container:~# hostname container root@container:~# uname -n container IPC Namespace IPC(Inter-Process Communication)，是Unix/Linux下的一种通信方式。IPC有共享内存、信号量、消息队列等方法。所以如果要隔离我们也要把IPC进行隔离。换句话说，这样可以保证只有在同一个namespace下的进程之间才能互相通信。目前我对IPC的原理没什么研究，查了查资料，IPC需要有个全局的ID，那么如果我们要做隔离，namespace肯定需要对这个全局ID进行隔离，不能和其他namespace中的进程共享。\n要启动IPC隔离，我们需要在clone时加上CLONE_NEWPIC的参数:\n1 2 int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL); 我们先创建一个IPC的queue，下面的全局ID为0：\n1 2 3 4 5 6 derios@ubuntu:~$ ipcmk -Q Message queue id: 0 derios@ubuntu:~$ ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 0xd0d56eb2 0 hchen 644 0 0 如果我们不加CLONE_NEWIPC参数运行程序，我们可以看到在子进程中还是能看到全局的IPC queue：\n1 2 3 4 5 6 7 derios@ubuntu:~$ sudo ./uts Parent - start a container! Container - inside the container! root@container:~# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 0xd0d56eb2 0 hchen 644 0 0 如果我们运行加上了CLONE_NEWIPC的程序，可以有如下的结果:\n1 2 3 4 5 6 root@ubuntu:~$ sudo./ipc Parent - start a container! Container - inside the container! root@container:~/linux_namespace# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 可见IPC已经被隔离。\nPID Namespace 我们继续修改上述的程序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int container_main(void* arg) { /* 查看子进程的PID，我们可以看到其输出子进程的 pid 为 1 */ printf(\u0026#34;Container [%5d] - inside the container!\\n\u0026#34;, getpid()); sethostname(\u0026#34;container\u0026#34;,10); execv(container_args[0], container_args); printf(\u0026#34;Something\u0026#39;s wrong!\\n\u0026#34;); return 1; } int main() { printf(\u0026#34;Parent [%5d] - start a container!\\n\u0026#34;, getpid()); /*启用PID namespace - CLONE_NEWPID*/ int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); waitpid(container_pid, NULL, 0); printf(\u0026#34;Parent - container stopped!\\n\u0026#34;); return 0; } 运行看一下，发现子进程的PID为1：\n1 2 3 4 5 derios@ubuntu:~$ sudo ./pid Parent [ 3474] - start a container! Container [ 1] - inside the container! root@container:~# echo $$ 1 这里的1有啥意义，你可能会问。其实在传统UNIX系统中，PID为1的进程地位比较特殊，指代init\n，作为所有进程的父进程，有非常多的特权（信号屏蔽etc.），此外它还会检查所有进程的状态，而且如果子进程脱离了父进程（父进程没有wait它），那么init会负责回收资源并结束这个子进程。所以，要做到进程空间的隔离，首先要创建pid为1的进程，比如可以像chroot一样，把子进程的pid在容器内变为1。\n不过，很奇怪的是，**我们在子进程的shell里执行top, ps等命令，还是可以看到所有的进程。**这意味着隔离并没有完全。因为像ps, top这些命令会读取/proc文件系统，而因为/proc文件系统在父子进程里都是一样的，所以命令的回显也都是一样的。\n因此，我们还要做到对文件系统的隔离。\nMount Namespace 下面的程序，我们在启用mount namespace并在子进程中重新mount了/proc文件系统。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int container_main(void* arg) { printf(\u0026#34;Container [%5d] - inside the container!\\n\u0026#34;, getpid()); sethostname(\u0026#34;container\u0026#34;,10); /* 重新mount proc文件系统到 /proc下 */ system(\u0026#34;mount -t proc proc /proc\u0026#34;); execv(container_args[0], container_args); printf(\u0026#34;Something\u0026#39;s wrong!\\n\u0026#34;); return 1; } int main() { printf(\u0026#34;Parent [%5d] - start a container!\\n\u0026#34;, getpid()); /* 启用Mount Namespace - 增加CLONE_NEWNS参数 */ int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL); waitpid(container_pid, NULL, 0); printf(\u0026#34;Parent - container stopped!\\n\u0026#34;); return 0; } 执行结果如下:\n1 2 3 4 5 6 7 derioshen@ubuntu:~$ sudo ./pid.mnt Parent [ 3502] - start a container! Container [ 1] - inside the container! root@container:~# ps -elf F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD 4 S root 1 0 0 80 0 - 6917 wait 19:55 pts/2 00:00:00 /bin/bash 0 R root 14 1 0 80 0 - 5671 - 19:56 pts/2 00:00:00 ps -elf 我们看到只有2个进程了，pid=1的是我们的/bin/bash，同时再看看/proc目录，也变得比较干净:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@container:~# ls /proc 1 dma key-users net sysvipc 16 driver kmsg pagetypeinfo timer_list acpi execdomains kpagecount partitions timer_stats asound fb kpageflags sched_debug tty buddyinfo filesystems loadavg schedstat uptime bus fs locks scsi version cgroups interrupts mdstat self version_signature cmdline iomem meminfo slabinfo vmallocinfo consoles ioports misc softirqs vmstat cpuinfo irq modules stat zoneinfo crypto kallsyms mounts swaps devices kcore mpt sys diskstats keys mtrr sysrq-trigger 通过CLONE_NEWNS创建mount namespace以后，父进程会把自己的文件结构复制给子进程。而子进程中新的namespace中所有mount操作都只会影响自身的文件系统，不会对外界产生任何影响，这就做到了严格的隔离。\n那么我们是不是还有别的一些文件系统也要mount？答案是肯定的。\nDocker的Mount Namespace 我们可以简单搞个小的镜像，这种玩法是我google参考来的，模仿docker的mount namespace。\n首先，我们需要一个rootfs， 也就是我们需要把我们要做的镜像中的那些命令什么的copy到一个rootfs的目录下，我们模仿Linux构建如下的目录：\n1 2 derios@ubuntu:~/rootfs$ ls bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var 然后，我们把一些我们需要的命令copy到 rootfs/bin目录中（sh命令必需要copy进去，不然我们无法 chroot ）：\n1 2 3 4 5 6 7 8 9 10 derios@ubuntu:~/rootfs$ ls ./bin ./usr/bin ./bin: bash chown gzip less mount netstat rm tabs tee top tty cat cp hostname ln mountpoint ping sed tac test touch umount chgrp echo ip ls mv ps sh tail timeout tr uname chmod grep kill more nc pwd sleep tar toe truncate which ./usr/bin: awk env groups head id mesg sort strace tail top uniq vi wc xargs 注：你可以使用ldd命令把这些命令相关的那些so文件copy到对应的目录：\n1 2 3 4 5 6 derios@ubuntu:~/rootfs/bin$ ldd bash linux-vdso.so.1 =\u0026gt; (0x00007fffd33fc000) libtinfo.so.5 =\u0026gt; /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f4bd42c2000) libdl.so.2 =\u0026gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f4bd40be000) libc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4bd3cf8000) /lib64/ld-linux-x86-64.so.2 (0x00007f4bd4504000) 下面是我的rootfs中的一些so文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 derios@ubuntu:~/rootfs$ ls ./lib64 ./lib/x86_64-linux-gnu/ ./lib64: ld-linux-x86-64.so.2 ./lib/x86_64-linux-gnu/: libacl.so.1 libmemusage.so libnss_files-2.19.so libpython3.4m.so.1 libacl.so.1.1.0 libmount.so.1 libnss_files.so.2 libpython3.4m.so.1.0 libattr.so.1 libmount.so.1.1.0 libnss_hesiod-2.19.so libresolv-2.19.so libblkid.so.1 libm.so.6 libnss_hesiod.so.2 libresolv.so.2 libc-2.19.so libncurses.so.5 libnss_nis-2.19.so libselinux.so.1 libcap.a libncurses.so.5.9 libnss_nisplus-2.19.so libtinfo.so.5 libcap.so libncursesw.so.5 libnss_nisplus.so.2 libtinfo.so.5.9 libcap.so.2 libncursesw.so.5.9 libnss_nis.so.2 libutil-2.19.so libcap.so.2.24 libnsl-2.19.so libpcre.so.3 libutil.so.1 libc.so.6 libnsl.so.1 libprocps.so.3 libuuid.so.1 libdl-2.19.so libnss_compat-2.19.so libpthread-2.19.so libz.so.1 libdl.so.2 libnss_compat.so.2 libpthread.so.0 libgpm.so.2 libnss_dns-2.19.so libpython2.7.so.1 libm-2.19.so libnss_dns.so.2 libpython2.7.so.1.0 包括这些命令依赖的一些配置文件：\n1 2 3 derios@ubuntu:~/rootfs$ ls ./etc bash.bashrc group hostname hosts ld.so.cache nsswitch.conf passwd profile resolv.conf shadow 看到现在你可能比较懵逼，有的比较熟悉os的同学也可能会问：有的配置希望是在容器起动时给他设置的，而不是hard code在镜像中的。比如：/etc/hosts，/etc/hostname，还有DNS的/etc/resolv.conf文件。OK, 那我们在rootfs外面，我们再创建一个conf目录，把这些文件放到这个目录中:\n1 2 derios@ubuntu:~$ ls ./conf hostname hosts resolv.conf 这样，我们的父进程就可以动态地设置容器需要的这些文件的配置， 然后再把他们mount进容器，这样，容器的镜像中的配置就比较灵活了。\n接下来是程序(Google真好)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #define _GNU_SOURCE #include \u0026lt;sys types.h=\u0026#34;\u0026#34;\u0026gt; #include \u0026lt;sys wait.h=\u0026#34;\u0026#34;\u0026gt; #include \u0026lt;sys mount.h=\u0026#34;\u0026#34;\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;sched.h\u0026gt; #include \u0026lt;signal.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define STACK_SIZE (1024 * 1024) static char container_stack[STACK_SIZE]; char* const container_args[] = { \u0026#34;/bin/bash\u0026#34;, \u0026#34;-l\u0026#34;, NULL }; int container_main(void* arg) { printf(\u0026#34;Container [%5d] - inside the container!\\n\u0026#34;, getpid()); //set hostname sethostname(\u0026#34;container\u0026#34;,10); //remount \u0026#34;/proc\u0026#34; to make sure the \u0026#34;top\u0026#34; and \u0026#34;ps\u0026#34; show container\u0026#39;s information if (mount(\u0026#34;proc\u0026#34;, \u0026#34;rootfs/proc\u0026#34;, \u0026#34;proc\u0026#34;, 0, NULL) !=0 ) { perror(\u0026#34;proc\u0026#34;); } if (mount(\u0026#34;sysfs\u0026#34;, \u0026#34;rootfs/sys\u0026#34;, \u0026#34;sysfs\u0026#34;, 0, NULL)!=0) { perror(\u0026#34;sys\u0026#34;); } if (mount(\u0026#34;none\u0026#34;, \u0026#34;rootfs/tmp\u0026#34;, \u0026#34;tmpfs\u0026#34;, 0, NULL)!=0) { perror(\u0026#34;tmp\u0026#34;); } if (mount(\u0026#34;udev\u0026#34;, \u0026#34;rootfs/dev\u0026#34;, \u0026#34;devtmpfs\u0026#34;, 0, NULL)!=0) { perror(\u0026#34;dev\u0026#34;); } if (mount(\u0026#34;devpts\u0026#34;, \u0026#34;rootfs/dev/pts\u0026#34;, \u0026#34;devpts\u0026#34;, 0, NULL)!=0) { perror(\u0026#34;dev/pts\u0026#34;); } if (mount(\u0026#34;shm\u0026#34;, \u0026#34;rootfs/dev/shm\u0026#34;, \u0026#34;tmpfs\u0026#34;, 0, NULL)!=0) { perror(\u0026#34;dev/shm\u0026#34;); } if (mount(\u0026#34;tmpfs\u0026#34;, \u0026#34;rootfs/run\u0026#34;, \u0026#34;tmpfs\u0026#34;, 0, NULL)!=0) { perror(\u0026#34;run\u0026#34;); } /* * 模仿Docker的从外向容器里mount相关的配置文件 * 你可以查看：/var/lib/docker/containers/\u0026lt;container_id\u0026gt;/目录， * 你会看到docker的这些文件的。 */ if (mount(\u0026#34;conf/hosts\u0026#34;, \u0026#34;rootfs/etc/hosts\u0026#34;, \u0026#34;none\u0026#34;, MS_BIND, NULL)!=0 || mount(\u0026#34;conf/hostname\u0026#34;, \u0026#34;rootfs/etc/hostname\u0026#34;, \u0026#34;none\u0026#34;, MS_BIND, NULL)!=0 || mount(\u0026#34;conf/resolv.conf\u0026#34;, \u0026#34;rootfs/etc/resolv.conf\u0026#34;, \u0026#34;none\u0026#34;, MS_BIND, NULL)!=0 ) { perror(\u0026#34;conf\u0026#34;); } /* 模仿docker run命令中的 -v, --volume=[] 参数干的事 */ if (mount(\u0026#34;/tmp/t1\u0026#34;, \u0026#34;rootfs/mnt\u0026#34;, \u0026#34;none\u0026#34;, MS_BIND, NULL)!=0) { perror(\u0026#34;mnt\u0026#34;); } /* chroot 隔离目录 */ if ( chdir(\u0026#34;./rootfs\u0026#34;) != 0 || chroot(\u0026#34;./\u0026#34;) != 0 ){ perror(\u0026#34;chdir/chroot\u0026#34;); } execv(container_args[0], container_args); perror(\u0026#34;exec\u0026#34;); printf(\u0026#34;Something\u0026#39;s wrong!\\n\u0026#34;); return 1; } int main() { printf(\u0026#34;Parent [%5d] - start a container!\\n\u0026#34;, getpid()); int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL); waitpid(container_pid, NULL, 0); printf(\u0026#34;Parent - container stopped!\\n\u0026#34;); return 0; } \u0026lt;/container_id\u0026gt;\u0026lt;/unistd.h\u0026gt;\u0026lt;/signal.h\u0026gt;\u0026lt;/sched.h\u0026gt;\u0026lt;/stdio.h\u0026gt;\u0026lt;/sys\u0026gt;\u0026lt;/sys\u0026gt;\u0026lt;/sys\u0026gt; sudo运行上面的程序，你会看到下面的挂载信息以及一个所谓的“镜像”：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 derios@ubuntu:~$ sudo ./mount Parent [ 4517] - start a container! Container [ 1] - inside the container! root@container:/# mount proc on /proc type proc (rw,relatime) sysfs on /sys type sysfs (rw,relatime) none on /tmp type tmpfs (rw,relatime) udev on /dev type devtmpfs (rw,relatime,size=493976k,nr_inodes=123494,mode=755) devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000) tmpfs on /run type tmpfs (rw,relatime) /dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered) /dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered) /dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered) root@container:/# ls /bin /usr/bin /bin: bash chmod echo hostname less more mv ping rm sleep tail test top truncate uname cat chown grep ip ln mount nc ps sed tabs tar timeout touch tty which chgrp cp gzip kill ls mountpoint netstat pwd sh tac tee toe tr umount /usr/bin: awk env groups head id mesg sort strace tail top uniq vi wc xargs User Namespace User Namespace 主要用到了CLONE_NEWUSER参数，当我们使用这个参数以后，在内部看到的UID和GID和外部就不一样了，默认为65534。因为容器找不到其真正的UID，因此设置成了最大的UID（/proc/sys/kernel/overflowuid）。\n要把容器中的uid和真实系统的uid给映射在一起，需要修改 /proc//uid_map 和 /proc//gid_map 这两个文件。这两个文件的格式为：\nID-inside-ns ID-outside-ns length\n第一个字段ID-inside-ns表示在容器显示的UID或GID， 第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。 第三个字段表示映射的范围，一般填1，表示一一对应。 比如，把真实的uid=1000映射成容器内的uid=0\n1 2 $ cat /proc/2465/uid_map 0 1000 1 再比如下面的示例：表示把namespace内部从0开始的uid映射到外部从0开始的uid，其最大范围是无符号32位整形：\n1 2 $ cat /proc/$$/uid_map 0 0 4294967295 写这两个文件的进程需要这个namespace中的CAP_SETUID (CAP_SETGID)权限（可参看Capabilities） 写入的进程必须是此user namespace的父或子的user namespace进程。 另外需要满如下条件之一：1）父进程将effective uid/gid映射到子进程的user namespace中，2）父进程如果有CAP_SETUID/CAP_SETGID权限，那么它将可以映射到父进程中的任一uid/gid。 User Namespace是以普通用户运行，但是别的Namespace需要root权限，那么，如果我要同时使用多个Namespace，该怎么办呢？一般来说，我们先用一般用户创建User Namespace，然后把这个一般用户映射成root，在容器内用root来创建其它的Namesapce。\nNetwork Namespace 在linux中，一般用ip命令创建network namespace。不过在docker源码中并没有使用ip，而是自己实现了ip命令的一些内容。在这还是用ip命令描述一下做了啥。\n首先我们来看一个图，这个图是Docker在host主机上的网络示意图\n实际上图还是有问题的，因为Docker也可以运行在虚拟机中，所以所谓的物理网卡其实也就是一个有能够路由的IP的网卡。\n图中Docker用了一个私有的网段: 172.40.1.0，此外docker还会使用10.0.0.0以及192.168.0.0两个私有网段。如果你机器的路由表配置了(占用)所有的私有网段，那么docker就会无法启动。\n启动docker以后，可以使用ip link show和ip addr show来查看目前宿主机的网络情况。这里我在minikube容器里执行了指令:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 root@minikube:/# ip link show 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: tunl0@NONE: \u0026lt;NOARP\u0026gt; mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ipip 0.0.0.0 brd 0.0.0.0 3: ip6tnl0@NONE: \u0026lt;NOARP\u0026gt; mtu 1452 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/tunnel6 :: brd :: 4: docker0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:2e:11:a4:ae brd ff:ff:ff:ff:ff:ff 6: vetha68cfee@if5: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 46:2d:cf:22:79:9f brd ff:ff:ff:ff:ff:ff link-netnsid 1 12: veth8b65072@if11: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 3e:fb:54:6d:ae:1e brd ff:ff:ff:ff:ff:ff link-netnsid 3 14: veth61918b0@if13: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether d2:68:38:76:73:c9 brd ff:ff:ff:ff:ff:ff link-netnsid 4 18: vethd7fa219@if17: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 1e:14:79:68:f1:50 brd ff:ff:ff:ff:ff:ff link-netnsid 5 20: eth0@if21: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:c0:a8:31:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 21: veth3b477c9@if19: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 7e:b1:5d:53:00:ff brd ff:ff:ff:ff:ff:ff link-netnsid 2 23: veth265059d@if22: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 4a:22:a4:05:79:e1 brd ff:ff:ff:ff:ff:ff link-netnsid 6 可以看到有docker0还有一些虚拟网卡。\n为了能够做成这样，我又google了一段代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ## 首先，我们先增加一个网桥lxcbr0，模仿docker0 brctl addbr lxcbr0 brctl stp lxcbr0 off ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址 ## 接下来，我们要创建一个network namespace - ns1 # 增加一个namesapce 命令为 ns1 （使用ip netns add命令） ip netns add ns1 # 激活namespace中的loopback，即127.0.0.1（使用ip netns exec ns1来操作ns1中的命令） ip netns exec ns1 ip link set dev lo up ## 然后，我们需要增加一对虚拟网卡 # 增加一个pair虚拟网卡，注意其中的veth类型，其中一个网卡要按进容器中 ip link add veth-ns1 type veth peer name lxcbr0.1 # 把 veth-ns1 按到namespace ns1中，这样容器中就会有一个新的网卡了 ip link set veth-ns1 netns ns1 # 把容器里的 veth-ns1改名为 eth0 （容器外会冲突，容器内就不会了） ip netns exec ns1 ip link set dev veth-ns1 name eth0 # 为容器中的网卡分配一个IP地址，并激活它 ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up # 上面我们把veth-ns1这个网卡按到了容器中，然后我们要把lxcbr0.1添加上网桥上 brctl addif lxcbr0 lxcbr0.1 # 为容器增加一个路由规则，让容器可以访问外面的网络 ip netns exec ns1 ip route add default via 192.168.10.1 # 在/etc/netns下创建network namespce名称为ns1的目录， # 然后为这个namespace设置resolv.conf，这样，容器内就可以访问域名了 mkdir -p /etc/netns/ns1 echo \u0026#34;nameserver 8.8.8.8\u0026#34; \u0026gt; /etc/netns/ns1/resolv.conf 上述就是docker网络的原理，但是有几点要注意的\ndocker的resolv.conf（配置系统DNS解析器）没有采取这样的方式，而是采取类似上述mount namespace的方式 另外，docker采用进程的PID来做network namepspace的名称 我们原理了解了，甚至可以给正在运行的docker容器新增网卡:\n1 2 3 4 5 6 7 ip link add peerA type veth peer name peerB brctl addif docker0 peerA ip link set peerA up ip link set peerB netns ${container-pid} ip netns exec ${container-pid} ip link set dev peerB name eth1 ip netns exec ${container-pid} ip link set eth1 up ; ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ; 上述指令即为一个正在运行的docker容器新增一个eth1网卡，以及给了一个可以被外部访问到的IP静态IP地址。\n这种做法，需要把外部的物理网卡设置为混杂模式(Promiscuous Model)，也就是网卡接受所有流过网卡的帧(数据包)，包括那些不是发给本机的包，不验证MAC地址。这样这个eth1网卡就会向外部通过ARP地址解析协议发送自己的MAC地址，然后外部的交换机就会把这个IP地址的包转到物理网卡上。因为网卡工作在混杂模式，因此eth1就能收到相关的数据。如果发现是自己的数据，那么就接受，这样Docker容器的网络就与外部相通。\n其实不管是Docker的NAT模式，还是混杂模式都会存在性能问题。NAT很明显转发(NAT转换)就有开销，而混杂模式下，网卡收到的负载都会完全交给所有的虚拟网卡，所以想想哪怕一个网卡没有数据，也会被其他网卡的数据影响。\n因此这两种方式都不算完美，真正解决这样网络问题的是VLAN技术。因此Google的开发者为linux内核实现了一个IPVLAN驱动，基本为Docker量身定制。\nNamespace文件 整理完了linux namespace的玩法，在看一下ns的文件。\n我们再运行一遍PID Namepace篇章中的pid.mnt程序(mount proc)，然后不退出:\n1 2 3 4 $ sudo ./pid.mnt [sudo] password for derios: Parent [ 4599] - start a container! Container [ 1] - inside the container! 打开另外一个shell看一下父子进程的PID:\n1 2 derios@ubuntu:~$ pstree -p 4599 pid.mnt(4599)───bash(4600) 我们可以到proc下（/proc//ns）查看进程的各个namespace的id（内核版本需要3.8以上），下面是父进程的：\n1 2 3 4 5 6 7 8 derios@ubuntu:~$ sudo ls -l /proc/4599/ns total 0 lrwxrwxrwx 1 root root 0 4月 7 22:01 ipc -\u0026gt; ipc:[4026531839] lrwxrwxrwx 1 root root 0 4月 7 22:01 mnt -\u0026gt; mnt:[4026531840] lrwxrwxrwx 1 root root 0 4月 7 22:01 net -\u0026gt; net:[4026531956] lrwxrwxrwx 1 root root 0 4月 7 22:01 pid -\u0026gt; pid:[4026531836] lrwxrwxrwx 1 root root 0 4月 7 22:01 user -\u0026gt; user:[4026531837] lrwxrwxrwx 1 root root 0 4月 7 22:01 uts -\u0026gt; uts:[4026531838] 下面是子进程的:\n1 2 3 4 5 6 7 8 derios@ubuntu:~$ sudo ls -l /proc/4600/ns total 0 lrwxrwxrwx 1 root root 0 4月 7 22:01 ipc -\u0026gt; ipc:[4026531839] lrwxrwxrwx 1 root root 0 4月 7 22:01 mnt -\u0026gt; mnt:[4026532520] lrwxrwxrwx 1 root root 0 4月 7 22:01 net -\u0026gt; net:[4026531956] lrwxrwxrwx 1 root root 0 4月 7 22:01 pid -\u0026gt; pid:[4026532522] lrwxrwxrwx 1 root root 0 4月 7 22:01 user -\u0026gt; user:[4026531837] lrwxrwxrwx 1 root root 0 4月 7 22:01 uts -\u0026gt; uts:[4026532521] 仔细看一下区别，发现ipc, net, user为相同ID，而mnt, pid, uts都不同。如果两个进程指向的namespace编号相同，则说明它俩在同一个namespace下，否则就不在。（如果读者你想验证，docker exec -it \u0026lt;container name\u0026gt; bash到一个容器里，top找两个进程，然后cat一下proc中对应PID的ns即可）\n这些文件还有另一个作用，那就是，一旦这些文件被打开，只要其fd被占用着，那么就算PID所属的所有进程都已经结束，创建的namespace也会一直存在。比如：我们可以通过：mount –bind /proc/4600/ns/uts ~/uts 来hold这个namespace。\n我们在最开始点了一下setns系统调用，函数声明如下:\n1 int setns(int fd, int nstype); 其中第一个参数就是一个fd，也就是一个open()系统调用打开了上述文件后返回的fd，比如：\n1 2 fd = open(\u0026#34;/proc/4600/ns/nts\u0026#34;, O_RDONLY); // 获取namespace文件描述符 setns(fd, 0); // 加入新的namespace ","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/docker-namespace/","summary":"\u003cp\u003e容器技术出现已经很久，只不过Docker容器平台的出现它变火了。Docker是第一个让容器能在不同机器之间移植的系统，它简化了打包应用的流程，也简化了打包应用的库和各种依赖。思考下整个OS的file system能直接被打包成一个简单的可移植的包，一开始的时候概念上还是很有趣的。\u003c/p\u003e\n\u003cp\u003e有时候我认为自己的阅读比较碎片化(\u003cdel\u003eshort-term memory越来越少\u003c/del\u003e)，所以我想把之前学习容器知识的一些基础技术再整理出来，也算是给自己学习的反馈。这个基础系列从Linux Namespace开始，后续会陆续介绍比如cgroup、aufs、devicemapper等技术。\u003c/p\u003e\n\u003ch2 id=\"参考\"\u003e参考\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://lwn.net/Articles/531114/\"\u003eNamespace in operation\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://man7.org/linux/man-pages/man7/namespaces.7.html\"\u003eLinux namespace man page\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://blog.jtlebi.fr/2013/12/22/introduction-to-linux-namespaces-part-1-uts/\"\u003eIntroduction to linux namespace\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"什么是namespace\"\u003e什么是Namespace\u003c/h2\u003e\n\u003cp\u003e简单来说，linux namespace是Linux提供的一种内核级别环境隔离的方法。在早期的Unix中，提供了一种叫做chroot的系统调用：通过修改root目录把用户关到一个特定的目录下面。这种就是简单的隔离方式，也就是chroot内部的file system无法访问外部的内容。Linux Namespace在此基础之上，提供了对UTS、IPC、mount、network、PID、User等隔离机制。\u003c/p\u003e\n\u003cp\u003e这里可以简单举例，比如Linux的超级父进程的PID为1，如果我们可以把用户的进程空间关到某个进程分支之下，并且像chroot那样能够让下面的进程看到那个超级父进程的PID为1，而不同PID Namespace中的进程无法看到彼此，这样就能达到进程隔离。\u003c/p\u003e\n\u003cp\u003eLinux Namespace有以下的种类，供给后续参考（刚看有个印象就行）：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003e分类\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e系统调用参数\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e相关内核版本\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eMount namespaces\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eCLONE_NEWNS\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ca href=\"http://lwn.net/2001/0301/a/namespaces.php3\"\u003eLinux 2.4.19\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eUTS namespaces\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eCLONE_NEWUTS\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ca href=\"http://lwn.net/Articles/179345/\"\u003eLinux 2.6.19\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eIPC namespaces\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eCLONE_NEWIPC\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ca href=\"http://lwn.net/Articles/187274/\"\u003eLinux 2.6.19\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ePID namespaces\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eCLONE_NEWPID\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ca href=\"http://lwn.net/Articles/259217/\"\u003eLinux 2.6.24\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eNetwork namespaces\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eCLONE_NEWNET\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ca href=\"http://lwn.net/Articles/219794/\"\u003e始于Linux 2.6.24 完成于 Linux 2.6.29\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eUser namespaces\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eCLONE_NEWUSER\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ca href=\"http://lwn.net/Articles/528078/\"\u003e始于 Linux 2.6.23 完成于 Linux 3.8)\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e其主要涉及到三个系统调用：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eclone()\u003c/code\u003e： 实现线程的系统调用，用来创建新的线程，并可通过涉及上述参数做到隔离\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eunshare()\u003c/code\u003e： 让某一个线程脱离某namespace\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003esetns()\u003c/code\u003e: 把某一个线程加到某namespace\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果读者你想看具体的实例，请自己man一下(关注一下自己的linux虚拟机内核)，或者google一下，我这里贴一个\u003ccode\u003eclone()\u003c/code\u003e的source code：\u003c/p\u003e","title":"Docker Fundamentals: Namespace"},{"content":"概述 最近在看kubernetes的kubectl部分源码，记录一下其中用到的visitor编程模式(实际上kubectl主要用到了builder和visitor)。visitor模式是将算法和操作对象结构分离的一种方法。换句话说，这样的分离能够在不修改对象结构的情况下向原有对象新增操作，是符合开闭原则的。这个文章以一些例子去讨论kubectl中到底如何玩的。\n从一个例子出发 写一个简单的Visitor模式示例：\n我们的代码中有一个Visitor的函数定义，还有一个Shape接口，其需要使用 Visitor函数做为参数 我们的实例的对象 Circle和 Rectangle实现了 Shape 的接口的 accept() 方法，这个方法就是等外面给我传递一个Visitor。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;encoding/xml\u0026#34; \u0026#34;fmt\u0026#34; ) type Visitor func(shape Shape) type Shape interface { accept(Visitor) } type Circle struct { Radius int } func (c Circle) accept(v Visitor) { v(c) } type Rectangle struct { Width, Heigh int } func (r Rectangle) accept(v Visitor) { v(r) } 然后，我们实现两个Visitor，一个是用来做JSON序列化的，另一个是用来做XML序列化的:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func JsonVisitor(shape Shape) { bytes, err := json.Marshal(shape) if err != nil { panic(err) } fmt.Println(string(bytes)) } func XmlVisitor(shape Shape) { bytes, err := xml.Marshal(shape) if err != nil { panic(err) } fmt.Println(string(bytes)) } 下面是我们的使用Visitor这个模式的代码：\n1 2 3 4 5 6 7 8 9 func main() { c := Circle{10} r := Rectangle{100, 200} shapes := []Shape{c, r} for _, s := range shapes { s.accept(JsonVisitor) s.accept(XmlVisitor) } } 写这些代码的目的是为了解耦数据结构和算法，其实用Strategy模式也可以做到，在模式上也更简单点。但是需要注意的一点：在有些情况下，多个Visitor是来访问一个数据结构的不同部分，这种情况下，数据结构有点像一个数据库，而各个Visitor会成为一个个小应用。那么kubectl无疑是这样的场景。\nk8s一些背景 在博客的kubernetes \u0026amp; docker的专栏里，介绍了k8s的一些基本知识。其实对于k8s来说，其抽象出了很多资源Resource：Pod，ReplicaSet，ConfigMap，Volumes，Namespace, Roles\u0026hellip;等等。而这些构成了k8s的数据模型( Kubernetes Resources 地图)\nkubectl为k8s的客户端命令，其对接Kubernetes API Server，开发和运维通过此去和k8s进行交互。而API Server则联系到每个节点的kubelet控制每个节点。\nkubectl主要的工作就是处理用户提交的例如：命令行参数、yaml/json文件等。将用户提交的这些组织成数据结构体，发送给API Server。\n源码：src/k8s.io/cli-runtime/pkg/resource/visitor.go (链接)\n当然kubectl的源码复杂，用简单的话阐述其基本原理就是：它从命令行和yaml文件中获取信息，通过Builder模式并把其转成一系列的资源，最后用 Visitor 模式模式来迭代处理这些Reources。\n我先用一个小的例子来说明，忽略掉很多复杂的代码逻辑\nkubectl的实现 Visitor模式的定义 首先，kubectl 主要是用来处理 Info结构体，下面是相关的定义：\n1 2 3 4 5 6 7 8 9 10 11 12 type VisitorFunc func(*Info, error) error type Visitor interface { Visit(VisitorFunc) error } type Info struct { Namespace string Name string OtherThings string } func (info *Info) Visit(fn VisitorFunc) error { return fn(info, nil) } 上述拆解一下：\n有一个VisitorFunc函数类型的定义 Visitor接口，需要实现一个Visit(VisitorFunc) error的方法 最后，为Info实现Visitor接口中的Visit()方法，其就是直接调用传进来的fn 接下来再定义几种不同类型的Visitor\nName Visitor 这个Visitor 主要是用来访问 Info 结构中的 Name 和 NameSpace 成员\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 type NameVisitor struct { visitor Visitor } func (v NameVisitor) Visit(fn VisitorFunc) error { return v.visitor.Visit(func(info *Info, err error) error { fmt.Println(\u0026#34;NameVisitor() before call function\u0026#34;) err = fn(info, err) if err == nil { fmt.Printf(\u0026#34;==\u0026gt; Name=%s, NameSpace=%s\\n\u0026#34;, info.Name, info.Namespace) } fmt.Println(\u0026#34;NameVisitor() after call function\u0026#34;) return err }) } 拆解代码，可以看到:\n声明了一个NameVistor结构体，多态得加了一个Visitor接口成员 实现Visit()方法时，调用内部Visitor的Visit()方法，这也是一种修饰器模式。 Other Visitor 这个Visitor主要用来访问 Info 结构中的 OtherThings 成员\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 type OtherThingsVisitor struct { visitor Visitor } func (v OtherThingsVisitor) Visit(fn VisitorFunc) error { return v.visitor.Visit(func(info *Info, err error) error { fmt.Println(\u0026#34;OtherThingsVisitor() before call function\u0026#34;) err = fn(info, err) if err == nil { fmt.Printf(\u0026#34;==\u0026gt; OtherThings=%s\\n\u0026#34;, info.OtherThings) } fmt.Println(\u0026#34;OtherThingsVisitor() after call function\u0026#34;) return err }) } Log Visitor 1 2 3 4 5 6 7 8 9 10 11 type LogVisitor struct { visitor Visitor } func (v LogVisitor) Visit(fn VisitorFunc) error { return v.visitor.Visit(func(info *Info, err error) error { fmt.Println(\u0026#34;LogVisitor() before call function\u0026#34;) err = fn(info, err) fmt.Println(\u0026#34;LogVisitor() after call function\u0026#34;) return err }) } 如何使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { info := Info{} var v Visitor = \u0026amp;info v = LogVisitor{v} v = NameVisitor{v} v = OtherThingsVisitor{v} loadFile := func(info *Info, err error) error { info.Name = \u0026#34;Hao Chen\u0026#34; info.Namespace = \u0026#34;MegaEase\u0026#34; info.OtherThings = \u0026#34;We are running as remote team.\u0026#34; return nil } v.Visit(loadFile) } 拆解上述代码：\nVisitor为嵌套式的 LoadFile模拟读取文件数据 最后一条v.Visit()激活上述流程 上述的代码输出如下:\n1 2 3 4 5 6 7 8 LogVisitor() before call function NameVisitor() before call function OtherThingsVisitor() before call function ==\u0026gt; OtherThings=We are running as remote team. OtherThingsVisitor() after call function ==\u0026gt; Name=Hao Chen, NameSpace=MegaEase NameVisitor() after call function LogVisitor() after call function 我们可以看到，这种做法实现了几点功能：\n解耦了数据和算法程序 使用修饰器模式 有pipeline模式的味道 我们接下来再以修饰器模式重构下上述代码\nVisitor修饰器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type DecoratedVisitor struct { visitor Visitor decorators []VisitorFunc } func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor { if len(fn) == 0 { return v } return DecoratedVisitor{v, fn} } // Visit implements Visitor func (v DecoratedVisitor) Visit(fn VisitorFunc) error { return v.visitor.Visit(func(info *Info, err error) error { if err != nil { return err } if err := fn(info, nil); err != nil { return err } for i := range v.decorators { if err := v.decorators[i](info, nil); err != nil { return err } } return nil }) } 上述代码，实际上做了以下几点事情:\n以DecoratedVisitor结构存放所有的VisitorFunc NewDecoratedVisitor把所有的VisitorFunc传进去，构造DecoratedVisitor对象 DecoratedVisitor实现了Visit()方法，里面实际上就是个for-loop，以非嵌套的方式调用所有的VisitorFunc 所以我们可以这么使用这个重构：\n1 2 3 4 info := Info{} var v Visitor = \u0026amp;info v = NewDecoratedVisitor(v, NameVisitor, OtherVisitor) v.Visit(LoadFile) 这样看上去能简单很多。\n基本上如果读懂了上述的逻辑，kubectl的代码也差不多能看明白。\n","permalink":"https://pillumina.github.io/posts/programming/design-pattern/go-visitor/","summary":"\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003e最近在看kubernetes的\u003ccode\u003ekubectl\u003c/code\u003e部分源码，记录一下其中用到的visitor编程模式(实际上\u003ccode\u003ekubectl\u003c/code\u003e主要用到了builder和visitor)。visitor模式是将\u003cstrong\u003e算法和操作对象结构分离\u003c/strong\u003e的一种方法。换句话说，这样的分离能够在不修改对象结构的情况下向原有对象新增操作，是符合开闭原则的。这个文章以一些例子去讨论\u003ccode\u003ekubectl\u003c/code\u003e中到底如何玩的。\u003c/p\u003e\n\u003ch2 id=\"从一个例子出发\"\u003e从一个例子出发\u003c/h2\u003e\n\u003cp\u003e写一个简单的Visitor模式示例：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e我们的代码中有一个\u003ccode\u003eVisitor\u003c/code\u003e的函数定义，还有一个\u003ccode\u003eShape\u003c/code\u003e接口，其需要使用 \u003ccode\u003eVisitor\u003c/code\u003e函数做为参数\u003c/li\u003e\n\u003cli\u003e我们的实例的对象 \u003ccode\u003eCircle\u003c/code\u003e和 \u003ccode\u003eRectangle\u003c/code\u003e实现了 \u003ccode\u003eShape\u003c/code\u003e 的接口的 \u003ccode\u003eaccept()\u003c/code\u003e 方法，这个方法就是等外面给我传递一个Visitor。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003emain\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;encoding/json\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;encoding/xml\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;fmt\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eVisitor\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eshape\u003c/span\u003e \u003cspan class=\"nx\"\u003eShape\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eShape\u003c/span\u003e \u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eaccept\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eVisitor\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eCircle\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003eRadius\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ec\u003c/span\u003e \u003cspan class=\"nx\"\u003eCircle\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eaccept\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ev\u003c/span\u003e \u003cspan class=\"nx\"\u003eVisitor\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ev\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eRectangle\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003eWidth\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eHeigh\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003er\u003c/span\u003e \u003cspan class=\"nx\"\u003eRectangle\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eaccept\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ev\u003c/span\u003e \u003cspan class=\"nx\"\u003eVisitor\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ev\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e然后，我们实现两个Visitor，一个是用来做JSON序列化的，另一个是用来做XML序列化的:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efunc\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonVisitor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eshape\u003c/span\u003e \u003cspan class=\"n\"\u003eShape\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"n\"\u003ejson\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarshal\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eshape\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003enil\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epanic\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eerr\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003efmt\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efunc\u003c/span\u003e \u003cspan class=\"n\"\u003eXmlVisitor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eshape\u003c/span\u003e \u003cspan class=\"n\"\u003eShape\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"n\"\u003exml\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarshal\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eshape\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003enil\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epanic\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eerr\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003efmt\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003ebytes\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e下面是我们的使用Visitor这个模式的代码：\u003c/p\u003e","title":"Go编程模式：Visitor（k8s）"},{"content":"使用minikube构建本地单节点k8s集群 minikube ssh kubectl cluster-info kubectl get nodes #查看节点信息 kubectl describe node minikube #详细信息 多节点k8s集群，使用Google K8s Engine 构建方式看GKE官网即可\nk8s初步使用 kubectl run kubia \u0026ndash;image=derios/kubia \u0026ndash;port=8080 \u0026ndash;generator=run/v1\n--image=derios/kubia代表要运行的容器镜像 这里的--generator会被废弃，其含义指代的是创建一个ReplicationController而不是Deployment。 kubectl apply -f 更常用 kubectl get pods kubectl get pods -o wide 显示pod ip和pod的节点 如果使用GWE，可以访问集群的dashborad: kubectl clusert-info获取地址 gcloud container clusters describe kubia | grep -E \u0026ldquo;(username|password):\u0026ldquo;获取用户名和密码\n如果仅仅使用minikube，则如下不需要任何凭证即可访问: 1 minikube dashboard Namespace相关操作 1 kubectl config set-context --current --namespace=my-namespace 创建服务对象，访问Web应用 如果使用minikube或者kubeadm等自定义k8s，loadbalancer是没有集成的，需要AWS或者Google Cloud。最好使用NodePort或者Ingress Controller。如果真要用minikube, 可以使用minikube tunnel解决, 或者minikube service kubia-http\nkubectl expose rc/po/svc kubia \u0026ndash;type=LoadBalancer \u0026ndash;name kubia-http创建出的service介于pod和node之间, kubectl get services可以查看\n如果没有外部IP地址，因为k8s运行的云基础设施新建lb需要一段时间，lb启动以后应该会显示\nReplicationController的角色 复制pod，replica扩缩 kubectl scale rc kubia --replicas=3 可以将rc下的pods方便进行扩缩容 为何需要service 解决不断变化的pod IP问题，以及在一个固定的IP和端口上对外暴露多个pod。\n当一个service被创建，会得到一个静态IP，在service的生命周期这个IP不会被改变，service会确保其中提个pod接受连接，而非关注这个pod运行在何处.\nk8s的pod pod不一定要包含多个容器，单独容器的pod也是很常见的。如果pod包含多个容器，那么这些容器都工作在同一个工作节点上，pod是不能跨多个工作节点工作的。\npod概念的提出，是因为我们不能把多个进程聚集在一个单独的容器中，我们需要另一种更为高级的结构将容器绑定在一起，并把他们欧威一个单元进行管理。\n一个Pod下的所有容器都在相同的额network和UTS命名空间下运行，所以共享相同的主机名和网络接口，也能够通过IPC进行通信，甚至在最新的k8s和docker版本下，能共享相同的PID命名空间。\n容器如何共享相同IP和端口空间 由于pod中的容器运行于相同Namespace命名空间，所以他们共享相同的IP地址和端口空间。这意味着在同一个pod中的容器运行的多个线程不能绑定到相同的端口号，否则会冲突。此外，他们还有相同的loopback网络几口，所以容器可以通过localhost和同一个pod中的其他容器进行通信。\n平坦网络 k8s集群中所有pod都在同一个共享网络地址空间中，也就是说每个pod都可以通过其他pod的IP地址来实现相互访问，表示他们之间没有NAT(网络地址转换)网管。当两个pod彼此之间发送网络数据包时，都把对方的实际IP地址看作数据包中的源IP。\n因此可以看到，pod就是逻辑主机，行为和非容器世界中的物理主机或者虚拟机相似。运行在同一个pod中的进程和运行在同一物理机或者虚拟机上的进程类似，只是每个进程封装在一个容器里。\n通过pod合理管理容器 一个由前端应用服务器和后端数据库组成的多层应用程序，应该讲其配置为单个pod还是两个pod呢？\n把多层应用分散到多个pod中 一个pod下的所有容器运行在一起，而web服务器和数据库真的要在同一台计算机上运行吗？明显是否定的。而且如何k8s集群节点多了，如果只有一个单独的pod，其资源利用率也很低。\n基于扩缩容考虑而分隔到多个pod中 pod是扩缩容的基本单位，k8s不能横向扩缩单个容器，而只能扩缩整个pod，所以明显web服务器和数据库放在一起是不对的。\n而且数据库这种有状态的服务器，比无状态的web服务器更加难扩展，所以如果要单独扩缩容器，这个容器必须明确地部署在单独的pod当中\n在pod中使用多个容器的时机 一般来说，常见场景是应用由一个主进程和多个辅助进程组成，也就是这个容器组是紧密耦合的。举例就是，pod中的主容器是仅仅服务于每个目录中文件的Web服务器，另一个sidecar容器定期从外部资源下载内容并将其存储在Web服务器目录里。这种情形需要使用k8s的Volume，把其挂在到两个容器里。\nsidecar容器包括：日志轮转器、收集器、数据处理器、通信适配器等等\n因此容器如何分组到pod这个问题，我们需要问以下的问题：\n它们是否需要一起运行还是可以在不同的主机上运行？ 它们代表的是一个整体还是相互独立的组件？ 它们必须一起扩缩容还是可以分别单独进行？ 以YAML或者JSON创建pod 好处：kubectl run局限性很多，比如属性配置很有限，而资源配置的方式除了更方便定义k8s资源对象，还能把它们存储在版本控制系统中。\n注意参考: k8s api reference\n检查现有pod的YAML描述 kubectl get po kubia-zxzij -o yaml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 apiVersion: v1 kind: Pod metadata: creationTimestamp: \u0026#34;2021-03-31T03:00:40Z\u0026#34; generateName: kubia- labels: app: kubia managedFields: - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:metadata: f:generateName: {} f:labels: .: {} f:app: {} f:ownerReferences: .: {} k:{\u0026#34;uid\u0026#34;:\u0026#34;4b78e019-3753-40a1-998c-0d0ec4af9f29\u0026#34;}: .: {} f:apiVersion: {} f:blockOwnerDeletion: {} f:controller: {} f:kind: {} f:name: {} f:uid: {} f:spec: f:containers: k:{\u0026#34;name\u0026#34;:\u0026#34;kubia\u0026#34;}: .: {} f:image: {} f:imagePullPolicy: {} f:name: {} f:ports: .: {} k:{\u0026#34;containerPort\u0026#34;:90,\u0026#34;protocol\u0026#34;:\u0026#34;TCP\u0026#34;}: .: {} f:containerPort: {} f:protocol: {} f:resources: {} f:terminationMessagePath: {} f:terminationMessagePolicy: {} f:dnsPolicy: {} f:enableServiceLinks: {} f:restartPolicy: {} f:schedulerName: {} f:securityContext: {} f:terminationGracePeriodSeconds: {} manager: kube-controller-manager operation: Update time: \u0026#34;2021-03-31T03:00:40Z\u0026#34; - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:status: f:conditions: k:{\u0026#34;type\u0026#34;:\u0026#34;ContainersReady\u0026#34;}: .: {} f:lastProbeTime: {} f:lastTransitionTime: {} f:status: {} f:type: {} k:{\u0026#34;type\u0026#34;:\u0026#34;Initialized\u0026#34;}: .: {} f:lastProbeTime: {} f:lastTransitionTime: {} f:status: {} f:type: {} k:{\u0026#34;type\u0026#34;:\u0026#34;Ready\u0026#34;}: .: {} f:lastProbeTime: {} f:lastTransitionTime: {} f:status: {} f:type: {} f:containerStatuses: {} f:hostIP: {} f:phase: {} f:podIP: {} f:podIPs: .: {} k:{\u0026#34;ip\u0026#34;:\u0026#34;172.17.0.7\u0026#34;}: .: {} f:ip: {} f:startTime: {} manager: kubelet operation: Update time: \u0026#34;2021-03-31T03:00:47Z\u0026#34; name: kubia-2r2pb namespace: default ownerReferences: - apiVersion: v1 blockOwnerDeletion: true controller: true kind: ReplicationController name: kubia uid: 4b78e019-3753-40a1-998c-0d0ec4af9f29 resourceVersion: \u0026#34;11025\u0026#34; selfLink: /api/v1/namespaces/default/pods/kubia-2r2pb uid: 342307fe-f6d8-4201-ba02-0210cd75d0a8 spec: containers: - image: derios/kubia imagePullPolicy: Always name: kubia ports: - containerPort: 90 protocol: TCP resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: default-token-whxzx readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true nodeName: minikube preemptionPolicy: PreemptLowerPriority priority: 0 restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 volumes: - name: default-token-whxzx secret: defaultMode: 420 secretName: default-token-whxzx status: conditions: - lastProbeTime: null lastTransitionTime: \u0026#34;2021-03-31T03:00:40Z\u0026#34; status: \u0026#34;True\u0026#34; type: Initialized - lastProbeTime: null lastTransitionTime: \u0026#34;2021-03-31T03:00:47Z\u0026#34; status: \u0026#34;True\u0026#34; type: Ready - lastProbeTime: null lastTransitionTime: \u0026#34;2021-03-31T03:00:47Z\u0026#34; status: \u0026#34;True\u0026#34; type: ContainersReady - lastProbeTime: null lastTransitionTime: \u0026#34;2021-03-31T03:00:40Z\u0026#34; status: \u0026#34;True\u0026#34; type: PodScheduled containerStatuses: - containerID: docker://1cce1d07039838f1f4d825d088fca52b54b15808efebf20d1f38cc2952a65ebd image: derios/kubia:latest imageID: docker-pullable://derios/kubia@sha256:7aa603e50514206f8d40126520409a6814001c1bae68b9f286cca9c1ca271f7a lastState: {} name: kubia ready: true restartCount: 0 started: true state: running: startedAt: \u0026#34;2021-03-31T03:00:46Z\u0026#34; hostIP: 192.168.49.2 phase: Running podIP: 172.17.0.7 podIPs: - ip: 172.17.0.7 qosClass: BestEffort startTime: \u0026#34;2021-03-31T03:00:40Z\u0026#34; 虽然看着比较复杂，主要包含了k8s对象/资源类型、k8s API版本、pod元数据、pod规格和内容、pod和内部容器的详细状态等。\npod定义的主要部分 metadata: 名称、命名空间、标签和关于该容器的其他信息 spec：包含pod内容的实际说明，例如pod的容器、卷和其他数据 status: 包含运行中的pod的当前信息，比如pod所处的条件、每个容器的描述和状态，以及内部IP和其他基本信息 创建新的pod的时候，不需要提供status部分，这只是运行时数据。实际上深究每个属性的意义不是很大，我们需要关注创建pod的最基本信息。\n创建一个简单的YAML描述 1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Pod metadata: name: kubia-manual spec: containers: - image: derios/kubia name: kubia ports: - containerPort: 8080 protocol: TCP 可以用kubectl explain pods、kubectl explain pod.spec之类去看资源定义\n用kubectl create来创建pod 1 $ kubectl create -f kubia-manual.yaml 1 $ kubectl get pods 查看应用程序日志 小型node.js应用把日志记录到进程的标准输出，容器化的应用程序通常把日志记录到标准输出和标准错误流，而不是写入文件。所以这就允许user通过标准的方式查看不同应用程序的日志。\nDocker(或者广义的container runtime)把这些流重定向到文件，可以用以下命令获取容器的日志：\n1 $ docker logs \u0026lt;container id\u0026gt; 一般可以ssh到pod运行节点然后这样查看，不过k8s提供了更为简单的方式。\n使用kubectl logs命令获取pod日志：\n1 $ kubectl logs kubia-manual 如果一个pod只包含一个容器，那么查看k8s应用程序的日志会变得很简单。\n获取多容器pod的日志时指定容器名称\n1 $ kubectl logs kubia-manual -c kubia 请注意，如果pod被删了，那么pod里的日志也会被删。如果希望在pod删除之后仍然可以获取其日志，需要设置中心化、集群范围的日志系统，把所有日志存储到中心存储中，这个在后续会讨论。\n向pod发送请求 通过端口转发连接到pod以进行测试和调试。\n将本地网络端口转发到pod中的端口 如果不想通过service情形下对某个特定的pod进行通信(debug)，可以通过kubectl port-forward命令完成端口转发:\n1 $kubectl port-forward kubia-munual 8888:8080 这样就能通过本地端口连接到我们的pod。\n通过端口转发连接到pod 另一个终端curl localhost:8888即可调试。实际上述指令起了一个线程进行对调试请求的处理。\n使用Tag组织pod 对于微服务架构，部署数量能很轻松膨胀：多副本、不同版本等等。所以需要进行Pod分组去管理对象。Tag标签不仅仅可以组织pod，还包括了其他k8s资源。\n标签的定义：可以附加到资源的任意键值对。只要标签的key在资源内唯一，那么一个资源便可以拥有多个标签，一般我们在创建资源的时候回把标签附加到资源上，当然后续可以新增、修改标签。举个例子：\napp: 指定pod属于哪个应用、组件或者微服务 rel：显示在pod中运行的应用程序版本是stable、beta还是canary 利用这种组织形式，访问集群的开发或者运维能通过查看pod标签轻松看到系统结构以及每个pod的角色。\n创建pod的时候指定标签 1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: v1 kind: Pod metadata: name: kubia-manual-v2 labels: creation_method: manual env: prod spec: containers: - image: derios/kubia name: kubia ports: - containerPort: 91 protocol: TCP 可以通过以下查看标签\n1 $ kubectl get po --show-labels 如果只对某些标签的pod感兴趣，可以使用-L选项把tag放在列上：\n1 $ kubectl get po -L creation_method,env 1 2 3 4 5 6 7 NAME READY STATUS RESTARTS AGE CREATION_METHOD ENV kubia-2r2pb 1/1 Running 0 5h7m kubia-kt6gz 1/1 Running 0 5h7m kubia-manual-v2 1/1 Running 0 2m24s manual prod kubia-tqt52 1/1 Running 0 5h7m nginx-r4hdr 1/1 Running 0 18h nginx-rg76b 1/1 Running 0 18h 修改现有pod标签 新增的话，用kubectl label即可:\n1 kubectl label po kubia-manual creation_method=manual 更改的话，需要使用--overwrite选项:\nkubectl label po kubia-manual-v2 env=debug --overwrite 通过标签选择器列出pod子集 标签的强大功能体现在这里。\n标签选择器根据资源的以下条件来筛选资源:\n包含(or not)使用特定key的标签 包含具有特定key和value的标签 包含具有特定key的标签，但值和我们指定的不同 使用标签选择器列出pod 1 2 3 $ kubectl get po -l creation_method=manual NAME READY STATUS RESTARTS AGE kubia-manual-v2 1/1 Running 0 9m5s 列出包含env标签的所有pod，无论其值多少：\n1 2 3 $ kubectl get po -l env NAME READY STATUS RESTARTS AGE kubia-manual-v2 1/1 Running 0 10m 列出没有env标签的pod：\n1 2 3 4 5 6 7 $ kubectl get po -l \u0026#39;!env\u0026#39; NAME READY STATUS RESTARTS AGE kubia-2r2pb 1/1 Running 0 5h17m kubia-kt6gz 1/1 Running 0 5h17m kubia-tqt52 1/1 Running 0 5h17m nginx-r4hdr 1/1 Running 0 18h nginx-rg76b 1/1 Running 0 18h 其他的选择器语法解释：\ncreation_method!=manual：选择带有creation_method标签，但其值不为manual的pod env in (prod, devel): 选择带有env标签且值为prod或者devel的pod env notin (prod, devel): 差不多自己理解 在标签选择器中使用多个条件 精确匹配：app=pc,rel=beta\n这种列出pod子集的做法能够让我们能够进行子集的操作，简单的例如删除自己的pod\n使用标签和选择器约束pod调度 pod的随机调度是k8s集群的工作方式，但如果我们希望对将pod调度到何处有一定的发言权，比如我们的硬件是异构或者非同质的：cpu架构区别、节点磁盘种类区别、GPU密集型运算加速特定节点等。\n当然，我们不是说明pod应该调度到哪个节点，因为基础架构和应用程序的强耦合不是k8s的玩法。我们通过用某种方式描述对节点的需求，使得k8s选择一个符合这个需求的节点，也就是用节点标签和节点标签选择器完成。\n用标签分类工作节点 假设集群中的一个节点刚添加完成，其包含一个用于通用GPU计算的GPU，我们希望加标签去展现这个节点特性:\n1 $ kubectl label node \u0026lt;node name\u0026gt; gpu=true 1 2 3 $ kubectl get nodes -l gpu=true NAME STATUS ROLES AGE VERSION minikube Ready master 20h v1.19.2 将pod调度到特定节点 假设已经给节点打上了标签，为了让调度器只在提供了确定标签的节点中进行选择，需要在pod的YAML文件中添加一个节点选择器(这里是deploy:test标签的节点):\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: kubia-manual-v2 labels: creation_method: manual env: prod spec: nodeSelector: deploy: \u0026#34;test\u0026#34; containers: - image: derios/kubia name: kubia ports: - containerPort: 91 protocol: TCP 调度到特定一个节点(不推荐) 每个node有自己的一个唯一标签: kubernetes.io/hostname，但是如果节点离线了，就会导致pod无法调度，因此用标签选择器比较符合工作流程。当讨论Replication-Controllers和Service的时候，标签选择器的重要性也会彰显。\n注解pod 这块暂时不想深究，知道用法就行\n使用命名空间对资源进行分组 待研究\n停止和移除pod 按名称删除 1 \u0026amp; kubectl delete po kubia 注意删除pod的时候，是k8s向进程发送SIGTERM信号并等待一定的秒数(30s默认)，如果还没有正常关闭，则发送SIGKILL终止线程。因此，为了保证线程可以正常关闭，需要正确处理SIGTERM信号。\n使用标签选择器删除 1 $ kubectl delete po -l creation_method=manual 即删除带有此标签的所有pod。\n通过删除整个命名空间来删除pod 1 $ kubectl delete ns custom-namespace 删除命名空间的所有pod，但保留命名空间 1 $ kubectl delete po --all 删除当前命名空间的所有pod。\n注意如果空间内创建了replication controllers，删除所有还会新增新的pod出来，所以还需要删除rc。\n删除命名空间中(几乎)所有资源 1 kubectl delete all --all 使用all并不意味着会删除所有的内容，比如secret还是会被保留，这些是要明确指定删除的。\n注意：该命令会删除名为kubernetes的service，不过在几分钟之内会被重新创建。\n","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/k8s-basic/","summary":"\u003ch2 id=\"使用minikube构建本地单节点k8s集群\"\u003e使用minikube构建本地单节点k8s集群\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eminikube ssh\u003c/li\u003e\n\u003cli\u003ekubectl cluster-info\u003c/li\u003e\n\u003cli\u003ekubectl get nodes #查看节点信息\u003c/li\u003e\n\u003cli\u003ekubectl describe node minikube #详细信息\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"多节点k8s集群使用google-k8s-engine\"\u003e多节点k8s集群，使用Google K8s Engine\u003c/h2\u003e\n\u003cp\u003e构建方式看GKE官网即可\u003c/p\u003e\n\u003ch2 id=\"k8s初步使用\"\u003ek8s初步使用\u003c/h2\u003e\n\u003cp\u003ekubectl run kubia \u0026ndash;image=derios/kubia \u0026ndash;port=8080 \u0026ndash;generator=run/v1\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--image=derios/kubia\u003c/code\u003e代表要运行的容器镜像\u003c/li\u003e\n\u003cli\u003e这里的\u003ccode\u003e--generator\u003c/code\u003e会被废弃，其含义指代的是创建一个\u003ccode\u003eReplicationController\u003c/code\u003e而不是\u003ccode\u003eDeployment\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003ekubectl apply -f \u003cyaml name\u003e 更常用\u003c/li\u003e\n\u003cli\u003ekubectl get pods\u003c/li\u003e\n\u003cli\u003ekubectl get pods -o wide 显示pod ip和pod的节点\u003c/li\u003e\n\u003cli\u003e如果使用GWE，可以访问集群的dashborad:\u003c/li\u003e\n\u003cli\u003ekubectl clusert-info获取地址\u003c/li\u003e\n\u003cli\u003egcloud container clusters describe kubia | grep -E \u0026ldquo;(username|password):\u0026ldquo;获取用户名和密码\u003cbr\u003e\n如果仅仅使用minikube，则如下不需要任何凭证即可访问:\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eminikube dashboard\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"namespace相关操作\"\u003eNamespace相关操作\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"l\"\u003ekubectl config set-context --current --namespace=my-namespace\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"创建服务对象访问web应用\"\u003e创建服务对象，访问Web应用\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003e如果使用minikube或者kubeadm等自定义k8s，loadbalancer是没有集成的，需要AWS或者Google Cloud。最好使用NodePort或者Ingress Controller。如果真要用minikube, 可以使用minikube tunnel解决, 或者minikube service kubia-http\u003c/code\u003e\u003c/p\u003e","title":"Kubernetes Handbook (Start \u0026 Pod)"},{"content":"Books Docker in Action (English ver.)\nDocker入门到实践(中文)\n速查 Docker Cheat Sheet\n全量CLI 容器管理CLI 查看容器CLI 容器交互CLI 镜像管理CLI 镜像传输CLI DOCKERFILE主要命令 Dockerfile 基底 1 FROM ruby:2.2.2 变量 1 2 ENV APP_HOME/myapp RUN mkdir $APP_HOME 初始化 1 RUN bundle install 1 WORKDIR /myapp 1 2 VOLUME [\u0026#34;/data\u0026#34;] # Specification for mount point 1 2 ADD file.xyz /file.xyz COPY --chown=user:group host_file.xyz /path/container_file.xyz Onbuild 1 2 ONBUILD RUN bundle install # when used with another file 命令 1 2 EXPOSE 5900 CMD [\u0026#34;bundle\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;rails\u0026#34;, \u0026#34;server\u0026#34;] Entrypoint 1 ENTRYPOINT exec top -b Metadata 1 LABEL version=\u0026#34;1.0\u0026#34; 1 2 LABEL \u0026#34;com.example.vendor\u0026#34;=\u0026#34;ACME Incorporated\u0026#34; LABEL com.example.label-with-value=\u0026#34;foo\u0026#34; 1 2 LABEL description=\u0026#34;This text illustrates \\ that label-values can span multiple lines.\u0026#34; Docker Compose 基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # docker-compose.yml version: \u0026#39;2\u0026#39; services: web: build: . # build from Dockerfile context: ./Path dockerfile: Dockerfile ports: - \u0026#34;5000:5000\u0026#34; volumes: - .:/code redis: image: redis 指令 1 2 docker-compose start docker-compose stop 1 2 docker-compose pause docker-compose unpause 1 2 3 docker-compose ps docker-compose up docker-compose down Reference(例子) 构建 1 2 3 web: # build from Dockerfile build: . 1 2 3 4 # build from custom Dockerfile build: context: ./dir dockerfile: Dockerfile.dev 1 2 3 4 5 6 # build from image image: ubuntu image: ubuntu:14.04 image: tutum/influxdb image: example-registry:4000/postgresql image: a4bc65fd 端口 1 2 3 ports: - \u0026#34;3000\u0026#34; - \u0026#34;8000:80\u0026#34; # guest:host 1 2 # expose ports to linked services (not to host) expose: [\u0026#34;3000\u0026#34;] 指令 1 2 3 # command to execute command: bundle exec thin -p 3000 command: [bundle, exec, thin, -p, 3000] 1 2 3 # override the entrypoint entrypoint: /app/start.sh entrypoint: [php, -d, vendor/bin/phpunit] 环境变量 1 2 3 4 5 # environment vars environment: RACK_ENV: development environment: - RACK_ENV=development 1 2 3 # environment vars from file env_file: .env env_file: [.env, .development.env] 依赖 1 2 3 4 5 # makes the `db` service available as the hostname `database` # (implies depends_on) links: - db:database - redis 1 2 3 # make sure `db` is alive before starting depends_on: - db 其他选项 1 2 3 4 # make this service extend another extends: file: common.yml # optional service: webapp 1 2 3 volumes: - /var/lib/mysql - ./_data:/var/lib/mysql 高级特性 打标签 1 2 3 4 services: web: labels: com.example.description: \u0026#34;Accounting web app\u0026#34; DNS服务器 1 2 3 4 5 6 services: web: dns: 8.8.8.8 dns: - 8.8.8.8 - 8.8.4.4 设备绑定 1 2 3 4 services: web: devices: - \u0026#34;/dev/ttyUSB0:/dev/ttyUSB0\u0026#34; 外部链接 1 2 3 4 5 services: web: external_links: - redis_1 - project_db_1:mysql 主机设置 1 2 3 4 services: web: extra_hosts: - \u0026#34;somehost:192.168.1.100\u0026#34; Services 1 2 3 4 5 6 7 8 9 10 11 # To view list of all the services runnning in swarm docker service ls # To see all running services docker stack services stack_name # to see all services logs docker service logs stack_name service_name # To scale services quickly across qualified node docker service scale stack_name_service_name=replicas Clean up 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # To clean or prune unused (dangling) images docker image prune # To remove all images which are not in use containers , add - a docker image prune -a # To Purne your entire system docker system prune # To leave swarm docker swarm leave # To remove swarm ( deletes all volume data and database info) docker stack rm stack_name # To kill all running containers docker kill $(docekr ps -q ) ","permalink":"https://pillumina.github.io/posts/programming/cloud-computing/docker-basic/","summary":"\u003ch2 id=\"books\"\u003eBooks\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://pepa.holla.cz/wp-content/uploads/2016/10/Docker-in-Action.pdf\"\u003eDocker in Action (English ver.)\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://yeasy.gitbook.io/docker_practice/\"\u003eDocker入门到实践(中文)\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"速查\"\u003e速查\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/wsargent/docker-cheat-sheet/tree/master/zh-cn\"\u003eDocker Cheat Sheet\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"全量cli\"\u003e全量CLI\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"docker cheat sheet\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet8.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"容器管理cli\"\u003e容器管理CLI\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"container management commands\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet1.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"查看容器cli\"\u003e查看容器CLI\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"inspect container commands\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet3.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"容器交互cli\"\u003e容器交互CLI\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"interact with container commands\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet4.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"镜像管理cli\"\u003e镜像管理CLI\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image management commands\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet5.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"镜像传输cli\"\u003e镜像传输CLI\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image transfer commands\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet6.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"dockerfile主要命令\"\u003eDOCKERFILE主要命令\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"builder main commands\" loading=\"lazy\" src=\"https://raw.githubusercontent.com/sangam14/dockercheatsheets/master/dockercheatsheet7.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch2 id=\"dockerfile\"\u003eDockerfile\u003c/h2\u003e\n\u003ch3 id=\"基底\"\u003e基底\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"s\"\u003e ruby:2.2.2\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"变量\"\u003e变量\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eENV\u003c/span\u003e APP_HOME/myapp\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eRUN\u003c/span\u003e mkdir \u003cspan class=\"nv\"\u003e$APP_HOME\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"初始化\"\u003e初始化\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eRUN\u003c/span\u003e bundle install\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eWORKDIR\u003c/span\u003e\u003cspan class=\"s\"\u003e /myapp\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eVOLUME\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;/data\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"c\"\u003e# Specification for mount point\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eADD\u003c/span\u003e file.xyz /file.xyz\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e --chown\u003cspan class=\"o\"\u003e=\u003c/span\u003euser:group host_file.xyz /path/container_file.xyz\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"onbuild\"\u003eOnbuild\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eONBUILD\u003c/span\u003e \u003cspan class=\"k\"\u003eRUN\u003c/span\u003e bundle install\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"c\"\u003e# when used with another file\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"命令\"\u003e命令\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eEXPOSE\u003c/span\u003e\u003cspan class=\"s\"\u003e 5900\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eCMD\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;bundle\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;exec\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;rails\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;server\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"entrypoint\"\u003eEntrypoint\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eENTRYPOINT\u003c/span\u003e \u003cspan class=\"nb\"\u003eexec\u003c/span\u003e top -b\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"metadata\"\u003eMetadata\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eLABEL\u003c/span\u003e \u003cspan class=\"nv\"\u003eversion\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;1.0\u0026#34;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eLABEL\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;com.example.vendor\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;ACME Incorporated\u0026#34;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eLABEL\u003c/span\u003e com.example.label-with-value\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;foo\u0026#34;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eLABEL\u003c/span\u003e \u003cspan class=\"nv\"\u003edescription\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;This text illustrates \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003ethat label-values can span multiple lines.\u0026#34;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"docker-compose\"\u003eDocker Compose\u003c/h2\u003e\n\u003ch3 id=\"基本用法\"\u003e基本用法\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# docker-compose.yml\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;2\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ebuild\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e.\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# build from Dockerfile\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e./Path\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edockerfile\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDockerfile\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eports\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;5000:5000\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumes\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e- \u003cspan class=\"l\"\u003e.:/code\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eredis\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eredis\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"指令\"\u003e指令\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose start\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose stop\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose pause\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose unpause\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose ps\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose up\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker-compose down\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"reference例子\"\u003eReference(例子)\u003c/h2\u003e\n\u003ch3 id=\"构建\"\u003e构建\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# build from Dockerfile\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebuild\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e.\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# build from custom Dockerfile\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebuild\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e./dir\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edockerfile\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDockerfile.dev\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# build from image\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eubuntu:14.04\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003etutum/influxdb\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eexample-registry:4000/postgresql\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ea4bc65fd\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"端口\"\u003e端口\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eports\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;3000\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;8000:80\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# guest:host\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# expose ports to linked services (not to host)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eexpose\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;3000\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"指令-1\"\u003e指令\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# command to execute\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ebundle exec thin -p 3000\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003ebundle, exec, thin, -p, 3000]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# override the entrypoint\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eentrypoint\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e/app/start.sh\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eentrypoint\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003ephp, -d, vendor/bin/phpunit]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"环境变量\"\u003e环境变量\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# environment vars\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eRACK_ENV\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edevelopment\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003eRACK_ENV=development\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# environment vars from file\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eenv_file\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e.env\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eenv_file\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003e.env, .development.env]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"依赖\"\u003e依赖\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# makes the `db` service available as the hostname `database`\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# (implies depends_on)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003elinks\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003edb:database\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003eredis\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# make sure `db` is alive before starting\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003edepends_on\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003edb\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"其他选项\"\u003e其他选项\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# make this service extend another\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eextends\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003efile\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ecommon.yml \u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# optional\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eservice\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ewebapp\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003evolumes\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003e/var/lib/mysql\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"l\"\u003e./_data:/var/lib/mysql\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"高级特性\"\u003e高级特性\u003c/h2\u003e\n\u003ch3 id=\"打标签\"\u003e打标签\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003elabels\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ecom.example.description\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Accounting web app\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"dns服务器\"\u003eDNS服务器\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edns\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"m\"\u003e8.8.8.8\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edns\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"m\"\u003e8.8.8.8\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"m\"\u003e8.8.4.4\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"设备绑定\"\u003e设备绑定\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edevices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;/dev/ttyUSB0:/dev/ttyUSB0\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"外部链接\"\u003e外部链接\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eexternal_links\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"l\"\u003eredis_1\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"l\"\u003eproject_db_1:mysql\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"主机设置\"\u003e主机设置\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eextra_hosts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;somehost:192.168.1.100\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"services\"\u003eServices\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To view list of all the services runnning in swarm\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker service ls \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To see all running services\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker stack services stack_name\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# to see all services logs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker service logs stack_name service_name \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To scale services quickly across qualified node\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker service scale \u003cspan class=\"nv\"\u003estack_name_service_name\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ereplicas\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"clean-up\"\u003eClean up\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To clean or prune unused (dangling) images\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker image prune \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To remove all images which are not in use containers , add - a\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker image prune -a \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To Purne your entire system\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker system prune \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To leave swarm\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker swarm leave\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To remove swarm ( deletes all volume data and database info)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker stack rm stack_name \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# To kill all running containers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker \u003cspan class=\"nb\"\u003ekill\u003c/span\u003e \u003cspan class=\"k\"\u003e$(\u003c/span\u003edocekr ps -q \u003cspan class=\"k\"\u003e)\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"Docker Cheat Sheet"},{"content":"这篇文章的启发是我在阅读Go的http源码时获得的，之前对这块缺乏深入的了解，这篇文章会结合源码讨论包括典型http request的路由，还会涉及到一些并发和中间件的issue。\n我们先从一个简单的go server谈起，下面的代码从https://gobyexample.com/http-servers 截取：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, \u0026#34;hello\\n\u0026#34;) } func headers(w http.ResponseWriter, req *http.Request) { for name, headers := range req.Header { for _, h := range headers { fmt.Fprintf(w, \u0026#34;%v: %v\\n\u0026#34;, name, h) } } } func main() { http.HandleFunc(\u0026#34;/hello\u0026#34;, hello) http.HandleFunc(\u0026#34;/headers\u0026#34;, headers) http.ListenAndServe(\u0026#34;:8090\u0026#34;, nil) } 追踪请求的生命周期我们从http.ListenAndServe这个方法开始，下面的图示说明了这一层的调用关系:\n这里实际上inlined了一些代码，因为初始的代码有很多其他的细节不好追踪。\n主要的flow其实和我们预期的一致：ListenAndServe方法对你一个目标地址监听一个TCP端口，而后循环不断接受新的连接。每一个连接，它会起一个新的goroutine去serve，serve的具体操作是:\n从连接里解析HTTP请求： 产生http.Request 将http.Request传给用户自定义的handler 一个handler实际上就是实现了http.Handler接口：\n1 2 3 type Handler interface { ServeHTTP(ResponseWriter, *Request) } 默认Handler 在我们上述的代码中，ListenAndServe方法的第二个参数为nil，实际上应该是用户自定义的handler, 这是为何？我们的图解中省去了很多细节，实际上当HTTP包serve一个请求的时候，它并没有直接调用用户的handlers而是使用一个adaptor：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == \u0026#34;*\u0026#34; \u0026amp;\u0026amp; req.Method == \u0026#34;OPTIONS\u0026#34; { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) } 上述代码表示了，如果handler == nil, http.DefaultServeMux会作为默认的handler。这个default server mux是在http包中一个http.ServeMux类全局实例。而当我们的样例代码通过http.HandleFunc注册handlers的时候，同样会注册到default mux中。\n所以我们可以重写我们的样例代码如下:\n1 2 3 4 5 6 7 func main() { mux := http.NewServeMux() mux.HandleFunc(\u0026#34;/hello\u0026#34;, hello) mux.HandleFunc(\u0026#34;/headers\u0026#34;, headers) http.ListenAndServe(\u0026#34;:8090\u0026#34;, mux) } ServeMux只是一个Handler 在看了很多Go的server例子以后，很容易会把ListenAndServe想象成把mux作为参数，但是这个明显是不准确的。从上面的例子看到，ListenAndServe实际传入的是实现了http.Handler接口的值，我们可以重写一下代码并且不用任何的muxes：\n1 2 3 4 5 6 7 8 9 10 11 type PoliteServer struct { } func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome! Thanks for visiting!\\n\u0026#34;) } func main() { ps := \u0026amp;PoliteServer{} log.Fatal(http.ListenAndServe(\u0026#34;:8090\u0026#34;, ps)) } 这个snippet里面没有路由，所有的HTTP请求直接传进PoliteServer的ServeHTTP参数里，并且所有的请求都有相同的响应。可以尝试用不同的路径和方法去curl一下这个server。\n然后我们再用http.HandlerFunc简化一下这个polite server:\n1 2 3 4 5 6 7 func politeGreeting(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome! Thanks for visiting!\\n\u0026#34;) } func main() { log.Fatal(http.ListenAndServe(\u0026#34;:8090\u0026#34;, http.HandlerFunc(politeGreeting))) } http.HadnlerFunc是http包里的一个很好用的adaptor:\n1 2 3 4 5 6 7 8 9 10 // The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler that calls f. type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } 在这篇文章最开始的例子里，用到了http.HandleFunc，注意和http.HandlerFunc很像，但是他们是完全不同的实体，也承担着不同的任务。\n如同PoliteServer表现的那样，http.ServeMux是实现http.Handler接口的一个类，这里查看源码\nServeMux维护了一个以长度排序的{pattern, handler}切片 Handle或者HandleFunc向这个切片添加新的handler ServeHTTP: 通过查询这个排序好的切片，找到对应请求path的handler 调用handler的ServeHTTP方法 至此，mux可以被看作为一个forwarding handler，这种编程模式在HTTP server中很常见，也就是middleware。\nhttp.Handler Middleware 如何去定义清楚middleware的含义是比较困难的，因为在不同的上下文、语言以及框架里它的概念都有一些不同。我们再看一下文章一开始的信息流图解，这里我们再简化一下，隐藏一些http包做的细节：\n下面是我们增加了middleware以后的图解:\n在Go中，middleware只是一个HTTP handler，而这个handler包了一个不同的handler。middleware handler通过调用ListenAndServe被注册，当这个middleware被调用到，他可以做任意的预处理，调用到被包的handler然后做任意的后处理。\n我们在上面了解了一个middleware的例子\u0026ndash;http.ServeMux, 在那个例子中，预处理指的是基于特定的请求path去选择用户定义的handler，然后去调用。并且没有对应的后处理。\n举一个另外的例子，我们可以在polite server中加一个基本的logging middleware， 这个middleware能够对所有请求的的细节记录日志，包括了请求执行的时间等：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type LoggingMiddleware struct { handler http.Handler } func (lm *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http.Request) { start := time.Now() lm.handler.ServeHTTP(w, req) log.Printf(\u0026#34;%s %s %s\u0026#34;, req.Method, req.RequestURI, time.Since(start)) } type PoliteServer struct { } func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome! Thanks for visiting!\\n\u0026#34;) } func main() { ps := \u0026amp;PoliteServer{} lm := \u0026amp;LoggingMiddleware{handler: ps} log.Fatal(http.ListenAndServe(\u0026#34;:8090\u0026#34;, lm)) } 请注意logging middleware其本身就是一个http.Handler包含了用户定义的handler作为一个field。当ListenAndServe调用其ServeHTTP方法的时候，做了以下的事情:\n预处理： 在user handler被执行前打时间戳 调用user handler，传入请求体和response writer 后处理：日志记录请求细节，包括耗费的时间 middleware一个巨大的优点是composable（组合性），被middleware包着的handler可以是另一个middleware等等。所以这个是一个相互包裹的http.Handler链。实际上，这个是在Go中的常见模式，这个例子也像我们展现一个经典的Go middleware是怎么样的。下面是一个logging polite server的详细例子，写法上更容易辨认：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func politeGreeting(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome! Thanks for visiting!\\n\u0026#34;) } func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() next.ServeHTTP(w, req) log.Printf(\u0026#34;%s %s %s\u0026#34;, req.Method, req.RequestURI, time.Since(start)) }) } func main() { lm := loggingMiddleware(http.HandlerFunc(politeGreeting)) log.Fatal(http.ListenAndServe(\u0026#34;:8090\u0026#34;, lm)) } 这里省去了通过方法对结构体的创建，loggingMiddleware利用了http.HandlerFunc以及闭包让代码变得更为简洁，当然功能还是和前面代码相同。但是这个写法，彰显了一个middleware的标准特征：一个函数传入一个http.Handler以及其他状态，然后返回另一个http.Handler。被返回的handler可以视作传入middleware的handler的替代品，而且会magically执行middleware所拥有的功能。\n例如，标准库里有如下的middleware:\n1 func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler 所以我们可以这样玩:\n1 handler = http.TimeoutHandler(handler, 2 * time.Second, \u0026#34;timed out\u0026#34;) 这样就能创建一个2秒超时机制的handler了。\n而middleware的组合可以由如下所示：\n1 2 handler = http.TimeoutHandler(handler, 2 * time.Second, \u0026#34;timed out\u0026#34;) handler = loggingMiddleware(handler) 仅仅两行，handler能够有超时和记录日志的功能，你或许会感觉middleware的链条写起来可能比较繁琐，不过Go有很多流行的包会解决这个问题，当然已经超出了这篇文章讨论的范围，后续我也会补充。\n除此之外，http包本身也在按照其需求使用middleware，比如之前serverHandler适应器的例子，它能够使用非常简洁的手段去默认处理nilhandler的情况（通过把请求传给default mux）\n因此，middleware可以说是一种attractive design aid，我们能够聚焦在业务逻辑handler，同时利用一般性的middleware去增强handler的功能，更多的探讨会新开一些文章。\n并发和panic处理 最后我们来研究额外的两个主题：并发和panic处理，作为我们探究Go HTTP Server中HTTP请求路径问题的结尾。\n首先关于并发的问题，前面讨论了对于每一个连接，其都由http.Server.Serve去起一个新的gorountine去处理。这利用了Go强大的并发能力，因为goroutine非常cheap并且这种简洁的并发模型对于HTTP handlers的处理也很适宜。一个handler可以阻塞（例如读取数据库）且不会停止其他handlers。不过在处理一些共享数据的goroutine并发时，还是要注意一些东西，这点我会在另外的文章谈。\n最后，panic处理。HTTP Server一般来说是一个长期运行的程序。如果在一个用户定义的handler中发生了问题，例如一些导致runtime panic的bug，有可能会让整个server都挂掉。所以最好能够在main里用recover来保护你的server，不过这种方式还是有以下的问题:\n当控制返回到main中时，ListenAndServe已经结束了所以其他serving也结束了。 因为每一个独立的goroutine处理一个connection，handlers里的panic甚至不会到达main而是挂掉整个进程。 为了防止这些问题，net/http内置了对每个goroutine的recovery(在conn.serve方法中)，我们可以看一个例子:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, \u0026#34;hello\\n\u0026#34;) } func doPanic(w http.ResponseWriter, req *http.Request) { panic(\u0026#34;oops\u0026#34;) } func main() { http.HandleFunc(\u0026#34;/hello\u0026#34;, hello) http.HandleFunc(\u0026#34;/panic\u0026#34;, doPanic) http.ListenAndServe(\u0026#34;:8090\u0026#34;, nil) } 如果我们起这个server并且用/panic去curl:\n$ curl localhost:8090/panic curl: (52) Empty reply from server server端会打下以下的日志:\n2021/02/16 09:44:31 http: panic serving 127.0.0.1:52908: oops goroutine 8 [running]: net/http.(*conn).serve.func1(0xc00010cbe0) /usr/local/go/src/net/http/server.go:1801 +0x147 panic(0x654840, 0x6f0b80) /usr/local/go/src/runtime/panic.go:975 +0x47a main.doPanic(0x6fa060, 0xc0001401c0, 0xc000164200) [... rest of stack dump here ...] 当然server还在持续运行。\n虽然这种内置的方式比挂掉整个进程好，不过开发者还是觉得这样有很多限制。它能做的只有关闭连接然后记录下日志，但是一般的情形下，最好给client端返回一些错误信息（例如错误码500等）。\n","permalink":"https://pillumina.github.io/posts/programming/golang/lifecycle-of-http/","summary":"\u003cp\u003e这篇文章的启发是我在阅读Go的http源码时获得的，之前对这块缺乏深入的了解，这篇文章会结合源码讨论包括典型http request的路由，还会涉及到一些并发和中间件的issue。\u003c/p\u003e\n\u003cp\u003e我们先从一个简单的go server谈起，下面的代码从https://gobyexample.com/http-servers 截取：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003emain\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;fmt\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;net/http\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003ehello\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e \u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eResponseWriter\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ereq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eFprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;hello\\n\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e \u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eResponseWriter\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ereq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eh\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nx\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eFprintf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;%v: %v\\n\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eh\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eHandleFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/hello\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ehello\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eHandleFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/headers\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eListenAndServe\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;:8090\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e追踪请求的生命周期我们从\u003ccode\u003ehttp.ListenAndServe\u003c/code\u003e这个方法开始，下面的图示说明了这一层的调用关系:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"diagram\" loading=\"lazy\" src=\"https://eli.thegreenplace.net/images/2021/http-request-listenandserve.png\" data-zoomable\u003e\n\u003c/p\u003e\n\u003cp\u003e这里实际上\u003ccode\u003einlined\u003c/code\u003e了一些代码，因为初始的代码有很多其他的细节不好追踪。\u003c/p\u003e","title":"Life of an HTTP request in a Go server"},{"content":"如果你在考虑将你的在线电子业务转移到云上，下面三个名词会经常遇到：\nIaaS PaaS SaaS 这些是云计算领域的三个主要类别\n你很可能早就听说过它们，在各种各样的业务转移到云上的趋势下，它们各自占据的不同的比例：\nSaaS占据大概24%的企业负载 IaaS在12%左右波动 PaaS是近年来最流行的模型，大概占据32%，且当下和未来会有更多的增长 On-Premise, SaaS, PaaS, IaaS的关键区别 on-premise这里理解成本地部署的含义，比如企业内部部署的CRM软件系统，其反义词即为云端\n不久以前，所有公司的IT系统都是on-premise的，云的概念比较模糊和遥远。\nSaaS, PaaS, IaaS只是三种简单描述你如何为你的业务使用云设施的方式：\nIaaS: cloud-based services, 为存储、网络以及虚拟化等服务提供pay-as-you-go PaaS: 通过internet提供硬件和软件工具 SaaS: 完全由第三方提供的软件服务 On-Premise: 企业内部部署 下面是一个图解：\nSaaS, PaaS, IaaS 举例 大多数的业务一般使用SaaS以及IaaS云计算服务模型的组合，此外有很多业务会鼓励开发者使用PaaS去构建应用。\nSaaS举例： BigCommerce, Google Apps, Salesforce, Dropbox, MailChimp, ZenDesk, DocuSign, Slack, Hubspot.\nPaaS举例： AWS Elastic Beanstalk, Heroku, Windows Azure (mostly used as PaaS), Force.com, OpenShift, Apache Stratos, Magento Commerce Cloud.\nIaaS举例： AWS EC2, Rackspace, Google Compute Engine (GCE), Digital Ocean, Magento 1 Enterprise Edition*.\n三类云计算服务模型解释 IaaS (Infrastructure as a Service) IaaS业务提供诸如存储、网络和虚拟化的pay-as-you-go服务，也是对on-premise基础设施的替代。因此业务可以避免对庞大的on-site资源的投入。\n传统on-premise IT基础设施的维护是耗时耗力且耗费资金的，通常这种基础设施需要大量的初始投资在物理硬件，而且你经常也需要额外的IT运营去维护硬件以及维持更新。\nWith IaaS, you can buy what you need, as you need it, and purchase more as your business grows.\nIaaS解决方案非常灵活且便于扩展，使用者能够轻易得进行方案替代，并且使用方能对基础设施进行全面掌控，其特征为：\nHighly flexible and highly scalable.\nAccessible by multiple users.\nCost-effective.\n使用IaaS的时机：\nIaaS适用于所有体量的业务，因为它能让你对基础设施进行全面掌控，同时因为其工作模式为pay-as-you-use，因此任何的预算都能够很好cover。\nPaaS (Platform as a Service) PaaS供应商通过网络提供软硬件工具，用户基于此去开发应用，所以PaaS使用者倾向于开发者。\nA PaaS solution provides the platform for developers to create unique, customizable software.\n这意味着开发者不需要在开发应用时从零开始，也就意味着帮助他们节省资金和精力。因此PaaS对于那些目标为创建独特应用的同时避免承担所有责任的人是一个流行的选择。\nPaaS平台的特征为：\nAccessible by multiple users.\nScalable – you can choose from various tiers of resources to suit the size of your business.\nBuilt on virtualization technology.\nEasy to run without extensive system administration knowledge.\n使用PaaS的时机：\n开发者只需要投入app的开发、测试和部署，而不用关注其他细节比如软件更新维护、安全补丁等等。\nPaaS 的Non-Ecommerce例子：\n一个很好的例子就是AWS Elastic Beanstalk.\nAWS提供超过100个云计算服务比如EC2，RDS和S3。大多数这些服务可以被作为IaaS使用，而且大多数使用AWS的公司会选择他们想要的服务。\n然而，对于用户来说，管理很多不同的服务会很快变得艰难和耗时，所以AWS Elatsic Beanstalk的作用就在于：其工作为基础设施上面的一层，能够自动解决很多底层细节例如容量预测、负载均衡、扩缩容以及应用健康监控。\nPaaS的Ecommerce例子：\nMagento Commerce Cloud (also known as Magento Enterprise Cloud Edition) 便是最常见的例子。\nSaaS （Software as a Service） SaaS平台让用户能够通过网络使用软件，收费方式通常为月度订阅计费。\n其优势为，使用SaaS意味着你不需要在自身的计算机中运行软件应用，只需要登录账户即可。只要有网络连接，你就能在任何设备上访问你的软件。例如你的员工能够有个人的登录账户，对应着不同的访问权限。\nYou no longer need to engage an IT specialist to download the software onto multiple computers throughout your office or worry about keeping the software on every computer up-to-date.\n大部分订阅包含维护、编译以及安全服务，这些服务在on-premise中是非常费时费力的。\nSaaS平台的特征:\nAvailable over the internet.\nHosted on a remote server by a third-party provider.\nScalable, with different tiers for small, medium, and enterprise-level businesses.\nInclusive, offering security, compliance, and maintenance as part of the cost.\n使用SaaS的时机：\nSaaS platforms are ideal for when you want an application to run smoothly and reliably with minimal input from you.\nSaaS Ecommerce举例：\nBigCommerce是一个典型。\nBigCommerce provides complete shopping cart software, as well as hosting infrastructure to the user, allowing businesses to create an online shop within minutes without worrying about coding, hosting, or software.\nExecutive Summary 基本上三种云计算服务模型的流行也削减了on-premise的部署形式，可以用pizza再去总结刚开始的图:\n总结来看，具体的区别如下：\nIaaS is there to provide you with maximum flexibility when it comes to hosting custom-built apps, as well as a providing a general data center for data storage. PaaS is most often built on top of an IaaS platform to reduce the need for system administration. It allows you to focus on app development instead of infrastructure management. SaaS offers ready-to-use, out-of-the-box solutions that meet a particular business need (such as website or email). Most modern SaaS platforms are built on IaaS or PaaS platforms. ","permalink":"https://pillumina.github.io/posts/programming/iaas-paas-diff/","summary":"\u003cp\u003e如果你在考虑将你的在线电子业务转移到云上，下面三个名词会经常遇到：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIaaS\u003c/li\u003e\n\u003cli\u003ePaaS\u003c/li\u003e\n\u003cli\u003eSaaS\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这些是\u003ca href=\"https://www.bigcommerce.com/ecommerce-answers/what-is-cloud-computing/\"\u003e云计算领域的三个主要类别\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e你很可能早就听说过它们，在各种各样的业务转移到云上的趋势下，它们各自占据的不同的比例：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSaaS占据大概24%的企业负载\u003c/li\u003e\n\u003cli\u003eIaaS在12%左右波动\u003c/li\u003e\n\u003cli\u003ePaaS是近年来最流行的模型，大概占据32%，且当下和未来会有更多的增长\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"on-premise-saas-paas-iaas的关键区别\"\u003eOn-Premise, SaaS, PaaS, IaaS的关键区别\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eon-premise这里理解成本地部署的含义，比如企业内部部署的CRM软件系统，其反义词即为云端\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e不久以前，所有公司的IT系统都是on-premise的，云的概念比较模糊和遥远。\u003c/p\u003e\n\u003cp\u003eSaaS, PaaS, IaaS只是三种简单描述你如何为你的业务使用云设施的方式：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eIaaS\u003c/strong\u003e:  cloud-based services, 为存储、网络以及虚拟化等服务提供pay-as-you-go\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePaaS\u003c/strong\u003e: 通过internet提供硬件和软件工具\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSaaS\u003c/strong\u003e: 完全由第三方提供的软件服务\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOn-Premise\u003c/strong\u003e: 企业内部部署\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e下面是一个图解：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"breakdown\" loading=\"lazy\" src=\"https://www.bigcommerce.com/blog/wp-content/uploads/2018/10/saas-vs-paas-vs-iaas-breakdown.jpg\" data-zoomable\u003e\n\u003c/p\u003e\n\u003ch3 id=\"saas-paas-iaas-举例\"\u003eSaaS, PaaS, IaaS 举例\u003c/h3\u003e\n\u003cp\u003e大多数的业务一般使用SaaS以及IaaS云计算服务模型的组合，此外有很多业务会鼓励开发者使用PaaS去构建应用。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSaaS举例\u003c/strong\u003e： BigCommerce, Google Apps, Salesforce, Dropbox, MailChimp, ZenDesk, DocuSign, Slack, Hubspot.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePaaS举例\u003c/strong\u003e： AWS Elastic Beanstalk, Heroku, Windows Azure (mostly used as PaaS), Force.com, OpenShift, Apache Stratos, Magento Commerce Cloud.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIaaS举例\u003c/strong\u003e： AWS EC2, Rackspace, Google Compute Engine (GCE), Digital Ocean, Magento 1 Enterprise Edition*.\u003c/p\u003e","title":"IaaS vs PaaS vs SaaS"},{"content":"概述 这篇文章介绍Go编程里的Pipeline模式。如果是对Unix/Linux命令行熟悉的人会知道，Pipeline其实就是把每个命令拼接起来完成一个组合功能的技术。当下诸如流式处理，函数式编程，以及应用Gateway对微服务进行简单API编排，其实都受pipeline技术方式的影响。换句话说，这种技术能够很容易得把代码按照单一职责的原则拆分成多个高内聚低耦合的小模块，然后拼装起来去完成比较复杂的功能。\n​\n","permalink":"https://pillumina.github.io/posts/programming/design-pattern/go-pipeline/","summary":"\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003e这篇文章介绍Go编程里的Pipeline模式。如果是对Unix/Linux命令行熟悉的人会知道，Pipeline其实就是把每个命令拼接起来完成一个组合功能的技术。当下诸如流式处理，函数式编程，以及应用Gateway对微服务进行简单API编排，其实都受pipeline技术方式的影响。换句话说，这种技术能够很容易得把代码按照\u003ccode\u003e单一职责\u003c/code\u003e的原则拆分成多个\u003ccode\u003e高内聚低耦合\u003c/code\u003e的小模块，然后拼装起来去完成比较复杂的功能。\u003c/p\u003e\n\u003cp\u003e​\u003c/p\u003e","title":"Go编程模式：Pipeline"},{"content":"这篇文章摘自陈皓（左耳朵耗子）的blog（2019/12/01上传），作为给自己的提醒。\n这一两周与几个朋友聊天，有年轻的90后，也有大叔级的70后，这些人在我看来都是很有能力的人，但是一些喜好过于强烈，让我不经意地回顾了我工作20年来身边的人，有发展得好的，也有发展的不好的，有些人是很可惜的，因为限制他们的不是其它人，也不是环境，而是自己，所以，很想写下这篇文章。（注：这篇文章可能会是一篇说教的文章，所以，可能会让你看着犯困，所以，我会尽量地短一些，而且尽可能多讲故事，少道理，这里的故事，全是真实发生的）\n几个故事 2019年年初，我面试了一个很年轻的小伙子（93/94年出生），这个小伙子特别有灵性，也很聪明，计算机专业出身，也很喜欢技术，基础和学习能力也很好。在我这20年来认识的人中，如果他能呆在北京、上海、深圳这样的城市，我保证不出三年，他会成为他们同龄人中非常出色的技术人员，如果有个好的舞台有一个好的团队带他，他的未来会非常成功。然而，这个小伙子有两大喜好：1）只愿（或是说被迫）呆在一个毫无IT的环境的三/四线城市，2）对技术有非常大的偏好，只喜欢Go语言，非常不喜欢其它的语言，比如：Java（离开Java的世界，基本上离开了做架构的世界（相关解释见文末））。\n他的这两个喜好，足以让一个未来会很优秀的人毁掉，因为，这个时代没有限制他，他的能力也没有限制他，但是他的意识完完全全地限制了他。\n他把自己最宝贵的青春放在了很烂的项目上，就算能用一些新的技术，他也只能算是自娱自乐，在实验室中玩玩具罢了。 他把自己的技术栈封闭起来，而直接放弃了这个时代最具工业化的技术Java，对于一个好的程序员来说，同时掌握几门语言和技术完全是没什么问题，但是自己封闭了自己的视野。 实在是非常可惜，我本来是可以为他介绍到一些很不错的公司的，但是他这样的习性，等于自己把自己未来的门给关上了，虽然我跟他长谈过，但是我也没有办法叫醒不想醒的人……\n视野、环境和舞台，对一个人的限制是非常大的。井蛙不知道大海，被空间维度所限制；夏虫不知道冬天，是被时间维度所限制；圈养的动物没有斗志，是被自己意识所限制。 偏见和不开放，对一个人的限制是真正有毁灭性的。主动让自己成为一个瞎子和聋子，主动把自己的能力阉割掉，这是一件令人痛心的事。想想大清的闭关锁国是如何让亚洲第一的北洋水师给毁掉的…… 我还有个同学，他的技术并不差，就算呆在昆明这种很落后的地方，他也非常地好学，学习英文，学习各种新技术，对技术没有任何的偏好，喜欢C/C++/Java/Python/Shell，同样喜欢前端Javascript，对基础知识非常地踏实，他在技术上没有限制自己的潜力，有什么就学什么。后来，我带他玩Docker/Go/K8S……分布式架构，他也上手的很快……像他这样的人，技术能力完全没得说，比我还大一岁，44岁了，还是一样的天天追代码细节，看Youtube的各种大会，翻github里的各种issue和pull request……\n我同学这人，拥有了成为一个技术牛人几乎所有的条件：基础知识过硬，细节扎得深，面很广，学习能力强，有英文能力，逻辑思维能力不错，非常的自律，执行力也很强，抓得住重点……然而，只有一个小问题，就是没有到大公司历练过，我三番五次叫他从昆明出来，但是最终他都呆在昆明这个城市没有出来，因为有所谓的家庭约束。然而，我身边还有好些人，把自己家从北京搬到上海，从上海搬到深圳，从厦门搬到深圳……这样的人大有人在……像他这样的能力，在哪个公司都会是主力和骨干，对于一个公司的主力和骨干来说，家庭上的这些问题都是小问题都是有很多解的……\n另外，我这个同学还是一个比较悲观的人，任何事情都是先想到不好的事，他关注负面的东西会胜于正面的东西，而且他还有一定的社交恐惧，怕与人相处和交流，时间越长越害怕，甚至有时候直接跟我说，“我就是不想改变”这样的话……其实，我以前也是一个很害怕与人交流的人，面试的时候，我根本不敢正眼看面试官一眼，也不知道与人怎么交流。但是，我与他不一样，我努力克服，不断地面试，与人面对面的交流，到一线技术客服接用户的电话，在公司里做分享，慢慢地到外面分享……3-5年就完全克服掉了。\n其实，很多事情，完全是有解的，也没有必要担心，自己的心理障碍也是可以克服的，重点就是自己愿不愿意，只要愿意完成了一半，接下来就是不断的摸爬滚打坚持了。\n不限制自己的人，会穷举各种方法来解决问题，限制自己的人，只会找各式各样的问题或借口。 不限制自己的人，会努力改变自己的问题和缺陷，限制自己的人，会放任自己。 另外几个故事 我还有另外几个故事（活到四十多，能看到好多人十几年的发展过程，感觉有点上帝视角了）\n我还有一个以前团队里的一个小伙，人是很聪明，但就完全就是野路子，他对技术没有什么偏好，一个PHP程序员，做那个Discuz!论坛，公司被并购了，转成Java，开始研究Java的各种细节，对技术从来没有什么偏见，有什么就玩什么，每做一个项目，就算是一样的他都要用新的技术做一遍，然后跟着我做云计算，我教他TCP，教他C/C++，后来一起玩Docker/Go，等等，反正是一点就通，他是我见过学习能力最强的人。但是，有一个事他一直与我的想法不一样，就是我希望他先把软件设计好，再写代码，他非常不能理解，他习惯于直接动手开干，然后有什么问题就整什么问题，我也很难教育他。\n有一天，他电话面了一下Facebook，电话面了15分钟后对方就放弃了，他受到了严重的打击。然后，他就开始找菲利宾人练英文口语了，我也让他做算法题，然后，他才发现，一道连算法都不是的纯编程题都提交几次都过不了，等他做完了Leetcode最初的那151道题后，整个人都改变了，写代码前认认真真地在纸上把程序的状态，处理时序以及可能遇到的一些条件先罗列出来，然后，进行逻辑设计后，再写，从此，他就开启他更大的天地了。我后来把他推荐给了微软，先在中国的Bing，在中国升好2-3级，然后去了美国的Azure，现在听说他准备要跟 k8s 的 co-founder Brendan Burns 混了（虽然，他现在还在印度人手下，但是，我真的不知道他未来能玩多大，因为今年他才33岁，而且非常聪明）\n他以前是把自己封闭起来的，我叫他出来，他也不出来，后来因为一些办公室政治的原因不得不来找我，于是我就带着他玩了两年，跟他讲了很多外面的世界是怎么玩的，他这个人也是一个相当不善于社交的人，但是心是开放的，愿意接受新的东西，虽然对技术也有一定偏见，比如不喜欢Windows，但是也不会不喜欢到完全封闭。后来我跟他说，微软的技术相当的强的，你看到的技术只是表面，深层次的东西都是相通的，直到他到了微软后发现各种牛逼的东西，对微软系统的技术的态度也有了改变，而且我让他跟我说很多微软那边的事，我发现，他对技术了解的维度已经是越来越高级的了…… 还是我以前团队的一个小伙，他是一个前端，他说前端的东西没什么意思，想来找我做后端，我也一点点带他……后来，我说，你如果想要玩得好，你必需来北京，无论现在你觉得过得有多好，你都要放弃掉，然后，尽最大可能出去经历一下世界最顶尖的公司，我甚至跟他说，如果他女朋友不跟来的话，就先分开一段时间，先自己立业，他来北京的时候，他之前的同事都等着看他的笑话，我说，那些人连想都不敢想，不必管他们。于是，他去了Amazon，再过了一年去了西雅图，我跟他说，接下来就是去AWS，然后，如果有足够的野心，用自己的年轻这个资本去硅谷创业公司赌一把……未来他怎么样我不知道，但至少他没有限制自己，他的未来不会有封顶……\n也是我的同学，我跟他在大学是上下铺，后来他去了人民大学读计算机博士，大学的时候做国产数据库kingbase，然后去了一家外企，天天被派到用户那边做数据分析，后来，他想回科研单位做国产数据库，我说，别啊，你的技术比我好太多，还有博士理论加持，你不去国外顶尖公司玩玩，你不知道自己有多强的，于是他跟公司申请去了国外做核心，后来因为Hadoop的原因，公司的产品最终成为了历史，于是我说，你来了美国么，你一定要去AWS，于是他就去了AWS的Aurora团队，成为了AWS明星级产品的中坚力量，天天在改MySQL的核心源码，干了两年，正在晋升 Principal Software Engineer ……\n这里我到不是说出国有多牛，也许你只关注能挣多少钱，但是我想说，他们之所以能有这样的际遇，除了他们本来就有实力，还更因为他们从来不给自己设制什么限制，就是那种“艺多不压身”，有什么就学什么，有更高的就去向更高的迈进，其它的像家庭什么的问题其实都是会有解的，真的不必担心太多……\n别限制了自己 上面的这些故事，也许你能看得懂，也许你看得不一定能懂，这里，让我来做个总结吧\n做有价值的事。这个世界对计算机人才的要求是供不应求的，所以，不要让自己为自己找各式各样的借口，让自己活在“玩玩具”、“搬砖”和“使蛮力加班”的境地。其实，我发现这世界上有能力的人并不少，但是有品味的人的确很少。所谓的有价值，就是，别人愿付高价的，高技术门槛的，有创造力的，有颠覆性的…… 扩大自己的眼界，开放自己的内心。人要变得开放，千万不要做一个狭隘的民族主义者，做一个开放的人，把目光放在全人类这个维度，不断地把自己融入到世界上，而不是把自己封闭起来，这里，你的英文语言能力对你能不能融入世界是起决定性的作用。开放自己的心态，正视自己的缺点，你才可能往前迈进。你的视野决定了你的知不知道要去哪，你的开放决定了你想不想去。 站在更高的维度。面的维度会超过点的维点，空间的维度会超过面的维度，在更高维度上思考和学习，你会获得更多。整天在焦虑那些低维度的事（比如自己的薪水、工作的地点、稳不稳定、有没有户口……），只会让你变得越来越平庸，只要你站在更高的维度（比如： 眼界有没有扩大、可能性是不是更多、竞争力是不是更强、能不能解决更大更难的问题、能创造多大的价值……），时间会让你明白那些低维度的东西全都不是事儿。技术学习上也一样，站在学习编程语法特性的维度和站在学习编程范式、设计模式的维度是两种完全不一样的学习方式。 精于计算得失。很多人其实不是很懂计算。绝大多数人都是在算计自己会失去多少，而不会算会得到多少。而一般的人也总是在算短期内会失去什么，优秀则总是会算我投入后未来会有什么样的回报，前者在算计今天，目光短浅，而后者则是舍在今天，得在明天，计算的是未来。*精于计算得失的，就懂得什么是投资，不懂的只会投机。对于赚钱，你可以投机，但是对于自己最好还是投资。* 勇于跳出传统的束缚。有时候，跳出传统并不是一件很容易的事，因为大多数人都会对未知有恐惧的心理。比如：我看到很多人才都被大公司垄断了，其实，有能力的人都不需要加入大公司，有能力的人是少数，这些少数的人应该是所有的公司share着用的，这样一来，对于所有的人都是利益最大化的。这样的事现在也有，比如：律师、设计师……。但是，绝大多数有能力的技术人员是不敢走出这步。我在2015年到2016年实践过一年半，有过这些实践，做“鸡”的比“二奶”好多了，收入也好很多很多（不好意思开车了）…… 庄子说过几句话——\n井蛙不可以语于海者，拘于虚也；//空间局限\n夏虫不可以语于冰者，笃于时也；//时间局限\n曲士不可以语于道者，束于教也。//认识局限\n别自己墙了自己，人最可悲的就是自己限制自己，想都不敢想，共勉！\n————————————————————\n注：这篇文章就是要劝大家更为开放，让自己有更多的可能性，能到更高的层次，做更有价值的事，成为更强更好的人……当然，如果你觉得你只想做一个平凡人，也和本文并不冲突……另外你也不要觉得这篇文章是让你要成为一个精英，但你一定要去摸高……这篇文章是告诉你一种面对人生的思考方式，在这种思考方式下，你会有更多的可能性，更大的场景……而不是直接把自己归到“平常人”，把自己“墙”了！\n注：我以为用Java适合做架构这事应该是常识了，但是评论中有很多人非常反对这个事。那我解释一下吧：首先，小型的项目用什么语言都行，爱用什么用什么。但是，真正的企业级架构就不一样了，其中并不仅仅只是RESTful API或RPC，还有各种配套设施和控制系统，比如：应用网关，服务发现、配置中心、健康检查、服务监控、服务治理（熔断、限流、幂等、重试、隔离、事务补偿）、Tracing监控、SOA/ESB、CQRS、EDA……这些东西在非Java的技术栈体系内，很难看到全貌，Java强大的生态环境，就是让你把注意力放到更高层次的架构和业务上来的。（千万不要觉得，整几个服务RPC一下，加个缓存，加个队列，就能叫架构，那只是系统集成罢了）\n","permalink":"https://pillumina.github.io/posts/programming/do-not-wall/","summary":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003e这篇文章摘自陈皓（左耳朵耗子）的blog（2019/12/01上传），作为给自己的提醒。\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e这一两周与几个朋友聊天，有年轻的90后，也有大叔级的70后，这些人在我看来都是很有能力的人，但是一些喜好过于强烈，让我不经意地回顾了我工作20年来身边的人，有发展得好的，也有发展的不好的，有些人是很可惜的，因为限制他们的不是其它人，也不是环境，而是自己，所以，很想写下这篇文章。（注：这篇文章可能会是一篇说教的文章，所以，可能会让你看着犯困，所以，我会尽量地短一些，而且尽可能多讲故事，少道理，这里的故事，全是真实发生的）\u003c/p\u003e\n\u003ch2 id=\"几个故事\"\u003e几个故事\u003c/h2\u003e\n\u003cp\u003e2019年年初，我面试了一个很年轻的小伙子（93/94年出生），这个小伙子特别有灵性，也很聪明，计算机专业出身，也很喜欢技术，基础和学习能力也很好。在我这20年来认识的人中，如果他能呆在北京、上海、深圳这样的城市，我保证不出三年，他会成为他们同龄人中非常出色的技术人员，如果有个好的舞台有一个好的团队带他，他的未来会非常成功。然而，这个小伙子有两大喜好：1）只愿（或是说被迫）呆在一个毫无IT的环境的三/四线城市，2）对技术有非常大的偏好，只喜欢Go语言，非常不喜欢其它的语言，比如：Java（离开Java的世界，基本上离开了做架构的世界（\u003cstrong\u003e相关解释见文末\u003c/strong\u003e））。\u003c/p\u003e\n\u003cp\u003e他的这两个喜好，足以让一个未来会很优秀的人毁掉，因为，这个时代没有限制他，他的能力也没有限制他，但是他的意识完完全全地限制了他。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e他把自己最宝贵的青春放在了很烂的项目上，就算能用一些新的技术，他也只能算是自娱自乐，在实验室中玩玩具罢了。\u003c/li\u003e\n\u003cli\u003e他把自己的技术栈封闭起来，而直接放弃了这个时代最具工业化的技术Java，对于一个好的程序员来说，同时掌握几门语言和技术完全是没什么问题，但是自己封闭了自己的视野。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e实在是非常可惜，我本来是可以为他介绍到一些很不错的公司的，但是他这样的习性，等于自己把自己未来的门给关上了，虽然我跟他长谈过，但是我也没有办法叫醒不想醒的人……\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e视野、环境和舞台，对一个人的限制是非常大的。井蛙不知道大海，被空间维度所限制；夏虫不知道冬天，是被时间维度所限制；圈养的动物没有斗志，是被自己意识所限制。\u003c/li\u003e\n\u003cli\u003e偏见和不开放，对一个人的限制是真正有毁灭性的。主动让自己成为一个瞎子和聋子，主动把自己的能力阉割掉，这是一件令人痛心的事。想想大清的闭关锁国是如何让亚洲第一的北洋水师给毁掉的……\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我还有个同学，他的技术并不差，就算呆在昆明这种很落后的地方，他也非常地好学，学习英文，学习各种新技术，对技术没有任何的偏好，喜欢C/C++/Java/Python/Shell，同样喜欢前端Javascript，对基础知识非常地踏实，他在技术上没有限制自己的潜力，有什么就学什么。后来，我带他玩Docker/Go/K8S……分布式架构，他也上手的很快……像他这样的人，技术能力完全没得说，比我还大一岁，44岁了，还是一样的天天追代码细节，看Youtube的各种大会，翻github里的各种issue和pull request……\u003c/p\u003e\n\u003cp\u003e我同学这人，拥有了成为一个技术牛人几乎所有的条件：基础知识过硬，细节扎得深，面很广，学习能力强，有英文能力，逻辑思维能力不错，非常的自律，执行力也很强，抓得住重点……然而，只有一个小问题，就是没有到大公司历练过，我三番五次叫他从昆明出来，但是最终他都呆在昆明这个城市没有出来，因为有所谓的家庭约束。然而，我身边还有好些人，把自己家从北京搬到上海，从上海搬到深圳，从厦门搬到深圳……这样的人大有人在……像他这样的能力，在哪个公司都会是主力和骨干，对于一个公司的主力和骨干来说，家庭上的这些问题都是小问题都是有很多解的……\u003c/p\u003e\n\u003cp\u003e另外，我这个同学还是一个比较悲观的人，任何事情都是先想到不好的事，他关注负面的东西会胜于正面的东西，而且他还有一定的社交恐惧，怕与人相处和交流，时间越长越害怕，甚至有时候直接跟我说，“我就是不想改变”这样的话……其实，我以前也是一个很害怕与人交流的人，面试的时候，我根本不敢正眼看面试官一眼，也不知道与人怎么交流。但是，我与他不一样，我努力克服，不断地面试，与人面对面的交流，到一线技术客服接用户的电话，在公司里做分享，慢慢地到外面分享……3-5年就完全克服掉了。\u003c/p\u003e\n\u003cp\u003e其实，很多事情，完全是有解的，也没有必要担心，自己的心理障碍也是可以克服的，重点就是自己愿不愿意，只要愿意完成了一半，接下来就是不断的摸爬滚打坚持了。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e不限制自己的人，会穷举各种方法来解决问题，限制自己的人，只会找各式各样的问题或借口。\u003c/li\u003e\n\u003cli\u003e不限制自己的人，会努力改变自己的问题和缺陷，限制自己的人，会放任自己。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"另外几个故事\"\u003e另外几个故事\u003c/h2\u003e\n\u003cp\u003e我还有另外几个故事（活到四十多，能看到好多人十几年的发展过程，感觉有点上帝视角了）\u003c/p\u003e\n\u003cp\u003e我还有一个以前团队里的一个小伙，人是很聪明，但就完全就是野路子，他对技术没有什么偏好，一个PHP程序员，做那个Discuz!论坛，公司被并购了，转成Java，开始研究Java的各种细节，对技术从来没有什么偏见，有什么就玩什么，每做一个项目，就算是一样的他都要用新的技术做一遍，然后跟着我做云计算，我教他TCP，教他C/C++，后来一起玩Docker/Go，等等，反正是一点就通，他是我见过学习能力最强的人。但是，有一个事他一直与我的想法不一样，就是我希望他先把软件设计好，再写代码，他非常不能理解，他习惯于直接动手开干，然后有什么问题就整什么问题，我也很难教育他。\u003c/p\u003e\n\u003cp\u003e有一天，他电话面了一下Facebook，电话面了15分钟后对方就放弃了，他受到了严重的打击。然后，他就开始找菲利宾人练英文口语了，我也让他做算法题，然后，他才发现，一道连算法都不是的纯编程题都提交几次都过不了，等他做完了Leetcode最初的那151道题后，整个人都改变了，写代码前认认真真地在纸上把程序的状态，处理时序以及可能遇到的一些条件先罗列出来，然后，进行逻辑设计后，再写，从此，他就开启他更大的天地了。我后来把他推荐给了微软，先在中国的Bing，在中国升好2-3级，然后去了美国的Azure，现在听说他准备要跟 k8s 的 co-founder \u003ca href=\"https://github.com/brendandburns\"\u003eBrendan Burns\u003c/a\u003e 混了（虽然，他现在还在印度人手下，但是，我真的不知道他未来能玩多大，因为今年他才33岁，而且非常聪明）\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e他以前是把自己封闭起来的，我叫他出来，他也不出来，后来因为一些办公室政治的原因不得不来找我，于是我就带着他玩了两年，跟他讲了很多外面的世界是怎么玩的，他这个人也是一个相当不善于社交的人，但是心是开放的，愿意接受新的东西，虽然对技术也有一定偏见，比如不喜欢Windows，但是也不会不喜欢到完全封闭。后来我跟他说，微软的技术相当的强的，你看到的技术只是表面，深层次的东西都是相通的，直到他到了微软后发现各种牛逼的东西，对微软系统的技术的态度也有了改变，而且我让他跟我说很多微软那边的事，我发现，他对技术了解的维度已经是越来越高级的了……\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e还是我以前团队的一个小伙，他是一个前端，他说前端的东西没什么意思，想来找我做后端，我也一点点带他……后来，我说，你如果想要玩得好，你必需来北京，无论现在你觉得过得有多好，你都要放弃掉，然后，尽最大可能出去经历一下世界最顶尖的公司，我甚至跟他说，如果他女朋友不跟来的话，就先分开一段时间，先自己立业，他来北京的时候，他之前的同事都等着看他的笑话，我说，那些人连想都不敢想，不必管他们。于是，他去了Amazon，再过了一年去了西雅图，我跟他说，接下来就是去AWS，然后，如果有足够的野心，用自己的年轻这个资本去硅谷创业公司赌一把……未来他怎么样我不知道，但至少他没有限制自己，他的未来不会有封顶……\u003c/p\u003e\n\u003cp\u003e也是我的同学，我跟他在大学是上下铺，后来他去了人民大学读计算机博士，大学的时候做国产数据库kingbase，然后去了一家外企，天天被派到用户那边做数据分析，后来，他想回科研单位做国产数据库，我说，别啊，你的技术比我好太多，还有博士理论加持，你不去国外顶尖公司玩玩，你不知道自己有多强的，于是他跟公司申请去了国外做核心，后来因为Hadoop的原因，公司的产品最终成为了历史，于是我说，你来了美国么，你一定要去AWS，于是他就去了AWS的Aurora团队，成为了AWS明星级产品的中坚力量，天天在改MySQL的核心源码，干了两年，正在晋升 Principal Software Engineer ……\u003c/p\u003e\n\u003cp\u003e这里我到不是说出国有多牛，也许你只关注能挣多少钱，但是我想说，他们之所以能有这样的际遇，除了他们本来就有实力，还更因为他们从来不给自己设制什么限制，就是那种“艺多不压身”，有什么就学什么，有更高的就去向更高的迈进，其它的像家庭什么的问题其实都是会有解的，真的不必担心太多……\u003c/p\u003e\n\u003ch2 id=\"别限制了自己\"\u003e别限制了自己\u003c/h2\u003e\n\u003cp\u003e上面的这些故事，也许你能看得懂，也许你看得不一定能懂，这里，让我来做个总结吧\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e做有价值的事\u003c/strong\u003e。这个世界对计算机人才的要求是供不应求的，所以，不要让自己为自己找各式各样的借口，让自己活在“玩玩具”、“搬砖”和“使蛮力加班”的境地。其实，我发现这世界上有能力的人并不少，但是有品味的人的确很少。\u003cstrong\u003e所谓的有价值，就是，别人愿付高价的，高技术门槛的，有创造力的，有颠覆性的\u003c/strong\u003e……\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e扩大自己的眼界，开放自己的内心\u003c/strong\u003e。人要变得开放，千万不要做一个狭隘的民族主义者，做一个开放的人，把目光放在全人类这个维度，不断地把自己融入到世界上，而不是把自己封闭起来，这里，\u003cstrong\u003e你的英文语言能力对你能不能融入世界是起决定性的作用\u003c/strong\u003e。开放自己的心态，正视自己的缺点，你才可能往前迈进。\u003cstrong\u003e你的视野决定了你的知不知道要去哪，你的开放决定了你想不想去\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e站在更高的维度\u003c/strong\u003e。面的维度会超过点的维点，空间的维度会超过面的维度，在更高维度上思考和学习，你会获得更多。\u003cstrong\u003e整天在焦虑那些低维度的事（比如自己的薪水、工作的地点、稳不稳定、有没有户口……），只会让你变得越来越平庸，只要你站在更高的维度（比如： 眼界有没有扩大、可能性是不是更多、竞争力是不是更强、能不能解决更大更难的问题、能创造多大的价值……），时间会让你明白那些低维度的东西全都不是事儿\u003c/strong\u003e。技术学习上也一样，站在学习编程语法特性的维度和站在学习编程范式、设计模式的维度是两种完全不一样的学习方式。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e精于计算得失\u003c/strong\u003e。很多人其实不是很懂计算。绝大多数人都是在算计自己会失去多少，而不会算会得到多少。而一般的人也总是在算短期内会失去什么，优秀则总是会算我投入后未来会有什么样的回报，前者在算计今天，目光短浅，而后者则是舍在今天，得在明天，计算的是未来。\u003cem\u003e\u003cstrong\u003e*精于计算得失的，就懂得什么是投资，不懂的只会投机。对于赚钱，你可以投机，但是对于自己最好还是投资。*\u003c/strong\u003e\u003c/em\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e勇于跳出传统的束缚\u003c/strong\u003e。有时候，跳出传统并不是一件很容易的事，因为大多数人都会对未知有恐惧的心理。比如：我看到很多人才都被大公司垄断了，其实，有能力的人都不需要加入大公司，有能力的人是少数，这些少数的人应该是所有的公司share着用的，这样一来，对于所有的人都是利益最大化的。这样的事现在也有，比如：律师、设计师……。但是，绝大多数有能力的技术人员是不敢走出这步。我在2015年到2016年实践过一年半，有过这些实践，做“鸡”的比“二奶”好多了，收入也好很多很多（不好意思开车了）……\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e庄子说过几句话——\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e井蛙不可以语于海者，拘于虚也；//空间局限\u003c/p\u003e\n\u003cp\u003e夏虫不可以语于冰者，笃于时也；//时间局限\u003c/p\u003e\n\u003cp\u003e曲士不可以语于道者，束于教也。//认识局限\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e别自己墙了自己，人最可悲的就是自己限制自己，想都不敢想，共勉！\u003c/p\u003e\n\u003cp\u003e————————————————————\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e注：这篇文章就是要劝大家更为开放，让自己有更多的可能性，能到更高的层次，做更有价值的事，成为更强更好的人……当然，如果你觉得你只想做一个平凡人，也和本文并不冲突……另外你也不要觉得这篇文章是让你要成为一个精英，但你一定要去摸高……这篇文章是告诉你一种面对人生的思考方式，在这种思考方式下，你会有更多的可能性，更大的场景……而不是直接把自己归到“平常人”，把自己“墙”了！\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e注：我以为用Java适合做架构这事应该是常识了，但是评论中有很多人非常反对这个事。那我解释一下吧：首先，小型的项目用什么语言都行，爱用什么用什么。但是，真正的企业级架构就不一样了，其中并不仅仅只是RESTful API或RPC，还有各种配套设施和控制系统，比如：应用网关，服务发现、配置中心、健康检查、服务监控、服务治理（熔断、限流、幂等、重试、隔离、事务补偿）、Tracing监控、SOA/ESB、CQRS、EDA……这些东西在非Java的技术栈体系内，很难看到全貌，\u003cstrong\u003eJava强大的生态环境，就是让你把注意力放到更高层次的架构和业务上来的\u003c/strong\u003e。（千万不要觉得，整几个服务RPC一下，加个缓存，加个队列，就能叫架构，那只是系统集成罢了）\u003c/p\u003e","title":"(转)别让自己墙了自己"},{"content":"此文转自曹春晖blog，也用于自己学习的一些参考\n博客地址\n阅读书籍 对于工程师来说，从书籍得来的知识是必不可少的。现在很多年轻的程序员会从网络博客来学习技术，但博客内容大多缺乏体系(主要说总结性质的博客内容)，不系统。很多博主为了掩饰自己的未知，遇到不知道的关键点就一笔带过，进而导致缺失。即使原作者非常努力，内容上没有缺失，你能从中获取的也只是别人总结好的知识，没有自己的主动思考，这中间便缺少过程式的沉淀，一味地满足于背诵别人总结好的知识，最后也只不过沦为他人的复读机而已。\n对于工程师来说，书籍依然是最重要的知识获取媒介。即使只是通过目录概览，也能获取某个领域的大致蓝图。\n目前大部分优秀的技术书籍依然以英文为主，能够读懂英文技术书籍是工程师的硬实力。英语阅读能力怎么训练呢？如果不是为了应试，可以尝试逼迫自己去翻译一些英文文档/文章来进行专门训练。举个例子，为了学习 Go，笔者曾经和社区的小伙伴合作翻译过《The Go Programming Language》，后来为了深入学习 es，参与了社区的 《es 权威指南》 的翻译和校对工作。如果某篇技术文档你从生理上很厌恶它，但是又觉得内容不得不学习的话，那你就逼迫自己去翻译它。千字以内的文档，周末抽一个下午就可以搞定。这里已经说是逼迫了，自然可以想见这个过程并不像打游戏那么轻松愉快，但只要熬过了这个阶段，阅读技术相关的英文文档可以显著提升速度。\n具备一定的英语能力之后，接下来就是从什么渠道去获取内容。如果对电子书不排斥的话，目前比较经济的选择是申请一个国内的 ACM 会员，并且用该会员去注册oreily 在线书店。一年大概 20 美元，可以及时地阅读到大部分出版社的技术出版物。因为现在出版社的网站大多还支持 early preview，所以你甚至可以在书籍还没有上市之前就预先学习内容，第一时间获取整个业界的一线情报，能够帮助你站在时代的潮头。等两年后同事拿到翻译生涩的中文版的时候，你已经可以从容地 diss 他在信息源上落后了自己整整两年。(当然，如果你有兴趣，国内的出版社一般在英文新书出版的时候会招募翻译志愿者，如果你对一个领域特别感兴趣，关注出版社的这些招募消息也可以去参与。这里要指出，不要对做翻译这件事情抱太高期望，重在提升自己的能力。想赚钱的话就算了。)\n如果喜欢一边阅读一边在页边写写画画，电子书还是稍微困难点，当然这个问题你也可以用 ipad pro + apple pencil 来解决。但有些人比较喜欢实体书捧在手上的实感，国外有些书甚至是个人出版物，例如笔者之前购入的《timeless law of software development》，这些书在互联网上正盗均无，只能考虑通过海淘渠道入手。前些年淘宝的海淘服务也可以用来淘书，书籍大多比较重，运费不菲。有些老书运费可能会到书费的一半让人格外肉疼。又因为国内对出版物管制比较严格，所以为了避险，这两年这些海淘服务商基本上都不帮忙代购出版物了。对于个人用户来说，也就只剩下了直邮和转运两种选择。直邮虽然比较便宜，但是万国联盟的 EMS 一走到中国可能就上了牛车，速度慢到突破极限。基本脑子稍微正常的都会选择转运。除了速度之外，有些出版社的书籍是只在美帝本土销售的，不支持 global delivery。\n走转运的话，需要办一张 visa 或者 mastercard 的多币种信用卡，这样才能在海外网站进行支付 。\n上面这些问题都解决了以后，书籍的获取就不再是问题。\n信息源 虽然文章开头对于传统的书籍大赞特赞，但书籍的缺点也是显而易见的。在技术领域，这个缺点就尤为明显：时效问题。如果我们所学习的是相对比较老的领域知识，那只要读书基本上就够了。\n但技术的发展日新月异，如果你想要成为知晓领域内所有新技术的那个人。你还是需要关注一些书籍以外的新闻源，下面是一些例子：\nGithub Trending Github Trending 代表的是一种风向，一般一个项目能上 trending 的话，可能是作者自己去 hacker news、reddit 做了宣传，也可能是被某个业界大佬带了流量。因为现在 Github 上的国人越来越多，很多国人学习技术比较显著的诉求其实只是面试(汗，一旦什么 xx interview/xx road to 架构师之类的仓库开了就会吸引一大批国人点星，近两年 trending 的质量有下跌趋势。\n不过最近 Github Trending 增加了按照 Spoken Language 筛选功能，所以你可以过滤特定的语言，相对比之前还是好多了。\nGithub 的 trending 一般按照语言区分，如果你想要成为某门语言的专家，那这种语言每天世界各地的人又造了什么新轮子上榜了总还是需要知道的。星星特别多的 Repo 去读读源代码也是一种乐趣。\n除了按照时间和星星来进行聚合，Github 上也有人开了各种 awesome-xxx 的总结页，这种仓库对于学习语言或者特定领域的技术来说也是很实用的。如果你恰巧又发现了领域的空白，拿自己的业余时间去造个轮子，也可以提 PR 进行收录。因为 awesome-xxx 大多是个人项目，因此作者一般还是比较友好的。不过现在 awesome-xxx 的项目越来越多，就有人会想去做一些 awesome 仓库的汇总。然后这些 awesome 的汇总又越来越多，就会有人继续向上进行汇总。所以就陆续有了 awesome-xxx，awesome-awesome，awesome-awesome-awesome。。。真是工程师们的黑色幽默。\nfollow 优秀的工程师 在世界各地有很多杰出的工程师，每天每月每年活跃在 Github 上。我们总是希望自己能在技术上做到一直精进，同时随着年龄和工龄的增长又会时不时陷入迷茫，这时候去看看同龄的优秀工程师，年纪更大的优秀工程师在这个时间段在写什么代码，在写什么博客，可能对于解决自己特定时期的迷茫有益。或许就发现了一个新的领域值得自己去奉献青春。\n在起步阶段你可能不知道该从哪里去找这些优秀的工程师，也有几个源头，如果你阅读到了精辟的代码，觉得作者水平拔群，那么马上去 Github 找到这个项目，并 follow 作者就是一个比较好的选择。如果你在某个地方听人说起了一个传奇的程序员，比如 geohot？那么在 Github 就赶紧 follow 他。如果你发现自己 follow 的人又 follow 了别人，那么也可以对这些延伸关系链上的人进行考察，如果很对自己胃口的话，同样可以考虑。\n在 follow 了足够的人之后，你的 Github 首页就能看到这些人每天的动作了。又是一个新的新闻源。\nreddit 相关社区 很多火起来的语言、技术领域社区在国内其实都有点分裂，大家都想要占个山头当大佬。而每个社区也都零零碎碎地汇集了一些好的内容。但因为谁也不服谁，国内的社区生态被人为地割裂了。我们没有办法在同一个社区获取或者搜索到所有我们想要知道的技术文章、问题、思考、总结。所以我建议还是去关注国外的社区吧。\nreddit 是一个不错的选择。当然，因为敏感词的关系，这个社区本身是被强了的。这里需要考察到你的越墙技能。\nreddit 的 rust 社区。\nThe Morning Paper 除了工程以外，在某个领域做得稍深之后，我们就需要去关注更前沿的理论部分了。作为一个工作了的码农，想要研习理论需要去阅读一些每年新发表的论文。在学校里读过一点论文的基本都知道，再好的文章因为一些荒唐的理由，都会带有大量的废话。\nThe Morning Paper 是一个国外某企业的 CTO(CTO 还天天读论文，神奇)创建的个人 blog，其中会帮助我们去解读很多新论文，省去了我们去阅读大量废话以及对于工业界人来说不那么重要的公式的时间。只当了解的话，是个非常不错的渠道。\n技术会议和公开课 除了阅读文字，阅览视频和与人面对面交流在有些时候也是必不可少的。有些自己冥思苦想而不得解的问题，在牛人的点播下片刻茅塞顿开。这也是所谓听君一席话胜读十年书的充分体现。比我们工作时间长，且经常总结的人经常能够给后入行的人带来广阔的视野和冲击性的理论，这些是文字所不能带给我们的。\n当然，在技术会议上听分享也要保持自己的头脑清醒，不要被那些天花乱坠的概念所吓退。有些人的分享很功利，其实只是一些旧概念的包装。而这些人出现在现场的目的也可能重在宣传自己方便跳槽，不要被骗了。在听主题的时候稍微有一些选择性，尽量避开这种天花乱坠的忽悠选手。\n除了技术会议，ytb 上有很多优秀的公开课。从现在往前推十年的话，很多人抱怨因为经济、家庭原因，没有机会出国深造，接受世界一流大学的计算机教育。而公开课的出现已经使财务、距离对我们的限制消失了。在学校里学习的时候，一门课程也就 36-48 个课时，外加上思考、作业的时间，就算是 72-96 个小时。对于已经工作的社畜来说，一个月的周末有 8 天，稍微努努力，基本上一个月就可以学完一门公开课了。这既可以弥补你学生时代的遗憾，同时也可以填补某些人嘴里所谓的虚无缥缈的“基础不行”。吊打某些自视甚高的老油条。国外的计算机课程往往会随着工业界的发展而随时更新，例如早期 Sony 的 Playstation 3 上市后，便有学校的体系架构课专门讲解 IBM Cell 处理器的架构设计，前年或更早的时候，已经有学校向学生讲解 Intel i7 处理器中的一些更现代的设计理念。这可比一些工作十年但从来不关注新技术的人所告诉你的结论强太多了。\n多做开源 有些同学，在阅读了大量的代码、设计方法论之后，可能依然在工作时陷入焦虑。为什么我们在企业内维护的代码都是一坨屎，为什么我每天写的东西就是在屎山上堆屎？\n大多数公司的软件生命周期其实很短，开源界没有靠谱的解决方案时，为了解决一些临时问题，企业倾向于以最小资源获取最大收益，即使是纯技术项目，我们在做的也都是一些丑陋的临时解决方案。甚至有些 MVP 版本产品的代码能活过一个星期就不错了。有些企业的内部技术工具虽然从性质上可以开源，但为什么开不了？还不是因为历史包袱重，代码写的屎。有些时候不是工程师不追求优秀的代码，工期紧张的情况下写着写着就变形了。\n好在业余时间是属于我们自己的，我们可以用最严格的标准来要求自己编写自己的开源项目。并且以产品的方式来对这些项目进行运营，这不仅能提升我们的技术能力，同时也会给我们带来更多的机会。如果自己的开源项目成功的话，同时可以给自己带来更大的业界影响力，何乐而不为？\n多做总结 在有了足够的信息获取渠道，和个人能力提升手段之后，还应该多进行总结。对于工作 3 年以上的工程师，总结并不只是为了面试。一个人的工作内容一定会随着时间的推进而不断变化，工作领域也可能在跳槽之后发生变化，怎么让自己的历史积累不成为时代的眼泪？就是要做总结。\n比较好的总结手段有：\n建立自己的测试代码库 这一点我认为做的最好的是，learn，这是一个韩国裔的工程师。如果从大学开始就建立这样的习惯的，在技术上走过的所有的路都可以清晰可见。\n如果自己的总结能够让别人看上去也赏心悦目，那还是比较成功的。即使做不到这种程度，我们建立自己的代码库之后，想要搜索一些拿来演示的代码片段也会方便不少。\n建立自己的 blog 博客是个人思考的轨迹，我们的工作并不是简单的完成任务。在完成任务的同时应提炼自己的方法论，逐渐形成自己看待技术问题，看待业务，看待公司，看待业界乃至看待整个世界的观点。独立思考是一个人最重要的品质。\n从进入滴滴开始，我写个人 blog 也大概有 4 年有余，即使初始只能写一些简单的内容，到现在我已经可以从任意我想要的角度切入进行较为完备的分析和观点总结。\n对于工程师来说，除了代码能力之外，文字能力也是需要进行训练的。能够把事情做好，还要能在必要的时候，通过文章来把自己的工作宣传出去。建立自己的个人品牌。\n建立自己的笔记库 除了完整的博客输出，有一些零散的知识不好组织，便可以放在自己的笔记里，这些可以是别人的分享的备份，也可能只是记录一些简单的命令。记录笔记是很好的习惯，因为一个人不可能永远记得所有工具的使用方法。必要的时候通过查阅笔记快速地回忆起来就已经足够了。\n锻炼演技 因为工程师工作 80% 的时间都是在和技术、代码打交道，所以也比较容易忽视一些软技能的训练，这里不说一些敏感的内容，只说说口才。据我观察，大多数的工程师其实并没有意识到这是一个问题。即使在纯技术人员交流的场合也会发现有些人的表达能力著实一般，其它技术人员理解起来都费劲，何况去和非技术人员交流。\n人这一辈子，最重要的是能把路越走越宽。对于工程师来说，能够锻炼软技能的场合其实不是很多，但也不代表完全没有。即使没有也可以自己创造机会，例如组内、组间、部门内的技术分享都是不错的机会。\n更大规模的技术分享可能因为主办方“势力眼”，在你级别不高或者影响力不大的时候，不提供给你这样的机会，但是作为一个向上的人，迟早会有走到这一步的一天。你所要做的是提前做好准备，在那一天到来的时候，在聚光灯下旁征博引，谈笑风生。\n祝大家都能成为更好的自己！\n","permalink":"https://pillumina.github.io/posts/programming/how-to-learn/","summary":"\u003cp\u003e\u003ccode\u003e此文转自曹春晖blog，也用于自己学习的一些参考\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://xargin.com/every-arch-will-finally-become-shit/\"\u003e博客地址\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"阅读书籍\"\u003e阅读书籍\u003c/h2\u003e\n\u003cp\u003e对于工程师来说，从书籍得来的知识是必不可少的。现在很多年轻的程序员会从网络博客来学习技术，但博客内容大多缺乏体系(主要说总结性质的博客内容)，不系统。很多博主为了掩饰自己的未知，遇到不知道的关键点就一笔带过，进而导致缺失。即使原作者非常努力，内容上没有缺失，你能从中获取的也只是别人总结好的知识，没有自己的主动思考，这中间便缺少过程式的沉淀，一味地满足于背诵别人总结好的知识，最后也只不过沦为他人的复读机而已。\u003c/p\u003e\n\u003cp\u003e对于工程师来说，书籍依然是最重要的知识获取媒介。即使只是通过目录概览，也能获取某个领域的大致蓝图。\u003c/p\u003e\n\u003cp\u003e目前大部分优秀的技术书籍依然以英文为主，能够读懂英文技术书籍是工程师的硬实力。英语阅读能力怎么训练呢？如果不是为了应试，可以尝试逼迫自己去翻译一些英文文档/文章来进行专门训练。举个例子，为了学习 Go，笔者曾经和社区的小伙伴合作翻译过\u003ca href=\"https://github.com/gopl-zh/gopl-zh.github.com\"\u003e《The Go Programming Language》\u003c/a\u003e，后来为了深入学习 es，参与了社区的 \u003ca href=\"https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html\"\u003e《es 权威指南》\u003c/a\u003e 的翻译和校对工作。如果某篇技术文档你从生理上很厌恶它，但是又觉得内容不得不学习的话，那你就逼迫自己去翻译它。千字以内的文档，周末抽一个下午就可以搞定。这里已经说是逼迫了，自然可以想见这个过程并不像打游戏那么轻松愉快，但只要熬过了这个阶段，阅读技术相关的英文文档可以显著提升速度。\u003c/p\u003e\n\u003cp\u003e具备一定的英语能力之后，接下来就是从什么渠道去获取内容。如果对电子书不排斥的话，目前比较经济的选择是申请一个国内的 ACM 会员，并且用该会员去注册\u003ca href=\"https://learning.oreilly.com/home/\"\u003eoreily 在线书店\u003c/a\u003e。一年大概 20 美元，可以及时地阅读到大部分出版社的技术出版物。因为现在出版社的网站大多还支持 early preview，所以你甚至可以在书籍还没有上市之前就预先学习内容，第一时间获取整个业界的一线情报，能够帮助你站在时代的潮头。等两年后同事拿到翻译生涩的中文版的时候，你已经可以从容地 diss 他在信息源上落后了自己整整两年。(当然，如果你有兴趣，国内的出版社一般在英文新书出版的时候会招募翻译志愿者，如果你对一个领域特别感兴趣，关注出版社的这些招募消息也可以去参与。这里要指出，不要对做翻译这件事情抱太高期望，重在提升自己的能力。想赚钱的话就算了。)\u003c/p\u003e\n\u003cp\u003e如果喜欢一边阅读一边在页边写写画画，电子书还是稍微困难点，当然这个问题你也可以用 ipad pro + apple pencil 来解决。但有些人比较喜欢实体书捧在手上的实感，国外有些书甚至是个人出版物，例如笔者之前购入的《timeless law of software development》，这些书在互联网上正盗均无，只能考虑通过海淘渠道入手。前些年淘宝的海淘服务也可以用来淘书，书籍大多比较重，运费不菲。有些老书运费可能会到书费的一半让人格外肉疼。又因为国内对出版物管制比较严格，所以为了避险，这两年这些海淘服务商基本上都不帮忙代购出版物了。对于个人用户来说，也就只剩下了直邮和转运两种选择。直邮虽然比较便宜，但是万国联盟的 EMS 一走到中国可能就上了牛车，速度慢到突破极限。基本脑子稍微正常的都会选择转运。除了速度之外，有些出版社的书籍是只在美帝本土销售的，不支持 global delivery。\u003c/p\u003e\n\u003cp\u003e走转运的话，需要办一张 visa 或者 mastercard 的多币种信用卡，这样才能在海外网站进行支付 。\u003c/p\u003e\n\u003cp\u003e上面这些问题都解决了以后，书籍的获取就不再是问题。\u003c/p\u003e\n\u003ch2 id=\"信息源\"\u003e信息源\u003c/h2\u003e\n\u003cp\u003e虽然文章开头对于传统的书籍大赞特赞，但书籍的缺点也是显而易见的。在技术领域，这个缺点就尤为明显：时效问题。如果我们所学习的是相对比较老的领域知识，那只要读书基本上就够了。\u003c/p\u003e\n\u003cp\u003e但技术的发展日新月异，如果你想要成为知晓领域内所有新技术的那个人。你还是需要关注一些书籍以外的新闻源，下面是一些例子：\u003c/p\u003e\n\u003ch3 id=\"github-trending\"\u003eGithub Trending\u003c/h3\u003e\n\u003cp\u003eGithub Trending 代表的是一种风向，一般一个项目能上 trending 的话，可能是作者自己去 hacker news、reddit 做了宣传，也可能是被某个业界大佬带了流量。因为现在 Github 上的国人越来越多，很多国人学习技术比较显著的诉求其实只是面试(汗，一旦什么 xx interview/xx road to 架构师之类的仓库开了就会吸引一大批国人点星，近两年 trending 的质量有下跌趋势。\u003c/p\u003e\n\u003cp\u003e不过最近 Github Trending 增加了按照 Spoken Language 筛选功能，所以你可以过滤特定的语言，相对比之前还是好多了。\u003c/p\u003e","title":"(转)工程师应该如何高效学习"},{"content":"这篇文章是我研究高负载网络服务器架构看到的的一个有趣的story，添加了我自身学习websocket的感受和记录，希望我能在飞机落地前写完:-)\nPreface 我们先描述一个问题作为讨论的中心：用户邮件的存储方法。\n对于这种主题，有很多种方式在系统内对邮件状态进行持续的追踪，比如系统事件是一个方式，另一种方式可以通过定期的系统轮询有关状态变化。\n这两种方式各有利弊，不过当我们讨论到邮件的时候，用户希望收到新邮件的速度越快越好。邮件轮询每秒约有50000个HTTP请求，其中60%返回304状态，也就是邮箱内没有任何修改。\n因此，为了减少服务器的负载并加快向用户传递邮件的速度，我们决定通过编写publisher-subscriber服务器(即bus, message broker, event channel)来重新发明轮子。一方面接受有关状态变更的通知，另外一个方面接受此类通知的订阅。\n改进前：\n+--------------+ (2) +-------------+ (1) +-----------+ | | \u0026lt;--------+ | | \u0026lt;--------+ | | | Storage | | API | HTTP | Browser | | | +--------\u0026gt; | | +--------\u0026gt; | | +--------------+ (3) +-------------+ (4) +-----------+ 改进后:\n+--------------+ +-------------+ WebSocket +-----------+ | Storage | | API | +----------\u0026gt; | Browser | +--------------+ +-------------+ (3) +-----------+ + ^ | (1) | (2) v + +-----------------------------------------+ | Bus | +-----------------------------------------+ 改进前的方案也就是browser定期去查询api并访问存储更改\n改进后的方案描述了新的架构，browser和通知api建立websocket连接，通知api是总线服务器的客户端，收到新的电子邮件后，storage会将它的通知发送到总线，并将总线发送给其subscribers。api确定发送接收通知的连接，并将其发送到用户的浏览器。\n这里我们将讨论API或Websocket服务器，最后我会告诉你这个服务器能够保持三百万的在线连接。\n常见方式 我们先来看在没有任何优化的情况下使用Go功能实现服务器的某个部分。在使用net/http\t之前，先来看看如何去接受和发送数据。注意，基于WebSocket协议的数据(例如JSON对象)在上下文中被称为packets(分组)。\nChannel struct 先来实现Channel，它包含通过WebSocket连接发送和接受此类数据包的逻辑结构\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // Packet represents application level data. type Packet struct { } // Channel wraps user connection. type Channel struct { conn net.Conn // WebSocket connection send chan Packet // Outgoing packets queue } func NewChannel(conn net.Conn) *Channel { c := \u0026amp;Channel{ conn: conn, send: make(chan Packet, N), } go c.reader() go c.writer() return c } 这里有个信息需要重视，也就是这两个reader/writer的goroutine，每一个goroutine需要自己的内存栈，初始大小为2~8KB，取决于操作系统和Go版本。根据上面提到的三百万在线连接的数量，我们需要24GB的内存(设堆栈为4KB)来用于存储所有连接，这里甚至还没有为Channel结构，以及传出数据库包ch.send和其他内部字段分配内存。可见问题比较大。\nI/O goroutine 我们来看看 reader 的实现：\n1 2 3 4 5 6 7 8 func (c *Channel) reader() { // We make a buffered read to reduce read syscalls. buf := bufio.NewReader(c.conn) for { pkt, _ := readPacket(buf) c.handle(pkt) } } 这里我们使用 bufio.Reader 来减少 read() 系统调用的数量，并读取 buf 缓冲区大小允许的数量。在无限循环中，我们_期待新数据的到来_。注意：是_期待新数据的到来_，我们一会儿再仔细讨论这一点。\n我们不考虑传入数据包的解析和处理，因为它对我们将讨论的优化并不重要。但是，buf 现在值得我们注意：默认情况下，它为 4KB，这意味着我们的连接还剩余 12 GB 内存没有使用。同样的，我们可以实现 writer：\n1 2 3 4 5 6 7 8 9 func (c *Channel) writer() { // we make buffered write to reduce write syscalls. buf := bufio.NewWriter(c.conn) for pkt := range c.send { _ := writePacket(buf, pkt) buf.Flush() } } HTTP 我们已经写好了一个简单的 Channel 实现，现在我们需要制造一个 WebSocket 连接来协同工作。由于我们任然处于_常见做法_一节中，因此我们不妨也用常见的方式来完成。\n注意：如果你不知道 WebSocket 的工作原理，值得一提的就是客户端通过一个特殊的 HTTP Upgrade 机制来切换到 WebSocket 协议。成功处理 Upgrade 请求后，服务器和客户端将使用 TCP 连接来交换 Websocket 的二进制帧。这里 给出了连接内帧结构的描述。\n1 2 3 4 5 6 7 8 9 10 import ( \u0026#34;net/http\u0026#34; \u0026#34;some/websocket\u0026#34; ) http.HandleFunc(\u0026#34;/v1/ws\u0026#34;, func(w http.ResponseWriter, r *http.Request) { conn, _ := websocket.Upgrade(r, w) ch := NewChannel(conn) // ... }) 请注意，http.ResponseWriter 会为 bufio.Reader 和 bufio.Writer 分配内存（各需要 4KB 的缓存）来初始化 *http.Request 和之后的响应写入。\n无论使用哪种 WebSocket 库，在成功响应 Upgrade 请求后，在 responseWriter.Hijack() 调用后服务器会收到 IO 缓存和 TCP 连接。\n提示：在某些情况下，go:linkname 可以使用 net/http.putBufio{Read,Writer} 将缓存返回给 net/http 内部的 sync.Pool 。\n因此，我们还需要 24 GB 内存来支撑三百万的链接。\n终上所述，我们需要 72GB 内存来支撑一个什么都还没做的应用。\n优化 我们来回顾一下我们介绍部分中讨论的内容，并记住用户连接的行为方式。切换到 WebSocket 后，客户端发送包含相关事件的数据包，或者说订阅事件。然后（不考虑诸如技术消息 ping/pong），客户端可以在整个生命周期中不发送任何其他内容。连接寿命可能持续几秒到几天。\n因此对于大多数的时间来说，我们的 Channel.reader() 和 Channel.writer() 在等待数据的处理用于接受或发送。与他们一起等待的是每个 4KB 的 IO 缓存。\nNetpoller ","permalink":"https://pillumina.github.io/posts/programming/golang/websocket/","summary":"\u003cp\u003e\u003cem\u003e这篇文章是我研究高负载网络服务器架构看到的的一个有趣的story，添加了我自身学习websocket的感受和记录，希望我能在飞机落地前写完:-)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"preface\"\u003ePreface\u003c/h2\u003e\n\u003cp\u003e我们先描述一个问题作为讨论的中心：用户邮件的存储方法。\u003c/p\u003e\n\u003cp\u003e对于这种主题，有很多种方式在系统内对邮件状态进行持续的追踪，比如系统事件是一个方式，另一种方式可以通过定期的系统轮询有关状态变化。\u003c/p\u003e\n\u003cp\u003e这两种方式各有利弊，不过当我们讨论到邮件的时候，用户希望收到新邮件的速度越快越好。邮件轮询每秒约有50000个HTTP请求，其中60%返回304状态，也就是邮箱内没有任何修改。\u003c/p\u003e\n\u003cp\u003e因此，为了减少服务器的负载并加快向用户传递邮件的速度，我们决定通过编写publisher-subscriber服务器(即bus, message broker, event channel)来重新发明轮子。一方面接受有关状态变更的通知，另外一个方面接受此类通知的订阅。\u003c/p\u003e\n\u003cp\u003e改进前：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e+--------------+     (2)    +-------------+      (1)    +-----------+\n|              | \u0026lt;--------+ |             |  \u0026lt;--------+ |           |\n|    Storage   |            |     API     |     HTTP    |  Browser  |\n|              | +--------\u0026gt; |             |  +--------\u0026gt; |           |\n+--------------+     (3)    +-------------+      (4)    +-----------+\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e改进后:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e+--------------+            +-------------+   WebSocket  +-----------+\n|    Storage   |            |     API     | +----------\u0026gt; |  Browser  |\n+--------------+            +-------------+      (3)     +-----------+\n       +                           ^\n       | (1)                       | (2)\n       v                           +\n+-----------------------------------------+\n|                  Bus                    |\n+-----------------------------------------+\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e改进前的方案也就是browser定期去查询api并访问存储更改\u003c/p\u003e","title":"A Million WebSocket and Go"},{"content":"最近在看网络模型和go net的源码，以及各web框架例如fasthttp, weaver, gnet(更轻量)源码。fasthttp在github上已经写上了一个go开发的best practices examples,这里我也记录一些在源码中看到的一些技巧\n[]byte buffer的tricks 下面的一些tricks在fasthttp中被使用，自己的代码也可以用\n标准Go函数能够处理nil buffer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var ( // both buffers are uninitialized dst []byte src []byte ) dst = append(dst, src...) // is legal if dst is nil and/or src is nil copy(dst, src) // is legal if dst is nil and/or src is nil (string(src) == \u0026#34;\u0026#34;) // is true if src is nil (len(src) == 0) // is true if src is nil src = src[:0] // works like a charm with nil src // this for loop doesn\u0026#39;t panic if src is nil for i, ch := range src { doSomething(i, ch) } 所以可以去掉一些对[]bytebuffer的nil校验:\n1 2 3 4 srcLen := 0 if src != nil { srcLen = len(src) } 改成\n1 srcLen := len(src) 字符串能够直接append到[]byte上 1 dst = append(dst, \u0026#34;foobar\u0026#34;...) []bytebuffer能够扩展到它的cap 1 2 3 buf := make([]byte, 100) a := buf[:10] // len(a) == 10, cap(a) == 100. b := a[:100] // is valid, since cap(a) == 100. 所有fasthtto函数都接受nil的[]bytebuffer 1 2 statusCode, body, err := fasthttp.Get(nil, \u0026#34;http://google.com/\u0026#34;) uintBuf := fasthttp.AppendUint(nil, 1234) 减少[]byte的分配，尽量复用 有两种方式进行复用:\nsync.Pool slice = slice[:0] 所有的类型的Reset方法，都用了这个方式。比如类型URL，Args, ByteBuffer, Cookie, RequestHeader, ResponseHeader等 fasthttp里共有35个地方使用了sync.Pool。sync.Pool除了降低GC的压力，还能复用对象，减少内存分配，所以在自己写的goroutine pool中也对worker对象使用了sync.Pool。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 // 例如类型Server type Server struct { // ... ctxPool sync.Pool // 存RequestCtx对象 readerPool sync.Pool // 存bufio对象，用于读HTTP Request writerPool sync.Pool // 存bufio对象，用于写HTTP Request hijackConnPool sync.Pool bytePool sync.Pool } // 例如cookies var cookiePool = \u0026amp;sync.Pool{ New: func() interface{} { return \u0026amp;Cookie{} }, } func AcquireCookie() *Cookie { return cookiePool.Get().(*Cookie) } func ReleaseCookie(c *Cookie) { c.Reset() cookiePool.Put(c) } // 例如workPool. 每个请求以一个新的goroutine运行。就是workpool做的调度 type workerPool struct { // ... workerChanPool sync.Pool } func (wp *workerPool) getCh() *workerChan { var ch *workerChan // ... if ch == nil { if !createWorker { // 已经达到worker数量上限，不允许创建了 return nil } // 尝试复用旧worker vch := wp.workerChanPool.Get() if vch == nil { vch = \u0026amp;workerChan{ ch: make(chan net.Conn, workerChanCap), } } ch = vch.(*workerChan) // 创建新的goroutine处理请求 go func() { wp.workerFunc(ch) // 用完了返回去 wp.workerChanPool.Put(vch) }() } return ch } 复用已经分配的[]byte。\ns = s[:0]和s = append(s[:0], b…)这两种复用方式，总共出现了191次。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 清空 URI func (u *URI) Reset() { u.pathOriginal = u.pathOriginal[:0] u.scheme = u.scheme[:0] u.path = u.path[:0] // .... } // 清空 ResponseHeader func (h *ResponseHeader) resetSkipNormalize() { h.noHTTP11 = false h.connectionClose = false h.statusCode = 0 h.contentLength = 0 h.contentLengthBytes = h.contentLengthBytes[:0] h.contentType = h.contentType[:0] h.server = h.server[:0] h.h = h.h[:0] h.cookies = h.cookies[:0] } // 清空Cookies func (c *Cookie) Reset() { c.key = c.key[:0] c.value = c.value[:0] c.expire = zeroTime c.maxAge = 0 c.domain = c.domain[:0] c.path = c.path[:0] c.httpOnly = false c.secure = false c.sameSite = CookieSameSiteDisabled } func (c *Cookie) SetKey(key string) { c.key = append(c.key[:0], key...) } 方法参数尽量用[]byte, write only场景可以避免用bytes.Buffer 方法参数使用[]byte， 可以避免从[]byte到string转换时带来的内存分配和拷贝的开销。毕竟从net.Conn中读出来的数据也是[]byte类型。\n某些地方如果的确想穿string类型，fasthttp也提供XXXString()的方法。\nString方法用了a = append(a, string…)，这种写法不会造成string到[]byte的转换(汇编里没有用到runtime.stringtoslicebyte方法)\n1 2 3 4 5 // 例如写Response时，提供专门的String方法 func (resp *Response) SetBodyString(body string) { // ... bodyBuf.WriteString(body) } 上面的bodyBuf变量类型为ByteBuffer，来源于作者另外写的一个库，bytebufferpool。\n正如介绍一样，库的主要目标是反对多余的内存分配行为。与标准库的bytes.Buffer类型对比，性能高30%。\n但ByteBuffer只提供了write类操作。适合高频写场景。\n先看下标准库bytes.Buffer是如何增长底层slice的。重点是bytes.Buffer没有内存复用:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 增长slice时，都会调用grow方法 func (b *Buffer) grow(n int) int { // ... if m+n \u0026lt;= cap(b.buf)/2 { copy(b.buf[:], b.buf[b.off:]) } else { // 通过makeSlice获取新的slice buf := makeSlice(2*cap(b.buf) + n) // 而且还要拷贝 copy(buf, b.buf[b.off:]) b.buf = buf } // ... } func makeSlice(n int) []byte { // maekSlice 是直接分配出新的slice，没有复用的意思 return make([]byte, n) } 再看ByteBuffer的做法。重点是复用内存:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 通过复用减少内存分配，下次复用 func (b *ByteBuffer) Reset() { b.B = b.B[:0] } // 提供专门String方法，通过append避免string到[]byte转换带来的内存分配和拷贝 func (b *ByteBuffer) WriteString(s string) (int, error) { b.B = append(b.B, s...) return len(s), nil } // 如果写buffer的内容很大呢？增长的事情交给append // 但因为Reset()做了复用，所以cap足够情况下，append速度会很快 func (b *ByteBuffer) Write(p []byte) (int, error) { b.B = append(b.B, p...) return len(p), nil } Request和Response都是用ByteBuffer存body的。清空body是把ByteBuffer交还给pool，方便复用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var ( responseBodyPool bytebufferpool.Pool requestBodyPool bytebufferpool.Pool ) func (req *Request) ResetBody() { req.RemoveMultipartFormFiles() req.closeBodyStream() if req.body != nil { if req.keepBodyBuffer { req.body.Reset() } else { requestBodyPool.Put(req.body) req.body = nil } } } func (resp *Response) ResetBody() { resp.bodyRaw = nil resp.closeBodyStream() if resp.body != nil { if resp.keepBodyBuffer { resp.body.Reset() } else { responseBodyPool.Put(resp.body) resp.body = nil } } } 极限复用内存的地方 有些地方需要kv型数据，一般使用map[string]string。但map不利于复用。所以fasthttp使用slice来实现了map，这个优化其实挺极限的，而且查询复杂度会降到O(n)。所以这种优化适用于key数量不多，而且并发量大的场景，这样slice的方式就能很好得减少内存。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type argsKV struct { key []byte value []byte noValue bool } // 增加新的kv func appendArg(args []argsKV, key, value string, noValue bool) []argsKV { var kv *argsKV args, kv = allocArg(args) // 复用原来key的内存空间 kv.key = append(kv.key[:0], key...) if noValue { kv.value = kv.value[:0] } else { // 复用原来value的内存空间 kv.value = append(kv.value[:0], value...) } kv.noValue = noValue return args } func allocArg(h []argsKV) ([]argsKV, *argsKV) { n := len(h) if cap(h) \u0026gt; n { // 复用底层数组空间，不用分配 h = h[:n+1] } else { // 空间不足再分配 h = append(h, argsKV{}) } return h, \u0026amp;h[n] } 避免[]byte与string的转化开销 和上述提到的一样，这两种结构转化是带内存分配和拷贝开销的，这里fasthttp做了个trick避免开销。就是利用了string和slice在runtime里结构只差一个Cap字段实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } // []byte -\u0026gt; string func b2s(b []byte) string { return *(*string)(unsafe.Pointer(\u0026amp;b)) } // string -\u0026gt; []byte func s2b(s string) []byte { sh := (*reflect.StringHeader)(unsafe.Pointer(\u0026amp;s)) bh := reflect.SliceHeader{ Data: sh.Data, Len: sh.Len, Cap: sh.Len, } return *(*[]byte)(unsafe.Pointer(\u0026amp;bh)) } 不过这种trick的影响是：\n转换出来的[]byte不能有修改操作 依赖了XXHeader结构，runtime更改结构会受到影响 如果unsafe.Pointer作用被更改，也受到影响 总结 fasthttp github中提到的:\nDo not allocate objects and []byte buffers - just reuse them as much as possible. Fasthttp API design encourages this. sync.Pool is your best friend. Profile your program in production. go tool pprof --alloc_objects your-program mem.pprof usually gives better insights for optimization opportunities than go tool pprof your-program cpu.pprof. Write tests and benchmarks for hot paths. Avoid conversion between []byte and string, since this may result in memory allocation+copy. Fasthttp API provides functions for both []byte and string - use these functions instead of converting manually between []byte and string. There are some exceptions - see this wiki page for more details. Verify your tests and production code under race detector on a regular basis. Prefer quicktemplate instead of html/template in your webserver. 总结下来一些要点就是:\n","permalink":"https://pillumina.github.io/posts/programming/golang/fasthttp/","summary":"\u003cp\u003e\u003cem\u003e最近在看网络模型和go net的源码，以及各web框架例如fasthttp, weaver, gnet(更轻量)源码。fasthttp在github上已经写上了一个go开发的best practices \u003ca href=\"https://github.com/valyala/fasthttp#fasthttp-best-practices\"\u003eexamples\u003c/a\u003e,这里我也记录一些在源码中看到的一些技巧\u003c/em\u003e\u003c/p\u003e\n\u003ch3 id=\"byte-buffer的tricks\"\u003e\u003ccode\u003e[]byte\u003c/code\u003e buffer的tricks\u003c/h3\u003e\n\u003cp\u003e下面的一些tricks在fasthttp中被使用，自己的代码也可以用\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e标准Go函数能够处理nil buffer\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// both buffers are uninitialized\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003edst\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003esrc\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003edst\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eappend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edst\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esrc\u003c/span\u003e\u003cspan class=\"o\"\u003e...\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// is legal if dst is nil and/or src is nil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecopy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edst\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// is legal if dst is nil and/or src is nil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// is true if src is nil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// is true if src is nil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003esrc\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003esrc\u003c/span\u003e\u003cspan class=\"p\"\u003e[:\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// works like a charm with nil src\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// this for loop doesn\u0026#39;t panic if src is nil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003esrc\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003edoSomething\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e所以可以去掉一些对\u003ccode\u003e[]byte\u003c/code\u003ebuffer的nil校验:\u003c/p\u003e","title":"fasthttp对性能的优化压榨"},{"content":"- 当多个goroutine都需要创建同一个对象，如果gorountine数过多，导致对象的创建数目剧增，进而导致GC压力增大，形成“并发大-占用内存大-GC缓慢-并发处理能力弱-并发更大”这样的恶性循环 - 在这个时候，需要一个对象池，每个goroutine不再自己单独创建对象，而是从对象池中取出一个对象（如果池中已有） ","permalink":"https://pillumina.github.io/posts/programming/golang/sync-pool/","summary":"\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- 当多个goroutine都需要创建同一个对象，如果gorountine数过多，导致对象的创建数目剧增，进而导致GC压力增大，形成“并发大-占用内存大-GC缓慢-并发处理能力弱-并发更大”这样的恶性循环\n- 在这个时候，需要一个对象池，每个goroutine不再自己单独创建对象，而是从对象池中取出一个对象（如果池中已有）\n\u003c/code\u003e\u003c/pre\u003e","title":"[源码分析]sync pool"},{"content":"高性能Goroutine Pool go调度器没有限制对goroutine的数量，在goroutine瞬时大规模爆发的场景下来不及复用goroutine从而导致大量goroutine被创建，会导致大量的系统资源占用，尝试池化。\ngo调度器本身不应该对goroutine数量有限制，因为语言层面无法界定需要限制多少，毕竟程序跑在不同性能的环境，在并发规模不太大的场景做限制甚至会降低性能，原生支持限制goroutine数量无疑是得不偿失的。如果只是中等规模和比较小规模的并发场景其实pool的性能并没有优势\n目前设计上还需要加上周期性对空闲队列的prune，等写完再加看看benchmark会提升多少。目前来说对大规模goroutine异步并发的场景(1M, 10M)内存优化(10倍往上)和吞吐量优化效果(2-6倍)非常好。\n需求场景与目标 限制并发goroutine的数量 复用goroutine，减轻runtime调度压力，提升程序性能 规避过多的goroutine创建侵占系统资源，cpu\u0026amp;内存 关键技术 锁同步: golang有CAS机制，用spin-lock替代mutex 原理， 讨论 LIFO/FIFO队列: LIFO队列能直接有时间排序功能，方便对需要关联入队时间的操作进行处理 Pool容量限制和弹性伸缩 代码实现 pool.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 package go_pool import ( \u0026#34;errors\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;sync/atomic\u0026#34; \u0026#34;time\u0026#34; ) const( OPEN = iota CLOSED ) var ( ErrPoolClosed = errors.New(\u0026#34;this pool has been closed\u0026#34;) ErrPoolOverload = errors.New(\u0026#34;too many goroutines blocked on submit or Nonblocking is set\u0026#34;) ErrInvalidExpiryTime = errors.New(\u0026#34;invalid expiration time\u0026#34;) ErrInvalidPoolCapacity = errors.New(\u0026#34;invalid pool capacity\u0026#34;) DefaultScanInterval = time.Second ) type Pool struct { capacity int32 running int32 lock sync.Locker scanDuration time.Duration blockingTasksNum int maxBlockingTasks int state int32 cond *sync.Cond workers WorkerQueue // LIFO queue workerCache sync.Pool } func (p *Pool) Submit(task func()) error{ if atomic.LoadInt32(\u0026amp;p.state) == CLOSED{ return ErrPoolClosed } // retrieve worker to do the task // return error if no workers available var w *Worker if w = p.retrieveWorker(); w == nil{ return ErrPoolOverload } w.task \u0026lt;- task return nil } func (p *Pool) Shutdown() { atomic.StoreInt32(\u0026amp;p.state, CLOSED) p.lock.Lock() // reset worker queue p.workers.reset() p.lock.Unlock() } func (p *Pool) isClosed() bool{ return atomic.LoadInt32(\u0026amp;p.state) == CLOSED } // change the capacity of the pool func (p *Pool) Resize(size int){ if p.Cap() == size{ return } atomic.StoreInt32(\u0026amp;p.capacity, int32(size)) // need to stop certain workers if #running_workers \u0026gt; #new_capacity diff := p.Running() - size if diff \u0026gt; 0{ for i := 0; i\u0026lt; diff; i++{ p.retrieveWorker().task \u0026lt;- nil } } } func (p *Pool) Reboot() { if atomic.CompareAndSwapInt32(\u0026amp;p.state, CLOSED, OPEN){ // initialize the purging go routine go p.scavengerRoutine() } } func (p *Pool) Running() int{ return int(atomic.LoadInt32(\u0026amp;p.running)) } func (p *Pool) Cap() int{ return int(atomic.LoadInt32(\u0026amp;p.capacity)) } func (p *Pool) Free() int{ return p.Cap() - p.Running() } func (p *Pool) incRunning(){ atomic.AddInt32(\u0026amp;p.running, 1) } func (p *Pool) decRunning(){ atomic.AddInt32(\u0026amp;p.running, -1) } // put the worker back into the pool for recycling func (p *Pool) recycleWorker(worker *Worker) bool{ capacity := p.Cap() if p.isClosed() || (capacity \u0026gt;= 0 \u0026amp;\u0026amp; p.Running() \u0026gt; capacity){ return false } worker.recycleTime = time.Now() p.lock.Lock() // need to double check if state is CLOSED if p.isClosed(){ p.lock.Unlock() return false } err := p.workers.add(worker) if err != nil{ p.lock.Unlock() return false } // notify any request stuck in retrieveWorker that there is an available worker in pool p.cond.Signal() p.lock.Unlock() return true } func (p *Pool) spawnWorker() *Worker{ worker := p.workerCache.Get().(*Worker) worker.Run() return worker } func (p *Pool) retrieveWorker() (worker *Worker){ p.lock.Lock() worker = p.workers.detach() // get worker from queue successfully if worker != nil{ p.lock.Unlock() }else if capacity := p.Cap();capacity == -1{ p.lock.Unlock() // spawn worker return p.spawnWorker() }else if p.Running() \u0026lt; capacity{ // infinite pool p.lock.Unlock() // spawn worker return p.spawnWorker() }else{ // if the number of blocking tasks reaches the maximum blocking tasks threshold then returns nil // and throw the ErrPoolOverload error in Submit method if p.maxBlockingTasks != 0 \u0026amp;\u0026amp; p.maxBlockingTasks \u0026lt;= p.blockingTasksNum{ p.lock.Unlock() return } // the pool is full need to wait until worker is available for task handling Retry: // handle the number of blocking task handling requests // wait until condition being notified p.blockingTasksNum++ p.cond.Wait() p.blockingTasksNum-- // ensure there is a worker available because you don\u0026#39;t know if the recycled worker being closed then if p.Running() == 0{ p.lock.Unlock() // spawn worker return p.spawnWorker() } worker = p.workers.detach() if worker == nil{ goto Retry } p.lock.Unlock() } return } func (p *Pool) scavengerRoutine(){ heartbeat := time.NewTicker(p.scanDuration) defer heartbeat.Stop() for range heartbeat.C{ if p.isClosed(){ break } // all workers get cleaned up and some invokers still get stuck on cond.Wait() // we need to wake up all invokers in that situation. if p.Running() == 0{ p.cond.Broadcast() } } } func NewPool(capacity int)(*Pool, error){ if capacity \u0026lt;= 0{ capacity = -1 } pool := \u0026amp;Pool{ capacity: int32(capacity), lock: NewSpinLock(), } pool.workerCache.New = func() interface{}{ return \u0026amp;Worker{ pool: pool, task: make(chan func(), 1), } } pool.scanDuration = DefaultScanInterval // initialize the worker queue if capacity == -1{ return nil, ErrInvalidPoolCapacity } pool.workers = NewWorkerQueue(0) pool.cond = sync.NewCond(pool.lock) // initialize the purging goroutine go pool.scavengerRoutine() return pool, nil } worker.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package go_pool import ( \u0026#34;time\u0026#34; ) type Worker struct{ pool *Pool task chan func() recycleTime time.Time } func (w *Worker) Run(){ w.pool.incRunning() go func(){ defer func(){ w.pool.decRunning() w.pool.workerCache.Put(w) // todo: panic recovery strategy }() for f := range w.task{ // receiving nil indicates that the worker should stop and quit go routine if f == nil{ return } f() // recycle worker back into the pool, if not success quit go routine if success := w.pool.recycleWorker(w); !success{ return } } }() } worker_queue.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package go_pool type WorkerQueue interface { len() int isEmpty() bool add(worker *Worker) error detach() *Worker reset() } func NewWorkerQueue(size int) WorkerQueue{ return NewSimpleWorkerQueue(size) } func NewSimpleWorkerQueue(size int) *simpleWorkerQueue{ return \u0026amp;simpleWorkerQueue{ size: size, workers: make([]*Worker, 0, size), } } type simpleWorkerQueue struct{ workers []*Worker size int } func(sq *simpleWorkerQueue) len() int{ return len(sq.workers) } func(sq *simpleWorkerQueue) isEmpty() bool{ return sq.len() == 0 } func (sq *simpleWorkerQueue) add(worker *Worker) error{ sq.workers = append(sq.workers, worker) return nil } func (sq *simpleWorkerQueue) detach() *Worker{ length := sq.len() if length == 0{ return nil } worker := sq.workers[length - 1] sq.workers[length - 1] = nil // slice operation should avoid memory leak sq.workers = sq.workers[:length-1] return worker } func (sq *simpleWorkerQueue) reset(){ for i := 0;i \u0026lt; sq.len(); i++{ sq.workers[i].task \u0026lt;- nil sq.workers[i] = nil } sq.workers = sq.workers[:0] } lock.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package go_pool import ( \u0026#34;runtime\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;sync/atomic\u0026#34; ) type spinLock uint32 func (sl *spinLock) Lock() { for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) { runtime.Gosched() } } func (sl *spinLock) Unlock() { atomic.StoreUint32((*uint32)(sl), 0) } // NewSpinLock instantiates a spin-lock. func NewSpinLock() sync.Locker { return new(spinLock) } pool_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 package go_pool import ( \u0026#34;math\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) const( _ = 1 \u0026lt;\u0026lt; (10 * iota) KiB //1024 MiB // 1048578 ) const ( InfinitePoolSize = math.MaxInt32 PoolSize = 10000 SleepTime = 100 OverSizeTaskNum = 10 * PoolSize UnderSizeTaskNum = 0.2 * PoolSize ) var currentMem uint64 func demoTaskFunc(args interface{}){ n := args.(int) time.Sleep(time.Duration(n) * time.Millisecond) } func TestPoolWaitToGetWorker(t *testing.T){ var wg sync.WaitGroup p, err := NewPool(PoolSize) defer p.Shutdown() if err != nil { t.Errorf(\u0026#34;err: %s\u0026#34;, err.Error()) } for i:=0; i\u0026lt; OverSizeTaskNum; i++{ wg.Add(1) _ = p.Submit(func(){ demoTaskFunc(SleepTime) wg.Done() }) } wg.Wait() mem := runtime.MemStats{} runtime.ReadMemStats(\u0026amp;mem) currentMem = mem.TotalAlloc/KiB - currentMem t.Logf(\u0026#34;memory usage: %d KB\u0026#34;, currentMem) } func TestPoolGetWorkerFromCache(t *testing.T){ var currentMem uint64 var wg sync.WaitGroup p, err := NewPool(PoolSize) defer p.Shutdown() if err != nil { t.Errorf(\u0026#34;err: %s\u0026#34;, err.Error()) } for i:=0; i\u0026lt; UnderSizeTaskNum; i++{ wg.Add(1) _ = p.Submit(func(){ demoTaskFunc(SleepTime) wg.Done() }) } wg.Wait() mem := runtime.MemStats{} runtime.ReadMemStats(\u0026amp;mem) currentMem = mem.TotalAlloc/KiB - currentMem t.Logf(\u0026#34;memory usage: %d KB\u0026#34;, currentMem) } func TestNoPool(t *testing.T){ var wg sync.WaitGroup for i:=0; i\u0026lt;UnderSizeTaskNum; i++{ wg.Add(1) go func(){ defer wg.Done() demoTaskFunc(SleepTime) }() } wg.Wait() mem := runtime.MemStats{} runtime.ReadMemStats(\u0026amp;mem) currentMem = mem.TotalAlloc/KiB - currentMem t.Logf(\u0026#34;memory usage: %d KB\u0026#34;, currentMem) } func TestWithInfinitePool(t *testing.T){ var wg sync.WaitGroup p, err := NewPool(InfinitePoolSize) defer p.Shutdown() if err != nil { t.Errorf(\u0026#34;err: %s\u0026#34;, err.Error()) } for i:=0; i\u0026lt; UnderSizeTaskNum; i++{ wg.Add(1) _ = p.Submit(func(){ demoTaskFunc(SleepTime) wg.Done() }) } wg.Wait() mem := runtime.MemStats{} runtime.ReadMemStats(\u0026amp;mem) currentMem = mem.TotalAlloc/KiB - currentMem t.Logf(\u0026#34;memory usage: %d KB\u0026#34;, currentMem) } pool_benchmark_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package go_pool import ( \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) const ( RunTimes = 5000000 BenchParam = 10 BenchPoolSize = 200000 ) func demoFunc() { time.Sleep(time.Duration(BenchParam) * time.Millisecond) } func BenchmarkPoolThroughput(b *testing.B) { p, _ := NewPool(BenchPoolSize) defer p.Shutdown() b.StartTimer() for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; RunTimes; j++ { _ = p.Submit(demoFunc) } } b.StopTimer() } func BenchmarkGoroutinesThroughput(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; RunTimes; j++ { go demoFunc() } } } ","permalink":"https://pillumina.github.io/posts/programming/golang/goroutine-pool/","summary":"\u003ch1 id=\"高性能goroutine-pool\"\u003e高性能Goroutine Pool\u003c/h1\u003e\n\u003cp\u003ego调度器没有限制对goroutine的数量，在goroutine瞬时大规模爆发的场景下来不及复用goroutine从而导致大量goroutine被创建，会导致大量的系统资源占用，尝试池化。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ego调度器本身不应该对goroutine数量有限制，因为语言层面无法界定需要限制多少，毕竟程序跑在不同性能的环境，在并发规模不太大的场景做限制甚至会降低性能，原生支持限制goroutine数量无疑是得不偿失的。如果只是中等规模和比较小规模的并发场景其实pool的性能并没有优势\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e目前设计上还需要加上周期性对空闲队列的prune，等写完再加看看benchmark会提升多少。目前来说对大规模goroutine异步并发的场景(1M, 10M)内存优化(10倍往上)和吞吐量优化效果(2-6倍)非常好。\u003c/p\u003e\n\u003ch2 id=\"需求场景与目标\"\u003e需求场景与目标\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e限制并发goroutine的数量\u003c/li\u003e\n\u003cli\u003e复用goroutine，减轻runtime调度压力，提升程序性能\u003c/li\u003e\n\u003cli\u003e规避过多的goroutine创建侵占系统资源，cpu\u0026amp;内存\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"关键技术\"\u003e关键技术\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e锁同步: golang有CAS机制，用spin-lock替代mutex \u003ca href=\"https://ofstack.com/Golang/27085/implementation-of-golang-spin-lock.html\"\u003e原理\u003c/a\u003e， \u003ca href=\"https://stackoverflow.com/questions/5869825/when-should-one-use-a-spinlock-instead-of-mutex\"\u003e讨论\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eLIFO/FIFO队列: LIFO队列能直接有时间排序功能，方便对需要关联入队时间的操作进行处理\u003c/li\u003e\n\u003cli\u003ePool容量限制和弹性伸缩\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"代码实现\"\u003e代码实现\u003c/h2\u003e\n\u003ch3 id=\"poolgo\"\u003epool.go\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e  1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 54\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 55\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 56\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 57\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 58\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 59\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 60\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 61\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 62\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 63\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 64\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 65\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 66\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 67\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 68\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 69\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 70\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 71\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 72\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 73\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 74\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 75\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 76\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 77\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 78\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 79\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 80\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 81\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 82\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 83\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 84\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 85\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 86\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 87\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 88\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 89\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 90\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 91\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 92\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 93\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 94\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 95\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 96\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 97\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 98\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 99\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e100\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e101\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e102\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e103\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e104\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e105\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e106\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e107\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e108\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e109\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e110\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e111\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e112\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e113\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e114\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e115\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e116\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e117\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e118\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e119\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e120\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e121\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e122\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e123\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e124\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e125\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e126\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e127\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e128\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e129\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e130\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e131\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e132\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e133\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e134\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e135\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e136\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e137\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e138\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e139\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e140\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e141\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e142\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e143\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e144\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e145\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e146\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e147\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e148\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e149\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e150\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e151\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e152\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e153\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e154\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e155\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e156\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e157\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e158\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e159\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e160\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e161\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e162\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e163\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e164\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e165\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e166\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e167\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e168\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e169\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e170\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e171\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e172\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e173\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e174\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e175\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e176\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e177\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e178\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e179\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e180\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e181\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e182\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e183\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e184\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e185\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e186\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e187\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e188\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e189\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e190\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e191\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e192\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e193\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e194\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e195\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e196\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e197\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e198\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e199\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e200\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e201\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e202\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e203\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e204\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e205\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e206\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e207\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e208\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e209\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e210\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e211\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e212\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e213\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e214\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e215\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e216\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e217\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e218\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e219\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e220\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e221\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e222\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e223\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e224\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e225\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e226\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e227\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e228\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e229\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e230\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e231\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e232\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e233\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e234\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e235\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003ego_pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;errors\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;sync\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;sync/atomic\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eOPEN\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003eiota\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eCLOSED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eErrPoolClosed\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;this pool has been closed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eErrPoolOverload\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;too many goroutines blocked on submit or Nonblocking is set\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eErrInvalidExpiryTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;invalid expiration time\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eErrInvalidPoolCapacity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eerrors\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;invalid pool capacity\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eDefaultScanInterval\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eSecond\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003ePool\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003erunning\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003elock\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eLocker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003escanDuration\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eDuration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eblockingTasksNum\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003emaxBlockingTasks\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003estate\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003econd\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eCond\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e \u003cspan class=\"nx\"\u003eWorkerQueue\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// LIFO queue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworkerCache\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eSubmit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etask\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"kt\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLoadInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003eCLOSED\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eErrPoolClosed\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// retrieve worker to do the task\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// return error if no workers available\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ew\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ew\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eretrieveWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"nx\"\u003ew\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eErrPoolOverload\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etask\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e \u003cspan class=\"nx\"\u003etask\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eShutdown\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eStoreInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eCLOSED\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// reset worker queue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ereset\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eisClosed\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLoadInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003eCLOSED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// change the capacity of the pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eResize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esize\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eStoreInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003eint32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// need to stop certain workers if #running_workers \u0026gt; #new_capacity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ediff\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003esize\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ediff\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ediff\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eretrieveWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nx\"\u003etask\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eReboot\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCompareAndSwapInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eCLOSED\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eOPEN\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// initialize the purging go routine\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003escavengerRoutine\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLoadInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003erunning\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLoadInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eFree\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eincRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAddInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003erunning\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003edecRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAddInt32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003erunning\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// put the worker back into the pool for recycling\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003erecycleWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eisClosed\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003erecycleTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNow\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// need to double check if state is CLOSED\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eisClosed\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// notify any request stuck in retrieveWorker that there is an available worker in pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003econd\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSignal\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003espawnWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkerCache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eGet\u003c/span\u003e\u003cspan class=\"p\"\u003e().(\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eworker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eretrieveWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edetach\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// get worker from queue successfully\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// spawn worker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003espawnWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// infinite pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// spawn worker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003espawnWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// if the number of blocking tasks reaches the maximum blocking tasks threshold then returns nil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// and throw the ErrPoolOverload error in Submit method\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emaxBlockingTasks\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emaxBlockingTasks\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eblockingTasksNum\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// the pool is full need to wait until worker is available for task handling\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003eRetry\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// handle the number of blocking task handling requests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// wait until condition being notified\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eblockingTasksNum\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003econd\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eWait\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eblockingTasksNum\u003c/span\u003e\u003cspan class=\"o\"\u003e--\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// ensure there is a worker available because you don\u0026#39;t know if the recycled worker being closed then\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"c1\"\u003e// spawn worker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003espawnWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edetach\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003egoto\u003c/span\u003e \u003cspan class=\"nx\"\u003eRetry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003escavengerRoutine\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eheartbeat\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNewTicker\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003escanDuration\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003eheartbeat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eStop\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003eheartbeat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eisClosed\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003ebreak\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// all workers get cleaned up and some invokers still get stuck on cond.Wait()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"c1\"\u003e// we need to wake up all invokers in that situation.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003econd\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eBroadcast\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewPool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e  \u003cspan class=\"nb\"\u003eint32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewSpinLock\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkerCache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eNew\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kd\"\u003einterface\u003c/span\u003e\u003cspan class=\"p\"\u003e{}{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003etask\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003escanDuration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eDefaultScanInterval\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// initialize the worker queue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ecapacity\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eErrInvalidPoolCapacity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003econd\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNewCond\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elock\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// initialize the purging goroutine\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003escavengerRoutine\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"workergo\"\u003eworker.go\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003ego_pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003epool\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ePool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003etask\u003c/span\u003e \u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003erecycleTime\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eTime\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eincRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edecRunning\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkerCache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ePut\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// todo: panic recovery strategy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ef\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etask\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// receiving nil indicates that the worker should stop and quit go routine\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003ef\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nf\"\u003ef\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// recycle worker back into the pool, if not success quit go routine\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003esuccess\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epool\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003erecycleWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ew\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003esuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"worker_queuego\"\u003eworker_queue.go\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e54\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e55\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e56\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e57\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e58\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003ego_pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eWorkerQueue\u003c/span\u003e \u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003eisEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"kt\"\u003eerror\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003edetach\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003ereset\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esize\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nx\"\u003eWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewSimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewSimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esize\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e([]\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003esize\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eisEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"kt\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eappend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eworker\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003edetach\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003elength\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003elength\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eworker\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003elength\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003elength\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e \u003cspan class=\"c1\"\u003e// slice operation should avoid memory leak\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e[:\u003c/span\u003e\u003cspan class=\"nx\"\u003elength\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eworker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esimpleWorkerQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003ereset\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e].\u003c/span\u003e\u003cspan class=\"nx\"\u003etask\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003esq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eworkers\u003c/span\u003e\u003cspan class=\"p\"\u003e[:\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"lockgo\"\u003elock.go\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003ego_pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;runtime\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;sync\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;sync/atomic\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003espinLock\u003c/span\u003e \u003cspan class=\"kt\"\u003euint32\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esl\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003espinLock\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eLock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCompareAndSwapUint32\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"kt\"\u003euint32\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"nx\"\u003esl\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eGosched\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esl\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003espinLock\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eUnlock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eatomic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eStoreUint32\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"kt\"\u003euint32\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"nx\"\u003esl\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// NewSpinLock instantiates a spin-lock.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewSpinLock\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eLocker\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003espinLock\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"pool_testgo\"\u003epool_test.go\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e  1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e  9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 54\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 55\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 56\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 57\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 58\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 59\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 60\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 61\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 62\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 63\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 64\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 65\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 66\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 67\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 68\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 69\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 70\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 71\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 72\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 73\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 74\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 75\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 76\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 77\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 78\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 79\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 80\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 81\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 82\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 83\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 84\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 85\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 86\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 87\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 88\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 89\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 90\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 91\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 92\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 93\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 94\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 95\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 96\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 97\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 98\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 99\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e100\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e101\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e102\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e103\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e104\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e105\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e106\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e107\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e108\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e109\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e110\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003ego_pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;math\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;runtime\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;sync\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;testing\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"kc\"\u003eiota\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eKiB\u003c/span\u003e \u003cspan class=\"c1\"\u003e//1024\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eMiB\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 1048578\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eInfinitePoolSize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003emath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMaxInt32\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ePoolSize\u003c/span\u003e        \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e10000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eSleepTime\u003c/span\u003e       \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e100\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eOverSizeTaskNum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003ePoolSize\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eUnderSizeTaskNum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.2\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003ePoolSize\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e \u003cspan class=\"kt\"\u003euint64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003edemoTaskFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eargs\u003c/span\u003e \u003cspan class=\"kd\"\u003einterface\u003c/span\u003e\u003cspan class=\"p\"\u003e{}){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e.(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDuration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMillisecond\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eTestPoolWaitToGetWorker\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ewg\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eWaitGroup\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewPool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ePoolSize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eShutdown\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eErrorf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;err: %s\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e:=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eOverSizeTaskNum\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSubmit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nf\"\u003edemoTaskFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSleepTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDone\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eWait\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003emem\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eReadMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eTotalAlloc\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003eKiB\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLogf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;memory usage: %d KB\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eTestPoolGetWorkerFromCache\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e \u003cspan class=\"kt\"\u003euint64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ewg\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eWaitGroup\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewPool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ePoolSize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eShutdown\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eErrorf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;err: %s\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e:=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eUnderSizeTaskNum\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSubmit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nf\"\u003edemoTaskFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSleepTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDone\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eWait\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003emem\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eReadMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eTotalAlloc\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003eKiB\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLogf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;memory usage: %d KB\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eTestNoPool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ewg\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eWaitGroup\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e:=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eUnderSizeTaskNum\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDone\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nf\"\u003edemoTaskFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSleepTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eWait\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003emem\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eReadMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eTotalAlloc\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003eKiB\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLogf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;memory usage: %d KB\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eTestWithInfinitePool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003ewg\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eWaitGroup\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewPool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eInfinitePoolSize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eShutdown\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enil\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eErrorf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;err: %s\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerr\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e:=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eUnderSizeTaskNum\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSubmit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nf\"\u003edemoTaskFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSleepTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDone\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eWait\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003emem\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eruntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eReadMemStats\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003emem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eTotalAlloc\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003eKiB\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eLogf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;memory usage: %d KB\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentMem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"pool_benchmark_testgo\"\u003epool_benchmark_test.go\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003ego_pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;testing\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eRunTimes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e5000000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eBenchParam\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eBenchPoolSize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e200000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003edemoFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDuration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eBenchParam\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMillisecond\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eBenchmarkPoolThroughput\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003e_\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nf\"\u003eNewPool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eBenchPoolSize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eShutdown\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eStartTimer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eN\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eRunTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ej\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"nx\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSubmit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edemoFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eStopTimer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eBenchmarkGoroutinesThroughput\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eN\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eRunTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ej\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"nf\"\u003edemoFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"[自建轮]高性能Goroutine Pool"},{"content":"实际上对于一个有GC的语言，我们不必太多关心内存泄漏的问题，因为程序的runtime帮我们很好地额回收不再使用的内存。但是，我们还是得了解一些特殊的场景，这些场景会产生暂时性或者永久性的内存泄漏。\n待开坑...\n","permalink":"https://pillumina.github.io/posts/programming/golang/memory-leak/","summary":"\u003cp\u003e实际上对于一个有GC的语言，我们不必太多关心内存泄漏的问题，因为程序的runtime帮我们很好地额回收不再使用的内存。但是，我们还是得了解一些特殊的场景，这些场景会产生暂时性或者永久性的内存泄漏。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e待开坑...\u003c/code\u003e\u003c/p\u003e","title":"Possible Memory Leak"},{"content":"优雅地关闭通道 场景一：M个接收者和一个发送者。发送者通过关闭用来传输数据的通道来传递发送结束信号 这是最简单的一种情形。当发送者欲结束发送，让它关闭用来传输数据的通道即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;log\u0026#34; ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumReceivers = 100 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) // 发送者 go func() { for { if value := rand.Intn(Max); value == 0 { // 此唯一的发送者可以安全地关闭此数据通道。 close(dataCh) return } else { dataCh \u0026lt;- value } } }() // 接收者 for i := 0; i \u0026lt; NumReceivers; i++ { go func() { defer wgReceivers.Done() // 接收数据直到通道dataCh已关闭 // 并且dataCh的缓冲队列已空。 for value := range dataCh { log.Println(value) } }() } wgReceivers.Wait() } 场景二： 一个接收者和N个发送者，此唯一接收者通过关闭一个额外的信号通道来通知发送者不要在发送数据了 此情形比上一种情形复杂一些。我们不能让接收者关闭用来传输数据的通道来停止数据传输，因为这样做违反了通道关闭原则。 但是我们可以让接收者关闭一个额外的信号通道来通知发送者不要在发送数据了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;log\u0026#34; ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumSenders = 1000 wgReceivers := sync.WaitGroup{} wgReceivers.Add(1) // ... dataCh := make(chan int) stopCh := make(chan struct{}) // stopCh是一个额外的信号通道。它的 // 发送者为dataCh数据通道的接收者。 // 它的接收者为dataCh数据通道的发送者。 // 发送者 for i := 0; i \u0026lt; NumSenders; i++ { go func() { for { // 这里的第一个尝试接收用来让此发送者 // 协程尽早地退出。对于这个特定的例子， // 此select代码块并非必需。 select { case \u0026lt;- stopCh: return default: } // 即使stopCh已经关闭，此第二个select // 代码块中的第一个分支仍很有可能在若干个 // 循环步内依然不会被选中。如果这是不可接受 // 的，则上面的第一个select代码块是必需的。 select { case \u0026lt;- stopCh: return case dataCh \u0026lt;- rand.Intn(Max): } } }() } // 接收者 go func() { defer wgReceivers.Done() for value := range dataCh { if value == Max-1 { // 此唯一的接收者同时也是stopCh通道的 // 唯一发送者。尽管它不能安全地关闭dataCh数 // 据通道，但它可以安全地关闭stopCh通道。 close(stopCh) return } log.Println(value) } }() // ... wgReceivers.Wait() } 如此例中的注释所述，对于此额外的信号通道stopCh，它只有一个发送者，即dataCh数据通道的唯一接收者。 dataCh数据通道的接收者关闭了信号通道stopCh，这是不违反通道关闭原则的。\n在此例中，数据通道dataCh并没有被关闭。是的，我们不必关闭它。 当一个通道不再被任何协程所使用后，它将逐渐被垃圾回收掉，无论它是否已经被关闭。 所以这里的优雅性体现在通过不关闭一个通道来停止使用此通道。\n场景三：M个接收者和N个发送者。它们中的任何协程都可以让一个中间调解协程帮忙发出停止数据传送的信号 这是最复杂的一种情形。我们不能让接收者和发送者中的任何一个关闭用来传输数据的通道，我们也不能让多个接收者之一关闭一个额外的信号通道。 这两种做法都违反了通道关闭原则。 然而，我们可以引入一个中间调解者角色并让其关闭额外的信号通道来通知所有的接收者和发送者结束工作。 具体实现见下例。注意其中使用了一个尝试发送操作来向中间调解者发送信号。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;log\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumReceivers = 10 const NumSenders = 1000 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) stopCh := make(chan struct{}) // stopCh是一个额外的信号通道。它的发送 // 者为中间调解者。它的接收者为dataCh // 数据通道的所有的发送者和接收者。 toStop := make(chan string, 1) // toStop是一个用来通知中间调解者让其 // 关闭信号通道stopCh的第二个信号通道。 // 此第二个信号通道的发送者为dataCh数据 // 通道的所有的发送者和接收者，它的接收者 // 为中间调解者。它必须为一个缓冲通道。 var stoppedBy string // 中间调解者 go func() { stoppedBy = \u0026lt;-toStop close(stopCh) }() // 发送者 for i := 0; i \u0026lt; NumSenders; i++ { go func(id string) { for { value := rand.Intn(Max) if value == 0 { // 为了防止阻塞，这里使用了一个尝试 // 发送操作来向中间调解者发送信号。 select { case toStop \u0026lt;- \u0026#34;发送者#\u0026#34; + id: default: } return } // 此处的尝试接收操作是为了让此发送协程尽早 // 退出。标准编译器对尝试接收和尝试发送做了 // 特殊的优化，因而它们的速度很快。 select { case \u0026lt;- stopCh: return default: } // 即使stopCh已关闭，如果这个select代码块 // 中第二个分支的发送操作是非阻塞的，则第一个 // 分支仍很有可能在若干个循环步内依然不会被选 // 中。如果这是不可接受的，则上面的第一个尝试 // 接收操作代码块是必需的。 select { case \u0026lt;- stopCh: return case dataCh \u0026lt;- value: } } }(strconv.Itoa(i)) } // 接收者 for i := 0; i \u0026lt; NumReceivers; i++ { go func(id string) { defer wgReceivers.Done() for { // 和发送者协程一样，此处的尝试接收操作是为了 // 让此接收协程尽早退出。 select { case \u0026lt;- stopCh: return default: } // 即使stopCh已关闭，如果这个select代码块 // 中第二个分支的接收操作是非阻塞的，则第一个 // 分支仍很有可能在若干个循环步内依然不会被选 // 中。如果这是不可接受的，则上面尝试接收操作 // 代码块是必需的。 select { case \u0026lt;- stopCh: return case value := \u0026lt;-dataCh: if value == Max-1 { // 为了防止阻塞，这里使用了一个尝试 // 发送操作来向中间调解者发送信号。 select { case toStop \u0026lt;- \u0026#34;接收者#\u0026#34; + id: default: } return } log.Println(value) } } }(strconv.Itoa(i)) } // ... wgReceivers.Wait() log.Println(\u0026#34;被\u0026#34; + stoppedBy + \u0026#34;终止了\u0026#34;) } 在此例中，通道关闭原则依旧得到了遵守。\n请注意，信号通道toStop的容量必须至少为1。 如果它的容量为0，则在中间调解者还未准备好的情况下就已经有某个协程向toStop发送信号时，此信号将被抛弃。因为停止信号是通过非阻塞的尝试发送传递的。\n我们也可以不使用尝试发送操作向中间调解者发送信号，但信号通道toStop的容量必须至少为数据发送者和数据接收者的数量之和，以防止向其发送数据时（有一个极其微小的可能）导致某些发送者和接收者协程永久阻塞。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 ... toStop := make(chan string, NumReceivers + NumSenders) ... value := rand.Intn(Max) if value == 0 { toStop \u0026lt;- \u0026#34;sender#\u0026#34; + id return } ... if value == Max-1 { toStop \u0026lt;- \u0026#34;receiver#\u0026#34; + id return } ... 场景四： M个接收者和一个发送者”情形的一个变种：用来传输数据的通道的关闭请求由第三方发出 有时，数据通道（dataCh）的关闭请求需要由某个第三方协程发出。对于这种情形，我们可以使用一个额外的信号通道来通知唯一的发送者关闭数据通道（dataCh）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;log\u0026#34; ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumReceivers = 100 const NumThirdParties = 15 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) closing := make(chan struct{}) // 信号通道 closed := make(chan struct{}) // 此stop函数可以被安全地多次调用。 stop := func() { select { case closing\u0026lt;-struct{}{}: \u0026lt;-closed case \u0026lt;-closed: } } // 一些第三方协程 for i := 0; i \u0026lt; NumThirdParties; i++ { go func() { r := 1 + rand.Intn(3) time.Sleep(time.Duration(r) * time.Second) stop() }() } // 发送者 go func() { defer func() { close(closed) close(dataCh) }() for { select{ case \u0026lt;-closing: return default: } select{ case \u0026lt;-closing: return case dataCh \u0026lt;- rand.Intn(Max): } } }() // 接收者 for i := 0; i \u0026lt; NumReceivers; i++ { go func() { defer wgReceivers.Done() for value := range dataCh { log.Println(value) } }() } wgReceivers.Wait() } 上述代码中的stop函数中使用的技巧偷自Roger Peppe在此贴中的一个留言。\n场景五：“N个发送者”的一个变种：用来传输数据的通道必须被关闭以通知各个接收者数据发送已经结束了 在上面的提到的“N个发送者”情形中，为了遵守通道关闭原则，我们避免了关闭数据通道（dataCh）。 但是有时候，数据通道（dataCh）必须被关闭以通知各个接收者数据发送已经结束。 对于这种“N个发送者”情形，我们可以使用一个中间通道将它们转化为“一个发送者”情形，然后继续使用上一节介绍的技巧来关闭此中间通道，从而避免了关闭原始的dataCh数据通道。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;log\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 1000000 const NumReceivers = 10 const NumSenders = 1000 const NumThirdParties = 15 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) // 将被关闭 middleCh := make(chan int) // 不会被关闭 closing := make(chan string) closed := make(chan struct{}) var stoppedBy string stop := func(by string) { select { case closing \u0026lt;- by: \u0026lt;-closed case \u0026lt;-closed: } } // 中间层 go func() { exit := func(v int, needSend bool) { close(closed) if needSend { dataCh \u0026lt;- v } close(dataCh) } for { select { case stoppedBy = \u0026lt;-closing: exit(0, false) return case v := \u0026lt;- middleCh: select { case stoppedBy = \u0026lt;-closing: exit(v, true) return case dataCh \u0026lt;- v: } } } }() // 一些第三方协程 for i := 0; i \u0026lt; NumThirdParties; i++ { go func(id string) { r := 1 + rand.Intn(3) time.Sleep(time.Duration(r) * time.Second) stop(\u0026#34;3rd-party#\u0026#34; + id) }(strconv.Itoa(i)) } // 发送者 for i := 0; i \u0026lt; NumSenders; i++ { go func(id string) { for { value := rand.Intn(Max) if value == 0 { stop(\u0026#34;sender#\u0026#34; + id) return } select { case \u0026lt;- closed: return default: } select { case \u0026lt;- closed: return case middleCh \u0026lt;- value: } } }(strconv.Itoa(i)) } // 接收者 for range [NumReceivers]struct{}{} { go func() { defer wgReceivers.Done() for value := range dataCh { log.Println(value) } }() } // ... wgReceivers.Wait() log.Println(\u0026#34;stopped by\u0026#34;, stoppedBy) } 结论 并没有什么情况非得逼得我们违反通道关闭原则。 如果你遇到了此情形，就请考虑修改你的代码流程和结构设计。\n","permalink":"https://pillumina.github.io/posts/programming/golang/channel-graceful/","summary":"\u003ch2 id=\"优雅地关闭通道\"\u003e优雅地关闭通道\u003c/h2\u003e\n\u003ch3 id=\"场景一m个接收者和一个发送者发送者通过关闭用来传输数据的通道来传递发送结束信号\"\u003e场景一：M个接收者和一个发送者。发送者通过关闭用来传输数据的通道来传递发送结束信号\u003c/h3\u003e\n\u003cp\u003e这是最简单的一种情形。当发送者欲结束发送，让它关闭用来传输数据的通道即可。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e51\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003emain\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;math/rand\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;sync\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;log\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003erand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSeed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNow\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnixNano\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSetFlags\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eMax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e100000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eNumReceivers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e100\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewgReceivers\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eWaitGroup\u003c/span\u003e\u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewgReceivers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eNumReceivers\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003edataCh\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// 发送者\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003erand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eIntn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eMax\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"c1\"\u003e// 此唯一的发送者可以安全地关闭此数据通道。\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"nb\"\u003eclose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edataCh\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"nx\"\u003edataCh\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// 接收者\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"nx\"\u003eNumReceivers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003edefer\u003c/span\u003e \u003cspan class=\"nx\"\u003ewgReceivers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDone\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// 接收数据直到通道dataCh已关闭\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"c1\"\u003e// 并且dataCh的缓冲队列已空。\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003edataCh\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"nx\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ePrintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ewgReceivers\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eWait\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"场景二-一个接收者和n个发送者此唯一接收者通过关闭一个额外的信号通道来通知发送者不要在发送数据了\"\u003e场景二： 一个接收者和N个发送者，此唯一接收者通过关闭一个额外的信号通道来通知发送者不要在发送数据了\u003c/h3\u003e\n\u003cp\u003e此情形比上一种情形复杂一些。我们不能让接收者关闭用来传输数据的通道来停止数据传输，因为这样做违反了\u003cstrong\u003e通道关闭原则\u003c/strong\u003e。 但是我们可以让接收者关闭一个额外的信号通道来通知发送者不要在发送数据了。\u003c/p\u003e","title":"Close Channels Gracefully"},{"content":"记录了一些channels常见的场景，以及自己的一些感受：\n使用通道进行异步和并发编程是简单和惬意的；\n通道同步技术比被很多其它语言采用的其它同步方案（比如角色模型和async/await模式）有着更多的应用场景和更多的使用变种。\n通道作为同步手段，并非在任何情况下都是最佳的同步技术，本文也会补充原子操作和sync包内其他的技术作为参考。\n将通道用做future/promise 很多其它流行语言支持future/promise来实现异步（并发）编程。 Future/promise常常用在请求/回应场合。\n返回单向接收通道做为函数返回结果 在下面这个例子中，sumSquares函数调用的两个实参请求并发进行。 每个通道读取操作将阻塞到请求返回结果为止。 两个实参总共需要大约3秒钟（而不是6秒钟）准备完毕（以较慢的一个为准）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;fmt\u0026#34; ) func longTimeRequest() \u0026lt;-chan int32 { r := make(chan int32) go func() { time.Sleep(time.Second * 3) // 模拟一个工作负载 r \u0026lt;- rand.Int31n(100) }() return r } func sumSquares(a, b int32) int32 { return a*a + b*b } func main() { rand.Seed(time.Now().UnixNano()) a, b := longTimeRequest(), longTimeRequest() fmt.Println(sumSquares(\u0026lt;-a, \u0026lt;-b)) } 将单向发送通道类型用做函数实参 和上例一样，在下面这个例子中，sumSquares函数调用的两个实参的请求也是并发进行的。 和上例不同的是longTimeRequest函数接收一个单向发送通道类型参数而不是返回一个单向接收通道结果。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package main import ( \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;fmt\u0026#34; ) func longTimeRequest(r chan\u0026lt;- int32) { time.Sleep(time.Second * 3) // 模拟一个工作负载 r \u0026lt;- rand.Int31n(100) } func sumSquares(a, b int32) int32 { return a*a + b*b } func main() { rand.Seed(time.Now().UnixNano()) ra, rb := make(chan int32), make(chan int32) go longTimeRequest(ra) go longTimeRequest(rb) fmt.Println(sumSquares(\u0026lt;-ra, \u0026lt;-rb)) } 对于上面这个特定的例子，我们可以只使用一个通道来接收回应结果，因为两个参数的作用是对等的。\n1 2 3 4 5 6 7 8 ... results := make(chan int32, 2) // 缓冲与否不重要 go longTimeRequest(results) go longTimeRequest(results) fmt.Println(sumSquares(\u0026lt;-results, \u0026lt;-results)) } 这可以看作是后面将要提到的数据聚合的一个应用。\n采用最快回应 本用例可以看作是上例中只使用一个通道变种的增强。\n有时候，一份数据可能同时从多个数据源获取。这些数据源将返回相同的数据。 因为各种因素，这些数据源的回应速度参差不一，甚至某个特定数据源的多次回应速度之间也可能相差很大。 同时从多个数据源获取一份相同的数据可以有效保障低延迟。我们只需采用最快的回应并舍弃其它较慢回应。\n注意：如果有N个数据源，为了防止被舍弃的回应对应的协程永久阻塞，则传输数据用的通道必须为一个容量至少为N-1的缓冲通道。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; ) func source(c chan\u0026lt;- int32) { ra, rb := rand.Int31(), rand.Intn(3) + 1 // 睡眠1秒/2秒/3秒 time.Sleep(time.Duration(rb) * time.Second) c \u0026lt;- ra } func main() { rand.Seed(time.Now().UnixNano()) startTime := time.Now() c := make(chan int32, 5) // 必须用一个缓冲通道 for i := 0; i \u0026lt; cap(c); i++ { go source(c) } rnd := \u0026lt;- c // 只有第一个回应被使用了 fmt.Println(time.Since(startTime)) fmt.Println(rnd) } “采用最快回应”用例还有一些其它实现方式，本文后面将会谈及。\n更多“请求/回应”用例变种 做为函数参数和返回结果使用的通道可以是缓冲的，从而使得请求协程不需阻塞到它所发送的数据被接收为止。\n有时，一个请求可能并不保证返回一份有效的数据。对于这种情形，我们可以使用一个形如struct{v T; err error}的结构体类型或者一个空接口类型做为通道的元素类型以用来区分回应的值是否有效。\n有时，一个请求可能需要比预期更长的用时才能回应，甚至永远都得不到回应。 我们可以使用本文后面将要介绍的超时机制来应对这样的情况。\n有时，回应方可能会不断地返回一系列值，这也同时属于后面将要介绍的数据流的一个用例。\n使用通道实现通知 通知可以被看作是特殊的请求/回应用例。在一个通知用例中，我们并不关心回应的值，我们只关心回应是否已发生。 所以我们常常使用空结构体类型struct{}来做为通道的元素类型，因为空结构体类型的尺寸为零，能够节省一些内存（虽然常常很少量）。\n向一个通道发送一个值来实现单对单通知 我们已知道，如果一个通道中无值可接收，则此通道上的下一个接收操作将阻塞到另一个协程发送一个值到此通道为止。 所以一个协程可以向此通道发送一个值来通知另一个等待着从此通道接收数据的协程。\n在下面这个例子中，通道done被用来做为一个信号通道来实现单对单通知。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package main import ( \u0026#34;crypto/rand\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;sort\u0026#34; ) func main() { values := make([]byte, 32 * 1024 * 1024) if _, err := rand.Read(values); err != nil { fmt.Println(err) os.Exit(1) } done := make(chan struct{}) // 也可以是缓冲的 // 排序协程 go func() { sort.Slice(values, func(i, j int) bool { return values[i] \u0026lt; values[j] }) done \u0026lt;- struct{}{} // 通知排序已完成 }() // 并发地做一些其它事情... \u0026lt;- done // 等待通知 fmt.Println(values[0], values[len(values)-1]) } 从一个通道接收一个值来实现单对单通知 如果一个通道的数据缓冲队列已满（非缓冲的通道的数据缓冲队列总是满的）但它的发送协程队列为空，则向此通道发送一个值将阻塞，直到另外一个协程从此通道接收一个值为止。 所以我们可以通过从一个通道接收数据来实现单对单通知。一般我们使用非缓冲通道来实现这样的通知。\n这种通知方式不如上例中介绍的方式使用得广泛，基本很少用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { done := make(chan struct{}) // 此信号通道也可以缓冲为1。如果这样，则在下面 // 这个协程创建之前，我们必须向其中写入一个值。 go func() { fmt.Print(\u0026#34;Hello\u0026#34;) // 模拟一个工作负载。 time.Sleep(time.Second * 2) // 使用一个接收操作来通知主协程。 \u0026lt;- done }() done \u0026lt;- struct{}{} // 阻塞在此，等待通知 fmt.Println(\u0026#34; world!\u0026#34;) } 另一个事实是，上面的两种单对单通知方式其实并没有本质的区别。 它们都可以被概括为较快者等待较慢者发出通知。\n多对单和单对多通知 略微扩展一下上面两个用例，我们可以很轻松地实现多对单和单对多通知。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package main import \u0026#34;log\u0026#34; import \u0026#34;time\u0026#34; type T = struct{} func worker(id int, ready \u0026lt;-chan T, done chan\u0026lt;- T) { \u0026lt;-ready // 阻塞在此，等待通知 log.Print(\u0026#34;Worker#\u0026#34;, id, \u0026#34;开始工作\u0026#34;) // 模拟一个工作负载。 time.Sleep(time.Second * time.Duration(id+1)) log.Print(\u0026#34;Worker#\u0026#34;, id, \u0026#34;工作完成\u0026#34;) done \u0026lt;- T{} // 通知主协程（N-to-1） } func main() { log.SetFlags(0) ready, done := make(chan T), make(chan T) go worker(0, ready, done) go worker(1, ready, done) go worker(2, ready, done) // 模拟一个初始化过程 time.Sleep(time.Second * 3 / 2) // 单对多通知 ready \u0026lt;- T{}; ready \u0026lt;- T{}; ready \u0026lt;- T{} // 等待被多对单通知 \u0026lt;-done; \u0026lt;-done; \u0026lt;-done } 这种写法是比较少见的，因为not clean enough，一般用sync.WaitGroup实现多对单的通知，使用关闭一个通道方式实现单对多。\n通过关闭一个通道来实现群发通知（单对多模式优化） 关闭一个通道进行对多通知更简单。用到的特性是能够从一个已经关闭的通道接受到无穷多的值。\n我们可以把上一个例子中的三个数据发送操作ready \u0026lt;- struct{}{}替换为一个通道关闭操作close(ready)来达到同样的单对多通知效果。\n1 2 3 ... close(ready) // 群发通知Let\u0026#39;s go! ... 其实，单对单通知一般也是用关闭通道的方式，这也是实践中用到最多的通知实现方式。context库中用这种特性实现了传达操作取消消息，后续会介绍具体的cases。\n定时通知（timer） 标准库里的time.After的实现，也就是函数返回一个channel（容量为1的缓冲通道），起一个gorountine等待一段时间后往这个channel里送一个空结构体，类似的逻辑。\n将通道用做互斥锁（mutex） 运用容量为1的缓冲通道作为多次性二元semaphore，也就是mutex，这种mutex不如sync标准包里的高效。\n有两种方式将一个容量为1的缓冲通道用做互斥锁：\n通过发送操作来加锁，通过接收操作来解锁； 通过接收操作来加锁，通过发送操作来解锁。 写一个发送操作加锁的例子，第二种反一下就行：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package main import \u0026#34;fmt\u0026#34; func main() { mutex := make(chan struct{}, 1) // 容量必须为1 counter := 0 increase := func() { mutex \u0026lt;- struct{}{} // 加锁 counter++ \u0026lt;-mutex // 解锁 } increase1000 := func(done chan\u0026lt;- struct{}) { for i := 0; i \u0026lt; 1000; i++ { increase() } done \u0026lt;- struct{}{} } done := make(chan struct{}) go increase1000(done) go increase1000(done) \u0026lt;-done; \u0026lt;-done fmt.Println(counter) // 2000 } 将通道用做计数信号量（counting semaphore） 缓冲通道可以用于作为counting semaphore，也就是多主锁 \u0026ndash; 如果一个缓冲通道的容量为N，那么它可以被看作是一个在任何时刻最多可有N个主人的锁。 上面提到的二元信号量是特殊的计数信号量，每个二元信号量在任一时刻最多只能有一个主人。\n计数信号量经常被使用于限制最大并发数。\n和将通道用做互斥锁一样，也有两种方式用来获取一个用做计数信号量的通道的一份所有权。\n通过发送操作来获取所有权，通过接收操作来释放所有权； 通过接收操作来获取所有权，通过发送操作来释放所有权。 下面是一个酒吧在座位数一定的前提下服务客人的例子，以接受操作获取所有权，先来一个简化版:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package main import ( \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; ) type Seat int type Bar chan Seat func (bar Bar) ServeCustomer(c int) { log.Print(\u0026#34;顾客#\u0026#34;, c, \u0026#34;进入酒吧\u0026#34;) seat := \u0026lt;- bar // 需要一个位子来喝酒 log.Print(\u0026#34;++ customer#\u0026#34;, c, \u0026#34; drinks at seat#\u0026#34;, seat) log.Print(\u0026#34;++ 顾客#\u0026#34;, c, \u0026#34;在第\u0026#34;, seat, \u0026#34;个座位开始饮酒\u0026#34;) time.Sleep(time.Second * time.Duration(2 + rand.Intn(6))) log.Print(\u0026#34;-- 顾客#\u0026#34;, c, \u0026#34;离开了第\u0026#34;, seat, \u0026#34;个座位\u0026#34;) bar \u0026lt;- seat // 释放座位，离开酒吧 } func main() { rand.Seed(time.Now().UnixNano()) bar24x7 := make(Bar, 10) // 此酒吧有10个座位 // 摆放10个座位。 for seatId := 0; seatId \u0026lt; cap(bar24x7); seatId++ { bar24x7 \u0026lt;- Seat(seatId) // 均不会阻塞 } for customerId := 0; ; customerId++ { time.Sleep(time.Second) go bar24x7.ServeCustomer(customerId) } for {time.Sleep(time.Second)} // 睡眠不属于阻塞状态 } 在上例中，只有获得一个座位的顾客才能开始饮酒。 所以在任一时刻同时在喝酒的顾客数不会超过座位数10。最后的for循环其实不太好，用select{}阻塞比较好。\n这个例子不好的地方，在于尽管在任一时刻同时在喝酒的顾客数不会超过座位数10，但是在某一时刻可能有多于10个顾客进入了酒吧，因为某些顾客在排队等位子。 在上例中，每个顾客对应着一个协程。虽然协程的开销比系统线程小得多，但是如果协程的数量很多，则它们的总体开销还是不能忽略不计的。 所以，最好当有空位的时候才创建顾客协程。我们可以做个小的优化：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... // 省略了和上例相同的代码 func (bar Bar) ServeCustomerAtSeat(c int, seat Seat) { log.Print(\u0026#34;++ 顾客#\u0026#34;, c, \u0026#34;在第\u0026#34;, seat, \u0026#34;个座位开始饮酒\u0026#34;) time.Sleep(time.Second * time.Duration(2 + rand.Intn(6))) log.Print(\u0026#34;-- 顾客#\u0026#34;, c, \u0026#34;离开了第\u0026#34;, seat, \u0026#34;个座位\u0026#34;) bar \u0026lt;- seat // 释放座位，离开酒吧 } func main() { rand.Seed(time.Now().UnixNano()) bar24x7 := make(Bar, 10) for seatId := 0; seatId \u0026lt; cap(bar24x7); seatId++ { bar24x7 \u0026lt;- Seat(seatId) } // 这个for循环和上例不一样。 for customerId := 0; ; customerId++ { time.Sleep(time.Second) seat := \u0026lt;- bar24x7 // 需要一个空位招待顾客 go bar24x7.ServeCustomerAtSeat(customerId, seat) } for {time.Sleep(time.Second)} } 在上面这个修改后的例子中，在任一时刻最多只有10个顾客协程在运行（但是在程序的生命期内，仍旧会有大量的顾客协程不断被创建和销毁）。\n所以考虑每个gorountine作为消费者，不断从customers的通道去获取客人，这样消费者的数量是一定的。在下面这个更加高效的实现中，在程序的生命期内最多只会有10个顾客协程被创建出来:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ... // 省略了和上例相同的代码 func (bar Bar) ServeCustomerAtSeat(consumers chan int) { for c := range consumers { seatId := \u0026lt;- bar log.Print(\u0026#34;++ 顾客#\u0026#34;, c, \u0026#34;在第\u0026#34;, seatId, \u0026#34;个座位开始饮酒\u0026#34;) time.Sleep(time.Second * time.Duration(2 + rand.Intn(6))) log.Print(\u0026#34;-- 顾客#\u0026#34;, c, \u0026#34;离开了第\u0026#34;, seatId, \u0026#34;个座位\u0026#34;) bar \u0026lt;- seatId // 释放座位，离开酒吧 } } func main() { rand.Seed(time.Now().UnixNano()) bar24x7 := make(Bar, 10) for seatId := 0; seatId \u0026lt; cap(bar24x7); seatId++ { bar24x7 \u0026lt;- Seat(seatId) } consumers := make(chan int) for i := 0; i \u0026lt; cap(bar24x7); i++ { go bar24x7.ServeCustomerAtSeat(consumers) } for customerId := 0; ; customerId++ { time.Sleep(time.Second) consumers \u0026lt;- customerId } } 通过发送操作来获取所有权的实现相对简单一些，省去了摆放座位的步骤：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package main import ( \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;math/rand\u0026#34; ) type Customer struct{id int} type Bar chan Customer func (bar Bar) ServeCustomer(c Customer) { log.Print(\u0026#34;++ 顾客#\u0026#34;, c.id, \u0026#34;开始饮酒\u0026#34;) time.Sleep(time.Second * time.Duration(3 + rand.Intn(16))) log.Print(\u0026#34;-- 顾客#\u0026#34;, c.id, \u0026#34;离开酒吧\u0026#34;) \u0026lt;- bar // 离开酒吧，腾出位子 } func main() { rand.Seed(time.Now().UnixNano()) bar24x7 := make(Bar, 10) // 最多同时服务10位顾客 for customerId := 0; ; customerId++ { time.Sleep(time.Second * 2) customer := Customer{customerId} bar24x7 \u0026lt;- customer // 等待进入酒吧 go bar24x7.ServeCustomer(customer) } for {time.Sleep(time.Second)} } 使用通道传送传输通道(special case) 一个通道类型的元素类型可以是另一个通道类型。 在下面这个例子中， 单向发送通道类型chan\u0026lt;- int是另一个通道类型chan chan\u0026lt;- int的元素类型。\n下面的例子在这个场景其实不是最好的方案，还在关注类似的场景。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package main import \u0026#34;fmt\u0026#34; var counter = func (n int) chan\u0026lt;- chan\u0026lt;- int { requests := make(chan chan\u0026lt;- int) go func() { for request := range requests { if request == nil { n++ // 递增计数 } else { request \u0026lt;- n // 返回当前计数 } } }() return requests // 隐式转换到类型chan\u0026lt;- (chan\u0026lt;- int) }(0) func main() { increase1000 := func(done chan\u0026lt;- struct{}) { for i := 0; i \u0026lt; 1000; i++ { counter \u0026lt;- nil } done \u0026lt;- struct{}{} } done := make(chan struct{}) go increase1000(done) go increase1000(done) \u0026lt;-done; \u0026lt;-done request := make(chan int, 1) counter \u0026lt;- request fmt.Println(\u0026lt;-request) // 2000 } 检查通道的长度和容量 我们可以使用内置函数cap和len来查看一个通道的容量和当前长度。 但是在实践中我们很少这样做。我们很少使用内置函数cap的原因是一个通道的容量常常是已知的或者不重要的。 我们很少使用内置函数len的原因是一个**len调用的结果并不能总能准确地反映出的一个通道的当前长度。**\n但有时确实有一些场景需要调用这两个函数。比如，有时一个协程欲将一个未关闭的并且不会再向其中发送数据的缓冲通道中的所有数据接收出来，在确保只有此一个协程从此通道接收数据的情况下，我们可以用下面的代码来实现：\n1 2 3 4 for len(c) \u0026gt; 0 { value := \u0026lt;-c // 使用value ... } 不过这种场景可以用尝试接收机制来实现，这两种方式的运行效率差距不大，但尝试接收机制的优点是多个协程可以并发地进行读取操作。\n有时一个协程欲将一个缓冲通道写满而又不阻塞，在确保只有此一个协程向此通道发送数据的情况下，我们可以用下面的代码实现这一目的：\n1 2 3 for len(c) \u0026lt; cap(c) { c \u0026lt;- aValue } 当然，尝试发送机制也能cover。\n尝试发送和尝试接收(无阻塞select/default) 含有一个default分支和一个case分支的select代码块可以被用做一个尝试发送或者尝试接收操作，取决于case关键字后跟随的是一个发送操作还是一个接收操作。\n如果case关键字后跟随的是一个发送操作，则此select代码块为一个尝试发送操作。 如果case分支的发送操作是阻塞的，则default分支将被执行，发送失败；否则发送成功，case分支得到执行。 如果case关键字后跟随的是一个接收操作，则此select代码块为一个尝试接收操作。 如果case分支的接收操作是阻塞的，则default分支将被执行，接收失败；否则接收成功，case分支得到执行。 尝试发送和尝试接收代码块永不阻塞。\n标准编译器对尝试发送和尝试接收代码块做了特别的优化，使得它们的执行效率比多case分支的普通select代码块执行效率高得多。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package main import \u0026#34;fmt\u0026#34; func main() { type Book struct{id int} bookshelf := make(chan Book, 3) for i := 0; i \u0026lt; cap(bookshelf) * 2; i++ { select { case bookshelf \u0026lt;- Book{id: i}: fmt.Println(\u0026#34;成功将书放在书架上\u0026#34;, i) default: fmt.Println(\u0026#34;书架已经被占满了\u0026#34;) } } for i := 0; i \u0026lt; cap(bookshelf) * 2; i++ { select { case book := \u0026lt;-bookshelf: fmt.Println(\u0026#34;成功从书架上取下一本书\u0026#34;, book.id) default: fmt.Println(\u0026#34;书架上已经没有书了\u0026#34;) } } } 输出结果很简单:\n成功将书放在书架上 0 成功将书放在书架上 1 成功将书放在书架上 2 书架已经被占满了 书架已经被占满了 书架已经被占满了 成功从书架上取下一本书 0 成功从书架上取下一本书 1 成功从书架上取下一本书 2 书架上已经没有书了 书架上已经没有书了 书架上已经没有书了 无阻塞地检查一个通道是否已经关闭 假设我们可以保证没有任何协程会向一个通道发送数据，则我们可以使用下面的代码来（并发安全地）检查此通道是否已经关闭，此检查不会阻塞当前协程。\n1 2 3 4 5 6 7 8 func IsClosed(c chan T) bool { select { case \u0026lt;-c: return true default: } return false } 常用场景：此方法常用来查看某个期待中的通知是否已经来临。此通知将由另一个协程通过关闭一个通道来发送。\n峰值限制（peak/burst limiting） 通道用作counting semaphore + 通道尝试(发送/接收)可实现峰值限制。目的是防止过大的并发请求数。\n还是那个酒吧招待客户的例子，现在做一些修改能让顾客不再等待而是离去或者寻找其它酒吧。\n1 2 3 4 5 6 7 8 9 10 11 12 13 ... bar24x7 := make(Bar, 10) // 此酒吧只能同时招待10个顾客 for customerId := 0; ; customerId++ { time.Sleep(time.Second) consumer := Consumer{customerId} select { case bar24x7 \u0026lt;- consumer: // 试图进入此酒吧 go bar24x7.ServeConsumer(consumer) default: log.Print(\u0026#34;顾客#\u0026#34;, customerId, \u0026#34;不愿等待而离去\u0026#34;) } } ... 另一种“采用最快回应”的实现方式(复杂场景用) 在上面的“采用最快回应”用例一节已经提到，我们也可以使用选择机制来实现“采用最快回应”用例。 每个数据源协程只需使用一个缓冲为1的通道并向其尝试发送回应数据即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;time\u0026#34; ) func source(c chan\u0026lt;- int32) { ra, rb := rand.Int31(), rand.Intn(3)+1 // 休眠1秒/2秒/3秒 time.Sleep(time.Duration(rb) * time.Second) select { case c \u0026lt;- ra: default: } } func main() { rand.Seed(time.Now().UnixNano()) c := make(chan int32, 1) // 此通道容量必须至少为1 for i := 0; i \u0026lt; 5; i++ { go source(c) } rnd := \u0026lt;-c // 只采用第一个成功发送的回应数据 fmt.Println(rnd) } 注意，使用选择机制来实现“采用最快回应”的代码中使用的通道的容量必须至少为1，以保证最快回应总能够发送成功。 否则，如果数据请求者因为种种原因未及时准备好接收，则所有回应者的尝试发送都将失败，从而所有回应的数据都将被错过。\n少量数据源“采用最快回应”的实现方式 如果一个“采用最快回应”用例中的数据源的数量很少，比如两个或三个，我们可以让每个数据源使用一个单独的缓冲通道来回应数据，然后使用一个select代码块来同时接收这三个通道。 示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;time\u0026#34; ) func source() \u0026lt;-chan int32 { c := make(chan int32, 1) // 必须为一个缓冲通道 go func() { ra, rb := rand.Int31(), rand.Intn(3)+1 time.Sleep(time.Duration(rb) * time.Second) c \u0026lt;- ra }() return c } func main() { rand.Seed(time.Now().UnixNano()) var rnd int32 // 阻塞在此直到某个数据源率先回应。 select{ case rnd = \u0026lt;-source(): case rnd = \u0026lt;-source(): case rnd = \u0026lt;-source(): } fmt.Println(rnd) } 注意：如果上例中使用的通道是非缓冲的，未被选中的case分支对应的两个source函数调用中开辟的协程将处于永久阻塞状态，从而造成内存泄露。\n超时机制（timeout） 在一些请求/回应用例中，一个请求可能因为种种原因导致需要超出预期的时长才能得到回应，有时甚至永远得不到回应。 对于这样的情形，我们可以使用一个超时方案给请求者返回一个错误信息。 使用选择机制可以很轻松地实现这样的一个超时方案。\n下面这个例子展示了如何实现一个支持超时设置的请求：\n1 2 3 4 5 6 7 8 9 10 11 func requestWithTimeout(timeout time.Duration) (int, error) { c := make(chan int) go doRequest(c) // 可能需要超出预期的时长回应 select { case data := \u0026lt;-c: return data, nil case \u0026lt;-time.After(timeout): return 0, errors.New(\u0026#34;超时了！\u0026#34;) } } 脉搏器（ticker） 我们可以使用尝试发送操作来实现一个每隔一定时间发送一个信号的脉搏器。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package main import \u0026#34;fmt\u0026#34; import \u0026#34;time\u0026#34; func Tick(d time.Duration) \u0026lt;-chan struct{} { c := make(chan struct{}, 1) // 容量最好为1 go func() { for { time.Sleep(d) select { case c \u0026lt;- struct{}{}: default: } } }() return c } func main() { t := time.Now() for range Tick(time.Second) { fmt.Println(time.Since(t)) } } 事实上，time标准库包中的Tick函数提供了同样的功能，但效率更高。 我们应该尽量使用标准库包中的实现。\n速率限制（rate limiting） 前面实现了峰值限制，同样地我们可以使用尝试机制实现速率限制，但是这里要和定时器配合实现。速率限制常用来限制吞吐和确保在一段时间内的资源使用不会超标。\n下面的例子借鉴了官方Go维基中的例子。 在此例中，任何一分钟时段内处理的请求数不会超过200\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package main import \u0026#34;fmt\u0026#34; import \u0026#34;time\u0026#34; type Request interface{} func handle(r Request) {fmt.Println(r.(int))} const RateLimitPeriod = time.Minute const RateLimit = 200 // 任何一分钟内最多处理200个请求 func handleRequests(requests \u0026lt;-chan Request) { quotas := make(chan time.Time, RateLimit) go func() { tick := time.NewTicker(RateLimitPeriod / RateLimit) defer tick.Stop() for t := range tick.C { select { case quotas \u0026lt;- t: default: } } }() for r := range requests { \u0026lt;-quotas go handle(r) } } func main() { requests := make(chan Request) go handleRequests(requests) // time.Sleep(time.Minute) for i := 0; ; i++ {requests \u0026lt;- i} } 上例的代码虽然可以保证任何一分钟时段内处理的请求数不会超过200，但是如果在开始的一分钟内没有任何请求，则接下来的某个瞬时时间点可能会同时处理最多200个请求（试着将time.Sleep行的注释去掉), 这可能会造成卡顿情况。我们可以将速率限制和峰值限制一并使用来避免出现这样的情况。\n开关 向一个nil通道发送数据或者从中接收数据都属于阻塞操作。 利用这一事实，我们可以将一个select流程控制中的case操作中涉及的通道设置为不同的值，以使此select流程控制选择执行不同的分支。\n控制代码被执行的几率（少用） 我们可以通过在一个select流程控制中使用重复的case操作来增加对应分支中的代码的执行几率。这种操作比较少见，下面这个例子， 函数f的调用执行几率大致为函数g的两倍:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import \u0026#34;fmt\u0026#34; func main() { foo, bar := make(chan struct{}), make(chan struct{}) close(foo); close(bar) // 仅为演示目的 x, y := 0.0, 0.0 f := func(){x++} g := func(){y++} for i := 0; i \u0026lt; 100000; i++ { select { case \u0026lt;-foo: f() case \u0026lt;-foo: f() case \u0026lt;-bar: g() } } fmt.Println(x/y) // 大致为2 } 从动态数量的分支中选择（少用） reflection ain\u0026rsquo;t good\n每个select控制流程中的分支数量在运行中是固定的，但是我们可以使用reflect标准库包中提供的功能在运行时刻来构建动态分支数量的select控制流程。 但是请注意：一个select控制流程中的分支越多，此select控制流程的执行效率就越低（这是我们常常只使用不多于三个分支的select控制流程的原因）。 reflect标准库包中也提供了模拟尝试发送和尝试接收代码块的TrySend和TryRecv函数。涉及到反射的，肯定有更好的解决方案:-)。\n数据流操纵（Data Flow） 下面把数据流处理程序大致分成了常见的几类，概念上来说是不同模块由一个或者多个并行处理的协程组成：\n数据生成/搜集/加载；\n数据服务/存盘；\n数据计算/处理；\n数据验证/过滤；\n数据聚合/分流；\n数据组合/拆分；\n数据复制/增殖；\n一个模块中的工作协程从一些其它模块接收数据做为输入，并向另一些模块发送输出数据。 换句话数，一个模块可能同时兼任数据消费者和数据产生者的角色。多个模块一起组成了一个数据流处理系统。后续的一些实现可能并不高效，只是为了描述这些分类模块的实现，比较简单。\n数据生成/搜集/加载 一个数据产生者可能通过以下途径生成数据：\n加载一个文件、或者读取一个数据库、或者用爬虫抓取网页数据；\n从一个软件或者硬件系统搜集各种数据；\n产生一系列随机数；\netc.\n这里的例子是一个随机数生成器作为数据生产者，生产者只有数据输出，所以返回只读通道。实际上此随机数产生器是一个多返回值的future/promise，一个数据产生者可以在任何时刻关闭返回的通道以结束数据生成。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ( \u0026#34;crypto/rand\u0026#34; \u0026#34;encoding/binary\u0026#34; ) func RandomGenerator() \u0026lt;-chan uint64 { c := make(chan uint64) go func() { rnds := make([]byte, 8) for { _, err := rand.Read(rnds) if err != nil { close(c) // 若读取错误则关闭通道结束数据生成 break } c \u0026lt;- binary.BigEndian.Uint64(rnds) } }() return c } 数据聚合 例如把多个数据流合为一个数据流，下面这个函数把任意数量的数据流合为一个：\n1 2 3 4 5 6 7 8 9 10 11 func Aggregator(inputs ...\u0026lt;-chan uint64) \u0026lt;-chan uint64 { out := make(chan uint64) for _, in := range inputs { go func(in \u0026lt;-chan uint64) { for { out \u0026lt;- \u0026lt;-in // \u0026lt;=\u0026gt; out \u0026lt;- (\u0026lt;-in) } }(in) } return out } 但是这个例子，最好需要考虑一个输入数据流是否已经关闭：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import \u0026#34;sync\u0026#34; func Aggregator(inputs ...\u0026lt;-chan uint64) \u0026lt;-chan uint64 { output := make(chan uint64) var wg sync.WaitGroup for _, in := range inputs { wg.Add(1) go func(int \u0026lt;-chan uint64) { defer wg.Done() // 如果通道in被关闭，此循环将最终结束。 for x := range in { output \u0026lt;- x } }(in) } go func() { wg.Wait() close(output) }() return output } 如果被聚合的数据流的数量很小，我们也可以使用一个select控制流程代码块来聚合这些数据流。\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 假设数据流的数量为2。 ... output := make(chan uint64) go func() { inA, inB := inputs[0], inputs[1] for { select { case v := \u0026lt;- inA: output \u0026lt;- v case v := \u0026lt;- inB: output \u0026lt;- v } } } ... 数据分流 数据分流是数据聚合的逆过程。数据分流的实现很简单，但在实践中用的并不多。\n1 2 3 4 5 6 7 8 9 func Divisor(input \u0026lt;-chan uint64, outputs ...chan\u0026lt;- uint64) { for _, out := range outputs { go func(o chan\u0026lt;- uint64) { for { o \u0026lt;- \u0026lt;-input // \u0026lt;=\u0026gt; o \u0026lt;- (\u0026lt;-input) } }(out) } } 数据合成 数据合成将多个数据流中读取的数据合成一个。\n下面是一个数据合成工作函数的实现中，从两个不同数据流读取的两个uint64值组成了一个新的uint64值。 当然，在实践中，数据的组合比这复杂得多。\n1 2 3 4 5 6 7 8 9 10 func Composor(inA, inB \u0026lt;-chan uint64) \u0026lt;-chan uint64 { output := make(chan uint64) go func() { for { a1, b, a2 := \u0026lt;-inA, \u0026lt;-inB, \u0026lt;-inA output \u0026lt;- a1 ^ b \u0026amp; a2 } }() return output } 数据分解 数据分解是数据合成的逆过程。一个数据分解者从一个通道读取一份数据，并将此数据分解为多份数据。 这里就不举例了。\n数据复制/增殖 数据复制（增殖）可以看作是特殊的数据分解。一份输入数据将被复制多份并输出给多个数据流。\n1 2 3 4 5 6 7 8 9 10 func Duplicator(in \u0026lt;-chan uint64) (\u0026lt;-chan uint64, \u0026lt;-chan uint64) { outA, outB := make(chan uint64), make(chan uint64) go func() { for x := range in { outA \u0026lt;- x outB \u0026lt;- x } }() return outA, outB } 数据计算/分析 数据计算和数据分析模块的功能因具体程序不同而有很大的差异。 一般来说，数据分析者接收一份数据并对之加工处理后转换为另一份数据。\n下面的简单示例中，每个输入的uint64值将被进行位反转后输出。\n1 2 3 4 5 6 7 8 9 10 11 func Calculator(in \u0026lt;-chan uint64, out chan uint64) (\u0026lt;-chan uint64) { if out == nil { out = make(chan uint64) } go func() { for x := range in { out \u0026lt;- ^x } }() return out } 数据验证/过滤 一个数据验证或过滤者的任务是检查输入数据的合理性并抛弃不合理的数据。 比如，下面的工作者协程将抛弃所有的非素数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import \u0026#34;math/big\u0026#34; func Filter0(input \u0026lt;-chan uint64, output chan uint64) \u0026lt;-chan uint64 { if output == nil { output = make(chan uint64) } go func() { bigInt := big.NewInt(0) for x := range input { bigInt.SetUint64(x) if bigInt.ProbablyPrime(1) { output \u0026lt;- x } } }() return output } func Filter(input \u0026lt;-chan uint64) \u0026lt;-chan uint64 { return Filter0(input, nil) } 数据服务/存盘 一般，一个数据服务或者存盘模块为一个数据流系统中的最后一个模块。 这里的实现值是简单地将数据输出到终端。\n1 2 3 4 5 6 7 import \u0026#34;fmt\u0026#34; func Printer(input \u0026lt;-chan uint64) { for x := range input { fmt.Println(x) } } 组装数据流系统 Now，让我们使用上面的模块工作者函数实现来组装一些数据流系统。 组装数据流仅仅是创建一些工作者协程函数调用，并为这些调用指定输入数据流和输出数据流。\n数据流系统例子1（一个流线型系统）\n1 2 3 4 5 6 7 8 9 10 11 12 13 package main ... // 上面的模块工作者函数实现 func main() { Printer( Filter( Calculator( RandomGenerator(), nil, ), ), ) } 数据流系统例子2（一个单向无环图系统）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main ... // 上面的模块工作者函数实现 func main() { filterA := Filter(RandomGenerator()) filterB := Filter(RandomGenerator()) filterC := Filter(RandomGenerator()) filter := Aggregator(filterA, filterB, filterC) calculatorA := Calculator(filter, nil) calculatorB := Calculator(filter, nil) calculator := Aggregator(calculatorA, calculatorB) Printer(calculator) } 更复杂的数据流系统可以表示为任何拓扑结构的图。比如一个复杂的数据流系统可能有多个输出模块。 但是有环拓扑结构的数据流系统在实践中很少用。\n从上面两个例子可以看出，使用通道来构建数据流系统是很简单和直观的。而且，通过使用数据聚合模块，我们可以很轻松地实现各个模块的工作协程数量的扇入（fan-in）和扇出（fan-out）。\n事实上，我们也可以使用一个简单的通道来代替数据聚合模块的角色。比如，下面的代码使用两个通道代替了上例中的两个数据聚合器。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main ... // 上面的模块工作者函数实现 func main() { c1 := make(chan uint64, 100) Filter0(RandomGenerator(), c1) // filterA Filter0(RandomGenerator(), c1) // filterB Filter0(RandomGenerator(), c1) // filterC c2 := make(chan uint64, 100) Calculator(c1, c2) // calculatorA Calculator(c1, c2) // calculatorB Printer(c2) } 上面的代码示例并没有太多考虑如何关闭一个数据流，会单独开一个文章介绍。\n","permalink":"https://pillumina.github.io/posts/programming/golang/channels/","summary":"\u003cp\u003e记录了一些channels常见的场景，以及自己的一些感受：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e使用通道进行异步和并发编程是简单和惬意的；\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e通道同步技术比被很多其它语言采用的其它同步方案（比如\u003ca href=\"https://en.wikipedia.org/wiki/Actor_model\"\u003e角色模型\u003c/a\u003e和\u003ca href=\"https://en.wikipedia.org/wiki/Async/await\"\u003easync/await模式\u003c/a\u003e）有着更多的应用场景和更多的使用变种。\u003c/p\u003e\n\u003cp\u003e通道作为同步手段，并非在任何情况下都是最佳的同步技术，本文也会补充原子操作和sync包内其他的技术作为参考。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"将通道用做futurepromise\"\u003e将通道用做future/promise\u003c/h3\u003e\n\u003cp\u003e很多其它流行语言支持future/promise来实现异步（并发）编程。 Future/promise常常用在请求/回应场合。\u003c/p\u003e\n\u003ch4 id=\"返回单向接收通道做为函数返回结果\"\u003e返回单向接收通道做为函数返回结果\u003c/h4\u003e\n\u003cp\u003e在下面这个例子中，\u003ccode\u003esumSquares\u003c/code\u003e函数调用的两个实参请求并发进行。 每个通道读取操作将阻塞到请求返回结果为止。 两个实参总共需要大约3秒钟（而不是6秒钟）准备完毕（以较慢的一个为准）。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003emain\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;math/rand\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"s\"\u003e\u0026#34;fmt\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003elongTimeRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003er\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eSecond\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 模拟一个工作负载\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003er\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e \u003cspan class=\"nx\"\u003erand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eInt31n\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003er\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003esumSquares\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"kt\"\u003eint32\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003erand\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSeed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eNow\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003eUnixNano\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nf\"\u003elongTimeRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \u003cspan class=\"nf\"\u003elongTimeRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ePrintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003esumSquares\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch4 id=\"将单向发送通道类型用做函数实参\"\u003e将单向发送通道类型用做函数实参\u003c/h4\u003e\n\u003cp\u003e和上例一样，在下面这个例子中，\u003ccode\u003esumSquares\u003c/code\u003e函数调用的两个实参的请求也是并发进行的。 和上例不同的是\u003ccode\u003elongTimeRequest\u003c/code\u003e函数接收一个单向发送通道类型参数而不是返回一个单向接收通道结果。\u003c/p\u003e","title":"Channels Concurrency Work-Around"},{"content":"阅读到的一些方便、有趣的技巧或者ideas的随手记录，后续考虑对相关话题专门开坑\n一行代码画出专业的论文图 SciencePlots\n计算机专业向来不缺少专业的绘图软件，从Excel到PPT，从最近沸沸扬扬的Matlab到Matplotlib、pyplot、ggplot，乃至其他更为专业的软件，着实丰富了我们的画图生活。\n但是，这些软件或工具的背后，常常需要我们付出更多的努力：调色、统一格式、展示要高大上，等等。\n现在，一款开源的软件工具包问世了：SciencePlots。它让你用一行代码画出天然高端且美观的论文图。\nSciencePlots是一个依附于Matplotlib的扩展包，可以通过pip一键安装：\npip install SciencePlots 然后我们在画图时，只需要一句with.plt.style.context(['science']):，就可以画出非常美观且专业的图：\n你还可以加一个选项with.plt.style.context(['science','ieee']):，就能画出IEEE格式的图：\n甚至是超美的散点图：\n还有很多自定义的图像风格，保证节约我们的画图时间\n这个包默认会调用latex来画图，如果不想用latex（也不是完全需要），可以在context里写一个属性\u0026rsquo;nolatex\u0026rsquo;即可。不然如果没有安装latex或latex路径配置有问题，则会报错。\n","permalink":"https://pillumina.github.io/posts/programming/black-magic/","summary":"\u003cp\u003e\u003ccode\u003e阅读到的一些方便、有趣的技巧或者ideas的随手记录，后续考虑对相关话题专门开坑\u003c/code\u003e\u003c/p\u003e\n\u003ch2 id=\"一行代码画出专业的论文图\"\u003e一行代码画出专业的论文图\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/garrettj403/SciencePlots\"\u003eSciencePlots\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e计算机专业向来不缺少专业的绘图软件，从Excel到PPT，从最近沸沸扬扬的Matlab到Matplotlib、pyplot、ggplot，乃至其他更为专业的软件，着实丰富了我们的画图生活。\u003c/p\u003e\n\u003cp\u003e但是，这些软件或工具的背后，常常需要我们付出更多的努力：调色、统一格式、展示要高大上，等等。\u003c/p\u003e\n\u003cp\u003e现在，一款开源的软件工具包问世了：\u003cstrong\u003eSciencePlots\u003c/strong\u003e。它让你用一行代码画出天然高端且美观的论文图。\u003c/p\u003e\n\u003cp\u003eSciencePlots是一个依附于Matplotlib的扩展包，可以通过pip一键安装：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epip install SciencePlots\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后我们在画图时，只需要一句\u003ccode\u003ewith.plt.style.context(['science']):\u003c/code\u003e，就可以画出非常美观且专业的图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"plot1\" loading=\"lazy\" src=\"https://pic1.zhimg.com/80/v2-90ced58bd948b48122c7c49f6dd3aeb8_1440w.jpg\" data-zoomable\u003e\n\u003c/p\u003e\n\u003cp\u003e你还可以加一个选项\u003ccode\u003ewith.plt.style.context(['science','ieee']):\u003c/code\u003e，就能画出IEEE格式的图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"plot-ieee\" loading=\"lazy\" src=\"https://pic3.zhimg.com/80/v2-65a94e294409928599dc91745f01662e_1440w.jpg\" data-zoomable\u003e\n\u003c/p\u003e\n\u003cp\u003e甚至是超美的散点图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"plot-scatter\" loading=\"lazy\" src=\"https://pic4.zhimg.com/80/v2-db5c1cc749638e5bfba236fa9acdb4ff_1440w.jpg\" data-zoomable\u003e\n\u003c/p\u003e\n\u003cp\u003e还有很多自定义的图像风格，保证节约我们的画图时间\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这个包默认会调用latex来画图，如果不想用latex（也不是完全需要），可以在context里写一个属性\u0026rsquo;nolatex\u0026rsquo;即可。不然如果没有安装latex或latex路径配置有问题，则会报错。\u003c/p\u003e\u003c/blockquote\u003e","title":"Black Magic"},{"content":"这个post为记录目前正在阅读与研究的section\nGo语言设计 Go语言设计与实现\nGo Under The Hood\n这两本在写作目的和内容规划都是一致的，不过第二个原本不再维护内容，作者开了下面的新的项目，把撰写原本而积累的与Go相关的资源进行了重新的整理。 Go设计历史\npprof对服务端性能影响的研究 考虑一些极端场景，比如极度追求性能，压榨系统资源以及技术栈必须是Go的业务场景下，是否能自己构建Reactor网络模型\nGRPC框架对服务侧性能的影响 Russ Cox正则表达式系列 You should not be permitted to write production code if you do not have an journeyman license in regular expressions or floating point math. \u0026ndash; Rob Pike\nRegular Expression Matching Can Be Simple And Fast\n编译器词法分析:正则语言和正则表达式\nGo内存原理与调度模型 正在整理专栏\nBound Checking Elimination Crafting Interpreter 时常看PL和Compiler的基础\ncrafting interpreters\nKosaraju\u0026rsquo;s Algorithm 看William Lin的coding interview觉得用来处理树和图很好，算法4里也有\nHeilmeier问题系列 思考某篇paper的选题\nWhat are you trying to do? Articulate your objectives using absolutely no jargon. How is it done today, and what are the limits of current practice? Who cares? [Support other’s research? Shape research landscape? Power applications in industry?] What\u0026rsquo;s new in your approach and why do you think it will be successful? If you\u0026rsquo;re successful, what difference will it make? [e.g. Contributions in theory/modeling? Improve accuracy by 5% on dataset A, B, C…?] What are the risks and the payoffs? [Further, how would you mitigate the risks? If your proposed method does not work, what could be alternative design? These can end up as discussions such as ablation studies in your paper.] How much will it cost? [e.g. How many GPUs do your experiments require? How long is each training process? How about data storage?] How long will it take? [How many hours are you going to work on this per week? When is the submission DDL? Can you make it?] What are the midterm and final \u0026ldquo;exams\u0026rdquo; to check for success? ","permalink":"https://pillumina.github.io/posts/programming/links/","summary":"\u003cp\u003e\u003ccode\u003e这个post为记录目前正在阅读与研究的section\u003c/code\u003e\u003c/p\u003e\n\u003ch3 id=\"go语言设计\"\u003eGo语言设计\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://draveness.me/golang/\"\u003eGo语言设计与实现\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://golang.design/under-the-hood/\"\u003eGo Under The Hood\u003c/a\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e这两本在写作目的和内容规划都是一致的，不过第二个原本不再维护内容，作者开了下面的新的项目，把撰写原本而积累的与Go相关的资源进行了重新的整理。\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003ca href=\"[golang.design/history]%28https://changkun.de/s/go-history%29\"\u003eGo设计历史\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"pprof对服务端性能影响的研究\"\u003epprof对服务端性能影响的研究\u003c/h2\u003e\n\u003cp\u003e考虑一些极端场景，比如极度追求性能，压榨系统资源以及技术栈必须是Go的业务场景下，是否能自己构建Reactor网络模型\u003c/p\u003e\n\u003ch2 id=\"grpc框架对服务侧性能的影响\"\u003eGRPC框架对服务侧性能的影响\u003c/h2\u003e\n\u003ch2 id=\"russ-cox正则表达式系列\"\u003eRuss Cox正则表达式系列\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003eYou should not be permitted to write production code if you do not have an journeyman license in regular expressions or floating point math. \u0026ndash; Rob Pike\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://swtch.com/~rsc/regexp/regexp1.html\"\u003eRegular Expression Matching Can Be Simple And Fast\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/Ninputer/archive/2011/06/08/2075714.html\"\u003e编译器词法分析:正则语言和正则表达式\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"go内存原理与调度模型\"\u003eGo内存原理与调度模型\u003c/h2\u003e\n\u003cp\u003e正在整理专栏\u003c/p\u003e\n\u003ch2 id=\"bound-checking-elimination\"\u003eBound Checking Elimination\u003c/h2\u003e\n\u003ch2 id=\"crafting-interpreter\"\u003eCrafting Interpreter\u003c/h2\u003e\n\u003cp\u003e时常看PL和Compiler的基础\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://craftinginterpreters.com/contents.html\"\u003ecrafting interpreters\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"kosarajus-algorithm\"\u003eKosaraju\u0026rsquo;s Algorithm\u003c/h2\u003e\n\u003cp\u003e看William Lin的coding interview觉得用来处理树和图很好，算法4里也有\u003c/p\u003e\n\u003ch2 id=\"heilmeier问题系列\"\u003eHeilmeier问题系列\u003c/h2\u003e\n\u003cp\u003e思考某篇paper的选题\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eWhat are you trying to do?\u003c/strong\u003e Articulate your objectives using absolutely no jargon.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHow is it done today, and what are the limits of current practice?\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWho cares?\u003c/strong\u003e [Support other’s research? Shape research landscape? Power applications in industry?]\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhat\u0026rsquo;s new in your approach\u003c/strong\u003e and why do you think it will be successful?\u003c/li\u003e\n\u003cli\u003eIf you\u0026rsquo;re successful, \u003cstrong\u003ewhat difference will it make?\u003c/strong\u003e [e.g. Contributions in theory/modeling? Improve accuracy by 5% on dataset A, B, C…?]\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhat are the risks and the payoffs?\u003c/strong\u003e [Further, how would you mitigate the risks? If your proposed method does not work, what could be alternative design? These can end up as discussions such as ablation studies in your paper.]\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHow much will it cost?\u003c/strong\u003e [e.g. How many GPUs do your experiments require? How long is each training process? How about data storage?]\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHow long will it take?\u003c/strong\u003e [How many hours are you going to work on this per week? When is the submission DDL? Can you make it?]\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWhat are the midterm and final \u0026ldquo;exams\u0026rdquo; to check for success?\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e","title":"书单记录"},{"content":"Preface 本文整理golang编码的单元测试常用示例，以及TDD的简要流程。\n单元测试基础 单元测试文件以_test.go结尾，需要记住以下原则：\n文件名必须是_test.go结尾的，这样在执行go test的时候才会执行到相应的代码 你必须import testing这个包 所有的测试用例函数必须是Test开头 测试用例会按照源代码中写的顺序依次执行 测试函数TestXxx()的参数是testing.T，我们可以使用该类型来记录错误或者是测试状态 测试格式：func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合，但是首字母不能是小写字母[a-z]，例如Testintdiv是错误的函数名。 函数中通过调用testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法，说明测试不通过，调用Log方法用来记录测试的信息。 Table-Driven-Testing 测试讲究 case 覆盖，当我们要覆盖更多 case 时，显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试，标准库中有很多测试是使用这种方式写的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func TestFib(t *testing.T) { var fibTests = []struct { in int // input expected int // expected result }{ {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}, {7, 13}, } for _, tt := range fibTests { actual := Fib(tt.in) if actual != tt.expected { t.Errorf(\u0026#34;Fib(%d) = %d; expected %d\u0026#34;, tt.in, actual, tt.expected) } } } 由于我们使用的是 t.Errorf，即使其中某个 case 失败，也不会终止测试执行。\nT类型 单元测试中，传递给测试函数的参数是 *testing.T 类型。它用于管理测试状态并支持格式化测试日志。测试日志会在执行测试的过程中不断累积，并在测试完成时转储至标准输出。\n当测试函数返回时，或者当测试函数调用 FailNow、 Fatal、Fatalf、SkipNow、Skip、Skipf 中的任意一个时，则宣告该测试函数结束。跟 Parallel 方法一样，以上提到的这些方法只能在运行测试函数的 goroutine 中调用。\n至于其他报告方法，比如 Log 以及 Error 的变种， 则可以在多个 goroutine 中同时进行调用。\n报告方式 上面提到的系列包括方法，带 f 的是格式化的，格式化语法参考 fmt 包。\nT 类型内嵌了 common 类型，common 提供这一系列方法，我们经常会用到的（注意，这里说的测试中断，都是指当前测试函数）：\n1）当我们遇到一个断言错误的时候，标识这个测试失败，会使用到：\nFail : 测试失败，测试继续，也就是之后的代码依然会执行 FailNow : 测试失败，测试中断 在 FailNow 方法实现的内部，是通过调用 runtime.Goexit() 来中断测试的。\n2）当我们遇到一个断言错误，只希望跳过这个错误，但是不希望标识测试失败，会使用到：\nSkipNow : 跳过测试，测试中断 在 SkipNow 方法实现的内部，是通过调用 runtime.Goexit() 来中断测试的。\n3）当我们只希望打印信息，会用到 :\nLog : 输出信息 Logf : 输出格式化的信息 注意：默认情况下，单元测试成功时，它们打印的信息不会输出，可以通过加上 -v 选项，输出这些信息。但对于基准测试，它们总是会被输出。\n4）当我们希望跳过这个测试，并且打印出信息，会用到：\nSkip : 相当于 Log + SkipNow Skipf : 相当于 Logf + SkipNow 5）当我们希望断言失败的时候，标识测试失败，并打印出必要的信息，但是测试继续，会用到：\nError : 相当于 Log + Fail Errorf : 相当于 Logf + Fail 6）当我们希望断言失败的时候，标识测试失败，打印出必要的信息，但中断测试，会用到：\nFatal : 相当于 Log + FailNow Fatalf : 相当于 Logf + FailNow Parallel并行测试 这里简单测试一个对Map的读写并行测试。注意：Parallel方法表示只与其他带有Parallel方法的测试并行进行测试。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var ( data = make(map[string]string) locker sync.RWMutex ) func WriteToMap(k, v string) { locker.Lock() defer locker.Unlock() data[k] = v } func ReadFromMap(k string) string { locker.RLock() defer locker.RUnlock() return data[k] } 测试用例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var pairs = []struct { k string v string }{ {\u0026#34;polaris\u0026#34;, \u0026#34;calvin1\u0026#34;}, {\u0026#34;studygolang\u0026#34;, \u0026#34;oops1\u0026#34;}, {\u0026#34;stdlib\u0026#34;, \u0026#34;go demo1\u0026#34;}, {\u0026#34;polaris1\u0026#34;, \u0026#34;calvin2\u0026#34;}, {\u0026#34;studygolang1\u0026#34;, \u0026#34;oops2\u0026#34;}, {\u0026#34;stdlib1\u0026#34;, \u0026#34;go demo2\u0026#34;}, {\u0026#34;polaris2\u0026#34;, \u0026#34; calvin3\u0026#34;}, } // 注意 TestWriteToMap 需要在 TestReadFromMap 之前 func TestWriteToMap(t *testing.T) { t.Parallel() for _, tt := range pairs { WriteToMap(tt.k, tt.v) } } func TestReadFromMap(t *testing.T) { t.Parallel() for _, tt := range pairs { actual := ReadFromMap(tt.k) if actual != tt.v { t.Errorf(\u0026#34;the value of key(%s) is %s, expected: %s\u0026#34;, tt.k, actual, tt.v) } } } 试验步骤：\n注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码，同时注释掉测试代码中的 t.Parallel，执行测试，测试通过，即使加上 -race，测试依然通过； 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码，执行测试，测试失败（如果未失败，加上 -race 一定会失败）； 如果代码能够进行并行测试，在写测试时，尽量加上 Parallel，这样可以测试出一些可能的问题。\n子测试与子基准测试(Run) Go1.7开始引入的特性，即能够执行嵌套测试，对于过滤执行特性测试用例非常有用。\nT 和 B 的 Run 方法允许定义子单元测试和子基准测试，而不必为它们单独定义函数。这便于创建基于 Table-Driven 的基准测试和层级测试。它还提供了一种共享通用 setup 和 tear-down 代码的方法：\n1 2 3 4 5 6 7 func TestFoo(t *testing.T) { // \u0026lt;setup code\u0026gt; t.Run(\u0026#34;A=1\u0026#34;, func(t *testing.T) { ... }) t.Run(\u0026#34;A=2\u0026#34;, func(t *testing.T) { ... }) t.Run(\u0026#34;B=1\u0026#34;, func(t *testing.T) { ... }) // \u0026lt;tear-down code\u0026gt; } 每个子测试和子基准测试都有一个唯一的名称：由顶层测试的名称与传递给 Run 的名称组成，以斜杠分隔，并具有可选的尾随序列号，用于消除歧义。\n命令行标志 -run 和 -bench 的参数是非固定的正则表达式，用于匹配测试名称。对于由斜杠分隔的测试名称，例如子测试的名称，它名称本身即可作为参数，依次匹配由斜杠分隔的每部分名称。因为参数是非固定的，一个空的表达式匹配任何字符串，所以下述例子中的 “匹配” 意味着 “顶层/子测试名称包含有”：\ngo test -run \u0026#39;\u0026#39; # 执行所有测试。 go test -run Foo # 执行匹配 \u0026#34;Foo\u0026#34; 的顶层测试，例如 \u0026#34;TestFooBar\u0026#34;。 go test -run Foo/A= # 对于匹配 \u0026#34;Foo\u0026#34; 的顶层测试，执行其匹配 \u0026#34;A=\u0026#34; 的子测试。 go test -run /A=1 # 执行所有匹配 \u0026#34;A=1\u0026#34; 的子测试。 子测试也可用于程序并行控制。只有子测试全部执行完毕后，父测试才会完成。在下述例子中，所有子测试之间并行运行，此处的 “并行” 只限于这些子测试之间，并不影响定义在其他顶层测试中的子测试：\n1 2 3 4 5 6 7 8 9 func TestGroupedParallel(t *testing.T) { for _, tc := range tests { tc := tc // capture range variable t.Run(tc.Name, func(t *testing.T) { t.Parallel() ... }) } } 在所有子测试并行运行完毕之前，Run 方法不会返回。下述例子提供了一种方法，用于在子测试并行运行完毕后清理资源：\n1 2 3 4 5 6 7 8 9 func TestTeardownParallel(t *testing.T) { // This Run will not return until the parallel tests finish. t.Run(\u0026#34;group\u0026#34;, func(t *testing.T) { t.Run(\u0026#34;Test1\u0026#34;, parallelTest1) t.Run(\u0026#34;Test2\u0026#34;, parallelTest2) t.Run(\u0026#34;Test3\u0026#34;, parallelTest3) }) // \u0026lt;tear-down code\u0026gt; } Test Coverage 测试覆盖率，这里讨论的是基于代码的测试覆盖率。\nGo 从 1.2 开始，引入了对测试覆盖率的支持，使用的是与 cover 相关的工具（go test -cover、go tool cover）。虽然 testing 包提供了 cover 相关函数，不过它们是给 cover 的工具使用的。\n关于测试覆盖率的更多信息，可以参考官方的博文：The cover story\ngotest变量(参考) gotest 的变量有这些：\ntest.short : 一个快速测试的标记，在测试用例中可以使用 testing.Short() 来绕开一些测试 test.outputdir : 输出目录 test.coverprofile : 测试覆盖率参数，指定输出文件 test.run : 指定正则来运行某个 / 某些测试用例 test.memprofile : 内存分析参数，指定输出文件 test.memprofilerate : 内存分析参数，内存分析的抽样率 test.cpuprofile : cpu 分析输出参数，为空则不做 cpu 分析 test.blockprofile : 阻塞事件的分析参数，指定输出文件 test.blockprofilerate : 阻塞事件的分析参数，指定抽样频率 test.timeout : 超时时间 test.cpu : 指定 cpu 数量 test.parallel : 指定运行测试用例的并行数 gotest结构体(参考) B : 压力测试 BenchmarkResult : 压力测试结果 Cover : 代码覆盖率相关结构体 CoverBlock : 代码覆盖率相关结构体 InternalBenchmark : 内部使用的结构体 InternalExample : 内部使用的结构体 InternalTest : 内部使用的结构体 M : main 测试使用的结构体 PB : Parallel benchmarks 并行测试使用的结构体 T : 普通测试用例 TB : 测试用例的接口 压力测试基础 压测检测函数(方法)的性能，和编写UT类似，所以不再赘述，但需要注意以下几点：\n压力测试用例必须遵循如下格式，其中XXX可以是任意字母数字的组合，但是首字母不能是小写字母 func BenchmarkXXX(b *testing.B) { ... } go test不会默认执行压力测试的函数，如果要执行压力测试需要带上参数-test.bench，语法:-test.bench=\u0026quot;test_name_regex\u0026quot;,例如go test -test.bench=\u0026quot;.*\u0026quot;表示测试全部的压力测试函数 在压力测试用例中,请记得在循环体内使用testing.B.N,以使测试可以正常的运行 文件名也必须以_test.go结尾 下面是一个压测的例子，测试除法函数的性能：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package gotest import ( \u0026#34;testing\u0026#34; ) func Benchmark_Division(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { //use b.N for looping Division(4, 5) } } func Benchmark_TimeConsumingFunction(b *testing.B) { b.StopTimer() //调用该函数停止压力测试的时间计数 //做一些初始化的工作,例如读取文件数据,数据库连接之类的, //这样这些时间不影响我们测试函数本身的性能 b.StartTimer() //重新开始时间 for i := 0; i \u0026lt; b.N; i++ { Division(4, 5) } } 我们执行命令go test webbench_test.go -test.bench=\u0026quot;.*\u0026quot;，可以看到如下结果：\nBenchmark_Division-4 500000000\t7.76 ns/op\t456 B/op\t14 allocs/op Benchmark_TimeConsumingFunction-4 500000000\t7.80 ns/op\t224 B/op\t4 allocs/op PASS ok gotest\t9.364s 上面的结果显示我们没有执行任何TestXXX的单元测试函数，显示的结果只执行了压力测试函数，第一条显示了Benchmark_Division执行了500000000次，每次的执行平均时间是7.76纳秒，第二条显示了Benchmark_TimeConsumingFunction执行了500000000，每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。\n性能测试进阶(benchstat) sync.Map优化例子 在sync.Map中存储一个值，然后再并发删除该值:\n1 2 3 4 5 6 7 8 func BenchmarkDeleteCollision(b *testing.B){ benchMap(b, bench{ setup: func(_ *testing.B, m mapInterface){m,LoadOrStore(0, 0)}, perG: func(b *testing.B, pb *testing.PB, i int, m mapInterface){ for; pb.Next(); i++ {m.Delete(0)} } }) } 优化 src/sync/map.go 275 -delete(m.dirty, key) 275 +e, ok = m.dirty[key] 276 +m.misslocked() $ git stash $ git test -run=none -bench=BenchmarkDeleteCollision -count=20 | tee old.txt $ git stash pop $ git test -run=none -bench=BenchmarkDeleteCollision -count=20 | tee new.txt $ benchstat old.txt new.ext 编译器优化例子 查看编译器优化，测试函数被编译成了什么\n1 2 3 4 5 6 7 8 9 10 11 12 13 package compile func comp1(s1, s2 []byte)bool{ return string(s1) == string(s2) } func comp2(s1, s2 []byte)bool{ return conv(s1) == conv(s2) } func conv(s []byte) string{ return string(s) } $GOSSAFUNC=com1 go build // 会生成ssa.html，open它即可看到comp1函数编译后的代码 假设性检验 统计是一套在总体分布函数完全未知或者只知道形式、不知道参数的情况下，为了由样本推断总体的某些未知特性，形成的一套方法论。 多次抽样：对同一个性能基准测试运行多次，根据中心极限定理，如果理论均值存在，则抽样噪声服从正态分布。 当重复执行完某个性能基准测试后，benchstat先帮我们剔除掉了一些异常值，我们得到了关于某段代码在可控的环境条件E下的性能分布的一组样本。 T检验：参数检验，假设数据服从正态分布，且方差相同 (最严格) Welch T检验(ttest)： 参数检验，假设服从正态分布，但方差不一定相同 Mann-Whitney U检验(utest， benchstat的default): 非参数检验，假设最少，最通用，值假设两组样本来自于同一个总体（例如两个性能测试是否在同一个机器跑的），只有均值的差异。当对数据的假设减少时，结论的不确定性增大，p值会因此增大，进而使得性能基准测试的条件更加严格。 局限和应对 perflock降低系统噪音，作用是限制CPU时钟频率，从而一定程度上消除系统对性能测试程序的影响，仅支持Linux。\n$ go get github.com/aclements/perflock/cmd/perflock $ sudo install $GOPATH/bin/perflock /usr/bin/perflock $ sudo -b perflock -daemon $ perflock $ perflock -governer 70% go test -test=none -bench=. Mocking GoMock GoMock为很常用的测试mock框架，虽然我自己不常用:0（因为我自身并不非常喜欢mock), 并且对在生产开发环境使用mock有点意见，代码增长（和Injection类似），以及如果不单独部署一个mock server很多修改并不能很好得share。\n虽然如此，这里还是记录一下GoMock的quick start。\nInstall 首先就是安装gomock包，以及mockgen代码生成工具，后者其实并不是必要的，但是如果没有自己就要写一个容易出错并且繁琐的mock代码。\ngo get github.com/golang/mock/gomock go get github.com/golang/mock/mockgen 检查一下有没有成功，会打印一些使用帮助信息:\n$GOPATH/bin/mockgen 基本使用 基本上使用gomock遵循以下几个步骤：\n使用mockgen去对你想要mock的interface生成mock对象 在测试代码中，创建一个gomock.Controller实例，并且将其传入mock对象的constructor中获取一个mock对象 在你的mock中调用EXPECT()去设置测试期望以及返回值 在mock controller调用FINISH()去设置进行mock期望的assert（断言） 下面记录一个小的demo展示上述的workflow，为了让展示简单，我们可以只是聚焦两个文件- 一个接口文件doer.go中的Doer接口（希望mock的），以及user.go文件中的结构体User，这个接口体用到了Doer接口。\ndoer.go：\n1 2 3 4 5 package doer type Doer interface { DoSomething(int, string) error } user.go\n1 2 3 4 5 6 7 8 9 10 11 package user import \u0026#34;github.com/sgreben/testing-with-gomock/doer\u0026#34; type User struct { Doer doer.Doer } func (u *User) Use() error { return u.Doer.DoSomething(123, \u0026#34;Hello GoMock\u0026#34;) } 下面是project的layout：\n\u0026#39;-- doer \u0026#39;-- doer.go \u0026#39;-- user \u0026#39;-- user.go 我们接下来要在mocks文件夹内添加Doer的mock，并且新增一个user_test.go文件：\n1 2 3 4 5 6 7 \u0026#39;-- doer \u0026#39;-- doer.go \u0026#39;-- mocks \u0026#39;-- mock_doer.go \u0026#39;-- user \u0026#39;-- user.go \u0026#39;-- user_test.go 为了生成这个mock_doer.go，我们创建mocks目录后调用：\nmockgen -destination=mocks/mock_doer.go -package=mocks github.com/sgreben/testing-with-gomock/doer Doer 这里的mockgen传入以下几个参数:\n-destination=mocks/mock_doer.go 目标路径 -package=mocks：在mockspackage内生成mocks github.com/sgreben/testing-with-gomock/doer： 为这个package生成mocks (包名而已，根据实际情况定) Doer: 为这个interface生成mocks，如果想要mock多个接口，可以传入以逗号分隔的列表Doer1,Doer2，对接口的声明必须清楚。 注意如果$GOPATH/bin不在$PATH中，mockgen要改成$GOPATH/bin/mockgen\n最终mockgen会生成mock_doer.go这个文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // Code generated by MockGen. DO NOT EDIT. // Source: github.com/sgreben/testing-with-gomock/doer (interfaces: Doer) package mocks import ( gomock \u0026#34;github.com/golang/mock/gomock\u0026#34; ) // MockDoer is a mock of Doer interface type MockDoer struct { ctrl *gomock.Controller recorder *MockDoerMockRecorder } // MockDoerMockRecorder is the mock recorder for MockDoer type MockDoerMockRecorder struct { mock *MockDoer } // NewMockDoer creates a new mock instance func NewMockDoer(ctrl *gomock.Controller) *MockDoer { mock := \u0026amp;MockDoer{ctrl: ctrl} mock.recorder = \u0026amp;MockDoerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use func (_m *MockDoer) EXPECT() *MockDoerMockRecorder { return _m.recorder } // DoSomething mocks base method func (_m *MockDoer) DoSomething(_param0 int, _param1 string) error { ret := _m.ctrl.Call(_m, \u0026#34;DoSomething\u0026#34;, _param0, _param1) ret0, _ := ret[0].(error) return ret0 } // DoSomething indicates an expected call of DoSomething func (_mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, \u0026#34;DoSomething\u0026#34;, arg0, arg1) } 浏览一下代码，可以看到生成的EXPECT()方法和mock接口的方法在一个层级，这里是DoSomething，因为要避免名字冲突，所以这里把EXPECT定义成全大写。\n下面，我们在测试中创建一个mock controller。 mock controller的作用是跟踪以及对相关mocks对象的进行期望断言(asserting the expectations)。\n创建controller的方法就是，传入构建函数代表*testing.T的t，而后将其作为参数传入Doermock对象的构建函数:\n1 2 3 4 mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockDoer := mocks.NewMockDoer(mockCtrl) 上述对Finish的defer后面再说。\n假设我们想要断言mockerDoer的Do方法将会被调用一次，传入123以及Hello GoMock作为参数并且返回nil。\n为了实现这个断言，我们在mockDoer对象上调用EXPECT()设置期望。EXPECT()其实返回的是一个mock recorder的对象，它包含了真实对象的所有同名方法。\n我们能够进行如下的链式调用:\n1 mockDoer.EXPECT().DoSomething(123, \u0026#34;Hello GoMock\u0026#34;).Return(nil).Times(1) 从这个调用其实你也能理解每个的意义，如果要设置方法被调用的次数，除了上述的Times(number)，还有诸如MaxTimes(number)以及MinTimes(numbers)这种显性的限制。\n看上去差不多了，接下来写一个完整的user_test.go`:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package user_test import ( \u0026#34;github.com/sgreben/testing-with-gomock/mocks\u0026#34; \u0026#34;github.com/sgreben/testing-with-gomock/user\u0026#34; ) func TestUse(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockDoer := mocks.NewMockDoer(mockCtrl) testUser := \u0026amp;user.User{Doer:mockDoer} // Expect Do to be called once with 123 and \u0026#34;Hello GoMock\u0026#34; as parameters, and return nil from the mocked call. mockDoer.EXPECT().DoSomething(123, \u0026#34;Hello GoMock\u0026#34;).Return(nil).Times(1) testUser.Use() } 可能这个代码里对mock期望的断言并不明显，断言发生在defer掉的Finish()。相当于对Finish的调用发生在mock controller的声明的时候 - 这样我们不会忘记在后面加上期望断言。\n最后跑一下测试:\n$ go test -v github.com/sgreben/testing-with-gomock/user === RUN TestUse --- PASS: TestUse (0.00s) PASS ok github.com/sgreben/testing-with-gomock/user 0.007s 当然如果你想构建多个mock对象，你可以对mock controller进行复用，它的Finish相当于会发生在所有和controller关联的mock对象的期望断言被设置之后。\n我们也可以测试一下mock方法的返回值，这里改写一下测试返回一个dummyError：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func TestUseReturnsErrorFromDo(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() dummyError := errors.New(\u0026#34;dummy error\u0026#34;) mockDoer := mocks.NewMockDoer(mockCtrl) testUser := \u0026amp;user.User{Doer:mockDoer} // Expect Do to be called once with 123 and \u0026#34;Hello GoMock\u0026#34; as parameters, and return dummyError from the mocked call. mockDoer.EXPECT().DoSomething(123, \u0026#34;Hello GoMock\u0026#34;).Return(dummyError).Times(1) err := testUser.Use() if err != dummyError { t.Fail() } } 通过go:generate使用GoMock 有些人可能发现一个workflow的问题，如果对每个package以及interface都用mockgen肯定是非常繁琐的，特别是如果我们开发的项目有大量的接口和包定义。为了解决这个问题，mockgen命令行能够被特殊的go:generate注释去替代。\n比如，在我们的例子里，我们能够在doer.go的package声明下面添加注释:\n1 2 3 4 5 6 7 package doer //go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks github.com/sgreben/testing-with-gomock/doer Doer type Doer interface { DoSomething(int, string) error } 但是这种写法也有个问题，因为代码文件目录和mocks目录的不一致，导致我们需要添加../mocks类似的路径而不是简单的mocks/，我们可以在项目的根路径下生成所有mocks:\ngo generate ./... 写法上注意代码里//和go:generate之间没有空格。\n对于添加go:generate注释的原则以及一些mock的构建命名原则如下:\n每个包含需要mock的interfaces的文件中添加一个go:generate注释 如果要用mockgen要传入清晰的interface名 把mock文件放在mocks包下，名称改写X.go到mocks/mock_X.go 使用参数匹配器 有些情况下，你对mock中的特定参数不太关心，当然我们可以清楚地固定参数，也可以用参数匹配器去匹配参数，我们称之为Matcher，熟悉Ginkgo框架的同学应该很清楚。\nGoMock中预设了几个matchers：\ngomock.Any()： 匹配所有类型、所有值 gomock.Eq(x): 使用反射去匹配任何与x为DeepEqual的值 gomock.Nil()： 匹配nil gomock.Not(m): 这里m是一个Matcher，也就是匹配所有没有被m匹配的值 gomock.Not(x): 这里x不是一个Matcher，匹配所有与x不DeepEqual的值 举个例子，如果我们不关心Do方法的第一个参数:\n1 mockDoer.EXPECT().DoSomething(gomock.Any(), \u0026#34;Hello GoMock\u0026#34;) GoMock会自动把非匹配类型的参数转化为Eq匹配器：\n1 mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq(\u0026#34;Hello GoMock\u0026#34;)) 当然我们也可以自定义Matchers，实现接口就行, gomock/matchers.go :\n1 2 3 4 type Matcher interface { Matches(x interface{}) bool String() string } 这里的Matches方法是实例匹配发生的地方，String方法针对测试失败时生成human-readable的信息，我们可以自己写一个matcher去检查参数类型：\nmatch/oftype.go\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package match import ( \u0026#34;reflect\u0026#34; \u0026#34;github.com/golang/mock/gomock\u0026#34; ) type ofType struct{ t string } func OfType(t string) gomock.Matcher { return \u0026amp;ofType{t} } func (o *ofType) Matches(x interface{}) bool { return reflect.TypeOf(x).String() == o.t } func (o *ofType) String() string { return \u0026#34;is of type \u0026#34; + o.t } 然后我们就可以使用我们的matcher:\n1 2 3 4 5 // Expect Do to be called once with 123 and any string as parameters, and return nil from the mocked call. mockDoer.EXPECT(). DoSomething(123, match.OfType(\u0026#34;string\u0026#34;)). Return(nil). Times(1) 注意下上述我们分行写，要把.写在行末尾，不然编译器会报错。\n断言调用顺序 对一个对象的调用顺序也是很重要的，GoMock提供了.After方法显式地定义一个方法必须在另一个方法后面被调用:\n1 2 3 callFirst := mockDoer.EXPECT().DoSomething(1, \u0026#34;first this\u0026#34;) callA := mockDoer.EXPECT().DoSomething(2, \u0026#34;then this\u0026#34;).After(callFirst) callB := mockDoer.EXPECT().DoSomething(2, \u0026#34;or this\u0026#34;).After(callFirst) 这个代码都能理解。\n此外还提供了一个更直观的手段去定义断言顺序，也就是gomock.InOrder，这种写法更容易阅读:\n1 2 3 4 5 6 gomock.InOrder( mockDoer.EXPECT().DoSomething(1, \u0026#34;first this\u0026#34;), mockDoer.EXPECT().DoSomething(2, \u0026#34;then this\u0026#34;), mockDoer.EXPECT().DoSomething(3, \u0026#34;then this\u0026#34;), mockDoer.EXPECT().DoSomething(4, \u0026#34;finally this\u0026#34;), ) 定义mock的actions 本质上就是mock其实不会执行其他行为，我们可以人为使用.Do方法，并且传入调用的函数，意味着如果调用的参数匹配上了，就会执行.Do提供的函数：\n1 2 3 4 5 6 mockDoer.EXPECT(). DoSomething(gomock.Any(), gomock.Any()). Return(nil). Do(func(x int, y string) { fmt.Println(\u0026#34;Called with x =\u0026#34;,x,\u0026#34;and y =\u0026#34;, y) }) 一些复杂的动作，比如下面这个例子，DoSomething方法的第一个int参数应该小于或者等于第二个string参数的长度:\n1 2 3 4 5 6 7 8 mockDoer.EXPECT(). DoSomething(gomock.Any(), gomock.Any()). Return(nil). Do(func(x int, y string) { if x \u0026gt; len(y) { t.Fail() } }) 这种写法不能通过自定义matcher实现，因为我们关联了多个具体的值，而matcher每次只能访问一个参数。\nsql-mock(GORM) 常规的database/sql/driver的接口mocking可以用GoMock，但是像gorm之类的ORM框架就很难用常规的mock方法，以为有其他很多额外的苦力活。sql-mock的介绍为Sql mock driver for golang to test database interactions. 可以帮助解决这个问题。\n下面用BDD框架Ginkgo写测试用例，展示一个如何使用Sqlmock去测试一个简单blog应用的例子，这个例子的后端为pg并且使用了gorm。\n源码\n定义GORM数据模型与Repository 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // modle.go import \u0026#34;github.com/lib/pq\u0026#34; ... type Blog struct { ID uint Title string Content string Tags pq.StringArray // string array for tags CreatedAt time.Time } // repository.go import \u0026#34;github.com/jinzhu/gorm\u0026#34; ... type Repository struct { db *gorm.DB } func (p *Repository) ListAll() ([]*Blog, error) { var l []*Blog err := p.db.Find(\u0026amp;l).Error return l, err } func (p *Repository) Load(id uint) (*Blog, error) { blog := \u0026amp;Blog{} err := p.db.Where(`id = ?`, id).First(blog).Error return blog, err } ... Repository结构非常简单，有着*gorm.DB字段，所有的DB操作依赖于此。这里为了简洁把一些多余的代码省略了。除了Load、ListAll当然还有类似Save、Delete、SearchByTitle等方法。\n单元测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import ( ... . \u0026#34;github.com/onsi/ginkgo\u0026#34; . \u0026#34;github.com/onsi/gomega\u0026#34; \u0026#34;github.com/DATA-DOG/go-sqlmock\u0026#34; \u0026#34;github.com/jinzhu/gorm\u0026#34; ) var _ = Describe(\u0026#34;Repository\u0026#34;, func() { var repository *Repository var mock sqlmock.Sqlmock BeforeEach(func() { var db *sql.DB var err error db, mock, err = sqlmock.New() // mock sql.DB Expect(err).ShouldNot(HaveOccurred()) gdb, err := gorm.Open(\u0026#34;postgres\u0026#34;, db) // open gorm db Expect(err).ShouldNot(HaveOccurred()) repository = \u0026amp;Repository{db: gdb} }) AfterEach(func() { err := mock.ExpectationsWereMet() // make sure all expectations were met Expect(err).ShouldNot(HaveOccurred()) }) It(\u0026#34;test something\u0026#34;, func(){ ... }) }) 如果读者对Ginkgo的测试语法表示不熟悉的，可以去参阅posts里的BDD相关章节。在这里，BeforeEach中做一些测试初始化，例如Repository的实例化等。在AfterEach中加入各种断言。\nBeforeEach中的初始化分为几个步骤：\n创建*sql.DB的mock实例，利用sqlmock.New()创建mock控制器。 gorm.Open(\u0026quot;postgres\u0026quot;, db)使用GORM。 创建Repository实例。 在AfterEach中，我们使用mock.ExpectationsWereMet()确保所有的期望都被满足。\n测试ListAll方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // repository.go ... func (p *Repository) ListAll() ([]*Blog, error) { var l []*Blog err := p.db.Find(\u0026amp;l).Error return l, err } ... // repository_test.go ... Context(\u0026#34;list all\u0026#34;, func() { It(\u0026#34;empty\u0026#34;, func() { const sqlSelectAll = `SELECT * FROM \u0026#34;blogs\u0026#34;` mock.ExpectQuery(sqlSelectAll). WillReturnRows(sqlmock.NewRows(nil)) l, err := repository.ListAll() Expect(err).ShouldNot(HaveOccurred()) Expect(l).Should(BeEmpty()) }) }) ... 上述snippet中，ListAll找到DB中的所有记录，并map到*Blog的切片中。测试语句非常直观，我们设置了该查询语句返回的是nil，也就是空集合。跑一下测试：\n➜ ginkgo Running Suite: Pg Suite ======================= Random Seed: 1585542357 Will run 8 of 8 specs (/Users/dche423/dbtest/pg/repository.go:24) [2020-03-30 12:26:01] Query: could not match actual sql: \u0026#34;SELECT * FROM \u0026#34;blogs\u0026#34;\u0026#34; with expected regexp \u0026#34;SELECT * FROM \u0026#34;blogs\u0026#34;\u0026#34; • Failure [0.001 seconds] Repository /Users/dche423/dbtest/pg/repository_test.go:16 list all /Users/dche423/dbtest/pg/repository_test.go:37 empty [It] /Users/dche423/dbtest/pg/repository_test.go:38 ... Test Suite Failed ➜ 测试失败了\u0026hellip;不过回显可以知道信息: could not match actual sql with expected regexp.。实际上Sqlmock使用sqlmock.QueryMatcherRegex为默认的SQL匹配器。在这个例子中，sqlmock.ExpectQuery输入一个正则表达式字符串而不是一个SQL的文本。所以我们有两种方式去解决这个问题:\n使用regexp.QuoteMeta， 也就是mock.ExpectQuery(regexp.QuoteMeta(sqlSelectAll)) 更改默认的SQL匹配器，当我们在创建mock实例的时候可以配置: sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 其实一般来说，正则表达式匹配器能更灵活一些。\n测试Load方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // repository.go func (p *Repository) Load(id uint) (*Blog, error) { blog := \u0026amp;Blog{} err := p.db.Where(`id = ?`, id).First(blog).Error return blog, err } ... // repository_test.go Context(\u0026#34;load\u0026#34;, func() { It(\u0026#34;found\u0026#34;, func() { blog := \u0026amp;Blog{ ID: 1, Title: \u0026#34;post\u0026#34;, ... } rows := sqlmock. NewRows([]string{\u0026#34;id\u0026#34;, \u0026#34;title\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;tags\u0026#34;, \u0026#34;created_at\u0026#34;}). AddRow(blog.ID, blog.Title, blog.Content, blog.Tags, blog.CreatedAt) const sqlSelectOne = `SELECT * FROM \u0026#34;blogs\u0026#34; WHERE (id = $1) ORDER BY \u0026#34;blogs\u0026#34;.\u0026#34;id\u0026#34; ASC LIMIT 1` mock.ExpectQuery(regexp.QuoteMeta(sqlSelectOne)).WithArgs(blog.ID).WillReturnRows(rows) dbBlog, err := repository.Load(blog.ID) Expect(err).ShouldNot(HaveOccurred()) Expect(dbBlog).Should(Equal(blog)) }) It(\u0026#34;not found\u0026#34;, func() { // ignore sql match mock.ExpectQuery(`.+`).WillReturnRows(sqlmock.NewRows(nil)) _, err := repository.Load(1) Expect(err).Should(Equal(gorm.ErrRecordNotFound)) }) }) ... Load方法输入一个blog id作为参数，找到这个id对应的第一条记录。\n我们测试两种场景:\n名为found的场景，我们创建blog实例并将其转换为sql.Row。随后调用ExpectQuery定义期望，在语句的最后，我们断言loaded blog实例和原来的一样。 注意：如果你不清楚GORM使用的是什么SQL，可以打开debug flag \u0026ndash; gorm.DB的Debug() 名为not found的场景，这里使用正则匹配来简化，表示不管什么sql都返回空。这里我们期望的是当找不到对应的blog时候，gorm.ErrRecordNotFound会被抛出。 测试Save方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // repository.go ... func (p *Repository) Save(blog *Blog) error { return p.db.Save(blog).Error } // repository_test.go ... Context(\u0026#34;save\u0026#34;, func() { var blog *Blog BeforeEach(func() { blog = \u0026amp;Blog{ Title: \u0026#34;post\u0026#34;, Content: \u0026#34;hello\u0026#34;, Tags: pq.StringArray{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;}, CreatedAt: time.Now(), } }) It(\u0026#34;insert\u0026#34;, func() { // gorm use query instead of exec // https://github.com/DATA-DOG/go-sqlmock/issues/118 const sqlInsert = ` INSERT INTO \u0026#34;blogs\u0026#34; (\u0026#34;title\u0026#34;,\u0026#34;content\u0026#34;,\u0026#34;tags\u0026#34;,\u0026#34;created_at\u0026#34;) VALUES ($1,$2,$3,$4) RETURNING \u0026#34;blogs\u0026#34;.\u0026#34;id\u0026#34;` const newId = 1 mock.ExpectBegin() // begin transaction mock.ExpectQuery(regexp.QuoteMeta(sqlInsert)). WithArgs(blog.Title, blog.Content, blog.Tags, blog.CreatedAt). WillReturnRows(sqlmock.NewRows([]string{\u0026#34;id\u0026#34;}).AddRow(newId)) mock.ExpectCommit() // commit transaction Expect(blog.ID).Should(BeZero()) err := repository.Save(blog) Expect(err).ShouldNot(HaveOccurred()) Expect(blog.ID).Should(BeEquivalentTo(newId)) }) It(\u0026#34;update\u0026#34;, func() { ...\t}) }) 当data模型有已有的主键，Save方法能够更新DB记录；反之则插入一条新的记录。上面的snippet表现的插入的测试。\n创建一个新的blog实例，并且不给其设置主键。而后定义mock.ExpectQuery。在Query开始前begin一个事务，在之后commit。一般情况下，非查询语句(Insert/Update)应该被mock.ExepectExec定义，但是这个是个特殊场景。因为某些原因，对于pg的语法，GORM使用QueryRow而非Exec。\n最后，使用Expect(blog.ID).Should(BeEquivalentTo(newId)) 来断言blog.ID在Save方法调用之后被设置了。其实一般来说，不太需要去对简单的Insert/Update语句进行单元测试，但是这里只是对一些GORM会进行的一些特殊场景进行说明，像其他的后端场景不用太多关注。\n依赖注入 Test Driven Development TDD Reference\nchannel TDD 过程 目标 目标： 写一个 CheckWebsites 的函数检查 URL 列表的状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { results[url] = wc(url) } return results } 它返回一个 map，由每个 url 检查后的得到的布尔值组成，成功响应的值为 true，错误响应的值为 false。\n你还必须传入一个 WebsiteChecker 处理单个 URL 并返回一个布尔值。它会被函数调用以检查所有的网站。\n使用 依赖注入，允许在不发起真实 HTTP 请求的情况下测试函数，这使测试变得可靠和快速。\n下面是简单的测试：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package concurrency import ( \u0026#34;reflect\u0026#34; \u0026#34;testing\u0026#34; ) func mockWebsiteChecker(url string) bool { if url == \u0026#34;waat://furhurterwe.geds\u0026#34; { return false } return true } func TestCheckWebsites(t *testing.T) { websites := []string{ \u0026#34;http://google.com\u0026#34;, \u0026#34;http://blog.gypsydave5.com\u0026#34;, \u0026#34;waat://furhurterwe.geds\u0026#34;, } actualResults := CheckWebsites(mockWebsiteChecker, websites) want := len(websites) got := len(actualResults) if want != got { t.Fatalf(\u0026#34;Wanted %v, got %v\u0026#34;, want, got) } expectedResults := map[string]bool{ \u0026#34;http://google.com\u0026#34;: true, \u0026#34;http://blog.gypsydave5.com\u0026#34;: true, \u0026#34;waat://furhurterwe.geds\u0026#34;: false, } if !reflect.DeepEqual(expectedResults, actualResults) { t.Fatalf(\u0026#34;Wanted %v, got %v\u0026#34;, expectedResults, actualResults) } } 该功能在生产环境中被用于检查数百个网站。但是它速度很慢，所以需要为程序提速。\n写一个测试 首先我们对 CheckWebsites 做一个基准测试，这样就能看到我们修改的影响。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package concurrency import ( \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) func slowStubWebsiteChecker(_ string) bool { time.Sleep(20 * time.Millisecond) return true } func BenchmarkCheckWebsites(b *testing.B) { urls := make([]string, 100) for i := 0; i \u0026lt; len(urls); i++ { urls[i] = \u0026#34;a url\u0026#34; } for i := 0; i \u0026lt; b.N; i++ { CheckWebsites(slowStubWebsiteChecker, urls) } } 基准测试使用一百个网址的 slice 对 CheckWebsites 进行测试，并使用 WebsiteChecker 的伪造实现。slowStubWebsiteChecker 故意放慢速度。它使用 time.Sleep 明确等待 20 毫秒，然后返回 true。\n当我们运行基准测试时使用 go test -bench=. 命令 (如果在 Windows Powershell 环境下使用 go test -bench=\u0026quot;.\u0026quot;)：\npkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v0 BenchmarkCheckWebsites-4 1 2249228637 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v0 2.268s CheckWebsite 经过基准测试的时间为 2249228637 纳秒，大约 2.25 秒。\n让我们尝试去让它运行得更快。\n编写足够的代码让它通过 现在我们终于可以谈论并发了，以下内容是为了说明「不止一件事情正在进行中」。这是我们每天很自然在做的事情。\n比如，今天早上我泡了一杯茶。我放上水壶，然后在等待它煮沸时，从冰箱里取出了牛奶，把茶从柜子里拿出来，找到我最喜欢的杯子，把茶袋放进杯子里，然后等水壶沸了，把水倒进杯子里。\n我 没有 做的事情是放上水壶，然后呆呆地盯着水壶等水煮沸，然后在煮沸后再做其他事情。\n如果你能理解为什么第一种方式泡茶更快，那你就可以理解我们如何让 CheckWebsites 变得更快。与其等待网站响应之后再发送下一个网站的请求，不如告诉计算机在等待时就发起下一个请求。\n通常在 Go 中，当调用函数 doSomething() 时，我们等待它返回（即使它没有值返回，我们仍然等待它完成）。我们说这个操作是 阻塞 的 —— 它让我们等待它完成。Go 中不会阻塞的操作将在称为 goroutine 的单独 进程 中运行。将程序想象成从上到下读 Go 的 代码，当函数被调用执行读取操作时，进入每个函数「内部」。当一个单独的进程开始时，就像开启另一个 reader（阅读程序）在函数内部执行读取操作，原来的 reader 继续向下读取 Go 代码。\n要告诉 Go 开始一个新的 goroutine，我们把一个函数调用变成 go 声明，通过把关键字 go 放在它前面：go doSomething()。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } return results } 因为开启 goroutine 的唯一方法就是将 go 放在函数调用前面，所以当我们想要启动 goroutine 时，我们经常使用 匿名函数（anonymous functions）。一个匿名函数文字看起来和正常函数声明一样，但没有名字（意料之中）。你可以在 上面的 for 循环体中看到一个。\n匿名函数有许多有用的特性，其中两个上面正在使用。首先，它们可以在声明的同时执行 —— 这就是匿名函数末尾的 () 实现的。其次，它们维护对其所定义的词汇作用域的访问权 —— 在声明匿名函数时所有可用的变量也可在函数体内使用。\n上面匿名函数的主体和之前循环体中的完全一样。唯一的区别是循环的每次迭代都会启动一个新的 goroutine，与当前进程（WebsiteChecker 函数）同时发生，每个循环都会将结果添加到 results map 中。\n但是当我们执行 go test：\n-------- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s 不可预知的问题 你可能不会得到这个结果。你可能会得到一个 panic 信息，这个稍后再谈。如果你得到的是那些结果，不要担心，只要继续运行测试，直到你得到上述结果。或假装你得到了，这取决于你。欢迎来到并发编程的世界：如果处理不正确，很难预测会发生什么。别担心 —— 这就是我们编写测试的原因，当处理并发时，测试帮助我们预测可能发生的情况。\n让我们困惑的是，原来的测试 WebsiteChecker 现在返回空的 map。哪里出问题了？\n我们 for 循环开始的 goroutines 没有足够的时间将结果添加结果到 results map 中；WebsiteChecker 函数对于它们来说太快了，以至于它返回时仍为空的 map。\n为了解决这个问题，我们可以等待所有的 goroutine 完成他们的工作，然后返回。两秒钟应该能完成了，对吧？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package concurrency import \u0026#34;time\u0026#34; type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } time.Sleep(2 * time.Second) return results } 现在当我们运行测试时获得的结果（如果没有得到 —— 参考上面的做法）：\n-------- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[waat://furhurterwe.geds:false] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s 这不是很好 - 为什么只有一个结果？我们可以尝试通过增加等待的时间来解决这个问题 —— 如果你愿意，可以试试。但没什么作用。这里的问题是变量 url 被重复用于 for 循环的每次迭代 —— 每次都会从 urls 获取新值。但是我们的每个 goroutine 都是 url 变量的引用 —— 它们没有自己的独立副本。所以他们 都 会写入在迭代结束时的 url —— 最后一个 url。这就是为什么我们得到的结果是最后一个 url \u0026mdash;- 注意：闭包情况下的引用关系一直是需要注意的\n解决这个问题:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import ( \u0026#34;time\u0026#34; ) type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func(u string) { results[u] = wc(u) }(url) } time.Sleep(2 * time.Second) return results } 通过给每个匿名函数一个参数 url(u)，然后用 url 作为参数调用匿名函数，我们确保 u 的值固定为循环迭代的 url 值，重新启动 goroutine。u 是 url 值的副本，因此无法更改。\n现在，如果你幸运的话，你会得到：\nPASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v1 2.012s 但是，如果你不走运（如果你运行基准测试，这很可能会发生，因为你将发起多次的尝试）。\nfatal error: concurrent map writes goroutine 8 [running]: runtime.throw(0x12c5895, 0x15) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/panic.go:605 +0x95 fp=0xc420037700 sp=0xc4200376e0 pc=0x102d395 runtime.mapassign_faststr(0x1271d80, 0xc42007acf0, 0x12c6634, 0x17, 0x0) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:783 +0x4f5 fp=0xc420037780 sp=0xc420037700 pc=0x100eb65 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1(0xc42007acf0, 0x12d3938, 0x12c6634, 0x17) /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x71 fp=0xc4200377c0 sp=0xc420037780 pc=0x12308f1 runtime.goexit() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/asm_amd64.s:2337 +0x1 fp=0xc4200377c8 sp=0xc4200377c0 pc=0x105cf01 created by github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xa1 ... many more scary lines of text ... 这看上去冗长、可怕，我们需要深呼吸并阅读错误：fatal error: concurrent map writes。有时候，当我们运行我们的测试时，两个 goroutines 完全同时写入 results map。Go 的 Maps 不喜欢多个事物试图一次性写入，所以就导致了 fatal error。\n这是一种 race condition（竞争条件），当软件的输出取决于事件发生的时间和顺序时，因为我们无法控制，bug 就会出现。因为我们无法准确控制每个 goroutine 写入结果 map 的时间，两个 goroutines 同一时间写入时程序将非常脆弱。\nGo 可以帮助我们通过其内置的 race detector 来发现竞争条件。要启用此功能，请使用 race 标志运行测试：go test -race。\n你应该得到一些如下所示的输出：\n================== WARNING: DATA RACE Write at 0x00c420084d20 by goroutine 8: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Previous write at 0x00c420084d20 by goroutine 7: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Goroutine 8 (running) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c Goroutine 7 (finished) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c ================== 细节还是难以阅读 - 但 WARNING: DATA RACE 相当明确。阅读错误的内容，我们可以看到两个不同的 goroutines 在 map 上执行写入操作：\nWrite at 0x00c420084d20 by goroutine 8: 正在写入相同的内存块\nPrevious write at 0x00c420084d20 by goroutine 7: 最重要的是，我们可以看到发生写入的代码行：\n/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 和 goroutines 7 和 8 开始的代码行号：\n/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 你需要知道的所有内容都会打印到你的终端上 - 你只需耐心阅读就可以了。\n使用channels处理race condition 我们可以通过使用 channels 协调我们的 goroutines 来解决这个数据竞争。channels 是一个 Go 数据结构，可以同时接收和发送值。这些操作以及细节允许不同进程之间的通信。\n在这种情况下，我们想要考虑父进程和每个 goroutine 之间的通信，goroutine 使用 url 来执行 WebsiteChecker 函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package concurrency type WebsiteChecker func(string) bool type result struct { string bool } func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) resultChannel := make(chan result) for _, url := range urls { go func(u string) { resultChannel \u0026lt;- result{u, wc(u)} }(url) } for i := 0; i \u0026lt; len(urls); i++ { result := \u0026lt;-resultChannel results[result.string] = result.bool } return results } 除了 results map 之外，我们现在还有一个 resultChannel 的变量，同样使用 make 方法创建。chan result 是 channel 类型的 —— result 的 channel。新类型的 result 是将 WebsiteChecker 的返回值与正在检查的 url 相关联 —— 它是一个 string 和 bool 的结构。因为我们不需要任何一个要命名的值，它们中的每一个在结构中都是匿名的；这在很难知道用什么命名值的时候可能很有用。\n现在，当我们迭代 urls 时，不是直接写入 map，而是使用 send statement 将每个调用 wc 的 result 结构体发送到 resultChannel。这使用 \u0026lt;- 操作符，channel 放在左边，值放在右边：\n1 2 // send statement resultChannel \u0026lt;- result{u, wc(u) 下一个 for 循环为每个 url 迭代一次。 我们在内部使用 receive expression，它将从通道接收到的值分配给变量。这也使用 \u0026lt;- 操作符，但现在两个操作数颠倒过来：现在 channel 在右边，我们指定的变量在左边：\n1 2 // receive expression result := \u0026lt;-resultChannel 然后我们使用接收到的 result 更新 map。\n通过将结果发送到通道，我们可以控制每次写入 results map 的时间，确保每次写入一个结果。虽然 wc 的每个调用都发送给结果通道，但是它们在其自己的进程内并行发生，因为我们将结果通道中的值与接收表达式一起逐个处理一个结果。\n我们已经将想要加快速度的那部分代码并行化，同时确保不能并发的部分仍然是线性处理。我们使用 channel 在多个进程间通信。\n当我们运行基准时：\npkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v2 BenchmarkCheckWebsites-8 100 23406615 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v2 2.377s 23406615 纳秒 —— 0.023 秒，速度大约是最初函数的一百倍，这是非常成功的。\n总结 某种程度说，我们已经参与了 CheckWebsites 函数的一个长期重构；输入和输出从未改变，它只是变得更快了。但是我们所做的测试以及我们编写的基准测试允许我们重构 CheckWebsites，让我们有信心保证软件仍然可以工作，同时也证明它确实变得更快了。\n在使它更快的过程中，我们明白了\ngoroutines 是 Go 的基本并发单元，它让我们可以同时检查多个网站。 anonymous functions（匿名函数），我们用它来启动每个检查网站的并发进程。 channels，用来组织和控制不同进程之间的交流，使我们能够避免 race condition（竞争条件） 的问题。 the race detector（竞争探测器） 帮助我们调试并发代码的问题。 ","permalink":"https://pillumina.github.io/posts/programming/golang/go-testing/","summary":"\u003ch2 id=\"preface\"\u003ePreface\u003c/h2\u003e\n\u003cp\u003e本文整理golang编码的单元测试常用示例，以及TDD的简要流程。\u003c/p\u003e\n\u003ch2 id=\"单元测试基础\"\u003e单元测试基础\u003c/h2\u003e\n\u003cp\u003e单元测试文件以\u003ccode\u003e_test.go\u003c/code\u003e结尾，需要记住以下原则：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e文件名必须是\u003ccode\u003e_test.go\u003c/code\u003e结尾的，这样在执行\u003ccode\u003ego test\u003c/code\u003e的时候才会执行到相应的代码\u003c/li\u003e\n\u003cli\u003e你必须import \u003ccode\u003etesting\u003c/code\u003e这个包\u003c/li\u003e\n\u003cli\u003e所有的测试用例函数必须是\u003ccode\u003eTest\u003c/code\u003e开头\u003c/li\u003e\n\u003cli\u003e测试用例会按照源代码中写的顺序依次执行\u003c/li\u003e\n\u003cli\u003e测试函数\u003ccode\u003eTestXxx()\u003c/code\u003e的参数是\u003ccode\u003etesting.T\u003c/code\u003e，我们可以使用该类型来记录错误或者是测试状态\u003c/li\u003e\n\u003cli\u003e测试格式：\u003ccode\u003efunc TestXxx (t *testing.T)\u003c/code\u003e,\u003ccode\u003eXxx\u003c/code\u003e部分可以为任意的字母数字的组合，但是首字母不能是小写字母[a-z]，例如\u003ccode\u003eTestintdiv\u003c/code\u003e是错误的函数名。\u003c/li\u003e\n\u003cli\u003e函数中通过调用\u003ccode\u003etesting.T\u003c/code\u003e的\u003ccode\u003eError\u003c/code\u003e, \u003ccode\u003eErrorf\u003c/code\u003e, \u003ccode\u003eFailNow\u003c/code\u003e, \u003ccode\u003eFatal\u003c/code\u003e, \u003ccode\u003eFatalIf\u003c/code\u003e方法，说明测试不通过，调用\u003ccode\u003eLog\u003c/code\u003e方法用来记录测试的信息。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"table-driven-testing\"\u003eTable-Driven-Testing\u003c/h3\u003e\n\u003cp\u003e测试讲究 case 覆盖，当我们要覆盖更多 case 时，显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试，标准库中有很多测试是使用这种方式写的。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eTestFib\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003efibTests\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nx\"\u003ein\u003c/span\u003e       \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"c1\"\u003e// input\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nx\"\u003eexpected\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"c1\"\u003e// expected result\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e6\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"mi\"\u003e7\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e13\u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ett\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003efibTests\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nx\"\u003eactual\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nf\"\u003eFib\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ett\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003eactual\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"nx\"\u003ett\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eexpected\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eErrorf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Fib(%d) = %d; expected %d\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ett\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eactual\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ett\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e由于我们使用的是 \u003ccode\u003et.Errorf\u003c/code\u003e，即使其中某个 case 失败，也不会终止测试执行。\u003c/p\u003e","title":"Golang TDD"},{"content":"性能提升不会凭空出现，它总是伴随着代码复杂度的上升。\nThe performance improvement does not materialize from the air, it comes with code complexity increase.\n\u0026ndash; Dmitry Vyukov\nGo 语言的调度器我认为应该是整个运行时最有趣的组件了。对于Go本身，它的设计和实现直接牵动了Go运行时的其他组件，也是和用户态代码直接打交道的部分；对于Go用户而言，调度器将其极为复杂的运行机制隐藏在了简单的关键字go下。为了保证高性能，调度器必须有效得利用计算的并行性和局部性原理；为了保证用户态的简洁，调度器必须高效得对调度用户态不可见的网络轮训器、垃圾回收器进行调度；为了保证代码执行的正确性，必须严格实现用户态代码的内存顺序等。总而言之，调度器的设计直接决定了Go运行时源码的表现形式。\n设计原理 数据结构: MPG 调度器启动 创建Goroutine 调度循环 触发调度 线程管理 总结 ","permalink":"https://pillumina.github.io/posts/programming/golang/schedualing/","summary":"\u003cp\u003e\u003cem\u003e性能提升不会凭空出现，它总是伴随着代码复杂度的上升。\u003c/em\u003e\u003cbr\u003e\n\u003cem\u003eThe performance improvement does not materialize from the air, it comes with code complexity increase.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u0026ndash; Dmitry Vyukov\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eGo 语言的调度器我认为应该是整个运行时最有趣的组件了。对于Go本身，它的设计和实现直接牵动了Go运行时的其他组件，也是和用户态代码直接打交道的部分；对于Go用户而言，调度器将其极为复杂的运行机制隐藏在了简单的关键字\u003ccode\u003ego\u003c/code\u003e下。为了保证高性能，调度器必须有效得利用计算的并行性和局部性原理；为了保证用户态的简洁，调度器必须高效得对调度用户态不可见的网络轮训器、垃圾回收器进行调度；为了保证代码执行的正确性，必须严格实现用户态代码的内存顺序等。总而言之，调度器的设计直接决定了Go运行时源码的表现形式。\u003c/p\u003e\n\u003ch2 id=\"设计原理\"\u003e设计原理\u003c/h2\u003e\n\u003ch2 id=\"数据结构-mpg\"\u003e数据结构: MPG\u003c/h2\u003e\n\u003ch2 id=\"调度器启动\"\u003e调度器启动\u003c/h2\u003e\n\u003ch2 id=\"创建goroutine\"\u003e创建Goroutine\u003c/h2\u003e\n\u003ch2 id=\"调度循环\"\u003e调度循环\u003c/h2\u003e\n\u003ch2 id=\"触发调度\"\u003e触发调度\u003c/h2\u003e\n\u003ch2 id=\"线程管理\"\u003e线程管理\u003c/h2\u003e\n\u003ch2 id=\"总结\"\u003e总结\u003c/h2\u003e","title":"Golang并发调度"},{"content":"python类关键字 __init__ vs __new__ __init__为初始化方法，__new__为真正的构造函数。\n描述符Descriptor __contains__ __slots__ 定制类 type() python作为动态语言，和静态语言最大的不同，即函数和类的定义，不是编译的时候创建的而是动态创建的。我们常见的对类的定义:\n1 2 3 class Hello(object): def hello(self, name=\u0026#39;world\u0026#39;): print(\u0026#39;Hello, %s.\u0026#39; % name) \u0026gt;\u0026gt;\u0026gt; from hello import Hello \u0026gt;\u0026gt;\u0026gt; h = Hello() \u0026gt;\u0026gt;\u0026gt; h.hello() Hello, world. \u0026gt;\u0026gt;\u0026gt; print(type(Hello)) \u0026lt;class \u0026#39;type\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; print(type(h)) \u0026lt;class \u0026#39;hello.Hello\u0026#39;\u0026gt; type()函数可以查看一类类型或者变量的类型，Hello是一个class， 它的类型是个type，而h是一个instance, 它的类型就是class Hello。\n同时有一个概念，就是type()不仅可以返回对象的类型，还可以创建出新的类型。我们可以不用定义class Hello() ...而动态创建出Hello类。\n\u0026gt;\u0026gt;\u0026gt; def fn(self, name=\u0026#39;world\u0026#39;): # 先定义函数 ... print(\u0026#39;Hello, %s.\u0026#39; % name) ... \u0026gt;\u0026gt;\u0026gt; Hello = type(\u0026#39;Hello\u0026#39;, (object,), dict(hello=fn)) # 创建Hello class \u0026gt;\u0026gt;\u0026gt; h = Hello() \u0026gt;\u0026gt;\u0026gt; h.hello() Hello, world. \u0026gt;\u0026gt;\u0026gt; print(type(Hello)) \u0026lt;class \u0026#39;type\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; print(type(h)) \u0026lt;class \u0026#39;__main__.Hello\u0026#39;\u0026gt; 创建一个class对象，type()函数传入3个参数：\nclass名称 继承的父类集合，python支持多重继承，只有一个父类的话需要加上tuple的单元素写法 class的方法名称与参数绑定，上面的例子里就把函数fn绑定到方法名hello上 通过type()函数创建的类和直接写class是一样的，python解释器遇到class定义时，也仅仅是扫描class定义的语法，然后调用type()函数创建出class。\nMetaClass 除了使用type()进行动态类创建，如果要控制类的创建行为，还可以使用metaclass。对其简单的解释就是：当我们定义了class以后，就可以根据这个class创建出实例，也就是先定义class，再创建实例；但是，如果我们希望创建class该怎么办？这里就必须用到metaclass创建class，所以先定义metaclass，然后创建class。\n因此，metaclass允许创建类或者修改类，也就是我们可以把类看成metaclass创建出来的“实例”。metaclass在python中相对比较难理解，而且很多场景不需要用，毕竟它能够改变类创建时的行为(behaviour)，不熟容易导致一些问题。\n下面是一个简单的为自定义的MyList类增加一个add方法的例子：\n先定义ListMetaClass, 一般来说metaclass的类以Metaclass结尾。\n1 2 3 4 5 # metaclass是类的模板，所以必须从`type`类型派生： class ListMetaclass(type): def __new__(cls, name, bases, attrs): attrs[\u0026#39;add\u0026#39;] = lambda self, value: self.append(value) return type.__new__(cls, name, bases, attrs) 有了这个定义，在定义MyList的时候传入关键字metaclass即可：\n1 2 class MyList(list, metaclass=ListMetaclass): pass 使用关键字后，python解释器在创建MyList的时候，要通过ListMetaclass.__new__()来创建。\n__new__()方法接受到的参数为：\n当前准备创建的类的对象 类的名字 类继承的父类集合 类的方法集合 测试一下是否正确加上方法：\n1 2 3 4 \u0026gt;\u0026gt;\u0026gt; L = MyList() \u0026gt;\u0026gt;\u0026gt; L.add(1) \u0026gt;\u0026gt; L [1] 而普通的liat是没有add方法的。那么动态修改的意义何在？直接在MyList里新增add方法不是更简单？正常情况下的确不需要用metaclass，不过还是有一些场景需要用到，比如ORM(Object Relational Mapping) \u0026mdash; 把关系型数据库的一行映射成一个对象，即一个类对应一个表，这样写代码更简单而不需要SQL语句。如果要编写这样一个ORM框架，所有的类都只能动态定义了，因为只有使用者才能根据表的结构定义对应的类。\n下面写一个简单的ORM框架，比如使用者想定义一个User类来操作对应的数据库表user，我们期待使用者写出如下的代码:\n1 2 3 4 5 6 7 8 9 10 11 class User(Model): # 定义类的属性到列的映射： id = IntegerField(\u0026#39;id\u0026#39;) name = StringField(\u0026#39;username\u0026#39;) email = StringField(\u0026#39;email\u0026#39;) password = StringField(\u0026#39;password\u0026#39;) # 创建一个实例： u = User(id=12345, name=\u0026#39;Michael\u0026#39;, email=\u0026#39;test@orm.org\u0026#39;, password=\u0026#39;my-pwd\u0026#39;) # 保存到数据库： u.save() 其中父类Model和属性类型StringField，IntegerField由ORM框架提供，剩下的save()全部由metaclass自动完成。\n先定定义Field类，用于负责保存数据库表的字段名和字段类型：\n1 2 3 4 5 6 7 8 class Field(object): def __init__(self, name, column_type): self.name = name self.column_type = column_type def __str__(self): return \u0026#39;\u0026lt;%s:%s\u0026gt;\u0026#39; % (self.__class__.__name__, self.name) 基于此类，定义其他Field子类：\n1 2 3 4 5 6 7 8 9 class StringField(Field): def __init__(self, name): super(StringField, self).__init__(name, \u0026#39;varchar(100)\u0026#39;) class IntegerField(Field): def __init__(self, name): super(IntegerField, self).__init__(name, \u0026#39;bigint\u0026#39;) 编写Model和ModelMetaclass：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class ModelMetaclass(type): def __new__(cls, name, bases, attrs): if name==\u0026#39;Model\u0026#39;: return type.__new__(cls, name, bases, attrs) print(\u0026#39;Found model: %s\u0026#39; % name) mappings = dict() for k, v in attrs.items(): if isinstance(v, Field): print(\u0026#39;Found mapping: %s ==\u0026gt; %s\u0026#39; % (k, v)) mappings[k] = v for k in mappings.keys(): attrs.pop(k) attrs[\u0026#39;__mappings__\u0026#39;] = mappings # 保存属性和列的映射关系 attrs[\u0026#39;__table__\u0026#39;] = name # 假设表名和类名一致 return type.__new__(cls, name, bases, attrs) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Model(dict, metaclass=ModelMetaclass): def __init__(self, **kw): super(Model, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r\u0026#34;\u0026#39;Model\u0026#39; object has no attribute \u0026#39;%s\u0026#39;\u0026#34; % key) def __setattr__(self, key, value): self[key] = value def save(self): fields = [] params = [] args = [] for k, v in self.__mappings__.items(): fields.append(v.name) params.append(\u0026#39;?\u0026#39;) args.append(getattr(self, k, None)) sql = \u0026#39;insert into %s (%s) values (%s)\u0026#39; % (self.__table__, \u0026#39;,\u0026#39;.join(fields), \u0026#39;,\u0026#39;.join(params)) print(\u0026#39;SQL: %s\u0026#39; % sql) print(\u0026#39;ARGS: %s\u0026#39; % str(args)) 注意: 当用户定义一个class User(Model)的时候，python解释器首先在当前类User定义中查找metaclass，如果没找到，继续在父类Model中找，找到了就使用Model中定义的metaclass的ModelMetaclass来创建User类，所以metaclass可以隐式得继承到子类\nModelMetaclass中的逻辑：\n排除掉对Model类的修改 在当前类（如User）中查找定义的类的所有属性，如果找到一个Field类，则将其保存到一个__mapping__的dict中，同时从类属性中删除该Field属性，防止runtime错误。（实例同名属性对类同名属性的覆盖） 把表名保存到__table__中 Model类中，就可以定义各种操作数据库的方法，比如save, delete, update等等。\n用上述的模块，可以编写出如：\n1 2 u = User(id=12345, name=\u0026#39;Michael\u0026#39;, email=\u0026#39;test@orm.org\u0026#39;, password=\u0026#39;my-pwd\u0026#39;) u.save() 获得的结果:\n1 2 3 4 5 6 7 Found model: User Found mapping: email ==\u0026gt; \u0026lt;StringField:email\u0026gt; Found mapping: password ==\u0026gt; \u0026lt;StringField:password\u0026gt; Found mapping: id ==\u0026gt; \u0026lt;IntegerField:uid\u0026gt; Found mapping: name ==\u0026gt; \u0026lt;StringField:username\u0026gt; SQL: insert into User (password,email,username,id) values (?,?,?,?) ARGS: [\u0026#39;my-pwd\u0026#39;, \u0026#39;test@orm.org\u0026#39;, \u0026#39;Michael\u0026#39;, 12345] 这里只是简单打出参数列表，对backend连接没有进行真正的处理。\n","permalink":"https://pillumina.github.io/posts/programming/python/python-functionality/","summary":"\u003ch2 id=\"python类关键字\"\u003epython类关键字\u003c/h2\u003e\n\u003ch3 id=\"__init__-vs-__new__\"\u003e\u003ccode\u003e__init__\u003c/code\u003e vs \u003ccode\u003e__new__\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003e__init__\u003c/code\u003e为初始化方法，\u003ccode\u003e__new__\u003c/code\u003e为真正的构造函数。\u003c/p\u003e\n\u003ch3 id=\"描述符descriptor\"\u003e描述符Descriptor\u003c/h3\u003e\n\u003ch3 id=\"__contains__\"\u003e\u003ccode\u003e__contains__\u003c/code\u003e\u003c/h3\u003e\n\u003ch3 id=\"__slots__\"\u003e\u003ccode\u003e__slots__\u003c/code\u003e\u003c/h3\u003e\n\u003ch3 id=\"定制类\"\u003e定制类\u003c/h3\u003e\n\u003ch3 id=\"type\"\u003etype()\u003c/h3\u003e\n\u003cp\u003epython作为动态语言，和静态语言最大的不同，即函数和类的定义，不是编译的时候创建的而是动态创建的。我们常见的对类的定义:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eHello\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ehello\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;world\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nb\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Hello, \u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s1\"\u003e.\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; from hello import Hello\n\u0026gt;\u0026gt;\u0026gt; h = Hello()\n\u0026gt;\u0026gt;\u0026gt; h.hello()\nHello, world.\n\u0026gt;\u0026gt;\u0026gt; print(type(Hello))\n\u0026lt;class \u0026#39;type\u0026#39;\u0026gt;\n\u0026gt;\u0026gt;\u0026gt; print(type(h))\n\u0026lt;class \u0026#39;hello.Hello\u0026#39;\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003etype()函数可以查看一类类型或者变量的类型，\u003ccode\u003eHello\u003c/code\u003e是一个class， 它的类型是个\u003ccode\u003etype\u003c/code\u003e，而\u003ccode\u003eh\u003c/code\u003e是一个instance, 它的类型就是class \u003ccode\u003eHello\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e同时有一个概念，就是type()不仅可以返回对象的类型，还可以创建出新的类型。我们可以不用定义\u003ccode\u003eclass Hello() ...\u003c/code\u003e而动态创建出Hello类。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt; def fn(self, name=\u0026#39;world\u0026#39;): # 先定义函数\n...     print(\u0026#39;Hello, %s.\u0026#39; % name)\n...\n\u0026gt;\u0026gt;\u0026gt; Hello = type(\u0026#39;Hello\u0026#39;, (object,), dict(hello=fn)) # 创建Hello class\n\u0026gt;\u0026gt;\u0026gt; h = Hello()\n\u0026gt;\u0026gt;\u0026gt; h.hello()\nHello, world.\n\u0026gt;\u0026gt;\u0026gt; print(type(Hello))\n\u0026lt;class \u0026#39;type\u0026#39;\u0026gt;\n\u0026gt;\u0026gt;\u0026gt; print(type(h))\n\u0026lt;class \u0026#39;__main__.Hello\u0026#39;\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e创建一个class对象，\u003ccode\u003etype()\u003c/code\u003e函数传入3个参数：\u003c/p\u003e","title":"Python类自定义"},{"content":"Preface BDD和TDD都是test case first的实现，无非是把后者的test改成前者的behavior。在TDD中，关注的核心点是function，即认为程序最基本单元是function，其test case可以认为是unit test，TDD和unit test的区别是TDD强调测试和开发结合而成的工作流: 写test case -\u0026gt; 写代码 -\u0026gt; 通过测试，继续写更多测试，写一次循环。\n而BDD比TDD更关注高层的行为，而不是函数级别的行为，也就是在BDD中，不会强调函数的功能正确，这是unit test应该做的事。BDD更关注user story，即用户在特定场景，与软件交互发生的行为，这个behavior指的就是高层模块的行为。\n如何区分BDD和TDD，简单理解，TDD是给programmer的，用来验证开发者的最基本模块的功能：在什么输入，应该产生什么输出，保证实现的边界，健全性。而BDD，其test case描述的是更高级的模块行为，脱离了具体的实现，容易用自然语言去描述，也就是BDD是给product manager的，告诉其系统的行为。\nBDD in golang ​\t实现的时候，我们需要把Given-When-Then这种story格式组织test case翻译为测试代码，通过一系列的assertion来检查实现是否符合test case的预期，我们完全可以直接通过golang自带的testing模块来实现，不过testing的功能有时候比较简陋，本文记录了用Ginkgo+Gomega来组织test case，让我们的测试语言更加接近自然语言。\n二者结合的目的是，ginkgo实现了test case的组织，并加入了其他方便的功能: 初始化，后续处理，异步等等。而gomega设计的目的是与ginkgo一起工作，实现易读的assertion(ginkgo中称为match)功能。\nGomega is ginkgo\u0026#39;s preferred matcher library 初始化 ginkgo依托golang原生testing框架，即可以用go test ./.. 执行，也可以通过ginkgo binrary安装go install github.com/onsi/ginkgo，封装了ginkgo测试框架的各种feature。\n初始化首先进入待测试的package:\ncd /path/to/package 执行初始化:\nginkgo bootstrap 生成以suite_test.go文件，接下来向suite添加测试specs，生成比如ginkgo_cart package测试文件。\nginkgo generate ginkgo_cart 运行 生成ginkgo_cart_test.go，注意测试文件在ginkgo_cart_testpackage， 需要import package ginkgo_cart，即BDD层级高于unit test, 不应该了解package内部的具体实现，测试package的外部接口即可。编写测试代码，运行go test ./..即可。\nGinkgo Keyword Ginkgo测试代码骨架由一系列keyword关联的闭包组成，常用的有：\nDescribe/Context/When: 测试逻辑块 BeforeEach/AfterEach/JustBeforeEach/JustAfterEach: 初始化测试用例块 It: 单一Spec，测试case keyword的声明均为传入Body参数，比如Describe:\n1 Describe(text string, body func()) bool 一个样例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var _ = Describe(\u0026#34;Nest Test Demo\u0026#34;, func() { Context(\u0026#34;MyTest level1\u0026#34;, func() { BeforeEach(func() { fmt.Println(\u0026#34;beforeEach level 1\u0026#34;) }) It(\u0026#34;spec 3-1 in level1\u0026#34;, func(){ fmt.Println(\u0026#34;sepc on level 1\u0026#34;) }) Context(\u0026#34;MyTest level2\u0026#34;, func() { BeforeEach(func() { fmt.Println(\u0026#34;beforeEach level 2\u0026#34;) }) Context(\u0026#34;MyTest level3\u0026#34;, func() { BeforeEach(func() { fmt.Println(\u0026#34;beforeEach level 3\u0026#34;) }) It(\u0026#34;spec 3-1 in level3\u0026#34;, func() { fmt.Println(\u0026#34;A simple spec in level 3\u0026#34;) }) It(\u0026#34;3-2 in level3\u0026#34;, func() { fmt.Println(\u0026#34;A simple spec in level 3\u0026#34;) }) }) }) }) }) Describe, Context, When 这三种都称为Container，对于ginkgo属于同一类，只是名称不同\n一般Describe用于最顶层：描述完整的测试场景，包含Context/When，而Context/When本身可以嵌套包含下级的Context/When。\n三者组织成Tree结构：Describe是root, Context和When是普通的TreeNode。\n三者包含的节点，除了自身，还包括其他keyword节点：BeforeEach, JustBeforeEach, It。\n测试代码逻辑应该包含在BeforeEach, It等类别中，而不应该在container类别中体现。\nIt Ginkgo执行以It为基本单元，以定义的顺序执行，It一般包含Assertion逻辑: Expect(\u0026hellip;)，即最终的测试结果和预期的比较，测试执行逻辑实现于BeforeEach, JustBeforeEach中\nBeforeEach, JustBeforeEach BeforeEach声明于Container节点内部，container node每个child执行前都会执行BeforeEach，一般用来Setup test env：声明测试用例变量，初始化。\nJustBeforeEach类似，区别是永远执行于BeforeEach之后：等从root到lt node所有BeforeEach执行完: 才再从root到lt node执行所有JustBeforeEach；一般实现测试执行逻辑：如request http，以便It node与expect比较。\nDemo code 示意 示例中各种节点的内部组成为如下tree：\n运行示例可以得到:\nbeforeEach level 1 sepc 1-1 on level 1 •beforeEach level 1 beforeEach level 2 beforeEach level 3 Spec 3-1 in level 3 •beforeEach level 1 beforeEach level 2 beforeEach level 3 Spec 3-2 in level 3 我么可以得到一些结论:\n执行是以It node定义顺序执行 每个It执行前，走了从root到It的path，顺序执行各context node的BeforeEach函数 It 与 Matcher 购物车demo中，其中一个lt:\n1 Expect(cart.TotalItems()).To(Equal(3)) 这种自然语言风格的assertion是由Ginkgo配套的Gomega实现的: expect返回封装了测试输出值的Assertion:\n1 func Expect(actual interface{}, extra ...interface{}) Assertion Assertion是interface, 简化版本(为语义通顺，还包含几个类似function):\n1 2 3 4 type Assertion interface { To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool } To接收GomegaMatcher, 其封装了Expect value: Equal调用了Ginkgo的EqualMatcher.\n1 2 3 4 5 func Equal(expected interface{}) types.GomegaMatcher { return \u0026amp;matchers.EqualMatcher{ Expected: expected, } } 加上Assertion封装了实际value, 两者的比较可得出结论.而ToNot是To的相反情况.\n如果想比较自定义的复杂类型: 可实现GomegaMatcher:\n1 2 3 加上Assertion封装了实际value, 两者的比较可得出结论.而ToNot是To的相反情况. 如果想比较自定义的复杂类型: 可实现GomegaMatcher: 其他features Focus:\n仅执行特定Node及之下的It: 在keyword之前加F: FContext, FIt, 但会使go testfail(返回 1), CI集成Ginkgo需注意.\nPending\n与Focus相反: 不执行特定Node及之下的It. 在keyword之前加X.但默认不会使go test fail(若想让其fail, 加 —failOnPending)\nSkip:\n根据代码runtime结果决定是否跳过某It(Pending是编译时):\n1 2 3 4 5 6 It(\u0026#34;spec 1-1 in level1\u0026#34;, func(){ if somecondition { Skip(\u0026#34;special condition wasn\u0026#39;t met\u0026#34;) } fmt.Println(\u0026#34;sepc 1-1 on level 1\u0026#34;) }) Skip仅能置于It之下，否则会Panic.\nEventually\n测试异步逻辑: 如发送请求到队列, 需持续polling. 在Gomega实现:\n1 2 3 Eventually(func() []int { return thing.SliceImMonitoring }, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2)) TIMTOUT为总超时时间, 默认１s;POLLING_INTERVAL为每次polling间隔, 默认10ms.\nGinkgo还支持benchmark及run in parallel, 可参考Ginkgo doc\n","permalink":"https://pillumina.github.io/posts/programming/golang/bdd-testing-framework/","summary":"\u003ch2 id=\"preface\"\u003ePreface\u003c/h2\u003e\n\u003cp\u003eBDD和TDD都是test case first的实现，无非是把后者的test改成前者的behavior。在TDD中，关注的核心点是function，即认为程序最基本单元是function，其test case可以认为是unit test，TDD和unit test的区别是TDD强调测试和开发结合而成的工作流: 写test case -\u0026gt; 写代码 -\u0026gt; 通过测试，继续写更多测试，写一次循环。\u003c/p\u003e\n\u003cp\u003e而BDD比TDD更关注高层的行为，而不是函数级别的行为，也就是在BDD中，不会强调函数的功能正确，这是unit test应该做的事。BDD更关注user story，即用户在特定场景，与软件交互发生的行为，这个behavior指的就是高层模块的行为。\u003c/p\u003e\n\u003cp\u003e如何区分BDD和TDD，简单理解，TDD是给programmer的，用来验证开发者的最基本模块的功能：在什么输入，应该产生什么输出，保证实现的边界，健全性。而BDD，其test case描述的是更高级的模块行为，脱离了具体的实现，容易用自然语言去描述，也就是BDD是给product manager的，告诉其系统的行为。\u003c/p\u003e\n\u003ch2 id=\"bdd-in-golang\"\u003eBDD in golang\u003c/h2\u003e\n\u003cp\u003e​\t实现的时候，我们需要把Given-When-Then这种story格式组织test case翻译为测试代码，通过一系列的assertion来检查实现是否符合test case的预期，我们完全可以直接通过golang自带的testing模块来实现，不过testing的功能有时候比较简陋，本文记录了用Ginkgo+Gomega来组织test case，让我们的测试语言更加接近自然语言。\u003c/p\u003e\n\u003cp\u003e二者结合的目的是，ginkgo实现了test case的组织，并加入了其他方便的功能: 初始化，后续处理，异步等等。而gomega设计的目的是与ginkgo一起工作，实现易读的assertion(ginkgo中称为match)功能。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eGomega is ginkgo\u0026#39;s preferred matcher library\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"初始化\"\u003e初始化\u003c/h2\u003e\n\u003cp\u003eginkgo依托golang原生testing框架，即可以用\u003ccode\u003ego test ./..\u003c/code\u003e 执行，也可以通过ginkgo binrary安装\u003ccode\u003ego install github.com/onsi/ginkgo\u003c/code\u003e，封装了ginkgo测试框架的各种feature。\u003c/p\u003e\n\u003cp\u003e初始化首先进入待测试的package:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecd /path/to/package\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e执行初始化:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eginkgo bootstrap\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e生成以suite_test.go文件，接下来向suite添加测试specs，生成比如ginkgo_cart package测试文件。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eginkgo generate ginkgo_cart\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"运行\"\u003e运行\u003c/h2\u003e\n\u003cp\u003e生成\u003ccode\u003eginkgo_cart_test.go\u003c/code\u003e，注意测试文件在\u003ccode\u003eginkgo_cart_test\u003c/code\u003epackage， 需要import package \u003ccode\u003eginkgo_cart\u003c/code\u003e，即BDD层级高于unit test, 不应该了解package内部的具体实现，测试package的外部接口即可。编写测试代码，运行\u003ccode\u003ego test ./..\u003c/code\u003e即可。\u003c/p\u003e\n\u003ch2 id=\"ginkgo-keyword\"\u003eGinkgo Keyword\u003c/h2\u003e\n\u003cp\u003eGinkgo测试代码骨架由一系列keyword关联的闭包组成，常用的有：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDescribe/Context/When: 测试逻辑块\u003c/li\u003e\n\u003cli\u003eBeforeEach/AfterEach/JustBeforeEach/JustAfterEach: 初始化测试用例块\u003c/li\u003e\n\u003cli\u003eIt: 单一Spec，测试case\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003ekeyword的声明均为传入Body参数，比如Describe:\u003c/p\u003e","title":"BDD: Ginkgo测试框架"},{"content":"设计原则 现在我们来看 Go 中另一重要的关键组件：内存分配器。\nGo 的内存分配器基于 Thread-Cache Malloc (tcmalloc) ，tcmalloc 为每个线程实现了一个本地缓存， 区分了小对象（小于 32kb）和大对象分配两种分配类型，其管理的内存单元称为 span。\n我们不再介绍更多 tcmalloc 的具体细节，因为 Go 的内存分配器与 tcmalloc 存在一定差异。 这个差异来源于 Go 语言被设计为没有显式的内存分配与释放， 完全依靠编译器与运行时的配合来自动处理，因此也就造就了内存分配器、垃圾回收器两大组件。\n我们知道，在计算机领域中，无外乎时间换空间、空间换时间。统一管理内存会提前分配或一次性释放一大块内存， 进而减少与操作系统沟通造成的开销，进而提高程序的运行性能。 支持内存管理另一个优势就是能够更好的支持垃圾回收，这一点我们留到垃圾回收器的章节中进行讨论。\n主要结构 Go 的内存分配器主要包含以下几个核心组件：\nheapArena: 保留整个虚拟地址空间 mheap：分配的堆，在页大小为 8KB 的粒度上进行管理 mspan：是 mheap 上管理的一连串的页 mcentral：收集了给定大小等级的所有 span mcache：为 per-P 的缓存。 其中页是向操作系统申请内存的最小单位，目前设计为 8KB。\n每一个结构虽然不都像是调度器 M/P/G 结构那样的大部头，但初次阅读这些结构时想要理清他们之间的关系还是比较麻烦的。 传统意义上的栈被 Go 的运行时霸占，不开放给用户态代码；而传统意义上的堆内存，又被 Go 运行时划分为了两个部分， 一个是 Go 运行时自身所需的堆内存，即堆外内存；另一部分则用于 Go 用户态代码所使用的堆内存，也叫做 Go 堆。 Go 堆负责了用户态对象的存放以及 goroutine 的执行栈。\nArena heapArena Go 堆被视为由多个 arena 组成，每个 arena 在 64 位机器上为 64MB，且起始地址与 arena 的大小对齐， 所有的 arena 覆盖了整个 Go 堆的地址空间。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const ( pageSize = 8192 // 8KB heapArenaBytes = 67108864 // 64MB heapArenaBitmapBytes = heapArenaBytes / 32 // 2097152 pagesPerArena = heapArenaBytes / pageSize // 8192 ) //go:notinheap type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan pageInUse [pagesPerArena / 8]uint8 pageMarks [pagesPerArena / 8]uint8 zeroedBase uintptr } arenaHint 结构比较简单，是 arenaHint 链表的节点结构，保存了 arena 的起始地址、是否为最后一个 arena，以及下一个 arenaHint 指针。\n1 2 3 4 5 6 //go:notinheap type arenaHint struct { addr uintptr down bool next *arenaHint } mspan 然而管理 arena 如此粒度的内存并不符合实践，相反，所有的堆对象都通过 span 按照预先设定好的 大小等级分别分配，小于 32KB 的小对象则分配在固定大小等级的 span 上，否则直接从 mheap 上进行分配。\nmspan 是相同大小等级的 span 的双向链表的一个节点，每个节点还记录了自己的起始地址、 指向的 span 中页的数量。它要么位于\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //go:notinheap type mspan struct { // 双向链表 next *mspan // 链表中的下一个 span，如果为空则为 nil prev *mspan // 链表中的前一个 span，如果为空则为 nil ... startAddr uintptr // span 的第一个字节的地址，即 s.base() npages uintptr // 一个 span 中的 page 数量 manualFreeList gclinkptr // mSpanManual span 的释放对象链表 ... freeindex uintptr nelems uintptr // span 中对象的数量 allocCache uint64 allocBits *gcBits ... allocCount uint16 // 分配对象的数量 spanclass spanClass // 大小等级与 noscan (uint8) incache bool // 是否被 mcache 使用 state mSpanState // mspaninuse 等等信息 ... } mcache 是一个 per-P 的缓存，它是一个包含不同大小等级的 span 链表的数组，其中 mcache.alloc 的每一个数组元素 都是某一个特定大小的 mspan 的链表头指针。\n1 2 3 4 5 6 7 8 9 10 //go:notinheap type mcache struct { ... tiny uintptr tinyoffset uintptr local_tinyallocs uintptr alloc [numSpanClasses]*mspan // 用来分配的 spans，由 spanClass 索引 stackcache [_NumStackOrders]stackfreelist ... } 当 mcache 中 span 的数量不够使用时，会向 mcentral 的 nonempty 列表中获得新的 span。\nmcentral mcentral\n1 2 3 4 5 6 7 8 //go:notinheap type mcentral struct { lock mutex spanclass spanClass nonempty mSpanList // 带有自由对象的 span 列表，即非空闲列表 empty mSpanList // 没有自由对象的 span 列表（或缓存在 mcache 中） ... } 当 mcentral 中 nonempty 列表中也没有可分配的 span 时，则会向 mheap 提出请求，从而获得 新的 span，并进而交给 mcache。\nmheap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 //go:notinheap type mheap struct { lock mutex pages pageAlloc ... allspans []*mspan // 所有 spans 从这里分配出去 scavengeGoal uint64 reclaimIndex uint64 reclaimCredit uintptr arenas [1 \u0026lt;\u0026lt; arenaL1Bits]*[1 \u0026lt;\u0026lt; arenaL2Bits]*heapArena heapArenaAlloc linearAlloc arenaHints *arenaHint arena linearAlloc allArenas []arenaIdx curArena struct { base, end uintptr } central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } ... // 各种分配器 spanalloc fixalloc // span* 分配器 cachealloc fixalloc // mcache* 分配器 treapalloc fixalloc // treapNodes* 分配器，用于大对象 specialfinalizeralloc fixalloc // specialfinalizer* 分配器 specialprofilealloc fixalloc // specialprofile* 分配器 speciallock mutex // 特殊记录分配器的锁 arenaHintAlloc fixalloc // arenaHints 分配器 ... } 分配概览 在分析具体的分配过程之前，我们需要搞清楚究竟什么时候会发生分配。\nGo 程序的执行是基于 goroutine 的，goroutine 和传统意义上的程序一样，也有栈和堆的概念。只不过 Go 的运行时帮我们屏蔽掉了这两个概念，只在运行时内部区分并分别对应：goroutine 执行栈以及 Go 堆。\ngoroutine 的执行栈与传统意义上的栈一样，当函数返回时，在栈上就会被回收，栈中的对象都会被回收，从而 无需 GC 的标记；而堆则麻烦一些，由于 Go 支持垃圾回收，只要对象生存在堆上，Go 的运行时 GC 就会在 后台将对应的内存进行标记从而能够在垃圾回收的时候将对应的内存回收，进而增加了开销。\n下面这个程序给出了四种情况：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package main type smallobj struct { arr [1 \u0026lt;\u0026lt; 10]byte } type largeobj struct { arr [1 \u0026lt;\u0026lt; 26]byte } func f1() int { x := 1 return x } func f2() *int { y := 2 return \u0026amp;y } func f3() { large := largeobj{} println(\u0026amp;large) } func f4() { small := smallobj{} print(\u0026amp;small) } func main() { x := f1() y := f2() f3() f4() println(x, y) } 我们使用 -gcflags \u0026quot;-N -l -m\u0026quot; 编译这段代码能够禁用编译器与内联优化并进行逃逸分析：\n1 2 3 4 5 6 7 8 # alloc.go # go build -gcflags \u0026#34;-N -l -m\u0026#34; -ldflags=-compressdwarf=false -o alloc.out alloc.go # command-line-arguments ./alloc.go:18:9: \u0026amp;y escapes to heap ./alloc.go:17:2: moved to heap: y ./alloc.go:22:2: moved to heap: large ./alloc.go:23:10: f3 \u0026amp;large does not escape ./alloc.go:28:8: f4 \u0026amp;small does not escape 情况1: f1 中 x 的变量被返回，没有发生逃逸； 情况2: f2 中 y 的指针被返回，进而发生了逃逸； 情况3: f3 中 large 无法被一个执行栈装下，即便没有返回，也会直接在堆上分配； 情况4: f4 中 small 对象能够被一个执行栈装下，变量没有返回到栈外，进而没有发生逃逸。 如果我们再仔细检查一下他们的汇编：\nTEXT main.f2(SB) /Users/changkun/dev/go-under-the-hood/demo/4-mem/alloc/alloc.go ... alloc.go:17\t0x104e086\t488d05939f0000\tLEAQ type.*+40256(SB), AX\talloc.go:17\t0x104e08d\t48890424\tMOVQ AX, 0(SP)\talloc.go:17\t0x104e091\te8cabffbff\tCALL runtime.newobject(SB)\talloc.go:17\t0x104e096\t488b442408\tMOVQ 0x8(SP), AX\talloc.go:17\t0x104e09b\t4889442410\tMOVQ AX, 0x10(SP)\talloc.go:17\t0x104e0a0\t48c70002000000\tMOVQ $0x2, 0(AX)\t... TEXT main.f3(SB) /Users/changkun/dev/go-under-the-hood/demo/4-mem/alloc/alloc.go ... alloc.go:22\t0x104e0ed\t488d05ecf60000\tLEAQ type.*+62720(SB), AX\talloc.go:22\t0x104e0f4\t48890424\tMOVQ AX, 0(SP)\talloc.go:22\t0x104e0f8\te863bffbff\tCALL runtime.newobject(SB)\talloc.go:22\t0x104e0fd\t488b7c2408\tMOVQ 0x8(SP), DI\talloc.go:22\t0x104e102\t48897c2418\tMOVQ DI, 0x18(SP)\talloc.go:22\t0x104e107\tb900008000\tMOVL $0x800000, CX\talloc.go:22\t0x104e10c\t31c0\tXORL AX, AX\talloc.go:22\t0x104e10e\tf348ab\tREP; STOSQ AX, ES:0(DI)\t... 就会发现，对于产生在 Go 堆上分配对象的情况，均调用了运行时的 runtime.newobject 方法。 当然，关键字 new 同样也会被编译器翻译为此函数，这个我们已经在实践中知道了。 所以 runtime.newobject 就是内存分配的核心入口了。\n分配入口 单看 runtime.newobject 其实非常简单，他只是简单的调用了 mallocgc：\n1 2 3 4 // 创建一个新的对象 func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) // true 内存清零 } 其中 _type 为 Go 类型的实现，通过其 size 属性能够获得该类型所需要的大小。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { // 创建大小为零的对象，例如空结构体 if size == 0 { return unsafe.Pointer(\u0026amp;zerobase) } mp := acquirem() mp.mallocing = 1 ... // 获取当前 g 所在 M 所绑定 P 的 mcache c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.kind\u0026amp;kindNoPointers != 0 if size \u0026lt;= maxSmallSize { if noscan \u0026amp;\u0026amp; size \u0026lt; maxTinySize { // 微对象分配 ... } else { // 小对象分配 ... } } else { // 大对象分配 ... } ... mp.mallocing = 0 releasem(mp) ... return x 在分配过程中，我们会发现需要持有 M 才可进行分配，这是因为分配不仅可能涉及 mcache，还需要将正在分配的 M 标记为 mallocing，用于记录当前 M 的分配状态。\n小对象分配 当对一个小对象（\u0026lt;32KB）分配内存时，会将该对象所需的内存大小调整到某个能够容纳该对象的大小等级（size class）， 并查看 mcache 中对应等级的 mspan，通过扫描 mspan 的 freeindex 来确定是否能够进行分配。\n当没有可分配的 mspan 时，会从 mcentral 中获取一个所需大小空间的新的 mspan，从 mcentral 中分配会对其进行加锁， 但一次性获取整个 span 的过程均摊了对 mcentral 加锁的成本。\n如果 mcentral 的 mspan 也为空时，则它也会发生增长，从而从 mheap 中获取一连串的页，作为一个新的 mspan 进行提供。 而如果 mheap 仍然为空，或者没有足够大的对象来进行分配时，则会从操作系统中分配一组新的页（至少 1MB）， 从而均摊与操作系统沟通的成本。\n微对象分配 对于过小的微对象（\u0026lt;16B），它们的分配过程与小对象的分配过程基本类似，但是是直接存储在 mcache 上，并由其以 16B 的块大小直接进行管理和释放。\n大对象分配 大对象分配非常粗暴，不与 mcache 和 mcentral 沟通，直接绕过并通过 mheap 进行分配。\n小结 该图展示了所有结构的关系。\nheap 最中间的灰色区域 arena 覆盖了 Go 程序的整个虚拟内存， 每个 arena 包括一段 bitmap 和一段指向连续 span 的指针； 每个 span 由一串连续的页组成；每个 arena 的起始位置通过 arenaHint 进行记录。\n分配的顺序从右向左，代价也就越来越大。 小对象和微对象优先从白色区域 per-P 的 mcache 分配 span，这个过程不需要加锁（白色）； 若失败则会从 mheap 持有的 mcentral 加锁获得新的 span，这个过程需要加锁，但只是局部（灰色）； 若仍失败则会从右侧的 free 或 scav 进行分配，这个过程需要对整个 heap 进行加锁，代价最大（黑色）。\n","permalink":"https://pillumina.github.io/posts/programming/golang/memory-management/","summary":"\u003ch1 id=\"设计原则\"\u003e设计原则\u003c/h1\u003e\n\u003cp\u003e现在我们来看 Go 中另一重要的关键组件：内存分配器。\u003c/p\u003e\n\u003cp\u003eGo 的内存分配器基于 Thread-Cache Malloc (tcmalloc) ，tcmalloc 为每个线程实现了一个本地缓存， 区分了小对象（小于 32kb）和大对象分配两种分配类型，其管理的内存单元称为 span。\u003c/p\u003e\n\u003cp\u003e我们不再介绍更多 tcmalloc 的具体细节，因为 Go 的内存分配器与 tcmalloc 存在一定差异。 这个差异来源于 Go 语言被设计为没有显式的内存分配与释放， 完全依靠编译器与运行时的配合来自动处理，因此也就造就了内存分配器、垃圾回收器两大组件。\u003c/p\u003e\n\u003cp\u003e我们知道，在计算机领域中，无外乎时间换空间、空间换时间。统一管理内存会提前分配或一次性释放一大块内存， 进而减少与操作系统沟通造成的开销，进而提高程序的运行性能。 支持内存管理另一个优势就是能够更好的支持垃圾回收，这一点我们留到垃圾回收器的章节中进行讨论。\u003c/p\u003e\n\u003ch2 id=\"主要结构\"\u003e主要结构\u003c/h2\u003e\n\u003cp\u003eGo 的内存分配器主要包含以下几个核心组件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eheapArena: 保留整个虚拟地址空间\u003c/li\u003e\n\u003cli\u003emheap：分配的堆，在页大小为 8KB 的粒度上进行管理\u003c/li\u003e\n\u003cli\u003emspan：是 mheap 上管理的一连串的页\u003c/li\u003e\n\u003cli\u003emcentral：收集了给定大小等级的所有 span\u003c/li\u003e\n\u003cli\u003emcache：为 per-P 的缓存。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e其中页是向操作系统申请内存的最小单位，目前设计为 8KB。\u003c/p\u003e\n\u003cp\u003e每一个结构虽然不都像是调度器 M/P/G 结构那样的大部头，但初次阅读这些结构时想要理清他们之间的关系还是比较麻烦的。 传统意义上的栈被 Go 的运行时霸占，不开放给用户态代码；而传统意义上的堆内存，又被 Go 运行时划分为了两个部分， 一个是 Go 运行时自身所需的堆内存，即堆外内存；另一部分则用于 Go 用户态代码所使用的堆内存，也叫做 Go 堆。 Go 堆负责了用户态对象的存放以及 goroutine 的执行栈。\u003c/p\u003e\n\u003ch3 id=\"arena\"\u003eArena\u003c/h3\u003e\n\u003ch4 id=\"heaparena\"\u003eheapArena\u003c/h4\u003e\n\u003cp\u003eGo 堆被视为由多个 arena 组成，每个 arena 在 64 位机器上为 64MB，且起始地址与 arena 的大小对齐， 所有的 arena 覆盖了整个 Go 堆的地址空间。\u003c/p\u003e","title":"Golang内存管理"},{"content":"问题： golang函数传参是不是应该和c一样，尽量不要直接传结构体，而是要传结构体指针？\n逃逸分析 逃逸分析指的是，在计算机语言编译器优化原理中，分析指针动态范围的方法，和编译器优化原理的指针分析和外形分析相关联。当变量（或者对象）在方法中被分配后，其指针有可能被返回或者被全局引用，这种现象就是指针（或引用）的逃逸（Escape）。\n其实在java概念中有一个误解 \u0026mdash; new出来的东西都在堆上，栈上存的是它的引用。 这句话在现代JVM上有问题，就是因为逃逸分析机制。简单来说，就是JVM的逃逸分析会在运行时(runtime)检测当前方法栈帧(frame)内new出来的对象的引用，是否被传出当前的栈帧。如果传出，就会发生逃逸，没有传出则不会。对于未发生逃逸的变量，则会直接在栈上分配内存。因为栈上内存由在函数返回时自动回收，而堆上的的内存需要gc去回收，如果程序中有大量逃逸的对象，那么势必会增加gc的压力。\n1 2 3 4 5 6 7 8 9 10 public void test(){ List\u0026lt;Integer\u0026gt; a = new ArrayList\u0026lt;\u0026gt;(); a.add(1); // a 未逃逸，在栈上分配 } public List\u0026lt;Integer\u0026gt; test1(){ List\u0026lt;Integer\u0026gt; a = new ArrayList\u0026lt;\u0026gt;(); a.add(1); return a // 发生逃逸，因此分配在堆上 } 区别 不同于JVM运行时的逃逸分析，Golang的逃逸分析是在编译期完成。 golang的逃逸分析只针对指针。一个值引用变量如果没有被取址，那么它永远不可能逃逸。 go version go1.13.4 darwin/amd64 验证某个函数的变量是否发生逃逸的方法：\ngo run -gcflags \u0026ldquo;-m -l\u0026rdquo; (-m打印逃逸分析信息，-l禁止内联编译)\ngo tool compile -S xxxx.go | grep runtime.newobject（汇编代码中搜newobject指令，这个指令用于生成堆对象）\n备注： 关于-gcflags \u0026ldquo;-m -l\u0026quot;的输出，有两种情况：\nMoved to heap: xxx xxx escapes to heap 二者都表示发生了逃逸，当xxx变量为指针的时候，出现第二种；当xxx变量为值类型时，为上一种，测试代码：\n1 2 3 4 5 6 7 8 type S int func main(){ a := S(0) b := make([]*S, 2) b[0] = \u0026amp;a c := new(S) b[1] = c } Golang逃逸分析 本文探究什么时候，什么情况下会发生逃逸\ncase 1 最基本的情况\n在某个函数中new或者字面量创建出的变量，将其指针作为函数返回值，则该变量一定发生逃逸 下面是例子:\n1 2 3 4 func test() *User{ a := User{} return \u0026amp;a } case 2 需要验证文章开头情况的正确性，也就是当某个值取指针并传给另一个函数的时候，是否有逃逸：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type User struct{ Username string Password string Age\tint } func main(){ a := \u0026#34;aaa\u0026#34; u := \u0026amp;User{a, \u0026#34;123\u0026#34;, 12} Call1(u) } func Call1(u *User){ fmt.Printf(\u0026#34;%v\u0026#34;, u) } 逃逸情况:\n-\u0026gt; go run -gcflags \u0026#34;-m -l\u0026#34; main.go # command-line-arguments ./main.go:18:12: leaking param: u ./main.go:19:12: Call1... argument does bnot escape ./main.go:19:13 u escapes to heap ./main.go:14:23 \u0026amp;User literal escapes to heap 可见发生了逃逸，这里将指针传给一个函数并打印，如果不打印，只对u进行读写：\n1 2 3 4 func Call1(u *User) int{ u.Username = \u0026#34;bbb\u0026#34; return u.Age * 20 } 结果:\n-\u0026gt; go run -gcflags \u0026#34;-m -l\u0026#34; main.go # command-line-arguments ./main.go:19:12: Call1 u does not escape ./main.go:14:23 main \u0026amp;User literal does not escape 并没有发生逃逸。其实如果只是对u进行读写，不管调用几次函数，传了几次指针，都不会逃逸。所以我们可以怀疑fmt.Printf的源码有问题，可以发现传入的u被赋值给了pp指针的一个成员变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Printf formats according to a format specifier and writes to standard output. // It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } // Fprintf formats according to a format specifier and writes to w. // It returns the number of bytes written and any write error encountered. func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return } // doPrintf里有 // .... p.printArg(a[argNum], rune(c)) // .... func (p *pp) printArg(arg interface{}, verb rune) { p.arg = arg p.value = reflect.Value{} // .... } 这个pp类型的指针p是由构造函数newPrinter返回，根据case1，p一定会发生逃逸，而p引用了传入指针，所以我们可以总结：\n被已经逃逸的变量引用的指针，一定发生逃逸。 case3 上述备注代码的例子：\n1 2 3 4 5 func main(){ a := make([]*int, 1) b := 12 a[0] = \u0026amp;b } 实际上这个代码中, slice a不会逃逸，而被a引用的b会逃逸。类似的情况会发生在map和chan之中\n1 2 3 4 5 6 7 8 9 10 11 12 13 func main(){ a := make([]*int, 1) b := 12 a[0] = \u0026amp;b c := make(map[string]*int) d := 14 c[\u0026#34;aaa\u0026#34;] = \u0026amp;d e := make(chan *int, 1) f := 15 e \u0026lt;- \u0026amp;f } 结果可以发现, b, d, f都逃逸了。所以我们可以得出结论：\n被指针类型的slice, map和chan引用的指针一定会发生逃逸。 备注： stack overflow上有人提问为何使用指针的chan比使用值得chan慢%30， 答案就在这里。使用指针的chan发生逃逸，gc拖慢了速度。 总结与深入本质 变量的逃逸，本质由于对于stack栈帧的内存分配，对于函数的调用将开辟一个栈帧frame，在这个栈帧内定义局部变量，当传出栈帧内创建的变量引用到前一个栈帧离去，如果函数结束，那么原来这块栈帧有可能被其他覆盖，这个传出去的引用就有问题。所以编译器把这种函数返回的变量可能在后续被引用的情况，将变量逃逸到堆上是一个非常合理的策略。 GopherCon SG 2019 1. When a value could possibly be reference after the function that constructed the value returns. 2. When the compiler determines a value is too large to fit on the stack. 3. When the compiler doesn\u0026#39;t know the size of a value at compile time. 我们得出指针必然逃逸的情况：\n在某个函数中new或者字面量创建出的变量，将其指针作为函数返回，则该变量一定发生逃逸（构造函数返回的指针变量一定逃逸） 被已经逃逸的变量引用的指针，一定发生逃逸 被指针类型slice, map和chan引用的指针，一定发生逃逸 同时我们也得出一些必然不会逃逸的情况：\n指针被未发生逃逸的变量引用 仅仅在函数内对变量做取址操作，而未将指针传出 有些情况可能发生逃逸，也可能不会发生逃逸 ：\n将指针作为入参传给别的函数，这里还是要看指针在被传入的函数中的处理过程，如果发生了上述三种情况，则会逃逸；否则不会发生逃逸。 因此，对于文章开头的问题，我们不能仅仅依据使用值引用作为函数入参可能因为copy导致额外内存开销而放弃这种值引用类型入参的写法。因为如果函数内有造成变量逃逸的操作情形，gc可能会成为程序效率不高的瓶颈。\n对io.Reader的解释 1 2 3 4 5 6 7 8 type Reader struct{ Read(p []byte) (n int, err error) } // Instead of type Reader struct{ Read(n int) (b []byte, err error) } 对于一个Reader来说当然第二种写法更为贴近逻辑，但是根据逃逸分析，第二种写法明显在不断的Read时在堆上产生过多的垃圾。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // escape to heap func main(){ b := read() // use b } func read() []byte{ // return a new slice b := make([]byte, 32) return b } // stay on stack func main(){ b := make([]byte, 32) read(b) // use b } func read(b []byte){ // write into slice } 几点强调 Optimize for correctness, not performance. Go only puts function variables on the stakc if it can prove a variable is not used after the function returned. Sharing down typically stay on the stack (传递指针给函数) Sharing up typically escapes to the heap (返回指针，不过不必须，都加了typically，比如内联可能会让情形不太一样) Ask the compiler to find out 深入逃逸和内联 逃逸的深入解释 ​ 前面尝试了几个例子去分析逃逸的场景，实际上我们还是需要理解其内部机制，才能把收益最大化（开发效率v.s.运行效率）。逃逸分析的本质是当compiler发现函数变量将脱离函数栈的有效域或被函数栈域外的变量所引用时，把变量分配在堆上而不是栈上，分析一些典型的场景：\n上述讨论过的，函数返回变量地址，或者返回包含变量地址的结构体。\n把变量地址写入channel或者sync.Pool，compiler无法获取goroutine如何使用这个变量，也就无法在编译的时候决定变量的生命周期。\n闭包可能导致闭包上下文逃逸，\nslice变量超过cap重新分配时，将在堆上进行，栈的大小毕竟是固定和有限的。\n上述讨论过的把变量地址赋值给可扩容容器（map, slice）时。\n把变量赋给可扩容interface容器（k或v为interface的map，或[]interface）的时候。\n几乎涉及到interface的地方都有可能导致对象逃逸，MyInterface(x).Foo(a)会导致a逃逸，如果a是引用语义(pointer, slice, map etc.)，那么a也会分配到堆上。涉及到interface的很多逃逸优化都很保守，比如reflect.ValueOf(x)会显式调用escapes(x)导致x逃逸。\n我们分析一下slice重分配的场景。这个场景是在堆上发生的，因为slice重分配时，会发生数据迁移，此时会把原本slice len内的元素浅拷贝到新的space。这个浅拷贝会导致新的slice(堆内存)引用了p(栈内存)的内容，而栈内存和堆内存的生命周期不一样，导致了可能出现函数return了以后，堆内存引用无效的栈内存的情形，这无疑会影响到运行的稳定。所以即使slice变量本身没有显式得逃逸，由于隐式的数据迁移，compiler会保守把slice或者map的指针元素逃逸到堆上。\n对于interface相关的，interface{}把值语义变为引用语义，其本质是type+pointer，这个pointer指向实际的data (源码分析开坑)。如果把值语义的变量赋值给interface容器，那么容器会持有变量的引用，所以这个变量会逃逸到堆上分配。\n案例里也分析了，fmt.Printf会导致逃逸，其实fmt.Sprintf或者logrus.Debugf都会导致所有传入参数逃逸，因为不定参数实际上是slice语法糖，编译器无法确定这些函数不会对参数slice进行append操作导致重分配，所以基于保守策略，都会把这些传入的参数分配到堆上以保证浅拷贝是准确的。\n这里我评价golang编译器的逃逸策略为保守应该是比较合适的，好的逃逸分析需要在编译期更深入地理解程序，这无疑非常困难，特别是涉及到interface{}，指针，可扩展容器的时候。\n内联 关于内联我需要在另一篇post中深入讨论，这里简单地说些感受。逃逸分析+GC很好用但是如果没有内联就会显得很昂贵，所有函数返回的地方会有一道“墙”，任何想要从墙逃逸到墙外的变量都会分配到堆上，比如：\n1 2 3 4 5 6 7 8 9 10 11 func NewCoord() *Coord{ return \u0026amp;Coord{ x : 1, z : 2, } } func foo(){ c := NewCoord() return c.x } 像NewCoord这样简单的构造函数都会导致返回值分配在堆上，抽离函数的代价也会更大。所以Go的内联，逃逸分析，GC像是三剑客，共同把其他语言避之不及的指针变得cheap。\nGo1.9开始对内联做了比较大的runtime优化，开始支持mid-stack inline ，并且通过-l编译参数指定内联等级(参数定义)。并且只在-l=4中提供了mid-stack inline，Go官方统计，这大概可以提升9%的性能，不过也增加了11%左右的二进制大小。\nGo1.10做一些interface相关的优化，比如devirtualization , compiler能够知道interface具体对象的情况下(如var i Iface = \u0026amp;myStruct{})可以直接生成对象相关代码调用(而非内联)，无需走interface方法查找。不过目前这个优化还不完善，还不能应用于逃逸分析优化。\nGo1.12开始默认支持了mid-stack inline\n在目前的项目中，似乎还不需要去调整内联参数，因为这个操作是个trade-off，过于激进的内联会导致生成的二进制文件更大你，CPU intstruction cache miss也可能会增加。默认等级的内联大部分时候都工作得很好并且保持稳定，到Go1.13为止，对interface方法的调用还不能被内联（哪怕compiler知道其具体的类型）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type I interface { F() int } type A struct{ x int y int } func (a *A) F() int { z := a.x + a.y return z } func BenchmarkX(b *testing.B) { b.ReportAllocs() for i:=0; i\u0026lt;b.N; i++ { // F() 会被内联 0.36 ns/op // var a = \u0026amp;A{} // a.F() // 对Interface的方法调用不能被内联 18.4 ns/op var i I = \u0026amp;A{} i.F() } } 对于一些偏底层基础的结构体，像上述的外层抽象了接口interface用于提供简单的对字段的访问设置，按照目前的分析和测试，内联会把字段访问速度提升一个数量级。\nPS： 个人的感受是目前Go interfaced的内联做的不够好，或许可以用公共API返回具体类型而不是interface，比如etcdclient.New, grpc.NewServer这些都是这样实践的，它们通过private fields加public methods让外部用起来像interface一样，但是数据逻辑层可能实践起来比较麻烦，因为Go的访问控制太差。\n","permalink":"https://pillumina.github.io/posts/programming/golang/golang-escape-analysis/","summary":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003e问题： golang函数传参是不是应该和c一样，尽量不要直接传结构体，而是要传结构体指针？\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"逃逸分析\"\u003e逃逸分析\u003c/h2\u003e\n\u003cp\u003e逃逸分析指的是，在计算机语言编译器优化原理中，分析指针动态范围的方法，和编译器优化原理的指针分析和外形分析相关联。当变量（或者对象）在方法中被分配后，其指针有可能被返回或者被全局引用，这种现象就是指针（或引用）的逃逸（Escape）。\u003c/p\u003e\n\u003cp\u003e其实在java概念中有一个误解 \u0026mdash; new出来的东西都在堆上，栈上存的是它的引用。 这句话在现代JVM上有问题，就是因为逃逸分析机制。简单来说，就是JVM的逃逸分析会在运行时(runtime)检测当前方法栈帧(frame)内new出来的对象的引用，是否被传出当前的栈帧。如果传出，就会发生逃逸，没有传出则不会。对于未发生逃逸的变量，则会直接在栈上分配内存。因为栈上内存由在函数返回时自动回收，而堆上的的内存需要gc去回收，如果程序中有大量逃逸的对象，那么势必会增加gc的压力。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInteger\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eArrayList\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c1\"\u003e// a 未逃逸，在栈上分配\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInteger\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003etest1\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInteger\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eArrayList\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 发生逃逸，因此分配在堆上\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"区别\"\u003e区别\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e不同于JVM运行时的逃逸分析，Golang的逃逸分析是在编译期完成。\u003c/li\u003e\n\u003cli\u003egolang的逃逸分析只针对指针。一个值引用变量如果没有被取址，那么它永远不可能逃逸。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ego version go1.13.4 darwin/amd64\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e验证某个函数的变量是否发生逃逸的方法：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003ego run -gcflags \u0026ldquo;-m -l\u0026rdquo; (-m打印逃逸分析信息，-l禁止内联编译)\u003c/p\u003e","title":"Golang逃逸分析"},{"content":"Design pattern Builder Pattern scenario：build complicated object 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package msg type Message struct { Header *Header Body *Body } type Header struct { SrcAddr string SrcPort uint64 DestAddr string DestPort uint64 Items map[string]string } type Body struct { Items []string } // Message对象的复杂对象 type builder struct{ once *sync.Once msg *Message } // 返回Builder对象 func Builder() *builder{ return \u0026amp;builder{ once: \u0026amp;sync.Once{}, msg: \u0026amp;Message{Header: \u0026amp;Header{}, Body: \u0026amp;Body{}}, } } func (b *builder) WithSrcAddr(srcAddr string) *builder{ b.msg.Header.SrcAddr = srcAddr return b } //...... func (b *builder) WithHeaderItem(key, value string) *builder{ //map只初始化一次 b.once.Do(func(){ b.msg.Header.Items = make(map[string]string) }) b.msg.Header.Items[key] = value return b } func (b *builder) WithBodyItem(record string) *builder{ b.msg.Body.Items = append(b.msg.Body.Items, record) return b } func (b *builder) Build() *Message{ return b.msg } Test code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package test func TestMessageBuilder(t *testing.T) { // 使用消息建造者进行对象创建 message := msg.Builder(). WithSrcAddr(\u0026#34;192.168.0.1\u0026#34;). WithSrcPort(1234). WithDestAddr(\u0026#34;192.168.0.2\u0026#34;). WithDestPort(8080). WithHeaderItem(\u0026#34;contents\u0026#34;, \u0026#34;application/json\u0026#34;). WithBodyItem(\u0026#34;record1\u0026#34;). WithBodyItem(\u0026#34;record2\u0026#34;). Build() if message.Header.SrcAddr != \u0026#34;192.168.0.1\u0026#34; { t.Errorf(\u0026#34;expect src address 192.168.0.1, but actual %s.\u0026#34;, message.Header.SrcAddr) } if message.Body.Items[0] != \u0026#34;record1\u0026#34; { t.Errorf(\u0026#34;expect body item0 record1, but actual %s.\u0026#34;, message.Body.Items[0]) } } Abstract Factory Pattern 常规的工厂模式，如果新增一个对象，需要修改原来的工厂对象代码，违反单一职责原则，最好增加一个抽象层。\ninterfaces definitions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package plugin //插件抽象接口定义 type Plugin interface{} // 输入插件，用于接收消息 type Input interface{ Plugin Receive() string } // 过滤插件，用于处理消息 type Filter interface{ Plugin Process(msg string) string } // 输出插件，用于发送消息 type Output interface{ Plugin Send(msg string) } pipeline composition 管道由上述三种插件定义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package pipeline ... // 消息管道的定义 type Pipeline struct { input plugin.Input filter plugin.Filter output plugin.Output } // 一个消息的处理流程为 input -\u0026gt; filter -\u0026gt; output func (p *Pipeline) Exec() { msg := p.input.Receive() msg = p.filter.Process(msg) p.output.Send(msg) } plugins implementation 定义三种插件的具体实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package plugin ... // input插件名称与类型的映射关系，主要用于通过反射创建input对象 var inputNames = make(map[string]reflect.Type) // Hello input插件，接收“Hello World”消息 type HelloInput struct {} func (h *HelloInput) Receive() string { return \u0026#34;Hello World\u0026#34; } // 初始化input插件映射关系表 func init() { inputNames[\u0026#34;hello\u0026#34;] = reflect.TypeOf(HelloInput{}) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package plugin ... // filter插件名称与类型的映射关系，主要用于通过反射创建filter对象 var filterNames = make(map[string]reflect.Type) // Upper filter插件，将消息全部字母转成大写 type UpperFilter struct {} func (u *UpperFilter) Process(msg string) string { return strings.ToUpper(msg) } // 初始化filter插件映射关系表 func init() { filterNames[\u0026#34;upper\u0026#34;] = reflect.TypeOf(UpperFilter{}) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package plugin ... // output插件名称与类型的映射关系，主要用于通过反射创建output对象 var outputNames = make(map[string]reflect.Type) // Console output插件，将消息输出到控制台上 type ConsoleOutput struct {} func (c *ConsoleOutput) Send(msg string) { fmt.Println(msg) } // 初始化output插件映射关系表 func init() { outputNames[\u0026#34;console\u0026#34;] = reflect.TypeOf(ConsoleOutput{}) } abstract factory interface definition \u0026amp; implementation 定义抽象工厂接口，和对应插件的工厂实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package plugin ... // 插件抽象工厂接口 type Factory interface { Create(conf Config) Plugin } // input插件工厂对象，实现Factory接口 type InputFactory struct{} // 读取配置，通过反射机制进行对象实例化 func (i *InputFactory) Create(conf Config) Plugin { t, _ := inputNames[conf.Name] return reflect.New(t).Interface().(Plugin) } // filter和output插件工厂实现类似 type FilterFactory struct{} func (f *FilterFactory) Create(conf Config) Plugin { t, _ := filterNames[conf.Name] return reflect.New(t).Interface().(Plugin) } type OutputFactory struct{} func (o *OutputFactory) Create(conf Config) Plugin { t, _ := outputNames[conf.Name] return reflect.New(t).Interface().(Plugin) } pipeline factory definition 最后定义pipeline工厂方法，调用plugin.Factory 抽象工厂完成pipeline对象的实例化\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package pipeline ... // 保存用于创建Plugin的工厂实例，其中map的key为插件类型，value为抽象工厂接口 var pluginFactories = make(map[plugin.Type]plugin.Factory) // 根据plugin.Type返回对应Plugin类型的工厂实例 func factoryOf(t plugin.Type) plugin.Factory { factory, _ := pluginFactories[t] return factory } // pipeline工厂方法，根据配置创建一个Pipeline实例 func Of(conf Config) *Pipeline { p := \u0026amp;Pipeline{} p.input = factoryOf(plugin.InputType).Create(conf.Input).(plugin.Input) p.filter = factoryOf(plugin.FilterType).Create(conf.Filter).(plugin.Filter) p.output = factoryOf(plugin.OutputType).Create(conf.Output).(plugin.Output) return p } // 初始化插件工厂对象 func init() { pluginFactories[plugin.InputType] = \u0026amp;plugin.InputFactory{} pluginFactories[plugin.FilterType] = \u0026amp;plugin.FilterFactory{} pluginFactories[plugin.OutputType] = \u0026amp;plugin.OutputFactory{} } Prototype Pattern 场景：对象的复制，如果对象成员变量复杂，或者对象有不可见变量，即会有问题\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package prototype ... // 原型复制抽象接口 type Prototype interface { clone() Prototype } type Message struct { Header *Header Body *Body } func (m *Message) clone() Prototype { msg := *m return \u0026amp;msg } Adapter Pattern 最常用的模式之一，典型场景是系统中老的接口过时或者即将废弃，可以新增一个适配器，把老的接口适配成新的接口使用，践行了开闭原则。该模式即把一个接口adaptee，通过适配器adapter转换成client锁期望的另一个接口target，也就是adapter通过实现target接口，并在对应的方法里调用adaptee的接口实现。\n继续消息处理系统的例子，目前系统的输入都来自HelloInput, 假设需要新增一个kafka消息队列中接收数据的功能，其中kafka消费者的接口如下：\n1 2 3 4 5 6 7 8 9 package kafka type Records struct{ Items []string } type Comsumer interface{ Poll() Records } 而Pipeline的设计是通过plugin.Input接口进行消息接收，所以这个kafka的接口无法直接集成。因此需要用适配器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package plugin ... type KafkaInput struct { status Status consumer kafka.Consumer } func (k *KafkaInput) Receive() *msg.Message { records := k.consumer.Poll() if k.status != Started { fmt.Println(\u0026#34;Kafka input plugin is not running, input nothing.\u0026#34;) return nil } return msg.Builder(). WithHeaderItem(\u0026#34;content\u0026#34;, \u0026#34;kafka\u0026#34;). WithBodyItems(records.Items). Build() } // 在输入插件映射关系中加入kafka，用于通过反射创建input对象 func init() { inputNames[\u0026#34;hello\u0026#34;] = reflect.TypeOf(HelloInput{}) inputNames[\u0026#34;kafka\u0026#34;] = reflect.TypeOf(KafkaInput{}) } 这里有个问题就是KafkaInput这个对象的成员构造问题，需要特别的init函数去初始化，可以考虑在Plugin接口新增一个Init方法，用于定义插件的一些初始化操作，并在工厂返回实例前调用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package plugin ... type Plugin interface { Start() Stop() Status() Status // 新增初始化方法，在插件工厂返回实例前调用 Init() } // 修改后的插件工厂实现如下 func (i *InputFactory) Create(conf Config) Plugin { t, _ := inputNames[conf.Name] p := reflect.New(t).Interface().(Plugin) // 返回插件实例前调用Init函数，完成相关初始化方法 p.Init() return p } // KakkaInput的Init函数实现 func (k *KafkaInput) Init() { k.consumer = \u0026amp;kafka.MockConsumer{} } 上述的MockConsumer的实现如下：\n1 2 3 4 5 6 7 8 9 package kafka ... type MockConsumer struct {} func (m *MockConsumer) Poll() *Records { records := \u0026amp;Records{} records.Items = append(records.Items, \u0026#34;i am mock consumer.\u0026#34;) return records } Test code 测试代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package test ... func TestKafkaInputPipeline(t *testing.T) { config := pipeline.Config{ Name: \u0026#34;pipeline2\u0026#34;, Input: plugin.Config{ PluginType: plugin.InputType, Name: \u0026#34;kafka\u0026#34;, }, Filter: plugin.Config{ PluginType: plugin.FilterType, Name: \u0026#34;upper\u0026#34;, }, Output: plugin.Config{ PluginType: plugin.OutputType, Name: \u0026#34;console\u0026#34;, }, } p := pipeline.Of(config) p.Start() p.Exec() p.Stop() } // 运行结果 === RUN TestKafkaInputPipeline Console output plugin started. Upper filter plugin started. Kafka input plugin started. Pipeline started. Output: Header:map[content:kafka], Body:[I AM MOCK CONSUMER.] Kafka input plugin stopped. Upper filter plugin stopped. Console output plugin stopped. Pipeline stopped. --- PASS: TestKafkaInputPipeline (0.00s) PASS Bridge Pattern 场景： 如果一个对象存在多个变化的方向，而且每个变化方向都需要扩展，桥接是好的选择。\n实际上上述的消息处理系统就是这样，一个pipeline有三个特征，且pipeline只依赖这三个接口而非具体的实现细节。\nProxy Pattern 代理模式为一个对象提供一种代理以控制对该对象的访问，使用率非常高。\n","permalink":"https://pillumina.github.io/posts/programming/design-pattern/design-pattern/","summary":"\u003ch1 id=\"design-pattern\"\u003eDesign pattern\u003c/h1\u003e\n\u003ch2 id=\"builder-pattern\"\u003eBuilder Pattern\u003c/h2\u003e\n\u003ch3 id=\"scenariobuild-complicated-object\"\u003escenario：build complicated object\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e54\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eMessage\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e   \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eSrcAddr\u003c/span\u003e  \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eSrcPort\u003c/span\u003e  \u003cspan class=\"kt\"\u003euint64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eDestAddr\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eDestPort\u003c/span\u003e \u003cspan class=\"kt\"\u003euint64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e    \u003cspan class=\"kd\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eBody\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Message对象的复杂对象\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eonce\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eOnce\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003emsg\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eMessage\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 返回Builder对象\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003eonce\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003esync\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eOnce\u003c/span\u003e\u003cspan class=\"p\"\u003e{},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e{},\u003c/span\u003e \u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\u003cspan class=\"p\"\u003e{}},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eWithSrcAddr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esrcAddr\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eSrcAddr\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003esrcAddr\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//......\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eWithHeaderItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e//map只初始化一次\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eonce\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eDo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eWithBodyItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erecord\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003eappend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003erecord\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"test-code\"\u003eTest code\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e \u003cspan class=\"nx\"\u003etest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nf\"\u003eTestMessageBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003et\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003etesting\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// 使用消息建造者进行对象创建\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003emessage\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithSrcAddr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;192.168.0.1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithSrcPort\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1234\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithDestAddr\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;192.168.0.2\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithDestPort\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e8080\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithHeaderItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;contents\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;application/json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithBodyItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;record1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eWithBodyItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;record2\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eSrcAddr\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;192.168.0.1\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eErrorf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;expect src address 192.168.0.1, but actual %s.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eHeader\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eSrcAddr\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;record1\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eErrorf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;expect body item0 record1, but actual %s.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eBody\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"abstract-factory-pattern\"\u003eAbstract Factory Pattern\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"abstract factory\" loading=\"lazy\" src=\"https://tva1.sinaimg.cn/large/007S8ZIlgy1ghkw23e4r3j31bs0nge82.jpg?imageslim\" data-zoomable\u003e\n\u003c/p\u003e","title":"Design Pattern: Overview"}]