目录
ServiceInstance(截取部分代码,并做简单标方法
2.Server端:基本参数+response cache参数+peer相关参数+http参数
干货分享,感谢您的阅读!
随着信息技术的发展和应用系统规模的不断扩大,服务发现与注册成为了构建可靠和高效分布式系统的关键技术之一。本文将探讨服务发现与注册的演进历程,从传统单体架构到当前微服务时代的技术选型和实现方案,重点介绍了Eureka作为一种经典的服务发现和注册中心的设计理念、架构特点以及高可用解决方案。
一、服务发现与注册的由来
1.单体架构时代
服务自成一体,依赖的少数外部服务会采用配置域名的方式访问
eg:使用外部短信供应商的短信发送接口,会使用appId和appKey调用该供应商的域名接口即可。
2.SOA时代
由单体应用拆分为粒度较粗的服务化架构,依赖的内部服务比较多,服务间调用采用nginx策略。
eg:A服务部署在3台虚拟机上,这三个服务实例都有自己独立的内网IP,假设B服务要调用A服务的接口服务,可通过以下两种方式:
方式一
假设知道A服务的地址,则直接修改B服务的nginx如下
upstream servicea_api_servers {
server 192.168.12.01:80 weight=3 max_fails=3 fail_timeout=20s;
server 192.168.12.02:80 weight=3 max_fails=3 fail_timeout=20s;
server 192.168.12.03:80 weight=3 max_fails=3 fail_timeout=20s;
}
server {
listen 8080;
listen 80;
server_name serviceb.com.cn;
if ($request_method !~* (GET|POST)) {return 503;}
proxy_http_version 1.1;
location /api/servicea {
proxy_http_version 1.0;
proxy_pass http://servicea_api_servers/api/;
}
}
缺点很明显,B服务耦合了A服务的实现细节,A服务实例发生变化变更时,需要下游B服务做相应的改动,不理想。
方式二
对方式一解耦合,由服务提供者A服务自己维护自己的IP实例,对下流服务B只暴露统一内网域名来消费,所以,B服务的nginx配置应该改为
server {
listen 8080;
listen 80;
server_name serviceb.com.cn;
if ($request_method !~* (GET|POST)) {return 503;}
proxy_http_version 1.1;
location /api/servicea {
proxy_http_version 1.0;
proxy_pass http://servicea_api_servers/api/;
}
}
而A服务的nginx也许修改如下
upstream servicea_api_servers {
server 192.168.12.01:80 weight=3 max_fails=3 fail_timeout=20s;
server 192.168.12.02:80 weight=3 max_fails=3 fail_timeout=20s;
server 192.168.12.03:80 weight=3 max_fails=3 fail_timeout=20s;
}
server {
listen 8080;
listen 80;
server_name servicea.com.cn;
if ($request_method !~* (GET|POST)) {return 503;}
proxy_http_version 1.1;
location /api/ {
proxy_http_version 1.0;
proxy_pass http://servicea_api_servers/api/;
}
}
3.微服务时代
随着Docker流行,业务服务不再部署在固定的虚拟机上,其IP地址也不在固定,不同的思路提出了不同的方案:
方案一
以nginx为例,采用手工或通过脚本方式,在部署的时候去更新nginx配置文件,然后reload。
或采用ngx_http_dyups_module通过rest api来在运行时直接更新upstream而不需要reload。
方案二
将服务注册中心作为一个标配的分布式服务组件,网关等都从服务注册中心获取相关服务的实例信息,实现动态路由。
二、服务发现与注册的技术选型与Eureka简介
1.服务发现与注册的技术选型
名称 | 类型 | AP或CP | 语言 | 依赖 | 集成 | 一致性算法 |
Zookeeper | General | CP | Java | JVM | Client Binding | Paxos |
Doozer | General | CP | Go | Client Binding | Paxos | |
Consul | General | CP | Go | HTTP/DNS Library | Raft | |
Etcd | General | CP or Mixed(1) | Go | Client Binding/HTTP | Raft | |
SmartStack | Dedicated | AP | Rubby | haproxy/Zookeeper | Sidekick(nerve/sysnapse) | |
Euerka | Dedicated | AP | Java | JVM | Java Client | |
NSQ(lookupd) | Dedicated | AP | Go | Client Binding | ||
Serf | Dedicated | AP | Go | Local CLI | ||
Spotify | Dedicated | AP | N/A | Bind | DNS Library | |
SkyDNS | Dedicated | AP | Go | HTTP/DNS Library |
Eureka Server端采用的是P2P的复制模式,但不保证复制操作一定成功,因此提供的是一种最终一致性的服务实例视图;
我们为什么选用Eureka?
- 选择AP而不是CP,设计理念中讲解
- 如果团队是Java 体系或偏好Java 体系,则技术体系比较统一,出问题对于组内人员好排查
- 是Netflix开源套件的组成部分,与zuul、ribbon等整合比较好
2.Eureka简介
Eureka包括Eureka Server及Eureka Client,Eureka Server提供REST服务,Eureka Client则是使用Java编写的客户端,是Netflix公司开源的一款服务发现组件,为负载均衡、failover等提供支持,简图如下:
Eureka最初是针对AWS不提供中间服务层的负载均衡的限制而设计开发的。那么Netflix为什么要设计Eureka而不是直接利用AWS Elastic Load Balancer或者AWS Route 53呢?
- 安全性问题,AWS Elastic Load Balancer存在暴露外网风险
- AWS Elastic Load Balancer采用传统负载均衡,无法直接基于服务元数据信息定制负载均衡算法
- AWS Route 53基于传统DNS服务存在缓存更新延时问题
3.新的替换方案---Nacos
我在总结这些的时候,当时nacos还没有开源问世,但是在2019年 6 月份 Aliware 技术行上海站 Dubbo 开发者沙龙上,阿里巴巴高级技术专家郭平 (坤宇) 宣布了阿里巴巴的一个新开源项目 Nacos,在上周五凌晨 (7 月 20 日) 低调发布了第一个版本。Nacos 会无缝支持 Spring Cloud,为 Spring Cloud 用户其提供更简便的配置中心和注册中心的解决方案,使用 Nacos 不用再仅仅为服务和配置就需要在生产上 hold 住 Eureka,Spring Cloud Config Server,Git,RabbitMQ/Kafka 起码四个开源产品。
三、Eureka设计理念
1.主要解决的三大问题
服务实例如何注册到服务中心
本质上就是在服务启动的时候,需要调用Eureka Server的REST API的register方法,去注册该应用的实例信息。
对于Spring Cloud应用,可以使用spring-cloud-starter-netflix-eureka-client,基于Spring Boot自动配置,自动实现服务信息注册。
服务实例如何从服务中心剔除
正常情况下,服务应用在关闭应用的时候,应通过钩子方法或其他生命周期回调方法去调用Eureka Server的REST API的de-register方法,实现删除自身服务实例信息。
异常情况下因服务实例挂掉没有及时删除自身信息的问题,Eureka Server要求Client端定时进行续约,也就是发送心跳来证明服务实例依旧是存活的。若租约超过一定时间没有进行续约操作,Eureka Server端主动剔除。
服务实例信息一致性问题
服务注册及发现中心不可能是单点的,其自身势必有个集群,服务实例注册信息如何在这个集群中保持一致与Eureka Server的架构有关,这需要通过后面几个设计理念分析。
2.AP由于CP
具体的CAP理论见微服务架构-实现技术之三大关键要素2数据一致性:分布式事物+CAP&BASE+可靠事件模式+补偿模式+Sagas模式+TCC模式+最大努力通知模式+人工干预模式_张彦峰ZYF的博客-CSDN博客对于分布式系统来说,一般网络条件相对不可控,出现网络分区是不可避免的,因此系统必须具备分区容忍性P。
由于Eureka是部署在AWS的背景下设计的,所以在云端,特别是大规模部署的情况下,失败是必不可免的,可能因为Eureka自身部署失败,注册服务不可用,或者由于网络分区导致服务不可用,因为不能回避这个问题,需要Eureka在网络分区的时候,还能正常提供服务注册及发现功能,故Eureka选择满足Availability这个特性。
Eureka的AP特性才是符合实际情况的。
3.Peer to Peer架构
一般,分布式系统的数据在多个副本之间的复制方式分为主从复制和对等复制。
主从复制
Master-Slave模式,有一个主副本,其他副本为从副本。主副本变化更新到从副本的具体更新情况又分为同步更新、异步更新、同步及异步混合。
对于主从复制模式来讲,写操作的压力都在主副本上,其成为了整个系统的瓶颈,但从副本可帮主副本分担读请求。
对等复制
Peer to Peer模式,副本之间无主从概念,任何副本都可以接收写操作,不存在写操作压力瓶颈,然后每个副本之间都可以相互进行数据更新,但是,各副本之间的数据同步与冲突处理就变为了一个棘手的问题。
Eureka Server采用的就是Peer to Peer复制模式。站在客户端和服务端两个角度分析:
(1)客户端
eureka:
client:
serviceUrl:
defaultZone:http://192.168.8.220:8082/eureka/,http://192.168.8.226:8082/eureka/
实际代码里支持preferSameZoneEureka,即有多个分区的话,优先选择与应用所在分区一样的其他服务的实例,如果没有找到则默认使用defaultZone。
客户端使用quarantineSet维护了一个不可用的Eureka Server列表,进行请求的时候,优先从可用列表中进行选择,如果请求失败则切换到下一个Eureka Server进行重试,重试次数默认为3。
为防止每个Clent端都按配置文件指定的顺序进行请求造成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模式中数据复制的冲突问题:lastDirtyTimestamp+heartbeat。
如果开启SyncWhenTimestampDiffers配置(默认开启),当lastDirtyTimestamp不为空的时候,有以下处理:
- 若请求的lastDirtyTimestamp值大于Server本地实例的若请求的lastDirtyTimestamp值,则表示Eureka Server间数据冲突,返回404,要求应用实例重新进行register操作。
- 若请求的lastDirtyTimestamp值小于Server本地实例的若请求的lastDirtyTimestamp值,如果是peer节点的复制请求,则表示数据出现冲突,返回409给peer节点,要求其同步最新的数据信息。
peer节点之间的相互复制并不保证所有操作都能成功,故Eureka需通过应用实例与Server之间的heartbeat也就是renewLease操作来进行数据的最终修复,即发现数据不一致,Server返回404,应用实例重新进行register操作。
Eureka Server采用的是对等通信(P2P),无中心化的架构,无master/slave区分,每一个server都是对等的,既是Server又是Client,所以其集群方式可以自由发挥,可以各点互连,也可以接力互连。Eureka Server通过运行多个实例以及彼此之间互相注册来提高可用性,每个节点需要添加一个或多个有效的serviceUrl指向另一个节点。
4.Zone及Region设计
Netflix的服务大部分是在Amazon上,故Eureka的设计有一部分也是基于Amazon的Zone及Region的基础设施上。
Eureka提供了region和zone两个概念来进行分区,这两个概念均来自于亚马逊的AWS(AWS即Amazon Web Services,是亚马逊公司旗下云计算服务平台,为全世界范围内的客户提供云解决方案。AWS面向用户提供包括弹性计算、存储、数据库、应用程序在内的一整套云计算服务,帮助企业降低IT投入成本和维护成本。)
- region:可以简单理解为地理上的分区,比如亚洲地区,或者华北地区,再或者北京等等,没有具体大小的限制。根据项目具体的情况,可以自行合理划分region。
- zone:可以简单理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2两个zone。
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复制模式。
5.SELF PRESERVATION设计
Eureka的自我保护特性主要用于减少在网络分区或者不稳定状况下的不一致性问题
- Eureka自我保护的产生原因:
Eureka在运行期间会统计心跳失败的比例,在15分钟内是否低于85%,如果出现了低于的情况,Eureka Server会将当前的实例注册信息保护起来,同时提示一个警告,一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据。也就是不会注销任何微服务。
- Kubernetes环境
但在Kubernetes环境下,如果某个节点的Kubelet下线,比较容易造成自我保护。阶段如下:
-
- Kubelet下线,会造成大量的服务处于Unknow状态, Kubernetes为了维持Deployment指定的Pod数量,会在其他节点上启动服务,注册到Eureka,这个时候是不会触发自我保护的。
- 重新启动Kubelet进行让节点Ready,这时候Kubernetes发现Pod数量超过了设计值,然后Terminate原来Unknown的Pod,这个时候就会出现大量的服务下线状态,从而触发自我保护
- 而处于自我保护状态的Eureka不再同步服务的信息,同时也不再和另一个实例保持同步。
这个是个比较核心的问题,如果这样的话,只能够手工删除Eureka实例让他重建,恢复正常状况。
所以在Kubernetes环境下,关闭服务保护,让Eureka和服务保持同步状态。
目前的解决办法
- Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔。
eureka.server.enable-self-preservation# 设为false,关闭自我保护
eureka.server.eviction-interval-timer-in-ms # 清理间隔(单位毫秒,默认是60*1000)
- Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间
eureka.client.healthcheck.enabled# 开启健康检查(需要spring-boot-starter-actuator依赖)
eureka.instance.lease-renewal-interval-in-seconds# 续约更新时间间隔(默认30秒)
eureka.instance.lease-expiration-duration-in-seconds # 续约到期时间(默认90秒)
四、Eureka基本架构与细化架构分析
(一)基本架构分析
从基本架构分析,有三个基本逻辑角色组成:
Eureka Server:提供服务注册和发现;
Eureka Client Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到;
Eureka Client Service Consumer:服务消费方,从Eureka获取注册服务列表,从而实现消费服务。
(二)细化结构分析
将基本架构展开,我们可以参看具体的细化架构分析:
1.与服务治理直接相关的几个概念:
register
服务注册,当Eureka客户端向Eureka Server注册时,客户端会提供自身的元数据,如IP地址、端口、运行状况指示符URL等信息。
renew
服务续约,Eureka客户端会每隔30秒发送一次心跳来进行服务续约。
通过服务续约来告知Eureka Server该客户端仍然存在,希望服务器不要剔除自己。
cancel
服务下线,Eureka客户端在程序关闭时向Eureka服务器发送取消请求。
发送请求后,该客户端实例信息将从服务器的实例注册列表中删除。
eviction
服务剔除,在默认情况下,如Eureka客户端连续90秒没有向Eureka服务器发送续约心跳,Eureka服务器就会将该服务实例从服务列表中删除。
fetch register
获取服务注册列表信息,Eureka客户端从服务器获取服务注册列表信息,并将其缓存本地。
客户端会使用该信息查找其他服务,从而进行远程调用。
该注册列表信息每隔30秒更新一次,每次返回的注册列表信息可能与Eureka客户端缓存信息有所不同,Eureka客户端会自动处理两者直接的差异。
2.注册中心服务器
实现服务的定义,必须包含服务的含义,在Eureka中使用两层HashMap来达到服务存储的效果。
第一层HashMap的key是APP Name,也就是应用的名字。
第二层HashMap的key是Instance Name,也就是实例的名字。
面向服务注册中心的操作主要包括服务注册、服务续约和服务下线,实际上这三种不同的操作对于注册中心服务器而言其执行的工作流程基本一致,都是对服务存储的操作,并把这一操作同步到其他Eureka节点。
注册中心服务器同时提供服务列表,为提高性能,服务列表在Eureka Server会缓存一份,同时每30秒更新一次。
3.服务提供者
服务提供者关注服务注册、服务续约和服务下线等功能,可以使用服务器提供的RESTful API(基于Jersey框架实现)完成上述操作。
4.服务消费者
本质上与服务提供者是同一个客户端,服务消费者在本地会有一份缓存,需要定期更新缓存。
五、Eureka高可用方案与Eureka区域亲和性
1.Eureka Serve的高可用
Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,形成一组互相注册的服务中心,以实现服务列表的相互同步,达到高可用的效果。
知识点1:replicateToPeers操作,该操作用来实现服务器节点之间的状态同步。
通过这种方式,任意一个Eureka Server获取来自客户端的通知之后就能保证状态会在所有的Eureka Server中得到更新。
具体实现方式,接收到请求的Eureka Server把请求再次转发到其他Eureka Server,调用同样的接口,传入同样的参数,唯一不同之处就是会告诉其他服务器不需要在进行复制。
知识点2:如何知道有哪些Eureka Server节点?
Eureka Server在启动后会调用EurekaClientConfig.getEurekaServerServiceUrl来获取所有的peer节点,并且会定期更新。
getEurekaServerServiceUrl方法的默认实现是从配置文件读取,所以如果Eureka Server节点相对固定,可以通过在配置文件中配置节点列表的方式来实现。
如果在运行过程中新增加了一个节点或者服务器重启,那么每个节点可以把自己当作是服务消费者从其他Eureka Server节点获取所有服务的注册信息,然后在对每个服务执行注册操作,从而完成初始化。
2.Eureka Server的区域亲和性
具体见Eureka设计理念4.
六、Eureka核心类与核心操作分析
1.Eureka核心类
-
InstanceInfo(截取部分代码,并做简单标注)
@ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)
@Serializer("com.netflix.discovery.converters.EntityBodyConverter")
@XStreamAlias("instance")
@JsonRootName("instance")
public class InstanceInfo
{
............
public static final int DEFAULT_PORT = 7001;
public static final int DEFAULT_SECURE_PORT = 7002;
public static final int DEFAULT_COUNTRY_ID = 1;
private volatile String instanceId; //实例id
private volatile String appName; //应用名
@Auto
private volatile String appGroupName; //应用所属分组
private volatile String ipAddr; //iP地址
private static final String SID_DEFAULT = "na";
@Deprecated
private volatile String sid = "na"; //被废弃的属性
private volatile int port = 7001; //端口号
private volatile int securePort = 7002; /https的端口
@Auto
private volatile String homePageUrl; //应用实例的首页URL
@Auto
private volatile String statusPageUrl; //应用实例的状态页URL
@Auto
private volatile String healthCheckUrl; //应用实例的健康检查URL
@Auto
private volatile String secureHealthCheckUrl; //应用实例的健康检查https URL
@Auto
private volatile String vipAddress; //虚拟ip地址
@Auto
private volatile String secureVipAddress; //https虚拟ip地址
@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 = 1;
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 = new ConcurrentHashMap();//应用实例元数据
@Auto
private volatile Long lastUpdatedTimestamp =Long.valueOf(System.currentTimeMillis());
@Auto
private volatile Long lastDirtyTimestamp = Long.valueOf(System.currentTimeMillis());
@Auto
private volatile ActionType actionType;
@Auto
private volatile String asgName;
..............
}
-
LeaseInfo(截取部分代码,并做简单标注)
public class LeaseInfo
{
//以下这些参数主要用于标识实例的心跳情况
public static final int DEFAULT_LEASE_RENEWAL_INTERVAL = 30;
public static final int DEFAULT_LEASE_DURATION = 90;
private int renewalIntervalInSecs = 30;
private int durationInSecs = 90;
private long registrationTimestamp;
private long lastRenewalTimestamp;
private long evictionTimestamp;
private long serviceUpTimestamp;
.................
}
-
ServiceInstance(截取部分代码,并做简单标方法
方法 | 说明 |
getServiceId() | 服务id |
getHost() | 实例的host |
getPort() | 实例的端口 |
isSecure() | 实例是否开启https |
getUri() | 实例的uri地址 |
getMetadata() | 实例的元数据信息 |
getScheme() | 实例的scheme |
这是一个Spring Cloud对service discovery的实例信息抽象接口,约定了服务发现的实例应用有哪些通用信息,由于适配了Zookeeper、Consul、Netflix Eureka等注册中心,故ServiceIntance定义更为抽象和通用。
-
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;
}
}
Eureka instance的overriddenstatus对于部署来说非常好用,比如red/black升级,将部分原服务先设置为OUT_OF_SERVICE,停止接收请求,即变为black,之后新部署的服务启动起来,即为red。如果新服务正常,就可以关闭旧服务了,假设新服务出现问题,则立马删除掉新服务,将原有服务的overriddenstatus删除掉,恢复UP,恢复接收流量。
2.核心操作
我们将一个普通的spring boot 应用注册到Eureka Server 或是从 Eureka Server 中获取服务列表时,主要就做了两件事:
- 在应用类中配置了 @EnableDiscoveryClient 注解。
- 在application.properties 中用 eureka-client.service-url.defaultZone 参数指定了注册中心的位置。
@EnableDiscoveryClient 的源码,具体如下:
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.client.discovery;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.client.discovery.EnableDiscoveryClientImportSelector;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
boolean autoRegister() default true;
}
从该注解的注释中我们可以知道,它主要用来开启 DiscoveryClient 的实例。通过搜索 DiscoveryClient ,我们可以发现有一个类和一个接口。通过梳理可以得到如下图所示的关系:
其中,1 是 Spring Cloud 的接口,它定义了用来发现服务的常用抽象方法,通过该接口可以有效的屏蔽服务治理的实现细节,所以使用 Spring Cloud 构建的微服务应用可以方便的切换不同服务治理框架,而不改动程序代码,只需要另外添加一些针对服务治理框架的配置即可。2 是对 1 接口的实现,从命名判断。它实现的是对 Eureka 发现服务的封装。所以 EurekaDiscoveryClient 依赖了 Netflix Eureka 的 EurekaClient 接口,EurekaClient 接口继承了 LookupService 接口,它们都是 Netflix 开源包中的内容,主要定义了针对 Eureka 的发现服务的抽象发放,而真正实现发现服务的则Netflix包中的 DiscoveryClient (5)类。
具体详见https://www.cnblogs.com/liao-xx/p/7326614.html.
七、Eureka参数调优及监控
(一)核心参数
1.Client端:基本参数+定时任务参数+http参数
(1)基本参数
参数名称 | 说明 | 默认值 |
eureka.client.enabled | 用于指示Eureka客户端已启用的标志 | true |
eureka.client.filter-only-up-instances | 是否过滤掉非up实例,默认为true | true |
eureka.client.availability-zones.* | 获取此实例所在区域的可用区域列表(在AWS数据中心中使用)。更改在运行时在registryFetchIntervalSeconds指定的下一个注册表获取周期生效。 | |
eureka.client.region | 获取此实例所在的区域(在AWS数据中心中使用)。 | us-east-1 |
eureka.client.register-with-eureka | 指示此实例是否应将其信息注册到eureka服务器以供其他服务发现,默认为false | true |
eureka.client.prefer-same-zone-eureka | 实例是否使用同一zone里的eureka服务器,默认为true,理想状态下,eureka客户端与服务端是在同一zone下 | true |
eureka.client.on-demand-update-status-change | 客户端的状态更新到远程服务器上,默认为true | true |
eureka.instance.prefer-ip-address | 是否优先使用服务实例的IP地址,相较于hostname | false |
eureka.instance.lease-expiration-duration-in-seconds | 指示eureka服务器在删除此实例之前收到最后一次心跳之后等待的时间(s) | 90 |
(2)定时任务参数
参数名称 | 说明 | 默认值 |
eureka.client.cache-refresh-executor-thread-pool-size | 缓存刷新线程池初始化线程数量 | 2 |
eureka.client.cache-refresh-executor-exponential-back-off-bound | 在发生一系列超时的情况下,它是重试延迟的最大乘数值。 | 10 |
eureka.client.heartbeat-executor-thread-pool-size | 心跳保持线程池初始化线程数,默认2个 | 2 |
eureka.client.heartbeat-executor-exponential-back-off-bound | 心跳超时重试延迟时间的最大乘数值,默认10 | 10 |
eureka.client.registry-fetch-interval-seconds | 指示从eureka服务器获取注册表信息的频率(s) | 30 |
eureka.client.eureka-service-url-poll-interval-seconds | 询问Eureka Server信息变化的时间间隔(s),默认为300秒 | 300 |
eureka.client.initial-instance-info-replication-interval-seconds | 初始化实例信息到Eureka服务端的间隔时间,(s) | 40 |
eureka.client.instance-info-replication-interval-seconds | 更新实例信息的变化到Eureka服务端的间隔时间,(s) | 30 |
eureka.instance.lease-renewal-interval-in-seconds | 该服务实例向注册中心发送心跳间隔(s) | 30 |
(3)http参数
参数名称 | 说明 | 默认值 |
eureka.client.eureka-server-connect-timeout-seconds | 连接Eureka Server 超时时间(s),默认5秒 | 5 |
eureka.client.eureka-server-read-timeout-seconds | 读取Eureka Server 超时时间(s),默认8秒 | 8 |
eureka.client.eureka-server-total-connections | 获取从eureka客户端到所有eureka服务器的连接总数,默认200个 | 200 |
eureka.client.eureka-server-total-connections-per-host | 获取从eureka客户端到eureka服务器主机允许的连接总数,默认50个 | 50 |
eureka.client.eureka-connection-idle-timeout-seconds | 连接到 Eureka Server 空闲连接的超时时间(s),默认30 | 30 |
2.Server端:基本参数+response cache参数+peer相关参数+http参数
(1)基本参数
参数名称 | 说明 | 默认值 |
eureka.server.enable-self-preservation | 启用自我保护机制,默认为true | true |
eureka.server.renewal-percent-threshold | 15分钟内续约服务的比例小于0.85,则开启自我保护机制,再此期间不会清除已注册的任何服务(即便是无效服务) | 0.85 |
eureka.instance.registry.expected-number-of-renews-per-min | 【Eureka Server 端属性】每分钟续约次数 | 1 |
eureka.server.renewal-threshold-update-interval-ms | 更新续约阈值的间隔(分钟),默认15分钟 | 15 |
eureka.server.eviction-interval-timer-in-ms | 清除无效服务实例的时间间隔(ms),默认1分钟 | 60000 |
(2)esponse cache参数
参数名称 | 说明 | 默认值 |
eureka.server.use-read-only-response-cache | 是否使用只读缓存策略 | true |
eureka.server.response-cache-update-interval-ms | 注册信息缓存更新间隔(s),默认30秒 | 30 |
eureka.server.response-cache-auto-expiration-in-seconds | 注册信息缓存有效时长(s),默认180秒 | 180 |
(3)peer相关参数
参数名称 | 说明 | 默认值 |
eureka.server.peer-eureka-nodes-update-interval-ms | 节点更新数据间隔时长(分钟) | 10 |
eureka.server.peer-eureka-status-refresh-time-interval-ms | 节点之间状态刷新间隔时长(ms) | 30000 |
(4)http参数
参数名称 | 说明 | 默认值 |
eureka.server.peer-node-connect-timeout-ms | 节点之间连接超时时长(ms) | 200 |
eureka.server.peer-node-read-timeout-ms | 节点之间数据读取超时时间(ms) | 200 |
eureka.server.peer-node-total-connections | 集群中节点连接总数 | 1000 |
eureka.server.peer-node-total-connections-per-host | 节点之间连接,单机最大连接数量 | 500 |
eureka.server.peer-node-connection-idle-timeout-seconds | 节点之间连接后,空闲时长(s) | 30 |
(二)参数调优
常见问题
- 为什么服务下线了,Eureka Server 接口返回的信息还会存在。
- 为什么服务上线了,Eureka Client 不能及时获取到。
- 为什么有时候会出现如下提示:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
解决之道
对于第一个问题,Eureka Server 并不是强一致的,因此 registry 中会存留过期的实例信息,这里头有几个原因:
- 应用实例异常挂掉,没能在挂掉之前告知 Eureka Server 要下线掉该服务实例信息。这个就需要依赖 Eureka Server 的 EvictionTask 去剔除。
- 应用实例下线时有告知 Eureka Server 下线,但是由于 Eureka Server 的 REST API 有 response cache,因此需要等待缓存过期才能更新。
- Eureka Server 由于开启并引入了 SELF PRESERVATION 模式,导致 registry 的信息不会因为过期而被剔除掉,直到退出 SELF PRESERVATION 模式。
针对 Client 下线没有通知 Eureka Server 的问题,可以调整 EvictionTask 的调度频率,比如下面配置将调度间隔从默认的 60 秒,调整为 5 秒:
eureka:
server:
# 指定 Eviction Task 定时任务的调度频率,用于剔除过期的实例,此处未使用默认频率,频率为:5/秒,默认为:60/秒
# 有效防止的问题是:应用实例异常挂掉,没能在挂掉之前告知Eureka server要下线掉该服务实例信息。这个就需要依赖Eureka server的EvictionTask去剔除。
eviction-interval-timer-in-ms: 5000
针对 response cache 的问题,可以根据情况考虑关闭 readOnlyCacheMap:
eureka:
server:
# 此处不开启缓存
use-read-only-response-cache: false
或者调整 readWriteCacheMap 的过期时间:
eureka:
server:
# 设置read Write CacheMap的expire After Write参数,指定写入多长时间后过期
# 有效防止的问题是:应用实例下线时有告知Eureka server下线,但是由于Eureka server的REST API有response cache,因此需要等待缓存过期才能更新
response-cache-auto-expiration-in-seconds: 60
针对 SELF PRESERVATION 的问题,在测试环境可以将 enable-self-preservation
设置为 false:
eureka:
server:
# 是否开启自我保护机制
#在分布式系统设计里头,通常需要对应用实例的存活进行健康检查,
#这里比较关键的问题就是要处理好网络偶尔抖动或短暂不可用时造成的误判。
#另外Eureka Server端与Client端之间如果出现网络分区问题,在极端情况下可能会使得Eureka Server清空部分服务的实例列表,这个将严重影响到Eureka server的 availibility属性。因此Eureka server引入了SELF PRESERVATION机制。
## Eureka client端与Server端之间有个租约,Client要定时发送心跳来维持这个租约,表示自己还存活着。 Eureka通过当前注册的实例数,去计算每分钟应该从应用实例接收到的心跳数,如果最近一分钟接收到的续约的次数小于指定阈值的话,则关闭租约失效剔除,禁止定时任务剔除失效的实例,从而保护注册信息。
# 此处关闭可以防止问题(测试环境可以设置为false):Eureka server由于开启并引入了SELF PRESERVATION模式,导致registry的信息不会因为过期而被剔除掉,直到退出SELF PRESERVATION模式才能剔除。
enable-self-preservation: false
如果关闭的话会提示:
RENEWALS ARE LESSER THAN THE THRESHOLD. THE SELF PRESERVATION MODE IS TURNED OFF.
THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.
或者:
THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.
针对新服务上线,Eureka Client 获取不及时的问题,在测试环境,可以适当提高 Client 端拉取 Server 注册信息的频率,例如下面将默认的30秒改为5秒:
eureka:
client:
# 针对新服务上线, Eureka client获取不及时的问题,在测试环境,可以适当提高Client端拉取Server注册信息的频率,默认:30秒
registry-fetch-interval-seconds: 5
在实际生产过程中,经常会有网络抖动等问题造成服务实例与 Eureka Server的心跳未能如期保持,但是服务实例本身是健康的,这个时候如果按照租约剔除机制剔除的话,会造成误判,如果大范围误判的话,可能会导致整个服务注册列表的大部分注册信息被删除,从而没有可用服务。Eureka 为了解决这个问题引入了 SELF PRESERVATION 机制,当最近一分钟接收到的续约的次数小于等于指定阈值的话,则关闭租约失效剔除,禁止定时任务剔除失效的实例,从而保护注册信息。
对于开发测试环境,开启这个机制有时候反而会影响系统的持续集成,因此可以通过如下参数关闭该机制。
eureka:
server:
enable-self-preservation: false
在生产环境中可以把 renewalPercentThreshold 及 leaseRenewalIntervalInSeconds 参数调小一点,进而提高触发 SELF PRESERVATION 机制的门槛,比如:
eureka:
server:
# 指定每分钟需要收到的续约次数的阈值,默认值就是:0.85
renewal-percent-threshold: 0.49
# 续约频率提高,默认:30
leaseRenewalIntervalInseconds: 10
八、总结
随着微服务架构的普及,服务发现与注册成为构建分布式系统的重要组成部分。本文围绕Eureka这一服务发现与注册中心展开,详细介绍了其设计理念、基本架构及高可用性方案。Eureka通过提供AP特性(高可用性)和Peer to Peer复制模式,确保服务实例能够在动态变化的环境中快速注册和发现。同时,通过区域亲和性和自我保护机制,Eureka提升了系统在网络不稳定情况下的鲁棒性。
此外,Eureka与其他技术(如Zookeeper、Consul等)相比,因其Java生态系统的兼容性及Netflix的背景而受到广泛应用。总结来看,Eureka不仅是服务治理的基础组件,更是现代微服务架构中的关键支撑,能够有效解决服务实例注册、续约及一致性问题,为分布式系统的高效运行提供了可靠保障。未来,随着云计算和容器化技术的发展,Eureka也将继续发挥其重要作用。
参考书籍、文献和资料
【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.
【2】徐进,叶志远,钟尊发,蔡波斯等. 重新定义Spring Cloud. 北京:机械工业出版社. 2018.
【3】服务发现组件Eureka_eureka 主要组件_chengqiuming的博客-CSDN博客.
【4】eureka分区的深入讲解,region和zone的使用_eureka region_Michaelwubo的博客-CSDN博客
【5】http://blog.springcloud.cn/sc/sc-tt-eureka/.
【6】https://www.cnblogs.com/liao-xx/p/7326614.html.
【7】聊聊eureka instance的overriddenstatus - 简书.
【8】https://www.cnblogs.com/liao-xx/p/7326614.html.
【9】Spring Cloud Eureka(四):Eureka 配置参数说明 - IT码客 - 博客园.