Dựng một cái API cùi bắp để réo gọi Graphhopper qua lệnh http.Get thì dễ ợt. Nhưng để rèn ra một cái API Gateway đẳng cấp Principal dư sức gánh còng lưng 10,000 cuốc xe nã request cùng lúc mà không đột tử, đó đích thực là một khóa masterclass về Hệ thống Phân tán (Distributed Systems).
Answer-first: Graphhopper là một con service tuyến dưới (downstream service) lệ thuộc cực nặng vào CPU. Nếu cái API Golang của bạn cứ nhắm mắt hứng đạn (traffic) rồi ném tuốt luốt thẳng xuống dưới, chỉ cần Graphhopper hắt hơi sổ mũi chạy chậm lại một nhịp thôi là bầy Goroutines sẽ dồn ứ lại thành một cục máu đông, vắt kiệt sạch RAM của server và kích hoạt một cú sập dây chuyền (cascading failure). Bạn buộc phải dựng lên một phòng tuyến “Bảo Vệ Đa Lớp” (Defense in Depth) xài tới chiêu Giới hạn Đồng thời (Concurrency Bounding), Cầu dao điện (Circuit Breakers), và trò Bắn tin Bất đồng bộ (Asynchronous Pub/Sub).
1. Phòng Tuyến Đa Lớp (Defense in Depth): Che Chắn Cho Routing Engine
Còng Tay Giới Hạn Đồng Thời (errgroup)
Khi phải cày cuốc tính toán nhiều lộ trình riêng biệt cùng lúc, luôn nhớ lôi thằng golang.org/x/sync/errgroup ra xài. Cái chí mạng ở đây là bạn bắt buộc phải gọi lệnh g.SetLimit(10) để chặn đứng thảm họa “đàn voi giẫm đạp” (thundering herd). Việc xích cổ (Limiting) số lượng request đẩy ra ngoài sẽ chặn cái thảm cảnh chính service của bạn lỡ tay tự DDOS luôn con Graphhopper nhà trồng của mình.
Cầu Dao Điện - Circuit Breaker (gobreaker)
Chuyện gì sẽ xảy ra nếu Graphhopper cà rề cà rề tốn tận 5 giây mới thèm trả lời? Nếu không lắp Cầu dao điện (Circuit Breaker), con API Golang nhà bạn sẽ cứ điên cuồng đẻ thêm kết nối mới cho tới khi cạn kiệt RAM mới thôi. Bằng cách lấy thằng sony/gobreaker bọc lại mấy cú gọi (calls), cái cầu dao này sẽ nhảy “Chết Nhanh” (Fail Fast - trạng thái Open) ngay khi tỷ lệ lỗi (error rate) dựng đứng lên, lập tức tát gáo nước lạnh 503 vỗ mặt client và tranh thủ câu giờ cho Graphhopper hồi sức.
Triệt Tiêu Trùng Lặp (singleflight)
Tưởng tượng 100 con giời cùng mở app để hóng ETA tới một cái show ca nhạc khổng lồ trong cùng một tích tắc. Thay vì ngu ngốc dội 100 cú request y hệt nhau vào đầu Graphhopper, hãy móc chiêu golang.org/x/sync/singleflight ra xài. Nó bóp cổ 100 cú request đồng thời (concurrent requests) giống nhau như đúc gộp lại thành đúng một (a single) cú gọi HTTP xuống tuyến dưới, rồi ngay tức thì bố cáo (broadcasting) kết quả về lại cho cả 100 miệng ăn đang há mỏ chờ.
2. Bẫy Rập Dọn Rác (GC) Protobuf (Mảng Bẹp Dúm - Flattened Arrays)
Nếu bạn tính phơi cái routing engine ra cho anh em xài nội bộ qua gRPC, bạn định nghĩa (define) một cái Ma trận Khoảng cách khổng lồ 10,000 x 10,000 kiểu gì đây?
Cách của mấy tay lơ tơ mơ là xài mảng lồng nhau (nested arrays): repeated MatrixRow rows với mỗi row chứa một đống repeated double distances. Ở sân chơi Golang, việc lôi một cái mảng lồng nhau 10,000x10,000 ra nhào nặn (deserializing) sẽ đẻ ra 100 triệu cái object bé tí tẹo. Quả bom này sẽ kích nổ một chuỗi thảm họa tạm dừng Dọn Rác (Garbage Collection - GC pause), đóng băng cứng ngắc con API của bạn suốt mấy giây trời.
Chiêu Của Dân Senior: Xài một cái Mảng Phẳng 1 Chiều (Flattened 1D Array). Khai báo cái Protobuf của bạn thành repeated double data kẹp chung với int32 rows và int32 cols. Nó sẽ đúc ra đúng một (exactly one) cục object duy nhất trên RAM. Bạn có thể tự mình bới ra đúng cái ô (cell) cần tìm bằng toán học thông qua công thức index = row * cols + col.
3. Định Tuyến Bất Đồng Bộ (Asynchronous) Với Dapr Workflows
HTTP là trò đồng bộ (synchronous). Mấy bài toán ma trận (Matrix calculations) thì toàn ngốn hàng phút đồng hồ. Hai cái này vốn dĩ là nước với lửa chả ưa gì nhau.
Khi phải rặn ra mấy cái ma trận siêu to khổng lồ (ví dụ: đo đường từ 1,000 cái nhà kho tới 1,000 cái tiệm), bạn éo thể nào gồng mình mở toang cái cổng kết nối HTTP mãi được.
- Thằng Gateway Golang hứng request xong là ngay lập tức phun ra (publishes) một cái sự kiện
RouteRequestedchui qua ngả Dapr Pub/Sub. Nó vỗ mặt client bằng cái mã202 Acceptedrồi lủi mất. - Một gã công nhân ngầm (background worker) sẽ hốt cái sự kiện đó lên. Vì cái nghiệp vụ tìm đường nó lằng nhằng rắc rối lôi kéo nhiều khâu (Geocoding -> Graphhopper -> Notification), nên nhét vào Dapr Workflows.
- Dapr Workflows bảo kê cho trò Thực Thi Bất Tử (Durable Execution). Lỡ gã worker có đột quỵ giữa chừng lúc đang tính toán, Dapr sẽ tự động hốt xác và chạy lại (resumes) cái workflow từ điểm lưu (checkpoint) gần nhất ngay khi nó tỉnh lại.
Ngon lành, vác trên vai một cái API Gateway vững như bàn thạch rồi, giờ làm sao để hiển thị (visualize) hàng trăm ngàn lộ trình xe cộ đang nườm nượp chạy ngoài kia mà không làm cái trình duyệt (browser) đơ vêu mỏ? Lăn bánh qua Phần 5: UI Trực Quan Hóa Lộ Trình Bằng Mapbox & Deck.gl để vọc cách dựng một bảng điều khiển bản đồ (map dashboard) hiệu năng bàn thờ.
Hỏi Nhanh Đáp Gọn (FAQ): Ba Cái Bẫy Rập Định Tuyến Đằng Sau Cánh Gà
ch.disable=true. Trò Phân Cấp Rút Gọn (Contraction Hierarchies - Speed Mode) nó tính toán sẵn hết ba cái đường nhanh nhất rồi, nên nó bị mù (cannot process) với mấy cái trọng số nhảy múa (dynamic weights) ngay lúc đang chạy (runtime). Để ép nó xài luật tự chế, bạn BẮT BUỘC phải nhét cú POST request kèm theo cái đuôi ?ch.disable=true để bắt Graphhopper quay về chế độ Uốn Dẻo (Flexible Mode - xài Dijkstra/A*).http.WithMiddleware(tracing.Client()). Trò này sẽ bơm cái bối cảnh (context) traceparent chuẩn W3C vào đầu (headers) thằng HTTP, hàn dính (linking) cái request vã từ gateway thẳng tới tận mâm log của server Graphhopper.Maximum visited nodes exceeded của Graphhopper rồi. Đây là cái chốt an toàn (safety mechanism) ngự trong file config.yml (routing.max_visited_nodes) để ngăn không cho cái RAM cạn sạch (RAM exhaustion). Đừng có nhắm mắt nhắm mũi bơm bự cái trần giới hạn này lên; thay vào đó, hãy thiết kế lại cái worker Golang của bạn để băm (split) cái ma trận khổng lồ đó ra thành những cái lưới nhỏ hơn (smaller sub-grids) cho dễ nuốt.