GoSuda

Verkürzung der Antwortzeit beim Datei-Upload per Go HTTP: Implementierung einer kanalbasierten asynchronen Worker-Queue

By Yunjin Lee views ...

Übersicht

Wenn man Go zum ersten Mal lernt, geschieht es häufig, dass man dabei einen Backend-Server implementiert. Nehmen wir als Beispiel den Fall, dass man ein Beispiel erstellt, bei dem ein Dateistream über eine REST-API empfangen und auf den Server hochgeladen wird. Der net/http-Server von Go verarbeitet standardmäßig mehrere Anfragen gleichzeitig, daher gibt es beim gleichzeitigen, massenhaften Hochladen an sich kein Problem. Wenn jedoch sämtliche Vorgänge nach dem Empfang des Streams synchron verarbeitet werden, verzögert sich die Antwort des Endpunkts. Lassen Sie uns Techniken betrachten, mit denen diese Situation vermieden werden kann.

Ursache

Auch das Empfangen eines Streams benötigt im Allgemeinen viel Zeit; bei großen Dateien kann eine einzelne Anfrage mehrere Minuten in Anspruch nehmen. In solchen Fällen ist es wichtig, die Vorgänge nach dem Empfang möglichst zügig zu verarbeiten. Das Beispielszenario besteht darin, den Stream zu empfangen, als temporäre Datei zu speichern und anschließend in einen Container zu übertragen. Wenn dabei der Teil, der die temporäre Datei in den Container überträgt, über einen Worker-Pool verarbeitet wird, lässt sich die Antwortverzögerung verkürzen.

 1package file_upload
 2
 3import (
 4	"fmt"
 5	"io"
 6	"log"
 7	"net/http"
 8	"os"
 9	"path/filepath"
10	"time"
11)
12
13const uploadTempDir = "/tmp/incus_uploads" // Host temporary directory
14
15// UploadTask holds data for asynchronous file push.
16type UploadTask struct {
17	HostTempFilePath         string
18	ContainerName            string
19    HostFilename             string
20	ContainerDestinationPath string
21}
22
23// UploadHandler processes file uploads. Saves to temp file, then queues for Incus push.
24func UploadHandler(wr http.ResponseWriter, req *http.Request) {
25	if req.Method != http.MethodPost {
26		http.Error(wr, "POST method required.", http.StatusMethodNotAllowed)
27		return
28	}
29	originalFilePath := req.Header.Get("X-File-Path")
30    originalFilename := filepath.Base(req.Header.Get("X-Host-Path"))
31	containerName := req.Header.Get("X-Container-Name")
32	if originalFilePath == "" || containerName == "" {
33		http.Error(wr, "Missing X-File-Path or X-Container-Name header.", http.StatusBadRequest)
34		return
35	}
36
37	cleanContainerDestPath := filepath.Clean(originalFilePath)
38	if !filepath.IsAbs(cleanContainerDestPath) {
39		http.Error(wr, "File path must be absolute.", http.StatusBadRequest)
40		return
41	}
42
43	// Create unique temporary file path on host
44	tempFileName := fmt.Sprintf("%d-%s", time.Now().UnixNano(), filepath.Base(originalFilePath))
45	hostTempFilePath := filepath.Join(uploadTempDir, tempFileName)
46
47	if err := os.MkdirAll(uploadTempDir, 0755); err != nil {
48		log.Printf("ERROR: Failed to create temp upload directory: %v", err)
49		http.Error(wr, "Server error.", http.StatusInternalServerError)
50		return
51	}
52
53	// Create and copy request body to temporary file (synchronous)
54	outFile, err := os.Create(hostTempFilePath)
55	if err != nil {
56		log.Printf("ERROR: Failed to create temporary file: %v", err)
57		http.Error(wr, "Server error.", http.StatusInternalServerError)
58		return
59	}
60	defer outFile.Close()
61
62	bytesWritten, err := io.Copy(outFile, req.Body)
63	if err != nil {
64		outFile.Close()
65		os.Remove(hostTempFilePath)
66		log.Printf("ERROR: Failed to copy request body to temp file: %v", err)
67		http.Error(wr, "File transfer failed.", http.StatusInternalServerError)
68		return
69	}
70	log.Printf("Upload Info: Received %d bytes, saved to %s.", bytesWritten, hostTempFilePath)
71
72	// Enqueue task for asynchronous Incus push
73	task := UploadTask{
74		HostTempFilePath:         hostTempFilePath,
75		ContainerName:            containerName,
76        HostFilename :            originalFilename,
77		ContainerDestinationPath: cleanContainerDestPath,
78	}
79	EnqueueTask(task) //THIS PART
80	log.Printf("Upload Info: Task enqueued for %s to %s.", originalFilePath, containerName)
81
82	// Send immediate 202 Accepted response
83	wr.WriteHeader(http.StatusAccepted)
84	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
85}

Den Abschnitt, den wir mit THIS PART markiert haben, haben Sie sicherlich bemerkt: Dort wird die Aufgabe in eine Warteschlange eingefügt.

Schauen wir uns nun an, wie die Aufgabenwarteschlange funktioniert.

 1package file_upload
 2
 3import (
 4	"log"
 5	"sync"
 6)
 7
 8var taskQueue chan UploadTask
 9var once sync.Once
10
11// InitWorkQueue initializes the in-memory task queue.
12func InitWorkQueue() {
13	once.Do(func() {
14		taskQueue = make(chan UploadTask, 100)
15		log.Println("Upload Info: Work queue initialized.")
16	})
17}
18
19// EnqueueTask adds an UploadTask to the queue.
20func EnqueueTask(task UploadTask) {
21	if taskQueue == nil {
22		log.Fatal("ERROR: Task queue not initialized.")
23	}
24	taskQueue <- task
25	log.Printf("Upload Info: Queue: Task enqueued. Size: %d", len(taskQueue))
26}
27
28// DequeueTask retrieves an UploadTask from the queue, blocking if empty.
29func DequeueTask() UploadTask {
30	if taskQueue == nil {
31		log.Fatal("ERROR: Task queue not initialized.")
32	}
33	task := <-taskQueue
34	log.Printf("Upload Info: Queue: Task dequeued. Size: %d", len(taskQueue))
35	return task
36}
37
38// GetQueueLength returns current queue size.
39func GetQueueLength() int {
40	if taskQueue == nil {
41		return 0
42	}
43	return len(taskQueue)
44}

Die als Beispiel bereitgestellte Aufgabenwarteschlange ist einfach implementiert. Sie besitzt eine schlichte Struktur, bei der die in der Warteschlange befindlichen Aufgaben aus dem Channel entnommen werden.

Im Folgenden finden Sie die Worker-Methode, die nach dem Datei-Upload die Übertragung in den Container übernimmt. Die aktuelle Methode ist für gute Reaktionsfähigkeit und einfache Implementierung als Endlosschleife ausgelegt; je nach Zweck kann jedoch auch ein zusätzlicher Algorithmus ergänzt werden.

 1func StartFilePushWorker() {
 2	for {
 3		task := DequeueTask()
 4		log.Printf("Upload Info: Worker processing task for %s from %s.", task.ContainerName, task.HostFilename)
 5
 6		// Defer cleanup of the temporary file
 7		defer func(filePath string) {
 8			if err := os.Remove(filePath); err != nil {
 9				log.Printf("ERROR: Worker: Failed to remove temp file '%s': %v", filePath, err)
10			} else {
11				log.Printf("Upload Info: Worker: Cleaned up temp file: %s", filePath)
12			}
13		}(task.HostTempFilePath)
14
15		// Process task with retries for transient Incus errors
16		for i := 0; i <= MaxRetries; i++ {
17			err := processUploadTask(task) //separate upload task
18			if err == nil {
19				log.Printf("SUCCESS: Worker: Task completed for %s.", task.ContainerName)
20				break
21			}
22
23			isTransient := true
24			if err.Error() == "incus: container not found" { // Example permanent error
25				isTransient = false
26			}
27
28			if isTransient && i < MaxRetries {
29				log.Printf("WARNING: Worker: Task failed for %s (attempt %d/%d): %v. Retrying.",
30					task.ContainerName, i+1, MaxRetries, err)
31				time.Sleep(RetryDelay)
32			} else {
33				log.Printf("ERROR: Worker: Task permanently failed for %s after %d attempts: %v.",
34					task.ContainerName, i+1, err)
35				break
36			}
37		}
38	}
39}

Zunächst versucht diese Funktion in einer Schleife, Tasks aus der Warteschlange abzurufen. Anschließend wird innerhalb des Wiederholungsbereichs der Upload-Schritt ausgeführt, der nicht vom Stream zur temporären Datei, sondern von der temporären Datei zum Container erfolgt.

Vorteile

Der Vorteil dieser Verarbeitungsweise liegt darin, dass solange der Stream-Upload erfolgreich verläuft sich die Latenz der nachfolgenden Verarbeitung verringern lässt und eine Erschöpfung der Ressourcen durch gleichzeitige Container-Operationen vermieden werden kann. Wie im aktuellen Code ersichtlich ist, wird die Anzahl der gleichzeitig ausführbaren Container-Operationen durch die Kapazität des Channels begrenzt. Auf diese Weise haben wir ein Beispiel kennengelernt, das die Parallelverarbeitung in Go praxisnah nutzbar macht. Wenn Sie weitere Beispiele sehen möchten, besuchen Sie bitte die folgenden Links.Modul mit Beispielen Projekt unter Verwendung des Beispiels Da das Projekt selbst viele Begleitkomponenten enthält, genügt es für das Erlernen des Workers selbst, kurz zu betrachten, wie die Worker-Init-Funktion in main.go aufgerufen wird, und dann weiterzugehen. Das Modul enthält außerdem weitere Arten von Workern; bitte schauen Sie sich diese ebenfalls an.