更多内容参见个人技术博客,无广告欢迎关注
1.1 API网关
1.1.1 概念
Amazon Andriod 应用的商品详情页显示内容
l 购物车中的商品数量
l 历史订单
l 客户评论
l 低库存预警
l 送货选项
l 各种推荐,包括经常与该商品一起购买的其它商品、购买该商品的客户购买的其它商品、购买该商品的客户看过的其它商品
l 其它的购物选择
使用单体应用程序架构时,移动客户端通过向应用程序发起一次REST请求,来获取这些数据,负载均衡器将请求路由给N个相同的应用程序实例中的其中之一,然后应用程序会查询各种数据库表,并将响应返回给客户端。
相反,若是采用微服务架构,显示在产品页上的数据会分布在不同的微服务上,下面列举了可能与产品详情页数据有关的一些微服务:
l 购物车服务~购物车中的件数
l 订单服务~历史订单
l 目录服务~商品基本信息、如名称、图片和价格
l 评论服务~客户的评论
l 库存服务~低库存预警
l 送货服务~送货选项、期限和费用,这些信息单独从送货方API获取
l 推荐服务~推荐商品
1.1.2 客户端与微服务直接通信的问题
1. 客户端需求和每个微服务暴露的细粒度 API 不匹配。在这个例子中,客户端需要发送 7 个独立请求。在更复杂的应用程序中,可能要发送更多的请求;按照 Amazon 的说法,他们在显示他们的产品页面时就调用了数百个服务。然而,客户端通过 LAN 发送许多请求,这在公网上可能会很低效,在移动网络上就根本不可行。这种方法还使得客户端代码非常复杂。
2. 客户端直接调用微服务的另一个问题是,部分服务使用的协议对 web 并不友好。一个服务可能使用 Thrift 二进制 RPC,而另一个服务可能使用 AMQP 消息传递协议。不管哪种协议对于浏览器或防火墙都不够友好,最好是内部使用。在防火墙之外,应用程序应该使用诸如 HTTP 和 WebSocket 之类的协议。
3. 另一个缺点是,它会使得微服务难以重构。随着时间推移,我们可能想要更改系统拆分服务的方式。例如,我们可能合并两个服务,或者将一个服务拆分成两个或更多服务。然而,如果客户端与微服务直接通信,那么执行这类重构就非常困难了。
1.1.3 使用 API 网关构建微服务
通常来说,使用 API 网关是更好的解决方式。API 网关是一个服务器,也可以说是进入系统的唯一节点。API 网关封装内部系统的架构,并且提供 API 给各个客户端。它还可能还具备授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等功能。下图展示了一个适应当前架构的 API 网关。
API 网关负责服务请求路由、组合及协议转换。客户端的所有请求都首先经过 API 网关,然后由它将请求路由到合适的微服务。API 网关经常会通过调用多个微服务并合并结果来处理一个请求。它可以在 web 协议(如 HTTP 与 WebSocket)与内部使用的非 web 友好协议之间转换。
API 网关还能为每个客户端提供一个定制的 API。通常,它会向移动客户端暴露一个粗粒度的 API。以产品详情的场景为例,API 网关可以提供一个端点(/productdetails?productid=xxx),使移动客户端可以通过一个请求获取所有的产品详情。API 网关通过调用各个服务(产品信息、推荐、评论等等)并合并结果来处理请求。
Netflix API 网关是一个很好的 API 网关实例。Netflix 流媒体服务提供给成百上千种类型的设备使用,包括电视、机顶盒、智能手机、游戏系统、平板电脑等等。
最初,Netflix 试图为他们的流媒体服务提供一个通用的 API。然而他们发现,由于各种各样的设备都有自己独特的需求,这种方式并不能很好地工作。如今,他们使用一个 API 网关,通过运行与针对特定设备的适配器代码,来为每种设备提供定制的 API。通常,一个适配器通过调用平均 6 到 7 个后端服务来处理每个请求。Netflix API 网关每天处理数十亿请求。
1.1.4 API 网关的优点和缺点
如你所料,使用 API 网关有优点也有不足。使用 API 网关的最大优点是,它封装了应用程序的内部结构。客户端只需要同网关交互,而不必调用特定的服务。API 网关为每一类客户端提供了特定的 API,这减少了客户端与应用程序间的交互次数,还简化了客户端代码。
API 网关也有一些不足。它增加了一个我们必须开发、部署和维护的高可用组件。还有一个风险是,API 网关变成了开发瓶颈。为了暴露每个微服务的端点,开发人员必须更新 API 网关。API网关的更新过程要尽可能地简单,这很重要;否则,为了更新网关,开发人员将不得不排队等待。不过,虽然有这些不足,但对于大多数现实世界的应用程序而言,使用 API 网关是合理的。
1.1.5 Zuul
Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行。
Zuul可以通过加载动态过滤机制,从而实现以下各项功能:
l 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
l 审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
l 动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
l 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
l 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
l 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
l 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
除此之外,Netflix公司还利用Zuul的功能通过金丝雀版本实现精确路由与压力测试。
1.1.6 拓展:金丝雀测试(灰度测试)
现今我们的服务集群数量都非常大,系统操作的人非常多,此时更新代码也不能停机来更新,必须逐步更新。例如:金丝雀版本发布方式。
金丝雀发布:一般先发 1 台,或者一个小比例,例如 2% 的服务器,主要做流量验证用,也称为金丝雀 (Canary) 测试(国内常称灰度测试)。以前旷工开矿下矿洞前,先会放一只金丝雀进去探是否有有毒气体,看金丝雀能否活下来,金丝雀发布由此得名。简单的金丝雀测试一般通过手工测试验证,复杂的金丝雀测试需要比较完善的监控基础设施配合,通过监控指标反馈,观察金丝雀的健康状况,作为后续发布或回退的依据。
1.1.7 烂接口的特征
l 没有接口文档
l 出入参数风格不一致
l 异常提示不友好
l 模型接口混乱,粗暴升级
l 稳定性差,还找不到人
1.1.8 Zuul生命周期
1.2 Zuul实现网关
1.2.1 创建Maven项目
1.2.2 pom.xml
父工程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 http://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.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wood</groupId>
<artifactId>spring-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud</name>
<description>Demo project for Spring Boot</description>
<!--Maven项目可以继承,三个子工程-->
<modules>
<module>eureka-server</module>
<module>provider-a</module>
<module>provider-b</module>
<module>consumer-client</module>
<module>consumer-client-ribbon</module>
<module>feign-client</module>
<module>consumer-hystrix</module>
<module>geteway-zuul</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<!--公共依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
注意zuul也要注册到注册中心Eureka上
<?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>
<parent>
<groupId>com.wood</groupId>
<artifactId>spring-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wood</groupId>
<artifactId>geteway-zuul</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>geteway-zuul</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>LATEST</version>
</dependency>
</dependencies>
</project>
1.2.3 application.properties
zuul的配置:app-a路由名称随意,path映射路径,访问路径就无需敲入serviceId那么长,可以利用path映射路径简写。
旧的方式:http://localhost:8095/provider-user/hello/wood
新的方式:http://localhost:8095/user/hello/wood
server.port=8095
spring.application.name=getway-zuul
eureka.client.serviceUrl.defaultZone=http://localhost:6001/eureka
# 定义根目录下日志级别
logging.level.root=DEBUG
# zuul配置:app-a路由名称随意,path映射路径,访问路径就无需敲入serviceId那么长,可以利用path映射路径简写
# 默认会把所有注册在eureka上的微服务都反向代理
##############################################################################
zuul.routes.app-a.path=/user/**
zuul.routes.app-a.service-id=provider-user
##############################################################################
# 这样配置后,只有provider-user的被映射到user,其它的服务访问形式不变,如果有多个之间用逗号隔开
##############################################################################
#zuul.routes.app-a.path=/user/**
#zuul.routes.app-a.service-id=provider-user
zuul.ignored-services="*"
##############################################################################
ignoredServices: '*'
这样配置后,只有provider-user的被映射到user,其它的服务访问形式不变,如果有多个之间用逗号隔开
直接访问: http://localhost:8095/hello/wood
未映射访问(无法访问): http://localhost:8095/provider-user/hello/wood
ZUUL访问: http://localhost:8095/user/hello/wood
1.2.4 GetewayZuulApplication.java
package com.wood.getewayzuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableEurekaClient // zuul服务要注册到Eureka上
@EnableZuulProxy // 开启Zuul
public class GetewayZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GetewayZuulApplication.class, args);
}
}
1.2.5 测试
启动EurekaServerApplication
启动ProviderAApplication
启动GetewayZuulApplication
直接访问: http://localhost:8081/hello/wood
未映射访问: http://localhost:8095/provider-user/hello/wood
ZUUL访问: http://localhost:8095/user/hello/wood
1.2.6 日志
1.3 拓展:Zuul中实现断路器
1.3.1 HelloFallback.java
package cn.wood.fallback;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
@Component //Zuul实现熔断机制
public class HelloFallback implements ZuulFallbackProvider{
@Override
public String getRoute() {
return "provider-user";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(("fallback "+HelloFallback.this.getRoute()).getBytes());
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST.value();
}
@Override
public void close() {
}
};
}
}
执行业务正常,停掉provider-user服务,断路器生效
但这样实现的断路器刷新时间比较长,一旦服务正常,刷新正常。
demo下载 spring-cloud-zuul.zip