分布式系统中的安全互操作性



哈勃!

我叫Alexey Solodky,我是Badoo的PHP开发人员。 今天,我将分享第一次Badoo PHP Meetup的演讲文本。 有关mitap的此报告和其他报告的视频可在此处找到。

任何包含至少两个组件的系统(如果同时拥有PHP和数据库,则这是两个组件),在这些组件之间进行交互时会面临全部风险。

我工作的平台部门将新的内部服务与我们的应用程序集成在一起。 解决这些问题,我们积累了经验,我想与大家分享。

我们的后端是一个与许多服务交互的PHP整体(目前大约有50个服务)。 服务很少相互交互。 但是我在本文中讨论的问题也与微服务架构有关。 确实,在这种情况下,服务之间的交互非常活跃,而您之间的交互越多,您遇到的问题就越多。

考虑当服务崩溃或死亡时该怎么办,如何组织度量标准集合以及以上所有这些都不能拯救您时该怎么办。

服务崩溃


迟早,安装您的服务的服务器将掉落。 它肯定会发生,并且您不能防御它-仅降低可能性。 您可能会因硬件,网络,代码,不成功的部署-一切而失望。 并且您拥有的服务器越多,这种情况就越经常发生。

在服务器不断崩溃的世界中,如何使您的服务得以生存? 解决此类问题的通用方法是冗余。

从铁到整个数据中心,冗余在各个级别得到广泛使用。 例如,RAID1可防止硬盘驱动器发生故障或在第一个服务器出现故障时为服务器提供备用电源。 而且,该方案被广泛应用于数据库。 例如,您可以为此使用主从。

让我们以最简单的方案为例来考虑冗余的典型问题:


该应用程序专门与主机通信,而在后台异步地将数据传输到从机。 当主机崩溃时,我们将切换到从机并继续工作。



还原主服务器后,我们只是从中创建了一个新的从服务器,而旧的从服务器变成了主服务器。

该方案很简单,但是即使有冗余方案,它也具有许多细微差别。

负荷


假设上例中的一台服务器可以承受约100k RPS。 现在负载为60k RPS,一切都像时钟一样。

但是随着时间的流逝,应用程序的负载增加,从而主服务器上的负载也随之增加。 您可能需要通过将部分读数移至从站来实现平衡。

看起来还不错 保持负载,服务器不再空闲。 但这是一个坏主意。 重要的是要记住为什么最初要举起从属设备-如果主用设备出现问题,请切换到它。 如果您开始同时加载两个服务器,则当主服务器崩溃时(或早或晚崩溃),您将必须将主要流量从主服务器切换到备份服务器,并且该服务器已经加载。 这样的过载将使您的系统非常慢,或者完全禁用它。

资料


向服务添加容错功能时的主要问题是本地状态。 如果您的服务是无状态的,即不存储任何可变数据,那么对其进行扩展就不会出现问题。 我们只根据需要举起多个实例,并在它们之间平衡请求。

如果服务是有状态的,我们将无法再这样做。 您需要考虑如何在我们服务的所有实例上存储相同的数据,以便它们保持一致。

为了解决此问题,使用了两种方法之一:同步或异步复制。 在一般情况下,我建议您使用异步选项,因为它通常更简单,更快速地编写,并且根据情况,请查看是否需要切换到同步。

使用异步复制时要考虑的重要细微差别是最终一致性 。 这意味着在不同从属服务器上的特定时间点,数据可能会以不可预测的不同时间间隔落后于主机。
因此,您不能每次都从随机服务器读取数据,因为不同的答案可能会传给同一用户请求。 要变通解决此问题,使用了粘性会话机制,可确保来自一个用户的所有请求都转到一个实例。

同步方法的优点是数据始终处于一致状态,并且丢失数据的风险较低(因为仅在所有服务器完成数据处理后,才认为是记录数据)。 但是,您必须为此付出代价,要以系统本身的写入速度和复杂性为代价(例如,用于防止裂脑的各种仲裁算法)。

结论


  • 储备。 如果数据本身和特定服务的可用性很重要,则请确保您的服务将在特定计算机崩溃后继续存在。
  • 在计算负载时,请考虑某些服务器的故障。 如果您的集群有四台服务器,请确保当一台服务器掉落时,剩下的三台服务器将减轻负载。
  • 根据任务选择复制类型。
  • 不要将所有鸡蛋都放在一个篮子里。 确保服务器之间的距离足够远。 根据服务可用性的重要性,您的服务器可以位于一个数据中心的不同机架中,也可以位于不同国家/地区的不同数据中心中。 这完全取决于您想要多少全球灾难并准备生存。

静音服务


在某些时候,您的服务可能会开始非常缓慢地工作。 出现此问题的原因有很多:负载过大,网络滞后,硬件问题或代码错误。 它看起来像一个不那么可怕的问题,但实际上它比看起来更阴险。

想象:一个用户请求一个页面。 我们同时并依次访问四个恶魔来绘制它。 他们反应迅速,一切正常。

