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

313 阅读5分钟

前言

上一篇介绍了Eureka Server端服务的注册功能,本篇将继续介绍Eureka的下线、续约相关的源码和优化点。

Server端服务的续约

当客户端通过心跳检测来续约时,就会触发Eureka Server端去执行服务续约操作。

实例的下线和续约的实现类,主要都在实例策略类InstanceRegistry中,Controller:

public class InstanceRegistry extends PeerAwareInstanceRegistryImpl {

    @Override
    public boolean renew(final String appName, final String serverId, boolean isReplication) {
        // 所有已经注册的服务列表
        List<Application> applications = getSortedApplications();
        
        // 遍历
        for (Application input : applications) {
            // 先匹配服务名
            // 这里很好理解, 一个服务可能部署了多个节点, 但appName是一样的
            // 区别在于instanceId, 也就是IP+Port是不同的
            if (input.getName().equals(appName)) {
                InstanceInfo instance = null;
                // 再匹配instanceId, 并存到instance变量中
                for (InstanceInfo info : input.getInstances()) {
                        if (info.getId().equals(serverId)) {
                            instance = info;
                            break;
                        }
                }
                // 触发一个事件, instance就是上面if里设置的
                publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
                                instance, isReplication));
                break;
            }
        }
        
        // 执行续约的逻辑, isReplication这里传入的值为false
        return super.renew(appName, serverId, isReplication);
    }
}

接着看最后一行代码,是续约的主要代码:

# PeerAwareInstanceRegistryImpl

public boolean renew(final String appName, final String id, final boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

其中处理的逻辑在if的 super.renew(..) 方法中,继续跟进:

public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    public boolean renew(String appName, String id, boolean isReplication) {
        RENEW.increment(isReplication);
        
        // 从registry注册表中获取appName所有注册了的服务
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = gMap.get(id);
        }
        if (leaseToRenew == null) {
            RENEW_NOT_FOUND.increment(isReplication);
            return false;
        } else {
            // 获取续约的实例相关信息, 并进行状态的检查
            InstanceInfo instanceInfo = leaseToRenew.getHolder();
            if (instanceInfo != null) {
                InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                        instanceInfo, leaseToRenew, isReplication);
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
                }
            }
            renewsLastMin.increment();
            
            // 续约, 仅仅将租约的最后更新时间 "lastUpdateTimestamp" 属性的值更新为当前时间
            leaseToRenew.renew();
            return true;
        }
    }
}

// 更新续租时间为当前日期
public class Lease<T> {
    // 其他代码, 省略
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis() + duration;
    }
}

在上面更新租约的信息中,if代码块里的replicateToPeers(..)方法,在第二篇已经说过,这里直接略过了。

简单总结一下上面的代码:

  • isReplication=false,所以会续约信息会一直同步到其他的所有peer
  • 续约主要操作,其实就是将租约的最后更新时间更新为当前时间(当然事件通知不是主要的)

Server端服务的下线

当客户端需要停止服务时,可以手动下线,即通过调用Rest请求,直接要求Eureka Server对该服务执行下线操作。
主要逻辑如下:

public class InstanceRegistry extends PeerAwareInstanceRegistryImpl
		implements ApplicationContextAware {
    // client主动下线
    @Override
    public boolean cancel(String appName, String serverId, boolean isReplication) {
        // 触发一个事件, 不细说
        handleCancelation(appName, serverId, isReplication);
        return super.cancel(appName, serverId, isReplication);
    }
}

public abstract class AbstractInstanceRegistry implements InstanceRegistry {

    // client请求服务下线, 这里的isReplication传入的也是false
    @Override
    public boolean cancel(String appName, String id, boolean isReplication) {
        return internalCancel(appName, id, isReplication);
    }
    
    
    protected boolean internalCancel(String appName, String id, boolean isReplication) {
        try {
            read.lock();
            
            CANCEL.increment(isReplication);
            
            // 根据appName, 获取所有的服务列表
            Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
            
            // 封装主动下线的服务包装类
            Lease<InstanceInfo> leaseToCancel = null;
            
            if (gMap != null) {
                // 把要下线的服务从gMap中移除掉, 并返回给leaseToCancel变量
                leaseToCancel = gMap.remove(id);
            }
            
            // 添加到最近下线队列中
            recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
            
            InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
            
            if (instanceStatus != null) {
                logger.debug("Removed instance id ...");
            }
            
            if (leaseToCancel == null) {
                CANCEL_NOT_FOUND.increment(isReplication);
                return false;
            } else {
                // 下线逻辑
                leaseToCancel.cancel();
                
                // 获取租约的具体服务信息
                InstanceInfo instanceInfo = leaseToCancel.getHolder();
                String vip = null;
                String svip = null;
                if (instanceInfo != null) {
                    // 触发事件类型为DELETED, 即下线, 见下面的代码, 仅仅更新了时间属性值
                    instanceInfo.setActionType(ActionType.DELETED);
                    
                    // 因为上面移除了id这个项对应的服务, 所以自然要加到最近修改的队列中
                    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                    
                    // 更新下线的这个服务的最后更新时间属性值
                    instanceInfo.setLastUpdatedTimestamp();
                    vip = instanceInfo.getVIPAddress();
                    svip = instanceInfo.getSecureVipAddress();
                }
                
                // 清空缓存, 前面文章有遇到过, 后面会详细说
                invalidateCache(appName, vip, svip); 
            }
        } finally {
            read.unlock();
        }

        synchronized (lock) {
            // 由于有服务下线, 所以得更新心跳数量和自我保护的阈值
            if (this.expectedNumberOfClientsSendingRenews > 0) {
                this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
                updateRenewsPerMinThreshold();
            }
        }

        return true;
    }
}

public class Lease<T> {
    // 更新 剔除/下线时间为当前时间
    public void cancel() {
        if (evictionTimestamp <= 0) {
            evictionTimestamp = System.currentTimeMillis();
        }
    }
}

总结一下剔除处理的主要逻辑

  • 从注册表registry(我有时也称作一级缓存)中移除
  • 清除缓存中这个下线的服务

细节

在线上环境,有可能需要对某个微服务执行下线操作,正确的做法,应该是先停止服务,再通过rest等命令从注册中心下线,原因在于,如果先下线,再停服,由于客户端的心跳功能,可能又会续约上了,从而导致出现问题。

再谈一下isReplication

本篇介绍了Server端服务的续约和下线的处理逻辑,包括上一篇的服务注册,都涉及到了isReplication这个字段,前面提到过:

  • 当isReplication为false时:表示服务的注册请求,会一直进行集群间的同步
  • 当isReplication为true时:表示集群间的服务同步请求

通过前面源码处,该参数的值,可以得出总结

  • 服务注册: 在ApplicationResource.addInstance(..)中传入的是null,所以在后面的传递过程中,isReplication=false, 所以在PeerAwareInstanceRegistryImpl.replicateToPeers()中会执行集群同步。 但不会一直同步,因为在下一下时候,该参数就 被修改成了true,所以不会一直传递。概括起来: 会注册一次、同步仅一次,结束。

  • 续约/主动下线: 第一次进入的时候,InstanceRegistry.renew()传入的值为false,所以在第一次进入PeerAwareInstanceRegistryImpl.replicateToPeers()方法的时候,是可以进行集群同步方法内的, 在PeerAwareInstanceRegistryImpl.replicateInstanceActionsToPeers()的Heartbeat这个case中,node.heartbeat()方法里,创建InstanceReplicationTask实例的时候,在构造函数中传入了isReplication=false,所以下次还会一直同步。主动下线也是同样的逻辑。