DB 페일오버했는데 앱이 옛 주소로 붙는다 — DNS 캐시
주 DB 가 죽고 대기 서버가 몇 초 만에 승격됐는데도 서비스는 계속 죽어 있어요. 앱이 여전히 옛 주소로 붙고 있었거든요. 전환 사실이 앱까지 늦게 닿는 경로와 그 시간을 줄이는 법을 정리했어요.
주 DB 가 죽었어요. 다행히 대기 서버가 있었고, 자동 전환이 정상적으로 동작해서 몇 초 만에 새 주 서버가 승격됐어요.
그런데 서비스는 계속 죽어 있어요. 애플리케이션이 여전히 옛 주소로 접속을 시도하고 있거든요.
DNS 레코드는 바뀌었어요. 그런데 애플리케이션이 그걸 안 봐요. 이름을 한 번 해석한 뒤로는 그 결과를 계속 쓰고 있었어요.
전환은 성공했는데 그 사실이 전달되지 않은 상태예요.
그리고 이건 자동 전환을 구성할 때 대개 시험하지 않는 부분이에요. 승격까지만 확인하고 끝내거든요. 전환이 끝났다는 판단 기준 자체가 틀린 겁니다.
왜 그런가
페일오버는 두 단계로 이뤄져요. 새 주 서버를 정하는 것과, 그 사실을 클라이언트들에게 알리는 것.
첫 번째는 대개 잘 동작해요. 감지하고 승격하는 절차가 자동화되어 있고 테스트도 되어 있으니까요.
문제는 두 번째예요. 클라이언트가 새 주소를 알게 되는 경로가 여러 가지고, 각각 반영 시간이 달라요.
DNS 로 알리는 경우, 레코드의 수명만큼 옛 값이 캐시돼요. 그리고 더 나쁜 건 애플리케이션이나 런타임이 자체적으로 결과를 캐시하는 경우예요. 어떤 런타임은 이름 해석 결과를 프로세스가 살아 있는 동안 영원히 들고 있어요. 그러면 재시작 전까지는 절대 안 바뀝니다.
커넥션 풀도 문제예요. 이미 맺어진 연결은 옛 서버를 향하고 있어요. 그 서버가 완전히 죽었으면 연결이 끊기면서 새로 맺겠지만, 반쯤 살아 있으면 연결이 유지된 채로 응답만 안 와요. 앞서 본 그 상황이죠.
그래서 전환의 성공은 승격 시점이 아니라 마지막 클라이언트가 새 주소로 붙은 시점에 판단해야 해요. 그리고 그 시간은 대개 아무도 측정하지 않습니다.
코드로 보면
알리는 경로를 짧게 만든다:
· DNS 로 알린다면 수명을 짧게
TTL 30초 이하 (평시 부하는 감수)
그래도 애플리케이션이 자체 캐시하면 소용없다
· 런타임의 이름 해석 캐시를 확인한다
어떤 런타임은 성공한 해석을 영원히 캐시한다
캐시 수명을 명시적으로 짧게 설정한다
· 더 확실한 방법 — 클라이언트가 여러 주소를 안다
접속 문자열에 모든 노드를 나열하고
드라이버가 현재 주 서버를 스스로 찾게 한다
· 또는 앞에 프록시를 둔다
애플리케이션은 항상 프록시만 본다
프록시가 뒤쪽 전환을 흡수한다✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.
실전에서는
알리는 방법이 몇 가지 있고 각각 성격이 달라요.
DNS 는 가장 흔하지만 가장 느려요. 수명을 짧게 잡아도 여러 겹의 캐시가 있고, 애플리케이션이 자체 캐시하면 통제가 안 돼요. 그래도 구성이 단순해서 많이 씁니다. 쓸 거면 애플리케이션 쪽 캐시 설정까지 반드시 확인해야 해요.
가상 IP 를 옮기는 방법은 빠르지만 같은 네트워크 안에서만 돼요. 그리고 옛 서버가 그 주소를 놓지 않으면 두 서버가 같은 주소를 갖는 상태가 될 수 있어요.
드라이버가 스스로 찾는 방법이 요즘 흔해요. 접속 문자열에 모든 노드를 나열하면 드라이버가 어느 게 주 서버인지 확인하고 붙어요. 전환이 일어나면 드라이버가 감지해서 다시 찾고요. DNS 를 안 거치니 빠르고, 애플리케이션 코드도 안 바뀌어요.
프록시를 두는 방법도 있어요. 애플리케이션은 항상 프록시만 보고, 프록시가 뒤쪽 전환을 흡수해요. 애플리케이션 쪽 설정이 아예 필요 없다는 게 장점이고, 프록시 자체가 단일 실패 지점이 되지 않게 이중화해야 한다는 게 대가예요.
그리고 앞 편의 이중 주 서버 문제를 여기서도 조심해야 해요. 옛 주 서버가 자기가 여전히 주 서버라고 믿고 쓰기를 받으면 데이터가 갈려요. 승격할 때 옛 서버를 확실히 차단하는 절차가 함께 있어야 합니다.
마지막으로 애플리케이션 쪽 준비도 필요해요. 전환 중에는 연결이 끊기고 몇 초간 실패해요. 그 실패를 재시도로 넘길 수 있어야 하고, 앞 편에서 본 대로 그 재시도가 증폭되지 않아야 합니다.
처방
- 전환을 실제로 연습하세요. 승격까지가 아니라 마지막 클라이언트가 새 주소로 붙을 때까지 재세요.
- 애플리케이션의 이름 해석 캐시 설정을 확인하세요. 런타임 기본값이 영원인 경우가 있습니다.
- 드라이버가 여러 노드를 알고 스스로 찾게 하세요. 지원한다면 가장 확실한 방법입니다.
- 프록시를 두는 방법도 검토하세요. 애플리케이션 설정이 필요 없어집니다.
- 옛 주 서버를 확실히 차단하는 절차를 넣으세요. 두 서버가 동시에 쓰기를 받으면 데이터가 갈립니다.
- 전환 중 실패를 재시도로 넘길 수 있게 하세요. 다만 증폭되지 않게 예산을 두세요.
- 커넥션 풀이 죽은 연결을 감지하고 버리는지 확인하세요. 유효성 검사 설정이 있습니다.
- 전환 소요 시간을 지표로 남기세요. 연습할 때마다 이 값이 줄어드는지 봐야 합니다.
- 전환 뒤 새 주 서버의 상태를 확인하세요. 혼자 전체 부하를 받고, 캐시도 비어 있고, 복제본도 없는 상태입니다.
지금 확인해 보세요
전환은 언제 끝나는가:
· 승격 시각이 아니라 마지막 클라이언트가 붙은 시각을 잰다
애플리케이션의 DB 연결 실패가 0 이 되는 시점
이 값을 안 재면 전환이 성공했는지도 모른다
· 이름 해석 캐시 설정 확인
런타임·JVM·OS·라이브러리 각각의 설정을 본다
기본값이 '영원히' 인 경우가 실제로 있다
· 강제 종료로 연습한다 — 정상 종료는 실제와 다르다
프로세스를 강제로 죽이거나 네트워크를 막는다
· 복제본도 새 주 서버를 따라갔는가
SELECT * FROM pg_stat_replication; -- 새 주 서버에서
비어 있으면 복제가 끊긴 채로 남은 것이다핵심 한 줄전환은 승격이 아니라 마지막 클라이언트가 붙었을 때 끝난다.