今天我们来谈一下akka的序列化框架,其实序列化、反序列化是一个老生常谈的问题,那么我们为什么还要研究一下akka的序列化框架呢?不就是使用哪种序列化、反序列化方法的区别么?其实刚开始的时候我也是这么想的,但是针对性、系统性的分析一下akka的序列化、反序列化过程,就会发现这个问题其实还是挺有意思的。

  我们首先来看下什么是序列化、反序列化。序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程;反序列化,就是通过从存储区中读取或反序列化对象的状态,重新创建该对象。举个栗子,在跨网络传输的过程中,传输的都是二进制数,是无法直接传输对象的。对象是什么呢?数据和算法,状态和方法。数据就是对象的状态,比如一个整型字段的值,字符串的具体内容;算法就是对象所属的方法,简单来说就是二进制可执行代码。统一来看,他们都可以是二进制数据。那么为啥在序列化的定义中,没有说算法的。想想都知道,当然是为了减少传输的数据量啊。而且代码这部分不传输也是可以的,毕竟网络的两端代码这部分是需要预先协商好的。

  那既然序列化就是用来保存、传输对象的状态的,那为啥还要单独研究akka的序列化框架呢?因为我喜欢!啊哈哈,其实也不是。主要是akka的序列化是一个框架,而不是只单独的某个算法或jar包。因为在这个框架内,你可以单独制定每个class的序列化、反序列化对应的实现。

  记得之前在分析remote的时候,我们有谈到过序列化,下面还从这部分开始分析,因为这比较直接。如果上来就分析整个框架,比较容易迷失。

  上面是跨网咯传输数据的相关代码,很明显,这里调用了codec.constructMessage,生成了pdu,然后调用handle.write把数据发送出去了。

  def constructMessage(
localAddress: Address,
recipient: ActorRef,
serializedMessage: SerializedMessage,
senderOption: OptionVal[ActorRef],
seqOption: Option[SeqNo] = None,
ackOption: Option[Ack] = None): ByteString

  也就是说constructMessage把待发送的消息转化成了ByteString,这就是可以跨网络传输的二进制数据流。不过还调用了serializeMessage这个方法,把用户待发送的消息序列化了。其实我们应该特别注意这一点,为啥呢?因为这意味着用户待发送数据的序列化和系统消息的序列化分开了,或者说是两个层面的东西。啥意思呢?简单来说就是,akka先调用序列化方法(可以是任意自定义)把用户待发送数据转成ByteString,然后把这个数据设置成系统消息的某个字段,再调用系统序列化器把系统消息序列化。

override def constructMessage(
localAddress: Address,
recipient: ActorRef,
serializedMessage: SerializedMessage,
senderOption: OptionVal[ActorRef],
seqOption: Option[SeqNo] = None,
ackOption: Option[Ack] = None): ByteString = { val ackAndEnvelopeBuilder = AckAndEnvelopeContainer.newBuilder val envelopeBuilder = RemoteEnvelope.newBuilder envelopeBuilder.setRecipient(serializeActorRef(recipient.path.address, recipient))
senderOption match {
case OptionVal.Some(sender) ⇒ envelopeBuilder.setSender(serializeActorRef(localAddress, sender))
case OptionVal.None ⇒
} seqOption foreach { seq ⇒ envelopeBuilder.setSeq(seq.rawValue) }
ackOption foreach { ack ⇒ ackAndEnvelopeBuilder.setAck(ackBuilder(ack)) }
envelopeBuilder.setMessage(serializedMessage)
ackAndEnvelopeBuilder.setEnvelope(envelopeBuilder) ByteString.ByteString1C(ackAndEnvelopeBuilder.build.toByteArray) //Reuse Byte Array (naughty!)
}

  上面是constructMessage的具体实现,可看到envelopeBuilder.setMessage(serializedMessage)这行代码确实把序列化后的数据赋值给了某个字段。

public Builder setMessage(akka.remote.WireFormats.SerializedMessage value) {
if (messageBuilder_ == null) {
if (value == null) {
throw new NullPointerException();
}
message_ = value;
onChanged();
} else {
messageBuilder_.setMessage(value);
}
bitField0_ |= 0x00000002;
return this;
}

  这是setMessage的代码。需要特别注意的是它的参数是SerializedMessage类型,而不是ByteString。这意味着什么呢?这意味着,还有一层。

  目前来看,序列化的过程大概是上面这个样子。那上面的系统消息又是啥呢?我们来分析ackAndEnvelopeBuilder.build.toByteArray这段代码。首先来看build返回了什么。

public akka.remote.WireFormats.AckAndEnvelopeContainer build() {
akka.remote.WireFormats.AckAndEnvelopeContainer result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}

  它返回了一个AckAndEnvelopeContainer,那就可以再补充一下上面的图。

  下面是AckAndEnvelopeContainer的继承关系。

  其实AckAndEnvelopeContainer内部还有其他的对象,但也不需要再过多深入分析,因为他们的序列化方法都是一样的。

  从上面红框开始,相关的序列化好像都是akka框架自定义的,那么它又是如何支持用户自定义序列化方法的呢?答案就在前面的serializeMessage方法中。

private def serializeMessage(msg: Any): SerializedMessage = handle match {
case Some(h) ⇒
Serialization.currentTransportInformation.withValue(Serialization.Information(h.localAddress, extendedSystem)) {
MessageSerializer.serialize(extendedSystem, msg.asInstanceOf[AnyRef])
}
case None ⇒
throw new EndpointException("Internal error: No handle was present during serialization of outbound message.")
}

  很明显,最终调用了MessageSerializer.serialize把用户自定义消息转化成了SerializedMessage。那SerializedMessage是用来干啥的呢?其实直观上来说,它还是用来保存用户序列化后的数据的,跟前面的伎俩一样,把ByteString赋值给某个字段。目前猜测来看,的确是这样的,但又感觉那里不对。4

  从SerializedMessage的继承关系和字段来看,好像确实是这样的。但需要注意这个类又两个ByteString的字段,分别是message_ 和 messageManifest_。一个是序列化后的消息,一个是序列化后消息的声明。

/**
* Uses Akka Serialization for the specified ActorSystem to transform the given message to a MessageProtocol
* Throws `NotSerializableException` if serializer was not configured for the message type.
* Throws `MessageSerializer.SerializationException` if exception was thrown from `toBinary` of the
* serializer.
*/
def serialize(system: ExtendedActorSystem, message: AnyRef): SerializedMessage = {
val s = SerializationExtension(system)
val serializer = s.findSerializerFor(message)
val builder = SerializedMessage.newBuilder val oldInfo = Serialization.currentTransportInformation.value
try {
if (oldInfo eq null)
Serialization.currentTransportInformation.value = system.provider.serializationInformation builder.setMessage(ByteString.copyFrom(serializer.toBinary(message)))
builder.setSerializerId(serializer.identifier) val ms = Serializers.manifestFor(serializer, message)
if (ms.nonEmpty) builder.setMessageManifest(ByteString.copyFromUtf8(ms)) builder.build
} catch {
case NonFatal(e) ⇒
throw new SerializationException(s"Failed to serialize remote message [${message.getClass}] " +
s"using serializer [${serializer.getClass}].", e)
} finally Serialization.currentTransportInformation.value = oldInfo
}

  这两个字段先忽略,先来看serialize这个方法。需要注意这段代码builder对象的三个set方法。它分别设置了序列化后的消息、消息的声明,还有一个是序列化器的identifier。

  上面是Serializer的trait。抛开与序列化、反序列化相关的方法定义,第一个字段是一个identifier。从中文释义和官方注释来看,这是一个序列化器的数字标志,用以区分不同的序列化器。0~40是官方预留值,标志内部序列化用途,那为啥是40呢?你猜,哈哈哈,我也不知道,大概是官方觉得预留40个内部序列化器应该够用了吧。啥?如果不够用?鬼知道。

  那为什么每个序列化器都需要一个identifier呢?后面再说吧。

/**
* Returns whether this serializer needs a manifest in the fromBinary method
*/
def includeManifest: Boolean

  其实Serializer还有一个非常重要的方法:includeManifest。它标志当前序列化器是否包含消息声明。那这个消息声明具体有啥用呢?

