Posts for: #Learning

Lua:Concurrency

Lua 的并发(Concurrency)设计核心在于其轻量级、嵌入式的哲学,以及对协作式多任务的首选。它通过强大的协程机制实现并发,但本身不提供多线程/多进程的并行能力。


多线程/多进程

  • 核心语言无内置支持: Lua 语言本身的核心 VM 被设计为单线程执行。它不提供内置的语法或标准库来直接创建和管理线程(std::thread)或进程(fork)。
  • 独立的 Lua State: 一个 Lua VM 实例被称为一个“Lua State”。每个 Lua State 是完全独立的运行时环境,拥有自己的全局变量、栈、打开的文件、垃圾回收器等。它们之间默认不共享任何数据。
  • 宿主语言的责任: 如果需要在 Lua 中实现真正的并行(多核利用),必须依赖于宿主语言(如 C/C++)的多线程/多进程机制
    • 实现方式: 在宿主语言的每个线程或进程中,创建并运行一个独立的 Lua State
    • 数据交换: 这些独立的 Lua State 之间无法直接共享内存。数据交换必须通过宿主语言提供的进程间通信 (IPC) 或线程间通信 (ITC) 机制(如消息队列、共享内存、管道、套接字等)来完成。
    • 优点: 简单安全,因为 Lua State 之间是隔离的,避免了复杂的并发同步问题。
    • 缺点: 额外的通信开销和复杂性,且无法在单个 Lua State 内部实现并行。
  • 第三方库(封装): 存在一些第三方库(如 LuaLanes)试图提供在 Lua 中模拟多线程/多进程的 API。这些库通常是在底层创建独立的 Lua State,并封装了 IPC 机制,方便 Lua 开发者使用,但其本质仍然是基于宿主语言的底层能力和独立的 Lua State。

协程

协程的设计与实现

  • 设计理念: Lua 协程是为了提供协作式多任务 (Cooperative Multitasking) 而设计。它们允许在单个线程中实现任务的暂停和恢复,以模拟并发,而无需复杂的锁机制。
  • “有栈协程” (Stackful Coroutines): Lua 协程是有栈的。这里的“栈”指的不是操作系统的 C 语言栈,而是 Lua 虚拟机内部维护的Lua VM 栈
    • Lua VM 栈: 每个协程在创建时都会分配一个独立的 Lua VM 栈(或在需要时动态扩展)。这个栈存储着协程的局部变量、函数参数、中间表达式结果和函数调用上下文。
  • 实现机制:
    • coroutine.create(function) 创建一个新的协程(一个thread类型的值),但并不立即执行。它会分配并初始化一个新的 Lua VM 栈。
    • coroutine.yield(...) (保存栈): 当一个协程调用 yield 时,Lua VM 会:
      1. 保存当前 Lua VM 栈的完整状态(包括所有活跃的栈帧、局部变量值、程序计数器等)。这些信息会被存储在协程对象本身(在堆上分配)中。
      2. 暂停当前协程的执行。
      3. 将控制权返回给调用 coroutine.resume 的那个协程或主线程。 C 语言栈会正常展开,yield 作为一个 C 函数正常返回。
    • coroutine.resume(co, ...) (恢复栈): 当一个协程被 resume 时,Lua VM 会:
      1. 从协程对象中加载并恢复其之前保存的 Lua VM 栈状态。 这包括设置栈顶指针、恢复所有栈帧和程序计数器,使得协程能够从上次 yield 的点继续执行。
      2. 将控制权转移给被恢复的协程。 C 语言栈上会为 resume 函数创建一个新的栈帧,并在其中运行被恢复的 Lua 协程。
  • 优点: 简单、高效、避免了与 OS 栈相关的复杂性,并且由于是协作式的,没有竞态条件和锁的开销。
  • 缺点: 无法利用多核 CPU。

协程示例:

Lua - An Overview

Lua 和 LuaJIT

Lua

Lua 是一个开源项目,由巴西里约热内卢天主教大学的 Roberto Ierusalimschy、Luiz Henrique de Figueiredo 和 Waldemar Celes 创建。它的版本控制相对简单明了。

Lua 的版本体系

Lua 的版本号通常是 X.Y 的形式,例如 5.1, 5.2, 5.3, 5.4

  • X (主版本号/Major Version): 表示一个重大更新,通常会引入不兼容的更改(breaking changes),新的核心特性,或者对虚拟机架构的显著改进。从 5.x5.yy 增加通常意味着语法、API 或语义的更改,可能需要修改现有代码。
    • 例子:
      • Lua 5.0: 引入了协程 (coroutines)。
      • Lua 5.1: 引入了模块系统 (module system)、vararg 参数的改进。
      • Lua 5.2: 引入了 goto 语句、环境 (environments) 的重新设计、新的 _ENV 上值。
      • Lua 5.3: 引入了整数类型、位操作 (bitwise operations)、UTF-8 支持。
      • Lua 5.4: 引入了新的垃圾回收器、弱表 (weak tables) 的改进。
  • Y (次版本号/Minor Version) 或补丁版本 (Patch Version): 在主版本中,通常用于表示错误修复、性能优化或次要的功能增强。这些更改通常是向后兼容的(backward compatible),不会破坏现有代码运行。有时候,一个版本号会是 X.Y.Z 的形式,Z 就代表补丁版本。
    • 例子: Lua 5.3.1, Lua 5.3.2, Lua 5.3.3 等等。这些通常是修复 bug。

LuaJIT

https://github.com/LuaJIT/LuaJIT

Wasm Internals: Stack Machine

Wasm 中的“栈机”(Stack Machine),这正是其核心执行模型之一。Wasm 是一种基于栈的虚拟机,这意味着它的所有操作都通过从一个操作数栈中弹出值、执行操作并将结果压回栈中来完成。它没有传统的“寄存器”概念。

什么是栈机?

在计算机科学中,栈机是一种计算模型,其中指令操作数被隐含地从一个被称为“操作数栈”的内存区域中获取,并且结果被隐含地压回这个栈。这种模型与基于寄存器或基于累加器的模型形成对比。

Wasm 栈机的工作原理

Wasm 模块中的函数是由一系列指令组成的。这些指令会操作一个中央的操作数栈。

  1. 操作数栈(Operand Stack)

    • 这是 Wasm 执行函数时最核心的数据结构。
    • 所有的操作数(如整数、浮点数)和操作结果都临时存储在这个栈上。
    • 指令不会像在注册机中那样直接指定操作数的位置(如“将 R1 的值加到 R2”)。相反,它们会假定操作数已经在栈的顶部。
  2. 局部变量(Local Variables)

    • 除了操作数栈,每个函数调用还有一个独立的“局部变量”区域。
    • 局部变量是命名的存储位置,可以在函数的整个执行过程中被访问和修改。
    • 虽然局部变量不是栈的一部分,但有很多指令允许你将局部变量的值压入栈中,或者将栈顶部的值存储到局部变量中。
  3. 参数(Parameters)

    • 函数的参数在函数被调用时,会被初始化为局部变量的一部分(通常是前几个局部变量)。
    • 它们也可以被认为是函数执行上下文的一部分。
  4. 指令的操作

    • 压栈(Push):很多指令会将值压入栈中。例如:
      • i32.const 42:将整数常量 42 压入栈。
      • local.get <idx>:获取索引为 <idx> 的局部变量的值并压入栈。
    • 弹栈(Pop):大多数操作指令会从栈顶弹出所需数量的操作数。例如:
      • i32.add:弹出栈顶的两个 i32 整数,将它们相加,然后将结果压回栈。
      • if/else/loop 等控制流指令的条件值也会从栈中弹出。
    • 复合操作:一些指令可能弹出一个值,执行一些副作用(如内存写入),而不压入任何新值。例如:
      • i32.store:弹出内存地址和要存储的值,将值写入内存。

栈机模型的优势与特点

  1. 紧凑性(Compactness)

    • 指令更短,因为它们不需要编码操作数的位置。例如,一个加法操作,在寄存器机中可能需要指定两个源寄存器和一个目标寄存器;在栈机中,它只是一个简单的 add 指令。
    • 这有助于生成更小的二进制代码,对于 Web 环境中的快速下载和解析非常有利。
  2. 简化编译器后端(Simplified Compiler Backends)

    • IR(中间表示)到指令的映射通常更直接。许多高级语言的语义本身就可以很容易地映射到栈操作。
    • 这使得将 C/C++/Rust 等语言编译到 Wasm 变得相对容易。
  3. 易于验证(Easy to Validate)

    • Wasm 在加载时会进行严格的类型检查和结构验证。栈机模型使得验证其类型安全变得相对容易。例如,当检查 i32.add 指令时,验证器只需确保栈顶有两个 i32 类型的值。
    • 这对于沙盒环境中的安全性至关重要。
  4. 独立于目标架构(Architecture-Independent)

Kubernetes CSI 简介

K8s CSI 是 Kubernetes 中非常重要的一个组件,它解决了存储与计算分离的复杂性,并为容器化应用提供了持久化存储的能力。

1. 基本概念

CSI (Container Storage Interface) 译为 容器存储接口。它是由 Kubernetes 社区与存储厂商共同制定的一套标准接口规范。

在 CSI 出现之前,Kubernetes 存储插件的开发和管理存在以下痛点:

  • 紧耦合问题: Kubernetes 内部集成了大量的存储驱动(In-tree 存储插件),例如 AWS EBS、GCE PD、Azure Disk、Ceph RBD 等。这意味着每当存储厂商需要支持 Kubernetes 时,他们都必须将其存储驱动代码提交到 Kubernetes 的核心代码库中。这种方式导致了:
    • Kubernetes 代码库臃肿: 集成了大量存储逻辑,增加了核心代码的复杂性和维护难度。
    • 发布周期长: 存储驱动的更新需要跟随 Kubernetes 的发布周期,新功能和 bug 修复不能及时推送到用户。
    • 存储厂商开发受限: 每次更新都需要与 Kubernetes 社区协调,开发和测试流程繁琐。
  • 兼容性问题: 不同存储厂商的存储系统差异巨大,缺乏统一的接口规范,导致存储系统与 Kubernetes 之间的集成困难。

