<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>

          Redis6.0 為何引入多線程?單線程它不香嗎?

          共 4913字,需瀏覽 10分鐘

           ·

          2020-08-13 13:06

          一百天前 Redis 作者 antirez 在博客上(antirez.com)發(fā)布了一條重磅消息,Redis6.0 正式發(fā)布了。其中最引人注目的改動(dòng)就是,Redis6.0 引入了多線程

          本文主要分兩部分。首先我們先聊一下?Redis6.0 之前為什么采用單線程模型,然后再詳細(xì)解釋 Redis6.0 的多線程

          Redis6.0 之前為何采用單線程模型

          嚴(yán)格地說,從 Redis 4.0 之后并不是單線程。除了主線程外,還有一些后臺(tái)線程處理一些較為緩慢的操作,例如無用連接的釋放、大 key 的刪除等等。

          單線程模型,為何性能那么高?

          Redis 作者從設(shè)計(jì)之初,進(jìn)行了多方面的考慮。最終選擇使用單線程模型來處理命令。之所以選擇單線程模型,主要有如下幾個(gè)重要原因:

          ?Redis 操作基于內(nèi)存,絕大多數(shù)操作的性能瓶頸不在 CPU?單線程模型,避免了線程間切換帶來的性能開銷?使用單線程模型也能并發(fā)的處理客戶端的請(qǐng)求(多路復(fù)用 I/O)?使用單線程模型,可維護(hù)性更高,開發(fā),調(diào)試和維護(hù)的成本更低

          上述第三個(gè)原因是 Redis 最終采用單線程模型的決定性因素,其他的兩個(gè)原因都是使用單線程模型額外帶來的好處,在這里我們會(huì)按順序介紹上述的幾個(gè)原因。

          性能瓶頸不在 CPU

          下圖是 Redis 官網(wǎng)對(duì)單線程模型的說明。大概意思是:Redis 的瓶頸并不在 CPU,它的主要瓶頸在于內(nèi)存和網(wǎng)絡(luò)。在 Linux 環(huán)境中,Redis 每秒甚至可以提交 100 萬次請(qǐng)求。

          為什么說 Redis 的瓶頸不在 CPU?

          首先,Redis 絕大部分操作是基于內(nèi)存的,而且是純 kv(key-value)操作,所以命令執(zhí)行速度非常快。我們可以大概理解成,Redis 中的數(shù)據(jù)存儲(chǔ)在一張大 HashMap 中,HashMap 的優(yōu)勢就是查找和寫入的時(shí)間復(fù)雜度都是 O(1)。Redis 內(nèi)部采用這種結(jié)構(gòu)存儲(chǔ)數(shù)據(jù),就奠定了 Redis 高性能的基礎(chǔ)。根據(jù) Redis 官網(wǎng)描述,在理想情況下 Redis 每秒可以提交一百萬次請(qǐng)求,每次請(qǐng)求提交所需的時(shí)間在納秒的時(shí)間量級(jí)。既然每次的 Redis 操作都這么快,單線程就可以完全搞定了,那還何必要用多線程呢!

          線程上下文切換問題

          另外,多線程場景下會(huì)發(fā)生線程上下文切換。線程是由 CPU 調(diào)度的,CPU 的一個(gè)核在一個(gè)時(shí)間片內(nèi)只能同時(shí)執(zhí)行一個(gè)線程,在 CPU 由線程 A 切換到線程 B 的過程中會(huì)發(fā)生一系列的操作,主要過程包括保存線程 A 的執(zhí)行現(xiàn)場,然后載入線程 B 的執(zhí)行現(xiàn)場,這個(gè)過程就是“線程上下文切換”。其中涉及線程相關(guān)指令的保存和恢復(fù)。

          頻繁的線程上下文切換可能會(huì)導(dǎo)致性能急劇下降,這會(huì)導(dǎo)致我們不僅沒有提升處理請(qǐng)求的速度,反而降低了性能,這也是 Redis 對(duì)于多線程技術(shù)持謹(jǐn)慎態(tài)度的原因之一。

          在 Linux 系統(tǒng)中可以使用 vmstat 命令來查看上下文切換的次數(shù),下面是 vmstat 查看上下文切換次數(shù)的示例:

          vmstat 1 表示每秒統(tǒng)計(jì)一次, 其中 cs 列就是指上下文切換的數(shù)目. 一般情況下, 空閑系統(tǒng)的上下文切換每秒在 1500 以下。

          并行處理客戶端的請(qǐng)求(I/O 多路復(fù)用)

          如上所述:Redis 的瓶頸并不在 CPU,它的主要瓶頸在于內(nèi)存和網(wǎng)絡(luò)。所謂內(nèi)存瓶頸很好理解,Redis 做為緩存使用時(shí)很多場景需要緩存大量數(shù)據(jù),所以需要大量內(nèi)存空間,這可以通過集群分片去解決,例如 Redis 自身的無中心集群分片方案以及 Codis 這種基于代理的集群分片方案。

          對(duì)于網(wǎng)絡(luò)瓶頸,Redis 在網(wǎng)絡(luò) I/O 模型上采用了多路復(fù)用技術(shù),來減少網(wǎng)絡(luò)瓶頸帶來的影響。很多場景中使用單線程模型并不意味著程序不能并發(fā)的處理任務(wù)。Redis 雖然使用單線程模型處理用戶的請(qǐng)求,但是它卻使用 I/O 多路復(fù)用技術(shù)“并行”處理來自客戶端的多個(gè)連接,同時(shí)等待多個(gè)連接發(fā)送的請(qǐng)求。使用 I/O 多路復(fù)用技術(shù)能極大地減少系統(tǒng)的開銷,系統(tǒng)不再需要為每個(gè)連接創(chuàng)建專門的監(jiān)聽線程,避免了由于大量的線程創(chuàng)建帶來的巨大性能開銷。

          下面我們?cè)敿?xì)解釋一下多路復(fù)用 I/O 模型。為了能更充分理解,我們先了解幾個(gè)基本概念。

          Socket(套接字):Socket 可以理解成,在兩個(gè)應(yīng)用程序進(jìn)行網(wǎng)絡(luò)通信時(shí),分別在兩個(gè)應(yīng)用程序中的通信端點(diǎn)。通信時(shí),一個(gè)應(yīng)用程序?qū)?shù)據(jù)寫入 Socket,然后通過網(wǎng)卡把數(shù)據(jù)發(fā)送到另外一個(gè)應(yīng)用程序的 Socket 中。我們平常所說的 HTTP 和 TCP 協(xié)議的遠(yuǎn)程通信,底層都是基于 Socket 實(shí)現(xiàn)的。5 種網(wǎng)絡(luò) IO 模型也都要基于 Socket 實(shí)現(xiàn)網(wǎng)絡(luò)通信。

          阻塞與非阻塞:所謂阻塞,就是發(fā)出一個(gè)請(qǐng)求不能立刻返回響應(yīng),要等所有的邏輯全處理完才能返回響應(yīng)。非阻塞反之,發(fā)出一個(gè)請(qǐng)求立刻返回應(yīng)答,不用等處理完所有邏輯。

          內(nèi)核空間與用戶空間:在 Linux 中,應(yīng)用程序穩(wěn)定性遠(yuǎn)遠(yuǎn)比不上操作系統(tǒng)程序,為了保證操作系統(tǒng)的穩(wěn)定性,Linux 區(qū)分了內(nèi)核空間和用戶空間。可以這樣理解,內(nèi)核空間運(yùn)行操作系統(tǒng)程序和驅(qū)動(dòng)程序,用戶空間運(yùn)行應(yīng)用程序。Linux 以這種方式隔離了操作系統(tǒng)程序和應(yīng)用程序,避免了應(yīng)用程序影響到操作系統(tǒng)自身的穩(wěn)定性。這也是 Linux 系統(tǒng)超級(jí)穩(wěn)定的主要原因。所有的系統(tǒng)資源操作都在內(nèi)核空間進(jìn)行,比如讀寫磁盤文件,內(nèi)存分配和回收,網(wǎng)絡(luò)接口調(diào)用等。所以在一次網(wǎng)絡(luò) IO 讀取過程中,數(shù)據(jù)并不是直接從網(wǎng)卡讀取到用戶空間中的應(yīng)用程序緩沖區(qū),而是先從網(wǎng)卡拷貝到內(nèi)核空間緩沖區(qū),然后再從內(nèi)核拷貝到用戶空間中的應(yīng)用程序緩沖區(qū)。對(duì)于網(wǎng)絡(luò) IO 寫入過程,過程則相反,先將數(shù)據(jù)從用戶空間中的應(yīng)用程序緩沖區(qū)拷貝到內(nèi)核緩沖區(qū),再從內(nèi)核緩沖區(qū)把數(shù)據(jù)通過網(wǎng)卡發(fā)送出去。

          多路復(fù)用 I/O 模型,建立在多路事件分離函數(shù) select,poll,epoll 之上。以 Redis 采用的 epoll 為例,在發(fā)起 read 請(qǐng)求前,先更新 epoll 的 socket 監(jiān)控列表,然后等待 epoll 函數(shù)返回(此過程是阻塞的,所以說多路復(fù)用 IO 本質(zhì)上也是阻塞 IO 模型)。當(dāng)某個(gè) socket 有數(shù)據(jù)到達(dá)時(shí),epoll 函數(shù)返回。此時(shí)用戶線程才正式發(fā)起 read 請(qǐng)求,讀取并處理數(shù)據(jù)。這種模式用一個(gè)專門的監(jiān)視線程去檢查多個(gè) socket,如果某個(gè) socket 有數(shù)據(jù)到達(dá)就交給工作線程處理。由于等待 Socket 數(shù)據(jù)到達(dá)過程非常耗時(shí),所以這種方式解決了阻塞 IO 模型一個(gè) Socket 連接就需要一個(gè)線程的問題,也不存在非阻塞 IO 模型忙輪詢帶來的 CPU 性能損耗的問題。多路復(fù)用 IO 模型的實(shí)際應(yīng)用場景很多,大家耳熟能詳?shù)?Redis,Java NIO,以及 Dubbo 采用的通信框架 Netty 都采用了這種模型。

          下圖是基于 epoll 函數(shù) Socket 編程的詳細(xì)流程。

          可維護(hù)性

          我們知道,多線程可以充分利用多核 CPU,在高并發(fā)場景下,能夠減少因 I/O 等待帶來的 CPU 損耗,帶來很好的性能表現(xiàn)。不過多線程卻是一把雙刃劍,帶來好處的同時(shí),還會(huì)帶來代碼維護(hù)困難,線上問題難于定位和調(diào)試,死鎖等問題。多線程模型中代碼的執(zhí)行過程不再是串行的,多個(gè)線程同時(shí)訪問的共享變量如果處理不當(dāng)也會(huì)帶來詭異的問題。

          我們通過一個(gè)例子,看一下多線程場景下發(fā)生的詭異現(xiàn)象。看下面的代碼:

          class MemoryReordering {  int num = 0;  boolean flag = false;  public void set() {    num = 1;     //語句1    flag = true; //語句2  }  public int cal() {    if( flag == true) {    //語句3      return num + num; //語句4    }    return -1  }}

          flag 為 true 時(shí),cal() 方法返回值是多少?很多人會(huì)說:這還用問嗎!肯定返回 2

          結(jié)果可能會(huì)讓你大吃一驚!上面的這段代碼,由于語句 1 和語句 2 沒有數(shù)據(jù)依賴性,可能會(huì)發(fā)生指令重排序,有可能編譯器會(huì)把 flag=true 放到 num=1 的前面。此時(shí) set 和 cal 方法分別在不同線程中執(zhí)行,沒有先后關(guān)系。cal 方法,只要 flag 為 true,就會(huì)進(jìn)入 if 的代碼塊執(zhí)行相加的操作。可能的順序是:

          ?語句 1 先于語句 2 執(zhí)行,這時(shí)的執(zhí)行順序可能是:語句 1->語句 2->語句 3->語句 4。執(zhí)行語句 4 前,num = 1,所以 cal 的返回值是 2?語句 2 先于語句 1 執(zhí)行,這時(shí)的執(zhí)行順序可能是:語句 2->語句 3->語句 4->語句 1。執(zhí)行語句 4 前,num = 0,所以 cal 的返回值是 0

          我們可以看到,在多線程環(huán)境下如果發(fā)生了指令重排序,會(huì)對(duì)結(jié)果造成嚴(yán)重影響。

          當(dāng)然可以在第三行處,給 flag 加上關(guān)鍵字 volatile 來避免指令重排。即在 flag 處加上了內(nèi)存柵欄,來阻隔 flag(柵欄)前后的代碼的重排序。當(dāng)然多線程還會(huì)帶來可見性問題,死鎖問題以及共享資源安全等問題。

          boolean volatile flag = false;

          Redis6.0 為何引入多線程?

          Redis6.0 引入的多線程部分,實(shí)際上只是用來處理網(wǎng)絡(luò)數(shù)據(jù)的讀寫和協(xié)議解析,執(zhí)行命令仍然是單一工作線程。

          從上圖我們可以看到 Redis 在處理網(wǎng)絡(luò)數(shù)據(jù)時(shí),調(diào)用 epoll 的過程是阻塞的,也就是說這個(gè)過程會(huì)阻塞線程,如果并發(fā)量很高,達(dá)到幾萬的 QPS,此處可能會(huì)成為瓶頸。一般我們遇到此類網(wǎng)絡(luò) IO 瓶頸的問題,可以增加線程數(shù)來解決。開啟多線程除了可以減少由于網(wǎng)絡(luò) I/O 等待造成的影響,還可以充分利用 CPU 的多核優(yōu)勢。Redis6.0 也不例外,在此處增加了多線程來處理網(wǎng)絡(luò)數(shù)據(jù),以此來提高 Redis 的吞吐量。當(dāng)然相關(guān)的命令處理還是單線程運(yùn)行,不存在多線程下并發(fā)訪問帶來的種種問題。

          性能對(duì)比

          壓測配置:

          Redis Server: 阿里云 Ubuntu 18.048 CPU 2.5 GHZ, 8G 內(nèi)存,主機(jī)型號(hào) ecs.ic5.2xlargeRedis Benchmark Client: 阿里云 Ubuntu 18.048 2.5 GHZ CPU, 8G 內(nèi)存,主機(jī)型號(hào) ecs.ic5.2xlarge

          多線程版本 Redis 6.0,單線程版本是 Redis 5.0.5。多線程版本需要新增以下配置:

          io-threads 4 # 開啟 4 個(gè) IO 線程io-threads-do-reads yes # 請(qǐng)求解析也是用 IO 線程

          壓測命令:

          redis-benchmark -h 192.168.0.49 -a foobared -t set,get -n 1000000 -r 100000000 --threads 4 -d \${datasize} -c 256
          圖片來源于網(wǎng)絡(luò)
          圖片來源于網(wǎng)絡(luò)

          從上面可以看到 GET/SET 命令在多線程版本中性能相比單線程幾乎翻了一倍。另外,這些數(shù)據(jù)只是為了簡單驗(yàn)證多線程 I/O 是否真正帶來性能優(yōu)化,并沒有針對(duì)具體的場景進(jìn)行壓測,數(shù)據(jù)僅供參考。本次性能測試基于 unstble 分支,不排除后續(xù)發(fā)布的正式版本的性能會(huì)更好。

          最后

          可見單線程有單線程的好處,多線程有多線程的優(yōu)勢,只有充分理解其中的本質(zhì)原理,才能靈活運(yùn)用于生產(chǎn)實(shí)踐當(dāng)中。

          希望本文對(duì)大家有所幫助。如果感覺本文有幫助,有勞轉(zhuǎn)發(fā)或點(diǎn)一下“在看”!讓更多人收獲知識(shí)!


          長按識(shí)別下圖二維碼,關(guān)注公眾號(hào)「Doocs 開源社區(qū)」,第一時(shí)間跟你們分享好玩、實(shí)用的技術(shù)文章與業(yè)內(nèi)最新資訊。



          瀏覽 139
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  伊人99热 | 国产精品盗摄!偷窥盗摄 | 日本天堂在线播放 | 粉嫩99精品99久久久久久特污兔 | 亚洲无码久久 |