2.2 Spring Cloud Eureka 进阶
上面一节介绍了服务发现以及Eureka的由来,同时展示了Eureka的最基础的搭建以及使用,包括Eureka Server和Eureka Client。还介绍了Eureka对于非Java应用提供的REST API。本节将介绍Eureka的进阶内容,包括源码的分析、设计思想以及参数调优。
建议各位读者打开 IDE 工具一起阅读源码。
2.2.1 Eureka Client源码解读
作为一个Eureka Client,它的主要功能有以下两点:
- 启动时向Eureka Server注册自己。
- 当服务的消费者需要提供服务提供者的服务地址信息时,需要从Eureka Server获取。
在Eureka Client启动类中,我们需要添加一个注解@EnableDiscoveryClient
来开启Eureka Client的功能。
@SpringBootApplication
@EnableDiscoveryClient
public class Ch21EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(Ch21EurekaClientApplication.class, args);
}
}
我们打开这个注解看一下。
/**
* Annotation to enable a DiscoveryClient implementation.
* @author Spencer Gibb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {
/**
* If true, the ServiceRegistry will automatically register the local server.
* @return - {@code true} if you want to automatically register.
*/
boolean autoRegister() default true;
}
这里文档注释:Annotation to enable a DiscoveryClient implementation.
,告诉我们这个注解是用来开启DiscoveryClient
的,那我们打开看一下DiscoveryClient
做了什么。
org.springframework.cloud.client.discovery.DiscoveryClient
如下:
/**
* Represents read operations commonly available to discovery services such as Netflix
* Eureka or consul.io.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public interface DiscoveryClient extends Ordered {
/**
* Default order of the discovery client.
*/
int DEFAULT_ORDER = 0;
/**
* A human-readable description of the implementation, used in HealthIndicator.
* @return The description.
*/
String description();
/**
* Gets all ServiceInstances associated with a particular serviceId.
* @param serviceId The serviceId to query.
* @return A List of ServiceInstance.
*/
List<ServiceInstance> getInstances(String serviceId);
/**
* @return All known service IDs.
*/
List<String> getServices();
/**
* Default implementation for getting order of discovery clients.
* @return order
*/
@Override
default int getOrder() {
return DEFAULT_ORDER;
}
}
DiscoveryClient
这个接口的文档注释很清楚的写明了该接口是用于服务发现的读取操作,比如说Netflix。
这个接口下总共有三个方法:
-
description()
:获取服务的描述信息。 -
getInstances(String serviceId)
:通过serviceId
获取所有的服务实例 -
getServices()
:获取所有的serviceId
这个接口貌似到顶了,再往上走是一个Spring的Ordered
接口,这个Ordered
主要是用过排序的,主要用来控制优先级,这里和我们想要看到的服务发现关系不大。我们可以看一下DiscoveryClient
这个接口的实现类,会发现一个我们非常熟悉的EurekaDiscoveryClient
,我们去看一下EurekaDiscoveryClient
做了什么操作:
/**
* A {@link DiscoveryClient} implementation for Eureka.
*
* @author Spencer Gibb
* @author Tim Ysewyn
*/
public class EurekaDiscoveryClient implements DiscoveryClient {
/**
* Client description {@link String}.
*/
public static final String DESCRIPTION = "Spring Cloud Eureka Discovery Client";
private final EurekaClient eurekaClient;
private final EurekaClientConfig clientConfig;
@Deprecated
public EurekaDiscoveryClient(EurekaInstanceConfig config, EurekaClient eurekaClient) {
this(eurekaClient, eurekaClient.getEurekaClientConfig());
}
public EurekaDiscoveryClient(EurekaClient eurekaClient,
EurekaClientConfig clientConfig) {
this.clientConfig = clientConfig;
this.eurekaClient = eurekaClient;
}
@Override
public String description() {
return DESCRIPTION;
}
@Override
public List<ServiceInstance> getInstances(String serviceId) {
List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,
false);
List<ServiceInstance> instances = new ArrayList<>();
for (InstanceInfo info : infos) {
instances.add(new EurekaServiceInstance(info));
}
return instances;
}
@Override
public List<String> getServices() {
Applications applications = this.eurekaClient.getApplications();
if (applications == null) {
return Collections.emptyList();
}
List<Application> registered = applications.getRegisteredApplications();
List<String> names = new ArrayList<>();
for (Application app : registered) {
if (app.getInstances().isEmpty()) {
continue;
}
names.add(app.getName().toLowerCase());
}
return names;
}
@Override
public int getOrder() {
return clientConfig instanceof Ordered ? ((Ordered) clientConfig).getOrder()
: DiscoveryClient.DEFAULT_ORDER;
}
// 省略...
}
这里可以看到,在getServices()
获取服务Id的方法中和getInstances(String serviceId)
获取服务实例,都依赖了EurekaClient
接口,调用了this.eurekaClient
,我们继续追踪EurekaClient
:
@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService
这里继承了LookupService
,继续追踪LookupService
:
/**
* Lookup service for finding active instances.
*
* @author Karthik Ranganathan, Greg Kim.
* @param <T> for backward compatibility
*/
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);
}
从注释中:Lookup service for finding active instances.
,我们可以知道,这是一个查找活动实例的方法,并提供了一些查找方法。
回到上面的EurekaClient
,在EurekaDiscoveryClient
使用的eurekaClient
具体是谁的实现类呢,我们继续看一下EurekaClient
的实现类DiscoveryClient
:
/**
* The class that is instrumental for interactions with <tt>Eureka Server</tt>.
*
* <p>
* <tt>Eureka Client</tt> is responsible for a) <em>Registering</em> the
* instance with <tt>Eureka Server</tt> b) <em>Renewal</em>of the lease with
* <tt>Eureka Server</tt> c) <em>Cancellation</em> of the lease from
* <tt>Eureka Server</tt> during shutdown
* <p>
* d) <em>Querying</em> the list of services/instances registered with
* <tt>Eureka Server</tt>
* <p>
*
* <p>
* <tt>Eureka Client</tt> needs a configured list of <tt>Eureka Server</tt>
* {@link java.net.URL}s to talk to.These {@link java.net.URL}s are typically amazon elastic eips
* which do not change. All of the functions defined above fail-over to other
* {@link java.net.URL}s specified in the list in the case of failure.
* </p>
*
* @author Karthik Ranganathan, Greg Kim
* @author Spencer Gibb
*
*/
@Singleton
public class DiscoveryClient implements EurekaClient
这段注释的大致含义为:DiscoveryClient
是用于和Eureka Server进行交互的类,主要用户Eureka Client向Eureka注册,向 Eureka Server租约续期,服务关闭,取消租约续期,获取服务实例列表,Eureka client需要配置 Eureka Server的url列表。
看到这里,我们基本可以确定,真正的服务发现的包是在Netflix包中的com.netflix.discovery.DiscoveryClient
,我们整理一下这几个接口/类的关系图,如图2-5:
我们接着详细看一下DiscoveryClient
是如何实现服务注册和发现等功能的。首先看一下DiscoveryClient
的构造方法
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
// 省略...
try {
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 省略...
initScheduledTasks();
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// 省略...
}
可以看到这个构造方法里面,主要做了以下两件事:
-
创建了scheduler定时任务的线程池,heartbeatExecutor心跳检查线程池(服务续约),cacheRefreshExecutor(服务获取)
-
然后
initScheduledTasks()
开启上面三个线程池,往上面3个线程池分别添加相应任务。
再看一下initScheduledTasks()
里面具体做了什么:
/**
* Initializes all scheduled tasks.
*/
private void initScheduledTasks() {
// 省略...
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
// 省略...
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
// 省略...
}
这里创建了一个instanceInfoReplicator
(Runnable任务),然后调用InstanceInfoReplicator.start
方法,把这个任务放进上面scheduler
定时任务线程池(服务注册并更新)。
- 服务注册
上面说了,initScheduledTasks()
方法中调用了InstanceInfoReplicator.start()
方法,InstanceInfoReplicator.run()
方法代码如下:
public void run() {
try {
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
这里调用discoveryClient.register()
,DiscoveryClient
的 register
方法 代码如下:
/**
* Register with the eureka service by making the appropriate REST call.
*/
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
随后又经过一系列的调用,最终调用到了AbstractJerseyEurekaHttpClient.register(InstanceInfo info)
:
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
可以看到最终通过http rest请求Eureka Server,把应用自身的InstanceInfo实例注册给Eureka Server,具体注册流程如图2-6:
- 服务续约
我们回到最开始的DiscoveryClient
中的定时方法initScheduledTasks()
,继续看另一个定时任务:
// 省略...
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// 省略...
heartbeat
:使用心跳机制实现服务续约,即每间隔多少秒去请求一下注册中心证明服务还在线,防止被剔除。renewalIntervalInSecs
:就是心跳时间间隔 对应的配置。
这里启动了一个HeartbeatThread()
的线程,点过去看一下:
// 省略...
/**
* The heartbeat task that renews the lease in the given intervals.
*/
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
// 省略...
/**
* Renew with the eureka service by making the appropriate REST call
*/
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
REREGISTER_COUNTER.increment();
logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
long timestamp = instanceInfo.setIsDirtyWithTime();
boolean success = register();
if (success) {
instanceInfo.unsetIsDirty(timestamp);
}
return success;
}
return httpResponse.getStatusCode() == Status.OK.getStatusCode();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
return false;
}
}
// 省略...
这里不难看出,服务续约和服务注册的流程基本是一致的,发送心跳,请求Eureka Server,如果接口返回值为404,就是说服务不存在,那么重新走注册流程。
整体流程图如图2-7:
- 服务下线
在服务shutdown的时候,需要及时通知Eureka Server将自己DELETE,避免客户端调用已经下线的服务。
// 省略...
/**
* Shuts down Eureka Client. Also sends a deregistration request to the
* eureka server.
*/
@PreDestroy
@Override
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (statusChangeListener != null && applicationInfoManager != null) {
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null
&& clientConfig.shouldRegisterWithEureka()
&& clientConfig.shouldUnregisterOnShutdown()) {
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
logger.info("Completed shut down of DiscoveryClient");
}
}
// 省略...
private void cancelScheduledTasks() {
if (instanceInfoReplicator != null) {
instanceInfoReplicator.stop();
}
if (heartbeatExecutor != null) {
heartbeatExecutor.shutdownNow();
}
if (cacheRefreshExecutor != null) {
cacheRefreshExecutor.shutdownNow();
}
if (scheduler != null) {
scheduler.shutdownNow();
}
}
// 省略...
/**
* unregister w/ the eureka service.
*/
void unregister() {
// It can be null if shouldRegisterWithEureka == false
if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
try {
logger.info("Unregistering ...");
EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
logger.info(PREFIX + "{} - deregister status: {}", appPathIdentifier, httpResponse.getStatusCode());
} catch (Exception e) {
logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e);
}
}
}
@PreDestroy
:会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次。
先关闭各种定时任务,然后向Eureka Server发送服务下线通知。流程图如图2-8:
2.2.2 Eureka设计理念
作为一个服务中心,主要需解决下面几个问题:
-
服务如何注册到服务中心
-
服务如何从服务中心剔除
-
服务信息一致性的问题
关于前两个问题,其实就是在服务启动的时候,通过REST API的方式向Eureka Server发送一个请求,将该应用实例的信息注册在Eureka Server上,当应用实例在关闭的时候,通过钩子函数或其他生命周期方法已REST API的方式向Eureka Server发送一个请求,将该应用实例的信息在Eureka Server上进行注销操作。另外为了防止Eureka Client意外挂掉等情况,而没有及时删除自身信息,要求Client端定时续约,发送心跳操作证明自己还活着,是健康可调用的。如果超过一段时间Client端没有进行续约操作,Server端是会主动删除当前Client端的服务信息。
服务中心不可能是单点应用,本身肯定会是集群应用,从而引发了第三个问题,我们在前面提到过Eureka是保证AP的,这里我们详细介绍一下什么是CAP定理。
CAP定理(CAP theorem)又被称作布鲁尔定理(Brewer’s theorem),是回加州大学伯克得分校的计算机科学家埃里克·布鲁尔(Eric Brewer)在2000年的ACM PODC上提出的一个猜想。2002 年,麻省理工学院的赛斯·吉尔伯特(Seth Gilbert)和南希·林奇(Nancy Lynch)发表了布鲁尔猜想的证明,使之成为分布式计算领域公认的一个定理。
- 一致性(Consistency) - 对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。
A read is guaranteed to return the most recent write for a given client.
- 可用性(Availability) - 非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
A non-failing node will return a reasonable response within a reasonable amount of time (no error or timeout).
- 分区容错性(Partition Tolerance) - 当出现网络分区后,系统能够继续履行职责。
The system will continue to function when network partitions occur.
对于分布式系统来说,网络条件一般来讲是不可控的,出现网络分区是不可避免的,因此,系统必须具备分区容错性。在这个前提下,分布式系统的设计原则只能在AP或者CP之间选择。注意,这里切记不能理解为CAP是三选二,因为P是客观存在不可绕过的事实,所以P是必选项。
而Eureka的设计者认为,在云端,在大规模部署的环境下,失败是不可避免的,那么Eureka就不能回避这个问题,需要拥抱这个问题,就需要Eureka在网络分区的环境下还要能正常提供服务。因此Eureka选择满足Availability这个特性。Peter Kelley在《Eureka! Why You Shouldn’t Use ZooKeeper for Service Discovery》(https://medium.com/knerd/eureka-why-you-shouldnt-use-zookeeper-for-service-discovery-4932c5c7e764 )中指出,在实际生产实践中,服务注册发现中心保留可用及过期数据总比丢失掉可用数据要好。这样的话,应用实例的注册信息在集群的所有节点中并不是保持强一致的,这就需要客户端能够支持负载均衡和重试。恰巧,在Netflix中,由Ribbon提供了此功能。
Eureka Server中服务的注册信息,是通过Peer to Peer的方式来进行复制和传输的,在这种方式中,副本没有主从之分,所有的副本都是对等关系,任何副本都可以接收写操作,所有的副本之间都可以进行数据更新。
但是这种Peer to Peer的方式又引发了一个新的问题,各个副本之间进行数据同步的时候冲突处理如何解决?
针对这个问题,Eureka采用如下两种方式来解决:
- lastDirtyTimesStamp标记
这种问题常规解决方案是在需要同步的注册信息中加上版本号,在同步的时候只需要判断版本号的大小就可以了,Eureka本身并没有使用版本号,而是使用了lastDirtyTimestamp
时间戳属性,在服务注册时,Eureka Server同步服务注册信息,通过lastDirtyTimestamp
判断注册信息是否覆盖。
- 定时心跳
复杂的网络环境中,仅凭服务注册时的信息同步是远远不够的,这时Eureka Server服务同步最终一致性最关键的的方式是Heartbeat
,因为Heartbeat
会周期性执行,通过它可以判断当前Eureka Server是否存在对应的心跳对应的服务实例,同时比较对应的lastDirtyTimestamp
,当条件不满足时,会发出404的状态码响应,应用实例需重新进行注册操作。
2.2.3 参数调优
Eureka Server和Eureka Client可配置的参数加起来有一百多项配置,本书仅介绍其中使用频率较高的介绍,具体的其余配置可以参考源码:org.springframework.cloud.netflix.eureka.EurekaClientConfigBean
以及org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean
。
对于初学者来讲,一般会有几个问题:
- 为什么服务明明下线了,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的注销服务,从而Eureka Server并不知道服务实例已经下线。
对于Client端下线而没有通知Server端的情况,我们可以降低EvictionTask的执行间隔,默认时间间隔为60s,可以修改为10s(生产环境慎重调整,会增加Eureka Server端自身服务压力)
eureka.server.eviction-interval-timer-in-ms=10000
服务实例已经下线,并且已经通知Eureka Server,但是由于Eureka Server本身的缓存机制,需要等待缓存过期才会下线服务实例。
对于此问题,可以选择考虑关闭Eureka Server本身缓存readOnlyCacheMap
(生产环境不推荐)
eureka.server.use-read-only-response-cache=false
获取减少Eureka Server本身缓存时间,默认180s(生产环境慎重调整)
eureka.server.response-cache-auto-expiration-in-seconds=60
- 一个服务启动,等了很久都没有在Eureka Server上看到注册信息,这里可以适当降低Client端从Server端拉取注册信息的时间间隔,例如从默认的30s改为5s:
eureka.client.registry-fetch-interval-seconds=5
- Eureka Server本身的自我保护模式,导致服务注册信息并不会因为过期而被清除掉,本身是一种安全保障措施,可在配置中关闭(生产环境不推荐)。
eureka.server.enable-self-preservation=false
关于Eureka Server本身的自我保护模式,虽然很不便与开发测试,但是在生产环境还是十分重要的,主要应对与整体网络波动,导致服务实例端与Eureka Server的心跳未能如期保持,这时服务实例本身是健康的,如果在Eureka Server中剔除下线,会造成误判,如果大范围误判的话,可能会造成整个网络服务大量下线,整体服务瘫痪。Eureka为了解决这个问题,引入了自我保护模式,当最近一分钟接受到的续约小于等于指定阀值的时候,触发自我保护模式,其实在生产环境,整体下线一个服务是一件非常非常罕见的事情,如果真的需要整体下线一个服务,完全可以通过其他方式来操作,比如调用Eureka的REST API进行单个服务的下线。
2.2.4 指标统计
Eureka内置了基于Netflix Servo的指标统计,具体可见com.netflix.eureka.util.EurekaMonitors
的枚举类,其统计指标如下:
名称 | 说明 |
---|---|
renewCounter | 自启动以来看到的总续约数 |
cancelCounter | 自启动以来看到的总注销数 |
getAllCacheMissCounter | 自启动以来看到的注册表查询总数 |
getAllCacheMissDeltaCounter | 启动以来使用delta查询register的总数 |
getAllWithRemoteRegionCacheMissCounter | 启动以来使用remote region查询register的总数 |
getAllWithRemoteRegionCacheMissDeltaCounter | 自启动以来使用remote region以及delta查询register的总数 |
getAllDeltaCounter | 启动以来deltas的总数 |
getAllDeltaWithRemoteRegionCounter | 启动以来deltas和remote regions的总数 |
getAllCounter | 启动以来registry的总数 |
getAllWithRemoteRegionCounter | 启动以来传递remote regions参数查询register的总数 |
getApplicationCounter | 自启动以来查询的服务实例的总数 |
registerCounter | 自启动以来看到的register总数 |
expiredCounter | 自启动以来已过期的租约总数 |
statusUpdateCounter | 自启动以来的状态更新总数 |
statusOverrideDeleteCounter | 状态覆盖移除的数目 |
cancelNotFoundCounter | 自启动以来,请求cancel找不到对应实例的数量 |
renewNotFoundexpiredCounter | 自启动以来,请求renew找不到对应实例的数量 |
numOfRejectedReplications | 由于队列已满而拒绝的复制数 |
numOfFailedReplications | 复制失败的数量-可能来自超时 |
numOfRateLimitedRequests | rate limiter丢弃的请求数 |
numOfRateLimitedRequestCandidates | 如果激活rate limiter的节流,将丢弃的请求数 |
numOfRateLimitedFullFetchRequests | rate limiter丢弃的完整注册表获取请求的数目 |
numOfRateLimitedFullFetchRequestCandidates | 如果激活rate limiter节流,将丢弃的完整注册表获取请求的数量 |
2.2.5 Zone与Region
伴随着业务的发展,线上的用户持续增长,用户遍布全国各地,我们的微服务需要全国多地多机房部署,这时,我们肯定希望用户可以优先使用就近的服务,而不是北京的用户使用的是上海的机房的服务,只有当就近的服务不可用的时候才会调用远程的服务,这样不会浪费大量的时间在信息的传输上。异地机房网络连通情况更加不可控,心跳和服务注册都有可能出现问题,
Eureka提供了Zone和Region来进行区分,这两个概念均来自于亚马逊的AWS:
-
region:可以简单理解为地理上的分区,比如亚洲地区,或者华北地区,再或者北京等等,没有具体大小的限制。根据项目具体的情况,可以自行合理划分region。
-
Zone:可以简单理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2两个zone。
具体架构如下图2-9:
在相同的Region下面,Zone可以看做是一个个的机房,各机房相对独立,当Region下面一个Zone不可用时,还有其他的Zone可用,由于在不同的Region下面,资源是不会复制的,Eureka的高可用主要就在Region下面的Zone。