直播廣告翻車記

關鍵詞: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,以下摘錄說明了這個情況(坑呀,寶寶心裏苦%>_<%)

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源碼進行分析

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過期的策略在這裏

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. 除此之外也有同學提出了另外的解決方案
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章