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

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

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

從上圖可以看出,ip_vs_service?對象的?destinations?字段用于保存?ip_vs_dest?對象的列表,而?scheduler?字段指向了一個?ip_vs_scheduler?對象。
ip_vs_scheduler?對象的?schedule?字段指向了一個調(diào)度算法函數(shù),通過這個調(diào)度函數(shù)可以從?ip_vs_service?對象的?ip_vs_dest?對象列表中選擇一個合適的真實服務(wù)器。
那么,ip_vs_service?對象和?ip_vs_dest?對象的信息怎么來的呢?答案是通過用戶配置創(chuàng)建。例如可以通過下面的命令來創(chuàng)建?ip_vs_service?對象和?ip_vs_dest?對象:
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)建一個?ip_vs_service?對象,而第二和第三行用于向?ip_vs_service?對象添加?ip_vs_dest?對象到?destinations?列表中。關(guān)于 LVS 的配置這里不作詳細介紹,讀者可以參考其他關(guān)于 LVS 配置的資料。
ip_vs_service 對象創(chuàng)建
我們來看看 LVS 源碼是怎么創(chuàng)建一個?ip_vs_service?對象的,創(chuàng)建?ip_vs_service?對象通過?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)度策略對象...// 申請一個 ip_vs_service 對象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 對象的各個字段svc->protocol = ur->protocol; // 協(xié)議svc->addr = ur->vaddr; // 虛擬IPsvc->port = ur->vport; // 虛擬端口svc->fwmark = ur->vfwmark; // 防火墻標記svc->flags = ur->vs_flags; // 標志位svc->timeout = ur->timeout * HZ; // 超時時間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對象的hash表中...*svc_p = svc;return 0;}
先說明一下,參數(shù)?ur?是用戶通過命令行配置的規(guī)則信息。上面的代碼主要完成以下幾個工作:
通過調(diào)用?
ip_vs_scheduler_get()?函數(shù)來獲取一個?ip_vs_scheduler?(調(diào)度器) 對象。然后申請一個?
ip_vs_service?對象并且根據(jù)用戶的配置設(shè)置其各個參數(shù),并且把調(diào)度器對象綁定這個?ip_vs_service?對象。最后把?
ip_vs_service?對象添加到?ip_vs_service?對象的全局哈希表中(這是由于可以創(chuàng)建多個?ip_vs_service?對象,這些對象通過一個全局哈希表來存儲)。
ip_vs_dest 對象創(chuàng)建
創(chuàng)建?ip_vs_dest?對象通過?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)建一個 ip_vs_dest 對象ret = ip_vs_new_dest(svc, ur, &dest);...// 把 ip_vs_dest 對象添加到 ip_vs_service 對象的 destinations 列表中list_add(&dest->n_list, &svc->destinations);svc->num_dests++;/* 調(diào)用調(diào)度器的 update_service() 方法更新 ip_vs_service 對象 */svc->scheduler->update_service(svc);...return 0;}
ip_vs_add_dest()?函數(shù)主要通過調(diào)用?ip_vs_new_dest()?創(chuàng)建一個?ip_vs_dest?對象,然后將其添加到?ip_vs_service?對象的?destinations?列表中。我們來看看?ip_vs_new_dest()?函數(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 對象的各個字段dest->protocol = svc->protocol; // 協(xié)議dest->vaddr = svc->addr; // 虛擬IPdest->vport = svc->port; // 虛擬端口dest->vfwmark = svc->fwmark; // 虛擬網(wǎng)絡(luò)掩碼dest->addr = ur->daddr; // 真實IPdest->port = ur->dport; // 真實端口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ù)的實現(xiàn)也比較簡單,首先通過調(diào)用?kmalloc()?函數(shù)申請一個?ip_vs_dest?對象,然后根據(jù)用戶配置的規(guī)則信息來初始化?ip_vs_dest?對象的各個字段。
ip_vs_scheduler 對象
ip_vs_scheduler?(調(diào)度器) 對象用于從?ip_vs_service?對象的?destinations?列表中選擇一個合適的?ip_vs_dest?對象,其定義如下:
struct ip_vs_scheduler {struct list_head n_list; // 連接所有調(diào)度策略char *name; // 調(diào)度策略名稱atomic_t refcnt; // 應(yīng)用計數(shù)器struct module *module; // 模塊對象(如果是通過模塊引入的)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ù)// 用于獲取一個真實服務(wù)器對象 (Real-Server)struct ip_vs_dest *(*schedule)(struct ip_vs_service *svc, struct iphdr *iph);};
ip_vs_scheduler?對象的各個字段都在注釋說明了,其中?schedule?字段是一個函數(shù)的指針,其指向一個調(diào)度函數(shù),用于從?ip_vs_service?對象的?destinations?列表中選擇一個合適的?ip_vs_dest?對象。
我們可以通過一個最簡單的調(diào)度模塊(輪詢調(diào)度模塊)來分析?ip_vs_scheduler?對象的工作原理(文件路徑:/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)度模塊定義了一個?ip_vs_scheduler?對象,其中?schedule?字段設(shè)置為?ip_vs_rr_schedule()?函數(shù)。我們來看看?ip_vs_rr_schedule()?函數(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);// 找到一個權(quán)限值大于 0 的 ip_vs_dest 對象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)度算法的實現(xiàn),其實現(xiàn)原理如下:
ip_vs_service?對象的?sched_data?字段保存了最后一次調(diào)度的位置,所以每次調(diào)度時都是從這個字段讀取到最后一次調(diào)度的位置。從最后一次調(diào)度的位置開始遍歷,找到一個權(quán)限值(weight)大于 0 的?
ip_vs_dest?對象。如果找到就把?
ip_vs_service?對象的?sched_data?字段設(shè)置為最后被選擇的?ip_vs_dest?對象的位置。
其原理可以通過以下圖片說明:

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

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

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