親熱接觸Redis-次日(Redis Sentinel)

簡單介紹


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


Redis的一些坑


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


Redis是單線程工做的


這邊的單線程不是指它就是順序式工做的,這邊的單線程主要關注的是Redis的一個很重要的功能即「持久化」工做機制。node

Redis一般會使用兩種持久化工做機制,這樣的工做機制假設在單個Redis Node下工做是沒有意義的,所以你必需要有兩個Redis Nodes,如:

linux

IP 端口 身份
192.168.56.101 7001 主節點
192.168.56.101 7002 備節點

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

RDB


RDB 的長處:

RDB 是一個很緊湊(compact)的文件。它保存了 Redis 在某個時間點上的數據集。

這樣的文件很適合用於進行備份: 比方說,你可以在近期的 24 小時內,每小時備份一次 RDB 文件。並且在每個月的每一天,也備份一個 RDB 文件。web

這樣的話。即便趕上問題,也可以隨時將數據集還原到不一樣的版本號。RDB 很適用於災難恢復(disaster recovery):它僅僅有一個文件,並且內容都很緊湊。可以(在加密後)將它傳送到別的數據中心,或者亞馬遜 S3 中。RDB 可以最大化 Redis 的性能:父進程在保存 RDB 文件時惟一要作的就是 fork 出一個子進程,而後這個子進程就會處理接下來的所有保存工做,父進程無須運行不論什麼磁盤 I/O 操做。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。

RDB 的缺點:
假設你需要儘可能避免在server故障時丟失數據,那麼 RDB 不適合你。 儘管 Redis 贊成你設置不一樣的保存點(save point)來控制保存 RDB 文件的頻率, 但是, 因爲RDB 文件需要保存整個數據集的狀態。 因此它並不是一個輕鬆的操做。redis

所以你可能會至少 5 分鐘才保存一次 RDB 文件。 在這樣的狀況下, 一旦發生問題停機, 你就可能會丟失好幾分鐘的數據。spring

每次保存 RDB 的時候,Redis 都要 fork() 出一個子進程,並由子進程來進行實際的持久化工做。數據庫

在數據集比較龐大時, fork() 可能會很耗時。形成server在某某毫秒內中止處理client; 假設數據集很巨大。並且 CPU 時間很緊張的話,那麼這樣的中止時間甚至可能會長達整整一秒。 儘管 AOF 重寫也需要進行 fork() ,但不管 AOF 重寫的運行間隔有多長,數據的耐久性都不會有不論什麼損失。apache


AOF


AOF 的長處:
使用 AOF 持久化會讓 Redis 變得很耐久(much more durable):你可以設置不一樣的 fsync 策略。比方無 fsync ,每秒鐘一次 fsync 。或者每次運行寫入命令時 fsync 。

AOF 的默認策略爲每秒鐘 fsync 一次,在這樣的配置下,Redis 仍然可以保持良好的性能,並且就算髮生問題停機,也最多僅僅會丟失一秒鐘的數據( fsync 會在後臺線程運行。因此主線程可以繼續努力地處理命令請求)。windows

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 文件的內容很easy被人讀懂, 對文件進行分析(parse)也很輕鬆。 導出(export) AOF 文件也很easy: 舉個樣例。 假設你不當心運行了 FLUSHALL 命令, 但僅僅要 AOF 文件未被重寫, 那麼僅僅要中止server, 移除 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內的操做了。


再舉一例來講:
在一主一備的緩存環境下。用戶正在訪問。此時主緩存server宕機了,備用機頂上去了,而此時當中有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會仍然響應client請求,此時可能會有問題。



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、一、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又一次設置新的masterserver。
sentinel是一個分佈式系統。在源代碼包的src文件夾下會有redis-sentinel命令。你甚至還可以在多臺機器上部署sentinel進程。共同監控redis實例。

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

Redis Sentinel規劃


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

IP 端口 身份
192.168.56.101 7001 master
192.168.56.101 7002 slave
192.168.56.101 26379 sentinel

因此咱們在一臺server上安裝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與同步時,將會終止client請求。此值較大。意味着「集羣」終止client請求的時間總和和較大。此值較小,意味着「集羣」在故障轉移期間,多個salve向client提供服務時仍然使用舊數據。咱們這邊僅僅想讓一個slave來作此時的響應以取得較好的client體驗。


  • 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,那很easy,你僅僅需要在redis2的配置文件的最未尾增長一句:

slaveof 192.168.56.101 7001

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

master->slave->sentinel

啓動後咱們經過windowsclient使用命令:

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對client沒法及時響應時拋出的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正在承擔着client的訪問。

結束今天的教程。
相關文章
相關標籤/搜索