🇬🇧 Read the English version of this article on tanhdev.com

Answer-first: Triển khai GraphHopper trên Kubernetes chuẩn production yêu cầu cấu hình StatefulSet với SSD PVC lưu trữ OSM PBF và CH cache, cấp phát 768MB JVM Heap cùng 4GB off-heap Direct Memory (mmap), thiết lập Readiness Probe 600s bảo vệ giai đoạn CH pre-processing, và áp dụng chiến lược Blue-Green để cập nhật bản đồ không downtime.

GraphHopper là một cỗ máy định tuyến (routing engine) mã nguồn mở mạnh mẽ — hỗ trợ thuật toán Rút Ngọn Thứ Bậc (Contraction Hierarchies - CH) cho các câu truy vấn phản hồi dưới 1 mili-giây, hồ sơ phương tiện tùy chỉnh (custom vehicle profiles), quy định cấm rẽ (turn restrictions) và tích hợp dữ liệu bản đồ OpenStreetMap. Thách thức lớn nhất đối với các đội ngũ kỹ thuật không phải là thuật toán, mà là việc vận hành và triển khai trên Kubernetes: nạp tập tin OSM PBF dung lượng lớn, tối ưu hóa bộ nhớ JVM, xử lý giai đoạn tiền xử lý đồ thị CH (CH pre-processing), và cập nhật dữ liệu bản đồ mà không gây gián đoạn hệ thống (zero-downtime).

Bài viết này hướng dẫn chi tiết cách triển khai GraphHopper chuẩn production trên Kubernetes với dữ liệu OpenStreetMap. Bạn sẽ nắm được cách thiết lập StatefulSet/Deployment với lưu trữ bền vững (persistent OSM graph files), cấu hình tài nguyên JVM chuẩn xác, thiết lập liveness và readiness probes cho giai đoạn CH pre-processing, và chiến lược cập nhật bản đồ 0% downtime.

Để so sánh các thuật toán định tuyến và tích hợp API, bạn có thể xem bài viết GraphHopper và CARTO: Bộ Máy Điều Phối Lộ Trình Giao Hàng cùng chuyên mục trong series Routing & Geospatial Architecture.


Tại Sao Nên Tự Triển Khai GraphHopper? So Sánh Chi Phí Với Cloud API

Trước khi quyết định tự triển khai hệ thống (self-hosted deployment), hãy so sánh các phương án:

Hệ Đo LườngTự Triển Khai GraphHopperGraphHopper Cloud APIGoogle Maps Routes API
Chi phí mỗi truy vấnChi phí hạ tầng cố định (Infrastructure)~$0.005–0.008/request$0.005–0.010/request
Tại mốc 1 Triệu queries/ngày~$200–400/tháng (K8s)~$5,000–8,000/tháng~$5,000–10,000/tháng

Trích Xuất Dữ Liệu OSM PBF: Phạm Vi Quốc Gia, Khu Vực Hoặc Bounding Box

Dữ liệu OpenStreetMap được phân phối miễn phí theo định dạng PBF (Protocol Buffer Format). Bạn có thể tải các bản trích xuất bản đồ từ Geofabrik (máy chủ mirror chính).

Bảng Dung Lượng Dữ Liệu Và Yêu Cầu Tài Nguyên

Khu VựcKích Thước File PBFBộ Nhớ RAM Cho CH Graph (Car profile)
Việt Nam~170 MB~2–3 GB
Đông Nam Á (Southeast Asia)~1.2 GB~15–20 GB
Nhật Bản (Japan)~1.0 GB~12–18 GB
Đức (Germany)~3.5 GB~40–60 GB
Toàn Châu Âu (Europe)~28 GB~300+ GB

Đối với các ứng dụng logistics hoạt động trong phạm vi một quốc gia, bạn nên tải tệp dữ liệu trích xuất theo quốc gia (country-level extract). Nếu ứng dụng hoạt động trên phạm vi nhiều quốc gia, hãy chọn bản trích xuất theo khu vực (regional extract). Tránh sử dụng tệp dữ liệu toàn cầu (global planet file) (dung lượng nén >90 GB), trừ khi hệ thống máy chủ của bạn được trang bị tối thiểu 512 GB RAM trở lên để phục vụ quá trình tiền xử lý đồ thị (graph pre-processing).

Lệnh Tải Dữ Liệu OSM Data

# Tải dữ liệu bản đồ Việt Nam
wget https://download.geofabrik.de/asia/vietnam-latest.osm.pbf

