Akka源码分析-Serialization
今天我们来谈一下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的更多相关文章
- Akka源码分析-Akka Typed
对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...
- Akka源码分析-Akka-Streams-概念入门
今天我们来讲解akka-streams,这应该算akka框架下实现的一个很高级的工具.之前在学习akka streams的时候,我是觉得云里雾里的,感觉非常复杂,而且又难学,不过随着对akka源码的深 ...
- Akka源码分析-Cluster-Metrics
一个应用软件维护的后期一定是要做监控,akka也不例外,它提供了集群模式下的度量扩展插件. 其实如果读者读过前面的系列文章的话,应该是能够自己写一个这样的监控工具的.简单来说就是创建一个actor,它 ...
- Akka源码分析-Cluster-Distributed Publish Subscribe in Cluster
在ClusterClient源码分析中,我们知道,他是依托于“Distributed Publish Subscribe in Cluster”来实现消息的转发的,那本文就来分析一下Pub/Sub是如 ...
- Akka源码分析-Cluster-Singleton
akka Cluster基本实现原理已经分析过,其实它就是在remote基础上添加了gossip协议,同步各个节点信息,使集群内各节点能够识别.在Cluster中可能会有一个特殊的节点,叫做单例节点. ...
- Akka源码分析-Persistence
在学习akka过程中,我们了解了它的监督机制,会发现actor非常可靠,可以自动的恢复.但akka框架只会简单的创建新的actor,然后调用对应的生命周期函数,如果actor有状态需要回复,我们需要h ...
- Akka源码分析-local-DeathWatch
生命周期监控,也就是死亡监控,是akka编程中常用的机制.比如我们有了某个actor的ActorRef之后,希望在该actor死亡之后收到响应的消息,此时我们就可以使用watch函数达到这一目的. c ...
- Akka源码分析-Cluster-ActorSystem
前面几篇博客,我们依次介绍了local和remote的一些内容,其实再分析cluster就会简单很多,后面关于cluster的源码分析,能够省略的地方,就不再贴源码而是一句话带过了,如果有不理解的地方 ...
- Akka源码分析-Persistence-AtLeastOnceDelivery
使用过akka的应该都知道,默认情况下,消息是按照最多一次发送的,也就是tell函数会尽量把消息发送出去,如果发送失败,不会重发.但有些业务场景,消息的发送需要满足最少一次,也就是至少要成功发送一次. ...
随机推荐
- 1370 - Bi-shoe and Phi-shoe(LightOJ1370)(数论基础,欧拉函数)
http://lightoj.com/volume_showproblem.php?problem=1370 欧拉函数: 在数论,对正整数n,欧拉函数是少于或等于n的数中与n互质的数的数目. φ(n) ...
- Last Defence - UVA7045
https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_probl ...
- 洛谷——P1608 路径统计
P1608 路径统计 题目描述 “RP餐厅”的员工素质就是不一般,在齐刷刷的算出同一个电话号码之后,就准备让HZH,TZY去送快餐了,他们将自己居住的城市画了一张地图,已知在他们的地图上,有N个地方, ...
- hdu 4971
记忆花搜索 dp #include <cstdio> #include <cstdlib> #include <cmath> #include <set& ...
- 将oracle10g 升级至10.2.0.4
http://blog.csdn.net/launch_225/article/details/7221489 一.单实例环境,全时长一个半钟多. 详细图文说明到这下载 1.停止所有oracle相关进 ...
- [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 ...
- jquery全局变量---同步请求设置
1.同步 $.ajaxSetup({ async: false }); 2.异步 $.ajaxSetup({ async: true }); 3.说明:我们一般使用同步完要恢复异步.由于js默 ...
- 九度OJ1004 Median
题目描写叙述: Given an increasing sequence S of N integers, the median is the number at the middle positio ...
- C语言 字符串操作 笔记
/* C语言字符串的操作笔记 使用代码和注释结合方式记录 */ # include <stdio.h> # include <string.h> int main(void) ...
- How to Use SFTP ?
Usage Build a SFTP session with your linux like server, e.g, by the tool "Xshell" or any y ...