深入理解 Golang HTTP Timeout

深入理解 Golang HTTP Timeout

背景

前段时间,线上服务器因为部分微服务提供的 HTTP API 响应慢,而我们没有用正确的姿势来处理 HTTP 超时(当然,还有少量 RPC 超时), 同时我们也没有服务降级策略和容灾机制,导致服务相继挂掉?。服务降级和容灾需要一段时间的架构改造,但是以正确的姿势使用 HTTP 超时确是马上可以习得的。

超时的本质

所有的 Timeout 都构建于 Golang 提供的 Set[Read|Write]Deadline 原语之上。

服务器超时

server timeout

  • ReadTimout 包括了TCP 消耗的时间,可以一定程度预防慢客户端和意外断开的客户端占用文件描述符
  • 对于 https请求,ReadTimeout 包括了 TLS 握手的时间;WriteTimeout 包括了 TLS握手、读取 Header 的时间(虚线部分), 而 http 请求只包括读取 body 和写 response 的时间。

此外,http.ListenAndServe, http.ListenAndServeTLS and http.Serve 等方法都没有设置超时,且无法设置超时。因此不适合直接用来提供公网服务。正确的姿势是:

客户端超时

client timeout

  • http.Client 会自动跟随重定向(301, 302), 重定向时间也会记入 http.Client.Timeout, 这点一定要注意。

取消一个 http request 有两种方式:

  1. Request.Cancel
  2. Context (Golang >= 1.7.0)

后一种因为可以传递 parent context, 因此可以做级联 cancel, 效果更佳。代码示例:

Credits

  1. The complete guide to Go net/http timeouts
  2. Go middleware for net.Conn tracking (Prometheus/trace)

从支付宝到支付鸨,信用的双杀

从支付宝到支付鸨,信用的双杀

似乎每年年底支付宝都要为了 KPI 放大招。去年的支付宝几乎全盘「复制」微信聊天和朋友圈,今年的大戏变成了「陌生人圈组」——校园日记,白领日记,海外生活,点进去一下差点以为进了陌陌。注意,我说的是「复制」,不是「抄袭」,评价产品一定要回归它解决的问题;我并不是看不起陌陌,而是产品不能背离用户对产品的认知。

PC 互联网时代,圈组经营得最小清新的是豆瓣。移动互联网时代,小密圈是玩得最优逼格的。支付宝的各种日记,是目前为止见过的最掉节操的。一句「支付宝产品经理都是S13」的智力偷懒吐槽显然暴露了你缺乏思考的习惯,虽然我不反对。支付宝日记的推出本质已然是对齐工具属性的担忧,对社交领域的眼红。这是一个值得尝试的方向,但是却用了很差劲的方式——通过仅允许女性晒照「rou」的方式,鼓励陌生人之间评价,打赏,添加好友建立关系。嗯,我说的是一般的支付宝好友关系,虽然你可能不信。从数据看,这个做法实在是「高明」,但这吃相真的是非常难看。引导人心善恶都有可能取得商业的成功。有一大把选择的时候,为何不站着有尊严的赚钱呢?况且这种方式并一定比这种难看的吃相利益低。

至于支付工具,人们心中是有一个不一样的「安全感」期望的。日记的粗暴引入,在用户关系数据飙升的背后,其实是在消耗支付宝品牌的信用。支付宝评论中的头像是可以点击查看详情的,虽然部分敏感字段做了打码处理,但是对于做黑产的人来说这简直是天赐良机。微信的公众号是不能点击详情的,微信群可以查看详情,但是添加时候会显示来源,想想为什么会有这个细节的不同。

圈组最头疼的就是 spam. 支付宝的方法「高明」得让你怀疑人生:仅芝麻信用750以上的可以评论。你说芝麻信用750以上的绝大部分会洁身自好,但是你怎么能无视遍及中华大地的刷子呢。你看现在评论里面的 spam 有多少是赤裸裸的说互加好友提升芝麻信用,甚至有高芝麻分用户已经把这个变成了一门生意。曹政说,征信是一门大生意,而我之前一直以为芝麻信用是拥有绝好基础和机会的,无论是数据规模和纬度还是本身的土壤。鄙人也很小心的对待自己的芝麻信用,闲鱼、58交易都会把芝麻信用当成一个硬指标,低于某个分支的买/卖家都会毫不客气的屏蔽掉。但是,经过这一轮刷子的洗礼,信用的通胀堪比当年的金融危机。然而,我并不幸灾乐祸,而是真的替支付宝感到可惜。

Golang中WaitGroup使用的一点坑

Golang中WaitGroup使用的一点坑

Golang 中的 WaitGroup 一直是同步 goroutine 的推荐实践。自己用了两年多也没遇到过什么问题。直到一天午睡后,同事扔过来一段奇怪的代码:

坑1

撇了一眼,觉得没什么问题。然而,它的运行结果是这样:

或这样:

或这样:

一度让我以为手上的 mac 也没睡醒……
这个问题如果理解了 WaitGroup 的设计目的就非常容易 fix 啦。因为 WaitGroup 同步的是 goroutine, 而上面的代码却在 goroutine 中进行 Add(1) 操作。因此,可能在这些 goroutine 还没来得及 Add(1) 已经执行 Wait 操作了。

于是代码改成了这样:

坑2

然而,mac 又睡了过去,而且是睡死了过去:

wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done操作是在 wg 的副本执行的。因此 Wait 就死锁了。于是代码改成了这样:

填坑

至此,午睡终于睡醒了。Sigh…