# HuaweiCloud-Unitized-Architecture-Demo **Repository Path**: HuaweiCloudDeveloper/huawei-cloud-unitized-architecture-demo ## Basic Information - **Project Name**: HuaweiCloud-Unitized-Architecture-Demo - **Description**: 单元化方案架构 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2022-11-14 - **Last Updated**: 2025-04-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 云部署架构类型 - 单体应用(web层、业务层、数据层) - 单IDC服务化(单AZ模式:服务化即微服务化) - 同城多机房(多AZ模式:混合调用--应用层单元化) - 两地三中心(多Region、多AZ模式:主备、异地备库,网络耗时与数据一致性矛盾) - 多地多活方案 # 多活与单元化 多活架构的基础是对系统进行单元化改造。每一个单元可以认为是一个缩小规模的、包含接入、应用到数据的全功能系统。每个单元负责一定比例的数据与用户访问。单元有以下特点: - **自包含性**:比如用户 ID A 的一次账户充值交易,涉及到的所有计算与数据都在一个单元内完成。 - **松耦合性**:跨单元之间只能进行服务调用,不能直接访问数据库或其它存储。 对于一些必须跨单元的交易处理,比如分属于两个不同单元的用户之间的转账交易,跨单元的服务调用次数尽可能少,在业务与用户体验允许的情况下尽量异步处理 。达成即使两个单元之间相距上千公里,也可以容忍跨单元的访问时延。 - **故障独立性**:一个单元内的故障,不会传播到其它单元,即:满足业务 单元故障不扩散原则。 - **容灾性**:单元之间相互备份, 确保每个单元在同城和异地都有可在故障期间进行接管的单元 ;数据在单元间的复制方式,以提供的多地多中心强一致方案为主。 金融核心系统、互联网系统、其他业务考虑多活和单元化必要性? # 单元化架构设计 单元化解决的问题:集中式的容量问题(扩容、数据库连接压力、服务耗时、中心化);容灾问题,降低故障半径;就近访问,提升用户体验问题;服务管理:故障隔离和灰度发布 - 单元化流程标记和控制:DNS层、反向代理层、网关/web层、业务层、数据层;多层级路由,尽可能早的确定正确的单元。 - 全局服务注册与发现,全局统一的路由规则。 - 应用层单元化:无状态与有状态;减少跨区调用。 - 数据层单元化:分区与容灾逻辑。 - 跨单元访问:数据漫游、异步机制。 # 华为云单元化实践 ## 数据漂移场景概述 ### 背景介绍 本文档主要是以出行场景为背景,用户信息数据保存在不同分区的数据库中,在单元化改造过程中遇到数据漂移的问题,基于此问题实现在当前分区数据查询和修改,以及发生数据漂移之后,在当前分区中对其他分区中的数据进行查询和修改等,本方案适用于少量数据漂移场景。 ### 漂移数据逻辑图 ![](figures/zh-cn_image_0000001401494840.png) ### 漂移数据查询——时序图 ![](figures/zh-cn_image_0000001441815325.png) ### 漂移数据更新——时序图 ![](figures/zh-cn_image_0000001390612018.png) ### 基于华为云容器引擎CCE部署架构图 ![](figures/zh-cn_image_0000001392009096.png) 整体流程如下: 1. 客户端请求到静态页面。 2. 静态页面调用后端user服务接口。 3. user服务查询当前分区的RDS For MySql数据库,如果查到用户直接返回,流程结束。 4. 在MySql数据库中没有查询到用户信息,则查询DCS For Redis缓存,如果查到用户信息直接返回,流程结束。 5. 如果在当前分区的没有查询到用户信息,则发送消息到DMS For RabbitMQ。 6. cache服务获取到DMS For RabbitMQ的消息。 7. cache服务消费消息,调用其他分区的user服务查询接口,获取用户信息。 8. 如果有在其他分区上获取到用户信息,则写入当前分区的DCS For Redis缓存中,如果没有则返回空,流程结束。 注:如果是通过缓存调用其他分区的user服务,则只会查询当前分区的RDS For MySql数据库,后续[4](#li34096421500)-[8](#li1141034215012)不会执行。 ## 具体实现和补充说明 以查询为例,下面是user服务和cache服务的代码,以及对目前实现方式的一些总结。 ### user模块代码示例 **用户信息查询** ``` public User getUser(String phone, String region) { //根据用户所在地查询对应数据库 User user =selectUser(phone); //如果没有查到,缓存中存在就读取缓存 if(user==null && redisUtil.exists(phone)){ user = JSONObject.parseObject(redisUtil.get(phone).toString(), User.class); } //region等于all,表示是通过缓存查询当前区域的数据,不需要在通过异步查询其他区域数据库 if(region.equals("all")){ return user; } //如果缓存没有,异步查询其他分区数据并且更新缓,每隔1一秒查询一次缓存 if(user == null){ //发送消息 sendMessage(phone); //每隔一秒查询缓存,5次没有就返回null for(int i=0;i<5;i++){ if(redisUtil.exists(phone)) { user = JSONObject.parseObject(redisUtil.get(phone).toString(), User.class); break; } try{ Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } return user; } ``` **发送消息** ``` public String sendQueryUserMessage(String phone) { String messageId = String.valueOf(UUID.randomUUID()); String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); //构建消息 Map map=new HashMap<>(); map.put("messageId",messageId); map.put("phone",phone); map.put("createTime",createTime); //将消息携带绑定键值:UserDirectRouting 发送到交换机SelectUserExchange rabbitTemplate.convertAndSend("SelectUserExchange", "SelectUserRouting", map); return "ok"; } ``` ### cache模块代码示例 **接收消息** ``` @Service @RabbitListener(queues = "SelectUserQueue")//监听的队列名称 SelectUserQueue public class SelectUserReceiver { @Autowired CacheService redisService; @RabbitHandler public void process(Map Message) { System.out.println("SelectUser消费者收到消息 : " + Message.toString()); redisService.doTask(Message.get("phone").toString()); } } ``` **消费消息** ``` public void doTask(String phone) { System.out.println("调用服务查询其他分区的用户信息"); long start = System.currentTimeMillis(); User user = selectUser(phone); if(user!=null){ redisUtil.set(phone,JSONObject.toJSONString(user),1L, TimeUnit.DAYS); } long end = System.currentTimeMillis(); System.out.println("调用服务查询其他分区的用户信息,耗时:" + (end - start) + "毫秒"); } //多线程查询其他分区集合 public User selectUser(String phone) { User user = new User(); List fanList = new ArrayList<>(); //定义可以提交给线程池的任务集合 List>> tasks = new ArrayList<>(); //每个线程的任务 Callable> task = null; //遍历添加任务 for (Constant.UrlEnum type : Constant.UrlEnum.values()) { task = new Callable>() { @Override public List call() throws Exception { RestTemplate restTemplate = new RestTemplate(); String url = type.getDesc() + "/user/getUser/" + phone + "/all"; System.out.println(Thread.currentThread().getName() + "调用分区的url:" + url); List result = restTemplate.getForObject(url, List.class); //返回result return result; } }; //添加任务 tasks.add(task); } try { //执行所有任务并获取结果,捕获异常InterruptedException List>> results = executor.invokeAll(tasks); for (Future> result : results) { //调用get方法获取每个返回的结果,将结果添加到定义好的集合中。捕获异常ExecutionException if (result.get().get(0) != null) { fanList.addAll(Collections.singleton(JSON.toJSONString(result.get().get(0)))); } } } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } //关闭线程池 executor.shutdown(); //处理返回的结果 if (fanList.size() > 0) { user = JSONObject.parseObject(fanList.get(0), User.class); } return user; } ``` ### 补充 对于当前的实现方式, 在其他的一些场景可能不够完善,为此总结以下几点: 1. 为什么DMS For RabbitMQ和DCS For Redis在当前方案是每个分区单独使用? 此处是针对安全性去考虑,将DMS For RabbitMQ和DCS For Redis做成公共的,每个分区通过公共消息队列去监听和消费消息。 当前分区查询没查询到数据时,使用公共消息队列通过广播的方式发送到其他分区,再将结果写入到公共缓存中,也可以实现。 2. 在对数据的一致性要求比较高时,在本区域修改数据之后,如果其他分区存在缓存,如何同步? 针对此种场景,可以通过缓存双删策略,在本区修改数据之前,异步调用删除其他分区中的缓存数据,然后修改本分区数据,修改完成之后,再删除其他分区中的需要修改缓存数据。 3. 跨分区修改数据时,实现点对点修改? 针对这种情况,可以维护一个路由表,每个地区代码为key,url为valu,通过用户的注册地去找到所在的分区地址,然后修改数据。 ## 结果验证 以查询为例进行结果验证:在当前分区数据查询平均耗时114ms,查询跨区域数据时,第一次平均耗时1.12s,第二次查询该用户时平均耗时69ms。 ### 当前分区数据验证 1. 在北京分区的RDS For MySQL数据库中新建test数据库,添加一张user表,数据如下图所示。 ![](figures/zh-cn_image_0000001391369572.png) 2. 前端页面进行查询,耗时114ms,结果如下图: ![](figures/zh-cn_image_0000001391259816.png) ### 跨区域数据验证 1. 在广州分区的RDS For MySQL数据库中新建test数据库,添加一张user表,数据如下图所示。 ![](figures/zh-cn_image_0000001391425360.png) 2. 在北京分区第一次查询广州用户信息时,耗时1.12s,结果如下图所示: ![](figures/zh-cn_image_0000001391104448.png) 3. 此时北京分区的Redis缓存中也有用户信息,如下图所示: ![](figures/zh-cn_image_0000001441793361.png) 4. 缓存中已经有用户信息时,再次查询,耗时69ms,结果如下图所示: ![](figures/zh-cn_image_0000001441704125.png) ## jmeter压力测试 使用jmeter对查询接口模拟1000线程并发,每间隔3分钟执行一次,共测试5次,汇总结果如下: ![](figures/zh-cn_image_0000001392771786.png) CCE节点资源使用情况如下: ![](figures/zh-cn_image_0000001442825361.png) ## 华为云资源准备 本次演示通过以北京分区为例进行部署演示,广州分区同理。 ### 云服务资源清单

