0

Bài 9: Server Nâng cao và Graceful Shutdown

Ở Bài 4, chúng ta đã khởi động server bằng hàm http.ListenAndServe(":8080", nil). Tuy nhiên, cách làm này ẩn chứa hai rủi ro lớn khi đưa lên môi trường Production (thực tế):

  1. Thiếu Timeouts cấu hình Server: Nếu một hacker cố tình mở kết nối nhưng gửi dữ liệu cực kỳ chậm (Slowloris attack), server của bạn sẽ bị "treo" các kết nối đó mãi mãi.

  2. Ngắt Server đột ngột: Khi bạn cập nhật code và khởi động lại Server (hoặc nhấn Ctrl+C), hàm ListenAndServe sẽ tắt ngay lập tức. Mọi Request của người dùng đang được xử lý dở dang (ví dụ: đang ghi file, đang thanh toán) sẽ bị chặt đứt (Dropped).

Bài này giải quyết triệt để hai vấn đề trên bằng cách cấu hình Server tùy chỉnh và áp dụng Graceful Shutdown (Tắt máy êm ái).

1. Cấu hình Timeout cho http.Server

Thay vì dùng hàm toàn cục, bạn cần khởi tạo struct http.Server và thiết lập các mốc thời gian chặn (Timeout) cho quá trình Đọc (Read) và Ghi (Write) dữ liệu ở tầng TCP/HTTP.

Go

package main

import (
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, Secure Server!"))
	})

	// Cấu hình Server chặt chẽ cho Production
	srv := &http.Server{
		Addr:    ":8080",
		Handler: mux,

		// Thời gian tối đa để đọc toàn bộ Request (bao gồm cả Body).
		// Ngăn chặn attacker gửi request cực chậm.
		ReadTimeout:  5 * time.Second,

		// Thời gian tối đa để xử lý logic và ghi toàn bộ Response trả về.
		WriteTimeout: 10 * time.Second,

		// Thời gian tối đa giữ kết nối nhàn rỗi (Keep-Alive) trước khi đóng.
		IdleTimeout:  120 * time.Second,
	}

	srv.ListenAndServe()
}

2. Graceful Shutdown là gì?

Graceful Shutdown là kỹ thuật thông báo cho Server: "Đừng nhận thêm Request mới nữa, nhưng hãy chờ cho các Request hiện tại xử lý xong rồi mới tắt máy hoàn toàn."

Để làm được điều này, chúng ta cần:

  1. Chạy server trong một Goroutine riêng (để nó không block hàm main).

  2. Lắng nghe các tín hiệu ngắt từ Hệ điều hành (ví dụ: SIGINT khi nhấn Ctrl+C, hoặc SIGTERM khi Docker/Kubernetes ra lệnh tắt container).

  3. Khi nhận được tín hiệu, gọi hàm srv.Shutdown(ctx) để chờ các tiến trình dọn dẹp.

3. Thực hành: Xây dựng Server chống rớt Request

Đây là "đoạn code chuẩn mực" mà mọi hệ thống Backend bằng Go đều sử dụng làm nền tảng. Bạn hãy chú ý cách chúng ta kết hợp context.Context (đã học ở Bài 8) và os/signal.

Go

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	mux := http.NewServeMux()
	
	// Tạo một endpoint mô phỏng công việc tốn thời gian (3 giây)
	mux.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("Đang xử lý request...")
		time.Sleep(3 * time.Second) // Giả lập query DB
		w.Write([]byte("Đã xử lý xong tác vụ nặng!"))
		fmt.Println("Hoàn thành request.")
	})

	srv := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  120 * time.Second,
	}

	// 1. Chạy Server trong một Goroutine riêng lẻ
	go func() {
		fmt.Println("Server đang khởi động tại port 8080...")
		// ErrServerClosed là lỗi trả về khi gọi srv.Shutdown(), đây là hành vi bình thường
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Lỗi server: %v", err)
		}
	}()

	// 2. Tạo một Channel để nhận tín hiệu tắt từ Hệ điều hành
	quit := make(chan os.Signal, 1)
	
	// Lắng nghe SIGINT (Ctrl+C) và SIGTERM (Docker/K8s stop)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	// 3. Block hàm main tại đây cho đến khi có tín hiệu chui vào channel 'quit'
	<-quit
	fmt.Println("\nNhận được lệnh tắt máy. Đang thực hiện Graceful Shutdown...")

	// 4. Tạo một Context có Timeout (ví dụ: 10 giây). 
	// Ép server phải tắt hoàn toàn sau 10s dù có request chưa chạy xong (tránh bị treo vĩnh viễn)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 5. Ra lệnh Shutdown. Hàm này sẽ block chờ cho đến khi mọi request đang mở hoàn thành 
	// HOẶC cho đến khi context bị timeout (10s).
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatalf("Lỗi trong quá trình Shutdown: %v", err)
	}

	fmt.Println("Server đã dừng an toàn.")
}

Cách kiểm chứng tính năng này:

  1. Chạy chương trình.

  2. Mở trình duyệt truy cập http://localhost:8080/process. Trình duyệt sẽ quay đều (đang xử lý 3 giây).

  3. Ngay lập tức quay lại Terminal và nhấn Ctrl+C.

  4. Bạn sẽ thấy Terminal hiện "Nhận được lệnh tắt máy..." nhưng chương trình KHÔNG THOÁT NGAY. Nó chờ trình duyệt tải xong (in ra "Hoàn thành request.") rồi mới báo "Server đã dừng an toàn." và kết thúc. Không một Request nào bị đánh rơi!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí