天天看点

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

1.微服务场景模拟:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

服务提供者

我们新建一个项目,对外提供查询用户的服务。

创建数据库

CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL,
  `user_name` varchar(50) DEFAULT NULL,
  `password` varchar(50) DEFAULT NULL,
  `name` varchar(50) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `sex` int(11) DEFAULT NULL,
  `birthday` date DEFAULT NULL,
  `created` date DEFAULT NULL,
  `updated` date DEFAULT NULL,
  `note` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES ('1', 'tom', '123456', 'tom', '20', '1', '1999-11-29', '2018-11-29', '2018-11-29', null);
INSERT INTO `tb_user` VALUES ('2', 'lucy', '123456', 'lucy', '20', '2', '2018-11-29', '2018-11-29', '2018-11-29', null);
           

Spring脚手架创建工程

借助于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.kismet.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.4.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>
           

注意:SpringCloud中mysql驱动包默认是8.x,太高了,可能会引起问题,所以需要修改成自己的版本号

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    <version>8.0.20</version>
</dependency>
           

编写代码

添加一个对外查询的接口:

@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: 8020
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb1?useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
           

启动并测试:

启动项目,访问接口:http://localhost:8020/user/1

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

服务调用者

创建工程

详细跟上面的工程一样,但是需要注意的是,我们调用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.kismet.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.2.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>
    
	</dependencies>

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

</project>

           

编写代码

首先注册

RestTemplate

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }

}
           

然后编写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> queryUserByIds(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);
    }
}
           

复制pojo

启动测试:

因为我们没有配置端口,那么默认就是8080,我们访问:http://localhost:8080/consume?ids=1,2

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

暴露的问题!

简单回顾一下,刚才我们写了什么:

  • user-service-demo:一个提供根据id查询用户的微服务
  • user-consumer-demo:一个服务调用者,通过RestTemplate远程调用user-service-demo

流程如下:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

存在什么问题?

  • 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护
  • consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效
  • consumer不清楚user-service的状态,服务宕机也不知道
  • user-service只有1台服务,不具备高可用性
  • 即便user-service形成集群,consumer还需自己实现负载均衡

其实上面说的问题,概括一下就是分布式服务(SpringBoot搭建的微服务)必然要面临的问题:

  • 服务管理
    • 如何自动注册和发现服务
    • 如何实现服务状态的监管
    • 如何实现动态路由,从而实现负载均衡
  • 服务如何实现负载均衡
  • 服务如何解决容灾问题
  • 服务如何实现统一配置

以上的问题,我们都将在SpringCloud中得到答案。成套的解决方案!

Eureka注册中心:

原理见:微服务之 Eureka注册中心,Ribbon负载均衡 --理论梳理

入门案例

接下来我们创建一个项目,启动一个EurekaServer:

依然使用spring提供的快速搭建工具:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

完整的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.kismet.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.2.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>Hoxton.RELEASE</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);
	}
}
           

编写配置application.yml:

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

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon
微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

将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>Hoxton.RELEASE</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/@EnableEurekaClient

来开启Eureka客户端功能

@SpringBootApplication
@EnableEurekaClient // 开启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/mydb1?useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: user-service # 应用名称
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
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信息,不指定的话会自己寻找
           

启动服务:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

我们发现user-service服务已经注册成功了

消费者从Eureka获取服务

接下来我们修改consumer-demo,尝试从EurekaServer获取服务。

方法与消费者类似,只需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了!

1)添加依赖:

先添加SpringCloud依赖:

<!-- SpringCloud的依赖 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.RELEASE</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 {
    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)修改user-consumer-demo项目中的UserDao代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例:

// 必须导入org.springframework.cloud.client.discovery.DiscoveryClient
    @Autowired
    private DiscoveryClient discoveryClient;


    public User queryUserById(Long id){
        //1、 根据user-service获取user-serivce 的集群的信息
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        //2、由于我们没有集群,只有一个,所以直接取出第一个
        ServiceInstance instance = instances.get(0);
        //3、拼接URL
        String url = "http://"+instance.getHost()+":"+instance.getPort()+"/user/"+id;

        // 使用restTemplate发起请求
        ResponseEntity<User> entity = restTemplate.getForEntity(url, User.class);
        // 获取返回对象
        User user = entity.getBody();
        return user;
    }
           

访问结果:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

高可用的Eureka Server

原理见:微服务之 Eureka注册中心,Ribbon负载均衡 --理论梳理

动手搭建高可用的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)另外一台配置恰好相反:(可以复制出来一个EurekaServer,也可以新配置一个启动器)

