Một cẩm nang thực dụng cho Odoo 16 đến 19

Mọi hệ thống Odoo đều phình ra ở đúng một nhúm chỗ giống nhau. Một bộ công cụ nhỏ phủ được phần lớn trong số đó. Còn đúng một khoảng trống, lưu trữ lâu dài theo tuổi và theo từng loại bản ghi, đến nay vẫn chưa có lời giải đóng gói sẵn.

Bài này là một cẩm nang năm bước dành cho trưởng nhóm kỹ thuật Odoo và kỹ sư DevOps đang vận hành hệ thống production từ Odoo 16.0 đến 19.0. Chúng ta sẽ xem các byte thực sự nằm ở đâu, xóa cái gì, vì sao chỉ xóa thôi thì đĩa chưa nhỏ lại, cách đưa file đính kèm ra khỏi database, và khuôn mẫu lưu trữ lâu dài mà chưa module nào phủ.

Đây không phải bài về tinh chỉnh PostgreSQL. Chúng ta không đụng tới shared_buffers, work_mem hay ngưỡng autovacuum. Chúng ta đụng tới cấu hình Odoo, các module OCA, và một nhóm nhỏ lệnh PostgreSQL thật sự thu hồi được dung lượng đĩa sau khi Odoo xóa dòng.

Bước 1: Chẩn đoán

Đừng hành động theo cảm tính. Hãy chạy ba truy vấn và một lệnh shell trước khi cài bất cứ thứ gì.

Các bảng lớn nhất, tách riêng TOAST và index:

SELECT c.relname,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total,
       pg_size_pretty(pg_relation_size(c.oid))       AS heap,
       pg_size_pretty(pg_total_relation_size(c.reltoastrelid)) AS toast,
       pg_size_pretty(pg_indexes_size(c.oid))        AS indexes
FROM pg_class c
WHERE c.relkind = 'r'
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
20 bảng PostgreSQL lớn nhất theo tổng dung lượng, tách riêng heap, toast và index

Cột toast thường là chỗ gây bất ngờ. Trên một database Odoo điển hình, mail_message.body (phần thân HTML của các thông báo) và ir_attachment.db_datas (khi file đính kèm lưu trong database) đẩy hàng gigabyte vào bảng phụ TOAST mà lời gọi pg_relation_size cơ bản không cho thấy.

Cơ cấu nơi lưu file đính kèm:

SELECT type, COUNT(*), pg_size_pretty(SUM(file_size))
FROM ir_attachment
GROUP BY type;
Các dòng ir_attachment nhóm theo type, cho thấy số lượng url và binary cùng tổng dung lượng binary

Truy vấn này cho biết các byte đang nằm trong PostgreSQL (binary với db_datas có dữ liệu), trong filestore, hay đã ở trên cloud (kiểu cloud_storage, có từ Odoo 18.0 trở đi).

Lượng chatter theo từng model:

SELECT model, COUNT(*)
FROM mail_message
GROUP BY model
ORDER BY 2 DESC
LIMIT 20;
Số dòng mail_message nhóm theo model, với account.move đứng đầu

Phần đầu danh sách đó (thường là account.move, stock.picking, sale.order ở hầu hết hệ thống) cho bạn biết model nào có dấu vết thông báo dày đặc nhất.

Dung lượng filestore trên đĩa: du -sh <data_dir>/filestore/<dbname>. Kết quả mẫu trên một hệ thống production cỡ vừa:

$ du -sh /var/lib/odoo/filestore/prod
42G     /var/lib/odoo/filestore/prod

Hãy so với truy vấn cơ cấu nơi lưu file đính kèm ở trên. Một filestore lớn không phải là phình vô ích. Một filestore lớn cộng với ir_attachment.db_datas lớn mới là.

Nếu bạn dùng CLI odoo-db của Trobz, ba lệnh phủ được phần lớn những việc trên: odoo-db stats <db> cho số bản ghi và dung lượng theo từng bảng kèm mức tăng trưởng theo năm, odoo-db bloat <db> cho phần phình có thể thu hồi ở bảng và index (chính xác qua pgstattuple khi extension đã cài, kèm tỷ lệ tuple chết, autovacuum bị ì và index không dùng đến), và odoo-db attachments <db> cho phân bố nơi lưu file đính kèm cùng danh sách ứng viên dọn dẹp và lưu trữ.

Đến đây bạn đã biết thủ phạm. Thứ tự xử lý: dọn, nén, đưa ra ngoài, lưu trữ lâu dài.

Bước 2: Dọn

Hãy bắt đầu bằng thứ Odoo đã cho sẵn. Cron ir.attachment._gc_file_store() có sẵn chạy hằng ngày và xóa các file trong filestore mà dòng ir.attachment tương ứng đã bị xóa, cộng thêm các dòng đính kèm trỏ tới bản ghi không còn tồn tại (res_model cộng res_id mồ côi). Nó không xóa mail.message hay các dòng đính kèm theo tuổi. Nếu dung lượng filestore của bạn lớn hơn hẳn tổng ir_attachment tương ứng ở bước 1, hãy kiểm tra cron này có đang bật và không bị lỗi, trước khi cài thêm bất cứ thứ gì.

Ngoài mail.message và ir.attachment, còn bốn bảng nữa phình lên âm thầm trên các hệ thống bận rộn. Hãy đối chiếu từng bảng với top 20 ở bước 1:

  • mail.notification (mỗi người nhận mỗi tin nhắn một dòng — nhân lên theo số người theo dõi phục vụ kiểm toán)
  • mail.tracking.value (mỗi lần đổi một trường được theo dõi là một dòng — account.move và stock.picking chiếm đa số)
  • bus.bus (hàng đợi thông báo thời gian thực — có cron GC riêng từ 16.0; hãy kiểm tra nó đang bật)
  • ir.logging (chỉ có dữ liệu khi log_db được đặt trong file INI của máy chủ; nếu không thì rỗng)

Module OCA nói ở phần tiếp theo đã tự động lan sang mail.tracking.value và mail.followers. Các bảng còn lại cần câu DELETE có phạm vi riêng nếu chúng đang phình.

Phần lớn mức tăng của mail.message là những dòng bạn sẽ không bao giờ xem lại: thay đổi trạng thái, thông báo tự động, sự kiện theo dõi. Hãy xóa chúng một cách an toàn bằng module autovacuum_message_attachment của OCA.

Module này mang theo hai cron (AutoVacuum Mails and Messages và AutoVacuum Attachments) và một bảng quy tắc tại Configuration, Technical, Email, Message And Attachment Vacuum Rules. Mỗi quy tắc cho tin nhắn kết hợp một model, một kiểu hoặc phân kiểu tin nhắn, và một thời hạn giữ tính bằng ngày. Mỗi quy tắc cho file đính kèm kết hợp một model, một chuỗi con trong tên, và một thời hạn giữ. Việc xóa tự động lan sang mail.tracking.value và mail.followers.

Nhịp chạy khuyến nghị: hằng ngày, vào giờ thấp điểm. Hãy bắt đầu với thời hạn giữ dè dặt (180 ngày cho thông báo, 365 ngày cho file đính kèm) rồi siết lại sau vài chu kỳ.

Một cái bẫy: quy tắc cho file đính kèm khớp theo chuỗi con trong tên. Một quy tắc với chuỗi report cộng model account.move thì không sao. Một quy tắc chỉ có chuỗi report sẽ đụng vào mọi file đính kèm trong database có chứa từ đó trong tên. Hãy luôn giới hạn kèm theo model.

Với luồng thư đến qua IMAP, module mail_cleanup của OCA bổ trợ cho những thứ trên bằng cách đánh dấu, di chuyển hoặc xóa hẳn thư ngay trên máy chủ IMAP, trước khi fetchmail nạp chúng vào mail.message. Cấu hình nằm trong file INI của máy chủ với các khóa cleanup_days, purge_days và cleanup_folder. Nó chỉ xử lý thư đến, không xử lý chatter nội bộ hay thông báo.

Ràng buộc phiên bản cần nhớ: cả hai module đều có trên các nhánh OCA 14.0, 16.0 và 18.0. Chúng không có trên 15.0, 17.0 hay 19.0 tại thời điểm viết bài. Trên các nhánh đó, hoặc bạn tự backport module, hoặc quay về viết script SQL xóa có phạm vi theo model, subtype_id và create_date.

Bước 3: Nén

Bạn xóa một triệu dòng. Đĩa không nhỏ lại. Đây là chuyện của PostgreSQL, không phải của Odoo.

Trang trong PostgreSQL có kích thước 8 kilobyte. Một tuple không thể trải ra nhiều trang, nên khi một dòng vượt quá khoảng 2 kilobyte, PostgreSQL nén các giá trị lớn của nó và, nếu vẫn còn quá lớn, chuyển chúng ra ngoài vào một bảng phụ TOAST (mỗi bảng cha một bảng phụ). Khi bạn DELETE dòng cha, tuple chết ở cả bảng chính lẫn bảng TOAST chỉ được đánh dấu là chết chứ không được giải phóng. Phần đĩa đã cấp vẫn nằm nguyên cho tới khi có thứ gì đó thu hồi nó.

Ba đường thu hồi, xếp theo chi phí vận hành:

pg_repack (pg_repack -t mail_message <db>) dựng lại bảng ngay khi đang chạy mà không cần khóa độc quyền. Cần extension pg_repack do superuser cài. An toàn cho production. Trên các dịch vụ PostgreSQL có quản lý như AWS RDS hay Aiven, hãy kiểm tra extension có sẵn không trước khi trông cậy vào nó. Một lần chạy mẫu:

$ pg_repack -d odoo_prod -t mail_message
INFO: repacking table "public.mail_message"
NOTICE: Setting up workspaces
NOTICE: Copying tuples
NOTICE: Swapping tables
NOTICE: Dropping the old tables

VACUUM (FULL, VERBOSE, ANALYZE) mail_message ghi lại bảng ngay tại chỗ. Giữ khóa độc quyền trong suốt thời gian chạy. Hãy xem nó như một lần ngừng dịch vụ có kế hoạch, dài tỷ lệ với kích thước bảng: một bảng mail_message 50 GB sẽ khóa hàng chục phút. Dùng trên staging, hoặc trong một khung bảo trì đã lên lịch. Kết quả mẫu:

INFO:  vacuuming "public.mail_message"
INFO:  "mail_message": found 0 removable, 412903 nonremovable row versions in 28741 pages
DETAIL:  0 dead row versions cannot be removed yet.
CPU: user: 1.34 s, system: 0.48 s, elapsed: 18.27 s.
INFO:  analyzing "public.mail_message"
VACUUM

pg_dump -Fc | pg_restore vào một database mới ghi từng bảng một cách tuần tự sang database mới. Không còn phần phình nào ở đích. Thời gian ngừng dịch vụ nặng nhất, nhưng đây cũng là cách duy nhất thu hồi được dung lượng trên các dịch vụ có quản lý chặn VACUUM FULL hoặc pg_repack. Hãy gộp nó vào lần nâng cấp Odoo lớn kế tiếp.

Chọn theo ngân sách ngừng dịch vụ của bạn.

Bước 4: Đưa ra ngoài

Một ir.attachment luôn có một dòng trong database. Phần byte thì có thể nằm ở một trong ba nơi: một cột bytea của PostgreSQL (db_datas), thư mục filestore (store_fname trỏ tới một file đặt tên theo sha1 dưới <data_dir>/filestore/<dbname>/), hoặc kho lưu trữ đám mây bên ngoài.

Lựa chọn này do tham số hệ thống ir_attachment.location điều khiển (db hoặc file, mặc định là file), cộng thêm bộ module cloud_storage từ Odoo 18.0 trở đi.

@api.model
def _storage(self):
    return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file')

Cái bẫy quan trọng: đổi ir_attachment.location chỉ ảnh hưởng tới các lần tải lên mới. Các dòng sẵn có vẫn nằm nguyên chỗ cũ. Nếu bạn chuyển từ db sang file và mong phần phình ở db_datas biến mất, sẽ chẳng có gì dịch chuyển cho tới khi bạn chạy thao tác quản trị Force Storage (Settings, Technical, Database Structure, Attachments, Action, Force Storage) hoặc, trên 18.0 và 19.0 có cloud storage, chạy cron cloud_storage_migration. Hãy lên kế hoạch cho việc di chuyển tách bạch với việc đổi tham số.

Quyết định rẽ nhánh theo phiên bản Odoo và backend đích.

Trên Odoo 18.0 hoặc 19.0, với Azure Blob hoặc Google Cloud Storage: cài các module gốc cloud_storage cộng với cloud_storage_azure hoặc cloud_storage_google, và có thể thêm cloud_storage_migration để chuyển các file sẵn có. File đính kèm mới sẽ mang type='cloud_storage', và việc tải xuống được phục vụ bằng một lần chuyển hướng có chữ ký: Odoo ký một URL ngắn hạn, client tải thẳng từ cloud, và Odoo không bao giờ tự truyền các byte đó. Để chuyển các file sẵn có, hãy điền vào hai tham số hệ thống cloud_storage_migration_all_models và cloud_storage_migration_message_models các model bạn muốn đẩy ra ngoài, rồi kích hoạt cron Migrate Local Attachment Binaries to Cloud Storage. Cron duyệt từng file đính kèm một, với ngưỡng tuổi tối thiểu nằm sẵn trong truy vấn (rút gọn từ cloud_storage_migration/models/ir_attachment.py):

query = SQL("""
    SELECT ia.id FROM ir_attachment ia
    WHERE ia.id <= %(max_attachment_id)s
      AND ia.type = 'binary'
      AND ia.store_fname IS NOT NULL
      AND ia.create_date < %(create_date)s
      -- plus filters on file_size, url, res_id, res_field,
      -- model scope, and documents.document linkage
    ORDER BY ia.id ASC LIMIT 1;
""",
    create_date=fields.Datetime.now() - timedelta(days=7),
)

Ngưỡng bảy ngày đó được gán cứng. Nó là một lan can an toàn, không phải một thời hạn giữ cấu hình được.

Trên Odoo 16.0 hoặc 17.0, hoặc với bất kỳ backend nào khác ngoài Azure và GCS (S3, MinIO, OVH, Wasabi, SFTP, WebDAV): cài fs_storage cộng fs_attachment của OCA. Model fs.storage bọc thư viện Python fsspec và chấp nhận mọi giao thức mà fsspec phơi ra. fs_attachment mở rộng ir.attachment để định tuyến các byte theo từng model hoặc từng trường qua các backend đó, giữ nguyên tên file gốc (không còn tên sha1 khó hiểu), và tích hợp với X Sendfile để nginx phục vụ thẳng các khối dữ liệu. JSON Force DB For Default Attachment Rules giữ các ảnh nhỏ, bundle JavaScript và CSS trong PostgreSQL vì lý do hiệu năng: mặc định giữ lại trong database mọi ảnh dưới 50 kilobyte cùng toàn bộ asset.

Một cái bẫy cần tránh: OCA cũng có một họ module cũ hơn (storage_backend, storage_file, attachment_s3) do cùng nhóm tác giả. Trên Odoo 18.0 chúng vẫn cài được, nhưng dự án mới nên dùng fs_storage và fs_attachment. Họ module cũ chỉ còn ở chế độ sửa lỗi.

Lưu ý về sao lưu: cột db_datas có attachment=False trong định nghĩa trường. Công cụ sao lưu filestore của Odoo không lấy nó. Chỉ pg_dump mới lấy. Nếu bạn đang ở chế độ lưu db và chiến lược sao lưu của bạn giả định là filestore cộng với một bản dump lược đồ logic không có phần nhị phân, thì bạn không có bản sao lưu nào cho các khối dữ liệu của mình.

