[입 개발] Hive MetaStore 에서 Location은 어떻게 관리될까?

최근에 아주 이상한 에러를 경험했습니다. 다음과 같은 managed table 이 있다고 가정합니다.

CREATE TABLE `test1`(
  `id` bigint
PARTITIONED BY (
  `datestamp` date)
ROW FORMAT SERDE
  'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'
STORED AS INPUTFORMAT
  'org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat'
LOCATION
  'hdfs://a.b.c:8020/user/hive/warehouse/charsyam.db/test1'
TBLPROPERTIES (
  'transient_lastDdlTime'='1556186715')

test1 이라는 table에 drop table 을 시도했는데!!! 다음과 같은 에러가 발생했습니다.

FAILED: Execution Error, return code 1 from org.apache.hadoop.hive.ql.exec.DDLTask. MetaException(message:java.lang.IllegalArgumentException: Wrong FS: hdfs://a.b.c:8020/user/hive/warehouse/charsyam.db/test1, expected: hdfs://ip-b.b.c:8020)

사실 이 에러는 hadoop 에서 checkPath 라는 함수에 의해서 발생하게 됩니다.(https://github.com/apache/hadoop/blob/trunk/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileSystem.java#L711) 코드를 보면 실제 schema에 있는 Location 경로와 실제 hadoop 에서 인식하는 자신의 도메인이 맞지 않을 때 발생하는 에러입니다. 즉 a.b.c 도 ip 가 1.1.1.1 이고 b.b.c 도 ip 가 1.1.1.1 이지만 table location 이 hdfs://a.b.c:8020 인것과 hdfs://b.b.c:8020 은 서로 다른 도메인이 되는 거죠.

  protected void checkPath(Path path) {
    URI uri = path.toUri();
    String thatScheme = uri.getScheme();
    if (thatScheme == null)                // fs is relative
      return;
    URI thisUri = getCanonicalUri();
    String thisScheme = thisUri.getScheme();
    //authority and scheme are not case sensitive
    if (thisScheme.equalsIgnoreCase(thatScheme)) {// schemes match
      String thisAuthority = thisUri.getAuthority();
      String thatAuthority = uri.getAuthority();
      if (thatAuthority == null &&                // path's authority is null
          thisAuthority != null) {                // fs has an authority
        URI defaultUri = getDefaultUri(getConf());
        if (thisScheme.equalsIgnoreCase(defaultUri.getScheme())) {
          uri = defaultUri; // schemes match, so use this uri instead
        } else {
          uri = null; // can't determine auth of the path
        }
      }
      if (uri != null) {
        // canonicalize uri before comparing with this fs
        uri = canonicalizeUri(uri);
        thatAuthority = uri.getAuthority();
        if (thisAuthority == thatAuthority ||       // authorities match
            (thisAuthority != null &&
             thisAuthority.equalsIgnoreCase(thatAuthority)))
          return;
      }
    }
    throw new IllegalArgumentException("Wrong FS: " + path +
                                       ", expected: " + this.getUri());
  }

그런데 이런 일이 왜 일어날까가 사실 더 궁금할껍니다. 보통은 발생하면 안되는 일이죠. 사실 이런 현상은 Hadoop 노드를 계속 새로운 장비에 재 구축하는데, Hive Metastore 는 동일하게 저장하기 때문에 발생하는 현상입니다. 보통 하둡 장비들이 크게 바뀔일이 없으니… 거의 발생할 일도 없는거죠. 그런데 이런 일이 발생하게 되면, 다음과 같은 에러를 맞게 됩니다. 그래서 이걸 파다보니 실제로 Hive MetaStore 에서 Location 을 어떻게 저장하는가가 궁금해졌습니다. 보통 우리는 show create table 등으로 볼 수 있습니다. 그럼 이 정보들은 어디에 저장될까요?

일단 Hive Metastore 는 실제 저장은 다른 곳 DBMS나 glue 등에 저장을 할 수 있습니다. 여기서는 MYSQL에 저장된 것으로 설명을 하겠습니다. 먼저 mysql 에 hive_metastore 라는 DB가 생성이 됩니다. 그리고 여기서 우리가 살펴볼 table 은 DBS, TBLS, SDS 입니다.(이거 세 개만 보면 됩니다.) 마지막 S는 전부 복수의 S로 보이고 각각 Database, Table, StorageDescriptor 로 보입니다.

먼저 hive 등에서 create database 로 DB를 생성할 때 마다 해당 정보가 Database 에 저장이 됩니다. 먼저 DBS 테이블의 스키마는 다음과 같습니다.

CREATE TABLE `DBS` (
  `DB_ID` bigint(20) NOT NULL,
  `DESC` varchar(4000) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `DB_LOCATION_URI` varchar(4000) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
  `NAME` varchar(128) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `OWNER_NAME` varchar(128) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `OWNER_TYPE` varchar(10) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  PRIMARY KEY (`DB_ID`),
  UNIQUE KEY `UNIQUE_DATABASE` (`NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

여기서 주목할 것은 역시 primary key 인 DB_ID 와 DB_LOCATION_URI 입니다. 만약 charsyam db라면, 이 DB_LOCATION_URI 에 들어가는 값이 hdfs://a.b.c/user/hive/warehouse/charsyam.db/ 까지가 들어가게 됩니다. 그리고 이 db에 테이블이 생성되면 table의 location이 항상 이 밑에 생기게 됩니다.

실제로 TBLS 테이블을 보면 내부에 Location이 없습니다. 그래서 해당 DBS의 DB_LOCATION_URI를 바꾸면 바로 되지 않을까라고 생각을 처음에 했었는데… 사실 이건 말도 안되는 소리입니다. 왜냐하면 hive 등의 툴을 써보신 분들은 바로 아시겠지만, location 자체는 얼마든지 다른 것으로 바꿀 수 있습니다. 그렇다면 무엇인가? 테이블에는 자기만의 Location 정보가 꼭 있어야 한다는 것입니다. 즉 TBLS에 없다면, 다른곳에 있어야 합니다. 다음은 TBLS 의 schema 입니다.

CREATE TABLE `TBLS` (
  `TBL_ID` bigint(20) NOT NULL,
  `CREATE_TIME` int(11) NOT NULL,
  `DB_ID` bigint(20) DEFAULT NULL,
  `LAST_ACCESS_TIME` int(11) NOT NULL,
  `OWNER` varchar(767) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `RETENTION` int(11) NOT NULL,
  `SD_ID` bigint(20) DEFAULT NULL,
  `TBL_NAME` varchar(256) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `TBL_TYPE` varchar(128) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `VIEW_EXPANDED_TEXT` mediumtext,
  `VIEW_ORIGINAL_TEXT` mediumtext,
  `IS_REWRITE_ENABLED` bit(1) NOT NULL,
  PRIMARY KEY (`TBL_ID`),
  UNIQUE KEY `UNIQUETABLE` (`TBL_NAME`,`DB_ID`),
  KEY `TBLS_N50` (`SD_ID`),
  KEY `TBLS_N49` (`DB_ID`),
  CONSTRAINT `TBLS_FK1` FOREIGN KEY (`SD_ID`) REFERENCES `SDS` (`SD_ID`),
  CONSTRAINT `TBLS_FK2` FOREIGN KEY (`DB_ID`) REFERENCES `DBS` (`DB_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

그렇다면 해당 정보는 어디에 있을까요? 당연히!!! 아까 언급했지만, 아직까지 안나온 SDS 입니다. 어떻게 SD 가 StorageDescriptor라고 생각하냐고 하면… Hive 소스를 보면 ql/src/java/org/apache/hadoop/hive/ql/metadata/Table.java 에 getPath 라는 메서드가 있고 여기서 getSd() 라는 메서드를 호출합니다.

final public Path getPath() {
    String location = tTable.getSd().getLocation();
    if (location == null) {
      return null;
    }
    return new Path(location);
  }

이걸 자세히 보면 다음과 같이 StorageDescriptor 라는 클래스를 리턴합니다. 아마도 그러니 저것도…

  public StorageDescriptor getSd() {
    return this.sd;
  }

이제 SDS 테이블을 살펴보시죠. Location 항목이 보입니다. 여기에 저장이 되는겁니다.

CREATE TABLE `SDS` (
  `SD_ID` bigint(20) NOT NULL,
  `CD_ID` bigint(20) DEFAULT NULL,
  `INPUT_FORMAT` varchar(4000) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `IS_COMPRESSED` bit(1) NOT NULL,
  `IS_STOREDASSUBDIRECTORIES` bit(1) NOT NULL,
  `LOCATION` varchar(4000) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `NUM_BUCKETS` int(11) NOT NULL,
  `OUTPUT_FORMAT` varchar(4000) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `SERDE_ID` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`SD_ID`),
  KEY `SDS_N49` (`SERDE_ID`),
  KEY `SDS_N50` (`CD_ID`),
  CONSTRAINT `SDS_FK1` FOREIGN KEY (`SERDE_ID`) REFERENCES `SERDES` (`SERDE_ID`),
  CONSTRAINT `SDS_FK2` FOREIGN KEY (`CD_ID`) REFERENCES `CDS` (`CD_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

그런데 이거랑 실제 Table과 어떻게 매칭을 시킬 수 있을까요? 그 비밀은 Primary Key인 SD_ID가 중요합니다. 위에서 언급한 TBLS 테이블을 보면 DB_ID, SD_ID, TBL_NAME 이 있습니다. 즉, 먼저 DBS 에서 database 이름으로 DB_ID 값을 찾고, 그 DB_ID와 TBL_NAME을 이용해서 해당 테이블의 SD_ID를 찾을 수 있습니다. 그리고 그 SD_ID를 이용하면 마지막으로 SDS 테이블에서 최종적으로 해당 테이블의 StorageDescriptor 정보를 볼 수 있는 겁니다. 그리고 SDS 의 Location 값이 show create table 에서 볼 수 있는 바로 그값입니다. 그래서 여기서 해당 컬럼의 값을 바꿔버리면… show create table 시에도 값이 바뀌게 됩니다.

당연하지만, SDS 테이블에 데이터가 많은 것은 StorageDescriptor는 테이블마다 하나씩이지만, 당연히 Partition 마다도 존재합니다. 그렇기 때문에 최소한 전체 테이블 수 + 전체 파티션 수 만큼 존재하는게 맞습니다. 🙂