Ceph Objet Storage and object versioning на практике

Версионирование объектов в S3 довольно подробно описано в документации AWS но все же периодически возникают вопросы по этой теме. Решил сделать маленькую заметку и показать как это выглядит на практике, в том числе и на уровне хранения объектов.

В моем случае, вместо оригинального черного ящика под названием AWS S3 используется Ceph и Ceph Object Gateway(RadosGW).
Ceph Object Gateway — это надстройка над довольно низкоуровневым хранилищем под названием Ceph, которая реализует AWS S3 API(REST only).
По сути это open source реализация S3-совместимого объектного хранилища.

Для работы с объектами я буду использовать awscli. Я не буду останавливаться на том как ее установить и настроить — это подробно описано в документации. Я лишь покажу как переопределить адрес для подключения к s3 если вы как и я работаете не с AWS S3 а каким то другим S3).

Далее мы будем использовать переменную $EP при каждом вызове awscli

$ export EP='--endpoint-url=https://storage.fatruden.pro'

Создадим тестовый бакет — test-versioning:

$ aws s3api $EP create-bucket --bucket test-versioning

Метаданные бакета:

radosgw-admin bucket stats --bucket=test-versioning
{
    "bucket": "test-versioning",
    "pool": ".rgw.buckets",
    "index_pool": ".rgw.buckets.index",
    "id": "default.9434548.3769828",
    "marker": "default.9434548.3769828",
    "owner": "21cc0720-53e0-4f02-b080-52bfedabda14",
    "ver": "0#1",
    "master_ver": "0#0",
    "mtime": "2017-07-10 00:42:29.000000",
    "max_marker": "0#",
    "usage": {},
    "bucket_quota": {
        "enabled": false,
        "max_size_kb": -1,
        "max_objects": -1
    }
}

radosgw-admin— утилита для управления упомянутым выше Ceph Object Gateway на стороне самого хранилища, у вас иакой утилиты нет, зазве только если вы распологаете своим кластером Ceph)

Включаем версионирование бакета:

$ aws s3api $EP put-bucket-versioning --bucket test-versioning --versioning-configuration Status=Enabled

Версионирование объектов включается на уровне бакета. При отключенном версионировании, put по одному и тому же ключу будет перезаписывать объект.

Создадим первый тестовый файл:

$ echo version-1 > test.txt

Создадим первый объект из нашего тестового файла:

$ aws s3api $EP put-object --bucket test-versioning --key test.txt --body ./test.txt

Снова смотрим метаданные бакета на стороне хранилища:

radosgw-admin bucket stats --bucket=test-versioning
{
    "bucket": "test-versioning",
    "pool": ".rgw.buckets",
    "index_pool": ".rgw.buckets.index",
    "id": "default.9434548.3769828",
    "marker": "default.9434548.3769828",
    "owner": "21cc0720-53e0-4f02-b080-52bfedabda14",
    "ver": "0#4",
    "master_ver": "0#0",
    "mtime": "2017-07-10 00:45:30.000000",
    "max_marker": "0#",
    "usage": {
        "rgw.main": {
            "size_kb": 1,
            "size_kb_actual": 4,
            "num_objects": 1
        }
    },
    "bucket_quota": {
        "enabled": false,
        "max_size_kb": -1,
        "max_objects": -1
    }
}

В выводе выше мы видим, что в секции usage говорится об одном объекте и о уже о каком-то размере.

Создадим вторую версию файла:

$ echo version-2 > test.txt

PUT-им снова файл указывая тоже имя в качестве имени объекта, что и выше:

$ aws s3api $EP put-object --bucket test-versioning --key test.txt --body ./test.txt

Смотрим что у нас по версиям:

$ aws s3api $EP list-object-versions --bucket test-versioning --prefix test.txt
{
    "Versions": [
        {
            "StorageClass": "STANDARD",
            "ETag": "\"d51dc42f616b67126fd2aa1e1f43385b\"",
            "Size": 10,
            "Owner": {
                "DisplayName": "arudenko@cloud.croc.ru",
                "ID": "21cc0720-53e0-4f02-b080-52bfedabda14"
            },
            "Key": "test.txt",
            "LastModified": "2017-07-09T21:53:00.000Z",
            "IsLatest": true,
            "VersionId": "cyORxr9.b2drOED8Xy-7vcUN4XgH2QF"
        },
        {
            "StorageClass": "STANDARD",
            "ETag": "\"fb735ea6a5dddf137c7229513cae6296\"",
            "Size": 10,
            "Owner": {
                "DisplayName": "arudenko@cloud.croc.ru",
                "ID": "21cc0720-53e0-4f02-b080-52bfedabda14"
            },
            "Key": "test.txt",
            "LastModified": "2017-07-09T21:47:03.000Z",
            "IsLatest": false,
            "VersionId": "9YsMTYTk5Vrg3UiOhmOq5EPl6tUP3xf"
        }
    ]
}

Каждый объект имеет уникальный идентификатор VersionId. Так же отличается ETagт.к. это были разные файлы с разным md5. ETag это md5 если, что.

Самое важное в выводе выше — поле IsLatest. Объект у которого оно имеет значение true является самым свежим и будет отдан при GET-запросе без указания VersionId.

На уровне хранилища это полноценные два объекта:

radosgw-admin bucket stats --bucket=test-versioning
{
    "bucket": "test-versioning",
    "pool": ".rgw.buckets",
    "index_pool": ".rgw.buckets.index",
    "id": "default.9434548.3769828",
    "marker": "default.9434548.3769828",
    "owner": "21cc0720-53e0-4f02-b080-52bfedabda14",
    "ver": "0#10",
    "master_ver": "0#0",
    "mtime": "2017-07-10 00:45:30.000000",
    "max_marker": "0#",
    "usage": {
        "rgw.main": {
            "size_kb": 1,
            "size_kb_actual": 8,
            "num_objects": 2
        }
    },
    "bucket_quota": {
        "enabled": false,
        "max_size_kb": -1,
        "max_objects": -1
    }
}

Как я уже говорил, при GET’е без указания версии мы получаем объект у которого  "IsLatest": true, 

$ aws s3api $EP get-object --bucket test-versioning --key test.txt test_out.txt; cat test_out.txt
{
    "Metadata": {},
    "ContentType": "binary/octet-stream",
    "ETag": "\"0ab528f8c6ca77b68640eae1f8cae1f3\"",
    "AcceptRanges": "bytes",
    "LastModified": "Sun, 09 Jul 2017 21:57:50 GMT",
    "ContentLength": 10
}

version-2

Удалим объект:

$ aws s3api $EP delete-object --bucket test-versioning --key test.txt

Если мы посмотрим на уровне хранилища на изменения, то увидим, что ничего не поменялось, ни занятый объем не количество объектов.

Теперь версии объектов будут выглядеть следующим образом:

$ aws s3api $EP list-object-versions --bucket test-versioning --prefix test.txt
{
    "DeleteMarkers": [
        {
            "Owner": {
                "DisplayName": "arudenko@cloud.croc.ru",
                "ID": "21cc0720-53e0-4f02-b080-52bfedabda14"
            },
            "IsLatest": true,
            "Key": "test.txt",
            "LastModified": "2017-07-09T22:02:07.000Z",
            "VersionId": "2dzVIMT7uTNUMNwsxHbbwCVFNzR6PPu"
        }
    ],
    "Versions": [
        {
            "IsLatest": false,
            "Owner": {
                "DisplayName": "arudenko@cloud.croc.ru",
                "ID": "21cc0720-53e0-4f02-b080-52bfedabda14"
            },
            "StorageClass": "STANDARD",
            "Size": 10,
            "LastModified": "2017-07-09T21:53:00.000Z",
            "Key": "test.txt",
            "ETag": "\"d51dc42f616b67126fd2aa1e1f43385b\"",
            "VersionId": "cyORxr9.b2drOED8Xy-7vcUN4XgH2QF"
        },
        {
            "IsLatest": false,
            "Owner": {
                "DisplayName": "arudenko@cloud.croc.ru",
                "ID": "21cc0720-53e0-4f02-b080-52bfedabda14"
            },
            "StorageClass": "STANDARD",
            "Size": 10,
            "LastModified": "2017-07-09T21:47:03.000Z",
            "Key": "test.txt",
            "ETag": "\"fb735ea6a5dddf137c7229513cae6296\"",
            "VersionId": "9YsMTYTk5Vrg3UiOhmOq5EPl6tUP3xf"
        }
    ]
}

Появился специальный объект(object instance) — DeleteMarkers который имеет значение "IsLatest": true.

Если мы попробуем получить наш объект без указания версии то получим 404:

$ aws s3api $EP get-object --bucket test-versioning --key test.txt test_out.txt

An error occurred (NoSuchKey) when calling the GetObject operation: Unknown

В принципе, справедливо, мы же его удалили)
Но на самом деле не одна из версий нашего объекта не была удалены.
Только теперь не одна из них не является Latest и для того, что бы их получить нужно указать версию:

$ aws s3api $EP get-object --bucket test-versioning --key test.txt test_out.txt --version-id 9YsMTYTk5Vrg3UiOhmOq5EPl6tUP3xf; cat test_out.txt
{
    "LastModified": "Sun, 09 Jul 2017 21:47:03 GMT",
    "ContentType": "binary/octet-stream",
    "ContentLength": 10,
    "ETag": "\"fb735ea6a5dddf137c7229513cae6296\"",
    "AcceptRanges": "bytes",
    "Metadata": {}
}
version-1

Востановить последнюю до удаления версию можно удалив DeleteMarker.
DeleteMarker для нас является просто еще одной версией нашего объекта, при удалении нужно указать его версию:

$ aws s3api $EP delete-object --bucket test-versioning --key test.txt --version-id 2dzVIMT7uTNUMNwsxHbbwCVFNzR6PPu

Теперь "IsLatest": true, снова сместился к последнему объекту, что был загружен нами:

$ aws s3api $EP get-object --bucket test-versioning --key test.txt test_out.txt; cat test_out.txt
{
    "Metadata": {},
    "AcceptRanges": "bytes",
    "LastModified": "Sun, 09 Jul 2017 21:57:50 GMT",
    "ContentType": "binary/octet-stream",
    "ETag": "\"0ab528f8c6ca77b68640eae1f8cae1f3\"",
    "ContentLength": 10
}


version-2

Если же удалить последний объект но не просто по ключу а указать его версию, то Latest флаг перейден к предидущему по времени объекту. Ну вы поняли в общем.

Резюмируя:

  • Версии одного объекта это по сути отдельные объекты которые полноценно занимают место в хранилище и имеют уникальный  `VersionId`;
  • Каждый PUT это новая версия и по сути новый объект;
  • При GET’е без указания версии будет получена версия с флагом `»IsLatest»: true` или 404 если было выполнено удаление без указания версии);
  • Можно GET’ить объект любой версии указав ее явно;
  • Что бы удалить объект определенной версии из хранилища необходимо указывать явно его версию иначе он будет лежать и занимать место. Либо использовать Object Lifecycle Management.