ב-5000 בקשות בשנייה, PostgreSQL מתחיל לפגר: שאילתות N+1, נעילות, פיגור רפליקה. מעבר ל-DynamoDB פותר את זה — זמן האחזור יורד מ-50ms ל-5ms, שיפור של פי 10, ועלויות התשתית יורדות ב-40% (חיסכון של $1,200 בחודש במקרה של לקוח אחד) על ידי ביטול רפליקות קריאה. אבל הגדרה לא נכונה של DynamoDB מובילה למחיצות חמות ולהגבלת קצב (throttling). אנו מגדירים DynamoDB מקצה לקצה: מעיצוב סכמת טבלה יחידה ועד לפריסת תשתית באמצעות AWS CDK. הניסיון שלנו: 5+ שנים על AWS, מעל 50 פרויקטים עם יישומי אינטרנט בעומס גבוה. אנו פועלים לפי השיטות המומלצות של AWS Well-Architected Framework. עלות הגדרת DynamoDB טיפוסית נעה בין $2,500 ל-$5,000 בהתאם למורכבות. קבלו הערכת פרויקט להגדרת DynamoDB שלכם במפתח אחד.
DynamoDB הוא מסד נתונים NoSQL מנוהל מ-AWS עם זמן אחזור מובטח <10ms בכל קנה מידה ו-SLA זמינות של 99.999%. אין שרתים לתחזוק, אין צורך ב-sharding ידני. אידיאלי לארכיטקטורות serverless ויישומים עם עומסי שיא. על פי סטטיסטיקות, 90% מהיישומים עם עומסים >1000 בקשות/שנייה מרוויחים ממעבר ל-DynamoDB. טכניקות האופטימיזציה שלנו ל-DynamoDB מפחיתות את מספר מדיניות ה-IAM ב-60% ואת זמן האחזור של שאילתות מתחת ל-10ms עבור 99% מהבקשות.
אילו בעיות DynamoDB פותר ביישומי אינטרנט?
בעיות טיפוסיות עם מסדי נתונים רלציוניים תחת עומס גבוה: שאילתות N+1, JOIN מורכבים, פיגור רפליקה. DynamoDB מבטל את אלה באמצעות denormalization ועיצוב טבלה יחידה. עם זאת, עיצוב לקוי מוביל למחיצות חמות, דלדול RCU/WCU ועלויות גבוהות. אנו פותרים בעיות אלה בשלב העיצוב. לדוגמה, בפרויקט אחד העברנו מ-PostgreSQL ל-DynamoDB, הפחתנו את זמן האחזור מ-50ms ל-5ms (פי 10 מהר יותר) וקיצצנו את עלויות התשתית ב-40% על ידי הסרת רפליקות קריאה. זה אפשר טיפול בעומסי שיא של עד 10,000 בקשות/שנייה ללא throttling. עיצוב טבלה יחידה קל פי 3 לניהול מאשר טבלאות מרובות עם JOIN מורכבים.
מדוע עיצוב טבלה יחידה הוא הסטנדרט עבור DynamoDB?
עיצוב טבלה יחידה — טבלה אחת לכל הישויות עם PK/SK עמוסים. גישת עיצוב NoSQL זו למסדי נתונים serverless מגדירה את כל דפוסי הגישה לפני יצירת הטבלה. עבור חנות מסחר אלקטרוני, דפוסים טיפוסיים: קבלת משתמש לפי אימייל, רשימת הזמנות משתמש לפי תאריך, פרטי הזמנה עם פריטים, מוצרים לפי קטגוריה. סכמה לדוגמה:
Сущность: User
PK: USER#<userId>
SK: METADATA
GSI1PK: EMAIL#<email>
GSI1SK: USER#<userId>
Сущность: Order
PK: USER#<userId>
SK: ORDER#<orderId>
GSI1PK: ORDER#<orderId>
GSI1SK: USER#<userId>
Сущность: OrderItem
PK: ORDER#<orderId>
SK: ITEM#<itemId>
Сущность: Product
PK: PRODUCT#<productId>
SK: METADATA
GSI1PK: CATEGORY#<cat>
GSI1SK: PRODUCT#<productId>גישה זו מאפשרת לכל השאילתות לפגוע בטבלה אחת באמצעות GSI. גישת טבלאות מרובות מגדילה את מספר הטבלאות, מסבכת את מדיניות IAM DynamoDB ומגדילה את זמן האחזור. LSI (אינדקסים משניים מקומיים) שימושיים למפתחות מיון חלופיים בתוך אותו PK.
לפי AWS Well-Architected Framework, שימוש בעיצוב טבלה יחידה ו-GSI הוא שיטה מומלצת עבור DynamoDB.
השוואה בין מצבי On-Demand ו-Provisioned
| מאפיין | On-Demand | Provisioned |
|---|---|---|
| עלות כתיבה | יקר פי ~1.5–2 ($0.75 ל-WCU לעומת $0.50) | זול יותר |
| גמישות | קנה מידה אוטומטי | הגדרת קנה מידה אוטומטי ידנית |
| עומסי שיא | ללא הגבלות | סיכון ל-throttling |
| המלצה | עומסים לא צפויים | עומסים יציבים |
On-Demand עדיף לעומסים משתנים אך יקר פי 1.5–2 בעומסים יציבים.
תשתית באמצעות AWS CDK (TypeScript)
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
import { RemovalPolicy } from 'aws-cdk-lib'
const table = new dynamodb.Table(this, 'AppTable', {
tableName: 'MyApp',
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
// или PROVISIONED + auto scaling
pointInTimeRecovery: true,
deletionProtection: true,
removalPolicy: RemovalPolicy.RETAIN,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
})
table.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
}) יישום מאגרים עם AWS SDK v3 DynamoDB
export class UserRepository {
async create(user: CreateUserInput): Promise<User> {
const id = crypto.randomUUID()
const now = new Date().toISOString()
await db.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName: TABLE,
Item: {
PK: `USER#${id}`,
SK: 'METADATA',
GSI1PK: `EMAIL#${user.email}`,
GSI1SK: `USER#${id}`,
id,
email: user.email,
name: user.name,
passwordHash: user.passwordHash,
role: 'user',
createdAt: now,
updatedAt: now,
_type: 'User'
},
ConditionExpression: 'attribute_not_exists(PK)'
}
}
]
}))
return { id, ...user, role: 'user', createdAt: now, updatedAt: now }
}
async findByEmail(email: string): Promise<User | null> {
const result = await db.send(new QueryCommand({
TableName: TABLE,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk',
ExpressionAttributeValues: {
':pk': `EMAIL#${email}`
},
Limit: 1
}))
return (result.Items?.[0] as User) ?? null
}
async findById(id: string): Promise<User | null> {
const result = await db.send(new GetCommand({
TableName: TABLE,
Key: {
PK: `USER#${id}`,
SK: 'METADATA'
}
}))
return (result.Item as User) ?? null
}
} כיצד להגדיר DynamoDB Streams ו-Lambda Triggers?
Streams מאפשרים תגובה לשינויי נתונים בזמן אמת — פי 10 מהר יותר מסקר (polling) לשינויי מסד נתונים. תרחיש טיפוסי הוא עדכון אינדקס חיפוש או שליחת התראות. דוגמה ל-Lambda handler:
export const handler = async (event: DynamoDBStreamEvent) => {
for (const record of event.Records) {
if (record.eventName !== 'MODIFY') continue
const newImage = unmarshall(record.dynamodb!.NewImage!)
const oldImage = unmarshall(record.dynamodb!.OldImage!)
if (newImage._type === 'Order' && newImage.status !== oldImage.status) {
await notifyOrderStatusChange(newImage.id, newImage.status)
}
}
} כיצד לנטר DynamoDB ולהגדיר התראות?
מדדים מרכזיים: Сущность: User PK: USER#<userId> SK: METADATA GSI1PK: EMAIL#<email> GSI1SK: USER#<userId> Сущность: Order PK: USER#<userId> SK: ORDER#<orderId> GSI1PK: ORDER#<orderId> GSI1SK: USER#<userId> Сущность: OrderItem PK: ORDER#<orderId> SK: ITEM#<itemId> Сущность: Product PK: PRODUCT#<productId> SK: METADATA GSI1PK: CATEGORY#<cat> GSI1SK: PRODUCT#<productId> , import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' import { RemovalPolicy } from 'aws-cdk-lib' const table = new dynamodb.Table(this, 'AppTable', { tableName: 'MyApp', partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING }, sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, // или PROVISIONED + auto scaling pointInTimeRecovery: true, deletionProtection: true, removalPolicy: RemovalPolicy.RETAIN, stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, }) table.addGlobalSecondaryIndex({ indexName: 'GSI1', partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING }, sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING }, projectionType: dynamodb.ProjectionType.ALL, }) , export class UserRepository { async create(user: CreateUserInput): Promise<User> { const id = crypto.randomUUID() const now = new Date().toISOString() await db.send(new TransactWriteCommand({ TransactItems: [{ Put: { TableName: TABLE, Item: { PK: `USER#${id}`, SK: 'METADATA', GSI1PK: `EMAIL#${user.email}`, GSI1SK: `USER#${id}`, id, email: user.email, name: user.name, passwordHash: user.passwordHash, role: 'user', createdAt: now, updatedAt: now, _type: 'User' }, ConditionExpression: 'attribute_not_exists(PK)' } }] })) return { id, ...user, role: 'user', createdAt: now, updatedAt: now } } async findByEmail(email: string): Promise<User | null> { const result = await db.send(new QueryCommand({ TableName: TABLE, IndexName: 'GSI1', KeyConditionExpression: 'GSI1PK = :pk', ExpressionAttributeValues: { ':pk': `EMAIL#${email}` }, Limit: 1 })) return (result.Items?.[0] as User) ?? null } async findById(id: string): Promise<User | null> { const result = await db.send(new GetCommand({ TableName: TABLE, Key: { PK: `USER#${id}`, SK: 'METADATA' } })) return (result.Item as User) ?? null } } , export const handler = async (event: DynamoDBStreamEvent) => { for (const record of event.Records) { if (record.eventName !== 'MODIFY') continue const newImage = unmarshall(record.dynamodb!.NewImage!) const oldImage = unmarshall(record.dynamodb!.OldImage!) if (newImage._type === 'Order' && newImage.status !== oldImage.status) { await notifyOrderStatusChange(newImage.id, newImage.status) } } } , ConsumedReadCapacityUnits. הגדר התראה מיידית על ConsumedWriteCapacityUnits. השתמש בלוחות מחוונים של CloudWatch לניטור DynamoDB. אנו ממליצים להגדיר התראות על חריגה מ-80% מהקיבולת המוקצית.
תהליך הגדרת DynamoDB
- ניתוח דפוסי גישה של היישום
- עיצוב סכמת טבלה יחידה
- יצירת תשתית באמצעות CDK
- יישום מאגרים עם AWS SDK v3 DynamoDB
- הגדרת Streams ו-Lambda triggers
- הגדרת ניטור והתראות
תוצרים
| רכיב | תיאור |
|---|---|
| עיצוב סכמה | הגדרת PK/SK, GSI, LSI |
| תשתית | מחסנית CDK עם טבלה, Streams, IAM |
| קוד מאגר | TypeScript/Node.js עם AWS SDK v3 |
| אינטגרציה | Lambda triggers, API Gateway |
| ניטור | לוח מחוונים CloudWatch, התראות |
| תיעוד | תיאור סכמה, דפוסי גישה, הוראות |
| גישה | גישה לקונסולת AWS ולמאגר |
| מסירה | מפגש הדרכה של שעה |
| תמיכה | חודש תמיכה לאחר ההשקה |
לוחות זמנים ועלות
עיצוב ואינטגרציה בסיסית: 3–5 ימים (מסירה במפתח אחד). הוספת Streams, Lambda, ניטור: עוד 3–5 ימים. מעבר ממסד נתונים רלציוני: 2–4 שבועות בהתאם לנפח. התמחור מחושב באופן אישי, תלוי במורכבות הסכמה ובהיקף האינטגרציה. צרו קשר להערכת פרויקט — נעריך מורכבות ולוחות זמנים. עלות הגדרת DynamoDB טיפוסית נעה בין $2,500 ל-$5,000. בקשו הערכה מקדימה כדי להבין כמה זמן ומשאבים יידרשו.







