.NET — Сохранение данных с помощью таблиц хранилища Azure Storage

Что такое Azure Storage Table?

Azure Storage Tables — это служба персистентности, которая работает с нереляционными структурированными данными в облаке. Он предлагает механизм хранения ключей/атрибутов (таблиц и строк) без принудительной схемы (как T-SQL).

Storage Tables — это недорогая и эффективная услуга, позволяющая хранить терабайты данных. Благодаря отсутствию принудительного использования схемы, как в SQL Server (или любой другой реляционной базе данных), внедрение и развитие происходит быстро и легко.

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

Вы можете использовать его как угодно, только учтите, что он не предназначен для реляционной базы данных, а для хранения такой информации, как:

  • Терабайты структурированной информации, которая не требует сложных взаимосвязей или внешних ключей и может быть денормализована для более простого и быстрого доступа.
  • Примеры включают:
    • Журналы регистрации действий
    • У меня есть системы, где хранятся все действия пользователей, и даже если записей миллионы, к ним можно обратиться.
    • Телеметрия
    • Некоторое время назад мы реализовали это в проекте IoT, где мы обрабатывали около 30 миллионов сообщений в месяц.
    • Массовое протоколирование
    • В этой статье в качестве примера мы рассмотрим протоколирование запросов.

Примечание 💡: Вы можете найти исходный код на моем github здесь

Важные понятия

  • Учетные записи: Для доступа к API таблиц нам нужна учетная запись хранилища.
    • Наряду со счетами хранения у нас также есть:
    • Очереди. Для простого обмена сообщениями, но с миллионами сообщений.
      • Я уже писал об этом здесь
    • Блобы. Файлы для хранения / двоичные файлы
  • Таблица: Это коллекция сущностей. Таблицы не заставляют вас следовать схеме в каждой сущности, что означает, что в одной и той же таблице вы можете содержать различные свойства (они же столбцы).
  • Сущность: Сущность — это набор свойств, точно так же, как строка в базе данных. Сущность в Azure Storage может весить до 1 МБ. В Azure Cosmos DB он может весить до 2 МБ.
  • Свойства: Свойство состоит из имени-значения. Каждая сущность может иметь до 252 свойств.
    • Каждая сущность также имеет 3 свойства по умолчанию, в которых указываются ключ раздела, ключ строки и метка времени.
    • Сущности в пределах одного и того же PartitionKey могут запрашиваться значительно быстрее.
    • RowKey — это первичный ключ каждой сущности.

Руководящие принципы хорошего дизайна

Важно учитывать исходные требования при использовании Azure Tables, поскольку необходимо решить, как мы будем хранить информацию в зависимости от того, как мы хотим эффективно запрашивать ее.

Дизайн для эффективного чтения

  • Всегда указывайте PartitionKey и RowKey в запросах. Когда бы мы ни делали запросы, гораздо эффективнее, если мы всегда указываем раздел и ключ, который ищем.
    • В приведенном ниже примере мы сосредоточимся на этом моменте.
  • Рассмотрите возможность дублирования данных. Хранилище таблиц очень двоично, что размер персистентности больше не имеет значения, поэтому рекомендуется хранить одну и ту же сущность несколько раз для улучшения запросов (в зависимости от потребностей запроса).
  • Денормализуйте данные. Как указано выше, хранение данных не требует больших затрат, поэтому рекомендуется денормализовать данные. Повторение информации внутри сущности, чтобы не нужно было запрашивать ее с помощью какого-либо отношения, делает запросы более быстрыми.
    • Примите во внимание, что информация не меняется постоянно, а если меняется, то подумайте дважды.

Дизайн для эффективного письма

  • Избегайте разделов, к которым требуется слишком часто обращаться. Создание разделов из ключа делает распределение информации очень динамичным.
    • Например. В журналах я обычно создаю ключи разделов следующим образом: Events{DDDMMYYYYYYYY} и затем каждый месяц у меня будет раздел.
    • Эксем. Говоря об IoT, я использовал сохранение истории в разделах в соответствии с устройством: Device{ID_DEVICE}_{DDDMMYYYY} и таким образом я создаю разделы на устройство и на месяц (все зависит от того, как мне нужно обращаться к информации).
  • Не всегда необходимо иметь таблицу для каждой сущности. Поскольку мы не обязаны следовать схеме, мы можем хранить различные типы сущностей в одной таблице, что позволяет обеспечить атомарность операций при хранении информации. Если это не так, лучше разделить на разные таблицы.

