背景
compression是基于Placement Targets的,是其中的一个配置项。
Placement Targets
我们知道,在默认情况下,bucket index、bi log是存储在
pool中的,object data是存储在
pool中的,这其实是由配置在zone上的default-placement决定的。
Placement Targets可以让radosgw同时拥有多组不同的数据pool。用户上传数据时,可以选择不同Placement Targets,将数据存储到不同的pool中。不仅如此,还可以在Placement Targets上配置压缩算法等属性。使使用该规则上传的数据被压缩存储。
与Placement Targets相关的配置项主要有以下几个:
$ ./bin/radosgw-admin zone get --rgw-zone=default
{
......
"placement_pools": [
{
"key": "default-placement", // 名称
"val": {
"index_pool": "default.rgw.buckets.index",
"data_pool": "default.rgw.buckets.data",
"data_extra_pool": "default.rgw.buckets.non-ec",
"index_type": 0,
"compression": "" //采用的数据压缩算法
}
}
],
......
}
$ ./bin/radosgw-admin zonegroup get --rgw-zonegroup=default
{
......
"placement_targets": [
{
"name": "default-placement", //需要是zone中已存在的key,否则无效
"tags": [] //该placement pools对应的tags,用于给用户授权
}
],
"default_placement": "default-placement",
......
}
zonegroup和user的配置项中都有一项default_pacement
配置,如果user的配置为“”,那么采用zonegroup的配置,不为空则以user中的配置为准。
$ ./bin/radosgw-admin user info --uid=testid
{
......
"default_placement": "", //该用户操作时的默认placement pools
"placement_tags": [], //该用户有权使用的placement pools
......
}
使用时,我们需要先创建一组pool,然后在zone的配置中增加placement_pools
配置,然后在zonegroup中用placement_targets
项,为zone中的placement_pools
指定tags。最后在用户的placement_tags
中加入对应的tags为用户授权。
Compression
介绍
文档:http://docs.ceph.com/docs/luminous/radosgw/compression/
在前面的placement pools的配置中,有这样一项配置:
"compression": "" //采用的数据压缩算法
,这就是我们的压缩功能的唯一配置点,对于每条placement_pools
,我们可以为其配置数据压缩算法。
要点
配置压缩算法之后,用户上传的每个对象都会以该算法进行压缩,然后存入rados中,要注意的是,某rgw对象使用的压缩算法会一并保存在rados中。所以在用户更改压缩算法之前上传的对象,不会被新的压缩算法所影响,其仍然保持其原有的压缩方式,并会被正确解压。
当用户上传对象时使用了sse(服务端加密),该对象不会被compressed。GetObj代码逻辑似乎可以同时支持sse和compression,但PutObj的逻辑不支持。这里mark下,之后持续关注下。
目前支持4种压缩算法,分别是 snappy、 zlib、 zstd、 以及lz4;但lz4算法需要在编译前定义HAVE_LZ4宏才会被支持。ceph默认是不支持的。另外,compression还可以设置成random,这时会随机选择一个算法。根据注释描述,random是用于测试的,不建议使用。
当使用Multipart Upload时,每一个part都会以上传时的compression算法进行压缩。所以,如果在Multipart Upload期间,修改了compression算法,会导致多个parts之间压缩算法不一致,最终在用户发送完成Multipart请求时,会被检查出来,并报错返回。
当进行range读时,用户传入的偏移量是压缩前的偏移量,所以需要通过对象上传时在对象xattr存入的blocks列表做转换,blocks列表中存储了每个块的原始尺寸和偏移以及压缩后的尺寸和偏移。range读取时,用户传入对象的起始偏移ofs和结束偏移end,先根据blocks的old_ofs对ofs和end分别做一次二分查找,找到ofs所在的块和end所在的块,然后将范围内的数据块读取并解压后,再根据原始偏移取出需要的部分。
各压缩算法压缩比率、资源消耗
三种算法的压缩比例,计算方法:压缩后size/压缩前size
总结:三种算法对文本压缩效果显著,对视频和图片收效甚微。
算法 | 文本压缩比 | 图片压缩比 | 视频压缩比 |
---|---|---|---|
zlib | 0.0739 | 0.924 | 0.974 |
zstd | 0.0733 | 0.931 | 0.974 |
snappy | 0.151 | 0.925 | 0.988 |
在radosgw节点上使用s3cmd的上传延迟,5次平均
算法 | 单个40MB文本文件 | 单个78MB视频文件 |
---|---|---|
zlib | 2.0s | 7.5s |
zstd | 3.0s | 9.0s |
snappy | 2.0s | 5.0s |
不压缩 | 3.0s | 5.0s |
总结:压缩后数据体积更小可节省一定的网络传输时间,但会消耗额外的压缩时间。总上传延迟取决于两者的差值。
算法 | 单个40MB文本文件 | 单个78MB视频文件 |
---|---|---|
zlib | 2.0s | 7.5s |
zstd | 3.0s | 9.0s |
snappy | 2.0s | 5.0s |
不压缩 | 3.0s | 5.0s |
三种算法运行时的cpu占用
总结:压缩算法会占用大量cpu资源,内存变化不大。
算法 | cpu峰值 | 内存占用 |
---|---|---|
snappy | 35% | 变化不大 |
zlib | 80% | - |
zstd | 80% | - |
不压缩 | 20% | - |
使用
如果只是想在已有的数据pool上打开compression,只需要修改对应的zone的配置:
$ ./bin/radosgw-admin zone placement modify --rgw-zone=default --placement-id=default-placement --compression=zlib
但如果要对新创建的pool设置压缩选项,最好是创建新的placement pools配置项,如果选择替换原有placement pools配置中的pool,会使被替换的pool游离在rgw之外,无法通过rgw读取和写入它的数据。
假设现在用户想要创建创建一组额外的存储pool,并需要在pool上开启compression功能。相关的步骤如下:
--index-pool=
--data-pool=
--data-extra-pool=
--placement-index-type=
placement target index type (normal, indexless, or #id)
--compression=
1.创建对应的pool
$ ceph osd pool create default.rgw.buckets-compression.index 8
$ ceph osd pool create default.rgw.buckets-compression.data 32
$ ceph osd pool create default.rgw.buckets-compression.non-ec 8
2.在zone的配置中加入新的placement pools
这里有两种配置的方式,一种是通过radosgw-admin zone placement add
命令,配合相关的options进行配置。另一种方式是使用radosgw-admin zone get/set
通过编辑json文件来设置,这里选择前者。
$ radosgw-admin zone placement add --rgw-zone=default --placement-id=compression --index-pool=default.rgw.buckets-compression.index --data-pool=default.rgw.buckets-compression.data --data-extra-pool=default.rgw.buckets-compression.non-ec --compression=zlib
结果如下:
{
......
"placement_pools": [
{
"key": "compression",
"val": {
"index_pool": "default.rgw.buckets-compression.index",
"data_pool": "default.rgw.buckets-compression.data",
"data_extra_pool": "default.rgw.buckets-compression.non-ec",
"index_type": 0,
"compression": "zlib"
}
},
{
"key": "default-placement",
"val": {
"index_pool": "default.rgw.buckets.index",
"data_pool": "default.rgw.buckets.data",
"data_extra_pool": "default.rgw.buckets.non-ec",
"index_type": 0,
"compression": ""
}
}
],
......
}
3.在zonegroup的配置中设置tags
$ radosgw-admin zonegroup placement add --rgw-zonegroup=default --placement-id=compression --tags="compress"
结果如下:
[
{
"key": "compression",
"val": {
"name": "compression",
"tags": [
"compress"
]
}
},
{
"key": "default-placement",
"val": {
"name": "default-placement",
"tags": []
}
}
]
4.通过配置placement tags为user授权
在授权之前,我们试试看能否在ssd-placement对应的pool中进行操作。
$ radosgw-admin metadata get user:testid > /tmp/json
$ cat /tmp/json | grep placement
"default_placement": "default-placement",
"placement_tags": [],
$ s3cmd mb --bucket-location=':default-placement' s3://buckettwo
Bucket 's3://buckettwo/' created
$ s3cmd mb --bucket-location=':compression' s3://bucketthree
ERROR: Access to bucket 'bucketthree' was denied
ERROR: S3 error: 403 (AccessDenied)
给testid授权并测试权限。成功。
$ cat /tmp/json | grep placement
"default_placement": "default-placement",
"placement_tags": ["compress"],
$ radosgw-admin metadata put user:testid < /tmp/json
$ s3cmd mb --bucket-location=':compression' s3://bucketthree
Bucket 's3://bucketthree/' created
测试上传数据是否被压缩
$ s3cmd mb s3://bucket2 --bucket-location=':compression'
$ dd if=/dev/sda1 of=/tmp/m bs=4096 count=10000
$ s3cmd put /tmp/m s3://bucket2
size_utilized
和size_kb_utilized
为实际占用的磁盘空间,可以看出,压缩生效了。
$ ./bin/radosgw-admin bucket stats --bucket=bucket2
{
"bucket": "bucket2",
"zonegroup": "f14e1358-dda3-470e-b395-7a8bd5a9aeee",
"placement_rule": "ssd-placement",
......
"usage": {
"rgw.main": {
"size": 40960000,
"size_actual": 40960000,
"size_utilized": 258473,
"size_kb": 40000,
"size_kb_actual": 40000,
"size_kb_utilized": 253,
"num_objects": 1
},
"rgw.multimeta": {
"size": 0,
"size_actual": 0,
"size_utilized": 0,
"size_kb": 0,
"size_kb_actual": 0,
"size_kb_utilized": 0,
"num_objects": 0
}
},
......
}
实现
存储位置
我们在zone上设置的placement targets(包含compression属性)被存储在.rgw.root
pool中的对象中。
$ ./bin/rados -p .rgw.root ls
zone_info.c3682995-3a15-4161-b55a-a7f48ddbdbee
zonegroup_info.f14e1358-dda3-470e-b395-7a8bd5a9aeee
zone_names.default
zonegroups_names.default
作用机制
对象上传/读取
上传对象并压缩的相关代码在RGWPutObj::execute()函数中,要点代码如下:
// filter用于对数据进行处理,比如加密和压缩
RGWPutObjDataProcessor *filter = nullptr;
......
// 根据zone配置选择object的压缩类型,可为none或具体的压缩插件名字
// http://docs.ceph.com/docs/kraken/radosgw/compression/
const auto& compression_type = store->get_zone_params().get_compression_type(
s->bucket_info.placement_rule);
CompressorRef plugin;
boost::optional compressor;
......
// 需要加密时,filter用于加密数据
if (encrypt != nullptr) {
filter = encrypt.get();
} else {
//no encryption, we can try compression
if (compression_type != "none") {
// 不需要加密时,并且compression_type被设置了,filter被用于压缩数据
plugin = get_compressor_plugin(s, compression_type);
if (!plugin) {
ldout(s->cct, 1) << "Cannot load plugin for compression type "
<< compression_type << dendl;
} else {
// 如果一切都没问题,构造compressor
compressor.emplace(s->cct, plugin, filter);
filter = &*compressor;
}
}
}
......
do{
......
// 在put_data_and_throttle函数中,会调用filter->handle_data,对数据进行压缩或加密
// 然后通过next调用`RGWPutObjProcessor_Atomic`的`handle_data`
// 将处理后的数据切分成一个head和多个tail对象
// handle_data最终调用`store->aio_put_obj_data`函数,将对象写入rados
op_ret = put_data_and_throttle(filter, data, ofs, need_to_wait);
......
} while (len > 0);
......
// 如果进行了压缩,将压缩信息加入xattr
if (compressor && compressor->is_compressed()) {
bufferlist tmp;
RGWCompressionInfo cs_info;
cs_info.compression_type = plugin->get_type_name();
cs_info.orig_size = s->obj_size;
cs_info.blocks = move(compressor->get_compression_blocks());
::encode(cs_info, tmp);
attrs[RGW_ATTR_COMPRESSION] = tmp;
ldout(s->cct, 20) << "storing " << RGW_ATTR_COMPRESSION
<< " with type=" << cs_info.compression_type
<< ", orig_size=" << cs_info.orig_size
<< ", blocks=" << cs_info.blocks.size() << dendl;
}
......
// 完成之前未完成的head和tail的写入,为head设置xattr
op_ret = processor->complete(s->obj_size, etag, &mtime, real_time(), attrs,
(delete_at ? *delete_at : real_time()), if_match, if_nomatch,
(user_data.empty() ? nullptr : &user_data));
......
上述代码中的compressor负责进行数据的压缩,在rgw_compression.h/cc
中,是其压缩和解压的函数实现。在压缩函数中,每次传入存有部分对象数据的bufferlist,压缩之后的内容放入一个名为blocks的数组(并没有另外分配空间,压缩的数据会覆盖原有bufferlist的数据),其实这个blocks数组仅仅起到一个管理的作用,用于存储压缩数据的界限,用于之后的解压。之后可以通过compressor->get_compression_blocks()
获得blocks的数据,会被编码进对象的xattr。
当使用Multipart Upload时,每一个part都会以上传时的compression算法进行压缩。所以,如果在Multipart Upload期间,修改了compression算法,会导致多个parts之间压缩算法不一致,最终在用户发送完成Multipart请求时,会被检查出来,并报错返回。相关代码在RGWCompleteMultipart::execute()
函数中。
对象读取的流程基本和上传对应,需要注意的是,range读取操作。用户传入的偏移量是压缩前的偏移量,所以需要通过对象上传时在对象xattr存入的blocks列表做转换,blocks列表中存储了每个块的原始尺寸和偏移以及压缩后的尺寸和偏移。range读取时,用户传入对象的起始偏移ofs和结束偏移end,先根据blocks的old_ofs对ofs和end分别做一次二分查找,找到ofs所在的块和end所在的块,然后将范围内的数据块读取并解压后,再根据原始偏移取出需要的部分。
压缩/解压实现
至于具体的压缩算法则在ceph/src/compressor/中实现,目前支持4种压缩算法,分别是snappy, zlib, zstd ,还有lz4(可能不完善,需要在编译前定义HAVE_LZ4宏才会支持)。
下面的函数是用于根据compression配置获得compression algorithm的函数,我们看到lz4算法需要定义HAVE_LZ4宏才会被支持默认是不支持的。
boost::optional Compressor::get_comp_alg_type(const std::string &s) {
if (s == "snappy")
return COMP_ALG_SNAPPY;
if (s == "zlib")
return COMP_ALG_ZLIB;
if (s == "zstd")
return COMP_ALG_ZSTD;
#ifdef HAVE_LZ4
if (s == "lz4")
return COMP_ALG_LZ4;
#endif
if (s == "" || s == "none")
return COMP_ALG_NONE;
return boost::optional();
}