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

Почему Дебеизум?

Какие причины могут побудить вас интегрировать этот инструмент как часть вашей системы?

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

Однако, когда мы начали внедрять нашу платформу Treasury, которая требовала поддержки ACID, мы перешли на MySQL. Впоследствии мы повторно оценили пригодность решения CDC, что привело к выбору Дебезиума.

Debezium предлагает решение для фиксации и реагирования на изменения в базах данных вашей системы в режиме реального времени. Благодаря беспрепятственному подключению к различным ядрам баз данных, таким как MySQL, PostgreSQL, MongoDB и другим.

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

Введение

Давайте начнем с объяснения того, что такое Дебезиум, и понимания концепции CDC.

CDC, что означает Change Data Capture, представляет собой программный шаблон, используемый для мониторинга и регистрации изменений данных, позволяя другому программному обеспечению реагировать соответствующим образом.

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

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

  1. Binlog. Двоичный журнал (binlog) в MySQL представляет собой хронологическую запись изменений данных, фиксирующую все операции записи, выполняемые в базе данных, необходимые для репликации, восстановления на определенный момент времени и целостности данных. Binlog также имеет функцию сохранения, которая определяет продолжительность хранения журналов, обеспечивая баланс между потребностями восстановления и эффективностью хранения.
  2. Сервер Debezium. Debezium предоставляет готовое к использованию приложение, которое передает события изменений из исходной базы данных в инфраструктуру обмена сообщениями, такую ​​как Kafka. Для потоковой передачи событий изменений в Kafka мы развертываем соединители Debezium через Kafka Connect.
  3. Коннектор Debezium. Коннектор Debezium считывает binlog, создает события изменения для операций INSERT, UPDATE и DELETE на уровне строк и отправляет события изменения в темы Kafka.
  4. Kafka — высокопроизводительная распределенная система очередей сообщений с публикацией и подпиской, которая эффективно обрабатывает потоки данных в реальном времени, что делает ее идеальной для архитектур, управляемых событиями, и обработки больших данных.
  5. Потребители Kafka. Потребители Kafka обрабатывают данные в режиме реального времени внутри Kafka, обеспечивая преобразования посредством потоковой обработки и плавно интегрируясь с моделью Kafka. Это упрощает построение масштабируемых и отказоустойчивых конвейеров данных.

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

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

Чтобы фиксировать изменения, мы настроили соединитель Debezium, специально настроенный для фиксации изменений, происходящих в этой таблице.

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

Конфигурации и другие конфигурации

Как только Debezium развертывается в среде, начинается самое интересное.

Чтобы начать фиксировать изменения в базе данных, вам необходимо создать соединитель Debezium.

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

Ниже приведен пример базовой конфигурации соединителя, которая была у нас при первоначальном использовании Debezium (v1.6).

{
   "name":"debezium-packages-connector-sandbox",
   "connector.class":"io.debezium.connector.mysql.MySqlConnector",
   "include.schema.changes":"false",
   "decimal.handling.mode":"precise",
   "transforms":"Reroute,unwrap",
   "transforms.unwrap.type":"io.debezium.transforms.ExtractNewRecordState",
   "transforms.Reroute.type":"io.debezium.transforms.ByLogicalTableRouter",
   "transforms.Reroute.topic.regex":"(.*)payouts_sandbox_qa(.*)",
   "transforms.Reroute.topic.replacement":"packages_sandbox",
   "transforms.unwrap.add.fields":"",
   "database.server.name":"packages_sandbox",
   "database.user":"**********",
   "database.server.id":"**********",
   "database.port":"**********",
   "database.hostname":"**********",
   "database.password":"**********",
   "database.history.kafka.bootstrap.servers":"**********",
   "database.history.kafka.topic":"debezium_history_packages_sandbox",
   "database.whitelist":"payouts_sandbox_qa",
   "table.whitelist":"payouts_sandbox_qa.packages",
   "snapshot.mode":"initial",
   "snapshot.new.tables":"parallel"
}

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

  1. database.whitelist- Список регулярных выражений, разделенных запятыми, которые соответствуют именам баз данных, для которых фиксируются изменения. Коннектор не фиксирует изменения ни в одной базе данных, имя которой отсутствует в database.whitelist.
  2. table.whitelist — список регулярных выражений, разделенных запятыми, которые соответствуют полным идентификаторам таблиц, изменения которых вы хотите зафиксировать.
  3. transforms.Reroute.topic.regex — регулярное выражение, которое преобразование применяется к каждой записи события изменения, чтобы определить, следует ли направить ее в конкретную тему.
  4. transforms.Reroute.topic.replacement — регулярное выражение, представляющее имя целевой темы. Преобразование направляет каждую соответствующую запись в тему, определенную этим выражением.

Проще говоря, мы настраиваем packages-connector для установления соединения с базой данных payouts_sandbox_qa и отслеживания изменений в таблице payouts_sandbox_qa.package. При обнаружении изменений он проверяет, произошли ли они из таблицы, соответствующей шаблону регулярного выражения (.*)payouts_sandbox_qa(.*), и если да, то эти изменения затем экспортируются в тему Kafka с именем packages_sandbox.

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

Что мы можем сделать со всеми этими данными?

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

В нашем случае мы экспортируем наши данные в два основных места назначения:

  1. Elasticsearch 🔍 — Мы потребляем события, обогащаем их дополнительными данными из различных сервисов нашей системы, а затем экспортируем их в Elasticsearch. Это позволяет нам эффективно искать и получать к ним доступ через наш пользовательский интерфейс.
  2. Snowflake ❄️ — Мы обрабатываем события, а затем экспортируем данные в Snowflake, наш облачный инструмент для работы с большими данными. Snowflake в основном служит платформой для создания различных отчетов для наших продавцов, таких как отчеты о расчетах, платежах и другие типы отчетов.

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

Не так просто, как кажется

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

Мы столкнулись с несколькими незначительными проблемами, часто возникающими из-за неправильной настройки разъемов. Однако самая важная тема, которую мы обсудим, — это когда коннектор теряет смещение binlog.

Как упоминалось ранее, каждый соединитель можно настроить для отслеживания изменений в определенных таблицах базы данных. Это работает так: соединитель фиксирует изменения, связанные с этой конкретной таблицей, в журнале binlog.

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

Как видно на снимке экрана пользовательского интерфейса Kafka ниже, в специальной теме, посвященной смещениям Debezium, мы видим, что самое последнее сообщение о смещении для соединителя packages_sandbox указано как 1435109 (pos).

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

  1. Бинлог содержит изменения из всех таблиц MySQL, а не только из таблиц, для которых мы настроили соединитель для фиксации изменений.
  2. MySQL также имеет настройку хранения binlog, которая означает, что по истечении определенного периода времени, например 7 дней, binlog меняется. Это означает, что изменения, которые были записаны в него, больше не доступны.

Как эти два фактора сочетаются друг с другом? Позвольте мне поделиться примером.

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

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

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

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

По истечении двух недель в таблицу packages не вносилось никаких обновлений или вставок.

Внезапно наше соединение с MySQL прерывается, что приводит к перезапуску Debezium из-за потери соединения с MySQL.

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

io.debezium.DebeziumException: The connector is trying to read binlog starting at SourceInfo [...], but this is no longer available on the server.

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

Не зная, что делать в этой ситуации, коннектор выдает исключение и выходит из строя.

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

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

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

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

Повышение надежности и наблюдаемости

Со временем мы начали замечать, что в нашей производственной среде разъемы теряют смещения.

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

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

Наблюдаемость

Журналирование

Coralogix служит нашей основной платформой ведения журналов. Исходная структура журналов Debezium состояла из длинных неразбираемых строк.

Чтобы обеспечить эффективный поиск в Coralogix, нам пришлось преобразовать журналы в объекты JSON.

Дебезиум использует библиотеку log4j (не волнуйтесь, это после того, как уязвимость была устранена). Чтобы настроить его регистратор, нам нужно было перезаписать файл log4j.properties, который определяет формат журналов.

# This file will override the default configuration of logs format

kafka.logs.dir=logs

log4j.rootLogger=INFO, stdout, appender

# Disable excessive reflection warnings - KAFKA-5229
log4j.logger.org.reflections=ERROR

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.threshold=INFO
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern={"timestamp":"%d{ISO8601}","level":"%p","thread":"%t","logger":"%c","message":"%m","connector_type":"%X{dbz.connectorType}","connector_name":"%X{dbz.connectorName}","connector_context":"%X{dbz.connectorContext}"}%n


log4j.appender.appender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.appender.DatePattern='.'yyyy-MM-dd-HH
log4j.appender.appender.File=${kafka.logs.dir}/connect-service.log
log4j.appender.appender.layout=org.apache.log4j.PatternLayout
log4j.appender.appender.layout.ConversionPattern={"timestamp":"%d{ISO8601}","level":"%p","thread":"%t","logger":"%c","message":"%m","connector_type":"%X{dbz.connectorType}","connector_name":"%X{dbz.connectorName}","connector_context":"%X{dbz.connectorContext}"}%n

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

Дополнительную информацию о логировании можно увидеть здесь.

Ниже вы можете увидеть наши последние журналы JSON, отображаемые в Coralogix.

— —

Показатели

Мы также обнаружили, что Debezium включил в свою документацию способ его мониторинга. Это предполагает использование JMX метрик, предоставленных Kafka, которые позже можно экспортировать в Prometheus и Grafana.

Поскольку мы уже использовали Prometheus и Grafana, мы сразу же ухватились за подвох.

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

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

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

sum(debezium_metrics_Connected{context="streaming", env="eks-prd-apps", name=~"balances_test_incoming_operation.*"}) or vector(0)

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

Надежность

Здесь в игру вступают передовые концепции.

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

В Debezium уже была логика пульса (v1.6), но она нам не помогла. Это связано с тем, что существующий пульс был сосредоточен на Kafka, а не на самой базе данных.

Однако удача была на нашей стороне, так как в более поздних версиях было введено новое поле: heartbeat.action.query, и вот что оно делает —

«Указывает запрос, который соединитель выполняет в исходной базе данных, когда соединитель отправляет контрольное сообщение»

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

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

Излишне говорить, что мы были в восторге от результата!

Теперь, чтобы решить проблему смещения, мы решили следующее.

Для каждого микросервиса с таблицей, из которой мы фиксировали изменения, мы создали специальную таблицу с именем debezium_heartbeat. Эта таблица зарезервирована для heartbeat.action.query, который мы планировали настроить для каждого соединителя.

Определение MySQL таблицы debezium_heartbeat выглядит следующим образом:

CREATE TABLE IF NOT EXISTS debezium_heartbeat (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    connector_name VARCHAR(255) NOT NULL,
    last_heartbeat TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    UNIQUE KEY connector_name_index (connector_name)
);

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

INSERT INTO <database_name>.debezium_heartbeat (connector_name, last_heartbeat) VALUES ('<connector_name>', NOW()) ON DUPLICATE KEY UPDATE last_heartbeat = NOW()

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

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

Наша конфигурация соединителя теперь выглядит так, как показано в v2.3.0.

