Liz'Blog - 2026年5月
type: channel
link: http://www.lizzycat.cn/2026/05/
description: Liz的博客
items: 线上内存优化方案实战
items: Needle小模型
items: 基于原型驱动的ai工作流程
items: Erlang Memory Sub System
items: Erlang Schedualing
items: Erlang Garbage Collection
items: Erlang generic beam instructions
items: Erlang Distribution
items: Erlang Compiler
items: gb_trees

线上内存优化方案实战
type: item
date: 2026-05-27T08:19:00+00:00
description: 一次 Erlang 线上内存优化分析：ETS 大对象、compressed 表与 eheap allocator 碎片背景近期对一批 Erlang 线上节点做内存采样时，发现整体内存水位偏高。采样内容主要包含两部分：ETS 表维度的内存占用；ets_alloc 与 eheap_alloc 分配器的 block、carrier、usage 情况。从采样结果看，这批节点的内存问题并不是单一来源，而是由两类问题叠加造成：部分核心业务 ETS 表中存在大对象，尤其是超长 list、复杂 record、大 map 或嵌套结构，导致单条记录非常重；eheap_alloc 的 mbcs carrier 利用率偏低，部分高负载节点在进程堆分配高峰后留下较大的 carrier 空洞。这两类问题性质不同：ETS 数据结构优化，解决的是业务数据本体过大的问题；allocator 参数调整，解决的是进程堆分配碎片和 carrier 利用率偏低的问题。两者可以叠加优化，但需要分别评估收益、性能成本和适用场景。一、线上内存现状1. 节点整体情况本次采样覆盖节点数：节点数：444从 ETS 表占用看，内存主要集中在少数几个大表上。尤其是某个核心业务表，单表全服可见占用已经达到：核心业务表 X 当前可见 ETS 占用：约 227.70 GiB从 allocator 看，eheap_alloc 的 mbcs 当前全服情况如下：eheap_alloc mbcs: block = 74.87 GiB carrier = 157.78 GiB waste = 82.91 GiB也就是说，eheap_alloc 的 mbcs 中有相当一部分 carrier 已经被申请出来，但当前并没有被有效使用。2. ETS 表占用特征ETS 侧的问题比较明确：内存主要集中在少数大表中，其中最突出的表是核心业务表 X。这类表有几个典型特征：单条记录很大；记录数量不一定特别夸张，但 value 结构非常重；value 中包含超长 list、复杂 record、大 map、嵌套结构或冗余缓存字段；数据长期常驻 ETS/DETS，生命周期较长。在抽样分析中，某个核心记录的大致结构占用如下：单条核心记录：约 52,576 bytes 其中超长列表字段：约 51,792 bytes也就是说，单条记录的大部分内存都来自某个超长 list 字段，而不是 record 的其他字段。这个现象说明，ETS 表本身不是简单的“记录数太多”问题，而是存在明显的单条对象过胖问题。3. allocator 占用特征allocator 侧主要观察了 ets_alloc 与 eheap_alloc。ets_alloc 的整体使用率相对较高，说明 ETS 这部分主要是业务数据真实占用，并不是 allocator 碎片导致的假性膨胀。相比之下，eheap_alloc 的 mbcs 问题更明显：eheap_alloc mbcs block = 74.87 GiB eheap_alloc mbcs carrier = 157.78 GiB eheap_alloc mbcs waste = 82.91 GiB这说明进程堆分配曾经被拉高，carrier 扩张后没有充分回收，导致保留了大量低利用率 carrier。部分同类负载节点的 eheap_alloc mbcs usage 只有：10% - 30%单节点碎片空间可能达到：500 MiB - 1 GiB因此，这批节点的内存问题可以拆成两层：ETS 大表是真实业务数据占用，是主因；eheap_alloc 存在明显 carrier 浪费，是额外放大的因素。二、核心问题定位1. 核心业务表 X 中存在超长 list 字段抽样结果显示，某个核心业务表 X 中有一个超长列表字段，占用了单条记录的大部分空间。抽样数据如下：单条记录：约 52,576 bytes 超长列表字段本身：约 51,792 bytes将该列表字段压缩后：term_to_binary(List, [compressed]) 后正文：约 1,612 bytes 压缩后整条 record：约 2.4 KB 单条缩小比例：约 95%这说明该字段非常适合压缩存储。对于这类“写入后长期存放、读取频率相对可控、字段内容重复度较高”的列表数据，直接以 Erlang list 常驻 ETS 的成本非常高。2. 大 term 表适合 ETS compressed除了针对某个字段做 term_to_binary(..., [compressed]) 之外，还测试了 ETS 表自身的 compressed 参数。例如某类大对象业务表中存放的是完整的大 record。开启 ETS compressed 后，内存下降非常明显。测试结果中，某张…

