WG Contract API:服务动物园



随着软件系统中组件数量的增加,参与其开发的人数通常也会增加。 因此,为了保持开发的速度和易于维护,API的组织方法应成为特别注意的主题。

如果您想更深入地了解Wargaming平台团队如何应对由一百多个Web服务相互交互的系统的复杂性,那么欢迎您。

大家好! 我叫Valentine,是Wargaming平台的工程师。 对于那些不知道平台是什么及其功能的人,我将在此处留下指向我的一位同事的最新出版物的链接 -max_posedon

此刻,我已经在公司工作了五年以上,部分地发现了《战车世界》活跃发展的时期。 为了揭示本文中提出的问题,我需要首先简要介绍一下Wargaming平台的历史。

一点历史


事实证明,“坦克”的日益流行就像雪崩一样,在这种情况下,游戏的基础设施开始迅速发展。 结果,该游戏迅速增加了各种Web服务,而在我加入团队时,他们的得分已经提高到数十分(顺便说一下,现在有100多个平台组件可以使公司受益)。

随着时间的流逝,新游戏问世了,并且了解Web服务之间的复杂集成不再是一件容易的事。 只有其他Wargaming办公室的团队加入了该平台的开发后,情况才进一步恶化。 发展已经分散,所有后果以距离,时区和语言障碍的形式出现。 还有更多服务。 找到一个了解平台整体工作原理的人并非易事。 通常必须从不同来源收集信息。

各种Web服务的界面在风格上可能会有很大差异,这使得与平台的集成过程更加困难。 直接的组件间依赖性通过使平台内功能的分解复杂化而降低了开发灵活性。 更糟糕的是,游戏-平台的客户端-非常了解我们的拓扑,因为它们必须直接与每个平台服务集成。 这给了他们使用水平连接的机会,可以游说直接在与它们集成的组件中实施某些改进。 这导致在平台的各个组件中出现重复的功能,以及无法将现有功能扩展到其他游戏。 很明显,继续围绕每个特定游戏构建平台是开发的死胡同。 我们需要技术和组织上的变革,因此,我们可以控制快速增长的系统日益增长的复杂性,并使所有平台功能都适合任何游戏使用。

在此,我想结束历史考察,最后谈一谈我们的一种技术解决方案,它有助于控制由于不断增长的服务数量而导致的复杂性。 此外,它降低了开发新功能的成本,并大大简化了与平台的集成。

符合合同API


在平台内部,我们将其称为Contract API。 它的核心是一个集成框架,该集成框架由针对我们堆栈中的每种技术(Erlang / Elixir,Java / Scala,Python)的一组文档和客户端库表示。 首先,正在开发它,以简化平台组件之间的集成。 第二,帮助我们解决以下一些问题:

  • 程序界面的风格差异
  • 直接的组件间依赖关系的存在
  • 保持文档最新
  • 内省和调试端到端功能

所以,首先是第一件事。

软件界面的风格差异


我认为,这个问题是由以下几个因素共同导致的:

  • 缺少API外观的严格标准。 推荐集通常没有理想的效果,API仍然不同。 特别是如果开发是由公司不同部门的团队进行的。 每个团队都有自己的习惯和做法。 总体而言,此类API通常看起来不像是整体的一部分。
  • 缺少单个目录,其中没有特定于业务的实体的名称和格式。 通常,您不能从一个API的结果中获取实体并将其传递给另一服务的API。 这需要进行转换。
  • 缺少针对API的强制性集中审核系统。 总是有最后期限,没有时间收集更新,而且没有对API进行更改,实际上,事实证明,该API已经过一半测试。

设计Contract API时,我们要做的第一件事就是说,从现在开始,API属于平台,而不是单个组件。 这导致以下事实:新功能的开发始于对集中式存储API的拉取请求。 当前,我们使用GIT存储库作为存储。 为了方便起见,我们将整个API划分为单独的业务功能,正式化了该功能的结构,并将其称为“合同”。

从那时起,应以特殊格式描述我们合同API中的每个新业务功能,并通过强制要求进行拉取请求。 没有其他方法可以将新的API发布到Contract API。 在同一存储库中,我们定义了业务特定实体的目录,并建议合同开发人员重用它们,而不是描述这些实体本身。

因此,尽管实际上它是使用各种技术堆栈在许多平台组件上实现的,但我们得到了一个概念上集成的平台API,看起来像是一个产品。

直接的组件间依赖关系的存在


我们这个问题的体现是,每个平台组件都必须知道谁专门为其所需的功能提供服务。

甚至不难于保持该目录的最新性,而是直接依赖性极大地复杂化了业务功能从一个平台组件到另一个平台组件的迁移。 当我们开始将整料分解成较小的组件时,问题特别严重。 事实证明,从业务的角度说服客户用任何功能替换工作集成,从业务的角度来看,而从技术的角度来看,则不是一个琐碎的管理任务。 客户根本看不出这一点,因为一切对他来说都很好。 结果,编写了令人难以置信的向后兼容性层,这仅使平台的支持复杂化,并且对服务质量产生了不良影响。 并且由于我们已经要标准化平台API,因此有必要同时解决此问题。

我们面临着几种选择。 其中,我们特别仔细考虑过:

  • 在每个组件上实现服务发现协议。
  • 使用中介程序将客户端请求重定向到正确的平台组件。
  • 使用消息代理作为消息传递总线。

经过一些思考和实验,尽管消息经纪人认为我们是潜在的单点故障并增加了平台操作的开销,但还是选择了消息代理。 当时该平台已经具备与RabbitMQ协作的专业知识,这在选择过程中发挥了重要作用。 经纪人本身可以很好地扩展,并且具有用于确保容错能力的内置机制。 作为奖励,我们有机会“在幕后”实施事件驱动的体系结构事件驱动的体系结构EDA )。 随后,与点对点交互相比,跨服务交互的可能性有了更大的发展。

因此,从拓扑上看,该平台开始从具有随机连通性的图形变成星形。 平台组件颠倒了它们的依赖关系,并有机会通过在中央存储库中注册的合同来专门进行交互,而无需知道谁专门实现了特定的合同。 换句话说,平台内的所有组件都可以使用单个集成点进行交互,从而极大地缩短了开发人员的生命。

保持文档最新


几乎总是遇到与缺少文档或文档失去相关性相关的问题。 而且,发展的步伐越高,它越经常表现出来。 事实上,要在一个分散的跨国团队中以单一位置和格式收集所有API规范,以为一百多个服务提供服务是一项艰巨的任务。

在开发Contract API时,我们也为自己设定了解决此问题的目标。 而我们做到了。 严格的合同说明格式使我们能够建立一个流程,据此,在新合同出现后立即开始自动组装文件。 这使我们确信我们的API文档始终是最新的。 此过程是完全自动化的,不需要开发或管理。

内省和调试端到端功能


很自然地,当我们将整体拆分成较小的组件时,调试端到端功能就开始出现困难。 如果业务功能的服务分布在多个平台组件上,那么通常是为了定位和调试问题,则必须从每个组件中寻找代表。 鉴于与我们的一些同事之间的11小时时差,有时很难实现。

随着Contract API的问世,特别是由于底层的消息代理,我们能够接收与执行业务功能有关的消息副本,而不会对交互参与者产生副作用。 为此,甚至不必知道平台的哪些组件负责处理特定合同。 定位问题后,我们可以从问题消息的元数据中获取损坏组件的标识符。

我们在Contract API的基础上还开发了什么


除了其主要目的和解决上述问题之外,Contract API还使我们能够实现许多有用的服务。

网关访问平台功能


通过合同形式的API标准化,我们可以通过HTTP开发对平台功能的单个访问点。 而且,随着新功能(合同)的出现,我们不需要以任何方式修改此访问点。 它与所有未来合同都具有向前兼容性。 这使您可以使用通常的HTTP接口将平台作为单个产品使用。

大众运营服务


任何合同都可以作为批量操作的一部分启动,并能够跟踪其状态,然后接收有关此操作结果的报告。 与上一个服务一样,该服务可以预先与所有将来的合同兼容。

统一平台错误处理


Contract API协议也将错误标准化。 这使我们能够实现一个错误拦截器,该拦截器可以分析错误的严重性并通知监视系统平台组件上的潜在问题。 将来,他将能够独立决定在平台组件上发现错误。 错误拦截器直接从消息代理捕获它们,并且对合同或错误的目的一无所知,仅根据元信息来执行。 这使他以及本节中介绍的所有服务都可以与所有将来的合同向前兼容。

自动生成用户界面


严格规范的合同允许您自动构建用户界面组件。 我们开发了一项服务,使您可以基于合同集合生成管理界面,然后将该界面嵌入到我们的任何平台工具中。 因此,我们以前用手写的那些管理员现在可以在自动模式下生成(尽管到目前为止只有一部分)。

平台记录


该组件尚未实现,正在开发中。 但是将来,它将允许“即时”打开和关闭平台中任何业务功能的日志记录,直接从消息代理中提取此信息,而不会产生任何不利影响交互组件的副作用。

合约API的主要目的


但是,Contract API的主要目的是降低集成平台组件的成本。

通过为每个技术堆栈开发的库,从传输级别抽象了开发人员。 如果我们不得不更改消息代理,甚至切换到点对点交互,这为我们提供了一些回旋余地。 库的外部接口将保持不变。

后台的库根据某些规则生成一条消息,并将其发送给代理,然后,在等待响应消息之后,它将结果返回给开发人员。 在外部,它看起来像是常规的同步(或异步,与实现有关的)请求。 作为演示,我将举几个例子。

Python合约呼叫范例
from platform_client import Client client = Client(contracts_path=CONTRACTS_PATH, url=AMQP_URL, app_id='client') client.call("ban-management.create-ban.v1", { "wgid": 1234567890, "reason": "Fraudulent activity", "title": "ru.wot", "component": "game", "bantype": "access_denied", "author_id": "v_nikonovich", "expires_at": "2038-01-19 03:14:07Z" }) { u'ban_id': 31415926, u'wgid': 1234567890, u'title': u'ru.wot', u'component': u'game', u'reason': u'Fraudulent activity', u'bantype': u'access_denied', u'status': u"active", u'started_at': u"2019-02-15T15:15:15Z", u'expires_at': u"2038-01-19 03:14:07Z" } 

相同的合同通话,但使用Elixir
 :platform_client.call("ban-management.create-ban.v1", %{ "wgid" => 1234567890, "reason" => "Fraudulent activity", "title" => "ru.wot", "component" => "game", "bantype" => "access_denied", "author_id" => "v_nikonovich", "expires_at" => "2038-01-19 03:14:07Z" }) {:ok, %{ "ban_id" => 31415926, "wgid" => 1234567890, "title" => "ru.wot", "conponent" => "game", "reason" => "Fraudulent activity", "bantype" => "access_denied", "status" => "active", "started_at" => "2019-02-15T15:15:15Z", "expires_at" => "2038-01-19 03:14:07Z" }} 

代替合同“ ban-management.create-ban.v1”,可以有任何其他平台功能,例如:“ account-management.rename-account.v1”或“ notification-center.create-sms-notification.v1”。 并且所有这些都可以通过与平台的这一单点集成来获得。

如果您未从服务器开发人员的角度演示Contract API,则概述将不完整。 考虑一种情况,开发人员需要为同一ban-management.create-ban.v1合同实现一个处理程序。

 from platform_server import BlockingServer, handler class CustomServer(BlockingServer): @handler('ban-management.create-ban.v1') def handle_create_ban(self, params, context): response = do_some_usefull_job(params) return response d = CustomServer(app_id="server", amqp_url=AMQP_URL, contracts_path=CONTRACTS_PATH) d.serve() 

此代码足以开始提供给定的合同。 服务器库将解压缩并检查请求参数的正确性,然后使用准备处理的请求参数调用合同处理程序。 因此,服务器开发人员受到该库的保护,该库在接收到不正确的请求参数的情况下,本身会将验证错误发送给客户端并记录问题的事实。

由于事实是,Contract API是在事件的基础上实现的,因此我们有机会超越Request / Response脚本的范围,并实现更广泛的服务间交互。

例如:

  • 提出请求并忘记(无需等待答案)
  • 同时向多个合同提出请求(即使不使用事件循环)
  • 发出请求并同时从多个处理程序接收答案(如果集成脚本提供了)
  • 注册一个响应处理程序(如果合同处理程序报告完成了则触发,接受合同处理程序的工作结果,即其响应)

这不是可以通过交互事件模型表达的方案的完整列表。 这是我们当前正在使用的列表。

而不是结论


我们使用Contract API已经有好几年了。 因此,不可能在一篇评论文章的框架内讨论其使用的所有情况。 出于同样的原因,我没有在文章中添加技术细节。 她已经变得非常庞大。 提出问题,我将尝试直接在评论中回答。 如果某个主题特别有趣,则可以在另一篇文章中更详细地披露它。

Source: https://habr.com/ru/post/zh-CN441708/


All Articles