Eureka源码解读与优化实践(二)

307 阅读5分钟

前言

在前一篇文章里,描述了Eureka的启动流程和剔除任务,本篇将会继续介绍Eureka Server端的注册、集群同步的相关概念

Server端服务的注册

首先找到入口类ApplicationResource:

@Produces({"application/xml", "application/json"})
public class ApplicationResource {

    // 省略其他方法和属性
     
    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        
        // 1. 参数验证, 省略
        
        // 2. 数据中心相关的处理, 不做介绍
        DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
        if (dataCenterInfo instanceof UniqueIdentifier) {
            String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
            if (isBlank(dataCenterInfoId)) {
                boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                // 
                if (experimental) {
                    String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                    return Response.status(400).entity(entity).build();
                } else if (dataCenterInfo instanceof AmazonInfo) {
                    AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                    String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                    if (effectiveId == null) {
                         amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                    }
                } else {
                    logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                }
            }
        }

        // 完成注册功能的逻辑, 注意isReplication
        registry.register(info, "true".equals(isReplication));
        
        // 注册成功后的状态码
        return Response.status(204).build();  
    }
}

完成注册的核心逻辑为register方法,里面有个参数isReplication,该参数从header中获取,其含义为:

  • 当isReplication为null时:表示服务的注册请求(对应即为false)。
  • 当isReplication为true时:表示集群间的服务同步请求。

这里先看注册,所以isReplication为null,那么上面倒数第二行代码处调用的逻辑,参数实际为registry.register(info, false);

下面来看该方法的具体实现:

# PeerAwareInstanceRegistryImpl

@Override
public void register(final InstanceInfo info, final boolean isReplication) {
    // 租约的有效期, 默认时间为90秒
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }

    super.register(info, leaseDuration, isReplication);    // [1]
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);   // [2]
}

该方法的实现是在PeerAwareInstanceRegistryImpl类中,通过上面的源码,得出注册时, 主要完成两件事:

  • 完成注册功能。该功能是在其父类AbstractInstanceRegistry中实现的
  • 将刚刚注册的这个服务,同步到集群中的其他peer中去

先看注册功能,对应上面[1]处的代码:

# stractInstanceRegistry

public abstract class AbstractInstanceRegistry implements InstanceRegistry {

    // 省略其他的属性和方法、日志等代码
    
    // 存储注册过来的服务,可以理解为一级缓存,是实时的
    // 结构为:Map<appName, <instanceId, InstanceInfo>>
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
            
    // 该类完成了二级缓存和三级缓存的同步、读取等功能,会详细说明
    protected volatile ResponseCache responseCache;        

    // 新服务过来的注册, 就是通过该方法完成的, 这里的isReplication值前面已经说过了, 为false
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            read.lock();
            // 取出appName对应的服务集合
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            
            REGISTER.increment(isReplication);
            
            // 为空, 则创建一个, 这里其实就是为空的话, 初始化一下。
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                // 把新创建的空元素gNewMap, 存入registry中, 并把gNewMap赋值给gMap
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            
            // 获取注册前的服务列表
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
               
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    registrant = existingLease.getHolder();
                }
            } else {
                synchronized (lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        // Since the client wants to register it, increase the number of clients sending renews
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
            }
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            
            // 完成向registry中添加一条租约信息
            gMap.put(registrant.getId(), lease);
            
            // 往recentRegisteredQueue添加一条信息
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
            
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                registrant.getId());
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                // 修改服务的启动时间为当前时间
                lease.serviceUp();
            }
            
            // 注意这里的动作类型为add
            registrant.setActionType(ActionType.ADDED);
            
            // 往recentlyChangedQueue添加该租约信息
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            
            // 使appName对应的注册表readWriteCacheMap(二级缓存)中的服务,全都失效
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress()); 
        } finally {
            read.unlock();
        }
    }
}   

总结一下上面的这个注册方法:

  • 往注册表registry中添加一条注册信息
  • 使注册表readWriteCacheMap中,appName对应的租约信息失效。 这里就引申出一个问题,新注册服务后,readWriteCacheMap中如果存在旧的对应服务会被清除掉,那这个缓存是什么时候被放进去的?这个后面会详细说明。

上面聊了[1]处服务的注册,下面再聊聊[2]处的集群同步逻辑。

# PeerAwareInstanceRegistryImpl

private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
    Stopwatch tracer = action.getTimer().start();
    try {
        // 如果是新注册, 而不是向其他集群同步, 则数量原子加1
        if (isReplication) {
            numberOfReplicationsLastMin.increment();
        }
        
        // 这个很重要, 下面会细说
        if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
            return;
        }

        // 完成将刚刚注册完成的服务, 向集群中其他peer节点的同步功能
        for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
            // 得把自己排除掉
            if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                continue;
            }
            // 这里的action是个枚举,上一步传过来的为注册类型Register,
            replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
        }
    } finally {
        tracer.stop();
    }
}

来分析下上面代码的第二个if。该if有两个条件,第一个条件表示存不存在对等节点peer,第二个参数isReplication,在前面说过其含义,这里是新注册的功能,所以这里的值是false。所以当在集群环境下,又是新册的时候,那么这个if条件不满足,所以会执行下面的for循环,向其他的peer同步。

下面再看replicateInstanceActionsToPeers(..)方法:

@Singleton
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {

    private void replicateInstanceActionsToPeers(Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
        try {
            InstanceInfo infoFromRegistry;
            CurrentRequestVersion.set(Version.V2);
            switch (action) {
                case Cancel:
                    node.cancel(appName, id);
                    break;
                case Heartbeat:
                    InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                    break;
                    
                // 向其他peer注册
                case Register:
                    node.register(info);
                    break;
                case StatusUpdate:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                    break;
                case DeleteStatusOverride:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.deleteStatusOverride(appName, id, infoFromRegistry);
                    break;
            }
        } catch (Throwable t) {
            logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
        } finally {
            CurrentRequestVersion.remove();
        }
    }
}
# PeerEurekaNode
public void register(final InstanceInfo info) throws Exception {
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
            taskId("register", info),
            // 这里最后一个参数即前面说的isReplication,传入了true
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}

上面再创建实例复制任务InstanceReplicationTask时候,传入的最后一个参数isReplication为true,所在在下一次向其他peer再次进行同步的时候,前面说过得两个if条件,第二个参数变成了true,所以会结束掉,不会执行下面的for循环。从这里我们可以得出结论,Eureka在超过两个节点之后,最好在配置里,将所有的节点都配置上,否则会出现数据不同的情况。这里补充一下,Eureka是基于Http完成节点的注册、续约等功能的。

总结

本篇主要介绍了Server端服务注册的过程,在该过程中,主要完成了三件事:

  • 完成注册功能,添加到registry中。
  • 清空对应服务的缓存。
  • 向其他peer节点同步,并且只会同步一次,由参数isReplication来控制,true表示同步,false表示注册。

文章最后简单说下一peer的概念:

# 7900.yml
eureka:
  client:
    service-url:
      defaultZone: http://euk1.com:7900/eureka/,http://euk1.com:7901/eureka/,http://euk1.com:7902/eureka/

在7900.yml这个配置里, 7901和7902就是7900的peer。也就是说,出现在它的配置列表里的Eureka节点,就是他的peer