导语:CloudFlare是一家美国的跨国科技企业,总部位于旧金山,在英国伦敦亦设有办事处。CloudFlare以向客户提供网站安全管理、性能优化及相关的技术支持为主要业务。
CloudFlare是一家美国的跨国科技企业,总部位于旧金山,在英国伦敦亦设有办事处。CloudFlare以向客户提供网站安全管理、性能优化及相关的技术支持为主要业务。通过基于反向代理的内容传递网络及分布式域名解析服务,CloudFlare可以帮助受保护站点抵御大多数网络攻击,同时提升网站的性能、访问速度以改善访客体验。
闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒)。
而在几天前,协调世界时(UTC)给格林尼治标准时间2016年12月31日23时59分增加了一秒,而这多出的第61秒在2017年的午夜给 Cloudflare的自定义RRDNS软件造成了很大的恐慌——使其一个数字变成了负数,原本最糟糕的情况本应该是零,但显然这比那更糟,不过此次的过程以及影响被其使用Go语言的恢复功能所捕获了,从最终的情况来看这次的事件导致了某些使用Cloudflare托管网络资源的网站出现了DNS解析失败的情况。
增加一秒钟造成了非常严重的后果
不过,该问题仅影响了使用CNAME DNS记录与Cloudflare的客户,并且只影响到了Cloudflare 102个数据中心中的少数机器。在峰值时,大约有0.2%的对Cloudflare的DNS查询受到影响,并且对Cloudflare的所有HTTP请求中只有不到1%遇到了错误。
这个问题很快就被发现了,并且受影响最严重的机器在90分钟内进行修补,修复在1日的06:45 世界范围内推出了。对此我们感到很抱歉,因为我们的客户受到影响,但我们认为很有必要去写下此次问题的根本原因,使大家能够更好的去进行理解这一问题:
Cloudflare客户往往会使用我们的DNS服务为其域的DNS查询提供权威答案。他们需要告诉我们原始Web服务器的IP地址,以便我们可以联系服务器来处理非缓存的请求。他们这样做有两种方式:他们输入与名称相关联的IP地址(例如,example.com的IP地址是192.0.2.123,并作为A记录输入)或他们输入CNAME(例如example.com是origin -server.example-hosting.biz)。
此图片显示的测试网站包含aburritobot.com的A记录以及直接指向Heroku的www.theburritobot.com的CNAME。
当客户使用CNAME选项时,Cloudflare偶尔会使用DNS查找原始服务器的实际IP地址。而它使用标准递归DNS自动执行此操作,正是这个CNAME查找代码包含了导致中断的错误。
在内部,Cloudflare操作DNS解析程序从Internet查找DNS记录,RRDNS与这些解析程序会话,以在执行CNAME查找时获取IP地址。 RRDNS跟踪内部解析器执行的性能,并对可能的解析器进行加权选择(我们为每个数据中心冗余运行多个),选择其中性能最好的。而这些选择中的一些最终在闰秒期间在其数据结构中记录一个负值。
加权选择代码在一个稍后的节点会发现此负数,从而导致其发生恐慌。事实上负数通过闰秒以及进行平滑的组合得到的。对于时间的惯性认识导致了这一问题。实际上,影响我们的DNS服务错误的根本原因是我们始终相信时间不能倒退。在我们的例子中,假定一些代码两次之间的差异总是最差的,那么它也应该为零。
验证问题
RRDNS是在Go中编写的,并使用Go的time.Now()函数来获取时间。不幸的是,这个函数不保证单调性—— Go目前不提供单调的时间源。
在测试用于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服务器,然后解决任何重现的问题。
以下是关于闰秒bug事件的完整时间表。
2017-01-01 00:00 UTC影响开始 2017-01-01 00:10 UTC上报给工程师 2017-01-01 00:34 UTC问题确认 2017-01-01 00:55 UTC减缓措施部署到一个canary节点并确认 2017-01-01 01:03 UTC减缓部署到canary数据中心并确认 2017-01-01 01:23 UTC修复部署在最受影响的数据中心 2017-01-01 01:45 UTC修复部署到主要数据中心 2017-01-01 01:48 UTC Fix正在部署 2017-01-01 02:50 UTC修补方案推出到大多数受影响的数据中心 2017-01-01 06:45 UTC影响结束
此图表显示每个Cloudflare数据中心的错误率(一些数据中心比其他数据中心受到更多影响),以及部署修订时错误的快速下降。我们部署了修复,优先处理那些具有最多错误的位置。
结论
我们很抱歉使我们的客户受到这一问题的影响,目前我们正在检查所有代码,以确保没有其他相关问题。