前言
SpringCloud API Gateway大家都知道可以限流、可以拦截流量、可以抓到请求,可是它的最重要作用是:充当docker中的Load Balance。而这一特性几乎有90%的开发者们都被前3个特性所蒙蔽了双眼而忽略了。
Spring Cloud API Gateway诞生的真正的目的
Docker化进程带来的影响
优势
docker化带来的是开机快速、资源节约、云原生。
问题
Docker化带来的另一个问题就是:每次重启或者是重新布署后docker内的镜像ip是"漂移"的。
譬如说:
docker内有一镜像:
- 镜像名:TestWeb1;
- 端口号:运行在9081端口上
- IP:刚布署完时TestWeb1位于172.0.1.52
当你每次发布时你会发觉TestWeb1一会跑到了:172.0.1.55上一会又变成了172.0.1.21了。
而api gateway里如果我们配置路由是这样的话:

可能一开始我们是可以访问得了的,而当每次镜像一被重新布署,那么ip地址就会发生"漂移"。
我们总不见得每次都要改hosts或者是DNS,这哪是云原生。
api gateway对于docker容器中的镜像代理的正确做法

即配置成:
lb://TestWeb1
*注:上述api gateway叫:venus,是基于spring cloud api gateway2.0做的带有图形化界面可动态配置的企业级api gateway,它跟随着我打过双11和618应对过10万级每秒并发,目前已经完全开源在git了,需要的人可自行下载。
api gateway中的lb的用法
什么是lb://协议,它有什么作用?
我们假设有以下这种架构

我们可以看到一旦当:
- 所有的微服务以nacos discovery把自己当成服务注册进nacos里;
- 同时当spring cloud api gateway也用nacos discovery把自己注册进nacos里;
此时,nacos服务器就是一个load balance。或者说nacos本身就是具备load balance功能的。它本身就可以记住当前使用nacos discovery注册进它服务列表的服务的真实ip到底是在哪里的。因此通过spring cloud api gateway里的lb://服务名就相当于访问了:http://一个服务的ip:端口号/sample/hello这样 的一个调用了。
比如说:我们的TestWeb1上有一个服务:http://localhost:9081/sample/hello。只要按照上述这样的架构把TestWeb1做成镜像包放于docker中,我们只需要在spring cloud api gateway上配置成:

当访问spring cloud api gateway所在服务器(它位于http://localhost:9080上):http://localhost:9080/sample/hello时,spring cloud api gateway会自动路由到docker上的TestWeb1上去的。
NACOS里怎么做到路由的?

这是我们在nacos里的两个服务。
- TestWeb1位于docker容器内;
- apigateway运行在我们的本地的intellij上;
现在,我们点击具体的apigateway这个服务,进入查看详情

我们可以看到spring cloud api gateway的ip是:169.254.57.189。这是我本地的IP。
我们再点击TestWeb1这个服务,进入查看详情。

我们可以看到它的IP是:host.docker.internal,由于host.docker.internal等于:本地docker安装的那台机器的IP就等于:169.254.57.189。
因此,api gateway会通过: lb://服务名自动取得相应的服务的具体ip和端口到底在哪里。
以上就是lb://协议通过nacos的服务名获取当前服务的ip和端口号的寻址路径。
要知道nacos这东西一出现后,在大厂内部当时瞬间就让万台nginx以及F5成为了历史,这是因为nacos本身就是一个load balance,这一把直接就可以省出一笔巨款来,同时又因为只要你的服务使用了nacos discovery client,哪怕服务本身的ip马上发生了变更nacos也可以马上知道当前你的服务跑到了哪个IP上去了。
是不是很强大?
动手实现api gateway 2.0通过注册nacos动态获得docker内境像实例的例子
我们这个例子里用的api gateway是标准的spring cloud api gateway2.0,我使用了api gateway的动态实时路由并使用了vue3做了界面,项目名叫:venus,已经完全开源在git了,需要的人可自行下载。
我们来看我们的api gateway2.0的启动方式。
spring cloud api gateway2.0
spring cloud启动文件:FountainGatewayApplication.java
java
package com.mkyuan.fountaingateway;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import javax.annotation.PostConstruct;
@SpringBootApplication
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, RedisAutoConfiguration.class, RedissonAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class
})
@EnableDiscoveryClient
public class FountainGatewayApplication {
protected Logger logger = LogManager.getLogger(this.getClass());
public static void main(String[] args) {
SpringApplication.run(FountainGatewayApplication.class, args);
}
}
关键在于这一行:@EnableDiscoveryClient
pom.xml
java
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mkyuan</groupId>
<artifactId>apigateway</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>apigateway</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<spring-cloud.version>2020.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<log4j2.version>2.17.1</log4j2.version>
<spring.data.redis>1.8.14-RELEASE</spring.data.redis>
<redisson.version>3.19.1</redisson.version>
<common.langs3.version>3.17.0</common.langs3.version>
<fastjson.version>1.2.59</fastjson.version>
<jedis.version>3.9.0</jedis.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<!-- <exclusions> <exclusion> <groupId>org.redisson</groupId> <artifactId>redisson-spring-data-23</artifactId>
</exclusion> </exclusions> -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-actuator
</artifactId>
</exclusion>
</exclusions>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-24</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${common.langs3.version}</version>
</dependency>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Bootstrap -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
<!-- 排除了老版本log4j2后升级到最新的2.15.0 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<!-- 排除了老版本log4j2后升级到最新的2.15.0 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Alibaba Cloud -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
bootstrap.yml
java
spring:
application:
name: apigateway
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
username: nacos
password: nacos
namespace: local
config:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
username: nacos
password: nacos
namespace: local
group: DEFAULT_GROUP
file-extension: yaml
prefix: apigateway
refresh-enabled: true
TestWeb1工程
pom.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mkyuan.sample</groupId>
<artifactId>TestWeb1</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<spring-cloud.version>2020.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<log4j2.version>2.17.1</log4j2.version>
<spring.data.redis>1.8.14-RELEASE</spring.data.redis>
<redisson.version>3.19.1</redisson.version>
<common.langs3.version>3.17.0</common.langs3.version>
<fastjson.version>1.2.59</fastjson.version>
<jedis.version>3.9.0</jedis.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${common.langs3.version}</version>
</dependency>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Bootstrap -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
<!-- 排除了老版本log4j2后升级到最新的2.15.0 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<!-- 排除了老版本log4j2后升级到最新的2.15.0 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Alibaba Cloud -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.mkyuan.sample.Application</mainClass>
<layout>JAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml
java
server:
port: 9081
logging:
config: classpath:log4j2.xml
level:
root: info
com.mkyuan: debug
org.springframework.cloud.gateway: debug
bootstrap.yml
java
spring:
application:
name: TestWeb1
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
username: nacos
password: nacos
namespace: local
config:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
username: nacos
password: nacos
namespace: local
group: DEFAULT_GROUP
file-extension: yaml
prefix: TestWeb1
refresh-enabled: true
spring cloud启动文件:Application.java
java
package com.mkyuan.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@ComponentScan(basePackages = "com.mkyuan")
@EnableDiscoveryClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
关键同样有一行:@EnableDiscoveryClient。
对外暴露API-HelloApi.java
java
package com.mkyuan.sample.hello.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@RestController
@RequestMapping("/sample")
public class HelloApi {
@PostMapping("/hello")
@ResponseBody
public String sayHello() {
return "{\"message\": \"hello\"}";
}
}
很简单。
运行TestWeb1

它只是返回一个hello。
把TestWeb1做成Docker镜像
在TestWeb1根目录下建立两个文件,它们分别为:
- docker-compose.yml
- Dockerfile
docker-compose.yml
- SPRING_CLOUD_NACOS_DISCOVERY_IP=host.docker.internal # 使用宿主机地址或同apigateway服务ip网段
这儿需要注意的两点
image: localhost:9991/testweb1:1.0
此处的localhost:9991是我docker的本地镜像仓库所在。
这是因为,我的docker基础镜像用的是:openjdk:11-jre-slim,它是一个自带openjdk11的linux镜像。它有一两百兆左右。
每次构建时都会:从docker.io去下载这个镜像文件。那么你的构建过程所耗时间就会很长。
而用了docker本地镜像仓库后,每次只会构建你本地的应用镜像而不会再去docker.io去重复下载这个基础镜像了。
SPRING_CLOUD_NACOS_DISCOVERY_IP=host.docker.internal # 使用宿主机地址或同apigateway服务ip网段
这儿可不是nacos服务器所在的ip,而是指的是:当nacos根据请求的服务名获得需要路由到的服务的ip地址和端口所在的TestWeb1真实的服务地址所在。
比如说:我们的TestWeb1的docker以及api gateway还有一系列微服务运行在:192.168.0.1这台宿主机上,那么此时这儿的SPRING_CLOUD_NACOS_DISCOVERY_IP就要设成192.168.0.1上。
这边由于我用的宿主机上用的是本地的windows docker deskstop,因此我设成了:host.docker.internal(相当于localhost的意思)。
Dockerfile
java
# 使用更小的openjdk基础镜像
FROM openjdk:11-jre-slim
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 设置工作目录
WORKDIR /app
# 创建一个非root用户来运行应用
RUN groupadd -r spring && useradd -r -g spring spring
# 创建日志目录并设置权限
RUN mkdir -p /app/logs && chown -R spring:spring /app
# 复制配置文件到指定位置
COPY --chown=spring:spring src/main/resources/application.yml /app/config/
COPY --chown=spring:spring src/main/resources/bootstrap.yml /app/config/
COPY --chown=spring:spring src/main/resources/log4j2.xml /app/config/
# 切换到非root用户
USER spring:spring
# 复制jar文件
COPY --chown=spring:spring target/TestWeb1-1.0-SNAPSHOT.jar app.jar
# 设置JVM参数
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC"
# 暴露应用端口
EXPOSE 9081
# 启动应用,指定配置文件位置
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Dspring.config.location=file:/app/config/ -Dlogging.config=file:/app/config/log4j2.xml -jar app.jar"]
构建本地镜像仓库
创建目录
java
mkdir E:\docker_images\docker-registry\data
创建或修改Docker配置文件
必须使用docker desktop里的settings->docker engine里列出的daemon.json图形界面来修改

加的是这一句:
java
"insecure-registries": ["localhost:9991"]
这就是我们的本地镜像仓库。
更改完后点击docker desktop右下角的【Apply & Restart】按钮。如果是在mac或者是ubuntu/centos里记得要重启docker的服务以使得配置生效。
运行以下命令来构建本地仓库
java
docker run -d --name local-registry --restart=always -p 9991:5000 -v E:/docker_images/docker-registry/data:/var/lib/registry registry:2
此处的:5000为镜像仓库内部的默认端口号,外部的9991就是我们推送镜像的端口号。
验证本地镜像仓库registry是否正常运行
java
docker ps | grep local-registry
curl http://localhost:5001/v2/_catalog
开始构建TestWeb1应用镜像
构建应用
在TestWeb1根目录下即有docker-compose.yml和Dockerfile的那一层目录键入以下命令
java
docker compose build
大约5分钟左右,TestWeb1应用构建完毕。
构建完应用后把基础镜像推送到本地docker镜像仓库中去
java
docker push localhost:9991/testweb1:1.0
我们可以看到如下这样的输出就代表基础镜像已经成功推送到了本地镜像仓库中去了。

启动应用镜像
java
docker compose up -d
然后我们通过:http://localhost:9081/sample/helo访问,当看到:

那么就代表我们的应用docker启动成功了。
启动api gateway(venus)
它启动后位于9080端口。
配置一条路由

通过api gateway访问位于docker内的testweb1
http://localhost:9080/sample/hello
同样是成功返回了TestWeb1应用的api的。

再观察api gateway的控台,看下方红色箭头所指之处。

这就代表了api gateway成功的把请求转到了docker内的TestWeb1应用上去了。
好了,结束这次的教程,关键还是大家自己要多动动手并亲自实验才能真正掌握这些精髓!