内容简介:线上服务为了限制用户频繁访问敏感资源,通常会引入一种机制来限制这种访问操作。其中一种常见的方案就是为每个用户的访问做一次时间戳,同一个用户再次访问对应资源时,检查当前时间和已经记录的时间戳的差值 -- 如果此差值小于我们定义的超时时间,此次访问被判定为频繁访问。我们在某系统的实现中便采用了此种机制,限定用户在 1s内不能连续访问2次,配合Memcache,实现起来非常简单。 核心代码如下:
线上服务为了限制用户频繁访问敏感资源,通常会引入一种机制来限制这种访问操作。其中一种常见的方案就是为每个用户的访问做一次时间戳,同一个用户再次访问对应资源时,检查当前时间和已经记录的时间戳的差值 -- 如果此差值小于我们定义的超时时间,此次访问被判定为频繁访问。
我们在某系统的实现中便采用了此种机制,限定用户在 1s内不能连续访问2次,配合Memcache,实现起来非常简单。 核心代码如下:
public boolean isOutOfTime(String key){ return memCachedClient.add(key,"abc",new Date(System.currentTimeMillis() + 1000)); }
问题
一切看起来很顺利,直到有一天线上报错资源在100ms内被访问两次。也就是说,同一个用户的超时键被设置为1s以后,100ms再次去检查居然键过期了。 什么鬼?逻辑上无懈可击的代码怎么可能会有漏洞?先不管那些,复现再说。
代码简单粗暴,就是启5个线程,每个线程连续尝试过滤某个键十万次。
运行上述代码,每次都有很多键被判定为过期。充分分析整个流程,定位可能的问题原因:
- 后台业务服务器与Memcache服务器时钟不同步。Memcache的过期时间是一个时间戳,而不是相对时间偏移量,所以如果Memcache客户端和服务器有时间差的话,比如客户端的时间比服务器时间慢1s,那么客户端设置的过期时间(它当前的时间 + 1000ms)在服务器看来却已经过期了。
- Memcache的键清理机制导致。在极端情况下(比如说Memcache被分配的内存不够用了),Memcache会清理一些键值对,即使这些键还没有过期。
但是以上两个原因中,时钟不同步的原因很快被排除了。因为从日志分析来看,相当一部分频繁请求是被拦截下来的,如果时钟不同步,应该有相当比例的频繁请求被放过才对。并且跟运维确认,线上的服务器都开启了时钟同步功能,两个服务器的时钟差不会超过10ms。
现在看来只有内存清理机制这一个原因了。研究了下Memcache的键清理机制,总结如下:
- 当有新数据需要存储的时候,Memcache会先看数据大小对应的Slab是否有空闲Item,如果有,将数据存入Item,同时更新LRU表。
- 如果没有空闲Item,Memcache会尝试去看对应Slab是否有过期键。如果有,清空过期键,将数据存入新的Item,同时更新LRU表。
- 如果没有过期键,Memcache会尝试申请一个新的Slab,如果申请成功,将数据存入新Slab对应的Item,同时更新LRU表。
- 如果申请失败,并且Memcache配置了强制淘汰机制,会将LRU链表尾部的Item强制清空,并存入新Item,同时更新LRU表。
总体看下来,强制淘汰的触发条件还是很苛刻的,并且具体的实现中,LRU链表分为Hot,Warm,Cold三个区域,新加入的数据会在Hot区,等Hot区满了,较早的数据才会被降级到其他区。也就是说,假设存入数据为大小为100B,对应Slab在Memcache服务器上只有一个(一般会有很多),那么此Slab中可用Item数量约为10000个。在这种情况下,如果要触发刚刚存入100ms的未过期键被强制清理的话,需要在100ms内有超过10000条100B左右大小的数据写入Memcache。在测试环境几乎不可能。但是这是一个公共的Memcache,谁知道呢?所以需要排除一下这个情况。
诊断
本地起一个虚拟机,装个Memcache,顺便打开日志打印(本来的目的是为了看到键淘汰日志)。如果是强制淘汰机制引起,那在只有一个client的本地Memcache上,应该就不会出现这个问题(测试代码可以控制键数量和写入速度),但是不幸的是,在这个空的Memcache上也出现了同样的现象 -- 这直接排除了此现象是由强制淘汰机制导致的的可能性。
在本地虚拟机启动的Memcache打印的日志中,发现了一个现象:所有时间戳都是类似于这样的格式:1527001620,有点奇怪,比毫秒时间戳短。去查了一下源码,果然被猜中:
而rel_time_t的定义为:
typedef unsigned int rel_time_t;
毫无疑问,Memcache的时间是用秒计算而不是毫秒。我们使用的客户端接口方法:
public boolean add(String key, Object value, Date expiry);
非常具有误导性,因为Date是精确到毫秒的,这也使我们一直理所当然地以为Memcache提供毫秒精度的过期时间校验,然而这是不对的。
原因
至此,问题的原因就很明朗了,Memcache的过期判断代码如下:
最重要的一句是:
it->exptime <= current_time
即:过期检测中,当前时间与过期时间相等即被判定为过期。 在这个前提下,当如下情况发生时就会偶现线上的现象。
- 第一个请求,当前时间××××01900 ,计算出的过期时间是××××02900(+1000ms) → 存入的过期时间是××××02
- 第二次请求,当前时间××××02000,计算出的过期时间是××××03000(+1000ms) → 请求时,服务器判断键过期(键过期时间 ××××02,当前时间××××02) 此次请求add成功。
第一次请求和第二次请求仅隔100ms。
事实上,如果过期时间设置为1000ms,Memcache能帮我们随机过滤0 ~ 1000ms内的请求。频繁请求是否被过滤依赖于最后一次成功请求的时间。
总结
使用Memcache的add方法做过期判断时需要注意以下三点:
- Memcache客户端与服务器时间要同步;
- 内存被强制淘汰的可能性极低,除非过期时间比较长,Memcache内存吃紧时,需要关注此问题;
- 过期时间精度为秒。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Redis实战之限制操作频率
- nginx限制客户端请求数+iptables限制TCP连接和频率来防止DDOS
- Laravel最佳实践 -- API请求频率限制(Throttle中间件)
- 使用 Dingo API 扩展包快速构建 Laravel RESTful API(十) —— 路由访问频率限制
- Go 基于 Redis 通用频率控制的实现
- MapReduce实战 - 根据文章记录获取时段内发帖频率
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Caching
Duane Wessels / O'Reilly Media, Inc. / 2001-6 / 39.95美元
On the World Wide Web, speed and efficiency are vital. Users have little patience for slow web pages, while network administrators want to make the most of their available bandwidth. A properly design......一起来看看 《Web Caching》 这本书的介绍吧!