###出现故障
昨天下午的时候,在微博上有人私信我。原来是一个用户(应该是在oschina上找到了BlackHole),在公司内网使用了BlackHole作为内部DNS Server(因为BlackHole配置比较简单嘛),然后发现,在大规模访问量下,会出现浏览器破页的现象,而且越来越严重。
当时第一反应很开心,自己鼓捣的这个东西确实派上了用场。后来跟他沟通,与我偶尔遇到的情况是一样的:浏览网页的时候偶尔会有DNS解析不到,使用nslookup结果无问题,但是在终端下ping显示找不到host。使用sudo killall -HUP mDNSResponder刷新系统缓存后,该请求解析正常。
###初步检查
怀疑是操作系统缓存了错误的结果。因为BlackHole在实现的时候有一个trick的技巧:在转发请求的时候,因为发送和返回的设计是异步的,所以需要一个key来将请求和响应对应起来。因为使用了Map结构,所以担心条目太多导致内存泄漏,所以直接使用了DNS头的ID作为key,这个ID是一个16位的整数,空间足够小,不用担心泄漏问题。但是特殊情况下,如果ID冲突,甚至可能发生返回错误的响应的问题。
但是后来进行了尝试,对某个请求,返回伪造的相应体,并分别尝试了question区伪造和answer区伪造两个方法。但是发现,操作系统能够检查出错误的响应体并进行过滤,然后再重试!于是,这个假设的故障其实是不存在的。
###按部就班
感觉“蒙”的方法总是不准确,还是老老实实按部就班的来吧。个人觉得找bug最好的方法就是重现并记录现场,比没头苍蝇乱找,或者一遍一遍看代码靠谱多了。发现chrome自带了一个工具net-internel可以查看DNS缓存情况,打开之后发现,确实有部分DNS出现了*error: -105 (ERR_NAME_NOT_RESOLVED)*的错误。
因为错误情况非常少且跟输入无关,靠debug是行不通了,只能靠log。在程序中将一个DNS query从接收到转发、返回都打印了log。经过测试发现,原来某些请求返回了空结果!
原因是我模仿Servlet的ServletContext机制,使用了一个ThreadLocal来记录一些状态,其中就有一条是:ServerContext.hasRecord(),表示是否已经存在答案体。结果这个设计并未经过仔细推敲,里面存在一个重大问题:ThreadLocal是单个线程对应的上下文,而我在主方法中使用了线程池ThreadPoolExecutor,而实际上线程池的线程是复用的!也就是说我这次请求,会拿到另外请求的上下文,所以有些请求本来没有记录,却当作有记录,结果返回了空响应!而且因为这个ThreadLocal变量没有清理机制,所以后来会有越来越多的空响应。
而DNS server返回空响应(有完整的header和question)也就相当于说,我正常处理了这条请求,但是这个domain是没有数据的,你下次别查了。操作系统的甄别能力是有限的,遇到这个空响应,它就傻傻的相信了,并且缓存了起来。
至此故障成功定位。解决方案:去掉这个半成品的ServerContext,改为参数传递。改天详细研究一下Servlet的上下文保存机制才行。
###总结:操作系统DNS缓存及重试机制
经过这次排查,也发现了操作系统DNS的一些机制:
-
操作系统能够在一定范围内识别不正确的DNS响应(DNS头ID、question区、answer区name错误),并进行重试。
-
操作系统会缓存空记录。
-
某些浏览器(例如chrome)会在DNS返回空记录的情况下,让缓存立即过期。这相当于浏览器级别的重试机制。浏览器是能够清除操作系统缓存的。
经过这番折腾,BlackHole之前一直存在的一个问题也算是解决了。有人把它应用到企业内网,还是挺令人鼓舞的。