SpringCloud之Eureka实战和架构设计解析
Netflix Eureka(后文简称Eureka)是由Netflix开源的一款基于REST的服务发现组件,包括Eureka Server及Eureka Client。从2012年9月在GitHub上发布1.1.2版本以来,至今已经发布了231次,最新版本为2018年8月份发布的1.9.4版本。期间有进行2.x版本的开发,不过由于各种原因内部已经冻结开发,目前还是以1.x版本为主。Spring Cloud Netflix Eureka是Pivotal公司为了将Netflix Eureka整合于Spring Cloud生态系统提供的版本。
本文以Spring Cloud Finchley版本展开,对应Eureka的1.9.2版本,将系统全面地介绍服务发现的由来、Eureka的核心概念及其设计理念以及在实际企业级开发中的使用。
一、服务发现
1.1 服务发现的由来
服务发现及注册中心或是名字服务(后文统一简称服务发现),不是凭空出现的,其演进与软件开发的架构方式的演进有密切关联,大致如下:
1.单体架构时代
早期的互联网开发,多使用单体架构,服务自成一体,对于依赖的少数外部服务,会采用配置域名的方式访问,比如要使用外部短信供应商的短信发送接口,会使用appId和appKey,调用该供应商的域名接口即可。
2.SOA架构时代
随着SOA架构的流行,公司的内部服务开始从单体架构拆分为粒度较粗的服务化架构,这个时候,依赖的内部服务会比较多,那么内部的服务之间如何相互调用呢?以基于HTTP形式暴露服务为例,假设A服务部署在3台虚拟机上,这3个服务实例均有各自的独立内网ip,此时假设B服务要调用A服务的接口服务,有几种方式。
方式一:A服务把这3个服务实例的内网ip给到消费者B服务,这个时候B服务在没有Client端负载均衡技术的条件下,通常会在B服务自己的Nginx上配置A服务的upstream,即将A服务的3个服务实例配置进去,比如:
upstream servicea_api_servers {
server 192.168.99.100:80 weight=3 max_fails=3 fail_timeout=20s;
server 192.168.99.101:80 weight=1 max_fails=3 fail_timeout=20s;
server 192.168.99.102:80 weight=4 max_fails=3 fail_timeout=20s;
}
##......
server {
listen 80 default_server;
server_name serviceb.com.cn;
location /api/servicea/ {
proxy_pass http://servicea_api_servers/api/ ;
}
}
通过B服务自己的Nginx来维护A服务的具体实例ip,这种方式缺点比较明显,那就是B服务耦合了A服务的实现细节,当A服务实例扩充或者ip地址变化的时候,其下游的消费者都需要去修改这些ip,非常费劲。
方式二:为了解耦合,采用服务提供方A服务自己维护ip实例的方式,暴露统一的内网域名给消费者去消费,这样B服务只需要配置一个内网域名即可,比如:
server {
listen 80 default_server;
server_name serviceb.com.cn;
location /api/servicea/ {
proxy_pass http://servicea.com.cn/api/ ;
}
}
而A服务自己的Nginx则自己维护ip实例,比如:
upstream servicea_api_servers {
server 192.168.99.100:80 weight=3 max_fails=3 fail_timeout=20s;
server 192.168.99.101:80 weight=1 max_fails=3 fail_timeout=20s;
server 192.168.99.102:80 weight=4 max_fails=3 fail_timeout=20s;
}
##......
server {
listen 80 default_server;
server_name servicea.com.cn;
location /api/ {
proxy_pass http://servicea_api_servers/api/ ;
}
}
这样即实现了服务提供方与消费者之间的解耦,若A服务要变更实例ip地址,自己更改自身的Nginx配置即可。
3.微服务时代
在微服务时代,底层运维方式发生了巨大的变化,随着Docker的流行,业务服务不再部署在固定的虚拟机上,其ip地址也不再固定,这个时候前面的解决方案就显得捉襟见肘了。针对合格问题,不同的思考方式提出了不同的解决方案,这里列举几个。
方案一:以Nginx为例,在没有引入服务注册中心的时候,那就是手工或是通过脚本的方式,在部署的时候去更新Nginx的配置文件,然后reload。抑或是使用ngx_http_dyups_module通过rest api来在运行时直接更新upstream而不需要reload。
方案二:将服务注册中心作为一个标配的分布式服务组件,网关等都从服务注册中心获取相关服务的实例信息,实现动态路由。比如consul-template+Nginx的方案,通过consul监听服务实例变化,然后更新Nginx的配置文件,通过reload实现服务列表更新。又拍云的slardar也是这个思路,不过不是通过reload的方式来,而是通过在运行时通过consul获取服务列表来实现动态upstream的路由。
由此可见,随着服务架构模式以及底层运维方式的变化,服务注册中心逐步在分布式系统架构中占据了一个重要的地位。
二、Eureka
Eureka简介
Eureka是Netflix公司开源的一款服务发现组件,该组件提供的服务发现可以为负载均衡、failover等提供支持,如图2-1所示。Eureka包括Eureka Server及Eureka Client。Eureka Server提供REST服务,而Eureka Client则是使用Java编写的客户端,用于简化与Eureka Server的交互。
Eureka最初是针对AWS不提供中间服务层的负载均衡的限制而设计开发的。AWS Elastic Load Balancer用来对客户端或终端设备请求进行负载均衡,而Eureka则用来对中间层的服务做服务发现,配合其他组件提供负载均衡的能力。
Netflix为什么要设计Eureka,而不是直接利用AWS Elastic Load Balancer或者AWS Route 53呢?其官方文档说明简要如下:
理论上可以使用AWS Elastic Load Balancer对内部进行负载均衡,但是这样就会暴露到外网,存在安全性问题,另外AWS Elastic Load Balancer是传统的基于代理的负载均衡解决方案,无法直接基于服务元数据信息定制负载均衡算法。因此Netflix设计了Eureka,一方面给内部服务做服务发现,另一方面可以结合ribbon组件提供各种个性化的负载均衡算法。
而AWS Route 53是一款命名服务,可以给中间层的服务提供服务发现功能,但它是基于DNS的服务,传统的基于DNS的负载均衡技术存在缓存更新延时问题,另外主要是无法对服务健康状态进行检查,因此Netflix就自己设计了Eureka。
服务发现技术选型
Jason Wilder在2014年2月的时候写了一篇博客《Open-Source Service Discovery》,总结了当时市面上的几类服务发现组件,这里补充上consul以及一致性算法,如下所示。
从列表看,有很多服务发现组件可以选择,针对AP及CP,本书主要选取了Eureka及Consul为代表来阐述。关于Eureka及Consul的区别,Consul的官方文档有一个很好的阐述(http://www.consul.io/intro/vs/eureka.html),具体如下:
Eureka Server端采用的是P2P的复制模式,但是它不保证复制操作一定能成功,因此它提供的是一个最终一致性的服务实例视图;Client端在Server端的注册信息有一个带期限的租约,一旦Server端在指定期间没有收到Client端发送的心跳,则Server端会认为Client端注册的服务是不健康的,定时任务会将其从注册表中删除。Consul与Eureka不同,Consul采用Raft算法,可以提供强一致性的保证,Consul的agent相当于Netflix Ribbon + Netflix Eureka Client,而且对应用来说相对透明,同时相对于Eureka这种集中式的心跳检测机制,Consul的agent可以参与到基于gossip协议的健康检查,分散了Server端的心跳检测压力。除此之外,Consul为多数据中心提供了开箱即用的原生支持等。
那么基于什么考虑因素可以选择Eureka呢,主要有如下几点:
❑ 选择AP而不是CP,这一点在后面的章节会阐述。
❑ 如果团队是Java语言体系的,则偏好Java语言开发的,技术体系上比较统一,出问题也好排查修复,对组件的掌控力较强,方便扩展维护。
❑ 当然除此之外,更主要的是Eureka是Netflix开源套件的一部分,跟zuul, ribbon等整合的比较好。
三、入门案例
1.创建父工程
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>cn.springcloud.book</groupId>
<artifactId>ch2-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>ch2-1</name>
<description>ch2-1</description>
<modules>
<module>ch2-1-eureka-server</module>
<module>ch2-1-eureka-client</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.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>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<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>
<dependencies>
<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>
</dependencies>
<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>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.创建Server工程
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>cn.springcloud.book</groupId>
<artifactId>ch2-1-eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>ch2-1-eureka-server</name>
<description>ch2-1-eureka-server</description>
<parent>
<groupId>cn.springcloud.book</groupId>
<artifactId>ch2-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</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>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类:Ch21EurekaServerApplication
package cn.springcloud.book;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Ch21EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(Ch21EurekaServerApplication.class, args);
}
}
配置文件application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
waitTimeInMsWhenSyncEmpty: 0
enableSelfPreservation: false
3.创建Client工程
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>cn.springcloud.book</groupId>
<artifactId>ch2-1-eureka-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>ch2-1-eureka-client</name>
<description>ch2-1-eureka-client</description>
<parent>
<groupId>cn.springcloud.book</groupId>
<artifactId>ch2-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</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.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类:Ch21EurekaClientApplication
package cn.springcloud.book;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class Ch21EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(Ch21EurekaClientApplication.class, args);
}
}
配置文件:application.yml
server:
port: 8081
spring:
application:
name: demo-client1
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
4.启动测试
分别启动eureka-server及eureka-client,然后访问http://localhost:8761,结果如下图所示。
通过访问Eureka Server的rest api接口,比如http://localhost:8761/eureka/apps,返回的结果略。
示例源码下载链接。
四、源码实现
4.1 核心类
InstanceInfo
Eureka使用InstanceInfo
来代表注册的服务实例。
@ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)
@Serializer("com.netflix.discovery.converters.EntityBodyConverter")
@XStreamAlias("instance")
@JsonRootName("instance")
public class InstanceInfo {
private static final String VERSION_UNKNOWN = "unknown";
/**
* {@link InstanceInfo} JSON and XML format for port information does not follow the usual conventions, which
* makes its mapping complicated. This class represents the wire format for port information.
*/
public static class PortWrapper {
private final boolean enabled;
private final int port;
@JsonCreator
public PortWrapper(@JsonProperty("@enabled") boolean enabled, @JsonProperty("$") int port) {
this.enabled = enabled;
this.port = port;
}
public boolean isEnabled() {
return enabled;
}
public int getPort() {
return port;
}
}
private static final Logger logger = LoggerFactory.getLogger(InstanceInfo.class);
public static final int DEFAULT_PORT = 7001;
public static final int DEFAULT_SECURE_PORT = 7002;
public static final int DEFAULT_COUNTRY_ID = 1; // US
// The (fixed) instanceId for this instanceInfo. This should be unique within the scope of the appName.
private volatile String instanceId;
private volatile String appName;
@Auto
private volatile String appGroupName;
private volatile String ipAddr;
private static final String SID_DEFAULT = "na";
@Deprecated
private volatile String sid = SID_DEFAULT;
private volatile int port = DEFAULT_PORT;
private volatile int securePort = DEFAULT_SECURE_PORT;
@Auto
private volatile String homePageUrl;
@Auto
private volatile String statusPageUrl;
@Auto
private volatile String healthCheckUrl;
@Auto
private volatile String secureHealthCheckUrl;
@Auto
private volatile String vipAddress;
@Auto
private volatile String secureVipAddress;
@XStreamOmitField
private String statusPageRelativeUrl;
@XStreamOmitField
private String statusPageExplicitUrl;
@XStreamOmitField
private String healthCheckRelativeUrl;
@XStreamOmitField
private String healthCheckSecureExplicitUrl;
@XStreamOmitField
private String vipAddressUnresolved;
@XStreamOmitField
private String secureVipAddressUnresolved;
@XStreamOmitField
private String healthCheckExplicitUrl;
@Deprecated
private volatile int countryId = DEFAULT_COUNTRY_ID; // Defaults to US
private volatile boolean isSecurePortEnabled = false;
private volatile boolean isUnsecurePortEnabled = true;
private volatile DataCenterInfo dataCenterInfo;
private volatile String hostName;
private volatile InstanceStatus status = InstanceStatus.UP;
private volatile InstanceStatus overriddenStatus = InstanceStatus.UNKNOWN;
@XStreamOmitField
private volatile boolean isInstanceInfoDirty = false;
private volatile LeaseInfo leaseInfo;
@Auto
private volatile Boolean isCoordinatingDiscoveryServer = Boolean.FALSE;
@XStreamAlias("metadata")
private volatile Map<String, String> metadata;
@Auto
private volatile Long lastUpdatedTimestamp;
@Auto
private volatile Long lastDirtyTimestamp;
@Auto
private volatile ActionType actionType;
@Auto
private volatile String asgName;
private String version = VERSION_UNKNOWN;
private InstanceInfo() {
this.metadata = new ConcurrentHashMap<String, String>();
this.lastUpdatedTimestamp = System.currentTimeMillis();
this.lastDirtyTimestamp = lastUpdatedTimestamp;
}
@JsonCreator
public InstanceInfo(
@JsonProperty("instanceId") String instanceId,
@JsonProperty("app") String appName,
@JsonProperty("appGroupName") String appGroupName,
@JsonProperty("ipAddr") String ipAddr,
@JsonProperty("sid") String sid,
@JsonProperty("port") PortWrapper port,
@JsonProperty("securePort") PortWrapper securePort,
@JsonProperty("homePageUrl") String homePageUrl,
@JsonProperty("statusPageUrl") String statusPageUrl,
@JsonProperty("healthCheckUrl") String healthCheckUrl,
@JsonProperty("secureHealthCheckUrl") String secureHealthCheckUrl,
@JsonProperty("vipAddress") String vipAddress,
@JsonProperty("secureVipAddress") String secureVipAddress,
@JsonProperty("countryId") int countryId,
@JsonProperty("dataCenterInfo") DataCenterInfo dataCenterInfo,
@JsonProperty("hostName") String hostName,
@JsonProperty("status") InstanceStatus status,
@JsonProperty("overriddenstatus") InstanceStatus overriddenStatus,
@JsonProperty("overriddenStatus") InstanceStatus overriddenStatusAlt,
@JsonProperty("leaseInfo") LeaseInfo leaseInfo,
@JsonProperty("isCoordinatingDiscoveryServer") Boolean isCoordinatingDiscoveryServer,
@JsonProperty("metadata") HashMap<String, String> metadata,
@JsonProperty("lastUpdatedTimestamp") Long lastUpdatedTimestamp,
@JsonProperty("lastDirtyTimestamp") Long lastDirtyTimestamp,
@JsonProperty("actionType") ActionType actionType,
@JsonProperty("asgName") String asgName) {
this.instanceId = instanceId;
this.sid = sid;
this.appName = StringCache.intern(appName);
this.appGroupName = StringCache.intern(appGroupName);
this.ipAddr = ipAddr;
this.port = port == null ? 0 : port.getPort();
this.isUnsecurePortEnabled = port != null && port.isEnabled();
this.securePort = securePort == null ? 0 : securePort.getPort();
this.isSecurePortEnabled = securePort != null && securePort.isEnabled();
this.homePageUrl = homePageUrl;
this.statusPageUrl = statusPageUrl;
this.healthCheckUrl = healthCheckUrl;
this.secureHealthCheckUrl = secureHealthCheckUrl;
this.vipAddress = StringCache.intern(vipAddress);
this.secureVipAddress = StringCache.intern(secureVipAddress);
this.countryId = countryId;
this.dataCenterInfo = dataCenterInfo;
this.hostName = hostName;
this.status = status;
this.overriddenStatus = overriddenStatus == null ? overriddenStatusAlt : overriddenStatus;
this.leaseInfo = leaseInfo;
this.isCoordinatingDiscoveryServer = isCoordinatingDiscoveryServer;
this.lastUpdatedTimestamp = lastUpdatedTimestamp;
this.lastDirtyTimestamp = lastDirtyTimestamp;
this.actionType = actionType;
this.asgName = StringCache.intern(asgName);
// ---------------------------------------------------------------
// for compatibility
if (metadata == null) {
this.metadata = Collections.emptyMap();
} else if (metadata.size() == 1) {
this.metadata = removeMetadataMapLegacyValues(metadata);
} else {
this.metadata = metadata;
}
if (sid == null) {
this.sid = SID_DEFAULT;
}
}
private Map<String, String> removeMetadataMapLegacyValues(Map<String, String> metadata) {
if (InstanceInfoSerializer.METADATA_COMPATIBILITY_VALUE.equals(metadata.get(InstanceInfoSerializer.METADATA_COMPATIBILITY_KEY))) {
// TODO this else if can be removed once the server no longer uses legacy json
metadata.remove(InstanceInfoSerializer.METADATA_COMPATIBILITY_KEY);
} else if (InstanceInfoSerializer.METADATA_COMPATIBILITY_VALUE.equals(metadata.get("class"))) {
// TODO this else if can be removed once the server no longer uses legacy xml
metadata.remove("class");
}
return metadata;
}
/**
* shallow copy constructor.
*
* @param ii The object to copy
*/
public InstanceInfo(InstanceInfo ii) {
this.instanceId = ii.instanceId;
this.appName = ii.appName;
this.appGroupName = ii.appGroupName;
this.ipAddr = ii.ipAddr;
this.sid = ii.sid;
this.port = ii.port;
this.securePort = ii.securePort;
this.homePageUrl = ii.homePageUrl;
this.statusPageUrl = ii.statusPageUrl;
this.healthCheckUrl = ii.healthCheckUrl;
this.secureHealthCheckUrl = ii.secureHealthCheckUrl;
this.vipAddress = ii.vipAddress;
this.secureVipAddress = ii.secureVipAddress;
this.statusPageRelativeUrl = ii.statusPageRelativeUrl;
this.statusPageExplicitUrl = ii.statusPageExplicitUrl;
this.healthCheckRelativeUrl = ii.healthCheckRelativeUrl;
this.healthCheckSecureExplicitUrl = ii.healthCheckSecureExplicitUrl;
this.vipAddressUnresolved = ii.vipAddressUnresolved;
this.secureVipAddressUnresolved = ii.secureVipAddressUnresolved;
this.healthCheckExplicitUrl = ii.healthCheckExplicitUrl;
this.countryId = ii.countryId;
this.isSecurePortEnabled = ii.isSecurePortEnabled;
this.isUnsecurePortEnabled = ii.isUnsecurePortEnabled;
this.dataCenterInfo = ii.dataCenterInfo;
this.hostName = ii.hostName;
this.status = ii.status;
this.overriddenStatus = ii.overriddenStatus;
this.isInstanceInfoDirty = ii.isInstanceInfoDirty;
this.leaseInfo = ii.leaseInfo;
this.isCoordinatingDiscoveryServer = ii.isCoordinatingDiscoveryServer;
this.metadata = ii.metadata;
this.lastUpdatedTimestamp = ii.lastUpdatedTimestamp;
this.lastDirtyTimestamp = ii.lastDirtyTimestamp;
this.actionType = ii.actionType;
this.asgName = ii.asgName;
this.version = ii.version;
}
public enum InstanceStatus {
UP, // Ready to receive traffic
DOWN, // Do not send traffic- healthcheck callback failed
STARTING, // Just about starting- initializations to be done - do not
// send traffic
OUT_OF_SERVICE, // Intentionally shutdown for traffic
UNKNOWN;
public static InstanceStatus toEnum(String s) {
if (s != null) {
try {
return InstanceStatus.valueOf(s.toUpperCase());
} catch (IllegalArgumentException e) {
// ignore and fall through to unknown
logger.debug("illegal argument supplied to InstanceStatus.valueOf: {}, defaulting to {}", s, UNKNOWN);
}
}
return UNKNOWN;
}
}
@Override
public int hashCode() {
String id = getId();
return (id == null) ? 31 : (id.hashCode() + 31);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
InstanceInfo other = (InstanceInfo) obj;
String id = getId();
if (id == null) {
if (other.getId() != null) {
return false;
}
} else if (!id.equals(other.getId())) {
return false;
}
return true;
}
public enum PortType {
SECURE, UNSECURE
}
public static final class Builder {
private static final String COLON = ":";
private static final String HTTPS_PROTOCOL = "https://";
private static final String HTTP_PROTOCOL = "http://";
private final Function<String,String> intern;
private static final class LazyHolder {
private static final VipAddressResolver DEFAULT_VIP_ADDRESS_RESOLVER = new Archaius1VipAddressResolver();
}
@XStreamOmitField
private InstanceInfo result;
@XStreamOmitField
private final VipAddressResolver vipAddressResolver;
private String namespace;
private Builder(InstanceInfo result, VipAddressResolver vipAddressResolver, Function<String,String> intern) {
this.vipAddressResolver = vipAddressResolver;
this.result = result;
this.intern = intern != null ? intern : StringCache::intern;
}
public Builder(InstanceInfo instanceInfo) {
this(instanceInfo, LazyHolder.DEFAULT_VIP_ADDRESS_RESOLVER, null);
}
public static Builder newBuilder() {
return new Builder(new InstanceInfo(), LazyHolder.DEFAULT_VIP_ADDRESS_RESOLVER, null);
}
public static Builder newBuilder(Function<String,String> intern) {
return new Builder(new InstanceInfo(), LazyHolder.DEFAULT_VIP_ADDRESS_RESOLVER, intern);
}
public static Builder newBuilder(VipAddressResolver vipAddressResolver) {
return new Builder(new InstanceInfo(), vipAddressResolver, null);
}
public Builder setInstanceId(String instanceId) {
result.instanceId = instanceId;
return this;
}
// ...省略get/set方法
}
其实我们需要重点关注其中各个的字段的所代表的意义,基本上就对整个类的功能掌握了一大半。
是不是感觉很熟悉,其实这些内容就是我们在yml或properties配置中配置项。
我们可以看出InstanceInfo
中既有metadata
,也有dataCenterInfo
,还有一个比较重要的leaseInfo
,用来标识该应用实例的租约信息。
LeaseInfo
Eureka
使用LeaseInfo
(com/netflix/appinfo/LeaseInfo.java)来标识应用实例的租约信息。
@JsonRootName("leaseInfo")
public class LeaseInfo {
public static final int DEFAULT_LEASE_RENEWAL_INTERVAL = 30;
public static final int DEFAULT_LEASE_DURATION = 90;
// Client settings
private int renewalIntervalInSecs = DEFAULT_LEASE_RENEWAL_INTERVAL;
private int durationInSecs = DEFAULT_LEASE_DURATION;
// Server populated
private long registrationTimestamp;
private long lastRenewalTimestamp;
private long evictionTimestamp;
private long serviceUpTimestamp;
public static final class Builder {
@XStreamOmitField
private LeaseInfo result;
private Builder() {
result = new LeaseInfo();
}
public static Builder newBuilder() {
return new Builder();
}
/**
* Sets the registration timestamp.
*
* @param ts
* time when the lease was first registered.
* @return the {@link LeaseInfo} builder.
*/
public Builder setRegistrationTimestamp(long ts) {
result.registrationTimestamp = ts;
return this;
}
/**
* Sets the last renewal timestamp of lease.
*
* @param ts
* time when the lease was last renewed.
* @return the {@link LeaseInfo} builder.
*/
public Builder setRenewalTimestamp(long ts) {
result.lastRenewalTimestamp = ts;
return this;
}
/**
* Sets the de-registration timestamp.
*
* @param ts
* time when the lease was removed.
* @return the {@link LeaseInfo} builder.
*/
public Builder setEvictionTimestamp(long ts) {
result.evictionTimestamp = ts;
return this;
}
/**
* Sets the service UP timestamp.
*
* @param ts
* time when the leased service marked as UP.
* @return the {@link LeaseInfo} builder.
*/
public Builder setServiceUpTimestamp(long ts) {
result.serviceUpTimestamp = ts;
return this;
}
/**
* Sets the client specified setting for eviction (e.g. how long to wait
* without renewal event).
*
* @param d
* time in seconds after which the lease would expire without
* renewa.
* @return the {@link LeaseInfo} builder.
*/
public Builder setDurationInSecs(int d) {
if (d <= 0) {
result.durationInSecs = DEFAULT_LEASE_DURATION;
} else {
result.durationInSecs = d;
}
return this;
}
/**
* Sets the client specified setting for renew interval.
*
* @param i
* the time interval with which the renewals will be renewed.
* @return the {@link LeaseInfo} builder.
*/
public Builder setRenewalIntervalInSecs(int i) {
if (i <= 0) {
result.renewalIntervalInSecs = DEFAULT_LEASE_RENEWAL_INTERVAL;
} else {
result.renewalIntervalInSecs = i;
}
return this;
}
/**
* Build the {@link InstanceInfo}.
*
* @return the {@link LeaseInfo} information built based on the supplied
* information.
*/
public LeaseInfo build() {
return result;
}
}
private LeaseInfo() {
}
/**
* TODO: note about renewalTimestamp legacy:
* The previous change to use Jackson ser/deser changed the field name for lastRenewalTimestamp to renewalTimestamp
* for serialization, which causes an incompatibility with the jacksonNG codec when the server returns data with
* field renewalTimestamp and jacksonNG expects lastRenewalTimestamp. Remove this legacy field from client code
* in a few releases (once servers are updated to a release that generates json with the correct
* lastRenewalTimestamp).
*/
@JsonCreator
public LeaseInfo(@JsonProperty("renewalIntervalInSecs") int renewalIntervalInSecs,
@JsonProperty("durationInSecs") int durationInSecs,
@JsonProperty("registrationTimestamp") long registrationTimestamp,
@JsonProperty("lastRenewalTimestamp") Long lastRenewalTimestamp,
@JsonProperty("renewalTimestamp") long lastRenewalTimestampLegacy, // for legacy
@JsonProperty("evictionTimestamp") long evictionTimestamp,
@JsonProperty("serviceUpTimestamp") long serviceUpTimestamp) {
this.renewalIntervalInSecs = renewalIntervalInSecs;
this.durationInSecs = durationInSecs;
this.registrationTimestamp = registrationTimestamp;
this.evictionTimestamp = evictionTimestamp;
this.serviceUpTimestamp = serviceUpTimestamp;
if (lastRenewalTimestamp == null) {
this.lastRenewalTimestamp = lastRenewalTimestampLegacy;
} else {
this.lastRenewalTimestamp = lastRenewalTimestamp;
}
}
// 省略get/set方法...
}
字段意义说明
这些参数主要用于标识应用实例的心跳情况,比如约定的心跳周期,租约有效期,最近一次续约的时间等。
ServiceInstance
ServiceInstance
(org/springframework/cloud/client/ServiceInstance.java)是Spring Cloud对service discovery的实例信息的抽象接口,约定了服务发现的实例应用有哪些通用的信息。
public interface ServiceInstance {
/**
* @return the service id as registered.
*/
String getServiceId();
/**
* @return the hostname of the registered ServiceInstance
*/
String getHost();
/**
* @return the port of the registered ServiceInstance
*/
int getPort();
/**
* @return if the port of the registered ServiceInstance is https or not
*/
boolean isSecure();
/**
* @return the service uri address
*/
URI getUri();
/**
* @return the key value pair metadata associated with the service instance
*/
Map<String, String> getMetadata();
/**
* @return the scheme of the instance
*/
default String getScheme() {
return null;
}
}
由于Spring Cloud Discovery适配了Zookeeper、Consul、Netflix Eureka等注册中心,因此其ServiceInstance定义更为抽象和通用,而且采取的是定义方法的方式。Spring Cloud对该接口的实现类为EurekaRegistration(org/springframework/cloud/netflix/eureka/serviceregistry/EurekaRegistration.java), EurekaRegistration实现了ServiceInstance接口,同时还实现了Closeable接口,它的作用之一就是在close的时候调用eurekaClient.shutdown()方法,实现优雅关闭Eureka Client。
InstanceStatus
InstanceStatus
用于标识服务实例的状态,它是一个枚举。
public enum InstanceStatus {
UP, // Ready to receive traffic
DOWN, // Do not send traffic- healthcheck callback failed
STARTING, // Just about starting- initializations to be done - do not
// send traffic
OUT_OF_SERVICE, // Intentionally shutdown for traffic
UNKNOWN;
public static InstanceStatus toEnum(String s) {
if (s != null) {
try {
return InstanceStatus.valueOf(s.toUpperCase());
} catch (IllegalArgumentException e) {
// ignore and fall through to unknown
logger.debug("illegal argument supplied to InstanceStatus.valueOf: {}, defaulting to {}", s, UNKNOWN);
}
}
return UNKNOWN;
}
}
从定义可以看出,服务实例主要有UP、DOWN、STARTING、OUT_OF_SERVICE、UNKNOWN这几个状态。其中OUT_OF_SERVICE标识停止服务,即停止接收请求,处于这个状态的服务实例将不会被路由到,经常用于升级部署的场景。
4.2 核心操作
对于服务发现来说,围绕服务实例主要有如下几个重要的操作:
❑ 服务注册(register)
❑ 服务下线(cancel)
❑ 服务租约(renew)
❑ 服务剔除(evict)
围绕这几个功能,Eureka设计了几个核心操作类:
❑ com/netflix/eureka/lease/LeaseManager.java
❑ com/netflix/discovery/shared/LookupService.java
❑ com/netflix/eureka/registry/InstanceRegistry.java
❑ com/netflix/eureka/registry/AbstractInstanceRegistry.java
❑ com/netflix/eureka/registry/PeerAwareInstanceRegistryImpl.java
Spring Cloud Eureka在Netflix Eureka的基础上,抽象或定义了如下几个核心类:
❑ org/springframework/cloud/netflix/eureka/server/InstanceRegistry.java
❑ org/springframework/cloud/client/serviceregistry/ServiceRegistry.java
❑ org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistry.java
❑ org/springframework/cloud/netflix/eureka/serviceregistry/EurekaRegistration.java
❑ org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java
❑ org/springframework/cloud/netflix/eureka/EurekaClientConfigBean.java
❑ org/springframework/cloud/netflix/eureka/EurekaInstanceConfigBean.java
其中LeaseManager
以及LookupService
是Eureka关于服务发现相关操作定义的接口类,前者定义了服务写操作相关的方法,后者定义了查询操作相关的方法。下面我们重点看下这两个类。
LeaseManager
public interface LeaseManager<T> {
/**
* Assign a new {@link Lease} to the passed in {@link T}.
*
* @param r
* - T to register
* @param leaseDuration
* @param isReplication
* - whether this is a replicated entry from another eureka node.
*/
void register(T r, int leaseDuration, boolean isReplication);
/**
* Cancel the {@link Lease} associated w/ the passed in <code>appName</code>
* and <code>id</code>.
*
* @param appName
* - unique id of the application.
* @param id
* - unique id within appName.
* @param isReplication
* - whether this is a replicated entry from another eureka node.
* @return true, if the operation was successful, false otherwise.
*/
boolean cancel(String appName, String id, boolean isReplication);
/**
* Renew the {@link Lease} associated w/ the passed in <code>appName</code>
* and <code>id</code>.
*
* @param id
* - unique id within appName
* @param isReplication
* - whether this is a replicated entry from another ds node
* @return whether the operation of successful
*/
boolean renew(String appName, String id, boolean isReplication);
/**
* Evict {@link T}s with expired {@link Lease}(s).
*/
void evict();
}
LeaseManager(com/netflix/eureka/lease/LeaseManager.java)接口定义了应用服务实例在服务中心的几个操作方法:register、cancel、renew、evict。
这里简单介绍下这几个方法:
❑ Register:用于注册服务实例信息。
❑ Cancel:用于删除服务实例信息。
❑ Renew:用于与Eureka Server进行心跳操作,维持租约。
❑ evit是Server端的一个方法,用于剔除租约过期的服务实例信息。
LookupService
public interface LookupService<T> {
/**
* Returns the corresponding {@link Application} object which is basically a
* container of all registered <code>appName</code> {@link InstanceInfo}s.
*
* @param appName
* @return a {@link Application} or null if we couldn't locate any app of
* the requested appName
*/
Application getApplication(String appName);
/**
* Returns the {@link Applications} object which is basically a container of
* all currently registered {@link Application}s.
*
* @return {@link Applications}
*/
Applications getApplications();
/**
* Returns the {@link List} of {@link InstanceInfo}s matching the the passed
* in id. A single {@link InstanceInfo} can possibly be registered w/ more
* than one {@link Application}s
*
* @param id
* @return {@link List} of {@link InstanceInfo}s or
* {@link java.util.Collections#emptyList()}
*/
List<InstanceInfo> getInstancesById(String id);
/**
* Gets the next possible server to process the requests from the registry
* information received from eureka.
*
* <p>
* The next server is picked on a round-robin fashion. By default, this
* method just returns the servers that are currently with
* {@link com.netflix.appinfo.InstanceInfo.InstanceStatus#UP} status.
* This configuration can be controlled by overriding the
* {@link com.netflix.discovery.EurekaClientConfig#shouldFilterOnlyUpInstances()}.
*
* Note that in some cases (Eureka emergency mode situation), the instances
* that are returned may not be unreachable, it is solely up to the client
* at that point to timeout quickly and retry the next server.
* </p>
*
* @param virtualHostname
* the virtual host name that is associated to the servers.
* @param secure
* indicates whether this is a HTTP or a HTTPS request - secure
* means HTTPS.
* @return the {@link InstanceInfo} information which contains the public
* host name of the next server in line to process the request based
* on the round-robin algorithm.
* @throws java.lang.RuntimeException if the virtualHostname does not exist
*/
InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}
LookupService(com/netflix/discovery/shared/LookupService.java)接口定义了Eureka Client从服务中心获取服务实例的查询方法。
这个接口主要是给Client端用的,其定义了获取所有应用信息、根据应用id获取所有服务实例,以及根据visualHostname使用round-robin方式获取下一个服务实例的方法。
4.3 核心实现逻辑
实现逻辑也可以理解成Eureka所需要解决的问题。
服务实例如何注册到服务中心
本质上就是在服务启动的时候,需要调用Eureka Server的REST API的register方法,去注册该应用实例的信息。对于使用Java的应用服务,可以使用Netflix的Eureka Client封装的API去调用;对于Spring Cloud的应用,可以使用spring-cloud-starter-netflix-eureka-client,基于Spring Boot的自动配置,自动帮你实现服务信息的注册。具体感兴趣的可以看一下源码实现。
服务实例如何从服务中心剔除
正常情况下服务实例在关闭应用的时候,应该通过钩子方法或其他生命周期回调方法去调用Eureka Server的REST API的de-register方法,来删除自身服务实例的信息。另外为了解决服务实例挂掉或其他异常情况没有及时删除自身信息的问题,Eureka Server要求Client端定时进行续约,也就是发送心跳,来证明该服务实例还是存活的,是健康的,是可以调用的。如果租约超过一定时间没有进行续约操作,Eureka Server端会主动剔除。这一点Eureka Server采用的就是分布式应用里头经典的心跳模式。
服务实例信息的一致性问题
由于服务注册及发现中心不可能是单点的,其自身势必有个集群,那么服务实例注册信息如何在这个集群里保持一致呢?这跟Eureka Server的架构有关,理解其设计理念有助于后面的实战及调优,下面主要分AP优于CP、Peer to Peer架构、Zone及Region设计、SELF PRESERVATION设计四个方面来阐述。
AP由于CP
分布式系统领域有个重要的CAP理论,该理论由加州大学伯克利分校的Eric Brewer教授提出,由麻省理工学院的Seth Gilbert和Nancy Lynch进行理论证明。该理论提到了分布式系统的CAP三个特性:
❑ Consistency:数据一致性,即数据在存在多副本的情况下,可能由于网络、机器故障、软件系统等问题导致数据写入部分副本成功,部分副本失败,进而造成副本之间数据不一致,存在冲突。满足一致性则要求对数据的更新操作成功之后,多副本的数据保持一致。
❑ Availability:在任何时候客户端对集群进行读写操作时,请求能够正常响应,即在一定的延时内完成。
❑ Partition Tolerance:分区容忍性,即发生通信故障的时候,整个集群被分割为多个无法相互通信的分区时,集群仍然可用。
对于分布式系统来说,一般网络条件相对不可控,出现网络分区是不可避免的,因此系统必须具备分区容忍性。在这个前提下分布式系统的设计则在AP及CP之间进行选择。不过不能理解为CAP三者之间必须三选二,它们三者之间不是对等和可以相互替换的。在分布式系统领域,P是一个客观存在的事实,不可绕过,所以P与AC之间不是对等关系。
对于ZooKeeper,它是"C"P的,之所以C加引号是因为ZooKeeper默认并不是严格的强一致,比如客户端A提交一个写操作,ZooKeeper在过半数节点操作成功之后就返回,此时假设客户端B的读操作请求到的是A写操作尚未同步到的节点,那么读取到的就不是客户端A写操作成功之后的数据。如果在使用的时候需要强一致,则需要在读取数据的时候先执行一下sync操作,即与leader节点先同步下数据,这样才能保证强一致。在极端的情况下发生网络分区的时候,如果leader节点不在nonquorum分区,那么对这个分区上节点的读写请求将会报错,无法满足Availability特性。
Eureka是在部署在AWS的背景下设计的,其设计者认为,在云端,特别是在大规模部署的情况下,失败是不可避免的,可能因为Eureka自身部署失败,注册的服务不可用,或者由于网络分区导致服务不可用,因此不能回避这个问题。要拥抱这个问题,就需要Eureka在网络分区的时候,还能够正常提供服务注册及发现功能,因此Eureka选择满足Availability这个特性。Peter Kelley在《Eureka! Why You Shouldn’t Use ZooKeeper for Service Discovery》(http://medium.com/knerd/eureka-why-you-shouldnt-use-zookeeper-for-service-discovery-4932c5c7e764)一文中指出,在实际生产实践中,服务注册及
中心保留可用及过期的数据总比丢失掉可用的数据好。这样的话,应用实例的注册信息在集群的所有节点间并不是强一致的,这就需要客户端能够支持负载均衡及失败重试。在Netflix的生态中,由ribbon提供这个功能。
Peer to Peer架构
一般而言,分布式系统的数据在多个副本之间的复制方式,可分为主从复制和对等复制。
1.主从复制
主从复制也就是广为人知的Master-Slave模式,即有一个主副本,其他副本为从副本。所有对数据的写操作都提交到主副本,最后再由主副本更新到其他从副本。具体更新的方式,还可以细分为同步更新、异步更新、同步及异步混合。
对于主从复制模式来讲,写操作的压力都在主副本上,它是整个系统的瓶颈,但是从副本可以帮主副本分担读请求。
2.对等复制
即Peer to Peer的模式,副本之间不分主从,任何副本都可以接收写操作,然后每个副本之间相互进行数据更新。
对于对等复制模式来讲,由于任何副本都可以接收写操作请求,不存在写操作压力瓶颈。但是由于每个副本都可以进行写操作处理,各个副本之间的数据同步及冲突处理是一个比较棘手的问题。
Eureka Server采用的就是Peer to Peer的复制模式。这里我们分为客户端及服务端两个角度来阐述。
(1)客户端
Client端一般通过如下配置Eureka Server的peer节点:
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:8761/eureka/, http://127.0.0.1:8762/eureka/
实际代码里支持preferSameZoneEureka,即有多个分区的话,优先选择与应用实例所在分区一样的其他服务的实例,如果没找到则默认使用defaultZone。客户端使用quarantineSet维护了一个不可用的Eureka Server列表,进行请求的时候,优先从可用的列表中进行选择,如果请求失败则切换到下一个Eureka Server进行重试,重试次数默认为3。
另外为了防止每个Client端都按配置文件指定的顺序进行请求造成Eureka Server节点请求分布不均衡的情况,Client端有个定时任务(默认5分钟执行一次)来刷新并随机化Eureka Server的列表。
(2)服务端
Eureka Server本身依赖了Eureka Client,也就是每个Eureka Server是作为其他Eureka Server的Client。在单个Eureka Server启动的时候,会有一个syncUp的操作,通过Eureka Client请求其他Eureka Server节点中的一个节点获取注册的应用实例信息,然后复制到其他peer节点。
Eureka Server在执行复制操作的时候,使用HEADER_REPLICATION的http header来将这个请求操作与普通应用实例的正常请求操作区分开来。通过HEADER_REPLICATION来标识是复制请求,这样其他peer节点接收到请求的时候,就不会再对它的peer节点进行复制操作,从而避免死循环。
Eureka Server由于采用了Peer to peer的复制模式,其重点要解决的另外一个问题就是数据复制的冲突问题。针对这个问题,Eureka采用如下两个方式来解决:
❑ lastDirtyTimestamp标识
❑ heartbeat
针对数据的不一致,一般是通过版本号机制来解决,最后在不同副本之间只需要判断请求复制数据的版本号与本地数据的版本号高低就可以了。Eureka没有直接使用版本号的属性,而是采用一个叫作lastDirtyTimestamp的字段来对比。
如果开启SyncWhenTimestampDiffers配置(默认开启),当lastDirtyTimestamp不为空的时候,就会进行相应的处理:
❑ 如果请求参数的lastDirtyTimestamp值大于Server本地该实例的lastDirtyTimestamp值,则表示Eureka Server之间的数据出现冲突,这个时候就返回404,要求应用实例重新进行register操作。
❑ 如果请求参数的lastDirtyTimestamp值小于Server本地该实例的lastDirtyTimestamp值,如果是peer节点的复制请求,则表示数据出现冲突,返回409给peer节点,要求其同步自己最新的数据信息。
peer节点之间的相互复制并不能保证所有操作都能够成功,因此Eureka还通过应用实例与Server之间的heartbeat也就是renewLease操作来进行数据的最终修复,即如果发现应用实例数据与某个Server的数据出现不一致,则Server返回404,应用实例重新进行register操作。
Zone及Region设计
由于Netflix的服务大部分在Amazon上,因此Eureka的设计有一部分也是基于Amazon的Zone及Region的基础设施之上。
在Amazon EC2托管在全球的各个地方,它用Region来代表一个独立的地理区域,比如Eureka Server默认设置了4个Region:us-east-1、us-west-1、us-west-2、eu-west-1。Amazon的部分Region代码及名称列表如下所示。
在每个Region下面,还分了多个AvailabilityZone,一个Region对应多个AvailabilityZone。每个Region之间是相互独立及隔离的,默认情况下资源只在单个Region之间的Availability-Zone进行复制,跨Region之间不会进行资源复制。Region与AvailabilityZone之间的关系下图所示。
AvailabilityZone就类似Region下面的子Region,比如us-east-1的Region可分为us-east-1a、us-east-1c、us-east-1d、us-east-1e这几个AvailabilityZone。AvailabilityZone可看作Region下面的一个个机房,各个机房相对独立,主要是为了Region的高可用设计,当同一个Region下面的AvailabilityZone不可用时,还有其他AvailabilityZone可用。
Eureka Server原生支持了Region及AvailabilityZone,由于资源在Region之间默认是不会复制的,因此Eureka Server的高可用主要就在于Region下面的AvailabilityZone。
Eureka Client支持preferSameZone,也就是获取Eureka Server的serviceUrl优先拉取跟应用实例同处于一个AvailabilityZone的Eureka Server地址列表。一个AvailabilityZone可以设置多个Eureka Server实例,它们之间构成peer节点,然后采用Peer to Peer的复制模式。
Netflix的Ribbon组件针对多个AvailabilityZone提供了ZoneAffinity的支持,允许在客户端路由或网关路由时,优先选取与自身实例处于同一个AvailabilityZone的服务实例。
SELF PRESERVATION设计
在分布式系统设计里头,通常需要对应用实例的存活进行健康检查,这里比较关键的问题就是要处理好网络偶尔抖动或短暂不可用时造成的误判。另外Eureka Server端与Client端之间如果出现网络分区问题,在极端情况下可能会使得Eureka Server清空部分服务的实例列表,这个将严重影响到Eureka Server的Availability属性。因此Eureka Server引入了SELF PRESERVATION机制。
Eureka Client端与Server端之间有个租约,Client要定时发送心跳来维持这个租约,表示自己还存活着。Eureka通过当前注册的实例数,去计算每分钟应该从应用实例接收到的心跳数,如果最近一分钟接收到的续约的次数小于等于指定阈值的话,则关闭租约失效剔除,禁止定时任务剔除失效的实例,从而保护注册信息。
五、Eureka参数调优及实战
5.1 核心参数
Client端
这里笔者将Client端的参数分为基本参数、定时任务参数、http参数三大类来梳理。
基本参数
定时任务参数
http参数
Server端
笔者将Server端的参数分为如下几类:基本参数、response cache参数、peer相关参数、http参数。
基本参数
response cache参数
Eureka Server为了提升自身REST API接口的性能,提供了两个缓存:一个是基于ConcurrentMap的readOnlyCacheMap,一个是基于Guava Cache的readWriteCacheMap。其相关参数如下所示。
peer相关参数
http参数
六、其他
- 线上问题解决
- ❑ 为什么服务下线了,Eureka Server接口返回的信息还会存在。
- ❑ 为什么服务上线了,Eureka Client不能及时获取到。
准备再开一篇文章进行讲解,这里大家可以思考下。