Hôm nay mình sẽ giới thiêu cho các bạn các config một ứng dụng web sử dụng kiến trúc database replication.
Trước khi bắt đầu thì đảm bảo rằng máy tính của bạn đã cài những chương trình sau:
- Docker
- Ruby on Rails v6.0
- MySQL Workbench
References
Nếu bạn không biết kiến trúc database replication là gì thì có thể tham khảo bài viết sau để hiểu thêm
https://kipalog.com/posts/Gioi-thieu-MySQL-Replication
Ngoài ra, bài viết này mình còn tham khảo thêm ở các link sau:
https://github.com/wagnerjfr/mysql-master-slaves-replication-docker
https://tomkadwill.com/multiple-databases-in-rails
1. Overview
Mình sẽ build một ví dụ về ứng dụng web bằng framework Rails, có database sử dụng kiến trúc Replication, gồm 1 master và 2 slave. Công việc mà chúng ta cần phải làm sẽ gồm 2 công việc chính:
- Thiết lập kiến trúc Replication ở tầng Database
- Xây dựng Web app có thể đọc và ghi dữ liệu với kiến trúc database đã xây dựng.
2. Xây dựng kiến trúc Master Slave với MySQL
Trên thực tế, khi xây dựng kiến trúc Master Slave, thì mỗi instance sẽ nằm ở mỗi host khác nhau, tuy nhiên lúc thực hiện demo này mình chỉ có thể sử dụng 1 máy tính mà thôi, cho nên mình sẽ sử dụng docker để có thể tạo ra các intance Mysql server để xây dựng.
2.1 Cài đặt Mysql Server docker container
Khởi tạo một docker network
1 2 | $ docker network create replicanet |
Để xem các docker network đang có, sử dụng câu lệnh
1 2 | $ docker network ls |
Sử dụng các dòng lệnh dưới đây để khởi tạo 3 MySQL container.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $ docker run -d --name=master --net=replicanet --hostname=master -p 3308:3306 -v $PWD/d0:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mypass mysql/mysql-server:5.7 --server-id=1 --log-bin='mysql-bin-1.log' $ docker run -d --name=slave1 --net=replicanet --hostname=slave1 -p 3309:3306 -v $PWD/d1:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mypass mysql/mysql-server:5.7 --server-id=2 $ docker run -d --name=slave2 --net=replicanet --hostname=slave2 -p 3310:3306 -v $PWD/d2:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mypass mysql/mysql-server:5.7 --server-id=3 |
Có thể kiếm tra các container đã start hay chưa bằng câu lệnh
1 2 | $ docker ps -a |
Chờ đến khi các container đạt trạng thái healthy thì mới tiếp tục.
1 2 3 4 5 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 33790572c785 mysql/mysql-server:5.7 "/entrypoint.sh --..." 22 minutes ago Up 22 minutes (healthy) 33060/tcp, 0.0.0.0:3310->3306/tcp slave2 0918892aaad3 mysql/mysql-server:5.7 "/entrypoint.sh --..." 22 minutes ago Up 22 minutes (healthy) 33060/tcp, 0.0.0.0:3309->3306/tcp slave1 3ece985d3470 mysql/mysql-server:5.7 "/entrypoint.sh --..." 22 minutes ago Up 22 minutes (healthy) 33060/tcp, 0.0.0.0:3308->3306/tcp master |
2.2 Configuring Master và Slave
2.2.1 Cài đặt Master Node
[Optional] Nếu như bạn muốn sử dụng cơ chế semisynchronous replication, thì hãy chạy câu lệnh bên dưới
1 2 3 4 5 6 | docker exec -it master mysql -uroot -pmypass -e "INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';" -e "SET GLOBAL rpl_semi_sync_master_enabled = 1;" -e "SET GLOBAL rpl_semi_sync_master_wait_for_slave_count = 2;" -e "SHOW VARIABLES LIKE 'rpl_semi_sync%';" |
Việc cài đặt plugin semisynchronous nhầm mục đích giảm thiểu việc lag dữ liệu trong quá trình đồng bộ data giữa master và slave.
Nếu cài thành công thì sẽ có output như sau
1 2 3 4 5 6 7 8 9 10 11 12 | mysql: [Warning] Using a password on the command line interface can be insecure. +-------------------------------------------+------------+ | Variable_name | Value | +-------------------------------------------+------------+ | rpl_semi_sync_master_enabled | ON | | rpl_semi_sync_master_timeout | 10000 | | rpl_semi_sync_master_trace_level | 32 | | rpl_semi_sync_master_wait_for_slave_count | 2 | | rpl_semi_sync_master_wait_no_slave | ON | | rpl_semi_sync_master_wait_point | AFTER_SYNC | +-------------------------------------------+------------+ |
Khởi tạo ở master node một user replicate để các slave có thể access vào và lấy dữ liệu.
1 2 3 4 5 | docker exec -it master mysql -uroot -pmypass -e "CREATE USER 'repl'@'%' IDENTIFIED BY 'slavepass';" -e "GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';" -e "SHOW MASTER STATUS;" |
Output:
1 2 3 4 5 6 7 | mysql: [Warning] Using a password on the command line interface can be insecure. +--------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +--------------------+----------+--------------+------------------+-------------------+ | mysql-bin-1.000003 | 595 | | | | +--------------------+----------+--------------+------------------+-------------------+ |
2.2.2 Cài đặt Slaves Node
Tương tự, để có thể sử dụng được cơ chế semisynchronous thì ở các node slave cần cài đặt plugin
1 2 3 4 5 6 7 | for N in 1 2 do docker exec -it slave$N mysql -uroot -pmypass -e "INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';" -e "SET GLOBAL rpl_semi_sync_slave_enabled = 1;" -e "SHOW VARIABLES LIKE 'rpl_semi_sync%';" done |
Output
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | mysql: [Warning] Using a password on the command line interface can be insecure. +---------------------------------+-------+ | Variable_name | Value | +---------------------------------+-------+ | rpl_semi_sync_slave_enabled | ON | | rpl_semi_sync_slave_trace_level | 32 | +---------------------------------+-------+ mysql: [Warning] Using a password on the command line interface can be insecure. +---------------------------------+-------+ | Variable_name | Value | +---------------------------------+-------+ | rpl_semi_sync_slave_enabled | ON | | rpl_semi_sync_slave_trace_level | 32 | |
Thay đổi tên của log file ở Master Node mà chúng ta đã print ra trước khi chạy câu lệnh setting ở slave node
Giá trị cần thay đổi theo hệ thống trên máy của bản:
1 2 | MASTER_LOG_FILE='mysql-bin-1.000003' |
Chạy lệnh bên dưới để setting Slave Node
1 2 3 4 5 6 7 8 | for N in 1 2 do docker exec -it slave$N mysql -uroot -pmypass -e "CHANGE MASTER TO MASTER_HOST='master', MASTER_USER='repl', MASTER_PASSWORD='slavepass', MASTER_LOG_FILE='mysql-bin-1.000003';" docker exec -it slave$N mysql -uroot -pmypass -e "START SLAVE;" done |
Kiểm tra trạng thái slave replication trên slave1 và slave2:
1 2 3 4 | $ docker exec -it slave1 mysql -uroot -pmypass -e "SHOW SLAVE STATUSG" $ docker exec -it slave2 mysql -uroot -pmypass -e "SHOW SLAVE STATUSG" |
Nếu output tương tự như bên dưới thì thành công.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | *************************** 1. row *************************** Slave_IO_State: Checking master version Master_Host: master Master_User: repl Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin-1.000003 Read_Master_Log_Pos: 595 Relay_Log_File: slave1-relay-bin.000001 Relay_Log_Pos: 4 Relay_Master_Log_File: mysql-bin-1.000003 Slave_IO_Running: Yes Slave_SQL_Running: Yes ... |
2.2.3 Testing kết quả cài đặt
Bây giờ mình sẽ kiểm tra xem kết quả cài đặt có thành công hay không bằng cách tạo một database mới ở Master Node, nếu đúng thì sẽ đồng bộ database đó sang Slave Node.
Thực hiện chạy câu lệnh bên dưới ở Master Node
1 2 | $ docker exec -it master mysql -uroot -pmypass -e "CREATE DATABASE TEST; SHOW DATABASES;" |
Output:
1 2 3 4 5 6 7 8 9 10 11 | mysql: [Warning] Using a password on the command line interface can be insecure. +--------------------+ | Database | +--------------------+ | information_schema | | TEST | | mysql | | performance_schema | | sys | +--------------------+ |
Kiểm tra ở slave node đã được đồng bộ sang hay chưa
1 2 3 4 5 6 | for N in 1 2 do docker exec -it slave$N mysql -uroot -pmypass -e "SHOW VARIABLES WHERE Variable_name = 'hostname';" -e "SHOW DATABASES;" done |
Nếu output ra ở 2 slave node đều tồn tại Database TEST thì xem như chúng ta đã cài đặt thành công.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | mysql: [Warning] Using a password on the command line interface can be insecure. +---------------+--------+ | Variable_name | Value | +---------------+--------+ | hostname | slave1 | +---------------+--------+ +--------------------+ | Database | +--------------------+ | information_schema | | TEST | | mysql | | performance_schema | | sys | +--------------------+ mysql: [Warning] Using a password on the command line interface can be insecure. +---------------+--------+ | Variable_name | Value | +---------------+--------+ | hostname | slave2 | +---------------+--------+ +--------------------+ | Database | +--------------------+ | information_schema | | TEST | | mysql | | performance_schema | | sys | +--------------------+ |
2.3 Lưu ý nhỏ
Khi cài đặt các instance mysql server với docker, thì đê có thể access mysql từ môi trường bên ngoài vào trong docker container thì bạn cần tạo một account mysql vào cấp quyền cho nó như sau (sử dụng để access với mysql workbench hoặc một ứng dụng rails truy xuất vào như demo mà mình thực hiện bên dưới):
B1: Vào bash của docker container
1 2 | docker exec -it <mysql container name> /bin/bash |
B2: Mở terminal của mysql server
1 2 | mysql -u root -p |
B3: Thực hiện các dòng lệnh sau:
1 2 3 4 | create user 'user'@'%' identified by 'pass'; grant all privileges on *.* to 'user'@'%' with grant option; flush privileges; |
Kí hiệu (%) nghĩa là allow tất cả ip có thể truy cập vào. Nếu bạn muốn hạn chế thì có thể thay đổi dấu % bằng ip mà bạn mong muốn truy cập đến database.
3. Xây dựng webapp với Rails framework
3.1 Sử dụng multiple database trong Rails 6
Có 1 điểm thú vị là từ Rails 6 trở đi, thì framework này đã hỗ trợ multiple database, cho nên chúng ta có thể dễ dàng cài đặt ứng dụng của mình sử dụng kiến trúc replication.
Đầu tiên mình khởi tạo web app với rails 6 như sau:
1 2 | $ rails new rails_replication_without_gem -d mysql |
Sau đó dùng lệnh scaffold để generate nhanh một flow CRUD cơ bản
1 2 | $ rails g scaffold User name:string address:string |
Bây giờ đến phần quan trọng nhất là tiến hành config database, sử dụng kiến trúc replication.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> socket: /var/run/mysqld/mysqld.sock development: master: <<: *default database: master username: user password: pass host: 127.0.0.1 port: 3308 slave1: <<: *default database: slave1 host: 127.0.0.1 port: 3309 username: user password: pass replica: true slave2: <<: *default database: slave2 host: 127.0.0.1 port: 3309 username: user password: pass replica: true |
OK, tương tự như việc config multiple database với Rails 6 thì việc config cho database replication cũng tương tự, chỉ khác là chúng ta sẽ thêm vào option
1 2 | replica: true |
để xác định node nào là node slave.
Sau đó tiến hành create và migrate database
1 2 | rails db:create && rails db:migrate |
Có 1 chỗ mà bạn nên chú ý, khi mình tiến hành create database thì log chỉ trả về một database master được tạo, nhờ vào option replica: true mà mình đã config. Trong trường hợp không config hoặc là set bằng false, thì sẽ có 3 database được khởi tạo đó là master, slave1 và slave2.
Sau đó trong model, chúng ta sẽ sử dụng hàm connects_to để chỉ định database nào dùng để write và databas nào dùng để đọc. Theo như kiến trúc master-slave thì dữ liệu sẽ được ghi vào master và đọc ở slave.
1 2 3 4 | class User << ApplicationRecord connects_to database: { writing: :master, reading: :slave } end |
OK, bây giờ có 1 vấn đề đặt ra là, ở phần overview ban đầu, thì kiến trúc database của mình bao gồm 1 master và 2 slave cơ mà. Tại sao ở trong này mình lại config có 1 node slave mà thôi ?
Đây là một hạn chế khi sử dụng multiple database của Rails khi áp dụng với kiến trúc Replication. Chúng ta chỉ có thể setting 1 Node Master và 1 Node Slave mà thôi. Trong trường hợp sử dụng với 2 Slave trở lên thì mình sẽ tiếp tục hướng dẫn ở section tiếp theo.
Thêm vào đó, có một vấn đề mà Rails 6 đã support chúng ta đó là giải quyết việc lag dữ liệu bằng việc tự động switching read/write database.
Như chúng ta đã biết mỗi request ghi/chỉnh sửa dữ liệu sẽ đươc gửi đến node master, và request đọc dữ liệu sẽ gửi đến node slave. Tuy nhiên trong trường hợp có request gửi đến yêu cầu đọc dữ liệu trong vòng chưa tới 2s sau 1 request write, thì request read đó sẽ được gửi tới node master thay vì slave để đảm bảo có thể đọc được dữ liệu mới nhất thay vì dữ liệu cũ chưa được cập nhật ở slave. Mặc định sẽ là 2s, tuy nhiên chúng ta có thể setting ở file application.rb
Để enable thì chúng ta config như sau ở file application.rb
1 2 3 4 | config.active_record.database_selector = { delay: 2.seconds } config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session |
3.2 Sử dụng gem Makara
Như mình đã đề cập ở trên, thì khi sử dụng multiple với Rails 6, thì nó có 1 nhược điểm là không thể nào sử dụng 2 slave trở lên cho toàn bộ model. Tuy nhiên một số gem lại có thể làm được điều này đó là: Octopus và Makara.
Theo như thông báo thì gem Octopus đang ở mode maitain do Rails 6 ra mắt đã có support multiple database. Cho nên mình sẽ sử dụng gem Makara để làm example. Mặc dù publish muộn hơn gem Octopus, nhưng Makara được người dùng đánh giá tốt hơn hẳn so với Octopus.
Còn việc cài đặt gem thì cực kì đơn giản, bạn chỉ cần vào trang document của gem thì hoàn toàn có thể làm đươc.
Ngoài việc cho phép người dùng có thể setup master slave dễ dàng, thì gem Makara còn cho phép chúng ta thêm nhiều tùy biến khác như là điều chỉnh trọng số nhận request của các slave, khả năng thông báo lỗi cũng rất rõ ràng.
Kết luận
Trên đây là phần hướng dẫn của mình về cách cài đặt một kiến trúc database replication, thêm vào đó là cách cấu hình để để sử dụng với một web app Rails cơ bản.
Và các bạn cần lưu ý là tùy vào ứng dụng của bạn và ưu nhược điểm của mỗi loại kiến trúc database để lựa chọn kiến trúc cho phù hợp với ứng dụng của mình.
Hy vọng bài viết mang lại cho bạn nhiều kiến thức bổ ích. Thân