Расскажи про самые сложные задачи, которые решал

Ответ

Одной из самых сложных задач была миграция монолитного приложения на Express.js с 500k строк кода на микросервисную архитектуру без downtime для пользователей.

Вызовы:

  1. Распутывание зависимостей: Монолит имел запутанные модульные связи и общее состояние в памяти.
  2. Согласованность данных: Необходимо было разделить общую PostgreSQL базу между новыми сервисами, сохраняя целостность транзакций.
  3. Коммуникация: Организация взаимодействия между 10+ новыми сервисами (ордеры, пользователи, платежи и т.д.).

Мое решение и реализация:

1. Стратегия «Strangler Fig»:

  • Мы не переписывали всё сразу. Я создал API Gateway (на Node.js с @nestjs/platform-express), который перенаправлял трафик либо в старый монолит, либо в новые сервисы.
  • Постепенно мы «отрезали» модули от монолита, превращая их в независимые сервисы (например, первым вынесли сервис нотификаций на Nest.js с RabbitMQ).

2. Синхронизация данных через события:

  • Для замены общих JOIN-запросов внедрили механизм событийной синхронизации. При изменении сущности в «сервисе пользователей» он публиковал событие в Kafka.
  • Другие сервисы (например, «сервис ордеров») подписывались на эти события и поддерживали свою денормализованную копию необходимых данных в своей БД.

    // Пример публикации события в сервисе пользователей (Nest.js)
    @Injectable()
    export class UserService {
    constructor(private kafkaProducer: KafkaProducerService) {}
    
    async updateUser(userId: string, data: UpdateUserDto) {
    const updatedUser = await this.userRepo.update(userId, data);
    // Публикуем событие для других сервисов
    await this.kafkaProducer.emit('user.updated', {
      id: updatedUser.id,
      email: updatedUser.email,
      updatedAt: new Date().toISOString()
    });
    return updatedUser;
    }
    }

3. Управление распределенными транзакциями:

  • Для сценариев, требующих согласованности (например, создание заказа со списанием денег), реализовали паттерн Saga с компенсирующими транзакциями, используя состояние в PostgreSQL.

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

Ответ 18+ 🔞

Ёпта, представляешь, эту историю про миграцию монолита? Это ж пиздец какой-то, ядрёна вошь! Сидит себе приложение на Express, полмиллиона строк кода, как здоровенный булыжник. И надо его, сука, разобрать на микросервисы, но так, чтобы пользователи нихуя не заметили. Ну, типа, «да похуй, ребята, мы тут архитектуру меняем, а вы сидите, не дёргайтесь».

Вызовы были, блядь, огого:

  1. Запутанные зависимости. Там внутри всё переплетено, как провода у старого телевизора. Один модуль за другого держится, общая память, пизда рулю. Чистый ад.
  2. Согласованность данных. Все сервисы в одну базу PostgreSQL тыкались, как в общую тарелку. А теперь надо развести, но чтобы данные не разъехались, и транзакции чтобы работали. Доверия ебать ноль к этому процессу изначально было.
  3. Коммуникация. Ну, отпилили куски, а теперь они между собой как общаться будут? Ордера, пользователи, платежи — всем друг от друга что-то надо. Организовать эту толкучку — ещё та задача.

Как мы это, сука, провернули:

1. Стратегия «Удушающего Фига» (Strangler Fig). Не стали, как дураки, всё ломать и переписывать с нуля. Сделали умно: поставили API Gateway (на Node.js, естественно), который стал как диспетчер. Он смотрел на запрос и решал: «Ага, этот путь ещё в старом монолите живет» или «О, а эту фичу мы уже в новый сервис вынесли, маршрут туда». И так потихоньку, как проказой, начали «отъедать» куски от монолита. Первым, помню, выпилили сервис нотификаций на Nest.js с RabbitMQ. Ну, чтоб попроще, для раскатки.

2. Синхронизация через события. Раньше-то всё через JOIN-ы в базе дружило. А теперь сервисы живут отдельно. Как быть? Завезли Кафку! Теперь, если в «сервисе пользователей» что-то обновили, он не молчит, а кричит на всю площадку: «Эй, народ, у юзера Васи данные поменялись!». А другие сервисы, которым это надо (например, «сервис ордеров»), подписываются и тихонько себе копию этих данных в свою локальную базу сохраняют. Денормализация, хуле.

// Примерно так это в коде выглядело (сервис пользователей на Nest.js)
@Injectable()
export class UserService {
  constructor(private kafkaProducer: KafkaProducerService) {}

  async updateUser(userId: string, data: UpdateUserDto) {
    const updatedUser = await this.userRepo.update(userId, data);
    // А тут бабах! Кидаем событие в Кафку для всех желающих
    await this.kafkaProducer.emit('user.updated', {
      id: updatedUser.id,
      email: updatedUser.email,
      updatedAt: new Date().toISOString()
    });
    return updatedUser;
  }
}

3. Управление распределёнными транзакциями. А вот это была, блядь, головная боль. Ну, например, создание заказа: надо и заказ записать, и деньги списать, и инвентарь проверить. В монолите это одна транзакция. А тут сервисы разные! Решили через паттерн Saga, с компенсирующими действиями. То есть, если на каком-то шаге пиздец, мы откатываем всё, что успели наделать в других сервисах. Состояние саги хранили прямо в PostgreSQL, без выебонов.

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