Protobuf evolúcia v eventoch: Prečo buf breaking nestačí
Protobufy som bral ako stabilne, kym mismatch nezozral eventy. “Schému vieš udržať kompatibilnú, až kým nezistíš, že musíš prečítať event z 2019.” Toto je realita event-driven systémov. Protobuf wire format je backward compatible, ale tvoja aplikačná logika nie vždy.
Buf breaking checks v CI zachytia syntaktické breaking changes. Nezachytia však sémantické problémy - keď field zmenil význam alebo business logiku.
Testované na: Protobuf 3, buf CLI 1.28+, Kafka + event store. Produkčne na systémoch s 3+ rokmi event histórie.
Typy Kompatibility
Wire Compatibility (Protobuf garantuje)
// v1
message OrderCreated {
string order_id = 1;
int32 amount = 2;
}
// v2 - wire compatible
message OrderCreated {
string order_id = 1;
int64 amount = 2; // int32 → int64 je OK
string currency = 3; // nový optional field je OK
}
Sémantická Kompatibilita (Ty musíš garantovať)
// v1 - amount v centoch
message OrderCreated {
int32 amount = 2; // 1000 = $10.00
}
// v2 - amount v dolároch (BREAKING!)
message OrderCreated {
int32 amount = 2; // 1000 = $1000.00 !!!
}
Buf to nezachytí. Stará aplikácia číta nové eventy a interpretuje $10 ako $1000.
Pravidlá pre .proto Súbory
Nikdy Nerob
// 1. NIKDY nemeň typ poľa nekompatibilne
string order_id = 1; → int64 order_id = 1; // ZAKÁZANÉ
// 2. NIKDY nemeň číslo poľa
string order_id = 1; → string order_id = 2; // ZAKÁZANÉ
// 3. NIKDY neodstráň required pole (proto2)
required string name = 1; // Ak existuje, musí zostať
// 4. NIKDY nepremenuj enum hodnotu (ak používaš name matching)
enum Status {
PENDING = 0;
ACTIVE = 1; → RUNNING = 1; // ZAKÁZANÉ
}
Bezpečné Zmeny
// 1. Pridaj optional pole
message Order {
string id = 1;
string customer_id = 2; // NOVÉ - OK
}
// 2. Deprecate pole (neodstráň)
message Order {
string id = 1;
string old_field = 2 [deprecated = true];
}
// 3. Rozšír enum (ale pozor na default)
enum Status {
UNKNOWN = 0; // Vždy maj UNKNOWN/UNSPECIFIED = 0
PENDING = 1;
ACTIVE = 2;
CANCELLED = 3; // NOVÉ - OK
}
// 4. Zmeň optional na repeated (wire compatible)
string tag = 1; → repeated string tags = 1; // OK
Buf CI Setup
buf.yaml
version: v1
breaking:
use:
- FILE
except:
- FIELD_SAME_JSON_NAME # Povoľ JSON name zmeny
lint:
use:
- DEFAULT
except:
- PACKAGE_VERSION_SUFFIX
buf.gen.yaml
version: v1
plugins:
- plugin: buf.build/protocolbuffers/go
out: gen/go
opt: paths=source_relative
- plugin: buf.build/grpc/go
out: gen/go
opt: paths=source_relative
GitHub Action
name: Protobuf Check
on: [pull_request]
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-setup-action@v1
with:
version: '1.28.0'
- name: Lint
run: buf lint
- name: Breaking change check
run: buf breaking --against '.git#branch=main'
- name: Generate
run: buf generate
- name: Verify generated files
run: |
git diff --exit-code gen/
Upcaster Pattern pre Event Evolution
Keď sa schéma zmení a máš staré eventy:
interface Event {
type: string;
version: number;
payload: unknown;
timestamp: Date;
}
interface Upcaster<TOld, TNew> {
inputVersion: number;
outputVersion: number;
upcast(old: TOld): TNew;
}
class OrderCreatedV1ToV2 implements Upcaster<OrderCreatedV1, OrderCreatedV2> {
inputVersion = 1;
outputVersion = 2;
upcast(old: OrderCreatedV1): OrderCreatedV2 {
return {
orderId: old.orderId,
amount: old.amount,
currency: 'USD', // Default pre staré eventy
createdAt: old.timestamp,
};
}
}
class EventUpcastingService {
private upcasters: Map<string, Upcaster<any, any>[]> = new Map();
register(eventType: string, upcaster: Upcaster<any, any>) {
const chain = this.upcasters.get(eventType) || [];
chain.push(upcaster);
chain.sort((a, b) => a.inputVersion - b.inputVersion);
this.upcasters.set(eventType, chain);
}
upcast<T>(event: Event, targetVersion: number): T {
const chain = this.upcasters.get(event.type) || [];
let payload = event.payload;
let currentVersion = event.version;
for (const upcaster of chain) {
if (currentVersion >= targetVersion) break;
if (upcaster.inputVersion === currentVersion) {
payload = upcaster.upcast(payload);
currentVersion = upcaster.outputVersion;
}
}
return payload as T;
}
}
Versioning Stratégie
Option 1: Version v Názve Eventu
// events/order/v1/order_created.proto
package events.order.v1;
message OrderCreated {
string order_id = 1;
}
// events/order/v2/order_created.proto
package events.order.v2;
message OrderCreated {
string order_id = 1;
Money amount = 2;
}
Option 2: Version Field v Evente
message OrderCreated {
int32 schema_version = 1;
string order_id = 2;
// V2 fields
optional Money amount = 3;
}
Option 3: Envelope Pattern
message EventEnvelope {
string event_type = 1;
int32 schema_version = 2;
google.protobuf.Any payload = 3;
google.protobuf.Timestamp timestamp = 4;
}
Production Checklist
## Protobuf Event Evolution Checklist
### Schema Design
- [ ] Vždy UNKNOWN = 0 pre enums
- [ ] Optional fields pre všetko nové
- [ ] Nikdy nemaž polia, len deprecate
- [ ] Dokumentuj sémantiku v komentároch
### CI/CD
- [ ] buf lint v každom PR
- [ ] buf breaking against main/master
- [ ] Generované súbory v repo (nie runtime)
- [ ] Version tag pre schema changes
### Evolution
- [ ] Upcasters pre major zmeny
- [ ] Backward compatibility testy
- [ ] Dual-read period pre breaking changes
- [ ] Monitoring starých verzií
### Documentation
- [ ] ADR pre každú schema zmenu
- [ ] Changelog pre .proto súbory
- [ ] Migration guide pre consumers
Záver
Protobuf evolúcia v event-driven systémoch vyžaduje viac než buf breaking checks:
- Sémantická kompatibilita - význam polí sa nesmie meniť
- Upcasters - transformuj staré eventy na nový formát
- Versioning - jasná stratégia pre major zmeny
- Testing - backward compatibility ako súčasť CI
FAQ
Môžem zmazať nepoužívané pole?
Nie. Označ ako deprecated a rezervuj číslo (reserved 5;). Staré eventy ho stále môžu obsahovať.
Čo ak potrebujem breaking change?
Vytvor nový event type (OrderCreatedV2), dual-write počas migrácie, potom ukončí starý.
Koľko verzií podporovať?
Minimum: všetky verzie v event store. Prakticky: 2-3 major verzie s upcast chain.
Súvisiace články
- Transactional Outbox - Event publishing patterns
- API Idempotency - Event deduplication
Súvisiace články
Polia zmizli ale nič nespadlo: Zachyť Schema Evolution bugy pred produkciou
Producer upgradol Protobuf, consumer ešte na starej verzii. Žiadne errory, žiadne warningy—len tichá strata dát v produkcii. Tvoja schema evolúcia rozbila backward compatibility a CI si toho nevšimlo.
gRPC v Kubernetes: Prečo Service round-robin klame
Prečo má jeden pod 90% trafficu pri gRPC. Reprodukovateľný lab, riešenia od client-side LB po service mesh, a production checklist.
Architectural Linting: Automatizovaná ochrana proti spaghetti kódu
Ako vynútiť architektonické pravidlá v CI/CD. Dependency Cruiser pre JS/TS, ArchUnit pre Java a praktické príklady konfigurácie.
gRPC Deadline Propagácia: Prevencia Kaskádových Zlyhaní
Frontend sa vzdá po 5s ale backend pracuje ďalších 30s. Bez deadline propagácie mrháte resources na odsúdené requesty. Ukážem ako to implementovať v Go.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.