目标

以前单位主要是外包项目为主,在设计微服务架构时需要满足以下条件

  • 技术组件模块化,按需依赖加载
  • 对开发人员要求低,2年经验满足业务开发.
  • 学习成本低,正常情况2周就要能够上手开发业务.
  • 方便开发运维调试,最好是单机调试
  • DevOps工具链完善,摆脱人力运维
  • 场景灵活,不修改业务代码,可以实现单体,微服务部署模式切换.
  • 支持自动化的分布式事务,不写补偿代码.

方案选型

  • 鉴于SpringCloud的组件复杂度,侵入性,不采用.
  • 最新的Serverless基本是推翻了现有运维开发体系,目前还处于大厂验证使用阶段,不采用.
  • Service Mesh 能够利用现在的开发运维体系,把SpringCloud的组件下沉到平台层,业务层基本可以做到无侵入无感知,Istio+K8S黄金搭档,让基础组件和业务分离,采用此方案.

组件选型

软件环节越多,整体技术栈的复杂度就越高,坚持最少组件思想.比如ES即做全文检索又做NoSQL,RocketMQ即处理日志又参与业务.

  • Istio和K8S作为基础平台,使用istio集成的组件:EFK,Prometheus,Grafana,Jaeger,Kiali
  • CephFS实现分布式文件系统,和K8S对接,解决应用pod漂移之后的数据同步问题.
  • MySQL MGR作为数据库集群方案,保障数据库集群安全和读写性能.
  • Redis作为分布式缓存和分布式锁.
  • RocketMQ作为消息队列,处理流量削峰和日志等异步处理.
  • ES做为全文检索和NoSQL引擎.
  • Flink作为大数据引擎,使用K8S代替yarn进行资源调度,cephfs代替hdfs,数据落盘到ES,整体脱离Hadoop技术栈,降低开发运维成本.
  • GRPC作为服务通讯协议
  • Seata作为分布式事务方案
  • Sharding-jdbc作为分库分表方案
  • springrain作为业务开发平台.
  • Jenkins作为代码部署平台

设计实现

说明

springrain实现功能模块拆分,根据业务需要,选择不同的依赖,例如springrain-frame-cache-memoryspringrain-frame-cache-redis 缓存组件.

根据场景修改Maven打包POM依赖,隔离业务代码的影响
每个业务模块(例如springrain-system)里有service接口,service实现,web三个子模块项目,隔离相互的关联性.service接口service实现对类的命名和路径由严格的规范要求. 通过Maven的POM配置,按需引用依赖的模块.
例如在微服务方式下,服务B依赖服务A,服务B的POM只需要依赖服务Aservice接口项目,不依赖服务Aservice实现.如果是单体项目,依赖服务Aservice实现即可.

实现思路:

  • 启动加载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模块中org.springrain.rpc.springbind.GrpcBeanFactoryPostProcessor 在springbean容器初始化前,会检查业务service的接口和对应实现,如果service有接口也有实现,可以认为是本地模式,不做处理.
    如果只有接口没有实现,认为是RCP远程调用,会获取service接口上的RpcServiceAnnotation注解,获取到远程的RCP地址和端口,然后为这个service做一个代理,让spring正常加载代理bean,代理bean再RPC调用远程,返回结果.这样就对业务逻辑代码是透明的,业务逻辑代码只有在运行时,才知道是本地运行还是RPC调用.

     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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    
    
             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模式,自动回滚,不写补偿事务.

    1
    2
    3
    4
    5
    6
    7
    8
    
             // 因为有远程调用的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;
             }
    

    设置开启事务之后,就会在springrain-frame-dao模块的org.springrain.frame.config.DataSourceConfig类中设置dataSource

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
     /**
      * 自定义 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模块的org.springrain.frame.dao.SeataDataSourceTransactionManager,实现本地事务和Seata的XID绑定,隔离本地事务和Seata事务的切换过程.

    使用了Seata代理的DataSource,开启了分布式事务.需要把Seata服务端启动,具体参考Seata的官方文档.

  3. GRPC调用
    没有找到本地service实现的时候,就会开启GRPC调用,通过注册SpringBean的代理类,实现业务代码隔离.
    springrain-grpc-client模块中使用通用的grpcCommonService.proto文件,把请求参数封装成org.springrain.rpc.grpcimpl.GrpcCommonRequest对象,作为序列化参数,放到grpcCommonService通用模板.
    springrain-grpc-server中使用org.springrain.rpc.grpcimpl.CommonGrpcService响应GRPC调用,处理grpc事务,同时把SessionUser这个载体处理好.

    一般是业务系统只调用第三方系统,只依赖springrain-grpc-client就可以了,如果也提供RPC服务也需要依赖springrain-grpc-server模块,一般都依赖就可以了.

  4. POM依赖例子

1
2
3
4
5
6
7
8

        <!-- 单体项目依赖springrain-system-serviceimpl,如果是微服务依赖 springrain-system-service 接口即可 -->
        <dependency>
            <groupId>org.springrain</groupId>
            <artifactId>springrain-system-serviceimpl</artifactId>
            <version>6.0.0-SNAPSHOT</version>
        </dependency>

  1. 效果演示
    从业务代码已经无法感知是本地调用还是GRPC了.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@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<Menu> list( Menu menu, Page page)
            throws Exception {

        // 有可能是GRPC调用返回的结果,也可能是本地代码调用
        List<Menu> datas = menuService.findListDataByFinder(null, page, Menu.class, menu);

        return null;
    }
}

总结

技术是为管理服务的,管理是为经营服务的.
作为技术管理者,应该注重底层框架的学习成本,扩展,场景适应和技术选型.
Service Mesh (Istio+K8S) 是目前比较优秀的平台解决方案,以上基本做到了小微型项目和大型项目的技术栈统一.

架构中一些参考资料: