SpringCloud微服務治理

QLQ發表於2019-05-09

1.遠端呼叫方式

瞭解一下微服務的呼叫方式

1.1.RPC

RPC,即 Remote Procedure Call(遠端過程呼叫),是一個計算機通訊協議。 該協議允許執行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計。

1.2.Http

Http協議:超文字傳輸協議,是一種應用層協議。

2.Http客戶端工具

2.1 Spring的RestTemplate

Spring提供了一個RestTemplate模板工具類,對基於Http的客戶端進行了封裝,並且實現了物件與json的序列化和反序列化,非常方便。RestTemplate並沒有限定Http的客戶端型別,而是進行了抽象,目前常用的3種都有支援:

  • HttpClient
  • OkHttp
  • JDK原生的URLConnection(預設的)

首先在專案中註冊一個RestTemplate物件,可以在啟動類位置註冊:

@SpringBootApplication
public class HttpDemoApplication {

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

	@Bean
	public RestTemplate restTemplate() {
        // 預設的RestTemplate,底層是走JDK的URLConnection方式。
		return new RestTemplate();
	}
}
複製程式碼

在測試類中直接@Autowired注入:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {

	@Autowired
	private RestTemplate restTemplate;

	@Test
	public void httpGet() {
		User user = this.restTemplate.getForObject("http://localhost/hello", User.class);
		System.out.println(user);
	}
}
複製程式碼

接下來正式介紹微服務。

3.SpringCloud

3.1.簡介

SpringCloud是Spring旗下的專案之一,官網地址:http://projects.spring.io/spring-cloud/

SpringCloud將現在非常流行的一些技術整合到一起,實現了諸如:配置管理,服務發現,智慧路由,負載均衡,熔斷器,控制匯流排,叢集狀態等等功能。其主要涉及的元件包括:

netflix

  • Eureka:註冊中心
  • Zuul:服務閘道器
  • Ribbon:負載均衡
  • Feign:服務呼叫
  • Hystix:熔斷器

接下來,我們就一一學習SpringCloud中的重要元件。

4.微服務場景模擬

首先,我們需要模擬一個服務呼叫的場景。方便後面學習微服務架構

4.1.服務提供者

我們新建一個專案,對外提供查詢使用者的服務。

4.1.1.Spring腳手架建立工程

依賴也已經全部自動引入:

<?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.leyou.demo</groupId>
	<artifactId>user-service-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>user-service-demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.1.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>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

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

複製程式碼

當然,因為要使用通用mapper,所以我們需要手動加一條依賴:

<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>
複製程式碼

非常快捷啊!

4.1.2.編寫程式碼

新增一個對外查詢的介面:

@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return this.userService.queryById(id);
    }
}
複製程式碼

Service:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) {
        return this.userMapper.selectByPrimaryKey(id);
    }
}
複製程式碼

mapper:

@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{
}
複製程式碼

實體類:

@Table(name = "tb_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 使用者名稱
    private String userName;

    // 密碼
    private String password;

    // 姓名
    private String name;

    // 年齡
    private Integer age;

    // 性別,1男性,2女性
    private Integer sex;

    // 出生日期
    private Date birthday;

    // 建立時間
    private Date created;

    // 更新時間
    private Date updated;

    // 備註
    private String note;

   // 。。。省略getters和setters
}

複製程式碼

屬性檔案,這裡我們採用了yaml語法,而不是properties:

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb01
    username: root
    password: 123
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
mybatis:
  type-aliases-package: com.leyou.userservice.pojo
複製程式碼

4.2.服務呼叫者

4.2.1.建立工程

與上面類似,需要注意的是,我們呼叫user-service的功能,因此不需要mybatis相關依賴了。

pom:

<?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.leyou.demo</groupId>
	<artifactId>user-consumer-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>user-consumer-demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.1.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>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
        <!-- 新增OkHttp支援 -->
		<dependency>
			<groupId>com.squareup.okhttp3</groupId>
			<artifactId>okhttp</artifactId>
			<version>3.9.0</version>
		</dependency>
	</dependencies>

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

</project>

複製程式碼

4.2.2.編寫程式碼

首先在啟動類中註冊RestTemplate

@SpringBootApplication
public class UserConsumerDemoApplication {

    @Bean
    public RestTemplate restTemplate() {
        // 這次我們使用了OkHttp客戶端,只需要注入工廠即可
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }

    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}
複製程式碼

通過RestTemplate遠端查詢user-service-demo中的介面:

@Component
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    public User queryUserById(Long id){
        String url = "http://localhost:8081/user/" + id;
        return this.restTemplate.getForObject(url, User.class);
    }
}
複製程式碼

