1 .大量的连接不仅仅会造成大量的内存消耗,在开发服务的时候,还会遇到竞争条件和死锁。
2 .随之而来的是自我分布式阻断攻击,在这种情况下,客户端会不断地尝试重新连接服务端而把情况弄得更加糟糕
3 .比如我们的服务端可能某种情况无法处理ping消息了,这些空闲连接在服务端就会被不断被关闭。但是客户端以为是失去了连接,然后尝试重新建立连接,而不是继续等待服务端发来的消息
4 .这种情况就需要让负载过重得服务端停止接收新的连接,然后负载均衡器就会把请求转到其他得服务端上面
利用groutine池来限制同时处理包的数量
1 .
主流线程模型-区别在于用户线程与内核调度实体的对应关系上
1 .内核级线程模型
1 .内核调度实体KSE就是指可悲操作系统内核调度器调度的对象实体,简单来说就是内核级线程,是操作系统内核的最小调度单元
2 .用户线程与内核线程KSE是一对一的映射模型,每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做。对线程的创建,终止以及同步都是基于内核提供的系统调用来完成
3 .优势:简单。直接借助操作系统内核的线程以及调度器,所以CPU可以快速切换调度线程,多个进程可以同时运行
4 .缺点:直接借助操作系统内核来创建,销毁和以及多个线程之间的上下文切换和调度,因此资源成本大幅度上涨
2 .用户级线程模型-传统的用户线程模型
1 .用户线程与内核线程KSE是多对一的映射关系,多个用户线程一般重属于单个进程并且多线程的调度是用户自己的线程库来完成的
2 .线程的创建,销毁以及多线程之间的协调操作等都是由用户自己的线程库来负责而无需借助系统调用来实现。
3 .一个进程中所有创建的线程都只和一个KSE在运行时动态绑定
4 .操作系统只知道用户进程,而对其中的线程无感
5 .由于线程调度是在用户层面完成的,也就是相对于内核调度不需要让CPU在用户态和内核态之间切换,所以实现是很轻量级的
6 .缺点:不能真正的做到并发。
7 .假如某个进程上的线程因为一个阻塞调用而被CPU而被中断,那么所有的线程都是被阻塞的,整个进程都被挂起
8 .所以很多写成库都会把一些阻塞操作封装为非阻塞形式,然后再以前阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他执行的用户线程在改KSE上运行,从而避免了内核调度器由于KSE阻塞而做上下文切换,这样进程也不会被阻塞了
9 .
3 .两级线程模型-golang的实现
1 .用户线程和内核KSE是多对多的映射模型
2 .任意一个进程可以和多个内核线程KSE关联,于是进程内的多个线程可以绑定不同的KSE
3 .但是他进程里面的所有线程并不与KSE一一绑定,而是动态绑定同一个KSE,当某个KSE因为其绑定的线程的阻塞操作而被内核调度出cpu时,其关联进程中的其余用户线程可以重新与其他KSE绑定运行
4 .用户调度器实现用户线程到KSE的调度,内核调度器实现KSE到CPU上的调度
5 .
大规模goroutine的瓶颈
1 .100万个,内存暴涨导致对GC压力增大,这个时候就会有性能瓶颈
2 .runtime和GC也是goroutine,当goroutine规模太大。内存吃紧的时候,runtime调度和垃圾回收同样会出问题
3 .http标准库实现
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
// 不断循环取出TCP连接
for {
rw, e := l.Accept()
...
go c.serve(ctx)
//每来一个连接,开一个goroutine,这就意味着来10万个会来10万个
}
}
解决问题
1 .根本原因:大规模goroutine导致的资源侵占,限制goroutine的数量,合理复用。
实现思路
1 .初始化一个pool池,维护了一个类似栈的队列,来存放负责处理任务的worker
2 .提交task到pool中
3 .检查当前worker队列中是否有空闲的worker,如果有,取出当前的task
4 .没有,检查当前worker是否已经达到上限,是,一直阻塞到有worker被放回到pool,否则新开一个worker处理
5 .每个worker执行完任务之后,放回pool的队列中等待
有疑问加站长微信联系(非本文作者)