2014/12/25

疑似コードで、昨今のIn-Memoryとかカラム型とかを味わう

とうとう、JPOUG Advent Calendar 2014 も最終日となりました。今年もご参加頂いた皆様に感謝しつつも、去年に続き、オオトリを務めさせていただきます。

Oracleデータベースも12.1.0.2というバージョンでIn-Memoryかつカラム型で分析系ワークロード用を高速化するオプションが導入されていることはご存じの通りです。

このIn-Memoryオプションという文脈で

"ディスクは遅くメモリーは速い。だからIn-Memoryなデータベースは速い"
とか

"分析系ワークロードはカラム型といったデータフォーマット合っている。だからカラム型が速い"
とか

"データベースの処理をSIMD(シムディー)とかVector処理といった処理で行うと速い"
とか

なかなか、上記のキーワードがどのようにデータベース処理と関連しているか不明な状態で説明されることが多いのではないか。と思う今日この頃です。

今日は、In-Memoryやカラム型やSIMDといったキーワードを自分なりに関連あることとして、ここにメモ書きを残そうと思います。

言葉でアレコレ説明しても、よく分からん。(どなたかはSIMDの気持が知りたい。と仰っていた)なので、とてつもなくシンプルな疑似コードで説明してみたいと思います。

* ちなみに、以下示される疑似コードはOracleとも他のデータベースの作りとも全く関係ありません。目的は、疑似コードで処理のイメージを掴めれば良い程度の簡単なものです。(要はこんなにデータ構造はシンプルではないですし、コードもこんなに非汎用的ではないですし、正直、WHERE句のSIMD適用はこんなにお手軽ではないですし)

前提)
以下のような数十カラムを含むテーブル構成で1億件のランダムなデータが入っていると想定

SQLで書くと以下のような感じ

create table large_table (
 l_quantity number
,l_tax      number
,...
,(沢山)
,...
);

こんなテーブルをコードで書くと(とりあえずC)、以下のような構造体の配列(これは通常のロー型を想定)になると思います

typedef struct {
 int l_quantity;
 int l_tax;
 ...
 (沢山)
 ...
} LARGE_TABLE_t;

LARGE_TABLE_t large_table = (LARGE_TABLE_t*)malloc(sizeof(LARGE_TABLE_t) * 100000000);

上記に適当にデータを詰め込んだあと(適当なランダムな値という意味で、値がソート済みだとリソース使用状況が大きく異なりますので注意)、以下のようなSQLを想定したコードを書いてみます。

SQLで書くと以下のような感じ

SELECT count(*) FROM large_table WHERE l_quantity > 50;

普通(ロー型をイメージした)バージョン

これをCで表現して以下のような関数にしてみました。

/*
 引数
  第1引数: LARGE_TABLE_tとして渡される構造体の配列の個数(ここでは100,000,000)
  第2引数: LARGE_TABLEの構造体のポインター
  第3引数: 条件に指定する数値(ここでは50)

 戻り
  結果として、条件にマッチしたデータの個数
*/
int select_count_gt(int n, LARGE_TABLE_t* tab, int* val)
{
 int cnt = 0, i;
 int v = *val;
 for (i=0; i<n; ++i) {
  if (tab[i].l_quantity > v) {
   cnt++;
  }
 }
 return cnt;
}

さあ、これで、一応In-Memoryで処理する準備ができましたので、これを実行してみます。が、ディスクベースより当然高速なのは明白で且つCPUは100%/coreの状態で処理するので、perfコマンドで、CPUのプロファイルを取りながら、上記コードのどこに問題があるか見てみるとしましょう。

1億件のデータで、そのSELECTIVITYを0% ~ 100%まで10段階で評価してみましょう。

ロー型データ構造時のperfの結果





まず目を引くのはCPT(Cycles Per Tuple)の大きさです。これは1行の処理をするのにかかったCPUサイクルを示しますが、1行あたり23.8サイクルもかかっています。
似たような指標としてIPC(Instructions Per Cycle)の低さも目につきます。これは1CPUサイクルあたり実行したCPUインストラクション数を示していますが、昨今のCPUは複数のCPUインストラクション実行ユニットを持っているにも関わらず、IPCは1を下回る低い数値です。

この原因として、大きく2+αあると思われます。

  1. CacheミスによるCPUのストール
  2. 分岐ミスによるCPUにパイプラインハザード
  3. そもそも、こんな簡単なコードをもっと効率的に実行できないか?(要はCPUインストラクション数、結果的にCPUサイクル数を減らせないか?)


1に関して
In-Memoryで、ディスクI/Oがないとはいえ、CPUから見れば、非常に低速なMemoryからデータをCPUキャッシュに持ってくるのはコストが高く、上手くCPUキャッシュを使うことを考えないと、データベースの性能も上手く引き出せない。という事で、最初に定義したような構造体の配列(いわゆるArray of Structure(AoS))では、l_quantityというメンバーしか参照していないにも関わらず、無駄にキャッシュロードが発生しています。であれば、配列の構造体(Structure of Array(SoA))にした方が良いのではないか?
非常に乱暴に言うと、AoSをロー型のデータ構造だとするとSoAはカラム型のデータ構造と言えると思います。

2に関して
最初に示したselect_count_gt()なる関数ではforループの脱出条件の評価とループ内のWHERE句に相当する条件評価の2つがありますが、SELECTIVITYを可変にして行った実験からもWHERE句に相当するIF分岐の予測は、SELECTIVITYが50%になると最悪になっています。これは当然なわけですが、何とか分岐予測ミスを発生しない。つまり分岐そのものをなくす方向で処理したい。

3に関して
少しだけ2とも関連しますが、今までのムーアの法則に従ってCPUクロックがどんどん高くなる世界なら、CPU内の多少の無駄な処理も、高くなるクロック数が隠ぺいしてくれていましたが、さすがに、クロック数の伸びは鈍化しています。これからは、パフォーマンスを最適化したいと思ったら、CPUに無駄な処理をさせないことを考える必要があります。つまりCPUインストラクションの削減と、結果としてCPUサイクルの低減が主なターゲットとなります。そこでSIMDが登場します。SIMDを使ったコードは基本的にバルク処理で、いちいち分岐するような処理には向いていません。なので、SIMDを考えるなら2も合わせて考えることになります。

という事で、まず1のデータ構造をロー型からカラム型(非常におおざっぱなのは前述の通り)に変えてみます。

カラム型をイメージしたバージョン


先ほどの構造体はこんな感じになります。

typedef struct {
 int *l_quantity;
 int *l_tax;
 ...
 (沢山)
 ...
} LARGE_TABLE_t;

LARGE_TABLE_t large_table;
large_table.l_quantity = (int*)malloc(sizeof(int) * 100000000);
...

さらに先ほどのデータ件数を数える関数は以下のようになります。

int select_count_gt_columnar(int n, LARGE_TABLE_t* tab, int* val)
{
 int cnt = 0, i;
 int v = *val;
 for (i=0; i<n; ++i) {
  if (tab.l_quantity[i] > v) {
   cnt++;
  }
 }
 return cnt;
}

先ほどと同じ条件とデータで実行してみます。

カラム型データ構造時のperfの結果




どうでしょう?

キャッシュミスが大きく減少しました(1/100程度)。それにともなって、CPT(1行を処理するのに必要な平均CPUサイクル数)やIPC(CPU命令の実行効率)が大きく改善されるSELECTIVITYがある事が分かりますね。

ただ、データ構造を変えただけだと、最初のロー型(を模したもの)に比べてSELECTIVITYによる分岐予測ミスが増減する傾向に変化はありません。(最終的には、CPTもSELECTIVITYによって悪化する傾向に変化はありません)

カラム型をイメージしつつブランチフリーなバージョン


次は、この分岐ミスをなくすようにコードを書き換えてみます。
(* SIMDに行く前に通らねばならない道ですので)

int select_count_gt_columnar_branch_free(int n, LARGE_TABLE_t* tab, int* val)
{
 int cnt = 0, i;
 int v = *val;
 for (i=0; i<n; ++i) {
  cnt += (tab.l_quantity[i] > v)
 }
 return cnt;
}

上記は、WHERE句の条件がTRUEであろうがFALSEであろうがcntという変数を加算します。ただしTRUEは1、FALSEは0という言語仕様を利用しています。これは、分岐せずに常に一定の処理(ここではcnt変数への加算)を実行するため、CPUインストラクション数は増加しますが、分岐ミスが減少し、分岐ミスによるパイプラインハザードのペナルティを払わない方がCPUサイクルを消費しないであろうとの仮定の上に成り立っています。

では、結果を見てみましょう。

カラム型データ構造かつブランチフリー時のperfの結果




どうでしょうか?SELECTIVITYに依存せず、どのような条件下でも一定のパフォーマンスを出すことが出来ていますね。分岐予測ミス恐ろしや。

カラム型をイメージしつつSIMD(SSE)なバージョン


それでは最後に、さらにパフォーマンスを上げたい(というか、さらに最適化させたい)ので、SIMDを使ったコードに書き換えてみます。
x86系プロセッサではSIMDとしてSSEやAVXがサポートされています。SSEは128ビット幅のレジスターを使っていて、AVXからは256ビット幅のレジスターを使えます。

では、SSE(128ビット幅)のコードを以下に示します。

int select_count_gt_sse(int n, LARGE_TABLE_t* tab, int *val)
{
 int cnt = 0, i;
 int v = *val;
 __m128i vec, cmp, msk;

 cmp = _mm_set1_epi32(v);
 for (i = 0; i<n; i+=4) {
  vec = _mm_load_si128((__m128i*)&tab.l_quantity[i]);
  msk = _mm_cmpgt_epi32(vec, cmp);
  cnt += _mm_popcnt_u32(_mm_movemask_ps((__m128)msk));
 }
 return cnt;
}

少しだけ説明すると
_mm_set1_epi32()
比較対象(ここでは50)のINT型を128ビットレジスターに登録します(INT型は32ビットなので、128ビットレジスターには4つ同時に格納されます)

_mm_load_si128()
比較元のデータを4つ分(INT(32ビット)を128ビット分)SIMDのレジスターに登録します。

_mm_cmpgt_epi32()
上記のレジスター2つを一気に比較します。その戻りは、128ビットレジスターに真なら32ビット分の0xff....f、偽なら0となります。

_mm_movemask_ps()
先ほどの32ビットマスクをINTに変換し、_mm_popcnt_u32()で、INT中に1のフラグが立っている数を数えます。

少々、見慣れませんが、まぁ、そんな感じです。

では、結果を見てみましょう。(基本的に他と変化のあるCPTとIPCのみチャート化)

カラム型データ構造かつSIMD(SSE)処理時のperfの結果


どうでしょう?
先ほどのブランチフリー版にくらべて、IPCが少し落ちていますが、CPU命令数が減少したおかげで、CPTが減少していることが分かりますね。

蛇足でSIMD(AVX2)なバージョン


おまけでAVX(正確にはAVX2の256ビット幅のレジスター)でも実験
* 実際に256ビット幅で整数を扱おうとするとAVX2が必要で、AVX2はHaswellからのサポートです。

int select_count_gt_avx(int n, LARGE_TABLE_t* tab, int *val)
{
 int cnt = 0, i;
 int v = *val;
 __m256i vec, cmp, msk;

 cmp = _mm256_set1_epi32(v);
 for (i = 0; i<n; i+=8) {
  vec = _mm256_loadu_si256((__m256i*)&tab.l_quantity[i]);
  msk = _mm256_cmpgt_epi32(vec, cmp);
  cnt += _mm_popcnt_u64(_mm256_movemask_ps((__m256)msk));
 }
 return cnt;
}

結果は以下のようになります。

カラム型データ構造かつSIMD(AVX)処理時のperfの結果



どうでしょう?
AVX2使用時はSSE時と比較してレジスター幅が倍増している恩恵をきちんと受けていることが分かりますね。

これで、本当に最後ですが、各処理(ロー型、カラム型、ブランチフリー、SSE、AVX2)での処理時間のチャートを示しておきます。


まとめめいたもの


少しだけ、まとめめいたものを書くとすると、大量のデータを効率良く処理するときには

  • CPU命令の効率を上げること
  • 1行処理するのにかかるCPUサイクルを少なくすること

を考えないといけない。

その時にネックになるのは、

  • CPUキャッシュミス
  • 分岐予測ミス

とか。

これらをなるべく排除する方法として

  • カラム型のデータ構造(これはIn-Memoryに特化した話ではないですが、ここでは、In-Memoryという流れで書いています)
  • 分岐予測をミスさせない(そもそも分岐させない)アルゴリズム
  • CPU命令自体を減少させてCPUサイクルを下げようとするSIMD命令

等があるというのが私の理解です。さらには、SIMDを使うには、そもそもデータ構造(ロー型とかカラム型)を最初に考えていないと効率よく使えないような気がしているし、SIMDを使って効率良く処理にするには分岐が沢山あっちゃいけない気もしています。

という事で、最初に上げたキーワード(In-Memory、カラム型、SIMD)は私の中では、かなりつながった感があります。

ちょっと長いエントリになってしまいましたが、私の理解するIn-Memoryとかカラム型とかSIMDとかの気持ちが分かってもらえたでしょうか?

それでは、皆様よいクリスマスを!

2013/12/25

12cでリソースの共有と非共有のはざまで... その3

I/Oリソース編


 これまで、マルチテナント・アーキテクチャにおけるCPUリソース、Memoryリソースの制御方法を考察してきたが最後にI/Oリソースの制御について考えてみる。

 Oracle Databaseは共有ディスクアーキテクチャーをとることから、従来からI/Oはボトルネックになりやすい重要なコンポーネントだった。マルチテナント・アーキテクチャにより、多くのインスタンスが集約されるとさらに、I/Oのリソース管理の重要性が増してくることは明らかである。

 さらに、初回のCPUリソース編でも述べたが、Database Resource ManagerではI/Oのリソース管理は提供されておらず(*1)I/Oリソース管理をするにはExadataによりI/O Resource Managerを使用する必要がある。

 筆者はExadataを検証環境として持ち合わせていないので、ここでExadata I/O Resource Managerの検証結果を記載できない。

 そこで、CPUリソース編で説明したOSネイティブなリソースマネージャ(*2)を使用したI/Oリソースの管理の方法を紹介したい。

(*1) Runaway SessionとしてのI/Oリソース管理は提供されている
(*2) Linuxのcgroupsと初期化パラメーターPROCESSOR_GROUP_NAMEを使用する


I/Oリソース制御としてPROCESSOR_GROUP_NAME


 初回のCPUリソース編で初期化パラメーターPROCESSOR_GROUP_NAMEを紹介したが、これはPROCESSORに限らずOSの持つcgroups(本件検証環境はOracle Linux 6.4 x86_64)の全ての機能を使用できる。


注意
cgroupsの機能をOracle Database 12cがどこまでサポートしているか明確なドキュメントはなく、現時点は検証目的としてPROCESSOR_GROUP_NAMEでcgroupsのI/O制御を行っている事に注意

 まず、I/Oリソースに関して全く制御していない状況で、I/Oの速度を計測してみる。I/Oの速度計測にはOracleが提供しているDBMS_RESOURCE_MANAGER.CALIBRATE_IOプロシージャを使用する

set serveroutput on

declare
 l_latency   integer;
 l_iops      integer;
 l_mbps      integer;
begin
 dbms_resource_manager.calibrate_io (
  num_physical_disks => 12, /* # of disks */
  max_latency => 10,  /* max latency */
  max_iops => l_iops,  /* I/O Ops/sec */
  max_mbps => l_mbps,  /* MBytes/sec */
  actual_latency => l_latency /* actual latency */
 );
 dbms_output.put_line ('I/O Ops/sec = ' || l_iops);
 dbms_output.put_line ('Actual Latency = ' || l_latency);
 dbms_output.put_line('MB/sec = ' || l_mbps);
end;
/

 上記のmax_iopsはDB_BLOCK_SIZEのI/Oサイズ読み込みでの最大IOPSを表示する。検証環境のDB_BLOCK_SIZEは8KBとなっている。
また、actual_latencyもmax_iops同様にDB_BLOCK_SIZEのI/Oサイズ読み込みでの平均レイテンシー(ミリ秒)を表示する。

 さらにmax_mbpsは、I/Oサイズ1MB読み込みでの最大スループット(MB/秒)を表示する。

 まず、ハードウェアのベースラインを確認するため、cgroupsでI/Oリソースの制限をかけていない状況でI/O速度を計測してみる。


 続いて、cgroupsでI/O制御を行ってみるが、cgroupsの場合、I/Oリソースの制御はblkioで行う。blkioでは様々なI/Oリソースの制御が可能だが、今回はblkio.throttle.read_bps_deviceで行う。cgroupsのblkioで様々な制御が可能だが、詳細はOSのドキュメントを参照してもらいたい。

 今回のI/Oスループット(bps)の制御には、デバイス毎に上限値となるスループット(バイト)を設定する必要がある。今回はASMを使って複数デバイスでディスクグループを作成しているので、"ターゲットとなるスループット/ASMのディスクグループを構成するデバイス数"を各デバイスに設定していくことになる。

 イメージだけつかんでもらうためにサンプルのスクリプトを記載しておく。



#!/bin/sh

GROUP_NAME=IQCDB02

echo PROCESSOR_GROUP_NAME
echo "  ${GROUP_NAME}"

#exit code
ExitSuccess=0
ExitError=1
ExitSQLError=3

function execSQL() {
 local sqlStmt=${1}
 local retCode=-1
 local SID=$(ps -ef | grep +ASM | grep -i pmon | awk {'print $8'} | sed -e 's/asm_pmon_//g')
 local EUSER=$(ps -eo "euser,args" | grep +ASM | grep -i pmon | awk '{print $1}')
 local resultSet=$(su - ${EUSER} <<- _END_OF_SQL_ | grep -v ^$
  export ORACLE_SID=${SID}
  export ORAENV_ASK=NO

  . oraenv <<- _EOF > /dev/null
  _EOF


  export LANG=C
  export NLS_LANG=American_America.us7ascii

  sqlplus -s -L / as sysasm
   set head off
   set feed off
   set echo off
   set lin 500
   set pages 1000
   set null #
   whenever sqlerror exit ${ExitSQLError}
   ${sqlStmt}
   exit
  _END_OF_SQL_
  retCode=${PIPESTATUS[0]}
 )

 local errCnt=$(echo "${resultSet}" | grep -c ^ORA-)

 if [[ ${errCnt} -ne 0 ]]
 then
  echo "${resultSet}" | grep ^ORA- | while read Line
  do
   echo ${Line}
  done
  retCode=${ExitSQLError}
 else
  echo "${resultSet}" | grep -v ^$
  retCode=${ExitSuccess}
 fi

 return ${retCode}
}

function getMajor() {
 local dev=`basename ${1}`
 cat /proc/partitions | grep -E "${dev}$" | awk '{print $1}'
}

function getMinor() {
 local dev=`basename ${1}`
 cat /proc/partitions | grep -E "${dev}$" | awk '{print $2}'
}

function calcBandwidth() {
 local disks=${1}
 expr ${LimitedBW} / ${disks}
}

function blkio_bps() {
 sqlStmt="select d.path
    from v\$asm_disk d, v\$asm_diskgroup g
    where g.group_number=d.group_number
    and g.name=upper('${DG}');"
 resultSet=`execSQL "${sqlStmt}"`

 echo blkio.throttle.read_bps_device
 REC_COUNT=`echo "${resultSet}" | wc -l`
 BW_DEV=`calcBandwidth ${REC_COUNT}`
 echo "  ${REC_COUNT} devices"
 echo "  ${BW_DEV} bytes/deivce"

 for asm in ${resultSet[@]}
 do
  dev=`readlink -f ${asm}`
  mjr=`getMajor ${dev}`
  mnr=`getMinor ${dev}`
  echo "${mjr}:${mnr} ${BW_DEV}" > /mnt/cgroup/${GROUP_NAME}/blkio.throttle.read_bps_device
 done
}

function cpus() {
 NODES=`cat /mnt/cgroup/cpuset.mems`
 CPU_LIST=$(echo $CPUS | awk -F"," '{$1=$1; print}')

 echo cpuset.cpus
 echo "  ${CPU_LIST}"
 echo ${CPU_LIST} > /mnt/cgroup/${GROUP_NAME}/cpuset.cpus
 echo cpuset.mems
 echo "  ${NODES}"
 echo ${NODES} > /mnt/cgroup/${GROUP_NAME}/cpuset.mems
}

DG=""
LimitedBW=""
CPUS=""
while getopts "d:b:c:" GETOPTS
do
 case ${GETOPTS} in
  "d")
   DG=${OPTARG}
   ;;
  "b")
   LimitedBW=${OPTARG}
   ;;
  "c")
   CPUS=${OPTARG}
   ;;
 esac
done

if [ ! -d /mnt/cgroup ]; then
 mkdir /mnt/cgroup
fi

if [ `mount | grep /mnt/cgroup | wc -l` -eq 0 ]; then
 mount -t cgroup cgroup /mnt/cgroup
 chown -R oracle:oinstall /mnt/cgroup
 if [ ! -d /mnt/cgroup/${GROUP_NAME} ]; then
  mkdir /mnt/cgroup/${GROUP_NAME}
  chown -R oracle:oinstall /mnt/cgroup/${GROUP_NAME}
 fi
fi

if [ ! -z ${CPUS} ]; then
 cpus
 if [ "${DG}" != "" -a  "${LimitedBW}" != "" ]; then
  blkio_bps
 fi
fi

 今回はASMLibを使わず、udevによるシンボリックリンクでデバイスを設定しているので、シンボリックリンク先のデバイスを探すような処理が入っている。環境により物理デバイス(/dev/sdaなど)のメジャー番号、マイナー番号を取得する処理に変更が必要。

 それでは、2つ存在するコンテナ・データベースの内IQCDB02のみ100MB/sでスループット制限をかける。先ほどのスクリプトを使用して、ASMのディスクグループ"DATA"に対して、100MB/sで制限し、CPUは全CPU(CPUIDを0番から15番、つまり全16CPUコア)を許可する設定を行う。

# ./io_cgroup_manager.sh -d data -b 104857600 -c 0-15
PROCESSOR_GROUP_NAME
  IQCDB02
cpuset.cpus
  0-15
cpuset.mems
  0
blkio.throttle.read_bps_device
  12 devices
  8738133 bytes/deivce
# su - oracle
$ export ORACLE_SID=IQCDB02
$ sqlplus / as sysdba
SQL> alter system set processor_group_name='IQCDB02' scope=spfile;
SQL> shutdown immediate
SQL> startup
SQL> alter pluggable database all open;

 ここで、PROCESSOR_GROUP_NAME='IQPDB02'を設定してあるコンテナ・データベースIQCDB02とPROCESSOR_GROUP_NAMEを設定していないコンテナ・データベースIQCDB01で先ほどのCALIBRATE_IOプロシージャを実行してI/Oリソースの状況を比較してみる。


 上記では、スループット制限を100MB/sと設定したが88MB/sという結果になった。これはASMがI/Oを各デバイスに分散するが、その際、I/Oが完全に均等にならない、または何らかのオーバーヘッドがある事が原因だと思われる。しかしながら、I/Oリソースを制御するという観点から、多少の誤差はあるものの十分機能していると考える。


I/Oリソースを制御するといった観点でのまとめ


 Linuxのcgroupsと初期化パラメーターRESOURCE_GROUP_NAMEでのI/Oリソース制御はインスタンス単位(本検証ではコンテナ・データベース単位)での制御となるが、データベースの設計次第で十分使えると思う。

 前回のMemoryリソース編でも述べたが、マルチテナント・アーキテクチャでは、ワークロード種別やサービスレベル別で複数のコンテナ・データベースを作成し、そのコンテナ・データベース毎にプラガブル・データベースを作成し、データベースの集約を図るのが、筆者の思うベストプラクティスである。

 今回のLinux cgroupsと初期パラメーターPROCESSOR_GROUP_NAMEを使用する場合、プラガブル・データベース単位でのI/Oリソースの制御はできない。しかし、DWH用コンテナ・データベースには高スループット、開発用コンテナ・データベースには低スループット等の設定が可能となる。これにより、完璧ではないが、最低限のI/Oリソースの制御が可能であると思う。

12cでリソースの共有と非共有のはざまで... その2

Memoryリソース編

 前回CPUリソース編として、Oracle Database 12cのマルチテナント・アーキテクチャにおけるCPUリソースの制御の様子を検証してみた。今回は、Oracle Databaseで重要なコンポーネントであるSGA(System Global Area)を含むMemoryリソースの制御の様子を見てみたい。

 Oracle Databaseはインスタンス単位でSGAをもち、SGA内のコンポーネント(つまりMemoryリソース)の配分は、DBAが手動もしくは、Oracle Databaseによる自動管理で行うことが従来より可能であった。ここでのポイントは、Memoryリソースはインスタンス単位である点であり、プラガブル・データベース単位での制御はないということである。

 結論を先に書くと、Database Resource Managerや従来のDBAによる手動もしくはOracle Databaseによる自動によるMemory管理では、プラガブル・データベース毎にMemoryリソースを制御することは不可能である。

 本検証では、Memoryリソース管理が不可能である点を踏まえ、マルチテナント・アーキテクチャを考える際に注意しなければいけない(であろう)事を検証してみる。


 まず、マルチテナント・アーキテクチャにおけるSGAの仕組みを簡単に見てみる。


 この図から、SGAはコンテナ・データベースが管理し、プラガブル・データベース固有のSGAは存在しないことが分かる。

 さらに、重要なのは従来からSGAコンポーネントの中での鬼門であったSHARED POOLもコンテナ・データベースに1つしか存在しないという事をである。これは、1つのプラガブル・データベースでSHARED POOLの枯渇を誘発するような処理(例えばバインド変数を使用しないSQLが大量に実行されている等)により、他の問題のないプラガブル・データベースでエラー(ORA-4031など)が発生するといった悪影響に関する懸念が残る。

 今回は、このSHARED POOL、特にLIBRARY CACHE周りの動きを少し検証してみようと思う。

LIBRARY CACHE内のカーソルは誰のもの?

 SHARED POOL内のコンポーネントの中でも、SQL文の解析情報を格納するLIBRARY CACHEは特に重要なコンポーネントになるわけだが、そもそも、マルチテナント・アーキテクチャをとる場合、このLIBRARY CACHEにまつわる管理方法は従来とどう変わったのか少し見ておきたい。

 カーソルが誰のものか確認するため、いくつかSQLを実行して、その後のLIBRARY CACHEのダンプを取得する。

  1. PDB1のKSHINKUB.DUALテーブルにSQLを実行
  2. PDB2のKSHINKUB.DUALテーブルに上記と同一のSQLを実行
  3. コンテナ・データベース上でLIBRARY CACHEのダンプを取得
 以下に取得したダンプの抜粋を記載する。


Bucket: #=68414 Mutex=0x10540ea4d8(270582939648, 6, 0, 6)
  LibraryHandle:  Address=0x1067379978 Hash=5aa90b3e LockMode=N PinMode=0 LoadLockMode=0 Status=VALD
    ObjectName:  Name=select /* Tokuno JPOUG */ 1 from dual
      FullHashValue=4a990487ce5893eba2a8a5285aa90b3e Namespace=SQL AREA(00) Type=CURSOR(00) ContainerId=1 ContainerUid=0 Identifier=1521027902 OwnerIdn=106
    Statistics:  InvalidationCount=0 ExecutionCount=2 LoadCount=3 ActiveLocks=1 TotalLockCount=2 TotalPinCount=1
    Counters:  BrokenCount=1 RevocablePointer=1 KeepDependency=2 Version=0 BucketInUse=1 HandleInUse=1 HandleReferenceCount=0
    Concurrency:  DependencyMutex=0x1067379a28(0, 2, 0, 0) Mutex=0x1067379ac0(63, 33, 0, 6)
    Flags=RON/PIN/TIM/PN0/DBN/[10012841]
    WaitersLists:
      Lock=0x1067379a08[0x1067379a08,0x1067379a08]
      Pin=0x10673799e8[0x10673799e8,0x10673799e8]
      LoadLock=0x1067379a60[0x1067379a60,0x1067379a60]
    Timestamp:  Current=08-12-2013 21:59:15
    HandleReference:  Address=0x1067379b58 Handle=(nil) Flags=[00]
    ReferenceList:
      Reference:  Address=0x1066f3d6e8 Handle=0x1016c8c820 Flags=ROD[21]
      Reference:  Address=0x1066e9c730 Handle=0x10673f07c8 Flags=ROD[21]
    LibraryObject:  Address=0x10673f0a88 HeapMask=0000-0001-0001-0000 Flags=EXS[0000] Flags2=[0000] PublicFlags=[0000]
      DataBlocks:
        Block:  #='0' name=KGLH0^5aa90b3e pins=0 Change=NONE
          Heap=0x1016c8c640 Pointer=0x10673f0b50 Extent=0x10673f09f8 Flags=I/-/P/A/-/-
          FreedLocation=0 Alloc=3.187500 Size=3.976562 LoadTime=4537161860
      ChildTable:  size='16'
        Child:  id='0' Table=0x10673f1920 Reference=0x10673f1388 Handle=0x106735bc10
        Child:  id='1' Table=0x10673f1920 Reference=0x10673f1658 Handle=0x106744c510

 上記は親カーソルに該当する部分のダンプとなるが、3行目のContainerId=1に注目してもらいたい。親カーソルは常にCDB$ROOTがオーナーになっている。さらに、最終行付近のChildTableでは、関連する子カーソルが2つ存在することが示されている。

1番目の子カーソルのダンプ

Child:  childNum='0'
          LibraryHandle:  Address=0x106735bc10 Hash=0 LockMode=N PinMode=0 LoadLockMode=0 Status=VALD
            Name:  Namespace=SQL AREA(00) Type=CURSOR(00) ContainerId=4
            Statistics:  InvalidationCount=0 ExecutionCount=1 LoadCount=1 ActiveLocks=1 TotalLockCount=1 TotalPinCount=2
            Counters:  BrokenCount=1 RevocablePointer=1 KeepDependency=0 Version=0 BucketInUse=0 HandleInUse=0 HandleReferenceCount=0
            Concurrency:  DependencyMutex=0x106735bcc0(0, 0, 0, 0) Mutex=0x1067379ac0(63, 33, 0, 6)
            Flags=RON/PIN/PN0/EXP/CHD/[10012111]
            WaitersLists:
              Lock=0x106735bca0[0x106735bca0,0x106735bca0]
              Pin=0x106735bc80[0x106735bc80,0x106735bc80]
              LoadLock=0x106735bcf8[0x106735bcf8,0x106735bcf8]
            ReferenceList:
              Reference:  Address=0x10673f1388 Handle=0x1067379978 Flags=CHL[02]
            LibraryObject:  Address=0x1067405ba0 HeapMask=0000-0001-0001-0000 Flags=EXS[0000] Flags2=[0000] PublicFlags=[0000]
              Dependencies:  count='4' size='16' table='0x10674069c8'
                Dependency:  num='0'
                  Reference=0x1067406210 Position=0 Flags=DEP[0001]
                  Handle=0x10575b4a98 Type=NONE(255) Parent=PDB3.KSHINKUB
                Dependency:  num='1'
                  Reference=0x1067406260 Position=33 Flags=DEP[0001]
                  Handle=0x1056b7cab0 Type=CURSOR(00) Parent=PDB3.KSHINKUB.DUAL
                Dependency:  num='2'
                  Reference=0x10674062a0 Position=33 Flags=DEP[0001]
                  Handle=0x101734c0d8 Type=SYNONYM(05) Parent=PDB3.PUBLIC.DUAL
                Dependency:  num='3'
                  Reference=0x10674062e0 Position=33 Flags=DEP[0001]
                  Handle=0x105744ff30 Type=TABLE(02) Parent=PDB3.SYS.DUAL
              ReadOnlyDependencies:  count='1' size='16'
                ReadDependency:  num='0' Table=0x1067406a60 Reference=0x1067406110 Handle=0x10673f07c8 Flags=DEP/ROD/KPP[61]
              Accesses:  count='1' size='16'
                Dependency:  num='3' Type=0009
              Translations:  count='1' size='16'
                Translation:  num='0' Original=0x101734c0d8 Final=0x105744ff30
              DataBlocks:
                Block:  #='0' name=KGLH0^5aa90b3e pins=0 Change=NONE
                  Heap=0x1066eac258 Pointer=0x1067405c68 Extent=0x1067405b10 Flags=I/-/P/A/-/-
                  FreedLocation=0 Alloc=2.750000 Size=3.937500 LoadTime=4537161860
                Block:  #='6' name=SQLA^5aa90b3e pins=0 Change=NONE
                  Heap=0x10673f1168 Pointer=0x103f390598 Extent=0x103f38f978 Flags=I/-/-/A/-/E
                  FreedLocation=0 Alloc=4.828125 Size=7.898438 LoadTime=0
            NamespaceDump:
              Child Cursor:  Heap0=0x1067405c68 Heap6=0x103f390598 Heap0 Load Time=08-12-2013 21:59:15 Heap6 Load Time=08-12-2013 21:59:15

2番目の子カーソルのダンプ

Child:  childNum='1'
          LibraryHandle:  Address=0x106744c510 Hash=0 LockMode=0 PinMode=0 LoadLockMode=0 Status=VALD
            Name:  Namespace=SQL AREA(00) Type=CURSOR(00) ContainerId=3
            Statistics:  InvalidationCount=0 ExecutionCount=1 LoadCount=1 ActiveLocks=0 TotalLockCount=1 TotalPinCount=2
            Counters:  BrokenCount=1 RevocablePointer=1 KeepDependency=0 Version=0 BucketInUse=0 HandleInUse=0 HandleReferenceCount=0
            Concurrency:  DependencyMutex=0x106744c5c0(0, 0, 0, 0) Mutex=0x1067379ac0(63, 33, 0, 6)
            Flags=RON/PIN/PN0/EXP/CHD/[10012111]
            WaitersLists:
              Lock=0x106744c5a0[0x106744c5a0,0x106744c5a0]
              Pin=0x106744c580[0x106744c580,0x106744c580]
              LoadLock=0x106744c5f8[0x106744c5f8,0x106744c5f8]
            ReferenceList:
              Reference:  Address=0x10673f1658 Handle=0x1067379978 Flags=CHL[02]
            LibraryObject:  Address=0x106731a248 HeapMask=0000-0001-0001-0000 Flags=EXS[0000] Flags2=[0000] PublicFlags=[0000]
              Dependencies:  count='4' size='16' table='0x106731b070'
                Dependency:  num='0'
                  Reference=0x106731a8b8 Position=0 Flags=DEP[0001]
                  Handle=0x10576ab810 Type=NONE(255) Parent=PDB2.KSHINKUB
                Dependency:  num='1'
                  Reference=0x106731a908 Position=33 Flags=DEP[0001]
                  Handle=0x10673c7e30 Type=CURSOR(00) Parent=PDB2.KSHINKUB.DUAL
                Dependency:  num='2'
                  Reference=0x106731a948 Position=33 Flags=DEP[0001]
                  Handle=0x1066ea55b8 Type=SYNONYM(05) Parent=PDB2.PUBLIC.DUAL
                Dependency:  num='3'
                  Reference=0x106731a988 Position=33 Flags=DEP[0001]
                  Handle=0x1024f289e8 Type=TABLE(02) Parent=PDB2.SYS.DUAL
              ReadOnlyDependencies:  count='1' size='16'
                ReadDependency:  num='0' Table=0x106731b108 Reference=0x106731a7b8 Handle=0x1016c8c820 Flags=DEP/ROD/KPP[61]
              Accesses:  count='1' size='16'
                Dependency:  num='3' Type=0009
              Translations:  count='1' size='16'
                Translation:  num='0' Original=0x1066ea55b8 Final=0x1024f289e8
              DataBlocks:
                Block:  #='0' name=KGLH0^5aa90b3e pins=0 Change=NONE
                  Heap=0x1066e9c2c0 Pointer=0x106731a310 Extent=0x106731a1b8 Flags=I/-/-/A/-/-
                  FreedLocation=0 Alloc=2.750000 Size=3.937500 LoadTime=4537166160
                Block:  #='6' name=SQLA^5aa90b3e pins=0 Change=NONE
                  Heap=0x10673f14f8 Pointer=0x103f38e598 Extent=0x103f38d978 Flags=I/-/-/A/-/E
                  FreedLocation=0 Alloc=4.828125 Size=7.898438 LoadTime=0
            NamespaceDump:
              Child Cursor:  Heap0=0x106731a310 Heap6=0x103f38e598 Heap0 Load Time=08-12-2013 21:59:19 Heap6 Load Time=08-12-2013 21:59:19


 子カーソルのダンプから親カーソル同様に、ContainerIdを見ると実行されたプラガブル・データベースがオーナーとなっていることが分かる。さらに、カーソルに依存するオブジェクトとして[プラガブル・データベース名].[スキーマ名].[オブジェクト名]という表記で存在することも分かる。

 つまり、(誤解を恐れずに言うと)マルチテナント・アーキテクチャにおいて、プラガブル・データベースで実行されるSQL(カーソル)は従来のスキーマの拡張として扱われていることになる。


 ちなみに、この時、コンテナ・データベース上でV$SQL_SHARED_CURSORで子カーソルが生成された理由を見るとHASH_MATCH_FAILEDとなっていた。マニュアルによれば、「既存の子カーソルに、現在のカーソルに必要な安全でないリテラル・バインド・ハッシュ値がない」という何とも意味不明な原因なのだが、これは、CURSOR_SHARING時にリテラルをバインド変数に変換しようとしたが、そのリテラルが安全にバインド変数化できない可能性があるので、そのまま(新規に子カーソルを作成して)実行した。といった場合に多く見られる。今回のマルチテナント・アーキテクチャで、AUTH_CHECK_MISMATCHやTRANSLATION_MISMATCHではなく、HASH_MATCH_FAILEDとなるのは興味深い。

 親カーソルはコンテナ・データベースをオーナーとしてプラガブル・データベース間で共有するが、子カーソルはプラガブル・データベースをオーナーとして別スキーマで実行されたという扱いに見える。マルチテナント・アーキテクチャのMemory管理は、従来と同じ(ような)枯れたアーキテクチャであり、今まで通りの信頼性が担保できそうだが、逆に、従来から問題だった点にも十分注意が必要だという事だと思う。

 冒頭、ORA-4031の危険性を述べたが、クラウドでのマルチテナント・アーキテクチャを考えた時、1つのプラガブル・データベースの挙動で、コンテナ・データベース内の全てのプラガブル・データベースの動作に影響を与える可能性がある。次に、実際の1つのプラガブル・データベースでSHARED POOL不足を発生させ、その影響を見てみる事にする。


いざ、ORA-4031へ


 今回は、SHARED POOL不足(ORA-4031)を発生させやすい状況を作るため、プラガブル・データベースを2つもつコンテナ・データベースの初期化パラメーターMEMORY_TARGETを4GBと設定した。1つのコンテナ・データベースで以下のコードを実行する。



create or replace procedure proc_4031(p_depth in number, p_com in number default 1)
is
 v_cursor sys_refcursor;
 v_sql  varchar2(30000);
begin
 v_sql := 'select /* '||p_com||' */ 1 ' || rpad(' ', 4000) || rpad(' ', 4000) || rpad(' ', 4000) || rpad(' ', 4000) || rpad(' ', 4000) || rpad(' ', 4000) || rpad(' ', 4000) || 'from dual a_' || p_depth;
 open v_cursor for v_sql;
 proc_4031(p_depth+1, p_com);
end;
/

alter system set open_cursors = 65535 scope=memory;

exec proc_4031(1)

 上記のPL/SQLを実行すると運が良ければ(?)、SHARED POOLが枯渇しORA-4031が発生する。ここで、別のプラガブル・データベースでも同様にORA-4031が発生することが確認できる。(ただし、タイミングに依存する)

 この別プラガブル・データベースによる不安定な処理(SHARED POOL不足を誘発する処理)の影響は、他のプラガブル・データベースだけにとどまらないであろうことも、先のメモリー構造を見れば予測できる。つまり、コンテナ・データベースにも影響は波及するであろうということである。

 1つのプラガブル・データベースが1つのSGA内のコンポーネントを大量に確保している場合、仮にコンテナ・データベースのバックグランド・プロセスがORA-4031の被害を受けると、その影響はインスタンスダウンにもつながる重大なものになる。

 また、コンテナ・データベースの運用における、全プラガブル・データベースのダウンといった危険性は、リソース制御といった観点とは若干異なるので、ここでは述べないが、今後、十分な検証が必要であると思う。

各種ラッチのリソース不足も見過ごせない


 Oracle Database上では、SGAの共有リソースの排他制御のために各種ラッチを使って、そのリソースの同時実行を制御している。今まで述べてきたSHARED POOLの場合、shared pool latchを使ってSHARED POOL上のオブジェクトの排他制御をしている。このshared pool latchの数はCPU数やSHARED POOLのサイズにより異なるが有限のリソースとなる。

 筆者の環境ではshared pool latchは4つとなっていた。


SQL> col param for a20
SQL> col sessionval for a10
SQL> col instanceval for a10
SQL> col descr for a30
SQL>
SQL> SELECT a.ksppinm Param
  2        ,b.ksppstvl SessionVal
  3        ,c.ksppstvl InstanceVal
  4        ,a.ksppdesc Descr
  5  FROM   x$ksppi a ,
  6         x$ksppcv b ,
  7         x$ksppsv c
  8  WHERE  a.indx = b.indx AND
  9         a.indx = c.indx AND
 10         a.ksppinm like '/_kghdsidx_count' escape '/'
 11  ORDER BY 1;

PARAM                SESSIONVAL INSTANCEVA DESCR
-------------------- ---------- ---------- ------------------------------
_kghdsidx_count      4          4          max kghdsidx count


 この有限のリソースを使ってデータベース内のオブジェクトを管理している点も従来のデータベースと同じである。しかし、マルチテナント・アーキテクチャは多くのプラガブル・データベースを1つのコンテナ・データベースに集約することを目的の一つにしているにも関わらず、ラッチのリソースの上限は従来と変わらない構造となっている。これらラッチのリソース不足も懸念材料の一つだと思われる。

Memoryリソースを考慮したコンテナ・データベースの設計


 マルチテナント・アーキテクチャに限った事ではないが、データベースを集約して1つのインスタンスに多様なワークロードをかける場合、Memoryリソースの取り扱いには注意が必要だ。特にマルチテナント・アーキテクチャの場合、データベースの集約が大きな目的の1つになるので、これまで以上に多用なワークロードが発生する事を想定しておく必要がある。

 筆者は、1つのデータベースに全てを集約するのではなく、ある程度の単位、例えば、本番OLTP系システム、本番DW系システム、開発系システム等の単位でコンテナ・データベースを分ける方が良いのではないかと思う。

 ある程度分けられたコンテナ・データベースの中にいくつかのプラガブル・データベースを構築することで、ワークロードの分離やサービスレベルの維持など柔軟な運用が可能になると思う。

 本番OLTP系システムにはインスタンス・ケージングでCPUを全体の30%割り当て、Memoryはデータベース内のMEMORY_TARGETで全体の60%を割り当てる。同様に本番DW系システム、開発系システムにはCPUをそれぞれ、60%、10%、Memoryを30%、10%といった具合だ。その後、各プラガブル・データベースにはCDBリソースプランを適用していく。

 ここまでくると、I/OリソースもMemoryリソース同様に制御しなくていけない事は明白だが、次回でI/Oリソースの制御について考えてみたい。