【深入理解SpringCloud微服务】深入理解Eureka核心原理

深入理解Eureka核心原理

Eureka整体设计

Eureka是一个经典的注册中心,通过http接收客户端的服务发现和服务注册请求,使用内存注册表保存客户端注册上来的实例信息。

Eureka服务端接收的是http请求,通过ApplicationResource 接收服务注册请求,通过ApplicationsResource 接收服务发现请求,这两个类相当于Spring MVC中的Controller ,Eureka使用的不是Spring MVC,而是Jersey,我们直接把他们当成Controller即可。

然后Eureka用一个内存实例注册表PeerAwareInstanceRegistry保存服务提供者注册上来的实例信息,当ApplicationResource接收到服务注册请求时,会把服务实例信息存入PeerAwareInstanceRegistry;当ApplicationsResource接收到服务发现请求时,会从PeerAwareInstanceRegistry拉取服务实例信息返回给客户端

java 复制代码
public class ApplicationsResource {

	private final PeerAwareInstanceRegistry registry;

	...
}
java 复制代码
public class ApplicationResource {

	private final PeerAwareInstanceRegistry registry;

	...
}

Eureka服务端启动

Eureka服务端启动时会初始化PeerAwareInstanceRegistry接口的实现类以及其他核心类,除了初始化PeerAwareInstanceRegistry等一些核心类之外,还会做两件事:

  1. 从集群中的其他Eureka拉取服务实例列表,注册到自己本地的服务注册表
  2. 开启服务剔除定时任务,定时扫描超过一定期限没有续约的服务实例,把它剔除出内存注册表

初始化PeerAwareInstanceRegistry的代码在EurekaServerAutoConfiguration中,通过@Bean往Spring容器注册一个InstanceRegistry对象,这个InstanceRegistry就是peerAwareInstanceRegistry的实现类。

java 复制代码
	@Bean
	public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
			ServerCodecs serverCodecs) {
		...
		return new InstanceRegistry(...);
	}

EurekaServerAutoConfiguration还会通过@Import注解导入一个EurekaServerInitializerConfiguration,这个EurekaServerInitializerConfiguration的start()方法会触发集群同步和启动服务剔除定时任务。

java 复制代码
	@Override
	public void start() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					// EurekaServerAutoConfiguration导入的EurekaServerBootstrap
					// 启动Eureka服务
					eurekaServerBootstrap.contextInitialized(
							EurekaServerInitializerConfiguration.this.servletContext);
					...
				}
				catch (...) {...}
			}
		}).start();
	}
java 复制代码
	public void contextInitialized(ServletContext context) {
		try {
			//初始化Eureka运行环境
			initEurekaEnvironment();
			//初始化Eureka服务上下文
			initEurekaServerContext();

			...
		}
		catch (...) {...}
	}

重点是initEurekaServerContext()方法:

java 复制代码
protected void initEurekaServerContext() throws Exception {
		...
		
		// 集群同步(从集群中的其他Eureka实例拉取服务实例列表)
		int registryCount = this.registry.syncUp();
		// 启动服务提测定时任务
		this.registry.openForTraffic(this.applicationInfoManager, registryCount);

		...
	}

Eureka三级缓存

Eureka处理服务发现请求时,其实并不是直接读取内存注册表的,而是读的缓存。Eureka除了内存注册表以外,还有两个缓存,一个是读写缓存readWriteCacheMap,一个是只读缓存readOnlyCacheMap。内存注册表、readWriteCacheMap、readOnlyCacheMap三个组成了Eureka的三级缓存。其中readWriteCacheMap和readOnlyCacheMap被包装在一个ResponseCache对象中。整个三级缓存的结果就是这样:

java 复制代码
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
	...
}
java 复制代码
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    ...
    protected volatile ResponseCache responseCache;
    ...

当ApplicationsResource接收到服务发现请求时:

  1. 先从只读缓存中取
  2. 如果只读缓存中没有,则从读写缓存获取并且回写只读缓存
  3. 如果读写缓存中也没有,则从内存注册表中获取并回写到读写缓存。

Eureka会开启一个定时任务,每隔30s从读写缓存同步数据到只读缓存。

ResponseCacheImpl#getValue:

