Hiển thị các bài đăng có nhãn Redis. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Redis. Hiển thị tất cả bài đăng

28 tháng 7, 2018

Sử dụng Lock trong hệ thống phân tán với Redis

Sử dụng Lock trong hệ thống phân tán với Redis

Sử dụng lock là một phương pháp đơn giản nhưng rất hữu ích khi mà các tiến trình khác nhau trong nhiều môi trường phải vận hành và chia sẻ các tài nguyên theo cách đồng bộ. Tuy nhiên, quản lý lock như thế nào lại là một bài toán không đơn giản.

Redis có đầy đủ các tính năng mà ta có thể sử dụng như một công cụ quản lý lock trong hệ thống phân tán, và Redis Lab cũng đưa ra một giải thuật mà họ gọi là Redlock để quản lý lock khi sử dụng Redis.

Redis Lab đưa ra ba tiêu chí tối thiểu để sử dụng lock phân tán một cách hiệu quả:

  1. Safety property: Loại trừ lẫn nhau. Tại bất kì thời điểm nào thì chỉ có duy nhất một client được phép giữ lock.

  2. Liveness property A: Giải phóng deadlock. Một tài nguyên không được phép bị lock mãi mãi, ngay cả khi một client đã lock tài nguyên đó nhưng bị treo hoặc gặp sự cố.

  3. Liveness property B: Tính chịu lỗi: Nếu phần lớn các node Redis vẫn đang hoạt động thì client vẫn có thể nhận và giải phóng lock.

Tại sao triển khai dựa trên tính dự phòng vẫn chưa đủ?

Cách đơn giản nhất khi sử dụng Redis để lock một tài nguyên đó là tạo ra một key. Key này thường được tạo có giới hạn thời gian tồn tại bằng cách sử dụng tính năng expire của Redis, vì vậy nó sẽ đảm bảo lock luôn luôn được giải phóng. Khi một máy client cần giải phóng tài nguyên thì chỉ cần xóa bỏ key đã tạo.

Bề ngoài thì rất hợp lí, nhưng có một vấn đề lớn ở đây. Chuyện gì sẽ xảy ra nếu Redis master bị ngừng hoạt động? Đơn giản, hãy chuyển sang sử dụng slave! Thoạt nghe thì điều này không có gì sai, nhưng việc này lại không thể thực hiện được. Chính điều này dẫn đến tiêu chí Loại trừ lẫn nhau không được triển khai vì Redis không đồng bộ.

Ví dụ sau giải thích tại sao sử dụng slave lại không được:

  1. Client A nhận lock ở master.
  2. Master bị treo trước khi đồng bộ key sang slave.
  3. Slave bây giờ được chuyển thành master.
  4. Client B nhận lock trên cùng tài nguyên với client A đang giữ lock. Vì vậy tính loại trừ đã bị vi phạm!

Cách triển khai trên một instance

Trước khi cố gắng tìm cách vượt qua những giới hạn đã được mô tả ở trên, ta hãy kiểm tra tính đúng đắn của giải pháp trong một trường hợp đơn giản xem sao:

SET resource_name my_random_value NX PX 30000 

Câu lệnh trên sẽ chỉ thiết đặt một key (NX option) nếu nó chưa tồn tại, với expire time là 30,000 ms (PX option). Key này được thiết lập giá trị là "myrandomvalue". Giá trị này phải là duy nhất trong tất cả client và tất cả yêu cầu tạo lock.

Về cơ bản thì sử dụng giá trị ngẫu nhiên là để có thể giải phóng lock theo cách an toàn, bằng tập lệnh gửi cho Redis: chỉ xóa key nếu nó tồn tại và giá trị đang lưu trữ bởi key đó phải giống với giá trị mà client mong đợi. Điều này được thực hiện bằng cách sử dụng Lua script sau đây:

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

Điều này rất quan trọng để có thể tránh được việc xóa một lock đã được tạo bởi một client khác. Ví dụ, một client nhận lock, chặn một số hoạt động với thời gian lâu hơn thời gian tồn tại của lock đó, và sau đó xóa lock, trong khi thời điểm này lock đó đã được một client khác giữ. Chỉ sử dụng DEL sẽ không an toàn vì client này có thể xóa lock của một client khác đang giữ.

Thuật toán Redlock

Ta đã tìm hiểu qua cách nhận và giải phóng một lock trên một instance đơn lẻ. Bây giờ, giả định là ta sẽ có N=5 Redis master, và các node này hoàn toàn độc lập với nhau, được đặt trên các máy khác nhau.

Để nhận lock, một client thực hiện các thao tác sau:

  1. Nhận thời gian hiện tại theo đơn vị mili giây.
  2. Cố gắng tạo lock tuần tự trong tất cả N instance, sử dụng cùng một tên key và giá trị ngẫu nhiên cho tất cả instance. Trong suốt bước 2, khi thiết đặt lock trong mỗi instance, client sử dụng một timeout nhỏ hơn so với tổng thời gian tự động giải phóng lock để tạo lock. Ví dụ, nếu thời gian giải phóng lock tự động là 10 giây, thì timeout có thể nằm trong khoảng 5-50 ms. Việc này ngăn chặn client cố gắng kết nối với một Redis node bị treo trong một thời gian dài: nếu một instance không sẵn dùng, ta nên thử kết nối với instance tiếp theo càng sớm càng tốt.
  3. Client sẽ phải tính toán xem bao nhiêu thời gian đã trôi qua để nhận được lock bằng cách sử dụng thời gian hiện tại đã lấy được từ bước 1. Nếu và chỉ nếu client nhận được lock trong phần lớn các instance (ít nhất là 3), và tổng thời gian trôi qua để có được lock ít hơn thời gian hiệu lực của lock, lock đó sẽ được nhận.
  4. Nếu lock đã được nhận, thời gian hiệu lực thực sự của nó chính là thời gian hiệu lực khởi tạo ban đầu trừ đi thời gian trôi qua trong lúc tạo lock, như đã được tính trong bước 3.
  5. Nếu client không thể nhận được lock vì một vài lí do (hoặc nó không thể tạo lock trên N/2 + 1 instance, hoặc thời gian hiệu lực là số âm), nó sẽ cố gắng unlock trên tất cả instance (ngay cả những instance mà nó cho rằng vẫn chưa tạo được lock).

Thuật toán này có thể xử lý bất đồng bộ hay không?

Thuật toán này giả định thời gian trong mọi tiến trình phải xấp xỉ nhau và với cùng tốc độ

Thử lại khi không thành công

Khi một client không thể nhận được lock, nó nên cố gắng thử lại sau một thời gian trễ ngẫu nhiên để tránh không đồng bộ hóa nhiều client đang cố gắng tạo lock cho cùng một tài nguyên và trong cùng một thời điểm (điều này có thể dẫn đến tình trạng split-brain, thiếu nhất quán về dữ liệu, dữ liệu bị chồng chéo lên nhau, không client nào tạo được lock). Như vậy, một client nếu nhanh hơn sẽ lấy được lock trong phần lớn các Redis instance, đây là một cách nhìn khác dẫn của tình trạng split-brain, vì vậy lí tưởng nhất là client nên cố gắng gửi các lệnh SET đến N instance trong cùng một thời điểm bằng phương pháp ghép kênh.

Điều này nhấn mạnh tầm quan trọng của việc giải phóng lock ngay khi có thể khi client thất bại tạo lock trong phần lớn các instance.

Giải phóng lock

Giải phóng lock không có gì phức tạp, chỉ việc giải phóng lock trong tất cả các instance cho dù client không biết là lock đó có được tạo trên một instance cụ thể nào hay không.

Thuật toán Redlock có an toàn hay không?

Chúng ta có thể thử trong các kịch bản khác nhau.

