SpringCloud与Docker微服务架构实战

微服务注册与发现

服务发现简介

通过前面的讲解,我们知道硬编码提供者地址的方式有不少问题。要想解决这些问题,服务消费者需要一个强大的服务发现机制,服务消费者使用这种机制获取服务提供者的网络信息。不仅如此,即使服务提供者的信息发生变化,服务消费者也无需修改配置文件。


服务发现组件提供这种能力。在微服务架构中,服务发现组件是一个非常关键的组件。


使用服务发现组件后的架构图,如下:




服务提供者、服务消费者、服务发现组件这三者之间的关系大致如下;


各个微服务在启动时,将自己的网络地址等信息注册到服务发现组件中,服务发现组件会存储这些信息。

服务消费者可从服务发现组件查询服务提供者的网络地址,并使用该地址调用服务提供者的接口。

各个微服务与服务发现组件使用一定机制(例如心跳)通信。服务发现组件如长时间无法与某微服务实例通信,就会注销该实例。

微服务网络地址发生变更(例如实例增减或者IP端口发生变化等)时,会重新注册到服务发现组件。是用这种方式,服务消费者就无须人工修改提供者的网络地址了。

综上,服务发现组件应具备以下功能:


服务注册表:是服务发现组件的核心,它用来记录各个微服务的信息,例如微服务的名称、IP、端口等。服务注册表提供查询API和管理API,查询API用于查询可用的微服务实例,管理API用于服务的注册和注销。

服务注册与服务发现:服务注册是指微服务在启动时,将自己的信息注册到服务发现组件上的过程。服务发现是指查询可用微服务列表及其网络地址的机制。

服务检查:服务发现组件使用一定机制定时检测已注册的服务,如发现某实例长时间无法访问,就会从服务注册表中移除该实例。

综上,使用服务发现的好处是显而易见的。SpringCloud提供了多种服务发现组件的支持,例如Eureka、Consul和ZooKeeper等。


Eureka简介

Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。SpringCloud将它集成在子项目Spring Cloud Netflix中,从而实现微服务的注册与发现。


Eureka的GitHub:https://github.com/Netflix/Eureka。

Netflix是一家在线影片租赁提供商。

Eureka原理

在分析Eureka的原理之前,先来了解一下Region和Availability Zone,如下图所示:




Region和Availability Zone均是AWS的概念。其中,Region表示AWS中的地理位置,每个Region都有多个Availability Zone,各个Region之间完全隔离。AWS通过这种方式实现了最大的容错和稳定性。


SpringCloud默认使用的Region是us-east-1,在非AWS环境下,可以将Availability Zone理解成机房,将Region理解为跨机房的Eureka集群。


下面分析一下Eureka的原理,Eureka架构如下图所示。




Application Service相当于服务提供者

Application Client相当于服务消费者

Make Remote Call,可以理解成调用RESTful API的行为

us-east-1c、us-east-1d等都是zone,它们都属于us-east-1这个region

由上图可知,Eureka包含两个组件:Eureka Server和Eureka Client,它们的作用如下:


Eureka Server提供服务发现的能力,各个微服务启动时,会向Eureka Server注册自己的信息(例如IP、端口、微服务名称等),Eureka Server会存储这些信息。

Eureka Client是一个Java客户端,用于简化与Eureka Server的交互。

微服务启动后,会周期性(默认30秒)地向Eureka Server发送心跳以续约自己的“租期”。

如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。

默认情况下,Eureka Server同时也是Eureka Client。多个Eureka Server实例,互相之间通过复制的方式,来实现服务注册表中数据的同步。

Eureka Client会缓存服务注册表中的信息。这种方式有一定的优势——首先,微服务无须每次请求都查询Eureka Server,从而降低了Eureka Server的压力;其次,即使Eureka Server所有节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者并完成调用。

综上,Eureka通过心跳检查、客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。


编写Eureka Server

本节来编写一个Eureka Server。


创建一个ArtifactId是microservice-discovery-eureka的Maven工程,并为项目添加以下依赖。

<dependency>  

    <groupId>org.springframework.cloud</groupId>   

    <artifactId>spring-cloud-starter-eureka-server</artifactId>

</dependency>

1

2

3

4

编写启动类,在启动类上添加@EnableEurekaServer注解,声明这是一个Eureka Server。

package com.itmuch.cloud;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication

@EnableEurekaServer

public class EurekaApplication {    

    public static void main(String[] args) {   

        SpringApplication.run(EurekaApplication.class, args); 

    }

}

1

2

3

4

5

6

7

8

9

10

11

在配置文件application.yml中添加如下内容。

server:  

port: 8761

eureka:  

client:    

registerWithEureka: false    

fetchRegistry: false    

serviceUrl:      

defaultZone: http://localhost:8761/eureka/

1

