본문으로 건너뛰기
최종 수정: 에

고동시성 포인트 쿼리

고동시성 포인트 쿼리란​

고동시성 포인트 쿼리는 Doris에서 기본 키 동등 조건 쿼리를 위해 제공하는 전용 최적화 기능입니다. 동시성이 높은 서비스 환경에서 사용자는 기본 키를 사용해 시스템에서 한 행의 전체 데이터를 가져오고자 합니다. 하지만 Doris의 기본 열 지향 저장 형식과 쿼리 계획 경로는 이러한 KV 방식의 요청에 적합하지 않습니다.

이를 해결하기 위해 Doris는 다음 계층에 최적화를 적용합니다.

최적화해결하는 문제
행 저장소열이 많은 테이블에서 행 전체를 읽으면 열 지향 스토리지의 랜덤 IO가 증가합니다. 행 저장소는 IO 오버헤드를 줄입니다
Short-Circuit 쿼리 경로(SHORT-CIRCUIT)단순한 쿼리에서는 FE의 쿼리 계획 및 파싱 작업이 지나치게 무겁습니다. short-circuit 경로는 일반적인 계획 수립 과정을 우회합니다
PreparedStatementSQL 파싱과 표현식 평가가 FE CPU를 소모합니다. 계획과 표현식을 캐시하면 오버헤드가 줄어듭니다
행 캐시Page Cache는 대규모 쿼리로 인해 쉽게 축출됩니다. 전용 행 캐시는 적중률을 높입니다

빠른 활성화를 위한 체크리스트​

고동시성 포인트 쿼리를 사용하기 전에 다음 조건을 모두 충족하는지 확인하세요.

  • 테이블이 Unique Key 모델을 사용하며 enable_unique_key_merge_on_write = true가 활성화되어 있어야 합니다
  • 테이블 생성 시 store_row_column = true를 설정해 행 저장소를 활성화해야 합니다
  • 테이블 생성 시 light_schema_change = true를 활성화해야 합니다
  • 쿼리에는 Key 열에 대한 동등 조건만 포함되어야 하며, 조인이나 중첩 서브쿼리가 없어야 합니다
  • JDBC URL에서 useServerPrepStmts=true를 활성화해 PreparedStatement를 사용해야 합니다
  • (선택 사항) BE 설정 disable_storage_row_cache = false를 지정해 행 캐시를 활성화합니다
  • EXPLAIN을 사용해 실행 계획에 SHORT-CIRCUIT 표시가 포함되어 있는지 확인합니다

행 저장소​

행 저장소 모드는 열이 많은 테이블에서 행 전체를 읽을 때 발생하는 랜덤 IO 오버헤드를 줄이는 데 사용됩니다. 현재 구현은 한 행의 데이터를 인코딩하여 별도의 열에 저장합니다.

  • 테이블 생성 시에만 활성화할 수 있으며 이후에는 변경할 수 없습니다.
  • 다음 속성을 PROPERTIES에 지정하여 CREATE TABLE 문에 포함합니다.
"store_row_column" = "true"
  • 행 저장소를 활성화하면 저장 공간 사용량이 증가합니다. Doris 3.0부터 일부 열만 쿼리하면 되는 경우에는 row_store_columns를 사용해 필요한 열만 행 저장소에 포함하는 것을 권장합니다.
"row_store_columns" = "key,v1,v2"

다음 예와 같이 쿼리에서 해당 열에만 접근하면 됩니다.

SELECT k1, v1, v2 FROM tbl_point_query WHERE k1 = 1;

Unique 모델의 포인트 쿼리 최적화​

Unique 모델에서 Merge-On-Write와 행 저장소를 모두 활성화하면 기본 키 포인트 쿼리는 자동으로 short-circuit 경로를 사용합니다. 이를 통해 SQL 실행이 최적화되어 한 번의 RPC만으로 쿼리를 완료할 수 있습니다.

테이블 생성 예제​

CREATE TABLE `tbl_point_query` (
`k1` int(11) NULL,
`v1` decimal(27, 9) NULL,
`v2` varchar(30) NULL,
`v3` varchar(30) NULL,
`v4` date NULL,
`v5` datetime NULL,
`v6` float NULL,
`v7` datev2 NULL
) ENGINE=OLAP
UNIQUE KEY(`k1`)
COMMENT 'OLAP'
DISTRIBUTED BY HASH(`k1`) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"store_row_column" = "true"
);

주요 제약 조건 및 참고 사항​

