初識SpringCloud及SpringCloud入門案例一

在上一篇博客我們瞭解完微服務之後,我們知道微服務是一種架構方式,最終肯定需要技術架構去實施。

微服務的實現方式很多,但是最火的莫過於Spring Cloud了。爲什麼?

  • 後臺硬:作爲Spring家族的一員,有整個Spring全家桶靠山,背景十分強大。
  • 技術強:Spring作爲Java領域的前輩,可以說是功力深厚。有強力的技術團隊支撐,一般人還真比不了
  • 羣衆基礎好:可以說大多數程序員的成長都伴隨着Spring框架,試問:現在有幾家公司開發不用Spring?SpringCloud與Spring的各個框架無縫整合,對大家來說一切都是熟悉的配方,熟悉的味道。
  • 使用方便:相信大家都體會到了SpringBoot給我們開發帶來的便利,而SpringCloud完全支持SpringBoot的開發,用很少的配置就能完成微服務框架的搭建。

簡介

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

Spring最擅長的就是集成,把世界上最好的框架拿過來,集成到自己的項目中。

SpringCloud也是一樣,它將現在非常流行的一些技術整合到一起,實現了諸如:配置管理,服務發現,智能路由,負載均衡,熔斷器,控制總線,集羣狀態等等功能。

其主要涉及的組件包括:

架構圖:在這裏插入圖片描述

版本

SpringCloud的版本命名比較特殊,因爲它不是一個組件,而是許多組件的集合,它的命名是以A到Z的爲首字母的一些單詞組成:
在這裏插入圖片描述

下面先列常見的版本進行簡要說明:

Alpha:內測版本,BUG會比較多,一般是開發人員在開發過程中使用;
Beta:早期的版本,所有一般仍有缺陷,但無大的BUG,可能會加入新的功能,需要進行完善
GA:General Availability,正式發佈的版本,官方推薦使用的版本;在國外用GA來說明是RELEASE版本
SNAPSHOT:快照版,可穩定使用,且仍在繼續改進版本
Final:正式版本;
SR:修正版本;
Trial:試用版本,一般會有時間或功能的限制;
Build:修正版;
PRE:預覽版本,內部測試版本,主要是給開發和測試人員測試及檢查

我一般會是以Finchley的版本。其中包含的組件,也都有各自的版本,如下表:

Component Edgware.SR3 Finchley.RC1 Finchley.BUILD-SNAPSHOT
spring-cloud-aws 1.2.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-bus 1.3.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-cli 1.4.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-commons 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-contract 1.2.4.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-config 1.4.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-netflix 1.4.4.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-security 1.2.2.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-cloudfoundry 1.1.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-consul 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-sleuth 1.3.3.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-stream Ditmars.SR3 Elmhurst.RELEASE Elmhurst.BUILD-SNAPSHOT
spring-cloud-zookeeper 1.2.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-boot 1.5.10.RELEASE 2.0.1.RELEASE 2.0.0.BUILD-SNAPSHOT
spring-cloud-task 1.2.2.RELEASE 2.0.0.RC1 2.0.0.RELEASE
spring-cloud-vault 1.1.0.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-gateway 1.0.1.RELEASE 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT
spring-cloud-openfeign 2.0.0.RC1 2.0.0.BUILD-SNAPSHOT

接下來,我們就通過一個入門案例來一一學習SpringCloud中的重要組件。


微服務場景模擬

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

1.Spring腳手架創建父工程

藉助於Spring提供的快速搭建工具:
在這裏插入圖片描述

填寫項目信息:在這裏插入圖片描述

選依賴時可以跳過,

填寫項目位置:
在這裏插入圖片描述

項目結構:
在這裏插入圖片描述

因爲父工程只用來管理依賴項其他用不到的已刪除。

父工程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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
     <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.demo</groupId>
    <artifactId>cloud-demo-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cloud-demo-parent</name>
    <packaging>pom</packaging>
    <description>Demo project for Spring Boot</description>

     <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.SR1</spring-cloud.version>
        <mapper.srarter.version>2.0.4</mapper.srarter.version>
        <mysql.version>8.0.11</mysql.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--springCloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--通用mapper啓動器-->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper.srarter.version}</version>
            </dependency>

            <!--mysql驅動-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
    	<!--lombok插件沒下載的得先下載插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
1.1.服務提供者

我們新建一個子模塊,對外提供查詢用戶的服務。(我就不一步一步創建了。)

結構圖:
在這裏插入圖片描述

1.2.編寫代碼

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example.demo</groupId>
        <artifactId>cloud-demo-parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.example.demo</groupId>
    <artifactId>user-service-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user-service-demo</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

然後添加一個對外查詢的接口:

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

實體類

@Data
@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 phone;

    private String email;

    // 創建時間
    private Date created;

    // 更新時間
    private Date updated;

}

屬性文件,這裏我們採用了yaml語法,而不是properties:

server:
  port: 8081
spring:
  application:
    name: user-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///zuka?useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: sasa
mybatis:
  type-aliases-package: com.example.demo.userservicedemo.pojo

項目結構:
在這裏插入圖片描述

1.3啓動並測試:

啓動項目,訪問接口:http://localhost:8081/user/7在這裏插入圖片描述

2.1.服務調用者
創建工程

與上面類似,這裏不再贅述,需要注意的是,我們調用user-service的功能,因此不需要mybatis相關依賴了。

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">
    <parent>
        <artifactId>cloud-demo</artifactId>
        <groupId>cn.itcast.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-consumer-demo</artifactId>
    <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>

</project>
編寫代碼

首先在啓動類中註冊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);
    }
}

然後編寫UserDao,注意,這裏不是調用mapper查數據庫,而是通過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);
    }
}
2.2.啓動測試

因爲我們沒有配置端口,那麼默認就是8080,我們訪問:http://localhost:8080/consume?ids=44,45,46
在這裏插入圖片描述

一個簡單的遠程服務調用案例就實現了。


有沒有問題?

簡單回顧一下,剛纔我們寫了什麼:

  • use-service-demo:一個提供根據id查詢用戶的微服務
  • consumer-demo:一個服務調用者,通過RestTemplate遠程調用user-service-demo

流程如下:在這裏插入圖片描述

存在什麼問題?

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

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

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

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


Eureka註冊中心

認識Eureka

首先我們來解決第一問題,服務的管理。

問題分析

在剛纔的案例中,user-service對外提供服務,需要對外暴露自己的地址。而consumer(調用者)需要記錄服務提供者的地址。將來地址出現變更,還需要及時更新。這在服務較少的時候並不覺得有什麼,但是在現在日益複雜的互聯網環境,一個項目肯定會拆分出十幾,甚至數十個微服務。此時如果還人爲管理地址,不僅開發困難,將來測試、發佈上線都會非常麻煩,這與DevOps的思想是背道而馳的。

網約車

這就好比是 網約車出現以前,人們出門叫車只能叫出租車。一些私家車想做出租卻沒有資格,被稱爲黑車。而很多人想要約車,但是無奈出租車太少,不方便。私家車很多卻不敢攔,而且滿大街的車,誰知道哪個纔是願意載人的。一個想要,一個願意給,就是缺少引子,缺乏管理啊。

此時滴滴這樣的網約車平臺出現了,所有想載客的私家車全部到滴滴注冊,記錄你的車型(服務類型),身份信息(聯繫方式)。這樣提供服務的私家車,在滴滴那裏都能找到,一目瞭然。

此時要叫車的人,只需要打開APP,輸入你的目的地,選擇車型(服務類型),滴滴自動安排一個符合需求的車到你面前,爲你服務,完美!

Eureka做什麼?

Eureka就好比是滴滴,負責管理、記錄服務提供者的信息。服務調用者無需自己尋找服務,而是把自己的需求告訴Eureka,然後Eureka會把符合你需求的服務告訴你。

同時,服務提供方與Eureka之間通過“心跳”機制進行監控,當某個服務提供方出現問題,Eureka自然會把它從服務列表中剔除。

這就實現了服務的自動註冊、發現、狀態監控。


原理圖

基本架構:

在這裏插入圖片描述

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

入門案例

編寫EurekaServer

接下來我們創建一個項目,啓動一個EurekaServer:

依然使用spring提供的快速搭建工具:(這裏我略過創建步驟)

