Что такое 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