監視やスケーリングを実施していても、本番システムは予期せぬ方法で失敗することがあります。今週、私たちのチームは、2日間で2回本番サーバーをダウンさせる重大な問題に直面しました。根本原因は、Redisメモリエクゾーストです。

このインシデントが興味深いのは、Redisが通常の容疑者(長寿命キーや欠落しているevictionポリシーなど)のために失敗したわけではないことです。代わりに、犯人は、高負荷下でのCeleryタスクとRedisのやり取り方法に隠されていました。

発生した状況

  • ピークトラフィック時に、複数のクライアントが同時に大規模なファイルをアップロードしました。
     

  • 各アップロードは、Celeryを介してバックグラウンド計算をトリガーしました。
     

  • ピーク時には、最大10,000リクエスト/秒を観測しました。

私たちのカスタムCeleryタスクベースクラスは、Redisにロックキーを書き込むことでタスクのレート制限を行うように設計されていました。

self.first_task_at_lock_key = f"{base_task_id}-first_task_at"
self.count_tasks_lock_key = f"{base_task_id}-count_tasks"

問題点?

入力データ(sale_orders)が非常に大きい場合、これらの生成されたキーはそれぞれ10 KBに達する可能性があります。タスクごとに2つのキーを使用すると、Redisはタスクあたり〜20 KBを保存することになります。

大規模な場合:

  • 1,000タスク = 約20 MBのRedisメモリ

  • 毎秒数千のタスクがキューイングされるため、メモリ使用量は制御不能に増加し、Redisは書き込みを拒否しました。

redis.exceptions.ResponseError:
OOM command not allowed when used memory > 'maxmemory'

そして、そのようにして、本番サーバーがクラッシュしました。トレースログを以下に示します。

django_redis.exceptions.ConnectionInterrupted: Redis ResponseError: OOM command not allowed when used memory > 'maxmemory'.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "venv/lib/python3.8/site-packages/sentry_sdk/integrations/django/views.py", line 67, in sentry_wrapped_callback
    return callback(request, *args, **kwargs)
  File "venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "venv/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "venv/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "venv/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "venv/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "venv/lib/python3.8/site-packages/rest_framework/views.py", line 497, in dispatch
    self.initial(request, *args, **kwargs)
  File "venv/lib/python3.8/site-packages/sentry_sdk/integrations/django/__init__.py", line 270, in sentry_patched_drf_initial
    return old_drf_initial(self, request, *args, **kwargs)
  File "venv/lib/python3.8/site-packages/rest_framework/views.py", line 416, in initial
    self.check_throttles(request)
  File "venv/lib/python3.8/site-packages/rest_framework/views.py", line 359, in check_throttles
    if not throttle.allow_request(request, self):
  File "venv/lib/python3.8/site-packages/rest_framework/throttling.py", line 132, in allow_request
    return self.throttle_success()
  File "venv/lib/python3.8/site-packages/rest_framework/throttling.py", line 140, in throttle_success
    self.cache.set(self.key, self.history, self.duration)
  File "venv/lib/python3.8/site-packages/django_redis/cache.py", line 38, in _decorator
    raise e.__cause__
  File "venv/lib/python3.8/site-packages/django_redis/client/default.py", line 175, in set
    return bool(client.set(nkey, nvalue, nx=nx, px=timeout, xx=xx))
  File "venv/lib/python3.8/site-packages/redis/client.py", line 1801, in set
    return self.execute_command('SET', *pieces)
  File "venv/lib/python3.8/site-packages/redis/client.py", line 901, in execute_command
    return self.parse_response(conn, command_name, **options)
  File "venv/lib/python3.8/site-packages/redis/client.py", line 915, in parse_response
    response = connection.read_response()
  File "venv/lib/python3.8/site-packages/redis/connection.py", line 756, in read_response
    raise response
redis.exceptions.ResponseError: OOM command not allowed when used memory > 'maxmemory'.

根本原因

  • 生のデータがCeleryタスクに直接渡されたため、キーが大きすぎました。
     

  • Redisは事実上、キャッシュ + タスクペイロードストレージとして使用されており、最適化されていませんでした。
     

  • 高並行性により問題が増幅され、Redisは設定されたメモリ制限を超えました。

修正

キューイング戦略を変更しました。

生のデータをタスクに直接渡す代わりに、以下を実行しました。

  1. 生のデータをデータベースに保存します。
     

  2. 軽量なID + ハッシュのみをCeleryタスクに渡します。

これにより、キーサイズは〜10 KB → <100バイトに削減されました。

結果

  • ピークメモリ使用量は20 MB/秒 → 約0.2 MB/秒に低下しました(99%削減)。
     

  • Redisの安定性が回復しました。
     

  • 同時の大規模ファイルアップロード時でも、サーバーはスムーズに動作しました。

重要なポイント

  1. 隠れたペイロードに注意: Celeryタスクに大量のデータを渡すことは、スケールするまでは無害に見えるかもしれません。
     

  2. Redisはデータウェアハウスではありません: 軽量なキー/値に使用し、大規模なペイロードストレージには使用しないでください。
     

  3. 常にメモリへの影響を測定する: キーあたりの数KBは問題ありません…数千のタスク/秒で乗算されるまでは。
     

  4. 早期にスケールを考慮した設計を行う: 「小さい」非効率性でも、現実世界のトラフィック下では大きな障害を引き起こす可能性があります。

結論

このようなインシデントは苦痛ですが、貴重な経験です。これにより、前提条件を見直し、アーキテクチャを改善し、より堅牢なシステムを構築することができます。私たちの場合、これは、バックグラウンドタスク設計の効率性は、ビジネスロジックの正確性と同じくらい重要であるという戒めでした。

この共有が、他の人が同様の障害を回避するのに役立つことを願っています。