聊聊Postgres中的IPC之SI Message Queue

solution發表於2021-09-09

在 PostgreSQL中,每一個程式都有屬於自己的共享快取(shared cache)。例如,同一個系統表在不同的程式中都有對應的Cache來快取它的元組(對於RelCache來說快取的是一個RelationData結構)。同一個系統表的元組可能同時被多個程式的Cache所快取,當其中某個Cache中的一個元組被刪除或更新時 ,需要通知其他程式對其Cache進行同步。在 PostgreSQL的實現中,會記錄下已被刪除的無效元組 ,並透過SI Message方式(即共享訊息佇列方式)在程式之間傳遞這一訊息。收到無效訊息的程式將同步地把無效元組(或RelationData結構)從自己的Cache中刪除。


1.無效訊息(Invalid Message)概述

當前系統支援傳遞6種無效訊息:
第一種是使給定的catcache中的一個元組無效;
第二種是使給定的系統表的所有catcache結構全部失效;
第三種是使給定的邏輯表的Relcache中RelationData結構無效;
第四種是使給定的物理表的SMGR無效(表物理位置發生變化時,需要通知SMGR關閉表檔案);
第五種是使給定的資料庫的mapped-relation失效;
第六種是使一個已儲存的快照失效。

可以看出這六種訊息對應的影響範圍越來越大。

PostgreSQL使用以下所示的結構體來儲存無效訊息。

typedef union{
    int8        id;             /* type field --- must be first */
    SharedInvalCatcacheMsg cc;
    SharedInvalCatalogMsg cat;
    SharedInvalRelcacheMsg rc;
    SharedInvalSmgrMsg sm;
    SharedInvalRelmapMsg rm;
    SharedInvalSnapshotMsg sn;
} SharedInvalidationMessage;

其中,id為:

  • 0或正數表示一個CatCache元組;

  • -1表示整個CatCahe快取;

  • -2表示RelCache;

  • -3表示SMGR;

  • -4表示mapped-relation mapping;

  • -5表示Snapshot

當id為0或正數時 ,它同時也表示產生該Invalid Message的CatCache的編號。

具體我們可以看註釋:

src/include/storage/sinval.h
 *  * invalidate a specific tuple in a specific catcache
 *  * invalidate all catcache entries from a given system catalog
 *  * invalidate a relcache entry for a specific logical relation
 *  * invalidate an smgr cache entry for a specific physical relation
 *  * invalidate the mapped-relation mapping for a given database
 *  * invalidate any saved snapshot that might be used to scan a given relation

程式透過呼叫函式CachelnvalidateHeapTuple()對Invalid Message進行註冊,主要包括以下幾步:

  • 1) 註冊SysCache無效訊息。

  • 2) 如果是對pg_class系統表元組進行的更新/刪除操作,其 relfilenode或 reltablespace可能發生變化,即該表物理位置發生變化,需要通知其他程式關閉相應的SMGR。這時首先設定relationid和databaseid,然後註冊SMGR無效訊息;否則轉而執行步驟3。

  • 3) 如果是對pg_attribute或者pg_index系統表元組進行的更新/刪除操作,則設定relationid和 dalabaseid,否則返回。

  • 4) 註冊RelCache無效訊息(如果有的話)。

  • 5) 事務結束時註冊mapped-relation mapping和snapshot無效訊息(如果有的話)。

當一個元組被刪除或者更新時,在同一個SQL命令的後續執行步驟中我們依然認為該元組是有效的,直到下一個命令開始或者亊務提交時改動才生效。在命令的邊界,舊元組變為失效,同時新元組置為有效。因此當執行heap_delete或者heap_update時,不能簡單地重新整理Cache。而且,即使重新整理了,也可能由於同一個命令中的請求把該元組再次載入到Cache中。

因此正確的方法是保持一個無效連結串列用於記錄元組的delete/update操作。事務完成後,根據前述的無效連結串列中的資訊廣播該事務過程中產生的Invalid Message,其他程式透過SI Message佇列讀取Invalid Message對各自的Cache進行重新整理。當子事務提交時,只需要將該事務產生的Invalid Message提交到父事務,最後由最上層的事務廣播Invalid Message。

需要注意的是,若涉及對系統表結構的改變,還需要重新載入pg_internal.init檔案,因為該檔案記錄了所有系統表的結構。


2.SI Message全景

以下是相關的函式,寫在前面,先混個臉熟:

CreateSharedInvalidationState()  /* Create and initialize the SI message buffer

SharedInvalBackendInit()  /* 每個backend初始化時要初始化在 SI message buffer 中的Per-backend invalidation state,procState[MaxBackends]

CleanupInvalidationState() /*每個backend shutdown時在呼叫on_shmem_exit()函式清空對應的procState[i]

SICleanupQueue()  /* Remove messages that have been consumed by all active backends
                 * Possible side effects of this routine include marking one or more
                * backends as "reset" in the array, and sending PROCSIG_CATCHUP_INTERRUPT
                * to some backend that seems to be getting too far behind.  We signal at
                * most one backend at a time, for reasons explained at the top of the file.
                
 SendSharedInvalidMessages() /* Add shared-cache-invalidation message(s) to the global SI message queue.

那麼整個SI Message佇列工作的流程大致如下:

  1. SI message 佇列的初始化。這個是由postmaster在啟動伺服器時做的,作為共享記憶體的一部分,由postmaster初始化。此時,SI message為空,因為此時還沒有Invalid Message產生。

  2. 每個backend初始化(我們知道這些Invalid Message是由於我執行了SQL文對資料庫進行了修改才產生的,那麼很顯然我們執行SQL文的途徑是前端傳送SQL文,後端啟動一個backend程式去處理)時,需要初始化自己的共享記憶體並且向SI message註冊自己。註冊的目的有兩個,一個是宣告自己作為Invalid Message的生產者的身份,另一個表示自己也需要接受其他backend的Invalid Message。

  3. 每個backend執行SQL文,產生Invalid Message,其他backend接收該Invalid Message,當然,這個過程複雜點,會在後面細說。那麼每個backend接收和傳送Invalid Message的時機是什麼呢?

當然啦,你每次執行SQL的時候,是一個好時機,在執行SQL文的開頭和結尾,backend都會去check SI message佇列中的無效訊息。以下是呼叫棧:

exec_simple_query
    ->start_xact_command
        ->StartTransactionCommand         /* 事務開始
            ->StartTransaction
                ->AtStart_Cache
                    ->AcceptInvalidationMessages
                        ->ReceiveSharedInvalidMessages /* consume SI message
                            ->SIGetDataEntries
                        
    -> do query
    
    ->finish_xact_command
        ->CommitTransactionCommand         /* 事務結束
            ->CommitTransaction
                ->AtEOXact_Inval
                    ->SendSharedInvalidMessages       /*  send SI message
                        ->SIInsertDataEntries   
                            ->SICleanupQueue

那麼,難道我不執行SQL文,我的backend就不重新整理無效訊息麼?

我們看一段註釋:

/*
 * Because backends sitting idle will not be reading sinval events, we
 * need a way to give an idle backend a swift kick in the rear and make
 * it catch up before the sinval queue overflows and forces it to go
 * through a cache reset exercise.  This is done by sending
 * PROCSIG_CATCHUP_INTERRUPT to any backend that gets too far behind.
 *
 * The signal handler will set an interrupt pending flag and will set the
 * processes latch. Whenever starting to read from the client, or when
 * interrupted while doing so, ProcessClientReadInterrupt() will call
 * ProcessCatchupEvent().
 */

沒有錯,要是某個backend長時間不讀取SI Message或者backend落後太多,超過了SI Message佇列可以接受的最大長度,那麼就向該backend傳送SIGUSR1,喚醒該backend讓其做適當的操作。


3.實現細節

為了實現SI Message的這一功能,PostgreSQL在共享記憶體中開闢了shmInvalBuffer記錄系統中所發出的所有Invalid Message以及所有程式處理無訊息的進度。shmInvalBuffer是一個全域性變數,其資料結構如下:

typedef struct SISeg
{    /*
     * General state information
     */
    int         minMsgNum;      /* oldest message still needed */
    int         maxMsgNum;      /* next message number to be assigned */
    int         nextThreshold;  /* # of messages to call SICleanupQueue */
    int         lastBackend;    /* index of last active procState entry, +1 */
    int         maxBackends;    /* size of procState array */

    slock_t     msgnumLock;     /* spinlock protecting maxMsgNum */

    /*
     * Circular buffer holding shared-inval messages
     */
    SharedInvalidationMessage buffer[MAXNUMMESSAGES];    /*
     * Per-backend invalidation state info (has MaxBackends entries).
     */
    ProcState   procState[FLEXIBLE_ARRAY_MEMBER];
} SISeg;

在shmInvalBuffer中,Invalid Message儲存在由Buffer欄位指定的定長陣列中(其長度MAXNUMMESSAGES預定義為4096),該陣列中每一個元素儲存一個Invalid Message,也可以稱該陣列為無效訊息佇列。無效訊息佇列實際是一個環狀結構,最初陣列為空時,新來的無效訊息從前向後依次存放在陣列中,當陣列被放滿之後,新的無效訊息將回到Buffer陣列的頭部開始插人。minMsgNum欄位記錄Buffer中還未被所有程式處理的無效訊息編號中的最小值,maxMsgNum欄位記錄下一個可以用於存放新無效訊息的陣列元素下標。實際上,minMsgNum指出了Buffer中還沒有被所有程式處理的無效訊息的下界,而maxMsgNum則指出了上界,即編號比minMsgNmn小的無效訊息是已經被所有程式處理完的,而編號大於等於maxMsgNum的無效訊息是還沒有產生的,而兩者之間的無效訊息則是至少還有一個程式沒有對其進行處理。因此在無效訊息佇列構成的環中,除了 minMsgNum和maxMsgNum之間的位置之外,其他位置都可以用來存放新增加的無效訊息。

PostgreSQL在shmInvalBuffer中用一個ProcState陣列(procState欄位)來儲存正在讀取無效訊息的程式的讀取進度,該陣列的大小與系統允許的最大程式數MaxBackends有關,在預設情況下這個
陣列的大小為100 (系統的預設最大程式數為100,可在postgresql.conf中修改)。ProcState的結構如資料結構如下所示。

/* Per-backend state in shared invalidation structure */typedef struct ProcState
{    /* procPid is zero in an inactive ProcState array entry. */
    pid_t       procPid;        /* PID of backend, for signaling */
    PGPROC     *proc;           /* PGPROC of backend */
    /* nextMsgNum is meaningless if procPid == 0 or resetState is true. */
    int         nextMsgNum;     /* next message number to read */
    bool        resetState;     /* backend needs to reset its state */
    bool        signaled;       /* backend has been sent catchup signal */
    bool        hasMessages;    /* backend has unread messages */

    /*
     * Backend only sends invalidations, never receives them. This only makes
     * sense for Startup process during recovery because it doesn't maintain a
     * relcache, yet it fires inval messages to allow query backends to see
     * schema changes.
     */
    bool        sendOnly;       /* backend only sends, never receives */

    /*
     * Next LocalTransactionId to use for each idle backend slot.  We keep
     * this here because it is indexed by BackendId and it is convenient to
     * copy the value to and from local memory when MyBackendId is set. It's
     * meaningless in an active ProcState entry.
     */
    LocalTransactionId nextLXID;
} ProcState;

在ProcSlate結構中記錄了PID為procPid的程式讀取無效訊息的狀態,其中nextMsgNum的值介於 shrolnvalBuffer 的 minMsgNum 值和 maxMsgNum 值之間。

如下圖所示,minMsgmun和MaxMsgmim就像兩個指標,它們區分出了哪些無效訊息已經被所有的程式讀取以及哪些訊息還在等待某些程式讀取。在minMsgnum之前的訊息已經被所有程式讀完;maxMsgnum之後的區域尚未使用;兩者之間的訊息是還沒有被所有程式讀完的。當有程式呼叫函式SendSharedlnvalidMessage將其產生的無效訊息新增到shmInvalBuffer中時,maxMsgnum就開始向後移動。SendSharedlnvalidMessage中將呼叫SIInsertDataEntries來完成無效訊息的插人。

圖片描述

在向SI Message佇列中插入無效訊息時,可能出現可用空間不夠的情況(此時佇列中全是沒有完全被讀取完畢的無效訊息),需要清空一部分未處理無效訊息,這個操作稱為清理無效訊息佇列,只有噹噹前訊息數與將要插人訊息數之和超過shmInvalBuffer中nextThreshold時才會進行清理操作。這時,那些還沒有處理完SI Message佇列中無效訊息的程式將收到清理通知,然後這些程式將拋棄其Cache中的所有元組(相當於重新載人Cache的內容)。

顯然,讓所有程式過載Cache會導致較高的I/O次數。為了減少過載Cache的次數,PostgreSQL會在無效訊息佇列中設定兩個界限值lowbound和minsig,其計算方式如下:

• lowbound=maxMsgNum-MAXNUMMESSAGES+minFree,其中 minFree 為需要釋放的佇列空間的最小值(minFree指出了需要在無效訊息佇列中清理出多少個空位用於容納新的無效訊息)。

• minsig = maxMsgNum-MAXNUMMESSAGES/2,這裡給出的是minsig的初始值,在程式過載過程中minsig會進行調整。
SICleanupQueue

    /*
     * Recompute minMsgNum = minimum of all backends' nextMsgNum, identify the
     * furthest-back backend that needs signaling (if any), and reset any
     * backends that are too far back.  Note that because we ignore sendOnly
     * backends here it is possible for them to keep sending messages without
     * a problem even when they are the only active backend.
     */
    min = segP->maxMsgNum;
    minsig = min - SIG_THRESHOLD;
    lowbound = min - MAXNUMMESSAGES + minFree;

可以看到,lowbound實際上給出了此次清理過程中必須要釋放的空間的位置,這是一個強制性的限制,nextMsgNum值低於lowbound的程式都將其resetState欄位置為真,這些程式將會自動進行過載Cache的工作。對於那些nextMsgNum值介於lowbound和minaig之間的程式,雖然它們並不影響本次淸理,但是為了儘量避免經常進行清理操作,會要求這些程式加快處理無效訊息的進度(CatchUp)。淸理操作會找出這些程式中進度最慢的一個,向它傳送SIGUSR1訊號。該程式接收到SIGUSR1後會一次性處理完所有的無效訊息,然後繼續向下一個進度最慢的程式傳送SIGUSR1讓它也加快處理進度。

清理無效訊息佇列的工作由函式SICleanupQueue實現,該函式的minFree引數給出了這一次淸理操作至少需要釋放出的空間大小。該函式的流程如下:

SICleanupQueue
    ->SendProcSignal

1)計算 lowbound 和 minsig 的值。

2) 對每一個程式的ProcState結構進行檢査,將nextMsgNum低於lowbound的程式resetState欄位設定為true,並在nextMsgNum介於lowboumi和minsig之間的程式中找出進度最慢的一個。

3) 重新計算nextThreshoW引數。

4) 向步驟2中找到的進度最慢的程式傳送SIGUSR1訊號。

Postgres程式透過函式ProcessCatchupInterrupt來處理SIGUSR1訊號,該函式最終將呼叫ReceiveSharedlnvalidMessages來處理所有未處理的無效訊息,最後呼叫SICleanupQueue (minFree引數為0)向下一個進度最慢的程式傳送SIGUSR1訊號(呼叫棧如下)。

ProcessCatchupInterrupt
    ->AcceptInvalidationMessages
        ->ReceiveSharedInvalidMessages
            ->SICleanupQueue

每個程式在需要重新整理其Cache時也會呼叫ReceiveSharedInvalidMessages函式用於讀取並處理無效訊息,函式引數為兩個函式指標:

1) invalFunction:用於處理一條無效訊息。

2) resetFunction:將該後臺程式的Cache元組全部拋棄。

對於resetState設定為真的程式,函式ReceiveSharedInvalidMessages會呼叫resetFunction拋棄其所有的Cache元組。否則,ReceiveSharedInvalidMessages將從訊息佇列中讀取每條無效訊息並呼叫invalFunction對訊息進行處理。如果該程式是根據SIGUSR1訊號呼叫該函式,那麼還將呼叫SICleanupQueue函式將這個訊號傳給比它進度慢的程式。


4.其他

在PMsignal.c中,包含後臺程式向Postmaster傳送訊號的相關函式。在實現中,後臺程式是這樣通知Postmaster的:

1) 首先在共享記憶體中開闢一個陣列PMSignalFlags(PMsignal.c),陣列中的每一位對應一個訊號。

2) 然後如果後臺程式希望向Postmaster傳送一個訊號,那麼後臺首先將訊號在陣列PMSignalFlags中相應的元素置1 (邏輯真),然後呼叫kill函式向Postmaster傳送SIGUSR1訊號。

3) 當Postmaster收到SIGUSR1後首先檢測共享儲存中PMSignalFlags,確認具體的訊號是什麼。同時將訊號在陣列PMSignalFlags中相應的元素置0 (邏輯假)然後作出相應反應。

每一個後臺程式都有一個結構PGPROC儲存在共享記憶體中。Procarray.c在共享記憶體中分配ProcArrayStruct型別的陣列procArray,統一管理這些PGPROC結構。PGPROC結構中包含很多的資訊,Procarray.c中的函式主要處理 PGPROC中的 pid、databaseld、roleld、xmin、xid、subxids 等欄位。這些函式的功能或是統計事務的資訊,或是透過databaseId統計有多少個pid (也就是多少個後臺程式)與指定資料庫相連線等統計資訊。

IPC負責的清除工作有兩個方面:一個是與共享記憶體相關的清除,另一個是與各個後臺程式相關的清除工作。與共享記憶體相關的淸除並不是將共享記憶體丟棄,而是重新設定共享記憶體。清除工作的流程可以描述如下:首先在申請資源的時候,系統會同時為該資源註冊一個清除函式,當要求做清除操作時,系統將會呼叫對應的淸除函式。


IPC的內容還有不少,本次只是大致說了下關於SI Message共享佇列的處理,其它的以後有時間再去寫寫吧。

作者:

出處:http://www.cnblogs.com/flying-tiger/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利.

感謝您的閱讀。如果覺得有用的就請各位大神高抬貴手“推薦一下”吧!你的精神支援是博主強大的寫作動力。

如果覺得我的部落格有意思,歡迎點選首頁左上角的“+加關注”按鈕關注我!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2459/viewspace-2802204/,如需轉載,請註明出處,否則將追究法律責任。

相關文章