項目結構:
在這裏插入圖片描述

完整的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">
    <parent>
        <artifactId>cloud-demo-parent</artifactId>
        <groupId>com.example.demo</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example.demo</groupId>
    <artifactId>eureka-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
</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在這裏插入圖片描述在這裏插入圖片描述

將user-service-demo註冊到Eureka

註冊服務,就是在服務上添加Eureka的客戶端依賴,客戶端代碼會自動把服務註冊到EurekaServer中。

我們在user-service-demo中添加Eureka客戶端依賴:

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

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

通過添加@EnableDiscoveryClient來開啓Eureka客戶端功能

@SpringBootApplication
@MapperScan("com.example.demo.userservicedemo.mapper")
@EnableDiscoveryClient // 開啓EurekaClient功能
public class UserServiceDemoApplication {

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

添加配置

server:
  port: 8081
spring:
  application:
    name: user-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///zuka?useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: sasa
mybatis:
  type-aliases-package: com.example.demo.userservicedemo.pojo
eureka:
  client:
	#registerWithEureka: true #服務註冊,是否將自己註冊到Eureka服務中
    #fetchRegistry: true #服務發現,是否從Eureka中獲取註冊信息
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

注意:

  • 這裏我們添加了spring.application.name屬性來指定應用名稱,將來會作爲應用的id使用。
  • 不用指定register-with-eureka和fetch-registry,因爲默認是true

重啓項目,訪問Eureka監控頁面查看

在這裏插入圖片描述
我們發現user-service服務已經註冊成功了


消費者從Eureka獲取服務

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

方法與消費者類似,只需要在項目中添加EurekaClient依賴,然後再啓動類上加@EnableDiscoveryClient註解開啓Eureka客戶端即可,就可以通過服務名稱來獲取信息了!

前兩步這裏我就略過了,大家按照之前步驟操作。

添加配置:

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信息,不指定的話會自己尋找

修改代碼,用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;
    }
}

Debug跟蹤運行:
在這裏插入圖片描述

生成的URL:

在這裏插入圖片描述

訪問結果:
在這裏插入圖片描述

Eureka詳解

接下來我們詳細講解Eureka的原理及配置。

基礎架構

Eureka架構中的三個核心角色:

  • 服務註冊中心

    Eureka的服務端應用,提供服務註冊和發現功能,就是剛剛我們建立的eureka-server

  • 服務提供者

    提供服務的應用,可以是SpringBoot應用,也可以是其它任意技術實現,只要對外提供的是Rest風格服務即可。本例中就是我們實現的user-service-demo

  • 服務消費者

    消費應用從註冊中心獲取服務列表,從而得知每個服務方的信息,知道去哪裏調用服務方。本例中就是我們實現的user-consumer-demo


高可用的Eureka Server

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

服務同步

多個Eureka Server之間也會互相註冊爲服務,當服務提供者註冊到Eureka Server集羣中的某個節點時,該節點會把服務的信息同步給集羣中的每個節點,從而實現數據同步。因此,無論客戶端訪問到Eureka Server集羣中的任意一個節點,都可以獲取到完整的服務列表信息。

動手搭建高可用的EurekaServer

我們假設要搭建兩條EurekaServer的集羣,端口分別爲:10086和10087

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

server:
  port: ${PORT:10086} #服務端口
spring:
  application:
    name: eureka-server #指定服務名
eureka:
  client:
    registerWithEureka: true #服務註冊,是否將自己註冊到Eureka服務中
    fetchRegistry: true #服務發現,是否從Eureka中獲取註冊信息
    serviceUrl: #Eureka客戶端與Eureka服務端的交互地址,高可用狀態配置對方的地址,單機狀態配置自己(如果不配置則默認本機8761端口)
      defaultZone: ${EUREKA_SERVER:http://eureka02:10087/eureka/}
  server:
    enable-self-preservation: false #是否開啓自我保護模式
    eviction-interval-timer-in-ms: 60000 #服務註冊表清理間隔(單位毫秒,默認是60*1000)
  instance:
    hostname: ${EUREKA_DOMAIN:eureka01}

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

  • 刪除了register-with-eureka=false和fetch-registry=false兩個配置。因爲默認值是true,這樣就會吧自己註冊到註冊中心了。
  • 把service-url的值改成了另外一臺EurekaServer的地址,而不是自己
  • PORT,EUREKA_SERVER等變量從外部配置, ${PORT:10086} 表示如有外部變量則使用外部PORT如沒有則使用10086端口。

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

在這裏插入圖片描述

-DPORT=10086 -DEUREKA_SERVER=http://eureka02:10087/eureka/ -DEUREKA_DOMAIN=eureka01

然後我們啓動即可

在這裏插入圖片描述

客戶端註冊服務到集羣

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

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

服務提供者

服務提供者要向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監控頁面,查看服務註冊信息
在這裏插入圖片描述
在status一列中,顯示以下信息:

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

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

eureka:
  instance:
  	prefer-ip-address: true # 當其它服務獲取地址時提供ip而不是hostname
    instance-id: ${spring.application.name}:${server.port}	# 指定自己的ip信息,不指定的話會自己尋找

重啓服務再試試看:

在這裏插入圖片描述

服務消費者

獲取服務列表

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

eureka:
  client:
    registry-fetch-interval-seconds: 5

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

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

失效剔除和自我保護

失效剔除

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

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

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

自我保護

我們關停一個服務,就會在Eureka面板看到一條警告:
在這裏插入圖片描述

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

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

eureka:
  server:
    enable-self-preservation: false # 關閉自我保護模式(缺省爲打開)
    eviction-interval-timer-in-ms: 1000 # 掃描失效服務的間隔時間(缺省爲60*1000ms)

負載均衡Robbin

在剛纔的案例中,我們啓動了一個user-service,然後通過DiscoveryClient來獲取服務實例信息,然後獲取ip和端口來訪問。

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

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

不過Eureka中已經幫我們集成了負載均衡組件:Ribbon,簡單修改代碼即可使用。

什麼是Ribbon:
在這裏插入圖片描述

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

啓動兩個服務實例

首先我們啓動兩個user-service實例,一個8081,一個8082。
在這裏插入圖片描述

Eureka監控面板:
在這裏插入圖片描述

開啓負載均衡

因爲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;
    }
}

訪問頁面,查看結果:
在這裏插入圖片描述

源碼跟蹤

爲什麼我們只輸入了service名稱就可以訪問了呢?之前還要獲取ip和端口。

顯然有人幫我們根據service名稱,獲取到了服務實例的ip和端口。它就是LoadBalancerInterceptor

我們進行源碼跟蹤:
在這裏插入圖片描述

繼續跟入execute方法:發現獲取了8082端口的服務
在這裏插入圖片描述

再跟下一次,發現獲取的是8081:
在這裏插入圖片描述

負載均衡策略

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

編寫測試類,在剛纔的源碼中我們看到攔截中是使用RibbonLoadBalanceClient來進行負載均衡的,其中有一個choose方法,是這樣介紹的:

在這裏插入圖片描述

現在這個就是負載均衡獲取實例的方法。

我們對注入這個類的對象,然後對其測試:

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

結果:
在這裏插入圖片描述

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

我們是否可以修改負載均衡的策略呢?

繼續跟蹤源碼,發現這麼一段代碼:
在這裏插入圖片描述

我們看看這個rule是誰:
在這裏插入圖片描述

這裏的rule默認值是一個RoundRobinRule,看類的介紹:
在這裏插入圖片描述

這不就是輪詢的意思嘛。

我們注意到,這個類其實是實現了接口IRule的,查看一下:
在這裏插入圖片描述
定義負載均衡的規則接口。

它有以下實現:
在這裏插入圖片描述

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

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

格式是:{服務名稱}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的實現類。

再次測試,發現結果變成了隨機:
在這裏插入圖片描述

重試機制

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

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

我們現在關閉一個user-service實例:
在這裏插入圖片描述
因爲服務剔除的延遲,consumer並不會立即得到最新的服務列表,此時再次訪問你會得到錯誤提示:

在這裏插入圖片描述

但是此時,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宕機,也能通過另一臺服務實例獲取到結果!
在這裏插入圖片描述

初識SpringCloud及SpringCloud入門案例二

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章