天天看点

Spring Cloud之Eureka服务注册与发现

随着微服务的兴起,很多单体架构的项目被拆分成多个微服务,虽然服务的复杂性和耦合性被降低,但是微服务数量的增加,从而增加了多个微服务协同管理的成本,比如不同服务之间请求转发,相同服务器之间的负载均衡等,为了解决这些问题,服务注册和服务发现越发让人青睐。

服务注册,提供一个服务注册中心,它可以和服务提供者交互,服务提供者可以动态的把服务注册到注册中心,并且服务提供者和注册中心保持心跳检测,一旦服务提供者故障或宕机,注册中心可以及时移除该服务,当服务提供者故障修复后能及时重新注册服务,较常用的为Eureka、Consul、Zookeeper等。

服务发现可以分服务端发现和客户端发现。

服务端发现,即服务的发现放在服务端来做,通过服务端和注册中心交互,来实现请求的转发和负载均衡等,这种方式对于客户端来说,调用简单,但是这种方式调度以及存储都由中间件服务器完成,中间件服务器可能会面临过高的负载,如果中间服务器故障,服务将全面受影响。

Spring Cloud之Eureka服务注册与发现

客户端发现,即服务器的发现放在客户端来做,客户端定时从注册中心拉取相应的服务到客户端,由客户端来做负载均衡和服务器调用,从而把负载均衡和调用的压力分摊到客户端,由于客户端会从注册中心定时拉取服务到本地缓存,即使注册中心故障,只是不能再从注册中心获取新服务而已,也不影响已经缓存在本地的服务正常调用,Eureka的服务发现便是基于这种模式。

Spring Cloud之Eureka服务注册与发现

什么是Eureka

Eureka是Netflix开源的一个RESTful服务,主要用于服务的注册发现。Eureka由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个Java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡,更多可以参考官网:点击打开链接

Eureka的高层架构

这个张架构图来自Eureka的wiki

Spring Cloud之Eureka服务注册与发现

这个张图首先被竖线分割成3块,这表示这3个区域(Region),即根据地理位置把某个地区的基础设施服务集合称为一个区域,并且每个区域又可以划分为多个可用区(Zone),每个可用区可以由多个数据中心组成,其实这些都是一些大的集群或数据中心才需要考虑的问题,对于小范围内集群,整个集群可以看成一个Region,每台Eureka server可以看做一个Zone。

 EureKa Server: 服务注册中心,它维护服务注册表,并且将信息同步给其他Eureka Server,在默认配置下如果90秒没有收到服务提供发送的续约,则Eureka服务器会将该服务实例从服务注册列表删除。

Application Service:  服务提供者,它通过Eureka的客户端和Euraka注册中心交互,可以向Eureka服务器注册(Registry)服务,服务下线(cancel,服务提供者主动发送服务下线请求让服务下线),服务续约(renew,Eureka客户端会每隔30秒发送一次心跳来续约。 通过续约来告知Eureka Server该Eureka客户仍然存在,服务是可用状态)

Application Client: 服务消费者,客户端从服务注册中心获取注册表信息,并将其缓存在本地,默认配置下该注册列表信息定期(每30秒钟)更新一次,客户端会根据服务注册表查询其他服务,并根据负载均衡算法,从而进行远程服务调用。

 Eureka高可用(HA)集群搭建

HA系统的CAP特性 一致性(C):任何时刻,所用的应用程序都能访问得到相同的数据。

可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。

分区容错性(P):分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性或可用性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

由上面描述看,C要求数据一致性,最强的数据一致性便是单机服务器,这样肯定是100%的数据一致性。A要求是系统的可用性,一个系统要可用性高,肯定要大于一台机器,否则存在明显的单点故障,然而随着机器的增加,每台机器都有一个数据备份,从而使得数据一致变得更加困难,从而也降低了数据的一致性,由此可见在某种程度上C和A是互斥的,所以很多系统都选择了AP或CP特性。

CP系统:在尽量保证可用性(A)的情况下,如果发生了分区(P),优先保证数据的一致性(C);一些主备自动切换的系统(多台服务器采用主从模式提供服务,在主机宕机或故障的情况下,备机可以快速接替主机工作,即主备自动切换)基本上都属于这一类,因为备机在切换成主机时不对外提供服务,并且为了保证数据一致性需要进行数据同步,如zookeeper 主节点Leader宕机,Follower节点会自动切换成Leader节点;Hadoop的Active的NameNode节点宕掉,Standby的NameNode节点在Zookeeper的协调下主备切换,Redis中的哨兵模式Slave节点切换成Master节点等。

AP系统:在尽量保证数据一致性(C)的情况下,如果发生分区(P),优先保证系统的可用性(A);采用p2p对等模式提供服务,即多台服务器部不分主从,对等的向外提供服务,即使某台机器故障,时时也不影响对外提供服务。一般客户端需要在服务调用的时候,如果某个台服务器故障需自动屏蔽该服务器,并可以采取一些措施,如服务降级,断路器模式等,暂时避开故障服务,调用可用的服务。Eureka集群便是基于这种模式,Eureka采用节点之间相互注册,尽可能的保证数据一致性,采用对等模式对外提供服务实现AP特性。

HA原理介绍完,下面开始Eureka集群搭建,可上GitHub下载Eureka源码编译打包,然后在web服务器中运行即可,这里不做具体介绍,下面我们介绍通过定制项目的模式来搭建集群。首先建立一个maven项目,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.test.eureka</groupId>
	<artifactId>eureka</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>eureka</name>
	<description>eureka</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.3.RELEASE</version>
		<relativePath /> 
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.7</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-eureka-server</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-actuator</artifactId>  
        </dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Camden.BUILD-SNAPSHOT</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-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>

</project>
           

在项目的类路径下,建立一个名叫application.yml或application.properties配置文件,yml文件和properties文件只是配置key的形式不一样,properties文件:

com.test.eurake=test
           

yml文件:

com:
  test:
     eureka:test
           

集群的application.yml简单配置如下:

spring:
  application:
    name: eureka-server-ha   

server:
  port: 8011  
eureka:
  instance:
    hostname: node1     
  client:      
    serviceUrl:
      defaultZone: http://node2:8012/eureka/
           

eureka-server-ha :是集群的名称 , 

8011 : 是web服务器的端口号,spring boot默认web服务器为tomcat, 

node1: 主机名称, 

http://node2:8012/eureka/  是本服务node1向集群node2注册的地址

写一个简单的SpringBoot启动类,加上注解@EnableEurekaServer即可启动Eureka服务:

package eurekademo;

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) throws Exception {
		SpringApplication.run(EurekaApplication.class, args);
	}

}
           

第一台Eureka注册服务器搭建完毕,按照上面的步骤再新建一个工程,其他都一样,application.yml配置如下:

spring:
  application:
    name: eureka-server-ha      
server:
  port: 8012
eureka:
  instance:
    hostname: node2       
  client:     
    serviceUrl:
      defaultZone: http://node1:8011/eureka/
           

和第一个配置相比,端口、主机名称、defaultZone注册地址不一样,由于我运行在同一台电脑上,所以端口也修改成8012,如果在不同的机器上端口可以不改,如果有更多的defaultZone路径,用逗号隔开即可。

分别启动node1节点和node2节点,我们发现在node1节点启动时,控制台可以看到有网络连接失败的异常,这是正常情况,因为node1启动的时候,我们配置了向node2注册服务,但是此时node2还未启动,所以连接异常,在启动node2时node1已经启动成功,node2没有网络异常出现。

