问题复现
-
直接使用minio java sdk(8.3.3版本)对ceph集群中的compose桶中的已存在的多个文件(test1,test2)进行合并(test),代码如下
@Test void contextLoads() throws Exception{ MinioClient minioClient = MinioClient.builder() .endpoint("http://172.23.27.119:7480") .credentials("4S897Y9XN9DBR27LAI1L", "WmZ6JRoMNxmtE9WtXM9Jrz8BhEdZnwzzAYcE6b1z") .build(); composeObject(minioClient,"compose","compose",List.of("test1","test2"),"test"); } public boolean composeObject(MinioClient minioClient,String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size()); for (String chunk : chunkNames){ sourceObjectList.add( ComposeSource.builder() .bucket(chunkBucKetName) .object(chunk) .build() ); } minioClient.composeObject( ComposeObjectArgs.builder() .bucket(composeBucketName) .object(objectName) .sources(sourceObjectList) .build() ); return true; }
-
运行报错
报错为400,BadDigest
完全一脸懵逼o((⊙﹏⊙))o,说好的兼容S3呢?
问题排查
日志
-
第一点想到的就是查看rgw日志,看看究竟出错是什么原因
日志如下:(在此只截取了部分日志)
日志经过分析之后,之前的两次head request(获取test1和test2的元数据信息)以及init multipartUpload都是成功的,但是最终在第一次上传第一块的时候出现了错误(也就是test1),报了400的错误,op状态码为-2005,报了等于没有报错,这不搞笑吗?我咋知道-2005啥意思?
-
不行,去看看rgw源码中-2005究竟是啥(唉,笔者C++只知道一些语法,不太会,以下都是随机分析的结果,有点破案的感觉)
-
最终在rgw下面的
好家伙,终于找到2005这个错误码
成功了一半了,哈哈哈哈(现在回过头来看看还是自己太年轻了。。。。)
-
继续跟踪
ERR_BAD_DIGEST
,出现的位置最终在rgw_op.cc下面找到了藏身之处,看名字,嗯,应该是处理上传的代码,一共出现了4次,要排查的地方还不多,内心暗自高兴
-
首先看到了第三处和第四处,发现如果是这两处报错的,一定会有对应的日志输出,但是明显之前的rgw日志值没有对应的错误日志,因此排除
-
那么剩下的只剩下第一处和第二处,这两处代码几乎都是一致的,如下
if (supplied_md5_b64 && strcmp(calc_md5, supplied_md5)) { op_ret = -ERR_BAD_DIGEST; return; }
貌似是在对提供的md5(supplied,命名有点意思)进行校验,但是不太确定
-
随后我仔细看了一下两处所在对应的方法
-
可以很明显的看到第一处为Put,第二处为Post,而我的rgw日志是Put请求,因此锁定在第一处(就快要找到真相了)
![image-20211104145215146](https://img-blog.csdnimg.cn/img_convert/b5ce8c0abcbfd2853c85c60cadb9249d.png)
-
继续分析第一处的代码
发现这个东西
由于这几处都是出现在一起的,第一个是supplied_md5_b64,但是还是不太确定是不是`Content-MD5`,但是下面的几个和请求头中的非常像
![image-20211104145703581](https://img-blog.csdnimg.cn/img_convert/8c45dff24c0291946829de5962e4696a.png)
因此我就怀疑是请求头中的MD5了
对比日志
-
为了有对比参考的样本,和报错的rgw日志比较,我决定使用aws s3的sdk进行一次实验,原理和minio的一样,也是将compose中原本存在test1,test2进行合并,需要注意点就是partNumber是从1开始的,不是从0开始,从0开始会报错,别问我为啥知道,因为踩过坑了。。。。
@Test public void test1(){ String bucketName = "compose"; String keyName = "test"; AWSCredentials awsCredentials = new BasicAWSCredentials("4S897Y9XN9DBR27LAI1L","WmZ6JRoMNxmtE9WtXM9Jrz8BhEdZnwzzAYcE6b1z"); AmazonS3 s3Client = AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://172.23.27.119:7480","")) .withPathStyleAccessEnabled(true) .build(); InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest("compose","test"); InitiateMultipartUploadResult initResponse = s3Client.initiateMultipartUpload(initRequest); List<PartETag> partETags = new ArrayList<>(); List<String> list = List.of("test1","test2"); for (int i = 0; i < list.size(); i++) { CopyPartRequest request = new CopyPartRequest() .withDestinationBucketName(bucketName) .withPartNumber(i+1) .withUploadId(initResponse.getUploadId()) .withDestinationKey(keyName) .withSourceBucketName(bucketName) .withSourceKey(list.get(i)); partETags.add(s3Client.copyPart(request).getPartETag()); } CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(bucketName, keyName, initResponse.getUploadId(), partETags); s3Client.completeMultipartUpload(compRequest); }
-
日志如下
可以看到成功了,但是眼尖的我看到了不一样的地方
-
不同之处的查找
minio
aws
可以看到aws这里没有minio绿色箭头执行的日志,
-
再次查看rgw代码
可以看到在第一处之前确实有这行代码
到此为止可以确定一点,那就是minio那边在Put操作的时候携带了一个请求头,可能是md5
抓包进行分析
- 对minio的请求进行分析
确实有一个Content-MD5请求,突然间我发现这个md5好像有点眼熟,又经过一次不同的请求测试,发现这家伙没变!!
后来发现,这玩意儿是''
的md5之后进行base64的请求结果,难怪不同请求都一样,原来发送的body都是空,因此计算md5的时候才会有这东西
下面这个x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
,也是代表发送的body为空,难怪每次都一致
原因分析
minio每次发送Put请求,为了数据安全都会加上md5的请求头,但是composeObject该动作本身上传的body就是空的(本质是复制copyPart操作),因此本地的请求body为空,因此md5值与ceph在接收到该md5与自己计算出来的md5值不一致,那么就导致了合并失败。
验证
minio代码验证
可以看到minio在构造请求的时候,始终会带上md5
修改源码 (对S3Base.java进行修改)
为了将md5值不强制发送,这两处进行注释掉之后再进行composeObject,最终测试结果成功!
总结
说实话,这是第一次排查过程我感觉自己成长了很多,也算是第一次这么仔细地看源码吧,也是真的就是在源码面前,一切bug都能够找到原因。
总之能够找到原因还是非常高兴的。
不过仔细想想也没有错,minio和ceph两者是竞争关系,你用minio的sdk去操作ceph,难免会出现bug,为啥我要贴合你呢?我有我的规则你有你的规则