2014년 12월 24일 수요일

MySQL 서버에서 UUID 활용


이 글은 
http://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/ 블로그의 내용에 다른 내용들을 더 보완해서 구성된 내용입니다. UUID 재정렬에 의한 성능 향상은 원문 블로그에서 더 자세히 확인할 수 있습니다.


UUID는 16 옥텟(128비트)의 숫자이며, 때로는 32 글자의 소문자 16진수 문자열로 표현되기도 하는데 이때에는 "-"로 5개의 영역으로 분리되어서 표시된다. 즉 8-4-4-4-12 형태로 전체 32 알파뉴메릭으로 표시된다.

예제 : 123e4567-e89b-12d3-a456-426655440000

Variant와 Version
Variant는 UUID 정렬의 변형 형태를 의미하는데, 현재 UUID 스펙상에서는 대표적인 하나의 Variant만 지원하고 있으며, 나머지는 모두 이전 버전과의 호환성을 위해서 사용된다.
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx 형태에서 최상위 비트(MSb)는 Variant를 의미(물론 Variant에 따라서 1~3비트가 사용되기도 함)한다. UUID 스펙에 지정된 Variant는 "N"의 최상위 2비트가 "10"으로 지정된다. 즉 Variant를 의미하는 "N"에는 8이나 9 또는 'A'나 'B'만 가능한 것이다. UUID 스펙에서 지원하는 Variant는 다섯 개의 버전을 지원하는데, 이 Variant에 대해서는 "M"의 4비트는 UUID의 버전을 의미한다. UUID의 버전은 1~5까지 가능하다.

버전-1 (MAC address & date-time)
  네트워크 랜 카드와 시간을 기반으로 유니크한 ID를 생성한다. 이 아이디는 형태를 기반으로 예측이 가능하며, 이 값을 이용해서 네트워크 카드를 트레이스할 수도 있다. 
  Version 1 UUID는 48 비트의 네트워크 카드의 MAC 주소와 60비트의 시간 정보로 구성된다. 시간 정보는 100 Nano second 단위(Resolution)로 관리되는데, 대략 3655년 정도의 데이터가 저장된다.
  하지만 시간 정보는 1582년 10월 15일부터 시작된 값이므로, 다시 초기화되기까지는 상당히 많은 시간이 남아있다. 또한 UUID Version 1은 100 Nanosecond 단위이므로, 한 서버에서 최대 초당 1000000(1000000000/100)개의 유니크한 UUID를 만들어낼 수 있다.

버전-2 (DCE Security)
  Version1과 비슷하지만, 시간을 위한 비트수가 더 적어서 Version 1보다는 더 짧은 기간내에 시간 부분이 반복(Wrap)될 가능성이 높다.
  이는 DCE (Distributed Computing Evn)를 위해서 고안된 버전이므로, 그다지...

버전-3 (MD5 hash & namespace)
  Namespace와 Name의 MD5 해시 값을 이용해서 유니크한 ID를 생성한다. 만약 특정 Name을 이용해서 다른 시스템에서 생성된 UUID와의 호환성을 유지하고자 한다면 Version 3 UUID를 사용하는 것이 좋다.
  이 방식은 MD5로 해시 값을 생성해서, UUID 포맷으로 변환하는 것과 거의 흡사하게 작동한다고 생각해도 무방하다.
  Name과 Namespace에 대한 자세한 내용은 "http://stackoverflow.com/questions/10867405/generating-v5-uuid-what-is-name-and-namespace" 참조.

버전-4 (random)
  랜덤한 숫자 값을 이용해서 UUID를 생성한다. 그냥 단순히 UUID를 생성하고자 한다면 Version 4 UUID를 사용하는 것이 좋다.

버전-5 (SHA-1 hash & namespace)
  Namespace와 Name의 SHA-1 해시 값을 이용해서 유니크한 ID를 생성한다. 
  이 방식은 SHA-1 알고리즘으로 해시 값을 생성해서, UUID 포맷으로 변환하는 것과 거의 흡사하게 작동한다고 생각해도 무방하다.
  MD5는 이미 충돌이 많이 발생하고 보안성이 떨어지는(Broken) 암호화 해시 알고리즘이므로 MD5보다는 SHA-1을 사용하는 Version 5 UUID를 사용할 것을 권장한다.

만약 유니크한 UUID를 생성하고자 한다면, Version 1과 4를 사용하는 것이 좋고,
주어진 Name을 이용하면 타 시스템에서도 동일한 UUID를 생성해야 한다면 Version 3와 5를 이용하는 것이 좋다.

하지만 UUID Version 1은 시간을 기반으로 하고 있기 때문에, 매우 빠른 처리를 수행하는 시스템(Multi-process && Multi-thread)에서는 중복된 값이 생성될 수도 있다. 
이런 단점을 보완하기 위해서 Time-based UUID(http://johannburkard.de/software/uuid/)가 도입되었는데, 이는 실제 100 Nano-second 단위의 타임 스탬프를 가져오는 것이 아니라 Milli-second 수준의 타임스탬프만 가져오고
나머지는 UUID Class에서 AutoIncrement와 Seudo-Random number를 이용해서 중복 가능성을 낮춘 형태이다. Time-based UUID는 Cassandra에서 식별자 용도로 자주 사용된다.

MySQL 서버에서도 UUID() 함수를 제공하는데, 이는 UUID Version1을 지원하는 함수이다. 때로는 이런 UUID 함수나 Application에서 제공하는 UUID 기능을 이용해서 MySQL 서버의 PK 또는 Unique Key로 사용하는 경우가 많이 있는데,
UUID 값의 특징은 생성되는 값이 전혀 단순 증가나 단순 감소 형태가 아니라 매우 랜덤하게 생성된다는 것이다. 이는 UUID Version 1이 시간을 기반으로 만들어지기는 하지만, 실제 Timestamp 값이 3개 파트로 나뉘어져서 재구성되기 때문이다.

예를 들어서 60bits 타임 스탬프 값이 "1d8eebc58e0a7d7"라고 가정해보자. 그러면 이 값이 part1(1d8)과 part2(eebc) 그리고 part3(58e0a7d7)으로 나뉘어지고, 이 값들이 순서 관계없이 아래와 같이 Version 1의 UUID의 각 블록에 설정된다.

[Timestamp(part3)]-[Timestamp(part2)]-1[Timestamp(part1)]-[NetworkCardMAC]

그래서 최종 생성된 UUID 값은 "58e0a7d7-eebc-11d8-9669-0800200c9a66"가 되는 것이다. 이렇게 랜덤하게 생성되는 값은 MySQL 서버의 InnoDB 테이블에서 PK나 Unique Key로 사용되기에는 (성능상) 아주 부적절한 값이다. 
PK나 Unique Key로 사용되기에 아주 좋은 값은 단조 증가나 감소하는 패턴인 것이 가장 좋은데, (이미 눈치챘겠지만) UUID 값을 그대로 사용하는 것이 아니라 재 조합을 해서 원래 Timestamp를 제일 앞 부분으로 꺼내어서 만들어주면 단순 증가하는 형태의 값을 만들어낼 수 있다.
단순히 이렇게 정렬된 UUID를 생성하는 MySQL 함수를 아래와 같이 생성할 수 있다. 또한 저장 공간을 줄이기 위해서 VARCHAR(32)보다는 BINARY(16)이나 VARBINARY(16)으로 컬럼을 생성해주는 것이 좋다.

DELIMITER ;;
CREATE 
  DEFINER=`user`@`host`
  SECURITY=INVOKER
FUNCTION ordered_uuid(uuid BINARY(36)) RETURNS BINARY(16) DETERMINISTIC
  RETURN UNHEX(CONCAT(SUBSTR(uuid, 15, 4),SUBSTR(uuid, 10, 4),SUBSTR(uuid, 1, 8),SUBSTR(uuid, 20, 4),SUBSTR(uuid, 25)));
;;
DELIMITER ;

물론 Statement 기반의 복제(SBR)을 사용하는 경우라면 MySQL 서버의 UUID() 함수 사용은 복제를 불가능하게 만들수도 있으므로 주의하도록 하자. 복제가 수행되는 MySQL 서버에서 UUID를 사용해야 한다면, 응용 프로그램에서 생성된 값을 MySQL 서버로 INSERT 또는 UPDATE 하는 형태로 사용하도록 해야 한다.

그리고 때로는 Unique ID를 생성하기 위해서 UUID를 사용하지 않고 개발자가 직접 커스텀하게 개발하는 경우도 있는데, 이런 경우에도 Timestamp를 꼭 ID의 앞쪽에 위치하도록 만들어준다면 똑같이 InnoDB 테이블에서 PK나 Unique Key로 사용하기에 적절한 값을 만들어낼 수 있다.