Cụm PaanChat · cấp 1 và cấp 2

Cấu hình chuẩn bị ngày 05/10/2026 · cần nghiệm thu trên các máy triển khai

Đã có adapter PostgreSQL, phiên và giới hạn thao tác dùng chung, thông báo giữa máy ứng dụng, API đồng bộ không gian công việc theo tài khoản, đối chiếu xung đột và kho file mã hóa có bản sao. Kiểm thử cục bộ đã chạy hai máy ứng dụng và ba nút file. Chưa triển khai cụm trên máy thật hoặc diễn tập Patroni/etcd, mất kết nối giữa địa điểm và watchdog. Máy hiện tại vẫn dùng SQLite; dữ liệu thật chưa chuyển đổi.

Người dùng → https://chat.tenmien.vn → Cloudflare Load Balancing
Tunnel riêng cho node1 · node2 · node3 → proxy khỏe → máy ứng dụng khỏe
Ba PostgreSQL/Patroni + ba etcd · một nguồn ghi hiện tại, hai bản dự phòng
Tầng file cấp 2 · ba nút độc lập · xác nhận sau ít nhất hai bản sao

Subdomain thuộc tên miền của bạn

Địa chỉVai trò
chat.tenmien.vnĐịa chỉ chung cho đăng nhập và người dùng; PUBLIC_URL giống nhau ở mọi máy.
node1.tenmien.vn, node2.tenmien.vn, node3.tenmien.vnĐi tới tunnel của từng máy. Dùng kiểm tra /health/ready; không tạo các phiên đăng nhập độc lập theo từng subdomain.
10.60.0.11–13 trong ví dụIP WireGuard riêng dùng cho database, etcd, API nội bộ và file. Thay bằng mạng riêng thực tế của bạn.
  1. Đưa DNS của domain vào cùng tài khoản Cloudflare. Tạo ba named tunnel và cài cloudflared như dịch vụ trên ba máy riêng.
  2. Mỗi tunnel có route chat.tenmien.vn → http://127.0.0.1:3000 và route nodeN.tenmien.vn → http://127.0.0.1:3000. File cloudflared.yml được bộ tạo cấu hình sinh sẵn; thay UUID và đặt credential đúng /etc/cloudflared, chỉ tài khoản dịch vụ đọc được.
  3. Tạo CNAME riêng cho nodeN tới UUID.cfargotunnel.com hoặc dùng cloudflared tunnel route dns TUNNEL_UUID nodeN.tenmien.vn. Địa chỉ chat chung do Load Balancing quản lý; không tạo ba bản CNAME cùng tên.
  4. Tạo Cloudflare public Load Balancer tại chat.tenmien.vn, endpoint từng tunnel là UUID.cfargotunnel.com, Host header là chat.tenmien.vn. Gắn HTTPS monitor, path /health/ready, expected code 200. Load Balancing là dịch vụ riêng cần kiểm tra gói đang dùng. DNS round-robin đơn thuần không thay thế kiểm tra ứng dụng.
  5. Bypass cache /api/*, /superadmin và dữ liệu có phiên. Giữ no-store. SSE cần giữ kết nối, không buffering; có heartbeat 15 giây và tải lại dữ liệu khi kết nối trở lại. Thử thực tế qua Cloudflare trước khi nghiệm thu.

Cloudflare: hostname, DNS và cân bằng tải qua tunnel. Named tunnel và nhiều replica cùng UUID có thể cung cấp dự phòng cơ bản; tunnel riêng cho từng máy giúp kiểm tra và chọn endpoint độc lập. Quick Tunnel phục vụ thử nghiệm, không dùng làm địa chỉ cố định.

Chuẩn bị ba máy độc lập

Mỗi máy có Docker Engine + Compose, ổ đĩa bền, IP WireGuard cố định, đồng hồ NTP và watchdog hoạt động. Có thể đặt các máy ở nhiều nơi, nhưng độ trễ giữa nơi sẽ ảnh hưởng tốc độ xác nhận ghi. Ba container trên cùng một máy chỉ kiểm tra phần mềm, không chịu được mất máy vật lý. Tách các nút file sang máy khác được bằng STORAGE_NODES, luôn giữ ít nhất ba nút để tiếp tục ghi hai bản sao sau một lỗi.

Cổng 2379/2380, 5432/8008, 3002 và 3100 chỉ nhận qua VPN giữa các máy cụm. Proxy web lắng nghe localhost:3000; router database localhost:6432 và IP VPN của máy:6432 để các node native macOS/Windows/Linux kết nối. Firewall chỉ cho IP node trong cụm tới router; mỗi node chọn một proxy HA riêng để tránh phụ thuộc một máy. Không công bố database, etcd, Patroni hoặc object server qua subdomain công khai. Cấu hình ví dụ dùng HTTP/PostgreSQL trên IP VPN; mã hóa đường truyền được WireGuard đảm nhiệm. Nếu bỏ VPN, phải chuyển sang TLS/mTLS và chứng thư kiểm tra đúng tên.

  1. Build image PaanChat bằng Dockerfile tại gốc dự án, build image database bằng deploy/cluster/patroni/Dockerfile. Đẩy vào registry tin cậy, ghi lại digest SHA-256; ký và kiểm tra bản phát hành theo quy trình của đơn vị vận hành.
  2. Copy topology.example.json thành topology.json. Điền domain, ba IP riêng, UUID tunnel, group ID có quyền đọc/ghi /dev/watchdog, appImage và patroniImage dạng registry/image@sha256:... . Pin cả image etcd/proxy theo digest khi triển khai chính thức.
  3. Kiểm tra watchdog của từng máy theo hướng dẫn Patroni. Cấu hình đặt mode=required: máy không có watchdog đúng quyền sẽ không được trở thành chủ ghi. Container chỉ được cấp thiết bị watchdog đã chọn; không bật privileged toàn bộ. Với VM cần thống nhất cơ chế fencing của nền tảng với người vận hành trước.
  4. Chạy lệnh bên dưới tại dự án. Bộ tạo kiểm tra ba node riêng, domain và digest; sinh file 0600, không ghi đè thư mục đã có. Với dữ liệu cũ, truyền shared-keys.json đã chứa đúng PRIVATE_DATA_KEY và ARCHIVE_SIGNING_KEY hiện tại, tuyệt đối không sinh khóa thay thế.
node deploy/cluster/prepare.mjs topology.json deploy/cluster/generated [shared-keys.json]
# Copy node1/* lên máy 1, node2/* lên máy 2, node3/* lên máy 3.
# Giữ shared-keys.json trong kho bí mật ngoài Git và ngoài image.
docker compose -f compose.json up -d etcd postgres proxy storage
# Sau khi Patroni có một primary và hai replica:
docker compose -f compose.json up -d app

Ba node etcd cần cùng initial-cluster và cluster token khi dựng mới. Không đổi token hay initial-cluster-state để cố sửa cụm đang có dữ liệu. Cấu hình bootstrap Patroni chỉ áp dụng lần khởi tạo đầu; đổi tham số động bằng patronictl theo tài liệu chính thức. PostgreSQL dùng quorum synchronous mode, strict=true, synchronous_node_count=1, timeline checks và pg_rewind. Patroni quản lý việc chọn primary; ứng dụng chỉ ghi qua router tới primary hiện tại.

Ứng dụng dùng role workchat, không dùng superuser hoặc quyền đọc mọi thống kê phiên. Bootstrap cài workchat_ops.sync_ready() bằng operator, chỉ trả về boolean primary có standby đồng bộ. CLUSTER_REQUIRE_SYNC=1 từ chối readiness khi thiếu hàm, thiếu quyền hoặc chưa có standby. Với cụm đã khởi tạo trước bản này, operator cần chạy deploy/cluster/patroni/readiness.sql trong database workchat; không cấp pg_read_all_stats cho ứng dụng để thay thế.

Patroni: chế độ replication và giới hạn mất dữ liệu, watchdog/fencing, cấu hình bootstrap. Khi không đủ quorum, hệ thống tạm ngừng ghi; không ép một máy tách mạng thành chủ ghi mới. Yêu cầu ghi bị timeout có thể chưa xác định được kết quả: đọc lại và thử cùng mã thao tác, không tạo mã tin nhắn mới để gửi lại.

Chuyển dữ liệu cũ

  1. Tạm dừng ghi ở máy SQLite cũ, tạo snapshot nhất quán bằng SQLite backup/VACUUM INTO; không copy riêng file .sqlite trong khi WAL còn hoạt động. Giữ bản gốc và hai file khóa ngoài cụm.
  2. Khởi tạo schema PostgreSQL ở cụm mới rồi dừng các máy ứng dụng. Dùng cùng khóa mã hóa/ký và cấu hình feature đã có ở nguồn. Chạy dry-run, đối chiếu số bản ghi rồi apply vào database chưa có người dùng.
  3. Công cụ chuyển dùng một transaction, giữ ID, dữ liệu và thứ tự sequence, tái tạo chỉ mục tìm kiếm PostgreSQL. Dừng nếu thiếu bảng tương ứng; không bỏ qua dữ liệu không nhận biết. Với tập lớn cần kế hoạch COPY/batch và thời gian ngừng ghi riêng.
  4. Đối chiếu tài khoản, chat, quyền, avatar, hồ sơ mã hóa và bản lưu ZIP cũ; mở ba app node. Công việc/file đã nằm trên trình duyệt chỉ được tải lên theo tài khoản khi bật đồng bộ và người dùng mở thiết bị đó; chuyển database không tự lấy được dữ liệu ở những máy người dùng chưa kết nối.
npm run migrate:postgres -- /duong-dan/snapshot.sqlite --dry-run
npm run migrate:postgres -- /duong-dan/snapshot.sqlite --apply

File cấp 2 và dữ liệu công việc

File được mã hóa AES-256-GCM trước khi đưa xuống nút lưu trữ. Request giữa app và nút file có HMAC, hạn thời gian, checksum và chống phát lại trong phiên tiến trình. File là bất biến theo digest ciphertext. API gắn file với tài khoản, mã file dùng lại chỉ chấp nhận cùng byte; direct download không thực thi HTML/SVG. Mỗi file tối đa 20 MB, quota hiện tại 1 GB/tài khoản. Khóa được tách theo mục đích bằng HKDF. Đây là mã hóa tại nơi lưu; chưa phải E2EE: máy chủ ứng dụng vẫn có khóa đọc file.

Chạy npm run storage:repair bằng cấu hình cụm sau khi nút bị lỗi hoạt động lại. Công cụ đọc bản khỏe, kiểm checksum và sao chép lại; báo complete/pending. Lập lịch tác vụ này trong hạ tầng triển khai và theo dõi pending. Chưa có garbage collection tự động: ciphertext mồ côi sau upload lỗi hoặc xóa tài khoản phải được đối chiếu/thu gom có kiểm soát, tránh xóa file còn được tham chiếu. Bản sao đồng bộ không thay thế backup chống xóa nhầm.

Không gian công việc hiện được đồng bộ giữa thiết bị của cùng một tài khoản, có revision, hợp nhất ba phía, đối chiếu xung đột tài chính và tombstone cho xóa bằng mật khẩu. Dữ liệu được mã hóa ở database. Chưa phải cộng tác công việc giữa các tài khoản: tên thành viên trong bản lưu và các quyền giao diện cũ chưa trở thành ACL server để chia sẻ task. Nhóm dùng thật hiện đồng bộ danh sách thành viên/metadata; chat riêng dùng API. Không gửi toàn bộ bản lưu sang tài khoản thành viên khi chưa có kiểm quyền từng trường tài chính.

Phản hồi đồng bộ đến trong lúc nhập biểu mẫu sẽ được hoãn, giữ bản nháp tại thiết bị đến khi lưu. Kiểm thử controller đã kiểm tra việc chọn bản tài chính cần giữ, hợp nhất chat/checklist độc lập, bỏ qua phản hồi của tài khoản cũ và tạo ID mới khi người dùng giữ phần chưa gửi của việc đã xóa trên cụm. Vẫn cần nghiệm thu trải nghiệm trên các thiết bị thật.

Nghiệm thu và vận hành

  1. Đăng nhập qua địa chỉ chung, gửi tin nhắn và file, đổi avatar, cập nhật nhóm/quyền quản trị; xác nhận máy khác đọc đúng.
  2. Dừng một app: truy cập vẫn hoạt động qua endpoint khỏe, SSE kết nối lại và tải lịch sử. Dừng một nút file: đọc vẫn được, file mới có hai bản sao. Dừng thêm nút file: ghi file phải bị từ chối, file tại thiết bị được giữ để thử lại.
  3. Diễn tập dừng PostgreSQL primary, đo thời gian chọn chủ mới, kiểm các tin/file đã xác nhận và mã gửi lặp. Chặn kết nối VPN giữa 1 và 2 máy: chỉ phía có quorum và standby hợp lệ được ghi; kiểm watchdog thực sự fencing phía tách mạng.
  4. Đưa máy cũ trở lại, đối chiếu timeline/rejoin, chạy repair file. Diễn tập mất điện đột ngột, đầy ổ đĩa, sai khóa và khôi phục từ backup ngoài cụm. Cần kiểm tra RPO/RTO thực tế trước khi dùng dữ liệu thật.
  5. Backup PostgreSQL base backup + WAL/PITR, snapshot etcd, file và khóa trong các kho riêng, ít nhất một bản ngoài cụm; kiểm thử restore định kỳ. Không dùng pg_dump đơn lẻ thay thế kế hoạch PITR khi cần khôi phục đến thời điểm cụ thể.

Cập nhật đồng bộ và khả năng tải

Mỗi lần phát hành dùng cùng image digest đã ký cho cả ba máy. Triển khai từng app, đợi /health/ready và kiểm lỗi rồi mới cập nhật máy tiếp theo. Giữ image cũ và migration tương thích để quay lui; không tự git pull hoặc chạy mã tải từ một máy PaanChat khác. Bộ này đã có cấu hình image bất biến và readiness; controller CI/CD tự triển khai, dashboard vận hành và ký release còn cần gắn với registry/hạ tầng thực tế.

Adapter PostgreSQL hiện giữ hợp đồng SQL đồng bộ bằng worker để tương thích các tính năng cũ; thread HTTP vẫn chờ kết quả SQL. Chưa đo tải triệu người dùng, chưa phân vùng task, chưa có delta sync; snapshot công việc giới hạn 1.000 việc/200 nhóm/4 MB. Giai đoạn tải lớn phải chuyển sang pool async, outbox cùng transaction, hàng đợi và delta sync theo cursor, giới hạn tải file đồng thời, rồi đo số kết nối/tin nhắn mỗi giây và độ trễ P95. Không suy ra sức tải từ số lượng máy hoặc số tài khoản.

Cấu hình trực tiếp trong Superadmin

Vào tab Cụm máy chủ để xem chế độ máy đang phục vụ, điền domain/subdomain chung, VPN, ba node cấp 1, watchdog, UUID tunnel và digest image. Có thể chọn ba máy lưu file cấp 2 riêng; mặc định kho file cùng cấp 1. Lưu bản dự kiến, bổ sung các trường còn thiếu rồi tải workchat-topology.json và truyền vào bộ tạo ở trên. Khi tách kho file, bộ tạo xuất thêm thư mục file1/file2/file3, các app.env trỏ về IP của ba máy này. Tab có hướng dẫn từng bước và quyền xem/sửa riêng cho nhân viên. Cấu hình được mã hóa và có revision; lưu không đổi runtime, không tự triển khai hoặc kiểm chứng node đã kết nối. Khóa/credential luôn nằm ở kho bí mật riêng.

Thiết kế mở rộng · Về superadmin