How to Handle One Million Requests per Minute in Go: Scalable Goroutine Pool Design
This article explains how to build a high‑performance Go server capable of processing up to one million requests per minute using efficient goroutine‑pool patterns, detailing the design of two worker structures, their dispatch, start, stop mechanisms, and practical optimizations.
Large internet applications such as e‑commerce platforms, social networks, and financial transaction systems receive massive request volumes, often requiring the ability to handle a million requests within a minute. To meet this demand, Go’s efficient concurrency model and goroutine pools are employed.
W1
The W1 struct defines five fields:
WgSend – a *sync.WaitGroup that waits for task‑sending goroutines to finish.
Wg – a *sync.WaitGroup that waits for task‑processing goroutines to finish.
MaxNum – the size of the goroutine pool.
Ch – a chan string used to deliver tasks.
DispatchStop – a chan struct{} used to stop task dispatch.
type W1 struct {
WgSend *sync.WaitGroup
Wg *sync.WaitGroup
MaxNum int
Ch chan string
DispatchStop chan struct{}
}The Dispatch method sends ten times MaxNum tasks to Ch, launching a goroutine for each task and using defer to decrement WgSend. A select statement allows early exit when DispatchStop is closed.
func (w *W1) Dispatch(job string) {
w.WgSend.Add(10 * w.MaxNum)
for i := 0; i < 10*w.MaxNum; i++ {
go func(i int) {
defer w.WgSend.Done()
select {
case w.Ch <- fmt.Sprintf("%d", i):
return
case <-w.DispatchStop:
fmt.Println("exit dispatch job:", fmt.Sprintf("%d", i))
return
}
}(i)
}
}The StartPool method creates the channel and wait groups if they are nil, then launches MaxNum worker goroutines that read from Ch and process tasks.
func (w *W1) StartPool() {
if w.Ch == nil {
w.Ch = make(chan string, w.MaxNum)
}
if w.WgSend == nil {
w.WgSend = &sync.WaitGroup{}
}
if w.Wg == nil {
w.Wg = &sync.WaitGroup{}
}
if w.DispatchStop == nil {
w.DispatchStop = make(chan struct{})
}
w.Wg.Add(w.MaxNum)
for i := 0; i < w.MaxNum; i++ {
go func() {
defer w.Wg.Done()
for v := range w.Ch {
fmt.Printf("completed job: %s
", v)
}
}()
}
}The Stop method closes DispatchStop, waits for the sending goroutines, then closes Ch and waits for the processing goroutines.
func (w *W1) Stop() {
close(w.DispatchStop)
w.WgSend.Wait()
close(w.Ch)
w.Wg.Wait()
}W2
SubWorker
type SubWorker struct {
JobChan chan string
}A SubWorker runs a Run method that continuously reads from JobChan, processes jobs, and exits when QuitChan is closed.
func (sw *SubWorker) Run(wg *sync.WaitGroup, poolCh chan chan string, quitCh chan struct{}) {
if sw.JobChan == nil {
sw.JobChan = make(chan string)
}
wg.Add(1)
go func() {
defer wg.Done()
for {
poolCh <- sw.JobChan
select {
case res := <-sw.JobChan:
fmt.Printf("completed job: %s
", res)
case <-quitCh:
fmt.Printf("consumer exit...
")
return
}
}
}()
}W2
type W2 struct {
SubWorkers []SubWorker
Wg *sync.WaitGroup
MaxNum int
ChPool chan chan string
QuitChan chan struct{}
}The Dispatch method retrieves a worker channel from ChPool and sends a job, falling back to a log message when all workers are busy.
func (w *W2) Dispatch(job string) {
jobChan := <-w.ChPool
select {
case jobChan <- job:
fmt.Printf("sent job: %s completed
", job)
return
case <-w.QuitChan:
fmt.Printf("sender (%s) exit
", job)
return
}
}The StartPool method initializes ChPool, creates the slice of SubWorker s, and launches a goroutine for each worker.
func (w *W2) StartPool() {
if w.ChPool == nil {
w.ChPool = make(chan chan string, w.MaxNum)
}
if w.SubWorkers == nil {
w.SubWorkers = make([]SubWorker, w.MaxNum)
}
if w.Wg == nil {
w.Wg = &sync.WaitGroup{}
}
for i := 0; i < w.MaxNum; i++ {
w.SubWorkers[i].Run(w.Wg, w.ChPool, w.QuitChan)
}
}The Stop method closes QuitChan, waits for all workers, then closes ChPool.
func (w *W2) Stop() {
close(w.QuitChan)
w.Wg.Wait()
close(w.ChPool)
for _, sw := range w.SubWorkers {
close(sw.JobChan)
}
}The entry function DealW2 creates a W2 instance, starts the pool, dispatches many jobs, and finally stops the pool.
func DealW2(max int) {
w := NewWorker(w2, max)
w.StartPool()
for i := 0; i < 10*max; i++ {
go w.Dispatch(fmt.Sprintf("%d", i))
}
w.Stop()
}Optimized Version (W2New)
An improved implementation adds dynamic scaling via AddWorker and RemoveWorker, uses a dedicated WaitGroup for lifecycle management, and blocks on dispatch when no worker is immediately available.
type SubWorkerNew struct {
Id int
JobChan chan string
}
type W2New struct {
SubWorkers []SubWorkerNew
MaxNum int
ChPool chan chan string
QuitChan chan struct{}
Wg *sync.WaitGroup
}
func NewW2(maxNum int) *W2New {
chPool := make(chan chan string, maxNum)
subWorkers := make([]SubWorkerNew, maxNum)
for i := 0; i < maxNum; i++ {
subWorkers[i] = SubWorkerNew{Id: i, JobChan: make(chan string)}
chPool <- subWorkers[i].JobChan
}
wg := new(sync.WaitGroup)
wg.Add(maxNum)
return &W2New{MaxNum: maxNum, SubWorkers: subWorkers, ChPool: chPool, QuitChan: make(chan struct{}), Wg: wg}
}
func (w *W2New) StartPool() {
for i := 0; i < w.MaxNum; i++ {
go func(sw *SubWorkerNew) {
defer w.Wg.Done()
for {
select {
case job := <-sw.JobChan:
fmt.Printf("SubWorker %d processing job %s
", sw.Id, job)
time.Sleep(time.Second)
case <-w.QuitChan:
return
}
}
}(&w.SubWorkers[i])
}
}
func (w *W2New) Dispatch(job string) {
select {
case jobChan := <-w.ChPool:
jobChan <- job
default:
fmt.Println("All workers busy")
}
}
func (w *W2New) AddWorker() {
newWorker := SubWorkerNew{Id: w.MaxNum, JobChan: make(chan string)}
w.SubWorkers = append(w.SubWorkers, newWorker)
w.ChPool <- newWorker.JobChan
w.MaxNum++
w.Wg.Add(1)
go func(sw *SubWorkerNew) {
defer w.Wg.Done()
for {
select {
case job := <-sw.JobChan:
fmt.Printf("SubWorker %d processing job %s
", sw.Id, job)
time.Sleep(time.Second)
case <-w.QuitChan:
return
}
}
}(&newWorker)
}
func (w *W2New) RemoveWorker() {
if w.MaxNum > 1 {
worker := w.SubWorkers[w.MaxNum-1]
close(worker.JobChan)
w.MaxNum--
w.SubWorkers = w.SubWorkers[:w.MaxNum]
}
}
func (w *W2New) Stop() {
close(w.QuitChan)
for i := 0; i < w.MaxNum; i++ {
close(w.SubWorkers[i].JobChan)
}
w.Wg.Wait()
}Test Case
func TestW2New(t *testing.T) {
pool := NewW2(3)
pool.StartPool()
pool.Dispatch("task 1")
pool.Dispatch("task 2")
pool.Dispatch("task 3")
pool.AddWorker()
pool.AddWorker()
pool.RemoveWorker()
pool.Stop()
}The article demonstrates that when the number of tasks exceeds the number of workers, a worker’s JobChan receives multiple tasks and processes them sequentially, highlighting potential bottlenecks and the importance of proper pool sizing and graceful shutdown.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Linux
Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
