Linux 内核 dm-zoned 完全指南:在 Device Mapper 层将分区块设备包装为普通块设备

发布时间:2026/9/8 18:19:58
Linux 内核 dm-zoned 完全指南:在 Device Mapper 层将分区块设备包装为普通块设备 Linux 内核 dm-zoned 完全指南在 Device Mapper 层将分区块设备包装为普通块设备【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxdm-zoned 是 Linux 内核 Device Mapper 框架下的一个 target它把底层符合 ZBCSCSI或 ZACATA规范的分区块设备zoned block device透明地包装成一个不受任何写模式约束的常规块设备供文件系统或直接访问块设备的应用直接使用。本文以官方文档 Documentation/admin-guide/device-mapper/dm-zoned.rst 为骨架结合本仓库 drivers/md/ 下的源码实现完整梳理 dm-zoned 的架构设计、写缓冲算法、磁盘元数据布局、元数据保护机制、reclaim 回收过程与 dmzadm/dmsetup 实际使用方法。读完本文你将理解 host-managed 与 host-aware 两种分区块设备模型下 dm-zoned 的工作原理并掌握从格式化、加载 target 到监控状态与手动触发回收的完整实操链路。一、dm-zoned 解决的问题与设计目标1.1 背景分区块设备的写约束分区块设备zoned block device将存储空间划分为固定大小的 zone。其中sequential zone顺序写 zone要求写入必须严格从 zone 写指针write pointer位置顺序推进任何乱序写、覆盖写都会被设备拒绝只有conventional zone常规 zone即随机写 zone允许任意地址随机写入。文档中将其划分为 ZBCSCSI 设备与 ZACATA 设备两个规范家族。这一约束让上层文件系统与裸设备应用难以直接使用这类硬盘。dm-zoned 正是在此背景下提出的内核级解决方案。1.2 dm-zoned 的目标形态dm-zoned.rst 明确指出dm-zoned 将一块 ZBC/ZAC 分区块设备暴露为无任何写模式约束的常规块设备等效于实现了一块“drive-managed zoned block device设备托管型分区块设备”。其价值在于对host-managed主机托管型设备向用户文件系统或裸设备应用隐藏顺序写约束对host-aware主机感知型设备缓解设备端因过多随机写导致的潜在性能劣化。官方文档对实现的总体评价是“simple and minimizes system overhead”——即在 CPU 开销、内存占用与存储容量损失三个方面都做到最小化。文档给出的量化参考对于一块10TB、256MB zone的 host-managed 硬盘每个磁盘实例的 dm-zoned 内存占用最多不超过 4.5MB且内部仅需约 5 个 zone用于存放元数据与执行回收reclaim操作。该开销极小的特性与下文 4KB 逻辑块、压缩化元数据布局的设计直接相关。二、核心架构两类 zone、两级设备与 4KB 逻辑块2.1 zone 的功能划分dm-zoned 把设备上或多个设备的全部 zone 分成两类见 dm-zoned.rst元数据 zoneMetadata zones属于 conventional zone用于保存 dm-zoned 自身管理所需元数据。这部分 zone不会作为可用容量上报给用户。数据 zoneData zones其余全部 zone。其中绝大部分是专门存放用户数据的 sequential zone设备的 conventional zone 也可用于缓冲用户随机写——即写入的数据可以先把常规 zone 当作随机写缓冲之后再搬移到 sequential zone把常规 zone 腾出来继续承接新的随机写流量。在源码 drivers/md/dm-zoned.h 中zone 的用途通过位标志刻画DMZ_META元数据、DMZ_DATA数据、DMZ_BUF缓冲区、DMZ_RESERVED保留再叠加写类型DMZ_RND随机 zone、DMZ_SEQ顺序 zone以及较新版本引入的DMZ_CACHE缓存 zone供dmz_is_meta/dmz_is_data/dmz_is_buf/dmz_is_seq/dmz_is_rnd等访问器使用dm-zoned.h。2.2 常规块设备 分区块设备的双设备组合dm-zoned 不仅支持单块分区块设备还支持将一个常规块设备与分区块设备组合使用文档见 dm-zoned.rst。其做法是常规块设备被按分区块设备的 zone 大小做逻辑切分切分出的“逻辑 zone”放在分区块设备的所有 zone之前这些逻辑 zone 被当作 conventional zone 使用充当缓存/随机写缓冲实际数据最终仍沉淀到真正的分区块设备上。这一点在较新源码中体现为每个struct dmz_dev持有zone_offset该设备 zone 号的起始偏移与dev_idx设备序号并通过设备标志DMZ_BDEV_REGULAR标识常规块设备dm-zoned.h。多设备场景下第一个设备通常只承担 cache常规随机写缓冲zone 的角色。2.3 固定的 4KB 逻辑块与扇区dm-zoned 暴露的逻辑设备扇区大小固定为 4096 字节与底层分区块设备的物理扇区大小无关。文档给出的理由是更大的逻辑块粒度可以显著压缩用于管理“有效块已写且未被丢弃的块”的元数据量。源码通过一组宏将其固化drivers/md/dm-zoned.h/* dm-zoned creates block devices with 4KB blocks, always. */ #define DMZ_BLOCK_SHIFT 12 #define DMZ_BLOCK_SIZE (1 DMZ_BLOCK_SHIFT) #define DMZ_BLOCK_MASK (DMZ_BLOCK_SIZE - 1) #define DMZ_BLOCK_SECTORS_SHIFT (DMZ_BLOCK_SHIFT - SECTOR_SHIFT) #define DMZ_BLOCK_SECTORS (DMZ_BLOCK_SIZE SECTOR_SHIFT) #define DMZ_BLOCK_SECTORS_MASK (DMZ_BLOCK_SECTORS - 1) /* 4KB block - 512B sector conversion. */ #define dmz_blk2sect(b) ((sector_t)(b) DMZ_BLOCK_SECTORS_SHIFT) #define dmz_sect2blk(s) ((sector_t)(s) DMZ_BLOCK_SECTORS_SHIFT)即内部所有记账都以 4KB 块为单位块号与 512B 扇区之间通过移位换算bio 在进入目标时由dmz_bio_block()/dmz_bio_blocks()归一化为块号。2.4 逻辑地址到 zone 的按 chunk 映射dm-zoned 的逻辑设备地址空间被切分为chunkchunk 大小等于底层 zone 大小。每个 chunk 对应一条映射记录指示存放该 chunk 数据的 zone。源码 drivers/md/dm-zoned.h 中的两个宏清晰地表达了这一点#define dmz_bio_chunk(zmd, bio) ((bio)-bi_iter.bi_sector \ dmz_zone_nr_sectors_shift(zmd)) #define dmz_chunk_block(zmd, b) ((b) (dmz_zone_nr_blocks(zmd) - 1))bio 起始扇区右移zone_nr_sectors_shift位即得到 chunk 号dmz_chunk_block则把块号投影到 chunk 内部等价于 mod zone 块数。这种设计保证了一个 chunk 恰好对应一个 zone 的容量简化了映射与回收时的地址换算。三、写缓冲算法与读写路径3.1 对常规 zone 的直接写若某个逻辑 chunk 被映射到 conventional zone则所有写操作都直接写入该 zone无需任何缓冲。随机写性能与普通块设备一致。3.2 对顺序 zone 的对齐写与缓冲写若 chunk 映射到 sequential zone写操作是否直接下发取决于写偏移是否对齐 zone 写指针对齐写chunk 内的写偏移 顺序 zone 内写指针偏移时写操作直接下发到该 zone未对齐写此时写操作改走buffer zone缓冲 zone间接完成。dm-zoned 会分配一个空闲 conventional zone 作为该 chunk 的缓冲 zone把未对齐的随机写落到缓冲 zone 中。在源码层面zone 描述符struct dm_zone通过bzone指针把主映射 zone 与其缓冲 zone 互相关联对顺序数据 zone它指向用来处理未对齐写的随机 zone对缓冲 zone它指回数据 zonedrivers/md/dm-zoned.h。而按 chunk 映射与缓冲分配的核心函数为dmz_get_chunk_mapping()与dmz_get_chunk_buffer()声明于 dm-zoned.h。写入缓冲 zone 一个块时会自动使顺序 zone 中对应位置的同一块失效——这正是通过按块维护的 bitmap 实现的关键机制见下文第四节。当顺序 zone 的所有块都被写失效后该 zone 即被释放缓冲 zone 顺势“转正”成为该 chunk 的主映射 zone此时该 chunk 后续写入重新表现为原生随机写性能与普通块设备相当。3.3 读路径与“补零读”读操作按 bitmap 提供的块有效性信息执行chunk 映射的 zone或缓冲 zone中有效块直接从对应位置读取未映射 chunk 或所访问块已失效dm-zoned 不会发起底层读而是将读缓冲区清零后立即结束该读请求即返回全零数据避免无意义地访问已失效数据。对文件系统而言这种“读到已丢弃块返回零”的语义与普通写后校验的块设备行为一致保证了逻辑一致性。源码 drivers/md/dm-zoned-metadata.c 中通过dmz_block_valid()、dmz_validate_blocks()、dmz_invalidate_blocks()实现块级有效性查询与翻转在 target 侧dmz_map()中据以决定实际下发路径。四、磁盘元数据格式文档给出的 on-disk 元数据布局如下细节可对照 drivers/md/dm-zoned-metadata.c 中struct dmz_super、struct dmz_map与宏DMZ_MAP_ENTRIES的实现Super block超级块1 个 4KB 块位于找到的第一个 conventional zone 的第一块描述元数据块在盘上的数量与位置。源码中的 on-disk 结构struct dmz_superdm-zoned-metadata.c只使用 512B、但占据整块 4KB字段包括magic魔数DZBD(D)24 | (Z)16 | (B)8 | (D)用于识别 dm-zoned 元数据version元数据版本号当前实现为DMZ_META_VER 2gen64 位世代计数器generation counter用于主/备元数据集新旧判定nr_meta_blocks元数据块总数含超级块自身nr_reserved_seq为回收reclaim预留的顺序 zone 数量nr_chunks映射表条目数nr_map_blockschunk 映射表占用的块数nr_bitmap_blocks有效性 bitmap 占用的块数crc校验和dmz_label[32]、dmz_uuid[16]、dev_uuid[16]标签、dm-zoned 实例 UUID 与底层设备 UUID。整体 on-disk 布局在注释中概括为(1) Super block (1 block) → (2) Chunk mapping table (nr_map_blocks) → (3) Bitmap blocks (nr_bitmap_blocks)全部存放在从盘上第一个 conventional zone 起的元数据 zone 中。Chunk 映射表Chunk mapping table紧随超级块的若干块用于描述逻辑设备块的映射。映射按 chunk 进行chunk 大小即底层 zone 大小映射表以 chunk 号为索引每个条目给出存储该 chunk 数据的 zone 号并且可能额外指出一个用于缓冲该 chunk 随机修改的 conventional zone 号。源码struct dmz_mapdm-zoned-metadata.c恰好两个 32 位字段dzone_id与bzone_id每个 4KB 块可容纳DMZ_MAP_ENTRIES 512个 8 字节条目dm-zoned-metadata.cDMZ_MAP_UNMAPPED UINT_MAX表示未映射。这一点在源码注释中有清晰的佐证“if it is a sequential zone, a second zone (bzone_id) used as a write buffer may also be specified. This second zone will always be a randomly writeable zone.”块有效性 bitmapBitmaps映射表之后是一组块存放各数据 zone 内块的有效性位图。有效块定义为“已写入且未被丢弃”的块。对于被缓冲的数据 chunk其某个块只可能在两种位置之一有效要么在映射该 chunk 的数据 zone 中要么在该 chunk 的缓冲 zone 中绝不同时有效从而保证了读路径二选一的正确性。五、元数据保护与 flush 提交点5.1 双元数据集 世代计数为应对突然断电或系统崩溃导致的元数据损坏dm-zoned 使用两份元数据 zone 集主集primary set作为主元数据区常驻使用备集secondary set作为暂存区staging area。其更新流程遵循“先备后主、世代确认”的提交协议详见 dm-zoned.rst被修改的元数据先写入备集通过更新备集中的超级块并利用世代计数器generation counter指示备集包含最新元数据从而“验证”备集验证完成后才在主元数据集内就地更新元数据块。该机制保证任意时刻两份集合中总有一份是完整一致的要么所有修改全部提交要么一个都没有提交崩溃后总能从有效的那一份恢复。5.2 flush 作为提交点Flush 请求被用作元数据提交点。当收到 flush 请求时元数据修改活动被暂时阻塞既阻塞新的 BIO 处理也阻塞 reclaim 回收进程所有脏元数据块被 staged暂存并完成更新随后恢复正常操作。因此一次元数据 flush 只会短暂延迟写与 discard丢弃请求文档特别强调元数据 flush 执行期间读请求可以并发处理。这与 flush 串行化、读无需加元数据锁的设计取向一致也解释了为什么 dm-zoned 能把系统开销控制得很低。5.3 双设备场景的第三份标识元数据当常规块设备与分区块设备配合使用时见 dm-zoned.rst第1、2 份元数据含 bitmap 的完整元数据位于常规块设备开头的元数据 zone 内另有一份不含 zone bitmap 的第三份元数据写入分区块设备开头其世代计数固定为0正常运行期间永远不会被更新仅用于设备识别identification目的。5.4 锁与 flush 的源码印证源码将元数据同步与并发控制在 drivers/md/dm-zoned.h 暴露为清晰的锁接口dmz_lock_map/dmz_lock_metadata/dmz_lock_flush及对应的 unlock 版本加上dmz_flush_metadata()执行实际落盘。target 侧 drivers/md/dm-zoned-target.c 维护独立的 flush 工作队列与DMZ_FLUSH_PERIOD (10 * HZ)周期定时器将积压的 flush/写请求批量提交从而摊薄元数据写入的固定开销。六、Reclaim常规 zone 的回收与重排6.1 为什么需要 reclaimconventional zone 的数量是有限的。持续运行一段时间后空闲常规 zone 可能全部被占用要么被映射到 chunk要么正充当顺序 zone 的缓冲 zone此时对未缓冲 chunk 的未对齐写将无法继续。为避免死锁式停滞dm-zoned 内置reclaim回收后台进程。6.2 reclaim 的目标与复制策略文档描述的回收逻辑是reclaim 进程定期扫描已使用的常规 zone把“最久未使用least recently used”的 zone 中缓冲的有效块复制到空闲 sequential zone复制完成后更新 chunk 映射指向该顺序 zone并释放原缓冲 zone 供复用。从源码看回收按被回收 zone 的三种状态执行不同的策略见 drivers/md/dm-zoned-reclaim.c 的dmz_do_reclaim()分发逻辑dmz_reclaim_buf()回收“被用作缓冲的 zone”——把缓冲 zonebzone中的有效块复制回其主数据 zonedzone随后释放缓冲 zonedmz_reclaim_rnd_data()回收“映射 chunk 的常规数据 zone”——先分配空闲顺序 zone把该常规 zone 的有效块复制过去更新映射后释放常规 zonedmz_reclaim_seq_data()回收“纯顺序数据 zone”——把 dzone 中有效块合并merge进其缓冲 zone再释放 dzonedmz_reclaim_empty()被回收 zone 已无有效块时直接释放免去复制。所有数据搬迁都经由内核的dm-kcopyd客户端异步完成drivers/md/dm-zoned-reclaim.c单 zone 复制期间通过DMZ_RECLAIM_KCOPY位串行化避免同一 zone 上的复制互相重叠。6.3 回收触发阈值与限速当前源码与文档的差异官方文档对触发条件的表述是“一旦空闲常规 zone 少于 50% 就开始回收”。不过需要说明的是当前仓库中 dm-zoned target 版本为{2,0,0}其实际阈值逻辑已演进drivers/md/dm-zoned-reclaim.c/* Percentage of unmapped (free) random zones below which reclaim starts */ #define DMZ_RECLAIM_LOW_UNMAP_ZONES 30 /* Percentage of unmapped (free) random zones above which reclaim will stop */ #define DMZ_RECLAIM_HIGH_UNMAP_ZONES 50结合dmz_should_reclaim()dm-zoned-reclaim.c可得到当前实现下的真实触发规则空闲无活跃 IO且存在可回收 zone 时总是执行回收空闲常规 zone 百分比≥ 50%HIGH时即使空闲也不回收“还不缺”即文档所述“低于 50% 才启动”这一上限语义仍在只是现在由 HIGH 水位表达空闲常规 zone 百分比≤ 30%LOW时即便 target 繁忙也会强制回收以免随机写完全无法进行双设备场景下若第一块设备仅含 cache zone则永不在该设备上启动回收dm-zoned-reclaim.c。回收速度也做了自适应限速通过dmz_reclaim_percentage()计算空闲比例后dmz_reclaim_work()设置 kcopyd 节流值——target 空闲或空闲比例极低低于 LOW 的一半时以100全速回收繁忙但仍有富余常规 zone 时节流值取min(75, 100 - p_unmap / 2)在保证回收进度的同时尽量不拖累用户业务 IOdm-zoned-reclaim.c。6.4 保留顺序 zoneSuper block 中有一个值得注意的字段nr_reserved_seqdrivers/md/dm-zoned-metadata.c它记录为 reclaim 预留的顺序 zone 数量。这是回收过程可持续性的关键前提——只有始终留有空闲顺序 zone 作为复制目标回收流水线才能持续运转用户容量 全部 zone − 元数据 zone − 保留顺序 zone。七、实际操作格式化、加载、状态查询与手动回收7.1 使用 dmzadm 格式化分区块设备分区块设备必须先由dmzadm 工具格式化。该工具由 dm-zoned 作者维护的独立用户态工具集会分析设备的 zone 配置决定两份元数据集在盘上的安放位置并完成元数据初始化。单设备格式化dmzadm --format /dev/sdxx若使用“常规块设备 分区块设备”双盘组合则两个设备都必须指定且常规块设备必须作为第一个设备对应前文 2.2 节“逻辑 zone 排在最前”的布局约定dmzadm --format /dev/sdxx /dev/sdyy7.2 用 dmzadm 启动已格式化设备格式化完成的设备也可以用 dmzadm 启动其内部会调用 dmsetup 装载内核 dm-zoned targetdmzadm --start /dev/sdxx /dev/sdyy7.3 查询内部布局与 zone 用量dmsetup status关于设备内部布局与 zone 当前使用情况可通过 dmsetup 的status 回调查询dmsetup status /dev/dm-X官方文档给出的经典单设备输出为一行0 size zoned nr_zones zones nr_unmap_rnd/nr_rnd random nr_unmap_seq/nr_seq sequential各字段含义nr_zoneszone 总数nr_unmap_rnd空闲未映射随机 zone 数nr_rnd随机 zone 总数nr_unmap_seq空闲顺序 zone 数nr_seq顺序 zone 总数。需要提醒的是当前仓库源码中 dm-zoned target 版本为{2,0,0}见 drivers/md/dm-zoned-target.c 的struct target_type zoned_targetdmz_status()在STATUSTYPE_INFO分支实际输出的格式已经扩展为带cache zone 统计的形式dm-zoned-target.c总zone数 zones 空闲cache/总cache cache 空闲随机/总随机 random 空闲顺序/总顺序 sequential其中双设备场景下若第一台设备只含 cache zone则其 random/sequential 统计会被跳过、仅输出 cache 计数。两者分别刻画的是不同内核版本的行为文档保留的是早期单设备格式以本仓库源码为准的读者请按 2.0.0 版本的实际输出含 cache 字段解析。7.4 手动触发回收dmsetup message正常情况下回收在空闲比例跌破阈值后自动开始。若希望在到达阈值之前手动启动回收可使用 dmsetup 的message 函数dmsetup message /dev/dm-X 0 reclaim该命令会触发 reclaim 进程把随机常规zone 中的数据搬移到顺序 zone、释放常规 zone。源码中对应dmz_message()的回调当消息为reclaim时对 target 下的每一块底层设备逐一调用dmz_schedule_reclaim()以立即调度回收工作项drivers/md/dm-zoned-target.c若消息不被识别则返回错误并记录unrecognized message。八、源码组织与内核配置8.1 三文件模块划分dm-zoned 的源码高度模块化全部位于 drivers/md/由三部分组成drivers/md/Makefiledm-zoned-y dm-zoned-target.o dm-zoned-metadata.o dm-zoned-reclaim.o obj-$(CONFIG_DM_ZONED) dm-zoned.odm-zoned-target.ctarget 生命周期与 BIO 处理主路径。实现zonedtarget单例 targetmap回调按 chunk 分发 BIO、按需克隆下发并负责 flush 工作队列与消息处理drivers/md/dm-zoned-target.c#L1140-L1155dm-zoned-metadata.con-disk 元数据读写、super block 校验、chunk 映射表与 bitmap 维护、元数据双集 flushdm-zoned-reclaim.c回收工作队列、zone 选择与 kcopyd 数据搬迁。三者通过头文件 drivers/md/dm-zoned.h 统一接口契约。构建与装载由内核配置项CONFIG_DM_ZONED控制。8.2 目标能力与错误处理在 target 描述符中可以看到 dm-zoned 具备DM_TARGET_SINGLETON | DM_TARGET_MIXED_ZONED_MODEL特性drivers/md/dm-zoned-target.c#L1143前者保证一个映射设备上只能有一个该 target后者表示它可以同时处理分区块设备与常规块设备混合的底层拓扑——与 2.2 节双设备设计相互印证。zone 级错误处理同样在源码中可见当对顺序 zone 的写失败时target 会在 zone 上置DMZ_SEQ_WRITE_ERR标志并把设备标记为DMZ_CHECK_BDEVdrivers/md/dm-zoned-target.c设备“将死dying”时dmz_dev_is_dying()会引导回收与写路径及时退出避免在坏设备上无限重试。8.3 与通用块层的衔接dm-zoned 并不重新发明 zone 发现逻辑而是依赖通用 dm 层提供的 zone 报告基础设施本仓库 drivers/md/dm-zone.c 中的dm_blk_report_zones()/dm_report_zones_cb()负责把底层各 target含 dm-zoned的 zone 信息汇聚上报给块层。dm-zoned 只负责在 zone 之上实现映射、缓冲与回收从而与blk-mq、zone 重新校验zone revalidate等机制平滑衔接。九、关键设计取舍与适用建议综合文档与源码可以提炼出 dm-zoned 的几条关键设计取舍供实际选型与排障时参考容量换随机写能力但损失被压到极小文档给出 10TB/256MB zone 场景下仅约 5 个 zone 被内部占用元数据 回收内存开销 ≤ 4.5MB/盘。用户可见容量损失近乎可忽略。4KB 固定块粒度元数据按块记账配合 4KB 逻辑扇区bitmap 与映射表体积可控缺点是小于 4KB 的 IO 需要由上层对齐/合并。缓冲“转正”路径某个 chunk 的顺序 zone 被写满无效数据后直接释放、缓冲 zone 转为主映射 zone是 dm-zoned 能把常规 zone 流量长期维持在高吞吐的关键——它会动态把随机写热点 chunk 变成真正的随机写 zone。回收是吞吐的守卫当空闲常规 zone 逼近 30% 水位或设备空闲时回收才加速繁忙且富余时会主动限速。若出现大量未对齐随机写且常规 zone 不足优先通过dmsetup message ... reclaim手动回收或评估底层盘上常规 zone 配额是否足够。双元数据集保证崩溃一致性读请求在 flush 期间仍可并行处理是 dm-zoned 保持低延迟的重要细节不要在高并发只读负载下误判其为写路径瓶颈。若需深入探索建议从 drivers/md/dm-zoned-metadata.c 的 super block 读写与 drivers/md/dm-zoned-reclaim.c 的 zone 调度开始阅读用户态配套工具 dmzadm 需在 dm-zoned-tools 独立仓库获取与内核侧版本应保持匹配本仓库元数据版本为DMZ_META_VER 2格式化的盘如需回读须使用兼容版本工具。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询