随着微服务的兴起,很多单体架构的项目被拆分成多个微服务,虽然服务的复杂性和耦合性被降低,但是微服务数量的增加,从而增加了多个微服务协同管理的成本,比如不同服务之间请求转发,相同服务器之间的负载均衡等,为了解决这些问题,服务注册和服务发现越发让人青睐。
服务注册,提供一个服务注册中心,它可以和服务提供者交互,服务提供者可以动态的把服务注册到注册中心,并且服务提供者和注册中心保持心跳检测,一旦服务提供者故障或宕机,注册中心可以及时移除该服务,当服务提供者故障修复后能及时重新注册服务,较常用的为Eureka、Consul、Zookeeper等。
服务发现可以分服务端发现和客户端发现。
服务端发现,即服务的发现放在服务端来做,通过服务端和注册中心交互,来实现请求的转发和负载均衡等,这种方式对于客户端来说,调用简单,但是这种方式调度以及存储都由中间件服务器完成,中间件服务器可能会面临过高的负载,如果中间服务器故障,服务将全面受影响。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2LcVzaU9kbK52YuRmMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DO3YTMycDMyIDOxQDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
客户端发现,即服务器的发现放在客户端来做,客户端定时从注册中心拉取相应的服务到客户端,由客户端来做负载均衡和服务器调用,从而把负载均衡和调用的压力分摊到客户端,由于客户端会从注册中心定时拉取服务到本地缓存,即使注册中心故障,只是不能再从注册中心获取新服务而已,也不影响已经缓存在本地的服务正常调用,Eureka的服务发现便是基于这种模式。
什么是Eureka
Eureka是Netflix开源的一个RESTful服务,主要用于服务的注册发现。Eureka由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个Java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡,更多可以参考官网:点击打开链接
Eureka的高层架构
这个张架构图来自Eureka的wiki
这个张图首先被竖线分割成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服务控制台:
从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 # 安全的注册地址
然后启动再次访问集群控制台,则弹出需要输入密码界面:
服务提供者注册服务到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集群,访问注册中心控制台,可以发现服务注册成功:
服务的状态: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 ,查看服务的健康状况:
如需定制健康检查,我们可以实现接口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扫描的包和子包中。