本文来源:IT之家(今日头条)
在新年第一天协调世界时(UTC)子夜,在Cloudflare的自定义RRDNS软件内部深处,一个数字本该在最糟糕的情况下总是为零,结果却变成负数。稍后,这个负值引起RRDNS“恐慌”。这个恐慌是使用Go语言的恢复特性而发现的。最终影响就是,为Cloudflare的一些托管网站提供的一些DNS解析以失败告终。
这个问题只影响了使用CNAME DNS记录的Cloudflare客户,只影响了Cloudflare的102个接入点(PoP)处的少量机器。高峰时段,向Cloudflare发出的DNS查询中大约只有0.2%受到了影响,向Cloudflare发出的所有HTTP请求中不到1%遇到了错误。
这个问题很快就被发现了。大多数受到影响的机器在90分钟内打上了补丁,等到UTC 06:45,修复程序已向全球用户发布。我们对客户受到了影响深表歉意,但是我们认为有必要叙述根源,让别人明白来龙去脉。
Cloudflare DNS方面先介绍一下
Cloudflare的客户使用我们的DNS服务,以便为其域名提供DNS查询的权威答案。他们需要告诉我们其原始Web服务器的IT地址,那样我们才能联系上服务器,处理非缓存请求。他们以双向方式做这项工作:要么输入与名称有关的IP地址(比如example.com的IP地址是192.0.2.123,并作为一条A记录来输入),要么输入CNAME(比如example.com是origin-server.example-hosting.biz)。
这张图显示的一个测试网站既有theburritobot.com的A记录,又有www.theburritobot.com的CNAME,直接指向Heroku。
客户使用CNAME这种选择时,Cloudflare偶尔要执行查询,使用DNS,查询原始服务器的实际IP地址。它是使用标准的递归DNS自动执行这项操作的。含有导致故障的那个软件错误的正是这个CNAME查询代码。
在内部,执行CNAME查询时,Cloudflare运行DNS解析器,查询来自互联网的DNS记录,以及RRDNS与这些解析器之间的对话,以便获得IP地址。RRDNS跟踪记录内部解析器的性能有多好,并对可能的解析器(我们每个接入点运行多个解析器,以确保冗余性)进行权重选择,选择性能最好的那个解析器。其中一些解析最后在数据结构中记录下了闰秒期间的一个负值。
权重选择代码在稍后被馈送到这个负数,因而引起了恐慌。负数是通过闰秒和平滑处理(smoothing)这两个因素出现在那里的。
程序员在时间方面的错误认识
影响了我们DNS服务的那个错误的根源出在时间不会倒退这一观念上。以我们为例,一些代码想当然地以为:在最糟糕的情况下,两个时间之间的时差总是为零。
RRDNS是用Go编写的,使用Go的time.Now函数来获得时间。遗憾的是,这个函数并不保证单调性(monotonicity)。Go目前并不提供单调的时间源(详情请参阅https://github.com/golang/go/issues/12914)。
在评估用于CNAME查询的上游DNS解析器的性能时,RRDNS含有下列代码:
// Update upstream sRTT on UDP queries, penalize it if it fails
if !start.IsZero {
rtt := time.Now.Sub(start)
if success && rcode != dns.RcodeServerFailure {
s.updateRTT(rtt)
} else {
// The penalty should be a multiple of actual timeout
// as we don't know when the good message was supposed to arrive,
// but it should not put server to backoff instantly
s.updateRTT(TimeoutPenalty * s.timeout)
}
}
在上述代码中,如果time.Now早于start(这是由调用早些时候的time.Now()来设定的),rtt可能是负数。
如果时间往前进,这个代码可顺畅运行。遗憾的是,我们将解析器的速度调整得非常快,这意味着它们在几毫秒内回答是很正常的事。如果解析发生时,时间后退一秒,所谓的解析时间会是负值。
RRDNS根本不为每个解析器保留单一的度量指标,它拿来许多度量指标后,对它们进行平滑处理。所以,单一的度量指标不会引起RRDNS认为解析器在负时间内工作,但是经过几个度量指标后,经过平滑处理的值最终会变成负值。
当RRDNS选择上游解析CNAME时,它使用一种权重选择算法。代码拿来上游时间值后,将它们馈送到Go的 rand.Int63n函数。如果变量是负值,rand.Int63n就会立即恐慌。这就是造成RRDNS恐慌的根源。
(另外,程序员在时间方面还有其他的许多错误认识)
只需一个字符的修复程序
使用非单调性时钟源时要防范的一个地方就是,始终要检查两个时间戮之间的差异是不是负数。要是出现这种情况,除非时钟停止倒回,否则就不可能准确地确定时间差。
在这个补丁中,我们让RRDNS忘记目前的上游性能,如果时间向后跳过,让它再次规范化。这可以防止负数泄露给服务器的选择代码,从而避免在试图联系上游服务器之前导致抛出错误信息。
我们采用的修复办法防止服务器选择代码记录负值。重启所有的RRDNS服务器,然后修复这个问题再次出现的毛病。
时间表
下面是闰秒错误方面的事件的完整时间表:
2017-01-01 00:00 UTC 影响启动
2017-01-01 00:10 UTC 上报给工程师
2017-01-01 00:34 UTC 确认问题
2017-01-01 00:55 UTC 缓解措施部署到一个金丝雀代码上,并加以核实
2017-01-01 01:03 UTC 缓解措施部署到金丝雀接入点上,并加以核实
2017-01-01 01:23 UTC 修复程序部署到大多数受影响的接入点
2017-01-01 01:45 UTC 修复程序部署到主要的接入点
2017-01-01 01:48 UTC 修复程序部署到每个地方
2017-01-01 02:50 UTC 修复程序部署到大多数受影响的接入点
2017-01-01 06:45 UTC 影响已消除
这张图显示了每个Cloudflare接入点的错误率(一些接入点受到的影响比其他接入点要大),修复程序部署后,错误率迅速下降。我们将修复程序优先部署在错误最多的那些地方。
结束语
我们为我们的客户受到这个错误的影响而表示歉意,我们在检查所有代码,确保时间间隔没有出现其他对闰秒敏感的地方。