Reddit이 댓글 기능을 Python에서 Go로 마이그레이션한 방법
원문: How Reddit Migrated Comments Functionality from Python to Go

면책 조항: 이 게시물의 세부 내용은 Reddit 엔지니어링 팀이 온라인에 공유한 내용에서 파생되었습니다. 모든 기술적 세부 사항에 대한 공로는 Reddit 엔지니어링 팀에게 있습니다. 원본 기사 및 출처에 대한 링크는 게시물 끝의 참고 자료 섹션에 있습니다. 우리는 세부 사항을 분석하고 이에 대한 의견을 제공하려고 시도했습니다. 부정확하거나 누락된 내용이 있으면 댓글을 남겨주시면 최선을 다해 수정하겠습니다.
Reddit에서 영리한 댓글에 투표하거나 토론 스레드에 답글을 달 때, 여러분은 Reddit의 댓글 모델과 상호작용하고 있습니다. 이 모델은 아마도 Reddit의 아키텍처 설정에서 가장 중요하고 트래픽이 많은 모델일 것입니다.
Reddit의 인프라는 네 가지 핵심 모델을 중심으로 구축되었습니다: 댓글(Comments), 계정(Accounts), 게시물(Posts), 서브레딧(Subreddits).
이러한 모델은 사용자가 플랫폼에서 하는 거의 모든 작업을 지원합니다. 수년 동안 네 가지 모델 모두 단일 레거시 Python 서비스에서 제공되었으며, 소유권은 여러 팀에 걸쳐 어색하게 분할되어 있었습니다. 2024년까지 이 모놀리식 아키텍처는 문제가 되었습니다:
-
서비스는 반복적인 안정성 및 성능 문제를 겪었습니다.
-
관련된 모든 팀에게 유지 관리가 점점 더 어려워졌습니다.
-
소유권 책임이 불명확하고 분산되어 있었습니다.
2024년, Reddit 엔지니어링 팀은 이 모놀리스를 현대적인 도메인별 Go 마이크로서비스로 분해하기로 결정했습니다.
그들은 댓글을 첫 번째 마이그레이션 대상으로 선택했습니다. 왜냐하면 댓글이 Reddit의 가장 큰 데이터 세트를 나타내고 모든 핵심 모델 중 가장 높은 쓰기 처리량을 처리했기 때문입니다. 댓글을 성공적으로 마이그레이션할 수 있다면, 그들의 접근 방식이 무엇이든 처리할 수 있다는 것을 증명할 수 있을 것입니다.
이 글에서는 Reddit이 이 마이그레이션을 어떻게 수행했으며 직면한 과제들을 살펴보겠습니다.
The Easy Part: 읽기 작업 마이그레이션
복잡한 시나리오로 들어가기 전에, Reddit이 이 마이그레이션의 더 간단한 부분인 읽기 엔드포인트를 어떻게 접근했는지 이해할 가치가 있습니다.
댓글을 볼 때, 그것은 읽기 작업입니다. 서버는 스토리지에서 데이터를 가져와 아무것도 변경하지 않고 반환합니다.
Reddit은 읽기 마이그레이션을 위해 "tap compare"라는 테스트 기술을 사용했습니다. 개념은 간단합니다:
-
트래픽의 일부가 새로운 Go 마이크로서비스로 라우팅됩니다.
-
새 서비스는 내부적으로 응답을 생성합니다.
-
무엇이든 반환하기 전에, 이전 Python 엔드포인트를 호출하여 그 응답도 가져옵니다.
-
시스템은 두 응답을 비교하고 차이점을 기록합니다.
-
이전 엔드포인트의 응답이 실제로 사용자에게 반환되는 것입니다.
이 접근 방식은 새 서비스에 버그가 있더라도 사용자는 절대 그것을 보지 못한다는 것을 의미했습니다. 팀은 사용자 경험에 대한 제로 리스크를 유지하면서 실제 트래픽으로 프로덕션에서 새 코드를 검증할 수 있었습니다.
The Hard Part: 쓰기 작업 마이그레이션
쓰기 작업은 완전히 다른 도전입니다. 댓글을 게시하거나 투표할 때, 여러분은 데이터를 수정하고 있습니다.
Reddit의 댓글 인프라는 여러분의 작업을 한 곳에만 저장하지 않습니다. 동시에 세 개의 서로 다른 데이터 저장소에 씁니다:
-
Postgres: 모든 댓글 데이터가 영구적으로 저장되는 기본 데이터베이스입니다.
-
Memcached: 자주 액세스되는 댓글을 빠른 메모리에 보관하여 읽기 속도를 높이는 캐싱 레이어입니다.
-
Redis: 댓글이 변경될 때마다 다른 서비스에 알리는 CDC(Change Data Capture) 이벤트를 위한 이벤트 저장소입니다.
CDC 이벤트는 특히 중요했습니다. Reddit은 플랫폼 전체의 다운스트림 시스템이 의존하기 때문에 이러한 이벤트의 100% 전달을 보장합니다. 이벤트를 놓치면 다른 곳의 기능이 손상될 수 있습니다.
팀은 근본적인 제약 때문에 쓰기에 기본 tap compare를 단순히 사용할 수 없었습니다: 댓글 ID는 고유해야 합니다. 고유 키 제약이 거부하므로 동일한 댓글을 프로덕션 데이터베이스에 두 번 쓸 수 없습니다.
그러나 프로덕션에 쓰지 않고, 새로운 구현이 올바르게 작동하는지 어떻게 검증할까요?
Sister Datastore 솔루션
Reddit의 엔지니어링 팀은 "sister datastores"라고 부르는 솔루션을 생각해냈습니다. 그들은 프로덕션 인프라를 미러링하는 세 개의 완전히 별도의 데이터 저장소(Postgres, Memcached, Redis)를 만들었습니다. 중요한 차이점은 새로운 Go 마이크로서비스만 이러한 sister 저장소에 쓴다는 것이었습니다.
이중 쓰기 흐름이 어떻게 작동했는지는 다음과 같습니다:
-
쓰기 트래픽의 일부가 Go 마이크로서비스로 라우팅됩니다.
-
Go는 레거시 Python 서비스를 호출하여 실제 프로덕션 쓰기를 수행합니다.
-
사용자는 댓글이 정상적으로 게시되는 것을 봅니다(Python이 여전히 실제 작업을 처리하고 있습니다).
-
Go는 완전히 격리된 sister 데이터 저장소에 자체 쓰기를 수행합니다.
-
두 쓰기가 모두 완료되면 시스템은 프로덕션 데이터를 sister 데이터와 비교합니다.

