PostgreSQL 9.6 성능 이야기

96
  • date post

    09-Apr-2017
  • Category

    Software

  • view

    361
  • download

    53

Transcript of PostgreSQL 9.6 성능 이야기

Page 1: PostgreSQL 9.6 성능 이야기
Page 2: PostgreSQL 9.6 성능 이야기

PostgreSQL 9.6

성능 이야기

(1장.아키텍처 개요)

(2장.Shared Buffer 동작원리)

미리보기입니다.

김시연• 최두원 지음

Page 3: PostgreSQL 9.6 성능 이야기

시연아카데미

Page 4: PostgreSQL 9.6 성능 이야기

PostgreSQL 9.6 성능 이야기

Copyright 2017. 김시연 all rights reserved.

발 행|2017년 3월 20일

지은이|김시연, 최두원

이메일|[email protected]

블로그 | siyeonacademy.wordpress.com

출판사|시연아카데미

출판사 주소|경기도 일산 서구 일현로 97-11 108동 603호

출판사 등록|제 410-2017-000029 호

출판사 전화|(031) 909-0895

디자인 | 김명희

ISBN|979-11-960339-1-0 95500

값|17,000 원

이 책은 저작권의 보호를 받으며 저자의 허락 없이는 어떠한 형태의 번역이나 번안, 유포, 복사,

출판, 게재, 디지털 매체로의 저장 및 전송, 촬영을 할 수 없습니다.

Page 5: PostgreSQL 9.6 성능 이야기

늘 곁에서 응원해주는 가족과

사랑스러운 딸 지율, 지수에게 이 책을 바칩니다.

김시연, 최두원

Page 6: PostgreSQL 9.6 성능 이야기

저자 소개

김시연

DBMS 연구와 지식 공유에 힘쓰는 20년 차 엔지니어이다.

‘PostgreSQL 9.6 성능 이야기’를 시작으로 다양한 ‘이야기’ 시리즈를 계

획하고 있다. 서적뿐만 아니라 온, 오프라인 세미나, 블로그 등 다양한 경

로를 통해 독자와의 만남을 준비하고 있다.

최두원

필드에서 활약 중인 19년 차 엔지니어이다.

지금까지 ORACLE DBMS 튜닝 및 DBA 역할을 수행해왔고, 앞으로는

EDB 관련 업무를 수행할 예정이다. C 개발자 출신답게 PostgreSQL을

소스 레벨에서 깊이 있게 연구하는 걸 좋아한다.

Page 7: PostgreSQL 9.6 성능 이야기

서문

알고 있다고 생각하는 것과 실제로 아는 것.

실제로 아는 것과 그것을 글로 설명하는 것.

그들 간의 너무나 큰 간극.

5개월간의 연구와 집필 과정, 그리고 수차례의 교정, 교열, 윤문 과정을

끝내고 서문을 쓰는 이 순간에도 여전히 아쉬움이 남습니다.

하지만 연구 과정 동안 더 이상의 최선은 없을 정도로 정성을 다했던 시

간이 있었기에 용기를 내서 책을 마무리했습니다.

집필 과정이 힘들 때면 힘이 되던 소설 문구를 끝으로 서문을 마무리하

겠습니다.

“당신의 노력은 절대로 쓸데없는 일이 되지 않습니다.” [나미야 잡화점

의 기적]

2017년 2월 김시연

Page 8: PostgreSQL 9.6 성능 이야기

책 소개

어떤 내용을 다뤘나?

이 책은 PostgreSQL 성능을 다룬 책입니다.

성능을 논하기 위해서는 기본 구조, Shared Buffer 동작 원리, IO 처리 방

식, 옵티마이저 동작 원리, 통계 정보, Explain 분석 방법, 액세스 방법,

조인 방법, 쿼리 재작성, 힌트, 히스토그램, 인덱스, 파티션, 병렬 처리 등

다양한 내용을 알아야 합니다.

이 책은 이와 같은 내용과 더불어 PostgreSQL MVCC 특징 때문에 필요

한 Vacuum, HOT를 설명합니다. 그리고 BRIN에 대한 매우 실용적인

내용을 다루고 있습니다.

책에서 다루지 않는 내용은?

WAL, 백업, 복구, 복제는 다루지 않았습니다. 이 책에서 다루기에는 성

격이 조금 달라서입니다.

누가 읽으면 좋은가요?

PostgreSQL을 2~3년 정도 경험해본 분들과 타 DBMS에 익숙하면서

PostgreSQL를 시작하는 단계인 분들이 읽으면 아주 좋습니다. 그리고

동작 원리에 관심 있는 분들도 읽으면 큰 도움이 될 것입니다.

Page 9: PostgreSQL 9.6 성능 이야기

읽으면 안 되는 분들도 있나요?

DBMS 입문자와 PostgreSQL 전문가는 읽는 것을 권하지 않습니다. 입

문자가 읽기에는 너무 어렵습니다. 반면, 전문가가 읽기에는 너무 평이

합니다. 이 책의 난이도는 ‘중’ 정도입니다.

책을 쓰면서 가장 신경 쓴 부분은?

연구의 정확성을 위해 다양한 테스트와 소스 분석을 병행했습니다. 현학

적인 지식이 아닌 실용적인 지식을 전달하기 위해 노력했습니다. 독자들

이 쉽게 이해할 수 있도록, 60 여 개의 그림으로 동작 원리를 설명했습니

다. 모든 예제는 스크립트를 이용해서 재현 가능합니다. 십 여차례의 문

장 교정을 통해 조금이나마 더 편하게 읽을 수 있도록 노력했습니다. 부

끄럽지 않은 책을 만들려고 최선을 다했습니다.

책을 읽는 방법

책은 1장부터 순서대로 읽는 게 좋습니다. 1장은 아주 기본적인 내용입

니다. PostgreSQL에 익숙한 분들은 건너뛰셔도 됩니다. 2장의 소스 분

석 부분은 관심 있는 분들만 보길 권합니다. 해당 내용을 자세히 몰라도

나머지 부분을 읽는 데 큰 문제는 없습니다. 다만, 소스 설명 중간중간 나

오는 용어는 숙지해야 합니다. 테스트 환경 구성을 위한 스크립트는

http://siyeonacademy.wordpress.com에서 다운로드 할 수 있습니다.

Page 10: PostgreSQL 9.6 성능 이야기

감사의 글

늘 무한의 사랑을 주시는 부모님께 감사합니다.

늘 곁에서 든든한 힘이 돼주는 아내에게 감사합니다.

사랑스러운 딸에게 감사합니다.

연구 기간 내내 같이 고생한 저자 최두원 선생에게 감사합니다.

표지 디자인을 위해 애쓴 김명희 디자이너에게 감사합니다.

이 책이 나오기를 고대하는 최영준 선생에게 감사합니다.

그리고 책 쓰는 것에 대한 가치를 일깨워주신 이전 직장의 조종암 대표

님께 감사합니다.

마지막으로, 이 책을 만나게 될 모든 독자에게 감사의 마음을 전합니다.

Page 11: PostgreSQL 9.6 성능 이야기
Page 12: PostgreSQL 9.6 성능 이야기

목차 | I

Contents

1장. 아키텍처 개요 3

PostgreSQL 아키텍처 3

Shared Memory ......................................................................... 4

프로세스 유형 ................................................................................ 4

Postmaster 프로세스 ................................................................... 5

Background 프로세스 ................................................................. 6

Backend 프로세스........................................................................ 7

데이터베이스 구조 ......................................................................... 8

Vacuum이란? 18

ORACLE과 PostgreSQL의 차이점 19

MVCC 모델의 차이점 ................................................................. 19

Shared Pool 존재 여부 ............................................................ 20

요약 20

2장. Shared Buffer 동작 원리 25

성능 향상을 위한 Shared Buffer의 3가지 목표 26

Shared Buffer 구조 27

해시 테이블 자세히 살펴보기 ..................................................... 28

해시 엘리먼트 자세히 살펴보기 ................................................. 34

버퍼 디스크립터 자세히 살펴보기.............................................. 40

Page 13: PostgreSQL 9.6 성능 이야기

II | PostgreSQL 9.6 성능 이야기

Spin 락과 LW 락 ....................................................................... 45

Shared Buffer에서 버퍼 읽기 48

Shared Buffer 내에 있는 블록을 읽는 경우 ............................ 49

DISK Read가 발생하는 경우 ..................................................... 55

Buffer Replacement를 위한 Clock Sweep 알고리즘 61

Clock Sweep 알고리즘 설명 ..................................................... 61

Clock Sweep 알고리즘 수행 절차 ............................................ 63

공정한 경쟁이란? ....................................................................... 65

Bulk IO 처리를 위한 IO 전략과 Ring Buffer 67

IO 전략이란? .............................................................................. 67

Ring Buffer 살펴보기 ................................................................ 68

요약 77

3장. Query Optimizer 동작 원리 81

CBO (Cost Based Optimizer) 개요 82

COST란? .................................................................................... 83

COST 계산 방식 ........................................................................ 83

통계 정보 96

통계 정보 생성 단위 ................................................................... 96

통계 정보 수동 생성 ................................................................... 97

통계 정보 자동 생성 ................................................................... 98

통계 정보 확인 ........................................................................... 101

통계 정보 제어 ........................................................................... 106

Explain 도구 113

Page 14: PostgreSQL 9.6 성능 이야기

목차 | III

Explain 사용 모드 ..................................................................... 114

Explain 결과 분석 방법 ............................................................ 116

실행 계획 읽는 방법 .................................................................. 119

쿼리 파싱 128

Plan Caching 개요 .................................................................. 129

Prepare Statement 처리 절차 ............................................... 131

ORACLE Bind Peeking에 대한 고찰 ..................................... 133

PostgreSQL Bind Peeking에 대한 고찰 ............................... 134

Literal SQL과 Bind SQL 성능 비교 ........................................ 137

액세스 방법 139

Seq Scan 방식 ......................................................................... 139

Index Scan 방식 ...................................................................... 139

Bitmap Index Scan 방식 ........................................................ 141

Index Only Scan 방식 ............................................................ 157

Tid Scan 방식 ........................................................................... 162

액세스 방식 제어하기 ................................................................ 163

조인 방법 167

Nested Loop 조인 .................................................................. 168

해시 조인 ................................................................................... 182

Outer 조인 203

NL Outer 조인 ........................................................................ 205

해시 Outer 조인 ...................................................................... 209

쿼리 재작성 (Query Rewrite) 212

서브쿼리 Collapse ................................................................... 213

Page 15: PostgreSQL 9.6 성능 이야기

IV | PostgreSQL 9.6 성능 이야기

View Merging과 JPPD (Join Predicate Push-Down) ..... 222

LATERAL 인라인 뷰 ................................................................ 230

PG_HINT_PLAN을 이용한 튜닝 방안 234

PG_HINT_PLAN 설치 ............................................................ 235

PG_HINT_PLAN 동작 원리와 힌트 목록 ............................... 236

PG_HINT_PLAN 예제 ............................................................ 238

히스토그램 256

히스토그램 개요 ....................................................................... 256

히스토그램 생성 ....................................................................... 259

Height-Balanced 히스토그램................................................ 260

Frequency 히스토그램 ........................................................... 264

Hybrid 히스토그램 .................................................................. 268

요약 275

4장. Vacuum 동작 원리 281

PostgreSQL MVCC 모델 282

MVCC란? ................................................................................. 282

PostgreSQL MVCC의 특징 ................................................... 284

4바이트 정수로 XID를 사용할 때의 문제점은? ....................... 286

Frozen XID란? ........................................................................ 288

중간요약 .................................................................................... 289

Vacuum 290

Vacuum 기본기 다지기 ........................................................... 290

Vacuum의 효과 ....................................................................... 292

Page 16: PostgreSQL 9.6 성능 이야기

목차 | V

Vacuum과 락 .......................................................................... 294

Vacuum과 Redo .................................................................... 297

Age란? ..................................................................................... 306

vacuum_freeze_min_age 파라미터 ..................................... 310

Quiz를 통해 이해하는 Vacuum의 원리 .................................. 311

Autovacuum 321

Auovacuum 설정 방법 ............................................................ 321

Autovacuum을 완전히 off 시킬 수 있을까? .......................... 321

autovacuum_freeze_max_age 파라미터 ........................... 322

Quiz를 통해 이해하는 Autovacuum의 원리 ........................ 323

Visibility Map 334

Visibility Map이란? ................................................................ 334

ALL_FROZEN 비트로 인한 Vacuum 성능 향상 ................... 335

HOT (Heap Only Tuple) 339

HOT 탄생 배경......................................................................... 339

HOT 컨셉 ................................................................................. 340

HOT 동작 원리......................................................................... 342

HOT 성능 테스트 ..................................................................... 349

요약 353

5장. Partial Index와 BRIN 357

Partial 인덱스 357

Partial 인덱스의 용도 .............................................................. 357

Partial 인덱스의 예 .................................................................. 358

Page 17: PostgreSQL 9.6 성능 이야기

VI | PostgreSQL 9.6 성능 이야기

BRIN (Block Range Index) 361

BRIN 개념 ................................................................................ 362

BRIN에 대한 궁금증 ................................................................. 364

요약 399

6장. Partition 동작 원리 403

파티션 기본기 다지기 403

생성 방식 .................................................................................. 403

파티션 Pruning ....................................................................... 404

파티션 유형 및 Pruning 테스트 405

Range 파티션 .......................................................................... 405

List 파티션 ................................................................................. 411

해시 파티션 ................................................................................ 414

서브 파티션 ................................................................................ 417

파티션 인덱스 423

파티션 입력 성능 향상을 위한 Tip 426

요약 433

7장. Parallel 프로세싱 437

Parallel 처리 기본기 다지기 438

Parallel 처리 관련 파라미터 .................................................... 438

Worker 프로세스 개수 산정 방식 ........................................... 439

Parallel 프로세스 모델............................................................. 440

Parallel 처리 예제 442

Page 18: PostgreSQL 9.6 성능 이야기

목차 | VII

Parallel 스캔 ............................................................................ 443

Parallel 처리 모니터링 방법 .................................................... 445

Parallel Group By .................................................................. 446

Parallel 해시 조인 .................................................................... 449

요약 450

Page 19: PostgreSQL 9.6 성능 이야기

VIII | PostgreSQL 9.6 성능 이야기

Page 20: PostgreSQL 9.6 성능 이야기

chapter 1.

아키텍처 개요

PostgreSQL 아키텍처

Vacuum이란?

ORACLE과 PostgreSQL의 차이점

요약

Page 21: PostgreSQL 9.6 성능 이야기

2 | PostgreSQL 9.6 성능 이야기

Page 22: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 3

1장. 아키텍처 개요

들어가기

이장에서는 PostgreSQL 구조와 PostgreSQL의 몇 가지 특징을 설명할 것이다.

PostgreSQL 아키텍처

PostgreSQL의 물리적 구조는 매우 단순하다. 공유 메모리, 매우 적은 수의 백

그라운드 프로세스와 데이터파일로 구성된다. (그림 1-1. 참조)

그림 1-1. PostgreSQL 구조

Page 23: PostgreSQL 9.6 성능 이야기

4 | PostgreSQL 9.6 성능 이야기

Shared Memory

Shared Memory에서 가장 중요한 요소는 Shared Buffer와 WAL 버퍼이다.

Shared Buffer

Shared Buffer의 목적은 DISK IO를 최소화하는 것이다. 이를 위해, 아래 항목

을 만족해야 한다. 이 부분은 ‘2장. Shared Buffer 동작 원리’에서 자세히 설명

할 것이다.

매우 큰 (수십, 수백 GB) 버퍼를 빠르게 액세스해야 한다.

많은 사용자가 동시에 접근할 때 경합을 최소화해야 한다.

자주 사용되는 블록은 최대한 오랫동안 버퍼 내에 있어야 한다.

WAL 버퍼

WAL 버퍼는 데이터베이스의 변경 사항을 잠시 저장하는 버퍼이다. WAL 버

퍼 내에 저장된 내용은 정해진 시점에 WAL 파일로 기록된다. 백업 및 복구 관

점에서 보면, WAL 버퍼와 WAL 파일은 매우 중요하다. 다만, 이 책의 주제와

는 다소 차이가 있다. 따라서 이 책에서는 WAL과 관련된 내용은 다루지 않는

다.

프로세스 유형

PostgreSQL의 프로세스 유형은 4가지이다.

Postmaster (Daemon) 프로세스

Page 24: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 5

Background 프로세스

Backend 프로세스

Client 프로세스

Postmaster 프로세스

Postmaster 프로세스는 PostgreSQL을 기동할 때 가장 먼저 시작되는 프로세

스이다. 초기 기동 시에 복구 작업, Shared 메모리 초기화 작업, 백그라운드 프

로세스 구동 작업을 수행한다. 또한, 클라이언트 프로세스의 접속 요청이 있을

때 Backend 프로세스를 생성한다. (그림 1-2. 참조)

그림 1-2. 프로세스 관계도

Page 25: PostgreSQL 9.6 성능 이야기

6 | PostgreSQL 9.6 성능 이야기

pstree 명령어로 프로세스 간의 관계를 확인하면, Postmaster 프로세스가 모든

프로세스의 부모 프로세스인 것을 확인할 수 있다. (이해를 돕기 위해, 프로세

스 ID 뒤에 프로세스 명과 아규먼트를 추가했다)

$ pstree -p 1125

postgres(1125) /usr/local/pgsql/bin/postgres -D

/usr/local/pgsql/data

├─postgres(1249) postgres: logger process

├─postgres(1478) postgres: checkpointer process

├─postgres(1479) postgres: writer process

├─postgres(1480) postgres: wal writer process

├─postgres(1481) postgres: autovacuum launcher process

├─postgres(1482) postgres: archiver process

├─postgres(1483) postgres: stats collector process

Background 프로세스

PostgreSQL 운영에 필요한 백그라운드 프로세스 목록은 다음과 같다. (표 1-1.

참조) Autovacuum launcher를 제외한 나머지 프로세스들은 ORACLE에

서도 쉽게 찾아볼 수 있는 것들이다.

표 1-1. PostgreSQL 백그라운드 프로세스 목록

프로세스 명 수행하는 일

logger 에러 메시지를 로그 파일에 기록한다.

checkpointer 체크포인트 발생 시, dirty 버퍼를 파일에 기록한다.

writer 주기적으로 dirty 버퍼를 파일에 기록한다.

wal writer WAL 버퍼 내용을 WAL 파일에 기록한다.

Page 26: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 7

autovacuum

launcher

Vacuum이 필요한 시점에 autovacuum worker를 fork

한다. Vacuum은 ‘4장. Vacuum 동작 원리’에서 자세히 설

명할 것이다.

archiver Archive log 모드일 때, WAL 파일을 지정된 디렉토리에

복사한다.

stats

collector

세션 수행 정보 (pg_stat_activity )와 테이블 사용 통

계 정보 (pg_stat_all_tables)와 같은 DBMS 사용 통

계 정보를 수집한다.

Backend 프로세스

Backend 프로세스의 최대 개수는 max_connections 파라미터로 설정하며 기

본 설정값은 100이다. Backend 프로세스는 사용자 프로세스의 쿼리 요청을 수

행한 후, 결과를 전송하는 역할을 수행한다. 쿼리 수행에 필요한 몇 가지 메모

리 구조가 필요한데 이것을 통칭해서 로컬 메모리라고 한다. 로컬 메모리와 관

련된 주요 파라미터는 다음과 같다.

work_mem 파라미터

정렬 작업, Bitmap 작업, 해시 조인과 Merge 조인 작업 시에 사용되는 공간이

다. 기본 설정값은 4 MiB이다.

maintenance_work_mem 파라미터

Vacuum 및 CREATE INDEX 작업 시에 사용되는 공간이다. 기본 설정값은 64

MiB이다.

Page 27: PostgreSQL 9.6 성능 이야기

8 | PostgreSQL 9.6 성능 이야기

temp_buffers 파라미터

Temporary 테이블을 저장하기 위한 공간이다. 기본 설정값은 8 MiB이다.

데이터베이스 구조

데이터베이스 구조를 이해하는 데 필요한 몇 가지 사항을 살펴보자.

데이터베이스 관련 사항

1. PostgreSQL은 여러 개의 데이터베이스로 구성된다. 이를 데이터베이

스 클러스터라고 한다.

2. initdb() 수행 시에 template0, template1, postgres 데이터베이

스가 생성된다.

3. template0과 template1 데이터베이스는 사용자 데이터베이스 생성

을 위한 템플릿 데이터베이스이며 시스템 카탈로그 테이블을 포함하

고 있다.

4. initdb() 수행 직후에 template0과 template1데이터베이스의 테

이블 목록은 같다. 단, template1데이터베이스에는 사용자가 필요한

오브젝트를 생성할 수 있다.

5. 사용자 데이터베이스를 생성할 때 template1 데이터베이스를 복제한

다. 이점을 이용하면, 사용자 데이터베이스마다 특정한 오브젝트를 매

번 생성하는 번거로움을 해결할 수 있다.

Page 28: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 9

테이블스페이스 관련 사항

1. initdb() 수행 직후에 pg_default, pg_global 테이블스페이스가 생

성된다.

2. 테이블 생성 시에 테이블스페이스를 지정하지 않으면 pg_dafault 테

이블스페이스에 저장된다.

3. 데이터베이스 클러스터 레벨에서 관리되는 테이블은 pg_global 테이

블스페이스에 저장된다.

4. pg_default 테이블스페이스의 물리적 위치는 $PGDATA\base이다.

5. pg_global 테이블스페이스의 물리적 위치는 $PGDATA\global 이다.

6. 1개의 테이블스페이스를 여러 개의 데이터베이스가 사용할 수 있다.

이때, 테이블스페이스 디렉토리 내에 데이터베이스별 서브 디렉토리

가 생성된다.

7. 사용자 테이블스페이스를 생성하면 $PGDATA\tblspc 디렉토리에 사

용자 테이블스페이스와 관련된 심볼릭 링크가 생성된다.

테이블 관련 사항

1. 테이블 별로 3개의 파일이 존재한다.

2. 1개는 테이블 데이터를 저장하기 위한 파일이다. 파일명은 테이블의

OID이다.

Page 29: PostgreSQL 9.6 성능 이야기

10 | PostgreSQL 9.6 성능 이야기

3. 1개는 테이블 여유 공간을 관리하기 위한 파일이다. 파일명은

OID_fsm 이다.

4. 1개는 테이블 블록의 visibility를 관리하기 위한 파일이다. 파일명은

OID_vm이다.

5. 인덱스는 _vm 파일이 없다. 즉, OID, OID_fsm 2개의 파일로 구성된다.

Note 테이블과 인덱스 생성 시점의 파일명은 OID이며 이 시점에 OID와

pg_class.relfilenode의 값은 같다. 하지만 Rewrite 작업 (Truncate,

CLUSTER, Vacuum Full, REINDEX 등)이 수행되면, 영향받는 오브젝트의

relfilenode 값이 변경되고, 파일명 또한 relfilenode 값으로 변경된다. 참고

로, pg_relation_filepath(‘<object명>’) 명령어를 이용하면 파일 위치

와 이름을 쉽게 확인할 수 있다.

template0, template1, postgres 데이터베이스

앞서 살펴본 내용을 테스트를 통해 확인해보자.

initdb() 수행 후에 pg_database 뷰를 조회하면, template0, template1,

postgres 데이터베이스가 생성된 것을 확인할 수 있다.

select oid, datname, datistemplate, datallowconn

from pg_database order by 1;

oid | datname | datistemplate | datallowconn

-------+-----------+---------------+--------------

1 | template1 | t | t

13321 | template0 | t | f

13322 | postgres | f | t

Page 30: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 11

datistemplate 칼럼을 통해서 template0과 template1 데이터베이

스는 사용자 데이터베이스 생성을 위한 template용 데이터베이스라는

사실을 알 수 있다.

datallowconn 칼럼은 데이터베이스 접속 가능 여부를 알려준다.

template0 데이터베이스는 접속할 수 없으므로, 해당 데이터베이스의

내용 또한 변경할 수 없다.

template 용 데이터베이스를 2개 제공하는 이유는 template0 데이터베

이스는 초기 상태 템플릿을, template1 데이터베이스는 사용자가 추가

한 템플릿을 제공하기 위함이다.

postgres 데이터베이스는 template1 데이터베이스를 이용해서 생성

된 기본 데이터베이스이다. 접속 시에 데이터베이스를 지정하지 않으면

postgres 데이터베이스로 접속된다.

데이터베이스는 $PGDATA/base 디렉토리 아래에 위치한다. 디렉토리 명은 데

이터베이스 OID 번호이다.

[postgres@pgserver data]$ ls -l $PGDATA/base

drwx------. 2 postgres postgres 8192 Nov 4 19:34 1

drwx------. 2 postgres postgres 8192 Nov 4 19:34 13321

drwx------. 2 postgres postgres 8192 Nov 4 19:34 13322

Page 31: PostgreSQL 9.6 성능 이야기

12 | PostgreSQL 9.6 성능 이야기

사용자 데이터베이스 생성

사용자 데이터베이스는 template1 데이터베이스를 복제해서 생성된다. 이를

확인하기 위해서 template1 데이터베이스에 사용자 테이블 T1을 생성한다.

그리고 mydb01 데이터베이스를 생성한 후에, T1 테이블이 존재하는지 확인해

보자.

-- template 데이터베이스에 접속해서 T1 테이블을 생성한다.

template1=# create table t1 (c1 integer);

-- mydb01 데이터베이스를 생성한다.

postgres=# create database mydb01;

-- mydb01 데이터베이스에 접속한 후 T1 테이블이 존재하는지 확인한다.

mydb01=# \d t1

Table "public.t1"

Column | Type | Modifiers

--------+---------+-----------

c1 | integer |

이를 그림으로 표현하면 다음과 같다. (그림 1-3. 참조)

그림 1-3. 템플릿 데이터베이스와 사용자 데이터베이스 간의 관계

Page 32: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 13

pg_default 테이블스페이스

initdb() 수행 후에 pg_tablespace 를 조회하면 pg_default와 pg_global

테이블스페이스가 생성된 것을 확인할 수 있다.

postgres=# select oid, * from pg_tablespace;

oid | spcname | spcowner | spcacl | spcoptions

------+------------+----------+--------+------------

1663 | pg_default | 10 | |

1664 | pg_global | 10 | |

pg_default 테이블스페이스의 위치는 $PGDATA\base이다. 해당 디렉

토리에는 데이터베이스 OID 별 서브 디렉토리가 존재한다.

즉, 물리적 구성으로 보면, 테이블스페이스 하위에 해당 테이블스페이스

를 사용하는 데이터베이스 디렉토리가 존재하는 것이다. (그림 1-4. 참

조)

[postgres@pgserver ~]$ ls -l $PGDATA/base

drwx------. 2 postgres postgres 8192 Nov 4 20:06 1

drwx------. 2 postgres postgres 8192 Nov 4 20:02 13321

drwx------. 2 postgres postgres 8192 Nov 4 20:02 13322

drwx------. 2 postgres postgres 8192 Nov 4 20:06 16396

Page 33: PostgreSQL 9.6 성능 이야기

14 | PostgreSQL 9.6 성능 이야기

그림 1-4. 물리적 구성 관점에서 본 pg_default 테이블스페이스와 데이터베이스 관계

pg_global 테이블스페이스

pg_global 테이블스페이스는 ‘데이터베이스 클러스터’ 레벨에서 관리해야

할 데이터들을 저장하는 용도의 테이블스페이스이다.

예를 들어, pg_database 테이블과 같은 유형의 테이블들은 어떤 데이

터베이스에서 접속해서 조회하더라도 동일한 정보를 제공한다. (그림 1-

5. 참조)

pg_global 테이블스페이스의 위치는 $PGDATA\global이다.

Page 34: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 15

그림 1-5. pg_global 테이블스페이스와 데이터베이스 간의 관계

사용자 테이블스페이스 생성

사용자 테이블스페이스를 생성한 후 변경사항을 확인해보자.

postgres=# create tablespace myts01 location '/data01';

pg_tablespace 조회 결과, myts01 테이블스페이스가 생성된 것을 알 수 있

다.

postgres=# select oid, * from pg_tablespace;

oid | spcname | spcowner | spcacl | spcoptions

-------+------------+----------+--------+------------

1663 | pg_default | 10 | |

1664 | pg_global | 10 | |

24576 | myts01 | 10 | |

Page 35: PostgreSQL 9.6 성능 이야기

16 | PostgreSQL 9.6 성능 이야기

$PGDATA/pg_tblspc 디렉토리 내의 심볼릭 링크는 테이블스페이스 디렉토리

를 가리킨다.

[postgres@pgserver ~]$ ls -l $PGDATA/pg_tblspc

lrwxrwxrwx. 1 postgres postgres 7 Nov 8 15:10 24576 -> /data01

postgres 및 mydb01 데이터베이스에 접속해서 테이블을 생성한다.

[postgres@pgserver ~]$ psql -p 5436 -d postgres

-- 테이블을 생성한다.

postgres=# create table t1 (c1 integer) tablespace myts01;

-- OID 를 확인한다.

postgres=# select oid from pg_class where relname='t1';

oid

-------

24577

[postgres@pgserver ~]$ psql -p 5436 -d mydb01

-- 테이블을 생성한다.

mydb01=# create table t1 (c1 integer) tablespace myts01;

-- OID 를 확인한다.

mydb01=# select oid from pg_class where relname='t1';

oid

-------

24580

테이블 생성 후에 /data01디렉토리를 조회하면, postgres 와 mydb01 데이

터베이스 OID 디렉토리가 생성됐고, 각 디렉토리 내에는 T1 테이블 OID와 동

일한 파일이 존재한다는 것을 알 수 있다.

Page 36: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 17

[postgres@pgserver ~]$ ls -Rl /data01

/data01:

drwx------. 4 postgres postgres 30 Nov 8 15:19

PG_9.6_201608131

/data01/PG_9.6_201608131:

drwx------. 2 postgres postgres 18 Nov 8 15:18 13322

drwx------. 2 postgres postgres 18 Nov 8 15:19 16396

/data01/PG_9.6_201608131/13322:

-rw-------. 1 postgres postgres 40960 Nov 8 15:40 24577

/data01/PG_9.6_201608131/16396:

-rw-------. 1 postgres postgres 40960 Nov 8 15:41 24580

테이블스페이스 location 변경 방법

PostgreSQL 테이블스페이스는 디렉토리를 지정하는 방식이다. 따라서 해당 디

렉토리가 위치한 파일시스템이 꽉 차면 더 이상 데이터를 저장할 수 없다. 이

문제를 해결하기 위해서는 볼륨 매니저를 이용하면 된다. 그런데 볼륨매니저

를 이용할 수 없는 환경이라면 테이블스페이스 location을 변경하는 방법을 고

려해볼 수 있다. 작업 순서는 다음과 같다.

-- PostgreSQL 을 종료한다.

/usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data stop

-- myts01 테이블스페이스 location 을 새로운 파일시스템으로 복사한다.

[postgres@pgserver data01]$ cp -rp /data01/PG* /data02

-- pg_tblspc 디렉토리 내용을 확인한다.

Page 37: PostgreSQL 9.6 성능 이야기

18 | PostgreSQL 9.6 성능 이야기

[postgres@pgserver pg_tblspc]$ ls -l

lrwxrwxrwx. 1 postgres postgres 7 Nov 8 15:52 24576 -> /data01

-- 심볼릭 링크를 삭제한다.

[postgres@pgserver pg_tblspc]$ rm 24576

-- 새로운 심볼릭 링크를 생성한다.

[postgres@pgserver pg_tblspc]$ ln -s /data02 24576

-- 내용 확인

[postgres@pgserver pg_tblspc]$ ls -l

lrwxrwxrwx. 1 postgres postgres 7 Nov 8 15:53 24576 -> /data02

-- PostgreSQL 을 기동한다.

/usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data start

Note 테이블스페이스는 파티션을 사용하는 환경에서도 매우 유용하다. 파티션

테이블 별로 다른 테이블스페이스를 사용할 수 있으므로, 더욱 유연하게 파일시스템

용량 문제를 대처할 수 있기 때문이다.

Vacuum이란?

Vacuum은 다음과 같은 일을 수행한다.

1. 테이블 및 인덱스 통계 정보 수집

2. 테이블 재구성

3. 테이블 및 인덱스 Dead 블록 정리

4. XID Wraparound 방지를 위한 레코드별 XID Frozen

Page 38: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 19

1, 2번 항목은 DBMS 관리를 위해 일반적으로 필요한 작업이다. 하지만 3, 4

번 항목은 PostgreSQL MVCC 특징 때문에 필요한 작업이다. (이 부분은 ‘4장.

Vacuum 동작 원리’에서 자세히 설명할 것이다)

ORACLE과 PostgreSQL의 차이점

필자가 생각하는 가장 큰 차이점은 MVCC 모델 구현 방식과 Shared Pool 존재

여부이다. 이는 PostgreSQL의 특징이기도 하다. (표 1-2. 참조)

표 1-2. ORACLE과 PostgreSQL의 가장 큰 차이점

항목 ORACLE PostgreSQL

MVCC 모델 구현 방식 UNDO 세그먼트 블록 내에 이전 레코드 저장

Shared Pool 존재 여부 존재 존재하지 않음

MVCC 모델의 차이점

동시성을 높이기 위해서는 ‘읽기 작업은 쓰기 작업을 블로킹하지 않고, 쓰기 작

업은 읽기 작업을 블로킹하지 않아야 한다’는 원칙을 지켜야 한다. 이 원칙을

구현하기 위해서는 MVCC (Multi Version Concurrency Control)가 필요하다.

ORACLE 은 MVCC 를 구현하기 위해 UNDO 세그먼트를 사용한다. 반면,

PostgreSQL은 블록 내에 이전 레코드를 저장하는 방식을 사용한다. 이 부분은

‘4장. Vacuum 동작 원리’에서 자세히 설명할 것이다.

Page 39: PostgreSQL 9.6 성능 이야기

20 | PostgreSQL 9.6 성능 이야기

Shared Pool 존재 여부

PostgreSQL은 Shared Pool을 제공하지 않는다. 이것은 ORACLE에 익숙한

사용자에게는 다소 (어쩌면 매우) 당황스러운 사실이다. ORACLE에서 Shared

Pool은 매우 중요하고 필수적인 구성요소이기 때문이다.

PostgreSQL은 Shared Pool 대신 프로세스 레벨에서 SQL 정보를 공유하는 기

능을 제공한다. 즉, 하나의 프로세스에서 같은 SQL을 여러 번 수행하면 최초

1회만 하드 파싱을 한다. 이 부분은 ‘3장. 옵티마이저 동작 원리’에서 자세히

설명할 것이다.

요약

PostgreSQL 구조는 매우 단순하다. 물리적인 관점에서 보면, 공유 메모리, 프

로세스, 데이터 파일로 구성된다. 공유 메모리의 주요 요소는 Shared Buffer와

WAL 버퍼이다.

프로세스는 Postmaster 프로세스, 백그라운드 프로세스, Backend 프로세스와

Client 프로세스로 구분된다.

백그라운드 프로세스는 에러 로그 기록, 버퍼 기록, WAL 버퍼 기록용이 존재

하며 autovacuum을 위한 프로세스가 존재한다. Backend 프로세스는 클라이

언트 프로세스가 요청하는 쿼리를 수행하며, 작업 수행을 위한 로컬 메모리 영

역이 존재한다.

Page 40: PostgreSQL 9.6 성능 이야기

1장. 아키텍처 개요 | 21

PostgreSQL은 여러 개의 데이터베이스를 생성할 수 있고, 사용자 데이터베이

스를 생성할 때는 템플릿 데이터베이스 내용을 복제한다.

물리적인 관점에서 데이터베이스와 테이블스페이스 간의 관계를 보면, 테이블

스페이스 디렉토리 내에 해당 테이블스페이스를 사용하는 데이터베이스들이

위치하는 방식이다.

pg_global 테이블스페이스는 데이터베이스 클러스터 레벨에서 관리해야 할

데이터를 저장한다.

1장을 통해 PostgreSQL 아키텍처를 간단하게 살펴보았다. 2장부터는 본격적

으로 PostgreSQL 동작 원리를 살펴볼 것이다.

Page 41: PostgreSQL 9.6 성능 이야기

22 | PostgreSQL 9.6 성능 이야기

Page 42: PostgreSQL 9.6 성능 이야기

chapter 2.

Shared Buffer 동작 원리

성능 향상을 위한 Shared Buffer의 3가지 목표

Shared Buffer 구조

Shared Buffer에서 버퍼 읽기

Buffer Replacement를 위한 Clock Sweep 알고리즘

Bulk IO 처리를 위한 IO 전략과 Ring Buffer

요약

Page 43: PostgreSQL 9.6 성능 이야기

24 | PostgreSQL 9.6 성능 이야기

Page 44: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 25

2장. Shared Buffer 동작 원리

들어가기

Shared Buffer는 효율적인 IO 처리를 위한 필요적인 요소이다.

이 같은 중요성 때문에 DBMS 버퍼 매니저는 매우 최적화된 상태일 것이다. 다

시 말해, Shared Buffer를 적당한 크기 (수 Gb 또는 수십 Gb, 심지어는 수백 Gb)

로 설정하면, Shared Buffer 때문에 발생하는 성능 문제는 없을 수도 있다.

그렇다면 왜? Shared Buffer 동작 원리를 알아야 할까? 라는 의문이 든다.

이 부분은 필자가 Shared Buffer 연구를 시작하면서 가졌던 의문이기도 하다.

활용되지 못하는 인터널 지식은 실용성 측면에서 보면 의미가 없기 때문이다.

하지만 Shared Buffer에 관한 연구를 거듭할수록 Shared Buffer의 특징이 있

다는 점을 알 수 있었다. IO 전략, Ring 버퍼, Clock Sweep 알고리즘 등이 대표

적인 것들이다. 이러한 특성을 정확히 알지 못하면 IO 처리가 갑자기 느려지는

원인을 파악하는 것이 힘들 수 있을 것이다.

또한 ‘왜?’라는 질문은 고급 기술자가 되기 위해 가장 필요한 요소이기도 하

다. 이는 실용성과는 무관하지만 통찰력과는 깊은 관계가 있다. ‘왜?’라는 질문

을 하고 이에 대한 상상력을 발휘하고, 연구를 통해 상상들의 연결고리를 꿰어

맞추는 일련의 활동들은 고급 엔지니어로 가는 지름길일 것이다. 그럼 본론으

로 들어가보자.

Page 45: PostgreSQL 9.6 성능 이야기

26 | PostgreSQL 9.6 성능 이야기

성능 향상을 위한 Shared Buffer의 3가지 목표

Shared Buffer의 목적은 DISK IO를 최소화함으로써 IO 성능을 향상하는 것

이다. 이를 위해서는 다음과 같은 3가지 목표를 완수해야 한다.

1. 매우 큰 (수십, 수백 GB) 버퍼를 빠르게 액세스해야 한다.

2. 많은 사용자가 동시에 접근할 때 경합을 최소화해야 한다.

3. 자주 사용되는 블록은 최대한 오랫동안 버퍼 내에 있어야 한다.

1, 2 번째 목표는 Shared Buffer 구조와 관련이 있고, 3 번째 목표는 Shared

Buffer 알고리즘과 관련이 있다. 조금 더 자세히 살펴보자.

매우 큰 (수십, 수백 GB) 버퍼를 빠르게 액세스해야 한다.

이 목표를 완수하기 위해서는, Shared Buffer 내에 있는 블록을 매우 빠르게 찾

을 수 있어야 한다. 또한, 읽어야 할 블록이 Shared Buffer 내에 없다면, 없다는

사실을 빠르게 확인할 수 있어야 한다. Shared Buffer 구조는 이 목표를 완수하

기 위한 노력의 결과물이다. 세부적인 내용은 ‘Shared Buffer 아키텍처’에서 설

명할 것이다.

많은 사용자가 동시에 접근할 때 경합을 최소화해야 한다.

Shared Buffer는 많은 사용자가 동시에 사용하는 공유 자원이다. 공유 자원을

보호하기 위해서는 반드시 Lock 메커니즘이 필요하다. 또한, Lock 획득 시의

경합을 최소화하기 위해서는 하나 이상의 Lock을 이용하는 방식이 필요하다.

이 부분도 ‘Shared Buffer 아키텍처’에서 설명할 것이다.

Page 46: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 27

자주 사용되는 블록은 최대한 오랫동안 버퍼 내에 있어야 한다.

이 목표는 Shared Buffer 관리의 효율성과 관련이 있다. 성능 관점에서 보면, 자

주 액세스되는 블록을 최대한 오랫동안 Shared Buffer 내에 유지하는 것은 매

우 중요한 일이다. 이를 위해 PostgreSQL은 Clock Sweep 알고리즘을 사용한

다. 이 부분은 ‘Clock Sweep 알고리즘’에서 설명할 것이다.

Shared Buffer 구조

Shared Buffer를 구성하는 요소는 크게 4가지이다.

1. 해시 테이블

2. 해시 테이블에 딸린 해시 엘리먼트 (및 엘리먼트 키)

3. 버퍼 상태를 관리하는 버퍼 디스크립터

4. 실제 블록을 저장하는 버퍼 풀

Shared Buffer 구조는 다음과 같다.

Page 47: PostgreSQL 9.6 성능 이야기

28 | PostgreSQL 9.6 성능 이야기

그림 2-1. Shared Buffer 구조

해시 테이블 자세히 살펴보기

해시 테이블은 메모리 내의 버퍼를 관리 (검색, 입력)할 때 매우 효과적인 자료

구조이다. 그런데 해시 테이블은 해시 충돌 (Hash Collision)이 발생하면 성능

이 떨어지는 문제점이 있다.

Note 해시 충돌이란, 서로 다른 입력값에 대해서 같은 해시값이 출력되는 것을

의미하는 전산 용어이다. 이를 Buffer 관점에서 보면, 서로 다른 블록이 같은 해시 버

킷에 연결된 것을 의미한다.

Page 48: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 29

해시 충돌 문제를 완화하기 위한 방법의 하나는 해시 테이블을 논리적인 N개

의 세그먼트로 나누어서 관리하는 것이다. 이것을 ‘Segmented 해시 테이블’이

라고 하며, PostgreSQL은 이 방식을 사용한다. (그림 2-2. 참조)

그림 2-2. Segmented 해시 테이블 구조

DIRECTORY란?

Segmented 해시 테이블은 해시 테이블을 N개의 논리적인 해시 세그먼트로 나

눈 것이다. 따라서 N개로 나눈 각 세그먼트의 시작 위치를 가리키는 별도의 배

열이 필요하다. 이 배열을 ‘디렉토리’라고 한다. 디렉토리 기본 설정값은 256

Page 49: PostgreSQL 9.6 성능 이야기

30 | PostgreSQL 9.6 성능 이야기

이며, Shared Buffer를 크게 설정한 경우에는 디렉토리 크기가 증가한다. 이 부

분은 ‘해시 테이블 크기와 해시 세그먼트 개수는?’ 단락에서 설명할 것이다.

해시 세그먼트 크기는?

해시 세그먼트 1개는 256개의 버킷으로 구성된다.

Note 버킷은 배열 요소를 의미한다. 즉, 버킷의 개수가 배열의 길이이다.

해시 테이블 크기와 해시 세그먼트 개수는?

소스를 통해, 해시 테이블 크기를 계산하는 방식을 확인해보자. (내부 구조에

관심 없는 독자들은 이 부분을 스킵해도 된다)

소스 2-1. src/backend/utils/hash/dynahash.c

#define DEF_SEGSIZE 256

#define DEF_SEGSIZE_SHIFT 8 /* must be log2(DEF_SEGSIZE) */

#define DEF_DIRSIZE 256

#define DEF_FFACTOR 1 /* default fill factor */

소스 2-2. src/include/storage/lwlock.h

#define NUM_BUFFER_PARTITIONS 128

소스 2-3. src/backend/utils/hash/dynahash.c

nbuckets = next_pow2_int((nelem - 1) / hctl->ffactor + 1);

hctl->max_bucket = hctl->low_mask = nbuckets - 1;

nsegs = (nbuckets - 1) / hctl->ssize + 1;

nsegs = next_pow2_int(nsegs);

Page 50: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 31

계산 방식을 이해하는 데 필요한 변수는 다음과 같다.

nelem: 버퍼 수 + NUM_BUFFER_PARTITIONS

hctl->ffactor: fill factor를 의미. 기본 설정값은 1

hctl->max_bucket: 해시 테이블 버킷 개수

nsegs: 해시 세그먼트 개수

수식을 차근차근 살펴보자. (블록 크기는 8 KiB, Shared Buffer는 1 GiB라고

가정한다)

nelem

nelem 값은 ‘버퍼 수 + NUM_BUFFER_PARTITIONS’ 이다.

이 값은 해시 테이블 버킷 수를 계산하기 위한 입력 값으로 사용된다. 1 GiB인

경우에 버퍼 수는 131,072이고 NUM_BUFFER_PARTITIONS의 값은 128이다.

따라서 nelem의 값은 131,200이다.

nelem = (1 GiB / 8KiB = 131,072) + (NUM_BUFFER_PARTITIONS=128)

= 131,200

nbuckets

nbuckets 값은 해시 테이블 버킷 수를 의미한다.

아래 수식을 적용하면 해시 테이블 버킷 수는 262,144이다. 이 값은 실제 버퍼

수인 131,072의 2배 정도이다. 이처럼 해시 테이블 버킷 수를 크게 설정하는

Page 51: PostgreSQL 9.6 성능 이야기

32 | PostgreSQL 9.6 성능 이야기

이유는 해시 충돌을 최소화하기 위해서이다. 가장 이상적인 경우는 해시 버킷

1개당 1개의 버퍼가 할당되는 것이다. 하지만 이처럼 완벽한 해시 함수를 적

용하는 것은 매우 힘들다. 따라서 버킷 수를 버퍼 수보다 크게 설정함으로써 해

시 충돌을 최소화하는 방법을 사용하는 것이다.

nbuckets = next_pow2_int((nelem - 1) / hctl->ffactor + 1);

= next_pow2_int((131,200 - 1) / 1 + 1)

= next_pow2_int(131,200)

= 262,144

위의 수식에서 hctl->ffactor은 Fill Factor를 의미하며 기본 설정값은 1이

다. 그리고 next_pow2_int()는 입력 값보다 큰 2의 지수 값을 리턴하는 함수

이다. 따라서 입력 값인 131,200보다 큰 2의 지수승인 2^18 (262,144)을 리턴

한다.

Note 수식을 보면, Fill Factor 값이 클수록 버킷 수가 감소하고, Fill Factor 값이

작을수록 버킷 수가 증가하는 것을 알 수 있다. 이처럼 Fill Factor 값을 조정함으로

써 버킷 수를 증가하거나 감소할 수 있다. 해시 충돌을 최소화하기 위해서는 버킷 수

가 많은 것이 좋으므로, 기본 설정값인 ‘1’을 사용하는 것이 바람직하다.

hctl->max_bucket

hctl->max_bucket 값은 해시 테이블 배열 크기를 의미한다. 배열 인덱스는

0부터 시작하므로 nbuckets -1 로 설정한다.

Page 52: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 33

nseg 설정값

nseg 는 해시 세그먼트 개수를 의미한다. 아래 결과를 보면, Shared Buffer가

1 GiB인 경우에 해시 세그먼트 개수는 1,024라는 것을 알 수 있다. 해시 세그

먼트 개수가 1,024이므로 디렉토리 크기도 1,024로 설정된다.

nsegs = (nbuckets - 1) / hctl->ssize + 1

= (262,144 – 1) / 256 + 1

= 1,024

nsegs = next_pow2_int(nsegs);

= next_pow2_int(1,024)

= 1,024

위의 수식에, hctl->ssize 값은 해시 세그먼트 당 버킷 수를 의미하며, 기본

설정값은 256이다.

버퍼 파티션이란?

앞서 언급한 디렉토리, 해시 세그먼트, 해시 테이블은 모두 Shared Buffer 내에

존재하는 공유 리소스이다.

공유 리소스는 LW (Light Weight) 락을 이용해서 보호한다. 즉, Backend 프로

세스가 공유 메모리를 액세스하기 위해서는 LW 락을 획득해야 한다.

이처럼 LW락은 공유 리소스를 보호한다는 긍정적인 측면이 있지만, LW락 경

합 때문에 성능 저하가 발생할 수 있다는 문제점이 있다.

예를 들어, 해시 테이블을 관리하는 LW락이 1개라면, 해시 테이블을 액세스

하는 대다수 프로세스는 LW락을 대기하게 된다. PostgreSQL은 이 문제점을

Page 53: PostgreSQL 9.6 성능 이야기

34 | PostgreSQL 9.6 성능 이야기

해결하기 위해서 해시 테이블을 N개의 ‘버퍼 파티션’으로 나누고, 버퍼 파티

션마다 1개의 LW 락을 할당하는 방식을 사용한다.

버퍼 파티션 개수는?

9.4버전까지, 버퍼 파티션 개수는 16개밖에 되지 않았다. 이로 인해, 동시성이

높은 환경에서는 버퍼 파티션 액세스 시에 LW 락 경합이 발생할 가능성이 컸

고, 실제로 이로 인한 성능 문제가 발생하기도 했다. 이 문제를 해결하기 위해

9.5 버전부터는 버퍼 파티션 개수를 128개로 상향 조정했다. 결과적으로, 버퍼

파티션을 관리하는 LW락 또한 16개에서 128개로 증가했다. 참고로, 버퍼 파

티션의 개수는 NUM_BUFFER_PARTITIONS 변수로 설정할 수 있다.

Note LW락은 Light Weight 락의 약어이다. 이름에서 알 수 있듯이, LW락은

매우 가벼운 락을 의미하며 메모리를 액세스할 때 사용하는 방식이다. LW락은 테

이블 내의 데이터를 보호하는 락과는 다른 개념이란 점을 유의해야 한다. LW락은

‘Spin락과 LW락’ 단락에서 설명할 것이다. 참고로 ORACLE은 LW락을 Latch라

고 표현한다.

해시 엘리먼트 자세히 살펴보기

소스 분석을 통해서 자세히 살펴보자.

(내부 구조에 관심 없는 독자들은 이 부분을 스킵해도 된다) 해시 엘리먼트는

엘리먼트와 엘리먼트 키로 구성된다. 엘리먼트를 먼저 살펴보자.

Page 54: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 35

엘리먼트 구성 요소

엘리먼트는 Next 엘리먼트를 가리키는 엘리먼트 포인터와 hashvalue로 구성

된다. (소스 2-4. 참조) hashvalue는 BufferTag를 이용해서 계산한 값이다.

소스 2-4. src/include/utils/hsearch.h

typedef struct HASHELEMENT {

struct HASHELEMENT *link; /*link to next entry in same bucket */

uint32 hashvalue; /* hash function result for this entry */

} HASHELEMENT;

엘리먼트 KEY 구성 요소

엘리먼트 KEY는 다음과 같이 구성된다. (그림 2-3. 참조)

엘리먼트 KEY는 BufferTag 구조체와 버퍼 디스크립터 배열 인덱스로

구성된다. (소스 2-5. 참조)

BufferTag 구조체는 RelFileNode 구조체, forkNum, blockNum으로

구성된다. (소스 2-6. 참조)

RelFileNode 구조체는 테이블스페이스 번호, 데이터베이스 번호, 오브

젝트 번호로 구성된다. (소스 2-7. 참조)

소스 2-5. /src/backend/storage/buffer/buf_table.c

typedef struct {

BufferTag key; /* Tag of a disk page */

int id; /* Associated buffer ID */

Page 55: PostgreSQL 9.6 성능 이야기

36 | PostgreSQL 9.6 성능 이야기

} BufferLookupEnt;

소스 2-6. /src/include/storage/buf_internals.h

typedef struct buftag {

RelFileNode rnode; /* physical relation identifier */

ForkNumber forkNum;

BlockNumber blockNum; /* blknum relative to begin of reln */

} BufferTag;

소스 2-7. /src/include/storage/relfilenode.h

typedef struct RelFileNode {

Oid spcNode; /* tablespace */

Oid dbNode; /* database */

Oid relNode; /* relation */

} RelFileNode;

그림 2-3. 엘리먼트 KEY 구성 요소

Page 56: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 37

BufferTag란?

BufferTag는 블록의 주민등록번호와 같은 개념이다. 즉, 클러스터 데이터베이

스 내에서 각 블록을 유일하게 식별할 수 있는 데이터로 구성된다. 이를 위해,

다음과 같은 구조를 이용한다.

BufferTag는 BufferTag 구조체에 저장된다.

BufferTag 구조체는 RelFileNode 구조체, forkNum, blockNum으로

구성된다.

RelFileNode 구조체는 테이블스페이스 번호, 데이터베이스 번호, 오브

젝트 번호로 구성된다. 그래야만, 클러스터 데이터베이스 내에서 유일한

오브젝트 번호를 획득할 수 있기 때문이다.

forkNum은 오브젝트 유형을 의미한다. 0은 테이블 (또는 인덱스), 1은

FSM, 2는 VM이다. (소스 2-8. 참조)

blockNum은 블록 번호를 의미한다.

정리하면, RelFileNode 구조체에 유일한 오브젝트 번호를 저장하고,

forkNum과 blockNum을 이용해서 오브젝트 유형별로 유일한 블록 번호를 저

장함으로써, 클러스터 데이터베이스 내의 각 블록을 유일하게 식별할 수 있다.

소스 2-8. /src/include/common/relpath.h

typedef enum ForkNumber {

InvalidForkNumber = -1,

MAIN_FORKNUM = 0,

FSM_FORKNUM,

Page 57: PostgreSQL 9.6 성능 이야기

38 | PostgreSQL 9.6 성능 이야기

VISIBILITYMAP_FORKNUM,

INIT_FORKNUM

} ForkNumber;

해시 엘리먼트 메모리 할당 방식

설명을 진행하기 전에 퀴즈를 하나 풀어보자. 해시 엘리먼트 메모리 구조는 어

느 시점에 할당될까?

1. DB 시작 시점에 일정 개수만큼 미리 할당

2. DB 시작 후에 요청 시점마다 할당

정답은 1번이다.

PostgreSQL은 DB 기동 시에 ‘버퍼 수 + NUM_BUFFER_PARTITIONS’ 개의 해

시 엘리먼트 배열을 미리 할당한다. 그리고 필요할 때마다 (버퍼가 할당될 때

마다) 맨 뒤에 있는 것부터 꺼내 쓰는 방식을 사용한다.

9.5 버전까지는 1개의 freeList가 해시 엘리먼트 배열 전체를 관리했다. 따

라서 많은 프로세스가 동시에 버퍼를 요청하면 freeList에 대한 병목이 발

생하는 문제가 있었다. 이 문제를 해결하기 위해 9.6 버전부터는 여러 개의

freeList를 사용한다.

Page 58: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 39

멀티 freeList

9.6 버전부터는 32개의 freeList를 사용한다.

freeList 개수는 NUM_FREELISTS 로 설정할 수 있으며, 기본 설정값은 32이

다. 따라서 Shared Buffer가 1 GiB라면, 1개의 freeList 마다 4,100개의 해

시 엘리먼트를 관리하게 된다. 공식을 대입하면 다음과 같다.

Shared Buffer = 1 GiB

=> 초기 할당되는 해시 엘리먼트 배열 수

(132,000) = 버퍼 수 (131,072) + NUM_BUFFER_PARTITIONS (128)

=> 1 개의 FreeList 가 관리하는 해시 엘리먼트 배열 수

(4,100) = 132,000 / NUM_FREELISTS (32)

그리고 앞서 ‘맨 뒤에 있는 것부터 꺼내 쓴다’라고 설명한 바 있다. 이것은 해시

엘리먼트 연결 방식과 관련이 있다. 그림 2-4에서 보는 것과 같이, 32개의 각

해시 엘리먼트 배열은 맨 뒤에서부터 앞으로 링크가 연결된 구조이다. 그리고

HASHHDR 구조체 내의 freeList는 각 해시 엘리먼트 배열 맨 뒤의 해시 엘리

먼트를 가리킨다. 즉, 32개의 freeList를 이용해서 32개의 해시 엘리먼트

배열을 관리하는 방식을 적용한 것이다. 이로써, 버퍼 할당 시 경합이 감소하게

된다.

Page 59: PostgreSQL 9.6 성능 이야기

40 | PostgreSQL 9.6 성능 이야기

그림 2-4. DB 기동 직후에 해시 엘리먼트 배열이 할당된 모습

버퍼 디스크립터 자세히 살펴보기

버퍼 디스크립터는 버퍼 메타데이터를 관리하기 위한 구조체이다. 소스를 통

해서 구성 요소를 자세히 살펴보자. (내부 구조에 관심 없는 독자들은 이 부분

을 스킵해도 된다)

Page 60: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 41

버퍼 디스크립터 구성 요소

소스 2-9. src/include/storage/buf_internals.h

typedef struct BufferDesc {

BufferTag tag; /* ID of page contained in buffer */

int buf_id; /* buffer's index number (from 0) */

/*state of the tag, containing flags, refcount and usagecount*/

pg_atomic_uint32 state;

int wait_backend_pid;/* backend PID of pin-count waiter */

int freeNext; /* link in freelist chain */

LWLock content_lock; /* to lock access to buffer contents*/

} BufferDesc;

버퍼 디스크립터 구조체의 주요 구성 요소는 다음과 같다.

tag: BufferTag를 저장한다.

buf_id: 실제 버퍼가 저장된 ‘버퍼 풀’ 배열 내의 인덱스 번호이다.

wait_backend_pid: 버퍼를 액세스하기 위해서는 버퍼 PIN을 획득해

야 한다. 만일 버퍼 PIN을 획득하지 못하면 대기해야 한다. 해당 칼럼은

버퍼 PIN을 대기하는 프로세스 ID를 제공한다.

context_lock: 버퍼를 액세스할 때 필요한 LW락이다. LW락 획득과

관련한 내용은 ‘Spin 락과 LW 락’ 단락에서 설명할 것이다.

state와 freeNext 항목에 대한 자세한 설명은 다음과 같다.

Page 61: PostgreSQL 9.6 성능 이야기

42 | PostgreSQL 9.6 성능 이야기

state 항목 자세히 살펴보기

9.5 버전까지는 버퍼를 액세스하기 위해 PIN 을 설정한 프로세스 수

(refcount), 버퍼 액세스 횟수 (usage_count), 버퍼 상태 플래그 (flags)를

각각 제공했었다. 하지만 9.6부터는 이 3가지 항목을 state 항목 1개로 제공

한다. (그림 2-5. 참조)

그림 2-5. state 항목 구성 요소

정보 획득을 위한 비트 연산

이처럼 비트 단위로 데이터를 저장하므로, 비트 마스크와 비트 시프트 연산을

수행해서 데이터를 추출해야 한다. 비트 연산 방식은 다음과 같다.

소스 2-10. src/include/storage/buf_internals.h

#define BUF_REFCOUNT_ONE 1

#define BUF_REFCOUNT_MASK ((1U << 18) - 1)

#define BUF_USAGECOUNT_MASK 0x003C0000U

#define BUF_USAGECOUNT_ONE (1U << 18)

#define BUF_USAGECOUNT_SHIFT 18

#define BUF_FLAG_MASK 0xFFC00000U

/* Get refcount and usage_count from buffer state */

Page 62: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 43

#define BUF_STATE_GET_REFCOUNT(state) ((state) &

BUF_REFCOUNT_MASK)

#define BUF_STATE_GET_USAGECOUNT(state) (((state) &

BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT)

소스 중에서 BUF_STATE_GET_USAGECOUNT 예를 보면, 해당 값을 추출하기 위

해 비트 마스크 연산과 우 시프트 연산을 이용한다는 것을 알 수 있다. (그림 2-

6. 참조)

그림 2-6. 비트 연산을 이용해서 usage_count 획득하는 절차

flags 항목 자세히 살펴보기

flags 항목은 10개의 비트를 이용해서 10개의 버퍼 상태를 나타낸다. 예를

들어, 5번째 비트 (32비트 중에서는 27번째 비트)인 BM_IO_IN_PROGRESS 비

트는 ‘디스크 IO 진행 중’ 상태를 관리한다. 따라서 버퍼가 디스크 IO 중이면,

해당 비트를 ‘1’로 설정해야 한다. 이를 위해, 1을 왼쪽으로 26번 시프트 한다.

(1U는 Unsigned Integer 1을 의미한다)

Page 63: PostgreSQL 9.6 성능 이야기

44 | PostgreSQL 9.6 성능 이야기

소스 2-11. src/include/storage/buf_internals.h

#define BM_LOCKED (1U << 22)

#define BM_DIRTY (1U << 23)

#define BM_VALID (1U << 24)

#define BM_TAG_VALID (1U << 25)

#define BM_IO_IN_PROGRESS (1U << 26)

#define BM_IO_ERROR (1U << 27)

#define BM_JUST_DIRTIED (1U << 28)

#define BM_PIN_COUNT_WAITER (1U << 29)

#define BM_CHECKPOINT_NEEDED (1U << 30)

#define BM_PERMANENT (1U << 31)

freeNext 항목 자세히 살펴보기

freeNext는 다음번 empty 버퍼를 가리키는 포인터이다.

DB 기동 직후에, 모든 버퍼의 상태는 empty이므로 freeNext는 다음번 배열

요소를 가리키게 된다. 이후에 버퍼 할당 요청을 받으면 그림 2-7과 같이 동작

한다. 즉, BufferStrategyControl 구조체의 firstFreeBuffer 항목은

freeNext 링크 헤더를 가리키고, lastFreeBuffer 항목은 freeNext 링크

Tail을 가리킨다.

모든 버퍼가 할당된 후에는 empty 버퍼가 존재하지 않는다. 따라서 이 시점 이

후에 버퍼 할당 요청을 받으면 Victim 버퍼를 선정해야 한다. (이때,

firstFreeBuffer는 -1 값을 갖는다) 이 부분은 ‘Clock Sweep 알고리즘’ 단

락에서 설명할 것이다.

Page 64: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 45

그림 2-7. 버퍼 할당에 따른 freeNext 링크 변경 사항

Spin 락과 LW 락

Shared Buffer를 액세스할 때 Spin락과 LW 락을 획득해야 한다. ORACLE에

익숙한 독자라면, Spin락은 mutex, LW 락은 latch라고 생각하면 된다.

Spin 락과 LW 락을 구분하는 이유는 성능 향상을 위해서이다. LW 락은 매우

가벼운 락이고, Spin 락은 이보다 더 가벼운 락이다. 따라서 가능하다면 Spin

락을 사용하는 것이 성능상 유리하다. 2개 차이점을 정리하면 다음과 같다. (표

2-1. 참조)

Page 65: PostgreSQL 9.6 성능 이야기

46 | PostgreSQL 9.6 성능 이야기

표 2-1. Spin락과 LW락 차이점

항목 Spin 락 LW 락

사용 부하 매우 매우 적음 매우 적음

Context

Switching

발생하지 않음 발생할 수 있음

동작 방식 Spin 방식 큐 & 포스팅 방식

사용 용도 구조체 내 변수를 액세스

할 때 사용

구조체를 액세스할 때 사용

사용 모드 EXCLUSIVE SHARE & EXCLUSIVE

Spin 락

Spin 락은 매우 매우 가볍게 동작한다.

예를 들어 구조체 변숫값을 변경한다고 가정해보자. 구조체 변수도 공유 메모

리 내에 존재하므로 해당 값을 읽거나 변경하기 위해서는 락이 필요하다. 이때

Spin 락을 사용한다. Spin 락을 사용하는 이유는 변숫값을 변경하는 것과 같은

오퍼레이션은 매우 짧은 시간에 수행될 수 있기 때문이다. 만일 다른 프로세스

가 이미 Spin 락을 획득한 상태라고 할지라도, 몇 차례 Spin (루프)을 수행한 후

에는 Spin 락을 획득할 가능성이 매우 크다.

Spin을 수행하면 Sleep 상태로 빠지지 않기 때문에 Context Switching이 발생

하지 않는다는 장점이 있다.

Page 66: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 47

Spin 락 구현 방식

Spin 락을 구현하는 방식은 2가지이다.

mutex를 이용하는 방식

TAS (Test & Set)를 인라인 어셈블리어로 구현하는 방식

PostgreSQL은 두번째 방식을 사용한다.

LW 락

PostgreSQL은 ‘큐 & 포스팅’ 방식을 이용해서 LW 락을 획득한다. (SPIN 방식

은 사용하지 않는다)

해시 버킷에 딸린 해시 엘리먼트를 검색한다고 가정해보자. 이때는 읽기

모드이므로 LW 락을 SHARED 모드로 획득해야 한다.

해시 엘리먼트 내에 BufferTag 정보를 입력한다고 가정해보자. 이때는

쓰기 모드이므로 LW 락을 EXCLUSIVE 모드로 획득해야 한다.

LW 락 획득 절차

LW 락 획득을 위한 Pseudo코드는 다음과 같다.

LWLockAcquire(LWLock *lock, LWLockMode mode)

LOOP

LWLockAttemptLock() 함수를 호출해서 LW 락 획득을 시도한다.

락을 획득하면 LOOP를 탈출한다.

만일 락을 획득하지 못하면 대기 큐에 등록한다.

Page 67: PostgreSQL 9.6 성능 이야기

48 | PostgreSQL 9.6 성능 이야기

한 번 더 LWLockAttemptLock() 함수를 호출한다.

만일 락을 획득하면 대기 큐에 등록된 항목을 삭제한 후 LOOP를 탈출한다.

만일 락을 획득하지 못한다면 대기 상태를 시작한다.

(다른 프로세스가 깨워줄 때까지 대기 상태가 유지된다)

END LOOP

Shared Buffer에서 버퍼 읽기

지금까지 우리는 Shared Buffer 주요 구성 요소들에 대해서 살펴보았다. 주요

사항들을 요약하면 다음과 같다.

Shared Buffer는 크게 해시 테이블, 해시 엘리먼트, 버퍼 디스크립터와

버퍼 풀로 구성된다.

해시 테이블은 배열 구조이며, 해시 충돌을 최소화하기 위해 Segmented

해시 테이블 구조를 사용한다.

또한 동시성을 높이기 위해 논리적인 파티션으로 나눠서 관리한다.

이때, 파티션 당 LW 락은 1개이다.

해시 엘리먼트는 엘리먼트와 엘리먼트 키로 구성된다.

엘리먼트는 BufferTag에 대한 hashvalue와 다음번 엘리먼트를 가리키

는 포인터로 구성된다.

엘리먼트 키는 BufferTag 와 버퍼 id를 저장한다.

BufferTag란 블록에 대한 주민등록번호와 같다.

Page 68: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 49

BufferTag는 데이터베이스번호, 테이블스페이스 번호, 오브젝트 번호,

fork 번호 및 블록 번호로 구성된다.

MAIN 오브젝트 fork 번호는 0, FSM은 1, VM은 2이다.

9.6 버전부터 해시 엘리먼트는 32개의 freeList로 관리된다. 이는 경

합 해소를 위함이다.

버퍼 디스크립터는 버퍼의 메타 정보를 관리하는 구조체 배열이다. 이때,

배열 수는 버퍼 수와 동일하다.

버퍼 디스크립터에서 관리하는 주요 메타 정보는 refcount,

usage_count, flags, freeNext 등이다.

이제 독자들은 Shared Buffer 각 요소에 대한 대략적인 이미지를 머릿속에 담

고 있을 것이다. 이번 단락에서는 아래 2가지 처리 흐름을 살펴보도록 하자.

1. Shared Buffer 내에 있는 블록을 읽는 경우

2. Disk Read가 발생하는 경우

Shared Buffer 내에 있는 블록을 읽는 경우

Shared Buffer 내에 있는 블록 읽기를 먼저 설명하는 이유는 가장 단순하기 때

문이다. 처리 순서상으로는 Disk Read가 발생하는 경우를 먼저 설명하는 것이

맞지만, Disk Read가 발생하는 경우의 처리 순서는 다소 복잡하다.

Page 69: PostgreSQL 9.6 성능 이야기

50 | PostgreSQL 9.6 성능 이야기

따라서 읽고자 하는 블록이 이미 Shared Buffer 내에 존재하는 경우를 먼저 설

명한다. 이때, Shared Buffer 크기는 256 MiB이고 현재 Shared Buffer에 로딩

된 블록은 3 개라고 가정한다. 그리고 읽고자 하는 버퍼의 BufferTag 는

‘Tag_B’라고 가정하고 설명을 진행할 것이다. (그림 2-8. 참조)

그림 2-8. 현재 Shared Buffer 상태

Note 실제 BufferTag에 저장되는 값은 ‘Tag_A’, ‘Tag_B’와 같은 값들은 아니

다. 앞서 설명한 것처럼 BufferTag는 블록을 유일하게 구분할 수 있는 값을 저장한

다. 다만, 설명의 편의를 위해 ‘Tag_A’와 같은 값을 사용하는 것이다.

그림 2-8에서 보이는 각 요소의 크기는 표 2-2와 같다. (앞서 ‘Shared Buffer 구

조’ 단락에서 설명한 내용들이지만 편의를 위해 다시 한번 정리했다)

Page 70: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 51

표 2-2. Shared Buffer 요소별 수치

항목 계산 방식 Value

버퍼 수 256 MiB / 8 KiB 32,768

버퍼 파티션 수 #define

NUM_BUFFER_PARTITIONS

128

nelem 버퍼 수 + 버퍼 파티션 수 32,896

hctl-ffactor #define DEF_FFACTOR 1

해시 테이블 배열

(버킷 수)

nbuckets =

next_pow2_int((nelem - 1)

/ hctl->ffactor + 1);

65,536

해시 세그먼트 크기 #define DEF_SEGSIZE 256

해시 세그먼트 수 해시 테이블 배열 크기 / 해시 세

그먼트 크기

256

FreeList 수 #define NUM_FREELISTS 32

해시 엘리먼트

풀 크기

nelem 32,896

= 1,028 * 32

버퍼 디스크립터 배열 버퍼 수 32,767

버퍼 풀 배열 버퍼 수 32,767

Page 71: PostgreSQL 9.6 성능 이야기

52 | PostgreSQL 9.6 성능 이야기

처리 순서

Shared Buffer 내에 존재하는 버퍼를 읽는 순서는 다음과 같다. (그림 2-9. 참조)

1. BufferTag를 생성한다. (‘Tag_B’가 생성된다)

A. 생성된 BufferTag를 이용해서 hashvalue를 계산한다.

B. 생성된 hashvalue를 이용해서 버퍼 파티션 번호를 계산한다.

2. 해당 버퍼 파티션#에 대한 LW 락을 SHARE 모드로 획득한다.

3. hashvalue를 이용해서 해시 테이블 버킷 번호를 계산한다.

A. 버킷 번호를 해시 세그먼트 번호와 인덱스 번호로 치환한다.

B. 해시 체인을 따라가면서 ‘Tag_B’를 찾는다.

4. ‘Tag_B’의 버퍼 ID=1이므로 버퍼 디스크립터 배열 인덱스[1]에 PIN

을 설정한다. (이때 refcount와 usage_count를 각각 1씩 증가시킨

다)

5. 버퍼 파티션#에 대한 LW락을 해제한다.

6. 버퍼 풀 배열 인덱스[1] 번 내용을 읽는다.

7. PIN을 해제한다. (이때 refcount를 1 만큼 감소시킨다)

Page 72: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 53

그림 2-9. Shared Buffer내 블록 읽기 처리 순서

CALC_BUCKET 함수 설명

수행 과정 중에서 해시 테이블 내 버킷 번호를 찾는 부분을 소스를 통해서 자세

히 살펴보자. 해시 버킷을 찾는 함수는 calc_bucket() 이다. (소스 2-12. 참

조)

Page 73: PostgreSQL 9.6 성능 이야기

54 | PostgreSQL 9.6 성능 이야기

소스 2-12. src/backend/utils/hash/dynahash.c

/* Convert a hash value to a bucket number */

calc_bucket(HASHHDR *hctl, uint32 hash_val){

uint32 bucket;

bucket = hash_val & hctl->high_mask;

if (bucket > hctl->max_bucket)

bucket = bucket & hctl->low_mask;

return bucket;

}

cacl_bucket()의 처리 로직은 매우 간단하다. 입력된 hash_val 과 hctl-

>high_mask와 BITAND 연산을 수행해서 bucket 번호를 획득한다. 만일 bucket

값이 hctl->max_bucket보다 크면, bucket 값을 다시 hctl->row_mask와

BITAND 연산을 수행한다. 이때 사용되는 값들은 표 2-3과 같다.

표 2-3. HASHHDR값 설정 내용

항목 Value 계산 공식

hctl->high_mask 131,071 (nbuckets << 1) – 1

hctl->row_mask 65,535 nbuckets -1

그리고 calc_bucket() 함수를 호출한 쪽에서는 다음과 같은 연산을 통해 해

시 세그먼트 번호와 인덱스 번호를 계산한다. (표 2-4. 참조)

HASHHDR *hctl = hashp->hctl;

bucket = calc_bucket(hctl, hashvalue);

segment_num = bucket >> hashp->sshift;

segment_ndx = MOD(bucket, hashp->ssize);

Page 74: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 55

표 2-4. HASHHDR값 설정 내역

항목 Value 계산 공식

hashp->sshift 8 #define DEF_SEGSIZE_SHIFT 8

hashp->ssize 256 #define DEF_SEGSIZE 256

해시 세그먼트 번호를 구하기 위해서 8번 우 시프트를 하는 것은 256으로 나

눈 것과 동일한 의미이다. 해시 세그먼트 내 인덱스는 버킷 값을 256으로 나눈

나머지 값이다. 이를 그림으로 표현하면 다음과 같다. (그림 2-10. 참조)

그림 2-10. 해시 테이블 내의 버킷 번호를 찾는 절차

DISK Read가 발생하는 경우

Shared Buffer 내에 존재하지 않는 블록을 읽기 위해서는 Disk Read를 수행해

야 한다. 즉, Disk Read를 통해 해당 블록을 Shared Buffer로 로딩한 후에 해당

버퍼를 읽는다. 현재 Shared Buffer 상태는 그림 2-8 이라고 가정하자. 이때

BufferTag가 ‘Tag_D’인 블록을 읽는 순서는 다음과 같다.

Page 75: PostgreSQL 9.6 성능 이야기

