从零到一搭建基础架构(5)-让你的RPC原地起飞

4,145 阅读11分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!


Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。

本篇是手把手搭建基础架构专栏的第五篇,👇🏻是专栏历史文章,依次读取效果更佳。

第一篇:从零到一搭建基础架构(1)-玩转maven依赖版本管理

第二篇:从零到一搭建基础架构(2)-如何构建基础架构模块划分

第三篇:从零到一搭建基础架构(3)-base模块搭建上篇

第四篇:从零到一搭建基础架构(4)-base模块搭建下篇

微服务的生态体系下,RPC是服务之间不可缺少的通信方式。

在庞大的微服务生态下,可能存在成百上千的服务。越上层的业务服务,对于一些基础服务的依赖就会越多。比如订单服务会依赖支付服务、用户服务、商品服务等等。

这么多的依赖,我们如何让服务之间的依赖松散化,开箱即用化?

这将是本文为大家所解释的,废话不多说,Let‘s get it.

image.png

本文无特殊说明,均已Spring Cloud Open-Feign作为RPC框架做演示

  1. 基础架构:common-frame
  2. 业务应用:Authentication

你需要先clone common-dependency

然后执行mvn clean install 将 common-dependency包打到你本地仓库

否则你拉下来common-frame工程后会报找不到

<parent>
    <groupId>com.baiyan</groupId>
    <artifactId>common-dependency</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</parent>

一、RPC调用与依赖的困境

我在看过很多大的单体服务做微服务改造的时候,从业务出发,将工程划分为多个微服务。

原有业务之间的关联与调用关系从系统内变为系统外

服务之间的通信方式从service调用改为rpc调用,从spring的事件通知改为MQ的消息通知。

跨服务之间消息通信也是遵守增删改查的逻辑。

我们以订单服务call用户服务获取用户信息为例👇🏻

image-20221020152744392.png

这种比较常见的方式就是使用订单服务访问用户服务的RPC接口。

在同一注册中心下,使用openFeign作为RPC框架的调用下,比较常见的开发步骤是怎么样的?

1.用户服务在UserController里面定义一个detail的方法。

@ApiOperation(value = "用户详情")
@GetMapping("{id}")
public Result<UserDetailDTO> detail(@PathVariable Long id) {
  return Result.success(userService.detail(id));
}

2.订单服务在工程内定义UserClient的OpenFeign定义。

@FeignClient(
        name = "user",
        url = "https://www.baiyan.com/user",
        fallback = UserClient.UserFallback.class,
        configuration = UserClient.UserFallback.class
)
public interface UserClient {
​
    @GetMapping("/{id}")
    Result<UserDTO> getUserDetail(@PathVariable("id") Long id);
    
    @Component
    class UserFallback {
        public Result<UserDTO> getUserDetail(Long id){return null;}
   }
}

3.订单服务启动类增加 @EnableFeignClients 注解扫描UserClient,将UserClient定义为一个bean。

4.订单服务在需要获取用户数据的地方直接注入订单工程内定义的UserClient使用。


应该有很大一部分同学都是这么接入openFeign的。从功能角度来说,这种编码方式能够实现订单服务获取用户服务所提供的的用户信息。但是从编码角度,上面的代码存在很多问题

问题描述
POJO重复定义细心的小伙伴应该发现,UserController里面返回的实体是UserDetailDTO,UserClient接收的结构是UserDTO。服务间的对接需要通过文档来进行对齐,用户服务已经定义的结构,在订单服务还要再定义一遍。用户服务一旦对于用户信息结构做了改动,增加字段或者修改字段等等,服务的请求方都感知不到,只能通过沟通,文档对接的方式拉平
过多的用户接口用户服务提供的是web层面的controller接口。由于web接口都是对用户开放的,内部的RPC接口需求也会被暴露给用户
鉴权的冗余同样的,内部服务之间的互相调用,理论上不需要再经过网关的鉴权,应该是互相信任的。但由于提供方定义的接口是Controller接口,暴露给了用户,因此鉴权也成为了必须的。对于订单服务的普通用户请求还好说,还能传递一下用户请求的身份信息。但是如果是定时任务,订单服务根本不存在用户身份,为了能够通过用户服务的鉴权,还需要去伪造一份身份信息,非常麻瓜。
异常感知弱我们需要依据标准Result里面的code字段来感知当前RPC接口出现了什么样的问题。但实际上,对于调用方来说,直接处理RPC接口的异常比解析RPC接口返回的errorCode是什么,再做出什么样的处理更加方便。
接入RPC不够丝滑增加一个服务的接口依赖,我们就要在依赖工程内增加一个openFeign的定义,太难受了。

二、基础架构能做点什么?

为了解决上述粗暴的openFeign使用方式所带来的问题,基础架构能做点什么?

我们先来回顾一下api包与rpc包的定义

Maven模块模块定义备注
api定义微服务提供者的接口定义,将openFeign相关的接口定义,所必须的交互实体,枚举等定义在此处Common-Frame工程中所定义的api包仅做模块分层指导,并无实际意义
rpcapi包的openFeign定义实现,这里如果嫌麻烦api包跟rpc包可以合并,我习惯分开,接口结构更加清晰Common-Frame工程中所定义的rpc包提供统一的序列化、熔断、异常处理等配置

先说为什么在架构指导上我们要把api包与rpc包分开来。

api包的定位更像是一个三方的结构声明包。里面将会包含所有可以暴露给外部工程的工具类、常量、POJO、枚举等。而rpc包的定位是将openFeign提供starter,让服务的依赖方引入了rpc的maven依赖后,可以直接使用注入openFeign开箱即用。在有的场景下,A服务只需要依赖B服务的某个工具类或者某个枚举定义,并不想要调用B服务所提供的RPC接口。这种时候就可以减少RPC定义的引入到A服务中。把包的职责划分清楚。

对于业务服务来说,RPC包中包含了api包中定义的rpc接口的openFeign定义,那在基础架构中,rpc包能做什么呢?

答案是:公共配置

在使用openFeign作为RPC框架之后,随之会引入很多常见的微服务问题:熔断、限流、降级等等。而这些配置对于所有业务服务来说都是互通的,无非是熔断的时间,限流的规则等差异。所以我们在基础架构中可以定义上述的这些公共配置,将这些配置做成base-rpc-starter包。业务服务中的rpc依赖common-frame中的rpc包就可以获取一系列的公共配置。

common-frame-rpc-demo

三、业务应用中如何定义与使用RPC

定义完common-frame中的公共配置后,在业务服务中,我们该如何定义并使用rpc包呢?

以文章开头的项目Authentication为例,我们可以看到工程中,我们已经划分了api包与rpc包。

Authentication服务是比较早期我做分布式用户中心时的demo,是按照common-frame的思想搭建的,为了项目开箱即用,所以并没有直接一对一的引入common-frame中已经设定好的maven配置。这个在最后一两章的时候会为大家全局性的演示如何完美使用common-frame。

这里Authentication服务仅为大家演示在api包与rpc包是如何应用在业务服务中的。

在api包中定义了所有需要暴露给服务调用方的POJO定义、枚举定义、常量定义和接口定义。

在rpc包中,pom依赖引入了api包,将在api包中定义的接口进行openFeign定义。并且我们在rpc包中定义了一个自动配置类

/**
 * auth服务RPC自动配置类
 *
 * @author baiyan
 * @date 2020/11/26
 */
@Configuration
@Import(AuthFeignConfig.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@EnableFeignClients(basePackages = "com.baiyan.auth.rpc")
public class AuthFeignClientAutoConfiguration {
​
}

这个自动配置类的好处是什么?

假设订单服务为服务调用方。那么订单服务只要引入了用户服务的rpc包,订单服务无需做任何配置,就可以直接注入api包中定义UserApi或者rpc中定义的UserApiClient,就可以实现访问用户服务。

如果只是在rpc中定义openFeign的结构,订单服务仍旧无法注入上述用户服务的bean,需要在订单服务的启动类上增加 @EnableFeignClients(basePackages = "com.baiyan.auth.rpc") 才行。这还只是依赖一个服务,如果依赖的服务很多,那定义的路径也太多了。

因此每个服务提供方在所要提供出去的rpc包中定义这样一个自动配置类,让使用者无需添加任何配置,只要引入提供方的Maven依赖即可实现对提供方的访问。

四、api包为什么要多实现

在第二篇多模块的文章下面有个小老弟提问

image-20221020181138353.png

api中定义的rpc接口是需要区分于controller中定义的web接口的。

api中定义的http接口是符合rpc结构定义规范的,在nginx层面是不会开放这部分接口的。api包内的http接口仅为其他服务提供业务支撑,并不是用户接口。而在controller层的http接口是用户接口,两者在应用场景上是有区别的。

现在我们有一个用户详情的接口,在interaction的controller包中已经定义了一个接口。

api包中对于此部分的接口返回值是一样的,我们能够直接把api中rpc接口的实现指向这个controller吗?

不能!

image.png

为啥?

1.api定义的接口可能有10个方法,controller中可能只有9个方法,在接口实现上有差异。有的同学可能会说接口可以定义默认实现,返回空值。

比如在api包中定义

@PostMapping("by_accessKey")
default UserDTO getUserDetail(@RequestParam("accessKey") String accessKey){return null;}

想想也不合理,第一api只是一层壳,你让壳做了默认实现。第二controller与api的结构无法匹配。

2.用户实现与rpc实现被捆绑。假设用户接口需要做升级,修改几个字段。但是RPC缺并不需要修改原有的逻辑与返回,这个时候rpc接口将会被迫去修改。rpc一旦deploy到中央仓库被其他服务使用,你的任意改动,其他服务都会感知到,从服务提供的了历史兼容角度来看,我们也应该将接口分开。

image.png

五、总结

本篇从微服务中如何使用openFeign触发,为大家介绍了RPC提供方与RPC使用方常见的使用坑点。从坑点触发,为大家介绍基础架构架构中我们如何定义api与rpc包。从底层rpc包的作用触发,介绍了业务服务在使用rpc包时,我们需要注意的点:

  1. api包中仅定义rpc对外提供http结构的壳,不定义rpc的框架。demo中我们使用openFeign做为rpc包中的rpc框架。如果我们要使用dubbo作为我们的rpc框架,我们可以将common-frame中公共配置修改为dubbo的公共配置,业务服务的rpc保重rpc的定义改为dubbo的rpc定义。后续就算rpc框架切换,那么对于服务提供方来说,影响也是非常小的。
  2. interaction中apiImpl与controller中的接口需要区分。controller中的接口是开放给用户的,interaction中apiImpl中的实现是开放给内部服务的。

基于上述的框架构造的微服务,服务之间基于openFeign的调用都不需要在调用方内定义提供方的数据结构与接口。调用方可以通过引入提供方的rpc包,开箱即用提供方的rpc接口,此刻纵享丝滑。

image.png

六、联系我

如果你觉得文章写得不错,点赞评论+关注,么么哒~

微信:baiyan_lou

我的第一本掘金小册《深入浅出DDD》已经在掘金上线,欢迎大家试读~

DDD的微信群我也已经建好了,由于文章内不能放二维码,大家可以加我微信,备注DDD交流,我拉你进群,欢迎交流共同进步。