Ceph蹚坑笔记 - (3)

时间:2021-11-25 12:38:19

Ceph蹚坑笔记 - (3)

分片上传中的数据丢失问题

该问题的发生过程如下
1. InitMultipart发起一次分片上传,获得相应的Upload ID
2. 逐片上传数据
3. 发出CompleteMultipart请求,但请求超时(客户端设置30秒超时)
4. 再次用相同Upload ID重试CompleteMultipart请求,返回成功
5. 下载对象以确认上传成功
6. 两小时后再次下载该对象,收到HTTP 404错误
该问题的原因是:步骤3和步骤4的操作由于线程调度的原因几乎同时被RGW执行,结果步骤4把步骤2上传的分片进行逻辑删除,即放入GC列表(发生在RGWRados::Object::complete_atomic_modification()),两个小时后GC线程对这些分片进行物理删除。
一个RGW对象所对应的分片存放在其head对象的manifest的结构中。为了处理数据覆盖的情况,RGWRados::Object::complete_atomic_modification()会把旧的manifest取出来,把里面列出的分片全部逻辑删掉,然后在把manifest指向新的分片。当步骤3调用完RGWRados::Object::complete_atomic_modification()之后,步骤4立即调用RGWRados::Object::complete_atomic_modification()。所以,在步骤4看来老的manifest和新的manifest都指向相同的分片。但按照固有的逻辑,步骤4的RGWRados::Object::complete_atomic_modification()却把老的manifest中所记录的分片逻辑删除了,实际上就是把自己的分片逻辑删除了。
当然步骤4不是每次都能重现的,因为如果步骤4执行得太晚的话,Upload ID已经被步骤3标记为失效,那么RGW会直接拒绝掉步骤4的请求,从而不会触发竞争条件。
从客户端角度,最好能为CompleteMultipart设置较长的超时时间,以免短促的重试会破坏数据。
从RGW这一侧,可做的改进是在RGWRados::Object::complete_atomic_modification()检查新旧manifest是否指向相同的分片。如果发现这种情况,就应当跳过删除分片的步骤。

分片上传中的存储空间泄漏问题

在使用某Upload ID上传了某个对象的若干分片之后,但在执行CompleteMultipart之前,用户可以使用同一Upload ID再次上传已经上传过的的任何一个分片。这样的操作会有可能导致存储空间的泄漏。

这里,以8MB为一个分片大小,上传10MB文件的第一个分片。RGW将其切成两个4MB的对象,存入RGW所配置的Pool里。2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0是Upload ID,紧跟其后的.1表示第一个分片

[root@ceph-dev ~]# rados -p .za.rgw.buckets ls 2>/dev/null
za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1
za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1

再次上传第一个分片。此时,可以看到za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1消失,出现了包含jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay的对象名。这是因为RGWPutObj::execute()会判断put_data_and_throttle()的返回值,如果是-EEXIST,就会做出上述动作。就是说,如果一个分片被第二次上传时,put_data_and_throttle()会发现第一次上传的za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1已经存在,返回-EEXIST错误。这时,RGWPutObj::execute()会把za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1删掉,再生成一个随机字符串jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay,然后用这个字符串代替Upload ID,以za.196717.1__multipart_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1za.196717.1__shadow_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1_1为对象名向Pool里写入数据。但显然,这里忘记删除za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1了。

[root@ceph-dev ~]# rados -p .za.rgw.buckets ls 2>/dev/null
za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1
za.196717.1__shadow_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1_1
za.196717.1__multipart_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1

第3次上传第一个分片。此时,由于za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1不存在了,所以put_data_and_throttle()成功写入,那么它的行为和第1次上传时是一样的。但是,第二次上传的za.196717.1__shadow_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1_1za.196717.1__multipart_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1残留下来了。

[root@ceph-dev ~]# rados -p .za.rgw.buckets ls 2>/dev/null
za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1
za.196717.1__shadow_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1_1
za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1
za.196717.1__multipart_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1

第4次上传第一个分片。这时,行为与第二次上传一样,za.196717.1__multipart_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1被清理掉,但za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1残留下来。

[root@ceph-dev ~]# rados -p .za.rgw.buckets ls 2>/dev/null
za.196717.1__multipart_10MB.bin.a.LMfvR4p8TF1JNdiNOkvUwFprauLadFG.1
za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1
za.196717.1__shadow_10MB.bin.a.LMfvR4p8TF1JNdiNOkvUwFprauLadFG.1_1
za.196717.1__shadow_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1_1
za.196717.1__multipart_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1

最后,把2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0这个Upload ID对应的分片上传Abort掉,但以下文件并没有被清理掉,因而造成了存储泄漏。

[root@ceph-dev ~]# rados -p .za.rgw.buckets ls 2>/dev/null
za.196717.1__shadow_10MB.bin.a.2~4KTKIzu7uYPV2xapDruHK8t_mXf0Kh0.1_1
za.196717.1__shadow_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1_1
za.196717.1__multipart_10MB.bin.a.jcnZC7Qg-Bb1j-xQpB_gviDe63x7uay.1

在新的radosgw-admin中提供了新命令orphans find可以找到这些泄漏的对象。但 orphans find的必须扫描所有的Bucket Index对象里的记录,并列出.za.rgw.buckets中的所有对象,通过两个列表的比较找到没有被Bucket Index中记录的对象,这些对象就是orphans。当存量数据很大时,其时间复杂度和空间复杂度可想而知。
RGW-EEXIST这种情况做这样的特殊处理,可能是为了确保多客户端使用同一Upload ID上传同一分片时,不至于在分片的内部出现数据交叉(比如同一分片的前100字节来自客户端1,接下来的100字节来自客户端2)。但感觉这种功能的用途不太大,有点多余。

Bucket删除操作的存储空间泄漏问题

RGW会拒绝对非空Bucket的删除。但这里的“空”仅是说,对Bucket做List操作时,里面没有对象,并不考虑还未完成的分片上传(还没执行CompleteMultipart)。
创建一个Bucket时,会有一个RGWBucketInfo(对象名是.bucket.meta.前缀拼上Bucket名和ID)和一个RGWBucketEntryPoint(对象名就是Bucket名)创建在domain_root Pool中,一个(不分shard)或多个(分shard)Bucket Index对象创建在index_pool Pool中(对象名是前缀是dir拼上Bucket ID再拼上shard编号)。当删除一个Bucket时,RGW仅是删除那个Bucket的RGWBucketEntryPoint对象(这是0.94的行为,新的版本中也删除RGWBucketInfo对象),至于Bucket Index以及未完成的分片上传的分片数据是不做处理的。这些孤儿分片所占用的存储空间就泄漏掉了。
如上节所述,orphans find可以用很高的时间复杂度和空间复杂度吧这些泄漏的孤儿分片清理掉。
鉴于Bucket Index并未被删除,一种更高效的方法就是把已经被删除的Bucket的Bucket Index对象读取出来(至少目前的RGW实现没有删除Bucket Index对象),从中获取未完成的分片上传的Upload ID列表,然后模拟AbortMultipart功能,将这些孤儿分片清理掉,最后在把这些无用的Bucket Index对象也删除掉。