深入了解RBKmoney付款-微服务,协议和平台配置

哈勃! RBKmoney再次与您取得联系,并继续撰写有关如何编写自己动手付款处理程序的系列文章。



我想立即深入介绍作为状态机的支付业务流程的实现的描述细节,展示这种具有一系列事件和实现功能的设备的示例。但是,似乎没有几篇评论文章就无从谈起。 主题区域过大。 这篇文章将揭示工作的细微差别和平台微服务之间的交互,与外部系统的交互以及我们如何管理业务配置。


宏服务


我们的系统由许多微服务组成,这些微服务实现了业务逻辑的每个完成部分,彼此交互并一起形成了宏服务。 实际上,部署在数据中心中并与银行和其他支付系统相连的宏服务是我们的支付处理。


微服务模板


我们使用统一的方法以任何书面语言开发任何微服务。 每个微服务都是一个Docker容器,其中包含:


  • 实现以Erlang或Java编写的业务逻辑的应用程序本身;
  • RPClib-一个实现微服务之间通信的库;
    • 我们使用Apache Thrift,它的主要优点是现成的客户端-服务器库以及能够严格代表每个微服务提供的所有公共方法的描述的功能;
    • 该库的第二个功能是我们对Google Dapper的实现,它使我们能够通过Elasticsearch中的简单搜索来快速跟踪请求。 接收到来自外部系统的请求的第一个微服务会生成一个唯一的trace_id ,每个后续请求链都会保存该唯一的trace_id 。 另外,我们生成并保存parent_idspan_id ,这使您可以构建查询树,以可视方式监视与处理请求有关的整个微服务链。
    • 第三个功能-我们在传输级别上积极使用有关请求上下文的不同信息的传输。 例如,截止日期(在客户端设置的请求的预期生存期),或者我们代表谁调用方法;
  • Consul模板是服务发现代理,用于维护有关微服务的位置,可用性和状态的信息。 微服务通过DNS名称相互查找,区域TTL为零,已死亡或未通过运行状况检查的服务停止解析并因此接收流量。
  • 应用程序以Elasticsearch可以理解的格式写入本地容器文件和filebeat的日志,该日志在相对于容器的主机上运行,​​并拾取这些日志并将其发送到Elasticsearch集群;
    • 由于我们根据事件源模型实现平台,因此,生成的日志链也以不同的Grafana仪表板的形式用于可视化,这减少了实现不同指标的时间(我们也使用单独的指标)。


在开发微服务时,我们会使用我们特别发明的限制,这些限制旨在解决平台的高可用性及其容错性的问题:


  • 超出限制时,每个容器都有严格的内存限制-OOM,大多数微服务都生活在256-512M之内。 这使得业务逻辑的实现更加细分,防止向整体迁移,降低故障点的成本,在廉价硬件上工作具有额外的优势(该平台已部署并在廉价的单处理器服务器上运行);
  • 尽可能少的有状态微服务,以及尽可能多的无状态实现。 这使我们能够解决容错性,恢复速度以及总体上将具有潜在不可理解行为的场所最小化的问题。 随着大量遗留物的积累,随着系统寿命的增加,这一点变得尤为重要。
  • 让它崩溃,“肯定会崩溃”的方法。 我们知道系统的任何部分都必定会发生故障,因此我们对其进行设计,以免影响平台中所积累信息的总体正确性。 帮助最小化系统中未定义状态的数量。

与第三方集成的许多人肯定会熟悉这种情况。 我们期望第三方对根据协议冲销款项的请求做出响应,并且得出了完全不同的答案,在任何规范中都没有描述,这是如何解释的。


在这种情况下,我们将终止提供该付款的状态机,从外部对其进行的任何操作都会收到500的错误。在内部,我们会找到付款的当前状态,使该状态与实际情况保持一致,并恢复该状态机。


面向协议的开发



在撰写本文时,已在我们的服务发现中注册了636种不同的支票,用于确保平台功能的服务。 即使考虑到对一项服务正在执行多项检查,并且考虑到大多数无状态服务至少在三重实例中运行,您仍然会获得五十个应用程序,这些应用程序必须能够以某种方式彼此连接且不会失败在RPC地狱中。


由于我们在堆栈上拥有三种开发语言(Erlang,Java,JS),因此情况变得复杂,它们都必须能够透明地相互通信。


需要解决的第一个任务是设计用于在微服务之间交换数据的正确体系结构。 作为基础,我们采用了Apache Thrift。 所有微服务都交换Trift二进制文件;我们使用HTTP作为传输。


我们将字体规范以单独的存储库形式放置在我们的github中,因此任何有权访问它们的开发人员都可以使用它们。 最初,他们为所有协议使用一个公共存储库,但是随着时间的推移,他们得出的结论是,这样做很不方便-协议的并行并行工作变成了长期困扰。 不同的团队甚至不同的开发人员都被迫就变量名称达成共识,尝试拆分为名称空间也无济于事。


通常,我们可以说我们具有协议驱动的开发。 在开始任何实施之前,我们以提升规范的形式开发未来的微服务协议,经过7个审核圈子,吸引该微服务的未来客户,并有机会同时开始并行开发多个微服务,因为我们知道其所有未来方法,并且我们已经可以编写其处理程序,可以选择使用moki。


协议开发过程中的一个单独步骤是安全性审查,在此过程中,专家们从五分之一的角度着眼于正在开发的规范的细微差别。


我们还认为应该突出协议所有者在团队中的独立角色。 这项任务很艰巨,一个人必须牢记所有微服务的细节,但是它的回报很高,而且存在单个升级点。


如果没有这些员工的最终批准请求,则该协议无法合并到master分支中。 github中有一个非常方便的功能- 密码所有者 ,我们很乐意使用它。


因此,我们解决了微服务之间的通信问题,可能误解了平台中出现了哪种微服务以及为什么需要它的问题。 这套协议也许是平台上我们唯一的部分,我们可以无条件地根据开发的成本和速度选择质量,因为一个微服务的实现可以相对轻松地重写,而几十个协议已经非常昂贵和痛苦。


在此过程中,准确的日志记录有助于解决文档问题。 合理地选择方法和参数的名称,一些注释以及自我记录的规范可以节省大量时间!


例如,这是我们微服务之一的方法规范的外观,使您可以获取平台中发生的事件的列表:


 /**    */ typedef i64 EventID /* Event sink service definitions */ service EventSink { /** *       ,   *    ,  ,  `range`.  *      `0`  `range.limit` . * *   `range.after`    ,   * ,        , *   `EventNotFound`. */ Events GetEvents (1: EventRange range) throws (1: EventNotFound ex1, 2: base.InvalidRequest ex2) /** *         *  . */ base.EventID GetLastEventID () throws (1: NoLastEvent ex1) } /* Events */ typedef list<Event> Events /** * ,    -,  . */ struct Event { /** *  . *    ,     *      (total order). */ 1: required base.EventID id /** *   . */ 2: required base.Timestamp created_at /** *  -,  . */ 3: required EventSource source /** *  ,    ( ) *   -,  . */ 4: required EventPayload payload /** *      . *    . */ 5: optional base.SequenceID sequence } // Exceptions exception EventNotFound {} exception NoLastEvent {} /** * ,       -   */ exception InvalidRequest { /**          */ 1: required list<string> errors } 

节俭的控制台客户端


有时,我们面临着直接调用必需的微服务的某些方法的任务,例如,我们从终端那里动手。 这对于调试,获取原始数据集或任务非常少以至于无法开发单独的用户界面非常有用。


因此,我们为自己开发了一个工具,该工具结合了curl函数,但允许您以JSON结构的形式发出Trift请求。 我们因此称呼他为-。 该实用程序是通用的,使用命令行参数将任何升降机规格的位置传递给它就足够了,它将自行完成其余工作。 一个非常方便的实用程序,例如,您可以直接从终端开始付款。


这就是直接吸引负责管理应用程序(例如,创建商店)的平台微服务的方式。 我在测试帐户中请求了数据:



观察者可能会注意到屏幕截图中的一项功能。 我们也不喜欢。 有必要加强微服务之间的Trift调用的授权,有必要很好地粘合TLS。 但是,尽管资源一如既往地不够。 我们将自己限制在处理微服务所处的整个外围环境中。


与外部系统通信的协议


为了发布外部升降机规范并迫使我们的商人使用二进制协议进行通信,我们认为这对他们来说太残酷了。 必须选择一种人类可读的协议,该协议将使我们能够方便地与我们集成,调试并能够方便地进行文档记录。 我们选择了Open API标准,也称为Swagger


回到记录协议的问题,Swagger使您可以快速,廉价地解决此问题。 该网络以开发人员文档的形式对Swagger规范的精美设计进行了许多实现。 我们仔细研究了发现的所有内容,最终选择了ReDoc (一个接受swagger.json作为输入的JS库),并在输出中生成了这样的三列文档: https : //developer.rbk.money/api/


内部Thrift和外部Swagger这两种协议的开发方法对我们来说都是完全相同的。 这增加了开发时间,但从长远来看是有回报的。


我们还需要解决另一个重要问题-我们不仅接受注销款项的请求,而且还将其进一步发送给银行和支付系统。


强迫他们实施我们的提升比将其提交给公共API更加不切实际。


因此,我们提出并实现了协议适配器的概念。 这只是另一种微服务,在一侧实现了我们的内部提升规范,这对于整个平台都是相同的,而第二种是特定于特定银行或PS的外部协议。


当您必须与第三方进行交互时编写此类适配器时出现的问题是一个充满不同故事的主题。 在我们的实践中,我们遇到了不同的事情,形式为:“您当然可以按照我们给您的协议中的描述来实现此功能,但我没有任何保证。这是我们两周后生病的人,所有这些答案,然后您要求他确认。” 同样,这种情况并不罕见:“这里是我们服务器的用户名和密码,请转到此处自行配置所有内容。”


当我们与付款合作伙伴集成时,我发现它特别有趣,后者又先前已经与我们的平台集成并成功地通过我们付款(这通常是付款行业的业务细节)。 为了响应我们对测试环境的要求,合作伙伴回答说他没有这样的测试环境,但是他可以获得与RBC(即我们可以参与其中的平台)集成的流量。 这就是我们通过合作伙伴与自己整合一次的方式。


因此,我们很简单地解决了实现各种支付系统和其他第三方的大规模并行连接的问题。 在大多数情况下,您无需为此触摸平台代码,只需编写适配器并向枚举添加更多支付工具。


结果,我们得到了这样的工作方案-我们在RBKmoney API微服务(我们将它们称为Common API或capi *,您在上面的领事中看到了它们)的外部,它们根据公共Swagger规范验证输入数据,授权客户,广播这些方法会发送给我们的内部客服人员,然后将请求沿着链发送到下一个微服务。 此外,这些服务还实现了另一个平台要求,其技术规范被表述为:“系统应始终有机会获得支持。”


当我们需要调用某个外部系统时,内部微服务会拉动相应协议适配器的提升方法,它们会将它们转换为特定银行或支付系统的语言,然后将其发送出去。


协议向后兼容困难


平台在不断发展,增加了新功能,改变了旧功能。 在这种情况下,您必须投资于支持向后兼容性,或者不断更新依赖的微服务。 而且,如果必填字段变成可选字段的情况很简单,那么您什么也做不了,那么在相反的情况下,您就不得不花费额外的资源。


使用一组内部协议,事情变得更容易。 支付行业很少变化,因此出现了一些根本上新的交互方法。 以我们的一项常见任务为例-连接新的提供商和新的付款方式。 例如,本地钱包处理,使您可以在哈萨克斯坦坚决地处理付款。 这是适用于我们平台的新钱包,但原则上与同一个Qiwi钱包没有什么不同-它始终具有一些唯一的标识符和方法,可让您从中借记/取消借记。


因此,我们为所有钱包提供商提供的提升规范如下所示:


 typedef string DigitalWalletID struct DigitalWallet { 1: required DigitalWalletProvider provider 2: required DigitalWalletID id } enum DigitalWalletProvider { qiwi rbkmoney } 

并以新钱包的形式添加新的付款方式只是对枚举的补充:


 enum DigitalWalletProvider { qiwi rbkmoney newwallet } 

现在,仍然需要使用该规范来破坏所有微服务,与具有该规范的存储库向导同步,并通过CI / CD进行部署。


外部协议更加复杂。 Swagger规范的每次更新,特别是没有向后兼容性的更新,几乎都不可能在合理的时间内应用-我们的合作伙伴不太可能会保留免费的开发人员资源专门用于更新我们的平台。


有时这简直是不可能的,我们偶尔会遇到这样的情况:“程序员写信给我们,离开了我们,随身带走了我们的资源,我们如何工作,我们不知道,它有效并且不动摇。”


因此,我们投资支持外部协议的向后兼容性。 在我们的体系结构中,这要容易一些-因为我们为Common API的每个特定版本使用了单独的协议适配器,所以我们只是让旧的capi微服务可以正常工作,并在必要时仅更改了看起来像是琐事的部分。 因此,微服务capi-v1capi-v2capi-v3等出现并永远存在。


capi-v33时会发生什么capi-v33我们可能不得不弃用某些旧版本。


在这一点上,我通常会开始很好地理解Microsoft等公司,以及它们在支持已经使用了数十年的解决方案的向后兼容性方面所经历的痛苦。


定制系统


并且,在结束本主题之后,我们将告诉您我们如何管理特定于业务的平台设置。


仅仅付款并不像看起来那样容易。 对于每次付款,业务客户都希望附加大量条件-从佣金到原则上取决于一天中的时间成功实施的可能性。 我们为自己设定了一项任务,即数字化业务客户现在和将来可以想到的全部条件,并将其应用于每个新近发起的付款。


结果,我们决定开发自己的DSL,并为其固定了方便管理的工具,这些工具使我们能够以正确的方式描述业务模型:协议适配器的选择,投递计划的描述,据此金钱将被分散到系统内的帐户,设置限制,佣金,类别和付款系统特有的其他内容。


例如,当我们要收取1%的佣金以购买大师和MS的卡并将其分散在系统内的帐户上时,我们可以这样配置域:


 { "cash_flow": { "decisions": [ { "if_": { "any_of": [ { "condition": { "payment_tool": { "bank_card": { "definition": { "payment_system_is": "maestro" } } } } }, { "condition": { "payment_tool": { "bank_card": { "definition": { "payment_system_is": "mastercard" } } } } } ] }, "then_": { "value": [ { "source": { "system": "settlement" }, "destination": { "provider": "settlement" }, "volume": { "share": { "parts": { "p": 1, "q": 100 }, "of": "operation_amount" } }, "details": "1% processing fee" } ] } } ] } } 

, , . , JSON. , , , . , , . , CVS/SVN-.


" ". , , , 1%, , , . , , . , .


cvs-like , . , — stateless, , . . .


- . , , . , , .


. , 10 , , .


, , , -, woorl-. - JSON- . - JS, , UX:



, , , .


, , .


, , SaltStack.


, !

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


All Articles