go线程池实现百万级高并发

实例代码解析

并发:严格意义上来说并发分为内核态,和用户态。

内核态:单个CPU处理一个进程,多个CPU 同时处理多个进程

用户态:单个的CPU的情况下,用户通过编程,在一个进程中通过多尔线程来实现并发

Go 的并发属于 CSP 并发模型的一种实现,
CSP 并发模型的核心概念是:“不要通过共享内存来通信,而应该通 过通信来共享内存”
这在 Go 语言中的实现就是 Goroutine 和 Channel。

实例解析一:

go实现一个线程池假定场景:

某平台的一个Restful 接口,该接口被调用后去处理一些事情,但是客户端需要快速得到响应,业务的处理过程将在后台继续进行。程序中我们可以开启一个协程去处理

func stratProcessor(){
    go dosomething()
}

对于一定量的负载,这种处理没问题,但是当大量请求时,程序会不断创建新的协程,进而导致程序崩溃所以有必要去控制创建goroutine 的数量。

实例解析二:

package main
import (   
 "fmt"    
 "runtime"    
 "time"
 )  
 func main(){
	dataChan:=make(chan int,100)
	 go func(){
  		 for{
         		select{        
         		case data:=<-dataChan:                
         		fmt.Println("data:",data)    
         		//这里延迟是模拟处理数据的耗时            
         		time.Sleep(1 * time.Second)
         		}
      		}
        }()

        //填充数据
       for i:=0;i<100;i++{
		dataChan<-i
       }
       
       //这里循环打印查看协程个数
       for {
            fmt.Println("runtime.NumGoroutine() :", runtime.NumGoroutine())   
            time.Sleep(2 * time.Second)
        }
 }

严格意义上来说上面处理并不是真正的并发处理。这种方法使用了缓冲队列一定程度上了提高了并发,但也是治标不治本,大规模并发只是推迟了问题的发生时间。当请求速度远大于队列的处理速度时,缓冲区很快被打满,后面的请求一样被堵塞了。

实例解析三:

真正意义上的并发处理请求

思想:领导 — > 工人 ---- > 任务

领导便是协程池,工人便是一个个协程,任务就是工人要做的事

通过一个二级通道实现 高并发控制
在这里插入图片描述

package main

import (    
	"fmt"    
	"runtime"
    	"time"
)

//任务
type Job interface {
    Do()//do something...
}

//----------------------------------------------
//worker 工人
type Worker struct {
    JobQueue chan Job   //任务队列
    Quit     chan bool //停止当前任务
}

//新建一个 worker 通道实例   新建一个工人
func NewWorker() Worker {
    return Worker{
            JobQueue: make(chan Job), //初始化工作队列为null
            Quit:     make(chan bool),
    }
}

/*
整个过程中 每个Worker(工人)都会被运行在一个协程中,
在整个WorkerPool(领导)中就会有num个可空闲的Worker(工人),
当来一条数据的时候,领导就会小组中取一个空闲的Worker(工人)去执行该Job,
当工作池中没有可用的worker(工人)时,就会阻塞等待一个空闲的worker(工人)。
每读到一个通道参数 运行一个 worker
*/

func (w Worker) Run(wq chan chan Job) {
    //这是一个独立的协程 循环读取通道内的数据,
    //保证 每读到一个通道参数就 去做这件事,没读到就阻塞
    go func() { 
               for {         
                   wq <- w.JobQueue //注册工作通道  到 线程池
                   select {            
                   case job := <-w.JobQueue: //读到参数                
                   	job.Do()
                   case <-w.Quit: //终止当前任务                
                   	return
                   }                                       
             }    
     }()
}
//----------------------------------------------
//workerpool 领导
type WorkerPool struct {
    workerlen   int      //线程池中  worker(工人) 的数量    
    JobQueue    chan Job //线程池的  job 通道    
    WorkerQueue chan chan Job
}

func NewWorkerPool(workerlen int) *WorkerPool {
    return &WorkerPool{        
    	workerlen:   workerlen,//开始建立 workerlen 个worker(工人)协程        
    	JobQueue:    make(chan Job), //工作队列 通道        
    	WorkerQueue: make(chan chan Job, workerlen), //最大通道参数设为 最大协程数 workerlen 工人的数量最大值    
    }
}

//运行线程池
func (wp *WorkerPool) Run() {
    //初始化时会按照传入的num,启动num个后台协程,然后循环读取Job通道里面的数据,    
    //读到一个数据时,再获取一个可用的Worker,并将Job对象传递到该Worker的chan通道    
    fmt.Println("初始化worker")    
    for i := 0; i < wp.workerlen; i++ {
            //新建 workerlen 20万 个 worker(工人) 协程(并发执行),每个协程可处理一个请求        
            worker := NewWorker()        //运行一个协程 将线程池 通道的参数  传递到 worker协程的通道中 进而处理这个请求        
            worker.Run(wp.WorkerQueue)
    }
    
    // 循环获取可用的worker,往worker中写job
    go func() { //这是一个单独的协程 只负责保证 不断获取可用的worker        
        for {            
        	select {            
        	case job := <-wp.JobQueue: //读取任务   
        	     //尝试获取一个可用的worker作业通道。
        	     //这将阻塞,直到一个worker空闲             
        	     worker := <-wp.WorkerQueue                
       		     worker <- job//将任务 分配给该工人            
       		}        
       	}    
     }()
}

//----------------------------------------------
type Dosomething struct {
    Num int
}

func (d *Dosomething) Do() {   
    fmt.Println("开启线程数:", d.Num)    
    time.Sleep(1 * 1 * time.Second)
}

func main() {
   
    //设置最大线程数    
    num := 100 * 100 * 20
    
    // 注册工作池,传入任务    
    // 参数1 初始化worker(工人)并发个数 20万个    
    p := NewWorkerPool(num)    
    p.Run()//有任务就去做,没有就阻塞,任务做不过来也阻塞
    
    //datanum := 100 * 100 * 100 * 100    //模拟百万请求    
    datanum := 100 * 100    
    go func() { //这是一个独立的协程 保证可以接受到每个用户的请求
            for i := 1; i <= datanum; i++ {
                sc := &Dosomething{Num: i}            
                p.JobQueue <- sc //往线程池 的通道中 写参数   每个参数相当于一个请求  来了100万个请求        
            }    
     }()
     
    for { //阻塞主程序结束   
    	fmt.Println("runtime.NumGoroutine() :", runtime.NumGoroutine())        
    	time.Sleep(2 * time.Second)
    }
    
}

参考:
http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/

注意到:
我们提供了初始化并加入到池子的worker的最大数量。

因为这个工程我们利用了Amazon Elasticbeanstalk带有的docker化的Go环境,所以我们常常会遵守12-factor方法论来配置我们的生成环境中的系统,我们从环境变了读取这些值。

这种方式,我们控制worker的数量和JobQueue的大小,所以我们可以很快的改变这些值,而不需要重新部署集群。

var (
    MaxWorker = os.Getenv("MAX_WORKERS")
    MaxQueue  = os.Getenv("MAX_QUEUE")
)

结果:
我们部署了之后,立马看到了延时降到微乎其微的数值,并未我们处理请求的能力提升很大。Elastic Load Balancers完全启动后,我们看到ElasticBeanstalk 应用服务于每分钟1百万请求。

通常情况下在上午时间有几个小时,流量峰值超过每分钟一百万次。

我们一旦部署了新的代码,服务器的数量从100台大幅 下降到大约20台。

我们合理配置了我们的集群和自动均衡配置之后,我们可以把服务器的数量降至4x EC2 c4.Large实例,并且Elastic Auto-Scaling设置为如果CPU达到5分钟的90%利用率,我们就会产生新的实例。

  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值