经验分享:Grubhub是如何自己制造服务框架的?

19-01-16 banq
                   

本文帮助你了解一个组织或公司内部的服务框架是什么样的,相比Spring Boot生态系统有什么好处?

在SOA时期,Grubhub的工程师需要针对具体技术提供商和基础设施配置推出大量代码,每当您想要增加监控指标时,都需要客户端库调用Datadog。必须为每个新服务配置Splunk,以确保它包含适当的跟踪和实例信息。此外,如果我们希望让自己对这些集成的变化持开放态度,我们可能需要承担将100多项服务调整为新集成模式的成本。这种情况很容易失控,我们希望在影响我们的业务之前避免这种情况。

在一些技术公司,特别是早期的技术公司,技术团队并不急于围绕他们的代码强制执行许多标准,并且团队可以自由地选择如何与他们选择的技术集成。虽然他们可能会将他们的组织限制为几种语言、限制数据库和云提供商的选择。

虽然让团队或代码贡献者在技术上做出独立选择可能具有吸引力,但公司经常发现自己因此放任政策而收到惩罚。团队必须花更多时间编写样板文件以集成到其他服务和工具中。他们将花费更多时间来测试此代码并解决集成中边缘情况的问题。从一个代码库跳到另一个代码库的开发人员需要重新调整自己的位置,才能进行更改。最后,这可能成为一个devops噩梦,因为通常有更多的系统和语言来学习如何支持。

Grubhub想出了一个替代方案:我们开发了一个类似于Dropwizard的基于Java的服务框架。该框架允许我们开发同质化服务,自动挂钩我们在数据存储,消息传递,日志记录等方面支持的所有供应商集成。它使我们能够接触到最小的样板代码。在同质性方面,我们能够更好地利用我们工程师过去的经验,使他们更快地定向,同时更舒适地在我们的服务架构中的任何领域中发布新功能。

管理RPC和API方法

该框架抽象出基础技术,以便我们可以专注于功能开发。例如,我们使用JSON / HTTP执行RPC,但框架提供了一个Java包装器,它不需要知道底层的序列化格式。如果我想创建一个接受某个RPC的服务(我们将其命名为handlePing),我会编写一些如下所示的代码:

public interface MyService {
   @RpcMethod
   PingResponse handlePing(@RpcParam(“request”) PingRequest request);
}

通过几行设置,我的服务现在已准备好接受handlePingRPC -  PingRequest并且PingResponse是其他地方定义的POJO(普通的旧Java对象)。该框架负责将POJO序列化到JSON和从JSON反序列化,并处理任何特定于传输的逻辑。我只需要添加我们在框架中定义的注释 -  RpcMethod并且RpcParam - 框架负责其余部分。

调用RPC很简单:

myServiceClientInstance.handlePing(request);

因为序列化格式和传输不是由代码决定的,所以如果我愿意,我可以轻松地将它们切换出来。如果我这样做,开发我的服务的开发人员就不必精通新的序列化格式和传输的细节,因为框架掩盖了所有这些。他们必须熟悉的只是Java和我们定义的语义。只要框架以一致的方式与底层RPC解决方案集成,我就可以更少地担心RPC框架的复杂性,这些复杂性可能各不相同。

记录和指标

在直接使用RPC或API调用时,如果您没有使用像Grubhub这样的框架,您将经常被迫编写满足内部技术约束的库以及您用于指标的任何提供程序日志记录。这可能会导致很多头痛。例如,在记录时,您可能没有一致的方法来跟踪一组请求,因此在调试问题时您可能无法将相关日志绑定在一起。度量标准可能更加麻烦:当不同的开发人员围绕代码集成度量标准时,他们可能会对如何计算这些度量标准以及不同的命名方案提出不同的解释。这些指标很难找到或解释为结果。

Grubhub框架将这些RPC与我们的日志管理提供程序(Splunk)集成在一起,在每个RPC上生成有用的日志:

INFO [2018-02-28 17:23:16,998] RPC-SERVER.request:{“req”:{“jsonrpc”:“2.0”,“id”:719737,“caller”:“InstanceInfo(service = MYSERVICE,版本= 1.5.1820)“,”方法“:”handlePing“}},request-id = 106669e0-1cac-11e8-ae4d-f9210915970a,tracking-id = 598523ae-c958-4729-b15e-576965e32c1e

请注意,这些日志通过显示的请求ID和跟踪ID为调试目的提供服务跟踪。该框架为RPC请求和API请求的每个请求周期添加新的请求ID,以及用于将更长的事件生命周期组合在一起的跟踪ID。我是否需要跟踪一系列服务调用以进行调试,我可以使用此请求ID查询Splunk以拼凑此请求的历史记录。此外,RPC自动与Datadog集成以提供大量有用的统计信息:

  • rpc.MyService_handlePing.request_count
  • rpc.MyService_handlePing.response_time.min (also max, p50, p75, and p99)
  • rpc.client.MyService_handlePing.request_count
  • rpc.client.MyService_handlePing.response_time.min (also max, p50, p75, and p99)

我们的框架为REST API提供了类似的功能,加快了开发人员的工作效率。这些集成可以为开发人员实现无缝处理,而无需任何干预。作为一名开发人员,我很高兴有这个框架可供我使用,因为我自己在检测这些集成时经常遇到fencepost错误和其他小问题,这会分散对核心应用程序逻辑的影响。

服务间消息传递

消息传递可能会导致围绕消息系统编写样板,日志记录和指标的所有相同的设置问题,但有时甚至可能更麻烦。许多解决方案在暴露重试,超时等方面提供的内容各不相同。如果您的组织不小心,与某些消息传递提供程序的集成将成为特定于供应商并且难以转移。

在我们的服务框架中,我们以一种可以容纳许多不同消息传递解决方案的方式抽象出消息传递实现。以类似于RPC的方式,我可以像这样定义一个消息处理程序类:

public class MyMessageHandler implements MessageHandler<MyMessage> {
   public void handleMessage(@NonNull MyMessage message) {
   // …
   }
}

与RPC库的情况一样,语义允许我在不了解底层消息传递提供程序的情况下进行编码。在框架的代码库中,有几个支持消息传递提供程序的适配器(例如SQS),但很少有这些必须在使用该框架的服务的代码库中公开。我觉得有必要切换到不同的消息传递解决方案,我只需要与符合当前的API,而不是框架的新的集成在我们的新的集成服务。

工程友好的代码

除了讨论的功能之外,我们的框架还促进了诸如领导者选举,断路器等常见服务功能,使我们的服务与供应商无关并且易于适应。如果我们希望更改我们的度量提供程序或日志记录平台,则不需要在我们的服务中进行任何代码更改,并且不需要向我们的所有开发人员教授新的专业知识。

无需担心实现服务功能,样板文件几乎没有减少,开发人员大多只关注功能开发。即使对于不熟悉底层技术的人来说,代码也很简单易读。这加快了每个开发人员加入并为他或她尚未开展的服务做出贡献的能力。故障排除变得更加容易。

框架的一致性使我们的SRE团队的生活更加美好。它最大限度地减少了专门的部署,从而最大限度地降低了SRE团队的工作量 - 提升新服务几乎毫不费力。

显然,我们框架的主要限制是它将我们与Java / JVM世界联系在一起。就个人而言,我并不一定认为这是一个缺点,因为许多流行的技术都有一流的支持和强大的Java客户端库。但即使JVM不是您的风格,您仍然可以创建一个框架或Golang,Ruby,Python或任何适合您团队的偏好。无论您选择哪种语言,编写一个抽象实现细节和服务基础设施琐事的图书馆都可以帮助您更快地开始实施重要的事情。

最重要的是,结果不言而喻 - 该框架在过去三年中促进了100多项服务的开发和发布。这种生产高质量服务的效率可能是不可能的。作为工程师,我们希望以最少的时间利用最高质量的输出,我们的服务框架使我们能够实现这一目标。

(banq注:公司内部搞一套服务中间件框架好处显而易见,坏处也不容忽视,闭门造车会与Spring Boot等主流框架脱节,程序员因被所在公司技术锁定而抱怨甚至跳槽,认为没有学习到新技术,主要问题是技术发展方向无法把握,现在都是通过开源世界Github程序员自由投票发展路线,集全球聪明人的大脑,一个公司的力量如何与人类通天塔抗衡,最后中间件框架反而成为公司发展的瓶颈。)

                   

1