PostgreSQL + QueryCache

last update: 16 Dec 2005

contents |index |previous |next

プログラム改造のヒント



組み込み関数を追加する方法などはすでにWEBや書籍に情報があるが、 パラメータの追加や、 共有メモリで新しい領域を確保するとか、 メモリコンテキスト関連の実装例は見当たらないので、 ちょっとだけ概略を書いておく。

4.1 実行パラメータの追加
実行パラメータを追加するには、src/include/util/guc.hとsrc/backend/utils/misc/guc.cを改造する。

    4.1.1 guc.c

    まず、設定したい項目を保存するグローバル変数を定義する。
    #ifdef _QUERY_CACHE
    bool		query_cache = false;
    bool		query_cache_local = true;
    int		query_cache_check_level = 0;
    int             query_cache_oblock_num = 4092;
    int             query_cache_qblock_num = 1024;
    int             query_cache_max_store_block_num = 20;
    #endif
    

    グローバル変数の定義


    データ型別に、配列にパラメータ情報を追加する。

    query_cacheははbool値なのでconfig_boolに追加。

    static struct config_bool ConfigureNamesBool[] =
    {
    	{
    		{"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD,
    			gettext_noop("Enables the planner's use of sequential-scan plans."),
    			NULL
    		},
    		&enable_seqscan,
    		true, NULL, NULL
    	},
    
    	…
    
    #ifdef _QUERY_CACHE
    	{
    		{"query_cache", PGC_POSTMASTER, DEVELOPER_OPTIONS,
    			gettext_noop("Query Cache (experimental)."),
    			NULL
    		},
    		&query_cache,
    		false, NULL, NULL
    	},
    	{
    		{"query_cache_local", PGC_USERSET, DEVELOPER_OPTIONS,
    			gettext_noop("Query Cache Local (experimental)."),
    			NULL
    		},
    		&query_cache_local,
    		true, NULL, NULL
    	},
    #endif
    
    	…
    

    bool値のパラメータの追加


    追加する項目の説明は次のとおり:
    {"query_cache", 			/* パラメータ名 */
    	PGC_POSTMASTER, 		/* コンテキスト名 -> 後述 */
    	DEVELOPER_OPTIONS,
    	gettext_noop("Query Cache (experimental)."),
    	NULL
    },
    &query_cache,				/* パラメータに対応するグローバル変数 /
    false, 					/* デフォルト値 */
    NULL,					/* 変更時に実行したい関数名 */ 
    NULL					/* 変更値をLOGに出す? */
         

    ここでパラメータのコンテキスト名について:
    const char *const GucContext_Names[] =
    {
    	 /* PGC_INTERNAL */ "internal",      // 設定も変更もできない
    	 /* PGC_POSTMASTER */ "postmaster",  // postmasterがつかう。postgresql.confに設定化、変更不可  
    	 /* PGC_SIGHUP */ "sighup",          // ?
    	 /* PGC_BACKEND */ "backend",        // postgresがつかう。postgresql.confに設定化、変更不可  
    	 /* PGC_SUSET */ "superuser",        // SETでスーパーユーザのみ変更可能
    	 /* PGC_USERSET */ "user"            // SETで一般ユーザが変更可能  
    };
    

    コンテキスト名


    int型なども同様。

    "query_cache_check_level", 
    PGC_USERSET, 
    RESOURCES_MEM,
    gettext_noop("Sets the maximum number of data block."),
    NULL
    },
    &query_cache_check_level,
    0,				// デフォルト値
    0,				// min
    4,				// max
    NULL,
    NULL
         

    変更時に実行できる関数を設定することもできるようだ。

    4.1.2 guc.h

    先に設定したグローバル変数を、extern宣言する。
    #ifdef _QUERY_CACHE
    extern bool	query_cache;
    extern bool	query_cache_local;
    extern int	query_cache_check_level;
    extern int      query_cache_oblock_num;
    extern int      query_cache_qblock_num;
    extern int      query_cache_max_store_block_num;
    #endif
    

    guc.h


4.2 共有メモリでの領域確保

    4.2.1 下準備

    共有メモリで領域を確保するには、まず src/backend/storage/ipc/ipci.cの CreateSharedMemoryAndSemaphores()を改造しておく。
    1箇所目は、 必要とするメモリ領域sizeをインクリメントするように関数(QueryCacheShmemSize)を追加。 2箇所目は、実際に領域を確保する関数(InitQueryCacheShmem)を追加。

    CreateSharedMemoryAndSemaphores(bool makePrivate, int port)
    {
    
    	…
    
    #ifdef _QUERY_CACHE
    		size += QueryCacheShmemSize();
    #endif
    
    	…
    
    	InitFreeSpaceMap();
    
    #ifdef _QUERY_CACHE
    	/*
    	 * Set up query-cache space
    	 */
    	InitQueryCacheShmem();
    #endif
    
    …
    

    ipci.cへの変更(2)


    4.2.2 メモリ領域の確保

    サイズを求める関数と、実際にメモリ領域を確保する関数は、各自準備する。 ここではquerycache.cに書いた。

    サイズを求めるのは簡単だから省略し、メモリ領域の確保についてだけ。

    簡単に言ってしまえば、ShmemAlloc()とMemSet()というPostgreSQLが準備した関数を使って メモリを確保しろ、ということ。

    void
    InitQueryCacheShmem(void)
    {
      if (query_cache)
        {
          create_header ();
          create_qc_oid_space ();
          create_qc_query_space ();
        }
    }
    
    /*
     * The initialization routine of QueryCache Header
     */
    static void
    create_header (void)
    {
      size_t size = header_size ();
      
      /*
       * Create query cache header 
       */
      QueryCacheHeader = (QueryCache_Header *) ShmemAlloc(size);
    
      if (QueryCacheHeader == NULL)
        ereport(FATAL,
    	    (errcode(ERRCODE_OUT_OF_MEMORY),
    	     errmsg("insufficient shared memory for query cache header")));
      
      MemSet(QueryCacheHeader, 0, size);
      
      /*
       * Initialize: header data
       */
      init_header_data ();
    }
    
    /*
     *
     */
    static void
    create_qc_oid_space (void)
    {
      size_t size;
    
      size = qc_oid_space_size ();
    
      /*   */
      QueryCacheOidSpace = (char *) ShmemAlloc (size);
    
      /*
       *
       */
      if (QueryCacheOidSpace == NULL)
        {
          ereport(FATAL,
    	      (errcode(ERRCODE_OUT_OF_MEMORY),
    	       errmsg("insufficient shared memory for qc_oid_space")));
        }
      MemSet(QueryCacheOidSpace, 0, size);
    
      /*  メモリ領域内部の初期化 */
      init_blocks (query_cache_oblock_num, 
      	       (Oid)0,
    	       (void (*)(int *, block_type *, void *, bid *, bid *))(set_table_block));
    }
    
    /*
     *
     */
    static void
    create_qc_query_space (void)
    {
      size_t size;
      query c_query;
    
      size = qc_query_space_size ();
    
      /* */
      QueryCacheQuerySpace = (char *) ShmemAlloc (size);
    
      /* */
      if (QueryCacheQuerySpace == NULL)
        {
          ereport(FATAL,
    	      (errcode(ERRCODE_OUT_OF_MEMORY),
    	       errmsg("insufficient shared memory for qc_query_space")));
        }  
    
      MemSet(QueryCacheQuerySpace, 0, size);
    
    
      /*  メモリ領域内部の初期化 */
      init_current_query (&c_query);
      init_blocks (query_cache_qblock_num, 
    	       &c_query,
    	       (void (*)(int *, block_type *, void *, bid *, bid *))(set_query_block));
    }
    

    共有メモリの確保


    ちなみに、src/backend/storage/ipc/shmem.cによれば、 共有メモリの領域確保する関数は3つある。
    1. fixed-size
    2. queue
    3. hash table

    今回つかったのはfixed-sizeで、これは単に領域を確保するだけ、内部構造は各自決めていかならない。

    確保したメモリ領域でhashを使いたい場合には、freespace/freespace.cを参考にするとよい。 手順はShmemInitStruct()で領域を確保し、ShmemInitHash()でハッシュを構築する。

     * NOTES:
     *		(a) There are three kinds of shared memory data structures
     *	available to POSTGRES: fixed-size structures, queues and hash
     *	tables.  Fixed-size structures contain things like global variables
     *	for a module and should never be allocated after the process
     *	initialization phase.  Hash tables have a fixed maximum size, but
     *	their actual size can vary dynamically.  When entries are added
     *	to the table, more space is allocated.	Queues link data structures
     *	that have been allocated either as fixed size structures or as hash
     *	buckets.  Each shared data structure has a string name to identify
     *	it (assigned in the module that declares it).
         

    4.2.3 共有メモリ領域へのアクセス

    いかにもポインタのように扱えるマクロがあるようだが、 今回は車輪の再発明というか、PostgreSQLの内部を深く知ることが目的の一つなので、 memcpyでちまちま関数を書いている。

    先のメモリ領域確保の関数で、次のようにした。

      QueryCacheOidSpace = (char *) ShmemAlloc (size);
         
    これで、メモリ領域の先頭アドレスはQueryCacheOidSpaceということがわかる。 これはstaticな変数として、確保しておくこと。
    static char* QueryCacheOidSpace = NULL;
         

    では、以下が共有メモリ領域に読み書きする関数たち。 もっとスマートな方法もあるだろうが、PostgreSQLのソースのジャングルを探索をしながら作っていたら、 こうなってしまった。全部、直接アドレス計算をして読み書きする。

    注: (id - 1)としているのは、論理的にはidを1から数えているから。
    これはこのプログラムでの内部規約であり、一般的な話ではない。

    static void
    write_table_block (oblock *ob, bid id)
    {
      memcpy((void *) (QueryCacheOidSpace + sizeof(oblock) * (id - 1)),
             (const void *)ob,
             sizeof(oblock));
    }
    
    static void
    read_table_block (oblock *ob, bid id)
    {
      memcpy((void *)ob,
             (const void *) (QueryCacheOidSpace + sizeof(oblock) * (id - 1)),
             sizeof(oblock));
    }
    

    ブロックへの読み書き


    ブロック、つまり構造体の任意の要素に読み書きするには、マクロoffsetofを利用する。

    
    struct oblock {
    
    …
    
        } qid;
      };
      bid prev, next;
    };
    
    #define offset_oblock_prev offsetof (oblock, prev)
    #define offset_oblock_next offsetof (oblock, next)
    
    
    static void
    get_next_table_block (bid id, bid *next)
    {
      memcpy((void *)next,
             (const void *) (QueryCacheOidSpace + sizeof(oblock) * (id - 1) + offset_oblock_next),
             sizeof(bid));
    }
    
    static void
    set_next_table_block (bid id, bid next)
    {
      memcpy((void *)(QueryCacheOidSpace + sizeof(oblock) * (id - 1) + offset_oblock_next),
             (const void *) &next,
             sizeof(bid));
    }
    

    任意の要素に読み書き


