빅쿼리 Query Plan을 이용한 쿼리 실행 분석

최유석

개요

일반적으로 대부분의 데이터베이스의 SQL (MS-SQL, MySQL, Oracle, 등)에서 제공하는 Query Explain Plan(쿼리 실행 계획)기능은 사용자가 쿼리를 실행하기전에 쿼리를 분석할 수 있도록 쿼리가 실행되는 각각의 과정에 대해서 예상정보를 제공한다. 이 정보를 통해 해당 되는 쿼리에 대해서 성능에 대한 향상, 실행 오류 방지 등 쿼리를 실행하기 이전에 최적화 할 수 있게 도와준다. 빅쿼리에서도 이와 유사한 기능으로 Query Plan(쿼리 계획)을 제공한다. 이글에서는 빅쿼리의 Query Plan을 통해 쿼리 실행을 분석하는 방법에 대해서 알아보고자 한다.

 

빅쿼리와 다른 SQL엔진의 차이점

앞에서 언급한 것처럼 다른 SQL엔진은 쿼리를 실행하기 전에 해당되는 쿼리에 대한 예상 실행정보를 각각의 SQL에서 제공하는 Explain(각각 제공하는 이름은 다르지만 일반적인 의미)문을 적용해서 쿼리를 실행하고 분석해서 쿼리 튜닝을 하게 된다. 하지만 빅쿼리는 다른 SQL엔진과 다른 방식으로 실행이 완료된 쿼리에 대해서 쿼리 실행의 각 단계에 대한 정보를 메타데이터 형태로 제공한다. 또한 일반적인 Explain문이 빅쿼리에 따로 없기 때문에 직접 실행할 수 없으며, 실행이 완료된 쿼리에 대해서 Query Plan정보를 자동적으로 제공해준다.

 

빅쿼리의 부담 없는 쿼리 실행

빅쿼리는 구글 클라우드 플랫폼의 관리형 서비스로 구글에서 전체적인 시스템에 대한 관리를 하기 때문에 사용자 입장에서는 빅쿼리에 데이터를 저장하고 분석하는 등, 빅쿼리가 제공하는 서비스를 사용하기만 하면 된다. 따라서 일반적인 데이터베이스처럼 잘못된 쿼리로 인한 오류로 인해 데이터베이스에 문제를 일으키는 경우에 대해서 걱정 할 필요가 없다. 단지 쿼리가 실패할 뿐이며, 바로 다시 쿼리를 수정해서 실행할 수 있다. 또한 쿼리를 실행하기전에 쿼리에 대한 Validation (유효성 검사)기능을 제공하기 때문에 잘못된 쿼리의 경우 대부분 여기에서 걸러진다. 추가적으로Validation 을 통해 쿼리오류를 확인하지 못했다고 하더라도 쿼리가 잘못되거나 기타 다른 문제로 쿼리가 실패해서 오류가 발생되는 경우, 실패한 쿼리에 대해서는 비용이 부과되지 않기 때문에 부담이 적다.

 

빅쿼리에서 쿼리 실행 분석에 대한 접근

빅쿼리의 아키텍처를 보면 일반적인 데이터베이스에서 제공하는 Key, Index가 없으며, 기본적으로 쿼리요청의 대상이 되는 각각의 열에 대해서 Full Scan이다. 따라서 일반적으로 고려하는 Key, Index 기반의 쿼리 튜닝으로 방향이 아닌, 빅쿼리의 Query Plan에서 제공하는 메타데이터를 토대로 쿼리 실행단계에서 소요된 작업대기, 읽기, 쓰기, CPU연산 등에 대한 상대적인 시간 비율을 기준으로 실행 된 쿼리에 대한 분석 및 최적화 방향을 고려해야 한다.

 

*사실 빅쿼리의 쿼리 최적화는 쿼리실행이 열에 대한 풀스캔이고, 거기에 대한 비용이 발생하는 구조기 때문에 쿼리 비용자체를 감소시키기는 어렵다. 주요한 방향으로 생각해야 할 부분은 쿼리 실행 성능의 향상이다.

 

빅쿼리의 Query Plan 인터페이스

빅쿼리 콘솔(Web UI)

빅쿼리 웹 콘솔(Web UI)에서는 쿼리를 실행하고 나서 다음과 같이 Explanation 버튼을 클릭하면 쿼리 실행에 대한 각각의 스테이지로 정보가 간략하게 나타나고 각각의 스테이지에 있는 세모모양의 아이콘을 클릭하면 각각의 스테이지 별로 쿼리 실행의 세부정보가 나타난다.

 

 

API

빅쿼리에서 API형태로 제공하는 Query Plan 정보는 실행이 완료된 쿼리 결과에 대해서 자동으로 해당 쿼리 Job의 리소스로 포함되어 7일간 보관된다. 이 기간 안에 Job.get() 메소드를 호출하게 되면 응답 결과에 포함 된 JSON 배열 형식의 queryPlan 속성에서 상세한 쿼리 실행 정보를 얻을 수 있다.

 

