Khi bộ nhớ Redis cạn kiệt: Bài học từ sự cố sản xuất thực tế

By hientd, at: 17:11 Ngày 05 tháng 9 năm 2025

Thời gian đọc ước tính: __READING_TIME__ phút

When Redis Memory Ran Out: Lessons from a Real Production Outage
When Redis Memory Ran Out: Lessons from a Real Production Outage

Đôi khi, các hệ thống sản xuất gặp sự cố theo những cách bạn không ngờ tới, ngay cả khi đã có giám sát và mở rộng quy mô. Tuần này, nhóm của chúng tôi đã gặp phải một vấn đề nghiêm trọng khiến máy chủ sản xuất của chúng tôi bị ngừng hoạt động hai lần trong hai ngày. Nguyên nhân gốc rễ? Redis bị cạn kiệt bộ nhớ.

 

Điều khiến sự cố này trở nên thú vị là Redis không bị lỗi do những nguyên nhân thông thường (như khóa tồn tại lâu hoặc thiếu chính sách xóa). Thay vào đó, thủ phạm nằm ẩn trong cách các tác vụ Celery của chúng tôi tương tác với Redis khi tải nặng.

 

Điều gì đã xảy ra

 

  • Trong giờ cao điểm, nhiều khách hàng tải lên các tệp lớn cùng một lúc.
     

  • Mỗi lần tải lên kích hoạt một tính toán nền thông qua Celery.
     

  • Vào thời điểm cao điểm, chúng tôi thấy lên tới 10.000 yêu cầu/giây.

 

Lớp cơ sở tác vụ Celery tùy chỉnh của chúng tôi được thiết kế để giới hạn tốc độ các tác vụ bằng cách ghi khóa khóa vào 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"

 

Vấn đề?

 

Khi dữ liệu đầu vào (sale_orders) rất lớn, các khóa được tạo này có thể đạt 10 KB mỗi khóa. Với hai khóa mỗi tác vụ, Redis đã lưu trữ ~20 KB mỗi tác vụ.

 

Ở quy mô lớn:

 

  • 1.000 tác vụ = ~20 MB bộ nhớ Redis

  • Với hàng nghìn tác vụ được xếp hàng mỗi giây, việc sử dụng bộ nhớ tăng không kiểm soát cho đến khi Redis từ chối ghi:

 

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

 

Và thế là, máy chủ sản xuất của chúng tôi bị sập. Nhật ký theo dõi được hiển thị bên dưới:

 

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'. 

 

Nguyên nhân gốc rễ

 

  • Khóa quá lớn, vì dữ liệu thô được truyền trực tiếp vào tác vụ Celery.
     

  • Redis về cơ bản được sử dụng như một bộ nhớ cache + kho lưu trữ tải tác vụ, mà nó không được tối ưu hóa cho việc đó.
     

  • Độ đồng thời cao đã khuếch đại vấn đề, đẩy Redis vượt quá giới hạn bộ nhớ được cấu hình.

 

Khắc phục

 

Chúng tôi đã thay đổi chiến lược xếp hàng của mình:

 

Thay vì truyền dữ liệu thô trực tiếp vào các tác vụ, chúng tôi:

 

  1. Lưu trữ dữ liệu thô trong cơ sở dữ liệu.
     

  2. Chỉ truyền ID + băm nhẹ vào tác vụ Celery.

 

Điều này đã giảm kích thước khóa từ ~10 KB → <100 byte.

 

Kết quả

 

  • Việc sử dụng bộ nhớ cao điểm giảm từ 20 MB/s → ~0,2 MB/s (giảm 99%).
     

  • Độ ổn định của Redis được khôi phục.
     

  • Máy chủ của chúng tôi hoạt động trơn tru ngay cả trong quá trình tải lên tệp lớn đồng thời.

 

Những điểm chính

 

  1. Cẩn thận với tải ẩn: Truyền dữ liệu lớn vào các tác vụ Celery có vẻ vô hại cho đến khi nó được mở rộng.
     

  2. Redis không phải là kho dữ liệu: Hãy sử dụng nó cho các khóa/giá trị nhẹ, không phải để lưu trữ tải lớn.
     

  3. Luôn đo lường tác động bộ nhớ: Vài KB mỗi khóa là ổn… cho đến khi bạn nhân nó với hàng nghìn tác vụ/giây.
     

  4. Thiết kế cho quy mô sớm: Ngay cả những thiếu hiệu quả “nhỏ” cũng có thể gây ra sự cố lớn trong lưu lượng truy cập thực tế.

 

Suy nghĩ cuối cùng

 

Những sự cố như thế này rất đau đớn nhưng lại rất quý giá. Chúng buộc chúng ta phải xem xét lại các giả định, cải thiện kiến trúc và xây dựng các hệ thống mạnh mẽ hơn. Trong trường hợp của chúng tôi, đây là lời nhắc nhở rằng hiệu quả trong thiết kế tác vụ nền cũng quan trọng như tính chính xác của logic nghiệp vụ.

 

Chúng tôi hy vọng việc chia sẻ điều này sẽ giúp những người khác tránh được sự cố tương tự. 

Tag list:

Theo dõi

Theo dõi bản tin của chúng tôi và không bao giờ bỏ lỡ những tin tức mới nhất.