启动后我们可以访问(http://localhost:8011/)进入node1服务控制台:

Spring Cloud之Eureka服务注册与发现

从node1控制台信息,可以看到node1的备份节点是node2。此时我们两台机器的集群搭建完成,这只是一个集群模拟,生产环境可能由很多的机器构成的一个大集群。

Eureka集群的安全性配置

需要在集群每个项目pom.xml中引入安全控制依赖:

<dependency>  
  <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>  
           

认证用户和密码的配置,如果不在每个application.yml配置加入如下配置, 服务启动时会随机生成一个密码,默认用户是user

# 安全认证的配置  
security:  
  basic:  
    enabled: true  
  user:  
    name: test88  # 用户名  
    password: test123 # 用户密码 
           

加上安全认证后,原来服务相互注册的defaultZone也需要带上用户和密码,否则就会相互注册失败:

defaultZone: http://test88:[email protected]node2:8012/eureka  # 安全的注册地址 

然后启动再次访问集群控制台,则弹出需要输入密码界面:

Spring Cloud之Eureka服务注册与发现

服务提供者注册服务到Eureka集群

新建一个服务提供者maven项目,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.test.provider</groupId>
	<artifactId>provider</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>provider</name>
	<description>provider</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.3.RELEASE</version>
		<relativePath /> 
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.7</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-eureka</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-actuator</artifactId>  
        </dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Camden.BUILD-SNAPSHOT</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-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>

</project>
           

application.yml简单配置如下,主要是服务注册集群的地址配置,多个地址使用逗号隔开。

spring:
  application:
    name: server-provider  
server:
  port: 8888 
eureka:
  client:      
    serviceUrl:
      defaultZone: http://test88:[email protected]:8011/eureka/,http://test88:[email protected]:8012/eureka/
           

写一个简单的Spring boot启动类加上注解@EnableEurekaClient或@EnableDiscoveryClient开启服务注册功能:

package com.eureka.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;


@SpringBootApplication
@EnableEurekaClient
public class ServicProvider {

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

然后启动服务提供者,把服务注册到Eureka集群,访问注册中心控制台,可以发现服务注册成功:

Spring Cloud之Eureka服务注册与发现

服务的状态:UP 表示服务正常,Down表示服务异常,不能再对外提供服务。

服务的健康检查

默认情况下注册到eureka server的服务是通过心跳来告知自己是UP还是DOWN,并不是通过spring-boot-actuator模块的/health来实现健康检查,默认的心跳实现方式可以有效的检查eureka客户端进程是否正常运作,但是无法保证客户端服务功能是否在正常,比如数据库等外部资源无法连通的时候,实际上已经不能正常对外提供服务,但是由于客户端和服务器的心跳还保持正常,所以它还会被当做正常的服务调用从而导致问题,因此我们可以开启Eureka的健康检查,来自定义一些健康检查,首先需要在pom.xml加入如下依赖:

<dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-actuator</artifactId>  
        </dependency>  
           

然后在配置中加入开启健康检查(eureka.client.healthcheck.enabled=true),启动服务后,可在浏览器中输入:http://localhost:8011/health ,查看服务的健康状况:

Spring Cloud之Eureka服务注册与发现

如需定制健康检查,我们可以实现接口org.springframework.boot.actuate.health.HealthIndicator来实现,当系统服务由于外部资源不可用出现异常,可以通过new Health.Builder().withDetail("key", "value").down().build()让服务处Down状态,当服务器恢复通过new Health.Builder().withDetail("key", "value").up().build()让服务恢复UP状态。

服务的调用

到目前服务提供者已经把服务注册到了Eureka集群,现在我们需要来对服务进行调用,新建一个服务调用项目,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.test.customer</groupId>
	<artifactId>customer</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<name>customer</name>
	<description>customer</description>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.3.RELEASE</version>
		<relativePath /> 
	</parent>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.7</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-eureka</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-feign</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Camden.BUILD-SNAPSHOT</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-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>
</project>
           

application.yml主要配置和服务提供者配置差不多:

spring:
  application:
    name: server-customer 
server:
  port: 9999 
eureka:
  client:      
    serviceUrl:
      defaultZone: http://test88:[email protected]:8011/eureka/,http://test88:[email protected]:8012/eureka/
           

新建一个调用接口如下,我们使用Feign的@FeignClient(name="server-provider")注解来调用服务,name是服务提供的者的application名称,@RequestMapping路径即是服务提供的url。

package com.eureka.customers;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient(name="server-provider")
public interface CallProvierInterface {

	
	@RequestMapping("/t/test")
	public String testCall();
}
           

然后再写一个controller方法,在controller方法中直接调用接口即可:

package com.eureka.customers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {

	@Autowired
	CallProvierInterface call;
	
	@ResponseBody
	@RequestMapping(value="/test")
	public String test(){
		return call.testCall();
	}
}
           

Feign是Spring Cloud 基础工具类,它整合了Ribbon(负载均衡)和Hystrix(断路器),除了提供这两者的强大功能以外,它还提供了一种声明式的Web服务客户端定义方式,使用它可以进行服务的消费,但是它的客户端负载均衡仍是通过Ribbon实现的。

服务调用之负载均衡

常用的负载均衡算法有,1.轮询,2.加权轮询,3.随机,4.加权随机,5.哈希算法等,Eureka负载均衡通过Ribbon来实现,默认的负载均衡算法是轮询,我们也可以通过定制使用其他的负载均衡算法:

package com.eureka;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;

@Configuration
public class TestConf {
	
	@Bean
	public IRule ribbonRule(){
		return new RandomRule();//随机负载均衡算法
	}
}
           

在spring boot 启动类上加上注解@RibbonClient,把前面定制化配置类加进去,Feign在调用时会自动使用该规则,代码:

package com.eureka.customers;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import com.eureka.TestConf;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@RibbonClient(name="server-provider", configuration=TestConf.class)
public class ServicCustomer {

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

}
           

注意:如果不想配置的TestConf负载均衡规则被所有的@RibbonClient共享,那么TestConf不应该在@ComponentScan或@SpringBootApplication扫描的包和子包中。

00