이 비교는 세 개의 데이터 저장소 모두에서 발생했습니다. Go 서비스는 프로덕션과 sister 인스턴스 모두를 쿼리하고, 결과를 비교하고, 차이점을 기록합니다. 이 접근 방식의 아름다움은 Go의 구현에 버그가 있더라도 그 버그는 격리된 sister 데이터 저장소에만 영향을 미치고 실제 사용자 데이터에는 절대 닿지 않는다는 것이었습니다.
검증의 규모
검증 프로세스는 상당했습니다. Reddit은 세 개의 쓰기 엔드포인트를 마이그레이션했습니다:
-
댓글 생성: 새 댓글 게시
-
댓글 업데이트: 기존 댓글 편집
-
댓글 속성 증가: 투표와 같은 작업
각 엔드포인트는 세 개의 데이터 저장소에 쓰고, 데이터는 두 개의 다른 서비스 구현에서 검증되어야 했습니다. 이로 인해 여러 비교가 동시에 실행되었고, 각각은 신중한 검증과 버그 수정이 필요했습니다.
그러나 이것만으로는 충분하지 않았습니다. 마이그레이션 초기에 팀은 직렬화 문제를 발견했습니다. 직렬화는 데이터 구조를 저장하거나 전송할 수 있는 형식으로 변환하는 프로세스입니다. 서로 다른 프로그래밍 언어는 데이터를 다르게 직렬화합니다. Go가 데이터 저장소에 데이터를 쓸 때, Python 서비스가 때때로 그 데이터를 올바르게 역직렬화(다시 읽기)할 수 없었습니다.
이러한 문제를 포착하기 위해 팀은 또 다른 검증 레이어를 추가했습니다.
그들은 레거시 Python 서비스의 실제 CDC 이벤트 소비자를 통해 모든 tap 비교를 실행했습니다. 이는 Python 코드가 Go에 의해 작성된 이벤트를 역직렬화하고 처리하려고 시도한다는 것을 의미했습니다. Python이 이러한 이벤트를 성공적으로 읽고 처리할 수 있다면, 그들은 언어 간 호환성이 작동한다는 것을 알았습니다. 이 엔드 투 엔드 검증은 Go가 올바른 데이터를 썼을 뿐만 아니라 전체 생태계가 그것을 소비할 수 있다는 것을 보장했습니다.

