Published on

วิธีทำให้ Replicate บน PostgreSQL สามารถเขียนข้อมูลได้

Authors

วิธีทำให้ Replicate บน PostgreSQL สามารถเขียนข้อมูลได้

เนื่องจากมีโจทย์ว่าต้องการที่จะ sync ฐานข้อมูลบางตารางจากฐานข้อมูลจริง มายังฐานข้อมูลอีกตัว แต่ละต้องการที่เพิ่ม แก้ไข และลบได้ด้วย โดยไม่ผลกับฐานข้อมูลจริง ซึ่งโชคดีมากที่ PostgreSQL ตั้งแต่เวอร์ชัน 10 ขึ้นไป มีความสามารถหนึ่งที่เรียกว่า Logical Replication มาช่วยทำงานนี้ได้

Flow

Logical Replication คือ

Logical Replication มันก็คือการทำ replication data รูปแบบหนึ่ง ที่ต้องใช้ตัวอ้างอิงอย่างหนึ่งที่เรียกว่า replication identity โดยปกติมันจะใช้ primary key ซึ่งถ้าไม่มีก็จะใช้ unique key ตัวอื่น หรือถ้าไม่มีเลยก็ใช้ข้อมูลทั้งแถวนั้นๆ เป็นตัวอ้างอิง

โดยการทำงานจะทำผ่านวิธีการ publish และ subscribe โดยเริ่มต้นฝั่ง publisher จะทำการ snapshot ข้อมูล และทำการ copy ไปยังฝั่ง subscriber เมื่อเสร็จแล้ว และฝั่ง publisher เกิดการเปลี่ยนแปลงของข้อมูล (insert, update, delete) ฝั่ง subscriber ก็จะได้รับข้อมูลนั้นๆ ด้วย แบบ real-time โดยอาศัย replication identity เป็นตัวอ้างอิง

ซึ่งฝั่ง subscriber เองนั้นก็สามารถเพิ่ม หรือแก้ไขข้อมูลได้ด้วย โดยการเพิ่มข้อมูลใหม่นั้น มีข้อจำกัดอยู่อย่างหนึ่ง คือ ถ้าให้ replication identity นั้นซ้ำกันเด็ดขาด เพราะจะทำให้เกิด conflict และไม่สามารถทำการ sync ข้อมูลกันได้ ส่วนการอัพเดทข้อมูลในฝั่ง subscriber ถ้าฝั่ง publisher แก้ไขข้อมูลนั้นภายหลัง ข้อมูลนั้นจะถูกเขียนทับเป็นข้อมูลของฝั่ง publisher แทน

วิธีการติดตั้ง Logical Replication

ในการติดตั้งจะต้องใช้ server 2 ตัว ที่ติดตั้ง PostgreSQL เวอร์ชัน 10 ขึ้นไป ซึ่งผมจะเรียกชื่อว่า db-master และ db-replica แทน โดยในตัวอย่างนี้จะทำการติดตั้งผ่าน docker

docker-compose.yml
version: '3.1'

services:
  db-master:
    image: postgres:10-alpine
    restart: always
    ports:
      - 5433:5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: example
    volumes:
      - pg-data-master:/var/lib/postgresql/data
    command: ['postgres', '-c', 'wal_level=logical']

  db-replica:
    image: postgres:10-alpine
    restart: always
    ports:
      - 5434:5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: example
    volumes:
      - pg-data-rep:/var/lib/postgresql/data

volumes:
  pg-data-master:
  pg-data-rep:

ขั้นตอนที่ 1 - เปิดใช้งาน Logical Replication

ในขั้นตอนนี้ถ้าใช้งานผ่าน docker สามารถข้ามไปได้เลย แต่ถ้าไม่ใช่จะตั้งค่าที่ db-master โดยเริ่มจากการแก้ไขไฟล์ postgresql.conf ให้เปิดรับการเชื่อมต่อจาก db-replica ผ่านทาง ip ของตัว db-master เอง

# หาตำแหน่งไฟล์ postgresql.conf
# ใน docker ของผม จะอยู่ที่ /var/lib/postgresql/data/
$ psql -U postgres -c 'SHOW config_file'

# เปิดไฟล์มาแก้ไข
$ sudo nano /var/lib/postgresql/data/postgresql.conf

# แก้ที่ #listen_addresses = 'localhost' เป็น
...
listen_addresses = 'localhost, db_master_ip_address'
...

ใน docker จะตั้งค่าเป็น listen_addresses = '*'

ถัดมาต้องเปิดใช้งาน Write Ahead Log (WAL) เป็น logical

...
wal_level = logical
...

ใน docker สามารถตั้งค่าผ่าน command ได้ ที่ docker-compose.yml บรรทัดที่ 15

ถัดมาเพิ่มให้ db-replica สามารถเชื่อมต่อมาได้ โดยให้แก้ไฟล์ pg_hba.conf ซึ่งจะอยู่ที่ตำแหน่งเดียวกันกับ postgresql.conf

...
# TYPE      DATABASE        USER         ADDRESS                       METHOD
...
host         all            all          db_replica_ip_address/32      md5

ใน docker จะตั้งค่ามาเป็น host all all all md5

สุดท้ายให้ ทำการ restart PostgreSQL server

$ sudo systemctl restart postgresql

ขั้นตอนที่ 2 - สร้างฐานข้อมูล และตั้งค่า user role

เริ่มจากสร้างฐานข้อมูล (ถ้ารันผ่าน docker-compose.yml ข้ามได้ เพราะสร้างมาให้แล้ว)

db-master

$ createdb -U postgres example

db-replica

$ createdb -U postgres example

สร้างตารางชื่อ vitalsigns ทั้ง 2 ที่

db-master

$ psql -U postgres example

example=# CREATE TABLE vitalsigns (id text, weight text, height text, temp text, CONSTRAINT vitalsigns_pkey PRIMARY KEY (id));

db-replica

$ psql -U postgres example

example=# CREATE TABLE vitalsigns (id text, weight text, height text, temp text, CONSTRAINT vitalsigns_pkey PRIMARY KEY (id));

ในฝั่งของ db-replica ไม่จำเป็นที่จะต้องมี column เท่า db-master โดย column ที่เพิ่มมาจะต้องไม่ตั้งค่าเป็น NOT NULL หรือ มี constraints อื่นๆ เพราะจะให้ไม่สามารถ sync มาได้

ถัดมาจะเป็นการเพิ่ม user role สำหรับการทำ Replication โดยต้องทำที่ db-master

# on db-master
example=# CREATE ROLE rep_user WITH REPLICATION LOGIN PASSWORD 'rep_password';

# grant full privileges on the example database to the user role you just created
example=# GRANT ALL PRIVILEGES ON DATABASE example TO rep_user;

# grant privileges on all of the tables contained in the database to your user
example=# GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO rep_user;

ขั้นตอนที่ 3 - การตั้งค่าฝั่ง Publisher

จะเป็นการตั้งค่าที่ db-master ให้เป็น publisher โดยสามารถกำหนดได้ว่าจะทำการ replication ตารางไหนบาง หรือจะเอาทั้งหมดก็ได้ (เพิ่มเติม)

# on db-master

# create a publication called my_pub:
example=# CREATE PUBLICATION my_pub;

# Add the vitalsigns table you created previously to it:
example=# ALTER PUBLICATION my_pub ADD TABLE vitalsigns;

ขั้นตอนที่ 4 - การตั้งค่าฝั่ง Subscriber

จะเป็นการตั้งค่าที่ db-replica ให้เป็น subscriber (เพิ่มเติม)

# on db-replica

# create a subscription called my_sub:
example=# CREATE SUBSCRIPTION my_sub CONNECTION 'host=db-master port=5432 user=rep_user password=rep_password dbname=example' PUBLICATION my_pub;

# You will see the following output confirming the subscription:
# NOTICE:  created replication slot "my_sub" on publisher
# CREATE SUBSCRIPTION

ขั้นตอนที่ 5 - ทดสอบ

ทดลองเพิ่มข้อมูลที่ db-master

# on db-master

example=# INSERT INTO vitalsigns (id, weight, height, temp) VALUES ('1', '60', '160', '36'), ('2', '70', '170', '37'), ('3', '80', '180', '38');

ลองคิวรี่ข้อมูลที่ db-replica

# on db-replica

example=# SELECT * FROM vitalsigns;

# output
 id | weight | height | temp
----+--------+--------+------
  1 | 60     |  160   | 36
  2 | 70     |  170   | 37
  3 | 80     |  180   | 38
(3 rows)

ลองเพิ่มข้อมูลที่ db-replica

# on db-replica

example=# INSERT INTO vitalsigns (id, weight, height, temp) VALUES ('4', '90', '190', '37.5');

example=# SELECT * FROM vitalsigns;

# output
 id | weight | height | temp
----+--------+--------+------
  1 | 60     |  160   | 36
  2 | 70     |  170   | 37
  3 | 80     |  180   | 38
  4 | 90     |  190   | 37.5
(4 rows)

ลองคิวรี่ข้อมูลที่ db-master จะไม่ข้อมูล id = '4'

# on db-master

example=# SELECT * FROM vitalsigns;

# output
 id | weight | height | temp
----+--------+--------+------
  1 | 60     |  160   | 36
  2 | 70     |  170   | 37
  3 | 80     |  180   | 38
(3 rows)

ซึ่ง ถ้ามีการเพิ่มข้อมูลฝั่ง db-master ต้องห้ามใช้ id = '4' โดยเด็ดขาด

ทดลอง update และ delete ฝั่ง db-master

# on db-master

example=# UPDATE vitalsigns SET temp = '37.7' WHERE id = '3';

example=# DELETE FROM vitalsigns WHERE id = '1';

เมื่อมาดูฝั่ง db-replica จะพบว่า id = '3' มีการเปลี่ยนแปลงด้วย และจะไม่พบข้อมูล id = '1' แล้ว

# on db-replica

example=# SELECT * FROM vitalsigns;

# output
 id | weight | height | temp
----+--------+--------+------
  2 | 70     |  170   | 37
  3 | 80     |  180   | 37.7
  4 | 90     |  190   | 37.5
(3 rows)

สรุป

ในบทความนี้ได้แสดงวิธีการทำ Logical Replication ระหว่าง database server 2 ตัว ซึ่งจริงๆ แล้วเราสามารถมีหลาย subscribers ก็ได้ โดยวิธีการนี้สามารถทำ replication ได้แบบ real-time แถมยังเขียนข้อมูลเพิ่มในฝั่ง replication server ไดด้ด้วย แต่ก็มีข้อจำกัดอยู่ที่ replication identity ต้องระวังอย่าให้เกิด conflict ขึ้น เพราะจะทำให้ระบบไม่สามารถทำงานต่อได้