Elasticsearch 几乎不用 Java 自带的 `java.io.Serializable` 机制,而是自己定义了一套"写-读"协议,核心就是两个接口方法:
-
`void writeTo(StreamOutput out)`
-
`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
-
性能:Java 原生协议字段描述信息冗余、IO 量大;ES 的 `StreamOutput` 用数字常量标识字段,体积可缩小 35 倍。
-
可控:自己写读,可以针对版本号做向前向后兼容,滚动升级不会 break。
-
安全:反序列化时不会执行任何 `readObject` 魔术方法,杜绝了 "Java 原生反序列化 Gadget" 攻击面。
-
跨语言:虽然 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`?
-
构造函数/工厂方法签名各不相同,没法统一成接口方法;
-
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` 没有任何关系。