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

相关推荐
mit6.8247 小时前
[Docker#4] 镜像仓库 | 部分常用命令
linux·运维·docker·容器·架构
乌恩大侠11 小时前
了解 Open RAN 架构中的 DU 和 CU
架构
贵州晓智信息科技12 小时前
深入理解 React 架构从概览到核心机制
前端·react.js·架构
W Y12 小时前
【架构论文-1】面向服务架构(SOA)
架构·架构设计
Hello.Reader12 小时前
解析Eureka的架构
云原生·eureka·架构
航火火14 小时前
回首遥望-C++内存对齐的思考
c++·面试·架构
执着码农14 小时前
Tomcat源码解析之架构设计
后端·架构
白总Server17 小时前
Swagger UI
后端·ui·spring cloud·ribbon·架构·scala·1024程序员节
infiniteWei17 小时前
【Lucene】架构概览和核心组件介绍
搜索引擎·架构·全文检索·lucene
未命名冀20 小时前
微服务day06
微服务·云原生·架构