因微信支付API对IPv6支持不完整引起的一次故障
作者介绍
林伟壕,网络安全DevOps新司机,先后在中国电信和网易游戏从事数据网络、网络安全和游戏运维工作。对Linux运维、虚拟化和网络安全防护等研究颇多,目前专注于网络安全自动化检测、防御系统构建。
众所周知,随着“微信红包”等热门应用的红火,微信支付在我们的生活中已无处不在。今天我要分享的内容,正是关于微信支付面向商户的API 在IPv6支持上的一个问题。关注点不仅在于问题本身,更在排查故障过程中一些思路和方法论。
故障简述
最近有同事反馈去访问微信支付API https://api.mch.weixin.qq.com/pay/unifiedorder有一个奇怪的现象,比如第一次访问url需要等到10-30s不等的时间才有结果返回,之后连续多次访问却又是毫秒级的结果返回。等到一段时间不去访问,在相同环境下再次访问又会出现第一次访问很慢,之后连续多次访问很快返回的现象。因为是比较敏感的应用,所以这样的响应时间是不可接受,我们决定刨根问底彻查一下。
故障详情
发现故障,在处理的第一步经常是做故障重现,先使用shell脚本进行测试:
for x in $(seq 6) do curl -o /dev/null -s -w %{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}"\n" -d "" "https://api.mch.weixin.qq.com/pay/unifiedorder" done |
测试结果如下:
15.527::15.539::15.642::15.642::6.000 0.004::0.016::0.127::0.127::56.000 0.004::0.016::0.121::0.121::300.000 0.004::0.016::0.121::0.121::430.000 0.004::0.016::0.121::0.121::550.000 0.004::0.016::0.121::0.121::880.000 |
可以看到,第一次连接花费了15秒+的时间,之后都是毫秒级的返回。所以如何定位首次连接耗时过长的问题是当前重点。
处理过程
节点1
由于发生这个问题之前,我们有一个内部应用也有类似问题,比如第一次访问网页需要加载超过10s才能出来,之后访问就非常快了。当时通过抓包的方式定位到同一网络中有其他IP劫持了流量,增加了中间建立连接的时间,后来去除劫持源头后才恢复正常。于是,受过往经验影响,我首先猜想是可能是网络原因或者劫持。按照这个思路,先排查发起请求的客户端所在网络,发现是独立的外网IP,没有NAT等相对复杂的网络环境。同时,切换了一个网络环境进行测试,又发现每次请求响应都很快,测试结果如下:
sh test_wxapi.sh 0.006::0.015::0.056::0.056::1952.000 0.006::0.020::0.139::0.139::783.000 0.006::0.024::0.177::0.177::617.000 0.006::0.019::0.129::0.129::845.000 0.006::0.023::0.159::0.159::686.000 0.006::0.019::0.139::0.139::781.000 |
因此网络问题仍无法彻底排查,于是决定在curl的同时抓包。
tcpdump -w test.pcap
抓包下来,用wireshark打开,看看是怎么连接建立过程是怎样的。
发现第一次请求有TCP重传,于是更加怀疑是网络问题,但从这里又无法直观看到是具体原因。所以开始往客户端所在服务器环境进行排查。
节点2
处理过程中同样是想起,之前遇到一个iptables规则配置不合理导致数据包梳理效率很低的问题,所以一开始在服务器环境排查时优先检查了iptables配置规则,发现也挺合理的,对于首次建立的连接没有set tag,也没有对New有特别设置,然后又临时清除了所有iptables规则发现问题依旧,于是跳过iptables。
iptables -nL Chain INPUT (policy ACCEPT) target prot opt source destination ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED ... DROP all -- 0.0.0.0/0 0.0.0.0/0 |
节点3
到了这里,我开始怀疑DNS解析异常也会引起首次建立连接耗时过长的问题。于是开始检查/etc/resolve.conf,不管是检查配置,还是更换DNS服务器问题都是一样的。但是,“有心栽花花不开,无心栽柳柳成荫”,这个过程中我还是抓了包,这么一来,发现IPv6地址解析异常了!
可以看到,DNS服务器返回的IPv6解析结果是”server failure AAAA”,而且耗时超过5s,这里就有问题了。
节点4
下面开始把焦点集中到微信支付域名api.mch.weixin.qq.com的IPv6解析上,发现有SERVFAIL,而且重现率很高。
time dig api.mch.weixin.qq.com AAAA ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 59019 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;api.mch.weixin.qq.com. IN AAAA real 0m10.013s user 0m0.008s sys 0m0.004s |
同时指定了IPv4的解析,发现A的查询很快:
;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35327 ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 4, ADDITIONAL: 12 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;api.mch.weixin.qq.com. IN A ;; ANSWER SECTION: api.mch.weixin.qq.com. 245 IN CNAME forward.qq.com. forward.qq.com. 22 IN A 140.207.69.101 forward.qq.com. 22 IN A 140.207.69.102 real 0m0.016s user 0m0.012s sys 0m0.004s |
既然确定了IPv6解析有问题,IPv4解析正常,于是修改了相关代码,确认强制域名解析IPv4的话,都是正常的。
原因分析
从DNS原理看问题
1、整个DNS解析过程
如上图,当一个客户端发起DNS解析请求时,会有一个优先级的,按照本地DNS缓存->/etc/hosts的DNS配置->isp或本地域名解析缓存服务器->根域名服务器->顶级域名服务器->...->目标站点权威服务器的顺序一路解析下来。
结合本次故障来看,问题应该是在权威服务器这里。究竟如何,且看下文解说。
先来看看对比的解析结果:
A.api.mch.weixin.qq.com的IPv4解析结果
dig api.mch.weixin.qq.com A +trace weixin.qq.com. 86400 IN NS ns-cnc1.qq.com. weixin.qq.com. 86400 IN NS ns-cnc2.qq.com. api.mch.weixin.qq.com. 600 IN CNAME forward.qq.com. forward.qq.com. 120 IN A 123.126.122.46 forward.qq.com. 120 IN A 125.39.133.115 |
B.api2.mch.weixin.qq.com的IPv4解析结果
$ nslookup api2.mch.weixin.qq.com Non-authoritative answer:: api2.mch.weixin.qq.com Addresses: 182.254.10.62 182.254.12.36 |
C.api.mch.weixin.qq.com的IPv6解析结果
dig @ns-tel1.qq.com weixin.qq.com AAAA weixin.qq.com. ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 53871 api.mch.weixin.qq.com. 600 IN CNAME forward.qq.com. |
D.api2.mch.weixin.qq.com的IPv6解析结果
dig api2.mch.weixin.qq.com AAAA weixin.qq.com. 30 IN SOA ns-tel1.qq.com. webmaster.qq.com. 1293502040 300 600 86400 300 |
从上面的测试结果可看出,api2.mch.weixin.qq.com IPv6解析正常,api.mch.weixin.qq.com.IPv6解析失败是因为多了一层:
api.mch.weixin.qq.com. 600 IN CNAME forward.qq.com.,然后 forward.qq.com AAAA 的解析返回是错误的,这里是权威服务器处理失败了。
为什么客户端解析的是 api.mch.weixin.qq.com AAAA,而缓存回去查询 forward.qq.com AAAA 呢?
这是因为 api.mch.weixin.qq.com. 600 IN CNAME forward.qq.com. 这条记录被缓存了,导致后边后续的查询都会变成查 forward.qq.com 对应的记录。
下面再对比一下解析www.163.com和api.mch.weixin.qq.com看看在权威服务器层面有何不同。
#+++++++++++++++++++++++++++++++++++++++++++ dig www.163.com AAAA ;www.163.com. IN AAAA ;; ANSWER SECTION: www.163.com. 600 IN CNAME www.163.com.lxDNS.com. www.163.com.lxDNS.com. 216 IN CNAME 163.xdwscache.ourglb0.com. ;; AUTHORITY SECTION: ourglb0.com. 30 IN SOA DNS1.ourglb0.org. webmaster.ourglb0.com. 1107011041 10800 3600 64800 60 #+++++++++++++++++++++++++++++++++++++++++++ dig api.mch.weixin.qq.com AAAA @ns-cnc1.qq.com. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9615 ;; WARNING: recursion requested but not available ;; AUTHORITY SECTION: weixin.qq.com. 43200 IN SOA ns-tel1.qq.com. webmaster.qq.com. 1293502040 300 600 86400 300 |
这里api.mch.weixin.qq.com的AAAA 解析返回有问题,是SOA,按协议应该还是返回 CNAME 记录;可以对比 www.163.com 的解析结果发现后者就是返回的SOA。
我们会注意到api.mch.weixin.qq.com status的值 NOERROR ,如果一个返回为 NOERROR 且 没有 ANSWER SECTION,按照 (https://tools.ietf.org/html/rfc2308#section-2.2)这是一个 NO DATA 返回,表明这是一个指示返回(例如根权威返回顶级权威的 NS)或者是域名记录不存在(例如查询 163.com AAAA)。
从上文可以看到 forward.qq.com. 实际上是做了一层 NS,虽然没有真正的授权出去(ns-os1.qq.com. 上没有 forward.qq.com SOA 记录),但缓存看来,它已经授权了一层,而返回的 SOA 有误,因此缓存服务器无法判定该 NODATA 返回的类型,所以认为该返回不合法,最终导致了api.mch.weixin.qq.com和forward.qq.com 的AAAA记录一直返回 SERVFAIL,所以缓存服务器会尝试继续解析,从而导致首次解析时间过长。等到之后解析成功后,就使用的本地缓存,所以解析很快,等到缓存失效后,问题就会重现。
2、关于curl的DNS解析顺序
一般来说,如果服务器开启了IPv6,curl默认会优先解析 IPv6,在对应域名没有 IPv6 的情况下,会等待 IPv6 DNS解析失败 timeout 之后才按以前的正常流程去找 IPv4。DNS服务都是支持解析IPv6的,如果没有设置IPv6解析,查询结果就是空,不会是serverfail。
3、关于nslookup和dig的工作原理
不同操作系统nslookup和dig的原理并不同,比如Win7的nslookup每发一次IPv4请求就会产生2个A记录请求,换到Win XP下nslookup没发一次IPv4请求则只会产生一个A记录请求,所以在抓包分析时需要根据不同操作系统的应用特性进行区分。
微信原来已有解决方案
针对这个问题,其实微信官方文档
已经给出了答案。
商户访问微信支付
商户侧使用api.mch.weixin.qq.com(主)和api2.mch.weixin.qq.com(备)这两个支付api域名访问微信支付,这两个域名功能完全一样。
这两个域名分别配hosts绑定专线访问微信的两个131开头的策略IP,具体电信IP和联通IP那个绑定到api和api2, 商户侧可以测速后自己决定。
商户侧的访问支付接口的代码逻辑需具备失败换域名重试的机制,即访问api.mch.weixin.qq.com不通的话,就换成api2.mch.weixin.qq.com重试。
商户侧访问微信支付时,若两条专线都不通,要有能切回走公网访问的能力。若无法走公网,则商户侧需要知晓这里的风险。
可行的解决办法
关闭服务器IPv6功能
1、使用ifconfig验证是否开启IPv6:
eth0 Link encap:Ethernet HWaddr 00:13:D4:05:B2:ED inet addr:119.119.xxx.xx Bcast:119.119.115.255 Mask:255.255.255.0 #ipv4 inet6 addr: fe80::213:d4ff:fe05:b2ed/64 Scope:Link #IPv6 |
2、查看服务监听的IP验证是否开启IPv6:
netstat -nulp tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN #ipv4 tcp 0 0 :::8080 :::* LISTEN #IPv6 |
3、使用lsmod验证是否开启IPv6:
[root@linux ~]# lsmod |grep ip ip6t_REJECT 9409 1 ip6table_filter 6849 1 ip6_tables 18053 1 ip6table_filter |
关闭IPv6:修改/etc/modprobe.conf加入如下的两条,重启系统即可。
alias net-pf-10 off
alias IPv6 off
开启IPv6:IPv6是默认支持的,所以只需将/etc/modprobe.conf中的上面两条指令注释掉即可。
在/etc/hosts文件中指定域名ip解析记录
按照下面格式配置,但是如果指定的服务器故障了,业务也会受牵连,这不是很稳妥的方法。
api.mch.weixin.qq.com 183.3.235.18
在代码中指定域名解析使用ipv4结果
比如PHP:
if(defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) |
使用微信官方方案,设置主备域名进行冗余,支持域名解析失败切换。
经验教训
熟读用户文档
很多人学习技术知识时喜欢在搜索引擎找别人的博客或者问题解答来作为知识学习的入口,其实这里有个问题,博客或者问题答案经过别人的消化,一方面提高了易读性,另外也引入了偏见误解,同时无法保证完整性。反观官方的用户文档,特别是外文的,易读性相对差,不过很权威也很全面,很多细节问题都有。但很多人不愿意好好看用户文档,而是通过网络上很零碎的知识去学,这样难免有遗漏。像这次我们使用微信支付API过程中用户文档没有看到这一块,绕了一圈,反而增加了学习成本。因此,学习或者使用新系统新技术,熟读用户文档反而是一个很基础的地方呢。
不能思维固化
从上面的处理过程来看,可以发现由于之前有类似问题的处理经验,一开始我往网络或劫持,或iptables等方向去定位问题,并没有带来太大作用。很多时候有相关经验起码能提供思路,不失为一种优势。而有时候,太重视经验也会导致思维固化,没能把基础的信息全面收集后再做定性,后做定位,就会事倍功半。
参考资料
http://www.jb51.net/article/39788.htm
http://blog.csdn.net/ccscu/article/details/7814028
http://2014.54chen.com/blog/2016/06/18/wexin-resolve-slowly/
相关专题:
◆ 近期热文 ◆
踩坑CBO,解决那些坑爹的SQL优化问题
2017年数据架构师架构选型必读
当拳皇遇上数据库,会擦出什么样的火花?
运维DBA的4大纪律9项注意
冻结时间倒数前一小时,记一次步步惊心的SQL优化
◆ MVP专栏 ◆