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
然后我们构建一个 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 的并发机制,来实现多个 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
接下来就是简单的事情了,把前面的几个函数合在一起使用,先通过 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!
这很重要。