親密接觸Redis-第二天(Redis Sentinel)

簡介


經過上次輕鬆搭建了一個Redis的環境並用Java代碼調通後,這次我們要來看看Redis的一些坑以及Redis2.8以後帶來的一個新的特性即支持高可用特性功能的Sentinel(哨兵)。


Redis的一些坑


Redis是一個很優秀的NoSql,它支持鍵值對,查詢方便,被大量應用在Internet的應用中,它即可以用作Http Session的分離如上一次舉例中的和Spring Session的結合,還可以直接配置在Tomcat中和Tomcat容器結合並可以自動使用Redis作Session盛載器,同時它也可以作爲一個分佈式緩存。


Redis是單線程工作的


這邊的單線程不是指它就是順序式工作的,這邊的單線程主要關注的是Redis的一個很重要的功能即“持久化”工作機制。Redis一般會使用兩種持久化工作機制,這種工作機制如果在單個Redis Node下工作是沒有意義的,因此你必須要有兩個Redis Nodes,如:

IP端口身份
192.168.56.1017001主節點
192.168.56.1017002備節點

  • RDB模式
  • AOF模式
Redis所謂的持久化就是在N個Redis節點間進行數據同步用的,因爲在複雜的網絡環境下Redis服務有時會崩潰,此時主備結構就成了高可用方案中最常用的一種手段,那麼在主機宕機時,備機頂上此時會存在一個主機和備機間數據同步的問題,最好的情況是備機可以保有主機中所有的數據以便在主機宕掉時無差異的爲客戶進行着持續化的服務。因此Redis會使用RDB和AOF模式來保持多個Redis節點間的數據同步。

RDB


RDB 的優點:

RDB 是一個非常緊湊(compact)的文件,它保存了 Redis 在某個時間點上的數據集。 這種文件非常適合用於進行備份: 比如說,你可以在最近的 24 小時內,每小時備份一次 RDB 文件,並且在每個月的每一天,也備份一個 RDB 文件。 這樣的話,即使遇上問題,也可以隨時將數據集還原到不同的版本。RDB 非常適用於災難恢復(disaster recovery):它只有一個文件,並且內容都非常緊湊,可以(在加密後)將它傳送到別的數據中心,或者亞馬遜 S3 中。RDB 可以最大化 Redis 的性能:父進程在保存 RDB 文件時唯一要做的就是 fork 出一個子進程,然後這個子進程就會處理接下來的所有保存工作,父進程無須執行任何磁盤 I/O 操作。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。

RDB 的缺點:
如果你需要儘量避免在服務器故障時丟失數據,那麼 RDB 不適合你。 雖然 Redis 允許你設置不同的保存點(save point)來控制保存 RDB 文件的頻率, 但是, 因爲RDB 文件需要保存整個數據集的狀態, 所以它並不是一個輕鬆的操作。 因此你可能會至少 5 分鐘才保存一次 RDB 文件。 在這種情況下, 一旦發生故障停機, 你就可能會丟失好幾分鐘的數據。每次保存 RDB 的時候,Redis 都要 fork() 出一個子進程,並由子進程來進行實際的持久化工作。 在數據集比較龐大時, fork() 可能會非常耗時,造成服務器在某某毫秒內停止處理客戶端; 如果數據集非常巨大,並且 CPU 時間非常緊張的話,那麼這種停止時間甚至可能會長達整整一秒。 雖然 AOF 重寫也需要進行 fork() ,但無論 AOF 重寫的執行間隔有多長,數據的耐久性都不會有任何損失。

AOF


AOF 的優點:
使用 AOF 持久化會讓 Redis 變得非常耐久(much more durable):你可以設置不同的 fsync 策略,比如無 fsync ,每秒鐘一次 fsync ,或者每次執行寫入命令時 fsync 。 AOF 的默認策略爲每秒鐘 fsync 一次,在這種配置下,Redis 仍然可以保持良好的性能,並且就算髮生故障停機,也最多隻會丟失一秒鐘的數據( fsync 會在後臺線程執行,所以主線程可以繼續努力地處理命令請求)。AOF 文件是一個只進行追加操作的日誌文件(append only log), 因此對 AOF 文件的寫入不需要進行 seek , 即使日誌因爲某些原因而包含了未寫入完整的命令(比如寫入時磁盤已滿,寫入中途停機,等等), redis-check-aof 工具也可以輕易地修復這種問題。
Redis 可以在 AOF 文件體積變得過大時,自動地在後臺對 AOF 進行重寫: 重寫後的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。 整個重寫操作是絕對安全的,因爲 Redis 在創建新 AOF 文件的過程中,會繼續將命令追加到現有的 AOF 文件裏面,即使重寫過程中發生停機,現有的 AOF 文件也不會丟失。 而一旦新 AOF 文件創建完畢,Redis 就會從舊 AOF 文件切換到新 AOF 文件,並開始對新 AOF 文件進行追加操作。AOF 文件有序地保存了對數據庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕鬆。 導出(export) AOF 文件也非常簡單: 舉個例子, 如果你不小心執行了 FLUSHALL 命令, 但只要 AOF 文件未被重寫, 那麼只要停止服務器, 移除 AOF 文件末尾的 FLUSHALL 命令, 並重啓 Redis , 就可以將數據集恢復到 FLUSHALL 執行之前的狀態。

AOF 的缺點:
對於相同的數據集來說,AOF 文件的體積通常要大於 RDB 文件的體積。根據所使用的 fsync 策略,AOF 的速度可能會慢於 RDB 。 在一般情況下, 每秒 fsync 的性能依然非常高, 而關閉 fsync 可以讓 AOF 的速度和 RDB 一樣快, 即使在高負荷之下也是如此。 不過在處理巨大的寫入載入時,RDB 可以提供更有保證的最大延遲時間(latency)。AOF 在過去曾經發生過這樣的 bug : 因爲個別命令的原因,導致 AOF 文件在重新載入時,無法將數據集恢復成保存時的原樣。 (舉個例子,阻塞命令 BRPOPLPUSH 就曾經引起過這樣的 bug 。) 測試套件裏爲這種情況添加了測試: 它們會自動生成隨機的、複雜的數據集, 並通過重新載入這些數據來確保一切正常。 雖然這種 bug 在 AOF 文件中並不常見, 但是對比來說, RDB 幾乎是不可能出現這種 bug 的。


RDB 和 AOF間的選擇


一般來說,如果想達到足以媲美 PostgreSQL 的數據安全性, 你應該同時使用兩種持久化功能。如果你非常關心你的數據,但仍然可以承受數分鐘以內的數據丟失, 那麼你可以只使用 RDB 持久化。有很多用戶都只使用 AOF 持久化, 但我們並不推薦這種方式: 因爲定時生成 RDB 快照(snapshot)非常便於進行數據庫備份, 並且 RDB 恢復數據集的速度也要比 AOF 恢復的速度要快, 除此之外, 使用 RDB 還可以避免之前提到的 AOF 程序的 bug 。

如我在上篇中所述,RDB與AOF可以同時啓用,那麼就做到了“數據高同步不丟失”的效果,可是你會應此付出高昂的網絡IO開銷,因爲在使用AOF進行數據同步時造成的網絡讀寫也是開銷很大的。

因此這就要看你的設計了,一般來說一個設計不能夠因爲緩衝服務宕了或者不可用了而影響到整個應用不能使用,如果設計成這樣那麼這樣的架構是比較糟糕的。

舉一例來說:
用戶登錄後正在操作,他正在查詢着訂單,其中有一部分數據來自於緩存,這時緩存死了,用戶的查詢行爲此時應該被導向至DB而等緩存被恢復後才應該重新去查緩存,當然如果在緩存中找不到相關的信息自然還是應該去找數據庫,對不對?
因此你的僞代碼應該是:

object=queryFromCache();

if(object==null||queryFromCache throw any exception)
{
  object=queryFromDB();
}

如果因爲緩存服務不存在而在queryFromCache時拋錯一個exception以致於頁面直接回一個HTTP 500 error給用戶那是相當的不合理的。

因此,在緩存服務停止時你的DB在緩存被恢復前是需要頂出去的,如果說你的DB連這點時間都不能頂,那就需要好好的來優化你的DB內的操作了。

