Netflix Eureka - 目录结构及服务单元测试

371 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Github 源码下载

Netflix Eureka 源码地址

git clone https://github.com/Netflix/eureka.git // 克隆 Netflix Eureka 源代码到本地

git tag // 查看 tag 列表

git checkout tag_name // 检出合适的 tag

目录结构

  • eureka-core:Eureka的服务端,其实就是eureka的注册中心
  • eureka-client:Eureka的客户端,服务提供者或服务消费者。提供服务注册、服务发现、服务续约等能力。
  • eureka-resources:基于jsp开发的Eureka控制台
  • eureka-server:包含eureka-client、eureka-core、eureka-resources的能力。

  • eureka-examples:eureka使用的例子
  • eureka-test-utils:eureka的单元测试工具类

Gradle

gradle 入门到入门

Eureka Server 注册中心

Eureka-server其实是一个web应用,通过 web.xml 应用描述文件可以看到应用主要有一个监听器五个过滤器

监听器:

<!-- The class that kick starts the eureka server -->
<listener>
  <listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
</listener>

过滤器:

<!-- 状态过滤器 -->
<filter>
  <filter-name>statusFilter</filter-name>
  <filter-class>com.netflix.eureka.StatusFilter</filter-class>
</filter>
<!-- 请求认证过滤器 -->
<filter>
  <filter-name>requestAuthFilter</filter-name>
  <filter-class>com.netflix.eureka.ServerRequestAuthFilter</filter-class>
</filter>
<!-- 限流过滤器 -->
<!-- 默认不开启 -->
<!-- 只针对 /v2/apps 相关的请求进行过滤处理 -->
<filter>
  <filter-name>rateLimitingFilter</filter-name>
  <filter-class>com.netflix.eureka.RateLimitingFilter</filter-class>
</filter>

<!-- Uncomment this to enable rate limiter filter.
<filter-mapping>
  <filter-name>rateLimitingFilter</filter-name>
  <url-pattern>/v2/apps</url-pattern>
  <url-pattern>/v2/apps/*</url-pattern>
</filter-mapping>
-->
<!-- 压缩编码过滤器 -->
<!-- 只针对 /v2/apps 相关的请求进行过滤处理 -->
<filter>
  <filter-name>gzipEncodingEnforcingFilter</filter-name>
  <filter-class>com.netflix.eureka.GzipEncodingEnforcingFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>gzipEncodingEnforcingFilter</filter-name>
  <url-pattern>/v2/apps</url-pattern>
  <url-pattern>/v2/apps/*</url-pattern>
</filter-mapping>

<!-- 核心过滤器。接收所有的请求,作为请求的入口 -->
<filter>
  <filter-name>jersey</filter-name>
  <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class>
  <init-param>
    <param-name>com.sun.jersey.config.property.WebPageContentRegex</param-name>
    <param-value>/(flex|images|js|css|jsp)/.*</param-value>
  </init-param>
  <init-param>
    <param-name>com.sun.jersey.config.property.packages</param-name>
    <param-value>com.sun.jersey;com.netflix</param-value>
  </init-param>

  <!-- GZIP content encoding/decoding -->
  <init-param>
    <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
    <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
  </init-param>
  <init-param>
    <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
    <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
  </init-param>
</filter>

单元测试 EurekaClientServerRestIntegrationTest

  • 服务注册
  • 服务心跳(注册中心存在服务)
  • 服务心跳(注册中心不存在服务)
  • 服务主动下线(注册中心存在服务)
  • 服务主动下线(注册中心不存在服务)
  • testStatusOverrideUpdateAndDelete
  • testBatch
package com.netflix.eureka.resources;

import java.io.File;
import java.io.FilenameFilter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.shared.resolver.DefaultEndpoint;
import com.netflix.discovery.shared.transport.EurekaHttpClient;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import com.netflix.discovery.shared.transport.TransportClientFactory;
import com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory;
import com.netflix.discovery.util.InstanceInfoGenerator;
import com.netflix.eureka.EurekaServerConfig;
import com.netflix.eureka.cluster.protocol.ReplicationInstance;
import com.netflix.eureka.cluster.protocol.ReplicationInstanceResponse;
import com.netflix.eureka.cluster.protocol.ReplicationList;
import com.netflix.eureka.cluster.protocol.ReplicationListResponse;
import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.Action;
import com.netflix.eureka.transport.JerseyReplicationClient;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Test REST layer of client/server communication. This test instantiates fully configured Jersey
 * container,
 * which is essential to verifying content encoding/decoding with different format types (JSON vs
 * XML, compressed vs
 * uncompressed).
 *
 * @author Tomasz Bak
 */
public class EurekaClientServerRestIntegrationTest {

    private static final String[] EUREKA1_WAR_DIRS = {"build/libs", "eureka-server/build/libs"};

    private static final Pattern WAR_PATTERN = Pattern.compile("eureka-server.*.war");

    private static EurekaServerConfig eurekaServerConfig;

    private static Server server;
    private static TransportClientFactory httpClientFactory;

    private static EurekaHttpClient jerseyEurekaClient;
    private static JerseyReplicationClient jerseyReplicationClient;

    /**
     * We do not include ASG data to prevent server from consulting AWS for its status.
     */
    private static final InstanceInfoGenerator infoGenerator =
            InstanceInfoGenerator.newBuilder(10, 2).withAsg(false).build();
    // 服务实例信息 迭代器
    private static final Iterator<InstanceInfo> instanceInfoIt = infoGenerator.serviceIterator();

    private static String eurekaServiceUrl;

    @BeforeClass
    public static void setUp() throws Exception {
        injectEurekaConfiguration(); // 注入 Eureka 配置需要的信息
        startServer(); // 启动服务
        createEurekaServerConfig(); // mock EurekaServerConfig eurekaServerConfig 实例行为

        // 构造 TransportClientFactory 实例
        httpClientFactory = JerseyEurekaHttpClientFactory.newBuilder().withClientName(
                "testEurekaClient").withConnectionTimeout(1000).withReadTimeout(1000).withMaxConnectionsPerHost(1).withMaxTotalConnections(1).withConnectionIdleTimeout(1000).build();

        // 获取 EurekaHttpClient 实例
        jerseyEurekaClient = httpClientFactory.newClient(new DefaultEndpoint(eurekaServiceUrl));

        //
        ServerCodecs serverCodecs = new DefaultServerCodecs(eurekaServerConfig);

        // 构造 JerseyReplicationClient 实例
        jerseyReplicationClient =
                JerseyReplicationClient.createReplicationClient(eurekaServerConfig, serverCodecs,
                        eurekaServiceUrl);
    }

    @AfterClass
    public static void tearDown() throws Exception {
        removeEurekaConfiguration(); // 清理 Eureka 配置需要的信息
        if (jerseyReplicationClient != null) {
            jerseyReplicationClient.shutdown();
        }
        if (server != null) {
            server.stop();
        }
        if (httpClientFactory != null) {
            httpClientFactory.shutdown();
        }
    }

    /**
     * 测试 服务注册
     *
     * @throws Exception
     */
    @Test
    public void testRegistration() throws Exception {
        // 获取 服务实例信息 对象
        InstanceInfo instanceInfo = instanceInfoIt.next();

        // 通过 jerseyEurekaClient 网络请求 服务注册中心,将 instanceInfo 注册到注册中心
        EurekaHttpResponse<Void> httpResponse = jerseyEurekaClient.register(instanceInfo);

        // 断言 http code equal 204((No Content)表示服务器已成功完成请求,并且在响应有效负载正文中没有要发送的内容。)
        assertThat(httpResponse.getStatusCode(), is(equalTo(204)));
    }

    /**
     * 测试 服务心跳
     *
     * @throws Exception
     */
    @Test
    public void testHeartbeat() throws Exception {
        // 服务注册
        // Register first
        InstanceInfo instanceInfo = instanceInfoIt.next();
        jerseyEurekaClient.register(instanceInfo);

        // 发送 服务心跳
        // Now send heartbeat
        EurekaHttpResponse<InstanceInfo> heartBeatResponse =
                jerseyReplicationClient.sendHeartBeat(instanceInfo.getAppName(),
                        instanceInfo.getId(), instanceInfo, null);

        // 断言 http code equal 200()
        assertThat(heartBeatResponse.getStatusCode(), is(equalTo(200)));
        // 断言 响应体 为空
        assertThat(heartBeatResponse.getEntity(), is(nullValue()));
    }

    /**
     * 测试 丢失服务心跳
     * 没有注册服务就发送心跳
     *
     * @throws Exception
     */
    @Test
    public void testMissedHeartbeat() throws Exception {

        // 获取 服务实例信息
        InstanceInfo instanceInfo = instanceInfoIt.next();

        // 发送 服务心跳
        // Now send heartbeat
        EurekaHttpResponse<InstanceInfo> heartBeatResponse =
                jerseyReplicationClient.sendHeartBeat(instanceInfo.getAppName(),
                        instanceInfo.getId(), instanceInfo, null);

        // 由于没有注册服务,导致找不到对应服务更新租约信息
        assertThat(heartBeatResponse.getStatusCode(), is(equalTo(404)));
    }

