尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Uber微服务演进:不是设计出来的,是被增长逼出来的

Uber微服务演进:不是设计出来的,是被增长逼出来的 Uber 前 CTO 级技术负责人在公开分享里给过一个很反共识的总结Uber 的微服务不是设计出来的是被增长逼出来的。这句话不是谦虚也没有否定微服务它讲的是一个非常现实的工程过程。Uber 最早用一套单体系统就能跑通一个城市的实时派单但当业务扩展到全球几百个城市、多条产品线、上千名工程师同时开发的时候单体架构在代码维护、发布效率、团队协作和数据库扩展四个方向同时撞墙。于是团队开始一个一个地往外拆服务先拆最痛的模块再补基础设施最后形成了一套大规模微服务架构。对绝大多数开发者来说Uber 这个案例比任何微服务教程都有价值因为它回答了最实际的问题微服务到底什么时候该上、怎么上、上了以后要付什么代价、故障怎么查、面试怎么答。这篇文章会按这条线展开先还原 Uber 单体架构为什么一开始很快、后来为什么顶不住再拆解它被增长逼着走向微服务的真实路径然后把微服务化的前置条件、迁移步骤、通信治理、架构图、性能观测、高频面试题和常见故障做一次系统梳理。适合正在维护单体系统的后端工程师、准备微服务改造的技术团队以及备考微服务架构面试的开发者收藏阅读。1. 微服务演进核心看点速览在进入细节之前先把 Uber 微服务演进这件事的关键信息列成一个速览表方便你判断这篇文章讨论的到底是什么范围、和你关心的场景是否相关。维度关键信息演进起点单体应用核心是一个 Python 编写的实时派单系统演进动力城市数量、业务线、团队规模同步扩张单体先顶不住服务规模高峰时期 Uber 内部运行着 2000 到 2200 个微服务这个数字在公开技术分享中被广泛引用技术栈Go、Java、Python、Node.js 多语言并存按服务特性选择语言关键基础设施服务发现、RPC 框架、配置中心、分布式追踪、消息队列、工作流引擎主要代价分布式事务、链路排障、依赖治理、资源开销、版本兼容对普通团队的启示先单体后拆分是常态微服务不是架构先进而是增长成本的交换适合读者单体系统维护者、微服务改造团队、分布式系统学习者、后端面试备考者这个表里最值得记住的一件事是Uber 的核心系统最初是单体不是微服务。微服务是它在快速增长过程中逐步演化出来的结果而不是一开始就画好的蓝图。2. 单体没做错什么Uber 最初为什么能跑得飞快很多文章讲到 Uber 的微服务习惯从单体很烂讲起。但 Uber 技术团队自己的回顾口径恰恰相反最早的单体架构在对应的发展阶段里是高效、合理的选择。Uber 起家时核心业务就是一件事把乘客和司机撮合起来。整个系统的灵魂是一套实时派单模块它维护着城市里所有司机的位置、状态、订单和行程状态机。派单逻辑本质上就是状态流转空闲、接单、去接乘客、行程中、结束、支付。用一个 Python 进程把地图、定位、派单、计费、通知全部串在一起在单一城市、低并发、小团队的情况下开发和调试效率非常高。工程师改一行代码跑一遍本地测试部署上去就完事几乎没有跨服务联调成本。单体在这个阶段的优势是实打实的。业务逻辑在同一个进程内事务天然一致不需要考虑分布式事务调试可以直接看堆栈不需要链路追踪部署就是一个进程运维极其简单团队规模小代码冲突可控发布节奏快。也就是说Uber 并不是一开始就判断微服务更好而是先老老实实把单体用到了极限。这给我们的第一个教训是不要因为微服务流行就否定单体。单体架构本身不是反模式持续膨胀且无人治理的单体才是。3. 增长是怎么一步步逼垮单体的Uber 的转折点是城市数从几个变成几十个再变成上百个、几百个。这里的难点不是并发量爬升那么简单而是业务规则开始不受控制地膨胀。每个城市都有不同的市场规则和产品形态。有的城市允许路边招手叫车有的必须以预约为核心不同城市对车型、价格、支付方式、司机资质的要求都不一样同一个城市里还同时存在 UberX、UberBLACK、UberPool 等多条产品线。为了在单体里支撑这些差异工程团队只能不断往代码里加 if/else、加配置开关、加特殊逻辑。派单模块逐渐变成了公开分享里提到过的上帝对象一个对象里装着所有行程、所有司机、所有订单的全局状态。与此同时数据库的压力也开始显现。Uber 早期把城市数据放在同一个 MySQL 集群里数据量上来之后单库连接数、主从延迟、慢查询、分库分表全部成为问题。更麻烦的是当几百名工程师在同一个代码仓库里提交代码一次发布就要把所有人的改动一起带上线任何一个小模块出问题整个派单系统都可能不可用。发布窗口越来越长回滚越来越难线上故障的爆炸半径越来越大。这一段非常关键微服务拆分表面上是在解决代码结构问题实际上解决的是三个更深的问题——规则隔离、团队协作、故障隔离。代码乱只是症状规则互相干扰、团队互相阻塞、故障互相传染才是真正的病因。4. 微服务不是设计出来的是问题逼出来的Uber 的拆分顺序在公开分享里有比较一致的描述不是有人先画了一张理想架构图然后按图施工而是每个团队在具体业务压力下把最影响迭代速度、最容易出问题的模块一个个独立出去。派单、计费、用户、司机、支付、通知、地图服务都是先有独立部署的诉求才逐步发展成独立服务。每个独立服务的诞生都对应着一个具体痛点。派单模块要按城市水平扩展需要独立部署、独立扩缩容计费逻辑经常要按城市调价格不能每次改价都触发全系统发布支付需要对接多个第三方渠道必须保证高可用和幂等不适合和派单耦合在一起地图和路径规划计算量大需要单独的服务和资源池。这种从痛点出发、按业务域拆分的方式才是微服务正确落地的姿势。反过来讲微服务最容易失败的方式是架构师先画图、定规范、切边界然后强行推动一刀切拆分。这种设计出来的微服务往往会得到一堆无人维护、调用关系混乱的分布式单体。这里要区分一个概念Uber 拆出来的东西是能独立演进、独立部署、独立故障恢复的服务而不是把单体里的一个类、一个模块直接挂上 HTTP 接口就完事。如果只是换了个调用方式数据库还共用一个业务状态还互相依赖那跟分布式单体没有区别。很多团队拆了微服务之后反而更慢原因就在这里服务边界没有按业务域切数据没有真正隔离调用链变长了复杂度一点没减少。5. 微服务适用场景、前置条件与使用边界不是所有团队都该学 Uber。微服务化有一套明确的前置条件缺了任何一个拆分方案都很容易翻车。第一是组织条件。康威定律在这里是硬约束微服务的边界会很快对齐到团队边界。如果团队还是一个大后端组十个人互相改同一个仓库拆出来的服务反而会增加沟通成本。比较健康的节奏是服务跟着团队走——一个服务对应一个长期负责的小团队团队对服务的可用性、性能、迭代全权负责。第二是工程基础设施。微服务上线后代码能独立部署只是开始真正消耗精力的是服务发现、配置中心、网关、日志、监控、链路追踪、CI/CD 流水线。这些设施在单体阶段可以没有在微服务阶段几乎缺一不可。Uber 当年拆得快本质上也付出了大量自研基础设施的代价包括服务发现框架、RPC 框架、分布式追踪系统和工作流引擎。第三是数据能力。微服务化的最大难点从来不是接口而是数据。一个服务对应一个数据域用户数据、订单数据、资金数据要逐步从共享库中拆出去。拆分期间要考虑数据一致性、历史数据迁移、双写、回滚预案这些都需要专门的数据工程能力。如果出现下面这些信号说明现在不应该拆团队规模不到两位数单体开发效率并没有遇到瓶颈业务模型还没验证清楚需求还在快速变化没有配套的监控、日志、CI/CD 体系只是觉得微服务更高级或者想写进简历。这种情况下更务实的选择是先维护好一个结构清晰的单体或者采用模块化单体等痛点出现再拆。使用边界同样要讲清楚。微服务擅长解决的问题是大团队并行开发、多业务线规则隔离、关键模块独立扩缩容、故障局部化。它不擅长解决的是小团队快速验证业务、强一致事务密集场景、数据边界天然耦合的场景。涉及用户手机号、定位、支付账号等敏感数据时拆服务还必须做数据分类和权限隔离敏感字段加密存储、脱敏展示、访问审计跨服务传递遵循最小必要原则。Uber 自身在数据治理上也有过公开的教训这一点不能因为它在技术上成功就忽略。6. 从单体到微服务的迁移路径与落地策略结合 Uber 的案例大多数团队的微服务迁移都适合采用绞杀者模式新服务在老的单体旁边长出来流量逐步切换老单体逐步废弃而不是一次性重写。一个通用迁移步骤是这样的选一个边界清晰、变化频繁、对稳定性和扩展性要求高的业务域作为第一个拆分对象比如计费、支付、通知先把单体内部对该模块的调用抽象成接口出口定义清楚把这个模块抽成独立服务对外提供 HTTP 或 gRPC 接口采用双写或者读流量灰度逐步把调用切到新服务确认稳定后把老代码从单体中删除。第一步最重要的工作是定义接口契约。接口契约相当于两个团队之间的合同一旦定下来后续的字段新增、版本升级都围绕契约展开。一个简化版的 OpenAPI 契约示例openapi: 3.0.0 info: title: Billing Service version: v1 paths: /api/v1/bills: post: summary: 创建账单 requestBody: required: true content: application/json: schema: type: object required: [trip_id, amount, currency] properties: trip_id: type: string amount: type: number currency: type: string discount_code: type: string responses: 200: description: 创建成功返回账单ID content: application/json: schema: type: object properties: bill_id: type: string status: type: string这只是契约模板实际项目中需要根据业务场景补充鉴权、幂等键、错误码和限流字段。契约定义好后服务内部怎么实现、用什么语言都可以独立决策这正是微服务技术异构价值的来源。服务独立之后单体里的内部调用就要替换成网络调用。以 Python 单体为例拆分前可能是这样的# 拆分前单体内部直接调用 from billing import create_bill bill create_bill(trip, amount)拆分后变成调用独立计费服务# 拆分后调用独立计费服务 import requests def create_bill(trip, amount, idempotency_key): resp requests.post( http://billing-service/api/v1/bills, json{trip_id: trip.id, amount: amount}, headers{X-Idempotency-Key: idempotency_key}, timeout2 ) resp.raise_for_status() return resp.json()[bill_id]注意这里两个细节一是超时必须有上限二是调用必须带幂等键。在单体时代一次方法调用失败可以立刻重试在分布式环境下一次请求可能已经到达对端但对端处理超时不带幂等键的重试会导致重复创建账单、重复扣款。这种先抽象调用、再抽服务、再切流量的顺序比一次性重写安全得多也是 Uber 这类案例给到的最通用经验。7. 服务间通信与 API 治理微服务从两个服务开始服务间通信就会成为日常问题。通信方式的选择原则可以简单归纳为强一致、实时性要求高的场景用同步调用比如查询订单详情允许最终一致、对实时性不敏感的场景用异步消息比如发送通知、更新搜索索引。同步调用必须配三件套超时、重试带幂等、熔断。异步消费必须配消息幂等和顺序保障。Uber 这种规模下的真实教训是任何一个依赖服务的抖动如果不做保护都会顺着调用链一路传导最终表现为全线超时甚至服务雪崩。API 网关在微服务架构里承担统一出入口的角色路由转发、身份认证、限流、灰度、流量审计都在这一层处理。一个常见的网关路由配置示例spring: cloud: gateway: routes: - id: billing-service uri: lb://billing-service predicates: - Path/api/v1/bills/** - id: user-service uri: lb://user-service predicates: - Path/api/v1/users/**这段配置的意思是/api/v1/bills/**的请求转发到注册中心里名为 billing-service 的服务/api/v1/users/**的请求转发到 user-service。lb://前缀表示走客户端负载均衡从注册中心动态获取实例列表。服务发现是微服务基础设施中最基础的一环。每个服务启动时把自己注册到注册中心调用方从注册中心拿实例列表再结合负载均衡策略发起调用。国内团队常用的开源方案是 Nacos 或 Consul如果技术栈是 Spring Cloud Alibaba通常就是 Nacos OpenFeign Sentinel 的组合。服务发现配置一般是这样的模板spring: application: name: billing-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: dev在真实项目中注册中心地址、命名空间、鉴权信息都需要按环境替换。配置中心单独管理数据库连接串、开关、限流阈值这类配置不要写死在代码里。更稳妥的判断是微服务的通信问题本质上是把单体内部的函数调用升级成了服务调用。原来由编译器保证的东西现在要靠超时、重试、熔断、幂等、注册中心、网关来保证。这一层不做扎实服务拆得越多线上越不稳定。8. 微服务架构图与典型参考架构很多人在搜索微服务架构图的时候其实是想知道一套标准的微服务架构从上到下应该有哪些层、每个层放什么组件。结合 Uber 案例和国内主流开源方案下面这个分层结构可以作为参考客户端 / 外部系统 ↓ 接入层API 网关鉴权、限流、路由
返回列表