<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          7000字全面字講解 Redis 性能優(yōu)化點!

          共 7203字,需瀏覽 15分鐘

           ·

          2021-05-02 09:33

          點擊上方“服務端思維”,選擇“設為星標

          回復”669“獲取獨家整理的精選資料集

          回復”加群“加入全國服務端高端社群「后端圈」


          f262a09c1d1b70ebc841558aa1e7258b.webp

          作者 | 田維常出品?| Java后端技術全棧

          前言

          在一些網(wǎng)絡服務的系統(tǒng)中,Redis 的性能,可能是比 MySQL 等硬盤數(shù)據(jù)庫的性能更重要的課題。

          比如微博,把熱點微博[1],最新的用戶關系,都存儲在 Redis 中,大量的查詢擊中 Redis,而不走 MySQL。

          那么,針對 Redis 服務,我們能做哪些性能優(yōu)化呢?或者說,應該避免哪些性能浪費呢?

          Redis 性能的基本面

          在討論優(yōu)化之前,我們需要知道,Redis 服務本身就有一些特性,比如單線程運行。除非修改 Redis 的源代碼,不然這些特性,就是我們思考性能優(yōu)化的基本面。

          那么,有哪些 Redis 基本特性需要我們考慮呢?Redis 的項目介紹中概括了它特性:

          Redis?is?an?in-memory?database?that?persists?on?disk.?
          The?data?model?is?key-value,?but?many?different?kind?of?values?are?supported.
          • 首先,Redis 使用操作系統(tǒng)提供的虛擬內存來存儲數(shù)據(jù)。而且,這個操作系統(tǒng)一般就是指 Unix。Windows 上也能運行 Redis,但是需要特殊處理。如果你的操作系統(tǒng)使用交換空間,那么 Redis 的數(shù)據(jù)可能會被實際保存在硬盤上。

          • 其次,Redis 支持持久化,可以把數(shù)據(jù)保存在硬盤上。很多時候,我們也確實有必要進行持久化來實現(xiàn)備份,數(shù)據(jù)恢復等需求。但持久化不會憑空發(fā)生,它也會占用一部分資源。

          • 再次,Redis 是用 key-value 的方式來讀寫的,而 value 中又可以是很多不同種類的數(shù)據(jù);更進一步,一個數(shù)據(jù)類型的底層還有被存儲為不同的結構。不同的存儲結構決定了數(shù)據(jù)增刪改查的復雜度以及性能開銷。

          • 最后,在上面的介紹中沒有提到的是,Redis 大多數(shù)時候是單線程運行[2]的(single-threaded),即同一時間只占用一個?CPU,只能有一個指令在運行,并行讀寫是不存在的。很多操作帶來的延遲問題,都可以在這里找到答案。

          關于最后這個特性,為什么 Redis 是單線程的,卻能有很好的性能(根據(jù)?Amdahl’s Law,優(yōu)化耗時占比大的過程,才更有意義),兩句話概括是:Redis 利用了多路 I/O 復用機制[3],處理客戶端請求時,不會阻塞主線程;Redis 單純執(zhí)行(大多數(shù)指令)一個指令不到 1 微秒[4],如此,單核 CPU 一秒就能處理 1 百萬個指令(大概對應著幾十萬個請求吧),用不著實現(xiàn)多線程(網(wǎng)絡才是瓶頸[5])。

          優(yōu)化網(wǎng)絡延時

          Redis 的官方博客在幾個地方都說,性能瓶頸更可能是網(wǎng)絡[6],那么我們如何優(yōu)化網(wǎng)絡上的延時呢?

          首先,如果你們使用單機部署(應用服務和 Redis 在同一臺機器上)的話,使用 Unix 進程間通訊來請求 Redis 服務,速度比?localhost?局域網(wǎng)(學名?loopback)更快。官方文檔[7]是這么說的,想一想,理論上也應該是這樣的。

          但很多公司的業(yè)務規(guī)模不是單機部署能支撐的,所以還是得用 TCP。

          Redis 客戶端和服務器的通訊一般使用 TCP 長鏈接。如果客戶端發(fā)送請求后需要等待 Redis 返回結果再發(fā)送下一個指令,客戶端和 Redis 的多個請求就構成下面的關系:

          380338237f47dcd4cc644141b30e8878.webp

          如果不是你要發(fā)送的 key 特別長,一個 TCP 包完全能放下 Redis 指令,所以只畫了一個 push 包

          這樣這兩次請求中,客戶端都需要經歷一段網(wǎng)絡傳輸時間。

          但如果有可能,完全可以使用 multi-key 類的指令來合并請求,比如兩個 GET key 可以用 MGET key1 key2 合并。這樣在實際通訊中,請求數(shù)也減少了,延時自然得到好轉。

          如果不能用 multi-key 指令來合并,比如一個 SET,一個 GET 無法合并。怎么辦?

          Redis 中有至少這樣兩個方法能合并多個指令到一個 request 中,一個是?MULTI/EXEC,一個是 script。前者本來是構建 Redis 事務的方法,但確實可以合并多個指令為一個 request,它到通訊過程如下。至于 script,最好利用緩存腳本的?sha1 hash key?來調起腳本,這樣通訊量更小。

          ad07f3069d69690d67fd18c014188a10.webp

          這樣確實更能減少網(wǎng)絡傳輸時間,不是么?但如此以來,就必須要求這個?transaction / script?中涉及的 key 在同一個 node 上,所以要酌情考慮。

          如果上面的方法我們都考慮過了,還是沒有辦法合并多個請求,我們還可以考慮合并多個?responses。比如把 2 個回復信息合并:

          9400fc590e4ed97c102532c943843b41.webp

          這樣,理論上可以省去 1 次回復所用的網(wǎng)絡傳輸時間。這就是?pipeline?做的事情。舉個?ruby?客戶端使用?pipeline?的例子:

          require?'redis'
          @redis?=?Redis.new()
          @redis.pipelined?do
          [email protected]?'key1'
          ????@redis.set?'key2'?'some?value'
          end
          #?=>?[1,?2]

          據(jù)說,有些語言的客戶端,甚至默認就使用 ?pipeline?來優(yōu)化延時問題,比如?node_redis

          另外,不是任意多個回復信息都可以放進一個?TCP?包中,如果請求數(shù)太多,回復的數(shù)據(jù)很長(比如 get 一個長字符串),TCP 還是會分包傳輸,但使用?pipeline,依然可以減少傳輸次數(shù)。

          pipeline?和上面的其他方法都不一樣的是,它不具有原子性。所以在?cluster?狀態(tài)下的集群上,實現(xiàn)?pipeline?比那些原子性的方法更有可能。

          小總結

          • 使用 unix 進程間通信,如果單機部署
          • 使用 multi-key 指令合并多個指令,減少請求數(shù),如果有可能的話
          • 使用 transaction、script 合并 requests 以及 responses
          • 使用 pipeline 合并 response

          警惕執(zhí)行時間長的操作

          在大數(shù)據(jù)量的情況下,有些操作的執(zhí)行時間會相對長,比如?KEYS *,LRANGE mylist 0 -1,以及其他算法復雜度為?O(n)?的指令。因為 Redis 只用一個線程來做數(shù)據(jù)查詢,如果這些指令耗時很長,就會阻塞 Redis,造成大量延時。

          盡管官方文檔中說?KEYS *的查詢挺快的,(在普通筆記本上)掃描?1 百萬個 key,只需 40 毫秒(參見:https://redis.io/commands/keys),但幾十 ms 對于一個性能要求很高的系統(tǒng)來說,已經不短了,更何況如果有幾億個 key(一臺機器完全可能存幾億個 key,比如一個?key 100字節(jié),1 億個?key 只有 10GB),時間更長。

          所以,盡量不要在生產環(huán)境的代碼使用這些執(zhí)行很慢的指令,這一點 Redis 的作者在博客[8]中也提到了。另外,運維同學查詢 Redis 的時候也盡量不要用。甚至,Redis Essential?這本書建議利用?rename-command KEYS ''來禁止使用這個耗時的指令。

          除了這些耗時的指令,Redis 中?transaction,script,因為可以合并多個?commands?為一個具有原子性的執(zhí)行過程,所以也可能占用 Redis 很長時間,需要注意。

          如果你想找出生產環(huán)境使用的「慢指令」,那么可以利用?SLOWLOG GET count?來查看最近的?count個執(zhí)行時間很長的指令。至于多長算長,可以通過在?redis.conf?中設置?slowlog-log-slower-than?來定義。

          除此之外,在很多地方都沒有提到的一個可能的慢指令是?DEL,但?redis.conf文件的注釋[9]中倒是說了。長話短說就是 DEL 一個大的?object?時候,回收相應的內存可能會需要很長時間(甚至幾秒),所以,建議用 DEL 的異步版本:UNLINK。后者會啟動一個新的thread?來刪除目標 key,而不阻塞原來的線程。

          更進一步,當一個 key 過期之后,Redis?一般也需要同步的把它刪除。其中一種刪除?keys的方式是,每秒 10 次的檢查一次有設置過期時間的?keys,這些 keys 存儲在一個全局的?struct?中,可以用?server.db->expires?訪問。檢查的方式是:

          1. 從中隨機取出 20 個 keys
          2. 把過期的刪掉。
          3. 如果剛剛 20 個 keys 中,有 25% 以上(也就是 5 個以上)都是過期的,Redis 認為,過期的 keys 還挺多的,繼續(xù)重復步驟 1,直到滿足退出條件:某次取出的 keys 中沒有那么多過去的 keys。這里對于性能的影響是,如果真的有很多的 keys 在同一時間過期,那么 Redis 真的會一直循環(huán)執(zhí)行刪除,占用主線程。

          對此,Redis 作者的建議[10]是警惕?EXPIREAT?這個指令,因為它更容易產生 keys 同時過期的現(xiàn)象。我還見到過一些建議是給 keys 的過期時間設置一個隨機波動量。最后,redis.conf中也給出了一個方法,把 keys 的過期刪除操作變?yōu)楫惒降模矗?redis.conf?中設置?lazyfree-lazy-expire yes

          優(yōu)化數(shù)據(jù)結構、使用正確的算法

          一種數(shù)據(jù)類型(比如?stringlist)進行增刪改查的效率是由其底層的存儲結構決定的。

          我們在使用一種數(shù)據(jù)類型時,可以適當關注一下它底層的存儲結構及其算法,避免使用復雜度太高的方法。舉兩個例子:

          ZADD?的時間復雜度是?O(log(N)),這比其他數(shù)據(jù)類型增加一個新元素的操作更復雜,所以要小心使用。若?Hash?類型的值的?fields?數(shù)量有限,它很有可能采用?ziplist?這種結構做存儲,而?ziplist?的查詢效率可能沒有同等字段數(shù)量的?hashtable?效率高,在必要時,可以調整 Redis 的存儲結構。

          除了時間性能上的考慮,有時候我們還需要節(jié)省存儲空間。比如上面提到的?ziplist?結構,就比?hashtable?結構節(jié)省存儲空間(Redis Essentials?的作者分別在?hashtable?和?ziplist?結構的 Hash 中插入 500 個 fields,每個 field 和 value 都是一個 15 位左右的字符串,結果是hashtable?結構使用的空間是?ziplist?的 4 倍。)。但節(jié)省空間的數(shù)據(jù)結構,其算法的復雜度可能很高。所以,這里就需要在具體問題面前做出權衡。

          如何做出更好的權衡?我覺得得深挖 Redis 的存儲結構才能讓自己安心。這方面的內容我們下次再說。

          以上這三點都是編程層面的考慮,寫程序時應該注意啊。下面這幾點,也會影響 Redis 的性能,但解決起來,就不只是靠代碼層面的調整了,還需要架構和運維上的考慮。

          考慮操作系統(tǒng)和硬件是否影響性能

          Redis 運行的外部環(huán)境,也就是操作系統(tǒng)和硬件顯然也會影響 Redis 的性能。在官方文檔中,就給出了一些例子:

          • CPU:Intel 多種 CPU 都比 AMD 皓龍系列好

          • 虛擬化:實體機比虛擬機好,主要是因為部分虛擬機上,硬盤不是本地硬盤,監(jiān)控軟件導致 fork 指令的速度慢(持久化時會用到?fork),尤其是用?Xen?來做虛擬化時。

          • 內存管理:在 linux 操作系統(tǒng)中,為了讓?translation lookaside buffer,即 TLB,能夠管理更多內存空間(TLB 只能緩存有限個 page),操作系統(tǒng)把一些?memory page?變得更大,比如 2MB 或者 1GB,而不是通常的 4096 字節(jié),這些大的內存頁叫做?huge pages。同時,為了方便程序員使用這些大的內存 page,操作系統(tǒng)中實現(xiàn)了一個?transparent huge pages(THP)機制,使得大內存頁對他們來說是透明的,可以像使用正常的內存?page一樣使用他們。但這種機制并不是數(shù)據(jù)庫所需要的,可能是因為 THP 會把內存空間變得緊湊而連續(xù)吧,就像mongodb?的文檔[11]中明確說的,數(shù)據(jù)庫需要的是稀疏的內存空間,所以請禁掉 THP 功能。Redis 也不例外,但 Redis 官方博客上給出的理由是:使用大內存 page 會使bgsave?時,fork?的速度變慢;如果 fork 之后,這些內存 page 在原進程中被修改了,他們就需要被復制(即?copy on write),這樣的復制會消耗大量的內存(畢竟,人家是?huge pages,復制一份消耗成本很大)。所以,請禁止掉操作系統(tǒng)中的?transparent huge pages?功能。

          • 交換空間:當一些內存 page 被存儲在交換空間文件上,而 Redis 又要請求那些數(shù)據(jù),那么操作系統(tǒng)會阻塞 Redis 進程,然后把想要的 page,從交換空間中拿出來,放進內存。這其中涉及整個進程的阻塞,所以可能會造成延時問題,一個解決方法是禁止使用交換空間(Redis Essentials?中如是建議,如果內存空間不足,請用別的方法處理)。

          考慮持久化帶來的開銷

          Redis 的一項重要功能就是持久化,也就是把數(shù)據(jù)復制到硬盤上。基于持久化,才有了 Redis 的數(shù)據(jù)恢復等功能。

          但維護這個持久化的功能,也是有性能開銷的。

          首先說,RDB 全量持久化。

          這種持久化方式把 Redis 中的全量數(shù)據(jù)打包成 rdb 文件放在硬盤上。但是執(zhí)行 RDB 持久化過程的是原進程 fork 出來一個子進程,而 fork 這個系統(tǒng)調用是需要時間的,根據(jù)Redis Lab 6年前做的實驗[12],在一臺新型的?AWS EC2 m1.small^13上,fork?一個內存占用?1GB?的 Redis 進程,需要 700+ 毫秒,而這段時間,redis 是無法處理請求的。

          雖然現(xiàn)在的機器應該都會比那個時候好,但是 fork 的開銷也應該考慮吧。為此,要使用合理的 RDB 持久化的時間間隔,不要太頻繁。

          接下來,我們看另外一種持久化方式:AOF 增量持久化。

          這種持久化方式會把你發(fā)到?redis server?的指令以文本的形式保存下來(格式遵循?redis protocol),這個過程中,會調用兩個系統(tǒng)調用,一個是 write(2),同步完成,一個是 fsync(2),異步完成。

          這兩部都可能是延時問題的原因:

          write 可能會因為輸出的 buffer 滿了,或者 kernal 正在把 buffer 中的數(shù)據(jù)同步到硬盤,就被阻塞了。fsync 的作用是確保 write 寫入到 aof 文件的數(shù)據(jù)落到了硬盤上,在一個 7200 轉/分的硬盤上可能要延時 20 毫秒左右,消耗還是挺大的。更重要的是,在 fsync 進行的時候,write 可能會被阻塞。其中,write 的阻塞貌似只能接受,因為沒有更好的方法把數(shù)據(jù)寫到一個文件中了。但對于 fsync,Redis 允許三種配置,選用哪種取決于你對備份及時性和性能的平衡:

          1. always:當把 appendfsync 設置為 always,fsync 會和客戶端的指令同步執(zhí)行,因此最可能造成延時問題,但備份及時性最好。
          2. everysec:每秒鐘異步執(zhí)行一次 fsync,此時 redis 的性能表現(xiàn)會更好,但是 fsync 依然可能阻塞 write,算是一個折中選擇。
          3. no:redis 不會主動出發(fā) fsync (并不是永遠不 fsync,那是不太可能的),而由 kernel 決定何時 fsync

          使用分布式架構

          讀寫分離、數(shù)據(jù)分片。

          以上,我們都是基于單臺,或者單個 Redis 服務進行優(yōu)化。下面,我們考慮當網(wǎng)站的規(guī)模變大時,利用分布式架構來保障 Redis 性能的問題。

          首先說,哪些情況下不得不(或者最好)使用分布式架構:

          數(shù)據(jù)量很大,單臺服務器內存不可能裝得下,比如 1 個 T 這種量級 需要服務高可用 單臺的請求壓力過大 解決這些問題可以采用數(shù)據(jù)分片或者主從分離,或者兩者都用(即,在分片用的 cluster 節(jié)點上,也設置主從結構)。

          這樣的架構,可以為性能提升加入新的切入點:

          把慢速的指令發(fā)到某些從庫中執(zhí)行 把持久化功能放在一個很少使用的從庫上 把某些大 list 分片 其中前兩條都是根據(jù) Redis 單線程的特性,用其他進程(甚至機器)做性能補充的方法。

          當然,使用分布式架構,也可能對性能有影響,比如請求需要被轉發(fā),數(shù)據(jù)需要被不斷復制分發(fā)。(待查)

          后話

          其實還有很多東西也影響 Redis 的性能,比如 active rehashing(keys 主表的再哈希,每秒 10 次,關掉它可以提升一點點性能),但是這篇博客已經寫的很長了。而且,更重要不是收集已經被別人提出的問題,然后記憶解決方案;而是掌握 Redis 的基本原理,以不變應萬變的方式決絕新出現(xiàn)的問題。

          我是老田,求、求在看、求轉發(fā),跪謝!

          源 |?rrd.me/gteAC

          — 本文結束 —


          597112c11212229729f8e99a592c4730.webp

          ●?漫談設計模式在 Spring 框架中的良好實踐

          ●?顛覆微服務認知:深入思考微服務的七個主流觀點

          ●?人人都是 API 設計者

          ●?一文講透微服務下如何保證事務的一致性

          ●?要黑盒測試微服務內部服務間調用,我該如何實現(xiàn)?



          關注我,回復 「加群」 加入各種主題討論群。



          對「服務端思維」有期待,請在文末點個在看

          喜歡這篇文章,歡迎轉發(fā)、分享朋友圈


          8029e7deadf1255fe32542059ba2d72c.webp在看點這里bb8d5fdaa403850844884e60674de93f.webp
          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费 无码 国产在线怀 | 性生活免费网站 | 丁香六月婷婷综合激情欧美 | 豆花一区二区三区 | 天天操天天日天天射 |