深度解读 Webhook 与 API

协变珊瑚
• 阅读 220

Webhooks 是许多 API 的补充。通过设置 webhook 系统,系统 B 可以注册接收有关系统 A 某些更改的通知。当更改发生时,系统 A 推送 更改到系统 B,通常是以发出 HTTP POST 请求的形式。

Webhooks 旨在消除或减少不断轮询数据的需要。但根据我的经验,webhooks 带来了一些挑战。

通常,您不能单靠 webhooks 来保持两个系统的一致性。我参与过的每一个集成项目最终都意识到这一点,并通过轮询增强了 webhooks。这是由于几个问题领域。

首先,当您的系统宕机时存在风险。是的,发送者通常会用某种指数退避重试未送达的 webhooks。但保证往往是宽松或不明确的。而且系统从灾难中恢复后,最不需要的可能就是处理积压的 webhooks 大量任务。

其次,webhooks 是短暂的。它们太容易处理不当或丢失。如果您在部署代码更改后意识到您输入了一个错误的 JSON 字段,并且正在将 null 插入到您的数据库中,那么您无法回放 webhooks。或者,您可能会在 webhook 请求之外处理部分 webhook 处理流程——比如数据库插入。但那样您就冒着失败并丢失 webhook 的风险。

为了缓解这两个问题,许多开发人员最终将 webhooks 缓存在像 Kafka 这样的消息总线系统上,这感觉像是一个笨重的妥协。

考虑两方之间复杂的 webhook 管道结构:

深度解读 Webhook 与 API

我们在发送端和接收端各有一个消息总线。复杂性显而易见,而且可能出问题的阶段很多。例如:在接收端,即使您的系统运行良好,您仍然可能受到发送者可交付性失败的影响。如果发送者的队列开始经历背压,webhook 事件将被推迟,而您可能很难知道这种滑移正在发生。

增加复杂性的是,两者之间的安全层通常是某种 HTTP 请求签名协议,如 HMAC。这很稳固且减轻了管理秘密的负担。但对于您的普通开发人员来说,这也不太熟悉,因此更容易头疼和出错。(HTTP 请求 签名和验证是那些任务之一,我觉得一个人做得不够频繁,以至于永远不会完全记住。)

因此,不仅仅是 webhooks 使您最终面临不一致,它们对每个人来说也是更多的工作。

那么我们还能用什么来保持两个系统的同步呢?

/events 接口

要寻找保持两个数据集和谐的灵感,我们不妨看看数据库。考虑 Postgres 的复制插槽:您为每个从数据库创建一个复制插槽,从数据库订阅该复制插槽以获取更新。

两个关键组成部分是:

  • 主数据库保持最近变更的日志
  • 主数据库保持一个游标,跟踪每个从数据库在更改日志中的位置

如果从数据库宕机,当它恢复时,可以自由地翻阅历史记录。没有队列,也没有在每端尝试像接力棒一样传递事件的工人。

API 也可以遵循这种模型。以 Stripe 为例。他们有一个 /events 接口,包含过去 30 天内对 Stripe 帐户进行的所有创建、更新和删除操作。每个事件对象都包含所操作实体的完整有效载荷。这里有一个事件的示例,针对一个 subscription 对象:

{
  "id": "evt_1J7rE6DXGuvRIWUJM7m6q5ds",
  "object": "event",
  "created": 1625012666,
  "data": {
    "object": {
      "id": "sub_JgFEscIjO0YEHN",
      "object": "subscription",
      "canceled_at": 1625012666,
      "customer": "cus_Jff7uEN4dVIeMQ",
      "items": {
        "object": "list",
        "data": [
          // ...
        ],
        "url":"/v1/subscription_items?subscription=sub_JgFEscIjO0YEHN"
      },
      "start_date": 1623826800,
      "status": "canceled",
    }
  },
  "type": "customer.subscription.deleted"
}

一些重要的特质:

  • 每个事件有一个 type,告诉我们这个事件是什么。在这个案例中,我们看到一个客户的订阅已被删除。因为包含了完整的订阅有效载荷,我们可以更新我们的数据库以反映字段如 canceled_at 和它的新 status 为 canceled
  • 每个嵌入对象包含一个 object 字段,所以我们可以轻松地提取和解析它们。
  • 事件对象大量嵌入子对象,让我们全面了解所有变化,无需轮询 API

因此,我们可以轮询 /events 来保持事物的最新状态,而不是监听 webhooks。我们只需在本地保持一个游标,我们在请求中使用它来指示给 Stripe 我们已经看到了哪些事件。

优势:

  • 如果我们宕机,我们不用担心错过 webhooks。当我们恢复时,我们可以自己的步调赶上。
  • 如果我们部署了一个错误地处理事件的错误,不用担心。我们可以部署修复,并为 /events 倒带游标,它将回放它们。
  • 我们端不需要消息总线。
  • 我们不必担心 Stripe 的 webhook 发送者延迟交付。速度掌握在我们手中。我们与最新数据之间的唯一障碍是 Stripe 在 API 层面所做的任何缓存(看起来是没有的)。
  • 我们使用简单的基于令牌的身份验证方案。
  • 我们拉取和处理事件的方式与我们处理任何其他端点的方式相同。我们可以重用许多相同的 API 请求/处理代码。

在生产者端,为了支持 /events,您需要添加监控创建/更新/删除的同样仪式,就像您会为 webhooks 使用的那样。除了,您不需要构建交付管道,您只需要将记录插入到一个仅追加的数据库表中。

在消费者端,您将需要设置一些轮询基础设施。这比如说,一个处理一切的基本 webhook 处理端点需要更多的基础工作。但是,我敢打赌一个像样的轮询系统构建起来并不比一个健壮的 webhook 处理系统难,例如,一个带有消息总线的系统。而且您得到了更好的一致性保证。

使 /events 更好

在 /events 接口中有一个明显的低效之处:为了尽可能保持实时性,您必须非常频繁地轮询。我们每个帐户每 500 毫秒轮询一次 Stripe /events 端点,并考虑将其减半。

这些请求很轻,因为对于大多数活跃的 Stripe 帐户来说,响应往往是空的。但作为程序员,我们不禁想寻找一种更有效的方式。

对于 Stripe 和其他 API 平台来说,一个创意:支持长轮询!

在长期被遗忘的长轮询艺术中,客户端发出标准的 HTTP 请求。如果服务器没有新的信息需要传递给客户端,服务器将请求保持开放,直到有新的信息要传递。

在我们与 Stripe 的集成中,如果我们可以请求 /events 并指出我们希望进行长轮询,那会很好。根据我们发送的游标,如果有新事件 Stripe 将立即返回这些事件。但如果没有,Stripe 可以保持请求开放,直到创建了新事件。当请求完成时,我们只需重新打开它并重复循环。这不仅意味着我们可以尽快获得事件,还可以减少总体网络流量。

长轮询相对于 websockets 的优势在于代码重用和简单性。大多数集成本来就涉及某种形式的轮询,无论您是在回填数据还是重播错误处理的事件。能够通过单一参数调整从例如回填切换到实时监听新事件的能力是一个巨大的胜利。

我应该使用哪个?

对于 API 消费者来说,如果您幸运地有选择使用轮询 /events 或使用 webhooks 的选择,那么该选择使用哪个主要取决于您的一致性需求。Webhooks 可以更快地开始使用,特别是如果您只关心几个 API 对象。对于某些工作流程,如果 webhooks 被丢弃也没关系,比如您正在向 Slack 频道发布“新订阅者”公告。

但随着集成的重要性增长以及确保不丢失任何东西的需求出现,我们认为轮询 /events 很难被超越。

对于 API 生产者来说,支持 /events 不仅是给您的 API 消费者的一份大礼。/events 可以轻松成为提供 webhooks 的跳板。您的 events 表可以作为您的 webhook 发送者的出站工作的“队列”。事实上,/events 可以解锁急需的 webhook 功能,比如允许您的 webhook 消费者重放或重置其 webhook 订阅的位置。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
协变珊瑚
协变珊瑚
Lv1
看不清的挽留,正如你执着地向前走
文章
4
粉丝
0
获赞
0