内容服务锁优化实践

简介: 内容服务锁优化实践

背景

随着内部重点服务接入的内容越来越多,维护各种内容的动态信息(例如:播放量,删除状态等)的成本越来越高。独立出一个内容服务成为很自然的选择。

内容提供方按照调用快慢依次:

  • NoSQL数据库:耗时5ms以下
  • BG内部服务:耗时10~20ms左右
  • 外BG服务:耗时50ms+

由于内容经常会出现下线,而下线的内容一般在推荐结果中的位于头部,较大可能逃脱推荐服务的若干次截断,如果只在最终返回给用户的时候再进行过滤,就会出现部分类目的内容空白或者缺失。考虑到后端接口的性能,内容服务使用了二次过滤的方式来保证,因此服务提供了两个接口:

前置接口:粗粒度的状态过滤

后置接口:内容获取与状态再过滤

所谓粗粒度的过滤,就是可以容忍少量下线内容不被过滤掉,但是输入的数据量很大。再过滤则要求过滤掉所有下线内容,不过数据量只有前者的十分之一左右。根据以上分析就可以得到大致的架构,前置接口只查询缓存;后置接口依次按照缓存、KV、内部调用。而外部调用则通过异步更新的方式来保证。(由于内容在打开的时候,同样会进行内容有效性的检查,所以不涉及到缓存数据一致性)。

缓存访问分析

上图可见,我们可以得到以下结论

  1. 单机缓存查询:kw/m
    所有的请求都会查询本地缓存,缓存查询次数每分钟会有数亿次。以十台机器算,每台机器每分钟有kw级的查询,10w+/m写入。
  2. 缓存淘汰算法、加锁方式影响巨大
    缓存的空间是有限的,缓存更新需要合适的淘汰机制,而共享数据淘汰则需要添加锁。

第一版:LRU缓存

服务重构、完善监控、联调、灰度上线,Leader给的时间只有一周,所以缓存直接使用部门LRU缓存组件。缓存实现是unordered_map + 链表 + 互斥锁

第二版:分桶LRU缓存+状态/内容分离

第一版快速上线之后,发现单机并发始终上不去,性能瓶颈在前置接口,更准确说是锁冲突。LRU缓存组件被广泛使用在我们的后台系统中,之所以之前没有遇到类似的问题,是因为之前缓存读写的QPS远小于当前的应用场景,锁冲突的概率也远小于当前场景。那么如何减少锁冲突呢?参考深入了解锁细节以及我们的业务,可能有的选择是:

  • 减少锁请求频率:批量读写代替单个读写

批量读写的问题也很明显,意味着临界区内的时间大大加长。持有锁的时间越长,锁冲突的几率越大,效果难说。

  • 分离/分拆锁:将缓存分段,每段使用一个锁

效果明显,值得实施。

  • 替代独占锁:使用自旋锁/读写锁代替独占锁
  • 考虑使用LRU缓存的场景,对于内容缓存更新使用LRU没有问题。但对于状态数据等需要强制过期淘汰的数据来说,更合适的缓存更新的策略其实是FIFO。所以可以考虑内容和状态分开缓存。FIFO由于读数据不需要更新状态,可以使用读写锁代替独占锁
  • 由于线程切换导致的代价详细位置,所以LRU使用自旋锁代替互斥锁,带来的收益以及付出的CPU代价,难以简单评估。

因此,第二版我们使用:“分桶LRU缓存” + **”状态/内容分离”**来进行优化

第三版:内存拷贝和对象析构优化

做完以上优化,使用perf工具跑下服务的性能。通过火焰图发现,由于单次请求查询数百条数据,会涉及到多次的对象创建和析构:

创建对象->【 拷贝放入缓存】-> 原对象析构

【缓存拷贝取出数据(对象过期析构)】 -> 返回对象析构

备注:【】临界区

而且对象的拷贝发生在临界区内,直接影响了持有锁的时间,而这些拷贝都可以通过share_ptr来避免,而过期对象的析构也可以通过异步处理来优化,这样大大减少了持有锁的时间,明显降低了锁冲突的概率。

总结

通过以上优化服务的并发成倍提升,耗时降低为第一版的一半,相同QPS CPU消耗降低三分之一,锁优化为缓存强依赖的服务带来明显收益。

本文作者 : cyningsun

本文地址https://www.cyningsun.com/05-04-2018/lock-practice.html

版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

目录
相关文章
|
2月前
|
缓存 监控 算法
代码性能优化:解锁应用潜能的关键策略
【10月更文挑战第20天】代码性能优化:解锁应用潜能的关键策略
|
6月前
|
Java
探秘死锁:原理、发生条件及解决方案
探秘死锁:原理、发生条件及解决方案
139 1
|
7月前
|
安全 Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第30天】 在多线程环境下,确保数据的一致性和程序的正确性是至关重要的。Java提供了多种锁机制来管理并发,但不当使用可能导致性能瓶颈或死锁。本文将深入探讨Java中锁的优化策略,包括锁粗化、锁消除、锁降级以及读写锁的使用,以提升并发程序的性能和响应能力。通过实例分析,我们将了解如何在不同场景下选择和应用这些策略,从而在保证线程安全的同时,最小化锁带来的开销。
|
7月前
|
Java
【专栏】Java多线程中,锁用于控制共享资源访问,确保数据一致性和正确性,锁是什么意思,有哪些分类?
【4月更文挑战第28天】Java多线程中,锁用于控制共享资源访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类:乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁。使用锁需注意避免死锁、合理选择锁粒度及性能优化。理解锁有助于提升多线程编程的效率和稳定性。
110 0
|
安全 Java
锁升级原理
锁升级是指在多线程环境下,当一个线程持有了低级别的锁(如偏向锁或轻量级锁)时,如果有其他线程也要获取这个锁,那么就需要将锁升级为重量级锁。这样可以保证在并发情况下,多个线程之间的互斥访问。
248 1
|
Java 编译器 调度
锁的优化过程
锁的优化过程
|
并行计算 安全 算法
Oh!老伙计,提高自己的并发技能,先从锁优化开始吧
锁是最常用的同步方法之一。在高并发的环境下,激烈的锁竞争会导致程序的性能下降。 对于单任务或者单线程的应用而言,其主要资源消耗都花在任务本身,它既不需要维护并行数据结构间的一致性状态,也不需要为线程的切换和调度花费时间。对于多线程应用来说,系统除了处理功能需求外,还需要额外维护多线程环境的特有信息,如线程本身的元数据、线程的调度、线程上下文的切换等。并行计算之所以能提高系统的性能,并不是因为它"少干活"了,而是因为并行计算可以更合理地进行任务调度,充分利用各个CPU资源。
synchronized锁升级原理剖析 ✨ 每日积累
synchronized锁升级原理剖析 ✨ 每日积累
synchronized锁升级原理剖析 ✨ 每日积累
|
存储 安全 Java
小白也能看懂的锁升级过程和锁状态
小白也能看懂的锁升级过程和锁状态
263 0
小白也能看懂的锁升级过程和锁状态
|
SQL 运维 监控
锁优化|学习笔记
快速学习锁优化
100 0