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

Database

Делаем страницу на React с базой сотрудников при помощи Airtable и Quarkly

31.01.2021 16:07:40 | Автор: admin

Слышали про такой инструмент, как Airtable, но не знали, с чего начать? Тогда приглашаем в мир визуального программирования построения БД!


Этим постом мы начинаем цикл обучающих статей, в которых будем давать практические примеры работы с нашим инструментом Quarkly. В этом уроке мы сделаем простое веб-приложение, которое будет показывать сотрудников компании. При создании приложения ни один сотрудник РЖД не пострадал.


Фронт будем делать при помощи Quarkly, а данные подтягивать из базы в Airtable. На выходе получим react-приложение, синхронизированное с базой данных.



Преамбула. Почему Airtable


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


Если вы впервые слышите про Airtable, перед началом работы не будет лишним почитать официальное руководство на сайте компании. Также советуем не стесняться и задавать вопросы в чатике Airtable Chat & Community в телеграм.


Фронтендная часть работ будет сделана в Quarkly, и для этого мы будем использовать всего два компонента:


  • Карточка сотрудника. В ней будут фото, текстовые данные и две кнопки: отправить email и позвонить. Эти данные карточка будет получать от родительского компонента обертки.
  • Обертка. Она будет принимать данные из Airtable, генерировать карточки и передавать в них данные.

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




Часть 1. Делаем визуал в Quarkly


Создание карточки:


  1. Создадим новый проект в Quarkly, назовем его Airtable Example;
  2. Перейдем внутрь проекта;
  3. Добавим готовый блок с карточками сотрудников. Для этого кликаем на черную кнопку + посередине и выбираем блок из категории Team;



  4. Выбираем на панели слоев первую карточку (StackItem) и преобразуем её в компонент;



    Для этого нажмите на троеточие и выберите пункт Convert to Component. Назовем этот компонент EmployeeCard.

  5. Теперь мы можем свободно редактировать код этого react-компонента, но пока этого делать не будем и перейдем к созданию компонента-обертки.

Создание обертки:


  1. Подготовим обертку к преобразованию в компонент. Для этого сначала удалим оставшиеся три карточки, они нам не нужны;

  2. Теперь вытащим EmployeeCard из Stack. Затем преобразуем Stack в компонент, как мы уже делали ранее с EmployeeCard: правая панель, кнопка троеточие и Convert to Component. Компонент назовем EmployeeTable.



    На этом пока всё, подготовительный этап завершен. Оставим компоненты и займемся базой в Airtable.


Часть 2. Создаем базу данных в Airtable


Переходим на сайт Airtable и регистрируемся/авторизуемся.


  1. Кликаем на кнопку Add a base, чтобы создать новую базу. Из выпадающего меню выберите пункт Start with a template;

  2. Выбираем категорию HR & Recruiting и шаблон Employee directory. Далее кликаем на кнопку Use template;

  3. Переходим в созданный проект;


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



Часть 3. Получаем доступ к API


Изначально Airtable интересен для нас именно за счет удобного API. При этом, что немаловажно, возможность забирать данные и отправлять их в нашу базу Airtable предоставляет бесплатно.


  1. Переходим на страницу выбора API проектов: https://airtable.com/api

  2. Выбираем наш проект Employee directory. В появившейся документации переходим на раздел AUTHENTICATION.

  3. Скопируйте две строчки, расположенные ниже заголовка EXAMPLE USING BEARER TOKEN (RECOMMENDED).

    У меня они выглядят так:

    $ curl https://api.airtable.com/v0/app2MdLITmRTBsrkg/Employee%20directory \
    -H "Authorization: Bearer YOUR_API_KEY"
  4. Теперь нам нужен YOUR_API_KEY. Это уникальный ключ доступа, который генерируется для каждого аккаунта. Найти его можно в настройках.

  5. На открывшейся странице перейдите в раздел API и нажмите на кнопку Generate API key;

  6. Скопируйте ключ. Сохраните его рядом со строчками из пункта 3. Они пригодятся нам далее.


Часть 4. Интегрируем базу Airtable в Quarkly


На этом этапе мы добавим в компонент EmployeeTable код, который будет забирать данные из API.


  1. Переходим в редактирование кода компонента. Для этого откроем вкладку Components и нажмем на кнопку <> у EmployeeTable (она появится при наведении курсора на компонент);

  2. Сейчас код компонента выглядит так:

  3. Заменим:

    import React from "react";
    

    на:

    import React, { useEffect, useState } from "react";
    

    Таким образом мы импортируем хуки useEffect и useState, которые помогут нам в дальнейшем;
  4. Ниже добавляем строчку, чтобы импортировать наш компонент EmployeeCard:

    import EmployeeCard from "./EmployeeCard";
    
  5. Заменим children (они нам пока не нужны) на override (пригодятся, чтобы выбирать элементы и стилизовать их на странице):

    const EmployeeTable = props => {const {children,rest} = useOverrides(props, overrides, defaultProps);
    

    на:

    const EmployeeTable = props => {const {override,rest} = useOverrides(props, overrides, defaultProps);
    
  6. Ниже добавим вызов хука useState, который будет следить за состоянием:

    const [employees, setEmployees] = useState([]);
    
  7. Далее добавим хук useEffect, который будет делать запросы к API Airtable и помещать полученные данные в наше состояние через функцию setEmployees.

    Добавляем сюда строчки, которые скопировали ранее. В fetch мы добавляем URL адрес нашей базы, добавляя параметр ?view=All%20employees. В headers мы добавляем параметры авторизации и непосредственно сам API ключ, который мы сгенерировали в 3 части этой статьи, подпункт 4.

    useEffect(() => {fetch("https://api.airtable.com/v0/appWw7KBKSc9bPjZE/Employee%20directory?view=All%20employees", {headers: {'Authorization': 'Bearer YOUR_API_KEY'}}).then(response => response.json()).then(data => setEmployees(data.records.map(({ fields }) => fields)));}, []);
    
  8. Теперь будем генерировать карточки из полученных данных, передавая им props с данными и override. Он нужен, чтобы выбирать и стилизовать элементы на странице.

    Меняем:

    return <Stack {...rest}>{children}</Stack>;};
    

    на:

    return <Stack {...rest}>{employees.map(employee => <EmployeeCard  {...override("employeeCard")}  employee={employee} />)}</Stack>;};
    
  9. Нажмите Ctrl + S (или Cmd + S на Mac). Окончательный код выглядит так:

    import React, { useEffect, useState } from "react";import { useOverrides, Stack } from "@quarkly/components";import EmployeeCard from "./EmployeeCard";const defaultProps = {"margin-top": "40px"};const overrides = {};const EmployeeTable = props => {const {override,rest} = useOverrides(props, overrides, defaultProps);const [employees, setEmployees] = useState([]);useEffect(() => {fetch("https://api.airtable.com/v0/appWw7KBKSc9bPjZE/Employee%20directory?view=All%20employees", {headers: {'Authorization': 'Bearer YOUR_API_KEY'}}).then(response => response.json()).then(data => setEmployees(data.records.map(({ fields }) => fields)));}, []);return <Stack {...rest}>{employees.map(employee => <EmployeeCard  {...override("employeeCard")} employee={employee} />)}</Stack>;};Object.assign(EmployeeTable, {...Stack,defaultProps,overrides});export default EmployeeTable;
    

    Важно: не забудьте вставить свой уникальный API ключ вместо текста YOUR_API_KEY.

Готово! Теперь мы получаем данные от Airtable, помещаем их в employees и проходимся по нему методом map. На каждую запись в employees мы создаем <EmployeeCard/>, в который передаем как пропс конкретные данные.


Осталось научить EmpolyeeCard принимать эти данные и показывать их в нужном месте.



Часть 5. Учим EmpolyeeCard работать с БД


Здесь мы научим карточку сотрудника принимать данные и показывать их.


  1. Откроем код компонента. Для этого заходим во вкладку Components, ищем там EmployeeCard, наводим курсор и жмем на кнопку <>.
  2. Сейчас код компонента выглядит так:

    import React from "react";import { useOverrides, Override, StackItem } from "@quarkly/components";import { Box, Text } from "@quarkly/widgets";const defaultProps = {"width": "25%","lg-width": "50%","sm-width": "100%"};const overrides = {"box": {"kind": "Box","props": {"height": "0","margin": "0 0 20px 0","padding-bottom": "100%","background": "url(http://personeltest.ru/aways/images.unsplash.com/photo-1503443207922-dff7d543fd0e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=582&q=80) 50% 0/cover no-repeat"}},"text": {"kind": "Text","props": {"color": "--grey","margin": "0","children": "CEO"}},"text1": {"kind": "Text","props": {"as": "h3","font": "--headline3","margin": "5px 0 20px 0","children": "Nathan K. Joe"}},"text2": {"kind": "Text","props": {"as": "p","margin": "20px 0 5px 0","children": "This space is 100% editable. Use it to introduce a team member, describe their work experience and role within the company. This is also a great place to highlight a team member's strong sides."}}};const EmployeeCard = props => {const {override,children,rest} = useOverrides(props, overrides, defaultProps);return <StackItem {...rest}><Override slot="StackItemContent" flex-direction="column" /><Box {...override("box")} /><Text {...override("text")} /><Text {...override("text1")} /><Text {...override("text2")} />{children}</StackItem>;};Object.assign(EmployeeCard, { ...StackItem,defaultProps,overrides});export default EmployeeCard;
    
  3. Ищем строчку:

    } = useOverrides(props, overrides, defaultProps);
    

    и добавляем ниже:

    const { employee = {} } = rest;
    

    В объект employee помещаем наши данные.
  4. На примере фотографии сотрудника проверим, что всё работает, как нужно. Ищем строку и меняем:

    <Box {...override("box")} />
    

    на:

    <Box {...override("box")} background-image={`url(${employee.Photo && employee.Photo[0] && employee.Photo[0].url})`}/>
    

    Также ищем:

    "background": "url(http://personeltest.ru/aways/images.unsplash.com/photo-1503443207922-dff7d543fd0e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=582&q=80) 50% 0/cover no-repeat"
    

    и меняем на:

    "background-size": "cover","background-position": "center","background-image": "url(http://personeltest.ru/aways/images.unsplash.com/photo-1503443207922-dff7d543fd0e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=582&q=80) 50% 0/cover no-repeat"
    

    Должно получится так:

  5. Смотрим, какие поля у нас есть. Документация для API в Airtable сделана очень хорошо. Название полей можно посмотреть в https://airtable.com/api, выбрав свою базу.

    Далее ищем раздел EMPLOYEE DIRECTORY TABLE.

    Итак, у нас есть:

    Name
    Department
    Home address
    Email address
    DOB
    Start date
    Phone
    Reports to
    Title
    Status
    Photo
    Location
  6. Добавим Title. Для этого заменим:

    <Text {...override("text")} />
    

    на:

    <Text {...override("title")} children={employee.Title} />
    

    И не забудем отредактировать overrides этого компонента, чтобы мы могли его выбирать и редактировать на странице.

    Меняем:

    "text": {"kind": "Text","props": {"color": "--grey","margin": "0","children": "CEO"}},
    

    на:

    "title": {"kind": "Text","props": {"color": "--grey","margin": "0","children": "Title"}},
    

    Сохраняем и проверяем:



    Результат: в карточки добавилась строка с профессией.
  7. Повторим такие же действия для Name и Home address.

    Заменим:

    <Text {...override("text1")} /><Text {...override("text2")} />
    

    на:

    <Text {...override("name")} children={employee.Name} /><Text {...override("address")} children={employee['Home address']} />
    

    И поправим их overrides. Для этого заменим:

    "text1": {"kind": "Text","props": {"as": "h3","font": "--headline3","margin": "5px 0 20px 0","children": "Nathan K. Joe"}},"text2": {"kind": "Text","props": {"as": "p","margin": "20px 0 5px 0","children": "This space is 100% editable. Use it to introduce a team member, describe their work experience and role within the company. This is also a great place to highlight a team member's strong sides."}}
    

    на:

    "name": {"kind": "Text","props": {"as": "h3","font": "--headline3","margin": "5px 0 5px 0","children": "Name"}},"address": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Home address"}},
    

    Сохраняем и снова проверяем:

  8. Добавим ещё несколько Text по аналогии. Для простоты мы не будем брать Department и Reports to, потому что эти данные находятся в другой базе DEPARTMENTS TABLE.

    Добавляем:

    <Text {...override("address")} children={employee['Home address']} /><Text {...override("Start date")} children={`Start date: ${employee['Start date']}`} /><Text {...override("Status")} children={employee['Status']} /><Text {...override("DOB")} children={`Birth date: ${employee['DOB']}`} />
    

    и

    "address": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Home address"}},"Start date": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Start date"}},"Status": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Status"}},"DOB": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Birth date"}},
    

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

  9. Теперь добавим два компонента Link, в которых у нас будут Phone и Email:

    import { Box, Text } from "@quarkly/widgets";
    

    меняем на:

    import { Box, Text, Link } from "@quarkly/widgets";
    

    И добавляем следующие строки:

    <Link {...override("Email address")} children={employee['Email address']} href={`mailto:${employee['Email address']}`} /><Link {...override("Phone")} children={employee['Phone']} href={`tel:${employee['Phone']}`}/>
    

    Не забыв про их overrides:

    "Email address": {"kind": "Link","props": {"margin": "10px 0 5px 0","color": "--primary","text-decoration": "none","children": "Email"}},"Phone": {"kind": "Link","props": {"margin": "10px 0 5px 0","color": "--primary","text-decoration": "none","children": "Phone"}},
    

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


Финально наш код выглядит так:

import React from "react";import { useOverrides, Override, StackItem } from "@quarkly/components";import { Box, Text, Link } from "@quarkly/widgets";const defaultProps = {"width": "25%","lg-width": "50%","sm-width": "100%"};const overrides = {"box": {"kind": "Box","props": {"height": "0","margin": "0 0 20px 0","padding-bottom": "100%","background-size": "cover","background-position": "center","background-image": "url(http://personeltest.ru/aways/images.unsplash.com/photo-1503443207922-dff7d543fd0e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=582&q=80) 50% 0/cover no-repeat"}},"title": {"kind": "Text","props": {"color": "--grey","margin": "0","children": "title"}},"name": {"kind": "Text","props": {"as": "h3","font": "--headline3","margin": "5px 0 5px 0","children": "Name"}},"address": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Home address"}},"Start date": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Start date"}},"Status": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Status"}},"DOB": {"kind": "Text","props": {"as": "p","margin": "10px 0 5px 0","children": "Birth date"}},"Email address": {"kind": "Link","props": {"margin": "10px 0 5px 0","color": "--primary","text-decoration": "none","children": "Email"}},"Phone": {"kind": "Link","props": {"margin": "10px 0 5px 0","color": "--primary","text-decoration": "none","children": "Phone"}},};const EmployeeCard = props => {const {override,children,rest} = useOverrides(props, overrides, defaultProps);const { employee = {} } = rest;return <StackItem {...rest}><Override slot="StackItemContent" flex-direction="column" /><Box {...override("box")} background-image={`url(${employee.Photo[0].url})`}/><Text {...override("title")} children={employee.Title} /><Text {...override("name")} children={employee.Name} /><Text {...override("address")} children={employee['Home address']} /><Text {...override("Start date")} children={`Start date: ${employee['Start date']}`} /><Text {...override("Status")} children={employee['Status']} /><Text {...override("DOB")} children={`Birth date: ${employee['DOB']}`} /><Link {...override("Email address")} children={employee['Email address']} href={`mailto:${employee['Email address']}`} /><Link {...override("Phone")} children={employee['Phone']} href={`tel:${employee['Phone']}`}/>{children}</StackItem>;};Object.assign(EmployeeCard, { ...StackItem,defaultProps,overrides});export default EmployeeCard;

Делаем коммит в GitHub и публикуем на Netlify:





Ждем несколько минут и проверяем: https://keen-varahamihira-c54ae1.netlify.app/



Для проверки синхронизации меняем данные в базе:


Теперь они появятся в приложении:


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


Репозиторий на GitHub: https://github.com/quarkly-dev/Getting-data-from-Airtable-tutorial


Спасибо за внимание!


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

Подробнее..

Перевод Доступна бесплатная версия cloud-native хранилища для Kubernetes от robin.io

21.10.2020 14:12:03 | Автор: admin


Jacky Parker Photography / Getty Images


Компания Robin, автор одноименного cloud-native решения для управления данными и приложениями корпоративных клиентов, например USAA, Sabre, SAP, Palo Alto Networks и Rakuten Mobile, сегодня рассказала о запуске новой бесплатной версии своего сервиса, в дополнение к крупному обновлению основного инструментария.


Robin.io обещает возможность cloud-native управления данными для контейнеризированных приложений с поддержкой типовых операций, например, резервное копирование и восстановление, снимки, возможность откатов и многое другое. Компания обеспечивает работоспособность на уровне железа, а также поддерживает основных поставщиков облачных услуг. Сервис не зависит от используемой базы данных, поддерживает PostgreSQL, MySQL, MongoDB, Redis, MariaDB, Cassandra, Elasticsearch и прочие.



Robin Cloud Native Storage работает с любыми нагрузками на любых платформах, основанных на Kubernetes, а также в любом облаке. Наша платформа с возможностями хранения, создания снимков, клонирования, миграции и обеспечения безопасности данных все они работают с простейшими командами предлагает командам разработчиков и DevOps-командам суперпростой, но высокопроизводительный инструмент для быстрого развертывания и управления вашими корпоративными нагрузками в Kubernetes.
Основатель и генеральный директор Robin, Partha Seetala.

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


Если говорить о корпоративных планах, то компания сегодня заявила, что она смещает фокус на цены, формируемые по потреблению, начиная с 0.42$ за узло-час (также предлагаются и годовые подписки). Корпоративный план включает поддержку 24x7 и не ограничивает число узлов и емкость хранилища.


Среди новых функций основного сервиса хранения Robin поддержка управления данными для Helm Charts, возможность указания точного местоположения данных (в основном это нужно для того, чтобы они находились как можно ближе к вычислительным ресурсам) и политики связывания, подразумевающей доступность для stateful приложений, зависящих от распределенных баз данных и платформ данных.

Подробнее..

Перевод 14 вещей, которые я хотел бы знать перед началом работы с MongoDB

23.09.2020 20:15:33 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Нереляционные базы данных.



Основные моменты:

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


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

Создание сервера MongoDB без аутентификации


К сожалению, MongoDB по умолчанию ставится без аутентификации. Для рабочей станции, доступ к которой устанавливается локально, такая практика нормальна. Но поскольку MongoDB это многопользовательская система, которая любит использовать большие объемы памяти, будет лучше, если вы поставите ее на сервер с максимально возможным в ваших условиях количеством оперативной памяти, даже если собираетесь использовать ее только для разработки. Установка на сервер через порт по умолчанию может оказаться проблемной, особенно, если в запросе можно выполнить любой код на javascript (например, $where в качестве идеи для инъекции).

Есть несколько методов аутентификации, но проще всего установить для пользователя ID/пароль. Воспользуйтесь этой идеей, пока будете думать над причудливой аутентификацией на основе LDAP. Если говорить о безопасности, то MongoDB должна постоянно обновляться, а логи всегда следует проверять на наличие несанкционированного доступа. Мне, например, нравится выбирать другой порт в качестве порта по умолчанию.

Не забудьте привязать поверхность атаки к MongoDB


Чек-лист обеспечения безопасности MongoDB содержит хорошие советы для снижения риска проникновения в сеть и утечки данных. Легко отмахнуться и сказать, что сервер для разработки не нуждается в высоком уровне безопасности. Однако все не так просто и это относится ко всем серверам MongoDB. В частности, если нет веской причины использовать mapReduce, group или $where, нужно отключить использование произвольного кода на JavaScript, написав в файле конфигурации javascriptEnabled:false. Поскольку в стандартной MongoDB файлы данных не зашифрованы, разумно запускать MongoDB с Dedicated User, у которого есть полный доступ к файлам, с ограниченным доступом только для него и возможностью использовать собственные средства управления доступом к файлам операционной системы.

Ошибка при разработке схемы


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

Классическая статья 6 эмпирических правил для проектирования схем MongoDB стоит того, чтобы ее прочитать, а такие функции, как Schema Explorer в стороннем инструменте Studio 3T, стоит использовать для регулярных проверок схем.

Не забудьте о порядке сортировки


Забыв о порядке сортировки можно сильнее всего разочароваться и потерять больше времени, чем при использовании любой другой неправильной конфигурации. По умолчанию MongoBD использует бинарную сортировку. Но вряд ли она будет кому-то полезна. Чувствительные к регистру, ударению, бинарные сортировки считались любопытными анахронизмами наряду с бусами, кафтанами и завивающимися усами еще в 80-х годах прошлого века. Теперь же их использование непростительно. В реальной жизни мотоцикл это то же самое, что и Мотоцикл. А Британия и британия одно и то же место. Строчная буква это просто прописной эквивалент большой буквы. И не заставляйте меня говорить о сортировке диакритических знаков. При создании базы данных в MongoDB используйте параметры сортировки без учета ударения и регистра, которые соответствуют языку и культуре пользователей системы. Так вы значительно упростите поиск по строковым данным.

Создание коллекций с большими документами


MongoDB рада разместить большие документы размером до 16 МБ в коллекциях, а GridFS предназначена для больших документов размером больше 16 МБ. Но только потому, что большие документы там можно разместить, хранить их там не лучшая идея. Лучше всего MongoDB будет работать, если вы будете сохранять отдельные документы размером в несколько килобайт, рассматривая их больше, как строки в широкой SQL-таблице. Большие документы будут источником проблем с производительностью.

Создание документов с большими массивами


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

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

Не забудьте, что порядок стадий в агрегации имеет значение


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

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

Такие инструменты как Studio 3T упростят построение запросов агрегации в MongoDB. Функция Aggregation Editor позволит вам применять операторы пайплайна по одному этапу за раз, а также проверять входные и выходные данные на каждом этапе для упрощения дебага.

Использование быстрой записи


Никогда не устанавливайте в MongoDB параметры записи с высокой скоростью, но низкой надежностью. Этот режим file-and-forget кажется быстрым, поскольку команда возвращается до того, как осуществляется запись. Если система упадет до того, как данные будут записаны на диск, они потеряются и окажутся в несогласованном состоянии. К счастью, в 64-битном MongoDB включено журналирование.

Движки для хранения MMAPv1 и WiredTiger используют логирование для предотвращения этого, хотя WiredTiger может восстановиться до последней согласованной контрольной точки, если журналирование отключено.

Журналирование гарантирует, что база данных находится в согласованном состоянии после восстановления и хранит все данные до момента записи в журнале. Периодичность записей настраивается с помощью параметра commitIntervalMs.

Чтобы быть уверенным в записях, убедитесь, что в файле конфигурации журналирование включено (storage.journal.enabled), а периодичность записей соответствует тому объему информации, который вы можете позволить себе потерять.

Сортировка без индекса


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

Если подходящего индекса нет, MongoDB обойдется без него. Существует ограничение памяти в 32 Мб на общий размер всех документов в операции сортировки, и если MongoDB достигнет этого предела, то она либо выдаст ошибку, либо вернет пустой набор записей.

Поиск без поддержки индексов


Поисковые запросы выполняют функцию аналогичную операции JOIN в SQL. Для лучшей работы им нужен индекс значения ключа, используемого в качестве внешнего ключа. Это неочевидно, поскольку использование не отражено в explain(). Такие индексы являются дополнением к индексу, записанному в explain(), который в свою очередь используется операторами пайплайна $match и $sort, когда те встречаются в начале пайплайна. Индексы теперь могут охватывать любую стадию пайплайна агрегации.

Отказ от использования мультиобновлений


Метод db.collection.update() используется для изменения части существующего документа или целого документа, вплоть до полной замены в зависимости от заданного вами параметра update. Не так очевидно, что он не обработает все документы в коллекции, пока вы не установите параметр multi для обновления всех документов, отвечающих критериям запроса.

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


В JSON объект состоит из неупорядоченной коллекции размером ноль или более пар имя/значение, где имя это строка, а значение это строка, число, логическое значение, ноль, объект или массив.

