Polia zmizli ale nič nespadlo: Zachyť Schema Evolution bugy pred produkciou
Schema evolution bugy ma naučili, že “úspešne kompilovalo” neznamená nič keď tvoj producer a consumer hovoria rôzne verzie schémy. “Kde sa podelo pole email? Eventy stále prídu.” Debugovali sme produkčný incident kde user emaily zrazu prestali prichádzať do analytického pipelinu. Žiadne errory v logoch. Kafka consumer lag vyzeral normálne. Správy sa spracovávali v bežnom throughpute. Ale pole email v našich eventoch bolo vždy prázdne.
Časová os bola krupobití. Štvrtok 15:00: backend tím deployol upgrade služby ktorý “vyčistil nepoužívané Protobuf polia.” Štvrtok 15:05: analytický pipeline začal dostávať eventy s chýbajúcimi email dátami. Piatok 9:00: business tím si všimol že email campaign metriky sú všetky nuly. Piatok 14:00: nakoniec sme spojili bodky—backend odstránili pole ktoré si mysleli že je nepoužívané, ale analytický consumer ho stále čítal na starej schéme.
Čo robilo toto zvlášť zákerným bolo že všetko vyzeralo zdravo. Protobuf deserializácia nezlyhala—len ticho ignorovala chýbajúce field mapping. Consumer spracovával eventy úspešne, zapisoval do databázy úspešne a nereportoval žiadne errory. Mali sme komplexné unit testy, integration testy a dokonca load testy. Ale nikdy sme netestovali schema kompatibilitu naprieč verziami.
Toto je typ bugu čo ťa prinúti spochybniť hodnotu celého testovacieho suite. Schema evolution je v skutočnosti API kontrakt medzi producerom a consumerom, ale na rozdiel od REST API s explicitným verzovaním a OpenAPI schémami, message schémy sa menia neviditeľne. Jeden tím upgraduje svoju Protobuf definíciu, regeneruje kód, deployuje—a ticho rozbije každý downstream consumer ktorý ešte neupgradoval.
Prostredie: Protobuf 3.x, Kafka-based event streaming, polyglotné služby (Java, Go, Python), nezávislé deployment scheduly
Pochopenie Schema Evolution Zlyhaní
Ako by Schema Evolution mala fungovať
Producer v1 (stará schema):
message UserEvent {
string user_id = 1;
string email = 2; ← Pole existuje
string name = 3;
}
Consumer v1 (stará schema):
Číta: user_id ✓, email ✓, name ✓
Producer upgraduje na v2 (nová schema):
message UserEvent {
string user_id = 1;
// email pole odstránené! ← Breaking change
string name = 3;
string phone = 4; ← Nové pole pridané
}
Consumer stále na v1 (stará schema):
Číta event z Producer v2:
- user_id: "123" ✓
- email: "" ← TICHO PRÁZDNE (pole nie je v novej schéme)
- name: "John" ✓
Výsledok: Žiaden error, žiaden warning, len chýbajúce data
Štyri smrteľné Schema zmeny
1. ODSTRÁNENIE POĽA (rozbije backward compatibility)
┌─────────────────────────────────────────────────┐
│ Producer v2: odstráni "email" │
│ Consumer v1: očakáva "email" │
│ Výsledok: Consumer dostane prázdnu/default hodnotu│
│ Impact: TICHÁ STRATA DÁT │
└─────────────────────────────────────────────────┘
2. PRIDANIE REQUIRED POĽA (rozbije backward compatibility)
┌─────────────────────────────────────────────────┐
│ Producer v1: nevie o "phone" │
│ Consumer v2: vyžaduje "phone" (bez defaultu) │
│ Výsledok: Consumer dostane prázdne/zlyhá validácia│
│ Impact: PROCESSING ZLYHANIA alebo INVALIDNÉ DATA│
└─────────────────────────────────────────────────────┘
3. ZMENA TYPU POĽA (rozbije všetko)
┌─────────────────────────────────────────────────┐
│ Producer v1: user_id je string │
│ Producer v2: user_id zmenené na int64 │
│ Consumer: deserializácia zlyhá alebo corrupt │
│ Impact: PÁDY alebo KORUPCIA DÁT │
└─────────────────────────────────────────────────┘
4. REUSNUTIE FIELD NUMBERS (Protobuf) (corruptnuté data)
┌─────────────────────────────────────────────────┐
│ Producer v1: string email = 2; │
│ Producer v2: int32 age = 2; ← Reusnutý number! │
│ Consumer v1: číta field 2 ako string │
│ Výsledok: Type confusion, poškodené data │
│ Impact: KORUPCIA DÁT │
└─────────────────────────────────────────────────┘
Bežné Schema Evolution katastrofy
1. “Cleanup” ktorý rozbil všetko
// Predtým (v1)
message OrderEvent {
string order_id = 1;
string user_email = 2; // "Už toto nepoužívame"
double amount = 3;
}
// Potom (v2) - "Vyčistené nepoužívané pole"
message OrderEvent {
string order_id = 1;
// user_email odstránený! ← Analytics tím to stále používal
double amount = 3;
}
Impact: Analytics pipeline ticho prestal zbierať emaily. Trvalo 2 týždne kým sme si všimli.
2. “Optional” pole ktoré nebolo
// Producer pridá nové pole
message UserEvent {
string user_id = 1;
string email = 2;
string phone = 3; // Nové optional pole
}
// Consumer validačná logika (pridaná bez kontroly schémy):
if event.phone == "" {
log.Error("Invalid event: phone required")
return error
}
Impact: Starí produceri (v1) posielajú eventy bez phone. Consumer (v2) ich všetky zamieta ako invalidné.
3. Type Change horror
// v1: user_id bol string
message Event {
string user_id = 1;
}
// v2: "Spravme to numerické kvôli efektivite"
message Event {
int64 user_id = 1; ← BREAKING CHANGE
}
Impact: Deserializácia katastroficky zlyhá. Protobuf možno ani neerrorne—len vráti smetie.
4. Avro Union Expansion
// Avro v1
{
"type": "record",
"fields": [
{"name": "status", "type": {"type": "enum", "symbols": ["PENDING", "COMPLETE"]}}
]
}
// Avro v2 - pridaná nová enum hodnota
{
"type": "record",
"fields": [
{"name": "status", "type": {"type": "enum", "symbols": ["PENDING", "COMPLETE", "CANCELLED"]}}
]
}
Impact: Starí consumeri nepoznajú “CANCELLED”, môžu spadnúť alebo treatovať ako unknown.
Schema Evolution Contract prístup
Definuj explicitné compatibility pravidlá ktoré CI vynucuje:
# schema_contract.yml
version: 1
schemas:
user_events:
format: protobuf
path: proto/user_events.proto
compatibility: backward # Nové schémy musia byť čitateľné starými consumermi
order_events:
format: avro
path: schemas/order_events.avsc
compatibility: full # Backward AJ forward compatible
rules:
- no_field_removal: true # Nikdy neodstraňuj polia
- no_type_changes: true # Nikdy nemeň typy polí
- no_required_fields: true # Všetky nové polia musia mať defaulty
- no_reused_field_numbers: true # Protobuf: nikdy nereusuj numbery
Tento kontrakt hovorí:
- backward compatibility: Nová producer schema musí byť čitateľná starou consumer schémou
- forward compatibility: Stará producer schema musí byť čitateľná novou consumer schémou
- full compatibility: Backward AJ forward (najbezpečnejšie pre nezávislé deploymenty)
Implementácia: CI-Based Schema Validácia
Kľúč: testuj schema compatibility v CI pred merge, použitím reálnych schema evolution nástrojov.
Krok 1: Ulož Schema Históriu
# schemas/user_events/
├── v1.proto # Historická verzia
├── v2.proto # Aktuálna verzia v main
└── v3.proto # Navrhnutý change v PR
Krok 2: Protobuf Compatibility Check (buf)
# buf.yaml - Protobuf linting a breaking change detekcia
version: v1
breaking:
use:
- FILE
rules:
- FIELD_NO_DELETE
- FIELD_SAME_TYPE
- FIELD_SAME_CARDINALITY
- ENUM_VALUE_NO_DELETE
- ONEOF_NO_DELETE
# V CI: porovnaj s predchádzajúcou verziou
buf breaking --against '.git#branch=main'
Čo to chytí:
- Odstránenia polí
- Zmeny typov
- Odstránenia enum hodnôt
- Reusnutie field numbers
Krok 3: Avro Compatibility Check (Schema Registry)
// AvroCompatibilityTest.java
import org.apache.avro.Schema;
import org.apache.avro.SchemaCompatibility;
import org.junit.jupiter.api.Test;
class AvroCompatibilityTest {
@Test
void newSchemaIsBackwardCompatible() throws Exception {
// Načítaj schémy
Schema oldSchema = loadSchema("schemas/user_events/v2.avsc");
Schema newSchema = loadSchema("schemas/user_events/v3.avsc");
// Test backward compatibility (nová schema môže čítať staré data)
var result = SchemaCompatibility.checkReaderWriterCompatibility(
newSchema, // reader (nový consumer)
oldSchema // writer (starý producer)
);
if (result.getCompatibility() != SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE) {
fail("Schema NIE JE backward compatible: " + result.getDescription());
}
}
@Test
void newSchemaIsForwardCompatible() throws Exception {
Schema oldSchema = loadSchema("schemas/user_events/v2.avsc");
Schema newSchema = loadSchema("schemas/user_events/v3.avsc");
// Test forward compatibility (stará schema môže čítať nové data)
var result = SchemaCompatibility.checkReaderWriterCompatibility(
oldSchema, // reader (starý consumer)
newSchema // writer (nový producer)
);
if (result.getCompatibility() != SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE) {
fail("Schema NIE JE forward compatible: " + result.getDescription());
}
}
}
Krok 4: Cross-Version Serialization Test
Ultimátny test: serializuj s novou schémou, deserializuj so starou schémou (a naopak).
// schema_compat_test.go
package schematest
import (
"testing"
pb_v2 "yourapp/proto/v2"
pb_v3 "yourapp/proto/v3"
"google.golang.org/protobuf/proto"
)
func TestBackwardCompatibility(t *testing.T) {
// Producer používajúci NOVÚ schému (v3)
newEvent := &pb_v3.UserEvent{
UserId: "user-123",
Name: "John Doe",
Phone: "+1234567890", // Nové pole vo v3
}
// Serializuj s v3
data, err := proto.Marshal(newEvent)
if err != nil {
t.Fatal(err)
}
// Consumer používajúci STARÚ schému (v2) sa ju snaží čítať
var oldEvent pb_v2.UserEvent
err = proto.Unmarshal(data, &oldEvent)
if err != nil {
t.Fatalf("Starý consumer ZLYHALO čítať nové producer data: %v", err)
}
// Overenie že core polia stále fungujú
if oldEvent.UserId != "user-123" {
t.Errorf("user_id mismatch: got %v", oldEvent.UserId)
}
if oldEvent.Name != "John Doe" {
t.Errorf("name mismatch: got %v", oldEvent.Name)
}
// oldEvent.Phone neexistuje vo v2, to je OK (forward compatibility)
}
func TestForwardCompatibility(t *testing.T) {
// Producer používajúci STARÚ schému (v2)
oldEvent := &pb_v2.UserEvent{
UserId: "user-456",
Name: "Jane Smith",
// Žiadne phone pole vo v2
}
// Serializuj s v2
data, err := proto.Marshal(oldEvent)
if err != nil {
t.Fatal(err)
}
// Consumer používajúci NOVÚ schému (v3) sa ju snaží čítať
var newEvent pb_v3.UserEvent
err = proto.Unmarshal(data, &newEvent)
if err != nil {
t.Fatalf("Nový consumer ZLYHALO čítať staré producer data: %v", err)
}
// Overenie polí
if newEvent.UserId != "user-456" {
t.Errorf("user_id mismatch: got %v", newEvent.UserId)
}
// Phone by malo byť prázdne (default hodnota) - toto je očakávané
if newEvent.Phone != "" {
t.Errorf("Očakávané prázdne phone, got %v", newEvent.Phone)
}
}
CI Integrácia (GitHub Actions)
name: schema-evolution-contract
on: [pull_request]
jobs:
schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Potreba história pre buf breaking
- uses: bufbuild/buf-setup-action@v1
- name: Skontroluj Protobuf breaking changes
run: |
buf breaking --against '.git#branch=main'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Testuj Avro schema kompatibilitu
run: ./gradlew test --tests AvroCompatibilityTest
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Testuj cross-version serializáciu
run: go test ./pkg/schematest -v
- name: Zlyhá ak nekompatibilné
if: failure()
run: |
echo "❌ Schema zmena rozbíja kompatibilitu!"
echo "Pozri https://protobuf.dev/programming-guides/proto3/#updating"
exit 1
Toto beží na každej PR a blokuje merge ak je schema compatibility rozbitá.
Runtime Monitoring (Produkcia)
CI chytí design problémy. Produkcia potrebuje monitoring pre reálne incompatibility:
# Počet deserializačných errorov
increase(protobuf_unmarshal_errors_total[5m]) > 10
# Track schema version skew
max(schema_version{topic="user-events"})
- min(schema_version{topic="user-events"})
> 2 # Viac ako 2 verzie odlišné
# Alert na unknown fields (forward incompatibility signal)
increase(protobuf_unknown_fields_total[5m]) > 100
Instrumentuj tvoje deserializery:
func decodeEvent(data []byte) (*Event, error) {
var event Event
err := proto.Unmarshal(data, &event)
if err != nil {
unmarshalErrors.Inc()
return nil, err
}
// Skontroluj unknown fields (Protobuf ich drží)
if len(event.XXX_unrecognized) > 0 {
unknownFields.Inc()
log.Warn("Event obsahuje unknown fields - schema version skew?")
}
return &event, nil
}
Keď sa Schema Break stane v produkcii
Krok 1: Identifikuj Breaking Change
# Skontroluj schema verzie v používaní
kubectl exec -it producer-pod -- env | grep SCHEMA_VERSION
kubectl exec -it consumer-pod -- env | grep SCHEMA_VERSION
# Porovnaj schémy
buf breaking --against 'https://github.com/yourorg/schemas#branch=v2.3.0'
Krok 2: Okamžitá mitigácia
Možnosť 1: Rollback producer (najbezpečnejšie)
# Vráť na predchádzajúcu producer verziu
kubectl rollout undo deployment/event-producer
Možnosť 2: Fast-forward consumer (ak možné)
# Deployni consumer s novou schémou
# Len ak nový consumer zvládne staré producer data
kubectl set image deployment/event-consumer app=consumer:v3
Možnosť 3: Replay s korektnou schémou
# Ak data boli stratené, replay Kafka topic s kompatibilným consumerom
kafka-consumer-groups --reset-offsets --to-earliest
Krok 3: Oprav Schému
Pre odstránené pole:
// Zlé: pole odstránené
message UserEvent {
string user_id = 1;
// string email = 2; ← REMOVED
}
// Dobré: označ deprecated namiesto toho
message UserEvent {
string user_id = 1;
string email = 2 [deprecated = true]; // Ponechaj pre compatibility
}
Pre pridané required pole:
// Zlé: nové pole bez defaultu
message UserEvent {
string user_id = 1;
string phone = 2; // Starí produceri toto nepošlú!
}
// Dobré: urob optional alebo poskytni default
message UserEvent {
string user_id = 1;
optional string phone = 2; // Explicitne optional
}
Pre zmenu typu:
// Zlé: zmenený typ
message UserEvent {
int64 user_id = 1; // Bol string!
}
// Dobré: pridaj NOVÉ pole s novým typom, deprecni staré
message UserEvent {
string user_id = 1 [deprecated = true];
int64 user_id_v2 = 4; // Nové pole, nový number
}
Checklist
## Schema Evolution Checklist
### Pred zmenou Schémy
- [ ] Skontroluj aké consumery existujú pre túto schému
- [ ] Urči požadovanú kompatibilitu (backward/forward/full)
- [ ] Nikdy neodstraňuj polia (deprecni namiesto toho)
- [ ] Nikdy nemeň typy polí (pridaj nové pole namiesto toho)
- [ ] Nikdy nereusuj Protobuf field numbers
- [ ] Všetky nové polia musia mať defaulty alebo byť optional
### CI Kontrakt
- [ ] buf breaking check pre Protobuf
- [ ] Avro SchemaCompatibility test
- [ ] Cross-version serialization test (starý→nový, nový→starý)
- [ ] Testuj s realistickými data samples
### Produkčné Monitorovanie
- [ ] Alert na deserializačné errory
- [ ] Alert na schema version skew > 2 verzie
- [ ] Track unknown field warningy
- [ ] Dashboard ukazujúci schema verzie naprieč službami
### Keď je Break detekovaný
- [ ] Identifikuj ktorá schema verzia rozbila kompatibilitu
- [ ] Rollback producer ALEBO fast-forward consumer
- [ ] Oprav schému (pridaj pole späť, urob optional, atď.)
- [ ] Replay stratené data ak treba
Záver
Schema evolution je neviditeľný API kontrakt. Zmeníš Protobuf pole, regeneruješ kód, deploynešš—a ticho rozbijéš každý consumer ktorý ešte neupgradoval. Žiaden compile error, žiaden runtime exception, len chýbajúce data v produkcii.
Schema Evolution Contracts spravía z “nerozbij schému” vynucovaný CI gate namiesto code review komentára. Testovaním backward a forward compatibility s reálnou serializáciou chytíš breaking changes pred produkciou.
Kľúčový insight: schema kompatibilita nie je dokumentačný problém, je to testovací problém. Nemôžeš code reviewom z toho von—potrebuješ CI aby serializovalo data so schémou v3 a deserializovalo so schémou v2, a zlyhalo build ak sa to rozbije.
Kľúčové princípy:
- Nikdy neodstraňuj polia—deprecni ich namiesto toho
- Nikdy nemeň typy—pridaj nové pole s novým numbrom
- Vždy pridaj defaulty—nové polia musia byť optional alebo mať default hodnoty
- Testuj naprieč verziami—serializuj s novou, deserializuj so starou (a naopak)
- Monitoruj version skew—alertuj keď producer/consumer schémy odídu príliš ďaleko
Nabudúce keď niekto navrhne “vyčistiť nepoužívané Protobuf polia,” spýtaj sa: “Spustili sme schema evolution kontrakt?”
Súvisiace články
- Kafka Partition Skew Contracts - Ďalší neviditeľný kontrakt ktorý sa rozbije v produkcii
- RabbitMQ Ack Contracts - Testovanie message behavior v CI
Súvisiace články
Jedna partition na 99% CPU: Zastav Kafka hotspoty skôr ako dorazia do produkcie
Všetky partitiony vyzerajú vyvážené v testovaní, potom príde produkčný traffic a jedna partition sa roztopí. Vinník: tvoj partition key má otrásnú kardinalitu a nikto si toho nevšimol.
5000 Unacked správ a stúpa: Zastav RabbitMQ consumer meltdowny v CI
Queue vyzerá zdravo až do deploymentu, potom messages_unacknowledged exploduje, pamäť stúpa a redelivery storms začínajú. Vinník: tvoj prefetch je príliš vysoký a nikto netestoval skutočné ack správanie.
Protobuf evolúcia v eventoch: Prečo buf breaking nestačí
Ako bezpečne evolovať Protobuf schémy v event-driven systémoch. Pravidlá pre .proto, upcaster pattern a backward compatibility.
RSS Contracts: Ako prestat zabijat Java pody v Kubernetes (OOMKilled) testovanim RSS ako API
Cgroup RSS budgety, CI sampling a runtime headroom ti chytia JVM memory regresie skor, nez trafia produkciu.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.