Ders 20 / 34
Sürümleme Stratejileri
Aynı kaynağın iki sürümünün tek sunucuda yayımlanması, yol, başlık ve içerik tabanlı sürümlemenin istemci koduna ölçülen eşit maliyeti ve aracıların sürümü ayırt edip edemediğinden doğan gerçek fark.
İçindekiler
Doğrulama şeması bir önceki derste sabit kabul edildi: kalem sayısı en çok üç, iade tarihi
en çok otuz gün ileri. Bu sayılar değişebilir; daha önemlisi, gövdenin yapısı değişebilir.
Ödünç kaydının uye alanı düz bir kimlik dizgisiyken bir ada da ihtiyaç duyulduğunda nesneye
dönüşür, tek isbn alanı çok kalemli ödünçler gerektiğinde diziye dönüşür, iadeTarihi
alanının adı sonTarih olur.
Bu değişikliklerin ortak özelliği, hâlihazırda çalışan istemcileri bozmalarıdır. Sürümleme, bozmayı önlemez; eski sözleşmenin yürürlükte kalmasını sağlar. Bu ders üç sürümleme biçemini aynı sunucuda yan yana çalıştırır ve ikisini ölçer: istemci kodunun değişen satır sayısı ve aracıların sürümü ayırt edip edemediği.
Aynı Kaynağın İki Sürümü
Aşağıdaki sunucu tek bir ödünç kaydını iki sürümde sunar. Sürümler aynı veriden türetilir; farklı olan yalnızca dışa verilen gövdenin biçimidir. Üç biçem de aynı süreçte çalışır.
// sunucu.mjs — ayni odunc kaydini iki surumde, uc ayri surumleme bicemiyle yayimlar import { createServer } from "node:http"; const KAYIT = { id: "O-1", uye: "U-1001", ad: "Ayse Yilmaz", isbn: "978-0262033848", sonTarih: "2026-03-20" }; // v1: duz yapi, tek kitap. v2: uye nesnesi, kalem dizisi, alan adi degismis. const SURUMLER = { 1: (k) => ({ id: k.id, uye: k.uye, isbn: k.isbn, iadeTarihi: k.sonTarih }), 2: (k) => ({ id: k.id, uye: { kimlik: k.uye, ad: k.ad }, kalemler: [{ isbn: k.isbn }], sonTarih: k.sonTarih }), }; const ORTAM_TIPI = "application/vnd.kutuphane.odunc+json"; // Accept basligindan surum parametresini cikarir: ...+json;surum=2 const acceptSurumu = (accept) => { const m = /surum=(\d+)/.exec(accept ?? ""); return m ? Number(m[1]) : null; }; const yaz = (yanit, surum, ekBaslik) => { yanit.writeHead(200, { "content-type": `${ORTAM_TIPI}; surum=${surum}`, ...ekBaslik }); yanit.end(JSON.stringify(SURUMLER[surum](KAYIT))); }; createServer((istek, yanit) => { yanit.sendDate = false; const yol = istek.url.split("?")[0]; // 1) Yol tabanli: surum adresin parcasi. Iki surum iki ayri kaynak adresidir. const m = /^\/v(\d+)\/odunc\/(.+)$/.exec(yol); if (m && SURUMLER[m[1]]) return yaz(yanit, Number(m[1]), {}); if (yol === "/odunc/O-1") { // 2) Icerik tabanli: surum ortam turunun parametresi. Vary: Accept gerekir. const kabul = acceptSurumu(istek.headers.accept); if (kabul && SURUMLER[kabul]) return yaz(yanit, kabul, { vary: "Accept" }); // 3) Baslik tabanli: surum ayri bir baslikta. Vary: o baslik. const baslik = Number(istek.headers["api-surum"] ?? 1); if (SURUMLER[baslik]) return yaz(yanit, baslik, { vary: "Api-Surum" }); } yanit.writeHead(404, { "content-type": "application/problem+json; charset=utf-8" }); yanit.end(JSON.stringify({ type: "https://ornek.kutuphane/sorunlar/kaynak-yok", title: "Kaynak bulunamadı", status: 404, detail: `${yol} yok.`, instance: "ol-0001" })); }).listen(8434, "127.0.0.1", () => console.log("sunucu 127.0.0.1:8434"));
#!/usr/bin/env bash # Ayni kaydin iki surumunu uc ayri surumleme bicemiyle ister. node sunucu.mjs & sunucu=$! sleep 0.5 echo "--- yol tabanli ---" curl -sS -w ' [%{content_type}]\n' http://127.0.0.1:8434/v1/odunc/O-1 curl -sS -w ' [%{content_type}]\n' http://127.0.0.1:8434/v2/odunc/O-1 echo "--- baslik tabanli ---" curl -sS -H 'Api-Surum: 1' -w ' [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1 curl -sS -H 'Api-Surum: 2' -w ' [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1 echo "--- icerik tabanli ---" curl -sS -H 'Accept: application/vnd.kutuphane.odunc+json;surum=1' -w ' [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1 curl -sS -H 'Accept: application/vnd.kutuphane.odunc+json;surum=2' -w ' [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1 kill "$sunucu"; wait "$sunucu" 2>/dev/null
sunucu 127.0.0.1:8434
--- yol tabanli ---
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"} [application/vnd.kutuphane.odunc+json; surum=1]
{"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ayse Yilmaz"},"kalemler":[{"isbn":"978-0262033848"}],"sonTarih":"2026-03-20"} [application/vnd.kutuphane.odunc+json; surum=2]
--- baslik tabanli ---
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"} [application/vnd.kutuphane.odunc+json; surum=1]
{"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ayse Yilmaz"},"kalemler":[{"isbn":"978-0262033848"}],"sonTarih":"2026-03-20"} [application/vnd.kutuphane.odunc+json; surum=2]
--- icerik tabanli ---
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"} [application/vnd.kutuphane.odunc+json; surum=1]
{"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ayse Yilmaz"},"kalemler":[{"isbn":"978-0262033848"}],"sonTarih":"2026-03-20"} [application/vnd.kutuphane.odunc+json; surum=2]
Üç biçem de aynı iki gövdeyi üretir. Fark, sürümün nereye yazıldığındadır: yol tabanlı
biçemde adrese, başlık tabanlı biçemde ayrı bir isteğe özgü başlığa, içerik tabanlı biçemde
Accept başlığındaki ortam türünün parametresine.
İçerik tabanlı biçem, HTTP’nin kendi içerik pazarlığı düzeneğini kullanır. Sunucunun
döndürdüğü Content-Type da sürümü taşır, yani yanıt kendi sürümünü bildirir. Başlık
tabanlı biçemde bu bilgi ayrı bir başlıktadır ve HTTP’nin bildiği bir kavram değildir; işe
yaraması için istemci ile sunucunun aynı başlık adı üzerinde anlaşması gerekir.
İstemci Kodunun Maliyeti
Biçem seçiminin ilk gerekçesi genellikle “istemcinin daha az kod değiştirmesi” diye sunulur. Bu, ölçülebilir bir iddiadır. Aşağıdaki program üç çağrı yeri olan bir istemcinin kaynağını üç biçem için üretir ve iki senaryoda değişen satır sayısını sayar: bütün çağrı yerlerinin v2’ye geçmesi ve yalnız birinin geçmesi.
// istemci-maliyeti.mjs — uc surumleme bicemi icin istemci kodunun degisen satir sayisi // Uc cagri yeri, uc senaryo: [1,1,1] hepsi v1 | [2,2,2] tam gecis | [2,1,1] kismi gecis // Sabit adlari kararli tutulur: cogunlugun surumu ana sabitte kalir, azinlik icin sabit eklenir. const TIP = "application/vnd.kutuphane.odunc+json"; const YOLLAR = [["oduncGetir ", "(id)", "/odunc/${id}"], ["oduncListesi", "() ", "/odunc"], ["uyeGetir ", "(id)", "/uyeler/${id}"]]; const cogunluk = (s) => (s.filter((v) => v === 1).length >= 2 ? 1 : 2); const cagri = (k, adres, ek) => `export const ${k[0]} = ${k[1]} => iste(\`${adres}\`${ek});`; const BICEMLER = { "yol tabanlı": (s) => { const c = cogunluk(s), ad = (v) => (v === c ? "TABAN" : `TABAN_V${v}`); return [ `const TABAN = "http://127.0.0.1:8434/v${c}";`, ...[...new Set(s)].filter((v) => v !== c).map((v) => `const TABAN_V${v} = "http://127.0.0.1:8434/v${v}";`), ...YOLLAR.map((k, i) => cagri(k, "${" + ad(s[i]) + "}" + k[2], "")), ]; }, "başlık tabanlı": (s) => { const c = cogunluk(s), ad = (v) => (v === c ? "SURUM" : `SURUM_V${v}`); return [ 'const TABAN = "http://127.0.0.1:8434";', `const SURUM = { "api-surum": "${c}" };`, ...[...new Set(s)].filter((v) => v !== c).map((v) => `const SURUM_V${v} = { "api-surum": "${v}" };`), ...YOLLAR.map((k, i) => cagri(k, "${TABAN}" + k[2], `, ${ad(s[i])}`)), ]; }, "içerik tabanlı": (s) => { const c = cogunluk(s), ad = (v) => (v === c ? "SURUM" : `SURUM_V${v}`); return [ 'const TABAN = "http://127.0.0.1:8434";', `const SURUM = { accept: "${TIP};surum=${c}" };`, ...[...new Set(s)].filter((v) => v !== c).map((v) => `const SURUM_V${v} = { accept: "${TIP};surum=${v}" };`), ...YOLLAR.map((k, i) => cagri(k, "${TABAN}" + k[2], `, ${ad(s[i])}`)), ]; }, }; const degisen = (onceki, sonraki) => sonraki.filter((x) => !onceki.includes(x)).length; console.log("biçem satır tam geçiş kısmi geçiş"); for (const [ad, f] of Object.entries(BICEMLER)) { const v1 = f([1, 1, 1]); console.log(`${ad.padEnd(16)} ${String(v1.length).padStart(5)} ` + `${String(degisen(v1, f([2, 2, 2]))).padStart(10)} ${String(degisen(v1, f([2, 1, 1]))).padStart(12)}`); } for (const ad of ["yol tabanlı", "başlık tabanlı"]) { console.log(`\n— kısmi geçişte ${ad} istemci —`); for (const s of BICEMLER[ad]([2, 1, 1])) console.log(s); }
biçem satır tam geçiş kısmi geçiş
yol tabanlı 4 1 2
başlık tabanlı 5 1 2
içerik tabanlı 5 1 2
— kısmi geçişte yol tabanlı istemci —
const TABAN = "http://127.0.0.1:8434/v1";
const TABAN_V2 = "http://127.0.0.1:8434/v2";
export const oduncGetir = (id) => iste(`${TABAN_V2}/odunc/${id}`);
export const oduncListesi = () => iste(`${TABAN}/odunc`);
export const uyeGetir = (id) => iste(`${TABAN}/uyeler/${id}`);
— kısmi geçişte başlık tabanlı istemci —
const TABAN = "http://127.0.0.1:8434";
const SURUM = { "api-surum": "1" };
const SURUM_V2 = { "api-surum": "2" };
export const oduncGetir = (id) => iste(`${TABAN}/odunc/${id}`, SURUM_V2);
export const oduncListesi = () => iste(`${TABAN}/odunc`, SURUM);
export const uyeGetir = (id) => iste(`${TABAN}/uyeler/${id}`, SURUM);
Üç biçem de aynı sayıları veriyor: tam geçişte bir satır, kısmi geçişte iki. Beklenen sonuç bu değildi; yol tabanlı sürümlemenin daha pahalı olduğu yaygın bir kanıdır. Kanının kaynağı, adresin çağrı yerlerine dağılmış olduğu varsayımıdır. Adres tek bir sabitte toplandığında sürüm de o sabitte toplanır ve maliyet başlıktakiyle eşitlenir.
Ölçümün gösterdiği asıl şey şudur: sürüm bilgisi istemcide kaç yerde duruyorsa maliyet odur, biçem değil. Üç biçemde de tek bir yerde durabiliyorsa üçü de aynı fiyattadır. Bu durumda seçim, istemci kodunun dışında bir gerekçeye dayanmak zorundadır.
Aracıların Gördüğü
O gerekçe, isteğin istemci ile sunucu arasındaki yolda geçtiği aracılardadır. Aşağıdaki
program küçük bir önbellektir ve iki kiple çalışır: anahtarı yalnız adresten üreten kip ve
yanıttaki Vary başlığında bildirilen istek başlıklarını da anahtara katan kip.
// onbellek.mjs — sunucunun onunde duran kucuk bir onbellek // Kullanim: node onbellek.mjs adres (anahtar yalniz istek adresi) // node onbellek.mjs vary (anahtar adres + yanittaki Vary'de bildirilen basliklar) import { createServer } from "node:http"; const KIP = process.argv[2] ?? "adres"; const YUKARI = "http://127.0.0.1:8434"; const kayitlar = new Map(); // anahtar -> { govde, tip } const varyBilgisi = new Map(); // adres -> yanitin Vary'sinde bildirilen baslik adlari createServer(async (istek, yanit) => { yanit.sendDate = false; const istekBasliklari = { accept: istek.headers.accept ?? "", "api-surum": istek.headers["api-surum"] ?? "" }; const anahtarla = (adlar) => istek.url + adlar.map((a) => `|${a}=${istekBasliklari[a]}`).join(""); const adlar = KIP === "vary" ? (varyBilgisi.get(istek.url) ?? []) : []; const bulunan = kayitlar.get(anahtarla(adlar)); if (bulunan) { yanit.writeHead(200, { "content-type": bulunan.tip, "x-onbellek": "isabet" }); return yanit.end(bulunan.govde); } const cevap = await fetch(YUKARI + istek.url, { headers: istekBasliklari }); const govde = await cevap.text(); const tip = cevap.headers.get("content-type"); const vary = (cevap.headers.get("vary") ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean); varyBilgisi.set(istek.url, vary); kayitlar.set(anahtarla(KIP === "vary" ? vary : []), { govde, tip }); yanit.writeHead(200, { "content-type": tip, "x-onbellek": "iska" }); yanit.end(govde); }).listen(8435, "127.0.0.1", () => console.log(`onbellek 127.0.0.1:8435 kip=${KIP}`));
#!/usr/bin/env bash # Onbellegin arkasindan: once v2 isteyen istemci, sonra v1 isteyen istemci. node sunucu.mjs & s=$! sleep 0.4 sor() { # sor <etiket> <curl-argumanlari...> printf '%-34s ' "$1"; shift curl -sS -D /tmp/b -o /tmp/g "$@" printf '%-9s surum=%s %s\n' \ "$(grep -i '^x-onbellek' /tmp/b | tr -d '\r' | cut -d' ' -f2)" \ "$(grep -io 'surum=[0-9]' /tmp/b | head -1 | cut -d= -f2)" \ "$(head -c 46 /tmp/g)" } for kip in adres vary; do node onbellek.mjs "$kip" & o=$! sleep 0.4 echo "== onbellek kipi: $kip ==" echo "-- baslik tabanli (tek adres) --" sor "v2 isteyen istemci" -H 'Api-Surum: 2' http://127.0.0.1:8435/odunc/O-1 sor "v1 isteyen istemci" -H 'Api-Surum: 1' http://127.0.0.1:8435/odunc/O-1 echo "-- yol tabanli (iki adres) --" sor "v2 isteyen istemci" http://127.0.0.1:8435/v2/odunc/O-1 sor "v1 isteyen istemci" http://127.0.0.1:8435/v1/odunc/O-1 kill "$o"; wait "$o" 2>/dev/null done kill "$s"; wait "$s" 2>/dev/null
sunucu 127.0.0.1:8434
onbellek 127.0.0.1:8435 kip=adres
== onbellek kipi: adres ==
-- baslik tabanli (tek adres) --
v2 isteyen istemci iska surum=2 {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci isabet surum=2 {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
-- yol tabanli (iki adres) --
v2 isteyen istemci iska surum=2 {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci iska surum=1 {"id":"O-1","uye":"U-1001","isbn":"978-0262033
onbellek 127.0.0.1:8435 kip=vary
== onbellek kipi: vary ==
-- baslik tabanli (tek adres) --
v2 isteyen istemci iska surum=2 {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci iska surum=1 {"id":"O-1","uye":"U-1001","isbn":"978-0262033
-- yol tabanli (iki adres) --
v2 isteyen istemci iska surum=2 {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci iska surum=1 {"id":"O-1","uye":"U-1001","isbn":"978-0262033
Kritik satır, adres kipindeki ikinci satırdır. Api-Surum: 1 isteyen istemci önbellek
isabeti almış ve kendisine v2 gövdesi dönmüştür: uye alanı düz bir kimlik yerine
nesnedir. Önbellek yalnız adrese baktığı için iki isteği aynı istek saymış, ilk isteğin
yanıtını ikinciye vermiştir. Bu sessiz bir tutarsızlıktır — istemci beklediği yapıyı
alamaz ve hiçbir hata yanıtı üretilmez; durum kodu 200’dür. Yol tabanlı biçemde aynı
senaryo iki ayrı ıska verir; iki adres iki kayıttır, karışma olanağı yoktur.
vary kipinde başlık tabanlı biçem de doğru çalışır. Yani sorun biçemde değil, aracının
Vary başlığını doğru işleyip işlemediğindedir. Ama karar verirken önemli olan budur:
başlık ve içerik tabanlı biçemler, doğruluk için yol üzerindeki her aracının Vary
başlığını doğru işlemesine bağımlıdır; yol tabanlı biçem hiçbir aracıdan bir şey
beklemez. Aynı gerekçe kütükler, ölçüm panoları ve hata ayıklama için de geçerlidir: yol
tabanlı biçemde hangi sürümün ne kadar çağrıldığı adres kütüğünden okunur, diğerlerinde
başlık kaydedilmemişse okunamaz.
İçerik tabanlı biçem bu ölçekte başlık tabanlı biçemle aynı davranır, ama bir üstünlüğü
vardır: Accept başlığı HTTP’nin tanıdığı bir başlıktır ve önbelleklerin Vary: Accept
ile karşılaşması olağandır. Kendi uydurduğu bir başlık adına göre değişen bir yanıt,
aracılar açısından daha az beklenen bir durumdur.
Neyin Sürümlendiği
Üç biçemin hiçbiri, sürümün neye iliştiği sorusunu yanıtlamaz. İki seçenek vardır. API bütünüyle sürümlenirse tek bir sürüm numarası bütün kaynakları kapsar; bir kaynaktaki kırıcı değişiklik hepsinin sürümünü yükseltir ve hiçbir şeyi değişmemiş istemciler de yeni sürüme geçmek zorunda kalır. Kaynak başına sürümlenirse her kaynak kendi hızında ilerler, ama istemcinin izlemesi gereken sürüm sayısı kaynak sayısı kadar olur ve yanıtlar arasındaki bağlantılar hangi sürüme gideceğini ayrıca belirtmek zorunda kalır.
Seçim, kaynakların birbirine ne kadar bağlı olduğuna bakar: ödünç kaydı üyeye ve kitaba gömülü alanlarla bağlıysa üçünü ayrı sürümlemek, tutarsız bileşimlere yol açar. Tasarım Sistemleri kursundaki tek paket ile bileşen başına paket seçimi aynı ödünleşimin başka bir alandaki karşılığıdır.
Özet
- Sürümleme kırıcı değişikliği önlemez; eski sözleşmenin yürürlükte kalmasını sağlar, yani iki sözleşme bir süre yan yana yaşar.
- Yol, başlık ve içerik tabanlı biçemler aynı iki gövdeyi üretir; fark sürümün adrese, ayrı bir başlığa ya da ortam türünün parametresine yazılmasıdır.
- İstemci kodunun maliyeti üç biçemde de aynıdır — tam geçişte bir satır, kısmi geçişte iki; belirleyici olan biçem değil, sürüm bilgisinin istemcide kaç yerde durduğudur.
- Gerçek fark aracılardadır: adres anahtarlı bir önbellek, başlık ve içerik tabanlı biçemlerde v1 isteyen istemciye v2 gövdesi döndürür ve hiçbir hata üretmez.
- Başlık ve içerik tabanlı biçemler doğruluk için yol üzerindeki her aracının
Varybaşlığını işlemesine bağımlıdır; yol tabanlı biçem hiçbir aracıdan bir şey beklemez. - Sürümün API’ye mi kaynağa mı iliştiği ayrı bir karardır ve kaynakların birbirine bağlılığına göre verilir.
Sonraki Adım
Bu ders iki sürümü yan yana yayımladı ama sürüm numarasının neden ikiye çıktığını
sormadı: uye alanının nesneye dönmesi, isbn alanının diziye dönmesi ve iadeTarihi
adının değişmesi kırıcıdır diye kabul edildi. Peki sube alanına yeni bir seçenek eklemek
kırıcı mıdır? Yanıtı boş bırakılabilen bir alanı zorunlu yapmak? Yanıta yeni bir alan
eklemek? Bu soruların yanıtı sezgiye bırakıldığında her ekip başka yanıt verir ve sürüm
numarası tartışma konusu olur. Sonraki ders iki şemayı karşılaştırıp kırıcı değişiklikleri
saptayan bir betik yazar ve sürüm numarasını tartışmadan, farktan türetir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.