0%

应用多活AppActive简介

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安装和启动应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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

    推送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,把路由规则推送过去。

1
2
3
4
5
6
7
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应用对应的目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

4.gateway实现分析

gateway主要实现规则动态更新,基于Nginx+Lua来实现,openresty是利用Nginx Lua构建的一个web平台,实现通过lua处理http请求。这里的gateway主要由openresty镜像,lua处理脚本和路由规则组成。

/nginx-plugin/etc/conf/sys.conf,定义共享缓存,监听8090端口,处理/get /set请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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,处理逻辑类似写数据库同时写缓存。

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
--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配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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配置实现应用流量控制

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
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

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
#通过脚本计算所属单元
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

1
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 unitend-- mainlocal ok, res = pcall(doMain)if not ok then    ngx.log(ngx.ERR, "[unit] calc error "..res);    return -1else    ngx.log(ngx.INFO, "[unit] calc "..res);    return resend

三、总结

目前开源的比较简陋,感觉没有达到生产级可用,需要根据自己的产品规划理解后,再进行开发,网关的核心就是nginx + lua。服务层主要是基于Dubbo,数据层是Mysql,这里使用有局限性,后面再分析。