ЁЯПл The SchoolтА║ЁЯУИ ScalingтА║ЁЯЧВя╕П рдзрдбрд╛ 11 тАФ Partitioning + DynamoDB: рдкрд╕рд░рдгрд╛рд▒реНрдпрд╛ key рдиреЗ рдиреЛрдВрджрд╡рд╣реАрдЪреА рд╡рд┐рднрд╛рдЧрдгреА рдХрд░рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЧВя╕П рдзрдбрд╛ 11 тАФ Partitioning + DynamoDB: рдкрд╕рд░рдгрд╛рд▒реНрдпрд╛ key рдиреЗ рдиреЛрдВрджрд╡рд╣реАрдЪреА рд╡рд┐рднрд╛рдЧрдгреА рдХрд░рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 13 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдорд╛рдЧреАрд▓: lesson-10-replicas-and-pools ┬╖ рдкреБрдвреАрд▓: lesson-12-queues-and-blueprint


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ data рдЪреАрдЪ рд╡рд┐рднрд╛рдЧрдгреА: hash partitioning, partition key, рдПрдХрд╛ key рд▓рд╛ рдмрд╣реБрддреЗрдХ traffic рдорд┐рд│рддреЗ рддреЗрд╡реНрд╣рд╛рдЪрд╛ hot partition, рддреЛ рдкрд╕рд░рд╡рдгреНрдпрд╛рд╕рд╛рдареА write-sharding, рдЖрдгрд┐ DynamoDB рдЪреЗ per-partition capacity units (RCU рдЖрдгрд┐ WCU). scale/demo.py рдордзрд▓реЗ shards() 1,000 keys 4 partitions рд╡рд░ hash рдХрд░рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдиреЛрдВрджрд╡рд╣реА рдПрдХрд╛ рдХрд╛рд░рдХреБрдирд╛рд╕рд╛рдареА рдЦреВрдк рдореЛрдареА рдЖрд╣реЗ, рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рддреА 4 рдХрд╛рд░рдХреБрдирд╛рдВрд╕рд╣ 4 рдХрдкреНрдкреНрдпрд╛рдВрдордзреНрдпреЗ ЁЯЧДя╕П рд╡рд┐рднрд╛рдЧрддреЗ. рдХреЛрдгрддрд╛ рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдХреЛрдгрддреНрдпрд╛ рдХрдкреНрдкреНрдпрд╛рдд? рдПрдХ рдард░рд▓реЗрд▓рд╛ рдирд┐рдпрдо: рдПрдХ рдпрдВрддреНрд░ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдЪреНрдпрд╛ рдирд╛рд╡рд╛рдЪреЗ рдПрдХрд╛ рдЖрдХрдбреНрдпрд╛рдд рд░реВрдкрд╛рдВрддрд░ рдХрд░рддреЗ, рдЖрдгрд┐ рддреЛ рдЖрдХрдбрд╛ рдХрдкреНрдкрд╛ рдирд┐рд╡рдбрддреЛ. рдПрдХрд╛рдЪ рдирд╛рд╡рд╛рд▓рд╛ рдиреЗрд╣рдореА рдПрдХрдЪ рдХрдкреНрдкрд╛ рдорд┐рд│рддреЛ. 1,000 рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдЕрд╕рддреАрд▓ рддрд░ рдкреНрд░рддреНрдпреЗрдХ рдХрдкреНрдкреНрдпрд╛рд▓рд╛ рд╕реБрдорд╛рд░реЗ 250. рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рд░рдХреВрди рд╕рд╛рд░рдЦрд╛рдЪ рд╡реНрдпрд╕реНрдд.

рдордЧ рдХреЛрдгреАрддрд░реА рдореНрд╣рдгрддреЗ: "рддреНрдпрд╛рдРрд╡рдЬреА рд╡рд░реНрдЧрд╛рдиреБрд╕рд╛рд░ рд▓рд╛рд╡реВрдпрд╛." рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╢реА 1,000 рдкреИрдХреА 900 рдирд╡реАрди рдиреЛрдВрджреА 3A рд╡рд░реНрдЧрд╛рдЪреНрдпрд╛ рдЕрд╕рддрд╛рдд. 3A рдЪреЗ рд╕рдЧрд│реЗ рдПрдХрд╛рдЪ рдХрдкреНрдкреНрдпрд╛рдд рдЬрд╛рддреЗ. рддреНрдпрд╛ рдХрд╛рд░рдХреБрдирд╛рдХрдбреЗ рдкреНрд░рдЪрдВрдб рдХрд╛рдо, рдЖрдгрд┐ рдмрд╛рдХреАрдЪреЗ рддрд┐рдШреЗ рд░рд┐рдХрд╛рдореЗ. рдпрд╛рд▓рд╛рдЪ hot partition рдореНрд╣рдгрддрд╛рдд.

рдЙрдкрд╛рдп: 3A рдЪреНрдпрд╛ рдиреЛрдВрджреА "3A#0" рддреЗ "3A#39" рдЦрд╛рд▓реА рд▓рд┐рд╣рд╛ тАФ 40 рдЫреЛрдЯреА рд▓реЗрдмрд▓реЗ. рдпрдВрддреНрд░ рддреА рдЪрд╛рд░рд╣реА рдХрдкреНрдкреНрдпрд╛рдВрдд рдкрд╕рд░рд╡рддреЗ. рд╕рдЧрд│рд╛ 3A рд╡рд╛рдЪрд╛рдпрдЪрд╛ рдЕрд╕реЗрд▓ рддрд░ рд╕рдЧрд│реНрдпрд╛ 40 рд▓реЗрдмрд▓рд╛рдВрдирд╛ рд╡рд┐рдЪрд╛рд░рд╛.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    w["тЬПя╕П 1,000 writes"] --> h["#я╕ПтГг hash(partition key) mod 4"]
    h --> p0["ЁЯЧДя╕П drawer 0"]
    h --> p1["ЁЯЧДя╕П drawer 1"]
    h --> p2["ЁЯЧДя╕П drawer 2"]
    h --> p3["ЁЯЧДя╕П drawer 3"]
    good["pupil-N тЖТ 245 ┬╖ 262 ┬╖ 249 ┬╖ 244"]
    hot["class тЖТ 19 ┬╖ 31 ┬╖ 928 ┬╖ 22 ЁЯФе"]
    fix["class-3A#0тАж#39 тЖТ 179 ┬╖ 253 ┬╖ 277 ┬╖ 291"]

ЁЯЧ║я╕П рд░реЗрдЦрд╛рдЯрд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l11

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг replicas рдЖрдгрд┐ caches (рдзрдбреЗ 09тАУ10) рдлрдХреНрдд reads рд▓рд╛ рдорджрдд рдХрд░рддрд╛рдд. рдирд┐рдХрд╛рд▓рд╛рдЪрд╛ рджрд┐рд╡рд╕ рд╕реЗрдХрдВрджрд╛рд▓рд╛ рд╣рдЬрд╛рд░реЛ writes рдЖрдгрддреЛ, рдХрд┐рдВрд╡рд╛ data рдПрдХрд╛ machine рд╕рд╛рдареА рдЦреВрдк рдореЛрдард╛ рд╣реЛрддреЛ, рддреЗрд╡реНрд╣рд╛ рдкреБрдвреЗ рдЬрд╛рдгреНрдпрд╛рдЪрд╛ рдПрдХрдЪ рдорд╛рд░реНрдЧ рдореНрд╣рдгрдЬреЗ рддреНрдпрд╛рдЪреА рд╡рд┐рднрд╛рдЧрдгреА. рд╡рд┐рднрд╛рдЧрдгреА key рдЗрддрдХреАрдЪ рдЪрд╛рдВрдЧрд▓реА рдЕрд╕рддреЗ: рдкрд╕рд░рдгрд╛рд░реА key рдЕрд╕реЗрд▓ рддрд░ N partitions N рдкрдЯ рдХрд╛рдо рдХрд░рддрд╛рдд; рдПрдХрдЪ hot value рдЕрд╕рд▓реЗрд▓реА key рдЕрд╕реЗрд▓ рддрд░ N partitions рдПрдХрд╛рдЪреЗ рдХрд╛рдо рдХрд░рддрд╛рдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

scale/sim.py рдордзрд▓реЗ shard_of(key, shards) рдореНрд╣рдгрдЬреЗ md5(key) mod shards тАФ рдПрдХ рд╕реНрдерд┐рд░ hash, рдореНрд╣рдгрдЬреЗ рдПрдХрдЪ key рдиреЗрд╣рдореА рдПрдХрд╛рдЪ рдард┐рдХрд╛рдгреА рдЬрд╛рддреЗ. spread(keys, shards) рдкреНрд░рддреНрдпреЗрдХ partition рдордзреНрдпреЗ рдХрд┐рддреА keys рдЬрд╛рддрд╛рдд рддреЗ рдореЛрдЬрддреЗ. shards() рдЖрдзреА 1,000 pupil ids рдкрд╕рд░рд╡рддреЗ, рдордЧ class рдиреЗ key рдХреЗрд▓реЗрд▓реЗ 1,000 writes (рддреНрдпрд╛рддрд▓реЗ 900 3A), рдордЧ рддреЗрдЪ writes 3A рдЪреЗ 40 рднрд╛рдЧ рдХрд░реВрди.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 scale/demo.py shards
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import spread, shard_of
keys = [f"pupil-{i}" for i in range(1000)]
for n in (2, 4, 8):
    print(f"{n} partitions тЖТ", spread(keys, n))
print("class-3A always lands on partition", shard_of("class-3A", 4))
hot = ["class-3A"] * 900 + [f"class-{c}" for c in range(100)]
for suffixes in (4, 10, 40):
    fixed = [f"class-3A#{i % suffixes}" for i in range(900)] + [f"class-{c}" for c in range(100)]
    print(f"write-sharding with {suffixes:>2} suffixes тЖТ", spread(fixed, 4))
share = max(spread(hot, 4)) / 1000
print(f"at 2,000 writes/s the hot partition gets ~{2000 * share:.0f} writes/s (limit 1,000 WCU per partition)")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

shards рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА 1,000 pupils hashed across 4 partitions тЖТ [245, 262, 249, 244]
   partition key = class, results day (900 of 1,000 writes are 3A) тЖТ [19, 31, 928, 22] тАФ one HOT partition
   write-sharding the hot key (class-3A#0 тАж #39) тЖТ [179, 253, 277, 291]
   DynamoDB: a partition gives up to 3,000 RCU + 1,000 WCU (1 RCU = one strong 4 KB read/s, 1 WCU = one 1 KB write/s)
   bigger items use more units; adaptive capacity helps a bit тАФ a key that spreads is still the real fix

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

2 partitions тЖТ [494, 506]
4 partitions тЖТ [245, 262, 249, 244]
8 partitions тЖТ [106, 133, 129, 128, 139, 129, 120, 116]
class-3A always lands on partition 2
write-sharding with  4 suffixes тЖТ [244, 31, 253, 472]
write-sharding with 10 suffixes тЖТ [109, 121, 478, 292]
write-sharding with 40 suffixes тЖТ [179, 253, 277, 291]
at 2,000 writes/s the hot partition gets ~1856 writes/s (limit 1,000 WCU per partition)

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЕрдиреЗрдХ values рдЕрд╕рд▓реЗрд▓реА key 2, 4 рдХрд┐рдВрд╡рд╛ 8 partitions рд╡рд░ рд╕рд╛рд░рдЦреА рдкрд╕рд░рддреЗ. Class key 1,000 рдкреИрдХреА 928 writes partition 2 рдордзреНрдпреЗ рдЯрд╛рдХрддреЗ тАФ рд╕реЗрдХрдВрджрд╛рд▓рд╛ 2,000 writes рдЕрд╕рддрд╛рдирд╛ рддреЗ рд╕реЗрдХрдВрджрд╛рд▓рд╛ рд╕реБрдорд╛рд░реЗ 1,856 рд╣реЛрддрд╛рдд, DynamoDB рдЪреНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ partition рдЪреНрдпрд╛ 1,000 WCU рдкреЗрдХреНрд╖рд╛ рдЦреВрдк рдЬрд╛рд╕реНрдд (рдЖрдгрд┐ рд╣реЗ 1 KB рдХрд┐рдВрд╡рд╛ рддреНрдпрд╛рд╣реВрди рд▓рд╣рд╛рди items рд╕рд╛рдареА; рдореЛрдареЗ items рдкрд░рд┐рд╕реНрдерд┐рддреА рдЖрдгрдЦреА рдмрд┐рдШрдбрд╡рддрд╛рдд). рдЖрдгрд┐ рдереЛрдбреЗ suffixes рдкреБрд░реЗрд╕реЗ рдирд╛рд╣реАрдд: 4 рдХрд┐рдВрд╡рд╛ 10 рдЕрд╕рддреАрд▓ рддрд░ hash рдЕрдЬреВрдирд╣реА рджреЛрди рдХрд┐рдВрд╡рд╛ рдЬрд╛рд╕реНрдд suffixes рдПрдХрд╛рдЪ рдХрдкреНрдкреНрдпрд╛рдд рдЯрд╛рдХрддреЛ (472, 478). 40 рдЕрд╕рддреАрд▓ рддрд░ рдкрд╕рд░рдг рдкреБрд░реЗрд╢реА рд╕рд╛рд░рдЦреА рд╣реЛрддреЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ sharded class рдЖрдгрд┐ рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдпрд╛рдВрдиреА key рдХреЗрд▓реЗрд▓реЗ on-demand DynamoDB table:

aws dynamodb create-table --table-name results \
    --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
    --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST

Suffix рд╕рд╣ write рдХрд░рд╛; рд╕рдЧрд│реЗ suffixes рд╡рд╛рдЪреВрди рдПрдХрддреНрд░ рдХрд░рд╛ (Python, boto3):

import random, boto3
from boto3.dynamodb.conditions import Key
table = boto3.resource("dynamodb").Table("results")
SUFFIXES = 40

def save(cls, pupil, grade):
    table.put_item(Item={"pk": f"class-{cls}#{random.randrange(SUFFIXES)}", "sk": f"pupil#{pupil}", "grade": grade})

def class_results(cls):
    items = []
    for n in range(SUFFIXES):                   # one query per suffix тАФ run them in parallel in real code
        items += table.query(KeyConditionExpression=Key("pk").eq(f"class-{cls}#{n}"))["Items"]
    return items

DynamoDB рд╕рд╛рдареА CloudWatch Contributor Insights рдиреЗ hot keys рд╢реЛрдзрд╛:

aws dynamodb update-contributor-insights --table-name results --contributor-insights-action ENABLE

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: key рдирд┐рд╡рдбрдгреНрдпрд╛рдЖрдзреА app рд╡рд┐рдЪрд╛рд░рдд рдЕрд╕рд▓реЗрд▓реЗ рдкрд╛рдЪ рдореБрдЦреНрдп рдкреНрд░рд╢реНрди рд▓рд┐рд╣реВрди рдХрд╛рдврд╛, рдЖрдгрд┐ рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╕рд╛рдЪреНрдпрд╛ data рдиреЗ load test рдЪрд╛рд▓рд╡рд╛ тАФ рдЬрд┐рдереЗ рдПрдХ value рдмрд╛рдХреАрдЪреНрдпрд╛рдВрдкреЗрдХреНрд╖рд╛ рдЦреВрдкрдЪ рд▓реЛрдХрдкреНрд░рд┐рдп рдЕрд╕рддреЗ тАФ рд╕рд╛рд░рдЦреНрдпрд╛ random test data рдиреЗ рдирд╛рд╣реА.

тПня╕П рдкреБрдвреЗ

рдЬреЗ рдХрд╛рдо рдЖрддреНрддрд╛рдЪ рд╡реНрд╣рд╛рдпрдЪреА рдЧрд░рдЬ рдирд╛рд╣реА тАФ рдкреНрд░рдорд╛рдгрдкрддреНрд░реЗ, emails, PDFs тАФ рддреЗ token рд░рд╛рдВрдЧреЗрдд рдЬрд╛рддреЗ. рдЖрдгрд┐ рдордЧ рд╕рдВрдкреВрд░реНрдг рдЬрддреНрд░рд╛, рд╕реНрддрд░рд╛рдиреБрд╕рд╛рд░.

git checkout lesson-12-queues-and-blueprint

ЁЯЧВя╕П Lesson 11 тАФ Partitioning + DynamoDB: split the register by a key that spreads

ЁЯУН You are here: Lesson 11 of 13 ┬╖ Previous: lesson-10-replicas-and-pools ┬╖ Next: lesson-12-queues-and-blueprint


ЁЯУж What's in this branch

Lessons 01тАУ10, plus splitting the data itself: hash partitioning, the partition key, the hot partition when one key gets most of the traffic, write-sharding to spread it, and DynamoDB's per-partition capacity units (RCU and WCU). shards() in scale/demo.py hashes 1,000 keys across 4 partitions.

ЁЯзТ Explain like I'm 5

The register is too big for one clerk, so Dipika splits it into 4 drawers ЁЯЧДя╕П with 4 clerks. Which drawer holds which pupil? A fixed rule: a machine turns each pupil's name into a number, and the number picks the drawer. The same name always gives the same drawer. With 1,000 pupils, each drawer gets about 250. Every clerk is equally busy.

Then someone says: "Let's file by class instead." On results day, 900 of 1,000 new entries are for class 3A. All of 3A goes to one drawer. That clerk has far too much work, and the other three are idle. That is a hot partition.

The fix: write 3A's entries under "3A#0" to "3A#39" тАФ 40 little labels. The machine spreads them over all four drawers. To read all of 3A, you ask all 40 labels.

ЁЯЧ║я╕П Diagram

flowchart LR
    w["тЬПя╕П 1,000 writes"] --> h["#я╕ПтГг hash(partition key) mod 4"]
    h --> p0["ЁЯЧДя╕П drawer 0"]
    h --> p1["ЁЯЧДя╕П drawer 1"]
    h --> p2["ЁЯЧДя╕П drawer 2"]
    h --> p3["ЁЯЧДя╕П drawer 3"]
    good["pupil-N тЖТ 245 ┬╖ 262 ┬╖ 249 ┬╖ 244"]
    hot["class тЖТ 19 ┬╖ 31 ┬╖ 928 ┬╖ 22 ЁЯФе"]
    fix["class-3A#0тАж#39 тЖТ 179 ┬╖ 253 ┬╖ 277 ┬╖ 291"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l11

тЭУ What

ЁЯдФ Why

Because replicas and caches (lessons 09тАУ10) only help reads. When results day brings thousands of writes a second, or the data is too big for one machine, the only way forward is to split it. The split is only as good as the key: a key that spreads makes N partitions do N times the work; a key with one hot value makes N partitions do the work of one.

ЁЯФз How (in this repo)

shard_of(key, shards) in scale/sim.py is md5(key) mod shards тАФ a stable hash, so the same key always lands in the same place. spread(keys, shards) counts how many keys land in each partition. shards() spreads 1,000 pupil ids, then 1,000 writes keyed by class (900 of them 3A), then the same writes with 3A split 40 ways.

ЁЯзк Try it

python3 scale/demo.py shards
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import spread, shard_of
keys = [f"pupil-{i}" for i in range(1000)]
for n in (2, 4, 8):
    print(f"{n} partitions тЖТ", spread(keys, n))
print("class-3A always lands on partition", shard_of("class-3A", 4))
hot = ["class-3A"] * 900 + [f"class-{c}" for c in range(100)]
for suffixes in (4, 10, 40):
    fixed = [f"class-3A#{i % suffixes}" for i in range(900)] + [f"class-{c}" for c in range(100)]
    print(f"write-sharding with {suffixes:>2} suffixes тЖТ", spread(fixed, 4))
share = max(spread(hot, 4)) / 1000
print(f"at 2,000 writes/s the hot partition gets ~{2000 * share:.0f} writes/s (limit 1,000 WCU per partition)")
EOF

тЬЕ Verify тАФ what you should see

shards prints:

тФАтФА 1,000 pupils hashed across 4 partitions тЖТ [245, 262, 249, 244]
   partition key = class, results day (900 of 1,000 writes are 3A) тЖТ [19, 31, 928, 22] тАФ one HOT partition
   write-sharding the hot key (class-3A#0 тАж #39) тЖТ [179, 253, 277, 291]
   DynamoDB: a partition gives up to 3,000 RCU + 1,000 WCU (1 RCU = one strong 4 KB read/s, 1 WCU = one 1 KB write/s)
   bigger items use more units; adaptive capacity helps a bit тАФ a key that spreads is still the real fix

Your snippet prints:

2 partitions тЖТ [494, 506]
4 partitions тЖТ [245, 262, 249, 244]
8 partitions тЖТ [106, 133, 129, 128, 139, 129, 120, 116]
class-3A always lands on partition 2
write-sharding with  4 suffixes тЖТ [244, 31, 253, 472]
write-sharding with 10 suffixes тЖТ [109, 121, 478, 292]
write-sharding with 40 suffixes тЖТ [179, 253, 277, 291]
at 2,000 writes/s the hot partition gets ~1856 writes/s (limit 1,000 WCU per partition)

ЁЯПБ What you just proved

A key with many values spreads evenly at 2, 4 or 8 partitions. A class key puts 928 of 1,000 writes in partition 2 тАФ at 2,000 writes a second that is about 1,856 per second, far over DynamoDB's 1,000 WCU per partition (and that is for items of 1 KB or less; bigger items make it worse). And a few suffixes are not enough: with 4 or 10, the hash still puts two or more suffixes in the same drawer (472, 478). With 40 the spread is even enough.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ an on-demand DynamoDB table keyed by a sharded class and the pupil:

aws dynamodb create-table --table-name results \
    --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
    --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST

Write with a suffix; read all suffixes and merge (Python, boto3):

import random, boto3
from boto3.dynamodb.conditions import Key
table = boto3.resource("dynamodb").Table("results")
SUFFIXES = 40

def save(cls, pupil, grade):
    table.put_item(Item={"pk": f"class-{cls}#{random.randrange(SUFFIXES)}", "sk": f"pupil#{pupil}", "grade": grade})

def class_results(cls):
    items = []
    for n in range(SUFFIXES):                   # one query per suffix тАФ run them in parallel in real code
        items += table.query(KeyConditionExpression=Key("pk").eq(f"class-{cls}#{n}"))["Items"]
    return items

Find hot keys with CloudWatch Contributor Insights for DynamoDB:

aws dynamodb update-contributor-insights --table-name results --contributor-insights-action ENABLE

ЁЯПн Why this matters in production: write down the top five questions the app asks before you choose a key, and run a load test with results-day data тАФ where one value is far more popular than the rest тАФ not with evenly random test data.

тПня╕П Next

Work that does not need to happen now тАФ certificates, emails, PDFs тАФ goes into a token queue. And then the whole fair, tier by tier.

git checkout lesson-12-queues-and-blueprint
тЖР Previousreplicas and poolsNext тЖТqueues and blueprint

This page is the lesson's README from the lesson-11-partitioning branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.