1. 程式人生 > >SpringCloud(3) :微服務閘道器(Zuul)

SpringCloud(3) :微服務閘道器(Zuul)

在一個實際業務當中通常都會呼叫多個服務介面,而每個服務介面的ip/埠or域名都不一樣,這樣在實際呼叫中會變得十分繁瑣,而且當服務介面ip/埠or域名修改後,業務系統也需要進行相應的修改,大大增加了開發維護成本,所以一般的做法都是在多個服務介面上游再新增一層,我們通常稱之為閘道器。閘道器能夠實現多種功能,比如反向代理,負載均衡,攔截器。在攔截器中我們還可以實現身份驗證,反網路爬蟲等等功能。 在Spring Cloud中,可以使用Zuul來實現閘道器層。 在這裡插入圖片描述 服務呼叫者向Zuul服務傳送呼叫請求,Zuul服務通過各種filter進行身份驗證,反爬蟲等等操作後,根據配置資訊從Eureka服務註冊中心獲取到呼叫的服務的實際ip/埠等資訊,然後將請求發向服務提供者。

PS:本片內容都基於Spring Boot 2.X

這裡繼續在上篇中的專案基礎上進行擴充套件。 總體為1個服務註冊中心,1個配置中心,3個服務(serviceI,serviceII,serviceIII),1個閘道器。其中I,II兩個服務為不同的服務,剩下的III服務與I服務完全一樣,註冊用的service id一致,只有埠和提供的服務輸出不同(來驗證負載均衡)。 整體程式碼下載Spring Cloud Zuul服務示例

一.服務註冊中心

SpringCloudServiceCenter專案繼續維持不變,啟動。(埠8761)

二.配置中心

SpringCloudConfig專案也繼續維持不變,啟動。(埠8091) 同時新建myServiceII-dev.properties和myServiceII-prod.properties(內容和myServiceI對應的相同即可),並向遠端git倉庫推送。

三.服務I

SpringCloudServiceI專案維持不變 service id 為myServiceI,並添加了路徑/myServiceI,埠為8762

四.服務II

新建SpringCloudServiceII專案,配置部分與SpringCloudServiceI大致一樣。 service id 為myServiceII,並添加了路徑/myServiceII,埠為8763 (1)pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.my.serviceII</groupId>
	<artifactId>SpringCloundServiceII</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>SpringCloundServiceII</name>
	<description>com.my.serviceII</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<spring-cloud.version>Greenwich.M1</spring-cloud.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<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-config</artifactId>
		</dependency>
		<!--新增  重試機制 的依賴
   		 因網路的抖動等原因導致config-client在啟動時候訪問config-server沒有訪問成功從而報錯,
    	希望config-client能重試幾次,故重試機制
    	-->
    	<dependency>
        	<groupId>org.springframework.retry</groupId>
       		<artifactId>spring-retry</artifactId>
    	</dependency>
    	<dependency>
        	<groupId>org.springframework.boot</groupId>
        	<artifactId>spring-boot-starter-aop</artifactId>
    	</dependency>
    	<!-- spring cloud actuator 配置資訊重新整理 -->
		<dependency>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>


</project>

(2)application.properties配置

server.servlet.context-path=/myServiceII
server.port=8763
#spring.application.name=myServiceII
spring.application.name=myServiceII
eureka.client.service-url.defautZone=http://serviceCenter:8761/eureka/

#retry
#和重試機制相關的配置有如下四個:
# 配置重試次數,預設為6
spring.cloud.config.retry.max-attempts=6
# 間隔乘數,預設1.1
spring.cloud.config.retry.multiplier=1.1
# 初始重試間隔時間,預設1000ms
spring.cloud.config.retry.initial-interval=1000
# 最大間隔時間,預設2000ms
spring.cloud.config.retry.max-interval=2000

#spring 2.X actuator  
#http://ip:port/actuator/refresh
management.endpoints.web.exposure.include=refresh,health,info

(3)bootstrap.properties配置

#config
#開啟配置服務發現
spring.cloud.config.discovery.enabled=true
#配置服務例項名稱
spring.cloud.config.discovery.service-id=myConfigServer
#配置檔案所在分支
spring.cloud.config.label=master
spring.cloud.config.profile=dev
#配置服務中心
spring.cloud.config.uri=http://localhost:8091/

#啟動失敗時能夠快速響應
spring.cloud.config.fail-fast=true

(4)新增ServiceApiController.java,其實和serviceI的一樣,這裡就是用來模擬另一個服務的介面。

