Cùng với Algorithm thì System Design là một phần rất quan trọng trong phỏng vấn và tuyển dụng, nhất là bạn ứng tuyển với vị trí từ Senior trở lên. Và để cùng tôi chuyển bị tốt cho đợt phỏng vấn tuyển dụng sắp tới, ta hãy cùng nhau tìm hiểu về những điều cơ bản và sơ lược nhất về thiết kế hệ thống nhé. Nói trước đây là một bài viết siêu dài, mời các bạn hãy ngồi xuống thư giãn và làm ly trà sữa cho tăng mood 😋 và bật nhạc thư giãn 🎼 trước khi đọc bài nhé.
Mục lục
Phần 1: Cách tính chất chính của một hệ thống phân tán
Phần 2: Cân bằng tải (Load Balancing)
Phần 3: Bộ đệm — Caching
Phần 4: Phân chia dữ liệu — Sharding/Data Partitioning
Phần 5: Indexes
Phần 6: Proxies
Phần 7: Sao lưu và đồng bộ dữ liệu — Redundancy and Replication
Phần 8: SQL vs. NoSQL
Phần 9: Định lý CAP / CAP Theorem
Phần 10: Consistent Hashing
Phần 11: Long-Polling vs WebSockets vs Server- Sent Events
Phần 1: Các tính chất chính của một hệ thống phân tán.
Hệ thống phân tán (distributed system) là hệ thống phần mềm mà các thành phần cấu tạo nên nó nằm ở trên các máy tính khác nhau được kết nối thành mạng lưới (network). Các máy tính này phối hợp hoạt động với nhau để hoàn thành một nhiệm vụ chung bằng cách trao đổi qua lại các thông điệp (message)
Nói nôm na là hệ thống phân tán là việc hệ thống bạn có nhiều quá trình xử lý độc lập trên nhiều nhiều server vật lý khác nhau.
Với các hệ thống doanh nghiệp lớn (enterprise) đòi hỏi sự linh hoạt trong mở rộng bảo trì thì distributed system là một lựa chọn hoàn hảo.Và các tính chất chính để hình thành một hệ thống phân tán là: Khả năng mở rộng(Scalability), Độ tin cậy (Reliability), Tính khả dụng (Availability), Hiệu suất (Efficiency) và Tình mở rộng và khả năng bảo trì (Manageability).
1. Khả năng mở rộng (Scalability)
Scalability là khả năng mở rộng (scaling) của hệ thống (system), quy trình (process) hay mạng lưới (network), với nhu cầu gia tăng về số lượng công việc tăng theo thời gian của mô hình kinh doanh (business model).
Mô hình kinh doanh có thể mở rộng quy mô vì nhiều lý do như gia tăng khối lượng dữ liệu lưu trữ (data storage) hay khối lượng công việc (process/request) , ví dụ: số lượng truy cập hay đặt hàng của một hệ thống thương mại điện tử. Và yêu cầu của sự mở rộng phải đạt được nhu cầu này mà không làm giảm hiệu suất, nói chung Scalability là đáp ứng được sử mở rộng hay giảm theo kích thước của hệ thống theo thời gian.
Có hai dạng scaling là mở rộng theo chiều dọc (vertical scaling) và mở rộng theo chiều ngang (horizontal scaling).
- Vertical scaling: là cách mở rộng server hiện tại bằng cách nâng cấp độ mạnh (power) bằng cách nâng cấp CPU, Ram, Storage, v.V… Vertical-scaling thường bị giới hạn bởi vượt quá khả năng về cấu hình vật lý hiện đại hay độ trễ khi “chẳng may” Server bị downtime để nâng cấp hay deploy hệ thống.
- Horizontal scaling: là cách mở rộng bằng cách thêm nhiều Node/Server vào một mạng lưới đang có, làm tăng khả năng chịu tải có hệ thống. Cách làm này rẻ và dễ làm hơn so với Vertical-scaling, đặc biệt là rất dễ dàng downsize cũng như upsize hệ thống
- Horizontal scaling: là cách mở rộng bằng cách thêm nhiều Node/Server vào một mạng lưới đang có, làm tăng khả năng chịu tải có hệ thống. Cách làm này rẻ và dễ làm hơn so với Vertical-scaling, đặc biệt là rất dễ dàng downsize cũng như upsize hệ thống
Một ví dụ của Horizontal-scaling là MongoDB và Cassandra, cả hai đều cung cấp sẵn những phương pháp để scale hệ thống bằng cách thêm nhiều node vào hoặc xóa bớt các node mà không hề có độ trễ (zero downtime). Và một ví dụ khác về Vertical-scaling là MySQL, nó có thể dễ dàng chuyển đổi một Server đang chạy sang một Server mới lớn hơn khỏe hơn, nhưng quá trình có downtime.
2. Độ tin cậy (Reliability)
Reliability có thể giải thích dân dã rằng đó là độ “lì” 💪 của hệ thống có nghĩa là hệ thống sẽ tiếp tục cung cấp dịch vụ của mình ngay khi có một hoặc nhiều thành phần (phần mềm/phần cứng) của hệ thống bị lỗi. Reliability là thành phần chính trong bất cứ một hệ thống phân tán (distributed system) nào, bởi vì trong một hệ thống như vậy, mọi hỏng hóc của một thành phần nào đó sẽ được thay thế một thành phần đang khỏe mạnh khác, đảm bảo luôn hoàn thành nhiệm vụ yêu cầu.
Ví dụ của độ tin cậy là, một trang thương mại điện tử hay hệ thống ngân hàng (banking) mọi thông tin về giao dịch (transaction) của người dùng sẽ không bao giờ bị hủy do lỗi server đang chạy giao dịch đó, mỗi server bị lỗi sẽ phải được thay thế ngay bởi một bản sao chứa đầy đủ thông tin của server đó.
Để đạt được độ tin cậy, hệ thống phải có chế độ back-up real time của từng thành phần trong hệ thống, đây cũng là một thách thức về mặt kỹ thuật cũng như chi phí của dự án.
3. Tính sẵn sàng (Availability)
Tính sẵn sàng là thời gian một hệ thống vẫn hoạt động bình thường trong một khoảng thời gian cụ thể, đây là thước đo đơn giản về tỷ lệ phần trăm thời gian mà hệ thống hoạt động liên tục trong một khoảng thời gian bình thường. Ví dụ như một chiếc xe hơi có thể chạy trong nhiều tháng mà không cần bảo trì bảo dưỡng, thì có thể nói là chiếc xe đó có tính availability cao. Nếu chiếc xe hơi đó ngừng hoạt động để đem tới gara để bảo trì, nó được coi là không availability trong thời gian đó.
Sự khác nhau của độ tin cậy (reliability) và tính sẵn sàng (availability):
Nếu hệ thống có tính reliability thì nó chắc chắn sẽ có availability, tuy nhiên hệ thống có tính availability không có nghĩa là nó có tính reliability. Nói một cách khác thì reliability có nghĩa là nó có tính high availability, tuy nhiên vẫn có thể đạt được tính availability với một hệ thống không có tính reliability bằng cách giảm thiểu tối đa thời gian bảo trì, sửa chữa. Hãy lấy một ví dụ, một hệ thống eCommerce có tỷ lệ availability lên đến 99,99% trong hai năm đầu tiên nó bắt đầu, nhưng hệ thống có một lỗi tiềm ẩn về bảo mật mà trong quá trình kiểm thử (testing) không phát hiện ra, khách hàng không hề biết về điều đó và họ vẫn rất hạnh phúc (happy) với hệ thống, cho đến một ngày đẹp trời vào bỗng nhiên lỗi tiềm ẩn đó bị khai thác dẫn đến hệ thống giảm tính sẵn sàng trong một thời gian dài hơn bình thường cho đến khi lỗi được “hot fix” ngay lập tức.
Kỳ thực mà nói một hệ thống có độ tin cậy cao (high reliability) gần như rất khó đạt được trong thực tế, mà hầu như chúng ta chỉ hướng tới một hệ thống có tính sẵn sàng cao (high availability) mà thôi.
4. Hiệu suất (Efficiency)
Hiệu suất của một hệ thống phân tán là khả năng chịu tải (high load) và thời gian phản hồi (low latency). Có nghĩa là một hệ thống có khả năng chịu được nhiều request đồng thời với độ trễ thấp là một hệ thống có hiệu suất cao. Thông thường nó được đo đếm bằng số lượng request nó nhận được và phản hồi trong một khoảng thời gian, thường được tính bằng giây. Ví dụ một hệ thống eCommerce có hiệu suất là chịu được 5k lượt đặt hàng trên một giây — 5k order / second, hay 500k lượt người cùng truy cập vào cùng một thời điểm.
5. Tính mở rộng và khả năng bảo trì (Manageability)
Một tính chất quan trọng khác của một distributed system đó là khả năng dễ dàng mở rộng và bảo trì của hệ thống, nói cách khác là tốc độ của hệ thống khi thực hiện sửa chữa (repair) hay bảo trì (maintain) khi cần, nếu thời gian trên càng cao thì tính availability càng thấp. Để đạt được điều này, hệ thống cần phải dễ dàng phát hiện lỗi hoặc lỗi tiềm tàng nếu có, khả năng hiểu nhanh được nguyên nhân lỗi (root cause), dễ dàng thực hiện các thay đổi cần thiết để điều chỉnh, hoặc đơn giản chỉ là dễ dàng mở rộng khi cần.
Việc sớm phát hiện cũng như giải quyết vấn đề sớm sẽ làm giảm downtime từ đó tăng tính sẵn sàng của hệ thống đi lên. Ví dụ những ứng dụng doanh nghiệp (enterprise system) có khả năng tự phát hiện lỗi sau đó cô lập và báo cáo nhanh cho người vận hành hệ thống.
Phần 2: Cân bằng tải (Load balancing)
Cân bằng tải (load balancing — LB) là một thành phần nữa cũng cực kỳ quan trọng trong bất kỳ một distributed system nào, nó giúp cho hệ thống có thể phân tải các request tới đều các Server (Application hoặc Database), giúp cho hệ thống có tính availability hơn. LB sẽ liên tục kiểm tra (health checks) những trạng thái của các server trong hệ thống, nếu trạng thái của server là khỏe mạnh (healthy) thì nó sẽ gửi quest tới, còn nếu server không available hoặc không có response trả về (not healthy) thì LB sẽ không gửi request tới server đó nữa (đá ra khỏi LB), và cho tới khi server đó healthy trở lại thì LB sẽ thêm lại server đó vào LB.
Có rất nhiều thuật toán để LB điều phối các request tới các resource của mình như thuật toán đơn giản nhất là thuật toán “Round Robin”. Là thuật toán luân chuyển vòng, các máy chủ sẽ được xem ngang hàng và sắp xếp theo một vòng quay, các truy vấn dịch vụ sẽ lần lượt được gửi tới các máy chủ theo thứ tự sắp xếp. Ngoài ra còn rất nhiều thuật toán phức tạp khác nữa, tuy nhiên trên thực tế chưa có một thuật toán LB nào thực sự hoàn hảo, nó chỉ một phần nào tương đối chấp nhận được thôi. Nếu có hứng thú tìm hiểu về các thuật toán của LB thì đây cũng là một câu chuyện dài và thú vị đấy các bạn 😜.
Bằng cách cân bằng tải các request tới ứng dụng trên nhiều server, LB sẽ giảm tải cho từng server và ngăn bất kỳ một máy chủ server nào trở thành một điểm lỗi duy nhất (Single point of failure) , do đó cải thiện khả năng đáp ứng và tính sẵn sàng của ứng dụng. Nhất là khi bạn scaling hệ thống theo chiều ngang (horizontal scaling) như đã đề cập ở phần Scalability.
Để hệ thống có tính high availability, thì ta có thể áp dụng LB nhiều lớp (layer) của hệ thống như LB cho nhiều DB, LB cho Web-Server hay LB cho Application Server, ví dụ như mô hình dưới.
Những lợi ích của LB:
- Trải nghiệm người của người dùng sẽ tốt hơn, và không bị gián đoạn bởi bất kỳ một sự cố nào xảy ra trên một hoặc vài server, ngoài ra cũng tăng tốc độ truy vấn của người dùng nhờ năng lực của nhiều server hợp lại.
- LB giúp cho quản trị viên rất dễ scale up hoặc scale down hệ thống mà không làm gián đoạn trải nghiệm của người dùng.
- Một LB thông minh sẽ giúp người quản trị viên xác định được những tắc nghẽn hay quá tải (traffic bottlenecks) trước khi nó xảy ra, để người quản trị có thể hành động kịp thời, như scale up hệ thống.
- LB giúp người quản trị viên (devops/system admin) đỡ căng thẳng hơn vì thay vì chỉ một Server duy nhất thực hiện nhiều công việc thì ta sẽ chia nhiều công việc nhỏ hơn cho nhiều server, từ đó việc cả hệ thống fail sẽ ít xảy ra hơn.
Tuy nhiên LB có thể trở thành một single point of failure, nếu nó bị quá tải hay lỗi phần cứng/mềm khiến LB sụp đổ dẫn đến cả hệ thống sụp đổ theo. Để khắc phục điều này tốt nhất hay có ít nhất 2 LB kết hợp với nhau thành một cụm LB chạy dưới dạng active-standby
tức là luôn có một LB backup cho một LB còn lại đang chạy để phục vụ cho trường hợp LB chính bị hỏng thì LB còn lại sẽ đảm nhiệm.
Phần 3: Bộ đệm — Caching:
Load balancing giúp hệ thống mở rộng theo chiều ngang bằng cách ngày càng tăng số lượng các máy chủ, nhưng Caching lại là cách để sử dụng tài nguyên (resource) hiệu quả hơn từ đó resource cần cung cấp giảm đi, nhằm tiết kiệm resource và giảm chi phí của hệ thống.
Để làm được điều này, hệ thống sẽ cung cấp một vùng nhớ đệm (cache) để chứa những dữ liệu được request nhiều lần mà không thường xuyên thay đổi vào đó, từ đó hệ thống sẽ lấy dữ liệu từ bộ nhớ cache mà không truy cập vào bộ nhớ chính. Điều này giúp bộ nhớ chính sẽ được giảm tải đi từ đó năng lực của nó tăng lên.
Bộ nhớ cache được sử dụng rất rộng rãi trong mọi thành phần của hệ thống như trong phần cứng (hardware) như RAM/CPU Cache, hệ điều hành (operating systems), trình duyệt web (web browsers), web applications v.v… Bộ nhớ cache giống như một bộ nhớ ngắn hạn (short- term memory) nó có giới hạn về kích cỡ, nhưng thường nhanh hơn rất nhiều lần bộ nhớ chính
Về kiến trúc phần mềm thì Cache có thể tồn tại ở hầu hết các thành phần, nhưng chủ yếu nó tồn tại ở phần thao tác với dữ liệu như Database Layer, giúp trả về dữ liệu nhanh hơn mà không cần tốn chi phí để truy cập vào Database, hay ở phần Front-end để Cache những resource tĩnh như js, css, image file bằng cách dùng CDN.
Bộ đệm tầng Backend(Application Server Cache):
Ở tầng backend Cache được áp dụng như một bản sao hoặc một phần của DB/Storage, mỗi lần ứng dụng request dữ liệu từ DB/Storage nó sẽ tìm trong bộ nhớ Cache, nếu có ứng sẽ trả về dữ liệu tức thì mà không tốn tài nguyên để query xuống DB/Storage. Tuy nhiên nó chỉ có tác dụng với những dữ liệu được truy cập nhiều lần nhưng ít được thay đổi, ví dụ thông tin cá nhân của người dùng (tên, quê quán/quốc tịch, ngày tháng năm sinh, địa chỉ, số điện thoại…).
Nếu ta có nhu cầu gia tăng khả năng chịu tải bộ nhớ Cache theo mô hình horizontal scaling, khi đó ta sẽ có thêm nhiều node và mỗi node sẽ chứa các dữ liệu của riêng nó. Tuy nhiên điều này sẽ dẫn đến số lượng “cache miss” tăng cao vì LB sẽ tùy chọn ngẫu nhiên 1 node mỗi request từ đó một request sẽ tới nhiều “cache node” khác nhau. Và để giải quyết điều này ta có hai sự lựa chọn là sử dụng duy nhất một nhớ Cache lớn, hoặc phân chia dữ liệu cache trên cụm server với Consistent Hashing
Bộ đệm tầng Frontend — Content Distribution Network (CDN)
CDN được sử dụng để caching những resource tĩnh trên các website như js, css, images… (static media) mạng lưới CDN sẽ được đặt ở nhiều vùng địa lý khác nhau và dựa theo địa chỉ của từng request mà các static media sẽ được trả về từ trên các máy chủ CDN ở gần nó nhất, nếu không có trên tất cả các máy chủ CDN thì nó sẽ lấy dữ liệu đó từ Back-end Server để trả về và đồng thời cached dữ liệu vừa lấy lên CDN. Nhờ vậy ta có thể tăng tốc khả năng chịu tải cũng như tốc độ của website.
Nếu hệ thống của bạn không đủ lớn để xây dựng một CDN riêng của mình thì bạn cũng có thể sử dụng các dịch vụ CDN sẵn có như như Cloudflare, BootstrapCDN hay Amazon CloudFront v.v…
Cache Invalidation
Về bản chất Cache là một bản sao của bộ nhớ chính có thể là DB hay các dạng Storage khác, ví dụ dữ liệu từ DB được update, thì cache phải được update theo (invalidated cache).Cho nên việc đồng bộ dữ liệu giữa bộ nhớ chính và Cache để giải quyết tính consistency của hệ thống là một vấn đề phải giải quyết.
Ta có 3 chiến thuật để giải quyết vấn đề về cache invalidation là:
- Write-through cache
Là chiến thuật khi có dữ liệu cần thay đổi, hệ thống sẽ ghi dữ liệu cả và Cache hoặc DB đồng thời. Việc này sẽ đảm bảo được việc dữ liệu sẽ luôn luôn consistency bởi vì process sẽ không thành công và phải thực hiện lại nếu một trong hai thao tác ghi vào Cache hoặc ghi vào DB bị lỗi. Tuy nhiên điểm yếu của chiến thuật này là độ trễ cao trong việc ghi dữ liệu, vì đòi hỏi cả hai thao tác ghi từ DB và Cache phải được hoàn thành. - Write-around cache
Chiến thuật này khi có dữ liệu cần thay đổi, sẽ chỉ ghi vào DB mà bỏ qua Cache. Điều này sẽ có thể giảm việc Cache bị tràn ngập các thao tác ghi mà rất có thể các dữ liệu này sẽ không bao giờ được sử dụng. Tuy nhiên việc này sẽ gây ra trường hợp “cache miss” nếu client gửi một request được từ một bản ghi mới được ghi vào, nó sẽ gây độ trễ khi đọc vì lúc này dữ liệu phải đọc từ DB sau đó mới được ghi ngược lại vào Cache và trả về cho Client. Và chiến thuật này chỉ phù hợp với những dữ liệu ít thay đổi và hệ thống chấp nhận độ trễ của dữ liệu, vì dữ liệu có thể không consistency trong lúc Cache chưa hết hạn (expired) mà có dữ liệu mới đã được update. - Write-back cache
Là khi có dữ liệu cần thay đổi, sẽ chỉ ghi vào bộ nhớ Cache mà bỏ qua DB, dữ liệu chỉ ghi vào DB sau một khoảng thời gian (interval) hoặc với điều kiện nào đó. Điều này sẽ có lợi ích tạo ra cho hệ thống độ trễ thấp (low latency) và lưu lượng cao (high throughput) với những hệ thống chuyên ghi (write-intensive). Tuy nhiên chiến thuật này có độ rủi ro cao về mất mát dữ liệu, bởi vì trước khi dữ liệu được ghi vào DB thì dữ liệu của chúng ta chỉ nằm ở trong Cache, bất kỳ một sự cố nào về Cache đều có thể gây mất mát dữ liệu.
Cache eviction policies
Ngoài ra ta cũng có các cách chiến thuật xóa dữ liệu không còn cần thiết từ Cache.
1. First In First Out (FIFO): Cách dữ liệu được ghi vào Cache trước thì sẽ được ưu tiên xóa đi trước mà không quan tâm tới tần suất hay số lượng truy cập của nó.
2. Last In First Out (LIFO): Sẽ ưu tiên xóa những dữ liệu được ghi cũ nhất mà không quan tâm tới tần suất hay số lượng truy cập của nó.
3. Least Recently Used (LRU): Sẽ xóa những dữ liệu ít được truy cập gần đây trước tiên.
4. Most Recently Used (MRU): Sẽ xóa những dữ liệu được truy cập thường xuyên gần đây nhất.
5. Least Frequently Used (LFU): Sẽ xóa những dữ liệu ít được truy cập nhất.
6. RR: Sẽ trọn ra một dữ liệu bất kỳ trên Cache để thực hiện thao tác xóa.
Thông thường thì hệ thống sẽ đánh dấu thời điểm hết hạn (expire time) của dữ liệu, nếu quá thời hạn thì Cache sẽ bị xóa đi. Tính toán expire time của Cache cũng là một bài toán khá đau đầu tùy vào logic của hệ thống đang phát triển.
Phần 4: Phân chia dữ liệu — Sharding/Data Partitioning
Phân chia dữ liệu (Sharding) là một giải pháp chia nhỏ một Database lớn thành nhiều Database nhỏ, ta có thể phân tách từng bảng hoặc cả một DB ra nhiều phần nhỏ đặt ở nhiều máy chủ (server) khác nhau. Điều này sẽ giúp cho hệ thống DB của chúng ta đạt được các tính chất khả năng bảo trì (manageability), hiệu xuất (performance), tính sẵn sàng (availability), và cân bằng tải (load balancing) của ứng dụng. Và giải pháp này cũng giảm chi phí cũng như tính mở rộng (scalability) để scale up DB bằng cách dùng nhiều server nhỏ gộp lại hơn là nâng cấp một server lớn.
Những cách thức phân chia dữ liệu
Ta có 3 cách thức Sharding dữ liệu như sau:
- Horizontal sharding
Là cách chia cùng dữ liệu của cùng một bảng (table) ra nhiều DB khác nhau. Ví dụ ta có bảng dữ liệu thông tin về người dùng, ta sẽ dựa trên location của người dùng để quyết định nó nằm ở DB nào, ví dụ người dùng ở Sài Gòn thì sẽ chứ ở DB_SG, thông tin người dùng ở Biên Hòa sẽ nằm ở DB_BH hay thông tin người dùng ở Vĩnh Long sẽ nằm ở DB_VL.
Giải pháp này có một vấn đề là ta phải chọn nơi dữ liệu Sharding rất cẩn thận để không gây mất cân bằng (unbalanced) giữa các DB dẫn tới rất có thể có một vài Server sẽ thành điểm nóng (hot spot), ví dụ người dùng ở Sài Gòn chắc chắn là đông hơn rất nhiều lần người dùng ở Biên Hòa hay Vĩnh Long. - Vertical sharing
Là cách sharding dữ liệu dựa trên tính năng (feature) của hệ thống. Ví dụ ta thiết kế một hệ thống chia sẻ ảnh giống Instagram, ta sẽ lưu thông tin của User vào DB_Users, lưu thông tin ảnh họ up lên trên một DB khác là DB_Photos và thông tin danh sách những người họ follow ở một DB thứ 3 là DB_Follow.
Cách làm này rất là rõ ràng dễ để implement và không làm ảnh hưởng lớn đến ứng dụng, nhưng khi hệ thống lớn dần lên thì dữ liệu cũng lớn dần theo, do đó ta lại phải thực hiện sharding tiếp những DB trên từng feature (bởi vì 1 DB không thể sử lý 10 tỷ bức ảnh của 140 triệu user được). - Directory Based sharding
Cách này sẽ yêu cầu ta phải thiết kế một “lookup service” có tác dụng quyết định ánh xạ (mapping) dữ liệu sẽ nằm ở đâu, DB nào. Mỗi khi có request ghi hoặc đọc sẽ thông qua lookup service để mapping vị trí sẽ đọc và ghi. Khi business mở rộng số lượng server có thể tăng lên mà không ảnh hưởng hay đòi hỏi ứng dụng phải thay đổi theo.
Những tiêu chí để phân vùng dữ liệu
Bên trên ta đã tìm hiểu và các method để sharding dữ liệu, giờ ta hãy tìm kiểu sâu hơn về các tiêu chí để phân vùng dữ liệu.
- Phân vùng theo key hoặc hash
Hệ thống sẽ áp dụng các hàm băm (hash function) cho một hoặc nhiều các key chính trong dữ liệu (thường là ID) để xác định ra một con số của phân vùng nó đang nằm. Ví dụ: nếu hệ thống có 100 máy chủ DB và ID của bản ghi sẽ tự tăng lên mỗi lần một bản ghi mới được chèn. Trong ví dụ này, hàm băm có thể là ‘ID% 100, từ đó ta có thể xác định vị trí của dữ liệu. Cách tiếp cận này cần đảm bảo phân bổ dữ liệu thống nhất giữa các máy chủ. Nhưng cách làm này có một điểm yếu là mỗi khi ta thay đổi số lượng Server tăng hoặc giảm thì hàm băm cũng sẽ phải thay đổi theo và sẽ xuất hiện hiện tượng xáo trộn tập dữ liệu trên mỗi server, và đòi hỏi ta phải phân phối lại dữ liệu và xuất hiện độ trễ dữ liệu. Có một cách giải quyết vấn đề này là sử dụng Consistent Hashing (hàm băm nhất quán). - Phân vùng theo danh sách (list)
Cách phân vùng sẽ được quyết định gán một danh sách các giá trị ngay từ đầu, từ đó mỗi lần ghi một bản ghi mới ta sẽ tìm ra giá trị của bản ghi nằm ở phân vùng nào và ghi vào đó. Ví dụ ta sẽ quyết định nhóm tất cả các người dùng ở Ai-len, Na Uy, Thụy Điển, Phần Lan và Đan Mạch và một phân vùng có tên là Nordic (Bắc Âu). - Phân vùng vòng tròn (round-robin)
Cách phân vùng này rất đơn giản là mỗi lần có thao tác ghi ta sẽ ghi vòng tròn quanh các phân vùng có sẵn. Cách làm này đơn giản nhưng rất khó để xác định dữ liệu nào ở đâu lấy ra khi cần. - Phân vùng tổng hợp
Là cách tổng hợp các giải pháp trên thành một giải pháp mới. Ví dụ ta có thể áp dụng phân vùng theo Key/Hash và sau đó xác định được key rồi ta sẽ áp dụng tiếp phân vùng theo danh sách để chứa key sau khi hash vào một danh sách cụ thể nào đó. Consistent Hashing có thể được coi là cách phân vùng tổng hợp.
Các vấn đề khi Sharding dữ liệu
Vì việc dữ liệu sẽ bị phân tán đi nhiều Server khác nhau do vậy sẽ phát sinh một vài vấn đề khi sharding dữ liệu như sau:
- Joins and De-normalization
Bởi vì việc dữ liệu ở các bảng được phân bố và trải rộng đi nhiều DB/Server khác nhau nên việc join bảng dữ liệu là điều rất khó khăn và cũng không đem lại hiệu xuất bởi vì việc dữ liệu phải được queries từ nhiều máy chủ khác nhau. Để giải quyết vấn đề này ta có thể thiết kế dữ liệu dạng non-relationship DB hay còn gọi là NoSQL, giống như MongoDB hay Cassandra hai hệ NoSQL rất nổi tiếng và hỗ trợ Sharding vô cùng tốt. Tuy nhiên việc này ta phải chấp nhận rủi ro việc không nhất quán dữ liệu (inconsistency) - Referential integrity
Cũng vì lý do trên về việc truy vấn chéo dữ liệu giữa các DB nằm trên các máy chủ khác nhau là bất khả thi, do vậy việc ràng buộc khóa ngoại để bảo đảm sự toàn vẹn dữ liệu cũng là một điều vô cùng khó khăn. Hầu hết RDBMS không hỗ trợ các ràng buộc khóa ngoại trên các cơ sở dữ liệu phân tán trền nhiều server. Do vậy để đạt được điều này ta phải thực hiện điều này trên mã ứng dụng (code), điều này sẽ tăng tính phức tạp của ứng dụng. - Rebalancing
Trong suốt quá trình hệ thống vận hành có rất nhiều lý do ta thay đổi các chiến thuật hay cách thức Sharding dữ liệu, như business tăng trưởng yêu cầu thêm Server… Và mỗi khi như vậy ta phải tái cân bằng (rebalancing) dữ liệu trên tất cả các Server, có nghĩa là dữ liệu phải được phân phối lại trên toàn bộ server. Để làm được điều này mà không có độ trễ (downtime) là cực kỳ khó khăn. Mô hình sharding “Directory Based” có khả năng rebalancing tốt nhất nhưng lại tăng độ phức tạp của ứng dụng và tạo thêm một single point of failure (ví dụ “lookup service”).
Tóm lại với Sharding thì mô hình sharding với key/hash với Consistent Hashing hiện tại là giải pháp tối ưu nhất cho việc Rebalancing.
Phần 5: Indexes
Có lẽ thuật ngữ “đánh index” đã quá quen với những ai làm việc với CSDL, đó là cách rất phổ biến để tăng tốc độ query của dữ liệu, khi dữ liệu Database ngày càng tăng và trở nên chậm dần đều theo thời gian. Mục tiêu của việc tạo Index là để tăng tốc độ trả về dữ liệu của một hoặc nhiều trường (rows) trên một bảng (table) cụ thể nào đó bằng cách tạo Index trên một hoặc nhiều cột (columns) của một database table.
Để hiểu rõ hơn thế nào là Indexes ta hãy đến thử một nhà sách hay thư viện, thường các cuốn sách sẽ được phân chia theo các danh mục về nội dung như: sách nấu ăn, sách tiểu thuyết nước ngoài, sách tâm lý, sách lịch sử … Nếu ta muốn tìm kiếm một loại sách theo nội dung mong muốn thì chỉ việc tới cá kệ sách với nội dung tương ứng, nó sẽ nhanh hơn là tìm kiếm từ toàn bộ cả nhà sách từ. Hoặc ví dụ khác về các phần mục lục trong muốn cuốn sách, nếu ta muốn tìm nhanh đến “chương hồi” ta đang cần tìm kiếm hoặc đọc dở chỉ cần tra mục lục rồi tìm tới đúng trang chứa nội dung.
Index trong Database cũng giống như vậy, ví dụ ta có một table là Books chứa 4 columns là “book_title”, “writer”, “subject”, và “date_of_publication”, thường thì khách hàng sẽ thường xuyên tìm kiếm sách theo hai tiêu chí là tên sách và tác giả, do đó ta sẽ tạo Index cho hai column là “book_title” và “writer”. Database sẽ tạo ra một data structure riêng biệt chứa hai giá trị của toàn bộ nội dung (content) các cột đánh index và một con trỏ (pointer) để trỏ tới dữ liệu thật sự đang nằm ở Database. Như vậy, sử dụng index yêu cầu cần disk space để chứa cấu trúc của nó và Index cũng không làm thay đổi cấu trúc của table. Do vậy mỗi làm tìm kiếm dữ liệu thì Database sẽ tìm kiếm ở Index sau đó dựa vào con trỏ của Index để trả về dữ liệu thật.
Nhưng tại sao tìm kiếm trên Index lại nhanh hơn tìm kiếm trên Database, bởi vì Index luôn luôn sắp xếp dữ liệu để tối ưu nhất cho các thuật toán thực hiện việc tìm kiếm, còn dữ liệu Database thật thì luôn sắp xếp lộn xộn không có thứ tự nên không thuận tiện cho việc tìm kiếm. Mỗi Database sẽ có cách sắp xếp Index và thuật toán tìm kiếm Index khác nhau.
Tuy nhiên Index cũng không phải là một magic keyword, việc đánh Index cần thật cẩn trọng,
- Thứ 1: Việc tạo Index sẽ tốn disk space, do đó chỉ nên đánh những cột dữ liệu có dung lượng nhỏ, và sẽ không có ý nghĩa gì nếu đánh Index cột contents kiểu CLOB
chứa nội dung của một article vì lúc đó dữ liệu của Index sẽ to bằng nguyên cái table gốc.
- Thứ 2: Index thì ta cũng phải cần tạo ra nó, với một dữ liệu lớn sẵn rồi mà lúc này ta mới đánh Index thì việc tạo ra nó là một công việc rất tốn thời gian và tài nguyên hệ thống. Cho nên tốt nhất hãy lường trước ta tạo Index ngay từ khi dữ liệu còn nhỏ.
- Thứ 3: Việc dữ liệu được thêm mới sửa xóa (CUD) thường xuyên trên Table gốc thì Index cũng sẽ phải thêm mới sửa và sắp xếp lại, với một Table có dữ liệu lớn thì việc này cũng rất mất thời gian và nó sẽ làm chậm đi quá trình update hay create dữ liệu từ table gốc.
Do đó chỉ những Index thực sự cần thiết mới nên thêm vào và nên thường xuyên xem xét lại và xóa những Index không thực sự cần thiết. Và mục tiêu chính của Index đó là tăng khả năng đọc (read) của dữ liệu, do đó những Table dạng thường xuyên ghi nhưng ít khi được đọc thì tốt nhất không nên tạo Index, vì nó sẽ giảm hiệu xuất của việc ghi dữ liệu.
Phần 6: Proxies
Proxy server là một server trung gian (intermediary) nằm giữa client và back-end server. Client kết nối với proxy server để yêu cầu (request) những dịch vụ (service) cần thiết như API, web page, file hay connection, v.V… Nói tóm lại, proxy server là một phần của hệ thống phần mềm hoặc phần cứng đóng vai trò trung gian kết nối các yêu cầu từ client tới các thành phần khác trong hệ thống.
Proxy được sử dụng chủ yếu để lọc hoặc ghi log các yêu cầu (filter request, log request) hoặc để thay đổi các request (như thêm hoặc xóa các header, mã hóa/giải mã hay nén các resource). Hoặc một ứng dụng rất hữu ích khác là dùng để cache response rồi trả về cho nhiều request cùng loại, như trong trường hợp nhiều client request tới cùng một resource thì Proxy có thể cache resource đó rồi trả về cho các client mà không cần gọi tới các “remote server” khác.
Các kiểu Proxy Server
- Open Proxy
Một Open Proxy Server là một máy chủ mà bất kỳ người dùng Internet nào cũng có thể truy cập. Thông thường nó thường được sử dụng để kiểm xoát băng thông hoặc lưu và chuyển tiếp các dịch vụ mạng như DNS hoặc các dịch vụ Web của một nhóm người dùng sử dụng chung Open Proxy đó. Có hai loại Open Proxy là
+ Proxy ẩn danh (Anonymous Proxy): Proxy server này có thể che dấu các IP thật của các user sử dụng nó khỏi các dịch vụ nó truy cập đến. Proxy ẩn danh có tác dụng tăng độ bảo mật cho người dùng vì địa chỉ IP thực của người dùng có thể được sử dụng để truy dẫn ra thông tin về người dùng và để thâm nhập vào máy tính của người đó. Hoặc dùng để vượt qua tường lửa của các tổ chức hoặc quốc gia Ngoài ra, proxy ẩn danh còn được dùng để lách qua sự kiểm duyệt Internet của các chính phủ hoặc tổ chức.
+ Proxy minh bạch (Trаnspаrent Proxy): Trái ngược với anonymous Proxy, thì proxy này cho phép các dịch vụ truy cập tới biết được IP thật của user bằng các HTTP header (ví dụ như X-Forwarded-For). Ưu điểm của Proxy này là khả năng cache response như website để tăng tốc độ kết nối mạng của những người dùng bên trong máy chủ Proxy. - Reverse Proxy
Reverse proxy là một proxy server mà khi đứng trước client, có tác dụng chuyển tiếp request đến một hoặc nhiều back-end server và nhận kết quả từ các server này rồi trả về phía client (forwarder), giúp che dấu danh tính của các back-end server bên dưới. Reverse proxy được cài đặt trong một private network của một hoặc nhiều server, và tất cả lưu lượng truy cập đều phải đi qua proxy này.
Thông thường, những server sẽ sử dụng cơ chế reverse proxy này để bảo vệ các ứng dụng có khả năng xử lý HTTP yếu kém hơn. Ví dụ như khả năng xử lý cực lớn các request, những hạn chế về xử lý sự đa dạng của các loại request (các dạng request có thể kể đến như: HTTP(S) 1.x, HTTP(S) 2.x, …) hay khả năng chuyển đổi HTTPS thành HTTP, cache request, xử lý dữ liệu của cookies/session, chia một request thành nhiều request nhỏ hơn rồi tổng hợp lại các response, …
Phần 7: Sao lưu và đồng bộ dữ liệu — Redundancy and Replication
Redundancy
Là phương thức sao lưu các dữ liệu quan trọng của hệ thống với mục đích tăng độ tin cậy (Reliability) của hệ thống. Ví dụ dữ liệu được lưu trên duy nhất một Server, nếu Server đó mất có nghĩa là tất cả dữ liệu đó mất, do đó việc tạo ra các bản sao lưu là để giải quyết vấn đề này. Redundancy là một điều kiện để xóa bỏ những “single points of failure” và tạo ra các bản sao lưu lúc cần thiết, ví dụ ta có hai máy chủ Database đang chạy trên production và dữ liệu luôn được sao lưu đồng bộ giữa hai máy chủ, và nếu có một trong hai máy chủ bị down thì ta có thể failover sang máy chủ còn lại. Redundancy nó giống như duplicate lại node/server dùng cho mục đích back-up vậy.
Replication
Gần tương tự với với Redundancy là ta cũng tạo ra một bản sao lưu dữ liệu của Server chính (Master) sang Server backup (Slave), nhưng khác nhau cơ bản là Replication sẽ update dữ liệu giữa Master và Slave là luôn luôn giống nhau và mọi thời điểm (real time synchronization) để tăng độ tin cậy (reliability), đặc biệt là khả năng chịu lỗi của hệ thống (fault-tolerance) và khả năng truy cập (accessibility). Replication được sử dụng rộng rãi ở các CSDLQH (RDBMS) trong mô hình master/slave, dữ liệu giữa master-slave luôn luôn được đồng bộ, trong trường hợp master down thì slave sẽ lên thay master sau khi master vừa down sống dậy nó sẽ trở thành slave mà sẽ thực hiện đồng bộ dữ liệu chưa được update trong thời gian nó bị down.
Phần 8: SQL vs. NoSQL
Trong thế giới của CSDL hiện tại ta có hai loại giải pháp chính đó là: SQL và NoSQL (cơ sở dữ liệu quan hệ và cơ sở dữ liệu phi quan hệ). Cả hai đều rất khác nhau về cách chúng được xây dựng, loại dữ liệu nó chúng lưu trữ và cả cách thức lưu trữ dữ liệu của chúng.
Cơ sở dữ liệu quan hệ lưu trữ những dữ liệu có cấu trúc rõ ràng ví dụ như danh bạ điện thoại thì sẽ chắc chắn sẽ chứa tên người và số điện thoại của người đó. Còn cơ sở dữ liệu phi quan hệ lưu trữ giữ liệu không có cấu trúc, cấu trúc của nó rất động và phân tán, ví dụ như chứa một tệp tài liệu (document) về người dùng như số điện thoại, địa chỉ, hay những activity của họ trên Facebook hay các trang mua sắm trực tuyến. Dữ liệu của nó thường dưới dạng key-value dạng JSON, và những document có thể được tạo mà không cần phải xác định trước cấu trúc.
SQL
CSDLQH chứa dữ liệu theo dạng dòng (rows) và cột(columns). Mỗi dòng sẽ chứa những thông tin của dữ liệu và mỗi cột sẽ chứa những đặc điểm của dữ liệu, giữa các bảng có thể có liên kết với nhau qua khóa ngoại (foreign key). Một số CSDLQH phổ biến là MySQL, Oracle, MS SQL Server, SQLite, Postgres, MariaDB và IMB D2.
NoSQL
NoSQL được phân thành 4 loại chính sau:
Key-Value Stores:
Dữ liệu sẽ được lưu dưới dạng cặp key-value, với ‘key’ là thuộc tính duy nhất để liên kết với giá trị (value) của nó. Các Key-Value stores nổi tiếng bao gồm Redis hay Amazon DynamoDB.
Document Databases:
Là DB chứa dữ liệu dạng theo kiểu các tệp tài liệu (document) để thay thế cho kiểu dòng và cột trong table và nhiều document gộp lại sẽ thành một Collection. Khác với kiểu table là mỗi document sẽ chưa các dạng cấu trúc khác nhau. Ví dụ trong MongoDB thì mỗi document sẽ chứa dữ liệu kiểu JSON, tức là mỗi document là một JSON. Document DB tiêu biểu hiện tại có MongoDB và CouchDB.
Wide-Column Databases:
Wide-Column hay còn gọi là Big-Table là mô hình dữ liệu để lưu trữ dữ liệu với khả năng mỗi rows chứa rất nhiều column(cột) mỗi cột lại là một cặp key-value, và ở đây số lượng cột là tùy biến (dynamic) cho mỗi rows, có nghĩa là số cột ở mỗi rows là khác nhau. Có thể thấy nó giống như kiểu key-value hai chiều với mỗi key lại chứa nhiều cặp key-value bên trong. Các Wide-Column DB tiêu biểu bao gồm Cassandra hay HBase.
Graph Databases
DB dạng này sẽ lưu dữ liệu dạng cấu trúc dữ liệu kiểu đồ thị (graph) để biểu thị mối quan hệ giữa các dữ liệu với nhau. Dữ liệu được lưu vào các nodes (entities) và thuộc tính của node, liên kết giữa các nodes sẽ qua các lines. Graph DB phổ biến có Neo4J và InfiniteGraph.
Sự khác nhau giữa SQL và NoSQL
Lưu trữ — Storage:
- SQL lưu trữ dữ liệu trong các bảng (tables) trong đó mỗi row đại diện cho một thực thể dữ liệu (entity), và mỗi column sẽ chứa các thuộc tính của entity; ví dụ nếu ta chứa dữ liệu của thực thể xe hơi (car entity) ta sẽ lưu trữ vào một bảng dữ liệu có nhiều column đại diện cho đặc điểm của chiếc xe đó như “màu” “nhãn hiệu” “hãng sản xuất” …
- NoSQL lưu trữ dữ liệu trên rất nhiều loại cấu trúc dữ liệu khác nhau như đã bàn bên trên như dạng key-value, document hay graph.
Lược đồ dữ liệu của thực thể — Schema:
- Trong SQL thì mỗi bản ghi có schema là cố định, tức là mỗi column trong table phải được định nghĩa từ trước khi tạo bảng, và mỗi khi dữ liệu được thêm vào một row thì các column của nó phải có giá trị (chấp nhận giá trị NULL). Các schema có thể được thay đổi sau đó (alter table), nhưng sự thay đổi này phải nằm ở phía Database và khi DB thực hiện thay đổi này nó sẽ offline tạm thời.
- Trong NoSQL, schema là động (dynamic) có nghĩa là ta không cần phải định nghĩa một schema nào trước mà schema sẽ được dựa vào cấu trúc của records được đưa vào.
Truy vấn — Querying:
- SQL truy vấn thông qua một ngôn ngữ truy vấn mang tính cấu trúc được gọi là Structured Query Language (SQL) thao tác với dữ liệu. SQL là một ngôn ngữ rất mạnh mẽ và lâu đời là đại diện duy nhất cho toàn bộ các RDBMS khác nhau.
- NoSQL truy vấn của nó thường được gọi là UnSQL (Unstructured Query Language) mỗi dạng NoSQL khác nhau sẽ có cấu trúc cú pháp (syntax) khác nhau để có thể lấy ra các dạng dữ liệu khác nhau (collection, key-value, node …)
Khả năng mở rộng — Scalability:
- Trong hầu hết các RDBMS thì khả năng mở rộng là theo chiều dọc (vertically scalable) ví dụ như tăng sức mạnh phần cứng của máy chủ hiện tại (tăng CPU, Ram…), tất nhiên rằng cách làm này rất đắt đỏ cũng như tốn thời gian mà đặc biệt là nó luôn có giới hạn nào đó.
- Còn với NoSQL DB thì hỗ trợ rất tốt cho mở rộng theo chiều ngang (horizontally scalable), có nghĩa là đơn giản chỉ thêm server vào các nodes hiện có, cách làm này đơn giản cũng như rẻ tiền hơn. Phần lớn các NoSQL DB công việc này đã được hỗ trợ sẵn và được làm tự động, có nghĩa là rất dễ để triển khai.
Độ tin cậy — Reliability:
- Hầu hết các RDBMS đều tuân thủ theo các thuộc tính của ACID (atomicity, consistency, isolation, và durability) vì vậy nó rất đảm bảo độ tin cậy và bảo toàn dữ liệu.
- Còn với NoSQL thì đa phần sẽ hy sinh tính chất ACID để đánh đổi với performance (hiệu năng) và khả năng mở rộng (scalability).
Khi nào thì chúng ta sử dụng SQL hay NoSQL
SQL hay NoSQL đều có những tính chất khác nhau phục vụ cho những nhu cầu đặc biệt khác nhau, do vậy không có DB nào thực sự là hoàn hảo phù hợp cho mọi nhu cầu tùy vào mục đích của hệ thống sở tại. Ngay cả khi hiện tại NoSQL đang trở nên phổ biến rộng rãi về tốc độ và khả năng mở rộng, nhưng vẫn có tình huống mà một SQL DB sẽ hoạt động tốt hơn (ví dụ hệ thống về giao dịch tiền tệ cần đảm bảo về tính ACID)
Lý do sử dụng SQL DB
1. Ứng dụng yêu cầu sử lý dữ liệu theo các transaction để tuân thủ tính ACID để bảo toàn tính nhất quán và toàn vẹn của dữ liệu bằng cách quy định các thuộc tính ACID bắt buộc khi thực hiện một transaction, ví dụ như các hệ thống về tài chính hay thương mại điện tử.
2. Nếu ứng dụng của hệ thống được xác định trước cấu trúc dữ liệu, và nghiệp vụ không đòi hỏi sự mở rộng trong tương lai và đòi hỏi sự nhất quán (consistent) trong dữ liệu.
Lý do sử dụng NoSQL DB
1. Hệ thống đòi hỏi cần lưu một số lượng cực lớn dữ liệu không có cấu trúc rõ ràng ngay từ đầu và tăng dần theo thời gian. Lúc này NoSQL là sự lựa chọn tốt nhất vì tính không rằng buộc trong kiểu dữ liệu (data type) bằng cách lưu trữ dữ liệu dạng document và tính chất mở rộng theo chiều ngang (horizontally scalable).
2. Các hệ thống cần phát triển nhanh (rapid development). NoSQL rất phù hợp với rapid development bởi vì nó không cần mất thời gian chuẩn bị (cấu trúc DB, cấu trúc bảng cột…). Nếu chúng ta cần làm việc với hệ thống nghiệp vụ chưa thực sự rõ ràng ngay từ đầu và đòi hỏi sự linh hoạt thay đổi “data type” giữa các sprint mà không ảnh hưởng cũng như đòi hỏi sự thay đổi hay độ trễ (downtime) tới hệ thống giữa các version, lúc này cơ sở dữ liệu quan hệ (RDBMS) sẽ làm chậm chúng ta lại.
3. Hệ thống được xây dựng và lưu trữ dữ liệu trên Cloud Computing. Lưu trữ dữ liệu trên Cloud là một giải pháp tiết kiệm chi phí tuyệt vời nhưng đòi hỏi dữ liệu phải lưu trữ đồng thời trên nhiều máy chủ để mở rộng quy mô (scale up). NoSQL như Cassandra được thiết kế tối ưu cho việc này mà không cần ta phải động tay động chân quá nhiều.
(*) Bonus: Bên trên tôi nhắc khá nhiều về transaction và ACID, vậy hãy đi qua một chút thế nào là một transaction và ACID?
Một transaction là một quá trình xử lý từ khi bắt đầu tới khi kết thúc thỏa mãn bốn tính chất ACID. Bốn chữ đó viết tắt của bốn tính chất quan trọng sau:
Atomicity: tính “nguyên tử” của giao dịch. Nghĩa là mọi giao dịch chỉ thành công khi tất cả các phần thành công All or Nothings.
Consistency: tính nhất quán. Nghĩa là mọi dữ liệu được thao tác đều nhất quán với tất cả các quy tắc (rules), các ràng buộc (constraint)… trong toàn bộ quá trình xử lý từ khi bắt đầu tới khi kết thúc.
Isolation: tính cô lập. Nó đảm bảo việc thực thi đồng thời của các giao dịch chỉ có thể có kết quả khi các giao dịch được thực hiện tuần tự. Ví dụ: hai giao dịch cùng sửa một table, thì các giao dịch đó phải được thực hiện tuần tự, giao dịch này xong mới tới giao dịch kia.
Durability: tính bền. Nghĩa là mọi giao dịch khi commit thì kết quả nó phải được đảm bảo, cho dù ứng dụng bị tắt, mất điện server.
Phần 9: Định lý CAP / CAP Theorem
Định lý CAP nói rằng một hệ thống phân tán (distributed system) không thể thỏa mãn cả ba yếu tố CAP đó là:
- Consistency: tính nhất quán, tất cả các node phải có dữ liệu đồng nhất với nhau.
- Availability: tính sẵn sàng hoạt động của các node. Hệ thống có thể vẫn hoạt động được khi một số node bị chết hoặc không sẵn sàng.
- Partition Fault Tolerance: trạng thái hoạt động của hệ thống khi đường kết nối (mạng) giữa các node bị đứt, hay còn gọi là khả năng chịu lỗi của hệ thống. Hệ thống vẫn phải hoạt động bình thường cho dù các kết nối của các node trong hệ thống bị đứt gãy.
Chúng ta không thể thiết kế một hệ thống bao gồm cả ba tính CAP, bởi vì đảm bảo tính C (consistency) tất cả các cập nhật dữ liệu phải được thực hiện trên các node cùng một thời điểm. Nhưng nếu đường kết nối (mạng) giữa các node không được đảm bảo dẫn đến việc các node sẽ không được update dữ liệu cùng một thời điểm, điều này dẫn tới việc một vài node dữ liệu sẽ bị out-of-date do chưa được cập nhật dữ liệu từ đó vi phạm tính C (consistency). Và để đảm bảo đối phó với điều này ta sẽ ngừng phục vụ nhữg node bị out-of-date đó cho tới khi nó được update dữ liệu đầy đủ, nhưng việc này lại vi phạm tính A (availability) của hệ thống.
Thông thường, người ta thường đánh đổi yếu tố C để lấy hai yếu tố A và P. Khi đó họ sẽ thay thế Consistency thành eventually consistency (tính nhất quán có độ trễ), làm như thế hệ thống sẽ có hiệu năng tốt hơn
Phần 10: Consistent Hashing
Như đã nhắc đến rất nhiều lần bên trên thì Distributed Hash Table (DHT — Bảng băm phân tán) là một thành phần cơ bản trong những distributed scalable systems (hệ thống phân tán có khả năng mở rộng). Một Hash Table cần một cặp key-value
, và một hàm “hash” để map key
với vị trí mà value
của nó được lưu trữ.
index = hash_function(key)
Giả sử ta có thiết kế một distributed caching system (Redis cache chẳng hạn). Chúng ta có “n” cache servers, thì hàm băm (hash function) để map key với vị trí của Cach server nó làm ở đâu sẽ là key % n
. Nó rất đơn giản và dễ sử dụng, tuy nhiên nó có hai nhược điểm chính là:
- Nó rất khó horizontally scalable (phân tán theo chiều ngang). Bởi vì mỗi lần một cache server mới được thêm vào LB, mọi mapping trước khia giữa key với value nó được lưu trữ ở đâu sẽ sai toét hết. Hiện tượng cache miss sẽ sẩy ra tràn lan và rất mất thời gian và khó khăn để cập nhật tất cả các mapping key-value, với một hệ thống lớn với số lượng caching data khổng lồ đây là một điểm ác mộng với những người quản lý dữ liệu.
- Nó có thể không đáp ứng được việc cân bằng tải (Load Balanced), bởi vì ta không thể chắc chắn rằng việc hash và mapping như vậy những data được lưu trữ ở các server khác nhau có độ truy cập có cân bằng nhau không? Rất có thể những data ít được truy cập có thể tập chung vào một vài server và vài server còn lại lại chứa những “hot key”, dẫn tới tình trang một vài server thì hoạt động hết công xuất, một vài server thì lại quá nhàn rỗi ngồi chơi không 😙.
Với những vấn đề như vậy thì Consistent Hashing là một giải pháp rất tốt để cải tiến việc chia server với các caching system.
Consistent Hashing giải quyết vấn đề này như thế nào ?
Consistent Hashing là một chiến thuật hiệu quả cho việc phân chia distributed caching systems và DHT. Nó cho phép việc thêm hay xóa các node trên một cụm server (cluster) mà ít gây ra sự xáo trộn dữ liệu, do đó nó các hệ thống caching system sẽ dễ dàng scale-up hay scale down.
Trong Consistent Hashing, khi bảng băm (hash table) thay đổi kích thước (ví dụ thêm một node và cluster), chỉ có “k/n” keys cần re-map với “k” là tổng số keys có trong hệ thống, và “n” là tổng số server. Có nghĩa là khi thay đổi kích thước của cluster thì có duy nhất keys trên một server cần phải re-map. Và khi một node bị xóa khỏi cluster, thì các data của node đó sẽ được di chuyển và chia sẻ với các node khác, và cũng tương tự khi một node được thêm vào cluster thì nó sẽ lấy tự động dữ liệu từ một vài node khác để chia sẻ tài nguyên.
Consistent Hashing hoạt động như thế nào?
Vậy làm thế nào để Consistent Hashing làm được những điều trên thì các Node trên cluster sẽ được lưu trữ dưới dạng vòng tròn bằng cách sử dụng hash function trong Consisten Hashing sẽ hash các key thành một một số nguyên (Integer) nằm trong một khoảng nào đó và các số nguyên đó sẽ được đặt trên một vòng tròn sao cho mỗi điểm trên vòng tròn sẽ tương ứng với một số nguyên trên dãy số nguyên đó.
Tóm lại các bước để Consistent Hashing băm dữ liệu như sau:
- Tiến hành hash các danh sách node trong cluster server thành một số nguyên trong một khoảng số được định nghĩa trước. Khoảng số này tùy vào người thiết kế hệ thống cân nhắc dựa trên số lượng Server tối đa mà hệ thống sẵn sàng scale up.
- Sau khi đã có danh sách mapping giữa các node, ta sẽ tiến hành mapping
key
của data tới các node bằng cách.
- Hash giá trị của key thành một số nguyên (integer).
- Di chuyển nó liên tục trong vòng tròn số nguyên trong khoảng đã tạo ở trên theo chiều kim đồng hồ cho tới khi giá trị integer đó bằng với giá trị hash key của một Node đầu tiên nó gặp. Tiến hành ghi dữ liệu của dữ liệu đó vào Node vừa gặp.
Do đó khi tiến hành thêm một Node vào Cluster Server thì dữ liệu Node gần nó nhất theo chiều kim đồng hồ sẽ được chia sẻ với Node mới thêm vào.
Tương tự như việc xóa một Node trên Cluster, khi cache miss sẩy ra, dữ liệu sẽ được di chuyển, lưu và lấy từ Node kế cận nhất với Node đã xóa theo chiều kim đồng hồ.
Như vậy, Consistent Hashing đã giải quyết được vấn đề xáo trộn data cache khi scale hệ thống theo chiều ngang, đảm bảo sự xáo trộn cache chỉ xảy ra với một server.
Vấn đề về phân phối đều data trên từng Node.
Bên trên chúng ta cũng đã đề cập về vấn đề Load Balancing, với dữ liệu thật thì khả năng nhiều data key đều được hash vào một dãy giá trị lưu trên một Node nào đó, khiến Node đó trở thành điểm nóng (hot spot) so với các server còn lại. Dẫn đến việc lệch cân bằng tải và rủi ro nếu node hot spot gặp sự cố, gần như toàn bộ cache sẽ bị mất, dẫn tới cache miss hàng loạt.
Để giải quyết vấn đề này, chúng ta sẽ thực hiện hash và thêm nhiều các Node ảo (virtual node) vào vòng tròn. Và thay vì việc chỉ mapping mỗi Node vào một điểm trên vòng tròn, mỗi node ảo tượng trưng cho một dãy giá trị mà một Node thật đảm nhiệm. Có nghĩa là mỗi Node thật sẽ lưu trữ dữ liệu được ánh xạ trên nhiều Virtual Node. Điều này sẽ làm cho việc phân phối key sẽ cân bằng hơn, nó giống với việc tạo ra nhiều Node con hơn cho hash function trong Consistent Hashing băm “nhuyễn” hơn 😃
Ví dụ ta có 3 Node trên Cluster, ta sẽ tạo thêm 3 virtual Node tương ứng, lúc này vòng tròn sẽ chia thành 6 điểm như trên hình vẽ. Mỗi Virtual Node sẽ ánh xạ tới các Node thật, như trong hình là Virutal Node mầu đỏ 🔴 sẽ ảnh xạ tới Real Node mầu đỏ 🔴, Virutal Node mầu xanh nước biển 🔵 sẽ ánh xạ tới Real Node mầu xanh nước biển 🔵 , tương tự với Virtual Nod mầu xanh nhạt. Do vậy với Node 🔴 nó sẽ chứa dữ liệu của khoảng 1 và 4 Node 🔵 sẽ chứa dữ liệu của 2 và 5, tương tự Node còn lại sẽ chứa dữ liệu của 0 và 6.
Consistent Hashing được ứng dụng rất rộng rãi nổi bật nhất là DynamoDB và Cassandra đã ứng dụng nó vào việc replicate dữ liệu giữa các Server Node của nó.
Phần 11: Long-Polling vs WebSockets vs Server-Sent Events
Long-polling, WebSockets hay Server-Sent là những cách giao tiếp phổ biến giữa Client và Server ví dụ những giữa Web-Browser và Web-Server. Đầu tiên ta hãy đi tìm hiểu về HTTP và các bước của nó trên môi trường Web như thế nào.
- Client sẽ mở ra một kết nối (connection) à yêu cầu (request) dữ liệu từ Server
- Server sẽ nhận yêu cầu và tính toán kết quả trả về
- Server sẽ trả về (response) kết quả cho Client vừa mở connection đó
Ajax Polling:
Polling là một kỹ thuật được sử dụng trong các ứng dụng AJAX, ý tưởng của nó là client sẽ liên tục gọi tới server để yêu cầu dữ liệu mới (polls/requests data). Client sẽ tạo ra một request và đợi kết quả trả về từ Server, nếu Server không tìm thấy hoặc trả kết quả về hoặc kết quả là rỗng (empty), thì một empty response sẽ được gửi về.
- Client sẽ mở một connection và request dữ liệu từ Server thông qua cổng kết nối HTTP.
- Những request trên sẽ được gửi đến Server theo định kỳ (intervals), ví dụ mỗi 1 giây sẽ có một request gửi đi.
- Server sẽ tính toán và trả về dữ liệu cũng thông qua kết nối HTTP.
- Client sẽ lặp lại cả 3 bước trên để liên tục cập nhật được dữ liệu mới nhất từ Server.
Vấn đề của Ajax Polling là Client phải liên tục gửi request tới Server, do đó sẽ tạo ra rất nhiều request với không có response nào trả về, gây lãng phí HTTP traffic của hệ thống.
HTTP Long-Polling
Đây là một biến thể của HTTP Polling truyền thống bằng cách cho phép Server chủ động đẩy (push) thông tin tới Client khi có dữ liệu mới, bằng cách Client sẽ gửi request tới Server mà không cần Server phải trả dữ liệu về ngay lập tức mà sẽ làm theo logic như bên dưới:
- Nếu Server không có dữ liệu mới, thay vì không trả về hay trả về dữ liệu rỗng, vì Server sẽ giữ request đó và đợi cho tới khi có dữ liệu mới về.
- Khi dữ liệu đã sẵn sàng thì Server sẽ gửi trả (response) về cho Client. Ngay lúc đó Client lại tiếp tục gửi một request tới Server, vì thế ở phía Server sẽ luôn luôn có một request mà nó có thể sử dụng để luôn cập nhật dữ liệu về phía Client.
Và một vòng đời của một HTTP Long-Polling sẽ như sau:
- Client tạo một HTTP request về phía Server và chờ đợi tới khi có response trả về.
- Server sẽ chờ cho tới khi có dữ liệu trả về hoặc quá thời gian time-out.
- Khi có dữ liệu Server sẽ trả về cho Client (HTTP respone)
- Client sau khi nhận được dữ liệu trả về hoặc quá time-out sẽ gửi tiếp một HTTP Long-Polling đến Server. Lúc này sẽ có một khoảng thời gian trễ giữa lúc client nhận và gửi request mới, nhưng thời gian trễ này tạm chấp nhậnd được.
- Mỗi HTTP Long-Polling sẽ có một khoảng time-out, nếu quá thời time-out connection sẽ đóng lại và mở lại một connection mới.
Web-socket
Websocket là giao thức chuẩn cho trao đổi dữ liệu hai chiều giữa client và server hay còn gọi là kênh Full Duplex. Giao thức WebSocket không chạy trên HTTP, thay vào đó nó thực hiện trên giao thức TCP.
Nó cung cấp một phương thức liên tục giữa Client và Server mà cả hai bên có thể gửi dữ liệu cho nhau bất kỳ lúc nào. Client kết nối với Server thông qua Websocket bằng một cú bắt tay (WebSocket handshake), nếu nó thành công thì dữ liệu có thể được trao đổi từ hai hướng bất kỳ lúc nào.
Người ta thường dùng Websocket thay vì HTTP cho những trường hợp yêu cầu real time (thời gian thực) bởi vì gói tin của WebSockets nhẹ hơn HTTP rất nhiều, giảm độ trễ của network và không cần phải gửi request liên tiếp như HTTP.
Điều này được hiện thực bằng cung cấp môt chuẩn hóa truyền tin giữa Server và client thông qua cổng ws://
hoặc wss://
có thêm bảo mật. Và dữ liệu truyền đi chấp nhận kiểu String và Binary Type ( large objects (blobs), ArrayBuffers)
Server-Sent Events (SSEs)
SSEs cũng gần giống với Long-Polling nhưng khác là connection sẽ được lưu trữ (persistent) sử dụng cho một thời gian dài (long-terms) mà không có time-out và chỉ có Server sẽ sử dụng connection này để gửi dữ liệu về cho Client, và client chỉ có request (GET) dữ liệu mà không được phép gửi dữ liệu (POST) lên Server.
Vòng đời của của SSEs
- Client yêu cầu lữ liệu từ Server sử dụg kết nối HTTP thông thường.
- Connection giữa client và server sẽ được mở và duy trì.
- Bất kỳ khi nào máy chủ sẽ gửi dữ liệu tới Client bất cứ khi nào có thông tin mới.
SSEs cũng giống WebSocket là thích hợp với các ứng dụng thời gian thực (real time) nhưng khác là SSEs chỉ là một chiều (Half duplex) giữa Server tới Client.
~ Hết ~
Nguồn tham khảo: https://www.educative.io/courses/grokking-the-system-design-interview