https://github.com/carnellj/spmia-chapter6
服务网关充当服务客户端和被调用的服务之间的中介。有了服务网关,服务客户端永远不会直接调用单个服务的URL,而是将所有调用都放到服务网关上。
可以在服务网关中实现的横切关注点包括以下几个:
-
静态路由:服务网关将所有的服务调用放置在单个URL和API路由的后面。这简化了开发,因为开发人员只需要直到所有服务的一个服务端点就可以了
-
动态路由:服务网关可以检查传入的服务请求,根据来自传入请求的数据和服务调用者的身份执行智能路由。例如,将参与测试版本程序的客户的所有调用路由到特定服务集群的服务,不影响其他用户使用正常的其他服务
-
验证和授权:由于所有服务调用都经过服务网关进行路由,所以服务网关是检查服务调用者是否已经进行了验证并被授权进行服务调用的自然场所
-
度量数据收集和日志记录:当服务调用通过服务网关时,可以使用服务网关来收集数据和日志信息,还可以使用服务网关确保在用户请求上提供关键信息以确保日志统一(例如,通过服务网关时拦截HTTP提供关联ID添加到HTTP首部,可以让Sleuth通过关联ID查找收集调用相关服务代码的日志)
Zuul
Zuul是一个服务网关,Zuul提供了许多功能,具体包括以下几个:
-
将应用程序中的所有服务的路由映射到一个URL:Zuul不局限于一个URL,可以定义多个路由条目,但最常见的用例是构建一个单一的入口点,所有服务都经过这个入口点
-
构建可以对通过网关的请求进行检查和操作的过滤器
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
@SpringBootApplication
// 使服务成为一个Zuul服务器
// 注意:@EnableZuulServer注解将创建一个Zuul服务器,它不会加载任何Zuul反向代理过滤器,也不会使用Eureka进行服务发现
// 开发人员想要构建自己的路由服务,而不适用Zuul预置的功能时会使用@EnableZuulServer
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
配置Zuul与Eureka通信
Zuul代理服务器默认设计为在Spring产品上工作。因此,Zuul将自动使用Eureka来通过服务ID查找服务,然后使用Ribbon对来自Zuul的请求进行客户端负载均衡。application.yml要添加Eureka的配置。
Zuul的核心是一个反向代理。反向代理是一个中间服务器,它位于尝试访问资源的客户端和资源本身之间。反向代理负责捕获客户端的请求,然后代表客户端调用远程资源。
服务客户端认为它只与Zuul通信。Zuul要与下游服务进行沟通,Zuul必须知道如何将进来的调用映射到下游路由。Zuul有几种机制来做到这一点:
-
通过服务发现自动映射路由
-
使用服务发现手动映射路由
-
使用静态URL手动映射路由
通过服务发现自动映射路由
如果没有指定任何路由,Zuul将自动使用正在调用的服务的Eureka服务ID(organizationservice),并将其映射到下游服务实例。
有了Eureka,开发人员还可以添加和删除服务的实例,而无需修改Zuul。例如,可以向Eureka添加新服务,Zuul将自动路由到该服务。
如果要查看由Zuul服务器管理的路由,可以通过Zuul服务器上的/routes端点来访问这些路由,这将返回服务中所有映射的列表:
http://localhost:5555/routes
// 返回结果
服务路由基于Eureka ID创建 Eureka ID
{
"/configserer/**": "configserver",
"/specialrouteservice/**": "specialroutesservice",
"/organizationservice/**": "organizationservice",
"/licensingservice/**": "licenseservice"
}
使用服务发现手动映射路由
假设开发人员希望通过缩短组织名称来简化路由,而不是通过默认路由 /organizationservice/v1/organizations/{organization-id}
在Zuul中访问组织服务:
zuul:
routes:
#将organizationservice简化为organization名称
organizationservice: /organization/**
修改后访问路径:http://localhost:5555/organization/v1/organizations/{organization-id}
使用/routes端点访问查找路由,会发现有两个端点:
{
"/organization/**": "organizationservie",
"/organizationservice/**": "organizationservice"
}
如果想要排除Eureka服务ID路由的自动映射,只提供自定义的组织服务路由,可以向application.yml文件添加一个额外的zuul参数ignored-services:
zuul:
#忽略默认的Eureka ID映射,如果要排除所有基于Eureka的路由,设置为'*'
ignored-services: 'organizationservice'
routes:
organizationservice: /organization/**
服务网关的一种常见模式是通过使用/api之类的标记来为所有的服务调用添加前缀,从而区分API路由与内容路由:
zuul:
ignored-services: '*'
#所有已定义的服务访问路径都添加/api前缀
prefix: /api
routes:
organizationservice: /organization/**
licensingservice: /licensing/**
/routes端点访问查找路由:
{
"/api/organization/**": "organizationservice"
"/api/licensing/**": "licensingservice"
}
路径访问:
http://localhost:5555/api/organization/v1/{organization-id}
http://localhost:5555/api/licensing/v1/{licensing-id}
使用静态URL手动映射路由
静态URL是指向未通过Eureka服务发现引擎注册的服务URL,Zuul可以用来路由那些不受Eureka管理的服务。
zuul:
routes:
#Zuul用于在内部识别服务的关键字
licensestatic:
#许可证服务的静态路由
path: /licensestatic/**
#已建立许可证服务的静态实例,它将被直接调用,而不是由Zuul通过Eureka调用
#url: http://licenseservice-static:8081
#定义一个服务ID,该服务ID将用于在Ribbon中查找服务
serviceId: licensestatic
#在Ribbon中禁用Eureka
ribbon:
eureka:
enabled: false
#指定请求会路由到的服务器列表
licensestatic:
ribbon:
listOfServers: http://licenseservice-static1:8081, http://licenseservice-static2:8082
/routes端点访问查找路由:
{
"/api/licensestatic/**": "licensestatic"
"/api/organization/**": "organizationservice"
"/api/licensing/**": "licensingservice"
"/api/auth/**": "authenticationservice"
}
静态映射路由并在Ribbon中禁用Eureka支持会造成一个问题,那就是禁用了对通过Zuul服务网关运行的所有服务的Ribbon支持。这意味着Eureka服务器将承受更多的负载,因为Zuul无法使用Ribbon来缓存服务的查找。
记住,Ribbon不会在每次发出调用的时候都调用Eureka。相反,它将本地缓存服务实例的位置,然后定期检查Eureka是否有变化。缺少了Ribbon,Zuul每次需要解析服务的位置时都会调用Eureka。
动态重新加载路由配置
动态重新加载路由的功能非常有用,因为它允许在不回收Zuul服务器的情况下更改路由的映射。现有的路由可以被快速修改,以及添加新的路由,都无须在环境中回收每个Zuul服务器。可以使用Spring Cloud Config来外部化Zuul路由,zuulservice.yml、zuulservice-dev.yml和zuulservice.prod.yml
通过Spring Cloud Config与Git远端配置后,如果想要动态地添加新的路由映射,只需对配置文件进行更改,然后将配置文件提交回Spring Cloud Config从中提取配置数据的Git存储库,然后,将更改提交给Github。Zuul公开了基于POST的端点路由/refresh,其作用是让Zuul重新加载路由配置。(简单说:就是在项目代码中修改zuulservice-*.yml路由配置,提交到git库中,然后调用/refresh端口刷新即可)
Zuul和服务超时
Zuul使用Hystrix和Ribbon库,来帮助放置长时间运行的服务调用影响服务网关的性能。
// 修改超时时间应该根据服务需要设置,在无法优化减少服务调用时间的情况下才设置超时规避
zuul:
prefix: /api
routes:
organizationservice: /organization/**
licensingservice: /licensing/**
debug:
request: true
#为所有通过Zuul运行的服务设置Hystrix超时,如果要设置特定服务超时,将default修改为对应服务的Eureka ID
hystrix:
command:
default: #default可以修改为特定Eureka ID
execution:
isolation:
thread:
timeoutInMilliseconds: 2500
#即使上面设置了Hystrix的超时,但Ribbon同样默认5s超时,还需要改Ribbon的超时时间
licensingservice:
ribbon:
ReadTimeout: 7000
Zuul过滤器
Zuul允许开发人员使用Zuul网关内的过滤器构建自定义逻辑。过滤器可用于实现每个服务请求在执行时都会经过的业务逻辑链。
Zuul支持以下3种类型的过滤器:
-
前置过滤器:前置过滤器在Zuul将实际请求发送到目的地之前被调用。前置过滤器通常执行确保服务具有一致的消息格式(例如,关键的HTTP首部是否设置妥当)的任务,或者充当看门人,确保调用该服务的用户已通过验证和授权
前置过滤器可以在HTTP请求到达实际服务之前对HTTP请求进行检查和修改。前置过滤器不能将用户重定向到不同的端点或服务
-
后置过滤器:后置过滤器在目标服务被调用并将响应发送回客户端后被调用。通常后置过滤器会用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应
-
路由过滤器:路由过滤器用于在调用目标服务之前拦截调用。通常使用路由过滤器来确定是否需要进行某些级别的动态路由
路由过滤器可以更改服务所指向的目的地。路由过滤器可以将服务调用重定向到Zuul服务器被配置的发送路由以外的位置。但Zuul路由过滤器不会执行HTTP重定向,而是会终止传入的HTTP请求,然后代表原始调用者调用路由。这意味着路由过滤器必须完全负责动态路由的调用,并且不会执行HTTP重定向
如果路由过滤器没有动态地将调用者重定向到新路由,Zuul服务器将发送到最初的目标服务的路由
// 过滤器工具类
public class FilterUtils {
// Zuul提供了一个专门的RequestContext,它具有几个额外的方法来访问Zuul的特定值。该请求上下文是com.netflix.zuul.context包的一部分
public String getCorrelationId() {
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(CORRELATION_ID) != null) {
return ctx.getRequest().getHeader(CORRELATION_ID);
} else {
return ctx.getZuulRequestHeaders().get(CORRELATION_ID);
}
}
// Zuul不允许直接添加或修改传入请求中的HTTP请求首部,想要添加tmx-correlation-id并且在以后的过滤器访问它,
// 要通过addZuulRequestHeader()添加
// addZuulRequestHeader()方法将一个单独的HTTP首部映射,这个映射是在请求通过Zuul服务器流经这些过滤器时添加的
// 当Zuul服务器调用目标服务时,包含在ZuulRequestHeader映射中的数据将被合并
public void setCorrelationId(String correlationId) {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(CORRELATION_ID, correlationId);
}
}
// 前置过滤器
// 所有Zuul过滤器必须扩展ZuulFilter类并覆盖方法
@Component
public class TrackingFilter extends ZuulFilter {
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER = true;
private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
// 自定义的过滤器工具类
@Autowired
FilterUtils filterUtils;
// 设置过滤器类型为前置过滤器
@Override
public String filterType() {
return filterUtils.PRE_FILTER_TYPE;
}
// 指示不同类型的过滤器的执行顺序
@Override
public int filterOrder() {
return FILTER_ORDER;
}
// 是否要拦截请求
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
// run()方法是每次服务通过过滤器时执行的代码。这里在HTTP首部添加tmx-correlation-id
@Override
public Object run() {
// tmx-correlation-id已经添加到HTTP首部
if (isCorrelationIdPresent()) {
logger.debug("tmx-correlation-id found in tracking filter:{}", filterUtils.getCorrelationId());
} else {
// txm-correlation-id没有在HTTP首部,使用Zuul.addZuulRequestHeader()添加到HTTP首部
filterUtils.setCorrelationId(generateCorrelationId());
logger.debug("tmx-correlation-id generated in tracking filter:{}", filterUtils.getCorrelationId());
}
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Processing incoming request for {}", ctx.getRequest().getRequestURI());
return null;
}
private boolean isCorrelationIdPresent() {
if (filterUtils.getCorrelationId() != null) {
return true;
}
return false;
}
private String generateCorrelationId() {
return UUID.randomUUID().toString();
}
}
既然已经确保每个流经Zuul的微服务调用都添加了关联ID,那么如何确保:
-
正在被调用的微服务可以很容易访问关联ID
-
下游服务调用微服务时可能也会将关联ID传播到下游调用中
// 信息存储的POJO
@Component
public class UserContext {
public static final String CORRELATION_ID = "tmx-correlation-id";
public static final String AUTH_TOKEN = "tmx-auth-token";
public static final String USER_ID = "tmx-user-id";
public static final String ORG_ID = "tmx-org-id";
private String correlationId = new String();
private String authToken = new String();
private String userId = new String();
private String orgId = new String();
getter and setter....
}
// 信息存储管理类
@Component
public class UserContextHolder {
private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
public static final UserContext getContext() {
UserContext context = userContext.get();
if (context == null) {
context = createEmptyContext();
userContext.set(context);
}
return userContext.get();
}
public static final void setContext(UserContext context) {
Assert.notNull(context, "Only non-null UserContext instances are permitted");
userContext.set(context);
}
public static final UserContext createEmptyContext() {
return new UserContext();
}
}
// 拦截HTTP请求,将关联ID从HTTP映射到UserContext
@Component
public class UserContextFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
UsetContextHolder
.getContext()
.setCorrelationId(request.getHeader(UserContext.CORRELATION_ID));
UsetContextHolder
.getContext()
.setUserId(request.getHeader(UsetContext.USER_ID));
UserContextHolder
.getContext()
.setAuthToken(request.getHeader(UserContext.AUTH_TOKEN));
UsetContextHolder
.getContext()
.setOrgId(request.getHeader(UserContext.ORG_ID));
filterChain.doFilter(request.getHeader(UserContext.ORG_ID));
}
}
// 该类用于将关联ID注入基于HTTP的传出服务器请求中
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) {
HttpHeaders headers = request.getHeaders();
headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
return execution.execute(request, body);
}
}
// 在使用RestTemplate时持有UserContextInterceptor,可以操作拦截HTTP
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
List interceptors = template.getInterceptors();
if (interceptors == null) {
// 将UserContextInterceptor添加到已创建的RestTemplate
template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
} else {
interceptors.add(new UserContextInterceptor());
template.setInterceptors(interceptors);
}
return template;
}
前置过滤器的拦截在上面已经结束,这里处理后置过滤器的拦截
// 后置过滤器,服务响应返回的HTTP拦截设置关联ID
@Component
public class ResponseFilter extends ZuulFilter {
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER = true;
private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
@Autowired
FilterUtils filterUtils;
@Override
public String filterType() {
return FilterUtils.POST_FILTER_TYPE;
}
@Override
public int filterOrder() {
return FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Adding the correlation id to the outbound headers.{}", filterUtils.getCorrelationId());
// 获取原始HTTP请求,传入关联ID
ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());
logger.debug("Completing outgoing request for {}.", ctx.getResponse().getRequestURI());
return null;
}
}
A/B测试是推出新功能的地方,在这里有一定比例的用户能够使用新功能,而其余的用户仍然使用旧服务。实现这种需要就需要用到动态路由。实现动态路由需要停止传过来的HTTP请求,自己处理路由请求到指定的服务。
在迄今为止所看到的所有过滤器中,实现Zuul路由过滤器所需进行的编码工作最多,因为通过路由过滤器,开发人员将接管Zuul功能的核心部分——路由,并使用自己的功能替换掉它。
// 路由过滤器,如果根据获取的A/B测试信息得到需要进行A/B测试,则重定向到A/B指定测试服务;否则照常路由
@Component
public class SpecialRoutesFilter extends ZuulFilter {
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER = true;
private static final Logger logger = LoggerFactory.getLogger(SpecialRoutesFilter.class);
@Autowired
FilterUtils filterUtils;
@Override
public String filterType() {
return filterUtils.ROUTE_FILTER_TYPE;
}
@Override
public int filterOrder() {
return FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
AbsTestingRoute abTestRoute = getAbRoutingInfo(filterUtils.getServiceId());
// 是否进行A/B测试,如果是则自己重定向路由服务,否则照常路由到指定服务
if (abTestRoute != null && useSpecialRoute(abTestRoute)) {
String route = buildRouteString(
ctx.getRequest().getRequestURI(),
abTestRoute.getEndpoint(),
ctx.get("serviceId").toString());
forwardToSpecialRoute(route);
}
return null;
}
// 调用SpecialRoutes服务,以确定该服务ID是否有路由记录
private AbTestingRoute getAbRoutingInfo(String serviceName) {
ResponseEntity<AbTestingRoute> restExchange = null;
try {
restExchange = restTemplate.exchange(
"http://specialroutesservice/v1/route/abtesting/{serviceName}",
HttpMethod.GET,
null,
AbTestingRoute.class,
serviceName);
} cacth (HttClientErrorException ex) {
if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
throw ex;
}
}
return restExchange.getBody();
}
// 接收路径的权重,生成一个随机数,并确定是否将请求转发到替代服务
private boolean useSpecialRoute(AbTestingRoute testRoute) {
Random random = new Random();
// 检查路由是否为活跃状态
if (testRoute.getActive().equals("N"))
return false;
int value = random.netxtInt((10 - 1) + 1) + 1;
if (testRoute.getWeight() < value)
return true;
return false;
}
// 转发路由
// ProxyRequestHelper是Spring Cloud提供的类,带有用于代理服务请求的辅助方法
private ProxyRequestHelper helper = new ProxyRequestHelper();
private void forwardToSpecialRoute(String route) {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
// 创建将发送到服务的所有HTTP请求首部的副本
MultiValueMap<String, String> headers = helper.buildZuulRequestHeaders(request);
// 创建所有HTTP请求参数的副本
MultiValueMap<String, String> params = helper.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
// 创建将被转发到替代服务的HTTP主体的副本
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0)
ctx.setChunkedRequestBody();
helper.addIgnoredHeaders();
CloseableHttpClient httpClient = null;
HttpResponse response = null;
try {
httpClient = HttpClients.createDefault();
// forward()调用替代服务
response = forward(
httpClient,
verb,
route,
request,
headers,
params,
requestEntity);
setResponse(response); // 将服务调用的结果保存回Zuul服务器
} catch (Exception e) {
;
}
}
}