56 | PostgreSQL 9.6 성능 이야기

처리 순서 (Part -1)

1. BufferTag를 생성한다. (‘Tag_D’가 생성된다)

A. 생성된 BufferTag를 이용해서 hashvalue를 계산한다.

B. 생성된 hashvalue를 이용해서 버퍼 파티션 번호를 계산한다.

2. 해당 버퍼 파티션#에 대한 LW 락을 SHARE 모드로 획득한다.

3. hashvalue를 이용해서 해시 테이블 버킷 번호를 계산한다.

A. 버킷 번호를 해시 세그먼트 번호와 인덱스 번호로 치환한다.

B. 해시 체인을 따라가면서 ‘Tag_D’를 찾지만 검색에 실패한다.

4. 버퍼 파티션#에 대한 LW 락을 해제한다.

여기까지는 Shared Buffer 내에 있는 블록을 읽을 때와 동일하다. 해시 체인 내

에 원하는 블록이 없다는 것만 다를 뿐이다. 지금까지의 처리 순서를 그림으로

표현하면 그림 2-11과 같다.

Page 76: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 57

그림 2-11. DISK Read가 발생할 때 처리 순서 (파트 #1)

처리 순서 (Part -2)

이후의 처리 순서는 다음과 같다. (그림 2-12. 참조)

5. BufferStrategyControl 구조체의 firstFreeBuffer 값을 획득한

다.

A. firstFreeBuffer 값을 변경하기 위해 Spin 락을 획득한다.

B. firstFreeBuffer의 값을 3에서 4로 변경한다.

C. Spin 락을 해제한다.

6. 버퍼 디스크립터 배열 인덱스[3]에 PIN을 설정한다. (이때 refcount

를 1로 설정한다)

Page 77: PostgreSQL 9.6 성능 이야기

58 | PostgreSQL 9.6 성능 이야기

여기까지 진행된 후에, 해시 엘리먼트를 해시 테이블에 연결하는 작업을 수행

한다.

7. 해당 버퍼 파티션#에 대한 LW 락을 EXCLUSIVE 모드로 획득한다.

8. 해시 엘리먼트 풀에서 엘리먼트 1개를 할당받는다.

A. freeList 포인터를 8단계에서 할당한 엘리먼트의 이전 엘리먼트

로 변경한다.

9. 할당받은 해시 엘리먼트를 해시 체인에 연결한 후 레코드를 복사한다.

10. 버퍼 디스크립터 배열 인덱스[3]에 버퍼 헤더 락을 설정한다.

A. usage_count를 1만큼 증가시킨다.

B. 버퍼 헤더 락을 해제한다.

11. 버퍼 파티션#에 설정한 LW 락을 해제한다.

Page 78: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 59

그림 2-12. DISK Read가 발생할 때 처리 순서 (파트 #2)

처리 순서 (Part -3)

이후의 처리 순서는 다음과 같다. (그림 2-13. 참조)

12. BufferIOLockArrary 배열 인덱스[3]에 대한 LW 락을 EXCLUSIVE

모드로 획득한다.

13. 버퍼 디스크립터 배열 인덱스[3]에 버퍼 헤더 락을 설정한다.

A. BM_IO_IN_PROGRESS 플래그 비트를 1로 설정한다.

B. 버퍼 헤더 락을 해제한다.

14. 버퍼 풀 배열 인덱스[3]에 블록을 로딩한다.

Page 79: PostgreSQL 9.6 성능 이야기

60 | PostgreSQL 9.6 성능 이야기

15. 버퍼 디스크립터 배열 인덱스[3]에 버퍼 헤더 락을 설정한다.

A. BM_IO_IN_PROGRESS 플래그 비트를 0으로 설정한다.

B. 버퍼 헤더 락을 해제한다.

16. BufferIOLockArrary 배열 인덱스[3]에 대한 LW락을 해제한다.

17. 버퍼 풀 배열 인덱스[3] 번 내용을 읽는다.

18. PIN을 해제한다. (이때 refcount를 1만큼 감소시킨다)

그림 2-13. DISK Read가 발생할 때 처리 순서 (파트 #3)

Page 80: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 61

Buffer Replacement를 위한 Clock Sweep 알고리즘

앞서 살펴본 내용은 Shared Buffer 내에 empty 버퍼가 존재하는 경우였다. 만

일 empty 버퍼가 없다면 Shared Buffer 내의 버퍼를 디스크에 기록해야 한다.

이때 디스크로 기록되는 버퍼를 Victim 버퍼라고 하고, Victim 버퍼를 선정하

는 알고리즘을 Buffer Replacement 알고리즘이라고 한다.

Shared Buffer의 목적이 DISK Read를 최소화함으로써 성능을 향상하는 것이

므로, 효율적으로 버퍼를 관리할 수 있는 Buffer Replacement 알고리즘을 사용

하는 것은 매우 중요한 일이다.

Clock Sweep 알고리즘 설명

PostgreSQL은 Buffer Replacement를 위해서 Clock Sweep 알고리즘을 사용

한다.

이 알고리즘은 덜 사용된 버퍼를 Victim 버퍼로 선정하는 알고리즘으로써 NFU

(Not Frequently Used) 알고리즘의 일종이다.

덜 사용된 버퍼를 선정하기 위해서는 버퍼마다 액세스된 횟수를 관리할 필요

가 있다. 버퍼 액세스 횟수는 버퍼 디스크립터 내의 state 항목을 비트 연산으

로 처리한다고 설명한 바 있다. 그리고 버퍼를 액세스할 때마다 state 항목 내

의 usage_count를 1만큼 증가시킨다고 설명했었다.

더 정확하게는 BM_MAX_USAGE_COUNT에 정의된 수치까지만 증가하고 그 이

상은 증가하지 않는다. BM_MAX_USAGE_COUNT 기본 설정값은 5이다. 5라는

Page 81: PostgreSQL 9.6 성능 이야기

62 | PostgreSQL 9.6 성능 이야기

값이 매우 적다고 의아해하는 독자들이 있을 것이다. 필자 역시 그런 생각이었

다.

하지만 Clock Sweep 알고리즘을 이해한 후에는 매우 적절한 값이라고 생각할

것이다. 이렇게 작은 값으로 최댓값을 설정한 이유를 간단히 설명하면 ‘공정한

경쟁’을 위함이다. 조금 더 자세한 설명은 Clock Sweep 알고리즘을 설명하면

서 진행할 것이다.

Clock Sweep 알고리즘의 ‘Clock’은 알고리즘 동작 원리를 직관적으로 표현한

것이다. 이 알고리즘은 Victim 버퍼를 찾기 위해 버퍼를 시계 방향으로 탐색한

다. 버퍼 디스크립터는 배열이므로 0번 인덱스와 최대 인덱스를 연결해서 논

리적인 원 형태로 만든다. 즉, 가장 처음에는 배열 0번부터 Victim 버퍼를 찾는

다. 만일 Victim 버퍼를 찾으면 Victim 버퍼를 반환하고 검색을 멈춘다. 그리고

다음 검색 시에는 해당 인덱스 다음부터 검색을 시작한다.

Clock Sweep 알고리즘의 ‘Sweep’은 검색을 수행하면서 청소를 한다는 것을

의미한다. 소스 코드를 보면 usage_count를 1만큼 감소시킨다. 이 작업을 청

소에 비유한 것이다.

즉, Clock Sweep 알고리즘은 이와 같은 작업을 통해 refcount 와

usage_count가 모두 0인 버퍼를 Victim 버퍼로 선정한다. 만일 Victim 버퍼

가 dirty 버퍼 (BM_DIRTY 비트가 1)이면 해당 버퍼 내용을 디스크로 기록한다.

Page 82: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 63

Clock Sweep 알고리즘 수행 절차

앞서 설명한 내용은 매우 간단한 코드로 구성된다. 실제 소스의 일부는 다음과

같다. (설명의 편의를 위해 일부분만 발췌했다)

소스 2-13. src/backend/storage/buffer/freelist.c

for (;;) {

(1) buf = GetBufferDescriptor(ClockSweepTick());

(2) local_buf_state = LockBufHdr(buf);

(3) if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)

(4) if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)

local_buf_state -= BUF_USAGECOUNT_ONE;

(5) else return buf;

(6) UnlockBufHdr(buf, local_buf_state);

}

소스에 대한 설명은 다음과 같다.

1. ClockSweepTick() 함수를 호출해서 Victim 버퍼 검색을 시작할 위치

를 확인한다. 해당 함수는 BufferStrategyControl구조체 내의

nextVictimBuffer 값을 리턴한다.

2. 버퍼 헤더 락을 수행한다.

3. refcount가 0인 경우에는 Step(4)를 수행한다. 그렇지 않으면

Step(6)을 수행한다.

4. usage_count가 0이 아니면 usage_count를 1 만큼 감소시킨다.

5. usage_count가 0이면 버퍼를 리턴한다.

Page 83: PostgreSQL 9.6 성능 이야기

64 | PostgreSQL 9.6 성능 이야기

6. 버퍼 헤더 락을 해제한 후 Step(1)을 반복한다.

이제 독자들은 Clock Sweep 알고리즘 기본 원리를 이해했을 것이다. Clock

Sweep 알고리즘 처리 절차를 그림으로 표현하면 그림 2-14와 같다.

그림 2-14. Clock Sweep 처리 절차

Step 별 설명은 다음과 같다.

Page 84: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 65

Step-1

nextVictimBuffer 인 buffer_id = 5 는 Pinned 버퍼는 아니다. 하지만

usage_count가 2이므로 usage_count를 1로 변경한 후 다음 버퍼로 이동

한다.

Step-2

nextVictimBuffer 인 buffer_id = 6 은 Pinned 버퍼이다. 따라서 다음 버퍼로

이동한다.

Step-3

nextVictimBuffer 인 buffer_id =7은 unpinned 버퍼이고 usage_count도 0

이다. 따라서 Victim 버퍼로 선정된다.

공정한 경쟁이란?

앞서 BM_MAX_USAGE_COUNT 기본 설정값을 다소 작게 설정한 이유는 ‘공정한

경쟁’ 때문이라고 언급했었다. Clock Sweep 알고리즘을 이해했고, ORACLE

의 터치 카운트 알고리즘에 익숙한 독자라면 ‘공정한 경쟁’의 의미를 어느 정

도 파악했을 것이다.

‘공정한 경쟁’을 이해하기 위해서는 ‘불공정한 경쟁’이 언제 발생하는지를 생

각해보면 된다. 예를 들어 매우 짧은 순간에 특정 버퍼를 수십만 번 액세스했

고, 그 이후로는 액세스가 없다고 가정해보자. 이 경우, 해당 버퍼를 Shared

Buffer에 오랫동안 상주시키는 것이 공정한가? 물론 아닐 것이다. 따라서 액세

Page 85: PostgreSQL 9.6 성능 이야기

66 | PostgreSQL 9.6 성능 이야기

스 횟수를 이용한 Buffer Replacement 알고리즘은 불공정한 경쟁을 제거하기

위한 로직을 포함해야 한다. 예를 들어, ORACLE은 N 초 이내에 동일 버퍼에

발생한 액세스는 touch_count를 증가하지 않는다. 또한, HOT 영역으로 버

퍼들이 이동하면 touch_count를 0으로 초기화한다. 이렇게 함으로써 해당

버퍼들을 공정하게 경쟁시킨다.

그런데 PostgreSQL은 이러한 처리 로직 대신, BM_MAX_USAGE_COUNT를 5라

는 다소 작은 값을 설정함으로써 ORACLE과 같은 효과를 내는 것이다. 다시

말해, N 초 이내에 동일 버퍼에 대한 액세스가 수십만 번 발생하더라도

usage_count는 5이다. 따라서 특정 시점에 아무리 액세스를 많이 해도 Clock

Sweep이 6차례 회전하기 전에 다시 액세스되지 않는다면, 해당 버퍼는 Victim

버퍼로 선정된다. (그림 2-15. 참조)

그림 2-15. 공정한 경쟁

Page 86: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 67

또 다른 관점으로 살펴보면, Clock Sweep이 수행될 때마다 usage_count를

1씩 감소시키는 것 역시 공정한 경쟁을 위해서이다. 정기적으로 자주 액세스

된다면 해당 버퍼의 usage_count는 항상 1보다 큰 값을 유지하기 때문이다.

Bulk IO 처리를 위한 IO 전략과 Ring Buffer

PostgreSQL은 Clock Sweep 알고리즘을 이용해서 Shared Buffer의 효율성을

높인다는 사실을 살펴보았다. 그렇다면 Seq Scan으로 인해 Shared Buffer가

훼손될 수 있는 문제점은 어떻게 해결할까? 여기서 ‘훼손’이라는 것은 Seq Scan

으로 인해 큰 테이블의 모든 블록이 Shared Buffer로 로딩되는 것을 의미한다.

PostgreSQL은 이 문제를 해결하기 위해 IO 전략과 Ring Buffer를 사용한다.

IO 전략이란?

IO 전략은 IO 유형에 따라 다른 전략 (방식)으로 처리하는 것을 의미한다.

PostgreSQL은 IO 유형을 4개로 구분한다. (소스 2-14. 참조)

소스 2-14. src/include/storage/bufmgr.h

typedef enum BufferAccessStrategyType {

BAS_NORMAL, /* Normal random access */

BAS_BULKREAD, /* Large read-only scan */

BAS_BULKWRITE, /* Large multi-block write (e.g. COPY IN) */

BAS_VACUUM /* VACUUM */

} BufferAccessStrategyType;

Page 87: PostgreSQL 9.6 성능 이야기

68 | PostgreSQL 9.6 성능 이야기

즉, IO 요청을 NORMAL (랜덤 액세스 용), BULK READ (Large Seq Scan 용),

BULK WRITE (대량 Write 용), VACUUM 으로 구분하는 것이다. 이때

NORMAL 요청을 제외한 모든 요청은 Ring Buffer를 이용한다.

Ring Buffer 살펴보기

Ring Buffer란 논리적으로 원형 형태의 배열을 의미한다. 즉, 일정 크기의 배

열을 순환 방식으로 사용하는 것이며, 이를 통해 Shared Buffer를 Seq Scan의

위험성으로부터 보호하는 역할을 한다. 물론 Ring Buffer 역시 Shared Buffer

내에 존재한다.

Ring Buffer의 크기

Ring Buffer의 크기는 IO 유형에 따라 약간씩 다르다. (소스 2-15. 참조)

소스 2-15. src/backend/storage/buffer/freelist.c

switch (btype) {

case BAS_NORMAL:

return NULL;

case BAS_BULKREAD:

ring_size = 256 * 1024 / BLCKSZ;

break;

case BAS_BULKWRITE:

ring_size = 16 * 1024 * 1024 / BLCKSZ;

break;

case BAS_VACUUM:

ring_size = 256 * 1024 / BLCKSZ;

break;

Page 88: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 69

BULK READ를 위한 Ring Buffer 크기는 256 KiB (32 블록)이다.

BULK READ 정의

그렇다면 BULK READ의 정의는 무엇일까? 모든 Seq Scan이 BULK READ

인가?

그렇지 않다. PostgreSQL은 Shared Buffer 크기의 1/4 이상인 테이블에 대한

Seq Scan 시에만 BULK READ 방식을 사용한다. 이때 테이블 크기는 통계 정

보를 이용한다.

BULK READ시 Ring Buffer 동작 원리

BULK READ를 위한 Ring Buffer 크기는 256 KiB (32블록)이다. 그런데 조금

만 더 생각해보면 몇 가지 의문이 든다. 아래 질문에 답해보자.

질문-1

매우 빈번하게 BULK READ를 수행하는 테이블이 있다.

해당 테이블을 반복적으로 100회 읽으면, 해당 테이블에 대한 Ring Buffer 크

기는 32블록일까? 3,200 블록일까?

질문-2

UNION ALL로 동일 테이블을 2회 읽으면, Ring Buffer 크기는 32블록일까?

아니면 64 블록일까?

정답은 다음과 같다.

Page 89: PostgreSQL 9.6 성능 이야기

70 | PostgreSQL 9.6 성능 이야기

1번) 3,200 블록

SQL 수행 시마다 Ring Buffer를 32블록씩 더 할당하는 이유는 버퍼 사용의 공정성

때문이다. 큰 테이블에 대한 Seq Scan의 위험성을 방지하기 위해 Ring Buffer 를 도

입했지만, 여러 SQL에서 해당 테이블을 자주 액세스한다면 Ring Buffer를 32블록

만 할당하는 것은 형평성에 어긋나기 때문이다.

2번) 64 블록

1개 SQL 내에서 BULK READ 대상 테이블이 출현할 때마다 Ring Buffer를 32블

록씩 할당한다.

위의 내용을 테스트를 통해 확인해보자.

테스트를 통한 Ring Buffer 동작 원리 확인

1. 테스트를 위해 pg_buffercache 익스텐션을 설치한다.

create extension pg_buffercache;

2. 현재 Shared Buffer가 256 MiB이므로 256 MiB의 1/4보다 큰 테이블을 생

성한다.

drop table b1;

create table b1 (c1 char(1000), c2 char(1000));

insert into b1 select i, i from generate_series(1,35000) a(i);

analyze b1;

select relname, relpages, relpages*8192/1024/1024 as size

from pg_class where relname='b1';

Page 90: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 71

relname | relpages | size

---------+----------+------

b1 | 8750 | 68

3. 테스트를 위해 DB를 재기동한다.

4. B1 테이블을 Seq Scan한다.

select count(*) from b1;

5. 체크용 스크립트 수행 결과 32블록이 Shared Buffer에 로딩된 것을 알 수 있

다.

Shared Buffer 체크용 스크립트 수행 결과 (스크립트 내용은 아래 참조)

buffers

---------

32

6. 한 번 더 수행한다.

select count(*) from b1;

7. 체크용 스크립트 수행 결과 64블록이 Shared Buffer에 로딩된 것을 알 수 있

다.

체크용 스크립트 수행 결과

buffers

---------

64

몇 차례 반복적인 테스트를 수행하면 ‘수행 횟수 * 32 블록’만큼 Shared Buffer

에 로딩되는 것을 알 수 있다.

Page 91: PostgreSQL 9.6 성능 이야기

72 | PostgreSQL 9.6 성능 이야기

8. 테스트를 위해 DB를 재기동한다.

9. UNION ALL 문장을 수행한다.

select count(*) from b1

union all

select count(*) from b1;

10. 체크용 스크립트 수행 결과 64블록이 Shared Buffer에 로딩된 것을 알 수

있다.

체크용 쿼리 수행 결과

buffers

---------

64

11. 한 번 더 수행한다.

select count(*) from b1

union all

select count(*) from b1;

12. 수행 결과 128블록이 Shared Buffer에 로딩된 것을 알 수 있다.

체크용 스크립트 수행 결과

buffers

---------

128

참고. Shared Buffer 체크용 쿼리

select count(*) as buffers

from pg_buffercache a, pg_class b

where a.relfilenode = pg_relation_filenode(b.oid)

Page 92: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 73

and a.reldatabase in (0,(select oid

from pg_database

where datname=current_database()))

and b.relname='b1';

Ring Buffer의 위험성

지금까지 설명을 통해, PostgreSQL은 Ring Buffer를 이용해서 대량 BULK

READ로 인한 Shared Buffer의 훼손을 방지함과 동시에, BULK READ를 반

복적으로 수행하는 경우에는 Ring Buffer 크기를 증가함으로써 테이블 액세스

효율을 높인다는 사실을 확인했다.

하지만 아직도 위험 요소가 존재한다.

예를 들어, 데이터가 갑자기 증가해서 테이블 크기가 Shared Buffer의 1/4보다

커졌다고 가정해보자. 이 경우, DB 재기동 이후 (또는 해당 테이블에 대한 버

퍼가 Shared Buffer에서 밀려난 이후)부터는 해당 테이블에 대한 Seq Scan은

BULK READ 방식으로 수행된다. 즉, Ring Buffer를 사용하게 된다. 따라서

처리 속도가 느려질 수 있다. 테스트를 통해 확인해보자.

1. Shared Buffer크기의 1/4보다 작은 테이블을 생성한다.

drop table s1;

create table s1 (c1 char(1000), c2 char(1000));

insert into s1 select i, i from generate_series(1,32000) a(i);

analyze s1;

select relname, relpages, relpages*8192/1024/1024 "SIZE"

Page 93: PostgreSQL 9.6 성능 이야기

74 | PostgreSQL 9.6 성능 이야기

from pg_class where relname='s1';

relname | relpages | SIZE

---------+----------+------

s1 | 8000 | 62

2. 테스트를 위해 DB를 재기동한다.

3. 테스트를 위해, 반복적으로 Seq Scan을 수행하는 function을 생성한다.

CREATE or replace FUNCTION

loop_fullscan(v_begin integer, v_end integer)

RETURNS VOID AS $$

DECLARE

rval integer;

BEGIN

FOR i in v_begin..v_end LOOP

SELECT COUNT(*) FROM s1 INTO rval;

END LOOP;

END;

$$ LANGUAGE plpgsql;

4. 테이블을 100회 Seq Scan 한 성능을 측정한다.

select loop_fullscan(1,100);

loop_fullscan

---------------

(1 row)

Time: 605.653 ms

5. 추가로 1,000건을 입력해서 테이블 크기를 Shared Buffer 크기의 1/4로 만

든다.

insert into s1 select * from c1 limit 1000;

Page 94: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 75

analyze s1;

select relname, relpages, relpages*8192/1024/1024 "SIZE"

from pg_class where relname='s1';

relname | relpages | SIZE

---------+----------+------

c1 | 8250 | 64

5. 테스트를 위해 DB를 재기동한다.

6. 해당 테이블을 100회 Seq Scan 한 성능을 측정한다.

select loop_fullscan(1,100);

loop_fullscan

---------------

(1 row)

Time: 1689.020 ms

테스트 결과를 보면, 단지 1,000건을 입력했을 뿐인데도 Ring Buffer를 사용

함으로써 성능이 저하됐다는 것을 알 수 있다.

물론 테이블 크기와 테스트 환경에 따라 정도의 차이는 있을 것이다. 하지만

Ring Buffer를 사용하면 기존보다 수행 속도가 느려진다는 것은 분명한 사실

이다.

그렇다면 이러한 문제는 어떻게 해결할 수 있을까?

다행히도 PostgreSQL은 pg_prewarm 익스텐션을 제공함으로써 이 문제를 해

결할 수 있는 방법을 제공한다.

Page 95: PostgreSQL 9.6 성능 이야기

76 | PostgreSQL 9.6 성능 이야기

pg_prewarm 익스텐션

pg_prewarm 익스텐션은 테이블과 인덱스를 Shared Buffer로 로딩하는 기능

을 제공한다. (Shared Buffer 크기의 1/4보다 큰 테이블도 로딩할 수 있다) 단,

영구적으로 Shared Buffer 내에 버퍼를 상주시키는 기능은 아니다. 다시 말해,

pg_prewam()으로 Shared Buffer에 로딩된 버퍼들도 Victim 버퍼 대상이다.

pg_prewarm()을 이용해서 이전 테스트를 다시 수행해보자.

1. pg_prewarm 익스텐션을 생성한다.

create extension pg_prewarm;

2. 테스트를 위해 DB를 재기동한다.

3. pg_prewam을 수행한다.

select pg_prewarm('s1');

pg_prewarm

------------

8250

Time: 51.789 ms

4. 해당 테이블을 100회 Seq Scan한 성능을 측정한다.

select loop_fullscan(1,100);

loop_fullscan

---------------

Time: 545.811 ms

Page 96: PostgreSQL 9.6 성능 이야기

2장. Shared Buffer 동작 원리 | 77

테스트 결과를 보면, pg_prewarm() 수행으로 인해 쿼리 성능이 향상된다는

것을 알 수 있다. 따라서 배치 프로그램 성능 개선 작업 시에 pg_prewarm()을

적절히 이용하면 좋을 것이다.

요약

이번 장에서 배운 내용을 간단히 정리해보자.

가장 먼저 Shared Buffer 구조를 배웠다. Shared Buffer는 해시 테이블, 해시 엘

리먼트, 버퍼 디스크립터, 버퍼 풀로 구성된다.

그리고 메모리 Read 및 DISK Read 수행 순서를 배웠다. 눈을 감고 Shared

Buffer 구조를 그린 후에 IO 처리 흐름을 그려낼 수 있기를 바란다.

또한, 효율적인 Shared Buffer 관리를 위한 Clock Sweep 알고리즘을 배웠다.

알고리즘의 이해를 통해서 BM_MAX_USAGE_COUNT 값을 매우 크게 설정하지 않

는 이유와 공정한 경쟁의 개념을 생각해볼 수 있었다.

마지막으로 BULK READ로부터 Shared Buffer를 보호할 수 있는 IO 전략과

Ring Buffer에 대해서 배웠다. 또한, Ring Buffer 동작 원리와 단점, 이를 극복

하기 위한 pg_prewarm() 익스텐션을 학습했다.