Animated Architecture Patterns

Amazon DynamoDB

database แบบ key-value และ document ที่เป็น serverless: partition key กระจาย item ไปตาม partition เพื่อให้อ่านได้ในระดับ millisecond หลักเดียว

หรือที่เรียกกันว่า DynamoDB, DDB

API Gateway POST /orders orders-api Lambda function Amazon S3 export → Athena other views stats · search order-views Lambda function Amazon DynamoDB table Orders Region us-east-1 · 3 AZs partition key customerId · sort key sk = orderDate#orderId capacity: on-demand request router PutItem · hash(partition key) picks the partition PutItem · hash("C-17") → P2 Query · customerId = "C-17" AND begins_with(sk, "2026-10") Scan · reads every item in every partition, 1 MB per call PutItem · hash("GUEST") → P3 · each partition: max 1,000 writes/s PutItem · hash("GUEST#0" … "GUEST#9") → P1, P2, P3 PutItem · hash("C-17") → P2 · sk 2026-10-06#o-982 Query index byOrderId · hash("o-982") → G1 P1 C-05 2026-07-30#o-611 2026-09-12#o-889 C-42 2026-10-01#o-958 AZ aAZ bAZ c P2 C-17 2026-09-28#o-950 2026-10-02#o-967 AZ aAZ bAZ c P3 C-29 2026-09-03#o-842 GUEST 2026-10-05#o-976 2026-10-05#o-977 AZ aAZ bAZ c 2026-10-06#o-981 2026-10-06#o-982 hot key throttled under the limit GSI byOrderId key orderId updated async G1 G2 o-611 C-05 o-950 C-17 o-981 C-17 not here yet o-982 C-17 DynamoDB Streams changes, kept 24 h 200 OK Query → 2 items 200 OK byOrderId → 0 items serverless no servers to patch no connection pool billed per request PutItem → P2, in sort order Query → one key range Scan → every item flash sale 2,400 guest orders/s, all saved under customerId GUEST fix the key GUEST#0…9, or one id per guest limits item ≤ 400 KB Query needs the key: no joins, no ad hoc ≤ 100 items per transaction analytics → S3 WHERE DYNAMODB SITS API Gateway → Lambda orders-api → PutItem on table Orders. Each call is a signed HTTPS request: no servers, versions or connection pools. On-demand capacity: nothing to provision, billed per request. DynamoDB Streams keeps each change for 24 h; order-views updates other views. Design for access patterns known up front: a customer's orders by customerId, and an order by its number through the GSI byOrderId. PARTITION KEY AND SORT KEY The partition key C-17 is hashed to pick P2; inside it, C-17's items are kept in order of the sort key sk (orderDate#orderId). P2 lives on 3 replicas in 3 Availability Zones; the write is acknowledged once 2 of the 3 have logged it (USENIX ATC 2022 paper). A Query with begins_with(sk, "2026-10") reads one range of one partition; a Scan reads every item in the table, 1 MB per call. THROUGHPUT AND HOT KEYS Each partition serves at most 3,000 read units and 1,000 write units per second (a write unit is one write of up to 1 KB), in either capacity mode. Every guest order is saved under GUEST, so 2,400 writes/s land on P3 and it throttles (TableWriteKeyRangeThroughputExceeded). Adaptive capacity gives a hot partition more of the table's throughput, but never past that limit. Fix the key: an id per guest, or write sharding. CONSISTENCY AND LIMITS Reads are eventually consistent unless ConsistentRead=true (twice the read units, not on GSIs). byOrderId is updated async, so it missed o-982. Conditional writes give optimistic locking; a transaction covers up to 100 items. Items are at most 400 KB, and a Query needs the partition key. No joins or ad hoc queries: export to S3 for analytics. Global tables replicate across Regions: last writer wins, or multi-Region strong consistency.

ขั้นที่ 1 · DynamoDB อยู่ตรงไหน API Gateway ของร้านเรียก Lambda function orders-api แล้ว function นี้ก็บันทึกแต่ละ order ลงตาราง Orders ด้วย PutItem ทุก call เป็น HTTPS request ที่เซ็นแล้ว เลยไม่มี server, version หรือ connection pool ให้ต้องดูแล และในโหมด on-demand ก็จ่ายตามจำนวน request ส่วน DynamoDB Streams ส่งทุกการเปลี่ยนแปลงไปที่ order-views ที่คอยอัปเดต view อื่น ๆ ให้ตรงกัน ตัวตารางเองออกแบบรอบ access pattern ที่รู้ล่วงหน้า

ขั้นที่ 2 · Partition key และ sort key partition key C-17 ถูก hash เพื่อเลือก partition P2 และในนั้น item ของ C-17 ถูกเก็บเรียงตาม sort key sk (orderDate#orderId) ทำให้ 2026-10-06#o-981 อยู่ถัดหลัง 2026-10-02#o-967 ส่วน P2 ถูกเก็บไว้บน 3 replica ใน 3 Availability Zone และการเขียนจะได้ ack เมื่อ 2 replica ในนั้นเก็บมันไว้แล้ว ตัว Query ที่หา C-17 ด้วย begins_with(sk, "2026-10") อ่านช่วงต่อเนื่องช่วงเดียวใน partition เดียว ส่วน Scan อ่านทุก item ในตาราง

ขั้นที่ 3 · Throughput และ hot key แต่ละ partition รับได้มากสุด 3,000 read unit และ 1,000 write unit ต่อวินาที ช่วง flash sale ทุก order ของ guest ถูกบันทึกไว้ใต้ customerId = GUEST ทำให้ 2,400 write/s ไป hash ลงที่ P3 แล้ว P3 ก็โดน throttle ระหว่างที่ P1 กับ P2 ว่างอยู่ และตารางโดยรวมก็ยังมีที่เหลือ ส่วน adaptive capacity ย้าย throughput ไปให้ partition ที่ยุ่งได้ แต่ดันให้เกินเพดานนั้นไม่ได้ ทางแก้คือ key ที่มีค่าหลากหลาย (id แยกให้ guest แต่ละคน) หรือ write sharding (GUEST#0 … GUEST#9)

ขั้นที่ 4 · Consistency และข้อจำกัด การอ่านเป็นแบบ eventually consistent เป็นค่าตั้งต้น ส่วน ConsistentRead=true ใช้ read unit สองเท่า และ global secondary index ไม่มีตัวเลือกนี้ ตัว byOrderId อัปเดตแบบ asynchronous การค้นหาทันทีหลัง PutItem เลยยังไม่เจอ o-982 จนกว่า index จะตามทัน ส่วน conditional write ให้ optimistic locking และ transaction หนึ่งครอบได้ถึง 100 item แต่ item หนึ่งใหญ่ได้ไม่เกิน 400 KB ส่วน query ต้องเดินตาม key ที่ออกแบบไว้ (ถ้าจะทำ analytics ให้ export ไป S3) และ global table จะตัดสิน write ที่ชนกันแบบ last writer wins เว้นแต่จะเลือก multi-Region strong consistency

ปัญหา

order service ของร้านออนไลน์ถามคำถามเดิม ๆ ไม่กี่ข้อทั้งวัน: บันทึก order นี้, ขอ list order ของลูกค้าคนนี้, หา order o-981 มันต้องตอบด้วย latency เท่าเดิม ทั้งตอนที่มีสิบ request ต่อวินาทีและตอน flash sale ถ้าใช้ relational server ตัวเดียว capacity ก็คือเครื่องหนึ่งเครื่องที่เราต้องกำหนดขนาด patch และ fail over เอง แถม Lambda execution environment ทุกตัวที่รันพร้อมกันก็เปิด database connection ของตัวเอง ส่วนการ scale out ด้วยการ shard database เองก็ต้องเพิ่มโค้ด routing, rebalancing และ resharding ที่ไม่เกี่ยวอะไรกับ order เลย

Amazon DynamoDB ตอบคำถามพวกนี้ในรูปของ service ไม่มี server หรือ version ให้ดูแล ทุก request เป็น HTTPS call ที่เซ็นแล้ว และตารางก็กระจายตัวเองไปตาม partition โดยดูจาก key ส่วนสิ่งที่ต้องแลกคือเราต้องออกแบบตารางรอบคำถามพวกนั้นก่อนจะเขียน item แรก

ทำงานยัง⁠ไง

Table, item และ key

table เก็บ item และ item หนึ่งคือชุดของ attribute: string, number, ค่า binary, Boolean และ null รวมถึง list, map และ set ที่ซ้อนกันได้ลึกถึง 32 ชั้น นอกจาก primary key แล้วก็ไม่มี schema ทำให้ item สองตัวในตารางเดียวกันมี attribute ต่างกันได้ ส่วน item หนึ่ง (นับชื่อ attribute ด้วย) ใหญ่ได้ไม่เกิน 400 KB

primary key เป็นได้ทั้ง partition key อย่างเดียว หรือ partition key คู่กับ sort key ส่วนตาราง Orders ใน diagram ใช้ key พวกนี้:

Attribute หน้าที่ ตัวอย่าง
customerId partition key ของตาราง C-17
sk sort key เขียนเป็น orderDate#orderId 2026-10-06#o-981
orderId partition key ของ global secondary index byOrderId o-981

DynamoDB ส่ง partition key เข้า hash function ภายใน แล้วผลลัพธ์ก็เป็นตัวเลือก partition ที่เก็บ item นั้น ส่วน item ทั้งหมดที่มี partition key เดียวกันรวมกันเป็น item collection ที่เก็บเรียงตาม sort key จากน้อยไปมาก นี่คือเหตุผลที่ "order ของ C-17 ในเดือนตุลาคม 2026" เป็นการอ่านช่วงเดียวจบ ค่าของ partition key ยาวได้ถึง 2,048 byte และค่าของ sort key ได้ถึง 1,024 byte

Partition, replica และ throughput

partition คือ storage บน SSD ที่ DynamoDB replicate ข้ามสาม Availability Zone และจัดการเองทั้งหมด มันเพิ่ม partition เมื่อตารางโตขึ้นหรือต้องการ throughput มากขึ้น และถ้าตารางไม่มี local secondary index มันก็กระจาย item collection ก้อนใหญ่ก้อนเดียวไปหลาย partition ได้

paper ของ USENIX ATC ปี 2022 อธิบายว่าข้างในเกิดอะไรขึ้น replica ของแต่ละ partition รวมกันเป็น replication group ที่เลือก leader ด้วย Multi-Paxos ตัว leader รับ write และการอ่านแบบ strongly consistent: มันต่อ write แต่ละตัวลง log ของตัวเอง ส่งไปให้ peer แล้วตอบรับเมื่อ replica สองในสามตัว ที่อยู่คนละ zone เก็บไว้แล้ว ส่วนการอ่านแบบ eventually consistent ใช้ replica ตัวไหนตอบก็ได้

แต่ละ partition ออกแบบมาให้รับได้มากสุด 3,000 read unit และ 1,000 write unit ต่อวินาที read unit หนึ่งหน่วยคือการอ่านแบบ strongly consistent หนึ่งครั้งขนาดไม่เกิน 4 KB (หรือแบบ eventually consistent สองครั้ง) ส่วน write unit หนึ่งหน่วยคือการเขียนหนึ่งครั้งขนาดไม่เกิน 1 KB และ item ที่ใหญ่กว่านั้นก็ใช้หลายหน่วย: การอ่านแบบ strongly consistent ของ item ขนาด 20 KB ใช้ 5 read unit

การอ่าน: GetItem, Query และ Scan

  • GetItem ดึง item ตัวเดียวด้วย primary key ครบทั้งชุด
  • Query ต้องมี partition key เป็นเงื่อนไขแบบเท่ากับ และกรอง sort key ให้แคบลงได้ด้วย =, <, <=, >, >=, BETWEEN หรือ begins_with ผลลัพธ์กลับมาเรียงตาม sort key หรือเรียงกลับด้านถ้าใส่ ScanIndexForward=false
  • Scan อ่านทุก item ในตารางหรือใน index

Query และ Scan คืนได้มากสุด 1 MB ต่อ call ถ้า response มี LastEvaluatedKey ให้ส่งมันกลับไปเป็น ExclusiveStartKey เพื่อดึงหน้าถัดไป และหยุดเมื่อ response ไม่มีมันแล้ว ตัว paginator ของ SDK ทำเรื่องนี้ให้ ส่วน FilterExpression แค่ตัดของที่จะส่งกลับ: item ที่มันตัดทิ้งก็ยังถูกอ่านและโดนคิดเงินไปแล้ว

# C-17's orders in October 2026, newest first (after step 2: o-981, then o-967)
aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "customerId = :c AND begins_with(sk, :month)" \
  --expression-attribute-values '{":c": {"S": "C-17"}, ":month": {"S": "2026-10"}}' \
  --no-scan-index-forward

Secondary index

  • global secondary index (GSI) อย่าง byOrderId มี partition key ของตัวเอง (และจะมี sort key ของตัวเองด้วยก็ได้) และมี partition ของตัวเอง DynamoDB copy ทุก write ของตารางเข้าไปแบบ asynchronous ปกติใช้เวลาไม่ถึงวินาที การอ่านจาก GSI เลยเป็น eventually consistent เสมอ เราเพิ่ม GSI ให้ตารางที่มีอยู่แล้วได้ แล้ว DynamoDB ก็จะ backfill ให้ ส่วน quota ตั้งต้นคือ 20 ตัวต่อตาราง และทุก index ที่ write ไปแตะจะใช้ write unit แยกของมันเอง บนตารางแบบ provisioned ถ้า GSI มี write capacity ไม่พอ มันจะทำให้ write ของตัวตารางเองโดน throttle ไปด้วย
  • local secondary index (LSI) ใช้ partition key ของตาราง แล้วเพิ่ม sort key อีกตัว ต้องสร้างพร้อมตาราง (มากสุด 5 ตัวต่อตาราง) และรองรับการอ่านแบบ strongly consistent แต่แลกกับการที่ item collection ทุกก้อนถูกจำกัดไว้ที่ 10 GB และ DynamoDB จะแบ่ง item collection ไปหลาย partition ไม่ได้อีก

Read consistency

การอ่านเป็น eventually consistent เป็นค่าตั้งต้น การอ่านทันทีหลัง write สำเร็จเลยอาจยังไม่เห็นมัน ถ้าใส่ ConsistentRead=true ตัว GetItem, Query และ Scan บนตารางหรือ LSI จะคืนค่าล่าสุดที่ commit แล้ว โดยใช้ read unit สองเท่า ส่วน GSI และ stream อ่านแบบ strongly consistent ไม่ได้

Conditional write และ transaction

ConditionExpression ทำให้ PutItem, UpdateItem หรือ DeleteItem มีผลก็ต่อเมื่อ item ยังเป็นอย่างที่เราคาดไว้ ไม่อย่างนั้น call จะพังด้วย ConditionalCheckFailedException และก็ยังใช้ write unit อยู่ดี ที่ใช้กันบ่อยมีสองแบบ:

  • Optimistic locking: เก็บ attribute version ไว้ และอัปเดตก็ต่อเมื่อมันไม่ได้เปลี่ยนไปตั้งแต่ตอนที่เราอ่าน item
  • Put if absent: attribute_not_exists(pk) ทำให้การเขียน key เดิมเป็นครั้งที่สองพัง idempotency record ก็ทำงานแบบนี้
# Mark o-981 as paid, but only if nobody changed it since we read version 1
aws dynamodb update-item \
  --table-name Orders \
  --key '{"customerId": {"S": "C-17"}, "sk": {"S": "2026-10-06#o-981"}}' \
  --update-expression "SET #st = :paid, version = :next" \
  --condition-expression "version = :seen" \
  --expression-attribute-names '{"#st": "status"}' \
  --expression-attribute-values '{":paid": {"S": "PAID"}, ":seen": {"N": "1"}, ":next": {"N": "2"}}'

status เป็นคำสงวนใน expression ของ DynamoDB เลยต้องใช้ placeholder #st

TransactWriteItems และ TransactGetItems รวมได้ถึง 100 action บน 100 item ที่ไม่ซ้ำกัน (รวมกันไม่เกิน 4 MB) ข้ามตารางได้ภายใน account และ Region เดียว แบบได้ทั้งหมดหรือไม่ได้เลย DynamoDB อ่านหรือเขียน item แต่ละตัวใน transaction สองรอบ transaction เลยแพงเป็นสองเท่า ส่วน ClientRequestToken ทำให้ call TransactWriteItems เป็น idempotent อยู่ 10 นาที

Capacity mode และราคา

On-demand (ค่าตั้งต้น) Provisioned
เราตั้งค่า ไม่ต้องตั้งอะไร จะตั้ง throughput สูงสุดต่อตารางหรือต่อ GSI ก็ได้ read และ write capacity unit ต่อวินาที ปกติใช้คู่กับ auto scaling
เราจ่ายค่า read และ write request unit ที่ใช้จริง capacity unit ต่อชั่วโมง จะใช้หรือไม่ใช้ก็ตาม ส่วน reserved capacity ลดราคาต่อชั่วโมงลงได้ ถ้าผูกสัญญาหนึ่งหรือสามปี
การ scale ตารางใหม่รับได้ต่อเนื่องถึง 4,000 write/s และ 12,000 read/s และรับ traffic ที่พุ่งถึงสองเท่าของ peak ครั้งก่อนได้ทันที auto scaling ปรับตาม target utilisation

ทั้งสองโหมดคิดค่า storage ต่อ GB-month ด้วย และมี table class แบบ Standard-Infrequent Access ที่ถูกกว่าไว้ให้ตารางที่ค่า storage เป็นส่วนใหญ่ แล้วก็มีของเสริมที่เลือกได้อย่าง backup, point-in-time recovery, การอ่าน stream, replication ของ global table และ data transfer แล้ว AWS ก็ลดราคา throughput แบบ on-demand ลง 50% และราคา global table ลงสูงสุด 67% มีผลตั้งแต่ 1 พฤศจิกายน 2024 และหลังจากนั้นก็วาง on-demand เป็นโหมดตั้งต้นที่แนะนำสำหรับ workload ส่วนใหญ่ ตารางหนึ่งเปลี่ยนจาก provisioned ไป on-demand ได้สูงสุดสี่ครั้งใน 24 ชั่วโมง และเปลี่ยนกลับได้ตลอด ก่อนช่วง peak ที่วางแผนไว้อย่างช่วง sale ตัว warm throughput ให้เรา pre-warm ตารางไว้ที่ rate ที่มันรับได้ทันที ราคาต่างกันไปในแต่ละ Region เพราะฉะนั้นให้เช็กหน้า pricing

Hot key และ adaptive capacity

throughput ถูกแบ่งกันตาม partition ทำให้ key ที่ยุ่งตัวเดียวโดน throttle ได้ ทั้งที่ตารางโดยรวมยังมีที่เหลืออีกเยอะ ใน diagram ทุก checkout ของ guest เขียนลง customerId = GUEST: ช่วง sale มี 2,400 write/s ไป hash ลงที่ P3 ที่รับได้ 1,000 แล้ว write ที่เกินจากนั้นก็โดน throttle ด้วยเหตุผล TableWriteKeyRangeThroughputExceeded ตัว AWS SDK จะ retry call ที่โดน throttle ด้วย exponential backoff ช่วยให้ burst สั้น ๆ เรียบขึ้น แต่ยกเพดานไม่ได้

Adaptive capacity เปิดอยู่ตลอด ในทั้งสอง capacity mode และไม่มีค่าใช้จ่ายเพิ่ม มันให้ throughput ของตารางกับ partition ที่ร้อนมากขึ้น ย้าย item ที่ถูกเข้าถึงบ่อยแยกออกจากกันเพื่อไม่ให้อยู่ partition เดียวกัน และหั่น item collection ที่ยุ่งเป็นช่วงตาม sort key ได้ แต่มันก็มีขีดจำกัด: partition เดียว (และเพราะฉะนั้น item เดียว) ยังได้มากสุด 3,000 read และ 1,000 write ต่อวินาที, collection ที่ write มาเรียงตาม sort key (อย่างวันที่ของ GUEST) หั่นแล้วไม่ช่วยอะไร และบนตารางที่มี LSI ทุก item collection จะอยู่ใน partition เดียว ทางแก้อยู่ที่ key:

  • เลือก partition key ที่มีค่าไม่ซ้ำกันเยอะ ๆ ให้ traffic กระจายไปทุก partition: ใช้ id จริงของ guest แต่ละคนแทน GUEST
  • Write sharding: ต่อท้าย key ด้วย suffix จะสุ่มก็ได้ (GUEST#0 … GUEST#9) หรือคำนวณจากสิ่งที่ใช้ค้นหา item ก็ได้ (hash ของ order id) แล้วอ่านกลับมาให้ครบทุก suffix ด้วย query แบบขนาน
  • ใช้ CloudWatch Contributor Insights ดูว่า item ไหนถูกเข้าถึงบ่อยที่สุดและโดน throttle มากที่สุด

Time to live

เปิด TTL บน attribute ประเภท Number ที่เก็บเวลาหมดอายุเป็น Unix epoch seconds แล้ว DynamoDB ก็จะลบ item ที่หมดอายุอยู่เบื้องหลัง ปกติภายในไม่กี่วันหลังเวลาหมดอายุ และไม่ใช้ write unit ส่วนระหว่างนั้นการอ่านก็ยังอาจได้ item พวกนั้นกลับมา เพราะฉะนั้นให้กรองด้วย attribute นี้ด้วย การลบจาก TTL จะโผล่ใน DynamoDB Streams เป็น service deletion ทำให้ consumer เก็บ archive ของที่หมดอายุไว้ได้

Change data capture: Streams หรือ Kinesis

DynamoDB Streams Kinesis Data Streams for DynamoDB
Retention 24 ชั่วโมง ได้ถึง 1 ปี
คนอ่าน ได้ถึง 2 ตัวต่อ shard ได้ถึง 5 ตัวต่อ shard หรือ 20 ถ้าใช้ enhanced fan-out
ลำดับและตัวซ้ำ แต่ละ record โผล่ครั้งเดียวพอดี เรียงตามลำดับในแต่ละ item อาจมีตัวซ้ำ เรียงด้วย timestamp ของ record
การ process Lambda หรือ DynamoDB Streams Kinesis Adapter Lambda, Managed Service for Apache Flink, Firehose, Glue streaming

ถ้าใช้ DynamoDB stream เป็น event source ตัว Lambda จะ poll stream วินาทีละสี่ครั้ง แล้วเรียก function พร้อม batch ของ record ถ้า function พัง Lambda จะ retry batch นั้นจนกว่าจะสำเร็จหรือ record หมดอายุ consumer เลยต้อง idempotent ส่วน partial batch response ช่วยไม่ให้ record ที่สำเร็จไปแล้วถูก process ซ้ำ

Global table

global table เก็บ replica ของตารางไว้ในหลาย Region และทุก replica รับ write ได้

  • Multi-Region eventual consistency (MREC) เป็นค่าตั้งต้น มัน replicate การเปลี่ยนแปลงแบบ asynchronous ปกติภายในหนึ่งวินาที ถ้า item เดียวกันถูกแก้ในสอง Region พร้อมกัน การเปลี่ยนแปลงที่มี timestamp ภายในล่าสุดจะชนะ (last writer wins) และ transaction จะ atomic แค่ใน Region ที่มันรัน
  • Multi-Region strong consistency (MRSC) copy ทุก write ไปอีก Region แบบ synchronous ก่อนจะตอบรับ ทำให้การอ่านแบบ strongly consistent ใน Region ไหนก็ได้ version ล่าสุด มันต้องใช้สาม Region พอดี (สาม replica หรือสอง replica กับหนึ่ง witness) จาก list ของ Region ที่รองรับ และ write กับการอ่านแบบ strong ก็ต้องจ่ายค่า round trip ข้าม Region ถ้า write ไปที่ item ที่อีก Region กำลังแก้อยู่ จะพังด้วย ReplicatedWriteConflictException และ retry ได้ และตาราง MRSC ไม่รองรับ transaction, TTL หรือ LSI

AWS ให้ SLA ด้าน availability 99.999% สำหรับ global table เทียบกับ 99.99% ของตารางใน Region เดียว

Backup, export และ caching

  • Backup: on-demand backup รวมถึง point-in-time recovery (PITR) ย้อนไปได้ทุกวินาทีในช่วง 1 ถึง 35 วันล่าสุด ตั้งด้วย RecoveryPeriodInDays การ restore จะสร้างตารางใหม่เสมอ
  • Export ไป S3: export แบบเต็มหรือแบบ incremental จากข้อมูล PITR ในรูป DynamoDB JSON หรือ Amazon Ion โดยไม่ใช้ read capacity แล้ว Athena, Glue หรือ EMR ก็ query มันได้
  • DAX (DynamoDB Accelerator) คือ in-memory cache ที่อยู่หน้าตาราง ตอบการอ่านแบบ eventually consistent ในระดับ microsecond แต่ไม่ช่วยโค้ดที่ต้องอ่านแบบ strongly consistent

Single-table design

คู่มือ data modelling ของ AWS ให้ฐานไว้สองแบบ Single-table design เก็บ entity หลายประเภทไว้ในตารางเดียวภายใต้ชื่อ key กลาง ๆ (pk = CUSTOMER#C-17 คู่กับ sk = PROFILE หรือ sk = ORDER#2026-10-06#o-981) ทำให้ Query ครั้งเดียวได้ลูกค้าพร้อม order ของเขา และมีตารางเดียวให้ดูแลเรื่อง security, monitor และ scale ส่วนที่ต้องแลกคือ learning curve ที่ชัน และ setting ที่ต้องใช้ร่วมกัน: backup policy เดียว, encryption key เดียว, table class เดียว และ stream เดียวที่พาการเปลี่ยนแปลงของทุก entity ส่วน Multiple-table design อ่านง่ายและปรับเปลี่ยนง่ายกว่า และก็พอแล้วถ้า entity แทบไม่ถูกดึงมาพร้อมกัน ไม่ว่าแบบไหน ให้เขียน access pattern ออกมาก่อนเลือก key

จาก Dynamo (2007) สู่ DynamoDB

paper Dynamo ปี 2007 อธิบาย key-value store ภายในของ Amazon สำหรับตะกร้าสินค้า ที่แต่ละทีมรันกันเอง ดีไซน์ของมันไม่มี leader และสร้างมาให้รับ write ได้ตลอดเวลา: consistent hashing กับ virtual node, sloppy quorum กับ hinted handoff, vector clock ที่ reconcile conflict ตอนอ่าน, anti-entropy ด้วย Merkle tree และ membership แบบ gossip

DynamoDB ที่เปิดเป็น public service ตั้งแต่ปี 2012 เป็นคนละระบบกัน paper ของ USENIX ATC ปี 2022 อธิบายว่ามันเอา incremental scalability และ performance ที่คาดเดาได้ของ Dynamo มารวมกับการเป็น managed service, consistency และ table model ของ SimpleDB และนอกจากชื่อแล้วก็แทบไม่ได้เก็บ architecture ของ Dynamo ไว้เลย มันเป็น service แบบ multi-tenant ที่มี request router, metadata service ที่ map key ไปหา partition, replication group แบบมี leader ที่ใช้ Multi-Paxos สำหรับแต่ละ partition และ global admission control ที่ติดตามการใช้งานรวมของแต่ละตาราง ส่วน Apache Cassandra ยังใกล้กับดีไซน์ Dynamo ดั้งเดิมมากกว่า: consistent hashing, gossip และ tunable consistency โดยใช้ timestamp แบบ last-write-wins แทน vector clock

อยู่ตรงไหนใน solution

  • Solution: back end ของเว็บและแอปมือถือแบบ serverless (API Gateway, Lambda และ DynamoDB อย่างใน diagram), ตะกร้าสินค้าและ user session ที่หมดอายุผ่าน TTL, idempotency record ที่ใช้ request id เป็น key, metadata store อย่าง product catalogue, registry ของอุปกรณ์ หรือ index ของ object ที่เก็บใน S3 และ key-value lookup ปริมาณสูงอย่าง profile, game state หรือ feature setting
  • Pattern ใน catalog นี้: data store ของ back end แบบ Serverless ที่สร้างบน AWS Lambda, Sharding ที่ DynamoDB ทำให้เอง โดย route แต่ละ key แบบเดียวกับที่ Hash Table เลือก bucket, Change Data Capture ผ่าน Streams ที่ป้อน Materialized View อย่าง order-views และฝั่งอ่านของ CQRS, record ของ Idempotent Consumer ที่เขียนด้วย conditional put และ Claim Check สำหรับ payload ที่ใหญ่เกิน 400 KB โดยเก็บไว้ใน S3 แล้วเก็บ key ไว้ใน item
  • ของที่อยู่ข้าง ๆ บ่อย ๆ: API Gateway, Lambda, Kinesis Data Streams, S3 และ Athena (export), Amazon OpenSearch Service (zero-ETL integration สำหรับ search), DAX, IAM, KMS และ CloudWatch
  • Managed offering: DynamoDB ก็คือ managed service อยู่แล้ว และรันได้แค่บน AWS ส่วน DynamoDB local เป็นเวอร์ชันที่ดาวน์โหลดมาใช้ตอน dev และเทสต์ ถ้าอยากได้ API ที่ย้ายไปที่อื่นได้ AWS ก็มี database ที่ compatible กับ Cassandra (Amazon Keyspaces) และกับ MongoDB (Amazon DocumentDB) รวมถึง PostgreSQL บน Amazon RDS และ Aurora

ใช้ตอนไหนดี

เลือก DynamoDB เมื่อ:

  • รู้ access pattern แล้วและเป็นแบบอิง key (get ด้วย id, list ตามเจ้าของ, N ตัวล่าสุดตามเวลา) และอยากได้ latency เท่าเดิมไม่ว่า request rate จะเท่าไร
  • สร้างบน Lambda และอยากได้ database ที่ไม่มี connection, server หรือการวางแผน capacity
  • traffic พุ่งเป็นช่วง ๆ หรือยังไม่รู้ (on-demand) หรือใหญ่มาก
  • ต้องเขียนได้ในหลาย Region พร้อมกัน (global table)

ให้มองหาตัวอื่นเมื่อ query เป็นแบบ ad hoc หรือ relational (join, report, filter ที่ยืดหยุ่น), เมื่อแอปต้องรันนอก AWS หรือเมื่อ entity ตัวเดียวโตเกิน 400 KB และแบ่งไม่ได้

Amazon DynamoDB Apache Cassandra MongoDB PostgreSQL / Aurora
Data model item ใต้ partition key และ sort key (ถ้ามี) row ใต้ partition key และ clustering column (CQL) document แบบ JSON ใน collection relational table (SQL)
Query ตาม key และช่วงของ sort key บวก secondary index ไม่มี join ตาม partition key และช่วงของ clustering ใช้ secondary index สำหรับการค้นหาแบบอื่น query แบบ ad hoc, secondary index, aggregation pipeline SQL แบบ ad hoc ที่มี join และ index หลายประเภท
Consistency eventual เป็นค่าตั้งต้น ขออ่านแบบ strongly consistent ได้ ส่วน transaction ได้ถึง 100 item ปรับได้ทีละ request (ONE, QUORUM, ALL …) และใช้ last write wins primary หนึ่งตัวต่อ replica set รับ write มี multi-document transaction ACID transaction บน primary
Scaling เพิ่ม partition ให้อัตโนมัติ ส่วน throughput เป็นแบบ on-demand หรือ provisioned เพิ่ม node เข้า ring shard collection ด้วย shard key scale up และเพิ่ม read replica (Aurora: ได้ถึง 15 ตัว บน storage ที่ copy ข้าม 3 AZ)
รันแบบ AWS service แบบ fully managed เท่านั้น ดูแลเอง หรือ Amazon Keyspaces ดูแลเอง, MongoDB Atlas หรือ Amazon DocumentDB (compatible) ดูแลเอง หรือ Amazon RDS และ Aurora
License (ตุลาคม 2026) service แบบ proprietary Apache License 2.0 Server Side Public License v1 สำหรับ release ตั้งแต่ตุลาคม 2018 PostgreSQL License ส่วน Aurora เป็น proprietary

ได้อะไร เสียอะไร

  • access pattern มาก่อน key ถูกออกแบบมาสำหรับคำถามที่เรารู้ ถ้ามีคำถามใหม่ก็ต้องเพิ่ม GSI (ที่ต้อง backfill และมีค่า write ของตัวเอง) หรือปรับรูปข้อมูลใหม่
  • ไม่มี join หรือ query แบบ ad hoc งาน analytics ต้องไปที่ S3 ผ่าน export หรือไปที่ service ด้าน search หรือ analytics ผ่าน stream หรือ zero-ETL integration
  • เพดานต่อ partition hot key โดน throttle ที่ 3,000 read หรือ 1,000 write ต่อวินาทีต่อ partition ไม่ว่าตารางจะมี throughput เท่าไร
  • consistency เป็นทางเลือกที่ต้องจ่าย การอ่านเป็นแบบ eventual เป็นค่าตั้งต้น การอ่านแบบ strong แพงสองเท่าและไม่มีบน GSI และ global table แบบ MREC ตัดสิน conflict แบบ last writer wins
  • ขนาด item 400 KB ต่อ item ส่วน payload ใหญ่ ๆ ให้เก็บใน S3
  • รูปแบบต้นทุน ทุก GSI เพิ่มค่า write และ operation แบบ strongly consistent และแบบ transaction ก็แพงกว่า ตั้งแต่ลดราคาเมื่อพฤศจิกายน 2024 ทาง AWS คาดว่า on-demand จะถูกกว่า provisioned สำหรับ workload ส่วนใหญ่ แต่ถ้าเป็นตารางที่ traffic นิ่งมากและใช้เต็มที่ ให้ลองคิดราคาทั้งสองแบบ
  • Lock-in API และพฤติกรรมของมันมีแค่บน AWS ส่วน DynamoDB local ช่วยเรื่องเทสต์ แต่ไม่ช่วยเรื่องย้ายที่

ข้อควรรู้ตอนลงมือทำ

  • เขียน access pattern ออกมาก่อน แล้วค่อยเลือก key ถ้าจะให้ entity หลายประเภทใช้ตารางเดียวกัน ให้ใช้ชื่อ attribute กลาง ๆ (pk, sk)
  • อย่าให้ Scan อยู่ในเส้นทางของ request: ใช้ Query, ตาม LastEvaluatedKey ไปจนครบ และ project เฉพาะ attribute ที่ GSI แต่ละตัวต้องใช้
  • ทำ write ให้ retry ได้อย่างปลอดภัย: conditional put สำหรับ idempotency, attribute version สำหรับ optimistic locking และ ClientRequestToken บน transaction
  • คอยดูการโดน throttle: ตั้ง alarm ที่ request ที่โดน throttle, อ่าน ThrottlingReasons ใน exception, ใช้ Contributor Insights หา hot key และให้ SDK retry พร้อม backoff
  • เปิด PITR ให้ทุกตารางที่สร้างใหม่ไม่ได้ ตั้ง TTL ให้ตะกร้า, session และ idempotency record และเลือก stream view type ที่ consumer ต้องใช้ (NEW_IMAGE หรือ NEW_AND_OLD_IMAGES ถ้าอยากเห็นว่าอะไรเปลี่ยน)
  • ทำ stream consumer ให้ idempotent: Lambda จะ retry batch ที่พังจนกว่าจะสำเร็จหรือ record หมดอายุ