微服务节点使用 Spring Boot 会方便很多,在搭建 Spring Boot 的时候碰到了个不大不小的问题,在这里记录下。

主要情况是配置好了 Dubbo Spring Boot 启动 Provider 节点的时候发现异常,抛出了两个错误:

Caused by: org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for ...
Caused by: java.lang.IllegalStateException: KeeperErrorCode = Unimplemented for ...

检查堆栈发现是 Dubbo 建立 ZooKeeper 链接的时候,就直接抛出了异常:

// from com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry.doRegister(ZookeeperRegistry.java:116) ~[dubbo-2.6.2.jar:2.6.2]

protected void doRegister(URL url) {
    try {
        this.zkClient.create(this.toUrlPath(url), url.getParameter("dynamic", true));
    } catch (Throwable var3) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + this.getUrl() + ", cause: " + var3.getMessage(), var3);
    }
}

但是 ZooKeeper 的服务器配置是正常的,百思不得:

$ echo stat | nc localhost 2181
Zookeeper version: 3.4.13, built on 06/29/2018 04:05 GMT

然后继续查看 KeeperException$UnimplementedException 异常的定义,

public static class UnimplementedException extends KeeperException {
    public UnimplementedException() {
        super(Code.UNIMPLEMENTED);
    }
}

对应的调用:

// @from org.apache.zookeeper.ZooKeeper.create
public String create(final String path, byte data[], List<ACL> acl,
        CreateMode createMode, Stat stat, long ttl)
        throws KeeperException, InterruptedException {
    final String clientPath = path;
    PathUtils.validatePath(clientPath, createMode.isSequential());
    EphemeralType.validateTTL(createMode, ttl);

    final String serverPath = prependChroot(clientPath);

    RequestHeader h = new RequestHeader();
    setCreateHeader(createMode, h);
    Create2Response response = new Create2Response();
    if (acl != null && acl.size() == 0) {
        throw new KeeperException.InvalidACLException();
    }
    Record record = makeCreateRecord(createMode, serverPath, data, acl, ttl);
    ReplyHeader r = cnxn.submitRequest(h, record, response, null);
    if (r.getErr() != 0) { // 这里抛出的异常
        throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                clientPath);
    }
    if (stat != null) {
        DataTree.copyStat(response.getStat(), stat);
    }
    if (cnxn.chrootPath == null) {
        return response.getPath();
    } else {
        return response.getPath().substring(cnxn.chrootPath.length());
    }
}

那么基本上可以判定 1、是 ZooKeeper 本身链接的问题,和 Dubbo 没有关系;2、实际上 ZooKeeper 本身的服务已经连接上,但 makeCreateRecord 方法调用出现了异常。

那么,可以得出结论是 Java 端和 ZooKeeper 服务端出现了通讯问题。然后发现日志上 jar 包的版本是 zookeeper-3.5.3-beta.jar ,同时查看了下 Manifest 内容如下:

Implementation-Title: org.apache.zookeeper
Implementation-Version: 3.5.3-beta
Implementation-Vendor: The Apache Software Foundation

然后再运行客户端查看了下服务器的版本,两者版本不一致,突然觉得应该是版本的问题。

$ echo stat | nc localhost 2181
Zookeeper version: 3.4.13, built on 06/29/2018 04:05 GMT

很明显 Java 端的版本比服务端的版本要新,那么考虑使用和服务端同个版本的 jar 包试试。

修改对应 build.gradle 的 dependencies 如下,不要纳入 dubbo-spring-boot-starter 提供的 ZooKeeper 的 jar 包

dependencies {
    // ...
    compile('com.alibaba.boot:dubbo-spring-boot-starter:0.2.0') {
        exclude(module: 'org.apache.zookeeper')
    }
    compile 'org.apache.zookeeper:zookeeper:3.4.13'
}

然后再运行 gradle clean bootRun -x test 发现 Dubbo 正常启动,问题解决。

这个问题有点坑,同时很难发现,我又查了下对应的资料。官方其实已经有对应说明,简单的说就是 ZooKeeper 3.5.x 和 ZooKeeper 3.4.x 有不兼容的情况。

而回过头来看 dubbo-spring-boot-starter 包的 pom.xml 定义,对应的 ZooKeeper 这块的引用是这样子的:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
</dependency>

没有指定版本,也就是默认是取 Maven 库的最新版本,目前是 3.5.4-beta 自然对应本地版本 3.4.13 就有冲突了(竟然不向下兼容,坑)。

相关讨论,看来被坑的不止我: https://stackoverflow.com/questions/35734590/apache-curator-unimplemented-errors-when-trying-to-create-znodes

最后顺便说一句,如果有用到 Spring Cloud 相关的 Zookeeper 组件,也要留个心眼:

dependencies {
    // ...
    compile('org.springframework.cloud:spring-cloud-starter-zookeeper-config') {
        exclude group: 'org.apache.zookeeper', module: 'zookeeper'
    }
    compile('org.springframework.cloud:spring-cloud-starter-zookeeper-discovery') {
        exclude group: 'org.apache.zookeeper', module: 'zookeeper'
    }
    compile 'org.apache.zookeeper:zookeeper:3.4.13'
}

这样子就能保证统一引用的是指定版本的 ZooKeeper 的 jar 包了。

总结下,dubbo-spring-boot-starter 项目目前相对来说还是比较新,相关的文档还是没跟上,但是已经能够日常和生产环境使用了,还是推荐使用简化配置提高些开发效率。

- eof -