[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表示单元化
安装完成后,访问http://demo.appactive.io/buyProduct?r_id=2000,注意先host文件映射一下域名。
![image-20220126012136145](http://img.vinin.me/image-20220126012136145.png)
推送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的流量发生了变化 :
![image-20220128002622077](http://img.vinin.me/image-20220128002622077.png)
## 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 http://$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,这里使用有局限性,后面再分析。