2021년 6월 11일 금요일

Greenplum 일반 튜닝 Day1

가끔식 Greenplum 튜닝 요청할 때가 있습니다. 이렇게 연락 올 때 어떻게 도움을 드릴 수 있을까 고민하기도 하고, 반복적인 내용들이 많아서 그 때에 진행했던 순서대로 정리를 하였습니다. 성능 튜닝에 도움이 되었으면 좋겠습니다.

Day 1 - 디스크 IO 줄이기
우선 Database 파라미터 부터 확인을 합니다. 
Greenplum에서 DB 파라미터는 크게 튜닝하지 않고 있어요. 몇 개에 대해서만 사전 적용하고, 나머지는 DB레벨, 유저 레벨, 세션 레벨에서 적용을 하죠.

1.H/W물리적인 성능 점검
보통 Greenplum을 설치할 때  gpcheckperf라는 툴을 이용해서 물리적인 성능을 체크합니다. 보통 Disk Read/Write IO, 네트워크 Read/Write IO 성능.

이것을 확인해보면 물리적인 Max 성능을 알 수 있습니다. Greenplum이 빠르다고 이야기를 하지만, 물리적인 성능을 극복할 수 없거든요. 그래서 임계치를 알면 내가 어느 부분을 튜닝해야 할지를 알 수 있습니다.
$ gpcheckperf -h sdw1 -h sdw2 -h sdw3 -h sdw4 -d /data1 -d /data2 -d /data3 -d /data4  -r dsN -D -v

아래는 가상화 시스템에서 나온 수치인데 (POC 테스트 결과) 어느 정도로 나오는지를 확인할 수 있습니다. 
중요한 것은 서버별/VM 별 최대 bandwidth이며, 모든 노드에서 골고루 잘 나와야합니다. 아래 결과는 조금의 차이가 있긴 한데, 그 정도는 무시해도 될 정도.

서버의 디스크/스토리지 구성에 따라 성능에 차이가 발생됩니다. HDD SATA디스크, SAS디스크, SSD와 같이 디스크 타입에서도 차이가 있고, 개수에도 영향을 받습니다. 그리고 가상화의 경우 SAN 스토리지의 성능, SAN 스토리지의 채널 개수에서도 영향을 받습니다. 
(인프라 측면이기에 복잡하고 어려운 이야기 일 수 있는데, 우선은 골고루 잘 나오는지가 중요합니다. 만약 그렇지 않다고 하면, 인프라 엔지니어에게 점검해달라고 요청을 해야 합니다.)

-- per host bandwidth --
    disk write bandwidth (MB/s): 928.03 [sdw4]
    disk write bandwidth (MB/s): 1214.23 [sdw5]
    disk write bandwidth (MB/s): 1024.39 [sdw6]
    disk write bandwidth (MB/s): 1107.05 [sdw7]
    disk write bandwidth (MB/s): 926.01 [sdw1]
    disk write bandwidth (MB/s): 1017.05 [sdw2]
    disk write bandwidth (MB/s): 948.57 [sdw3]
    disk write bandwidth (MB/s): 1115.67 [sdw8]

-- per host bandwidth --
    disk read bandwidth (MB/s): 1148.09 [sdw4]
    disk read bandwidth (MB/s): 1405.64 [sdw5]
    disk read bandwidth (MB/s): 1156.85 [sdw6]
    disk read bandwidth (MB/s): 1151.17 [sdw7]
    disk read bandwidth (MB/s): 1232.21 [sdw1]
    disk read bandwidth (MB/s): 1155.27 [sdw2]
    disk read bandwidth (MB/s): 1147.07 [sdw3]
    disk read bandwidth (MB/s): 1152.97 [sdw8]

2. DB 파라미터 점검 및 적용
$ gpconfig -c gp_workfile_compression -v on --masteronly
쿼리가 수행될 때 설정된 메모리보다 많이 필요할 경우, 파일을 이용해서 사용하게 됩니다. 테이블이 조인이 되던, Group by, Order by를 사용하게 되었을 때 statement_mem을 사용하게 됩니다. Default값은 128MB 이며, 이것 보다 많이 사용하게 될 경우 임시적으로 디스크를 사용하게 되죠.

이때 임시 파일을 생성할 때 압축할 것이냐, 하지 않을 것이냐 하는 파라미터인데, 100% 압축하는 것이 좋습니다.

한 사람이 쓸 경우에는 디스크 IO성능이 좋아서 별반 차이가 없지만, DB라는 것이 한 명을 위한 것이 아니죠. 10~20명 많게는 수백명이 동시에 쿼리가 수행되기 때문에 이때의 병목은 대부분 디스크 병목이 발생될 수 있습니다. 그래서 항상  gp_workfile_compression 파라미터를 on으로 설정하여, 디스크 IO병목을 줄이는 것을 권고하고 있어요. 당연히 on으로 설정하였습니다. 이 파라미터는 적용 후에 DB 리스타트는 필요없고, 설정 Reload만 하면 됩니다. ex) gpstop -u

