Web建站:服务器程序“进程备份”


  
引子:
笔者在尝试建站。做软件者,自然最担心服务器程序崩溃;即便能重启,也要损失一些资源。这就产生了一种需要: 能否象数据可以备份从而损失时可以用于恢复一样,让服务器程序在崩溃情况下可以有个备份来用于恢复?读到过 Richard Stevens在《UNIX环境高级编程》中论述过的“传递文件描述符”,这种强大的功能象进程之间的dup/dup2有象 生成备份的意思,让我产生了想法:使用它来实现“进程备份”;当然了,这个不是备份一个进程“映像”,而是“备份” 一个进程中的文件描述符,包括我希望处理的网络连接的套接字。写了个小程序试了一下,成功。还有待于在大规模 服务程序中试用验证。
 
 下面试验的思路是: 
(1)模拟两客户进程“聊天”;          
                                    S
                                   / \                                    
                                  C1  C2
                                  
(2)对Server进行“进程备份”;      
                                  S -> B
(3)Master发指令,原Server "crash";
(4)发现crash,启动新进程,监听原端口(比如websocket聊天端口);
(5)为新进程准备好fd、用户关系等资源;
(6)看两客户clients还能否继续聊天。
 
源码:server_crash_backup-restore_test_2.tgz 下载
该试验代码未加整理,:-(。
  
  
  该tgz文件解压缩后目录结构如下:
  
  
    
  目录reuse_test/ 、 pass_fd_and_struct_test/可以删去。建议保留空目录tmp/,用途见下。
  用到四个程序:master( server)、(data )server、(data )client、backup_process,各使用
  一个单独的目录。
  我们会用两个data client进程。我这儿意为两个聊天客户(data client)通过聊天服务器
  (data server)通信,担心聊天服务器崩溃,将其向backup_process进程“备份”,master则是
  试验时用来发指令的,每个发出的指令会发给所有slave,每个slave选择自己需要处理的指令
  执行相应的动作。
  master外的其他程序都是master的slave/client,(data )client是(data )server的client,
  data server又是backup_process的client。
  
  先进入各个目录运行make编译。
  
  由于backup进程和data server这里通过(有名)UNIX域套接字通信,要指定路径,在
  backup_process/client.cpp中,改
   const char* path="/usr/working3/server_crash_backup-restore_test/tmp/tmp.sock"
  为你指定的路径,我在百度BCC上测试,改为
   const char* path="/usr/working/test/server_crash_backup-restore_test_2/tmp/tmp.sock";
  在server_test/client.cpp中作同样改动。
  再编译。
  
  启动master,
                     (cd master-server_test ;)
                     ./server_crash_test-master 5000 3
                          参数3表示master开始将有3个slave,两个data client,
                          一个data server。
  启动(data )server,
                     (cd server_test/ ;)
                     ./server_test 5001
  启动clients:
                     (cd client_test;)
                     ./client_test 127.0.0.1 5001 c1           (客户1)
                     ./client_test 127.0.0.1 5001 c2           (客户2)
  这时master界面上提示等待输入指令。(所有指令均由master发出,不区分大小写字母。)
  >

         
         


  输入若干1to2 / 2to1 ,(模拟client 1和client 2聊天,) 收到消息的client端会显示
  接收到消息的序号(#1、#2……)。(输入下一条指令时可能需要回车一下显示提示符。)
         
         
         
         
  
  现在启动backup进程,
        >create
        应显示backup process started OK及已连接上master。 master又多了一个slave。
         
         
  进行备份,
         >backup
         
  然后来观察server crash,
  >crash
         
         
        观察server界面,应有segmentation fault (core dumped)。
         
         
  >restart 重启server(以参数restart:这次data server不再accept连接)
        应有new data server started OK. ps命令观察有server进程重新出现。
         
         
  >restore
        恢复
         
         
  
  
  再在>提示符下输入1to2 / 2to1指令,看到clients之间能继续正常收发数据,好象没有
  server crash一样。
         
         
         
         
  
  退出试验:
  >exit
  
 
下面顺序详述上面的过程及指令使用:
 启动Master,slaves 3个;
 启动Server,accept 2个连接;
 以参数C1启动Client 1(简称C1),连接Server,Server建立结构体信息;
 以参数C2启动Client 2(简称C2),连接Server,Server建立结构体信息;
 
server_test\server.cpp:
       data_sock  = server( service);
       data_sock2 = server( service);
       
       fd_info[0].fd      = data_sock;
       fd_info[0].owner   = 1;
       fd_info[0].target  = 2;
       fd_info[0].paired  = 1;
       fd_info[0].fd_orig = data_sock;
       
       fd_info[1].fd      = data_sock2;
       fd_info[1].owner   = 2;
       fd_info[1].target  = 1;
       fd_info[1].paired  = 1;
       fd_info[1].fd_orig = data_sock2;
 
 
 1TO2/2TO1用来指示Server和Client(指令目标):
 1TO2   :Server准备从C1接收;C1发给Server;Server寻找C2;Server发给C2;C2准备接收;C2接收;
 2TO1   :Server准备从C2接收;C2发给Server;Server寻找C1;Server发给C1;C1准备接收;C1接收;
                              这里是些普通的socket通信的代码。
 CREATE :指令目标:Master自身。
                             (Master)启动Backup进程。
 
master-server_test\server.cpp:
        if(!strcasecmp(buf, "CREATE" ))
        {
         int pid = fork();
             ......
         if(pid == 0)
         {
            execl("../backup_process/backup_process", "backup_process", NULL);
            ......
         }
        }
 
 
 
 BACKUP :指令目标:Server和Backup进程。
                              Server发送结构体信息给Backup进程;(后者整理信息;)
                              代码后述。
 CRASH  :指令目标:Server。
 RESTART:指令目标:Master自身。
                             (Master)启动新Server;(以参数restart:以不再进入
                                                      老Server启动后accept连接的代码;)
 
master-server_test\server.cpp:
        if(!strcasecmp(buf, "RESTART" ))
        {
         int pid = fork();
             ......
         if(pid == 0)
         {
            ......
            execl("../server_test/server_test", "server_test", "2000","restart", NULL);
            ......
         }
        }
 

server_test\server.cpp:
     if (!strcasecmp(argv[argc - 1], "restart"))
         ;
     else
         ......
 
 
 RESTORE:指令目标:Backup进程和新Server。
                              Backup进程发送结构体信息给新Server;(后者整理信息;)
                              代码后述。
 1TO2/2TO1:如上。继续观察聊天正常与否。
 
 (代码中还可见两个指令CTOS/STOC,这是老版本中用的,类似1TO2/2TO1,这里不用。)

 
仅仅传递fd是不够的,无法分清哪个是哪个;还好使用sendmsg/recvmsg可以传递结构信息。
data server向backup_process备份的信息是包括文件描述符fd在内的结构,在server_test/、backup_process/ 内均有
pass_fd_struct_rw.h
    typedef struct{
        SOCKET fd;       //socket fd
        int    owner;    //client id
        int    target;   //chating peer
        int    paired;   //is it a chat pair?
        int    fd_orig;  //some original information
    }fd_s;
    
    extern fd_s   fd_info[2];

现在来看BACKUP和RESTORE:
 
 BACKUP:
 
     (data )Server端:
server_test\client.cpp:
              if(!strcasecmp(buf, "BACKUP"))
              {
                  ......
                  int ret = to_backup(data_sock );
                  if(ret < 0)
                      printf("backup 1 error!\n");
                  sleep(3);
                  ret = 0;
                  ret = to_backup(data_sock2);
                  if(ret < 0)
                      printf("backup 2 error!\n");
              }
 

server_test\client.cpp:
        int to_backup(SOCKET fd_to_backup)
        {
          ......
          ret = write_fd_struct(sockfd, (fd_to_backup == fd_info[0].fd)? &fd_info[0] : &fd_info[1] ,  \
                                 sizeof(fd_s) );
          ......
        }
 
     Backup process端:
backup_process\client.cpp:
              if(!strcasecmp(buf, "BACKUP"))
              {
                  ......
                  for(int i = 0; i < 2; i++)
                  {
                      int ret = do_backup();
                      if(ret < 0)
                         fprintf(stderr, "do_backup() %d return FAIL.\n", i+1);
                      else
                         printf("got passed fd seems OK.\n");
                      if(i == 0)
                         fd_backup  = ret;
                      else
                         fd_backup2 = ret;
                  }
              }
 

backup_process\client.cpp:
        SOCKET do_backup()
        {
          ......
          static int count = 0;
          ret = read_fd_struct(fdaccept, &fd_info[count], sizeof(fd_s));
          ++count;
          ......
          return ret;
        }
 
     两个读写函数在pass_fd_struct_rw.cpp中实现,在Server和Backup程序中都有,是一样的。由网上代码(见下面参考网址)稍加改写而成。
此处可参见子目录pass_fd_and_struct_test/ 下的单独测试读写的代码及server_test/ 或backup_process/ 下的pass_fd_rw.c。
pass_fd_struct_rw.cpp:
    #include "pass_fd_struct_rw.h"
    
    int write_fd_struct(int sock, fd_s* data, int size)
    {
        msghdr msg;
        
        // init msg_control
        if(data->fd == -1){
            msg.msg_control = NULL;
            msg.msg_controllen = 0;
        }
        else{
            union {
                struct cmsghdr cm;
                char space[CMSG_SPACE(sizeof(int))];
            } cmsg;
            memset(&cmsg, 0, sizeof(cmsg));
            
            cmsg.cm.cmsg_level = SOL_SOCKET;
            cmsg.cm.cmsg_type = SCM_RIGHTS; // we are sending fd.
            cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
            
            msg.msg_control = (cmsghdr*)&cmsg;
            msg.msg_controllen = sizeof(cmsg);
            *(int *)CMSG_DATA(&cmsg.cm) = data->fd;
        }
        
        // init msg_iov
        iovec iov[1];
        iov[0].iov_base = data;
        iov[0].iov_len  = size;
        
        msg.msg_iov = iov;
        msg.msg_iovlen = 1;
        
        // init msg_name
        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        
        if (sendmsg(sock, &msg, 0) == -1){
            cout << "[write_fd_struct] sendmsg error" << endl;
            return (-1);
        }
        
        return 0;
    }
    
    int read_fd_struct(int sock, fd_s* data, int size)
    {
        msghdr msg;
        
        // msg_iov
        iovec iov[1];
        iov[0].iov_base = data;
        iov[0].iov_len = size;
        
        msg.msg_iov = iov;
        msg.msg_iovlen  = 1;
        
        // msg_name
        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        
        // msg_control
        union { // union to create a 8B aligned memory.
            struct cmsghdr cm; // 16B = 8+4+4
            char space[CMSG_SPACE(sizeof(int))]; // 24B = 16+4+4
        } cmsg;
        memset(&cmsg, 0, sizeof(cmsg));
        
        msg.msg_control = (cmsghdr*)&cmsg;
        msg.msg_controllen = sizeof(cmsg);
        
        if (recvmsg(sock, &msg, 0) == -1) {
            cout << "[read_fd_struct] recvmsg error" << endl;
            return (-1);
        }
    #if 1
        printf( "recvmsg() ends, data is: fd:%d\n", data->fd);
    #endif    
        data->fd = *(int *)CMSG_DATA(&cmsg.cm);
        //int fd = *(int *)CMSG_DATA(&cmsg.cm);
    #if 1
        printf( "recvmsg() ends, after pass, fd turns to:%d, different?\n", data->fd);
    #endif    
    
        return data->fd;
        //return fd;
    }
 
 
 RESTORE:与BACKUP读写方向相反。
 
     Backup process端:
backup_process\client.cpp:
              if(!strcasecmp(buf, "RESTORE"))
              {
                  ......
                  int ret = do_restore(fd_backup2);
                  if(ret < 0)
                     fprintf(stderr, "do_restore() II return FAIL.\n");
                  ret = do_restore(fd_backup);
                  if(ret < 0)
                     fprintf(stderr, "do_restore() I return FAIL.\n");
              }
 

backup_process\client.cpp:
        int do_restore(SOCKET fd)
        {
          ......
          ret = write_fd_struct(fdaccept, (fd == fd_info[0].fd)? &fd_info[0] : &fd_info[1] ,  \
                                    sizeof(fd_s) );
          ...... 
        }
 
     (data )Server端:
server_test\client.cpp:
              if(!strcasecmp(buf, "RESTORE"))
              {
                  ......
                  for(int i = 0; i < 2; i++)
                  {
                      sleep(3);
                      int fd_restore = to_restore();
                      if(fd_restore < 0)
                         fprintf(stderr, "to_restore() return FAIL.\n");
                  }
                  
                  data_sock  = fd_info[0].owner == 1 ? fd_info[0].fd : fd_info[1].fd ;
                  data_sock2 = fd_info[0].owner == 2 ? fd_info[0].fd : fd_info[1].fd ;
                  *p_data_sock[0] = data_sock ;
                  *p_data_sock[1] = data_sock2;
              }
 

server_test\client.cpp:
        int to_restore()
        {
         ......
         static int count = 0;
         ret = read_fd_struct(sockfd, &fd_info[count], sizeof(fd_s));
         ++count;
         ......
         return ret;
        }
 

传递描述符由sendmsg/recvmsg完成,书中有论述,网上也有资料提供,这里列举一些网址:
    
         如何在进程之间传递文件描述符
         高级进程间通信之传送文件描述符 
         进程间传递描述符一
         进程间传递描述符二
         进程间传递描述符三
         进程间传递文件描述符 这里
         使用Unix域套接字在进程间传递文件描述符
    
在《UNIX环境高级编程》中讲了,《TCP/IP详解》第3卷讲了其实现。《UNIX网络编程》第1卷也讲到了。其他书如《Linux网络编程》宋敬彬、
Linux Socket Programming By Example by Warren Gay(中文版名:《实战Linux socket编程》)、《Linux高级程序设计》杨宗德。
据说Windows上有类似机制:DuplicateHandle、WSADuplicateSocket。

此外,为了使服务器程序能立即重启,socket使用了SO_REUSEADDR选项。
server_test\server.cpp
    int opt_val = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));

代码在Redhat 9和Ubuntu 14上均试验通过。(cygwin上虽然可以编译,但无Linux上传递描述符的功能。)

当然最好是写的服务器程序毫无bug,绝不崩溃。但如果实现本文的想法,会是一个强大的工具。

  

More powered by