Java RPC: Transport, Discovery, and Reliability
RPC and HTTP
In this project, the RPC protocol uses a small, customizable message header. Providers register themselves when they start, and consumers discover their addresses through a registry. The client handles load balancing, retries, and circuit breaking; the server applies rate limiting.
Serialization
Kryo
Kryo serializes Java objects compactly and quickly, but a Kryo instance is not thread-safe. Registering a class assigns it an ID, so a serialized message can refer to that ID rather than repeatedly writing the full class name:
| |
Kryo can use generated serializers for particular classes. During deserialization, the class ID is resolved to its class, and an object can be instantiated without running its constructor when the serializer requires it.
Hessian
Hessian supports communication across languages. Each serialization call creates a local HessianOutput or HessianInput; the input reads from a corresponding ByteArrayInputStream. Its format does not require a separate IDL file for the two ends of this project.
Protocol Buffers
- Define the message in
krpc-common/src/main/proto/com/kama/proto/User.proto. - Run
protocto generate Java classes, including a parser, underkrpc-common/target/generated-sources/protobuf/java/com/kama/proto/UserProto.java. - Serialize a generated message with
toByteArray(). - On receipt, select a parser from a message-type-to-parser map and call
parseFrom().
Netty Transport
From One Socket per Call to Reused Channels
An initial blocking-I/O implementation created a new socket for every sendRequest(), paying for connection setup and teardown on each call. The Netty version caches active channels by server address:
| |
getChannel(InetSocketAddress address, Bootstrap bootstrap) returns an active cached channel, or connects and caches a new one. A close listener removes a closed channel from the pool:
| |
Client Pipeline
NettyClientInitializer installs an idle-state handler, decoder, encoder, and client handler. When a read-idle event occurs after 15 seconds, the client handler sends a heartbeat. When a response arrives, channelRead0 uses its request ID to complete the matching pending response future.
Server Pipeline
The server uses separate event-loop groups for accepting connections and handling I/O. Its pipeline includes LengthFieldBasedFrameDecoder, IdleStateHandler, the codec, and NettyRpcServerHandler. The request handler finds the implementation for the requested interface and invokes it. It replies to heartbeats, and closes a channel after a longer read-idle period (30 seconds in this setup).
Message Framing
TCP is a byte stream: one receive operation may contain several messages or only part of one. The custom protocol identifies each frame by a header:
- Message type: 2 bytes, distinguishing requests from responses
- Serializer type: 2 bytes
- Body length: 4 bytes
- Body: the serialized payload
Netty’s length-field decoder waits for a complete frame before passing it to the message decoder:
| |
Request Reliability
Client Retries
Before retrying a call, the client checks whether that method is eligible. It first looks in a local cache and, on a miss, reads instance metadata from Nacos. Eligible calls use a retrying version of sendRequest(). Retrying is reserved for operations whose behavior makes it safe.
Client Circuit Breaker
The client keeps a circuit breaker by service name and method, independent of the chosen Nacos instance:
| |
CLOSED permits calls. After enough failures, OPEN rejects calls. Once the recovery interval passes, HALF_OPEN permits trial requests; their results determine whether the breaker closes again.
Server Rate Limiting
The server applies a token-bucket limit per interface. The bucket has a capacity, a refill rate, a current token count, and a timestamp for its last update. Before invoking a method, the server checks whether a token is available; otherwise it rejects the request.
Nacos Service Discovery
- A server registers an instance with its IP address, port, and weight. The service name comes from the implementation class’s fully qualified name.
- A client proxy’s
invoke()looks up instances for the service. - A load balancer selects an instance and the client obtains a reusable Netty channel to it.
sendRequest()stores a future under the request ID, writes the request, and waits for the response.- The client handler receives the response on another thread, finds the future by request ID, and completes it. The channel remains open for later calls.
Load Balancing
The project gives load balancers a common interface:
| |
The round-robin implementation uses an AtomicInteger index modulo the number of servers, with addresses held in a CopyOnWriteArrayList. The random implementation chooses one address from the current list.