我不知道我不瞭解的Redis知識

"已知的東西; 有些事情我們知道我們知道。 我們也知道有未知的事物。 就是說我們知道有些事情我們不知道。 但是,還有未知的未知數-我們不知道的我們不知道的……後一類往往是困難的。"

唐納德·拉姆斯菲爾德

我不知道我不瞭解的Redis知識

> Redis Wikipedia Page Logo

我與Redis在一起已經有好幾年了,從來沒有真正遇到過任何問題。 我已經使用Redis計劃和部署了大規模的Web服務,並使用了數十億條日常讀寫命令(使用可上下伸縮的主從複製),並且可以"正常工作"。 從開發人員的角度來看,這導致了我一個錯誤的假設,即我瞭解關於Redis所需的全部知識(而且,正如一位偉大的詩人所說,假設是所有問題的源頭)。

當然,我不是DevOps工程師,也不知道Redis的所有細節(但我仍然不知道),但是我覺得使用Redis被"發現"了。 其他服務(例如MySQL)迫使我對它們的概念有紮實的瞭解,以便編寫快速高效的代碼(再次,無需像DBA一樣掌握MySQL,但至少您需要了解索引或JOIN才能快速編寫代碼 和高效的查詢)。 但是使用Redis,我可以將其"即插即用"到我的代碼中。

幾周前,我被分配來解決在我們的一項舊服務中從Redis提取數據時的性能問題。 該服務的業務邏輯並不那麼重要,但是對該服務的工作方式以及Redis的使用方式的總體概述將有助於我們理解問題所在以及最終如何解決。

我們擁有一個大型系統,可以存儲由不同實體產生的數十億每日事件。 查詢實體事件可能很慢,並且在許多情況下是多餘的,因為我們不需要所有數據。 這就是我們提供服務的地方-它提供了一個API,可以快速查詢最近30天的實體唯一事件。

例如,假設有一個系統監視著帶有傳感器的空調,該傳感器在每次AC狀態發生變化(打開/關閉會更改AC狀態以及改變溫度)時發送事件。 一個AC狀態每天可能會更改很多次,並且我們的系統將為每次更改存儲一個事件,但是我們的服務僅會為過去30天內所做的每個更改返回一個事件(無論AC被翻轉了多少次) ,我們的服務將返回單個"打開"事件)。

Redis快速,支持數據過期(我們只需要最近30天的事件,還記得嗎?),並且具有Set數據結構,是我們服務的理想存儲:將同一項目多次添加到Set中將導致只有一個副本 此商品在套裝中。 可以將存儲在我們系統中的每個實體(AC)的數百萬個不同事件轉換為幾十個唯一項(事件)的小集合。 我們的服務僅需要"偵聽"傳入的事件,並使用AC ID和日期作為鍵將它們存儲到Redis中,並將到期時間設置為30天。 當查詢AC事件時,我們的服務將計算所有此AC密鑰並返回存儲在這些密鑰中的所有事件的並集。

Redis中存儲的數據的一覽視圖如下所示:

: #{on, off, 24°, 28° ...}, expires 2019-10-30
: #{on, off, 25°, 27° ...}, expires 2019-10-29
: #{on, 26°, humidity 32° ...}, expires 2019-10-28
...
...
...


: #{off, humidity 35° ...}, expires 2019-10-01
: #{on, off, 23°, 27° ...}, expires 2019-10-30
: #{on, off, 25°, 28° ...}, expires 2019-10-29
...
...
...
: #{24°, 26°, humidity 42° ...}, expires 2019-10-01
: #{on, off, 23°, 27° ...}, expires 2019-10-30
...
...
...

密鑰是具有事件收集日期的實體的ID。 ac1-2019-09-30代表id = ac1的實體,其值是該日期收集的一組事件。 該密鑰將於2019–10–30到期。

在Redis內部,我們的服務使用SUNION命令來獲取特定實體在不同日期存儲的所有事件的聯合集:

SUNION

...
...

這個簡單的邏輯可以處理99.6%的查詢。 但是,有時從Redis獲取數據花費的時間很長(在某些情況下可能需要30秒),這在產品上並不被接受。

