KUDU
Impala와 Kudu의 조합
Impala는 분산 병렬 계산 엔진이다. Google의 "Dremel: Interactive Analysis of Web-Scale Datasets" 논문에서 아이디어를 얻어 Cloudera가 자체적으로 C++ 언어로 구현했다.
Kudu를 사용하기 위해 질의 처리 엔진으로 Impala를 선택했었다. 하지만 Impala와 Kudu의 조합은 OLTP 성격의 서비스에 알맞지 않다는 것을 알게 됐다.
자체 질의 처리 엔진을 개발하게 된 Impala의 문제를 간략하게 살펴보겠다.
LLVM(이전 이름: Low Level Virtual Machine)은 컴파일러의 기반구조이다. 프로그램을 컴파일 타임, 링크 타임, 런타임 상황에서 프로그램의 작성 언어에 상관없이 최적화를 쉽게 구현할 수 있도록 구성되어 있다
LLVM은 원래는 저급 가상 기계(low-level virtual machine)의 약자를 가리켰지만, LLVM이 성장하고 다양한 목적을 가지게 되면서 현재는 그 이름을 약자로서 사용하는 것이 아니라 그냥 프로젝트의 이름으로서 사용하고 있다.[2]
Impala와 Kudu 조합의 문제점
1. 질의 처리 시 기본 응답시간
Impala에서는 질의를 처리할 때 최소 140밀리초 이상의 응답시간이 기본으로 소요됐다. 기본 응답시간 이후부터는 몇 건의 레코드를 조회하든 항상 거의 일정한 시간이 소요됐다.
다음은 SELECT COUNT(*) FROM access_log WHERE url = 'url1' 질의를 Kudu API를 사용해서 처리했을 때와 Impala를 사용해서 처리했을 때의 응답시간을 비교한 결과다.
Impala에서 질의를 처리할 때는 기본으로 약 140밀리초 이상 소요됐다. Kudu의 테이블에 저장된 데이터가 많을 때뿐만 아니라 비어 있는 테이블을 대상으로 질의를 실행해도 항상 소요되는 시간이다.
그래서 초당 처리량이 7.14qps 이상을 넘을 수 없었다. 또한 IN() 함수 안에 원소의 개수가 많아지면 응답시간이 10초 이상 소요됐다.
위의 실험 결과는 Kudu API를 단일 스레드(single thread)로 실행한 결과다. Kudu API를 병렬 스레드로 실행하면 응답시간을 Impala 수준으로 낮출 수 있다.
2. IN() 함수 처리
Impala에서는 IN() 함수 안의 원소 개수가 늘어나면 질의 처리 시간이 기하급수적으로 늘어났다. 다음은 IN() 함수 안의 원소 개수를 바꾸며 테스트한 결과다.
여러 번의 실험 끝에 IN() 함수 안의 원소 개수에 대해 병렬성이 보장되지 않는다는 결론을 내렸다.
콘텐츠 통계 서비스의 질의 패턴에서는 IN() 함수 안에 원소가 최대 100개까지 들어올 수도 있는데, 이런 때 Impala에서는 1초 이내(sub-second)로 응답을 받을 수 있다고 보장할 수 없었다.
Kudu와 Impala 조합의 튜닝 시도
이미 성숙한 질의 처리 엔진인 Impala를 사용하기 위해 두 가지 문제를 해결하고자 여러 가지 튜닝을 시도했다.
IN() 함수 처리 시에 병렬성을 높일 수 있는 방법을 찾아보기도 했고, Kudu의 옵션을 조정해 Impala에서 빠르게 질의를 처리할 수 있도록 시도해 봤다.
Impala 클라이언트 라이브러리를 Node.js로 새로 개발해 비동기 요청을 사용해 보기도 했으나 모든 시도에서 응답 속도를 개선할 수 없었다.
Impala 힌트를 사용하기도 했으나 끝내 IN() 함수에 대해서 병렬성을 높이는데 실패했다.
Kudu와 Impala 조합의 성능을 높이기 위해 시도한 과정을 정리하면 다음과 같다.
--cfile_default_block_size 플래그의 설정을 수정해 Kudu 블록 크기를 256KB로 줄였다. Kudu에서는 데이터를 블록으로 저장을 하는데 랜덤 액세스 가 많을 때 블록의 크기가 크면 블록 I/O 비용이 증가한다. Kudu의 블록 크기를 줄였더니 성능이 소폭 상승했다. 블록 크기 설정과 성능에 관한 더 자세한 내용은 "Allow setting cfile block size on a per-column basis를 참고한다.
Impala의 COLUMN_STATS_ACCURATE 옵션 설정을 적용했다. 성능이 향상되는 효과는 없었다.
Impala의 MT_DOP 옵션을 설정해 병렬화 수준을 높였다. 하지만 처리 속도가 불규칙하게 빨라지거나 느려지는 현상이 나타났으며, 성능에는 영향이 없었다.
Node.js로 자체 클라이언트를 개발했다. Impala가 아니라 클라이언트에 문제가 있는 것이라 생각하고 클라이언트를 개선해 봤다. 당시에 Node.js로 시스템을 개발하고 있었고, Impala의 클라이언트로는 npm의 beeswax를 사용했다. 공식 Python 클라이언트는 hs2 포트로 질의를 전송했다. Python 클라이언트를 Node.js에서 실행할 수 있게 JavaScript로 포팅했다. 성능이 꽤 많이 향상됐지만 IN() 함수에 대해서는 별다른 영향을 끼치지 못했다. 원천적으로 Impala 내부에서 IN() 함수를 효율적으로 처리하지 못하는 것이 문제이기 때문에 Impala 앞에 위치하는 클라이언트 프로그램을 개선해서 해결할 수 없는 문제였다.
incremental statistics도 적용해 봤으나 성능이 향상되지는 않았다.
IN() 함수 외에 RANK() 함수로 윈도 개념을 적용한 집계도 시도했으나 여전히 원소의 개수가 늘어나면 응답시간이 10초를 넘어갔다.
그 외 Kudu 스키마 튜닝, Impala 힌트 사용 등의 튜닝을 시도했다.
아무리 튜닝해도 IN() 함수 안에 원소의 개수가 많으면 분당 몇 개 질의 밖에 처리하지 못했다.
데이터의 버전 부여
빅데이터에서 가장 번거로운 작업이 ETL(extractions, transformations and loads) 작업이다.
수많은 시스템이 연동돼 있으면 외부 시스템에서 데이터가 전달되는 과정에서 데이터가 잘못될 수 있다.
INSERT가 완료돼 서비스 중인 상황에서 다양한 이유로 기존 데이터를 새로 INSERT해야 할 상황도 종종 발생하다.
빅데이터의 특성상 INSERT 시간이 많이 걸리고 RDBMS의 트랜잭션 같은 기능도 없다.
Elasticsearch는 인덱스의 별칭(alias) 기능을 사용해 ETL 작업에서 발생하는 문제를 회피할 수 있으나 Kudu에는 문제를 회피할 수 있는 기능이 없다.
Kudu에서는 데이터의 업데이트는 가능하지만 기존에 INSERT된 데이터가 신규 INSERT에서 제외된 경우 데이터를 삭제할 수 없다.
또한 재INSERT 도중에는 과거 버전과 신규 버전의 데이터가 공존하는 상태로 서비스될 수 있다.
이런 불완전한 데이터의 제공을 방지하기 위해 테이블에는 version 필드를 만들어 두고 각 INSERT마다 버전 값을 발급했다.
버전별 상태는 다음과 같은 별도의 테이블인 version 테이블에서 date(날짜), version(버전), state(상태) 칼럼으로 관리한다.
Kudu에 부족한 점, 바라는 점
1. Secondary Index 미지원
Kudu는 primary key를 제공하기 때문에 이를 기준으로 데이터를 매우 빠르게 조회할 수 있다. primary key로만 데이터를 조회하는 곳에서는 Kudu가 최고의 선택이 될 수 있다. 하지만 콘텐츠 통계 서비스에서는 secondary index가 필요했다.
Kudu는 secondary index를 제공하지 않는다(Frequently Asked Questions의 'Does Kudu support secondary indexes?' 질문 참고).
Kudu의 스캔 성능이 뛰어나기 때문에 작은 클러스터에서도 10억 건 정도는 1초 내외에 스캔할 수 있다. 하지만 10억 건을 1초 내외(sub-second)로 스캔하려면 많은 자원을 사용해야 하기 때문에 처리량(throughput)이 떨어진다. 응답 속도와 처리량은 반비례 관계인데 Kudu에서 전체 스캔의 응답 속도를 빠르게 하기 위해서는 처리량을 희생하는 수밖에 없다.
secondary index와 비슷한 효과를 내기 위해 secondary index용 테이블을 만든 후 원본 테이블의 primary key를 참조하도록 하고 Impala에서 원본 테이블과 secondary index용 테이블에 대해 항상 JOIN을 실행하게 해 봤지만 성능이 잘 나오지 않았다. 데이터베이스 설계, 즉 primary key와 파티션 설계를 잘 해서 전체 스캔을 실행하는 양을 줄일 수 있지만 모든 질의 패턴에 대해 최적화된 설계를 하기는 결코 쉽지 않다.
'건초 더미에서 바늘 찾기'와 같은 질의 패턴을 위해서는 Kudu에 secondary index가 도입돼야 한다.
2. 질의 처리 엔진의 부재
Kudu는 칼럼 기반 데이터베이스며, 데이터 저장소 역할만 수행한다. 데이터를 저장하기 위한 연산(INSERT, DELETE, UPDATE)을 API로 제공한다. 데이터 조회용 API로는 전체 데이터 혹은 primary key 기준의 데이터를 읽는 API만 제공한다. 따라서 Kudu 자체는 SQL로 따진다면 단순한 SELECT 구문 기능만 제공할 뿐이다. 복잡한 질의를 처리하려면 Spark와 Impala 같은 질의 처리 엔진과 연동해야 한다.
Spark와 Impala도 워낙 좋은 질의 처리 엔진이고 Kudu도 좋은 데이터 저장소이기 때문에 이 둘의 조합으로 분석 시스템을 만들면 기본적으로 성능이 좋게 나온다. 하지만 Kudu 자체적으로 질의 처리 엔진을 포함하면 데이터 저장소와 질의 처리 엔진 간에 오가는 데이터를 줄여 더 좋은 성능이 나올 수 있을 것이라 생각한다.
3. Approximate Top-N 질의 부재 -> impala에서 존재하는데... 확인필요
Elasticsearch가 빠른 이유는 여러 가지가 있겠지만 가장 큰 이유는 근사치(approximation)의 질의 결과를 반환하기 때문이라고 생각한다. Elasticsearch에서 Aggregation 질의는 각 샤드의 상위 결과만 반환하기 때문에 대량의 셔플(shuffle)이 발생하지 않는다. 이 때문에 약간의 수치에 오차가 있을 수 있지만 응답 속도 및 처리량이 획기적으로 개선된다.
네이버와 같이 많은 사용자가 동시에 접속하더라도 1초 이내의 응답 속도로 서비스를 제공하려면 Approximate Top-N 질의가 거의 필수적이다.
4. Aggregate Pushdown 부재
Kudu에는 질의 처리 기능이 없기 때문에 자연스럽게 Aggregate Pushdown 기능도 존재하지 않는다.
GROUP BY 같은 질의를 실행할 때 데이터 저장소에서 질의 처리 엔진으로 모든 데이터가 전달되지 않고 부분 취합(partial aggregation) 결과만 엔진으로 전달된다면 응답 속도가 빠를 것이다. 이러한 플랫폼으로 Druid가 있다. Druid의 historical node에서 Broker로 결과를 전달할 때는 개별 historical node의 Aggregation 결과가 전달된다.
이러한 부분 취합(partial aggregation) 과정에서는 원본 값이 아닌 인코딩된 값을 이용해 취합할 수 있으므로 질의 성능이 빨라질 수 있다.
차트생성
select total_kudu_on_disk_size_across_kudu_replicas where category=KUDU_TABLE
CM -> 차트 -> 차트생성기 -> 구문 입력
[select total_kudu_on_disk_size_across_kudu_replicas where category=KUDU_TABLE] , 30d 로 설정
-> 차트생성 -> 저장
차트저장팝업
차트제목 : KUDU 테이블 스토리지 사용량
대시보드 이름 : 스토리지현황
차트 생성 후 다운로드
차트의 우상단 설정바퀴버튼 클릭 -> CSV내보내기 클릭