К сожалению, BSON придает большое значение порядку при поиске. В MongoDB порядок ключей внутри встроенных объектов имеет значение, т.е. { firstname: "Phil", surname: "factor" } это не то же самое, что { { surname: "factor", firstname: "Phil" }. То есть вы должны хранить в документах порядок пар имя/значение, если хотите быть уверены в том, что найдете их.

Не путайте null и undefined


Значение undefined никогда не было допустимым в JSON, согласно официальному стандарту JSON (ECMA-404, Раздел 5), несмотря на то, что оно используется в JavaScript. Более того, для BSON оно устарело и преобразовывается в $null, что не всегда является хорошим решением. Избегайте использования undefined в MongoDB.

Использование $limit() без $sort()


Очень часто, когда вы ведете разработку в MongoDB, полезно просто увидеть образец результата, который вернется из запроса или агрегации. Для этой задачи вам пригодится $limit(), но его никогда не должно быть в финальной версии кода, если только перед ним вы не используете $sort. Эта механика нужна, поскольку иначе вы не можете гарантировать порядок результата, и не сможете надежно просматривать данные. В верхней части результата вы будете получать разные записи в зависимости от сортировки. Для надежной работы запросы и агрегации должны быть детерминированными, то есть выдавать одинаковые результаты при каждом выполнении. Код, в котором есть $limit(), но нет $sort, не будет являться детерминированным и впоследствии может вызвать ошибки, которые будет трудно отследить.

Заключение


Единственный способ разочароваться в MongoDB это сравнивать ее непосредственно с другим типом баз данных, таким как СУБД, или прийти к ее использованию, исходя из каких-то определенных ожиданий. Это все равно что сравнивать апельсин с вилкой. Системы баз данных преследуют определенные цели. Лучше всего просто понять и оценить для себя эти различия. Было бы стыдно давить на разработчиков MongoDB из-за пути, который вынудил их идти по пути СУБД. Мне хочется видеть новые и интересные способы решения старых проблем, таких как обеспечение целостности данных и создание систем данных, устойчивых к сбоям и атакам злоумышленников.

Внедрение в MongoDB в версии 4.0 ACID transactionality хороший пример внедрения важных улучшений инновационным путем. Мультидокументальные и мультиоператорные транзакции теперь атомарные. Также появилась возможность регулировать время, необходимое для получения блокировок, и заканчивать зависшие транзакции, а также изменять уровень изоляции.


Читать ещё:


Подробнее..

Перевод Путеводитель по базам данных в 2021г

04.06.2021 20:14:20 | Автор: admin

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

Основные понятия баз данных

Что такое данные?

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

Что такое база данных?

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

Зачем нужна база данных?

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

Система управления базами данных (СУБД)

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

Пространственные данные и база данных

Особое внимание мы уделим обработке пространственных данных, поэтому я хотел бы обсудить здесь этот тип данных. Пространственные данные несколько отличаются от остальных. Координаты необходимо сохранять в особом формате, который обычно указан в документации на веб-сайте о базе данных. Этот формат позволяет базе считывать и правильно воспринимать координаты. Если обычно для поиска данных мы используем запросы типа Получить все результаты, где возраст>15, то пространственный запрос выглядит как-то так: Получить все результаты в радиусе 10км от определенной точки.Поэтому пространственные данные необходимо хранить в надлежащем формате.

Типы баз данных

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

Реляционные базы данных и РСУБД

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

Образец таблицы с информациейОбразец таблицы с информацией

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

Связь между двумя столбцамиСвязь между двумя столбцами

Взаимосвязи в реляционных базах данных мы подробно рассмотрим позже.

По сравнению с базами данных NoSQL, недостатком реляционных баз данных является относительно медленное получение результатов, когда количество данных стремительно увеличивается (по мнению автора статьи прим. пер.). Еще один недостаток заключается в том, что при добавлении каждой записи нужно следовать определенным правилам (типы столбцов, количество столбцов и т.д.), мы не можем просто добавить отдельный столбец только для одной записи.В реляционных базах данных используется SQL(Structured Query Language язык структурированных запросов), с помощью которого пользователи могут взаимодействовать с данными, хранящимися в таблицах. SQL стал одним из наиболее широко используемых языков для этой цели. Мы подробнее поговорим об SQL чуть позже.Вот примеры некоторых известных и часто используемых реляционных баз данных: PostgreSQL, MySQL, MSSQL и т.д. У каждой крупной компании, занимающейся реляционными базами данных, есть собственная версия SQL. В большинстве аспектов они выглядят одинаково, но иногда требуется немного изменить какой-нибудь запрос, чтобы получить те же результаты в другой базе данных (например, при переходе из PostgreSQL в MySQL).

Нереляционные базы данных (NoSQL)

Все базы данных, не являющиеся реляционными, относятся к категории нереляционных баз данных. Обычно данные хранятся в нетабличном формате, например:

  1. Пара ключ-значение

  2. Формат JSON, XML

  3. Графовый формат

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

База данных NoSQL реального времени в Google FirebaseБаза данных NoSQL реального времени в Google Firebase

База данных NoSQL реального времени в Google Firebase

При использовании баз данных NoSQL пользователям иногда приходится прописывать собственную логику, чтобы добавить уникальный ключ к каждой записи и тем самым обеспечить доступ к записям. В большинстве стандартных баз данных NoSQL, таких как Firebase и MongoDB, для хранения данных используется формат JSON. Благодаря этому очень легко и удобно выполнять операции с данными из веб-приложений, используя JavaScript, Python, Ruby и т.д.

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

Очевидно, что нам хотелось бы сохранить точку, линию, многоугольник, растры и т.д. так, чтобы это имело смысл, вместо того чтобы сохранять просто координаты. Нам нужна СУБД, которая позволяет не только сохранять данные, но и запрашивать их пространственными методами (буфер, пересечение, вычисление расстояния и т.д.). На сегодняшний день для этого лучше всего подходят реляционные базы данных, поскольку в SQL есть функции, помогающие выполнять подобные операции. Использование таких дополнительных средств, как PostGIS для PostgreSQL, открывает разработчикам возможности для написания сложных пространственных запросов. С другой стороны, NoSQL тоже работает в области геопространственных технологий: например, MongoDB предоставляет кое-какие функции для выполнения геопространственных операций. Однако реляционные базы данных все же лидируют на рынке с большим отрывом.

Работа с РСУБД

Основное внимание мы уделим РСУБД, так как именно эти системы в большинстве случаев мы будем использовать для хранения пространственных данных и работы с ними. В качестве примера мы будем использовать PostgreSQL, поскольку это самая перспективная реляционная база данных с открытым исходным кодом, а ее расширение PostGIS позволяет работать и с пространственными данными. Вы можете установить PostgreSQL, следуя инструкциям из документации. Помимо PostgreSQL рекомендуется также загрузить и установить pgAdmin. Платформа pgAdmin предоставляет веб-интерфейс для взаимодействия с базой данных. Также для этого можно загрузить и установить какое-либо другое совместимое ПО или использовать командную строку.

pgAdmin4 на MacpgAdmin4 на Mac

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

Создание базы данных. Нам нужно создать базу данных (в идеале должно быть по одной базе данных для каждого проекта).

Создание новой базы данных для проектаСоздание новой базы данных для проекта

В инструменте запросов (Query Tool) база данных создается следующим образом:

CREATE DATABASE <database_name>

Создание таблиц. Создание таблицы требует некоторых дополнительных соображений, поскольку именно здесь нам нужно определить все столбцы и типы данных в них. Все типы данных, которые можно использовать в PostgreSQL, вы найдете здесь.

pgAdmin позволяет нам выбрать в таблице различные ключи и ограничения, например Not Null (запрет на отсутствующие значения), Primary Key (первичный ключ) и т.д. Обсудим это подробнее чуть позже.

Создание таблицы пользователейСоздание таблицы пользователей

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

CREATE TABLE <table_name> (<column_1> <datatype>,<column_2> <datatype>,.....<column_n> <datatype>PRIMARY KEY (<column>));

CRUD-операции с данными в таблицах

CRUD-операции (создание, чтение, обновление и удаление Create, Retrieve, Update, Delete) это своего рода hello world в мире СУБД. Поскольку эти операции используются наиболее часто, команды для их выполнения одинаковы во всех РСУБД. Мы будем писать и выполнять запросы в инструменте запросов в pgAdmin, который вызывается следующим образом:

Инструмент запросов (Query Tool) в pgAdminИнструмент запросов (Query Tool) в pgAdmin

1. Создание новой записи

Для добавления новой записи в таблицу используйте следующую команду:

INSERT INTO <tablename> (column1, column2, column3,...) VALUES (value1, value2, value3,...);

INSERT, INTO, VALUE являются ключевыми словами в SQL, поэтому их нельзя использовать в качестве переменных, значений и т.д. Чтобы добавить новую запись в нашу таблицу пользователей, мы напишем в инструменте запросов следующий запрос:

INSERT INTO users(name, employed, address) VALUES ('Sheldon Cooper', true, 'Pasadena');

Обратите внимание: строки всегда следует заключать в'' (одинарные кавычки), а не в"" (двойные кавычки).

2. Получение записей (всех или нескольких)

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

select <column1, column2 ,...> from <tablename> 

Этот код извлекает весь набор данных. Если вы хотите получить только 20записей, напишите:

select <column1, column2 ,...> from <tablename> limit 20

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

select * from <tablename>

Если вы хотите получить результат с определенным условием, используйте ключевое слово WHERE, как показано ниже:

select * from <tablename> where <key> = <value>

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

--Retrieving Specific columns for all usersselect name,employed from users--Retrieving all columns for all usersselect * from users--Retrieving all columns for first 3 usersselect * from users limit 3--Retrieving all columns for all users where employed = trueselect * from users where employed = true

3. Обновление записей (всех или нескольких)РСУБД позволяет нам обновить все или только некоторые записи данных, указав новые значения для столбцов.

UPDATE <tablename> SET <column1> = <value1>, <column2> = <value2> 

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

UPDATE <tablename> SET <column1> = <value1>, <column2> = <value2>WHERE <column> = <value> 

В нашем случае мы обновим таблицы с помощью следующих запросов:

-- Make all rows as  employed = trueupdate users set employed = true-- change employed = false for entries with address = 'nebraska'update users set employed = false where address = 'nebraska'
Обновление записейОбновление записей

4. Удаление записей (всех или нескольких)Удалять записи в SQL легко. Пользователь может удалить либо все строки, либо только определенные строки, добавив условие WHERE.

-- Deleting all entries Delete from <tablename> -- Deleting entries based on conditionsDelete from <tablename> where <column> = <value> 
-- Deleting all entries Delete from users-- Deleting entries based on conditionsDelete from users where employed = false
Удаление записей из таблицыУдаление записей из таблицы

CRUD-операции используются очень часто, поскольку выполняют основные функции в базе данных.


Перевод подготовлен в рамках курса Базы данных. Все желающих приглашаем на бесплатный двухдневный онлайн-интенсив Бэкапы и репликация PostgreSQL. Практика применения. Цели занятия: настроить бэкапы; восстановить информацию после сбоя. Регистрация здесь.

Подробнее..

Синхронная репликация в Tarantool

02.02.2021 20:19:55 | Автор: admin


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

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

Задача реализации синхронной репликации стояла перед командой разработчиков Tarantool долгие годы, к ней было совершено несколько подходов. И вот теперь в релизе 2.6 Tarantool обзавёлся синхронной репликацией и выборами лидера на базе алгоритма Raft.

В статье описан долгий путь к схеме алгоритма и его реализации. Статья довольно длинная, но все её части важны и складываются в единую историю. Однако если нет времени на 60 000 знаков, то вот краткое содержание разделов. Можно пропустить те, которые уже точно знакомы.

  1. Репликация. Введение в тему, закрепление всех важных моментов.
  2. История разработки синхронной репликации в Tarantool. Прежде чем переходить к технической части, я расскажу о том, как до этой технической части дошли. Путь был длиной в 5 лет, много ошибок и уроков.
  3. Raft: репликация и выборы лидера. Понять синхронную репликацию в Tarantool без знания этого протокола нельзя. Акцент сделан на репликации, выборы описаны кратко.
  4. Асинхронная репликация. В Tarantool до недавнего времени была реализована только асинхронная репликация. Синхронная основана на ней, так что для полного погружения надо сначала разобраться с асинхронной.
  5. Синхронная репликация. В этом разделе алгоритм и его реализация описаны применительно к жизненному циклу транзакции. Раскрываются отличия от алгоритма Raft. Демонстрируется интерфейс для работы с синхронной репликацией в Tarantool.

1. Репликация


Репликация в базах данных это технология, которая позволяет поддерживать актуальную копию базы данных на нескольких узлах. Группу таких узлов принято называть репликационная группа, или менее формально репликасет. Обычно в группе выделяется один главный узел, который занимается обновлением/удалением/вставкой данных, выполнением транзакций. Главный узел принято называть мастером. Остальные узлы зовутся репликами. Ещё бывает мастер-мастер репликация, когда все узлы репликасета способны изменять данные.


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

Типично выделяется два типа репликации асинхронная и синхронная.

Асинхронная репликация


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

Цикл жизни асинхронной транзакции сводится к следующим шагам:

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

Параллельно с этим после записи в журнал транзакция поедет на реплики и там проживёт тот же цикл.


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

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

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

Решать эту проблему можно по-разному. Есть способы остаться на асинхронной репликации и действовать в зависимости от ситуации. В случае Tarantool можно написать логику своего приложения таким образом, чтобы после успешного коммита не торопиться отвечать клиенту, а подождать явно, пока реплики транзакцию подхватят. API Tarantool такое делать позволяет после определённых приседаний. Но подходит такое решение не всегда. Дело в том, что даже если запрос-автор транзакции будет ждать её репликации, остальные запросы к БД уже будут видеть изменения этой транзакции, и исходная проблема может повториться. Это называется грязные чтения (dirty reads).

            Client 1           |           Client 2-------------------------------+--------------------------------box.space.money:update(        v    {uid}, {{'+', 'sum', 50}}  |)                              v-------------------------------+--------------------------------                               v   -- Видит незакоммиченные                               |   -- данные!!!                               v   box.space.money:get({uid})-------------------------------+--------------------------------wait_replication(timeout)      |                               v

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

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

Синхронная репликация


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


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

Количество реплик, нужное для коммита транзакции, называется кворум. Обычно это 50 % размера репликасета + 1. То есть в случае двух узлов синхронная транзакция должна попасть на два узла, в случае трёх тоже на два, в случае четырёх на 3 узла, 5 на 3, и так далее.

50 % + 1 берётся для того, чтобы кластер мог пережить потерю половины узлов и всё равно не потерять данные. Это просто хороший уровень надёжности. Ещё одна причина: обычно в алгоритмах синхронной репликации предусмотрены выборы лидера, в которых для успешных выборов за нового лидера должно проголосовать более половины узлов. Любой кворум из половины или меньше узлов мог бы привести к выборам более чем одного лидера. Отсюда и выходит 50 % + 1 как минимум. Один кворум на любые решения коммиты транзакций и выборы.

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

  1. Расплата будет в скорости. Синхронная репликация медленнее, так как существенно возрастает задержка от начала коммита до его конца. Это происходит из-за участия сети и журналов других узлов: транзакцию надо на них послать, там записать и ответ получить. Сам факт присутствия сети увеличивает задержку потенциально до порядка миллисекунд.
  2. В синхронной репликации сложнее поддерживать доступность репликасета на запись. Ведь при асинхронной репликации правило простое: если мастер есть, то данные можно менять. Неважно, есть ли живые реплики и сколько их. При синхронной, даже если мастер доступен, он может быть не способен применять новые транзакции, если подключенных реплик слишком мало какие-то могли отказать. Тогда он не может собирать кворум на новые транзакции.
  3. Синхронную репликацию сложнее конфигурировать и программировать. Нужно аккуратно подбирать значение кворума (если каноническое 50 % + 1 недостаточно), таймаут на коммит транзакции, готовить мониторинг. В коде приложения придётся быть готовым к различным ошибкам, связанным с сетью.
  4. Синхронная репликация не предусматривает мастер-мастер репликацию. Это ограничение алгоритма, который используется в Tarantool в данный момент.

Но в обмен на эти сложности можно получить гораздо более высокие гарантии сохранности данных.

К счастью, придумывать алгоритм синхронной репликации с нуля не нужно. Есть уже созданные алгоритмы, принятые практически как стандарт. Сегодня самым популярным является Raft. Он обладает рядом особенностей, которые его популярность оправдывают:

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

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

2. История разработки синхронной репликации в Tarantool


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

Задача разработки синхронной репликации существует в Tarantool с 2015 года, сколько я помню себя работающим здесь. Синхронная репликация изначально не рассматривалась как что-то срочно необходимое. Всегда находились более важные задачи, или просто не хватало ресурсов. Хотя неизбежность её реализации была ясна.

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

Сначала на задачу был поставлен один человек, сделать её сразу и с наскоку. Но сложность оказалась слишком высока, а разработчик в итоге покинул команду. Было решено разбить синхронную репликацию на несколько задач поменьше, которые можно делать параллельно:

Реализация SWIM протокола построения кластера и обнаружения отказов. Дело в том, что в Raft выделяются два компонента, друг от друга почти не зависящие синхронная репликация при известном лидере и выборы нового лидера. Чтобы выбрать нового лидера, нужен способ обнаружить отказ старого. Это можно выделить в третью часть Raft, которую мог бы отлично решить протокол SWIM.

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

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

Ручные выборы лидера box.ctl.promote(). Это должна была быть такая функция, которую можно вызвать на инстансе и сделать его лидером, а остальных репликами. Предполагалось, что в выборах лидера самое сложное начать их, и что начать можно с того, чтобы запускать выборы вручную.

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

Всё это должно было закончиться вводом автоматических выборов лидера, логически завершая Raft.

Список задач был сформирован в 2015, и с тех пор на несколько лет был отложен в долгий ящик. Команда сильно отвлеклась на более приоритетные задачи, такие как дисковый движок vinyl, SQL, шардинг.

Ручные выборы


В 2018 году появились ресурсы, и синхронная репликация снова стала актуальна. В первую очередь попытались реализовать box.ctl.promote().

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

Получались выборы практически как в Raft, но автоматически голосование никто не начинает, даже если текущий лидер недоступен. В результате стало очевидно, что смысла делать box.ctl.promote() в его изначальной задумке нет. Это получалась чуть-чуть урезанная версия целого Raft.

Прокси


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

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

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

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

SWIM


Следующая попытка продолжить синхронную репликацию произошла в конце 2018 начале 2019. Тогда за год был реализован протокол SWIM. Реализация была выполнена в виде встроенного модуля, доступного для использования даже без репликации, для чего угодно, прямо из Lua. На одном инстансе можно было создавать много SWIM-узлов. Планировалось, что у Tarantool будет свой внутренний SWIM-узел специально для Raft-сообщений, обнаружений отказов и автопостроения кластера.

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

Оптимизации репликации


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

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

Человек, занимавшийся реализацией, покинул команду. Незадолго до него ушёл автор изначального разбиения задач и по совместительству CTO Tarantool. Почти одновременно с этим команду покинул ещё один сильный разработчик, соблазнившись оффером из Google. В итоге команда была обескровлена, а прогресс по синхронной репликации был отброшен практически к нулю.

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

Прогресс пошёл значительно быстрее. В 2020-м, менее чем за год была реализована синхронная репликация. За основу снова взяли протокол Raft. В качестве минимальной рабочей версии оказалось нужно сделать всего две вещи: синхронный журнал и выборы лидера. Вот так сразу, без годов подготовки, без бесчисленных подзадач и переработок существующих систем Tarantool. По крайней мере, для первой версии.

3. Raft: репликация и выборы лидера


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

Оригинальная статья с полным описанием Raft называется In Search of an Understandable Consensus Algorithm. Алгоритм делится на две независимые части: репликация и выборы лидера.

Под лидером подразумевается единственный узел, принимающий запросы. Можно сказать, что лидер в терминологии Raft это почти то же самое, что мастер в Tarantool.

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

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

В классическом Raft все узлы репликасета имеют роль: лидер (leader), реплика (follower) или кандидат (candidate):

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

При нормальной работе кластера (то есть почти всегда) в репликасете ровно один лидер, а все остальные реплики.

Выборы лидера


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

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


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

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

Синхронная репликация


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

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


На реплики, не попавшие в кворум сразу, транзакция и факт её коммита доставляются асинхронно.

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


Журнал в Raft устроен как последовательность записей вида key = value. Каждая запись содержит саму модификацию данных и метаданные индекс в журнале и терм, когда запись была создана на лидере.

В журнале лидера поддерживается два курсора: конец журнала и последняя закоммиченная транзакция. Журналы реплик же являются префиксами журнала лидера. Лидер по мере сборки подтверждений от реплик пишет коммиты в журнал и продвигает индекс последней завершённой транзакции.

В процессе работы Raft поддерживает два свойства:

  • Если две записи в журналах двух узлов имеют одинаковые индекс и терм, то и команда в них одна и та же. Та, которая key = value.
  • Если две записи в журналах двух узлов имеют одинаковые индекс и терм, то их журналы полностью идентичны во всём, вплоть до этой записи.

Первое следует из того, что в каждом терме новые изменения генерируются на единственном лидере. Они содержат одинаковые команды и термы, распространяемые на все реплики. Ещё индекс всегда возрастает, и записи в журнале никогда не переупорядочиваются.

Второе следует из проверки, встроенной в AppendEntries. Когда лидер этот запрос рассылает, он включает туда не только новые изменения, но и терм + индекс последней записи журнала лидера. Реплика, получив AppendEntries, проверяет, что если терм и индекс последней записи лидера такие же, как в её локальном журнале, то можно применять новые изменения. Они точно следуют друг за другом. Иначе реплика не синхронизирована не хватает куска журнала с лидера, и даже могут быть транзакции не с лидера! Не синхронизированные реплики, согласно Raft, должны отрезать у себя голову журнала такой длины, чтоб остаток журнала стал префиксом журнала лидера, и скачать с лидера правильную голову журнала.

Здесь стоит сделать лирическое отступление и отметить, что на практике отрезание головы журнала не всегда возможно. Ведь данные хранятся не только в журнале! Например, это может быть B-дерево в SQLite в отдельном файле, или LSM-дерево, как в Tarantool в движке vinyl. То есть только отрезание головы журнала не удалит данные, ждущие коммита от лидера, если они попадают в хранилище сразу. Для такого журнал, как минимум, должен быть undo. То есть из каждой записи журнала можно вычислить, как сделать обратную запись, откатив изменения. Undo-журнал может занимать много места. В Tarantool же используется redo-журнал, то есть можно его проигрывать с начала, но откатывать с конца нельзя.

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


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


На реплике журнал может быть длиннее и даже иметь термы новее, чем в журнале лидера. Хотя текущий терм лидера всё равно будет больше, даже если он ещё ничего не записал (иначе бы он не избрался). Такое может произойти, если реплика была лидером в терме 3 и успела записать две записи, но кворум на них не собрала. Потом был выбран новый лидер в терме 4, и он успел записать две другие записи. Но на них тоже кворум не собрал, а только реплицировал на лидера терма 3. А потом выбрался текущий лидер в терме 5.

В Raft истина всегда за лидером, а потому реплики с плохим журналом должны отрезать от него часть, чтобы стал префиксом лидера. Это полностью валидно и не ведёт к потере данных, так как такое может происходить только с изменениями, не собравшими кворум, а значит не закоммиченными и не отданными пользователю как успешные. Если изменение собрало кворум, то при выборах нового лидера будет выбран один из узлов этого кворума. Если более половины кластера живо. Но это уже отдельная задача для модуля выборов лидера.

Это краткое изложение сути Raft с упором на синхронную репликацию. Алгоритм достаточно несложный по сравнению с аналогами вроде Paxos. Для понимания данной статьи изложения выше хватит.

4. Асинхронная репликация


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

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

  • транзакционный поток (TX);
  • сетевой поток (IProto);
  • журнальный поток (WAL);
  • репликационный поток (Relay).

Транзакционный поток TX


Это главный поток Tarantool. TX transaction. В нём выполняются все пользовательские запросы. Поэтому Tarantool часто называют однопоточным.

Поток работает в режиме кооперативной многозадачности при помощи легковесных потоков корутин (coroutine), написанных на С и ассемблере. В Tarantool они называются файберами (fiber).

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

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

Сетевой поток IProto


Это поток, задачи которого чтение и запись данных в сеть и из сети, декодирование сообщений по протоколу Tarantool под названием IProto. Это значительно разгружает TX-поток от довольно тяжелой задачи ввода-вывода сети. Пользователю этот поток недоступен никак, но и делать ему в нём всё равно нечего.

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

Журнальный поток WAL


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

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

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

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

При этом пока WAL-поток пишет, TX-поток уже принимает новые транзакции и готовит следующую пачку транзакций. Так Tarantool экономит на системных вызовах. Пользователю этот поток недоступен никак. В схеме асинхронной репликации запись в журнал это единственное и достаточное условие коммита транзакции.

Репликационный поток Relay


Помимо трёх главных потоков Tarantool создает потоки репликации. Они есть только при наличии репликации и называются relay-потоками.

По одному relay-потоку создаётся на каждую подключённую реплику. Relay-поток получает от реплики запрос на получение всех транзакций, начиная с определённого момента. Он исполняет этот запрос в течение жизни репликации, постоянно отслеживая новые транзакции, попавшие в журнал, и посылая их на реплику. Это репликация на другой узел.

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

Например, есть узел 1 с конфигурацией box.cfg{listen = 3313, replication = {localhost:3314}}, и узел 2 с конфигурацией box.cfg{listen = 3314}. Тогда на обоих узлах будут TX-, WAL-, IProto-потоки. На узле 1 в TX-потоке будет жить applier-файбер, который скачивает транзакции с узла 2. На узле 2 будет relay-поток, который отправляет транзакции в applier узла 1.


Relay сделаны отдельными потоками, так как занимаются тяжёлой задачей: чтением диска и отправкой записей журнала в сеть. Чтение диска здесь самая долгая операция.

Идентификация транзакций


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

Идентификация записей в журнале происходит по двум числам: replica ID и LSN. Первое это уникальный ID узла, который создал транзакцию. Второе число LSN, Log Sequence Number, идентификатор записи. Это число постоянно возрастает внутри одного replica ID, и не имеет смысла при сравнении с LSN под другими replica ID.

Такая парная идентификация служит для поддержки мастер-мастер репликации, когда много инстансов могут генерировать транзакции. Для их различия они идентифицируются по ID узла-автора, а для упорядочивания по LSN. Разделение по replica ID позволяет не заботиться о генерировании уникальных и упорядоченных LSN на весь репликасет.

Всего реплик может быть 31, и ID нумеруются от 1 до 31. То есть журнал в Tarantool в общем случае это сериализованная версия 31-го журнала. Если собрать все транзакции со всеми replica ID на узле, то получается массив из максимум 31-го числа, где индекс это ID узла, а значение последний примененный LSN от этого узла. Такой массив называется vclock vector clock, векторные часы. Vclock это точный снимок состояния всего кластера. Обмениваясь vclock, инстансы сообщают друг другу, кто на сколько отстаёт, кому какие изменения надо дослать, и фильтруют дубликаты.

Есть ещё 32-я часть vclock под номером 0, которая отвечает за локальные транзакции и не связана с репликацией.

Реплицированные транзакции на репликах применяются ровно так же, как и на узле-авторе. С тем же replica ID и LSN. А потому продвигают ту же часть vclock реплики, что и на узле-авторе. Так автор транзакций может понять, надо ли посылать их ещё раз, если реплика переподключается, и сообщает свой полный vclock.

Далее следует пример обновления и обмена vclock на трёх узлах. Допустим, узлы имеют replica ID 1, 2 и 3 соответственно. Их LSN изначально равны 0.

Узел 1: [0, 0, 0]Узел 2: [0, 0, 0]Узел 3: [0, 0, 0]

Пусть узел 1 выполнил 5 транзакций и продвинул свой LSN на 5.

Узел 1: [5, 0, 0]Узел 2: [0, 0, 0]Узел 3: [0, 0, 0]

Теперь происходит репликация этих транзакций на узлы 2 и 3. Узел 1 будет посылать их через два relay-потока. Транзакции содержат в себе {replica ID = 1}, и потому будут применены к первой части vclock на других узлах.

Узел 1: [5, 0, 0]Узел 2: [5, 0, 0]Узел 3: [5, 0, 0]

Пусть теперь узел 2 сделал 6 транзакций, а узел 3 сделал 9 транзакций. Тогда до репликации vclock будут выглядеть так:

Узел 1: [5, 0, 0]Узел 2: [5, 6, 0]Узел 3: [5, 0, 9]

А после так:

Узел 1: [5, 6, 9]Узел 2: [5, 6, 9]Узел 3: [5, 6, 9]

Общая схема


Схема асинхронной репликации в такой архитектуре:

  1. Транзакция создаётся в TX-потоке, пользователь начинает её коммит и его файбер засыпает.
  2. Транзакция отправляется в WAL-поток для записи в журнал, записывается, в TX-поток уходит положительный ответ.
  3. TX-поток будит файбер пользователя, пользователь видит успешный коммит.
  4. Просыпается relay-поток, читает эту транзакцию из журнала и посылает её в сеть на реплику.
  5. На реплике её принимает applier-файбер, коммитит её.
  6. Транзакция отправляется в WAL-поток реплики, записывается в её журнал, в TX-поток уходит положительный ответ.
  7. Applier-файбер посылает ответ со своим новым vclock, что всё нормально применилось.

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

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

5. Синхронная репликация


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

  • Если синхронная репликация не используется, то протокол репликации и формат журнала не должны быть изменены никак, полная обратная совместимость. Это позволит обновить существующие кластеры на новый Tarantool и уже потом включить синхронность, по необходимости.
  • Если синхронность используется, нельзя менять формат существующих записей журнала, снова из целей обратной совместимости. Можно только добавлять новые типы записей. Или добавлять новые опциональные поля в существующие типы записей. То же самое про сообщения в репликационных каналах.
  • Нельзя существенно изменить архитектуру Tarantool. Иначе это приведет к изначальной проблеме, когда задача был раздута и растянута на годы. То есть надо оставить основные потоки Tarantool делать то, что они делали, и сохранить их связность в текущем виде. TX-поток должен управлять транзакциями, WAL должен остаться тривиальной системой записи на диск, IProto остается простейшим интерфейсом к клиентам из сети, и relay-потоки должны только читать журнал и посылать транзакции на реплики. Любые последующие оптимизации и перераспределения обязанностей систем должны быть выполнены отдельно, не являться блокировщиками.

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

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

  • создание;
  • начало коммита;
  • ожидание подтверждений;
  • сборка кворума;
  • коммит или отмена транзакции.

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

Создание транзакции


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

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

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

Как это выглядит в коде:

Включить синхронность на существующем спейсе:

box.space[name]:alter({is_sync = true})

Включить синхронность на новом спейсе:

box.schema.create_space(name, {is_sync = true})

Синхронная транзакция на одном спейсе:

sync = box.schema.create_space(   stest, {is_sync = true}):create_index(pk)-- Транзакция из одного выражения,-- синхронная.sync:replace{1}-- Транзакция из двух выражений, тоже-- синхронная.box.begin()sync:replace{2}sync:replace{3}box.commit()

Синхронная транзакция на двух спейсах, один из которых не синхронный:

async = box.schema.create_space(    atest, {is_sync = false}):create_index(pk)-- Транзакция над двумя спейсами, один-- из них  синхронный, а значит вся-- транзакция  синхронная.box.begin()sync:replace{5}async:replace{6}box.commit()

С момента создания и до начала коммита транзакция ведёт себя неотличимо от асинхронной.

Начало коммита транзакции


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

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

Чтобы транзакция не была потеряна при перезапуске инстанса, но и не была завершена до отправки на нужное число реплик, коммит нужно делить на две части. Это запись в журнал самой транзакции с её данными, и отдельно запись в журнал специального маркера COMMIT после того, как кворум собран. Это очень напоминает алгоритм двухфазного коммита. Если репликация не сработала за разумное время, то по таймауту надо писать маркер ROLLBACK.

На самом деле Raft это и подразумевает. Просто он не декларирует, как именно это сохранять в журнал, в каком формате. Столкновение с этими деталями происходит уже в процессе проектирования применительно к конкретной БД.

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

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

Ожидание подтверждений от реплик


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

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

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


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

Сбор кворума


Пока транзакция находится в лимбе, relay-потоки читают её из журнала и высылают на реплики. Реплика получает транзакцию и делает всё тоже самое: пишет её в свой журнал и кладет в свой собственный лимб. Синхронная транзакция или нет, реплика понимает так же, как лидер смотря на синхронность измененных спейсов.

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

После записи в журнал реплика посылает лидеру подтверждение о записи. Подтверждения в Tarantool посылались всегда, для разных подсистем, для мониторинга. И их формат не изменен нисколько.

Подтверждение суть vclock реплики. Он меняется при каждой записи в журнал. Получив это сообщение с vclock реплики, лидер может посмотреть, какой его LSN реплика уже записала в журнал. Например, лидер посылает 3 транзакции на реплику, одной пачкой, {LSN = 1}, {LSN = 2}, {LSN = 3}. Реплика отвечает {LSN = 3} это значит, что все транзакции с LSN <= 3 попали в её журнал. То есть они подтверждены.

На лидере подтверждения от реплик читаются в relay-потоке, оттуда попадают в TX-поток и становятся видны в box.info.replication. Лимб эти уведомления отлавливает и следит, не собрался ли кворум для старейшей ждущей транзакции.

Для отслеживания кворума по мере прогрессирования репликации лимб на лидере строит картину того, какая реплика как далеко зашла. Он поддерживает у себя векторные часы, в которых записаны пары {replica ID, LSN}. Только это не совсем обычный vclock. Первое число идентификатор реплики, а второе последний LSN от лидера, применённый на этой реплике.

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

Для любой синхронной транзакции по её LSN лимб может точно сказать, сколько реплик её применило, просто посчитав, сколько частей этих специальных векторных часов >= этого LSN. Это немного отличается от того, какой vclock пользователи могут видеть в box.info. Но суть очень похожа. В итоге, каждое подтверждение от реплики немного продвигает одну часть этих часов.

Далее следует пример, как обновляется vclock лимба на лидере в кластере из трех узлов. Узел 1 лидер.

Узел 1: [0, 0, 0], лимб: [0, 0, 0]Узел 2: [0, 0, 0]Узел 3: [0, 0, 0]

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

Узел 1: [5, 0, 0], лимб: [5, 0, 0]Узел 2: [0, 0, 0]Узел 3: [0, 0, 0]

В vclock лимба продвинулась первая компонента, так как эти 5 транзакций были применены на узле с replica ID = 1, и совершенно не важно, лидер это или нет. Лидер тоже участник кворума.

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

Узел 1: [5, 0, 0], лимб: [5, 3, 4]Узел 2: [3, 0, 0]Узел 3: [4, 0, 0]

Стоит обратить внимание, как обновился vclock лимба. Он фактически является столбиком в матрице vclock-ов. Так как узел 2 подтвердил LSN 3, в лимбе это отражено как LSN 3 во второй компоненте. Так как узел 3 подтвердил LSN 4, в лимбе это LSN 4 в третьей компоненте. Так, глядя на этот vclock, можно сказать, на какой LSN есть кворум.

Например, здесь на LSN 4 есть кворум два узла: 1 и 3, так как они этот LSN подтвердили. А на LSN 5 кворума ещё нет этот LSN есть только на узле 1. Под кворумом подразумевается 50 % + 1, то есть два узла.

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

Коммит транзакции


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

Транзакции упорядочены по LSN, поэтому в какой-то момент встретится транзакция, у которой кворума ещё нет, и 100 % у следующих транзакций его тоже нет. Либо лимб окажется пуст. Для последней собранной транзакции с максимальным LSN лимб пишет в журнал запись COMMIT. Это также автоматически подтвердит все предыдущие транзакции, поскольку репликация строго последовательна. То есть если транзакция с LSN L собрала кворум, то все транзакции с LSN < L тоже его собрали. Это экономит количество операций записи в журнал и место в нём.

После записи COMMIT все завершённые транзакции отвечаются пользователям как успешные и удаляются из памяти.

Рассмотрим пример, как лимб сворачивается. Пусть в кластере 5 узлов. Лидер третий. В лимбе накопились транзакции в ожидании кворума.


Коммитить пока ничего нельзя: самая старая транзакция имеет LSN 1, который подтверждён только лидером. Пусть часть реплик подтвердила несколько LSN-ов.


Теперь LSN 1 подтвержден узлами 1, 3, 4, 5 то есть это больше половины и кворум собран, можно коммитить. Следующий LSN 2, на него только два подтверждения, от узлов 3 и 4. Его коммитить пока нельзя, как и все последующие. Значит в журнал надо записать COMMIT LSN 1.


Спустя ещё время получены новые подтверждения от реплик.


Теперь кворум есть на LSN 5 его подтвердили все. Так как везде LSN >= 5. На LSN 6 кворума нет, он есть только на двух узлах (3-й и 5-й), а это меньше половины. Значит коммитить можно все LSN <= 5.


Стоит обратить внимание, как одна запись COMMIT завершает сразу 4 транзакции.

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

Отмена транзакции


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

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

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

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

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

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

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

К сожалению, в распределённых системах нет никаких 100 % гарантий ни на что. Можно лишь увеличивать шансы на успех, наращивать надёжность, но идеального решения физически невозможно создать.

Смена лидера


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

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

При выборах надо выбирать новым лидером узел с максимальным LSN относительно старого лидера. Такой узел точно будет содержать последние закоммиченные транзакции старого лидера. То есть он участвовал в последнем кворуме.

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

Рассмотрим пример. Есть 5 узлов. Один из них лидер, и он выполнил транзакцию по обновлению ключа A в значение 20 вместо старого 10. На эту транзакцию он собрал кворум из трёх узлов, закоммитил её, ответил клиенту.


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


Новым лидером может стать только один из узлов под синим контуром. Если лидером сделать один из узлов под красным контуром, то он форсирует на остальных состояние {a = 10}, что приведёт к потере транзакции. Несмотря на то, что на неё был собран кворум, произошел коммит и более половины кластера всё ещё цело.

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

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

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

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

Интерфейс


Функции для работы с синхронной репликацией в Tarantool делятся на две группы: для управления синхронностью и для выборов лидера. Для включения синхронности на спейсе нужно указать опцию is_sync со значением true при его создании или изменении.

Создание:

box.schema.create_space('test', {is_sync = true})

Изменение:

box.space[test]:alter({is_sync = true})

Теперь любая транзакция, меняющая синхронный спейс, становится синхронной. Для настройки параметров синхрона есть глобальные опции:

box.cfg{    replication_synchro_quorum = <count or expression>,    replication_synchro_timeout = <seconds>,    memtx_use_mvcc_engine = <boolean>}

Replication_synchro_quorum это количество узлов, которые должны подтвердить транзакцию для её коммита на лидере. Можно задать его как число, а можно как выражение над размером кластера. К примеру, каноническая форма box.cfg{replication_synchro_quorum = N/2 + 1}, которая означает кворум 50 % + 1. Tarantool вместо N подставляет количество узлов, известных лидеру. Кворум можно выбрать и больше канонического, если нужны более сильные гарантии. Но выбирать половину или меньше уже небезопасно.

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

Memtx_use_mvcc_engine позволяет избавиться от грязных чтений. Дело в том, что в Tarantool грязные чтения существовали всегда, так как изменения транзакций (асинхронных) становились видны ещё до записи в журнал. Это стало серьёзной проблемой с появлением синхронной репликации, так как вступить в них стало сильно проще. Но просто взять и выключить грязные чтения по умолчанию нельзя, это может сломать совместимость с существующими приложениями. Кроме того, для их выключения требуется выполнение большого количества дополнительной работы в процессе выполнения транзакций, что может повлиять на производительность. Поэтому выключение грязных чтений опционально и контролируется этой опцией. По умолчанию грязные чтения включены!

Для выборов лидера можно пользоваться автоматикой, освещённой в отдельной статье. А можно выбирать вручную или своей собственной автоматикой через API Tarantool.

Для поиска нового лидера надо знать, у кого самый большой LSN от старого лидера. Чтобы его найти, следует воспользоваться box.info.vclock, где указан весь vclock узла, и в нём надо найти компоненту старого лидера. Ещё можно попытаться искать узел, где все части vclock больше или равны всех частей vclock на других узлах, но можно наткнуться на несравнимые vclock.

После нахождения кандидата следует позвать на нем box.ctl.clear_synchro_queue(). Пока эта функция не вернёт успех, лидер не может начать делать новые транзакции.

Отличия от Raft


Идентификация транзакций


Главное отличие от Raft идентификация транзакций. Происходит отличие из формата журнала. Дело в том, что в Raft журнал един. В нём нет векторности. Записи журнала Raft имеют формат вида {{key = value}, log_index, term}. В терминологии Tarantool это изменения транзакции и её LSN. Tarantool не хранит термы в каждой записи, и в нём нет единой последовательности log_index нужно хранить replica ID. В Tarantool расчёт LSN идёт индивидуально на каждом узле для транзакций его авторства.

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

Сложность возникает, когда лидер меняется. Тогда нужно во всём кластере перевести отсчёт LSN на другую часть vclock, с отличным replica ID. Для этого новый лидер завершает все транзакции старого лидера, захватывает лимб транзакций и начинает генерировать свои собственные транзакции. На репликах произойдёт то же самое: они получат от нового лидера COMMIT и ROLLBACK на транзакции старого лидера, и потом новые транзакции с другим replica ID. Лимбы всего кластера переключаются автоматически, когда их опустошили, и начали давать новые транзакции с другим replica ID.

Это выглядит почти как если бы в кластере был 31 протокол Raft, работающий поочередно.

Нет отката журнала


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

В Tarantool отката журнала нет, так как он redo, а не undo. Кроме того, архитектурой не предусмотрен откат LSN. Если в кластере появляются такие реплики, то нет выбора кроме как их удалить и подключать как новые, скачать все данные с лидера заново. Это называется rejoin.

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

Заключение


Синхронная репликация в Tarantool доступна с версии 2.5, а автоматические выборы с версии 2.6. На данный момент эта функциональность находится в бета-версии, то есть ещё не обкатана в реальных системах, а интерфейсы и их поведение ещё могут измениться. И пока существующая реализация полируется, есть планы по её оптимизации и расширению. Оптимизации главным образом технические.

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

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

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

Транзакции в бинарном протоколе
С момента создания Tarantool в нём не было возможно делать долгие транзакции из более чем одного выражения прямо по сети, используя только удалённый коннектор к Tarantool. Для любой операции сложнее, чем один replace/delete/insert/update, требовалось написать код на Lua, который бы делал нужные операции в одной транзакции, и вызывать этот код как функцию.

В данный момент запланирована реализация транзакций прямо в протоколе. Со стороны клиента на Lua это будет выглядеть, например, так:

c = netbox.connect(host)c:begin()c.space.test1:replace{100}c.space.test2:delete({5})c:commit()

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

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

Запланировано расширение интерфейса коммита, чтобы файбер не блокировался. Выглядеть будет, например, вот так:

box.begin()box.space.test1:replace{100}box.commit({is_lazy = true})box.begin()box.space.test2:replace{200}box.space.test3:replace{300}box.commit({is_lazy = true})

Оба box.commit() вернут управление сразу, а транзакция попадёт в журнал и будет закоммичена в конце итерации цикла событий Tarantool (event loop). Такой подход не только может уменьшить задержку на ответ клиенту, но и лучше использовать ресурсы WAL-потока, так как больше транзакций сможет попасть в одну пачку записи на диск к концу итерации цикла событий.

Кроме того, касательно синхронных транзакций иногда может быть удобно сделать синхронным не целый спейс, а только определённые транзакции, даже над обычными спейсами. Для такого запланировано добавлении ещё одной опции в box.commit() is_sync. Выглядеть будет так: box.commit({is_sync = true}).

Мониторинг
В данный момент нет способа узнать, сколько синхронных транзакций ожидают коммита (находятся в лимбе). Ещё нет способа узнать, каково значение кворума, если пользователь использовал выражение в replication_synchro_quorum. Например, если было задано N/2 + 1, то в коде узнать фактическое значение кворума нельзя никаким вменяемым способом (но способ есть).

Для устранения этих неизвестностей будет выведена отдельная функция мониторинга box.info.synchro.
Подробнее..

Clarion. Процесс миграции Clarion приложения на Microsoft SQL 2019

30.05.2021 18:13:46 | Автор: admin

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

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

Проблематика

Главная на мой взгляд проблема и сложность это работа программы только с нативной СУБД Clarion, доступ к данным при таком подходе очень неудачный, требуется большой объем кода для написания даже простейших задач, которые решаются отправкой на сервер простейшего Update или Insert в Clarion это десятки строчек кода по открытию файла, получению доступа к инфе и его последующего закрытия. Ниже пример:

       Access:Agent.Open !Открываем файл       Access:Agent.UseFile!Открываем файл       clear(AGN:Record)!Делаем очистку записи на всякий случай       AGN:ID_AGENT = some_id !Присваиваем ключу значение       set(AGN:BY_ID,AGN:BY_ID)!Устанавливаем "каретку" на первое значение ключа       next(agent)!Встаем на первую запись удовлетворяющую ключу       IF errorcode() or AGN:ID_AGENT <> some_id!Проверяем не вышла ли каретка за область ключа            RETVAL = 'Контрагент не найден'!Выкидываем ошибку          ELSE            RETVAL = AGN:N_AGENT!Возвращаем имя агента       .       Access:Agent.Close  !Закрываем файл

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

select agent.name where id = some_id

Задача

Первые задачи передо мной встали очень грустной перспективой, одна из них звучала как "написать модуль выгрузки данные в 1С Бухгалтерию", я сразу представил алгоритм длиной в пару тысяч строк, который можно заменить на один запрос SQL, и вопрос перехода на SQL уже был решен. Речь конечно не о полном переходе, а только о частичной замене части таблиц на версию SQL и поэтапной миграции системы в SQL.

Характеристики системы

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

Общее количество пользователей: около 80

Общее количество таблиц: около 250

Сфера деятельности: Торговля + Сфера обслуживания (Салоны красоты)

Подразделения:

3 Салона красоты

5 Подразделений торговых предприятий - мелкооптовая торговля

Используемые инструменты

  • Самодельная программа миграции

  • DCT2SQL

  • Cldump

  • BULK insert

  • UltimateSQL & Ultimate Debug

Самодельная программа миграции

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

DCT2SQL

Данный компонент позволяет генерировать скрипты для экспорта структуры БД из Dictionary в SQL, поддерживает экспорт всех возможных типов данных, также экспортирует индексы и foreign keys. Очень удобно работает, все импортируется в пару кликов. Данные скрипты я храню в таблице миграций.

Можно скачать на Github - https://github.com/RobertArtigas/DCT2SQL

Также есть обучающие ролики на youtube по работе и правильной выгрузке. Вообще там очень много инфы в этих роликах по миграции на SQL.

https://www.youtube.com/watch?v=MjMgQYMc_xY

https://www.youtube.com/watch?v=bAolfvrz2oE&t=7067s

CLDUMP

Данная программа конвертирует данные из *.dat файла в csv таблицы готовые для загрузки через скрипт BULK. Достоинство этой программы - скорость. Она может сконвертировать таблицу накладных за 10 лет за 15-20 секунд. Главная проблема данной утилиты в том, что она доступна только в репозиториях Linux, в частности debian. Пришлось на основе этой команды создать микро-сервис, который на входе принимает post запрос, а на выходе выдает ссылку для скачивания данного файла в виде csv таблицы.

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

Программу cldump можно скачать командой в любой debian подобной системе:

apt-get install cldump

BULK insert

За доли секунд втягивает таблицу из csv в SQL. В данном случает, т.к. связки данных уже настроены на существующие ID, но при этом надо чтобы работал автоинкремент, поэтому его временно надо отключить, также потратил довольно много времени чтобы подобрать подходящие разделители:

BULK INSERT dbo.%table_name%FROM table_name.csv WITH ( FORMAT = 'CSV', FIELDQUOTE = '', FIRSTROW = 1, FIELDTERMINATOR = '0x3b', ROWTERMINATOR = '0x0a', CODEPAGE='65001',TABLOCK, KeepIdentity)

UltimateSQL & Ultimate Debug

Данные компоненты позволяют загружать данные из SQL в QUEUE примерно таким образом:

SQL_Result = sql.query('select id, path_to_result from dbo.export_tasks as et where (status_complete = 0 or status_complete = 2) and export_table_id = '& exp:id,qexport_tasks)

Выполнять запросы без возвращаемых значений:

sql.Query('Update export_tasks set status_complete = 2 where id = ' & qexport_tasks.id)

Есть отличное описание как использовать на youtube:

https://www.youtube.com/watch?v=RVit-5RPncs&t=2259s

Также при установке внутри шаблонов есть "пасхалка" от автора, как решить квест описывается по ссылке:

https://clarionhub.com/t/need-some-help-with-ultimatesql-error-when-trying-to-include-it-in-my-project/4182

Подробнее..
Категории: Data engineering , Субд , Mssql , Database , Clarion 11

Категории

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

  • Имя: Макс
    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