🇬🇧 Read the English version of this article on tanhdev.com
Answer-first: So sánh OSRM và GraphHopper cho thấy OSRM vượt trội về tính ma trận cự ly cực nhanh (dưới 21ms cho 100x100) nhờ Contraction Hierarchies và C++ mmap. Ngược lại, GraphHopper trên Java linh hoạt hơn khi hỗ trợ Custom Models thời gian thực cho đội xe tải có giới hạn chiều cao và tải trọng logistics.
Điều kiện tiên quyết: Kiến thức vững chắc về giải thuật đồ thị (Dijkstra, A*), cơ chế quản lý bộ nhớ ảo Linux (
mmap, POSIX shared memory), cấu trúc dữ liệu OpenStreetMap (OSM .pbf) và lập trình mạng đồng thời với Go 1.25.
1. Khi Nào Doanh Nghiệp Cần Tự Triển Khai Routing Engine?
Trong giai đoạn đầu phát triển các ứng dụng gọi xe, giao thức ăn nhanh hoặc thương mại điện tử, việc sử dụng các API bản đồ thương mại như Google Maps Routes API hay Mapbox Directions mang lại sự tiện lợi vượt trội: độ sẵn sàng cao, bản đồ cập nhật liên tục và không tốn chi phí bảo trì hạ tầng.
Tuy nhiên, khi lưu lượng truy cập vượt ngưỡng 100.000 requests/ngày hoặc khi bài toán tối ưu hóa lộ trình đội xe (Vehicle Routing Problem - VRP) đòi hỏi phải tính toán các ma trận khoảng cách khổng lồ (ví dụ ma trận 500 × 500 = 250.000 cặp khoảng cách mỗi chu kỳ 30 giây), chi phí hóa đơn Cloud API sẽ bùng nổ ngoài tầm kiểm soát:
Chi phí tháng = 250.000 × 120 chu kỳ/h × 16 h/ngày × 30 ngày × $0,005 ≈ $72.000.000 (bất khả thi!)
Bên cạnh bài toán kinh tế, các API thương mại đóng kín hoàn toàn logic định tuyến. Doanh nghiệp logistics không thể tiêm các ràng buộc vật lý và quy định đô thị đặc thù vào thuật toán tìm đường:
- Đội xe tải 5 tấn bị cấm vào nội đô TP.HCM từ 06:00 đến 09:00 và từ 16:00 đến 20:00.
- Đội xe máy giao hàng cần ưu tiên luồn lách qua mạng lưới ngõ hẻm rộng dưới 1.5m để cắt giảm thời gian di chuyển, trong khi xe van 500kg tuyệt đối phải tránh các ngõ cụt này.
- Các nút giao thông cấm ô tô rẽ trái trong giờ cao điểm nhưng cho phép xe hai bánh chuyển hướng tự do.
Đây chính là thời điểm các kiến trúc sư trưởng bắt buộc phải đưa ra quyết định: Tự vận hành cụm Open-Source Routing Engine dựa trên dữ liệu OpenStreetMap (OSM). Hai cái tên xuất sắc nhất hiện nay là OSRM (Open Source Routing Machine) và GraphHopper. Cả hai đều mạnh mẽ, nhưng được xây dựng trên hai triết lý kiến trúc hoàn toàn đối lập.
(Nếu bạn đang xử lý tín hiệu GPS bị nhảy cóc và nhiễu do hiệu ứng hẻm vực đô thị nhà cao tầng, hãy tham khảo bài viết chuyên sâu về Kiến trúc Map Matching xử lý nhiễu GPS Urban Canyon).
2. OSRM: Tượng Đài Tốc Độ Viết Bằng C++ và Tối Ưu Hóa Bộ Nhớ Cực Hạn
Được phát triển hoàn toàn bằng C++, OSRM nổi danh trong giới kỹ nghệ không gian địa lý nhờ độ trễ truy vấn ở mức sub-millisecond (dưới 1 phần nghìn giây). Triết lý của OSRM là: Chuyển toàn bộ gánh nặng tính toán nặng nề sang giai đoạn tiền xử lý ngoại tuyến (Offline Pre-processing), tạo ra một cấu trúc đồ thị tinh gọn tối đa để có thể tra cứu tức thì tại runtime.
flowchart TD
OSM["Dữ liệu thô OpenStreetMap (.pbf)"] --> Extract["osrm-extract (Chạy Lua Profile)"]
Extract --> Graph["Đồ thị phân tích (.osrm)"]
subgraph PipelineCH ["Chế độ Contraction Hierarchies (CH)"]
Graph --> Contract["osrm-contract (Tạo đường tắt Shortcuts)"]
Contract --> CHFiles["Tệp CH Graph (.osrm.hsgr)"]
end
subgraph PipelineMLD ["Chế độ Multi-Level Dijkstra (MLD)"]
Graph --> Partition["osrm-partition (Chia cụm tế bào Cells)"]
Partition --> Customize["osrm-customize (Cập nhật trọng số Live Traffic)"]
Customize --> MLDFiles["Tệp MLD Graph (.osrm.mldgr)"]
end
CHFiles --> Engine["osrm-routed (C++ Engine)"]
MLDFiles --> Engine
Engine --> SHM["POSIX Shared Memory (mmap)"]
SHM --> Worker1["Pod Worker 1"]
SHM --> Worker2["Pod Worker 2"]
SHM --> WorkerN["Pod Worker N"]
2.1. Thuật Toán Contraction Hierarchies (CH): Con Quái Vật Về Tốc Độ
Vũ khí bí mật làm nên tên tuổi của OSRM là giải thuật Contraction Hierarchies (CH).
Trong giai đoạn tiền xử lý (osrm-contract), thuật toán xếp hạng toàn bộ các nút giao thông (nodes) trên bản đồ theo thứ tự mức độ quan trọng. Sau đó, nó tiến hành “co cụm” (contract) từng nút ít quan trọng hơn. Khi một nút bị co lại, nếu đường đi ngắn nhất giữa hai nút láng giềng đi qua nút này, một cạnh “đường tắt” (shortcut edge) sẽ được tạo ra nối trực tiếp hai nút láng giềng.
Nhờ vậy, khi có một truy vấn định tuyến từ điểm A đến điểm B, thuật toán tìm kiếm hai chiều (Bidirectional Dijkstra) không cần phải duyệt qua từng nút giao nhỏ trong khu dân cư. Thay vào đó, nó chỉ việc nhảy lên các đường tắt tương ứng với các trục cao tốc và quốc lộ lớn. Không gian tìm kiếm bị thu hẹp hàng ngàn lần:
- Thời gian truy vấn đơn lẻ (A-B query): Thường dao động từ 0.5ms đến 1.5ms.
- Ma trận khoảng cách (100x100): Hoàn thành trong khoảng 15ms - 20ms.
Nhược điểm chí mạng của CH: Tính bất biến cực đoan. Bất kỳ sự thay đổi nào về trọng số cạnh (như tắc đường đột xuất, tai nạn phong tỏa cầu, hay sửa đổi vận tốc di chuyển) đều làm vô hiệu hóa hệ thống đường tắt đã co. Bạn phải chạy lại toàn bộ quy trình osrm-contract kéo dài hàng giờ đồng hồ cho bản đồ cấp quốc gia. Do đó, CH hoàn toàn bất lực trước bài toán cập nhật dữ liệu giao thông thời gian thực (Live Traffic).
2.2. Multi-Level Dijkstra (MLD / CRP): Cân Bằng Giữa Tốc Độ và Cập Nhật Tức Thời
Để khắc phục tử huyệt của CH, OSRM cung cấp thêm kiến trúc Multi-Level Dijkstra (MLD), vốn được phát triển dựa trên giải thuật Customizable Route Planning (CRP).
MLD chia bản đồ thành các ô tế bào lồng nhau (nested cells) theo cấp bậc (Level 1: Phường/Xã, Level 2: Quận/Huyện, Level 3: Tỉnh/Thành). Trong giai đoạn tùy biến (osrm-customize), hệ thống chỉ tính toán ma trận chi phí giữa các nút biên (boundary nodes) của từng tế bào.
Khi xảy ra ùn tắc giao thông trên một tuyến đường, hệ thống chỉ cần cập nhật lại trọng số của các ô tế bào chứa tuyến đường đó và các tế bào cha phía trên, thay vì phải xử lý lại toàn bộ hành tinh. Quá trình cập nhật lưu lượng giao thông trực tiếp chỉ tốn vài giây. Đổi lại, độ trễ truy vấn của MLD chậm hơn CH khoảng 3-5 lần (khoảng 3ms - 8ms cho một truy vấn A-B), vẫn là một con số cực kỳ ấn tượng. Xem hướng dẫn chi tiết tại bài viết Triển khai OSRM Shared Memory trên Kubernetes cho Live Traffic.
2.3. Cơ Chế Linux Kernel mmap và POSIX Shared Memory
Điểm sáng kiến trúc rực rỡ nhất của OSRM nằm ở cách quản lý bộ nhớ. Thay vì đọc toàn bộ file nhị phân đồ thị hàng chục Gigabyte vào RAM của tiến trình ứng dụng, OSRM sử dụng syscall mmap kết hợp cờ MAP_SHARED.
Khi bạn chạy 10 Pod OSRM trên cùng một Kubernetes Node để phục vụ lưu lượng truy vấn cao, Linux Kernel sẽ nạp các trang nhớ (memory pages) của đồ thị vào Page Cache vật lý chung. Cả 10 tiến trình worker đều đọc chung một vùng nhớ vật lý này. Kết quả: 10 worker OSRM chỉ tiêu tốn lượng RAM vật lý tương đương với 1 worker duy nhất cộng thêm vài Megabyte RAM riêng cho heap execution context.
3. GraphHopper: Linh Hồn Java và Sự Linh Hoạt Tối Thượng Tại Runtime
Trái ngược với triết lý “hy sinh tất cả vì tốc độ thô” của OSRM, GraphHopper được viết bằng Java và hướng tới sự linh hoạt tối đa trong môi trường vận hành thực tế.
flowchart TD
Client["Client Request (Gửi kèm Custom Model JSON)"] --> GHAPI["GraphHopper HTTP/gRPC Service"]
subgraph MemoryArchitecture ["Kiến Trúc Bộ Nhớ Off-Heap"]
GHAPI --> JVMHeap["JVM Heap Space (Quản lý Context, Objects ngắn hạn)"]
GHAPI --> RAMDir["RAMDirectory (DirectByteBuffer)"]
RAMDir --> OffHeapGraph["Đồ Thị Bản Đồ Nằm Hoàn Toàn Off-Heap (Bypass GC)"]
end
subgraph Algorithms ["Thuật Toán Linh Hoạt"]
GHAPI --> CCH["Customizable Contraction Hierarchies (CCH)"]
GHAPI --> LM["Landmarks Algorithm (A* + ALT)"]
end
CCH --> EngineCore["Core Routing Engine"]
LM --> EngineCore
EngineCore --> DynamicWeights["Áp Dụng Custom Weighting (Tải trọng xe, Phí cầu đường, Hẻm nhỏ)"]
DynamicWeights --> FinalRoute["Kết Quả Lộ Trình Phù Hợp Phương Tiện"]
3.1. Custom Models: Vũ Khí Tối Thượng Của Logistics 3PL
Tính năng đỉnh cao nhất của GraphHopper là Custom Models. Bạn có thể gửi kèm một payload JSON trực tiếp trong request HTTP/gRPC để điều chỉnh trọng số di chuyển tức thời mà không cần recompile hay khởi động lại server.
Ví dụ, một đơn hàng yêu cầu xe tải 3.5 tấn vận chuyển hàng dễ vỡ cần tránh đường gồ ghề và tránh các tuyến đường thu phí:
{
"priority": [
{
"if": "surface == UNPAVED || surface == GRAVEL",
"multiply_by": "0.1"
},
{
"if": "toll == YES",
"multiply_by": "0.3"
},
{
"if": "maxweight < 3.5",
"multiply_by": "0.0"
}
],
"speed": [
{
"if": "road_class == RESIDENTIAL",
"limit_to": "30"
}
]
}
GraphHopper sẽ đánh giá biểu thức này trực tiếp trong quá trình duyệt đồ thị. OSRM hoàn toàn không có khả năng làm được điều này nếu không sửa mã Lua và biên dịch lại đồ thị!
3.2. Thuật Toán Landmarks (LM / ALT) và CCH
Để dung hòa giữa tốc độ tính toán và khả năng tiêm luật động qua Custom Models, GraphHopper kết hợp hai giải thuật:
- Landmarks (ALT - A*, Landmarks, Triangle Inequality): GraphHopper chọn ra các nút “cột mốc” trên bản đồ và tính trước khoảng cách từ tất cả các nút tới cột mốc. Khi tìm đường, bất đẳng thức tam giác giúp ước lượng hàm heuristic cực kỳ chặt chẽ, thu hẹp không gian tìm kiếm của A*. Giải thuật này cho phép tùy biến trọng số cạnh tự do tại runtime miễn là trọng số mới không nhỏ hơn trọng số cơ sở.
- Customizable Contraction Hierarchies (CCH): Khác với CH truyền thống của OSRM vốn phụ thuộc chặt vào trọng số cạnh khi xếp hạng nút, CCH của GraphHopper chỉ phụ thuộc vào cấu trúc hình học topo của đồ thị. Do đó, việc cập nhật trọng số giao thông hay thay đổi profile xe chỉ tốn vài trăm mili-giây.
3.3. RAMDirectory và DirectByteBuffer: Vượt Qua Rào Cản Java Garbage Collection
Một trong những nỗi sợ lớn nhất khi chạy hệ thống đồ thị hàng chục triệu cạnh trên JVM là hiện tượng Stop-The-World (STW) Garbage Collection. Nếu lưu trữ đồ thị hàng chục GB trên JVM Heap, tiến trình thu gom rác của Java có thể đóng băng toàn bộ hệ thống trong vài giây.
GraphHopper giải quyết triệt để vấn đề này thông qua lớp trừu tượng RAMDirectory kết hợp DirectByteBuffer. Toàn bộ cấu trúc đồ thị được cấp phát trên bộ nhớ Off-Heap (nằm ngoài vùng quản lý của JVM Garbage Collector). JVM Heap chỉ dùng để chứa các đối tượng request/response tồn tại trong tích tắc. Nhờ đó, P99 latency của GraphHopper luôn giữ được sự ổn định cao dưới tải lớn. Tham khảo hướng dẫn chi tiết tại Cách tự host GraphHopper trên Kubernetes với OSM.
4. Ma Trận So Sánh Kỹ Thuật Chi Tiết OSRM vs GraphHopper
Bảng so sánh dưới đây tổng hợp các tiêu chí kỹ thuật cốt lõi được đúc kết từ môi trường thực chiến phục vụ hàng triệu cuốc xe mỗi ngày:
| Tiêu Chí Kiến Trúc | OSRM (C++) | GraphHopper (Java) | Phân Tích Kỹ Sư Trưởng |
|---|---|---|---|
| Ngôn Ngữ Lõi | C++17/C++20 | Java 17/21 (LTS) | C++ cho hiệu năng kim loại; Java cho sự mở rộng và tích hợp hệ sinh thái enterprise. |
| Thuật Toán Chính | Contraction Hierarchies (CH), MLD/CRP | Landmarks (LM/ALT), CCH, Flexible Dijkstra | OSRM thiên về đường tắt tĩnh; GraphHopper linh hoạt đa thuật toán. |
| Độ Trễ Điểm A-B (P99) | 0.5ms - 2ms (CH Mode) | 8ms - 35ms (LM Mode) | OSRM nhanh hơn từ 5 đến 10 lần ở các truy vấn cơ bản. |
| Tính Ma Trận 500x500 | Sub-second (~350ms) | 2.5s - 5s (Cần tuning) | OSRM là lựa chọn số 1 cho bài toán Dispatching xe cần ma trận tức thì. |
| Quản Lý Bộ Nhớ | Linux mmap + POSIX Shared Memory (shm) | Off-Heap DirectByteBuffer (RAMDirectory) | OSRM cực kỳ tiết kiệm RAM khi scale nhiều Pods cùng node; GraphHopper ngốn nhiều RAM hơn. |
| Tùy Biến Trọng Số | Tĩnh qua tệp Lua (cần compile lại) | Cực cao qua JSON Custom Models tại Runtime | GraphHopper chiến thắng áp đảo về năng lực xử lý ràng buộc xe phức tạp. |
| Cập Nhật Giao Thông (Live Traffic) | Hỗ trợ qua MLD (osrm-customize) | Hỗ trợ qua CCH hoặc Speed Calculator | Cả hai đều đáp ứng tốt chu kỳ cập nhật traffic 1-2 phút/lần. |
| Hỗ Trợ Xe Máy / Hẻm Đô Thị | Cần biên dịch riêng một profile moto.osrm | Linh hoạt chuyển đổi profile hoặc filter bằng tags | GraphHopper thuận tiện hơn khi điều hành đội xe hỗn hợp (hàng ngàn xe máy + xe tải). |
| Thời Gian Khởi Động Pod | 1 - 3 giây (đọc shared memory) | 20 - 60 giây (warmup JVM & JIT compiler) | OSRM vượt trội về tốc độ Horizontal Pod Autoscaling (HPA). |
| Mở Rộng Logic Nghiệp Vụ | Phức tạp (đòi hỏi kỹ sư C++ chuyên sâu) | Rất thuận lợi (hàng ngàn kỹ sư Java/Kotlin có sẵn) | Chi phí nhân sự và bảo trì code của GraphHopper thường dễ chịu hơn. |
5. Hiện Thực Giao Thông Đô Thị Tại TP.HCM và Hà Nội: Những Cạm Bẫy Thực Địa
Khi thiết kế hệ thống định tuyến cho thị trường Việt Nam, các mô hình toán học thuần túy trên lý thuyết thường sụp đổ trước địa hình đô thị đặc thù.
[ Quận Bình Thạnh / Quận 1 ]
||
(Cầu Sài Gòn / Cầu Thủ Thiêm)
||
=================== Sông Sài Gòn ===================
||
(Cầu Ba Son / Hầm Thủ Thiêm)
||
[ Thành phố Thủ Đức ]
5.1. Rào Cản Địa Lý Sông Sài Gòn và Lỗi Khoảng Cách Đường Chim Bay
Khu vực Quận 1 / Bình Thạnh và TP. Thủ Đức bị chia cắt hoàn toàn bởi sông Sài Gòn.
- Một khách hàng tại Thảo Điền (Thủ Đức) có khoảng cách đường chim bay (Euclidean) tới tài xế ở bờ đối diện (phường 22, Bình Thạnh) chỉ chưa đầy 700 mét.
- Nếu thuật toán điều phối xe sử dụng bán kính tròn hoặc GeoHash/H3 đơn thuần để tìm tài xế gần nhất, nó sẽ liên tục gán tài xế tại Bình Thạnh cho cuốc xe ở Thảo Điền.
- Thực tế, tài xế phải di chuyển vòng qua Cầu Sài Gòn với quãng đường đường bộ hơn 5.2 km, mất 18 phút do ùn tắc chân cầu!
Bài học kiến trúc: Tuyệt đối không sử dụng khoảng cách hình học để dispatch cuốc xe chặng cuối. Bắt buộc phải truy vấn Distance Matrix Table thời gian thực từ OSRM để có chi phí thời gian di chuyển (travel duration) chính xác.
5.2. Mạng Lưới Hẻm Sâu và Giao Thông Hai Bánh (Motorcycle Routing)
Hơn 85% đơn hàng thương mại điện tử và đồ ăn nhanh tại Việt Nam được giao bằng xe máy. Hệ thống định tuyến bắt buộc phải giải quyết bài toán:
- Lọc ngõ hẻm theo độ rộng: Trong OSM, các tuyến ngõ thường được gắn thẻ
highway=residentialhoặchighway=service. Xe máy có thể đi qua hẻm rộng 1m, nhưng xe tải 1 tấn bắt buộc phải lọc bỏ các cạnh cówidth < 2.5hoặcmaxwidth < 2.5. - Cấm rẽ theo giờ: Đường Điện Biên Phủ, Nam Kỳ Khởi Nghĩa, Nguyễn Thị Minh Khai có các biển cấm ô tô rẽ trái trong khung giờ 06:00 - 09:00 và 16:00 - 19:30. GraphHopper cho phép cấu hình
turn_restrictionsphụ thuộc thời gian cực kỳ chính xác.
6. Sơ Đồ Kiến Trúc Hệ Thống Định Tuyến Hỗn Hợp (Hybrid Routing Architecture)
Để tối ưu hóa chi phí hạ tầng và hiệu năng, các công ty quy mô lớn thường áp dụng mô hình kiến trúc kết hợp cả OSRM và GraphHopper:
sequenceDiagram
autonumber
actor Driver as Tài xế / Khách hàng
participant GW as API Gateway (Envoy)
participant Dispatch as Go Dispatch Service
participant OSRM_Cluster as Cụm OSRM Matrix (C++)
participant GH_Cluster as Cụm GraphHopper Fleet (Java)
participant Redis_H3 as Redis H3 Spatial Cache
Driver->>GW: Yêu cầu đặt xe / Tìm lộ trình
GW->>Dispatch: Chuyển tiếp Request Context
alt Bài toán Ghép tài xế (1-to-N hoặc M-to-N Matrix)
Dispatch->>Redis_H3: Tra cứu ma trận khoảng cách đã cache
alt Cache Miss
Dispatch->>OSRM_Cluster: Gửi ma trận 100x100 (/table/v1/driving)
OSRM_Cluster-->>Dispatch: Trả về ma trận chi phí (Durations < 20ms)
Dispatch->>Redis_H3: Lưu cache với TTL 30s
end
Dispatch->>Dispatch: Chạy thuật toán Hungarian / VRP Matching
else Bài toán Lộ trình chi tiết Xe tải có tải trọng (Detailed Route)
Dispatch->>GH_Cluster: Gửi Custom Model (Weight, Height, Time Window)
GH_Cluster-->>Dispatch: Trả về Waypoints, Turn-by-Turn, Polyline (<30ms)
end
Dispatch-->>GW: Kết quả điều phối & Lộ trình tối ưu
GW-->>Driver: Hiển thị trên ứng dụng Mobile
7. Mã Nguồn Production: Go High-Throughput Routing Client với Fallback & Connection Pool
Dưới đây là đoạn mã nguồn chuẩn production viết bằng Go, hiện thực hóa bộ định tuyến thông minh (Smart Router Client). Client này có khả năng kết nối đồng thời tới cụm OSRM (để tính ma trận siêu tốc) và GraphHopper (để tính lộ trình chi tiết có ràng buộc tải trọng), tích hợp HTTP/2 Connection Pooling, Timeout Context, và cơ chế tự động Fallback.
// Package routing cung cấp client hiệu năng cao giao tiếp với OSRM và GraphHopper.
package routing
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
)
// Coordinate đại diện cho tọa độ kinh độ và vĩ độ (WGS84).
type Coordinate struct {
Longitude float64 `json:"lng"`
Latitude float64 `json:"lat"`
}
// VehicleConstraint định nghĩa các ràng buộc vật lý cho phương tiện vận tải.
type VehicleConstraint struct {
MaxWeightTons float64 `json:"max_weight_tons"`
AvoidTolls bool `json:"avoid_tolls"`
AvoidUnpaved bool `json:"avoid_unpaved"`
IsMotorcycle bool `json:"is_motorcycle"`
}
// RouteResult chứa thông tin lộ trình trả về cho dịch vụ điều phối.
type RouteResult struct {
DistanceMeters float64 `json:"distance_meters"`
DurationSeconds float64 `json:"duration_seconds"`
EngineUsed string `json:"engine_used"`
PolylineGeometry string `json:"geometry"`
}
// RouterClient quản lý kết nối HTTP pool tới các routing cluster.
type RouterClient struct {
httpClient *http.Client
osrmBaseURL string
graphhopperBaseURL string
}
// NewRouterClient khởi tạo client với cấu hình Connection Pool tối ưu cho throughput cao.
func NewRouterClient(osrmURL, ghURL string) *RouterClient {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 1 * time.Second,
ForceAttemptHTTP2: true,
}
return &RouterClient{
httpClient: &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
},
osrmBaseURL: osrmURL,
graphhopperBaseURL: ghURL,
}
}
// CalculateMatrixOSRM tính toán ma trận thời gian di chuyển qua OSRM Table Service.
func (c *RouterClient) CalculateMatrixOSRM(ctx context.Context, coords []Coordinate) ([][]float64, error) {
if len(coords) < 2 {
return nil, errors.New("cần tối thiểu 2 điểm tọa độ để tạo ma trận")
}
var coordStr string
for i, coord := range coords {
if i > 0 {
coordStr += ";"
}
coordStr += fmt.Sprintf("%.6f,%.6f", coord.Longitude, coord.Latitude)
}
url := fmt.Sprintf("%s/table/v1/driving/%s?annotations=duration", c.osrmBaseURL, coordStr)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("lỗi khởi tạo request OSRM: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("lỗi gọi OSRM Table API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("OSRM trả về lỗi HTTP %d: %s", resp.StatusCode, string(body))
}
var responseData struct {
Code string `json:"code"`
Durations [][]float64 `json:"durations"`
}
if err := json.NewDecoder(resp.Body).Decode(&responseData); err != nil {
return nil, fmt.Errorf("lỗi giải mã JSON từ OSRM: %w", err)
}
if responseData.Code != "Ok" {
return nil, fmt.Errorf("OSRM trả về mã lỗi nghiệp vụ: %s", responseData.Code)
}
return responseData.Durations, nil
}
// RouteWithFallback tính lộ trình chi tiết: ưu tiên GraphHopper nếu có ràng buộc tải trọng;
// tự động fallback sang OSRM nếu GraphHopper gặp sự cố.
func (c *RouterClient) RouteWithFallback(ctx context.Context, origin, dest Coordinate, vc VehicleConstraint) (*RouteResult, error) {
// Nếu có ràng buộc tải trọng hoặc đường tránh, ưu tiên gọi GraphHopper
if vc.MaxWeightTons > 0 || vc.AvoidTolls || vc.AvoidUnpaved {
res, err := c.routeGraphHopper(ctx, origin, dest, vc)
if err == nil {
return res, nil
}
// Log cảnh báo sự cố GraphHopper và thực hiện fallback xuống OSRM
fmt.Printf("GraphHopper gặp sự cố: %v. Thực hiện fallback sang OSRM...\n", err)
}
// Chạy qua OSRM (nhanh, tiêu chuẩn)
return c.routeOSRM(ctx, origin, dest)
}
func (c *RouterClient) routeGraphHopper(ctx context.Context, origin, dest Coordinate, vc VehicleConstraint) (*RouteResult, error) {
url := fmt.Sprintf("%s/route", c.graphhopperBaseURL)
profile := "car"
if vc.IsMotorcycle {
profile = "motorcycle"
}
payload := map[string]interface{}{
"points": [][]float64{
{origin.Longitude, origin.Latitude},
{dest.Longitude, dest.Latitude},
},
"profile": profile,
"elevation": false,
"points_encoded": true,
}
// Tiêm Custom Model động nếu xe có tải trọng
if vc.MaxWeightTons > 0 {
payload["custom_model"] = map[string]interface{}{
"priority": []map[string]interface{}{
{
"if": fmt.Sprintf("maxweight < %.1f", vc.MaxWeightTons),
"multiply_by": 0.0,
},
},
}
}
bodyBytes, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GraphHopper status code %d", resp.StatusCode)
}
var ghResp struct {
Paths []struct {
Distance float64 `json:"distance"`
Time float64 `json:"time"` // milliseconds
Points string `json:"points"`
} `json:"paths"`
}
if err := json.NewDecoder(resp.Body).Decode(&ghResp); err != nil {
return nil, err
}
if len(ghResp.Paths) == 0 {
return nil, errors.New("không tìm thấy lộ trình GraphHopper phù hợp")
}
return &RouteResult{
DistanceMeters: ghResp.Paths[0].Distance,
DurationSeconds: ghResp.Paths[0].Time / 1000.0,
EngineUsed: "GraphHopper",
PolylineGeometry: ghResp.Paths[0].Points,
}, nil
}
func (c *RouterClient) routeOSRM(ctx context.Context, origin, dest Coordinate) (*RouteResult, error) {
url := fmt.Sprintf("%s/route/v1/driving/%.6f,%.6f;%.6f,%.6f?overview=simplified",
c.osrmBaseURL, origin.Longitude, origin.Latitude, dest.Longitude, dest.Latitude)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OSRM status code %d", resp.StatusCode)
}
var osrmResp struct {
Code string `json:"code"`
Routes []struct {
Distance float64 `json:"distance"`
Duration float64 `json:"duration"`
Geometry string `json:"geometry"`
} `json:"routes"`
}
if err := json.NewDecoder(resp.Body).Decode(&osrmResp); err != nil {
return nil, err
}
if osrmResp.Code != "Ok" || len(osrmResp.Routes) == 0 {
return nil, errors.New("không tìm thấy lộ trình OSRM")
}
return &RouteResult{
DistanceMeters: osrmResp.Routes[0].Distance,
DurationSeconds: osrmResp.Routes[0].Duration,
EngineUsed: "OSRM",
PolylineGeometry: osrmResp.Routes[0].Geometry,
}, nil
}
8. Cấu Hình Kubernetes Production Sẵn Sàng Chịu Tải Cao
8.1. DaemonSet OSRM Khai Thác POSIX Shared Memory
Để tận dụng tối đa mmap dùng chung trên máy chủ Kubernetes vật lý, hãy triển khai OSRM dưới dạng DaemonSet hoặc gắn HostPath volume để chia sẻ vùng nhớ chung giữa các Pods:
apiVersion: apps/v1
kind: Deployment
metadata:
name: osrm-vietnam-car
namespace: routing-system
spec:
replicas: 8
selector:
matchLabels:
app: osrm-vietnam-car
template:
metadata:
labels:
app: osrm-vietnam-car
spec:
containers:
- name: osrm-engine
image: osrm/osrm-backend:v5.27.1
command: ["osrm-routed"]
args: ["--algorithm", "ch", "--shared-memory", "no", "/data/vietnam-latest.osrm"]
ports:
- containerPort: 5000
resources:
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
volumeMounts:
- name: map-data
mountPath: /data
readOnly: true
volumes:
- name: map-data
hostPath:
path: /mnt/disks/fast-nvme/osrm-data
type: Directory
9. Câu Hỏi Thường Gặp (FAQ)
Khi nào tôi nên chọn OSRM thay vì GraphHopper cho hạ tầng của mình?
mmap) giúp scale hàng chục Pods trên cùng node mà không tốn thêm RAM vật lý.GraphHopper giải quyết bài toán tùy biến xe tải và xe máy linh hoạt như thế nào?
Tại sao việc tính ma trận khoảng cách bằng OSRM lại quan trọng đối với các đô thị như TP.HCM hay Hà Nội?
Làm thế nào để GraphHopper tránh được lỗi Stop-The-World (STW) Garbage Collection của Java?
RAMDirectory dựa trên Java DirectByteBuffer. Toàn bộ cấu trúc đồ thị OpenStreetMap hàng chục Gigabyte được nạp hoàn toàn vào bộ nhớ Off-Heap (nằm ngoài sự quản lý của JVM Garbage Collector). JVM Heap chỉ xử lý các object ngắn hạn của từng HTTP request, nhờ đó loại bỏ hoàn toàn các đợt tạm dừng thu gom rác kéo dài và giữ P99 latency luôn ổn định dưới 30ms.Có thể cập nhật tình trạng kẹt xe trực tiếp (Live Traffic) trên OSRM được không?
osrm-customize chỉ mất 2-3 giây để cập nhật lại trọng số các ô bị ảnh hưởng mà không cần chạy lại quy trình tiền xử lý bản đồ.10. Tài Liệu Kỹ Thuật Liên Quan
- Giải Pháp VRP Đội Xe Đa Điểm: Khám phá cách nạp ma trận OSRM/GraphHopper vào bộ giải thuật toán tại Tối Ưu Đội Xe Giao Hàng CVRP & VRPTW với Go ALNS Engine.
- Tối Ưu OSRM Trên Kubernetes: Hướng dẫn cấu hình shared memory và live traffic chi tiết tại OSRM Shared Memory trên Kubernetes cho Live Traffic.
- Sửa Nhiễu Tọa Độ GPS Đô Thị: Kỹ thuật lọc nhiễu nhà cao tầng bằng Hidden Markov Model tại Kiến Trúc Map Matching Xử Lý Nhiễu GPS Urban Canyon.
- Hướng Dẫn Vận Hành GraphHopper: Cấu hình Docker và tuning JVM Off-Heap tại GraphHopper Distance Matrix Production Guide.
- Chuyên Đề Điều Phối Đơn Hàng: Xem trọn bộ kiến trúc chuỗi cung ứng tại Series Điều Phối Đơn Hàng & Tồn Kho Phân Tán.
