浅谈server端基本的设计模型及部分问题

标签: server 端基 设计 | 发表时间:2013-11-16 04:16 | 作者:xianszm007
出处:http://blog.csdn.net

     用了大概一个半月的时间都在做OS相关的实验感觉操作系统的东西自己还是了解适可而止,当然OS中包含了太多的设计模式以及底层相关的东西都会对自己在server端处理起到指引的作用,但是目前自己还是还是感觉自己还是对server端的处理比较感兴趣,固不再废话,进入正题--server端基本的设计模式。

     [注]:所有东西基于Linux环境,并且部分设计模型在Linux下有良好的表现,不一定在Windows下适用。

     说起服务端编程,自己算是了解一些基本的概念,当然在这方面就装逼的逼格来说还是不够的,说不了太深层次的东西,只是简单的想提及一些我们会常用到的概念以及模型:阻塞与非阻塞、同步异步、IO多路复用以及多线程、多进程并发、事件轮询驱动等服务端模型。

     我们的目的就是将Linux网络编程和多线程、多进程以及Linux底层的设计机制结合起来组建高性能的服务端代码(老装逼了),也算是网络端的业务应用程序。基本的Linux进程线程的概念不是我们讨论的东西,当然Linux网络编程的基本TCP socket流程也不是这里要说的,我假设这些内容已经是基本的概念。关于Linux网络编程我之前的博文连接:http://blog.csdn.net/sim_szm/article/details/9569607 这里不再赘述。

     下面开始我们的正文:

     [一]、 一些概念(算是赘述)
    [阻塞]是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
    [非阻塞]和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
    [同步]所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
    [异步]异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
    IO多路复用(I/O multiplexing),是一个重要的概念,简单来说就是系统内核缓冲I/O数据,当某个I/O准备好后,系统通知应用程序该I/O可读或可写,这样应用程序可以马上完成相应的I/O操作,而不需要等待系统完成相应I/O操作,从而应用程序不必因等待I/O操作而阻塞,并且系统开销小,系统不必花费多余的进程、线程创建以及维护的开销成本,当然不可避免的会提升系统性能。所有可支持海量链接的系统大多都是基于IO多路复用IPC事件驱动模型的服务端架构(non-blocking IO + IO multiplexing),基本都是一个事件循环(event loop)以及事件驱动(event-driven)和事件回调的方式实现业务逻辑。在Linux中的即是我们常说的select( ) / poll( )调用,但在Linux2.6(实际是2.5.5)中实现的epoll( )(具体下面会阐述)方式才是最为强大的高性能替代产品。

    关于I/O模型在《unix网络编程》中提及了以下几种:

        - blocking I/O
   - nonblocking I/O
   - I/O multiplexing (select and poll)
   - signal driven I/O (SIGIO)
   - asynchronous I/O (the POSIX aio_functions)

    I/O多路复用即使上述第三种,其他I/O模型这里不再赘述。

    [二] 几种服务端的编程模型

    对于一个较为实用的服务端业务逻辑实现来说,好的代码架构直接决定了其真实的处理能力,而好的架构必定是最能有效并且充分地利用系统资源,我们可以从下面的几种server模型中体会其不同的处理方式带来的系统性能的差异。
  ·1· 基本的socket建立TCP链接,从开始的socket( )到最后的accept( )之后哦的send( )/recv( )操作,即实现了最为间的单进程模型,很明显其不具备并发的能力,这种模型下server只能简单的处理一个客户端的请求,所以这种最为简单的模型只是适合一对一下基于严格时序逻辑的网络业务。
  ·2·在1的基础上我们如果需要支持并发,最为简单的操作便是为每个请求的任务fork( )出一个子进程来处理。这也是最简单的迭代式并发模型,从某种角度来说,进程是可以并发执行的,所以进程的并发促进了server的并发逻辑,并且进程具有很好的隔离性,模个任务出现问题一般不会影响到其他的业务。这种模型看起来简单,但是在对性能要求不是很高并且链接数较低的场景中还是应用蛮多的。因为太简单,所以就不用说为什么简单了。
  ·3·preforking
  Preforking即是预先建立一定数量的进程/线程来提高请求的速度,相对于2中接受一个请求去fork必定是有性能和效率上的提升,但也带来了一个问题,那就是需要对空闲的进程/线程进行管理,即要求有一定的空闲进程/线程来处理业务请求,又不能使空闲的进程/线程过多造成对系统资源的浪费(注意这里需要做的有两点,1 进程空闲检查 2 资源释放)。但是一旦当需要处理的连接较多时,就会有严重的系统性能消耗,比如我们需要预先建立1000个进程,那时操作系统对进程的切换带来的开销都已经够了,还别谈什么业务处理性能。
  ·4· 当然preforking这种概念即可引申为我们所谓的进程/线程池,但在其具体的实现策略上还是会产生不同的结果。比如在实际应用时,我们做TCP连接处理,我们假设进程池(或线程)内的每一个进程都在做accept( ),当进程池内无业务处理时,所有的业务进程都会转到休眠状态,一旦有任务投放进来,就会产生引发所有空闲进程的争夺,这就是所谓的“惊群”现象,因为到底哪个进程去优先处理不是我们可控的(当然这里所有线程的业务逻辑是一样的),就会产生业务竞争的现象。这样的结果必然引发的是对系统性能的白白损耗。解决的办法是我们需要将原来的竞争策略转为分配,我们可以让父进程统一做accept( )操作,然后将所有的socket描述符作为资源统一分配给空闲子进程,可以的操作方式为IPC管道或是socketpair( )来实现。
  这里插一点内容,对于进程间通信,管道的方式是单向的,进程间需要有父子关系才可以,如要双向通信必须开两个描述符,很不方便。大牛陈硕的推荐是只用TCP,因为tcp协议可以跨主机,具有较强的架构伸缩性,我们的任务可以通过这种方式分散在不同的主机上(真实的物理机,不牵扯虚拟的概念),当然IPC有很多方式,但就集群的方式而言,这种业务的伸缩性是必需的。
  ·5·重点的多线程处理
  对于多线程来实现服务端的业务逻辑,我算是较为重视的一个,因为其中牵扯的问题是最多的,在逻辑架构上的设计也是多样性的,陈硕大牛的那本《Linux多线程服务端编程》中较为推崇的一中策略就是“ one loop per thread + thread pool ”,即为每一个IO线程中有一个event loop(无论是周期性还是单次的),它代表了线程的主循环,当前我需要让那个线程来处理业务我就只需要将对应的timer或是tcp连接注册到对应线程的事件轮询中(当然这里我们并不考虑同一个TCP连接的事件并发),关于事件轮询在Linux下我们常用的就是epoll,因为它的确是一种高性能的机制,对因还有FreeBSD的kqueue,但目前主流的服务端都是Linux,所以epoll的学习性价比还是蛮高的。
对于thread pool , 它的作用更多的是计算和实际业务处理,当然这里在管理方面需要对应的条件变量和mutex来管理,对于互斥锁这种东西,很多人都在排斥,如果在服务端代码出现了大量的锁必定是低效的,但我认为引起低效的更多是锁间引发的竞争关系,而不是单纯的互斥。
  对于用不用多线程,这个问题并不是一定的,因为对于不同的情况而言,具体的问题要结合业务的复杂性和时序逻辑来分析。但是对于提升响应速度,让IO操作和任务计算处理并行或是从降低延时来说,多线程是不错的选择。

     [三]、epoll的部分内容

       对于epoll这个东西本来不打算再说,因为之前有过一篇Blog来说它的简单实用,这里再赘述一下吧,因为的确是蛮重要的东西,它是Linux下最经典的异步IO框架。
  Epoll的函数调用主要有:epoll_create( )、 epoll_wait( )、 epoll_ctl( )、close( ).
  根据man手册介绍, epoll_create(int size) 用来创建一个epoll实例,向内核申请支持size个句柄的资源(存储)。Size的大小不代表epoll支持的最大句柄个数,而隐射了内核扩展句柄存储的尺寸,也就是说当后面需要再向epoll中添加句柄遇到存储不够的时候,内核会按照size追加分配。在2.6以后的内核中,该值失去了意义,但必须大于0。epoll_create执行成功,返回一个非负的epoll描述句柄,用来指定该资源,否则返回-1。
  对于事件的轮询控制主要通过epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)完成。控制对象是用户申请的句柄,即fd;Epfd指定所控制的epoll资源;op指对fd的动作,包括向epoll中添加一个句柄EPOLL_CTL_ADD,删除一个句柄EPOLL_CTL_DEL,修改epoll对一个存在句柄的监控模式EPOLL_CTL_MOD;event指出需要让epoll对fd的监控模式(收、发、触发方式等)。epoll_ctl执行成功返回0, 否则返回-1。
  关于epoll的事件类型定义如下:


  typedef union epoll_data {
            void    *ptr;
            int      fd;
            uint32_t u32;
            uint64_t u64;
  } epoll_data_t;
  struct epoll_event {
           uint32_t   events;    /* Epoll events */
           epoll_data_t data;      /* User data variable */
  };
        该结构中我们主要看epoll_event。epoll_event->data涵盖了调用epoll_ctl增加或者修改某指定句柄时写入的信息,epoll_event->event,则包含了返回事件的位域。
  具体的添加句柄操作,限于篇幅就不再多说,seracher一下一大堆介绍,我的之前一篇Blog也有简单的介绍:
http://blog.csdn.net/sim_szm/article/details/8860803 具体可以参照。
  当向epoll中添加若干句柄后,就要进入监控状态,此时通过系统调用epoll_wait(int epfd, struct epoll_event *events,  int maxevents,  int timeout)完成。epoll_wait在执行的时候,在timeout内,将有动作的句柄的信息填充到event,event和maxevents决定了epoll监控句柄的上限。timeout的单位是微妙级别,当为-1时,除非内部句柄有动作,否则持续等待。epoll_wait执行成功返回有动作的句柄的总数,句柄信息在events中包含;如果在超时timeout内返回零,表示没有io请求的句柄;否则返回-1。
  对于epoll的使用有两种重要的事件触发方式,边沿触发(Edge Triggered)和水平触发(Level Triggered),边沿触发,效率较高,只在Socket发送缓冲区由满变成不满和接收缓冲区由空变成非空的瞬间,EPOLL会分别检测到EPOLLOUT和EPOLLIN事件,其它时候,没有任何事件可被检测到,为确保Socket上的收、发正常,应用程序必需确保“发则发到发不出,收必收至收不到”。在边沿触发下,正确的读写操作便是:
 · 读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN ·
 · 写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN ·
  至于水平触发方式,只要发送缓冲区不为满,即可检测到EPOLLOUT,只要接收缓冲区不为空,即可检测到EPOLLIN,效率不如前者高,但编程更容易,不容易出错。
  我们用的一般都为边沿触发。给一段epoll的使用代码示例:


#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/resource.h>


#define MAXBUF 1024
#define MAXEPOLLSIZE 10000
#define BACKLOG 1
/*
setnonblocking - 设置句柄为非阻塞方式
*/
int setnonblocking(int sockfd)
{
    if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) {
        return -1;
    }
    return 0;
}

/*
handle_message - 处理每个 socket 上的消息收发
*/
int handle_message(int new_fd)
{
    char buf[MAXBUF + 1];
    int len;
    bzero(buf, MAXBUF + 1);
    len = recv(new_fd, buf, MAXBUF, 0);
    if (len > 0)
        printf
            ("socket %d recv message :'%s',size as \n",
             new_fd, buf, len);
    else {
        if (len < 0)
            printf
                ("消息接收失败!错误代码是%d,with error code '%s'\n",
                 errno, strerror(errno));
        close(new_fd);
        return -1;
    }
    return len;
}
int main(int argc, char **argv){
    int listener, new_fd, kdpfd, nfds, n, ret, curfds,opt;
    socklen_t len;
    struct sockaddr_in my_addr, their_addr;
    struct epoll_event ev;
    struct epoll_event events[MAXEPOLLSIZE];
    struct rlimit rt;

    /* 设置每个进程允许打开的最大文件数 */
    rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
    if (setrlimit(RLIMIT_NOFILE, &rt) == -1) {
        perror("setrlimit");
        exit(1);
    }
    else printf("设置系统资源参数成功!\n");

    if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket error ");
        exit(1);
    } else
        printf("socket init succeed !\n");

    setnonblocking(listener);
    opt=1;
    setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    bzero(&my_addr, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(8080);
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr))== -1) {
        perror("bind");
        exit(1);
    }

    if (listen(listener,BACKLOG) == -1) {
        perror("listen");
        exit(1);
    } else
        printf("our service start !\n");

    /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */
    kdpfd = epoll_create(MAXEPOLLSIZE);
    len = sizeof(struct sockaddr_in);
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listener;
    if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0) {
        fprintf(stderr, "epoll set insertion error: fd=%d\n", listener);
        return -1;
    } else
        printf("监听 socket 加入 epoll 成功!\n");
    curfds = 1;
    while (1) {
        nfds = epoll_wait(kdpfd, events, curfds, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            break;
        }
        /* 处理所有事件 */
        for (n = 0; n < nfds; ++n) {
            if (events[n].data.fd == listener) {
                new_fd = accept(listener, (struct sockaddr *) &their_addr,
                                &len);
                if (new_fd < 0) {
                    perror("accept");
                    continue;
                } else
                    printf("connect with: %s socket is:%d\n", inet_ntoa(their_addr.sin_addr), new_fd);

                setnonblocking(new_fd);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = new_fd;
                if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0) {
                    fprintf(stderr, "把 socket '%d' 加入 epoll 失败!%s\n",
                            new_fd, strerror(errno));
                    return -1;
                }
                curfds++;
            } else {
                ret = handle_message(events[n].data.fd);
                if (ret < 1 && errno != 11) {
                    epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,
                              &ev);
                    curfds--;
                }
            }
        }
    }
    close(listener);
    return 0;
}

  关于epoll的缓冲区处理还是一个很复杂的问题,限于篇幅,没办法在多说。今天就先说到这里,很多问题也只是很浅层次的说明,服务端牵扯了太多的机制,没办法一一列举,当然也是限于自己的知识水平,好多问题都还不了解。慢慢学习吧。

作者:xianszm007 发表于2013-11-15 20:16:43 原文链接
阅读:184 评论:0 查看评论

相关 [server 端基 设计] 推荐:

浅谈server端基本的设计模型及部分问题

- - CSDN博客架构设计推荐文章
     用了大概一个半月的时间都在做OS相关的实验感觉操作系统的东西自己还是了解适可而止,当然OS中包含了太多的设计模式以及底层相关的东西都会对自己在server端处理起到指引的作用,但是目前自己还是还是感觉自己还是对server端的处理比较感兴趣,固不再废话,进入正题--server端基本的设计模式.

SQL Server--索引

- - CSDN博客推荐文章
         1,概念:  数据库索引是对数据表中一个或多个列的值进行排序的结构,就像一本书的目录一样,索引提供了在行中快速查询特定行的能力..             2.1优点:  1,大大加快搜索数据的速度,这是引入索引的主要原因..                             2,创建唯一性索引,保证数据库表中每一行数据的唯一性..

SQL Server 面试

- - SQL - 编程语言 - ITeye博客
在SQL语言中,一个SELECT…FROM…WHERE语句称为一个查询块,将一个查询块嵌套在另一个查询块的WHERE子句中的查询称为子查询. 子查询分为嵌套子查询和相关子查询两种. 嵌套子查询的求解方法是由里向外处理,即每个子查询在其上一级查询处理之前求解,子查询的结果作为其父查询的查询条件. 子查询只执行一次,且可以单独执行;.

什么是Server SAN? Server SAN精解

- - 云存储技术网--光头老蒋
Server SAN是现在一个全新的概念,现在给出的概念都太笼统. 按照老蒋的想法:Server San应该是一个利用软件将基于DAS存储(包含闪存卡,直连存储)的集合,做成一个能共享的SAN存储网络.      SAN存储区域网,大家都很熟悉. 普遍使用的就两种: FC SAN;ISCSI SAN.

weblogic server启动慢

- - Linux - 操作系统 - ITeye博客
(1)较好的解决办法: 在Weblogic启动参数里添加 “-.   Djava.security.egd=file:/dev/./urandom” (/dev/urandom 无法启动). 修改Linux上Weblogic使用的jdk $JAVA_HOME/jre/lib/security/java.security 文件.

Server-Sent Events 教程

- - 阮一峰的网络日志
服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE). 严格地说, HTTP 协议无法做到服务器主动推送信息. 但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming). 也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来.

译|High-Performance Server Architecture

- - 掘金 架构
本文的目的是分享我多年来关于如何开发某种应用程序的一些想法,对于这种应用程序,术语“服务”只是一个无力的近似称呼. 更准确地说,将写的与一大类程序有关,这些程序旨每秒处理大量离散的消息或请求. 网络服务通常最适合此定义,但从某种意义上讲,实际上并非所有的程序都是服务. 但是,由于“高性能请求处理程序”是很糟糕的标题,为简单起见,倒不如叫“服务”万事大吉.

X.Org Server 1.11正式发布

- xing - cnBeta.COM
X.Org Server 1.11正式版于昨日晚间发布了. 因为发布管理员Keith Packard的母亲不幸去世(节哀顺变),新版发布比原计划推迟了一周. 这是一次比较重大的更新,汇聚了过去六个月的增强,并修复了大量bug,但却没有带来什么新特性. 支持触摸操作的X Input 2.1被推迟到了下一个X.Org Server版本或者更晚,也没有任何RandR扩展.

Apache HTTP server 2.2.21发布

- tinda - Solidot
两周前,Apache软件基金会发布了Apache 2.2.20版,修正了被广泛讨论的字节范围头(range-header)拒绝服务漏洞. 现在,它发布了v 2.2.21,修正了另一个拒绝服务安全漏洞,以及对V 2.2.20版所修正漏洞的进一步修复. Apache鼓励所有旧版本用户升级到新版本.

Web server调研分析

- flychen50 - 搜索研发部官方博客
简单可依赖的架构首先需要有一个简单可依赖的前端WebServer集群. 本文通过深入调研当前主流的异步web服务器Lighttpd和Nginx,从业界使用情况、架构原理、扩展开发、功能对比、性能对比等多个方面进行分析. 从业界使用情况来看,最新Web Server使用情况的数据如下:Nginx的使用率是6.6%,Lighttpd的使用率是0.51%.