Needle小模型
type: item
date: 2026-05-25T12:54:00+00:00
description: Needle：26M 参数超轻量级工具调用（Function Calling）模型Needle 是一项模型蒸馏的标志性成果——它将 Google Gemini 的工具调用能力压缩到仅 26M 参数的微型模型中。这意味着开发者可以在资源极度受限的边缘设备上部署高质量的函数调用能力，而无需依赖云端大模型。即使在 E5 + 1030 或单核 800M 内存的 VPS 上也能流畅运行。项目地址：https://github.com/cactus-compute/needle安装与快速启动1. 克隆仓库并安装git clone https://github.com/cactus-compute/needle.git cd needle source ./setup # 自动创建虚拟环境并安装依赖2. 启动 Playground（推荐）needle playground打开浏览器访问 http://127.0.0.1:7860首次运行会下载 needle.pkl（约 50MB）的模型文件到项目根目录的 checkpoints/Playground 提供 Web UI，可直接测试工具调用，也支持一键基于你的工具数据微调模型CLI 使用示例进入虚拟环境后，可直接使用 needle 命令：# 激活虚拟环境（如果未自动激活） source .venv/bin/activate单次推理（Run）needle run \ --checkpoint checkpoints/needle.pkl \ --query "I want to get a full system overview" \ --tools "[ {\"name\":\"mem\",\"description\":\"View Erlang VM memory usage. Returns byte counts for various memory types (total, processes, ETS, binary, etc.).\",\"parameters\":{}}, {\"name\":\"info\",\"description\":\"View comprehensive Erlang VM runtime information, including node name, scheduler count, process count/limit, ETS count/limit, and detailed memory usage.\",\"parameters\":{}}, {\"name\":\"top_mem\",\"description\":\"View the top 10 processes with highest memory usage. Returns PID, registered name, initial call, current function, memory, reductions, and message queue length for each.\",\"parameters\":{}}, {\"name\":\"top_queue\",\"description\":\"View the top 10 processes with longest message queues. Returns PID, registered name, initial call, current function, memory, reductions, and message queue length.\",\"parameters\":{}}, {\"name\":\"top_reds\",\"description\":\"View the top 10 processes with highest reductions (execution units). Returns PID, registered name, initial call, current function, memory, reductions, and message queue length.\",\"parameters\":{}}, {\"name\":\"top_bin\",\"description\":\"View the top 10 processes with highest binary memory usage. Returns PID, registered name, initial call, current function, memory, reductions, and message queue length.\",\…

基于原型驱动的ai工作流程
type: item
date: 2026-05-25T09:15:00+00:00
description: 游戏开发中模糊需求的应对：用「最小原型 + 反向校准」替代传统前置对齐背景：我们一直面临的沟通困境在游戏功能开发中，策划经常会给出一份相对模糊的需求文档。程序员阅读后往往无法完全把握核心意图，尤其是一些涉及服务端状态流转、边界处理、异常路径、权限控制、跨系统协作的内容，很难通过纯语言描述清楚。传统流程通常是：策划输出文档程序员（或 AI）尝试理解并实现双方花费大量时间进行语言层面的对齐即使对齐了，实际做出来也经常偏离预期这种「信息传递损耗」几乎是不可避免的。无论程序员如何仔细阅读、如何用 chain-of-thought 约束 AI，最终还是容易因为隐式假设、状态边界理解偏差而返工。我的思考：反向校准是否比纯前向对齐更有效？我提出了一种反向处理的思路：不强求在编码前把需求完全说清楚，而是先基于原始文档快速落地一个最小可运行的原型，再用这个原型作为媒介，反向和策划进行对齐。核心逻辑是：代码是可执行的、具体的，比抽象文档更能暴露理解偏差。策划看到实际运行效果后，反馈会更精准。程序员也不用在前期和策划进行大量低效的语言拉锯。这种方式把「沟通媒介」从纯语言升级为「可运行原型 + 结构化分析报告」，在需求天然模糊的游戏开发领域，性价比更高。工具选择：Superpowers + OpenSpec 的组合要让这个思路真正可落地，我选择了目前开源生态中比较成熟的两个工具组合：Superpowers：一个 agentic skills 框架，能给 AI 编码代理提供结构化的开发流程（计划、TDD、代码审查、子代理执行等），属于 execution 层。OpenSpec：一个轻量级的 Spec-Driven Development 框架，负责把需求结构化成可追溯的变更文件夹（proposal、specs、design、tasks），属于 spec 管理层。两者可以很好地协同使用。推荐的实际工作流（针对策划需求模糊场景）经过讨论，我们最终形成了一个更务实的混合流程：阶段 1：基于原始文档生成最小可运行原型策划给出初始（可能模糊）的需求文档。程序员不强行先用 OpenSpec 做完整前置 spec。直接使用 Superpowers + Code Agent（Claude Code、Codex 等），基于原始文档生成一个最小可运行的框架（能把核心状态流转、关键接口、主要业务流程跑起来即可）。这一步的目标是「可验证」，而不是「完整」。阶段 2：Agent 进行反向校准让 Agent 以「原始文档 + 已生成的原型代码」为输入，进行反向分析。输出结构化报告，包含：原型实际实现了什么（便于策划理解）与原始文档的偏差点文档未说明但代码必须决定的隐式假设边界处理、异常路径、状态一致性等存在风险的位置需要向策划澄清的具体问题清单阶段 3：用原型 + 报告与策划进行对齐把最小原型实际跑给策划看。结合反向校准报告，针对具体问题点进行讨论。这一步的核心价值是：把「文档没写清楚、状态流转不明确、边界缺失、异常情况未定义」这类问题，明确地转移给策划承担，而不是让程序员一直猜测和背锅。阶段 4：对齐后用 OpenSpec 固化需求对齐完成后，使用 OpenSpec 生成正式的 proposal.md、specs/、tasks.md 等结构化文档。后续精炼实现时，就基于这个确认过的 spec 进行。这种方式的核心价值1. 更好的职责划分程序员的主要职责是「把当前理解执行成可验证的原型 + 用工具结构化地暴露偏差」。策划的职责是「通过原型确认和补充需求」。双方都不需要在信息严重不对称的情况下做出过重承诺。2. 降低沟通成本用「可运行原型」作为媒介，比纯语言对齐效率更高、误解更少。3. 减少程序员无效精力消耗程序员不需要在前期花费大量时间去「猜」策划到底要什么，而是通过执行来暴露问题。4. 更适合游戏服务端开发的现实很多服务端需求本身就涉及复杂状态、异步流程、跨系统协作与边界控制，仅靠文档很难一次性描述完整。这种方式更尊重这个现实。适用场景与注意事项适合场景：需求具有较高不确定性（系统功能、服务端逻辑、状态流转、活动框架等）策划难以在编码前给出清晰确认团队希望减少前期无效对齐时间需要注意的点：最小原型的质量仍然重要，不能完全放飞。即使使用 Superpowers，也需要程序员对 Agent 的输出进行判断和把关。如果团队流程强行要求「所有需求必须先评审确认」，则需要先解决流程层面的阻力。反向校准不是用来逃避沟通，而是用更好的媒介进行更高效的沟通。总结在 AI 编码能力已经比较强的今天，「先快速做出最小可验证原型 → 用结构化方式反向校准 → 再固化需求」这种模式，比传统「强求前期完全确认」在很多游戏服务端开发场景下更加务实。它不是在回避沟通，而是在用执行 + 工具把原本模糊的沟通，…

Erlang Memory Sub System
type: item
date: 2026-05-02T04:39:00+00:00
description: 这一章节只是简单教一下内存子系统相关的知识，如果想更深入学习内存子系统可以读一下 "Understanding the Linux Kernel" or "Linux System Programming"这一章节主要是教两种动态可分配内存：堆、内存映射段本章节把堆叫做C-heap，以此区分erlang进程里面的堆；本章节把栈叫做C-stack；本章节把内存映射段（memory mapped segment）叫做segmentC-heap：通过malloc来分配C-heap的内存segment：通过mmap来分配segment的内存11.1 内存子系统有关erts_alloc的概述，可以看文档：erts\_alloc — erts v16.2目前有11种不同的分配器、6种不同的分配策略和18种以上的其他不同设置，其中一些设置采用任意数值。为了能够以任何有意义的方式使用这些设置，我们必须了解这些分配器是如何工作的，以及每个设置如何影响分配器的性能。erts_alloc手册警告：只有绝对明白你在干嘛的时候再调整这些参数，不合适的设置会导致严重的性能衰减甚至系统崩溃。11.2 不同类型的内存分配器详细了解内存是如何分配的,以及不同的分配器是如何工作的，关于系统可能遇到的问题以及如何分析和纠正行为的精彩故事,请阅读 Fred Hébert 的文章 "Troubleshooting Down the Logplex Rabbit Hole" https://blog.heroku.com/archives/2013/11/7/logplex-down-the-rabbit-hole内存分配器：管理特定类型内存的分配和释放。每个分配器都针对特定类型的数据,并且通常针对一种大小的数据进行专门化。每个内存分配器都实现了分配器接口,该接口可以使用不同的算法和设置来进行实际的内存分配。这么多不同的内存分配器的目的：是通过将相同大小的分配分组来减少碎片,并通过使频繁的分配成本降低来提高性能。有两种特殊的、基础的或通用的内存分配器类型：sys_alloc 和 mseg_alloc,以及通过 alloc_util 框架实现的九个特定分配器。表 1. 内存分配器列表名称描述C-nameType-nameFlag基础分配器malloc 接口sys_allocSYSTEMY内存段分配器mmap 接口mseg_alloc-M临时分配器临时分配temp_allocTEMPORARYT堆分配器Erlang 堆数据eheap_allocEHEAPH二进制分配器二进制数据binary_allocBINARYBETS 分配器ETS 数据ets_allocETSE驱动分配器驱动数据driver_allocDRIVERR短生命周期分配器短生命周期内存sl_allocSHORT_LIVEDS长生命周期分配器长生命周期内存ll_allocLONG_LIVEDL固定分配器固定大小数据fix_allocFIXED_SIZEF标准分配器大多数其他数据std_allocSTANDARDD字面量分配器模块常量literal_allocLITERAL(none)11.2.1. 基础分配器:sys_alloc不能被禁用，如果没指定用什么allocator，默认使用这个，基本上是对底层操作系统 libc 中 malloc 实现的直接映射。所有特定分器都使用 sys_alloc 或 mseg_alloc 根据需要从操作系统分配内存。当从操作系统分配内存时,sys_alloc 可以向请求的数量添加(填充)固定数量的千字节。这可以通过过度分配内存来减少系统调用的次数。默认填充为零。当释放内存时,sys_alloc 会在进程中保留一些空闲内存。这个空闲内存的大小称为修剪阈值(trim threshold),默认值为 128 kb。这也减少了系统调用的次数,但代价是更高的内存占用。这意味着如果您使用默认设置运行系统,您可能会遇到 Beam 进程不会在释放内存时立即将内存返回给操作系统的情况。sys_alloc 分配的内存 存储在 beam 进程的 C 堆中,该堆会根据需要通过 brk 系统调用增长。11.2.2. 内存段分配器:mseg_alloc如果底层操作系统支持 mmap,特定的内存分配器可以使用 mseg_alloc 而不是 sys_alloc 从操作系统分配内存。通过 mseg_alloc 分配的内存区域称为段(segments)。当释放一个段时,它不会立即返回给操作系统,而是保存在段缓存中。当分配一个新段时,如果可能的话会重用缓存的segment,即如果它的大小相同或大于请求的大小,但又不太大。absolute_max_cache_bad_fit 的值决定了被认为不太大的额外大小的千字节数。…

Erlang Schedualing
type: item
date: 2026-05-02T04:38:00+00:00
description: 调度器是用来让erlang系统决定哪些代码要跑和什么时候跑的，10.1 并发、并行、抢占式多任务（Preemptive Multitasking）SMP：Symmetric Multi-Processing（对称多处理）在Erlang/ERTS中的含义SMP是Erlang虚拟机的一个特性，使其能够：在多核处理器上真正地并行执行使用多个OS线程，每个线程运行一个独立的调度器充分利用多核CPU的计算能力并发 vs 并行并发(Concurrency)：多个进程在逻辑上同时执行，从外部观察者看起来是同时进行。在单核系统中通过抢占式多任务处理实现——一个进程运行一段时间后被中断，另一个进程运行并行(Parallelism)：多个进程在物理上真正同时执行，仅在多核或分布式系统中可实现10.2 ERTS中的抢占式多任务 与C代码的协作抢占式多任务是通过C级别的协作多任务实现的规约计数(Reduction Counting)：Erlang语言、编译器和虚拟机三者共同协作，以确保Erlang进程的执行在有限的时间内让出CPU，允许下一个进程运行。这是一种用于测量和限制允许执行时间的技术。10.3 ReductionsBEAM中的调度是一种协作式调度之上的抢占式调度。 一个进程只能在执行的特定点被中断，例如在receive或函数调用处。从这个意义上讲，调度是协作式的——进程必须执行允许被暂停的代码。Erlang代码的特性使得进程几乎不可能在不做函数调用的情况下运行很长时间。还有一些内置函数(BIF)仍然可能在不让出的情况下花费太长时间。另外，如果在实现不当的本地实现函数(NIF)中调用C代码，你可能会阻塞一个调度器很长时间。我们将何编在第16章讨论如写表现良好的NIF。协作式调度：进程/线程主动让出CPU执行权，而不是被操作系统或虚拟机强制中断。协作式调度之上的抢占式调度：erlang牛的地方，对内是一种协作式的调度，有自己的规矩决定进程什么时候需要让出cpu去执行其他进程的任务；对外看起来是抢占式调度，开发人员不需要自己手动去yield()，不需要自己去决定进程什么时候需要阻塞去让出cpu由于唯一的循环构造是递归和列表推导式，不做函数调用就不可能无限循环。每个函数调用都被计为一个reduction；当进程的reduction限制达到时，它就被暂停。规约(Reductions)术语来源于Erlang的Prolog血统。 在Prolog中，每个执行步骤都是一个目标化简(goal-reduction)，其中每个步骤将逻辑问题化简为其构成部分，然后尝试求解每个部分。BEAM的聪明设计： Erlang的语言特性（强制递归、频繁函数调用）天然地为规约计数提供了暂停点，使得系统既能实现看似抢占式的调度，又避免了真正抢占式调度的复杂性和性能开销。10.3.1 你能得到多少reductions这是定义在erl_vm.h文件里面的有一个CONTEXT_REDS字段，大约是4000左右。用完了reductions或者receive阻塞等待消息中，进程就会暂停，一个新的进程将被调度。如果虚拟机已执行由INPUT_REDUCTIONS定义的规约数量（目前为2*CONTEXT_REDS，也在erl_vm.h中定义），或者没有就绪进程可运行，调度器将进行系统级活动。 即基本上检查IO。reduction的双重作用：进程公平性：每个进程都获得固定的4000规约系统响应性：每8000规约检查一次IO，保证系统能及时响应外部事件这确保了Erlang系统既能实现进程间的公平调度，又不会因为CPU密集型计算而忽视IO事件。10.3.2 Reduction到底是是个啥？目前来说reduction没有一个完全的定义，但是至少每个函数调用算一个reduction。如果函数调用涉及到BIFs或者NIFs的话就有一些复杂了，首先用C写的函数不能中途yield，C函数无法自动暂停和恢复，所以很难支持中途让出，如果非要可重入（一个函数可以在自己还没执行完的时候被重新调用），需要保存一些执行状态和函数重新回到运行状态的时候要恢复状态，很麻烦。核心问题： C函数如果要支持中途让出，编程复杂度很高，所以通常的做法是：函数要么完整执行不让出，要么分解成多个小函数在调用点让出。比如说：在R16之前的Erlang版本中，BIF binary_to_term/1和term_to_binary/1是不让出的，只算作一个reduction。 这意味着对大型term调用这些函数的进程可能会使其他进程饥饿。而且这在SMP系统下也可能发生，因为进程在调度器之间是平衡的。这就是为什么Erlang在C中编写NIF时，官方建议如果任务太长，要么：✓ 分解成多个较小的NIF调用✓ 或使用可让出的NIF API（保存执行状态）当进…

Erlang Garbage Collection
type: item
date: 2026-05-02T04:37:00+00:00
description: 《The BEAM Book》第十二章：垃圾回收器 (Garbage Collection) 深度详析本章的核心在于解释 BEAM 如何在不停止整个系统的情况下，高效地管理内存。12.1 复制式垃圾回收 (Copying Garbage Collection)这一节介绍了 BEAM GC 的基本算法基础：分代（Generational） 和 复制（Copying）。基本原理触发时机：GC 发生时，系统分配一块新的内存区域（To-Space）。扫描过程：从根集合（Roots）（主要是栈 Stack、寄存器）开始扫描。复制操作：将所有“活”的对象从旧堆（From-Space）复制到新堆。清理：复制完成后，旧堆被整体废弃。详细步骤（Cheney 算法变体）根扫描：找到栈上引用的堆对象。复制与转发：当一个对象（如 Cons Cell）被复制到新地址后，BEAM 会在旧对象的内存位置写入一个转发指针（Forwarding Pointer），指向新地址。目的：如果堆中有多个指针指向同一个对象（共享数据），第一个指针触发复制后，后续指针看到“转发标记”，直接更新指向新地址，而不会重复复制对象。这保证了共享性（Sharing）在 GC 后依然存在。广度优先遍历 (BFS)：BEAM 的 GC 是按照广度优先的顺序进行的。书中 GDB 调试实验（有趣细节）书中通过一个 gc_example 模块演示了 GC 前后的内存变化。指针反转现象：Erlang 构建数据通常是自底向上（Bottom-up，先构建尾部再构建头部），而 GC 复制是自顶向下（Top-down，从根开始）。结果：GC 后，内存中对象的物理排列顺序往往与创建时相反。这虽然不改变逻辑结构，但会改变内存引用的方向。12.2 Erlang GC 中的分代机制 (Generations in Erlang's GC)这一节解释了 BEAM 如何区分“年轻数据”和“年老数据”。PCB 中的关键字段high_water（高水位线）：这是堆中的一个指针。地址低于此线的对象是“老年代”，高于此线的是“新生代”。old_heap：指向老年代堆的指针。gen_gcs：该进程经历过的次级 GC 次数。max_gen_gcs：在强制进行全量回收（Full Sweep）前的最大次级 GC 次数（默认 65535）。回收类型Minor GC (次级回收)：只回收 high_water 之上的新生代区域。存活下来的对象会被移动到老年代（Old Heap）。Major GC / Full Sweep (全量回收)：回收整个堆（包括老年代和新生代）。堆外内存管理Off-heap 数据：如大二进制（Refc Binaries）。它们不占堆空间，但占内存。虚拟堆 (Virtual Heap)：BEAM 维护 bin_vheap_sz 等字段，统计进程持有的二进制数据大小。如果虚拟堆太大，即使进程主堆还很空，也会强制触发 GC，以便减少二进制数据的引用计数。12.3 实际影响与考量 (Practical Implications and Considerations)这一节讨论了 GC 行为如何映射到我们在 process_info 或 Observer 中看到的指标。观察 high_water 的移动如果 high_water 线不断上移，说明该进程正在不断积累长期存活的数据。这可能是正常的（如 ETS 表的所有者进程），也可能是内存泄漏（无用的数据被意外保留引用）。消息缓冲区 (mbuf)当进程接收消息过快，来不及处理，或者发送方无法获取接收方的堆锁时，消息会被放在堆外的碎片区（Heap Fragments / mbufs）。警示：如果看到 mbuf_sz 很大，说明消息处理有瓶颈，或者发生了严重的锁竞争。12.4 优化建议 (Suggestions for Optimization)基于 GC 原理，作者给出了一些具体的优化建议。数据局部性 (Locality)由于 GC 采用广度优先复制，可能会破坏数据在内存中的局部性（原本相邻的数据被分散），但在现代大缓存 CPU 上，这通常不是主要瓶颈。预分配堆大小问题：如果一个进程启动后立刻处理大量数据，会触发多次扩容和 GC，浪费 CPU。方案：使用 spawn_opt(Fun, [{min_heap_size, Words}])。直接分配一个大堆，减少初期的 GC 抖动。减少临时垃圾虽然 Minor GC 很快，但大量临时数据依然会消耗内存带宽和 CPU 周期。12.5 栈与堆的增长 (Stack and Heap Growth)这一节通过物理布局解释了进程何时会“内存不足”。单一连续内存块Erlang 进程的栈和堆共用一块内存。相向生长Stack：从高地址向低地址增…

Erlang generic beam instructions
type: item
date: 2026-05-02T04:36:00+00:00
description: 通用的BEAM指令集通用指令集（Generic）：是官方标准，编译器和虚拟机都用它，相对稳定，只在大版本间变化；特定指令集（Specific）：是内部优化实现，可能随时改变，不应该基于它开发工具。要想开发编译器、虚拟机或分析工具，必须基于通用指令集，这样才能保证跨版本兼容性，特定指令集只是BEAM的内部实现细节。7.1 指令定义通用指令集的名称和操作码都定义在 lib/compiler/src/genop.tab.genop.tab 文件被 beam_makeops（一个Perl脚本）用作输入，从中生成各种代码。这个生成器（beam_makeops）会为编译器生成Erlang代码（beam_opcodes.hrl 和 beam_opcodes.erl），为模拟器生成C代码（beam_opcodes.c 和 beam_opcodes.h；对于传统BEAM解释器还会生成 beam_hot.h、beam_warm.h、beam_cold.h；对于BeamAsm会生成 beamasm_emit.h 和 beamasm_protos.h）。文件中以"#"开头的行是注释会被忽略，文件还可以包含定义来转换成Perl脚本中的绑定。genop.tab 文件的主要内容是操作码定义，格式是 OPNUM: [-]NAME/ARITY，其中OPNUM和ARITY是整数，NAME是以小写字母开头的标识符，冒号、减号和斜杠是字面符号。比如 1: label/1 就表示操作码1是label函数，参数数量为1。减号表示该函数已弃用，弃用函数会保留其操作码以便向后兼容，加载器会识别这些弃用指令但拒绝加载包含它们的代码。7.2 BEAM码列表给个 S 参数给编译器编译就可以得到 人可读和机器可读格式的 .S文件-module(beamexample1). -export([id/1]). id(I) when is_integer(I) -> I.When compiled with erlc -S beamexample1.erl we get the following beamexmaple1.S file:%% Module Declaration {module, beamexample1}. %% version = 0 %% Exported Functions {exports, [{id,1}, {module_info,0}, {module_info,1}]}. %% Attributes {attributes, []}. %% Labels Count {labels, 7}. %% ============================================================================ %% Function: id/1 %% ============================================================================ {function, id, 1, 2}. {label, 1}. % Error handling label {line, [{location, "beamexample1.erl", 5}]}. {func_info, {atom,beamexample1}, {atom,id}, 1}. {label, 2}. % Function entry point {test, is_integer, {f,1}, [{x,0}]}. % Test if x0 is integer return. % Return x0 %% ============================================================================ %% Function: module_info/0 (auto-generated) %% ============================================================================ {function, module_info, 0, 4}. {label, 3}. % Error handling label {line, []}. {func_info, {atom,beamexample1}, {atom,module_info}, 0}. {label, 4}. % Function entry point {move, {atom,beamexample1}, {x,0}}. % Move module name to x0 {line, []}. {call_e…

Erlang Distribution
type: item
date: 2026-05-02T04:36:00+00:00
description: BEAM Book 第14、15章节总结第14章：Distribution（分布式）14.1 节点与连接节点的概念定义：节点是ERTS的一个命名运行实例，只有命名节点才能通过Erlang分布进行通信命名规范：由原子 + '@' + 主机名组成，例如：node1@localhost短名称 vs 长名称特性短名称长名称格式仅原子（如 node1）原子@主机名使用场景所有节点在同一主机节点分布在不同主机互通性只能与其他短名称节点通信只能与其他长名称节点通信关键点：短名称和长名称不能互相通信，需要保持一致节点启动方式无名节点：无法连接到其他节点（Node.alive? -> false）有名节点：使用 --sname（短名）或 --name（长名）启动，可以连接其他节点动态启动：即使没有指定名称，也可通过 net_kernel:start([Name, shortnames]) 动态启动分布连接建立函数：net_kernel:connect_node/1默认行为：全连接拓扑 - 当N1连接到N2时，N1会自动连接到N2已连接的所有节点禁用方式：启动时使用 -connect_all false 标志Cookie（魔法cookie）认证作用：基本的安全认证机制，确保只有授权的节点能连接配置方式：启动时：-setcookie 选项动态设置：erlang:set_cookie/2 函数默认行为：节点启动时，在 ~/.erlang.cookie 中查找或创建自动生成随机atom作为默认cookie权限为用户只读（UNIX权限）SSL支持Erlang支持节点间的SSL连接配置：需要设置SSL证书、密钥和inet_dist选项性能注意：SSL加密/解密会造成性能开销，需权衡隐藏节点（Hidden Nodes）特点：不在 nodes() 返回列表中，不自动连接其他节点连接方式：必须显式使用 net_kernel:connect_node/1用途：隔离特定操作或管理网络流量大型动态网络中减少连接开销创建子网拓扑安全性：不是安全特性，不提供访问控制保护第15章：How the Erlang Distribution Works（分布式工作原理）15.1 EPMD（Erlang Port Mapper Daemon）EPMD的角色小型服务器，协助节点间建立连接独立的OS进程，即使Erlang节点停止也继续运行默认端口：4369工作流程Erlang节点启动时，自动启动EPMD（如果尚未运行）节点向EPMD注册，提供：节点名 + 监听端口其他节点连接前，先询问EPMD要连接的目标节点的端口号EPMD返回端口，节点建立TCP连接实现细节源代码：erts/epmd/src/epmd_srv.c简单的C实现，核心逻辑在单一C文件中协议：小型固定格式二进制协议15.2 Erlang Distribution Protocol（分布式协议）握手过程（Handshake）Node1 ──SEND_NAME──> Node2 <──ALIVE_ACK── (包含challenge随机数) ──CHALLENGE_REPLY──> (计算: challenge + magic_cookie) <──CHALLENGE_ACK── (完成握手)关键点：SEND_NAME：包含协议版本和发起节点名ALIVE_ACK/NACK：接受/拒绝连接Challenge-Response：使用魔法cookie进行身份验证连接建立后开始交换消息三种消息类型消息类型用途示例Control Messages分布式状态管理进程链接/取消链接、监控、EXIT/KILL信号Data Messages进程间通信发送项数据、RPCSystem MessagesVM内部通信分布式控制器、EPMD管理15.3 Alternative Distribution（可选的分布式实现）为什么需要可选分布式原因说明性能默认TCP可能不是最优的，可用更高效的协议安全增强加密、访问控制等超越魔法cookie的功能可靠性更精细的网络分区处理、更强的消息保证互操作性与其他语言系统（C、Scala等）通信实现方式Custom Distribution Driver（C语言驱动）完全控制低层通信细节协议和数据格式完全自定义Distribution Callback Module（Erlang回调）高层事件处理连接建立/关闭、消息收发Third-Party Libraries封装好的高层API抽象底层细节实现步骤编写分布式代码（C驱动或Erlang回调）配置VM（使用 -proto_dist 选项）测试机制（连接、消息传递、错误处理）15.4 Processes in Distributed Erlang（分布式进程）PID的结构和转…

Erlang Compiler
type: item
date: 2026-05-02T04:35:00+00:00
description: 从 .erl 文件中的源代码模块编译为二进制 .beam 文件。可以使用 shell 快捷命令 c()（它也会在编译后加载模块），或者通过调用 compile:file()（编译但不加载）参数P：预编译， 让你看到 Erlang 预处理器做完工作后、但还未被真正编译前的源代码样子。这对于理解宏和 include 文件如何被处理非常有用！编译时不同参数的意义（P，E，S） 可以看到不同阶段时，编译的源码是什么情况编译通道：简单来说就是.erl在编译时会经过不同的处理程序，看图来说好像有：前端流程：从 .erl 源码到 Core Erlang 的处理流程：.erl 文件↓[1] Scanner (erl_scan)• 将源代码文本转换为标记流（tokens）• 去除空格和注释输出: 标记序列↓[2] Preprocessor (epp)• 处理 -include() 包含文件• 展开 -define() 宏定义• 处理 -ifdef/-ifndef 条件编译输出: 展开后的标记 → (.P) [P参数输出]↓[3] Parser (erl_parse)• 根据 Erlang 语法检查标记• 将标记转换为 AST（Abstract Syntax Tree）输出: AST 数据结构↓[4] Parse Transforms• 用户定义的 AST 转换（可选）• 使用 -compile({parse_transform, module})输出: 修改后的 AST → (.Pbeam) [makedep选项]↓[5] Linter (erl_lint)• 检查语法错误• 检查未定义的变量/函数• 生成警告信息输出: AST → (.P) [P参数] / (.AST) [AST, binary参数]↓[6] Save AST for debug info• 保存完整的 AST（如果指定 +debug_info）• 用于调试器设置断点、单步执行输出: 调试信息↓[7] Expand Imports and Records• 展开 import 导入（转为明确的远程调用）• 展开 record 记录（转为 tuple 元组操作）• 添加 module_info/{1,2} 内置函数输出: 展开后的 Erlang → (.E) [E参数] / (.E, binary参数)↓[8] Core Erlang• 转换为更规范的函数式语言• 引入 let/letrec 表达式• 优化前的最后一步输出: Core Erlang → (.core) / (.core, binary参数)↓↓ 进入后端流程：从 Core Erlang 到 .beam 二进制的处理流程：Core Erlang↓[1] Core Erlang 优化通道Core Pass 0Core Pass 1...Core Pass N• 多次优化遍历• 常量折叠、死代码消除等输出: (.core) / (.core, binary参数)可中断点：from_core .erl 文件重新开始 ←→↓[2] Kernel Erlang• 将 Core Erlang 平化• 每个变量唯一化• 编译模式匹配到原始操作输出: 内核表示（无标准文件格式）↓[3] Kernel 优化通道Kernel Pass 0Kernel Pass 1...Kernel Pass N• 继续优化和简化↓[4] BEAM Assembly Code (ASM)• 生成 BEAM 虚拟机指令• 每条指令都是 Erlang 术语• {move, Src, Dst}、{call, Arity} 等输出: (.S) [S参数] / (.S, binary参数)可中断点：from_asm .S 文件 ←→↓[5] ASM 优化通道ASM Pass 0ASM Pass 1...ASM Pass N• 死代码消除• 窥视孔优化（peephole optimization）↓[6] BEAM Binary Format• 将 ASM 指令打包成二进制格式• 可以写入 .beam 文件• 可以通过网络发送• 可以通过 code:load_binary/3 加载↓(.beam 文件或内存)编译通道的意义：为什么要这样设计？模块化：每个阶段职责清晰可观察性：可以查看任何中间阶段的输出，便于学习和调试可扩展性：可以从任何中间点开始编译，方便定制编译流程优化机会：多个 Pass 可以在不同层次做优化（Core、Kernel、ASM）跨语言支持：其他语言可以生成 Core Erlang 或直接生成 BEAM ASM为什么需要分成前端和后端的说法？前端（语言相关）、后端（机器相关），前端处理各种语言（像Elixir这样的）最终变成Core Erlang…

gb_trees
type: item
date: 2026-05-02T01:30:28+00:00
description: (https://github.com/erlang/otp/blob/OTP-28.3.1/lib/stdlib/src/gb_trees.erl#L140 "View Source")General balanced trees.This module provides Prof. Arne Andersson's General Balanced Trees. These have no storage overhead compared to unbalanced binary trees, and their performance is better than AVL trees.官方文档是这么说的，但是Arne Andersson's General Balanced Trees说的不是AAtree的意思，这是Arne Andersson在1999年出的一篇论文，内容讨论优于AVL树的一个通用平衡二叉树，这种树的特点是：零存储开销 (Zero Storage Overhead)不用每个节点都存个平衡因子，只用存一个总大小和一个删除次数摊还重构成本更低 (Lower Amortized Restructuring Cost)对左右子树高度不敏感，需要达到超过了 clogn 的界限才进行干预有论文支持虽然偶合有稍微大代价一点的重构子树，但是维护整棵树的总工作量比的avl树要低一些算法逻辑的简单性 (Simplicity)General Balanced Trees 只需要关注一个全局标准：树的高度是否超过了 log⁡n，只需要在插入和删除操作时维护两个参数：总大小和删除次数，而无需复杂的平衡因子计算，这使得其在实现上更加高效和简洁。总结：为什么说它比 AVL 树“性能好”？表格特性AVL 树General Balanced Trees (论文方案)优势点存储需要存储平衡因子 (Per-node overhead)无节点存储开销 (仅需2个全局整数)内存更省，缓存更友好维护策略每次修改都可能触发旋转 (Strict)懒惰重建 (Lazy/Amortized)频繁修改场景下，总旋转/重构工作量更少时间复杂度最坏情况 O(log⁡n)O(logn)摊还 O(log⁡n)O(logn)AVL 保证单次快；GB 树保证多次操作总时间更短