问题与思考 ------ 关于 SpringCloud + Dubbo 的微服务架构项目
谁杀死了我的微服务?
2023年12月27日,上午9点57分。我打开了电脑,开始对 Sharine 项目进行重构,在上次更新中我决定将 Consul、Redis、MySQL 部署到 Docker,方便一键启动开发环境,于是我编写了相关的 Docker-Compose 。
arduino
name: "sharine-containers"
services:
redis:
container_name: redis
image: redis
ports:
- "6379:6379"
mysql:
container_name: mysql
image: mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
sonarqube:
container_name: sonarqube
image: sonarqube
ports:
- "9000:9000"
consul:
container_name: consul
image: "hashicorp/consul"
command: ["agent", "-server","-client", "0.0.0.0","-bootstrap","-ui"]
ports:
- "8500:8500"
- "8502:8502"
- "8503:8503"
- "8600:8600"
- "8301:8301"
- "8302:8302"
上午11点45分,项目开始测试。
docker-compose up 运行中...完成,3/3。UserService、InteractService、ContentService...通通启动, 随后...没有任何异常。我松了一口气,离开电脑去简单吃个午饭。
回到电脑前,访问 localhost:8500 进入 Consul 控制台,神奇的一幕发生了 ------ 健康检测没有一个微服务存活...可里面,明明是微服务的味道啊...
是谁杀死了我的微服务?
我立刻开始排查,健康检测...哦对,一定是没有添加相关的健康检查依赖,我查阅资料后补充了 Spring Boot Actuator 依赖,手动验证了 /health 路径,确保每个微服务都能够被访问到。然后接入了 Consul 。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
不,不不不...它们又一次当着我的面死去了。
我不知道是哪里出了问题,是 Docker 网络环境没有配置好吗?我分明给每一台容器都精心配置好了端口,这不可能错,不可能错的...到底是谁杀死了我的微服务?!
下午1点30分,成立 "Sharine 微服务健康检测异常调查小组",由我,我,还有我完成这次调查任务。
由于调查过程过于错综复杂,且枯燥,此处不做展开叙述。
傍晚19点12分,证据确凿,系 Docker 与 Consul 二人合伙作案!
Docker 提供作案条件,Consul 负责作案,由于 Docker 不支持容器访问宿主机网络,而 Consul 恰好部署在 Docker 内,微服务又在宿主机中,即使正确的暴露了容器的端口,也只能是宿主机访问容器,不能是容器访问宿主机。于是,微服务在 Consul 上能够正确的进行注册,而 Consul 却认为它们都已经死了!根本无法进行健康检测,无法访问微服务。如果不解决这个问题,将给未来实施 RPC 方案埋下巨大的风险。于是我果断地将 Consul 从 Docker 中抽离,一同部署到宿主机上,案件也至此落下帷幕。
less
@consul agent -data-dir "C:\Consul\data" -config-file "C:\Consul\config" -server -bind 127.0.0.1 -client 0.0.0.0 -bootstrap -ui
@pause
至于 Redis 和 MySQL,我想他们或许更喜欢待在 Docker 中,那就这样吧!开发环境至此也开始变得有些凌乱。
欢迎新成员:Dubbo 加入了 Sharine 微服务大家庭
Dubbo: 你们好,我是 Du ..
MikkoAyaka: 你先别急,该死的微服务又无法从 Consul 获取配置文件了,更离谱的是只有 AggregatedService 无法获取配置文件进行初始化,其它微服务都可以正常工作,可它们的配置文件分明是一模一样的啊,在 Consul 上的配置文件也是一模一样的,只是每个微服务我都创建了一个文件方便未来分别进行更新管理。这次总不能又是....
Docker: 别看我啊,不关我事哥们,Consul 在你自己电脑上,跟我没关系奥。
MySQL、Redis: 你m..你看你m呢。
AggregatedService: 呃,总不能怪我吧?
气氛陷入了沉寂,还伴随着快要凝结的空气,就连 MikkoAyaka 的气息都开始变得小心翼翼。仍然,没人承认自己是罪魁祸首,还得看大侦探 MikkoAyaka 如何揪出元凶。
时间线开始回溯,对于 AggregatedService 的修改,仅限于以下部分:
- SpringBoot 主类添加 @EnableDubbo 注解
- AggregatedService 业务类添加 @DubboService 注解
- Consul 中 AggregatedService 的配置文件添加了以下内容:
yaml
dubbo:
reference:
check: false
consumer:
check: false
protocol:
name: dubbo
application:
qos-enable: false
不得不说,Consul 配置文件编写还是比较人性化的,YAML 格式的配置文件,按 TAB 可以自动补充两个空格的缩进,非常方便。
诶等等?TAB 缩进?我曾经在使用某些文档软件的时候遇到过相关的问题,TAB 缩进打出来的符号和直接空格缩进是两种完全不同的符号内容。不会是这个问题吧?
我立刻删除刚才添加的包括 TAB 缩进的内容,再写了一遍,不同的是这次全部使用空格进行缩进。
微服务启动...Spring 开始初始化了!我去,Consul 你还在嘴硬,我******。
MikkoAyaka: 好了,你是新来的 Dubbo 是吧,别被吓到了,我们这个家庭还是非常的和谐温馨的,大家相处都十分友善,很少出现各种框架之间的兼容性问题,至于刚才的情况...完全是意外。快进来吧别搁门口站着了多见外呐。
Dubbo: 请问,我现在走还来得及吗?(
(沉默)
Dubbo: 我是说很高兴加入你们!我没有想逃跑的!别这样凶巴巴的看着我!
MikkoAyaka: 这才对嘛,你快去认识一下 Consul 吧,等会你跟他对接一下,把他作为你的服务中心。
为了让 Dubbo 与 Consul 能够协同工作,我往项目中引入了以下依赖:
xml
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-consul</artifactId>
<version>2.7.23</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-rmi</artifactId>
<version>2.7.23</version>
</dependency>
其中 dubbo-registry-consul 是 Dubbo 关于配置中心的 SPI 扩展,为 Dubbo 提供了连接 Consul 并将其作为服务中心的相关支持。
而 dubbo-rpc-rmi 则是 Dubbo 关于 RPC 协议的 SPI 扩展,使 Dubbo 可以基于 RMI 协议进行远程服务调用。
关于 RMI 的介绍,可参考 Dubbo 官网给出的内容:
特性说明
RMI 协议采用 JDK 标准的 java.rmi.* 实现,采用阻塞式短连接和 JDK 标准序列化方式。
- 连接个数:多连接
- 连接方式:短连接
- 传输协议:TCP
- 传输方式:同步传输
- 序列化:Java 标准二进制序列化
- 适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
- 适用场景:常规远程服务方法调用,与原生RMI服务互操作
约束
- 参数及返回值需实现 Serializable 接口
- dubbo 配置中的超时时间对 RMI 无效,需使用 java 启动参数设置:-Dsun.rmi.transport.tcp.responseTimeout=3000,参见下面的 RMI 配置
使用场景
是 Java 的一组拥护开发分布式应用程序的 API,实现了不同操作系统之间程序的方法调用。
在将 Dubbo 引入到 Sharine 项目中时,为了确保一切能够按预期运行,我特意新建了一个测试项目,创建了 Consumer、Provider 模块,引入相关依赖并测试了与 Consul 的连通性,一切正常。
于是我信心满满地将这些步骤再复刻到 Sharine 中,运行,报错了。
kotlin
java.lang.IllegalStateException: Failed to load extension class (interface: interface org.apache.dubbo.rpc.Protocol, class line: rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol) in jar:file:/C:/Users/34012/.m2/repository/org/apache/dubbo/dubbo-rpc-rmi/2.7.23/dubbo-rpc-rmi-2.7.23.jar!/META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol, cause: org/springframework/remoting/support/RemoteInvocation
java.lang.IllegalStateException: Failed to load extension class (interface: interface org.apache.dubbo.rpc.Protocol, class line: rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol) in jar:file:/C:/Users/34012/.m2/repository/org/apache/dubbo/dubbo-rpc-rmi/2.7.23/dubbo-rpc-rmi-2.7.23.jar!/META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol, cause: org/springframework/remoting/support/RemoteInvocation
无法加载拓展?不应该啊,我刚才测试还好好的怎么这边就加载不了了呢?
我检查了依赖,代码,都没有问题,反复对比两个项目,也没有发现哪里不对劲。是依赖冲突导致没能正确引入依赖包吗?我想我需要检查一下关键类是否正确加载。
kotlin
package org.wolflink.sharine;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableDiscoveryClient
@EnableDubbo
public class Application {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Class.forName("org.springframework.remoting.support.RemoteInvocation"));
SpringApplication.run(Application.class, args);
}
}
运行后:
csharp
Exception in thread "main" java.lang.ClassNotFoundException: org.springframework.remoting.support.RemoteInvocation
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:375)
at org.wolflink.sharine.Application.main(Application.java:15)
啊?SpringFramework 缺失了相关类???是我应用了某些配置卸载了 Remoting 相关的类吗,不可能啊,Remoting 包听上去像是远程协议相关的,这么重要的东西为什么会缺失?而且刚才在测试项目中明明没有问题的。
仔细检查后,我发现,在 Sharine 中使用的 Spring-Context 版本为 6.x,而测试项目是 5.x,查阅 Spring 官网更新记录后:
真相大白。在 6.x 版本 Spring 不再提供 rmi 相关支持,因为 RMI 存在太多的网络安全漏洞。
最终只好选择 dubbo RPC 协议,删除了 RMI 依赖。
我的思考
上述两件事情,笔者共花了接近两天时间进行排查,实际排查过程中有非常非常多的疑惑没有展示在这篇文章中,遇到的问题远比表面看上去的要多。这也提醒了我,要做大型项目,对于框架选型一定要慎重考虑,稍有不当就会引入数不胜数的新坑。
本来是希望使用 Nacos 的,听说太简单了就换了 Consul ,想试一些国内鲜有人尝试的框架。没想到小小 Consul 竟然暗藏如此之多的玄机,它不仅需要作为服务中心暴露给微服务进行访问,还需要主动访问微服务、配置相关 DNS 等,因此将 Consul 置于容器中并不是一个好的选择,要么将所有项目都部署到 Docker,要么将 Consul 拿出来部署到宿主机中。在一开始我其实是尝试的将所有项目都部署到 Docker,我原本以为项目调试时可以像本地一样方便,实际上错了,大错特错了,这给我带来数不胜数的烦恼。我在编写好某一个微服务希望运行时,我需要进行以下步骤:
构建并安装 common 模块 -> 构建微服务模块 -> 构建 Docker 镜像(会将微服务模块构建的 jar 包拷贝到容器中) -> 重新运行 Docker-Compose 集群
够麻烦吧,这只是测试一次就要花费我好几分钟的时间。最后不得不将 Consul 部署到宿主机,MySQL 和 Redis 仍然留在里面,挺好的。
RPC 框架选型上,我原本是想找一个尽可能轻量的 RPC 框架,例如 github.com/tang-jie/Ne... ,但是这种框架又没有提供与 Consul 集成相关的支持,需要额外暴露一个服务发现的端口,这也并非最佳实践。gRPC、Thrift 等框架呢?因为它们的目标是提供跨语言的服务调用,这些框架都需要编写额外的 proto 文件约定 service,entity 等,非常非常麻烦。
而我的 Sharine 显然是一个纯 Java 的微服务项目,何必大费周章呢?思来想去还是决定用 Dubbo 了,现在才刚解决完上面的问题,后面会遇到什么...我不好说,希望一切顺利吧!