预计阅读本页时间:-
7.4.8 案例分享八:巧用DNS轮询做负载均衡
笔者正在维护的DSP大型广告平台,用的是纯AWS云平台环境,业务高峰期时有十几万的并发量,3万左右的QPS,不仅Web层面,后面的数据库和redis缓存也面临着巨大压力(这一块的内容将在后面的章节里进行详细说明)。这么巨大的流量和并发量,只能采用分布式的思路来解决。
第一个方案是在前端用CDN的方式来解决压力的问题。但由于DSP业务的特殊性质,3万多QPS请求基本上都是动态请求,而非静态图片或CSS等静态请求等。所以前端放置CDN的方案第一个被否决。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
第二个方案是利用AWS EC2机器将其作为HAProxy/Nginx以起到承担分流的作用,但是这里又有一个解决不了的新问题。虽然AWS EC2机器性能卓越,但它们毕竟是共享带宽的,所以网络性能还是有影响的。就算我们采用最好的AWS EC2机器,入口带宽也是不可能超过100MB的。假设单个请求的数据在10KB左右,那么3万QPS就是300MB/s,所以这样做的意义不大。如果用单台AWS EC2机器作为LB提供网站入口,肯定会有大量的丢包和Timeout现象发生,当然了,如果这里有多台EC2机器一起来承担这个工作,这个还是可行的,所以这个方案是一个可选方案。
第三个方案是采用AWS EC2本身提供的Elastic Load Balancing服务,就服务本身而言是完全没什么问题的,而且价格方面也是比较实惠的。按照AWS的官方介绍,以美国东部(弗吉尼亚北部)举例来说,具体价格是$0.008/GB。如果该Elastic Load Balancer在30天的期间内最终传输了100 GB的数据流量,则该月Elastic Load Balancer使用小时数费用总额为18 USD(即每小时0.025 USD×每天24小时×30天×1个Elastic Load Balancer),通过Elastic Load Balancer传输的数据流量费用总额为0.80USD(即0.008USD/GB×100 GB),该月的总费用为18.80USD。但我们的业务高峰期间,每天传输的数据流量远远不止100GB。长此以往,Elastic Load Balancing的服务费用也是一笔不小的开销,会极大地增加网站的运营成本开销。
能不能找到一种最节约成本而且性价比又高的负载均衡方案呢?最后我们想到了用DNS轮询的方案,采用的是PowerDNS开源软件和ruby-pdns。放弃中间层的Nginx(LB)机器,直接将DNS指向为后端的bidder机器。
PowerDNS是高性能的域名服务器,除了支持普通的bind配置文件,PowerDNS还可以从MySQL、Oracle、PostgreSQL等数据库中读取数据。PowerDNS安装了Poweradmin,能实现Web管理DNS记录,非常方便。
ruby-pdns是一个简单的Ruby库,可用来开发基于PowerDNS的DNS动态记录应用,它将复杂的DNS操作过程封装起来并提供简单易用的方法,示例代码如下所示。
module Pdns
newrecord("www.your.net") do |query, answer|
case country(query[:remoteip])
when "US", "CA"
answer.content "64.xx.xx.245"
when "ZA", "ZW"
answer.content "196.xx.xx.10"
else
answer.content "78.xx.xx.140"
end
end
end
工作中采用ruby-pdns的主要原因是:修改PowerDNS记录是即时生效的,无须重启PowerDNS服务。另外ruby-pdns对GeoIP数据库支持得非常好(所谓GeoIP,就是通过来访者的IP,定位他的经纬度、国家、省市、地区,甚至街道等位置信息的一个数据库)。在此业务系统中,笔者利用其智能解析功能搭建了简单的CDN系统,方便美国东西部客户就近联结其业务图片机器,加快用户访问速度,提升用户体验,相关代码如下:
newrecord("bid-east.example.net") do |query, answer|
ips = ["54.175.1.2", "54.164.1.2", "52.6.1.2","54.164.1.2", "54.175.1.2","54.175.1.3","54.175.1.4","52.4.1.2"……
]
#bidder机器大约
20台,这里只列出其中的
8台公网
IP做了无害处理
ips = ips.randomize([1, 1, 1, 1, 1, 1, 1, 1])
answer.shuffle false
answer.ttl 30
answer.content ips[0]
answer.content ips[1]
answer.content ips[2]
answer.content ips[3]
answer.content ips[4]
answer.content ips[5]
answer.content ips[6]
answer.content ips[7]
end
module Pdns
newrecord("ads.bilinmedia.net") do |query, answer|
country_, region_ = country(query[:remoteip])
answer.qclass query[:qclass]
answer.qtype :A
case country_
when "US"
case region_
when "WI","IL","TN","MS","ID","KY","AL","OH","WV","VA","NC","SC","GA","FL","NY","PA","ME","VT","NH","MA","RI","CT","NJ","DE","MD","DC"
# 东部地区用户访问东部图片服务器
answer.ttl 300
answer.content "54.165.1.2"
else
# 西部地区用户访问西部图片服务器
answer.ttl 300
answer.content "54.67.1.2"
end
else
# 如果用户
IP都不在上面的城市,则选择默认的西部机器
answer.ttl 300
answer.content "54.67.1.2"
end
end
end
DNS轮询主要是靠如下代码来实现的。
newrecord("bid-east.example.net") do |query, answer|
ips = ["54.175.1.2", "54.164.1.2", "52.6.1.2","54.164.1.2", "54.175.1.2","54.175.1.3","54.175.1.4","52.4.1.2"……
]
#bidder机器大约
20台,这里只列出其中的
8台,公网
IP做了无害处理
ips = ips.randomize([1, 1, 1, 1, 1, 1, 1, 1])
answer.shuffle false
answer.ttl 30
answer.content ips[0]
answer.content ips[1]
answer.content ips[2]
answer.content ips[3]
answer.content ips[4]
answer.content ips[5]
answer.content ips[6]
answer.content ips[7]
end
我们可以用dig命令来解析下bid-east.example.com域,命令如下所示:
dig bid-east.example.com
命令结果显示如下所示:
; <<>> DiG 9.3.6-P1-RedHat-9.3.6-20.P1.el5_8.6 <<>> bid-east.bilinmedia.net
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36017
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 1, ADDITIONAL: 1
;; QUESTION SECTION:
;bid-east.bilinmedia.net. IN A
;; ANSWER SECTION:
bid-east.bilinmedia.net. 30 IN A 54.175.1.2
bid-east.bilinmedia.net. 30 IN A 54.164.1.2
bid-east.bilinmedia.net. 30 IN A 52.6.1.2
bid-east.bilinmedia.net. 30 IN A 54.164.1.2
bid-east.bilinmedia.net. 30 IN A 54.175.1.2
bid-east.bilinmedia.net. 30 IN A 54.175.1.3
bid-east.bilinmedia.net. 30 IN A 54.175.1.4
bid-east.bilinmedia.net. 30 IN A 52.4.1.2……
;; AUTHORITY SECTION:
bid-east.bilinmedia.net. 1799 IN NS ns.bilinmedia.net.
;; ADDITIONAL SECTION:
ns.bilinmedia.net. 599 IN A 54.173.66.112
;; Query time: 1530 msec
;; SERVER: 10.143.22.116#53(10.143.22.116)
;; WHEN: Thu Jan 14 09:55:23 2016
;; MSG SIZE rcvd: 202
这样配置上去以后,在业务最繁忙的时间段观察可以得知:20台bidder机器,Nginx+Lua作为Web服务器,平均每台的活动连接数在20000~22000左右,流量被平均分担下去了,达到了负载均衡的目的。我们可以用Ansible工具抽取下空闲时间(晚上凌晨2点左右)bidder集群机器的活动连接数情况,命令如下所示:
ansible bidder -m script -a "/home/ec2-user/counter.sh"
结果如下所示:
bidder1 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "FIN_WAIT1 13,ESTABLISHED 3193,LISTEN 6\r\n",
"stdout_lines": [
"FIN_WAIT1 13,ESTABLISHED 3193,LISTEN 6"
]
}
bidder2 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "TIME_WAIT 1,FIN_WAIT1 9,ESTABLISHED 3175,SYN_RECV 2,LISTEN 8\r\n",
"stdout_lines": [
"TIME_WAIT 1,FIN_WAIT1 9,ESTABLISHED 3175,SYN_RECV 2,LISTEN 8"
]
}
bidder4 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "FIN_WAIT1 15,ESTABLISHED 3176,LISTEN 6\r\n",
"stdout_lines": [
"FIN_WAIT1 15,ESTABLISHED 3176,LISTEN 6"
]
}
bidder5 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "TIME_WAIT 1,FIN_WAIT1 10,ESTABLISHED 3262,LISTEN 6\r\n",
"stdout_lines": [
"TIME_WAIT 1,FIN_WAIT1 10,ESTABLISHED 3262,LISTEN 6"
]
}
bidder3 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "TIME_WAIT 2,FIN_WAIT1 15,ESTABLISHED 3857,LISTEN 6\r\n",
"stdout_lines": [
"TIME_WAIT 2,FIN_WAIT1 15,ESTABLISHED 3857,LISTEN 6"
]
}
bidder7 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "FIN_WAIT1 7,ESTABLISHED 2821,LISTEN 6\r\n",
"stdout_lines": [
"FIN_WAIT1 7,ESTABLISHED 2821,LISTEN 6"
]
}
bidder6 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "TIME_WAIT 1,FIN_WAIT1 8,ESTABLISHED 3239,LISTEN 6\r\n",
"stdout_lines": [
"TIME_WAIT 1,FIN_WAIT1 8,ESTABLISHED 3239,LISTEN 6"
]
}
bidder8 | SUCCESS => {
"changed": true,
"rc": 0,
"stderr": "",
"stdout": "TIME_WAIT 1,FIN_WAIT1 7,ESTABLISHED 3238,LISTEN 6\r\n",
"stdout_lines": [
"TIME_WAIT 1,FIN_WAIT1 7,ESTABLISHED 3238,LISTEN 6"
]
}
基本上,Nginx的活动并发连接数也是比较平均的,维持在3200~3800左右,证明流量是平均分配下来的,PowerDNS的轮询功能是生效的,业务繁忙的时候通过ruby-pdns修改其配置文件,动态地添加bidder业务机器,就可以很轻松地进行水平扩展以应付新增的流量了。