序号

资源名称

配置规格

Region

数量

说明

1

CCE

cce.s2.small | 50节点(按需选择)

华北-北京四

广州

2

云容器引擎集群

2

ECS

通用计算增强型 | c6.xlarge.2 | 4vCPUs | 8GiB(按需选择)

华北-北京四

广州

2

云容器引擎节点部署应用

3

RDS

rds.mysql.n1.large.4.ha | 2 vCPUs | 8 GB

华北-北京四

广州

2

MYSQL数据库服务

4

Redis

geminidb.redis.large.4 | 2 vCPUs | 16GB

华北-北京四

广州

2

Redis缓存服务

5

RabbitMQ

rabbitmq.2u4g.single * 1 broker

华北-北京四

广州

2

RabbitMQ消息中间件

### 购买虚拟私有云VPC 虚拟私有云(Virtual Private Cloud,VPC),为云服务器、云容器、云数据库等云上资源构建隔离、私密的虚拟网络环境,后续其他资源应该都设置在同一个VPC下面,具体操作步骤如下。 登录华为云控制台—\>虚拟私有云VPC—\>创建虚拟私有云。 ![](figures/zh-cn_image_0000001390966944.png) ![](figures/zh-cn_image_0000001390488096.png) **设置具体参数可参考如下华为云官方文档\(ctrl键+鼠标左\):** [创建虚拟私有云和子网\_虚拟私有云 VPC\_用户指南\_虚拟私有云和子网\_虚拟私有云\_华为云 \(huaweicloud.com\)](https://support.huaweicloud.com/usermanual-vpc/zh-cn_topic_0013935842.html?utm_source=vpc_Growth_map&utm_medium=display&utm_campaign=help_center&utm_content=Growth_map) ### 创建安全组 安全组是一个逻辑上的分组,为同一个VPC内具有相同安全保护需求并相互信任的云服务器、云容器、云数据库等实例提供访问策略。安全组创建后,用户可以在安全组中定义各种访问规则,当实例加入该安全组后,即受到这些访问规则的保护。具体操作步骤如下: 1. 登录华为云控制台—\>虚拟私有云VPC—\>网络控制台—\>访问控制—\>安全组—\>创建安全组。 ![](figures/zh-cn_image_0000001390810792.png) ![](figures/zh-cn_image_0000001390971276.png) 2. 添加安全组规则。 ![](figures/zh-cn_image_0000001390651820.png) **具体详情可参考如下华为云官方文档**: 1. [创建安全组\_虚拟私有云 VPC\_用户指南\_安全性\_安全组\_华为云 \(huaweicloud.com\)](https://support.huaweicloud.com/usermanual-vpc/zh-cn_topic_0013748715.html) 2. [添加安全组规则\_虚拟私有云 VPC\_用户指南\_安全性\_安全组\_华为云 \(huaweicloud.com\)](https://support.huaweicloud.com/usermanual-vpc/zh-cn_topic_0030969470.html) ### 购买云容器引擎CCE 云容器引擎(Cloud Container Engine,简称CCE)提供高度可扩展的、高性能的企业级Kubernetes集群,支持运行Docker容器。借助云容器引擎,您可以在云上轻松部署、管理和扩展容器化应用程序。步骤如下: ![](figures/zh-cn_image_0000001390972796.png) 1. 登录华为云控制台—\>云容器引擎CCE—\>购买CCE集群。 ![](figures/zh-cn_image_0000001391294102.png) 2. 创建节点。 ![](figures/zh-cn_image_0000001391139470.png) ### 购买RDS For MySQL 1. 登录华为云控制台—\>云数据库RDS—\>购买数据库实例。 2. 选择MySQL数据库,其他按需选择即可。 ![](figures/zh-cn_image_0000001390987342.png) >![](public_sys-resources/icon-caution.gif) **注意:** >- 所选择的虚拟私有云VPC应和云容器引擎CCE保持一致。 >- 所选择的安全组的入方向规则需要允许CCE集群的容器网段访问,否则CCE容器的服务无法连接。 ### 购买云数据库GaussDB(For Redis) 1. 登录华为云控制台—\>云数据库GaussDB(For Redis)—\>购买数据库实例。 2. 选择Redis数据库,其他按需选择即可。 ![](figures/zh-cn_image_0000001441587977.png) >![](public_sys-resources/icon-caution.gif) **注意:** >- 所选择的虚拟私有云VPC应和云容器引擎CCE保持一致。 >- 所选择的安全组的入方向规则需要允许CCE集群的容器网段访问,否则CCE容器的服务无法连接。 ### 购买分布式消息服务RabbitMQ 1. 登录华为云控制台—\>分布式消息服务RabbitMQ版—\>购买RabbitMQ实例。 2. 按需选择即可。 ![](figures/zh-cn_image_0000001441311705.png) >![](public_sys-resources/icon-caution.gif) **注意:** >- 所选择的虚拟私有云VPC应和云容器引擎CCE保持一致。 >- 所选择的安全组的入方向规则需要允许CCE集群的容器网段访问,否则CCE容器的服务无法连接。