2

3

4

5

6

7

8

简要讲解一下application.yml中的赔指数型:


eureka.client.registerWithEureka:表示是否将自己注册到Eureka Server,默认为true。由于当前应用就是Eureka Server,故而设为false。

eureka.client.fetchRegistry:表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。

eureka.client.serviceUrl.defaultZone:设置与Eureka Server交互的地址,查询服务与注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka/;多个地址可使用‘,’分隔。

这样一个Eureka Server就编写完成了。


测试


启动Eureka Server,访问http://localhost:8761/,可看到如下图所示的界面。




由图可知,Eureka Server的首页展示了很多信息,例如当前实例的系统状态、注册到Eureka Server上的服务实例、常用信息、实例信息等。显然,当前还没有任何微服务实例被注册到Eureka Server上。


将微服务注册到Eureka Server上

本节将之前编写的用户微服务注册到Eureka Server上。


复制项目microservice-simple-provider-user,将ArtifactId修改为microservice-provider-user。

在pom.xml中添加以下依赖。

<dependency>  

    <groupId>org.springframework.cloud</groupId>   

    <artifactId>spring-cloud-starter-eureka-server</artifactId>

</dependency>

1

2

3

4

在配置文件application.ynl中添加以下配置。

spring:  

application:

name: microservice-provider-user

eureka:  

client: 

serviceUrl:      

defaultZone: http://localhost:8761/eureka/

instance:

prefer-ip-address: true

1

2

3

4

5

6

7

8

9

其中,spring.application.name用于指定注册到Eureka Server上的应用名称;eureka.instance.prefer-ip-address = true表示将自己的IP注册到Eureka Server。如不配置该属性或将其设置为false,则表示注册微服务所在操作系统的hostname到Eureka Server。


编写启动类,在启动类上添加@EnableDiscoveryClient注解,声明这是一个Eureka Client。

package com.itmuch.cloud;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient

@SpringBootApplication

public class ProviderUserApplication {  

    public static void main(String[] args) { 

        SpringApplication.run(ProviderUserApplication.class, args);  

    }

}

1

2

3

4

5

6

7

8

9

10

11

也可以使用@EnableEurekaClient注解替代@EnableDiscoveryClient。在SpringCloud中,服务发现组件有多种选择,例如ZooKeeper、Consul等。@EnableDiscoveryClient为各种服务组件提供了支持,该注解是spring-cloud-commons项目的注解,是一个高度的抽象;而@EnableEurekaClient表明是Eureka的Client,该注解是spring-cloud-netfix项目中的注解,只能与Eureka一起工作。当Eureka在项目的classpath中时,两个注解没有区别。


这样就可以将用户微服务注册到Eureka Server上了。同理,将电影微服务也注册到Eureka Server上,配置电影微服务的spring.application.name为microservice-consumer-movie。


相关文件如下:


MovieController.java


package com.itmuch.cloud.study.user.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.client.RestTemplate;

import com.itmuch.cloud.study.user.entity.User;

@RestControllerpublic class MovieController { 

    @Autowired  

    private RestTemplate restTemplate;  

    @GetMapping("/user/{id}")  

    public User findById(@PathVariable Long id) { 

        return this.restTemplate.getForObject("http://localhost:8000/" + id, User.class); 

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

User.java


package com.itmuch.cloud.study.user.entity;

import java.math.BigDecimal;

public class User { 

    private Long id; 

    private String username; 

    private String name; 

    private Integer age; 

    private BigDecimal balance; 

    

    public Long getId() {  

        return this.id;  

    } 

    public void setId(Long id) {

        this.id = id; 

    }  

    public String getUsername() {  

        return this.username; 

    }  

    public void setUsername(String username) {  

        this.username = username; 

    }  

    public String getName() {  

        return this.name; 

    }  

    public void setName(String name) {  

        this.name = name;  

    }  

    public Integer getAge() { 

        return this.age;  

    } 

    public void setAge(Integer age) { 

        this.age = age; 

    }  

    public BigDecimal getBalance() {

        return this.balance; 

    }  

    public void setBalance(BigDecimal balance) {  

        this.balance = balance;  

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

ConsumerMovieApplication.java


package com.itmuch.cloud.study;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.context.annotation.Bean;

import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient

@SpringBootApplication

public class ConsumerMovieApplication {

    @Bean  

    public RestTemplate restTemplate() { 

        return new RestTemplate(); 

    } 

    public static void main(String[] args) { 

        SpringApplication.run(ConsumerMovieApplication.class, args); 

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

application.yml


server:  

port: 8010

spring:  

application:    

name: microservice-consumer-movie

eureka:  

client:    

serviceUrl:      

defaultZone: http://localhost:8761/eureka/  

instance:    

prefer-ip-address: true

1

2

3

4

5

6

7

8

9

10

11

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.itmuch.cloud</groupId>

  <artifactId>microservice-consumer-movie</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>jar</packaging>


  <!-- 引入spring boot的依赖 -->

  <parent>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-parent</artifactId>

    <version>1.4.3.RELEASE</version>

  </parent>


  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <java.version>1.8</java.version>

  </properties>


  <dependencies>

    <dependency>

      <groupId>org.springframework.boot</groupId>

      <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

      <groupId>org.springframework.boot</groupId>

      <artifactId>spring-boot-starter-actuator</artifactId>

    </dependency>


    <dependency>

      <groupId>org.springframework.cloud</groupId>

      <artifactId>spring-cloud-starter-eureka</artifactId>

    </dependency>

  </dependencies>


  <!-- 引入spring cloud的依赖 -->

  <dependencyManagement>

    <dependencies>

      <dependency>

        <groupId>org.springframework.cloud</groupId>

        <artifactId>spring-cloud-dependencies</artifactId>

        <version>Camden.SR4</version>

        <type>pom</type>

        <scope>import</scope>

      </dependency>

    </dependencies>

  </dependencyManagement>


  <!-- 添加spring-boot的maven插件 -->

  <build>

    <plugins>

      <plugin>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-maven-plugin</artifactId>

      </plugin>

    </plugins>

  </build>

</project>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

测试


启动microservice-discovery-eureka。

启动microservice-provider-user。

启动microservice-consumer-movie。

访问http://localhost:8761/,可看到如图所示界面。



由图可知,此时用户微服务、电影微服务已经被注册到Eureka Server上了。


Eureka Server的高可用

有分布式应用开发经验的读者应该能够看出,前文编写的单节点的Eureka Server并不适合线上生产环境。Eureka Client会定时连接Eureka Server,获取服务注册表中的信息并缓存到本地。微服务在消费远程API时总是使用本地缓存中的数据。因此一般来说,即使Eureka Server发生宕机,也不会影响到服务之间的调用。但如果Eureka Server宕机时,某些微服务也出现了不可用的情况,Eureka Client中的缓存若不被更新,就可能会影响到微服务的调用,甚至影响到整个应用系统的高可用性。因此,在生产环境中,通常会部署一个高可用的Eureka Server集群。


Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为,还记得前文编写单节点Eureka Server时,额外配置了eureka.client.registerWithEureka=false、eureka.client.fetchRegistry=false吗?


本节在前文的基础上,构建一个双节点Eureka Server集群。


复制项目microservice-discovery-eureka,将ArtifactId修改为microservice-discovery-eureka-ha。


配置系统的hosts,Windows系统的hosts文件路径是C:\Windows\System32\drivers\etc\hosts;Linux及Mac OS等系统的文件路径是/etc/hosts。


127.0.0.1 peer1 peer2


将application.yml修改如下:让两个节点的Eureka Server相互注册。


spring: 

application:  

name: microservice-discovery-eureka-ha

---

spring: 

# 指定profile=peer1

profiles: peer1

server: 

port: 8761

eureka: 

instance:  

# 指定当profile=peer1时,主机名是peer1 

hostname: peer1

client:  

serviceUrl: 

        #将自己注册到peer2这个Eureka上面去

defaultZone: http://peer2:8762/eureka/

---

spring: 

profiles: peer2

server: 

port: 8762

eureka: 

instance:  

hostname: peer2 

client:  

serviceUrl:   

defaultZone: http://peer1:8761/eureka/

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

注意:配置application.yml时,各节点的缩进不要使用tab,而应使用空格,否则可能解析配置信息失败


如上,使用连字符(—)将该application.yml文件分为三段。第二段和第三段分别为spring.profiles指定了一个值,该值表示它所在的那段内容应用在哪个Profile里。第一段由于并未指定spring.profiles,因此这段内容会对所有Profile生效。


经过以上分析,不难理解,我们定义了peer1和peer2这两个Profile。当应用以peer1这个Profile启动时,配置该Eureka Server的主机名为peer1,并将其注册到http://peer2:8762/eureka/;反之,当应用以profile=peer2时,Eureka Server会注册到peer1节点的Eureka Server。


测试


打包项目,并使用以下命令启动两个Eureka Server节点。


java -jar microservice-discovery-eureka-ha-1.0-SNAPSHOT.jar --spring.profiles.active=peer1

java -jar microservice-discovery-eureka-ha-1.0-SNAPSHOT.jar --spring.profiles.active=peer2

1

2

通过spring.profiles.active指定使用哪个profile启动。


访问http://peer1:8761/,会发现“registered-replicas”中已有peer2节点;同理,访问http://peer2:8762/,也能发现其中的“registered-replicas”有peer1节点。


猜你喜欢

转载自blog.51cto.com/14661022/2464647