Elasticsearch 几乎不用 Java 自带的 `java.io.Serializable` 机制,而是自己定义了一套“写-读”协议,核心就是两个接口方法:

 

1. `void writeTo(StreamOutput out)`  

2. `static T readFrom(StreamInput in)`

 

只要一个类实现了这对方法,框架就能把它的字段按顺序写成字节、再按同样的顺序读回来;与 `Serializable` 没有任何关系。

 

`ClusterState` 以及 `DiscoveryNode`、`IndexMetaData`、`ShardRouting` 等所有集群级对象都遵循这套协议,所以它们虽然没实现 `Serializable`,照样可以在节点之间来回传输并完整重建。

 

---

 

一、ES 的网络层:基于 Netty 的 “Transport” 协议  

- 节点之间所有通信(包括集群状态发布、索引请求、搜索请求)统一走 TCP 传输层,称作 transport。  

- 传输的消息必须实现 `TransportRequest` 或 `TransportResponse` 接口,而这两个接口的唯一要求就是:

 

  “你会把自己写到 `StreamOutput`,也能从 `StreamInput` 读回来”。  

- 因此,ES 里只要出现跨节点的对象,就一定会配套实现 `writeTo`/`readFrom`,否则连编译都过不了。

 

---

 

二、StreamOutput / StreamInput:定制的“高效字节流”  

- 位于 `org.elasticsearch.common.io.stream` 包,本质是包装了 Netty 的 `ByteBuf`。  

- 提供了一系列 `writeVInt`、`writeString`、`writeOptionalWriteable`、`writeMap` 等紧凑方法,支持变长 int、ZigZag 压缩、字典写字符串、版本号兼容等优化,比 Java 默认协议体积小、速度快。  

- 字段顺序一旦约定好就不能随意改动,增删字段时必须根据 `Version.CURRENT` 做兼容分支,以保证滚动升级时老节点也能解析。

 

---

 

三、以 ClusterState 为例:手工可控的“逐字段写读”

 

下面是极度简化后的伪代码,真实源码在 `ClusterState.java` 及其内部嵌套类:

 

```java

public class ClusterState implements Writeable { // 注意接口是 Writeable,不是 Serializable

    private final long version;

    private final String clusterName;

    private final DiscoveryNodes nodes;

    private final MetaData metaData;

    private final RoutingTable routingTable;

    ...

 

    @Override

    public void writeTo(StreamOutput out) throws IOException {

        out.writeVLong(version);

        out.writeString(clusterName);

        nodes.writeTo(out); // 递归调用

        metaData.writeTo(out);

        routingTable.writeTo(out);

        ...

    }

 

    public static ClusterState readFrom(StreamInput in, DiscoveryNodes nodes) throws IOException {

        long version = in.readVLong();

        String clusterName = in.readString();

        // MetaData、RoutingTable 等同样调用各自的 readFrom

        MetaData md = MetaData.readFrom(in);

        RoutingTable rt = RoutingTable.readFrom(in);

        ...

        return new ClusterState(version, clusterName, nodes, md, rt, ...);

    }

}

```

 

Master 节点每次发布新集群状态时,就是把整个 `ClusterState` 按上面的顺序写进 `StreamOutput`,然后广播给所有节点;接收方用同样的顺序读回字段,再 `new` 一个新的 `ClusterState` 对象。全过程完全可控,没有反射、没有 Unsafe,也不需要 `Serializable`。

 

---

 

四、为什么要抛弃 Serializable  

1. 性能:Java 原生协议字段描述信息冗余、IO 量大;ES 的 `StreamOutput` 用数字常量标识字段,体积可缩小 35 倍。  

2. 可控:自己写读,可以针对版本号做向前向后兼容,滚动升级不会 break。  

3. 安全:反序列化时不会执行任何 `readObject` 魔术方法,杜绝了 “Java 原生反序列化 Gadget” 攻击面。  

4. 跨语言:虽然 ES 服务端只跑在 JVM,但 REST 层用 JSON,transport 层协议简单明了,方便以后出其他语言客户端。

 

---

 

结论

 

Elasticsearch 里的类(包括 `ClusterState`)不需要实现 `Serializable`,因为它们全部实现了 ES 自己的 `Writeable` 接口,用 `writeTo`/`readFrom` 这一套“手写”协议完成高效、紧凑、版本兼容的序列化与反序列化。Java 原生的 `ObjectOutputStream` 在 ES 内部根本不会被用到。

 

`org.elasticsearch.common.io.stream.Writeable` 是 Elasticsearch 8.x(以及 7.x、6.x)统一的序列化/反序列化契约接口,作用等价于 Java 的 `Serializable`,但完全走 ES 自己的 `StreamOutput/StreamInput` 协议。

 

---

 

接口源码(v8.1.0)只有两行核心定义:

 

```java

public interface Writeable {

 

    void writeTo(StreamOutput out) throws IOException;

 

    interface Reader<T> {

        T read(StreamInput in) throws IOException;

    }

}

```

 

- 实现类负责把自己的字段按顺序写进 `StreamOutput`;  

- 对应的 `Reader<T>`(通常是 `static readFrom(StreamInput)` 方法)负责按同样的顺序读回来并重建对象。  

 

---

 

为什么接口里只定义了 `writeTo`,而没有 `readFrom`?

 

1. 构造函数/工厂方法签名各不相同,没法统一成接口方法;  

2. ES 约定“每个 `Writeable` 类必须提供一个 `static T readFrom(StreamInput in)`”,调用处直接拿方法引用即可,例如:

 

```java

// Transport 层反序列化时统一调用

ClusterState state = new ClusterState.Reader().read(in);

// 或者更常见的写法

ClusterState state = ClusterState.readFrom(in, nodes);

```

 

---

 

总结

 

打开的这个 `Writeable.java` 就是 Elasticsearch 唯一且官方的“序列化接口”。

 

只要你在 ES 源码里看到 `implements Writeable`,就能肯定它一定配好了 `writeTo` 和对应的 `readFrom`,后续所有网络传输、持久化、集群状态广播都靠这对方法完成,与 `java.io.Serializable` 没有任何关系。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