You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 43 Next »

Motivation

Ignite use a few protocols of inter-node message exchange:

  1. Communication protocol
  2. Discovery protocol

All protocols have own serialization mechanisms and doesn't support message exchange with node of another version. For making Rolling upgrade feature possible we must make these protocols compatible between Ignite versions. 

Prerequisites

  1. Communication protocol is peer-to-peer protocol: every message is sent between one node-sender and one node-receiver.
  2. Communication protocol is an internal protocol used for system messages exchange between Ignite nodes:
    1. There is a limited amount of peers, and all peers are aware of versions of each other.
    2. All peers are aware of all possible messages and their schemas.
    3. At most time peers are of same version. In short period of time (during RU) messags schemas might differ, but not much (few messages might differ with few fields only).
  3. It is proposed to provide compatibility between versions that differ by 1 minor version, for example between 2.20.X and 2.19.X (but not between 2.20.X and 2.18.X).
  4. Messages can be pretty big, contains cache entries (putAll, historic rebalance).
  5. Users applications deployed on cluster (services, etc) must support compatibility by itself.

Current implementation

Communication protocol

  • All peers hold predefined messages schemas (described with java classes).
  • Messages follow schema in strict way - no fields are skipped and order of fields is guaranteed.
  • Peers send messages as byte stream serializing fields one by one, no delimiters are used between fields and messages.
  • For serializing POJO fields different techniques are used:
    • Message consumes already serialized byte array (e.g. JdkMarshaller for IgniteDiagnosticMessage, SchemaOperationStatusMessage... BinaryMarshaller for GridJobExecuteRequest#jobBytes).
    • User's BinaryObjects, Binarylizable are marshalled to byte array prior to serializing a message (the array is stored in cache structures).
    • Some messages contain POJO field annotated with GridDirectTransient, and additional byte array field fill with GridCacheMessage#prepareMarshal (e.g. BinaryMarshaller for GridChangeGlobalStateMessageResponse).
    • Some messages contains POJO that implements Message interface.

Discovery protocol

  • All peers hold predefined messages schemas (described with java classes).
  • Peers use JdkMarshaller (java serialization) to serialize and deserialize full message. Borders between messages are controlled by java serialization.
  • Responses with status code or ResponseMessage.

Management command protocol

There is an additional serialization protocol between control.sh (thin client) and server nodes

  • Management operates compute tasks for sending commands.
  • Compute args are serialized with IgniteDataTransferObject (that is actually simple compacted Externalizable - fields without java staff). 

This protocol is out of scope of this proposal.

Proposed changes

OnDiscoveryNodeValidationProcessor

This processor validates version of joining node. It should validate that versions differ on 1 minor version only.

Message DTO

There should be one serialization framework for communication and discovery protocol. 

  1. MessageWriter, MessageReader logic is depends on a remote IgniteProductVersion.
  2. Message#writeTo, Message#readFrom is auto-generated and stored separately from Message DTO classes.
  3. Add Message#writeJavaObject - for serializing Java functions and user objects (ComputeJob, etc) with JdkMarshaller.
  4. Message fields contain:
    1. primitive classes
    2. known collections
    3. POJO - that are other Message
    4. Users classes and java functions (e.g. ComputeJob).
    5. byte[] fields (objects serialized externally) must be avoided as much as possible, because their compatibility can't be guaranteed
  5. Add @Since, @Until  annotations for Message classes and fields.

Version check

  1. Ignite CI must notify for IF-clauses with condition based on IgniteVersion older than (curVer - 1).
  2. Ignite CI must forbid code changes if Message DTO changed without corresponding @Since, @Until annotations.
  3. Ignite CI must forbid code changes if Message DTO contains byte[] fields.

Nice to have (for later research)

These possible improvements can be implemented later. Ignite message code generator will support this features:

  1. Optional tagged fields. Such fields is not part of Message schema. The fields can be attached to any message. Cases: securityId, traceId, incremenalIndex, sessionAttributes, depInfo, etc.
    Current approach - is creating a new message-wrapper (IncrementalSnapshotAwareMessage, TransactionAttributesAwareRequest) that wraps original Message with extra data.
  2. Lazy deserialization/unmarshalling of specific fields. Cases: skip deserializing optional fields, transfer cache entries as byte arrays. It can be achieved by storing these fields as byte array.
  3. Compact length of varlen/collections - currently we use 4 bytes (int) to write length of collection or varlen type. In most cases this too much and the length can be encoded with less data (using 1-2 bytes instead).

Communication protocol

Communication protocol consist of 2 parts:

  1. Data - set of declared Message classes, including ser/des algorithm.
  2. Transport - algorithm of transport the Messages to remote node.

Data compatibility

Message - is base class for all messages transported between nodes. Proposed changes:

  1. Message#writeTo consumes MessageWriter that stores IgniteProduceVersion of destination node and use it for serializing data for this version (mostly, for ignoring some fields).
  2. Message#readFrom consumes MessageReader that stores IgniteProductVersion of source node and use it for deserializing data (mostly, for setting default values of new fields).

    Message
    public interface Message {    
        public boolean writeTo(ByteBuffer buf, MessageWriter writer);
    
        public boolean readFrom(ByteBuffer buf, MessageReader reader);
    
        public short directType();
    }
  3. Introduce annotations @Since and @Until for Message classes and Message fields, to use it for generating code for Message#writeTo and Message#readFrom:

MyMessage
// Package that store all schemas.
package org.apache.ignite.internal.messages.schema;

// package private class.
@Since(version = "2.19.0")
class MyMessage {
    private int id;
    
 	/** Remove field. */
    @Until(version = "2.20.0")
    private String rmFld;

    /** New field. */
    @Since(version = "2.20.0")
    private String newFld;
}

// Generated code from the schema ^. 
package org.apache.ignite.internal.messages;

// public class.
public class MyMessage {
    private int id;
    
	/**
     * Remove field. 
     * @deprecated since 2.20.0. 
     */
    @Deprecated
    private String rmFld;

    /** 
     * New field.
     * @since 2.20.0
     */
    private String newFld;

    @Override public boolean writeTo(ByteBuffer buf, MessageWriter writer, IgniteProductVersion destVer) {
        if (destVer.lessThan(2, 19, 0))
            throw new IgniteException("Must not send the message to destination node");

        if (!writer.writeString(id))
            return false;

        if (destVer.lessThan(2, 20, 0)) {
            if (!writer.writeString(rmFld))
                return false;
        }

        if (destVer.greaterThanEqual(2, 20, 0)) {
            if (!writer.writeString(newFld))
                return false;
        }

        return true;
    }

    @Override public boolean readFrom(ByteBuffer buf, MessageReader reader, IgniteProductVersion srcVer) {
        id = reader.readString();

        if (srcVer.greaterThanEqual(2, 20, 0))
            newFld = reader.readString();
        else
            newFld = null;

        return true;
    }
}


Rules to describe Message (must be automated and validated):

  1. Do not remove Message class or Message fields, but annotate it with @Until
  2. Do not change types or order of fields.
  3. New fields must be annotated with @Since. All such fields must be optional, default value is null. Handling the nulls is care of Message consumer on the reader side.

Removing annotated entities is allowed after current version is greater than (@Until + 1) or (@Since + 1).

Transport compatibility 

Communication handshake

Handshake algorithm is extended on new step - validating TcpCommunicationConfiguration consistency. Settings that affects both communicating nodes must be same:

  1. usePairedConnections
  2. connectionsPerNode

Marshaller compatibility

  1. JdkMarshaller - Jdk serialization is compatible between JDK versions if serialVersionUID is specified. Still some messages use it (IgniteDiagnosticMessage - should replace it is much as possible).
  2. BinaryMarshaller - backward compatibility is guaranteed. Require API for getting marshaller for specific version.


Other implementations

Protobuf

https://protobuf.dev/programming-guides/encoding/

  1. Field numbers are serialized with data:
    1. Do not send null fields, ignore unknown fields.
    2. Order of fields isn't guaranteed: message can be concatenations of same fields in different order. For optimizations (compression)?
    3. Make possible easily change schema (but user must preserve field ids).
    4. Serializer must know len of varlen fields (Messages). 
  2. It can be worked in streaming way using CodedOutputStream. To customize serialization, but it still requires len for Messages be written before.
  3. It allows deserialize only required fields using CodedInputStream. It requires writing a code for iterating over tags and skipping fields. 

FlatBuffers

https://flatbuffers.dev/internals/

  1. Offsets are serialized with data:
    1. Order of fields is not guaranteed - for optimization like compaction.
    2. First 4bytes - offset to the root of vtable, that stores offsets to other fields. Vtable can be anywhere relative to fields.
    3. Size of data must be know before serializing to prepare the vtable.
    4. It starts serializing from nested objects, calculate it sizes and fill tables, and then write root object.
  2. No streaming is possible.

Avro

https://avro.apache.org/docs/

  1. Avro relies that schema is known on both sides. And avoid writing field numbers, offsets. Only data. And it can be done in streaming way.
  2. Schema resolution is based on field names.

Bson

  1. Writes field names like json. Suppose that received doesn't have a schema.

Kafka

https://kafka.apache.org/protocol.html

KIP-482: The Kafka Protocol should Support Optional Tagged Fields

  1. All messages are size delimited
  2. Fields order is preserved in serialization.
  3. Clients and brokers are aware of versions of each other and send messages in the form for specific versions known by each others.
  4. There are optional tagged fields beyond a message schema, that can be attached to messages.


  • No labels