女主宣言

最近小编一直在做长连接相关的事情,最大的感触就是发版太痛苦,一个个踢掉连接然后发版,导致发版时长过长,操作繁琐。所以在想能不能实现优雅重启, 发版时客户端无感知。

PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!

难点

  • 如何做到不中断接收连接

  • 如何做到已有连接不中断

解决

如何做到不中断接受连接

以下是linux源码中bind的实现(linux-1.0)

// linux-1.0/net/socket.c 536
static int
sock_bind(int fd, struct sockaddr *umyaddr, int addrlen)
{struct socket *sock;int i;DPRINTF((net_debug, "NET: sock_bind: fd = %d\n", fd));if (fd < 0 || fd >= NR_OPEN || current->filp[fd] == NULL)return(-EBADF);//获取fd对应的socket结构if (!(sock = sockfd_lookup(fd, NULL))) return(-ENOTSOCK);// 转调用bind指向的函数,下层函数(inet_bind)if ((i = sock->ops->bind(sock, umyaddr, addrlen)) < 0) {DPRINTF((net_debug, "NET: sock_bind: bind failed\n"));return(i);}return(0);
}// linux-1.0/net/inet/sock.c 1012
static int
inet_bind(struct socket *sock, struct sockaddr *uaddr,int addr_len)
{...
outside_loop:for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)];sk2 != NULL; sk2 = sk2->next) {
#if     1   /* should be below! */if (sk2->num != snum) continue;
/*  if (sk2->saddr != sk->saddr) continue; */
#endifif (sk2->dead) {destroy_sock(sk2);goto outside_loop;}if (!sk->reuse) {sti();return(-EADDRINUSE);}if (sk2->num != snum) continue;        /* more than one */if (sk2->saddr != sk->saddr) continue;    /* socket per slot ! -FB */if (!sk2->reuse) {sti();return(-EADDRINUSE);}}...
}
  • sock_array是一个链式哈希表,保存着各端口号的sock结构

  • 通过源码可以看到,bind的时候会检测要绑定的地址和端口是否合法以及已被绑定, 如果发版时另一个进程和旧进程没有关系,则bind会返回错误Address already in use

  • 若旧进程fork出新进程,新进程和旧进程为父子关系,新进程继承旧进程的文件表,本身"本进程"就已经监听这个端口了,则不会出现上面的问题

如何做到已有连接不中断

  • 新进程继承旧进程的用于连接的fd,并且继续维持与客户端的心跳

    linux提供了unix域套接字可用于socket的传输, 新进程起来后通过unix socket通信继承旧进程所维护的连接

unix socket用于***一台***主机的进程间通信,不需要基于网络协议,主要是基于文件系统的。

#include <sys/types.h>
#include <sys/socket.h>ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

发送端调用sendmsg发送文件描述符,接收端调用revmsg接收文件描述符。

两进程共享同一打开文件表,这与fork之后的父子进程共享打开文件表的情况完全相同。

由此解决了文章开头提出的两个问题

Demo 实现

  • 进程每次启动时必须check有无继承socket(尝试连接本地的unix server,如果连接失败,说明是第一次启动,否则可能有继承的socket),如果有,就将socket加入到自己的连接池中, 并初始化连接状态

  • 旧进程监听USR2信号(通知进程需要重启,使用信号、http接口等都可),监听后动作:

  1. 监听Unix socket, 等待新进程初始化完成,发来开始继承连接的请求

  2. 使用旧进程启动的命令fork一个子进程(发布到线上的新二进制)。

  3. accept到新进程的请求,关闭旧进程listener(保证旧进程不会再接收新请求,同时所有connector不在进行I/O操作。

  4. 旧进程将现有连接的socket,以及连接状态(读写buffer,connect session)通过 unix socket发送到新进程。

  5. 最后旧进程给新进程发送发送完毕信号,随后退出

  • 以下是简单实现的demo, demo中实现较为简单,只实现了文件描述符的传递,没有实现各连接状态的传递。

  • // server.gopackage mainimport ("flag""fmt""golang.org/x/sys/unix""log""net""os""os/signal""path/filepath""sync""syscall""time"
    )var (workSpace stringlogger *log.LoggerwriteTimeout = time.Second * 5readTimeout  = time.Second * 5signalChan = make(chan os.Signal)connFiles sync.MapserverListener net.ListenerisUpdate = false
    )func init() {flag.StringVar(&workSpace, "w", ".", "Usage:\n ./server -w=workspace")flag.Parse()file, err := os.OpenFile(filepath.Join(workSpace, "server.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0777)if err != nil {panic(err)}logger = log.New(file, "", 11)go beforeStart()go signalHandler()
    }func main() {var err errorserverListener, err = net.Listen("tcp", ":7000")if err != nil {panic(err)}for {if isUpdate == true {continue}conn, err := serverListener.Accept()if err != nil {logger.Println("conn error")continue}c := conn.(*net.TCPConn)go connectionHandler(c)}
    }func connectionHandler(conn *net.TCPConn) {file, _ := conn.File()connFiles.Store(file, true)logger.Printf("conn fd %d\n", file.Fd())defer func() {connFiles.Delete(file)_ = conn.Close()}()for {if isUpdate == true {continue}err := conn.SetReadDeadline(time.Now().Add(readTimeout))if err != nil {logger.Println(err.Error())return}rBuf := make([]byte, 4)_, err = conn.Read(rBuf)if err != nil {logger.Println(err.Error())return}if string(rBuf) != "ping" {logger.Println("failed to parse the message " + string(rBuf))return}err = conn.SetWriteDeadline(time.Now().Add(writeTimeout))if err != nil {logger.Println(err.Error())return}_, err = conn.Write([]byte(`pong`))if err != nil {logger.Println(err.Error())return}}
    }func beforeStart() {connInterface, err := net.Dial("unix", filepath.Join(workSpace, "conn.sock"))if err != nil {logger.Println(err.Error())return}defer func() {_ = connInterface.Close()}()unixConn := connInterface.(*net.UnixConn)b := make([]byte, 1)oob := make([]byte, 32)for {err = unixConn.SetWriteDeadline(time.Now().Add(time.Minute * 3))if err != nil {fmt.Println(err.Error())return}n, oobn, _, _, err := unixConn.ReadMsgUnix(b, oob)if err != nil {logger.Println(err.Error())return}if n != 1 || b[0] != 0 {if n != 1 {logger.Printf("recv fd type error: %d\n", n)} else {logger.Println("init finish")}return}scms, err := unix.ParseSocketControlMessage(oob[0:oobn])if err != nil {logger.Println(err.Error())return}if len(scms) != 1 {logger.Printf("recv fd num != 1 : %d\n", len(scms))return}fds, err := unix.ParseUnixRights(&scms[0])if err != nil {logger.Println(err.Error())return}if len(fds) != 1 {logger.Printf("recv fd num != 1 : %d\n", len(fds))return}logger.Printf("recv fd %d\n", fds[0])file := os.NewFile(uintptr(fds[0]), "fd-from-old")conn, err := net.FileConn(file)if err != nil {logger.Println(err.Error())return}go connectionHandler(conn.(*net.TCPConn))}
    }func signalHandler() {signal.Notify(signalChan,syscall.SIGUSR2,)for {sc := <-signalChanswitch sc {case syscall.SIGUSR2:gracefulExit()default:continue}}
    }func gracefulExit() {var connWait sync.WaitGroup_ = syscall.Unlink(filepath.Join(workSpace, "conn.sock"))listenerInterface, err := net.Listen("unix", filepath.Join(workSpace, "conn.sock"))if err != nil {logger.Println(err.Error())return}defer func() {_ = listenerInterface.Close()}()unixListener := listenerInterface.(*net.UnixListener)connWait.Add(1)go func() {defer connWait.Done()unixConn, err := unixListener.AcceptUnix()if err != nil {logger.Println(err.Error())return}defer func() {_ = unixConn.Close()}()connFiles.Range(func(key, value interface{}) bool {if key == nil || value == nil {return false}file := key.(*os.File)defer func() {_ = file.Close()}()buf := make([]byte, 1)buf[0] = 0rights := syscall.UnixRights(int(file.Fd()))_, _, err := unixConn.WriteMsgUnix(buf, rights, nil)if err != nil {logger.Println(err.Error())}logger.Printf("send fd %d\n", file.Fd())return true})finish := make([]byte, 1)finish[0] = 1_, _, err = unixConn.WriteMsgUnix(finish, nil, nil)if err != nil {logger.Println(err.Error())}}()isUpdate = trueexecSpec := &syscall.ProcAttr{Env:   os.Environ(),Files: append([]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}),}pid, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)if err != nil {logger.Println(err.Error())return}logger.Printf("old process %d new process %d\n", os.Getpid(), pid)_ = serverListener.Close()connWait.Wait()os.Exit(0)
    }
    
    // client.go
    package mainimport ("fmt""net""time"
    )var (writeTimeout = time.Second * 5readTimeout  = time.Second * 5
    )func main() {conn, err := net.Dial("tcp", "127.0.0.1:7000")if err != nil {panic(err)}defer func() {conn.Close()}()for {time.Sleep(time.Second)err := conn.SetWriteDeadline(time.Now().Add(writeTimeout))if err != nil {fmt.Println(err.Error())break}fmt.Println("send ping")_, err = conn.Write([]byte(`ping`))if err != nil {fmt.Println(err.Error())break}err = conn.SetReadDeadline(time.Now().Add(readTimeout))if err != nil {fmt.Println(err.Error())break}rBuf := make([]byte, 4)_, err = conn.Read(rBuf)if err != nil {fmt.Println(err.Error())}fmt.Println("recv " + string(rBuf))}
    }
    

    360云计算

    由360云平台团队打造的技术分享公众号,内容涉及数据库、大数据、微服务、容器、AIOps、IoT等众多技术领域,通过夯实的技术积累和丰富的一线实战经验,为你带来最有料的技术分享

浅谈长连接的平滑重启相关推荐

  1. .net mysql和php mysql数据库连接_浅谈PHP连接MySQL数据库的三种方式

    本篇文章给大家介绍一下PHP连接MySQL数据库的三种方式(mysql.mysqli.pdo),结合实例形式分析了PHP基于mysql.mysqli.pdo三种方式连接MySQL数据库的相关操作技巧与 ...

  2. 浅谈APP的回收和重启机制

    /   今日科技快讯   / 近日不少用户发现微信开始内测在企业号入口增加微信公众号内容的推荐.具体情况是,在微信的服务通知的企业微信企业状态提醒中,会推送同事阅读较多的文章内容,而且微信还会选择性推 ...

  3. 深度学习之浅谈全连接层

    参考:https://www.zhihu.com/question/41037974 全连接层 全连接层(fully connected layers,FC)在整个卷积神经网络中起到"分类器 ...

  4. 浅谈各种连接池中连接数量的设置

    连接池中连接数量的配置 我们日常开发中经常会用到各种连接池,比如httpclient和jediscluster以及druid等数据库连接池,当使用这些连接池的时候我们总是很疑惑到底要怎么配置连接池中连 ...

  5. 浅谈MySQL连接查询与外键

    连接查询是同时查询多张表,通过多张表之间的关系得到最终的结果.连接查询又分成内连接.外链接和自然连接. 内连接:从左表中取出每一条记录,去右表中与所有的记录进行匹配;匹配必须是某个条件在左表中与右表中 ...

  6. 浅谈长轮询及其封装实现

    1. 简介 长轮询是与服务器保持即时通信的最简单的方式,它不使用任何特定的协议,例如 WebSocket ,所以也不依赖于浏览器版本等外部条件的兼容性,它很容易实现,也无需引入其他依赖,在很多场景下可 ...

  7. 浅谈linux的几种重启命令,linux用命令重启的两种方法(Linux重启关机命令经验之谈)...

    一般来说,Linux服务器都保存着重要文件和服务,不当使用将可能导致数据丢失甚至是灾难.同样,正确的关闭系统非常重要,本文将介绍常规安全的操作方法. 注意!!!重启或者关机之前,请紧慎评估对业务或客户 ...

  8. cnpm 网络不能连接_Android 架构之长连接技术

    读者好,很久没见,近期恢复更新啦,内容以Android技术.大前端.程序猿技术成长.跳槽内推等为主,欢迎继续关注. 上一篇文章<Android 架构之网络框架(上)>中,我们谈过了网络框架 ...

  9. 浅谈防火墙长连接与短连接

    浅谈防火墙长连接与短连接 引言:在银行项目日常投产中,开发人员会发现系统上线后应用访问数据库连接中断的问题,这很有可能是因为应用程序与数据库之间的连接使用了长连接.当应用程序与数据库建立的同一个TCP ...

最新文章

  1. tf.keras.losses.MeanAbsolutePercentageError 损失函数 示例
  2. 毕业论文 | 基于单片机的多功能智能小车设计论文(电路+程序+论文)
  3. 【通俗理解】锁存器,触发器,寄存器和缓冲器的区别
  4. 读书笔记---图解HTTP(一)
  5. [数据库] SQL语句select简单记录总结
  6. emacs python plugin_Windows上配置Emacs来开发Python及用Python扩展Emacs
  7. 为什么又要造一个叫 Latke 的轮子
  8. 数据告诉你:中年并不只有危机,创业或许正当时
  9. 赚钱的公式是资源加经营
  10. TensorFlow 学习(十四)—— contrib 与 slim
  11. java微信刷卡支付demo_微信刷卡支付例子
  12. 最大功率点跟踪测试软件,最大功率点跟踪(MPPT)
  13. 服务器系统能连wi-fi吗,电脑怎么用wifi连接iphone
  14. 批处理打开URL总结
  15. 无损压缩算法专题——无损压缩算法介绍
  16. B15 - 999、大数据组件学习⑫ - Hue
  17. 数据分析:杜邦分析法
  18. Lattice Mico8在LMS创建一个工程和创建LED程序
  19. inline 成员函数
  20. 发送邮件报错:452 Too many recipients

热门文章

  1. AtomicInteger 的使用
  2. Go学习笔记(一)windows下的Go 语言环境安装,并运行第一个Hello World程序
  3. SpringCloud发现服务代码(EurekaClient,DiscoveryClient)
  4. 设计模式之单例模式的多重实现
  5. python步态识别算法_译 | GaitSet:将步态作为序列的交叉视角步态识别(一)
  6. PyQt5笔记(01) -- 创建空白窗体
  7. c++使用单向链表存储一组有序数据_初试攻略丨计算机考研中数据结构知识点总结,硬核!...
  8. 安装配置Exchange 问题集
  9. python中字典和集合的使用
  10. 对于国产芯片何时能挑大梁