Dubbo 3.0 规模化落地基石 - 社区持续集成机制解析

本文涉及的产品
性能测试 PTS,5000VUM额度
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
简介: Dubbo 3.0 在阿里巴巴各条业务线包括淘宝、考拉、饿了么、钉钉、达摩院等已全量或开始分批上线,社区企业如小米、工商银行、平安健康等也纷纷引入了 3.0 版本的新特性。支持如此大规模的体量的业务平稳运行,对 Dubbo 3.0 的稳定性有很高的要求,尤其是考虑到社区极高的活跃度,这个挑战就更大了。目前社区有超过 600 位 contributor,近一年以来每天有大约 60 个 commits,陆续支持了 Triple 协议、多实例等功能,这些功能都会涉及到大量的代码变更。每一次提交代码的质量对 Dubbo 3.0 版本的质量、稳定性、可靠性等方面有着至关重要的影响。

作者 | 熊聘


背景


Dubbo 3.0 在阿里巴巴各条业务线包括淘宝、考拉、饿了么、钉钉、达摩院等已全量或开始分批上线,社区企业如小米、工商银行、平安健康等也纷纷引入了 3.0 版本的新特性。支持如此大规模的体量的业务平稳运行,对 Dubbo 3.0 的稳定性有很高的要求,尤其是考虑到社区极高的活跃度,这个挑战就更大了。目前社区有超过 600 位 contributor,近一年以来每天有大约 60 个 commits,陆续支持了 Triple 协议、多实例等功能,这些功能都会涉及到大量的代码变更。每一次提交代码的质量对 Dubbo 3.0 版本的质量、稳定性、可靠性等方面有着至关重要的影响。


现状


Dubbo 保证提交代码的质量主要体现在以下几个方面:

  • dubbo 代码仓库中的单测
  • dubbo-samples 中的集成测试
  • dubbo-benchmark 中的性能测试
  • code review 机制


其中,dubbo 的单测主要针对 Dubbo 代码单元的独立测试,侧重点在功能、分支覆盖等方面,是最基本的测试。dubbo-samples 主要是对 Dubbo 中常用的功能组合和场景进行测试,同时也会对齐 2.7.x 和 3.0 版本之间的功能。dubbo-benchmark 主要是在每次有重大版本升级变更的时候对性能进行压测。code review 机制是在每一次提交的代码都通过所有的单测和集成测试以后由社区的 contributor 和 committer 对提交的 pr 进行 review,当大家的意见不一致会在社区内进行讨论最终达成一致。

问题


集成测试和 benchmark 测试都是独立的代码仓库,进过 Dubbo 社区多年的演进,相对来说变更较少已经比较稳定了。code review 机制主要是通过人的主观判断来对代码的质量进行评估,Dubbo 社区一直在不断吸纳更多优秀的 contributor 加入,理论上这方面的质量是在不断提高的。相对而言,dubbo 的单测集中在 100 多个 module 中,是变更最多、最频繁的,也是最容易出问题的地方。


目前 dubbo 的单测存在以下几个问题:


1、耗时长


dubbo 的单测会在 Ubuntu 和 Windows 上分别对 JDK8 和 11 进行测试,其中 Ubuntu 和 Windows 上运行单测的超时时间分别是 40 分钟和 50 分钟,但是我们发现经常会出现超时的情况。


2、多注册中心场景无法覆盖


dubbo 支持多注册中心,但是在单测中很难覆盖到与多注册中心相关的逻辑。dubbo 支持的注册中心包含 zookeeper、nacos、apollo 等,多种不同注册中心的混合场景更加难以覆盖。


分析


通过对单测运行日志进行抽查和分析我们发现,dubbo 单测的耗时主要集中在与 zookeeper 相关的 module 中。

Testcase Time elapsed(s)
org.apache.dubbo.rpc.protocol.dubbo.ArgumentCallbackTest 76.013
org.apache.dubbo.config.spring.ConfigTest 61.33
org.apache.dubbo.config.spring.schema.DubboNamespaceHandlerTest 50.437
org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessorTest 44.678
org.apache.dubbo.rpc.cluster.support.AbstractClusterInvokerTest 29.954
org.apache.dubbo.remoting.zookeeper.curator.CuratorZookeeperClientTest 25.847
org.apache.dubbo.config.spring.beans.factory.annotation.MethodConfigCallbackTest 25.635
org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListenerTest 24.866
org.apache.dubbo.remoting.zookeeper.curator5.Curator5ZookeeperClientTest 19.664


导致这一问题的最主要原因是因为我们在写单测的时候通常会使用 zookeeper 作为注册中心和元数据中心,而在运行单测的过程中要对 zookeeper 进行频繁的 start 和 stop 操作。每一个单测希望彼此之间互不干扰,都采用了独立的 zookeeper 作为注册中心和元数据中心。当将单测并行运行时又会出现 zookeeper 端口冲突的问题。


现在需要解决的问题是:如何处理 dubbo 单测与注册中心和元数据中心的依赖问题。

解决方案


这个问题看似比较简单,但是在实践过程中却发现比想象的要复杂的多,分别对各种解决方案进行了尝试:


1、TestingServer


TestingServer 是 curator-test 包中提供的专门用来 mock zookeeper 的工具类,目前 dubbo 单测中大量使用了 TestingServer。对与多注册中心的场景是通过同时启动两个 TestingServer 对外暴露不同的端口的方式来实现的。代码如下:


class ZookeeperRegistryCenter extends AbstractRegistryCenter {
    /**
     * Initialize the default registry center.
     */
    public ZookeeperRegistryCenter(int... ports) {
        this.ports = ports;
        this.instanceSpecs = new ArrayList<>(this.ports.length);
        this.zookeeperServers = new ArrayList<>(this.ports.length);
    }
    private static final Logger logger = LoggerFactory.getLogger(ZookeeperRegistryCenter.class);
    /**
     * The type of the registry center.
     */
    private static final String DEFAULT_REGISTRY_CENTER_TYPE = "zookeeper";
    private int[] ports;
    private List<InstanceSpec> instanceSpecs;
    private List<TestingServer> zookeeperServers;
    private AtomicBoolean started = new AtomicBoolean(false);
    /**
     * start zookeeper instances.
     */
    @Override
    public void startup() throws RpcException {
        try {
            if (started.compareAndSet(false, true)) {
                logger.info("The ZookeeperRegistryCenter is starting...");
                for (int port : this.ports) {
                    InstanceSpec instanceSpec = this.createInstanceSpec(port);
                    this.instanceSpecs.add(instanceSpec);
                    this.zookeeperServers.add(new TestingServer(instanceSpec, true));
                }
                logger.info("The ZookeeperRegistryCenter is started successfully");
            }
        } catch (Exception exception) {
            started.set(false);
            throw new RpcException("Failed to initialize ZookeeperRegistryCenter instance", exception);
        }
    }
    /**
     * destroy the zookeeper instances.
     */
    @Override
    public void shutdown() throws RpcException {
        logger.info("The ZookeeperRegistryCenter is stopping...");
        List<RpcException> exceptions = new ArrayList<>(this.zookeeperServers.size());
        for (TestingServer testingServer : this.zookeeperServers) {
            try {
                testingServer.close();
                logger.info(String.format("The zookeeper instance of %s is shutdown successfully",
                    testingServer.getConnectString()));
            } catch (IOException exception) {
                RpcException rpcException = new RpcException(String.format("Failed to close zookeeper instance of %s",
                    testingServer.getConnectString()),
                    exception);
                exceptions.add(rpcException);
                logger.error(rpcException);
            }
        }
        this.instanceSpecs.clear();
        this.zookeeperServers.clear();
        if (!exceptions.isEmpty()) {
            logger.info("The ZookeeperRegistryCenter failed to close.");
            // throw any one of exceptions
            throw exceptions.get(0);
        } else {
            logger.info("The ZookeeperRegistryCenter close successfully.");
        }
    }
}


1、优点

  • 实现简单
  • 启动和停止 zookeeper 速度快


2、缺点

  • 无法模拟真实的 zookeeper
  • 无法测试 Curator 和 zookeeper 的版本兼容性问题
  • nacos 和 apollo 没有提供这种 embedded 的方式,无法适用于所有类型的注册中心


2、Github Actions


GitHub Actions 是 GitHub 上的 CI/CD,所有提交代码的单测都会在这上面运行。通过各种 action 来制定 workflow,可以在 workflow 中可以初始化运行的环境,所以想在初始化环境的时候将 zookeeper(或者 nacos 和 apollo)部署起来,然后再运行单测。当尝试这种方案的时候,我们进行了大量的尝试,也踩了不少的坑。


1、docker

zookeeper、nacos 和 apollo 的镜像都有官方的版本,如果我们通过 docker 来拉起这些镜像,这个问题就迎刃而解了。


