书籍地址: Spring Cloud 微服务架构开发实战
在微服务场景下,随着业务的发展,原来简单的系统会变复杂,微服务的数量会不断增加. 每个微服务都会对外暴露一组粒度不同的服务,客户端想完成一个功能时需要和众多的微服务打交道,甚至加载一个页面也需要调用一系列的服务接口才能完成,这样不但加重了客户端的负担,而且对基于互联网的访问也会造成性能低下.
同时,由于需要访问多个微服务,增加客户端的复杂度,而且客户端还需要指导各个微服务之间的关系,一个某个微服务更改就会造成前端噩梦.
因此对于基于微服务的应用,客户端和微服务之间的交互面临如下挑战:
- 需要解决微服务提供的API粒度与客户端需求的匹配问题. 通常微服务提供的API是细粒度的,而客户端往往需要众多这样的API才可以完成一个功能的处理,从而迫使客户端要与多个微服务进行频繁的交互。
- 客户端的多样性,造成了在处理时需要提供不同的数据,加上客户端网络差异的影响,即使一个简单的功能,如果放在互联网应用中需要考虑的就不仅仅是功能的实现了。
- 随着业务的演进,微服务的划分随时都可能会发生变化,如果不能对客户端隐藏这些细节,就会面临牵一发而动全身的困境。
针对上述挑战,不知各位有何良策应对呢?
笔者觉得,可在客户端和微服务之间加一层 saas 统一为客户端提供接口。
复杂的请求多个微服务调用的动作由saas完成。
saas也是个微服务只不过是单纯的消费者。
6.1 API服务网关
而为了解决上述问题,其实早已经有成熟的解决方案,那就是 API服务网关(API Gateway)
API服务网关: 就是出现在微服务边界上的一个面向API的、串行集中式的、对访问请求强管控的服务,采用的是一个外观模式。
API服务网关是微服务访问的统一入口,负责服务请求路由、组合及协议转换等处理。
API服务网关的核心是: 为所有客户端请求或其它消费者提供统一的网关,通过该网关接入不同的微服务,并隐藏架构实现的细节。
对于API服务网关,我们总结如下:
- API 服务网关帮助开发者隐藏系统架构实现的细节,提供统一的入口供客户端访问,让微服务使用更为友好。
- 通过微服务的同意访问控制,简化了客户端开发的复杂度,降低了客户端于微服务之间的通信次数,客户端不需要与多个微服务之间进行通信,也不需要了解各个微服务的详细信息。
- 借助API服务网关可统一做切面任务,避免每个微服务自己开发,提升效率,是系统更加标准化。
- 通过API服务网关,可以将异构系统进行统一整合。
- API服务网关需要实现一个高可用伸缩性强的服务,避免单点失效。
- API服务网关需要对所有微服务实例所暴露的端点进行统一的管理,这为开发和运维增加了一定的难度。
6.2 Spring Cloud 与 Netflix Zuul
Zuul 是 Netflix 框架中的 高可用伸缩性强的 API服务网关 的服务组件;
其功能有二:
功能1:路由
功能2:过滤器
Zuul 是一个基于JVM路由和服务端的负载均衡器
其参考GOF设计模式中的外观模式,将细粒度的服务组合起来提供了一个粗粒度的服务,以便所有的请求都导入一个统一的入口,整个服务只需要暴露一个API,对外屏蔽了服务端的实现细节;
Netflix Zuul 组件可以用于反向代理功能,通过路由寻址请求转发到后端的服务上,并增加一些通用逻辑处理。
Zuul对请求提供了路由和过滤器两个功能,其中,路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础。过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
通过Zuul组件,可以完成以下功能:
-
动态路由
Zuul 路由服务器支持与 Eureka 服务器的整合,可以动态对注册到 Eureka 服务器中的微服务进行路由映射。另外,Zuul 提供一系列的路由规则配置,可以针对生产中的实际情况进行配置,实现微服务路由的灵活控制;
-
监控与审查
通过对一些特定的接口设置访问白名单、访问次数、访问频率等各类设置,可以在不影响微服务实现的情况下,对访问实时监控和审查处理;
-
身份认证与安全
通过 Zuul 可以将认证的部分单独抽取出来,让微服务系统无须关注认证的逻辑,只需要关注业务本身即可。
可以统一在服务网关层增加一个额外的保护层来防止恶意攻击,如果客户端直连微服务的化,则每个暴露的微服务都需要面临这个安全问题;
-
压力测试
通过 Zuul 所提供的过滤功能可以逐渐增加对某一服务集群的流量,以了解服务性能,从而及早对运维架构做出调优。
-
金丝雀、AB测试
新版本、新功能可能都需要测试用户对其的反应,通过API服务网关,可以轻松控制部分用户访问服务实例,并且可以对用户行为进行记录和分析,以便对新版本及新功能进行评价,获取应用的最优方案。
-
服务迁移
通过 Zuul 代理可以处理来自旧端点的客户端上的所有流量,将一些请求重定向到新的端点,从而慢慢地用不同地实现来替换旧端点。
-
负载剪裁/限流
为每一个负载类型分配对应的容量,对超过限定值的请求弃用,这样可以防止站点不被未知的大流量冲垮。通常,可以利用 API服务网关配置一个阈值,当请求数超过该阈值时会直接返回错误。
6.3 启用Zuul路由服务
6.3.1 构建 Zuul 路由服务器
Spring Cloud 提供的 @EnableZuulProxy
注解可以用来创建 Zuul 路由服务器,所以创建的路由服务器可以是嵌入式服务,也可以进行独立部署。
1. 编写pom.yml文件
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.turnsole</groupId>
<artifactId>shop-zuul</artifactId>
<version>0.0.1</version>
<name>shop-zuul</name>
<description>shop-zuul project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--zuul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</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>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 编写启动类
@EnableZuulProxy
@SpringBootApplication
public class ShopZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ShopZuulApplication.class, args);
}
}
很明显,重点是
@EnableZuulProxy
,它用来告诉应用,启动运行Zuul服务,并根据配置进行响应的初始化处理。
@EnableZuulProxy
,包含@EnableCircuitBreaker
和@EnableDiscoveryClient
注解。
就是说,当使用了@EnableZuulProxy
注解,就自动为该应用增加了服务的容错保护功能,同时也将@EnableZuulProxy
注解走位一个服务注册到服务治理服务器中。
3. 编写配置文件
server:
port: 8280
spring:
application:
name: ZUUL-PROXY
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
attention1:
对于微服务模式的开发, 动辄会有十几个甚至几十个微服务, 因此对于 IP 地址及端口号的分配也非常重要, 越早规范越能够避免后续的运维压力.
比如: 服务治理服务器的端口号为默认端口8761, 各业务服务器端口号从2100开始,各业务端口按100进行递增, 一个业务微服务的服务器端口按 10 进行递增
attention2:
这里先把一个在下看书过程中,遇到的书中未提及的错误摆出来,可以先不解决,直接进行下面的测试环节,先遇上看一下这个问题
问题描述:
com.netflix.zuul.exception.ZuulException: Hystrix Readed time out
出现上述问题的原因是:zuul的默认超时时间比较小
因此我们需要为zuul设置一组超时时间,因zuul启用了ribbon的负载均衡,还需要设置ribbon的超时时间,注意ribbon的超时时间要小于zuul超时时间。
zuul:
host:
connect-timeout-millis: 15000 #HTTP连接超时要比Hystrix的大
socket-timeout-millis: 60000 #socket超时
ribbon:
ReadTimeout: 10000
ConnectTimeout: 10000
这里pom文件不需要额外引入feign或ribbon的依赖,并且在上述ribbon的相关设置中是不会提醒的,但是没关系,依然可以使用,注意
:
后面的空格
4. 路由测试
- 启动 service-discovery
- 启动 user-service
- 启动 product-service
- 启动 shop-zuul
- postman 访问url
http://localhost:8280/productservice/comment/2/comments
得到的响应结果,如果配置了上面的超时时间,那么是正常响应结果的
这说明Zuul路由服务器已经将请求自动转发到商品服务微服务中。您也可以通过zuul路由服务器访问用户接口,其实也可以正常响应
Zuul路由服务器的默认设计就是能够与SpringCloud相关产品进行整合,Zuul 将默认从Eureka服务器中获取所注册的服务,然后将服务的 ID 作为请求路径中的一部分,然后将用户的请求自动转发的这些服务中
5. 负载均衡测试
- 我们先把用户微服务打一个jar包,赋予其端口号为2110,通过 java -jar 将其启动
java -jar user-service-0.0.1.jar --server.port=2110
- 通过postman访问之前访问的接服务地址
http://localhost:8280/productservice/comment/2/comments
可以看到两个用户微服务,会被交替访问
注意:这里涉及到了Zuul 负载均衡的权重问题,因此在测试的时候会有访问同一个微服务的情况,我们在后面会进行学习。
我们要知道 Zuul 路由服务可能会成为另一个访问瓶颈.
需要把握以下两个原则:
- KISS原则
- stateless原则
KISS原则
Keep it Simple and Stupid
保持 API 服务网关的简单和轻量. 服务网关只不过是服务调用过程中的一个检查点, 不应该用来处理业务的复杂性, 也不应该将其用于解决架构的难点上, 这应该是微服务需要处理的事情.
stateless原则
stateless原则是指在 Zuul 服务网关中不应该、也不可以保存有关服务调用过程中的状态数据。
6. Zuul中的Hystrix容错
Zuul 路由服务已经默认整合了 Hystrix。现在看一下 @EnableZuulProxy 注解的源码:
@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}
可以看到 Zuul 本身已经默认集成了 Hystrix 和 Ribbon,所以 Zuul 天生就拥有线程隔离和服务容错的自我保护能力,以及对服务调用的客户端负载均衡功能。
注意:当使用path 与 url的映射关系来配置路由规则时,对于路由转发的请求则不会采用 Hystrix Command 来包装,所以这类路由请求请求就没有线程隔离和服务容错保护功能,并且也不会有负载均衡能力。
因此在使用 Zuul 的时候尽量使用 path 和 serviceId 的组合进行配置,这样不仅可以保证 API 网关的健壮和稳定,也能用到 Ribbon 的客户端负载均衡功能。