惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google Online Security Blog
Google Online Security Blog
S
Security @ Cisco Blogs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
人人都是产品经理
人人都是产品经理
The Hacker News
The Hacker News
W
WeLiveSecurity
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
博客园 - 司徒正美
雷峰网
雷峰网
L
LINUX DO - 最新话题
博客园 - 叶小钗
云风的 BLOG
云风的 BLOG
The Last Watchdog
The Last Watchdog
V2EX - 技术
V2EX - 技术
S
Security Affairs
有赞技术团队
有赞技术团队
月光博客
月光博客
T
Threatpost
T
Tor Project blog
O
OpenAI News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
博客园 - 三生石上(FineUI控件)
D
Docker
AWS News Blog
AWS News Blog
AI
AI
P
Proofpoint News Feed
K
Kaspersky official blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Securelist
F
Fortinet All Blogs
F
Full Disclosure
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
量子位
Hacker News - Newest:
Hacker News - Newest: "LLM"
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Palo Alto Networks Blog
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
美团技术团队
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 Go 内存模型 Kubernetes 核心概念 软件版本语义化
缓存策略和模式
2023-02-25 · via 蛮荆

缓存策略和模式

2023-02-25 软件工程 微服务

概述

缓存的目的是通过将经常访问的数据或计算结果存储在高速存储介质 (一般是内存) 中,以便在下次访问时可以更快地获取到数据,主要目的是提升性能和响应速度。

缓存常见问题

缓存击穿针对某一个 key,缓存雪崩针对很多 key。

缓存击穿

缓存击穿是指某个热点数据在缓存过期或被删除后,恰好有大量请求到来,请求无法命中缓存就会尝试从后端数据库加载,导致数据库等底层存储系统负载骤增,甚至出现宕机

常见的解决方案:

  • 加锁: 缓存未命中时,保证同时只有一个线程去后端数据库加载,并在查询到结果后更新缓存以便使其他线程共享,例如 Go 语言开源的 singleflight 组件
  • 设置热点数据永不过期: 保证缓存数据永远可用,不足之处是需要牺牲一些内存占用 (尤其是自带 GC 语言要避免数据被 GC),不过系统的热点数据一般不会太多,所以一般情况下这个方案是首选。 例如笔者之前开发的一个活动模块,服务启动时通过 K8S API 将 Pod 自动扩容到指定数量,然后将热点数据加载到 Pod 本地缓存并且设置为永不过期,最后根据结束时间通过 K8S API 设置定时自动缩容
  • 提前加载数据到缓存: 服务启动时就将热点数据加载到缓存中,可以设置数据永不过期,也可以同步异步任务定时刷新热点数据到缓存 (例如和时间段相关的 key, 可以在不同时间段使用不同的 key 名称)
  • 异步线程定时更新 : 服务启动时注册异步任务,然后在缓存数据过期之前完成定时刷新,不足之处是需要编写额外的定时任务代码,而且很多缓存数据是通过各种参数组合生成的, 定时任务无法提前得知具体的参数组合,如果将所有参数组合都生成缓存,内存占用方面也是一个问题

缓存穿透

缓存穿透是指对某个不存在的数据进行请求 (一般是攻击者伪造的请求参数),请求会因未命中缓存而到达后端的数据库

常见的解决方案:

  • 缓存空数据: 针对不存在的数据,设置一个专用的 “空缓存”,例如设置为 NULL, 这样请求会直接返回 NULL, 而不会穿透到后端数据库
  • 布隆过滤器, 使用过滤器确认数据是否一定不存在,如果不存在直接返回空数据或者 404 即可

缓存雪崩

缓存雪崩是指大量缓存数据在同一时间全部失效或者过期 (包括缓存服务器宕机),请求会因未命中缓存而到达后端的数据库,如果缓存承接了系统中相当比例的流量,那么缓存雪崩发生时数据库基本会宕机

常见的解决方案:

  • 缓存过期时间打散: 合理设置缓存数据的过期时间,并将过期时间进行分散,避免大量缓存同时失效。可以在设置缓存时随机添加一个时间间隔,使缓存失效时间分布在一个较大的范围内
  • 设置热点数据永不过期: 同上
  • 分布式锁: 同一时刻,保证同时只有一个线程去后端数据库加载,这样即使发生缓存雪崩,也不会造成严重后果
  • 接口限流
  • 通过监控和自动化告警设置,超过一定比例缓存数量后 触发熔断

