Русский
Русский
English
Статистика
Реклама

Nosql

Инструменты Node.js разработчика. Какие ODM нам нужны

18.10.2020 22:23:47 | Автор: admin

ODM - Object Document Mapper - используется преимущественно для доступа к документоориенриирвоанным базам данных, к которым относятся MongoDB, CouchDB, ArangoDB, OrientDB (последние две базы данных гибридные) и некоторые другие.

Прежде чем перейти к рассмотрению вопроса, озвученного в названии сообщения, приведу статистику скачивания пакетов из публичного регистра npm.

Таблица

Статистика скачивания пакетов для работы с реляционными и документоориентированными базами данных из публичного регистра npm

Пакет (npm)

Количество скачиваний в неделю

База данных

pg

1 660 369

PostgreSQL

mysql

713 548

MySQL

mongodb (из них 1 034 051 вместе с mongoose)

1 974 992

MongoDB

mongoose

1 034 051

MongoDB

nano

60 793

CouchDB

PouchDB

25 707

CouchDB

arangojs

7 625

ArangoDB

orientjs

598

OrientDB

Из таблицы можно сделать очень интересные выводы:

1) Использование MongoDB в проектах на nodejs почти сравнялось с использованием реляционных баз данных, и превысило использование одной отдельно взятой реляционной базы данных PostgreSQL или MySQL;

2) В половине проектов для доступа к MongoDB используется библиотека mongoose (ODM);

3) В половине проектов для доступа к MongoDB не используется библиотека mongoose. А это означает, что не используется сторонняя ODM, так как другой популярной библиотеки ODM для MongoDB нет.

4) Такие базы данных как ArangoDB, OrtintDB существенно менее популярны, чем MongoDB, и даже CoucbDB, и не имеют общепризнанных ODM.

Сначала остановлюсь на мотивации использования документоориентированных баз данных в проектах на Node.js. Отставим в сторону маркетинговую составляющую, хотя ее доля возможно более 50% из общего количества в приведенной статистике. Будем исходить что это все же кому-то действительно нужно, по-настоящему.

Модель данных в документоориентирвоанных базах данных близка к привычной многим в реляционных базах данных. Только вместо таблицы - коллекция, вместо строки таблицы - документ. Дает ли применение коллекции документов вместо таблицы преимущества и какие? Рассмотрим возможные варианты аргументации.

1) Отсутствие жесткой схемы. Сейчас это уже не аргумент. Так как MySQL и PostgrSQL обзавелись типом данных json, который может то же самое и даже больше (например остается возможность запросов с JOIN). Но даже не это главное. Как показывает статистика, половина проектов на MongoDB использует mongoose, в которой задается жесткая схема документа в коллекции.

2) Встроенные документы. Сейчас тоже не аргумент. MySQL и PostgrSQL имеют тип данных json, которые может то же самое. Надо отдать должное, что внедрение этого типа данных было ускорено развитием NoSQL.

3) Репликация и шардирование. Да, да и еще раз да. Вот то, что может быть аргументом "за" использование документоориентированных баз данных в проекте.

От чего придется отказаться, отказавшись от использования реляционных баз данных: как известно, ACID и JOIN. (Хотя тут есть варианты)

Итак, с мотивацией определились. Теперь я хотел бы сказать, что именно репликация и шардирование, по моему мнению, в MongoDB как раз не самый лучший из вариантов среди NoSQL. Поэтому я всегда смотрел и на других лидеров.

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

CouchDB, также как и реляционные базы данных, испытала влияние MongoDB, и в одной из новых версий получила более удобный язык запросов mango, который похож на язык запросов MongoDB.

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

Поэтому следующим кандидатом я всегда рассматривал ArangoDB. Эта база данных поддерживает работу с документами и графами. До версии 3.4 ArangoDB имела проблемы при работе с большими коллекциями (которые превышают объем оперативной памяти) - вплоть до полного зависания сервера. Проблема эта фигурировала в issue, и даже отрицалась разработчиками, но была пофикшена в версии 3.4, что открыло возможности для ее использования.

В плане репликации и шардирования ArangoDB напоминает CouchDB. И даже гарантирует ACID в Еnterprise версии продукта.

JOIN в ArangoDB также не является проблемой. Вот так будет выглядеть классический запрос MANY-TO-MANY:

 FOR a IN author      FOR ba IN bookauthor      FILTER a._id == ba.author        FOR b IN book        FILTER b._id == ba.book        SORT a.name, b.title    RETURN { author: a, book: b }

Это не выполняемый код на JavaScript с полным перебором всех записей (как может показаться на первый взгляд). Это такой очень оригинальный язык запросов, с синтаксисом JavaScript который является аналогом SQL и называется AQL. Подробности можно узнать в статье на Хабре.

ArangoDB также позволяет публиковать REST сервисы прямо в базе данных (Foxx) и делать поисковые запросы, включая нечеткий поиск (fuzzy search), что позволяет строить поиск без Elasticsearch (Elasticsearch конечно более мощный инструмент, но проблему составляет синхронизация данных в основной базе данных и в Elasticsearch).

Однако, на сегодняшний день нет ODM, которая позволила бы удобно и надежно работать с ArangoDB, и это существенный сдерживающий фактор в её использовании в реальном проекте. Поэтому я неоднократно приступал к поиску решения для ODM. В конце концов решение начало приобретать явные очертания и я его представляю в этом сообщении.

Самое существенное, что есть в решении - это понимание того функционала, который должна решать ODM. В этом смысле я хочу сослаться на проект universal-router (см. сообщение на Хабре), который из всего многообразия функций, к которым мы уже успели привыкнуть в роутерах React.js или Vue.js, выделили главный функционал на котором построили свое решение.

Если говорить об ODM, таким функционалом, без которого не обойтись, является по моему мнению ровно три функции:
1) Преобразование JSON (полученного из базы данных) в типизированную модель или коллекцию;

2) Преобразование типизированной модели или коллекции в JSON для сохранения в базе данных;

3) Преобразование типизированной модели или коллекции в JSON для отправки клиенту.

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

При этом есть смысл максимально использовать возможности Typescript (например декораторы, рефлексию, метаданные).

export class Reposytory {  private db: Database;  constructor() {    this.db = db;  }  @collection(Author)  async authorFindAll() {    const row = await this.db.query({      query: 'for row in authors return row',      bindVars: {},    });    return row.all();  }  @model(Author)  async authorCreate(author: Author): Promise<Author> {    const doc = await this.db.query({      query: 'insert @author into authors let doc = NEW return doc',      bindVars: { 'author': author }    });    return doc.next();  }}

Код декораторов будет довольно лаконичен:

export function collection(Constructor: new(...args: any[]) => void) {  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => {      const originalValue = descriptor.value;      descriptor.value = async function(...args: any[]) {        const plainData = await originalValue.apply(this, args)        const data = new Array();        plainData.forEach((item: any) => data.push(new Constructor(item)));        return data;      }    }}export function model(Constructor: new(...args: any[]) => void) {  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => {      const originalValue = descriptor.value;      descriptor.value = async function(...args: any[]) {        const plainData = await originalValue.apply(this, args)        return new Constructor(plainData);      }    }}

Что касается модели документа, я построил схему модели на таких декораторах:

attr(Type?) - атрибут - то есть то что сохраняется в базе данных и не является вычислимым параметром;

optional(Type?) - не обязательное значение;

array(Type?) - типизированная коллекция;

translatable(Type?) - значение, сохраняющееся в базе данных виде объекта и возвращающееся клиенту в виде строки с выбранной локалью;

group(...args: string[]) - список групп клиентов, которым доступно значение.

На таких декораторах можно построить описание объекта:

import {Model, ModelType} from '../src';import {optional, attr, array, getter, group, _type, translatable} from '../src';import {Translatable, TranslatableType} from '../src';interface AddressType extends ModelType {    city: string,    street: Translatable,    house: string,    appartment?: number,}interface AuthorType extends ModelType{    name: Translatable,    address: AddressType,}export class Address extends Model<AddressType> implements AddressType {    @attr()    @group('admin', 'user')    @translatable(Translatable)    public city!: string;    @attr()    @group('admin')    @translatable(Translatable)    public street!: Translatable;    @attr()    @group('admin')    public house!: string;    @attr()    @optional()    @group('admin')    public appartment?: number;}export class Author extends Model<AuthorType> implements AuthorType  {    @attr(Translatable)    @translatable(Translatable)    @group('admin', 'user')    public name!: string;    @attr(Address)    @group('admin', 'user')    public address!: Address}

Результаты исследования представлены в репозитарии.

Желающих обсудить приглашаю в чат https://t.me/arangodb_odm

Подробнее..
Категории: Javascript , Node.js , Nosql , Nodejs , Mongodb , Odm , Arangodb , Couchdb , Orientdb

Как лицензируется и чем отличаются лицензии Elastic Stack (Elasticsearch)

05.11.2020 02:15:38 | Автор: admin
В этой статье расскажем как лицензируется Elastic Stack, какие бывают лицензии, что туда входит (ключевые возможности), немножечко сравним Elastic с OpenDistro от AWS и другими известными дистрибутивами.



Как видно на картинке выше, существует 5 типов, условно говоря, подписок, по которым можно пользоваться системой. Подробности по тому, что написано ниже, вы можете узнать на специальной странице Elastic. Всё написанное в этой статье применимо к Elastic Stack, размещаемому на собственной инфраструктуре (on-premise).

Open Source. Это версия Elastic Stack, которая находится в свободном доступе в репозитории Elastic на Github. В принципе, вы можете взять ее и сделать убийцу Arcsight, QRadar, Splunk и других прямых конкурентов Elastic. Платить за это ничего не нужно.

Basic. Этот тип лицензии включает в себя возможности предыдущей лицензии, но дополнен функционалом, который не имеет открытого кода, но, тем не менее, доступен на бесплатной основе. Это, например, SIEM, доступ к ролевой модели, некоторые виды визуализаций в Kibana, Index Lifecycle Management, некоторые встроенные интеграции и другие возможности.

На этом бесплатные лицензии закончились и пришло время разобраться с платными лицензиями. Elastic Stack лицензируется по количеству нод Elasticsearch. Рядом может стоять хоть миллион Kibana и Logstash (или Fluentd, если угодно), но лицензии будут считаться именно по хостам, на которых развернут Elasticsearch. В расчет лицензий также не входят ноды с ролями Ingest, Client/Coordinating. На попадающее в расчет количество нод напрямую влияет объем входящего трафика и требования к хранению данных. Напомним, что для обеспечения надежности работы кластера, в нем должно быть минимум 3 ноды. Мы проводим расчет сайзинга исходя из методики, которую описывали в одной из предыдущих статей. При покупке лицензий Elasticsearch доступен только формат подписки длительностью от 1 года с шагом в 1 год (2, 3 и так далее). Теперь вернемся к типам лицензий.

Gold. В лицензии Elasticsearch уровня Gold появляется поддержка авторизации через LDAP/AD, расширеное логирование для внутреннего аудита, расширяются возможности алертинга и техподдержка вендора в рабочие часы. Именно подписка уровня Gold очень похожа на AWS OpenDistro.

Platinum. Наиболее популярный тип подписки. кроме возможностей уровня Gold, тут появляется встроенное в Elastic машинное обучение, кросс-кластерная репликация, поддержка клиентов ODBC/JDBC, возможность гранулярного управления доступом до уровня документов, поддержка вендора 24/7/365 и некоторые другие возможности. Ещё в рамках этой подписки они могут выпускать Emergency patches.

Enterprise. Самый выскоий уровень подписки. Кроме всех возможностей уровня Platinum, сюда входят оркестратор Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, решение по безопасности для конечных устройств Endgame (со всеми его возможностями), поддержка вендором неограниченного количества проектов на базе Elastic и другие возможности. Обычно используется в крупных и очень крупных инсталляциях.

У Elastic появилось уже немало форков, самый известный из которых OpenDistro от AWS. Его ключевым преимуществом является поддержка некоторых возможностей оригинального Elastic, доступных на платных подписках. Основные это интеграция с LDAP/AD (а еще SAML, Kerberos и другими), встроенный алертинг (на бесплатном Elastic это реализуется через Elast Alert), логирование действий пользователей и поддержка JDBC-драйверов.

Упомянем также про HELK и Logz.io. Первый проект на Github, который обвешивает Elasticsearch дополнительным ПО для аналитики угроз (пишут, что пока это всё находится в альфе), а второй облачный сервис, основанный на Elastic и добавляющий некоторые приятные фичи. В комментариях можно поделиться другими форками, о которых вам известно.

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

А ещё можно почитать:

Сайзинг Elasticsearch

Разбираемся с Machine Learning в Elastic Stack (он же Elasticsearch, он же ELK)

Elastic под замком: включаем опции безопасности кластера Elasticsearch для доступа изнутри и снаружи

Что полезного можно вытащить из логов рабочей станции на базе ОС Windows
Подробнее..

Elasticsearch сайзинг шардов как завещал Elastic анонс вебинара предложения по митапу

15.03.2021 20:13:27 | Автор: admin

Каждый индекс Elasticsearch состоит из шардов. Шарды это логическое и физическое разделение индекса. В этой статье мы расскажем о сайзинге шардов Elasticsearch важной его части, серьёзно влияющей на производительность кластера. В высоконагруженных системах выбор правильной конфигурации архитектуры хранения позволит серьёзно сэкономить на железе. Бльшая часть статьи основана на соответствующем разделе документации Elastic. Подробности под катом.

Сайзинг шардов Elasicsearch


Как Elasticsearch работает с шардами


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

Каждый шард использует ресурсы памяти и процессора. Небольшое количество бльших по объёму шардов использует меньше ресурсов, чем множество мелких.

Давайте теперь краем глаза взглянем на сегменты (см. картинку ниже). Каждый шард Elasticsearch является индексом Lucene. Максимальное количество документов, которое можно закинуть в индекс Lucene 2 147 483 519. Индекс Lucene разделен на блоки данных меньшего размера, называемые сегментами. Сегмент это небольшой индекс Lucene. Lucene выполняет поиск во всех сегментах последовательно. Большинство шардов содержат несколько сегментов, в которых хранятся данные индекса. Elasticsearch хранит метаданные сегментов в JVM Heap, чтобы их можно было быстро извлечь для поиска. По мере роста объёма шарда его сегменты объединяются в меньшее количество более крупных сегментов. Это уменьшает количество сегментов, что означает, что в динамической памяти хранится меньше метаданных (см. также forcemerge, к которому мы вернемся чуть дальше в статье).

Еще стоит сказать о ребалансировке кластера. Если добавляется новая нода или одна из нод выходит из строя, происходит ребалансировка кластера. Ребалансировка сама по себе недешёвая с точки зрения производительности операция. Кластер сбалансирован, если он имеет равное количество шардов на каждой ноде и отсутствует концентрация шардов любого индекса на любой ноде. Elasticsearch запускает автоматический процесс, называемый ребалансировкой, который перемещает шарды между узлами в кластере, чтобы его сбалансировать. При перебалансировке применяются заранее заданные правила выделения сегментов (об allocation awareness и других правилах мы подробнее расскажем в одной из следующих статей). Если вы используете data tiers, Elasticsearch автоматически разместит каждый шард на соответствующем уровне. Балансировщик работает независимо на каждом уровне.

Как заставить Elasticsearch ещё лучше работать с шардами


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

Создавать шарды размером от 10 до 50 ГБ. Elastic говорит, шарды размером более 50 ГБ потенциально могут снизить вероятность восстановления кластера после сбоя. Из-за той самой ребалансировки, о которой мы говорили в начале статьи. Ну, и большие шарды накладнее передавать по сети. Предел в 50 ГБ выглядит, конечно, как сферический конь в вакууме, поэтому мы сами больше склоняемся к 10 ГБ. Вот тут человек советует 10 ГБ и смотреть на размер документов в следующем плане:

  • От 0 до 4 миллионов документов на индекс: 1 шард.
  • От 4 до 5 миллионов документов на индекс: 2 шарда.
  • Более 5 миллионов документов считать по формуле: (количество документов / 5 миллионов) + 1 шард.

20 или менее шардов на 1 ГБ JVM Heap. Количество шардов, которыми может жонглировать нода, пропорциональны объему JVM Heap ноды. Например, нода с 30 ГБ JVM Heap должна иметь не более 600 шардов. Чем меньше, тем, скорее всего, лучше. Если это пропорция не выполняется можно добавить ноду. Посмотрим сколько там используется JVM Heap на каждой ноде:



А теперь посмотрим сколько шардов на каждой ноде и видим, что с нашим тестовым стендов всё в порядке. Жить будет.



Количество шардов на узле можно ограничить при помощи опции index.routing.allocation.total_shards_per_node, но если их уже много, присмотритесь к Shrink API.

Совсем необязательно создавать индексы размером в 1 день. Часто встречали у заказчиков подход, при котором каждый новый день создавался новый индекс. Иногда это оправдано, иногда можно и месяц подождать. Ролловер ведь можно запускать не только с max_age, но и с max_size или max_docs. На Хабре была статья, в которой Адель Сачков, в ту пору из Яндекс Денег (сейчас уже нет), делился полезным лайфхаком: создавал индексы не в момент наступления новых суток, а заранее, чтобы этот процесс не аффектил на производительность кластера, но у него там были микросервисы.
каждые сутки создаются новые индексы по числу микросервисов поэтому раньше каждую ночь эластик впадал в клинч примерно на 8 минут, пока создавалась сотня новых индексов, несколько сотен новых шардов, график нагрузки на диски уходил в полку, вырастали очереди на отправку логов в эластик на хостах, и Zabbix расцветал алертами как новогодняя ёлка. Чтобы этого избежать, по здравому размышлению был написан скрипт на Python для предварительного создания индексов.

С новогодней ёлкой неплохой каламбурчик получился.

Не пренебрегайте ILM и forcemerge. Индексы должны плавно перетекать между соответствующими нодами согласно ILM. В OpenDistro есть аналогичный механизм.



С индексами, в которые уже не ведется запись можно выполнить forcemerge слияние меньших по размеру сегментов в более крупные. Это в итоге снизит накладные расходы на эксплуатацию шардов и повысит скорость поиска. Forcemerge требует значительных ресурсов, поэтому лучше это делать к какие-то в непиковые часы. Добавим, что forcemerge это фактические создание нового сегмента из двух старых, поэтому свободное место на диске лишним точно не будет.

Приходите в комментарии и расскажите о своём опыте с раскладыванием шардов по нодам. Было бы интересно узнать о том, что работает в вашем случае.

Анонс вебинара. Elastic приглашает посетить 17 марта в 12 часов по московскому времени вебинар Elastic Telco Day: Applications and operational highlights from telco environments. Эксперты расскажут о применении в решений Elastic в телекоме. Регистрация.

Предложения по митапу. Планируем проведение онлайн-митап по Elastic в апреле. Напишите в комментариях или в личку какие темы вам было бы интересно разобрать, каких спикеров услышать. Если бы вы хотели сами выступить и у вас есть что рассказать, тоже напишите. Вступайте в группу Elastic Moscow User Group, чтобы не пропустить анонс митапа.

Канал в телеге. Подписывайтесь на наш канал Elastic Stack Recipes, там интересные материалы и анонсы мероприятий.

Читайте наши другие статьи:




Если вас интересуют услуги внедрения, администрирования и поддержки Elastic Stack, вы можете оставить заявку в форме обратной связи на специальной странице.
Подробнее..

ArangoDB в реальном проекте

15.03.2021 04:23:51 | Автор: admin

ArangoDB гибридная (документная и графовая) база данных. К ее положительным сторонам относятся:


  • мощный и удобный язык запросов AQL
  • JOIN (даже более мощный чем в реляционных базах данных)
  • репликация и шардинг
  • ACID (в кластере работает только в платной версии)

Из менее существенных, но не менее удобных возможностей:


  • нечеткий поиск
  • встроенный в базу данных движок микросервисов Foxx
  • работа в режиме подписки на изменения в базе данных

Справедливости ради отмечу и недостатки:


  • отсутствие ODM
  • низкая популярность (в сравнении например с MongoDB)

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


Возможности AQL (ArangoDB Query Language)


Один из главных вопросов, который меня волновал, будет ли выразительность AQL достаточной для выполнения всего спектра запросов в реальном приложении. И будет ли работа без ORM/ODM достаточно комфортной.


В ArangoDB есть несколько способов сделать запрос к данным. Есть привычный для тех, кто работает с MongoDB, объектно-ориентированный API, но такой способ в ArangoDB считается устаревшим и основной упор делается на запросы AQL.


Простейший запрос к одной коллекции выглядит так:


db.query({  query: `for doc in managers    filter doc.role == @role    sort doc.@field @order    limit @page * @perPage, @perPage    return doc`,  bindVars: { role, page, perPage, field, order },});

Такой вот интересный язык запросов, построенный на ключевом слове FOR, которое в данном случае не означает перебор всех документов коллекции, если, конечно, по полю role создан индекс.


В большинстве случаев, для работы приложения нужно выбрать связанные объекты из нескольких коллекций. В библиотеке mongoose (MongoDB) для этого используют метод populate(). В ArangoDB это можно сделать одним запросом AQL:


db.query({   query: `      for mall in malls        for city in cities          filter mall.cityId == city._key      return merge(mall, { city })  `,  bindVars: { },});

Это типичный INNER JOIN. Только немного удобнее, так как объект city будет присутствовать в виде вложенного объекта, а не сольётся в список полей, как это происходит в стандартном SQL.


Что касается LEFT JOIN для его реализации нужно использовать подзапросы и ключевое слово LET:


db.query({  query: `    for city in cities      let malls=(        for mall in malls          filter mall.cityId==city._key          return mall      )    return merge(city, {malls})`,  bindVars: { },});

Результирующий объект будет содержать поле malls типа array или значение null. Как Вы можете заметить, есть отличие от LEFT JOIN в стандартном SQL это то, что количество объектов в результирующей коллекции будет равно количеству объектов в коллекции city, и не будет повторяться для каждого значение mall. Вместо этого mall представлено массивом. Я бы сказал, что такой вариант даже более удобен для работы. Получить же "классический" результат, как в SQL, также можно, но запрос будет более сложный.


Я привел самые простые запросы из тех, что есть в реальном приложении. Но, как выяснилось, на базе вышеперечисленных средств можно строить самые сложные запросы, которые не менее, а может быть и более выразительны, чем запросы SQL. При этом умолчу о других документо-ориентированных NoSQL базах данных, где аналогичные запросы просто невозможны.


Графы


Для реализации граф-ориентированных возможностей в ArangoDB применяются коллекции ребер графа. Документы в этой коллекции отличаются от документов в простых коллекциях наличием двух служебных полей: _from и _to. Работать с коллекциями ребер графом можно теми же средствами, что и с коллекциями документов. В дополнение существует несколько специальных средств для обходя графов.


Я планировал реализовать на граф-ориентированных возможностях дерево категорий товаров. Однако, реализация операций update оказалась неожиданно сложной. Поэтому я отказался от этой идеи. Возможно, просто не нашел еще ключ к этим возможностям.


Нечеткий поиск


Есть такая часто встречающаяся задача: искать текст, если в исходной строке есть опечатки или ошибки. Как правило, для этого используется база данных Elacticsearch. У такого решения есть два недостатка. Во-первых, нужно согласовывать в режиме реального времени значения в основной базе данных и в Elasticsearch. Это непросто, часто эти значения расходятся, и тогда приходится принудительно переиндексировать базу данных. И, во-вторых, Elasticsearch требовательна по ресурсам, что также не всегда приемлемо из соображений финансового порядка.


В последних версиях ArangoDB можно создать SEARCH VIEW в котором можно искать значения с неполным совпадением:


  await db.createAnalyzer('fuzzy_brand_search_bigram', {    type: 'ngram',    properties: { min: 2, max: 2, preserveOriginal: true },    features: ['position', 'frequency', 'norm'],  });  await db.createView('brandSearch', {    links: {      brands: {        includeAllFields: true,        analyzers: ['fuzzy_brand_search_bigram'],      },    },  });

Сам запрос выглядит так:


db.query({    query: `       for brand in brandSearch          search NGRAM_MATCH(              brand.name,               @brandName,               0.4,               'fuzzy_brand_search_bigram'          )          filter brand.mallId == @mallId        return brand `,    bindVars: { mallId, brandName },});

Без ODM?


В своей статье я показал, что по статистике, MongoDB в половине случаев используется без ODM. То есть, это достаточно распространенная практика.


Действительно, сделать запрос, как это было показано выше, гораздо проще средствами AQL, чем определять схему с разными видами связей. Во всяком случае, не было еще ни одного проекта на Sequelize (ORM для реляционных баз данных), где не пришлось бы сделать один-два RAW запроса.


Однако, я, тем не менее, сторонник использования ODM. В своей статье я описал, что я хотел бы от ODM для ArangoDB. ODM не обязательно должна заниматься генерацией запросов в базу данных. Я бы хотел, чтобы ODM обеспечивала сохранение в базу данных только нужных полей, и следила за наличием обязательных полей. А при получении объекта из базы данных типизировала его, добавляла вычислимые поля, фильтровала набор полей для разных групп запросов, и обеспечивала локализацию значений полей.


В настоящее время я нашел всего один фреймвёрк, который очень близок к тому, что я хочу получить: https://github.com/rawmodel/framework. Но мне в нем не хватает двух возможностей. Во-первых для методов типа PATCH входной объект, как правило, содержит не все, а только изменяемые поля. Для таких запросов нужно отключать полные правила валидации. И, во-вторых, там невозможно сделать локализацию значений. Я незамедлительно создал два issue в этом репозитарии. К чести автора, он ответил почти мгновенно, но ответ меня далеко не устроил. По первому вопросу он рекомендовал сначала забирать полный объект из базы данных, а затем мерджить его с объектом с неполным набором полей. По второму порекомендовал локализацию делать на фронтенде.


В своей статье я описал и реализовал свою библиотеку. Её и использовал в реальном проекте. Конечно, были моменты стресса, когда выходило, что возможностей этой библиотеки недостаточно. Но их в основном удалось разрешить. Так что по-прежнему приглашаю к сотрудничеству желающих продвигать технологию ArangoDB.


apapacy@gmail.com
15 марта 2021 года

Подробнее..

Как бы я сейчас объяснил молодому себе зачем существуют требования ACID для баз данных?

30.12.2020 14:14:51 | Автор: admin
Фотограф: Elliott ErwittФотограф: Elliott Erwitt

Я выскочка. По крайней мере, так я себя иногда ощущаю. Закончив второй курс политологии и журналистики в университете, я увидел американский рейтинг профессий по уровню оплаты труда. Журналист в этом рейтинге был на последнем месте, а на первых местах были data scientists и data engineers (политолога в этом списке, почему-то, не было). Я не знал, кто составлял этот список, и понятия не имел, кто такие эти data-челы с первых строк, но он меня впечатлил. Я бросил пить и начал проходить курсы на Coursera, а потом каким-то чудом заполучил студенческую подработку в стартапе. Так я сделал своё войти в IT.

Когда человек, не имеющий университетской подготовки, пытается начать программировать, то он чувствует себя несчастным, который, увидев из окна солнце, вышел на улицу и попал под неожиданный в столь прекрасный день град: шаблоны проектирования, функции, классы, ООП, инкапсуляция, протоколы, потоки, ACID Хочется прокричать, как Виктор Фёдорович в своё время:

И вот опять, просто открыв с утра какую-то статью на Хабре, ты по итогу увязаешь в томах Википедии, пытаясь понять какую-то абстрактную мутотень. Проходит год, второй ты поучаствовал в нескольких проектах, разного повидал, и постепенно всё начинает становиться на свои места, и ты становишься восприимчив не только к конкретным инструкциям на Stack Overflow, но уже читаешь документацию и начинаешь проникаться какими-то концепциями. Ещё пара лет, и ты осознаёшь ценность вышеперечисленных терминов и аббревиатур, которые, оказывается, были придуманы для того, чтобы сделать жизнь разработчика не мучительнее, а легче.

Говорят, что процесс познания важнее самого знания. И всё же, если бы кто-то мне правильно объяснил некоторые из концепций, которыми я сам сейчас охотно пользуюсь, чуть раньше, то, возможно, я был бы сейчас лучшим разработчиком, чем я есть. Время назад не перемотаешь, но формализовать свой добытый потом и кровью опыт в виде доступного текста я могу. Зачем? Я разложу свои мысли по полочкам, а вы в который раз почитаете про ненавистный ACID и, возможно, узнаете что-то новое.

Дело в том, что многие разработчики, которых я знаю, имеют весьма отдалённое представление о том, что такое ACID и зачем он нужен, в чём именно различаются реляционные базы данных и NoSQL и как выбрать ту базу данных, которая будет отвечать требованиям приложения. Вот в этом всём и попробуем разобраться.

Что вы узнаете из статьи

Что побудило меня написать эту статью, если статей о ACID уже много? Дело в том, что большинство статей про ACID производили на меня одно из двух впечатлений: либо их авторы сами не разобрались толком в предмете (будет забавно, если я произведу такое же впечатление), либо они знают предмет настолько хорошо, что многие вещи кажутся им очевидными и недостойными описания (это было бы чуть более лестно). И те, и другие скудно описывают каждую из букв в аббревиатуре и в лучшем случае дают пример типичной финансовой транзакции: деньги снял деньги перевёл.

Мне не удастся полностью избежать этого заезженного примера, но я постараюсь привести и другие примеры, и вообще показать для разных понятий более широкий контекст, нежели исключительно транзакции и БД. Я покажу, как понимание транзакций может сделать ваш код лучше. Много кода в статье не будет, но кое-какие примеры вы всё-таки увидите (они будут на Python 3.X - его синтаксис будет понятен, думаю, каждому).

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

Главные тезисы для тех, кому лень читать всё

  • ACID это стандарт того, какие гарантии должна давать база данных (далее: БД), чтобы поддерживать транзакции; он не указывает деталей реализации;

  • Если вкратце, то транзакция это способ изменить состояние взаимосвязанных данных сразу в нескольких местах (например, в таблицах одной БД или в разных БД) так, чтобы эти изменения были действительными с точки зрения нескольких критериев;

  • Транзакции нужны далеко не каждому приложению;

  • Когда речь заходит о том, выполняет ли БД требования ACID, то, как правило, речь заходит о том, гарантирует ли она изолированность транзакций. Изолированные транзакции - те, что не видят промежуточные значения других транзакций;

  • Некоторые термины (например, согласованность) могут пониматься по-разному в зависимости от контекста, поэтому вместо того, чтобы искать где-то короткий ответ на вопрос, даёт ли БД гарантии ACID или какие-то другие, нужно углубиться в документацию и смотреть, как именно реализованы те или иные механизмы в этой базе данных. Ещё лучше тестировать всё самому;

  • Если БД не предлагает гарантии ACID, то их можно частично реализовать самому в приложении. Принципы реализации этих гарантий важно понимать любому разработчику, потому что они в любом случае помогут сделать его код лучше;

  • Любая БД, как и любая технология и подход вообще это компромисс, на который придётся пойти в угоду чему-то. Многие БД NoSQL жертвуют согласованностью данных или другими свойствами из ACID ради более высокой производительности, получая которую, вы перекладываете дополнительную ответственность на ваше приложение. Либо вы используете БД, которая предоставляет гарантии ACID, и лишаете себя головной боли, но получаете меньшую производительность;

  • Для правильного выбора БД важно понимать требования вашего приложения по скорости записи и чтения данных, одновременности доступа к данным, наличию взаимосвязей внутри данных и т.д..

Повесть об ACID

Фото с протестов, США, конец 1960-хФото с протестов, США, конец 1960-х

Проблема одновременности

Для начала немного отдалённой теории.

Любая информационная система (или попросту, приложение), которую создают программисты, состоит из нескольких типичных блоков, каждый из которых обеспечивают часть необходимой функциональности. Например, кэш используется для того, чтобы запоминать результат ресурсоёмкой операции для обеспечения более быстрого чтения данных клиентом, инструменты потоковой обработки позволяют отправлять сообщения другим компонентам для асинхронной обработки, а инструменты пакетной обработки используются для того, чтобы с некой периодичностью разгребать накопившиеся объёмы данных. И практически в каждом приложении так или иначе задействованы базы данных (БД), которые обычно выполняют две функции: сохранять при получении от вас данные и позднее предоставлять их вам по запросу. Редко кто задумывает создать свою БД, потому что существует уже множество готовых решений. Но как выбрать именно ту, которая подойдёт вашему приложению?

Итак, давайте представим себе, что вы написали приложение, с мобильным интерфейсом, которое позволяет загружать сохранённый ранее список дел по дому то есть, читать из БД, и дополнять его новыми заданиями, а также расставлять приоритеты для каждого конкретного задания от 1 (самый высокий) до 3 (самый низкий). Допустим, ваше мобильное приложение в каждый момент времени использует только один человек. Но вот вы осмелились рассказать о своём творении маме, и теперь она стала вторым постоянным пользователем. Что произойдёт, если вы решите одновременно, прямо в ту же миллисекунду, поставить какому-то заданию "помыть окна" разную степень приоритета?

