DB 페일오버했는데 앱이 옛 주소로 붙는다 — DNS 캐시

주 DB 가 죽고 대기 서버가 몇 초 만에 승격됐는데도 서비스는 계속 죽어 있어요. 앱이 여전히 옛 주소로 붙고 있었거든요. 전환 사실이 앱까지 늦게 닿는 경로와 그 시간을 줄이는 법을 정리했어요.

주 DB 가 죽었어요. 다행히 대기 서버가 있었고, 자동 전환이 정상적으로 동작해서 몇 초 만에 새 주 서버가 승격됐어요.

그런데 서비스는 계속 죽어 있어요. 애플리케이션이 여전히 옛 주소로 접속을 시도하고 있거든요.

DNS 레코드는 바뀌었어요. 그런데 애플리케이션이 그걸 안 봐요. 이름을 한 번 해석한 뒤로는 그 결과를 계속 쓰고 있었어요.

전환은 성공했는데 그 사실이 전달되지 않은 상태예요.

그리고 이건 자동 전환을 구성할 때 대개 시험하지 않는 부분이에요. 승격까지만 확인하고 끝내거든요. 전환이 끝났다는 판단 기준 자체가 틀린 겁니다.

왜 그런가

페일오버는 두 단계로 이뤄져요. 새 주 서버를 정하는 것과, 그 사실을 클라이언트들에게 알리는 것.

첫 번째는 대개 잘 동작해요. 감지하고 승격하는 절차가 자동화되어 있고 테스트도 되어 있으니까요.

문제는 두 번째예요. 클라이언트가 새 주소를 알게 되는 경로가 여러 가지고, 각각 반영 시간이 달라요.

DNS 로 알리는 경우, 레코드의 수명만큼 옛 값이 캐시돼요. 그리고 더 나쁜 건 애플리케이션이나 런타임이 자체적으로 결과를 캐시하는 경우예요. 어떤 런타임은 이름 해석 결과를 프로세스가 살아 있는 동안 영원히 들고 있어요. 그러면 재시작 전까지는 절대 안 바뀝니다.

커넥션 풀도 문제예요. 이미 맺어진 연결은 옛 서버를 향하고 있어요. 그 서버가 완전히 죽었으면 연결이 끊기면서 새로 맺겠지만, 반쯤 살아 있으면 연결이 유지된 채로 응답만 안 와요. 앞서 본 그 상황이죠.

그래서 전환의 성공은 승격 시점이 아니라 마지막 클라이언트가 새 주소로 붙은 시점에 판단해야 해요. 그리고 그 시간은 대개 아무도 측정하지 않습니다.

코드로 보면

알리는 경로를 짧게 만든다:

코드
· DNS 로 알린다면 수명을 짧게
 TTL 30초 이하 (평시 부하는 감수)
 그래도 애플리케이션이 자체 캐시하면 소용없다

· 런타임의 이름 해석 캐시를 확인한다
 어떤 런타임은 성공한 해석을 영원히 캐시한다
 캐시 수명을 명시적으로 짧게 설정한다

· 더 확실한 방법 — 클라이언트가 여러 주소를 안다
 접속 문자열에 모든 노드를 나열하고
 드라이버가 현재 주 서버를 스스로 찾게 한다

· 또는 앞에 프록시를 둔다
 애플리케이션은 항상 프록시만 본다
 프록시가 뒤쪽 전환을 흡수한다

✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.

실전에서는

알리는 방법이 몇 가지 있고 각각 성격이 달라요.

DNS 는 가장 흔하지만 가장 느려요. 수명을 짧게 잡아도 여러 겹의 캐시가 있고, 애플리케이션이 자체 캐시하면 통제가 안 돼요. 그래도 구성이 단순해서 많이 씁니다. 쓸 거면 애플리케이션 쪽 캐시 설정까지 반드시 확인해야 해요.

가상 IP 를 옮기는 방법은 빠르지만 같은 네트워크 안에서만 돼요. 그리고 옛 서버가 그 주소를 놓지 않으면 두 서버가 같은 주소를 갖는 상태가 될 수 있어요.

드라이버가 스스로 찾는 방법이 요즘 흔해요. 접속 문자열에 모든 노드를 나열하면 드라이버가 어느 게 주 서버인지 확인하고 붙어요. 전환이 일어나면 드라이버가 감지해서 다시 찾고요. DNS 를 안 거치니 빠르고, 애플리케이션 코드도 안 바뀌어요.

프록시를 두는 방법도 있어요. 애플리케이션은 항상 프록시만 보고, 프록시가 뒤쪽 전환을 흡수해요. 애플리케이션 쪽 설정이 아예 필요 없다는 게 장점이고, 프록시 자체가 단일 실패 지점이 되지 않게 이중화해야 한다는 게 대가예요.

그리고 앞 편의 이중 주 서버 문제를 여기서도 조심해야 해요. 옛 주 서버가 자기가 여전히 주 서버라고 믿고 쓰기를 받으면 데이터가 갈려요. 승격할 때 옛 서버를 확실히 차단하는 절차가 함께 있어야 합니다.

마지막으로 애플리케이션 쪽 준비도 필요해요. 전환 중에는 연결이 끊기고 몇 초간 실패해요. 그 실패를 재시도로 넘길 수 있어야 하고, 앞 편에서 본 대로 그 재시도가 증폭되지 않아야 합니다.

처방

  1. 전환을 실제로 연습하세요. 승격까지가 아니라 마지막 클라이언트가 새 주소로 붙을 때까지 재세요.
  2. 애플리케이션의 이름 해석 캐시 설정을 확인하세요. 런타임 기본값이 영원인 경우가 있습니다.
  3. 드라이버가 여러 노드를 알고 스스로 찾게 하세요. 지원한다면 가장 확실한 방법입니다.
  4. 프록시를 두는 방법도 검토하세요. 애플리케이션 설정이 필요 없어집니다.
  5. 옛 주 서버를 확실히 차단하는 절차를 넣으세요. 두 서버가 동시에 쓰기를 받으면 데이터가 갈립니다.
  6. 전환 중 실패를 재시도로 넘길 수 있게 하세요. 다만 증폭되지 않게 예산을 두세요.
  7. 커넥션 풀이 죽은 연결을 감지하고 버리는지 확인하세요. 유효성 검사 설정이 있습니다.
  8. 전환 소요 시간을 지표로 남기세요. 연습할 때마다 이 값이 줄어드는지 봐야 합니다.
  9. 전환 뒤 새 주 서버의 상태를 확인하세요. 혼자 전체 부하를 받고, 캐시도 비어 있고, 복제본도 없는 상태입니다.

지금 확인해 보세요

전환은 언제 끝나는가:

코드
· 승격 시각이 아니라 마지막 클라이언트가 붙은 시각을 잰다
 애플리케이션의 DB 연결 실패가 0 이 되는 시점
 이 값을 안 재면 전환이 성공했는지도 모른다

· 이름 해석 캐시 설정 확인
 런타임·JVM·OS·라이브러리 각각의 설정을 본다
 기본값이 '영원히' 인 경우가 실제로 있다

· 강제 종료로 연습한다 — 정상 종료는 실제와 다르다
 프로세스를 강제로 죽이거나 네트워크를 막는다

· 복제본도 새 주 서버를 따라갔는가
SELECT * FROM pg_stat_replication;   -- 새 주 서버에서
 비어 있으면 복제가 끊긴 채로 남은 것이다

핵심 한 줄전환은 승격이 아니라 마지막 클라이언트가 붙었을 때 끝난다.

브루니가 적어요

색을 바꾸며 숨는 카멜레온이에요. 운영에서 실제로 터지는 문제를 한 편씩 적어요. 같은 실수를 두 번 하지 않으려고 쓰는 기록이에요.

장애·확장 글 23편 모두 보기 →