再舉一例來說:
在一主一備的緩存環境下,用戶正在訪問,此時主緩存服務器宕機了,備用機頂上去了,而此時當中有7-10分鐘左右數據丟失,這樣的數據丟失不應該影響用戶的連續性操作即用戶不應該感覺到有服務切換的這種感覺比如說:要求 用戶重新做一個什麼操作,這種設計是不對的,因此你的設計上要有冗餘,比如說用戶正在操作時此時一切都在master nodes中,而此時master nodes突然崩潰了你的slave nodes頂上時在設計上你要允許用戶可以丟失緩存中的數據這樣的形爲,比如說可以用DB頂上,或者是用二級緩存,或者是用內存數據庫。。。bla...bla...bla...這裏具體就要看業務了。

上面說了這些,並不是說我們就可以因此不追求數據同步完整性了,而是要回到Redis這個坑,即數據同步時的一個坑。

Redis它是單線程的,當master和slave在正常工作時everything ok,它會保持着兩個節點中數據基本的同步,如果你開了AOF那你的同步率會很高。

但是,一旦當master宕機時,slave會變成master,這時它會使用它本身所在目錄內的RDB文件來作爲持久化的入口,此時還是everything ok,接着那臺原先宕機的舊master重新被恢復後。。。此時這臺舊的master上的RDB文件和從slave位置被提升成master(new master)間的RDB文件的出入,是不是就會比較高啊。。。因此此時old master會試圖和新的master進行RDB間的數據同步,而這個同步。。。是非常要命的,如果你的用戶併發量很大,在一瞬時內你的rdb增長的會非常高,因此當兩個redis nodes在同步RDB文件時就會直接把你的現在的new master(原來的slave)搞死進而搞死你的old master(原來的master),因爲它是單線程的,大數據量在同步時它會ban掉任何的訪問請求。

因此,在設有master & slave模式環境內的redis,請一定記得把配置文件中的這一行:slave-serve-stale-data 設置爲yes

來看看slave-server-stale-data爲什麼要設成yes的原因吧:


1) 如果 slave-serve-stale-data 設置成 'yes' (the default) slave會仍然響應客戶端請求,此時可能會有問題。

2) 如果 slave-serve-stale data設置成  'no'  slave會返回"SYNC with master in progress"這樣的錯誤信息。 但 INFO 和SLAVEOF命令除外。

想一下,當master-slave節點在因爲master節點有問題做切換時,此時不管是因爲slave在被提升(promopted)到master時需要同步數據還是因爲原有的master在宕機後再恢復而被decreased成了slave而同步new master數據時造成的“阻塞”,如果此時slave-server-stale-data設成了no。。。那麼你將會沒有一個可用的redis節點進而把整個環境搞死。

因此這也是爲什麼我上面要說設計上不能過多依賴於Redis的原因,它只因該是你一個錦上添花的東西,是一個輔助手段。


THP(Transparent Huge Pages)


這也是Redis的一個坑,來看看什麼是THP吧,Transparent Huge Pages。

Redis是安裝在Linux上的一個服務

Linux本身的頁大小是固定的4KB,在2.6.38內核新增了THP,透明地支持huge page(2MB)的使用,並且默認開啓。
  • 開啓THP的優勢在於:
  1.  減少page fault。一次page fault可以加載更大的內存塊.。
  2. 更小的頁表。相同的內存大小,需要更少的頁。
  3. 由於頁表更小,虛擬地址到物理地址的翻譯也更快。    
  • 劣勢在於:
  1. 降低分配內存效率。需要大塊、連續內存塊,內核線程會比較激進的進行compaction,解決內存碎片,加劇鎖爭用。
  2. 降低IO吞吐。由於swapable huge page,在swap時需要切分成原有的4K的頁。Oracle的測試數據顯示會降低30%的IO吞吐。

  • 對於redis而言,開啓THP的優勢:fork子進程的時間大幅減少。fork進程的主要開銷是拷貝頁表、fd列表等進程數據結構。由於頁表大幅較小(2MB / 4KB = 512倍),fork的耗時也會大幅減少。   

  • 劣勢在於: fork之後,父子進程間以copy-on-write方式共享地址空間。如果父進程有大量寫操作,並且不具有locality,會有大量的頁被寫,並需要拷貝。同時,由於開啓THP,每個頁2MB,會大幅增加內存拷貝。

針對這個特性,我做了一個測試,分別在開啓和關閉THP的情況下,測試redis的fork、響應時間。    
測試條件:redis數據集大小20G, rdb文件大小4.2G        我用jmeter做了100個併發乘1萬的壓力測試,測試過程中寫要比讀頻繁。

  • fork時間對比  開啓THP後,fork大幅減少。
  • 超時次數對比 開啓THP後,超時次數明顯增多,但是每次超時時間較短。而關閉THP後,只有4次超時,原因是與fork在同一事件循環的請求受到fork的影響。  關閉THP影響的只是零星幾個請求,而開啓後,雖然超時時間短了,但是影響面擴大了進而導致了整個Linux系統的不穩定。
因此,針對上述情況,建議大家在Linux系統中發一條這個命令:

echo never > /sys/kernel/mm/transparent_hugepage/enabled


Redis的maxmemory 0的問題


Redis配置文件中的這一行代表Redis會使用系統內存,你不該去限制Redis的內存開銷如:JVM中的-xmx這個參數,而是要讓Redis自動去使用系統的內存以獲得最高的性能,因此我們會把這個值設成0即代表無限使用系統內存,系統內存有多少我們用多少。默認它啓動後會消耗掉1個G的系統自有內存。


因此linux系統中有一個系統參數叫overcommit_memory,它代表的是內存分配策略,可選值爲:0、1、2。

0, 表示內核將檢查是否有足夠的可用內存供應用進程使用;如果有足夠的可用內存,內存申請允許;否則,內存申請失敗,並把錯誤返回給應用進程。
1, 表示內核允許分配所有的物理內存,而不管當前的內存狀態如何。
2, 表示內核允許分配超過所有物理內存和交換空間總和的內存

所以我們結合我們的Redis使用以下的linux命令:

echo 1 > /proc/sys/vm/overcommit_memory

上述兩條命令發完後不要完了刷新系統內存策略,因此我們接着發出一條命令

sysctl -p


Redis在Linux系統中Too many open files的問題

有時位於系統訪問高峯時間段突發的大量請求導致redis連接數過大,你會收到這樣的錯誤信息:

Too many open files.

這是因爲頻繁訪問Redis時造成了TCP連接數打開過大的主要原因, 這是因爲Redis源碼中在accept tcp socket時的實現裏面遇到句柄數不夠的處理方法爲:留在下次處理,而不是斷開TCP連接。

但這一行爲就會導致監聽套接字不斷有可讀消息,但卻accept無法接受,從而listen的backlog被塞滿;從而導致後面的連接被RST了。

這裏我多囉嗦一下也就是Redis和Memcached的比較,memcached對於這種情況的處理有點特殊,或者說周到!


如果memcache accept 的時候返回EMFILE,那麼它會立即調用listen(sfd, 0) , 也就是將監聽套接字的等待accept隊列的backlog設置爲0,從而拒絕掉這部分請求,減輕系統負載,保全自我。


因此爲了對付這個too many open files問題我們需要在Linux下做點小動作來改變ulimit的配置。


  • 修改/etc/security/limits.conf


通過 vi /etc/security/limits.conf修改其內容,在文件最後加入(數值也可以自己定義):

* soft  nofile = 65535
* hard  nofile = 65535

  • 修改/etc/profile

通過vi /etc/profile修改,在最後加入以下內容

ulimit -n 65535


修改完後重啓Linux系統。


通過上述一些設置,我們基本完成了Redis在做集羣前的準備工作了,下面就來使用Redis的Sentinel來做我們的高可用方案吧。


使用Redis Sentinel來做HA


sentinel是一個管理redis實例的工具,它可以實現對redis的監控、通知、自動故障轉移。sentinel不斷的檢測redis實例是否可以正常工作,通過API向其他程序報告redis的狀態,如果redis master不能工作,則會自動啓動故障轉移進程,將其中的一個slave提升爲master,其他的slave重新設置新的master服務器。
sentinel是一個分佈式系統,在源碼包的src目錄下會有redis-sentinel命令,你甚至還可以在多臺機器上部署sentinel進程,共同監控redis實例。

  1. 一個Master可以有多個Slave;
  2. Redis使用異步複製。從2.8開始,Slave會週期性(每秒一次)發起一個Ack確認複製流(replication stream)被處理進度;
  3. 不僅主服務器可以有從服務器, 從服務器也可以有自己的從服務器, 多個從服務器之間可以構成一個圖狀結構;
  4. 複製在Master端是非阻塞模式的,這意味着即便是多個Slave執行首次同步時,Master依然可以提供查詢服務;
  5. 複製在Slave端也是非阻塞模式的:如果你在redis.conf做了設置,Slave在執行首次同步的時候仍可以使用舊數據集提供查詢;你也可以配置爲當Master與Slave失去聯繫時,讓Slave返回客戶端一個錯誤提示;
  6. 當Slave要刪掉舊的數據集,並重新加載新版數據時,Slave會阻塞連接請求(一般發生在與Master斷開重連後的恢復階段);
  7. 複製功能可以單純地用於數據冗餘(data redundancy),也可以通過讓多個從服務器處理只讀命令請求來提升擴展性(scalability): 比如說, 繁重的 SORT 命令可以交給附屬節點去運行。
  8. 可以通過修改Master端的redis.config來避免在Master端執行持久化操作(Save),由Slave端來執行持久化。

Redis Sentinel規劃


考慮到大多數學習者環境有限,我們使用如下配置:

IP端口身份
192.168.56.1017001master
192.168.56.1017002slave
192.168.56.10126379sentinel

所以我們在一臺服務器上安裝3個目錄:

  • redis1-對應master
  • redis2-對應slave
  • redis-sentinel對應sentinel,它使用26379這個端口來監控master和slave

因此我們使用redis-stable源碼包來如此構建我們的實驗環境

make PREFIX=/usr/local/redis1 install
make PREFIX=/usr/local/redis2 install
make PREFIX=/usr/local/redis-sentinel install


下面給出sentinel的配置


Sentinel中的配置


更改/usr/local/redis-sentinel/bin/sentinel.conf文件:

port 26379
daemonize yes
logfile "/var/log/redis/sentinel.log"
sentinel monitor master1 192.168.56.101 7001 1
sentinel down-after-milliseconds master1 1000
sentinel failover-timeout master1 5000
#sentinel can-failover master1 yes #remove from 2.8 and aboved version

  • daemonize yes – 以後臺進程模式運行
  • port 26379 – 哨兵的端口號,該端口號默認爲26379,不得與任何redis node的端口號重複
  • logfile “/var/log/redis/sentinel.log“ – log文件所在地
  • sentinel monitor master1 192.168.56.101 7001 1 – (第一次配置時)哨兵對哪個master進行監測,此處的master1爲一“別名”可以任意如sentinel-26379,然後哨兵會通過這個別名後的IP知道整個該master內的slave關係。因此你不用在此配置slave是什麼而由哨兵自己去維護這個“鏈表”
  • sentinel monitor master1 192.168.56.101 7001 1 –  這邊有一個“1”,這個“1”代表當新master產生時,同時進行“slaveof”到新master並進行同步複製的slave個數。在salve執行salveof與同步時,將會終止客戶端請求。此值較大,意味着“集羣”終止客戶端請求的時間總和和較大。此值較小,意味着“集羣”在故障轉移期間,多個salve向客戶端提供服務時仍然使用舊數據。我們這邊只想讓一個slave來做此時的響應以取得較好的客戶端體驗。
  • sentinel down-after-milliseconds master1 1000 – 如果master在多少秒內無反應哨兵會開始進行master-slave間的切換,使用“選舉”機制
  • sentinel failover-timeout master1 5000 – 如果在多少秒內沒有把宕掉的那臺master恢復,那哨兵認爲這是一次真正的宕機,而排除該宕掉的master作爲節點選取時可用的node然後等待一定的設定值的毫秒數後再來探測該節點是否恢復,如果恢復就把它作爲一臺slave加入哨兵監測節點羣並在下一次切換時爲他分配一個“選取號”。
  • #sentinel can-failover master1 yes #remove from 2.8 and aboved version – 該功能已經從2.6版以後去除,因此註釋掉,網上的教程不適合於redis-stable版

在配置Redis Sentinel做Redis的HA場景時,一定要注意下面幾個點:

  • 除非有多機房HA場景的存在,堅持使用單向鏈接式的master->slave的配置如:node3->node2->node1,把node1設爲master
  • 如果sentinel(哨兵)或者是HA羣重啓,一定要使用如此順序:先啓master,再啓slave,再啓哨兵
  • 第一次配置完成“哨兵”HA羣時每次啓動不需要手動再去每個redis node中去更改master slave這些參數了,哨兵會在第一次啓動後記錄和動態修改每個節點間的關係,第一次配置好啓動“哨兵”後由哨兵以後自行維護一般情況下不需要人爲干涉,如果切換過一次master/slave後也因該記得永遠先起master再起slave再起哨兵這個順序,具體當前哪個是master可以直接看哨兵的sentinel.conf文件中最末尾哨兵自行的記錄

Redis Master和Redis Slave的配置


這部分配置除了端口號,所在目錄,pid文件與log文件不同其它配置相同,因此下面只給出一份配置:

daemonize yes

pidfile "/var/run/redis/redis1.pid"

port 7001

tcp-backlog 511
timeout 0

tcp-keepalive 0

loglevel notice

logfile "/var/log/redis/redis1.log"

databases 16


save 900 1
save 300 10
save 60 10000

stop-writes-on-bgsave-error no
rdbcompression yes
rdbchecksum yes
dbfilename "dump.rdb"
dir "/usr/local/redis1/data"

slave-serve-stale-data yes
slave-read-only yes #slave只讀,當你的應用程序試圖向一個slave寫數據時你會得到一個錯誤

repl-diskless-sync no

repl-disable-tcp-nodelay no

slave-priority 100

maxmemory 0


appendonly no

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof"


# appendfsync always
#appendfsync everysec
appendfsync no #關閉AOF


no-appendfsync-on-rewrite yes


auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

aof-load-truncated yes

lua-time-limit 5000


slowlog-log-slower-than 10000


slowlog-max-len 128

latency-monitor-threshold 0

notify-keyspace-events "gxE"

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

list-max-ziplist-entries 512
list-max-ziplist-value 64

set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

hz 10


其中:

  • slave-read-only yes 我們把slave設成只讀,當你的應用程序試圖向一個slave寫數據時你會得到一個錯誤
  • appendfsync no 我們關閉了AOF功能
這是192.168.56.101:7001master上的配置,你要把192.168.56.101:7002作爲slave,那很簡單,你只需要在redis2的配置文件的最未尾加入一句:

slaveof 192.168.56.101 7001

配完了master, slave和sentinel後,我們按照這個順序來啓動redis HA:

master->slave->sentinel

啓動後我們通過windows客戶端使用命令:

redis-cli -p 26379 -h 192.168.56.101
進入我們配置好的sentinel後並使用: info命令來查看我們的redis sentinel HA配置。
可以看到目前它的master爲7001,它有一個slave。
爲了確認,我們另外開一個command窗口,通過:
redis-cli -p 7001 -h 192.168.56.101

進入到7001後再使用redis內部命令info replication來查看相關信息



我們還可以通過命令:

redis-cli -h 192.168.56.101 -p 7002

進入到7002中並通過info replication來查看7002內的情況:



好了,環境有了,我們接下來要使用:
  • 模擬代碼
  • 模擬併發測試工具
來測一下我們這個redis sentinel了的自動故障轉移功能了。


使用 Spring Data + Jedis來訪問我們的Redis Sentinel


pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>webpoc</groupId>
	<artifactId>webpoc</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<jetty.version>9.3.3.v20150827</jetty.version>
		<slf4j.version>1.7.7</slf4j.version>
		<spring.version>4.2.1.RELEASE</spring.version>
		<spring.session.version>1.0.2.RELEASE</spring.session.version>
		<javax.servlet-api.version>2.5</javax.servlet-api.version>
		<activemq_version>5.8.0</activemq_version>
		<poi_version>3.8</poi_version>
	</properties>
	<dependencies>
		<!-- poi start -->
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi</artifactId>
			<version>${poi_version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi-ooxml-schemas</artifactId>
			<version>${poi_version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi-scratchpad</artifactId>
			<version>${poi_version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi-ooxml</artifactId>
			<version>${poi_version}</version>
		</dependency>
		<!-- poi end -->
		<!-- active mq start -->
		<dependency>
			<groupId>org.apache.activemq</groupId>
			<artifactId>activemq-all</artifactId>
			<version>5.8.0</version>
		</dependency>

		<dependency>
			<groupId>org.apache.activemq</groupId>
			<artifactId>activemq-pool</artifactId>
			<version>${activemq_version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.xbean</groupId>
			<artifactId>xbean-spring</artifactId>
			<version>3.16</version>
		</dependency>
		<!-- active mq end -->

		<!-- servlet start -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>${javax.servlet-api.version}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>
		<!-- servlet end -->

		<!-- redis start -->
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.7.2</version>
		</dependency>
		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>1.0.2</version>
		</dependency>
		<!-- redis end -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jcl-over-slf4j</artifactId>
			<version>${slf4j.version}</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>${slf4j.version}</version>
		</dependency>

		<!-- spring conf start -->
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
			<version>1.6.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
			<exclusions>
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context-support</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jms</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session</artifactId>
			<version>${spring.session.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<!-- spring conf end -->
	</dependencies>
	<build>
		<sourceDirectory>src</sourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-war-plugin</artifactId>
				<version>2.4</version>
				<configuration>
					<warSourceDirectory>WebContent</warSourceDirectory>
					<failOnMissingWebXml>false</failOnMissingWebXml>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

applicationContext.xml文件


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="  
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<context:property-placeholder location="classpath:/spring/redis.properties" />
	<context:component-scan base-package="org.sky.redis">
	</context:component-scan>

	<bean id="jedisConnectionFactory"
		class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<constructor-arg index="0" ref="redisSentinelConfiguration" />
		<constructor-arg index="1" ref="jedisPoolConfig" />
	</bean>

	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxTotal" value="${redis.maxTotal}" />
		<property name="maxIdle" value="${redis.maxIdle}" />
		<property name="maxWaitMillis" value="${redis.maxWait}" />
		<property name="testOnBorrow" value="${redis.testOnBorrow}" />
		<property name="testOnReturn" value="${redis.testOnReturn}" />
	</bean>
	<bean id="redisSentinelConfiguration"
		class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
		<property name="master">
			<bean class="org.springframework.data.redis.connection.RedisNode">
				<property name="name" value="master1" />
			</bean>
		</property>
		<property name="sentinels">
			<set>
				<bean class="org.springframework.data.redis.connection.RedisNode">
					<constructor-arg name="host" value="192.168.56.101" />
					<constructor-arg name="port" value="26379" />
				</bean>
			</set>
		</property>
	</bean>

	<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
		<property name="connectionFactory" ref="jedisConnectionFactory" />
	</bean>

	<!--將session放入redis -->
	<bean id="redisHttpSessionConfiguration"
		class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
		<property name="maxInactiveIntervalInSeconds" value="1800" />
	</bean>
	<bean id="customExceptionHandler" class="sample.MyHandlerExceptionResolver" />
</beans> 

其中:
<property name="master">
			<bean class="org.springframework.data.redis.connection.RedisNode">
				<property name="name" value="master1" />
			</bean>
</property>

此處的master1需要與sentinel中的名字一致:

sentinel down-after-milliseconds master1 1000

redis.properties文件


# Redis settings

redis.host.ip=192.168.56.101
redis.host.port=7001
  

redis.maxTotal=1000  
redis.maxIdle=100
redis.maxWait=2000
redis.testOnBorrow=false
redis.testOnReturn=true

redis.sentinel.addr=192.168.56.101:26379


SentinelController.java文件


package sample;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import util.CountCreater;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * Created by xin on 15/1/7.
 */
@Controller
public class SentinelController {
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	@Autowired
	private StringRedisTemplate redisTemplate;

	@RequestMapping("/sentinelTest")
	public String sentinelTest(final Model model,
			final HttpServletRequest request, final String action) {
		return "sentinelTest";
	}

	@ExceptionHandler(value = { java.lang.Exception.class })
	@RequestMapping("/setValueToRedis")
	public String setValueToRedis(final Model model,
			final HttpServletRequest request, final String action)
			throws Exception {
		CountCreater.setCount();
		String key = String.valueOf(CountCreater.getCount());
		Map mapValue = new HashMap();
		for (int i = 0; i < 1000; i++) {
			mapValue.put(String.valueOf(i), String.valueOf(i));
		}
		try {
			BoundHashOperations<String, String, String> boundHashOperations = redisTemplate
					.boundHashOps(key);
			boundHashOperations.putAll(mapValue);
			logger.info("put key into redis");
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new Exception(e);
		}

		return "sentinelTest";
	}

}

這個controller如果返回success會跳轉到一個叫/page/sentinelTest.jsp文件中,它的內容如下:

sentinelTest.jsp文件


<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; UTF-8">
<title>test sentinel r/w</title>
</head>
<body>

</body>
</html>

這個jsp文件的<title>內含有 test sentinel r/w字樣,我們在後面的jmeter壓力測試中就會用一個assertion(斷言)來判斷此字樣以示SentinelController成功跳轉,如果在jmeter的assertion中沒有讀到controller跳轉後的response中有此字樣,那麼這個請求即爲failed。


測試代碼運行



把工程編譯後啓動起來,啓動前不要忘了按照master->slave->sentinel的順序來啓動redis的sentinel HA羣。


啓動後我們訪問

http://localhost:8080/webpoc/setValueToRedis

看到我們的eclipse控制檯有這一行輸出,代表我們的sentinel羣和代碼已經完美合起來了。

於是我們分別登錄master和slave來查看我們剛纔插的值


可以看到,在master被插入值後,slave從master處同步了相應的值過來。

測試master不可訪問時sentinel的自動切換


我們現在的情況爲:

  • master-7001
  • slave-7002
於是,我們在Linux中發出一條命令



這條命令代表封閉192.168.56.101上的7001端口到達任何地方的路由,即人爲造成了一次master服務的“宕機”。

先來看eclipse中控制檯的情況:



看到沒有。。。jedispool to master at 192.168.56.101:7002,7002已經變成master了。

再來看我們的redis,看。。。7001上的服務不可用已經被我們位於26379端口的哨兵探測到了,它已經把7002變成master了。



不信,我們再登錄7002來看一下我們的服務



那麼我們可以確認,從7001宕機後7002已經從slave變成了master。

於是,我們在linux端打開sentinel.conf文件看一下,它已經發生了變化,這個變化是sentinel自己自動往配置文件里加入的內容:



 
現在,我們把7001重新“恢復”起來,因此我們發出如下的命令:



此刻我們再來登錄7001來看看它變成什麼狀態了:


看。。。7001恢復後從原來的old master成了new slave了。

那麼sentinel到底做了什麼,我們來看看sentinel的log日誌一探究竟吧:



我們再來看看redis1中的redis.conf文件中的內容:


再來看看redis2中的redis.conf文件



以上實驗成功,我們下面就用jmeter來進行大併發用戶操作下的sentinel切換吧。

使用jmter模擬大併發用戶操作下的故障自動轉移


壓力測試計劃











在我的jmeter測試計劃中我增加了4個監聽器,它們分別爲:
  • TPS值
  • summary report
  • 表格查看結果
  • 樹形查看結果

我們現在就來去選這個以 ”100個併發以每秒一次的請求來點擊這個SentinelController並永遠點擊下去“ 的壓力測試吧,在測試時我們會有意將master搞宕。

點擊菜單中的”運行->啓動“, 不一會我們可以看到jmeter中的TPS與Summary Report中的數字開始飛速轉動了起來。





人爲故意造成一次宕機


我們現在的master爲7002, 7001爲slave,因此,我們就把7002搞“崩”吧。



再看來jmeter中的TPS顯示:





  • 通過TPS我們可以發覺有藍色的線,這代表“出錯率”,這個出錯率應該是7002在“崩”掉後,7001從slave升級成master時redis對客戶端無法及時響應時拋出的HTTP 500即service unavailable的錯。

  • 通過Summary Report我們可以看到在主從切換的那一刻我們的fail rate爲千分之0.5,這個fail rate是完全可以在接受範圍內的,一般錯誤率在千分之一就已經很好了。

可以看到我們在搞“死”7002時,7001自動頂到了master的位置並及時響應了用戶的請求,要知道我們這個測試只是在一臺4GB的虛擬出來的Linux Fedora22上進行的redis 一主一備的sentinel測試,能夠達到這個測試結果已經是相當的perfect了。

於是我們登錄一下7001來看看



我們可以看到,7001成了master了。

於是我們”恢復“7002。





它便自己成了slave了,而此時7001作爲new master正在承擔着客戶端的訪問。

結束今天的教程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章