几个月前,我们讨论过Uber关于放弃它单一整体的代码库,而支持一种模块化的灵活的微服务结构。自那时候以来,我们已经花费了数千个小时,使用多种语言和多种不同的框架来扩展Uber的微服务(数以百计)生态系统。这种持续的重构是一个巨大的挑战,因此,我们趁机在Uber的微服务中采用了一套新的技术。通过一个技术栈和一套符合SOA迁移的标准,我们已经大大简化了整个服务的开发。
开始一个新服务
在一个快速发展工程组织中,我们可能很难跟踪所有正在进行的工作。这种增长需要一个流程来防止不同团队之间的重复工作。在Uber,我们通过要求新服务的作者提交一份请求注解(RFC)来解决这个问题,RFC是一个新服务的高层次提案,概述了新服务的目的,架构,依赖,以及其他实现细节,以便其他Uber工程师进行讨论。RFC的目的有两个:
1)征求意见,以便提高开发的服务质量,
2)避免重复工作和挖掘合作机会。
多个熟悉该领域的工程师会审核服务的设计。一旦反馈被纳入服务提案,就可以开始构建有趣的服务。
实现一个新服务
Tincup,我们的货币和汇率服务,这是一个很好的关于在Uber如何实现微服务的例子。Tincup是一个最新货币和汇率数据的接口。它有两个主要的服务:第一个是获取货币对象,第二个是获取给定货币的当前汇率(兑美元)。这两项功能是必须的,因为Uber是全球性的业务,汇率变动频繁,我们支持近60种货币的便捷交易。
无论你在什么地方,你都可以通过点击侧边栏的图标来请求搭车。Tincup能保证你支付的是你所在国家的货币。
通过新技术启动微服务
在构建Tincup时,需要重写所有与货币和汇率相关的逻辑,这就提供了一个好的机会来重新评估很久以前在Uber的设计方案。我们使用了很多新的框架,协议,约定来实现Tincup。
首先,我们讨论了货币和汇率相关代码的整体结构。
在Uber,近几年我们已经多次修改了许多数据集的持久层(像这一次)。每次的改变都是漫长而繁琐的。我们在这个过程中得到了很多经验教训,如果有可能的话,最好是将持久层与具体的应用逻辑进行分离。这就产生了一种应用开发的模式,我们称之为MVCS,它是对我们常用的MVC模式的扩展。它包含了一个服务层来实现应用的业务逻辑,通过将包含业务逻辑的服务层与应用的其它部分进行隔离,持久层就可以在不需要重构业务逻辑的前提下进行升级和替换(只有那些直接处理存储和读取数据的代码需要改变)。
其次,我们认真考虑了货币和汇率的持久层。在Tincup之前,数据存在一个ID递增的关系型数据库——PostgreSQL中。
然而,这种数据的存储方式不允许Uber的所有数据中心进行全球化的数据复制,因此,这一模式不符合我们现在全面发展(所有的数据中心同时提供服务)的架构。由于全球的数据中心都需要访问货币和汇率,我们在持久层使用UDR(Uber的全球复制可扩展数据存储)来实现数据交换。
微服务中心成长的预期顾虑
在决定改变货币和汇率的设计之后,我们解决了很多工程生态系统中由微服务数量不断增加而自然而然会出现的问题。
网络I/O的阻塞是一个严重的问题,可能会导致uWSGI工作线程的饥饿。如果所有的请求服务跟Tincup一样都是同步的,一个服务恶化的风险是可能会引起连锁反应,从而影响所有的调用者。我们决定采用Tornado(一个Python的基于循环事件的异步框架)来防止阻塞。由于我们已经将大量的代码从整体的基础代码库分离出来,我们便通过选择一个异步框架来保持现有应用逻辑保持与原来逻辑相同,这一点对保证风险最小化是非常重要的。Tornado满足我们这个要求,因为它允许我们使用一种非阻塞式的I/O来同步查找代码(此外,为了解决上述的I/O问题,许多服务的提供者使用一种新的Go语言)。
曾经单个API的调用现在可能会波及到许多的微服务调用。为了便于在一个大的生态系统中发现其它服务和识别失败的点,Uber微服务在Hyperbahn(一个针对RPC在内部开发的网络多路复用帧协议)上使用开源的TChannel,TChannel为客户端和服务端提供协议,这两者通过Hyperbahn的智能路由进行连接。它解决了微服务世界中的几个核心的问题:
1
服务发现。所有的生产者和消费者通过路由网进行自我注册。消费者通过生产者的名称进行访问,而不需要知道生产者的服务器或端口。
2
容错。路由网可以追踪故障率和SLA( 服务等级协议)违例。它可以检测到不健康的主机,并随后将其从可用的主机池中进行删除。
3
速率限制和电路断开。这些特性确保来自客户端的错误请求和响应慢不会引起级联故障。
由于微服务调用数量的快速增长,有必要为每个调用维护一个定义良好的接口。我们很清楚需要使用IDL(接口定义语言)来进行维护,我们最终决定使用Thrift。Thrift强制服务的提供者发布严格的接口定义,这就简化了整合服务的过程。Thrift拒绝不遵守接口定义的调用,而不是将其泄露到更深层次的代码而引起故障。这种公开声明接口的策略,强调向后兼容的重要性,因为多个版本的Thrift服务接口可能在任意时间中同时使用。服务的作者不能对接口做重大修改,只能对定义好的接口添加一些非重大改变的内容,直到所有消费者都不再使用。
让Tincup成为行业佼佼者的准备:产品
最后,在Tincup的实现阶段将要完成时,我们是用一些有用的工具来准备生产环境的Tincup:
首先,我们知道Uber的流行每天、每周、每年都是变化的。我们可以在预期的时间内看到巨大的高峰期,如除夕和万圣节,所以我们必须在发布之前确保这些服务能够处理这些增加的负载。按要求,在Uber发布一个新服务时,我们使用我们内建的Hailstorm来进行负载测试,确定Tincup功能点的缺陷和服务的中断点。
然后,我们考虑Uber工程的另一个主要目标:更加有效的使用硬件。因为Tincup是一个相对轻量级的服务,它很容易与其他微服务共享机器。服务就是关爱,对吧?当然,事情也不总是如此:我们还是想确保每个服务独立运行,而不影响在同一台机器上的其他服务。为了防止这个问题,我们使用uContainer(Uber的Docker)来部署Tincup,以实现资源的隔离和限制。
正如其名称所暗示的,uContainer使用Linux的容器功能和Docker来实现Uber服务的容器化。它将一个服务包装在一个隔离的环境中,以保证即使有其他服务在同一台主机上运行,该服务也能够始终如一的运行。uContainer扩展了Docker的功能,在Docker容器中增加了:
1)更灵活的构建功能
2)更多可视化工具。
最后,为应对生产环境中不可避免的宕机和网络连接问题所做的准备,我们使用一个称之为uDestroy的内部工具来释放服务控制混乱问题。通过模拟宕机,我们可以认识的系统的恢复能力。由于我们在其发展的过程中定期的、有目的破坏系统,我们可以发现系统的漏洞,不断的提高系统耐用性。
完成后的经验教训
通过构建Tincup来扩展 SOA的过程中,我们学到几条经验教训:
- 用户的的迁移是一个长期的、缓慢的过程,所以尽可能的做到操作简单。提供代码示例。预测迁移完成所需的时间。
- 技术栈的学习最好是建立在一个小的服务上。Tincup的应用逻辑非常简单,这样开发者可以集中精力去学习新的技术栈,而不是浪费时间在业务逻辑的迁移上。
- 开发的初始阶段投入大量的时间在单元测试和集成测试,最终会收到回报的。在开发环境中执行代码调试会简单很多(压力也更小)。
- 尽可能早的、频繁的去做负载测试。没有什么事情比发现你花费了几周或几个月实现的系统无法处理流量峰值更糟的了。
Uber的微服务
Uber的SOA迁移为许多服务提供者,甚至是那些行业经验有限的人展示了机会。拥有一个服务是一项很大的责任,但是Uber开放的、知识共享的文化使得选择一套新的技术和拥有一个代码库成为了一种有回报、有价值的体验。
来源:解放号杰微刊
译者:刘晓鹏