    /**
     * 测试 服务主动下线
     * <p>
     * 服务注册中心有服务信息
     *
     * @throws Exception
     */
    @Test
    public void testCancelForEntryThatExists() throws Exception {
        // Register first
        InstanceInfo instanceInfo = instanceInfoIt.next();
        jerseyEurekaClient.register(instanceInfo);

        // Now cancel
        EurekaHttpResponse<Void> httpResponse =
                jerseyEurekaClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());

        assertThat(httpResponse.getStatusCode(), is(equalTo(200)));
    }

    /**
     * 测试 服务主动下线
     * <p>
     * 服务注册中心无服务信息
     *
     * @throws Exception
     */
    @Test
    public void testCancelForEntryThatDoesNotExist() throws Exception {
        // Now cancel
        InstanceInfo instanceInfo = instanceInfoIt.next();
        EurekaHttpResponse<Void> httpResponse =
                jerseyEurekaClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());

        assertThat(httpResponse.getStatusCode(), is(equalTo(404)));
    }


    /**
     * 测试
     *
     * @throws Exception
     */
    @Test
    public void testStatusOverrideUpdateAndDelete() throws Exception {
        // Register first
        InstanceInfo instanceInfo = instanceInfoIt.next();
        jerseyEurekaClient.register(instanceInfo);

        // Now override status
        EurekaHttpResponse<Void> overrideUpdateResponse =
                jerseyEurekaClient.statusUpdate(instanceInfo.getAppName(), instanceInfo.getId(),
                        InstanceStatus.DOWN, instanceInfo);
        assertThat(overrideUpdateResponse.getStatusCode(), is(equalTo(200)));

        InstanceInfo fetchedInstance = expectInstanceInfoInRegistry(instanceInfo);
        assertThat(fetchedInstance.getStatus(), is(equalTo(InstanceStatus.DOWN)));

        // Now remove override
        EurekaHttpResponse<Void> deleteOverrideResponse =
                jerseyEurekaClient.deleteStatusOverride(instanceInfo.getAppName(),
                        instanceInfo.getId(), instanceInfo);
        assertThat(deleteOverrideResponse.getStatusCode(), is(equalTo(200)));

        fetchedInstance = expectInstanceInfoInRegistry(instanceInfo);
        assertThat(fetchedInstance.getStatus(), is(equalTo(InstanceStatus.UNKNOWN)));
    }

    /**
     * 测试 
     *
     * @throws Exception
     */
    @Test
    public void testBatch() throws Exception {
        InstanceInfo instanceInfo = instanceInfoIt.next();
        ReplicationInstance replicationInstance =
                ReplicationInstance.replicationInstance().withAction(Action.Register).withAppName(instanceInfo.getAppName()).withId(instanceInfo.getId()).withInstanceInfo(instanceInfo).withLastDirtyTimestamp(System.currentTimeMillis()).withStatus(instanceInfo.getStatus().name()).build();
        EurekaHttpResponse<ReplicationListResponse> httpResponse =
                jerseyReplicationClient.submitBatchUpdates(new ReplicationList(replicationInstance));

        assertThat(httpResponse.getStatusCode(), is(equalTo(200)));
        List<ReplicationInstanceResponse> replicationListResponse =
                httpResponse.getEntity().getResponseList();
        assertThat(replicationListResponse.size(), is(equalTo(1)));
        assertThat(replicationListResponse.get(0).getStatusCode(), is(equalTo(200)));
    }

    private static InstanceInfo expectInstanceInfoInRegistry(InstanceInfo instanceInfo) {
        EurekaHttpResponse<InstanceInfo> queryResponse =
                jerseyEurekaClient.getInstance(instanceInfo.getAppName(), instanceInfo.getId());
        assertThat(queryResponse.getStatusCode(), is(equalTo(200)));
        assertThat(queryResponse.getEntity(), is(notNullValue()));
        assertThat(queryResponse.getEntity().getId(), is(equalTo(instanceInfo.getId())));
        return queryResponse.getEntity();
    }

    /**
     * This will be read by server internal discovery client. We need to salience it.
     */
    private static void injectEurekaConfiguration() throws UnknownHostException {
        String myHostName = InetAddress.getLocalHost().getHostName();
        String myServiceUrl = "http://" + myHostName + ":8080/v2/";

        System.setProperty("eureka.region", "default");
        System.setProperty("eureka.name", "eureka");
        System.setProperty("eureka.vipAddress", "eureka.mydomain.net");
        System.setProperty("eureka.port", "8080");
        System.setProperty("eureka.preferSameZone", "false");
        System.setProperty("eureka.shouldUseDns", "false");
        System.setProperty("eureka.shouldFetchRegistry", "false");
        System.setProperty("eureka.serviceUrl.defaultZone", myServiceUrl);
        System.setProperty("eureka.serviceUrl.default.defaultZone", myServiceUrl);
        System.setProperty("eureka.awsAccessId", "fake_aws_access_id");
        System.setProperty("eureka.awsSecretKey", "fake_aws_secret_key");
        System.setProperty("eureka.numberRegistrySyncRetries", "0");
    }

    private static void removeEurekaConfiguration() {

    }

    private static void startServer() throws Exception {
        // 使用war包方式启动
        // 查找 eureka-server war包
        File warFile = findWar();

        // 获取 jetty 容器,端口设置为8080
        server = new Server(8080);

        // 获取 web应用 容器
        WebAppContext webapp = new WebAppContext();
        //
        webapp.setContextPath("/");
        //
        webapp.setWar(warFile.getAbsolutePath());

        // 设置 jetty 容器 的处理器,即web应用
        server.setHandler(webapp);

        // 启动 jetty 容器
        server.start();

        // eureka-server 终端地址
        eurekaServiceUrl = "http://localhost:8080/v2";
  
        // 不使用war包方式
//        server = new Server(8080);
//
//        WebAppContext webAppCtx = new WebAppContext(new File("./eureka-server/src/main/webapp")
//        .getAbsolutePath(), "/");
//        webAppCtx.setDescriptor(new File("./eureka-server/src/main/webapp/WEB-INF/web.xml")
//        .getAbsolutePath());
//        webAppCtx.setResourceBase(new File("./eureka-server/src/main/resources")
//        .getAbsolutePath());
//        webAppCtx.setClassLoader(Thread.currentThread().getContextClassLoader());
//        server.setHandler(webAppCtx);
//        server.start();
//
    }

    private static File findWar() {
        File dir = null;
        for (String candidate : EUREKA1_WAR_DIRS) {
            File candidateFile = new File(candidate);
            if (candidateFile.exists()) {
                dir = candidateFile;
                break;
            }
        }
        if (dir == null) {
            throw new IllegalStateException("No directory found at any in any pre-configured " +
                    "location: " + Arrays.toString(EUREKA1_WAR_DIRS));
        }

        File[] warFiles = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return WAR_PATTERN.matcher(name).matches();
            }
        });
        if (warFiles.length == 0) {
            throw new IllegalStateException("War file not found in directory " + dir);
        }
        if (warFiles.length > 1) {
            throw new IllegalStateException("Multiple war files found in directory " + dir + ": " + Arrays.toString(warFiles));
        }
        return warFiles[0];
    }

    private static void createEurekaServerConfig() {
        eurekaServerConfig = mock(EurekaServerConfig.class);

        // Cluster management related // 集群管理相关
        when(eurekaServerConfig.getPeerEurekaNodesUpdateIntervalMs()).thenReturn(1000);

        // Replication logic related // 集群复制相关
        when(eurekaServerConfig.shouldSyncWhenTimestampDiffers()).thenReturn(true);
        when(eurekaServerConfig.getMaxTimeForReplication()).thenReturn(1000);
        when(eurekaServerConfig.getMaxElementsInPeerReplicationPool()).thenReturn(10);
        when(eurekaServerConfig.getMinThreadsForPeerReplication()).thenReturn(1);
        when(eurekaServerConfig.getMaxThreadsForPeerReplication()).thenReturn(1);
        when(eurekaServerConfig.shouldBatchReplication()).thenReturn(true);

        // Peer node connectivity (used by JerseyReplicationClient)
        when(eurekaServerConfig.getPeerNodeTotalConnections()).thenReturn(1);
        when(eurekaServerConfig.getPeerNodeTotalConnectionsPerHost()).thenReturn(1);
        when(eurekaServerConfig.getPeerNodeConnectionIdleTimeoutSeconds()).thenReturn(1000);
    }
}

Eureka Clien 客户端