Quantcast
Channel: Charsyam's Blog
Viewing all articles
Browse latest Browse all 122

[입 개발] MariaDB Connector 와 AWS Aurora

$
0
0

먼저 저는 자바를 잘 모르고 AWS Aurora도 잘 모르고 MariaDB Connector도 잘 모르는 초초보에 자바맹인것을 먼저 밝히고 해당 글을 작성합니다.

지인 분의 서비스가 Aurora RDS Mysql 을 쓰다가 Failover 를 한다고 해서 뭔가 잘못된 정보를 드렸다가, 자세히 보다보니, AWS Aurora의 Manual Scale Up을 하기 위해서는 다음과 같이 하면 된다고 합니다.

https://svrlove.blogspot.com/2019/05/aws-aurora-rds.html

요약하면, Reader를 먼저 Scale Up 하고 Failover 하면 된다고, 그냥 하면 될꺼라고 알려드려서 죄송합니다. 흑…

그런데 그 얘기를 듣고 나서, 다시 들은 얘기가 select 가 Reader로 가고 있다라는 것이었습니다. 이게 제가 듣기에는 완전히 이상한게… Connector 가 단순히 자동으로 Write는 Primary에게, Read는 Replica 에게 쿼리를 전달해 준다면, 굉장히 편리하긴 한데(완전히 다 좋은 건 아니지만…), 이렇게 될 경우 다음과 같이 Replication Lag가 발생하면, Write 후에 Read를 할 경우 새롭게 Write한 데이터가 아니라, 과거의 데이터를 읽을 수 있습니다. 다음과 같이 Primary DB에 A,B가 저장되면 이것은 Replica 에 다시 A,B가 저장되게 되는데 만약 Replica의 처리가 늦어져서 A, B가 아직 Replica에 반영되지 않았다면, 우리가 A를 select 하더라도 Replica에서는 데이터가 없다고 나올 것입니다.

그래서 이런 부분은 설정으로 명시적으로 이루어져야 사용자의 실수할 여지가 줄어들게 됩니다. 그런데 지인 분의 서버는 Select를 자동으로 Replica에서 했다고 하는 것입니다. 아무런 설정 없이…

그래서 먼저 관련 자료를 찾다보니, 인프런의 CTO 이신 창천항로님의 글이 보입니다. 일단 먼저 읽어보시길 권장합니다.

여길 보면 Aurora를 쓰면 @Transactional(readOnly=true) 를 붙여주면 자동으로 Reader 로 연결이 된다는 것입니다. 이걸 보고 제가 외친 한마디는 “이게 말이되?”, 어떻게 Spring JPA 쪽에서 Connection 정보를 알아서 연결이 됨? 이라는 의문에 쌓이게 되었습니다.

기본적으로 JPA에서 DataSource를 하나만 만들어주고 readOnly=true 가 셋팅이 되면 Connection에 Read Only를 설정하는 것으로 알고 있습니다.(그래봤자… DataSource 가 하나면 하나만 와야… 엉?)

