天天看點

萬字長文詳解微服務網關(上)

作者:馬士兵教育

1.微服務網關概述

在學習完前面的知識後,微服務架構已經初具雛形。但還有一些問題:不同的微服務一般會有不同的網絡位址,用戶端在通路這些微服務時必須記住幾十甚至幾百個位址,這對于用戶端方來說太複雜也難以維護。如下圖:

萬字長文詳解微服務網關(上)

如果讓用戶端直接與各個微服務通訊,可能會有很多問題:

  • 用戶端會請求多個不同的服務,需要維護不同的請求位址,增加開發難度
  • 在某些場景下存在跨域請求的問題
  • 加大身份認證的難度,每個微服務需要獨立認證

是以,我們需要一個微服務網關,介于用戶端與伺服器之間的中間層,所有的外部請求都會先經過微服務網關。用戶端隻需要與網關互動,隻知道一個網關位址即可,這樣簡化了開發還有以下優點:

  • 易于監控
  • 易于認證
  • 減少了用戶端與各個微服務之間的互動次數
萬字長文詳解微服務網關(上)

1.1 服務網關的概念

1.1.1 什麼是微服務網關

API網關是一個伺服器,是系統對外的唯一入口。API網關封裝了系統内部架構,為每個用戶端提供一個定制的API。API網關方式的核心要點是,所有的用戶端和消費端都通過統一的網關接入微服務,在網關層處理所有的非業務功能。通常,網關也是提供REST/HTTP的通路API。服務端通過API-GW注冊和管理服務。

1.1.2 作用和應用場景

網關具有的職責,如身份驗證、監控、負載均衡、緩存、請求分片與管理、靜态響應處理。當然,最主要的職責還是與“外界聯系”。

1.2 常見的API網關實作方式

Kong

基于Nginx+Lua開發,性能高,穩定,有多個可用的插件(限流、鑒權等等)可以開箱即用。問題:隻支援Http協定;二次開發,自由擴充困難;提供管理API,缺乏更易用的管控、配置方式。

Zuul

Netflflix開源,功能豐富,使用JAVA開發,易于二次開發;需要運作在web容器中,如Tomcat。問題:缺乏管控,無法動态配置;依賴元件較多;處理Http請求依賴的是Web容器,性能不如Nginx;

Traefifik

Go語言開發;輕量易用;提供大多數的功能:服務路由,負載均衡等等;提供WebUI問題:二進制檔案部署,二次開發難度大;UI更多的是監控,缺乏配置、管理能力;

Spring Cloud Gateway

SpringCloud提供的網關服務

Nginx+lua實作

使用Nginx的反向代理和負載均衡可實作對api伺服器的負載均衡及高可用問題:自注冊的問題和網關本身的擴充性

1.3 基于Nginx的網關實作

1.3.1 Nginx介紹

萬字長文詳解微服務網關(上)

1.3.2 正向/反向代理

(1)正向代理

萬字長文詳解微服務網關(上)

正向代理,"它代理的是用戶端,代用戶端送出請求",是一個位于用戶端和原始伺服器(origin server)之間的伺服器,為了從原始伺服器取得内容,用戶端向代理發送一個請求并指定目标(原始伺服器),然後代理向原始伺服器轉交請求并将獲得的内容傳回給用戶端。用戶端必須要進行一些特别的設定才能使用正向代理。

(2)反向代理

萬字長文詳解微服務網關(上)

多個用戶端給伺服器發送的請求,Nginx伺服器接收到之後,按照一定的規則分發給了後端的業務處理伺服器進行處理了。此時~請求的來源也就是用戶端是明确的,但是請求具體由哪台伺服器處理的并不明确了,Nginx扮演的就是一個反向代理角色。用戶端是無感覺代理的存在的,反向代理對外都是透明的,通路者并不知道自己通路的是一個代理。因為用戶端不需要任何配置就可以通路。反向代理,"它代理的是服務端,代服務端接收請求",主要用于伺服器叢集分布式部署的情況下,反向代理隐藏了伺服器的資訊

如果隻是單純的需要一個最基礎的具備轉發功能的網關,那麼使用Ngnix是一個不錯的選擇。

1.3.3 準備工作

啟動 shop_service_order 微服務,單獨請求位址:http://127.0.0.1:9001/

啟動 shop_service_product 微服務,單獨請求位址:http://127.0.0.1:9002/