Giả sử, một client có thể nhận được lock trong phần lớn các instance. Tất cả instance sẽ cùng chứa một key với thời gian tồn tại giống nhau. Tuy nhiên, key này được thiết lập ở những thời điểm khác nhau, vì vậy key này cũng sẽ hết hạn ở những thời điểm khác nhau. Trong tường hợp xấu nhất là key đầu tiên được thiết lập ở thời điểm T1, và key cuối cùng được thiết lập ở thời điểm T2, ta vẫn chắc chắn được rằng key đầu tiên sẽ có thời gian tồn tại nhỏ nhất MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT. Tất cả những key khác sẽ hết hạn sau, vì vậy chúng ta chắc chắn rằng các key sẽ đồng thời được thiết lập với thời gian này.

Trong suốt khoảng thời gian này, phần lớn các key đã được thiết lập, một client khác sẽ không thể tạo lock vì N/2+1 lệnh SET NX không thể thành công nếu N/2+1 key đã tồn tại. Vì vậy nếu một lock được tạo, thì nó không thể được tạo lại trong cùng một thời điểm.

Tuy nhiên, chúng ta cũng muốn đảm bảo rằng nhiều client đang cố gắng tạo lock trong cùng một thời điểm không thể thành công đồng thời.

Nếu một client đã tạo lock trong phần lớn các instance sử dụng một thời gian bằng, hoặc lớn hơn thời gian hiệu lực tối đa, thì nó sẽ quyết định lock này không hợp lệ và sẽ unlock các instance đó. Vậy chúng ta chỉ cần xem xét trường hợp client có thể lock phần lớn các instance trong thời gian ít hơn thời gian hiệu lực. Trong trường hợp này, nhiều client có thể tạo được lock trên N/2+1 các instance ở cùng một thời điểm (với thời gian khi kết thúc ở bước 2) chỉ khi thời gian để tạo lock trong phần lớn các instance lớn hơn thời gian TTL, làm cho lock đó không hợp lệ.

25 tháng 6, 2018

Cấu hình Redis tăng khả năng đáp ứng cao

High Availability (HA) - Khả năng đáp ứng cao

HA có thể được hiểu là:

  1. Một hệ thống có khả năng phục vụ liên tục
  2. Chịu nhiệt cao (tức là khi nhiều người yêu cầu phục vụ thì vẫn có thể chạy ổn định).

Bài viết này ta sẽ tập trung vào mục (1).

Redis replication

Là phương pháp sử dụng "lính dự bị", lên đảm nhiệm vai trò của "lính chủ lực" khi ông chủ lực kiệt sức chết.

Ta sẽ sử dụng 1 server chính (master) và 2 server dự bị (slave).

Tổng cộng sẽ cần 3 server vật lý, sau này sẽ gọi là node để khỏi nhầm lẫn với redis server.

3 node này có IP lần lượt là:

master: 10.0.1.100
slave-1: 10.0.1.101
slave-2: 10.0.1.102

Cài đặt Redis:

Mình sử dụng Ubuntu 16.04.

Cài redis vào cả 3 NODE

sudo apt-get update
sudo apt-get install redis-server -y

Cấu hình cho node master

Ở node master, mở file /etc/redis/redis.conf lên và cấu hình:

Tìm đến dòng bind 127.0.0.1, đây là khai báo redis server sẽ lắng nghe request ở đâu.

Với cấu hình mặc định thì nó chỉ lắng nghe từ localhost (hiện tại đang là dòng 69) nên mình đổi về IP của node:

bind 10.0.1.100 127.0.0.1

Lưu lại file sau đó khởi động lại redis server:

sudo systemctl restart redis-server.service

Test thử server master

$ redis-cli -h 10.0.1.100
10.0.1.100:6379> info replication
Redis master output
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

Mặc định, redis server sẽ chạy ở cổng 6379.

Ghi dữ liệu vào để lát nữa đọc thử từ slave ra:

10.0.1.100:6379> set test 'this key was defined on the master server'
OK

Thoát redis server:

10.0.1.100:6379> exit

Cấu hình cho các NODE slave

Trên 2 NODE slave:

Trước khi cấu hình thì kết nối vào server để đảm bảo rằng nó chưa có dữ liệu bên master.

$ redis-cli
127.0.0.1:6379> get test
(nil)

Tiếp đến, mở file /etc/redis/redis.conf lên và cấu hình:

bind 10.0.1.101 127.0.0.1

slave 2:

bind 10.0.1.102 127.0.0.1

Tiếp, tìm đến dòng có slaveof <masterip> <masterport> (dòng 281), điền vào:

slaveof 10.0.1.100 6379

Cấu hình này sẽ khai báo đây là node dự bị (slave) cho NODE chính 10.0.1.100 và liên lạc với nó thông qua cổng 6379 - cổng mà bên kia đang lắng nghe.

OK, lưu lại rồi khởi động lại redis server:

sudo systemctl restart redis-server.service

Test thử server slave:

Bây giờ ta sẽ vào server slave và đọc dữ liệu ở bên server master ra:

$ redis-cli
10.0.1.101:6379> get test
'this key was defined on the master server'

Kiểm tra thông tin về replication:

10.0.1.101:6379> info replication
Redis slave output
# Replication
role:slave
master_host:10.0.1.100
master_port:6379
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:1387
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

Nếu bạn kiểm tra thông tin tương tự bên server master thì sẽ thấy có chút cập nhật so với lúc nãy:

master$ redis-cli
10.0.1.100:6379> info replication
Redis master output
# Replication
role:master
connected_slaves:2
slave0:ip=10.0.1.101,port=6379,state=online,offset=1737,lag=1
slave0:ip=10.0.1.102,port=6379,state=online,offset=10000,lag=1
master_repl_offset:1737
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:1736

OK, đến đây là xong phần redis replication. Tiếp đến, dùng sentinel để quản lý việc đưa salve lên làm master khi ông master bị down.

Sentinel

Cơ chế hoạt động:

Các sentinel sẽ luôn quan sát master server, khi master sập, các sentinels sẽ loan truyền nhau 1 tín hiệu sdown: tao thấy đại ca chết rồi thì phải.

Khi đủ 1 số lượng n sentinel đồng ý rằng tao cũng thấy master sập rồi, tụi sentinels sẽ loan tiếp tín hiệu odown: nó thực sự chết rồi đó.

Lúc này, tụi sentinels sẽ bầu chọn ra 1 slave để nâng cấp lên làm master mới, đồng thời cập nhật các cấu hình theo bộ máy chính quyền mới.

Khi thằng master kia sống lại, nó sẽ được tham gia vào băng nhóm với vai trò slave.

Cài đặt và cấu hình:

Cài đặt sentinel trên cả 3 NODE:

$ sudo apt-get install redis-sentinel -y

Mở file /etc/redis/sentinel.conf và cấu hình:

daemonize yes
pidfile "/var/run/redis/redis-sentinel.pid"
logfile "/var/log/redis/redis-sentinel.log"

bind 10.0.1.100
port 26379

sentinel monitor mymaster 10.0.1.100 6379 2
sentinel down-after-milliseconds mymaster 2000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
  • bind 10.0.1.100: báo cho các sentinel ở NODE khác biết rằng tôi đang lắng nghe ở địa chỉ này.
  • port 26379: để cho dễ nhớ thì thường là lấy cổng của redis +20000 rồi làm cổng sentinel.
  • sentinel monitor mymaster 10.0.1.100 6379 2: lệnh này khai báo là sẽ lắng nghe thằng master ở địa chỉ 10.0.1.100:6379, tham số cuối cùng (2) là số lượng sentinel tối thiểu để tham gia việc bầu chọn (lúc xác định master chết, và bầu master mới), mymaster là tên của master.
  • sentinel down-after-milliseconds mymaster 2000: sau 2 giây mà không thấy đại ca phản hồi thì tao sẽ loan tin sdown đi.

Hai cấu hình cuối cùng thì có thể tham khảo thêm ở đây

Cấu hình cho 2 slave server cũng tương tự cho master, chỉ khác 1 chỗ duy nhất là địa chỉ để bind-dùng IP của slave server tương ứng:

daemonize yes
pidfile "/var/run/redis/redis-sentinel.pid"
logfile "/var/log/redis/redis-sentinel.log"

bind 10.0.1.101
port 26379

sentinel monitor mymaster 10.0.1.100 6379 2
sentinel down-after-milliseconds mymaster 2000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

OK, bây giờ khởi động lại cả 3 sentinel:

sudo systemctl restart redis-server.service

Kiểm tra việc bầu cử

Đầu tiên, mở các file log ra để xem diễn biến băng nhóm:

master$ tailf /var/log/redis/redis-sentinel.log
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 4.0.10 (01888d1e/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 16379
 |    `-._   `._    /     _.-'    |     PID: 57464
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               
 
57464:X 07 Jul 16:33:18.109 # Sentinel runid is 978afe015b4554fdd131957ef688ca4ec3651ea1
57464:X 07 Jul 16:33:18.109 # +monitor master mymaster 10.0.1.100 6379 quorum 2
57464:X 07 Jul 16:33:18.111 * +slave slave 10.0.1.101:6381 10.0.1.101 6379 @ mymaster 10.0.1.100 6379
57464:X 07 Jul 16:33:18.205 * +sentinel sentinel 10.0.1.101:16379 10.0.1.101 16379 @ mymaster 10.0.1.100 6379
57464:X 07 Jul 16:33:18.111 * +slave slave 10.0.1.102:6381 10.0.1.102 6379 @ mymaster 10.0.1.100 6379
57464:X 07 Jul 16:33:18.205 * +sentinel sentinel 10.0.1.102:16379 10.0.1.102 16379 @ mymaster 10.0.1.100 6379

Kiểm tra xem ai đang là master

$ redis-cli -p 26379 sentinel get-master-addr-by-name mymaster

 1) "10.0.1.100"
 2) "6379"

Đánh sập master để bầu master mới

Trên master server:

master$ sudo systemctl stop redis-server.service

Nhìn vào log, bạn sẽ thấy thông tin về việc loan tin và bầu cử:

57464:X 07 Jul 16:35:30.270 # +sdown master mymaster 10.0.1.100 6379
57464:X 07 Jul 16:35:30.301 # +new-epoch 1
57464:X 07 Jul 16:35:30.301 # +vote-for-leader 2a4d7647d2e995bd7315d8358efbd336d7fc79ad 1
57464:X 07 Jul 16:35:30.330 # +odown master mymaster 10.0.1.100 6379 #quorum 3/2
57464:X 07 Jul 16:35:30.330 # Next failover delay: I will not start a failover before Tue Jul  7 16:35:50 2015
57464:X 07 Jul 16:35:31.432 # +config-update-from sentinel 10.0.1.101:16379 10.0.1.101 16379 @ mymaster 10.0.1.101 6379
57464:X 07 Jul 16:35:31.432 # +switch-master mymaster 10.0.1.101 6379 10.0.1.101 6379
57464:X 07 Jul 16:35:31.432 * +slave slave 10.0.1.102:6379 10.0.1.102 6379 @ mymaster 10.0.1.101 6379
57464:X 07 Jul 16:35:36.519 # +sdown slave 10.0.1.102:6379 10.0.1.102 6379 @ mymaster 10.0.1.101 6379

Giờ thì kiểm tra xem NODE nào được lên làm master:

$ redis-cli -p 16379 sentinel get-master-addr-by-name mymaster

 1) "10.0.1.101"
 2) "6379"

Ta thử khởi động lại master lúc nãy, và xem trong log sẽ thấy nó đã được gia nhập nhóm lại, nhưng bây giờ với vai trò là slave