package com.my.serviceII.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value="/Api")
public class ServiceApiController {
	@Value("${name}")
	private String name;
	
	@ResponseBody
	@RequestMapping(value="/getInfo")
	public String getInfo() {
		return "serviceII+"+name;
	}
}

(5)啟動項SpringCloundServiceIiApplication.java

package com.my.serviceII;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class SpringCloundServiceIiApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringCloundServiceIiApplication.class, args);
	}
}

五.服務III

實際作為服務I的副本,當然直接用服務I改個埠號啟動也可以。我這裡是又新建了一個服務III(SpringCloudServiceIII) 內容和伺服器基本一致,不同的地方在配置中將埠號修改為8764 (1)修改application.properties

server.port=8764

(2)修改獲取的配置,改為dev。 修改bootstrap.properties

spring.cloud.config.profile=dev

(3)修改介面內容 ServiceApiController

package com.my.serviceIII.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RefreshScope
@RequestMapping(value="/Api")
public class ServiceApiController {
	@Value("${name}")
	private String name;
	
	@ResponseBody
	@RequestMapping(value="/getInfo")
	public String getInfo() {
		return "serviceIII+"+name;
	}
}

六.路由閘道器(Zuul)

新建SpringCloudZuul專案。 (1)pom.xml

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

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

(2)application.properties配置(重點)

spring.application.name=api-gateway
server.port=5555
 
#忽略所有請求,不包括zuul.routes指定的路徑
#zuul.ignored-services=* 
# routes to serviceId 這裡邊是通過serviceid來繫結地址,當在路徑後新增/api-a/   則是訪問service-A對應的服務。
# ** 表示多層級,*表示單層級
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=myServiceI
 
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=myServiceII
 
# routes to url  這裡是繫結具體的ip地址
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8762/
 
eureka.client.service-url.defautZone=http://serviceCenter:8761/eureka/

這裡配置當訪問/api-a/**路徑時將會把請求傳送到service id為myServiceI的服務,而上面的服務I和服務III的service id都是myServiceI,所以當訪問該路徑時將會被負載均衡。同時也可以採用zuul.routes.api-a-url.url來配置實際url地址,這裡訪問/api-a-url/**時將會轉發到服務I的介面。

(3)啟動項SpringCloundZuulApplication.java

package com.my.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class SpringCloundZuulApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringCloundZuulApplication.class, args);
	}
}

七.驗證

(1)現在啟動3個服務和Zuul閘道器。 能在註冊介面http://localhost:8761/看到如下情形,可以看到service id 為myServiceI的服務有2個,分別為8762(服務I)和8764(服務III) 在這裡插入圖片描述

(2)分別測試下3個服務介面是否能調通。正常情況為如下輸出 服務I 在這裡插入圖片描述 服務II 在這裡插入圖片描述 服務III 在這裡插入圖片描述

下面開始使用路由閘道器訪問服務介面,路由閘道器埠為5555 (3)負載均衡 多次訪問http://localhost:5555/api-a/myServiceI/Api/getInfo能看到如下兩種輸出 在這裡插入圖片描述 在這裡插入圖片描述

證明負載均衡正常執行。

(4)訪問服務II 在這裡插入圖片描述

(5)上面是通過service id 對映,這裡試試通過url對映的方式訪問 在這裡插入圖片描述 OK,能訪問到服務I。

八.熔斷處理

當路由閘道器後的微服務宕機或者無響應時,服務呼叫者卻還在不停的呼叫服務,每個呼叫的請求都會超時,久而久之Zuul路由閘道器就會累積大量的請求,這些又會消耗大量的系統資源,最後導致Zuul路由閘道器掛掉。所以Zuul提供了一套回退機制,能夠使得出現這類大量請求堆積時,讓系統進行熔斷處理,快速返回給呼叫者一些資訊,從而減輕Zuul路由閘道器負擔。 這裡有一個坑,大部分介紹Zuul熔斷處理的文章都會提到使用的是 Zuulfallbackprovider介面實現的回退,但是由於版本更替,該介面已經過時,現在所以用的是FallbackProvider介面,二者主要區別如下:

http://www.itmuch.com/spring-cloud/edgware-new-zuul-fallback/ Dalston及更低版本通過實現ZuulFallbackProvider 介面,從而實現回退; Edgware及更高版本通過實現FallbackProvider 介面,從而實現回退。 在Edgware中: FallbackProvider是ZuulFallbackProvider的子介面。 ZuulFallbackProvider已經被標註Deprecated ,很可能在未來的版本中被刪除。 FallbackProvider介面比ZuulFallbackProvider多了一個ClientHttpResponse fallbackResponse(Throwable cause); 方法,使用該方法,可獲得造成回退的原因。

這裡在SpringCloudZuul基礎上進行擴充套件 (1)新增ServiceFallback.java 在getRoute()方法中填寫需要進行回退處理的服務的service id,例如我寫的是服務I的service id :myServiceI。如果想要讓所有服務都進行回退處理的話就 return "*"

package com.my.zuul.fallback;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import com.netflix.hystrix.exception.HystrixTimeoutException;

/**
 * 
 * zuulfallbackprovider 已過時
 *
 */
@Component
public class ServiceFallback implements FallbackProvider{

	@Override
	public String getRoute() {
		// TODO Auto-generated method stub
		return "myServiceI";//service id ,如果想要支援所有的就return "*" or return null;
	}

	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
		if (cause instanceof HystrixTimeoutException) {
			return response(HttpStatus.GATEWAY_TIMEOUT);
	    } else {
	      return this.fallbackResponse();
	    }
	}

	public ClientHttpResponse fallbackResponse() {
		return this.response(HttpStatus.INTERNAL_SERVER_ERROR);
	}

	private ClientHttpResponse response(final HttpStatus status) {
		return new ClientHttpResponse() {
			
			@Override
			public HttpStatus getStatusCode() throws IOException {
				return status;
			}

			@Override
			public int getRawStatusCode() throws IOException {
				return status.value();
			}

			@Override
			public String getStatusText() throws IOException {
				return status.getReasonPhrase();
			}

			@Override
			public void close() {
			}

			@Override
			public InputStream getBody() throws IOException {
				String result = "服務不可用,請稍後再試。"+getStatusCode();
				return new ByteArrayInputStream(result.getBytes());
			}

			@Override
			public HttpHeaders getHeaders() {
				// headers設定
				HttpHeaders headers = new HttpHeaders();
				MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
				headers.setContentType(mt);
				return headers;
			}
	    };
	}
}

然後啟動註冊中心,配置中心,服務II,閘道器。 通過閘道器訪問服務I和III http://localhost:5555/api-a/myServiceI/Api/getInfo 然後也可以通過呼叫getStatusCode()這些方法來返回具體出錯的原因。而在ZuulFallbackProvider介面中是不提供具體錯誤資訊返回的,這也是ZuulFallbackProvider過時的原因。然後訪問服務II,應該是可以訪問的。 在這裡插入圖片描述

九.ZuulFilter過濾器

通常可以使用過濾器來進行身份驗證,反爬蟲等操作。 身份驗證一般來說在服務呼叫方都會發送一個token過來,然後就可以使用攔截器來效驗該token了,比如jwt驗證框架。 ZuulFilter使用方式 新建IdentityVerificationFilter.java

package com.my.zuul.filter;

import javax.servlet.http.HttpServletRequest;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

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

@Component
public class IdentityVerificationFilter extends ZuulFilter{

	@Override
	public boolean shouldFilter() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public Object run() throws ZuulException {
		// TODO Auto-generated method stub
		System.out.println("my filter");
		
		RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
 
        Object token = request.getParameter("token");
 
        //校驗token
        if (token == null) {
            //"token為空,禁止訪問!"
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        } else {
            //TODO 根據token獲取相應的登入資訊,進行校驗(略)
        }
 
        return null;
	}

	@Override
	public String filterType() {
		// TODO Auto-generated method stub
		return FilterConstants.PRE_TYPE;
	}

	@Override
	public int filterOrder() {
		// TODO Auto-generated method stub
		return 0;
	}

}

然後啟動註冊中心,配置中心,服務I,閘道器。 訪問http://localhost:5555/api-a/myServiceI/Api/getInfo 從控制檯可以看到輸出 在這裡插入圖片描述 網頁上訪問為401 在這裡插入圖片描述

然後我們使用http://localhost:5555/api-a/myServiceI/Api/getInfo?token=123訪問 在這裡插入圖片描述 就能訪問了。當然具體的token效驗規則還要看你的選型。

還有一種就是後面的微服務使用了spring security中的basic Auth(即:不允許匿名訪問,必須提供使用者名稱、密碼),也可以在Filter中處理。 可以這樣使用,修改run() 方法

	public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
         ......
        //新增Basic Auth認證資訊
        ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));
 
        return null;
    }