前情提要:本篇是 Redis 哨兵模式 (Sentinel) 搭配 Docker-compose 實作 的後續文章,主要會優化原本的程式碼,並加上 HAProxy 來做負載平衡!
HAProxy 是一個使用 C 語言編寫的自由及開放原始碼軟體,其提供高可用性、負載均衡,以及基於 TCP 和 HTTP 的應用程式代理。
負載平衡 (Load Balance):
現在很多網路服務都需要服務大量使用者,以前可以砸錢擴充機器硬體設施,但隨著網路服務的用量暴增,增加伺服器硬體設備已經無法解決問題。
為了可以擴充服務,負載平衡成為主流的技術,這幾年雖然雲端與分散式儲存運算技術火紅,除非有特別的使用需求,不然在技術上負載均衡算是比較容易達成與掌握的技術。
負載平衡除了分流能力之外,有另一個很大的好處就是可以提供 High Availability,也就是傳說中的 HA 架構,好讓你一台機器掛了其他伺服器可以繼續服務,降低斷線率。
(來源:HAProxy 與 Reids Sentinel 示意圖 selcukusta/redis-sentinel-with-haproxy)
本文章是使用 Docker-compose 實作 Redis 哨兵模式 + HAProxy,建議可以先觀看 用 HAProxy 對 Redis 做負載平衡 (Redis Sentinel、Docker-compose) 文章來簡單學習 HAProxy。
版本資訊
- macOS:11.6
- Docker:Docker version 20.10.12, build e91ed57
- Nginx:1.20
- PHP:7.4-fpm
- Redis:6.2.6
- HAProxy:HAProxy version 2.5.5-384c5c5 2022/03/14 - https://haproxy.org/
.
├── Docker-compose.yaml
├── docker-volume
│ ├── haproxy
│ │ └── haproxy.cfg
│ ├── nginx
│ │ └── nginx.conf
│ ├── php
│ │ ├── info.php
│ │ ├── r.php
│ │ └── rw.php
│ └── redis
│ ├── redis.conf
│ ├── redis1
│ ├── redis2
│ └── redis3
├── php
│ └── Dockerfile
├── redis.sh
└── sentinel
├── Docker-compose.yaml
├── sentinel1
│ └── sentinel.conf
├── sentinel2
│ └── sentinel.conf
└── sentinel3
└── sentinel.conf
這是主要的結構,簡單說明一下:
- Docker-compose.yaml:會放置要產生的 Nginx、PHP、redis1、redis2、redis3 容器設定檔。
- docker-volume/haproxy/haproxy.cfg:haproxy 的設定檔。
- docker-volume/nginx/nginx.conf:nginx 的設定檔。
- docker-volume/php/(r.php、rw.php):測試用檔案。
- docker-volume/redis/redis.conf:redis 的設定檔。
- docker-volume/redis/(redis1、redis2、redis3):放 redis 的資料。
- php/Dokcerfile:因為在 php 要使用 redis 需要多安裝一些設定,所以用 Dockerfile 另外寫 PHP 的映像檔。
- redis.sh:是我另外多寫的腳本,可以查看相對應的角色。
- sentinel/Docker-compose.yaml:會放置要產生的 haproxy、sentinel1、sentinel2、sentinel3 的容器設定檔。
- sentinel/(sentinel1、sentinel2、sentinel3)/.conf:哨兵的設定檔。
那我們就依照安裝的設定開始說明:
version: '3.8'
services:
nginx:
image: nginx:1.20
container_name: nginx
networks:
HAProxy_Redis:
ports:
- "8888:80"
volumes:
- ./docker-volume/nginx/:/etc/nginx/conf.d/
- ./log/nginx/:/var/log/nginx/
environment:
- TZ=Asia/Taipei
php:
build: ./php
container_name: php
networks:
HAProxy_Redis:
expose:
- 9000
volumes:
- ./docker-volume/php/:/var/www/html
redis1:
image: redis
container_name: redis1
command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes
volumes:
- ./docker-volume/redis/redis1/:/data
- ./docker-volume/redis/:/usr/local/etc/redis/
- ./log/redis1:/var/log/redis/
environment:
- TZ=Asia/Taipei
networks:
HAProxy_Redis:
ipv4_address: 172.20.0.11
ports:
- 6379:6379
redis2:
image: redis
container_name: redis2
command: redis-server /usr/local/etc/redis/redis.conf --slaveof redis1 6379 --appendonly yes
volumes:
- ./docker-volume/redis/redis2/:/data
- ./docker-volume/redis/:/usr/local/etc/redis/
- ./log/redis2:/var/log/redis/
environment:
- TZ=Asia/Taipei
networks:
HAProxy_Redis:
ipv4_address: 172.20.0.12
ports:
- 6380:6379
depends_on:
- redis1
redis3:
image: redis
container_name: redis3
command: redis-server /usr/local/etc/redis/redis.conf --slaveof redis1 6379 --appendonly yes
volumes:
- ./docker-volume/redis/redis3/:/data
- ./docker-volume/redis/:/usr/local/etc/redis/
- ./log/redis3:/var/log/redis/
environment:
- TZ=Asia/Taipei
networks:
HAProxy_Redis:
ipv4_address: 172.20.0.13
ports:
- 6381:6379
depends_on:
- redis1
- redis2
networks:
HAProxy_Redis:
driver: bridge
name: HAProxy_Redis
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
一樣詳細的 Docker 設定說明,可以參考 Docker 介紹 內有詳細設定說明。其他比較特別的地方是:
- 幫每一個容器都設定好 IP ,方便後續測試使用。
- 有掛載 log 目錄,可以將我們設定好的 log 做收集。
- 呈上,有加入 environment 時區,這樣在看 log 的時候才知道正確時間。
global
log stdout format raw local0 info
defaults
mode http # 默認模式 { tcp | http | health },tcp 是4層,http 是7層,health 只會返回 OK
timeout client 10s # 客戶端超時
timeout connect 5s # 連接超時
timeout server 10s # 伺服器超時
timeout http-request 10s
log global
listen admin_status
bind 0.0.0.0:8404
mode http
stats enable
stats uri /redis
stats realm Global\ statistics
stats refresh 1s
listen rw-redis # 判斷是否為 master 並可讀可寫
bind 0.0.0.0:16379
mode tcp
balance roundrobin
option tcp-check # redis 健康检查,確保是 master
tcp-check connect
tcp-check send PING\r\n
tcp-check expect string +PONG
tcp-check send info\ replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
server redis1 redis1:6379 check inter 2000
server redis2 redis2:6379 check inter 2000
server redis3 redis3:6379 check inter 2000
listen r-redis # 判斷是否為 master、slave 並可讀
bind 0.0.0.0:16380
mode tcp
balance roundrobin
server redis1 redis1:6379 check inter 2000
server redis2 redis2:6379 check inter 2000
server redis3 redis3:6379 check inter 2000
這裡是本章的重點,我們會在這邊設定好 haproxy,詳細說明請看:
defaults: 一些初始值,像是 mode 我們預設 http,它主要有三種模式 { tcp | http | health },tcp 是4層,http 是7層,health 只會返回 OK,以及客戶端、連接、伺服器、http 請求超時時間設定。
listen admin_status:
- bind:我們要開啟 HAProxy 監控平台的 port。
- mode:模式,我們使用 http 模式。
- stats :是否要啟動平台。
- stats uri:平台網址,我們使用 redis。
- stats refresh:平台自動更新時間,我們設定 1 秒。
listen rw-redis:
- bind :rw 使用 16379 Port 來當輸出。
- balance:使用負載平衡。
option tcp-check # redis 健康检查,確保是 master
tcp-check connect
tcp-check send PING\r\n
tcp-check expect string +PONG
tcp-check send info\ replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
上面這些是用來判斷角色是不是 master。
最後放我們 3 個 redis 服務:
server redis1 redis1:6379 check inter 2000
server redis2 redis2:6379 check inter 2000
server redis3 redis3:6379 check inter 2000
- check:開啟健康偵測。
- inter:參數更改檢查間隔,預設是 2 秒。
server {
listen 80;
server_name default_server;
return 404;
}
server {
listen 80;
server_name test.com;
index index.php index.html;
error_log /var/log/nginx/error.log warn;
access_log /var/log/nginx/access.log;
root /var/www/html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
}
}
Nginx 設定檔案。
<?php
$redis = new Redis();
$redis->connect('172.20.0.20', 16379);
$r = $redis->info();
echo $r['run_id'] . '<br>' . $r['role'] . '<br><br>';
echo '<pre>', print_r($r), '</pre>';
跟 r.php
比較不同的是,使用 16379 Port,我們在 haproxy.cfg
有設定 rw-redis,來判斷是不是 master 並且是可讀可寫。
<?php
$redis = new Redis();
$redis->connect('172.20.0.20', 16380);
$r = $redis->info();
echo $r['run_id'] . '<br>' . $r['role'] . '<br><br>';
echo '<pre>', print_r($r), '</pre>';
使用 16380 Port,在 haproxy.cfg
有設定 r-redis,來顯示是不是 master、slave 且可讀。
FROM php:7.4-fpm
RUN pecl install -o -f redis \
&& rm -rf /tmp/pear \
&& echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini \
&& echo "session.save_handler = redis" >> /usr/local/etc/php/conf.d/redis.ini \
&& echo "session.save_path = tcp://redis:6379" >> /usr/local/etc/php/conf.d/redis.ini
因為 PHP 要使用 Redis,會需要安裝一些套件,所以我們將 PHP 分開來,使用 Dockerfile 來設定映像檔。
#!/bin/bash
green="\033[1;32m";white="\033[1;0m";red="\033[1;31m";
echo "redis1 IPAddress:"
redis1_ip=`docker inspect redis1 | grep "IPv4" | egrep -o "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"`
echo $redis1_ip;
echo "------------------------------"
echo "redis2 IPAddress:"
redis2_ip=`docker inspect redis2 | grep "IPv4" | egrep -o "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"`
echo $redis2_ip;
echo "------------------------------"
echo "redis3 IPAddress:"
redis3_ip=`docker inspect redis3 | grep "IPv4" | egrep -o "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"`
echo $redis3_ip;
echo "------------------------------"
echo "haproxy IPAddress:"
haproxy_ip=`docker inspect haproxy | grep "IPv4" | egrep -o "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"`
echo $haproxy_ip;
echo "------------------------------"
echo "redis1:"
docker exec -it redis1 redis-cli info Replication | grep role
echo "redis2:"
docker exec -it redis2 redis-cli info Replication | grep role
echo "redis3:"
docker exec -it redis3 redis-cli info Replication | grep role
這個是我自己所寫的腳本,可以詳細知道目前服務的角色轉移狀況。
version: '3.8'
services:
haproxy:
image: haproxy
container_name: haproxy
volumes:
- ../docker-volume/haproxy/:/usr/local/etc/haproxy
environment:
- TZ=Asia/Taipei
networks:
HAProxy_Redis:
ipv4_address: 172.20.0.20
ports:
- 16379:6379
- 8404:8404
sentinel1:
image: redis
container_name: redis-sentinel-1
networks:
HAProxy_Redis:
ports:
- 26379:26379
command: redis-server /usr/local/etc/redis/sentinel.conf --sentinel
volumes:
- ./sentinel1:/usr/local/etc/redis/
- ../log/sentinel1:/var/log/redis/
environment:
- TZ=Asia/Taipei
sentinel2:
image: redis
container_name: redis-sentinel-2
networks:
HAProxy_Redis:
ports:
- 26380:26379
command: redis-server /usr/local/etc/redis/sentinel.conf --sentinel
volumes:
- ./sentinel2:/usr/local/etc/redis/
- ../log/sentinel2:/var/log/redis/
environment:
- TZ=Asia/Taipei
sentinel3:
image: redis
container_name: redis-sentinel-3
networks:
HAProxy_Redis:
ports:
- 26381:26379
command: redis-server /usr/local/etc/redis/sentinel.conf --sentinel
volumes:
- ./sentinel3:/usr/local/etc/redis/
- ../log/sentinel3:/var/log/redis/
environment:
- TZ=Asia/Taipei
networks:
HAProxy_Redis:
external:
name: HAProxy_Redis
因為 sentine 內容都基本上相同,所以舉一個來說明:
port 26379
logfile "/var/log/redis/redis-sentinel.log"
protected-mode no
#設定要監控的 Master,最後的 2 代表判定客觀下線所需的哨兵數
sentinel monitor mymaster 172.20.0.11 6379 2
#哨兵 Ping 不到 Master 超過此毫秒數會認定主觀下線
sentinel down-after-milliseconds mymaster 5000
要設定指定的 Port sentine1 是 26379、sentine2 是 26380、sentine3 是 26381。接下來要設定要監控的 Master,最後的數字代表我們前面有提到客觀下線需要達到的哨兵數。以及主觀下線的時間跟 failover 超過的時間。
我們先用 docker-compose up 來啟動 Docker-compose.yaml,接著再啟動 sentinel/Docker-compose.yaml:
由於為了測試,有先將所有容器設定好 IP,就不會像上一篇文章一樣要去抓 IP ,才能啟動 Sentinel。
這時候可以使用瀏覽器搜尋以下網址:
test.com:8404/redis
:HAProxy 監看平台(只取片段)。
test.com:8888/rw.php
:只會顯示 master,並且可讀可寫。
test.com:8888/r.php
:會顯示 master、slave,且可讀。
接下來可以執行:
$ sh redis.sh
redis1 IPAddress:
172.20.0.11
------------------------------
redis2 IPAddress:
172.20.0.12
------------------------------
redis3 IPAddress:
172.20.0.13
------------------------------
haproxy IPAddress:
172.20.0.20
------------------------------
redis1:
role:master
redis2:
role:slave
redis3:
role:slave
就會顯示三個 redis 的 IP 以及 haproxy 的 IP,這些都是已經寫在 Docker-compose.yaml 檔案內的,如果忘記的可以再往前看 ↑
接下來我們可以先一直 F5 test.com:8888/r.php
,來模擬大量的讀取請求,如果發現網站內容一直在更換,就代表我們成功透過 HAProxy 做到負載平衡了,可以將讀取的需求分給三個服務做處理!那因為 test.com:8888/rw.php
他只會抓 master,所以刷新還是同一個 master。
還記得我們上次用 Redis 的哨兵模式嗎?那我們用它來搭配 Haproxy 會有什麼結果呢?
我們先使用 docker stop
來模擬服務中斷:
$ docker stop redis1
redis1
可以看到 test.com:8404/redis
原本綠色的 redis1 開始變成黃色,最後變成紅色:
最後可以看到 Redis Sentinel 作動,將 master 轉移到 redis3:
這時候我們再去看 test.com:8888/rw.php
,就會發現與剛剛的 master 不太一樣囉,因為已經變成 redis2 了!
代表我們 HAProxy 也有成功將 master 給顯示出來!
我們再去看 test.com:8888/r.php
,就可以發現剩下 redis2 以及 redis 3 了,因為 redis1 被我們給暫停服務了,而且 redis2 變成 master!
富人用 L4 Switch,窮人用 Linux HAProxy!