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
}