一、前言
dubbo与springcloud都可以单独作为微服务治理框架在生产中进行使用,但使用过springcloud的同学大概了解到,springcloud生态的相关组件这些年已经逐步停更,这就导致在服务架构演进过程中的迭代断层,以至于一些新的技术组件引入困难重重,于是在国内的市场上就有了升级版的springcloud-alibaba。
二、springcloud-alibba与dubbo对比
2.1 springcloud-alibaba 简述
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
2.1.1 主要技术组件介绍
springcloud-alibaba提供了丰富的服务治理技术组件,列举如下:
- Sentinel:流控治理组件,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性;
- Nacos:一个更易于构建云原生应用的集动态服务发现、分布式配置管理和服务管理平台;
- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务;
- Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架,可以单独作为服务治理框架;
- Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案;
- Alibaba Cloud ACM(收费等同于Nacos):一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品;
- Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据;
- Alibaba Cloud SchedulerX: (收费) 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务;
- Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道;
利用上述这些组件的组合,可以较好的完成生产级项目的微服务改造,从技术演进来看,这一套技术组件也在逐步适配云原生的脚步,可以不用担心未来集成云原生过程中遇到问题得不到解决的麻烦。
2.1.2 springcloud-alibaba使用场景
springcloud架构升级改造
由于springcloud生态下部分组件很长时间未做更新,针对那些之前使用原生的springcloud但是又需要对架构进行更深层次的优化、调整、扩展等治理需求的场景。
更完备的流控治理
springcloud-alibaba中的sentinel,在流控治理方面更加出色,尽管springcloud中也有hystrix用于服务的降级、流控等功能,但是功能仍然比较单一。
服务与配置的集中式管理
springcloud-alibaba中nacos集服务与配置管理为一体,而且提供更友好更便捷的可视化界面方便开发运维使用,可以减少部署和运维成本,而原生的springcloud则需要同时使用eureka以及引入其他的配置管理组件共同使用。
容器化部署与云原生支持
背后有强大的技术团队背书,以及阿里云对云原生技术的支撑与融合,Spring Cloud Alibaba 支持 Kubernetes、Docker 等容器化技术,可以方便地部署。
跨应用接口调用
springcloud实现微服务之间相互调用主要是通过http接口实现的,对于不同应用跨机器,甚至跨网络部署的场景可以考虑使用。
2.2 dubbo 简述
Dubbo是阿里开源的分布式服务治理框架,其使用简单,高效便捷的使用方式得到跟多架构师的青睐,底层以RPC方式调用远程服务。
dubbo提供了三大核心能力:
- 面向接口的远程方法调用;
- 智能容错和负载均衡;
- 以及服务自动注册和发现;
2.2.1 dubbo业务模型
下面这张图是dubbo的模型架构图,不难看出,架构中主要包括4部分:
- Registry,服务注册中心;
- Provider,服务提供方,对外或其他应用提供服务接口;
- Consumer,服务消费方,调用Provider提供的服务,像本地一样调用;
- Monitor,服务监控,一般很少使用;
2.2.2 dubbo使用场景
尽管dubbo也可以单独拿出来作为微服务治理的框架,而且在很多公司中也是广泛实用,与springcloud-alibaba相比,大部分场景下都是可以互相替代的,关于dubbo的使用场景,这里总结下面几点作为参考;
微服务架构改造
微服务架构下服务的拆分很细,服务之间需要相互调用,使用Dubbo可以方便地实现微服务之间的通讯,对于服务消费方来说,只需引入提供方的SDK,像本地调用一样方便;
分布式系统
在分布式系统中,各微服务模块需要互相合作处理任务,这时可以考虑使用Dubbo作为服务调用中间件
内部系统架构
平台级内部各应用之间在进行服务的相互调用时,使用dubbo比springcloud在传输效率更高效
高并发、大流量场景
dubbo底层使用的是netty,相比http来说,一定程度上传输效率更好,在高并发下,dubbo的优势更明显
更关注接口级的API治理
dubbo提供了完备的服务治理技术,基于服务治理的思想提供了一整套服从务发现、负载均衡、容错机制、线程调优、通讯协议等解决方案,对API层面的治理更为友好和精细
三、服务改造的难题
3.1 技术选型难题
对于一个稍成规模的公司级产品来说,技术架构当然不是一成不变的,但有一点相信很多伙伴还是能达成共识的,即对大多数中小型公司的产品来说,其技术架构在相当长的一段时间内很难做较大的调整,而技术架构在一开始的选型之后基本上算是定型了,为什么这么说呢?
3.1.1 技术负责人的技术风格
可以说,很多团队的技术负责人的技术栈广度和深度在很大程度上面决定了一个产品的架构基调,有些负责人对dubbo技术比较深入,可能一开始就选择了使用dubbo作为服务治理框架,而如果对springcloud熟悉的话,可能就会偏向于springcloud;
3.1.2 业务增长快速难以定性
这几年,市场上很流行DDD的概念,事实上真正推动DDD落地还是有相当的代价的,这个需要公司的业务模型相对稳定、清晰、并且具备一定的可控性,假如一个公司的业务处于快速的扩张中,那将充满着较多的不确定性,不确定性带来的直接问题就是,技术架构在选型上需要进行合理的权衡,既不能太复杂,但也需要留有一定的扩展性,为后续架构的扩展留有余地,由于业务上的不确定性很难说采用那种微服务框架是服务治理的最好模式;
3.1.3 微服务框架的融合难题
不少同学肯定遇到这样的场景,比如说你的springboot版本是1.5.X,现在需要升级到2.X版本,看起来只是一个jar包版本的升级问题,但是有过升级经验的同学来说,这个升级过程堪比西天取经,苦难重重不说,一大堆的坑要填,最后还要深入到代码层面层层调整...
涉及到框架层面的调整也是司空见惯的事,举例来说,你的微服务治理框架使用的是springcloud,但是发现对于配置中心的管理使用git实在很不方便,而且配置托管在外网上总觉得不踏实,需要收回来,或者说,你需要对整个平台的各个微服务进行统一的可视化流控治理,如果你觉得springcloud中的hystrix不好用,可能会想到引入其他的第三方流控治理组件...
上面的这个情形就是,头疼医头,脚痛医脚,最后整个产品引入的第三方技术组件越来越多,维护的成本越来越高,部署、运维要投入的人力也越来越多,更要命的事情是,一旦出了问题,解决问题的效率很低。究其原因,原生的springcloud框架自身在微服务治理体系下,在应对如今越来越复杂的服务治理需求上,开始显得力不从心,于是很多人开始考虑springcloud-alibaba进行整合并实现架构上的调整。
事实上,尽管springcloud-alibaba也是完美支持springcloud并且做了不少的升级,但是从架构调整来说,这也是一项改造量相当大的工作,主要难题聚集在下面几点:
顶级pom依赖不兼容
springcloud与springcloud-alibaba的根pom适配版本是不相同的,毫不夸张的说,springcloud-alibaba几乎不可能与springboot 1.5.X系列的版本在一起工作了;
内部组件需要调整和兼容
springcloud的注册中心是eureka,而springcloud-alibaba的配置管理主推nacos,如何将eureka调整为使用nacos?还是直接在springcloud-alibaba中继续使用nacos呢?
部署模式上的调整
springcloud-alibaba有阿里技术团队背书,其部署模式在适配容器化以及云原生上具有天然的优势,而springcloud在这一点来说已经失去了竞争力,但同时,这也给技术团队从开发到运维提出了更高的要求;
团队人员的学习成本
相对而言,springloud-alibaba涉及到的技术栈更为全面,并且今后考虑与云原生融合的话这个学习成本甚至更高,这就需要开发人员掌握更多的新的技能,这项成本不能不考虑。
3.2 dubbo与springcloud的融合难题
随着dubbo在微服务治理中的优势越来越明显,springcloud-alibaba这一套治理体系为了能够一通吃掉也是煞费苦心,举个实际的场景来说,假如你现在所在团队使用的是springcloud-alibaba这一套框架,那么服务之间的RPC调用自然少不了ribbon或者openfeign;
以openfeign为例进行说明,对于服务消费方来说,使用起来很方便,也很顺手,调用远程服务的时候像是调用本地服务一样简单,但是,从单个API服务的治理和管控来说,不管是openfeign,还是ribbon,都不是很友好,而且就算是进行治理,还需要借助外部的手段,比如agent,或者编码的方式,或者其他的外部组件进行服务监测,比较要命的是,如果都是内部的服务,走openfeign的调用,在高并发场景下,这个性能问题多少会暴露出来;
于是架构师或技术负责人想到如果能使用dubbo就好了,毕竟人家dubbo在API级别的服务治理上还是很出色,并且提供了一整套治理方案的。但是直接生硬的把openfeign干掉更换dubbo也是不现实的,如果微服务模块多的话,那就不是单单改造一个两个应用的事情了。
于是,进一步思考,能否采用折中的方案,在现有的基础上引入dubbo,即同时支持openfeign与dubbo的调用呢?这个考虑似乎可行,而且也是一种相对合理的解决方案,事实上,很多产品在做架构调整上也是依照这个思路,为了干掉旧的,引入新的技术,然后让新旧并存一段时间之后再逐步废弃旧的技术。
以上面的场景和解决思路为背景,下面以实际操作案例演示使用springcloud-alibaba 整合dubbo与openfeign的完成过程。
四、springcloud-alibaba整合openfeign
4.1 前置准备
为了方便后文的测试,提前创建一个数据库并创建一张测试使用的数据表,数据库名为:dbuser01,建表sql如下:
CREATE TABLE `user` (
`id` varchar(32) NOT NULL,
`user_name` varchar(32) NOT NULL,
`pass_word` varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `dbuser01`.`user`(`id`, `user_name`, `pass_word`) VALUES ('001', 'jerry', '123456');
INSERT INTO `dbuser01`.`user`(`id`, `user_name`, `pass_word`) VALUES ('002', 'mike', '123456');
INSERT INTO `dbuser01`.`user`(`id`, `user_name`, `pass_word`) VALUES ('003', 'john', '123456');
4.2 操作步骤
4.2.1 工程目录结构
完整的工程目录结构如下
4.2.2 父模块导入如下依赖
根pom中定义基础的全局版本号依赖,这里主要包括:springcloud,springcloud-alibaba等版本,其他的可以在具体模块中增量补充即可,另外需要注意springcloud 与springcloud-alibaba以及springboot的版本兼容性问题,这个可以参考相关的资料,网上有很多。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
<mysql.version>5.1.47</mysql.version>
<mybatis.version>2.1.1</mybatis.version>
<dubbo.version>2.7.18</dubbo.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
4.2.3 服务提供方核心依赖
主要包括:mysql连接,mybatis,nacos服务注册中心,以及openfeign的依赖,dubbo的依赖可以在后面添加;
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.congge</groupId>
<artifactId>biz-common</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Dubbo Spring Cloud Starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
</dependencies>
4.2.3 公共模块定义实体类和服务接口
在公共模块中定义一个实体类,与数据库user表映射,注意这里一定要实现序列化,避免后面调用dubbo接口时出现序列化错误;
@Data
public class User implements Serializable {
private static final long serialVersionUID = -2896873555774275129L;
private String id;
private String userName;
private String passWord;
}
定义服务接口 UserDubboApiService
public interface UserDubboApiService {
User getById(String id);
}
最后,将common模块通过mvn的install命令安装到本地仓库,然后引用到其他两个模块中
4.2.4 服务提供方核心代码
核心配置文件
server:
port: 8081
spring:
application:
name: biz-provider-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
url: jdbc:mysql://IP:3306/dbuser01?useUnicode=true&characterEncoding=utf-8
username: 用户名
password: 密码
driver-class-name: com.mysql.jdbc.Driver
#mybatis相关配置
mybatis:
type-aliases-package: com.congge.entity
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mybatis/*.xml
添加一个服务接口用于测试
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//http://localhost:8081/user/getById?id=001
@GetMapping("/getById")
public Object getByUserId(@RequestParam("id") String id){
/*User user = new User();
user.setId("001");
user.setUserName("jerry");
user.setPassWord("123456");
return user;*/
return userService.getById(id);
}
}
服务实现
import com.congge.entity.User;
import com.congge.mapper.UserMapper;
import com.congge.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getById(String id) {
return userMapper.getById(id);
}
}
服务启动类
@SpringBootApplication
@EnableFeignClients
@MapperScan("com.congge.mapper")
public class BizProviderApp {
public static void main(String[] args) {
SpringApplication.run(BizProviderApp.class,args);
}
}
4.2.5 服务提供方接口测试
这里使用了nacos作为服务注册中心,因此在启动服务之前,需要启动nacos
启动提供方的服务,然后浏览器调用一下测试接口,看到下面的效果说明整合完成;
4.2.6 服务消费方核心代码
工程目录结构,依赖与生产方的相同,就不再赘述了
添加一个Feign的接口类
import com.congge.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
@FeignClient(name = "biz-provider-service",path = "/user")
public interface UserFeignService {
@GetMapping("/getById")
public User get(@RequestParam("id") String id);
}
添加一个测试接口,这里为了方便省略了服务实现类,实际开发中建议添加实现类
@RestController
public class BizMockController {
@Autowired
private UserFeignService userFeignService;
//localhost:8082/getFromRemote
@GetMapping("/getFromRemote")
public Object getFromRemote(){
return userFeignService.get("001");
}
}
核心配置文件,主要是配置一个工程的服务名注册到nacos;
server:
port: 8082
spring:
application:
name: biz-consumer-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心地址
服务启动类
@SpringBootApplication
@EnableFeignClients
public class BizConsumerApp {
public static void main(String[] args) {
SpringApplication.run(BizConsumerApp.class,args);
}
}
4.2.7 服务消费方接口测试
以上步骤做完后,启动消费方服务,通过上面提供的测试接口调用一下,看到下面的效果,说明消费方通过openfeign调用远程的服务完成;
五、springcloud-alibaba整合openfeign并兼容dubbo
回到第三章中最后的原始诉求,当工程需要适配架构调整,引入dubbo框架时该怎么做呢?改造的效果是,对于服务消费方来说,原来已经在使用openfeign的继续使用,同时可以更换openfeign的调用为dubbo调用,接下来做对上述的两个模块代码做一些调整即可;
5.1 服务提供方改造
5.1.1 添加dubbo依赖
注意这里直接引入的是springcloud整合的dubbo而没有单独引dubbo依赖,也可以自己单独引,但是需要注意版本匹配的问题(慎重慎重)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
通过点击进去可以发现这里使用的是2.7.8的dubbo版本,而在这个版本中,dubbo在配置时可以不用依赖zk了,所以我并没有引zk或curator相关依赖;
5.1.2 增加dubbo相关配置
dubbo的配置这里只添加了基础的配置信息,相信使用过的同学应该不陌生,主要是一个dubbo协议,端口,外加dubbo扫描的业务实现类包路径;
dubbo:
protocol:
name: dubbo
port: 20881
scan:
base-packages: com.congge.service.impl.dubbo
5.1.3 代码改造
引入dubbo后,为了方便开发的包目录清晰,以及减少后续的维护成本,将工程的目录结构做如下调整:
关于该目录结构,做如下补充说明,也是一种生产上的实践经验:
- dubbo的实现类建议与工程自身的业务实现类分开;
- 本工程的业务实现类主要编写自身的业务给当前工程使用;
- dubbo的业务实现类单独提供服务实现,给其他调用方使用;
- 为了减少编码的冗余和维护成本,针对两者相同的或者重用度很高的逻辑,建议再单独拿出一个包目录,在该目录下编写通用的业务逻辑被两个实现类引用;
添加dubbo实现类,这里的UserDubboApiService即为公共模块中定义的服务接口;
import com.congge.entity.User;
import com.congge.service.UserDubboApiService;
import com.congge.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
@DubboService
public class UserDubboApiServiceImpl implements UserDubboApiService {
@Autowired
private UserService userService;
@Override
public User getById(String id) {
User dbUser = userService.getById(id);
return dbUser;
}
}
服务提供方的代码到这里就改造完成了,接下里看消费方的改造
5.2 服务消费方改造
大体步骤和生产方差不多,引入dubbo依赖和上面相同
5.2.1 配置文件改造
补充dubbo的配置,添加下面这段即可
dubbo:
protocol:
name: dubbo
port: 20882
5.2.2 添加测试接口
添加一个测试接口,用于通过dubbo接口调用
@RestController
public class BizMockController {
@Autowired
private UserFeignService userFeignService;
//localhost:8082/getFromRemote
@GetMapping("/getFromRemote")
public Object getFromRemote(){
return userFeignService.get("001");
}
@DubboReference(retries = 0, timeout = 10000)
private UserDubboApiService userDubboApiService;
//localhost:8082/getFromDubbo
@GetMapping("/getFromDubbo")
public Object getFromDubbo(){
return userDubboApiService.getById("002");
}
}
5.2.3 接口测试
分别启动生产方和消费方的服务,调用上述的新增的接口,看到下面的效果说明通过dubbo也能成功调用远程服务;
小结,通过上面的改造就完成了从单独使用openfeign到同时兼容dubbo的效果,当然,有两点还需要再次强调一下:
1)版本兼容性问题,这也是在实际引入新的外部组件时与当前工程的springboot版本兼容性最容易踩坑的地方;
2)引入dubbo之后,如何更优雅的管理好本工程自身的业务实现与dubbo的业务实现;
六、写在文末
本文从一个生产实践中可能遇到的技术架构上的调整问题,通过实践操作,详细说明了如何在openfeign中引入dubbo,使得当前的服务提供方同时兼容这两种服务调用的模式,事实上,实际项目中,影响的外部因素可能源于比这个复杂,不过可以作为一个参考提供一个解决的思路,最后感谢各位的观看。需要源码的同学可前往下载:源码地址