问题与思考 —— 关于 SpringCloud + Dubbo 的微服务架构项目

问题与思考 ------ 关于 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 了,现在才刚解决完上面的问题,后面会遇到什么...我不好说,希望一切顺利吧!

相关推荐
稻草人222211 小时前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构
数据智能老司机12 小时前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
数据智能老司机13 小时前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
bobz96517 小时前
k8s svc 实现的技术演化:iptables --> ipvs --> cilium
架构
云舟吖17 小时前
基于 electron-vite 实现一个 RPA 网页自动化工具
前端·架构
brzhang18 小时前
当AI接管80%的执行,你“不可替代”的价值,藏在这20%里
前端·后端·架构
Lei活在当下1 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计
架构师沉默1 天前
设计多租户 SaaS 系统,如何做到数据隔离 & 资源配额?
java·后端·架构
kfyty7251 天前
不依赖第三方,不销毁重建,loveqq 框架如何原生实现动态线程池?
java·架构
刘立军2 天前
本地大模型编程实战(33)用SSE实现大模型的流式输出
架构·langchain·全栈