ノイジーネイバー(Noisy Neighbor)問題とは、同じ物理サーバー・ノード・データベースを共有する別の利用者(隣人)が資源を大量に使い、自分のシステムの応答が遅くなったり失敗したりする現象です。「ノイジー(noisy)」は英語で「うるさい・騒がしい」の意味で、日本語では「うるさい隣人問題」とも呼ばれます。
この記事では、仕組みを押さえたうえで、遅延の原因が「自分の負荷」なのか「隣の負荷」なのかを指標で切り分ける方法と、クラウドVM・Kubernetes・マルチテナントSaaSそれぞれの対策を扱います。
まとめ:ノイジーネイバー問題の要点
- 原因は資源の共有そのもの。Microsoftも「完全に回避できない」リスクと位置づけており、目標は発生確率と影響範囲を下げることです。
- 切り分けの指標は、VMならCPUのスチール時間(
topのst)、Linuxなら/proc/pressureのPSI、コンテナならcpu.statのnr_throttled。自分の使用率が平常のまま遅延だけが増えていれば、隣を疑います。 - 対策は「テナント別の計測 → クォータ・レート制限 → 分離」の順で進めます。いきなり全テナントを専用環境に分けると、環境の数だけ費用と運用の手間が増えます。
| レイヤー | 典型的な症状 | 最初に打つ手 |
|---|---|---|
| クラウドVM・仮想化基盤 | st値の上昇、処理時間のばらつき | 専有インスタンス、インスタンス変更 |
| Kubernetes | 同一ノードのPodが遅延・退避 | requests/limits、ResourceQuota |
| マルチテナントSaaS | 特定テナントの集中で全体が遅延 | テナント単位のレート制限・公平キュー |
ノイジーネイバーの意味と、問題が起きる条件
「neighbor」は隣人、「noisy」は騒がしいという意味です。IT用語としては、マルチテナント環境で他のテナントの活動が自分の性能を下げる状況を指します。Azure Architecture Centerのアンチパターン解説は、この問題を「別のテナントのアクティビティが原因で 1 つのテナントのパフォーマンスが低下したときに発生」するものと定義しています。
起きる条件は2通りあります。1つは、1テナントが突出して資源を使うケースです。もう1つは見落とされがちで、各テナントの使用量は小さくても、ピーク時間帯が重なって合計が容量を超えるケースです。後者では「犯人」のテナントがいないため、特定のテナントを止めても解決しません。
アプリケーションがシングルテナントでも、物理ホスト・ストレージ・ネットワークを共有していれば、その層で性能干渉が起こり得ます。分離の効果は、どの資源まで専用化するかで決まります。テナントの分け方ごとの特徴はシングルテナントとマルチテナントの違いで比較しています。
競合が起きる共有リソースと発生の仕組み
「隣に負ける」資源はCPUだけではありません。どの資源で競合しているかによって、見るべき指標も打てる対策も変わります。
| 共有リソース | 競合の中身 | 確認する指標の例 |
|---|---|---|
| CPU時間 | ハイパーバイザーやカーネルの割り当て待ち | steal、PSI cpu |
| メモリ・キャッシュ | メモリ不足による回収待ち、LLC・メモリ帯域の競合 | 不足:PSI memory/競合:キャッシュミス率・帯域 |
| ディスクI/O | IOPS・帯域の飽和 | PSI io、I/O待ち時間 |
| ネットワーク | NIC・上流帯域の飽和 | 再送、帯域使用率 |
| データベース | 接続枠・ロック・重いクエリ | 接続数、テナント別クエリ時間 |
| キュー・ワーカー | 1テナントの大量ジョブで滞留 | テナント別の待ち時間 |
下の層ほど利用者側からは見えません。クラウドVMの利用者は同じ物理ホストに誰がいるかを知る手段がなく、SaaSの利用者は同じデータベースを使う他社の負荷を知りません。そのため、症状は「同じリクエストが、時間によって成功したり失敗したりする」形で現れます。Azureのガイドも、この不規則さを検出の手がかりに挙げています。
レイヤー別に見る発生パターン
クラウドVM・仮想化基盤:CPUスチール時間
仮想マシンでは、vCPUが実行したいのにハイパーバイザーが物理CPUを他のVMに渡している時間が生じます。Linuxの top の st 欄がこれで、man pageは「time stolen from this vm by the hypervisor」と説明しています。自分のプロセスが同じ量の計算をしているのに処理時間だけ延び、st値が上がっていれば、同じホストに重いVMが同居している可能性が高いと判断できます。
Kubernetes:requests・limits未設定Podによる資源競合
Kubernetesでは、複数のPodが同じノードのCPUとメモリを分け合います。requests・limitsを1つも指定していないPodは BestEffort クラスになり、ノードの空き資源を上限なく使えます。そのPodが急にメモリを使えば、同じノードの別チームのPodが遅くなるか、ノードの圧迫で退避(eviction)されます。退避対象はQoSクラスの名前だけでは決まりません。Kubernetesの公式ドキュメントによると、kubeletは使用量がrequestsを超えているか、Podの優先度(Priority)、requestsに対する使用量を基に選びます。requestsを超えて使っている BestEffort・Burstable のPodが先に、Guaranteed とrequests以内の Burstable が最後に回ります。
マルチテナントSaaS:1テナントのクエリ・バッチ・ジョブ
SaaSでは、1社の月次バッチや大量CSVインポート、重い集計クエリが共有データベースやジョブキューを占有し、他社の画面操作まで遅くします。キューの場合は、先に投入された大量ジョブが後から来た他テナントのジョブを待たせる「先着順の不公平」が起きます。LayerXのエンジニアブログは、Temporalでのノイジーネイバー対策として、レート制限と2本のタスクキューを組み合わせた再現テストを公開しています。
ノイジーネイバーの検知指標と自分の負荷との切り分け
遅延が起きたとき、最初に判定すべきは「自分の使用量が増えたのか」です。自分の使用率・リクエスト数が平常で、待ち時間の指標だけが増えていれば、ノイジーネイバーを疑う根拠になります。
ホスト・コンテナで見る指標
| 指標 | 取得元 | 読み方 |
|---|---|---|
| st(スチール時間) | top、vmstat | 上昇=ハイパーバイザーによるCPU実行待ち |
| PSI some / full | /proc/pressure/* | 資源待ちで止まった時間の割合 |
| nr_throttled | cgroup v2 cpu.stat | 自分のCPU上限で止められた回数 |
| p99レイテンシー | APM・メトリクス | 平均が平常でも裾が伸びる |
PSI(Pressure Stall Information)は、CPU・メモリ・I/Oの資源不足でタスクが止まった時間の割合を示すLinuxカーネルの機能です。some 行は少なくとも一部のタスクが待たされた時間、full 行は処理可能なタスクがすべて待たされた時間を表し、直近10秒・60秒・300秒の平均が出ます。ただし、システム全体のCPUのfullは未定義で、表示されても互換性のために0が出るだけなので、CPU競合の判定には使えません。
# CPUのスチール時間(st)を表示。初回は起動後の平均、以降4回は1秒間隔
vmstat 1 5
# 資源待ちの割合(some/full、avg10は直近10秒の平均)
cat /proc/pressure/cpu
cat /proc/pressure/io
# Linux・cgroup v2環境で実行する例
# 以下のパスは対象コンテナのcgroupがこの位置に見える場合のみ有効
# ホストから読む場合は対象コンテナのcgroupディレクトリ内のcpu.statを指定
# CPUコントローラーが有効な対象cgroupのスロットリング回数と時間
cat /sys/fs/cgroup/cpu.stat | grep -E 'nr_throttled|throttled_usec'
Kubernetesでは、コンテナがCPU上限で止められた回数を、kubeletに組み込まれたcAdvisorが container_cpu_cfs_throttled_periods_total として出します。Prometheusで監視を構築する手順の構成で集める場合は、まずkubeletのcAdvisorメトリクスが収集対象に含まれるか確認します。ノードのsteal・PSIやアプリケーションのp99は取得元が異なるため、それぞれ別に収集設定が必要です。
ノイジーネイバーと誤認しやすい現象
次の2つは、症状が似ていても原因は自分側にあります。隣を疑う前に除外してください。
- バースト可能インスタンスのCPUクレジット枯渇:AWSのT系インスタンスは、ベースラインを超えて使うとクレジットを消費します。Standardモードでクレジットが尽きると、CPUはベースラインまで徐々に下がります。CloudWatchの
CPUCreditBalanceが0付近なら、隣ではなく自分の使いすぎです。 - 自分のCPU limitによるスロットリング:cgroup v2の
cpu.maxは「一定期間(既定は100,000マイクロ秒)にどれだけCPUを使えるか」の上限で、ノードのCPUが空いていても上限に達すれば止められます。nr_throttledが増えているなら、limitの設定値を見直す問題です。
SaaSの検知指標:テナントIDによる負荷の集計
SaaSの場合、全体の平均レイテンシーを見ても「誰が騒いでいるか」は分かりません。Azureのガイドは、Cosmos DBの要求ユニット(RU)を要求ごとに記録し、テレメトリにテナントIDを含めてテナント別に集計する方法を例に挙げています。ログとトレースにはテナントIDを記録し、資源使用量と対応づけます。Prometheusなどのメトリクスにも付ける場合は、テナント数と他のラベルの組み合わせによる時系列数を見積もり、対象指標を絞ります。
インフラ層の対策:VMとKubernetes
クラウドVM:専有環境と上位ティアへの移行
他のAWSアカウントのVMとホストを共有しないようにするには、AWSのDedicated Instances・Dedicated Hostsで専有ハードウェアを使います。ただし、同一アカウントのVM同士の競合や、別途共有されるストレージ(EBSなど)での競合は残り得ます。マネージドサービスでも、Azureは分離が強いティアへの移行(Service BusのPremiumレベルなど)や予約容量の購入を利用者側の対策に挙げています。
なお、Firecracker(マイクロVM)とは?で解説しているマイクロVMはセキュリティ境界を強める技術で、物理CPUやメモリを共有する以上、性能面の競合は残ります。資源の割り当てとレート制限は別途必要です。
Kubernetes:QoSクラスとNamespace単位の上限
メモリ圧迫時の退避されにくさを重視するPodでは、負荷に見合うCPU・メモリ量を見積もり、全コンテナの requests と limits を同じ値にして Guaranteed にします。ただし、あらゆる資源の圧迫で最後になる保証はありません。また、CPU limitによるスロットリングは残るため、遅延要件を満たすかは負荷試験で確認します。
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: example.com/api:1.4.2 # 例示用。利用可能な自分のイメージに置換
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m" # requests と同じ値で Guaranteed になる
memory: "512Mi"
チームや顧客ごとにNamespaceを分けている場合は、Namespace内のrequests・limitsの設定値の合計を ResourceQuota で、コンテナの既定値を LimitRange で制限します。LimitRangeで既定のrequestsを入れておけば、指定を忘れたPodが BestEffort になることは避けられます。ただし、requestsは使用上限ではないため、上限も必要ならlimitsの既定値や最大値を別途設定します。重要度の違うワークロードが混在するなら、PriorityClass で優先度を付けます。高優先のPodが配置先を見つけられないとき、低優先のPodを押し出して(プリエンプション)場所を空けられます。
アプリケーション層の対策:マルチテナントSaaS
テナント単位のレート制限
API全体に1つの上限を設けても、1テナントが上限を使い切れば他テナントが弾かれます。上限はテナントごとに持たせます。次はトークンバケット方式の最小実装で、テナントごとに毎秒5トークンを補充し、最大10トークンを保持します。満杯なら10件を直ちに通せますが、任意の1秒間を必ず5件以下に制限する方式ではありません。
import time
from collections import defaultdict
class TenantTokenBucket:
"""テナントごとに独立したトークンバケット。rate=毎秒の補充数、burst=上限"""
def __init__(self, rate: float, burst: int):
self.rate, self.burst = rate, burst
self.tokens = defaultdict(lambda: float(burst))
self.updated = defaultdict(time.monotonic)
def allow(self, tenant_id: str) -> bool:
now = time.monotonic()
elapsed = now - self.updated[tenant_id]
self.updated[tenant_id] = now
self.tokens[tenant_id] = min(self.burst, self.tokens[tenant_id] + elapsed * self.rate)
if self.tokens[tenant_id] >= 1:
self.tokens[tenant_id] -= 1
return True
return False # 呼び出し側は HTTP 429 と Retry-After を返す
limiter = TenantTokenBucket(rate=5, burst=10)
heavy = sum(limiter.allow("tenant-a") for _ in range(100))
light = sum(limiter.allow("tenant-b") for _ in range(3))
print(f"tenant-a: {heavy}/100 通過, tenant-b: {light}/3 通過")
# 実行結果: tenant-a: 10/100 通過, tenant-b: 3/3 通過
tenant-aが100件を連打しても通るのは10件だけで、tenant-bの3件はすべて通ります。この例は単一スレッド向けです。本番で複数のAPIサーバーに分散させる場合は、Redisなどの共有ストア上で補充・判定・消費を原子的に実行します。同一プロセス内でも並行呼び出しには同期が必要で、未使用テナントの状態には有効期限を設けます。制限値はテナントに公開し、429を受けたときの再試行方法も文書化しておきます。
キューの公平化:SQS fair queuesとTemporal Fairness
非同期ジョブは、キューを先着順にしている限り1テナントの大量投入で他テナントが待たされます。Amazon SQSの標準キューは、送信時に MessageGroupId へテナントIDを入れるだけで公平キュー(fair queues)が自動で働き、コンシューマー側のコード変更は要りません。SQSは、処理中メッセージの10%超かつ30件以上を1テナントが占める場合と、直近の処理時間の10%超を占める場合に、そのテナントを「ノイジー」と判定します。これらの閾値は近似値であり、境界値ちょうどで判定が切り替わるとは限りません。ノイジーと判定されたメッセージは捨てられず、静かなテナントのメッセージが優先的に配信されます。CloudWatchの ApproximateNumberOfNoisyGroups で判定数を監視できます。
ワークフローエンジンのTemporalにも、タスクキュー内でfairness key(テナント名など)ごとに仮想キューを作り、ラウンドロビンで配信するFairness機能があります。同じ優先度階層で各キーに待機タスクがある場合、fairness weightを2.0にしたキーには、1.0のキーの2倍の割合でタスクが配信されます。処理時間やCPU使用量が2倍になる保証ではありません。
シャッフルシャーディングとテナントの階層化
ワーカーを固定のグループに分ける通常のシャーディングでは、1グループが落ちると、そこに属する全テナントが巻き込まれます。AWSのシャッフルシャーディングは、テナントごとにワーカーの組み合わせをずらして割り当てる方式です。Amazon Builders’ Libraryの例では、8台のワーカーを2台ずつ組むと28通りの組み合わせができ、組み合わせにテナントを均等に割り当て、片方のワーカーが失敗してももう片方へ再試行できる場合、両方のワーカーを失って処理を継続できなくなるテナントの割合は約1/28になります。4グループに分ける通常のシャーディング(1/4)より7倍狭い計算です。Route 53は、2048台の仮想ネームサーバーから4台ずつ組むことで、7,300億通りの組み合わせを作っています。
もう1つの定番は、テナントを料金プランで階層化し、大口・上位プランだけを専用環境(サイロ)に置き、他は共有環境(プール)に集約する方法です。データベース側でテナントの行を分けるには、RLS(Row Level Security)を併用します。
対策を選ぶ順番と、やりすぎの失敗パターン
対策は、計測、ガバナンス(クォータ・レート制限・公平キュー)、分離の順に進めます。テナント別の計測がないまま分離に進むと、どのテナントを隔離すべきかを決められません。分離は効果が確実な反面、専用環境の数だけ費用と運用の手間が増えるため、最後の手段です。
避けたい失敗は3つあります。
- CPU limitを一律に低く設定する:隣の暴走は防げても、ノードが空いている時間にまで自分のPodがスロットリングされ、p99レイテンシーが悪化します。
Guaranteedの採用は退避されにくさとCPU上限による遅延を比較して判断し、limitの値はnr_throttledを見ながら決めます。 - 全テナントをサイロ化する:共有による費用効率というマルチテナントの利点がなくなります。専用環境は、SLAで性能を約束する上位プランに限定します。
- 制限値をテナントに知らせない:Azureのガイドも、スロットリングやクォータを透明に説明するよう求めています。黙って429を返すと、テナント側の無制限リトライで負荷がかえって増えます。
一方で、データベースのように分割しにくい資源をどう扱うかは、テナント数と負荷の偏りで決まります。1テナントの負荷が全体の大半を占めるなら、共有を続ける利点は小さいので、そのテナントだけを専用環境へ移すのが妥当です。
ノイジーネイバー問題に関するよくある質問
ノイジーネイバーは日本語で何と言いますか?
直訳すると「うるさい隣人」で、「うるさい隣人問題」と呼ばれることもあります。IT分野では、共有リソース上の他の利用者の負荷で自分の性能が落ちる現象を指します。
ノイジーネイバー問題は完全に防げますか?
資源を共有する限り、完全には防げません。Azure Architecture Centerも、共有には「完全に回避できない」リスクが伴うとしています。現実的な目標は、クォータやレート制限で発生確率を下げ、シャッフルシャーディングなどで影響を受けるテナントの範囲を狭めることです。
AWSではどんな対策がありますか?
インフラ層ではDedicated Instances・Dedicated Hostsによる物理ホストの専有、アプリケーション層ではSQS fair queues(標準キューで MessageGroupId を付けると自動で有効)があります。T系インスタンスの遅延はクレジット枯渇の可能性があるため、CPUCreditBalance を先に確認してください。
Kubernetesでノイジーネイバーを防ぐには?
重要なPodでは、実負荷とCPUスロットリングを確認したうえでrequestsとlimitsを同じ値にして Guaranteed にし、Namespaceごとに ResourceQuota と LimitRange を設定します。LimitRangeでrequestsの既定値を設定すると、CPU・メモリの指定漏れによるBestEffort化を避けられます。資源使用の上限も必要なら、limitsの既定値や最大値を設定します。
ノイジーネイバーはセキュリティの問題でもありますか?
多くの場合、原因のテナントに悪意はなく、自分が他に迷惑をかけていることに気づいていません。ただしAzureのガイドは、共有コンポーネントの弱点を突いて意図的に他テナントを妨害する(サービス拒否攻撃)テナントもありうるとしています。原因を問わず、リソースガバナンスの問題としてクォータ・スロットリングで抑えるのが基本です。