← 返回信息流
AI 资讯Hacker News·2 天前

Erlang集群遭遇蠕虫攻击及微流控技术探索

原标题:A worm in my Erlang cluster, and adventures in microfluidics

速览

本文首先回顾了Erlang分布式集群中遭遇蠕虫攻击的具体案例与应对过程。随后,内容转向微流控技术领域的探索与实验。这展示了从软件系统安全到硬件物理实验的跨领域技术冒险。

AI 深度解读

我的 Erlang 集群中的“蠕虫”与微流控冒险

背景

在分布式系统领域,Erlang 和 Elixir 以其强大的并发能力和容错机制著称。默认情况下,Erlang 集群采用全互联(fully meshed)拓扑结构,即集群中的每个节点都与其他所有节点保持直接连接。这种设计虽然简单直观,但在节点数量庞大时,会导致网络通信量激增(excessive chatter)以及连接边数呈指数级爆炸,从而带来严重的性能开销和管理复杂性。

为了解决这一问题,开发者可以构建稀疏连接(sparsely connected)的集群拓扑。在这种模式下,并非所有节点都两两相连,而是将集群划分为若干子网(sub-meshes),并通过少量的“桥梁”节点(bridge nodes)或稀疏连接将它们串联起来。这种结构类似于图论中的非完全图,能够显著降低网络负载,但同时也给集群的可视化和拓扑发现带来了挑战。

本文作者分享了一次技术探索:如何从一个单一节点出发,通过一种类似“蠕虫”(worm)的自传播代码机制,自动探测并映射出一个任意拓扑结构(无论是全互联还是稀疏连接)的 Erlang/Elixir 集群。有趣的是,作者还将其与微流控(microfluidics)实验中的墨水流动可视化进行了类比,以此形象地解释图遍历的过程。

核心内容

稀疏连接集群的拓扑挑战

作者首先定义了稀疏连接集群的概念。在全互联集群中,节点 abc 互相连接。而在稀疏结构中,例如节点 d 仅连接到节点 c,而 abc 构成一个三角形,d 作为“尾巴”挂在 c 上。

在 Erlang 中,可以通过 :erlang.nodes() 或 Elixir 中的 Node.list() 获取当前节点可见的邻居列表。然而,如果集群拓扑是任意且稀疏的,单个节点无法直接知晓整个集群的全貌。作者提出的核心问题是:如何从单个节点出发,映射出整个集群的任意连接拓扑?

图遍历算法:洪水填充(Flood-fill)

作者的解决方案是模拟图遍历算法。其逻辑如下:

  1. 当前节点询问其直接邻居:“你看到了谁?”
  2. 邻居返回其邻居列表。
  3. 当前节点将返回的列表与自身已知的邻居进行对比,发现新的节点。
  4. 对于新发现的节点,递归地执行上述步骤,询问它们的邻居。

这个过程类似于图论中的广度优先搜索(BFS)或深度优先搜索(DFS),旨在对图进行“洪水填充”,无论其拓扑结构如何,都能遍历所有可达节点。

“蠕虫”代码:自传播模块的实现难点

为了实现这一目标,作者希望编写一个单文件工具,只需将其粘贴到集群中的任意一个节点,该工具就能自动传播到所有其他节点并收集拓扑信息。

这里存在一个核心技术障碍:Erlang 的模块加载机制

  • 集群节点之间没有义务共享代码。
  • 即使在一个节点上通过 iex 定义了模块,该模块的二进制代码(beam code)也不会自动加载到其他节点。
  • 使用 :erpc.call(neighbor, Module, :run, []) 调用远程节点上的函数时,如果远程节点尚未加载该模块,调用将失败。
  • 更棘手的是,:code.get_object_code(Module) 通常只能获取从文件系统加载的模块的二进制代码。对于在内存中动态定义(如通过 iex 粘贴代码)的模块,该函数往往返回 :error

技术突破:提取并重新加载模块二进制