4.3 メモリコンテキスト
メモリコンテキストは、以前はなんだか良く分からなかったが、 今回いろいろいじってみて、多少分かってきた(つもり)。
詳細は: src/backend/utils/mmgr/README

要するに、適したメモリコンテキストを選んで、そこでpalloc()で領域を確保すればよい。


予めコンテキスト毎に十分な量のメモリが確保されているので、 効率を気にすることなく、がんがんpallocを使ってもよい(OSがalloc()しているわけでは*ない*)。

またほとんどのコンテキストは、クエリが終了したり(QueryContext, ErrorContext…)、 トランザクションが終了すると、 (勝手に)メモリ領域を解放してくれるので、かなり便利。

    4.3.1 独自のメモリコンテキスト生成

    独自のメモリコンテキストの生成も簡単。以下のはTopMemoryContextの子どもとして生成する。

    言い忘れていたが、メモリコンテキストはTopMemoryContxtを根とする木構造で、 どんどん子どもを作れる。 途中のメモリコンテキストが削除されると、その子どもたちも自動的に削除される。

    static MemoryContext query_cache_context = NULL;
    
    
    static void
    create_query_cache_context (void)
    {
      query_cache_context = 
        AllocSetContextCreate(TopMemoryContext,
    			  "query cached data cell ",
    			  ALLOCSET_DEFAULT_MINSIZE,
    			  sizeof (struct data_cell),
    			  sizeof (struct data_cell) * ceil((double) query_cache_max_store_block_num / MAX_DATA));
    }
    

    独自のメモリコンテキスト生成


    メモリコンテキストの使い方は、いろいろなところに書かれているとおり。

    	struct cell *c;
    
    	…
    
    	MemoryContext oldcontext = MemoryContextSwitchTo(CurTransactionContext);
    
    	…
    
    	c = (struct cell *) palloc (sizeof (struct cell));
    
    
    	MemoryContextSwitchTo(oldcontext);
    
    

    メモリコンテキストの使い方


    4.3.2 CurTransactionContextを使った例

    トランザクション処理の対応は仮実装なのだが、 CurTransactionContextの例として、分かりやすいと思うので、概略を書く。

    以下、簡単にトランザクション処理への対応を説明する。
    ポイントはメモリコンテキストの中のCurTransactionContext。 BEGIN文を受け取ったら、CurTransactionContextにstruct *Tx_info tx_infoというデータ領域を生成する。

    そして、COMMIT文かROLLBACK文を受け取ると、 CurTransactionContextは初期化され、*Tx_info tx_info = NULLに戻る。

    Remark: プログラム中では、struct *Tx_info == NULLならトランザクション外、!=ならトランザクション内という判断をしている。 論理的でなく実装依存の汚い部分。

    トランザクション内でのSELECT文はすべてスルー(NO_CACHE)する。DELETEやINSERT,UPDATEは、 関連するリレーション=テーブルOIDを、oid_cellという構造体に格納する。 oid_cellはその都度CurTransactionContext上に生成され、 リストとしてtx_infoに追加されていく。

    COMMIT文が実行されると、tx_infoに追加されたoid_cellを辿りつつ、 格納されていたOIDを(共有)メモリ上から削除していく。

    これにより、Transaction処理内で操作されたであろうテーブルの情報は、一旦キャッシュ上から削除され、 ACIDのCIが保証されていると(勝手に)思っている。

    ちなみに、勝手にトランザクションがアボートされた場合、CurTransactionContextは初期化されるので、 勝手にOIDの削除が行なわれる心配はない。

    と、まあ、こんな感じで実装してみた。


contents |index |previous |next


since 04/Oct/2004