安裝資料中提供的ngnix。找到ngnix.exe輕按兩下運作即可

萬字長文詳解微服務網關(上)

1.3.4 配置Nginx的請求轉發

location /api-order {
 proxy_pass http://127.0.0.1:9001/;
}
location /api-product {
 proxy_pass http://127.0.0.1:9002/;
}           

2 微服務網關Zuul

2.1 Zuul簡介

ZUUL是Netflflix開源的微服務網關,它可以和Eureka、Ribbon、Hystrix等元件配合使用,Zuul元件的核心是一系列的過濾器,這些過濾器可以完成以下功能:

  • 動态路由:動态将請求路由到不同後端叢集
  • 壓力測試:逐漸增加指向叢集的流量,以了解性能
  • 負載配置設定:為每一種負載類型配置設定對應容量,并棄用超出限定值的請求
  • 靜态響應處理:邊緣位置進行響應,避免轉發到内部叢集
  • 身份認證和安全: 識别每一個資源的驗證要求,并拒絕那些不符的請求。Spring Cloud對Zuul進行了整合和增強。

Spring Cloud對Zuul進行了整合和增強

2.2 搭建Zuul網關伺服器

(1)建立工程導入依賴

在IDEA中建立ZUUL網關工程 shop_zuul_server ,并添加響應依賴

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
           

(2)編寫啟動類

建立啟動類 ZuulServerApplication

@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的網關功能
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}           

@EnableZuulProxy :通過 @EnableZuulProxy 注解開啟Zuul網管功能

(3)編寫配置

建立配置檔案 application.yml ,并添加相應配置

server:
port: 8080 #服務端口
spring:
application:
name: api-gateway #指定服務名           

2.3 Zuul中的路由轉發

最直覺的了解:“路由”是指根據請求URL,将請求配置設定到對應的處理程式。在微服務體系中,Zuul負責接收所有的請求。根據不同的URL比對規則,将不同的請求轉發到不同的微服務處理。

