Gem 记录使用 typhoeus 调用 360 HTTPS API 时碰到的坑

doitian · 2015年09月26日 · 最后由 karma 回复于 2015年10月20日 · 5113 次阅读
本帖已被管理员设置为精华贴

--- 本文原文发布在 https://www.3pjgames.com/archives/229

第一款游戏上线之后,陆续发现了一些诡异的问题。其中一个是通过 360 SDK 登录的用户在进行服务器验证的时候,偶尔会出现 HTTP 客户端没有收到任何回应就返回的情况。怪异的是当尝试手动发送请求的时候,却无法重现,即使是用脚本模拟不停的发送请求。后来陆续发现其它一些渠道,像豌豆荚也出现类似情况。

经过差不多一天的研究和分析,最终发现是这些登录验证服务提供的是 HTTPS 接口,但是可能服务器版本比较老,并没有完全符合 HTTPS 协议中的一些标准。而我们使用的客户端 typhoeus 做了非常多的性能优化,其中对 HTTPS 的一项优化最终导致了 TLS 握手失败,客户端没有收到任何回应就返回了。

TLDR; 问题出在 typhoeus 启用了 SSL session id cache,而这些渠道提供的 HTTPS 服务器未能正确处理该标准 (RFC5246)。解决方案只能是牺牲性能,将连接选项中的 :ssl_sessionid_cache 设置为 false。Typhoeus 是对 libcurl 的封装,对应的选项是 CURLOPT_SSL_SESSIONID_CACHE。如果使用了其它基于 libcurl 的 HTTP 客户端启用了这项优化,也需要找到地方关闭这个选项。

变通方案

我们通过统一的网关来集成各种第三方 SDK 的登录验证,该服务使用 Ruby 实现,使用了 HTTP 客户端框架 Faraday,底层传输使用了 typoeus,而 typoeus 又是对 libcurl 的封装。

问题发生之后,首先想到的还是网络问题,另外线上问题需要快速解决,所以首先加上了自动重试。Faraday 已经提供了 retry 中间件,只需要在出现这种问题的时候抛出特定类型的异常就可以了。因为没返回结果的时候 status code 是 0,加个中间件去检查 status code 就行了。

当 status 为 0 的时候抛出 EmptyResponseError

class EmptyResponseError < HttpSdkError; end
class CheckEmptyResponse < Faraday::Response::Middleware
  def on_complete(env)
    if env[:status].to_i == 0
      raise EmptyResponseError
    end
  end
end

当抛出 EmptyResponseError 的时候重试两次。注意 response 中间件是以相反的顺序处理,中间件 CheckEmptyResponse 加到最后面让它最先处理返回结果。

conn = Faraday.new('https://openapi.360.cn') do |builder|
  builder.request :retry, max: 2, interval: 0.05,
    interval_randomness: 0.5, backoff_factor: 2,
    exceptions: [EmptyResponseError]

  builder.request :json
  builder.response :json
  builder.use CheckEmptyResponse

  builder.adapter :typhoeus 
end

加了之后还是会出现连续 3 次都是空结果的情况,但是频率已经大大降低。

定位问题

加上重试问题的影响大大降低,也赢得时间去彻底解决问题。

我们自架了 Sentry 来收集各种异常,问题最初就是通过 Sentry 的通知发现的。但是因为出错时,HTTP 客户端没有收到返回,所以完全无法得知原因,而手动改送请求或者通过 Sentry 的『重放』都能拿到正确的返回结果,无法重现。

首先想到的是加日志,于是在向 360 发送请求时开启了 Faraday 的中间件 logger 来打印更详细的日志,但是依然未能发现问题。于是只能去看 typoeus 的源代码,发现可以通过全局设置来显示更多的信息:

Typhoeus::Config.verbose = true

但是另外一个问题是没办法手动去重现,只能等着问题发生。虽然这个问题隔一段时间就会出现,可是有时候要等上几个小时。这会导致添加日志和尝试修改之后确认的反馈环节被拉长,可以要几天才能有点眉目,很难排除无效的修复方案。

之前提到过用脚本模拟循环发也没办法重现,但是线上却一定会发生,于是想到通过解析日志,重发所有 360 的请求。这个方法果然有效,终于能比较快速的去获得反馈了,而且可以在出现问题的时候,在脚本中插入 pry 进行检查,大大加快了定位问题的速度。最终通过 Typhoeus 的日志可以确定问题是因为 TLS 握手失败了

Hostname was found in DNS cache
  Trying 220.181.132.231...
Connected to openapi.360.cn (220.181.132.231) port 443 (#2)
successfully set certificate verify locations:
  CAfile: none
  CApath: /etc/ssl/certs
SSL re-using session ID
SSLv3, TLS handshake, Client hello (1):
...

 SSLv3, TLS alert, Server hello (2):
Ferror:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number
Closing connection 2

解决问题

找到问题原因但是怎么解决还是一头雾水,求助 Google 也基本说得是证书问题。然而关闭 peer verification 并没有什么用。也有提到 openssl 版本问题的,安装了 libcurl4-openssl-dev,更新了 gems faradaytyphoeus 也没有任何用。

不过对比成功和失败请求的 Typoeus 请求,注意到所有空结果都会包含

SSL re-using session ID

一开始以为是 keep alive 导致的问题,关闭之后也没能解决问题。最后定位到 RFC5246,而且在 Mac 下 man curl 也提到

--no-sessionid (SSL) Disable curl's use of SSL session-ID caching. By default all transfers are done using the cache. Note that while nothing should ever get hurt by attempting to reuse SSL session-IDs, there seem to be broken SSL implementations in the wild that may require you to disable this in order for you to succeed. (Added in 7.16.0) Note that this is the negated option name documented. You can thus use --sessionid to enforce session-ID caching.

简要来说就是 sessionid 缓存不会带来任何坏处,但是这个世界上还有很出有问题的 SSL 实现,这时可以通过该选项禁用。

通过这个线索去 typhoeus 源码里去搜索相关选项,最终找到 ssl_sessionid_cache。不过因为 faraday 没有暴露出该选项的设置,所以采用了 monkey pack 的方式强制将该选项设置为 false

require 'typhoeus/adapters/faraday'

module TyphoeusRequestPatch
  def request(env)
    req = super(env)
    req.options[:ssl_sessionid_cache] = false
    req
  end
end

Faraday::Adapter::Typhoeus.prepend TyphoeusRequestPatch

测试之后问题解决。

这也说明了为何手动难重现,因为这个问题只会在 keep alive 的连接失效,而 session id 的缓存又还没失效的窗口之间发生。

后记

国内环境中使用 HTTPS 各种坑,这里提到的是服务端的问题。另外在手机客户端上也是一堆问题,比如相当一部份的国产 Android 系统手机上不信任 GoDaddy 颁发的证书,有些低端机上发送 HTTPS 请求会静默失败,没有返回结果也不报任何错。所以不管前端、后端,碰到了 HTTPS,预先考虑各种情况,记录下详细日志方便日后调试。

@doitian 非常精彩的问题分析,受教了! 👍

faraday 虽然支持比较丰富,但是没有直接用 typhoeus 方便。

@doitian 在之前的项目中也是通过 Faraday 去适配 Typhoeus 的,没有直接用过。 按照 #2 楼 @nouse 的建议,直接用 Typhoeus 是不是更方便呢?

#2 楼 @nouse 要接几十个不同的渠道,Faraday 的 middleware 架构会比较方便写客户端

很有用,👍

太喜欢 typhoeus 了

需要 登录 后方可回复, 如果你还没有账号请 注册新账号