Если вы хотите узнать больше

  • Шаблоны проектирования таблиц
  • Дизайн для составления запросов

Практический пример: массовая регистрация заявок на вакцины

Сейчас почти конец 2022 года, и я знаю, что уже вышло из моды говорить о вакцинах и пандемии, но этот пример пришел мне на ум, потому что это то, что является ситуативным для данной технологии.

В Мексике механика получения вакцинации (и знания о том, сколько людей планируют пройти вакцинацию) заключалась в регистрации на веб-сайте с использованием уникального регистрационного номера населения (называемого CURP). Миллионы людей зарегистрировались, и после регистрации можно было проверить свою заявку, а если вы пытались зарегистрироваться снова, то не могли.

Что мы будем учитывать при разработке этого решения:

  • Вы можете зарегистрироваться только один раз, поэтому CURP работает как Row Key.
  • Чтобы запросить «n» количество приложений, необходимо указать штат и город (Ключ раздела)
  • Чтобы найти ваше заявление, вам нужен штат и город, а также ваш CURP (ключ раздела и ключ строки).
  • Поля: Полное имя, КУРП, штат и город

Цель построения ключа раздела таким образом заключается в том, чтобы иметь возможность быстро просмотреть все приложения города. Непрактично запрашивать всю страну, но по городам объем уменьшается, и мы можем делать более конкретные запросы (например, фильтровать по возрасту, полу и т.д.).

Проект API: AzureTableStorage

Прежде чем начать, нам нужно убедиться, что у нас установлен эмулятор (он же Azurite). Он поставляется с инструментами Visual Studio для Azure, но если у вас его нет, посетите эту страницу, чтобы узнать больше.

Мне лично нравится использовать Azurite с npm, потому что я могу запустить его всего одной командой.

После установки Azurite создадим пустой веб-проект в папке проекта (в моем случае он назывался AzureStorageTables):

dotnet new web
Войдите в полноэкранный режим Выход из полноэкранного режима

И установите следующие пакеты:

dotnet add package Azure.Data.Tables
dotnet add package System.Linq.Async
Войдите в полноэкранный режим Выход из полноэкранного режима

Примечание 💡: Мы будем использовать System.Linq.Async для обработки асинхронных потоков с помощью Linq.

Модели > VaccineRequest

Мы определяем нашу сущность, нам нужно реализовать интерфейс ITableEntity. Вот три обязательных свойства сущности в таблицах:

using Azure;
using Azure.Data.Tables;

namespace AzureStorageTables.Models;

public class VaccineRequest : ITableEntity
{
    public string Curp { get; set; }
    public string FullName { get; set; }
    public string City { get; set; }
    public string State { get; set; }

    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public DateTimeOffset? Timestamp { get; set; }
    public ETag ETag { get; set; }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Примечание 😂: ETag я никогда не использовал и по сей день не знаю для чего он нужен….

Услуги > IVaccineRequestStoreService

Здесь мы определяем наш контракт для нашего хранилища информации, ничего особенного. Пока у нас будет только три метода: один для создания и два для запроса:

using AzureStorageTables.Models;

namespace AzureStorageTables.Services;

public interface IVaccineRequestStoreService
{
    Task CreateRequestAsync(VaccineRequest entity);
    Task<VaccineRequest> GetRequestAsync(string curp, string state, string city);
    IAsyncEnumerable<VaccineRequest> GetRequestsByCityAsync(string state, string city);
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Ключ раздела, как я полагал, состоит из государства и города. Поэтому запросы всегда должны выполняться с указанием этих данных (чтобы сделать их супербыстрыми).

Если ключ раздела не указан в запросе, он будет работать очень медленно.

Поэтому, в зависимости от потребностей, мы можем думать по-другому и построить, например, ключ разделения только по штату, году рождения, полу или комбинации всего этого. Всегда помните, что вы должны (но не обязаны) всегда искать по ключу раздела.

Услуги > VaccineRequestStoreService

Это реализация Store, здесь мы используем Tables Storage:

using Azure.Data.Tables;
using AzureStorageTables.Models;

namespace AzureStorageTables.Services;

public class VaccineRequestStoreService : IVaccineRequestStoreService
{
    public const string TableName = "VaccineRequests";
    private TableClient _tableClient;

