Всем привет! Я Станислав Бушуев, инженер-программист Semrush. Сегодня я хочу поделиться идеями, как можно реализовать синхронизацию данных между разными хранилищами.
Такие задачи иногда возникают на работе, например, при удалении пользовательских данных в рамках Общего регламента по защите данных ( GDPR ) и Закон Калифорнии о конфиденциальности потребителей ( CCPA ).
Следовать этим законам несложно, если вы поддерживаете средний веб-сайт или небольшой продукт. Скорее всего, в этих случаях используется один из популярных фреймворков, а пользовательская таблица хранится в известной базе данных (MySQL, PostgreSQL).
Вы получили запрос на удаление всех пользовательских данных? Без проблем! Удалите строку в таблице, и все готово.
Не считая логов, следов ошибок, резервных копий и прочих интересных мест. Предполагаем, конечно, что все настроено правильно и персональные данные пользователей скрыты звездочками или чем-то еще.
В нашем случае ситуация немного сложнее.
В течение 13 лет в Семраш создали более 50 инструментов, каждый из которых поддерживается десятками команд разработчиков.
Несколько лет назад каждая команда вынесла код своего инструмента в отдельный микросервис; мы получаем пользовательские данные от пользовательского сервиса.
Но мы так или иначе храним всю информацию, которая необходима для работы инструмента.
Таким образом, перед нами встал вопрос: как синхронизировать хранилища данных микросервиса и пользовательского сервиса.
Методы синхронизации
1. Время от времени мы можем обращаться в службу поддержки пользователей в соответствии с ОТДЫХ API :Можно даже сразу отправить список пользователей на проверку, но это как-то странно.$ curl '< http://user-service.internal.net/api/v1/users/42 >' [ { "id": 42, "registration_date": "2015-03-08 01:00:00", "email": "[email protected]", "name": "John", .
Удаление пользователей происходит не так часто, чтобы вызвать DDoS-атаки на сервис.
2. Событийно-ориентированная архитектура - еще один подход к решению нашей проблемы.
Здесь появляются две сущности: генератор сообщений (Publisher) и подписчик (Subscriber), который читает канал событий (тема).
Вы можете отказаться от сущности подписчика и настроить для всех микрослужб API конечной точки, к которому пользовательская служба будет отправлять запросы при возникновении нового события.
В этом случае вам необходимо согласовать протоколы взаимодействия API: REST, JSON-RPC, gRPC, GraphQL, OpenAPI или что-то еще.
Кроме того, необходимо хранить файлы конфигурации микросервисов, куда отправлять запросы и самое главное: что делать, если запрос не доходит до микросервиса после третьего повторения? Преимущества этой архитектуры:
- Асинхронная автоматическая синхронизация хранилищ данных.
- Нагрузка на пользовательский сервис увеличивается незначительно, поэтому в канал событий добавляем только асинхронную запись событий.
- Синхронизация данных из различных хранилищ осуществляется отдельно от (уже загруженного) пользовательского сервиса.
- Недостаток, вытекающий из первого преимущества: несогласованность данных между пользовательскими сервисами и другими микросервисами.
- Никаких транзакций: генерируются простые сообщения.
- Обратите внимание, что сообщения в очереди могут повторяться.
На мой взгляд, универсальных решений не существует. По этой теме будет полезно прочитать о Теорема CAP .
Реализация событийно-ориентированной архитектуры на примере Pub/Sub из Google Cloud
Альтернатив много: Kafka, RabbitMQ, но наша команда выбрала решение от Pub/Sub Google Cloud, так как мы уже используем Google Cloud (подробнее можно прочитать в статье моего коллеги Никиты Шальнова: Что такое неизменяемая инфраструктура ), и проще настроить тот же Kafka или RabbitMQ.В нашем случае Publisher — это пользовательский сервис, а Subscriber — микросервис команды конкретного инструмента.
Подписчиков может быть любое количество (например, Подписка).
Учитывая, что у Semrush большое количество инструментов и команд, очередь с подписчиками нам подойдет идеально:
- Каждый читает очередь с той частотой, которая ему нужна.
- Кто-то может установить конечную точку, и тема будет вызывать ее сразу при появлении сообщения (если вам нужно мгновенно получать новые сообщения).
- Даже экзотические варианты, например, откат базы инструмента из старой резервной копии, не вызывают проблем: просто перечитываем сообщения из темы (стоит отметить, что нужно учитывать идемпотентность запросов, и возможно вам стоит читать тему не с начала, а с какого-то момента).
- Абонент предоставляет протокол REST, но для упрощения разработки также имеются клиенты для различных языков программирования: Go, Java, Python, Node.js, C#, C++, PHP, Ruby.
Пример реализации
Создание темы и подписчика: gcloud pubsub topics create topic
gcloud pubsub subscriptions create subscription --topic=topic
Более подробную информацию можно найти в документация .
Создание пользователя для чтения темы: gcloud iam service-accounts create SERVICE_ACCOUNT_ID \
--description="DESCRIPTION" \
--display-name="DISPLAY_NAME"
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:SERVICE_ACCOUNT_ID@PROJECT_ID.iam.gserviceaccount.com" \
--role="pubsub.subscriber"
gcloud iam service-accounts keys create key-file \
[email protected]
Скачанный ключ в формате json необходимо сохранить и отправить в сервис.
Не забывайте о правилах обращения с секретами! Об этом знают все, а мои коллеги из службы безопасности знают немного больше.
Дайте мне знать в комментариях, если вы считаете тему полезной и интересной для нашей следующей статьи.
Пользователь для публикации сообщений создается аналогично, за исключением роли: --role="pubsub.subscriber" → --role="pubsub.publisher".
Например, давайте возьмем один из наших микросервисов, работающий на Python с Celery. Для сообщений от пользовательского сервиса существует схема, описанная с помощью протокольных буферов.
import json
import os
import celery
from google.cloud import pubsub_v1
from google.oauth2 import service_account
from user_pb2 import UserEventData
PUBSUB_SERVICE_ACCOUNT_INFO = json.loads(os.environ.get('PUBSUB_SERVICE_ACCOUNT', '{}'))
PUBSUB_PROJECT = 'your project'
PUBSUB_SUBSCRIBER = 'subscription'
@celery.shared_task
def pubsub_synchronisation() -> None:
credentials = service_account.Credentials.from_service_account_info(
PUBSUB_SERVICE_ACCOUNT_INFO, scopes=['< https://www.googleapis.com/auth/pubsub >']
)
with pubsub_v1.SubscriberClient(credentials=credentials) as subscriber:
subscription_path = subscriber.subscription_path(PUBSUB_PROJECT, PUBSUB_SUBSCRIBER)
response = subscriber.pull(request={"subscription": subscription_path, "max_messages": 10000})
ack_ids, removed_user_ids = [], []
for msg in response.received_messages:
user_event_data = UserEventData()
user_event_data.ParseFromString(msg.message.data)
removed_user_ids.append(user_event_data.Id)
ack_ids.append(msg.ack_id)
# Here you can do everything with removed users :)
subscriber.acknowledge(request={"subscription": subscription_path, "ack_ids": ack_ids})
А задачу запускаем раз в пять минут, так как удаление пользователей — не такая уж и частая операция: CELERY_BEAT_SCHEDULE = {
'pubsub_synchronisation': {
'task': 'tasks.pubsub_ubs_synchronisation',
'schedule': timedelta(minutes=5)
},
Аналогичным образом реализован пример публикации сообщений в теме на Python. Используйте PublisherClient вместо SubscriberClient и вызывайте публикацию вместо извлечения.
В результате происходит синхронизация удаления пользователей в соответствии с законами GDPR/CCPA. Например, в 7:37 утра произошло массовое удаление учетных записей из службы хранения учетных записей пользователей.
В 7:40 сработало задание на получение данных из Топика.
Все задачи выбраны, локальная база данных синхронизирована.
В статье мы рассмотрели две архитектуры и остановились на событийно-ориентированной.
Вполне вероятно, что в вашем случае можно обойтись ручной синхронизацией.
Надеюсь, этот материал будет полезен.
Спасибо за внимание! Теги: #Хранилища данных #python #Микросервисы
-
Доступные Услуги Seo В Торонто
19 Oct, 24 -
Мини-Пк На Базе Intel Atom X5-Z8350
19 Oct, 24 -
Мекс Ищет Работу В Сша
19 Oct, 24 -
Red Hat Представила Релиз Платформы Mrg 1.2
19 Oct, 24