Golang: facing database problems with mssqlx
Go
50
Mysql
44
Database
22
SQL
15
White

Linh Tran Tuan viết ngày 03/11/2017

mssqlx - viết tắt của master-slave sqlx (tên mình tự đặt nên hơi chuối)

TL;DR

Mình còn nhớ trước khi xây dựng backend của dự án iParking, mình đã phải đắn đo rất nhiều để lựa chọn công nghệ.

Sau khi tính toán, mình chọn main language là Go còn main database là Mysql. Mình sẽ không phân tích về advantage hay disadvantage của lựa chọn này (mình là fanboy của Golang và cả Mysql nên thiên vị là đúng thôi =)) ).

Tuy nhiên luôn gặp phải những thách thức, kể cả trong những dự án cỡ nhỏ. Trong bài viết này mình xin share thư viện do mình phát triển, hiện nay đã và đang power cho toàn bộ backend của hệ thống iParking: mssqlx. Đồng thời mình cũng introduce các lỗi thú vị mà mình đã facing.

PS: Thư viện này đã run stable in production, tổng hợp nhiều xử lý và handling lỗi phát sinh trong quá trình thử nghiệm và chạy thật. Có những lỗi mà bạn không tưởng nó sẽ xảy ra mình cũng sẽ explain trong bài viết.

System Requirement

Non functional:

  • High Availability (HA)
  • Hướng hiệu năng cao (high performance oriented)
  • Dễ mở rộng
  • Có thể thay đổi các thành phần trong hệ thống bằng những công nghệ/ứng dụng khác nhau. Chẳng hạn lúc này mình dùng mysql, sau này postgres tốt hơn, đáp ứng được yêu cầu của mình thì có thể thay đổi.

Sizing:

  • Tiết kiệm resource tối đa, sử dụng ít server nhất có thể, bần cùng lắm mới phải tăng thêm. Đây cũng là yêu cầu chung của rất nhiều hệ thống, thay vì scaling/tuning bằng cách mua thêm máy chủ thì thiết kế phần mềm ở mức tối ưu nhất để tránh lãng phí.

Piece of Stack - Powered by MariaDB

Có hai architecture được enable để đáp ứng các yêu cầu khác nhau của hệ thống:

  • Master - Slave
  • Master - Master với MariaDB Galera Cluster - bạn có thể tìm hiểu nó trong link

Tản mạn về master-master with Galera Cluster:

  • Đảm bảo hệ thống HA
  • Select/Insert/Update vào bất cứ máy chủ nào trong cluster cũng được.
  • Khi một máy fail, bạn có thể query vào các máy còn lại. Sau khi được khôi phục lại, máy này sẽ tự join vào cụm masters và thực hiện wsrep sync lại dữ liệu nó behind.

Điểm yếu của Galera Cluster:

  • Việc quản lý và khôi phục data cho một hay nhiều failed node là không hề dễ dàng. Mình đã từng gặp rất nhiều tình huống khó khôi phục trong quá trình thử nghiệm =)) Nhiều khi phải nhìn bằng mắt xem cái máy chủ nào đang chứa dữ liệu latest rồi trigger trong file grastate.dat để biến nó thành Primary mới bootstrap được. Chi tiết bạn có thể tham khảo link.
  • Tốc độ của các câu truy vấn DML sẽ chậm hơn các kiến trúc khác rất nhiều. Lý do là cần sync data giữa các node master khi dữ liệu thay đổi. Như mình có nói ở trên, dữ liệu có thể update từ bất kì máy chủ nào trong cluster, đây là điểm mạnh cũng là điểm yếu. Tradeoff giữa hiệu năng và HA.
  • Bạn có thể sử dụng method rsync hoặc xtrabackup cho quá trình State Snapshot Transfer - tạm dịch là đồng bộ snapshot và state của data - để tăng tốc wsrep sync. Cơ mà tin mình đi, vẫn chậm lắm so với master-slave hoặc standalone (duy nhất một máy chủ cơ sở dữ liệu), chỉ bằng 10-20% gì đó.
  • Không hỗ trợ engine nào khác ngoài InnoDB (hoặc XTRADB). Mình thì mong tương lai có thể hỗ trợ thêm RocksDB, một trong những engine mà mình cực kì ấn tượng, do Facebook phát triển. Để mình làm một bài khác giới thiệu về nó sau ;)

However: mình vẫn chọn nó cho những thành phần của hệ thống cần đảm bảo HA, never fail to write.

Challenge

Để archive các yêu cầu về performance, mình phải xử lý ở nhiều layer khác nhau của toàn hệ thống. Trong đó database là một yếu tố then chốt.

So, muốn tận dụng tối đa sức mạnh của mấy cụm máy chủ db kia, việc chia tải workload là điều vô cùng cần thiết:

  • Với kiến trúc master-slave: write trên master còn balancing query trên các máy slaves.
  • Với kiến trúc master-master: write và query chia đều giữa các masters.

Pre-Solution

Để chia tải thì cách duy nhất là ứng dụng của bạn access vào database thông qua database proxy.

  • Với Galera/Mysql nói chung: MaxScale
  • Postgres: mình thấy hơi phức tạp và có nhiều opensource quá, chưa biết chọn cái nào để đưa lên
  • Others: mình không biết hết được nên đừng gạch đá mình :frowning:

alt

Điểm lợi và bất lợi của việc xài proxy:

  • Lợi ở chỗ bạn không cần phải làm gì, chỉ việc connect vào proxy và thực hiện DML như thông thường; các proxy này thường rất stable :+1:
  • Bất lợi: cần quản lý riêng cụm máy chủ proxy này (thêm việc cho devops nhỉ); cần đầu tư vài máy chủ nữa để đảm bảo HA và tránh single point of failure (SPOF)
  • Bất lợi thứ hai là nếu hệ thống của bạn có rất nhiều microservices, một vài microserivce đôi khi lại cần một vùng trời riêng, thế là bạn lại đầu tư một loạt các máy để làm proxy.

Việc tối thiểu hóa số lượng máy chủ làm proxy khá quan trọng để giảm thiểu chi phí không cần thiết.

Chẳng hạn nếu microservice của bạn chỉ có 2 máy chủ application, cần access vào 2 máy chủ db master một cách riêng biệt, lẽ nào lại đầu tư thêm 2 máy proxy :sob:

Solution

Đây là solution của mình, các bạn có thể chọn giải pháp khác nha: builtin một database proxy vào ứng dụng dưới dạng library .

Mình xin paste lại cái link thư viện này: mssqlx. Và tất nhiên nếu cần, builtin proxy này có thể đặt trước các proxy xịn trên, connect tới nó giống như khi bạn không xài thư viện này vậy.

alt text

Why?

Ngoài điểm lợi nêu trên về việc cắt giảm các máy proxy không cần thiết:

  • Nếu database của bạn không hỗ trợ một proxy kiểu như MaxScale, khi đó bạn cần tìm một solution giống như lib mà mình viết
  • No SPOF
  • Khi bạn connect tới các proxy xịn, thực chất bạn chỉ connect tới một node trong cụm proxy mà thôi. Ở hình vẽ trên, Server 1 hay Server 2 thực chất chỉ connect tới một trong các Node1 Node2 hoặc Node3 mà thôi. Như vậy vẫn còn SPOF.
  • Thư viện mình viết gồm tổng hợp rất nhiều lỗi (cả dị lẫn không dị), các bạn yên tâm là luôn có mình encounter chúng hộ các bạn.

Ngoài lề: strange error

Chúng ta hãy thử nghiệm đoạn code sau:


package main

import (
  _ "github.com/go-sql-driver/mysql"
  // "github.com/jmoiron/sqlx"
  "database/sql"
  "fmt"
  "time"
)

func main() {
    db, err := sql.Open("mysql", "root:123@/test")
    if err != nil {
        panic(err)
    }

    db.SetMaxIdleConns(10)
    db.SetMaxOpenConns(20)
    daemon(db)
}

func daemon(db *sql.DB) {
    count := 0
    for {
        count++
        if err := db.Ping(); err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("SQL: no error on %dth ping\n", count)
        }

        /* COMMENT OUT ME
        if _, err := db.Query("SELECT 1"); err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("No error on %dth select\n", count)
        }
        */

        time.Sleep(2 * time.Second)
    }

Đoạn code trên rất đơn giản, chỉ là connect tới db và thực hiện Ping sau mỗi 2 giây. Tiếp theo mình tắt database đi thì nhận được lỗi sau: bad connection

Rồi mình bật lại database, theo các bạn thì chuyện gì xảy ra? Không còn in ra lỗi nữa?

Câu trả lời của mình là không! Vẫn tiếp tục là bad connection. Tại sao lại như vậy trong khi thư viện database/sql của golang có recommend là sẽ tự động reconnect, vậy mà failed to ping :D

alt text

So why again?

Code inside driver:

func (mc *mysqlConn) Ping(ctx context.Context) error {
    if mc.closed.IsSet() {
        errLog.Print(ErrInvalidConn)
        return driver.ErrBadConn
    }

Đơn giản là driver này của mysql set flag và check flag thay vì reconnect lại. Vì vậy nếu bạn chọn db.Ping() để làm chỉ số đánh giá xem kết nối của bạn đã ổn chưa hoặc server đã khôi phục lại chưa, bạn sẽ fail forever :sob:

Đây lại còn là driver phổ biến nhất hiện nay để connect tới mysql :scream:

Giải pháp mình lựa chọn để failover là comment out cái code db.Query("SELECT 1") ở trên để trigger việc reconnect của driver.

Strange error #2

Khi mình thử bật tắt tùm lum Galera Cluster, mình nhận được lỗi đại khái như sau:

WSREP 140x: wsrep not ready

Lỗi này được trả thẳng từ driver lên application code của mình. OMG, nó không nằm trong bảng lỗi nào của mysql để mà catch.

Mọi thao tác check như Ping hay thậm trí là trigger như trên đầu vô nghĩa. Bởi ping ok, no error! Query cũng ok, no error!

Nhưng! Khi bạn query bất kì table nào đều failed! Lý do là lỗi trên là lỗi của galera, nó thông báo là máy chủ này đang ở giai đoạn sync vì trước đó nó bị failed và phải khởi động lại :scream:

Mình đã phải query tới myql system variable để check available của db: wsrep_onwsrep_ready :sob:

Limited

Điểm hạn chế của mssqlx là mình quyết định base trên thư viện khá nổi tiếng là sqlx để làm underlying.

Nếu bạn nào dùng sqlx rồi sẽ thấy mssqlx khá thoải mái để xài. Còn nếu bạn muốn có một feature rich sql builder thì mssqlx này không dành cho bạn.

Tuy nhiên mình lưu ý là các thư viện khác cũng giống như sqlx, không hỗ trợ kết nối kiểu proxy thế này tới nhiều máy chủ cùng lúc.

Vì vậy mình mới quyết định làm thay việc đó :disappointed_relieved:

Conclusion

Việc sử dụng thư viện trên giúp team mình tối thiểu hóa số lượng server cần thiết, tối thiểu hóa cả công sức của quá trình deployment khi phải setup proxy, tự động phát hiện các máy chủ failed để chuyển luồng query.

PS: còn nhiều loại lỗi khác mà mình đã phát hiện hoặc chưa phát hiện. Nếu các bạn gặp phải các lỗi lạ nào khác thì ping mình với nhé, mình còn update thư viện của mình. Nếu các bạn muốn contribute thư viện trên, vui lòng make PR, mình sẽ active check and merge if ok.

Bonus Point

Nếu bạn đặt timeout connection ở ứng dụng của bạn lớn hơn cấu hình của mysql: wait_timeout hoặc interactive_timeout, connection của bạn sẽ fail unfortunately vì mysql tự động drop khi không có IO Interaction.

Bạn sẽ cần retry lại câu query của mình khi rơi vào trường hợp này hoặc set timeout nhỏ hơn hai thông số trên để driver của bạn tự handle việc close.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Linh Tran Tuan

5 bài viết.
71 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
45 23
Một trong những điểm thú vị nhất khi phát triển các hệ thống Business là lập báo cáo doanh thu. Mình đã từng maintain hệ thống cảnh báo sớm của Cụ...
Linh Tran Tuan viết 9 tháng trước
45 23
White
27 4
Bit operations Các phép toán trên bit luôn give best performance và tối giản hóa bộ nhớ. Hôm nay mình viết bài này note lại cho mọi người xài chơi...
Linh Tran Tuan viết 5 tháng trước
27 4
White
23 3
Introduction Buffering (buffered IO) là một trong những kỹ thuật kinh điển khi chúng ta cần đọc/ghi dữ liệu. Trong bài viết này mình sẽ đi sâu hơn...
Linh Tran Tuan viết 9 tháng trước
23 3
Bài viết liên quan
White
9 2
Makefile thực hiện một số thao tác thường dùng trong Go Khi làm project Go mình thường tạo một file Makefile dạng này: Lưu ý nhớ thay thành tên m...
Huy Trần viết 2 năm trước
9 2
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
5 bài viết.
71 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!