Scaling Reads with Single Leader Replication in MySQL

Scaling Reads with Single Leader Replication in MySQL
Photo by Kyaw Tun / Unsplash

One of the reasons for replication is to increase read throughput. This is done by scaling out the number of nodes that can serve read queries. In this blog post, we will setup one leader node (Leader Replica) to handle all write operations and three follower nodes (Follower Replica 1, 2 and 3) to process read requests. The leader node can also process read requests.

We will set this Single Leader Replication model using MySQL Docker containers.

1. Setup Docker Compose

version: '3.8'

services:
  my-sql-write-read-replica:
    image: mysql:8.0
    container_name: my-sql-write-read-replica
    environment:
      MYSQL_ROOT_PASSWORD: Root1234!
      MYSQL_DATABASE: ecommerce_db
      MYSQL_USER: replica_user
      MYSQL_PASSWORD: Replica1234!
    volumes:
      - ./mysql-certs:/etc/mysql/certs
    ports:
      - "3306:3306"
    command: >
      --server-id=1
      --log-bin=mysql-bin
      --binlog-do-db=ecommerce_db
      --require_secure_transport=ON
      --ssl-ca=/etc/mysql/certs/ca-cert.pem
      --ssl-cert=/etc/mysql/certs/server-cert.pem
      --ssl-key=/etc/mysql/certs/server-key.pem
    networks:
      - mysql-net
    restart: always

  mysql-read-replica-1:
    image: mysql:8.0
    container_name: mysql-read-replica-1
    environment:
      MYSQL_ROOT_PASSWORD: Root1234!
      MYSQL_DATABASE: ecommerce_db
      MYSQL_USER: replica_user
      MYSQL_PASSWORD: Replica1234!
    volumes:
      - ./mysql-certs:/etc/mysql/certs
    ports:
      - "3307:3306"
    command: >
      --server-id=2
      --log-bin=mysql-bin      
      --relay-log=slave-relay-bin
      --read-only=ON
      --require_secure_transport=ON
      --ssl-ca=/etc/mysql/certs/ca-cert.pem
      --ssl-cert=/etc/mysql/certs/client-cert.pem
      --ssl-key=/etc/mysql/certs/client-key.pem
    networks:
      - mysql-net
    restart: always

  mysql-read-replica-2:
    image: mysql:8.0
    container_name: mysql-read-replica-2
    environment:
      MYSQL_ROOT_PASSWORD: Root1234!
      MYSQL_DATABASE: ecommerce_db
      MYSQL_USER: replica_user
      MYSQL_PASSWORD: Replica1234!
    volumes:
      - ./mysql-certs:/etc/mysql/certs
    ports:
      - "3308:3306"
    command: >
      --server-id=3
      --log-bin=mysql-bin
      --relay-log=slave-relay-bin
      --read-only=ON
      --require_secure_transport=ON
      --ssl-ca=/etc/mysql/certs/ca-cert.pem           
      --ssl-cert=/etc/mysql/certs/client-cert.pem     
      --ssl-key=/etc/mysql/certs/client-key.pem       
    networks:
      - mysql-net
    restart: always

  mysql-read-replica-3:
    image: mysql:8.0
    container_name: mysql-read-replica-3
    environment:
      MYSQL_ROOT_PASSWORD: Root1234!
      MYSQL_DATABASE: ecommerce_db
      MYSQL_USER: replica_user
      MYSQL_PASSWORD: Replica1234!
    volumes:
      - ./mysql-certs:/etc/mysql/certs
    ports:
      - "3309:3306"
    command: >
      --server-id=4
      --log-bin=mysql-bin
      --relay-log=slave-relay-bin
      --read-only=ON
      --require_secure_transport=ON
      --ssl-ca=/etc/mysql/certs/ca-cert.pem
      --ssl-cert=/etc/mysql/certs/client-cert.pem
      --ssl-key=/etc/mysql/certs/client-key.pem
    networks:
      - mysql-net
    restart: always

networks:
  mysql-net:
    driver: bridge

Few things to note about MySQL server replication options:

  • --log-bin=mysql-bin - all database changes will be logged to a binary log file.
  • --binlog-do-db=ecommerce_db - only log changes for a specific database (in this case ecommerce_db).
  • --relay-log=slave-relay-bin - relay log stores changes sent from the leader node. The read replicas (followers) uses these logs to update its own database with those changes.

2. Spin up Containers

docker-compose up -d

3. Configure the Leader

1. Access Leader Node

docker exec -it my-sql-write-read-replica mysql -u root -p

2. Execute SQL

CREATE USER 'replica_user'@'%' IDENTIFIED BY 'Replica1234!';
GRANT REPLICATION SLAVE ON *.* TO 'replica_user'@'%';
FLUSH PRIVILEGES;

3. View Binary Log Status

SHOW MASTER STATUS;

4. Configure Follower (Read Replica 1)

1. Connect to Read Replica 1

docker exec -it mysql-read-replica-1 mysql -u root -p

2. Execute SQL

CHANGE MASTER TO 
MASTER_HOST='my-sql-write-read-replica',
MASTER_USER='replica_user',
MASTER_PASSWORD='Replica1234!',
MASTER_SSL=1,
MASTER_SSL_CA='/etc/mysql/certs/ca-cert.pem',
MASTER_SSL_CERT='/etc/mysql/certs/client-cert.pem',
MASTER_SSL_KEY='/etc/mysql/certs/client-key.pem',
MASTER_LOG_FILE='mysql-bin.000003',  
MASTER_LOG_POS=  552;  

3. Start the Replica Node and Check Status

START SLAVE;

SHOW SLAVE STATUS\G;

Note: ensure Slave_IO_Running and Slave_SQL_Running is set to Yes

5. Configure Follower (Read Replica 2)

1. Connect to Read Replica 2

docker exec -it mysql-read-replica-2 mysql -u root -p

2. Execute SQL

CHANGE MASTER TO 
MASTER_HOST='my-sql-write-read-replica',
MASTER_USER='replica_user',
MASTER_PASSWORD='Replica1234!',
MASTER_SSL=1,
MASTER_SSL_CA='/etc/mysql/certs/ca-cert.pem',
MASTER_SSL_CERT='/etc/mysql/certs/client-cert.pem',
MASTER_SSL_KEY='/etc/mysql/certs/client-key.pem',
MASTER_LOG_FILE='mysql-bin.000003',  
MASTER_LOG_POS=  552;  

3. Start the Replica Node and Check Status

START SLAVE;

SHOW SLAVE STATUS\G;

Note: ensure Slave_IO_Running and Slave_SQL_Running is set to Yes

6. Configure Follower (Read Replica 3)

1. Connect to Read Replica 3

docker exec -it mysql-read-replica-3 mysql -u root -p

2. Execute SQL

CHANGE MASTER TO 
MASTER_HOST='my-sql-write-read-replica',
MASTER_USER='replica_user',
MASTER_PASSWORD='Replica1234!',
MASTER_SSL=1,
MASTER_SSL_CA='/etc/mysql/certs/ca-cert.pem',
MASTER_SSL_CERT='/etc/mysql/certs/client-cert.pem',
MASTER_SSL_KEY='/etc/mysql/certs/client-key.pem',
MASTER_LOG_FILE='mysql-bin.000003',  
MASTER_LOG_POS=  552;  

3. Start the Replica Node and Check Status

START SLAVE;

SHOW SLAVE STATUS\G;

Note: ensure Slave_IO_Running and Slave_SQL_Running is set to Yes

7. Test the Replication

1. Execute SQL on the Leader Node

USE ecommerce_db;
CREATE TABLE Customers (
    CustomerID INT AUTO_INCREMENT PRIMARY KEY,
    FirstName VARCHAR(50),
    LastName VARCHAR(50),
    Email VARCHAR(100)
);
INSERT INTO Customers (FirstName, LastName, Email)
VALUES ('John', 'Doe', 'john.doe@example.com');

2. Execute SQL on Read Replica 1

USE ecommerce_db; 
SELECT * FROM Customers;

3. Execute SQL on Read Replica 2

USE ecommerce_db; 
SELECT * FROM Customers;

4. Execute SQL on Read Replica 3

USE ecommerce_db; 
SELECT * FROM Customers;

We can see consistent results on all read replicas.

8. Let's try inserting data into Read Replica 1

1. Access Read Replica 1

docker exec -it mysql-read-replica-1 mysql -u replica_user -p

2. Execute SQL

USE ecommerce_db;
INSERT INTO Customers (FirstName, LastName, Email)
VALUES ('Jane', 'Smith', 'jane.smith@example.com');

Inserting into the database from the read replica is denied:

Pros and Cons of Single Leader Replication

Single Leader Replication allows one leader to co-ordinate all writes. This ensures data consistency for follower nodes. With one leader handling all write operations, there is no need for conflict resolution strategies. However, since there is only one leader co-ordinating all write operations, the leader itself becomes a single point of failure. Write scalability is also limited.

The docker-compose.yml file is available in the GitHub repository