2023年12月

本文全面探讨了人脸识别技术的发展历程、关键方法及其应用任务目标,深入分析了从几何特征到深度学习的技术演进。

关注TechLead,分享AI全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、人脸识别技术的发展历程

file
人脸识别技术作为一种生物识别技术,在过去几十年中经历了显著的发展。其发展可以分为几个主要阶段,每个阶段都对应着特定的技术进步和应用模式的变化。

早期探索:20世纪60至80年代

在这个阶段,人脸识别技术的研究还处于起步阶段。最初的方法侧重于几何特征的手动测量和比较,如眼睛、鼻子和嘴的相对位置。这些方法的精度受限于图像质量和手动测量的不准确性。

技术价值点:

  • 几何特征方法
    :标志着对人脸识别的第一步尝试,奠定了后续自动化和算法化发展的基础。

自动化与算法化:20世纪90年代

随着计算机视觉和图像处理技术的进步,人脸识别开始转向更自动化的方法。这一时期,特征匹配和模板匹配技术开始流行。例如,基于特征的识别方法(如Eigenfaces)通过提取和比较面部的主要特征,实现了更高的识别准确率。

技术价值点:

  • Eigenfaces方法
    :利用主成分分析(PCA),这是第一次使用统计方法对面部图像进行编码和识别。
  • 模板匹配技术
    :这为后续更复杂的人脸识别算法奠定了基础。

深度学习的革命:21世纪初至今

深度学习的兴起彻底改变了人脸识别领域。卷积神经网络(CNN)的应用大幅提高了识别的准确度和效率,尤其是在大规模人脸数据库中。现代人脸识别系统能够处理更复杂的变化,如不同的光照条件、表情变化和姿态变化。

技术价值点:

  • 卷积神经网络(CNN)
    :CNN能够自动学习和提取高层次的面部特征,大大提高了识别的准确性。
  • 大数据和GPU加速
    :海量数据的训练和GPU的加速计算为深度学习模型的训练提供了可能。
  • 跨领域应用
    :深度学习使得人脸识别技术在安全、金融、零售等多个领域得到应用。

二、几何特征方法详解与实战

file
几何特征方法是人脸识别领域的一种传统技术。它依赖于面部的特定几何标记,如眼睛、鼻子和嘴的位置,以及这些标记之间的距离和角度。

几何特征方法的原理

这种方法的基本思想是,每个人的面部几何结构都是独特的。通过测量这些结构之间的相对位置和大小,可以生成一个独特的面部“指纹”。这种方法通常包括以下步骤:

  1. 面部检测
    :首先确定图像中面部的位置。
  2. 特征点定位
    :识别面部的关键特征点,如眼角、鼻尖、嘴角等。
  3. 特征提取
    :计算这些特征点之间的距离和角度。
  4. 面部比对
    :将提取的特征与数据库中的特征进行比对,以识别个体。

几何特征方法的局限性

尽管这种方法在早期人脸识别系统中被广泛使用,但它有一些局限性:

  • 对图像质量敏感
    :几何特征方法对图像的大小、分辨率和光照条件非常敏感。
  • 缺乏灵活性
    :它难以处理面部表情变化、姿态变化或部分遮挡的情况。
  • 手动特征点标定的挑战
    :早期的方法需要手动标记特征点,这既费时又不精确。

实战案例:简单的几何特征人脸识别

为了展示几何特征方法的基本原理,我们将使用Python编写一个简单的人脸识别脚本。

环境配置

首先,需要安装必要的库,例如
OpenCV
,它是一个开源的计算机视觉和机器学习软件库。

!pip install opencv-python

代码实现

import cv2
import math

# 加载面部和眼睛检测器
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')

def calculate_distance(p1, p2):
    """计算两点之间的距离"""
    return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)

def geometric_features(image_path):
    """处理图像并提取几何特征"""
    # 读取图像
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 检测面部
    faces = face_cascade.detectMultiScale(gray, 1.3, 5)
    for (x, y, w, h) in faces:
        roi_gray = gray[y:y+h, x:x+w]

        # 检测眼睛
        eyes = eye_cascade.detectMultiScale(roi_gray)
        if len(eyes) >= 2:
            # 选取两个主要的眼睛
            eye1 = (eyes[0][0], eyes[0][1])
            eye2 = (eyes[1][0], eyes[1][1])

            # 计算眼睛间距
            eye_distance = calculate_distance(eye1, eye2)
            return eye_distance

    return None

# 示例:处理图像并提取几何特征
eye_distance = geometric_features('path_to_image.jpg')
print(f"Eye Distance: {eye_distance}")

代码说明

在这个简单的例子中,我们使用OpenCV库来检测面部和眼睛。然后,我们计算两只眼睛之间的距离作为一个基本的几何特征。尽管这个例子相对简单,但它展示了几何特征方法的基本思路。

三、自动化与算法化详解与实战

file
自动化与算法化标志着人脸识别技术的一个重要转折点。在这个阶段,人工干预逐渐减少,计算机视觉和模式识别算法开始在人脸识别过程中扮演核心角色。

自动化与算法化的进展

这一阶段的主要进展体现在以下几个方面:

  1. 特征自动提取
    :通过算法自动识别和提取面部特征,减少了对人工干预的依赖。
  2. 模板匹配技术
    :使用一系列标准化的面部模板来识别个体。
  3. 特征融合方法
    :结合多种类型的特征,如几何特征、纹理特征等,以提高识别的准确性和鲁棒性。

技术创新点:

  • 特征自动提取
    :引入更先进的图像处理技术,如边缘检测、纹理分析等。
  • 模板匹配
    :这种方法简化了识别过程,适用于较小规模的人脸识别应用。

实战案例:基于特征匹配的人脸识别

在本实战案例中,我们将使用Python和OpenCV库来实现一个基于特征匹配的简单人脸识别系统。

环境配置

首先,需要安装必要的库,例如
OpenCV

!pip install opencv-python

代码实现

import cv2
import numpy as np

# 加载人脸检测器
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

def feature_matching(image_path, template_path):
    """使用特征匹配进行人脸识别"""
    # 读取图像和模板
    img = cv2.imread(image_path)
    template = cv2.imread(template_path, 0)
    w, h = template.shape[::-1]

    # 转换为灰度图
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 人脸检测
    faces = face_cascade.detectMultiScale(gray_img, 1.1, 5)
    for (x, y, w, h) in faces:
        roi_gray = gray_img[y:y+h, x:x+w]

        # 模板匹配
        res = cv2.matchTemplate(roi_gray, template, cv2.TM_CCOEFF_NORMED)
        threshold = 0.8
        loc = np.where(res >= threshold)

        for pt in zip(*loc[::-1]):
            cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), (0, 255, 0), 2)

    cv2.imshow('Detected Faces', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 示例:使用特征匹配进行人脸识别
feature_matching('path_to_image.jpg', 'path_to_template.jpg')

代码说明

这个脚本首先读取一张图片和一个人脸模板。然后,使用OpenCV的模板匹配功能在图片中查找与模板相似的区域。如果找到匹配度高的区域,脚本将在这些区域周围绘制矩形框。

四、深度学习方法

file
深度学习方法在人脸识别领域引起了一场革命。通过利用大数据和强大的计算能力,深度学习算法能够学习复杂的面部模式,大幅提升识别的准确性和效率。

深度学习方法的核心概念

  1. 卷积神经网络(CNN)
    :CNN是深度学习中最常用于图像识别的模型之一。它通过多个卷积层自动提取图像的特征。
  2. 数据和训练
    :深度学习模型需要大量的数据进行训练。数据的质量和多样性对模型的性能有重要影响。
  3. 优化和调整
    :模型的结构和训练过程需要细致地调整,以提高准确率和处理复杂场景的能力。

技术创新点

  • 自动特征提取
    :深度学习模型能够自动学习面部的复杂特征,无需手动设计。
  • 大规模数据处理
    :深度学习能够有效处理和学习海量的图像数据。

实战案例:使用深度学习进行人脸识别

在这个实战案例中,我们将使用Python和PyTorch框架来实现一个基于深度学习的人脸识别系统。

环境配置

首先,需要安装必要的库,包括
PyTorch

OpenCV

!pip install torch torchvision
!pip install opencv-python

代码实现

import torch
import torchvision
import cv2
import torch.nn as nn
import torch.nn.functional as F
from torchvision import transforms

# 定义一个简单的CNN模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=5)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=5)
        self.fc1 = nn.Linear(1024, 128)
        self.fc2 = nn.Linear(128, 2)  # 假设有两个类别

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2(x), 2))
        x = x.view(x.size(0), -1)  # 展平
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# 加载模型
model = SimpleCNN()
model.load_state_dict(torch.load('model.pth'))
model.eval()

# 图像预处理
transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.Grayscale(),
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
])

def predict_face(image_path):
    """预测图像中的人脸"""
    img = cv2.imread(image_path)
    img = transform(img)
    img = img.unsqueeze(0)  # 增加一个批次维度

    with torch.no_grad():
        outputs = model(img)
        _, predicted = torch.max(outputs, 1)

    return predicted.item()

# 示例:预测图像中的人脸
result = predict_face('path_to_face_image.jpg')
print(f"Predicted class: {result}")

代码说明

在这个例子中,我们定义了一个简单的卷积神经网络模型,并加载了预先训练好的模型权重。图像通过一系列的预处理操作,然后被输入到模型中进行预测。这个简单的案例展示了如何使用深度学习进行基本的人脸识别。

总结

人脸识别技术的发展历程展示了技术创新的连续性和累积性。从最初的几何特征方法到现代的深度学习方法,每一步技术进步都是建立在前人基础之上的。这种连续的技术进化不仅推动了识别准确率的提高,也促进了人脸识别在更广泛领域的应用。

深度学习时代的到来凸显了大数据在人脸识别技术中的重要性。数据的质量、多样性和规模直接影响到模型的性能。未来,如何有效收集、处理和利用数据,将是技术发展的关键。

随着技术的发展和应用领域的拓展,隐私和伦理问题日益凸显。如何在提升技术性能的同时保护用户隐私,是人脸识别技术未来发展需要着重考虑的问题。未来的技术创新将不仅仅聚焦于提高算法的性能,也将包括如何设计符合伦理标准和隐私保护的应用系统。

未来人脸识别技术可能会与其他技术领域,如人工智能的其他分支、物联网、移动计算等领域进行更深层次的融合。这种跨领域的融合不仅能够提高识别技术的准确性和适用性,也能够创造出全新的应用场景和业务模式。

总的来说,人脸识别技术的未来发展将是一个多维度、跨学科的过程。这一过程不仅涉及技术层面的创新,也包括对社会、法律和伦理方面问题的深入思考。随着技术的不断成熟和社会对隐私权益的日益重视,人脸识别技术的健康和可持续发展将更加受到重视。

关注TechLead,分享AI全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

官方接口图

我实际会用到的就几个接口,背面的话就一个M.2固态的位置:

其中WIFI模块的接口应该也可以插2230的固态,不过是pcie2.0的速度,背面的接口则是pcie3.0*4的速度,差距还是挺大的。

开始安装系统

准备工作

  1. 一张内存卡(如果买的时候没有emmc的话)
  2. 下载并安装
    balenaEtcher
  3. 一个m.2固态硬盘
  4. 读卡器(可选,有的电脑能直接插内存卡)

下载系统镜像

可选镜像看
官网
,我选择的是第三方维护的
Ubuntu

香橙派5plus下载的是这个文件:

如果不知道自己的设备下载的哪个文件可以先打开
Wiki
看一下,比如

也可以去
https://joshua-riek.github.io/ubuntu-rockchip-download/
这个链接下载。

系统刷入内存卡

刷入前先解压下下载的系统镜像,最后得到一个img格式的文件,然后在balenaEtcher里选择这个镜像和你的内存卡,点击
现在烧录
,等待烧录成功的提示。

启动系统

假设你现在已经将固态硬盘、内存卡和网线都插上了,现在只需要接通电源后将HDMI的接口接到显示器上,按下电源按钮,等待片刻。

电源按钮旁边的红灯会开始闪烁,然后显示器也会显示Ubuntu的启动画面,使用
ip addr
查看IP地址后,然后使用xshell连接到ssh。

先运行下
sudo fdisk -l | grep nvme0n1
查看下固态是否被识别,我以前买的杂牌固态不识别,换了下双十一新买的识别正常

主要看有没有
/dev/nvme0n1
这个设备

先将bootloader刷写到SPI Flash(时间较长,耐心等待):

sudo u-boot-install-mtd /dev/mtdblock0

然后将系统复制到固态:

sudo ubuntu-rockchip-install /dev/nvme0n1

完成后关机,把内存卡取下来断电再接电系统应该就已经启动了,不需要再按电源键

默认启动顺序

这个bootloader默认加载系统的顺序

  • SD Card
  • USB
  • NVMe
  • eMMC

参考

1.简介

按理说,现在这种一闪而过的toast的已经相当普及或者是见怪不怪了,应该网上的大网站会用到的,偶然的在一次租房中,看到了这种场景,所以宏哥决定将其拿来主义,进行演示实践一下。

2.租房网站

事先声明啊,宏哥没有收他们的广告费啊,纯粹是为了学习和实战的。

2.1被测网址

1.被测网址的地址:

https://login.anjuke.com/login/form?history=aHR0cHM6Ly9iZWlqaW5nLmFuanVrZS5jb20v

2.网页如下图:

3.项目实战

今天就利用上边的实际网站,进行讲解和演示一下,同时来巩固一下前边的知识,温故而知新。

3.1代码设计

3.2参考代码


# coding=utf-8

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍
TableView

TreeView
组件联动的常用方法及灵活运用。

本章我们继续实现表格的联动效果,当读者点击
TableView

TreeView
中的某一行时,我们让其实现自动跟随功能,且当用户修改行中特定数据时也让其动态的跟随改变,首先绘制一个主界面如图,分别放置两个组件框,底部保留两个按钮,按钮1用于该表表格的行列个数,按钮2则用于设置
TableView
表格表头参数,整个表格我们将其设置为可编辑状态。

在函数中我们需要定义一个
QStandardItemModel
模型,这个模型的作用在之前的文章中有具体介绍,它是一个灵活且功能强大的模型类,适用于需要自定义数据结构、支持编辑、表头等功能的场景。通常用于与视图组件(如
QTableView

QTreeView
等)一起使用。它提供了一个表格结构,可以包含行和列,每个单元格可以存储一个
QStandardItem
对象。

这里的
QStandardItemModel
只适用于将两个不同类型的组件进行关联,简单点来说就是将两个组件指向同一个数据容器内,这样当用户修改任意一个组件内的数据另一个组件也会同步发生变更,但要想实现联动则还需要使用
QItemSelectionModel
模型,它负责跟踪哪些项被选中,以及在模型中项的选择状态发生变化时发出信号。

以下是
QItemSelectionModel
的一些重要特性和方法:

  • 选择项:
    负责管理模型中的项的选择状态,可以单独选择项、选定范围内的项或清除所有选择项。
  • 信号:
    当选择状态发生变化时,
    QItemSelectionModel
    会发出相应的信号,如
    selectionChanged
    信号。
  • 选择模式:
    提供多种选择模式,包括单选、多选、扩展选择等,可通过设置
    SelectionMode
    进行配置。
  • 选择策略:
    提供多种选择策略,用于定义选择行为,如
    SelectItems

    SelectRows

    SelectColumns
    等。
  • 与视图的集成:
    通常与
    QTableView

    QTreeView
    等视图组件结合使用,以实现对视图中项的选择操作。

该组件是实现模型-视图架构中选择的关键组件。通过它,可以轻松管理和操作模型中的项的选择状态,实现各种灵活的用户交互。下面是
QItemSelectionModel
类的一些主要方法:

方法 描述
QItemSelectionModel(QAbstractItemModel *model, QObject *parent = nullptr) 构造函数,创建一个与指定模型关联的
QItemSelectionModel
对象。
QModelIndexList selectedIndexes() const 获取当前被选中的项的索引列表。
void clear() 清除所有的选择项。
void setSelectionMode(QItemSelectionModel::SelectionFlags mode) 设置选择模式,可以选择多个项、单个项等。
void setSelectionBehavior(QItemSelectionModel::SelectionBehavior behavior) 设置选择策略,如选择单个项、选择整行、选择整列等。
void select(const QModelIndex &topLeft, const QModelIndex &bottomRight, QItemSelectionModel::SelectionFlags command) 在指定范围内进行选择操作,使用
SelectionFlags
定义选择操作。
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) 当选择状态发生变化时发出的信号,可以通过连接这个信号来处理选择状态变化的事件。
bool hasSelection() const 判断是否有选中的项。

上述方法提供了管理选择项的一些基本操作,包括清除选择、获取选中项的索引、设置选择模式和策略,以及在指定范围内进行选择操作。


MainWindow
构造函数中,我们以此执行如下关键部分,来实现对主界面的初始化工作;

创建模型和选择模型

首先创建一个包含4行5列的
QStandardItemModel
模型,并为其创建了一个
QItemSelectionModel
选择模型。

model = new QStandardItemModel(4, 5, this);
selection = new QItemSelectionModel(model);

关联到
tableView

treeView

将模型和选择模型关联到
tableView

treeView
上,这样它们会共享同一份数据模型,也就是无论两个组件哪一个发生变化均会影响双方组件中的内容。

ui->tableView->setModel(model);
ui->tableView->setSelectionModel(selection);

ui->treeView->setModel(model);
ui->treeView->setSelectionModel(selection);

添加表头与初始化数据

创建一个包含列名的
HeaderList
字符串列表,并将其设置为模型的水平表头标签。继续创建一个包含三个字符串列表的数组
DataList
,每个列表代表一行数据。然后使用嵌套的循环遍历数组,将数据逐个添加到模型中。

QStringList HeaderList;
HeaderList << "序号" << "姓名" << "年龄" << "性别" << "婚否";
model->setHorizontalHeaderLabels(HeaderList);

QStringList DataList[3];
QStandardItem *Item;

DataList[0] << "1001" << "admin" << "24" << "男" << "是";
DataList[1] << "1002" << "lyshark" << "23" << "男" << "否";
DataList[2] << "1003" << "lucy" << "37" << "女" << "是";

通过循环添加数据到模型

使用两个循环,外层循环遍历数组,内层循环遍历每个数组中的元素,创建
QStandardItem
对象并将其添加到模型的相应位置。

cppCopy codeint Array_Length = DataList->length();               // 获取每个数组中元素数
int Array_Count = sizeof(DataList) / sizeof(DataList[0]);        // 获取数组个数

for(int x=0; x<Array_Count; x++)
{
    for(int y=0; y<Array_Length; y++)
    {
        Item = new QStandardItem(DataList[x][y]);
        model->setItem(x, y, Item);
    }
}

如上这段代码初始化了一个包含表头和数据的
QStandardItemModel
模型,然后将模型和选择模型关联到
tableView

treeView
上,最后通过循环将数据逐个添加到模型中。这样就创建了一个主窗口,其中包含了一个表格视图和一个树形视图,它们共享相同的数据模型。如下图所示;

DialogSize.ui

接着来看
on_pushButton_clicked
按钮是如何实现的,该按钮主要用于实现改变表格行与列,当点击后则会弹出一个
DialogSize
自定义对话框,至于对话框是如何添加的在之前的文章中已经详细介绍过了。

在如下代码中我们通过
model->rowCount()
以及
model->columnCount()
获取到父
UI
界面中
tableView
表格的行列数,并通过
ptr->setRowColumn
将这些数据设置到了子对话框的编辑框上面,而
ptr->columnCount()
则用于接收子对话框的返回值,并将其动态设置到对应的模型中;

void MainWindow::on_pushButton_clicked()
{
    // //模态对话框,动态创建,用过后删除
    DialogSize *ptr = new DialogSize(this);     // 创建一个对话框
    Qt::WindowFlags flags = ptr->windowFlags(); // 需要获取返回值
    ptr->setWindowFlags(flags | Qt::MSWindowsFixedSizeDialogHint);  // 设置对话框固定大小
    ptr->setRowColumn(model->rowCount(),model->columnCount());      // 对话框数据初始化

    int ref = ptr->exec();             // 以模态方式显示对话框
    if (ref==QDialog::Accepted)        // OK键被按下,对话框关闭
    {
        // 当BtnOk被按下时,则设置对话框中的数据
        int cols=ptr->columnCount();
        model->setColumnCount(cols);

        int rows=ptr->rowCount();
        model->setRowCount(rows);
    }

    // 最后删除释放对话框句柄
    delete ptr;
}

接着来看下子对话框
DialogSize
做了什么,在对话框代码中
rowCount()
是给主窗体调用的函数其功能是获取到当前对话框中
spinBoxRow
组件中的数值,而
columnCount()
同理用于得到
spinBoxColumn
组件中的数值,最后的
setRowColumn()
则是用于接收主窗体的船只,并设置到对应的子对话框上的
SpinBox
组件内,其代码如下;

DialogSize::DialogSize(QWidget *parent) :QDialog(parent),ui(new Ui::DialogSize)
{
    ui->setupUi(this);
}

DialogSize::~DialogSize()
{
    delete ui;
}

// 主窗体调用获取当前行数
int DialogSize::rowCount()
{
    return  ui->spinBoxRow->value();
}

// 主窗体调用获取当前列数
int DialogSize::columnCount()
{
    return  ui->spinBoxColumn->value();
}

// 设置主窗体中的TableView行数与列数
void DialogSize::setRowColumn(int row, int column)
{
    ui->spinBoxRow->setValue(row);
    ui->spinBoxColumn->setValue(column);
}

运行程序,并点击左侧第一个按钮,此时我们可以将表格设置为
6*6
的矩阵,如下图所示;

DIalogHead.ui

对于第二个按钮
on_pushButton_2_clicked
的功能实现与第一个按钮完全一致,该按钮主要实现对父窗体中
TableView
的表头进行重新设置,在弹出对话框之前,需要将当前表头元素复制到
strList
列表容器内,并通过使用子对话框中的
ptr->setHeaderList
将其拷贝到子对话框中,并通过
QDialog::Accepted
等待对话框按下修改按钮,如下代码所示;

void MainWindow::on_pushButton_2_clicked()
{
    DialogHead *ptr = new DialogHead(this);
    Qt::WindowFlags flags = ptr->windowFlags();
    ptr->setWindowFlags(flags | Qt::MSWindowsFixedSizeDialogHint);

    // 如果表头列数变化,则从新初始化
    if(ptr->headerList().count() != model->columnCount())
    {
        QStringList strList;

        // 获取现有的表头标题
        for (int i=0;i<model->columnCount();i++)
        {
            strList.append(model->headerData(i,Qt::Horizontal,Qt::DisplayRole).toString());
        }

        // 用于对话框初始化显示
        ptr->setHeaderList(strList);
    }

    // 调用弹窗
    int ref = ptr->exec();
    if(ref==QDialog::Accepted)
    {
        // 获取对话框上修改后的StringList
        QStringList strList=ptr->headerList();

        // 设置模型的表头标题
        model->setHorizontalHeaderLabels(strList);
    }

    delete ptr;
}

当读者按下了修改按钮之后,由于通过
ui->listView->setModel(model)
已经与父窗体建立了关联,则此时通过
model->setStringList(headers)
就可以实现对父窗体中数据的修改,代码如下所示;

DialogHead::DialogHead(QWidget *parent) :QDialog(parent),ui(new Ui::DialogHead)
{
    ui->setupUi(this);
    model = new QStringListModel;
    ui->listView->setModel(model);
}

DialogHead::~DialogHead()
{
    delete ui;
}

// 设置当前listView中的数据
void DialogHead::setHeaderList(QStringList &headers)
{
    model->setStringList(headers);
}

// 返回当前的表头
QStringList DialogHead::headerList()
{
    return model->stringList();
}

程序运行后,读者可以先将表格的行与列修改为
7*7
,接着再通过设置表头的方式更新表头,效果如下;

NAVER 是一家多元化的互联网公司,拥有韩国最大的搜索引擎并在人工智能、自动驾驶等高科技领域积极投入。

在搭建 AI 平台时,NAVER 评估了公有云平台的存储产品、Alluxio 以及高性能专用存储产品等多种选项后,最终决定采用 JuiceFS。通过使用JuiceFS,NAVER 成功地将内部存储资源升级为高性能、适应 AI 工作负载的存储解决方案。

AiSuite 是 NAVER 开发者所使用的人工智能平台,它支持 NAVER 的各种服务的开发和运维。

AiSuite 提供基于 Kubernetes 的容器环境,用于高效管理成本高昂的 GPU 资源。它支持 Kubeflow,不仅便于 AI 模型的开发,还能整合模型训练和部署服务的完整 AI 工作流程。此外,AiSuite 还支持使用集成了公司内部数据平台 Kubeflow 工作流组件。

在建设 AI 平台时,最大的挑战就是提供适合 AI 工作负载的存储。
随着大型语言模型(LLM)的普及,为了生成优质的 AI 模型,所需数据的规模越来越大,且分布式学习需要多个节点能够同时访问数据。此外,还应能够轻松应用像
Llama 2

MPT
等迅速出现的各种大型语言模型开源项目。
适用于 AI 平台的存储需求如下:

  • 必须能够处理大规模数据;

  • 为了进行重复的训练,高性能是必须的;

  • 必须能作为 Kubernetes 持久卷(persistent volume)使用,即支持
    Kubernetes CSI Driver

  • 为了确保能够直接使用各种开源软件和库而无需进行任何修改,存储系统应具备 POSIX 兼容性;

  • 对于分布式学习和大规模服务,必须支持多个进程同时访问(参考
    ReadWriteMany, ReadOnlyMany
    );

  • 必须确保数据的一致性;

  • 运维工作应当尽可能小。

寻找能够满足所有这些要求的存储解决方案并非易事。云平台如
AWS EFS

Google Filestore
等服务与这些要求相似。但是,这些服务与 AWS S3 或 Google Cloud Storage 等对象存储服务相比,它们的成本要高得多(标准费率下 EFS和 AWS S3 有10倍的差异)。此外,由于 AiSuite 是在 NAVER 内部部署的,因此无法使用 AWS、GCP 等外部云存储服务。我们也可以引入一些专用的存储解决方案,如
DDN EXAScaler
,但这会带来高昂的成本。本文将介绍为解决这些问题所进行的探讨以及引入新存储解决方案的经验。

01 存储方案选型

我们曾考虑引入像
GlusterFS

CephFS
这样的开源解决方案,但由此会带来比较大的运维负担。

我们希望能够使用 NAVER 内部现有资源可支持的存储解决方案。 NAVER 内部现有的存储方案情况如下:

  • NAVER 的 C3 HDFS(Hadoop 分布式文件系统)可以处理大规模数据。但是,由于 HDFS 不支持 Kubernetes CSI Driver,因此无法将其用作 Kubernetes 的持久卷;

  • NAVER 的对象存储 nubes 也可以处理大规模数据,并支持 CLI、REST、S3、POSIX 等多种接口。但是,由于对象存储的特性,它并不完全支持 POSIX API。此外,由于不支持 Kubernetes -- CSI Driver,因此也不能将其用作 Kubernetes 的持久卷;

  • Ceph RBD 不支持 ReadWriteMany,
    ReadOnlyMany
    ,因此无法在多个 Pod 中同时访问;

  • NFS 可以简单设置,但存在扩展性和高可用性(HA)的问题;

  • Local Path
    将数据存储在 Kubernetes 节点的磁盘上。尽管访问速度快,但不支持多用户同时访问。此外,由于存储的数据位于各个节点上,因此需要进行特定的调度或在应用程序级别实现相关功能。

初步方案:引入 Alluxio

为了在 AiSuite 中快速且轻松地使用在 Hadoop 集群中处理并存储在 HDFS 上的数据,我们引入了
Alluxio
。但 Alluxio 在我们的场景中存在以下问题:

不完全的 POSIX 兼容性

虽然可以将 Alluxio 用作 Kubernetes 持久卷,但它不支持某些 POSIX API,例如符号链接、截断、fallocate、追加、xattr 等。例如,在挂载 Alluxio 的路径 /data 后,追加操作将失败,如下所示:

$ cd /data/
$ echo "appended" >> myfile.txt
bash: echo: write error: File exists  

许多 AI 开源软件和库默认实现为假设数据位于本地文件系统上。如果不支持某些 POSIX API,可能无法正常工作。因此,在使用 Alluxio 的情况下,有时需要将数据复制到
ephemeral storage
后再使用。这样做会导致 AI 开发变得不便和低效。

数据不一致

Alluxio 更类似于现有存储系统上的一个缓存层,并非一个独立的存储解决方案。当底层存储采用 HDFS 时,直接对 HDFS 进行更改而没有经过 Alluxio,可能会引起 Alluxio 与HDFS 数据的不同步。

在 Alluxio 中,可以设置与原始存储数据同步的时间间隔。更多详细信息,请参考
UFS Metadata Sync
。但是,如果同步过于频繁,会对原始存储产生过多的元数据请求。AiSuite 运行了一个以 HDFS 作为原始存储的 Alluxio 实例,以便与基于 Hadoop 的数据平台进行交互。但是,频繁同步导致管理 HDFS 元数据的
NameNode
负载增加。

运维压力

Alluxio 需要运行一个由 master 和 worker 服务器组成的单独集群,这也带来了一定的运维压力。不仅如此,由于 AiSuite 的所有用户都共享这个系统,一旦出现问题,可能会影响到所有用户。

为什么选择使用 JuiceFS

JuiceFS
是一种分布式文件系统,采用“数据”与“元数据”分离存储的架构,文件数据本身会被切分保存在
对象存储
(例如 Amazon S3),而元数据则可以保存在 Redis、MySQL、TiKV、SQLite 等多种
数据库
中,这使得企业能够利用现有存储和数据库。

接下来,我会详细介绍 JuiceFS,并解释为什么选择应用 JuiceFS。

配置

元数据引擎
(Metadata Engine):负责管理文件的元数据(文件名、大小等)。可以使用多种数据库,如 Redis、TiKV、MySQL/MariaDB、PostgreSQL 等(文档:
如何设置元数据
)。

数据存储
(Data Storage):实际存储数据的地方。可以使用多种存储,包括 S3、OpenStack Swift、Ceph、MinIO、HDFS 等(文档:
如何设置对象存储
);客户端(Client):与元数据引擎、数据存储进行交互,执行文件 I/O 操作。支持多种接口,适用于不同的环境;JuiceFS 的元数据和数据存储能够使用现有存储和数据库,并且可适配 Kubernetes 环境。例如,只要准备好了 S3 对象存储和 Redis,就可以通过 JuiceFS 创建一个高性能且功能丰富的存储解决方案。这也是 JuiceFS 吸引我们的原因。使用 NAVER 内部支持的存储和数据库,可以方便地搭建存储系统。

特性

JuiceFS 支持并发访问,同时支持 POSIX 和 Kubernetes 环境。满足前面提到的用于 AI 平台的存储要求,具体特性如下:

  • POSIX 兼容:可像本地文件系统一样使用;
  • HDFS 兼容:支持
    HDFS API
    ,可用于 Spark、Hive等数据处理框架;
  • S3 兼容:通过启用
    S3 网关
    ,可以使用 S3 兼容接口进行访问;
  • 云原生:支持
    CSI Driver
    ,可用于 Kubernetes 持久卷;
  • 分布式:可在多个服务器上同时共享;
  • 强一致性:提交的更改立即在所有服务器上生效;
  • 出色性能:详细信息请参阅性能
    基准测试
  • 数据安全:支持
    数据加密
  • 文件锁定:支持 BSD 锁(flock)和 POSIX 锁(fcntl);
  • 数据压缩:支持
    LZ4
    ,
    Zstandard
    ,可节省存储空间。

存储原理

JuiceFS 引入了以下概念来处理文件,目的是为了弥补分布式存储在物理上的分散性和对象存储中对象难以修改的缺点。

  • Chunk:每个文件被划分为 64MB 大小的 Chunk 进行管理。大文件可以根据偏移量并行读取或写入,这对于处理大规模数据非常有效;

  • Slice:每个 Chunk 由一个或多个 Slice 组成。每次写入时都会创建一个新的 Slice,它们可以与同一个 Chunk 的其他 Slice 重叠。在读取 Chunk 时,会优先读取最新的 Slice。为了避免过多的 Slice 导致读取性能下降,会定期将它们合并为一个。这使得 JuiceFS 能够灵活地修改文件,解决了对象存储在数据修改方面的限制;

  • Block:在实际存储中,Slice 被划分为基础大小为 4MB(最大可达 16MB)的 Block 进行存储。Chunk 和 Slice 主要是逻辑概念,而实际存储中可见的数据单位是 Block。通过分割为较小的 Block 并进行并行处理,JuiceFS 弥补了分布式对象存储远程且较慢的特点;

元数据引擎管理着诸如文件名、文件大小等元数据。此外,它还包含了文件与实际存储数据之间的映射信息。

缓存

JuiceFS 为了提高性能,采用了多个层级的缓存。在读取请求时,首先尝试从内核页缓存、客户端进程缓存和本地磁盘缓存中读取数据。若这些缓存未命中,则会从远端存储中读取所需数据。从远端存储中获取的数据随后会被异步地存储在各级缓存中,以便未来能更快速地访问同样的数据。

02 Alluxio vs JuiceFS

早期引入的 Alluxio 并没有满足我们所需的存储要求。那么 JuiceFS 呢?以下是对 Alluxio 和 JuiceFS 进行比较的表格。

JuiceFS 和 Alluxio 都支持多种接口,并可以通过缓存来提高性能。与 Alluxio 相比,JuiceFS 具有以下优点:

完全兼容 POSIX

Alluxio 在某些 POSIX API 上提供有限支持。而 JuiceFS 能够完全支持 POSIX 标准,因此可以像本地文件系统一样使用。
这意味着,无需修改存储在 JuiceFS 中的训练数据和代码,就可以使用各种 AI 开源工具和库。
下面是 POSIX 兼容性测试
pjdfstest
的结果。与AWS EFS 和 Google Filestore 相比,JuiceFS 在支持 POSIX 方面表现更佳。

强一致性

Alluxio 更接近于对原始存储进行缓存,而 JuiceFS 是一个独立的存储系统。在 JuiceFS 中,元数据由元数据引擎管理,不依赖于外部系统。数据存储仅用于存放 Block 数据。因此,不会像 Alluxio 那样出现与原始存储不同步的问题。

减轻运维负担

Alluxio 需要运行和维护 master 和 worker 服务器,这增加了一定的运维负担。此外,因为Alluxio被所有用户共享,一旦发生故障,可能会对所有用户造成影响。JuiceFS 可以直接利用现有熟悉的存储和数据库作为元数据引擎和数据存储,仅需 JuiceFS 客户端即可运行,无需部署独立服务器。此外,每个用户都可以独立配置自己的元数据引擎和数据存储,从而避免相互干扰。

03 如何使用 JuiceFS 构建存储方案

使用 JuiceFS,需要准备一个用作元数据引擎的数据库和对象存储。如前所述,JuiceFS 支持多种数据库和对象存储。为了减轻运维负担,我们使用了 NAVER 现有的平台。

如下图所示,AiSuite 通过以下内部平台使用 JuiceFS。

元数据引擎

在 NAVER,可以使用
nBase-ARC
Redis 服务或通过 MySQL 支持来设置元数据引擎。如果是进行开发和测试,也可以通过
Helm chart
直接安装并使用 Redis、PostgreSQL 等。JuiceFS 默认每小时将元数据自动备份到数据存储中,且备份周期是可配置的。因此,即使元数据引擎的数据丢失,也可以进行恢复,但由于元数据备份周期的设定,仍有可能会有部分数据丢失。有关详细信息,请参考
元数据备份和恢复

数据存储

可以使用 NAVER 内部的 HDFS 或 nubes Object Storage 存储大规模数据。利用这些资源,可以进行大容量且稳定的数据存储。

nubes

nubes
是 NAVER 的对象存储。JuiceFS 本身不直接支持 nubes,但可以通过使用具有 MinIO 接口的 nubes-s3-proxy 来访问。

HDFS

HDFS
是 JuiceFS 默认支持的存储系统。但是,为了在大规模、多租户的 HDFS 中应用 Kerberos,需要进行以下改进:

支持 Kerberos keytab 文件

NAVER 内部的 HDFS 应用了 Kerberos。因此,当 JuiceFS 使用 HDFS 时,需要进行 Kerberos 认证。原先的 JuiceFS 是通过设置 KRB5CCNAME 环境变量为
credential cache
进行 HDFS 认证。但这个方式会在一定时间后过期失效。为解决这一问题,进行了改进,可以通过设置 KRB5KEYTAB、KRB5PRINCIPAL 环境变量使用 keytab 文件。(参见
JuiceFS issue #3283

支持 base64 编码的 keytab 文件

AiSuite 是一个多租户 Kubernetes 集群,共享给多个用户,目标是允许每个用户使用自己选择的元数据引擎和数据存储来运行 JuiceFS。用户需要自己编写
Kubernetes Secret
,设置访问元数据引擎和数据存储的路径和认证信息。但是,KRB5KEYTAB 仅是一个文件路径,并不能让用户传递实际的 keytab 文件。为解决这个问题,进行了改进,允许通过设置 KRB5KEYTAB_BASE64 环境变量使用 base64 编码的 keytab 文件字符串。(参见
JuiceFS issue #3817

支持用户指定的 HDFS 路径

NAVER 内部的 HDFS 由多个用户共享,每个用户仅在分配给他们的 HDFS 路径上拥有权限。但是,原来的 JuiceFS 无法指定用于数据存储的 HDFS 路径,因此总是必须将数据存储在 root 目录下,这导致用户遇到了没有权限访问的路径问题。为解决这个问题,进行了改进,允许用户设置自己的 HDFS 路径。(参见
JuiceFS issue #3526

支持 HDFS 路径 hdfs://nameservice

NAVER 内部的 HDFS 为了大规模运营,采用了
HDFS Federation
,由多个 NameNode 和 Namespace 组成。原本的 JuiceFS 需要直接指定 NameNode 路径,如 nn1.example.com:8020,这对用户来说确认和设置很不方便。为了解决这个问题,进行了改进,现在可以像 hdfs://nameservice 这样设置 Namespace。(参见
JuiceFS issue #3576

CSI Driver

AiSuite 是一个多租户 Kubernetes 集群,每个用户通过
Kubernetes namespace
进行区分。如果用户间共享 JuiceFS,可能会相互影响,降低稳定性并增加运维难度。因此,我们的目标是使每个用户都能单独准备自己的元数据引擎和数据存储,并独立使用 JuiceFS。此外,为了减轻运维负担并提高用户便利性,支持
Dynamic Volume Provisioning
,使用户无需管理员介入,就能直接定义和使用 PVC(PersistentVolumeClaim)。为此,需要进行以下改进:

支持模板 Secret

用户需要创建
Secret
,以设置各自准备的元数据引擎和数据存储的访问路径和认证信息。然后,需要在
StorageClass
中设置以引用这些 Secret。这些设置的 Secret 会被用于 Dynamic Volume Provisioning。但是,原有的 JuiceFS CSI Driver 只能在 StorageClass 中设置一个固定的 Secret。为解决这一问题,进行了改进,允许通过 ${pvc.name}, ${pvc.namespace}, ${pvc.annotations['
']} 等方式,从用户创建的 PVC 中引用 Secret。(参见 JuiceFS CSI Driver issue #698

支持 Secret Finalizer

用户的 Secret 不仅在创建 PVC 时使用,还在删除 PVC 时用于删除 JuiceFS 数据。如果在删除 PVC 之前关联的 Secret 被移除,JuiceFS 数据将不会被清理,而会持续留存。为了防止这个问题,在 PVC 被删除之前,设置
Finalizer
以防止关联的 Secret 被移除。在 StorageClass 的
parameters
中设置 secretFinalizer: "true" 可以启用此功能。(参见
JuiceFS CSI Driver issue #707

支持根据 PVC 元数据设置 mountOptions

AiSuite 中存在多种 AI 工作负载,如 AI 学习、服务和数据处理等。为了实现最优性能,根据工作类型可能需要单独配置 JuiceFS。例如,在使用只读数据进行 AI 训练的情况下,可以通过添加
--open-cache
设置来提高读取性能。有关详细信息,请参考
客户端内存中的元数据缓存
。原先的 JuiceFS 只能应用 StorageClass 中固定的配置。现在进行了改进,允许根据用户创建的 PVC 进行设置,例如使用 ${.PVC.namespace}, ${.PVC.name}, ${.PVC.labels.foo}, ${.PVC.annotations.bar} 这样的方法。(参见
JuiceFS CSI Driver issue

04 JuiceFS 应用方案

为了在 Kubernetes 中支持 JuiceFS,管理员需要部署 JuiceFS CSI Driver,而用户则需要定义自己的 Secret 和 PVC。在多租户 Kubernetes 环境 AiSuite 中,将详细说明如何部署和提供 JuiceFS,包括具体的示例。

部署方法

安装 JuiceFS CSI Driver 后,可以按照标准 Kubernetes 卷的使用方式进行操作,支持通过 Helm 或 kubectl 进行安装。有关详细信息,请参考
JuiceFS CSI Driver 安装指南
。进行部署需要 Kubernetes 管理员权限。为了让每个用户使用自己的元数据引擎和数据存储,StorageClass 设置如下。

apiVersion: storage.k8s.io/v1  
kind: StorageClass  
metadata:  
  name: juicefs
provisioner: csi.juicefs.com  
parameters:  
  #配置系统以便用户可以引用自己创建的 Secret
  #用户需要在 PVC 'csi.juicefs.com/secret-name'注释中设置Secret的名称
  csi.storage.k8s.io/provisioner-secret-name: ${pvc.annotations['csi.juicefs.com/secret-name']}
  csi.storage.k8s.io/provisioner-secret-namespace: ${pvc.namespace}
  csi.storage.k8s.io/node-publish-secret-name: ${pvc.annotations['csi.juicefs.com/secret-name']}
  csi.storage.k8s.io/node-publish-secret-namespace: ${pvc.namespace}
  csi.storage.k8s.io/controller-expand-secret-name: ${pvc.annotations['csi.juicefs.com/secret-name']}
  csi.storage.k8s.io/controller-expand-secret-namespace: ${pvc.namespace}
  juicefs/clean-cache: "true"
  #激活 secretFinalizer,以防止用户定义的 Secret 被随意删除
  secretFinalizer: "true"
  #通过指定 pathPattern来设置路径,可以挂载所需的路径
  #用户可以在 PVC 'csi.juicefs.com/subdir'注释中设置所需的路径
  pathPattern: "${.PVC.annotations.csi.juicefs.com/subdir}"
allowVolumeExpansion: true  
reclaimPolicy: Delete  
mountOptions:  
  #允许用户根据需要设置各自的选项
  #用户可以在PVC的'csi.juicefs.com/additional-mount-options'注释中设置所需的JuiceFS选项。
  - ${.PVC.annotations.csi.juicefs.com/additional-mount-options}

使用方法

用户需要为自己准备的元数据引擎和数据存储创建 Secret,包括路径和认证信息等。

apiVersion: v1  
kind: Secret  
metadata:  
 name: myjfs
type: Opaque  
stringData:  
 #JuiceFS文件系统名称。
 name: myjfs
 #MINIO_ROOT_USER
 access-key: user
 #MINIO_ROOT_PASSWORD
 secret-key: password
 #元数据引擎 Redis 路径
 metaurl: redis://:@redis.user1.svc.cluster.local:6379/0
 #minio
 storage: minio
 #bucket1
 bucket: http://nubes-s3-proxy.user1.svc.cluster.local:10000/bucket1
 #https://juicefs.com/docs/community/command_reference/#format
 format-options: trash-days=0,block-size=16384

定义 PVC。此外,需要设置以下注释(annotation):

  • csi.juicefs.com/secret-name
    :指定引用的 Secret 名称;

  • csi.juicefs.com/subdir
    :如果是新的卷,则指定 PVC 名称。如果需要挂载已存在的 JuiceFS 路径,则可以指定所需的路径;

  • csi.juicefs.com/additional-mount-options
    :可以添加适合工作负载的 JuiceFS 挂载选项。有关详细信息,请参考
    挂载指南

apiVersion: v1  
kind: PersistentVolumeClaim  
metadata:  
 name: myjfs
 annotations:
   csi.juicefs.com/secret-name: myjfs # 之前创建的Secret名称
   csi.juicefs.com/subdir: myjfs # 在JuiceFS文件系统中的路径
   csi.juicefs.com/additional-mount-options: "writeback,upload-delay=1m" # 如果需要,可以添加JuiceFS的配置
spec:  
 accessModes:
 - ReadWriteMany
 resources:
   requests:
     storage: 100Gi
 storageClassName: juicefs

创建 Secret 和 PVC 后,可以像使用普通卷一样使用它们。下面是一个将之前创建的 myjfs PVC 挂载到 /data 上的 Pod 示例。

apiVersion: v1  
kind: Pod  
metadata:  
 name: example
spec:  
 containers:
 - name: app
...
   volumeMounts:
   - mountPath: /data
     name: juicefs-pv
 volumes:
 - name: juicefs-pv
   persistentVolumeClaim:
     claimName: myjfs

05 性能测试

JuiceFS 性能基准测试如下所示,与
EFS
,
S3FS
相比,其性能更高。

我们需要验证当使用 nubes 对象存储和 HDFS 作为数据存储时的性能表现
。由于 JuiceFS 的性能可能会根据所使用的数据存储类型而有所差异,我们还需关注由于使用 UserSpace 中的 Fuse 而可能出现的性能降低。特别是需要检验因 JuiceFS Fuse 引起的性能下降情况。测试的主要目的是确定,相比直接使用数据存储,JuiceFS 是否会导致性能下降。如果性能差异不显著,那么我们可以在保持性能的同时支持 POSIX 兼容性、并发访问等多项功能。

顺序读写

参考
Fio Standalone Performance Test
,按照以下方式进行了测试:

  • 元数据引擎使用 Redis;

  • 在单个节点上使用
    fio
    进行测试时,调整
    --numjobs
    选项;

  • 单节点的最大网络带宽为 1200MB/s,即此测试的可能最高值;

  • 由于 Object Storage 基本上不支持 POSIX,因此没有使用 fio,而是采用了针对 nubes Object Storage 性能优化的其他方法,仅测量 read(1 job), write(1 job) 项目;

  • JuiceFS 设置中,块大小设置为 16MB,其他选项使用默认值;

  • 没有使用 JuiceFS 缓存,仅进行了读写新数据的测试;

  • 对于 Alluxio,由于在执行 fio 时出现“fio: posix_fallocate fails: Not supported”等错误而导致失败,因此在测试中被排除了。

测试结果如下:

  • 在读取方面,与单独的 nubes 相比,JuiceFS+nubes 和 JuiceFS+hdfs 的性能更好,并且随着并发数量的增加而提升。这可能是因为通过 Chunk 进行的并发读取更为有利;

  • 在写入方面,与单独的 nubes 相比,JuiceFS+nubes 和 JuiceFS+hdfs 的性能相似或略低。并发数量增加时,性能会降低。这可能是由于多个 Slice 带来的负担所致。

文件创建

此次测试比较了创建 1 万个小文件所需的时间。测量使用 nubes 作为数据存储时处理元数据的性能,并与 JuiceFS 进行比较。

  • 元数据引擎使用 Redis;

  • 测量了在 10 个进程中使用 cp 命令复制 100 字节文件 1 万个时的每秒文件创建数;

  • 由于 Object Storage 基本上不支持 POSIX,因此没有使用 cp,而是采用了针对 nubes Object Storage 性能优化的其他方法来测量 nubes 项;

  • 也测量了设置了 JuiceFS 的 writeback 选项的情况。此选项首先在本地进行数据更新,然后异步保存到数据存储。更多详细信息,请参考
    client-write-cache

测试结果如下:

  • nubes 和 juicefs+nubes 之间没有显著差异。这意味着使用 nubes 作为数据存储的 JuiceFS 不会导致性能下降;

  • 与 HDFS 配合使用时, JuiceFS+hdfs 和 alluxio 似乎与 HDFS 的元数据处理性能,即 NameNode 的性能趋于一致;

  • 使用 writeback 选项可实现数十倍的性能提升。然而,启用 writeback 选项可能导致数据丢失,因此适用于临时数据的场景。

测试结论

JuiceFS 的性能基本上取决于存储数据设备的性能。它能够支持 POSIX 兼容性且无性能损失、还能支持并发访问等功能。

在某些工作负载和使用情况下,JuiceFS 的性能有时甚至可能优于数据存储设备的原始性能。尽管本文未进行测试,但读取缓存数据时,因为是从本地磁盘读取,因此有可能提高性能。写入临时数据时,应用 writeback 选项也可以提升性能。

JuiceFS 支持多种可根据工作负载进行调整的缓存选项。更多详细信息,请参考
cache

06 小结

企业内部存储 + JuiceFS

在 AiSuite 中,我们利用公司内部支持的 HDFS 和 nubes 对象存储来构建 JuiceFS。这样既能为 AI 工作负载提供适合的存储,又能减轻运维负担。对于前文中评估的企业内部存储,可以总结如下:

在 AiSuite 中,相比于 HDFS,更推荐使用 nubes Object Storage 用作数据存储。这是因为在 HDFS 中,如果文件数量众多,会对管理 HDFS 元数据的 NameNode 造成负担。而 JuiceFS 会将文件分割成小的 Block 来存储,从而在 HDFS 中产生大量文件。即使是最大可设置的 Block 大小 16MB,为了存储 1TB 数据,也需要创建超过 62,500 个文件。虽然考虑过将最大 Block 大小提升至 64MB,但 Block 变大可能会导致效率低下。更多详细信息,请参考
Increase the maximum of blockSize to 64MB

优势

在 AI 平台 AiSuite 中,我们评估了 JuiceFS 作为 AI 工作负载的存储解决方案的可行性。
JuiceFS 具有以下多种优势:

  • 可以使用大容量、可共享(ReadWriteMany, ReadOnlyMany)卷;

  • 高性能(缓存),可以替代 hostPath、local-path。可以轻松实现有状态应用的云原生转换;

  • 在 AI 分布式学习中,可以作为共享的工作区、checkpoint、日志存储;

  • 能够处理 AI 学习过程中所需的大量小文件(HDFS/Alluxio 的替代品);

  • 可利用企业内部的 HDFS、nubes Object Storage 存储,降低运维负担;

  • 通过用户各自的数据存储和元数据引擎运行,互不影响;

  • 支持多种数据存储和元数据引擎,适用于大多数 k8s 环境;

  • 可以替代高成本的共享存储,如 AWS EFS、Google filestore、DDN exascaler。

通过使用 JuiceFS,能够将企业内部传统的存储转换为具有高性能和多功能的、适合 AI 工作负载的存储。JuiceFS 支持多种存储和多种数据库。这篇文章主要介绍了在 NAVER 内部的 on-premise 环境中的应用案例,但它也可以应用于 AWS、Google Cloud 等公共云环境。希望这篇文章能对面临类似问题的用户提供帮助。

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