• 关于高并发的几个基础问题


    什么是C10K?

    C10K 就是 Client 10000 问题,即

    在同时连接到服务器的客户端数量超过 10000 个的环境中,即便硬件性能足够, 依然无法正常提供服务。”,

    简而言之,就是单机1万个并发连接问题。

    这个概念最早由 Dan Kegel 提出并发布于其个人站点

    解决方案就是IO多路复用机制(select、poll、epoll等)。

    最弱连接(Weakest link)

    如果往两端用力拉一条由很多环 (连接)组成的锁链,其中最脆弱的一个连接会先断掉。

    因此,锁链整体的强度取决于其中最脆弱的一环。

    select和epoll模型的区别是什么?

    1. 不同点一:文件描述符限制
    select单个进程能够监视的文件描述符的数量存在最大限制。
    epoll没有文件描述符限制。
    
    2. 不同点二:监听方式
    select调用会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间),函数返回。
    当select函数返回后,需要通过遍历fdset,才能找到就绪的描述符。
    epoll事先通过epoll_ctl()来注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。
    即此处去掉了遍历文件描述符,而是通过监听回调的的机制。通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
    
    3. 相同点一:实现机制
    select和epoll都是IO多路复用的机制。
    I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
    
    4. 相同点二:同步I/O
    select和epoll都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
    而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

    水平触发(level triggered)和边缘触发(edge triggered)

    LT模式
    当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    关注点是数据(读操作缓冲区不为空,写操作缓冲区不为满),epoll_wait 总会返回就绪。
    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。
    在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。
    如果你不作任何操作,内核还是会继续通知你的。
    
    
    ET模式
    当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件;如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
    关注点是变化,只有监视的文件上有数据变化发生(读操作关注有数据写进缓冲区,写操作关注数据从缓冲区取走),epoll_wait 才会返回。
    
    ET(edge-triggered)是高速工作方式,只支持no-block socket。
    在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。
    然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,
    直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求。
    但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
    
    ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
    epoll工作在ET模式的时候,必须使用非阻塞套接字接口,
    以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

    协程解决的是什么问题?

    在高并发场景上,协程解决了c10k的问题(c10k的一个表现就是系统CPU高,因为操作系统要调度工作线程)。
    这是由于IO的使用方式是一个连接fd对应一个线程,fd采用阻塞方式调用,当fd不可读、写时,线程不可调度。
    当连接过高时,线程数量也大量增长,线程不仅占用了大量内存,
    而且调度线程也需要大量的cpu,所以并发到10k的时候就达到瓶颈。
    
    操作系统为解决这种问题,提供了多路复用的接口。
    一个线程可以处理多个连接,当一个连接不可读写时,线程不会阻塞,线程检查是否有其他可读写的连接;
    这样操作系统节省了大部分内存和线程调度所需要的的cpu,基于这种技术单机并发可以达到百万甚至更高。
    
    这个工作方式解决了并发的问题,但是这种方式操作复杂,当连接不可用时,应用程序需要保存连接的上下文,待连接可用时在继续之前的操作。
    
    协程解决了这种问题,协程内部帮应用程序保存了连接的上下文,
    开发者不用关心IO多路复用的实现,可以认为IO操作是阻塞的调用,极大方便的开发者;
    比如openresty就是在lua层面实现了协程,它不仅可以保证高并发,对开发者而言编程也特别简单。

    golang协程及其调度

    golang不仅支持IO的协程处理,还提供了事件、管道等阻塞调用的组件,对于使用者是阻塞的,

    对于程序而言只是不再处理当前的逻辑,转而去执行其他可执行的逻辑,将cpu利用率最大化,线程调度最小化。

    golang抽象了P、M、G三种对象实现了协程的调度

    G(goroutine)是协程(用户线程),执行应用程序逻辑代码,数量动态增减。主要有以下几种状态:

      - 初始

      - 待运行(G处在运行队列中,等待M取出并运行)

      - 运行中

      - 等待(G在等待某些条件完成,比如执行了一个不可读的channel,这时G既不在运行中也不再运行队列中)

      - 系统调用(M正在运行这个G发起的系统调用)

      - 已终止。

    P(process)是逻辑cpu,就是计算资源,在程序启动时创建,P的数量默认等于cpu核心数, 但可以通过环境变量GOMAXPROC修改,配置后不可变更,主要有两种状态空闲、运行。

    M(machine)是内核线程,用于在P上调度G,数量动态增加,只增不减,主要有以下几种状态:

      - 自旋(即M正在从运行队列中获取G,这时M拥有一个P)

      - 运行G

      - 等待(找不到可运行的G,就要从自旋变成等待状态,这时M并不拥有P;因为自旋也是占用CPU的,等待就让出CPU了;如果之后有可运行的时,可以通过futex去唤醒等待中的M去执行)

      - 系统调用(阻塞状态)

    G的调度就是M调度G在P上运行,让最少的M将P的利用率最大化。

    M=P是最完美的状态(openresty),但是当M由于系统调用变成不可用时(阻塞),P不能被利用,如果有待运行的G时,就要考虑新建M运行待运行的G。

    M是工作线程,用最少的M在P上运行G,这是golang设计的目标,因为M多了,操作系统要调度M。

    golang的GMP原理

    GMP模型

    - Go需要保证有足够的M可以运行G, 不让CPU闲着, 也需要保证M的数量不能过多,避免过度的CPU调度消耗。

    - M(内核线程)是运行goroutine的实体,goroutine调度器的功能是把可运行的goroutine分配到内核线程(即工作线程)上。

    - Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到物理CPU的核上执行。

    - P可以理解为控制go代码并行度的机制。

    go func()调度流程

    参考:Linux IO模式及 select、poll、epoll详解

    参考:Golang调度器GMP原理与调度全分析

    作者:Standby一生热爱名山大川、草原沙漠,还有妹子
    出处:http://www.cnblogs.com/standby/

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    函数PARSENAME使用和截取字符串
    转换为标准IPv4格式
    字母转为大写字母
    字母转为小写字母
    删除多个重复记录
    大写字母或小写字母转换为数字
    给不合法的格式转换成标准格式(案例)
    SQL SERVER动态列名
    快速生成基数的辅助表
    动态为表添加字段
  • 原文地址:https://www.cnblogs.com/standby/p/14827733.html
Copyright © 2020-2023  润新知