CSI 的目标就是解决这些问题,实现存储系统与 Kubernetes 的解耦。 它定义了一套通用的接口,允许任何存储厂商开发自己的 CSI 驱动,然后通过这些驱动来与 Kubernetes 进行交互,从而为容器提供存储服务。

1.1. 资源定义

1.1.1. Volumes

Volumes 是 Kubernetes 中 Pod 通过文件系统访问和共享数据的抽象。它主要提供了如下功能:

  • 通过 ConfigMap 或者 Secret 共享配置;
  • 跨容器、跨 Pod 甚至跨 Node 共享数据;
  • 数据持久化。在 Pod 销毁之后仍能继续访问数据。 对于 Pod 来说,Volumes 通过 .spec.volumes 提供给 Pod,容器通过 .spec.containers[*].volumeMounts 来将指定 Volumes 挂载到指定目录。

1.1.2. Persistent Volumes 和 Persistent Volumes Claim

PV 和 PVC 提供了两套 API 将存储的提供和消费分离。

VRF: An Overview

什么是 VRF(virtual routing forwarding)

VRF是一种实现三层网络隔离的关键技术。它通过创建多个路由表,为不同的网络流量提供独立的转发路径。 这意味着,任何三层网络结构,如接口的IP地址、静态路由的配置,甚至BGP(边界网关协议)会话,都可以被映射到特定的VRF中。这种映射机制就像是为每个VRF构建了一个独立的网络空间,彼此之间相互隔离,极大地增强了网络的安全性和管理的便利性。在MPLS VPN(多协议标签交换虚拟专用网络)等应用场景中,VRF为实现大规模的网络隔离和灵活的路由策略提供了基础框架。就像 VLAN 隔离了二层网络一样,VRF 隔离了三层网络。

5eb20c3e919fe3724b92c2ae7a66a7da_MD5

为什么需要 VRF

在 VRF 出现之前,Linux 用户主要采用两种方式来尝试实现类似的功能:策略路由(policy routing)和网络命名空间(net namespace)。然而,这两种方法都存在明显的局限性。

策略路由虽然能够通过多个路由表和策略规则来模拟 VRF 的部分功能(事实上,在 Linux 中,也是基于策略路由来去对 VRF 做的实现),但它的缺点十分突出。这种方式在配置和管理上非常复杂,难以确保网络隔离的有效性,在面对严格的网络审计时,往往无法通过。其复杂性不仅增加了运维的难度,还可能导致网络故障的风险上升,因此不被推荐使用。

网络命名空间在容器技术兴起后得到了广泛应用,它能够为容器提供全面的网络隔离。但在模拟 VRF 功能时,却显得有些“大材小用”。网络命名空间会对所有网络相关的资源进行完全隔离,包括设备、接口、ARP 表和路由表等。这意味着,即使是一些不需要隔离的服务,也会被隔离在不同的命名空间中。以 LLDP(链路层发现协议)为例,在使用网络命名空间的情况下,若要在不同的网络隔离环境中使用 LLDP,就需要在每个命名空间中单独运行实例,并且由于默认套接字相同,还需要为每个实例创建独特的套接字。这不仅增加了系统的开销,还使得管理变得更加复杂。相比之下,VRF 在隔离三层网络结构的同时,允许全局配置的共享和非三层感知服务的统一运行,大大提高了资源的利用效率。

Policy Routing VRF Net Namespace
隔离路由表 隔离三层网络 整个协议栈从二层到 socket 隔离

a9f7c55f8f39572d339b138fb1e12429_MD5c9b6614c864c7d35a8ef0a4f12ecdbfa_MD59edc8b051f504bf72140d1238513d687_MD5

VRF 配置

在 Linux 系统中配置 VRF,主要借助iproute2包来完成一系列操作。

  • 创建 VRF,并关联到 table 1
test1@test1:~$ ip link add vrf-1 type vrf table 1
test1@test1:~$ ip link set vrf-1 up
  • 添加接口到 VRF,可以看到 wg0 的 master 是 vrf-1,所有 wg0 的流量会使用关联的 vrf-1 路由表进行路由
test1@test1:~$ ip link set wg0 master vrf-1
test1@test1:~$ ip -d link show wg0 
9: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1400 qdisc noqueue master vrf-1 state UNKNOWN mode DEFAULT group default qlen 1000
    link/none  promiscuity 0 minmtu 0 maxmtu 2147483552 
    wireguard 
    vrf_slave table 1 addrgenmode none numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
  • 添加和查看 VRF 静态路由
root@test1:~# ip route add default via 10.1.0.10 vrf vrf-1
root@test1:~# ip route show table 1 
default via 10.1.0.10 dev wg0 
local 10.1.0.10 dev wg0 proto kernel scope host src 10.1.0.10 
root@test1:~# ip route show vrf vrf-1 
default via 10.1.0.10 dev wg0 

VRF 之间路由

有两种方法可以执行跨 VRF 路由。第一种方法涉及一个 VRF 的表中配置的路由,指向绑定到不同 VRF 的设备。