缓存无底洞

缓存无底洞是指在添加了大量缓存节点之后,服务性能不但没有提升反而下降

产生问题的原因是: 缓存系统通常采用 hash 函数将键值 key 映射到对应的节点,但是随着缓存节点的添加,键值 key 会被分布到更多的节点上, 客户端每次获取数据时都需要访问多个缓存节点,这会造成多次网络 IO, 网络连接数量也会增多,对缓存节点性能造成一定影响。

常见的解决方案:

  • 缓存键值 key 的优化,例如可以将同一用户的所有缓存数据放在同一节点 (类似 Redis 中的 key 前缀绑定)
  • 客户端与缓存节点之间使用连接池等网络连接优化技术
  • 如果缓存键值 key 对应的数据不可避免地分布在多个节点上面,使用批量获取操作 (类似 Redis 中的 Pipe Line)

分布式一致性哈希

传统哈希算法在服务节点数量发生变化时,因为节点编号经过计算后的哈值发生变化,可能会导致大量的数据需要进行迁移。 例如原本存储在节点 A 上面的数据,在新加入节点后,需要将数据迁移到 B C D 三个节点,这会造成大量的数据迁移工作, 如果缓存节点涉及到多个数据中心,这个工作量就很可观了。

分布式一致性哈希(Distributed Consistent Hashing)是一种用于解决分布式系统中数据分片和负载均衡的算法,通过将数据和节点映射到一个环形空间中, 使节点和数据在环上均匀分布,从而实现了高效的数据查找和负载均衡,主要目的是为了解决传统哈希中的问题。

基本原理

一致性哈希算法也使用取模运算,但与传统哈希算法不同的是:

  • 传统哈希算法是对节点的数量进行取模运算
  • 一致性哈希算法是固定对 2^32 进行取模运算

具体的规则如下:

  1. 将哈希算法对 2^32 的取模运算结果连接起来,形成一个哈希环
  2. 对所有服务节点 ID 进行哈希计算,然后和 2^32 进行取模运算并放入环中对应的节点上
  3. 每个键值 key 通过计算值后得到哈希值,然后找到该哈希值在环中的位置,按照顺时针方向查找,直到找到第一个服务节点 X
  4. 最后将数据放入 X 节点

例如图中的数据 Object B 经过计算得到哈希值,然后将数据存放在顺时针方向的下一个节点 NodeB 上。

哈希空间示例

一致性哈希在增加或者删除节点时,只会影响到哈希环中相邻的节点

例如下图中新增节点 X,只需要将它前一个节点 C 上的所有数据重新计算并进行划分即可,其他三个节点 A、B、D 没有任何影响。

新增节点示例

虚拟节点

前文描述到一致性哈希存存在数据分布不均匀的问题,每个节点存储到数据量可能差异很大,例如大部分数据只落在少数到几个节点上面。 造成这个问题的主要原因是就节点在哈希环上分布不均匀,尤其是在节点数量很少的情况下,例如下面的示例图中,大部分数据都存放到来节点 A 上面。

数据分布不均匀

解决方案是增加虚拟节点,然后将虚拟节点映射到真实节点上

因为虚拟节点的数量比真实节点要多,那么虚拟节点就会比真实节点在哈希环上有更好的分布均匀性,从而使数据分布也更加均匀。

如图所示,分别为每个真实服务节点设置三个虚拟节点,形成下列映射关系:

虚拟节点 真实节点
A#1 A
A#2 A
A#3 A
B#1 B
B#2 B
B#3 B

例如当键值 key 对应的哈希值定位到 A#1 A#2 A#3 中的任意一个虚拟节点时,实际数据都是存放在真实节点 A 上面,在实际项目中, 通常会将虚拟节点数量设置得非常大,以便数据分布的更加均匀,此外当真实节点数量发生变化时,会将数据迁移工作分摊到更多节点,提升系统的稳定性。

虚拟节点

缓存淘汰策略

限于篇幅,下面几个算法的具体实现本文就不展开了。

FIFO

先进先出 (First In First Out) 策略,缓存数据按照添加的顺序进行删除,不考虑数据已经被访问的次数和频率。

LRU

最近最久未使用 (Least Recently Used) 策略,优先淘汰最久未使用的数据,关注的是数据最后访问时间。

LFU

最不经常使用 (Least Frequently Used) 策略,优先淘汰最近时间段内使用次数最少的数据,关注的是数据最近使用频率,使用该策略可以保证没有被淘汰的都是热点数据,提高了缓存命中率。

