סטנדרטיזציה של אירועי מיקרוסרוויס: Avro, Schema Registry וחוזים
ללא חוזה ברור, כל שינוי בפורמט האירוע שובר את הצרכנים. פעם אחת, שינוי שם שדה באירוע OrderShipped גרם לשלושה שירותים לקרוס בשעה 3:00 לפנות בוקר. ההתאוששות ארכה 4 שעות, וההפסד הממוצע מתקרית כזו הוא $15,000. סכמת אירועים היא חוזה שמבטיח שכל השינויים יהיו מפורשים ומבוקרים. היא מונעת 80% מבעיות התאימות לפני הפריסה. צור קשר — נעזור לך לעצב סכמות לארכיטקטורה שלך ולחסל קריסות בשעות הלילה.
Avro עם Schema Registry מהיר פי 3 בסריאליזציה מ-JSON ללא סכמה ומספק טיפוסיות קפדנית. אנו מבטיחים תאימות לאחור בכל שלבי האבולוציה — כך תימנע מתקריות לילה ותאיץ את הפיתוח.
למה Avro ולא JSON?
Avro הוא פורמט סריאליזציה בינארי שהוא קומפקטי פי 3-5 מ-JSON. הוא טיפוסי בקפדנות: שדה יכול להיות רק מהסוג המתואר בסכמה. Schema Registry בודק אוטומטית את תאימות הסכמות החדשות לישנות: אם תוסיף שדה חובה ללא ברירת מחדל, הרישום ידחה את ההרשמה. JSON אינו מספק ערבויות כאלה, ושגיאות תאימות מתגלות רק בזמן ריצה.
| מאפיין | Avro | JSON (ללא סכמה) |
|---|---|---|
| טיפוסיות | קפדנית | דינמית |
| גודל הודעה | קומפקטי (בינארי) | מפורט (טקסט) |
| תאימות | אוטומטית (Schema Registry) | ידנית |
| מהירות סריאליזציה | גבוהה (עד פי 3 מהר יותר) | נמוכה |
| תמיכה באבולוציה | מובנית (ברירת מחדל, alias) | אין |
אילו עקרונות עומדים בבסיס סכמת אירועים?
אירועים מתארים עובדות, לא פקודות. OrderShipped היא עובדה. ShipOrder היא פקודה. אירוע התרחש ואי אפשר לבטלו (רק לפצות עליו באירוע אחר).
הסכמה צריכה להיות עצמאית. הצרכן לא צריך לבצע בקשות נוספות כדי לעבד את האירוע. כל הנתונים הדרושים נמצאים בגוף האירוע.
תאימות לאחור כברירת מחדל. צרכנים ישנים חייבים לעבוד עם אירועים חדשים ללא שינויים.
מבנה אירוע
{ "eventId": "01HQ2XK4VB8M9QXYZ123456789",
"eventType": "order.shipped",
"eventVersion": "1.2",
"occurredAt": "2025-03-28T14:22:00.000Z",
"producedBy": "order-service",
"correlationId": "req-abc-123",
"causationId": "cmd-xyz-456",
"aggregateType": "Order",
"aggregateId": "12345",
"aggregateVersion": 7,
"payload": {
"orderId": 12345,
"userId": 67890,
"carrier": "DHL",
"trackingCode": "JD123456789DE",
"estimatedDelivery": "2025-03-31",
"items": [
{
"sku": "PROD-001",
"quantity": 2,
"warehouseId": "WH-MSK"
}
]
}
} | שדה מעטפת | תיאור |
|---|---|
| eventId | ULID או UUID עבור אידמפוטנטיות |
| eventType | היררכי: domain.aggregate.action |
| eventVersion | גרסת סמנטית של סכמת המטען |
| occurredAt | UTC ISO 8601 |
| correlationId | למעקב אחר שרשראות בקשות |
| aggregateId + aggregateVersion | לנעילה אופטימית |
סכמת Avro עם אבולוציה
{ "type": "record", "name": "OrderShipped", "namespace": "com.example.orders.events", "doc": "Событие отгрузки заказа со склада", "fields": [
{"name": "eventId", "type": "string"},
{"name": "eventType", "type": "string", "default": "order.shipped"},
{"name": "occurredAt", "type": {"type": "long", "logicalType": "timestamp-millis"}},
{"name": "orderId", "type": "long"},
{"name": "userId", "type": "long"},
{"name": "carrier", "type": "string"},
{"name": "trackingCode", "type": "string"},
{
"name": "estimatedDelivery",
"type": ["null", "string"],
"default": null,
"doc": "ISO date, может отсутствовать для некоторых перевозчиков"
},
{
"name": "warehouseId",
"type": ["null", "string"],
"default": null,
"doc": "Добавлено в v1.1 — необязательное поле для backward compatibility"
},
{
"name": "shippingCost",
"type": ["null", {"type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 2}],
"default": null,
"doc": "Добавлено в v1.2"
}
] }כללי אבולוציה לתאימות לאחור:
- שדות חדשים — תמיד עם ברירת מחדל (null או ערך)
- לא ניתן להסיר שדות חובה
- לא ניתן לשנות את סוג השדה
- לא ניתן לשנות שמות שדות (הוסף alias, ואז שנה שם דרך גרסה ראשית)
כדי להוסיף שדה { "eventId": "01HQ2XK4VB8M9QXYZ123456789", "eventType": "order.shipped", "eventVersion": "1.2", "occurredAt": "2025-03-28T14:22:00.000Z", "producedBy": "order-service", "correlationId": "req-abc-123", "causationId": "cmd-xyz-456", "aggregateType": "Order", "aggregateId": "12345", "aggregateVersion": 7, "payload": { "orderId": 12345, "userId": 67890, "carrier": "DHL", "trackingCode": "JD123456789DE", "estimatedDelivery": "2025-03-31", "items": [ {"sku": "PROD-001", "quantity": 2, "warehouseId": "WH-MSK"} ] } } מבלי לשבור תאימות, השתמש ב-{ "type": "record", "name": "OrderShipped", "namespace": "com.example.orders.events", "doc": "Событие отгрузки заказа со склада", "fields": [ {"name": "eventId", "type": "string"}, {"name": "eventType", "type": "string", "default": "order.shipped"}, {"name": "occurredAt", "type": {"type": "long", "logicalType": "timestamp-millis"}}, {"name": "orderId", "type": "long"}, {"name": "userId", "type": "long"}, {"name": "carrier", "type": "string"}, {"name": "trackingCode", "type": "string"}, { "name": "estimatedDelivery", "type": ["null", "string"], "default": null, "doc": "ISO date, может отсутствовать для некоторых перевозчиков" }, { "name": "warehouseId", "type": ["null", "string"], "default": null, "doc": "Добавлено в v1.1 — необязательное поле для backward compatibility" }, { "name": "shippingCost", "type": ["null", {"type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 2}], "default": null, "doc": "Добавлено в v1.2" } ] } ובסוג warehouseId. צרכנים ישנים שלא יודעים על שדה זה פשוט יקבלו null.
איך לבדוק תאימות סכמה?
אנו בודקים אוטומטית תאימות לאחור ב-CI/CD: סריאליזציה של האירוע בצד המפיק, דה-סריאליזציה בצד הצרכן, ומוודאים שגרסאות ישנות לא קורסות. זה תופס שינויים שוברים לפני הפריסה. בדיקות חוזה מתקנות ציפיות משני הצדדים. רק תקרית אחת עקב אי-תאימות סכמה עולה בממוצע $15,000 במהלך פריסת לילה.
איך להבטיח תאימות לאחור?
# Настройка Schema Registry — BACKWARD совместимость для всех событий orders
curl -X PUT http://schema-registry:8081/config/order-events-value \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"compatibility": "BACKWARD_TRANSITIVE"}'
# BACKWARD_TRANSITIVE — новая схема совместима со ВСЕМИ предыдущими версиями,
# не только с последнейשינוי ראשי (שינוי שובר) — נושא חדש:
-
"default": null→ עבור צרכנים עם סכמה ישנה -
["null", "string"]→ סכמה חדשה, צרכנים נודדים בהדרגה
תקופת מעבר: המפיק מפרסם לשני הנושאים. לאחר הגירה מלאה, # Настройка Schema Registry — BACKWARD совместимость для всех событий orders curl -X PUT http://schema-registry:8081/config/order-events-value \ -H "Content-Type: application/vnd.schemaregistry.v1+json" \ -d '{"compatibility": "BACKWARD_TRANSITIVE"}' # BACKWARD_TRANSITIVE — новая схема совместима со ВСЕМИ предыдущими версиями, # не только с последней מופסק.
קטלוג אירועים — תיעוד סכמות
עבור צוות עם מספר שירותים, רישום אירועים מרכזי הוא קריטי. אנו משתמשים ב-AsyncAPI כדי לתאר ערוצים והודעות.
asyncapi: 3.0.0
info:
title: Order Service Events
version: 1.0.0
description: События, публикуемые Order Service
channels:
order-events:
address: order-events
messages:
OrderCreated:
$ref: '#/components/messages/OrderCreated'
OrderShipped:
$ref: '#/components/messages/OrderShipped'
OrderCancelled:
$ref: '#/components/messages/OrderCancelled'
components:
messages:
OrderCreated:
name: OrderCreated
title: Заказ создан
summary: Публикуется при успешном создании нового заказа
contentType: application/avro
headers:
type: object
properties:
correlationId:
type: string
description: ID входящего HTTP-запроса
payload:
type: object
required: [eventId, orderId, userId, items, totalAmount]
properties:
eventId:
type: string
format: ulid
orderId:
type: integer
format: int64
userId:
type: integer
format: int64
items:
type: array
items:
type: object
properties:
sku:
type: string
quantity:
type: integer
price:
type: number
totalAmount:
type: number
createdAt:
type: string
format: date-time
מפרסם אירועים טיפוסי (TypeScript/Node.js)
import { SchemaRegistry } from '@kafkajs/confluent-schema-registry';
import { Kafka } from 'kafkajs';
interface EventEnvelope<T> {
eventId: string;
eventType: string;
eventVersion: string;
occurredAt: string;
producedBy: string;
correlationId?: string;
aggregateType: string;
aggregateId: string;
aggregateVersion: number;
payload: T;
}
interface OrderShippedPayload {
orderId: number;
userId: number;
carrier: string;
trackingCode: string;
estimatedDelivery?: string;
}
class OrderEventPublisher {
private registry: SchemaRegistry;
private producer: ReturnType<Kafka['producer']>;
async publishOrderShipped(data: OrderShippedPayload, correlationId?: string): Promise<void> {
const envelope: EventEnvelope<OrderShippedPayload> = {
eventId: ulid(),
eventType: 'order.shipped',
eventVersion: '1.2',
occurredAt: new Date().toISOString(),
producedBy: 'order-service',
correlationId,
aggregateType: 'Order',
aggregateId: String(data.orderId),
aggregateVersion: await this.getAggregateVersion(data.orderId),
payload: data,
};
const schemaId = await this.registry.getLatestSchemaId('order-events-value');
const encoded = await this.registry.encode(schemaId, envelope);
await this.producer.send({
topic: 'order-events',
messages: [
{
key: String(data.orderId),
value: encoded,
headers: {
'correlation-id': correlationId ?? '',
'event-type': 'order.shipped',
},
},
],
});
}
} איך עובד יישום סכמת אירועים?
יום 1: אנו מקיימים סדנה עם צוותי השירות: יצירת מפת Event Storming, זיהוי כל אירועי הדומיין והגבולות שלהם. יום 2: פיתוח סכמות Avro לכל סוג אירוע, הגדרת כללי שמות ומבנה מעטפת. רישום שלהם ב-Schema Registry. יום 3: יישום מפרסמי אירועים טיפוסיים בכל שירות מפיק ויצירת תיעוד AsyncAPI. יום 4: בדיקות חוזה, שילוב בדיקות תאימות ב-CI/CD, ומתן הנחיות לצוות על כללי אבולוציית סכמות.
מה כלול בפיתוח סכמת אירועים במפתחה מלאה
- סדנת Event Storming ותיעוד כל האירועים
- סכמות Avro עם תאימות לאחור וגרסאות
- הגדרת Schema Registry (Kafka) עם כללי BACKWARD_TRANSITIVE
- מפרסמי אירועים טיפוסיים ב-TypeScript/Node.js או Java/Scala
- ספריית סריאליזציה/דה-סריאליזציה לאירועים
- מפרט AsyncAPI לרישום האירועים המרכזי
- בדיקות חוזה משולבות ב-CI/CD
- תיעוד צוות והדרכת מפתחים
- תמיכה במהלך הגירת צרכנים ישנים
אנו מבטיחים איכות תוצאות: הניסיון שלנו הוא 7 שנים בעיצוב מערכות ריאקטיביות וארכיטקטורת מיקרוקרנל, עם למעלה מ-30 פרויקטים של אינטגרציות מונעות אירועים. קבל ייעוץ על עיצוב סכמת אירועים — נבחן את הפרויקט שלך ונציע את הפתרון האופטימלי.







