跪拜 Guibai
← All articles
Backend · Interview · Java

How Real-Time Bus Arrival Works: GPS Drift, ETA Accuracy, and 100K QPS Reads

By Rain的Java大神之路 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

For anyone building location-aware, high-write, high-read systems, the article delivers a production-grade pattern for turning noisy GPS streams into stable predictions while absorbing 100K QPS reads. The practical techniques - hash-tagged Lua scripts, Kalman-baseline ETA, hysteresis state machines, and cache-breakdown defenses - transfer directly to ride-hailing, delivery tracking, or any IoT telemetry platform.

Summary

The article walks through the entire architecture behind a bus app's "2 stops, about 5 minutes" message. Key decisions are driven by scale: 30,000 vehicles writing 3,000-6,000 GPS reports per second, while peak read QPS reaches 100K inside a first-tier city. Writes are buffered through Kafka and processed in Flink, which cleans raw GPS, projects positions onto a route model, and computes ETA. Everything about the data model avoids the common mistake of using straight-line distance; instead, a dist_from_start field in the route-stop table lets ETA be computed as a simple subtraction along the route.

The write path tackles GPS drift with wild-point filters, coordinate system conversion between WGS84 and GCJ-02, and a timestamp-version Lua optimistic lock for out-of-order idempotency. Arrival judgment uses a hysteresis dual-threshold state machine to prevent fence-edge flicker. ETA prediction evolves from a naive distance/speed formula to a segmented baseline plus real-time correction, then to a three-layer weighted speed model, and finally to a Kalman increment correction that only adjusts a bias coefficient per vehicle, converging as the bus approaches. Flink implements this with keyed state and only predicts the next 3-10 stops.

For reads, the system uses a Caffeine local cache, Redis, and a Lua batch query on a ZSET whose score is the vehicle's distance along the route. SingleFlight and logical expiration protect against cache breakdown, while aggressive degradation rules mean availability beats accuracy: better to show a timetable estimate than a white screen. The push side aggregates subscribers by line+stop to cut a million-user push down to a few thousand channel broadcasts. Storage follows a hot/warm/cold three-tier separation to keep billions of trajectory records affordable.

Takeaways
— The route-stop table must store dist_from_start, the cumulative distance along the route, so ETA becomes a subtraction instead of a straight-line measurement.
— Raw GPS is filtered by two hard rules: speed jumps above 120 km/h or position jumps over 500 meters in under 5 seconds are discarded; the wild-point rate falls from 3% to 0.1%.
— Coordinate systems matter: GPS chips output WGS84 while Chinese map services use GCJ-02; failing to convert shifts the entire fleet by hundreds of meters.
— A timestamp-version optimistic lock in Lua atomically discards out-of-order GPS packets and automatically expires offline vehicles, and it is about 10x faster than a distributed lock.
— Arrival/departure judgment uses hysteresis dual thresholds: enter at 40m with speed under 2.0 m/s, leave at 80m with speed over 5.0 m/s, eliminating fence-edge jitter.
— ETA prediction moves from pure distance/speed (average error 180s) to segmented historical baselines with real-time correction (75s) to three-layer weighted speed plus smoothing (28s).
— A Kalman filter estimates a per-vehicle duration deviation coefficient k, correcting only remaining segments; cold start uses the historical baseline with k=1.0.
— Flink predictions are limited to the next 3-10 stops to cap write amplification.
— The read path uses a Caffeine local cache (1-3s TTL), Redis (3-10s), and a single Lua script that does ZREVRANGEBYSCORE over a ZSET whose score is the vehicle's distance along the route, picking out all not-yet-arrived vehicles in one command.
— Cache breakdown protection relies on SingleFlight (one rebuild per key) and logical expiration (return stale value, rebuild async); TTL random offsets and empty-value caching handle avalanche and penetration.
— Push traffic is collapsed from millions of users to thousands of channel broadcasts by aggregating subscribers per line+stop, with 5-second batched pushes.
— Storage uses three tiers: Redis for hot real-time, TDengine/ClickHouse for warm 7-day retention, and S3/OSS for cold archives, cutting storage cost by 60-70%.
— Degradation is designed top-down: Redis failure falls back to local cache for 60 seconds; Flink failure returns historical timetable ETA; the system never shows a blank screen.
— The article reports a P99 read latency under 100ms and a target availability of 99.9% (with 99.95% achievable via infrastructure redundancy).
Conclusions

The most common misunderstanding in real-time bus systems is treating vehicles as "nearby points" with Redis GEO; the actual core is a distance-along-route model because buses travel fixed lines.

The article's ETA evolution shows that rule-based statistical baselines quickly get error down from 180s to 28s, and a Kalman correction adds stability without requiring heavy ML, which is a pragmatic path for production.

Availability is explicitly prioritized over accuracy: the design accepts eventual consistency and 3-second delays as long as the UI never shows a white screen, a tradeoff that matches real user tolerance for transit data.

The single most transferable pattern is using a ZSET score as the vehicle's route-distance, which turns a multi-step query into one Lua script - a clean example of pushing computation into the data store.

The article's advice to "quantify first, then design" is not just interview advice; the specific QPS/TPS estimates shape every subsequent choice, from Kafka partition keys to cache TTLs.

Concepts & terms
dist_from_start
A column in the route-stop table that stores the cumulative distance (in meters) of each stop from the start of the route. It's the foundation for computing remaining distance and ETA as simple arithmetic along the route, rather than using straight-line distance.
Hysteresis dual threshold
A state machine technique where two asymmetric thresholds are used for entering and leaving a state (e.g., arrival at a bus stop). The wide gap between thresholds forms a delay zone that prevents oscillation when GPS jitter causes repeated threshold crossings.
Hash tag in Redis Cluster
A way to force related keys to land in the same Redis Cluster slot by wrapping a common substring in braces, e.g., `{line:302:1}:veh:...`. This allows multi-key operations like Lua scripts to run without cross-slot errors.
SingleFlight
A concurrency pattern where only one request for a given key is allowed to rebuild a cache entry; all other concurrent requests wait for that single flight. It prevents cache breakdown when a hot key expires and many requests hit the data source simultaneously.
Logical expiration
A cache technique where the cache entry does not physically expire; instead, a flag in the value marks it as logically expired. When read, the stale value is returned immediately and an async thread rebuilds it. This avoids the empty window and stampede of physical TTL expiry.
From the discussion
Featured comments
PasteCode

What's more reasonable is to not have the onboard terminal at all [grin][grin][grin]

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