DSP系统是互联网广告需求方平台,用于承接媒体流量,投放广告。业务特色是并发度高,平均响应低(百毫秒)。算法
为了可以有效提升DSP系统的性能,美团平台引入了一种带有清退机制的缓存结构LruCache(Least Recently Used Cache),在目前的DSP系统中,使用LruCache + 键值存储数据库的机制将远端数据变为本地缓存数据,不只可以下降平均获取信息的耗时,并且经过必定的清退机制,也能够维持服务内存占用在安全区间。数据库
本文将会结合实际应用场景,阐述引入LruCache的缘由,并会在高QPS下的挑战与解决方案等方面作详细深刻的介绍,但愿能对DSP感兴趣的同窗有所启发。后端
LruCache采用的缓存算法为LRU(Least Recently Used),即最近最少使用算法。这一算法的核心思想是当缓存数据达到预设上限后,会优先淘汰近期最少使用的缓存对象。缓存
LruCache内部维护一个双向链表和一个映射表。链表按照使用顺序存储缓存数据,越早使用的数据越靠近链表尾部,越晚使用的数据越靠近链表头部;映射表经过Key-Value结构,提供高效的查找操做,经过键值能够判断某一数据是否缓存,若是缓存直接获取缓存数据所属的链表节点,进一步获取缓存数据。LruCache结构图以下所示,上半部分是双向链表,下半部分是映射表(不必定有序)。双向链表中value_1所处位置为链表头部,value_N所处位置为链表尾部。安全
LruCache读操做,经过键值在映射表中查找缓存数据是否存在。若是数据存在,则将缓存数据所处节点从链表中当前位置取出,移动到链表头部;若是不存在,则返回查找失败,等待新数据写入。下图为经过LruCache查找key_2后LruCache结构的变化。性能优化
LruCache没有达到预设上限状况下的写操做,直接将缓存数据加入到链表头部,同时将缓存数据键值与缓存数据所处的双链表节点做为键值对插入到映射表中。下图是LruCache预设上限大于N时,将数据M写入后的数据结构。数据结构
LruCache达到预设上限状况下的写操做,首先将链表尾部的缓存数据在映射表中的键值对删除,并删除链表尾部数据,再将新的数据正常写入到缓存中。下图是LruCache预设上限为N时,将数据M写入后的数据结构。架构
线程安全的LruCache在读写操做中,所有使用锁作临界区保护,确保缓存使用是线程安全的。并发
在美团DSP系统中普遍应用键值存储数据库,例如使用Redis存储广告信息,服务能够经过广告ID获取广告信息。每次请求都从远端的键值存储数据库中获取广告信息,请求耗时很是长。随着业务发展,QPS呈现巨大的增加趋势,在这种高并发的应用场景下,将广告信息从远端键值存储数据库中迁移到本地以减小查询耗时是常看法决方案。另外服务自己的内存占用要稳定在一个安全的区间内。面对持续增加的广告信息,引入LruCache + 键值存储数据库的机制来达到提升系统性能,维持内存占用安全、稳定的目标。机器学习
在实际应用中,LruCache + Redis机制实践分别经历了引入LruCache、LruCache增长时效清退机制、HashLruCache知足高QPS应用场景以及零拷贝机制四个阶段。各阶段的测试机器是16核16G机器。
在较低QPS环境下,直接请求Redis获取广告信息,能够知足场景需求。可是随着单机QPS的增长,直接请求Redis获取广告信息,耗时也会增长,没法知足业务场景的需求。
引入LruCache,将远端存放于Redis的信息本地化存储。LruCache能够预设缓存上限,这个上限能够根据服务所在机器内存与服务自己内存占用来肯定,确保增长LruCache后,服务自己内存占用在安全范围内;同时能够根据查询操做统计缓存数据在实际使用中的命中率。
下图是增长LruCache结构先后,且增长LruCache后命中率高于95%的状况下,针对持续增加的QPS得出的数据获取平均耗时(ms)对比图:
根据平均耗时图显示能够得出结论:
下图是增长LruCache结构先后,且增长LruCache后命中率高于95%的状况下,针对持续增加的QPS得出的数据获取Top999耗时(ms)对比图:
根据Top999耗时图能够得出如下结论:
引入LruCache结构,在实际使用中,在必定的QPS范围内,确实能够有效减小数据获取的耗时。可是QPS超出必定范围后,平均耗时和Top999耗时都很高。因此LruCache在更高的QPS下性能还须要进一步优化。
在业务场景中,Redis中的广告数据有可能作修改。服务自己做为数据的使用方,没法感知到数据源的变化。当缓存的命中率较高或者部分数据在较长时间内屡次命中,可能出现数据失效的状况。即数据源发生了变化,但服务没法及时更新数据。针对这一业务场景,增长了时效清退机制。
时效清退机制的组成部分有三点:设置缓存数据过时时间,缓存数据单元增长时间戳以及查询中的时效性判断。缓存数据单元将数据进入LruCache的时间戳与数据一块儿缓存下来。缓存过时时间表示缓存(的)单元缓存的时间上限。查询中的时效性判断表示查询时的时间戳与缓存时间戳的差值超过缓存过时时间,则强制将此数据清空,从新请求Redis获取数据作缓存。
在查询中作时效性判断能够最低程度的减小时效判断对服务的中断。当LruCache预设上限较低时,按期作全量数据清理对于服务自己影响较小。但若是LruCache的预设上限很是高,则一次全量数据清理耗时可能达到秒级甚至分钟级,将严重阻断服务自己的运行。因此将时效性判断加入到查询中,只对单一的缓存(的)单元作时效性判断,在服务性能和数据有效性之间作了折中,知足业务需求。
LruCache引入美团DSP系统后,在一段时间内较好地支持了业务的发展。随着业务的迭代,单机QPS持续上升。在更高QPS下,LruCache的查询耗时有了明显的提升,逐渐没法适应低平响的业务场景。在这种状况下,引入了HashLruCache机制以解决这个问题。
线程安全的LruCache中有锁的存在。每次读写操做以前都有加锁操做,完成读写操做以后还有解锁操做。在低QPS下,锁竞争的耗时基本能够忽略;可是在高QPS下,大量的时间消耗在了等待锁的操做上,致使耗时增加。
针对大量的同步等待操做致使耗时增长的状况,解决方案就是尽可能减少临界区。引入Hash机制,对全量数据作分片处理,在原有LruCache的基础上造成HashLruCache,以下降查询耗时。
HashLruCache引入某种哈希算法,将缓存数据分散到N个LruCache上。最简单的哈希算法即便用取模算法,将广告信息按照其ID取模,分散到N个LruCache上。查询时也按照相同的哈希算法,先获取数据可能存在的分片,而后再去对应的分片上查询数据。这样能够增长LruCache的读写操做的并行度,减少同步等待的耗时。
下图是使用16分片的HashLruCache结构先后,且命中率高于95%的状况下,针对持续增加的QPS得出的数据获取平均耗时(ms)对比图:
根据平均耗时图能够得出如下结论:
下图是使用16分片的HashLruCache结构先后,且命中率高于95%的状况下,针对持续增加的QPS得出的数据获取Top999耗时(ms)对比图:
根据Top999耗时图能够得出如下结论:
引入HashLruCache结构后,在实际使用中,平均耗时和Top999耗时都有很是明显的降低,效果很是显著。
根据以上分析,进一步提升HashLruCache性能的一个方法是肯定最合理的分片数量,增长足够的并行度,减小同步等待消耗。因此分片数量能够与CPU数量一致。因为超线程技术的使用,能够将分片数量进一步提升,增长并行性。
下图是使用HashLruCache机制后,命中率高于95%,不一样分片数量在不一样QPS下得出的数据获取平均耗时(ms)对比图:
平均耗时图显示,在较高的QPS下,平均耗时并无随着分片数量的增长而有明显的减小,基本维持稳定的状态。
下图是使用HashLruCache机制后,命中率高于95%,不一样分片数量在不一样QPS下得出的数据获取Top999耗时(ms)对比图:
Top999耗时图显示,QPS为750时,分片数量从8增加到16再增加到24时,Top999耗时有必定的降低,并不显著;QPS为1000时,分片数量从8增加到16有明显降低,可是从16增加到24时,基本维持了稳定状态。明显与实际使用的机器CPU数量有较强的相关性。
HashLruCache机制在实际使用中,能够根据机器性能并结合实际场景的QPS来调节分片数量,以达到最好的性能。
线程安全的LruCache内部维护一套数据。对外提供数据时,将对应的数据完整拷贝一份提供给调用方使用。若是存放结构简单的数据,拷贝操做的代价很是小,这一机制不会成为性能瓶颈。可是美团DSP系统的应用场景中,LruCache中存放的数据结构很是复杂,单次的拷贝操做代价很大,致使这一机制变成了性能瓶颈。
理想的状况是LruCache对外仅仅提供数据地址,即数据指针。使用方在业务须要使用的地方经过数据指针获取数据。这样能够将复杂的数据拷贝操做变为简单的地址拷贝,大量减小拷贝操做的性能消耗,即数据的零拷贝机制。直接的零拷贝机制存在安全隐患,即因为LruCache中的时效清退机制,可能会出现某一数据已通过期被删除,可是使用方仍然经过持有失效的数据指针来获取该数据。
进一步分析能够肯定,以上问题的核心是存放于LruCache的数据生命周期对于使用方不透明。解决这一问题的方案是为LruCache中存放的数据添加原子变量的引用计数。使用原子变量不只确保了引用计数的线程安全,使得各个线程读取的引用计数一致,同时保证了并发状态最小的同步性能开销。不管是LruCache中仍是使用方,每次获取数据指针时,即将引用计数加1;同理,再也不持有数据指针时,引用计数减1。当引用计数为0时,说明数据没有被任何使用方使用,且数据已通过期从LruCache中被删除。这时删除数据的操做是安全的。
下图是使零拷贝机制后,命中率高于95%,不一样QPS下得出的数据获取平均耗时(ms)对比图:
平均耗时图显示,使用零拷贝机制后,平均耗时降低幅度超过60%,效果很是显著。
下图是使零拷贝机制后,命中率高于95%,不一样QPS下得出的数据获取Top999耗时(ms)对比图:
根据Top999耗时图能够得出如下结论:
引入零拷贝机制后,经过拷贝指针替换拷贝数据,大量下降了获取复杂业务数据的耗时,同时将临界区减少到最小。线程安全的原子变量自增与自减操做,目前在多个基础库中都有实现,例如C++11就提供了内置的整型原子变量,实现线程安全的自增与自减操做。
在HashLruCache中引入零拷贝机制,能够进一步有效下降平均耗时和Top999耗时,且在高QPS下对于稳定Top999耗时有很是好的效果。
下图是一系列优化措施先后,命中率高于95%,不一样QPS下得出的数据获取平均耗时(ms)对比图:
平均耗时图显示,优化后的平均耗时仅为优化前的20%之内,性能提高很是明显。优化后平均耗时对于QPS的增加敏感度更低,更好的支持了高QPS的业务场景。
下图是一系列优化措施先后,命中率高于95%,不一样QPS下得出的数据获取Top999耗时(ms)对比图:
Top999耗时图显示,优化后的Top999耗时仅为优化前的20%之内,对于长尾请求的耗时有很是明显的下降。
LruCache是一个很是常见的数据结构。在美团DSP的高QPS业务场景下,发挥了重要的做用。为了符合业务须要,在本来的清退机制外,补充了时效性强制清退机制。随着业务的发展,针对更高QPS的业务场景,使用HashLruCache机制,下降缓存的查询耗时。针对不一样的具体场景,在不一样的QPS下,不断尝试更合理的分片数量,不断提升HashLruCache的查询性能。经过引用计数的方案,在HashLruCache中引入零拷贝机制,进一步大幅下降平均耗时和Top999耗时,更好的服务于业务场景的发展。
王粲,2018年11月加入美团,任职美团高级工程师,负责美团DSP系统后端基础架构的研发工做。
崔涛,2015年6月加入美团,任职资深广告技术专家,期间一手指导并从0到1搭建美团DSP投放平台,具有丰富的大规模计算引擎的开发和性能优化经验。
霜霜,2015年6月加入美团,任职美团高级工程师,美团DSP系统后端基础架构与机器学习架构负责人,全面负责DSP业务广告召回和排序服务的架构设计与优化。
美团在线营销DSP团队诚招工程、算法、数据等各方向精英,发送简历至cuitao@meituan.com,共同支持百亿级流量的高可靠系统研发与优化。