Spring Cloud Zuul 那些你不知道的功能點


本文摘自於 《Spring Cloud微服務 入門 實戰與進階》 一書。

1. /routes 端點

當@EnableZuulProxy與Spring Boot Actuator配合使用時,Zuul會暴露一個路由管理端點/routes。

借助這個端點,可以方便、直觀地查看以及管理Zuul的路由。

將所有端點都暴露出來,增加下面的配置:

management.endpoints.web.exposure.include=*

訪問 http://localhost:2103/actuator/routes 可以顯示所有路由信息:

{
  "/cxytiandi/**": "http://cxytiandi.com", 
  "/hystrix-api/**": "hystrix-feign-demo", 
  "/api/**": "forward:/local", 
  "/hystrix-feign-demo/**": "hystrix-feign-demo"
}

2. /filters 端點

/fliters端點會返回Zuul中所有過濾器的信息。可以清楚的了解Zuul中目前有哪些過濾器,哪些被禁用了等詳細信息。

訪問 http://localhost:2103/actuator/filters 可以顯示所有過濾器信息:

{
  "error": [
    {
      "class": "com.cxytiandi.zuul_demo.filter.ErrorFilter", 
      "order": 100, 
      "disabled": false, 
      "static": true
    }
  ], 
  "post": [
    {
      "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter", 
      "order": 1000, 
      "disabled": false, 
      "static": true
    }
  ], 
  "pre": [
    {
      "class": "com.cxytiandi.zuul_demo.filter.IpFilter", 
      "order": 1, 
      "disabled": false, 
      "static": true
    }
  ], 
  "route": [ 
    {
      "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter", 
      "order": 10, 
      "disabled": false, 
      "static": true
    }
  ]
}

3. 文件上傳

創建一個新的Maven項目zuul-file-demo,編寫一個文件上傳的接口,如代碼清單7-20所示。

代碼清單 7-20 文件上傳接口

@RestController
public class FileController {

  @PostMapping("/file/upload")
  public String fileUpload(@RequestParam(value = "file") MultipartFile file) throws IOException {
      byte[] bytes = file.getBytes();
      File fileToSave = new File(file.getOriginalFilename());
      FileCopyUtils.copy(bytes, fileToSave);
      return fileToSave.getAbsolutePath();
  }

}

將服務注冊到Eureka中,服務名稱為zuul-file-demo,通過PostMan來上傳文件,如圖7-4所示

圖7-4

可以看到接口正常返回了文件上傳之后的路徑,接下來我們換一個大一點的文件,文件大小為1.7MB。

圖7-5

可以看到報錯了(如圖7-5所示),通過Zuul上傳文件,如果超過1M需要配置上傳文件的大小, Zuul和上傳的服務都要加上配置:

spring.servlet.multipart.max-file-size=1000Mb
spring.servlet.multipart.max-request-size=1000Mb

配置加完后重新上傳就可以成功了,如圖7-6所示。

圖7-6

第二種解決辦法是在網關的請求地址前面加上/zuul,就可以繞過Spring DispatcherServlet進行上傳大文件。

# 正常的地址
http://localhost:2103/zuul-file-demo/file/upload
# 繞過的地址
http://localhost:2103/zuul/zuul-file-demo/file/upload

通過加上/zuul前綴可以讓Zuul服務不用配置文件上傳大小,但是接收文件的服務還是需要配置文件上傳大小,否則文件還是會上傳失敗。

在上傳大文件的時候,時間比較會比較長,這個時候需要設置合理的超時時間來避免超時。

ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000

在Hystrix隔離模式為線程下zuul.ribbon-isolation-strategy=thread,需要設置Hystrix超時時間。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000

4. 請求響應信息輸出

系統在生產環境出現問題時,排查問題最好的方式就是查看日志了,日志的記錄盡量詳細,這樣你才能快速定位問題。

下面帶大家學習如何在Zuul中輸出請求響應的信息來輔助我們解決一些問題。

熟悉Zuul的朋友都知道,Zuul中有4種類型過濾器,每種都有特定的使用場景,要想記錄響應數據,那么必須是在請求路由到了具體的服務之后,返回了才有數據,這種需求就適合用post過濾器來實現了。如代碼清單7-21所示。

代碼清單 7-21 Zull獲取請求信息

HttpServletRequest req = (HttpServletRequest)RequestContext.getCurrentContext().getRequest();
System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort());
StringBuilder params = new StringBuilder("?");
// 獲取URL參數
Enumeration<String> names = req.getParameterNames();
if( req.getMethod().equals("GET") ) {
   while (names.hasMoreElements()) {
         String name = (String) names.nextElement();
         params.append(name);
         params.append("=");
         params.append(req.getParameter(name));
         params.append("&");
   }
}
        
if (params.length() > 0) {
    params.delete(params.length()-1, params.length());
}
        
System.err.println("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol());
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
      String name = (String) headers.nextElement();
      String value = req.getHeader(name);
      System.err.println("REQUEST:: > " + name + ":" + value);
}
        
final RequestContext ctx = RequestContext.getCurrentContext();
// 獲取請求體參數
if (!ctx.isChunkedRequestBody()) {
    ServletInputStream inp = null;
    try {
         inp = ctx.getRequest().getInputStream();
         String body = null;
         if (inp != null) {
            body = IOUtils.toString(inp);
            System.err.println("REQUEST:: > " + body);  
         } catch (IOException e) {
                e.printStackTrace();
         }
    }
}

輸出效果如下:

獲取響應內容第一種方式,如代碼清單7-22所示。

代碼清單 7-22 獲取響應內容(一)

try {
     Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse");
     if (zuulResponse != null) {
          RibbonHttpResponse resp = (RibbonHttpResponse) zuulResponse;
          String body = IOUtils.toString(resp.getBody());
          System.err.println("RESPONSE:: > " + body);
          resp.close();
          RequestContext.getCurrentContext().setResponseBody(body);
     }
} catch (IOException e) {
     e.printStackTrace();
}

獲取響應內容第二種方式,如代碼清單7-23所示。

代碼清單 7-23 獲取響應內容(二)

InputStream stream = RequestContext.getCurrentContext().getResponseDataStream();
try {
      if (stream != null) {
          String body = IOUtils.toString(stream);
          System.err.println("RESPONSE:: > " + body);
          RequestContext.getCurrentContext().setResponseBody(body);
      }    
} catch (IOException e) {
      e.printStackTrace();
}

為什么上面兩種方式可以取到響應內容?

在RibbonRoutingFilter或者SimpleHostRoutingFilter中可以看到下面一段代碼,如代碼清單7-24所示。

代碼清單 7-24 響應內容獲取源碼

public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    this.helper.addIgnoredHeaders();
    try {
        RibbonCommandContext commandContext = buildCommandContext(context);
        ClientHttpResponse response = forward(commandContext);
        setResponse(response);
        return response;
    }
    catch (ZuulException ex) {
        throw new ZuulRuntimeException(ex);
    }
    catch (Exception ex) {
        throw new ZuulRuntimeException(ex);
    }
}

forward()方法對服務調用,拿到響應結果,通過setResponse()方法進行響應的設置,如代碼清單7-25所示。

代碼清單 7-25 setResponse(一)

protected void setResponse(ClientHttpResponse resp) throws ClientException, IOException {
    RequestContext.getCurrentContext().set("zuulResponse", resp);
    this.helper.setResponse(resp.getStatusCode().value(),
    resp.getBody() == null ? null : resp.getBody(), resp.getHeaders());
}

上面第一行代碼就可以解釋我們的第一種獲取的方法,這邊直接把響應內容加到了RequestContext中。

第二種方式的解釋就在helper.setResponse的邏輯里面了,如代碼清單7-26所示。

代碼清單 7-26 setResponse(二)

 public void setResponse(int status, InputStream entity,
            MultiValueMap<String, String> headers) throws IOException {
    RequestContext context = RequestContext.getCurrentContext();
    context.setResponseStatusCode(status);
    if (entity != null) {
        context.setResponseDataStream(entity);
    }

    // .....
}

5. Zuul自帶的Debug功能

Zuul中自帶了一個DebugFilter,一開始我也沒明白這個DebugFilter有什么用,看名稱很容易理解,用來調試的,可是你看它源碼幾乎沒什么邏輯,就set了兩個值而已,如代碼清單7-27所示。

代碼清單 7-27 DebugFilter run方法

@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ctx.setDebugRouting(true);
    ctx.setDebugRequest(true);
    return null;
}

要想讓這個過濾器執行就得研究下它的shouldFilter()方法,如代碼清單7-28所示。
代碼清單 7-28 DebugFilter shouldFilter 方法

@Override
public boolean shouldFilter() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
    if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
      return true;
    }
    return ROUTING_DEBUG.get();
}

只要滿足兩個條件中的任何一個就可以開啟這個過濾器,第一個條件是請求參數中帶了某個參數=true就可以開啟,這個參數名是通過下面的代碼獲取的,如代碼清單7-29所示。

代碼清單 7-29 DebugFilter啟用參數(一)

private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory
      .getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "debug");

DynamicStringProperty是Netflix的配置管理框架Archaius提供的API,可以從配置中心獲取配置,由於Netflix沒有開源Archaius的服務端,所以這邊用的就是默認值debug,如果大家想動態去獲取這個值的話可以用攜程開源的Apollo來對接Archaius,這個在后面章節給大家講解。

可以在請求地址后面追加debug=true來開啟這個過濾器,參數名稱debug也可以在配置文件中進行覆蓋,用zuul.debug.parameter指定,否則就是從Archaius中獲取,沒有對接Archaius那就是默認值debug。

第二個條件代碼,如代碼清單7-30所示。

代碼清單 7-30 DebugFilter啟用參數(二)

private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory
      .getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, false);
  

是通過配置zuul.debug.request來決定的,可以在配置文件中配置zuul.debug.request=true開啟DebugFilter過濾器。

DebugFilter過濾器開啟后,並沒什么效果,在run方法中只是設置了DebugRouting和DebugRequest兩個值為true,於是繼續看源碼,發現在很多地方有這么一段代碼,比如com.netflix.zuul.FilterProcessor.runFilters(String)中,如代碼清單7-31所示。

代碼清單 7-31 Debug信息添加

if (RequestContext.getCurrentContext().debugRouting()) {
    Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}

當debugRouting為true的時候就會添加一些Debug信息到RequestContext中。現在明白了DebugFilter中為什么要設置DebugRouting和DebugRequest兩個值為true。

到這步后發現還是很迷茫,一般我們調試信息的話肯定是用日志輸出來的,日志級別就是Debug,但這個Debug信息只是累加起來存儲到RequestContext中,沒有對使用者展示。

繼續看代碼吧,功夫不負有心人,在org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.addResponseHeaders()這段代碼中看到了希望。如代碼清單7-32所示。

代碼清單 7-32 Debug信息設置響應

private void addResponseHeaders() {
     RequestContext context = RequestContext.getCurrentContext();
     HttpServletResponse servletResponse = context.getResponse();
     if (this.zuulProperties.isIncludeDebugHeader()) {
         @SuppressWarnings("unchecked")
         List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY);
         if (rd != null) {
            StringBuilder debugHeader = new StringBuilder();
            for (String it : rd) {
               debugHeader.append("[[[" + it + "]]]");
            }
            servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
         }
     }
}

核心代碼在於this.zuulProperties.isIncludeDebugHeader(),只有滿足這個條件才會把RequestContext中的調試信息作為響應頭輸出,在配置文件中增加下面的配置即可:

zuul.include-debug-header=true

最后在請求的響應頭中可以看到調試內容,如圖7-7所示。
圖7-7

本文摘自於 《Spring Cloud微服務 入門 實戰與進階》 一書。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM