SurrealDB · Cobble hướng dẫn vận hành

Cobble là gì

Storage engine LSM cho SurrealDB: một node ghi, nhiều node đọc bám theo, dữ liệu nằm trên object store (S3/MinIO), độ bền do WAL cục bộ bảo đảm. Trang này là hướng dẫn dùng thật — kiến trúc, luồng, triển khai Kubernetes, và những chỗ nó không làm được.

Node ghi cobble://

Duy nhất một. Sở hữu WAL, memtable, và việc chụp snapshot lên object store. Mở cổng stream cho replica bám.

Node đọc cobblero://

Đọc snapshot từ object store, phủ thêm các bản ghi nhận qua stream. Giữ oplog bền để có thể được promote.

Witness cobble-witness

Không mang dữ liệu, ~380 KB. Chỉ đếm phiếu đa số và nhớ epoch. Phải đặt ở miền lỗi thứ ba.

Chạy thử trong 2 phút

Một node ghi đơn, không replica, không object store ngoài — đủ để thấy nó chạy.

surreal start --user root --pass "$PASS" --bind 127.0.0.1:8000 \
  cobble:///var/lib/cobble/data

Thêm một node đọc thì cần object store dùng chung và cổng stream:

# node ghi
export SURREAL_COBBLE_DISTRIBUTED=1
export SURREAL_COBBLE_S3_ENDPOINT=http://minio.internal:9000
export SURREAL_COBBLE_S3_BUCKET=cobble SURREAL_COBBLE_S3_ROOT=surreal
export SURREAL_COBBLE_S3_ACCESS_KEY=... SURREAL_COBBLE_S3_SECRET_KEY=...
export SURREAL_COBBLE_SYNC=stream SURREAL_COBBLE_STREAM_ADDR=0.0.0.0:8100
export SURREAL_COBBLE_STREAM_SECRET="$SHARED_SECRET"
surreal start --bind 0.0.0.0:8000 cobble:///var/lib/cobble/data

# node đọc — cùng bucket, trỏ upstream về cổng stream của node ghi
export SURREAL_COBBLE_UPSTREAM=writer.internal:8100
surreal start --bind 0.0.0.0:8000 cobblero:///var/lib/cobble/cache
Ba điều quyết định mọi thứ khác
  • Chỉ một node ghi. Hai node cùng ghi một bucket là hỏng dữ liệu, không phải chậm. Cơ chế term fencing sinh ra để chặn đúng điều này.
  • Endpoint S3 được nhúng trong manifest. Node được promote phải phục vụ được đúng endpoint cũ ⇒ luôn dùng tên DNS ổn định, đừng bao giờ dùng IP. Trên Kubernetes thì Service DNS giải quyết sẵn.
  • Replica chỉ tham gia được stream sau khi node ghi đã publish snapshot đầu tiên. Đặt SURREAL_COBBLE_SNAPSHOT_SECS để điều đó xảy ra sớm.

Kiến trúc

Bấm vào từng khối để xem nó giữ gì và hỏng thì mất gì.

L1 bản ghi + L2 control ACK (semisync) snapshot đọc snapshot probe term/seq pre-vote / quorum Node ghi cobble:// · WAL + memtable SURREAL_COBBLE_TERM cổng stream :8100 cổng peer :8110 Node đọc cobblero:// · overlay RAM oplog bền (R-A) base/ + con trỏ snapshot cổng peer :8110 Witness không dữ liệu nhớ term, đếm phiếu Object store (S3 / MinIO) snapshot/SNAPSHOT-N · manifest · con trỏ global endpoint bị NHÚNG trong manifest — phải là tên DNS ổn định Client ghi → node ghi đọc + LIVE → node đọc Failover rào node cũ → mirror base → cobble-ra-promote.sh (tăng term) → đổi hướng client
Chọn một khối ở trên.

Mỗi khối nói rõ: giữ gì, mất nó thì mất gì, và ai thay thế.

Ba đường dữ liệu, đừng lẫn

ĐườngĐi đâuBảo đảm
Độ bềnCommit → WAL cục bộ (fsync) → memtableĐây là nguồn sự thật. Snapshot chỉ là bản chụp định kỳ.
L1 — bản ghiNode ghi → node đọc, sau khi đã fsyncBest-effort, có thứ tự, có backfill từ WAL. Mất kết nối thì replica quay về đọc snapshot.
L2 — controlNode đọc → node ghi, trên cùng socketChuyển tiếp đăng ký LIVE/KILL. Có timeout, có khử trùng lặp theo request_id.

Luồng

Bốn luồng đáng hiểu. Bấm từng bước.

Semisync đổi điều gì

Mặc định (SURREAL_COBBLE_ACK=local) client được báo OK ngay khi WAL cục bộ đã fsync — mất nguyên máy là mất phần chưa kịp stream. Bật semi thì client chỉ được báo OK sau khi một replica đã fsync bản ghi vào oplog của nó.

localsemi
RPO khi mất node ghi= độ trễ stream0 cho ghi đã được báo OK
Giá mỗi commit+1 vòng mạng (đo được +0,2–0,45 ms trên loopback)
Không có replicavẫn ghitừ chối ghi sau ACK_TIMEOUT_MS
Ba điều phải biết trước khi bật semi
  • Cửa sổ khởi động. Những lệnh ghi đầu tiên là phần tự khai báo node của chính SurrealDB, xảy ra trước khi replica kịp nối. Trong ACK_ARM_MS (mặc định 30 giây, và chỉ cho lần boot đầu) ack vẫn dựa trên độ bền cục bộ. Khi node đã từng có replica bám luồng, file SURREAL_COBBLE_REPLICATED xuất hiện và mặc định thành 0 — restart lúc replica đang chết thì node từ chối phục vụ chứ không âm thầm hạ cấp.
  • Hết giờ ≠ mất dữ liệu. Bản ghi đã bền trong WAL; cái không giữ được là lời hứa "sống sót khi mất máy này".
  • Ghi hiện ra trước khi client biết kết quả. Bản ghi vào memtable và bắn LIVE tại thời điểm commit, nên phiên khác có thể đọc thấy dòng mà chủ nhân của nó vừa nhận lỗi.

Triển khai trên Kubernetes

Kubernetes hợp với cobble hơn máy trần ở đúng một điểm quan trọng: Service DNS là tên ổn định, nên chuyện endpoint S3 bị nhúng trong manifest không còn là cái bẫy khi failover.

Bốn ràng buộc phải tôn trọng
  • Node ghi đúng 1 pod. StatefulSet replicas: 1, không HPA, không hai bản cùng lúc lúc rolling — dùng podManagementPolicy: OrderedReady và cập nhật kiểu OnDelete nếu muốn kiểm soát tay.
  • Mỗi vai trò cần PVC riêng: node ghi giữ WAL + con trỏ; node đọc giữ oplog + base.
  • Witness phải ở miền lỗi khác — khác zone, tối thiểu khác node. Đặt cùng chỗ với node đọc là có hại: nó tự cấp đa số cho chính mình.
  • Endpoint S3 dùng Service DNS (http://minio.cobble.svc.cluster.local:9000), không dùng IP pod.
apiVersion: v1
kind: Secret
metadata: { name: cobble-shared, namespace: cobble }
type: Opaque
stringData:
  # Bí mật dùng chung cho stream + peer. Phải giống nhau ở MỌI node và witness.
  STREAM_SECRET: "thay-bang-32-byte-ngau-nhien"
  S3_ACCESS_KEY: "..."
  S3_SECRET_KEY: "..."
  ROOT_PASS: "..."
---
# Tên cụm: buộc vào mọi MAC. Hai cụm dùng chung secret mà khác CLUSTER_ID
# thì KHÔNG xác thực được với nhau — đó là mục đích.
apiVersion: v1
kind: ConfigMap
metadata: { name: cobble-env, namespace: cobble }
data:
  SURREAL_COBBLE_CLUSTER_ID: "prod"
  SURREAL_COBBLE_DISTRIBUTED: "1"
  SURREAL_COBBLE_S3_ENDPOINT: "http://minio.cobble.svc.cluster.local:9000"
  SURREAL_COBBLE_S3_BUCKET: "cobble"
  SURREAL_COBBLE_S3_ROOT: "surreal"
  SURREAL_COBBLE_SYNC: "stream"
  SURREAL_COBBLE_SNAPSHOT_EVERY: "256"
  SURREAL_COBBLE_SNAPSHOT_SECS: "30"
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: cobble-writer, namespace: cobble }
spec:
  serviceName: cobble-writer
  replicas: 1                      # BẤT BIẾN. Không bao giờ 2.
  podManagementPolicy: OrderedReady
  updateStrategy: { type: RollingUpdate }
  selector: { matchLabels: { app: cobble, role: writer } }
  template:
    metadata: { labels: { app: cobble, role: writer } }
    spec:
      terminationGracePeriodSeconds: 60     # để WAL kịp flush
      containers:
      - name: surreal
        image: your-registry/surreal-cobble:3.3.0
        args: ["start","--bind","0.0.0.0:8000","cobble:///data"]
        ports:
        - { name: http,   containerPort: 8000 }
        - { name: stream, containerPort: 8100 }
        - { name: peer,   containerPort: 8110 }
        envFrom:
        - configMapRef: { name: cobble-env }
        env:
        - { name: SURREAL_COBBLE_STREAM_ADDR, value: "0.0.0.0:8100" }
        - { name: SURREAL_COBBLE_PEER_ADDR,   value: "0.0.0.0:8110" }
        # Chính nó KHÔNG nằm trong danh sách. Cluster size = peers + 1.
        - { name: SURREAL_COBBLE_PEERS,
            value: "cobble-replica-0.cobble-replica.cobble.svc.cluster.local:8110,cobble-witness.cobble.svc.cluster.local:8110" }
        - { name: SURREAL_COBBLE_WAL,          value: "every" }
        # Trần đĩa nóng: không có nó, PVC phải to bằng cả dataset.
        # Offload SST nguội xuống S3 ở mức dùng 0.85, chặn ghi ở 0.95. Sàn 4 GiB.
        - { name: SURREAL_COBBLE_LOCAL_CAP_GB,   value: "20" }
        - { name: SURREAL_COBBLE_WAL_MAX_MB,     value: "1024" }   # cứng = 2x
        - { name: SURREAL_COBBLE_WAL_KEEP_MB,    value: "256" }    # đuôi cho replica backfill
        - { name: SURREAL_COBBLE_BLOCK_CACHE_MB, value: "512" }
        - { name: SURREAL_COBBLE_READ_CACHE_MB,  value: "256" }
        - { name: SURREAL_COBBLE_ACK,             value: "semi" }
        - { name: SURREAL_COBBLE_ACK_TIMEOUT_MS,  value: "5000" }
        - { name: SURREAL_USER, value: root }
        - { name: SURREAL_PASS, valueFrom: { secretKeyRef: { name: cobble-shared, key: ROOT_PASS } } }
        - { name: SURREAL_COBBLE_STREAM_SECRET,  valueFrom: { secretKeyRef: { name: cobble-shared, key: STREAM_SECRET } } }
        - { name: SURREAL_COBBLE_S3_ACCESS_KEY,  valueFrom: { secretKeyRef: { name: cobble-shared, key: S3_ACCESS_KEY } } }
        - { name: SURREAL_COBBLE_S3_SECRET_KEY,  valueFrom: { secretKeyRef: { name: cobble-shared, key: S3_SECRET_KEY } } }
        volumeMounts: [{ name: data, mountPath: /data }]
        readinessProbe: { httpGet: { path: /health, port: http }, periodSeconds: 5 }
        # KHÔNG dùng livenessProbe hung hăng: node bị fence cố ý từ chối phục vụ,
        # khởi động lại nó chỉ tạo vòng lặp restart, không sửa được gì.
        resources: { requests: { cpu: "2", memory: 4Gi } }
  volumeClaimTemplates:
  - metadata: { name: data }
    spec:
      accessModes: [ReadWriteOnce]
      resources: { requests: { storage: 40Gi } }   # >= LOCAL_CAP_GB + 2x WAL_MAX_MB + dư
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: cobble-replica, namespace: cobble }
spec:
  serviceName: cobble-replica
  replicas: 2                      # muốn bao nhiêu cũng được
  selector: { matchLabels: { app: cobble, role: replica } }
  template:
    metadata: { labels: { app: cobble, role: replica } }
    spec:
      topologySpreadConstraints:   # đừng dồn hết vào một zone
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector: { matchLabels: { app: cobble, role: replica } }
      containers:
      - name: surreal
        image: your-registry/surreal-cobble:3.3.0
        args: ["start","--bind","0.0.0.0:8000","cobblero:///data"]
        ports:
        - { name: http, containerPort: 8000 }
        - { name: peer, containerPort: 8110 }
        envFrom: [{ configMapRef: { name: cobble-env } }]
        env:
        - { name: SURREAL_COBBLE_UPSTREAM,
            value: "cobble-writer-0.cobble-writer.cobble.svc.cluster.local:8100" }
        - { name: SURREAL_COBBLE_PEER_ADDR, value: "0.0.0.0:8110" }
        - { name: SURREAL_COBBLE_PEERS,
            value: "cobble-writer-0.cobble-writer.cobble.svc.cluster.local:8110,cobble-witness.cobble.svc.cluster.local:8110" }
        - { name: SURREAL_COBBLE_ACK,              value: "semi" }   # phải khớp node ghi
        - { name: SURREAL_COBBLE_OPLOG_KEEP_MB,    value: "4096" }
        - { name: SURREAL_COBBLE_OVERLAY_CAP_MB,   value: "512" }
        - { name: SURREAL_COBBLE_BLOCK_CACHE_MB,   value: "256" }
        - { name: SURREAL_COBBLE_READ_CACHE_MB,    value: "128" }
        - { name: SURREAL_COBBLE_REFRESH_MS,       value: "1000" }
        - { name: SURREAL_LIVE_QUERY_ENGINE,       value: "router" } # cần cho LIVE trên replica
        - { name: SURREAL_USER, value: root }
        - { name: SURREAL_PASS, valueFrom: { secretKeyRef: { name: cobble-shared, key: ROOT_PASS } } }
        - { name: SURREAL_COBBLE_STREAM_SECRET, valueFrom: { secretKeyRef: { name: cobble-shared, key: STREAM_SECRET } } }
        - { name: SURREAL_COBBLE_S3_ACCESS_KEY, valueFrom: { secretKeyRef: { name: cobble-shared, key: S3_ACCESS_KEY } } }
        - { name: SURREAL_COBBLE_S3_SECRET_KEY, valueFrom: { secretKeyRef: { name: cobble-shared, key: S3_SECRET_KEY } } }
        volumeMounts: [{ name: data, mountPath: /data }]
        readinessProbe: { httpGet: { path: /health, port: http }, periodSeconds: 5 }
  volumeClaimTemplates:
  - metadata: { name: data }
    spec:
      accessModes: [ReadWriteOnce]
      resources: { requests: { storage: 60Gi } }   # oplog + base mirror
apiVersion: apps/v1
kind: Deployment
metadata: { name: cobble-witness, namespace: cobble }
spec:
  replicas: 1
  selector: { matchLabels: { app: cobble, role: witness } }
  template:
    metadata: { labels: { app: cobble, role: witness } }
    spec:
      affinity:
        podAntiAffinity:            # KHÔNG ở chung node với writer/replica
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector: { matchExpressions: [{ key: role, operator: In, values: [writer, replica] }] }
            topologyKey: kubernetes.io/hostname
      containers:
      - name: witness
        image: your-registry/cobble-witness:3.3.0   # ~380 KB, không phụ thuộc
        args: ["/state"]
        ports: [{ name: peer, containerPort: 8110 }]
        env:
        - { name: SURREAL_COBBLE_PEER_ADDR, value: "0.0.0.0:8110" }
        - { name: SURREAL_COBBLE_PEERS,
            value: "cobble-writer-0.cobble-writer.cobble.svc.cluster.local:8110,cobble-replica-0.cobble-replica.cobble.svc.cluster.local:8110" }
        - { name: SURREAL_COBBLE_CLUSTER_ID, value: "prod" }
        - { name: SURREAL_COBBLE_STREAM_SECRET, valueFrom: { secretKeyRef: { name: cobble-shared, key: STREAM_SECRET } } }
        volumeMounts: [{ name: state, mountPath: /state }]
        resources: { requests: { cpu: 10m, memory: 32Mi } }
      volumes:
      - name: state
        persistentVolumeClaim: { claimName: cobble-witness-state }
---
# Term phải sống sót qua restart: witness quên epoch là witness cho phép
# node ghi cũ quay lại.
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: cobble-witness-state, namespace: cobble }
spec:
  accessModes: [ReadWriteOnce]
  resources: { requests: { storage: 64Mi } }
# Headless: cần tên pod ổn định cho danh sách peer và cho upstream.
apiVersion: v1
kind: Service
metadata: { name: cobble-writer, namespace: cobble }
spec:
  clusterIP: None
  selector: { app: cobble, role: writer }
  ports:
  - { name: http, port: 8000 }
  - { name: stream, port: 8100 }
  - { name: peer, port: 8110 }
---
apiVersion: v1
kind: Service
metadata: { name: cobble-replica, namespace: cobble }
spec:
  clusterIP: None
  selector: { app: cobble, role: replica }
  ports: [{ name: http, port: 8000 }, { name: peer, port: 8110 }]
---
# Endpoint client dùng để ĐỌC: cân tải qua các replica.
apiVersion: v1
kind: Service
metadata: { name: cobble-read, namespace: cobble }
spec:
  selector: { app: cobble, role: replica }
  ports: [{ name: http, port: 8000 }]
---
# Endpoint client dùng để GHI. Khi failover, chỉ cần đổi selector sang node
# được promote — client không phải cấu hình lại.
apiVersion: v1
kind: Service
metadata: { name: cobble-write, namespace: cobble }
spec:
  selector: { app: cobble, role: writer }
  ports: [{ name: http, port: 8000 }]
---
apiVersion: v1
kind: Service
metadata: { name: cobble-witness, namespace: cobble }
spec:
  selector: { app: cobble, role: witness }
  ports: [{ name: peer, port: 8110 }]
---
# Chỉ cho phép ai được nói chuyện với cổng stream/peer. Giao thức có xác thực
# challenge-response, nhưng đừng phơi cổng ra ngoài cụm.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: cobble-internal, namespace: cobble }
spec:
  podSelector: { matchLabels: { app: cobble } }
  policyTypes: [Ingress]
  ingress:
  - from: [{ podSelector: { matchLabels: { app: cobble } } }]
    ports: [{ port: 8100 }, { port: 8110 }]
  - from: [{ namespaceSelector: {} }]
    ports: [{ port: 8000 }]
# CHẠY TAY, sau khi đã rào node ghi cũ. Không đặt vào CI, không tự động hoá.
#
#   1) kubectl -n cobble scale statefulset/cobble-writer --replicas=0
#   2) đợi pod biến mất thật:  kubectl -n cobble get pod -l role=writer -w
#   3) chạy Job này (nó chạy trên PVC của replica được chọn)
apiVersion: batch/v1
kind: Job
metadata: { name: cobble-promote, namespace: cobble }
spec:
  backoffLimit: 0            # thất bại thì DỪNG, đừng thử lại mù
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: promote
        image: your-registry/surreal-cobble:3.3.0
        command: ["/scripts/cobble-ra-promote.sh", "/data", "/data/base", "--", "true"]
        env:
        - { name: SURREAL_COBBLE_FENCE_CONFIRMED, value: "1" }
        # Cụm 3 thành viên (writer + replica + witness) thì KHÔNG cần cờ dưới đây:
        # script tự thấy đa số 2/3 qua file SURREAL_COBBLE_QUORUM.
        # Cụm 2 thành viên thì luôn cần — và đó là chữ ký của con người.
        # - { name: SURREAL_COBBLE_PROMOTE_WITHOUT_QUORUM, value: "1" }
        volumeMounts: [{ name: data, mountPath: /data }]
      volumes:
      - name: data
        persistentVolumeClaim: { claimName: data-cobble-replica-0 }
---
#   4) đổi Service cobble-write sang node vừa promote (hoặc gắn nhãn lại pod)
#   5) node ghi cũ: GIỮ replicas=0 cho tới khi đã cắt đuôi WAL và cho nó
#      gia nhập lại làm replica. Nếu nó bật dậy chui, pre-vote sẽ tự poison nó.

Image

Chưa có image chính thức — build từ nguồn:

cargo build --release --no-default-features \
  --features storage-cobble,http,scripting

cargo build --release -p cobble-witness   # binary ~380 KB, không phụ thuộc
glibc Binary link động vào glibc của máy build. Host cũ (Ubuntu 20.04, glibc 2.31) sẽ báo GLIBC_2.32 not found. Trong container thì dùng base image cùng đời hoặc mới hơn; trên máy trần thì build bằng toolchain cũ hơn, hoặc dựng bản musl tĩnh.

Cấu hình

Chọn ở đây, dán ra dùng.

Kết quả

Bảng biến môi trường

Chung cho mọi vai trò

BiếnMặc địnhÝ nghĩa
SURREAL_COBBLE_STREAM_SECRETBắt buộc khi có nhiều node. Phải khớp ở mọi node và witness. Không bao giờ được gửi lên dây — hai bên chứng minh bằng challenge-response.
SURREAL_COBBLE_CLUSTER_IDrỗngTrộn vào mọi MAC. Hai cụm dùng chung secret mà khác id thì không xác thực được với nhau.
SURREAL_COBBLE_PEER_ADDRCổng trả lời thăm dò term/seq.
SURREAL_COBBLE_PEERSrỗngCác thành viên khác, ngăn bằng dấu phẩy. Không gồm chính mình. Cluster size = số peer + 1.
SURREAL_COBBLE_PEER_INTERVAL_MS / _TIMEOUT_MS2000 / 1500Nhịp thăm dò và timeout mỗi lần.
SURREAL_COBBLE_S3_*ENDPOINT, BUCKET, ROOT, ACCESS_KEY, SECRET_KEY.

Node ghi

BiếnMặc địnhÝ nghĩa
SURREAL_COBBLE_DISTRIBUTED0Đưa manifest/schema/con trỏ global lên S3. Bắt buộc nếu muốn có node đọc.
SURREAL_COBBLE_SYNCstream để bật replication.
SURREAL_COBBLE_STREAM_ADDRCổng cho node đọc bám vào.
SURREAL_COBBLE_STREAM_BUF_MB64Vùng đệm mỗi subscriber. Đầy thì cắt subscriber chậm — fail-safe có chủ ý.
SURREAL_COBBLE_WALeveryevery (fsync mỗi commit, gộp nhóm) · interval · never.
SURREAL_COBBLE_SNAPSHOT_EVERY / _SECSChu kỳ chụp snapshot. Phải đặt _SECS nếu không replica mới không bao giờ vào được stream.
SURREAL_COBBLE_ACKlocallocal hoặc semi. Giá trị lạ là lỗi khởi động, không im lặng lùi về local.
SURREAL_COBBLE_ACK_TIMEOUT_MS5000Chờ replica xác nhận. Tính từ lúc commit, không phải lúc tới lượt.
SURREAL_COBBLE_ACK_ARM_MS30000 (chỉ boot đầu)Cửa sổ ack cục bộ lúc khởi động. Sau khi đã từng có replica → mặc định 0.
SURREAL_COBBLE_LOCAL_CAP_GBkhông đặt = không giới hạnTrần tầng nóng cục bộ — xem mục Dung lượng bên dưới. Không đặt thì đĩa local phình theo kích thước dữ liệu.