그래서 일단 MariaDB Connector 소스를 받아봤습니다.(이때, 좀 더 조사부터 했어야 하는데… 내가 미…) MariaDB Connector는 다음 github에서 받을 수 있습니다. (https://github.com/mariadb-corporation/mariadb-connector-j)

그리고 열심히 소스를 보는데…(솔직히… 봐도 모르는…), 그러다가 먼저 CHANGELOG.md 파일을 보니 Aurora로 기능이 들어온 것들이 있습니다. 그중에 Relesae 1.5.1 관련해서 CONJ-325의 제목이 Aurora host auto-discovery 입니다. 이거다 싶어서 Aurora로 소스 코드를 검색해도 아무런 내용이 없습니다.

그래서 삽질로 spring-cloud-aws 의 JDBC 도 보러 갔다가… 아닌듯하여… 돌아오게 되는… 흑…(몰라~~~ 알수가 없어…)

그런데 지인분이 다음 링크를 던져주면서, LoadBalacing 기능이 Connector에 있다는 것을 알게 되었습니다.(https://mariadb.com/kb/en/failover-and-high-availability-with-mariadb-connector-j/?fbclid=IwAR2EnwLRBGc1T0bQLJTloP9WnisrjM0smV2h4bGa23UcT9Teq55gYVkwctI)

아래 내용을 보시면 이렇게 호스트 주소를 여러개 적어두면, 첫번째는 Primary, 나머지는 Replica로 동작한다는 것을 알 수 있고, @Transactional(readOnly=true) 를 설정하면 readOnly 용 Connection을 가져가서 아까 말한 것과 유사한 상황이 발생한다는 것을 알게 되었습니다.

여기까지 보면, 사실 눈치챈 분들도 계시겠지만, 저는 아주 초초보라, “jdbc:mysql://host/test” 이런 형태의 endpoint 만 사용해 보아서 사실… 전혀 눈치를 못챘던…

그런데, 이제 지인분이 다시 얘기해주시는 게 보이는데, connection url 에 “aurora” 가 포함되어 있다고 하시는 겁니다. 엉, 이게 뭐야… 하면서 위의 endpoint url을 다시보니… “replication” 이라는게 포함되어 있습니다. 그리고 다시 창천항로 님의 글을 보니… 거기도 jdbc url 이 “jdbc:mysql:aurora:…..” 이런식으로 되어있습니다.

그런데 최신 소스에서 아무리 검색해도 aurora라는 파일이나 String이 존재하지가 않습니다.(CHANGLELOG.md 제외) 그런데 replication 으로 검색을 해보니, HaMode.java(./src/main/java/org/mariadb/jdbc/export/HaMode.java) 라는 파일이 발견이 됩니다. 아래 내용을 보면 뭔가 Ha 관련 설정이 있는 것이 보입니다. 위의 jdbc url 의 HaMode 와 관련해서 뭔가 설정을 할 것 같은 부분이 보이는 거죠.

public enum HaMode {
  REPLICATION("replication") {
    public Optional<HostAddress> getAvailableHost(
        List<HostAddress> hostAddresses,
        ConcurrentMap<HostAddress, Long> denyList,
        boolean primary) {
      return HaMode.getAvailableHostInOrder(hostAddresses, denyList, primary);
    }
  },
  SEQUENTIAL("sequential") {
    public Optional<HostAddress> getAvailableHost(
        List<HostAddress> hostAddresses,
        ConcurrentMap<HostAddress, Long> denyList,
        boolean primary) {
      return getAvailableHostInOrder(hostAddresses, denyList, primary);
    }
  },
  LOADBALANCE("load-balance") {
    public Optional<HostAddress> getAvailableHost(
        List<HostAddress> hostAddresses,
        ConcurrentMap<HostAddress, Long> denyList,
        boolean primary) {
      // use in order not blacklisted server
      List<HostAddress> loopAddress = new ArrayList<>(hostAddresses);
      loopAddress.removeAll(denyList.keySet());
      Collections.shuffle(loopAddress);

      return loopAddress.stream().filter(e -> e.primary == primary).findFirst();
    }
  },

그리고 HaMode.java 에서 from이란 함수를 보면 다음과 같습니다. 이게 분명히 aurora 라는 문자가 있으면 HaMode 로 바꿔줘야 하는데 그런게 없네요.

  public static HaMode from(String value) {
    for (HaMode haMode : values()) {
      if (haMode.value.equalsIgnoreCase(value) || haMode.name().equalsIgnoreCase(value)) {
        return haMode;
      }
    }
    throw new IllegalArgumentException(
        String.format("Wrong argument value '%s' for HaMode", value));
  }

그래서 해당 github에서 해당 파일의 History를 뒤지다 보니 뭔가 이상합니다. 파일의 기록이 특정 시점 이전이 없는… -_- 그리고 버전을 보니 master branch 는 3.0.x 입니다. 그런데 주변에서 많이 사용하는 건 2.7.x네요. 급히 소스를 바꾸고 검색을 해보니… HaMode.java 가 옮겨졌는데 다음 위치에 있습니다. 우리가 찾던 AURORA가 있는 것이 보입니다. (./src/main/java/org/mariadb/jdbc/internal/util/constant/HaMode.java)

public enum HaMode {
  AURORA,
  REPLICATION,
  SEQUENTIAL,
  LOADBALANCE,
  NONE
}

그럼 관련 호스트 목록은 어디서 가져오는가? 2.7.x대에는 ./src/main/java/org/mariadb/jdbc/internal/failover/impl/AuroraListener.java 라는 파일이 존재하고 AuroraListener 에서 information_schema.replica_host_status 테이블에서 server_id에서 endpoint를 생성할 수 있습니다.

  private List<String> getCurrentEndpointIdentifiers(Protocol protocol) throws SQLException {
    List<String> endpoints = new ArrayList<>();
    try {
      proxy.lock.lock();
      try {
        // Deleted instance may remain in db for 24 hours so ignoring instances that have had no
        // change
        // for 3 minutes
        Results results = new Results();
        protocol.executeQuery(
            false,
            results,
            "select server_id, session_id from information_schema.replica_host_status "
                + "where last_update_timestamp > now() - INTERVAL 3 MINUTE");
        results.commandEnd();
        ResultSet resultSet = results.getResultSet();

        while (resultSet.next()) {
          endpoints.add(resultSet.getString(1) + "." + clusterDnsSuffix);
        }

        // randomize order for distributed load-balancing
        Collections.shuffle(endpoints);

      } finally {
        proxy.lock.unlock();
      }
    } catch (SQLException qe) {
      logger.warning("SQL exception occurred: " + qe.getMessage());
      if (protocol.getProxy().hasToHandleFailover(qe)) {
        if (masterProtocol == null || masterProtocol.equals(protocol)) {
          setMasterHostFail();
        } else if (secondaryProtocol.equals(protocol)) {
          setSecondaryHostFail();
        }
        addToBlacklist(protocol.getHostAddress());
        reconnectFailedConnection(new SearchFilter(isMasterHostFail(), isSecondaryHostFail()));
      }
    }

    return endpoints;
  }

그래서 결론부터 말하자면 jdbc url 의 endpoint 에 “jdbc:mysql:aurora” 라고 설정을 하면 mariadb connector 가 내부적으로 저런 작업을 다 해주게 됩니다. 그게 아니라면 HaMode의 다른 값을 보고 적절히 설정하시면 원하는 형태로 나눌 수 가 있게 됩니다. 코드를 대충 보셨으니 저 값을 쓰면 어떻게 되겠구나라고 보시면 될듯 합니다. 참고로 저런 정책을 파싱하는 부분은 ./src/main/java/org/mariadb/jdbc/UrlParser.java 를 보시면 잘 나와있습니다.

즉 aurora를 안 붙이면, 저렇게 동작하지 않는다는 얘기…

그런데… One More Thing…

MariaDB Connector 가 있고, Mysql Connector 가 있습니다. Mysql Connector는 과연 같은 걸 지원할까요? 그래서 Mysql Connector 소스도 까 보았는데, 재밌는건 “jdbc:mysql:[replication|loadbalance|failover]” 는 공통으로 지원이 됩니다. 그런데 Aurora는 안보이더군요.

사실 이 두 개의 Driver를 바꾸면 다음과 같이 Timestamp나 여러가지 다른 이슈들이 있을 수 있으므로 확인이 필요합니다.

다 끝나고 나서, 좋은 레퍼런스들이 나오기 시작했는데, 기계인간 이종립 님의 문서도 좋았습니다.

솔직히 저는 저런 문법을 처음 보았기 때문에 흑… 이해를 잘 못했는데, 다른 분들은 이미 다 아실듯한…

참고로 최종적으로 @Transactional(readOnly=True) 일 경우 Connection.setReadOnly를 호출하게 되는데, src/main/java/org/mariadb/jdbc/internal/failover/FailoverProxy.java 를 보게 되면 해당 함수가 호출되면 다음과 같은 작업을 하게 됩니다.

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      ......
      case METHOD_SET_READ_ONLY:
        this.listener.switchReadOnlyConnection((Boolean) args[0]);
      case METHOD_GET_READ_ONLY:
        return this.listener.isReadOnly();
      ......
   }

위의 코드를 보면 listener의 switchReadOnlyConnection 를 호출하는 것을 보실 수 있습니다.

switchReadOnlyConnection 은 Connection의 종류에 따라서 달라지는데, MasterFailoverListener 이냐, MasterReplicasListener.java 냐에 따라서 다르게 구현되어 있습니다.

MasterReplicasListener가 우리가 원하는 read-only(secondary)로 바꾸어 주는 부분이므로 src/main/java/org/mariadb/jdbc/internal/failover/impl/MastersReplicasListener.java 를 확인해봅니다.

  /**
   * Switch to a read-only(secondary) or read and write connection(master).
   *
   * @param mustBeReadOnly the read-only status asked
   * @throws SQLException if operation hasn't change protocol
   */
  @Override
  public void switchReadOnlyConnection(Boolean mustBeReadOnly) throws SQLException {
    checkWaitingConnection();
    if (currentReadOnlyAsked != mustBeReadOnly) {
      proxy.lock.lock();
      try {
        // another thread updated state
        if (currentReadOnlyAsked == mustBeReadOnly) {
          return;
        }
        currentReadOnlyAsked = mustBeReadOnly;
        if (currentReadOnlyAsked) {
          if (currentProtocol == null) {
            // switching to secondary connection
            currentProtocol = this.secondaryProtocol;
          } else if (currentProtocol.isMasterConnection()) {
            // must change to replica connection
            if (!isSecondaryHostFail()) {
              try {
                // switching to secondary connection
                syncConnection(this.masterProtocol, this.secondaryProtocol);
                currentProtocol = this.secondaryProtocol;
                // current connection is now secondary
                return;
              } catch (SQLException e) {
                // switching to secondary connection failed
                if (setSecondaryHostFail()) {
                  addToBlacklist(secondaryProtocol.getHostAddress());
                }
              }
            }
            // stay on master connection, since replica connection is fail
            FailoverLoop.addListener(this);
          }
        } else {
          if (currentProtocol == null) {
            // switching to master connection
            currentProtocol = this.masterProtocol;
          } else if (!currentProtocol.isMasterConnection()) {
            // must change to master connection
            if (!isMasterHostFail()) {
              try {
                // switching to master connection
                syncConnection(this.secondaryProtocol, this.masterProtocol);
                currentProtocol = this.masterProtocol;
                // current connection is now master
                return;
              } catch (SQLException e) {
                // switching to master connection failed
                if (setMasterHostFail()) {
                  addToBlacklist(masterProtocol.getHostAddress());
                }
              }
            } else if (urlParser.getOptions().allowMasterDownConnection) {
              currentProtocol = null;
              return;
            }

            try {
              reconnectFailedConnection(new SearchFilter(true, false));
              handleFailLoop();

            } catch (SQLException e) {
              // stop failover, since we will throw a connection exception that will close the
              // connection.
              FailoverLoop.removeListener(this);
              HostAddress failHost =
                  (this.masterProtocol != null) ? this.masterProtocol.getHostAddress() : null;
              throwFailoverMessage(
                  failHost, true, new SQLException("master connection failed"), false);
            }

            if (!isMasterHostFail()) {
              // connection established, no need to send Exception !
              // switching to master connection
              try {
                syncConnection(this.secondaryProtocol, this.masterProtocol);
                currentProtocol = this.masterProtocol;
              } catch (SQLException e) {
                // switching to master connection failed
                if (setMasterHostFail()) {
                  addToBlacklist(masterProtocol.getHostAddress());
                }
              }
            } else {
              currentReadOnlyAsked = !mustBeReadOnly;
              HostAddress failHost =
                  (this.masterProtocol != null) ? this.masterProtocol.getHostAddress() : null;
              throwFailoverMessage(
                  failHost, true, new SQLException("master connection failed"), false);
            }
          }
        }
      } finally {
        proxy.lock.unlock();
      }
    }
  }


Viewing all articles
Browse latest Browse all 122

Trending Articles