Shared Buffer
구성요소
Shared Buffer
를 구성하는 요소
- 해시 테이블
- 해시 테이블에 딸린 해시 엘리먼트 (및 엘리먼트 키)
- 버퍼 상태를 관리하는 버퍼 디스크립터
- 실제 블록을 저장하는 버퍼 풀
1. 해시테이블
![[IMG_0388.jpg | 500]]
- 해시 테이블은 메모리 내의 버퍼를 관리 (검색, 입력) 할 떄 매우 효과적인 자료 구조이다.
- 해시 테이블은 해시 충돌 (
Hash Collision
) 이 발생하면 성능이 떨어지는 문제점이 있다. - 해시 충돌 문제를 완화하기 위한 방법의 하나는 해시 테이블을 논리적인
N
개의 세그먼트로 나누어서 관리하는 것이다. - 이것을
PostgreSQL
에서는Segmented
해시 테이블이라고 한다. - 해시 세그먼트는
256
개의 버킷으로 구성된다. - 디렉토리는 나누어진 해시 세그먼트의 시작 주소를 저장하는 배열이다. 디렉토리의 기본 설정값은
256
이며,Shared Buffer
를 크게 설정한 경우에는 디렉토리 크기가 증가한다.
버퍼 파티션이란?
![[IMG_0389.jpg | 500]]
- 위에서 언급한 디렉토리, 해시 세그먼트, 해시 테이블은 모두
Shared Buffer
내에 존재하는 공유 리소스이다. - 공유 리소스는
LW (Light Weight)
락을 이용하여 보호한다. 즉 백엔드 프로세스가 공유 메모리를 액세스 하기 위해서는LW
락을 획득해야 한다. LW
락은 공유 리소스를 보호한다는 긍정적인 측면이 있지만,LW
락 경합 때문에 성능 저하가 발생할 수 있다는 문제점이 있다.- 예를 들어서 해시 테이블을 관리하는
LW
락이 하나라면, 해시 테이블에 액세스하는 대다수의 프로세는LW
락을 대기하게 된다. PostgreSQL
에서는 해당 문제는 해결하기 위해서, 해시 테이블을N
개의 ‘버퍼 파티션’ 으로 나누고 버퍼 파티션마다1
개의LW
락을 할당하는 방식을 사용한다.- 이전에는 버퍼 파티션의 개수는 16개 밖에 되지 않았으며, 이로 인해서 동시성이 높은 환경에서는 버퍼 파티션 액세스 시에
LW
락 경합이 발생한 가능성이 컸고, 실제로 이로 인한 성능 문제가 발생하기도 하였다. 이러한 문제를 해결하기 위해서 버퍼 파티션의 개수가 늘어났다.
LW 락은
Light Weight
의 줄인말인데, 매우 가벼운 락을 의미하여 메모리에 액세스 할 때 사용하는 방식이다. LW 락은 테이블 내의 데이터를 보호하는 락과는 다른 개념이며, 참고로 오라클은 LW 락을Latch
라고 표현한다.
2. 해시 엘리먼트
- 해시 엘리먼트는 엘리먼트와 엘리먼트 키로 구성된다.
/*
* HASHELEMENT is the private part of a hashtable entry. The caller's data
* follows the HASHELEMENT structure (on a MAXALIGN'd boundary). The hash key
* is expected to be at the start of the caller's hash entry data structure.
*/
typedef struct HASHELEMENT
{
struct HASHELEMENT *link; /* link to next entry in same bucket */
uint32 hashvalue; /* hash function result for this entry */
} HASHELEMENT;
- 엘리먼트는 다음 엘리먼트를 가리키는 엘리먼트 포인터와 해시 값으로 구성된다.
엘리먼트 Key
구성 요소
- 엘리먼트
KEY
는 다음과 같이 구성된다.
- 엘리먼트 `KEY` 는 `BufferTag` 구조체와 버퍼 디스크립터 배열 인덱스로 구성된다.
- `BufferTag` 구조체는 `spcOid`, `dbOid`, `RelNumber`, `forkNum`, `blockNum` 으로 구성된다.
- `RelFileNode` 구조체는 테이블 스페이스 번호, 데이터베이스 번호, 오브젝트 번호로 구성된다.
Buffer Tag 란?
/* entry for buffer lookup hashtable */
typedef struct
{
BufferTag key; /* Tag of a disk page */
int id; /* Associated buffer ID */
} BufferLookupEnt;
/*
* Buffer tag identifies which disk block the buffer contains.
*
* Note: the BufferTag data must be sufficient to determine where to write the
* block, without reference to pg_class or pg_tablespace entries. It's
* possible that the backend flushing the buffer doesn't even believe the
* relation is visible yet (its xact may have started before the xact that
* created the rel). The storage manager must be able to cope anyway.
*
* Note: if there's any pad bytes in the struct, InitBufferTag will have
* to be fixed to zero them, since this struct is used as a hash key.
*/
typedef struct buftag
{
Oid spcOid; /* tablespace oid */
Oid dbOid; /* database oid */
RelFileNumber relNumber; /* relation file number */
ForkNumber forkNum; /* fork number */
BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;
Buffer Tag
는 블록의 주민 등록번호와 같은 개념이다. 즉, 클러스터 데이터베이스 내에서 각 블록을 유일하게 식별할 수 있는 데이터로 구성된다.Buffer Tag
는pg_class
또는pg_tablespace
항목을 참조하지 않고 블록을 쓸 위치를 결정하기 충분해야합니다.- 테이블 스페이스, 데이터베이스 와 비롯한 여러가지 정보를 가지고 있는데 그래야만 클러스터 데이터베이스 내에서 유일한 오브젝트를 획득할 수 있기 때문이다.
typedef enum ForkNumber
{
InvalidForkNumber = -1,
MAIN_FORKNUM = 0,
FSM_FORKNUM,
VISIBILITYMAP_FORKNUM,
INIT_FORKNUM
/*
* NOTE: if you add a new fork, change MAX_FORKNUM and possibly
* FORKNAMECHARS below, and update the forkNames array in
* src/common/relpath.c
*/
} ForkNumber;
ForkNumber
는 오브젝트 유형을 의미한다. 0은 테이블 또는 인덱스, 1은FSM
, 2는VM
이다.
3. 버퍼 디스크립터
- 버퍼 디스크립터는 버퍼 메타데이터를 관리하기 위한 구조체이다.
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_pgprocno; /* backend of pin-count waiter */
int freeNext; /* link in freelist chain */
LWLock content_lock; /* to lock access to buffer contents */
} BufferDesc;
tag
:BufferTag
를 저장한다.buf_id
: 실제 버퍼가 저장된 ‘버퍼 풀’ 배열 내의 인덱스 번호이다.state
: 이전에는 버퍼를 액세스 하기 위해서 프로세스 수, 버퍼 액세스 횟수, 버퍼 상태 플래그를 제공했었지만, 이 3가지 항목을state
값으로 제공하고 비트 마스크와 비스 시프트 연산을 통해서 추출하도록 변경하였다.freeNext
: 다음번empty
버퍼를 가리키는 포인터이다.
SPIN
락과 LW
락
Shared Buffer
를 액세스 할 때Spin
락과LW
락을 획득해야한다.Spin
락과LW
락을 구분하는 이유는 성능 향상을 위해서이다.LW
락은 매우 가벼운 락이고,Spin
락은 이보다 더 가벼운 락이다.- 따라서 가능하다면
Spin
락을 사용하는 것이 성능상 유리하다.
항목 | Spin 락 |
LW 락 |
---|---|---|
사용부하 | 매우 매우 적음 | 매우 적음 |
콘텍스트 스위칭 | 발생하지 않음 | 발생할 수 있음 |
동작 방식 | Spin 방식 |
큐 & 포스팅 방식 |
사용 용도 | 구조체 내 변수를 액세스 할 대 사용 | 구조체를 액세스 할 대 사용 |
사용모드 | EXCLUSIVE |
SHARE & EXCLUSIVE |
Spin
락
- 구조체 변수를 변경한다고 가정하면 구조체 변수도 공유 메모리 안에 있기 때문에 해당 값을 읽거나 변경하기 위해서는 락이 필요하다 이때
Spin
락을 사용한다. Spin
락을 사용하는 이융는 변수 값을 변경하는 것과 같은 오퍼레이션은 매우 짧은 시간에 수행될 수 있기 때문이다.- 다른 프로세스가 이미
Spin
락을 획득한 상태라고 할지라도, 몇 차례Spin(루프)
를 수행한 후에는Spin
락을 획득할 가능성이 매우 크다 Spin
을 수행하면,Sleep
상태로 빠지지 않기 때문에,Context Switching
이 발생하지 않는다.
Spin
락 구현방식
- `mutex` 를 이용하는 방식
- TAS (`Test` & `Set`)를 인라인 어셈블리어로 구현하는 방식
PostgreSQL
은 두 번재 방식을 사용한다.
src/backend/storage/lmgr.s_lock.c
/*
* s_lock(lock) - platform-independent portion of waiting for a spinlock.
*/
int
s_lock(volatile slock_t *lock, const char *file, int line, const char *func)
{
SpinDelayStatus delayStatus;
init_spin_delay(&delayStatus, file, line, func);
while (TAS_SPIN(lock))
{
perform_spin_delay(&delayStatus);
}
finish_spin_delay(&delayStatus);
return delayStatus.delays;
}
LW
락
- PostgreSQL 은
Queue & Posting
방식을 사용하여,LW
락을 획득한다. - 해시 버킷에 딸린 해시 엘리먼트를 검색한다고 가정을 했을 때, 이때는 읽기 모드이므로
LW
락을SHARED
모드로 획득해야 한다. - 해시 엘리먼트 내에
BufferTag
정보를 입력한다고 가정을 했을 떄 이때는 쓰기 모드이므로,LW
락을EXCLUSIVE
모드로 획득해야 한다.
LW
락 획득 절차
src/backend/storage/lmgr/lwlock.c
/*
* LWLockAcquire - acquire a lightweight lock in the specified mode
*
* If the lock is not available, sleep until it is. Returns true if the lock
* was available immediately, false if we had to sleep.
*
* Side effect: cancel/die interrupts are held off until lock release.
*/
LWLockAcquire(LWLock *lock, LWLockMode mode)
LOOP
LWLockAttemptLock() 함수를 호출해서 LW 락 획득을 시도한다.
락을 획득하면 LOOP를 탈출한다.
만일 락을 획득하지 못하면 대기 큐에 등록한다.
한 번 더 LwLockAttemptLock() 함수를 호출한다.
만일 락을 획득하면 대기 큐에 등록된 항목을 삭제한 후 LOOP를 탈출한다.
만일 락을 획득하지 ㅁ소하면 대기 상태를 시작한다.
(다른 프로세스가 깨워줄 때까지 대기 상태가 유지 된다.)
END
정리
![[IMG_0389.jpg | 500]]
Shared Buffer
는 크게 해시 테이블, 해시 엘리먼트, 버퍼 디스크립터와 버퍼 풀로 구성된다.- 해시 테이블은 배열 구조이며, 해시 충돌을 최소화 하기 위해
Segmented
해시 테이블 구조를 사용한다. - 동시성을 높이기 위해 논리적인 파티션으로 나눠서 관리한다. 이때 파티션 당
LW
락은 1개이다. - 해시 엘리먼트는 엘리먼트와 엘리먼트 키로 구성된다.
- 엘리먼트는
BufferTag
에 대한hashvalue
와 다음번 엘리먼트를 가리키는 포인터로 구성된다. - 엘리먼트 키는
BufferTag
와 버퍼id
를 저장한다. BufferTag
란 블록에 대한 주민등록번호와 같다.- 해시 엘리먼트는 32개의
freeList
로 관리된다. 이는 경합 해소를 위함이다. - 버퍼 디스크립터는 버퍼의 메타정보를 관리하는 구조체 배열이다. 이때 배열 수는 버퍼의 수와 동일하다.
- 버퍼 디스크립터에서 관리하는 주요 메타 정보는
refcount
,usage_count
,flags
,freeNext
등이다.
출처
PostgreSQL 9.6 성능 이야기(https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=107542125)
>> Home