def manifestFor(s: Serializer, message: AnyRef): String = s match {
case s2: SerializerWithStringManifest ⇒ s2.manifest(message)
case _ ⇒ if (s.includeManifest) message.getClass.getName else ""
}

  从获取消息声明的代码来看,它首先判断是否为SerializerWithStringManifest类型,如果是就调用manifest返回消息声明;否则就判断是否需要消息声明,如果需要则获取当前class的名称,否则返回空字符串。目前我们知道当前class的名称可以是消息的声明。那我们应该能大胆的猜测这个字段的意义了。

  它是用来给反序列化提供相关的参数或信息的。因为序列化后的对象都是二进制数据,在接收端怎么知道该用哪个序列化器进行反序列化呢?如果有这部分二进制数据对应的类名,那就可以从配置里面查找到了!!!这有什么意义呢?其实我们在开发系统的时候,一般都非常具有针对性,或者说不会考虑通用性。比如会在系统设计阶段,使用固定的序列化器,比如kryo,或Hessian。这就意味着我们已经提前知道了该用哪种序列化框架,不需要进行选择。拿过来的数据,用对应的序列化器反序列化就好了,不用担心格式不兼容!!!所以一般是没有manifest的。所以我觉得manifest是通用序列化框架的关键。

  其实SerializerWithStringManifest继承了Serializer,就是多提供了一个manifest方法。

  /**
* Return the manifest (type hint) that will be provided in the fromBinary method.
* Use `""` if manifest is not needed.
*/
def manifest(o: AnyRef): String

  就是返回给定消息的类型提示字符串。

  那么在序列化之前是如何知道改用那个序列化器呢?答案就在SerializationExtension.findSerializerFor这个方法上。

  /**
* Returns the Serializer configured for the given object, returns the NullSerializer if it's null.
*
* Throws akka.ConfigurationException if no `serialization-bindings` is configured for the
* class of the object.
*/
def findSerializerFor(o: AnyRef): Serializer =
if (o eq null) NullSerializer else serializerFor(o.getClass)
/**
* Returns the configured Serializer for the given Class. The configured Serializer
* is used if the configured class `isAssignableFrom` from the `clazz`, i.e.
* the configured class is a super class or implemented interface. In case of
* ambiguity it is primarily using the most specific configured class,
* and secondly the entry configured first.
*
* Throws java.io.NotSerializableException if no `serialization-bindings` is configured for the class.
*/
@throws(classOf[NotSerializableException])
def serializerFor(clazz: Class[_]): Serializer =
serializerMap.get(clazz) match {
case null ⇒ // bindings are ordered from most specific to least specific
def unique(possibilities: immutable.Seq[(Class[_], Serializer)]): Boolean =
possibilities.size == 1 ||
(possibilities forall (_._1 isAssignableFrom possibilities(0)._1)) ||
(possibilities forall (_._2 == possibilities(0)._2)) val ser = {
bindings.filter {
case (c, _) ⇒ c isAssignableFrom clazz
} match {
case immutable.Seq() ⇒
throw new NotSerializableException(s"No configured serialization-bindings for class [${clazz.getName}]")
case possibilities ⇒
if (unique(possibilities))
possibilities.head._2
else {
// give JavaSerializer lower priority if multiple serializers found
val possibilitiesWithoutJavaSerializer = possibilities.filter {
case (_, _: JavaSerializer) ⇒ false
case (_, _: DisabledJavaSerializer) ⇒ false
case _ ⇒ true
}
if (possibilitiesWithoutJavaSerializer.isEmpty) {
// shouldn't happen
throw new NotSerializableException(s"More than one JavaSerializer configured for class [${clazz.getName}]")
} if (!unique(possibilitiesWithoutJavaSerializer)) {
_log.warning(LogMarker.Security, "Multiple serializers found for [{}], choosing first of: [{}]",
clazz.getName,
possibilitiesWithoutJavaSerializer.map { case (_, s) ⇒ s.getClass.getName }.mkString(", "))
}
possibilitiesWithoutJavaSerializer.head._2 } }
} serializerMap.putIfAbsent(clazz, ser) match {
case null ⇒
if (shouldWarnAboutJavaSerializer(clazz, ser)) {
_log.warning(LogMarker.Security, "Using the default Java serializer for class [{}] which is not recommended because of " +
"performance implications. Use another serializer or disable this warning using the setting " +
"'akka.actor.warn-about-java-serializer-usage'", clazz.getName)
}
log.debug("Using serializer [{}] for message [{}]", ser.getClass.getName, clazz.getName)
ser
case some ⇒ some
}
case ser ⇒ ser
}

  简单来说这个方法就是从SerializationExtension这个扩展配置的序列化器中根据类的claz信息找到一个序列化器,如果找不到就招父类对应的序列化器,如果还找不到那就用Java默认的序列化器。

  其实序列化的过程可以简单总结为上面这样的流程图。下面我们简要分析一下反序列化的过程。

  上面是收到消息时的处理过程,很显然调用了tryDecodeMessageAndAck方法。

 private def tryDecodeMessageAndAck(pdu: ByteString): (Option[Ack], Option[Message]) = try {
codec.decodeMessage(pdu, provider, localAddress)
} catch {
case NonFatal(e) ⇒ throw new EndpointException("Error while decoding incoming Akka PDU", e)
}

  这个方法输入是一个ByteString,返回一个Tuple,第二个类型是Message。

override def decodeMessage(
raw: ByteString,
provider: RemoteActorRefProvider,
localAddress: Address): (Option[Ack], Option[Message]) = {
val ackAndEnvelope = AckAndEnvelopeContainer.parseFrom(raw.toArray) val ackOption = if (ackAndEnvelope.hasAck) {
import scala.collection.JavaConverters._
Some(Ack(SeqNo(ackAndEnvelope.getAck.getCumulativeAck), ackAndEnvelope.getAck.getNacksList.asScala.map(SeqNo(_)).toSet))
} else None val messageOption = if (ackAndEnvelope.hasEnvelope) {
val msgPdu = ackAndEnvelope.getEnvelope
Some(Message(
recipient = provider.resolveActorRefWithLocalAddress(msgPdu.getRecipient.getPath, localAddress),
recipientAddress = AddressFromURIString(msgPdu.getRecipient.getPath),
serializedMessage = msgPdu.getMessage,
senderOption =
if (msgPdu.hasSender) OptionVal(provider.resolveActorRefWithLocalAddress(msgPdu.getSender.getPath, localAddress))
else OptionVal.None,
seqOption =
if (msgPdu.hasSeq) Some(SeqNo(msgPdu.getSeq)) else None))
} else None (ackOption, messageOption)
}

  上面是decodeMessage的源码,第一行是用来反序列化的。

public static akka.remote.WireFormats.AckAndEnvelopeContainer parseFrom(byte[] data)
throws akka.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}

  它返回一个AckAndEnvelopeContainer对象。怎么样这个类型是不是比较熟悉?没错,在序列化的过程中,用户消息的最终序列化数据传给了这个对象,然后序列化发送出去的。

  后面的代码就是在解析AckAndEnvelopeContainer的其他字段了,比如Recipient/Sender/Seq等。其中我们最关心的是Message。还记得这个Message的类型是什么吗?没错就是SerializedMessage。

  不过上面的代码并没有对用户消息进行反序列化,而是调用msgDispatch.dispatch(msg.recipient, msg.recipientAddress, msg.serializedMessage, msg.senderOption)对消息进行处理。

  很显然,用户消息最终调用了MessageSerializer.deserialize进行反序列化。

/**
* Uses Akka Serialization for the specified ActorSystem to transform the given MessageProtocol to a message
*/
def deserialize(system: ExtendedActorSystem, messageProtocol: SerializedMessage): AnyRef = {
SerializationExtension(system).deserialize(
messageProtocol.getMessage.toByteArray,
messageProtocol.getSerializerId,
if (messageProtocol.hasMessageManifest) messageProtocol.getMessageManifest.toStringUtf8 else "").get
}

  它还是通过SerializationExtension这个扩展来反序列化的。同样有三个参数:message,serializerID,Manifest。

/**
* Deserializes the given array of bytes using the specified serializer id,
* using the optional type hint to the Serializer.
* Returns either the resulting object or an Exception if one was thrown.
*/
def deserialize(bytes: Array[Byte], serializerId: Int, manifest: String): Try[AnyRef] =
Try {
val serializer = try getSerializerById(serializerId) catch {
case _: NoSuchElementException ⇒ throw new NotSerializableException(
s"Cannot find serializer with id [$serializerId]. The most probable reason is that the configuration entry " +
"akka.actor.serializers is not in synch between the two systems.")
}
deserializeByteArray(bytes, serializer, manifest)
}

  首先根据serializerId找到本节点对应的序列化器,这意味着什么呢?这意味着所有的节点,相同的序列化器都必须具有相同的serializerId。这就是serializerId的意义。

private def deserializeByteArray(bytes: Array[Byte], serializer: Serializer, manifest: String): AnyRef = {

    @tailrec def updateCache(cache: Map[String, Option[Class[_]]], key: String, value: Option[Class[_]]): Boolean = {
manifestCache.compareAndSet(cache, cache.updated(key, value)) ||
updateCache(manifestCache.get, key, value) // recursive, try again
} withTransportInformation { () ⇒
serializer match {
case s2: SerializerWithStringManifest ⇒ s2.fromBinary(bytes, manifest)
case s1 ⇒
if (manifest == "")
s1.fromBinary(bytes, None)
else {
val cache = manifestCache.get
cache.get(manifest) match {
case Some(cachedClassManifest) ⇒ s1.fromBinary(bytes, cachedClassManifest)
case None ⇒
system.dynamicAccess.getClassFor[AnyRef](manifest) match {
case Success(classManifest) ⇒
val classManifestOption: Option[Class[_]] = Some(classManifest)
updateCache(cache, manifest, classManifestOption)
s1.fromBinary(bytes, classManifestOption)
case Failure(e) ⇒
throw new NotSerializableException(
s"Cannot find manifest class [$manifest] for serializer with id [${serializer.identifier}].")
}
}
}
}
}
}

  很明显,首先判断Serializer是不是SerializerWithStringManifest,如果是就传入发送过来的manifest;如果不是且manifest是空字符串,则调用fromBinary时,第二个参数为空(也就是manifest);如果manifest不为空,则从manifestCache中找到对应的manifest信息,传入fromBinary;如果manifestCache没找到,则动态加载manifest信息,更新manifestCache后再传入fromBinary。

  至此,反序列化过程结束。

  上面是我们总结的akka的序列化框架。消息的序列化可以分为三层:用户消息、用户消息序列化后对应的SerializedMessage、SerializedMessage对应的AckAndEnvelopeContainer。其中AckAndEnvelopeContainer的序列化是系统提供的,用户无法自定义;SerializedMessage是系统序列化和用户自定义序列化的中间层,提供一个适配的功能,比如提供序列化器的ID以及消息的声明信息;用户消息是基础层,可以使用自定义序列化器进行序列化。在反序列化时先反序列化城AckAndEnvelopeContainer,进行一些系统级的操作,然后根据SerializedMessage的序列化器ID查找对应的序列化器,反序列化用户消息。至此一个通用的序列化、反序列化流程设计完毕,而且还很通用的额。其实简单来说就是,解耦了“变”和“不变”。“变”是指用户的序列化器可能不变通,而且支持自定义;“不变”是指系统级消息的序列化过程不变,所有节点都一样。

  怎么样,你对akka设计的这个通用的序列化框架怎么看?是不是觉得挺好用的?我觉得这个分层设计的概念大家应该学会,这有助于我们照葫芦画瓢也设计一个类似的序列化框架。啥?你的序列化不需要通用?那你总会遇到schema变迁、升级的问题的。

Akka源码分析-Serialization的更多相关文章

  1. Akka源码分析-Akka Typed

    对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...

  2. Akka源码分析-Akka-Streams-概念入门

    今天我们来讲解akka-streams,这应该算akka框架下实现的一个很高级的工具.之前在学习akka streams的时候,我是觉得云里雾里的,感觉非常复杂,而且又难学,不过随着对akka源码的深 ...

  3. Akka源码分析-Cluster-Metrics

    一个应用软件维护的后期一定是要做监控,akka也不例外,它提供了集群模式下的度量扩展插件. 其实如果读者读过前面的系列文章的话,应该是能够自己写一个这样的监控工具的.简单来说就是创建一个actor,它 ...

  4. Akka源码分析-Cluster-Distributed Publish Subscribe in Cluster

    在ClusterClient源码分析中,我们知道,他是依托于“Distributed Publish Subscribe in Cluster”来实现消息的转发的,那本文就来分析一下Pub/Sub是如 ...

  5. Akka源码分析-Cluster-Singleton

    akka Cluster基本实现原理已经分析过,其实它就是在remote基础上添加了gossip协议,同步各个节点信息,使集群内各节点能够识别.在Cluster中可能会有一个特殊的节点,叫做单例节点. ...

  6. Akka源码分析-Persistence

    在学习akka过程中,我们了解了它的监督机制,会发现actor非常可靠,可以自动的恢复.但akka框架只会简单的创建新的actor,然后调用对应的生命周期函数,如果actor有状态需要回复,我们需要h ...

  7. Akka源码分析-local-DeathWatch

    生命周期监控,也就是死亡监控,是akka编程中常用的机制.比如我们有了某个actor的ActorRef之后,希望在该actor死亡之后收到响应的消息,此时我们就可以使用watch函数达到这一目的. c ...

  8. Akka源码分析-Cluster-ActorSystem

    前面几篇博客,我们依次介绍了local和remote的一些内容,其实再分析cluster就会简单很多,后面关于cluster的源码分析,能够省略的地方,就不再贴源码而是一句话带过了,如果有不理解的地方 ...

  9. Akka源码分析-Persistence-AtLeastOnceDelivery

    使用过akka的应该都知道,默认情况下,消息是按照最多一次发送的,也就是tell函数会尽量把消息发送出去,如果发送失败,不会重发.但有些业务场景,消息的发送需要满足最少一次,也就是至少要成功发送一次. ...

随机推荐

  1. 1370 - Bi-shoe and Phi-shoe(LightOJ1370)(数论基础,欧拉函数)

    http://lightoj.com/volume_showproblem.php?problem=1370 欧拉函数: 在数论,对正整数n,欧拉函数是少于或等于n的数中与n互质的数的数目. φ(n) ...

  2. Last Defence - UVA7045

    https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_probl ...

  3. 洛谷——P1608 路径统计

    P1608 路径统计 题目描述 “RP餐厅”的员工素质就是不一般,在齐刷刷的算出同一个电话号码之后,就准备让HZH,TZY去送快餐了,他们将自己居住的城市画了一张地图,已知在他们的地图上,有N个地方, ...

  4. hdu 4971

    记忆花搜索   dp #include <cstdio> #include <cstdlib> #include <cmath> #include <set& ...

  5. 将oracle10g 升级至10.2.0.4

    http://blog.csdn.net/launch_225/article/details/7221489 一.单实例环境,全时长一个半钟多. 详细图文说明到这下载 1.停止所有oracle相关进 ...

  6. [Javascript] Link to Other Objects through the JavaScript Prototype Chain

    Objects have the ability to use data and methods that other objects contain, as long as it lives on ...

  7. jquery全局变量---同步请求设置

    1.同步 $.ajaxSetup({ async: false }); 2.异步 $.ajaxSetup({   async: true   }); 3.说明:我们一般使用同步完要恢复异步.由于js默 ...

  8. 九度OJ1004 Median

    题目描写叙述: Given an increasing sequence S of N integers, the median is the number at the middle positio ...

  9. C语言 字符串操作 笔记

    /* C语言字符串的操作笔记 使用代码和注释结合方式记录 */ # include <stdio.h> # include <string.h> int main(void) ...

  10. How to Use SFTP ?

    Usage Build a SFTP session with your linux like server, e.g, by the tool "Xshell" or any y ...