然後編寫user-service,迴圈查詢UserDAO資訊:

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public List<User> querUserByIds(List<Long> ids){
        List<User> users = new ArrayList<>();
        for (Long id : ids) {
            User user = this.userDao.queryUserById(id);
            users.add(user);
        }
        return users;
    }
}
複製程式碼

編寫controller:

@RestController
@RequestMapping("consume")
public class ConsumerController {

    @Autowired
    private UserService userService;

    @GetMapping
    public List<User> consume(@RequestParam("ids") List<Long> ids) {
        return this.userService.queryUserByIds(ids);
    }
}
複製程式碼

4.3.問題?

  • 在consumer中,我們把url地址硬編碼到了程式碼中,不方便後期維護
  • consumer需要記憶user-service的地址,如果出現變更,可能得不到通知,地址將失效
  • consumer不清楚user-service的狀態,服務當機也不知道
  • user-service只有1臺服務,不具備高可用性
  • 即便user-service形成叢集,consumer還需自己實現負載均衡

其實上面說的問題,概括一下就是分散式服務必然要面臨的問題:

  • 服務管理
    • 如何自動註冊和發現
    • 如何實現狀態監管
    • 如何實現動態路由
  • 服務如何實現負載均衡
  • 服務如何解決容災問題
  • 服務如何實現統一配置

以上的問題,我們都將在SpringCloud中得到答案。

5.Eureka註冊中心

5.1.原理圖

基本架構:

1525597885059

  • Eureka:就是服務註冊中心(可以是一個叢集),對外暴露自己的地址
  • 提供者:啟動後向Eureka註冊自己資訊(地址,提供什麼服務)
  • 消費者:向Eureka訂閱服務,Eureka會將對應服務的所有提供者地址列表傳送給消費者,並且定期更新
  • 心跳(續約):提供者定期通過http方式向Eureka重新整理自己的狀態

5.2.入門案例

5.2.1.編寫EurekaServer

接下來我們建立一個專案,啟動一個EurekaServer:

依然使用spring提供的快速搭建工具,選擇依賴:

1525598312368

完整的Pom檔案:

<?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.leyou.demo</groupId>
	<artifactId>eureka-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>eureka-demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.1.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>
        <!-- SpringCloud版本,是最新的F系列 -->
		<spring-cloud.version>Finchley.RC1</spring-cloud.version>
	</properties>

	<dependencies>
        <!-- Eureka服務端 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
            <!-- SpringCloud依賴,一定要放到dependencyManagement中,起到管理版本的作用即可 -->
			<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>
複製程式碼

編寫啟動類:

@SpringBootApplication
@EnableEurekaServer // 宣告這個應用是一個EurekaServer
public class EurekaDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(EurekaDemoApplication.class, args);
	}
}
複製程式碼

編寫配置:

server:
  port: 10086 # 埠
spring:
  application:
    name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
  client:
    register-with-eureka: false # 是否註冊自己的資訊到EurekaServer,預設是true
    fetch-registry: false # 是否拉取其它服務的資訊,預設是true
    service-url: # EurekaServer的地址,現在是自己的地址,如果是叢集,需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:${server.port}/eureka

複製程式碼

啟動服務,並訪問:http://127.0.0.1:10086/eureka

1525604959508

1525605081129

5.2.2.將user-service註冊到Eureka

註冊服務,就是在服務上新增Eureka的客戶端依賴,客戶端程式碼會自動把服務註冊到EurekaServer中。

我們在user-service-demo中新增Eureka客戶端依賴:

先新增SpringCloud依賴:

<!-- SpringCloud的依賴 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.RC1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<!-- Spring的倉庫地址 -->
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
複製程式碼

然後是Eureka客戶端:

<!-- Eureka客戶端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
複製程式碼

在啟動類上開啟Eureka客戶端功能

通過新增@EnableDiscoveryClient來開啟Eureka客戶端功能

@SpringBootApplication
@EnableDiscoveryClient // 開啟EurekaClient功能
public class UserServiceDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(UserServiceDemoApplication.class, args);
	}
}
複製程式碼

編寫配置

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb01
    username: root
    password: 123
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
  application:
    name: user-service # 應用名稱
mybatis:
  type-aliases-package: com.leyou.userservice.pojo
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 當呼叫getHostname獲取例項的hostname時,返回ip而不是host名稱
    ip-address: 127.0.0.1 # 指定自己的ip資訊,不指定的話會自己尋找
複製程式碼

注意:

  • 這裡我們新增了spring.application.name屬性來指定應用名稱,將來會作為應用的id使用。
  • 不用指定register-with-eureka和fetch-registry,因為預設是true

重啟專案,訪問Eureka監控頁面http://127.0.0.1:10086/eureka檢視

1525609225152

我們發現user-service服務已經註冊成功了

5.2.3.消費者從Eureka獲取服務

接下來我們修改consumer-demo,嘗試從EurekaServer獲取服務。

方法與消費者類似,只需要在專案中新增EurekaClient依賴,就可以通過服務名稱來獲取資訊了!

1)新增依賴:

先新增SpringCloud依賴:

<!-- SpringCloud的依賴 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.RC1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<!-- Spring的倉庫地址 -->
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
複製程式碼

然後是Eureka客戶端:

<!-- Eureka客戶端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
複製程式碼

2)在啟動類開啟Eureka客戶端

@SpringBootApplication
@EnableDiscoveryClient // 開啟Eureka客戶端
public class UserConsumerDemoApplication {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}

複製程式碼

3)修改配置:

server:
  port: 8080
spring:
  application:
    name: consumer # 應用名稱
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 當其它服務獲取地址時提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip資訊,不指定的話會自己尋找
複製程式碼

4)修改程式碼,用DiscoveryClient類的方法,根據服務名稱,獲取服務例項:

@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;// Eureka客戶端,可以獲取到服務例項資訊

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        // String baseUrl = "http://localhost:8081/user/";
        // 根據服務名稱,獲取服務例項
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 因為只有一個UserService,因此我們直接get(0)獲取
        ServiceInstance instance = instances.get(0);
        // 獲取ip和埠資訊
        String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/";
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次間隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}
複製程式碼

5.4.Eureka進階

5.4.1.高可用的Eureka Server

Eureka Server即服務的註冊中心,在剛才的案例中,我們只有一個EurekaServer,事實上EurekaServer也可以是一個叢集,形成高可用的Eureka中心。

動手搭建高可用的EurekaServer

我們假設要搭建兩條EurekaServer的叢集,埠分別為:10086和10087

1)我們修改原來的EurekaServer配置:

server:
  port: 10086 # 埠
spring:
  application:
    name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
  client:
    service-url: # 配置其他Eureka服務的地址,而不是自己,比如10087
      defaultZone: http://127.0.0.1:10087/eureka

複製程式碼

所謂的高可用註冊中心,其實就是把EurekaServer自己也作為一個服務進行註冊,這樣多個EurekaServer之間就能互相發現對方,從而形成叢集。因此我們做了以下修改:

  • 刪除了register-with-eureka=false和fetch-registry=false兩個配置。因為預設值是true,這樣就會吧自己註冊到註冊中心了。
  • 把service-url的值改成了另外一臺EurekaServer的地址,而不是自己

2)另外一臺配置恰好相反:

server:
  port: 10087 # 埠
spring:
  application:
    name: eureka-server # 應用名稱,會在Eureka中顯示
eureka:
  client:
    service-url: # 配置其他Eureka服務的地址,而不是自己,比如10087
      defaultZone: http://127.0.0.1:10086/eureka

複製程式碼

注意:idea中一個應用不能啟動兩次,我們需要重新配置一個啟動器:

1525615070033

1525615095693

1525615026937

然後啟動即可。

3)啟動測試:

1525615165157

4)客戶端註冊服務到叢集

因為EurekaServer不止一個,因此註冊服務的時候,service-url引數需要變化:

eureka:
  client:
    service-url: # EurekaServer地址,多個地址以','隔開
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
複製程式碼

5.4.2.服務提供者

服務提供者要向EurekaServer註冊服務,並且完成服務續約等工作。

服務註冊

服務提供者在啟動時,會檢測配置屬性中的:eureka.client.register-with-erueka=true引數是否正確,事實上預設就是true。如果值確實為true,則會向EurekaServer發起一個Rest請求,並攜帶自己的後設資料資訊,Eureka Server會把這些資訊儲存到一個雙層Map結構中。第一層Map的Key就是服務名稱,第二層Map的key是服務的例項id。

服務續約

在註冊服務完成以後,服務提供者會維持一個心跳(定時向EurekaServer發起Rest請求),告訴EurekaServer:“我還活著”。這個我們稱為服務的續約(renew);

有兩個重要引數可以修改服務續約的行為:

eureka:
  instance:
    lease-expiration-duration-in-seconds: 90
    lease-renewal-interval-in-seconds: 30
複製程式碼
  • lease-renewal-interval-in-seconds:服務續約(renew)的間隔,預設為30秒
  • lease-expiration-duration-in-seconds:服務失效時間,預設值90秒

也就是說,預設情況下每個30秒服務會向註冊中心傳送一次心跳,證明自己還活著。如果超過90秒沒有傳送心跳,EurekaServer就會認為該服務當機,會從服務列表中移除,這兩個值在生產環境不要修改,預設即可。

但是在開發時,這個值有點太長了,經常我們關掉一個服務,會發現Eureka依然認為服務在活著。所以我們在開發階段可以適當調小。

eureka:
  instance:
    lease-expiration-duration-in-seconds: 10 # 10秒即過期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
複製程式碼

例項id

先來看一下服務狀態資訊:

在Eureka監控頁面,檢視服務註冊資訊:

1525617060656

在status一列中,顯示以下資訊:

  • UP(1):代表現在是啟動了1個示例,沒有叢集
  • DESKTOP-2MVEC12:user-service:8081:是示例的名稱(instance-id),
    • 預設格式是:${hostname} + ${spring.application.name} + ${server.port}
    • instance-id是區分同一服務的不同例項的唯一標準,因此不能重複。

我們可以通過instance-id屬性來修改它的構成:

eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}
複製程式碼

重啟服務再試試看:

1525617542081

5.4.3.服務消費者

獲取服務列表

當服務消費者啟動時會檢測eureka.client.fetch-registry=true引數的值,如果為true,則會從Eureka Server服務的列表只讀備份,然後快取在本地。並且每隔30秒會重新獲取並更新資料。我們可以通過下面的引數來修改:

eureka:
  client:
    registry-fetch-interval-seconds: 5
複製程式碼

生產環境中,我們不需要修改這個值。

但是為了開發環境下,能夠快速得到服務的最新狀態,我們可以將其設定小一點。

5.4.4.失效剔除和自我保護

失效剔除

有些時候,我們的服務提供方並不一定會正常下線,可能因為記憶體溢位、網路故障等原因導致服務無法正常工作。Eureka Server需要將這樣的服務剔除出服務列表。因此它會開啟一個定時任務,每隔60秒對所有失效的服務(超過90秒未響應)進行剔除。

可以通過eureka.server.eviction-interval-timer-in-ms引數對其進行修改,單位是毫秒,生成環境不要修改。

這個會對我們開發帶來極大的不變,你對服務重啟,隔了60秒Eureka才反應過來。開發階段可以適當調整,比如10S

自我保護

我們關停一個服務,就會在Eureka皮膚看到一條警告:

1525618396076

這是觸發了Eureka的自我保護機制。當一個服務未按時進行心跳續約時,Eureka會統計最近15分鐘心跳失敗的服務例項的比例是否超過了85%。在生產環境下,因為網路延遲等原因,心跳失敗例項的比例很有可能超標,但是此時就把服務剔除列表並不妥當,因為服務可能沒有當機。Eureka就會把當前例項的註冊資訊保護起來,不予剔除。生產環境下這很有效,保證了大多數服務依然可用。

但是這給我們的開發帶來了麻煩, 因此開發階段我們都會關閉自我保護模式:

eureka:
  server:
    enable-self-preservation: false # 關閉自我保護模式(預設為開啟)
    eviction-interval-timer-in-ms: 1000 # 掃描失效服務的間隔時間(預設為60*1000ms)
複製程式碼

6.負載均衡Robbin

在剛才的案例中,我們啟動了一個user-service,然後通過DiscoveryClient來獲取服務例項資訊,然後獲取ip和埠來訪問。

但是實際環境中,我們往往會開啟很多個user-service的叢集。此時我們獲取的服務列表中就會有多個,到底該訪問哪一個呢?

一般這種情況下我們就需要編寫負載均衡演算法,在多個例項列表中進行選擇。

接下來,我們就來使用Ribbon實現負載均衡。

6.1.啟動兩個服務例項

首先我們啟動兩個user-service例項,一個8081,一個8082。

Eureka監控皮膚:

1525619546904

6.2.開啟負載均衡

因為Eureka中已經整合了Ribbon,所以我們無需引入新的依賴。直接修改程式碼:

在RestTemplate的配置方法上新增@LoadBalanced註解:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
複製程式碼

修改呼叫方式,不再手動獲取ip和埠,而是直接通過服務名稱呼叫:

@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        // 地址直接寫服務名稱即可
        String baseUrl = "http://user-service/user/";
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次間隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}
複製程式碼

6.3.負載均衡策略

Ribbon預設的負載均衡策略是簡單的輪詢,我們可以測試一下:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserConsumerDemoApplication.class)
public class LoadBalanceTest {

    @Autowired
    RibbonLoadBalancerClient client;

    @Test
    public void test(){
        for (int i = 0; i < 100; i++) {
            ServiceInstance instance = this.client.choose("user-service");
            System.out.println(instance.getHost() + ":" + instance.getPort());
        }
    }
}

複製程式碼

結果:

1525622357371

符合了我們的預期推測,確實是輪詢方式。

SpringBoot也幫我們提供了修改負載均衡規則的配置入口:

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
複製程式碼

格式是:{服務名稱}.ribbon.NFLoadBalancerRuleClassName

6.4.重試機制

Eureka的服務治理強調了CAP原則中的AP,即可用性和可靠性。它與Zookeeper這一類強調CP(一致性,可靠性)的服務治理框架最大的區別在於:Eureka為了實現更高的服務可用性,犧牲了一定的一致性,極端情況下它寧願接收故障例項也不願丟掉健康例項,正如我們上面所說的自我保護機制。

但是,此時如果我們呼叫了這些不正常的服務,呼叫就會失敗,從而導致其它服務不能正常工作!這顯然不是我們願意看到的。

我們現在關閉一個user-service例項:

1525653565855

因為服務剔除的延遲,consumer並不會立即得到最新的服務列表,此時再次訪問你會得到錯誤提示:

1525653715488

但是此時,8081服務其實是正常的。

因此Spring Cloud 整合了Spring Retry 來增強RestTemplate的重試能力,當一次服務呼叫失敗後,不會立即丟擲一次,而是再次重試另一個服務。

只需要簡單配置即可實現Ribbon的重試:

spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true # 開啟Spring Cloud的重試功能
user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的連線超時時間
    ReadTimeout: 1000 # Ribbon的資料讀取超時時間
    OkToRetryOnAllOperations: true # 是否對所有操作都進行重試
    MaxAutoRetriesNextServer: 1 # 切換例項的重試次數
    MaxAutoRetries: 1 # 對當前例項的重試次數
複製程式碼

根據如上配置,當訪問到某個服務超時後,它會再次嘗試訪問下一個服務例項,如果不行就再換一個例項,如果不行,則返回失敗。切換次數取決於MaxAutoRetriesNextServer引數的值

引入spring-retry依賴

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
複製程式碼

我們重啟user-consumer-demo,測試,發現即使user-service2當機,也能通過另一臺服務例項獲取到結果!

1525658269456

7.Hystix

7.1.簡介

Hystix是Netflix開源的一個延遲和容錯庫,用於隔離訪問遠端服務、第三方庫,防止出現級聯失敗。

7.2.熔斷器的工作機制:

正常工作的情況下,客戶端請求呼叫服務API介面:

當有服務出現異常時,直接進行失敗回滾,服務降級處理:

當服務繁忙時,如果服務出現異常,不是粗暴的直接報錯,而是返回一個友好的提示,雖然拒絕了使用者的訪問,但是會返回一個結果。

7.3.動手實踐

7.3.1.引入依賴

首先在user-consumer中引入Hystix依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
複製程式碼

7.3.2.開啟熔斷

7.3.2.改造消費者

我們改造user-consumer,新增一個用來訪問的user服務的DAO,並且宣告一個失敗時的回滾處理函式:

@Component
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

    @HystrixCommand(fallbackMethod = "queryUserByIdFallback")
    public User queryUserById(Long id){
        long begin = System.currentTimeMillis();
        String url = "http://user-service/user/" + id;
        User user = this.restTemplate.getForObject(url, User.class);
        long end = System.currentTimeMillis();
        // 記錄訪問用時:
        logger.info("訪問用時:{}", end - begin);
        return user;
    }

    public User queryUserByIdFallback(Long id){
        User user = new User();
        user.setId(id);
        user.setName("使用者資訊查詢出現異常!");
        return user;
    }
}
複製程式碼
  • @HystrixCommand(fallbackMethod="queryUserByIdFallback"):宣告一個失敗回滾處理函式queryUserByIdFallback,當queryUserById執行超時(預設是1000毫秒),就會執行fallback函式,返回錯誤提示。
  • 為了方便檢視熔斷的觸發時機,我們記錄請求訪問時間。

在原來的業務邏輯中呼叫這個DAO:

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.userDao.queryUserById(id));
        });
        return users;
    }
}
複製程式碼

7.3.3.改造服務提供者

改造服務提供者,隨機休眠一段時間,以觸發熔斷:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) throws InterruptedException {
        // 為了演示超時現象,我們在這裡然執行緒休眠,時間隨機 0~2000毫秒
        Thread.sleep(new Random().nextInt(2000));
        return this.userMapper.selectByPrimaryKey(id);
    }
}

複製程式碼

7.3.4.啟動測試

