前面的文章我们介绍了,Eureka用于服务的注册于发现,Feign支持服务的调用以及均衡负载,Hystrix处理服务的熔断防止故障扩散,Spring Cloud Config服务集群配置中心,似乎一个微服务框架已经完成了。
我们还是少考虑了一个问题,外部的应用如何来访问内部各种各样的微服务呢?在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。
1、简化客户端调用复杂度
在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言很难发现动态改变的服务实例的访问地址信息。因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入API Gateway作为轻量级网关,同时API Gateway中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。
2、数据裁剪以及聚合
通常而言不同的客户端对于显示时对于数据的需求是不一致的,比如手机端或者Web端又或者在低延迟的网络环境或者高延迟的网络环境。
因此为了优化客户端的使用体验,API Gateway可以对通用性的响应数据进行裁剪以适应不同客户端的使用需求。同时还可以将多个API调用逻辑进行聚合,从而减少客户端的请求数,优化客户端用户体验
3、多渠道支持
当然我们还可以针对不同的渠道和客户端提供不同的API Gateway,对于该模式的使用由另外一个大家熟知的方式叫Backend for front-end, 在Backend for front-end模式当中,我们可以针对不同的客户端分别创建其BFF,进一步了解BFF可以参考这篇文章:Pattern: Backends For Frontends
4、遗留系统的微服务化改造
对于系统系统而言进行微服务改造通常是由于原有的系统存在或多或少的问题,比如技术债务,代码质量,可维护性,可扩展性等等。API Gateway的模式同样适用于这一类遗留系统的改造,通过微服务化的改造逐步实现对原有系统中的问题的修复,从而提升对于原有业务响应力的提升。通过引入抽象层,逐步使用新的实现替换旧的实现。
在Spring Cloud体系中, Spring Cloud Zuul就是提供负载均衡、反向代理、权限认证的一个API gateway。
Spring Cloud Zuul路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。
下面我们通过代码来了解Zuul是如何工作的
1、添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zuulartifactId>
dependency>
引入spring-cloud-starter-zuul
包
2、配置文件
spring.application.name=gateway-service-zuul
server.port=8888
#这里的配置表示,访问/it/** 直接重定向到http://www.ityouknow.com/**
zuul.routes.baidu.path=/it/**
zuul.routes.baidu.url=http://www.ityouknow.com/
3、启动类
@SpringBootApplication
@EnableZuulProxy
public class GatewayServiceZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceZuulApplication.class, args);
}
}
启动类添加@EnableZuulProxy
,支持网关路由。
史上最简单的zuul案例就配置完了
4、测试
启动gateway-service-zuul-simple
项目,在浏览器中访问:http://localhost:8888/it/spring-cloud
,看到页面返回了:http://www.ityouknow.com/spring-cloud
页面的信息,如下:
我们以前面文章的示例代码spring-cloud-producer
为例来测试请求的重定向,在配置文件中添加:
zuul.routes.hello.path=/hello/**
zuul.routes.hello.url=http://localhost:9000/
启动spring-cloud-producer
,重新启动gateway-service-zuul-simple
,访问:http://localhost:8888/hello/hello?name=%E5%B0%8F%E6%98%8E
,返回:hello 小明,this is first messge
说明访问gateway-service-zuul-simple
的请求自动转发到了spring-cloud-producer
,并且将结果返回。
通过url映射的方式来实现zull的转发有局限性,比如每增加一个服务就需要配置一条内容,另外后端的服务如果是动态来提供,就不能采用这种方案来配置了。实际上在实现微服务架构时,服务名与服务实例地址的关系在eureka server中已经存在了,所以只需要将Zuul注册到eureka server上去发现其他服务,就可以实现对serviceId的映射。
我们结合示例来说明,在上面示例项目gateway-service-zuul-simple
的基础上来改造。
1、添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eurekaartifactId>
dependency>
增加spring-cloud-starter-eureka
包,添加对eureka的支持。
2、配置文件
配置修改为:
spring.application.name=gateway-service-zuul
server.port=8888
zuul.routes.api-a.path=/producer/**
zuul.routes.api-a.serviceId=spring-cloud-producer
eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/
3、测试
依次启动 spring-cloud-eureka
、 spring-cloud-producer
、gateway-service-zuul-eureka
,访问:http://localhost:8888/producer/hello?name=%E5%B0%8F%E6%98%8E
,返回:hello 小明,this is first messge
说明访问gateway-service-zuul-eureka
的请求自动转发到了spring-cloud-producer
,并且将结果返回。
为了更好的模拟服务集群,我们复制spring-cloud-producer
项目改为spring-cloud-producer-2
,修改spring-cloud-producer-2
项目端口为9001,controller代码修改如下:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String index(@RequestParam String name) {
return "hello "+name+",this is two messge";
}
}
修改完成后启动spring-cloud-producer-2
,重启gateway-service-zuul-eureka
。测试多次访问http://localhost:8888/producer/hello?name=%E5%B0%8F%E6%98%8E
,依次返回:
hello 小明,this is first messge
hello 小明,this is two messge
hello 小明,this is first messge
hello 小明,this is two messge
...
说明通过zuul成功调用了producer服务并且做了均衡负载。
网关的默认路由规则
但是如果后端服务多达十几个的时候,每一个都这样配置也挺麻烦的,spring cloud zuul已经帮我们做了默认配置。默认情况下,Zuul会代理所有注册到Eureka Server的微服务,并且Zuul的路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**
会被转发到serviceId对应的微服务。
我们注销掉gateway-service-zuul-eureka
项目中关于路由的配置:
#zuul.routes.api-a.path=/producer/**
#zuul.routes.api-a.serviceId=spring-cloud-producer
重新启动后,访问http://localhost:8888/spring-cloud-producer/hello?name=%E5%B0%8F%E6%98%8E
,测试返回结果和上述示例相同,说明spirng cloud zuul默认已经提供了转发功能。
到此zuul的基本使用我们就介绍完了。关于zuul更高级使用,我们下篇再来介绍。
参考:
API网关那些儿
示例代码
作者:纯洁的微笑
出处:http://www.ityouknow.com/
版权归作者所有,转载请注明出处
原文链接:https://www.cnblogs.com/ityouknow/p/6944096.html
-------------------------分割线-----------------------
https://www.cnblogs.com/yjmyzz/p/spring-cloud-zuul-demo.html
微服务架构体系中,通常一个业务系统会有很多的微服务,比如:OrderService、ProductService、UserService...,为了让调用更简单,一般会在这些服务前端再封装一层,类似下面这样:
前面这一层俗称为“网关层”,其存在意义在于,将"1对N"问题 转换成了"1对1”问题,同时在请求到达真正的微服务之前,可以做一些预处理,比如:来源合法性检测,权限校验,反爬虫之类...
传统方式下,最土的办法,网关层可以人肉封装,类似以下示例代码:
LoginResult login(...){
//TODO 预处理...
return
userService.login();
//调用用户服务的登录方法
}
Product queryProduct(...){
//TODO 预处理...
return
productService.queryProduct();
//调用产品服务的查询方法
}
Order submitOrder(...){
//TODO 预处理...
return
orderService.submitOrder();
//调用订单服务的查询方法
}
|
这样做,当然能跑起来,但是维护量大,以后各个微服务增加了新方法,都需要在网关层手动增加相应的方法封装,而spring cloud 中的zuul很好的解决了这一问题,示意图如下:
Zuul做为网关层,自身也是一个微服务,跟其它服务Service-1,Service-2, ... Service-N一样,都注册在eureka server上,可以相互发现,zuul能感知到哪些服务在线,同时通过配置路由规则(后面会给出示例),可以将请求自动转发到指定的后端微服务上,对于一些公用的预处理(比如:权限认证,token合法性校验,灰度验证时部分流量引导之类),可以放在所谓的过滤器(ZuulFilter)里处理,这样后端服务以后新增了服务,zuul层几乎不用修改。
使用步骤:
一、添加zuul依赖的jar包
1
|
compile
'org.springframework.cloud:spring-cloud-starter-zuul'
|
二、application.yml里配置路由
1
2
3
4
5
6
7
8
9
|
zuul:
routes:
api-a:
path: /api-user/**
service-id: service-provider
sensitive-headers:
api-b:
path: /api-order/**
service-id: service-consumer
|
解释一下:上面这段配置表示,/api-user/开头的url请求,将转发到service-provider这个微服务上,/api-order/开头的url请求,将转发到service-consumer这个微服务上。
三、熔断处理
如果网关后面的微服务挂了,zuul还允许定义一个fallback类,用于熔断处理,参考下面的代码:
开发人员只要在getRoute这个方法里指定要处理的微服务实例,然后重写fallbackResponse即可。
此时,如果观察/health端点,也可以看到hystrix处于融断开启状态
四、ZuulFilter过滤器
过滤器是一个很有用的机制,下面分几种经典场景演示下:
4.1、token校验/安全认证
网关直接暴露在公网上时,终端要调用某个服务,通常会把登录后的token传过来,网关层对token进行有效性验证,如果token无效(或没传token),提示重新登录或直接拒绝。另外,网关后面的微服务,如果设置了spring security中的basic Auth(即:不允许匿名访问,必须提供用户名、密码),也可以在Filter中处理。参考下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
package
com.cnblogs.yjmyzz.spring.cloud.study.gateway;
import
com.netflix.zuul.ZuulFilter;
import
com.netflix.zuul.context.RequestContext;
import
org.apache.commons.codec.binary.Base64;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import
org.springframework.stereotype.Component;
import
javax.servlet.http.HttpServletRequest;
/**
* Created by yangjunming on 2017/7/13.
*/
@Component
public
class
AccessFilter
extends
ZuulFilter {
private
static
Logger logger = LoggerFactory.getLogger(AccessFilter.
class
);
@Override
public
String filterType() {
return
FilterConstants.PRE_TYPE;
}
@Override
public
int
filterOrder() {
return
0
;
}
@Override
public
boolean
shouldFilter() {
return
true
;
}
@Override
public
Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object token = request.getParameter(
"token"
);
//校验token
if
(token ==
null
) {
logger.info(
"token为空,禁止访问!"
);
ctx.setSendZuulResponse(
false
);
ctx.setResponseStatusCode(
401
);
return
null
;
}
else
{
//TODO 根据token获取相应的登录信息,进行校验(略)
}
//添加Basic Auth认证信息
ctx.addZuulRequestHeader(
"Authorization"
,
"Basic "
+ getBase64Credentials(
"app01"
,
"*****"
));
return
null
;
}
private
String getBase64Credentials(String username, String password) {
String plainCreds = username +
":"
+ password;
byte
[] plainCredsBytes = plainCreds.getBytes();
byte
[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
return
new
String(base64CredsBytes);
}
}
|
Filter一共有4种类型,其常量值在org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 中定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// Zuul Filter TYPE constants -----------------------------------
/**
* {@link ZuulFilter#filterType()} error type.
*/
String ERROR_TYPE =
"error"
;
/**
* {@link ZuulFilter#filterType()} post type.
*/
String POST_TYPE =
"post"
;
/**
* {@link ZuulFilter#filterType()} pre type.
*/
String PRE_TYPE =
"pre"
;
/**
* {@link ZuulFilter#filterType()} route type.
*/
String ROUTE_TYPE =
"route"
;
|
安全校验,一般放在请求真正处理之前,所以上面的示例filterType指定为pre,剩下的只要在shouldFilter()、run()方法中重写自己的逻辑即可。
4.2 动态修改请求参数
zuulFilter可以拦截所有请求参数,并对其进行修改,比如:终端发过来的数据,出于安全要求,可能是经过加密处理的,需要在网关层进行参数解密,再传递到后面的服务;再比如:用户传过来的token值,需要转换成userId/userName这些信息,再传递到背后的微服务。参考下面的run方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public
Object run() {
try
{
RequestContext context = getCurrentContext();
InputStream in = (InputStream) context.get(
"requestEntity"
);
if
(in ==
null
) {
in = context.getRequest().getInputStream();
}
String body = StreamUtils.copyToString(in, Charset.forName(
"UTF-8"
));
body =
"动态增加一段内容到body中: "
+ body;
byte
[] bytes = body.getBytes(
"UTF-8"
);
context.setRequest(
new
HttpServletRequestWrapper(getCurrentContext().getRequest()) {
@Override
public
ServletInputStream getInputStream()
throws
IOException {
return
new
ServletInputStreamWrapper(bytes);
}
@Override
public
int
getContentLength() {
return
bytes.length;
}
@Override
public
long
getContentLengthLong() {
return
bytes.length;
}
});
}
catch
(IOException e) {
rethrowRuntimeException(e);
}
return
null
;
}
|
更多filter的示例,可以参考官网:https://github.com/spring-cloud-samples/sample-zuul-filters
4.3 灰度发布(Gated Launch/Gray Release)
大型分布式系统中,灰度发布是保证线上系统安全生产的重要手段,一般的做法为:从集群中指定一台(或某几台)机器,每次做新版本发布前,先只发布这些机器上,先观察一下是否正常,如果稳定运行后,再发布到其它机器。这种策略(相当于按部分节点来灰度),大多数情况下可以满足要求,但是有一些特定场景,可能不太适用。
比如:笔者所在的“美味不用等”公司,主要B端用户为各餐饮品牌的商家,多数情况下,如果新上了一个功能,希望找一些规模较小的餐厅做试点,先看看上线后的运行情况,如果运行良好,再推广到其它商家。
再比如:后端服务有N多个版本在同时运行,比如V1、V2,现在新加了一个V3版本(这在手机app应用中很常见),希望只有部分升级了app的用户访问最新的V3版本服务,其它用户仍然访问旧版本,待系统稳定后,再大规模提示用户升级。
对于这些看上去需求各异的灰度需求,其实本质是一样的:将请求(根据参数内容+业务规则),将其转向到特定的灰度机器上。Spring Cloud MicroService中有一个metadata-map(元数据)设置,可以很好的满足这类需求。
首先要引入一个jar包:(这是github上开源的一个项目ribbon-discovery-filter-spring-cloud-starter)
1
|
compile
'io.jmnarloch:ribbon-discovery-filter-spring-cloud-starter:2.1.0'
|
示例如下:
在各个服务的application.yml中设置以下metadata-map
1
2
3
4
|
eureka:
instance:
metadata-map:
gated-launch:
false
|
即:所有节点发布后,默认灰度模式为false。然后把特定的灰度机器上的配置,该参数改成true(表明这台机器是用于灰度验证的)。
然后在ZuulFilter中参考下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Override
public
Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object token = request.getParameter(
"token"
);
//校验token
if
(token ==
null
) {
logger.info(
"token为空,禁止访问!"
);
ctx.setSendZuulResponse(
false
);
ctx.setResponseStatusCode(
401
);
return
null
;
}
else
{
//TODO 根据token获取相应的登录信息,进行校验(略)
//灰度示例
RibbonFilterContextHolder.clearCurrentContext();
if
(token.equals(
"1234567890"
)) {
RibbonFilterContextHolder.getCurrentContext().add(
"gated-launch"
,
"true"
);
}
else
{
RibbonFilterContextHolder.getCurrentContext().add(
"gated-launch"
,
"false"
);
}
}
//添加Basic Auth认证信息
ctx.addZuulRequestHeader(
"Authorization"
,
"Basic "
+ getBase64Credentials(
"app01"
,
"*****"
));
return
null
;
}
|
注意18-23行,这里演示了通过特定的token参数值,将请求引导到gated-lanuch=true的机器上。(注:参考这个原理,大家可以把参数值,换成自己的version-版本号,shopId-商家Id之类)。只要请求参数中的token=1234567890,这次请求就会转发到灰度节点上。
如果有朋友好奇这是怎么做到的,可以看下io.jmnarloch.spring.cloud.ribbon.predicate.MetadataAwarePredicate 这个类:
1
2
3
4
5
6
7
8
|
@Override
protected
boolean
apply(DiscoveryEnabledServer server) {
final
RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
final
Set
final
Map
return
metadata.entrySet().containsAll(attributes);
}
|
大致原理就是拿上下文中,开发人员设置的属性 与 服务节点里的metadata-map 进行比较,如果metadata-map中包括开发人员设置的属性,就返回成功(即:选择这台服务器)
示例源码:https://github.com/yjmyzz/spring-cloud-demo