【openresty】如何处理 Dog Pile Effect

本文探讨了缓存系统中DogPile效应的产生原因及两种避免方法:使用独立更新进程和加锁机制。介绍了lua-resty-lock非阻塞锁的实现,及其在缓存更新中的应用,确保并发请求下的数据一致性。

在缓存系统中,缓存总有失效的时候,比如我们经常使用的 Memcache 和 Redis ,都会设置超时时间;而一旦缓存到了超时时间失效之后,如果此时再有大量的并发向数据库发起请求,就会造成服务器卡顿甚至是系统当机。这就是 Dog Pile Effect 。

Dog Pile Effect

避免这样的 Dog Pile 效应,通常有两种方法:

使用独立的更新进程

使用独立的进程(比如 cron job)去更新缓存,而不是让 web 服务器即时更新数据缓存。举个例子:一个数据统计需要每五分钟更新一次(但是每次计算过程耗时1分钟),那么可以使用 cron job 去计算这个数据,并更新缓存。这样的话,数据永远都会存在,即使不存在也不用担心产生 Dog Pile 效应,因为客户端没有更新缓存的操作。这种方法适合不需要即时运算的全局数据。但对用户对象、朋友列表、评论之类的就不太适用。

使用“锁”

除了使用独立的更新进程之外,我们也可以通过加“锁”,每次只允许一个客户端请求去更新缓存,以避免 Dog Pile 效应。

处理过程大概是这样的:

A 请求的缓存没命中
A 请求“锁住”缓存 key
B 请求的缓存没命中
B 请求需要等待直到“锁”释放
A 请求完成,并且释放“锁”
B 请求缓存命中(由于 A 的运算)

lua-resty-lock - 基于共享内存的非阻塞锁实现。

首先,我们先来消除下大家对锁的抗拒,事实上这把共享内存锁非常轻量。第一,它是非阻塞的,也就是说锁的等待并不会导致 NGINX Worker 进程阻塞;第二,由于锁的实现是基于共享内存的,且创建时总会设置一个过期时间,因此这里不用担心会发生死锁,哪怕是持有这把锁的 NGINX Worker Crash 了。

那么,接下来我们只要利用这把锁按如下步骤来更新缓存即可:

  • 检查某个 Key 的缓存是否命中,如果 MISS,则进入步骤 2。
  • 初始化 resty.lock 对象,调用 lock 方法将对应的 Key 锁住,检查第一个返回值(即等待锁的时间),如果返回 nil,按相应错误处理;反之则进入步骤 3。
  • 再次检查这个 Key 的缓存是否命中,如果依然 MISS,则进入步骤 4;反之,则通过调用 unlock 方法释放掉这把锁。
  • 通过数据源(这里特是 Redis)查询数据,把查询到的结果缓存起来,最后通过调用 unlock 方法释放当前 Hold 住的这把锁。

具体代码实现请参考:lua-resty-lock#for-cache-locks

当数据源故障的时候怎么办?NO_DATA?

同样,我们以上面的代码片段为例,当 Redis 返回出现 err 的时候,此时的状态即不是 MISS 也不是 NO_DATA,而这里统一把它归类到 NO_DATA 了,这就可能会引发一个严重的问题,假设线上这么一台 Redis 挂了,此时,所有更新缓存的操作都会被标记为 NO_DATA 状态,原本旧的拷贝可能还能用的,只是可能不是最新的罢了,而现在却都变成空数据缓存起来了。

那么如果我们能在这种情况下让缓存不过期是不是就能解决问题了?答案是 yes。

lua-resty-shcache - 基于 ngx.shared.DICT 实现了一个完整的缓存状态机,并提供了适配接口
恩,这个库几乎解决了我们上面提到的所有问题:

  1. 内置缓存锁实现
  2. 故障时使用陈旧的拷贝 - STALE

所以,不想折腾的话,直接用它就是的。

 

参考:

[1] 如何处理 Dog Pile Effect

[2] 浅谈 ngx_lua 在 UPYUN 的应用

### 三级标题:OpenResty处理大请求的策略与优化方法 OpenResty基于Nginx的事件驱动架构和Lua脚本的扩展能力,能够高效处理包括大请求在内的多种复杂场景。在面对大请求(如大文件上传、下载或处理大量数据的API请求)时,OpenResty通过异步非阻塞IO、缓冲控制、Lua协程调度等机制来优化性能。 在处理大文件传输时,OpenResty支持`sendfile`机制,该机制允许数据在内核空间直接从磁盘读取并发送到网络,减少用户态与内核态之间的数据拷贝,从而降低CPU和内存的消耗。此外,OpenResty支持`aio`(异步IO)和`directio`(直接IO),可以绕过文件系统缓存,避免因缓存频繁读写造成的性能下降。例如: ```nginx location /largefile { sendfile on; aio on; directio 512k; output_buffers 1 128k; } ``` 上述配置通过启用异步IO和调整输出缓冲区大小,提升大文件传输的效率[^3]。 对于大请求的处理OpenResty还通过Lua协程来管理异步操作。Lua脚本可以在遇到IO阻塞时挂起当前协程,释放主线程去处理其他任务,待IO完成后继续执行,从而避免阻塞整个Nginx进程。这种机制非常适合在Lua中执行数据库访问、远程API调用等耗时操作[^1]。 为了进一步优化大请求的处理OpenResty支持调整客户端请求体缓冲策略。例如,通过设置`client_body_buffer_size`和`client_max_body_size`可以控制客户端上传数据的缓冲大小和最大允许上传体积,避免内存被大量占用: ```nginx http { client_body_buffer_size 16k; client_max_body_size 100m; } ``` 同时,OpenResty还支持使用Lua脚本动态控制上传和下载过程,例如分块读取、流式处理、断点续传等,这些策略可以有效降低内存压力并提升处理效率: ```lua local file = io.open("/path/to/largefile", "r") while true do local chunk = file:read(8192) if not chunk then break end ngx.say(chunk) ngx.flush(true) end file:close() ``` 上述Lua脚本示例展示了如何以流式方式读取大文件并逐步输出到客户端,避免一次性加载整个文件到内存中。 ### 三级标题:总结与建议 在处理大请求时,OpenResty结合Nginx的异步非阻塞IO模型和Lua的协程调度能力,提供了多种优化手段。通过合理配置`sendfile`、`aio`、`directio`、缓冲区大小以及使用Lua进行流式处理,可以显著提升大请求的处理效率和稳定性。 为了进一步提升性能,建议: - 启用异步IO和直接IO以减少磁盘访问延迟; - 调整客户端缓冲区大小以适应大请求上传; - 使用Lua协程实现非阻塞的远程调用或本地IO操作; - 利用流式处理机制分块读取和输出大文件内容。 通过上述策略,OpenResty可以在高并发、大数据量场景下保持良好的响应能力和资源利用率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值