场景:图片迁移用到fastdfs。为了防止多次创建服务器链接,使用静态代码块。在服务启动时加载配置,创建与服务器连接。
后来在程序运行过程中偶尔会抛出Read timed out 和NullPointerException。前者是等待服务器返回数据超时,后者是对象为空调用了close方法。
在超时情况下观察nginx日志发现该请求执行了50.39秒,返回错误码为499(client has closed connection)表示此链接已经建立,nginx在想给客户端发送数据时发现客户端已经关闭了链接。查看服务器图片已经存在。那么猜想既然是超时那么就有两种情况:连接超时和数据读写超时。499表示客户端已经主动关闭了链接,那么就考虑代码里设置的超时时间是多少。发现都是50秒,上传一张图片顶多几十毫秒不可能因超时而关闭关闭连接,猜想可能是程序主动关闭了此链接。而关于关闭连接的操作自己不可能写。看fastdfs源码发现,其不支持多线程上传。多图片上传时会使用一个stock,在一张图片上传成功后程序关闭了这个stock,fastdfs服务端想反回数据发现没有stock,所以会返回499,客户端主动关闭了连接。既然关闭了链接那么空指针也可能就是因为关闭这个连接导致的
源码跟进:
第一步:ClientGlobal.initByProperties(CONFIG_FILENAME);
这一行主要作用是读取配置文件。统计所有trackerServer服务器,为以后创建socket做准备。下面是源码主要部分:
第二步:获取调度服务器客户端
源码:new TrackerClient();
第三步:实例化TrackerServer
与其说实例化不如说是与服务器创建socket链接。
源码:
第四步:创建java存储客户端
// 声明一个StorageServer对象,null
StorageServer storageServer = null;
// 获得StorageClient1对象
new StorageClient1(trackerServer, storageServer);
并发原因:
到此为止,我们已经大致了解了fastdfs初始化以及创建连接情况。这个时候我们调用StorageClient.upload()是可以成功上传的,似乎已经完成了全部工作。但是刚刚报的读取超时和空指针又从何而来?为什么会偶尔发生?
如果细心我们会发现第四步传入的StorageServer是一个null。那么这个对象是什么时候实例化的。我们并没有做任何事情除了upload()方法。似乎这一切都指向了这个方法。
源码:
看到这三段主要代码一切问题都解开了。
原来在每次调用上传方法时如果没有StorageServer都会创建一个新的。创建方法也很简单,就是根据ip和端口号创建的。这样才使我们每次上传都能成功。
出现空指针和读取超时也正是因为这种做法。注意上边最后一个图,在完成所有操作后需要关闭StorageServer。但是此步操作和创建新socket没有加锁,导致这样一种情况:
两个线程同时同步上传,A先进来,发现StorageServer没有,创建了新连接,但是还没有完成上传时B抢到了资源,B一看本来就有StorageServer,于是没有创建新的连接。接着A执行完上传,并关闭了连接,而B已经连上服务器但是还没有上传完成,一直等待,但是A关闭了迟迟没有返回数据给B,那么B就抛出读取超时异常。如果A传完了,B也传完了,A先把连接关闭了,没事,等到B也要关闭连接时发现StorageServer为null,此时就抛出空指针异常。
解决:
每次都创建一个新的socket。
注意这个new StorageClient1(trackerServer, storageServer);必须使用new 不能使用全局变量。不然使用的还是一个StorageServer。