序列化(Serialization)

反序列化(Deserialization)
是计算机科学中用于数据存储和传输的两种基本操作。

  1. 序列化

    • 序列化是将对象的状态信息转换为可以存储或传输的形式的过程。简单来说,就是将对象转换为字节序列(比如JSON、XML等格式)。
    • 目的:使得对象可以在网络上进行传输,或者存储到文件、数据库中。
    • 例子:将一个Java对象转换为JSON字符串,以便通过网络发送到另一个系统。
  2. 反序列化

    • 反序列化是序列化的逆过程,即从存储或传输的形式中恢复对象的状态信息,重新构建对象。
    • 目的:从文件、数据库或网络接收到的数据中恢复出原来的对象。
    • 例子:从一个JSON字符串中解析出数据,并根据这些数据创建一个新的Java对象。
      序列化和反序列化的应用场景
  • 网络传输
    :在网络通信中,对象需要在不同的服务间传输,序列化可以将对象转换为字节流,通过网络发送,接收方再通过反序列化恢复为对象。
  • 数据存储
    :将对象序列化后存储到文件或数据库中,以便后续读取和使用。
  • 分布式系统
    :在分布式系统中,对象可能需要在不同的节点间传递,序列化和反序列化是实现这一功能的关键技术。
  • 远程方法调用(RPC)
    :在RPC中,对象作为方法参数或返回值需要在不同的地址空间间传递,也需要经过序列化和反序列化。
    注意
    :序列化和反序列化时,需要确保双方使用相同的格式和规则,否则可能导致数据错误或无法解析。此外,序列化和反序列化过程中还可能涉及到安全问题,如序列化漏洞等,需要特别注意。

蓝图

image-20241108094408670

数据库自己管理磁盘数据和缓冲区,而不是通过操作系统管理(
Os is not your friend.
)。

数据存储

三层视图

数据库以页(page)为存储数据的基本单位,文件(file)是一系列页的集合,页中存储页数据(data),形成
文件-页-数据
三层架构。

文件有不同的组织形式,页包含页头和页数据,页数据可以采用不同方式组织:元组,日志,索引。

黄色部分为课程会提及的内容。

image-20241108102336292

采用Heapfile进行文件存储时的执行图:

image-20241108093210314

  • 页目录:存储管理的页的元信息(空闲页,空页)
  • 页头:存储页的元信息(页大小,校验和,数据库版本,事务可见性,压缩元数据)

面向元组的数据存储

image-20241108102828520

  • 通过<FileId, PageId, Slot>定位到一个指向tuple的指针(磁盘地址),然后找到tuple。

  • slot指针的灵活性:内部元组位置变化时,外部无感知;指针可以指向其他页,可以存储大数据(文件,大文本);支持变长记录。

  • 数据库会为每个元组分配一个数据记录的唯一标识(record identifier),来表示元组的
    物理位置
    。SQLite和Oracle中为ROWID,Pg中是CTID,<PageId, Slot>。但是他们对于应用程序是无用的。

image-20241108103947481

  • Header包含:可见性信息;NULL Bit Map。

  • Data包含:行数据。

Tuple只是一个字符串(char[]),本身不存储类型信息,类型信息存在数据库的
System Catalogs
中。(为了保证数据紧凑;非自解释的)

存数据时会遇到的问题:

  • 数据对齐:填充,重排序

image-20241108123458670

  • 精确值问题:BIGDECIMAL(转为字符串存储)

image-20241108123607713

  • 空值:Bit Map;特殊值

  • 大值和文件:Overflow Page和External File。

    大值采用溢出页;大文件可以采用溢出页,也可以用外部文件系统存储,然后存储一个指向文件路径的指针,而不是直接存储文件内容(Oracle:BFILE, Microsoft: FILESTREAM)。

image-20241108123852928

image-20241108123909572

日志结构存储

基本概念:

  • 利写不利读,非原地更新:只有PUT和DELETE操作,顺序IO。查询时由最新到最老时查询日志。
  • 加速查询:索引。

image-20241108132439250

  • 加速查询:日志压缩,且压缩时会排序日志。
  • 压缩方式:层级压缩,统一压缩

image-20241108133449643

特点 Level Compaction Universal Compaction
层级结构 有多层级,L0、L1、L2 等 无层级结构,所有文件在同一级别
文件组织方式 每个层级内文件不重叠,跨层逐渐下推 基于文件大小和数量合并,文件可能有重叠
合并策略 层级压缩,按顺序下推合并 文件数量和大小超过阈值时触发合并
写放大 较高,因为需要不断下推文件至更低层级 较低,因为减少频繁合并
读放大 较低,因为相同键在每层只存在一次 较高,因为没有严格层级,需检查多个文件
适用场景 读多写少的场景 写多读少、实时数据的高写入场景

索引组织存储

直接用索引组织数据,数据挂在叶子结点上,Page内部的tuple有序。

SQLite和MySQL默认用这种方式组织数据,Oracle和SQL Server可选。

image-20241108133519165

和基于元组的存储对比:

特性 Index-Organized Storage Tuple-Oriented Storage
数据与索引存储 数据存储在主键索引结构中 数据和索引独立存储
数据排序 数据按照主键顺序排序 数据无序存储
主键查询性能 高效,因数据已按主键排序 依赖主键索引,但数据本身无序
插入和更新性能 插入和更新时可能需要索引重排,较慢 插入和更新较快,无需主键排序
适用场景 主键查询频繁,数据顺序性强的场景 多种查询模式,插入和更新频繁的场景

数据模型

image-20241108160451120

  1. N-ary Storage Model (NSM)
  2. Decomposition Storage Model (DSM)
  3. Hybrid Storage Model (PAX)

NSM

优点:操作一条完整记录时快速。

缺点:操作一批记录的某一个特定列的时候,非顺序读取,会有无效IO,且数据不好做压缩。

image-20241108160903173

DSM

优点:可以取到一批特定列;可以做数据压缩。

缺点:在操作完整记录的时候,需要分解查询,得到结果后还需要再进行合并。

image-20241108161201607

PAX

结合了NSM和DSM,既能一次处理一个完整记录,也能在读取一系列特定的属性时顺序读取并避免过多的无用IO。

image-20241108161918379

数据压缩方式

目标:

  1. 压缩结果为定长值(存储定长数据)。

  2. 仅在需要时解压缩,否则都采用压缩形式处理数据。

  3. 必须是无损的。

常用的压缩方式:字典压缩。

维护字典映射,数据表中存储映射值。结果定长,且可以支持范围查询。

image-20241108162715705

image-20241108162731540

字典映射的实现方式:数组。【哈希表不支持范围查询,B+树内存消耗大】

image-20241108163114414

Elf(可执行和可链接文件)是一个永远也绕不开的话题,只要我们还在使用安卓手机/linux服务器,我们就需要了解elf的一些方方面面,现在就让我们从一个常量值提取的小需求出发,逐步解析elf文件结构吧!

一、写作目的:

网络上关于elf文件结构描述的文章不在少数,但能具体到二进制分析的却屈指可数,总给人一种八股文的感觉,而最近恰好又遇到了一个需要通过符号表获取其表示的常量值的需求,在完成之后,我将实现的过程进行总结提炼写下这么一篇elf结构入门的文章供后续学习回顾。

二、需求:

在C++中存在许多常量赋值和使用的操作,现在我们获取到了一个由C++编译成的动态链接库.so文件,我们想要反推一下其中可能的符号及其所表示的值。

三、基础知识

①.Elf文件类型:

