Uber 架构
2010 年 3 月,Travis Kalanick 在旧金山的 iPhone 4 发布会上,收到了一条来自朋友 Garrett Camp 的短信:「人们总是在需要打车的时候找不到车。我们应该做个 App,让车来找你。」
Travis 回复:「我马上过来。」
那一年,Uber 还叫 UberCab,只在旧金山市中心运营,车型仅限于豪华轿车(黑屏)。用户打开 App,按一下按钮,几分钟后一辆黑色林肯城市车就会出现在面前。那时候的 Uber 完全不像现在这么普及——它更像是一种「用钱换便利」的小众服务,只有科技圈和商旅人士知道。
但 Travis Kalanick 的野心远不止于此。他的长期愿景是:让每个人随时随地都能叫到车,而且价格合理。这个愿景后来演变成了共享出行经济的基础范式,也催生了全球估值最高的未上市科技公司之一。
公司画像
Uber 是一家总部位于旧金山的科技公司,核心业务是连接乘客和司机——乘客通过 App 叫车,司机通过 App 接单,Uber 从每笔订单中抽取佣金。截至 2024 年,Uber 业务覆盖全球 70+ 个国家、10,000+ 个城市,平台上有超过 500 万名司机,每季度完成超过 20 亿次行程。
理解 Uber 技术挑战的关键,在于它的核心业务特征——实时匹配:
- 秒级响应:用户叫车后,等待时间超过 30 秒就会感到焦虑,超过 60 秒就可能取消
- 位置强相关:司机和乘客都在地理空间中移动,匹配算法必须考虑实时位置
- 供需动态变化:上下班高峰期、演唱会散场、雨雪天气,供需关系剧烈波动
- 多边市场:平台同时服务乘客和司机,两边用户的行为互相影响
- 全球化复杂度:不同城市的法规、货币、语言、支付方式都不同
这些特征让 Uber 的技术挑战跟 Google(搜索)、Netflix(视频)、阿里(电商)都有本质区别。Google 的核心是「找出用户想要的信息」,Netflix 的核心是「把视频稳定地送到用户屏幕上」,而 Uber 的核心是在正确的时间,把正确的人和正确的车匹配在一起。
架构演进时间线
第一阶段:单体起步(2010-2013)
起步:Node.js 单体架构
Uber 的第一批工程师是 Travis Kalanick 从团购网站 Stamped 挖来的。Stamped 用的是 Node.js,所以 Uber 的后端也顺理成章地选了 Node.js。
这个选择有它的道理:Node.js 的异步 I/O 模型天然适合 I/O 密集型的 Web 服务,开发效率高,团队上手快。在 2010 年,Node.js 是硅谷创业公司的热门选择。
// Uber 早期 Node.js 单体服务(示意)
const express = require('express');
const app = express();
// 叫车接口
app.post('/api/v1/ride', async (req, res) => {
const { userId, pickupLocation, dropoffLocation } = req.body;
// 1. 验证用户
const user = await userService.findById(userId);
if (!user) return res.status(401).json({ error: 'Unauthorized' });
// 2. 查询附近司机(直接查 MySQL)
const nearbyDrivers = await db.query(`
SELECT * FROM drivers
WHERE status = 'available'
AND ST_DWithin(
current_location,
ST_Point(${pickupLocation.lng}, ${pickupLocation.lat}),
5000 -- 5km 半径
)
ORDER BY distance
LIMIT 10
`);
// 3. 匹配最优司机
const bestDriver = matchingAlgorithm.findBestMatch(nearbyDrivers, user);
// 4. 创建行程记录
const ride = await rideService.create({
userId,
driverId: bestDriver.id,
pickup: pickupLocation,
dropoff: dropoffLocation,
status: 'requested'
});
// 5. 通知司机
await pushNotification.send(bestDriver.deviceToken, {
type: 'NEW_RIDE_REQUEST',
rideId: ride.id
});
res.json({ rideId: ride.id, status: ride.status });
});
早期 Uber 的架构就是一个标准的 Node.js 单体:一个代码库、一个数据库(PostgreSQL)、一个团队维护所有功能。所有模块(用户、司机、行程、支付、通知)都在同一个代码库里,模块之间直接函数调用。
快速验证商业模式
这个阶段的 Uber,技术上没有任何特别之处。真正重要的是商业模式验证——证明了「用手机叫车」这个需求是真实存在的,用户愿意为此付费。
2010 年底,Uber 扩展到纽约、芝加哥、西雅图等城市。2011 年拿到了第一笔 1100 万美元的 A 轮融资,同年获得了知名风投 Benchmark 的 3200 万美元 B 轮投资。2012 年,Uber 推出了更便宜的车型 UberX,开始从高端小众走向大众市场。
但代码库也在快速膨胀。到 2013 年,Uber 的代码库已经有数十万行,数百个路由处理器,数十个数据库表。单体的开发效率开始下降:每次部署都要全量测试,一个小 bug 可能影响整个系统。
第二阶段:微服务拆分(2013-2016)
为什么必须拆分
Uber 早期架构有三个致命问题:
问题一:数据库写入成为瓶颈。MySQL 在高并发写入场景下性能急剧下降。行程创建、状态更新、位置上报,每秒可能有数万次写入。MySQL 的主从复制在高并发下延迟飙升,导致读操作读到旧数据。
问题二:扩展困难。所有功能都在一个代码库里,一个团队的修改可能意外影响另一个团队的功能。数据库表之间的依赖错综复杂,改一个表可能影响数十个接口。
问题三:故障隔离差。一个模块的 bug 可能拖垮整个系统。2013 年,Uber 的支付模块出现了一次 bug,导致部分行程被重复扣款。由于所有模块共享同一个数据库连接池,支付模块的 bug 耗尽了整个应用的连接资源,导致地图、通知等其他功能全部不可用。
MySQL Schemaless
Uber 工程师在 2013 年开发了一个介于关系数据库和 NoSQL 之间的存储系统——Schemaless。它基于 MySQL,但提供了类似 NoSQL 的灵活写入能力:
- schema-less 写入:可以在不修改表结构的情况下写入任意字段
- MySQL 底层:保留 MySQL 的事务能力和强一致性
- 水平分片:按 UUID 分片,每个分片独立扩展
-- Schemaless 的底层存储仍是 MySQL,但提供 KV + JSON 能力
-- 写入:不需要预定义字段
INSERT INTO trip_data (uuid, body, created_at)
VALUES (
'ride-uuid-12345',
'{"driver_id": 1001, "status": "in_progress",
"route": {"distance_km": 5.2, "duration_min": 12},
"surge_multiplier": 1.5}',
NOW()
);
-- 读取:MySQL 的事务保证一致性
START TRANSACTION;
SELECT * FROM trip_data WHERE uuid = 'ride-uuid-12345';
-- 复杂的读写混合操作都可以在事务内完成
COMMIT;
Schemaless 的核心价值是让 Uber 在不放弃事务能力的前提下,获得了 NoSQL 的扩展性。这对金融级数据(如行程、支付)尤其重要。
Cassandra 的引入
2014 年,Uber 引入了 Cassandra 来存储事件日志和时序数据:
- 高吞吐写入:Cassandra 的 LSM-Tree 存储引擎支持每秒百万级写入,非常适合 GPS 位置上报
- 多数据中心复制:Cassandra 原生支持跨数据中心复制,Uber 在全球多个数据中心部署了 Cassandra 集群
- 无需 schema:新增数据字段不需要 ALTER TABLE,对快速迭代友好
// Cassandra 存储司机位置轨迹(高吞吐写入)
@Service
public class DriverLocationEventStore {
@Autowired private CassandraTemplate cassandraTemplate;
private static final String TABLE = "driver_location_events";
// 写入位置事件:每天数十亿条
public void recordLocation(Long driverId, double lat, double lng,
long timestampMs) {
// 按天分表:driver_location_events_2024_03_15
String table = getTableForDate(new Date(timestampMs));
String cql = "INSERT INTO " + table +
" (driver_id, ts, lat, lng, geohash) VALUES (?, ?, ?, ?, ?)";
cassandraTemplate.execute(cql,
driverId, timestampMs, lat, lng, Geohash.encode(lat, lng));
// 这条数据会被用于:行程分析、司机审核、地图优化
}
}
Kafka 异步消息
Uber 在这个阶段引入了 Kafka 来解耦服务间通信:
- 异步削峰:高峰期的大量事件(位置上报、行程状态变更)先进入 Kafka,后端服务按自己的节奏消费
- 解耦:发消息的服务不需要知道谁在消费,消费的服务也不需要知道消息是谁发的
- 容错:Kafka 持久化消息,消费失败可以重试,不会丢失数据
// Kafka 事件处理:行程状态变更
@Service
public class TripEventProducer {
@Autowired private KafkaTemplate kafkaTemplate;
// 发布事件:任何服务都可以订阅这个 topic
public void publishTripCreated(Trip trip) {
TripEvent event = new TripEvent();
event.setEventType("TRIP_CREATED");
event.setTripId(trip.getId());
event.setTimestamp(System.currentTimeMillis());
event.setPayload(trip.toJson());
kafkaTemplate.send("trip-events", trip.getId().toString(), event);
}
public void publishTripCompleted(Trip trip) {
TripEvent event = new TripEvent();
event.setEventType("TRIP_COMPLETED");
event.setTripId(trip.getId());
event.setTimestamp(System.currentTimeMillis());
event.setAmount(trip.getFare().getAmount());
event.setDriverPayout(trip.getDriverPayout());
kafkaTemplate.send("trip-events", trip.getId().toString(), event);
// 消费方:支付系统、财务系统、数据分析系统
}
}
// 消费事件:支付服务
@KafkaListener(topics = "trip-events", groupId = "payment-service")
public class PaymentEventConsumer {
public void onEvent(TripEvent event) {
switch (event.getEventType()) {
case "TRIP_COMPLETED":
// 触发支付扣款
paymentService.chargeForTrip(event.getTripId());
break;
case "TRIP_CANCELLED":
// 处理退款(如有预授权)
paymentService.handleCancellation(event.getTripId());
break;
}
}
}
第三阶段:混合存储与智能化(2016-至今)
为什么需要混合存储
到 2016 年,Uber 的数据规模和复杂度已经远超早期架构的设计预期:
- 行程数据:需要强一致性(金融级),事务性写入,推荐关系数据库
- 位置数据:每秒数百万次 GPS 上报,需要极高写入吞吐,推荐 Cassandra / Druid
- 司机状态:需要毫秒级读取延迟(附近有没有车),推荐 Redis
- 分析数据:需要 OLAP 能力,支持复杂查询,推荐 ClickHouse / Druid
- 配置和元数据:需要强一致性的 KV,推荐 etcd / Zookeeper
没有一个单一数据库能同时满足所有需求,Uber 的选择是为不同类型的数据选择最适合的存储引擎。
核心调度系统
DISPATCH(调度服务)是 Uber 技术栈中实时性要求最高的组件。用户按下叫车按钮的那一刻,DISPATCH 系统需要在 100ms 以内完成以下全部工作:
- 验证用户身份和支付方式
- 查询附近可用司机(Geofence 查询)
- 计算每个司机的匹配分数(距离、评分、实时供需)
- 选择最优司机
- 发送派单请求,等待司机响应
// 调度服务:核心决策流程(伪代码)
@Service
public class DispatchService {
@Autowired private DriverService driverService;
@Autowired private PricingService pricingService;
@Autowired private LocationService locationService;
@Autowired private MetricsService metrics;
// 调度决策:SLA = 100ms
public DispatchDecision dispatch(Long passengerId, Location pickup) {
long startTime = System.nanoTime();
// 1. 查询附近可用司机(Redis GEO 查询,< 5ms)
// 这个查询本身要快,因为是整个链路的第一步
List<Driver> nearbyDrivers = driverService.findNearby(
pickup.getLat(), pickup.getLng(), 5000); // 5km 半径
if (nearbyDrivers.isEmpty()) {
metrics.record("dispatch.no_drivers", 1);
return DispatchDecision.noDriversAvailable();
}
// 2. 过滤不在线的司机(Redis 缓存,< 2ms)
// 司机 App 定期上报心跳,心跳超时视为离线
List<Driver> onlineDrivers = nearbyDrivers.stream()
.filter(d -> driverService.isOnline(d.getId()))
.collect(Collectors.toList());
// 3. 并行计算每个司机的匹配分数
// 充分利用多核,不要串行
List<DriverScore> scoredDrivers = onlineDrivers.parallelStream()
.map(driver -> calculateScore(driver, pickup))
.collect(Collectors.toList());
// 4. 选择最优司机
DriverScore bestMatch = scoredDrivers.stream()
.max(Comparator.comparingDouble(DriverScore::getTotalScore))
.orElseThrow();
// 5. 尝试派单(乐观锁,失败则重试最多 3 次)
DispatchResult result = attemptDispatchWithRetry(
bestMatch.getDriver(), passengerId, 3);
long latencyMs = (System.nanoTime() - startTime) / 1_000_000;
metrics.record("dispatch.latency_ms", latencyMs);
metrics.record("dispatch.candidates_count", scoredDrivers.size());
return result;
}
// 计算匹配分数
private DriverScore calculateScore(Driver driver, Location pickup) {
// 距离分数:越近越高(指数衰减,5km 外接近 0)
double distanceScore = calculateDistanceScore(
driver.getCurrentLocation(), pickup);
// 评分分数:越高越好(线性归一化)
double ratingScore = driver.getRating() / 5.0;
// 供需系数:高峰期加成
double surgeMultiplier = pricingService.getSurgeMultiplier(pickup);
// 综合分数:距离权重最高(60%),因为等待时间是用户最敏感的指标
double totalScore =
distanceScore * 0.6 +
ratingScore * 0.3 +
surgeMultiplier * 0.1;
return new DriverScore(driver, totalScore,
distanceScore, ratingScore, surgeMultiplier);
}
}
DISPATCH 的延迟预算分配(总计 100ms):
Redis GEO:实时位置服务
Uber 用 Redis 的 GEO 功能存储司机的实时位置,核心操作是 GEOADD(更新位置)和 GEORADIUS(查询附近)。
Redis GEO 基于 Geohash 算法,将经纬度编码为一个字符串,使得「查询附近」操作变成了字符串范围查询,在 Redis 的有序集合(Sorted Set)中可以高效完成。
// Redis GEO 实现司机位置服务
@Service
public class DriverLocationService {
@Autowired private RedisTemplate<String, String> redisTemplate;
private static final String GEO_KEY = "driver:locations";
// 更新司机位置:每 5 秒调用一次
public void updateLocation(Long driverId, double longitude, double latitude) {
// GEOADD:原子操作,同时设置经纬度和 Geohash
redisTemplate.opsForGeo().add(
GEO_KEY,
new Point(longitude, latitude),
driverId.toString()
);
// 同时更新司机在线状态(带过期时间)
String availabilityKey = "driver:" + driverId + ":available";
redisTemplate.opsForValue().set(availabilityKey, "1",
Duration.ofMinutes(2)); // 2 分钟内无心跳视为离线
}
// 查询附近可用司机
public List<Driver> findNearby(double longitude, double latitude,
double radiusMeters) {
// 构建圆形查询范围
Circle circle = new Circle(
new Point(longitude, latitude),
new Distance(radiusMeters, MetricsUnit.METERS)
);
// GEORADIUS:返回指定半径内的所有成员及其距离
GeoResults<GeoOperations.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(GEO_KEY, circle);
if (results == null) return Collections.emptyList();
return results.getContent().stream()
.filter(r -> {
// 检查司机是否在线(心跳未超时)
String key = "driver:" + r.getContent().getName() + ":available";
return Boolean.TRUE.equals(
redisTemplate.hasKey(key));
})
.map(r -> {
// 转换为完整的 Driver 对象
Long driverId = Long.parseLong(r.getContent().getName());
return driverService.findById(driverId);
})
.collect(Collectors.toList());
}
}
支付系统
金融级系统的金额处理
Uber 支付系统的设计原则:金额必须精确,服务可以降级但不能计费错误。
BigDecimal 而非 double:double 和 float 在浮点数计算中存在精度损失,比如 0.1 + 0.2 = 0.30000000000000004。涉及金钱时,这是不可接受的。Uber 所有金额计算使用 BigDecimal(或自定义的 Money 类型)。
// 支付服务:使用事务保证一致性
@Service
public class PaymentService {
@Autowired private PaymentRepository paymentRepository;
@Autowired private TripRepository tripRepository;
@Autowired private LedgerService ledgerService;
@Autowired private PaymentGateway paymentGateway;
@Transactional(isolation = Isolation.SERIALIZABLE)
public PaymentResult chargeRide(Trip trip) {
// 1. 幂等检查:防止重复扣款
if (paymentRepository.existsByTripId(trip.getId())) {
return PaymentResult.alreadyCharged(trip.getId());
}
// 2. 验证行程状态:只有完成的行程才能扣款
if (trip.getStatus() != TripStatus.COMPLETED) {
throw new IllegalStateException(
"Trip not completed: " + trip.getStatus());
}
// 3. 计算费用(使用 BigDecimal,精确到分)
Money fare = pricingService.calculateFare(trip);
// platformFee = fare * 25%(Uber 佣金)
Money platformFee = fare.multiply(PLATFORM_COMMISSION_RATE);
// driverPay = fare - platformFee(司机实收)
Money driverPay = fare.subtract(platformFee);
// 4. 创建支付记录(先插入,状态为 PENDING)
Payment payment = new Payment();
payment.setId(UUID.randomUUID().toString());
payment.setTripId(trip.getId());
payment.setPassengerId(trip.getPassengerId());
payment.setAmount(fare.getAmount()); // BigDecimal 类型
payment.setCurrency(fare.getCurrency()); // "USD", "CNY" 等
payment.setStatus(PaymentStatus.PENDING);
paymentRepository.save(payment);
try {
// 5. 调用支付网关(Stripe、Braintree 等)
PaymentGatewayResult gatewayResult =
paymentGateway.charge(trip.getPassenger().getPaymentMethod(), fare);
if (gatewayResult.isSuccess()) {
// 6. 支付成功:更新状态 + 写入交易流水
payment.setStatus(PaymentStatus.COMPLETED);
payment.setGatewayTransactionId(
gatewayResult.getTransactionId());
paymentRepository.save(payment);
// 7. 更新司机账户(异步,通过 Kafka 消息触发)
ledgerService.creditDriver(trip.getDriverId(), driverPay);
metrics.record("payment.success", 1);
return PaymentResult.success(payment);
} else {
// 8. 支付失败:记录原因,等待重试
payment.setStatus(PaymentStatus.FAILED);
payment.setFailureReason(gatewayResult.getErrorMessage());
payment.setRetryCount(payment.getRetryCount() + 1);
paymentRepository.save(payment);
metrics.record("payment.failed", 1);
return PaymentResult.failed(gatewayResult.getErrorMessage());
}
} catch (PaymentGatewayException e) {
// 9. 网关超时:标记为待重试
// 定期重试任务会处理这些待重试的支付
payment.setStatus(PaymentStatus.RETRY);
payment.setRetryCount(payment.getRetryRetry() + 1);
paymentRepository.save(payment);
metrics.record("payment.retry", 1);
return PaymentResult.retry();
}
}
}
架构启示
启示一:按需选择存储引擎
Uber 从 MySQL 一把梭,到 Schemaless + Cassandra + Redis + Druid 的混合架构,每一步都是因为碰到了单引擎的边界:
- MySQL 事务好,但写入吞吐不够 → 引入 Cassandra 存事件日志
- Cassandra 查询能力弱,但 Redis 读写极快 → 用 Redis 做热点数据和缓存
- 单机房 MySQL 有单点 → 跨机房多活
对普通项目的建议:不要迷信 NoSQL,MySQL 在 90% 的业务场景下依然是最佳选择。先用 MySQL 解决问题,等碰到具体的性能瓶颈,再考虑引入其他存储引擎。不要因为「大家都用 MongoDB」就去用 MongoDB。
启示二:低延迟系统的设计要点
Uber 调度系统的 100ms SLA 拆解了每个环节的预算。要做到这一点,有几个关键设计:
内存优先:热点数据(司机位置、用户信息、定价系数)全部放在 Redis,避免磁盘 I/O。
并行计算:多个司机的匹配分数计算是完全独立的,parallelStream 可以将计算分散到多个 CPU 核心,20ms 内完成数百个候选司机的评分。
简化链路:从用户按下按钮到派单成功,整个链路只有 6 个关键步骤。每多一步网络调用,就要多预留 20-50ms 的缓冲。
预计算和预加载:热门路线沿途的司机位置会提前预加载到内存,定价系数每小时刷新到 Redis。尽可能减少实时计算的比例。
启示三:金融级系统的金额处理
Uber 支付系统展示了几个关键原则:
始终使用 BigDecimal:任何涉及金钱的计算、存储、传输,必须用 BigDecimal,不能用 double/float。这是最容易犯的错误,也是后果最严重的错误。
支付和行程状态同步:支付必须和行程状态在同一事务内更新(或通过消息队列保证最终一致)。不能让用户看到「行程已完成但扣款失败」的矛盾状态。
幂等性设计:支付网关调用、数据库写入必须支持幂等(同一个操作重复执行结果相同)。网络超时后重试不会导致重复扣款。
最终一致性优先:分布式环境下,强一致性的代价太高(需要分布式锁或两阶段提交)。Uber 支付系统的策略是:写入操作尽量保证强一致,读操作允许短暂的不一致(最终一致),通过补偿机制修复不一致状态。
术语表
总结
Uber 是实时调度系统的典型案例,其技术演进始终围绕一个核心命题:如何在秒级时间内,把人和车精准匹配在一起?
演进脉络:
- 2010-2013:Node.js 单体 + PostgreSQL,快速验证商业模式
- 2013-2016:微服务拆分 + Schemaless + Cassandra + Kafka,解决扩展性和数据吞吐问题
- 2016-至今:混合存储架构 + Redis GEO 实时位置 + Flink 实时计算
核心技术亮点:
- 调度系统:100ms 内完成全链路决策,parallelStream 并行计算匹配分数
- 位置服务:Redis GEO 支撑每秒百万次位置查询,Geofence 精确圈选可用车辆
- 支付系统:
BigDecimal 精确计费,事务保证一致性,幂等设计防止重复扣款
- 动态定价:实时供需关系驱动价格调整,高峰期通过价格杠杆平衡供需
对普通项目的启发:
- 按业务数据特征选择存储引擎,不要一把梭
- 低延迟系统要专门优化,内存 > 磁盘,同步 > 异步(对于核心链路)
- 金融系统金额必须用
BigDecimal,幂等性是支付系统的生命线