Nhật ký công việc cho nhiệm vụ "Tìm hiểu nguyên nhân tại sao MySQL tiêu tốn quá nhiều bộ nhớ trên HappyBaby"

23 сент. 2026 г., 03:56:44

Chẩn đoán mức tiêu thụ bộ nhớ bất thường của MySQL 5.7 trong Docker và cách khắc phục

Trong quá trình làm việc với môi trường Docker, một sự cố không điển hình đã được phát hiện khi khởi động container mysql:5.7: trước khi khởi động bình thường, container đã tăng vọt mức tiêu thụ bộ nhớ trong vài phút, đạt khoảng một phần tư RAM khả dụng của máy chủ (host), sau đó giải phóng bộ nhớ và lặp lại chu kỳ. Trên một máy có RAM ~64 GB, các mức tăng vọt đạt khoảng 16 GB; trong môi trường có RAM ~2 GB, mức tiêu thụ là khoảng 500 MB. Sau một vài chu kỳ như vậy, mức tiêu thụ đã ổn định ở mức bình thường.

Cấu hình MySQL ban đầu gần như là tối thiểu:

mysql:
  image: mysql:5.7
  environment:
    - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-your_mysql_password}

Không có cài đặt riêng biệt nào cho innodb_buffer_pool_size hoặc giới hạn bộ nhớ (memory limit).

Quan sát ban đầu

Theo biểu đồ docker stats, có thể thấy hình "răng cưa" đặc trưng: tiến trình nhanh chóng chiếm lấy hàng gigabyte bộ nhớ, đạt đến một giới hạn nhất định, sau đó bộ nhớ được giải phóng gần như hoàn toàn, rồi chu kỳ lặp lại. Đồng thời, ở giai đoạn đầu khởi động hầu như không có nhật ký (log), tạo cảm giác như MySQL chỉ đang khởi tạo trong thời gian dài mà không có bất kỳ thông báo nào.

Sau khi khởi động với thư mục dữ liệu (datadir) đã được làm trống, chúng tôi đã có thể thấy trình tự chính xác của các sự kiện. Trước khi sửa lỗi, tệp nhật ký trông như thế này:

03:33:05 Entrypoint script for MySQL Server 5.7.44 started
        ... 37 giây im lặng ...
03:33:42 Switching to dedicated user 'mysql'
03:33:42 Entrypoint script for MySQL Server 5.7.44 started
        ... thêm 37 giây im lặng nữa ...
03:34:19 Initializing database files

Nghĩa là, gần như toàn bộ thời gian bất thường không dành cho việc khởi tạo InnoDB hay tạo các tệp cơ sở dữ liệu, mà dành cho các thao tác của docker-entrypoint.sh, được thực thi trước khi máy chủ khởi động hoàn toàn.

Đồng thời, bản thân MySQL đã báo rõ ràng:

InnoDB: Initializing buffer pool, total size = 128M

Điều này loại trừ giả thuyết cho rằng việc tiêu thụ bộ nhớ lớn có liên quan đến InnoDB buffer pool. Theo mặc định trong cấu hình này, nó chỉ có dung lượng 128 MB.

Nguyên nhân

Vấn đề hóa ra có liên quan đến hành vi đã biết của MySQL 5.7 khi giới hạn tệp mở (RLIMIT_NOFILE) quá lớn.

Docker entrypoint chính thức của MySQL trước khi thực sự khởi động máy chủ sẽ thực hiện các lệnh gọi dịch vụ mysqld, cụ thể là ở chế độ như sau:

mysqld --verbose --help

Lệnh này được sử dụng để kiểm tra cấu hình và lấy các giá trị như datadir, socket và các tham số khác.

Trong MySQL 5.7, có một vấn đề được biết đến: nếu RLIMIT_NOFILE lớn một cách bất thường, việc khởi chạy dịch vụ mysqld --verbose --help có thể kích hoạt việc phân bổ bộ nhớ tạm thời rất lớn. Trong Docker, điều này thể hiện rõ ràng nhất nếu container kế thừa giá trị nofile khổng lồ từ Docker daemon / systemd.

Do đó, trình tự sau đây đã diễn ra:

docker-entrypoint.sh
    ↓
khởi chạy dịch vụ mysqld --verbose --help
    ↓
phân bổ tạm thời khổng lồ do RLIMIT_NOFILE
    ↓
RAM và CPU tăng trong hàng chục giây
    ↓
tiến trình dịch vụ kết thúc
    ↓
bộ nhớ được giải phóng
    ↓
entrypoint thực hiện lệnh gọi tương tự tiếp theo
    ↓
lặp lại sự bùng nổ bộ nhớ

Điều này giải thích đồng thời một số quan sát:

  • các đợt tăng vọt bộ nhớ lớn xảy ra trước khi MySQL khởi động bình thường;
  • bộ nhớ sau mỗi lần tăng vọt đều được trả lại cho hệ thống;
  • có một số chu kỳ có thời lượng bằng nhau;
  • vào thời điểm này hầu như không có nhật ký máy chủ thông thường nào;
  • sau khi quá trình bootstrap hoàn tất, MySQL hoạt động với mức tiêu thụ bộ nhớ bình thường.

