LVS原理與實(shí)現(xiàn) - 實(shí)現(xiàn)篇
在上一篇文章中,我們主要介紹了?LVS?的原理,接下來(lái)我們將會(huì)介紹?LVS?的代碼實(shí)現(xiàn)。
本文使用的內(nèi)核版本是:2.4.23,而 LVS 的代碼在路徑:?
/src/net/ipv4/ipvs?中。
Netfilter
在介紹?LVS?的實(shí)現(xiàn)前,我們需要了解以下?Netfilter?這個(gè)功能,因?yàn)?LVS?的實(shí)現(xiàn)使用了?Netfilter?的功能。
Netfilter:顧名思義就是網(wǎng)絡(luò)過(guò)濾器(Network Filter),是 Linux 系統(tǒng)特有的網(wǎng)絡(luò)子系統(tǒng),用于過(guò)濾或修改進(jìn)出內(nèi)核協(xié)議棧的網(wǎng)絡(luò)數(shù)據(jù)包。一般可以用來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)防火墻功能,其中?iptables?就是基于?Netfilter?實(shí)現(xiàn)的。
Linux 內(nèi)核處理進(jìn)出網(wǎng)絡(luò)協(xié)議棧的數(shù)據(jù)包分為5個(gè)不同的階段,Netfilter?通過(guò)這5個(gè)階段注入鉤子函數(shù)(Hooks Function)來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)包的過(guò)濾和修改。如下圖的藍(lán)色方框所示:

這5個(gè)階段分為:
PER_ROUTING:路由前階段,發(fā)生在內(nèi)核對(duì)數(shù)據(jù)包進(jìn)行路由判決前。LOCAL_IN:本地上送階段,發(fā)生在內(nèi)核通過(guò)路由判決后。如果數(shù)據(jù)包是發(fā)送給本機(jī)的,那么就把數(shù)據(jù)包上送到上層協(xié)議棧。FORWARD:轉(zhuǎn)發(fā)階段,發(fā)生在內(nèi)核通過(guò)路由判決后。如果數(shù)據(jù)包不是發(fā)送給本機(jī)的,那么就把數(shù)據(jù)包轉(zhuǎn)發(fā)出去。LOCAL_OUT:本地發(fā)送階段,發(fā)生在對(duì)發(fā)送數(shù)據(jù)包進(jìn)行路由判決之前。POST_ROUTING:路由后階段,發(fā)生在對(duì)發(fā)送數(shù)據(jù)包進(jìn)行路由判決之后。
當(dāng)向?Netfilter?的這5個(gè)階段注冊(cè)鉤子函數(shù)后,內(nèi)核會(huì)在處理數(shù)據(jù)包時(shí),根據(jù)所在的不同階段來(lái)調(diào)用這些鉤子函數(shù)對(duì)數(shù)據(jù)包進(jìn)行處理。向?Netfilter?注冊(cè)鉤子函數(shù)可以通過(guò)函數(shù)?nf_register_hook()?來(lái)進(jìn)行,nf_register_hook()?函數(shù)的原型如下:
int nf_register_hook(struct nf_hook_ops *reg);其中參數(shù)?reg?是類型為?struct nf_hook_ops?結(jié)構(gòu)的指針,struct nf_hook_ops?結(jié)構(gòu)的定義如下:
struct nf_hook_ops{struct list_head list;nf_hookfn *hook;int pf;int hooknum;int priority;};
struct nf_hook_ops?結(jié)構(gòu)各個(gè)字段的作用如下:
list:用于連接同一階段中所有相同的鉤子函數(shù)列表。hook:鉤子函數(shù)指針。pf:協(xié)議類型,因?yàn)?Netfilter?可以用于不同的協(xié)議,如 IPV4 和 IPV6 等。hooknum:所處的階段,也就是上面所說(shuō)的5個(gè)不同的階段。priority:優(yōu)先級(jí),值越大優(yōu)先級(jí)約小。
所以要使用?Netfilter?對(duì)網(wǎng)絡(luò)數(shù)據(jù)包進(jìn)行處理,只需要編寫好處理數(shù)據(jù)包的鉤子函數(shù),然后通過(guò)調(diào)用?nf_register_hook()?函數(shù)向?Netfilter?注冊(cè)即可。
另外,鉤子函數(shù)?nf_hookfn?的原型如下:
typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff **skb,const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));
其參數(shù)說(shuō)明如下:
hooknum:所處的階段,也就是上面所說(shuō)的5個(gè)不同的階段。skb:要處理的數(shù)據(jù)包。in:輸入設(shè)備。out:輸出設(shè)備。okfn:如果鉤子函數(shù)執(zhí)行成功,即調(diào)用這個(gè)函數(shù)完成對(duì)數(shù)據(jù)包的后續(xù)處理工作。
Netfilter?相關(guān)的知識(shí)點(diǎn)就介紹到這里,以后有機(jī)會(huì)會(huì)詳解講解?Netfilter?的原理和現(xiàn)實(shí)。
LVS 實(shí)現(xiàn)
前面我們主要簡(jiǎn)單介紹了?Netfilter?的使用,接下來(lái)我們將要分析?LVS?的代碼實(shí)現(xiàn)。
1. 鉤子函數(shù)注冊(cè)
LVS?主要通過(guò)向?Netfilter?的3個(gè)階段注冊(cè)鉤子函數(shù)來(lái)對(duì)數(shù)據(jù)包進(jìn)行處理,如下圖:

在?
LOCAL_IN?階段注冊(cè)了?ip_vs_in()?鉤子函數(shù)。在?
FORWARD?階段注冊(cè)了?ip_vs_out()?鉤子函數(shù)。在?
POST_ROUTING?階段注冊(cè)了?ip_vs_post_routing()?鉤子函數(shù)。
我們?cè)?LVS 的初始化函數(shù)?ip_vs_init()?可以找到這些鉤子函數(shù)的注冊(cè)代碼,如下:
static struct nf_hook_ops ip_vs_in_ops = {{ NULL, NULL },ip_vs_in, PF_INET, NF_IP_LOCAL_IN, 100};static struct nf_hook_ops ip_vs_out_ops = {{ NULL, NULL },ip_vs_out, PF_INET, NF_IP_FORWARD, 100};static struct nf_hook_ops ip_vs_post_routing_ops = {{ NULL, NULL },ip_vs_post_routing, PF_INET, NF_IP_POST_ROUTING, NF_IP_PRI_NAT_SRC-1};static int __init ip_vs_init(void){int ret;...ret = nf_register_hook(&ip_vs_in_ops);...ret = nf_register_hook(&ip_vs_out_ops);...ret = nf_register_hook(&ip_vs_post_routing_ops);...return ret;}
LOCAL_IN?階段:在路由判決之后,如果發(fā)現(xiàn)數(shù)據(jù)包是發(fā)送給本機(jī)的,那么就調(diào)用?ip_vs_in()?函數(shù)對(duì)數(shù)據(jù)包進(jìn)行處理。FORWARD?階段:在路由判決之后,如果發(fā)現(xiàn)數(shù)據(jù)包不是發(fā)送給本機(jī)的,調(diào)用?ip_vs_out()?函數(shù)對(duì)數(shù)據(jù)包進(jìn)行處理。POST_ROUTING?階段:在發(fā)送數(shù)據(jù)前,需要調(diào)用?ip_vs_post_routing()?函數(shù)對(duì)數(shù)據(jù)包進(jìn)行處理。
2. LVS 角色介紹
在介紹這些鉤子函數(shù)之前,我們先來(lái)了解一下?LVS?中的四個(gè)角色。如下:
ip_vs_service:服務(wù)配置對(duì)象,主要用于保存 LVS 的配置信息,如 支持的?傳輸層協(xié)議、虛擬IP?和?端口?等。ip_vs_dest:真實(shí)服務(wù)器對(duì)象,主要用于保存真實(shí)服務(wù)器 (Real-Server) 的配置,如?真實(shí)IP、端口?和?權(quán)重?等。ip_vs_scheduler:調(diào)度器對(duì)象,主要通過(guò)使用不同的調(diào)度算法來(lái)選擇合適的真實(shí)服務(wù)器對(duì)象。ip_vs_conn:連接對(duì)象,主要為了維護(hù)相同的客戶端與真實(shí)服務(wù)器之間的連接關(guān)系。這是由于 TCP 協(xié)議是面向連接的,所以同一個(gè)的客戶端每次選擇真實(shí)服務(wù)器的時(shí)候必須保存一致,否則會(huì)出現(xiàn)連接中斷的情況,而連接對(duì)象就是為了維護(hù)這種關(guān)系。
各個(gè)角色之間的關(guān)系如下圖所示:

從上圖可以看出,ip_vs_service?對(duì)象的?destinations?字段用于保存?ip_vs_dest?對(duì)象的列表,而?scheduler?字段指向了一個(gè)?ip_vs_scheduler?對(duì)象。
ip_vs_scheduler?對(duì)象的?schedule?字段指向了一個(gè)調(diào)度算法函數(shù),通過(guò)這個(gè)調(diào)度函數(shù)可以從?ip_vs_service?對(duì)象的?ip_vs_dest?對(duì)象列表中選擇一個(gè)合適的真實(shí)服務(wù)器。
那么,ip_vs_service?對(duì)象和?ip_vs_dest?對(duì)象的信息怎么來(lái)的呢?答案是通過(guò)用戶配置創(chuàng)建。例如可以通過(guò)下面的命令來(lái)創(chuàng)建?ip_vs_service?對(duì)象和?ip_vs_dest?對(duì)象:
node1?>?$?ipvsadm?-A?-t?node1:80?-s?wrrnode1?>?$?ipvsadm?-a?-t?node1:80?-r?node2?-m?-w?3node1?>?$?ipvsadm?-a?-t?node1:80?-r?node3?-m?-w?5
第一行用于創(chuàng)建一個(gè)?ip_vs_service?對(duì)象,而第二和第三行用于向?ip_vs_service?對(duì)象添加?ip_vs_dest?對(duì)象到?destinations?列表中。關(guān)于 LVS 的配置這里不作詳細(xì)介紹,讀者可以參考其他關(guān)于 LVS 配置的資料。
ip_vs_service 對(duì)象創(chuàng)建
我們來(lái)看看 LVS 源碼是怎么創(chuàng)建一個(gè)?ip_vs_service?對(duì)象的,創(chuàng)建?ip_vs_service?對(duì)象通過(guò)?ip_vs_add_service()?函數(shù)完成,如下:
static intip_vs_add_service(struct ip_vs_rule_user *ur, struct ip_vs_service **svc_p){int ret = 0;struct ip_vs_scheduler *sched;struct ip_vs_service *svc = NULL;sched = ip_vs_scheduler_get(ur->sched_name); // 根據(jù)調(diào)度器名稱獲取調(diào)度策略對(duì)象...// 申請(qǐng)一個(gè) ip_vs_service 對(duì)象svc = (struct ip_vs_service *)kmalloc(sizeof(struct ip_vs_service), GFP_ATOMIC);...memset(svc, 0, sizeof(struct ip_vs_service));// 設(shè)置 ip_vs_service 對(duì)象的各個(gè)字段svc->protocol = ur->protocol; // 協(xié)議svc->addr = ur->vaddr; // 虛擬IPsvc->port = ur->vport; // 虛擬端口svc->fwmark = ur->vfwmark; // 防火墻標(biāo)記svc->flags = ur->vs_flags; // 標(biāo)志位svc->timeout = ur->timeout * HZ; // 超時(shí)時(shí)間svc->netmask = ur->netmask; // 網(wǎng)絡(luò)掩碼INIT_LIST_HEAD(&svc->destinations);svc->sched_lock = RW_LOCK_UNLOCKED;svc->stats.lock = SPIN_LOCK_UNLOCKED;ret = ip_vs_bind_scheduler(svc, sched); // 綁定調(diào)度器...ip_vs_svc_hash(svc); // 添加到ip_vs_service對(duì)象的hash表中...*svc_p = svc;return 0;}
先說(shuō)明一下,參數(shù)?ur?是用戶通過(guò)命令行配置的規(guī)則信息。上面的代碼主要完成以下幾個(gè)工作:
通過(guò)調(diào)用?
ip_vs_scheduler_get()?函數(shù)來(lái)獲取一個(gè)?ip_vs_scheduler?(調(diào)度器) 對(duì)象。然后申請(qǐng)一個(gè)?
ip_vs_service?對(duì)象并且根據(jù)用戶的配置設(shè)置其各個(gè)參數(shù),并且把調(diào)度器對(duì)象綁定這個(gè)?ip_vs_service?對(duì)象。最后把?
ip_vs_service?對(duì)象添加到?ip_vs_service?對(duì)象的全局哈希表中(這是由于可以創(chuàng)建多個(gè)?ip_vs_service?對(duì)象,這些對(duì)象通過(guò)一個(gè)全局哈希表來(lái)存儲(chǔ))。
ip_vs_dest 對(duì)象創(chuàng)建
創(chuàng)建?ip_vs_dest?對(duì)象通過(guò)?ip_vs_add_dest()?函數(shù)完成,代碼如下:
static int ip_vs_add_dest(struct ip_vs_service *svc, struct ip_vs_rule_user *ur){struct ip_vs_dest *dest;__u32 daddr = ur->daddr; // 目的IP__u16 dport = ur->dport; // 目的端口int ret;...// 調(diào)用 ip_vs_new_dest() 函數(shù)創(chuàng)建一個(gè) ip_vs_dest 對(duì)象ret = ip_vs_new_dest(svc, ur, &dest);...// 把 ip_vs_dest 對(duì)象添加到 ip_vs_service 對(duì)象的 destinations 列表中list_add(&dest->n_list, &svc->destinations);svc->num_dests++;/* 調(diào)用調(diào)度器的 update_service() 方法更新 ip_vs_service 對(duì)象 */svc->scheduler->update_service(svc);...return 0;}
ip_vs_add_dest()?函數(shù)主要通過(guò)調(diào)用?ip_vs_new_dest()?創(chuàng)建一個(gè)?ip_vs_dest?對(duì)象,然后將其添加到?ip_vs_service?對(duì)象的?destinations?列表中。我們來(lái)看看?ip_vs_new_dest()?函數(shù)的實(shí)現(xiàn):
static intip_vs_new_dest(struct ip_vs_service *svc,struct ip_vs_rule_user *ur,struct ip_vs_dest **destp){struct ip_vs_dest *dest;...*destp = dest = (struct ip_vs_dest*)kmalloc(sizeof(struct ip_vs_dest), GFP_ATOMIC);...memset(dest, 0, sizeof(struct ip_vs_dest));// 設(shè)置 ip_vs_dest 對(duì)象的各個(gè)字段dest->protocol = svc->protocol; // 協(xié)議dest->vaddr = svc->addr; // 虛擬IPdest->vport = svc->port; // 虛擬端口dest->vfwmark = svc->fwmark; // 虛擬網(wǎng)絡(luò)掩碼dest->addr = ur->daddr; // 真實(shí)IPdest->port = ur->dport; // 真實(shí)端口atomic_set(&dest->activeconns, 0);atomic_set(&dest->inactconns, 0);atomic_set(&dest->refcnt, 0);INIT_LIST_HEAD(&dest->d_list);dest->dst_lock = SPIN_LOCK_UNLOCKED;dest->stats.lock = SPIN_LOCK_UNLOCKED;__ip_vs_update_dest(svc, dest, ur);...return 0;}
ip_vs_new_dest()?函數(shù)的實(shí)現(xiàn)也比較簡(jiǎn)單,首先通過(guò)調(diào)用?kmalloc()?函數(shù)申請(qǐng)一個(gè)?ip_vs_dest?對(duì)象,然后根據(jù)用戶配置的規(guī)則信息來(lái)初始化?ip_vs_dest?對(duì)象的各個(gè)字段。
ip_vs_scheduler 對(duì)象
ip_vs_scheduler?(調(diào)度器) 對(duì)象用于從?ip_vs_service?對(duì)象的?destinations?列表中選擇一個(gè)合適的?ip_vs_dest?對(duì)象,其定義如下:
struct ip_vs_scheduler {struct list_head n_list; // 連接所有調(diào)度策略char *name; // 調(diào)度策略名稱atomic_t refcnt; // 應(yīng)用計(jì)數(shù)器struct module *module; // 模塊對(duì)象(如果是通過(guò)模塊引入的)int (*init_service)(struct ip_vs_service *svc); // 用于初始化服務(wù)int (*done_service)(struct ip_vs_service *svc); // 用于停止服務(wù)int (*update_service)(struct ip_vs_service *svc); // 用于更新服務(wù)// 用于獲取一個(gè)真實(shí)服務(wù)器對(duì)象 (Real-Server)struct ip_vs_dest *(*schedule)(struct ip_vs_service *svc, struct iphdr *iph);};
ip_vs_scheduler?對(duì)象的各個(gè)字段都在注釋說(shuō)明了,其中?schedule?字段是一個(gè)函數(shù)的指針,其指向一個(gè)調(diào)度函數(shù),用于從?ip_vs_service?對(duì)象的?destinations?列表中選擇一個(gè)合適的?ip_vs_dest?對(duì)象。
我們可以通過(guò)一個(gè)最簡(jiǎn)單的調(diào)度模塊(輪詢調(diào)度模塊)來(lái)分析?ip_vs_scheduler?對(duì)象的工作原理(文件路徑:/net/ipv4/ipvs/ip_vs_rr.c):
static struct ip_vs_scheduler ip_vs_rr_scheduler = {{0}, /* n_list */"rr", /* name */ATOMIC_INIT(0), /* refcnt */THIS_MODULE, /* this module */ip_vs_rr_init_svc, /* service initializer */ip_vs_rr_done_svc, /* service done */ip_vs_rr_update_svc,/* service updater */ip_vs_rr_schedule, /* select a server from the destination list */};
首先輪詢調(diào)度模塊定義了一個(gè)?ip_vs_scheduler?對(duì)象,其中?schedule?字段設(shè)置為?ip_vs_rr_schedule()?函數(shù)。我們來(lái)看看?ip_vs_rr_schedule()?函數(shù)的實(shí)現(xiàn):
static struct ip_vs_dest *ip_vs_rr_schedule(struct ip_vs_service *svc, struct iphdr *iph){register struct list_head *p, *q;struct ip_vs_dest *dest;write_lock(&svc->sched_lock);p = (struct list_head *)svc->sched_data; // 最后一次被調(diào)度的位置p = p->next;q = p;// 遍歷 destinations 列表do {if (q == &svc->destinations) {q = q->next;continue;}dest = list_entry(q, struct ip_vs_dest, n_list);// 找到一個(gè)權(quán)限值大于 0 的 ip_vs_dest 對(duì)象if (atomic_read(&dest->weight) > 0)goto out;q = q->next;} while (q != p);write_unlock(&svc->sched_lock);return NULL;out:svc->sched_data = q; // 設(shè)置最后一次被調(diào)度的位置...return dest;}
ip_vs_rr_schedule()?函數(shù)是輪詢調(diào)度算法的實(shí)現(xiàn),其實(shí)現(xiàn)原理如下:
ip_vs_service?對(duì)象的?sched_data?字段保存了最后一次調(diào)度的位置,所以每次調(diào)度時(shí)都是從這個(gè)字段讀取到最后一次調(diào)度的位置。從最后一次調(diào)度的位置開始遍歷,找到一個(gè)權(quán)限值(weight)大于 0 的?
ip_vs_dest?對(duì)象。如果找到就把?
ip_vs_service?對(duì)象的?sched_data?字段設(shè)置為最后被選擇的?ip_vs_dest?對(duì)象的位置。
其原理可以通過(guò)以下圖片說(shuō)明:

上圖描述的原理還是比較簡(jiǎn)單,首先從?sched_data?處開始遍歷,查找一個(gè)合適的?ip_vs_dest?對(duì)象,然后更新?sched_data?的位置。
另外,由于?LVS?可以存在多種不同的調(diào)度對(duì)象(提供不同的調(diào)度算法),所以?LVS?把這些調(diào)度對(duì)象通過(guò)一個(gè)鏈表(ip_vs_schedulers)存儲(chǔ)起來(lái),而這些調(diào)度對(duì)象可以通過(guò)調(diào)度對(duì)象的名字(name?字段)來(lái)查詢。
可以通過(guò)調(diào)用?register_ip_vs_scheduler()?函數(shù)向?LVS?注冊(cè)調(diào)度對(duì)象,而通過(guò)調(diào)用?ip_vs_scheduler_get()?函數(shù)來(lái)獲取指定名字的調(diào)度對(duì)象,這兩個(gè)函數(shù)的實(shí)現(xiàn)比較簡(jiǎn)單,這里就不作詳細(xì)介紹了。
ip_vs_conn 對(duì)象
ip_vs_conn?對(duì)象用于維護(hù)?客戶端?與?真實(shí)服務(wù)器?之間的關(guān)系,為什么需要維護(hù)它們之間的關(guān)系?原因是?TCP協(xié)議?面向連接的協(xié)議,所以每次調(diào)度都必須選擇相同的真實(shí)服務(wù)器,否則連接就會(huì)失效。

如上圖所示,剛開始時(shí)調(diào)度器選擇了?Real-Server(1)?服務(wù)器進(jìn)行處理客戶端請(qǐng)求,但第二次調(diào)度時(shí)卻選擇了?Real-Server(2)?來(lái)處理客戶端請(qǐng)求。
由于?TCP協(xié)議?需要客戶端與服務(wù)器進(jìn)行連接,但第二次請(qǐng)求的服務(wù)器發(fā)生了變化,所以連接狀態(tài)就失效了,這就為什么?LVS?需要維持客戶端與真實(shí)服務(wù)器連接關(guān)系的原因。
LVS?通過(guò)?ip_vs_conn?對(duì)象來(lái)維護(hù)客戶端與真實(shí)服務(wù)器之間的連接關(guān)系,其定義如下:
struct ip_vs_conn {struct list_head c_list; /* 用于連接到哈希表 */__u32 caddr; /* 客戶端IP地址 */__u32 vaddr; /* 虛擬IP地址 */__u32 daddr; /* 真實(shí)服務(wù)器IP地址 */__u16 cport; /* 客戶端端口 */__u16 vport; /* 虛擬端口 */__u16 dport; /* 真實(shí)服務(wù)器端口 */__u16 protocol; /* 協(xié)議類型(UPD/TCP) */.../* 用于發(fā)送數(shù)據(jù)包的接口 */int (*packet_xmit)(struct sk_buff *skb, struct ip_vs_conn *cp);...};
ip_vs_conn?對(duì)象各個(gè)字段的作用都在注釋中進(jìn)行說(shuō)明了,客戶端與真實(shí)服務(wù)器的連接關(guān)系就是通過(guò)?協(xié)議類型、客戶端IP、客戶端端口、虛擬IP?和?虛擬端口?來(lái)進(jìn)行關(guān)聯(lián)的,也就是說(shuō)根據(jù)這五元組能夠確定一個(gè)?ip_vs_conn?對(duì)象。
另外,在《原理篇》我們說(shuō)過(guò),LVS 有3中運(yùn)行模式:NAT模式、DR模式?和?TUN模式。而對(duì)于不同的運(yùn)行模式,發(fā)送數(shù)據(jù)包的接口是不一樣的,所以?ip_vs_conn?對(duì)象的?packet_xmit?字段會(huì)根據(jù)不同的運(yùn)行模式來(lái)選擇不同的發(fā)送數(shù)據(jù)包接口,綁定發(fā)送數(shù)據(jù)包接口是通過(guò)?ip_vs_bind_xmit()?函數(shù)完成,如下:
static inline void ip_vs_bind_xmit(struct ip_vs_conn *cp){switch (IP_VS_FWD_METHOD(cp)) {case IP_VS_CONN_F_MASQ: // NAT模式cp->packet_xmit = ip_vs_nat_xmit;break;case IP_VS_CONN_F_TUNNEL: // TUN模式cp->packet_xmit = ip_vs_tunnel_xmit;break;case IP_VS_CONN_F_DROUTE: // DR模式cp->packet_xmit = ip_vs_dr_xmit;break;...}}
一個(gè)客戶端請(qǐng)求到達(dá)?LVS?后,Director服務(wù)器?首先會(huì)查找客戶端是否已經(jīng)與真實(shí)服務(wù)器建立了連接關(guān)系,如果已經(jīng)建立了連接,那么直接使用這個(gè)連接關(guān)系。否則,通過(guò)調(diào)度器對(duì)象選擇一臺(tái)合適的真實(shí)服務(wù)器,然后創(chuàng)建客戶端與真實(shí)服務(wù)器的連接關(guān)系,并且保存到全局哈希表?ip_vs_conn_tab?中。流程圖如下:

上面對(duì)?LVS?各個(gè)角色都進(jìn)行了介紹,下面開始講解?LVS?對(duì)數(shù)據(jù)包的轉(zhuǎn)發(fā)過(guò)程。
3. 數(shù)據(jù)轉(zhuǎn)發(fā)
因?yàn)?LVS?是一個(gè)負(fù)載均衡工具,所以其最重要的功能就是對(duì)數(shù)據(jù)的調(diào)度與轉(zhuǎn)發(fā), 而對(duì)數(shù)據(jù)的轉(zhuǎn)發(fā)是在前面介紹的?Netfilter?鉤子函數(shù)進(jìn)行的。
對(duì)數(shù)據(jù)的轉(zhuǎn)發(fā)主要是通過(guò)?ip_vs_in()?和?ip_vs_out()?這兩個(gè)鉤子函數(shù):
ip_vs_in()?運(yùn)行在?Netfilter?的?LOCAL_IN?階段。ip_vs_out()?運(yùn)行在?Netfilter?的?FORWARD?階段。
FORWARD?階段發(fā)送在數(shù)據(jù)包不是發(fā)送給本機(jī)的情況,但是一般來(lái)說(shuō)數(shù)據(jù)包都是發(fā)送給本機(jī)的,所以對(duì)于?ip_vs_out()?這個(gè)函數(shù)的實(shí)現(xiàn)就不作介紹,我們主要重點(diǎn)分析?ip_vs_in()?這個(gè)函數(shù)。
ip_vs_in() 鉤子函數(shù)
有了前面的知識(shí)點(diǎn),我們對(duì)?ip_vs_in()?函數(shù)的分析就不那么困難了。下面我們分段對(duì)?ip_vs_in()?函數(shù)進(jìn)行分析:
static unsigned intip_vs_in(unsigned int hooknum,struct sk_buff **skb_p,const struct net_device *in,const struct net_device *out,int (*okfn)(struct sk_buff *)){struct sk_buff *skb = *skb_p;struct iphdr *iph = skb->nh.iph; // IP頭部union ip_vs_tphdr h;struct ip_vs_conn *cp;struct ip_vs_service *svc;int ihl;int ret;...// 因?yàn)長(zhǎng)VS只支持TCP和UDPif (iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_UDP)return NF_ACCEPT;ihl = iph->ihl << 2; // IP頭部長(zhǎng)度// IP頭部是否正確if (ip_vs_header_check(skb, iph->protocol, ihl) == -1)return NF_DROP;iph = skb->nh.iph; // IP頭部指針h.raw = (char*)iph + ihl; // TCP/UDP頭部指針
上面的代碼主要對(duì)數(shù)據(jù)包的?IP頭部?進(jìn)行正確性驗(yàn)證,并且將?iph?變量指向?IP頭部,而?h?變量指向?TCP/UDP?頭部。
// 根據(jù) "協(xié)議類型", "客戶端IP", "客戶端端口", "虛擬IP", "虛擬端口" 五元組獲取連接對(duì)象cp = ip_vs_conn_in_get(iph->protocol, iph->saddr,h.portp[0], iph->daddr, h.portp[1]);// 1. 如果連接還沒(méi)建立// 2. 如果是TCP協(xié)議的話, 第一個(gè)包必須是syn包, 或者UDP協(xié)議。// 3. 根據(jù)協(xié)議、虛擬IP和虛擬端口查找服務(wù)對(duì)象if (!cp &&(h.th->syn || (iph->protocol != IPPROTO_TCP)) &&(svc = ip_vs_service_get(skb->nfmark, iph->protocol, iph->daddr, h.portp[1]))){...// 通過(guò)調(diào)度器選擇一個(gè)真實(shí)服務(wù)器// 并且創(chuàng)建一個(gè)新的連接對(duì)象, 建立真實(shí)服務(wù)器與客戶端連接關(guān)系cp = ip_vs_schedule(svc, iph);...}
上面的代碼主要完成以下幾個(gè)功能:
根據(jù)?
協(xié)議類型、客戶端IP、客戶端端口、虛擬IP?和?虛擬端口?五元組,然后調(diào)用?ip_vs_conn_in_get()?函數(shù)獲取連接對(duì)象。如果連接還沒(méi)建立,那么就調(diào)用?
ip_vs_schedule()?函數(shù)調(diào)度一臺(tái)合適的真實(shí)服務(wù)器,然后創(chuàng)建一個(gè)連接對(duì)象,并且建立真實(shí)服務(wù)器與客戶端之間的連接關(guān)系。
我們來(lái)分析一下?ip_vs_schedule()?函數(shù)的實(shí)現(xiàn):
static struct ip_vs_conn *ip_vs_schedule(struct ip_vs_service *svc, struct iphdr *iph){struct ip_vs_conn *cp = NULL;struct ip_vs_dest *dest;const __u16 *portp;...portp = (__u16 *)&(((char *)iph)[iph->ihl*4]); // 指向TCP或者UDP頭部...dest = svc->scheduler->schedule(svc, iph); // 通過(guò)調(diào)度器選擇一臺(tái)合適的真實(shí)服務(wù)器...cp = ip_vs_conn_new(iph->protocol, // 協(xié)議類型iph->saddr, // 客戶端IPportp[0], // 客戶端端口iph->daddr, // 虛擬IPportp[1], // 虛擬端口dest->addr, // 真實(shí)服務(wù)器的IPdest->port ? dest->port : portp[1], // 真實(shí)服務(wù)器的端口0, // flagsdest);...return cp;}
ip_vs_schedule()?函數(shù)的主要工作如下:
首先通過(guò)調(diào)用調(diào)度器(
ip_vs_scheduler?對(duì)象)的?schedule()?方法從?ip_vs_service?對(duì)象的?destinations?鏈表中選擇一臺(tái)真實(shí)服務(wù)器(ip_vs_dest?對(duì)象)然后調(diào)用?
ip_vs_conn_new()?函數(shù)創(chuàng)建一個(gè)新的?ip_vs_conn?對(duì)象。
ip_vs_conn_new()?主要用于創(chuàng)建?ip_vs_conn?對(duì)象,并且根據(jù)?LVS?的運(yùn)行模式為其選擇正確的數(shù)據(jù)發(fā)送接口,其實(shí)現(xiàn)如下:
struct ip_vs_conn *ip_vs_conn_new(int proto, // 協(xié)議類型__u32 caddr, __u16 cport, // 客戶端IP和端口__u32 vaddr, __u16 vport, // 虛擬IP和端口__u32 daddr, __u16 dport, // 真實(shí)服務(wù)器IP和端口unsigned flags, struct ip_vs_dest *dest){struct ip_vs_conn *cp;// 創(chuàng)建一個(gè) ip_vs_conn 對(duì)象cp = kmem_cache_alloc(ip_vs_conn_cachep, GFP_ATOMIC);...// 設(shè)置 ip_vs_conn 對(duì)象的各個(gè)字段cp->protocol = proto;cp->caddr = caddr;cp->cport = cport;cp->vaddr = vaddr;cp->vport = vport;cp->daddr = daddr;cp->dport = dport;cp->flags = flags;...ip_vs_bind_dest(cp, dest); // 將 ip_vs_conn 與真實(shí)服務(wù)器對(duì)象進(jìn)行綁定...ip_vs_bind_xmit(cp); // 綁定一個(gè)發(fā)送數(shù)據(jù)的接口...ip_vs_conn_hash(cp); // 把 ip_vs_conn 對(duì)象添加到連接信息表中return cp;}
ip_vs_conn_new()?函數(shù)的主要工作如下:
創(chuàng)建一個(gè)新的?
ip_vs_conn?對(duì)象,并且設(shè)置其各個(gè)字段的值。調(diào)用?
ip_vs_bind_dest()?函數(shù)將?ip_vs_conn?對(duì)象與真實(shí)服務(wù)器對(duì)象(ip_vs_dest?對(duì)象)進(jìn)行綁定。根據(jù)?
LVS?的運(yùn)行模式,調(diào)用?ip_vs_bind_xmit()?函數(shù)為連接對(duì)象選擇一個(gè)正確的數(shù)據(jù)發(fā)送接口,ip_vs_bind_xmit()?函數(shù)在前面已經(jīng)介紹過(guò)。調(diào)用?
ip_vs_conn_hash()?函數(shù)把新創(chuàng)建的?ip_vs_conn?對(duì)象添加到全局連接信息哈希表中。
我們接著分析?ip_vs_in()?函數(shù):
if (cp->packet_xmit)ret = cp->packet_xmit(skb, cp); // 把數(shù)據(jù)包轉(zhuǎn)發(fā)出去else {ret = NF_ACCEPT;}...return ret;}
ip_vs_in()?函數(shù)的最后部分就是通過(guò)調(diào)用數(shù)據(jù)發(fā)送接口把數(shù)據(jù)包轉(zhuǎn)發(fā)出去,對(duì)于?NAT模式?來(lái)說(shuō),數(shù)據(jù)發(fā)送接口就是?ip_vs_nat_xmit()。
數(shù)據(jù)發(fā)送接口:ip_vs_nat_xmit()
接下來(lái),我們對(duì)?NAT模式?的數(shù)據(jù)發(fā)送接口?ip_vs_nat_xmit()?進(jìn)行分析。由于?ip_vs_nat_xmit()?函數(shù)的實(shí)現(xiàn)比較復(fù)雜,所以我們通過(guò)分段來(lái)分析:
static int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp){struct rtable *rt; /* Route to the other host */struct iphdr *iph;union ip_vs_tphdr h;int ihl;unsigned short size;int mtu;...iph = skb->nh.iph; // IP頭部ihl = iph->ihl << 2; // IP頭部長(zhǎng)度h.raw = (char*) iph + ihl; // 傳輸層頭部(TCP/UDP)size = ntohs(iph->tot_len) - ihl; // 數(shù)據(jù)長(zhǎng)度...// 找到真實(shí)服務(wù)器IP的路由信息if (!(rt = __ip_vs_get_out_rt(cp, RT_TOS(iph->tos))))goto tx_error_icmp;...// 替換新路由信息dst_release(skb->dst);skb->dst = &rt->u.dst;
上面的代碼主要完成兩個(gè)工作:
調(diào)用?
__ip_vs_get_out_rt()?函數(shù)查找真實(shí)服務(wù)器 IP 對(duì)應(yīng)的路由信息對(duì)象。把數(shù)據(jù)包的舊路由信息替換成新的路由信息。
我們接著分析:
iph->daddr = cp->daddr; // 修改目標(biāo)IP地址為真實(shí)服務(wù)器IP地址h.portp[1] = cp->dport; // 修改目標(biāo)端口為真實(shí)服務(wù)器端口...// 更新UDP/TCP頭部的校驗(yàn)和if (!cp->app && (iph->protocol != IPPROTO_UDP || h.uh->check != 0)) {ip_vs_fast_check_update(&h, cp->vaddr, cp->daddr, cp->vport,cp->dport, iph->protocol);if (skb->ip_summed == CHECKSUM_HW)skb->ip_summed = CHECKSUM_NONE;} else {switch (iph->protocol) {case IPPROTO_TCP:h.th->check = 0;h.th->check = csum_tcpudp_magic(iph->saddr, iph->daddr,size, iph->protocol,csum_partial(h.raw, size, 0));break;case IPPROTO_UDP:h.uh->check = 0;h.uh->check = csum_tcpudp_magic(iph->saddr, iph->daddr,size, iph->protocol,csum_partial(h.raw, size, 0));if (h.uh->check == 0)h.uh->check = 0xFFFF;break;}skb->ip_summed = CHECKSUM_UNNECESSARY;}
上面的代碼完成兩個(gè)工作:
修改目標(biāo)IP地址和端口為真實(shí)服務(wù)器IP地址和端口。
更新?
UDP/TCP 頭部?的校驗(yàn)和(checksum)。
我們接著分析:
ip_send_check(iph); // 計(jì)算IP頭部的校驗(yàn)和...skb->nfcache |= NFC_IPVS_PROPERTY;ip_send(skb); // 把包發(fā)送出去...return NF_STOLEN; // 讓其他 Netfilter 的鉤子函數(shù)放棄處理該包}
上面的代碼完成兩個(gè)工作:
調(diào)用?
ip_send_check()?函數(shù)重新計(jì)算數(shù)據(jù)包的?IP頭部?校驗(yàn)和。調(diào)用?
ip_send()?函數(shù)把數(shù)據(jù)包發(fā)送出去。
這樣,數(shù)據(jù)包的目標(biāo)IP地址和端口被替換成真實(shí)服務(wù)器的IP地址和端口,然后被發(fā)送到真實(shí)服務(wù)器處。至此,NAT模式?的分析已經(jīng)完畢。下面我們來(lái)總結(jié)一下整個(gè)流程:
當(dāng)數(shù)據(jù)包進(jìn)入到?
Director服務(wù)器?后,會(huì)被?LOCAL_IN階段?的?ip_vs_in()?鉤子函數(shù)進(jìn)行處理。ip_vs_in()?函數(shù)首先查找客戶端與真實(shí)服務(wù)器的連接是否存在,如果存在就使用這個(gè)真實(shí)服務(wù)器。否則通過(guò)調(diào)度算法對(duì)象選擇一臺(tái)最合適的真實(shí)服務(wù)器,然后建立客戶端與真實(shí)服務(wù)器的連接關(guān)系。根據(jù)運(yùn)行模式來(lái)選擇發(fā)送數(shù)據(jù)的接口(如?
NAT模式?對(duì)應(yīng)的是?ip_vs_nat_xmit()?函數(shù)),然后把數(shù)據(jù)轉(zhuǎn)發(fā)出去。轉(zhuǎn)發(fā)數(shù)據(jù)時(shí),首先會(huì)根據(jù)真實(shí)服務(wù)器的IP地址更新數(shù)據(jù)包的路由信息,然后再更新各個(gè)協(xié)議頭部的信息(如IP地址、端口和校驗(yàn)和等),然后把數(shù)據(jù)發(fā)送出去。
總結(jié)
本文主要分析 LVS 的實(shí)現(xiàn)原理,但由于本人能力有限,并且很多細(xì)節(jié)沒(méi)有分析,所以有問(wèn)題可以通過(guò)評(píng)論指出。另外,本文只介紹了?NAT模式?的原理,還有?DR模式?和?TUN模式?的沒(méi)有分析,有興趣可以自行閱讀源碼
