KyaniteLabs Stüdyo — 2017’den beri
Vaka çalışması 001 — Lojistik

Kıtasal bir sevkiyat motorunu çevrimdışı bırakmadan yeniden inşa etmek.

Sekiz bin sürücü, 220 sevk memuru, on dört yıllık birikmiş mantık. Yük taşınmaya devam ederken operasyonun beynini değiştirdik.

Müşteri
Northwind Freight Inc.
Proje
Yerleşik geliştirme · 11 ay
Ekip
4 mühendis, 1 PM
Yıl
2024—2025
Alan
Yük taşımacılığı · LTL sevkiyat
Teknoloji
Rust, Postgres, Kafka, React
Rol
Mimari, geliştirme, nöbet rotasyonu
Durum
Yayında · üretimde
Şek. 1.0 — Sevkiyat konsolu, ana görünüm. Doğu koridorunda gerçek zamanlı yük ataması.

01Sorun

Northwind’in sevkiyat sistemi 2011’de yazılmıştı; üç kez yerinde göç ettirilmiş bir Microsoft SQL Server şemasına dayanıyordu. 2024’e geldiğimizde 1,3 milyar dolarlık bir cironun tek arıza noktasıydı. Bir sevk memurunun işlemini çözmesi ortalama 1,4 saniye sürüyordu; sabah yoğunluğunda, ET 05:30–07:00 arası, bu süre dokuz saniyenin üstüne çıkıyor ve sistem sürücü tabletlerine değişiklikleri sessizce iletmeyi bırakıyordu.

On sekiz ay önceki yeniden yazma denemesi, bir tedarikçinin Northwind tarafında kimsenin işletmek konusunda kendine güvenmediği Kubernetes biçiminde bir ürün teslim etmesinin ardından öldürülmüştü. Yönetimin içgüdüsü, bizce haklı olarak, “sıfırdan yeniden inşa” gibi görünen her şeye son derece şüpheyle yaklaşmaktı. Bir kısıt üzerinde anlaştık: proje boyunca sevkiyat sistemi otuz saniyeden fazla kullanılamaz hâlde kalamazdı.

02Mimari ve kararlar

Sevkiyat alanını, operasyon ekibinin dilinde zaten var olan dikişler boyunca üç servise böldük: yük planlama (yükü bir çekiciye atama), rotalama (durakların gerçek sırasını hesaplama) ve yürütme (tabletlerden ve ELD’lerden gelen olay akışı). Her biri kendi Postgres örneğini ve tek bir Rust süreci aldı. Her şey Kafka üzerinden, küçük ve elle yazılmış bir şema kayıt defteri ile konuşuyor — Protobuf ve Avro denedik, sonunda olaylar sırasında grep ile kolayca aranabilmesi için sürümlenmiş bir JSON sözleşmesini tercih ettik.

Göç dokuz ay boyunca gölge modda çalıştı. Yazma işlemleri hem eski hem yeni sisteme gidiyor, okumalar eskiden geliyor, yeni sistemin çıktısı eskinin çıktısıyla sürekli karşılaştırılıyordu. Eski kodun on dört yılda biriktirdiği yaklaşık kırk davranışsal tuhaflığı yakaladık — kimsenin açıklayamadığı kurallar — ve sevk memurlarının bağımlı olduklarını yeniden ürettik, kimsenin gerekçelendiremediklerini ise kapattık. Geçişin kendisi bir pazar günü ET 04:00’te yayımlanan üç satırlık bir yapılandırma değişikliğiydi.

Şek. 2.1 — Göç sonrası servis topolojisi
dispatcher UI driver tablet ELD · webhooks api gateway load planning Rust · pg-A routing Rust · pg-B execution Rust · pg-C Kafka 9 topics data lake replay audit log

Savunacağımız ödün: Servis başına Postgres’i koruduk ve dağıtık işlemlerden tamamen kaçındık. Rotalamadaki her durum değişikliği, yük planlamanın Kafka’dan tüketip değişmez biçimde uyguladığı bir olay yayar — bir rota sonradan iptal edilirse telafi edici düzeltme dahil. Bu, sevk memurunun ekranında yaklaşık kırk milisaniyelik nihai tutarlılık bedeline mal oluyor. Operasyon ekibiyle bunun kabul edilebilir olduğunu test ettik; öyle.

Daha erken yapmış olmayı istediğimiz şey: Eskiyle karşılaştırma kablajını altıncı haftada değil, birinci haftada yazmak. Geçiş sırasında herkesi sakin tutan tek araçtı.

Şek. 2.2 — Değişmez uygulama deseni, basitleştirilmiş
// load_planning::apply_route_event
pub async fn apply(evt: RouteEvent, db: &Pg) -> Result<()> {
    // natural key: (load_id, sequence_no) is unique in route_assignments
    let existing = db.fetch_one(
        /* sql */ r#"SELECT applied_at FROM route_assignments
                          WHERE load_id = $1 AND sequence_no = $2"#,
        &[&evt.load_id, &evt.sequence_no],
    ).await.optional()?;

    if existing.is_some() { return Ok(()); }   // already applied -- safe replay

    db.execute(/* sql */ r#"
        INSERT INTO route_assignments (load_id, sequence_no, route, applied_at)
        VALUES ($1, $2, $3, now())
    "#, &[&evt.load_id, &evt.sequence_no, &evt.route]).await?;

    audit::record(&evt).await?;
    Ok(())
}
Tek bir benzersizlik kısıtı, dağıtık işlem yok. Denetim kaydı, ekleme ile aynı Postgres işleminde yazılır.

03Ürün

01Sevk memurunun ana görünümü. Onu yeniden tasarlama dürtüsüne direndik. Mevcut klavye kısayolları ve sütun sıralaması on dört yıllık uzman kullanımın bir sonucuydu ve birebir korunmasına özen gösterdik — “yeni” sistem ilk günden eskisi gibi hissettiriyordu; istediğimiz buydu.
02Canlı sürücü durumuyla rota detayı. Sağdaki ince sütun olay günlüğü — her durum değişikliği görünür, kopyalanabilir ve kaynak servis adıyla belirtilmiş. Operasyon ekibi dördüncü haftada bunu eklememizi istedi; üretimde hata ayıklamada en çok kullanılan yüzey oldu.
03Bölge müdürünün sahada olduğu zamanlar için saha amiri görünümü. Bunu masaüstünün uyumlu bir portu olarak değil, sıkı kapsamlı ikinci bir istemci olarak inşa etmeyi bilinçli seçtik. Farklı ergonomik bağlam, farklı yazılım.
04Dokuz ay boyunca çalıştırdığımız gölge mod karşılaştırma panosu. Eski ve yeni sevkiyat arasındaki her anlaşmazlık burada bir satırdı; her gün önceliklendirdik. Altıncı ayda haftada yaklaşık iki tanesini kapatıyorduk. Geçiş yaptığımızda fark on dört ardışık gün boyunca boştu.

04Sonuç

Sevk memuru işlemi · p95
800ms 120ms
API ağ geçidinde sunucu tarafında ölçüldü. Sabah yoğunluğunda p99, 9,2sn’den 410ms’ye düştü.
Geçiş kesintisi
0sn
Bir pazar günü ET 04:00’te üç satırlık yapılandırma değişikliği. Müşteri tarafında gözle görülür hiçbir etki kaydedilmedi.
Yıllık altyapı maliyeti
−38%
Büyük ölçüde iki eski Windows sunucu çiftliğinin emekliye ayrılması ve t4g örneklerinde konsolidasyondan.

On iki ay sonra sistem, Northwind tarafında dört mühendis tarafından işletiliyor — önceki monolit on bir kişi tutuyordu. Çeyrek başına sayfalama olayıyla ölçülen nöbet yükü üçte iki azaldı. Hâlâ mimari incelemesi için küçük bir aylık ödemedeyiz; kalan her şey onların işletmesi.