다른 언어로 인한 과제
프로그래밍 언어 간 마이그레이션은 직렬화를 넘어 예상치 못한 복잡성을 도입했습니다.
한 가지 주요 문제는 데이터베이스 상호작용과 관련이 있었습니다. Python은 데이터베이스 쿼리를 단순화하는 도구인 ORM(Object-Relational Mapping)을 사용합니다. Reddit의 Go 서비스는 ORM을 사용하지 않고 대신 직접 데이터베이스 쿼리를 작성합니다.
Python의 ORM에는 팀이 완전히 이해하지 못한 숨겨진 최적화가 있다는 것이 밝혀졌습니다. Go 서비스를 증가시키기 시작했을 때, Postgres 데이터베이스에 예상치 못한 압박을 가했습니다. Python에서 원활하게 실행되던 동일한 작업이 Go에서 성능 문제를 일으켰습니다.
다행히도 그들은 이것을 일찍 포착하고 Go 쿼리를 최적화했습니다. 그들은 또한 데이터베이스 리소스 사용률에 대한 더 나은 모니터링을 구축했습니다. 이 경험은 향후 마이그레이션이 애플리케이션 로직뿐만 아니라 데이터베이스 액세스 패턴에 세심한 주의를 기울여야 한다는 것을 가르쳤습니다.
The Race condition 문제
또 다른 까다로운 문제는 tap compare 로그의 경쟁 조건이었습니다.
팀은 말이 안 되는 불일치를 보기 시작했습니다. 그들은 몇 시간을 조사한 후 "버그"가 전혀 버그가 아니라 타이밍 문제라는 것을 발견했습니다.
예를 들어 다음과 같은 시나리오가 있습니다:
-
사용자가 댓글을 업데이트하여 텍스트를 "hello"로 변경합니다
-
Go는 "hello"를 sister 데이터 저장소에 씁니다
-
Go는 Python을 호출하여 "hello"를 프로덕션에 씁니다
-
그 밀리초 동안 다른 사용자가 동일한 댓글을 "hello again"으로 편집합니다
-
Go가 프로덕션에 대해 쓰기를 비교할 때, 일치하지 않습니다
이러한 타이밍 기반 오탐지는 디버깅을 어렵게 만들었습니다.
불일치가 Go 구현의 실제 버그로 인한 것인지, 아니면 단지 불운한 타이밍 때문인지?
팀은 경쟁 조건 불일치를 감지하고 무시하기 위한 사용자 정의 코드를 개발했습니다. 향후 마이그레이션을 위해 그들은 데이터베이스 버전 관리를 구현할 계획입니다. 이를 통해 동일한 논리적 변경에서 발생한 업데이트만 비교할 수 있습니다.
흥미롭게도 이 문제는 특정 데이터 저장소에만 해당되었습니다:
-
Redis 이벤트 저장소: 고유한 이벤트 ID를 사용했기 때문에 경쟁 조건 문제가 없었습니다
-
Postgres 및 Memcached: 경쟁 조건이 일반적이었고 특별한 처리가 필요했습니다
테스트 전략 및 댓글 복잡성
마이그레이션 시간의 대부분은 프로덕션에서 tap compare 로그를 수동으로 검토하는 데 소비되었습니다.
차이가 나타나면 엔지니어가 코드를 조사하고, 문제를 수정하고, 특정 불일치가 더 이상 나타나지 않는지 확인했습니다. tap compare 로그는 차이만 캡처하므로 문제가 수정되면 해당 로그는 사라집니다.
이 프로덕션 중심 테스트 접근 방식은 효과가 있었지만 시간이 많이 걸렸습니다. 팀은 프로덕션에 배포하기 전에 더 포괄적인 로컬 테스트가 필요하다는 것을 깨달았습니다. 과제의 일부는 댓글 데이터의 엄청난 복잡성이었습니다.
댓글은 단순한 텍스트처럼 보일 수 있지만 Reddit의 댓글 모델에는 수많은 변형이 포함됩니다:
-
단순 텍스트 vs 리치 텍스트 포맷팅 vs 미디어 콘텐츠
-
다양한 차원 및 콘텐츠 유형의 사진 및 GIF
-
서브레딧별 워크플로(일부는 승인 상태가 필요한 Automod 사용)
-
첨부할 수 있는 다양한 유형의 어워드
-
다양한 조정 및 승인 상태
이러한 모든 변형은 단일 댓글이 시스템에서 표현될 수 있는 수천 가지 가능한 조합을 만듭니다. 초기 테스트 전략은 일반적인 사용 사례를 로컬로 다루고, 프로덕션의 "tap compare"에 의존하여 엣지 케이스를 표면화했습니다. 향후 마이그레이션을 위해 팀은 프로덕션에 배포하기 전에 실제 프로덕션 데이터를 사용하여 포괄적인 테스트 케이스를 생성할 계획입니다.
Go 대신 Python 마이크로서비스를 사용하지 않은 이유는?
이 시나리오에서 제기될 수 있는 중요한 질문은 다음과 같습니다: 언어 차이가 많은 문제를 일으켰다면 왜 Python 마이크로서비스를 만들지 않았을까요?
Python을 고수하면 직렬화 문제와 데이터베이스 액세스 패턴 변경을 완전히 피할 수 있었을 것입니다.
대답은 Reddit의 전략적 인프라 방향을 드러냅니다. Reddit의 인프라 조직은 여러 가지 이유로 Go에 대한 강력한 약속을 했습니다:
-
동시성 이점: 트래픽이 많은 서비스의 경우 Go는 Python보다 더 높은 처리량을 달성하면서 더 적은 수의 포드를 실행할 수 있습니다.
-
기존 생태계: Go는 이미 Reddit의 인프라 전체에서 널리 사용되고 있습니다.
-
더 나은 도구: 기존 Go 지원은 개발을 더 쉽고 일관되게 만듭니다.
엔지니어링 팀은 이 마이그레이션을 위해 Go만 고려했습니다. 그들의 관점에서 Go로 표준화하는 전략적 장기 이점은 언어 간 호환성의 단기 과제를 능가했습니다.
결론
마이그레이션은 완전히 성공했습니다. 모든 댓글 엔드포인트는 이제 사용자에게 제로 중단으로 새로운 Golang 마이크로서비스에서 실행됩니다. 댓글은 레거시 모놀리스에서 완전히 벗어난 Reddit의 네 가지 핵심 모델 중 첫 번째가 되었습니다.
주요 목표가 안정성을 개선하면서 성능 패리티를 유지하는 것이었지만, 마이그레이션은 예상치 못한 보너스를 제공했습니다: 세 가지 마이그레이션된 쓰기 엔드포인트 모두 p99 레이턴시가 절반으로 줄었습니다. p99 레이턴시는 가장 느린 1%의 요청이 얼마나 오래 걸리는지 측정하며, 이러한 느린 요청이 최악의 사용자 경험을 나타내기 때문에 중요합니다.
개선은 상당했습니다:
-
레거시 Python 서비스는 때때로 15초에 달하는 레이턴시 스파이크를 가졌습니다
-
새로운 Go 서비스는 일관되게 더 낮고 더 안정적인 레이턴시를 보여줍니다
-
일반적인 레이턴시는 100밀리초 미만을 유지합니다
다양한 시나리오에 대한 레이턴시 개선을 보여주는 아래 차트를 참조하세요:

마이그레이션은 또한 향후 작업을 위한 귀중한 교훈을 제공했습니다:
-
데이터베이스 버전 관리는 비교되는 데이터의 버전을 추적하여 경쟁 조건을 적절하게 처리하는 데 필수적입니다
-
실제 프로덕션 데이터를 기반으로 한 포괄적인 로컬 테스트는 프로덕션에서 디버깅 시간을 줄일 것입니다
-
데이터베이스 모니터링은 애플리케이션 로직을 변경할 때뿐만 아니라 서비스가 데이터에 액세스하는 방식을 변경할 때도 중요합니다
-
엔드 투 엔드 검증은 바이트 수준 데이터 비교뿐만 아니라 실제 다운스트림 소비자를 포함해야 합니다
-
사용자 정의 도구는 수동 검토 프로세스의 일부를 자동화하는 데 도움이 됩니다(경쟁 조건 감지 코드와 같이)
나머지 핵심 모델을 계속 마이그레이션하면서(계정은 완료되었고 게시물과 서브레딧은 진행 중입니다), 이러한 교훈은 각 후속 마이그레이션을 더 원활하게 만들 것입니다.
참고 자료:
Related Articles
Thank you for reading.