Hãy tưởng tượng mua một cái két sắt, mở hộp ra thì thấy nó nằm sẵn với cửa mở toang, mã đặt là 0000, và nhà sản xuất kê nó ngay lối ra vào cho bạn dễ khiêng. Không phải hãng làm ẩu — họ tối ưu cho việc bạn lắp thử được ngay, và một cái két khoá kín từ trong hộp thì phiền lúc dựng. Nhưng nếu bạn cất tiền vào rồi để nguyên hiện trạng, đó không còn là lỗi của hãng. docker run -d redis giao cho bạn đúng cái két đó: nó chạy được ngay, và bài này đo xem "chạy được ngay" thật ra cho bạn cái gì — cùng những mã khoá bạn phải tự vặn trước khi cất dữ liệu thật.

Mặc định của ảnh redis chính thức

maxmemory          0            không giới hạn
maxmemory-policy   noeviction   đầy thì TỪ CHỐI GHI
requirepass        (trống)      không mật khẩu
bind               * -::*       nghe mọi giao diện
protected-mode     no           TẮT
timeout            0            không đóng kết nối nhàn rỗi
appendonly         no           chỉ có RDB, mất tối đa 1 giờ

Dòng thứ năm là dòng đáng chú ý nhất: ảnh chính thức tự tắt protected-mode. protected-mode là cơ chế Redis thêm vào từ 3.2 để chặn kết nối từ ngoài khi chưa đặt mật khẩu. Ảnh Docker tắt nó — hợp lý cho việc phát triển, vì container luôn được truy cập từ ngoài, nhưng nó gỡ bỏ lớp bảo vệ cuối cùng.

Tôi chạy FLUSHALL từ một container khác

PING        ->  PONG
CONFIG GET  ->  maxmemory 0
FLUSHALL    ->  OK

Không mật khẩu, không cấu hình gì. Bất kỳ container nào trên cùng mạng đều xoá sạch dữ liệu được. Với mạng Docker nội bộ thì đây là rủi ro chấp nhận được. Nhưng chỉ cần một dòng -p 6379:6379 là cổng đó ra tới giao diện mạng của máy chủ, và nếu máy chủ có IP công khai thì bạn vừa đặt một Redis mở lên Internet. Đây là nguyên nhân của phần lớn các vụ Redis bị xâm nhập — không phải mật khẩu yếu, mà là không có mật khẩu nào cả.

--memory của Docker không bảo vệ được Redis

Container --memory 128m, nạp 600.000 khoá:

Cấu hình Kết quả
maxmemory 0 OOMKilled, ExitCode 137
maxmemory 80mb, nạp trong Lua OOMKilled
maxmemory 80mb, nạp từ ngoài Sống

Dòng cuối, chi tiết:

used_memory        80,00M
used_memory_rss    88,06M
đuổi 319.257 khoá, giữ 280.743
container          83,99 MiB / 128 MiB

Redis không đọc giới hạn cgroup. Nó không biết container bị giới hạn 128 MB; nó chỉ biết maxmemory, và nếu bạn không khai thì nó lớn tới lúc kernel giết tiến trình. Đặt --memory mà không đặt maxmemory chỉ thay đổi cách Redis chết: thay vì máy chủ hết RAM, container bị giết. Dữ liệu mất như nhau.

Dòng thứ hai: Lua vẫn vượt giới hạn

maxmemory 80mb mà nạp bằng một script Lua vẫn bị OOMKilled. Lý do nối lại với phần 14: Redis kiểm maxmemory một lần ở đầu script, không kiểm từng lệnh bên trong. 600.000 lệnh SET trong một script chạy hết mà không đuổi khoá nào — và vượt hẳn giới hạn container.

Nạp cùng lượng dữ liệu từ ngoài, mỗi lệnh là một lệnh riêng, thì maxmemory được kiểm mỗi lần và LRU đuổi đều đặn: 319.257 khoá bị đuổi, container ổn định ở 84/128 MiB. Bài học: script Lua dài có thể phá vỡ mọi giới hạn bộ nhớ bạn đặt. Đây là một lý do nữa để giữ script ngắn, bên cạnh chuyện chặn máy chủ đã đo ở phần 14.

Cấu hình tối thiểu cho môi trường thật

services:
  redis:
    image: redis:7-alpine
    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 800mb
      --maxmemory-policy allkeys-lru
      --appendonly yes
      --appendfsync everysec
      --timeout 300
      --save ""
    volumes:
      - redis-data:/data
    mem_limit: 1200m
    sysctls:
      net.core.somaxconn: 1024
    ports:
      - "127.0.0.1:6379:6379"

Từng dòng có lý do đo được trong sê-ri:

  • requirepass — vì protected-mode đã tắt.
  • maxmemory 800mb với mem_limit 1200m — chừa 50% cho copy-on-write khi fork (phần 18), bộ đệm khách (phần 30), và phân mảnh (phần 10). Tỷ lệ 2:3 là điểm khởi đầu an toàn.
  • allkeys-lru thay cho noeviction — trừ khi Redis là kho dữ liệu chính (phần 9).
  • appendonly yes — vì RDB mặc định mất tối đa một giờ (phần 20).
  • timeout 300 — vì mặc định 0 để kết nối rò rỉ sống mãi (phần 31).
  • 127.0.0.1:6379:6379 — chỉ nghe trên loopback của máy chủ, không phải mọi giao diện.

Bốn điều khác đáng biết

Volume là bắt buộc nếu bật lưu trữ. Không có volume, /data nằm trong lớp ghi của container và biến mất khi container bị xoá — kể cả khi appendonly yes.

CONFIG SET không bền. Như đo ở phần 19: CONFIG SET appendonly yes mất khi khởi động lại container. Mọi thứ phải nằm trong command hoặc tệp cấu hình.

Cảnh báo lúc khởi động đáng đọc. Ảnh này in cảnh báo về somaxconn và overcommit_memory nếu máy chủ chưa chỉnh. Cái thứ hai đặc biệt quan trọng: vm.overcommit_memory=0 có thể làm fork cho BGSAVE thất bại, và đó là bản sao lưu của bạn.

Kiểm tra tình trạng nên gọi PING chứ đừng chỉ xem cổng mở. Redis đang nạp RDB vẫn mở cổng nhưng trả LOADING cho mọi lệnh — như đo ở phần 18.

Mẫu số chung: mặc định tối ưu cho "chạy được ngay", không cho "an toàn khi dùng thật"

Cái bẫy ở đây không riêng gì Redis: mặc định của gần như mọi công cụ được chọn để lần chạy đầu thành công — cài xong gõ một lệnh là thấy nó sống — chứ không phải để an toàn ở production. Mỗi mặc định thuận-tiện ấy là một quyết định mà bạn chưa đưa ra, và "chạy được ngay" bị nhầm thành "sẵn sàng dùng thật" là nguồn của rất nhiều sự cố.

Trong thế giới Java, đây đúng là mặc định của Spring Boot: một datasource H2 in-memory sống động ngay (và mất sạch khi restart), Actuator phơi endpoint không xác thực, ddl-auto tiện cho dev mà nguy ở production. Tất cả tối ưu cho "clone về chạy mvn spring-boot:run là thấy trang" — không cho việc bạn quên đổi chúng khi lên thật.

Trong Go, http.Server{} mặc định không có ReadTimeout/WriteTimeout nào — chạy hoàn hảo trong demo, nhưng phơi ra Slowloris và kết nối treo vô hạn ở production; mặc định "trống" ấy là một quyết định an toàn bạn phải tự thêm, đúng như timeout 300 ở trên.

Còn ở cơ sở dữ liệu, đây là pg_hba.conf với trust cho local, mật khẩu postgres/postgres trong mọi hướng dẫn, shared_buffers mặc định nhỏ xíu — tiện cho lần đầu docker run postgres, thảm hoạ nếu bê nguyên lên máy có IP công khai. Cùng một logic "ảnh chính thức tắt protected-mode".

Mẫu số chung: mặc định của một công cụ là hợp đồng ngầm tối ưu cho trải nghiệm-lần-đầu và tính tiện, không phải cho an toàn hay bền vững — nên mỗi giá trị mặc định là một quyết định đã được chọn hộ bạn theo hướng 'dễ khởi động nhất', và ở production bạn phải xét lại từng cái. Nên khi đưa bất cứ thứ gì "chạy được ngay" lên thật, câu hỏi đúng không phải "nó có chạy không" (mặc định đảm bảo có) mà là "những mặc định này được chọn cho ai — cho tôi lúc thử, hay cho hệ thống lúc bị tấn công và quá tải — và cái nào tôi chưa cố ý xác nhận lại". Bảng mặc định ở đầu bài chính là danh sách những quyết định đó.

Muốn tự kiểm container Redis của bạn:

docker exec <container> redis-cli config get \
  maxmemory maxmemory-policy requirepass appendonly protected-mode timeout | paste - -
docker inspect <container> -f 'mem_limit={{.HostConfig.Memory}} ports={{.HostConfig.PortBindings}}'
docker inspect <container> -f 'mounts={{range .Mounts}}{{.Destination}} {{end}}'

Bốn dấu hiệu:

  • requirepass trống và cổng ánh xạ ra 0.0.0.0 — Redis của bạn đang mở.
  • maxmemory 0 với mem_limit khác 0 — container sẽ bị OOMKilled, không phải Redis từ chối ghi.
  • appendonly yes mà không có mount /data — lưu trữ đang ghi vào một nơi sẽ biến mất.
  • maxmemory bằng hoặc gần mem_limit — chưa chừa chỗ cho fork và bộ đệm khách.

Phần sau: khi nào không nên dùng Redis — và dùng gì thay thế.