빅쿼리 Query plan 메타데이터

STAGE 메타데이터

API속성 Web UI 설명
Id Stage x 스테이지의 고유한 정수 ID
(해당 Query Plan 범위 한정)
recordsRead Input 스테이지에서 읽은 행의
recordsWritten Output 스테이지가 작성한 행의

 

상대적인 시간비율에 따른 메타데이터

API속성 Web UI 설명
waitRatioAvg 작업 대기에 소요된 평균시간
waitRatioMax 작업 대기에 소요된 최대시간
readRatioAvg 입력데이터의 읽기에 소요된 평균시간
readRatioMax 입력데이터의 읽기에 소요된 최대시간
computeRatioAvg CPU연산에 소요된 평균시간
computeRatioMax CPU연산에 소요된 최대시간
writeRatioAvg 출력데이터의 쓰기에 소요된 평균시간
writeRatioMax 출력데이터의 쓰기에 소요된최대시

*빅쿼리 콘솔에서는 AVG, MAX 따로 표시되지 않는다. 또한, 정확한 수치를 확인하기 위해서는 API 이용한 접근이 필요하다.

 

간단한 쿼리를 이용한 기본적인 메타데이터 정보 확인하기

먼저 다음의 쿼리를 빅쿼리 웹 콘솔에서 실행하고, Query Plan에 대한 메타데이터 정보를 확인해보자.

SELECT COUNT(*)
FROM [publicdata:samples.shakespeare]
WHERE corpus = 'hamlet'

쿼리를 실행하고 Explanation 탭을 클릭하면 다음과 같이 Query Plan에 대한 정보를 확인 할 수 있다.

먼저 좌측의 Stage 뒤의 1, 2가 실행 된 해당 쿼리에 대한 고유한 ID이며,우측 Input, Output 아래의 수치가 해당 쿼리에 대한 각각의 스테이지에서 읽거나 기록한 행의 수이다. 중간의 Stage timing 아래에 나타나는 부분이 해당 쿼리가 실행되는 각각의 스테이지에서 [스케줄에 대한 작업대기, 읽기, CPU연산, 쓰기]에 의해서 소요된 시간 비율들을 나타내며, 위의 쿼리의 경우 실행 데이터의 크기가 작기 때문에 각각의 상대적인 시간비율들의 평균값과 최대값이 같게 나타난다.

 

이번에는 다음과 같이 시간비율의 편차를 보기 위해 쿼리의 대상이 되는 데이터의 크기가 크고 쿼리 연산이 복잡한 경우를 살펴보자.

SELECT language, SUM(views) as views

FROM [bigquery-samples:wikipedia_benchmark.Wiki1B]

WHERE regexp_match(title,"G.*o.*o.*g")

GROUP by language

ORDER by views DESC

위의 결과에서 보듯이 스테이지1에서 데이터의 읽기와 CPU연산에 소요된 시간 비율의 평균과 최대치의 차이가 발생하는 것을 확인할 수 있다.

 

*사용자가 실행하는 쿼리와 대상이 되는 데이터의 크기, 분포 등에 따라서 위와 같이 쿼리 실행 단계의 각각의 상대적인 시간비율들의 평균, 최대 값의 차이가 발생하게 된다.

 

STEP(스테이지의 세부 단계) 메타데이터 정보

Query Plan에서 제공하는 각각의 스테이지 내부에서의 확인 가능한 세부적인 메타데이터 정보는 다음과 같다.

STEP(세부 속성) 설명
READ 입력 테이블 또는 중간 결과에서 하나 이상의 열을 읽는다.
WRITE 출력 테이블 또는 중간 결과에 대해 하나 이상의 열을 기록한다.
COMPUTE 대부분의 식에 대한 모든 계산 및 내장된 함수의 호출한다
FILTER WHERE, OMIT IF, HAVING절을 구현하는 연산자이다.
SORT 정렬 연산을 나타내며 열의 키와 정렬방향을 포함한다.
AGGREGATE 집계연산(예:GROUP BY)을 나타내며, 여러 단계로 분할된다.
LIMIT LIMIT절을 구현하는 연산자이다.
JOIN JOIN연산을 나타내며, JOIN의 종류와 JOIN에 사용되는 열의 정보를 포함한다.
ANALYTIC_FUNCTION 분석 기능(CF. window functions)을 호출한다.
USER_DEFINED_FUNCTION 사용자 정의 함수를 호출한다

 

간단한 쿼리를 이용한 상세한 메타데이터 정보 확인하기

앞서 메타데이터 정보를 확인하기 위해 처음 실행 했던 쿼리를 이용해서 Query Plan의 각각의 세부적인 단계(STEP)에 대한 정보를 빅쿼리 웹 콘솔(Web UI)과 API를 통해서 각각 확인해보자.

 

빅쿼리 콘솔(Web UI) 에서 STEP정보 확인하기

앞서 간단하게 메타데이터 정보를 확인하기 위해 처음 실행 했던 쿼리의 Explanation을 통해서 쿼리 실행의 각각의 단계(STEP)에 대한 정보를 확인해보자. 실행 된 쿼리의 Explanation에서 각각의 스테이지 좌측에 나타나는 삼각형모양의 아이콘을 클릭하면 각각의 스테이지에 대한 세부적인 메타데이터 정보를 확인 할 수 있다.

먼저 스테이지 1, 2에서 READ, AGGREGATE, WRITE스탭을 포함하고 있는 것을 볼 수 있다.

 

스테이지1에서는 READ스탭에서 [publicdata:samples.shakespeare] 테이블의 대상이 되는 "corpus" 열을 읽어서 값이 "hamlet"인 행을 찾는다. 그 다음 AGGREGATE스탭에서 앞서 읽은 행을 카운트하고 WRITE에서 __stage1_output 이라는 식별자에 기록을 한다.

 

스테이지2에서는 스테이지1에서 __stage1_output에 기록한 정보를 받아서 READ스탭에서 스테이지1의 작업의 카운트한 정보를 읽고 AGGREGATE스탭에서 카운트 된 정보에 대한 합산을 하고 WRITE스탭에서 합산한 결과를 출력한다.

 

API 이용해서 STEP정보 확인하기

구글에서 제공하는 APIs Explorer을 사용해서 빅쿼리의 Query Plan정보를 간단하게 API를 호출하고 결과를 확인해보도록 하자.

 

Job ID 확인

앞서 언급한 것처럼, 빅쿼리 Query Plan정보는 실행된 쿼리에 대한 결과만 확인 할 수 있으며, Query Plan정보를 얻기 위해서는 실행된 쿼리에 대한 정보 중 하나인 Job ID가 필요하다. 빅쿼리 웹 콘솔(Web UI)에서 좌측상단 Query History클릭하고 앞서 실행한 쿼리정보에 대한Job ID의 값을 확인한다.(복사하거나 창을 열어둔다.)

*빅쿼리 웹 콘솔의 Query History에서 Job ID 는 Project ID:Job ID 형식으로 되어있으니, job.get() 메소드를 호출할 때는 각각 나눠서 입력해야 한다. 

Google APIs Explorer 페이지 접속

다음으로 APIs Explorer에서 빅쿼리 Job.get()메소드 테스트 페이지인 다음의 주소로 접속한다.

https://developers.google.com/apis-explorer/#p/bigquery/v2/bigquery.jobs.get

 

권한 부여

빅쿼리의 Job.get() 메소드를 사용하기 위해서는 빅쿼리에 대한 읽기 또는 전체 관리권한이 필요하다. 따라서 해당되는 권한을 임시로 부여하기 위해 우측의 off로 되어있는 버튼을 클릭하고 해당되는 권한범위를 체크하고(필요한 범위는 기본값으로 체크되어 있다.) Authorize를 클릭해서 실행 권한을 부여한다.

Job.get()메소드를 사용하기 위한 권한 범위URI는 위의 화면에서 체크된 항목과 같다.

*한 가지만 허용해도 Job.get() 메소드를 사용할 수 있다.

 

Job.get() 메소드 실행하기

API 사용에 대한 권한범위를 허용하고 버튼이 ON되었으면 해당 프로젝트 ID와 위에서 확인한 Job ID를 입력하고 Execute버튼을 클릭해서 실행한다.

 

Job.get() 메소드 실행결과 확인

아래에 실행된 결과를 보면 Job.get()메소드에 대해서 GET으로 성공적으로 요청이 이루어졌으며, 응답된 결과로 해당 Job에 대한 모든 정보를 나타내며(해당 속성만 필드를 지정해서 실행할 수 있다.), 쿼리 실행 정보를 얻기 위한 queryPlan속성은 statistics속성의 하위 속성인 query안에 포함되어 있다.

*속성 관계: statistics > query > queryPlan 

"queryPlan": [

  {

    "name": "Stage 1",

    "id": "1",

    "waitRatioAvg": 1,

    "waitRatioMax": 1,

    "readRatioAvg": 0.8650635652173914,

    "readRatioMax": 0.8650635652173914,

    "computeRatioAvg": 0.47558847826086953,

    "computeRatioMax": 0.47558847826086953,

    "writeRatioAvg": 0.19849230434782608,

    "writeRatioMax": 0.19849230434782608,

    "recordsRead": "164656",

    "recordsWritten": "1",

    "steps": [

      {

        "kind": "READ",

        "substeps": [

          "corpus",

          "FROM publicdata:samples.shakespeare",

          "WHERE EQUAL(corpus, 'hamlet')"

            ]

        },

        {

        "kind": "AGGREGATE",

        "substeps": [

          "COUNT_STAR() AS f0_"

            ]

        },

        {

        "kind": "WRITE",

        "substeps": [

          "f0_",

          "TO __stage1_output"

                 ]

            }

        ]

    },

    {

    "name": "Stage 2",

    "id": "2",

    "waitRatioAvg": 1,

    "waitRatioMax": 1,

    "readRatioAvg": 0,

    "readRatioMax": 0,

    "computeRatioAvg": 0.05080473913043479,

    "computeRatioMax": 0.05080473913043479,

    "writeRatioAvg": 0.12621304347826087,

    "writeRatioMax": 0.12621304347826087,

    "recordsRead": "1",

    "recordsWritten": "1",

    "steps": [

      {

        "kind": "READ",

        "substeps": [

          "f0_",

          "FROM __stage1_output AS publicdata:samples.shakespeare"

            ]

        },

        {

        "kind": "AGGREGATE",

        "substeps": [

          "SUM_OF_COUNTS(f0_) AS f0_"

            ]

        },

        {

        "kind": "WRITE",

        "substeps": [

          "f0_",

          "TO __output"

                ]

            }

        ]

    }

],

앞서 언급한대로 쿼리 실행의 상대적인 시간 비율들의 정확한 수치를 포함해서, 빅쿼리 웹 콘솔(Web UI)에서 확인한 정보와 동일한 쿼리 실행 정보를 확인 할 수 있다.

 

*Job.get()메소드의 Query Plan정보를 확인할 수 있는 queryPlan속성은 기록된 정보의 출력만 가능하다.

 

Query Plan결과 해석 해결 방안

Query Plan의 결과에 대해서 앞서 언급한대로 작업대기, 읽기, CPU 연산, 쓰기의 상대적인 시간 비율을 토대로 해석이 이루어져야 한다. 따라서 Query Plan 정보를 통해 쿼리의 문제를 분석하고 해결하는 방법을 알아보도록 하자.

 

평균 최대 시간 사이에 차이가 발생하는 경우

평균, 최대시간의 차이가 발생하는 이유는 데이터가 고르지 못하게 비대칭 분포된 경우이다. 이러한 비대칭 분포를 가지는 데이터들은 불균형하게 분포 되어 있기 때문에 쿼리 실행 시 오버헤드를 발생시켜 쿼리가 실행되는 속도를 느리게 만들 수 있다.

 

평균, 최대 시간의 차이가 발생하는 일반적인 원인으로 쿼리에서 JOIN, GROUP BY, PARTITION 절에서 사용할 데이터가 NULL, empty, default 값을 포함하고 있는 경우이다. 따라서 이미 데이터가 고르게 분포되어 있지 않기 때문에 쿼리를 실행 하는 과정에서 편차가 발생한다.

 

트러블 슈팅방법으로 TOP COUNT문에 중간결과를 넣어서, 해당 쿼리 데이터에서 가장 일반적인 값을 분산된 데이터의 Key로써 확인한다.

* 빅쿼리는 Key가 없다

 

데이터의 비대칭을 완화하기 위해서는 필터링을 적용해서 NULL, empty, default값과 같은 쿼리에서 불 필요한 데이터를 감소시켜야 한다. 필터링을 하기위한 방법으로는 고르지 못한 값들에 대해서 쿼리를 실행하고, 나머지 값에 대해서 쿼리를 실행한다.

 

추가적인 방법으로 고르지 못한 데이터를 작은 크기로 세분화하는 방법을 고려한다. 이 경우 세분화 한 데이터 그룹들을 재결합할 때 집계 연산자 사용의 주의가 필요하다.

) 비대칭분포를 가지는 불균형 데이터 세분화

세분화 전

SELECT ... GROUP BY a

세분화 후

SELECT ... FROM (SELECT ... (GROUP by a, b) GROUP BY a)

 

중간 스테이지에서 읽기 또는 쓰기에 대부분의 시간이 소요된 경우

이전 스테이지에서 데이터가 예상보다 많이 생성되는 경우로, 이전 스테이지에서 필터링과 JOIN연산을 사용해서 생성되는 데이터의 크기를 줄인다.

 

입력테이블의 읽기에 대부분의 시간이 소요된 경우

입력테이블 읽기가 비용이 큰 경우이다. 따라서 명확한 필터를 사용하여 불 필요한 데이터의 크기를 감소시키는 방법과 테이블에 대한 파티셔닝, 즉 작은 테이블들로 분할을 해서 대상이 되는 테이블 크기를 줄이는 방법을 통해 쿼리의 성능 향상을 기대해 볼 수 있다.

 

스케줄의 대기에 대부분의 시간이 소요된 경우

쿼리의 스케줄에 많은 작업이 포함되어 있는 경우이다. 만약 작업 시간 자체가 중요한 경우가 아니라면 기다린다. 즉시 작업이 완료 되어야하는 등의 작업 시간이 중요한 경우는 빅쿼리의 slots을 추가해서 사용하는 것을 고려해본다. 여기서 말하는 slots는 CPU연산이 이루어지는 서버계층을 의미한다.

 

출력결과를 쓰는데 대부분의 시간이 소요된 경우

처음 입력된 데이터를 읽은 결과 보다 쿼리가 실행되면서 더 많은 데이터를 내보내는 경우로 볼 수 있으며, 출력하기 전에 일부의 데이터의 필터링으로 불 필요한 데이터의 양을 줄여본다.

 

CPU 연산에 대부분의 시간이 소요된 경우

CPU의 연산에 시간이 많이 소요되는 쿼리의 경우, I/O보다 쿼리 실행과정에서 데이터의 변환 및 프로세싱에 더 많은 시간이 소요되는 경우로 볼 수 있다. 보통 사용자 정의 함수(User defined functions) 또는 JSON데이터, 정규표현식, 등을 포함하는 복잡한 쿼리에서 많이 발생하기 때문에, 필터링을 통해 복잡한 쿼리에서 사용되는 데이터의 크기를 줄여야 한다. 또한 자주 사용되는 쿼리라면, 일부 식에 대한 사전 연산등을 고려하는 것도 하나의 방법이다.

 

결론

앞서 빅쿼리의 Query Plan(쿼리 계획)을 통해 쿼리 실행을 분석하는 방법에 대해서 알아보았다. 빅쿼리가 일반적인 데이터베이스 시스템들과 다른 구조를 가지고 있고 실행된 쿼리에 대해서만 Query Plan정보를 제공하기 때문에 제한적인 부분은 있다. 실행 한 쿼리에 대해서 Query Plan의 상대적인 시간비율을 지표로 세부적인 스탭을 고려해서 데이터의 비대칭 분포를 완화시키거나, 불 필요한 데이터의 크기를 줄여서 성능을 향상 시키는 방법이 주요한 쿼리의 최적화 방향으로 보여진다.

 

참고자료

https://cloud.google.com/bigquery/query-plan-explanation

 

https://cloud.google.com/bigquery/docs/reference/v2/jobs/get

 

https://cloud.google.com/bigquery/docs/reference/v2/jobs

 

빠르게 훑어보는 구글 클라우드 플랫폼

나온지는 조금 되었지만, 방금 받아본 책

일부지만 열심히 작성했던 구글 클라우드 플랫폼 가이드가 E-Book으로 나오고 종이책(주문형)으로도 나왔습니다.

간단하게 Google Cloud Platform 입문 또는 사용하시는 분들에게 도움이 되시길.

E북은 무료고 종이책은 유료(인세를 포함한 관련 수익금은 전액 사회 기부)입니다. 

2권도 조만간 만들어질 거 같네요.


빠르게 훑어보는 구글 클라우드 플랫폼 - 무료 E-Book 링크

http://www.hanbit.co.kr/realtime/books/book_view.html?p_code=E5359426070#hanbit_network




'일상' 카테고리의 다른 글

생각의 전환  (0) 2017.03.08

In-memory query execution in Google BigQuery

 

빅쿼리의 In-memory query 실행

 

최유석

원글 주소 : https://cloud.google.com/blog/big-data/2016/08/in-memory-query-execution-in-google-bigquery

원글 작성자 : Hossein Ahmadi, BigQuery Technical Lead

 

개요

빅쿼리는 대규모의 데이터에 대해서 실시간에 가까운 쿼리실행 속도를 제공한다. 빅쿼리는 높은 성능을 제공하기 위해서 모든 연산이 메모리에서 이루어진다. 이러한 쿼리 실행 속도의 배경에는 빅쿼리의 심플한 아키텍쳐 구조와, 빠른 쿼리 실행이 가능하게 하는 메모리 기반의 연산, 그리고 페타바이트 급의 분석이 가능하게 하는 확장성 있는 데이터의 재분할(repartitioning), 셔플(shuffle) 기능이 있다. 이 글에서는 빅쿼리의 셔플(shuffle)에 대해서 자세히 알아보고, 구글의 페타비트급(petabit-scale) 네트워킹 기술(Jupiter)을 활용해 어떻게 높은 성능으로 메모리상에서 쿼리 실행을 가능하게 하는지 알아보고자 한다.

 

구글의 빅데이터 분석

시작하기에 앞서 구글의 경우, 구글의 수많은 서비스들(YouTube, Gmail. Search,등) 각각의 이용자들만 해도 수억 명에 이른다. 따라서 자연스럽게 구글 내부적으로 빅데이터를 분석하기 위한 기술들이 개발되고 사용되어 왔다. 구글은 오픈소스 플랫폼을 지향하는 모습으로 빅데이터를 포함한 여러 기술들의 논문을 공개해왔다. 몇가지 사례를 보면, Apache Hadoop의 근간이 되는 GFS와 MapReduce, Apache HBase의 근간이 되는 Big Table등이 있으며, BigQuery는 2010년 문서로 공개된 Dremel이라는 기술을 근간으로 한다.

빅쿼리의 성능과 인프라

빅쿼리의 쿼리 실행의 대한 내용을 알아보기에 앞서, 먼저 예제를 통해 빅쿼리의 성능과 인프라에 관련한 부분을 알아보도록 하자.

예제로 보는 빅쿼리의 성능

빅쿼리의 쿼리 실행속도, 즉 성능을 보기 위한 사례로 다음의 예제가 많이 사용된다.

(출처: https://cloud.google.com/blog/big-data/2016/01/anatomy-of-a-bigquery-query )

 

빅쿼리에서 제공하는 공개 데이터 셋에 저장 되어있는 위키피디아 페이지의 제목, 뷰수, 등의 정보가 저장된 테이블에서 1000억(100 billion record)개의 레코드를 스캔하고 제목(title)컬럼에서 정규표현식(regular expression)과 3개의 wildcard를 사용해서 해당하는 문자열("G.*o.*o.*g")을 검색한 결과를 가지고 해당 제목(title)을 가진 페이지의 뷰(views) 수를 카운트하고 각각의 언어별로 그룹으로 묶고 내림차순으로 정렬해서 결과를 보여주는 예제이다.

 

위의 예제에서 쿼리가 실행된 시간은 24.7초가 소요되었으나, 항상 동일한 속도로 실행되지는 않는다. 하지만 직접 실행해서 확인하더라도 동일한 쿼리에 대해서 약 30초 이내에 실행이 되는 것을 확인 할 수 있다.

 

예제로 보는 빅쿼리의 인프라

이제 앞의 예제가 실행되는 동안, 빅쿼리 내부적으로는 사용된 인프라에 대해서 알아보도록 하자. 먼저 위의 쿼리가 실행되는 약 30초 동안 약 4TB의 데이터를 처리하게 된다. 이때 사용되는 빅쿼리 인프라에 대한 대략적인 수치는

이 사용되고 위의 예제의 쿼리를 실행하는데 약 $20 정도의 비용이 발생한다. 위와 같은 쿼리실행 및 인프라 사용을 가능하게 하는 것은 빅쿼리가 방대한 규모의 인프라를 공유하는 멀티 테넌트(multitenancy)서비스이기 때문이다.

빅쿼리의 심플한 사용

일반적인 관계형 데이터베이스(RDBMS) 또는 여러 빅데이터 솔루션을 사용하더라도 위의 예제 분량의 쿼리실행을 하기 위해서는 해당 솔루션에 대한 이해와 설치, 분석에 필요한 코드실행이 필요하고, 또한 제대로 실행하기 위해서 끊임없는 튜닝과 모니터링을 포함한 유지보수가 필요하다. 하지만 빅쿼리의 경우 표준 ANSI SQL과 유사한 SQL(표준 ANSI SQL은 현재 베타로 지원)을 사용하여 쿼리를 입력하고 쿼리 실행버튼인 RUN QUERY만 클릭하면 자동적으로 빅쿼리 내부적으로 위와 비슷하거나, 더 많은 규모의 인프라를 사용하여 실행된 쿼리의 결과를 볼 수 있다.

 

빅쿼리 실행엔진(기본 아키텍쳐)

구글 클라우드 플랫폼(Google Cloud Platform)에서 서비스되고 있는 빅쿼리는 앞서 언급한 Dramel뿐만아니라 구글의 자체적인 기술들인 Borg, Colossus, Jupiter가 융합되어 놀라운 성능을 만들어낸다. 그렇다면 이와 같은 기술들이 어떤 구조로 동작하는지 알아보도록 하자.

 

빅쿼리의 기본 구조

(출처 : https://cloud.google.com/blog/big-data/2016/01/bigquery-under-the-hood )

Dremel

빅쿼리에서 요청이 들어오면 분산 스토리지인 Colossus에서 데이터를 읽고

그 해당 데이터를 페타비트(Petabit)급 네트워크인 Jupiter를 통해 컴퓨팅 처리 계층인 Dremel로 전달한다. 이 때, 데이터를 전달 받은 Dremel에서는 디스크 없이 컴퓨팅 처리가 이루어 진다. Dremel에서는 Leaf Nodes(slots), Mixers, Root server계층으로 나눠지고, Leaf Nodes (slots)에서 필요한 모든 계산을 수행하고, 앞의 예제 기준으로는 Colossus에서 읽은 1000억개의 각각의 레코드에 대한 정규표현식 체크를 하는 것이 이 계층(slots)이다. 그 다음 Leaf Nodes (slots)에서 처리된 데이터에 대해서 Mixer에서 aggregation(집계) 작업을 수행한다. 마지막으로 Mixer에서 집계된 데이터에 대한 정렬 등의 작업을 처리 하고 결과를 리턴 한다.

이렇게 데이터를 처리하는 단계에서 셔플(shuffle)작업이 이루어지며, Jupiter를 통해 빠른 속도로 데이터의 이동이 가능하게 한다. 또한 Dremel, Colossus, Jupiter의 리소스는 Borg를 통해 클러스터로 관리되고 동작한다.

 

 

Colossus

GFS(Google File System)의 후속 버전인 분산 파일 시스템으로 구글 데이터 센터에서 Colossus 클러스터를 가지고 있으며, 한번에 모든 빅쿼리 사용자의 요청에 대해 쿼리 실행 시 수천개의 디스크를 제공할 수 있도록 되어있다. 또한 콜로서스에 저장되는 모든 빅쿼리의 데이터는 자동으로 여러 데이터 센터에 분산 및 복제(replication)되어 저장되기 때문에 높은 안정성을 제공한다. Colossus를 통해 빅쿼리는 In-memory 데이터베이스와 유사한 성능을 보이지만, 저렴한 가격, 높은 병렬성, 확장성, 내구성을 가진 인프라이기 때문에 활용가치가 높다.

빅쿼리는 콜로서스에 구조화된 데이터에 대한 컬럼(Column) 기반 저장형식과 압축 알고리즘을 활용해서 데이터를 저장한다. 이를 통해 빠른 쿼리 실행을 가능하게 하며, 높은 비용을 들이지 않고도 데이터에 대한 저장을 할 수 있다.

Borg

구글의 대규모의 클러스터 관리시스템으로 구글의 서비스들에 사용되는 자원(CPU, RAM, disk, network)을 관리하고 할당하는 역할을 하며, 빅쿼리에서도 쿼리실행,등의 작업요청이 들어오면 미리 예약한 자원을 기반으로 필요한 자원을 할당해서 해당 작업의 실행을 가능하게 한다. 위의 예제처럼 쿼리가 실행 될 때, Dremel클러스터에 수천개의 CPU를 제공하는 것도 Borg의 역할이다. 또한 서버를 포함해서 전원, 네트워크에 이르기까지 수많은 오류에 대한 보호를 하고 관리를 하기 때문에 사용자 입장에서는 발생하는 문제를 개의치 않고 이용 할 수 있다.

 

Jupiter

대규모의 데이터를 원활하게 처리하려면 그에 걸맞는 네트워크도 필수로 필요하다. 구글이 클라우드 플랫폼을 통해 구축하여 제공하는 네트워크(Jupiter)는 양방향 1 Petabit/sec의 대역폭을 제공한다. 10만VM이 각각 10Gb로 통신할 수 있는 속도이다. 이렇듯 상상이상의 대역폭을 자랑하는 Jupiter를 통해 모든 쿼리에 대해서 데이터가 저장된 스토리지(Colossus)에 직접 접근해서 수초만에 테라바이트(terabytes)의 데이터를 읽을 수 있다.

빅쿼리 셔플(Shuffle)의 변화

하둡(Hadoop), 스파크(Spark), 구글 데이터 플로우(Google Cloud Dataflow)에 이르기까지 모든 분산 데이터 처리 시스템에서 셔플은 핵심요소로 작용한다. 데이터 처리 중간의 셔플단계는 크고 복잡한 조인, 집계 및 분석 작업의 실행을 위해 필요하다. 빅쿼리의 셔플은 쿼리 특성과 요구사항의 증가로 인한 셔플 처리 단계의 개선 요구로 인해, 2014년 메모리 기반(디스크 스풀링 지원) 및 구글의 데이터 센터에서 네트워크 기술(Jupiter)로 특별하게 설계되었고 새롭게 개발된 인프라로 이전되었다. 게다가, 특정한 분산 작업을 넘어선 활용사례(예를 들면, 해시조인)와 유연한 데이터 전송 시스템으로 설계되었다. 이 프로젝트는 데이터 전송 기술에 다년간의 연구 및 개발 노력의 산물이다.

빅쿼리 셔플의 차이점

빅쿼리의 셔플은 전용된 호스팅 원격 메모리의 노드 집합에 쿼리 처리의 다양한 단계에서 생산되는 중간 데이터를 셔플링해서 저장한다. 스파크(Spark), 피콜로(Piccolo) 등의 많은 시스템에서 일반적으로 데이터를 처리 할 때 중간 데이터 결과를 지속하여 저장한다. 그러나 빅쿼리는 셔플 작업에서 긴밀한 통합으로 메모리에서 처리된 중간 결과에 대해 다른 방향을 보인다.

 

빅쿼리 셔플의 구성요소

빅쿼리의 셔플 구성은 3가지의 컴포넌트로 구성된다.

빅쿼리 셔플의 구성요소

 

셔플에서 Producer, Consumer, Controller는 다음과 같이 구현되어 있다.

Producer (producer_id) {
void SendRow(row, consumer_id) : Called to send a row to a given consumer
on behalf of this producer.
}
Consumer (consumer_id) {
string ReceiveRow() : Called to receive one row for this consumer.
}
Controller {
StartShuffle() : Called before any producers or consumers start sending or
receiving rows.
EndShuffle() : Called after all producers and consumers have successfully
sent and received all rows.
}

위의 API들은 공유 메모리의 개념을 제공하도록 설계되었기 때문에, 데이타 프로세싱 파이프라인상에서, 데이타를 파티셔닝하는데 범용적으로 사용될 수 있다.

 

빅쿼리 셔플(shuffle)의 개념

빅쿼리 셔플의 기본 동작은 다음의 그림과 같이 설명될 수 있다.

(출처 : https://cloud.google.com/blog/big-data/2016/08/in-memory-query-execution-in-google-bigquery )

 

빅쿼리의 셔플은 많은 Producer들이 효율적으로 원격지의 머신의 메모리에 데이타를 저장할 수 있도록 하며, Consumer역시 높은 처리량으로 동시 읽기가 가능하다. 특히 Producer는 인접한 메모리 블록에 생성된 행(rows)에 로그(Log)하고 색인(index)을 남긴다.

이 색인(index)은 Consumer가 해당 행을 효율적으로 검색하고 읽을 수 있도록 해준다

빅쿼리 셔플의 복합적인 동작

(출처 : https://cloud.google.com/blog/big-data/2016/08/in-memory-query-execution-in-google-bigquery )

 

재분할한 데이타를 메모리에 저장하는 특징이외에도, 빅쿼리 셔플은 또 다른 측면에서 MapReduce스타일의 셔플과 다르다. MapReduce 스타일의 셔플은 모든 행이 재정렬 된 다음에 데이타를 접근할 수 있는데 반하여 빅쿼리에서는 producers에 의해 셔플된 각각 행(rows)은 바로 workers에 의해 접근이 가능하다. 그래서 파이프 라인에서 분산된 작업을 수행 하게 할 수 있게 한다.

빅쿼리의 데이터 분할(partitioning)

데이터의 파티셔닝(partitioning)은 BigQuery의 쿼리의 성능에 상당한 영향을 미친다.

제대로 된 결과를 얻기 위해서는 Consumer와, Provider를 적절한 숫자로 맞추는 것이 중요하다. (빅쿼리가 자동으로 수행)

최적화 되지 않은 데이터의 분할로 쿼리가 매우 느리게 실행되거나, 심지어는 자원의 제약으로 실패할 수 도 있다.

빅쿼리의 파티셔닝은 데이터 크기, 백그라운드의 부하, 기타 요인에 기초하여 쿼리에 사용 된 연산자의 종류에 따라 파티셔닝(분할)을 지능적으로 선택하는 동적 분할 메카니즘을 사용한다. 이로인하여 빅쿼리는 데이타의 특정 분포나, 키에 따른 정렬에 따른 오버헤드 없이 임의의 데이터 셋에 대한 효율적인 쿼리 실행을 할 수 있게 한다.

 

빅쿼리에서 콜로서스를 활용한 이점

빅쿼리는 페타바이트의 데이터에 쿼리를 사용할 수 있다. 대부분의 케이스에서 빅쿼리는 인메모리 셔플링을 통하여 데이타를 재분할 하는데, 메모리만 사용할 경우, 연산 비용이 매우 크기 때문에, 이를 해결하기 위해서 콜로서스 파일 시스템에 경우에 따라 데이타를 메모리에서 부터 이동하여 저장한다.

디스크의 경우 메모리에 비해서 많이 느리기 때문에, 빅쿼리에서는 디스크 억세스를 최소화하는 방법으로 성능 문제를 최소화한다.

 

빅쿼리는 multi-tenant service 이다.

빅쿼리는 사용자가 인프라를 공유해서 사용하는 멀티 테넌트 (multi-tenant) 서비스로 모든 고객이 쿼리를 실행하기 위해 VM의 클러스터의 크기를 조정하고 배포하거나, 리소스(자원)에 대한 프로비저닝이 필요 없다. 그렇게 하기 위해 빅쿼리의 셔플은 메모리에서 대부분의 쿼리를 실행 할 수 있는 지능적인 메모리 자원 관리 시스템을 사용한다. 빅쿼리는 고객으로부터 발생하는 부하의 변동에 따라 즉각적으로 적응한다.

 

결론

모든 빅쿼리의 쿼리는 하나 또는 다수의 셔플 동작을 포함하고, 단일 행 데이터 또는 페타바이트의 쿼리 데이터를 전송하는데 동일한 인프라를 사용한다. 구글 네트워크 기술과 함께 긴밀한 통합(tight intergration)으로 만들어내는 빅쿼리 셔플의 극한의 유연성은 빅쿼리의 사용자가 모든 규모에서 빠른 데이터 분석을 할 수 있게 한다.

 

 

참고자료

https://cloud.google.com/blog/big-data/2016/08/in-memory-query-execution-in-google-bigquery

 

https://cloud.google.com/blog/big-data/2016/01/anatomy-of-a-bigquery-query

 

https://cloud.google.com/blog/big-data/2016/01/bigquery-under-the-hood

 

+ Recent posts