2024年11月

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没显示还是不太友好,相信很快就会修复。针对这个变化,其实对于开发人员影响有限,如果遇到同样的情况的小伙伴可以当做一个快速参考即可。篇中错误的地方欢迎指出,有不懂欢迎留言。

之前有过一系列的文章来介绍我们的
多云系统
,多云系统可以将不同云厂商不同云账号不同云区域下的资源进行集中管理,通过多云系统,用户可以像使用一朵云一样使用多云,极大的提升了多云使用的便捷性。虽然如今已经步入云时代,多云的使用十分广泛,但IDC机房仍然存在,不少企业也都还是IDC机房托管的模式,尤其是一些传统行业或是金融机构等。鉴于此,我们对多云系统进行了全面的升级,升级成了全新的资产系统,不仅可以管理多云资源,同时还能够接入IDC机房资源,实现云上云下的统一管理

资产系统针对资源的纳管提供了三种方式,分别是:多云自动同步、Agent主动上报和手工添加,这篇文章将详细介绍下这三种方式的设计、用法和区别。大概的图示如下:

这里边有个
资源池
的概念,在之前
「多云系统之资源管理」
的文章里介绍过,资源池是个虚拟的池子,用来
暂存
云上同步下来的资源,为什么说是暂存,主要是因为资源池内的资源都是没有项目归属的,属于无人认领或暂未认领的无主资源,而所有的资源最终都应该归属于项目,以便于项目维护和成本核算。由此可知,一个资源要么归属于项目,要么暂存于资源池,正常情况下所有资源都应归属到项目,那资源池也应该是空的。资源池内的资源有两种方式归属到项目,其一是通过动态规则自动处理,其二便是人工认领手动处理,这里我们当然是推荐动态规则自动处理啦,减少人为介入,一切都自动化完成

还有个
资源树节点
的概念,资源树就是我们通常所说的服务树,或者CI树,都是一样的东西,只是不同的地方叫法不一样,这里就统一叫资源树好了,资源树是一个树状结构,会有很多的节点,项目下的资源就隶属于不同的节点,通过树的方式去组织资源,可以很清晰的了解资源关系,使用起来也十分方便,应用特别广泛。就像下方图示的左侧就是一个资源树,资源归属到不同的树节点,可以根据不同的资源节点动态获取节点下的资源数据,使用起来清晰方便

有了这个基本的概念我们再来聊聊这三种资源纳管的方式

多云同步

首先是多云同步,多云同步是通过云平台的API或SDK,自动同步云资源数据到资源池,这里只需要添加云平台的账号即可,自动同步程序会根据配置的时间周期自动去云上拉取资源数据然后入库,入库时会自动判断是更新资源属性还是新建资源。新增还是更新的主要判断依据是:云账号+云区域+资源ID联合唯一,正常情况下同一云账号同一云区域下的资源ID是唯一的,所以遇到这三者一致的资源则更新,否则就新建。整体的逻辑如下图所示

动态规则自动匹配需要资源具有一定的规律性,最常用的就是通过资源名称来区别,例如一个项目A正式环境使用的Nginx服务器,我们想要给归属到项目A下的资源树节点
ProjectA/PROD/Nginx
下,那我们最好在资源命名时进行同一,例如都符合
ProjectA-PROD-Nginx-{number}
这样的格式,那动态规则就比较好创建,所有名称以
ProjectA-PROD-Nginx-
开头的资源都自动给归属到资源树节点
ProjectA/PROD/Nginx

当然资源命名没有那么规范且数量不是很多的情况下,也可以直接在资源列表页面,直接添加资源池内的资源到资源树节点下,手动添加有一个前提,那就是资源已经在资源池里了,资源到资源池里除了多云自动同步外,还有个方式那就是我们下边会讲到的Agent主动上报

多云同步比较简单,如果用到了云,那首先推荐的就是这种方式进行资源纳管,这里的云可以是公有云,例如阿里云、腾讯云、华为云等等,或者是私有云,例如VMWare、OpenStack、OpenShift等等

Agent上报

除了多云同步的方式进行资源纳管之外,我们还提供了Agent,可以部署在服务器上以实现资源主动上报统一纳管,这种方式对于IDC之类没有提供API/SDK获取资源的方式来说非常有效,通过安装客户端Agent,自动上报云资源数据,这里只需要服务器安装Agent即可,Agent只要与服务器连通,则会自动上报资源数据到资源池,然后配合动态规则自动将资源归属到资源树节点,这个过程与多云到自动纳管逻辑一致

对于安装了Agent的服务器来说,除了资源先到资源池再配合动态规则自动归属到资源树节点的管理方式外,Agent还提供直接归属到资源树节点而不经过资源池的方式。这就涉及Agent的高级配置,Agent的配置文件默认只需要配置一个Server服务器的地址即可启动,像下边这样

SERVER-ADDRESS: "wss://agent-server.ops-coffee.cn/api/v1/agent"

Agent启动之后会与Server建立连接,Server通过周期任务把Agent上报的资源入库,默认也是先进了资源池,但当Agent配置了参数
BIZ-ID
时,Server入库则首先去匹配
BIZ-ID
所对应的资源树节点是否存在,如果存在则直接给归属到资源树节点下,不存在则进资源池。这样的话,当用户明确知道自己的资源归属到哪个资源树节点时,则可以在Agent的配置文件中添加
BIZ-ID
配置,像下边这样

SERVER-ADDRESS: "wss://agent-server.ops-coffee.cn/api/v1/agent"
BIZ-ID: "37"

当Server入库数据时会先检查ID为37的资源树节点是否存在,如果存在则直接归属到节点,这样就实现了Agent直接归属到节点,而无需经过资源池的步骤,这样的话也就不需要动态规则自动匹配再归属到资源树节点了,更加方便

手动添加

以上两种方式无论是多云自动同步还是Agent主动上报,都可以实现资源的自动入库,自动归属到资源树节点,这两种方式都有一定的前提条件,要么属于云资源可以自动同步,要么安装了Agent可以主动上报,如果不属于云资源,也不想安装Agent,如何管理资源呢?那系统提供了一种最原始的方式,用户自己手工录入,或是通过Excel批量导入资源

这两种方式都比较原始,资源属性的更新依赖人工操作,不像多云同步或Agent上报,资源属性的更新都可以自动完成,所有不是特殊情况都不建议使用,这里就不多赘述了

至此,资产系统所支持的三种资源纳管理介绍完毕

一、介绍
今天是这个系列《C++之 Opencv 入门到提高》得第四篇文章。这篇文章很简单,介绍如何使用 Mat 对象来实例化图像实例,了解它的构造函数和常用的方法,这是基础,为以后的学习做好铺垫。虽然操作很简单,但是背后有很多东西需要我们深究,才能做到知其然知其所以然。OpenCV 具体的简介内容,我就不多说了,网上很多,大家可以自行脑补。
OpenCV 的官网地址:
https://opencv.org/
,组件下载地址:
https://opencv.org/releases/

OpenCV 官网学习网站:
https://docs.opencv.ac.cn/4.10.0/index.html

我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10(64位)
开发组件:OpenCV – 4.10.0
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
开发语言:C++(VC16)

二、知识学习
如果我们想学习或者使用 OpenCV,它的图像对象 Mat 是绕不过去的。我们对它越熟悉,使用的越灵活。俗话说得好,欲善其事必先利其器。内容很简单,直接上代码,代码里面有注释。废话不说了。

1 #include <opencv2/opencv.hpp>
2 #include <iostream>
3 #include <math.h>
4 
5 using namespacestd;6 using namespacecv;7 
8 intmain()9 {10     //1、Mat 对象与 IplImage 对象11     //1.1、IplImage 对象是从 2001 年 OpenCV 发布之后就一直存在的,它是一个具有 C 语言风格的数据结构,需要开发者自己分配和管理内存,在大型项目中容易导致内存泄漏。12     //1.2、Mat 对象是在 OpenCV 2.0 之后才引进的图像数据结构,自动分配内存,不存在内存泄漏的问题,是下面向对象的数据结构。该 Mat 对象分成两部分,头部和数据部分。13     //1.3、 Mat 类型构造函数和常用方法。14     // 
15     //1.3.1、构造函数16     //1.3.1.1、Mat()17     //1.3.1.2、Mat(int rows, int cols, int type);18     //1.3.1.3、Mat(Size size, int type);19     //1.3.1.4、Mat(int rows, int cols, int type, const Scalar& s);20     //1.3.1.5、Mat(Size size, int type, const Scalar& s);21     //1.3.1.6、Mat(int ndims, const int* sizes, int type);22     //1.3.1.7、Mat(int ndims, const int* sizes, int type, const Scalar& s);23     //还有很多,就不列举了24     // 
25     //1.3.2、常用方法26     //1.3.2.1、void copyTo( OutputArray m ) const;27     //1.3.2.2、void convertTo( OutputArray m, int rtype, double alpha=1, double beta=0 ) const;28     //1.3.2.3、Mat clone() const; 是完全拷贝,可以得到一个全新的 Mat 对象。29     //1.3.2.4、int channels() const;30     //1.3.2.5、int depth() const;31     //1.3.2.6、bool empty() const;32     //1.3.2.7、uchar* ptr(int i0=0);33     //还有很多,就不列举了34     // 
35     // 
36     //2、Mat 对象使用37     //2.1、部分复制:一般情况下只会复制 Mat 对象的头部和指针部分,数据部分是不会复制的。38     //Mat a=imread(imagefilePath);39     //Mat b(a); 通过拷贝构造就不会数据部分。40     //2.2、完全复制:如果想把 Mat 对象的头部和数据部分一起复制,可以通过 Mat 的 clone() 或者 copyTo() 方法。41     //2.3、使用 Mat 对象的四个要点42     //A、输出图像的内存是自动分配的。43     //B、使用OpenCV的 C++ 接口,不需要考虑内存分配的问题。44     //C、赋值操作和拷贝构造函数只会复制头部和指针部分,数据部分不会复制。45     //D、使用 clone 和 copyTo 两个函数可以实现完全复制。46     // 
47     //3、Mat 定义数组48     //创建多维数据 Mat::create49     //int sz[3]={2,2,2};50     //Mat a(3,sz,CV_8UC1,Scalar::all(0));
51 
52 
53 Mat src;54     src = imread("F:\\TestImage\\ZZImage\\psb14.jpg", IMREAD_UNCHANGED);55     if(src.empty())56 {57         cout << "加载图像有错误!!" <<endl;58         return -1;59 }60 
61     namedWindow("DemoWindow", WINDOW_AUTOSIZE);62     imshow("DemoWindow", src);63 
64 
65 
66     /*Mat dst;67 dst = Mat(src.size(),src.type());68 dst = Scalar(0,255,0);*/
69 
70 
71     //Mat dst;72     //dst = Mat(src.size(), src.type());//此语句可省略73     //dst = src.clone();
74 
75     /*Mat dst;76 src.convertTo(dst,src.type());*/
77 
78     /*Mat dst;79 src.copyTo(dst);*/
80 
81     /*Mat dst;82 
83 cvtColor(src, dst, COLOR_BGR2GRAY);84 cout << "原图通道:" << src.channels() << endl;85 cout << "目标图通道:" << dst.channels() << endl;86 
87 int height = dst.rows;88 int width = dst.cols;89 cout << "行数:" << height << endl;90 cout << "列数:" << width << endl;91 
92 const uchar* firstRow = dst.ptr<uchar>(0);93 cout << "第一行的值:" << *firstRow << endl;*/
94 
95     /*Mat dst(3, 3, CV_8UC1, Scalar(0));96 
97 cout << "dst=" << endl << dst << endl;*/
98 
99     //Mat dst;100     //dst.create(src.size(),src.type());
101     ////通过 create 方法不能直接复制。
102     //dst = Scalar(0,0,255);
103 
104 
105     /*Mat dst = Mat::zeros(src.size(),src.type());*/
106 
107     Mat dst = Mat::eye(2,2,CV_8UC1);108     cout << "dst=" << endl << dst <<endl;109 
110     namedWindow("DemoWindow2", WINDOW_AUTOSIZE);111     imshow("DemoWindow2", dst);112 
113     waitKey(0);114 
115     return 0;116 }

内容很简单,就不多说了。


三、总结
这是 C++ 使用 OpenCV 的第四篇文章,其实也没那么难,感觉是不是还是很好入门的,那就继续。初见成效,继续努力。皇天不负有心人,不忘初心,继续努力,做自己喜欢做的,开心就好。