title: 58.springrain云原生微服务架构 CreateTime: 2020-09-15 00:00:00 UpdateTime: 2020-09-15 00:00:00 CategoryName: web --- --- title: "58.springrain云原生微服务架构" date: 2020-09-15T00:00:00+08:00 draft: false tags: ["springrain"] categories: ["web"] author: "springrain" --- ## 目标 以前单位主要是外包项目为主,在设计微服务架构时需要满足以下条件 - 技术组件模块化,按需依赖加载 - 对开发人员要求低,2年经验满足业务开发. - 学习成本低,正常情况2周就要能够上手开发业务. - 方便开发运维调试,最好是单机调试 - DevOps工具链完善,摆脱人力运维 - 场景灵活,不修改业务代码,可以实现单体,微服务部署模式切换. - 支持自动化的分布式事务,不写补偿代码. ## 方案选型 - ~~鉴于SpringCloud的组件复杂度,侵入性,不采用.~~ - ~~最新的Serverless基本是推翻了现有运维开发体系,目前还处于大厂验证使用阶段,不采用.~~ - Service Mesh 能够利用现在的开发运维体系,把SpringCloud的组件下沉到平台层,业务层基本可以做到无侵入无感知,istio+K8S黄金搭档,让基础组件和业务分离,采用此方案. ## 组件选型 软件环节越多,整体技术栈的复杂度就越高,坚持最少组件思想.比如Redis同时用于缓存,分布式锁,分布式计数器和消息队列. - istio和K8S作为基础平台,使用istio集成的组件:EFK,Prometheus,Grafana,Jaeger,Kiali - CephFS实现分布式文件系统,和K8S对接,解决应用pod漂移之后的数据同步问题. - MySQL MGR作为数据库集群方案,保障数据库集群安全和读写性能. - Redis作为缓存,分布式锁和分布式计数器. - Redis Stream作为消息队列,处理流量削峰和日志等异步处理. - ClickHouse做数据存储检索引擎. - Flink作为大数据引擎,使用K8S代替yarn进行资源调度,cephfs代替hdfs,数据落盘到ClickHouse,整体脱离Hadoop技术栈,降低开发运维成本. - gRPC作为服务通讯协议 - Seata作为分布式事务方案 - Sharding-jdbc作为分库分表方案 - springrain作为业务开发平台. - Jenkins作为代码部署平台 ## 设计实现 ### 说明 springrain实现功能模块拆分,根据业务需要,选择不同的依赖,例如```springrain-frame-cache-memory``` 和 ```springrain-frame-cache-redis``` 缓存组件. **根据场景修改Maven打包POM依赖,隔离业务代码的影响** 每个业务模块(例如[springrain-system](https://gitee.com/chunanyong/springrain/tree/master/springrain-system))里有```service接口```,```service实现```,```web```三个子模块项目,隔离相互的关联性.```service接口```和```service实现```对类的命名和路径由严格的规范要求. 通过Maven的POM配置,按需引用依赖的模块. 例如在微服务方式下,```服务B```依赖```服务A```,```服务B```的POM只需要依赖```服务A```的```service接口```项目,不依赖```服务A```的```service实现```.如果是单体项目,依赖```服务A```的```service实现```即可. ### 实现思路: - 启动加载springbean时,先检查本地是否有实现,如果没有就启动gRPC远程调用,如果开启了gRPC,就会调用Seata的配置,同时开启分布式事务.(开发人员无感知) - 基于seata分布式事务实现.支持有注解和无注解(底层记录日志)混合使用.(开发人员无感知) - 基于K8S的Service实现服务注册和发现,ConfigMap实现配置中心.(开发人员无感知) - 基于Istio实现微服务的发现,监控,熔断,限流.(开发人员无感知) ### 限制: - 接口和实现的命名强制规范. - 一个RPC接口只能有一个实现. - 分布式事务,一定要避免A服务update表t,RPC调用B服务,B服务也update表t.这样A等待B结果,B等待A释放锁,造成死锁. - Service层不可以使用Servlet API,例如 HttpRequest ### 实现代码 在service mesh,业务系统只需要处理rpc和分布式事务,其他的微服务功能由istio完成.所以重点说下对springrain的设计实现. 根据项目情况,最小只需要springrain做为单体项目裸机运行,不使用k8s和istio及相关组件,把运维成本降到最低;复杂点可以使用Nginx负载gRPC协议,更复杂的就是K8S+Istio了...... 1. 项目启动加载SpringBean 在[springrain-grpc-client](https://gitee.com/chunanyong/springrain/tree/master/springrain-grpc-client)模块中[org.springrain.rpc.springbind.GrpcBeanFactoryPostProcessor](https://gitee.com/chunanyong/springrain/blob/master/springrain-grpc-client/src/main/java/org/springrain/rpc/springbind/GrpcBeanFactoryPostProcessor.java) 在springbean容器初始化前,会检查业务service的接口和对应实现,如果service有接口也有实现,可以认为是本地模式,不做处理. 如果只有接口没有实现,认为是RCP远程调用,会获取service接口上的[RpcServiceAnnotation](https://gitee.com/chunanyong/springrain/blob/master/springrain-grpc-client/src/main/java/org/springrain/rpc/annotation/RpcServiceAnnotation.java)注解,获取到远程的RCP地址和端口,然后为这个service做一个代理,让spring正常加载代理bean,代理bean再RPC调用远程,返回结果.这样就对业务逻辑代码是透明的,业务逻辑代码只有在运行时,才知道是本地运行还是RPC调用. ```java if ((!clazz.isInterface()) || (!clazz.isAnnotationPresent(RpcServiceAnnotation.class))) {// 只处理有@RpcService注解的接口 continue; } // 获取实现类名 String classSimpleName = clazz.getSimpleName(); String classImplSimpleName = classSimpleName.substring(1, classSimpleName.length()) + "Impl"; // 实现类的全路径 String rpcServiceImplPath = null; RpcServiceAnnotation rpcServiceAnnotation = clazz.getAnnotation(RpcServiceAnnotation.class); String implpackage = rpcServiceAnnotation.implpackage(); if (StringUtils.isBlank(implpackage)) {//没有指定包路径 // 根据包名规则,组装接口默认实现的class路径,如果类存在,就认为是本机加载,找不到就启用RPC String rpcServiceImplClassName = rpcServiceClassName.replace(".service.", ".service.impl."); rpcServiceImplPath = rpcServiceImplClassName.substring(0, rpcServiceImplClassName.lastIndexOf("."))+ "." + classImplSimpleName; } else { rpcServiceImplPath = basepackagepath + "." + implpackage + "." + classImplSimpleName; } try { Class rpcServiceImplClass = Class.forName(rpcServiceImplPath); if (rpcServiceImplClass != null) { continue; } } catch (Exception e) { logger.error("未找到接口" + clazz.getName() + "的实现类" + rpcServiceImplPath + ",开始RPC调用远程实现"); } // 因为有远程调用的service,设置seata为启用状态. if (org.springrain.frame.util.GlobalStatic.seataGlobalEnable) { if (!org.springrain.frame.util.GlobalStatic.seataEnable) { org.springrain.frame.util.GlobalStatic.seataEnable = true; } } else {// 如果全局禁用seata,就设置为false org.springrain.frame.util.GlobalStatic.seataEnable = false; } String rpcHost = rpcServiceAnnotation.rpcHost(); Integer rpcPort = rpcServiceAnnotation.rpcPort(); String beanName = rpcServiceAnnotation.beanName(); if (rpcHost == null || rpcHost.equals("")) { rpcHost = GlobalStatic.rpcHost; } if (rpcPort == null || rpcPort <= 0) { rpcPort = GlobalStatic.rpcPort; } if (beanName == null || beanName.equals("")) { beanName = clazz.getName(); } // 开始gRPC请求调用 GrpcCommonRequest grpcRequest = new GrpcCommonRequest(); grpcRequest.setClazz(clazz.getName()); grpcRequest.setBeanName(beanName); grpcRequest.setTimeout(rpcServiceAnnotation.timeout()); grpcRequest.setVersionCode(rpcServiceAnnotation.versionCode()); grpcRequest.setAutocommit(rpcServiceAnnotation.autocommit()); // 创建接口实现的gRPC代理类 // Object invoker = new Object(); InvocationHandler invocationHandler = new GrpcServiceProxy<>(rpcHost, rpcPort, grpcRequest); Object proxy = Proxy.newProxyInstance(RpcServiceAnnotation.class.getClassLoader(), new Class[]{clazz}, invocationHandler); // 手动注册 springbean beanFactory.registerSingleton(beanName, proxy); ``` 2. 事务处理 如果是本地运行,使用本地普通事务,不做任何处理.如果是RPC调用,根据配置,设置开启Seata事务,使用Seata的AT模式,自动回滚,不写补偿事务. ```java // 因为有远程调用的service,设置seata为启用状态. if (GlobalStatic.seataGlobalEnable) { if (!GlobalStatic.seataEnable) { GlobalStatic.seataEnable = true; } } else {// 如果全局禁用seata,就设置为false GlobalStatic.seataEnable = false; } ``` 设置开启事务之后,就会在[springrain-frame-dao](https://gitee.com/chunanyong/springrain/tree/master/springrain-frame-dao)模块的[org.springrain.frame.config.DataSourceConfig](https://gitee.com/chunanyong/springrain/blob/master/springrain-frame-dao/src/main/java/org/springrain/frame/config/DataSourceConfig.java)类中设置dataSource ```java /** * 自定义 dataSource,用户扩展实现 */ @Bean("dataSource") public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username);// 用户名 dataSource.setPassword(password);// 密码 // 设置属性 setDataSourceProperties(dataSource); // 如果使用seata if (GlobalStatic.seataEnable) { // 设置seata的datasource代理 DataSourceProxy proxy = new DataSourceProxy(dataSource); // 初始化注册seata RMClient.init(GlobalStatic.seataApplicationId, GlobalStatic.seataTransactionServiceGroup); TMClient.init(GlobalStatic.seataApplicationId, GlobalStatic.seataTransactionServiceGroup); return proxy; } return dataSource; } ``` 使用[springrain-frame-dao](https://gitee.com/chunanyong/springrain/tree/master/springrain-frame-dao)模块的[org.springrain.frame.dao.SeataDataSourceTransactionManager](https://gitee.com/chunanyong/springrain/blob/master/springrain-frame-dao/src/main/java/org/springrain/frame/dao/SeataDataSourceTransactionManager.java),实现本地事务和Seata的XID绑定,隔离本地事务和Seata事务的切换过程. 使用了Seata代理的DataSource,开启了分布式事务.需要把Seata服务端启动,具体参考[Seata的官方文档](http://seata.io/zh-cn/). 3. gRPC调用 没有找到本地service实现的时候,就会开启gRPC调用,通过注册SpringBean的代理类,实现业务代码隔离. 在[springrain-grpc-client](https://gitee.com/chunanyong/springrain/tree/master/springrain-grpc-client)模块中使用通用的[grpcCommonService.proto](https://gitee.com/chunanyong/springrain/blob/master/springrain-grpc-client/src/main/proto/grpcCommonService.proto)文件,把请求参数封装成[org.springrain.rpc.grpcimpl.GrpcCommonRequest](https://gitee.com/chunanyong/springrain/blob/master/springrain-grpc-client/src/main/java/org/springrain/rpc/grpcimpl/GrpcCommonRequest.java)对象,作为序列化参数,放到grpcCommonService通用模板. 在[springrain-grpc-server](https://gitee.com/chunanyong/springrain/tree/master/springrain-grpc-server)中使用[org.springrain.rpc.grpcimpl.CommonGrpcService](https://gitee.com/chunanyong/springrain/blob/master/springrain-grpc-server/src/main/java/org/springrain/rpc/grpcimpl/CommonGrpcService.java)响应gRPC调用,处理grpc事务,同时把SessionUser这个载体处理好. 一般是业务系统只调用第三方系统,只依赖[springrain-grpc-client](https://gitee.com/chunanyong/springrain/tree/master/springrain-grpc-client)就可以了,如果也提供RPC服务也需要依赖[springrain-grpc-server](https://gitee.com/chunanyong/springrain/tree/master/springrain-grpc-server)模块. 4. POM依赖例子 ```xml org.springrain springrain-system-serviceimpl 6.0.0-SNAPSHOT ``` 5. serviceimpl独立运行 微服务模式下,service实现需要独立运行,例如springrain-system-serviceimpl模块的pom.xml配置如下图,需要依赖springrain-grpc-server模块,使用springboot的maven打包插件. ```xml org.springrain springrain-grpc-server 6.0.0-SNAPSHOT .... org.springframework.boot spring-boot-maven-plugin ``` 6. 效果演示 从业务代码已经无法感知是本地调用还是gRPC了. ```java @RestController @RequestMapping(value = "/api/system/menu", method = RequestMethod.POST) public class MenuController extends BaseController { @Resource //有可能实现在远程,也可能是本地代码 private IMenuService menuService; @RequestMapping(value = "/list", method = RequestMethod.POST) public ReturnDatas list( Menu menu, Page page) throws Exception { // 有可能是gRPC调用返回的结果,也可能是本地代码调用 List datas = menuService.findListDataByFinder(null, page, Menu.class, menu); return null; } } ``` ## 总结 技术是为管理服务的,管理是为经营服务的. 作为技术管理者,应该注重底层框架的学习成本,扩展,场景适应和技术选型. Service Mesh (Istio+K8S) 是目前比较优秀的平台解决方案,以上基本做到了小微型项目和大型项目的技术栈统一. 架构中一些参考资料: - [springrain](https://gitee.com/chunanyong/springrain) - [CloudNative](/public/categories/cloudnative/) - [05.MySQL-MGR和主从分离](/public/post/05-mysql-mgr/)