Giải pháp

Trong Docker Compose, giới hạn số lượng tệp mở bình thường đã được chỉ định rõ ràng:

mysql:
  image: mysql:5.7
  environment:
    - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-your_mysql_password}

  ulimits:
    nofile:
      soft: 65536
      hard: 65536

Quan trọng: tham số này không phải là giới hạn bộ nhớ của container. Nó chỉ giới hạn số lượng bộ mô tả tệp (file descriptor) mà tiến trình có thể mở đồng thời. Bộ mô tả bao gồm các tệp, socket và kết nối.

Nghĩa là nofile: 65536 không có nghĩa là 64 MB, 64 GB hoặc bất kỳ dung lượng bộ nhớ nào khác và không trực tiếp thiết lập giới hạn bộ nhớ của container dưới bất kỳ hình thức nào.

Giá trị 65536 được chọn làm giới hạn bình thường và đủ lớn cho một MySQL thông thường trong một dự án web. Nó để lại một khoảng dự phòng lớn về số lượng tệp/socket, nhưng không cho phép MySQL 5.7 rơi vào kịch bản sự cố với RLIMIT_NOFILE quá lớn.

Kiểm tra kết quả trên cơ sở dữ liệu hiện có

Sau khi thêm ulimits.nofile, việc khởi động lại datadir hiện có đã thay đổi triệt để:

03:38:54 Entrypoint started
03:38:54 Switching to dedicated user 'mysql'
03:38:54 Entrypoint started
03:38:54 mysqld starting
03:38:54 mysqld: ready for connections

Cả hai khoảng tạm dừng trước đó, mỗi khoảng khoảng 37 giây, đã biến mất hoàn toàn. Máy chủ đã sẵn sàng kết nối gần như ngay lập tức.

Kiểm tra trên datadir hoàn toàn sạch

Để kiểm tra chính xác quá trình khởi tạo ban đầu, dữ liệu MySQL đã được làm sạch thực tế có tính đến việc sử dụng bind mounts:

volumes:
  - ./mysql/data:/var/lib/mysql
  - ./mysql/conf.d:/etc/mysql/conf.d

Sau đó, một cold start hoàn toàn đã được thực hiện. Nhật ký mới:

03:41:42 Entrypoint started
03:41:42 Switching to dedicated user 'mysql'
03:41:42 Entrypoint started
03:41:43 Initializing database files
03:41:45 Database files initialized
03:41:45 Starting temporary server
03:41:49 MySQL init process done. Ready for start up
03:41:49 mysqld: ready for connections

Như vậy, lần khởi động đầu tiên hoàn chỉnh với cơ sở dữ liệu trống mất khoảng 7 giây thay vì vài phút.

Đồng thời, InnoDB vẫn sử dụng bộ đệm tiêu chuẩn:

InnoDB: Initializing buffer pool, total size = 128M

Nghĩa là bản sửa lỗi không làm giảm bộ nhớ làm việc của MySQL bằng một giới hạn nhân tạo — nó loại bỏ chính xác việc phân bổ tạm thời bị lỗi ở giai đoạn bootstrap.

Mức tiêu thụ bộ nhớ thực tế sau khi sửa lỗi

Sau khi khởi động bình thường, docker stats hiển thị:

173.4MiB / 61.5GiB

Nghĩa là container thực sự sử dụng khoảng 173 MB RAM. Con số 61.5GiB bên phải là bộ nhớ máy chủ khả dụng cho container trong trường hợp không có giới hạn bộ nhớ riêng biệt, chứ không phải dung lượng dự trữ của MySQL.

ulimits.nofile không ảnh hưởng trực tiếp đến giá trị này. Để giới hạn RAM của container, cần phải có giới hạn bộ nhớ Docker riêng (mem_limit, resources limits, v.v.), nhưng trong khuôn khổ sự cố này thì không cần thiết.

Tổng kết

Nguyên nhân dẫn đến thời gian khởi động lâu và các đợt tăng vọt bộ nhớ khổng lồ không nằm ở mức tiêu thụ làm việc thực tế của MySQL, không phải ở InnoDB buffer pool và cũng không phải ở dung lượng dữ liệu. Vấn đề gây ra bởi MySQL 5.7 trong các lần chạy dịch vụ từ Docker entrypoint khi RLIMIT_NOFILE quá lớn.

Cách khắc phục:

ulimits:
  nofile:
    soft: 65536
    hard: 65536

Kết quả:

  • các đợt tăng vọt RAM hàng gigabyte lặp đi lặp lại đã biến mất;
  • các khoảng dừng ~37 giây trên các lệnh gọi dịch vụ entrypoint đã biến mất;
  • cold start của cơ sở dữ liệu trống đã giảm xuống còn khoảng 7 giây;
  • mức tiêu thụ bộ nhớ làm việc đã ổn định ở mức khoảng 170–180 MB;
  • giới hạn bộ nhớ của container không được nhập hoặc thay đổi trong quá trình này;
  • nofile=65536 được giữ lại làm giải pháp tạm thời (workaround) vĩnh viễn cho mysql:5.7 trong môi trường Docker hiện tại.