缓存更新策略

Cache Aside Pattern

图片来源: https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside

模式的核心思想是将数据的读写操作进行分离,来提升数据的访问效率和一致性。

具体的规则如下:

  1. 读取数据时,先尝试从缓存中获取数据

    • 如果缓存命中,直接返回数据
    • 如果缓存未命中,从底层的存储层 (如数据库) 读取,并将读取到的数据更新到缓存中
  2. 写入数据时,先写入底层的存储层,然后将缓存设置为失效或者删除缓存,这样下次再读取时就可以读到最新数据了

为什么要先写入存储层,然后再删除缓存

因为如果先删除缓存,在存在并发请求的情况下,可能出现数据不一致性的问题。

例如下面的一个业务场景:

  1. 请求 1 读取数据,发现缓存未命中,然后去存储层读取并且读取到了数据 A,此时 请求 2 到了
  2. 请求 2 写入数据 B 到存储层,然后删除缓存 (下次应该存入缓存的值应该是 B)
  3. 此时请求 1 将读取到的旧数据 A 更新到缓存中,覆盖了请求 2 的写入操作 (本来应该是 B, 现在变成了 A)
  4. 后续请求读取到的全部是旧数据 A

当然了,即使先写入存储层,上面提到的这个业务场景的问题依然存在,读者可以思考一下什么条件下会触发?

Write Behind Caching Pattern

图片来源: https://en.wikipedia.org/wiki/Cache_(computing)

模式的核心思想是通过后台线程异步写入数据,避免高并发场景下写入延迟造成的性能问题,具体的流程如下:

  1. 写入数据请求到达时,现将数据写入到缓存
  2. 同时将写入的数据添加到异步队列
  3. 后台线程定时将异步队列中的数据写入到数据库

如果你对 MySQL 数据库的写入缓冲区底层实现有所了解的话,应该对这种模式有似曾相识的感觉 :-)

从功能使用的角度来看,因为只需要将数据写入缓存即可,而且异步队列的实现一般是内存或者顺序写磁盘,所以响应速度很快。同时,异步队列中的数据写入数据库时,一般是批量操作, 可以提升系统的吞吐量。最后,可以减少数据一致性问题,如果异步队列不出现数据堆积现象,甚至可以说完全消除了数据一致性问题 (非强一致性,断电数据会丢失,这里不考虑单点故障等问题)。

从功能实现的角度来看,该模式的实现较为复杂,除了数据相关操作外,还有实现一个异步队列或引用第三方的开源组件。

Cache-As-SoR Pattern

图片来源: https://en.wikipedia.org/wiki/Cache_(computing)

模式的核心思想把数据库 + 缓存作为一个存储整体,调用方无需关心数据是来自于缓存还是数据库,模式内部只需要暴露两个语义操作: Read/Write。

Read-Through

调用方使用 Read 方法读取数据,SoR 内部负责读取缓存,如果没有命中缓存就直接返回,否则从数据库读取,然后设置缓存数据并将数据返回给调用方。 这里需要注意的是: 因为存在并发请求同一个数据并且该数据正好缓存失效,所以从数据库读取数据时需要做好并发控制

Write-Through

调用方使用 Write 方法写入数据,SoR 内部负责具体的写入动作,可以使用 Cache Aside Pattern, 也可以使用 Write Behind Caching Pattern。

缓存强一致性

缓存强一致性要求数据库更新的同时,缓存也能够实时更新。要同时实现这两点,需要付出很大的代价,可以通过 2PC 协议或者 Paxos 协议来保证, 但是 2PC 协议太慢、 Paxos 协议太复杂,而且缓存数据本来就是实时性和一致性要求不高的数据,所以可以容忍也必须容忍不一致性带来的问题。

题外话

网上看到很多文章提到 通过消息队列或数据库订阅机制 来解决写入数据时的一致性问题,暂且不说额外的基础组件运维和代码维护带来的工作量, 这种解决方案的本质就是把业务层的功能下沉到了中间件层,而且大多数情况下只会增加系统的复杂度。试想一下,用户使用手机转账完成后,难道要等消息队列或数据库订阅更新到缓存吗? 如果这个过程中,中间件出了任何问题,怎么向用户解释?所以针对数据一致性要求高的场景,最直观的方案就是直接丢掉数据缓存层,保证强一致性就好了。

Reference