Говоря профессиональным языком, ваш и мамин запросы в БД можно рассмотреть как 2 процесса, которые совершили запрос в БД. Процесс это сущность компьютерной программы, которая может выполняться в одном или нескольких потоках. Обычно процесс обладает образом машинного кода, памятью, контекстом и другими ресурсами. Иными словами характеризовать процесс можно как выполнение инструкций программы на процессоре. Когда ваше приложение делает запрос в БД, то мы говорим о том, что ваша БД обрабатывает полученный по сети запрос от одного процесса. Если пользователей, одновременно сидящих в приложении, двое, то и процессов в какой-то конкретный момент времени может быть двое.

Когда какой-то процесс делает запрос в БД, он застаёт её в определённом состоянии. Система, имеющая состояние (stateful) это такая система, которая помнит предыдущие события и хранит некую информацию, которая и называется состоянием. Переменная, объявленная как integer, может иметь состояние 0, 1, 2 или, скажем, 42. Mutex (взаимное исключение) имеет два состояния: locked или unlocked, так же, как и двоичный семафор (required vs. released) и вообще двоичные (бинарные) типы данных и переменные, которые могут иметь только два состояния 1 или 0. На основе понятия состояния базируются несколько математических и инженерных конструкций, таких как конечный автомат модель, которая имеет по одному входу и выходу и в каждый момент времени находящаяся в одном из конечного множества состояний и шаблон проектирования состояние, при котором объект меняет поведение в зависимости от внутреннего состояния (например, в зависимости от того, какое значение присвоено той или иной переменной).

Итак, большинство объектов в мире машин имеет некое состояние, которое с течением времени может меняться: наша pipeline, обрабатывающая большой пакет данных, выдаёт ошибку и становится failed, либо свойство объекта Кошелёк, хранящее сумму денег, оставшихся на счету пользователя, меняется после поступления на счёт зарплаты. Переход (transition) от одного состояния к другому скажем, от in progress к failed называется операцией. Наверное, всем известны операции CRUD create , read , update , delete , либо аналогичные им методы HTTP POST , GET , PUT , DELETE Но программисты в своём коде часто дают операциям другие имена, потому что операция может быть более сложной, чем просто прочитать некое значение из базы данных она может заодно проверить данные, и тогда наша операция, приобретшая вид функции, будет называться, например, validate() А кто выполняет эти операции-функции? Уже описанные нами процессы.

Ещё немного, и вы поймёте, почему я так подробно описываю термины!

Любая операция будь то функция, или, в распределённых системах, посылка запроса к другому серверу имеет 2 свойства: время вызова (invocation time) и время завершения (completion time), которое будет строго больше времени вызова (исследователи из Jepsen исходят из теоретического предположения, что оба этих timestamp будут даны воображаемыми, полностью синхронизированными, глобально доступными часами). Давайте представим себе наше приложение со списком дел. Вы через мобильный интерфейс делаете запрос в БД в 14:00:00.014, а ваша мама в 13:59:59.678 (то есть, за 336 миллисекунд до этого) через тот же интерфейс обновила список дел, добавив в него мытьё посуды. Учитывая задержку сети и возможную очередь заданий для вашей БД, если кроме вас с мамой вашим приложением пользуются ещё все мамины подруги, БД может выполнить мамин запрос уже после того, как обработает ваш. Иными словами, есть вероятность того, что два ваших запроса, а также запросы маминых подруг будут направлены на одни и те же данные одновременно (concurrently).

Так мы и подошли к важнейшему термину в области БД и распределённых приложений concurrency. Что именно может означать одновременность двух операций? Если даны некая операция T1 и некая операция T2, то:

  • Т1 может быть начата до времени начала исполнения Т2, а закончена между временем начала и конца исполнения Т2,

  • Т2 может быть начата до времени начала исполнения Т1, а закончена между временем начала и конца исполнения Т1,

  • Т1 может быть начата и закончена между временем начала и конца исполнения Т1,

  • и любой другой сценарий, при котором T1 и T2 имеют некое общее время выполнения.

Понятно, что в рамках данной статьи мы говорим в первую очередь про запросы, поступающие в БД, и то, как система управления БД эти запросы воспринимает, но термин конкурентности важен, например, и в контексте операционных систем. Я не буду слишком сильно отходить в сторону от темы данной статьи, но считаю важным упомянуть, что конкурентность, о которой мы здесь говорим, не связана с дилеммой о конкурентности и параллелизме и их разнице, которую обсуждают в контексте работы операционных систем и high-performance computing. Параллелизм это один из способов достижения конкурентности в среде с несколькими ядрами, процессорами или компьютерами. Мы же говорим о конкурентности в значении одновременного доступа разных процессов к общим данным.

Справка для тех, кто не хочет путать конкурентность и параллелизм, как сделал я при сдаче диплома.

Компьютерная программа после компиляции в бинарный код может быть исполнена либо более легковесным потоком выполнения, либо процессом. Если у вашего компьютера один одноядерный CPU (процессор), что в 2020 году довольно маловероятно, то ваша программа не сможет быть исполнена параллельно ни на уровне потоков, ни на уровне процессов. В этом случае CPU используется одновременно попеременно несколькими потоками или процессами, которые сменяются друг другом программным кодом, который называется планировщиком (или диспетчером) и использует алгоритм планирования выполнения задач. Он попеременно даёт каждому заданию некое окно времени (time slice). В этом случае мы говорим о конкурентности, но не о параллелизме, который мы получаем, когда наш CPU имеет несколько ядер, либо мы имеем несколько процессоров. Поток выполнения может выполняться параллельно на разных ядрах одного CPU, в то время, как параллельные процессы могут быть запущены на разных ядрах, процессорах и даже физических узлах (компьютерах). Если вас интересует разница между потоками и процессами, а также вы хотите узнать конкретный пример того, как использование процессов вместо потоков дало преимущество Google Chrome, можете ознакомиться вот с этим материалом).

Давайте вернёмся к нашему примеру. Вы читаете данные из БД в то время, как другой пользователь эти данные меняет. А что произойдёт, если вы соберётесь, например, отредактировать описание одного и то же задания в один и тот же момент? А что, если над одними и теми же данными одновременно будут работать тысячи людей?

А что, собственно, может пойти не так, чисто теоретически?

При работе над общими данными могут произойти многочисленные проблемы, связанные с конкурентностью, также названные race conditions. Первая проблема возникает тогда, когда процесс получает данные, которые он не должен был получить: неполные, временные, отменённые или по какой-то иной причине неправильные данные. Вторая проблема когда процесс получает неактуальные данные, то есть данные, которые не соответствуют последнему сохранённому состоянию БД. Скажем, какое-то приложение сняло деньги со счёта пользователя с нулевым балансом, потому что БД вернуло приложению состояние счёта, не учитывающее последнее снятие денег с него, произошедшее буквально пару миллисекунд назад. Ситуация так себе, не правда ли?

Транзакции пришли, чтобы спасти нас

Race condition - явление неприятное и опасное.Race condition - явление неприятное и опасное.

Для того, чтобы решать такие проблемы, и появилось понятие транзакции некоей группы последовательных операций (изменений состоянии) с БД, которая представляет собой логически единую операцию. Снова приведу пример с банком и не случайно, ведь концепция транзакции появилась, судя по всему, именно в контексте работы с деньгами. Классический пример транзакции - перевод денег с одного банковского счета на другой: вам необходимо сначала снять сумму с исходного счета, а затем внести ее на целевой счет. Чтобы эта транзакция была осуществлена, приложению понадобится совершить несколько действий в БД: проверка баланса отправителя, блокировка суммы на счету отправителя, прибавление суммы к счёту получателя и вычет суммы у отправителя. Требований к такой транзакции будет несколько. Например, приложение не может получить устаревшую или неверную информацию о балансе например, в случае, если в то же время параллельная транзакция завершилась ошибкой на полпути, и средства со счёта так и не списались а наше приложение уже получило информацию о том, что средства были списаны.

Решить эту проблему было призвано такое свойство транзакции, как изолированность: наша транзакция выполняется так, словно других транзакций, выполняемых в тот же момент, не существует. Наша БД выполняет одновременные операции так, словно она выполняет их друг за другом, sequentially собственно, самый высокий уровень изоляции и называется Strict Serializable. Да, самый высокий, что означает, что уровней бывает несколько.

Стоп, - скажете вы. Попридержи коней, сударь.

Давайте вспомним, как я описывал, что каждая операция имеет время вызова и время выполнения. Для удобства можно рассматривать вызов и выполнение как 2 действия. Тогда отсортированный список всех действий вызова и выполнения можно назвать историей БД. Тогда уровень изоляции транзакций это набор историй. Мы используем уровни изоляции, чтобы определить, какие истории являются хорошими. Когда мы говорим, что история нарушает сериализуемость или не сериализуема, мы имеем в виду, что история не входит в набор сериализуемых историй.

Чтобы было понятно, про какого рода истории мы говорим, приведу примеры. Например, есть такой вид истории "intermediate read". Он происходит, когда транзакции А разрешено читать данные из строки, которая была изменена другой запущенной транзакцией Б и еще не зафиксирована ("not committed") - то есть, фактически, измнения ещё не были окончательно совершены транзакцией Б, и она может в любой момент их отменить. А, например, "aborted read" это как раз наш пример с отменённой транзакцией снятия денег. Таких возможных аномалий несколько, и вы можете ознакомиться с ними более подробно вот тут или тут. То есть, аномалии это некое нежелательное состояние данных, которое может возникнуть при конкурентном доступе к БД. И чтобы избежать тех или иных нежелательных состояний, БД используют различные уровни изоляции то есть, различные уровни защиты данных от нежелательных состояний. Эти уровни (4 штуки) были перечислены в стандарте ANSI SQL-92.

Описание этих уровней некоторым исследователям кажется расплывчатым, и они предлагают свои, более детальные, классификации. Советую обратить внимание на уже упомянутый Jepsen, а также на проект Hermitage, который призван внести ясность в то, какие именно уровни изоляции предлагают конкретные СУБД, такие как MySQL или PostgreSQL. Если вы откроете файлы из этого репозитория, то вы можете увидеть, какую череду SQL-команд они применяют, чтобы тестировать БД на те или иные аномалии, и можете сделать нечто подобное для интересующих вас БД). Приведу один пример из репозитория, чтобы заинтересовать вас:

-- Database: MySQL-- Setup before testcreate table test (id int primary key, value int) engine=innodb;insert into test (id, value) values (1, 10), (2, 20);-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomalyset session transaction isolation level read uncommitted; begin; -- T1set session transaction isolation level read uncommitted; begin; -- T2update test set value = 101 where id = 1; -- T1select * from test; -- T2. Shows 1 => 101update test set value = 11 where id = 1; -- T1commit; -- T1select * from test; -- T2. Now shows 1 => 11commit; -- T2-- Result: doesn't prevent G1b
Согласитесь, неприятно оказаться в ситуации аномалии данных: вроде данные только что были здесь, а вот теперь их уже и нет? Фотограф: Rene Maltete.Согласитесь, неприятно оказаться в ситуации аномалии данных: вроде данные только что были здесь, а вот теперь их уже и нет? Фотограф: Rene Maltete.

Важно понимать, что у одной и той же БД, как правило, можно выбрать один из нескольких видов изоляции. Почему же не выбрать самую сильную изоляцию? Потому что, как и всё в информатике, выбранный уровень изоляции должен соответствовать копромиссу, на который мы готовы идти в данном случае, компромисс по скорости выполнения: чем сильнее уровень изоляции, тем медленнее будут обрабатываться запросы. Чтобы понять, какой уровень изоляции вам нужен, вам нужно понять требования к вашему приложению, а чтобы понять, предлагает ли выбранная вами БД этот уровень, придётся лезть в документацию для большинства приложений этого будет достаточно, но если у вас какие-то особенно жёсткие требования, то лучше устроить тест наподобие того, что делают ребята с проекта Hermitage.

"I" и другие буквы в ACID

Изоляция это, в основном то, что и подразумевают люди, когда говорят об ACID в целом. И именно по этой причине я начал разбор этой аббревиатуры с изоляции, а не пошёл по порядку, как обычно делают те, кто пытаются объяснить эту концепцию. А теперь давайте рассмотрим и оставшиеся три буквы.

Вспомним опять наш пример с банковским переводом. Транзакция по переводу средств с одного счета на другой включает в себя операцию вывода с первого счета и операцию пополнения на втором. Если операция пополнения второго счета не удалась, вы наверняка не хотите, чтобы операция вывода средств с первого произошла. Иными словами, либо транзакция удаётся полностью, или не происходит вообще, но она не может быть произведена лишь на какую-то часть. Это свойство называется атомарностью (atomicity), и это A в ACID.

Когда наша транзакция выполняется, то, как и любая операция, она переводит БД из одного действительного состояния в другое. Некоторые БД предлагают так называемые constraints то есть, правила, применяемые к сохраняемым данным, например, касающиеся первичных или вторичных ключей, индексов, default-значений, типов столбцов и т.д. Подробнее с ними вы сможете ознакомиться вот тут. Так вот, при осуществлении транзакции мы должны быть уверены, что все эти constraints будут выполнены. Эта гарантия получила название согласованность (consistency) и букву C в ACID (не путать с согласованностью из мира распределённых приложений, о которой мы поговорим позже). Приведу понятный пример для consistency в смысле ACID: приложение для онлайн-магазина хочет добавить в таблицу orders строку, и в столбце product_id будет указан ID из таблицы products типичный foreign key. Если продукт, скажем, был удалён из ассортимента, и, соответственно, из БД, то операция вставки строки не должна случиться, и мы получим ошибку. Эта гарантия, по сравнению с другими, немного притянута за уши, на мой взгляд хотя бы потому, что активное использование constraints от БД означает перекладывание ответственности за данные (а также частичное перекладывание бизнес-логики, если мы говорим о таком constraint, как CHECK) с приложения на БД, что, как нынче принято говорить, ну такое себе.

Ну и наконец остаётся D стойкость (durability). Системный сбой или любой другой сбой не должен приводить к потере результатов транзакции или содержимого БД. То есть, если БД ответила, что транзакция прошла успешно, то это означает, что данные были зафиксированы в энергонезависимой памяти например, на жёстком диске. Это, кстати, не означает, что вы немедленно увидите данные при следующем read-запросе. Вот буквально на днях я работал с DynamoDB от AWS (Amazon Web Services), и послал некие данные на сохранение, а получив ответ HTTP 200 (OK), или что-то вроде того, решил проверить и не видел эти данные в базе в течение последующих 10 секунд. То есть, DynamoDB зафиксировала мои данные, но не все узлы моментально синхронизировались, чтобы получить последнюю копию данных (хотя возможно, дело было и в кэше). Тут мы опять залезли на территорию согласованности в контексте распределённых систем, но момент поговорить о ней по-прежнему не настал.

Итак, теперь мы знаем, что из себя представляют гарантии ACID. И мы даже знаем, почему они полезны. Но действительно ли они нам нужны в каждом приложении? И если нет, то когда именно? Все ли БД предлагают эти гарантии, а если нет, то что они предлагают взамен?

Битва аббревиатур: BASE vs. ACID

"В химии pH измеряет относительную кислотность водного раствора. Шкала pH простирается от 0 (сильнокислые вещества) до 14 (сильнощелочные вещества); чистая вода при температуре 25 C имеет pH 7 и является нейтральной.

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

Наверное, замысел был такой: чем выше pH, т.е. чем ближе БД к "щёлочи" (BASE), тем менее надёжны транзакции.

Популярные реляционные БД, такие, как MySQL, появились как раз на почве ACID. Но за последние лет десять так называемые базы NoSQL, которые объединяют под этим названием несколько весьма различных типов БД, довольно неплохо справляются и без ACID. На самом деле, есть большое количество разработчиков, которые работают с БД NoSQL и нисколько не запариваются по поводу транзакций и их надёжности. Давайте разберёмся, правы ли они.

Нельзя общо говорить о БД NoSQL, ведь это просто удачная абстракция. БД NoSQL различаются между собой и по дизайну подсистем хранения данных, и даже по моделям данных: NoSQL это и документо-ориентированная CouchDB, и графовая Neo4J. Но если говорить о них в контексте транзакций, то все они, как правило, похожи в одном: они предоставляют ограниченные версии атомарности и изоляции, а значит, не предоставляют гарантии ACID. Чтобы понять, что это значит, давайте ответим на вопрос: а что же они предлагают, если не ACID? Ничего?

Не совсем. Ведь им, как и реляционным БД, тоже нужно продавать себя в красивой упаковке. И они придумали свою химическую аббревиатуру BASE.

BASE как антагонист

И тут я снова пойду не по порядку буковок, а начну с основополагающего термина consistency. Мне придётся нивелировать ваш эффект узнавания, ибо эта согласованность имеет мало общего с согласованностью из ACID. Проблема с термином согласованности заключается в том, что он употребляется в слишком большом кол-ве контекстов. Зато эта согласованность имеет куда более широкий контекст употребления, да и вообще это именно та согласованность, о которой идёт речь при обсуждении распределённых систем.

Реляционные БД, о которых мы говорили выше, предоставляют разные уровни изоляции транзакций, и самые строгие из них гарантируют, что одна транзакция не сможет увидеть недействительные изменения, осуществлённые другой транзакцией. Если вы стоите на кассе в магазине, и в этот момент с вашего счёта снимутся деньги за квартплату, но транзакция с переводом денег за квартплату провалится и ваш счёт снова примет прежнее значение (деньги не спишутся), то ваша транзакция оплаты на кассе не заметит всех этих телодвижений ведь та транзакция так и не прошла, а исходя из требования изоляции транзакций, её временные изменения не могут быть замечены другими транзакциями. Многие NoSQL БД отказываются от гарантии изоляции и предлагают согласованность в конечном счёте (eventual consistency), согласно которой вы в конце концов увидите действительные данные, но есть вероятность, что ваша транзакция прочитает недействительные значения то есть, временные, или частично обновлённые, или устаревшие. Возможно, данные станут согласованными в ленивом режиме при чтении ("lazily at read time").

Strong consistency? Нет, показалось - eventual... Фотограф: Jacques-Henri LartigueStrong consistency? Нет, показалось - eventual... Фотограф: Jacques-Henri Lartigue

NoSQL были задуманы как БД для аналитики в режиме реального времени, и чтобы достигнуть бОльшую скорость, они пожертвовали согласованностью. А Eric Brewer, тот же парень, что придумал термин BASE, сформулировал так называемую "CAP-теорему", согласно которой:

Для любой реализации распределённых вычислений возможно обеспечить не более двух из трёх следующих свойств:

  • согласованность данных (consistency) - данные на разных узлах (instances) не противоречат друг другу;

  • доступность (availability) любой запрос к распределённой системе завершается корректным откликом, однако без гарантии, что ответы всех узлов системы совпадают;

  • устойчивость к разделению (распределению) (partition tolerance) Даже если между узлами нет связи, они продолжают работать независимо друг от друга.

Если вам нужно совсем простое объяснение CAP, то держите.

Есть мнения о том, что теорема CAP не работает, и вообще сформулирована слишком абстрактно. Так или иначе, базы NoSQL зачастую отказываются от согласованности в контексте теоремы CAP, что описывает следующую ситуацию: данные были обновлены в кластере с несколькими instances, но изменения были синхронизированны ещё не на всех instances. Помните, я выше упоминал пример с DynamoDB, которая сказала мне: твои изменения стали durable вот тебе HTTP 200 - но изменения я увидел лишь через 10 секунд? Ещё один пример из повседневной жизни разработчика DNS, система доменных имён. Если кто не знает, то это именно тот словарь, который переводит http(s)-адреса в IP-адреса. Обновлённая DNS-запись распространяется по серверам в соответствии с настройками интервалов кэширования поэтому обновления становятся заметными не моментально. Так вот, подобная временная несогласованность (т.е. согласованность в конечном счёте) может приключиться и с кластером реляционной БД (скажем, MySQL) ведь эта согласованность не имеет ничего общего с согласованностью из ACID. Поэтому важно понимать, что в этом смысле БД SQL и NoSQL вряд ли будут сильно отличаться, если речь идёт о нескольких instances в кластере.

Помимо этого, согласованность в конечном счёте может означать, что запросы на запись будут осуществлены не в порядке поступления: то есть, все данные будут записаны, но значение, которое будет принято в конечном счёте, будет не тем, что поступило последним в очередь на запись.

Не предоставляющие гарантии ACID базы данных NoSQL имеют так называемое мягкое состояние (soft state) вследствие модели согласованности в конечном счёте, что означает следующее: состояние системы может меняться со временем, даже без вводных данных (input). Зато такие системы стремятся обеспечить бОльшую доступность. Обеспечить стопроцентную доступность нетривиальная задача, поэтому речь идёт о базовой доступности. А вместе эти три понятия: базовая доступность (basically available), мягкое состояние (soft state) и согласованность в конечном счёте (eventual consistency) формируют аббревиатуру BASE.

Может, это strong consistency? Нет, снова не то... Фотограф: Robert DoisneauМожет, это strong consistency? Нет, снова не то... Фотограф: Robert Doisneau

Если честно, мне понятие BASE кажется более пустой маркетинговой обёрткой, чем ACID потому что оно не даёт ничего нового и никак не характеризует БД. А навешивание ярлыков (ACID, BASE, CAP) на те или иные БД может лишь запутать разработчиков. Я решил вас всё-таки познакомить с этим термином, потому что миновать его при изучении БД трудно, но теперь, когда вы знаете, что это, я хочу, чтобы вы поскорее про него забыли. И давайте снова вернёмся к понятию изоляции.

Получается, базы данных BASE совсем не выполняют критерии ACID?

По сути, чем отличаются БД ACID от не-ACID, так это тем, что не-ACID фактически отказываются от обеспечения изоляции. Это важно понимать. Но ещё важнее читать документацию БД и тестировать их так, как это делают ребята из проекта Hermitage. Не столь важно, как именно называют своё детище создатели той или иной БД ACID или BASE, CAP или не CAP. Важно то, что именно предоставляет та или иная БД.

Если создатели БД утверждают, что она предоставляет гарантии ACID, то, наверное, у этого есть основания, но желательно самому протестировать, чтобы понять, так ли это и в какой степени. Если же они заявляют, что их БД такие гарантии не предоставляет, то это может значить следующие вещи:

  • БД не предоставляет гарантии атомарности. Хотя некоторые NoSQL базы данных предлагают отдельную API для атомарных операций (например, DynamoDB);

  • БД не предоставляет гарантии изоляции. Это может означать, например, что БД запишет данные не в том порядке, в котором они поступили на запись.

Что касается гарантии durability, то и по этому пункту многие БД идут на копромисс в угоду производительности. Запись на диск является слишком долгой операцией, и есть несколько способов решения этой проблемы. Я не хочу сильно вдаваться в теорию баз данных, но чтобы вы примерно понимали, в какую сторону глядеть, опишу в общих чертах, как разные БД решают проблему с durability.

Чтобы сравнивать разные БД, помимо всего прочего, нужно знать, какие структуры данных лежат в основе подсистемы хранения и извлечения данных конкретной БД. Вкратце: разные БД имеют разные реализации индексации то есть, организации доступа к данным. Некоторые из них позволяют быстрее писать данные, другие быстрее их читать. Но нельзя общо сказать, что какие-то структуры данных делают durability выше или ниже. Длинная версия:

Для особо интересующихся: как разные БД индексируют данные, и как это влияет на durability, и не только

Есть два основных подхода к хранению и поиску данных.

Самый простой способ сохранять данные это добавление операций в конец файла по принципу журнала (то есть, всегда происходит операция append): неважно, хотим ли мы добавить, изменить или удалить данные все операции CRUD просто записываются в журнал. Искать по журналу занятие неэффективное, и вот где на помощь приходит индекс особая структура данных, которая хранит метаданные о том, где именно хранятся данные. Простейшая стратегия индексация для журналов хэш-таблица (hash map), которая отслеживает ключи и значения. Значениями будут ссылки на байтовое смещение для данных, записанных внутрь файла, которая и представляет из себя журнал (log) и хранится на диске. Эта структура данных целиком хранится в памяти, в то время, как сами данные на диске, и называется LSM-деревом (log structured merge). Вы, наверное, задались вопросом: если мы всё время пишем наши операции в журнал, то он же будет непомерно расти? Да, и поэтому была придумана техника уплотнения (compaction), которая с некоей периодичностью подчищает данные, а именно оставляет для каждого ключа лишь наиболее актуальное значение, либо удаляет его. А если иметь не один журнал на диске, а несколько, и они все будут отсортированы, то мы получим новую структуру данных под названием SSTable (sorted string table), и это, несомненно, улучшит нашу проивзодительность. Если же мы захотим сортировать в памяти, то получим похожую структуру так называемую таблицу MemTable, но с ней проблема в том, что если происходит фатальный сбой БД, то записанные позже всего данные (находящиеся в MemTable, но еще не записанные на диск) теряются. Собственно, в этом заключается потенциальная проблема с durability у БД, базирующихся на LSM-деревьях.

Другой подход к индексации основывается на B-деревьях (B-trees). В B-дереве данные записываются на диск страницами фиксированного размера. Эти блоки данных часто имеют размер около 4 КБ и имеют пары ключ-значение, отсортированные по ключу. Один узел B-дерева похож на массив со ссылками на диапазон страниц. Макс. количество ссылок в массиве называется фактором ветвления. Каждый диапазон страниц - это еще один узел B-дерева со ссылками на другие диапазоны страниц. В конце концов, на уровне листа вы найдете отдельные страницы. Эта идея похожа на указатели в языках программирования низкого уровня, за исключением того, что эти ссылки на страницы хранятся на диске, а не в памяти. Когда в БД происходят INSERTs и DELETEs, то какой-нибудь узел может разбиться на два поддерева, чтобы соответствовать коэффициенту ветвления. Если база данных выйдет из строя по какой-либо причине в середине процесса, то целостность данных может нарушиться. Чтобы предотвратить такой случай, использующие B-деревья БД ведут журнал упреждающей записи (write-ahead log, или WAL), в котором записывается каждая отдельная транзакция. Этот WAL используется для восстановления состояния B-дерева в случае его повреждения. И кажется, что именно это делает использующие B-деревья БД лучше в плане durability. Но основанных на LSM БД также могут вести файл, по сути выполняющий такую же функцию, как WAL. Поэтому я повторю то, что уже говорил, и, возможно, не раз: разбирайтесь в механизмах работы выбранной вами БД.

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

Вместе с тем, дизайн индекса напрямую отражается на производительности БД. При LSM-дереве запись на диск осуществляется последовательно, а B-деревья вызывают множественные случайные доступы к диску, поэтому операции записи происходят у LSM быстрее, чем у B-деревьев. Разница особенно существенна для магнитных жёстких дисков (HDD), на которых последовательные операции записи работают намного быстрее, чем произвольные. Чтение же выполняется медленнее на LSM-деревьях потому, что приходится просматривать несколько различных структур данных и SS-таблиц, находящихся на разных стадиях уплотнения. Более детально это выглядит следующим образом. Если мы сделаем простой запрос к базе данных с LSM, мы сначала поищем ключ в MemTable. Если его там нет, мы смотрим в самую последнюю SSTable; если нет и там, то мы смотрим в предпоследнюю SSTable и т.д. Если запрашиваемый ключ не существует, то при LSM мы это узнаем в последнюю очередь. LSM-деревья используются, например, в: LevelDB, RocksDB, Cassandra и HBase.

Я так подробно это всё описываю, чтобы вы поняли, что при выборе БД нужно учитывать много разных вещей: например, рассчитываете ли вы больше писать или читать данные. И это я ещё не упомянул различие в моделях данных (нужно ли вам делать обход данных, как позволяет графовая модель? Есть ли в ваших данных вообще какие-то отношения между различными единицами тогда вам на выручку придут реляционные БД?), и 2 вида схемы данных при записи (как во многих NoSQL) и чтении (как в реляционных).

Если же мы вернёмся к аспекту durability, то вывод будет звучать следующим образом: любая БД, записывающая на диск, вне зависимости от механизмов индексации может предоставить хорошии гарантии стойкости ваших данных, но нужно разбираться с каждой конкретной БД, что именно она предлагает.

Между прочим, помимо БД, записывающих на диск, ещё есть так называемые "in-memory" БД, которые работают преимущественно с RAM. Вкратце: располагаемые в памяти БД обычно предлагают более низкую durability ради большей скорости записи и чтения, но это может подходить для некоторых приложений. Длинная версия:

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

Дело в том, что память RAM долгое время была дороже, чем диски, но в последнее время она начала стремительно дешеветь, что и породило новый вид БД что логично, учитывая быстроту чтения и записи данных из RAM. Но вы справедливо спросите: а что с сохранностью данных у этих БД? Тут опять-таки нужно смотреть на детали реализации. В целом, разработчики таких БД предлагают следующие механизмы:\

  • Можно использовать RAM, питающейся от аккумуляторов;

  • Можно записывать на диск журналы изменений (что-то вроде упомянутых выше WAL), но не сами данные;

  • Можно периодически записывать на диск копии состояния БД (что без использования других опций не даёт гарантии, а лишь улучшает durability);

  • Можно проводить репликацию состояния оперативной памяти на другие машины.

Например, in-memory БД Redis, которая в основном используется как очередь сообщений или кэш, недостаёт именно durability из ACID: она не гарантирует, что успешно выполненная команда сохранится на диске, поскольку Redis сбрасывает данные на диск (если у вас включена сохраняемость) только асинхронно, через определённые интервалы. Впрочем, не для всех приложений это критично: я нашёл пример кооперативного онлайн-редактора EtherPad, который делал flush раз в 1-2 секунды, и потенциально пользователь мог потерять пару букв или слово, что вряд ли было критичным. В остальном же, поскольку располагаемые в памяти БД хороши тем, что они предоставляют модели данных, которые было бы тяжело реализовать с помощью дисковых индексов, Redis можно использоваться для реализации транзакций её очередь по приоритету позволяет это сделать.

Как реализовать ACID в приложении? И зачем это надо

Вы могли подумать, что эта глава - для любителей переизобретать велосипеды. Но всё не так однозначно... Фотограф: Josef KoudelkaВы могли подумать, что эта глава - для любителей переизобретать велосипеды. Но всё не так однозначно... Фотограф: Josef Koudelka

Мы с вами довольно подробно проговорили все свойства ACID, их предназначение и сценарии использования. Как вы уже поняли, не все БД предлагают гарантии ACID, жертвуя ими ради более высокой производительности. Поэтому вполне может случиться, что на вашем проекте будет выбрана БД, не предлагающая ACID, и вам может понадобиться воплотить часть необходимого функционала ACID на стороне приложения. А если ваша система будет спроектирована как микросервисы, или какой-то другой вид распределённых приложений, то то, что в одном сервисе было бы обычной локальной транзакцией, теперь станет распределённой транзакцией и, конечно, потеряет свою ACID-природу, даже если БД каждого отдельного микросервиса будет ACID.

Я не хочу давать вам исчерпывающее руководство по тому, как создать менеджера транзакций просто потому, что это слишком большая и сложная тема, а я хочу описать лишь несколько основных техник. Если же речь не идёт о распределённых приложениях, то я не вижу смысла пытаться полностью воплотить ACID на стороне приложения, если вам нужны гарантии ACID ведь проще и дешевле во всех смыслах будет взять уже готовое решение (то есть, БД с ACID).

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

Базовый инструментарий для любителей транзакций

Оптимистичная и пессимистичная блокировка. Это два типа блокировки неких данных, к которым может возникнуть одновременный доступ.

Оптимист полагает, что вероятность одновременного доступа не так велика, а потому он делает следующее: читает нужную строку, запоминает номер её версии (или timestamp, или checksum / hash если вы не можете изменить схему данных и добавить столбец для версии или timestamp), и перед тем, как записать в БД изменения для этих данных, проверяет, не изменилась ли версия этих данных. Если версия изменилась, то нужно как-то решить создавшийся конфликт и обновить данные (commit), либо откатить транзакцию (rollback). Минус этого метода в том, что он создаёт благоприятные условия для бага с длинным названием time-of-check to time-of-use, сокращённо TOCTOU: состояние в период времени между проверкой и записью может измениться. Я не имею опыта использования оптимистичной блокировки, а Википедия в качестве решения предлагает использовать exception handling вместо проверки, что мне лично в контексте баз данных мало о чём говорит, если честно.

В качестве примера я нашёл одну технологию из повседневной жизни разработчика, которая использует нечто вроде оптимистичной блокировки это протокол HTTP. Ответ на изначальный HTTP-запрос GET может включать в себя заголовок ETag для последующих запросов PUT со стороны клиента, который тот может использовать в заголовке If-Match. Для методов GET и HEAD сервер отправит обратно запрошенный ресурс, только если он соответствует одному из знакомых ему ETag. Для PUT и других небезопасных методов он будет загружать ресурс также только в этом случае. Если вы не знаете, как работает ETag, то вот хороший пример, с использованием библиотеки "feedparser" (которая помогает парсить RSS и прочие feeds). Источник.

>>> import feedparser>>> d = feedparser.parse('http://feedparser.org/docs/examples/atom10.xml')>>> d.etag'"6c132-941-ad7e3080"'>>> d2 = feedparser.parse('http://feedparser.org/docs/examples/atom10.xml', etag=d.etag)>>> d2.feed{}>>> d2.debug_message'The feed has not changed since you last checked, so the server sent no data.  This is a feature, not a bug!'

Пессимист же исходит из того, что транзакции часто будут встречаться на одних и тех же данных, и чтобы упростить себе жизнь и избежать лишних race conditions, он просто блокирует необходимые ему данные. Для того, чтобы воплотить механизм блокировки, вам нужно либо поддерживать соединение с БД для вашей сессии (а не брать соединения из пула в этом случае вам, скорее всего, придётся работать с оптимистичной блокировкой), либо использовать ID для транзакции, которая может быть использована независимо от соединения. Минус пессимистичной блокировки в том, что её использование замедляет обработку транзакций в целом, но зато вы можете быть спокойны за данные и получаете настоящую изоляцию. Дополнительная опасность, правда, таится в возможной взаимной блокировке (deadlock), при которой несколько процессов ожидают ресурсы, заблокированные друг другом. Например, для проведения транзакции нужные ресурсы А и Б. Процесс 1 занял ресурс А, а процесс 2 ресурс Б. Ни один из двух процессов не может продолжить выполнение. Существуют различные способы решения этого вопроса я не хочу сейчас вдаваться в детали, поэтому для начала почитайте Википедию , но если вкратце, то есть возможность создания иерархии блокировок. Если вы хотите познакомиться подробнее с этой концепцией, то предлагают вам поломать голову над Задачей об обедающих философах (dining philosophers problem).

Вот тут есть хороший пример того, как поведут себя обе блокировки в одном и том же сценарии.

Касательно реализаций locks. Не хочу вдаваться в подробности, но для распределённых систем существуют менеджеры блокировок, например: ZooKeeper, Redis, etcd, Consul.

Идемпотентность операций

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

Проявлений у идемпотентности может быть несколько. Одно из них это просто рекомендация к тому, как надо писать свой код. Вы же помните, что лучшая функция это та, которая делает одну вещь? И что хорошо бы написать для этой функции unit-тесты? Если вы придерживаетесь этих двух правил, то вы уже повышаете шанс на то, что ваши функции будут идемпотентны. Чтобы не возникло путаницы, уточню, что идемпотентные функции не обязательные чистые (в смысле function purity). Чистые функции это те функции, которые оперируют только теми данными, которые получили на входе, никак их не меняя и возвращая обработанный результат. Это те функции, которые позволяют скалировать приложение, используя техники функционального программирования. Поскольку мы говорим про некие общие данные и БД, то наши функции вряд ли будут чистыми, ибо они будут менять состояние БД или программ (сервисов).

Вот это - чистая функция:

def square(num: int) -> int:    return num * num

А вот эта функция - не чистая, но идемпотентная (прошу не делать выводов о том, как я пишу код, по этим кускам):

def insert_data(insert_query: str, db_connection: DbConnectionType) -> int:  db_connection.execute(insert_query)  return True

Вместо множества слов, я могу просто рассказать о том, как я вынужденно научился писать идемпотентные программы. Я много работаю с AWS, как вы уже могли понять, и там есть сервис под названием AWS Lambda. Lambda позволяет не заботиться о серверах, а просто загружать код, который будет запускаться в ответ на какие-то события или по расписанию. Событием могут быть сообщения, которые доставляются брокером (message broker). В AWS таким брокером является AWS SNS. Думаю, что это должно быть понятно даже для тех, кто не работает с AWS: у нас есть брокер, который отправляет сообщения по каналам (topics), и микросервисы, которые подписаны на эти каналы, получают сообщения и как-то на них реагируют.

Проблема заключаются в том, что SNS доставляет сообщения как минимум один раз (at-least-once delivery). Что это значит? Что рано или поздно ваш код на Lambda будет вызван дважды. И это действительно случается. Существует целый ряд сценариев, когда ваша функция должна быть идемпотентной: например когда со счёта снимаются деньги, мы можем ожидать, что кто-то снимет одну и ту же сумму дважды, но мы должны убедиться, что это действительно 2 независимых друг от друга раза иначе говоря, это 2 разные транзакции, а не повтор одной.

Я же для разнообразия приведу другой пример ограничение частоты запросов к API (rate limiting). Наша Lambda принимает событие с неким user_id для которого должна быть сделана проверка, не исчерпал ли пользователь с таким ID своё кол-во возможных запросов к некой нашей API. Мы могли бы хранить в DynamoDB от AWS значение совершённых вызовов, и увеличивать его с каждым вызовов нашей функции на 1.

Но что делать, если эта Lambda-функция будет вызвана одним и тем же событием дважды? Кстати, вы обратили внимание на аргументы функции lambda_handler() Второй аргумент, context в AWS Lambda даётся по умолчанию, и он содержит разные метаданные, в том числе request_id , который генерируется для каждого уникального вызова. Это значит, что теперь, вместо того, чтобы хранить в таблице число совершённых вызовов, мы можем хранить список request_id и при каждом вызове наша Lambda будет проверять, был ли данный запрос уже обработан:

import jsonimport osfrom typing import Any, Dictfrom aws_lambda_powertools.utilities.typing import LambdaContext  # нужно только для аннотации типа аргументаimport boto3limit = os.getenv('LIMIT')def handler_name(event: Dict[str: Any], context: LambdaContext):    request_id = context.aws_request_id    # Находим user_id во входящем событии    user_id = event["user_id"]    # Наша таблица на DynamoDB    table = boto3.resource('dynamodb').Table('my_table')    # Делаем update    table.update_item(        Key={'pkey': user_id},        UpdateExpression='ADD requests :request_id',        ConditionExpression='attribute_not_exists (requests) OR (size(requests) < :limit AND NOT contains(requests, :request_id))',        ExpressionAttributeValues={            ':request_id': {'S': request_id},            ':requests': {'SS': [request_id]},            ':limit': {'N': limit}        }    )    # TODO: написать дальнейшую логику    return {        "statusCode": 200,        "headers": {            "Content-Type": "application/json"        },        "body": json.dumps({            "status ": "success"        })    }

Поскольку мой пример фактически взят из интернета, то я оставлю ссылку на первоисточник, тем более, что он даёт чуть больше информации-

Помните, выше я уже упоминал, что что-то наподобие уникального ID транзакции можно использовать для блокировки общих данных? Теперь мы узнали, что его можно использовать и для обеспечения идемпотентности операций. Давайте же узнаем, какими способами можно самим генерировать такие ID.

ID транзакций

Обозначается как XID или TxID (если есть разница подскажите). В качестве TxID можно использовать timestamps, что может сыграть на руку, если мы захотим восстановить все действия к какому-то моменту времени. Проблема может возникнуть, если timestamp недостаточно гранулярный тогда транзакции могут получить один и тот же ID.

Поэтому наиболее надёжный вариант это генерировать уникальные ID проде UUID. В Python это делается очень просто:

>>> import uuid>>> str(uuid.uuid4())'f50ec0b7-f960-400d-91f0-c42a6d44e3d0'>>> str(uuid.uuid4())'d15bed89-c0a5-4a72-98d9-5507ea7bc0ba'

Также есть вариант хэшировать набор определяющих транзакцию данных и использовать этот хэш в качестве TxID

Повторные попытки ("retries")

Если мы знаем, что некая функция или программа идемпотентна, то это значит, что мы можем и должны пробовать повторить её вызов в случае ошибки. А мы просто обязаны быть готовы к тому, что какая-то операция выдаст ошибку учитывая, что современные приложения распределены по сети и железу, ошибка должна рассматриваться не как исключение, а как норма. Ошибка может произойти из-за падения сервера, ошибки сети, перегруженности удалённого приложения. Как себя должно вести наше приложение? Правильно, попробовать повторить операцию.

Поскольку один кусочек кода может сказать больше, чем целая страница слов, то давайте на одном примере разберём, как в идеале должен работать механизм повторения операции в духе naive retrying. Я продемонстрирую это с использованием библиотеки Tenacity (у неё настолько продуманный дизайн, что даже если вы не планируете использовать её, пример должен наглядно показать, как можно спроектировать механизм повторения):

import loggingimport randomimport sysfrom tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential, retry_if_exception_type, before_loglogging.basicConfig(stream=sys.stderr, level=logging.DEBUG)logger = logging.getLogger(__name__)@retry(    stop=(stop_after_delay(10) | stop_after_attempt(5)),    wait=wait_exponential(multiplier=1, min=4, max=10),    retry=retry_if_exception_type(IOError),    before=before_log(logger, logging.DEBUG))def do_something_unreliable():    if random.randint(0, 10) > 1:        raise IOError("Broken sauce, everything is hosed!!!111one")    else:        return "Awesome sauce!"print(do_something_unreliable.retry.statistics)

На всякий случай скажу: \@retry(...) - это такой специальный синтаксис Python, именуемый "декоратором". Это просто функция retry(...) , которая оборачивает другую функцию и выполняет некие действия до или после её исполнения.

Как мы видим, повторные попытки можно оформить креативно:

  • Можно ограничить попытки по времени (10 секунд) или количеству попыток (5);

  • Можно экспоненциально (то есть, 2 ** некоторое увеличивающееся число n ). или как-то ещё (например, фиксированно) увеличивать время между отдельными попытками. Экспоненциальный вариант носит название "congestion collapse";

  • Можно делать повторные попытки лишь для некоторых видов ошибок (IOError);

  • Повторные попытки можно предварять или завершать какими-то специальными записями в лог.

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

Продвинутый инструментарий для любителей транзакций

Я лишь дам довольно общие определения, поскольку эта тема достойна отдельной большой статьи. Кстати, я сразу и оставлю тут две ссылки: вот и вот.

Two-phase commit (2pc). 2pc имеет две фазы: фазу подготовки и фазу фиксации. На этапе подготовки всем микросервисам будет предложено подготовиться к некоторым изменениям данных, которые могут быть выполнены атомарно. Как только они все будут готовы, то на этапе фиксации будут внесены фактические изменения. Для координации процесса необходим глобальный координатор, который блокирует необходимые объекты то есть, они становятся недоступны для изменений, пока координатор их не разблокирует. Если какой-то отдельный микросервис не готов к изменениям (например, не отвечает), координатор прервёт транзакцию и начнёт процесс отката.

Чем хорош этот протокол? Он обеспечивает атомарность. К тому же, он гарантирует изоляцию при записи и чтении. Это означает, что изменения одной транзакции не видны остальным, пока координатор не зафиксирует изменения. Но в этих свойствах кроется и минус: поскольку этот протокол синхронен (блокирующий), он замедляет работу систему (при том, что вызов RPC, сам по себе довольно медленный). И опять-таки, возникает опасность взаимной блокировки.

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

Плюсы Saga в том, что никакие объекты не блокируются. Но есть, конечно, и минусы. Saga сложно отлаживать, особенно когда задействовано много микросервисов. Еще один недостаток шаблона Saga - в нем отсутствует изоляция чтения. То есть, если нам важны свойства, обозначенные в ACID, то Saga нам не очень подходит.

Что мы видим из описания этих двух техник? То, что в распределённых системах ответственность за атомарность и изоляцию ложится на приложение. То же самое происходит и при использовании БД, которые не предоставляют гарантии ACID. То есть, такие вещи, как разрешение конфликтов, откаты, коммиты и высвобождение места ложатся на плечи разработчика.

Как понять, когда мне нужны гарантии ACID?

Я очень много растекался мыслью по древу на протяжении всей статьи, и именно поэтому главную главу я хочу сделать максимально лаконичной. Если совсем кратко, то очень малое количество приложений нуждается в ACID везде. Как правило, они если и нуждаются в нём, то лишь в некоторых местах.

В каких случаях мне нужны ACID?

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

Простите за банальность, но типичный пример - финансовые транзакции.

Когда порядок выполнения транзакций имеет значение.

Представьте себе, что ваша компания собралась переходить с мессенджера FunnyYellowChat в мессенджер FunnyRedChat, потому что в FunnyRedChat можно отсылать гифки, а в FunnyYellowChat - нельзя. Но вы не просто меняете мессенджер - вы мигрируете переписку вашей компании из одного мессенджера в другой. Вы делаете это, потому что ваши программисты ленились документировать программы и процессы где-то централизованно, и вместо этого всё публиковали в разных каналах в мессенджере. Да и ваши продажники детали переговоров и соглашений публиковали там же. Короче, вся жизнь вашей компании - там, и поскольку ни у кого нет времени переносить всё это дело в сервис для документации, а поиск у мессенджеров работает неплохо, вы решили вместо разгребания завалов просто скопировать все сообщения в новое место. Очерёдность сообщений важна, потому что иначе всё может перепутаться, и вы, например, не будете понимать, где именно находится ответ на тот или иной вопрос.

Кстати, для переписки в мессенджере вообще важна очерёдность, но когда два человека одновременно пишут что-то в одном чате, то в целом не так важно, чьё сообщение покажется первым. Так что, именно для этого сценария ACID был бы не нужен.

Другой возможный пример - это биоинформатика. Я в этом совсем не разбираюсь, но предполагаю, что при расшифровке генома человека порядок важен. Впрочем, я слышал, что биоинформатики вообще какие-то свои инструменты для всего используют - возможно, у них и БД свои.

Когда нельзя выдать пользователю или процессу устаревшие данные.

И снова - финансовые транзакции. Честно говоря, не придумал иного примера.

Когда незавершенные транзакции связаны со значительными издержками.

Представьте себе проблемы, которые могут возникнуть, когда врач и медсестра одновременно обновляют карту пациента и стирают изменения друг друга, потому что БД не может изолировать транзакции. Система здравоохранения - это ещё одна сфера, помимо финансовой, для которой гарантии ACID, как правило, критически важны.

В каких случаях мне не нужны ACID?

Когда пользователи обновляют лишь некие свои приватные данные.

Например, пользователь оставляет комментарии или sticky notes к веб-странице. Или редактирует личные данные в личном кабинете у провайдера каких-либо услуг.

Когда пользователи вообще не обновляют данные, а только дополняют новыми (append).

Например, приложение для бега, которое сохраняет данные по вашим пробежкам: сколько пробежали, за какое время, маршрут и т.д. Каждая новая пробежка - новые данные, а старые вообще не редактируются. Возможно, на основании данных вы получаете аналитику - и как раз БД NoSQL хороши для этого сценария.

Когда бизнес-логика не определяет необходимость некоего порядка выполнения транзакций.

Наверное, для блогера на Youtube, который во время очередного прямого эфира собирает пожертвования для производства нового материала, не так важно, кто когда именно и в какой очерёдности кинул ему денежку.

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

Теоретически, это любые новостные онлайн-медиа, или тот же Youtube. Или "Хабр".

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

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

Эпилог

Я надеюсь, что вам было интересно.

Если вы нашли какие-то фактические ошибки обязательно сообщите об этом в комментариях.

Также я, как мне кажется, привёл довольно мало конкретных примеров реализации тех или иных вещей в тех или иных БД главным образом, из-за того, что я не хотел погрязнуть в деталях. Если вы знаете какие-то хорошие примеры, упомяните их в комментариях пожалуйста, со ссылкой на документацию или исследование.

Кроме того, если у вас есть предложения по тому, как расширить списки "Когда мне нужно ACID" и "Когда мне не нужно ACID", буду рад услышать их.

Подробнее..

Паспортный контроль, или Как сжать полтора гигабайта до 42 мегабайт

05.02.2021 14:16:53 | Автор: admin

Однажды, в качестве тестового задания на позицию PHP разработчика была предложена задача реализации сервиса проверки номеров паспортов граждан РФ на предмет нахождения в списке недействительных. Текст задания был лаконичным: Пользовательская база 10 миллионов, время ответа 1 миллисекунда, аптайм 99%.

Входные данные

Для начала посмотрим, в каком виде представлены записи в списке недействительных паспортов. На сайте МВД РФ можно скачать bzip2-архив размером около 460 МБ, внутри которого CSV-файл с двумя колонками PASSP_SERIES,PASSP_NUMBER. Размер распакованного файла примерно 1.5 ГБ. Всего в списке около 130 миллионов записей. Стоит отметить, что не все записи в файле имеют правильный формат номер серии из 4 цифр и номер паспорта из 9 цифр. Встречаются буквенные серии, номера из 5 и меньше цифр, либо номера с символами 3,+,] артефакты распознавания. Итого около 10 тыс. записей имеют неправильный формат. Их можно игнорировать при условии проверки входных данных будущего сервиса не пытаться искать в списке заведомо неправильные номера.

Способ хранения

Требуемое время ответа сервиса в 1 мс накладывает ограничение на возможные способы хранения и поиска по списку. Данные нужно проиндексировать, а индекс хранить в оперативной памяти.

Первое очевидное решение создать таблицу в SQL базе данных с индексом по двум колонкам. В качестве индекса под условия задачи больше подойдет Hash Table со средней сложностью поиска O(1) против O(log n) для B-Tree индекса. Но у такого подхода есть существенный минус избыточность хранимых данных. Например, MEMORY таблица в MySQL занимает 5 ГБ (2 ГБ данные и 3 ГБ индекс).

Для решения исходной задачи необходим только факт наличия или отсутствия записи в списке и не обязательно хранить саму запись. Закодируем серию и номер бинарными значениями: 1 паспорт присутствует в списке, 0 отсутствует. Для всего множества возможных серий и номеров потребуется 9999 х 999999 ~ 10^10 бит ~ 1.25ГБ. Это сопоставимо с размером исходного файла, но уже с поиском за O(1). Но всё множество хранить не обязательно. Заметим, что в исходном списке около 3 тысяч уникальных серий, их можно сделать ключом для секционирования записей хранить номера паспортов с одинаковой серией в одном битовом массиве. Номер паспорта будет соответствовать отступу в массиве. Длина массива будет зависеть от максимального номера в серии. Другими словами, если в серии 3382 встречается только один паспорт с номером 000032, то для всей серии потребуется 4 байта. Однако, если в этой же серии будет ещё паспорт с номером 524288, то размер вырастет до 64 килобайт, при этом почти весь массив будет состоять из нулей. Проанализировав распределение максимальных номеров в сериях, можно приблизительно рассчитать требуемый объем памяти. 3389 серий, среднее максимальное значение номера паспорта 567750. Что даст около 229 мегабайт (в Redis полный список занял 236 МБ). Таким образом мы получим возможность за константное время проверить, присутствует ли конкретный паспорт в списке недействительных, используя объем памяти, в два раза меньший исходного bzip2-архива.

Еще большей экономии можно добиться, воспользовавшись методом сжатия разреженных битовых массивов, например, библиотекой Roaring Bitmaps. Рассчитать занимаемый объем в таком случае уже сложнее, поэтому воспользуемся эмпирическими данными, загрузив список в сервер Pilosa. В итоге получим 42 мегабайта.

Реализация

Соблюдая баланс между эффективным и простым, выберем в качестве хранилища Redis. Используем команды SETBIT/GETBIT для работы с бинарными строками, в качестве имени ключа возьмем серию паспорта, номер паспорта отступ в строке. Чтобы упростить процесс обновления, новый список будем загружать во временную логическую базу Redis-а, а после окончания поменяем местами с активной (команда SWAPDB).

Архив со списком на сервере МВД РФ обновляется раз в сутки. С помощью HTTP запроса HEAD и заголовка ответа Last-Modified можно узнать время последнего обновления и не загружать большой файл без необходимости. Сам файл можно распаковывать и загружать в Redis в потоковом режиме, не сохраняя на диск и используя фильтр потока 'bzip2.decompress'.

Проверку паспортов на вхождение в список недействительных будем осуществлять в пакетном режиме, принимая до 500 номеров в одном запросе. Это позволит проверить всю базу 10 миллионов пользователей при 8 параллельных потоках меньше чем за 5 секунд.

Развёртывание

Осталось выполнить последнее требование задания аптайм 99%. Это означает, что сервис может быть недоступен 3,5 дня в течение года, либо по 14 минут каждый день. Такой доступности можно добиться, разместив сервис у провайдера с соответствующим SLA, добавив репликацию и балансировку.

Следуя современным методикам развёртывание приложений, упакуем сервис в контейнер Docker.

Исходный код

Сервис реализован на PHP 8.0 с использованием библиотек Guzzle, PHP-DI, Workerman.
Исходный код доступен в репозитории https://github.com/maurokouti/passport-control/.

Подробнее..

Что нам стоит дом построить? (часть 2)

21.06.2021 12:17:59 | Автор: admin

Привет, Хабр. В прошлой статье я рассказал о начальном анализе предметной области и базовом проектировании нашей новой ECM-системы. Теперь я расскажу о первой практической задаче, которую мы решили. А именно - о выборе способа организации структуры хранилища бизнес-атрибутов объектов.

Напомню, что в результате анализа мы пришли к следующей структуре объекта:

Системные и интеграционные поля содержат техническую информацию и редко подвержены модификациям. А вот бизнес-поля - искомые атрибуты объектов - могут и будут различаться для разных типов документов, которых у нас на входе предполагается около 1000. Проанализировав возможность повторного использования полей, мы пришли к выводу, что у разных типов документов может быть не более 15% общих свойств. И при этом всё равно придется скрепя сердце согласиться на неполное соответствие названия и содержания.

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

Какие есть варианты?

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

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

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

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

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

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

Также стоит упомянуть про еще один способ организации данных, с которым я сталкивался. Он используется не в пример реже и больше напоминает "велосипед" - назовем его вертикальный. В хранилище создается таблица, которая содержит, условно, название поля, данные в нем, тип данных и связь с объектом. Возможно создание нескольких таких таблиц для разных типов данных или несколько колонок в одной таблице. Далее, при извлечении такие данные трансформируются из списка в плоский набор полей. Понимаете, почему я упомянул про велосипед?

А что у нас?

Второй вариант - это использование специализированных документоориентированных (или документных, как больше нравится) баз данных, реализующих NoSQL-подход к хранению и обработкенеструктурированной или слабоструктурированной информации. Наиболее часто данные хранятся в виде JSON объектов, но с предоставлением производителями СУБД инструментария для доступа к данным внутри этих структур.

У такого подхода, применительно к проектируемой системе, можно выделить несколько плюсов:

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

  • ограничиваем ситуации случайного воздействия на данные - модифицировать json объект гораздо сложнее, чем данные в колонке.

  • проще описывать объекты в коде - иногда можно вообще не описывать структуру документа в коде, а работать прямо с полями в JSON.

Но есть и минусы:

  • невозможно нативно реализовать проверки данных при размещении в хранилище.

  • валидацию данных придется проводить в коде.

  • невозможно организовать внутреннюю связность подчиненных данных, например, справочников.

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

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

Делаем прототип

Возьмем гипотезу, что NoSQL-подход для нашей системы применим как минимум не хуже, чем классический. Для ее проверки создадим прототип, в рамках которого реализуем оба подхода. В качестве СУБД возьмем Postgre, который уже давно умеет хорошо работать с JSON полями.

Создадим следующие таблицы:

Для описания объектов в табличном виде:

  • r_objects, базовые данные по объектам: тип, дата создания и ссылка на хранилище атрибутов.

  • r_attributes. атрибуты, во всех возможные колонки для всех объектов. В идеале конечно же хранилища атрибутов для разных объектов лучше разбить по разным таблицам, но для прототипа это не критично.

Для описания объектов в виде JSON:

  • objects. Данные по объектам, где в поле data формата jsonb хранятся искомые атрибуты.

Остальные таблицы - это различные вспомогательные хранилища.

Реализуем в коде 5 разных типов объектов, для каждого из них описав программную структуру, механизм извлечения данных из хранилища и наполнения этими данными самих объектов.

Методика тестирования

Для тестирования обоих подходов хранения данных используем следующие методы:

  • добавление данных по объекту. Критерий успешности: объект с данными появился в хранилище, метод вернул в ответе его идентификатор.

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

  • извлечение данных по объекту. Критерий успешности: объект с данными возвращен в ответе на запрос. Извлечение объекта происходит по конкретному идентификатору, по критериям поиска и постранично (пагинация).

Генерация запросов будет происходить в 20 параллельных потоков по 50 запросов в каждом потоке. Для того, чтобы тестирование было действительно показательным с точки зрения производительности, предварительно наполним базу 200 млн. объектов.

Тестирование показало следующие результаты:

График по тестированию табличного хранилищаГрафик по тестированию табличного хранилищаГрафик по тестированию NoSQL-хранилищаГрафик по тестированию NoSQL-хранилища

Первая (высокая) часть графика - это получение объектов по случайной странице - пагинация. Здесь в обоих случаях пришлось применить небольшой трюк - так как Postgres не агрегирует точное число строк в таблице, то узнать, сколько всего записей на объеме данных теста простым count - это долго, и для получения количества записей пришлось брать статистику данных по таблице. Также время получения данных на страницах свыше 10000-й неприлично велико, поэтому верхняя планка для получения случайного номера страницы была установлена в 10000. Учитывая специфику нашей системы, получение пагинированных данных не будет частой операцией, и поэтому такое извлечение данных применяется исключительно в целях тестирования.

Вторая (средняя) часть графика - вставка или обновление данных.

Третья (низкая) часть графика - получение данных по случайному идентификатору.

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

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

Результаты тестов на 40000 запросов приведу в виде таблицы:

Табличная

NoSQL

Объем хранилища

74

66

Среднее количество операций в секунду

970

1080

Время тестирования, секунды

42

37

Количество запросов

40000

40000

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

Что получилось?

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

Подробнее..

Фирма 1С приглашает вас принять участие в нашей первой конференции для системных разработчиков

15.01.2021 12:16:15 | Автор: admin
Всем добрый день!

Фирма 1С приглашает вас принять участие в нашей первой конференции для системных
разработчиков, которая пройдет 23 января в онлайн-формате.

Наверное, вы сейчас думаете да 1С это ж бухгалтерия, какая системная разработка?

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

Например, знаете ли Вы, что среди технологий 1С есть высоконагруженный кластер, с
продвинутой балансировкой нагрузки и обеспечением отказоустойчивости?

Или зачем нам вдруг понадобилось использовать NoSQL DB при разработке собственной IDE? (Да-да, у нас есть собственная IDE, да не одна, а целых три!)

Чем же мы на самом деле занимаемся
Если же говорить, чем же мы на самом деле занимаемся наш ключевой продукт это фреймворк для создания бизнес-приложений 1С: Предприятие. Написан он на C++, Java, и JS. Умеет работать на разных платформах (Windows, Linux, MacOS, Android, iOS), в вебе, поддерживает разные СУБД и многое другое. Да и не просто работать, а инкапсулировать особенности каждой СУБД и выглядеть одинаково на разных платформах (интерфейсы генерируются автоматически про это, кстати, будет отдельный доклад).

Платформа 1С: Предприятие, на самом деле, двойственна она объединяет в себе как инструменты разработки, так и среду исполнения в различных вариантах (локальном, клиент-серверном, кластерном, мобильном, облачном, распределенном). Да, кстати, про облака тоже будет без них сейчас никуда.

Почему мы вдруг решили, что нам есть, что рассказать миру? Потому что более 5 млн.
пользователей работают в приложениях, написанных на платформе 1С: Предприятие, ежедневно, и не просто разок запустить а целый рабочий день. И нам пришлось решить много сложных системных задач, чтобы добиться этого!

Еще немного спойлеров:

  • Расскажем, зачем нам понадобился GraalVM
  • Как заставить Xtext эффективно работать с миллионами строк программного кода?

Так что приходите, очень ждем!
Программа конференции и регистрация по ссылке.
Ну и на сладкое какая конференция без машинного обучения? Расскажем о нашем собственном механизме распознавания документов (и покажем детали)!
Подробнее..

Перевод Argo CD готов к труду и обороне в Kubernetes

26.02.2021 20:18:58 | Автор: admin

Привет, Хабр. В рамках курса Инфраструктурная платформа на основе Kubernetes подготовили для вас перевод полезного материала.

Также приглашаем на открытый вебинар Работа с NoSQL базами в k8s (на примере Apache Cassandra). На вебинаре участники вместе с экспертом рассмотрят плюсы и минусы запуска Apache Cassandra в k8s: насколько такой вариант установки готов к продакшену, и какие подводные камни имеются.


В этой статье мы рассмотрим несколько вопросов касательно Argo CD: что это такое, зачем его используют, как его развернуть (в Kubernetes), как его использовать для реализации непрерывного развертывания (continuous deployment), как настроить SSO с помощью GitHub и разрешений и т. д.

Что такое Argo CD и GitOps

Argo CD это декларативный GitOps-инструмент непрерывной доставки (continuous delivery) для Kubernetes.

Но что же такое GitOps?

Официальное определение гласит, что GitOps это способ реализации непрерывного развертывания (continuous deployment) облачных приложений. Он фокусируется на создании ориентированного на разработчиков опыта эксплуатации инфраструктуры с использованием инструментов, с которыми разработчики уже знакомы, включая Git и Continuous Deployment.

Официальное определение не вдается в подробности, не так ли? Возможно, вы не так часто слышали об этой концепции, но, скорее всего, вы уже использовали ее: вы определяете свои K8s ресурсы в YAML или с помощью Helm-диаграммы, вы используете Git в качестве единого источника истины (single source of truth), вы запускаете одни автоматизированные CI задачи для деплоя в продакшн, когда ваша ветвь master изменена, и вы запускаете другие задачи по пул реквесту для деплоя на стейджи.

Почему GitOps

Определения, конфигурации и среды приложений должны быть декларативными и подлежать контролю версий.

Развертывание (деплой) приложений и управление жизненным циклом должны быть:

  • автоматизированными

  • проверяемыми

  • простыми для понимания

Эти правила не требуют пояснений, поскольку они приносят очевидную пользу. Вам не хотелось бы тратить много времени на выяснение, какая версия развернута в какой среде и какой тег git ей соответствует; вам нужна возможность отката до определенной версии; вы хотели бы, чтобы определение приложений и деплоев было легко читаемым для людей; вы хотите, чтобы все было автоматизировано и не исключало возможность проверки.

В этом и заключается концепция GitOps и то, почему он хорош.

Почему Argo CD

Можно ли достичь вышеупомянутых преимуществ GitOps, используя любые другие инструменты CI/CD? Скорее всего, да. Например, вы можете использовать старый добрый Jenkins, определить разные задачи для разных ветвей, после чего задачи будут следить за Git-репозиториями или использовать хуки для реакции на события, а в конвейере вы можете выполнить несколько git clone и helm install. Нет ничего плохого в Jenkins, на что я указывал в моей предыдущей статье о CI: Введение в CI: сравнение 17 основных инструментов CI или Как выбрать лучшее CI в 2020 году.

Если вы не читали ее, я рекомендую вам сделать это, чтобы лучше понять, что мы ищем при выборе инструмента.

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

Все еще неубедительно

Хорошо, я понимаю, что у вас есть опасения по поводу внедрения новых инструментов, поэтому вот несколько фактов, которые могут вас убедить:

Это часть фонда Cloud Native Computing Foundation (CNCF).

Он активно поддерживается и постоянно улучшается. Посмотрите количество на коммитов:

Мне порой доставляет удовольствие покопаться в git-репозиториях в поисках разнообразной информации, и вот что я нашел здесь:

  • первый релиз v0.1.0 состоялся в марте 2018 года (относительно новый проект)

  • v1.0.0 зарелижен в мае 2019 (быстро развивается)

  • на момент написания статьи имеет версию v1.7.8 (ноябрь 2020, развивается очень быстро)

  • 4,3 тыс. звезд в репозитории Argo CD (еще больше на других репозиториях того же проекта)

  • 60 открытых PR и 500 открытых задач на сегодняшний день (очень даже неплохо, это означает, что сообщество активно занимается им и постоянно исправляет и придумывает запросы на новый функционал)

Он также упоминается в техническом радаре CNCF:

Он все еще находится на стадии ОЦЕНКА (ASSESS), что означает, что члены CNCF протестировали его, и он показался многообещающим. Он рекомендован к рассмотрению, когда вы сталкиваетесь с конкретной потребностью в подобной технологии в вашем проекте.

Радар является инициативой CNCF End User Community. Это группа из более чем 140 ведущих компаний и стартапов, которые регулярно встречаются для обсуждения проблем и передовых методов внедрения облачных технологий. Если вам лень разбираться, какой инструмент использовать, или если вы чувствуете себя неуверенно в отношении новых вещей, которые вы еще не пробовали, довольно безопасно выбрать один из вариантов, которые предлагает радар, потому что многие крупные компании уже протестировали его за вас, множество из имен которых вы уже слышали. Если это подходит им, есть большая вероятность, что и вам это понравится.

Развертывание

Окей, приступим к практике. У вас должен быть запущен кластер K8s, потому что мы собираемся делать это внутри k8s.

kubectl create namespace argocdkubectl apply -n argocd -f \https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml 

Один YAML, чтоб править всеми.

