0%

Redis叢集以及大使模式應用

簡介 Intro

相信在這個年代(2021)大多數的後端開發者對於分散式系統開發及加速都有不少心得

其中非常知名的快取應用,就包含了Memory Storage類型的NoSQL,比如RedisMemcached

今天寫的文章主要介紹在GCP上開發分散式系統,Redis的簡單應用

準備階段

既然是要介紹應用,那肯定是有實作的階段,要完成這個實作需要準備的東西有:

  • GCP Computer Engine 兩台Instance兩台電腦 或 你有其他手段,比如容器/VM,能產生兩個概念實體的環境

  • Ubuntu 20.04 或其他支援Redis 6.0以上版本的OS(可能Redis 5.0應該也能完成這個小測驗)

  • 基礎Shell技能

  • 基礎網路技能

  • Node.js v14.x.x以上

  • NPM 套件管理工具

  • ioredis(nodejs對redis讀寫的套件之一)


介紹

在安裝完畢所有的東西後,首先要來說明原理,知行合一是非常重要的事情。

文中會提到Redis的設定配置,設計理念以及好處…等相關內容


記憶體資料庫

首先我打算介紹Redis,它是一個記憶體資料庫,相似的產品有memcached

這兩者都屬於NoSQL也都是記憶體資料庫,這兩者取決差異在於

Redis更適合

  • 複雜的資料型態,像是 string、hash、list 和 set
  • 排序 in-memory 內的資料
  • 持久性的 key 儲存
  • 將你的資料做多個點的讀寫分離
  • 自動化錯誤修復
  • 使用/訂閱模式
  • 備份和恢復資料

Memcached更適合

  • 簡單的模型
  • 使用多執行續來執行多節點程序
  • 擴展/收縮功能
  • 根據需求增加/減少節點
  • 將資料分至不同區儲存
  • 快取物件 (圖片、音檔等等)

兩者之間的選擇需要讀者自行選擇,雖然實作手段不同,但設計理念是相似的。

以筆者目前處理的業務IoT系統平台來說,會面臨更複雜的資料型態。

且由於採用MQTT的方式進行機台與系統的資料傳遞,MQTT傳遞方式是屬於pub/sub的模式(也就是使用/訂閱模式)

因此在筆者經手的案子上,Redis更加適合。因此這次實作以Redis Cluster為主


Redis

基本上Google搜尋能力是必備的,關於Redis的介紹隨便查一下就有很多比這篇文章更詳細的介紹。

主要幫讀者整理了Redis的幾個特性,避免讀者在未來的設計及開發上踩雷

長話短說,基本上整理了這三點提供參考

  • Redis在6.0版本以前是單執行續的,準確地說

    其網路I/O和key-value讀寫是由一個執行序完成的

    而其他部分,e.g. 持久化模組、叢集模組等都還是多執行序的

    詳細的說明請點我

  • 在試圖使用多執行序前,請先試著優化自己的查詢方式,改採用pipe。

    如果不跳脫直覺設計觀念,維持著one command In,one response Out,就算用上多執行序

    我可以肯定效能還是一樣糟糕,因為每一次下達命令對Redis server來說都是一個很大的資源消耗

    如果情況允許,請多沉澱心情,設計適合pipe查詢模式的系統

  • 由於Redis資料結構的特性,Key的長度不建議太長,太長的key會導致redis下降

    • 長度為10 : 寫->平均消耗時間0.053ms,讀->0.040ms
    • 長度為20000:寫->平均消耗時間0.352ms,讀->0.084ms

Redis Cluster

先不要直接被叢集這個名詞嚇到了,其實官方很親民的提供了很強大的cluster設置工具及組件。

基本上只要跟著官方的文件操作,很快也能搞出一個屬於自己的Redis Cluster

而Redis Cluster的特性自行搜尋也有很多結果,這邊也不多提。

但相對少見的資料大概是對於Redis Cluster的效能描述。

雖然對這類資料庫做基準測試是一件很沒有意義的事情,因為基本上只是在考驗網路I/O速度

不過Cluster的效能可以簡單理解為N*M(Master Node 數量)

e.g.

使用Redis官方文件配置Cluster,應該會有3個主節點以及3個從結點。

那這個Cluster的效能,可以直接將在部屬的機器上測得單個Redis server效能 乘上 3個主節點數量

假設我在我的電腦測得Redis IOPS 分別為 SET 200000/s GET 400000/s

那Cluster的效能直接*3就好,但實務上受限於網路及socket分配速度等因素,實際效能會有瓶頸

取決於系統負荷如何,事實上不應該將所有的資料只交由一台機器負責


Redis安全策略

先假設讀者已經裝好的Redis,並且也很順利的利用cli連上Redis。

在預設情況下可以試著,從另外一台電腦,透過網路連線到Redis Server。

預設情況下應該會回報一些關於security的問題。(金魚腦)

這是因為Redis server為了效能考量其實並沒有真正的考慮到Security。

因此如果直接將Redis server對外開放IP,很可能會受到反序列化這類的攻擊。

(mongoDB存在類似問題)

所以我們需要一個大使對外開放,代替我們對Redis server進行操作。

而大使模式本身真正的用途不僅僅如此,也不僅僅用於Redis server,這後面會再度提到。


Redis安裝及配置

雖然網路上已經有很多資源了,但這裡除了安裝,也會提到一些需要注意的小細節。

安裝Redis

1
2
3
4
5
6
7
8
wget https://download.redis.io/releases/redis-6.2.5.tar.gz -P /usr/local/src
# 版本可以自行更換
cd /usr/local/src/ #移動到剛剛下載的資料夾
tar -zxvf redis-6.2.5.tar.gz #解壓縮Redis安裝包
cd redis-6.2.5 #移動到解壓縮後的資料夾
install GCC #安裝GCC
yum install -y gcc-c++ #利用yum安裝C++ 如果沒有安裝yum自己想辦法
make MALLOC=libc install #正式安裝Redis server

配置Redis Cluster

安裝完畢之後,接下來要進行Cluster配置

1
2
3
4
5
cd ~ #或是移動到你想配置從集的資料夾
mkdir redis-cluster && cd redis-cluster
mkdir 7000 7001 7002 7003 7004 7005
# 這邊要從7000重覆到7005 可以透過寫shell script解放雙手
vim ./7000/7000.redis.conf

要撰寫的內容為

1
2
3
4
5
6
7
8
9
port 7000
bind 0.0.0.0
dir ~/redis-cluster/7000/data
cluster-config-file nodes-7000.conf
cluster-enabled yes
cluster-node-timeout 5000
cluster-announce-ip 127.0.0.1
appendonly yes
daemonize yes

從7000~7005都配置完畢之後

1
redis-server 127.0.0.1:7000 #這個也要從7000~7005

基本上這樣應該已經先架設完畢cluster,測試連線可以輸入

1
redis-cli -c -p 7000 -h 127.0.0.1 #只有在本機可以輸入127.0.0.1,如果是另一台要連線需要輸入對外的IP

如果可以正常使用redis那基本就是安裝且配置成功了


Ambassador

大使模式在分散式系統中,其實主要是提供不同應用程式之間的溝通,大使的功能跟應用程式分離。

可以使在撰寫應用程式時,減少與其他服務的耦合。

因此大使不只能夠使用在Redis,也能應用在SQL或其他服務之上

能夠使整體架構更加彈性且增加解耦合性,但要注意的是透過大使處理服務,會產生延遲…等常見問題

而本篇文章要說的,正是由於Redis server極度不建議對外開放,所以透過大使操作在遠端機器的Redis server

大使也能夠過濾惡意字串,避免Redis server直接遭受攻擊

總結的說,大使大致上做的事情有;

  • 防止惡意請求直接破壞應用程式
  • 增加系統架構整體解耦合性
  • 監控性能指標以及機器環境…等

接下來要實作的內容的架構圖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

10.0.14.15 |---------------------------| |-------------
|-----------------| | |-----------------| | |
| | | | |-------------| | | |
| | | | |Redis server| | | |
| | | | |------|------| | | |
| Instance A | ====|===>| | |<===|=X=X=X=|===>
| | | | |------|------| | | Don't |
| | | | | Ambassador | | |Provide|
| | | | |------|------| | |Service|
|-----------------| | |--------|--------| | |
| | | |
|-----------------| | |--------|--------| | | INTERNET
| | | | |------|------| | | |
| | | | | | | | |
| | | | | | | | |
| Instance B | ====|===>| |WebSiteServer| |<===|=======|===>
| | | | | | | |Service|
| | | | | | | | |
| | | | |-------------| | | |
|-----------------| | |-----------------| | |
10.0.14.16 | VPC NetWork [Same Region] | |
35.x.x.x |---------------------------| |-------------

Ambassador撰寫

使用Node.js搭配Express撰寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const ioredis = require("ioredis");
const express = require("express");
const router = express.router();
const config = [
{port:"7000",host:"127.0.0.1"},{port:"7001",host:"127.0.0.1"},
{port:"7002",host:"127.0.0.1"},{port:"7003",host:"127.0.0.1"},
{port:"7004",host:"127.0.0.1"},{port:"7005",host:"127.0.0.1"}
]
let cluster = ioredis.Cluster(config);

function getKeyAndValue({data}){
return {
key:data.key,
value:data.value
}
}

router.post('/set',(req,res,next)=>{
let payload = getKeyAndValue({data:req.body});//取得key跟value
cluster.set(payload.key,payload.value);//將key跟value寫入redis
res.status(200).send();//寫入後 發送response http code 200 給發出請求的服務
});

router.post('/get',(req,res,next)=>{
let payload = getKeyAndValue({data:req.body});//取得key跟value
cluster.get(payload.key,payload.value,(error,apiResponse)=>{
if(error){
console.log(error)//如果無法取得值則在server寫log
res.staus(404).send(`${error}`);//回應http code 404給發出請求的服務
}else{
console.log(apiResponse);
res.status(200).send(apiResponse);//如果取得值,則將取得的資料回應給請求的服務
}
});
res.status(504).send();
});

順便附上Redis Cluster自動安裝架設的Shell Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/bin/bash
BASE_DIR=/usr/local/redis-cluster
PORTS=`seq 7000 7005`

START_UP=$BASE_DIR/startup.sh

SERVICE=redis-cluster.service

#Remove redis cluster
function remove_cluster(){
# kill redis servers
ps -ef | grep redis-server | grep cluster | awk '{print $2}' | xargs kill -9
#disable systemd
systemctl disable redis-cluster.serivce
# rm cluster data
if [ -d $BASE_DIR ];then
rm -rf $BASE_DIR
fi
}

if [ "$1" = "--remove" ];then
remove_cluster
exit 0
fi
#Check redis command
if [ ! -f "/usr/local/bin/redis-server" ];then
echo "Redis not ready,Please install redis firstly!"
echo ""
echo "===== Install redis as follows ====="
wget https://download.redis.io/releases/redis-6.2.5.tar.gz -P /usr/local/src
cd /usr/local/src/
tar -zxvf redis-6.2.5.tar.gz
cd redis-6.2.5
install GCC if not exists
yum install -y gcc-c++
make MALLOC=libc install

echo ""
fi

# User custom setting
echo -n "Enter your host's public address(default 127.0.0.1):"
read cluster_address

#enter work directory
mkdir -p $BASE_DIR
cd $BASE_DIR

#generate configuration files
function generate_instance_conf(){
echo "configuring server $1"

#clean conf file
echo "" > $1/redis.conf

#write conf
echo "port $1" >> $1/redis.conf
echo "bind 0.0.0.0" >> $1/redis.conf
echo "dir $BASE_DIR/$1/data" >> $1/redis.conf
echo "cluster-config-file nodes-$1.conf" >> $1/redis.conf
echo "cluster-enabled yes" >> $1/redis.conf
echo "cluster-node-timeout 5000" >> $1/redis.conf
if [ -n "$cluster_address" ];then
echo "cluster-announce-ip $cluster_address" >> $1/redis.conf
else
echo "cluster-announce-ip 127.0.0.1" >> $1/redis.conf
fi
echo "appendonly yes" >> $1/redis.conf
echo "daemonize yes" >> $1/redis.conf
}

# mkdir dirs and setup startup.sh

echo "#!/bin/bash" > $START_UP
servers=
for port in $PORTS;do
mkdir -p $BASE_DIR/$port/data
#generate conf files
generate_instance_conf $port
#
echo "/usr/local/bin/redis-server $BASE_DIR/$port/redis.conf" >> $START_UP
#servers
servers="$servers 127.0.0.1:$port"
done
#startup instances
chmod +x $START_UP
echo "starting servers..."
$START_UP
sleep 5s
echo "servers ready!"

#create cluster
echo "configuring cluster..."
/usr/local/bin/redis-cli --cluster create $servers --cluster-replicas 1
echo "configured!"

#generate redis-cluster server file
cat << EOT > $BASE_DIR/redis-cluster.service
[Unit]
Description=Redis 6.2.5 Cluster Service
After=network.target

[Service]
WantedBy=default.target
EOT
#create service
echo "Creating redis cluster serivce..."
ln -s $BASE_DIR/$SERVICE /etc/systemd/system/$SERVICE
sudo systemctl daemon-reload && sudo systemctl enable $SERVICE && sudo systemctl start $SERVICe

#Cluster OK
echo ""
echo "Completed!"
echo ""
echo "Test cluster with: /usr/local/bin/redis-cluster -c -h 127.0.0.1 -p 7000"
echo ""
echo "127.0.0.1:7000>cluster nodes"

撰寫中


參考資料