# Tải dữ liệu khu vực Đông Nam Á (South-East Asia - bao gồm Việt Nam, Thái Lan, Indonesia, Philippines...)
wget https://download.geofabrik.de/asia/south-east-asia-latest.osm.pbf

# Kiểm tra tính toàn vẹn của tệp đã tải (verify integrity)
wget https://download.geofabrik.de/asia/vietnam-latest.osm.pbf.md5
md5sum -c vietnam-latest.osm.pbf.md5

Lưu tệp PBF vào một PersistentVolumeClaim (PVC) trong Kubernetes — tuyệt đối không đóng gói tệp PBF trực tiếp vào trong container image. Tệp OSM có dung lượng rất lớn sẽ làm tăng kích thước của container image không cần thiết và dữ liệu bản đồ cũng cần lưu trữ bền vững qua các lần restart Pod.


Docker Image Của GraphHopper (The GraphHopper Docker Image): Đóng Image Tự Chế Hay Dùng Official Image

GraphHopper cung cấp sẵn Docker image chính thức trên GitHub Container Registry:

docker pull ghcr.io/graphhopper/graphhopper:latest

Đối với môi trường production, bạn nên gắn thẻ (tag) phiên bản cụ thể thay vì dùng latest:

docker pull ghcr.io/graphhopper/graphhopper:10.0

Tùy Chỉnh Cấu Hình Qua ConfigMap

Tạo tập tin cấu hình graphhopper.yml tối giản cho môi trường production:

# graphhopper.yml
graphhopper:
  # Đường dẫn tới file OSM PBF bên trong container (gắn qua PVC)
  datareader.file: /data/osm/vietnam-latest.osm.pbf
  
  # Thư mục chứa CH graph cache
  graph.location: /data/graph-cache
  
  # Các profile phương tiện được hỗ trợ
  profiles:
    - name: car
      vehicle: car
      weighting: fastest
    - name: motorcycle
      vehicle: motorcycle
      weighting: fastest
    - name: bike
      vehicle: bike
      weighting: fastest
  
  ch.profiles: car,motorcycle,bike
  
  # Cấu hình Server ports
  server:
    application_connectors:
      - type: http
        port: 8989
    admin_connectors:
      - type: http
        port: 8990
  
  import.osm.ignored.highways: ""

Tạo Kubernetes ConfigMap từ tập tin cấu hình:

kubectl create configmap graphhopper-config \
    --from-file=graphhopper.yml=./graphhopper.yml \
    -n logistics

Kiến Trúc Kubernetes PersistentVolume Cho Dữ Liệu OSM Graph

GraphHopper yêu cầu hai (2) thư mục chính cần lưu trữ lâu dài (persistent directories):

  1. Thư mục chứa OSM PBF (/data/osm/): Chứa tập tin dữ liệu bản đồ OSM thô.
  2. Thư mục Graph Cache (/data/graph-cache/): Chứa các tập tin CH graph đã được xử lý xong (lưu trữ lại qua các lần khởi động lại Pod để tránh phải build lại từ đầu).
flowchart TD
    PVC["PersistentVolumeClaim: graphhopper-data (30Gi RWO)"]
    PVC --> OSM["/data/osm/vietnam-latest.osm.pbf"]
    PVC --> GRAPH["/data/graph-cache/ (CH graphs)"]
    
    POD["GraphHopper Pod"]
    POD --> PVC
    POD --> CM["ConfigMap: graphhopper-config"]
    
    SVC["Service: graphhopper-svc :8989"] --> POD

Cấu Hình PersistentVolumeClaim

# graphhopper-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: graphhopper-data
  namespace: logistics
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 30Gi    # Áp dụng cho Việt Nam: OSM PBF (~170MB) + CH graphs (~3GB) + bộ nhớ đệm
  storageClassName: ssd  # Sử dụng SSD để tăng tốc độ nạp đồ thị

Phương Pháp Tải File OSM PBF Vào PVC

Sử dụng Pod tạm thời để sao chép file OSM vào PVC:

# Tạo Pod uploader tạm thời
kubectl run osm-uploader --image=alpine --restart=Never \
    --overrides='{"spec":{"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"graphhopper-data"}}],"containers":[{"name":"uploader","image":"alpine","command":["sleep","3600"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}]}}' \
    -n logistics

# Chờ Pod sẵn sàng
kubectl wait --for=condition=ready pod/osm-uploader -n logistics

# Sao chép file OSM PBF vào PVC
kubectl cp vietnam-latest.osm.pbf logistics/osm-uploader:/data/osm/vietnam-latest.osm.pbf

# Dọn dẹp Pod tạm
kubectl delete pod osm-uploader -n logistics

Tối Ưu RAM Và JVM (RAM and JVM Tuning): Thiết Lập Requests/Limits Cho CH Graphs Trên Kubernetes

Nguyên nhân chính khiến GraphHopper bị sập hoặc gặp sự cố trên Kubernetes chính là lỗi Out Of Memory (OOMKill) của JVM.

Yêu Cầu Bộ Nhớ (Memory Requirements)

GraphHopper nạp toàn bộ dữ liệu đồ thị CH (Contraction Hierarchies) vào bộ nhớ RAM bằng cơ chế memory-mapped files qua MappedByteBuffer của Java. Nguồn bộ nhớ này nằm ngoài vùng Heap (off-heap memory) — không tính vào dung lượng Heap JVM (-Xmx) nhưng lại chiếm dụng giới hạn bộ nhớ (memory limit) của container.

Ví dụ với dữ liệu bản đồ Việt Nam cho các profile xe hơi (car), xe máy (motorcycle), xe đạp (bike):

  • Bộ nhớ JVM Heap (-Xmx): 512MB–1GB (phục vụ xử lý rác GC overhead và tính toán thuật toán routing)
  • Bộ nhớ CH Graph mmap (off-heap): ~2–3 GB
  • Giới hạn bộ nhớ Container (Memory Limit): -Xmx + dung lượng Graph + 512MB buffer đệm = 4–5 GB

Cấu Hình JVM Configuration

Thiết lập các tham số JVM thông qua biến môi trường JAVA_OPTS:

env:
  - name: JAVA_OPTS
    value: >-
      -Xmx768m
      -Xms256m
      -XX:+UseG1GC
      -XX:MaxDirectMemorySize=4g
      -XX:+ExitOnOutOfMemoryError
      -Djava.nio.file.spi.DefaultFileSystemProvider=sun.nio.fs.UnixFileSystemProvider

Ý nghĩa các cờ cấu hình quan trọng:

  • -Xmx768m: Giới hạn bộ nhớ JVM Heap (dùng cho tính toán route và GC metadata)
  • -XX:MaxDirectMemorySize=4g: Giới hạn dung lượng Direct Memory ngoài Heap (phục vụ mmap dữ liệu đồ thị)
  • -XX:+ExitOnOutOfMemoryError: Buộc JVM thoát ngay khi gặp lỗi OOM thay vì rơi vào trạng thái degraded — giúp Kubernetes chủ động restart Pod một cách sạch sẽ và tức thì

Cấu Hình Tài Nguyên (Requests và Limits) Trên K8s

resources:
  requests:
    memory: "4Gi"   # Đảm bảo Node có đủ bộ nhớ trống tối thiểu
    cpu: "1000m"    # 1 vCPU dành cho xử lý truy vấn CH (tùy chỉnh dựa theo lượng query rate)
  limits:
    memory: "5Gi"   # 4GB graph mmap + 768MB heap + 512MB buffer
    cpu: "4000m"    # Cho phép bùng nổ (bursting) CPU trong quá trình xử lý CH pre-processing lúc khởi động

Lựa Chọn StatefulSet Hay Deployment Trên Kubernetes

GraphHopper yêu cầu dữ liệu PersistentVolume ở chế độ ReadWriteOnce (đơn ghi - single writer). Do đó trên Kubernetes, bạn không thể gắn 2 replicas của GraphHopper cùng lúc vào một PVC tại cùng một thời điểm.

Nên dùng StatefulSet khi:

  • Mỗi replica cần một PVC riêng biệt (ví dụ từng node GraphHopper xử lý dữ liệu cho từng khu vực riêng).
  • Cần định danh Pod cố định (stable pod names) cho mục đích service discovery.

Nên dùng Deployment khi:

  • Bạn chỉ chạy 1 replica duy nhất (mô hình logistics phổ biến).
  • Bạn sử dụng lưu trữ chia sẻ ReadWriteMany (như NFS hay Ceph) hỗ trợ nhiều replicas đọc đồng thời.

Dưới đây là file manifest StatefulSet hoàn chỉnh cho môi trường production:

# graphhopper-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: graphhopper
  namespace: logistics
spec:
  serviceName: graphhopper
  replicas: 1
  selector:
    matchLabels:
      app: graphhopper
  template:
    metadata:
      labels:
        app: graphhopper
    spec:
      containers:
        - name: graphhopper
          image: ghcr.io/graphhopper/graphhopper:10.0
          args: ["start", "/config/graphhopper.yml"]
          
          ports:
            - name: http
              containerPort: 8989
            - name: admin
              containerPort: 8990
          
          env:
            - name: JAVA_OPTS
              value: "-Xmx768m -Xms256m -XX:+UseG1GC -XX:MaxDirectMemorySize=4g -XX:+ExitOnOutOfMemoryError"
          
          resources:
            requests:
              memory: "4Gi"
              cpu: "1000m"
            limits:
              memory: "5Gi"
              cpu: "4000m"
          
          volumeMounts:
            - name: data
              mountPath: /data
            - name: config
              mountPath: /config
          
          # Readiness Probe: GraphHopper chỉ báo sẵn sàng khi đã hoàn thành giai đoạn CH pre-processing
          readinessProbe:
            httpGet:
              path: /health
              port: 8990
            initialDelaySeconds: 60    # Thời gian chờ ban đầu để graph bắt đầu khởi tạo
            periodSeconds: 15
            failureThreshold: 40       # 40 * 15s = 10 phút tối đa cho giai đoạn CH pre-processing
          
          # Liveness Probe: Khởi động lại Pod nếu JVM bị treo hoặc ngưng hoạt động
          livenessProbe:
            httpGet:
              path: /health
              port: 8990
            initialDelaySeconds: 300   # 5 phút chờ ban đầu cho lần khởi tạo CH pre-processing đầu tiên
            periodSeconds: 30
            failureThreshold: 3
      
      volumes:
        - name: config
          configMap:
            name: graphhopper-config
  
  # Template PVC tự động cho từng replica
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 30Gi
        storageClassName: ssd

Cập Nhật Dữ Liệu Bản Đồ OSM Không Downtime (Zero-Downtime Map Updates)

Dữ liệu OSM cần được cập nhật thường xuyên để cập nhật tuyến đường mới, biển cấm hoặc thay đổi luồng giao thông. Tần suất cập nhật trong hệ thống logistics thường diễn ra hàng tuần (Geofabrik cập nhật dữ liệu quốc gia hàng tuần).

Chiến Lược Blue-Green Cập Nhật Graph

Do chế độ ReadWriteOnce của PVC ngăn việc hai instance GraphHopper ghi chung vào một ổ đĩa, chúng ta sử dụng chiến lược Blue-Green:

flowchart LR
    SVC["Service: graphhopper-svc"] -->|phân tuyến route tới| ACTIVE["StatefulSet: graphhopper-blue (ACTIVE)"]
    STANDBY["StatefulSet: graphhopper-green (UPDATING)"]
    
    subgraph "Quy Trình Cập Nhật"
        DOWNLOAD["Tải file OSM PBF mới"] --> COPY["Sao chép vào PVC của green"]
        COPY --> RESTART["Khởi động lại graphhopper-green"]
        RESTART --> WAIT["Chờ Readiness Probe"]
        WAIT --> SWITCH["Patch Service selector trỏ sang green"]
        SWITCH --> CLEANUP["Dọn dẹp Blue cũ"]
    end

Phương pháp này yêu cầu duy trì 2 StatefulSet với 2 PVC riêng biệt (Active và Standby). Quy trình cập nhật tự động bằng CronJob:

  1. Tải tập tin OSM PBF mới.
  2. Sao chép vào PVC của StatefulSet Standby.
  3. Khởi động lại Pod Standby và chờ Readiness Probe vượt qua.
  4. Thay đổi Service Selector để trỏ sang Pod Standby mới.
  5. Xóa bỏ Pod Active cũ hoặc chuyển thành Standby mới.

Để áp dụng quy trình GitOps quản lý cả CronJob và StatefulSet, hãy xem GitOps Ở Quy Mô Lớn (GitOps at Scale): Kubernetes & ArgoCD Cho Microservices.


Cấu Hình Health Probes Và Readiness Gates: Xử Lý Quá Trình CH Pre-Processing

Điểm hay mắc lỗi nhất là cấu hình initialDelaySeconds quá ngắn cho Readiness Probe. GraphHopper thực hiện các bước Khởi động:

  1. Tải tập tin OSM PBF (lần đầu: 2–5 phút cho bản đồ Việt Nam).
  2. Xây dựng đồ thị Contraction Hierarchies (lần đầu: 5–15 phút).
  3. Ghi dữ liệu CH graph ra đĩa.
  4. Nạp dữ liệu CH graph vào bộ nhớ (các lần khởi động tiếp theo: 1–3 phút).

Trong các lần khởi động lại sau (khi CH graph đã được cache sẵn), thời gian khởi động chỉ tốn 1–3 phút. Tuy nhiên ở lần đầu tiên khi đọc file OSM mới, quá trình có thể kéo dài 15–20 phút.

Vì vậy, cấu hình Readiness Probe nên dùng initialDelaySeconds: 60 kết hợp failureThreshold: 40 (tổng thời gian chờ tối đa 600 giây). Cấu hình Liveness Probe sử dụng initialDelaySeconds: 300 để tránh việc Kubernetes khởi động lại Pod trong khi đang thực hiện CH pre-processing.


Giám Sát GraphHopper Trên K8s Với Prometheus Và Grafana

Cổng Admin của GraphHopper phơi các chỉ số theo định dạng Dropwizard. Bạn có thể sử dụng Prometheus JMX Exporter hoặc cấu hình Prometheus ServiceMonitor:

# Cấu hình ServiceMonitor cho Prometheus Operator
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: graphhopper
  namespace: logistics
spec:
  selector:
    matchLabels:
      app: graphhopper
  endpoints:
    - port: admin
      path: /metrics
      interval: 30s

Các chỉ số quan trọng cần cảnh báo (Alerting Metrics):

  • graphhopper_routing_requests_total: Tổng lượng request routing (cảnh báo khi tỷ lệ lỗi > 0).
  • graphhopper_routing_time_seconds: Độ trễ P99 tính toán route (cảnh báo nếu > 200ms).
  • jvm_memory_used_bytes{area="heap"}: Dung lượng JVM Heap đã dùng (cảnh báo nếu > 85% -Xmx).
  • process_resident_memory_bytes: Dung lượng RSS thực tế của tiến trình (cảnh báo khi tiến trình áp sát giới hạn memory limit của container).

Những Câu Hỏi Thường Gặp (FAQ)

GraphHopper tiêu tốn bao nhiêu RAM để phục vụ mạng lưới giao thông Việt Nam?

Với dữ liệu bản đồ Việt Nam và các profile car + motorcycle + bike, GraphHopper cần khoảng 3–4 GB off-heap memory cho dữ liệu đồ thị mmap, cộng thêm 512MB–1GB cho JVM Heap. Nên thiết lập giới hạn Memory Limit trên Kubernetes là 5 GB để đảm bảo an toàn. Với khu vực Đông Nam Á (Thái Lan, Indonesia, Philippines…), dung lượng RAM yêu cầu là 15–20 GB.

Có thể chạy GraphHopper trên Kubernetes với nhiều replicas không?

Có, nhưng có ràng buộc. Nếu dùng PVC ở chế độ ReadWriteOnce, bạn không thể mount 1 PVC cho nhiều Pods cùng lúc. Để scale ngang nhiều replicas, bạn cần dùng đĩa lưu trữ ReadWriteMany (NFS, Ceph) hoặc triển khai mỗi replica một StatefulSet/PVC riêng biệt tương ứng với từng khu vực địa lý.

Làm sao để cập nhật dữ liệu bản đồ OSM mà không bị downtime?

Sử dụng mô hình Blue-Green: duy trì 2 StatefulSet (Active và Standby). Bạn tiến hành tải file OSM mới và build lại đồ thị trên StatefulSet Standby. Khi Standby đã sẵn sàng (Readiness Probe chuyển sang Ready), tiến hành cập nhật Kubernetes Service Selector để chuyển hướng toàn bộ traffic sang Standby.


🤝 Kết nối với tôi

Bạn đang gặp phải những thách thức tương tự về kiến trúc hệ thống, mở rộng quy mô (scaling) hay dịch chuyển (migration)? Hãy kết nối với tôi trên LinkedIn, theo dõi GitHub của tôi, hoặc gửi một email để trao đổi nhé.


❓ Câu Hỏi Thường Gặp (FAQ)

Q1: Tự Tổ Chức Triển Khai GraphHopper trên Kubernetes với Dữ liệu OSM giải quyết vấn đề cốt lõi nào trong kiến trúc hệ thống?

Step-by-step guide to deploying GraphHopper on Kubernetes with OpenStreetMap data: Docker image, PVC for OSM PBF files, RAM tuning, and health probes.

Q2: Những lưu ý quan trọng nhất khi triển khai thực tế là gì?

Cần chú trọng phân tầng ranh giới trách nhiệm (bounded context), thiết lập cơ chế fallback dự phòng, và giám sát chặt chẽ qua metrics OpenTelemetry để phát hiện sớm các điểm nghẽn.

Q3: Làm sao để kiểm thử và đánh giá hiệu quả sau khi áp dụng?

Áp dụng kiểm thử tải (load test), benchmark độ trễ P95/P99 trước và sau triển khai, kết hợp tracing phân tán để xác minh tính ổn định dưới tải cao.