Bước 5: Lưu trữ lâu dài

Đây là chỗ mọi công cụ ở trên đều dừng lại.

Khách hàng Odoo thật sự có những yêu cầu lưu giữ kéo dài nhiều năm. Kế toán giữ hóa đơn theo khung luật định (thường là mười năm), nhưng khoảng thời gian truy cập nóng chỉ là một năm. Nhân sự giữ phiếu lương năm năm, nhưng phần lớn lượt đọc diễn ra trong năm hiện tại. Sản xuất giữ báo cáo sản xuất theo khung quy định, nhưng bộ phận vận hành chỉ đụng tới chúng trong quý hiện tại.

Khuôn mẫu các khách hàng này mong muốn khá đơn giản: sau N ngày hoặc N tháng, phân theo từng loại bản ghi, chuyển file đính kèm từ kho nóng sang một tầng lạnh, theo kiểu bất đồng bộ, có thử lại, và nạp lại theo kiểu lười khi có người mở tài liệu.

Hôm nay chưa module nào phủ chuyện này.

cloud_storage_migration gốc là gần nhất, nhưng nó vướng ba thứ. Nó chỉ hỗ trợ Azure và Google (không có S3, không có fsspec). Bộ lọc theo tuổi của nó là một ngưỡng bảy ngày gán cứng nhằm bảo vệ các file mới, chứ không phải một thời hạn giữ cấu hình được theo từng model. Nó chạy trong một worker cron duy nhất, không có cơ chế thử lại kiểu queue_job.

fs_attachment của OCA thì không có chính sách theo tuổi nào cả. Nó định tuyến file đính kèm mới theo model hoặc theo trường, nhưng việc định tuyến đó là tĩnh.

Tra cứu nhanh

Toàn bộ ma trận phiên bản ở dạng văn bản, để bạn grep ra đúng ô của mình:

Chẩn đoán: các truy vấn SQL trên Odoo 16.0, 17.0, 18.0, 19.0. Không khác nhau về công cụ.

Dọn trong database: autovacuum_message_attachment của OCA trên 14.0, 16.0 và 18.0. Không có module trên 15.0, 17.0 hay 19.0; hãy backport hoặc chạy SQL có phạm vi.

Dọn thư đến qua IMAP: mail_cleanup của OCA trên cùng các nhánh nói trên. Vẫn thiếu ở 15.0, 17.0 và 19.0.

Nén: pg_repack hoặc VACUUM FULL hoặc dump rồi restore bằng pg_dump. Mọi phiên bản Odoo, tùy bạn chọn theo ngân sách ngừng dịch vụ.

Đưa file đính kèm mới ra Azure hoặc GCS: module gốc cloud_storage trên 18.0 và 19.0. Không có trên 16.0 hay 17.0.

Đưa file đính kèm mới ra S3, MinIO, OVH, SFTP, WebDAV và các backend khác: fs_storage cộng fs_attachment của OCA trên 16.0, 17.0 và 18.0. Bản port 19.0 chưa được công bố tại thời điểm viết bài; hãy kiểm tra repo storage của OCA trước khi trông cậy vào nó.

Chuyển các file đính kèm sẵn có, một lần, không bộ lọc: thao tác quản trị force_storage() có sẵn trên mọi phiên bản.

Chuyển các file đính kèm sẵn có sang Azure hoặc GCS, theo từng model với giới hạn theo lô: cron cloud_storage_migration trên 18.0 và 19.0, kèm ngưỡng bảy ngày mô tả ở trên.

Kết luận

Hãy đo trước, sửa theo thứ tự (dọn, rồi nén, rồi đưa ra ngoài), sau đó chọn công cụ theo phiên bản Odoo nhân với backend. Nếu bài toán của bạn là lưu trữ lâu dài theo tuổi và theo từng loại bản ghi, hãy liên hệ với chúng tôi: chúng tôi đang làm một module attachment_archive_policy chưa công bố nhưng có thể hợp với nhu cầu của bạn.