java 复制代码
    Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {
            if (useReadOnlyCache) {
            	// 从只读缓存取
                final Value currentPayload = readOnlyCacheMap.get(key);
                if (currentPayload != null) {
                    payload = currentPayload;
                } else {
                	// 只读缓存没有,则从读写缓存取,回写只读缓存
                    payload = readWriteCacheMap.get(key);
                    readOnlyCacheMap.put(key, payload);
                }
            } else {
                payload = readWriteCacheMap.get(key);
            }
        } catch (...) {...}
        return payload;
    }

readWriteCacheMap的类型是LoadingCache,LoadingCache是Guava库提供的一个本地缓存实现。当LoadingCache缓存缺失时,LoadingCache会触发CacheLoader的load方法,加载数据到缓存中,此时就会从内存注册表中加载数据到readWriteCacheMap中。关于LoadingCache的使用、作用、原理等知识,可以参考讲解Guava缓存相关的资料。

当ApplicationResource接收到服务注册请求时,会把服务实例信息写入内存注册表,并失效掉读写缓存,然后把新注册上来的实例信息异步同步到集群中的其他Eureka节点。

PeerAwareInstanceRegistryImpl#register

java 复制代码
    @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        ...
        super.register(info, leaseDuration, isReplication);
        // 同步到集群中的其他Eureka节点
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }

AbstractInstanceRegistry#register:

java 复制代码
    public void register(InstanceInfo registrant, ...) {
        try {
            ...
            // 失效读写缓存
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            ...
        } finally {
            ...
        }
    }

    private void invalidateCache(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {
        // invalidate cache
        responseCache.invalidate(appName, vipAddress, secureVipAddress);
    }

Eureka客户端启动

Eureka的客户端启动时会创建一个DiscoveryClient对象,它是Eureka的客户端对象,它会创建两个定时任务,一个异步延时任务。

两个定时任务:

  1. 定时拉取服务实例列表(服务发现
  2. 定时发送心跳(服务续约

一个延时任务:服务注册

DiscoveryClient的构造方法:

java 复制代码
    @Inject
    DiscoveryClient(...) {
        ...
        initScheduledTasks();
        ...
    }

DiscoveryClient#initScheduledTasks

java 复制代码
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            ...
            // 开启服务发现定时任务
            scheduler.schedule(
                    new TimedSupervisorTask(
                            ...
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {
            ...
            // 开启服务续约(定时发送心跳)定时任务
            scheduler.schedule(
                    new TimedSupervisorTask(
                    		...
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            
            instanceInfoReplicator = new InstanceInfoReplicator(...);
            ...
			// 服务注册
            instanceInfoReplicator.start(...);
        } else {...}
    }

它们都是通过Jersey客户端向Eureka服务端发起http请求。

其中服务发现的定时任务在首次拉取是会全量拉取,后续会进行增量拉取。增量拉取返回的服务实例列表会合并到Eureka客户端的本地缓存中,然后根据本地缓存的服务实例列表计算一个hashCode,与Eureka服务端返回的hashCode进行比较,如果不一致,还要再进行一次全量拉取。

以上就是Eureka全部的核心原理,下面放一张源码图,对源码有兴趣的可以跟一跟,没有兴趣的可以直接忽略。

相关推荐
_风中无我。1 小时前
Spring的过滤器获取请求体中JSON参数,同时解决Controller获取不到请求体参数的问题。
java·spring·json
茶本无香1 小时前
kafka+spring cloud stream 发送接收消息
spring cloud·kafka·java-zookeeper
大脑经常闹风暴@小猿1 小时前
1.1 go环境搭建及基本使用
开发语言·后端·golang
菜鸟一枚在这1 小时前
深入剖析抽象工厂模式:设计模式中的架构利器
设计模式·架构·抽象工厂模式
尚学教辅学习资料2 小时前
基于SpringBoot的美食分享平台+LW示例参考
spring boot·后端·美食
Swift社区2 小时前
【微服务优化】ELK日志聚合与查询性能提升实战指南
spring·elk·微服务·云原生·架构
丰年稻香3 小时前
系统架构设计师备考策略
架构·系统架构设计师
Vitalia4 小时前
从零开始学 Rust:基本概念——变量、数据类型、函数、控制流
开发语言·后端·rust
陌殇殇6 小时前
002 SpringCloudAlibaba整合 - Feign远程调用、Loadbalancer负载均衡
java·spring cloud·微服务
猎人everest7 小时前
SpringBoot应用开发入门
java·spring boot·后端