제약 조건 / 속성설명
enable_unique_key_merge_on_write반드시 활성화해야 합니다. 스토리지 엔진은 이 속성을 기반으로 기본 키 포인트 쿼리를 빠르게 처리합니다
light_schema_change반드시 활성화해야 합니다. 기본 키 포인트 쿼리는 이 기능의 column unique id를 사용해 열을 찾습니다
쿼리 조건단일 테이블의 Key 열에 대한 동등 조건 쿼리만 지원합니다. 조인과 중첩 서브쿼리는 지원하지 않습니다
조건식 형태WHERE 절에는 Key 열에 대한 동등 조건만 포함되어야 하며, 이는 KV 방식의 쿼리로 볼 수 있습니다
행 저장소 공간행 저장소를 활성화하면 저장 공간 사용량이 증가합니다. 3.0+ 버전에서는 row_store_columns를 사용해 일부 열만 지정하는 것을 권장합니다

예를 들어 SELECT * FROM tbl_point_query WHERE k1 = 123은 조건을 충족하므로 short-circuit 최적화 경로를 사용합니다.

PreparedStatement 사용​

SQL 파싱과 표현식 평가의 오버헤드를 줄이기 위해 Doris는 FE 측에서 MySQL 프로토콜과 완전히 호환되는 PreparedStatement 기능을 제공합니다(현재는 기본 키 포인트 쿼리만 지원).

  • 활성화하면 SQL과 표현식을 미리 계산하여 세션 수준의 메모리 캐시에 저장합니다.
  • 이후 쿼리는 캐시된 객체를 직접 재사용하여 반복적인 파싱과 계산을 방지합니다.
  • CPU가 기본 키 포인트 쿼리의 병목일 때 PreparedStatement를 활성화하면 4배를 넘는 성능 향상을 얻을 수 있습니다.

1단계: JDBC URL에서 서버 측 PreparedStatement 활성화​

url = jdbc:mysql://127.0.0.1:9030/ycsb?useServerPrepStmts=true

2단계: 코드에서 PreparedStatement 사용​

// use `?` for placement holders, readStatement should be reused
PreparedStatement readStatement = conn.prepareStatement("select * from tbl_point_query where k1 = ?");
...
readStatement.setInt(1, 1234);
ResultSet resultSet = readStatement.executeQuery();
...
readStatement.setInt(1, 1235);
resultSet = readStatement.executeQuery();
...

행 캐시 활성화​

Doris는 기본적으로 페이지 단위 캐시를 제공합니다. 각 페이지는 단일 열의 데이터를 저장하므로 Page Cache는 열 지향적입니다. 행 저장소에서는 한 행에 여러 열이 포함되며, 대규모 쿼리로 인해 캐시가 비워질 수 있습니다.

적중률을 높이기 위해 Doris는 LRU Cache 메커니즘을 재사용하여 메모리 사용량을 제어하는 별도의 행 캐시를 제공합니다. 다음 BE 설정으로 활성화할 수 있습니다.

설정기본값설명
disable_storage_row_cachetrue (기본적으로 비활성화)행 캐시를 비활성화할지 여부입니다. 활성화하려면 false로 설정합니다
row_cache_mem_limit20%행 캐시가 사용하는 메모리의 비율

성능 튜닝 권장 사항​

위 기능을 활성화한 후 배포 아키텍처에 따라 포인트 쿼리 처리량과 안정성을 추가로 개선할 수 있습니다.

  1. Observer 노드 수 늘리기: Observer를 추가하는 것은 일반적으로 쿼리 처리 용량을 높이는 효과적인 방법입니다.
  2. 쿼리 부하 분산: 포인트 쿼리 요청을 받는 FE의 CPU 사용량이 높거나 처리 속도가 느려지면 JDBC Load Balance를 사용해 요청을 여러 노드로 분산하세요. Nginx나 ProxySQL 같은 다른 솔루션을 사용할 수도 있습니다.
  3. 포인트 쿼리 요청을 Observer로 전달하기: FE Master로 전송되는 포인트 쿼리 요청 수를 줄이면 일반적으로 FE Master의 쿼리 지연 시간 변동이 완화되어 성능과 안정성이 향상됩니다.

자주 묻는 질문​

Q1: 설정이 올바르고 고동시성 포인트 쿼리가 short-circuit 최적화를 사용하는지 어떻게 확인하나요?​

EXPLAIN을 실행하세요. 실행 계획에 SHORT-CIRCUIT이 표시되면 short-circuit 최적화가 사용되고 있는 것입니다.

mysql> explain select * from tbl_point_query where k1 = -2147481418 ;
+-----------------------------------------------------------------------------------------------+
| Explain String(Old Planner) |
+-----------------------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS: |
| `test`.`tbl_point_query`.`k1` |
| `test`.`tbl_point_query`.`v1` |
| `test`.`tbl_point_query`.`v2` |
| `test`.`tbl_point_query`.`v3` |
| `test`.`tbl_point_query`.`v4` |
| `test`.`tbl_point_query`.`v5` |
| `test`.`tbl_point_query`.`v6` |
| `test`.`tbl_point_query`.`v7` |
| PARTITION: UNPARTITIONED |
| |
| HAS_COLO_PLAN_NODE: false |
| |
| VRESULT SINK |
| MYSQL_PROTOCAL |
| |
| 0:VOlapScanNode |
| TABLE: test.tbl_point_query(tbl_point_query), PREAGGREGATION: ON |
| PREDICATES: `k1` = -2147481418 AND `test`.`tbl_point_query`.`__DORIS_DELETE_SIGN__` = 0 |
| partitions=1/1 (tbl_point_query), tablets=1/1, tabletList=360065 |
| cardinality=9452868, avgRowSize=833.31323, numNodes=1 |
| pushAggOp=NONE |
| SHORT-CIRCUIT |
+-----------------------------------------------------------------------------------------------+

Q2: PreparedStatement가 적용되었는지 어떻게 확인하나요?​

Doris에 요청을 보낸 후 fe.audit.log에서 해당 쿼리 요청을 찾으세요. Stmt=EXECUTE()가 표시되면 PreparedStatement가 적용된 것입니다.

2024-01-02 11:15:51,248 [query] |Client=192.168.1.82:53450|User=root|Db=test|State=EOF|ErrorCode=0|ErrorMessage=|Time(ms)=49|ScanBytes=0|ScanRows=0|ReturnRows=1|StmtId=51|QueryId=b63d30b908f04dad-ab4a
3ba21d2c776b|IsQuery=true|isNereids=false|feIp=10.16.10.6|Stmt=EXECUTE(-2147481418)|CpuTimeMS=0|SqlHash=eee20fa2ac13a4f93bd4503a87921024|peakMemoryBytes=0|SqlDigest=|TraceId=|WorkloadGroup=|FuzzyVariables=

Q3: 기본 키를 사용하지 않는 쿼리도 고동시성 포인트 쿼리 전용 최적화를 사용할 수 있나요?​

아니요. 고동시성 포인트 쿼리는 Key 열에 대한 동등 조건 쿼리만 대상으로 하며, 쿼리에 조인이나 중첩 서브쿼리가 포함되어서는 안 됩니다.

Q4: useServerPrepStmts는 일반 쿼리에도 유용한가요?​

현재 PreparedStatement는 기본 키 포인트 쿼리에만 적용됩니다.

Q5: 옵티마이저 선택을 전역으로 설정해야 하나요?​

아니요. PreparedStatement로 쿼리하면 Doris가 성능이 가장 좋은 쿼리 방식을 자동으로 선택하므로 옵티마이저를 수동으로 설정할 필요가 없습니다.

Q6: FE가 병목이 되면 어떻게 해야 하나요?​

FE CPU 사용량이 너무 높으면(%CPU가 높은 경우) JDBC URL에서 다음 부하 분산 및 캐시 설정을 활성화하는 것을 권장합니다.

jdbc:mysql:loadbalance://[host1][:port],[host2][:port][,[host3][:port]]/${tbl_name}?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=500&prepStmtCacheSqlLimit=1024
매개변수용도
loadbalance여러 FE가 요청을 처리할 수 있도록 합니다. FE가 많을수록 좋습니다(노드마다 인스턴스 하나씩 배포)
useServerPrepStmtsFE의 파싱 및 계획 수립 오버헤드를 줄입니다
cachePrepStmts클라이언트 측에 PreparedStatement를 캐시하여 FE로 준비 요청이 빈번하게 전송되는 것을 방지합니다
prepStmtCacheSize캐시할 수 있는 쿼리 템플릿의 최대 개수를 설정합니다
prepStmtCacheSqlLimit캐시되는 개별 SQL 템플릿의 최대 길이를 설정합니다

Q7: 스토리지와 컴퓨팅을 분리한 배포 환경에서 쿼리 성능을 어떻게 최적화할 수 있나요?​

다음 두 가지 방향으로 조정할 수 있습니다.

  • 스냅샷 포인트 쿼리 비활성화:

    SET GLOBAL enable_snapshot_point_query = false;

    포인트 쿼리가 Meta Service에서 버전을 가져오면 추가 RPC가 발생하며, QPS가 높을 때 Meta Service가 쉽게 병목이 될 수 있습니다. 이 값을 false로 설정하면 쿼리가 빨라지지만 데이터 가시성이 낮아집니다(성능과 가시성 사이의 균형을 고려하세요).

  • Base Compaction 출력 캐시 활성화: BE 매개변수 enable_file_cache_keep_base_compaction_output=1을 설정하면 Base Compaction 이후의 결과 데이터가 캐시에 저장되어 원격 접근으로 인한 쿼리 성능 변동을 방지할 수 있습니다.