Elf文件类型分为三种,.o\.so\.exe,普通的.exe可执行文件相信大家并不陌生,这里主要介绍一下.o和.so文件。.o为可重定位的目标文件,.so为共享目标文件,两者的区别就是.o是静态的,.so是动态的,静态就是指它将被链接器在编译时合并到可执行文件中,而动态则是在可执行文件要使用它时才进行加载。除了用途不同,其文件结构和文件结构中各种数据类型都是相同的,elf文件中数据类型大致为图中几类有符号无符号1/2/4大小的地址偏移整数等(我们解析时只需记住char是1字节half是2字节其余4字节即可)。

②.Elf文件链接视图和执行视图

elf的文件结构就稍微复杂一些,其分为链接视图和执行视图两种视角,之所以叫视角,是因为如同人看待一个物体的不同角度,虽然看上去不一样,但本质都是同一个物体。这里的链接视图即指以链接器的角度来看elf文件,它关注的是elf文件的节区,即用头部节取表去定位各个节区然后进行链接,而执行视图则是以程序执行的角度来看elf文件,它关注的是如何使用程序头部表去定位各段然后加载到内存中去。其两种视图的对应关系也如上图所示,.text节对应代码段,.rodata/.data/.bbs等包含数据的节对应数据段,其余还有一些专门链接用的如动态符号表等,则不会被加载到内存中。

四、实现

在了解了上述的一些基础知识后,我们也知道要获取符号及其对应值,我们不能从执行视图出发,因为符号表可能都不会被程序头部表识别到,所以我们从链接视图出发,根据头部节区表定位数据节区和符号表节区,根据其索引关系完成匹配,具体实现过程如下所示。

接着,让我们一步一步梳理

①解析elf文件头部:

首先贴出elf文件头的结构定义

想必精通C/C++的各位大佬一定是一眼秒懂的,这里就不过多解释构造了,其ELF32的数据类型具体表示含义在上面已有展示,这里也不多说。这里关键数据有以下几点:

e_ident:十六字节数组

首先就是魔数了,看文件先看魔数,这里的16位比特的e_ident的前4位数据只能是0x7F454C46,转换成ascii码即0x7F ELF。然后依次表示进制(1为32位/2为64位)、大小端(1为LSB/2为MSB)、版本信息(1为当前版本)、运行所在系统(0为UNIX/3为Linux...)、操作系统ABI、7位填充数据。

e_type:两字节目标文件类型

1表示可重定位文件、2表示可执行文件、3表示共享对象文件、4表示核心转储文件

依照这个规律解读上图所示例子,即这是一个32位/LSB/当前版本/运行在UNIX上的.so文件。

在确定了文件类型之后,我们便可以依照上述的流程接着往下解析...

节区头部表格偏移、表项大小、表项数

根据elf头部结构我们可以轻松知道上面我们要的信息

e_shoff(32-35)\e_shentsize(46-47)\e_shnum(48-49)

该测试文件头部节区表偏移为2896(LSB)、表项大小为40、表项数为22

节区头部表名称字符串表索引

节区头部表中每一个表项所需使用的名称字符串,对应的字符串表,这个在解析节区头部表项时会使用到,此处为21(0X15)

验证

我们知道从链接视图来看,elf文件头部节区表结束后文件也就读取完了,故我们2896+40*22应该就是文件大小3776了(果真如此,看来上述分析工作全对)

②获取头部节区表:

在elf头部中,我们已经获取了头部节区表偏移、表项大小、表项数,现在我们就可以根据头部表项依次读取节区了,节区表表项结构如下图所示

其中我们需要关注的有以下几点:

表项序号(index)、节区名(sh_name)、节区类型(sh_type)、节区偏移(sh_offset)、节区长度(sh_size)、附加信息(sh_info)

表项序号:

加载时通过计数获得

节区偏移:

表项内第17-20个字节,表示文件内节区数据偏移

节区长度:

表项内第21-24个字节,表示文件内节区数据偏移

节区名:

节点区名为在对应(文件头部的字符串表索引)字符串表中的索引,再以\0结尾取得一个字符串。

节区类型:

位于单个表项的第5-8位比特,表示节区用途,常见的有:

SHT_PROGBITS(0x1):包含程序定义的数据,如代码、只读数据、可读写数据等。

SHT_SYMTAB(0x2):包含符号表信息,用于链接或调试。

SHT_STRTAB(0x3):包含字符串表,通常用于表示符号表或节区表中的名字。

...

SHT_DYNSYM(11):包含动态链接符号表,用于运行时的符号解析。

...

③获取节区字符串表

而上文文件头部中我们已经得到节区使用的字符串表项的索引为15,而节区表偏移为2896、表项大小为40,所以该字符串表表项的偏移为2896 + 21*40 = 3736

从字符串表的节区表项中我们可以得到其实际字符串表的偏移为2691(0xA83),长度为202(CA)

而一个表项的节区名即表项内第1-4个字节,为对应字符串表的内部索引,字符串表的节区名索引为178(0XB2),再根据\0结尾断句,即头部节区表对应字符串表名字为.shstrtab

(通过节区头部表项对应的字符串表.shstrtab我们也能够大致知道该elf文件中的成分信息了--如是否包含某些特定节区)

上述字符串表第5-8位为0x03000000,即表示它包含字符串表(其他节区也可能包含字符串表,但用法就不尽相同了)...

④获取符号表

(从上述节区名字符串表中我们可以得知存在动态符号表.dynsym,不存在静态符号表.symtab,所以在遍历节区表项的时候,我们不仅可以通过名称字符串”.dynsym”也可以通过节区类型11/0x0B来定位动态符号表项)

根据符号表节区表项的信息我们可以知道符号表存放的具体位置及单个项目大小

sh_addr(节区在内存中位置):第13-16个字节,值为524(0x0C020000)

sh_offset(节区数据文件中偏移):第17-20个字节,值为524(0x0C020000)

sh_size(节区长度):第21-24个字节,值为304(0x30010000)

sh_link(节区头部表索引):第25-28个字节,值为7

sh_entsize(节区中单个项目大小):第37-40个字节,值为16(0x10000000)

在上述符号表中,实际存储19个符号结构体(304 / 16)

单个符号项如上图所示,其中有这么几个值

符号名称:

st_name,第1-4个字节,为符号表中sh_link指向的字符串表中的索引,同样通过索引+\0结尾的方式获取该符号名称字符串。

符号值:

st_value,第5-8个字节,根据具体情况取得含义,例如符号表示函数时,该值为函数在内存中的起始地址,若该符号表示全局或静态变量时,表示内存在变量中的位置。

符号值值大小:

st_size,第9-12个字节,变量长度或者函数代码所占字节数

符号类型:

st_info,第13个字节,根据1个字节的八位比特作为flag标注符号的特征,高4位表示绑定属性(Binding),低4位表示符号类型(Type)

Type:

STT_OBJECT(1):数据对象,通常是变量

STT_FUNC(2):函数或其他可执行代码

...

Binding:

STB_LOCAL(0):局部符号,只在当前模块中可见

STB_GLOBAL(1):全局符号,在所有模块中可见

...

节区头部索引:

st_shndx,第15-16个字节,根据具体情况取得含义

⑤获取符号名称字符串表

首先我们要获取符号名,符号名即变量名/函数名...

根据节区头部符号表项中的sh_link值7,我们可以计算出对应字符串表的起始地址

2896+40*7

(根据节区表项中的节区类型为3,我们也可以笃定该节区就是我们要找的字符串节区)

读取节区信息:

名称:93(0x5D),加上名称符号表偏移2691,得到该名称字符串.dynstr

偏移:1180(0x9C04)

大小:341(0x0155)

以下即符号名字字符串表

⑥遍历符号

获取符号类型

遍历符号表(与4中图重复)每一个符号(4中已简述每个符号结构),获取其符号名和符号类型,st_info的低四位为1,则符号为OBJECT变量,若4size则可能为字符串指针

如这六个符号,其符号值大小为4,st_info(0x11)为00010001即全局的数据对象

获取符号值节区

在根据st_shndx值18(0x12),定位到符号值存储节区头部表项偏移2896 + 18*40 = 3616

名称值为77+2691即.ARM.attributes

内存中地址为14620(0x1C39)

文件偏移为2332(0x1C09)

节区长度为24(0x18)

获取符号值和对应常量

再结合上述六个符号(符号名对应字符串表已在上文给出),我们可以得到以下信息

(ad_value为通过计算st_value与上述内存偏移14620获得的符号变量值-地址)(注意:此处地址值仍需LSB转换)

(value为通过地址值偏移获取的变量对应的常量值)

st_name-1: 1426 = 1180 + 246(0xF6) -> “global_var2”

st_value-1: 14628(0x2439)

ad_value-1: 1831(0x2707)

Value: ”测”

st_name-2: 1385 = 1180 + 205(0xCD) -> “a”

st_value-2: 14636(0x2C39)

ad_value-2: 0xFFFFFF7F

Value-2: 0x7FFFFFFF(INT_MAX)

st_name-3: 1438 = 1180 + 258(0x0201) -> “global_var3”

st_value-3: 14632(0x2839)

ad_value-3: 1743(0xCF06)

Value: ”abc”

st_name-4: 1401 = 1180 + 221(0xDD) -> “b”

st_value-4: 14640(0x3039)

ad_value-4: 0xFFFFFF7F

Value-4: 0x7FFFFFFF(INT_MAX)

st_name-5: 1403 = 1180 + 223(0xDF) -> “global_var”

st_value-5: 14620(0x1C39)

ad_value-5: 1747(0xD306)

Value: ”doGlobalVarTest测试”

st_name-6: 1414 = 1180 + 234(0xEA) -> “global_var1”

st_value-6: 14624(0x2039)

ad_value-6: 1652(0x7406)

Value: “测aaa”

四、总结

通过上述步骤,我们依托定位符号常量的需求,逐步分析了elf的文件架构。并根据以下测试程序我们实验了有哪些数据会在编译成.so文件后保留符号(全局非静态变量),以及如何获取其变量值

好未来,前身学而思,于 2010 年在美国纽约证券交易所上市。公司积极将大模型研究应用于教学产品中,近期推出了数学领域的千亿级大模型。

在大模型的背景下,存储系统需处理巨量数据和复杂文件操作,要求支持高并发和高吞吐量。此外,还需应对版本管理、模型训练性能优化和多云分发的挑战。

为解决这些问题,团队基于 JuiceFS 开发了一个模型仓库,支持用户训练过程存储 checkpoint,并且控制面支持用户从各个云环境上传并统一管理模型。通过 JuiceFS CSI 组件,好未来将模型仓库挂载到各个集群中,大模型文件挂载配置只需 1-3 分钟,使得 AI 应用弹性变得更加容易。

此外,通过实施权限控制、克隆备份等策略,有效减少了用户误操作的损失并提高了数据安全性。目前好未来在多云多地部署了两套元数据和数据仓库;对象存储的使用规模达 6TB,存储超过 100 个模型。

01 大模型背景下模型仓库的挑战

在以往传统的 DevOps 研发流程中,我们通常以容器镜像为交付物,即由 Docker Builder 构建出一个镜像,然后通过 docker push 命令将其推送至 Docker Hub 或其他镜像仓库中。客户端的数据面则会通过 Docker 的方式,从中心镜像仓库拉取镜像至本地,这一过程中可能会采用一些加速手段以提高效率。

到 AI 场景时,情况就有所不同了。以训练任务为例,它可能会使用 Torch Save、Megatron save_checkpoint或其他方式生成一个序列化的文件,这个文件随后会以 Linux POSIX 方式写入到存储中,这个存储可以是对象存储(如阿里云的 OSS)或文件系统(如 GPFS 或 NFS)。简而言之,训练任务通过写文件系统的方式,将模型写入到远程存储中,从而实现了模型的上传。整个过程中还包含一些评估步骤,但在此我们略去不谈,以精简描述整个流程。与传统 IT 交付中仅仅涉及镜像的推送和拉取不同,AI 场景需要处理更大规模的数据和更复杂的文件操作,对存储系统的要求更为苛刻,常常需要高并发和高吞吐量的支持。

到了推理阶段,在容器的场景下,需要通过 CSI 方式去挂载 NFS 或者 GPFS 系统或对象存储系统来达到将远程的模型拉取到容器场景中。从表面上看,这个流程似乎并无问题,也能够正常工作。然而,在实际运行过程中,我们还是发现了不少明显的问题。

  • 首要问题是缺乏版本管理
    。对于传统的容器镜像,我们有明确的交付物和版本信息,包括上传者、大小等详细信息。然而,在模型仓库中,由于模型通常是以 Linux 文件系统的方式存储的(文件或文件夹形式),因此缺乏版本管理和元数据信息,如上传者、修改者等。

  • 其次,模型仓库无法实现加速和多云分发
    。在 Docker 场景中,我们可以使用如 Dragonfly 等工具进行加速。但是,在使用 NFS、GFS 或 OSS 等存储系统时,却缺乏一个有效的加速方案。同时,也无法实现多云分发。以 NFS 为例,它基本上是闭环于一个 IDC 内部的,无法实现跨 IDC 甚至跨云的挂载。即使能够实现挂载,其速度也会非常慢,因此我们可以认为它无法实现多云分发。

  • 最后,安全性差
    。在推理场景下,整个模型仓库需要挂载到容器中。如果客户端的挂载权限过大(例如挂载了一个包含大量模型的目录),就可能导致模型泄露或误删除的问题。特别是当挂载方式为可读可写时,客户端甚至有可能删除模型文件。

由此衍生出不同场景对模型仓库的存储需求。

训练场景的存储需求:

  • 模型下载与处理
    :在算法建模阶段,需要下载并可能转换及切分各种模型,这包括开源模型、参考模型或自设计的网络结构。例如,进行模型的 TP(Tensor Parallelism)和 PP(Pipeline Parallelism)切分。
  • 高性能读写
    :训练阶段要求存储系统具备极高的读写吞吐能力,以便存储大规模的 checkpoint 文件。这些文件可能非常庞大,如单个 checkpoint 文件大小接近 1TB。

推理场景的存储需求:

  • 模型版本管理与服务化
    :当模型更新至新版本时,需要进行版本发布和审批流程。此外,在模型服务化过程中,可能需要频繁地扩展或收缩使用的 GPU 资源,这通常在夜间进行资源释放,在白天进行资源扩展。
  • 高读吞吐性能
    :由于白天需频繁拉取模型副本以应对资源扩展,存储系统需支持高效的读操作,确保快速响应模型拉取需求。

此外,从管理者角度存在以下需求:

  • 团队级模型管理
    :模型仓库应支持按团队进行模型的隔离管理,确保不同团队之间模型的隐私和独立性。
  • 详尽的版本控制
    :存储系统应能清晰记录模型的迭代时间、版本用途等元信息,支持模型的上传、下载、审计和分享功能。

02 存储选型:如何在成本、性能、稳定性之间取舍?

核心考量点

首先,要降低对云厂商的依赖
,确保技术方案在自建 IDC 以及多个云厂商之间保持一致性和统一性;

其次,成本也是一个重要的考量因素
。虽然资金充足可以支持更好的解决方案,但成本效益分析同样重要。我们需要综合考虑各种存储方案的成本,包括本地磁盘、GPFS 以及对象存储(如 OSS)等;

第三,性能是关键因素
。根据之前的背景介绍,模型仓库的读写性能都有较高要求。因此,我们需要在单个 IDC 或单个云上实现模型读写流量的闭环,以确保高性能;

最后,稳定性也是不可忽视的因素
。我们不会为了支撑模型仓库而引入过高的运维复杂度。因此,对组件的繁杂程度和稳定性都有很高的要求。

主要技术选型对比

Fluid + Alluxio + OSS
:该方案在前几年已经相对成熟并受到广泛关注。它融合了云原生的编排能力和对象存储的加速能力,实现了多云技术上的统一。无论是在阿里云、腾讯云还是自建的 IDC 中,都能采用这一方案。此外,该方案在社区的应用也相当广泛。

然而,它也存在一些不足。例如,它无法与 Ceph Rados 进行结合,这在我们集团内部是一个已有的技术栈。同时,该方案的运维复杂度较高,组件较多且资源消耗较大。对于大文件的读取速度,该方案的表现也并不理想。此外,客户端的稳定性也有待进一步验证。

GPFS
:它是一个商业并行文件系统,读写性能强劲,且我们集团已经购买了这一产品。此外,GPFS 在处理海量小文件方面也具有显著优势。然而,它的劣势同样明显。首先,它无法实现多云同步,这意味着我们在 IDC 购买的 GPU 无法在其他云上再购买一套 GPFS,成本高昂。其次,GPFS 的容量价格也非常昂贵,是 OSS 的好几倍。

CephFS
:我们集团内对该技术有一定的技术沉淀和优势。然而,它同样无法实现多云同步,运营成本较高。

JuiceFS
:它的优势在于支持多云同步,运维简单,组件较少且观测性较好。成本方面,它基本上只有对象存储的费用,除了元数据管理外,没有其他额外成本。此外,JuiceFS 既可以在云上搭配对象存储使用,也可以在 IDC 搭配 Ceph Rados 使用。综合考虑以上因素,我们选择了 JuiceFS 作为支持大模型模型仓库的底层存储系统。

03 好未来模型仓库实践方案

训练场景中模型仓库读写设计: 单云训练

首先,聚焦于训练场景中的模型仓库读写设置
。我们采用单云训练策略,即在单一云平台上进行模型训练,而非跨多云进行,这主要考虑到实际操作中的可行性和效率。

针对训练场景下的读写需求,我们制定了以下方案:我们将大量 GPU 机器上冗余的 NVMe 磁盘组成一个 ceph 集群,使用 JuiceFS 对接 Rodos, 从而实现 ceph 集群的读写操作。 在模型训练过程中,模型会以 JuiceFS CSI 方式挂载一个盘。当需要执行 checkpoint 存储或加载时,将直接对 Rodos 进行读写操作。
经实测,在大模型训练过程中,checkpoint 写入速度可达到 15GB/s

在元数据管理方面,我们选择了 Redis。相较于 OceanBase 或 TiKV 等复杂元数据管理引擎,我们选择 redis 主要出于以下考虑:
我们仅将其用于大文件的存储,而每个文件的大小可能达到数 GB。因此,我们判断其数据量相对较小,无需采用复杂的元数据管理引擎,以减轻运维负担

推理场景中模型仓库读写设计: 多云推理

与训练场景不同,推理资源通常分布在多云平台上,如阿里云、腾讯云等。在这些云平台上,我们不会购买大量的 NVMe 机器,因为云上本身具备对象存储能力。因此,我们采用了 JuiceFS 的经典模式,即 JuiceFS 加上 Redis,与云厂商的对象存储组成一个集群。在推理过程中,模型文件以只读方式挂载,以避免程序对其进行修改。此外,我们还设计了一个面向多云环境的间歇性同步方案,以确保模型能够同步到所有云的 JuiceFS 上。

在面对某些挑战时,当需要在白天大规模扩容推理服务时,以扩容 HPA(Horizontal Pod Autoscaler)为例,这种定点扩容会导致大量推理服务同时启动,并需要迅速读取大量的模型文件。这种情况下,如果没有本地缓存的支持,带宽消耗将极为巨大。

为了应对这一挑战,我们采用了 “warm up” 策略。即在定时扩容之前,通过预热的方式将即将被读取的模型文件预先加载到缓存中。这样做可以显著提升扩容的弹性速度,确保推理服务能够迅速启动并投入运行。

管理端:模型仓库上传与下载的设计

管理端主要聚焦于上传和下载功能。我们自主开发了一个客户端,该客户端支持通过 S3 协议上传模型文件。S3 网关会接收并转化这些请求,然后与元数据系统进行交互,如 Redis 等。

在我们的应用场景中,还有一项重要的设计是对相同文件进行去重处理。我们采用了类似于 Docker 镜像仓库的设计思路,即为每个文件计算 MD5 值。如果两个文件的 MD5 值相同,则不会进行重复上传。这一设计不仅节省了存储空间,还提高了上传效率。

此外,在更新模型时,我们还会保留一些快照。使用 JuiceFS 快照功能复制文件时,它并不会在 OSS 中新增文件,而只是在元数据中进行记录。这种方式使得我们在进行模型更新和快照保留时更加便捷高效。

04 未来展望

按需同步多云的模型仓库
:我们目前的做法是采取定期批量同步的方式,将 JuiceFS 在某个云上的集群数据进行同步。然而,这种做法相对简单粗暴,可能无法满足未来对于数据同步的更高需求。因此,我们计划进行改进,实现一个标记型的同步系统。该系统将能够识别需要同步的区域,并在收到模型上传事件后,自动将这些数据同步到多云平台上。此外,我们还将引入一些 warm up 策略,以优化数据同步的过程,提高同步效率和准确性。

扩展单机 cache 和分布式 cache
:我们目前单机使用了3T MVME的 cache 方式,在短期内来看,这种方式的容量还是相对充足的。然而,从长远来看,为了满足更大的数据存储和访问需求,我们将基于一致性哈希的原理,在客户端自主研发一个分布式 cache 组件。这个组件将能够更大程度地提升开启的容量和命中率,从而满足未来对于数据存储和访问的更高要求。

希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入
JuiceFS 社区
与大家共同交流。

本篇参考:
https://help.salesforce.com/s/articleView?id=sf.data_sandbox_selective_access.htm&type=5

背景:
最近同事刷新sandbox发现点击create不生效,并且无任何提示(后续可能优化)。

习惯了直接创建或者刷新的老司机们可能看不出来Sandbox Access标红提示来着,恰巧当前的org还没有使用group,所以提了case,然后得到的回复是针对Developer 以及Developer Pro sandbox,最新的release需要选择group才可以创建,这个group的人才可以使用当前的sandbox。

步骤很简单。

1. setup 搜索group然后创建一个group,group选择user需要访问当前sandbox的user。

2. sandbox 新建或者刷新最后步骤选择需要的group即可。

问题: 这个变化对我的影响大吗?这个需要确定你当前的sandbox类型以及你的操作,其实最大的变化是针对developer / developer pro的心间sandbox的变化,以下是一览图。

Sandbox 操作类型 访问Sandbox的User Group 谁可以访问Sandbox 备注
Create(Partial / Full sandbox) All User 和production user相同 Partial 以及full sandbox不严格要求创建group,可以选择All User选项,和未更新前相同
Create(所有类型) User Group User Group 当创建sandbox选择user group时,只有这些user默认active,其他user默认deactive
Refresh 和production user相同 针对dev以及dev pro sandbox,虽然我们选择了指定的group,但是刷新以后拥有production访问权限的人也可以访问这个sandbox

总结:
篇中主要介绍最新release中的sandbox创建和刷新的变化,虽然官方在 Sandbox Access标住了红色,但是报错信息前端报错导致UI没显示还是不太友好,相信很快就会修复。针对这个变化,其实对于开发人员影响有限,如果遇到同样的情况的小伙伴可以当做一个快速参考即可。篇中错误的地方欢迎指出,有不懂欢迎留言。