    public VaccineRequestStoreService(IConfiguration config)
    {
        _tableClient = new TableClient(config["TableStorage:ConnectionString"], TableName);
    }

    public async Task CreateRequestAsync(VaccineRequest entity)
    {
        await _tableClient.CreateIfNotExistsAsync();

        entity.PartitionKey = $"{entity.State}_{entity.City}";
        entity.RowKey = entity.Curp;

        _ = await _tableClient.AddEntityAsync(entity);
    }

    public async Task<VaccineRequest> GetRequestAsync(string curp, string state, string city)
    {
        var results = _tableClient
            .QueryAsync<VaccineRequest>(q => q.PartitionKey == $"{state}_{city}" && q.RowKey == curp);

        await foreach (var entity in results)
        {
            return entity;
        }

        return null;
    }


    public IAsyncEnumerable<VaccineRequest> GetRequestsByCityAsync(string state, string city) =>
         _tableClient
            .QueryAsync<VaccineRequest>(q => q.PartitionKey == $"{state}_{city}");

}
Войдите в полноэкранный режим Выход из полноэкранного режима

Краткое описание каждого метода:

  • CreateRequestAsync
    • Обратите внимание, что это HTTP-запрос, вы можете найти способ делать это только в начале, а не при каждом Create
  • GetRequestAsync:
    • Итерация с помощью await foreach для доступа к результатам. Мы делаем это таким образом, потому что SDK автоматически обрабатывает пагинацию.
    • В данном случае нам нужен только первый результат (потому что он будет результатом, так как мы используем ключ Row).
    • Ключ раздела состоит из штата и города ($"{state}_{city}").
  • GetRequestsByCityAsync:
    • Аналогично с QueryAsync мы выполняем запрос, но здесь мы делаем кое-что другое.
    • Мы возвращаем поток информации с помощью IAsyncEnumerable для обработки информации, пока мы обращаемся к ней.
    • Поскольку записей может быть тысячи, мы не хотим запрашивать их все в памяти, а затем преобразовывать в DTO.
    • Мы возвращаем AsyncPage, который является реализацией IAsyncEnumerable, а презентация будет отвечать за преобразование его в DTO по мере поступления информации.

Program.cs

Наконец, мы будем использовать Minimal APIs для создания конечных точек и демонстрации этой функциональности:

using Azure;
using AzureStorageTables.Models;
using AzureStorageTables.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IVaccineRequestStoreService, VaccineRequestStoreService>();

var app = builder.Build();

// Endpoints Aquí...

app.Run();

Войдите в полноэкранный режим Выход из полноэкранного режима

Для создания и чтения запросов на вакцинацию мы будем использовать DTO, чтобы избежать раскрытия ненужных нам свойств (таких как Partition Key и Etags).

namespace AzureStorageTables.Models;

public record VaccineRequestDto(string Curp, string FullName, string City, string State);
Войдите в полноэкранный режим Выход из полноэкранного режима

Конечная точка POST api/vaccine-requests

При создании мы вызываем Store напрямую:

app.MapPost("api/vaccine-requests", async (VaccineRequestDto request, IVaccineRequestStoreService store) =>
{
    try
    {
        await store.CreateRequestAsync(new VaccineRequest
        {
            City = request.City,
            State = request.State,
            Curp = request.Curp,
            FullName = request.FullName
        });
    }
    catch (RequestFailedException ex)
        // Ya existe una solicitud con esta CURP
        when (ex.Status == StatusCodes.Status409Conflict)
    {
        return Results.BadRequest($"Ya existe una solicitud del CURP {request.Curp}");
    }

    return Results.Ok();
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы используем фильтры исключений для возврата Bad Request, когда хотим создать повторный CURP-запрос. Azure возвращает 409, если мы пытаемся создать запись с существующим ключом раздела и ключом строки.

Конечная точка GET api/vaccine-requests

Запрос заявок по городам:

app.MapGet("api/vaccine-requests", (string state, string city, IVaccineRequestStoreService store) =>
{
    return Results.Ok(store
        .GetRequestsByCityAsync(state, city)
        .Select(s => new VaccineRequestDto(s.Curp, s.FullName, s.City, s.State))
    );
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Здесь мы используем преимущества IAsyncEnumerable для обработки и сериализации информации одновременно с обращением к ней. Таким образом, мы избегаем загрузки всех данных в память, но обрабатываем их в потоке асинхронным способом.

Идея не в том, чтобы возвращать тысячи или миллионы записей, а в том, чтобы иметь возможность запрашивать несколько сотен внутри тысяч или миллионов записей, но обработка информации таким образом все еще полезна, если она высоко параллельна.

Конечная точка POST api/vaccine-requests/{curp}

Запрашивайте запросы индивидуально:

app.MapGet("api/vaccine-requests/{curp}", async(string curp, string state, string city, IVaccineRequestStoreService store) =>
{
    var request = await store.GetRequestAsync(curp, state, city);

    return new VaccineRequestDto(request.Curp, request.FullName, request.State, request.City);
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Теоретически, этот метод должен быть очень быстрым, даже если у нас миллионы записей, это происходит потому, что мы указываем раздел и первичный ключ сущности.

appsettings.json

Используйте UseDevelopmentStorage=true для использования Azurite (эмулятор локального хранилища):

{
  "TableStorage": {
    "ConnectionString": "UseDevelopmentStorage=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Тестирование с помощью REST-клиента

Для тестирования конечных точек мы можем настроить swagger, но иногда мне нравится делать это с помощью ручных HTTP-запросов (что я до сих пор и делаю), используя расширение VS Code под названием REST Client.

Запрос

### Create Vaccine Request
POST  https://localhost:7066/api/vaccine-requests
Content-Type:  application/json

{
    "fullName": "Isaac Ojeda",
    "curp": "test00001",
    "city": "Chihuahua",
    "state": "Chihuahua"
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Ответ

HTTP/1.1 200 OK
Content-Length: 0
Connection: close
Date: Sun, 09 Oct 2022 04:59:38 GMT
Server: Kestrel
Войдите в полноэкранный режим Выход из полноэкранного режима

Запрос

### Get Vaccine Requests by State
GET https://localhost:7066/api/vaccine-requests?state=Chihuahua&city=Chihuahua
Content-Type: application/json
Войдите в полноэкранный режим Выход из полноэкранного режима

Ответ

[
  {
    "curp": "1234567890",
    "fullName": "Isaac Ojeda",
    "city": "Chihuahua",
    "state": "Chihuahua"
  },
  {
    "curp": "test00001",
    "fullName": "Isaac Ojeda",
    "city": "Chihuahua",
    "state": "Chihuahua"
  },
  {
    "curp": "test00002",
    "fullName": "Isaac Ojeda",
    "city": "Chihuahua",
    "state": "Chihuahua"
  }
]
Войдите в полноэкранный режим Выход из полноэкранного режима

Запрос

### Get Vaccine Request by CURP
GET https://localhost:7066/api/vaccine-requests/test00001?state=Chihuahua&city=Chihuahua
Content-Type: application/json
Войдите в полноэкранный режим Выход из полноэкранного режима

Ответ

{
  "curp": "test00001",
  "fullName": "Isaac Ojeda",
  "city": "Chihuahua",
  "state": "Chihuahua"
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы можем использовать Storage Explorer для визуализации информации, которую мы создаем в учетной записи.

Заключение

Azure Tables Storage используется во всех проектах, в которых я участвую, будь то хранение журналов, информации приложений или чего-либо еще.

Хранилище таблиц можно геореплицировать, его можно интегрировать в виртуальную сеть Azure, частные каналы для гибридного облака и многое другое, что предлагает Azure.

Лично у него есть множество применений, и все становится еще лучше, если вы используете Azure Cosmos DB. Поэтому я рекомендую вам посмотреть больше, поэкспериментировать и оценить, подходит ли эта технология для вашего проекта.

Ссылки

  • Обзор системы хранения
  • Рекомендации по проектированию
  • Дизайн для запроса
  • Клиентская библиотека .NET

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *