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所示

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

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

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

配置加完後重新上傳就可以成功了,如圖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);

nstance().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所示。


免費分享架構是資料,需要的直接私信我就好

來源:微信公眾號

作者:石衫的架構筆記

原文:

https://mp.weixin.qq.com/s?src=11&timestamp=1562896411&ver=1723&signature=lGQJ92AYDgPC*zWpWeZhTgXAIhdsWRg0-iNY6OkR1-dYQa3O2DfaltVtqupPk3rUF1OZBC-vinGg9nahhIGg6krLkFjyS7t*MMRMmc-2aftzGHW6ZVUQlMn55yLEKjO5&new=1?

mp.weixin.qq.com


推薦閱讀:

相关文章