获得 DNS 根节点的状态

DNS 表面上虽然是去中心化的,实际上,仍然存在一定的中心化,这个中心化就是全球的 DNS 根服务器
本篇的内容,我们会尝试去 ping 每一个 DNS 根服务器,了解跟当前设备的线路状态。另外更具体的信息可以通过对应的 IP 获取,去什么地方获取?你可以使用 https://myip.ms/ 类似的在线服务,或者接入此类服务商的一些 API,在代码中进一步获取更多的状态信息。

准备工作

我们先 import 本篇内容需要用到的 package,因为要用到 gevent,常例是先调用 gevent 的 patch_all。
然后,通过 os.popen 来调用系统命令,你也可以在 命令行窗口 直接执行 dig,看什么样的输出效果。这个 dig 不跟任何参数,会显示全球的 DNS 根服务器 以及对应的 IP。

from gevent.monkey import patch_all; patch_all()
import subprocess, shlex, re
from gevent.pool import Pool
import os

def get_dns_root_servers():
    with os.popen('dig') as f:
        raw_result = f.read()
    root_servers = re.findall('(\w\.root-servers\.net).*?(\d+\.\d+\.\d+\.\d+)', raw_result, flags=re.I)
    root_servers = dict(root_servers)
    if not root_servers:
        print('dig error')
        print(raw_result)
    return root_servers

ping

然后我们构建一个 Python 中的 ping 的函数,利用 MacOS 系统内自带的 ping 命令,但这里用的不是 os.popen,而是 subprocess.Popen。为什么呢?如果只是 os.popen,它只是单进程,很多个 IP 一起 ping 的时候,只能一个个等待下去,而 subprocess 则可以支持异步并发。 参考代码如下:

def ping(ip, times=10):
    cmd = 'ping %s -c %s' % (ip, times)
    try:
        p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        stdout = p.communicate()[0]
        #print(ip)
        #print(stdout)
        #print('\n'*5)
        raw_result = stdout.split('statistics', 1)[-1]
        lost_percent = None
        lost_percent_search = re.search('([\d.]+)%', raw_result)
        if lost_percent_search:
            lost_percent = float(lost_percent_search.group(1))
        raw_statistics = re.findall('([\d.]+)(?:/| ms)', raw_result)
        try:
            min_ms, avg_ms, max_ms = raw_statistics[:3]
            min_ms, avg_ms, max_ms = float(min_ms), float(avg_ms), float(max_ms)
        except:  # 100% lost
            min_ms, avg_ms, max_ms = None, None, None
        status = dict(
            ip = ip,
            lost_percent = lost_percent,
            min = min_ms,
            avg = avg_ms,
            max = max_ms,
        )
        return status
    except:
        # should add breakpoint here!!!
        return {}

我们在写代码的时候,使用某个 package 时,其实就是一个不断 尝试 的过程,为了尽可能避免问题的存在,往往要多一份谨慎。比如上面 ping 这个函数,默认单 IP 会尝试 ping 10 次,如果很多个 IP 一起异步并发,彼此会不会混淆而导致结果失准?而且,subprocess 具体的运行机制,自己也并不是非常清楚,它是否真的能支持 Gevent 的协程异步?这些都需要实际的运行后才能确定。
还有比如上文中 get_dns_root_servers 这个函数要考虑『没有提取到 servers 信息』时的情况,比如示例代码中针对没有 min/avg/max 信息可提取的情况对应,这些都是基于谨慎原则做的预防性对应,更多细节还是要在具体实践过程中发现才能进一步对应规则。

gevent + ping

我们使用 Gevent 的并发机制,来实现多个 IP 并发地 ping,以提高最终的运行速度。 spawn 之后,我们设定的变量名为 job,实际上是一个 greenlet (相当于一个异步协程的工作对象),greenlet 上有一个子函数 link_value,接受一个函数类型的参数,当单个结果运行后,会运行传递给 link_value 的函数。如下面代码所示,link_value 调用的是 multi_ping 的子函数 collect_info子函数 在此时可以共用父函数所在的 NameSpace。

代码细节解释起来都是很琐碎的,具体请参考代码:

def multi_ping(ips, times=10):
    if not isinstance(ips, (list, tuple)):  # 允许单IP测试
        ips = [ips]
    to_return = []
    def collect_info(greenlet):
        if greenlet.value:
            to_return.append(greenlet.value)
    pool = Pool(6)
    for ip in ips:
        if pool.full():
            pool.wait_available()
        job = pool.spawn(ping, ip, times)
        job.link_value(collect_info)
    pool.join()
    return to_return

获取 status

接下来就是简单的事情了,把前面的几个函数合在一起使用,先通过 get_dns_root_servers 获得 DNS 根服务器的域名IP,数据为 dict 类型,其中 域名 为 key,IP 为 value。
我们也希望产生一个 IP 为 key,域名 为 value 的反向映射,这样 dict(zip(root_servers.values(), root_servers.keys())) 一行可以搞定,zip 是 Python 的内置函数,而 dict 虽然是一个数据类型,但本质上也算一个 class,那么调用它进行实例化的过程,可以将 [(k1, v1), (k2, v2] 这样类型的 list/tuple 直接转为需要的 dict 类型。
再接下来,就是多个 IP 一起去 ping,然后获得最终的结果。
参考源码:

def get_dns_root_servers_status():
    status = {}
    root_servers = get_dns_root_servers()
    root_servers_r = dict(zip(root_servers.values(), root_servers.keys()))
    ips_ping_status = multi_ping(root_servers.values())
    for ip_ping_status in ips_ping_status:
        if not ip_ping_status:
            continue
        ip = ip_ping_status['ip']
        root_server_name = root_servers_r.get(ip)
        status[root_server_name] = ip_ping_status
    return status

运行

我们直接看代码吧,将上面的函数关联起来进行调用,并最终输出结果:

if __name__ == '__main__':
    status = get_dns_root_servers_status()
    status_items = status.items()
    status_items.sort()
    for root_server_name, root_server_status in status_items:
        print(root_server_name)
        print(root_server_status)
        print('- - '*20)
        print('\n'*2)

异步并发获取 list 又转为 dict,其 items 是没有绝对次序的,status_items.sort() 只是为了结果输出时候好看一些,list 类型的 sort 命令,可以接受特定的排序函数作为排序的依据。你也可以将 status_items.sort() 这一行注释掉,再看是什么结果。

大概结果如下图所示:

后记

一个新的领域,一个新的 package/module 对我们来说,可能很像一个黑盒,但随着逐渐地掌握,它就会变成灰盒的状态。至于是否最终能成为白盒,一般来说,白盒对应『完全掌握』的话,那投入产出的性价比是不高的。
就比如这篇文章写成之前,我唯一确定的是 这个议题 不错,会带来启发,而整个过程是从头到尾从零完成代码,甚至说,我也并不能在一开始确定Gevent+调用系统的 ping 命令 是可行的,只有写下来,跑起来了,才能确定。当然,随着经验的累计,一个想法产生的时候,大抵已经知道能否实现了。

另外,本文的参考代码并不太适合在正式产品中使用。
通过调用系统命令的方式,最后作为 Python 内的 API 被反复调用,会不会有性能问题?每一次 ping 的过程,是否会自动关闭 connection?我们在前面的《Socket 编程》中讲过,一台设备的 socket 的最大值也就 6 万多个,如果每次去 ping 的时候,一个 connection 对应一个新的 socket 且不及时关闭,那么当前设备迟早要完蛋,socket 被占用完之后,本地的 Web 请求都将无法发送了。另外 ping 是 ICMP 协议,ping out 的时候有没有特定 client 的端口,是否会持续产生新 socket 呢?
抱歉,这些,我都不知道。
解决这种未知的方法,一般来说分两种: 1,花时间去知道,2,去验证
我会选择第 2 种方法。
如果临时用用,或者使用的频率不高,那么,验证的必要性也会降低…… 我们考量问题的时候,尽可能要多一些全局性,在写代码的时候也是如此。

验证 本身也是有成本高低的,如果依赖于外部的一个命令行调用,则要去测试这个命令本身的各种情况、边缘性的性能问题。是故,我会更倾向于更透明的解决方案,比如 ping 为例,如果有纯 Python 的 module 支持,肯定会选择这个 module 而不是通过调用系统命令的方式。
在课程的最开始,想必大家已经理解 Python 的速度是比不过 C 的,但是 Python 的速度已经足够快了,如果真有性能瓶颈,可以借助外部工具 (比如用 C 写的) 来弥补。
另外一方面,Python 其实作为一个长期的 backend 程序运行的过程中,未必能做到 100% 的稳健,有时因为我们自己的问题,有时甚至会碰到 Python 自身的问题,哪怕概率很低,崩溃就是崩溃了。那么有什么解决的办法吗? 比如说 Python 内部有自动回收垃圾的机制,但并不总是完美的,所以有些时候还需要自己手工进行回收;即便如此,仍然会存在一些边界性问题而难以解决,特别是代码中资源调用比较复杂,又过度产生以及依赖内存的自动回收。
我们到底如何提高 Python 的稳定性呢?其实也有个简单的思路,就是定期、定条件地 kill 掉 Python 的默认进程,然后再重新启动,并且保证这个过程中无缝衔接。简而言之,就是: 定期关机、开机,像我们部署 Python 的 Web 项目用到的 Gunicorn 就是这样的思路……

It Works!
这很重要。