1 前言
在放假之前搞定这个问题,回家也安心了,感谢同事的帮忙
2 现象描述
这段异常在一次上线之后,经常出现。但是在上线之前,测试环境中没有出现。
java.io.EOFException
at java.io.DataInputStream.readUnsignedShort(DataInputStream.java:323)
at java.io.ObjectInputStream$BlockDataInputStream.readUnsignedShort(ObjectInputStream.java:2763)
at java.io.ObjectInputStream$BlockDataInputStream.readUTF(ObjectInputStream.java:2819)
at java.io.ObjectInputStream.readUTF(ObjectInputStream.java:1050)
at com.netease.xxx.po.XXXAdaptor.readExternal(XXXAdaptor.java:622)
at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:1791)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
...............
at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:1791)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
at org.jboss.netty.handler.codec.serialization.ObjectDecoder.decode(ObjectDecoder.java:129)
at org.jboss.netty.handler.codec.frame.FrameDecoder.callDecode(FrameDecoder.java:282)
at org.jboss.netty.handler.codec.frame.FrameDecoder.messageReceived(FrameDecoder.java:216)
XXXAdaptor是项目中一个rpc传输的po对象,实现了Externalizable接口,能够保证序列化的版本兼容。版本兼容的实现方案是参考protocol buffer的方式,新增的属性直接放到一个hashmap中,序列化传输的时候遍历map中的元素,这样就可以无限制的添加新的字段。
出问题的那一行代码:someField.readUTF(); 每次rpc调用,会把同时所有rpc server 节点都请求一次,每个请求是由一个线程完成的,请求的参数是线程共享的。
3 第一次解决尝试
刚在线上发现这个问题以为是因为网络原因导致的,在网上查的资料说是因为流数据已经读完,就会抛出EOFException,是正常情况。在测试环境上没有出现,只在正式环境中出现,这种情况肯定是不正常。难道是网络数据没有发送完,那Netty的ObjectEncoder和ObjectDecorder肯定会出现异常,抱着试一试的想法,在XXXAdaptor.writeExternal方法结束的地方调用ObjectOutput.flush(),结果是。。。果然没有效果
4 第二次尝试
在反序列的代码中加上异常捕获,XXXAdaptor.readExternal反序列化hashmap的代码块中捕获异常,并且把hashmap中的entry总数打印出来,抛出异常的时候,已经遍历的entry index也输出。
上线之后找出眉目了:size=2,index=1。在序列化中,hashmap只有一对key-value,size怎么回变成2 ???!
我们重新走查了一边XXXAdaptor.writeExternal方法,发现有一句话很可疑:
hashmap.put(“someKey”,”someValue”);
一般的rpc调用情况下,这句话没有任何问题,在线上环境,这个服务有n个节点,每次调用,rpc框架在客户端都会启动n个线程来调用服务节点,每个线程都会对这个对象进行序列化,XXXAdaptor.writeExternal方法被多个线程调用,在没有保证每个线程一个XXXAdaptor clone实例的情况下,hashmap.put(“someKey”,”someValue”)实际上是被多线程操作---这是一个很悲催的thread race condition。而在测试环境中,只有一个服务节点,肯定不会出现这种情况。
用测试代码连接线上服务节点,调用只读操作,debug模式下,果然出现hashmap size=2,内部的table数组只有一对key-value,但是hashmap toString出来的数据是2对一样的key-value(这是偶然情况,还有可能出现size=2,其他数据都正常的情况)。至于hashmap多线程写操作导致的情况,网络上有很多具体的描述,严重的例如cpu load 100%,死循环。我的这段代码是多线程对同一个key多次put操作引起的,不会出现死循环,这是不幸中的万幸。
故在发序列化代码中,捕获忽略EOFException就可以解决这个问题。
5 最终解决
将Hashmap类型属性改为ConcurrentMap,put操作使用putIfAbsent代替。
项目中使用的rpc框架底层使用了多线程,但是在上层代码中并不能体现出来,为使用人员屏蔽了细节,但是没有却不能保证100%的可靠,这才悲催。。。有失败就会有进步!
参考
http://docs.jboss.org/process-guide/en/html/serialization.html jboss在序列化过程中对不同版本对象的兼容性实现的方式:也是捕获忽略EOFException