簡介 Intro 相信在這個年代(2021)大多數的後端開發者對於分散式系統開發及加速都有不少心得
其中非常知名的快取應用,就包含了Memory Storage類型的NoSQL,比如Redis 或Memcached 
今天寫的文章主要介紹在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});     cluster.set(payload.key,payload.value);     res.status(200 ).send(); }); router.post('/get' ,(req,res,next )=> {     let  payload = getKeyAndValue({data :req.body});     cluster.get(payload.key,payload.value,(error,apiResponse )=> {         if (error){             console .log(error)             res.staus(404 ).send(`${error} ` );         }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 function  remove_cluster  (){         ps -ef | grep redis-server | grep cluster | awk '{print $2}'  | xargs kill  -9          systemctl disable  redis-cluster.serivce          if  [ -d $BASE_DIR  ];then          rm -rf $BASE_DIR      fi  } if  [ "$1 "  = "--remove"  ];then     remove_cluster     exit  0 fi 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 echo  -n "Enter your host's public address(default 127.0.0.1):" read  cluster_addressmkdir -p $BASE_DIR  cd  $BASE_DIR function  generate_instance_conf  (){    echo  "configuring server $1 "           echo  ""  > $1 /redis.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 } echo  "#!/bin/bash"  > $START_UP servers= for  port in  $PORTS ;do     mkdir -p $BASE_DIR /$port /data          generate_instance_conf $port           echo  "/usr/local/bin/redis-server $BASE_DIR /$port /redis.conf"  >> $START_UP           servers="$servers  127.0.0.1:$port "  done chmod +x $START_UP  echo  "starting servers..." $START_UP sleep 5s echo  "servers ready!" echo  "configuring cluster..." /usr/local /bin/redis-cli --cluster create $servers  --cluster-replicas 1 echo  "configured!" cat << EOT > $BASE_DIR/redis-cluster.service  [Unit] Description=Redis 6.2.5 Cluster Service After=network.target [Service] WantedBy=default.target EOT 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  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" 
 
 
撰寫中  
參考資料