「翻译」利用幂等性来构建健壮和可预期的API

图片来源:http://blog.jessitron.com/2013/08/idempotence-in-math-and-computing.html

原文链接:stripe.com 作者:Brandur Leach

网络是不可靠的。我们都有连不上 Wi-Fi 或者电话突然中断的经历(译者注:看来还是国内运营商靠谱)。

服务器间的网络连接一般来说相对用户端的最后一公里网络(如:手机网络、家用宽带网络)要好很多,但是传输大量数据的时候还是会出各种奇葩的问题。停电、路由问题还有其他各种间歇性的网络失败从整体统计角度来说不是经常性的,但在固有的发生率下(原文:ambient background rate)一定会发生。

在这种本质上不可靠的环境下,设计一套失败时足够健壮,而且能保证复杂状态一致性的 API 和客户端就非常重要了。我们来看几种实现这个目标的方式。

为故障做打算(原文:Planning for failure)

仅考虑两个节点之间的远程调用,就有多种可能出现的故障:

  • 客户端尝试连接服务端的时候可能失败
  • 请求可能在服务端处理过程中失败。导致请求处理工作处在不确定的状态。
  • 请求处理成功了,但是服务端返回给客户端处理成功结果时网络连接中断了。

上面这些情况都会导致发出请求的客户端处于不确定的状态。有些情况下失败很明确,客户端可以放心的进行重试。比如无法建立链接的失败。但是很多其它情况下重试是否成功对于客户端来说是有歧义的。它不知道重试是否是安全的。一个例子就是请求处理过程中的链接断开。

这是分布式系统的经典问题。如果把「分布式系统」描述为最少两台计算机组成的通过网络互相连接并传递消息的系统,那么这里「分布式系统」的定义是很宽泛的。Stripe API 和另外一台给它发请求的服务器就能组成一个分布式系统。

灵活运用幂等性

要对付失败造成的分布式状态不一致,最简单的方法就是把服务器节点实现成幂等的。也就是说不管调用多少次,都能保证副作用(译者注:实体状态变化)只生效一次。

这样不管客户端遇到什么样的错误,都能通过不断重试来保证自己的状态和服务端的状态最终收敛一致。这样彻底解决未决失败的问题,因为客户端知道仅用一个简单的技术(译者注:重试)就能安全地处理失败。

下面给出一个例子。比如一个向某域名服务商发出的调用添加子域名 API 的 HTTP 请求:

curl https://example.com/domains/stripe.com/records/s3.stripe.com \
   -X PUT \
   -d type=CNAME \
   -d value="stripe.s3.amazonaws.com" \
   -d ttl=3600

这个请求包含了创建记录的所有信息,而且客户端可以绝对安全地多次调用。如果服务端收到请求时发现要创建的域名域名已经存在,是重复调用,那服务端就简单的忽略掉请求,然后返回操作成功的响应就好了。

按照 HTTP 的语义,PUTandDELETE动词是幂等的。并且PUT 动词专门用来表示目标资源需要用请求负载(payload)来创建或完全替换。(现代 RESTful 语境中部分修改用PATCH来表示

保证有且仅有一次的语义

尽管 HTTP 的 PUTDELETE 这种本身就幂等的语义很好地支持了很多 API 调用,那如果我们有一个需要执行一次且只能执行一次的操作呢?例如我们要设计一个向客户收款的 API,如果不小心调用了两次导致客户被收了两次款,那就太糟了。

这种时候就需要幂等键(idempotency keys)登场了。客户端发送请求时先产生一个唯一的 ID 来标识这次请求,然后和常规负载一起发送给服务端。服务端收到这个 ID 后和把它和这个请求在服务端的状态关联起来。如果客户端发现请求失败了,它会带上同一个 ID 重新请求,然后由服务端来决定怎么来处理这个请求。

我们来考虑之前给出的网络故障的例子:

  • 如果是创建连接失败,服务端收到第二个请求时发现这个 ID 是第一次收到,正常处理这个请求就好了。

  • 如果是请求处理过程中的失败,服务端需要继续处理过程。具体行为取决于系统实现。如果之前的操作被 ACID 数据库成功回滚了,那把处理过程完整重试就是安全的。否则就要把状态恢复,然后继续调用过程。

  • 如果是响应时失败(比如操作已经成功执行了,但是客户端没能收到结果),服务端就把缓存的操作成功结果返回就好了。

Stripe API 在变更节点(如我们的例子里所有的 POST 请求)上实现了幂等键。实现方式是让客户端用特殊的 Idempotency-Key header 来传一个唯一的值,以确保分布式操作的安全性:

curl https://api.stripe.com/v1/charges \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
   -H "Idempotency-Key: AGJ6FJMkGQIpHUTX" \
   -d amount=2000 \
   -d currency=usd \
   -d description="Charge for Brandur" \
   -d customer=cus_A8Z5MHwQS7jUmZ

如果上面所说的 Stripe 请求因为网络问题失败了,然后用相同的幂等键来重试,那个客户只会被收一次款。

做一个好的分布式市民

安全地处理失败是非常重要的。不仅如此,最好还要体贴细致地处理。客户端碰到网络请求失败的时候很可能是因为偶然的失败,重试一下就好了。但也能是因为更严重的问题,没那么好恢复,比如服务器因为故障停机了。这时候重试可能不仅没有效果,反而可能让情况更糟,引起进一步的降级。

客户端遇到错误时一般建议采用类似 指数延时(exponential backoff)算法的方法。客户端第一次重试前等待一个初始的时间长度,随后每次重试前等待按照 2^n 递增的时间长度,其中 n 为失败次数。通过这种方法我们就能保证客户端不会给自身难保的服务器火上浇油。

指数延时在计算机网络领域有一段很长很有趣的历史。除此之外为延时加入一些随机元素也是个好主意。如果大量客户端在相近的时间点一起出现故障,延时重试会导致它们在某些时间点集中进行重试,进而对深陷困境服务器造成很大的冲击。这个问题被称为雷暴问题

我们可以通过给客户端的重试等待时间长度加一个随机的‘抖动’来对付雷暴问题。这样客户端的重试请求可以被排布开,以给服务端一些喘息的空间。

服务端遇到大量客户端同步进行重试的时候出现的雷暴问题

制订健壮的 API 设计

构建健壮且可预期的 API 过程中,极其重要的一点就是要考虑分布式系统中可能出现的各种失败以及怎么处理失败。客户端加入重试逻辑以及在服务端实现幂等性对于实现这个目标是行之有效的。同时这两种技巧在各种技术栈中都是比较好实现的。

下面给出几条设计客户端和 API 的核心原则:

  • 确保一致地处理失败。客户端向远程服务进行重试请求时, 不这么做会导致数据的不一致,进而跟多问题随之而来。

  • 确保安全地处理失败。通过幂等性和幂等键让客户端在重试时可以传递一个唯一的标识。

  • 确保负责任地处理失败。使用类似指数延时和随机抖动的技巧。要考虑到服务端可能已经陷入降级状态。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,521评论 0 6
  • 云应用设计模式 下面的章节详细介绍了一些设计模式,这些现有的设计模式可以有效地应用到云服务应用程序设计中去。 电路...
    MagicBowen阅读 746评论 1 2
  • 转载自 Programming.log - a place to keep my thoughts on prog...
    厨子阅读 408评论 0 4
  • 【公司】浙江康意洁具有限公司 【姓名】景桃桃 【组别】235期六项精进【乐观二组】 【日精进打卡第006天】 【知...
    景桃桃阅读 181评论 0 0