Lỗi file txt bị null khi tắt máy đột ngột năm 2024

Ở các bài trước chúng ta đã tìm hiểu về các vấn đề trong lập trình web mà cụ thể là lập trình dịch vụ web với Go. Phần kiểm thử dịch vụ, các bạn có thể xem ở đây. Trong bài này chúng ta sẽ bàn về công đoạn triển khai dịch vụ và quản lý bảo trì dịch vụ.

Ghi log

Mặc dù việc kiểm thử có thể giúp hạn chế lỗi khi phát triển phần mềm, nhưng sự cố là không thể tránh khỏi nhất là với những chương trình phức tạp. Lúc này, việc ghi lại trạng thái của chương trình tại các thời điểm khác nhau sẽ rất cần thiết cho việc phát hiện nguyên nhân và xử lý sự cố sau đó. Ngoài ra, các thông tin này có thể giúp cải tiến chương trình về giao diện sử dụng, tốc độ, v.v. Đó là lí do chúng ta cần ghi log. Thông thường log được ghi xuống file hoặc vào ghi thẳng vacơ sở dữ liệu.

Log thường chia ra các cấp độ dựa trên mức độ cần thiết của thông tin lưu và mức nghiêm trọng của sự cố như sau:

  • OFF: Không cần ghi log.
  • DEBUG: Các thông tin cần trong quá trình kiểm thử sẽ được ghi lại.
  • INFO: ghi các thông tin về trạng thái, hoạt động của chương trình.
  • WARNING: các thông tin mang tính chất cảnh báo được ghi lại.
  • ERROR: ghi nhận các lỗi chương trình.
  • FATAL: các lỗi rất nghiêm trọng có thể khiến ứng dụng không thể tiếp tục hoạt động.
  • ALL: tất cả các thông tin và lỗi đều được ghi nhận.

Go cung cấp package log giúp ghi log ở mức đơn giản. Vì chỉ là log đơn giản nên nó không cung cấp các cấp độ log. Tất cả các tính năng mà package log cung cấp có thể tóm gọn như sau:

- Hỗ trợ 3 nhóm hàm ghi log: Print[f|ln], Fatal[f|ln] và Panic[f|ln]. Nhóm Fatal thực chất là gọi Print tương ứng rồi gọi os.Exit(1) còn nhóm Panic gọi panic sau khi gọi Print tương ứng.

func Print(v ...interface{}) func Printf(format string, v ...interface{}) func Println(v ...interface{}) func Fatal(v ...interface{}) func Fatalf(format string, v ...interface{}) func Fatalln(v ...interface{}) func Panic(v ...interface{}) func Panicf(format string, v ...interface{}) func Panicln(v ...interface{})

- Hỗ trợ ghi ra file hay các môi trường ghi chuẩn: os.Stdout, os.Stderr, v.v... thông qua hàm func SetOutput(w io.Writer)

- Hỗ trợ thêm nhãn để xác định loại log thông qua hàm func SetPrefix(prefix string)

- Hỗ trợ xuất các thông tin phụ: thời gian, file và dòng xuất thông tin này:

const ( Ldate \= 1 << iota // Ngày theo giờ địa phương: 11/11/2017 Ltime // Giờ theo giờ địa phương: 11:23:23 Lmicroseconds // Xuất micro giây: 01:23:23.123123 dùng với Ltime. Llongfile // Đường dẫn đầy đủ đến file và số dòng: /a/b/c/d.go:23 Lshortfile // Tên file và số dòng: d.go:23. LUTC // Lấy giờ UTC thay vì giờ địa phương nếu có xuất thời gian LstdFlags \= Ldate | Ltime // Giá trị mặc định: 11/11/2017 11:23:23 )

Ví dụ sau thể hiện cách ghi log ra file sử dụng package log nhưng có tùy biến để có thể cung cấp các cấp độ log:

- Ở đây chúng ta sử dụng struct Logger thuộc package log để tạo các đối tượng cho cấp độ log. Hàm new tạo con trỏ đối tượng Logger:

func New(out io.Writer, prefix string, flag int) *Logger

- Hàm Init tạo 3 đối tượng cho 3 cấp độ log: INFO, WARNING và ERROR với nhãn tương ứng và xuất các thông tin file, vị trí và thời gian.

var (

lInfo *log.Logger lWarning *log.Logger lError *log.Logger )

func Init(out io.Writer) { flag := log.LstdFlags | log.Lshortfile lInfo \= log.New(out, "INFO: ", flag) lWarning \= log.New(out, "WARNING: ", flag) lError \= log.New(out, "ERROR: ", flag) }

- Môi trường ghi được xác định là file "log.txt" được khởi tạo ở đầu hàm main như sau:

f, err := os.OpenFile("log.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) if err != nil { return } defer f.Close()

- Khai báo thông tin xuất file và ghi log các loại:

Init(f) lInfo.Println("Thông tin") lWarning.Println("Cảnh báo") lError.Println("Lỗi")

- Nội dung trong file log.txt như sau:

INFO: 2017/11/11 21:42:57 logs.go:29: Thông tin WARNING: 2017/11/11 21:42:57 logs.go:30: Cảnh báo ERROR: 2017/11/11 21:42:57 logs.go:31: Lỗi

Do package log khá đơn giản nên đã có rất nhiều package khác được các lập trình viên viết ra để phục vụ cho việc ghi log. Có thể kể đến:

  • https://github.com/op/go-logging: package nhỏ gọn.
  • https://github.com/Sirupsen/logrus: được nhiều dự án lớn sử dụng.
  • https://github.com/inconshreveable/log15
  • https://github.com/golang/glog: từ Google, chuyển từ thư viện glog viết bằng C++ sang Go.
  • https://github.com/cihub/seelog
  • https://github.com/uber-go/zap
  • https://github.com/kardianos/service
  • https://github.com/natefinch/lumberjack

Lỗi ứng dụng

Khi dịch vụ chạy, những lỗi không biết trước sẽ xuất hiện. Các lỗi thường hay xảy ra là:

- Lỗi cơ sở dữ liệu: liên quan đến việc truy xuất hoặc lưu trữ dữ liệu như:

+ Lỗi kết nối: do sai thông tin tài khoản, database không tồn tại hoặc lỗi ứng dụng quản lý cơ sở dữ liệu hoặc lỗi khi truy xuất cơ sở dữ liệu từ xa.

+ Lỗi truy vấn: thực hiện câu truy vấn SQL sai, cái này thường nếu kiểm thử kỹ sẽ tránh được lỗi này.

+ Lỗi dữ liệu: lỗi liên quan đến thao tác với dữ liệu null/rỗng hoặc trùng khóa. Lỗi này cũng có thể tránh với bước kiểm thử kỹ càng.

- Lỗi ứng dụng khi thực thi: liên quan đến tất cả các lỗi khác trong hoạt động ứng dụng:

+ Lỗi file: lỗi này liên quan đến ghi file lỗi do không đủ quyền, đọc file lỗi do file không tồn tại hoặc sai định dạng mong muốn.

+ Lỗi thư viện: lỗi này liên quan đến lỗi có trong các thư viện dịch vụ sử dụng.

+ Lỗi HTTP: Lỗi này thường liên quan đến gửi yêu cầu sai hoặc không được phép. Các mã lỗi nhận về là 404 (không tìm thấy), 401 (lỗi xác thực), 403 (cấm), 503 (lỗi không xác định).

+ Lỗi hệ thống: lỗi này thường do hệ điều hành hoạt động không ổn định dẫn đến lỗi nhập xuất tài nguyên.

+ Lỗi mạng: lỗi này do mất kết nối sau khi gửi yêu cầu nên không nhận được phản hồi hoặc khi gửi yêu cầu đến server đã bị ngưng hoạt động hoặc mất kết nối.

Các mục tiêu cần đạt khi xử lý lỗi:

- Thông báo lỗi cho người dùng: khi có lỗi xảy ra, người dùng cần được biết. Thông báo có thể gửi về cho client qua mã lỗi, qua trang web thông báo. Client phải hiển thị thông báo phù hợp cho người dùng.

- Ghi log: tất cả các lỗi đều cần phải ghi log lại cụ thể để có thể tìm ra nguyên nhân và sửa lỗi sau đó. Phần ghi log ở trên giúp giải quyết chuyện này. Nếu là lỗi nghiêm trọng (FATAL), quản trị cần được báo qua tin nhắn SMS hoặc email thay vì chỉ đơn thuần là ghi log lại.

- Hủy bỏ tác vụ lỗi: Thường liên quan đến cập nhật dữ liệu vào database, nếu có một bước bị lỗi thì các bước trước đó trong tác vụ cần phải hủy bỏ bằng cách khôi phục lại trạng thái trước (roll back) để tránh bị bất đồng bộ dữ liệu.

- Đảm bảo ứng dụng luôn chạy: Với những tình huống lỗi nghiêm trọng, ứng dụng bị tắt thì cần đảm bảo ứng dụng được chạy lại ngay sau đó để tiếp tục phục vụ client.

Lưu ý: Khi gặp tình huống lỗi nghiêm trọng, không nên sử dụng log.Fatal vì bản chất nó gọi os.Exit(1) mà hàm Exit này sẽ ngưng ứng dụng ngay mà không xử lý an toàn ví dụ như chạy các defer chẳng hạn. Do đó chúng ta cần sử dụng log.Panic để đảm bảo trước khi thoát ứng dụng, các tác vụ an toàn hệ thống được thực thi. Về việc xử lý ngoại lệ với panic, các bạn có thể xem lại ở bài 16.

Quản lý dịch vụ

Sau khi biên dịch chúng ta sẽ có file thực thi. File này có thể chạy độc lập dễ dàng tạo ra dịch vụ web phục vụ mọi yêu cầu từ client. Với các cấu hình phù hợp, chúng ta có thể giúp dịch vụ tự động chạy khi hệ thống khởi động. Nhưng khi cần ngưng dịch vụ để nâng cấp hoặc thay đổi cấu hình chúng ta phải làm sao? Cách nhanh nhất và thô bạo nhất là ngừng tiến trình dịch vụ web này nhưng làm vậy việc tắt dịch vụ sẽ diễn ra đột ngột dẫn đến các bước hủy tài nguyên bị bỏ qua. Rất may là từ phiên bản 1.8 Go đã cung cấp cách thức giúp chúng ta chủ động ngưng dịch vụ web. Sau đây chúng ta sẽ tìm hiểu cách thức tắt dịch vụ với package net/http và cả cách mở lại dịch vụ bằng cách sử dụng package bên thứ ba:

Đóng dịch vụ web

Để có thể chủ động đóng dịch vụ web, chúng ta phải khai báo một biến kiểu http.Server và sử dụng phương thức Shutdown do nó cung cấp: func Shutdown(ctx context.Context) error Ví dụ hàm mở và tắt dịch vụ web sẽ như sau:

func startServer(port string) {

btServer \= &http.Server{Addr: "127.0.0.1:" + port}

http.Handle("/", http.FileServer(http.Dir("public")))

fmt.Printf("Lắng nghe port 127.0.0.1:%s...", port) go func() { if err := btServer.ListenAndServe(); err != nil { fmt.Println("Lỗi Httpserver: ListenAndServe()", err) } }() }

func stopServer() error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()

err := btServer.Shutdown(ctx) if err != nil { fmt.Println("Lỗi tắt server", err) } return err }

Khi phương thức Shutdown được gọi, chúng sẽ lần lượt dừng việc lắng nghe kết nối mới, đóng các kết nối đang mở hoặc tạm nghỉ. Nếu việc tắt dịch vụ lâu hơn thời gian khai báo ở biến ngữ cảnh ctx, hệ thống sẽ báo lỗi. Như vậy với phương thức Shutdown, chúng ta đã có thể tự mình tắt dịch vụ một cách chủ động. Nếu muốn sử dụng dịch vụ bên thứ ba, chúng ta có thể thử tylerb/graceful hay facebookgo/httpdown

Chạy lại dịch vụ với fvbock/endless

Package endless giúp chúng ta đảm bảo dịch vụ có thể chạy liên tục ngay cả khi chúng ta cần chạy lại dịch vụ. Cách thức xử lý của nó sẽ như sau:

- Chạy tiến trình mới đảm nhận lắng nghe cổng dịch vụ.

- Tiến trình này khởi tạo và chấp nhận các kết nối trên cổng này.

- Gửi tín hiệu yêu cầu tiến trình cũ ngừng nhận kết nối và đóng tiến trình.

Nó lắng nghe các tín hiệu ngắt syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGINT, syscall.SIGTERM, và syscall.SIGTSTP. Khi nhận SIGHUP, nó sẽ tiến hành chạy lại dịch vụ. Khi nhận SIGINT hoặc SIGTERM nó sẽ tiến hành đóng dịch vụ an toàn.

Sử dụng nó cũng rất đơn giản như sau:

package main

import ( "log" "net/http" "os"

"github.com/fvbock/endless" "github.com/gorilla/mux" )

func handler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World!")) }

func main() { mux := mux.NewRouter() mux.HandleFunc("/hello", handler).Methods("GET")

err := endless.ListenAndServe(":4242", mux) if err != nil { log.Println(err) } log.Println("Server ở port 4242 đã ngừng!")

os.Exit(0) }

Lưu ý là do endless hỗ trợ một số tín hiệu ngắt không có trên Windows như SIGUSR1, SIGUSR2,... nên thực hiện go get với package này sẽ bị lỗi. Một package khác cũng cung cấp dịch vụ tương tự là facebookgo/grace.

Sao lưu và phục hồi

Một trong những công việc quan trọng khi đưa dịch vụ vào hoạt động là sao lưu dữ liệu dịch vụ trong quá trình hoạt động và phục hồi lại khi dữ liệu gặp vấn đề. Dù không mong muốn nhưng đôi khi dữ liệu sẽ bị mất bởi hư ổ cứng dữ liệu, lỗi hệ điều hành hay cơ sở dữ liệu bị lỗi. Tất cả những lỗi này cần phải khắc phục nhanh nhất có thể để tránh làm gián đoạn dịch vụ.

Sao lưu dữ liệu

Hiện nay để đảm bảo an toàn cho dữ liệu, tốt nhất nên đưa dữ liệu lên lưu trữ trên các dịch vụ trên đám mây (cloud). Các dịch vụ này sẽ đảm bảo dữ liệu của chúng ta không mất bởi chúng sẽ được sao lưu ở nhiều nơi khác nhau. Khi không có cloud hoặc muốn tạo một bản sao an toàn thì làm thế nào? Có rất nhiều công cụ hỗ trợ việc đồng bộ lưu trữ các file. Ở đây tôi giới thiệu với các bạn Rsync (Remote Sync), một dịch vụ đồng bộ file nổi tiếng trên môi trường Unix và Linux. Trên Windows có thể dùng Cwrsync.

Tính năng nổi bật của Rsync là:

- Rsync hỗ trợ sao chép giữ nguyên thông số của files/folder như liên kết, quyền, thời gian, sở hữu và nhóm.

- Rsync chỉ trao đổi những dữ liệu thay đổi nên thời gian đồng bộ nhanh.

- Rsync tiết kiệm băng thông do sử dụng phương pháp nén và giải nén khi đồng bộ.

- Rsync không yêu cầu quyền super-user.

Chúng ta có thể lấy file cài đặt từ trang download chính của Rsync, hiện có bản rpm cho Linux và cwRsync cho Windows. Các distro Linux có thể dùng câu lệnh cài đặt vì rsync có sẵn trong các bản phân phối Linux:

sudo apt-get install rsync // Dùng cho debian, ubuntu, và tương tự

yum install rsync // Dùng cho Fedora, Redhat, CentOS, và tương tự

rpm -ivh rsync // Dùng cho Fedora, Redhat, CentOS, và tương tự

Ngoài ra chúng ta có thể cài đặt thông qua việc biên dịch mã nguồn, được cung cấp sẵn trên trang download.

Câu lệnh của Rsync:

rsync <tham số> <dữ liệu nguồn> <dữ liệu đích>

Các tham số phổ biến:

  • -v: hiển thị trạng thái kết quả.
  • -r: copy dữ liệu một cách đệ quy, nhưng không đảm bảo thông số của file và thư mục.
  • -a: cho phép copy dữ liệu một cách đệ quy, đồng thời giữ nguyên được tất cả các thông số của thư mục và file.
  • -z: nén dữ liệu khi đồng bộ, tiết kiệm băng thông tuy nhiên tốn thêm một chút thời gian.
  • -h: xuất kết quả dễ đọc.
  • --delete: xóa dữ liệu ở đích nếu bên nguồn không tồn tại dữ liệu đó.
  • --exclude: loại trừ ra những dữ liệu không muốn truyền đi, nếu bạn cần loại ra nhiều file hoặc folder ở nhiều đường dẫn khác nhau thì mỗi cái bạn phải thêm --exclude tương ứng.

Tất cả các tham số có thể tham khảo ở đây.

Ví dụ đồng bộ từ thư mục /home/test/Documents/ebook/ đến /media/test/bkdisk/ebook ta thực hiện:

rsync -avzh /home/test/Documents/ebook /media/test/bkdisk/

Thường để sao lưu dữ liệu trên server chúng ta sẽ dùng rsync để đồng bộ về máy khác, lúc này ta có thể dùng câu lệnh như sau:

rsync -avz /home/test/Documents/ebook --password-file=/etc/rsyncd.secrets [email protected]:/media/test/bkdisk/

Tham số --password-file khai báo thông tin mật khẩu. Việc này giúp chúng ta không phải nhập mật khẩu mỗi lần chạy rsync. Tạo file chứa mật khẩu như sau:

echo '<tên truy cập>: <Mật khẩu>' \> /etc/rsyncd.secrets chmod 600 /etc/rsyncd.secrets

Thường đồng bộ sao lưu sẽ chạy tự động nhưng rsync không có chức năng này nên cần kết hợp với dịch vụ kiểu như crontab trên Linux/Mac OS hay Task Scheduler trên Windows.

Sao lưu cơ sở dữ liệu

Ở đây tôi bàn về sao lưu MySQL, cơ sở dữ liệu phổ phiến nhất cho các ứng dụng dịch vụ web vừa và nhỏ. Cách phổ biến để sao lưu cơ sở dữ liệu MySQL là sử dụng mysqldump để sao lưu ra file.

Câu lệnh đơn giản của nó như sau:

mysqldump -u <tên truy cập> -p<mật khẩu> --<tùy chọn> <database cần sao lưu> \> <tên file sao lưu>.sql

Ví dụ cần sao lưu nhiều database, ta dùng tùy chọn --databases, câu lệnh sẽ như sau:

mysqldump -u <tên truy cập> -p<mật khẩu> --databases <database 1\> <database 2\> \> <tên file sao lưu>.sql

Với --all-databases, mọi database sẽ được sao lưu.

Lưu ý ở các câu lệnh trên là mật khẩu viết ngay sau -p, không có khoảng trắng. Nếu không có mật khẩu thì sau khi thực thi lệnh hệ thống yêu cầu nhập mật khẩu trước khi thực hiện sao lưu.

Các tham số khác:

  • --add-drop-table: Thêm câu lệnh DROP TABLE trước mỗi lệnh CREATE TABLE trong file sao lưu.
  • --no-data: Chỉ sao lưu cấu trúc database, không sao lưu dữ liệu.

Để tạo file sao lưu nén chúng ta có thể dùng pipeline theo câu lệnh sau:

mysqldump -u <tên truy cập> -p<mật khẩu> --<tùy chọn> <database cần sao lưu> | gzip -9 \> <tên file sao lưu>.sql.gz

Cách phục hồi lại như sau:

mysql -u <tên truy cập> -p<mật khẩu> <database cần sao lưu> < <tên file sao lưu>.sql

Với database đã tồn tại, để phục hồi chúng ta dùng:

mysqlimport -u <tên truy cập> -p<mật khẩu> <database cần sao lưu> <tên file sao lưu>.sql

Để phục hồi với file đã nén, chúng ta sử dụng câu lệnh:

gunzip < [<tên file sao lưu>.sql.gz] | mysql -u <tên truy cập> -p<mật khẩu> <database cần sao lưu>

Như vậy, mọi vấn đề mà tôi nghĩ cần thiết để xây dựng dịch vụ web với Go đều đã được cố gắng chia sẻ trong 12 bài, từ bài 25 đến bài 36 này. Hy vọng các bạn nắm được cơ bản và tự xây dựng cho mình dịch vụ web ưng ý với Go. Trong loạt bài tới tôi sẽ hệ thống lại phần này bằng cách mô tả chi tiết việc xây dựng một dịch vụ web cụ thể.