OceanBaseデータベースはマルチテナント型のデータベースシステムであり、テナント間でのリソース競合を防ぎ、業務の安定稼働を確保するために、OceanBaseデータベースではテナント間のリソースが分離されています。
OceanBaseデータベースでは、ユニット(Unit)をテナントに割り当てる基本単位としており、1つのユニットはDockerコンテナに例えられます。1つのノード上に複数のユニットを作成でき、ノード上でユニットを作成するたびに、そのノードのCPUやメモリなどの物理リソースの一部が占有されます。ノードのリソース割り当て状況は内部テーブルに記録され、DBAが確認できるようになっています。
1つのテナントは複数のノード上に複数のユニットを配置できますが、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バージョンからは、CPU分離を行うためにcgroupの設定がサポートされています。
異なるテナントの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ユニットで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であり、物理CPUの最大使用率は400%になることを意味します。
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チェックポイントのslogチェックポイント
mode=0,
seq=139),
tablet_meta_entry:[141](ver=0, //テナントtablet metaのslogチェックポイントの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 queue[6]=0 queue[7]=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に対応するグループのスレッドとキュー状況が続きます
queue_size = 0, //グループキューのキュー状況
recv_req_cnt = 13526, //グループが累計でキューにプッシュしたリクエスト数
pop_req_cnt = 13526, //グループスレッドが累計でポップしたリクエスト数
token_cnt = 2, //グループが割り当てたトークン数。1トークンは1ワーカースレッドに変換されます
min_token_cnt = 2,
max_token_cnt = 2,
ass_token_cnt = 2 , //グループが現在確認しているトークン数(token_cntに基づいて確認され、通常は両者が等しい)
rpc_stat_info: pcode=0x150a:cnt=1489 pcode=0x150b:cnt=1091}) //テナントが一定期間内に受信した最も多いRPC pcode。統計期間は10秒で、上位5つまで表示されます