Facebook Webhook处理的演变:从零到每秒25,000

最有可能的是,没有人需要告诉您什么是网络钩子。但是以防万一:Webhooks是一种用于报告外部系统中的事件的机制。例如,关于通过在线收银员在在线商店中购买商品,向GitHub存储库发送代码或聊天中的用户操作。在典型的API中,如果用户在聊天中写了一些内容,则需要不断查询服务器。使用webhook机制,您可以“订阅”通知,并且服务器本身将在事件发生时发送HTTP请求。这比在服务器上不断请求新数据更方便,更快捷。



ManyChat是一个平台,可帮助企业通过即时通讯程序中的聊天与客户进行沟通。 Webhooks是ManyChat的重要组成部分之一,因为企业通过它与客户进行通信。他们之间进行了很多交流-例如,通过系统,企业每月向其客户发送数十亿条消息。

大多数消息都是通过Facebook Messenger发送的。它具有一个功能-慢速API。当客户写消息订购披萨时,Facebook将网络挂钩发送给ManyChat。平台对其进行处理,将请求发送回去,并且用户会收到一条消息。由于API速度较慢,某些请求会持续几秒钟。但是,当平台长时间没有响应时,企业就会失去客户端,Facebook可以将应用程序与网络挂钩断开。

因此,处理Webhook是平台的主要工程任务之一。为了解决该问题,在三年的过程中,ManyChat几次将其处理体系结构从Yii中的简单控制器更改为带有星系的分布式系统。阅读更多关于Dmitry Kushnikov(Cancellarius

梅德Kushnikov导致发展的ManyChat,并已在专业PHP自2001年以来的编程。Dmitry将告诉您架构如何随着服务和负载的增长而变化,在不同阶段应用了哪些解决方案和技术,webhook处理如何演变以及平台如何使用PHP中的少量资源来应对巨大的负载。

注意。本文基于Dmitry在PHP Russia 2019中的报告“ Facebook Webhook处理的演变:从零到每秒12,500” 但是在他准备的时候,指标上升到25,000。


什么是ManyChat


首先,我将向您介绍我们的任务范围。 ManyChat是一项服务,可帮助企业使用即时通讯工具进行营销,销售和支持。主要产品是Facebook Messenger上的Messenger营销平台。三年来,来自全球100个国家/地区的超过100万家企业使用该服务与7亿客户进行了交流。

在客户端,它看起来像这样。


Facebook Messenger中对话框中的按钮,图片和图库。

这是Facebook Messenger界面。除了文本消息,您还可以在其中发送交互元素以与客户交互,进行对话,增加对产品的兴趣和销售。

从业务方面一切看起来都不同。这是我们的Web应用程序的界面,业务代表使用可视界面在其中创建和编程对话脚本。图片是一个场景的例子。


我们系统的核心是Flow Builder组件。

这套脚本和自动化规则称为bot因此,为简化起见,我们可以说ManyChat是机器人设计师。


机器人的例子。

参与对话的业务的客户称为订户,因为进行交互时,该客户会订阅机器人

为什么选择Facebook


为什么选择Facebook Messenger,我们是尚存电报的国家?这是有原因的。

  • Telegram , №1 Facebook. 1,5 , Telegram 200-300 .
  • Facebook , . , Facebook - .
  • Facebook F8 - 300 . Facebook Messenger. 20 . ManyChat 40%.

Facebook


与Facebook的交互组织如下:



业务使用Web应用程序配置漫游器的逻辑。当客户通过电话与漫游器进行互动时,Facebook会接收有关此信息,并向我们发送网络挂钩。ManyChat根据业务编程的逻辑对其进行处理,然后将请求发送回去。然后,Facebook将消息传递到用户的手机。

技术栈


我们在适度的堆栈上完成所有这些操作。当然,核心是PHP。Web服务器运行Nginx,主数据库是PostgreSQL,还有Redis和Elasticsearch。所有这些都在Amazon Web Services云中旋转。

处理Facebook Webhook


这就是Facebook的网络摄像头的样子:这是带有JSON格式有效负载的请求。

{
    "object":"page",
    "entry":[
        }
            "id":"<PAGE_ID>",
            "time":1458692752478,
            "messaging":[
                {
                    "sender":{
                        "id":"<PSID>"
                    },
                    "recipient":{
                        "id":"<PAGE_ID>"
                    },

                    ...
                }
            ]  
        }
    ]
}

Webhook仅占我们负载的10%,但却是系统中最重要的部分。通过它们,企业与用户进行通信。如果消息变慢或没有发送,则用户拒绝与该机器人进行交互,从而使企业失去客户。

让我们来看看自产品推出以来我们架构的发展。

2016年5月我们刚刚启动了我们的服务:20个机器人,其中10个是测试机器人,还有20个订阅者。负载为0 RPS。

交互方案如下所示:



  • 该请求转到nginx。
  • Nginx访问PHP-FPM。
  • PHP-FPM将应用程序升级到Yii。
  • Webhook控制器处理逻辑,并根据逻辑将请求发送到Facebook。


一堆Nginx和PHP-FPM


2016年6月。一个月后,我们在ProductHunt上宣布了ManyChat,而僵尸程序的数量增加到了2000。订户数量已增加到7,000。

此时,第一个问题出现在系统中。Facebook API的运行速度不是很快:有些请求可能需要几秒钟,而有些请求可能需要数十秒钟。但是webhook服务器希望我们快速响应。由于API的速度较慢,我们很长一段时间都没有响应:服务器先发誓,然后它才能完全断开应用程序与webhooks的连接。

用户很少,我们仍在开发应用程序,我们正在寻找我们的市场,受众,并且负载问题已经出现。但是我们被一个简单的解决方案所拯救:在控制器启动时,我们中断了对Facebook的访问我们告诉Facebook一切都很好,但是在后台我们处理请求和Webhook。



PostgreSQL上的队列


2016年12月。服务增长了5到10倍:1万个自动程序和70万个订阅者。

同时,我们从事新任务:显示统计信息,消息传递,印象转换和过渡。还实现了实时聊天。除了自动进行交互之外,它还使企业能够直接将消息写入其订户。

这些问题的解决方案使跟踪的挂钩数增加了4倍。对于我们发送的每条消息,我们还会收到3个额外的Webhook。处理系统需要再次改进。我们是一个小型平台,后端只有两个人工作,因此我们选择了最简单的解决方案-PostgreSQL上的队列。

我们还不想实现复杂的系统,因此我们只是共享处理流程。需要快速处理以使用户收到响应的Webhook会被同步处理。所有其余的都在队列中发送以进行异步请求。



Redis的队列


2017年6月。服务在增长:7.5万个机器人,700万用户。

我们正在实现另一个新功能。我们处理的所有Webhook仅与Messenger中的通信有关。但是现在,我们决定给企业提供与企业页面订阅者进行交流的机会,并开始处理新型的Webhooks-与页面本身的提要相关的那些。

业务页面供稿不会经常更新。营销人员经常发布一些东西,然后他们按照每个人的喜好进行计数。业务页面上没有大量流量。但是也有相反的情况,例如,Katy Perry Day

凯蒂·佩里(Katy Perry)是一位著名的美国歌手,在世界各地都有大量的歌迷。仅在她的Facebook组中就有6400万订户。在某个时候,歌手的营销商决定在Facebook Messenger上制作一个机器人,并选择了我们的平台。那时,当他们发布一条消息来订阅该机器人时,我们的负载增加了3-4倍。

这种情况有助于我们了解,如果没有正常执行队列,我们​​将无能为力。作为解决方案,他们选择了Redis。
为队列选择Redis是一个非常好的决定。
他帮助解决了许多问题。现在,通过我们的Redis集群,每秒传递一百万个不同的请求。我们不仅将其用于所有级联队列,还用于其他任务,例如监视。

第一次尝试时未实现Redis上的队列。当我们开始只在Redis中折叠Webhook并在一个过程中对其进行处理时,我们在顶部扩展了该漏斗:也处理了更多传入的Webhook,但该过程本身仍需要一些时间。最初的决定没有成功。



当他们尝试扩展这些请求的数量时,出现了轻微的崩溃。队列可以累积来自不同页面的请求,但是来自一个页面的请求可以连续进行。如果一个处理程序运行缓慢,则来自一个订户和一个机器人的请求将以错误的顺序处理。用户发送消息,使用机器人执行一些操作,但随机接收响应。



这似乎很少见,但是对我们的工作负载进行测试表明,这种情况经常发生。

我们开始寻找另一种解决方案。 Redis的简单性和强大功能在这里得到了拯救-我们决定为每个机器人排队



怎么运行的?与每个漫游器相关的消息将添加到队列中。为了不将处理程序提升到每个队列,我们​​制作了一个控制队列。她那样工作。每次来自机器人的请求时,都会在Redis中发布两条消息:一条消息在机器人的队列中,第二条消息在控件中。处理程序监视控件,并在有任务要处理僵尸程序时每次监视守护程序。恶魔在相应机器人的队列中抽风。

除了主要任务,我们还解决了“吵闹的邻居”问题。这是一个机器人生成大量Webhooks并减慢系统速度的原因,因为其他页面正在等待处理。要解决该问题,就可以进行扩展:当控制队列已满时,我们添加新的处理程序。

另外,队列是虚拟的这些只是Redis内存中的单元格。当队列中没有任何内容时,它不存在,不会占据任何内容。

ReactPHP


2018年1月。我们每月已达到10亿个职位。

每个系统的负载为5000 RPS。这不是峰值负载,而是标准负载。当著名歌手的机器人出现时,这个数字已经使一切增长了数倍。但这不是问题。问题出在PHP-FPM中:它再也无法承受5000 RPS的负载。

当时每个人都在谈论时尚的异步处理。我们仔细研究了一下,看到ReactPHP,进行了快速测试,将其替换为PHP-FPM,并立即增长了4倍。



我们没有重写处理过程-ReactPHP提出了Yii框架。首先,我们提出了4种ReactPHP服务,后来又增加到30种。很长一段时间,我们都依靠它们来生存,而框架可以应付负载。

扩展漏斗后,又发生了另一次崩溃:在接收端启动漏斗后,处理又开始受到影响。为了已经解决了这个问题,我们决定将处理分为几类。

集群


他们拿走了机器人,将它们分配到集群中,并从Redis,Postgres和处理程序构建了逻辑链。



结果,我们形成了“银河”的概念-处理上的逻辑物理抽象它由实例组成:Redis,PostgreSQL和一组PHP服务。每个机器人都属于一个特定的集群,ReactPHP知道需要将该机器人的消息放置在哪个集群中。上面的方案更有效。


宇宙正在扩展,我们系统的宇宙也在扩展,并且当这种情况发生时,我们添加了一个新的“ Galaxy”。
星系是我们扩展的方式。

用一堆Nginx和Lua替换ReactPHP


在接下来的六个月中,我们继续增长:每月有2亿订户和30亿条消息。想象一个拥有2亿注册用户的网站-同样的负载。

出现了一个新问题。Webhooks是相同类型的小任务,PHP不适合解决它们。甚至ReactPHP也无济于事。

  • 他无法应付1万RPS的负载-自从引入ReactPHP以来,负载已经增加。
  • 而且,即使在进行部署的情况下也必须按顺序重新启动它,因为您不能中断传入Webhooks的处理。当Facebook意识到存在问题时,将禁用该应用程序。对于ManyChat来说,这是一场灾难-650,000个活跃运营的企业不会原谅我们。

因此,我们逐渐摆脱了ReactPHP的不同逻辑,将其传递给处理器,并隔离了新队列。在此过程中,他们注意到ReactPHP执行了一个简单的任务-它需要一个webhook并将其放入队列中。其余的全部通过处理完成。有没有类似的东西可以完成这么简单的任务?

我们记得Nginx有模块,并注意到了OpenResty。除了支持Lua编程语言外,她还有一个用于Redis的模块。在3个小时内编写的测试表明,ReactPHP上30个服务的所有工作都可以直接在nginx端完成。



结果是这样的:我们处理某种端点,选择请求主体并将其直接添加到Redis。

location / {
    error_log /var/log/nginx/error.log;

    resolver ###resolver###;

    content_by_lua '

        ngx.req.read_body()
        local mybody = ngx.req.get_body_data()

        if not mybody then
            return ngx.exit(400)
        end

        local hash = ngx.crc32_long(mybody)
        local cluster = hash % ###wh_inbound_shards### + 1

        local redis = require "resty.redis";
        local red = radis.new()
        red:set_timeout(3000)

        local ok, err = red:connect("###redisConnectionWh2.server.host###", 6379)
		
        if not ok then
            ngx.log(ngx.ERR, err, "Redis failed to connect")
            return ngx.exit(403)
        end

        local ok, err = red:rpush("###wh_inbound_queue###" .. queuesuffix .. cluster, mybody)
        
        if not ok then
            ngx.log(ngx.ERR, err, "Failed to write data", mybody)
            return ngx.exit(500)
        end

        local ok, err = red:set_keepalive(10000, 100)

        ngn.say("ok")
    ';
}

OpenResty和Lua帮助提高了吞吐量。我们将继续应付工作量,服务永存,每个人都很高兴。

改进Lua上的解决方案


最后一个阶段(注意:在撰写报告时)是2019年2月每月有5亿订户发送和接收来自100万个机器人的700万条消息。

这是改进我们在Lua上的解决方案的步骤。逐渐消除队列中的某些逻辑,并将在系统之间分发Webhook的主要过程转移到Lua。现在,我们的系统生产率更高,依赖性更低。



我们维护分开的处理和异步处理处理涉及统计数据和其他事项-现在它是一个完全不同的系统。

该系统看起来很简单,但事实并非如此。在后台,有500个服务处理它们的请求。整个系统在50个Amazon实例上运行:Redis,PostgreSQL和PHP处理程序本身。

处理演变


在PHP中,高负载可以很酷。

简要回顾一下我们在系统开发过程中是如何做到的。

  • 从常规的Nginx和PHP-FPM开始。
  • 将队列添加到PostgreSQL,然后添加到Redis。
  • 添加了集群。
  • 实现了ReactPHP。
  • 我们用一堆Nginx和Lua替换了ReactPHP,然后将逻辑移到了那堆。



从我们的经验中我们发现,可以通过使用简单的众所周知的方法连续更改易受攻击的部分来增长和构建体系结构,同时又不扩展堆栈。

, , 11 TeamLead Conf. , LeSS, .

PHP Russia Saint HighLoad++, . PHP , — PHP Russia 13 . highload PHP, Saint HighLoad++ .

Source: https://habr.com/ru/post/undefined/


All Articles