當我們做技術預研/業務起步的時候,功能性(Functionality)是最重要的,能跑通就行。對於最流行的C/S架構來說,下面的架構就是能滿足功能需求的最簡模式:
但隨著的業務的發展,量級越來越大的時候,可伸縮性(Scalability)和高可用性(HighAvailability)都會逐漸變成非常重要的議題,除此以外可管理性(Manageability)和成本效益(Cost-effectiveness)都會在我們的考慮範圍之內。本文重點關注業務發展中的高可用建設。
其實考慮到上面這些因素,LVS這個強大的模組幾乎就是我們的必選項(有些場景中,LVS不是最佳選擇,比如內網負載均衡),也是業務同學接觸到最多的模組。下面讓我們從LVS體驗開始,一步步擴大視野看高可用是怎麼做的。
注:本文不會講解LVS的基礎知識,欠缺的地方請大家自行Google。
LVS初體驗
要一大堆機器做實驗是不現實的,所以我們就在docker裡面做實驗。
第一步:建立網路:
docker network create south
然後用 docker network inspect south
得到網路資訊 "Subnet": "172.19.0.0/16","Gateway": "172.19.0.1"
。也可以選擇在 create
的時候用 --subnet
自行指定子網,就不用去查了。
第二步:建立RS
兩臺real server,rs1和rs2。Dockerfile如下
FROM nginx:stable
ARG RS=default_rs
RUN apt-get update \
&& apt-get install -y net-tools \
&& apt-get install -y tcpdump \
&& echo $RS > /usr/share/nginx/html/index.html
分別構建和啟動
docker build --build-arg RS=rs1 -t mageek/ospf:rs1 .
docker run -itd --name rs1 --hostname rs1 --privileged=true --net south -p 8888:80 --ip 172.19.0.5 mageek/ospf:rs1
docker build --build-arg RS=rs2 -t mageek/ospf:rs2 .
docker run -itd --name rs2 --hostname rs2 --privileged=true --net south -p 9999:80 --ip 172.19.0.6 mageek/ospf:rs2
這裡面比較重要的是privileged,沒有這個引數,我們在容器內是沒法繫結vip的(許可權不夠)。此外,啟動時候固定ip也是便於後續lvs配置簡單可重複
第三步:建立LVS
Dockerfile如下
FROM debian:stretch
RUN apt-get update \
&& apt-get install -y net-tools telnet quagga quagga-doc ipvsadm kmod curl tcpdump
這裡面比較重要的quagga是用來執行動態路由協議的,ipvsadm是lvs的管理軟體。
啟動lvs:
docker run -itd --name lvs1 --hostname lvs1 --privileged=true --net south --ip 172.19.0.3 mageek/ospf:lvs1
依然需要privileged和固定ip。
第四步:VIP配置
LVS配置
docker exec -it lvs1 bash
進入容器。我們直接採用LVS最高效的模式,DR模式,以及最常見的負載策略:round_robin:
ipvsadm -A -t 172.19.0.100:80 -s rr
ipvsadm -a -t 172.19.0.100:80 -r 172.19.0.5 -g
ipvsadm -a -t 172.19.0.100:80 -r 172.19.0.6 -g
# 檢視配置的規則
ipvsadm -Ln
# 啟用
ifconfig eth0:0 172.19.0.100/32 up
RS配置
ifconfig lo:0 172.19.0.100/32 up
echo "1">/proc/sys/net/ipv4/conf/all/arp_ignore
echo "1">/proc/sys/net/ipv4/conf/lo/arp_ignore
echo "2">/proc/sys/net/ipv4/conf/all/arp_announce
echo "2">/proc/sys/net/ipv4/conf/lo/arp_announce
其中
arp_ignore
是為了避免 rs 響應 arp 請求,確保 dst ip 是 vip 的包一定會路由到 lvs 上arp_announce
是為了避免 r s在發起 arp 請求時用 vip 汙染區域網中其它裝置的 arp 表- 同一個配置寫兩遍,是為了確保生效,因為核心會選擇 all 和具體網路卡中的較大值
第五步:觀察
進入 south 網路的另一個容器 switch(不要介意名字),訪問 vip
> for a in {1..10}
> do
> curl 172.19.0.100
> done
rs2
rs1
rs2
rs1
rs2
rs1
rs2
rs1
rs2
rs1
可見是 round robin 的模式。
再看看是否是 DR 模式
root@switch:/# curl 172.19.0.100
rs2
root@switch:/# curl 172.19.0.100
rs1
root@lvs1:/# tcpdump host 172.19.0.100
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:52:47.967790 IP switch.south.35044 > 172.19.0.100.http: Flags [S], seq 3154059648, win 64240, options [mss 1460,sackOK,TS val 1945546875 ecr 0,nop,wscale 7], length 0
14:52:47.967826 IP switch.south.35044 > 172.19.0.100.http: Flags [S], seq 3154059648, win 64240, options [mss 1460,sackOK,TS val 1945546875 ecr 0,nop,wscale 7], length 0
14:52:47.967865 IP switch.south.35044 > 172.19.0.100.http: Flags [.], ack 3324362778, win 502, options [nop,nop,TS val 1945546875 ecr 1321587858], length 0
14:52:47.967868 IP switch.south.35044 > 172.19.0.100.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 1945546875 ecr 1321587858], length 0
14:52:47.967905 IP switch.south.35044 > 172.19.0.100.http: Flags [P.], seq 0:76, ack 1, win 502, options [nop,nop,TS val 1945546875 ecr 1321587858], length 76: HTTP: GET / HTTP/1.1
14:52:47.967907 IP switch.south.35044 > 172.19.0.100.http: Flags [P.], seq 0:76, ack 1, win 502, options [nop,nop,TS val 1945546875 ecr 1321587858], length 76: HTTP: GET / HTTP/1.1
14:52:47.968053 IP switch.south.35044 > 172.19.0.100.http: Flags [.], ack 235, win 501, options [nop,nop,TS val 1945546875 ecr 1321587858], length 0
14:53:15.037813 IP switch.south.35046 > 172.19.0.100.http: Flags [S], seq 2797683020, win 64240, options [mss 1460,sackOK,TS val 1945573945 ecr 0,nop,wscale 7], length 0
14:53:15.037844 IP switch.south.35046 > 172.19.0.100.http: Flags [S], seq 2797683020, win 64240, options [mss 1460,sackOK,TS val 1945573945 ecr 0,nop,wscale 7], length 0
14:53:15.037884 IP switch.south.35046 > 172.19.0.100.http: Flags [.], ack 1300058730, win 502, options [nop,nop,TS val 1945573945 ecr 1321614928], length 0
14:53:15.037887 IP switch.south.35046 > 172.19.0.100.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 1945573945 ecr 1321614928], length 0
14:53:15.037925 IP switch.south.35046 > 172.19.0.100.http: Flags [P.], seq 0:76, ack 1, win 502, options [nop,nop,TS val 1945573945 ecr 1321614928], length 76: HTTP: GET / HTTP/1.1
14:53:15.037942 IP switch.south.35046 > 172.19.0.100.http: Flags [P.], seq 0:76, ack 1, win 502, options [nop,nop,TS val 1945573945 ecr 1321614928], length 76: HTTP: GET / HTTP/1.1
14:53:15.038023 IP switch.south.35046 > 172.19.0.100.http: Flags [.], ack 235, win 501, options [nop,nop,TS val 1945573945 ecr 1321614928], length 0
root@rs1:/# tcpdump host 172.19.0.100
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:53:15.037848 IP switch.south.35046 > 172.19.0.100.80: Flags [S], seq 2797683020, win 64240, options [mss 1460,sackOK,TS val 1945573945 ecr 0,nop,wscale 7], length 0
14:53:15.037873 IP 172.19.0.100.80 > switch.south.35046: Flags [S.], seq 1300058729, ack 2797683021, win 65160, options [mss 1460,sackOK,TS val 1321614928 ecr 1945573945,nop,wscale 7], length 0
14:53:15.037888 IP switch.south.35046 > 172.19.0.100.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 1945573945 ecr 1321614928], length 0
14:53:15.037944 IP switch.south.35046 > 172.19.0.100.80: Flags [P.], seq 1:77, ack 1, win 502, options [nop,nop,TS val 1945573945 ecr 1321614928], length 76: HTTP: GET / HTTP/1.1
14:53:15.037947 IP 172.19.0.100.80 > switch.south.35046: Flags [.], ack 77, win 509, options [nop,nop,TS val 1321614928 ecr 1945573945], length 0
14:53:15.037995 IP 172.19.0.100.80 > switch.south.35046: Flags [P.], seq 1:235, ack 77, win 509, options [nop,nop,TS val 1321614928 ecr 1945573945], length 234: HTTP: HTTP/1.1 200 OK
14:53:15.038043 IP switch.south.35046 > 172.19.0.100.80: Flags [.], ack 235, win 501, options [nop,nop,TS val 1945573945 ecr 1321614928], length 0
root@rs2:/# tcpdump host 172.19.0.100
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:52:47.967830 IP switch.south.35044 > 172.19.0.100.80: Flags [S], seq 3154059648, win 64240, options [mss 1460,sackOK,TS val 1945546875 ecr 0,nop,wscale 7], length 0
14:52:47.967853 IP 172.19.0.100.80 > switch.south.35044: Flags [S.], seq 3324362777, ack 3154059649, win 65160, options [mss 1460,sackOK,TS val 1321587858 ecr 1945546875,nop,wscale 7], length 0
14:52:47.967869 IP switch.south.35044 > 172.19.0.100.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 1945546875 ecr 1321587858], length 0
14:52:47.967908 IP switch.south.35044 > 172.19.0.100.80: Flags [P.], seq 1:77, ack 1, win 502, options [nop,nop,TS val 1945546875 ecr 1321587858], length 76: HTTP: GET / HTTP/1.1
14:52:47.967910 IP 172.19.0.100.80 > switch.south.35044: Flags [.], ack 77, win 509, options [nop,nop,TS val 1321587858 ecr 1945546875], length 0
14:52:47.967990 IP 172.19.0.100.80 > switch.south.35044: Flags [P.], seq 1:235, ack 77, win 509, options [nop,nop,TS val 1321587858 ecr 1945546875], length 234: HTTP: HTTP/1.1 200 OK
14:52:47.968060 IP switch.south.35044 > 172.19.0.100.80: Flags [.], ack 235, win 501, options [nop,nop,TS val 1945546875 ecr 1321587858], length 0
可見確實 lvs1 只會收到 switch 的包並轉發給 rs(有來無回),而 rs1 和 rs2 和 switch 就是正常的三步握手後再進行 http 報文的傳輸(有來有回),是 DR 模式。
細心的同學發現了,lvs1 中怎麼報文都出現了兩次?
這是因為 DR 收到 IP 報文後,不修改也不封裝 IP 報文,而是將資料幀的 MAC 地址改為選出伺服器的 MAC 地址,再將修改後的資料幀在與伺服器組相同的區域網上傳送,如圖所示:
tcpdump是能抓到修改前後的包的,所以有兩條。實際上,在tcpdump命令加上-e引數後,就能看到mac地址變化。
root@lvs1:/# tcpdump host 172.19.0.100 -e
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
15:58:57.245917 02:42:ac:13:00:02 (oui Unknown) > 02:42:ac:13:00:03 (oui Unknown), ethertype IPv4 (0x0800), length 74: switch.south.35070 > 172.19.0.100.http: Flags [S], seq 422105942, win 64240, options [mss 1460,sackOK,TS val 1949516153 ecr 0,nop,wscale 7], length 0
15:58:57.245950 02:42:ac:13:00:03 (oui Unknown) > 02:42:ac:13:00:05 (oui Unknown), ethertype IPv4 (0x0800), length 74: switch.south.35070 > 172.19.0.100.http: Flags [S], seq 422105942, win 64240, options [mss 1460,sackOK,TS val 1949516153 ecr 0,nop,wscale 7], length 0
最後得到的架構如圖:
RS高可用
上面我們配置了 LVS 後面兩個 RS,這樣能起到增大吞吐量的作用(伸縮性),但其實並沒有做到 RS 的高可用,因為一臺 RS 掛了後,LVS 依然會往該 RS 上打流量,造成這部分請求失敗。所以我們還需要配置健康檢查,當 LVS 檢查到 RS 不健康的時候,主動剔除這臺 RS,讓流量不往這裡打。這樣就做到了RS的高可用,也就是說,RS 掛了一臺後不影響業務(當然,實際場景還要考慮吞吐量、建連風暴、資料等問題)。
首先安裝keepalived,裝好後配置如下
global_defs {
lvs_id LVS1
}
virtual_server 172.19.0.100 80 {
delay_loop 5
lb_algo rr
lb_kind DR
persistence_timeout 50
protocol TCP
real_server 172.19.0.5 80 {
weight 2
HTTP_GET {
url {
path /
}
connect_timeout 3
retry 3
delay_before_retry 2
}
}
real_server 172.19.0.6 80 {
weight 2
HTTP_GET {
url {
path /
}
connect_timeout 3
retry 3
delay_before_retry 2
}
}
}
然後啟動:
chmod 644 /etc/keepalived/keepalived.conf
# 新增keepalived專用的使用者
groupadd -r keepalived_script
useradd -r -s /sbin/nologin -g keepalived_script -M keepalived_script
# 啟動
keepalived -C -D -d
注意這裡只用了 keepalived 的健康檢查功能,沒有用 VRRP 功能。
關閉 rs2 後,訪問 vip 可以發現,vip 只會導向 rs1
root@switch:/# curl 172.19.0.100
rs1
root@switch:/# curl 172.19.0.100
rs1
root@switch:/# curl 172.19.0.100
rs1
root@switch:/# curl 172.19.0.100
rs1
root@switch:/# curl 172.19.0.100
rs1
root@switch:/# curl 172.19.0.100
rs1
然後 lvs1 的 ipvs 配置也發生了變化
root@lvs1:/# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 172.19.0.100:80 rr
-> 172.19.0.5:80 Route 1 0 1
當恢復 rs2 後,ipvs 配置恢復如初,針對 vip 發起的請求也在 rs1 和 rs2 之間均勻響應,實現了 RS 的高可用。
root@lvs1:/# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 172.19.0.100:80 rr
-> 172.19.0.5:80 Route 1 0 4
-> 172.19.0.6:80 Route 2 0 0
LVS高可用
所謂高可用,其實核心就是冗餘(當然,不止是冗餘),所以我們可以用多臺LVS來做高可用。這裡又會有兩種選擇:一是主備模式,可以利用 Keepalived 的 VRRP 功能,但是大規模生產環境中,用叢集模式更好,因為其同時提高了伸縮性和可用性,而前者只解決了可用性(當然,也更簡單)。
架構分別如圖:
主備模式 | 叢集模式 |
---|---|
簡要說明一下原理:
- 主備模式: lvs主備之間執行 VRR P協議,日常態流量都從主走,當備檢測到主掛了(停止收到主發來的VRRP通告後一段時間),便通過傳送 free arp 來搶佔vip,使得所有流量都從自己走,實現 failover
- 叢集模式: lvs叢集和上聯交換機執行 OSPF,生成該 vip 的多路等價路由 ecmp,這樣流量就能根據使用者自定義的策略流向 lvs。當某臺lvs掛了,交換機會將其從路由表中剔除,實現 failover
動態路由協議的配置過程比較複雜,限於篇幅這裡就不展開了,大家感興趣的可以自行Google。
到這裡LVS相關的就看的差不多了,我們再往外延伸一下,看看其它領域是怎麼做高可用的。
交換機/鏈路高可用
上面可以看到,做了 LVS 的高可用後,交換機又成了單點。實際上,交換機有很多方法做高可用,分為二層和三層:
三層
跟上面的LVS一樣,利用VRRP實現交換機的主備高可用,也可以利用OSPF/ECMP實現叢集高可用(當然,僅針對三層交換機)。
二層
這裡簡單舉個例子,傳統園區網路採用三層網路架構模型,如圖所示
匯聚交換機和接入交換機之間通常就使用 STP/ MSTP(Spanning Tree Protocol),該協議演算法在交換機有多條可達鏈路時只保留一條鏈路,其它鏈路在故障時啟用。
另外還有 Smartlink,也能實現二層鏈路的主備模式。
此外,為了避免單臺交換機故障,可以在伺服器上掛主備網路卡,雙上聯,當主網路卡所在鏈路故障時,伺服器啟用備網路卡所在鏈路即可,如圖:
裝置高可用
不管是交換機還是伺服器、路由器等,最終都是放在機房機櫃中,作為物理裝置存在的,它們是怎麼做高可用的呢?
物理裝置,其可用性核心就是電源供應:
- 首先用到了UPS,主要就是儲能技術:在市電供應時,給蓄電池充電;在市電斷電時,將電池放電,給機櫃供能。
- 其次用到了雙路供電,也就是說,市電直接來源於兩個供電系統,避免單個供電系統故障造成不可用
機房高可用
上面的過程保證了機房內部的可用性,那麼整個機房掛了該怎麼辦?有不少方法能解決這個問題
DNS輪詢
假設我們的業務域名為a.example.com,給他新增兩條A記錄,分別指向機房a和機房b。當a機房掛了,我們將a機房的A記錄刪除,這樣所有使用者就都只能拿到b機房的A記錄,從而訪問到b機房,實現高可用。
這種方式的問題在於 DNS 的 TTL 是不可控的,一般os、localDNS、權威DNS都會做快取,尤其是其中的localDNS,一般都在運營商手中,尤為難控(運營商不一定嚴格按照TTL進行更新)。退一步來說,即使TTL都可控(比如用httpDNS),但這個TTL的設定比較難把握:太長則故障 failover 時間太長;太短則使用者頻繁發起 DNS 解析請求,影響效能。
所以目前這種方式都只作為高可用的輔助手段,而不是主要手段。
值得一提的是,F5 的 GTM 可以實現此功能,通過動態地給客戶返回域名解析記錄,實現就近、容錯等效果。
優先順序路由
主和備的地址都在路由範圍內,但是優先順序不同。這樣日常情況流量都到主,當主掛掉並被檢測到的時候,主的路由被刪除,備的路由自動生效,這樣就實現了主備failover。
路由優先順序有幾個理解:
- 不同協議形成的路由表是有優先順序的,比如直連路由為0,OSPF 為 110,IBGP 為 200,當同一個目標地址有多個下一跳時,使用優先順序更高的路由表。實踐中,我還沒見到過這種做法實現主備的。
- 同一個協議內,不同的路徑是有優先順序的,比如 OPSF 協議的 cost,日常主備路徑的 cost 設定不一樣,cost小的為主並被寫入路由表走所有流量,當主掛掉了,其路徑被路由表刪除,備路徑自動進入路由表。實踐中,F5 LTM 的 route health injection 是利用這個原理實現主備的。
- 同一個協議內,路由匹配是講究最長字首的,比如路由表中有兩個條目,172.16.1.0/24、172.16.2.0/24,當收到 dst ip 為 172.16.2.1 的包時,出於最長字首匹配原則(越長越精準),就應該走172.16.2.0/24這一條路由。實踐中,阿里雲 SLB 利用此原理來做同城容災。
Anycast
上面講到了用 DNS 來高可用,那 DNS 本身也是需要做高可用的。DNS 高可用的一個重要手段就是 Anycast,這個手段也能被其他業務借鑑,所以我們也來看看。
網際網路上目前真正落地的 EGP 就是 BGP(這也是網際網路能互聯互通的基石協議),通過不同的 AS 對外宣告同一個 IP,使用者在訪問這個IP的時候就能依據特定的策略訪問到最佳的 AS(比如就近訪問策略)。當某個 AS 中的服務掛掉且被 BGP 路由器檢測到了,BGP 路由器會自動停止該 IP 對上聯 AS 的廣播,這樣到該 IP 的使用者流量就不會被路由到這個 AS 來,從而實現故障的 failover。
對於DNS來說,邏輯上,根域名伺服器只有13個,但是利用Anycast,實際部署數量是遠不止13的,不同國家和地區都可以自行部署根域名伺服器的映象,並且具備同樣的IP,從而實現本地就近訪問、冗餘、安全等特性。
業務高可用
上面講了一大堆的高可用,有這些方案後,業務是不是直接多地部署就實現了高可用了呢?當然不是。
仍以 DNS 為例,雖然全球都能部署根域名映象伺服器,但是真實的域名解析資料還是要從根域名伺服器同步,這裡面就有資料一致性問題,雖然 DNS 本身是個對資料一致性沒那麼高的服務,但是我們更多的服務都對資料一致性有要求(比如庫存、餘額等)。這也是上面說的,高可用雖然和冗餘關係很大,但也不只是冗餘,還要關注資料一致性等方面(也就是 CAP 定理)。這方面,不同的業務有不同的做法。
對於常見的web服務,可以通過對業務做自頂向下的流量隔離來實現高可用:將單個的使用者的流量儘可能在一個單元處理完畢(單元封閉),這樣當這個單元發生故障時,就能快速地將流量切到另一個單元,實現 failover,如圖:
寫在最後
上面每一個環節的高可用,其實並不需要每個企業自己投入。很多環節都已經有相當專業的雲產品了。
企業全鏈路自建的話,既不專業(做不好),還會造成浪費(精力集中於主業,才不會錯過商機)。
可用的產品有:
- LVS產品:阿里雲ALB,華為ELB,騰訊CLB;
- DNS產品:阿里雲/華為雲解析DNS,騰訊雲DNSPod;
- Anycast產品:阿里雲Anycast EIP,騰訊雲Anycast公網加速,
- 業務高可用產品:阿里雲MSHA;
- 等等
最後,本文撰寫過程中,思路比較發散,梳理不全面的地方,請大家批評指正。
參考
- Linux伺服器叢集系統(三)--LVS叢集中的IP負載均衡技術: http://www.linuxvirtualserver...
- https://www.kernel.org/doc/Do...
- Case Study: Healthcheck — Keepalived 1.4.3 documentation: https://www.keepalived.org/do...
- VIPServer:阿里智慧地址對映及環境管理系統詳解_CSDN 人工智慧-CSDN部落格: https://blog.csdn.net/heyc861...
- 資料中心網路高可用架構-新華三集團-H3C: http://www.h3c.com/cn/d_20100...
- 阿里云云原生異地多活解決方案: https://baijiahao.baidu.com/s...
- AskF5 | Manual Chapter: Working with Dynamic Routing: https://techdocs.f5.com/kb/en...
- 阿里雲SLB同城容災方案-中儲存網: https://www.chinastor.com/fan...