我已經開始解決因搜索通用規則而導致的響應緩慢的問題,這並不奇怪,這通常發生在SUNION處理大型Set時(也並非總是如此)。 Redis中存儲的大多數Set(基本代表元素數)的基數小於100。 但是對於少量的Set,大小可能會很大。 如您在下表中所見,有85%的組合有0–50個項目,少於0.5%的組合有1,000個或更多項目)

我不知道我不瞭解的Redis知識

我在閱讀服務代碼時注意到的另一件事是,為了避免產生較大的響應負載,無論對服務進行編碼的是誰,都對API返回的事件數量進行了限制(我們將在後面看到為什麼這與 解決方案)。

瞭解我對Redis的瞭解後,使用SUNION命令似乎是一個合理的解決方案。 如果我是首先編寫此服務的人,那麼我可能會做同樣的事情。 但是,您不知道的內容有時會造成很多痛苦……谷歌搜索很快發現了兩個我對Redis不瞭解或不完全瞭解的事實:

· SUNION是一個緩慢的命令,採用兩個大集合的交集可能會花費大量時間。 實際上,我在Redis官方文檔中閱讀它感到很驚訝,因為我一直認為Redis會一直保持快速。

· 從命令執行的角度來看,Redis主要是單線程服務器,這意味著當請求緩慢時,所有其他客戶端將等待該請求得到處理。

瞭解問題是找到正確解決方案的一半。 我意識到SUNION並不是我需要的命令,但是有哪些替代方案? 好吧,感謝Google和Redis.io文檔,我遇到了SSCAN命令。 SSCAN命令(實際上,Redis中有針對不同數據類型的一堆不同的SCAN命令)允許在Set上進行增量迭代,每次調用僅返回少量元素。 可以使用它而不會受到諸如SMEMBERS或SUNION之類的命令的負面影響,這些命令在針對大型集合進行調用時可能會長時間阻塞服務器。

請注意,SSCAN在一個集合上完全迭代的時間複雜度為O(N),其中N是集合基數。 對於SMEMBERS來說,這也是命令的複雜性,但是如果您還記得,我還說過我們的服務對從其API返回的商品數量有一定的限制。 由於一旦達到此限制就可以中斷SSCAN迭代,因此我們的時間複雜度實際上將是O(Min(N,Limit)),這是SUNION的要求,它將始終從Redis(和我們的服務邏輯)中提取數據 將對整個項目集合中返回的項目數設置限制)

到目前為止,一切都很好,但是在SUNION上使用SSCAN可能會有不利的一面嗎? 好吧,可能存在—儘管像SUNION這樣的阻塞命令可以保證在給定的時間內提供集合中的所有元素,但是SSCAN命令只能提供有限的保證,因為集合可以在迭代過程中進行更改。 在完整的SSCAN迭代過程中,集合中不存在的元素可能會返回,也可能不會返回:它是未定義的。 但是,考慮到我們不會從Redis中刪除項目(只是添加新項目),因此這並不是我們服務中的真正問題。

確實運行了一些基準測試可以測試SSCAN(僅提取有限數量的項目)與SUNION提取所有數據,並且它們以編程方式僅從中提取第一個有限的項目,這顯示出明顯的改進。 對於小型Set,時間大致相同,但是對於具有1,000多個項目的大型Set,時間要好得多

我不知道我不瞭解的Redis知識

繁榮! 問題解決了。 從這個過程中我得出什麼結論? -可以說我們需要更好地瞭解我們的知識,但是在現實生活中,我們在日常工作中使用許多不同的服務和程序包(數據庫,雲服務,開源程序包等),而這幾乎是不可能的 掌握所有這些知識(不僅僅是性能,請考慮其中一些軟件包可能存在的安全性問題)。

但是,我們至少應該嘗試掌握我們所依賴的服務,這些軟件包會對我們的服務產生巨大影響。

(本文翻譯自Tzafrir Ben Ami的文章《What I didn't know I didn't know about Redis》,參考:https://medium.com/swlh/what-i-didnt-know-i-didn-t-know-about-redis-6ad3729f29ed)


分享到:


相關文章: