[AppActive]是阿里云計算產品應用多活AHAS中開源出來的一部分功能,應用多活AHAS主要有三塊,第一塊流量防護主要是基于阿里本身的[Sentinel]開源項目,與Hystrix類似,用于微服務故障熔斷和恢復。第二塊故障演練是基于[chaosblade]開源項目,混沌工程,也就是故障注入。最后一塊就是多活容災,這個的能力正是來源于AppActive。目前AppActive開源出來的代碼比較簡單,也不完善,但可以看出來一些實現思路。
一、應用雙活、多活的原理和實現方案
關于應用雙活、多活,首先要了解一些分布式理論如CAP、BASE。可以看看[基于庫存的異地雙活方案],這是我幾年前實現的方案和思路。業務單元化,基于規則的路由/流量調度,業務降級、業務接管與恢復、基于Mysql的雙寫和主從同步控制緩存、消息、ES等數據同步以達到數據最終一致性等。都是應用雙活實現的主要技術點。在云計算時代,結合K8S和容器技術,基礎設施更容易管理,多活應該更好做了。
二、分析AppActive
首先從github先把代碼clone下來。
## 1.了解規則文件
規則文件其實就是一些JSON文件,其中描述了流量的定義、轉換和流量轉發規則
- idSource.json: 描述如何從 http 流量中提取路由標,比如請示中帶有r_id標識或cookie中的用戶標識
- idTransformer.json: 描述如何解析路由標
- idUnitMapping.json: 描述路由標和單元的映射關系
- machine.json: 描述當前機器的歸屬單元
- mysql-product: 描述數據庫的屬性
## 2.安裝組件和推送規則
通過demo代碼目錄下的sh run-quick.sh進行docker-compose安裝和啟動應用
```shell
[docker@ccse-0004 product-center]$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------------------------------
frontend sh -c java -jar /app/front ... Up 0.0.0.0:8885->8885/tcp
frontend-unit sh -c java -jar /app/front ... Up 0.0.0.0:8886->8886/tcp
gateway nginx -p /etc/nginx -c /et ... Up 0.0.0.0:80->80/tcp, 0.0.0.0:8090->8090/tcp
mysql docker-entrypoint.sh --cha ... Up 3306/tcp, 33060/tcp, 0.0.0.0:3307->3307/tcp
nacos bin/docker-startup.sh Up 0.0.0.0:8848->8848/tcp
product sh -c java -jar /app/produ ... Up 0.0.0.0:8883->8883/tcp
product-unit sh -c java -jar /app/produ ... Up 0.0.0.0:8884->8884/tcp
storage sh -c java -jar /app/stora ... Up 0.0.0.0:8881->8881/tcp
storage-unit sh -c java -jar /app/stora ... Up 0.0.0.0:8882->8882/tcp
```
組件作用
- Nacos:服務注冊中心,安裝的幾個微服務使用
- MySql:存儲
- gateway:應用網關,執行切流規則。開了兩個端口,80給應用使用,8090用于規則推送
- frontend、product、storage則分別是不同的微服務,-unit表示單元化
安裝完成后,訪問//demo.appactive.io/buyProduct?r_id=2000,注意先host文件映射一下域名。

推送portal下的baseline.sh:
- 通過 http 通道給 gateway 推送規則,即curl調用nginx 8090,通過lua腳本設置網關流量規則
- 通過 文件 通道給 其他應用 推送規則,即通過cp方式把portal的rule目錄下的規則,復制到demo/data對應的應用目錄下
## 3.切流
portal下的cut.sh,可以認為portal為管理控制臺,cut.sh即為管理控制臺給gateway傳遞切流指令。
執行切流過程:
- 構建新的映射關系規則和禁寫規則(手動)
- 將新的映射關系規則推送給gateway
- 將禁寫規則推送給其他應用
- 等待數據追平后將新的映射關系規則推送給其他應用
注意,新的映射關系是你想達到的目標狀態,而禁寫規則是根據目標狀態和現狀計算出來的差值。當前,這兩者都需要你手動設置并更新到 `appactive-portal/rule` 下對應的json文件中去,然后運行 `./cut.sh`
cut.sh通過curl調用openresty的set api,把路由規則推送過去。
```shell
gatewayRule="{\"idSource\" : $idSource, \"idTransformer\" : $idTransformer, \"idUnitMapping\" : $idUnitMapping}"
data="{\"key\" : \"459236fc-ed71-4bc4-b46c-69fc60d31f18_test1122\", \"value\" : $gatewayRule}"
echo $data
curl --header "Content-Type: application/json" \
--request POST \
--data "$data" \
127.0.0.1:8090/set
```
通過cp命令把portal rule目錄下的文件拷貝到demo應用對應的目錄下
```shell
for file in $(ls ../appactive-demo/data/); do
if [[ "$file" == *"path-address"* ]]; then
echo "continue"
continue
fi
echo "$(date "+%Y-%m-%d %H:%M:%S") 應用 ${file} 禁寫規則推送中)"
cp -f ./rule/$forbiddenFile "../appactive-demo/data/$file/forbiddenRule.json"
echo "$(date "+%Y-%m-%d %H:%M:%S") 應用 ${file} 禁寫規則推送完成"
done
echo "等待數據追平......"
sleep 3s
for file in $(ls ../appactive-demo/data/); do
if [[ "$file" == *"path-address"* ]]; then
echo "continue"
continue
fi
echo "$(date "+%Y-%m-%d %H:%M:%S") 應用 ${file} 新規則推送中"
cp -f ./rule/$idUnitMappingNextFile "../appactive-demo/data/$file/idUnitMapping.json"
echo "$(date "+%Y-%m-%d %H:%M:%S") 應用 ${file} 新規則推送完成"
done
```
切流完成后,再次刷新,r_id=2000的流量發生了變化 :

## 4.gateway實現分析
gateway主要實現規則動態更新,基于Nginx+Lua來實現,openresty是利用Nginx Lua構建的一個web平臺,實現通過lua處理http請求。這里的gateway主要由openresty鏡像,lua處理腳本和路由規則組成。
/nginx-plugin/etc/conf/sys.conf,定義共享緩存,監聽8090端口,處理/get /set請求
```shell
#openresty共享內存,多nginx worker共享
lua_shared_dict kv_shared_dict 32m;
server {
listen 8090;
location /get {
content_by_lua_file 'conf/lua/kv/kv_get.lua';
}
location /set {
content_by_lua_file 'conf/lua/kv/kv_set.lua';
}
location /demo {
content_by_lua_file 'conf/lua/demo.lua';
}
}
```
處理set請求的lua腳本在conf/lua/kv/kv_set.lua,處理邏輯類似寫數據庫同時寫緩存。
```lua
--main
local req_method = ngx.var.request_method
if "PUT" == req_method or "POST" == req_method then
local data = getRuleBody()
if data then
local dataDecoded = cjson.decode(data)
if not dataDecoded then
kv.print("set value invalid", 400)
end
if dataDecoded.key and dataDecoded.value then
--打開文件,nginx的docker目錄/etc/nginx/store
local f = io.open(kv.storePath .. dataDecoded.key, "w+")
if f then
--以key做為文件名,value為作文件內容寫入
local ret = f:write(cjson.encode(dataDecoded.value))
f:close()
--寫入成功則同時寫入緩存
if ret then
local rule_ver = kv.kvShared:get(dataDecoded.key..kv.versionKey)
if rule_ver == nil then
rule_ver = 1
else
rule_ver = rule_ver + 1
end
kv.kvShared:set(dataDecoded.key..kv.versionKey, rule_ver)
kv.kvShared:set(dataDecoded.key, cjson.encode(dataDecoded.value))
kv.print("success", 200)
else
kv.print("write disk failed", 500)
end
else
kv.print("open file failed", 500)
end
else
kv.print("null key or value not supported", 400)
end
end
end
```
## 5.實現流量調度
主要通過nginx配置和lua腳本實現流量控制與調度
nginx配置:
```shell
events {
use epoll;
worker_connections 20480;
}
http {
log_format proxyformat "$status|$upstream_status|$remote_addr|$upstream_addr|$upstream_response_time|$time_local|$request_method|$scheme://$log_host:$server_port$request_uri|$body_bytes_sent|$http_referer|$http_user_agent|$http_x_forwarded_for|$http_accept_language|$connection_requests|$router_rule|$unit_key|$unit|$is_local_unit|$ups|$cell_key|$cell|";
access_log "logs/access.log" proxyformat;
#lua相關配置
lua_package_path "${prefix}/conf/lua/?.lua;;";
init_by_lua_file 'conf/lua/init_by_lua_file.lua';
lua_use_default_type off;
lua_max_pending_timers 32;
lua_max_running_timers 16;
#http 8090,用lua腳本處理http請求
include sys.conf;
#網關處理
include apps/*.conf;
}
```
apps/exmaple.conf,通過upstream配置實現應用流量控制
```shell
server {
listen 80 ;
server_name demo.appactive.io center.demo.appactive.io unit.demo.appactive.io ;
include srv.cfg;
location / {
set $app "demo_appactive_io@";
#開始寫死了單元類型、規則ID
set $unit_type test1122;
set $rule_id 459236fc-ed71-4bc4-b46c-69fc60d31f18;
set $router_rule ${rule_id}_${unit_type};
set $unit_key '';
set $cell_key '';
set $unit_enable 1;
#實現proxy配置
include loc.cfg;
}
location /demo {
set $app "demo_appactive_io@demo";
set $unit_type test1122;
set $rule_id 459236fc-ed71-4bc4-b46c-69fc60d31f18;
set $router_rule ${rule_id}_${unit_type};
set $unit_key '';
set $cell_key '';
set $unit_enable 1;
include loc.cfg;
}
}
#中心
upstream demo_appactive_io@_center_default {
server frontend:8885;
}
#單元
upstream demo_appactive_io@_unit_default {
server frontend-unit:8886;
}
upstream demo_appactive_io@demo_center_default {
server 127.0.0.1:8090;
}
upstream demo_appactive_io@demo_unit_default {
server 127.0.0.1:8090;
}
```
loc.cfg
```shell
#通過腳本計算所屬單元
set_by_lua_file $unit "conf/lua/set_user_unit.lua" $router_rule $unit_enable;
if ($unit = "-2") {
return 500 "wrong route condition";
}
if ($unit = "-1") {
set $unit $self_unit;
}
set $is_local_unit 1;
if ($unit != $self_unit) {
set $is_local_unit 0;
}
#計算upstream name
set $ups "${app}_${unit}";
set $cell "default";
set $ups "${ups}_${cell}";
# attention no _ in key
proxy_set_header "unit-type" $unit_type;
proxy_set_header "unit" $unit;
proxy_set_header "unit-key" $unit_key;
proxy_set_header "host" $host;
proxy_pass //$ups;
```
set_user_unit.lua
```lua
local kv = require("kv.kv_util")
local ruleChecker = require("util.rule_checker")
local unitFilter = require("util.unit_filter")
local function doMain()
-- rule_id
local ruleKey = ngx.arg[1]
-- unit enable?
local unitEnabled = ngx.arg[2]
-- 獲取規則原始內容
local ruleRaw = kv.get(ruleKey)
-- 規則版本
local ruleRawVersion = kv.get(ruleKey ..kv.versionKey)
-- 規則轉換檢查
local ruleParsed = ruleChecker.doCheckRule(ruleRawVersion, ruleKey, ruleRaw)
-- 計算出單元編號
local unit = unitFilter.getUnitForRequest(ruleParsed, unitEnabled == '1')
return unit
end
-- main
local ok, res = pcall(doMain)
if not ok then
ngx.log(ngx.ERR, "[unit] calc error "..res);
return -1
else
ngx.log(ngx.INFO, "[unit] calc "..res);
return res
end
```
三、總結
目前開源的比較簡陋,感覺沒有達到生產級可用,需要根據自己的產品規劃理解后,再進行開發,網關的核心就是nginx + lua。服務層主要是基于Dubbo,數據層是Mysql,這里使用有局限性,后面再分析。