关于高并发的几个基础问题
什么是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()调度流程
出处:http://www.cnblogs.com/standby/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。