Dabai的个人博客

Kryo序列化及其在ONOS中的应用

Kryo是一个快速高效的Java序列化框架,旨在提供快速、高效和易用的API。无论文件、数据库或网络数据Kryo都可以随时完成序列化。Kryo还可以执行自动深拷贝(克隆)、浅拷贝(克隆)。这是对象到对象的直接拷贝,而不是对象->字节->对象的拷贝。

1. Kryo的快速入门

首先,建议使用maven的方式在pom.xml中添加Kryo依赖:

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>

然后,使用下面的方式就能完成对象的序列化了:

1
2
3
4
5
6
7
8
9
10
Kryo kryo = new Kryo();
// ...
Output output = new Output(new FileOutputStream("file.bin"));
SomeClass someObject = ...
kryo.writeObject(output, someObject);
output.close();
// ...
Input input = new Input(new FileInputStream("file.bin"));
SomeClass someObject = kryo.readObject(input, SomeClass.class);
input.close();

2. 注册要序列化的类

当事先没有注册要序列化的类时,Kryo会自动的注册所有要使用的类,但这会增加序列化的一些overhead,例如需要在序列化时添加完整的类名称信息。而且,这种自动注册方式会使得不同程序或线程对同样的类的注册顺序不一样,从而对同样的类的产生不一样的注册ID,这样不同程序或线程对同样的类的序列化和去序列化就会发生问题。

可以使用 kryo.setRegistrationRequired(true) 要求显示注册要序列化的类。这样如果类没有注册,在序列化过程中就会产生java.lang.IllegalArgumentException的异常:

1
2
3
4
5
Exception in thread "main" java.lang.IllegalArgumentException: Class is not registered: kryo.test.A
Note: To register this class use: kryo.register(kryo.test.A.class);
at com.esotericsoftware.kryo.Kryo.getRegistration(Kryo.java:503)
at com.esotericsoftware.kryo.Kryo.writeObject(Kryo.java:557)
......

使用下面的方法注册要序列化的类:

1
2
3
4
5
6
Kryo kryo = new Kryo();
kryo.register(SomeClass.class);
// ...
Output output = ...
SomeClass someObject = ...
kryo.writeObject(output, someObject);

在注册类的时候,Kryo会给每个类关联一个唯一的ID,不同的类的ID不一样,当在序列化类的对象时,Kryo只需保存这个类的ID信息,就可以识别序列化对象的类信息了。相对于保存完整的类名称信息,这种序列化方式能够提高效率。因此,不同程序或线程在对同样的对象信息序列化和去序列化时,要保证同样的类的注册ID是一样的。

当然,也可以指定类的注册ID信息:

1
2
3
4
Kryo kryo = new Kryo();
kryo.register(SomeClass.class, 10);
kryo.register(AnotherClass.class, 11);
kryo.register(YetAnotherClass.class, 12);

3. 序列化器(serializer)

对Java中的一些基本数据类型,如bood,short,int,char等,字符串类型String,基本数据类型的装箱类,Boolean,Short,Integer等,以及常见的集合类型,Kryo都有默认的序列化方式。参考Default serializers

用户可以在注册过程中添加指定的serializer:

1
2
3
Kryo kryo = new Kryo();
kryo.register(SomeClass.class, new SomeSerializer());
kryo.register(AnotherClass.class, new AnotherSerializer());

4. 多线程下使用Kryo

Kryo不是线程安全的,每一个线程应该有自己的Kryo,Input,和Output实例。另外,在去序列化过程中使用byte[] Input时,这个byte[]数组会被修改并在去序列化完成后返回到初始状态。因此不同的线程不能同时使用相同的byte[] Input。

5. 池化Kryo实例

由于Kryo实例的创建和初始化的代价很高,并且不同的线程需要独立的Kryo实例。因此,多线程环境下应该考虑池化Kryo实例,从而减少Kryo实例创建和初始化的开销,提高序列化的效率。

  • 方法一:使用ThreadLocal建立Kryo实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Setup ThreadLocal of Kryo instances
    private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
    protected Kryo initialValue() {
    Kryo kryo = new Kryo();
    // configure kryo instance, customize settings
    return kryo;
    };
    };

    // Somewhere else, use Kryo
    Kryo k = kryos.get();
    ...

    使用ThreadLocal创建只能被一个线程读写的变量,不同线程可以同时使用一个ThreadLocal变量的引用,但它可访问的ThreadLocal变量保存的值是相互独立的。这里将Kryo实例保存在ThreadLocal变量中,不同线程使用get方法获得一个独立的Kryo实例,这样使非线程安全的Kryo对象变得线程安全。

    FIXME:虽然这样做能够实现线程安全的Kryo对象,但由于每个线程获得的是与该线程相关的独立Kryo实例,因此并不能减少Kryo实例创建和初始化的开销?

  • 方法二:使用Kryo提供的KryoPool产生Kryo实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import com.esotericsoftware.kryo.Kryo;
    import com.esotericsoftware.kryo.pool.*;

    KryoFactory factory = new KryoFactory() {
    public Kryo create () {
    Kryo kryo = new Kryo();
    // configure kryo instance, customize settings
    return kryo;
    }
    };
    // Build pool with SoftReferences enabled (optional)
    KryoPool pool = new KryoPool.Builder(factory).softReferences().build();
    Kryo kryo = pool.borrow();
    // do s.th. with kryo here, and afterwards release it
    pool.release(kryo);

    // or use a callback to work with kryo - no need to borrow/release,
    // that's done by `run`.
    String value = pool.run(new KryoCallback() {
    public String execute(Kryo kryo) {
    return kryo.readObject(input, String.class);
    }
    });

    KryoPool将所有的Kryo实例保存在一个Queue中,查看Kryo源码可以发现该Queue是一个ConcurrentLinkedQueue,因此是线程安全的。同时,可以使用borrow和release方法租用和释放Kryo实例,减少Kryo实例创建和初始化的开销。

6. Kryo在ONOS中的应用

ONOS使用Kryo序列化对象,使用一个KryoNamespace类来管理类的注册,Kryo类的池化,并提供序列化方法。

kryonamespaces实现KryoFactory, KryoPool接口,kryonamespaces类中有最重要的两个数据属性,一个是:

1
private List<Pair<Class<?>, Serializer<?>>> types = new ArrayList<>();

它保存注册的序列化的类和其对应的序列化器,当我们将类注册进kryonamespace时就是在向这个List里面添加pair。

另一个是:

1
private int blockHeadId = INITIAL_ID;

当我们在注册需要序列化的类的时候该int类型的数据就会在原来的数据上加1。另外就是序列化实现的时候是将list类型的type改造成RegistrationBlock类之后再用list将其整块整块装进去,具体过程参考KryoNamespace.Builder。

kryonamespaces提供如下的序列化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Serializes given object to byte array using Kryo instance in pool.
* <p>
* Note: Serialized bytes must be smaller than {@link #MAX_BUFFER_SIZE}.
*
* @param obj Object to serialize
* @return serialized bytes
*/
public byte[] serialize(final Object obj) {
return serialize(obj, DEFAULT_BUFFER_SIZE);
}

/**
* Serializes given object to byte array using Kryo instance in pool.
*
* @param obj Object to serialize
* @param bufferSize maximum size of serialized bytes
* @return serialized bytes
*/
public byte[] serialize(final Object obj, final int bufferSize) {
Output out = new Output(bufferSize, MAX_BUFFER_SIZE);
return pool.run(kryo -> {
kryo.writeClassAndObject(out, obj);
out.flush();
return out.toBytes();
});
}

可以看出,serialize最终使用kryo.writeClassAndObject方法完成对象的序列化。

参考: