下面由Redis教學欄位介紹Redis6.0到底為何介紹多執行緒? ,希望對需要的朋友有幫助!
作者簡介:曾任職於阿里巴巴,每日優鮮等網路公司,擔任技術總監。 15年電商互聯網經驗。
一百天前Redis作者antirez在部落格上(antirez.com)發布了一條重磅消息,Redis6.0正式發布了。其中最引人注目的改動就是,Redis6.0引入了多執行緒。
本文主要分成兩部分。首先我們先聊聊Redis6.0之前為什麼要採用單執行緒模型。然後再詳細解釋Redis6.0的多執行緒。
Redis6.0之前為什麼採用單執行緒模型
嚴格地說,從Redis 4.0之後並不是單線程。除了主執行緒外,還有一些後台執行緒處理一些較為緩慢的操作,例如無用連線的釋放、大 key 的刪除等等。
單執行緒模型,為何效能那麼高?
Redis作者從設計之初,進行了多方面的考慮。最後選擇使用單線程模型來處理命令。之所以選擇單線程模型,主要有以下幾個重要原因:
Redis操作基於內存,絕大多數操作的性能瓶頸不在CPU
單執行緒模型,避免了執行緒間切換帶來的效能開銷
使用單執行緒模型也能並發的處理客戶端的請求(多路復用I/O)
使用單執行緒模型,可維護性更高,開發,除錯和維護的成本較低
上述第三個原因是Redis最終採用單線程模型的決定性因素,其他的兩個原因都是使用單線程模型額外帶來的好處,在這裡我們會按順序介紹上述的幾個原因。
#下圖是Redis官網對單執行緒模型的說明。大概意思是:Redis的瓶頸並不在CPU,它的主要瓶頸在於記憶體和網路。在Linux環境中,Redis每秒甚至可以提交100萬次請求。
為什麼說Redis的瓶頸不在CPU?
首先,Redis絕大部分的操作是基於記憶體的,而且是純kv(key-value)操作,所以指令執行速度非常快。我們可以大概理解成,redis中的資料儲存在一張大HashMap中,HashMap的優勢就是尋找和寫入的時間複雜度都是O(1)。 Redis內部採用這種結構儲存數據,就奠定了Redis高效能的基礎。根據Redis官網描述,在理想情況下Redis每秒可以提交一百萬次請求,每次請求提交所需的時間在奈秒的時間量級。既然每次的Redis操作都這麼快,單執行緒就可以完全搞定了,那還何必要用多執行緒呢!
執行緒上下文切換問題
另外,多執行緒場景下會發生執行緒上下文切換。線程是由CPU調度的,CPU的一個核在一個時間片內只能同時執行一個線程,在CPU由線程A切換到線程B的過程中會發生一系列的操作,主要過程包括保存線程A的執行現場,然後載入線程B的執行現場,這個過程就是「線程上下文切換」。其中涉及線程相關指令的保存和恢復。
頻繁的執行緒上下文切換可能會導致效能急劇下降,這會導致我們不僅沒有提升處理請求的速度,反而降低了效能,這也是 Redis 對於多執行緒技術持謹慎態度的原因之一。
vmstat 1 表示每秒統計一次, 其中cs列就是指上下文切換的數目. 一般情況下, 空閒系統的上下文切換每秒在1500以下。
#如上所述:Redis的瓶頸並不在CPU,它的主要瓶頸在於記憶體和網路。所謂內存瓶頸很好理解,Redis做為緩存使用時很多場景需要緩存大量數據,所以需要大量內存空間,這可以通過集群分片去解決,例如Redis自身的無中心集群分片方案以及Codis這種基於代理的集群分片方案。
#對於網路瓶頸,Redis在網路I/O模型上採用了多路復用技術,來減少網路瓶頸帶來的影響。很多場景中使用單線程模型並不意味著程式不能並發的處理任務。 Redis 雖然使用單線程模型處理用戶的請求,但是它卻使用 I/O 多路復用技術「並行」處理來自客戶端的多個連接,同時等待多個連接發送的請求。使用 I/O多路復用技術能大幅減少系統的開銷,系統不再需要為每個連接創建專門的監聽線程,避免了由於大量的線程創建帶來的巨大性能開銷。
下面我們詳細解釋一下多路復用I/O模型。為了能更充分理解,我們先了解幾個基本概念。
Socket(套接字):Socket可以理解成,在兩個應用程式進行網路通訊時,分別在兩個應用程式中的通訊端點。通訊時,一個應用程式將資料寫入Socket,然後透過網路卡把資料發送到另外一個應用程式的Socket。我們平常所說的HTTP和TCP協定的遠端通信,底層都是基於Socket實現的。 5種網路IO模型也都要基於Socket實現網路通訊。
阻塞與非阻塞:所謂阻塞,就是發出一個請求不能立刻回傳回應,要等所有的邏輯全處理完才能回傳回應。非阻塞反之,發出一個請求立刻返回應答,不用等處理完所有邏輯。
核心空間與使用者空間:在Linux中,應用程式穩定性遠遠比不上作業系統程序,為了確保作業系統的穩定性,Linux區分了核心空間和使用者空間。可以這樣理解,內核空間運行作業系統程式和驅動程序,用戶空間運行應用程式。 Linux以這種方式隔離了作業系統程式和應用程序,避免了應用程式影響到作業系統本身的穩定性。這也是Linux系統超穩定的主因。所有的系統資源操作都在核心空間進行,例如讀寫磁碟文件,記憶體分配和回收,網路介面呼叫等。所以在一次網路IO讀取過程中,資料並不是直接從網路卡讀取到用戶空間中的應用程式緩衝區,而是先從網卡拷貝到核心空間緩衝區,然後再從核心拷貝到用戶空間中的應用程式緩衝區。對於網路IO寫入過程,過程則相反,先將資料從用戶空間中的應用程式緩衝區拷貝到核心緩衝區,再從核心緩衝區把資料透過網卡發送出去。
多路復用I/O模型,建立在多路事件分離函數select,poll,epoll之上。以Redis採用的epoll為例,在發起read請求前,先更新epoll的socket監控列表,然後等待epoll函數返回(此過程是阻塞的,所以說多路復用IO本質上也是阻塞IO模型)。當某個socket有資料到達時,epoll函數會傳回。此時用戶執行緒才正式發起read請求,讀取並處理資料。這個模式用一個專門的監視線程去檢查多個socket,如果某個socket有資料到達就交給工作執行緒處理。由於等待Socket資料到達過程非常耗時,所以這種方式解決了阻塞IO模型一個Socket連接就需要一個線程的問題,也不存在非阻塞IO模型忙輪詢帶來的CPU效能損耗的問題。多工IO模型的實際應用場景很多,大家耳熟能詳的Redis,Java NIO,以及Dubbo採用的通訊框架Netty都採用了這種模型。
下圖是基於epoll函數Socket程式設計的詳細流程。
#我們知道,多執行緒可以充分利用多核心CPU,在高並發場景下,能夠減少因I/O等待而帶來的CPU損耗,帶來良好的效能表現。不過多執行緒卻是一把雙面刃,帶來好處的同時,還會帶來程式碼維護困難,線上問題難於定位與調試,死鎖等問題。多執行緒模型中程式碼的執行過程不再是串列的,多個執行緒同時存取的共享變數如果處理不當也會帶來詭異的問題。
我們透過一個例子,看多執行緒場景下發生的詭異現象。看下面的程式碼:
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時,cal() 方法回傳值是多少?很多人會說:這還用問嗎!肯定回傳2
结果可能会让你大吃一惊!上面的这段代码,由于语句1和语句2没有数据依赖性,可能会发生指令重排序,有可能编译器会把flag=true放到num=1的前面。此时set和cal方法分别在不同线程中执行,没有先后关系。cal方法,只要flag为true,就会进入if的代码块执行相加的操作。可能的顺序是:
语句1先于语句2执行,这时的执行顺序可能是:语句1->语句2->语句3->语句4。执行语句4前,num = 1,所以cal的返回值是2
语句2先于语句1执行,这时的执行顺序可能是:语句2->语句3->语句4->语句1。执行语句4前,num = 0,所以cal的返回值是0
我们可以看到,在多线程环境下如果发生了指令重排序,会对结果造成严重影响。
当然可以在第三行处,给flag加上关键字volatile来避免指令重排。即在flag处加上了内存栅栏,来阻隔flag(栅栏)前后的代码的重排序。当然多线程还会带来可见性问题,死锁问题以及共享资源安全等问题。
boolean volatile flag = false;
Redis6.0引入的多线程部分,实际上只是用来处理网络数据的读写和协议解析,执行命令仍然是单一工作线程。
从上图我们可以看到Redis在处理网络数据时,调用epoll的过程是阻塞的,也就是说这个过程会阻塞线程,如果并发量很高,达到几万的QPS,此处可能会成为瓶颈。一般我们遇到此类网络IO瓶颈的问题,可以增加线程数来解决。开启多线程除了可以减少由于网络I/O等待造成的影响,还可以充分利用CPU的多核优势。Redis6.0也不例外,在此处增加了多线程来处理网络数据,以此来提高Redis的吞吐量。当然相关的命令处理还是单线程运行,不存在多线程下并发访问带来的种种问题。
性能对比
压测配置:
Redis Server: 阿里云 Ubuntu 18.04,8 CPU 2.5 GHZ, 8G 内存,主机型号 ecs.ic5.2xlarge Redis Benchmark Client: 阿里云 Ubuntu 18.04,8 2.5 GHZ CPU, 8G 内存,主机型号 ecs.ic5.2xlarge
多线程版本Redis 6.0,单线程版本是 Redis 5.0.5。多线程版本需要新增以下配置:
io-threads 4 # 开启 4 个 IO 线程 io-threads-do-reads yes # 请求解析也是用 IO 线程
压测命令: redis-benchmark -h 192.168.0.49 -a foobared -t set,get -n 1000000 -r 100000000 --threads 4 -d ${datasize} -c 256
图片来源于网络
图片来源于网络
从上面可以看到 GET/SET 命令在多线程版本中性能相比单线程几乎翻了一倍。另外,这些数据只是为了简单验证多线程 I/O 是否真正带来性能优化,并没有针对具体的场景进行压测,数据仅供参考。本次性能测试基于 unstble 分支,不排除后续发布的正式版本的性能会更好。
最后
可见单线程有单线程的好处,多线程有多线程的优势,只有充分理解其中的本质原理,才能灵活运用于生产实践当中。
以上是Redis6.0到底為何引入多執行緒?的詳細內容。更多資訊請關注PHP中文網其他相關文章!