Исследование миграции устаревших интеграций на асинхронную обработку с использованием паттерна входящих сообщений

Исследование миграции устаревших интеграций на асинхронную обработку с использованием паттерна входящих сообщений

-Опыт перевода этапа приема информации о массовой рассылке из устаревшей системы с синхронной обработки на структуру «сохранить и выйти»-

1. Введение — с какой проблемой мы столкнулись?

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

  • Время обработки напрямую соответствовало времени занятия транзакции в устаревшей системе. Для обработки одного принятого элемента адаптер синхронно выполняет два вызова устаревшей системы, от четырех до пяти вызовов сервиса учетных записей и около десяти вызовов сервиса опросов — всего примерно 20 REST-переходов. Поскольку устаревшая система выполняет последовательные вызовы для элементов массовой рассылки в рамках собственной транзакции, она удерживала транзакцию на протяжении всего времени обработки адаптером. При изучении трассировок я обнаружил, что время HTTP-ответа для одного элемента составляло 1,4–2,2 секунды, а в периоды, когда одновременно поступали сотни элементов, тайм-ауты на стороне устаревшей системы постоянно оставались реальной угрозой.

  • Частичные сбои оставляли осиротевшие ресурсы. Поскольку адаптер был спроектирован без собственной базы данных (DB-less), аннотация @Transactional, добавленная к точке входа, фактически ничего не делала. Если промежуточный REST-вызов завершался с ошибкой, гостевые учетные записи или сведения о получателях, созданные на предыдущих этапах, оставались без отката.

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

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

2. Обоснование выбора технологий — процесс исключения альтернатив

2.1 Выбор метода асинхронной обработки

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

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

  • Добавление базы данных в адаптер — Это позволило бы сохранять записи о получении, но нарушило бы идентичность сервиса: «адаптер не владеет данными». Как только он начинает владеть данными, возникают новые проблемы синхронизации с исходным сервисом.

  • Прямая загрузка исходных сообщений в брокер сообщений — Это предотвращает потерю, но не дает возможности запрашивать состояние отдельных элементов (количество повторных попыток, причину сбоя и завершенность обработки) или визуально проверять их операторам. Мы пришли к выводу, что структуру, которая при масштабном сбое не способна ответить на вопрос «Сколько элементов сейчас накопилось в очереди?», будет сложно эксплуатировать.

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

2.2 Триггер обработки — события вместо обратного вызова Feign

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

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

2.3 Использованный технологический стек

  • Apache Kafka — Публикует и принимает события о завершении сохранения. Ключ партиционирования обеспечивает инфраструктурную основу для гарантии порядка.

  • PostgreSQL и JPA — Сохранение Inbox. Исходное сообщение управляется как текст JSON, а состояние, количество повторных попыток и причина сбоя — как нормализованные столбцы.

  • ShedLock — Был применен распределенный блокировщик, чтобы резервная пакетная обработка не выполнялась избыточно на нескольких экземплярах.

  • Redis — Используется для кэширования запросов к внешним системам (§4.4), а также как уровень хранения метаданных рассылки.

  • Spring Cloud OpenFeign — Вызывает API регистрации, получения и обновления состояния Inbox. Контракт был выделен в отдельный клиентский модуль и распространен через внутренний репозиторий компании.

3. Процесс реализации

3.1 Этап приема — что сохранить, а что убрать

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

// 전환 전: 진입점이 등록 전체를 동기로 수행 (@Transactional은 DB가 없어 무효)
@Transactional
public void registerSurveySendInfo(SurveySendInfo info) {
    // 레거시 조회 2회 + 계정 4~5회 + 설문 10여 회 ... 총 20여 회 REST 왕복
    // 중간 실패 시 앞 단계 산출물(게스트, 수신자정보 등)이 롤백 없이 잔존
}

// 전환 후: 저장-후-종료
public void acceptSurveySendInfo(SrvySendInfoDto ipd) {
    validateRequired(ipd); // srvyDrawupNo, patno, formCd 존재만 확인
    smsMsgInfoAction.saveMsgInfo(ipd);  // 발송 메타데이터 보존(Redis)
    surveySendInfoInboxProxy.register( // 원문 그대로 저장 + 커밋 후 이벤트 발행
            SurveySendInfoInboxCdo.from(ipd));
}

[Code 1] Этап до и после изменения точки входа приема. Контракт с устаревшей системой (URL, формат сообщения и заголовки ответа) не изменился.

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

3.2 Конечный автомат и единый журнал повторных попыток

Сохранённые записи проходят через пять состояний: RECEIVED → PROCESSING → COMPLETED; при сбое обработки они записываются в состояние FAILED, а после превышения лимита повторных попыток переходят в DEAD и становятся объектом ручного вмешательства.

Принципу, которому мы уделили наибольшее внимание при проектировании, было ведение единого журнала повторных попытокКогда повторные попытки на уровне обмена сообщениями (по умолчанию 10 попыток, после чего используется DLT) пересекаются с повторными попытками приложения, никто не может объяснить фактическое количество попыток. Поэтому consumer не выбрасывает исключения наружу: он перехватывает всё и только записывает это в Inbox, а решения о повторной попытке принимаются исключительно на основе счётчика попыток Inbox и резервного batch.

3.3 Атомарное резервирование — единственный шлюз, предотвращающий повторную обработку

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

@Modifying
@Query("UPDATE SurveySendInfoInboxJpo i " +
       " SET i.status = 'PROCESSING', i.modifiedOn = :now " +
       " WHERE i.id = :id AND i.status IN ('RECEIVED', 'FAILED')")
int claim(@Param("id") Long id, @Param("now") LocalDateTime now);

// 갱신 행 수가 1이면 점유 성공 → 원문 반환, 0이면 이미 점유/종결된 건 → null
public SurveySendInfoInboxRdo claimAndLoad(Long id) {
    if (repository.claim(id, LocalDateTime.now()) == 0) {
        return null;
    }
    return SurveySendInfoInboxRdo.from(repository.getById(id));
}

[Code 2] Резервирование должно выполняться посредством условного одиночного UPDATE, а не чтением с последующим обновлением. Если чтение и обновление разделены, интервал между ними становится состоянием гонки.

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

3.4 Consumer — не выбрасывать исключения наружу

@KafkaListener(topics = SurveyTopic.SURVEY_SEND_INFO_RECEIVED, groupId = "...")
public void handle(SurveySendInfoReceivedDomainEvent event) {
    SurveySendInfoInboxRdo rdo = inboxProxy.claim(event.getInboxId());
    if (rdo == null) {
        return;   // 이미 점유·종결됐거나 선행 미종결 건이 있어 보류된 경우
    }
    try {
        surveyFlow.registerSurveySendInfo(rdo.toSurveySendInfo());
        inboxProxy.complete(rdo.getId());
    } catch (PromsBizException e) { // 업무 예외 → 메시지를 그대로 사유로
        handleFailure(rdo, e.getMessage());
    } catch (Exception e) { // 시스템 예외 → 고정 문구
        log.error("inbox 처리 실패. id={}", rdo.getId(), e);
        handleFailure(rdo, "시스템 오류");
    }
}

private void handleFailure(SurveySendInfoInboxRdo rdo, String reason) {
    String status = InboxProxy.fail(rdo.getId(), reason);  // FAILED 또는 DEAD 반환
    if ("DEAD".equals(status)) {
        amcFlowProxy.sendResult(rdo.getSrvyDrawupNo(), "N", reason); // 레거시 회신
    }
}

[Code 3] API регистрации сбоя определяет и возвращает, нужно ли перевести запись в DEAD, а адаптер управляет триггером ответа.

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

3.5 Резервный batch — повторно публиковать только событие вместо непосредственной обработки

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

@Scheduled(cron = "0 * * * * *")                       // 1분 주기
@SchedulerLock(name = "surveySendInfoInboxRedeliver")  // 다중 인스턴스 중복 방지
public void redeliver() {
    // ① 이벤트 유실 의심: RECEIVED 이면서 생성 후 2분 경과
    // ② 재시도 대상: FAILED 이면서 retryCount < 5
    // ③ 처리중 고착: PROCESSING 이면서 15분간 갱신 없음 → RECEIVED 복귀 후 재발행
    for (SurveySendInfoInbox target : InboxStore.findRedeliverTargets()) {
        try {
            eventPublisher.publish(SurveySendInfoReceivedDomainEvent.of(target));
        } catch (Exception e) { // 한 건 실패가 라운드 전체를 막지 않도록
            log.warn("재발행 실패. id={}", target.getId(), e);
        }
    }
}

[Code 4] Один batch охватывает три типа зависших ситуаций. При написании batch безопаснее выработать привычку добавлять try-catch для каждой записи.

4. Извлечённые уроки из решения проблем

4.1 Безопасность повторных попыток — атомаризируйте вместо того, чтобы разбрасывать защитные проверки

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

Сначала мы рассматривали возможность добавить в адаптер API для «обратной проверки наличия существующей записи» и установить защитную проверку на каждом этапе. Однако такой подход распределяет проверки по четырём местам и оставляет возможность повторять ту же ошибку каждый раз при добавлении нового этапа.

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

4.2 Гарантии порядка и сохраняющиеся риски

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

Мы заблокировали такую ситуацию непосредственно перед резервированием с помощью проверки, которая завершает работу без обработки текущей записи, если существует более ранняя незавершённая запись для того же объектаУдержанная запись повторно публикуется batch в правильном порядке, поэтому в конечном итоге обрабатывается в порядке поступления. Это была недорогая защитная проверка, которую можно было реализовать с помощью одного индекса; впоследствии она также продолжила служить механизмом безопасности, когда мы добавили политику «интерпретировать повторное получение того же сообщения как запрос на повторную отправку».

4.3 Цена сокращения синхронного контракта — создание пути для сообщения об ошибках

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

  • Записи с окончательной ошибкой (DEAD) — уведомлять получателя, включая причину ошибки, о невозможности завершить отправку.

  • Уведомление о пропущенной отправке — уведомлять вас с указанием причины о том, что этот запрос только формирует анкету и не отправляет сообщение AlimTalk.

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

Сервис survey, владеющий данными, принимает решение и сообщает о нём в ответе, а адаптер, понимающий формат legacy-сообщения, непосредственно обрабатывает ответ. Однако отсутствие возможности повторно отправить ответ в случае сбоя ответа по-прежнему остаётся проблемой.

4.4 Не угадывайте узкие места — инструментируйте их — кэширование внешних запросов

Даже после перехода пропускная способность не выросла настолько, насколько ожидалось, поэтому мы перепроверили несколько трассировок с помощью инструмента мониторинга. В результате из двух legacy-запросов, формируемых для каждой регистрации, Один запрос к форме согласия стабильно занимал 760–984 миллисекундытогда как другой занимал всего 50–150 миллисекунд. Поскольку с точки зрения структуры кода эти два вызова выглядели почти одинаково, без инструментирования мы, скорее всего, решили бы, что они одинаково медленные, и работали бы не над тем участком.

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

public List<AmcSurveyConsent> findAmcSurveyConsent(String srvyNo) {
    String key = "proms:amc:survey-consents:" + srvyNo;
    try {
        String cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return objectMapper.readValue(cached, new TypeReference<>() {});
        }
    } catch (Exception e) {
        log.warn("캐시 조회 실패 — 원 호출로 폴백. srvyNo={}", srvyNo, e);
    }

    List<AmcSurveyConsent> result = amcProxy.findSurveyConsent(srvyNo);
    // 동의서는 빈 목록도 캐시한다 — 동의서 없는 설문이 일반적이라 여기가 절감의 핵심
    cachePut(key, result, Duration.ofMinutes(5));
    return result;
}

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

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

4.5 Исключения и границы транзакций — почему мы решили не использовать noRollbackFor

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

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

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

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

4.6 Включение метода отката в проектирование

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

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

// 롤백 토글: false면 Inbox를 거치지 않고 구 동기 경로로 즉시 처리한다.
// off 상태에서도 컨슈머·fallback은 유지되어 잔여 inbox 건은 계속 소화된다.
@Value("${proms.survey-send-info.inbox-enabled:true}")
private boolean inboxEnabled;

if (inboxEnabled) {
    surveySendInfoInboxFlow.acceptSurveySendInfo(ipd); // 저장-후-종료
} else {
    surveyFlow.registerSurveySendInfo(new SurveySendInfo(ipd)); // 구 동기 경로
}

[Code 6] Мы сделали возможным восстановление пути приёма только посредством изменения конфигурации и перезапуска, без повторного развёртывания образа.

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

5. Результаты проверки

Мы провели четыре раунда проверки на реальном трафике в среде разработки и один — в staging, измеряя показатели непосредственно с помощью инструментов журналирования и трассировки.

  • Время ответа при приёме — Сократилось с прежних 1,4–2,2 секунды до медианного значения 33 миллисекунды, то есть примерно в 50 раз. Проблема занятия транзакции в устаревшей системе была устранена по существу.

  • Полное завершение — Все 111 сообщений были обработаны за 38 секунд, а все 273 сообщения — за 94 секунды, без единой ошибки и без единого вмешательства через резервный путь. Пропускная способность составила примерно три сообщения в секунду; мы также подтвердили, что при необходимости масштабирования основными рычагами являются разбиение на разделы и параллелизм потребителей.

  • Сокращение количества внешних запросов — После внедрения кэша количество запросов к устаревшей системе сократилось примерно на 98% относительно количества сообщений (при обработке 111 сообщений было выполнено примерно пять запросов из-за промаха кэша).

  • Проверка пути уведомления об ошибке — На реальном трафике мы проверили, что 29 случаев с незарегистрированными формами после пяти повторных попыток переходят в состояние DEAD и возвращаются в устаревшую систему вместе с причиной.

  • Проверка автоматического восстановления после ошибки — Так получилось, что проверка в staging совпала с кратковременным прерыванием работы базы данных устаревшей системы. Все 273 сообщения первоначально были успешно приняты, но каждое сообщение завершилось ошибкой на первом раунде. После восстановления базы данных повторная публикация пакетным заданием привела к тому, что все 273 сообщения автоматически восстановились в течение 3,5 минут (итоговых ошибок — 0). Поскольку запланированная архитектура сработала во время реальной ошибки, а не только в рамках заранее подготовленного сценария, лично для меня это был самый значимый результат перехода.

6. Ретроспектива — что следует перенести дальше

  • Когда меняется смысл ответа, мы должны проследить его влияние до самого конца. Изменение с «Регистрация завершена» на «Приём завершён» затронуло всего несколько строк кода, но породило два важных требования: создание пути уведомления об ошибке и обеспечение завершения обработки перед откатом. Я выработал привычку сначала спрашивать себя, изменился ли смысл, а не сосредотачиваться на формулировке контракта.

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

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

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

  • Включение процедур отката в проект позволяет выявить недостатки проекта.Разрабатывая процедуру отката, мы обнаружили ограничение: «Для случаев, по которым в старой версии уже был отправлен ответ с подтверждением, не существовало способа выполнить обработку». Это позволило нам подготовить более эффективную первоочередную меру: переключатель.

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

conley

Site footer