实验结果图中的毫秒时间为单条记录的压缩或解压缩时间。压缩比的计算方式为压缩前字节码长度/压缩后字节码长度。可以看出,所有压缩算法的压缩/解压时间都会随着压缩比的上升而整体呈上升趋势。其中LZ4的Java Unsafe、Java Safe版由于考虑平台兼容性问题,出现了明显的速度异常。
从使用场景(一次全量写入,多次逐条读取)出发,特征系统主要的服务指标是特征高并发下的响应时间与特征数据存储效率。因此特征压缩关注的指标其实是:快速的解压速度与较高的压缩比,而对压缩速度其实要求不高。因此综合上述实验中各个算法的表现,Snappy是较为合适我们的需求。
2.2.3 字典压缩
压缩的本质是利用共性,在不影响信息量的情况下进行重新编码,以缩减空间占用。上节中的字节压缩是单行压缩,因此只能运用到同一条记录中的共性,而无法顾及全局共性。举个例子:假设某个用户维度特征所有用户的特征值是完全一样的,字节压缩逐条压缩不能节省任何的存储空间,而我们却知道实际上只有一个重复的值在反复出现。即便是单条记录内部,由于压缩算法窗口大小的限制,长Pattern也很难被顾及到。因此,对全局的特征值做一次字典统计,自动或人工的将频繁Pattern加入到字典并重新编码,能够解决短文本字节压缩的局限性。
2.3 数据同步
当每次请求,策略计算需要大量的特征数据时(比如一次请求上千条的广告商特征),我们需要非常强悍的在线数据获取能力。而在存储特征的不同方法中,访问本地内存毫无疑问是性能最佳的解决方式。想要在本地内存中访问到特征数据,通常我们有两种有效手段:内存副本和客户端缓存。
2.3.1 内存副本技术
当数据总量不大时,策略使用方可以在本地完全镜像一份特征数据,这份镜像叫内存副本。使用内存副本和使用本地的数据完全一致,使用者无需关心远端数据源的存在。内存副本需要和数据源通过某些协议进行同步更新,这类同步技术称为内存副本技术。在线特征系统的场景中,数据源可以抽象为一个KV类型的数据集,内存副本技术需要把这样一个数据集完整的同步到内存副本中。
推拉结合——时效性和一致性
一般来说,数据同步为两种类型:推(Push)和拉(Pull)。Push的技术比较简单,依赖目前常见的消息队列中间件,可以根据需求做到将一个数据变化传送到一个内存副本中。但是,即使实现了不重不漏的高可靠性消息队列通知(通常代价很大),也还面临着初始化启动时批量数据同步的问题——所以,Push只能作为一种提高内存副本时效性的手段,本质上内存副本同步还得依赖Pull协议。Pull类的同步协议有一个非常好的特性就是幂等,一次失败或成功的同步不会影响下一次进行新的同步。
Pull协议有非常多的选择,最简单的每次将所有数据全量拉走就是一种基础协议。但是在业务需求中需要追求数据同步效率,所以用一些比较高效的Pull协议就很重要。为了缩减拉取数据量,这些协议本质上来说都是希望高效的计算出尽量精确的数据差异(Diff),然后同步这些必要的数据变动。这里介绍两种我们曾经在工程实践中应用过的Pull型数据同步协议。
基于版本号同步——回放日志(RedoLog)和退化算法
在数据源更新时,对于每一次数据变化,基于版本号的同步算法会为这次变化分配一个唯一的递增版本号,并使用一个更新队列记录所有版本号对应的数据变化。
内存副本发起同步请求时,会携带该副本上一次完成同步时的最大版本号,这意味着所有该版本号之后的数据变化都需要被拉取过来。数据源方收到请求后,从更新队列中找到大于该版本号的所有数据变化,并将数据变化汇总,得到最终需要更新的Diff,返回给发起方。此时内存副本只需要更新这些Diff数据即可。
对于大多数的业务场景,特征数据的生成会收口到一个统一的更新服务中,所以递增版本号可以串行的生成。如果在分布式的数据更新环境中,则需要利用分布式id生成器来获取递增版本号。