通过 DNS 构建负载均衡

温故知新

在开始之前,我们先使用 nslookup 以及 ping 两名命令,查询 baidu.com 的记录,baidu.com是一个很好的网络状态侦测样本。
一次是 nslookup baidu.com 114.114.114.114 查询 114.114.114.114 这个公共 DNS 服务器上的记录,一次是直接 nslookup baidu.com,走的是当前局域网系统内默认路由上的 DNS 服务器,两者得到的结果是一样的。
然后再进行一次 ping baidu.com,过了一会儿,又 ping baidu.com 一次,此时,baidu.com 被 ping 的 IP 虽然都是 nslookup 中出现的结果,但是 IP 是不一样。

一个 A 记录多个 IP?

一个 A 记录可以有多个 IP 地址吗?这是可以的。

我们略微改造下 simple_dns_server.py,在 reply_for_A 这个函数后面增加下面两行:

tmp_r_data = A('127.0.0.1')
record.add_answer(RR(domain, query_type_int, rdata=tmp_r_data, ttl=ttl))

命令行窗口中退出 (可以使用快捷键 Control+C) 并重新运行 python sudo simple_dns_server.py,再检验下实际效果:

我们更要关心的问题,应该是:如果有多个 IP 地址,那我们访问这个域名,到底是哪个 IP 在起作用呢?
坦率的说,这个问题我也没有答案,它可能是选第一个、可能是随机的、也可能是轮询,具体什么策略,是 client 端决定的,而不同的 DNS client 策略可未必是统一的。对了,(有些)浏览器本身也相当于一个 DNS client,它的策略跟操作系统的 DNS client 策略不一定一样。就当作是一个黑盒好了,我们不能控制 client 端 (下游),但是可以控制服务端 (上游)。

一个 IP 就好了

A 记录给一个 IP 还是多个 IP,视具体的场景而言,但对于多数场景而言,给一个 IP 就可以了。因为 DNS 客户端,我们控制不了,与其交给 client 选择,不如先在服务端选择好了,直接给最终结果就好。

什么是负载均衡

负载均衡有硬件上的,也有软件上的。
硬件层面的,我们接触不到。软件层面的,比如我们在 《FirstWeb》中接触过 Nginx 了,它是一个 Web 服务器软件,当一个请求经过 Nginx 时,它可以把请求分派给多个 backend,这也是负载均衡。
负载均衡的意图很简单,当一台服务器无法承载的时候,增加第二台,如果第二台还不够,增加第三台,以此类推。
一般在做均衡时,也会判断某个节点是否坏掉了,如果坏掉了,就不把请求指派给它了,所以说,负载均衡系统,也能实现一些故障服务器的平滑迁移。

使用 DNS 实现负载均衡

一个 Web 请求到了服务器端,我们可以用 Nginx 来负载均衡,而在这个请求产生之前,是需要经过 DNS 系统的。
如果在 DNS 解析域名的过程中,就分派了线路,自然也能实现负载均衡。跟 Nginx 相比,DNS 系统上实现负载均衡存在两个问题: 1,会有缓存期的存在,这是由 TTL 决定的;2,有些 ISP 某些区域的 DNS 服务器,可能并不太遵守 TTL 规则,由此产生的 DNS 缓存,可能会导致访客并不能正确命中服务器,特别是当某台服务器出错下线后,我们的 DNS 系统已经给出了正确解析,而访客所处位置的当地 ISP 的 DNS 仍然返回了错误服务器的 IP。
阐述弊端所在,真正目的并不是为了说明弊端本身,而是我们需要站在 DNS 的系统逻辑中,分析这些显然易见的问题。 因为这些问题都是可以直接推论出来的,并不需要像读教科书一般,一点点去读细节。

假设有域名 domain.com,我们在两台服务器上都部署了相同网站,服务器的 IP 分别为 AB
如果要实现 DNS 系统上的负载均衡,就要给 domain.com 返回解析结果的时候,均匀地返回 A 或 B。
一般有两种做法:
1,随机选择

import random 
ip = random.choice(['A', 'B'])

上面参考代码,我们一般要先写成 ips = ['A', 'B'] (假设它们是固定的值),然后 random.choice(ips)。为什么呢?因为这样可以避免每次运行时,都创建一个 新的变量 ['A', 'B'],以节省资源。但是,不去节省这个资源,也没有关系。这又是为什么呢?因为,影响真的太小了,没有大的意义。重要的是,代码写下来时,要有节省资源的基本概念在,如果一个临时变量是非常大的,且是基本不变动的,那么每次函数被调用,还不断产生一个全新的变量,那就是性能问题了。

2,轮询
轮询 就是第一次选 A,第二次选 B,第三次再选 A,接下来选 B,如此往复。如何实现 轮询 的方法很多,你完全可以按照自己的思路去实现,比如记录上一次命中了谁,那么接下来再命中谁就很清晰了。
下面的代码是用了 取余 的方法,比如 7 除以 3 等于 2 余 1,如果 ips 的长度是 2,余数就是 0、1,刚好可以作为 list 的 index(索引、序号) 来用。参考代码如下:

last_matched_ip_id = 0
def get_matched_ip(*ips):
    if not ips:
        return None
    global last_matched_ip_id
    matched_index = last_matched_ip_id % len(ips)
    last_matched_ip_id += 1
    return ips[matched_index]

小的任务

我们在 《写一个 DNS 服务器》一文中,就留了一个小任务,你应该已经完成了指定域名对应指定 IP 的逻辑。
现在,我们要把上面利用 DNS 实现负载均衡的逻辑,也添加进去。

另外还有一个小任务,多个 IP 存在时的分派,除了随机、轮询之外,希望可以一个 client (也可以简单理解为来访 IP) 对应到的 IP 是固定的,比如返回的 IP 候选的有 A 和 B,那么对于某个 client 而言,它的请求永远都会得到 A (或者永远都是 B),不会出现一会儿 A、一会儿 B 的现象。