1、保活的問題
以前一個同事問起一個問題:服務器一般不會主動檢測客戶端是否依然有效,在這種狀況下,若是客戶端異常退出後服務器依然維護着這條鏈路,隨着時間的推移,過多的無效連接最終將會把服務器的資源消耗殆盡。舉個例子:假設客戶端是一個手機終端,用戶能夠摳出電池重啓系統,這種狀況下客戶端的TCP協議棧沒有機會向服務器發送FIN包來完成正常的斷鏈過程,因此服務器沒法感知到該鏈路的終結。若是這樣的無效鏈路愈來愈多,將會嚴重影響服務器的服務能力。
如今再把場景具體一點,假設服務器接入層使用了LVS,此時若是客戶端出現這種狀況,服務器將會有什麼樣的表現行爲?
2、LVS對於鏈路狀態的維護
有一點是肯定的,雖而後端服務器的回包能夠不通過LVS而直接返回給客戶端,可是入包都會通過LVS轉發,因此對於這種狀況,咱們只須要先看下LVS對於入包的處理流程便可。linux-2.6.17\net\ipv4\ipvs\ip_vs_core.c
static unsigned int
ip_vs_in(unsigned int hooknum, struct sk_buff **pskb,
const struct net_device *in, const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
……
restart = ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pp);
……
ip_vs_conn_put(cp);
return ret;
}
這裏注意到,其中的ip_vs_set_state函數會在每一個包到來的時候都被調用便可。
static inline int
ip_vs_set_state(struct ip_vs_conn *cp, int direction,
const struct sk_buff *skb,
struct ip_vs_protocol *pp)
{
if (unlikely(!pp->state_transition))
return 0;
return pp->state_transition(cp, direction, skb, pp);
}
對於TCP來講,它執行的狀態轉換函數爲tcp_state_transition
linux-2.6.17\net\ipv4\ipvs\ip_vs_proto_tcp.c
/*
* Handle state transitions
*/
static int
tcp_state_transition(struct ip_vs_conn *cp, int direction,
const struct sk_buff *skb,
struct ip_vs_protocol *pp)
{
struct tcphdr _tcph, *th;
th = skb_header_pointer(skb, skb->nh.iph->ihl*4,
sizeof(_tcph), &_tcph);
if (th == NULL)
return 0;
spin_lock(&cp->lock);
set_tcp_state(pp, cp, direction, th);
spin_unlock(&cp->lock);
return 1;
}
static inline void
set_tcp_state(struct ip_vs_protocol *pp, struct ip_vs_conn *cp,
int direction, struct tcphdr *th)
{
……
cp->timeout = pp->timeout_table[cp->state = new_state];
}
3、鏈路定時器的使用
你們可能不會注意到,在ip_vs_in函數最後的ip_vs_conn_put函數,它默默無聞的完成了這個鏈條的最後一環。
linux-2.6.17\net\ipv4\ipvs\ip_vs_conn.c
void ip_vs_conn_put(struct ip_vs_conn *cp)
{
/* reset it expire in its timeout */
mod_timer(&cp->timer, jiffies+cp->timeout);
__ip_vs_conn_put(cp);
}
函數使用最新的超時時間來修改定時器,這意味着當每個入包通過服務器的時候,都將會修更新該鏈路對應的定時器。反過來講,在此時設置的超時時間是100秒,那麼在定時器超時之間沒有在收到指望的入包,就會觸發鏈路的後續操做。
4、以TCP的ESTABLISHED狀態爲例
TCP使用的超時表爲tcp_timeouts,能夠看到其中的超時時間配置爲15*60*HZ,也就是ESTABLISHED狀態下,若是15分鐘沒有包在該鏈路上再次通過,則認爲鏈路已經超時。
linux-2.6.17\net\ipv4\ipvs\ip_vs_proto_tcp.c
/*
* Timeout table[state]
*/
static int tcp_timeouts[IP_VS_TCP_S_LAST+1] = {
[IP_VS_TCP_S_NONE] = 2*HZ,
[IP_VS_TCP_S_ESTABLISHED] = 15*60*HZ,
[IP_VS_TCP_S_SYN_SENT] = 2*60*HZ,
[IP_VS_TCP_S_SYN_RECV] = 1*60*HZ,
[IP_VS_TCP_S_FIN_WAIT] = 2*60*HZ,
[IP_VS_TCP_S_TIME_WAIT] = 2*60*HZ,
[IP_VS_TCP_S_CLOSE] = 10*HZ,
[IP_VS_TCP_S_CLOSE_WAIT] = 60*HZ,
[IP_VS_TCP_S_LAST_ACK] = 30*HZ,
[IP_VS_TCP_S_LISTEN] = 2*60*HZ,
[IP_VS_TCP_S_SYNACK] = 120*HZ,
[IP_VS_TCP_S_LAST] = 2*HZ,
};
當鏈路超時以後
static void ip_vs_conn_expire(unsigned long data)
{
struct ip_vs_conn *cp = (struct ip_vs_conn *)data;
cp->timeout = 60*HZ;
/*
* hey, I'm using it
*/
atomic_inc(&cp->refcnt);
/*
* do I control anybody?
*/
if (atomic_read(&cp->n_control))
goto expire_later;
/*
* unhash it if it is hashed in the conn table
*/
if (!ip_vs_conn_unhash(cp))
goto expire_later;
/*
* refcnt==1 implies I'm the only one referrer
*/
if (likely(atomic_read(&cp->refcnt) == 1)) {
/* delete the timer if it is activated by other users */
if (timer_pending(&cp->timer))
del_timer(&cp->timer);
/* does anybody control me? */
if (cp->control)
ip_vs_control_del(cp);
if (unlikely(cp->app != NULL))
ip_vs_unbind_app(cp);
ip_vs_unbind_dest(cp);
if (cp->flags & IP_VS_CONN_F_NO_CPORT)
atomic_dec(&ip_vs_conn_no_cport_cnt);
atomic_dec(&ip_vs_conn_count);
kmem_cache_free(ip_vs_conn_cachep, cp);
return;
}
/* hash it back to the table */
ip_vs_conn_hash(cp);
expire_later:
IP_VS_DBG(7, "delayed: conn->refcnt-1=%d conn->n_control=%d\n",
atomic_read(&cp->refcnt)-1,
atomic_read(&cp->n_control));
ip_vs_conn_put(cp);
}
從這個操做流程上來看,lvs只是簡單的刪除了本身本地維護的路由信息,而沒有進行額外的TCP斷鏈(也就是給realserver發送FIN進行正常關閉連接的優雅處理),由於LVS自己並無socket這個概念,它本質上說是維護了一個鏈路的路由信息。這也就是說,雖然LVS內部實現了TCP鏈路的超時回收,可是這個超時回收只是回收本身本地資源,不會進行TCP標準協議的跨機處理。
5、假設被LVS回收的鏈路再次直接發送數據包過來會如何
linux-2.6.21\net\ipv4\ipvs\ip_vs_core.c
static unsigned int
ip_vs_in(unsigned int hooknum, struct sk_buff **pskb,
const struct net_device *in, const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
……
if (unlikely(!cp)) {
int v;
if (!pp->conn_schedule(skb, pp, &v, &cp))
return v;
}
if (unlikely(!cp)) {
/* sorry, all this trouble for a no-hit :) */
IP_VS_DBG_PKT(12, pp, skb, 0,
"packet continues traversal as normal");
return NF_ACCEPT;
}
……
}
對於TCP進入該函數處理,能夠看到,在sched的時候,若是包頭中沒有syn標誌位,此時並不會建立一個新的鏈接,而且返回1。進而致使在ip_vs_in函數中沒有繼續處理,而是把它看成一個普通的數據包上傳,而這個上傳會到達本機的協議棧中,因爲本機協議棧中沒有這個socket,因此客戶端可能會收到reset消息被重置。
static int
tcp_conn_schedule(struct sk_buff *skb,
struct ip_vs_protocol *pp,
int *verdict, struct ip_vs_conn **cpp)
{
……
if (th->syn &&
(svc = ip_vs_service_get(skb->mark, skb->nh.iph->protocol,
skb->nh.iph->daddr, th->dest))) {
……
}
return 1;
}
關於這個問題,在stackoverflow網站的一個回覆的註釋中
有一個說明:
The connections that you can see and change with ipvsadm are used to that the connection doesn't break between the load balancer and the real server. If you have two servers in the pool and the session on the load balancer times out after 1 second and I am using telnet, if I do nothing for a couple of seconds and then run a command the connection may appear on a different backend server and I will get disconnected. The connection may still be valid on the real backend server (as it wasn't closed) but that is assuming ipvs doesn't close the connection when it has a low timeout set. – shthead Dec 15 '13 at 4:51
6、persistent的實現
一、首先目的地址要經過配置使能了persistent選項
struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, const struct sk_buff *skb)
{
……
if (svc->flags & IP_VS_SVC_F_PERSISTENT)
return ip_vs_sched_persist(svc, skb, pptr);
……
}
能夠看到,所謂的「模版」,在內部實現的時候一樣是掛靠在已經創建的常規鏈路管理結構上的,只是這些鏈路添加了IP_VS_CONN_F_TEMPLATE標誌位。
static struct ip_vs_conn *
ip_vs_sched_persist(struct ip_vs_service *svc,
const struct sk_buff *skb,
__be16 ports[2])
{
……
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr, 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
……
}
/* Get reference to connection template */
struct ip_vs_conn *ip_vs_ct_in_get
(int protocol, __be32 s_addr, __be16 s_port, __be32 d_addr, __be16 d_port)
{
……
list_for_each_entry(cp, &ip_vs_conn_tab[hash], c_list) {
if (s_addr==cp->caddr && s_port==cp->cport &&
d_port==cp->vport && d_addr==cp->vaddr &&
cp->flags & IP_VS_CONN_F_TEMPLATE &&
protocol==cp->protocol) {
/* HIT */
atomic_inc(&cp->refcnt);
goto out;
}
}
cp = NULL;
……
}
二、從ip_vs_conn_tab超找時如何區分template和普通鏈路
以網段爲例,這裏傳給ip_vs_ct_in_get的參數不是一個有效的地址,例如svc->fwmark或者port都不是一個實實在在的有效地址,因此一般不會和真實地址衝突
static struct ip_vs_conn *
ip_vs_sched_persist(struct ip_vs_service *svc,
const struct sk_buff *skb,
__be16 ports[2])
{
……
if (svc->fwmark)
ct = ip_vs_ct_in_get(IPPROTO_IP, snet, 0,
htonl(svc->fwmark), 0);
else
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, 0);
7、Direct Routing策略的實現
在LVS的三種策略中,直觀上看,DR是一種在協議層中直接修改網絡報MAC地址的實現方法,這種方法的優勢是realserver的網絡協議棧對於load balancer的感知最弱。
linux-2.6.21\net\ipv4\ipvs\ip_vs_xmit.c
int
ip_vs_dr_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
struct ip_vs_protocol *pp)
{
……
if (!(rt = __ip_vs_get_out_rt(cp, RT_TOS(iph->tos))))
goto tx_error_icmp;
……
/* drop old route */
dst_release(skb->dst);
skb->dst = &rt->u.dst;
/* Another hack: avoid icmp_send in ip_fragment */
skb->local_df = 1;
IP_VS_XMIT(skb, rt);
LeaveFunction(10);
return NF_STOLEN;
……
}
從實現上看,在發送的時候是經過__ip_vs_get_out_rt函數,把選中的realserver做爲目的地址從路由表中查詢到下一條的位置,而後把這個下一條做爲發送的目的entry傳送給鏈路層的發送接口。而一般來講,本機的報文發送都是以ip層的目的地址爲目標進行路由選擇。例如,在TCP發送報文時,
int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
……
/* Use correct destination address if we have options. */
daddr = inet->daddr;
if(opt && opt->srr)
daddr = opt->faddr;
{
struct flowi fl = { .oif = sk->sk_bound_dev_if,
.nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = inet->saddr,
.tos = RT_CONN_FLAGS(sk) } },
.proto = sk->sk_protocol,
.uli_u = { .ports =
{ .sport = inet->sport,
.dport = inet->dport } } };
……
}
其中使用的就是daddr = inet->daddr;,這個也就是socket中使用的目的地址。
8、說明
lvs我沒有使用過,也沒有搭建一個實際環境測試,因此前面的內容都只是根據代碼分析、推測的結論,並無在環境驗證。這些問題是最先了解lvs實現時想到的一些問題,如今簡單整理下作個備份。