通过查看 GitHub Actions 的官网发现 GitHub 提供的环境是内置了 docker 环境的,所以这条道路貌似比较容易,但是当 yml 文件写好以后发现在 Ubuntu 上是可以正常运行的,在 Windows上 会报错,错误信息如下:


ERROR: no matching manifest for windows/amd64 in the manifest list entries


通过研究发现这个问题主要是由于 Linux 上的镜像在 Windows 上无法识别,需要 Switch to Linux Container,这 GitHub Actions 中只能通过命令来实现,具体命令为:"C:\Program Files\Docker\Docker\DockerCli.exe" -SwitchLinuxEngine,但是执行这条命令后需要重启一下 docker。如果是在自己的 Windows 机器上这么操作是可行的,但是在 Github Actions 中操作显然是行不通的。


当然,还有另一个思路:那就是 build 一个基于 Windows 版本的镜像。很快我们也尝试了这种方案,发现 Windows 上创建 zookeeper 的镜像与 Windows 系统的版本密切相关,并不适用于所有版本的 Windows 操作系统。同时,创建镜像一般都是采用 Powershell 脚本,这种脚本相比 shell 脚本来说使用的要少很多,必然会给后面的维护带来了很多的隐患。在编写 GitHub workflows 是还要考虑两个不同分支的问题。


最后,我们通过修改现有的镜像生成脚本创建了基于 Windows 系统的 zookeeper 镜像,这个问题貌似总算是解决了。不幸的是,当我们运行单测的时候发现,在 Ubuntu 环境下拉起 zookeeper 镜像的耗时大约是 20 秒,而在 Windows 环境下需要 2 分钟左右,完全不是一个量级,这是我们完全不能接受的。通过分析我们发现,zookeeper 在 Ubuntu 上的镜像大小为 100MB 以内,而在 Windows 上却是 2.7G 左右,在 Windows 上拉取镜像的耗时最多,同时 zookeeper 在 Ubuntu 上的启动时间要比 Windows 快。


2、shell 脚本

docker 的方式既然行不通,那是不是可以尝试一下使用脚本部署的方式呢?考虑到 Windows 和 Linux 环境上脚本的差异,事先查了一下 Github Actions 上看看在不通操作系统上对脚本支持的情况:

1.png

我们可以看到在 Windows 上对 bash shell 的支持仅仅是 Git for Windows 附带的 bash shell,往往一些复杂一点的 bash shell 命令就不支持了。由于我们使用的都是比较常见的 bash shell 命令,所以想试一试,于是就开始撸起了 shell 脚本。


ZOOKEEPER_BINARY_URL="https://archive.apache.org/dist/zookeeper/zookeeper-$ZK_VERSION/apache-zookeeper-$ZK_VERSION-bin.tar.gz"
case $1 instart)    echo "Setup zookeeper instances...."    # download zookeeper instances    mkdir -p $ZK_2181_DIR $ZK_2182_DIR $LOG_DIR/2181 $LOG_DIR/2182    # download zookeeper archive binary if necessary    if [ ! -f "$DIR/zookeeper/apache-zookeeper-$ZK_VERSION-bin.tar.gz" ]; then      wget -P $DIR/zookeeper -c $ZOOKEEPER_BINARY_URL    fi    # setup zookeeper with 2182    tar -zxf $DIR/zookeeper/apache-zookeeper-$ZK_VERSION-bin.tar.gz -C $ZK_2181_DIR    cp $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo_sample.cfg $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    # mac OS    if [ "$(uname)" == "Darwin" ]    then      sed -i "_bak" "s#^clientPort=.*#clientPort=2181#g" $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg      sed -i "_bak" "s#^dataDir=.*#dataDir=$LOG_DIR/2181#g" $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    else      sed -i "s#^clientPort=.*#clientPort=2181#g" $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg      sed -i "s#^dataDir=.*#dataDir=$LOG_DIR/2181#g" $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    fi    echo "admin.serverPort=8081" >> $ZK_2181_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    # setup zookeeper with 2182    tar -zxf $DIR/zookeeper/apache-zookeeper-$ZK_VERSION-bin.tar.gz -C $ZK_2182_DIR    cp $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo_sample.cfg $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    # mac OS    if [ "$(uname)" == "Darwin" ]    then      sed -i "_bak" "s#^clientPort=.*#clientPort=2182#g" $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg      sed -i "_bak" "s#^dataDir=.*#dataDir=$LOG_DIR/2182#g" $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    else      sed -i "s#^clientPort=.*#clientPort=2182#g" $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg      sed -i "s#^dataDir=.*#dataDir=$LOG_DIR/2182#g" $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    fi    echo "admin.serverPort=8082" >> $ZK_2182_DIR/apache-zookeeper-$ZK_VERSION-bin/conf/zoo.cfg    echo "Start zookeeper instances...."    $ZK_SERVER_2181 start    $ZK_SERVER_2182 start    ;;stop)    echo "Stop zookeeper instances...."    $ZK_SERVER_2181 stop    $ZK_SERVER_2182 stop    rm -rf $DIR/zookeeper    ;;status)    echo "Get status of all zookeeper instances...."    $ZK_SERVER_2181 status    $ZK_SERVER_2182 status    ;;reset)    echo "Reset all zookeeper instances"    $ZK_CLIENT_2181 -timeout 1000  -server 127.0.0.1:2181 deleteall /dubbo    $ZK_CLIENT_2182 -timeout 1000  -server 127.0.0.1:2182 deleteall /dubbo    ;;*)    echo "./zkCmd.sh start|stop|status|reset"    exit 1    ;;esac


脚本写完以后发现,在 Mac 和 Linux 系统上运行都是正常的,但是这个脚本在 Windows 上的 git-bash 却是行不通的。主要原因是对 wget、sed 命令不支持。wget 通过 curl 命令来替换可以实现下载的功能,但是 sed 命令确实很难替换。


最后,在赫炎大佬的提示下,在 Github Workflows 中安装了 msys2 插件,msys2 可以建立一个简单易用的统一环境,也就是说可以在 Windows 上运行 bash shell,经过验证发现这一方案确实可行。


steps:      - name: "Set up msys2 if necessary"        if: ${{ startsWith( matrix.os, 'windows') }}        uses: msys2/setup-msys2@v2        with:          release: false  # support cache, see https://github.com/msys2/setup-msys2#context


优点

  • 使用全局 zookeeper 来作为注册中心和元数据中心,大大降低了注册中心的启动和销毁次数
  • 代码无侵入,写单测的时候不用维护注册中心的生命周期
  • 更接近生产环境
  • 可以支持多版本、不同类型的注册中心混用


缺点

  • 代码在本地运行时需要手动启动注册中心


一切看似很完美,但是如果在本地运行单测的时候强制要求开发者在本机启动注册中心,这貌似违背了单测的原则。为了解决这一问题,我们又尝试了新的解决方案。


3、TestExecutionListener


TestExecutionListener 是 Junit5 在运行单测的过程中对外提供的一个接口,可以通过这个接口获取所有单测启动、所有单测全部结束、单个单测启动、单个单测结束等相关的事件。


/**
 * Register a concrete implementation of this interface with a {@link Launcher}
 * to be notified of events that occur during test execution.
 *
 * <p>All methods in this interface have empty <em>default</em> implementations.
 * Concrete implementations may therefore override one or more of these methods
 * to be notified of the selected events.
 *
 * <p>JUnit provides two example implementations.
 *
 * <ul>
 * <li>{@link org.junit.platform.launcher.listeners.LoggingListener}</li>
 * <li>{@link org.junit.platform.launcher.listeners.SummaryGeneratingListener}</li>
 * </ul>
 *
 * <p>Contrary to JUnit 4, {@linkplain org.junit.platform.engine.TestEngine test engines}
 * are supposed to report events not only for {@linkplain TestIdentifier identifiers}
 * that represent executable leaves in the {@linkplain TestPlan test plan} but also
 * for all intermediate containers. However, while both the JUnit Vintage and JUnit
 * Jupiter engines comply with this contract, there is no way to guarantee this for
 * third-party engines.
 */
public interface TestExecutionListener {
  /**
   * Called when the execution of the {@link TestPlan} has started,
   * <em>before</em> any test has been executed.
   *
   * @param testPlan describes the tree of tests about to be executed
   */
  default void testPlanExecutionStarted(TestPlan testPlan) {
  }
  /**
   * Called when the execution of the {@link TestPlan} has finished,
   * <em>after</em> all tests have been executed.
   *
   * @param testPlan describes the tree of tests that have been executed
   */
  default void testPlanExecutionFinished(TestPlan testPlan) {
  }
  /**
   * Called when the execution of a leaf or subtree of the {@link TestPlan}
   * has finished, regardless of the outcome.
   *
   * <p>The {@link TestIdentifier} may represent a test or a container.
   *
   * <p>This method will only be called if the test or container has not
   * been {@linkplain #executionSkipped skipped}.
   *
   * <p>This method will be called for a container {@code TestIdentifier}
   * <em>after</em> all of its children have been
   * {@linkplain #executionSkipped skipped} or have
   * {@linkplain #executionFinished finished}.
   *
   * <p>The {@link TestExecutionResult} describes the result of the execution
   * for the supplied {@code TestIdentifier}. The result does not include or
   * aggregate the results of its children. For example, a container with a
   * failing test will be reported as {@link Status#SUCCESSFUL SUCCESSFUL} even
   * if one or more of its children are reported as {@link Status#FAILED FAILED}.
   *
   * @param testIdentifier the identifier of the finished test or container
   * @param testExecutionResult the (unaggregated) result of the execution for
   * the supplied {@code TestIdentifier}
   *
   * @see TestExecutionResult
   */
  default void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
  }
}


我们通过实现 TestExecutionListener#testPlanExecutionStarted 的接口启动注册中心,TestExecutionListener#testPlanExecutionFinished 接口用来销毁注册中心,在每个单测运行结束以后,通过 TestExecutionListener#executionFinished 的接口来重置注册中心中的数据。通过这种方式就可以解决开发者在本地运行单测的时候需要手动启动注册中心的问题了。但是实现起来却是并不是那么简单。


我们将注册中心的生命周期抽象成以下几个部分:

  • 初始化:这个过程包括下载 zookeeper 的 jar 包、解压 jar 包和修改配置文件 3 个过程
  • 启动:启动多个注册中心
  • 重置:每次单测运行结束后重置注册中心中的数据
  • 销毁:关闭多个注册中心


1、初始化下载文件、解压和修改配置这几个过程看似简单,但是想实现的比较优雅却还是要花一番工夫的。需要考虑异步下载、重复下载、重复解压、并行解压、无网络或者网络不稳定情况下的解决方案、端口占用等一系列问题。


2、启动启动 zookeeper 在 Windows 上是通过 zkServer.cmd 脚本,而在 Linux 上是通过 zkServer.sh 脚本,这两个脚本实现的方式是截然不同的。同时,我们希望注册中心是运行在单独进程中,这就需要使用 ProcessBuilder 的方式来启动 zookeeper。


在 Linux 上相对简单一点,代码如下:


protected Process doProcess(ZookeeperContext context, int clientPort) throws DubboTestException {
    logger.info(String.format("The zookeeper-%d is starting...", clientPort));
    List<String> commands = new ArrayList<>();
    Path zookeeperBin = Paths.get(context.getSourceFile().getParent().toString(),
        String.valueOf(clientPort),
        context.getUnpackedDirectory(),
        "bin");
    commands.add(Paths.get(zookeeperBin.toString(), "zkServer.sh")
        .toAbsolutePath().toString());
    commands.add("start");
    commands.add(Paths.get(zookeeperBin.getParent().toString(),
        "conf",
        "zoo.cfg").toAbsolutePath().toString());
    try {
        return new ProcessBuilder().directory(zookeeperBin.getParent().toFile())
            .command(commands).inheritIO().redirectOutput(ProcessBuilder.Redirect.PIPE).start();
    } catch (IOException e) {
        throw new DubboTestException(String.format("Failed to start zookeeper-%d", clientPort), e);
    }
}


通过检测 Process 中输出的日志来的关键词来判断是否启动成功。


在 Windows 上会比较复杂一点,因为 Windows 上运行 zkServer.cmd 脚本的时候会弹出一个黑框,需要采用后台的方式来运行。同时,当这个黑框关闭以后 zookeeper 就被关闭了。同样尝试使用 ProcessBuilder 的方式来实现发现遇到不少问题,特别是通过 start /b cmd.exe /c zkServer.cmd 命令来启动 zookeeper。最后通过 commons-exec 包中的 Executor 来实现了在 Windows 环境启动 zookeeper。


protected void doProcess(ZookeeperWindowsContext context) throws DubboTestException {
    ......
    for (int clientPort : context.getClientPorts()) {
        logger.info(String.format("The zookeeper-%d is starting...", clientPort));
        Path zookeeperBin = Paths.get(context.getSourceFile().getParent().toString(),
            String.valueOf(clientPort),
            context.getUnpackedDirectory(),
            "bin");
        Executor executor = new DefaultExecutor();
        executor.setExitValues(null);
        executor.setWatchdog(context.getWatchdog());
        CommandLine cmdLine = new CommandLine("cmd.exe");
        cmdLine.addArgument("/c");
        cmdLine.addArgument(Paths.get(zookeeperBin.toString(), "zkServer.cmd")
            .toAbsolutePath().toString());
        context.getExecutorService().submit(() -> executor.execute(cmdLine));
    }
}


这里需要提醒的是,启动 Process 以后需要保存 pid,后面销毁的时候会用到。


3、重置这个功能是最简单的,因为现在注册中心已经启动了,只需要连接上注册中心删除注册上去的节点即可。


public void process(Context context) throws DubboTestException {
    ZookeeperContext zookeeperContext = (ZookeeperContext)context;
    for (int clientPort : zookeeperContext.getClientPorts()) {
        CuratorFramework client;
        try {
            CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:" + clientPort)
                .retryPolicy(new RetryNTimes(1, 1000));
            client = builder.build();
            client.start();
            boolean connected = client.blockUntilConnected(1000, TimeUnit.MILLISECONDS);
            if (!connected) {
                throw new IllegalStateException("zookeeper not connected");
            }
            // 删除dubbo节点
            client.delete().deletingChildrenIfNeeded().forPath("/dubbo");
        } catch (Exception e) {
            throw new DubboTestException(e.getMessage(), e);
        }
    }
}


4、销毁通过研究 zookeeper 的 zkServer.cmd 和 zkServer.sh 脚本发现,在执行 zkServer.sh stop 命令的时候其实是找到 zookeeper 的 pid 然后再 kill 掉。


stop)
  echo -n "Stopping zookeeper ... "
    if [ ! -f "$ZOOPIDFILE" ]
    then
      echo "no zookeeper to stop (could not find file $ZOOPIDFILE)"
    else
      $KILL $(cat "$ZOOPIDFILE")
      rm "$ZOOPIDFILE"
      sleep 1
      echo STOPPED
    fi
    exit 0


在 Linux 上相对简单一点,代码如下:


protected Process doProcess(ZookeeperContext context, int clientPort) throws DubboTestException {
    logger.info(String.format("The zookeeper-%d is stopping...", clientPort));
    List<String> commands = new ArrayList<>();
    Path zookeeperBin = Paths.get(context.getSourceFile().getParent().toString(),
        String.valueOf(clientPort),
        context.getUnpackedDirectory(),
        "bin");
    commands.add(Paths.get(zookeeperBin.toString(), "zkServer.sh")
        .toAbsolutePath().toString());
    commands.add("stop");
    try {
        return new ProcessBuilder().directory(zookeeperBin.getParent().toFile())
            .command(commands).inheritIO().redirectOutput(ProcessBuilder.Redirect.PIPE).start();
    } catch (IOException e) {
        throw new DubboTestException(String.format("Failed to stop zookeeper-%d", clientPort), e);
    }
}


但是在 Windows 上却有所不同,需要将之前记录的 pid 杀掉,代码如下:


protected void doProcess(ZookeeperWindowsContext context) throws DubboTestException {
    logger.info("All of zookeeper instances are stopping...");
    // find pid and save into global context.
    this.findPidProcessor.process(context);
    // kill pid of zookeeper instance if exists
    this.killPidProcessor.process(context);
    // destroy all resources
    context.destroy();
}


在这个过程中需要考虑重重复销毁的问题,所以在销毁之前会先查一下 pid 然后再销毁。查找 pid 是通过 netstat 命令实现的,代码如下:


private void findPid(ZookeeperWindowsContext context, int clientPort) {
    logger.info(String.format("Find the pid of the zookeeper with port %d", clientPort));
    Executor executor = new DefaultExecutor();
    executor.setExitValues(null);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ByteArrayOutputStream ins = new ByteArrayOutputStream();
    ByteArrayInputStream in = new ByteArrayInputStream(ins.toByteArray());
    executor.setStreamHandler(new PumpStreamHandler(out, null, in));
    CommandLine cmdLine = new CommandLine("cmd.exe");
    cmdLine.addArgument("/c");
    cmdLine.addArgument("netstat -ano | findstr " + clientPort);
    try {
        executor.execute(cmdLine);
        String result = out.toString();
        logger.info(String.format("Find result: %s", result));
        if (StringUtils.isNotEmpty(result)) {
            String[] values = result.split("\\r\\n");
            // values sample:
            // Protocol Local address          Foreign address        Status          PID
            //   TCP    127.0.0.1:2182         127.0.0.1:56672        ESTABLISHED     4020
            //   TCP    127.0.0.1:56672        127.0.0.1:2182         ESTABLISHED     1980
            //   TCP    127.0.0.1:56692        127.0.0.1:2182         ESTABLISHED     1980
            //   TCP    127.0.0.1:56723        127.0.0.1:2182         ESTABLISHED     1980
            //   TCP    [::]:2182              [::]:0                 LISTENING       4020
            if (values != null && values.length > 0) {
                for (int i = 0; i < values.length; i++) {
                    List<String> segments = Arrays.stream(values[i].trim().split(" "))
                        .filter(str -> !"".equals(str))
                        .collect(Collectors.toList());
                    // segments sample:
                    // TCP
                    // 127.0.0.1:2182
                    // 127.0.0.1:56672
                    // ESTABLISHED
                    // 4020
                    if (segments != null && segments.size() == 5) {
                        if (this.check(segments.get(1), clientPort)) {
                            int pid = Integer.valueOf(segments.get(segments.size() - 1).trim());
                            context.register(clientPort, pid);
                            return;
                        }
                    }
                }
            }
        }
    } catch (IOException e) {
        throw new DubboTestException(String.format("Failed to find the PID of zookeeper with port %d", clientPort), e);
    }
}


通过 killtask 命令来销毁进程,代码如下:


protected void doProcess(ZookeeperWindowsContext context) throws DubboTestException {
    for (int clientPort : context.getClientPorts()) {
        Integer pid = context.getPid(clientPort);
        if (pid == null) {
            logger.info("There is no PID of zookeeper instance with the port " + clientPort);
            continue;
        }
        logger.info(String.format("Kill the pid %d of the zookeeper with port %d", pid, clientPort));
        Executor executor = new DefaultExecutor();
        executor.setExitValues(null);
        executor.setStreamHandler(new PumpStreamHandler(null, null, null));
        CommandLine cmdLine = new CommandLine("cmd.exe");
        cmdLine.addArgument("/c");
        cmdLine.addArgument("taskkill /PID " + pid + " -t -f");
        try {
            executor.execute(cmdLine);
            // clear pid
            context.removePid(clientPort);
         } catch (IOException e) {
            throw new DubboTestException(String.format("Failed to kill the pid %d of zookeeper with port %d", pid, clientPort), e);
        }
    }
}


至此,彻底解决了单测在本地运行时需要手动启动注册中心的问题了。


优点

  • 支持多种类型注册中心的混用。如果需要支持 nacos,只要按照这个方案针对 nacos 实现一遍即可。
  • 最大限度的模拟生产环境
  • 开发者使用无感
  • 代码无倾入


缺点

  • 依赖网络环境


针对无网络或者网络不稳定的情况,可以把下载好的 zookeeper 二进制文件 copy 放到指定的目录下即可。

总结


通过代码测试优化,Dubbo 3.0 的单测运行耗时在 Ubuntu 环境下从 40 分钟下降到 24 分钟,在 Windows 环境下从 50 分钟下降到 30 分钟左右,也很好的解决了多注册中心无法覆盖的问题。


提高 Dubbo 3.0 版本的质量、稳定性和可靠性一直是 Dubbo 社区契而不舍的目标。这一次的优化仅仅只是向前迈出了一小步,后续还会朝着这个方向不断改进,也希望有更多对 Dubbo 感兴趣的同学积极参与社区贡献!在此也非常感谢赫炎和河清两位大佬的指导!

Dubbo 社区刚刚发布了 3.0.5 版本,Go 语言的首个 3.0 正式版本也已发布。欢迎更多的 Dubbo 3 用户在此登记,以便能更好的与社区交流使用问题。

https://github.com/apache/dubbo/issues/9436


对 dubbogo感兴趣的同学,可加入钉钉群 23331795 进行交流。


作者介绍

熊聘,Apache Dubbo Committer,Github 账号:pinxiong微信公众号:技术交流小屋。关注 RPC、Service Mesh 和云原生等领域。现任职于携程国际事业部研发团队,负责市场营销、云原生等相关工作。

相关文章
|
27天前
|
数据采集 安全 数据管理
深度解析:DataHub的数据集成与管理策略
【10月更文挑战第23天】DataHub 是阿里云推出的一款数据集成与管理平台,旨在帮助企业高效地处理和管理多源异构数据。作为一名已经有一定 DataHub 使用经验的技术人员,我深知其在数据集成与管理方面的强大功能。本文将从个人的角度出发,深入探讨 DataHub 的核心技术、工作原理,以及如何实现多源异构数据的高效集成、数据清洗与转换、数据权限管理和安全控制措施。通过具体的案例分析,展示 DataHub 在解决复杂数据管理问题上的优势。
139 1
|
2月前
|
C# Windows
visual studio 2022 社区版 c# 环境搭建及安装使用【图文解析-小白版】
这篇文章提供了Visual Studio 2022社区版C#环境的搭建和安装使用指南,包括下载、安装步骤和创建C#窗体应用程序的详细图文解析。
visual studio 2022 社区版 c# 环境搭建及安装使用【图文解析-小白版】
|
25天前
|
负载均衡 监控 Dubbo
Dubbo 原理和机制详解(非常全面)
本文详细解析了 Dubbo 的核心功能、组件、架构设计及调用流程,涵盖远程方法调用、智能容错、负载均衡、服务注册与发现等内容。欢迎留言交流。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Dubbo 原理和机制详解(非常全面)
|
24天前
|
安全 测试技术 数据安全/隐私保护
原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
|
1月前
|
存储 SQL 分布式计算
湖仓一体架构深度解析:构建企业级数据管理与分析的新基石
【10月更文挑战第7天】湖仓一体架构深度解析:构建企业级数据管理与分析的新基石
93 1
|
2月前
|
缓存 负载均衡 Dubbo
Dubbo技术深度解析及其在Java中的实战应用
Dubbo是一款由阿里巴巴开源的高性能、轻量级的Java分布式服务框架,它致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
81 6
|
2月前
|
存储 缓存 自然语言处理
深度解析ElasticSearch:构建高效搜索与分析的基石
【9月更文挑战第8天】在数据爆炸的时代,如何快速、准确地从海量数据中检索出有价值的信息成为了企业面临的重要挑战。ElasticSearch,作为一款基于Lucene的开源分布式搜索和分析引擎,凭借其强大的实时搜索、分析和扩展能力,成为了众多企业的首选。本文将深入解析ElasticSearch的核心原理、架构设计及优化实践,帮助读者全面理解这一强大的工具。
194 7
|
2月前
|
测试技术 UED 开发者
软件测试的艺术:从代码审查到用户反馈的全景探索在软件开发的宇宙中,测试是那颗确保星系正常运转的暗物质。它或许不总是站在聚光灯下,但无疑是支撑整个系统稳定性与可靠性的基石。《软件测试的艺术:从代码审查到用户反馈的全景探索》一文,旨在揭开软件测试这一神秘面纱,通过深入浅出的方式,引领读者穿梭于测试的各个环节,从细微处着眼,至宏观视角俯瞰,全方位解析如何打造无懈可击的软件产品。
本文以“软件测试的艺术”为核心,创新性地将技术深度与通俗易懂的语言风格相结合,绘制了一幅从代码审查到用户反馈全过程的测试蓝图。不同于常规摘要的枯燥概述,这里更像是一段旅程的预告片,承诺带领读者经历一场从微观世界到宏观视野的探索之旅,揭示每一个测试环节背后的哲学与实践智慧,让即便是非专业人士也能领略到软件测试的魅力所在,并从中获取实用的启示。
|
3月前
|
持续交付 jenkins Devops
WPF与DevOps的完美邂逅:从Jenkins配置到自动化部署,全流程解析持续集成与持续交付的最佳实践
【8月更文挑战第31天】WPF与DevOps的结合开启了软件生命周期管理的新篇章。通过Jenkins等CI/CD工具,实现从代码提交到自动构建、测试及部署的全流程自动化。本文详细介绍了如何配置Jenkins来管理WPF项目的构建任务,确保每次代码提交都能触发自动化流程,提升开发效率和代码质量。这一方法不仅简化了开发流程,还加强了团队协作,是WPF开发者拥抱DevOps文化的理想指南。
87 1
|
2月前
|
图形学 iOS开发 Android开发
从Unity开发到移动平台制胜攻略:全面解析iOS与Android应用发布流程,助你轻松掌握跨平台发布技巧,打造爆款手游不是梦——性能优化、广告集成与内购设置全包含
【8月更文挑战第31天】本书详细介绍了如何在Unity中设置项目以适应移动设备,涵盖性能优化、集成广告及内购功能等关键步骤。通过具体示例和代码片段,指导读者完成iOS和Android应用的打包与发布,确保应用顺利上线并获得成功。无论是性能调整还是平台特定的操作,本书均提供了全面的解决方案。
153 0
下一篇
无影云桌面