Redis PSYNC2 bug post mortem

Salvatore Sanfilippo

Redis PSYNC2 버그 포스트모템

원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기

나흘 전 한 사용자가 Redis GitHub 저장소에 치명적인 이슈를 등록했다. 문제는 새로 도입된 Redis 4.0의 PSYNC2 복제 프로토콜과 관련된 것으로 매우 심각했다. PSYNC2는 Redis 복제에 여러 장점을 가져다준다. 페일오버 이후는 물론 슬레이브가 제어된 방식으로 재시작된 이후에도 전체 데이터셋이 아니라 차이분만 교환해 재동기화할 수 있다는 점이 대표적이다. 문제는 바로 이 후자의 기능에 관한 것이었다. PSYNC2에서는 RDB 파일에 복제 정보가 함께 담긴다. 슬레이브가 재시작되면 복제 메타데이터가 다시 로드되고, 슬레이브는 마스터와 핸드셰이크를 시도해 마지막 연결이 끊긴 이후의 차이분을 받아오는 PSYNC 시도를 수행할 수 있다.

운영 관점에서 보면 이 모든 것은 좋은 소식이지만, PSYNC2 자체는 Redis 4.0.0 정식 버전부터 꽤 안정적이었던 반면 슬레이브 재시작과 관련된 기능은 확실히 신뢰성이 떨어졌다. 이 기능에는 두 가지 문제가 있었다. 첫째, PSYNC2에 막판에 추가된 기능이라 원래 설계 문서에 포함되어 있지 않았다는 점이다. PSYNC2에서 진행한 작업의 당연한 연장선에 가까웠지만, 명세의 나머지 부분만큼 꼼꼼하게 잠재적 버그와 이슈를 검토하지는 못했다. 둘째, 이 기능이 겉보기보다 훨씬 복잡하다는 점이다. 재시작 후 슬레이브 측 복제 상태 *전체*를 정확히 복원하는 것이 까다롭기 때문이다. 게다가 상태의 일부를 제대로 복원하지 못하더라도 대부분의 경우 눈에 띄는 버그로 이어지지 않아 통합 테스트로 발견하기 어렵다. 특정 조건이 충족될 때만 일부 상태의 누락이 문제로 드러난다. 예를 들어 슬레이브 복제 상태에서 현재 선택된 DB를 제대로 복원하지 못하면 서로 다른 Redis DB에 쓰기가 발생할 때만 문제가 생기며, 그런 버그는 특수한 조건에서만 현재 선택된 DB를 올바르게 저장하지 못한다.

많은 Redis 컨트리뷰터들의 도움, 특히 Alibaba의 Redis 개발자인 GitHub 사용자 @soloestoy의 도움 덕분에 최근 우리는 PSYNC2의 여러 잠재적 문제들을 개선하는 작업을 진행했다. 지금까지 작성된 모든 패치에 대한 충분한 테스트를 거쳐, 며칠 내로 그 작업 결과물이 마침내 Redis 4.0.3에 담길 예정이었다. 하지만 이슈 #4483을 접수한 뒤, 지금까지 발견한 다른 Redis 4의 PSYNC2 이슈들보다 훨씬 심각하다는 것을 깨닫고 Redis 4.0.3 릴리스를 서둘러야겠다고 판단했다.

이슈 #4483에 설명된 버그는 상당히 단순했지만 매우 치명적이었다. 슬레이브가 재시작되고 그 결과 RDB로부터 복제 상태가 다시 로드된 뒤, 마스터의 복제 백로그에 EVALSHA 명령어 형태의 Lua 스크립트 실행 기록이 포함될 수 있었다. 하지만 슬레이브의 Lua 스크립팅 엔진은 재시작 후 모든 스크립트가 비워지기 때문에 해당 명령어를 처리할 수 없다. 그 결과 슬레이브는 Lua 스크립트에서 비롯된 쓰기를 처리하지 못하게 된다. 해당 스크립트가 기본값이 아닌 ‘커맨드 복제’를 사용하고 있는 경우가 아니라면 말이다. 기본값은 스크립트 자체를 복제하는 방식이다.

나는 다소 패닉 상태에 빠졌고… 몇 가지 대체 패치를 작성해 이슈에 제출했다. 결국 이전 버전과의 RDB 호환성을 깨뜨리지 않는 패치가 채택되었고, 나는 곧바로 Redis 4.0.3을 최대한 빨리 릴리스했다. 하지만 실수를 저질렀다… 나는 몇 주 전부터 집이 아닌 사무실에서 일하고 있었다. 평소에는 밤에 일하지 않는데, 4.0.3을 준비하면서는 집에 돌아온 뒤 집 노트북을 열고 해당 패치를 4.0 브랜치에 머지한 뒤 몇 가지 테스트를 진행했다. 다음 날 사무실로 돌아와 다른 컴퓨터에서 작업을 이어갔는데, 집에서 머지하고 저장소에 푸시하지 않은 커밋 하나가 빠졌다는 사실을 깨닫지 못했다.

결국 나는 복제 버그에 대한 가장 중요한 수정을 제외한 나머지 PSYNC2 수정사항만 포함된 4.0.3을 릴리스한 셈이 되었다. 나는 곧바로 해당 복제 수정을 포함한 새로운 패치 버전인 Redis 4.0.4를 릴리스했다. 이미 이것만으로도 좋지 않은 상황이었다. Redis 업그레이드는 계획된 작업인데, 내가 릴리스를 준비하며 실수했다는 이유로 누구도 두 번씩 업그레이드하고 싶어 하지 않기 때문이다… 하지만 최악은 아직 남아 있었다. PSYNC2를 수행하는 재시작된 슬레이브에서의 스크립트 복제와 관련해 4.0.4에 추가한 수정에는 모든 복제 통합 테스트를 완전히 통과해 버린 오류가 있었다. 해당 수정은 나중에 다시 로드할 수 있도록 슬레이브 메모리에 있는 Lua 스크립트를 그대로 RDB에 저장하는 방식이었다. 하지만 스크립트를 로드하는 함수가 이미 메모리에 있는 스크립트가 있으면 assert를 발생시킨다는 점을 고려하지 못했다. 그래서 슬레이브가 마스터로부터 전체 동기화를 받게 되면 RDB 파일을 로드하기 시작하자마자 이미 메모리에 있는 중복 스크립트 때문에 즉시 크래시가 발생했다. 한 사용자가 Twitter를 통해 이를 바로 제보했고, 나는 제보 시점부터 4.0.5가 배포될 때까지 45분도 채 되지 않아 문제를 수정했지만, 이러한 수정 전달 과정에서의 연쇄적인 실패가 초래한 잠재적 문제들을 덮을 수는 없다.

위와 같은 문제들은 여러 가지 원인으로 발생했다:

  1. Redis 4.0은 PSYNC2의 복잡성과 코드 변경량을 고려하면 정식 버전으로 너무 일찍 출시되었다. 다음부터는 릴리스가 지연되더라도 더 기다리고, 새로운 메이저 버전의 마지막 릴리스 후보 단계에서 더 많은 시간을 확보해 GA 버전을 내보내기 전에 이런 버그들을 찾을 수 있도록 하겠다.
  2. 이슈 #4483에 대한 수정을 제공하려 너무 서둘렀다. 버그가 치명적이었더라도 무슨 일이 일어나고 있는지, 그리고 수정 자체가 야기할 수 있는 잠재적 문제를 확인하기 위해 시간을 들이는 편이 더 나았다. 게다가 너무 급한 나머지 4.0.3을 구성하는 커밋들을 충분히 꼼꼼히 확인하지 않아 수정 중 하나가 빠졌고, 새로운 릴리스가 필요하게 되었다.
  3. Redis 4.0에는 매 단계마다 데이터 일관성을 검사하며 연속적인 페일오버를 시뮬레이션하는 테스트를 포함해 매우 엄격한 PSYNC2 유닛 및 통합 테스트가 있지만, 이번 버그를 통해 테스트가 PSYNC2와 Lua 스크립트 복제를 함께 섞어서 시도해 본 적이 없다는 사실을 알게 되었다. 이 부분은 반드시 개선되어야 한다. 또한 슬레이브 RDB 재시작 이후의 PSYNC2 복제에 대한 테스트도 강화해야 한다.

나는 먼저 ‘3’에 대한 개선부터 시작할 것이며, 앞으로는 새로운 릴리스를 할 때마다 ‘1’과 ‘2’의 교훈을 되새길 것이다. 이번 일로 인해 내 실수로 불편을 겪은 모든 분께 진심으로 사과드리며, 엄청난 도움과 지원을 보내준 @soloestoy에게 큰 감사를 전한다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글