본문 바로가기
엑셈 경쟁력/DB 인사이드

DB 인사이드 | PWI - Shared Buffer > 동작원리

by exemtech 2024. 10. 25.

 

 

지금까지 PostgreSQL의 Lock에 대해서 알아보았습니다.

이어서 PostgreSQL을 구성하는 각각의 내부 아키텍처와 주요 메커니즘을 우선 이해하고, 그 과정에서 나타나는 Wait Event의 발생 원인과 해결 방안에 대해 살펴보겠습니다.

 

첫 번째 주제로 PostgreSQL의 Shared Buffer에 대해서 알아보고 그와 관련된 Wait Event는 어떤 것이 있는지 알아보겠습니다.

 

Shared Buffer

Shared Buffer는 모든 프로세스가 공유해서 사용하는 Shared Memory 내부에 위치하고 있으며, 데이터를 페이지 단위로 캐싱하여 I/O를 빠르게 처리하기 위한 목적으로 사용됩니다. 또한 Oracle의 Buffer Cache와 매우 유사하며 shared_buffers 파라미터를 통해 사이즈를 설정할 수 있습니다.

Shared Buffer 내 데이터에 Access 하기 위해서는 Access 하는 시점을 기준으로 데이터가 Shared Buffer에 존재하는지 여부에 따라 다음과 같이 두 가지로 분류할 수 있습니다.

Shared Buffer 내부에 존재할 때
Shared Buffer 내부에 존재하지 않을 때

 

본 문서에서는 Shared Buffer 내 데이터에 접근하는 과정에 대한 전체적인 흐름을 먼저 설명 한 후, 각각의 상황에 맞춰 세부적인 처리 과정까지 알아보도록 하겠습니다.

 

Shared Buffer Access

Backend Process가 원하는 데이터를 찾기 위하여 Shared Buffer에 Access 하는 전체 과정을 간략히 표현하면 다음과 같습니다.

  1. buffer_tag 생성 및 전달
    Backend Process는 원하는 데이터에 대한 정보(Tablespace, Database, Object 등)를 기반으로 buffer_tag를 생성, 이를 포함한 요청을 Shared Buffer 내 Buffer Manager에 전달합니다.
  2. Buffer 탐색
    Buffer Manager에서는 Backend Process로부터 전달받은 정보에 대응하는 데이터가 있는지 탐색합니다.
  3. buffer_id 위치 반환
    대응하는 데이터가 있다면 Shared Buffer 내부에 원하는 데이터가 존재하는 것을 의미하지만, 반면 대응하는 데이터가 없을 수도 있습니다. 다음과 같이 상황에 따라 동작 과정에 차이가 있습니다. 
    • Shared Buffer 내부에 데이터가 존재할 때 - 원하는 데이터의 위치 정보를 포함한 buffer_id를 Backend Process에게 바로 반환할 수 있습니다.(3-a)
    • Shared Buffer 내부에 데이터가 존재하지 않을 때 - 반면, 이 경우에는 Shared Buffer에 없는 데이터를 먼저 디스크로부터 Shared Buffer로 가져와야 합니다.(3-b) 그러고 나서 가져온 데이터의 위치 정보를 포함하여 buffer_id를 Backend Process에게 반환할 수 있습니다.(3-a)
  4. 데이터 Access
    이전 단계에서 확인한 buffer_id를 활용하여 Backend Process는 원하는 데이터에 Access 할 수 있습니다.

 

Buffer Manager

Shared Buffer에 Access하는 일련의 처리 과정에 대한 이해를 완료했다면, 각 과정 중 나타난 주요 요소의 역할 및 동작 방식까지 이해할 필요가 있습니다.

이 중 Buffer Manager는 Backend Process가 Shared Buffer에 저장된 데이터에 Access 할 때 이를 효율적으로 관리하고 제어하는 메커니즘으로, Shared Memory와 Disk Storage 간의 데이터 전송을 조율합니다.

Buffer Manager 관리 요소

Buffer Manager는 Hash Table, Buffer Descriptor, Buffer Pool을 포함하여 Shared Buffer 내부 구성 요소를 관리합니다.

Shared Buffer의 데이터에 Access 하는 과정에서 각 요소는 어떤 역할을 하는지 자세히 알아보겠습니다.

Hash Table(Buffer Table)

Shared Buffer 내에 저장된 Data Page의 buffer_tagbuffer_id를 매핑하기 위한 정보를 저장하고 있으며, 배열 구조로 이루어져 있습니다. 프로세스가 데이터에 Access 하거나 새로운 데이터를 저장해야 하는 경우, Shared Buffer 내부에 대상이 되는 위치를 특정해 주는 역할을 합니다.

Hash Table의 논리적 요소는 Hash Function, Hash Bucket, Data Entry 세 가지가 있습니다.

  • Hash Function - 프로세스가 원하는 Hash Bucket의 위치를 찾기 위해 연산하는 함수입니다.
  • Hash Bucket - Hash Table를 구성하는 요소이며, N개의 Hash Bucket이 모여 하나의 파티션을 이루고 이러한 여러 개의 파티션이 전체 Hash Table을 구성합니다. 또한 Hash Bucket 마다 매핑되는 Data Entry가 연결되어 있습니다.
  • Data Entry - Data Page의 buffer_tag와 Buffer Descriptor의 buffer_id로 구성되며 연결 리스트에 저장됩니다.

Buffer Descriptor

Buffer Pool에 저장된 Data Page에 대한 메타데이터를 저장하고 있는 배열입니다. Buffer Descriptor와 Buffer Pool은 1:1로 대응됩니다.

  • tag : Buffer Pool에 저장된 Data Page의 buffer_tag
  • buf_id : Buffer의 인덱스(buffer_id), Buffer Descriptor와 1:1 대응하는 Buffer Pool 식별에 사용
  • state : Buffer Pool에 저장되어 있는 Data Page에 대한 프로세스에 의해 사용된 이력 정보를 포함하여 상태와 같은 메시지를 표현
    state 항목은 flags, usage_count, refcount 세 가지가 32bit로 결합되어 있으며, CPU 원자 연산을 사용합니다.
    • flags - 다음과 같이 10가지의 형태로 Data Page의 상태를 표현
      더보기
      /*
      * Flags for buffer descriptors
      *
      * Note: BM_TAG_VALID essentially means that there is a buffer hashtable
      * entry associated with the buffer's tag.
      */
      #define BM_LOCKED                (1U << 22)    /* buffer header is locked */
      #define BM_DIRTY                (1U << 23)    /* data needs writing */
      #define BM_VALID                (1U << 24)    /* data is valid */
      #define BM_TAG_VALID            (1U << 25)    /* tag is assigned */
      #define BM_IO_IN_PROGRESS        (1U << 26)    /* read or write in progress */
      #define BM_IO_ERROR                (1U << 27)    /* previous I/O failed */
      #define BM_JUST_DIRTIED            (1U << 28)    /* dirtied since write started */
      #define BM_PIN_COUNT_WAITER        (1U << 29)    /* have waiter for sole pin */
      #define BM_CHECKPOINT_NEEDED    (1U << 30)    /* must write for checkpoint */
      #define BM_PERMANENT            (1U << 31)    /* permanent buffer (not unlogged,
                                                 * or init fork) */
      flags 설명
      LOCKED Buffer Descriptor의 상태 확인 혹은 변경이 진행 중인 상태
      DIRTY Buffer에 저장된 Data Page가 Dirty 상태로서, Disk로 Flush가 필요함
      VALID Buffer에 저장된 Data Page가 Read 또는 Write가 가능한 상태
      TAG_VALID Buffer에 저장된 Data Page에 buffer_tag가 할당된 상태
      IO_IN_PROGRESS Buffer에 저장된 Data Page에 대해 Read 또는 Write가 진행 중인 상태
      IO_ERROR Buffer에 저장된 Data Page에 대해 이전에 수행된 I/O가 실패한 상태
      JUST_DIRTIED Buffer에 저장된 Data Page에 대해 Disk Flush가 시작 혹은 완료된 이후에 변경 사항이 생긴 상태
      PIN_COUNT_WAITER sole pin에 대한 waiter가 있는 상태
      (sole pin : Buffer에 대해 단독으로 Buffer Pin 설정이 필요한 프로세스에 의해 설정된 Buffer Pin)
      CHECKPOINT_NEEDED Buffer에 저장된 Data Page가 Checkpoint에 의해 Disk Flush 되어야 하는 상태
      PERMANENT Buffer에 저장된 Data Page는 영구적으로 Buffer에 머무름(not unlogged, or init fork)
    • usage_count - 프로세스에 의해 사용된 횟수, 최대치(Default: BM_MAX_USAGE_COUNT=5)를 설정하여 제어 가능함
    • refcount - Buffer Descriptor에 대응하는 Buffer Pool을 현재 사용하고 있는 프로세스의 수, 다른 말로 pin count라고도 함 (사용하기 시작할 때 1만큼 증가, 사용 종료 시 1만큼 감소함)
  • wait_backend_pgprocno : Buffer Pin 설정을 대기하고 있는 Backend Process
  • freeNext : 다음 FreeList Entry를 가리키는 포인터
  • content_lock : Buffer Content에 대한 Access를 제한하기 위해 사용하는 LWLock

Buffer Pool

Buffer Descriptor와 1:1로 대응되는 Buffer Pool은, 실제로 프로세스가 요청한 데이터(테이블, 인덱스 등)를 저장하고 있는 배열입니다. 이때, Buffer Pool 배열의 인덱스는 buffer_id와 동일합니다.

 

Buffer Manager 동작 과정

관리 요소에 이어, 이번에는 Data Page에 대한 Access 요청과 관련한 Buffer Manager의 동작 과정에 대해 알아보도록 하겠습니다.

이 과정에서 Backend Process가 요청한 Data Page가 Shared Buffer 내부에 존재해서 바로 Access 할 수도 있지만, 미존재하는 경우도 발생할 수 있습니다. 각각의 상황에 따라 Buffer Manager 동작 과정에는 어떤 차이가 있는지 자세하게 알아보겠습니다.

Shared Buffer 내부에 존재할 때

  1. Backend Process는 원하는 Data Page에 대한 buffer_tag를 생성합니다.
  2. 과정 ①에서 생성된 buffer_tag를 Hash Function을 통해 Hash Value로 변환합니다.
  3. 과정 ②의 결과인 Hash Value를 사용하여 Access 하고자 하는 Hash Table 영역(Partition)을 특정하고 그에 대한 BufferMapping Lock*을 확인 및 획득합니다.
    이때 획득하는 Lock Mode는 Shared Mode이며, Hash Table의 해당 영역에 Access 하는 동안에는 같은 영역에 대한 다른 프로세스의 일부 작업(Hash Table의 변경을 필요로 하는 새로운 Data Entry 추가/삭제 등)은 제한됩니다.
  4. 과정 ②의 결과인 Hash Value를 바탕으로 BufferMapping Lock이 보호하고 있는 Hash Table 영역에서 Hash Bucket을 특정하고, 해당 Bucket에 연결되어 있는 Data Entry를 탐색합니다. 이때 Data Entry를 찾기 위해 과정 ①에서 생성한 buffer_tag를 사용합니다. buffer_tag와 일치하는 값이 포함된 Data Entry를 찾아 buffer_id를 확인합니다. (위 그림에서는 buffer_tag=Tag_B와 일치하는 Data Entry의 buffer_id=1입니다.)
  5. 과정 ④에서 확인한 buffer_id가 가리키는 Buffer Descriptor에 대응하는 Buffer Pool를 사용하고자 Buffer Pin*을 설정합니다.
  6. Buffer Pin 설정 후, 과정 ③에서 획득한 BufferMapping Lock은 해제합니다.
  7. Buffer Pin을 설정한 Buffer Descriptor에 대응하는 Buffer Pool에 BufferContent Lock*을 획득합니다. 이는 Buffer Pool에 저장된 Data Page를 보호하는 역할을 하며, Data Page를 읽거나 변경하는 등의 작업을 완료할 때까지 획득 상태를 유지합니다. (동일한 Data Page에 대한 변경 작업을 제한하기 위한 Exclusive Mode의 BufferContent Lock은 하나의 프로세스에만 허용됩니다.)
  8. Data Page에 대한 작업이 완료되면, 과정 ⑦에서 획득한 BufferContent Lock을 해제하고 이어서 과정 ⑤에서 설정한 Buffer Pin도 해제합니다.

Shared Buffer 내부에 존재하지 않을 때

다음으로 Backend Process가 원하는 Data Page가 Shared Buffer 내부에 존재하지 않을 때 Buffer Manager의 동작 과정을 살펴보겠습니다.

이 경우, Shared Buffer 내부로 Data Page를 Disk로부터 가져오는 과정이 필요합니다. 이를 위해서 Data Page를 저장하기 위한 공간을 먼저 확인해야 합니다. 그리고 Shared Buffer 내 여유 공간이 존재하면 바로 Buffer를 할당받아 Data Page를 저장하지만, 그렇지 않다면 Buffer를 재사용하기 위한 과정이 선행되어야 합니다.

Shared Buffer에 여유 공간이 있다는 것은 Freelist에 Empty Buffer Descriptor가 존재한다는 것을 의미합니다. PostgreSQL에서 Freelist는 다음과 같이 Empty 상태에 있는 Buffer Descriptor를 모아 놓은 연결된 리스트(Linked List)를 지칭하며, Freelist에 포함된 Buffer Descriptor는 Shared Buffer 내부에 있는 BufferStrategyControl*이라는 구조체를 통해 알 수 있습니다.

📢 BufferStrategyControl
Freelist의 첫 번째 위치(firstFreeBuffer)와 마지막 위치(lastFreeBuffer)를 포함하여, Buffer 할당에 필요한 정보를 가지고 있는 구조체입니다. Freelist에 사용가능한 Empty Buffer Descriptor가 없을 경우, firstFreeBuffer-1로 설정됩니다.
더보기
* The shared freelist control information.
   */
  typedef struct
  {
      /* Spinlock: protects the values below */
      slock_t        buffer_strategy_lock;

      /*
       * Clock sweep hand: index of next buffer to consider grabbing. Note that
       * this isn't a concrete buffer - we only ever increase the value. So, to
       * get an actual buffer, it needs to be used modulo NBuffers.
       */
      pg_atomic_uint32 nextVictimBuffer;

      int            firstFreeBuffer;    /* Head of list of unused buffers */
      int            lastFreeBuffer; /* Tail of list of unused buffers */

      /*
       * NOTE: lastFreeBuffer is undefined when firstFreeBuffer is -1 (that is,
       * when the list is empty)
       */

      /*
       * Statistics.  These counters should be wide enough that they can't
       * overflow during a single bgwriter cycle.
       */
      uint32        completePasses; /* Complete cycles of the clock sweep */
      pg_atomic_uint32 numBufferAllocs;    /* Buffers allocated since last reset */

      /*
       * Bgworker process to be notified upon activity or -1 if none. See
       * StrategyNotifyBgWriter.
       */
      int            bgwprocno;
  } BufferStrategyControl;

 

원하는 Data Page가 Shared Buffer에 존재하지 않을 때에 대한 Buffer Manager 동작 과정은 Freelist에 사용 가능한 Empty Buffer Descritpor가 있는 경우와 그렇지 않은 경우로 나누어서 살펴보겠습니다.

 

Freelist에 Empty Buffer Descriptor가 있는 경우

Freelist에서 Buffer를 바로 할당받아 사용할 수 있습니다.

1~3 과정은 Backend Process가 원하는 Data Page를 찾기 위한 과정이므로 “Shared Buffer 내부에 존재할 때”와 동일하게 진행됩니다.

4. 원하는 Data Page가 Shared Buffer 내부에 존재하지 않으므로 Tag_C와 일치하는 Data Entry를 찾지 못합니다.

5. 탐색 실패 후, 획득했던 Hash Table 영역을 보호하는 Shared Mode의 BufferMapping Lock을 획득 해제합니다.

6. 그리고 Data Page를 Disk로부터 가져와서 저장하기 위하여 Buffer Descriptor와 Buffer Pool을 할당하며, 다음의 세부 과정을 거칩니다.

  • BufferStrategyControl의 firstFreeBuffer를 확인하여, 그 값이 가리키는 Empty Buffer Descriptor를 Freelist에서 할당받습니다. (Freelist에 사용가능한 Empty Buffer Descriptor가 존재하는 상황이므로 firstFreeBuffer-1입니다.)
  • Freelist에서 할당 후, firstFreeBuffer 값을 변경합니다. (그림에서는 34)
  • firstFreeBuffer 값을 변경할 때 Spin Lock이 아주 짧은 시간 동안 설정되었다가 해제됩니다.

7. 과정 ⑥에서 Freelist에서 할당받은 Buffer Descriptor에 Buffer Pin을 설정합니다.

8. 이번에는 Hash Table 영역에 새로운 Data Entry를 연결하기 위해서 BufferMapping Lock을 Exclusive Mode로 다시 획득합니다.

9. 과정 ⑦에서 Buffer Pin을 설정한 Buffer Descriptor의 buffer_idbuffer_tag로 구성된 Data Entry를 생성하여 연결한 후에 BufferMapping Lock을 획득 해제합니다. (그림에서는 buffer_id=3, buffer_tag=Tag_C)

10. Disk에서 Data Page를 가져와서 할당받은 Buffer Pool에 저장하고, Buffer Descriptor 내용 변경 및 과정 ⑦에서 설정한 Buffer Pin을 해제합니다.

 

위 모든 과정은 마친 후 Backend Process는 Shared Buffer 내부에서 원하는 Data Page를 찾아 Access 할 수 있습니다.

📢 Disk에서 Data Page를 가져오는 과정 ⑩에 대한 세부 내용은 이후 작성 예정인 “PWI - I/O” 글에서 다루도록 하겠습니다.

 

Freelist에 Empty Buffer Descriptor가 없는 경우

이번에는 모든 Buffer Descriptor와 Buffer Pool에 Data Page가 저장되어 있는 상황으로, 다시 말해 모두 사용 중인 경우입니다.

1~5 과정은 Backend Process가 원하는 Data Page를 찾기 위해 Hash Table 영역을 탐색하고, 일치하는 Data Page를 찾지 못해 BufferMapping Lock을 해제하는 것까지 “Freelist에 Empty Buffer Descriptor가 있는 경우”와 동일합니다.

그 이후 과정부터 살펴보겠습니다.

6. Disk로부터 Data Page를 가져와서 저장하기 위해 Buffer Descriptor와 Buffer Pool을 할당하기 위해 Freelist를 확인하지만, BufferStrategyControl의 firstFreeBuffer=-1인 것을 통해 Freelist에 Empty Buffer Descriptor가 없다는 것을 알 수 있습니다.

7. Freelist를 사용할 수 없으므로 Buffer Descriptor 및 Buffer Pool을 재사용하기 위해 Victim을 선정합니다.

  • 재사용의 대상이 되는 Buffer Descriptor와 Buffer Pool을 Victim으로 칭합니다.
  • Victim을 선정하기 위하여 Clock-Sweep* 알고리즘을 사용하였습니다.

8. Victim Buffer Descriptor에 Buffer Pin을 설정하고, 선정된 Victim Buffer Descriptor의 State를 확인합니다.

  • StateDIRTY flags 항목이 1이라면, 기존에 저장되어 있는 Data Page를 Disk로 Flush 하는 작업이 필요한 상태를 의미합니다. 따라서 해당 Page는 현재 재사용이 불가하므로 다시 Victim을 선정하기 위해 과정 ⑦로 돌아갑니다. 동시에 해당 Page는 Disk로 Flush 됩니다.
  • StateDIRTY flags1이 아닌 경우라 할지라도 과정 ⑦까지 진행되는 동안 다른 프로세스에 의해 Victim이 Access 되었다면 해당 Page는 재사용될 수 없습니다. 이 경우에도 다시 Victim 선정이 필요하기 때문에 과정 ⑦로 돌아갑니다.

9. Victim Buffer Descriptor의 buffer_id를 가지고 있는 Data Entry를 포함하는 Hash Table 영역을 찾고, BufferMapping Lock을 Exclusive Mode로 획득합니다. (그림 buffer_id=2)

10. 해당 Data Entry를 Hash Table 영역에서 제거합니다. (그림 buffer_tag=Tag_V, buffer_id=2)

11. 과정 ⑨에서 획득한 BufferMapping Lock을 획득 해제합니다.

12. 그리고 새로운 Data Entry를 연결할 Hash Table 영역에 대해 다시 BufferMapping Lock을 Exclusive Mode로 획득합니다.

13. buffer_tag와 Victim의 buffe_id의 조합으로 Data Entry를 생성하고 Hash Table에 연결합니다. (그림 buffer_tag=Tag_C, buffer_id=2)

14. 과정 ⑫에서 획득한 BufferMapping Lock을 획득 해제합니다.

15. 준비된 Buffer에 Disk로부터 Data Page를 적재한 후 과정 ⑧에서 설정한 Buffer Pin을 해제합니다.

 

마찬가지로 위 모든 과정이 끝난 후에는 Backend Process가 원하는 Data Page를 Shared Buffer 내부에서 찾을 수 있습니다.

📢 Clock-Sweep 알고리즘 (페이지 교체 알고리즘)
가장 덜 사용된(Less Frequently Used) 페이지를 효율적으로 선택하기 위한 알고리즘으로, NFU(Not Frequently Used) 알고리즘을 기반으로 합니다. PostgreSQL은 이 알고리즘을 Buffer Descriptor 교체 시 사용합니다.

”Freelist에 Empty Buffer Descriptor가 없는 경우의 동작 과정 ⑦에서 Victim 선정하는데, 이때 Clock-Sweep 알고리즘을 사용하며 Victim 선정의 기준은 Buffer Descriptor의 Staterefcountusage_count입니다.

  1. Buffer Descriptor 탐색
    BufferStrategyControl의 nextVictimBuffer가 가리키는 Buffer Descriptor를 확인하고 다음 단계를 진행한다.
  2. refcount 확인
    • refcount=0 이면(Unpinned), 다음 단계를 진행한다.
    • refcount0 이면(Pinned), nextVictimBuffer를 다음 위치로 이동시키고 이전 단계로 돌아간다.
  3. usage_count 확인
    • usage_count=0 이면, 해당 Buffer Descriptor를 Victim으로 선정, 탐색을 중지한다.
    • usage_count0 이면, usage_count-1하고 nextVictimBuffer를 다음 위치로 이동시키고 처음(Buffer Descriptor 탐색)부터 다시 진행한다.

위 과정은 refcountusage_count가 모두 0인 Buffer Descriptor를 찾아 Victim을 선정할 때까지 반복 수행됩니다. nextVictimBuffer를 이동하는 과정에서 Buffer Descriptor 배열의 끝을 만나면 첫 번째 배열로 이동합니다. (wraparound)

Victim으로 선정된 Buffer Descriptor는 새로운 메타데이터와 Data Page 저장을 위해 재사용되어 덮어 씌워집니다.

더보기
  /*
   * StrategyGetBuffer
   *
   *    Called by the bufmgr to get the next candidate buffer to use in
   *    BufferAlloc(). The only hard requirement BufferAlloc() has is that
   *    the selected buffer must not currently be pinned by anyone.
   *
   *    strategy is a BufferAccessStrategy object, or NULL for default strategy.
   *
   *    To ensure that no one else can pin the buffer before we do, we must
   *    return the buffer with the buffer header spinlock still held.
   */
  BufferDesc *
  StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring)
  {

  ...

  /* Nothing on the freelist, so run the "clock sweep" algorithm */
      trycounter = NBuffers;
      for (;;)
      {
          buf = GetBufferDescriptor(**ClockSweepTick**());

          /*
           * If the buffer is pinned or has a nonzero usage_count, we cannot use
           * it; decrement the usage_count (unless pinned) and keep scanning.
           */
          local_buf_state = LockBufHdr(buf);

          if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)
          {
              if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
              {
                  local_buf_state -= BUF_USAGECOUNT_ONE;

                  trycounter = NBuffers;
              }
              else
              {
                  /* Found a usable buffer */
                  if (strategy != NULL)
                      AddBufferToRing(strategy, buf);
                  *buf_state = local_buf_state;
                  return buf;
              }
          }
          else if (--trycounter == 0)
          {
              /*
               * We've scanned all the buffers without making any state changes,
               * so all the buffers are pinned (or were when we looked at them).
               * We could hope that someone will free one eventually, but it's
               * probably better to fail than to risk getting stuck in an
               * infinite loop.
               */
              UnlockBufHdr(buf, local_buf_state);
              elog(ERROR, "no unpinned buffers available");
          }
          UnlockBufHdr(buf, local_buf_state);
      }

  ...

  }
더보기
  /*
   * ClockSweepTick - Helper routine for StrategyGetBuffer()
   *
   * Move the clock hand one buffer ahead of its current position and return the
   * id of the buffer now under the hand.
   */
  static inline uint32
  ClockSweepTick(void)
  {
      uint32        victim;

      /*
       * Atomically move hand ahead one buffer - if there's several processes
       * doing this, this can lead to buffers being returned slightly out of
       * apparent order.
       */
      victim =
          pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);

      if (victim >= NBuffers)
      {
          uint32        originalVictim = victim;

          /* always wrap what we look up in BufferDescriptors */
          victim = victim % NBuffers;

          /*
           * If we're the one that just caused a wraparound, force
           * completePasses to be incremented while holding the spinlock. We
           * need the spinlock so StrategySyncStart() can return a consistent
           * value consisting of nextVictimBuffer and completePasses.
           */
          if (victim == 0)
          {
              uint32        expected;
              uint32        wrapped;
              bool        success = false;

              expected = originalVictim + 1;

              while (!success)
              {
                  /*
                   * Acquire the spinlock while increasing completePasses. That
                   * allows other readers to read nextVictimBuffer and
                   * completePasses in a consistent manner which is required for
                   * StrategySyncStart().  In theory delaying the increment
                   * could lead to an overflow of nextVictimBuffers, but that's
                   * highly unlikely and wouldn't be particularly harmful.
                   */
                  SpinLockAcquire(&StrategyControl->buffer_strategy_lock);

                  wrapped = expected % NBuffers;

                  success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
                                                           &expected, wrapped);
                  if (success)
                      StrategyControl->completePasses++;
                  SpinLockRelease(&StrategyControl->buffer_strategy_lock);
              }
          }
      }
      return victim;
  }

 

 

이상으로 PostgreSQL의 Shared Buffer와 그 내부 데이터에 Access 하는 메커니즘에 대해 알아보았습니다.

 

마무리

  • Shared Buffer는 데이터를 페이지 단위로 캐싱하여 I/O를 빠르게 처리하기 위한 한정적인 Shared Memory 영역입니다.
  • Buffer Manager는 Hash Table, Buffer Descriptor, Buffer Pool을 포함하여 Shared Buffer 내부 구성 요소를 관리하며, Shared Memory와 Disk Storage 간의 데이터 전송을 조율합니다.
  • Shared Buffer 내 데이터에 Access 하는 과정은 buffer_tag 생성 → Buffer 탐색 → buffer_id 반환 → Data Access 순서로 진행됩니다.
  • Shared Buffer에서 원하는 데이터를 찾기 못한 경우, Disk로부터 Data Page를 가져오는 과정이 추가됩니다.
    이때 Data Page를 저장하기 위한 공간은 Freelist에서 Empty Buffer Descriptor를 할당받습니다. 하지만 Empty Buffer Descriptor가 없는 경우에는 Clock Sweep 알고리즘을 이용해 재사용될 Victim을 선정하여 Buffer Descriptor를 재활용합니다.

 

 

댓글