Spring Cloud(5):服務路由(Zuul)

Zuul簡介html

全部微服務之間的調用,都應該經過服務網關進行路由,服務網關充當服務與服務之間的中介。服務網關像交通警察同樣指揮交通,將用戶引導到目標微服務實例。服務網關還充當着應用程序內全部微服務調用的入站流量的守門人。有了服務網關,服務客戶端永遠不會直接調用單個服務的URL,而是將全部調用都放到服務網關上。java

 

構建一個Zuul Spring boot項目python

首先,在pom.xml中添加依賴spring-cloud-starter-netflix-zuul。git

<!-- Spring cloud starter: netflix-zuul -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

其次,在啓動類Application中加入@EnableZuulProxy註解。github

@SpringBootApplication
@EnableZuulProxy public class ServerZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerZuulApplication.class, args);
    }
}

此外,它仍是一個Eureka Client和Config Client,如何配置Eureka Client和Config Client請看前面章節。web

 

在Zuul中配置路由spring

Zuul的核心是一個反向代理,即一箇中間服務器,它位於客戶端服務器與資源服務器之間,客戶端服務器只需訪問反向代理服務器,而反向代理服務器負責捕獲客戶端請求,而後表明客戶端調用遠程資源。配置Zuul有3種方式:sql

(1)經過服務發現自動映射路由,此時不須要任何配置。api

好比咱們正常訪問一個在Eureka Server註冊的服務(Eureka的服務ID爲app-sql):安全

http://localhost:10200/app-sql/sql-sp-search/list(格式爲http://[host]:[port]/[context-path]/[path])

若是使用Zuul訪問,則爲:

http://localhost:10030/server-zuul/app-sql/app-sql/sql-sp-search/list(格式爲http://[host]:[port]/[context-path]/[app service-id]/[app context-path]/[path])

 

(2)經過服務發現手動映射路由,Zuul使用了Hystrix和Ribbon庫,來幫助方式長時間運行服務調用而影響服務網關的性能。

zuul:
  # 排除全部的基於Eureka的服務ID註冊的路由
  ignored-services: '*'
  # 添加前綴
  prefix: /api
  # Eureka的服務ID
  routes:
    app-sql: /s1/**
    app-one: /s2/**
    app-anther-one: /s3/**
# 設置Hystrix超時(default能夠替換成具體的某個服務ID)
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 18000
# 設置Ribbon超時(若是是具體的某個服務ID,能夠用[service-id].ribbon)
ribbon:
  ConnectTimeout: 1000
  ReadTimeout: 8000
# Zuul不會將敏感HTTP首部(如Cookie,Set-Cookie,Authorization)轉發到下游服務。這裏排除了Authorization爲後面的OAuth2服務
sensitiveHeaders: Cookie,Set-Cookie

此時url爲:http://localhost:10030/server-zuul/api/s1/app-sql/sql-sp-search/list(格式爲http://[host]:[port]/[context-path]/[prefix]/[app routes.app-sql]/[app context-path]/[path])

[注1] 通常來講,hystrixTimeout >= ribbonTimeout(ReadTimeout + ConnectTimeout)。若是小於,則會出現警告(參考AbstractRibbonCommand.getHystrixTimeout())。其中ribbonTimeout的計算公式能夠參考AbstractRibbonCommand.getRibbonTimeout()。

這裏計算公式是ribbonTimeout = (ReadTimeout + ConnectTimeout)*(MaxAutoRetries+ 1)*(MaxAutoRetriesNextServer + 1) = (8000 + 1000)* 1 * 2 = 18000ms,因此hystrixTimeout要設置>=18000。

[注2]  這裏配置的sensitiveHeaders會在Spring Cloud Security OAuth2中用到。

 

(3)使用靜態URL手動映射路由

有些服務沒有向Eureka Server註冊,並無受到Eureka Server的管理,好比一個用python寫的服務,這時仍然能夠創建Zuul直接路由到靜態URL,而且能夠手動配置Hystrix和Ribbon作到熔斷和負載均衡。

zuul:
  routes:
    python-service:
      path: /ps1/**
      # 定義一個服務ID
      serviceId: python-service
hystrix:
  command:
    python-service:
execution: isolation: thread: timeoutInMilliseconds: 18000 python-service: ribbon: NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList # 若是python-service服務有多個實例,則能夠負載均衡映射到多個路由 listOfServers: http://localhost:9221,http://localhost:9222 # 設置ribbon的timeout ConnectTimeout: 1000 ReadTimeout: 8000 MaxTotalHttpConnections: 500 MaxConnectionsPerHost: 100

[注1] 參考https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.1.2.RELEASE/single/spring-cloud-netflix.html#netflix-zuul-reverse-proxy

 

過濾器

當咱們經過網關自定義邏輯時(如安全性,日誌,服務跟蹤等),咱們可使用Zuul過濾器

(1)前置過濾器(PRE Filters):在Zuul將請求發送到目的地前調用,能夠檢查request header,驗證用戶信息,log記錄等。

(2)路由過濾器(ROUTING Filters):調用目標服務前調用。好比它能夠將服務調用重定向到另外一個地方,這裏的重定向並非HTTP重定向,而是會終止傳入的HTTP請求,而後再表明原始調用者發送新的請求。

(3)後置過濾器(POST Filters):在目標服務被調用並返回響應後調用。好比在response header中添加一些信息。

(4)Error過濾器(ERROR Filters):發生error時調用。

它們之間的關係以下圖:

[注] 參考https://github.com/Netflix/zuul/wiki/How-it-Works

 

下面是3個過濾器的代碼示例:

package com.mytools.filter;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

/**
 * 前置過濾器<br>
 */
