原文: Rewriting Uber Engineering: The Opportunities Microservices Provide
作者: Emily Reinhold,Uber的工程师
翻译:孙薇
责编:钱曙光,关注架构和算法领域
几个月前,我们讨论到 Uber 决定将原有的整体单一式代码库更换成模块化、更具灵活性的微服务架构。从那时起,Uber 有许多工程师投入了数千小时,改造拓展 Uber 微服务的生态环境。新的架构使用了多种语言以及很多不同的框架结构,鉴于重构任务非常庞大,我们也利用此机会在 Uber 使用了一套新的微服务构建技术。借助适合 SOA 迁移的技术堆栈及标准,我们改进了在 Uber 开发服务的方式。 开始一项新服务在一家快速成长的工程类公司,想要追踪所有进行中的任务是非常困难的,需要有相应的方法,才能避免各团队工作重复。在 Uber,我们通过要求新服务的编写者提交 RFC(Request for Comments) 来解决这个问题。RFC 是一份关于新服务的详细议案,其中需要列出新服务的目的、架构、依赖与其他执行细节,让 Uber 工程部门的其他员工一并参与讨论。 提交 RFC 有两个目的: 为即将开发的服务征集反馈,以提高服务质量; 避免工作重复,或者提供合作机会。
其他一些熟悉该领域的工程师会审阅这份服务设计稿,一旦将反馈融入到服务议案中,我们就可以开始快乐地投入新服务的构建了。 执行一项新服务我们的货币与汇率服务「Tincup」正是微服务在 Uber 实现的良好案例。Tincup 是最新货币与汇率数据的接口,负责两个主要端点任务: 无论你在什么地方,都可以点击按钮随时叫车,Tincup会为你确保自动使用当前所在国的货币单位支付车费。 通过新技术来引导微服务构建 Tincup 需要重构所有与货币和汇率相关的逻辑,这正好为我们提供了机会,重新评估 Uber 一些很久之前所做的设计决策。我们使用了一些新的框架、协议和约定来执行 Tincup。 MVCS 首先,我们搞定了货币与汇率相关的整体代码结构。近几年,我们修改了 Uber 很多数据集的持久层(点击查看样例),由于所有变化都是长期而繁琐的,从这个过程中我们学到:如果可能的话,最好将持久层规范从应用逻辑中分离出来。这样就形成了我们所谓的 MVCS 应用开发方法:扩大常见的 MVC 方法,将应用逻辑所在的服务层也包括在内。通过隔离服务层中的应用逻辑以及应用的其他部分,就能在无需重构业务逻辑的情况下,修改或替换持久层的内容——只需改动直接与存储/读取相关的那部分代码。 UDR 其次,我们考虑了货币与汇率的持久层。在使用 Tincup 之前,这些数据存储在PostgreSQL 关系数据库中,以增量整数 ID 为标记。然而这种数据存储方法无法支持 Uber 数据中心在全球范围内执行数据复制,无法匹配我们的 all-active(所有数据中心同时提供行程服务)工作架构。由于访问货币和汇率时,需要涉及所有的数据中心,我们换掉了持久层,用 UDR(Uber 的全球复制可扩展数据库)来代替。 预测微服务成长中的问题在决定对货币及汇率作出设计改动后,我们解决了随着工程生态环境中的微服务数量增长而自然出现的新问题。 Tornado 网络吞吐堵塞是非常严重的问题,可能会导致 uWSGI 的 worker 无事可做,如果类似 Tincup 的所有服务请求都是同步的,某个服务出现问题会导致连锁反应,并影响所有调用者。我们决定采用 Tornado,这是一个基于 event-loop 的 Python 异步框架,目的是为了防止出现阻塞。由于我们从 Flask 整体单一式数据库中剥离了大量的代码,选择使大多现有应用逻辑保持不变的异步框架让风险降到最低,对我们来说非常重要。Tornado 符合这一需求,因为它允许同步查看代码,但不会堵塞输入/输出。另外还有个替代方案:为了解决上述的吞吐问题,很多服务提供者都在使用新语言 Go。 TChannel 曾经对单独一个API的调用,现在可能扇出成大量对微服务的调用。为了促进在大型生态系统中发现其他服务,并找出故障点,Uber 的微服务在 Hyperbahn 上使用了开源的 TChannel ,这是一个 RPC 内部开发的网络多路复用和框架协议。TChannel 为客户端和服务器提供协议,Hyperbahn 的智能路由网将这两者连接起来。这样一来,微服务中产生的几个核心问题都得以解决: 服务的发现:所有生产者和使用者都注册到了路由网上,使用者可以通过名称来访问生产者,而无需知道主机或端口名。 容错问题:路由网络追踪类似故障率和SLA违反之类的指标,它可以检测到出现问题的主机,将其从可用的主机池中移除出去。 速率限制与断路器:这些功能可以确保在请求出错的情况下,或者从客户端发回的响应速度过慢的时候,不会造成级联故障。
Thrift 由于所调用服务的数量增长迅猛,很有必要为每个调用维护一个定义良好的接口。由于想用 IDL 来管理这个接口,最终我们决定了使用 Thrift。Thrift 强制服务所有者发布严格的接口定义,从而简化了服务的合成过程。不遵守接口定义的调用在 Thrift 层面上就被拒绝了。对接口公开声明的策略也强调了向后兼容的重要性,因为某个服务Thrift 接口的多个版本可能会在指定时间内同时使用。服务编写者绝对不能对接口定义作出重大修改,只能添加一些影响不大的内容,直到消费者不再使用为止。 为生产环境的服务联合做好准备最终,在 Tincup 的实现阶段几近完成时,我们使用了一些有用的工具为生产环境做准备: Hailstorm 首先,我们知道 Uber 的流量在每天、每周以及每年的时间中都是变化的,在我们预测的时间——比如新年年夜及万圣节时会出现流量高峰,因此我们必须在发布前确保这些服务能够处理这些增额负载。按照规定,每当在 Uber 发布新服务的时候,我们会使用内部构建的 Hailstorm 服务来加载并测试 Tincup 的端点,确定缺陷以及在压力下出现断点的地方。 uContainer 下一步我们考虑到了 Uber 工程部的另一个主要目标:更高效地使用硬件。由于 Tincup 是很轻量级的服务,可以很容易地与其他微服务共享机器,分享就是关爱不是吗?当然也并非总是如此:我们还想确保每项服务都能独立运行,不会影响在同一台机器上所运行的其他服务。为了避免这种问题出现,我们使用 uContainer(Uber 的 Docker)来执行资源隔离与限制的任务。uContainer “人”如其名,借助 Linux 的容器性能以及 Docker 来容器化 Uber 的服务。它会将某个服务打包到某个隔离的环境中,以确保无论在同一台主机上还有什么其他进程运行,这项服务都能持续运行。uContainer 在 Docker 的基础上添加了:1. 更灵活的构建功能;2. Docker 容器可见度更高的工具。 uDestroy 最后,为了迎接在生产环境中不可避免会出现的宕机及网络连接的问题,我们使用了一个内部工具uDestroy,以测试在我们控制的混乱下服务的表现——通过模拟宕机的情况,来观察系统的弹性。在定期、有目的地破坏系统的过程中,我们可以发现漏洞并不断努力提高系统的耐久性。 实现完成后的心得通过构建 Tincup 来扩展 SOA ,我们学到了一些经验: 用户迁移是一项长期、缓慢的过程,因此尽可能将其简单化。提供代码实例,预测迁移完成的时间。 我们了解到:技术堆栈最好存在于小的服务中,Tincup 的应用逻辑非常简单,因此开发者得以集中精力来研究新的技术堆栈,而不需要将精力浪费在业务逻辑的迁移细节上。 一开始先开发通用单元并执行集成测试,如果是在开发环境中,使用代码来debug要容易得多(压力也更小)。 尽可能提早、频繁地执行负载测试,没有什么能比在花了数周或数月时间执行实现,却发现系统无法应付峰值流量更糟糕了。
Uber的微服务Uber 迁移到 SOA 的过程为许多服务的拥有者,甚至是行业经验贫乏的人展示了机会。开发并拥有一项服务是很大的责任,不过 Uber 开放性的知识共享文化使得选择一套新技术以及拥有代码库都成为了让人收获颇丰的珍贵体验。
|