Eureka源码分析:Eureka不会进行二次Replication的原因

时间:2022-12-19 10:54:57

Eureka不会进行二次同步注册信息

Eureka会将本实例中的注册信息同步到它的​​peer​​​节点上,这是我们都知道的特性。然而,当​​peer​​​节点收到同步数据后,并不会将这些信息再同步到它自己的​​peer​​节点上。如果有A, B, C三个实例,A配B, B配C, C配A, 那么当向A注册一个新服务时,只有A, B两个Eureka实例会有新服务的注册信息,C是没有的。这一点在官方wiki上并没有明确说明。下面通过源码来查找一下原因。

构建

Eureka当前版本 (​​https://github.com/Netflix/eureka​​​) 使用​​gradle​​​作为构建工具,不过​​Git​​仓库里提供了​​gradlew​​​,所以也不用我们自己去下载。如果自己下载了​​gradle​​​的最新版本​​3.X​​,那么反而会因为兼容性问题导致构建失败,因为官方使用的是2.1.0版本。构建之前一定要*,否则很多依赖是下载不到的:

./gradlew build -x test // 跳过测试

源码定位与分析

Eureka本质上是一个Servlet应用,它使用​​jersey​​​作为​​RESTful​​​框架来构造自己的REST服务。在​​eureka-server​​​模块中可以找到​​web.xml​​​文件,在​​build/libs​​​目录下可以看到构建完成后生成的​​war​​​包。我们要找的代码,在​​eureka-core​​​模块的​​com.netflix.eureka.resource​​​包下。​​jersey​​​相比​​SpringMVC​​​来说它是一个更加标准的HTTP RESTful框架,因此很自然可以猜想到​​resource​​就是请求路由所在的包。

查阅官方wiki, 我们知道eureka对外暴露接口的形式为:

GET/POST/PUT/DELETE /eureka/v2/apps/{APP-ID}

我们在​​ApplicationsResource​​类中可以发现如下注解:

@Path("/{version}/apps")
@Produces({"application/xml", "application/json"})
public class ApplicationsResource {
// ... ...
}

由此可以确定该类就是我们要找的重点。查阅该类,会发现有如下方法:

/**
* Gets information about a particular {@link com.netflix.discovery.shared.Application}.
*
* @param version
* the version of the request.
* @param appId
* the unique application identifier (which is the name) of the
* application.
* @return information about a particular application.
*/
@Path("{appId}")
public ApplicationResource getApplicationResource(
@PathParam("version") String version,
@PathParam("appId") String appId) {
CurrentRequestVersion.set(Version.toEnum(version));
return new ApplicationResource(appId, serverConfig, registry); // 交给ApplicationResource处理
}

通过注释得知该方法用来处理客户端查询某个服务信息的HTTP请求。但是方法体中并没有具体的查询逻辑,而是委托给了​​ApplicationReource​​​类进行处理。查阅该类,可以发现注册信息的CRUD操作逻辑都是在这里实现的。我们重点关注一下注册方法,因为Eureka在收到新服务的注册信息时会马上将其同步到​​peer​​节点中:

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
// validate that the instanceinfo contains all the necessary required fields

// 参数检查 ...

// 调用注册逻辑
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}

注意​​register​​的第二个参数:

"true".equals(isReplication)

​isReplication​​​是从请求头中获取的字符串,因此可以得知,Eureka在向peer节点发送同步请求时会在请求头中携带自定义的​​x-netflix-discovery-replication​​头:

public static final String HEADER_REPLICATION = "x-netflix-discovery-replication";

peer节点通过该请求头来判断当前请求是其它Eureka节点的同步请求还是服务的注册请求。我们假定当前请求是上一个Eureka发来的同步请求,那么这里第二个参数的值应该为​​true​​。

继续看​​register()​​​方法,这是个​​PeerAwareInstanceRegistry​​​接口的接口方法,该接口只有一个唯一实现​​PeerAwareInstanceRegistryImpl​​:

/**
* Registers the information about the {@link InstanceInfo} and replicates
* this information to all peer eureka nodes. If this is replication event
* from other replica nodes then it is not replicated.
*
* @param info
* the {@link InstanceInfo} to be registered and replicated.
* @param isReplication
* true if this is a replication event from other replica nodes,
* false otherwise.
*/
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
// 调用父类的实现执行注册逻辑
super.register(info, leaseDuration, isReplication);
// 将该注册请求同步到peer节点中
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

首先通过英文注释可以了解到,如果当前注册请求是一个Replication请求,那么该注册请求不会被再次Replicate到下一个​​peer​​​节点中。我们进入到关键的​​replicateToPeers()​​方法中看看为什么不会被同步:

private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
// ... ...

// If it is a replication already, do not replicate again as this will create a poison replication
// 如果 isReplication 为true, 即当前是个同步注册信息的请求
// 这里就直接返回了
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}

// 向自己的peer节点同步注册信息
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}

可以看到下面的代码

if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}

就是Eureka不进行二次Replication的原因,如果当前请求是从其他Eureka发来的同步请求,那么该方法就直接返回了,不再执行后面的同步逻辑。

结论

通过上面的追踪我们确认了Eureka不进行二次同步是作者有意而为之,并不是bug。但是官方wiki上并没有明确写明这一点,文档并不太完善。可以考虑提一个Issue提醒一下开发组。


 

Eureka注册不了,如果查看是否是因为注册的Eureka服务器不对。

解决办法:

com.sun.jersey.client.apache4.ApacheHttpClient4Handler

Eureka源码分析:Eureka不会进行二次Replication的原因