1. 热更新
热更新的目的是保证新进程启动,老的进程可以处理完已经处理的连接,然后退出。通用的方法是父子进程共享文件句柄方法,热更新时,新创建新的子进程,然后通知老的子进程退出。
这个方案有两个问题:
- 父进程一直活跃,这样才能fork新子进程。
- 父进程需要知道业务监听哪些端口
2. reuseport的方法
reuseport能够在两个不相关的进程监听同一端口,有负载均衡请求能力。在热更新的过程中,启动新进程,通知老进程关闭端口,处理完已经接收的连接后退出。
但在实际过程中,这样处理过程并不完美,监听端口有半连接或者全连接的队列没有处理完成,导致关闭过程中连接被reset。
3. 源码分析
shutdown丢弃连接的源码如下:
int inet_shutdown(struct socket *sock, int how)
{
switch (sk->sk_state) {
case TCP_LISTEN:
if (!(how & RCV_SHUTDOWN)) //
break;
fallthrough;
case TCP_SYN_SENT:
err = sk->sk_prot->disconnect(sk, O_NONBLOCK);
sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
break;
}
}
int tcp_disconnect(struct sock *sk, int flags)
{
int old_state = sk->sk_state;
/* ABORT function of RFC793 */
if (old_state == TCP_LISTEN) {
inet_csk_listen_stop(sk);
}
}
void inet_csk_listen_stop(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
...
while ((req = reqsk_queue_remove(queue, sk)) != NULL) {
struct sock *child = req->sk;
inet_child_forget(sk, req, child);
}
..
}
static void inet_child_forget(struct sock *sk, struct request_sock *req,
struct sock *child)
{
sk->sk_prot->disconnect(child, O_NONBLOCK);
...
inet_csk_destroy_sock(child);
}
4 解决思路
目前linux不支持,这种解决的办法可以有3种:
- 增加一种状态gracefulshutdown状态,如果新syn连接过来,发现reuseport状态,不分配此sock,已经建立的正常逻辑走,不过这个状态不好维护,需要应用层来决定关闭的时刻
- 在关闭reuseport的时候,把队列内的连接重新hash到其他进程(为了均匀性还是保持hash状态),队列迁移
- bpf方案