使用Go語言每分鐘處理一百萬個請求

banq發表於2015-07-07
該文是Malwarebytes首席架構師介紹其希望如何使用Go語言實現每分鐘處理100萬個請求。

其主要職責是加強系統基礎架構,以支援每天數百萬人使用,其本人已經在反病毒和反惡意軟體領域工作12年,他深深知道這些系統的複雜原因最終是由於每天處理大量資料。

其過去九年後端經歷大部分使用Ruby On Rails,雖然他相信RoR是一個令人驚奇的環境,但是當你以Ruby方式開始思考和設計系統一段時間以後,在你需要考慮多執行緒 並行和快速執行以及小記憶體消耗的情況下,你會忘記原來系統架構的所謂有效與簡單。多年來,他也是C/C++, Delphi 和C# ,他開始認識到使用正確工具做事能減少不必要的複雜性。

作為一個首席架構師,他並不執著於語言和框架,而這兩點總是網上爭論的焦點,他相信效率 產品性和程式碼維護性大部分依賴於你的架構解決方案有多簡單。

問題
目標是能夠處理來自數百萬端點的POST請求,Web處理器會接受到一個JSON文件,其中包含一個許多資料集合,這些需要寫入到Amazon S3,然後Map-reduce系統稍後可以操作分析這些資料。

傳統地闖進一個worker-tier工作層架構,如:

1.Sidekiq
2.Resque
3.DelayedJob
4. Elasticbeanstalk Worker Tier
5,RabbitMQ

然後設定兩個不同叢集,一個是用於Web前端,另外一個是用於worker,這樣我們可以擴充套件很多後端伺服器數量以應付大量增長的請求。

當他們發現這是高流量系統時,意識到應該使用Go語言完成,之前其本人已經使用Go語言兩年,開發過一些系統,但是沒有人確證Go語言能夠應付如此大的負載。

他們定義了資料結構,這是透過POST提交獲得的請求,然後透過一個方法上傳到S3。

type PayloadCollection struct {
	WindowsVersion  string    `json:"version"`
	Token           string    `json:"token"`
	Payloads        []Payload `json:"data"`
}

type Payload struct {
    // [redacted]
}

func (p *Payload) UploadToS3() error {
    // the storageFolder method ensures that there are no name collision in
    // case we get same timestamp in the key name
    storage_path := fmt.Sprintf("%v/%v", p.storageFolder, time.Now().UnixNano())

	bucket := S3Bucket

	b := new(bytes.Buffer)
	encodeErr := json.NewEncoder(b).Encode(payload)
	if encodeErr != nil {
		return encodeErr
	}

    // Everything we post to the S3 bucket should be marked 'private'
    var acl = s3.Private
	var contentType = "application/octet-stream"

	return bucket.PutReader(storage_path, b, int64(b.Len()), contentType, acl, s3.Options{})
}
<p class="indent">


起初他們試圖並行化這種接受請求然後轉發上傳的處理工作,將這些處理過程放入一個簡單的goroutine(Go語言並行協程)

func payloadHandler(w http.ResponseWriter, r *http.Request) {

    if r.Method != "POST" {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}

    // Read the body into a string for json decoding
	var content = &PayloadCollection{}
	err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&content)
    if err != nil {
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

    // Go through each payload and queue items individually to be posted to S3
    for _, payload := range content.Payloads {
        go payload.UploadToS3()   // <----- DON'T DO THIS
    }

    w.WriteHeader(http.StatusOK)
}
<p class="indent">

在負載穩定情況下,這種方案能夠大部分工作得很好,但是在大規模訪問下,卻工作得不怎麼樣。當每分鐘達到100萬POST請求時,這段程式碼崩潰了。

他們引入了buffered channel緩衝通道,將一些工作放入佇列。

var Queue chan Payload

func init() {
    Queue = make(chan Payload, MAX_QUEUE)
}

func payloadHandler(w http.ResponseWriter, r *http.Request) {
    ...
    // Go through each payload and queue items individually to be posted to S3
    for _, payload := range content.Payloads {
        Queue <- payload
    }
    ...
}
<p class="indent">

在佇列另外一端,也就是出列工作中再處理上傳到S3的工作,如下:

func StartProcessor() {
    for {
        select {
        case job := <-Queue:
            job.payload.UploadToS3()  // <-- STILL NOT GOOD
        }
    }
}
<p class="indent">

但是帶來問題是這個佇列迅速達到其上限,堵塞住了請求處理器,這樣就不能向佇列中繼續放入。這可以透過加入一個倒數計數來避免,但是系統的延遲會恆速上升(系統變慢)。

導致這個原因是因為上傳到S3這個工作非常耗時,因為是透過網路連線,因此引入Java和C概念中的執行緒池來處理上傳工作,這樣使用Channel實現Queue+Worker的概念.


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

// Job represents the job to be run
type Job struct {
	Payload Payload
}

// A buffered channel that we can send work requests on.
var JobQueue chan Job

// A pool of workers that are instantianted to perform the work
var WorkerPool chan chan Job

// Worker represents the worker that executes the job
type Worker struct {
	ID          int
	JobChannel  chan Job
	WorkerPool  chan chan Job
	QuitChan    chan bool
}

func NewWorker(id int, workerPool chan chan Job) Worker {
	worker := Worker{
		ID:          id,
		Work:        make(chan Job),
		WorkerPool:  workerPool,
		QuitChan:    make(chan bool)}

	return worker
}

// Start method starts the run loop for the worker, listening for a quit channel in
// case we need to stop it
func (w Worker) Start() {
	go func() {
		for {
			// register the current worker into the worker queue.
			w.WorkerPool <- w.JobChannel

			select {
			case job := <-w.JobChannel:
				// we have received a work request.
				if err := job.Payload.UploadToS3(); err != nil {
					log.Errorf("Error uploading to S3: %s", err.Error())
				}

			case <-w.QuitChan:
				// we have received a signal to stop
				return
			}
		}
	}()
}

// Stop signals the worker to stop listening for work requests.
func (w Worker) Stop() {
	go func() {
		w.QuitChan <- true
	}()
}
<p class="indent">


在佇列放入的一端程式碼改為如下:

func payloadHandler(w http.ResponseWriter, r *http.Request) {

    if r.Method != "POST" {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}

    // Read the body into a string for json decoding
	var content = &PayloadCollection{}
	err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&content)
    if err != nil {
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

    // Go through each payload and queue items individually to be posted to S3
    for _, payload := range content.Payloads {

        // let's create a job with the payload
        work := Job{Payload: payload}

        // Push the work onto the queue.
        JobQueue <- work
    }

    w.WriteHeader(http.StatusOK)
}
<p class="indent">


透過StartDispatcher建立worker池,然後開始監聽JobQueue中的job:

func StartDispatcher(maxWorkers int) {

	WorkerPool = make(chan chan Job, maxWorkers)

    // starting n number of workers
	for i := 0; i < maxWorkers; i++ {
		worker := NewWorker(i+1, WorkerPool)
		worker.Start()
	}

	go func() {
		for {
			select {
			case job := <-JobQueue:
                // a job request has been received
				go func(jobChannel chan Job) {
                    // try to obtain a worker that is available.
                    // this will block until a worker is idle
					worker := <-WorkerPool

                    // dispatch the job to the worker, dequeuing from
                    // the jobChannel
					worker <- jobChannel
				}(job)
			}
		}
	}()
}
<p class="indent">


最後終於達到了每分鐘處理100萬個請求,更多測試結果見原文:

Handling 1 Million Requests per Minute with Go · m

相關文章