zuul:
routes:
product-service: # 這裡是路由id,随意寫
path: /product-service/** # 這裡是映射路徑
url: http://127.0.0.1:9002 # 映射路徑對應的實際url位址
sensitiveHeaders: #預設zuul會屏蔽cookie,cookie不會傳到下遊服務,這裡設定為空則取
消預設的黑名單,如果設定了具體的頭資訊則不會傳到下遊服務
           

隻需要在application.yml檔案中配置路由規則即可:

  • product-service:配置路由id,可以随意取名
  • url:映射路徑對應的實際url位址
  • path:配置映射路徑,這裡将所有請求字首為/product-service/的請求,轉發到http://127.0.0.1:9002處理

配置好Zuul路由之後啟動服務,在浏覽器中輸入 http://localhost:8080/productservice/product/1 ,即可通路到訂單微服務。

萬字長文詳解微服務網關(上)

2.3.1 面向服務的路由

微服務一般是由幾十、上百個服務組成,對于一個URL請求,最終會确認一個服務執行個體進行處理。如果對每個服務執行個體手動指定一個唯一通路位址,然後根據URL去手動實作請求比對,這樣做顯然就不合理。

Zuul支援與Eureka整合開發,根據ServiceID自動的從注冊中心中擷取服務位址并轉發請求,這樣做的好處不僅可以通過單個端點來通路應用的所有服務,而且在添加或移除服務執行個體的時候不用修改Zuul的路由配置。

(1)添加Eureka用戶端依賴

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>           

(2)開啟Eureka用戶端發現功能

@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的網關功能
@EnableDiscoveryClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
           

(3)添加Eureka配置,擷取服務資訊

eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:8761/eureka/
registry-fetch-interval-seconds: 5 # 擷取服務清單的周期:5s
instance:
preferIpAddress: true
ip-address: 127.0.0.1           

(4)修改映射配置,通過服務名稱擷取

因為已經有了Eureka用戶端,我們可以從Eureka擷取服務的位址資訊,是以映射時無需指定IP位址,而是通過服務名稱來通路,而且Zuul已經內建了Ribbon的負載均衡功能。

#配置路由規則
zuul:
routes:
product-service: # 這裡是路由id,随意寫
path: /product-service/** # 這裡是映射路徑
serviceId: shop-service-product #配置轉發的微服務名稱           

serviceId: 指定需要轉發的微服務執行個體名稱

依次啟動Eureka,商品微服務,API網關,在浏覽器上通過通路 http://localhost:8080/product

service/product/1 檢視最終效果。

2.3.2 簡化的路由配置

在剛才的配置中,我們的規則是這樣的:

  • zuul.routes.<route>.path=/xxx/** :來指定映射路徑。 <route> 是自定義的路由名
  • zuul.routes.<route>.serviceId=/product-service :來指定服務名。

而大多數情況下,我們的 <route> 路由名稱往往和服務名會寫成一樣的。是以Zuul就提供了一種簡化的配置文法: zuul.routes.<serviceId>=<path>

上面的配置可以簡化為一條

zuul:
routes:
shop-service-product: /product-service/**           

2.3.3 預設的路由規則

在使用Zuul的過程中,上面講述的規則已經大大的簡化了配置項。但是當服務較多時,配置也是比較繁瑣的。是以Zuul就指定了預設的路由規則:

預設情況下,一切服務的映射路徑就是服務名本身。

例如服務名為: shop-service-product ,則預設的映射路徑就是: /shop-serviceproduct/

2.3.4 Zuul加入後的架構

萬字長文詳解微服務網關(上)

2.4 Zuul中的過濾器

通過之前的學習,我們得知Zuul它包含了兩個核心功能:對請求的路由和過濾。其中路由功能負責将外部請求轉發到具體的微服務執行個體上,是實作外部通路統一入口的基礎;而過濾器功能則負責對請求的處理過程進行幹預,是實作請求校驗、服務聚合等功能的基礎。其實,路由功能在真正運作時,它的路由映射和請求轉發同樣也由幾個不同的過濾器完成的。是以,過濾器可以說是Zuul實作API關功能最為核心的部件,每一個進入Zuul的HTTP請求都會經過一系列的過濾器處理鍊得到請求響應并傳回給用戶端。

那麼接下來,我們重點學習的就是Zuul的第二個核心功能:過濾器。

2.4.1 ZuulFilter簡介

Zuul 中的過濾器跟我們之前使用的 javax.servlet.Filter 不一樣,javax.servlet.Filter 隻有一種類型,可以通過配置 urlPatterns 來攔截對應的請求。而 Zuul 中的過濾器總共有 4 種類型,且每種類型都有對應的使用場景。

1. PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實作身份驗證、在叢集中選擇請求的微服務、記錄調試資訊等。

2. ROUTING:這種過濾器将請求路由到微服務。這種過濾器用于建構發送給微服務的請求,并使用Apache HttpClient或Netfifilx Ribbon請求微服務。

3. POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應添加标準的HTTPHeader、收集統計資訊和名額、将響應從微服務發送給用戶端等。

4. ERROR:在其他階段發生錯誤時執行該過濾器。

Zuul提供了自定義過濾器的功能實作起來也十分簡單,隻需要編寫一個類去實作zuul提供的接口

public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 來自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}           

ZuulFilter是過濾器的頂級父類。在這裡我們看一下其中定義的4個最重要的方法

shouldFilter :傳回一個 Boolean 值,判斷該過濾器是否需要執行。傳回true執行,傳回false不執行。

run :過濾器的具體業務邏輯。

filterType :傳回字元串,代表過濾器的類型。包含以下4種:

  1. pre :請求在被路由之前執行
  2. routing :在路由請求時調用
  3. post :在routing和errror過濾器之後調用
  4. error :處理請求時發生錯誤調用

filterOrder :通過傳回的int值來定義過濾器的執行順序,數字越小優先級越高。

2.4.2 生命周期

萬字長文詳解微服務網關(上)

正常流程:

請求到達首先會經過pre類型過濾器,而後到達routing類型,進行路由,請求就到達真正的服務提供者,執行請求,傳回結果後,會到達post過濾器。而後傳回響應。

異常流程:

  • 整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢後,會将請求交給POST過濾器,最後傳回給使用者。
  • 如果是error過濾器自己出現異常,最終也會進入POST過濾器,而後傳回。
  • 如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達POST過濾器了。

不同過濾器的場景:

  • 請求鑒權:一般放在pre類型,如果發現沒有通路權限,直接就攔截了
  • 異常處理:一般會在error類型和post類型過濾器中結合來處理。
  • 服務調用時長統計:pre和post結合使用。

所有内置過濾器清單:

萬字長文詳解微服務網關(上)

2.4.3 自定義過濾器

接下來我們來自定義一個過濾器,模拟一個登入的校驗。基本邏輯:如果請求中有access-token參數,則認為請求有效,放行。

@Component
public class LoginFilter extends ZuulFilter{
@Override
public String filterType() {
// 登入校驗,肯定是在前置攔截
return "pre";
}
@Override
public int filterOrder() {
// 順序設定為1
return 1;
}
@Override
public boolean shouldFilter() {
// 傳回true,代表過濾器生效。
return true;
}
@Override
public Object run() throws ZuulException {
// 登入校驗邏輯。
// 1)擷取Zuul提供的請求上下文對象
RequestContext ctx = RequestContext.getCurrentContext();
// 2) 從上下文中擷取request對象
HttpServletRequest req = ctx.getRequest();
// 3) 從請求中擷取token
String token = req.getParameter("access-token");
// 4) 判斷
if(token == null || "".equals(token.trim())){
// 沒有token,登入校驗失敗,攔截
ctx.setSendZuulResponse(false);
// 傳回401狀态碼。也可以考慮重定向到登入頁。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校驗通過,可以考慮把使用者資訊放入上下文,繼續向後執行
return null;
}
}
           

RequestContext:用于在過濾器之間傳遞消息。它的資料儲存在每個請求的ThreadLocal中。它用于存儲請求路由到哪裡、錯誤、HttpServletRequest、HttpServletResponse都存儲在RequestContext中。RequestContext擴充了ConcurrentHashMap,是以,任何資料都可以存儲在上下文中!

2.5 服務網關Zuul的核心源碼解析

萬字長文詳解微服務網關(上)

在Zuul中,整個請求的過程是這樣的,首先将請求給zuulservlet處理,zuulservlet中有一個zuulRunner對象,該對象中初始化了RequestContext:作為存儲整個請求的一些資料,并被所有的zuulfifilter共享。zuulRunner中還有 FilterProcessor,FilterProcessor作為執行所有的zuulfifilter的管理器。FilterProcessor從fifilterloader 中擷取zuulfifilter,而zuulfifilter是被fifilterFileManager所加載,并支援groovy熱加載,采用了輪詢的方式熱加載。有了這些fifilter之後,zuulservelet首先執行的Pre類型的過濾器,再執行route類型的過濾器,最後執行的是post 類型的過濾器,如果在執行這些過濾器有錯誤的時候則會執行error類型的過濾器。執行完這些過濾器,最終将請求的結果傳回給用戶端。

(1)初始化

SpringCloud對Zuul的封裝使得釋出一個ZuulServer無比簡單,根據自動裝載原則可以在 springcloud-netflix-zuul-2.1.0.RELEASE.jar 下找到 spring.factories

萬字長文詳解微服務網關(上)

ZuulServerAutoConfiguration,ZuulProxyAutoConfiguration 是Zuul服務端的自動配置類,這些配置類究竟負責什麼工作,我們繼續來看

@Configuration
@Import({RestClientRibbonConfiguration.class, OkHttpRibbonConfiguration.class,
HttpClientRibbonConfiguration.class, HttpClientConfiguration.class})
@ConditionalOnBean({Marker.class})
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
//省略
}           

ZuulProxyAutoConfiguration 繼承了 ZuulServerAutoConfiguration ,我們先看下這個配置類

@Configuration
@EnableConfigurationProperties({ZuulProperties.class})
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean({Marker.class})
public class ZuulServerAutoConfiguration {
@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(Collection<RouteLocator>
routeLocators) {
return new CompositeRouteLocator(routeLocators);
}
@Bean
@ConditionalOnMissingBean({SimpleRouteLocator.class})
public SimpleRouteLocator simpleRouteLocator() {
return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
this.zuulProperties);
}
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
protected ZuulFilterConfiguration() {
}
@Bean
public ZuulFilterInitializer zuulFilterInitializer(CounterFactory
counterFactory, TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory,
tracerFactory, filterLoader, filterRegistry);
}
}
//其他省略
}
           

整理一下這裡配置類裡面做了哪些事情呢?

  • CompositeRouteLocator:組合路由定位器,看入參就知道應該是會儲存好多個RouteLocator,構造過程中其實僅包括一個DiscoveryClientRouteLocator。
  • SimpleRouteLocator:預設的路由定位器,主要負責維護配置檔案中的路由配置。
  • ZuulController:Zuul建立的一個Controller,用于将請求交由ZuulServlet處理。
  • ZuulHandlerMapping:這個會添加到SpringMvc的HandlerMapping鍊中,隻有選擇了ZuulHandlerMapping的請求才能出發到Zuul的後續流程。
  • 注冊ZuulFilterInitializer,通過FilterLoader加載應用中所有的過濾器并将過濾器注冊到
  • FilterRegistry,那我們接下來一起看下過濾器是如何被加載到應用中的
public class ZuulFilterInitializer {
private static final Log log =
LogFactory.getLog(ZuulFilterInitializer.class);
private final Map<String, ZuulFilter> filters;
private final CounterFactory counterFactory;
private final TracerFactory tracerFactory;
private final FilterLoader filterLoader;
private final FilterRegistry filterRegistry;
public ZuulFilterInitializer(Map<String, ZuulFilter> filters, CounterFactory
counterFactory, TracerFactory tracerFactory, FilterLoader filterLoader,
FilterRegistry filterRegistry) {
this.filters = filters;
this.counterFactory = counterFactory;
this.tracerFactory = tracerFactory;
this.filterLoader = filterLoader;
this.filterRegistry = filterRegistry;
}
@PostConstruct
public void contextInitialized() {
log.info("Starting filter initializer");
TracerFactory.initialize(this.tracerFactory);
CounterFactory.initialize(this.counterFactory);
Iterator var1 = this.filters.entrySet().iterator();
while(var1.hasNext()) {
Entry<String, ZuulFilter> entry = (Entry)var1.next();
this.filterRegistry.put((String)entry.getKey(),
(ZuulFilter)entry.getValue());
}
}           

(2)請求轉發

在Zuul的自動配置中我們看到了 ZuulHandlerMapping ,為SpringMVC中 HandlerMapping 的拓展實作,會自動的添加到HandlerMapping鍊中。

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
private final RouteLocator routeLocator;
private final ZuulController zuul;
private ErrorController errorController;
private PathMatcher pathMatcher = new AntPathMatcher();
private volatile boolean dirty = true;
public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
this.routeLocator = routeLocator;
this.zuul = zuul;
this.setOrder(-200);
}
private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
} else {
Iterator var2 = routes.iterator();
while(var2.hasNext()) {
Route route = (Route)var2.next();
this.registerHandler(route.getFullPath(), this.zuul);
}
}
}
}
           

其主要目的就是把所有路徑的請求導入到ZuulController上.另外的功效是當覺察RouteLocator路由表變更,則更新自己dirty狀态,重新注冊所有Route到ZuulController。

public class ZuulController extends ServletWrappingController {
public ZuulController() {
//在這裡已經設定了ZuulServlet
this.setServletClass(ZuulServlet.class);
this.setServletName("zuul");
this.setSupportedMethods((String[])null);
}
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
ModelAndView var3;
try {
//在這裡面會調用ZuulServlet的service方法
var3 = super.handleRequestInternal(request, response);
} finally {
RequestContext.getCurrentContext().unset();
}
return var3;
}
}
           

在 ZuulController 中的 handleRequest 方法,會調用已經注冊的 ZuulServlet 完成業務請求,我們進入 ZuulServlet 看下内部是如何處理的

public void service(ServletRequest servletRequest, ServletResponse
servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest,
(HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
this.preRoute();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" +
var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}           

(3)過濾器

Zuul預設注入的過濾器可以在 spring-cloud-netflix-core.jar 中找到。

萬字長文詳解微服務網關(上)

2.6 Zuul網關存在的問題

在實際使用中我們會發現直接使用Zuul會存在諸多問題,包括:

性能問題

Zuul1x版本本質上就是一個同步Servlet,采用多線程阻塞模型進行請求轉發。簡單講,每來一個請求,Servlet容器要為該請求配置設定一個線程專門負責處理這個請求,直到響應傳回用戶端這個線程才會被釋放傳回容器線程池。如果背景服務調用比較耗時,那麼這個線程就會被阻塞,阻塞期間線程資源被占用,不能幹其它事情。我們知道Servlet容器線程池的大小是有限制的,目前端請求量大,而背景慢服務比較多時,很容易耗盡容器線程池内的線程,造成容器無法接受新的請求。

不支援任何長連接配接,如websocket

2.7 Zuul網關的替換方案

Zuul2.x版本

SpringCloud Gateway

繼續閱讀