一.flask-limit限流原理

项目配置采用了固定时间窗口策略 incr和expire配合,每次请求来的时候 incr后的值与amount上限比较,如果超过则返回http 429

1
2
3
4
5
6
7
8
9
# lua脚本 incr和expire结合,避免incr成功,expire不成功
 SCRIPT_INCR_EXPIRE = """
        local current
        current = redis.call("incr",KEYS[1])
        if tonumber(current) == 1 then
            redis.call("expire",KEYS[1],ARGV[1])
        end
        return current
        """
<!-- more -->

二.排查redis服务

1.redis有其他阻塞操作,引起了expire失效?

1
2
3
# 显示平均超时时间0.03ms。
> redis-cli --latency -h 127.0.0.1 -p 6379
min: 0, max: 4, avg: 0.03 (12235 samples)

2.其他人操作了同样的key?

1.换个db, 取特殊的key名称
2.监控redis命令操作
1
2
# monitor db7
> redis-cli monitor | grep '\[7'

3.redis过期机制

1.定时删除

对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间

2.惰性删除

当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。

3.定期删除

每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。 默认是每秒运行 10 次, 平均每 100 毫秒运行一次。 从 Redis 2.8 开始, 用户可以通过修改 hz选项来调整 serverCron 的每秒执行次数, 具体信息请参考 redis.conf 文件中关于 hz 选项的说明。

4.当内存达到maxmemory配置时候,会触发Key的删除操作

当mem_used内存已经超过maxmemory的设定,对于所有的读写请求,都会触发redis.c/freeMemoryIfNeeded(void)函数以清理超出的内存。 注意这个清理过程是阻塞的,直到清理出足够的内存空间。 清理时会根据用户配置的maxmemory-policy来做适当的清理(一般是LRU或TTL)

三.故障分析

线下我们配置的是redis主从服务,线上阿里云的连接方式为直连,所以线下连接redis也为直连master 某日哨兵检测到master故障,进行了主从切换,之前的主节点降级为从节点,导致我们变为了直连从节点。 经过测试,redis在从节点设置expire的话,ttl一直减少到0后,从节点不自行执行删除key到操作,key的清除操作只能由主节点发起来,然后rsync到从节点进行同步。 故我们从节点的expire操作一直不生效,限流的时间窗口不过期,限流计数一直在累加!!!

To “maintain consistency”, slaves aren’t allowed to expire keys unless they receive a DEL from the master branch, even if they know the key is expired. The only exception is when a slave becomes master. So basically, if the master doesn’t send a DEL to the slave, the key (which might have been set with a TTL using the Redis API contract), is not guaranteed to respect the TTL it was set with. This is when you scale to have read slaves, which, apparently, is a shocking requirement in production systems. 为了维护一致性,salve不被容许删除key除非从master收到del命令

附录:redis相关命令

1.expire key

1
2
3
4
> set key_melon "cantaloupe"
> expire key_melon 450

# Expire设置成功后,返回1,设置失败返回0

2.某个时间点expire

1
2
# 某个unix timestamp时间过期
expireat key_melon 1746131400

3.查询key存活时间

1
2
3
4
5
# 秒
> ttl key_melon

# 毫秒
> pttl key_melon

4.清除expire

1
2
3
4
# Set或者getset会使过期时间清除
# persist持久化key,过期时间清除

persist key_melon

5.判断某个key是否存在

1
exists key_melon

参考 https://cloud.tencent.com/developer/article/1044436

https://www.hoohack.me/2019/06/24/redis-expire-strategy

https://www.w3cschool.cn/redis_all_about/redis_all_about-xzio26xj.html

https://www.digitalocean.com/community/cheatsheets/how-to-expire-keys-in-redis

https://engineering.grab.com/a-key-expired-in-redis-you-wont-believe-what-happened-next