{
   "name":"debezium-packages-connector-sandbox",
   "connector.class":"io.debezium.connector.mysql.MySqlConnector",
   "topic.heartbeat.prefix":"debezium_heartbeat",
   "topic.prefix":"packages_sandbox", // previously "database.server.name"
   "include.schema.changes":"false",
   "decimal.handling.mode":"precise",
   "transforms":"Reroute,unwrap",
   "transforms.unwrap.type":"io.debezium.transforms.ExtractNewRecordState",
   "transforms.Reroute.type":"io.debezium.transforms.ByLogicalTableRouter",
   "transforms.Reroute.topic.regex":"(.*)payouts_sandbox_qa.(.*)",
   "transforms.Reroute.topic.replacement":"$2_sandbox",
   "transforms.unwrap.add.fields":"",
   "database.user":"**********",
   "database.server.id":"**********",
   "database.port":"**********",
   "database.hostname":"**********",
   "database.password":"**********",
   "database.include.list":"payouts_sandbox_qa", // previously "database.whitelist"
   "table.include.list":"payouts_sandbox_qa.packages,payouts_sandbox_qa.debezium_heartbeat", // previously "table.whitelist"
   "schema.history.internal.kafka.bootstrap.servers":"**********", // previously "database.history.kafka.bootstrap.servers"
   "schema.history.internal.kafka.topic":"debezium_history_packages_sandbox", // previously "database.history.kafka.topic"
   "heartbeat.interval.ms":"3600000",
   "heartbeat.action.query":"INSERT INTO payouts_sandbox_qa.debezium_heartbeat (connector_name, last_heartbeat) VALUES ('debezium-packages-connector-sandbox', NOW()) ON DUPLICATE KEY UPDATE last_heartbeat = NOW()",
   "snapshot.mode":"initial",
   "snapshot.new.tables":"parallel"
}

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

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

  1. payouts_sandbox_qa.packages
  2. payouts_sandbox_qa.debezium_heartbeat

Те, у кого острый глаз, заметят, что в transforms.Reroute.topic.regex и transforms.Reroute.topic.replacement были внесены изменения.

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

Это означает, что когда изменение фиксируется в одной из таблиц, будут иметь место следующие сценарии:

  1. Изменение в payouts_sandbox_qa.packages$1 будет установлено как пустая строка, а $2 будет установлено как packages, в результате чего событие будет перенаправлено в тему packages_sandbox.
  2. Изменение в payouts_sandbox_qa.debezium_heartbeat$1 будет установлено как пустая строка, а $2 будет установлено как debezium_heartbeat, в результате чего событие будет перенаправлено в тему debezium_heartbeat_sandbox.

Учитывая, что тема packages_sandbox уже создана в Kafka, важно понимать, что темы Heartbeat должны быть созданы до этого изменения. В противном случае Debezium выдаст ошибку.

Кроме того, необходимо создать дополнительные темы, поскольку Debezium также отправляет сообщения Kafka в следующую тему:

__debezium-heartbeat.<topic.prefix>

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

Более того, этот соединитель экспортирует события в две отдельные темы Kafka: одну для пульса, а другую для пакетов.

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

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

Заключение

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

  1. Дебезий – сложный компонент, который нельзя недооценивать. Я затронул лишь часть тем, в Debezium есть гораздо больше, например режимы моментальных снимков, преобразования, функции безопасности и т. д. Он требует внимания и осторожности, а обучение требует времени. Однако, освоив его, вы поймете, насколько он мощен.
  2. Сообщество играет жизненно важную роль. Я обнаружил специальный форум Debezium, где свободно делятся знаниями и ежедневно задаются вопросы. Этот ресурс оказался для нас неоценимым в путешествии (ссылка на форум — https://debezium.zulipchat.com).
  3. Документация Debezium является всеобъемлющей, поэтому важно провести тщательный поиск по различным разделам. Обязательно выберите используемую версию и конкретный тип соединителя, связанный с вашей базой данных. Много раз мы сталкивались с проблемами, которые в итоге были освещены в документации (ссылка на документацию — https://debezium.io/documentation)
  4. Кроме того, крайне важно поддерживать тесные отношения с коллегами, особенно с отделами эксплуатации и обработки данных. Наше путешествие было бы невозможным без их опыта и рекомендаций, особенно в процессе обновления и понимания конвейеров данных. Их идеи помогли нам избежать ошибок и были поистине неоценимы.
  5. Я настоятельно рекомендую использовать Monorepo, который объединяет все ваши коннекторы в одном проекте. Если вы имеете дело с несколькими коннекторами, использование подхода Monorepo может значительно упростить ваш рабочий процесс — поверьте мне на слово!

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

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

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