跳到主要内容

· 阅读需 14 分钟

本文章主要叙述在 Java 应用适配 Graalvm Native Image 中的步骤和遇到的一些问题!因为 Graalvm 官方文档相关概念叙述过于简单。基本靠问才能知道些许有用信息。所以写此文章。

关于 Graalvm 基础知识的相关学习,可以参照 Seata 社区 commiter 王良的 Blog,本文章主要介绍元数据适配。不对 Graalvm 基础做过多介绍。

Graalvm 现状介绍

GraalVM:是一种高性能运行时,可显着提高应用程序性能和效率,是微服务的理想选择。它专为使用 Java、JavaScript、基于 LLVM 的语言(例如 C 和 C++)以及其他动态语言编写的应用程序而设计。它消除了编程语言之间的隔离,并在共享运行时实现了互操作性。它可以独立运行,也可以在 OpenJDK、Node.js 或 Oracle 数据库的上下文中运行。

Graalvm 主要涉及到 VmWare,RedHat 和 Oracle Labs 三家。RedHat 主要是直接参与 Graalvm 本体,VmWare参与的是 metadata 和 nativeTest,Oracle Labs作为主导方什么都干。

Native Image:是一种提前将 Java 代码编译为二进制文件(本机可执行文件)的技术。本机可执行文件仅包含运行时所需的代码,即应用程序类、标准库类、语言运行时和来自 JDK 的静态链接本机代码。它是 Oracle 官方首推的 Java AoT 解决方案,通过C语言实现了一个超微缩的运行时组件 —— Substrate VM,基本实现了 JVM 的各种特性,但足够轻量、可以被轻松内嵌,这就让 Java 语言和工程摆脱 JVM 的限制,能够真正意义上实现和 C/C++ 一样的 AoT 编译。这一方案在经过长时间的优化和积累后,已经拥有非常不错的效果。

Native Image 工具依赖于在运行时对应用程序可访问代码类的静态分析。但是,分析不能总是完全预测 Java 本机接口 (JNI)、Java 反射、动态代理对象或类路径资源的所有用法。必须以元数据 native-image 的形式(在代码中预先计算或作为 JSON 配置文件)向工具提供未检测到的这些动态功能的使用情况。

在项目中打包二进制文件时,需要使用到 JSON 配置文件,关于怎么获取相关配置和保证获取到的配置正确性,以及如何存放相关的元数据配置文件是目前需要迫切解决的问题!

相关概念

因为本文章主要介绍 Native Image 相关,所以只对在使用 Native Image 相关功能中用到的名词做解释!

插件相关

  1. nbt:graalvm native build tools 插件简称;
  2. tck plugin:Oracle 官方提供涉及元数据检测的 gradle 插件。

配置相关

  1. agent:GraalVM 提供的一个 Tracing Agent 来轻松收集元数据和准备配置文件;
  2. conditional mode:条件代理模式生成带有条件的元数据;
  3. standard mode:agent 默认选项,标准代理模式无条件生成元数据;
  4. direct mode:直接代理模式允许用户直接将选项传递给代理,(涉及到 tck 的诡异单元测试);
  5. experimental-conditional-config-filter-file:被认为包含在这个过滤器中的类将被指定为用户代码类;
  6. conditional-config-class-filter-file:要进一步过滤生成的配置;
  7. access-filter-file:访问过滤器适用于访问的目标。因此,访问过滤器可以直接从生成的配置中排除包和类(及其成员);
  8. caller-filter-file:过滤机制通过识别执行访问的 Java 方法(也称为调用方方法)并将其声明类与一系列过滤规则相匹配来工作;
  9. graalvm reachability metadata:graalvm 官方元数据中央仓库;
  10. imagecode:graalvm 本体的系统属性,相关文档中没有提及,服务于 nbt 的识别。Spring Context Framework NativeTest 靠此属性识别(涉及到 Oracle Labs 希望最小化这种属性判定影响的争议)。
  11. 特别注意的是:目前 access-filter 和 caller-filter 仅对 standard mode 起作用

关于 conditional mode 和 standard mode。,direct mode 只有以下几个仓库依稀记录,其他靠问!

以上相关概念均可在上面的链接中找到相关叙述!

以 Nacos 为例生成元数据文件

maven

graalvm 社区并没有使用 maven 作为插件管理工具,而是使用 gradle。因为 nbt 目前还没有支持 maven 3.9.0+, 同时相比 gradle plugin 存在线程安全问题,尤其是尝试在多模块并行执行 metadatacopy goal 时必定失败!

创建项目

参考 Demo: https://github.com/yuluo-yx/graalvm-demo

项目插件介绍及配置

在 pom.xml 中加入如下配置:

<profiles>
<profile>
<id>native</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>
-agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/,experimental-conditional-config-filter-file=src/main/resources/conditional-filter.json,access-filter-file=src/main/resources/user-code-filter.json
<!-- 被认为包含在这个过滤器中的类将被指定为用户代码类 -->
<!--experimental-conditional-config-filter-file=src/main/resources/user-code-filter.json,-->
<!-- 要进一步过滤生成的配置 -->
<!-- conditional-config-class-filter-file=<path> -->
<!-- 访问过滤器适用于访问的目标。因此,访问过滤器可以直接从生成的配置中排除包和类(及其成员)。 -->
<!--access-filter-file=src/main/resources/conditional-filter.json-->
<!-- 过滤机制通过识别执行访问的 Java 方法(也称为调用方方法)并将其声明类与一系列过滤规则相匹配来工作 -->
<!--caller-filter-file=src/main/resources/-->
</jvmArguments>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>

</configuration>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<!--<agent>
<enabled>true</enabled>
<defaultMode>conditional</defaultMode>
<modes>
<conditional>
<userCodeFilterPath>src/main/resources/user-code-filter.json</userCodeFilterPath>
</conditional>
</modes>
</agent>-->
<classesDirectory>${project.build.outputDirectory}</classesDirectory>
<!-- 需要修改成为自己本地的 元数据仓库地址,不然报错 此配置是为了本地调试方便 -->
<metadataRepository>
<enabled>true</enabled>
<localPath>
E:\Java\apache_maven\apache-maven-3.9.0\repository\org\graalvm\buildtools\graalvm-reachability-metadata\0.9.19\graalvm-reachability-metadata-0.9.19-repository.zip
</localPath>
</metadataRepository>
<requiredVersion>22.3</requiredVersion>
</configuration>
<executions>
<execution>
<id>add-reachability-metadata</id>
<goals>
<goal>add-reachability-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>

项目元数据生成

项目配置完成之后,在 terminal 执行如下命令:

mvn clean -Pnative spring-boot:run

即可在 resource 目录下看到生成的相关元数据文件。

gradle

gradle 对 spring context framework 并不友好,可能跑不通 nativeTest,中央仓库中有带着 spring framework。带着 spring boot 目前没有跑通 agent 的 test!

参考 url:

创建项目

参考 Demo:https://github.com/yuluo-yx/nacos-client-metadata

项目插件介绍及配置

gradle nbt plugin:https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#_introduction

NBT 插件的本质是拼接 shell 命令!

build.gradle 配置文件示例:

// 引入插件
plugins {
id 'java'
id 'org.graalvm.buildtools.native' version '0.9.22'
}

group 'indi.yuluo'
version '1.0-SNAPSHOT'

repositories {
// 配置 maven 中央仓库
maven { url 'https://maven.aliyun.com/repository/public' }
mavenCentral()
gradlePluginPortal()
}

dependencies {
testImplementation 'com.alibaba.nacos:nacos-client:2.2.1'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.awaitility:awaitility:4.2.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

// graalvm native 相关配置
graalvmNative {
agent {
// 设置代理模式
defaultMode = "conditional"
modes {
// 引入 conditional 的过滤文件配置
conditional {
userCodeFilterPath = "metadata-user-code-filter.json"
extraFilterPath = "metadata-extra-filter.json"
}
// conditional 使用不了 access-filter 和 caller-filter 具体自己可以创建 demo
}
metadataCopy {
mergeWithExisting = true
inputTaskNames.add("test")
outputDirectories.add("src/test/resources/META-INF/native-image/com.alibaba.nacos/nacos-jni")
}
}
metadataRepository {
enabled = true
}
// 指定运行参数 如果在 main 中使用 main {} test 使用 test {}
binaries {
test {
buildArgs("--enable-url-protocols=http")
}
}
}

test {
useJUnitPlatform()
}

生成元数据

生成元数据

./gradlew -Pagent clean test

使用如下命令导出元数据文件

./gradlew metadataCopy --task test

清理单元测试

./gradlew clean nativeTest

在生成单元测试的过程中,可能存在生成不完全的情况。当然也可以在maven中生成相关元数据,只要手动删除一些无关的json条目就可以了。最终的结果是能够通过native build tools的nativetest即可。

如果需要一个项目的metadata,就应该为此项目编写单元测试,用单元测试生成最初版本的metadata的json文件。最后手动根据nativetest的error log或warn log对(有些缺少的json条目会表现为nativetest出现死锁)改改json文件的条目

配置文件中 配置项说明

reflect-config.json

{
"name":"com.alibaba.nacos.api.naming.ability.ClientNamingAbility",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[
{"name":"isSupportDeltaPush","parameterTypes":[] },
{"name":"isSupportRemoteMetric","parameterTypes":[] }
]
},

proxy-config.json

 {
"interfaces":["io.seata.config.Configuration"]
},

提交元数据到中央仓库

gradle 提交元数据步骤参考:https://github.com/oracle/graalvm-reachability-metadata/blob/master/CONTRIBUTING.md

以提交 nacos 元数据为例演示!

  1. clone 元数据中央仓库到本地
git clone https://github.com/oracle/graalvm-reachability-metadata.git
  1. 执行如下命令
./gradlew scaffold --coordinates com.alibaba:nacos-client:2.2.1
  1. 填充对应测试类和元数据文件

填充单元测试时,注意需要修改如下内容

plugins {
id 'java'
id 'org.graalvm.buildtools.native' version '0.9.22'
}

修改为:

plugins {
id "org.graalvm.internal.tck"
}
  1. 填充完成之后,执行如下命令进行测试

注意:去 linux 环境测试!!!相关 issue 描述:https://github.com/oracle/graalvm-reachability-metadata/issues/24

  1. 确认测试通过之后,说明针对已有的单元测试,没有缺少的metadata。提交 pr 到中央仓库
nacos-client: https://github.com/oracle/graalvm-reachability-metadata/pull/325
  1. 如果使用到了 docker ,需要创建额外的 required-docker-images.txt 文件,之后需要提交镜像配置到中央仓库的 https://github.com/oracle/graalvm-reachability-metadata/blob/master/tests/tck-build-logic/src/main/resources/AllowedDockerImages.txt中, 不然 github ci 不通过。
https://github.com/oracle/graalvm-reachability-metadata/pull/321
  1. 如果使用虚拟机测试,注意虚拟机网络连接配置
  2. 测试通过示例:

image-20230627112831601.png

值得一提的是,graalvm 不怎么 review 外部 pr,除非你主动催!!Oracle Labs 是经典的四天工作制,周五,周六,周日基本找不到人的!

linux 测试环境搭建步骤:(环境搭建,特别是安装jdk,非常慢

cd /tmp
sudo apt install unzip zip curl sed -y
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 22.3.1.r17-grl
sdk use java 22.3.1.r17-grl
gu install native-image
sudo apt-get install build-essential libz-dev zlib1g-dev -y

sdk install gradle

踩坑记录

  1. 到目前为止,graalvm 提供的获取元数据方式任然存在有不包含指定类和缺少类的情况出现,需要手动补充,可参见 pr: https://github.com/oracle/graalvm-reachability-metadata/pull/167。优化此插件体验一直是 graalvm 讨论的问题之一。

  2. gradle 错误:

 [java.net.MalformedURLException: Accessing an URL protocol that was not enabled. The URL protocol http is
supported but not enabled by default. It must be enabled by adding the --enable-url-protocols=http option to the native-image command.]

需要加入相关配置:(作用给test文件夹的就是test buildargs,作用给main文件夹的就是main buildargs,在binary不同子级下)

   graalvmNative {
agent {
defaultMode = "conditional"
modes {
conditional {
userCodeFilterPath = "metadata-user-code-filter.json"
extraFilterPath = "metadata-extra-filter.json"
}
}
}
binaries {
test {
buildArgs("--enable-url-protocols=http")
}
}
}

相关URL参考地址

Graalvm Slack:https://app.slack.com/client/TN37RDLPK/CNBFR78F9

Graalvm 元数据中央仓库:https://github.com/oracle/graalvm-reachability-metadata

nbt 插件地址:https://github.com/graalvm/native-build-tools

graalvm blog:https://easyj.icu/blog/#/native-image/theory-practice

致谢

感谢 Apache ShardingSphere 社区 commiter 泠恒谦的指导

感谢 Seata 社区 commiter 王良 的 graalvm 相关知识介绍

· 阅读需 7 分钟

GitHub issue 参见:https://github.com/alibaba/spring-cloud-alibaba/issues/3101

经验教训

  • GraalVM Tracing Agent 收集到的信息可能不完整,所以依据这些信息编译出来的镜像运行时依然会报错,有时候需要手动补充 reflect-config.json中的内容。

适配过程

  1. fastjson 需要升级到 fastjson2 才支持 GraalVM,GraalVM 下不能用字节码做优化,走的是反射。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.22</version>
</dependency>
  1. pom.xml 中需要增加 native-maven-plugin这个插件并添加相应的配置
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<classesDirectory>${project.build.outputDirectory}</classesDirectory>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
<buildArgs>
<arg>--initialize-at-build-time=org.apache.commons.logging.LogFactoryService</arg>
</buildArgs>
<quickBuild>true</quickBuild>
</configuration>
</plugin>
  1. 由于 RocketMQ 大量使用 fastjson ,其核心的几个类对象大量会被反射调用,因此需要在 reflect-config.json 配置文件中,针对这几个核心类对象增加如下配置

以下配置是给 rocketmq-broadcast-producer-example这个用例中需要用到的。

{
"name":"org.apache.rocketmq.remoting.protocol.RemotingCommand",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"org.apache.rocketmq.common.protocol.route.BrokerData",
"allDeclaredFields":true,
"allPublicMethods":true,
"allPublicConstructors":true
},
{
"name":"org.apache.rocketmq.common.protocol.route.TopicRouteData",
"allDeclaredFields":true,
"allPublicMethods":true,
"allPublicConstructors":true
},
{
"name":"org.apache.rocketmq.common.protocol.route.QueueData",
"allDeclaredFields":true,
"allPublicMethods":true,
"allPublicConstructors":true
},

以下配置是给 rocketmq-broadcast-consuemr1-example这个用例中需要用到的。

{
"name":"org.apache.rocketmq.common.protocol.heartbeat.HeartbeatData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.ConsumerData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.ProducerData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData",
"allDeclaredFields":true,
"allPublicMethods":true
},

解释如下

  • allDeclaredFields 代码所声明的对象中的所有成员变量都可以被反射调用,包括 public 和 protected private 的成员变量
  • allPublicFields 代表所声明的对象中的所有 public 成员变量都可以被反射调用
  • allPublicMethods代码所声明的对象中所有的 public 方法都可以被反射调用
  • allPublicConstructors代码所声明的对象中所有的 public 的构造函数都可以被反射调用

以此类推。 当然,我们也可以声明某个具体的方法可以被反射调用,但是考虑到这几个核心对象的被反射的概率较大,而且方法和成员变量也不对,因此声明成所有都可以被反射。

graalvm-reachability-metadata

GraalVM 在编译的时候需要 reflect-json.config等一系列的 hint 文件来编译 native-image,但通过 Tracing Agent 收集的方式可能会有遗漏,最好的方式是让每个中间件自己提供一份文件出来,这份文件中的 hint 信息是经过充分测试,这样也是最可靠的。最好的方式是中间件提供的 jar 里面就自带了这份 hint 文件,但这样要求所有中间件要重新发布一个新版本,针对老的版本如果提供这样的信息呢?graalvm 提供了这样一个机制,也就是 graalvm-reachability-metadata 把这些信息放在一个外部的仓库中,在编译的时候会从这个仓库中拉取所需要的编译信息。

此处演示如果通过本地仓库的方式添加这个信息,可以再插件配置中,主要是在 12 行处,加入本地仓库的地址

<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<arg>--initialize-at-build-time=org.apache.commons.logging.LogFactoryService</arg>
</buildArgs>
<quickBuild>true</quickBuild>
<debug>true</debug>
<metadataRepository>
<enabled>true</enabled>
<localPath>/Users/wangtao/.m2/repository/org/graalvm/buildtools/graalvm-reachability-metadata/0.9.19</localPath>
</metadataRepository>
</configuration>
</plugin>

在这个仓库下创建如下目录格式

$ tree org.apache.rocketmq
org.apache.rocketmq
└── rocketmq-client
├── 4.9.5-SNAPSHOT
│   ├── index.json
│   ├── jni-config.json
│   ├── predefined-classes-config.json
│   ├── proxy-config.json
│   ├── reflect-config.json
│   ├── resource-config.json
│   └── serialization-config.json
└── index.json

其中 第12行的 index.json 内容如下

[
{
"latest": true,
"metadata-version": "4.9.5-SNAPSHOT",
"module": "org.apache.rocketmq:rocketmq-client",
"tested-versions": [
"4.9.5-SNAPSHOT"
]
}
]

org/apache/rocketmq/rocketmq-client/4.9.5-SNAPSHOT/index.json 内容如下:


$ cat org.apache.rocketmq/rocketmq-client/4.9.5-SNAPSHOT/index.json
[
"reflect-config.json",
"resource-config.json"
]

其中比较核心的是 reflect-config.json内容如下

[

{
"name":"org.apache.rocketmq.client.consumer.store.OffsetSerializeWrapper",
"allDeclaredFields":true,
"allPublicFields":true,
"queryAllPublicMethods":true,
"methods":[{"name":"getOffsetTable","parameterTypes":[] }]
},
{
"name":"org.apache.rocketmq.common.message.MessageQueue",
"allDeclaredFields":true,
"allPublicFields":true,
"queryAllPublicMethods":true,
"methods":[
{"name":"getBrokerName","parameterTypes":[] },
{"name":"getQueueId","parameterTypes":[] },
{"name":"getTopic","parameterTypes":[] }
]
},
{
"name":"org.apache.rocketmq.common.protocol.header.GetMaxOffsetRequestHeader",
"allDeclaredFields":true
},
{
"name":"org.apache.rocketmq.common.protocol.header.GetMaxOffsetResponseHeader",
"allDeclaredFields":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.apache.rocketmq.common.protocol.header.NotifyConsumerIdsChangedRequestHeader",
"allDeclaredFields":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader",
"allDeclaredFields":true
},
{
"name":"org.apache.rocketmq.common.protocol.header.PullMessageResponseHeader",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.apache.rocketmq.common.protocol.header.SendMessageRequestHeaderV2",
"allDeclaredFields":true
},
{
"name":"org.apache.rocketmq.common.protocol.header.SendMessageResponseHeader",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.apache.rocketmq.common.protocol.header.UnregisterClientRequestHeader",
"allDeclaredFields":true
},
{
"name":"org.apache.rocketmq.common.protocol.header.namesrv.GetRouteInfoRequestHeader",
"allDeclaredFields":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.ConsumerData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.HeartbeatData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.ProducerData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData",
"allDeclaredFields":true,
"allPublicMethods":true
},
{
"name":"org.apache.rocketmq.common.protocol.route.BrokerData",
"allDeclaredFields":true,
"allPublicMethods":true,
"allPublicConstructors":true
},
{
"name":"org.apache.rocketmq.common.protocol.route.QueueData",
"allDeclaredFields":true,
"allPublicMethods":true,
"allPublicConstructors":true
},
{
"name":"org.apache.rocketmq.common.protocol.route.TopicRouteData",
"allDeclaredFields":true,
"allPublicMethods":true,
"allPublicConstructors":true
},
{
"name":"org.apache.rocketmq.remoting.netty.NettyDecoder"
},
{
"name":"org.apache.rocketmq.remoting.netty.NettyEncoder"
},
{
"name":"org.apache.rocketmq.remoting.netty.NettyRemotingClient$4"
},
{
"name":"org.apache.rocketmq.remoting.netty.NettyRemotingClient$NettyClientHandler"
},
{
"name":"org.apache.rocketmq.remoting.netty.NettyRemotingClient$NettyConnectManageHandler",
"methods":[
{"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] },
{"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] },
{"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] },
{"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] },
{"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }
]
},
{
"name":"org.apache.rocketmq.remoting.protocol.LanguageCode",
"fields":[
{"name":"CPP"},
{"name":"DELPHI"},
{"name":"DOTNET"},
{"name":"ERLANG"},
{"name":"GO"},
{"name":"HTTP"},
{"name":"JAVA"},
{"name":"OMS"},
{"name":"OTHER"},
{"name":"PHP"},
{"name":"PYTHON"},
{"name":"RUBY"},
{"name":"RUST"}
]
},
{
"name":"org.apache.rocketmq.remoting.protocol.RemotingCommand",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"org.apache.rocketmq.remoting.protocol.RemotingSerializable",
"allDeclaredFields":true,
"queryAllPublicMethods":true
},
{
"name":"org.apache.rocketmq.remoting.protocol.SerializeType",
"fields":[
{"name":"JSON"},
{"name":"ROCKETMQ"}
]
}
]

这个内容是基于 Tracing Agent 收集的信息,加上手工补充的内容,形成的一个相对完整 hint 文件。GraalVM 在编译的时候会查找这个仓库,并使用这里面的信息编译 native image。在测试完成之后,可以提交到远程仓库中,https://github.com/oracle/graalvm-reachability-metadata

常见问题

如果运行时遇到以下异常:

Caused by: com.oracle.svm.core.jdk.UnsupportedFeatureError: Runtime reflection is not supported for public org.apache.rocketmq.common.protocol.route.TopicRouteData()
at org.graalvm.nativeimage.builder/com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:89)
at java.base@17.0.5/java.lang.reflect.Constructor.acquireConstructorAccessor(Constructor.java:68)
at java.base@17.0.5/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:496)
at java.base@17.0.5/java.lang.reflect.ReflectAccess.newInstance(ReflectAccess.java:128)
at java.base@17.0.5/jdk.internal.reflect.ReflectionFactory.newInstance(ReflectionFactory.java:347)
at java.base@17.0.5/java.lang.Class.newInstance(DynamicHub.java:645)
at com.alibaba.fastjson2.reader.ConstructorSupplier.get(ConstructorSupplier.java:27)
at com.alibaba.fastjson2.reader.ObjectReader4.readObject(ObjectReader4.java:309)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:496)
... 16 more

说明 fastjson 运行的时候进行了反射,调用了 public org.apache.rocketmq.common.protocol.route.TopicRouteData() 这个方法,但是 Tracing Agent 并没有收集到这个方法,导致编译 native image 的时候并没有把这个方法允许进行反射,所以就报错了,此时需要修改 reflect-config.json文件,讲该方法加入到运行反射的列表当中。

· 阅读需 11 分钟

Spring Cloud Alibaba项目简介

Spring Cloud Alibaba(下文简称为SCA) 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。 依托 SCA,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里分布式应用解决方案,通过阿里中间件来迅速搭建分布式应用系统。

项目最佳实践案例介绍

SCA的项目的最佳实践,是整合了SCA相关组件(Nacos,Sentinel,Seata,RocketMQ)的Example示例项目。方便用户全面体验SCA提供的一站式微服务解决方案。

  1. Spring Cloud Gateway: 网关
  2. Nacos:服务注册和配置中心
  3. Sentinel:熔断限流
  4. Seata:分布式事务
  5. RocketMQ:消息队列,削峰填谷
  6. Docker:使用Docker进行容器化部署
  7. Kubernetes:使用k8s进行容器化部署

下图为SCA最佳实践项目结构示意图:

image.png

组件详细说明

  1. 其中,用户下单购买货物的场景主要使用 Seata 来进行分布式事务的能力体现。
  2. 用户为商品进行点赞的场景,模拟大流量环境下通过 Sentinel 进行限流或是 RocketMQ 进行削峰填谷。在此场景下,SCA社区提供了两种应对大流量的处理方式:
    • Sentinel 在网关侧绑定指定网关路由进行服务的熔断降级。
    • RocketMQ 进行流量削峰填谷,在大流量请求下,生产者向 RocketMQ 发送消息,而消费者则通过可配置的消费速率进行拉取消费,减少大流量直接请求数据库增加点赞请求的压力。

Spring Cloud Gateway

Spring Cloud GateWay 是微服务模块的网关,整合 Nacos,实现动态路由的配置。通过监听 Nacos 配置的改变,实现服务网关路由配置动态刷新,每次路由信息变更,无需修改配置文件而后重启服务。

Nacos

Nacos 是SCA微服务模块的服务注册中心和配置中心。整合 Spring Cloud Gateway,所有的微服务模块都注册到 Nacos 中进行服务注册与发现。

Seata

基于 Seata 的 AT 模式,用于库存模块,账户模块,订单模块的分布式事务处理。当库存不足/账户余额不足时,进行事务回滚。

Sentinel

用于点赞场景的服务熔断限流。整合 Nacos 配置中心与 Spring Cloud Gateway,实现指定路由规则、熔断限流规则动态配置。

RocketMQ

用于进行点赞服务流量的削峰填谷。通过将大流量的点赞请求从生产者发送到mq,消费者模块从mq中拉取进行一定频率的消费,不是简单的直接服务熔断限流降级,实现 RocketMQ 针对大流量的削峰填谷能力。

应用场景说明

在本 Demo 示例中,SCA社区提供了两种业务场景:

  1. 用户下单购买货物的场景,下单后:

先请求库存模块,扣减库存; 扣减账户余额; 生成订单信息返回响应。

  1. 用户为商品进行点赞(模拟MQ的生产者消费者应用场景)返回商品点赞后的详细信息(点赞数等)。

Docker Compose 部署

容器化部署是将应用代码和所需的所有组件(例如库、框架和其他依赖项)打包在一起, 让它们隔离在自己的"容器"中去运行。 Note:如果以Docker Compose方式体验Demo时,需要确保内存资源 >= 24G!

Hosts 配置

在启动服务容器前,请先配置 Host 地址映射,确保服务能够正常启动!

# for integrated-example
127.0.0.1 integrated-mysql
127.0.0.1 nacos-server
127.0.0.1 seata-server
127.0.0.1 rocketmq
127.0.0.1 gateway-service
127.0.0.1 integrated-frontend

启动容器

组件容器启动

进入spring-cloud-alibaba-examples/integrated-example目录下,执行 docker-compose -f docker-compose-env.yml up -d 即可启动所有微服务应用依赖的组件,即Nacos,MySQL,RocketMQ,Seata-Server,完成需要的数据库表配置等相关操作。

添加应用配置信息

spring-cloud-alibaba-examples/integrated-example目录下,在终端中执行config-init/scripts/nacos-config-quick.sh脚本文件。即可自动注入应用的所有配置信息。

服务容器启动

spring-cloud-alibaba-examples/integrated-example目录下,执行以下命令docker-compose -f ./docker-compose/docker-compose-service.yml up -d来快速部署example应用。

访问 Demo

分布式事务能力

访问http://integrated-frontend:30080/order 来体验对应场景。 直接点击下单按钮提交表单,模拟客户端向网关发送了一个创建订单的请求。

  • 用户的 userId 为 admin
  • 用户下单的商品编号为1号
  • 此次订单购买的商品个数为1个

image.png

在本 Demo 示例中,为了便于演示,每件商品的单价都为2。 而在初始化业务数据库表的时候新建了一个用户,用户的userId为admin,余额为 3 元;同时新建了一个编号为 1 号的商品,库存为 100 件。 因此通过上述的操作,应用会创建一个订单,扣减对应商品编号为 1 号的库存个数(100-1=99),扣减 admin 用户的余额(3-2=1)。

image.png

如果再次请求相同的接口,同样是先扣减库存(99-1=98),但是会因为 admin 用户余额不足而抛出异常,并被 Seata 捕获,执行分布式事务二阶段提交,回滚事务。

image.png

可以看到数据库中库存的记录因为回滚之后仍然为 99 件。

熔断限流,削峰填谷能力
  1. 场景说明

    针对大流量背景下的服务熔断限流,削峰填谷,SCA社区提供了用户为商品进行点赞的场景。在此场景下,SCA社区提供了两种应对大流量的处理方式。

  • Sentinel 在网关侧绑定指定网关路由进行服务的熔断降级。
  • RocketMQ 进行流量削峰填谷,在大流量请求下,生产者向 RocketMQ 发送消息,而消费者则通过可配置的消费速率进行拉取消费,减少大流量直接请求数据库增加点赞请求的压力。
  1. 访问测试
  • Sentinel 熔断降级

访问 http://integrated-frontend:30080/sentinel 体验对应场景。

image.png

网关路由点赞服务的限流规则为 5,而在前端通过异步处理模拟了 10 次并发请求。 因此可以看到 Sentinel 在 Gateway 侧针对多出的流量进行了服务熔断返回 fallback 给客户端,同时数据库的点赞数进行了更新(+5)。

  • RocketMQ进行流量削峰填谷

访问 http://integrated-frontend:30080/rocketmq 体验对应场景。

image.png

由于之前在 Nacos 中配置了integrated-praise-consumer消费者模块的消费速率以及间隔,在点击按钮时应用将会模拟 1000 个点赞请求,针对 1000 个点赞请求,integrated-praise-provider 会将 1000 次请求都向 Broker 投递消息,而在消费者模块中会根据配置的消费速率进行消费,向数据库更新点赞的商品数据,模拟大流量下 RocketMQ 削峰填谷的特性。 可以看到数据库中点赞的个数正在动态更新。

image.png

服务容器停止

spring-cloud-alibaba-examples/integrated-example 目录下,执行以下命来停止正在运行的example服务容器。

docker-compose -f ./docker-compose/docker-compose-service.yml down

组件容器停止

spring-cloud-alibaba-examples/integrated-example 目录下,执行以下命令 docker-compose -f ./docker-compose/docker-compose-env.yml down 来停止正在运行的example组件容器。

更多信息请参考:Spring Cloud Alibaba容器化部署最佳实践 | Docker-Compose 版本

Kubernetes Helm 部署

启动测试

进入到 spring-cloud-alibaba-examples/integrated-example 目录下,执行如下命令利用 Helm 部署应用程序。

helm package helm-chart

helm install integrated-example integrated-example-1.0.0.tgz

通过运行上述命令,根据SCA社区提供的 Helm Chart 文档通过 Helm 快速完成最佳实践示例的部署。 可以通过 Kubernetes 提供的 kubectl 命令查看各容器资源部署的情况,耐心等待所有容器完成启动后即可到对应页面体验各个组件的使用场景及能力。

体验Demo

同 Docker Compose Demo 体验过程

停止测试

如果您想停止体验,输入如下命令。

helm uninstall integrated-example

更多信息请参考:Spring Cloud Alibaba容器化部署最佳实践 | Kubernetes Helm-Chart 版本

· 阅读需 6 分钟

Spring Boot 单体应用升级 Spring Cloud 微服务

通过以下示例,我们完整的演示了一个 Spring Boot 架构的单体应用集群,如何平滑的升级为一个 Spring Cloud 微服务集群,本文章包含源码、讲解、原理说明。

  1. 原始 Spring Boot 应用架构

在示例中,我们有如下基于 Spring Boot 开发的应用架构:

spring boot

我们这里列出来的只是一种示例架构。基于 Spring Boot 构建的应用架构变化多样,比如可能如下一些常用的架构,但不论哪种架构,升级 Spring Cloud 的大致改造方式都是类似的(都可以转为基于 Nacos 注册中心的地址发现与负载均衡)。

  • 基于 DNS 自定义域名,服务间的通过域名调用实现负载均衡
  • 基于 SLB 的中心化流量转发,调用直接转发到 SLB,由 SLB 实现在服务间实现流量转发
  • 基于 Kubernetes Service 微服务体系,依赖 Kubernetes ClusterIP 等实现负载均衡与调用转发
  1. 升级后的 Spring Cloud Alibaba 应用架构

我们将以上示例全部改造为 Spring Cloud 应用,改造后的架构如下:

spring cloud

新架构基于 Spring Cloud Service Discovery 机制实现地址自动发现与负载均衡,使用 Nacos 作为注册中心。

示例 Spring Boot 应用

示例包含 spring-boot-A(service-a) 和 spring-boot-B(service-b)两个应用(微服务),应用之间依赖 dns 域名完成互相调用。

spring boot

因此,要完整的运行示例,我们首先需要在本地 /etc/hosts 配置域名映射:

127.0.0.1 service-b.example.com
127.0.0.1 service-a.example.com

依次启动 spring-boot-B(service-b)、spring-boot-A(service-a) 应用,使用以下命令验证应用正常工作:

$ curl http://service-a.example.com:18000/test
Get result from service B.

示例 Spring Cloud 应用

spring cloud

接下来,我们分步骤将 Spring Boot A 应用和 Spring Boot B 应用改造为 Spring Cloud 应用。

改造应用B

首先将 Spring Boot B 应用进行改造,接入 Spring Cloud Nacos Registry

第一步:添加依赖

<properties>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>

第二步:完善配置文件

spring:
application:
name: service-b #项目名称必填,在注册中心唯一;最好和之前域名保持一致,第四步会讲到原因
cloud:
nacos:
discovery: #启用 spring cloud nacos discovery
server-addr: 127.0.0.1:8848

第三步:启动类注解

@SpringBootApplication
@EnableDiscoveryClient
public class SpringBootBApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootBApplication.class, args);
}
}

改造应用A

第一步:添加依赖

<properties>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 服务发现:OpenFeign服务调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>

第二步:配置文件

spring:
application:
name: service-a #项目名称必填,在注册中心唯一;最好和之前域名保持一致,第四步会讲到原因
cloud:
nacos:
discovery: #启用 spring cloud nacos discovery
server-addr: 127.0.0.1:8848

第三步:启动类注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SpringBootAApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootAApplication.class, args);
}
}

第四步:调整调用方式

改造后的应用我们要使用 Nacos 注册中心来做地址发现,并使用 Loadbalancer 来实现负载均衡。

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@RestController
public class AController {
@Autowired
private RestTemplate restTemplate;
private static final String SERVICE_PROVIDER_ADDRESS = "http://service-b";

@GetMapping("/test")
public String callServiceB() {
return restTemplate.getForObject(SERVICE_PROVIDER_ADDRESS +"/test-service-b", String.class);
}
}

增加 Gateway 网关

依赖

<properties>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 服务发现:OpenFeign服务调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 整合nacos需要添加负载均衡依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

启动类注解

@SpringBootApplication
@EnableDiscoveryClient
public class SpringGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SpringGatewayApplication.class, args);
}
}

路由配置

spring:
cloud:
gateway:
routes:
- id: service-a
uri: lb://service-a
predicates:
- Path=/service-a/test/**
filters:
- StripPrefix=1

- id: service-b
uri: lb://service-b
predicates:
- Path=/service-b/test-service-b/**
filters:
- StripPrefix=1

启动与验证

在本地 /etc/hosts 配置如下 hostname 映射

127.0.0.1  service.example.com

依次启动 A、B、Gateway 三个应用。

访问:http://service.example.com/service-a/test-servicer-b

可以看到,请求正常被转发到 service-a、service-b,并实现多实例地址自动发现、负载均衡。

· 阅读需 10 分钟

SCG 修改请求/响应

Spring Cloud Gateway(以下简称为 SCG) 中,当我们需要对 HTTP 请求或响应进行修改时,SCG 提供了许多内置的 GatewayFilter 来满足我们对这种应用场景的需求,例如 AddRequestHeader,AddRequestParameter, DedupeResponseHeader,MapRequestHeader, ModifyRequestBody 等。 考虑以下简单用例:

  • 添加请求头部 X-First,值从请求路径中获取,例如从 /response-headers?testKey=testValue 中获取 "response-headers";
  • 将请求头部 X-First 的值映射给 X-Second;
  • 添加请求查询参数 k1=v1;
  • 剔除重复的响应头部 X-Dedupe。

在 SCG 中使用 GatewayFilter 我们可以这样配置:

# application.yaml:

spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://httpbin-svc
predicates:
- Path=/{api}/**
filters:
- AddRequestHeader=X-First, {api}
- MapRequestHeader=X-First, X-Second
- AddRequestParameter=k1, v1
- DedupeResponseHeader=X-Dedupe, RETAIN_FIRST

相信拥有 SCG 使用经验的同学对上述配置一定不陌生,那么本文将重点给出另一种能够满足上述需求并且性能更加优越的解决方案——使用 Higress 云原生网关的 Transformer 插件。

Higress 插件与 SCG 性能比较

我们在同一吞吐量水平(QPS)上,开启/关闭 Higress Transformer 插件和 SCG 相应 GatewayFilters,统计两者在 CPU 和内存资源上的开销。 经过测试,我们得到的结论是:

  • 在 Higress 未启用 Transformer 插件,SCG 未启用 GatewayFilters 的条件下,SCG 的 CPU, 内存资源开销分别约为 Higress的 3.30, 4.88倍;
  • 在 Higress 启用 Transformer 插件,SCG 启用相应 GatewayFilters 的条件下,SCG 的 CPU,内存资源开销分别约为 Higress 的 2.98, 3.19倍。

image image

可见 Higress Transformer 相比于 SCG GatewayFilter 有着相当不错的性能表现!

接下来我们将进一步为大家介绍 Higress 云原生网关以及上述提到的 Higress Transformer 插件。

Higress 简介

Higress是基于阿里内部的 Envoy Gateway 实践沉淀、以开源 Istio + Envoy 为核心构建的下一代云原生网关,实现了流量网关+微服务网关+安全网关三合一的高集成能力,深度集成 Dubbo、Nacos、Sentinel 等微服务技术栈,能够帮助用户极大地降低网关的部署及运维成本且能力不打折;在标准上全面支持 Ingress 与 Gateway API,积极拥抱云原生下的标准 API 规范;同时,Higress Controller 也支持 Nginx Ingress 平滑迁移,帮助用户零成本快速迁移到 Higress。 Higress 提供了一套 Wasm (WebAssembly) SDK,使得开发者能够轻松使用 C++,Golang,Rust 开发 Wasm 插件增强网关能力。下面将为大家介绍 Higress Transformer 插件的基本功能,最后简单说明 Transformer 插件的核心代码逻辑。

image

Transformer 插件介绍

Higress Transformer 插件可以对请求/响应头部、请求查询参数、请求/响应体参数进行转换,支持的转换操作类型包括删除(remove)、重命名(rename)、更新(replace)、添加(add)、追加(append)、映射(map)和去重(dedupe)。 接下来我们复现最开始提到的 SCG GatewayFilter 简单用例,来演示如何使用该插件(以下使用 Higress 控制台可以很方便地部署插件,当然也可以使用 K8s YAML Manifests 的方式):

  1. 首先根据官方文档快速安装 Higress,结果如下:
$ kubectl -n higress-system get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
higress-console 1/1 1 1 1d
higress-console-grafana 1/1 1 1 1d
higress-console-prometheus 1/1 1 1 1d
higress-controller 1/1 1 1 1d
higress-gateway 1/1 1 1 1d
  1. 通过 Higress 控制台添加域名(foo.bar.com)、路由配置(foo),将流量转发至后端的 httpbin 服务:

image image

  1. 为 foo 路由添加 Transformer 插件(当前未推送插件至官方镜像仓库,可以先使用 docker.io/weixinx/transformer:v0.1.0,或到代码仓库自行构建):

image

注:为了能够同时完成请求和响应转换的需求,我们需要为 foo 路由再添加一个 Transformer 插件,命名为 transformer-resp,用于处理响应方向。

  1. 添加 Transformer 配置并开启该插件:
  • 添加请求头部 X-First,值从请求路径中获取,例如从 /response-headers?testKey=testValue 中获取 "response-headers";
  • 将请求头部 X-First 的值映射给 X-Second;
  • 添加请求查询参数 k1=v1;
  • 剔除重复的响应头部 X-Dedupe。
# transformer:
type: request # 指定 Transformer 类型
rules: # 指定转换规则
- operate: add # 指定转换操作类型
headers: # 指定头部转换规则
- key: X-First
value: $1 # 正则表达式捕获组 $1,支持 RE2 语法
path_pattern: ^\/(\w+)[\?]{0,1}.*$
querys: # 指定查询参数转换规则
- key: k1
value: v1
- operate: map
headers:
- key: X-First
value: X-Second
---
# transformer-resp:
type: response
rules:
- operate: dedupe
headers:
- key: X-Dedupe
value: RETAIN_FIRST
  1. 发送请求进行测试:
# 验证请求方向转换
$ curl -v -H "host: foo.bar.com" "console.higress.io/get"
...
>
< HTTP/1.1 200 OK
...
<
{
"args": {
# 添加了查询参数 k1=v1
"k1": "v1"
},
"headers": {
...
"X-First": "get", # 添加了请求头部 X-First,值 "get" 来自请求路径
"X-Second": "get" # 映射了请求头部 X-Second
},
...
# 添加了查询参数 k1=v1
"url": "http://foo.bar.com/get?k1=v1"
}


# 验证响应方向转换
$ curl -v -H "host: foo.bar.com" \
"console.higress.io/response-headers?X-Dedupe=1&X-Dedupe=2&X-Dedupe=3"
...
>
< HTTP/1.1 200 OK
< x-dedupe: 1 # 保留了响应头部 X-Dedupe 的第一个值
...
<
{
...
# 通过查询参数传给 httpbin 的自定义响应头部
"X-Dedupe": [
"1",
"2",
"3"
],
...
}

❗️需要注意的是:

  • 与上述例子相同,若有同时处理请求和响应转换的需求,则需要为相应路由添加两个 Transformer 插件,分别处理请求方向和响应方向(正在优化);
  • 请求体支持的 Content-Type 有:application/json,application/x-www-form-urlencoded,multipart/form-data;而响应体仅支持 application/json;
  • 更多说明详见插件文档[3]

Transformer 逻辑

本节将简单说明 Higress Transformer 插件的核心代码逻辑,希望可以为有兴趣优化该插件或进行二次开发的同学提供一些帮助。

首先该插件代码位于Higress 仓库的 plugins/wasm-go/extensions/transformer 目录下,使用 Higress 提供的 Wasm SDK[5]进行开发(关于如何开发 Wasm 插件详见官方文档)。

插件的配置模型 TransformerConfig:


# 模型以插件配置的形式暴露给用户
type TransformerConfig struct {
typ string # Transformer 类型,[request, response]
rules []TransformRule # 转换规则

trans Transformer # Transformer 实例,不对用户暴露配置,用于实际的转换操作
}

type TransformRule struct {
operate string # 转换操作类型
headers []Param # header 参数
querys []Param # query 参数
body []Param # body 参数
}

type Param struct {
key string # 表示字段的 key
value string # 表示字段的 value 或 key (map) 或 strategy (dedupe)
valueType string # 为 application/json body 指定 value 的数据类型
hostPattern string # host 正则匹配模式
pathPattern string # path 正则匹配模式
}

其中 Transformer 作为接口分别有请求和响应两个实现(requestTransformer, responseTransformer),主要实现了 3 个接口方法 TransformHeaders,TransformerQuerys 和 TransformBody:


type Transformer interface {
TransformHeaders(host, path string, hs map[string][]string) error
TransformQuerys(host, path string, qs map[string][]string) error
TransformBody(host, path string, body interface{}) error
...
}

var _ Transformer = (*requestTransformer)(nil)
var _ Transformer = (*responseTransformer)(nil)

由于头部(Headers)和查询参数(Querys)都是以 key-value 的形式存在,因此通过 kvHandler 对两者采用统一的处理逻辑;而 Body 由于请求、响应支持不同的 Content-Type,因此分别通过 requestBodyHandler (kvHandler,jsonHandler 组合)和 responseBodyHandler (jsonHandler) 进行处理。综上,在修改该插件逻辑时,主要对kvHandler 和 jsonHandler 进行修改即可,其中 jsonHandler 依赖 GJSONSJSON 工具库。

image

目前 handler 中的转换顺序是被硬编码的(remove -> rename -> replace -> add -> append -> map -> dedupe),我们对此有优化的打算,也欢迎感兴趣的同学参与进来 ~

总结

本文带大家了解了 Higress Transformer 插件,并与 Spring Cloud Gateway 进行了性能比较,在文章的最后还说明了该插件的核心代码逻辑,希望能够为大家从 Spring Cloud Gateway 迁移至 Higress 提供帮助!

image

如果您觉得 Higress 对您有帮助,欢迎前往 Github: Higress为我们 star⭐️ 一下!期待与您在 Higress 社区相遇 ~

· 阅读需 6 分钟

基于 Higress 网关实现 Spring Cloud 服务发现与路由

使用 Nacos 做注册中心

应用配置具体参考 Nacos-Spring-Cloud-快速开始 进行应用配置

不指定命名空间

如果 application.properties 中没有指定 Nacos 命名空间,例如:

server.port=8080
spring.application.name=my-service

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

则 Higress 的 McpBridge 中亦无需指定命名空间:

apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: default
namespace: higress-system
spec:
registries:
# 定义一个名为 my-nacos 的服务来源
- name: my-nacos
# 注册中心类型是 Nacos 2.x,支持 gRPC 协议
type: nacos2
# 注册中心的访问地址,可以是域名或者IP
domain: 127.0.0.1
# 注册中心的访问端口,Nacos 默认都是 8848
port: 8848
# Nacos 服务分组
nacosGroups:
- DEFAULT_GROUP

配置 Ingress 转发到这个服务(假设 /api 前缀的路由都转发给这个服务)需要做如下配置:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: my-service.DEFAULT-GROUP.public.nacos
name: demo
namespace: default
spec:
rules:
- http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /api
pathType: Prefix

注意这里通过注解 higress.io/destination 指定路由最终要转发到的目标服务。

对于 Nacos 来源的服务,这里的目标服务格式为:“服务名称.服务分组.命名空间 ID.nacos”,注意这里需要遵循 DNS 域名格式,因此服务分组中的下划线 '_' 被转换成了横杠 '-'。命名空间未指定时,这里默认值为 "public"。

指定命名空间、服务分组等信息

如果 application.properties 中指定了 Nacos 命名空间,服务分组等信息,例如:

server.port=8080
spring.application.name=my-service

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.namespace=d8ac64f3-xxxx-xxxx-xxxx-47a814ecf358
spring.cloud.nacos.discovery.group=custom-group

则 Higress 的 McpBridge 做相应配置即可

apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: default
namespace: higress-system
spec:
registries:
# 定义一个名为 my-nacos 的服务来源
- name: my-nacos
# 注册中心类型是 Nacos 2.x,支持 gRPC 协议
type: nacos2
# 注册中心的访问地址,可以是域名或者IP
domain: 127.0.0.1
# 注册中心的访问端口,Nacos 默认都是 8848
port: 8848
# Nacos 命名空间 ID
nacosNamespaceId: d8ac64f3-xxxx-xxxx-xxxx-47a814ecf358
# Nacos 服务分组
nacosGroups:
- custom-group

配置 Ingress 转发到这个服务(假设 /api 前缀的路由都转发给这个服务)需要做如下配置:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: my-service.custom-group.d8ac64f3-xxxx-xxxx-xxxx-47a814ecf358.nacos
name: demo
namespace: default
spec:
rules:
- http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /api
pathType: Prefix

使用 ZooKeeper 做注册中心

使用 Zookeeper 做注册中心时,注意必须配置 spring.cloud.zookeeper.discovery.preferIpAddress=true,否则注册到注册中心中的地址为主机名称,而不是 IP。

不指定注册根路径

如果 application.properties 中未指定注册根路径信息,例如:

spring.application.name=my-service
spring.cloud.zookeeper.connect-string=127.0.0.1:2181
spring.cloud.zookeeper.discovery.preferIpAddress=true
spring.cloud.zookeeper.discovery.enabled=true
spring.cloud.zookeeper.discovery.register=true

则 Higress 的 McpBridge 中亦无需指定 zkServicePath :

apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: default
namespace: higress-system
spec:
registries:
# 定义一个名为 my-zk 的服务来源
- name: my-zk
# 注册中心类型是 ZooKeeper
type: zookeeper
# 注册中心的访问地址,可以是域名或者IP
domain: 127.0.0.1
# 注册中心的访问端口
port: 2181

配置 Ingress 转发到这个服务(假设 /api 前缀的路由都转发给这个服务)需要做如下配置:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: my-service.services.zookeeper
name: demo
namespace: default
spec:
rules:
- http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /api
pathType: Prefix

注意对于 ZooKeeper 来源的服务,这里的目标服务格式为:"服务名称.服务注册根路径.zookeeper",Spring Cloud 在未指定服务注册根路径的情况下,根路径默认是"services"

指定注册根路径

如果 application.properties 中指定了注册根路径信息,例如:

spring.application.name=my-service
spring.cloud.zookeeper.connect-string=127.0.0.1:2181
spring.cloud.zookeeper.discovery.preferIpAddress=true
spring.cloud.zookeeper.discovery.enabled=true
spring.cloud.zookeeper.discovery.register=true
spring.cloud.zookeeper.discovery.root=my-services-root

则 Higress 的 McpBridge 中亦需指定 zkServicePath :

apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: default
namespace: higress-system
spec:
registries:
# 定义一个名为 my-zk 的服务来源
- name: my-zk
# 注册中心类型是 ZooKeeper
type: zookeeper
# 注册中心的访问地址,可以是域名或者IP
domain: 127.0.0.1
# 注册中心的访问端口
port: 2181
# 对应 spring.cloud.zookeeper.discovery.root 配置字段
zkServicePath:
- my-services-root

配置 Ingress 转发到这个服务(假设 /api 前缀的路由都转发给这个服务)需要做如下配置:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: my-service.my-services-root.zookeeper
name: demo
namespace: default
spec:
rules:
- http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /api
pathType: Prefix

注意如果 spring.cloud.zookeeper.discovery.root 中包含了下划线,需要被替换为横杠,因为目标服务整体格式需要满足 DNS 域名规范。

· 阅读需 13 分钟

摘要

Higress 是阿里巴巴开源的一款下一代云原生微服务网关。Higress 可以对接多种注册中心,包括 Nacos/Zookeeper/Eureka 等,能够无缝集成 Spring Cloud 应用,对 Dubbo/Sentinel/OpenSergo 等微服务生态也有着深度的集成。与此同时,Higress 采用 C++ 内核,相比于传统的 Java 网关来说性能更高,更稳定,对比 Spring Cloud Gateway 和 Zuul 来说,性能可以提升至 2-4 倍。另外,Higress 还天然兼容 K8s 的 Ingress/Gateway API 标准,是一款更符合云原生时代标准的微服务网关。

Higress 无缝对接 Spring Cloud 应用发布实战

在现代软件架构逐渐走向微服务化、云原生化的过程中,应用的更新和迭代的频率变得越来越快,如何在尽可能保证用户体验不受影响的情况下完成应用的迭代发布就显得至关重要。目前业界普遍采用的几种典型的应用发布策略包括蓝绿发布、金丝雀发布、A/B Testing 发布等。接下来本文将介绍如何使用 Higress 来实现 Spring Cloud Alibaba 应用发布的最佳实践。

前提条件

  1. 安装 Higress,并安装 Istio CRD,参考Higress 安装部署文档
  2. 安装 Naocs,参考Nacos 安装部署文档

Higress 支持将 Nacos,Spring Cloud 应用部署于 K8s 集群内,或者独立于 K8s 进行部署。为了演示方便,本文将 Higress,Nacos,Spring Cloud 应用都部署在本地 K8s 集群。

通过 Higress 实现 Spring Cloud 应用的服务发现和路由

部署 Spring Cloud Alibaba 应用

apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-demo-v1
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-demo
template:
metadata:
labels:
app: spring-cloud-demo
spec:
containers:
- name: server
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/samples/spring-cloud-demo:v1
imagePullPolicy: IfNotPresent
env:
# 注册到的nacos的地址
- name: NACOS_REGISTRY_ADDRESS
value: nacos-server.default.svc.cluster.local
# 注册时携带的version元信息
- name: SPRING_CLOUD_NACOS_DEMO_VERSION
value: v1

我们在 k8s 集群中部署如上 Deployment,其中通过 NACOS_REGISTRY_ADDRESS 和 SPRING_CLOUD_NACOS_DEMO_VERSION 两个环境变量指定了 Nacos 的地址以及注册时携带的 version 元信息。Spring Cloud 应用的 application.properties 配置会读取这两个环境变量,如下所示:

spring.cloud.nacos.discovery.server-addr=${NACOS_REGISTRY_ADDRESS}:8848
spring.cloud.nacos.discovery.metadata.version=${SPRING_CLOUD_NACOS_DEMO_VERSION}

配置服务来源

Higress 支持多种服务来源,包括 Nacos/Zookeeper/DNS/固定 IP,通过创建 Nacos 服务来源,Higress 就可以发现注册到 Nacos 上的服务,从而完成转发请求到这些服务上。 进入 Higress 控制台(http://console.higress.io/),点击 服务来源-创建服务来源 以创建服务来源。这里选择 Nacos 2.X,然后填写注册中心的地址,端口,命名空间,服务分组等信息。注册中心的地址可以填写 ip 或者域名,本文将 Nacos 部署在本地 K8s 中,通过 K8s service 暴露 Nacos 端口,因此这里填写对应的 service 域名。 image.png 配置好 Nacos 服务来源后,我们可以在服务列表中看到我们刚刚部署好的应用。 image.png

创建域名和路由

在 Higress 控制台上点击域名管理-创建域名,创建一条 demo.springcloud.com 域名用于后续的访问。 image.png 点击路由配置-创建路由,创建一条名为 demo 的路由,域名选择我们刚刚创建好的 demo.springcloud.com,目标服务选择我们在 1.2 中看到的 Spring Cloud 应用,path 配置为/version。 image.png

请求验证

接下来我们就可以用配置好的路由来访问 Spring Cloud 应用了,在请求时需要将 demo.springcloud.com 域名解析到本地 ip,如下所示,可以成功得到返回结果。 image.png 注:如果您将 Higress 的 80 和 443 端口通过 LoadBalancer 的方式暴露出来,这里需要将本地 ip 替换为对应 LoadBalancer 的 ip,详见Higress 快速开始文档

利用 Higress 进行蓝绿发布

在蓝绿发布中,有两套相同的运行环境,一套是当前正在使用的生产环境(蓝色环境),另一套是新版本的测试环境(绿色环境)。新版本的代码只在绿色环境中运行,测试通过后,直接将流量切换到绿色环境中,从而完成新版本的上线。与此同时蓝色环境作为热备环境,当绿色环境出现问题需要回滚时,也可以直接将流量全部再切换回蓝色环境。 image.png image.png

部署新版本应用

在本地 K8s 集群中 apply 如下资源,以部署 v2 版本的 Spring Cloud 应用。

apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-demo-v2
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-demo
template:
metadata:
labels:
app: spring-cloud-demo
spec:
containers:
- name: server
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/samples/spring-cloud-demo:v2
imagePullPolicy: IfNotPresent
env:
- name: NACOS_REGISTRY_ADDRESS
value: nacos-server.default.svc.cluster.local
- name: SPRING_CLOUD_NACOS_DEMO_VERSION
value: v2

部署完毕后,我们可以在 Higress 控制台的服务列表中看到应用已经有两个 endpoint 了,如下图所示: image.png

为服务划分子集

部署完 v2 版本的应用后,我们可以在 Nacos 控制台( http://localhost:8848/nacos ) 上看到 service-provider 这个服务有两个 ip,它们的 metadata 中的 version 字段分别为 v1 和 v2。Higress 可以根据服务的元信息将服务划分为不同的子集(subset),从而将请求转发到新版本或者老版本的应用中去。 image.png 在本地 K8s 集群中 apply 如下资源,从而根据应用元信息中的 version 字段将服务划分为 v1 和 v2 两个子集。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: demo
namespace: higress-system
spec:
host: service-provider.DEFAULT-GROUP.public.nacos
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2

修改 ingress 路由规则

新版本应用上线后,我们需要把流量全部切到新版本应用中去,这时只需要简单地修改一下我们在 1.3 中创建的路由即可。我们可以在本地 K8s 集群中找到如下 ingress 资源,这对应了我们在 1.3 中创建的那条路由。 image.png 我们直接编辑这条 ingress 资源,将 higress.io/destination 这条 annotation 的 value 改为 service-provider.DEFAULT-GROUP.public.nacos v2,即可将路由的目标服务修改为 v2 子集。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: service-provider.DEFAULT-GROUP.public.nacos v2
higress.io/ignore-path-case: "false"
labels:
higress.io/domain_demo.springcloud.com: "true"
higress.io/resource-definer: higress
name: demo
namespace: higress-system
spec:
ingressClassName: higress
rules:
- host: demo.springcloud.com
http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /version
pathType: Prefix

请求验证

我们再发送请求,可以看到此时得到的是 v2 版本应用的返回结果,如此便实现了新版本的上线发布。 image.png 如果发现已上线的新版本出现问题需要回滚,只需要修改 ingress 路由中的 higress.io/destination,将值更改为 service-provider.DEFAULT-GROUP.public.nacos v1 即可完成回滚。

利用 Higress 进行金丝雀发布

金丝雀发布是将少量的请求引流到新版本上,因此部署新版本服务只需极小数的实例。验证新版本符合预期后,逐步调整流量权重比例,使得流量慢慢从老版本迁移至新版本,期间可以根据设置的流量比例,对新版本服务进行扩容,同时对老版本服务进行缩容,使得底层资源得到最大化利用。 image.png

修改 ingress 路由规则

Higress 可以通过一条 Ingress 注解轻松完成应用的金丝雀发布。我们编辑 2.3 中的 ingress 资源,将 ingress 中的 higress.io/destination 注解按如下方式进行修改:

metadata:
annotations:
higress.io/destination: |
80% service-provider.DEFAULT-GROUP.public.nacos v1
20% service-provider.DEFAULT-GROUP.public.nacos v2

这样 Higress 就可以把 80%的流量转发到 v1 版本的应用,将 20%的流量转发到 v2 版本的应用。

请求验证

连续发送 20 条请求,可以看到 v1 和 v2 的比例符合我们在 ingress 中配置的比例。随着灰度的进行,可以逐渐调大 v2 版本的流量比例,最终完成新版本的平滑上线。 image.png

利用 Higress 进行 A/B Testing 发布

A/B 测试基于用户请求的元信息将流量路由到新版本,这是一种基于请求内容匹配的灰度发布策略。只有匹配特定规则的请求才会被引流到新版本,常见的做法包括基于 HTTP Header 和 Cookie 。基于 HTTP Header 方式,例如 User-Agent 的值为 Android 的请求 (来自 Android 系统的请求)可以访问新版本,其他系统仍然访问旧版本。基于 Cookie 方式,Cookie 中通常包含具有业务语义的用户信息,例如普通用户可以访问新版本,VIP 用户仍然访问旧版本。 image.png image.png

修改 ingress 路由规则

在本示例中,我们通过 HTTP header 中的 User-Agent 对流量进行区分,将 Android 系统的流量转发到 v2 版本,其他系统的流量仍保持 v1 版本。首先修改 2.3 中名叫 demo 的 ingress 资源,将 higress.io/destination 修改为 v1 版本,代表目前线上的流量全部会打到原来的 v1 版本:

metadata:
annotations:
higress.io/destination: service-provider.DEFAULT-GROUP.public.nacos v1

当新版本部署完成后,再新建一条如下所示的 ingress 路由。这里采用正则匹配的方式,当 User-Agent 中含有 Android 时,将请求转发到 v2 版本的服务。

kind: Ingress
metadata:
annotations:
higress.io/destination: service-provider.DEFAULT-GROUP.public.nacos v2
higress.io/canary: "true"
higress.io/canary-by-header: "User-Agent"
higress.io/canary-by-header-pattern: ".*Android.*"
higress.io/ignore-path-case: "false"
labels:
higress.io/domain_demo.springcloud.com: "true"
higress.io/resource-definer: higress
name: demo-ab
namespace: higress-system
spec:
ingressClassName: higress
rules:
- host: demo.springcloud.com
http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /version
pathType: Prefix

请求验证

可以看到来自 Android 系统的请求被转发到了 v2 版本,其余系统仍访问 v1 版本。 image.png 当新版本验证完毕需要全量上线时,只需要将 demo 路由的 higress.io/destination 注解修改为 v2 版本,并删除 demo-ab 路由,这样所有流量就都会访问 v2 版本了。

加入 Higress 和 Spring Cloud Aliaba 社区

Spring Cloud Alibaba 社区交流群:钉钉群号 2415000986

Higress 社区交流群:

higress-comm.png

Higress 社区交流钉钉群中有历次 Higress 社区周会录屏,包括本文中提到的结合 Spring Cloud 应用发布的完整实操视频。

· 阅读需 10 分钟

环境准备

Nacos: v2.3.0

Spring Boot: 3.2.0

Spring Cloud: 2023.0.0

Spring Cloud Alibaba Version: 2023.0.0.0-RC1

项目编写

github 地址:https://github.com/yuluo-yx/sca-k8s-demo/tree/openfeign

项目结构

├─docker-compose                  # Docker compose 部署文件(这里只是示例,没有任何其他用途)
├─kubernetes # Kubernetes 部署文件
└─docker-images
├─consumer
├─ application-k8s.yaml # k8s 环境的配置文件
├─ app.jar # 应用 jar 包
└─ Dockerfile # 打包的 Dockerfile
└─provider
├─sca-k8s-service-consumer # sca 服务消费者模块
├─sca-k8s-service-provider # sca 服务提供者模块

父 pom.xml 文件:

<modules>
<module>sca-k8s-service-provider</module>
<module>sca-k8s-service-consumer</module>
</modules>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud-alibaba.version>2023.0.0.0-RC1</spring-cloud-alibaba.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
<spring-boot.version>3.2.0</spring-boot.version>
</properties>

<dependencies>
<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>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--spring boot 打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

provider 模块

provider pom.xml 文件

<dependencies>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

</dependencies>

provider controller java 文件

@RestController
@RequestMapping("/provider")
public class ProviderController {

@Resource
private ProviderService providerService;


@GetMapping("/a")
public String providerA() {

return providerService.providerA();
}

@GetMapping("/b")
public String providerB() {

return providerService.providerB();
}

}

provider service java 文件

@Service
public class ProviderServiceImpl implements ProviderService {

@Override
public String providerA() {

return "This response from provider A!";
}

@Override
public String providerB() {

return "This response from provider B!";
}
}

provider application.yml 配置

server:
port: 8082

spring:
application:
name: sca-k8s-provider

cloud:
nacos:
discovery:
# nacos 服务发现地址
server-addr: 127.0.0.1:8848
username: 'nacos'
password: 'nacos'

# actuator 健康检查配置,用于做 kubernetes pod 的 liveness 探针接口使用
management:
server:
# management 类端口全部保持一致 (尽量让业务服务的 K8S Deployment 保持统一,便于维护)
port: 30000
endpoint:
health:
probes:
enabled: true
endpoints:
web:
exposure:
include: health

consumer 模块

Consumer 模块使用 spring-cloud-openfeign 作为 rpc 组件。

consumer pom.xml 文件

<dependencies>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>

consumer feign client java 文件

@FeignClient(
name = "sca-k8s-provider",
fallback = K8sFeignCallback.class,
configuration = K8sFeignConfig.class,
contextId = "sca-k8s-provider"
)
public interface K8sFeignClient {

@GetMapping("/provider/a")
String providerA();

@GetMapping("/provider/b")
String providerB();

}

consumer feign config java 文件

public class K8sFeignConfig {

@Bean
public K8sFeignCallback feignCallback() {

return new K8sFeignCallback();
}

}

Consumer feign fallback java 文件

public class K8sFeignCallback implements K8sFeignClient{

@Override
public String providerA() {

return "call provider A interface failed, fallback";
}

@Override
public String providerB() {

return "call provider B interface failed, fallback";
}
}

Consumer controller java

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

@Autowired
private ConsumerService consumerService;

@GetMapping("/a")
public String consumerA() {

return consumerService.consumerA();
}

@GetMapping("/b")
public String consumerB() {

return consumerService.consumerB();
}

}

Consumer service java 文件

@Service
public class ConsumerServiceImpl implements ConsumerService {

@Autowired
private K8sFeignClient feignClient;

@Override
public String consumerA() {

return feignClient.providerA();
}

@Override
public String consumerB() {

return feignClient.providerB();
}
}

Consumer application 主类 java 文件

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
@LoadBalancerClients({
@LoadBalancerClient("sca-k8s-provider")
})
public class SCAK8sConsumerApplication {

public static void main(String[] args) {

SpringApplication.run(SCAK8sConsumerApplication.class, args);
}

}

consumer application.yml 配置

server:
port: 8080

spring:
application:
name: sca-k8s-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: 'nacos'
password: 'nacos'

management:
server:
# management 类端口全部保持一致 (尽量让业务服务的 K8S Deployment 保持统一,便于维护)
port: 30000
endpoint:
health:
probes:
enabled: true
endpoints:
web:
exposure:
include: health

feign:
sentinel:
enabled: true

部署效果展示

本项目提供了 Docker compose 和 Kubernetes 部署两种方式。

Docker compose 运行方式参考 docker-compose 目录下的 README.md 文件。本节中主要以 k8s 部署方式演示为主。

本地运行需要准备 2.3.0 版本的 nacos server。

环境准备

准备 k8s 集群,此处为了演示方便,使用 kind 模拟 k8s cluster,初始化集群时创建的 pod 如下所示:

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-76f75df574-74qnn 1/1 Running 0 118s
kube-system coredns-76f75df574-q9hq5 1/1 Running 0 118s
kube-system etcd-sca-k8s-control-plane 1/1 Running 0 2m19s
kube-system kindnet-bmsmm 1/1 Running 0 118s
kube-system kube-apiserver-sca-k8s-control-plane 1/1 Running 0 2m24s
kube-system kube-controller-manager-sca-k8s-control-plane 1/1 Running 0 2m19s
kube-system kube-proxy-lllzb 1/1 Running 0 118s
kube-system kube-scheduler-sca-k8s-control-plane 1/1 Running 0 2m23s
local-path-storage local-path-provisioner-7577fdbbfb-zpwb4 1/1 Running 0 117s

部署流程

部署单节点 Nacos

执行 kubectl create -f sca-k8s-demo-mysql.yaml 创建 nacos 需要的 mysql 服务:

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl create -f sca-k8s-demo-mysql.yaml 
replicationcontroller/sca-k8s-demo-mysql created
service/sca-k8s-demo-mysql created

创建成功使用 kubectl get pods -A | grep mysql 查询,如下所示:

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl get pods -A | grep mysql
default sca-k8s-demo-mysql-87wft 1/1 Running 0 2m5s

执行 kubectl create -f sca-k8s-demo-nacos.yaml 创建 nacos 需要的 mysql 服务:

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl create -f sca-k8s-demo-nacos.yaml 
service/sca-k8s-demo-nacos-standalone created
configmap/sca-k8s-demo-nacos-cm created
deployment.apps/sca-k8s-demo-nacos-standalone created

创建成功如下所示:

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl get pods | grep nacos
sca-k8s-demo-nacos-standalone-854d8cfc88-rg7pp 1/1 Running 0 2m26s

这里没有使用 ingress 暴露 nacos 服务,使用端口转发的方式将 nacos 暴露出来。

nacos 被设计成在内网使用的中间件,在生产环境中应注意网络隔离策略。

暴露 nacos server:kubectl port-forward --address localhost,192.168.20.129 svc/sca-k8s-demo-nacos-standalone 8848:8848 9848:9848

这里的地址是虚拟机的 ip 地址,实际操作时注意 ip 地址的变化。

浏览器访问 nacos console:

image-20240225182956824

打包 Docker Images

修改应用配置文件中的 nacos server 地址为端口转发时使用的虚拟机地址,确保服务成功注册。consumer 和 provider 同时修改。

image-20240225183636922

为了使用方便,此处使用阿里云容器镜像服务。执行下述命令之前先使用 docker login 命令登陆 docker hub,确保 push 镜像成功!

修改 docker images 的 tag 为自己的阿里云镜像仓库用户名地址。之后执行 ./kubernetes/docker-images/build.sh 脚本

部署 Provider 和 Consumer 服务

k8s 部署资源文件解析

ConfigMap 资源文件:用于设置一些 jvm 调优参数和指定激活的配置文件:

更最佳的实践做法为:线上的配置不会写在 dockerfile 或者其他构件相关的配置文件中,而是直接维护在 k8s 的 ConfigMap 里面 (通常由运维人员维护,开发者不关注这些具体的参数信息)

通常情况下:应用的配置文件为

application.yml   # 通用配置项
application-dev.yml # 开发环境配置
application-test.yml # 测试环境配置
application-prod.yml # 生产环境配置

在此 demo 设置应用配置文件为 k8s,如上文项目结构所示!

apiVersion: v1
data:
spring-profiles-active: 'k8s'
kind: ConfigMap
metadata:
name: sca-k8s-demo-spring-profile-cm

provider deployment 资源文件:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: sca-k8s-demo-provider-service
name: sca-k8s-demo-provider-service
spec:
replicas: 1
selector:
matchLabels:
app: sca-k8s-demo-provider-service
template:
metadata:
labels:
app: sca-k8s-demo-provider-service
spec:
containers:
- env:
- name: SPRING_PROFILES_ACTIVE
valueFrom:
configMapKeyRef:
name: sca-k8s-demo-spring-profile-cm
key: spring-profiles-active
# jvm arguments
# - name: JAVA_OPTION
# value: "-Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:+PrintGCDetails -Xloggc:/var/log/devops-example.gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:+DisableExplicitGC"
# - name: XMX
# value: "128m"
# - name: XMS
# value: "128m"
# - name: XMN
# value: "64m"
name: sca-k8s-demo-provider-service
image: registry.cn-hangzhou.aliyuncs.com/yuluo-yx/sca-k8s-demo-provider-service:latest
ports:
- containerPort: 8082
livenessProbe:
httpGet:
path: /actuator/health/liveness
# port 尽量保持统一,方便维护
port: 30000
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 30000
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 10

consumer 对比 provider 多了一个 svc 配置,其他相同。之后需要做端口转发,提供外部访问,同样也可以使用 ingress 暴露服务出去。

部署

修改完成所有配置之后,执行 kubectl create -f sca-k8s-demo-cm.yaml 部署 provider 和 consumer 需要的 ConfigMap 资源,

之后部署 provider service kukbectl create -f sca-k8s-demo-provider.yaml

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl get pods -A | grep provider
default sca-k8s-demo-provider-service-7c6579956b-xswjq 1/1 Running 0 9m12s

执行 kubectl logs sca-k8s-demo-provider-service-xxxx 查看 pod 日志,发现激活的配置文件为 k8s,证明 ConfigMap 生效:

image-20240225210541866

查看 nacos 控制台,发现 provider 已经注册:

image-20240225210515801

尝试访问 provider service 接口服务:

转发 provider service pod:kubectl port-forward pod-name 8082:8082

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# curl 127.0.0.1:8082/provider/a
This response from provider A!

consumer 部署方式相同,执行 kubectl create -f sca-k8s-demo-consumer.yaml 即可部署。

部署之后查看 nacos 注册中心:

image-20240403165601573

最终部署所有的 k8s 资源如下:

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# kubectl get all | grep sca
pod/sca-k8s-demo-consumer-service-7ffff75969-zmtcw 1/1 Running 0 2m5s
pod/sca-k8s-demo-mysql-zmstn 1/1 Running 0 35m
pod/sca-k8s-demo-nacos-standalone-854d8cfc88-rg7pp 1/1 Running 0 34m
pod/sca-k8s-demo-provider-service-7c6579956b-xswjq 1/1 Running 0 15m
replicationcontroller/sca-k8s-demo-mysql 1 1 1 35m
service/sca-k8s-demo-consumer-service-svc ClusterIP 10.96.61.65 <none> 8080/TCP 2m5s
service/sca-k8s-demo-mysql ClusterIP 10.96.37.255 <none> 3306/TCP 35m
service/sca-k8s-demo-nacos-standalone ClusterIP None <none> 8848/TCP,9848/TCP,9849/TCP,7848/TCP 34m
deployment.apps/sca-k8s-demo-consumer-service 0/1 1 0 2m5s
deployment.apps/sca-k8s-demo-nacos-standalone 1/1 1 1 34m
deployment.apps/sca-k8s-demo-provider-service 1/1 1 1 15m
replicaset.apps/sca-k8s-demo-consumer-service-7ffff75969 1 1 0 2m5s
replicaset.apps/sca-k8s-demo-nacos-standalone-854d8cfc88 1 1 1 34m
replicaset.apps/sca-k8s-demo-provider-service-7c6579956b 1 1 1 15m

访问

转发 consumer 服务:kubectl port-forward --address localhost,192.168.20.129 svc/sca-k8s-demo-consumer-service-svc 8080:8080

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# curl 127.0.0.1:8080/consumer/a
This response from provider A!

root@yuluo-Inspiron-3647:/kubernetes/sca-k8s-best# curl 127.0.0.1:8080/consumer/b
This response from provider B!

释放资源

kubectl delete -f sca-k8s-demo-*.yaml

本文章主要介绍如何在 Kubernetes 环境中部署 Spring Cloud Alibaba 应用。在部署的同时,使用 liveness 探针确保 pod 正常启动可对外提供服务,使用 ConfigMap 配置使得应用的配置参数更加灵活。

· 阅读需 19 分钟

摘要

作为下一代互联网协议,向 IPv6 迁移是未来的大势所趋。但由于当前互联网中 IPv4 协议的应用规模非常大,对于用户来说,没办法通过规定一个时间日期,从那一刻开始,所有互联网上的设备全部使用 IPv6,这是不现实的。一次性迁移不仅在基础设施层面不可行,对企业用户来说,就算基础设施都能准备完毕,让其将少则上百,多则成千上万的应用实例在一段时间内一次性停机进行协议栈迁移,无论是在风险上,还是成本上,对企业用户来说都是难以接受的!既然无法一步到位,渐进式的 IP 地址迁移成为当前的主流选择。本文将介绍一些主流渐进式的 IP 地址迁移方法。

· 阅读需 24 分钟

摘要

经过过去几年的发展,Service Mesh 已再是一个新兴的概念,其从一经推出就受到来自全世界的主流技术公司关注和追捧。Proxyless Mesh 全称是 Proxyless Service Mesh,其是近几年在 Service Mesh 基础上发展而来的一种新型软件架构。Service Mesh 理想很丰满,但现实很骨感!通过一层代理虽然做到了对应用无侵入,但增加的网络代理开销对很多性能要求很高的互联网业务落地存在不少挑战。因此 Proxyless Mesh 作为一种在传统侵入式微服务框架与 Service Mesh 之间的折中方案,通过取众家之所长,为大量的非 Service Mesh 应用在云原生时代,拥抱云原生基础设施,解决流量治理等痛点提供了一种有效的解决方案。本文将介绍 Spring Cloud Alibaba 在 Proxyless Mesh 上的探索。