直播广告翻车记

关键词:redis slave spire 获取过期数据

周六晚会直播,有人反馈观看过广告后,再也不能触发广告了。第一次值班守护直播,就像守护女朋友一样,小心翼翼胆战心惊如履薄冰,怎奈还是翻船了。
话不多说,这锅我背了,快去找到原因解决问题吧。经过一番努力并没有头绪,经过项目组踩过坑的同事查证,redis cluster readonly=1, 导致了读取slave 过期expire数据的bug;
广告播放后,同一个用户接下来10分钟内不会再出广告。广告播放的标记存储在redis中,expire设置为600,按理10分钟后标记清除,广告系统获取不到播放标记会给用户再次下发广告。当晚有一些用户看过第一广告后,长时间无法第二次播放广告。经过查询相应用户后台日志,发现问题确实是10分钟不重复观看策略导致的。

也就是说redis存在expire过期数据仍可被读取的情况。

经过一番查证,redis曾发起Issue Improve expire consistency on slaves,以下摘录说明了这个情况(坑呀,宝宝心里苦%>_<%)

1
2
3
4
5
6
7
8
9
10
In order for Redis to ensure consistency between a master and its slaves, eviction of keys 
with an expire are managed by the master, which sends an explicit DEL to its slaves when
the key gets actually removed.
This means that slaves are not able to directly expire keys, even if these keys are
logically expired on the master side. So a GET that will return null in the master side,
may return a stale value in the slave side.
为了保证redis主、从一致性,expire数据的删除由master来进行,当expire数据删除的时候,
master会向slave发送删除命令这意味着,即使这些expire数据从逻辑上应该被master端删除,
slaves也不会直接删除expire数据。在master获取这些过期数据将会获取null,
而在slave端可能仍能获取到旧的数据

凭什么认定我们是读取的从库呢?
翻出武功秘籍,对项目用到的golang redis.v5源码进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (c *ClusterClient) cmdSlotAndNode(state *clusterState, cmd Cmder) (int, *clusterNode, error) {
if state == nil {
node, err := c.nodes.Random()
return 0, node, err
}

cmdInfo := c.cmds[cmd.name()]
firstKey := cmd.arg(cmdFirstKeyPos(cmd, cmdInfo))
slot := hashtag.Slot(firstKey)

if cmdInfo != nil && cmdInfo.ReadOnly && c.opt.ReadOnly {
if c.opt.RouteByLatency {
node, err := state.slotClosestNode(slot)
return slot, node, err
}

node, err := state.slotSlaveNode(slot)
return slot, node, err
}

node, err := state.slotMasterNode(slot)
return slot, node, err
}

redis cluster的readonly字段配置为1的情况下,c.opt.ReadOnly条件成立,会使用slaveNode,反过来则使用masterNode,而使用slaveNode则可能引发上面的问题。

为什么我们测试的时候没有发现这个问题

话说我们也有测试过,几个人没有出现这个问题,脸黑吗(⊙o⊙)
找啊找,低版本Redis expire过期的策略在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
How Redis expires keys
Redis keys are expired in two ways: a passive way, and an active way.
A key is passively expired simply when some client tries to access it,
and the key is found to be timed out.
Of course this is not enough as there are expired keys that will never
be accessed again. These keys should be expired anyway, so periodically
Redis tests a few keys at random among keys with an expire set. All the
keys that are already expired are deleted from the keyspace.

Specifically this is what Redis does 10 times per second:
1. Test 20 random keys from the set of keys with an associated expire.
2. Delete all the keys found expired.
3. If more than 25% of keys were expired, start again from step 1.
This is a trivial probabilistic algorithm, basically the assumption is
that our sample is representative of the whole key space,
and we continue to expire until the percentage of keys that are likely
to be expired is under 25%

Redis如何过期密钥
Redis密钥以两种方式过期:被动方式和主动方式。
当某个客户端尝试访问密钥时,密钥被动过期,并且发现密钥超时。
当然这还不够,因为有过期的密钥永远不会被再次访问。这些密钥无论如何都应该过期,
所以周期性地Redis会在具有过期集的密钥中随机测试几个密钥。已经过期的
所有密钥都将从密钥空间中删除。
具体来说,这就是Redis每秒做10次的事情:
1. 从具有相关过期的密钥集中测试20个随机密钥。
2. 删除找到的所有密钥已过期。
3. 如果超过25%的密钥已过期,请从步骤1重新开始。
这是一个简单的概率算法,基本上假设我们的样本代表整个密钥空间,
我们继续到期,直到可能过期的密钥百分比低于25

从以上信息可以归纳出:

  1. 测试的时候,QPS小,Redis主动过期策略1s内可以清楚10*20=200个已过期的key,完全能处理测试好测试时候的expire key;
  2. 到了正式上线,QPS增大,整体上会保留25%已过期的expire key,这也可以解释为什么有些人可以重复看到广告,有些人不可以;
    这种带有随机性质的问题,通常定位起来都会困难一些,脸确实有点黑O__O

解决方案:

  1. 查看我司服务器redis版本是redis_version:3.0.7-m,这个问题在Redis 3.2 中得到解决,升级大法保平安(万能解决之道,搞不定了,试试升级吧)。
  2. 结合go redis.v5库特性,将readonly字段配置为0,使用masterNode节点。当然,你可以直接连master,就不会有这个问题。但要注意这种方案将会增大master的压力,酌情考虑。
  3. 除此之外也有同学提出了另外的解决方案