假设使用带有固定数量的PHP FPM工人(例如,十个)的nginx处理这种情况。 如果每个请求的处理时间大约为20毫秒,则可以通过简单的计算来理解,我们的系统每秒能够处理大约500个请求。

当这四个服务之一开始变钝,并且对它的请求处理从20毫秒增加到1000毫秒超时时,会发生什么? 重要的是要记住,当我们使用网络时,延迟可能无限大。 因此,您必须始终设置一个超时(在这种情况下,它等于一秒)。

事实证明,后端被迫等待超时到期并接收并处理来自守护程序的错误。 这意味着用户将在一秒钟而不是十毫秒内收到该页面。 慢,但不致命。

但是,这里真正的问题是什么? 事实是,当我们每秒处理每个请求时,吞吐量可悲地下降到每秒十个请求。 第十一用户将不再能够获得响应,即使他请求的页面绝不与乏味服务相关联。 仅仅因为所有十个工作人员都在等待超时,并且无法处理新请求。

重要的是要理解,这个问题不能通过增加工人数量来解决。 毕竟,每个工人都需要一定数量的RAM才能工作,即使他没有执行实际工作,只是因为超时而挂起。 因此,如果您不根据服务器的功能来限制工作人员的数量,那么筹集越来越多的新工作人员将使整个服务器成为可能。 这种情况是级联故障的一个示例,当一项服务的崩溃(即使对用户而言并不重要)导致整个系统故障时,也是如此。

解决方案


有一种模式称为断路器 。 他的任务很简单:他必须在某个时间点削减枯燥的服务。 为此,在服务和工作人员之间放置一个代理。 它可以是带有存储的PHP代码,也可以是本地主机上的守护程序。 重要的是要注意,如果您有多个实例(您的服务已复制),则此代理必须分别跟踪每个实例。

我们已经编写了该模式的实现。 但这不是因为我们喜欢编写代码,而是因为许多年前解决这个问题时,还没有现成的解决方案。

现在,我将概述有关我们的实现及其如何避免此问题的概述。 6月下旬,米哈伊尔·库尔马耶夫(Mikhail Kurmaev)在Highload Siberia上发表的一份报告中,可以听到有关她的更多信息以及与其他解决方案的不同之处。 他的报告的成绩单也将在此博客上。

看起来像这样:

断路器面临着一种抽象的Sphinx服务。 断路器存储到特定守护程序的活动连接数。 一旦该值达到阈值(我们将其设置为机器上可用FPM工人的百分比),我们认为服务开始变慢。 达到第一个阈值后,我们会向负责该服务的人员发送通知。 这种情况要么是需要重新审视极限的迹象,要么是暗淡问题的预兆。

如果情况恶化,并且抑制工人的数量达到第二个阈值-在我们的生产中约为10%-我们将完全削减该主机。 更准确地说,该服务实际上继续工作,但是我们停止向其发送请求。 Circuit浏览器拒绝了它们,并立即向工作人员显示错误,就像服务在说谎一样。

有时,我们会自动跳过工作人员的请求,以查看该服务是否已生效。 如果他回答得当,那么我们会再次将他包括在内。

完成所有这些操作是为了将情况减少到以前的复制方案。 我们无需等待一秒钟即可意识到主机不可用,而是立即得到一个错误并转到备用主机。


实作


幸运的是,开源并没有停滞不前,今天您可以在Github上采用交钥匙解决方案。

实现断路器的主要方法有两种:代码级库和通过自身代理请求的独立守护程序。

如果您在PHP中具有一个主要的整体组件,并且与多个服务交互,并且这些服务几乎不相互通信,则该库选项更加合适。 以下是一些可用的实现:


如果您有许多使用不同语言的服务,并且它们彼此交互,那么必须在所有这些语言中复制代码级别的选项。 这在支持上很不方便,并最终导致实现上的差异。

在这种情况下放置一个守护程序要容易得多。 在这种情况下,您不必特别编辑代码。 恶魔试图使交互透明。 但是,此选项在架构要复杂得多

这里有一些选择(功能更丰富,但也有断路器):


结论


  • 不要依赖网络。
  • 所有网络请求都必须具有超时,因为网络可以提供无限长的时间。
  • 如果您要避免由于一个小型服务的速度降低而导致级联的应用程序崩溃,请使用断路器。

监控和遥测


它有什么作用


  • 可预测性。 重要的是要预测什么是负载以及一个月后的负载,以便及时增加服务实例的数量。 如果您要处理铁基础架构,则尤其如此,因为订购新服务器需要时间。
  • 调查事件。 迟早无论如何都会出问题,您必须进行调查。 而且重要的是要有足够的数据来理解问题并能够在将来防止此类情况。
  • 事故预防。 理想情况下,您应该了解哪些模式会导致崩溃。 跟踪这些模式并及时将其告知团队非常重要。


衡量什么


整合指标

由于我们正在谈论服务之间的交互,因此我们监视与服务与应用程序通信有关的所有可能情况。 例如:

  • 请求数量;
  • 要求处理时间(包括百分位数);
  • 逻辑错误数;
  • 系统错误数。

区分逻辑错误和系统错误很重要。 如果服务下降,这是正常现象:我们只需切换到第二个。 但这并不那么可怕。 如果您启动某种逻辑错误,例如,奇怪的数据进入服务或离开服务,则已经需要对此进行调查。 该错误很可能与代码中的错误有关。 她自己不会通过。

内部指标

默认情况下,该服务是一个黑匣子,无法理解其工作。 仍然需要了解并收集服务可以提供的最大数据。 如果服务是一个专门的数据库,用于存储您的业务逻辑的某些数据,请跟踪确切的数据量,数据类型和其他内容指标。 如果您具有异步交互,则监视服务通信所通过的队列也很重要:它们的到达和离开速度,不同阶段的时间(如果您有多个中间点),队列中的事件数。

让我们看看可以使用memcached作为示例收集哪些指标:

  • 命中率/未命中率;
  • 各种操作的响应时间;
  • RPS的各种操作;
  • 相同数据在不同键上的细分;
  • 顶部按键
  • stats命令给定的所有内部指标。


怎么做


如果您的公司规模较小,项目规模较小且服务器很少,那么连接某种SaaS进行收集和查看是一个很好的解决方案,它既简单又便宜。 在这种情况下,通常SaaS具有广泛的功能,而不必担心很多事情。 此类服务的示例:


另外,您始终可以在自己的计算机上安装Zabbix,Grafana或任何其他自托管解决方案。

结论


  • 收集所有可以度量的指标。 数据不是多余的。 当您必须进行调查时,您会说声谢谢。
  • 不要忘记异步交互。 如果您有任何逐步到达的线路,那么了解它们到达的速度,在服务之间的交汇处发生的事件很重要,这一点很重要。
  • 如果您编写服务,请教其提供有关工作的统计信息。 当我们与此服务进行通信时,可以在集成层上测量部分数据。 服务的其余部分应能够根据条件命令提供统计信息。 例如,在我们所有的Go服务中,此功能都是标准功能。
  • 自定义触发器。 图表很好,但是只有当您查看它们时才可以。 重要的是您有一个定制的系统,该系统可以在发生问题时通知您。


现在,关于悲伤的事情。 您可能会感觉到上述内容是灵丹妙药,现在什么也不会掉下来。 但是,即使您应用了上面描述的所有内容,总会有一些失败。 考虑这一点很重要。
下降的原因很多。 例如,您可能选择了一个偏执狂的复制方案。 一颗陨石落入您的数据中心,然后进入第二个。 或者,您刚刚部署的代码带有一个棘手的错误,该错误意外弹出。

例如,在Badoo中,有一个页面“附近的人”。 用户在那里搜索附近的其他人并与他们聊天。



现在,要渲染页面,后端将对大约七个服务进行同步调用。 为了清楚起见,将此数字减少为两个。 一种服务负责用照片渲染中心块。 第二个是左下角的广告块。 那些想要变得更加知名的人可以到达那里。 如果我们有一个显示此广告的服务,该块将简单消失。



大多数用户甚至都不知道这个事实:我们的团队迅速做出反应,很快就再次出现了障碍。

但是,并不是我们可以悄悄删除的所有功能。 如果我们丢失了负责页面中心部分的服务,则无法隐藏该服务。 因此,以用户的语言告诉用户正在发生的事情很重要。



还希望一种服务的故障不会导致级联故障。 对于每个服务,必须编写处理其崩溃的代码,否则应用程序可能会整体崩溃。

但这还不是全部。 有时会跌落,没有它,您将无法以任何方式生活。 例如,中央数据库或会话服务。 重要的是要正确地解决问题,并向用户展示适当的东西,以某种方式使他开心,以使一切都处于控制之中。 同时,非常重要的一点是,一切都必须受到真正的控制,并通知监视器有关问题。





死得这么对


  • 为秋天做好准备。 没有灵丹妙药,因此即使您使用冗余,也要始终吸管以防万一服务完全中断。
  • 当其中一项服务的问题导致整个应用程序中断时,请避免级联故障
  • 禁用非关键用户功能。 这很正常。 许多服务仅用于内部需求,并不影响所提供的功能。 例如,统计服务。 对用户而言,是否从您那里收集统计信息都无关紧要。 对于他来说,该站点正常工作很重要。

总结


为了将新服务可靠地集成到系统中,我们在Badoo中为其编写了一个特殊的包装API,该API承担以下任务:

  • 负载平衡;
  • 超时;
  • 逻辑故障转移;
  • 断路器;
  • 监测和遥测;
  • 授权逻辑;
  • 数据的序列化和反序列化。

最好确保所有这些项目也都包含在您的集成层中。 特别是在使用现成的开源API客户端时。 重要的是要记住,集成层增加了应用程序级联失败的风险。

感谢您的关注!

文学作品

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


All Articles