本文来源: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)