当前位置: 首页 > 科技观察

人工智能在线特征系统中的数据接入技术_1

时间:2023-03-15 20:14:58 科技观察

1.在线特征系统的主流互联网产品中,无论是经典的计算广告、搜索、推荐,还是垂直领域的路线规划、司机调度、物料智能设计,基于人工智能技术构建的策略体系已经渗透到产品功能的方方面面。相应地,每一个策略系统都离不开大量在线特征来支持模型算法或人工规则对请求的精确响应,因此特征系统成为支撑在线策略系统的重要支柱。美团点评技术博客此前发表了多篇关于特征系统的文章,如《机器学习中的数据清洗与特征处理综述》重点介绍特征生产过程中的离线数据清洗和挖掘方法,《业务赋能利器之外卖特征档案》重点解决不同存储的不同特征数据engines查询要求。而《外卖排序系统特征生产框架》专注于在线查询的特征计算、数据同步和特征生产流水线。本文以美团酒旅在线特征系统为原型,着重从在线数据访问的角度介绍一些实践中的通用技术点,以解决在线特征系统在高并发情况下面临的问题。1.1在线特征系统框架——生产、调度、服务一体化在线特征系统是通过系统上下文获取相关特征数据的在线服务。其功能可以是简单的Key-Value(KV)类型存储,提供在线特征查询服务,也可以辐射到通用特征生产、特征统一调度、特征实时监控等全套特征服务系统.可以说,一个简单易用的特性系统几天就可以完成,但在复杂的业务场景中,要让这个更加方便、快捷、稳定,需要一个团队长期的积累。上面的结构图是集成特征系统的概览,数据流向为自下而上。各部分的作用如下:数据源:用于计算特征的原始数据。根据业务需求,数据源可能是分布式文件系统(如Hive)、关系数据库(如MySQL)、消息队列(如Kafka)等。特征生产:这部分负责从各种数据源中读取数据并提供用于生成特征的计算框架。生产框架需要根据数据源的类型和不同的计算需求进行综合设计,因此会有多套生产框架。特征导入:这部分负责将计算出的特征写入在线存储,供特征服务读取。这部分主要关注导入作业之间的依赖关系、并发写入的速度和一致性等问题。要素服务:这部分是整个要素系统的核心功能部分,提供在线要素接入服务,直接服务于上层策略系统。根据以上流程,一个特征的生命周期可以抽象为读取、计算、写入、保存、获取五个步骤。整个过程在特征系统的框架内成为一个整体,作为特征工程的一体化解决方案。本文主要围绕要素服务的核心功能“存储”和“检索”,介绍一些通用的实践经验。特征系统的扩展部分,如特征制作、系统框架等话题,将在后续文章中详细介绍。1.2特征系统的核心——存储和检索简单来说,特征系统的核心功能可以认为是一个大的HashMap,用于存储和快速提取每个请求中相关维度的特征集。然而,实际情况并不像HashMap那么简单。以我们通用的在线特征系统(Datahub)的系统指标为例,其核心功能主要需要面对存储和读取的挑战:高并发:策略系统面向客户端,服务端峰值QPS超过10000,数据库峰值QPS超过100万(批量请求导致)。高吞吐量:每个请求可能包含数千维特征,网络IO高。服务器网络平均出口流量为500Mbps,峰值为1.5Gbps。大数据:虽然线上需要使用的特征数据不会像线下的Hive库那么大,但数据项数会超过10亿,字节数会达到TB级别。低延迟:面对用户请求,为了维护用户体验,接口的延迟应该尽可能的低,服务端的TP99指标在10ms以下。以上指标仅以我司系统为参考。各部门、各公司的特征系统实际规模可能相差很大,但无论一个特征系统的规模如何,其核心目标一定是要考虑:高并发、高吞吐、大数据、低延迟,但各有千秋不同的优先级。当系统的优化方向是多目标时,我们不可能在资源有限的情况下独立使用任何一种方法来实现一切。留给我们的是业务最重要的需求特性,以及这些特性对应的解决方案。2.在线特征接入技术本节介绍一些在线特征系统常用的接入技术点,以丰富我们的武器库。主要内容不是详细的系统设计,而是对一些常见问题的通用技术解决方案。但是,正如上一节所说,如何使用合适的技术,并根据战略需求制定相应的解决方案,才是架构师的核心价值所在。2.1数据分层特性当总数据量达到TB级别时,单一存储介质难以支撑完整的业务需求。高性能的在线服务内存或缓存,在数据量上是九牛一毛。分布式KV存储可以提供更大的存储空间,但在某些场景下速度不够快。我们使用的开源分布式KV存储或缓存方案有很多,比如Redis/Memcache、HBase、Tair等。这些开源解决方案拥有大量的贡献者,他们正在为它们的功能和性能不断努力。本文不再多墨。对于构建在线特征系统,我们实际上需要了解的是我们的特征数据是什么样的。有些数据很热,我们可以通过内存副本或者缓存,用很小的内存开销覆盖大量的请求。有些数据并不热,但是一旦访问需要稳定快速的响应速度,那么基于全内存的分布式存储方案是一个不错的选择。对于数据量非常大的数据,或者增长非常快的数据,我们需要选择磁盘支持的存储方案,我们必须选择基于各种读写分布的存储技术。当业务发展到一定程度时,单一的特性类型很难覆盖所有的业务需求。因此,在存储方案的选择上,需要根据特征类型对数据进行分层。分层后,不同的存储引擎统一向策略服务提供特征数据,是兼顾系统性能和功能的最佳实践。2.2数据压缩海量的离线特征被加载到在线系统中并在系统之间传输,这对于内存和网络带宽等资源来说是不小的开销。数据压缩是典型的以时间换空间的例子,往往可以将占用的空间翻倍,这对宝贵的在线内存和带宽资源来说是一大福音。数据压缩的本质是减少信息冗余。对于特征系统的应用场景,我们积累了一些实践经验,分享给大家。2.2.1存储格式特征数据就是特征名称和特征值。以用户画像为例,用户具有年龄、性别、兴趣爱好等特征。一般来说,这种特征数据的存储方式有以下几种:JSON格式,完整保留特征名-特征值对,以JSON字符串的形式表示。对于元数据提取,像Hive一样,将特征名(元数据)单独保存,特征数据用String格式的特征值列表表示。元数据固化了,元数据也是分开存储的,但是每个特征定义了一个强类型,比如Integer,Double等,而不是统一的String类型。三种格式各有优缺点:JSON格式的优点是特征数量的长度可以是可变的。以用户画像为例,用户A可能有年龄和性别标签。用户B可以有家乡和爱好的标签。不同类型的用户标签差异很大,都可以方便地存储。但缺点是每组特征都需要存储特征名称,当特征类型同构度高时,会包含大量冗余信息。元数据抽取的特点与JSON格式相反。它只保留特征值本身,特征名称作为元数据单独存储,减少了冗余特征名称的存储,但缺点是数据格式必须是同构的,如果需要添加或删除特征,则元数据更改后需要刷新整个数据集。元数据固化的优点和元数据抽取一样,更节省空间。但是其访问过程需要实现专有的序列化,无论是实现难度还是读写速度都比较昂贵。在特征系统中,一批特征数据通常是完全同构的。同时,为了应对高并发下的批量请求,我们在实践中使用元数据提取作为存储方案,比JSON格式快2到10倍。节省空间(具体比例取决于特征名称的长度、特征个数、特征值的类型)。2.2.2字节压缩说到数据压缩,很容易想到使用无损字节压缩算法。无损压缩的主要思想是用更短的字节码来表达频繁出现的模式(Pattern)。考虑到在线特征系统的读写方式是全写一次,一条一条读取多次,压缩需要针对单条数据,而不是全局压缩。目前Java实现的主流短文本压缩算法有Gzip、Snappy、Deflate、LZ4等,我们进行了两组实验,从单次平均压缩速度、单次平均解压速度、和压缩率。数据集:我们选择了两个在线真实特征数据集,每个数据集有100,000条特征记录。记录为纯文本格式,平均长度为300~400个字符(600~800字节)。压缩算法:Deflate算法有1~9个压缩级别,级别越高,压缩比越大,运行时间越长。LZ4算法有两个压缩级别,我们用0,1来表示。另外,LZ4有不同的实现版本:JNI、JavaUnsafe、JavaSafe。详细的区别可以参考https://github.com/lz4/lz4-java,这里不做过多解释。实验结果图中的毫秒时间为单条记录的压缩或解压时间。压缩比的计算方法是压缩前的字节码长度/压缩后的字节码长度。可以看出,随着压缩比的增加,所有压缩算法的压缩/解压时间都趋于增加。其中,LZ4的JavaUnsafe和JavaSafe版本出于对平台兼容问题的考虑,存在明显的速度异常。从使用场景(一次性全写,多次一次读取)出发,特征系统的主要服务指标是特征高并发下的响应时间和特征数据存储效率。因此,特征压缩重点关注的指标是:解压速度快,压缩比高,但对压缩速度要求不是很高。因此,综合上述实验中各个算法的表现,Snappy更适合我们的需求。2.2.3字典压缩压缩的本质是利用共性,在不影响信息量的情况下重新编码,减少空间占用。上一节的字节压缩是单行压缩,所以只能适用于同一条记录中的共性,不能考虑全局的共性。例如:假设某个用户维度特征的所有用户的特征值完全相同,字节压缩并不能节省任何存储空间,但我们知道重复出现的重复值其实只有一个。即使在单个记录内,由于压缩算法窗口大小的限制,长模式也很难被考虑在内。因此,对全局特征值进行字典统计,自动或手动将频繁模式添加到字典中并重新编码,可以解决短文本字节压缩的局限性。2.3数据同步当每次请求和策略计算都需要大量的特征数据时(比如一次请求上千个广告主特征),我们需要非常强大的在线数据采集能力。在存储特征的不同方法中,访问本地内存无疑是性能最高的解决方案。要访问本地内存中的特征数据,我们通常有两种有效的手段:内存拷贝和客户端缓存。2.3.1内存复制技术当数据总量不大时,策略用户可以在本地完全镜像一份特征数据。这个镜像叫做内存拷贝。使用内存拷贝与使用本地数据完全一样,用户不需要关心远程数据源的存在。内存副本需要通过一定的协议与数据源同步更新。这种同步技术称为内存复制技术。在在线特征系统的场景下,数据源可以抽象为一个KV类型的数据集,内存副本技术需要将这样的数据集完全同步到内存副本中。推拉结合——时效性和一致性一般来说,数据同步有两种:推(Push)和拉(Pull)。Push技术比较简单,依赖于目前常见的消息队列中间件,可以根据需要将一个数据的变化传递到内存副本中。但是,即使实现了高可靠的不重复、不漏消息的消息队列通知(通常成本很高),仍然面临着初始化和启动时批量同步数据的问题——所以,Push只能作为一种方式提高内存副本的时效性从本质上讲,内存副本的同步还是依赖于Pull协议。Pull类的同步协议一个很好的特性就是幂等性。失败或成功的同步不会影响下一次新的同步。Pull协议有多种选择,最简单的一种是每次都拉走所有数据的基本协议。但是业务需求需要追求数据同步的效率,所以使用一些效率更高的Pull协议就显得非常重要了。为了减少拉取的数据量,这些协议本质上是希望尽可能准确地高效计算数据差异(Diff),然后同步这些必要的数据变化。下面介绍我们在工程实践中使用过的两种拉式数据同步协议。基于版本号的同步——回放日志(RedoLog)和降级算法更新数据源时,对于每一次数据变化,基于版本号的同步算法都会为这次变化分配一个唯一的增量版本号,并使用一次更新queue记录所有版本号对应的数据变化。当内存副本发起同步请求时,会携带上次同步完成时副本的最新版本号,也就是说该版本号之后的所有数据变化都需要拉过来。数据源收到请求后,从更新队列中找出所有大于版本号的数据变化,汇总数据变化,得到最终需要更新的Diff,返回给发起者。此时内存副本只需要更新Diff数据即可。对于大部分业务场景,特征数据的生成都会被收集到一个统一的更新服务中,因此可以串行生成增量版本号。如果在分布式数据更新环境下,需要使用分布式id生成器获取增量版本号。另一个问题是更新队列的长度。如果不做任何优化,更新队列理论上是最长的,甚至超过了数据集的大小。一种优化方法是我们限制更新队列的最大长度,一旦长度超过限制,就进行合并(Merge)操作。Merge操作将队列中的数据成对合并。合并后的版本号以较大的版本号为准,合并后的更新数据集是两个数据集的并集。Merge后,新队列的长度减少为原来更新队列的一半。在Merge后的更新队列中,我们仍然可以使用相同的算法进行同步Diff计算:找到队列中所有大于上次更新版本号的数据集。可以看出,由于版本号的合并,计算出的Diff不再是完全准确的更新数据,队列中最早的更新数据集可能包含一些已经同步的数据——但这种退化并不影响同步的正确性,只会造成少量的同步冗余,冗余的多少取决于Diff中最早的数据集被合并的次数。MerkleTreeSynchronization—DatasetComparisonAlgorithm基于版本号的同步使用了类似RedoLog的概念,记录业务变化的历史,通过回放未同步的历史记录获得Diff。由于记录不断增长的RedoLog需要很大的开销,所以采用Merge策略对原有日志(Log)进行降级。对于批量或微批量更新,基于版本号的同步算法可以很好地工作;相反,如果数据是实时更新的,会有大量的RedoLog,会迅速退化,影响同步效率。MerkleTree同步算法走的是另一条路。简单的说,就是每次直接比较两个数据集的差异,得到Diff。先看最简单的算法:每个内存副本将所有数据的hash值发送给数据源,数据源对整个数据集进行比对,对不同hash值的数据进行一次同步操作——从而准确计算出两个数据集之间的差异。但明显的问题是,每次传输所有数据的哈希值可能并不比多传输几条数据更容易。MerkleTree同步算法使用MerkleTree数据结构来优化这个比较过程。简单的说,默克尔树就是把所有数据集的哈希值组织成一棵树,这棵树的叶子节点描述了一个(或一组)数据的哈希值。中间节点的值是从它所有儿子的Hash值中再次Hash得到的,它描述了以它为根的子树所包含的数据的整体Hash。显然,在不考虑Hash冲突的情况下,如果两棵MerkleTrees的根节点相同,则说明它们是两个相同的数据集。MerkleTree同步协议由副本发起,将副本根节点的值发送给数据源。如果与数据源根节点的哈希值一致,则没有数据变化,同步完成。否则,数据源会将根节点的所有子节点的哈希发送给副本进行递归比较。对于不同的hash值,通过不断获取直到叶子节点,就可以完全确定发生变化的数据。以二叉树为例,至多经过LogN次交互,所有数据同步完成。2.3.2客户端缓存技术当数据规模过大无法完全存储在内存中,数据冷热分明,对数据的时效性要求不高时,通常采用客户端缓存的方式来实现各种企业。客户端缓存的集中实现,是要素服务扩展的一部分。通用的缓存协议和使用方法就不多说了。从在线特征系统的业务角度,这里有几个方向的一些思考和体会。接口泛化——缓存逻辑与业务分离。为了满足各种业务需求,一个有特色的系统必须有丰富的接口。从数据含义上看,有用户类、商家类、产品类等;从数据传输协议上看,有Thrift、HTTP;从调用方式来看,有同步和异步之分;从数据组织形式来看,有单值、List、Map和相互嵌套等……一个好的架构设计应该尽可能将数据处理和业务分离,抽象出各个接口的公共部分,实现缓存,并同时受益于多个接口。下面分别以同步接口和异步接口为例,介绍客户端接口的泛化。同步接口只有一个步骤:向服务器发起请求获取结果。异步接口分为两步:向服务端发起请求,获取Future实例。向Future实例发起请求获取数据。同步接口和异步接口的数据处理只是顺序不同,只需要梳理一下每一步的执行顺序即可。引入缓存后,数据处理流程对比如下:不同颜色的处理框代表不同的请求。异步过程需要来自消费者的两个请求来获取数据。如图所示,在异步过程中的第二次请求中完成了“updatecachewithserver-sidedata”(更新缓存)和“summaryofserver-sidedataandcacheddata”(mergedata)这两个步骤,不同于同步流程**所有步骤都是在第一次请求时完成。将数据流拆分为这些子步骤,同步与异步只是这些步骤的不同顺序的组合。因此,可以将读写缓存(搜索缓存、更新缓存)这两个步骤抽象出来,与其他逻辑解耦。数据存储——时间先于空间,客户端和服务器分离。客户端之于服务器,就像服务器之于数据库一样。其实数据存储压缩的思路是一模一样的。具体的数据压缩和存储策略在上面的数据压缩部分已经详细介绍过了。这里我们主要想说明两点:客户端压缩和服务端压缩,由于应用场景不同,目的也不一样。服务器端压缩使用场景是一次性高吞吐写入,一对一高并发低延迟读取。主要关注读取时的解压时间和数据存储时的压缩率。客户端缓存属于数据存储层的最上层。由于读写场景都是高并发、低延迟的本地内存操作,对压缩速度、解压速度、数据量都有很高的要求。做更多的权衡。其次,客户端和服务端是两个完全独立的模块。说白了,虽然我们可以写客户端代码,但它不是服务的一部分,而是调用者服务的一部分。客户端的数据压缩尽量和服务端解耦,不要为了方便把两者的数据格式耦合在一起。与服务器端的数据通信格式应该理解为一个独立的协议,就像服务器端与数据库的连接一样。与通信一样,数据通信格式与数据库的存储格式无关。内存管理——缓存与分代回收的矛盾缓存的目标是将热数据(被频繁访问的数据)保留在内存中,从而提高缓存效率。JVM垃圾回收(GC)的目标是释放丢失引用的对象的内存空间。两者的目标看似相似,但细微的差别让两者很难在高并发场景下共存。缓存的淘汰会产生大量的内存垃圾,使得FullGC非常频繁。这种矛盾其实并不局限于客户端,而是所有JVM堆缓存共同面临的问题。下面详细分析一个场景:由于请求产生的数据会不断的添加到缓存中,当QPS高的时候,会频繁的发生YoungGC,从而导致缓存占用的内存不断的从新一代到老一代。缓存被填满后,使用最近最少使用(LRU)算法淘汰,冷数据被踢出缓存,成为垃圾内存。不幸的是,由于频繁的YoungGC,大量的冷数据进入了老年代,当老年代的缓存被淘汰后,老年代中就会产生垃圾,从而触发FullGC。可以看出,正是因为缓存淘汰机制与新生代的GC策略目标不一致,所以缓存淘汰会在老年代产生大量的内存垃圾,与垃圾产生的速度关系不大与缓存的大小有关,但与新生代的GC频率和堆缓存的淘汰速度有关。这两个指标都与QPS呈正相关。因此,堆内缓存似乎是一条通向老年代的垃圾管道。QPS越高,垃圾产生的速度就越快!因此,对于高并发的缓存应用,应该避免使用JVM的分区管理内存。也就是说,GC内存回收机制的开销和效率无法满足高并发情况下内存管理的需要。由于JVM虚拟机强制内存管理的限制,此时我们可以将对象序列化,存储在堆外(OffHeap),达到绕过JVM内存管理的目的,例如Ehcache等第三方技术和大内存。或者改变JVM的底层实现(类似于淘宝之前的做法)存储在堆中,避免GC。3.结语本文主要介绍了在线特征系统的一些技术要点,从系统的高并发、高吞吐、大数据、低延迟的需求出发,以一些实际的特征系统为原型,对在线特征的一些设计思路系统被提出。如上所述,特征系统的边界并不局限于数据的存储和读取。如数据导入作业调度、实时特征、特征计算与生产、数据备份、灾难恢复等,都可以看作是特征系统的一部分。本文是在线特征系统系列文章的第一篇。面对需求和挑战,我们的功能系统也在不断发展。以后我们会和大家分享更多的实践经验。一家之言难免有疏漏和偏颇,但他山之石可以攻玉。如果能为架构师在面对自己的业务时提供一些思路,那就太好了。