微服务API网关设计
当项目负载规模不大的时候, 基本上单个项目 http://域名/user 和 http://域名/pay 这样访问API就行了,
在之后如果项目规模开始上来最多用 nginx 负载均衡处理一下分流到不同服务器, 但是后续功能业务和流量大规模上来之后也会到达瓶颈.
按照业务程度分布起始就这以下阶段:
- 简单实现基础
api功能, 业务功能比较少且接口简单 - 流量上来需要对请求限流和负载均衡, 与此同时业务接口还是在可控可维护范围
- 不止接口流量庞大, 同时业务也规模上去(用户模块,订单模块,统计模块,广告模块,….将近上千多个), 这时候单项目维护就很麻烦了
当超大流量的情况把功能集中在单个项目里面, 哪怕按照目录区分(/user,/pay,/activity,...) 也是很耗费精力的事, 所以需要做模块拆分.
拆分出来之后很可能内部不同地址需要统一的网关服务器, 网关负责接收外部请求统一入口并协调转发到内网服务.
对于内网来说就是编写模块下业务并且注册到网关提供服务, 这样的好处就是隔离不同业务服务并且支持热更,
如果某个服务请求过大卡住的时候不会直接影响到其他服务, 只要不是同个模块下的服务都互不影响从而方便开发针对业务调试.
这里也引申出微架构的主要结构: 网关 和 模块
- 网关负责模块的服务注册和转发, 利用
eureka可以更加方便做服务注册中心来处理服务重连等情况 - 模块就是具体业务分层, 启动的时候注册到网关来暴露自己的业务功能, 利用
springCloud可以直接无缝接入到eureka
以下方案也是直接采用
eureka和springCloud, 同时最低Java版本为17
配置多模块
直接在项目目录下追加 pom.xml, 文件内容如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.meteorcat.fusion</groupId>
<artifactId>micro</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- 全局属性 -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.3.3</spring-boot.version>
<maven-compiler.version>3.13.0</maven-compiler.version>
<lombok.version>1.18.36</lombok.version>
<!-- 2023.0.x 又名 Leyton SpringBoot 3.3.x, 3.2.x -->
<spring-cloud.version>2023.0.2</spring-cloud.version>
</properties>
<!-- 子模块定义 -->
<modules>
</modules>
<!-- 管理子项目的依赖 -->
<dependencyManagement>
<dependencies>
<!-- LOMBOK组件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>${lombok.version}</version>
</dependency>
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- springCloud依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 配置子项目第三方库源 -->
<repositories>
<repository>
<id>central</id>
<url>https://maven.aliyun.com/repository/central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>netflix-candidates</id>
<name>Netflix Candidates</name>
<url>https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-candidates</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<!-- 配置公共的插件管理等 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
这就是最开始的起步属于自己的微服务基础架构, 之后就是创建名称为 cloud-gateway 的模块用来负担网关, pom.xml 内容如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 网关基础信息 -->
<modelVersion>4.0.0</modelVersion>
<groupId>com.game.fusion</groupId>
<artifactId>cloud-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-gateway</name>
<description>cloud-gateway</description>
<!-- 继承上级信息 -->
<parent>
<groupId>com.meteorcat.fusion</groupId>
<artifactId>micro</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 依赖模块 -->
<dependencies>
<!--引入gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--注入eureka client 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<!-- 打包配置 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.game.fusion.gateway.CloudGatewayApplication</mainClass>
<skip>false</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
如果这里报错有关于
Tests相关信息, 需要把项目内的tests目录删除
之后根项目的 pom.xml 追加模块执行命令刷新拉取第三方包信息即可:
<!-- 子模块定义 -->
<modules>
<module>cloud-gateway</module>
</modules>
拉取依赖的时候比较久要等一下, 没问题的话就代表我们网关服务器已经初步架设完成, 准备编写网关服务配置文件.
网关直接直接在配置文件编写就行了, 不存在文件直接创建 cloud-gateway/src/main/resources/application.yml, 配置文件如下:
server:
port: 9527 # 网关端口
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
# 路由支持多个匹配, 但是id必须是唯一
# 路由主要是为了区分模块让其转发到注册中心的指定某个服务, 可以有效将模块功能分流到其他注册的服务器
# 当然也可以直接不写直接默认 /** 转发到注册中心
# 这里假设设计个权限模块用于注册挂载玩家服务
routes:
# 路由的ID,没有固定规则,但要求唯一,建议和注册服务名一致
- id: authority-service
# 匹配后提供服务的路由地址
# 这里用的动态路由格式统一为 lb://注册进eureka服务中心注册的服务名称
uri: lb://authority-service
predicates:
# 断言,路径相匹配的进行路由
# 断言也就是匹配方式,当识别到/authority/**时就会跳转上面的uri
- Path=/authority/**
filters: # 这个必须写
# 请求/name/bar/foo,去除掉前面两个前缀之后,最后转发到目标服务的路径为/foo
# 这里写的是只去掉一个,多了自然会导致路径错误,不同的访问url配置也不同
- StripPrefix=1
eureka:
instance:
hostname: ${spring.application.name}-service
client: # 服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: false
fetch-registry: false
defaultZone: http://localhost:8761/eureka/
# 查看日志方便调试
logging:
level:
org.springframework.cloud: debug
这样启动之后没问题运行就代表网关启动完成, 之后就是要开始考虑注册模块服务的情况.
注册中心
网关架设之后就需要一个服务注册中心来让内网服务注册并提供服务, 也就是利用 eureka-server 来架设服务,
这里创建注册中心模块 authority-registry 来作为自己的权限服务模块注册中心, pom.xml 文件内容如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 权限注册中心基础信息 -->
<modelVersion>4.0.0</modelVersion>
<groupId>com.game.fusion</groupId>
<artifactId>authority-registry</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>authority-registry</name>
<description>authority-registry</description>
<!-- 继承上级信息 -->
<parent>
<groupId>com.meteorcat.fusion</groupId>
<artifactId>micro</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 依赖模块 -->
<dependencies>
<!-- eureka 服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
</dependency>
<!-- cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<!-- 配置装配 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
<!-- 打包配置 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.game.fusion.registry.authority.AuthorityRegistryApplication</mainClass>
<skip>false</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
之后就是创建注册中心的配置文件 application.yml 内容如下:
server:
port: 8761
spring:
application:
name: authority-registry
eureka:
instance:
hostname: authority-registry
prefer-ip-address: true # 是否使用ip注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
register-with-eureka: false # 是否将自己注册到注册中心
fetch-registry: false # 是否从注册中心服务注册信息
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://localhost:8761/eureka/
最后需要主要需要在注册中心启动类追加 @EnableEurekaServer 注解:
package com.game.fusion.registry.authority;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* 权限服务注册中心
*/
@EnableEurekaServer
@SpringBootApplication
public class AuthorityRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorityRegistryApplication.class, args);
}
}
启动之后没问题注册中心会提供两个地址:
- 状态面板地址: http://localhost:8761
- 模块注册地址: http://localhost:8761/eureka/
至此我们已经创建微服务网关, 并且还创建 权限服务中心(AuthorityRegistry), 那么问题就来了: 模块服务呢?
服务注册
现在已经搭建完整体服务架构但是关键的服务还没挂载, 这里创建模块服务 authority-service,
该模块其实就是和常规 springBoot 模块差不多, 本身 @SpringBootApplication 已经支持服务转发功能:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 权限服务基础信息 -->
<modelVersion>4.0.0</modelVersion>
<groupId>com.game.fusion</groupId>
<artifactId>authority-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>authority-service</name>
<description>authority-service</description>
<!-- 继承上级信息 -->
<parent>
<groupId>com.meteorcat.fusion</groupId>
<artifactId>micro</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 依赖模块 -->
<dependencies>
<!-- eureka 客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Web服务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 监控服务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<!-- 打包配置 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.game.fusion.service.authority.AuthorityServiceApplication</mainClass>
<skip>false</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
除了追加 spring-cloud-starter-netflix-eureka-client 依赖其他和常规 web 服务一致,
主要是配置文件追加上 eureka 就可以启动时候注册:
spring:
application:
name: authority-service # 应用名称, 注意这个名称必须和网关声明的服务一致, 这里就是网关能够转发地址名
server:
port: 0 # 随机端口
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
instance:
preferIpAddress: true
# 查看日志方便调试
logging:
level:
web: debug
最后该该模块服务暴露 REST 接口用于测试:
package com.game.fusion.service.authority.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 账号登录服务
*/
@RestController
@RequestMapping("/login")
public record LoginByUsername() {
/**
* 返回时间戳
*
* @return JSON
*/
@GetMapping
public Object timestamp() {
return Map.of("timestamp", System.currentTimeMillis() / 1000L);
}
}
启动之后可以通过 http://localhost:8761/ 访问状态看到刚刚启动服务注册到 eureka 之中,
最后访问网关 http://127.0.0.1:9527/authority/login 确认是否请求被转发到模块服务上,
如果能够看到返回 timestamp 就代表微架构服务已经正式生效.
架构核心
eureka 和 springCloud 配合的架构能够有效把业务抽离分布在各个设备上, 并且相互之间不会因单个服务故障导致全体服务瘫痪,
并且内部支持本身服务负载均衡和故障转移能够有效缓解大流量并发服务卡顿异常.
比较简单的部署如下:
192.168.1.5~192.168.1.7做授权authority服务192.168.1.8~192.168.1.10做支付pay服务192.168.1.11~192.168.1.14做活动activity服务
将业务直接拆分到不同设备上, 依靠内部负载均衡可以动态按照访问流量分布合理分配不同设备来做业务服务, 比如活动峰值不高的情况可以降低设备数量移交到比较频繁的支付服务等情况等.
不过微架构的情况只适合在数据量级到达一定程度上才推荐采用, 因为本身单单最精简的分布架构都要2~3台设备以上:
- 网关服务需要单独部署在一台服务器上
- 服务注册可以和网关服务器部署在一起, 也可以单独剔除出来
- 模块服务看业务情况来处理分布, 有的可能单机单服务,也可能单机N服务
所以在前期的时候实际上不太适合直接上该架构, 没办法对访问峰值做评估; 当然单机全部署的方式更不推荐, 全部服务集中一起服务器卡一起卡的情况会让架构变得更慢.
不过 cloud 网关不仅仅可以作为 eureka 的 lb:// 数据转发, cloud 还支持以下协议转发:
lb://:eureka的数据转发, 需要配置eureka-clienthttp|https: 直接声明http://或者https://即可websocket: 声明ws://或者wss://lb://websocket: 利用eureka负载均衡转发websocket, 声明为lb:ws(s)://serviceid(需要在eureka注册服务)rtsp: 用来实时数据流流转发, 实时数据的复杂转发和负载, 大大加强音视频|会议传输稳定性 -
另外需要注意如果使用 SockJS 需要配置个 http 协议路由来防止协议回滚, 所以需要额外加个转发:
spring:
cloud:
gateway:
routes:
# SockJS 路由
- id: websocket_sockjs_route
uri: http://localhost:3001
predicates:
- Path=/websocket/info/**
# 常规 Websocket 路由
- id: websocket_route
uri: ws://localhost:3001
predicates:
- Path=/websocket/**
除了支持转发协议不同, 路由(routes) 和 断言(predicates) 还支持拦截参数重新转发, 通过断言的 {segment} 实现数据重写:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red/{segment} # 获取路由切片值
filters:
# 利用 AddRequestHeadersIfNotPresent 是否存在该 header
# 如果不存在该头信息就自动追加上
- AddRequestHeadersIfNotPresent=X-Request-Red:Blue-{segment}
同时路由也支持泛匹配转发( {*segments} ):
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: lb://backing-service:8088
predicates:
# 获取路由之后全部路径参数
- Path=/consumingServiceEndpoint/{*segments}
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
# 拿到参数列表转发
fallbackUri: forward:/inCaseOfFailureUseThis/{segments}
这里在日常应用就比较广了, 最常用的就是业务分区的处理, 比如常见的 工作区(workspace) 和 区域(Region) 概念:
- 工作区(workspace): 请求路径首个路径参数为应用标识, 如
/1000/login代表需要请求app=1000的login服务 - 区域(Region): 请求路径首个参数为
UUID, 和上面工作区类似并适用于云服务器相关容器对象等切换
spring:
cloud:
gateway:
routes:
# 工作区网关
- id: workspace-service
uri: lb://workspace-service:8088
predicates:
- Path=/workspace/{segment}/** # 断言提取参数
- AddRequestHeader=F-Workspace-id, {segment} # 创建 F-Workspace-id 参数 header
filters:
- StripPrefix=2 # 将路径请求跳过前面两个路径参数将 '/workspace/{segment}/**' 转变为 '/**' 到远程服务
# 区域网关
- id: region-service
uri: lb://region-service:8088
predicates:
- Path=/region/{segment}/** # 断言提取参数
- AddRequestHeader=F-Region-id, {segment} # 创建 F-Region-id 参数 header
filters:
- StripPrefix=2 # 将路径请求跳过前面两个路径参数将 '/region/{segment}/**' 转变为 '/**' 到远程服务
这样就能直接转发划分不同的区域|业务模块来处理, 像云服务这种远程控制大规模主机管理的情况很容易出现单主机崩溃|卡顿|缓慢;
所以需要按照区域(region)划分不同业务集群, 从而避免出现某个范围的同时宕机导致全线奔溃让整个远程管理服务全部停止服务.
具体参照文档: SpringBoot
大数据架构
这里仅仅作为个人参考提供的整合架构, 可以按照自身具体情况去设计和衍生自己的功能架构:
- gateway: 直接采用
SpringCloud搭建服务网关 - register: 通过
Eureka构建服务注册中心 - timer: 在
Eureka创建Quartz做定时任务 - statistics: 利用
Kafka做统计|队列|消息订阅服务, 有些持久数据最后会统一落地到数据库当中 - authority: 统一授权中心, 授权完成将对象写入共享
Redis让整个服务共享, 客户端请求持有标识就放行 - database: 采用
Clickhouse|MySQL|MariaDB|PostgreSql等方便数据落地永久存储 - container: 网关|注册中心和数据库需要单独部署实体机以外, 其他业务可以直接一键部署到
Docker|Podman之中
以上基本上涵盖了大部分大数据会用用到的工具, 可以按照日常所需再扩展追加服务组件, 比如追加 Elasticsearch|Logstatsh 等
不过也能看到本身扩展出来之后技术栈也随之更加复杂, 按照
less is more来说引入这么多依赖对于维护的要求也就变得更高
可以学习下 Dockerfile 环境部署 用来直接一键部署业务环境.
细分出来的其他业务后续要过渡给
DBA和运维做服务维护才能有精力焦距到业务服务之中, 毕竟一切前提是为了整体服务稳定