@Component
public class PreFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(PreFilter.class);

    private static final String PRE_FILTER_TYPE = "pre";
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;

    /* Filter type: PRE Filter
     * @see com.netflix.zuul.ZuulFilter#filterType()
     */
    @Override
    public String filterType() {
        return PRE_FILTER_TYPE;
    }

    /* 過濾器的執行順序
     * @see com.netflix.zuul.ZuulFilter#filterOrder()
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /* 是否執行過濾器
     * @see com.netflix.zuul.IZuulFilter#shouldFilter()
     */
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    /* run()是每次服務經過過濾器時執行的代碼
     * @see com.netflix.zuul.IZuulFilter#run()
     */
    @Override
    public Object run() {

        logger.debug("<<<<< PreFilter start >>>>>");
        RequestContext ctx = RequestContext.getCurrentContext();
        printReqHeader(ctx);
        printZuulReqHeader(ctx);
        logger.debug("<<<<< PreFilter end >>>>>");

        return null;
    }

    private void printReqHeader(RequestContext ctx) {

        HttpServletRequest req = ctx.getRequest();
        List<String> headerNameList = new ArrayList<>();

        if (ctx.getRequest() != null) {
            Enumeration<String> headerNames = req.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                headerNameList.add(headerNames.nextElement());
            }
        }

        if (headerNameList.isEmpty()) {
            logger.info("----- Original Request Header is NULL. -----");
        } else {
            logger.info("----- Original Request Header: -----");
            for (String headerName : headerNameList) {
                logger.info(String.format("%s: %s", headerName, req.getHeader(headerName)));
            }
        }
    }

    private void printZuulReqHeader(RequestContext ctx) {
        Map<String, String> reqMap = ctx.getZuulRequestHeaders();
        if (reqMap == null || reqMap.isEmpty()) {
            logger.info("----- Zuul Request Header is NULL. -----");
        } else {
            logger.info("----- Zuul Request Header: -----");
            reqMap.forEach((p, q) -> {
                logger.info(String.format("%s: %s", p, q));
            });
        }
    }
}
PreFilter
package com.mytools.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;

/**
 * 路由過濾器<br>
 */
@Component
public class RoutingFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(PostFilter.class);

    public static final String ROUTE_FILTER_TYPE = "route";
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;

    /* Filter type: ROUTING Filter
     * @see com.netflix.zuul.ZuulFilter#filterType()
     */
    @Override
    public String filterType() {
        return ROUTE_FILTER_TYPE;
    }

    /* 過濾器的執行順序
     * @see com.netflix.zuul.ZuulFilter#filterOrder()
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /* 是否執行過濾器
     * @see com.netflix.zuul.IZuulFilter#shouldFilter()
     */
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    /* run()是每次服務經過過濾器時執行的代碼
     * @see com.netflix.zuul.IZuulFilter#run()
     */
    @Override
    public Object run() {

        logger.debug("<<<<< RoutingFilter start >>>>>");
        logger.info("This is Routing Filter.");
        logger.debug("<<<<< RoutingFilter end >>>>>");

        return null;
    }
}
RoutingFilter
package com.mytools.filter;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.util.Pair;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

/**
 * 後置過濾器<br>
 */
@Component
public class PostFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(PostFilter.class);

    private static final String POST_FILTER_TYPE = "post";
    private static final int FILTER_ORDER = 1;
    private static final boolean SHOULD_FILTER = true;

    /* Filter type: POST Filter
     * @see com.netflix.zuul.ZuulFilter#filterType()
     */
    @Override
    public String filterType() {
        return POST_FILTER_TYPE;
    }

    /* 過濾器的執行順序
     * @see com.netflix.zuul.ZuulFilter#filterOrder()
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /* 是否執行過濾器
     * @see com.netflix.zuul.IZuulFilter#shouldFilter()
     */
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    /* run()是每次服務經過過濾器時執行的代碼
     * @see com.netflix.zuul.IZuulFilter#run()
     */
    @Override
    public Object run() {

        logger.debug("<<<<< PostFilter start >>>>>");
        RequestContext ctx = RequestContext.getCurrentContext();
        printResHeader(ctx);
        printZuulResHeader(ctx);
        logger.debug("<<<<< PostFilter end >>>>>");

        return null;
    }

    private void printResHeader(RequestContext ctx) {

        HttpServletResponse res = ctx.getResponse();
        List<String> headerNameList = new ArrayList<>();

        if (ctx.getRequest() != null) {
            headerNameList.addAll(res.getHeaderNames());
        }

        if (headerNameList.isEmpty()) {
            logger.info("----- Original Response Header is NULL. -----");
        } else {
            logger.info("----- Original Response Header: -----");
            for (String headerName : headerNameList) {
                logger.info(String.format("%s: %s", headerName, res.getHeader(headerName)));
            }
        }
    }

    private void printZuulResHeader(RequestContext ctx) {
        List<Pair<String, String>> resList = ctx.getZuulResponseHeaders();
        if (resList == null || resList.isEmpty()) {
            logger.info("----- Zuul Response Header is NULL. -----");
        } else {
            logger.info("----- Zuul Response Header: -----");
            resList.forEach(elem -> {
                logger.info(String.format("%s: %s", elem.first(), elem.second()));
            });
        }
    }
}
PostFilter

 

使用Actuator查詢路由和過濾器信息

Zuul新添加了兩個Endpoints用於查看路由和過濾器信息,只需做如下配置便可。

## Actuator info (need add '/actuator' prefix)
management:
  endpoints:
    web:
      exposure:
        # routes: 查看全部路由 | filters: 查看全部過濾器
        include: routes,filters,info,health
相關文章
相關標籤/搜索