server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
      defaultZone: http://127.0.0.1:10086/eureka
           

因为idea无法同时启动来个工程,中间肯定有时间差,所以其中有一个启动的时候会报出异常!但是不用担心,因为eureka的消息是心跳消息形式的,不间断的去访问。当两个都启动起来,就会相互注册成功,就不会报错了!

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

3)启动测试: 

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

4)客户端注册服务到集群

因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:

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

服务提供者

服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。

服务注册

服务提供者在启动时,会检测yml中配置属性的:

eureka.client.register-with-erueka=true

参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务名称,第二层Map的key是服务的实例id。

服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时-每隔30s-向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秒

服务消费者

获取服务列表

当服务消费者启动时,会检测

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注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。

但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:

在eureka的yml文件中配置

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
           

负载均衡Ribbon

首先我们启动两个user-service实例,一个8020,一个8021。

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

Eureka监控面板: 

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

开启负载均衡

因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码:

在RestTemplate的配置方法上添加

@LoadBalanced

注解:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}
           

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:

@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;


    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));
          
        });
        return users;
    }
}
           

查看结果:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

源码跟踪

为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。

显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是

LoadBalancerInterceptor

我们进行源码跟踪:(Debug运行user-consumer-demo项目)

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

继续跟入execute方法:发现获取了8082端口的服务

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

再跟下一次,发现获取的是8081:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

负载均衡策略

Ribbon默认的负载均衡策略是简单的轮询,我们可以测试一下:

编写测试类,在刚才的源码中我们看到拦截中是使用RibbonLoadBalanceClient来进行负载均衡的,其中有一个choose方法,是这样介绍的:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡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());
        }
    }
}
           

结果:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

符合了我们的预期推测,确实是轮询方式。

我们是否可以修改负载均衡的策略呢?

继续跟踪源码,发现这么一段代码:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

我们看看这个rule是谁:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

这里的rule默认值是一个

RoundRobinRule

,看类的介绍:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

这不就是轮询的意思嘛。

我们注意到,这个类其实是实现了接口IRule的,查看一下:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

定义负载均衡的规则接口。

它有以下实现:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

SpringBoot也帮我们提供了修改负载均衡规则的配置入口:

复制到消费者的yml中

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

格式是:

{服务名称}.ribbon.NFLoadBalancerRuleClassName

,值就是IRule的实现类。

再次测试,发现结果变成了随机:

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

重试机制

当第一次调用某个服务失败的时候,不是立刻返回失败结果,而是尝试重新请求

默认情况下, 不会重试,只会请求一次

那我应该何时重试呢?

重试几次呢?

CAP原则:CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得

Eureka的服务治理强调了CAP原则中的AP,即可用性和可靠性。它与Zookeeper这一类强调CP(一致性,可靠性)的服务治理框架最大的区别在于:Eureka为了实现更高的服务可用性,牺牲了一定的一致性,极端情况下它宁愿接收故障实例也不愿丢掉健康实例,正如我们上面所说的自我保护机制。

但是,此时如果我们调用了这些不正常的服务,调用就会失败,从而导致其它服务不能正常工作!这显然不是我们愿意看到的。

我们现在关闭一个user-service实例。

因为服务剔除的延迟,consumer并不会立即得到最新的服务列表,此时再次访问你会得到错误提示。

微服务之 Eureka注册中心,Ribbon负载均衡 --代码实践1.微服务场景模拟:Eureka注册中心:高可用的Eureka Server 负载均衡Ribbon

但是此时,8081服务其实是正常的。

因此Spring Cloud 整合了Spring Retry 来增强RestTemplate的重试能力,当一次服务调用失败后,不会立即抛出一次,而是再次重试另一个服务。

只需要简单配置即可实现Ribbon的重试:---代码user-consumer消费者中

spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true # 开启Spring Cloud的重试功能
user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的连接超时时间,单位毫秒
    ReadTimeout: 1000 # Ribbon的数据读取超时时间,
    OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
    MaxAutoRetriesNextServer: 3 # 切换实例的重试次数
    MaxAutoRetries: 1 # 对当前实例的重试次数
           

根据如上配置,当访问到某个服务超时后,它会再次尝试访问下一个服务实例,如果不行就再换一个实例,如果不行,则返回失败。切换次数取决于

MaxAutoRetriesNextServer

参数的值

引入spring-retry依赖

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
           

我们重启user-consumer-demo,测试,发现即使user-service2宕机,也能通过另一台服务实例获取到结果!

继续阅读