被开源组件坑惨了,文件上传到MFS后MD5不一致

原创 吴就业 185 0 2022-12-10

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/7690437352104ce38555c99b8dd55e53

作者:吴就业
链接:https://wujiuye.com/article/7690437352104ce38555c99b8dd55e53
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

问题背景

我们自研的一款文件上传中间件,用于使用统一的上传协议支持客户端将文件上传到s3/ceph/mfs/oss/obs/minio等文件系统。其中一个很大的亮点,就是云原生的架构设计,对于上传文件到公司内部自己搭建的moosefs、cephfs文件系统,不再需要通过挂盘写,而是通过api操作将计算与存储分离。为了实现这一目标,费了很大劲。

由于moosefs没有关于底层自定义二进制通信协议的文档,且各大版本api上差异很大,幸运的是,我们在github上找到一个go语言实现api操作moosefs的开源库,但不幸的是,这个组件实现的api版本很低,并且调研阶段写demo的过程中就发现很多bug。但为了实现去挂盘提升稳定性的目标,也尝试去看moosefs的源码,从源码中抠出协议(新旧版本差异的地方),终于moosefs-client-go组件开发完成。

在测试阶段,我们发现通过这个中间件上传文件到moosefs后,moosefs上存储的文件的md5与本地原文件md5不一致,如果是图片,能很明显的看出少了一块像素。

MooseFS文件系统简介

简单介绍一下moosefs(mfs)文件系统,便于理解后续内容。

moosefs架构中的两个重要角色(moosefs-client-go需要实现与这两个角色交互):

mosefs将一个文件分成多个chunk(块)存储,一个chunk又分成多个block(段/小块),一个chunk的大小是64m,一个block的大小是65536字节。一个文件分多少个chunk存储,上传就需要写多少个chunk,一般上传的文件都很小,只需要写一个chunk。

上传过程先是通过master调度创建chunk,master返回chunk所在的chunk server节点(包括副本所在节点), 可从返回的chunk server节点中选择其中一个节点读/写chunk,写chunk时需要带上chunk所有节点信息,用于chunk server当前节点同步复制给其它chunk server节点。

写数据到Chunk Server以及副本同步的流程如下图所示。

img

文件上传后MD5不一致Bug原因分析

在测试阶段,我们通过调小数据包后发现没有再复现,但实际并没有修复问题,只是将概率调小了,所以很难再复现。

后面由于测试环境换了个mfs集群做测试,chunk server节点数比较多,配置的文件副本数是3, 而早期使用的moosefs集群,配置的文件副本数是1,也就是不需要copy。所以出现的概率比之前大了,很容易复现。 当文件副本数是3的时候,通过观察下载日记发现,多次下载每次下载的文件md5都是不一致的。

通过写测试用例分别从3个节点读某个出问题的文件,发现有一个节点的文件md5是正确的,另外两个是错误的(这是很关键的一步发现),说明是上传的问题。但通过抓包分析发送出去的数据包未发现问题,可以确定是发生在moosefs的内部逻辑, 只能是寄希望通过看moosefs的c++源码能够发现问题。

img

与chunk server交互传输数据的顺序是:

如果是发送CUTOCS_WRITE命令后的第一次写(CUTOCS_WRITE_DATA),moosefs会响应两次cmd=CSTOCL_WRITE_STATUS的数据包,如果wid=0(wid=写ID,可以理解为请求ID),说明后面还有一个数据包待接收。 (这是非常关键的一步发现)

开源组件漏了这一步(可能是BUG,也可能是版本差异),也就是没等待第二次真正的写成功的响应,就继续发送CLTOCS_WRITE_FINISH完成命令了。 由于CLTOCS_WRITE_FINISH无响应,不需要读响应,所以很难发现还有数据包未被接收。

因为写完成命令会中断过程释放资源,猜测是提前中断释放资源导致没写完、以及副本没copy完(例如,需要拷贝到另外两个节点,只完成了其中一个节点的拷贝,还有一个节点未拷贝)。

这样猜测的原因是,当我尝试重试写一次的时候,发现没问题了,因为在等待重试的响应时,接收到了wid等于前一次的wid(实际是前一次的响应,但以为是重试后的响应), 此时才会发送CLTOCS_WRITE_FINISH完成命令。 (这也是为什么循环多次写的时候,发现第一次写返回的wid是0,而后面每次写返回的wid都是小1的(一期排查过期中发现的))

一期通过调小数据包后没复现,是因为一次写的数据小了,写数据快了,并且副本数是1不需要copy,中断过程之前就完成写,所以概率变得非常小,200的并发,才没有再复现。

总结

这Bug难在对mfs通信协议的不了解,以及工作原理的不理解。在不了解协议的情况下,抓包分析只能看我们传给mfs的数据有没有问题,并且由于是二进制协议,不是简单的请求响应,很难想到发一条命令会有两次响应。而在不了解内部逻辑的情况下,也不知道不等待响应后再调用结束命令会发生什么。moosefs官方不提供sdk,是因为推荐使用挂盘的方式使用,所以也不会有协议文档。

在发现关键现象时,通过猜测+证实的方式去探测结论能够快速发现找到问题,而所有猜测都被结论否定时,就需要花时间耐心的学习研究底层逻辑,可能无法从中直接发现问题,但能提供更多排查问题的思路。

#中间件

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

组件和框架初始化顺序背后隐藏的线上故障

一个微服务可能引入非常多的SDK,例如消息中间件kafka的组件、RPC框架dubbo、定时任务调度平台xxl-job的组件,以及提供web服务的jetty/tomcat等。这些组件的初始化是不确定的,那么假如启动初始化过程中,其中某个组件初始化失败了,会发生什么?

S3文件上传403问题排查

业务反馈有个iOS设备上传出现403问题,打点日记只能看到403,没有详细的错误信息。开了s3的日记后,也只是看到403 AccessDenied。

Go调用Lua性能压测与调优

基于go提供的基准测试能力编写并发测试用例,为排除脚本本身的性能影响,脚本只实现简单的逻辑,并实现预编译。通过调整虚拟机池策略、cpu数、并行度等,输出调用lua脚本的平均耗时、占用的内存。

基于dubbo-go二次开发荔枝RPC框架

本文介绍如何基于dubbo-go的扩展点,二次开发支持公司内部rpc协议,支持java项目和go项目的互相调用。

如何开发一个Java微服务项目脚手架

如果没有脚手架,每当需要创建一个新的project,我们通常会选择基于现有的project复制一份,然后修改修改。

Dubbo支持自适应等待无损下线

无损上下线是服务治理不可忽视的问题,在应⽤上下线发布过程中,如果上下线不平滑,就会出现短时间的服务调⽤报错,如连接被拒绝(`Connection refused`)、请求超时或请求异常。