微服務網關 在微服務架構中,後端服務往往不直接開放給調用端,而是通過一個API網關根據請求的url,路由到相應的服務。當添加API網關後,在第三方調用端和服務提供方之間就創建了一面牆,這面牆直接與調用方通信進行許可權控制,後將請求均衡分發給後臺服務端。 為什麼需要API Gateway 1. 簡化客戶 ...
微服務網關
在微服務架構中,後端服務往往不直接開放給調用端,而是通過一個API網關根據請求的url,路由到相應的服務。當添加API網關後,在第三方調用端和服務提供方之間就創建了一面牆,這面牆直接與調用方通信進行許可權控制,後將請求均衡分發給後臺服務端。
為什麼需要API Gateway
1. 簡化客戶端調用複雜度
在微服務架構模式下後端服務的實例數一般是動態的,對於客戶端而言很難發現動態改變的服務實例的訪問地址信息。因此在基於微服務的項目中為了簡化前端的調用邏輯,通常會引入 API Gateway 作為輕量級網關,同時 API Gateway 中也會實現相關的認證邏輯從而簡化內部服務之間相互調用的複雜度。
2. 數據裁剪以及聚合
通常而言不同的客戶端對於顯示時對於數據的需求是不一致的,比如手機端或者 Web 端又或者在低延遲的網路環境或者高延遲的網路環境。
因此為了優化客戶端的使用體驗,API Gateway 可以對通用性的響應數據進行裁剪以適應不同客戶端的使用需求。同時還可以將多個 API 調用邏輯進行聚合,從而減少客戶端的請求數,優化客戶端用戶體驗.
3. 多渠道支持
當然我們還可以針對不同的渠道和客戶端提供不同的API Gateway,對於該模式的使用由另外一個大家熟知的方式叫Backend for front-end, 在Backend for front-end模式當中,我們可以針對不同的客戶端分別創建其BFF,進一步瞭解BFF可以參考這篇文章:Pattern: Backends For Frontends
4. 遺留系統的微服務化改造
對於系統而言進行微服務改造通常是由於原有的系統存在或多或少的問題,比如技術債務,代碼質量,可維護性,可擴展性等等。API Gateway的模式同樣適用於這一類遺留系統的改造,通過微服務化的改造逐步實現對原有系統中的問題的修複,從而提升對於原有業務響應力的提升。通過引入抽象層,逐步使用新的實現替換舊的實現。
在Spring Cloud體系中, Spring Cloud Zuul就是提供負載均衡、反向代理、許可權認證的一個API gateway。
Spring Cloud Zuul
Spring Cloud Zuul路由是微服務架構的不可或缺的一部分,提供動態路由,監控,彈性,安全等的邊緣服務。Zuul是Netflix出品的一個基於JVM路由和服務端的負載均衡器。
使用代碼示例進行說明,這裡直接將gateway服務化.
- 創建項目cloud-zuul-server-demo
引入依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
配置文件:
spring.application.name=zuul-server
server.port=8888
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
啟動類添加註解:
@SpringBootApplication
@EnableZuulProxy
public class ZuulServerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerDemoApplication.class, args);
}
}
這樣zuul就搭建成功了
和前面的項目進行結合,啟動前面的註冊中心和eureka的兩個提供者,再將zuul-server啟動,這樣就可以進行測試了.訪問localhost:8888/eureka-service-producter/hello?name=wangzhi,這樣就是通過zuul來訪問提供者,並且多刷新幾次,可以看到實現了負載均衡,均勻出現. 感興趣的可以將消費者也啟動一下,然後訪問消費者看看會出現什麼情況.額外說一下前面路徑中的eureka-service-producter其實就是訪問項目的spring.applicaiton.name,也就是註冊中心可以看到的application,這個看清楚就沒問題.
到這裡,zuul網關的使用和自動轉發機制就完成了,下麵說說zuul相對的高級知識.
Zuul的核心
Filter是Zuul的核心,用來實現對外服務的控制。Filter的生命周期有4個,分別是“PRE”、“ROUTING”、“POST”、“ERROR”,整個生命周期可以用下圖來表示。
Zuul大部分功能都是通過過濾器來實現的,這些過濾器類型對應於請求的典型生命周期。
- PRE: 這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集群中選擇請求的微服務、記錄調試信息等。
- ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
- POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
- ERROR:在其他階段發生錯誤時執行該過濾器。 除了預設的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定製一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。
Zuul中預設實現的Filter
類型 | 順序 | 過濾器 | 功能 |
pre | -3 | ServletDetectionFilter | 標記助理server的類型 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
route | 1 | DebugFilter | 標記調試標誌 |
route | 5 | PreDecorationFilter | 處理請求上下文供後續使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發 |
route | 100 | SimpleHostRoutingFilter | url請求轉發 |
route | 500 | SendForwardFilter | forward請求轉發 |
post | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
對於預設的過濾器,我們可以禁用的,在配置文件中添加(不過一般沒有取消的情況):
zuul.FormBodyWrapperFilter.pre.disable=true
自定義filter
其實也很簡單,繼承ZuulFilter就可以了,重寫四個方法,但是要瞭解個四個方法的返回值分別表示的是什麼意思?
public class MyFilter extends ZuulFilter {
/**
* 定義filter的類型,有pre、route、post、error四種
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 定義filter的順序,數字越小表示順序越高,越先執行
* @return
*/
@Override
public int filterOrder() {
return 10;
}
/**
* 表示是否需要執行該filter,true表示執行,false表示不執行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* filter需要執行的具體操作,比如說許可權認證,過濾等等
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
return null;
}
}
舉個例子:
public class TokenFilter extends ZuulFilter {
private final Logger logger = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
// 獲取請求參數信息
String token = request.getParameter("token");
if (StringUtils.isNotBlank(token)) {
//對請求進行路由
context.setSendZuulResponse(true);
context.setResponseStatusCode(200);
context.set("isSuccess", true);
return null;
} else {
//不對其進行路由
context.setSendZuulResponse(false);
context.setResponseStatusCode(400);
context.setResponseBody("token is empty");
context.set("isSuccess", false);
return null;
}
}
}
要想在過濾器中起作用,還需要額外配置一個,在啟動類中添加過濾器的Bean
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
啟動註冊中心,提供者和修改後的zuul,這次訪問的時候就需要在路徑後面加上token=xx,否則請求會被攔截.“PRE”類型的Filter做很多的驗證工作,在實際使用中我們可以結合shiro、oauth2.0等技術去做鑒權、驗證。
路由熔斷
我們的後端服務出現異常的時候,我們不希望將異常拋出給最外層,期望服務可以自動進行一降級。Zuul給我們提供了這樣的支持。當某個服務出現異常時,直接返回我們預設的信息。
我們通過自定義的fallback方法,並且將其指定給某個route來實現該route訪問出問題的熔斷處理。主要繼承ZuulFallbackProvider介面來實現,ZuulFallbackProvider預設有兩個方法,一個用來指明熔斷攔截哪個服務,一個定製返回內容。
public interface ZuulFallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
public String getRoute();
/**
* Provides a fallback response.
* @return The fallback response.
*/
public ClientHttpResponse fallbackResponse();
}
實現類通過實現getRoute方法,告訴Zuul它是負責哪個route定義的熔斷。而fallbackResponse方法則是告訴 Zuul 斷路出現時,它會提供一個什麼返回值來處理請求。後來Spring又擴展了此類,豐富了返回方式,在返回的內容中添加了異常信息,因此最新版本建議直接繼承類FallbackProvider。
案例:修改zuul-server-demo
創建HelloFallback
@Component
public class HelloFallback implements FallbackProvider {
private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
/**
* 返回指定要處理的 service。
* @return
*/
@Override
public String getRoute() {
return "eureka-service-producter";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause != null && cause.getCause() != null) {
String reason = cause.getCause().getMessage();
logger.info("Excption {}",reason);
}
return fallbackResponse();
}
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("The service is unavailable.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
進行測試,啟動註冊中心,兩個提供者,zuul. 輸入http://localhost:8888/eureka-service-producter/hello?name=wangzhi&token=wangzhi進行訪問,多刷新幾次可以看到沒有問題,這個時候將一個提供者關閉,再次刷新可以看到問題就出來了,返回The service is unavailable. 這個就是zuul的路由熔斷. 但是註意的是: ** Zuul 目前只支持服務級別的熔斷,不支持具體到某個URL進行熔斷。**
路由重試
有時候因為網路或者其它原因,服務可能會暫時的不可用,這個時候我們希望可以再次對服務進行重試,Zuul也幫我們實現了此功能,需要結合Spring Retry 一起來實現。下麵我們以上面的項目為例做演示。
- 改動zuul項目
添加依賴:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
添加配置:
#是否開啟重試功能
zuul.retryable=true
#對當前服務的重試次數
ribbon.MaxAutoRetries=2
#切換相同Server的次數
ribbon.MaxAutoRetriesNextServer=0
- 改動一個提供者項目
修改controller方法就好,只修改一個項目的就好
private Logger logger = Logger.getLogger(HelloController.class);
@GetMapping("hello")
public String hello(@RequestParam String name){
logger.info("request two name is "+name);
try{
Thread.sleep(1000000);
}catch ( Exception e){
logger.error(" hello two error",e);
}
return "hello," + name + "222222";
}
這次訪問,雖然還會返回The service is unavailable.但是可以在改動的提供者項目的控制台看到日誌列印了3遍,說明重試了兩次.
註意
開啟重試在某些情況下是有問題的,比如當壓力過大,一個實例停止響應時,路由將流量轉到另一個實例,很有可能導致最終所有的實例全被壓垮。說到底,斷路器的其中一個作用就是防止故障或者壓力擴散。用了retry,斷路器就只有在該服務的所有實例都無法運作的情況下才能起作用。這種時候,斷路器的形式更像是提供一種友好的錯誤信息,或者假裝服務正常運行的假象給使用者。
不用retry,僅使用負載均衡和熔斷,就必須考慮到是否能夠接受單個服務實例關閉和eureka刷新服務列表之間帶來的短時間的熔斷。如果可以接受,就無需使用retry。
對於微服務來說,zuul也是很重要的,所以在必要情況下應該實現高可用,也就是搭建集群,和前面搭建集群一樣道理沒有什麼區別.為了保證Zuul的高可用性,前端可以同時啟動多個Zuul實例進行負載,在Zuul的前端使用Nginx或者F5進行負載轉發以達到高可用性。
上面就是zuul的全部內容的,以後有需要再進行補充.