ceph rgw:compression实现

背景

compression是基于Placement Targets的,是其中的一个配置项。

Placement Targets

我们知道,在默认情况下,bucket index、bi log是存储在.rgw.buckets.index pool中的,object data是存储在.rgw.buckets.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,我们可以为其配置数据压缩算法。

要点

  1. 配置压缩算法之后,用户上传的每个对象都会以该算法进行压缩,然后存入rados中,要注意的是,某rgw对象使用的压缩算法会一并保存在rados中。所以在用户更改压缩算法之前上传的对象,不会被新的压缩算法所影响,其仍然保持其原有的压缩方式,并会被正确解压。

  2. 当用户上传对象时使用了sse(服务端加密),该对象不会被compressed。GetObj代码逻辑似乎可以同时支持sse和compression,但PutObj的逻辑不支持。这里mark下,之后持续关注下。

  3. 目前支持4种压缩算法,分别是 snappy、 zlib、 zstd、 以及lz4;但lz4算法需要在编译前定义HAVE_LZ4宏才会被支持。ceph默认是不支持的。另外,compression还可以设置成random,这时会随机选择一个算法。根据注释描述,random是用于测试的,不建议使用。

  4. 当使用Multipart Upload时,每一个part都会以上传时的compression算法进行压缩。所以,如果在Multipart Upload期间,修改了compression算法,会导致多个parts之间压缩算法不一致,最终在用户发送完成Multipart请求时,会被检查出来,并报错返回。

  5. 当进行range读时,用户传入的偏移量是压缩前的偏移量,所以需要通过对象上传时在对象xattr存入的blocks列表做转换,blocks列表中存储了每个块的原始尺寸和偏移以及压缩后的尺寸和偏移。range读取时,用户传入对象的起始偏移ofs和结束偏移end,先根据blocks的old_ofs对ofs和end分别做一次二分查找,找到ofs所在的块和end所在的块,然后将范围内的数据块读取并解压后,再根据原始偏移取出需要的部分。

  6. 各压缩算法压缩比率、资源消耗

三种算法的压缩比例,计算方法:压缩后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= placement target index pool
--data-pool= placement target data pool
--data-extra-pool= placement target data extra (non-ec) pool
--placement-index-type=
placement target index type (normal, indexless, or #id)
--compression= placement target compression type (plugin name or empty/none)

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_utilizedsize_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();
}

你可能感兴趣的:(ceph rgw:compression实现)