Node đọc

BiếnMặc địnhÝ nghĩa
SURREAL_COBBLE_UPSTREAMhost:port cổng stream của node ghi.
SURREAL_COBBLE_REFRESH_MS1000Nhịp poll con trỏ snapshot mới.
SURREAL_COBBLE_OVERLAY_CAP_MB512Trần overlay RAM; vượt thì quay về đọc snapshot.
SURREAL_COBBLE_OPLOG_KEEP_MB1024Trần oplog bền. Đầy thì fail-closed — replica ngừng nhận. Cần cảnh báo trước khi chạm.
SURREAL_COBBLE_CONTROL_TIMEOUT_MS5000Chờ node ghi xác nhận lệnh LIVE/KILL.
SURREAL_LIVE_QUERY_ENGINEinlineĐặt router để LIVE hoạt động trên node đọc.

Dung lượng & bộ nhớ — nhóm hay bị bỏ sót

Không đặt SURREAL_COBBLE_LOCAL_CAP_GB thì đĩa local phình theo dữ liệu. Đây là biến duy nhất biến đĩa cục bộ thành cache nóng có trần thay vì bản sao đầy đủ của toàn bộ dataset. Nó đặt size_limit cho volume local; tiến trình offload nền của cobble sẽ đẩy các SST nguội xuống S3 khi mức dùng vượt 0,85, và chặn ghi0,95.
BiếnMặc địnhÝ nghĩa và cái bẫy
SURREAL_COBBLE_LOCAL_CAP_GBkhông đặtTrần đĩa nóng cục bộ (GiB). Offload ở 0,85 · write-stop ở 0,95. Chỉ có tác dụng khi đã cấu hình S3 — offload cần một tầng dưới để đẩy file xuống. Giá trị bị kẹp sàn ở 4 GiB vì hybrid block cache đã giữ trước 2 GiB trên chính volume đó.
SURREAL_COBBLE_BLOCK_CACHE_MB64Ngân sách block cache (foyer). Áp dụng cho cả node ghi lẫn node đọc. Khác với cache đọc điểm của SurrealDB.
SURREAL_COBBLE_READ_CACHE_MB128 (sàn 1)Cache đọc điểm dùng chung. Khoá vắng mặt cũng được cache.
SURREAL_COBBLE_WAL_MAX_MB1024Ngưỡng mềm của WAL: chạm thì kích snapshot. Ngưỡng cứng = gấp đôi giá trị này, chạm thì dừng ghi.
SURREAL_COBBLE_WAL_KEEP_MB256Giữ lại ngần này WAL đã niêm phong để replica backfill. Chỉ có tác dụng khi bật stream. Quá nhỏ ⇒ replica tụt lại sẽ bị đẩy về đường snapshot.
SURREAL_COBBLE_WAL_GROUP_MAX64 (sàn 1)Số commit tối đa gộp sau một fsync.
SURREAL_COBBLE_SNAPSHOT_RETENTION2Số đời snapshot giữ lại. Mặc định của chính cobble là vô hạn — sẽ rò rỉ metadata/dữ liệu mãi mãi; SurrealDB ghi đè xuống 2 vì đường khôi phục con trỏ cần đúng 2 đời.
SURREAL_COBBLE_QUEUE_CAPACITY1024Trần hàng đợi thao tác. Chặn việc một lượt đọc object store bị treo biến tồn đọng kênh thành phình RAM.
SURREAL_COBBLE_OPLOG_KEEP_MB1024(node đọc) Trần oplog bền. Đầy là fail-closed.
SURREAL_COBBLE_OVERLAY_CAP_MB512(node đọc) Trần overlay RAM; vượt thì quay về đọc snapshot.
SURREAL_COBBLE_STREAM_BUF_MB64(node ghi) Vùng đệm mỗi subscriber; đầy thì cắt subscriber chậm.

Ước lượng PVC: node ghi cần LOCAL_CAP_GB + WAL (tới 2×WAL_MAX_MB) + dư. Node đọc cần base mirror + OPLOG_KEEP_MB + dư. Không đặt LOCAL_CAP_GB thì phải cấp PVC bằng cả dataset.

Còn lại

BiếnMặc địnhÝ nghĩa
SURREAL_COBBLE_S3_REGIONus-east-1Region. MinIO không quan tâm, S3 thật thì có.
SURREAL_COBBLE_PEER_TIMEOUT_MS1500Timeout mỗi lần thăm dò peer.
SURREAL_COBBLE_EXIT_ON_PANICbậtWorker panic ⇒ tiến trình thoát mã 70 để trình giám sát khởi động lại. Đặt 0/false để giữ tiến trình sống mà khám nghiệm — nó sẽ từ chối mọi thao tác.

Biến cho script promote

BiếnÝ nghĩa
SURREAL_COBBLE_FENCE_CONFIRMED=1Bắt buộc. Xác nhận đã rào node ghi cũ. Script không chạy nếu thiếu.
SURREAL_COBBLE_PROMOTE_WITHOUT_QUORUM=1Bỏ qua luật đa số — chữ ký của con người. Cụm 2 thành viên thì luôn cần.
SURREAL_COBBLE_QUORUM_MAX_AGE_STuổi tối đa của file chứng nhận còn được tin (mặc định 30 giây).
SURREAL_COBBLE_BASE_MC_URLĐặt thì script kiểm con trỏ base có tồn tại thật trong mirror không trước khi làm gì. Nên đặt — thiếu nó thì lỗi lộ ra dưới dạng 404 khó hiểu giữa lúc sự cố.

File trong thư mục dữ liệu

FileAi ghiÝ nghĩa
SURREAL_COBBLE_TERMpromote script, engineEpoch. Tăng đơn điệu, bền. Trái tim của fencing.
SURREAL_COBBLE_QUORUMluồng peerepoch_ms reachable configured peer_term local_term local_seq. Promote script đọc file này.
SURREAL_COBBLE_REPLICATEDnode ghiĐã từng có replica bám luồng ⇒ semisync không tự cấp lại cửa sổ khởi động.
SURREAL_COBBLE_SNAPSHOTnode ghi / mirror scriptCon trỏ snapshot bền, kèm .prev.
SURREAL_COBBLE_LOCKengineKhoá cố vấn: chặn hai tiến trình mở cùng thư mục trên cùng một máy. Không chặn được hai máy — đó là việc của term.
SURREAL_COBBLE_INITIALIZEDengineĐánh dấu thư mục đã khởi tạo, để phân biệt "mới tinh" với "con trỏ hỏng".

Vận hành

Failover — thứ tự không được đổi

  1. Xác nhận chết bằng ≥2 kênh độc lập. "Client kêu" không phải một kênh. Node ghi vẫn có thể sống và đang phục vụ trong khi bạn không thấy nó.
  2. Rào (F1). kubectl scale sts/cobble-writer --replicas=0, hoặc systemctl disable --now trên máy trần. Đợi cho tới khi nó thật sự biến mất.
  3. Chốt base. cobble-ra-mirror-base.sh SRC DST BASE_DIR — dạng có BASE_DIR để con trỏ được suy ra từ những gì mirror thật sự có. Bỏ bước này thì node promote chết lúc boot với SNAPSHOT-N: NotFound.
  4. Chuyển endpoint object store nếu node được promote dùng store riêng (trên K8s thì Service DNS lo sẵn).
  5. Promote. SURREAL_COBBLE_FENCE_CONFIRMED=1 cobble-ra-promote.sh REPLICA_DIR BASE_DIR -- surreal start .... Script sẽ kiểm đa số, kiểm tuổi file chứng nhận, từ chối nếu peer đã giữ term mới hơn, rồi tăng term.
  6. Kiểm chứng số bản ghi so mốc trước, ghi thử một dòng, xem term mới.
  7. Đổi hướng client ghi.
  8. Ghi biên bản: thời điểm, seq mốc, term mới, kênh xác nhận chết.
  9. Node cũ sống lại thì gia nhập làm node đọc, không bao giờ tự làm lại node ghi. Nếu nó bật dậy chui, pre-vote sẽ tự poison nó.

Cụm 2 thành viên: đọc kỹ

Hai node không bao giờ đạt đa số. Cần 2/2 mà một con vừa chết. Nên script luôn từ chối — trừ khi có người ký SURREAL_COBBLE_PROMOTE_WITHOUT_QUORUM=1. Đây là toán học, không phải thiếu tính năng. Muốn tự quyết thì thêm thành viên thứ ba: một witness ~380 KB là đủ.

Cần theo dõi gì

Chỉ sốNguồnNgưỡng cảnh báo
Độ trễ stream (seq)log stream_lag_seqTăng đều không tụt ⇒ replica không theo kịp
unreplicated_seqlog node ghi> 0 kéo dài ⇒ RPO thật đang lớn dần
Oplog sắp chạm capdung lượng thư mục oplogPhải cảnh báo trước khi đầy — đầy là fail-closed
Subscriber = 0subscriber_countVới semi: mọi ghi sắp fail
Tuổi file quorumSURREAL_COBBLE_QUORUM> 30 giây ⇒ promote sẽ bị từ chối
Đĩa của node đọcPVCCần chứa base mirror + oplog

Đọc log

Dòng logNghĩa là gì
lost an election and is now poisonedNode ghi này đã bị thay thế. Không restart — khôi phục từ node thắng.
pre-vote lost: a peer holds fencing term NNode cũ vừa cố bật dậy và bị chặn ngay lúc khởi động.
cobble semisync: commit N is durable locally but no replica…Ghi đã bền tại chỗ nhưng chưa nhân bản. Client nhận lỗi. Không mất dữ liệu.
disconnected slow subscriber (buffer full)Replica không theo kịp và bị cắt. Nó sẽ quay lại qua đường snapshot.
refusing promotion: … majority needs NỨng viên đang là thiểu số. Đây là script làm đúng việc.

Mô hình bảo mật

Một bí mật dùng chung, không bao giờ nằm trên dây.

Xác thực

Cả cổng peer lẫn cổng stream đều dùng challenge-response hai chiều trên HMAC-SHA256:

A → B   nonce_A
B → A   nonce_B
A → B   HMAC(secret, domain ‖ cluster_id ‖ nonce_A ‖ nonce_B ‖ "client" ‖ dữ_liệu_khẳng_định)
B → A   dữ_liệu ‖ HMAC(secret, … ‖ "server" ‖ dữ_liệu)

Chống ép cạn tài nguyên

Node không trả lời được sẽ bị các peer đọc là unreachable — mà unreachable chính là trạng thái cho phép một node ghi cũ vượt qua pre-vote. Nên im lặng không phải lỗi trung tính; đó là mục tiêu của kẻ tấn công. Vì thế:

Biện phápGiá trị
Một luồng cho mỗi requestVòng accept không bao giờ làm việc thay cho caller chưa xác thực
Trần luồng đồng thời32, dư thì drop ngay; cảnh báo giới hạn theo luỹ thừa 2
Trần khung tin trước xác thực64 KiB
Hạn chót theo đồng hồ2 giây cho cả request — timeout theo từng syscall là không đủ, kẻ nhỏ giọt 1 byte sẽ giữ luồng vô hạn

Chưa làm — nói thẳng

Kẻ tấn công nằm trên đường truyền vẫn có thể chèn hoặc sửa frame sau khi bắt tay đã qua, và bản ghi không được mã hoá.

MAC từng frame sẽ bịt vế đầu; vế sau là việc của tầng mạng. Lưu ý: đường object store cũng là cleartext, nên mã hoá riêng luồng replication là an ninh trình diễn. Cách đúng cho cụm chạy qua mạng không tin cậy: mTLS / service mesh trong Kubernetes, hoặc WireGuard giữa các máy trần — cả hai bọc luôn cả đường object store.

Ngoài ra: semisync tin biên nhận của replica. Sau khi có xác thực thì chỉ node hợp lệ mới ACK được, nhưng một replica lỗi vẫn có thể báo sai. Đó là giới hạn của mô hình, không phải lỗi lập trình.

Giới hạn đã biết

Danh sách này quan trọng hơn phần tính năng. Đọc trước khi đưa vào production.

Giới hạnHệ quả thậtAi gánh
Một node ghiKhông mở rộng được đường ghi; mất node ghi là mất khả năng ghi cho tới khi promote xongFailover là thao tác có con người; RTO tính bằng phút
Endpoint S3 nhúng trong manifestNode promote phải phục vụ được đúng endpoint cũDùng DNS ổn định. K8s Service DNS xử lý sẵn; máy trần cần floating IP / DNS TTL ngắn
Chia cắt mạngKhông cấu hình nào cho phép promote tự động an toàn khi chỉ có 2 thành viênThêm thành viên thứ 3, hoặc chấp nhận mất khả năng ghi tạm thời
Node bị cô lập vẫn phục vụ đọcClient còn tới được nó sẽ đọc dữ liệu cũ dần, vô hạn địnhChưa có cơ chế tự tắt đọc khi mất quorum
Oplog fail-closedOplog đầy thì replica ngừng nhận thay vì xoá lịch sửĐặt cap đủ lớn + cảnh báo trước khi chạm
Replica cần snapshot đầu tiênCụm mới toanh: replica không vào được stream cho tới khi có snapshotĐặt SNAPSHOT_SECS
LIVE đăng ký sau snapshot cuốiGiữ được nếu đuôi oplog còn; ngoài ra thì mất khi failoverClient LIVE phải tự đăng ký lại sau failover — SDK làm sẵn
Chèn frame bởi kẻ on-pathBắt tay đã xác thực, nhưng frame sau đó chưa được ký từng cáimTLS / mesh / WireGuard

Trước khi gọi là "có HA"

Phải diễn tập thật, giết máy thật, bấm đồng hồ thật: