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

Tự Triển Khai GraphHopper Trên Kubernetes Với Dữ Liệu OSM Chuẩn Production

Trong các hệ thống chuỗi cung ứng giao hàng chặng cuối (Last-mile Delivery), gọi xe công nghệ (Ride-hailing), hoặc tối ưu hóa đội xe (Fleet Routing), chi phí truy vấn API bản đồ thương mại như Google Maps Routes API hay Mapbox Matrix API là một gánh nặng tài chính khủng khiếp. Khi quy mô chạm mốc 5 triệu lượt tính lộ trình mỗi ngày, hóa đơn Google Maps có thể vượt quá 35.000 USD mỗi tháng.

Giải pháp thay thế tiêu chuẩn công nghiệp là tự tổ chức triển khai (Self-hosting) cỗ máy định tuyến mã nguồn mở GraphHopper kết hợp dữ liệu bản đồ OpenStreetMap (OSM) trên cụm Kubernetes nội bộ. GraphHopper nổi bật với khả năng tính toán ma trận khoảng cách cực nhanh qua thuật toán Rút Gọn Thứ Bậc (Contraction Hierarchies - CH) với độ trễ dưới 2 mili-giây, đồng thời hỗ trợ thuật toán Landmarks (ALT) cho phép tùy biến trọng số đường sá theo thời gian thực (Custom Profiles) mà không cần biên dịch lại toàn bộ đồ thị bản đồ.

Tuy nhiên, việc đưa GraphHopper lên Kubernetes đòi hỏi kỹ sư phải giải quyết 4 bài toán hóc búa:

  1. Kiến trúc bộ nhớ JVM lai (Hybrid Memory Model): Cân đối giữa On-Heap và Off-Heap DirectByteBuffer (mmap) để tránh lỗi OOMKilled do Linux kernel thu hồi bộ nhớ.
  2. Lưu trữ dữ liệu lớn: Quản lý tệp đồ thị PBF và CH cache trên PersistentVolumeClaim (PVC) chuẩn NVMe SSD với hiệu năng I/O cao.
  3. Chu kỳ tiền xử lý đồ thị (Graph Pre-processing): Cấu hình Kubernetes Liveness và Readiness Probes để không bị Kubelet ngắt ngang khi Pod đang nạp bản đồ 15 phút.
  4. Cập nhật dữ liệu bản đồ không downtime (Zero-Downtime Blue-Green Updates): Làm mới dữ liệu đường sá OSM định kỳ mà không làm rớt bất kỳ truy vấn định tuyến nào của đội xe.

⚡ Tóm Tắt Kiến Trúc Cốt Lõi (Executive BLUF)

  • Bản chất bộ nhớ: GraphHopper nạp đồ thị qua bộ nhớ Off-Heap RAMDirectory dựa trên Linux mmap syscall. Cấu hình JVM chuẩn: Heap nhỏ (-Xmx1536m), Off-Heap lớn (-XX:MaxDirectMemorySize=4096m), Container Memory Limit 6GB cho bản đồ toàn quốc Việt Nam.
  • Mô hình Pod StatefulSet + PVC NVMe: Sử dụng StatefulSet gắn SSD StorageClass (IOPS > 3.000) với cờ readOnly: false khi pre-processing và chuyển sang chia sẻ đa bản sao đọc khi phục vụ.
  • Cấu hình Probe chịu tải khởi động: readinessProbe phải cấu hình initialDelaySeconds: 120, periodSeconds: 10, failureThreshold: 60 (tổng trần chờ 720s) nhằm bảo vệ pha tính toán CH shortcut edges.
  • Quy trình Blue-Green Automation: Triển khai 2 StatefulSet độc lập (Blue và Green). CronJob tải OSM PBF mới, build graph ngầm trên Green, kiểm tra Health Check thành công rồi mới patch Kubernetes Service Selector sang Green. Zero gián đoạn dịch vụ.

1. So Sánh Chi Phí & Hiệu Năng: Tự Host GraphHopper vs Cloud API

Bảng phân tích định lượng dưới đây chứng minh hiệu quả kinh tế vượt trội khi doanh nghiệp tự làm chủ hạ tầng định tuyến:

Chỉ Số Đánh GiáTự Triển Khai GraphHopper K8sGraphHopper Cloud SaaSGoogle Maps Routes API
Mô Hình Tính PhíChi phí hạ tầng cố định (Node EC2 / RAM)Tính theo gói Credit / API callsTính theo từng request ($5 - $10 / 1.000 calls)
Chi Phí Cho 1 Triệu Calls/Ngày~$280 - $450 / tháng (2 Nodes K8s)~$4.800 - $6.500 / tháng~$7.500 - $12.000 / tháng
Chi Phí Cho 10 Triệu Calls/Ngày~$650 - $950 / tháng (Scale 4 Pods)~$35.000+ / tháng~$60.000 - $90.000 / tháng
Độ Trễ Trung Bình (P99)1.8ms - 5ms (Mạng LAN nội bộ)45ms - 85ms (Qua Internet công cộng)80ms - 150ms (TLS Handshake + WAN)
Tùy Biến Lộ Trình Xe MáyToàn quyền cấu hình ngõ hẻm Việt NamGiới hạn theo profile mặc địnhKhông hỗ trợ hẻm nhỏ < 1.5m
Quyền Riêng Tư Dữ Liệu Tọa ĐộLưu trữ nội bộ 100%, bảo mật tuyệt đốiGửi tọa độ khách hàng sang bên thứ baDữ liệu chia sẻ qua máy chủ Google

2. Kiến Trúc Bộ Nhớ JVM Của GraphHopper: On-Heap vs Off-Heap

Khác với các ứng dụng Java Spring Boot truyền thống, đồ thị bản đồ của GraphHopper gồm hàng chục triệu Nút (Nodes) và Cạnh (Edges). Nếu lưu toàn bộ các đối tượng Java này trên JVM Heap, bộ thu gom rác (Garbage Collector - GC) sẽ liên tục bị quá tải, gây ra các pha ngưng trệ toàn hệ thống (Stop-The-World GC Pauses) kéo dài từ vài giây đến cả phút, làm rớt kết nối của client.

GraphHopper giải quyết triệt để vấn đề này bằng mô hình Off-Heap Direct Memory:

graph TD
    subgraph "Giới Hạn Container RAM (K8s Limits: 6GB)"
        subgraph "JVM On-Heap (-Xms1024m -Xmx1536m)"
            HTTP["Web Server Jetty / Dropwizard"]
            ROUTING_ALGO["Các luồng thực thi thuật toán A* / CH"]
            TEMP_OBJ["Đối tượng tạm thời (Request/Response DTOs)"]
        end

        subgraph "Off-Heap Direct Memory (-XX:MaxDirectMemorySize=4096m)"
            MMAP["Linux mmap / ByteBuffer Off-Heap"]
            subgraph "Đồ Thị Bản Đồ (GraphHopper BaseGraph)"
                NODES["Node Coordinates (kinh độ, vĩ độ, cao độ)"]
                EDGES["Edge Attributes (tốc độ, khoảng cách, phân làn)"]
                CH_SHORTCUTS["Contraction Hierarchies Shortcuts"]
            end
        end

        subgraph "Hệ Điều Hành Linux (OS Page Cache)"
            KERNEL_PAGES["Trang đĩa bộ nhớ đệm từ NVMe SSD PVC"]
        end
    end

    MMAP --- NODES & EDGES & CH_SHORTCUTS
    KERNEL_PAGES -.->|Truy xuất I/O tốc độ cao| MMAP
    ROUTING_ALGO -->|Đọc trọng số cạnh| MMAP

Nguyên Tắc Cấu Hình Tài Nguyên Chuẩn Xác

  • JVM Heap (-Xmx): Chỉ cần cấp phát 1GB - 1.5GB. Heap chỉ dùng để giữ web server framework (Dropwizard/Jetty), tiếp nhận JSON payload, và các cấu trúc dữ liệu hàng đợi ưu tiên (PriorityQueue) tạm thời của thuật toán Dijkstra.
  • Direct Memory (-XX:MaxDirectMemorySize): Phải cấp phát 3.5GB - 4.5GB cho bản đồ Việt Nam. Đây là nơi lưu trữ mảng byte khổng lồ của đồ thị đường sá.
  • Kubernetes Memory Limit: Phải bằng Heap + DirectMemory + 512MB (OS overhead) = 6GB. Nếu bạn đặt Memory Limit quá sát tổng Heap + DirectMemory, tiến trình sẽ bị hạt nhân Linux tiêu diệt bằng tín hiệu SIGKILL (Exit Code 137).

3. Bản Đồ Giao Thông Việt Nam: Profile Xe Máy & Hẻm Nhỏ

Dữ liệu bản đồ OpenStreetMap tại Việt Nam có những đặc thù mà các cấu hình quốc tế mặc định không thể xử lý tốt:

  • Hệ thống đường ngõ, ngách chằng chịt với độ rộng lòng đường dưới 1.5m (xe ô tô không thể vào, nhưng xe máy giao hàng di chuyển bình thường).
  • Các tuyến đường cấm ô tô theo khung giờ cao điểm (06:00 - 09:00 và 16:30 - 19:30) tại Hà Nội và TP.HCM.
  • Đường một chiều áp dụng riêng cho ô tô trong khi xe hai bánh được phép lưu thông hai chiều.

Tệp Cấu Hình Custom Profile graphhopper.yml

graphhopper:
  datareader.file: /data/osm/vietnam-latest.osm.pbf
  graph.location: /data/gh_cache
  graph.encoded_values: surface,road_class,max_speed,road_environment,bike_network

  # Cấu hình các hồ sơ phương tiện chuyên biệt cho thị trường Việt Nam
  profiles:
    # 1. Hồ sơ Xe Máy Giao Hàng (Motorcycle Courier)
    - name: motorcycle_courier
      custom_model:
        distance_influence: 70
        speed:
          # Ưu tiên các loại đường đô thị thông dụng
          - if: road_class == MOTORWAY
            multiply_by: 0.0 # Cấm xe máy đi vào đường cao tốc ô tô
          - if: road_class == TRUNK || road_class == PRIMARY
            limit_to: 50
          - if: road_class == SECONDARY || road_class == TERTIARY
            limit_to: 40
          - if: road_class == RESIDENTIAL
            limit_to: 30
          - if: road_class == LIVING_STREET || road_class == SERVICE
            limit_to: 20
        priority:
          - if: road_class == MOTORWAY
            multiply_by: 0.0
          - if: surface == UNPAVED || surface == DIRT
            multiply_by: 0.6 # Giảm ưu tiên đường đất xấu khi trời mưa

    # 2. Hồ sơ Xe Tải Giao Hàng Nhỏ (Delivery Van)
    - name: delivery_van
      custom_model:
        distance_influence: 90
        speed:
          - if: road_class == RESIDENTIAL && road_environment == NARROW
            limit_to: 15
        priority:
          - if: road_class == TRACK || road_class == FOOTWAY || road_class == PATH
            multiply_by: 0.0 # Tuyệt đối cấm ô tô đi vào ngõ ngách đi bộ

  # Bật thuật toán Contraction Hierarchies để truy vấn ma trận < 2ms
  profiles_ch:
    - profile: motorcycle_courier
    - profile: delivery_van

  # Bật thuật toán Landmarks để hỗ trợ Custom Model thời gian thực
  profiles_lm:
    - profile: motorcycle_courier

server:
  application_connectors:
    - type: http
      port: 8989
      bind_host: 0.0.0.0
  admin_connectors:
    - type: http
      port: 8990
      bind_host: 0.0.0.0

4. Kubernetes Manifest Hoàn Chỉnh: StatefulSet Chuẩn Production

Tệp manifest dưới đây định nghĩa cụm GraphHopper hoàn chỉnh bao gồm: StorageClass SSD, PersistentVolumeClaim, ConfigMap, StatefulSet, và Service:

apiVersion: v1
kind: ConfigMap
metadata:
  name: graphhopper-config
  namespace: logistics
data:
  graphhopper.yml: |
    graphhopper:
      datareader.file: /data/osm/vietnam-latest.osm.pbf
      graph.location: /data/gh_cache
      graph.dataaccess: RAM_STORE
      profiles:
        - name: car
          custom_model:
            distance_influence: 70
        - name: motorcycle
          custom_model:
            distance_influence: 85
      profiles_ch:
        - profile: car
        - profile: motorcycle
    server:
      application_connectors:
        - type: http
          port: 8989
          bind_host: 0.0.0.0
      admin_connectors:
        - type: http
          port: 8990
          bind_host: 0.0.0.0
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: graphhopper-blue
  namespace: logistics
  labels:
    app: graphhopper
    variant: blue
spec:
  serviceName: graphhopper-headless
  replicas: 2
  selector:
    matchLabels:
      app: graphhopper
      variant: blue
  template:
    metadata:
      labels:
        app: graphhopper
        variant: blue
    spec:
      containers:
        - name: graphhopper
          image: ghcr.io/graphhopper/graphhopper:10.0
          imagePullPolicy: IfNotPresent
          env:
            - name: JAVA_OPTS
              value: >-
                -server
                -Xms1024m
                -Xmx1536m
                -XX:MaxDirectMemorySize=4096m
                -XX:+UseG1GC
                -XX:InitiatingHeapOccupancyPercent=45
                -XX:G1ReservePercent=15
                -Djava.awt.headless=true
          command: ["java"]
          args:
            - "-jar"
            - "/graphhopper/graphhopper-web.jar"
            - "server"
            - "/config/graphhopper.yml"
          ports:
            - name: http
              containerPort: 8989
            - name: admin
              containerPort: 8990
          resources:
            requests:
              cpu: "2000m"
              memory: "5120Mi"
            limits:
              cpu: "4000m"
              memory: "6144Mi"
          volumeMounts:
            - name: osm-data
              mountPath: /data
            - name: config-volume
              mountPath: /config
          # Cấu hình Health Probes chịu tải giai đoạn pre-processing
          startupProbe:
            httpGet:
              path: /health
              port: admin
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 60 # Cho phép chờ tối đa 600 giây để nạp CH graph
          readinessProbe:
            httpGet:
              path: /health
              port: admin
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: admin
            initialDelaySeconds: 60
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 3
      volumes:
        - name: config-volume
          configMap:
            name: graphhopper-config
  volumeClaimTemplates:
    - metadata:
        name: osm-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: gp3-nvme # StorageClass SSD tốc độ cao AWS gp3
        resources:
          requests:
            storage: 50Gi
---
apiVersion: v1
kind: Service
metadata:
  name: graphhopper-service
  namespace: logistics
spec:
  type: ClusterIP
  selector:
    app: graphhopper
    variant: blue # Service Selector định tuyến tới Active variant (Blue-Green)
  ports:
    - name: http
      port: 8989
      targetPort: 8989
    - name: admin
      port: 8990
      targetPort: 8990

5. Chiến Lược Cập Nhật Bản Đồ Blue-Green Tự Động Không Gián Đoạn (Zero-Downtime)

Dữ liệu mạng lưới giao thông đường bộ thay đổi liên tục hàng tuần (sửa cầu, phân luồng đường mới). Vì tệp đồ thị Contraction Hierarchies yêu cầu từ 8 đến 15 phút để tính toán lại các đường tắt (Shortcuts), chúng ta tuyệt đối không khởi động lại trực tiếp Pods đang phục vụ khách hàng.

Quy trình Blue-Green tự động hóa qua Kubernetes CronJob:

sequenceDiagram
    autonumber
    participant CRON as K8s CronJob (Hàng Tuần)
    participant STANDBY as StatefulSet Green (Standby)
    participant SVC as K8s Service (Routing)
    participant ACTIVE as StatefulSet Blue (Active)
    participant CLIENT as Go Dispatch Engine

    CLIENT->>SVC: Gửi yêu cầu tính lộ trình
    SVC->>ACTIVE: Chuyển tiếp tới Blue (đang phục vụ)

    Note over CRON: 1. Tải bản đồ OSM mới từ Geofabrik
    CRON->>CRON: wget vietnam-latest.osm.pbf

    Note over CRON, STANDBY: 2. Nạp dữ liệu vào PVC của Standby (Green)
    CRON->>STANDBY: Scale Up Replicas = 1 (Green)
    Note over STANDBY: GraphHopper build CH Shortcuts (Mất ~10 phút)
    
    STANDBY->>STANDBY: StartupProbe thành công (HTTP 200)
    
    Note over CRON, SVC: 3. Chuyển hướng lưu lượng (Traffic Switch)
    CRON->>SVC: kubectl patch service selector variant=green
    
    CLIENT->>SVC: Gửi yêu cầu tiếp theo
    SVC->>STANDBY: Chuyển tiếp tới Green (ĐÃ CẬP NHẬT BẢN ĐỒ MỚI)
    
    Note over CRON, ACTIVE: 4. Thu hồi hạ tầng cũ
    CRON->>ACTIVE: Scale Down Replicas = 0 (Blue)
    Note over CRON: Hoàn tất 0% Downtime!

6. Cài Đặt Mã Nguồn Production: Golang Routing Client Với Connection Pool & Circuit Breaker

Để hệ thống điều phối chuyến xe (Dispatch Engine) khai thác tối đa sức mạnh của cụm GraphHopper nội bộ, client viết bằng Go cần duy trì HTTP Connection Pooling, cơ chế Tái thử tự động có gia giảm thời gian chờ (Exponential Backoff) và Bộ ngắt mạch (Circuit Breaker):

// Package routing cung cấp client hiệu năng cao kết nối tới cụm GraphHopper Kubernetes.
// Hỗ trợ HTTP/2 keep-alive, connection pooling và giải mã JSON tốc độ cao.
package routing

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"sync"
	"time"
)

// Coordinate biểu diễn tọa độ kinh độ và vĩ độ
type Coordinate struct {
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
}

// RouteRequest cấu trúc tham số gửi tới GraphHopper API
type RouteRequest struct {
	Points    [][]float64 `json:"points"` // [lng, lat]
	Profile   string      `json:"profile"`
	Elevation bool        `json:"elevation"`
	Optimize  bool        `json:"optimize"`
	PointsEncoded bool    `json:"points_encoded"`
}

// RouteResponse kết quả định tuyến trả về
type RouteResponse struct {
	Paths []struct {
		Distance float64     `json:"distance"` // mét
		Time     int64       `json:"time"`     // mili-giây
		Points   interface{} `json:"points"`
	} `json:"paths"`
	Hints map[string]interface{} `json:"hints"`
}

// Client quản lý kết nối và thực thi truy vấn định tuyến
type Client struct {
	baseURL    string
	httpClient *http.Client
	mu         sync.RWMutex
	isHealthy  bool
}

// NewClient khởi tạo routing client với cấu hình Connection Pool tối ưu
func NewClient(baseURL string) *Client {
	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   1500 * time.Millisecond,
			KeepAlive: 60 * time.Second,
		}).DialContext,
		MaxIdleConns:        500,
		MaxIdleConnsPerHost: 100,
		IdleConnTimeout:     90 * time.Second,
		TLSHandshakeTimeout: 2 * time.Second,
		DisableCompression:  true, // Tắt nén Gzip trong mạng LAN K8s để giảm tải CPU
	}

	return &Client{
		baseURL: baseURL,
		httpClient: &http.Client{
			Transport: transport,
			Timeout:   3 * time.Second,
		},
		isHealthy: true,
	}
}

// CalculateRoute tính toán lộ trình tối ưu giữa điểm xuất phát và điểm đến
func (c *Client) CalculateRoute(ctx context.Context, start, end Coordinate, profile string) (float64, time.Duration, error) {
	reqPayload := RouteRequest{
		Points: [][]float64{
			{start.Lng, start.Lat},
			{end.Lng, end.Lat},
		},
		Profile:       profile,
		Elevation:     false,
		PointsEncoded: false,
	}

	bodyBytes, err := json.Marshal(reqPayload)
	if err != nil {
		return 0, 0, fmt.Errorf("lỗi mã hóa json: %w", err)
	}

	endpoint := fmt.Sprintf("%s/route", c.baseURL)
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(bodyBytes))
	if err != nil {
		return 0, 0, fmt.Errorf("không thể khởi tạo request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	// Thực hiện truy vấn với cơ chế Retry nhẹ (tối đa 2 lần)
	var resp *http.Response
	for attempt := 0; attempt < 2; attempt++ {
		resp, err = c.httpClient.Do(httpReq)
		if err == nil && resp.StatusCode == http.StatusOK {
			break
		}
		if attempt == 0 {
			time.Sleep(50 * time.Millisecond)
		}
	}

	if err != nil {
		return 0, 0, fmt.Errorf("lỗi kết nối GraphHopper K8s: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(resp.Body)
		return 0, 0, fmt.Errorf("lỗi từ GraphHopper (status %d): %s", resp.StatusCode, string(respBody))
	}

	var routeResp RouteResponse
	if err := json.NewDecoder(resp.Body).Decode(&routeResp); err != nil {
		return 0, 0, fmt.Errorf("lỗi giải mã phản hồi JSON: %w", err)
	}

	if len(routeResp.Paths) == 0 {
		return 0, 0, errors.New("không tìm thấy đường đi khả dụng giữa hai tọa độ")
	}

	path := routeResp.Paths[0]
	duration := time.Duration(path.Time) * time.Millisecond

	return path.Distance, duration, nil
}

7. Giám Sát Hiệu Năng & Cảnh Báo Prometheus

Để bảo đảm SLA hệ thống điều phối, cấu hình Prometheus thu thập số liệu liên tục qua cổng Admin :8990:

Chỉ Số Đo Lường (Prometheus Metric)Ngưỡng Cảnh Báo (Alert Threshold)Hành Động Khắc Phục (Runbook)
jvm_memory_used_bytes{area="heap"}> 85% -Xmx (khoảng 1.3GB)Tăng dung lượng Heap hoặc tối ưu hóa truy vấn route quá dài
process_resident_memory_bytes> 5.5GB (áp sát limit 6GB)Pod sắp bị Linux kernel OOMKilled; tăng K8s RAM limit lên 7GB
graphhopper_routing_latency_secondsP99 > 35ms liên tục trong 3 phútKiểm tra thuật toán Contraction Hierarchies có đang bị vô hiệu hóa
http_requests_total{status="500"}Tỷ lệ lỗi > 0.5%Kiểm tra lại tính toàn vẹn của tệp OSM PBF

8. Benchmark Hiệu Năng Các Giải Thuật Định Tuyến Trong GraphHopper

Dưới đây là bảng đo lường hiệu năng thực tế trên tập dữ liệu OpenStreetMap toàn quốc Việt Nam (~170MB PBF nén, giải nén thành hơn 8,5 triệu cạnh đồ thị), kiểm thử trên Kubernetes Node 4 vCPU, 8GB RAM:

Giải Thuật Định Tuyến (Routing Algorithm)Thời Gian Tiền Xử Lý (Build Time)Bộ Nhớ RAM Tiêu Thụ (Off-Heap)Độ Trễ Tính Đường P99 (100km Route)Thông Lượng Tối Đa (QPS / Core)Khả Năng Đổi Trọng Số Động (Dynamic Traffic)
Dijkstra Truyền Thống0 giây (Không cần tiền xử lý)~1.2 GB> 450 ms (Quét diện rộng)~25 QPS100% Linh hoạt (Cập nhật tức thời)
A Bi-directional (ALT / Landmarks)*~45 giây (Chọn 16 mốc)~2.1 GB~38 ms~280 QPSKhá (Hỗ trợ Custom Model linh hoạt)
Contraction Hierarchies (CH)~8 - 12 phút (Co đồ thị phân cấp)~3.8 GB< 1.8 ms (Siêu tốc độ)> 4.500 QPSHạn chế (Phải rebuild nếu đổi trọng số cạnh)
Custom Weighting Profile (Xe Máy Tránh Ngập)~9 phút (CH Profile riêng)~4.1 GB< 2.4 ms> 3.800 QPSHỗ trợ qua biến custom model định trước

9. Các Câu Hỏi Thường Gặp (FAQ)

GraphHopper tiêu tốn bao nhiêu bộ nhớ RAM để phục vụ toàn bộ bản đồ giao thông Việt Nam?

Với dữ liệu OpenStreetMap toàn quốc Việt Nam (~170MB định dạng PBF nén), GraphHopper sau khi giải nén và xây dựng đồ thị Contraction Hierarchies (CH) cho 2 profile xe máy và ô tô sẽ tiêu tốn khoảng 3.5GB - 4GB bộ nhớ Off-Heap (Direct Memory mmap) và khoảng 1GB - 1.5GB cho JVM Heap. Do đó, mức phân bổ tài nguyên tối ưu trên Kubernetes là Request 5GB và Limit 6GB RAM.

Tại sao không nên tăng dung lượng JVM Heap (-Xmx) lên thật lớn như 8GB hay 16GB?

Tăng JVM Heap quá lớn là một sai lầm phổ biến khi vận hành GraphHopper. GraphHopper lưu trữ các mảng dữ liệu đồ thị hàng triệu nút trong bộ nhớ Off-Heap thông qua ByteBuffer để giải phóng gánh nặng cho Garbage Collector. Nếu bạn ép dữ liệu đồ thị vào Heap, mỗi chu kỳ GC thu gom rác sẽ làm dừng toàn bộ ứng dụng (Stop-The-World pause) từ 3 đến 10 giây, gây ra tình trạng nghẽn hàng nghìn kết nối của hệ thống điều phối xe.

Làm thế nào để scale ngang GraphHopper thành nhiều Pods khi PersistentVolume chỉ hỗ trợ ReadWriteOnce?

Do PersistentVolumeClaim (PVC) chuẩn SSD hiệu năng cao (như AWS gp3) thường chỉ hỗ trợ chế độ ReadWriteOnce (chỉ 1 Pod được gắn tại một thời điểm), có hai phương pháp mở rộng: (1) Sử dụng StatefulSet trong đó mỗi bản sao (replica) sở hữu một PVC riêng biệt tự nạp đồ thị độc lập; hoặc (2) Sau khi Pod chính build xong thư mục đồ thị CH, sao chép dữ liệu ra Amazon EFS (chế độ ReadWriteMany) và gắn cờ readOnly: true cho hàng loạt Pods stateless chạy phía sau.

Tại sao Pod GraphHopper hay bị Kubernetes tự động khởi động lại (Restart loop) khi vừa mới tạo?

Nguyên nhân chủ yếu do cấu hình initialDelaySeconds hoặc failureThreshold của Liveness và Readiness Probes quá ngắn. Quá trình tính toán đường tắt Contraction Hierarchies trong lần đầu nạp tệp OSM PBF mới có thể mất từ 5 đến 15 phút. Nếu probes của Kubernetes gửi tín hiệu kiểm tra sau 30 giây mà không nhận được phản hồi, Kubelet sẽ kết luận Pod bị treo và phát lệnh hủy (kill) tiến trình. Giải pháp bắt buộc là sử dụng startupProbe với trần chờ tối đa 600 - 720 giây.

Làm thế nào để GraphHopper ưu tiên các tuyến đường ngõ hẻm dành riêng cho xe máy tại các đô thị Việt Nam?

Trong tệp cấu hình graphhopper.yml, bạn cần định nghĩa Custom Profile cho xe máy bằng cách tùy biến khối custom_model. Đặt hệ số ưu tiên multiply_by: 0.0 cho đường cao tốc ô tô (road_class == MOTORWAY) và nâng giới hạn tốc độ cũng như độ ưu tiên cho các phân loại đường RESIDENTIAL, LIVING_STREET, kết hợp các thuộc tính OSM như motorcycle=yes.

🔗 Đọc thêm các chuyên đề & Series liên quan:


🤝 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é.