После развертывания у вас будет несколько запущенных подов и сервисов. Примеры:

Pods:argocd-application-controller-6b47c9bd78-kp6djargocd-dex-server-7b6d8776d8-knsxxargocd-redis-99fb49846-l466kargocd-repo-server-b664bd94b-bmtwrargocd-server-768879948c-sx875Services:argocd-dex-serverargocd-metricsargocd-redisargocd-repo-serverargocd-serverargocd-server-metrics

Комментарии по доступу к сервису и ingress:

Обратите внимание, что в развертывании по умолчанию сервис работает как тип Cluster IP, и ingress по умолчанию нет. Поэтому, если вы хотите получить доступ к сервису, вам нужно будет либо выполнить переадресацию портов, либо изменить тип сервиса на балансировщик нагрузки, либо создать ingress.

Если вы делаете это в производственном кластере, например в EKS в AWS, скорее всего, вы хотите использовать ingress и, вероятно, у вас уже есть ingress-контроллер. Вход для Argo CD здесь немного сложен, потому что на порту 443 он имеет как HTTPS (для веб-интерфейса консоли), так и GRPC для вызовов API командной строки. Если вы используете EKS, например, с ingress-контроллером Nginx, скорее всего, вы уже выполнили завершение TLS там, поэтому вам может потребоваться несколько ingress-объектов и хостов, один для протокола HTTP, другой для GRPC. Подробнее смотрите здесь.

Установка CLI

Для Mac это всего лишь одна команда:

brew install argocd

Инициализация

После установки изначальный пароль совпадает с именем пода сервера. Мы можем войти в систему:

argocd login  (переадресация портов, служба балансировки нагрузки, ingress - на ваш выбор)

и изменить пароль:

argocd account update-password

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

Страница входа в пользовательский интерфейс Argo CD

Добавление кластеров

Вы можете управлять сразу несколькими кластерами внутри Argo CD, например, у вас могут быть разные кластеры для разных сред, таких как dev, test, staging, production или что-то еще.

По умолчанию кластер, в котором развернут Argo CD, уже настроен с помощью Argo CD:

Вы также можете увидеть список кластеров с помощью интерфейса командной строки:

argocd cluster list

Если вы хотите управлять другими кластерами, вы можете запустить:

argocd cluster add CONTEXTNAMECONTEXTNAME- имя kube контекста в вашей локальной конфигурации.

Helloworld-пример развертывания

Теперь мы можем попробовать создать приложение на Argo CD.

Версия TL;DR или версия Мне не нравится UI в этом разделе это одна команда:

argocd app create helloworld --repo https://github.com/ironcore864/go-hello-http.git --path helm --sync-policy automatic --dest-server https://kubernetes.default.svc --dest-namespace default --values values.yaml --values values.dev.yaml

Эта CLI-команда создаст приложение, развернет его и синхронизирует. Чтобы продемонстрировать простоту Argo CD, я буду делать то же самое в пользовательском интерфейсе, а именно:

Нажмите кнопку NEW APP в консоли пользовательского интерфейса:

Затем вам нужно ввести несколько параметров для задачи, например:

  • Application Name: имя этого приложения. Здесь я назову его просто helloworld

  • Project: вы можете выбрать default. Project (проект) это концепция внутри Argo CD, в рамках которой вы можете создать несколько приложений и связать каждое приложение с проектом

  • Sync policy: вы можете выбрать между Manual и Automatic (что даст вам настоящий GitOps). Здесь я выберу Automatic (обратите внимание, что здесь в пользовательском интерфейсе значение по умолчанию Manual).

Нажмите SYNC POLICY и выберите Automatic

Затем мы перейдем к SOURCE части. Нам нужно указать URL-адрес git. Я собираюсь использовать пример, расположенный по адресу. Если вы перейдете в этот репозиторий, вы обнаружите, что там нет ничего, кроме простого Golang приложения с папкой с именем helm, которая представляет собой диаграмму для развертывания с несколькими файлами значений.

После того, как вы ввели URL-адрес git-репозитория, кликните часть PATH, и вы обнаружите, что Argo CD уже автоматически обнаружил, что у нас есть папка helm в этом репозитории, которая содержит вещи, которые могут нас заинтересовать:

Кликните Path и выберите имя папки helm в раскрывающемся меню.

Итак, здесь мы просто кликаем раздел Path и выбираем папку helm.

Стоит отметить, что Argo CD поддерживает несколько инструментов для развертывания. Сам Argo CD не предвзят; он позволяет использовать собственный YAML k8s, или kustomize, или helm. Например, если файлы в Path представляют собой схему управления, Argo CD знает, что нужно запустить установку Helm; но если это просто файлы YAML k8s, Argo CD знает, что вместо этого нужно запустить kubectl apply. Умно, не правда ли?

В разделе Destination нам нужно выбрать, в каком кластере Kubernetes развернуть это приложение (выбор из раскрывающегося списка) и в каком пространстве имен (введите текст).

Щелкните URL-адрес кластера, выберите кластер для развертывания и введите пространство имен.

Поскольку в этом примере в нашей папке helm есть диаграмма, Argo CD автоматически загружает новый раздел с именем Helm, чтобы попросить вас выбрать, какой файл значений, который нужно применить:

Кликните раздел VALUES FILES, и вы можете выбрать один или несколько файлов из списка, который выбирается из Path, настроенного ранее.

Это раскрывающееся меню, вам даже не нужно вводить имена файлов. Эта часть хорошо интегрирована с helm причина, по которой я предпочитаю создавать приложение в пользовательском интерфейсе, хотя то же самое можно сделать с помощью одной единственной CLI-команды.

Здесь мы указали два файл со значениями, один по умолчанию, другой для dev стейджа, и в нем есть значения, которые переопределяют значения, определенные по умолчанию.

После нажатия кнопки Create Argo CD синхронизирует (sync) статус, определенный в git-репозитории, и если текущее состояние не совпадает с тем, что определено в git, Argo CD синхронизирует его и переведет состояние в то же, что определено в git. Через некоторое время вы увидите, что приложение синхронизировано, и какие компоненты развернуты:

Приложение синхронизированоПриложение синхронизированоПодробное представление приложенияПодробное представление приложения

В этом примере у нас есть развертывание, в котором развернут только 1 под (значения из values.dev.yaml переопределяет 3 пода по умолчанию, определенные в файле values.yaml), и сервис, раскрывающий развертывание. Теперь, если мы перейдем к целевому кластеру и проверим, он действительно уже развернут:

Приложение действительно развернуто, без шутокПриложение действительно развернуто, без шуток

Это демонстрирует весь процесс, в котором вы создаете приложение и развертываете его. Весь процесс занимает около 1 минуты, без написания bash-скриптов для cd в какую-либо папку, а затем для установки helm, или, что еще хуже, если у вас нет подходящего образа с helm, необходимости его собрать или найти.

Там также есть много CLI-команд, которые можно использовать, если вы предпочитаете их пользовательскому интерфейсу, чтобы у вас был контроль над всем, не выходя из зоны комфорта.

GitHub SSO

Последняя, но не по важности, тема это пользователи и разрешения, как и в каждом новом инструменте, который мы хотим внедрить в нашу команду.

После установки Argo CD имеет одного встроенного администратора, который имеет полный доступ к системе. Рекомендуется использовать пользователя с правами администратора только для изначальной настройки, а затем переключиться на локальных пользователей или настроить SSO-интеграцию.

Вы можете создать локального пользователя в Argo CD, но вы, вероятно, захотите настроить SSO.

Argo CD включает и поставляет Dex как часть установочного комплекта с целью делегирования аутентификации внешнему поставщику идентификации. Поддерживаются несколько типов поставщиков идентификации (OIDC, SAML, LDAP, GitHub и т. Д.). Для настройки единого входа (SSO) на Argo CD необходимо отредактировать файл конфигурации argocd-cm с настройками Dex-коннектора. После регистрации нового OAuth приложения в git вы можете отредактировать configmap argocd-cm, чтобы добавить следующие значения:

data:  url: https://argocd.example.com  dex.config: |    connectors:      # GitHub example      - type: github        id: github        name: GitHub        config:          clientID: aabbccddeeff00112233          clientSecret: $dex.github.clientSecret          orgs:          - name: your-github-org      # GitHub enterprise example      - type: github        id: acme-github        name: Acme GitHub        config:          hostName: github.acme.com          clientID: abcdefghijklmnopqrst          clientSecret: $dex.acme.clientSecret          orgs:          - name: your-github-org

После сохранения изменения должны вступить в силу автоматически, и у вас будет GitHub SSO уже для всей вашей команды.

GitHub SSO (здесь в примере корпоративный git)GitHub SSO (здесь в примере корпоративный git)

Если вы запустите GitHub SSO с новым пользователем, входящим в систему, он не увидит список кластеров или только что созданное приложение это из-за функций RBAC, позволяющих ограничивать доступ к ресурсам Argo CD. После того, как мы уже включили SSO, мы можем настроить RBAC, создав следующую configmap argocd-rbac-cm:

apiVersion: v1kind: ConfigMapmetadata:  name: argocd-rbac-cm  namespace: argocddata:  policy.default: role:readonly  policy.csv: |    p, role:org-admin, applications, *, */*, allow    p, role:org-admin, clusters, get, *, allow    p, role:org-admin, repositories, get, *, allow    p, role:org-admin, repositories, create, *, allow    p, role:org-admin, repositories, update, *, allow    p, role:org-admin, repositories, delete, *, allow    g, your-github-org:your-team, role:org-admin

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

Заключение

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


Узнать подробнее о курсе Инфраструктурная платформа на основе Kubernetes.

Смотреть открытый вебинар Работа с NoSQL базами в k8s (на примере Apache Cassandra).

Подробнее..

Четыре API для базы данных

10.02.2021 20:14:57 | Автор: admin

Одновременный сеанс в IRIS: SQL, объекты, REST, GraphQL

Казимир Малевич, Спортсмены (1932)Казимир Малевич, Спортсмены (1932)

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

Казимир Малевич (1916)

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

Есть несколько соображений о том, почему стоит поговорить про обмен данными через API на основе SQL/REST/GraphQL, в противовес представлению их в виде типов/объектов:

  • это массово изучаемые и достаточно легко используемые технологии;

  • их широкая популярность в доступных и открытых программных продуктах просто невероятна;

  • часто, особенно в вебе и в базах данных, у вас просто нет выбора;

  • или наоборот, когда у вас всё ж таки есть выбор его надо сделать осознанно:

  • и главное, внутри в границах этих API остаются объекты, как наиболее адекватный способ их реализации в коде.

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

Сегодня, данные хранятся, как давно повелось, на жёстких дисках в HDD или, уже по современному, в микросхемах флеш-памяти в SSD. Данные пишутся и читаются потоком, состоящем из отдельных блоков хранения на HDD/SSD.

Деление на блоки не случайное, а задаваемое физикой/механикой/электроникой накопителя данных. В HDD это дорожки/сектора на вращающемся магнитном диске. В SSD это сегменты памяти в перезаписываемом кремниевом чипе. Суть одна это блоки информации, из частей которых необходимо найти и собрать воедино нужные нам кусочки данных. Собрать в структуры, которые соответствуют нашей модели/типу данных со значением необходимым на момент запроса. За этот процесс как раз отвечает связка из СУБД и файловой подсистемы в операционной системе.

По правде говоря, можно обращаться и минуя СУБД напрямую к операционной системе или даже напрямую к HDD/SSD. Но тогда мы теряем два супер важных мостика к данным первый, между блоками хранения и файловыми потоками, и, второй, между файлами и упорядоченной структурой в модели базы данных. Или, говоря другими словами, мы берём на себя ответственность по разработке всего этого объёма кода для обработки блоков/файлов/моделей со всеми оптимизациями, тщательной отладкой и долговременными испытаниями на надёжность.

Так вот, СУБД дают нам прекрасную возможность обращаться с данными на языке высокого уровня сразу оперируя понятными моделями и представлениями. Хвала им за это. А хорошие СУБД и платформы данных, такие как InterSystems IRIS, предоставляют больше доступ к упорядоченным данным сразу множеством способов одновременно. И выбор уже за программистом, какой из них выбрать для своего проекта.

Так воспользуемся же множественными привилегиями, которые даёт нам IRIS. Сделаем код красивее и полезнее сразу будем применять объектно-ориентированные язык ObjectScript для использования и разработки API. То есть, например, SQL код будем вызывать прямо изнутри программы на ObjectScript. А для других API воспользуемся готовыми библиотеками и встроенными средствами ObjectScript.

Для примеров будем использовать данные из замечательного интернет-проекта "SQL Zoo", в котором обучают языку запросов SQL. Эти же данные будем использовать в других примерах API.

Если хочется сразу посмотреть на многообразие подходов к проектированию API и воспользоваться готовыми решениями, то вот интересная и полезная коллекция публичных API, которая коллективно собирается в проекте на гитхабе https://github.com/public-apis/public-apis

SQL

Начинаем вполне естественно с SQL. Кто ж его не знает?

Для изучения SQL есть гигантское количество учебных курсов и книг. Мы будем опираться на https://sqlzoo.net. Это хороший начальный онлайн курс по SQL с примерами, прохождением заданий и справочником языка.

Будем переносить задачки из SQLZoo на платформу IRIS и получать аналогичные решения разными способами.

Как быстро получить доступ к InterSystems IRIS на своём компьютере? Один из самых быстрых вариантов развернуть контейнер в докере из готового образа InterSystems IRIS Community Edition бесплатная версия InterSystems IRIS Data Platform

Другие способы получить доступ к InterSystems IRIS Community Edition на портале обучения.

Перенесём данные с SQLZoo в хранилище нашего собственного экземпляра IRIS. Для этого:

  1. Открываем портал управления (у меня, например, по ссылке http://localhost:52773/csp/sys/UtilHome.csp),

  2. Переключаемся на область USER - в Namespace %SYSнажимаем ссылку Switch и выбираем USER

  3. Переходим в меню Система > SQL - открываем Обозреватель системы, затем SQL и жмём кнопку Запустить.

  4. Справа на будет открыта закладка "Исполнить запрос" с кнопкой "Исполнить" - она то нам и нужна.

Более подробно о работе с SQL через портал управления можно посмотреть в документации.

Кстати, другим, удобным вам, способом попробовать SQL доступ к базе данных в IRIS, может оказаться популярный у разработчиков редактор Visual Studio Code с плагином SQLTools и драйвером "SQLTools Driver for InterSystems IRIS". Попробуйте этот вариант.

Инструкция как настроить доступ к IRIS и разработке на ObjectScript в VSCode.

Готовые скрипты для развёртывания базы данных и набора тестовых данных SQLZoo есть в описании на сайте в разделе Данные.

Пара прямых ссылок для таблицы World:

Скрипт для создания базы данных можно выполнить тут же на портале управления IRIS в форме "Исполнитель запросов".

CREATE TABLE world(

name VARCHAR(50) NOT NULL

,continent VARCHAR(60)

,area DECIMAL(10)

,population DECIMAL(11)

,gdp DECIMAL(14)

,capital VARCHAR(60)

,tld VARCHAR(5)

,flag VARCHAR(255)

,PRIMARY KEY (name)

)

Для загрузки набора тестовых над формой "Исполнитель запросов" есть меню Мастера > Импорт данных. Для этого каталог с файлом тестовых данных необходимо добавить заранее, при создании вашего контейнера, или загрузить с вашего компьютера через браузер такая опция есть тут же в мастере импорта данных панели управления.

Проверяем наличие таблицы с данными запуском простейшего скрипта в форме "Исполнитель запросов":

SELECT * FROM world

Теперь нам доступны примеры и задания с сайта "SQL Zoo". Все примеры ниже реализуют SQL запрос из первого задания:

SELECT population

FROM world

WHERE name = 'France'

Так что можете смело продолжать начатое нами исследование API перенося задания из SQLZoo на платформу IRIS.

Внимание! Как я обнаружил, данные в интерфейсе сайта SQLZoo и данные его экспорта отличаются. Как минимум в первом примере расходятся значения по населению Франции и Германии. Не переживайте. Вот данные Евростата для ориентировки.

Что бы плавно перейти к следующему шагу объектный доступ к нашей базе данных, сделаем небольшой промежуточный шаг от "чистых" SQL запросов к SQL запросам встроенные в код приложения на ObjectScript объектно-ориентированном языке, встроенном в IRIS.

Class User.worldquery

{

ClassMethod WhereName(name As %String)

{

&sql(

SELECT population INTO :population

FROM world

WHERE name = :name

)

IF SQLCODE<0 {WRITE "SQLCODE error ",SQLCODE," ",%msg QUIT}

ELSEIF SQLCODE=100 {WRITE "Query returns no results" QUIT}

WRITE name, " ", population

}

}

Проверяем результат в терминале:

do ##class(User.worldquery).WhereName("France")

В ответ должны вернуться название страны и число жителей.

Объекты/типы

Это общая преамбула к истории про REST/GraphQL. Мы реализуем API в сторону веб протоколов. Чаще всего у нас под капотом на серверной стороне будет исходный код на каком-то из языков, хорошо поддерживающих типизацию или даже целиком объектно-ориентированную парадигму. Вот те самые: Spring на Java/Kotlin, Django на Python, Rails на Ruby, ASP.NET на C# или даже Angular на TypeScript. И безусловно объекты в ObjectScript, родном для платформы IRIS.

Почему это важно? Типы и объекты в вашем коде, при отправке вовне, будут упрощены до структур данных. Необходимо учитывать упрощение моделей в программе, что аналогично учёту потерь в реляционных моделях. И заботиться о том, что на другой стороне API, модели будут адекватно восстановлены и использованы вами или другими разработчиками без искажений. Это дополнительная нагрузка и увеличение ответственности программиста. Вне кода, вне помощи трансляторов/компиляторов и других автоматических инструментов при создании программ, необходимо вновь и вновь заботиться о корректной передаче моделей.

Если посмотреть на вопрос с другой стороны, то пока не видно на горизонте технологий и инструментов легко передающих типы/объекты из программы на одном языке в программу на другом. Что остаётся? Вот эти упрощения SQL/REST/GraphQL и разливанное море документации с описанием API на человечески понятном языке. Неформальная, с компьютерной точки зрения, документация для разработчиков это как раз то, что следует всячески переводить в формальный код, который компьютер обрабатывать умеет.

Подходы к решению этих проблем предпринимаются регулярно. Одно из успешных это кросс языковая парадигма в объектной СУБД платформы IRIS.

Что бы чётко понимать как соотносятся модели ОПП и SQL в IRIS, предлагаю посмотреть на таблицу:

Объектно-ориентированное программирование (ООП)

Структурированный язык запросов (SQL)

Пакет

Схема

Класс

Таблица

Свойство

Столбец

Метод

Хранимая процедура

Отношение между двумя классами

Ограничение внешнего ключа, встроенный join

Объект (в памяти или на диске)

Строка (на диске)

Более подробно про отображение объектной и реляционной модели можно посмотреть в документации IRIS.

При выполнении нашего SQL запроса на создание таблицы world из примера выше, IRIS автоматически генерирует у себя описания соответствующего объекта класс с именем User.world.

Class User.world Extends %Persistent [ ClassType = persistent, DdlAllowed, Final, Owner = {_SYSTEM}, ProcedureBlock, SqlRowIdPrivate, SqlTableName = world ]

{

Property name As %Library.String(MAXLEN = 50) [ Required, SqlColumnNumber = 2 ];

Property continent As %Library.String(MAXLEN = 60) [ SqlColumnNumber = 3 ];

Property area As %Library.Numeric(MAXVAL = 9999999999, MINVAL = -9999999999, SCALE = 0) [ SqlColumnNumber = 4 ];

Property population As %Library.Numeric(MAXVAL = 99999999999, MINVAL = -99999999999, SCALE = 0) [ SqlColumnNumber = 5 ];

Property gdp As %Library.Numeric(MAXVAL = 99999999999999, MINVAL = -99999999999999, SCALE = 0) [ SqlColumnNumber = 6 ];

Property capital As %Library.String(MAXLEN = 60) [ SqlColumnNumber = 7 ];

Property tld As %Library.String(MAXLEN = 5) [ SqlColumnNumber = 8 ];

Property flag As %Library.String(MAXLEN = 255) [ SqlColumnNumber = 9 ];

Parameter USEEXTENTSET = 1;

/// Bitmap Extent Index auto-generated by DDL CREATE TABLE statement. Do not edit the SqlName of this index.

Index DDLBEIndex [ Extent, SqlName = "%%DDLBEIndex", Type = bitmap ];

/// DDL Primary Key Specification

Index WORLDPKey2 On name [ PrimaryKey, Type = index, Unique ];

}

Это отличная заготовка для разработки своего приложения в объектно-ориентированном стиле. Достаточно добавить методы в класс на ObjectScript, имеющий готовые привязки к базе данных. Фактически, методы этого класса являются хранимыми процедурами, если использовать терминологию SQL-подхода.

Пробуем реализовать тот же пример, что выше делали на SQL, Добавляем в класс User.world метод WhereName, который играет роль конструктора объекта "информация о стране"по заданному названию страны:

ClassMethod WhereName(name As %String) As User.world

{

Set id = 1

While ( ..%ExistsId(id) ) {

Set countryInfo = ..%OpenId(id)

if ( countryInfo.name = name ) { Return countryInfo }

Set id = id + 1

}

Return countryInfo = ""

}

Проверяем в терминале:

set countryInfo = ##class(User.world).WhereName("France")

write countryInfo.name

write countryInfo.population

Из этого примерна видно, что для поиска нужного объекта по названию страны, в отличие от SQL запроса, нам необходимо самостоятельно перебирать поочередно записи в базе данных. В худшем случае, если наш объект в конце списка или его вообще нет, перебирать придётся все записи. Про ускорение поиска через индексированные поля объекта и автогенерацию методов класса в IRIS разговор особый поищите сами в документации и в публикациях на портале сообщества разработчиков.

Например, для нашего класса, зная сгенерированное IRIS имя индекса по названию страны WORLDPKey2 можно инициализировать/конструировать объект из базы данных одним скоростным запросом:

set countryInfo = ##class(User.world).WORLDPKey2Open("France")

Проверяем так же:

write countryInfo.name

write countryInfo.population

Хорошие рекомендации по выбору между объектным и SQL доступом к хранимым объектам есть в документации. Хотя, в любом случае, вы вольны для своих задач использовать только один из них на 100%.

Плюс ещё и том, что благодаря наличию в IRIS готовых бинарных связок с распространёнными ООП языками, какими как Java, Python, С, C# (.Net), JavaScript и, даже быстро набирающему популярность, Julia [1][2]. Всегда есть возможность выбора удобных вам языковых средств разработки.

Теперь приступим непосредственно к разговору о данных в веб-API.

REST он же RESTful веб-API

Вырываемся за границы сервера и уютного терминала. Пробираемся поближе к массовым интерфейсам браузерам и им подобным приложениям. Туда, где в основе взаимодействия систем используются гипертекстовые протоколы семейства http. В IRIS "из коробки" для этого работает связка из собственно сервера баз данных и http-сервера Apache.

Representational state transfer (REST) архитектурный стиль проектирования распределённых приложений и, в частности, веб-приложений. Ему, между прочим, пошел уже третий десяток лет. REST применяется повсеместно не имея какой-либо стандартизации, кроме как опоры на протоколы http/https. Совсем не то, что его коллеги/конкуренты в веб-службах на базе стандартов для протоколов SOAP и XML-RPC.

Глобальным ID в REST является URL и он определяет каждую очередную информационную единицу при обмене с базой данных или с бекэнд приложением.

Документация по разработке REST-сервисов в IRIS.

В нашем примере основой идентификатором будет что-то вроде основы из адреса сервера IRIS http://localhost:52773 и приставленного к нему пути до наших данных /world/ или более конкретно по стране /world/France.

Примерно так в докер-контейнере:

http://localhost:52773/world/France

При разработке полноценного приложения в документации IRIS рекомендуется воспользоваться одним из трёх генераторов кода. Например, один из них основан на описании REST API по спецификации OpenAPI 2.0.

Мы пойдём простым путём реализуем API вручную. В нашем примере сделаем простейшее REST-решение, которое требует всего два шага в IRIS:

  1. Создать класс-диспетчер путей в URL, который будет унаследован от системного класса %CSP.REST

  2. Добавить обращение к нашему классу-диспетчеру при настройке веб-приложения IRIS

Шаг 1 Класс-диспетчер

Реализация класса должна быть достаточно понятна. Делаем по инструкции в документации для "ручного" REST: https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GREST_csprest#GREST_csprest_urlmap

/// Description

Class User.worldrest Extends %CSP.REST

{

Parameter UseSession As Integer = 1;

Parameter CHARSET = "utf-8";

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]

{

<Routes>

<Route Url="/:name" Method="GET" Call="countryInfo" />

</Routes>

}

}

И внутри добавляем метод-обработчик ровно так же, как были устроены вызовы в терминале из предыдущего примера:

ClassMethod countryInfo(name As %String) As %Status

{

set countryInfo = ##class(User.world).WhereName(name)

write "Country: ", countryInfo.name

write "<br>"

write "Population: ", countryInfo.population

return $$$OK

}


Как можете видеть, для передачи из входящего REST-запроса в параметра name вызываемого метода-обработчика в диспетчере указывается параметр с двоеточием в начале ":name".

Шаг 2 Настройка веб-приложения IRIS

В меню System Administration > Security > Applications > Web Applications

добавляем новое веб-приложение с указанием точки входа по URL на /world и обработчик наш класс-диспетчер worldrest.

Лайфхаки

Веб-приложение после настройки должно сразу отзываться по ссылке http://localhost:52773/world/France (регистр букв для передачи данных запроса в параметр метода важен, должно быть как в базе данных).

При необходимости используйте инструменты отладки хорошее описание есть в этой статье из двух частей (в комментариях загляните тоже). https://community.intersystems.com/post/debugging-web

Если появляется ошибка "401 Unauthorized", а вы уверены, что класс-диспетчер есть на сервере и ссылка правильная, то попробуйте добавить в настройках веб-приложения роль %All во вкладке "Application Roles". Это не совсем безопасно и надо понимать что вы разрешаете, но для локальной работы допустимо.

GraphQL

Эта территория новая, в том смысле, что в документации IRIS об API с использованием GraphQL вы ничего не найдёте сегодня. Но это не помешает нам воспользоваться таким замечательным инструментом.

Всего пять лет как GraphQL появился на публике.

Это язык запросов для API. И, наверное, это лучшая технология, которая возникла при совершенствовании REST-архитектуры и различных веб-API. Развитием GraphQL занимается Linux фонд. Небольшая вводная статья для начинающих.

А благодаря стараниям энтузиастов и инженеров InterSystems воспользоваться возможностями GraphQL в IRIS можно с 2018 года.

Статья на Хабре: Как я реализовал GraphQL для платформ компании InterSystems

En: GraphQL понимаем, объясняем, внедряем

Приложение для GraphQL состоит из двух модулей бекенд приложения на стороне IRIS и фронтэнд части, работающей в браузере. То есть, необходимо настроить по инструкции для веб-приложения GraphQL и GraphiQL.

Для примера, как это выглядит у меня на IRIS в докер-контейнере. Настройки веб-приложения GraphQL выполняющее функции REST-диспетчера и обработчика схемы базы данных:

И второе приложение GraphiQL пользовательских интерфейс для браузера, написан на HTML и JavaScript:

Запускает по адресу http://localhost:52773/graphiql/index.html

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

Пример GraphQL запроса к нашей базе данных:

{

User_world ( name: France ) {

name

population

}

}


И соотвествующий ему ответ:

{

"data": {

"User_world": [

{

"name": "France",

"population": 65906000

}

]

}

}

Так это выглядит в браузере:

Итоги


Возраст технологии

Пример запроса

SQL

50 лет

Кодд

SELECT population

FROM world

WHERE name = 'France'

ООП

40 лет

Кэй, Ингаллс

set countryInfo = ##class(User.world).WhereName("France")

REST

20 лет

Филдинг

http://localhost:52773/world/France

GraphQL

5 лет

Байрон

{ User_world ( name: France ) {

name

population

}

}

  1. Жизненной силы и энергии у технологий SQL, REST и, теперь наверное, GraphQL много и это надолго. Они все прекрасно уживаются внутри платформы IRIS и, при должном внимании разработчика, дарят радость в создании программ работы с данными.

  2. Хоть это не упомянуто в статье, другие великолепные API на основе XML (SOAP) и JSON в IRIS также реализованы на должном уровне. Пользуйтесь на здоровье.

  3. Помните, что любой обмен данными через API это всё же неполноценная, а урезанная версия передачи объектов. И задача корректной передачи информации о типа данных в объекте, потерянной в API остаётся, на разработчике, а не в коде.


Вопрос к вам, дорогие читатели

Эта статья готовилась не только для сравнения современных API и, даже, не столько для обзора базовых возможностей IRIS. Больше пользы от примеров выше смогут получить начинающие программисты: увидеть легкость в переключении между API при доступе к базе данных, сделать первые шаги в IRIS, получить быстрый результат для своей задачи.

И поэтому, очень интересно ваше мнение, помогает ли такой подход для "легкого старта", какие шаги процесса доставляют сложность для начинающих осваивать инструменты работы с API в IRIS? Что показалось "неочевидным препятствием"? Поспрашивайте у осваивающих IRIS и напишите мне в комментариях. Думаю, это всем будет полезно обсудить.

Подробнее..

Cassandra Day Russia 2021 онлайн-конференция 27 марта

18.02.2021 16:06:37 | Автор: admin
image

Что объединяет Apple, Netflix, Huawei и Instragram? Не только миллиарды запросов, петабайты данных и пользователи по всему миру. Все эти компании используют распределённую NoSQL базу данных Apache Cassandra.

Приглашаем на однодневную онлайн-конференцию Cassandra Day Russia 2021 в субботу 27 марта. Опытные NoSQL специалисты расскажут о возможностях одной из самых мощных баз данных современности и поделятся практическим опытом управления СУБД Cassandra.

О мероприятии


Конференция будет состоять из двух параллельных потоков:

  • Воркшопы для тех, кто только начинает или планирует работу с Cassandra;
  • Доклады для опытных специалистов.

Время проведения: 27 марта, 10:0017:00 (UTC+3)

Ведущий Александр Волочнев, Developer Advocate, DataStax, Inc.

Партнеры конференции: DataStax, Слёрм.

Для участников конференции DataStax, Inc. спонсирует сертификацию по Apache Cassandra: вы сможете бесплатно пройти обучение, сдать экзамен и стать Certified Apache Cassandra Developer/Administrator (обычная стоимость экзамена $145). Также будут другие подарки от партнёров.

Участие бесплатное, необходима регистрация.
Подробнее..

Перевод Apache Cassandra 4.0 бенчмарки

20.02.2021 14:06:35 | Автор: admin


Apache Cassandra 4.0 приближается к бете (прим. переводчика: на текущий момент уже доступна бета 4, выпущенная в конце декабря 2020), и это первая версия, которая будет поддерживать JDK 11 и более поздних версий. Пользователей Apache Cassandra, очевидно, волнует задержка, так что мы возлагаем большие надежды на ZGC новый сборщик мусора с низкой задержкой, представленный в JDK 11.


В JDK 14 он был выпущен уже в GA-версии, и нам было очень интересно оценить, насколько он подходит для кластеров Apache Cassandra. Мы хотели сравнить производительность Apache Cassandra 3.11.6 и 4.0 и проверить, подходит ли Shenandoah, сборщик мусора от Red Hat, для продакшена. Спойлер: Cassandra 4.0 значительно лучше по производительности сама по себе, а с новыми сборщиками мусора (ZGC и особенно Shenandoah) будет совсем хорошо.


Методология бенчмарков


Мы провели следующие бенчмарки, используя утилиту tlp-cluster для развертывания и настройки кластеров Apache Cassandra в AWS и tlp-stress для создания нагрузок и сбора метрик. Все эти инструменты доступны в опенсорсе, все бенчмарки легко воспроизвести при наличии учетки AWS.


Кластеры состояли из трех нод на инстансах r3.2xlarge и одной стресс-ноды на c3.2xlarge.


Мы использовали дефолтные параметры Apache Cassandra, кроме настроек кучи и сборщика мусора.


Для развертывания и настройки кластера мы взяли последний релиз tlp-cluster. Недавно мы добавили скрипты, чтобы автоматизировать создание кластера и установку Reaper и Medusa.


Установите и настройте tlp-cluster по инструкциям в документации, чтобы создать такие же кластеры, которые мы использовали для бенчмарков:


# 3.11.6 CMS JDK8build_cluster.sh -n CMS_3-11-6_jdk8 -v 3.11.6 --heap=16 --gc=CMS -s 1 -i r3.2xlarge --jdk=8 --cores=8# 3.11.6 G1 JDK8build_cluster.sh -n G1_3-11-6_jdk8 -v 3.11.6 --heap=31 --gc=G1 -s 1 -i r3.2xlarge --jdk=8 --cores=8# 4.0 CMS JDK11build_cluster.sh -n CMS_4-0_jdk11 -v 4.0~alpha4 --heap=16 --gc=CMS -s 1 -i r3.2xlarge --jdk=11 --cores=8# 4.0 G1 JDK14build_cluster.sh -n G1_4-0_jdk14 -v 4.0~alpha4 --heap=31 --gc=G1 -s 1 -i r3.2xlarge --jdk=14 --cores=8# 4.0 ZGC JDK11build_cluster.sh -n ZGC_4-0_jdk11 -v 4.0~alpha4 --heap=31 --gc=ZGC -s 1 -i r3.2xlarge --jdk=11 --cores=8# 4.0 ZGC JDK14build_cluster.sh -n ZGC_4-0_jdk14 -v 4.0~alpha4 --heap=31 --gc=ZGC -s 1 -i r3.2xlarge --jdk=14 --cores=8# 4.0 Shenandoah JDK11build_cluster.sh -n Shenandoah_4-0_jdk11 -v 4.0~alpha4 --heap=31 --gc=Shenandoah -s 1 -i r3.2xlarge --jdk=11 --cores=8

Примечание. Чтобы воспроизвести схожие условия, мы использовали для всех тестов один набор инстансов EC2.


Мы сделали апгрейд с Cassandra 3.11.6 на Cassandra 4.0~alpha4 и меняли JDK следующим кодом:


#!/usr/bin/env bashOLD=$1NEW=$2curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | bash. ~/.jabba/jabba.shjabba uninstall $OLDjabba install $NEWjabba alias default $NEWsudo update-alternatives --install /usr/bin/java java ${JAVA_HOME%*/}/bin/java 20000sudo update-alternatives --install /usr/bin/javac javac ${JAVA_HOME%*/}/bin/javac 20000

Мы использовали следующие значения JDK при вызове jabba:


  • openjdk@1.11.0-2
  • openjdk@1.14.0
  • openjdk-shenandoah@1.8.0
  • openjdk-shenandoah@1.11.0

OpenJDK 8 был установлен с помощью Ubuntu apt.


Вывод команды java -version для разных JDK, которые мы использовали для бенчмарков:


jdk8


openjdk version "1.8.0_252"OpenJDK Runtime Environment (build 1.8.0_252-8u252-b09-1~18.04-b09)OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)

jdk8 с Shenandoah


openjdk version "1.8.0-builds.shipilev.net-openjdk-shenandoah-jdk8-b712-20200629"OpenJDK Runtime Environment (build 1.8.0-builds.shipilev.net-openjdk-shenandoah-jdk8-b712-20200629-b712)OpenJDK 64-Bit Server VM (build 25.71-b712, mixed mode)

jdk11


openjdk version "11.0.2" 2019-01-15OpenJDK Runtime Environment 18.9 (build 11.0.2+9)OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)

jdk11 с Shenandoah


openjdk version "11.0.8-testing" 2020-07-14OpenJDK Runtime Environment (build 11.0.8-testing+0-builds.shipilev.net-openjdk-shenandoah-jdk11-b277-20200624)OpenJDK 64-Bit Server VM (build 11.0.8-testing+0-builds.shipilev.net-openjdk-shenandoah-jdk11-b277-20200624, mixed mode)

jdk14


openjdk version "14.0.1" 2020-04-14OpenJDK Runtime Environment (build 14.0.1+7)OpenJDK 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing)

CMS


CMS (Concurrent Mark Sweep) это текущий дефолтный сборщик мусора в Apache Cassandra. Он был удален из JDK 14, так что все тесты проводились с JDK 8 или 11.


Для CMS параметры были такие:


-XX:+UseParNewGC-XX:+UseConcMarkSweepGC-XX:+CMSParallelRemarkEnabled-XX:SurvivorRatio=8-XX:MaxTenuringThreshold=1-XX:CMSInitiatingOccupancyFraction=75-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSWaitDuration=10000-XX:+CMSParallelInitialMarkEnabled-XX:+CMSEdenChunksRecordAlways-XX:+CMSClassUnloadingEnabled-XX:ParallelGCThreads=8-XX:ConcGCThreads=8-Xms16G-Xmx16G-Xmn8G

Флаг -XX:+UseParNewGC удален из JDK 11 и является неявным. С этим флагом JVM не запустится.


Для CMS мы задали максимальный размер кучи 16 ГБ, иначе между сборками будут слишком длинные паузы.


G1


G1GC (сборщик мусора по принципу Garbage-First) настроить легче, чем CMS, потому что он динамически изменяет размер младшего поколения, но лучше работает с большими кучами (от 24 ГБ). Это объясняет, почему он не стал дефолтным сборщиком мусора. Задержки у него выше, чем у настроенного CMS, зато пропускная способность лучше.


Для G1 параметры были такие:


-XX:+UseG1GC-XX:G1RSetUpdatingPauseTimePercent=5-XX:MaxGCPauseMillis=300-XX:InitiatingHeapOccupancyPercent=70-XX:ParallelGCThreads=8-XX:ConcGCThreads=8-Xms31G-Xmx31G

Для версии 4.0 мы использовали JDK 14 в тестах с G1.


Мы взяли размер кучи 31 ГБ, чтобы использовать преимущества compressed oops и получить больше адресуемых объектов для кучи минимального размера.


ZGC


ZGC (Z Garbage Collector) это новейший сборщик мусора в JDK, который минимизирует задержку благодаря паузам stop-the-world меньше 10 мс. Предполагается, что благодаря этому размер кучи не будет влиять на продолжительность пауз, что позволит увеличить его до 16 ТБ. Если так и будет, хранилище вне кучи не понадобится, и некоторые аспекты разработки в Apache Cassandra станут проще.


Для ZGC параметры были такие:


-XX:+UnlockExperimentalVMOptions-XX:+UseZGC-XX:ConcGCThreads=8-XX:ParallelGCThreads=8-XX:+UseTransparentHugePages-verbose:gc-Xms31G-Xmx31G

-XX:+UseTransparentHugePages здесь нужен как обходной путь, чтобы избежать включения больших страниц в Linux. В официальной документации по ZGC сказано, что потенциально могут возникать пики задержки, но на практике мы такого не увидели. Может быть, следует протестировать пропускную способность на больших страницах и посмотреть, как это скажется на результате.


ZGC не может использовать compressed oops и не соблюдает порог 32 ГБ. Мы взяли кучу размером 31 ГБ, как и для G1, чтобы у системы был тот же объем свободной оперативки.


Shenandoah


Shenandoah это сборщик мусора с низкой задержкой от Red Hat. Он доступен как бэкпорт в JDK 8 и 11 и в mainline-сборках OpenJDK начиная с Java 13.


Как и ZGC, Shenandoah стремится к параллелизму, чтобы паузы не зависели от размера кучи.
Для Shenandoah параметры были такие:


-XX:+UnlockExperimentalVMOptions-XX:+UseShenandoahGC-XX:ConcGCThreads=8-XX:ParallelGCThreads=8-XX:+UseTransparentHugePages-Xms31G-Xmx31G

Shenandoah может использовать compressed oops, а значит лучше работает с кучами размером чуть меньше 32 ГБ.


Конфигурация Cassandra 4.0 JVM


Cassandra 4.0 поставляется с отдельными файлами jvm.options для Java 8 и Java 11, а именно:


  • conf/jvm-server.options
  • conf/jvm8-server.options
  • conf/jvm11-server.options

Апгрейд до 4.0 будет работать с имеющимся файлом jvm.options версии 3.11, если его переименовать в jvm-server.options, а файлы jvm8-server.options и jvm11-server.options удалить. Но мы такой подход не рекомендуем.


Лучше повторно применить параметры из предыдущего файла jvm.options к новым файлам jvm-server.options и jvm8-server.options. Конкретные файлы опций Java, в основном, связаны с флагами для сборки мусора. Если обновить эти два файла, будет проще настроить jvm11-server.options и перейти с JDK 8 на JDK 11.


Рабочие нагрузки


В бенчмарках мы использовали восемь потоков с ограничением скорости и соотношением записи и чтения 80/20%. tlp-stress широко использует асинхронные запросы, которые легко могут перегрузить ноды Cassandra с ограниченным числом стресс-потоков. В нагрузочных тестах каждый поток отправлял 50 параллельных запросов за раз. Мы создали keyspace с коэффициентом репликации 3, и все запросы выполнялись на уровне согласованности LOCAL_ONE.


Все сборщики мусора и версии Cassandra тестировались по нарастанию 25к, 40к, 45к и 50к операций в секунду, чтобы оценить производительность при разных уровнях нагрузки.


Команды tlp-stress:


tlp-stress run BasicTimeSeries -d 30m -p 100M -c 50 --pg sequence -t 8 -r 0.2 --rate <desired rate> --populate 200000

Все рабочие нагрузки выполнялись 30 минут, загружая от 5 до 16 ГБ данных на ноду и допуская нагрузку от compaction в разумной степени.


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


Результаты бенчмарков


3.11.6, 25-40к операций/с:



4.0, 25-40к операций/с:



4.0, 45-50к операций/с:




В плане пропускной способности Cassandra 3.11.6 осилила 41к операций в секунду, а Cassandra 4.0 51к, то есть на 25% больше. В обоих случаях использовался CMS. В версии 4.0 много улучшений, которые объясняют эти результаты, особенно в вопросах нагрузки на кучу, вызванной compaction (см. CASSANDRA-14654, например).


Shenandoah в jdk8 на Cassandra 3.11.6 не достигает максимальной пропускной способности в тесте на 40к операций в секунду появляются невыполненные запросы. С jdk11 и Cassandra 4.0 результат получше 49,6к, почти как у CMS. У G1 и Shenandoah с jdk 8 получилось 36к/с на Cassandra 3.11.6.


G1, видимо, усовершенствован в jdk14 и показал себя немного лучше, чем с jdk11, 47 против 50к/с.


ZGC не дотянул до конкурентов ни с jdk11, ни с jdk14, показав 41к/с максимум.








При умеренной нагрузке Shenandoah в jdk8 показывает хороший результат на Cassandra 3.11.6, но под высокой нагрузкой серьезно увеличиваются задержки.


С CMS Cassandra 4.0 показывает среднюю задержку для p99 (99-го перцентиля) от 11 до 31 мс при 50к операций в секунду. Средняя задержка операций чтения для p99 при умеренной нагрузке снизилась с 17 мс в Cassandra 3.11.6 до 11,5 мс в Cassandra 4.0, то есть на целых 30%.


В целом Cassandra 4.0 лучше Cassandra 3.11.6 с теми же сборщиками мусора на 2530% по пропускной способности и задержке.


Shenandoah дает очень низкие задержки при умеренной нагрузке в Cassandra 3.11.6, но с высокими нагрузками справляется не очень.


По задержкам ZGC хорошо себя проявляет при умеренной нагрузке, особенно с jdk14, но при более высокой скорости отстает от Shenandoah. Почти во всех нагрузочных тестах Shenandoah показал минимальные средние задержки для p99 для чтения и записи. Если сложить эти задержки с пропускной способностью в Cassandra 4.0, получится что этот сборщик мусора неплохой выбор, если вы обновляете версию. Средняя задержка 2,64 мс при чтении для p99 при умеренной нагрузке это впечатляет. Тем более на клиенте.


G1, в основном, соответствует настроенному максимальному времени паузы 300 мс для максимального значения p99, но с меньшей паузой при высоких нагрузках могут возникать нежелательные эффекты, и пауза может быть больше.


При умеренной нагрузке Shenandoah дает среднюю задержку для p99 на 77% ниже. 2,64 мс это самый низкий результат. Полезно для сценариев, где важна задержка. По сравнению с CMS в Cassandra 3.11.6 это на целых 85% ниже для операций чтения на p99!


Отдадим должное ZGC в jdk14, который хорошо себя показал при умеренных нагрузках, но с увеличением скорости, увы, не справился. Мы надеемся, что через несколько месяцев он станет лучше и сможет конкурировать с Shenandoah.


Итоги


G1 улучшает юзабилити Cassandra не нужно настраивать размеры поколений в ущерб производительности. В версии Apache Cassandra 4.0, которая и без того работает гораздо лучше, можно будет использовать сборщики мусора нового поколения, например Shenandoah и ZGC, которые не требуют тщательной настройки и сокращают задержки.


Мы не рекомендуем Shenandoah для Cassandra 3.11.6, поскольку он не справляется с высокими нагрузками, но начиная с jdk11 и Cassandra 4.0 этот сборщик мусора показывает низкие задержки и обеспечивает почти максимальную пропускную способность для базы данных.


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


Загрузите последнюю сборку Apache 4 и попробуйте сами. Будем рады обратной связи тут или в Slack компании ASF.


От редакции: приглашаем на открытую онлайн-конференцию Cassandra Day Russia 2021 в субботу 27 марта. В программе доклады и воркшопы от опытных NoSQL-специалистов.
Подробнее..

Tarantool vs Redis что умеют in-memory технологии

01.04.2021 18:23:17 | Автор: admin

В этой статье я хочу сравнить Redis и Tarantool. У меня нет цели сделать громогласный вывод Tarantool лучше! или Redis круче!. Я хочу понять их сходства и отличия, разобраться, для каких задач какую технологию выбрать. Потому что это очень близкие на первый взгляд вещи, и вопросы про их отличия я вижу часто.

Для этого мы посмотрим на технологии в трёх частях:

  • Вначале посмотрим глазами новичка. Что такое БД в памяти? Какие задачи они решают лучше дисковых БД?
  • Потом посмотрим архитектурно. Как обстоит вопрос с производительностью, надёжностью, масштабированием?
  • В третьей части лезем в технические вещи поглубже. Типы данных, итераторы, индексы, транзакции, ЯП, репликация, коннекторы.

Смело переходите сразу к наиболее интересной вам части. Или даже сразу к итоговой табличке сравнения, которую я прикладываю в заключении.

Поехали!

Содержание


  1. Вводная часть
    • Что такое БД в памяти
    • Зачем нужны решения в памяти
    • Что такое Redis
    • Что такое Tarantool
  2. Архитектурная часть
    • Производительность
    • Надёжность
    • Масштабируемость
    • Валидация схемы данных
  3. Технические особенности
    • Какие типы данных можно хранить
    • Вытеснение данных
    • Итерация по ключам
    • Вторичные индексы
    • Транзакции
    • Персистентность
    • Язык программирования для хранимых процедур
    • Репликация
    • Коннекторы из других языков программирования
    • Под какие задачи плохо подходят
    • Экосистема
    • Чем Redis лучше
    • Чем Tarantool лучше
  4. Вывод
  5. Ссылки

1. Вводная часть


Что такое БД в памяти


Redis и Tarantool это in-memory технологии. Их ещё называют резидентными БД, но я буду писать короче в памяти или in-memory. Так что такое БД в памяти?

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

Если данных слишком много, БД в памяти способны сохранять данные на диск. Можно перезагрузить узел и не потерять информацию. Стереотип про ненадёжность БД в памяти сильно устарел, их можно использовать как основное хранилище в production. Например, Mail.ru Cloud Solutions использует Tarantool как основную БД для хранения метаинформации в своём объектном хранилище [1].

БД в памяти нужны для высокой скорости доступа к данным, условно от 10 000 запросов в секунду. Например, запросы к ленте новостей Кинопоиска в день релиза Снайдерката Лиги Справедливости, Яндекс.Маркет перед Новым Годом или Delivery Club вечером в пятницу.

Зачем нужны решения в памяти


Кеш. БД в памяти традиционно используют как кеш для более медленных баз данных. Это логично, память быстрее диска (даже SSD). Но в жизненном цикле любого кеша случаются перезагрузки, падения, сетевая недоступность, нехватка памяти и прочие инфраструктурные беды.

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

  • Персистентность кеши сохраняют данные на диск. После перезагрузки восстанавливаем своё состояние без обращений к основному хранилищу. Если этого не делать, то обращение к холодному кешу будет очень долгим или даже положит основную БД.
  • Резервирование кеши содержат функции репликации данных. Если упал один узел, то второй возьмёт на себя запросы. Основное хранилище не упадёт от перегруза, нас спасает резервная нода.
  • Шардирование если горячие данные не помещаются в оперативную память одного узла, мы используем несколько узлов параллельно. Это горизонтальное масштабирование.

Шардирование это большая система. Резервирование это надёжная система. Вместе с персистентностью получается кластерное хранилище данных. Можно положить туда терабайты информации и крутить их на скорости 1 000 000 RPS.


OLTP. Расшифровывается как Online Transaction Processing, обработка транзакций в реальном времени. In-memory решения подходят для такого типа задач благодаря своей архитектуре. OLTP это большое количество коротких on-line транзакций (INSERT, UPDATE, DELETE). Главное в OLTP-системах быстро обработать запросы и обеспечить целостность данных. Эффективность чаще всего определяется количеством RPS.

Что такое Redis


  • Redis называет себя in-memory хранилище структур данных.
  • Redis это key-value.
  • Больше всего известен по кешированию дисковых баз. Если вы будете искать по ключевым словам кеширование баз данных, то в каждой статье найдёте упоминания Redis.
  • Redis поддерживает первичные индексы, не поддерживает вторичные.
  • Redis содержит в себе механизм хранимых процедур на Lua.

Что такое Tarantool


  • Tarantool называет себя платформа для in-memory вычислений.
  • Tarantool умеет в key-value. А еще в документы и реляционную модель данных.
  • Создан для горячих данных кеширования MySQL в соцсети. С течением времени развился в полноценную базу данных.
  • Tarantool может строить произвольное количество индексов по данным.
  • В Tarantool тоже можно написать хранимую процедуру и тоже на Lua.

Разобрались с основами, давайте переходить на следующий уровень.

2. Архитектурная часть


Производительность


Это самый любимый запрос про БД в памяти а насколько вы быстрые? Сколько миллионов РПС можно снять с одного ядра? Проведём простой синтетический тест, в нём максимально приблизим настройки баз данных. Скрипт на Go наполняет хранилище случайными ключами со случайными значениями.

MacBook Pro 2,9 GHz Quad-Core Intel Core i7Redis version=6.0.9, bits=64Tarantool 2.6.2

Redis

redis_test.gopackage mainimport (    "context"    "fmt"    "log"    "math/rand"    "testing"    "github.com/go-redis/redis")func BenchmarkSetRandomRedisParallel(b *testing.B) {    client2 := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})    if _, err := client2.Ping(context.Background()).Result(); err != nil {        log.Fatal(err)    }    b.RunParallel(func(pb *testing.PB) {        for pb.Next() {            key := fmt.Sprintf("bench-%d", rand.Int31())            _, err := client2.Set(context.Background(), key, rand.Int31(), 0).Result()            if err != nil {                b.Fatal(err)            }        }    })}

Tarantool

tarantool>box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024}box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})box.schema.space.create('kv', {if_not_exists=true,})box.space.kv:create_index('pkey', {type='TREE', parts={{field=1, type='str'}},                                   if_not_exists=true,})

tarantool_test.gopackage mainimport (    "fmt"    "math/rand"    "testing"    "github.com/tarantool/go-tarantool")type Tuple struct {    _msgpack struct{} `msgpack:",asArray"`    Key      string    Value    int32}func BenchmarkSetRandomTntParallel(b *testing.B) {    opts := tarantool.Opts{        User: "guest",    }    pconn2, err := tarantool.Connect("127.0.0.1:3301", opts)    if err != nil {        b.Fatal(err)    }    b.RunParallel(func(pb *testing.PB) {        var tuple Tuple        for pb.Next() {            tuple.Key = fmt.Sprintf("bench-%d", rand.Int31())            tuple.Value = rand.Int31()            _, err := pconn2.Replace("kv", tuple)            if err != nil {                b.Fatal(err)            }        }    })}

Запуск Чтобы полностью прогрузить базы данных, используем больше потоков.

go test -cpu 12 -test.bench . -test.benchtime 10sgoos: darwingoarch: amd64BenchmarkSetRandomRedisParallel-12          929368         15839 ns/opBenchmarkSetRandomTntParallel-12            972978         12749 ns/op

Результаты. Среднее время запроса к Redis составило 15 микросекунд, к Tarantool 12 микросекунд. Это даёт Redis 63 135 RPS, Tarantool 78 437 RPS.

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


Надёжность


Для надёжности хранения данных используют две основные техники:

  • Персистентность. При перезагрузке БД загрузит свои данные с диска, не будет запросов в сторонние системы.
  • Репликация. Если упал один узел, то есть копия на втором. Бывает асинхронная и синхронная.

И Redis, и Tarantool содержат эти функции. Технические подробности мы рассмотрим далее.

Масштабируемость


Масштабирование может рассматриваться для двух задач:

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

Redis

Узлы Redis можно соединить друг с другом асинхронной репликацией. Такие узлы будем называть репликационной группой, или replica set. Управлением такой репликационной группой занимается Redis Sentinel.

Redis Sentinel это один или несколько объединенных в кластер специальных процессов, которые следят за узлами Redis. Они выполняют четыре основные задачи:

  • Мониторинг узлов в группе: живой или мертвый.
  • Уведомление администратора или какой-то системы, если что-то случилось в группе.
  • Автоматическое переключение мастера.
  • Провайдер конфигурации для внешних клиентов, чтобы они знали, к кому подключиться.

В случае, когда данные необходимо расшардировать на несколько узлов, Redis предлагает open source-версию Redis Cluster. Она позволяет построить кластер, состоящий из нескольких репликационных групп. Данные в кластере шардируются по 16 384 слотам. Диапазоны слотов распределяются между узлами Redis.

Узлы в кластере общаются по отдельному открытому порту, чтобы понимать состояния соседей. Приложение при работе с Redis Cluster должно использовать специальный коннектор.

Tarantool

Tarantool также содержит в себе оба механизма масштабирования: репликацию и шардирование. Основной инструмент управления масштабированием Tarantool Cartridge. Он объединяет узлы в репликационные группы. В этой ситуации вы можете построить одну такую репликационную группу и использовать её аналогично Redis Sentinel. Tarantool Cartridge может управлять несколькими репликационными группами и шардировать данные между ними. Шардирование выполняется с помощью библиотеки vshard.

Различия

Администрирование

  • Администрирование Redis Cluster с помощью скриптов и команд.
  • В Tarantool Cartridge администрирование с помощью web-интерфейса или через API.

Корзины шардирования

  • Количество корзин шардирования в Redis фиксированное, 16 тыс.
  • Количество корзин шардирования Tarantool Cartridge (vshard) произвольное. Указывается один раз при создании кластера.

Ребалансировка корзин (решардинг)

  • В Redis Cluster настройка и запуск вручную.
  • В Tarantool Cartridge (vshard) автоматически.

Маршрутизация запросов

  • Маршрутизация запросов в Redis Cluster происходит на стороне клиентского приложения.
  • В Tarantool Cartridge маршрутизация запросов происходит на узлах-роутерах кластера.

Инфраструктура

  • Tarantool Cartridge также содержит:

    • механизм map/reduce запросов;
    • утилиту по упаковке приложения в пакеты rpm, dep и tar.gz;
    • Аnsible-роль для автоматического развёртывания приложения;
    • экспорт параметров мониторинга кластера.


Валидация схемы данных


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

В Tarantool на стороне сервера можно использовать валидацию по схеме данных:

  • с помощью встроенной валидации box.space.format, которая затрагивает только верхний уровень полей;
  • с помощью установленного расширения avro-schema.

3. Технические особенности


Какие типы данных можно хранить


В Redis ключом может быть только строка. В Redis можно хранить и манипулировать следующими типами данных:

  • строки;
  • списки строк;
  • неупорядоченные множества строк;
  • хешмапы или просто строковые пары ключ-значение;
  • упорядоченные множества строк;
  • Bitmap и HyperLogLog.

В Tarantool можно хранить и манипулировать следующими типами данных:

  • Атомарными:
    • строки;
    • логический тип (истина, ложь);
    • целочисленный;
    • с плавающей запятой;
    • с десятичной плавающей запятой;
    • UUID.

  • Комплексными:
    • массивы;
    • хешмапы.


Типы данных Redis лучше подходят для счётчиков событий, в том числе уникальных, для хранения небольших готовых витрин данных. А типы данных Tarantool лучше подходят для хранения объектов и/или документов, как в SQL и NoSQL СУБД.

Вытеснение данных


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

Перейдём к другому механизму, когда мы можем настроить алгоритм удаления больше ненужных данных. Redis содержит в себе несколько механизмов вытеснения:

  • TTL вытеснение объектов по завершении срока жизни;
  • LRU вытеснение давно использованных данных;
  • RANDOM вытеснение случайно попавшихся под руку объектов;
  • LFU вытеснение редко используемых данных.

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

В Tarantool для вытеснения данных можно использовать расширения expirationd или indexpiration, или создать собственную фоновую процедуру, которая будет проходить по вторичному индексу (с таймштампом) и удалять ненужные данные.

Итерация по ключам


В Redis можно это сделать с помощью операторов:

  • SCAN;
  • итерация по ключам.

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

В Tarantool доступна гибкая схема итерации по ключам. Можно итерироваться в прямом и обратном направлении. В процессе можно дополнительно фильтровать значения. Можно сместиться на определённое значение ключа, затем проходить по следующим ключам в сторону возрастания или убывания. Направление прохода на лету менять нельзя.

Например:

results = {}for _, tuple in box.space.pairs('key', 'GE') do    if tuple['value'] > 10 then        table.insert(results, tuple)  endendreturn results

Вторичные индексы


Redis

У Redis нет вторичных индексов. Есть некоторые трюки, чтобы их имитировать:

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

Tarantool

В Tarantool можно строить произвольное количество вторичных индексов для данных:

  • Вторичные ключи могут состоять из нескольких полей.
  • Для вторичных индексов можно использовать типы HASH, TREE, RTREE, BITSET.
  • Вторичные индексы могут содержать уникальные и не уникальные ключи.
  • У любых индексов можно использовать настройки локали, например, для регистронезависимых строковых значений.
  • Вторичные индексы могут строиться по полям с массивом значений (иногда их называют мультииндексы).

Вывод

Вторичные ключи и удобные итераторы позволяют строить в Tarantool реляционные модели хранения данных. В Redis такую модель построить невозможно.

Транзакции


Механизм транзакций позволяет выполнить несколько операций атомарно. И Redis, и Tarantool поддерживают транзакции. Пример транзакции в Redis:

> MULTIOK> INCR fooQUEUED> INCR barQUEUED> EXEC1) (integer) 12) (integer) 1

Пример транзакции в Tarantool:

do  box.begin()  box.space.kv:update('foo', {{'+', 'value', 1}})  box.space.kv:update('bar', {{'+', 'value', 1}})  box.commit()end

Персистентность


Персистентность данных обеспечивается двумя механизмами:

  • периодическим сбросом in-memory данных на диск snapshoting;
  • последовательной упреждающей записью всех приходящих операций в файл transaction journal.

И Redis, и Tarantool содержат оба механизма персистентности.

Redis

Redis периодически сбрасывает все данные из памяти на диск. Происходит это по-умолчанию каждые 60 секунд (настраивается). Redis использует механизм ОС fork для копирования текущих данных в памяти, затем информация сохраняется на диск. Если происходит аварийное завершение, то Redis восстановит состояние из последнего сохранения. Если последний снапшот был сделан давно, то данные, пришедшие после снапшота, будут утеряны.

Журнал операций используется для сохранения всей приходящей в базу информации. Каждая операция сохраняется в журнал на диске. Так, при запуске Redis восстанавливает своё состояние из снапшота и затем донакатывает оставшиеся транзакции из журнала.

  • Снапшот в Redis называется RDB (redis database).
  • Журнал операций в Redis называется AOF (append only file).

Tarantool

  • Механизм персистентности взят из архитектур баз данных.
  • Он является целостным снапшоты и журналирование.
  • Этот же механизм позволяет существовать надежной WAL-based репликации.

Tarantool периодически сохраняет текущие in-memory данные на диск и записывает каждую операцию в журнал.

  • Снапшот в Tarantool называется snap (snapshot). Можно делать с произвольной частотой.
  • Журнал транзакций в Tarantool называется WAL (write ahead log).

И в Redis, и в Tarantool каждый из механизмов может быть выключен. Для надёжного хранения данных оба механизма надо включить. Для максимального быстродействия можно отключить снапшотинг и журналирование, заплатив персистентностью. Слабоумие и отвага!

Различия

Для снапшотинга в Redis используется механизм ОС fork. Tarantool использует внутренний readview всех данных, это работает быстрее чем fork.

В Redis по умолчанию включён только снапшотинг. В Tarantool включён снапшотинг и журнал.

Redis хранит и использует только по одному файлу для снапшотов и журнала операций. Tarantool по-умолчанию хранит два файла снапшотов (можно настроить и больше) и консистентно дополняющее неограниченное количество журналов операций. При повреждении снапшот-файла Tarantool сможет загрузиться из предыдущего. Для Redis необходимо наладить механизм бекапов.

В Tarantool, в отличие от Redis, снапшоты и журналы образуют единый механизм отображения данных в файловой системе. Это значит, что в Tarantool и в файлах снапшотов и в журналах хранится полная метаинформация о транзакции, кто её сделал и когда. Она одного формата и взаимодополняющая.

Troubleshooting

Если повреждён файл журнала в Redis:

redis-check-aof --fix

Если повреждён файл журнала в Tarantool:

tarantool> box.cfg{force_recovery=true}

Язык программирования для хранимых процедур


Хранимые процедуры это код, выполняющийся рядом с данными. И Redis, и Tarantool предлагают Lua для создания хранимок. С точки зрения пользователя это очень простой язык. Он создавался для людей, для которых программирование будет инструментом решения задач в предметной области.

C точки зрения разработчика базы данных:

  • Lua это язык, который легко встраивается в существующее приложение.
  • Он просто интегрируется с объектами и процессами приложения.
  • Lua имеет динамическую типизацию и автоматическое управление памятью.
  • Язык имеет сборщик мусора incremental Mark&Sweep.

Различия

Реализация

  • В Redis используется ванильная реализация PUC-Rio.
  • В Tarantool используется LuaJIT.

Таймаут задач

  • В Redis можно задать таймаут, после которого выполнение хранимой процедуры прервётся.
  • В Tarantool хранимые процедуры компилируются и выполняются быстрее, но в этом механизме нет возможности выставить таймаут. Для прерывания хранимой процедуры пользователь должен предусмотреть механизм проверки флага прерывания.

Runtime

  • В Redis используется однозадачность: задачи выполняются по одной и целиком.
  • В Tarantool используется кооперативная многозадачность. Задачи выполняются по одной, но при этом задача отдаёт управление на операциях ввода-вывода или явно с помощью yield.

Вывод

  • В Redis Lua это просто хранимые процедуры.
  • В Tarantool это кооперативный runtime, в котором можно взаимодействовать со внешними системами.

Репликация


Репликация это механизм копирования объектов с одного узла на другой. Бывает асинхронная и синхронная.

  • Асинхронная репликация: при вставке объекта на один узел мы не дожидаемся, когда этот же объект будет отреплицирован на второй узел.
  • Синхронная репликация: при вставке объекта мы дожидаемся, когда он будет сохранён на первом и втором узлах.

И Redis, и Tarantool поддерживают асинхронную репликацию. Только Tarantool умеет в синхронную репликацию.

На практике бывают ситуации, когда мы хотим дождаться репликации объекта. И в Redis, и в Tarantool есть способы для этого:

  • В Redis это команда wait. Она принимает два параметра:
    • сколько реплик должны получить объект;
    • сколько ждать, пока это произойдёт.

  • В Tarantool это можно сделать фрагментом кода:

псевдокод:

local netbox = require('net.box')local replica = netbox.connect(...)local replica_vclock, err = replica.eval([[    return box.info().vclock]])while not vclock_compare(box.info().vclock, replica_vclock) do    fiber.sleep(0.1)end

Синхронная репликация

В Redis нет синхронной репликации. Начиная с Tarantool 2.6 синхронная репликация доступна [2].

Коннекторы из других языков программирования


И Redis, и Tarantool поддерживают коннекторы для популярных языков программирования:

  • Go;
  • Python;
  • NodeJS;
  • Java.

Полные списки:


Под какие задачи плохо подходят


И Redis, и Tarantool плохо подходят для решения OLAP-задач. Online analytical processing имеет дело с историческими или архивными данными. OLAP характеризуется относительно низким объёмом транзакций. Запросы часто очень сложны и включают агрегацию.

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

Redis и Tarantool однопоточные базы данных, что не позволяет распараллелить аналитические запросы.

Экосистема


Redis

Модули Redis представлены в трёх категориях:

  • Enterprise;
  • проверенные и сертифицированные для Enterprise и Open source;
  • непроверенные.

Enterprise-модули:

  • полнотекстовый поиск;
  • хранение и поиск по bloom-фильтрам;
  • хранение временных рядов.

Сертифицированные:

  • хранение графов и запросы к ним;
  • хранение JSON и запросы к нему;
  • хранение и работа с моделями машинного обучения.

Все модули, отсортированные по количеству звёзд на Github: https://redis.io/modules

Tarantool

Модули представлены в двух категориях:


Чем Redis лучше


  • Проще.
  • В интернете представлено больше информации, 20 тыс. вопросов на Stackoverflow (из них 7 тыс. без ответов).
  • Ниже порог входа.
  • Как следствие, проще найти людей, которые умеют работать с Redis.

Чем Tarantool лучше


  • Русскоязычная бесплатная поддержка в Telegram от разработчиков.
  • Есть вторичные индексы.
  • Есть итерация по индексу.
  • Есть UI для администрирования кластера.
  • Предлагает механику сервера приложений с кооперативной многозадачностью. Эта механика похожа на однопоточный Go.

4. Вывод


Redis это классный продвинутый кеш, но его нельзя брать как основное хранилище. Tarantool это мультипарадигменная база данных, можно брать как основное хранилище. Tarantool поддерживает:

  • Реляционную модель хранения с SQL.
  • Распределённое NoSQL-хранилище.
  • Создание продвинутых кешей.
  • Создание брокера очередей.

У Redis ниже порог входа. У Tarantool выше потолок в production.

Сравнение одной таблицей:

Redis Tarantool
Описание Продвинутый кэш в памяти. Мультипарадигменная СУБД с сервером приложений.
Модель данных Key-value Key-value, документы, реляционная
Сайт redis.io www.tarantool.io
Документация redis.io/%C2%ADdocumentation www.tarantool.io/ru/doc/latest
Разработчик Salvatore Sanfilippo, Redis Labs mail.ru Group
Текущий релиз 6.2 2.6
Лицензия The 3-Clause BSD License The 2-Clause BSD License
Язык реализации C C, C++
Поддерживаемые ОС BSD, Linux, MacOS, Win BSD, Linux, MacOS
Схема данных Key-value Гибкая
Вторичные индексы Нет Есть
Поддержка SQL Нет Для одного инстанса, ANSI SQL
Foreign keys Нет Есть, с помощью SQL
Триггеры Нет Есть
Транзакции Оптимистичные блокировки, атомарное выполнение. ACID, read commited
Масштабирование Шардинг по фиксированному диапазону. Шардинг по настраиваемому количеству виртуальных бакетов.
Многозадачность Да, сериализация сервером. Да, кооперативная многозадачность.
Персистентность Снапшоты и журналирование. Снапшоты и журналирование.
Концепция консистентности Eventual ConsistencyStrong eventual consistency with CRDTs Immediate Consistency
API Проприетарный протокол. Свой открытый бинарный протокол.
Язык скриптов Lua Lua
Поддерживаемые языки C
C#, C++, Clojure, Crystal, D, Dart, Elixir, Erlang, Fancy, Go, Haskell, Haxe, Java, JavaScript (Node.js), Lisp, Lua, MatLab, Objective-C, OCaml, Pascal, Perl, PHP, Prolog, Pure Data, Python, R, Rebol, Ruby, Rust, Scala, Scheme, Smalltalk, Swift, Tcl, Visual Basic
C, C#, C++, Erlang, Go, Java, JavaScript, Lua, Perl, PHP, Python, Rust

5. Ссылки


  1. Архитектура S3: три года эволюции Mail.ru Cloud Storage
  2. Синхронная репликация в Tarantool
  3. Скачать Tarantool можно на официальном сайте, а получить помощь в Telegram-чате.
Подробнее..

Шардинг, от которого невозможно отказаться

22.04.2021 10:12:30 | Автор: admin
image

А не пора ли нам шардить коллекции?
Не-е-е:


  • у нас нет времени, мы пилим фичи!
  • CPU занят всего на 80% на 64 ядерной виртуалке!
  • данных всего 2Tb!
  • наш ежедневный бекап идет как раз 24 часа!

В принципе, для большинства проектов вcё оправдано. Это может быть еще прототип или круг пользователей ограничен Да и не факт, что проект вообще выстрелит.
Откладывать можно сколько угодно, но если проект не просто жив, а еще и растет, то до шардинга он доберется. Одна беда, обычно, бизнес логика не готова к таким "внезапным" вызовам.
А вы закладывали возможность шардинга при проектировании коллекций?


Эта статья для продвинутых разработчиков.
Для тех, кто планирует шардинг своего кластера.
Для тех, кто уже шардировал кластер ранее, но админы все еще плачут.
Для тех, кто руками перемещал jumbo-чанки.


Сначала, мы будем учится жить со слонами.
Потом, мы их победим, но не сможем вернуться назад.


Всем привет, от команды разработки Smartcat и наших счастливых админов!


Мы разберём распространенный сценарий, который обычно мешает нам равномерно распределять данные. И опишем способ ослабления требований к ключу шардирования, который при этом не приведет к деградации производительности нашего кластера.


Зачем нам шардинг


Шардинг штатная возможность горизонтального масштабирования в MongoDB. Но, чтобы стоимость нашего шардинга была линейной, нам надо чтобы балансировщик MongoDB мог:


  • выровнять размер занимаемых данных на шард. Это сумма размера индекса и сжатых данных на диске.
  • выровнять нагрузку по CPU. Его расходуют: поиск по индексу, чтение, запись и агрегации.
  • выровнять размер по update-трафику. Его объем определяет скорость ротации oplog. Это время за которое мы можем: поднять упавший сервер, подключить новую реплику, снять дамп данных.

Конечно, мало толку будет от нового шарда, если на него переместится только пара процентов данных или не дойдет нагрузка на CPU или update-трафик.


Особенности шардинга в MongoDB


Как мы знаем балансировщик MongoDB при отсутствии ограничений (превышение размера данных на шард, нарушение границ зон) выравнивает только количество чанков на шард. Это простой, надежный и сходящийся алгоритм.
Есть документация про лимиты количества чанков и про порядок выбора размещения чанка.
Этот алгоритм и определяет главные ограничения на балансируемые данные:


  1. Ключ шардирования должен быть высокоселективный. В противном случае мы не получим достаточного числа интервалов данных для балансировки.
  2. Данные должны поступать с равномерным распределением на весь интервал значений ключа. Тривиальный пример неудачного ключа это возрастающий int или ObjectId. Все операции по вставке данных будут маршрутизироваться на последний шард (maxKey в качестве верхней границы).

Самое значимое ограничение гранулярность ключа шардирования.
Или, если сформулировать отталкиваясь от данных, на одно значение ключа должно приходиться мало данных. Где "мало" это предельный размер чанка (от 1Mb до 1Gb) и количество документов не превышает вот эту вот величину.


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


Бизнес логика требует слонов


Теперь давайте посмотрим, с чем мы будем сталкиваться при проектировании бизнес логики.
Я рассмотрю только самый чувствительный для шардирования случай, который заставляет наши данные группироваться в неравномерные объемы.
Рассмотрим следующий сценарий:


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

Пример модели:


{    _id: ObjectId("507c7f79bcf86cd7994f6c0e"),    projectId: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),    name: "job name 1",    creation: ISODate("2016-05-18T16:00:00Z"),    payload: "any additional info"}

Как может выглядеть логика выбора ключа шардирования?
Совершенно точно, в качестве первого поля стоит взять projectId. Он есть в большинстве запросов, т.е. бОльшая часть запросов будет направлена роутером на нужный шард.


Теперь, надо выбрать второе поле ключа или оставить только первое.
Например, у нас 20% запросов используют только поле name, еще 20% только поле creation, а остальные опираются на другие поля.
Если в ключ шардирования включить второе поле, то крупные проекты, те у которых объем работ не помещается в одном чанке, будут разделены на несколько чанков. В процессе разделения, высока вероятность, что новый чанк будет отправлен на другой шард и для сбора результатов запроса нам придётся обращаться к нескольким серверам. Если мы выберем name, то до 80% запросов будут выполняться на нескольких шардах, тоже самое с полем creation. Если до шардирования запрос выполнялся на одном сервере, а после шардирования на нескольких, то нам придется дополнительную читающую нагрузку компенсировать дополнительными репликами, что увеличивает стоимость масштабирования.


С другой стороны, если оставить в ключе только первое поле, то у нас "идеальное" линейное разделение нагрузки. Т.е. каждый запрос с полем projectId будет сразу отправлен на единственный шард. Поэтому имеет смысл остановиться на этом выборе.
Какие риски есть у этого выбора:


  • у нас могут появляться не перемещаемые jumbo-чанки. При большом объеме работ в одном проекте мы обязательно превысим предельный размер чанка. Эти чанки не будут разделены балансировщиком.
  • у нас будут появляться пустые чанки. При увеличении проекта, балансировщик будет уменьшать диапазон данных чанка для этого проекта. В пределе, там останется только один идентификатор. Когда большой проект будет удален, то на этот узкий чанк данные больше не попадут.

Оба этих случая несколько искажают KPI штатного балансировщика. В первом случае чанк вообще не перемещается. Во втором, он перемещается, но это не приводит к смещению данных. С течением времени, мы можем получить ситуацию когда число пустых чанков превышает число чанков с данными, а еще часть чанков не перемещаемые.


В общем, мы можем посмотреть текущее распределение данных или просто прикинуть развитие проекта и допустить, что хоть jumbo-чанки и будут появляться, но их должно быть не много.


Проблемы с масштабированием


Итак, мы, вопреки ожиданиям, набрали достаточно неперемещаемых чанков, чтобы это стало заметно. То есть буквально, случай из практики. Вы заказываете админам новый шард за X$ в месяц. По логам видим равномерное распределение чанков, но занимаемое место на диске не превышает половины. С одной стороны весьма странное расходование средств было бы, а с другой стороны возникает вопрос: мы что же, не можем прогнозировать совершенно рутинную операцию по добавлению шарда? Нам совсем не нужно участие разработчика или DBA в этот момент.


Что интересно, на этапе проектирования схемы данных, у нас и нет особого выбора. Мы не можем предсказать характер использования на годы вперед. А владельцы продукта очень не охотно формулируют строгие ограничения при проектировании. Даже оглядываясь назад, не всегда понятно, как можно было бы перепроектироваться, чтобы успеть все сделать тогда. Так что продолжаем работать с тем шардированием, которое заложили.


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


Надеюсь, тут уже все уже запуганы и потеряли надежду. ;)


Решение


Попробуем сначала поправить основную боль администрирования.
Конечно, не хочется заново писать балансировку данных, нам надо просто "попросить" существующий балансировщик перекинуть часть чанков туда, где есть неиспользуемое место. Вариантов у нас два.


1-й воспользоваться командой moveChunk прямое указание балансировщику о перемещении конкретного чанка.


2-й воспользоваться командой addTagRange привязка диапазона значений ключа шардирования к некоторому шарду и их группе.


В обоих случаях потребуется также точное знание размера чанка. Это можно сделать командой dataSize, которая возвращает некоторую статистику по объектам в коллекции на заданном интервале ключей.


Предварительное прототипирование 1-го варианта выявило дополнительные особенности.


Команды moveChunk не отменяют работы штатного балансировщика. Он вполне может принять решение об обратном переносе. Тут надо блокировать работу штатного балансировщика. Или кроме перемещения больших чанков на недогруженный шард, надо искать на нем маленькие чанки и синхронно перемещать их на перегруженный шард.


Данные dataSize вычисляются "долго", и при этом устаревают. На практике получается, что надо просканировать некоторое количество чанков, прежде чем найдешь чанк больше среднего. В идеале, надо искать самые большие перемещаемые чанки, т.е. некоторые варианты распределения данных в коллекции могут нам значительно замедлить сканирование.


Массовые сканирования dataSize ухудшают отзывчивость сервера на боевых запросах.


Ну и остается тупиковая ситуация, когда все чанки кроме jumbo просто равны по размеру. В этом случае перемещать просто нечего, или опять же придется блокировать работу штатного балансировщика.


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


Основная идея проста.
Давайте подберем такие диапазоны ключей на каждам шарде, чтобы в итоге части коллекции занимали одинаковый размер на шардах.


Диапазоны ключей привязываются к тегу, а не к шарду. А сам шард может содержать несколько тегов. Поэтому для упрощения дальнейшего описания и работы условимся, что на каждый шард мы добавляем по одному тегу с названием этого шарда. Это позволит привязывать диапазон ключей на конкретный шард.


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


Группировка данных


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


Итак, коллекция уже шардирована.


  • Читаем все ее чанки из коллекции config.chunks с сортировкой по возрастанию ключа {min: 1}
  • Распределяем чанки по шардам, так чтобы их было примерно одинаковое количество. Но при этом все чанки на одном шарде должны объединяться в один интервал.

Например:


У нас есть три шарда sh0, sh1, sh2 с одноименными тегами.
Мы вычитали поток из 100 чанков по возрастанию в массив


var chunks = db.chunks.find({ ns: "demo.coll"}).sort({ min: 1}).toArray();

Первые 34 чанка будем размещать на sh0
Следующие 33 чанка разместим на sh1
Последние 33 чанка разместим на sh2
У каждого чанка есть поля min и max. По этим полям мы выставим границы.


sh.addTagRange( "demo.coll", {shField: chunks[0].min}, {shField: chunks[33].max}, "sh0");sh.addTagRange( "demo.coll", {shField: chunks[34].min}, {shField: chunks[66].max}, "sh1");sh.addTagRange( "demo.coll", {shField: chunks[67].min}, {shField: chunks[99].max}, "sh2");

Обратите внимание, что поле max совпадает с полем min следующего чанка. А граничные значения, т.е. chunks[0].min и chunks[99].max, всегда будут равны MinKey и MaxKey соответственно.
Т.е. мы покрываем этими зонами все значения ключа шардирования.


Балансировщик начнёт перемещать чанки в указанные диапазоны.
А мы просто ждем окончания работы балансировщика. Т.е. когда все чанки займут свое место назначения. Ну за исключением jumbo-чанков конечно.


Коррекция размера


Продолжим пример выше и возьмем конкретные значения ключей. Первоначальная настройка будет выглядеть так:


sh.addTagRange( "demo.coll", {shField: MinKey}, {shField: 1025}, "sh0");sh.addTagRange( "demo.coll", {shField: 1025}, {shField: 5089}, "sh1");sh.addTagRange( "demo.coll", {shField: 5089}, {shField: MaxKey}, "sh2");

Вот так будет выглядеть размещение чанков:


Командой db.demo.coll.stats() можно получить объем данных, которые хранятся на каждом шарде. По всем шардам можно вычислить среднее значение, к которому мы хотели бы привести каждый шард.


Если шард надо увеличить, то его границы надо перемещать наружу, если уменьшить, то внутрь.
Так как уже явно заданы диапазоны ключей, то балансировщик не будет их перемещать с шарда на шард. Следовательно, мы можем пользоваться командой dataSize, мы точно знаем данные какого шарда мы сканируем.
Например, нам надо увеличить sh0 за счет h1. Границу с ключем будем двигать в бОльшую сторону. Сколько именно данных мы сместим перемещением конкретного 34-го чанка, мы можем узнать командой dataSize с границами этого чанка.


db.runCommand({ dataSize: "demo.coll", keyPattern: { shField: 1 }, min: { shField: 1025 }, max: { shField: 1508 } })

Последовательно сканируя чанки по одному мы мы можем смещать границу до нужного нам размера sh0.


Вот так будет выглядеть смещение границы на один чанк



Вот пример команд для фиксации нового состояния границ в конфигурации кластера.
Мы сначала удаляем пару интервалов со старыми значениями границ, потом заводим новые интервалы с актуальными значениями границ.


sh.removeTagRange( "demo.coll", {shField: MinKey}, {shField: 1025}, "sh0");sh.removeTagRange( "demo.coll", {shField: 1025}, {shField: 5089}, "sh1");sh.addTagRange( "demo.coll", {shField: MinKey}, {shField: 1508}, "sh0");sh.addTagRange( "demo.coll", {shField: 1508}, {shField: 5089}, "sh1");

После применения команд балансировщик самостоятельно переместит 34-й чанк на нужный нам шард. Конечно, нет необходимости менять границы после каждого сканирования чанка. Мы можем учесть полученный эффект, продолжить сканирование и сформировать уже итоговый пакет изменений границ. Таким образом, мы можем выровнять размер коллекции по шардам с точностью до максимального размера чанка.
Конечно, в процессе сканирования нам будут попадаться jumbo-чанки. Но ничего страшного, мы их просто игнорируем и переносим границу через него без учета эффекта от перемещения.
В общем случае, мы можем планировать произвольное распределений данных таких коллекций на шардах кластера.


Выгодные особенности этого подхода:


  • до смещения границ, данные не перемещаются. Мы можем их сканировать без опасения, что эта информация устареет. Ну конечно, исключая работу приложения для которого и заведена эта БД.
  • команда dataSize применяется только к перемещаемым данным. Мы не сканируем лишнего.
  • путь к целевому состоянию непрерывный. Т.е. мы можем проводить частичные коррекции, например если видим, что сканирование идет долго и реальное распределение данных успевает сильно измениться.

Дополнительные возможности


Вообще, на практике требуется выравнивание используемого объема диска на шардах, а не только части шардированных коллекции. Частенько, нет времени или возможности проектировать шардирование вообще всех БД и коллекций. Эти данных лежат на своих primary-shard. Если их объем мал, то его легко учесть при коррекции размера и просто часть данных оттащить на другие шарды.


С течением времени, особенно после частых удалений, будут образовываться пустые чанки. Не то чтобы они теперь сильно мешают, но их может быть сильно больше чанков с данными. Чтобы они не заставляли страдать роутер, было бы хорошо их прибрать. Раньше (до дефрагментации) их надо было найти, переместить к другому чанку-соседу на один шард, потом запустить команду mergeChunks. Перемещение нужно, т.к. команда объединяет только соседние чанки на одном шарде.
Есть даже скрипты для автоматизации, но они долго работают, и есть один тикет с печально низким приоритетом.


Теперь все значительно проще. Соседние чанки и так на одном шарде. Можно объединять хоть весь интервал целиком. Если он конечно без jumbo-чанков, их надо исключить. Есть на интервале пустые чанки или нет, это уже не важно. Балансировщик заново сделает разбиение по мере добавления или изменения данных.


Почти итог


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


Наши плюсы:


  • точная балансировка занимаемого размера
  • лучшее распределение update-трафика
  • минимальное сканирование при коррекциях
  • бесплатная уборка пустых чанков
  • минимальные допуски на запасное/неиспользуемое дисковое пространство на каждом шарде
  • мы всегда можем удалить привязки диапазонов ключей и вернуться к исходному состоянию

Минусы:


  • требуется настройка и сопровождение привязок диапазонов ключей.
  • усложняется процесс добавления нового шарда.

Но это было бы слишком скучно Время победить слонов и не вернуться!


Победа над слонами!


Как мы уже отмечали, единственная причина неперемещаемых чанков недостаточная селективность ключа шардирования. Мы сами снижали селективность ключа для привязки запроса к шарду. Но мы уже сгруппировали данные, а теперь мы можем этим коварно воспользоваться!


Можно увеличить селективность ключа шардирования, если добавить к нему второе поле с достаточной селективностью. Поле может быть любым. Подойдет даже _id.


Вспомним пример модели:


{    _id: ObjectId("507c7f79bcf86cd7994f6c0e"),    projectId: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),    name: "job name 1",    creation: ISODate("2016-05-18T16:00:00Z"),    payload: "any additional info"}

Ранее мы выбрали ключ шардирования {projectId: 1}
Но теперь при проектировании можно выбрать любые уточняющие поля для ключа шардирования:


  • {projectId: 1, name: 1}
  • {projectId: 1, creation: 1}
  • {projectId: 1, _id: 1}

А если вы пользуетесь MongoDB версии 4.4 и выше, то там есть удобная функция уточнение ключа шардирования на существующих данных.


Данные дефрагментированы, а это дает нам гарантии того, что основной объем документов работ с одинаковым projectId будет находиться на одном шарде. Как это выглядит на практике?


Вот иллюстрация примеров размещения работ по чанкам. В случае, если мы выбрали ключ шардирования {projectId: 1, _id: 1}



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


Некоторые проекты будут полностью умещаться в один чанк. Например, проекты 1 и 2 размещены в 1м чанке, а 7-й проект во 2-м.
Некоторые проекты будут размещены в нескольких чанках, но это будут чанки с соседними границами. Например, проект 10 размещен в 3, 4 и 5 чанках, а проект 18 в 6 и 7 чанках.
Если мы будем искать работу по ее полю projectId, но без _id, то как будет выглядеть роутинг запросов?


Планировщик запросов MongoDB отлично справляется с исключением из плана запроса тех шардов, на которых точно нет нужных данных.
Например, поиск по условию {projectId: 10, name: "job1"} будет только на шарде sh0


А если проект разбит границей шарда? Вот как 18-й проект например. Его 6-й чанк находится на шарде sh0, а 7-й чанк находится на шарде sh1.
В этом случае поиск по условию {projectId: 18, name: "job1"} будет только на 2х шардах sh0 и sh1. Если известно, что размер проектов у нас меньше размера шарда, то поиск будет ограничен только этими 2-мя шардами.


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


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


  • группа располагается на одном, максимум двух шардах.
  • число групп которые имели несчастье разместиться на 2х шардах ограничено числом границ. Для N шардов будет N-1 граница.

Больше одно шардовых запросов, меньше лишней нагрузки на чтение, меньше дополнительных реплик!
Мало нам было бы радости, но с таких сильно селективным ключом мы и от jumbo-чанков избавляемся.


И вот теперь, от дефрагментации данных нам уже никуда не деться.


Точно итог


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


Теперь уже можно оценить достижения и потери.
Достижения:


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

Потери:


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

Осталось спроектировать весь процесс дефрагментации, расчета поправок и коррекции границ Ждите!

Подробнее..

Cassandra в Yelp

07.05.2021 10:12:10 | Автор: admin

image


Yelp это крупнейшее в США приложение для заказа еды и услуг. Оно установлено более чем на 30 млн уникальных устройств, в нём зарегистрировано более 5 млн. компаний. Для хранения и доступа к данным в Yelp используют Cassandra. Как и для каких задач применяется эта база данных, на конференции Cassandra Day Russia 2021 рассказал Александр Широков, Database Reliability Engineer в Yelp.


Cassandra это база данных из семейства NoSQL. Согласно полному определению, это distributed wide-column NoSQL datastore. Cassandra оптимизирована для writeов, и consistency запросов можно двигать на очень низком уровне.


Не углубляясь в детали, я расскажу, как мы используем Cassandra в Yelp. У нас это один из самых популярных инструментов. Мы применяем MySQL и Cassandra. Если MySQL все девелоперы знают, то Cassandra нет. Но это то, что мы используем постоянно и на довольно большом масштабе.


Я расскажу:


  1. Как мы автоматизируем изменение схемы: как девелоперы изменяют таблицы, добавляют таблицы или колонки. Если позволить им делать это по своему усмотрению, то получается очень много проблем. Чтобы их избежать, мы этот процесс автоматизировали.
  2. Как разработчик получает доступ к Cassandra. Один из вариантов находить каждый инстанс Cassandra-кластера через host и port, но это решение не масштабируется. Я покажу статистику по нашим кластерам и вы увидите, что на нашем масштабе нужно что-то другое.
  3. Закончу одним из проектов, над которым мы работаем сейчас, и который выглядит довольно многообещающим.

Как используется Cassandra в Yelp


В любой месяц отправляется около 90 млрд запросов, в любой момент времени задеплоено около 60 Kubernetes-кластеров и около 500 подов (один под один инстанс Cassandra).


С точки зрения нагрузки на кластеры, наша самая высокая Read Load около 600 тыс. RPS на один из наших кластеров, Write Load около 400 RPS. В целом довольно высокая нагрузка.


image


В Yelp мы поддерживаем широкий спектр пользовательских кейсов. Мы поддерживаем и аналитический процессинг, и процессинг транзакций. Например, есть какой-то batch-сервис, онлайн или веб-сервис, который отправляет запросы в Cassandra и что-то пишет в Cassandra.


Также наша стриминг-инфраструктура зависит от Cassandra. Плюс мы собираем в Cassandra данные для аналитики, агрегируем какие-то метрики; от Cassandra зависит наша caching-инфраструктура и distributed tracing возможность для разработчиков трейсить жизненный цикл реквеста в Yelp, смотреть, как реквест движется между микросервисами (трейсинг инфраструктуры).


Как вы видите, спектр юзкейсов довольно широкий.


Мы деплоим Cassandra в основном в двух регионах: us-east и us-west.


image


Несмотря на то, что мы деплоим на us-east и us-west, каждый наш кластер называется multi region cluster. Если клиент хочет написать в us-east, а тот недоступен, тогда он напишет в us-west.


В каждом регионе мы деплоим (так как у нас инфраструктура основана на Kubernetes) наш собственный разработанный Kubernetes-оператор. Роль этого оператора мы даём ему кастомные ресурсы смотреть на Cassandra: количество подов, которые должны быть в любой момент времени, или какие-то значения CPU и памяти. Оператор просто сравнивает те значения, которые мы ему даём, с тем, что происходит на самом деле мониторит кластер. И если случается несовпадение, то оператор поднимет ещё один под.


Например, оператору сказали, что нужно пять подов, а сейчас задеплоено только четыре, он поднимет ещё один. Это снимает много работы с нас, как с команды, которая занимается поддержкой инфраструктуры. Синхронизация происходит с помощью etcd. Etcd это простенький distributed key-value store.


Мы деплоим всё на AWS, для хранения самих данных используем EBS volumes Amazon Elastic Block Storage. Вся инфраструктура основана на Kubernetes, плюс мы разработали собственный оператор.


Для подключения клиента к каждому кластеру, каждому инстансу кластера мы используем продукт Smartstack, а в следующей итерации Envoy.


Как в Yelp делают изменение схемы (Schema Changes)


С чего можно начать автоматизировать этот процесс: можно взять все схемы (schema), которые есть, и засунуть в один репозиторий самый простой вариант. И потом на каждой Cassandra node запустить процесс, который будет смотреть на то, что у нас в репозитории и то, что у нас в Cassandra, сравнивать, и если у нас тут есть какая-нибудь схема (например, какая-то таблица), а в кластере Cassandra она не существует, она просто создаст новую таблицу так, чтобы они просто совпадали.
Если, например, несовпадение, так, чтобы мы знали, что происходит, что делает этот процесс, процесс файлит Jira-тикет.


Это то как мы начинали. Но в этом есть несколько проблем: довольно сложно поддерживать Alter Table стейтменты. Сложно понять, куда это вообще, как это поддерживать. Разработчики не знали про это. Каждый раз, когда разработчикам нужно было изменить таблицу, они шли к нам и спрашивали. Это такая проблема, о которой нам нужно было бы рассказывать разработчикам каждый раз. Можно было бы, конечно, записать это куда-то в документацию, но у кого есть время её читать?


Если разработчики создают какую-то схему, они засабмитят в гит-репозиторий, но у них никакого фидбека, нет обратной связи, когда эта схема будет применена на сам кластер, потому что процесс, который запускается на Cassandra, он, естественно, не постоянный, как butch-процесс, который периодически пулит и пытается применить изменения. Также изменения схем были применены в очень рандомном порядке в наших окружениях. То есть у нас есть несколько окружений: development, staging, production, и возможен был такой вариант, когда production была применена ранее, чем development.


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


image


Если нам нужно было ревью каждого изменения схемы, не было процессов, когда мы, как инженеры по Database Reliability, обязательно бы это делали. И в том числе, когда разработчики пишут их SQL-команды, чтобы создать таблицу, например, не было никакой обратной связи с точки зрения ошибок: они написали, но не было возможности им сказать, что, например, вот этот тип колонки не поддерживается, или вы что-то делаете не так.


То есть довольно много проблем с таким процессом. Даже несмотря на то, что он работал изначально, мы открыли такие небольшие сюрпризы. И нам нужно было объяснять это разработчикам каждый раз, что вот это происходит, и если какие-то изменения были закомичены, то очень сложно найти, где была проблема. Нужен был постоянный дебаг что произошло. Если какая-то SQL statement была написана неправильно или неоптимально.


Как всё работает сейчас


Для начала надо дать терминологию. В Yelp используется pushplan это файл, который хранит инструкции по тому, как задеплоить какой-то код. Мы используем это в довольно широком контексте. Например, в контексте любой инфраструктурной работы. Если нам надо изменить часть инфраструктуры, добавить значение в конфиг, то мы обычно пишем pushplan. И эта pushplan-терминология используется и в случае с Cassandra.


Расскажу буквально в паре слов, как процесс выглядит сейчас с точки зрения инженера в Yelp. Если они хотят создать новую таблицу или новый keyspace в Cassandra, они создают папку pushplan и на каждом dev-боксе, который инженер использует, есть utility-функция или команда, которая называется pushplan. Они создают этот pushplan, этот текстовый файл с инструкциями, где они задают буквально пару значений: Jira-тикет, кластер, где они хотят создать, название keyspace, название таблицы и тип базы данных. В итоге эта функция генерирует SQL-файл.


image


Затем они пишут саму SQL-команду. То есть разработчики пишут, например, create table. Когда они пытаются сгенерировать pushplan, мы тут же можем применить так как всё автоматизировано, у нас есть контроль по тому, что мы можем делать с SQL-командами.


image


Разработчики видят, вот прямо сейчас, получают этот фидбек на их команду. Они видят, например, что стринг это не вали
дный тип данных. Рядом с указанием на ошибку мы даём ссылку на информацию о том, как пофиксить эту ошибку.


image


Разработчик исправляет ошибку, генерирует файл снова, теперь всё сработало отлично.


image


Пример файла, который сгенерируется: есть уникальный ID, имя автора, время, версия и сама команда.


image


Дальше разработчики открывают пул-реквест. Кто-то из команды DRE проверяет и говорит, что всё отлично, и мы мёрджим этот коммит.


После того, как мы замёрджили коммит, девелопер получает уведомление в слаке с результатами его pushplan. То есть он может увидеть в реальном времени, что их команда применилась сначала на окружение dev, потом на stage и prod. И видит, когда это случилось. Разработчики также могут запрашивать статус и видеть, на каких окружениях уже применяется и на каких нет. Если где-то ошибка, то могут увидеть, где она произошла. Для каждого кластера можно видеть историю, как изменялась схема всех таблиц.


image


Какие преимущества мы получили:


  • Разработчикам не надо вручную тестировать SQL-команды. Им не нужно переживать, что они не смогут исправить ошибку, если вдруг допустят её. Мы дадим им фидбек сразу, как только они напишут, довольно автоматизированным способом.
  • Мы как DRE-команда можем делиться best practices. Мы можем им сказать, например, что каждая таблица должна содержать как минимум один primary key; не нужно изменять какие-то колонки или что 20 колонок в одном pushplan это не лучшее решение. Мы также можем добавить правила проверки.

Вот так это и работает.


Cassandra для разработчиков


Теперь я немного расскажу, как Cassandra используется разработчиками: как они получают доступ к Cassandra-кластерам или Cassandra-инстансам.


Снова начну с истории. Около семи или восьми лет назад, когда мы начали использовать Cassandra, у нас был один главный web application и буквально несколько сервисов.


image


Потом мы задеплоили несколько Cassandra-кластеров и каждый сервис или каждая команда, которая работала с этим сервисом, должна была искать Cassandra-кластер вручную: искать host-порт, забивать это вручную в конфиг. С другой стороны, им тоже нужно было искать какой-то драйвер, чтобы отправлять запросы на Cassandra, эти драйверы имели разную версию получалась очень беспорядочная ситуация. И как вы видите, нагрузка между кластерами распределялась неравномерно.


image


К чему мы в итоге пришли довольно рано, это мы задеплоили proxy-сервис, который мы назвали Apollo. По сути это очень простой сервис на Python, который находится между сервисами и между Cassandra-кластерами.


image


Эта абстракция позволила нам решить несколько интересных проблем. Во-первых, разработчикам больше не нужно было думать, как подключиться к Cassandra, какие host и port, не искать всё это вручную, не поддерживать драйверы Cassandra. То есть вся сложность инфраструктуры была абстрагирована от разработчиков. Всё что им нужно знать это host и port сервиса Apollo.


image


Мы используем SmartStack, который я уже упоминал ранее, чтобы автоматически найти инстанс Apollo. То есть сервисам даже не нужно искать host и port, им просто нужно сказать: подключись к Apollo, и SmartStack найдёт нужный инстанс.


Это одна проблема, которую мы решили.


Другая решённая проблема это то, что сейчас мы, как команда DRE, могли производить эксперименты над нашим Cassandra-кластером. Мы могли, например, изменить версию драйвера Cassandra, если нам хотелось, или сделать scale up или scale down кластера, в зависимости от нагрузки, которую мы получаем. Это позволило нам равномерно распределить нагрузку на кластеры.


То есть одним proxy-сервисом мы решили несколько проблем.


Также мы могли изолировать клиентов в их собственный Apollo-кластер. Если, например, есть какие-то клиенты или сервисы, юзкейсы которых очень важны для бизнеса, мы просто изолируем их в отдельный кластер.


Ну и то что я сказал: изолирование сложности инфраструктуры от разработчиков это большая победа. Разработчикам стало очень легко работать над их Query-кодом. Нам было очень легко работать над инфраструктурой. Но проблема в том, что за семь лет, которые этот сервис существует, сервис вырос до довольно большого размера.


image


Идея сервиса в том, чтобы разработчики могли там создавать свой код добавлять бизнес-логику И мы заметили буквально недавно, что сервис вырос и сейчас поддерживает где-то около 140 юзкейсов и коннектится практически к 40 кластерам. И этот сервис хранит около 60 тыс. строк только бизнес-логики. И каждый раз, когда мы деплоим, мы раним 60 тысяч строк integration-тестов, что занимает около часа. Масштаб сервиса расширился до сотен инстансов.


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



Например, тут вы видите, что если они хотят написать простую функцию GET_REVIEWS, им нужно писать клиент, им нужно писать стейтменты, вызвать SQL-команду и возвратить результат.


Так же нужно писать unit-тесты, нужно писать swagger spec, потому что каждый клиент это end point или API в Apollo, и надо писать fake data и конфиги чтобы end-to-end тесты работали правильно.


image


image


Мы заметили, что (как и с pushplan) то, что работало вначале, сейчас не работает. Просто потому, что масштаб уже совсем другой. Мы заметили, что нужен какой-то другой, новый подход к решению этой проблемы, но мы не хотели начинать с абсолютного нуля. У Apollo есть некоторые преимущества, которые решали интересные проблемы: например, изоляция сложности в доступе и управлении инфраструктурой. Это важная проблема.


Но с другой стороны, с каким-то новым продуктом, с заменой Apollo, решить несколько проблем:


  1. Увеличить скорость итераций над клиентским кодом. Избавить инженеров от необходимости писать так много кода.
  2. Облегчить доступ к Cassandra-кластеру, ну и в целом модернизировать технологии, которые существовали.

В итоге мы пришли к продукту, который называется Stargate. Это Open Source продукт, который сделан и используется компанией DataStax. Про него можно думать как про API для базы данных. По сути это очень лёгкий слой между слоем приложения и базой данных.


Как это работает


Вы можете думать о Cassandra-кластере, как о коллекции нод, инстансов, организованных в кольцо. Когда приходит запрос от клиента, одна из нод выбирается как координатор, и её задача скоординировать запрос. Выбрать метрики, которые приходят от consistency requirements, от клиента, и отправить результат обратно клиенту.


image


Как это работает со Stargate. Stargate присоединяется к Cassandra-кластеру как координатор нод. И его задача просто скоординировать запрос: найти реплики Но единственное отличие в том, что Stargate не хранит никакие данные. То есть он решает две проблемы: работает как координатор, и он очень лёгкий просто потому, что ему не нужно думать о том, как хранить данные. Мы добавили небольшой proxy поверх Stargate.


image


Ну и вообще, Stargate можно думать об этом слое поверх Cassandra, который работает как координатор, но также предоставляет API для разработчиков. Он предоставляет REST API, можно коннектиться через gRPC, WebSocket или GraphQL.


image


Мы выбрали GraphQL API.


Почему GraphQL


Stargate берёт Cassandra schema все таблицы, которые в key space существуют, и автоматически генерирует GraphQL-запросы. Что это даёт разработчикам: Type-safe API. GraphQL решает очень интересную проблему over и under fetching. То есть клиенты могут очень чётко сказать: нам нужно вот такие колонки возвратить. Например, то что раньше было сложнее сделать с Apollo. И также мы можем запускать несколько Cassandra-запросов в одном запросе. Если нам нужно, например, синхронизировать какие-то таблицы в разных кластерах в одном запросе.


В целом GraphQL становится новым стандартом в API development и используется в больших технологических компаниях. Например, в Facebook (откуда он и произошёл), в Twitter, Github. В Yelp мы тоже довольно давно используем GraphQL.


Что дальше


Расскажу, что мы планируем делать с проектом дальше. Надеюсь, это вам даст немного больше информации о том, как мы делаем какие-то вещи в Yelp. Расскажу, как мы задеплоили MVP (взяли Stargate и просто задеплоили одну ноду) и потом расскажу немного про клиентские библиотеки то, как микросервисы взаимодействуют друг с другом в Yelp.


Тестирование Stargate


Перед тем как начать любую работу по замене Apollo, мы взяли Stargate и подумали: а что если мы задеплоим одну ноду (один node), которая присоединится к Cassandra-кластеру из трёх реплик, и потом запустим тестирование нагрузки запустим 100 тредов, поднимем нагрузку до 3 тыс. RPS и будем ранить это в течение 24 часов. Что тогда произойдёт? А что если потестим парочку сценариев: зарестартим один из Cassandra нодов или зарестартим сам инстанс? Что произойдёт тогда?


На графике видно, что мы проводили этот тест в течение 24 часов и подняли нагрузку примерно до 3 тыс. RPS.


image


Если посмотреть на rage of latencies всех запросов, то они довольно в допустимом пределе. Если посмотреть на высокий хвост (high tail) задержек (99.9+), то он немного spite.


image


Это дало нам несколько интересных инсайтов по тому, как Stargate работает:


  1. Да, память стабильная, CPU повышается вместе с нагрузкой, которую мы даём на кластер. Чтобы немного понизить high tail latencies, мы немного подтюнили JVM, на которой Stargate основан.
  2. В конце мы автоматизировали это, чтобы мы могли ранить это в любой момент времени, как мы девелопим Stargate.

Client libraries


Если так подумать про микросервисы, то какие варианты того, как они могут общаться между собой? Один из вариантов: отправлять друг другу http-запросы, основанные на REST-интерфейсе. Но мы идём немного дальше и пытаемся облегчить этот процесс для разработчиков.


Каждый сервис определяет свой swagger spec каждый endpoint, который экспозится сервисом. Для нашего сервиса есть один API endpoint GraphQL/{keyspace}. То есть разработчики дают по сути keyspace и отправляют их GraphQL-запрос. Что происходит дальше: мы берём swagger spec и генерируем Client Libraries.


image


На другой стороне сервис, который хочет законнектиться в наш сервис, работает с Client Libraries как с пакетом или локальной функцией, несмотря на то, что внизу, внутри этого пакета, на самом деле абстрагируется сложность отправления всех запросов, таймауты, ретраи.


Вот пример, как клиент коннектился бы к нашему Stargate-клиенту. Ему надо просто зарепортить Client Library, вызвать GraphQL Query метод и отправить запрос.


image


Что это даёт:


  1. Мы автоматически можем абстрагировать сложность Service Discovery (обратите внимание, другой сервис нигде не пишет host и port).
  2. Мы имеем тонкий контроль над request budget. Например, говорим, что на какую-то страницу в Yelp максимально можем потратить 2 секунды. Запрос идёт между кучей микросервисов и мы можем сказать, что некие два микросервиса имеют бюджет 100 миллисекунды. Если мы превышаем этот бюджет, то отправлять таймаут-ответ.
  3. Мы можем работать с distributed tracing инженеры могут следить, что происходит с их запросами.
  4. Можем обеспечивать метрики прозрачности: количество запросов, количество запросов, которые отправили ответ со статусом 200 или другим.

GraphQL Playground


Playground это интерактивный development environment, который экспозится. Если у вас бэкенд на GraphQL, вы можете использовать Playground. Stargate делает это out of the box. По сути это такой IDE, которому вы просто говорите /graphqlschema, и вы можете в интерактивном режиме ранить запросы на Cassandra-кластер, быстро итерировать над своим GraphQL Queries.


Что он даёт: автоматически можно экспозить, автоматически открыть все возможные операции, которые можно сделать над этим кластером.


Operability


С точки зрения Operability, когда мы работали над Stargate, то было буквально несколько вещей, которые нам нужно было сделать так, чтобы включить его в нашу Yelp-инфраструктуру. Первое это Distributed Tracing Integration. Второе это то, что нужно заэкспортить какие-то метрики Stargate. Опять же, Load чтобы мы могли в реальном времени наблюдать, что происходит в кластере, смотреть CPU, память


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

Подробнее..

Как быстро загрузить большую таблицу в Apache Ignite через Key-Value API

06.11.2020 10:14:52 | Автор: admin

Некоторое время назад на горизонте возникла и начала набирать популярность платформа Apache Ignite. Вычисления in-memory это скорость, а значит, скорость должна быть обеспечена на всех этапах работы, особенно при загрузке данных.
Под катом находится описание способа быстрой загрузки данных из реляционной таблицы в распределенный кластер Apache Ignite. Описана предобработка SQL query result set на клиентском узле кластера и распределение данных по кластеру с помощью задания map-reduce. Описаны кеши и соответствующие реляционные таблицы, показано, как создать пользовательский объект из строки таблицы и как применить ComputeTaskAdapter для быстрого размещения созданных объектов. Весь код полностью можно увидеть в репозитории FastDataLoad.


История вопроса


Этот текст перевод на русский моего поста в In-Memory Computing Blog на сайте GridGain.
Итак, некая компания решает ускорить медленное приложение путем переноса вычислений в in-memory кластер. Исходные данные для вычислений находятся в MS SQL; результат вычислений нужно положить туда же. Кластер распределенный, поскольку данных много уже сейчас, производительность приложения на пределе и объем данных растет. Заданы жесткие ограничения по времени работы.
Прежде чем писать быстрый код для обработки данных, эти данные нужно быстро загрузить. Неистовый поиск в сети показывает явную нехватку примеров кода, которые можно масштабировать до таблиц размером десятки или сотни миллионов строк. Примеров, которые можно загрузить, скомпилировать и пройти по шагам в отладке. Это с одной стороны.
С другой стороны, документация Apache Ignite / GridGain, вебинары и митапы дают представление о внутреннем устройстве кластера. Методом проб и ошибок удается сделать загрузчик, учитывающий распределение данных по партициям. И когда в один прекрасный день начальство спрашивает "а сыграл ли твой козырный туз?", ответ да, все получилось. Полученный код кажется некой самоделкой с привкусом внутренней архитектуры, но работает с достаточной скоростью.


Данные для загрузки (World Database)


Поскольку данных много, мы будем хранить записи в партиционированном виде и использовать data collocation, чтобы логически связанные значения хранились на одном и том же узле кластера. В качестве источника данных мы будем использовать файл world.sql из дистрибутива Apache Ignite.
Разделим его на три CSV файла в предположении, что каждый из них это результат SQL запроса:



Рассмотрим загрузку записей countryCache из файла country.csv. Ключ countryCache трехсимвольное поле code, тип ключа String, значение объект Country, созданный из остальных полей (name, continent, region).



Наивная загрузка


Поскольку опыта нет, то пляшем от печки будем загружать так же, как в монолитное нераспределенное приложение. Будем создавать пользовательский объект Country для каждой строки таблицы и класть в кеш перед тем, как перейти к следующей строке. Для этого используем библиотеку org.h2.tools.Csv, которая умеет конвертировать файл CSV в java.sql.ResultSet. Эта библиотека уже присутствует на каждом узле Apache Ignite и загружать ее не надо, поскольку подсистема SQL построена на субд H2.


    // define countryCache    IgniteCache<String,Country> cache = ignite.cache("countryCache");    try (ResultSet rs = new Csv().read(csvFileName, null, null)) {     while (rs.next()) {      String code = rs.getString("Code");      String name = rs.getString("Name");      String continent = rs.getString("Continent");      Country country = new Country(code,name,continent);      cache.put(code,country);     }    }

Код замечательно работает для небольших таблиц на кластере из одного узла. Но, если подать таблицу в десять миллионов строк и добавить узлов в кластер, приложение начинает засыпать. Причем с ростом объема данных засыпает оно как-то по экспоненте.
Это все потому, что кеш распределен по узлам кластера и каждое изменение кеша должно быть синхронизировано между всеми узлами. Чтобы загружать быстро, надо учитывать внутреннее устройство кластера.


Попартиционная загрузка


Основа кластера Apache Ignite распределенный кеш ключ-значение. Если объем хранимых данных велик, кеш создается в режиме PARTITIONED и каждая пара ключ-значение хранится в некоторой партиции (partition) на некотором узле кластера. Из соображений отказоустойчивости копия этой партиции может храниться еще и на другом узле; мы здесь для простоты будем считать, что копий нет. Чтобы определить расположение пары ключ-значение, кластер использует affinity function, которая определяет, в какой партиции будет находиться данная пара и на каком физическом узле кластера будет находиться эта партиция.
В нашем примере требуется обработать ResultSet на клиентском узле кластера и распределить данные по серверным узлам. Клиентский узел не хранит данные, поэтому распределение данных гарантированно будет происходить по сети. На рисунке показано взаимодействие клиентского узла с тремя серверными узлами.



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


  • для хранения предзагруженных данных создадим HashMap вида partition_number -> key -> Value
    Map<Integer, Map<String, Country>> result = new HashMap<>();
    
  • для каждой строки данных создадим ключ и с помощью affinity function определим его partition_number. Вместо cache.put() для каждой строки положим пару ключ-значение в раздел HashMap с номером partition_number
    try (ResultSet rs = new Csv().read(csvFileName, null, null)) { while (rs.next()) {  String code = rs.getString("Code");  String name = rs.getString("Name");  String continent = rs.getString("Continent");  Country country = new Country(code,name,continent);  result.computeIfAbsent(affinity.partition(key), k -> new HashMap<>()).put(code,country); }}
    

После группировки используем ComputeTaskAdapter в паре с ComputeJobAdapter. В нашем примере число заданий ComputeJobAdapter совпадает с числом партиций и равно 1024. Каждое задание содержит данные своей партиции и отправляется на тот узел кластера, где хранится партиция.
Результатом выполнения задания ComputeJobAdapter служит число добавленных в кеш записей. По завершении каждое задание возвращает результат на клиентский узел, который вычисляет общий итог.


Compute Task, вид изнутри


Согласно документации, "ComputeTaskAdapter initiates the simplified, in-memory, map-reduce process". Клиентский узел кластера сначала создает задания ComputeJobAdapter и выполняет map определяет, на какой физический узел кластера отправится каждое задание. Затем результаты выполнения заданий возвращаются на клиентский узел и там выполняется reduce вычисление общего числа добавленных записей.


Задание для узла данных (RenewLocalCacheJob)


В нашем примере партиция любого кеша наполняется данными на клиентском узле, заворачивается в объект RenewLocalCacheJob и отправляется по сети на нужный узел кластера. Там созданная партиция размещается в кеше целиком


targetCache.putAll(addend);

После размещения RenewLocalCacheJob печатает partition_number и число добавленных записей.


Задание для клиентского узла (AbstractLoadTask)


Каждое задание загрузки (пакет loader) наследник AbstractLoadTask. Задания отличаются именами извлекаемых полей и типами создаваемых пользовательских объектов. Загруженные данные могут предназначаться для кешей с ключами различных типов (примитивных либо пользовательских), поэтому AbstractLoadTask определен с параметром TargetCacheKeyType. Соответственно и предзагружаемый HashMap определен как


    Map<Integer, Map<TargetCacheKeyType, BinaryObject>> result;

В нашем примере только у countryCache ключ имеет примитивный тип String. Остальные кеши в качестве ключа используют пользовательские объекты. AbstractLoadTask определяет тип ключа параметром TargetCacheKeyType, а значение кеша и вовсе BinaryObject. Это все потому, что составной ключ это пользовательский объект и работать с ним просто так на узлах данных не получается.


Почему BinaryObject вместо пользовательского объекта


Наша цель положить в память узла кластера некоторое количество пользовательских объектов. Мы помним, что узел этот работает не только в другой JVM, но и на другом хосте где-то в сети. На этом узле class definition пользовательских объектов недоступен, он находится в JAR-файле на клиентском узле. Если мы определим кеш с типом Country


    IgniteCache<String, Country> countryCache;

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


Есть два способа преодолеть эту трудность. Первый обеспечить наличие классов на classpath, сам по себе достаточной элегантный:


  • сделать JAR-файл с пользовательскими классами внутри;
  • положить этот файл на classpath на каждом узле кластера;
  • не забывать обновлять этот файл при каждом изменении любого из пользовательских классов;
  • после обновления файла не забывать перезагружать узел.

Второй способ использовать интерфейс BinaryObject для доступа к данным в их исходном (сериализованном) виде. В нашем примере это выглядит так:


  • Кеш определяется в бинарном виде
    IgniteCache<String, BinaryObject> countryCache;    
    
  • Сразу после создания пользовательский объект Country преобразуется в BinaryObject (см. код в LoadCountries.java)
    Country country = new Country(code, name, .. );    BinaryObject binCountry = node.binary().toBinary(country);    
    
  • Созданный бинарный объект помещается в предзагружаемый HashMap, который определяется с типом BinaryObject
    Map<Integer, Map<String, BinaryObject>> result
    

Предзагрузка целиком выполняется на клиентском узле, где есть определения всех пользовательских классов. На удаленные узлы данные отправляются в бинарном виде, определения классов не нужны, ClassNotFoundException не возникает.


Практическая часть. Запуск узлов данных.


Мы будем запускать кластер Apache Ignite в минимально достаточной конфигурации: два узла данных и один клиентский узел.


Узлы данных


Запускаются почти что из коробки с единственным изменением в файле default-config.xml мы разрешаем передавать классы заданий по сети между узлами. Шаги для запуска узла данных:


  • Установить GridGain CE по инструкции Installing Using ZIP Archive. На странице загрузки важно выбрать версию 8.7.10, поскольку код в репозитории FastDataLoad сделан имеенно для нее, а кластер из узлов разных версий собрать не получится;
  • В папке {gridgain}\config открыть файл default-config.xml и добавить в него
    строки
    <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">    <property name="peerClassLoadingEnabled" value="true"/></bean>
    
  • Открыть окно командной строки, перейти в папку {gridgain}\bin и запустить узел командой ignite.bat. В процессе тестирования оба узла кластера могут размещаться на одном хосте; для разработки и запуска на бою надо использовать разные машины;
  • Открыть еще одно окно командной строки, повторить предыдущий шаг. Если в обоих окнах появилась строка наподобие вот этой, то все получилось
    [08:40:04] Topology snapshot [ver=2, locNode=d52b1db3, servers=2, clients=0, state=ACTIVE, CPUs=8, offheap=3.2GB, heap=2.0GB]
    

Важно. Если все же нужно загрузить последнюю версию, например 8.7.25, придется указать номер версии в файле pom.xml


    <gridgain.version>8.7.25</gridgain.version>

Иначе версии узлов данных и клиентского узла не совпадут и клиентский узел не сможет войти в кластер


class org.apache.ignite.spi.IgniteSpiException: Local node and remote node have different version numbers (node will not join, Ignite does not support rolling updates, so versions must be exactly the same) [locBuildVer=8.7.25, rmtBuildVer=8.7.10]

Клиентский узел


Вся работа выполняется клиентским приложением, которое содержит определение кешей, пользовательские объекты и логику map-reduce. Приложение это JAR-файл, который стартует клиентский узел кластера и запускает compute task для загрузки данных. Для демонстрации мы используем один хост Windows, для боевого запуска лучше использовать разные хосты Linux. Шаги для запуска клиентского узла:


  • Клонировать репозиторий FastDataLoad;
  • Перейти в корневой каталог проекта и собрать проект;
    mvn clean package
    
  • Находясь в корневом каталоге, запустить приложение.
    java -jar .\target\fastDataLoad.jar
    

Метод main() класса LoadApp создает пользовательский объект LoaderAgrument с названиями кеша и файла данных и преобразует его в бинарный формат. Далее бинарный объект используется как аргумент map-reduce задания LoadCountries.
LoadCountries создает для каждой партиции задание RenewLocalCacheJob и отправляет его по сети на соответствующий узел данных, где задание выполняется и выводит сообщение с номером обновленной партиции (номера партиций между узлами не пересекаются).


Узел данных #1



Узел данных #2



После этого клиентский узел суммирует возвращенные заданиями результаты и выводит общее число загруженных объектов



Файл country.csv загружен, ключи CountryCode и соответствующие значения собраны в партициях и каждая партиция размещена в памяти своего узла данных. Процесс повторяется для cityCache и countryLanguageCache; клиентское приложение выводит число объектов, время работы и завершается.


Заключение


Пару слов о скорости работы наивной и попартиционной загрузки.


Для больших таблиц наивный способ загрузки за разумное время не заканчивается вовсе. Попартиционный способ используется автором в реальной боевой эксплуатации и демонстрирует приемлемую скорость работы:


  • свойства загружаемой таблицы (SQL Server Management Studio):
    • число строк 44 686 837;
    • объем 1.071 GB;
  • время загрузки и предобработки данных на клиентском узле 0H:1M:35S;
  • время на создание заданий RenewLocalCacheJob и reduce результатов 0H:0M:9S.

На распределение данных по кластеру времени тратится меньше, чем на выполнение SQL-запроса.

Подробнее..
Категории: Nosql , Java , Apache , Apache ignite , High performance

Почтовая система Mailion как нам удалось создать эффективное объектное хранилище для электронной почты

11.12.2020 16:14:18 | Автор: admin

Недавно на Хабре вышли две статьи про новую корпоративную почтовую систему Mailion от МойОфис (1, 2) уникальную российскую разработку, которая отличается беспрецедентными возможностями масштабирования и способна работать в системах с более чем 1 миллионом пользователей.

Несложно подсчитать, что для обслуживания такого числа пользователей потребуется колоссальный объем дискового пространства вплоть до десятков петабайт. При этом почтовая система должна уметь быстро обрабатывать эту информацию и надежно хранить её. Сегодня мы объясним общие принципы организации хранения данных внутри почтовой системы Mailion и расскажем, к каким оптимизациям мы прибегли, чтобы значительно снизить количество операций ввода/вывода и сократить требования к инфраструктуре.


Хабр, привет! Меня зовут Виталий Исаев, я занимаюсь разработкой объектного хранилища почтовой системы Mailion. Этой статьёй мы открываем цикл публикаций, посвящённых архитектуре нашего хранилища, его масштабированию, отказоустойчивости и устройству его внутренних подсистем.

Немного истории

Структура электронного письма формировалась в течение последних пятидесяти лет. Этапы её развития зафиксированы несколькими стандартами. Первое электронное письмо было отправлено между компьютерами системы ARPANET ещё в 1971 г. Но разработчикам стандартов потребовалось ещё 11 лет на разработку RFC 822, который стал прообразом современного представления электронной почты. В нём был описан общий коммуникационный фреймворк текстовых сообщений. В это время электронное письмо рассматривалось просто как набор текстовых строк, а передача структурированных данных (изображений, видео, звука) не подразумевалась и никак не регламентировалась.

Примерно в середине 1990-х родилась концепция специальных расширений MIME (RFC 2045, RFC 2046, RFC 2047), которые позволили включать в состав электронного письма бинарные форматы данных в закодированном виде.

В 2001 году новый стандарт RFC 2822 отменил некоторые устаревшие положения RFC 822, и ввёл понятие частей сообщения message parts, или, проще говоря, "партов". А благодаря MIME появилась возможность создавать multipart-письма, которые могли бы состоять из множества частей различных типов. Наиболее актуальным стандартом электронной почты является RFC 5322, он был разработан в 2008 году.

Структура электронного письма

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

Когда письма поступают в Mailion по любому из транспортных каналов (SMTP, IMAP, API), они не сохраняются как единое целое, а разбиваются на составные части. В Mailion мы извлекаем информацию о структуре письма и некоторые другие служебные данные, и затем сохраняем их в хранилище метаинформации. "Парты" писем, которые составляют основной объем данных, направляются в объектное хранилище.

Рис. 1. Разбиение электронного письма на фрагменты и сохранение по частям в хранилище метаданных и объектном хранилище.

Поток данных, который проходит через почтовую систему, отличается следующими важными особенностями:

  • Письма часто пишутся и ещё чаще читаются. Работа пользователей с почтовой системой порождает огромное количество операций ввода-вывода в хранилище.

  • Письма иммутабельны однажды отправленное или полученное письмо хранится в почтовой системе в неизменном виде на протяжении всего своего жизненного цикла. Черновики писем могут редактироваться, но сами письма никогда.

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

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

Дедупликация

После детального анализа особенностей потока данных мы сформулировали основной принцип проектирования хранилища: оно должно эффективно проводить дедупликацию почтовых сообщений. Правильная работа с дубликатами позволяет экономить не только дисковое пространство, но и операции ввода-вывода за счёт кэширования наиболее часто встречающихся фрагментов данных. Недорогие, но ёмкие HDD-накопители не отличаются высокой пропускной способностью, их показатели IOPS часто являются лимитирующим фактором для производительности всей системы. Благодаря дедупликации данных увеличивается не только экономическая эффективность хранилища, но и его производительность.

Пример расчета ресурсов системы хранения

Представьте, что вам необходимо выполнить массовую рассылку письма по компании с 1000 сотрудников. В подписи отправителя письма находится картинка с логотипом компании, её размер составляет 256 Кб. Давайте оценим затраты ресурсов на чтение рассылки всеми сотрудниками компаниями.

В классических почтовых системах mbox-формата, в которых копии писем разных пользователей хранятся в разных файлах, данная операция будет порождать поток произвольных чтений (random read) с диска. Стандартные HDD в этом режиме работы выдают производительность около 100 IOPS. При размере блока в 4 Кб чтение картинки в худшем случае, когда блоки файла при записи не были размещены последовательно, потребует 64 операции чтения. Следовательно, для чтения всех писем с картинками будет необходимо выполнить 64000 операции (64 операции * 1000 пользователей). Для того, чтобы обработать данную операцию за 1 секунду, потребуется 640 HDD одновременно. Если же в системе присутствует только один накопитель, то он будет работать со 100% утилизацией более 10 минут.

Дедупликация позволяет обойтись лишь однократной загрузкой картинки с диска, после чего она попадёт в кеш и при дальнейших запросах будет отдаваться напрямую оттуда. Экономическая эффективность системы с дедупликацией многократно превышает эффективность системы с классической архитектурой.

Накладные расходы дедупликации

Безусловно, дедупликация не бесплатна и сильно усложняет процесс записи и чтения, налагает дополнительные расходы на CPU и RAM. Однако иммутабельность данных частично компенсирует этот рост сложности. Благодаря неизменности писем, можно не поддерживать операцию записи по смещению (при необходимости его можно реализовать по принципу copy-on-write) и сохранять интерфейс хранилища максимально простым. На данный момент он состоит из записи, чтения, удаления данных и ещё пары вспомогательных методов.

Известные решения с дедупликацией

На момент старта проекта Mailion ни в одном из известных нам объектных хранилищ с открытым кодом подобные оптимизации не были реализованы. Например, в Minio отказались от реализации дедупликации ещё в 2017 г. Попытки доработки других готовых решений, таких, как Elliptics, Riak или Swift, при очевидных и значительных трудозатратах также не гарантировали успеха. ZFS в течение долгих лет не поддерживалась в Linux и по причине лицензионных разногласий официально не может поставляться в дистрибутивах, отличных от Oracle, да и к производительности дедупликации в этой файловой системе есть вопросы.

В Ceph дедупликация появилась в 2019 г., но Ceph имеет репутацию сложного для эксплуатации хранилища с большим количеством историй неуспеха. Многим известны истории отказа Ceph в Росреестре и DigitalOcean и даже потеря бизнеса компанией DataMouse. В багтрекере Ceph можно найти большое количество тикетов с зависаниями, сегфолтами и другими ошибками. Причем некоторые баги, даже в статусе Immediate, могут не устраняться по полгода и больше. Это наводит на мысли о том, что Ceph дорог не только с точки зрения эксплуатации, но и с точки зрения разработки: ресурсы разработчиков будут уходить на стабилизацию самого Ceph, а не на разработку собственного продукта.

Что такое DOS и как это работает

Мы убедились в отсутствии подходящих для наших задач продуктов с открытым исходным кодом, и решили разработать собственное хранилище с поддержкой дедупликации в самом ядре системы. Мы вдохновились программной статьей аналитиков из компании Storage Switzerland, в которой кратко описываются принципы проектирования современных программно-ориентированных хранилищ, и решили назвать наш продукт Dispersed Object Store (DOS). Код DOS написан на языках Go (95%), C и C++ (5%).

Модель данных и метаданных

Иерархия сущностей DOS включает в себя четыре уровня:

  • документы;

  • версии документов;

  • чанки (chunks);

  • сегменты.

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

Каждый документ со стороны публичного API адресуется идентификатором, который состоит из пространства имён (namespace) и собственно ключа (key). В рамках одного пространства имён все ключи являются уникальными. Непосредственно в хранилище из внешнего идентификатора и с помощью криптографической хеш-функции генерируется внутренний ключ (storekey), который обладает свойством глобальной уникальности. Во внутренних API все документы адресуются уже через storekey.

В DOS применяется стриминговая запись. В рамках обработки одного запроса на запись поток данных, который поступает на сервер, частично буферизуется в оперативной памяти и только по накоплению определенного объёма сохраняется в персистентном хранилище. Полной буферизации крупных файлов не происходит никогда, поскольку это привело бы к чрезмерным расходам RAM. Частичная буферизация данных (но не только она) предопределяет в модели данных DOS появление чанков крупных байтовых последовательностей. Каждая версия документа может состоять из одного или нескольких чанков.

Чанки, в свою очередь, с помощью специальных алгоритмов разделяются на сегменты более короткие байтовые последовательности. Они выступают в роли элементарных единиц хранения в DOS. У сегментов тоже есть свои идентификаторы, ими являются хеши от их содержимого. В каком-то смысле, внутренним адресом данных в DOS являются сами данные (эта идея не нова и используется, например, в P2P-файловом хранилище IPFS).

Все документы, версии, чанки и сегменты в совокупности формируют внутренние метаданные DOS. Для персистентного хранения метаданных (документов и сегментов) вводятся индексы. В качестве хранилища индексов используется широко известная встраиваемая key-value база данных RocksDB. Индексы с метаданными должны размещаться на быстрых SSD дисках.

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

Рис. 2. Иерархия внутренних сущностей DOS.

Дедупликация в DOS

В нашей системе дедупликация реализована на двух разных уровнях: на уровне сегментов (блочная дедупликация); на уровне версий документов.

Каждый из этих подходов имеет свои плюсы и минусы. Дедупликация на уровне сегментов достигается за счёт совместного использования одних и тех же сегментов различными версиями документов. Каждый сегмент поддерживает внутри себя счётчик ссылок (reference counter) со стороны версий документов. В момент записи новой версии документа поток входящих данных разбивается на сегменты. Для каждого из них по индексу выполняется проверка существования. Если подобный сегмент встречается впервые, то выполняется его запись в бэкенд. Если же сегмент уже сохранялся ранее, то он просто получает новую ссылку со стороны только что созданной версии документа. Тем самым обеспечивается хранение каждого сегмента в единственном экземпляре.

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

Однако есть и минусы: существенные накладные расходы на поддержание счётчиков ссылок. Например, одна ссылка потребует от 8 до 40 байт, поэтому для очень популярных сегментов с сотнями тысяч ссылок такие счётчики могут достигать размера в несколько десятков мегабайт. Индекс сегментов раздувается, тяжёлые сегменты становятся слишком большими для пересылки по сети, в результате начинает возрастать время отклика. Для решения этой проблемы приходится вводить сложную систему шардирования счётчика ссылок.

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

Второй способ дедупликации несёт значительно меньше накладных расходов, но требует более глубокого вовлечения клиента. Внутрь версии документа встроена очень компактная структура счётчика копий (copy counter). Клиенту DOS предоставляются методы для проверки наличия версии документа и модификации её счётчика копий. Если бизнес-логика клиента позволяет выявить ранее записанную версию, то её повторная запись сводится лишь к дешёвой операции инкремента счётчика копий. В почтовой системе именно так реализовано сохранение партов письма: клиент использует хеш от содержимого парта для построения идентификатора документа, проверяет наличие документа с таким идентификатором и при его наличии просто увеличивает счётчик копий нужной версии на единицу. Иммутабельность данных версии документа даёт гарантию того, что все накопленные инкременты счётчика копий версии относились к одним и тем же данным.

Дедупликация на уровне версий документов фактически обеспечивает ещё один (очень экономичный) вариант записи, при котором оптимизируется не только дисковое пространство и операции ввода-вывода, но ещё и CPU и RAM, а также снижается нагрузка на сеть за счёт облегчения запросов.

Рис. 3. Два уровня дедупликации данных в DOS.

Отказоустойчивость

Если сегмент существует в единственном экземпляре, то что произойдёт в случае его потери? Если диск, на котором хранится популярный сегмент выйдет из строя, будет ли это означать невозможность чтения всех ссылающихся на него версий?

Для борьбы с этой проблемой в хранилищах самых разных классов (компакт-диски, RAID-массивы, промышленные СХД) традиционно используются коды коррекции ошибок. Наибольшую популярность получили коды Рида-Соломона (Подробнее в статьях на Хабре: 1, 2, 3).

В DOS во время записи чанк разбивается на заданное количество сегментов равного размера (data segments, d). С помощью кодирования Рида-Соломона из сегментов данных генерируются избыточные сегменты (parity segments, p). При потере любые p из d + p сегментов могут быть восстановлены из имеющихся d с помощью обратной операции. Таким образом, отказоустойчивость приобретается путём дополнительных расходов дискового пространства. Конкретные значения параметров определяются на этапах внедрения и эксплуатации системы. Исходя из потребностей в отказоустойчивости инсталляции, пользователи могут выбирать между стоимостью хранения и возможностью восстановления (часто встречаются комбинации d + p = 2 + 1 и d + p = 5 + 2).

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

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

Конечно, коды Рида-Соломона могут спасти далеко не всегда. Поэтому в DOS имплементирован ещё один способ восстановления данных: при запросе на инкремент счётчика копий клиент может получить ответ о том, что версия была повреждена и что надо заново записать точно такую же. В этом случае вновь записанная версия восстановит старые поврежденные.

Отдельного рассмотрения требует тема отказоустойчивости метаданных. Мы вернёмся к ней позже в публикациях, посвящённых кластеру DOS.

Конвейерная обработка данных

Во время обработки запроса на запись DOS буферизует первые несколько килобайт из входящего потока и отправляет их в библиотеку сигнатурного анализа файлов. Оттуда хранилище получает вычисленный MIME-тип данных. В контексте DOS все возможные MIME-типы подразделяются на группы форматов. Для каждой из групп в DOS организован отдельный конвейер преобразований (pipeline) и разные оптимизации.

Текстовые данные хорошо сжимаются. Для некоторых текстов с помощью алгоритмов ZSTD, Snappy, Brotli удаётся добиться более чем двукратного сжатия. Это существенная экономия дискового пространства и операций ввода-вывода, и она стоит дополнительных затрат CPU на компрессию и декомпрессию.

Для текстовых данных также можно повысить вероятность обнаружения дубликатов с помощью алгоритма content-defined chunking (CDC). Сущность алгоритма заключается в вычислении хеша Рабина в небольшом окне, скользящем по входящему потоку данных. В позициях, на которых вычисленный хеш принимает определённое значение, ставится граница между чанками. В результате поток разбивается на небольшие чанки разных размеров, однако при этом повышается вероятность обнаружения аналогичных чанков в других версиях с частично совпадающим содержимым.

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

Рис. 4. Различия в конвейерной обработке бинарных и текстовых данных в DOS.

Таким образом, конвейер для бинарных данных настроен на повышение пропускной способности сервера и на снижение времени отклика. А конвейер текстовых данных оптимизирует уровень потребления дискового пространства и IOPS ценой повышенного потребления CPU. Насколько же эффективны эти оптимизации?

Производительность конвейерной обработки данных

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

Параметры эксперимента

В систему сохраняется 10000 писем. Письма состоят только из текста и не включают в себя какие-либо бинарные объекты. Медианный размер письма около 500 Кб. При этом 70% писем имеют 50% совпадающих данных, остальные полностью уникальны. Полностью эквивалентные письма отсутствуют, поскольку они дедуплицировались бы на уровне счётчиков копий и сильно завысили бы итоговый результат. Настройки кодирования Рида-Соломона d + p = 2 + 1, избыточность данных составляет 50%.

Результат эксперимента

При прохождении через текстовый конвейер объем данных изменяется следующим образом: Входящий поток разделяется на чанки и компрессируется со средним коэффициентом сжатия 2.3. Затем кодирование Рида-Соломона увеличивает количество данных в 1.5 раза. Дедупликация cегментов (блочная дедупликация) снижает полученный с учётом избыточности объем данных в 1.17 раза.

Итоговое отношение записанных данных к хранимым данным составляет 2.3 * 1.17 / 1.5 = 1.79. При этом в нашей модели количество метаданных составляет 0.2% от количества данных, однако этот показатель очень вариативен и сильно зависит как от настроек конвейера, так и от характера и количества самих данных.

Удаление данных

В условиях совместного владения сегментами удаление версии документа становится нетривиальной операцией. Её нужно проводить очень аккуратно, чтобы, с одной стороны, не удалить данные, которые ещё кому-то нужны, а с другой не допустить утечек дискового пространства. Связи между всеми сущностями в DOS можно представить в виде большого ациклического ориентированного графа, в котором начальными вершинами являются документы, а конечными вершинами - сегменты. Также есть и промежуточный уровень версий документов. Для каждой из сущностей действуют свои правила удаления: Документ удаляется, если не содержит версий. Версия документа удаляется, если её счетчик копий равен нулю либо истекло время её жизни (TTL). Сегмент удаляется, если не имеет ссылок со стороны версий документов.

Клиент напрямую работает лишь с версиями документов; доступный клиенту метод удаления позволяет декрементировать счётчик копий у конкретной версии. Далее в действие приходят два асинхронных сборщика мусора (GC), один из которых проходит по индексу документов, а другой по индексу сегментов. GC документов удаляет версии с обнулённым счётчиком копий или истекшим TTL, собирает пустые документы и очищает счётчики ссылок сегментов от идентификаторов удаляемых версий. GC сегментов удаляет сегменты с пустыми счётчиками ссылок. Оба GC запускаются в фоновом режиме в периоды, свободные от пользовательской нагрузки.

Рис. 5. Иллюстрация работы сборщиков мусора (1 - граф связей в исходном состоянии; 2 - клиент декрементирует до нуля счётчик копий у одной из версий; 3 - GC документов удаляет версию и владеющий ей документ, GC сегментов удаляет сегмент без ссылок; 4 - граф связей в итоговом состоянии).

Заключение

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

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

Подробнее..

Сети данных

23.12.2020 08:22:02 | Автор: admin

Эта статья о следующем эволюционном шаге в развитии систем обработки данных. Тема амбициозная, поэтому расскажу сначала немного о себе. Вот уже больше 10 лет я работаю над проектами в области CRDT и синхронизации данных. За это время успел поработать на университеты, стартапы YCombinator и известные международные компании. Мои проект последние три года Replicated Object Notation, новыи формат представления данных, сочетающии возможности объектнои нотации (как JSON или YAML), сетевого протокола и оплога/бинлога. Вы могли слышать про другие проекты, работающие в том же направлении, например, Datanet, Automerge и другие. Также вы могли читать Local-first software, это наиболее полныи манифест данного направления Computer Science. Авторы - замечательный коллектив Ink&Switch, включая широко нам известного по "Книге с Кабанчиком" М.Клеппманна. Или вы, возможно, слушали мои выступления по этои теме на различных конференциях.

Идеи этои статьи перекликаются с тем, что пишет последние годы Pat Helland: Immutability Changes Everything и др. Они смежны с проектами IPFS и DAT, к которым я имею отношение.

Итак. Классические БД выстроены на линеином логе операции (WAL). От этого лога выстроены транзакции, от него же выстроена репликация master-slave. Теория репликации с линеиным логом написана еще в начале 1980-х с участием небезызвестного Л. Лампорта. В классических legacy системах с однои большои центральнои базои данных все это работает хорошо. Так работают Oracle, Postresql, MySQL, DB2 и прочие классические SQL БД. Так работают и многие key-value БД, например, LevelDB/RocksDB.

Но линеаризация не масштабируется. Когда система становится распределённой, все это начинает ломаться. Образно говоря, линеиная система это что-то вроде греческои фаланги. Нужно, чтобы все шли ровно, а для этого хорошо, чтобы земля была везде ровнои. Так получается не всегда: где-то электричество отключили, а где-то сеть медленная. Хотя в системе Google Spanner и было показано, что с достаточно большим бюджетом землю можно сделать ровнои абсолютно везде, мы все же отметим, что Google тоже бывает отключается целиком по совершенно смешным причинам.

Из россиян этот тезис в своих докладах развивал Бартунов. Чтобы такую систему отмасштабировать, добавляется асинхронныи механизм связи. Очередь сообщений, например, на сегодняшний день является типовым решением. Или же промежуточным вариантом, когда в центр ставится линеиная Kafka в качестве мастера.

Из популярных систем заметным исключением является Cassandra. За счет отката к last-write-wins консистентности она может работать совершенно анархически, то есть без учёта порядка записи. Это прекрасно подходит для хранения слабоструктурированных данных, например, адресных книжек с аифонов. Apple очень любит Cassandra.

Но между линеаризациеи и полнои анархиеи есть один интересныи уровень: Causal consistency, или причинно-следственная целостность. Это примерно то же, что и happened-before в Computer Science или геометрия Минковского в физике. Это и конус прошлого, и конус будущего, и транзитивность причинно-следственных связеи. Это максимально строгая модель, которая еще согласуется с физикои распределенных систем. В отличие от линеаризации, она допускает наличие параллельных, не влияющих друг на друга событии. В то же время она предусматривает и строгии порядок там, где он имеет смысл.

Единственное, что теория баз данных долго игнорировала этот замечательныи уровень консистентности, и в полнои мере его возможности стали раскрываться только с появлением CRDT (Conflict-Free Replicated Data Types). Эта теория подразумевает, что каждая структура данных существует во множестве реплик. Связь между репликами есть не всегда, поэтому изменения иногда приходится мержить. В отличие от git, CRDT структуры данных всегда можно помержить автоматически. В остальном же, git - очень хороший пример для объяснения свойств CRDT хранилищ:

1. Они также хранят всю историю (возможен аудит),
2. данные становится очень сложно потерять, и
3. появляется возможность работать с данными локально и мержить результат.

RON это нотация, максимально удобная для представления CRDT объектов. В RON есть 4 типа атомов: UUID, INT, STRING, FLOAT. Набор атомов называется RON tuple. А RON операция это tuple с парой UUID метаданных. В этой паре, один UUID это собственный идентификатор операции, а второй указывает на другую, ранее созданную операцию. Благодаря этим айдишникам и ссылкам, из операций можно собирать любые структуры данных. Примерно как из кусочков LEGO, если бы они ещё были пронумерованы, чтобы точно ничего не перепуталось.

Продолжая аналогию с git, в RON сама концепция бранчеи/веток обобщается до такои степени, что уже практически все состоит из веток. Сам RON построен на операциях, хотя и притворяется объектнои нотациеи изо всех сил. Поэтому в основе любая реплика это лог операции, как и в обычнои БД. Однако этот лог упорядочен частично по happened-before, и между разными репликами порядок операции может отличаться. C некоторои точки лога мы можем ответвить новую версию, новыи бранч. Точно так же мы можем привить другую ветку к нашеи (смержиться). В этои схеме и база данных, и бранч выглядят одинаково, как ветки. И одинаково мы их можем мержить. Будь это другая версия данных или же другои набор данных это все равно ветки оплога. Например, если в нашу БД включен справочник "курс валют", это будет отдельная маленькая ветка, которую мы постоянно подмерживаем в нашу базу.

Каждая ветка это частично упорядоченное множество операции. Над этими множествами мы можем производить все обычные операции. Merge это объединение; общии предок это пересечение; diff это XOR. Получается, что БД может выглядеть, как матрешка, ведь возможна вложенность. БД может быть сведением разных БД или поправками (бранчем) другои БД. Или все это вместе. Связь между репликами, что важно, не теряется, т. е. все можно мержить обратно в оригинал при желании. Алгебраично!

Единственная проблема сделать, чтобы это все достаточно быстро работало и не занимало много места. В частности, для этого у RON помимо текстовои формы есть более эффективные бинарные варианты RONv и RONp. Но и БД не ограничивается оплогом: она на нем строится, как дом на фундаменте.

А теперь главныи вопрос зачем? Зачем разрабатывать новые БД на новых принципах, когда старые вроде бы нормально работают?

Во-первых, сети данных обеспечивают связность без централизации. Поясню на примере медицинских БД. Допустим, вам не повезло прокатиться на скорои в Петербурге, а ваши медицинские документы в Екатеринбурге. Такое случается. Хотелось бы, конечно, чтобы документы синхронизировались в реальном времени и теперь, и потом, когда вы вернетесь в Екатеринбург.

Конечно, тут можно создать единую центральную медицинскую БД имени депутата Яровои. Но что делать, когда произоидет какои-нибудь сбои? Останавливать медицину? Когда Google или Amazon уходят в оффлаин, много чего перестает работать. И потом, центральная БД безусловно однажды утечет в паблик. Конечно, интересно почитать, как Алексея Навального трижды отравили химическим оружием агенты ФСБ. Но я не настолько бессмертныи и хотел бы, чтобы в здравоохранении работали более надежные и безопасные информационные системы.

Второи аспект это локальная доступность. Даже Google или Amazon уходят порои в оффлаин. Если же данные находятся в локальнои сети или непосредственно на устроистве, то они перестанут быть доступны только при поломке сети и устроиства. А тогда все равно все работать перестанет, как ни крути. Также синхронизируемая реплика на устроистве будет работать и в поле, и в таиге, и черти где в промзоне. Это актуально для промышленных применении.

Третии аспект коллаборативность. Благодаря автоматическому мержу CRDT является на сегодня наиболее удобнои технологиеи для реализации коллаборативных приложении. В эпоху коронавируса это более чем актуально!

Четвертыи аспект это разумные требования консистентности. Анархия в стиле Cassandra это, конечно, перебор. Но и линеиность лога даже в финансовых системах имеет ограниченную ценность. Как показано в исследовании ACIDRain, в реальных ACID/SQL системах линеаризация сегодня используется в качестве фетиша. Фактически, используемые настроики изоляции транзакции позволяют различные эксплуатируемые аномалии, и фактически безопасность реализуется другими методами. А если бизнес-логика говорит с БД по RPC, то тут уж вовсе говорить не о чем.

Пятыи аспект это целостность данных. Если мы задумаемся, как гарантируется целостность данных в классическом блокчеине, мы поимем, что это простое голосование (по PoW или PoS). В сетях данных реализуемы глобальная перекрестная подпись и массивное якорение. Это как в git, но только глобально. Эти инструменты гораздо более могучи, чем PoW. На сегодняшний день сравнимая степень защиты есть разве только у ядра Linux. Если задуматься, то в эпоху deep fake вообще может стать невозможным отличить реальность от иллюзии без помощи криптографии. Мы стали слишком зависимы от электронных средств коммуникации. Но это утверждение, конечно, заслуживает отдельнои статьи.

Подытожу.

В современном мире разных баз данных не много, а очень много, и мы критически от них зависим. RON позволяет объединить данные в единую живую сеть, не стаскивая их в единыи датацентр и не создавая единои точки отказа (SPoF). Это основные преимущества централизации, без характерных сопутствующих недостатков.

Хранение и передача, БД и сеть это две стороны однои медали, и нам нужна вся медаль, а не стороны.

Если вас заинтересовала эта тема, следите за проектом RON (ru, en), там скоро релиз документации новои версии. Что интересно, документация готовится в системе контроля версии DaRWiN, которая работает на оплоге RON.

Подробнее..

Кластеризация и классификация больших Текстовых данных с помощью М.О. на Java. Статья 3 АрхитектураРезультаты

24.01.2021 14:08:22 | Автор: admin

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

Статья описывает архитектуру системы, алгоритма, а также визуальные результаты. Все детали теории и алгоритмов вы найдете в первых двух статьей.

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

Алгоритм программного обеспечение для машинного обучение состоит из 3 основных частей:

  1. обработка естественного языка;

    1. токенизация;

    2. лемматизация;

    3. стоп-листинг;

    4. частота слов;

  2. методы кластеризации ;

    1. TF-IDF ;

    2. SVD;

    3. нахождение кластерных групп;

  3. методы классификации Aylien API.

Обработка естественного языка

Алгоритм начинается с чтение любых текстовых данных. Так как система у нас электронная библиотеку, то и книги в основном в формате pdf. Реализация и детали обработки NLP можно почитать тут.

Ниже приводим сравнение при запуске алгоритмов Лемматизации и Стеммитизации:

Общее количество слов: 4173415Количество слов после приминение Лемматизации: 88547Количество слов после приминение Стеммитизации: 82294

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

characterize, design, space, render, robot, face, alisa, kalegina, university, washington, seattle, washington, grace, schroeder, university, washington, seattle, washington, aidan, allchin, lakeside, also, il, school, seattle, washington, keara, berlin, macalester, college, saint, paul, minnesota, kearaberlingmailcom, maya, cakmak, university, washington, seattle, washington, abstract, face, critical, establish, agency, social, robot, building, expressive, mechanical, face, costly, difficult, robot, build, year, face, ren, der, screen, great, flexibility, robot, face, open, design, space, tablish, robot, character, perceive, property, despite, prevalence, robot, render, face, systematic, exploration, design, space, work, aim, fill, gap, conduct, survey, identify, robot, render, face, code, term, property, statistics

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

character, design, space, render, robot, face, alisa, kalegina, univers, washington, seattl, washington, grace, schroeder, univers, washington, seattl, washington, grsuwedu, aidan, allchin, lakesid, also, il, school, seattl, washington, keara, berlin, macalest, colleg, saint, paul, minnesota, kearaberlingmailcom, maya, cakmak, univers, washington, seattl, washington, abstract, face, critic, establish, agenc, social, robot, build, express, mechan, face, cost, difficult, mani, robot, built, year, face, ren, dere, screen, great, flexibl, robot, face, open, design, space, tablish, robot, charact, perceiv, properti, despit, preval, robot, render, face, systemat, explor, design, space, work, aim, fill, gap, conduct, survey, identifi, robot, render, face, code, term, properti, statist, common, pattern, observ, data, set, face, conduct, survey, understand, peopl, percep, tion, render, robot, face, identifi, impact, differ, face, featur, survey, result, indic, prefer, vari, level, realism, detail, robot, facecharacter, design, space, render, robot, face, alisa, kalegina, univers, washington, seattl, washington, grace, schroeder, univers, washington, seattl, washington, grsuwedu, aidan, allchin, lakesid, also, il, school, seattl, washington, keara, berlin, macalest, colleg, saint, paul, minnesota, kearaberlingmailcom, maya, cakmak, univers, washington, seattl, washington, abstract, face, critic, establish, agenc, social, robot, build, express, mechan, face, cost, difficult, mani, robot, built, year, face, ren, dere, screen, great, flexibl, robot, face, open, design, space, tablish, robot, charact, perceiv, properti, despit, preval, robot, render, face, systemat, explor, design, space, work, aim, fill, gap, conduct, survey, identifi, robot, render, face, code, term, properti, statist, common, pattern, observ, data, set, face, conduct, survey, understand, peopl, percep, tion, render, robot, face, identifi, impact, differ, face, featur, survey, result, indic, prefer, vari, level, realism, detail, robot, face

Методы кластеризации

Для применения алгоритма tf-idf нужно подсчитать сколько раз слово встречается в каждом документе. Можно использовать HashMap, где ключ - слово, значение - кол-во.

После этого нужно построит матрицу документы-слова:

Далее по формуле вычисляем tf-idf:

Следующий этап, использование метода сингулярного разложение, где на вход приходит результат tf-idf. Пример выходных данных алгоритма сингулярного разложение:

-0.0031139399383999997 0.023330604746 -1.3650204652799997E-4-0.038380206566 0.00104373247064 0.056140327901-0.006980774822399999 0.073057418689 -0.0035209342337999996-0.0047152503238 0.0017397257449 0.024816828582999998-0.005195951771999999 0.03189764447 -5.9991080912E-4-0.008568593700999999 0.114337675179 -0.0088221197958-0.00337365927 0.022604474721999997 -1.1457816390099999E-4-0.03938283525 -0.0012682796482399999 0.0023486548592-0.034341362795999995 -0.00111758118864 0.0036010404917-0.0039026609385999994 0.0016699372352999998 0.021206653766000002-0.0079418490394 0.003116062838 0.072380311755-0.007021828444599999 0.0036496566028 0.07869801528199999-0.0030219410092 0.018637386319 0.00102082843809-0.0042041069026 0.023621439238999998 0.0022947637053-0.0061050946438 0.00114796066823 0.018477825284-0.0065708646563999995 0.0022944737838999996 0.035902813761-0.037790461814 -0.0015372596281999999 0.008878823611899999-0.13264545848599998 -0.0144908102251 -0.033606397957999995-0.016229093174 1.41831464625E-4 0.005181988760999999-0.024075296507999996 -8.708131965899999E-4 0.0034344653516999997

Матрицу SVD можно использовать как координаты в трехмерном пространстве.

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

Теперь нужно применить данную операцию и для терминов, то есть слов.

Последний этап метода кластеризации найти кластерные группы. Так как у нас уже есть трехмерная пространство, где хранятся точки документов и терминов в виде вершин, то нужно соединить эти документы и слова использовав схожий метод кластеризации DBSCAN. Для определения расстояние между документом и словом используется Евклидовое расстояние. А радиус можно определить по формуле ниже. В данном примере и при тестировании используется r=0.007. Так как в пространстве находится 562 документов и более 80.000 тысяч слов, то они расположены близко. При большом радиусе алгоритм будет связывать термин и документ в один кластер, которые не должны быть в одной группе.

r=max(D)/n

где max(D) это дистанция между документом и самой дальней точкой термина, то есть максимальная дистанция документа в пространстве. n - это количество документов в пространстве

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

После этого нужно всего лишь соединить вершины документов, которые имеют общие вершины терминов. Для соединения документов нужно чтобы общее число терминов было больше 4-х. Формула определение общего сила слов (в данном случае > nt)

nt=N/S

N это количество кластерных групп термин - документов, S это количество связей в семантическом пространстве.

Данные результаты также записываются в базу данных, где вершины документов соединены. Каждая отдельная соединенная группа документов являются кластерами

Методы классификации Aylien API

Для классификации в инструменте Aylien API всего лишь нужно передать любой текст. API вернет ответ в виде json объекта, где внутри есть категории классификации. Можно было бы отправлять весь текст каждого документа в одной группе кластеров через API и получить категории классификации. Для примера рассмотрим 9 групп кластеров, которые состоят из статьи про ИТ технологии. Все тексты документов каждой группы записываются в массив и отправляют запрос POST через API:

String queryText = "select  DocText from documents where clusters = '" + cluster + "'";   OResultSet resultSet = database.query(queryText);   while (resultSet.hasNext()) {   OResult result = resultSet.next();   String textDoc = result.toString().replaceAll("[\\<||\\>||\\{||\\}]", "").replaceAll("doctext:", "")   .toLowerCase();   keywords.add(textDoc.replaceAll("\\n", ""));   }   ClassifyByTaxonomyParams.Builder classifyByTaxonomybuilder    = ClassifyByTaxonomyParams.newBuilder();   classifyByTaxonomybuilder.setText(keywords.toString());   classifyByTaxonomybuilder.setTaxonomy(ClassifyByTaxonomyParams.StandardTaxonomy.IAB_QAG);   TaxonomyClassifications response = client.classifyByTaxonomy(classifyByTaxonomybuilder.build());   for (TaxonomyCategory c : response.getCategories()) {   clusterUpdate.add(c.getLabel());   }

После успешного получение ответа от сервиса методам GET, данные группы обновляются:

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

Так же изучался и подход применение классификации без использования метода кластеризации. Результат очень сильно отличался. Так как если алгоритм не знает группы кластеров, то метод классификации будет классифицировать каждый документ отдельно. И предметы для каждого документа может быть различным и не обобщенным. Так для эксперимента, классифицируем каждый документ и находим предмет. Но для сравнения оставим кластерные группы, которые не будут влиять на саму классификацию:

Разработка веб-интерфейса

Цель разработки веб-интерфейса наглядный вид результата использование алгоритма кластеризации и классификации. Это дает пользователю удобный интерфейс не только увидеть сам результат, но и в дальнейшем использовать эти данные для нужд. Так же разработка веб-интерфейса показывает, что данный метод можно успешно использовать для онлайн библиотек. Веб приложение было написано с использованием Фреймворка Vaadin Flow:

В данном приложении есть следующие функции:

  • Документы, разделенные по предметам методом кластеризации и классификации.

  • Поиск по ключевым словам.

  • Поиск по хэш-тегам.

  • Весь список документов в базе данных, где есть возможность поиска по ИД документа, наименованию документа, предметам кластеров, ключевым словам и по хэш-тэгам.

  • Возможность скачивание файла.

Список документов классификации по предмету Technology & Computing:

Список документов найденные по ключевым словам:

Табличный список всех документов:

Заключение

В работе был подробно рассмотрен сама концепция машинного обучение, для понимания цели использование методов или алгоритмов машинного обучения. Подробно описаны актуальные и известные методы и алгоритмы машинного обучения для решения цели и задачи работы. Так как задачи кластеризации используется для разных областей и предметов, в данной исследовательской работе было выбрано цель автоматизация процесса классификации текстовых данных, которые считаются сложнее чем обычные задачи классификации других данных. Алгоритм описанный и разработанный в данной исследовательской работе можно применять на большие количества текстовых документов на английском языке. Хотя алгоритм не сильно подвязан на язык текста. Для использования алгоритма для других языков нужно изменить алгоритмы обработки естественного языка. Алгоритм включает в себе две основных методов: кластеризация текста и классификация групп кластеров.

Разработка алгоритма кластеризации, который включают в себе последовательное применение алгоритмов лемматизации, токенизации, стоп-листниг, tf-idf, сингулярного разложение. Первые три метода относится к методу обработки естественного языка, данные методы можно изменить под язык обрабатываемого текста. Для нахождение кластерных групп используется алгоритм на основе метода DBSCAN и использование Евклидового расстояние для определения расстояние между объектами. При исследовании было доказано что точность кластеризации зависит от отношения количества кластеров к количеству объектов в одном кластере. Количество кластеров определяется радиусом каждого документа, а количество объектов в одном кластере определяется средним количеством общих объектов, в данном случае слов или терминов. Алгоритм кластеризации описанный в работе можно использовать не только для классификации групп, а и для других целей, таких как нахождение ассоциативных правил, нахождение групп документов, которые схожи по смысловому тексту и т.д.

В результате исследование, было предложено использование NoSQL базы данных, о именно OrinetDB, который поддерживает все 4 модели NoSQL. Данный тип базы данных очень хорошо подходит для хранения результатов алгоритма кластеризации, так как данный результат является не реляционным. Стоит отметить что OrientDB очень удобен для хранения, обработки и визуализации хранимых данных.

Для классификации кластерных используется Aylien API, который использует подход классификации по таксономии и на базе кодов. В результате исследовании кластерные группы были разделены по предметным областям, который включает в себя более 100 контентной категории. Данный метод можно заменить и другими, более специфическими алгоритмами машинного обучение, таких как метод опорных векторов, метод k-ближайших, нейронную сеть. Но так как данные методы требуют большое количество данных который используется для построения модели, в данной работе было использована готовая модель классификации.

Подробнее..

Перевод Atlas как сервис

12.05.2021 20:07:31 | Автор: admin

Перевод материала подготовлен в рамках курса "NoSQL".

Приглашаем также всех желающих на двухдневный интенсив MongoDB Map-Reduce Framework.
Темы 1 дня: CRUD-операции; фильтрация по полям; sort, skip, limit; запросы по поддокументам.
Темы 2 дня: концепция map-reduce; концепция pipeline; структура и синтаксис агрегации; стадия $match; стадия $group; стадия $lookup.


Многие наши клиенты предоставляют своим командам разработчиков доступ к MongoDB как к сервису. Разработчики могут запросить экземпляр базы данных MongoDB и получить строку для подключения и учетные данные за считаные минуты. Переходя на использование MongoDBAtlas, наши клиенты заинтересованы и в том, чтобы дать своим разработчикам соответствующий уровень обслуживания.

В Atlas есть очень мощный уровень управления для предоставления кластеров. Однако в крупных организациях, где работают тысячи разработчиков, не всегда целесообразно предоставлять такому количеству людей прямой доступ к этому интерфейсу. Цель этой статьи показать, как можно использовать API-интерфейсы Atlas для предоставления доступа к MongoDB как к сервису, когда MongoDB работает под управлением Atlas.

В частности, мы продемонстрируем способ создания интерфейса, предлагающего разработчикам набор вариантов для создания экземпляра базы данных MongoDB. Чтобы упростить себе задачу, рассмотрим, как предоставить разработчикам набор вариантов памяти и хранилища для настройки кластера. Прочие параметры, например выбор провайдера облачных услуг и региона, мы рассматривать не будем. Мы также расскажем о том, как добавлять метки к кластерам Atlas, так как эта функция не поддерживается в пользовательском интерфейсе Atlas. Для примера мы добавили метку для описания кластера.

Архитектура

Хотя API-интерфейсы Atlas можно вызывать непосредственно из клиентского интерфейса, мы решили использовать трехуровневую архитектуру. Ее преимущества заключаются в следующем:

  • возможность ограничивать доступную функциональность по мере необходимости;

  • возможность упростить API-интерфейсы, доступные разработчикам клиентских частей приложений;

  • возможность тонкой настройки защиты конечных точек API.

  • Мы могли бы воспользоваться другими функциями серверной части, например триггерами, интеграцией с Twilio и т.д.

Конечно же, для размещения среднего уровня мы выбрали Realm.

Реализация

Серверная часть

API Atlas

API-интерфейсы Atlas обернуты в набор функций Realm.

По большей части все они вызывают API Atlas следующим образом (здесь мы взяли для примера getOneCluster):

/** Gets information about the requested cluster. If clusterName is empty, all clusters will be fetched.* See https://docs.atlas.mongodb.com/reference/api/clusters-get-one**/exports = async function(username, password, projectID, clusterName) {const arg = {scheme: 'https',host: 'cloud.mongodb.com',path: 'api/atlas/v1.0/groups/' + projectID +'/clusters/' + clusterName,username: username,password: password,headers: {'Content-Type': ['application/json'], 'Accept-Encoding': ['bzip, deflate']},digestAuth:true};// The response body is a BSON.Binary object. Parse it and return.response = await context.http.get(arg);return EJSON.parse(response.body.text());};

Исходный код каждой функции размещен на GitHub.

API MiniAtlas

Следующий шаг представление функций как конечных точек, которые может использовать клиентская часть. В качестве альтернативы мы могли бы вызывать функции с помощью Realm Web SDK, но мы решили придерживаться протокола REST; он более знаком нашим веб-разработчикам.

Используя функционал сторонних сервисов, мы разработали следующие 6 конечных точек:

API

Тип метода

Конечная точка

Получение списка кластеров

GET

/getClusters

Создание кластера

POST

/getClusters

Вывод состояния кластера

GET

/getClusterState?clusterName:cn

Изменение кластера

PATCH

/modifyCluster

Приостановка или возобновление работы кластера

POST

/pauseCluster

Удаление кластера

DELETE

/deleteCluster?clusterName:cn

Далее представлен исходный код конечной точки getClusters (примечание: имя пользователя и пароль извлекаются из констант Value и Secret):

/** GET getClusters** Query Parameters** None** Response - Currently all values documented at https://docs.atlas.mongodb.com/reference/api/clusters-get-all/**/exports = async function(payload, response) {var results = [];const username = context.values.get("username");const password = context.values.get("apiKey");projectID = context.values.get("projectID");// Sending an empty clusterName will return all clusters.var clusterName = '';response = await context.functions.execute("getOneCluster", username, password, projectID, clusterName);results = response.results;return results;};

Исходный код каждого веб-хука размещен на GitHub.

При сохранении веб-хука генерируется URL-адрес, который служит конечной точкой для API:

Защита конечных точек API

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

exports = function(payload) {const headers = context.request.requestHeadersconst { Authorization } = headersconst user_id = Authorization.toString().replace(/^Bearer/, '')return user_id};

MongoDB Realm имеет несколько встроенных провайдеров аутентификации, включая доступ для анонимных пользователей, доступ по комбинации электронная почта/ пароль, доступ по ключам API, а также аутентификацию OAuth2.0 через Facebook, Google и AppleID.

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

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

Клиентская часть

Клиентская часть реализована на JQuery и размещена в Realm.

Аутентификация

С помощью MongoDB Stitch Browser SDK клиент предлагает пользователю войти в учетную запись Google (если вход еще не выполнен) и передает учетные данные пользователя Google в StitchAppClient.

let credential = new stitch.GoogleRedirectCredential();client.auth.loginWithRedirect(credential);

Идентификатор пользователя, который необходимо использовать при отправке запроса API в серверную часть, можно получить из StitchAppClient следующим образом:

let userId = client.auth.authInfo.userId;

Затем его можно включить в заголовок при вызове API. Вот пример вызова API createCluster:

export const createCluster = (uid, data) => {let url = `${baseURL}/createCluster`const params = {method: "post",headers: {"Content-Type": "application/json;charset=utf-8",...(uid && { Authorization: uid })},...(data && { body: JSON.stringify(data) })}return fetch(url, params).then(handleErrors).then(response => response.json()).catch(error => console.log(error) );};

Все вызовы API можно посмотреть в файле webhooks.js.

Полезный совет

Нам принесли большую пользу командные рабочие пространства в Postman. Этот инструмент позволяет совместно разрабатывать серверные API и проверять их работоспособность.

Заключение

Этот прототип создан для демонстрации возможностей, о которых, надеемся, вы уже получили представление! Основа решения есть. Как ее использовать, решаете вы сами.


Узнать подробнее о курсе "NoSQL"

Участвовать в двухдневном интенсиве MongoDB Map-Reduce Framework

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru