레코드를 만들고 바로 그 값을 조회해 반환하는 코드가 간헐적으로 실패했다. 방금 넣은 것을 못 찾는다는 오류였다.
Table of contents
Open Table of contents
읽기와 쓰기가 갈린 경로
앞단에 라우터가 있어서 쿼리를 종류에 따라 나눠 보내고 있었다. 삽입은 주 서버로 가고 조회는 복제본으로 간다.
복제에는 몇 밀리초의 지연이 있고 그 사이에 조회하면 아직 없는 상태다. 값이 없는 것이 아니라 아직 도착하지 않은 것이었다.
트랜잭션이면 주 서버라는 말의 조건
명시적 트랜잭션 안에 넣으면 같은 서버로 간다는 말을 들었지만 조건이 붙어 있었다. 라우터 설정을 직접 열어 보니 낙관적 트랜잭션 옵션이 꺼져 있어서 평문 트랜잭션은 주 서버로 간다.
다만 읽기 전용을 명시한 트랜잭션은 그 옵션과 무관하게 복제본으로 갈 수 있었다. 우리가 쓰는 라이브러리가 어느 쪽을 만드는지 확인하니 평문이었다.
같은 세션이 아니면 보장 밖
설정에는 쓰기 직후 읽기의 일관성을 세션 단위로 보장하는 항목도 있었다. 라우터가 복제 위치를 확인하고 기다려 주지만 그 범위가 같은 세션 안이다.
우리는 커넥션 풀을 쓰므로 삽입과 조회가 서로 다른 커넥션을 잡을 수 있다. 라우터 입장에서 다른 세션이 되고 보장이 사라지는 것이 간헐적 실패의 이유였다.
한 커넥션을 점유하는 트랜잭션
해결은 두 쿼리를 하나의 명시적 트랜잭션에 넣는 것이었다. 트랜잭션은 한 커넥션을 점유하므로 삽입과 조회가 같은 세션이 된다.
평문 트랜잭션이라 주 서버로 가고 같은 커넥션이라 일관성 보장도 유효하다. 두 조건이 동시에 충족되어야 이 패턴이 안전해진다.
전제가 우리 설정에서 참인지 확인했다
수정하기 전에 그 전제가 우리 설정에서 참인지를 따로 봤다. 들은 말이 일반적으로 맞아도 이 환경에서 맞는지는 별개이기 때문이다.
라우터 설정 파일을 읽고 런타임에 덮어쓰는 경로가 있는지도 확인했다. 관리 인터페이스는 접근이 막혀 있어서 정적 설정과 덮어쓰기 부재 두 가지로 확인했고 하나만 봤으면 런타임에 바뀌었을 가능성이 남았을 것이다.
이 결론과 삽입 직후 재조회 패턴은 명시적 트랜잭션으로 묶는다는 규칙을 문서에 적었다. 코드 리뷰 항목에도 넣었는데 목적은 그 패턴 자체를 알아보게 하는 것이었다.
정리
- 읽기와 쓰기가 갈린 구성에서 삽입 직후 조회는 복제 지연에 걸린다
- 값이 없는 것이 아니라 아직 도착하지 않은 것이다
- 트랜잭션이 주 서버로 가는 것은 평문일 때다
- 읽기 전용을 명시하면 복제본으로 갈 수 있다
- 쓰기 직후 읽기 보장은 같은 세션 안에서만 유효하다
- 커넥션 풀은 다른 커넥션을 줄 수 있어 세션이 갈린다
- 명시적 트랜잭션은 한 커넥션을 점유해 두 조건을 동시에 만족한다
- 전제가 우리 설정에서 참인지 확인하고 그 결론을 문서에 적는다