linux tcp repair及tcp热迁移

概念

比如docker等容器在不同的机器之间无缝迁移(可能由于调度,维护,交割等原因),是常见的需求场景
但是又希望不能中断服务,因此各种虚拟机和容器的热迁移就得到很多关注。
linux也在3.5版本中引入TCP_REPAIR socket选项来支持热迁移

获取状态及还原

当需要迁移的时候,为迁移的socket进入repair模式

setsockopt设置TCP_PREPAIR选项
进入repair模式的要求:

  • 需要CAP_NET_ADMIN, 用户命名空间需要有网络管理能力
  • socket处于CLOSE状态或ESTABLISHED状态

从内核读取缓存数据

内核缓存区中未发送或未被确认的数据,或者未被应用程序读取的数据
setsockopt设置TCP_REPAIR_QUEUE选项的值分别为TCP_SEND_QUEUE和TCP_RECV_QUEUE, 从两个缓存中读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
...
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK)) //只支持peek方式
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE) //从发送队列中读取
err = tcp_peek_sndq(sk, msg, len);
goto out;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
//正常的peek流程,peek接收队列数据
/* 'common' recv queue MSG_PEEK-ing */
}
...
}

迁移完,把这些数据还原到对应的socket缓存中,通过send()接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
...
if (unlikely(tp->repair)) {
if (tp->repair_queue == TCP_RECV_QUEUE) {
copied = tcp_send_rcvq(sk, msg, size); //放到接收队列
goto out_nopush;
}
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out_err;
//TCP_SEND_QUEUE, 正常模式处理
/* 'common' sending to sendq */
}
...
//正常的copy到内核发送缓存,准备发送的处理过程
...
}

读取和还原tcp协商信息

当握手的时候,会用tcp扩展选项来协商支持的情况,比如sack,timestamp,wscale等
getsockopt TCP_REPAIR_OPTIONS选项来获取这些值, 迁移之后setsockopt还原这些值

读取和还原序号

1
2
3
4
5
6
7
8
case TCP_QUEUE_SEQ:
if (tp->repair_queue == TCP_SEND_QUEUE)
val = tp->write_seq;
else if (tp->repair_queue == TCP_RECV_QUEUE)
val = tp->rcv_nxt;
else
return -EINVAL;
break;

读取和还原MSS

1
2
3
4
5
6
7
case TCP_MAXSEG:
val = tp->mss_cache;
if (!val && ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)))
val = tp->rx_opt.user_mss;
if (tp->repair)
val = tp->rx_opt.mss_clamp;
break;

读取和还原滑动窗口信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case TCP_REPAIR_WINDOW: {
struct tcp_repair_window opt;
if (get_user(len, optlen))
return -EFAULT;
if (len != sizeof(opt))
return -EINVAL;
if (!tp->repair)
return -EPERM;
opt.snd_wl1 = tp->snd_wl1;
opt.snd_wnd = tp->snd_wnd;
opt.max_window = tp->max_window;
opt.rcv_wnd = tp->rcv_wnd;
opt.rcv_wup = tp->rcv_wup;
if (copy_to_user(optval, &opt, len))
return -EFAULT;
return 0;
}

静默关闭连接

close()将静默关闭,不会发送FIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void tcp_close(struct sock *sk, long timeout)
{
if (unlikely(tcp_sk(sk)->repair)) {
sk->sk_prot->disconnect(sk, 0);
} else if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
/* Check zero linger _after_ checking for unread data. */
sk->sk_prot->disconnect(sk, 0);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
} else if (tcp_close_state(sk)) {
tcp_send_fin(sk);
}
...
}
int tcp_disconnect(struct sock *sk, int flags)
{
struct inet_sock *inet = inet_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
int err = 0;
int old_state = sk->sk_state;
if (old_state != TCP_CLOSE)
tcp_set_state(sk, TCP_CLOSE);
/* ABORT function of RFC793 */
if (old_state == TCP_LISTEN) {
inet_csk_listen_stop(sk);
} else if (unlikely(tp->repair)) {
sk->sk_err = ECONNABORTED;
}
...
}

还原连接

connect()直接进入ESTABLISH状态, 然后setsockopt各个状态选项,再用send()两个缓存到对应内核队列中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
tcp_connect_init(sk);//初始化tcp配置,选项,滑动窗口等
if (unlikely(tp->repair)) {
tcp_finish_connect(sk, NULL); //repair模式直接进入TCP_ESTABLISHED状态
return 0;
}
...
//发送握手包
}

资料

TCP connection repair
CRIU