Data partition
Increasing the data, concurrent read / write traffic to the data puts scalability pressure on databases, which in turn increases the latency and impacts throughput.
At some point a single node isn’t enough to handle the load.
The goal is to have all nice properties as range queries, secondary indices, and transactions with the ACID properties while we distribute data over many nodes, it’s challenging to provide single-node like properties over a distributed database.
One solution is to use NoSQL database, however the historical codebases are usually built around relational db and migrating from relational is difficult problem.
Data partitioning enables us to use multiple nodes where each node manages some part of the data.
Sharding
Section titled “Sharding”Sharding is the approach in which we split a large dataset into smaller chunks stored in different nodes across the network.
Goal is to distribute the data in an evenly across nodes so that each nodes will get evenly distributed queries.
It’s of two types:
- Vertical sharding
- Horizontal sharding
Vertical sharding
Section titled “Vertical sharding”In Vertical sharding we split the table into multiple tables, and place them into multiple individual servers.
Often, if the table itself very large, eg. it contains contains columns with very wide texts or blob (binary large object). In this case such columns could be split into different table.
Eg. Let’s say we have an Employee table, which contains following columns - EmployeeID, Name, Picture.
We can split Employee table into two tables
-
Employeetable -EmployeeID,Name -
EmployeePicturetable -EmployeeID,Picture
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam package { borderColor #555555 backgroundColor #EEEEEE fontColor #333333 fontSize 14}
skinparam database { borderColor #007bff backgroundColor #cce5ff fontColor #004085}
skinparam cloud { borderColor #28a745 backgroundColor #d4edda fontColor #155724}
skinparam node { borderColor #6c757d backgroundColor #e2e3e5 fontColor #383d41}
skinparam component { borderColor #17a2b8 backgroundColor #d1ecf1 fontColor #0c5460}
skinparam actor { borderColor #dc3545 backgroundColor #f8d7da fontColor #721c24}
skinparam object { borderColor #17a2b8 backgroundColor #d1ecf1 fontColor #0c5460}
skinparam participant { borderColor #6c757d backgroundColor #e2e3e5 fontColor #383d41}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}left to right direction
package "Original Employee Table" { object "Employee" as original_e { +EmployeeID +Name +Picture }}
package "Vertical Sharding" { object "Employee" as new_e { +EmployeeID +Name } object "EmployeePicture" as ep { +EmployeeID +Picture }}
original_e --> new_eoriginal_e --> ep
@endumlHorizontal sharding
Section titled “Horizontal sharding”Some tables in databases becomes too big, that it impacts the read/write latencies.
Horizontal sharding / partitioning, partitions a table into multiple tables by splitting the data row-wise.
For example, a Users table can be split into multiple smaller tables (shards), with each shard containing a subset of the users.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam package { borderColor #555555 backgroundColor #EEEEEE fontColor #333333 fontSize 14}
skinparam object { borderColor #17a2b8 backgroundColor #d1ecf1 fontColor #0c5460}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}
title Horizontal Sharding: Row-wise Splitting
package "Original Users Table" { object "Users" as original_users { UserID Name Email -- 1, 'Alice', 'alice@example.com' 2, 'Bob', 'bob@example.com' 3, 'Charlie', 'charlie@example.com' 4, 'David', 'david@example.com' }}
package "Horizontally Sharded Users Tables" { object "Users_Shard1 (Range 1-2)" as shard1 { UserID Name Email -- 1, 'Alice', 'alice@example.com' 2, 'Bob', 'bob@example.com' }
object "Users_Shard2 (Range 3-4)" as shard2 { UserID Name Email -- 3, 'Charlie', 'charlie@example.com' 4, 'David', 'david@example.com' }}
original_users --> shard1original_users --> shard2
@endumlIt’s of two types
- Key-range based sharding
- Hash based sharding
Key-range based sharding
Section titled “Key-range based sharding”In this case each database node is assigned a range of keys (partition key), and based on these keys the data in a table is split into multiple tables.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam package { borderColor #555555 backgroundColor #EEEEEE fontColor #333333 fontSize 14}
skinparam database { borderColor #007bff backgroundColor #cce5ff fontColor #004085}
skinparam object { borderColor #17a2b8 backgroundColor #d1ecf1 fontColor #0c5460}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}
title Key-Range Based Sharding: Single Table
package "Original Orders Table" { object "Orders" as original { OrderID (PK) CustomerID OrderDate Amount -- 1001, 'C1', '2024-01-15', $50 1002, 'C2', '2024-01-16', $75 1003, 'C3', '2024-01-17', $100 1004, 'C4', '2024-01-18', $60 }}
package "Sharded by OrderID Range" { database "Shard 1\n(OrderID: 1000-1999)" as shard1 { object "Orders" as s1_orders { 1001, 'C1', '2024-01-15', $50 1002, 'C2', '2024-01-16', $75 } }
database "Shard 2\n(OrderID: 2000-2999)" as shard2 { object "Orders" as s2_orders { 2001, 'C3', '2024-01-17', $100 2002, 'C4', '2024-01-18', $60 } }}
original --> shard1original --> shard2
@endumlsometimes, there multiple tables which are bound by foreign key relationships, in such cases all the data in other tables which is related to the partition key are also stored in same shard.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam package { borderColor #555555 backgroundColor #EEEEEE fontColor #333333 fontSize 14}
skinparam database { borderColor #007bff backgroundColor #cce5ff fontColor #004085}
skinparam object { borderColor #17a2b8 backgroundColor #d1ecf1 fontColor #0c5460}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}
title Key-Range Based Sharding: Related Tables with Foreign Keys
package "Original Tables" { object "Orders" as orig_orders { OrderID (PK) CustomerID OrderDate -- 1001, 'C1', '2024-01-15' 2001, 'C2', '2024-01-16' }
object "OrderItems" as orig_items { ItemID (PK) OrderID (FK) ProductID Quantity -- 1, 1001, 'P1', 2 2, 1001, 'P2', 1 3, 2001, 'P3', 5 }}
package "Sharded by OrderID Range" { database "Shard 1\n(OrderID: 1000-1999)" as shard1 { object "Orders" as s1_orders { 1001, 'C1', '2024-01-15' } object "OrderItems" as s1_items { 1, 1001, 'P1', 2 2, 1001, 'P2', 1 } s1_orders .. s1_items : FK relationship\nstored together }
database "Shard 2\n(OrderID: 2000-2999)" as shard2 { object "Orders" as s2_orders { 2001, 'C2', '2024-01-16' } object "OrderItems" as s2_items { 3, 2001, 'P3', 5 } s2_orders .. s2_items : FK relationship\nstored together }}
orig_orders --> s1_ordersorig_orders --> s2_ordersorig_items --> s1_itemsorig_items --> s2_items
note right of shard1 All data related to OrderID 1001 (from both Orders and OrderItems) is stored in the same shardend note
@endumlHere’s a visual representation of the architecture:
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam package { borderColor #555555 backgroundColor #EEEEEE fontColor #333333 fontSize 14}
skinparam database { borderColor #007bff backgroundColor #cce5ff fontColor #004085}
skinparam cloud { borderColor #28a745 backgroundColor #d4edda fontColor #155724}
skinparam node { borderColor #6c757d backgroundColor #e2e3e5 fontColor #383d41}
skinparam component { borderColor #17a2b8 backgroundColor #d1ecf1 fontColor #0c5460}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}
title Key Range Based Sharding Architecture
cloud "Client Applications" as clients
node "Router / Query Layer" as router { component "Shard Key Logic" as logic}
package "Shards" { database "Shard 1\n(Range: 1-1000)" as shard1 database "Shard 2\n(Range: 1001-2000)" as shard2 database "Shard 3\n(Range: 2001-3000)" as shard3}
clients -> routerrouter -> shard1router -> shard2router -> shard3
note right of logic Determines which shard to route to based on the shard key's value.end note
@endumlWrite Operation
Section titled “Write Operation”When a client wants to write data, the router first determines the correct shard based on the shard key and then sends the write request to that shard.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam database { borderColor #007bff backgroundColor #cce5ff fontColor #004085}
skinparam participant { borderColor #6c757d backgroundColor #e2e3e5 fontColor #383d41}
skinparam actor { borderColor #dc3545 backgroundColor #f8d7da fontColor #721c24}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}
title Write Operation with Key Range Based Sharding
actor Clientparticipant "Router" as routerdatabase "Shard 1 (Users 1-1000)" as shard1database "Shard 2 (Users 1001-2000)" as shard2
Client -> router: Write User (ID: 1500, Name: 'Alice')activate router
router -> router: Analyze shard key (ID: 1500)router -> shard2: Store User Dataactivate shard2
shard2 --> router: Successdeactivate shard2
router --> Client: Write Acknowledgeddeactivate router@endumlRead Operation
Section titled “Read Operation”Similarly, for a read operation, the router identifies the correct shard to fetch the data from.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml!theme vibrantskinparam backgroundColor #FFFFFFskinparam shadowing trueskinparam roundcorner 10skinparam padding 5
skinparam database { borderColor #007bff backgroundColor #cce5ff fontColor #004085}
skinparam participant { borderColor #6c757d backgroundColor #e2e3e5 fontColor #383d41}
skinparam actor { borderColor #dc3545 backgroundColor #f8d7da fontColor #721c24}
skinparam arrow { color #333333}
skinparam title { fontColor #333333 fontSize 20}
title Read Operation with Key Range Based Sharding
actor Clientparticipant "Router" as routerdatabase "Shard 1 (Users 1-1000)" as shard1database "Shard 2 (Users 1001-2000)" as shard2
Client -> router: Read User (ID: 800)activate router
router -> router: Analyze shard key (ID: 800)router -> shard1: Fetch User Dataactivate shard1
shard1 --> router: User Data ('Bob')deactivate shard1
router --> Client: User Data ('Bob')deactivate router
@endumlHash-based sharding
Section titled “Hash-based sharding”Here a hash function is used to identify which shard a key (partition key) will belong to.
Idea here is to use the hash function to generate a hash value of a key and take the modulo of it with total number of shards to get the shard number.
Consistent hashing
Section titled “Consistent hashing”Consistent hashing assigns each server hash in an abstract circle, irrespective of number of servers in the table.
To determine which server a key is stored on, we move clockwise from the key’s position on the ring and pick the first server we encounter.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml
title Consistent Hashing - Initial State
circle "Node A" as NA #cce5ffcircle "Node B" as NB #cce5ffcircle "Node C" as NC #cce5ff
NA -[hidden]right-> NBNB -[hidden]right-> NCNC -[hidden]right-> NA
NA -down-> NC : clockwiseNB -down-> NA : clockwiseNC -down-> NB : clockwise
rectangle "Key 1" as K1 #d4eddarectangle "Key 2" as K2 #d4eddarectangle "Key 3" as K3 #d4eddarectangle "Key 4" as K4 #d4edda
K1 --> NBK2 --> NCK3 --> NAK4 --> NA
note as N Keys are stored on the next clockwise node on the ring.end note@endumlThe main advantage of consistent hashing is that it minimizes the number of keys that need to be remapped when a server is added or removed.
For example, if we add a new server, Node D, only the keys that fall between the new node and the next node clockwise need to be moved.
Error generating PlantUML diagram: connect ECONNREFUSED 127.0.0.1:8080
@startuml
title Consistent Hashing - After Adding Node D
circle "Node A" as NA #cce5ffcircle "Node B" as NB #cce5ffcircle "Node C" as NC #cce5ffcircle "Node D" as ND #fff3cd
NA -[hidden]right-> NBNB -[hidden]right-> NCNC -[hidden]right-> NDND -[hidden]right-> NA
NA -down-> NB : clockwiseNB -down-> NC : clockwiseNC -down-> ND : clockwiseND -down-> NA : clockwise
rectangle "Key 1" as K1 #d4eddarectangle "Key 2" as K2 #d4eddarectangle "Key 3" as K3 #ffccccrectangle "Key 4" as K4 #d4edda
K1 --> NBK2 --> NCK3 --> ND : remappedK4 --> NA
note as N When Node D is added, only Key 3 is remapped from Node A to Node D. Other keys are not affected.end note@endumlThis reduces the amount of data that needs to be moved between servers, making scaling easier.
Avoid hash mod n
Section titled “Avoid hash mod n”Partitioning and secondary indexes
Section titled “Partitioning and secondary indexes”key-value data model partitioning in which the records are retrieved with primary keys. But what if we have to access data with secondary indexes.
Secondary indexes are the records that aren’t identified by primary keys but are just a way of searching for some value.
We can partition with secondary indexes in the following ways.