Ceph蹚坑笔记 - (2)
大数据量引发的LevelDB问题
在使用Ceph RGW 0.80时,由于还没有Bucket Index Sharding机制,一个Bucket的Index只会存放在一个对象的omap中,而omap是存放在OSD的LevelDB中的。所以,当一个Bucket存放了大量对象(比如几千万上亿),omap中会有大量记录,即其所在OSD的LevelDB中的数据量也大增。在日常使用时,并未发现什么问题。问题出在OSD意外终止后的重启过程。我们注意到,在OSD启动过程中,CPU占用率很高,大量内存(包括swap)也被消耗,要经过几个小时,OSD才会启动完毕。我们用pstack观察,发现整个漫长的过程中OSD主要是在对其LevelDB进行recover和compact操作。在Ceph邮件组的讨论中,Sage提到这个可能是因为早期的LevelDB存在bug,导致LevelDB一直没有进行必要的compact。
具体原因,我们还未细究,目前使用Ceph 0.94的Bucket Index Sharding功能和RocksDB之后,未再发现该问题。
Bucket Index引发的瓶颈问题
当外部向一个Bucket上传一个对象时,RGW执行的步骤大致为:
1. 在对应的Bucket Index写上一条pending的entry(如果开启datalog的话,还要写一条pending的日志)
2. 存入用户数据
3. 把刚才的pending的entry改为complete(如果开启datalog的话,还要写一条complete的日志)
所以,如果对象不大的话,Bucket Index上所承受的IOPS甚至远超用于写入用户数据的IOPS。
在Ceph RGW 0.80里,由于一个Bucket的Index只存在一个对象中(一个对象只会在一个PG中,对一个对象进行修改时需要相应锁PG,FileStore中对同一文件的操作也是串行的)。于是,当对一个Bucket进行并发修改时,Bucket Index就成了瓶颈。这种瓶颈在recovery/backfill的时候表现得尤其显著。当一个对象在被recovery/backfill时,所有对该对象的读写都将被挡住。所以,Ceph RGW 0.80中,当一个Bucket Index对象正处于recovery/backfill过程中时,对该Bucket的写将被暂时挂起,等到Bucket Index对象的recovery/backfill完成后,才会被执行。由于客户端一般都有超时设置,而且可能不太长,但Ceph RGW 0.80中大的Bucket Index对象可能需要很长的时间才能完成recovery/backfill,所以在客户端看来,操作在一段时间内就是一直都超时失败。
Ceph RGW 0.94中引入了Bucket Index Sharding的机制(把原来一个Bucket Index对象切成多个)。通过配置较大的shard数(rgw_override_bucket_index_max_shards
),Bucket Index的瓶颈效应的到了很大改善。
应当注意的是,较大的shard数会有一个副作用,那就是降低List操作的性能。这时,可以适当调大rgw_bucket_index_max_aio
来降低List的相应时间。在Bucket Index Sharding的情况下,List操作本质上是向多个shard发出子List请求,把各个shard返回的有序列表进行归并排序后再返回给客户端。调大这个rgw_bucket_index_max_aio
意味着同时向更多的shard发出子List请求,从而降低总时间。当然,也不能因为有rgw_bucket_index_max_aio
而无顾忌地使用太大的rgw_override_bucket_index_max_shards
,因为shard数越多,List操作中的归并排序所需要的计算量和缓存也会大为增加。
分片上传导致Bucket List缓慢的问题
用S3接口对一个有大量对象的Bucket进行List操作时,一般是先调一次API列出几百调记录,然后在根据上一次API调用返回的marker继续列出后续的记录,如此反复直到Bucket中的所有对象被穷举完。
有一次,对一个大Bucket进行这样的操作时,我们注意到,前几次API调用都很快返回,然后中间有一次调用却耗时非常久,其后的API调用又恢复了较快返回。
从RGW的日志中发现,在执行耗时很久的那个操作时,RGW在过滤Bucket Index中的几万条以_multipart
为前缀的记录。原来Bucket Index不只存放着用户可见的对象的列表(直接以对象名为key),对于未完成的分片上传的每一个分片也保留了记录(它们的key是以_multipart
为前缀)。这些key在Bucket Index中是按字符串升序排列的。ASCII的数字和大写字母排在_
之前,小写字母排在_
之后。所以,前几个API调用还没有遇到_multipart
,RGW只需要把Bucket Index中的entry读出直接返回个客户段即可。但当遇到以_multipart
为前缀的key时,RGW就得逐一过滤掉这样的对象,这也就是那个耗时最久的操作所做的事。在走过所有_multipart
为前缀的记录后,RGW的逻辑就又恢复到了以前的模式,所以相应速度又变快了。
所以,对于中断的分片上传,应当及时调用AbortMultipart操作,否则日积月累会严重影响List的速度。当然,RGW应该也有优化的空间,比如见到_multipart
前缀时,直接换一个能够直接跳过所有_multipart
前缀的marker,这样就不必一条一条过滤了。
非法字符导致Bucket List失败的问题
s3cmd ls bucket-x
和s3 -u list bucket-x
出错,报XML解析失败,但使用curl发出同样的请求,却可以收到HTTP 200的返回。通过查看curl所返回的XML发现,有的对象名含有
(对应ASCII中的退格键)。原来%08在URL中是合法的,但根据https://www.w3.org/TR/REC-xml/#charsets 退格键不是XML中的合法字符。可见,RGW没有根据XML的标准对上传对象的名字进行校验,导致某些客户端能够使用含有非法字符的对象名上传对象。而且,这样的对象也无法用s3cmd
或s3
命令删除,因为它们都会在发出HTTP请求前把输入的非法字符替换掉。一个简单的清理方法是使用curl构造S3请求进行删除。以下是个例子:
host=127.0.0.1:7480
# %08 is backspace
file=abc%08def
bucket=bucket-x
resource="/${bucket}/${file}"
contentType="text/plain"
dateValue=`date -R -u`
stringToSign="DELETE
${contentType}
${dateValue}
${resource}"
s3Key=aaaaa
s3Secret=bbbbb
signature=`/bin/echo -n "$stringToSign" | openssl sha1 -hmac ${s3Secret} -binary | base64`
curl -X DELETE \
-H "Date: ${dateValue}" \
-H "Content-Type: ${contentType}" \
-H "Authorization: AWS ${s3Key}:${signature}" \
http://${host}/${bucket}/$file
当然,如果RGW返回的是JSON格式而不是XML,就不会遇到该问题。但RGW没有配置选项能够使其从XML切换到JSON的选项。从代码观察,对XML的使用是hard-code的,那么JSON格式的逻辑可能也缺乏测试。所以从稳定性角度考虑,目前也不太适合直接使用JSON格式。