天天看點

微服務之 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當機,也能通過另一台服務執行個體擷取到結果!

繼續閱讀