然後執行並檢視日誌:

id為9、10、11的訪問時間分別是:

1525661641660

id為12的訪問時間:

1525661669136

因此,只有12是正常訪問,其它都會觸發熔斷,我們來檢視結果:

1525661720656

7.3.5.優化

雖然熔斷實現了,但是我們的重試機制似乎沒有生效,是這樣嗎?

其實這裡是因為我們的Ribbon超時時間設定的是1000ms:

1525666632542

而Hystix的超時時間預設也是1000ms,因此重試機制沒有被觸發,而是先觸發了熔斷。

所以,Ribbon的超時時間一定要小於Hystix的超時時間。

我們可以通過hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds來設定Hystrix超時時間。

hystrix:
  command:
  	default:
        execution:
          isolation:
            thread:
              timeoutInMillisecond: 6000 # 設定hystrix的超時時間為6000ms
複製程式碼

8.Feign

8.1.簡介

Feign可以把Rest的請求進行隱藏,偽裝成類似SpringMVC的Controller一樣。你不用再自己拼接url,拼接引數等等操作,一切都交給Feign去做。

8.2.快速入門

8.2.1.匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
複製程式碼

8.2.2.Feign的客戶端

@FeignClient("user-service")
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User queryUserById(@PathVariable("id") Long id);
}
複製程式碼
  • 首先這是一個介面,Feign會通過動態代理,幫我們生成實現類。這點跟mybatis的mapper很像
  • @FeignClient,宣告這是一個Feign客戶端,類似@Mapper註解。同時通過value屬性指定服務名稱
  • 介面中的定義方法,完全採用SpringMVC的註解,Feign會根據註解幫我們生成URL,並訪問獲取結果

改造原來的呼叫邏輯,不再呼叫UserDao:

@Service
public class UserService {

    @Autowired
    private UserFeignClient userFeignClient;

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.userFeignClient.queryUserById(id));
        });
        return users;
    }
}
複製程式碼

8.2.3.開啟Feign功能

我們在啟動類上,新增註解,開啟Feign功能

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients // 開啟Feign功能
public class UserConsumerDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}
複製程式碼
  • 你會發現RestTemplate的註冊被我刪除了。Feign中已經自動整合了Ribbon負載均衡,因此我們不需要自己定義RestTemplate了

8.3.負載均衡

Feign中本身已經整合了Ribbon依賴和自動配置:

1525672070679

因此我們不需要額外引入依賴,也不需要再註冊RestTemplate物件。

另外,我們可以像上節課中講的那樣去配置Ribbon,可以通過ribbon.xx來進行全域性配置。也可以通過服務名.ribbon.xx來對指定服務配置:

user-service:
  ribbon:
    ConnectTimeout: 250 # 連線超時時間(ms)
    ReadTimeout: 1000 # 通訊超時時間(ms)
    OkToRetryOnAllOperations: true # 是否對所有操作重試
    MaxAutoRetriesNextServer: 1 # 同一服務不同例項的重試次數
    MaxAutoRetries: 1 # 同一例項的重試次數
複製程式碼

8.4.Feign對Hystix的整合

通過下面的引數來開啟:

feign:
  hystrix:
    enabled: true # 開啟Feign的熔斷功能
複製程式碼

但是,Feign中的Fallback配置不像Ribbon中那樣簡單了。

1)首先,我們要定義一個類,實現剛才編寫的UserFeignClient,作為fallback的處理類

@Component
public class UserFeignClientFallback implements UserFeignClient {
    @Override
    public User queryUserById(Long id) {
        User user = new User();
        user.setId(id);
        user.setName("使用者查詢出現異常!");
        return user;
    }
}

複製程式碼

2)然後在UserFeignClient中,指定剛才編寫的實現類

@FeignClient(value = "user-service", fallback = UserFeignClientFallback.class)
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User queryUserById(@PathVariable("id") Long id);
}

複製程式碼

8.5.請求壓縮

Spring Cloud Feign 支援對請求和響應進行GZIP壓縮,以減少通訊過程中的效能損耗。通過下面的引數即可開啟請求與響應的壓縮功能:

feign:
  compression:
    request:
      enabled: true # 開啟請求壓縮
    response:
      enabled: true # 開啟響應壓縮
複製程式碼

同時,我們也可以對請求的資料型別,以及觸發壓縮的大小下限進行設定:

feign:
  compression:
    request:
      enabled: true # 開啟請求壓縮
      mime-types: text/html,application/xml,application/json # 設定壓縮的資料型別
      min-request-size: 2048 # 設定觸發壓縮的大小下限
複製程式碼

注:上面的資料型別、壓縮大小下限均為預設值。

8.6.日誌級別

前面講過,通過logging.level.xx=debug來設定日誌級別。然而這個對Fegin客戶端而言不會產生效果。因為@FeignClient註解修改的客戶端在被代理時,都會建立一個新的Fegin.Logger例項。我們需要額外指定這個日誌的級別才可以。

1)設定com.leyou包下的日誌級別都為debug

logging:
  level:
    com.leyou: debug
複製程式碼

2)編寫配置類,定義日誌級別

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}
複製程式碼

這裡指定的Level級別是FULL,Feign支援4種級別:

  • NONE:不記錄任何日誌資訊,這是預設值。
  • BASIC:僅記錄請求的方法,URL以及響應狀態碼和執行時間
  • HEADERS:在BASIC的基礎上,額外記錄了請求和響應的頭資訊
  • FULL:記錄所有請求和響應的明細,包括頭資訊、請求體、後設資料。

3)在FeignClient中指定配置類:

@FeignClient(value = "user-service", fallback = UserFeignClientFallback.class, configuration = FeignConfig.class)
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    User queryUserById(@PathVariable("id") Long id);
}
複製程式碼

4)重啟專案,即可看到每次訪問的日誌:

1525674544569

9.Zuul閘道器

通過前面的學習,使用Spring Cloud實現微服務的架構基本成型,大致是這樣的:

1525674644660

在該架構中,我們的服務叢集包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務呼叫方。我們把焦點聚集在對外服務這塊,直接暴露我們的服務地址,這樣的實現是否合理,或者是否有更好的實現方式呢?

先來說說這樣架構需要做的一些事兒以及存在的不足:

  • 首先,破壞了服務無狀態特點。
    • 為了保證對外服務的安全性,我們需要實現對服務訪問的許可權控制,而開放服務的許可權控制機制將會貫穿並汙染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀態的特點。
    • 從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外考慮對介面訪問的控制處理。
  • 其次,無法直接複用既有介面。
    • 當我們需要對一個即有的叢集內訪問介面,實現外部服務訪問時,我們不得不通過在原有介面上增加校驗邏輯,或增加一個代理呼叫來實現許可權控制,無法直接複用原有的介面。

面對類似上面的問題,我們要如何解決呢?答案是:服務閘道器!

為了解決上面這些問題,我們需要將許可權控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器的 服務閘道器。

服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。

9.1.Zuul加入後的架構

1525675648881

  • 不管是來自於客戶端(PC或移動端)的請求,還是服務內部呼叫。一切對服務的請求都會經過Zuul這個閘道器,然後再由閘道器來實現 鑑權、動態路由等等操作。Zuul就是我們服務的統一入口。

9.2.Zuul簡介

9.3.快速入門

9.3.1.新建工程

填寫基本資訊:

1525675928548

新增Zuul依賴:

1525675991833

9.3.2.編寫啟動類

通過@EnableZuulProxy註解開啟Zuul的功能:

@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的閘道器功能
public class ZuulDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulDemoApplication.class, args);
	}
}
複製程式碼

9.3.3.編寫配置

server:
  port: 10010 #服務埠
spring: 
  application:  
    name: api-gateway #指定服務名
複製程式碼

9.3.4.編寫路由規則

我們需要用Zuul來代理user-service服務,先看一下控制皮膚中的服務狀態:

1525676797879

  • ip為:127.0.0.1
  • 埠為:8081

對映規則:

zuul:
  routes:
    user-service: # 這裡是路由id,隨意寫
      path: /user-service/** # 這裡是對映路徑
      url: http://127.0.0.1:8081 # 對映路徑對應的實際url地址
複製程式碼

我們將符合path 規則的一切請求,都代理到 url引數指定的地址

本例中,我們將 /user-service/**開頭的請求,代理到http://127.0.0.1:8081

9.3.5.啟動測試:

訪問的路徑中需要加上配置規則的對映路徑,我們訪問:http://127.0.0.1:8081/user-service/user/10

9.4.面向服務的路由

在剛才的路由規則中,我們把路徑對應的服務地址寫死了!如果同一服務有多個例項的話,這樣做顯然就不合理了。

我們應該根據服務的名稱,去Eureka註冊中心查詢 服務對應的所有例項列表,然後進行動態路由才對!

9.4.1.新增Eureka客戶端依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
複製程式碼

9.4.2.開啟Eureka客戶端發現功能

@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的閘道器功能
@EnableDiscoveryClient
public class ZuulDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulDemoApplication.class, args);
	}
}
複製程式碼

9.4.3.新增Eureka配置,獲取服務資訊

eureka:
  client:
    registry-fetch-interval-seconds: 5 # 獲取服務列表的週期:5s
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1
複製程式碼

9.4.4.修改對映配置,通過服務名稱獲取

因為已經有了Eureka客戶端,我們可以從Eureka獲取服務的地址資訊,因此對映時無需指定IP地址,而是通過服務名稱來訪問,而且Zuul已經整合了Ribbon的負載均衡功能。

zuul:
  routes:
    user-service: # 這裡是路由id,隨意寫
      path: /user-service/** # 這裡是對映路徑
      serviceId: user-service # 指定服務名稱
複製程式碼

9.4.5.啟動測試

再次啟動,這次Zuul進行代理時,會利用Ribbon進行負載均衡訪問:

日誌中可以看到使用了負載均衡器:

1525677891119

9.5.簡化的路由配置

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

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

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

比方說上面我們關於user-service的配置可以簡化為一條:

zuul:
  routes:
    user-service: /user-service/** # 這裡是對映路徑
複製程式碼

省去了對服務名稱的配置。

9.6.預設的路由規則

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

  • 預設情況下,一切服務的對映路徑就是服務名本身。
    • 例如服務名為:user-service,則預設的對映路徑就是:/user-service/**

也就是說,剛才的對映規則我們完全不配置也是OK的

9.7.路由字首

配置示例:

zuul:
  prefix: /api # 新增路由字首
  routes:
      user-service: # 這裡是路由id,隨意寫
        path: /user-service/** # 這裡是對映路徑
        service-id: user-service # 指定服務名稱
複製程式碼

我們通過zuul.prefix=/api來指定了路由的字首,這樣在發起請求時,路徑就要以/api開頭。

路徑/api/user-service/user/1將會被代理到/user-service/user/1

9.8.過濾器

Zuul作為閘道器的其中一個重要功能,就是實現請求的鑑權。而這個動作我們往往是通過Zuul提供的過濾器來實現的。

9.8.1.ZuulFilter

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

public abstract ZuulFilter implements IZuulFilter{

    abstract public String filterType();

    abstract public int filterOrder();
    
    boolean shouldFilter();// 來自IZuulFilter

    Object run() throws ZuulException;// IZuulFilter
}
複製程式碼
  • shouldFilter:返回一個Boolean值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。
  • run:過濾器的具體業務邏輯。
  • filterType:返回字串,代表過濾器的型別。包含以下4種:
    • pre:請求在被路由之前執行
    • routing:在路由請求時呼叫
    • post:在routing和errror過濾器之後呼叫
    • error:處理請求時發生錯誤呼叫
  • filterOrder:通過返回的int值來定義過濾器的執行順序,數字越小優先順序越高。

9.8.2.過濾器執行生命週期:

這張是Zuul官網提供的請求生命週期圖,清晰的表現了一個請求在各個過濾器的執行順序。

1525681866862

  • 正常流程:
    • 請求到達首先會經過pre型別過濾器,而後到達routing型別,進行路由,請求就到達真正的服務提供者,執行請求,返回結果後,會到達post過濾器。而後返回響應。
  • 異常流程:
    • 整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢後,會將請求交給POST過濾器,最後返回給使用者。
    • 如果是error過濾器自己出現異常,最終也會進入POST過濾器,而後返回。
    • 如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達POST過濾器了。

所有內建過濾器列表:

1525682427811

9.8.3.使用場景

場景非常多:

  • 請求鑑權:一般放在pre型別,如果發現沒有訪問許可權,直接就攔截了
  • 異常處理:一般會在error型別和post型別過濾器中結合來處理。
  • 服務呼叫時長統計:pre和post結合使用。

9.9.自定義過濾器

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

9.9.1.定義過濾器類

@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;
    }
}

複製程式碼

9.10.負載均衡和熔斷

Zuul中預設就已經整合了Ribbon負載均衡和Hystix熔斷機制。但是所有的超時策略都是走的預設值,比如熔斷超時時間只有1S,很容易就觸發了。因此建議我們手動進行配置:

zuul:
  retryable: true
ribbon:
  ConnectTimeout: 250 # 連線超時時間(ms)
  ReadTimeout: 2000 # 通訊超時時間(ms)
  OkToRetryOnAllOperations: true # 是否對所有操作重試
  MaxAutoRetriesNextServer: 2 # 同一服務不同例項的重試次數
  MaxAutoRetries: 1 # 同一例項的重試次數
hystrix:
  command:
  	default:
        execution:
          isolation:
            thread:
              timeoutInMillisecond: 6000 # 熔斷超時時長:6000ms
複製程式碼

相關文章