- 云原生
- 存储
- 高可用
- 容器编排
【免费下载链接】longhorn
Cloud-Native distributed storage built on and for Kubernetes
导读
本文围绕 Longhorn 加密卷的一个经典容量"缺口"问题展开:启用加密后,用户申请10 GiB的卷,实际在 Pod 内看到的块设备却是10 GiB - 16 MiB,这 16 MiB 被 LUKS2 加密头部(metadata header)占用。本文以增强提案 enhancements/20260413-luks2-header-space-preservation-for-encrypted-volumes.md 为核心骨架,从 LUKS2 头部结构讲起,深入分析 Longhorn 如何在后端层透明追加Requested Size + 16 MiB的物理容量,并完整覆盖重建(Rebuild)、备份恢复(Backup/Restore)、扩容(Expansion)、引擎在线/离线升级、虚拟机在线迁移(Live Migration)等全部受影响操作的实现与验证方案。读完本文,你将掌握:16 MiB 开销的精确构成、Longhorn 后端尺寸计算的源码级实现思路、新旧引擎镜像并存时的行为差异,以及一套可直接照做的加密卷容量验证测试清单。
问题背景:加密卷为何"凭空少 16 MiB"
当用户在 Kubernetes 中创建一个指定大小(例如10 GiB)且启用了加密的 Longhorn 卷时,Longhorn 需要在该卷的前部预留一部分空间存放 LUKS2 加密头部(LUKS2 metadata header)。在增强方案落地之前:
- 卷创建后,
lsblk输出与 Pod 内文件系统可用大小均表现为10 GiB - 16 MiB; - 用户感知到的容量与申请容量存在固定偏差,容易触发部分工作负载或数据库应用对块设备尺寸的严格校验(strict size validation checks)而部署失败。
从用户视角看,这 16 MiB 属于"凭空消失"的容量。本增强方案(关联 Issue:longhorn/longhorn#9205)的目标正是把这一开销透明化:让用户在加密卷上获得恰好等于申请值的可用容量,而 16 MiB 的额外空间由 Longhorn 在后端层自动追加并全程管理。
LUKS2 头部结构:16 MiB 是怎么算出来的
LUKS2 头部并非单一结构,而是由三部分组成:二进制/元数据(binary/metadata)、Keyslot 区(keyslot area)与对齐填充(alignment padding)。
+--------+--------+----------+---------+--------+----------+----------+ |Primary |First | Secondary| Second | KeySlot| Alignment| Encrypted| |binary |metadata| binary | metadata| area | padding | data | |header | | header | | | | | +--------+--------+----------+---------+--------+----------+----------+ |---------------------- LUKS2 Header ----------------------其总量满足如下关系:
Header Size = (Primary Metadata) + (Secondary Metadata) + (Keyslot Area) + (Alignment Padding)
三部分的默认规模如下:
- Keyslot Area(Keyslot 区):默认每个 keyslot 使用 4000 条 stripe,每条 stripe 占用 64 字节用于存放 master key 数据,因此单个 keyslot 的空间恰好为 8,000 KiB(约 7.8 MiB)。
- Binary & JSON Metadata(二进制与 JSON 元数据):二进制头部固定占用 4 KiB,标准使用场景下默认元数据分配为 12 KiB,两者合计(含 Primary/Secondary 双份布局)共 32 KiB。
- Alignment Padding(对齐填充):用于把加密数据的起始位置对齐到块(block)边界。块按块加密,一个块通常为 512 字节;该填充保证 LUKS 与加密扇区正确协作。
cryptsetup会将剩余约 8 MiB 作为空白填充,以确保文件系统与 SSD 的硬件块对齐。
三者相加即得到 16 MiB 的默认头部规模。值得注意的是,cryptsetup2.1.0 是"默认 LUKS2 头部大小变为 16 MiB"的确切最低版本——这是本方案中"16 MiB"这一数值的版本学依据(参见 Cryptsetup 2.1.0 Release Notes)。
影响头部大小的cryptsetup luksFormat参数
cryptsetup luksFormat执行时,以下参数决定最终头部大小:
--type:指定格式类型。Longhorn 默认使用luks2;若改用luks类型,头部将缩减为 2 MiB。--luks2-metadata-size与--luks2-keyslots-size:显式指定为 JSON 文本与二进制 keyslot 数据预留的空间,覆盖默认值。Longhorn 执行格式化命令时使用默认值。--offset:显式强制 payload 偏移到指定大小。该参数可用于确保头部大小保持固定。
补充说明:Longhorn 的加密卷运行依赖宿主机具备
dm_crypt内核模块并安装cryptsetup(详见加密方案文档 enhancements/20221024-pv-encryption.md)。这也解释了为何扩容与升级流程中需要先检查宿主机cryptsetup版本。
目标与非目标
Goals(目标)
- 将 16 MiB LUKS2 头部开销从用户视角中抽象掉;
- 让计算得到的后端尺寸(
Requested Size + 16 MiB)在所有操作中正确传递。
Non-goals(非目标)
- 在 Longhorn 升级时,为所有既有加密卷提供无需额外操作的自动透明迁移;
- 提供可配置的 LUKS2 头部/开销大小。
设计核心:后端尺寸的统一计算
方案的核心思路非常简洁:启动引擎(engine)与副本(replica)进程时,使用Engine.Spec.VolumeSize/Replica.Spec.VolumeSize的值,并在Volume.Spec.Encrypted为true时额外追加 16 MiB。
统一计算入口:getBackendSize
为把后端尺寸计算集中化,提案在go-common-libs/types/crypto.go中引入统一辅助函数,并定义常量LUKS2HeaderSize = 16 * 1024 * 1024(16 MiB):
const ( CryptoKeyProvider = "CRYPTO_KEY_PROVIDER" CryptoKeyValue = "CRYPTO_KEY_VALUE" ... LUKS2HeaderSize = 16 * 1024 * 1024 // 16 MiB ) func getBackendSize(requestedVolumeSize int64, encrypted bool, cliAPIVersion int, migrating bool) int64 { // The default LUKS2 header size is 16 MiB, so it must be added to the replica size when the volume is encrypted. Otherwise, the device // presented to the user will be 16 MiB smaller than the requested size. if encrypted && (cliAPIVersion >= 12 || migrating) { return requestedVolumeSize + LUKS2HeaderSize } return requestedVolumeSize }该函数有两个关键闸门:
cliAPIVersion >= 12:用 CLI API 版本区分"新建的加密卷"与"旧引擎镜像下的存量加密卷"——只有新引擎(CLI API v12 及以上)才追加 16 MiB;migrating:在线迁移路径上的兜底开关,保证迁移场景也按新尺寸计算。
API 版本升级:longhorn-engine/pkg/meta/version.go
为引入这一新的 API 层级,CLIAPIVersion需要递增 1:
const ( // CLIAPIVersion used to communicate with user e.g. longhorn-manager CLIAPIVersion = 12 CLIAPIMinVersion = 8 ... )CLIAPIMinVersion保持为 8,意味着新引擎仍可与旧版 longhorn-manager 通信,但只有双方都达到 v12 语义时才会启用 16 MiB 追加逻辑。
实例进程创建:engineapi/instance_manager.go
引擎与副本实例创建请求结构体分别新增Encrypted字段,并在发起 gRPCInstanceCreate时按数据引擎类型计算后端尺寸:
type EngineInstanceCreateRequest struct { Engine *longhorn.Engine Encrypted bool ... } type ReplicaInstanceCreateRequest struct { Replica *longhorn.Replica Encrypted bool ... } // EngineInstanceCreate creates a new engine instance func (c *InstanceManagerClient) EngineInstanceCreate(req *EngineInstanceCreateRequest) (*longhorn.InstanceProcess, error) { ... requestBackendSize := uint64(req.Engine.Spec.VolumeSize) case longhorn.DataEngineTypeV1: binary, args, err = getBinaryAndArgsForEngineProcessCreation(req.Replica, ..., req.Encrypted) case longhorn.DataEngineTypeV2: requestBackendSize = lhcrypto.getBackendSize(requestBackendSize, req.Encrypted, cliAPIVersion, false) ... instance, err := c.instanceServiceGrpcClient.InstanceCreate(&imclient.InstanceCreateRequest{ ... Size: requestBackendSize, Binary: binary, }) ... } // ReplicaInstanceCreate creates a new replica instance func (c *InstanceManagerClient) ReplicaInstanceCreate(req *ReplicaInstanceCreateRequest) (*longhorn.InstanceProcess, error) { ... if types.IsDataEngineV1(req.Replica.Spec.DataEngine) { binary, args = getBinaryAndArgsForReplicaProcessCreation(req.Replica, ..., req.Encrypted) } requestBackendSize := uint64(req.Replica.Spec.VolumeSize) if types.IsDataEngineV2(req.Replica.Spec.DataEngine) { requestBackendSize = lhcrypto.getBackendSize(requestBackendSize, req.Encrypted, cliAPIVersion, false) ... } ... instance, err := c.instanceServiceGrpcClient.InstanceCreate(&imclient.InstanceCreateRequest{ ... Size: requestBackendSize, Binary: binary, }) ... }从实现结构可以看出:v1(基于 longhorn-engine 进程)与 v2(基于 SPDK)数据引擎的接入方式不同——v1 通过启动参数(--encrypted)传递加密标记,v2 则在创建请求阶段直接把Size换成含 16 MiB 的后端尺寸。
副本代理:engineapi/proxy_replica.go
副本添加(ReplicaAdd)、重建(Rebuild)、恢复(Restore)都经由引擎代理发起。代理必须把正确的卷尺寸传给实例,才能让物理磁盘分配量正确:
func (p *Proxy) ReplicaAdd(e *longhorn.Engine, replicaName, replicaAddress string, restore, fastSync bool, ...) (err error) { cliAPIVersion, err := ec.ds.GetDataEngineImageCLIAPIVersion(e.Spec.Image, e.Spec.DataEngine) volumeSize := lhcrypto.getBackendSize(e.Spec.VolumeSize, e.Spec.VolumeEncrypted, cliAPIVersion, false) volumeCurrentSize := lhcrypto.getBackendSize(e.Status.CurrentSize, e.Spec.VolumeEncrypted, cliAPIVersion, false) return p.grpcClient.ReplicaAdd(string(e.Spec.DataEngine), e.Name, e.Spec.VolumeName, p.DirectToURL(e), replicaName, replicaAddress, restore, volumeSize, volumeCurrentSize, int(replicaFileSyncHTTPClientTimeout), fastSync, localSync, grpcTimeoutSeconds) }注意这里同时处理了Spec.VolumeSize(目标尺寸)与Status.CurrentSize(当前实际尺寸)两个维度,确保重建过程中目标副本从一开始就按VolumeSize + 16 MiB预分配。
引擎侧加密标记:longhorn-engine/app/cmd/replica.go
引擎侧新增一个--encrypted布尔参数,用于标记卷已加密,并在副本启动命令中透传:
func ReplicaCmd() cli.Command { ... Flags: []cli.Flag{ ... cli.BoolFlag{ Name: "encrypted", Hidden: false, Usage: "Volume is encrypted", }, }, Action: func(c *cli.Context) { if err := startReplica(c); err != nil { logrus.WithError(err).Fatalf("Error running start replica command") } }, ... }副本打开时的按需扩容:longhorn-engine/pkg/replica/server.go
Open流程判断副本镜像文件是否需要在打开时扩容。这是旧卷在升级后获得正确后端尺寸的关键钩子:
func (s *Server) Open(isUpgrade bool, expectedBackendSize int64) error { ... state, info := s.Status() sectorSize := s.getSectorSize() logrus.Infof("Opening replica: dir %s, size %d, sector size %d, state: %v, upgrading: %v", s.dir, info.Size, sectorSize, state, isUpgrade) expandingEncryptedDevSize := int64(0) // 0 means it doesn't need to expand the size for the encrypted volume. if isExpandingEncryptedDevRequired(state, s.encrypted, info.Size, expectedBackendSize, isUpgrade) { expandingEncryptedDevSize = expectedBackendSize } r, err := New(s.ctx, info.Size, sectorSize, s.dir, s.backing, s.revisionCounterDisabled, s.unmapMarkDiskChainRemoved, s.snapshotMaxCount, r.snapshotMaxSize, s.encrypted, expandingEncryptedDevSize) ... }在longhorn-engine/pkg/replica/replica.go的construct中,当副本镜像已存在且expectedBackendSize > 0时,会将副本镜像文件扩展到期望的后端尺寸:
func construct(ctx context.Context, readonly bool, size, sectorSize int64, dir, head string, backingFile *backingfile.BackingFile, disableRevCounter, unmapMarkDiskChainRemoved bool, snapshotMaxCount int, snapshotMaxSize int64, encrypted bool, expectedBackendSize int64) (*Replica, error) { ... if exists { if err := r.openLiveChain(); err != nil { return nil, err } if expectedBackendSize > 0 { // expand the replica image file to expectedBackendSize size. } } ... }受影响操作的行为变化
卷引擎升级(Volume Upgrade)
- 离线升级(Offline Upgrade):沿用现有离线引擎升级流程。引擎升级完成后,卷重新挂载(attach)时,打开副本的过程会检查副本镜像文件大小是否需要扩展(即触发上文
Open中的按需扩容逻辑)。 - 在线升级(Live Upgrade):必须先扩展后端尺寸,再进行在线升级。
controller/engine_controller.go的syncEngine在检测到升级副本地址映射非空、且当前镜像与目标镜像不一致时,若卷已加密,会先调用工具函数判断宿主cryptsetup是否属于"默认 16 MiB 头部"的版本,再通过expandEncryptedVolumeBeforeLiveUpgrade预先扩容:
func (ec *EngineController) syncEngine(key string) error { ... syncReplicaAddressMap := false if len(engine.Spec.UpgradedReplicaAddressMap) != 0 && engine.Status.CurrentImage != engine.Spec.Image { if volume.Spec.Encrypted { is16MiBHeaderPkgVersion, err := util.IsCryptsetupVerWithFixed16MiBHeaderSize() ... if is16MiBHeaderPkgVersion { if isExpanding, err := ec.expandEncryptedVolumeBeforeLiveUpgrade(volume, engine); err != nil || isExpanding { // Wait for the expected volume size to be updated before engine live upgrade for encrypted volume return err } } } if err := ec.Upgrade(engine, log); err != nil { // Engine live upgrade failure shouldn't block the following engine state update. log.WithError(err).Error("Failed to run engine live upgrade") // Sync replica address map as usual when the upgrade fails. syncReplicaAddressMap = true } } ... }副本重建(Rebuilding)
重建期间,引擎控制器指示新副本按正确尺寸供给空间;数据同步重建目标块,由于底层块设备文件容纳了VolumeSize + 16 MiB,快照树与原始同步(raw sync)可以安全地同时承载 LUKS 元数据与加密后的负载数据。
新旧引擎镜像的差异:对于仍使用旧引擎镜像的存量加密卷,重建继续沿用Replica.Spec.VolumeSize(不加 16 MiB),行为与当前实现保持一致。
备份与恢复(Backup and Restore)
用户从备份恢复时,应为恢复出的新卷正确设置Volume.Spec.Encrypted。卷控制器依据该字段判断备份是否源自加密卷,并据此计算用户申请的Volume.Spec.Size,以正确尺寸创建引擎与副本实例进程。因此"加密卷备份 → 恢复为加密卷"才能还原出完整容量;反之,恢复为未加密卷会因设备不匹配而无法正常挂载(详见下文测试计划)。
扩容(Expanding)
新引擎镜像下的加密卷扩容:
- 收到扩容请求,更新
Volume.Spec.Size; - 扩容前先检查宿主机
cryptsetup版本; - 卷控制器将
Engine.Spec.VolumeSize、Replica.Spec.VolumeSize更新为Volume.Spec.Size; - 引擎与副本控制器计算新的后端尺寸
NewBackendSize = NewVolumeSize + 16 MiB,并以正确后端尺寸启动扩容过程。
旧引擎镜像下的存量加密卷扩容:
- 收到扩容请求,更新
Volume.Spec.Size; - 卷控制器更新
Engine.Spec.VolumeSize、Replica.Spec.VolumeSize为Volume.Spec.Size; - 引擎控制器直接用
Engine.Spec.VolumeSize启动扩容过程(不加 16 MiB)。
对应的代理层实现位于engineapi/proxy_volume.go:
func (p *Proxy) VolumeExpand(e *longhorn.Engine) (err error) { v, err := p.ds.GetVolumeRO(e.Spec.VolumeName) cliAPIVersion, err := p.ds.GetDataEngineImageCLIAPIVersion(e.Spec.Image, e.Spec.DataEngine) return p.grpcClient.VolumeExpand(string(e.Spec.DataEngine), e.Name, e.Spec.VolumeName, p.DirectToURL(e), lhcrypto.getBackendSize(e.Spec.VolumeSize, v.Spec.Encrypted, cliAPIVersion, false)) }虚拟机在线迁移(Live Migration)
当用户尝试用旧引擎镜像对加密卷做在线迁移时:
- 收到在线迁移请求;
- 校验器(validators)检查引擎镜像是否为旧版本:
- 是旧版本:直接返回错误,提示必须先完成引擎升级再做在线迁移;
- 新版本:正常开始在线迁移。
校验逻辑分布在两处 webhook:
webhook/resources/volume/validator.go的Update:在原有的"迁移过程中禁止修改MigrationNodeID"校验之外,追加对newVolume.Spec.MigrationNodeID已设置且引擎镜像CLIAPIVersion为 11(即旧版)的拒绝逻辑;webhook/resources/volumeattachment/validator.go的verifyTicketCountForMigratableVolume:当 migratable 卷出现第二个 CSI ticket(numCSITickets == 2)且卷状态为VolumeStateAttached时,同样检查引擎镜像CLIAPIVersion是否为 11,是则拒绝。
func (v *volumeValidator) Update(request *admission.Request, oldObj runtime.Object, newObj runtime.Object) error { ... // prevent the changing v.Spec.MigrationNodeID to different node when the volume is doing live migration (when v.Status.CurrentMigrationNodeID != "") if newVolume.Status.CurrentMigrationNodeID != "" && newVolume.Spec.MigrationNodeID != oldVolume.Spec.MigrationNodeID && newVolume.Spec.MigrationNodeID != newVolume.Status.CurrentMigrationNodeID && newVolume.Spec.MigrationNodeID != "" { err := fmt.Errorf("cannot change v.Spec.MigrationNodeID to node %v when the volume is doing live migration to node %v ", newVolume.Spec.MigrationNodeID, newVolume.Status.CurrentMigrationNodeID) return werror.NewInvalidError(err.Error(), "") } // Check if the newVolume.Spec.MigrationNodeID is set and // Check if the engine image CLIAPIVersion is 11 // If yes, reject the request. ... }func (v *volumeAttachmentValidator) verifyTicketCountForMigratableVolume(va *longhorn.VolumeAttachment, vol *longhorn.Volume) error { ... switch { ... case numCSITickets == 2: if vol.Status.State != longhorn.VolumeStateAttached { msg := fmt.Sprintf("cannot have second CSI ticket for migratable volume %v while it is in state %v", vol.Name, vol.Status.State) return werror.NewInvalidError(msg, "spec.attachmentTickets") } // Check if the engine image CLIAPIVersion is 11 // If yes, reject the request. return nil ... } ... }升级计划(Upgrade Plan)
存量加密卷:在升级到新引擎镜像之前,不做任何额外处理。
新建卷:
- 将
CLIAPIVersion递增 1,引入新的 API 层级,以区分存量加密卷与新建加密卷; - 当以
CLIAPIVersion >= 12创建/重建/扩容加密卷时,为引擎与副本追加额外的 16 MiB 空间。
存量加密卷升级过程:
CLIAPIVersion < 12时沿用当前实现,不追加 16 MiB,直到完成引擎升级;- 引擎升级期间:
- 先在宿主机检查
cryptsetup版本; - 触发一次隐式卷扩容(implicit volume expansion),使加密卷后端尺寸变为正确值;
- 扩容过程中的任何错误都应返回错误或通过事件记录,且不影响其他卷操作;
- 先在宿主机检查
- 升级完成后,宿主机上的副本镜像文件被扩展为"卷大小 + 16 MiB",工作负载内的设备大小与申请值一致。
下图展示了加密卷引擎升级过程的完整流程:
用户视角:完全透明,无需改配置
从最终用户角度看,该功能完全透明,不需要任何配置变更:
- 用户创建标准 PVC,StorageClass 指向加密 Longhorn StorageClass,并请求
1Gi存储(例如spec.resources.requests.storage: 1Gi); - Longhorn 自动创建
Size: 1073741824(1 GiB)的卷,同时内部将后端存储分配调整为1 GiB + 16 MiB; - 工作负载挂载卷后,运行
lsblk或df -h,报告的分区大小恰好为1G,而非此前的1008M; - 备份恢复、快照、卷扩容都隐式遵循这一计算。例如用户把卷扩容到
2 GiB,Longhorn UI 与 Kubernetes PVC 都显示2 GiB,而后端加密副本自动扩展为2 GiB + 16 MiB。存量旧卷默认保持原有尺寸行为,只有按上文升级计划显式升级后才采用新尺寸计算。
加密卷的标准创建配置
结合仓库中的实际示例(examples/crypto/storageclass-crypto-global.yaml、examples/crypto/secret-crypto-global.yaml),一个采用全局密钥的加密 StorageClass 与 Secret 配置如下:
# StorageClass:加密卷入口 kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: longhorn-crypto-global provisioner: driver.longhorn.io allowVolumeExpansion: true parameters: numberOfReplicas: "3" staleReplicaTimeout: "2880" # 48 hours in minutes fromBackup: "" encrypted: "true" # Do not set provisioner-secret-* parameters: the CSI provisioner cannot read Secrets. # Use node-side Secret references for encrypted volumes. # Ensure the referenced Secret exists before starting a workload. csi.storage.k8s.io/node-publish-secret-name: "longhorn-crypto" csi.storage.k8s.io/node-publish-secret-namespace: "longhorn-system" csi.storage.k8s.io/node-stage-secret-name: "longhorn-crypto" csi.storage.k8s.io/node-stage-secret-namespace: "longhorn-system" # These two are for online expansion of encrypted volumes. csi.storage.k8s.io/node-expand-secret-name: "longhorn-crypto" csi.storage.k8s.io/node-expand-secret-namespace: "longhorn-system"# Secret:全局加密密钥(存储于 longhorn-system 命名空间) --- apiVersion: v1 kind: Secret metadata: name: longhorn-crypto namespace: longhorn-system stringData: CRYPTO_KEY_VALUE: "Simple passphrase" CRYPTO_KEY_PROVIDER: "secret" # this is optional we currently only support direct keys via secrets创建该 StorageClass 与 Secret 后,即可通过引用longhorn-crypto-global的 PVC 获得容量精确的加密卷。关于加密卷的完整背景(dm_crypt内核模块依赖、密钥参数CRYPTO_KEY_CIPHER/CRYPTO_KEY_HASH/CRYPTO_KEY_SIZE、CSI 加解密流程等),可继续阅读 enhancements/20221024-pv-encryption.md。
测试计划:如何验证 16 MiB 开销已透明化
增强提案给出了六组可直接执行的验证场景,覆盖新建、扩容、重建、备份恢复、引擎在线/离线升级与虚拟机迁移:
1. 新建卷(New Volume Creation)
- 创建一个
1 GiB加密卷; - 挂载到 Pod 后,在 Pod 内运行
lsblk或fdisk -l; - 预期结果:块设备显示恰好为
1G、1073741824 bytes(而不是1008M、1056964608 bytes); - 在卷所挂载的节点上,验证后端副本文件大小为
1 GiB + 16 MiB。
2. 卷扩容(Volume Expansion)
新引擎镜像 + 新建加密卷:
- 创建新的
1 GiB加密卷并扩容到2 GiB; - 等待扩容完成;
- 预期结果:Pod 内
lsblk/fdisk -l显示2G;工作节点上副本镜像文件为2 GiB + 16 MiB。
旧引擎镜像 + 存量加密卷:
- 将旧引擎下的
1 GiB加密卷扩容到2 GiB; - 等待扩容完成;
- 预期结果:Pod 内显示
1.9G;工作节点上副本镜像文件为2 GiB(不带 16 MiB)。
3. 副本重建(Replica Rebuild)
新建加密卷:
- 删除已挂载
1 GiB加密卷的一个副本; - 等待 Longhorn 自动重建降级副本;
- 预期结果:重建成功,新副本文件大小与其他副本一致(
1 GiB + 16 MiB),且数据完整性(例如存储文件的md5sum)保持一致。
旧引擎镜像存量卷:
- 删除已挂载
1 GiB加密卷的一个副本; - 等待自动重建;
- 预期结果:重建成功,新副本文件大小与其他副本一致(
1 GiB),数据完整性(md5sum)保持一致。
4. 备份恢复(Backup Restore)
- 创建
1 GiB加密卷,写入 100 MiB 负载并计算校验和; - 备份到远端备份服务器;
- 以
Volume.Spec.Encrypted为 true 恢复到新卷并挂载到工作负载; - 预期结果:恢复卷在工作负载内呈现恰好
1.0G,负载校验和匹配; - 再以
Volume.Spec.Encrypted为 false 恢复同一备份; - 预期结果:该卷无法挂载到工作负载(设备与加密元数据不匹配)。
5. 引擎在线升级(Engine Live Upgrade)
- 用旧引擎镜像创建
1 GiB加密卷并挂载到工作负载; - 确认工作节点上副本镜像文件为
1 GiB、工作负载内设备小于1 GiB; - 写入 100 MiB 负载并计算校验和;
- 将卷引擎镜像在线升级到新版本;
- 预期结果:副本镜像文件变为
1 GiB + 16 MiB,工作负载内设备为1 GiB,负载校验和匹配。
6. 引擎离线升级(Engine Offline Upgrade)
- 用旧引擎镜像创建
1 GiB加密卷并挂载; - 确认副本镜像文件为
1 GiB、设备小于1 GiB; - 写入 100 MiB 负载并计算校验和;
- 缩容工作负载、确保卷已分离;
- 升级卷引擎镜像到新版本后重新扩容工作负载;
- 预期结果:副本镜像文件为
1 GiB + 16 MiB,设备为1 GiB,校验和匹配。
7. 虚拟机在线迁移(VM Live Migration)
- 安装带 Longhorn v1.11.x 与 KubeVirt 的 k3s 集群;
- 创建带
1 GiBLonghorn 加密卷的 VM; - 确认 VM 内卷呈现
1.0G - 16 MiB; - 在 VM 内写入 100 MiB 负载并计算校验和;
- 升级 Longhorn 至 v1.12;
- 在升级卷引擎之前发起在线迁移;
- 预期结果:收到 Longhorn 校验器返回的错误;
- 升级卷引擎后再次发起在线迁移;
- 预期结果:迁移完成,迁移后卷在 VM 内呈现
1.0G,负载校验和匹配。
小结
本增强方案通过"后端追加 16 MiB、用户侧完全透明"的设计,彻底解决了 Longhorn 加密卷容量少 16 MiB 的体验问题。其实现要点可概括为三句话:统一计算函数getBackendSize以CLIAPIVersion >= 12与migrating为闸门决定是否追加开销;引擎/副本实例创建、副本代理、扩容代理等所有尺寸入口统一改用该函数;升级与迁移路径通过校验器、隐式扩容与副本打开时按需扩展完成存量卷的平滑过渡。对于使用加密卷且对块设备尺寸有严格校验的工作负载(如部分数据库应用),该方案上线后即可直接获得与 PVC 申请值完全一致的可用容量。
延伸阅读:本增强提案原文见 enhancements/20260413-luks2-header-space-preservation-for-encrypted-volumes.md,加密卷功能的完整背景见 enhancements/20221024-pv-encryption.md,可运行的 StorageClass/Secret 示例见 examples/crypto/storageclass-crypto-global.yaml 与 examples/crypto/secret-crypto-global.yaml。
- 云原生
- 存储
- 高可用
- 容器编排
【免费下载链接】longhorn
Cloud-Native distributed storage built on and for Kubernetes
相关推荐
Talos Linux 用户卷 UserVolumeConfig 配置完全指南:directory/disk/partition 三种卷类型与 LUKS2 加密实战
Talos Linux 用户卷 UserVolumeConfig 配置完全指南:directory/disk/partition 三种卷类型与 LUKS2 加密
云原生操作系统容器编排PyHessian核心原理深度解析:从数学理论到代码实现
PyHessian核心原理深度解析:从数学理论到代码实现 PyHessian是一个基于PyTorch的神经网络二阶优化分析库,它能够帮助开发者深入理解神经网络的
如何在MonoGame中高效生成字体图集:从基础到高级优化
如何在MonoGame中高效生成字体图集:从基础到高级优化 在跨平台游戏开发中,字体渲染性能直接影响游戏体验。MonoGame作为强大的游戏开发框架,提供了完整
游戏开发图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考