为了解决代码分发问题,作者设计了一个巧妙的“包装器”策略:

  1. 生成可提取的二进制代码: 作者没有直接使用内存中的模块,而是使用 Kernel.ParallelCompiler.compile_to_path/2 将一段 Elixir 代码编译为临时的 .beam 文件,并将该临时路径添加到代码服务器(code server)的搜索路径中。这样,模块就被视为从文件系统加载,从而可以通过 :code.get_object_code/1 获取其完整的二进制数据。

  2. 代码自包含: 作者构建了一个 ProbeWrapper 模块,它负责:

    • 动态生成 ActualProbe 模块的代码字符串。
    • 将其编译到临时目录。
    • 添加临时路径。
    • 提供 self_code/0 函数,返回 ActualProbe 的二进制数据。
  3. 远程加载与执行: 一旦获取了模块的二进制数据,就可以通过 :code.load_binary(Module, Filename, Binary) 将其加载到远程节点。注意,Filename 参数仅用于在代码服务器中标记模块,并不对应真实的文件系统路径。

  4. 递归遍历逻辑: 核心的 traverse/3visit/3 函数实现了递归传播:

    • visit/3 首先通过 :erpc.call 将模块二进制加载到邻居节点。
    • 然后调用邻居节点上的 run_probe/2,传入二进制数据和已访问节点集合。
    • 邻居节点重复此过程,询问其邻居,从而将“蠕虫”传播到整个集群。

微流控类比

作者在文中提到,图遍历的可视化灵感来源于微流控(microfluidics)实验。在实验中,墨水在微通道中流动,类似于数据在集群节点间传播。虽然作者自嘲应使用“minifluidics”或“millifluidics”,但“microfluidics”一词更具可读性。这种类比形象地说明了代码如何在网络拓扑中“流动”并填充所有空间。

潜在优化与扩展

  • 并行化:当前的实现是顺序遍历的。可以使用 :erpc.multicall 进行并行调用,但这需要处理重复工作的问题,或者在稀疏集群上构建分布式数据结构来追踪已访问节点。
  • 功能扩展:目前的“蠕虫”仅用于发现节点。通过传递函数参数,它可以执行任意任务,甚至在整个集群上加载代码,而无需关心具体的拓扑结构。

关键要点

  • 稀疏集群拓扑:Erlang/Elixir 集群可以通过稀疏连接(桥梁节点)降低全互联带来的网络开销,但这使得拓扑发现变得复杂。
  • 代码分发难题:Erlang 节点间默认不共享代码,且内存中动态定义的模块难以通过标准 API 获取二进制代码,阻碍了远程执行。
  • 编译到路径技巧:通过使用 Kernel.ParallelCompiler.compile_to_path/2 将代码编译为临时 .beam 文件并加入代码路径,成功绕过了 :code.get_object_code/1 对内存模块的限制,获取了可分发的二进制数据。
  • 自传播机制:利用 :code.load_binary/3:erpc.call/4,实现了代码从单一节点向整个集群的递归传播,无论集群拓扑如何。
  • 图遍历算法:该机制本质上是一种分布式图遍历算法,通过递归询问邻居的邻居,实现对集群全貌的映射。
  • 微流控类比:作者将代码在网络中的传播比作墨水在微通道中的流动,提供了直观的物理模型理解。

意义与影响

  1. 简化集群运维与调试: 对于大型或拓扑复杂的 Erlang/Elixir 分布式系统,手动维护节点连接关系极其困难。这种“蠕虫”工具提供了一种自动化的拓扑发现手段,帮助开发者快速了解集群结构,识别断连或异常节点。

  2. 展示 Erlang 运行时的高级特性: 文章深入展示了 Erlang 代码服务器(Code Server)和模块加载机制的底层细节。通过利用 :code.load_binary 和临时编译路径,作者展示了如何突破语言默认行为的限制,实现高度灵活的远程代码执行(RCE)和动态分发。

  3. 分布式系统设计的通用启示: 虽然代码针对 Erlang

查看原文 →lucassifoni.info