Vụ setup một cái hệ thống định tuyến local (local routing engine) nổi tiếng là khoai lang. Mấy bài tutorial chung chung toàn quang đại cho một cái lệnh Docker cơ bản, chạy xong nó lăn quay ra chết im ỉm (crashes silently) làm dân dev ngơ ngác chả hiểu gì.

Trong bài hướng dẫn này, chúng ta sẽ bỏ qua mấy cái setup “Hello World” con nít. Chúng ta sẽ xắn tay dựng một môi trường chuẩn production (production-grade) xịn xò, nhào nặn chung dữ liệu OpenStreetMap (OSM), một cái container Docker Graphhopper (Java) được tinh chỉnh đàng hoàng, và một cái API Gateway Golang gánh tải song song (high-concurrency) cực khỏe.

1. Tải Về Và Cắt Gọt Dữ Liệu Bản Đồ

Answer-first: Tải cái dữ liệu OpenStreetMap thô dưới định dạng .osm.pbf từ server của Geofabrik. Để tránh cái cảnh ngốn hàng Gigabyte RAM lúc dev local, hãy xài tool osmium extract để cắt gọt (crop) cái bản đồ to chà bá cỡ quốc gia xuống thành một cái ô vuông nhỏ bé (bounding box) vây quanh đúng một thành phố thôi.

Cái mỏ dữ liệu bản đồ thô chuẩn ngành (industry standard source) nằm ở download.geofabrik.de. Bạn bắt buộc phải tải định dạng Protocolbuffer Binary Format (.osm.pbf), vì nó được nén cực mạnh và thiết kế sinh ra là để bón cho mấy cái routing engine.

Ngặt một nỗi, nếu bạn khăng khăng nạp nguyên một cái quốc gia (ví dụ: vietnam-latest.osm.pbf) vào bộ nhớ, nó sẽ nuốt của bạn đâu đó tầm 16GB RAM trở lên. Đối với dân dev code local trên một cái laptop tàng tàng, trò này đúng kiểu sát thủ vô hình (silent killer).

Mẹo của dân Pro: Cắt gọt bằng Osmium (Osmium Cropping) Cài ngay cái tool osmium-tool rồi xẻo cái bản đồ xuống thành một cái hộp giới hạn (bounding box) cụ thể (ví dụ như cắt nguyên cục TP.HCM ra thôi):

# Cắt gọt bản đồ theo tọa độ: min_lon, min_lat, max_lon, max_lat
osmium extract -b 106.5,10.7,106.8,10.9 vietnam-latest.osm.pbf -o hcmc.osm.pbf

Lệnh này bóp cái file bản đồ của bạn từ hàng Gigabyte xuống còn vài chục Megabyte tép riu, đảm bảo thời gian khởi động (startup times) nhanh như chớp mắt.

2. Kéo Graphhopper Lên Bằng Docker Compose

Answer-first: Khởi chạy Graphhopper xài luôn cái image official graphhopper/graphhopper:latest. Bạn bắt buộc phải bơm đủ bộ nhớ heap (heap space) bằng lệnh JAVA_OPTS=-Xmx6g để chặn đứng cái trò sập nguồn vì thiếu RAM (OOM - Out-Of-Memory) ngay cái giai đoạn chép (import) dữ liệu .pbf ban đầu.

Đẻ ra một cái file docker-compose.yml để cai quản cái routing engine. Để mắt kỹ tới mấy chỗ bóp volume (volume mappings) và các biến môi trường (environment variables) cực kỳ nhạy cảm sau đây:

version: '3'
services:
  graphhopper:
    image: graphhopper/graphhopper:latest
    ports:
      - "8989:8989"
    volumes:
      - ./data:/data         # Đổ file PBF của bạn vào đây
      - ./config:/config     # Nhét config.yml vào đây
      - ./srtm:/data/srtm    # Chí mạng: Chỗ chứa Cache Dữ liệu Độ Cao (Elevation)
    environment:
      - JAVA_OPTS=-Xmx6g     # Chặn mầm mống sập OOM lúc import
    command: >
      --input /data/hcmc.osm.pbf
      --graph-location /data/graph-cache
      --config /config/config.yml

3. Nhào Nặn Các Model Tự Chế (Đường Cấm & Độ Cao)

Answer-first: Chui vào file config.yml để đẻ ra các Custom Models (ví dụ: né trạm thu phí - toll roads) nằm ngay bên dưới phần priority. Trót lỡ đam mê tính toán đường dốc/đèo (3D uphill/downhill), hãy kích hoạt cục cung cấp dữ liệu độ cao srtm (elevation provider). Luật thép: Nhớ dọn dẹp sạch sẽ cái folder graph-cache mỗi khi bạn động chạm thay đổi mấy cái luật này.

Để ra lệnh cho engine lé né ba cái trạm thu phí, hãy nặn ra một cái cấu hình trọng số (weighting profile) riêng:

profiles:
  - name: my_car_no_tolls
    vehicle: car
    weighting: custom
    custom_model:
      priority:
        - if: "toll != NO"
          multiply_by: 0.0

Để bật tính năng móc thời gian đến (ETA) bám theo độ dốc của đồi núi, hãy kích hoạt dữ liệu độ cao SRTM. Nhớ khắc cốt ghi tâm phải map cái cache_dir ở file Docker compose để tránh cái cảnh mỗi lần khởi động lại là phải è cổ tải xuống (re-download) hàng đống Gigabyte dữ liệu địa hình:

graph:
  elevation:
    provider: srtm
    cache_dir: /data/srtm

4. API Gateway Bằng Golang (Tránh Bị Nghẽn Socket)

Answer-first: Khi code Golang client để nã request vào Graphhopper Matrix API, bạn bắt buộc phải xào nấu lại cái http.Transport mặc định với con số MaxIdleConnsPerHost bơm lên thật cao (ví dụ: 100) và áp luôn cái Timeout dứt khoát. Xài cái Go client gốc bảo đảm sẽ vác họa cạn kiệt socket (socket exhaustion) thê thảm khi chạy tải cao.

Khổ cái, cái http.Client gốc rễ của Go chỉ cho mồi đúng 2 idle connections cho mỗi host. Giả sử con microservice của bạn dội 50 cái Matrix request cùng lúc (concurrent) vào Graphhopper, Go sẽ cong mông lên mở rồi đóng 48 cái TCP connections mới tanh cứ mỗi giây lướt qua. Trò này sẽ nặn ra những đợt tăng đột biến TIME_WAIT khổng lồ và đập sập số lượng port.

Đây là bộ sậu Golang cấu hình kiểu dũng sĩ chuẩn production:

package main

import (
	"net/http"
	"time"
)

// Đóng đinh một cái transport và client xài chung cho toàn cục
var routingTransport = &http.Transport{
	MaxIdleConns:        100,
	MaxIdleConnsPerHost: 100, // CHÍ MẠNG: Đè bẹp cái giới hạn mặc định khốn nạn là 2
	IdleConnTimeout:     90 * time.Second,
}

var routingClient = &http.Client{
	Transport: routingTransport,
	Timeout:   15 * time.Second, // CHÍ MẠNG: Dập ngay hiểm họa rò rỉ (leaks) goroutine
}

Nhớ ghi não: Khi chọc vào cục endpoint POST /matrix, thằng Graphhopper nó kỹ tính bắt ép bưng đúng định dạng tọa độ GeoJSON y boong: [Kinh độ (Longitude), Vĩ độ (Latitude)].

Môi trường giờ đã ngon lành cành đào. Thế nhưng cái bẫy chết người (common pitfall) là dân dev cứ cắm thẳng API Gateway vào thẳng Routing Engine mà chẳng thèm chắt lọc vị trí (location filtering) khỉ khô gì. Tạt ngang qua Phần 3: Chỉ Mục Không Gian (Uber H3, PostGIS & Redis GEO) để lãnh ngộ cách vác Spatial Indexing ra làm cái phễu lọc tốc độ bàn thờ (high-speed pre-filter).


Hỏi Nhanh Đáp Gọn (FAQ): Gỡ Rối Trên Production

Tại sao cái con Graphhopper Docker container của tui hễ vừa đẻ ra là lăn đùng ra chết ngay tắp lự?
Trăm phần trăm là bị kẹt bệnh thiếu RAM (OOM - Out of Memory) lúc đang cố sức import cái đồ thị .osm.pbf ban đầu. Vác cái biến môi trường JAVA_OPTS=-Xmx4g (hoặc bự hơn càng tốt) nhét vào cái file docker-compose nhà bạn lẹ lên.
Tui sửa cái file config.yml để né trạm thu phí cmnr, mà sao nó vẫn cứ cắm đầu chui vào đó. Ủa là sao?
Bạn dính cái “Bẫy Graph-Cache” (Graph-Cache Trap) cmnr. Graphhopper nó làm quái gì biết trò thay nóng (hot-reload) luật lệ địa hình (topology rules). Bạn phải tự động lấy tay xóa sổ (delete) cái folder graph-cache để ép con engine phải nhập liệu (re-import) lại từ đầu cái data OSM và nấu chín nặn vô cái custom model mới của bạn.
Con lap ghẻ của tui RAM đâu ra 16GB để xử hết nguyên cái bản đồ quốc gia. Có cách nào chữa cháy không?
Xài cái tool dòng lệnh osmium extract kìa thím. Bạn có quyền vạc (crop) cái file quốc gia PBF 2GB chà bá xuống thành cái hộp bé tí xíu 50MB (city bounding box) trước khi ném vào mồm Graphhopper, cứu vãn được cả đống RAM.
Lỡ cái bản đồ OpenStreetMap nó chả có mặt mũi mấy cái đường chạy trong khu nhà kho tư nhân của công ty tôi thì sao?
Data OSM nó dễ dãi mở toang hết (extensible). Bạn cứ lôi mấy tool chạy trên desktop kiểu JOSM ra mà tự cắm cờ vẽ lấy mấy cái đường tư nhân của công ty bạn, rồi xuất mẻ (export) dưới dạng file XML .osm, đem trộn (merge) với file PBF Geofabrik trước khi ném vô cho máy xử lý.
Tại sao chọc vào cái API Matrix nó cứ rống lên cái lỗi 'invalid coordinate format' là sao?
Khác với cha nội Google Maps đòi ăn [Vĩ độ, Kinh độ], cái Graphhopper Matrix POST API nó khắt khe bắt ép đút đúng định dạng mảng GeoJSON y boong: [Kinh độ, Vĩ độ].
Sao cái API Gateway Golang nhà tôi nó cứ treo vêu mỏ (hang forever) mỗi lần gọi một cái Ma trận to oạch 1000x1000?
Graphhopper nhai mấy cái ma trận siêu to khổng lồ phải tốn tới vài giây đồng hồ. Nấu cái http.Client Golang mà không thèm kẹp cái giới hạn thời gian (explicit Timeout) dứt khoát, con goroutine chọc gọi nó sẽ bị tắc nghẽn vô thời hạn (block indefinitely), đẻ ra cái trò rò rỉ bộ nhớ (memory leaks) làm đơ cứng cả cái API.
Tự nuôi (self-hosting) Graphhopper thì tôi xài thả cửa Matrix API khỏi lo giới hạn đúng không?
Đúng rồi, bạn qua mặt (bypass) được mấy cái chốt chặn đăng ký (subscription limits) của mấy API ngoài. Ngặt nỗi, ranh giới sinh tử của bạn bị bó buộc ngặt nghèo (strictly bound) bởi số RAM của cái máy chủ. Hứng lên mà phang một quả ma trận 10,000x10,000 thì nó sẽ đè sập cái Java bằng bệnh Out-Of-Memory cái rụp liền nếu bạn không chịu bơm đủ độ lớn cho cái heap space.