이전 게시물에서 언급했듯이 마이크로서비스로 실패하는 방법분산 시스템을 디버깅하는 것은 어려운 작업입니다. 네트워크 불안정, 일시적 사용 불가, 외부 버그 등 여러 가지 문제가 발생할 수 있으며, 그 중 일부는 우리가 통제할 수 없는 것이기도 합니다. 오늘은 이벤트 소싱/이벤트 로깅 패턴을 사용하여 "시간을 거슬러 올라가" 무엇이 잘못되었는지 이해하는 방법에 대해 설명하겠습니다.
다행히도 네트워크를 모니터링하여 예기치 않은 이벤트를 감지하고 기록할 수 있는 다양한 도구가 시중에 나와 있습니다. 서비스 메시 와 같은 도구를 사용할 수도 있습니다. 오픈트레이싱 를 사용할 수 있습니다. 하지만 엔티티의 상태를 파악하는 데 있어 빠른 플러그 앤 플레이 프레임워크는 없습니다.
데이터는 잠재적으로 코드보다 더 오래 사용할 수 있지만, 우리는 시간이 지남에 따라 데이터가 어떻게 진화하는지 간과하고 있습니다. 대부분의 시스템에서는 "이 엔티티가 어떻게 이 상태에 도달했나요?" 또는 "한 달 전 내 상태는 어땠나요?"와 같은 간단한 질문조차 변경 이력이 저장되지 않았기 때문에 대답할 수 없습니다. 이러한 변경 사항을 추적하는 것은 보안이나 디버깅 목적뿐만 아니라 방대한 비즈니스 가치 때문에(제품 소유자가 기뻐할 것입니다) 건강한 시스템을 위해 매우 중요합니다.
솔루션
서비스에서 일어나는 일에 대한 가시성을 추가하는 훌륭한 방법은 이벤트 소싱 | 이벤트 로깅을 이용하는 것입니다. 이 10년 된 패턴의 기본 개념은 애플리케이션의 상태에 대한 모든 변경 사항을 이벤트 객체에 캡슐화하여 순차적으로 저장해야 한다는 것입니다. 이 패턴이 익숙하게 들린다면, 제어 시스템이나 데이터베이스 트랜잭션 로그의 모든 버전이 이 이벤트 패턴을 많이 사용하기 때문일 수 있습니다.
하지만 어떻게 작동하는지 더 자세히 알아봅시다. 우리가 주문 서비스 의 경우 애플리케이션 상태와 이벤트가 어떻게 표시되는지 살펴보겠습니다:
많은 저자들은 이벤트 소싱/로깅 시스템에 대한 세 가지 주요 규칙을 정의합니다:
- 이벤트는 항상 불변입니다;
- 이벤트는 항상 과거에 일어난 일입니다. 일부 개발자는 명령어(예: PlaceOrder)를 이벤트(예: OrderPlaced)로 착각합니다.
- 이론적으로는 언제든지 현재 상태를 삭제하고 수신된 모든 메시지를 재처리하여 전체 시스템을 다시 구축할 수 있습니다.
이 패턴의 또 다른 좋은 점은 실제 구조가 어떻게 보일지 생각하기 전에 시스템의 이벤트에 대해 생각하도록 유도한다는 것입니다. 엔티티와 속성을 그려서 시스템을 설계하는 방법을 배웠기 때문에 처음에는 직관적이지 않을 수 있지만, 이는 서비스가 서로 어떻게 통신할지 먼저 생각하여 쉽게 식별하는 또 다른 일반적인 DDD 권장 사항과 잘 맞닿아 있습니다. 도메인.
이벤트 소싱 | 이벤트 로깅 흐름
이벤트 소싱의 가장 일반적인 흐름은 다음과 유사합니다:
- 메시지 수신기 는 수신 요청을 이벤트로 변환하고 유효성을 검사할 책임이 있습니다.
- 이벤트 스토어 는 이벤트를 순차적으로 저장하고 리스너에게 알림을 보내는 역할을 합니다.
- 이벤트 리스너: 짐작할 수 있듯이, 각 이벤트 유형에 따라 각각의 비즈니스 로직을 실행하는 코드입니다.
이 패턴을 구현할 수 있는 방법은 여러 가지가 있는데, 그 중 하나는 이벤트 서비스를 사용할 수 있습니다. 요약하면, 문서가 삽입/업데이트/삭제될 때마다 트리거되는 함수를 작성할 수 있습니다. 또한 이벤트 메커니즘을 사용하면 curl 요청을 사용하여 특정 문서가 데이터베이스에 저장될 때마다 애플리케이션의 엔드포인트를 트리거하여 처리할 수 있습니다. 이벤트 처리를 사용하면 어떻게 되는지 살펴보겠습니다:
이에 대한 자세한 내용을 알아보려면 카우치베이스 이벤트 공식 문서.
카우치베이스 이벤트는 비동기식이기 때문에 위의 구현은 애플리케이션이 비동기 호출만 수신하는 경우에만 적합합니다. 또한 누군가 이벤트를 수동으로 업데이트하려고 시도하는 경우 알림을 트리거하는 추가 보안 계층으로 작동할 수도 있습니다.
일부 시스템에서는 이벤트의 필드와 구조가 서로 상당히 다를 수 있으며, RDBM의 고정된 구조로 인해 이벤트 저장소를 모델링하기가 어렵습니다. 이러한 이유로 개발자는 일반적으로 이벤트 저장소를 JSON 문자열 를 varchar 필드에 추가합니다. 이 접근 방식에는 큰 문제가 있습니다: 대부분의 쿼리가 느리고 복잡하며 '좋아요'. 이를 위한 가능한 해결책 중 하나는 문서 데이터베이스를 사용하는 것입니다. 대부분의 문서 데이터베이스는 문서를 JSON으로 저장하고 다음과 같이 쿼리를 위한 적절한 SQL 유사 언어를 가지고 있기 때문입니다. N1QL.
스냅샷 찍기 - 상태에 맞게 버전 관리
상태에 버전 관리/이력을 추가하는 것을 이벤트 소싱 업계에서는 '스냅샷'이라고 부르기도 합니다. N일 전 상태를 알아야 할 때마다 모든 이벤트를 다시 처리하지 않아도 됩니다. 또한 이벤트 처리 후 애플리케이션의 상태가 예상과 다른 시점을 빠르게 인식할 수 있으므로 디버깅에도 도움이 됩니다.
스냅샷은 유용하고, 저렴하며, 구현하기 쉽고, 일시적 보고서에 탁월합니다. 이벤트 소싱 패턴을 구현하기로 결정했다면 약간의 추가 노력을 기울여 스냅샷도 구현하세요.
불일치 문제 해결
일반적으로 여기에서 모든 노력이 결실을 맺습니다. 이벤트 소싱/로깅 및 스냅샷을 설정한 후에는 약간 수정된 버전의 소급 이벤트 패턴을 사용하여 불일치를 수정합니다.
요약하면, 버그를 수정한 후 영향을 받는 엔티티의 상태도 조정해야 하는 경우 수동으로 업데이트하는 대신 엔티티의 상태를 버그 이전으로 설정하고 그 이후 관련된 모든 이벤트를 다시 재생할 수 있습니다. 이렇게 하면 자동으로 상태가 수정됩니다. 매뉴얼 개입.
- 롤백 상태를 사용하여 엔티티의 상태를 버그 발생 전으로 롤백할 수 있습니다. 다음을 피할 수 있습니다. 1단계 그리고 2 모든 이벤트를 재생하는 방법도 있습니다. 그러나 이 경우에는 전체 재처리를 피하고 싶기 때문에 이전 상태로 복원합니다.
- 스냅샷을 무시합니다: 복원된 스냅샷 이후의 모든 스냅샷은 다음과 같이 표시되어야 합니다. 무시됨 를 사용하여 나중에 일관성 없는 스냅샷을 복원하지 않도록 하세요.
- 이벤트 재구축대상부터 모든 이벤트를 다시 빌드합니다.
하지만 이벤트에 잘못된 데이터가 포함되어 있거나 애초에 트리거되어서는 안 되는 이벤트인 경우에는 어떻게 해야 할까요? 이벤트를 업데이트하거나 삭제하고 전체를 다시 처리할 수 있을까요?
기억하시겠지만, 이벤트 소싱의 첫 번째 규칙은 "이벤트는 항상 불변'라고 묻는다면 이는 매우 타당한 이유이며, 현재 보고 있는 로그를 신뢰해야 합니다. 하지만 이는 우리의 질문에 대한 답이 아니며, 이벤트를 변경하지 않고 어떻게 이벤트 로그를 변경할 수 있을까요? 라는 질문을 약간 수정했을 뿐입니다.
이 문제를 해결하는 쉬운 방법은 이벤트를 다음과 같이 표시하는 것입니다. 무시 를 사용하여 재구축 과정에서 무시할 수 있도록 합니다:
이벤트가 잘못된 데이터 또는 잘못된 순서로 트리거된 경우에는 어떻게 해야 할까요? 이 접근 방식을 사용하면 다음과 같이 이벤트를 무시할 수 없는 것으로 표시하고 올바른 값 또는 올바른 위치에 새 이벤트를 추가하기만 하면 됩니다:
멋지지 않나요? 하지만 여기에 까다로운 과제가 하나 더 있습니다. 중간에 이벤트를 추가할 수 있는 이벤트 시퀀스를 어떻게 만들 수 있을까요?
순진한 해결책은 각 엔티티에 플로트 카운터를 추가하는 것입니다. 이렇게 하면 다음과 같은 이론에 따라 중간에 항목을 무한대로 추가할 수 있습니다. 슈퍼태스크 (실제로는 플로트/두 배 최대 크기로 제한됨), 일반적으로 상태를 수정하는 데 필요한 모든 이벤트를 추가하기에 충분한 공간입니다:
물론 위의 접근 방식에는 많은 결함이 있지만 구현이 엄청나게 간단하고 쿼리하기 쉬우며 대부분의 경우에 꽤 잘 작동합니다. 보다 강력한 구조를 구축해야 한다면 이벤트를 링크드 리스트 구조로 저장하는 것을 고려해 보세요:
외부 시스템 | 기타 마이크로서비스는 어떻게 되나요?
마이크로서비스는 섬이 아니므로 이벤트 재생의 부작용 중 하나는 내 서비스가 외부 서비스에 메시지를 보낼 수 있다는 것입니다. 이러한 메시지는 다른 시스템에서 불일치를 유발하거나 오류를 전파하여 상황을 이전보다 더 악화시킬 수 있습니다.
안타깝게도 다양한 가능성으로 인해 이 문제를 해결할 수 있는 만병통치약은 없으며, 각 사례를 개별적으로 처리해야 합니다. 몇 가지 기존 솔루션이 있습니다:
- 외부 메시지를 보내지 않도록 일시적으로 구성을 변경하거나 어떤 메시지를 보내야 하는지 구성할 수 있는 인터셉터를 추가할 수 있습니다;
- 특정 요청을 가짜 서비스로 리라우팅(서비스 메시 패턴을 사용하는 경우 일반적인 시나리오)
- 다른 서비스에서 특정 작업이 과거에 동일한 매개 변수를 사용하여 이미 실행되었음을 인식한 다음 오류를 발생시키는 대신 이전과 동일한 성공 메시지를 반환할 수 있도록 합니다.
당연히 외부 불일치를 자동으로 수정할 수 없는 경우가 상당히 많으며, 이 시나리오에서는 다른 시스템에서 사람이 읽을 수 있는 오류를 출력하거나 누군가의 개입을 위한 알림을 트리거할 것으로 예상됩니다.
이벤트 소싱의 장점
간단한 패턴이지만 이 패턴을 사용하면 많은 이점이 있습니다:
- 이벤트 로그는 비즈니스 가치가 높습니다;
- DDD 및 이벤트 중심 아키텍처와 매우 잘 작동합니다.
- 애플리케이션 상태의 모든 변경 사항의 출처에 대한 오디션;
- 실패한 이벤트를 다시 재생할 수 있습니다;
- 대상 엔티티의 모든 이벤트를 머신에 복사하고 각 이벤트를 디버깅하여 애플리케이션이 특정 상태에 도달한 방법을 파악할 수 있으므로 디버깅이 간편합니다(프로덕션에서 데이터를 복사할 때의 보안 영향은 무시);
- 사용할 수 있습니다. 소급 이벤트 패턴을 사용하여 상태를 다시 빌드/수정합니다.
많은 저자가 일시적 쿼리 기능을 장점으로 꼽기도 하지만, 저는 여러 후속 이벤트를 쿼리하는 것이 사소한 작업이 아니라고 생각합니다. 따라서 저는 보통 임시 쿼리 를 스냅샷 패턴의 장점으로 꼽을 수 있습니다.
이벤트 소싱의 단점
- 요청을 먼저 이벤트로 변환해야 하므로 동기식 호출로 작업하는 것은 직관성이 약간 떨어집니다.
- 획기적인 변경 사항을 배포할 때마다 이전 버전과의 호환성을 유지하려면 이벤트 기록도 마이그레이션해야 합니다(이벤트 업그레이드라고도 함).
- 일부 구현에서는 모든 이벤트가 처리되었는지 확인하기 위해 최신 이벤트의 상태를 확인하는 추가 작업이 필요할 수 있습니다.
- 이벤트에는 개인 데이터가 포함될 수 있으므로 이벤트 로그가 적절하게 보호되는지 확인하는 것을 잊지 마세요.
결론
지난 몇 년 동안 저에게 잘 작동했던 이벤트 소싱/이벤트 로깅 패턴의 약간 수정된 버전을 보여드렸습니다. 제가 이 패턴에 대해 처음 들은 것은 거의 10년 전의 유명한 마틴 파울러 블로그 게시물 (꼭 읽어 보세요). 그 이후로 모든 보고 기능은 말할 것도 없고 마이크로서비스의 상태를 거의 깨지지 않게 만드는 데 많은 도움이 되었습니다.
하지만 모든 서비스에 무분별하게 사용해야 하는 것은 아닙니다. 저는 개인적으로 핵심적인 서비스에만 사용해야 한다고 생각합니다. 예를 들어 사용자가 자신의 이름을 변경한 모든 기록을 시스템에 보관할 필요는 없을 것입니다.
질문이 있으시면 언제든지 다음 주소로 트윗해 주세요. @deniswsrosa
훌륭한 조언이 담긴 좋은 기사였습니다, Denis!
저는 카우치베이스를 이벤트 저장소로 사용해 본 결과, 이벤트 서비스가 스냅샷 생성을 간소화하고 이벤트 저장소 정리 및 유지 관리 작업에 도움이 된다는 것을 알 수 있었습니다. 하지만 스냅샷이 없어도 Couchbase를 사용하면 이벤트를 검색하고 재생하는 속도가 매우 빠릅니다.