1. 什么是 NBD (Network Block Device)?
NBD (Network Block Device) 是一种 Linux 协议,允许通过网络访问块设备。在 E2B 的架构中,NBD 主要用于在宿主机上将构建好的 RootFS(通常是只读基础镜像 + 可写层的 Overlay)映射为一个块设备(例如 /dev/nbd0)。Firecracker MicroVM 需要一个块设备来作为其根文件系统启动,NBD 充当了文件系统(OverlayFS)与 Firecracker 块设备驱动之间的桥梁。
2. NBD 设备池的管理 (Device Pool)
E2B 通过 DevicePool 结构体来管理宿主机上的 NBD 设备槽位。为了高性能,它预先计算并维护一组可用的 NBD 槽位。
2.1 初始化与检测 (Initialization)
NewDevicePool 负责初始化池。它首先读取 /sys/module/nbd/parameters/nbds_max 来确定系统支持的最大 NBD 设备数量,并初始化一个 bitset 来跟踪使用情况。
func NewDevicePool() (*DevicePool, error) {
maxDevices, err := getMaxDevices()
if err != nil {
return nil, fmt.Errorf("failed to get max devices: %w", err)
}
if maxDevices == 0 {
return nil, errors.New("max devices is 0")
}
pool := &DevicePool{
done: make(chan struct{}),
usedSlots: bitset.New(maxDevices),
// 创建一个缓冲通道,用于存放准备好的槽位
slots: make(chan DeviceSlot, int(math.Min(maxSlotsReady, float64(maxDevices)))),
}
return pool, nil
}
func getMaxDevices() (uint, error) {
// 读取内核参数确定最大支持数量
data, err := os.ReadFile("/sys/module/nbd/parameters/nbds_max")
if errors.Is(err, os.ErrNotExist) {
return 0, ErrNBDModuleNotLoaded
}
// ... (省略错误处理和解析代码)
return uint(maxDevices), nil
}
2.2 预填充 (Populate)
Populate 方法是一个后台进程,它不断寻找空闲的 NBD 设备并将它们放入 slots 通道中,以供消费者快速获取。这避免了在请求到来时进行昂贵的查找操作。
func (d *DevicePool) Populate(ctx context.Context) {
defer close(d.slots)
failedCount := 0
for {
select {
case <-ctx.Done():
return
case <-d.done:
return
default:
}
// 获取一个空闲的设备槽位
device, err := d.getFreeDeviceSlot()
if err != nil {
// ... (省略错误日志和重试逻辑)
continue
}
// ...
// 将找到的空闲设备放入通道
select {
case <-ctx.Done():
return
case <-d.done:
return
case d.slots <- *device:
// sent successfully
}
}
}
2.3 申请与检查 (Acquisition)
GetDevice 只是简单地从 slots 通道中读取。真正的查找逻辑在 getFreeDeviceSlot 和 isDeviceFree 中。代码通过检查 /sys/block/nbd%d/pid (进程ID) 和 /sys/block/nbd%d/size (大小) 来判断设备是否真的空闲。
// GetDevice returns a slot if there is one available.
func (d *DevicePool) GetDevice(ctx context.Context) (DeviceSlot, error) {
select {
case <-d.done:
return 0, ErrClosed
case <-ctx.Done():
return 0, ctx.Err()
case slot := <-d.slots:
acquired.Add(ctx, 1)
slotCounter.Add(ctx, -1)
return slot, nil
}
}
func (d *DevicePool) isDeviceFree(slot DeviceSlot) (bool, error) {
// 检查 pid 文件,如果存在说明设备正在使用
pidFile := fmt.Sprintf("/sys/block/nbd%d/pid", slot)
_, err := os.Stat(pidFile)
if err == nil {
return false, nil
}
// ...
// 检查 size 文件,如果 size > 0 说明设备被映射了
sizeFile := fmt.Sprintf("/sys/block/nbd%d/size", slot)
data, err := os.ReadFile(sizeFile)
// ...
size, err := strconv.ParseUint(sizeStr, 10, 64)
// ...
return size == 0, nil
}
2.4 释放 (Release)
ReleaseDevice 用于释放槽位。它会再次确认设备在操作系统层面是否真的空闲,如果空闲,则清除 bitset 中的标记,使其可以被再次分配。
func (d *DevicePool) release(ctx context.Context, idx DeviceSlot) error {
// 双重检查设备是否真的空闲
free, err := d.isDeviceFree(idx)
if err != nil {
return fmt.Errorf("failed to check if device is free: %w", err)
}
if !free {
return DeviceInUseError{}
}
// 在位图中清除占用标记
d.mu.Lock()
d.usedSlots.Clear(uint(idx))
d.mu.Unlock()
released.Add(ctx, 1)
return nil
}
3. Firecracker 的使用
E2B 使用 apiClient 结构体通过 Unix Socket 与 Firecracker 进程进行 HTTP 通信。它配置 VM 的启动源(Kernel)和驱动器(RootFS,即上述的 NBD 设备)。
3.1 客户端初始化
Firecracker 客户端通过 Unix Socket 建立连接。
func newApiClient(socketPath string) *apiClient {
client := client.NewHTTPClient(strfmt.NewFormats())
transport := firecracker.NewUnixSocketTransport(socketPath, nil, false)
client.SetTransport(transport)
return &apiClient{
client: client,
}
}
3.2 配置 RootFS 驱动器
setRootfsDrive 方法将宿主机上的路径(这里就是 NBD 设备的路径,如 /dev/nbd0)配置为 Firecracker 的根设备。
func (c *apiClient) setRootfsDrive(ctx context.Context, rootfsPath string, ioEngine *string) error {
rootfs := "rootfs"
isRootDevice := true
driversConfig := operations.PutGuestDriveByIDParams{
Context: ctx,
DriveID: rootfs,
Body: &models.Drive{
DriveID: &rootfs,
PathOnHost: rootfsPath, // 这里传入 NBD 设备路径
IsRootDevice: &isRootDevice,
IsReadOnly: false,
IoEngine: ioEngine,
},
}
_, err := c.client.Operations.PutGuestDriveByID(&driversConfig)
if err != nil {
return fmt.Errorf("error setting fc drivers config: %w", err)
}
return nil
}
3.3 配置启动源 (Boot Source)
setBootSource 配置内核路径和启动参数。
func (c *apiClient) setBootSource(ctx context.Context, kernelArgs string, kernelPath string) error {
bootSourceConfig := operations.PutGuestBootSourceParams{
Context: ctx,
Body: &models.BootSource{
BootArgs: kernelArgs,
KernelImagePath: &kernelPath,
},
}
_, err := c.client.Operations.PutGuestBootSource(&bootSourceConfig)
return err
}
3.4 启动 VM
最后,通过调用 CreateSyncAction 并传入 InstanceStart 来启动虚拟机。
func (c *apiClient) startVM(ctx context.Context) error {
start := models.InstanceActionInfoActionTypeInstanceStart
startActionParams := operations.CreateSyncActionParams{
Context: ctx,
Info: &models.InstanceActionInfo{
ActionType: &start,
},
}
_, err := c.client.Operations.CreateSyncAction(&startActionParams)
if err != nil {
return fmt.Errorf("error starting fc: %w", err)
}
return nil
}