preview image
허원철의 개발 블로그

AWS Aurora DB Cluster - FailOver 대응하기

2021-02-13

최근 회사에서 오로라디비를 스케일 업하기 위해서 failover 테스트를 하다가 있었던 이슈를 공유합니다.

진행 플로우

진행했던 플로우는 다음과 같습니다.

1
2
3
slave 스케일 업
slave를 master로 승격(failover)
slave 스케일 업

에러 발생

1번의 경우, 인스턴스가 내려갔다 올라오기 때문에 잠시 동안 slave를 사용할 수 없게 됩니다. 하지만, cluster end-point를 사용하게 되면 자동적으로 slave로 가던 트래픽이 master를 바라보게 됩니다.
2번의 경우, master와 slave가 바뀌니 아주 잠깐의 순단이 있겠지만 이는 내부적으로 커넥션이 다시 맺어 정상 동작할것이라고 생각했습니다.

하지만… 2번을 진행함과 동시에 서버에서는 다음과 같은 예외가 출력되고 있었습니다.

1
The MySQL server is running with the --read-only option so it cannot execute this statement

자동으로 커낵션을 다시 맺어줄꺼란 생각이 틀렸고, 구글링을 통해 다음과 같은 게시물을 찾을 수 있었습니다.
(참고: https://aws.amazon.com/ko/premiumsupport/knowledge-center/aurora-mysql-db-cluser-read-only-error/)

해결방법

1. 클러스터 라이터 엔드포인트 활용

우리 서비스에서는 이미 cluster endpoint를 사용하고 있기 때문에 해당이 안되는 내용이였습니다. 혹시 instance endpoint를 사용하고 있다면 cluster endpoint를 사용하길 권장드립니다.

2. DNS를 과도하게 캐시하지 않음

이번에 알게된 사실이지만 JVM 애플리케이션이 실행된 이우에 DNS 캐시하게 됩니다. 이는 jdk 구현체 마다 옵션이 다르다고 알고 있지만 오라클 jdk를 사용하는 경우는 이를 무기한으로 가지게 됩니다. 변경이 되지 않는다는 얘기죠. 그래서 우리는 networkaddress.cache.ttl를 추가하여 테스트 해보기로 했습니다만… 동일한 예외가 발생했습니다.

3. 스마트 드라이버 사용

마지막 방법으로 커넥터를 변경하는 것입니다. 현재 사용 중인 커넥터는 mysql-connector였고, 이를 mariadb-connector로 변경하는 것이였습니다. 조금 더 찾아보니 mariadb-connector에는 mysql-connector와 달리 failover에 대한 대응이 가능한 옵션을 제공해주고 있었습니다.
(참고: https://mariadb.com/kb/en/failover-and-high-availability-with-mariadb-connector-j/#specifics-for-amazon-aurora)

1
jdbc:mysql:aurora:.....

AWS 커뮤니티에도 질문을 남겼지만 대다수의 분들이 mariadb-connector로 변경해서 해결했다는 것을 알 수 있었습니다.

그리고 이제는 마지막일거라고 생각하고 테스트를 했고 성공했습니다…! 조금 더 코드를 살펴보니 실패 시에 내부적으로 커낵션을 다시 맺는 과정이 포함되어 있는 것을 알 수 있었습니다.

또 다른 이슈

커낵션에 대한 부분은 해결했지만… 다른 예외가 발생합니다. connector를 변경함으로써 발생한 문제인데요. 이런 문제가 발생할 수 있구나 했습니다.

안타까운 현실이지만 서비스에서 SQL Mapper인 Mybatis를 사용하며, datetime의 컬럼 값을 String으로 받는 케이스가 있었습니다. 기존 mysql-connector에서는 이 값이 2021-02-07 17:19:00으로 할당 됐었다면, mariadb-connector를 사용하면 2021-02-07 17:19:00.0으로 할당 됩니다.

AS-IS:

  • mysql-connector
  • 2021-02-07 17:19:00

TO-BE

  • mariadb-connector
  • 2021-02-07 17:19:00.0

어떻게 된 일까요? 잠깐 코드를 디버깅해보니 이는 커넥터 구현체의 차이에서 서로 다른 응답을 주는 것을 알 수 있었습니다.

mysql-connector
1
2
3
4
public String createFromTimestamp(InternalTimestamp its) {
return String.format("%s %s", createFromDate(its), // 2021-02-07 17:19:00
createFromTime(new InternalTime(its.getHours(), its.getMinutes(), its.getSeconds(), its.getNanos(), its.getScale())));
}
mariadb-connector
1
2
3
4
5
6
7
8
9
10
case DATETIME:
Timestamp timestamp = getInternalTimestamp(columnInfo, cal, timeZone);
if (timestamp == null) {
if ((lastValueNull & BIT_LAST_ZERO_DATE) != 0) {
lastValueNull ^= BIT_LAST_ZERO_DATE;
return new String(buf, pos, length, StandardCharsets.UTF_8);
}
return null;
}
return timestamp.toString(); // 2021-02-07 17:19:00.0

대략적으로 정리하자면, mysql-connector는 내부적으로 String.format을 사용하여 YYYY-MM-dd HH:mm:ss 형태를 만들어주는 듯 보입니다. 반면, mariadb-connectorTimestamp.toString()을 사용합니다.

mybatis - typeHandler

우리는 이것 때문에 비즈니스 로직을 건드는 것은 크리티컬한 이슈가 발생할 수 있다고 판단하여 고민 끝에 Mybatis의 TypeHandler를 이용하기로 했습니다. (JPA를 사용하는 경우, AttributeConverter를 사용할 수 있습니다.)

이미 기본적인 typeHandler로 구현되어 있는 것을 어느정도 커스텀하면 비즈니스 로직을 수정하지 않고도 간단히 커넥터 변경 이슈를 수정할 수 있었고, 성공적으로 테스트를 완료할 수 있었습니다.