二、在线特征存取技术
本节介绍一些在线特征系统上常用的存取技术点,以丰富我们的武器库。主要内容也并非详细的系统设计,而是一些常见问题的通用技术解决方案。但如上节所说,如何根据策略需求,利用合适的技术,制定对应的方案,才是各位架构师的核心价值所在。
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等,我们做了两组实验,主要从单条平均压缩速度、单条平均解压速度、压缩率三个指标来对比以上各个算法。
数据集:我们选取了2份线上真实的特征数据集,分别取10万条特征记录。记录为纯文本格式,平均长度为300~400字符(600~800字节)。
压缩算法:Deflate算法有1~9个压缩级别,级别越高,压缩比越大,操作所需要的时间也越长。而LZ4算法有两个压缩级别,我们用0,1表示。除此之外,LZ4有不同的实现版本:JNI、Java Unsafe、Java Safe,详细区别参考 https://github.com/lz4/lz4-java ,这里不做过多解释。