3.물리스키마 점검
저는 첫날 지원 나갔을 때 무엇을 해야할지 좀 막막합니다. 그래서 위의 2개를 점검하고 테이블별 사이즈와 파티션 적용여부 등을 확인합니다. (가끔식은 인덱스도 확인하기도 합니다.)
  
테이블 사이즈를 확인하기 위한 쿼리
create schema dba;

CREATE OR REPLACE VIEW dba.v_tb_pt_size AS
SELECT a.schemaname AS schema_nm, a.tb_nm, a.tb_pt_nm, a.tb_kb, a.tb_tot_kb
FROM ( SELECT st.schemaname
, split_part(st.relname::text, '_1_prt_'::text, 1) AS tb_nm
, st.relname AS tb_pt_nm, round(sum(pg_relation_size(st.relid)) / 1024::bigint::numeric) AS tb_kb
, round(sum(pg_total_relation_size(st.relid)) / 1024::bigint::numeric) AS tb_tot_kb
FROM pg_stat_all_tables st
JOIN pg_class cl ON cl.oid = st.relid
WHERE st.schemaname !~~ 'pg_temp%'::text AND st.schemaname <> 'pg_toast'::name AND cl.relkind <> 'i'::"char"
GROUP BY 1,2,3) a
ORDER BY a.schemaname, a.tb_nm, a.tb_pt_nm;

## 예시
## 테이블 사이즈/파티션 사이즈/압축 정도를 확인하기 위함 
edu=# select * from dba.v_tb_pt_size where schema_nm = 'edu_sch';
schema_nm | tb_nm | tb_pt_nm | tb_kb | tb_tot_kb
-----------+------------------+-----------------------+--------+-----------
edu_sch | customer | customer | 28480 | 32128
edu_sch | customer_batch | customer_batch | 28512 | 28704
edu_sch | customer_com_col | customer_com_col | 8741 | 8933
edu_sch | customer_com_row | customer_com_row | 12491 | 12683
edu_sch | lineitem | lineitem | 0 | 288
edu_sch | lineitem | lineitem_1_prt_p1992 | 23077 | 40389

4.DB 개발자/DBA 분들께 설명
사실 테이블 사이즈/파티션/압축 등과 같이 이러한 부분은 대부분의 개발자, DBA 분들은 이미 알고 계십니다. 다만, 프로젝트를 하다보면 나중에 해야지 하면서 넘어가는 일이 많기 때문에, 프로젝트 시작시, 아니면 중간 중간에 체크를 하면 나중에 다시 데이터 처리하는 작업을 줄일 수 있을 것으로 생각됩니다.

이때 DB 개발자, DBA 분들께 설명을 드렸던 내용을 기술 드립니다.

대부분의 분석계 Database 들은 디스크 IO병목이 발생되어, CPU/Memory가 남아 돌아도 디스크 병목으로 인하여 성능이 저하되기도 합니다. 많은 경험들을 하시죠. 그래서 디스크 병목을 개선하기 위하여 만든 시스템들이 분산형 Database들이 그 자리를 차지하게 됩니다.

일반적인 SMP Database 아키텍처



디스크 IO 병목도 있지만, 연산 처리할 때에도 한대의 서버 또는 2대의 서버에는 CPU/Memory 성능 제약이 발생할 수 있기 때문에 병렬처리 기반의 DBMS 들이 나왔고, 그 중에 하나인 Greenplum database입니다.

아래 그림은 MPP기반의 DBMS 의 아키텍처 예제입니다.(참고용으로)



디스크 IO 병목만 잡아도 시스템 전반적으로 빨라질 수 있습니다. 항상 기본이지만, 이것 때문에 성능이 안나온적이 정말 많았거든요. 디스크 IO만 잘 나오면 될까요? 시스템은 물리적으로 한계가 있습니다. Greenplum이 빠르다고 이야기 하면서 다니지만, 물리적인 한계를 넘어설 수는 없습니다 
그 이야기는 제일 처음 언급했던 이야기 H/W물리적인 성능 입니다. 이것 보다 더 잘나올 수 없기 때문에 무조건 IO가 높다고 좋은 것은 아니죠. 어떻게 잘 쓰는지가 중요해 지는 부분입니다.

많은 Database 들은 디스크 병목 처리를 위하여 다양한 기법들을 사용하고 있습니다. 그중에 Greenplum도 타 DBMS와 동일하게 Disk IO를 줄이도록 하는 기능들이 있습니다. Disk IO를 줄인다는 이야기는 데이터 스캔을 줄인다는 이야기도 되고, DB에 저장된 파일을 최대한 적게 읽는다는 이야기가 되는 거죠. 즉, 디스크를 최대한 적게 읽으면 디스크 IO 병목을 방지할 수 있겠죠.

그래서 데이터 스캔을 줄이는 방법은 아래와 같습니다.
아래 4가지 중 파티션, 압축, 인덱스가 데이터 스캔을 줄여주는 방법이죠. 이중 데이터 분포/SKEW 도 매우 중요한데, 이것은 Day2일차에 설명 드리도록 하겠습니다. (지금은 무조건 IO를 줄이는 방법에 대해서 설명)



4.1 파티션
파티션을 적용하면 해당 파티션만 스캔하기 때문에 효과가 탁월합니다. 어떤 테이블에 적용을 해야할지, 어느 사이즈로 해야할지 이 부분이 항상 고민이죠. 질문도 많이 나오는 주제입니다. Greenplum 메뉴얼에는 어떤 사이즈로 파티션을 해라, 이러한 부분이 없습니다. 하지만 프로젝트에서는 어느 정도의 가이드 라인이 필요로 하죠. 그래서 제가 즐겨 적용했던 방법은 아래와 같습니다. 

Greenplum에서는 세그먼트 인스턴스라는 개념이 있습니다. 
마스터 서버 1대, 세그먼트(데이터) 서버 4대로 구성되어 있을 경우, 세그먼트(데이터) 서버에는 베어메탈 기준 8개의 Primary 인스턴스를 두게 됩니다. 그렇게 되면 4 세그먼트 노드 * 8 Primary instance = 32 Primary Instance가 되죠. 이것은 Greenplum 운영계 로서 가장 기본이 되는 시스템 구성입니다.

Greenplum의 테이블 파일은 Max 1GB이며, 이것보다 커질 경우 파일이 별도로 생성됩니다. 테이블이 클 경우에는 아래와 같이 존재하게 됩니다. (각 세그먼트 인스턴스에서 OS레벨에서 데이터 파일 구조)

테이블의 파일명       파일 사이즈
11111                      1GB
11111.1                   1GB
11111.2                0.5 GB

파티션 가이드
파티션별 사이즈: 1GB * 세그먼트 인스턴수 수
파티션 적용: 로그성 테이블
ex) 
1GB * 32개 인스턴스 => 32GB => 파티션 사이즐를 32GB로 기준 잡음.
만약 80개 인스턴스로 구성되어 있을 경우에는 80GB를 파티션 기준으로 잡으시면 됩니다.
파이션별 사이즈를 보고, 일/주/월 파티션으로 정리하시면 됩니다.

주의 사항
Greenplum에서 오브젝트 개수가 너무 많아 질 경우에는 카탈로그 사이즈에도 영향을 줄 수 있기 때문에 pg_class의 count 기준으로 10만개 정도해주시는 것이 좋습니다. (20만개, 40만개의 오브젝트를 사용한 시스템도 보긴 하였지만, 관리 차원에서 10만개라는 가이드 라인을 맞추는 것이 좋습니다.) 

4.2 압축
디스크 IO를 줄이는 두번째 효과적인 방법은 테이블을 압축하는 것입니다. 보통 70~85% 정도까지 압축이 됩니다. 그만큼 디스크 공간도 줄지만, Disk IO도 줄게 됩니다. 항상 질문 중에 하나가 그러면 CPU 쪽에 병목이 생기지 않느냐 라는 질문을 받게 됩니다. 압축하지 않으면 몇 세션만으로도 디스크 병목을 일으킬 수 있지만, 압축을 하면 가장 병목이 심한 디스크 병목을 방지할 수 있습니다. 그리고 쿼리의 latency도 보장을 해 줍니다. 
즉 비압축시 3초 내외로 나왔던 쿼리가 3초, 3초 이렇게 나오다가도 디스크 IO병목이 발생될 경우 10초대, 20초대로 튀는 현상이 발생되는데 이러한 현상을 압축으로 방지할 수 있습니다. 그러면 3~4초, 3~4초 나오던 쿼리가 시스템이 Busy하더라도 5~7초 내외로 나올 수 있도록 해주죠.
즉, 병목이 되는 것만 잡으면 Latency를 보장해 줍니다.

압축 적용 여부
단건 Insert/update/delete 발생 (OLTP 형태로 사용시) --> 테이블 비압축
- 마스터성이면서 단건 트랜잭션 위주의 테이블
배치 작업시 (insert/udpate/delete 발생시)                   --> 테이블 압축 
- 로그성 테이블 일 경우
임시 테이블                                                                --> 테이블 압축  

--비압축 적용, 만약 단건 처리가 아니고 배치 처리이면 압축 적용
--압축 적용시 with 절 적용

CREATE TABLE PUBLIC.CUSTOMER

(

    C_CUSTKEY     INTEGER NOT NULL,

    C_NAME        VARCHAR(25) NOT NULL,

    C_ADDRESS     VARCHAR(40) NOT NULL,

    C_NATIONKEY   INTEGER NOT NULL,

    C_PHONE       CHAR(15) NOT NULL,

    C_ACCTBAL     NUMERIC(15,2)   NOT NULL,

    C_MKTSEGMENT  CHAR(10) NOT NULL,

    C_COMMENT     VARCHAR(117) NOT NULL

)

--WITH (appendonly=true, compresslevel=7, compresstype=zstd) 

DISTRIBUTED BY(C_CUSTKEY)

;


--압축 적용

CREATE TABLE PUBLIC.LINEITEM

(   L_ORDERKEY    INT8 NOT NULL,

    L_PARTKEY     INTEGER NOT NULL,

    L_SUPPKEY     INTEGER NOT NULL,

    L_LINENUMBER  INTEGER NOT NULL,

    L_QUANTITY    NUMERIC(15,2) NOT NULL,

    L_EXTENDEDPRICE  NUMERIC(15,2) NOT NULL,

    L_DISCOUNT    NUMERIC(15,2) NOT NULL,

    L_TAX         NUMERIC(15,2) NOT NULL,

    L_RETURNFLAG  CHAR(1) NOT NULL,

    L_LINESTATUS  CHAR(1) NOT NULL,

    L_SHIPDATE    DATE NOT NULL,

    L_COMMITDATE  DATE NOT NULL,

    L_RECEIPTDATE DATE NOT NULL,

    L_SHIPINSTRUCT CHAR(25) NOT NULL,

    L_SHIPMODE     CHAR(10) NOT NULL,

    L_COMMENT      VARCHAR(44) NOT NULL

)

WITH (appendonly=true, compresslevel=7, compresstype=zstd) 

DISTRIBUTED BY(L_ORDERKEY)

partition by range (l_shipdate)

(

partition p1992 start('1992-01-01') end ('1993-01-01') ,

partition p1993 start('1993-01-01') end ('1994-01-01') ,

...

partition p2011 start('2011-01-01') end ('2012-01-01') ,

partition p2012 start('2012-01-01') end ('2013-01-01') ,

partition p2013 start('2013-01-01') end ('2014-01-01') ,

  DEFAULT PARTITION pother

);


--압축 적용

CREATE TABLE TABLE public.load_customer 

WITH (appendonly=true, compresslevel=7, compresstype=zstd) 

AS 

select * from tpcds.customer 

distributed randomly;


4.3 인덱스
특정 키 위주로 검색할 경우에는 인덱스의 성능을 따라 올수가 없습니다. MPP(병렬처리) DBMS에서 인덱스가 필요하냐고 문의 주실 때가 있는데, 없으면 Full scan 이 되기 때문에 특정 key 검색시에는 필요로 합니다.
다만, 운영계 DBMS 처럼 많은 인덱스 생성하지 않고, 정말 필요한 컬럼에 대해서만 싱글 컬럼, 복합 컬럼으로 인덱스를 생성하시면 됩니다. 메뉴얼에는 인덱스 개수가 제한이 없지만, 보통 테이블당 2개~4개 미만으로 권고 드리고 있습니다. 

인덱스 가이드
ID 컬럼과 같이 key 검색되는 컬럼에 대해서 인덱스 생성
테이블당 4개 이하로 인덱스 생성 권고

첫 날은 거의 디스크 IO에 대해서만 이야기를 드렸었습니다. 그리고 몇 일 후에 다시 찾아 뵈었을 때에는 압축 옵션/파티션 등이 잘 정리되었었고, 프로시저 내에서 임시 테이블을 사용하실 때에도 압축 옵션이 잘 적용해 주셨습니다. 
당연히 디스크 IO는 줄었으며, 전반적으로 쿼리 성능도 개선이 되었습니다. 
튜닝의 시작이라고 보시면 될 것 같으며, 그 다음에 찾아뵈었을 때에는 네트워크 IO를 줄이는 방법(분산키) 관련해서 설명을 드렸는데, 이부분은 추후에 다시 업데이트 하도록 하겠습니다.


Greenplum Backup & Restore

Greenplum에서는 gpbackup과 gprestore를 이용해서 대량의 데이터를 병렬로 백업/병렬로 복구를 지원하고 있습니다. Full 백업이외에도 incremental 백업을 지원하고 있습니다.  - incremental 백업시에는 반드시 사전...