为什么要使用服务网关
在跨多个服务进行调用的过程中会遇到以下问题:
- 在构建的每个服务中很难始终实现这些功能:开发人员专注交付,很容易忘记实现服务日志或跟踪等
- 正确是实现这些功能是一个挑战:每个在开发的服务进行诸如微服务安全的建立与配置可能是很痛苦的
- 这会在所有服务中创建一个顽固的依赖:在所有服务中共享的公共框架构建的功能越多,重新编译和部署或更改就越困难
为了解决这个问题,需要将这些横切关注点抽象成一个独立且作为应用程序中所有微服务调用的过滤器和路由服务。这种横切关注点被称为服务网关。服务客户端不在直接调用服务。取而代之是,服务网关作为单个策略执行点,所有调用都通过服务网关进行路由,然后被路由到最终目的地。
Zuul完成的操作:
- 将所有服务调用放在一个URL后面,并使用服务发现将这些调用映射到实际的服务实例。
- 将关联ID注入流经服务网关的每个服务调用中
- 在从客户端发回的HTTP响应中注入关联ID
- 构建一个动态路由机制,将各个具体的组织路由到服务实力端点,该端点与其他人使用的服务实例端点不同
什么是服务网关
服务网关中实现的横切关注点包括以下几个:
- 静态路由:服务网关将所有服务调用放在单个 URL 和 API 路由的后面
- 动态路由:检查传入服务请求,根据请求的数据和调用者身份执行智能路由
- 验证和授权:所有服务都经服务网关进行路由,检查服务是否已经进行了验证并授权进行服务调用
- 度量数据收集和日志记录:服务调用通过网关时,可以收集数据和日志信息,确保在用户请求上提供关键信息以确保日志统一
在构建服务网关时牢记以下几点:
- 在单独的服务组面前,负载均衡器仍然很有用。这种情况下,加负载均衡器放到多个服务网关实例前面的是一个恰当的设计,它确保服务网关实现可以伸缩性。将负载均衡器置于所有服务实例的前面并不是一个好主意,因为它会称为瓶颈
- 要保持为服务网关编写的代码是无状态的。不要在内存中为服务网关存储任何信息。
- 要保持为服务网关编写的代码是轻量的。服务网关时服务调用的“阻塞点”,多个数据库调用的复杂代码,可能是服务网关中难以追溯的性能问题的根源
Spring Cloud Zuul
Zuul具体包括以下几个功能:
- 将应用程序中的所有服务的路由映射到一个URl(不仅限一个URL)
- 构建可以对通过网关的请求进行检查和操作的过滤器
要开始使用Zuul,需要完成下面3件事:
- 建立一个Zuul Spring Boot项目,配置 Maven
- 使用Spring Cloud 注解修改这个额项目,声明Zuul服务
- 配置 Zuul 以便 Eureka 通信
配置一个Zuul Spring Boot 项目:
pom文件:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
配置Zuul 与 Eureka 进行通信 Application.yml:
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在Zuul中配置路由
Zuul的核心是一个反向代理。反向代理是一个中间服务器,他位于尝试访问资源的客户端和资源本身之间。
在微服务架构下,zuul需要与下游服务进行沟通。zuul有几种机制来做到这一点:
- 通过服务发现自动映射路由
- 使用服务发现手动映射路由
- 使用静态URL手动映射路由
通过服务发现自动映射路由
Zuul所有路由映射 都是 在 application.yml 文件汇总定义路由来完成。但是,Zuul可以 根据服务 ID自动路由 请求,而不需要配置。
如果没有 配置任何路由,Zuul将自动 使用正在调用的服务的Eureka服务ID,并映射到下游服务实例。
使用服务发现手动映射路由
上图中有显示
如果想禁用Eureka映射的路由,可以将 以下属性设置为*
zuul.ignored-services: "*"
静态路由
如果希望zuul来路由不收Eureka管理的服务,可以使用静态定义的URL
Zuul服务超时
Zuul使用Netflix的Hystrix和Ribbon库,来帮助防止长时间运行的服务调用影响网关性能
配置Hystrix超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 2500
配置指定服务超时时间
hystrix.command.licensingservice.execution.isolation.thread.timeoutInMilliseconds: 2
Zuul过滤器
Zuul支持3种类型过滤器:
- 前置过滤器
- 后置过滤器
- 路由过滤器
所有的过滤器都必须继承 ZuulFilter
前置过滤器类说明:
构建UserCOntextFilter类拦截器,将zuul生成的主键ID,与用户信息进行绑定,传入restTemplate请求
将构建好的UserContext传给zuul初始化的RestTemplate
这样每当使用@Autowired 注入的RestTemplate时,就会附带UserContextInterceptor
后置过滤器
这样在调用服务后,就会返回关联的ID
动态路由过滤器
Zuul路由过滤器,可以为服务客户端的调用添加智能路由
编写路由类
@Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); AbTestingRoute abTestRoute = getAbRoutingInfo( filterUtils.getServiceId() ); if (abTestRoute!=null && useSpecialRoute(abTestRoute)) { String route = buildRouteString(ctx.getRequest().getRequestURI(), abTestRoute.getEndpoint(), ctx.get("serviceId").toString()); forwardToSpecialRoute(route); } return null; }
查询路由
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); } catch(HttpClientErrorException ex){ if (ex.getStatusCode()== HttpStatus.NOT_FOUND) return null; throw ex; } return restExchange.getBody(); }
转发路由
private void forwardToSpecialRoute(String route) { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); MultiValueMap<String, String> headers = this.helper .buildZuulRequestHeaders(request); MultiValueMap<String, String> params = this.helper .buildZuulRequestQueryParams(request); String verb = getVerb(request); InputStream requestEntity = getRequestBody(request); if (request.getContentLength() < 0) { context.setChunkedRequestBody(); } this.helper.addIgnoredHeaders(); CloseableHttpClient httpClient = null; HttpResponse response = null; try { httpClient = HttpClients.createDefault(); response = forward(httpClient, verb, route, request, headers, params, requestEntity); setResponse(response); } catch (Exception ex ) { ex.printStackTrace(); } finally{ try { httpClient.close(); } catch(IOException ex){} } }