OceanBaseデータベースはマルチテナントデータベースシステムであり、テナント間でのリソース競合を防ぎ、業務の安定した運用を保証するために、テナント間のリソースが分離されています。
OceanBaseデータベースでは、ユニットをテナントへのリソース割り当ての基本単位としています。1つのユニットは、Dockerコンテナに例えることができます。1つのノード上には複数のユニットを作成でき、ノード上でユニットを作成するたびに、そのノードのCPU、メモリなどの物理リソースの一部を占有します。ノードのリソース割り当て状況は内部テーブルに記録され、DBAが確認できます。
1つのテナントは複数のノード上に複数のユニットを配置できますが、あるノード上においては1つのテナントにつき1つのユニットしか存在できません。テナント内の複数のユニットは相互に独立しており、OceanBaseデータベースは現在、複数のユニットのリソース使用量を集計してグローバルなリソース制御を行う仕組みは備えていません。具体的には、あるテナントが特定のノード上でリソースを満たせない場合でも、別のノードで他のテナントのリソースを奪うようなことは発生しません。
いわゆるリソース分離とは、ノードがローカルの複数のユニット間でリソースを制御する行為であり、それはノードのローカルな動作です。類似の技術としてDockerや仮想マシンがありますが、OceanBaseデータベースはDockerや仮想マシン技術に依存せず、データベース内部でリソース分離を実現しています。
OceanBaseデータベースのテナント隔離の利点
Dockerや仮想マシンと比較して、OceanBaseデータベースのテナント隔離はより軽量であり、優先順位などの高度な機能を実装しやすいです。 OceanBaseデータベースの要件から見ると、Dockerや仮想マシンには主に以下の問題があります:
Dockerまたは仮想マシンの実行環境のオーバヘッドが大きく、OceanBaseデータベースは軽量級のテナントをサポートする必要があります。
Dockerまたは仮想マシンのスペック変更および移行にかかるオーバヘッドが大きく、OceanBaseデータベースではテナントのスペック変更と移行を可能な限り迅速に行いたいと考えています。
Dockerまたは仮想マシンでは、テナント間のリソース共有(例:オブジェクトプールの共有)が容易ではありません。
Dockerまたは仮想マシンのリソース隔離はカスタマイズが難しく、例えばテナント内での優先順位サポートが困難です。
さらに、Dockerや仮想マシンの実装では統一されたビューを提供することが難しいです。
一般ユーザーの視点から見た分離効果
一般ユーザーの視点から見た分離効果は以下のとおりです:
メモリは完全に分離されています。
具体的には以下が含まれます:
SQL実行プロセスにおける各演算子が使用するメモリは分離されており、あるテナントのメモリ枯渇が別のテナントに影響を与えることはありません。
SQL実行プロセスの各演算子の詳細については、ユーザーインターフェースとクエリ言語の章を参照してください。
Block CacheとMemTableは分離されており、あるテナントのメモリ枯渇が別のテナントの書き込みや読み取りに影響を与えることはありません。
Block Cacheの詳細については、マルチレベルキャッシュを参照してください。
MemTableの詳細については、MemTableを参照してください。
CPUはユーザー空間スケジューリングによって分離されます。
あるテナントが使用できるCPUリソースは、Unit仕様によって決定されます。
より良い分離効果を得るため、V4.0.0バージョン以降では、cgroupを設定してCPU分離を行うことがサポートされています。
異なるテナントのSQLモジュールは互いに影響しません。
具体的には以下が含まれます:
SQLのPlan Cacheはテナントごとに分離されており、あるテナントのPlan Cacheのフラッシュが別のテナントに影響を与えることはありません。
SQLのAuditテーブルは分離されており、あるテナントのQPSが高すぎても、別のテナントのAudit情報がフラッシュされることはありません。
異なるテナントのトランザクションモジュールは互いに影響しません。
具体的には以下が含まれます:
あるテナントの行ロックの一時停止は、他のテナントに影響しません。
あるテナントのトランザクションの一時停止は、他のテナントに影響しません。
あるテナントのリリースに関する問題は、他のテナントに影響しません。
異なるテナントのCLOGモジュールは互いに影響しません。
ログストリームのディレクトリはテナント単位であり、最終的にディスク上での実装は以下のように表現されます:
tenant_id ├── unit_id_0 │ ├── log │ └── meta ├── unit_id_1 │ ├── log │ └── meta └── unit_id_2 ├── log └── meta
リソースの分類
設計上、OceanBaseデータベースはV4.0.0バージョンからテナント間のCPU、メモリ、IOPSの分離をサポートしています。1つのUnitでは、CPU、メモリ、IOPS、log_disk_sizeの4種類のリソースを指定できます。
obclient> CREATE RESOURCE UNIT name
MAX_CPU = 1, [MIN_CPU = 1,]
MEMORY_SIZE = '5G',
[MAX_IOPS = 1024, MIN_IOPS = 1024, IOPS_WEIGHT=0,]
[LOG_DISK_SIZE = '2G'];
ノードの利用可能なCPU
ノード起動時に物理マシンまたはコンテナのオンラインCPU数を検出しますが、ノードの検出が不正確な場合(例:コンテナ環境など)、cpu_count構成パラメータで指定することもできます。
ノードはバックグラウンドスレッド用に2つのCPUを予約するため、テナントに実際に割り当て可能なCPUの総数は2つ減ります。
ノードの利用可能なメモリ
ノード起動時に物理マシンまたはコンテナのメモリを検出します。ノードは他のプロセス用にメモリの一部を予約する必要があるため、observerプロセスの利用可能なメモリは物理メモリ * memory_limit_percentageに等しくなります。memory_limit構成パラメータを使用して、observerプロセスが使用可能なメモリサイズを直接設定することもできます。
observerプロセスが使用可能なメモリから、内部共有モジュールが使用するメモリ分をさらに差し引く必要があります。このメモリサイズはsystem_memory構成パラメータで指定され、残りのメモリがテナントが使用可能な総メモリとなります。
メモリ設定の詳細については、メモリ管理の章を参照してください。
各ノードの利用可能なリソースの確認
oceanbase.GV$OB_SERVERSビューを使用して、各ノードの利用可能なリソースを確認できます。例:
obclient> SELECT * FROM oceanbase.GV$OB_SERVERS\G
*************************** 1. row ***************************
SVR_IP: 172.xx.xxx.xxx
SVR_PORT: 2882
ZONE: zone1
SQL_PORT: 2881
CPU_CAPACITY: 64
CPU_CAPACITY_MAX: 64
CPU_ASSIGNED: 8
CPU_ASSIGNED_MAX: 8
MEM_CAPACITY: 75161927680
MEM_ASSIGNED: 18253611008
LOG_DISK_CAPACITY: 179583320064
LOG_DISK_ASSIGNED: 56103010304
LOG_DISK_IN_USE: 1409286144
DATA_DISK_CAPACITY: 179593805824
DATA_DISK_ASSIGNED: NULL
DATA_DISK_IN_USE: 207618048
DATA_DISK_HEALTH_STATUS: NORMAL
MEMORY_LIMIT: 107374182400
DATA_DISK_ALLOCATED: 179593805824
DATA_DISK_ABNORMAL_TIME: NULL
SSL_CERT_EXPIRED_TIME: NULL
SS_DATA_DISK_OPERATION_SUGGESTED: NULL
SS_DATA_DISK_SIZE_SUGGESTED: NULL
1 row in set
リソース分離
メモリの分離
ノードのメモリリソースは実際にはオーバーコミットをサポートしておらず、メモリのオーバーコミットを導入するとテナントの動作が不安定になることがあります。V4.0.0バージョン以降、OceanBaseデータベースではメモリのオーバーコミットはサポートされなくなりました。MIN_MEMORY および MAX_MEMORY 設定は廃止され、代わりに MEMORY_SIZE パラメータが採用されています。
CPUの分離
OceanBaseデータベースV3.1.xバージョン以前では、主にスレッド数を制御することでCPU使用率を調整していました。OceanBaseデータベースV3.1.x以降のバージョンでは、cgroupを設定してCPU使用率を制御できるようになりました。
スレッドの分類
ノードは多様な機能を持つスレッドを多数起動します。詳細な分類については、observerスレッドを参照してください。この節では、最も大まかな基準に従って、以下の2つのカテゴリに分けます:
一つはSQL処理やトランザクションコミットを担当するスレッドで、テナントワーカースレッドと総称されます。
残りはネットワークI/O、ディスクI/O、Compaction、タイマー任務を処理するスレッドで、バックグラウンドスレッドと総称されます。
現在のバージョンでは、テナントワーカースレッドとほとんどのバックグラウンドスレッドはテナントごとに分かれていますが、ネットワークスレッドは共有されています。
スレッド数に基づくテナントワーカースレッドのCPU分離
UnitのCPU分離は、そのUnitのアクティブなテナントワーカースレッド数によって実現されます。
SQL実行中にI/O待ちやロック待ちなどが発生する可能性があるため、一つのスレッドが物理CPUをフル活用することはできません。そのため、デフォルト設定では、OBServerノードは各CPUに対して4つのスレッドを起動します。この4という倍数はcpu_quota_concurrencyの設定で変更できます。これは、UnitのMAX_CPUが10の場合、同時に実行可能なアクティブスレッド数は40であることを意味します。
cgroupに基づくテナントワーカースレッドのCPU分離
cgroupを有効にすると、最大の変化は異なるテナントのワーカースレッドが異なるcgroupディレクトリに配置されることで、テナント間のCPU分離効果が向上します。最終的な分離効果は以下の通りです:
あるOBServer上で単一のテナントの負荷が高く、他のテナントが比較的空いている場合、その負荷の高いテナントのCPUも
MAX_CPUの制限を受けます。上記のシナリオを続けると、複数の空いているテナントの負荷が上昇し、物理CPUが不足した場合、cgroupはウェイトに応じてタイムスライスを割り当てます。
バックグラウンドスレッドのCPU分離
V4.0.0バージョン以降、バックグラウンドスレッドはテナントごとに分割され、各テナント内で対応するスレッドが管理します。
より良い分離効果を得るため、バックグラウンドスレッドも異なるcgroupディレクトリに配置され、テナント間およびテナント内でワーカースレッドから分離されます。
大規模クエリの処理
私たちは、短時間で返される短いクエリの方がユーザーにとって意味があると考えています。つまり、大規模クエリの優先順位は低く、大規模クエリと短いクエリが同時にCPUを競合する場合、システムは大規模クエリのCPU使用を制限します。
スレッドが実行するSQLクエリの実行時間が長すぎる場合、そのクエリは大規模クエリと判定されます。一度大規模クエリと判定されると、そのクエリを実行するスレッドはPthread Conditionで待機し、他のテナントのワーカースレッドにCPUを譲ります。
具体的な実装では、OBServerノードはコードに多くのチェックポイントを挿入しています。テナントワーカースレッドは実行中にチェックポイントを定期的に確認し、停止すべきと判断された場合はPthread Conditionで待機し、適切なタイミングで再開されます。
大規模クエリと短いクエリが同時に存在する場合、大規模クエリはテナントワーカースレッドの最大30%を占有します。この30%という割合はパラメータlarge_query_worker_percentageで設定できます。
説明が必要な点は2つあります:
短いクエリが存在しない場合、大規模クエリはテナントワーカースレッドの100%を使用できます。30%の割合が適用されるのは、大規模クエリと短いクエリが同時に存在する場合のみです。
テナントワーカースレッドが大規模クエリの実行で停止した場合、補償としてシステムは新しいテナントワーカースレッドを作成する可能性があります。ただし、テナントワーカースレッドの総数は
MAX_CPUの10倍を超えることはできません。この10という倍数はパラメータworkers_per_cpu_quotaで設定できます。
大規模クエリの事前特定
ノードが大規模クエリスレッドを停止するたびに、新しいテナントワーカースレッドが起動します。しかし、大量の大規模クエリが流入すると、OBServerノードが新しく作成したスレッドも大規模クエリの処理に使われ、すぐにテナントワーカースレッドの上限に達してしまいます。その結果、この大量の大規模クエリが処理されるまで、短いクエリを処理する機会が得られません。
このシナリオを最適化するため、OBServerノードはSQLの実行開始前にそれが大規模クエリかどうかを予測します。予測の本質はSQLの実行時間を推定することです。予測は主に以下の仮定に基づいています:もし2つのSQLの実行計画が同じであれば、実行時間も似ていると推測できます。そこで、計画の直近の実行時間を用いて、SQLが大規模クエリになるかどうかを判断できます。
特定のSQLが大規模クエリと予測された場合、そのクエリは特別な大規模クエリキューに投入され、そのスレッドは解放され、システムは後続のリクエストの処理を続けます。
モニタリング項目とログ
ログで dump tenant info キーワードを検索すると、テナントの仕様、スレッド、キュー、リクエスト統計などの情報が確認できます。このログは各テナントごとに30秒ごとに出力されます。
ログ例:
grep 'dump tenant info.tenant={id:1002' log/observer.log | sed 's/,/,\n/g'
[2022-07-20 14:55:40.774143] INFO [SERVER.OMT] run1 (ob_multi_tenant.cpp:1993) [80700][MultiTenant][T0][Y0-0000000000000000-0-0] [lt=621]
dump tenant info(tenant={id:1002, //テナントID
tenant_meta:{unit:{tenant_id:1002, //tenant_metaはテナントメタデータで、Unit構成情報、SuperBlock、およびcreate_statusを含みます
unit_id:1001, //Unit ID
has_memstore:true, //MemTableの有無
unit_status:"NORMAL", //Unitの状態: UNIT_NORMAL/UNIT_MIGRATE_IN/UNIT_MIGRATE_OUT/UNIT_MARK_DELETING/UNIT_WAIT_GC_IN_OBSERVER/UNIT_DELETING_IN_OBSERVER/UNIT_ERROR_STAT
config:{unit_config_id:1003, //Unitの構成ID
name:"2c2g", //Unit構成名
resource:{min_cpu:2, //Unit構成の最小CPU数
max_cpu:2, //Unit構成の最大CPU数
memory_size:"1.5GB", //Unit構成のメモリサイズ
log_disk_size:"5.4GB", //Unit構成のログディスクサイズ
min_iops:20000, //Unit構成の最小ディスクIOPS
max_iops:20000, //Unit構成の最大ディスクIOPS
iops_weight:2}}, //Unit構成のIOPS重み
mode:1, //CompatMode 0:MYSQL 1:ORACLE
create_timestamp:1658298418435426, //Unitの作成時間
is_removed:false}, //このUnitが削除フラグを付けられているかどうか
super_block:{tenant_id:1002, //ObTenantSuperBlock情報
replay_start_point:ObLogCursor{file_id=1, //テナントslogのログ再生ポイント
log_id=440,
offset=343322},
ls_meta_entry:[140](ver=0, //テナントls metaのslogチェックポイントエントリポイント
mode=0,
seq=139),
tablet_meta_entry:[141](ver=0, // テナントtablet metaのslogチェックポイントのエントリポイント
mode=0,
seq=140)},
create_status:1}, // テナントの作成状態: CREATING = 0,CREATE_COMMIT=1,CREATE_ABORT=2,DELETING=3,DELETE_COMMIT=4
unit_min_cpu:"2.000000000000000000e+00", //保証される最小CPUコア数
unit_max_cpu:"2.000000000000000000e+00", //制限上限の最大CPUコア数
slice:"0.000000000000000000e+00",
slice_remain:"0.000000000000000000e+00",
token_cnt:8, //スケジューラーが割り当てたトークン数。1つのトークンは1つのワーカースレッドに変換されます
sug_token_cnt:8,
ass_token_cnt:8, //テナントが現在確認しているトークン数(token_cntに基づいて確認され、通常は両者は等しい)
lq_tokens:2, //Large Queryトークンの個数。token_cntに大規模リクエストの割合を掛けて設定します
used_lq_tokens:0, //現在LQ Tokenを保持しているWorker数
stopped:false, //テナントunitが削除中かどうか
idle_us:6076629, //1ラウンド(10秒)中のワーカースレッドのアイドル総時間。いわゆるアイドルは、待機キューの時間のみを集計します
recv_hp_rpc_cnt:1, //テナントが累計で受信した異なるレベルのRPCリクエスト数。hp(High)、np(Normal)、lp(Low)
recv_np_rpc_cnt:5,
recv_lp_rpc_cnt:0,
recv_mysql_cnt:24, //テナントが累計で受信したMySQLリクエスト数
recv_task_cnt:1, //テナントが累計で受信した内部タスク数
recv_large_req_cnt:0, //テナントが累計で予測した大規模リクエスト数。増分のみで、リセットされません。実際には再試行時に増加します。
tt_large_quries:0, //テナントが累計で処理した大規模リクエスト数。増分のみで、リセットされません。実際にはチェックポイントチェック時に増加します。
pop_normal_cnt:1024555,
actives:8, //アクティブなワーカースレッド数。通常はworkersと等しく、その差には以下が含まれます:テナントワーカースレッドキャッシュ + ワーカースレッドを持つ大規模リクエストキャッシュ
workers:8, //テナントが保持しているワーカースレッド数。実際にはworkers_このリストのサイズです。
nesting workers:7, //テナントが保持しているネストリクエスト専用スレッド数。合計7つのスレッドが7つのネストレベルに対応します。
lq waiting workers:0, //大規模クエリと判断され、スケジューリングを待機しているワーカースレッド
req_queue:total_size=0 queue[0]=0 queue[1]=0 queue[2]=0 queue[3]=0 queue[4]=0 queue[5]=0 , //異なる優先順位のワークキュー。数字が小さいほど優先順位が高い
large queued:0, //現在予測された大規模リクエスト数
multi_level_queue:total_size=0 queue[0]=0 queue[1]=0 queue[2]=0 queue[3]=0 queue[4]=0 queue[5]=0 queue[6]=0 queue[7]=0 , //ネストリクエストを格納するワークキュー。1~7は7つのネストレベルに対応します(queue[0]は一時的に使用せず、queue[5]もInnersqlリクエストを格納します)。
recv_level_rpc_cnt:cnt[0]=0 cnt[1]=0 cnt[2]=0 cnt[3]=0 cnt[4]=0 cnt[5]=0 cnt[6]=0 cnt[7]=0 ,
group_map:group_id = 1, //group_mapの後には、各group_idに対応するgroupのスレッドとキュー状況が続きます
queue_size = 0, //groupキューのキュー状況
recv_req_cnt = 13526, //groupがキューにプッシュした累計リクエスト数
pop_req_cnt = 13526, //groupスレッドがポップした累計リクエスト数
token_cnt = 2, //groupに割り当てられたトークン数。1つのトークンは1つのワーカースレッドに変換されます
min_token_cnt = 2,
max_token_cnt = 2,
ass_token_cnt = 2 , //groupが現在確認しているトークン数(token_cntに基づいて確認され、通常は両者は等しい)
rpc_stat_info: pcode=0x150a:cnt=1489 pcode=0x150b:cnt=1091}) //テナントが一定期間内に受信した最多RPC pcode。統計周期は10秒で、最多で上位5つを出力します