脑积水的原因

首页 » 常识 » 常识 » 我们是如何测试人工智能的三数据构造与
TUhjnbcbe - 2025/4/28 19:31:00

作者:山治

前言

人工智能场景中的性能测试与我们在互联网中创建到的有很大的不同,因为它需要模拟更复杂的情况。当然它也有相似的地方,只不过今天我们主要介绍它们不同的地方。

产品分类

首先我们需要澄清一下,从AI产品的类型来划分的话,我们可以分成两个大的类别:

人工智能的业务类产品:AI就是为了某个特定的业务服务的。它的形态可能就是一个模型。可以是用来做广告或者内容推荐的模型,也可以是用来做人脸识别,对话问答的模型。测试人员通常的测试对象就是这个模型,或者是搭载这个模型的上层业务,比较少会涉及到其他的东西,行业中做这种测试的人数占了大多数。

人工智能的平台类产品:随着AI能力进入到越来越多的业务中,大家发现高昂的人力和算力的成本都是非常大的负担,所以业界期望有一种产品能够大幅度的降低AI的投入成本,希望能够通过一个平台产品让用户使用更简单的方法(比如UI上的操作)就可以进行数据的ETL,特征的工程,模型的训练,上线,自学习等能力。所以现在AI如此流行的大环境下,越来越多这样的产品在业界中流行,几乎每个有点规模的AI公司都会有这样的产品。

在这个背景下,我们可以知道人工智能的平台类产品测试是更有难度的,它不是只对着一个模型,而是涉及到了AI的整个生命周期。而我们第一个性能测试场景,就是针对模型训练的。

结构化数据模拟

在大数据与人工智能领域中的性能测试,往往要涉及到数据模拟。因为人工智能的基础是大数据,只有海量的高质量数据才能支持人工智能。所以我们在做性能测试的时候,也需要构造大量的数据来支撑模型的训练。主要是为了衡量在标准数据下,算法需要多长时间才能把模型训练出来。所以需要构建各种不同的标准数据。比如:

不同的数据规模(行数,列数)

不同的数据分布(数据倾斜)

不同的数据分片,文件数量(模拟数据切片规模和海量小文件场景)

不同的特征规模(高维特征,低维特征)

接下来要仔细的解释一下这几种数据场景:

不同的数据规模:这个比较好理解,就不细说了,我们需要评估在不同数据规模下算法的性能表现。从而推测出一个资源和时间的推导公式。

不同的数据分布:往往在特征工程和ETL中使用。这个是跟分布式计算的原理有关系的,分布式计算简而言之就是一个分而治之,比如用一个经典的计算词频的DEMO来解释。当程序想要计算一个大文件中每个单词出现的次数的时候,需要把这些单词传输到独立的任务中。比如把所有的hello传输到任务A中,所有的world传输到任务B中。然后每个任务只统计一个单词出现的次数,最后再把结果进行汇总。整个过程如下图,分布式计算追求的就是用多台机器的资源来加速计算过程,简而言之是任务被划分成多个子任务调度在多个机器里,而每个任务只负责其中一部分数据的计算。在这样的原理之前我们会发现,如果hello这个单词出现的特别多,占了整个数据量的99%。那么我们会发现就会有一个超大的子任务存在(99%的数据传送到了一个子任务里),这样就破坏了分布式计算的初衷了。变成了无限趋近于单机计算的形式。所以一般要求我们的程序需要用一定的方法去处理数据倾斜的情况。

不同的数据分片和文件数量:了解到分布式计算的原理后,我们也需要知道分布式存储的原理,当一份数据过大后,也需要对这份数据进行切片并保存在不同的机器中,所以一份数据其实是有很多个数据分片的。而数据分片多了以后也会出问题,因为按照分布式计算的原理,每个数据分片也都会有一个独立的子任务去处理。一个独立的子任务就是一个线程,所以如果数据分片太多了,就会有过多的线程在系统中争抢CPU资源,并且每个线程其实也处理不了多少数据(因为数量分片太多,每个分片就没多少数据了)这时候大多数的资源都用来维护线程之间的开销了。而海量的文件数量也会给存储系统带来压力。

不同的特征规模:通过之前的文章我们知道工程师需要先从数据中提取特征,然后把特征输入给模型进行训练的。所以除了原始数据的规模外,重要的是经过特征工程后,有多少特征被提取了出来,特征的规模也直接影响了模型的性能和大小。而特征也分围离散特征与连续特征。它们最大的区别在于算法在计算对应字段的时候如何处理,对于离散特征来说算法会将每一个唯一的值作为一个独立的特征,比如我们有用户表,其中职业这个字段中我们有种职业,也就是说这张表不论有多少行,职业这个字段的取值就是这个职业中的一个。而对于算法来说每一个职业就是一个独立的特征。老师是一个特征,学生是一个特征,程序员、环卫工人、产品经理、项目经理等等这些都是独立的特征。也就是说对于这张表,不论有多少行,算法针对职业这一字段提取出来的特征就是固定的这个。而连续特征则完全不同,它是一个数字的数字,算法会把它当成一个独立的特征,也就是当算法把一个字段提取成为连续特征后,不管这张表有多少行,这个字段固定就只有1个特征被提出来。所以我们做性能测试的时候,场景描述通常都是:多少行,多少列,多少特征。

造数工具

造数工具的重点是可以模拟出上面说这些不同的数据类型,以及性能要符合预期。毕竟我们在建模的时候动不动就要模拟几千万甚至几亿的,所以性能一定不能太差,要是造个数据就要等好几天的也是无法接受的。所以我们一般也决定使用分布式计算技术来造数,而我这里选择的就是spark。

利用spark编写造数据工具

利用k8s/hadoop或者sparkcluster作为集群,把任务调度到集群中分布到多个机器上加速计算。

根据业务情况可以双写或者多写到多个存储系统中,并进行压缩和固化

模拟设备规模

在计算机视觉场景中,我们可能往往要面对大量的设备模拟工作。因为在系统中我们的数据都是从各个摄像设备中采集到视频流,并进行解码抽帧等工作得到图片的。而在这种情况下的性能测试,我们需模拟大量的设备来制造系统压力(上面的场景是模拟结构化数据来制造压力,而这个就是模拟视频流来制造压力)。当然我们不能真的去架设海量的摄像头。所以我们不需要模拟设备,而是模拟视频流就可以了(设备和产品的通信一般都是通过视频流的,比如rtsp的实时视频流协议)。所以我们的解决方案是下面这个样子的:

ffmpeg这个工具是常用的图像处理工具,我们可以从一个视频数据中提取数据并转成视频流

我们建立一个流媒体服务器,把ffmpeg转成的视频流推送到这个流媒体服务器中。这样系统就可以跟流媒体服务器进行通信了。这个流媒体服务器可以有很多种实现。我们随便找一个开源的就可以了,比如easydarwin。

假设我们需要模拟0路摄像头,那就可以用ffmpeg+easydarwin生成0个rtsp流地址并配置到被测的系统中。当然模拟这么大规模的视频流数据,需要给流媒体服务器相当的资源才可以。

模拟节点规模

在上面的模拟设备规模的场景中,我们往往也需要面对边缘计算系统的情况。因为我们的视频设备往往都假设在靠近用户的地方(原理中心集群),比如路边的摄像头,抓拍机等等。由于我们要面对数据安全,网络带宽和性能等问题,不能把数据传回中心集群处理。所以需要用到边缘计算

云:边缘计算是代替不了云计算的,它与云计算更像是相辅相成的关系。在边缘计算的业务中,仍然需要把部分服务部署在云端中心集群中来控制整体业务的策略。

边:部署在每个地域的边缘机房,它们与终端设备距离最近并部署了绝大部分计算服务,接收到终端设备的请求后就开始就地计算并进行实时的反馈。

端:终端设备的统称,这些设备可以是手机,可穿戴设备,工厂中的工控机或是路旁的摄像头。它们负责采集数据并传递给距离自己最近的边缘机房进行计算。

它的业务流程可能是下面这个样子的:

我喜欢把这种架构简称为云边端,分别代表了在边缘计算中这3种不同的角色。在计算机视觉场景中,客户的设备可能存在于园区/工厂/学校/社区的摄像头或其他照相设备。这些设备更靠近终端的人而非中心集群,它们收集到图像数据后再经过一系列的处理(解码,抽帧,过滤等)再传输到边缘机房部署的模型服务中进行识别,比如识别人体的行为(打架,离岗,跌倒,野泳等),人体的属性(安全帽,手套,安全带等),又或者识别烟雾火灾人脸对比等等。

完成这样的架构是非常困难的,从物理设施角度看,通常终端设备分布在非常多的地域中,企业需要在这些地域架设大量的机房。而从软件角度看,产品的架构要能够完成所有边缘节点的统一管理,完成机器的区域划分,服务的统一下发以及网络的分发策略等等。为了支持这种部署架构,人们寻求了很多的解决方案,尤其对当前最主流的云平台技术–K8S中开发相关功能的呼声尤其强烈。目前国内比较主流的边缘计算项目有superedge、OpenYurt和KubeEdge,他们都是基于k8s扩展而来。事实上人工智能产品需要的调度需求会更多,比如对于GPU、FPGA、TPU的调度,对于大型存储设备的调度,对于网络流量的调度等等。

所以我们除了要模拟大规模路数的视频流之外,还需要模拟大规模的节点数量来验证集群的性能是否达标。

如果按照传统的思路,测试团队需要申请大量的虚拟机来组建K8S集群,但这无疑是成本非常高的选择,因为在实际项目中我们往往需要测试数百甚至数千台机器规模的集群。所以换一种思路,看一下K8S的集群架构:

K8S集群中每台节点中都会启动一个名为kubelet和kubeproxy的进程。kubelet负责与docker/containerD通信,维护容器状态并周期性的向APIServer(K8S集群中的主要服务)上报节点和容器状态,而kubeproxy则负责容器网络规则维护。而APIServer则会负责把用户创建容器的请求以及kubelet上报的容器和节点状态存储在ETCD中。可以看出来APIServer是维护K8S集群状态最关键的服务,一旦APIServer出现问题整个集群都会陷入异常状态。而通过上述两点的描述也可以知道随着集群内节点和Pod的增多,APIServer承受的压力也会越来越大。

除此之外在商业产品中往往会由于各种需要自研许多组件与APIServer进行交互,这些组件都会给APIServer带来不小的压力。所以APIServer的性能数据往往是测试中最关键的指标之一。业界也会推荐在K8S集群中的etcd要使用高性能固态硬盘进行部署,因为etcd的性能也是影响APIServer最主要的因素。同时还需要注意的是除了APIServer以外K8S还有一些其他组件也是随着Pod数量增长而增加压力的(比如调度器和用户自研operator)。

所以随着节点和容器的增多,对集群的主要压力在于etcd和apiserver这些组件。只要我们模拟真实的kubelet来上报容器状态给APIServer增加压力,那其实是不是有一堆真实的节点和容器并不重要。我们构建一些虚假的kubelet就可以达到目标了。

kubemark是一款针对k8s的性能测试工具,它能够使用少量的资源模拟出一个大规模K8S集群。为了演示这个工具需要一个待测试的K8S集群A以及一个装载Kubemark服务的K8S集群B(也称Kubemark集群),当然也可以使用同一个集群装载Kubemark,只不过在实践过程中我们习惯将待测试集群和Kubemark集群区别开来。这时在B集群中部署Kubemark的HollowPod,这些Pod会主动向A集群注册并成为该K8S集群中的虚拟节点。也就是说在B集群中启动的HollowPod模拟了K8S的kubelet和kube-proxy并与A集群的APIServer进行交互,让A集群误以为这些HollowPod是真实的节点。

这些虚拟节点会完全模拟真实节点的行为,比如它会定时向APIServer上报节点和Pod的状态,也会在用户发起创建Pod的请求时进行响应,只不过它不会真的去启动容器而已。也就是说虚拟节点除了不会真的启动容器外,其他的大部分功能都与真实节点差不多。这样设计的目的是为了能够模拟真实的节点对APIServer等组件造成的压力。这样测试人员就可以避免耗费巨额的成本去搭建真实的集群就可以评估K8S的性能了。我们可以认为kubemark就像是一个复杂的mock服务一样,不必启动容器,但可模拟kubelet的行为。如图:

大家感兴趣的可以去搜一下kubemark的文档看看它如何使用。

海量小文件的构建

spark虽然可以控制数据的分片数量,但它无法构建非结构化数据(图片,视频,音频)也无法构建过于庞大的文件数量(比如数亿个文件)。所以我们需要另外一种方法来构建这种量级的数据。通常模拟这样的数据是为了做计算机视觉的模型训练的性能测试,或者测试存储系统本身的性能。构造这样的数据我们要面临的问题:

IO:不管是生成图片,还是文件都会消耗网络和磁盘io,数亿张图片对于IO的考验是比较大的CPU:如果按照传统的思路,为了提升造数性能,会开很多个线程来并发生成图片,计算元数据和数据交互。但线程开的太多CPU的上下文切换也很损耗性能。尤其我们使用的是ssd磁盘,多线程的模式可能是无法最大化利用磁盘IO的。

所以我们的主要问题在于如何充分的利用IO,要尽量的打到IO瓶颈。后面经过讨论,最后的方案是使用golang语言,用协程+异步IO来进行造数:

首先golang语言的IO库都是使用netpoll进行优化过的,netpoll底层用的就是epoll。这种异步IO技术能保证用更少的线程处理更多的文件。关于epoll为什么性能好可以去查一下同步IO和异步IO的区别。我大概总结一下就是,传统的多线程+同步IO模型是开多个线程抗压力,每个线程同一时间只处理一个IO。一旦IO触发开始读写文件线程就会处于阻塞状态,这个线程就会从CPU队列中移除也就是切换线程。等IO操作结束了再把线程放到CPU队列里让线程继续执行下面的操作,而异步IO是如果一个线程遇到了IO操作,它不会进入阻塞状态,而是继续处理其他的事,等到那个IO操作结束了再通知程序(通过系统中断),调用回调函数处理后面的事情。这样就保证了异步IO的机制下可以用更少的线程处理更多的IO操作。这是为什么异步IO性能更好的原因,也是为什么异步IO能最大化利用磁盘性能。比如在这个造图片的场景里,我在内存中造好图片后,开始写入文件系统,如果是同步IO那这时候就要阻塞了,直到文件写入完毕线程才会继续处理,但因为我用的异步IO,调用玩函数让内存中的数据写入到文件就不管了,直接开始造下一张图片,什么时候IO结束通知我我在回过头来处理。所以是可以用更少的线程来完成更多的IO操作也就是异步IO能很容易的把磁盘性能打满。我自己测试的时候再自己的笔记本上造k的图片,大概是1s就能造1W张图片

其次golang的GMP模型本身就很高效,编写异步程序也非常简单。我也就花了一上午就把脚本写完了。最后利用k8s集群把造数任务调度到集群中,充分利用分布式计算的优势,在多台机器上启动多个造数任务共同完成。

原谅我懒了,上面这个方案的架构图我实在是不想画了,大家见谅。

尾声

今天就先这样吧,我了解到的比较典型的,并且比较有技术含量的数据构建场景就这些了。

1
查看完整版本: 我们是如何测试人工智能的三数据构造与