İçeriğe atla

Lambda Soğuk Başlangıçlarını Bundle Boyutu Değil, Boşluk Süresi Belirler

289'dan 44'e inen bir Lambda konsolidasyonunun prod logları: soğuk başlangıç oranını boşluk süresi belirliyor, bundle boyutu MB başına yaklaşık 150 ms; sorgular dahil.

Ayhan Sipahi Ayhan Sipahi

Bir Lambda fonksiyonunun soğuk başlangıç (cold start) oranını, fonksiyonun kendi çağrıları arasında ne kadar boş beklediği belirler. Bundle boyutu ise her soğuk başlangıcın fiyatını belirler ve oranı yerinden oynatmaz; aşağıdaki veride bu fiyat, 1,2 saniyelik bir tabanın üzerine sıkıştırılmamış MB başına yaklaşık 150 ms ekliyor. Yaygın tavsiye ters yönü gösterir: fonksiyonları küçük tutun ki hızlı başlasınlar. Bu tavsiye trafikten hiç söz etmez; oysa burada her şeyi belirleyen değişken trafikti.

Bir filo yönetimi platformu 289 tek amaçlı Node.js Lambda’sını 44 fonksiyonda topladı: domain başına bir Hono router, public endpoint’ler ayrı. Geçişten önce, log grubu olan 246 fonksiyonun 168’i bir hafta boyunca sıfır çağrı almıştı. Günde bir istekten az alan fonksiyonlar, aldıkları çağrıların %78’inde soğuktu. Konsolidasyonu zorlayan şey HTTP API’nin API başına 300 entegrasyon kotasıydı. Bu yüzden geçişin iki yanındaki loglar, genellikle kulaktan dolma bilgiyle tartışılan bir soru için aynı sistem üzerinde, eşleşen hafta içi pencereleriyle bir öncesi-sonrası karşılaştırması veriyor.

Lambda üzerinde onlarca ya da yüzlerce HTTP endpoint işleten bir TypeScript ekibinin birleştirme kararı için dört şeye ihtiyacı var: ölçülen boşluk eğrisi, fonksiyon başına trafik eğrisi, bundle boyutunun milisaniye cinsinden maliyeti ve aynı sayıları herhangi bir hesapta üreten iki Logs Insights sorgusu ile bir script. Dördü de aşağıda. Birleştirip birleştirmeme kararının kendisi AWS Lambda: Tek Amaçlı Fonksiyonlar mı, Lambdalith mi? yazısında verilmişti; buradaki sayılar o yazının ilk istisna kriterine bir değer koyuyor.

Aşağıdaki her sayının arkasındaki kurulum: production, eu-north-1, arm64 üzerinde Node.js 24, 1024 MB, production için minify edilmiş esbuild ESM bundle’ları; @aws-sdk/* runtime tarafından sağlandığı için external bırakılmış. Geçiş öncesi veri seti, arşivlenmiş log gruplarından okunan, tek amaçlı fonksiyon başına yedi günlük REPORT satırları. Geçiş sonrası veri seti, trafik taşıyan yedi domain router’ının 1,0 ile 1,6 günlük REPORT satırları. Eşleşen pencere karşılaştırmaları, bir hafta öncesinin aynı hafta içi saatlerini kullanıyor. Geçiş, Tek Amaçlı Lambda’ları Hono Lambdalith’e Route Route Taşımak yazısındaki route route sırayı izledi; router’da tek bir fark var: her handler’ın çekirdeğini ortak bir fonksiyona çıkarmak yerine Hono, endpoint’i domain’in route tablosundan seçip mevcut handler’ı orijinal API Gateway event’iyle çağırıyor. Handler kodu değişmedi. Public, kimlik doğrulamasız endpoint’ler kendi fonksiyonlarında kaldı; böylece hiçbir zaman bir router’ın IAM rolü altında çalışmıyorlar.

Boşluk Eğrisinin Gösterdiği#

Her çağrı için aynı fonksiyonun bir önceki çağrısından bu yana geçen dakikayı alın, bir aralığa yerleştirin ve o aralıktaki çağrıların Init Duration taşıyan payını sayın. Bu, fonksiyonun boşluğu; bir önceki isteği hangi ortam sunmuş olursa olsun. Ortamın kendi boş bekleme süresi en az bu kadar ve aşağıdaki ikinci tablo onu doğrudan ölçüyor. Geçiş öncesi sütunları trafik taşıyan 78 tek amaçlı fonksiyonu, geçiş sonrası sütunları yedi router’ı havuzluyor.

Önceki çağrıdan bu yana geçen boşlukÇağrı (önce)Soğuk (önce)Çağrı (sonra)Soğuk (sonra)
0-1 dk21.546%2,26.372%2,4
1-3 dk2.115%2,5389%1,8
3-5 dk265%4,925%0,0
5-7 dk119%44,516%31,3
7-10 dk147%83,716%50,0
10-15 dk106%98,113%84,6
15-30 dk191%10018%100
30+ dk707%10049%100

Yürütme ortamı kısa boşluklarda hayatta kaldı ve 5 ile 10 dakikalık boşluk arasında bir yerde öldü. 15 dakikaya gelindiğinde ise her seferinde kapanmış oluyordu. Geçiş sonrası sütunları aynı şekli taşıyor; ancak 5-15 dakika satırlarında satır başına yalnızca 13 ile 16 örnek var. Bu yüzden bu sütunlar şekli doğruluyor, kesinlik eklemiyor.

Fonksiyon boşluğu, bir önceki isteği sunan ortamın ne kadar boş beklediği için bir alt sınır: eşzamanlılık varsa o istek başka bir ortama gitmiş olabilir. Aynı geçiş öncesi satırlarını log stream’e göre yeniden okuyup her sıcak çağrıyı kendi ortamının boş bekleme süresine göre kovalayınca aşağıdaki ikinci sütun çıkıyor. Üçüncü sütun doğrudan sağkalım sayısı: bu kadar boşluktan sonra bir ortamı yeniden kullanan sıcak çağrılar.

BoşlukSoğuk, fonksiyon boşluğuna göreSoğuk, ortam boşluğuna göreBu kadar boşluktan sonra yeniden kullanılan ortam
5-7 dk%44,5%30,8119
7-10 dk%83,7%69,155
10-15 dk%98,1%95,45
15-30 dk%100%1000
30+ dk%100%1000

Ortam görünümü 5-10 dakika satırlarını yaklaşık 14 puan aşağı çekiyor, uçları yerinde bırakıyor. 23.483 sıcak çağrı içinde bir ortamın yeniden kullanıldığı en uzun boşluk 10,3 dakika; 10 dakikanın üzerinde beş yeniden kullanım var, 15’in üzerinde hiç yok. Tek amaçlı fonksiyonların çoğu aynı beş dakika içinde iki ortamı hiç etkin tutmadı (trafiği olan 78 fonksiyonun 54’ü). Router’lar aynı anda iki ile altı ortam çalıştırdı; logları aynı yöntemle yeniden okununca 5-7 dakikada %18, 7-10’da %42, 10-15’te %78, yeniden kullanılan en uzun boşluk 10,9 dakika ve 15’in ötesinde hiçbir şey çıkıyor.

5 dakikanın altındaki %2’lik taban, boş beklemeden başka bir mekanizmadan geliyor. Tek sıcak ortam meşgulken gelen ikinci bir eşzamanlı istek yeni bir ortam açtırıyor; buna AWS’nin belgelediği önceden başlatma ekleniyor: “Lambda might occasionally initialize execution environments ahead of invocation requests”, yani Lambda ara sıra çağrı gelmeden önce de yürütme ortamı açabiliyor. Konsolidasyon bu tabanı kaldıramıyor. Günde 3.484 istek alan notification router’ı %0,5’te oturuyordu.

AWS’nin kendi ifadesi, Lambda’nın yürütme ortamını “for some time in anticipation of another function invocation” tuttuğu, yani bir sonraki çağrıyı bekleyerek bir süre ayakta bıraktığı yönünde. Yukarıdaki eğri bu ifadenin arkasındaki sayı; bir hesap, bir bölge ve bir dönem için. Tekrarlanması gereken bir ölçüm. AWS ayrıca trafikten bağımsız olarak “Lambda terminates execution environments every few hours to allow for runtime updates and maintenance” diyor, yani ortamları runtime güncellemeleri ve bakım için birkaç saatte bir sonlandırıyor; dokümantasyonda 5-10 dakikalık pencereyi vaat eden hiçbir şey yok.

Oranı Fonksiyon Başına Trafik Belirler#

Çağrı sayısını soğuk başlangıç sayısına bölünce soğuk başlangıç başına istek çıkıyor; bu, buradaki en sezgisel sayı. Geçişten önce en sessiz fonksiyonlarda bir soğuk başlangıç 1,3 isteğe hizmet ediyordu. Geçişten sonra trafiği havuzlayan beş router’da bir soğuk başlangıç 8 ile 26 isteğe hizmet etti; domain’ini tek bir endpoint’in zaten taşıdığı iki router 7 ve 200’de kaldı. Tablo, log grubu olan 246 tek amaçlı fonksiyonu kapsıyor; 168’i o hafta sıfır çağrı aldığı için tablonun dışında.

Fonksiyon başına günlük istekFonksiyonÇağrıSoğukSoğuk başlangıç başına istek
1’in altı2683%78,31,3
1-315196%63,31,6
3-1018604%45,92,2
10-3091.196%32,43,1
30-10073.181%15,56,5
100-30011.124%1,662
300 üstü218.890%2,245

Geçiş sonrası router’lar aynı eğrinin üzerinde duruyor: günde 118 istek %14,5 soğuk verdi, 184 istek %11,7, 238 istek %12,1, 297 istek %6,6, 787 istek %6,8, 964 istek %3,9 ve 3.484 istek %0,5.

100-300 satırının 300 üstü satırı geçmesinin nedeni de aynı noktayı pekiştiriyor. O tek fonksiyon, istemcilerin bir iş sürerken sıkı aralıklarla sorguladığı, işler arasında ise hiç çağırmadığı bir iş durumu endpoint’i: boşlukları p90’da bir dakikanın altında, uçta 41 saate kadar çıkıyordu. Her iş bir soğuk başlangıç ödedi ve arkasından yaklaşık 60 sorgu sıcak geçti. O satırın ölçtüğü şey günlük istek değil, soğuk başlangıç başına istek. Çağrıların düzenliliği, sayıları kadar önemli.

Bu tablodan okununca konsolidasyon bir trafik havuzlama hamlesi. Günde 118 ile 3.484 istek alan yedi router, günde 0,1 ile 2.100 istek alan 78 fonksiyonun yerini aldı. Kodun hiçbir yanı ısınmadı; aynı fonksiyonun çağrıları arasındaki boşluklar kısaldı.

Eşleşen Pencerelerde Ne Değişti#

Geçiş sonrası pencereye karşı bir hafta öncesinin aynı hafta içi saatleri; yedi domain havuzlanmış:

MetrikÖnce (eşleşen)Sonra
Çağrı5.5496.905
Soğuk başlangıç oranı%7,9%3,8
İstek başına soğuk başlangıç maliyeti (oran × init p50)94 ms53 ms
Süre p5071 ms72 ms
Süre p90488 ms250 ms
Süre p993.264 ms3.283 ms

p50 yerinde duruyor, p90 yarıya iniyor. Tablo Duration ölçüyor, yani handler’ın kendi süresini; soğuk bir istek bunun üstüne Init Duration’ını ekliyor. p90’daki hareket soğuk payının aritmetiği: geçişten önce soğuk başlangıçlar isteklerin %7,9’uydu, dolayısıyla en üst onluk dilim neredeyse tamamen soğuktu ve p90 hemen altında duruyordu; geçişten sonra %3,8 ile en üst dilim çoğunlukla sıcak istek. Medyan iki pencerede de soğuk başlangıçla karşılaşmıyor. İstek karışımındaki bir değişim de p90’ı oynatabilir (orders geçiş sonrası %48 daha fazla trafik taşıdı); bu yüzden yarıya inmeyi oran düşüşünün kanıtı olarak değil, onunla tutarlı bir sonuç olarak okuyun.

Domain bazında soğuk başlangıç oranı ve istek başına soğuk maliyet şöyle hareket etti:

DomainSoğuk önceSoğuk sonraİstek başına maliyet önceİstek başına maliyet sonra
fleet%12,7%6,6154 ms80 ms
driver%9,4%6,8110 ms87 ms
auth%17,0%12,1200 ms153 ms
orders%20,4%3,9243 ms70 ms
notification%0,8%0,510 ms6 ms
tracking%11,4%14,5132 ms181 ms

İstek başına maliyet, her pencerenin soğuk başlangıç oranı çarpı o pencerenin init p50’si. Orders p90’da da hareket etti: 1.123 ms’den 510 ms’ye. Customers domain’inin eşleşen pencere tabanı yok, çünkü bir hafta önceki aynı dilimde sıfır istek vardı; eşleşmemiş rakamları, geçiş öncesi yedi güne karşı daha kısa geçiş sonrası pencere, %41,0’dan %11,7’ye indi.

İki domain yerinden oynamadı. Tracking’in on altı fonksiyonundan biri domain’in tüm trafiğini taşıyordu; notification’ın iki sıcak fonksiyonu ise zaten eşzamanlılık tabanındaydı. Tek bir endpoint domain’i zaten taşıyorsa havuzlanacak bir şey kalmıyor. Tracking yüzdelerinin arkasındaki 10’a karşı 25 soğuk başlangıç da herhangi bir yönde değişim ilan edemeyecek kadar az.

Orders’ın p50 kayması, eşleşen pencerelerde 109 ms’den 166 ms’ye, açıklanamadan kalıyor. Diğer altı domain’in p50’si yerinden oynamadı; bu, istek başına bir router vergisine karşı bir kanıt. Geçiş sonrası pencere %48 daha fazla orders trafiği taşıdı; bu da endpoint karışımının değiştiği yönünde bir kanıt. REPORT satırında endpoint yok, dolayısıyla veri bunu çözemiyor; ölçüm bölümündeki access log yaklaşımı çözerdi.

Konsolidasyonla iki yan etki geldi. Toplam sıkıştırılmış kod 89,5 MB’den 16,2 MB’ye, deploy başına paketlenen fonksiyon sayısı 308’den 63’e düştü; çünkü her tek amaçlı fonksiyon paylaşılan kodun ve middleware’in kendi kopyasını bundle’lıyordu. Tipik deploy süresi, CI iş sürelerine göre, dev’de 18,4 dakikadan 10,3 dakikaya, prod’da 15,0 dakikadan 8,6 dakikaya indi.

Bundle Boyutunun Milisaniye Cinsinden Maliyeti#

En az beş soğuk başlangıcı olan fonksiyonlar için sıkıştırılmamış bundle boyutuna karşı init p50:

Bundle, sıkıştırılmamışFonksiyonSoğuk başlangıçInit p50Init p90
1,0-1,5 MB, tek amaçlı421.6151.209 ms1.289 ms
1,5-2 MB, tek amaçlı8931.282 ms1.402 ms
2-3 MB, tek amaçlı2241.275 ms1.330 ms
1,6-2,3 MB, beş router51921.246-1.333 ms1.312-1.425 ms
5,5 MB, customers router1301.835 ms1.930 ms
5,8 MB, orders router1391.909 ms2.020 ms

İki şey görünüyor. 1,2 saniye civarında, byte’ların açıklamadığı bir taban var: 1,0 MB’lik bir tek amaçlı fonksiyon da 2,3 MB’lik bir router da bu tabanın yaklaşık 0,1 saniye yakınına iniyor. Tabanın üzerinde sıkıştırılmamış MB başına kabaca 150 ms’lik bir eğim var; iki ağır router’ı 1,8 ve 1,9 saniyeye taşıyan bu eğim. Eğimi iki router belirliyor; dolayısıyla bu, byte ile init işi arasında bu filoya özgü bir korelasyon, bir sabit değil. Modül sayısı ve import anında çalışan kod byte’larla birlikte hareket ediyor ve burada ayrıştırılmadı. Taban, init evresinin işi: runtime, SDK istemcileri ve modül kapsamında çalışan ne varsa. Bu işin neyden oluştuğu ve nasıl amorti edileceği Init Amortismanına Göre Kod Mimarisi yazısının konusu; yukarıdaki tablo yalnızca boyutunu ölçüyor.

İki ağır router, endpoint’lerinin yalnızca birkaçının kullandığı rapor üretimi (PDF, Excel) ve ödeme kodunu taşıyor. Fiyat MB başına olduğu için çözüm, bu modülleri statik import grafiğinin dışında tutup ilk kullanımda yüklemek. Eğime göre bu, router’ın init’ini diğer beş router’ın 1,3 saniyelik bandına doğru geri getirmeli; bu bir tahmin, bu veri setinden bir ölçüm değil:

// reports.ts: PDF yığınını ilk kullanımda yükle, init'e dahil etme
let pdf: typeof import('./pdf.js') | undefined

export async function renderPdf(input: ReportInput) {
  pdf ??= await import('./pdf.js')
  return pdf.render(input)
}

Kazanç dosyayı küçültmekten değil, modül değerlendirmesini ertelemekten geliyor. Code splitting olmadan esbuild dinamik import edilen modülü aynı bundle’ın içinde tutup ilk kullanımda çalışacak şekilde sarıyor; sıkıştırılmamış MB yerinde kalıyor, init işi yer değiştiriyor.

Ağır endpoint’e gelen ilk istek import’u bir kez ödüyor; sonraki her soğuk başlangıç bu adımı atlıyor. O modüle ait modül kapsamındaki tekil nesnelerin de import tamamlandıktan sonra oluşturulması gerekiyor.

Bellek aynı şekli izledi ve yeniden boyutlandırmak için bir neden vermiyor: p50 Max Memory Used tek amaçlı fonksiyonlarda 202 MB, iki ağır router’da yaklaşık 330 MB; hepsi yapılandırılan 1024 MB’nin çok altında.

Tek Amaçlı Fonksiyon Gerekçesinin Hâlâ Geçerli Olduğu Yerler#

Granülerlik yazısındaki yapısal gerekçelere bu veri dokunmuyor; üstelik ikisi bu geçişi şekillendirdi.

IAM kapsamı. Bir router, endpoint’lerinin izinlerinin birleşimiyle çalışır. Public endpoint’lerin kendi fonksiyonlarında kalmasının nedeni bu: hiçbir zaman bir router’ın rolü altında çalışmıyorlar.

Endpoint başına gözlemlenebilirlik. REPORT satırları endpoint’i kaybetti; yukarıdaki açıklanamayan orders p50’si bu maliyetin görünür hali. Ölçüm bölümü geri dönüşün iki yolunu veriyor: metriklerde bir route boyutu ya da API Gateway access log’ları.

Hata etki alanı ve ortak deploy. Kötü bir deploy artık bir endpoint yerine bir domain’i düşürüyor. Version ve alias’lar maruz kalmayı azaltıyor; ortadan kaldırmıyor.

Route başına reserved ya da provisioned concurrency. Route’lar bir fonksiyonu paylaştığı anda ikisi de kullanılamıyor. Bu domain’lerin hiçbirinin buna ihtiyacı yoktu; ama tek bir patlamalı route’u olan bir domain o route’u ayrı tutmalı, ki bu, granülerlik yazısının anlattığı hibrit yapı.

Tek amaçlı yapının sattığı düğmeler, endpoint başına bellek ve reserved concurrency, bu filoda hiç çevrilmedi: her fonksiyon 1024 MB’de çalıştı ve hiçbir domain reserved concurrency ayırmadı; granülerliğin bedeli kimsenin kullanmadığı seçenekler için ödendi.

Ölçülen iddia bu kısıtların içinde duruyor. Endpoint başına trafik inceyken ve IAM kapsamı paylaşılıyorken havuzlama soğuk başlangıçta, paket boyutunda ve deploy süresinde kazanıyor. Tek bir endpoint domain’i zaten taşıyorsa havuzlama bunların hiçbirinde bir şey kazandırmıyor.

On Kat ve Bin Kat Trafikte Ne Değişir#

Eğri, ölçek sorusunu yeni veri gerektirmeden yanıtlıyor. Soğuk başlangıç oranı fonksiyon başına bir trafik özelliği olduğu için her fonksiyonun trafiğini k ile çarpın, her birini yukarıdaki günlük istek eğrisinden okuyun ve istek sayısıyla ağırlıklandırın. Ölçülen filo ayda yaklaşık 0,1M istekte duruyor.

Aylık istekkTek amaçlı filo, soğukRouter’lar, soğukGünde 30’un altındaki endpoint’lerden gelen istek
0,1M (ölçülen)1%7,1%2,1 tahmin, %3,8 gözlenen%8,2
1M10%2,9%2,2%1,1
10M100%2,3%2,2%0,1
100M1.000%2,2%2,2%0,0

Havuzlama kazancı ayda yaklaşık 1M istekte büyük ölçüde, 10M’de tamamen bitiyor. Tekdüze büyüme altında tek amaçlı filo, router’ların zaten oturduğu tabana ulaşıyor. Tabanda kalan (burada yaklaşık %2) ölçek büyütme ve önceden başlatma; bunları hiçbir topoloji kaldırmıyor.

Tabanda denge tersine dönüyor. Her ölçek büyütme soğuk başlangıcı router’ın bundle’ını ödüyor: ağır router’larda 1,9 saniye, ince bir tek amaçlı fonksiyonda 1,2 saniye. O noktada havuzlama lehine boşluk argümanı bitmiş, aleyhine MB başına argüman hâlâ sürüyor. Önce lazy import, ağır endpoint’leri router’dan ayırmak ikinci sırada.

Havuzlanmış trafik parçalarından daha pürüzsüz; bu yüzden bir router’ın istek başına, endpoint’lerinin toplamından daha az ölçek büyütme ortamına ihtiyaç duyması gerekir. Bu teori; bu veri seti bunu gösteremeyecek kadar küçük.

Para, kısaca: INIT süresi Ağustos 2025’ten beri on-demand yönetilen runtime fonksiyonlarında faturalanıyor. Ayda 100M istek, %2,2 soğuk ve 1 GB’de 0,7 saniye ek init ile aradaki fark ayda onlarca dolar. Bundle boyutu, maliyeti etkilemeden çok önce gecikmeyi etkiler.

Trafikle büyüyen maliyetler bir önceki bölümdekiler. Tek bir bellek boyutu hem PDF üretimine hem 70 ms’lik bir GET’e hizmet ediyor. Tek bir eşzamanlılık havuzu, bir endpoint’in patlamasının kardeşlerini throttle etmesi demek. Tek bir deploy domain’in bütün ortamlarını aynı anda yeniliyor. Yüksek eşzamanlılıkta bu, domain çapında bir gecikme olayı; tek amaçlı yerleşim ise onu tek bir endpoint’le sınırlıyor.

Tablonun yanına iki uyarı gerekiyor. Tekdüze büyüme bir varsayım; gerçek büyüme önce sıcak endpoint’lere düşer, bu da kuyruğu daha uzun süre soğuk tutar. Fonksiyon başına günde 300 isteğin üzerinde hiçbir şey ölçülmedi; alt satırlardaki %2,2, bir varsayım altında yeniden okunan ölçülmüş bir nokta. Tabanı belirleyen patlamalılık; hacim tek başına onu oynatmıyor. AWS’nin kendi tipik rakamı daha da düşük: “Cold starts typically occur in under 1% of invocations.” (soğuk başlangıçlar tipik olarak çağrıların %1’inden azında gerçekleşir).

Gateway’i Bölmek Başka Bir Problemi Çözer#

Bu kotaya verilen yaygın yanıt API’yi bölmek; bu yanıt doğrudan bir karşılaştırmayı hak ediyor. HTTP API kotaları şöyle: API başına route 300 (artırılabilir); API başına entegrasyon 300 (sabit); custom domain başına çok seviyeli API mapping 200 (sabit); Region başına custom domain adı 120 (artırılabilir). Bir tek amaçlı Lambda bir entegrasyon demek; dolayısıyla bir route artışı, tek bir HTTP API’nin 300’den fazla tek amaçlı fonksiyon tutmasını sağlamıyor.

Bu hesapta route artışı vardı ve geçiş yine de yapıldı. HTTP API başına route sayısı ilk dalgadan önce, deploy sırasına alan açmak için, Service Quotas üzerinden 400’e çıkarıldı. CloudFormation yeni proxy route’larını açık route’ları silmeden önce oluşturuyor ve 293 route’taki bir API 300’ün altında yerinde takas yapamıyor. Artış, deploy sırasını mümkün kıldı. Prod API 287 entegrasyonda duruyordu ve entegrasyon kotası Service Quotas’ta hiç görünmüyor. Geçişten sonra aynı API’de 137 route ve 44 entegrasyon var; route’ların 40’ı {proxy+}.

300 entegrasyonun ötesinde iki çıkış var. Tek bir custom domain arkasında API mapping’lerle domain başına bir API (api.example.com/orders orders API’sine gider) her fonksiyonu korur. Router’lar üzerinden daha az entegrasyon ise tek API’yi korur. Geçiş ikincisini seçti; gerçek alternatif birincisi.

Bölmek, tek amaçlı yapının gateway’de kazandırdığı her şeyi korur: route başına JWT authorizer scope’ları, route başına throttling, API başına stage’ler, domain başına access log ve kota. {proxy+} route’u altında bunlar router’ın koduna taşınır; özellikle scope’lar, router token claim’lerini kendisi kontrol etmedikçe domain başına tek bir kümeye iner.

Bölmek kuyruğa dokunmaz. 168 sıfır trafikli fonksiyon ve günde bir isteğin altındaki %78 soğuk oranı fonksiyon başına özellikler; öne konan bir gateway daha ikisini de değiştirmez.

Bölmenin maliyeti domain başına bir stack ve bir API mapping; /orders gibi tek seviyeli mapping’ler 200’lük çok seviyeli sınıra sayılmıyor. Hesap düzeyindeki throttle Region’daki her API arasında paylaşıldığından bölmek throughput için bir kazanç da sağlamaz. İkisini birden yapmak, domain başına bir API ve domain başına bir router, maksimum izolasyon yerleşimi; ek stack’lere yalnızca domain’ler farklı authorizer ya da throttle gerektirdiğinde değer.

İki karar kısa bir tabloya iniyor:

DurumBöl mü, grupla mı
Yalnızca kota, endpoint’ler sıcakİkisi de olur; kotada eşit
Soğuk kuyruk, paylaşılan IAM kapsamıGrupla
Route başına gateway kontrolleri kullanımdaBöl
Kota ve soğuk kuyruk birlikteGrupla, ayrıcalıklı route’ları ayrı tut

Bu geçişin zamanlamasını kota belirledi; yanıtını trafik eğrisi.

Kendi Eğrinizi Ölçün#

Yukarıdaki her şey REPORT satırından geliyor; ücretsiz ve agent gerektirmiyor. AWS’nin belgelediği keşfedilen Lambda alanları listesinde init alanı yok (@requestId, @duration, @billedDuration, @type, @maxMemoryUsed ve @memorySize). Belgelenmemiş bir @initDuration alanı çözümleniyor ve bir router’da count(@initDuration) aşağıdaki parse ile birebir aynı sayıyı verdi; buradaki sorgular yine de @message içinden parse ediyor, çünkü bu hiçbir belgelenmemiş şeye bağlı değil. Init Duration orada yaşıyor; AWS’nin tanımıyla, runtime’ın fonksiyonu yükleyip handler dışındaki kodu çalıştırması için geçen süre (“the amount of time it took the runtime to load the function and run code outside of the handler method”). Her yürütme ortamı kendi log stream’ine yazıyor (“A separate log stream is created in the log group for each Lambda function instance”), dolayısıyla ortamı @logStream tanımlıyor.

Sorgu 1 fonksiyon başına soğuk başlangıç oranını, init’i ve ortam sayısını veriyor. Konsolda fonksiyon log gruplarını ya da bir ön eki seçin.

filter @type = "REPORT"
| parse @message /Init Duration: (?<init>[\d.]+) ms/
| stats count(*) as invocations,
        count(init) as coldStarts,
        count_distinct(@logStream) as environments,
        pct(init, 50) as initP50,
        pct(@duration, 50) as p50,
        pct(@duration, 90) as p90
  by @log
| sort invocations desc

invocations / coldStarts, soğuk başlangıç başına istek. environments ile coldStarts’ın eşit olması, her yeni stream’in tam olarak bir init olduğunun kontrolü; count_distinct bu kardinalitede kesin, yalnızca çok yüksek kardinalitede yaklaşık ve kontrol bu veri setindeki her fonksiyonda tuttu. Bir production router’ında üç günlük pencerede sorgu 1.471 çağrı, 80 soğuk başlangıç, 80 ayrı stream, init p50 1.878 ms, p50 142 ms ve p90 586 ms döndürdü.

Sorgu 2 her ortamın ne kadar yaşadığını ve ne kadar hizmet verdiğini gösteriyor.

filter @type = "REPORT"
| stats count(*) as invocations,
        min(@timestamp) as firstSeen,
        max(@timestamp) as lastSeen
  by @logStream
| sort invocations desc
| limit 50

Aynı router’da en yoğun ortam 67 dakikada 99 çağrıya hizmet etti; bir patlama sırasında dört ortam aynı saniyede oluşturuldu, bu %2’lik tabanın tek satırlık hali. Logs Insights varsayılan olarak en fazla 10.000, limit ile en fazla 100.000 satır döndürüyor. İki sorgu eşleşen her olayı topladığı için bu sınır yalnızca ham REPORT satırlarını dışa aktarırken önemli; aşağıdaki script bunu CLI üzerinden yapıyor.

Boşluk eğrisi bir script istiyor. Logs Insights’ta önceki satırın değeri için autoregress var, ama belgelenen söz dizimi log grupları arasında fonksiyon başına bölümleme sunmuyor ve kovalanmış histogram yerelde tek bir döngü. Script iki boşluk eğrisini de yazdırıyor: fonksiyon boşluğu ve stream adından türetilen ortam boşluğu, bir de bir ortamın yeniden kullanıldığı en uzun boşluk. Sorgular da script de varsayılan metin log biçimini varsayıyor; JSON log biçiminde rapor farklı alan adlarıyla bir JSON nesnesi olduğundan @type = "REPORT", regex ve CLI filter pattern hiçbir şeyle eşleşmiyor ve hiçbir şey raporlamıyor. Script, REPORT olaylarını log grubu başına AWS CLI üzerinden çekiyor (sayfalamayı CLI yapıyor, npm bağımlılığı yok), sonra fonksiyon başına oranı, ortamları ve soğuk başlangıç başına isteği, bir de havuzlanmış boşluk eğrisini yazdırıyor. analyze() fonksiyonu arşivlenmiş veri setinden yukarıdaki her tabloyu yeniden üretti.

#!/usr/bin/env node
// cold-gaps.cjs: Lambda REPORT satırlarından hesaplanan soğuk başlangıç oranı,
// ortam başına çağrı sayısı, iki yoldan boşluk eğrisi (fonksiyon başına ve yürütme
// ortamı başına) ve ortam sağkalımı. npm bağımlılığı yok; PATH üzerinde, log
// gruplarına okuma erişimi olan AWS CLI gerekir.
//
// kullanım: node cold-gaps.cjs --region eu-north-1 --days 7 /aws/lambda/fn-a /aws/lambda/fn-b
const { execFileSync } = require('node:child_process')

const MIN = 60_000
const GAP_BUCKETS = [[0, 1], [1, 3], [3, 5], [5, 7], [7, 10], [10, 15], [15, 30], [30, Infinity]]

// Çağrı başına bir satır: zaman damgası, Init Duration (sıcaksa null), yürütme ortamı.
function collect(logGroup, region, fromMs, toMs) {
  const out = execFileSync(
    'aws',
    ['logs', 'filter-log-events', '--region', region, '--log-group-name', logGroup,
      '--start-time', String(fromMs), '--end-time', String(toMs),
      '--filter-pattern', '"REPORT RequestId"', '--output', 'json'],
    { maxBuffer: 1 << 29 },
  )
  return JSON.parse(out).events
    .map((e) => {
      const init = e.message.match(/Init Duration: ([\d.]+) ms/)
      return { t: e.timestamp, init: init ? Number(init[1]) : null, env: e.logStreamName }
    })
    .sort((a, b) => a.t - b.t)
}

function analyze(rowsByFn) {
  const perFn = Object.entries(rowsByFn).map(([fn, rows]) => {
    const cold = rows.filter((r) => r.init !== null)
    const inits = cold.map((r) => r.init).sort((a, b) => a - b)
    return {
      fn,
      invocations: rows.length,
      coldStarts: cold.length,
      coldPct: rows.length ? (100 * cold.length) / rows.length : null,
      environments: new Set(rows.map((r) => r.env)).size,
      invPerColdStart: cold.length ? rows.length / cold.length : null,
      initP50: inits[Math.floor(inits.length / 2)] ?? null,
    }
  })
  // Boşluk eğrisi iki yoldan, tüm fonksiyonlar üzerinden havuzlanmış. Fonksiyon boşluğu:
  // AYNI fonksiyonun bir önceki çağrısından bu yana geçen dakika, hangi ortam sunmuş olursa
  // olsun. Ortam boşluğu: aynı ortamın (log stream'in) bir önceki çağrısından bu yana geçen
  // dakika. Soğuk başlangıcın ortam geçmişi yoktur; iki eğride de fonksiyon boşluğunda sayılır.
  const gaps = GAP_BUCKETS.map(([lo, hi]) => ({ gapMin: `${lo}-${hi === Infinity ? '' : hi}`, n: 0, cold: 0, envN: 0, envCold: 0, envReused: 0 }))
  const bucket = (g) => gaps[GAP_BUCKETS.findIndex(([lo, hi]) => g >= lo && g < hi)]
  const survival = { longestIdleMinReused: 0, reusesAfter10Min: 0 }
  for (const rows of Object.values(rowsByFn)) {
    const lastByEnv = new Map()
    for (let i = 0; i < rows.length; i++) {
      const r = rows[i]
      if (i > 0) {
        const fnGap = (r.t - rows[i - 1].t) / MIN
        const envGap = r.init === null && lastByEnv.has(r.env) ? (r.t - lastByEnv.get(r.env)) / MIN : fnGap
        const bf = bucket(fnGap), be = bucket(envGap)
        bf.n++
        be.envN++
        if (r.init !== null) {
          bf.cold++
          be.envCold++
        } else {
          be.envReused++
          if (envGap > survival.longestIdleMinReused) survival.longestIdleMinReused = envGap
          if (envGap >= 10) survival.reusesAfter10Min++
        }
      }
      lastByEnv.set(r.env, r.t)
    }
  }
  return {
    perFn,
    gaps: gaps.map((g) => ({ ...g, coldPct: g.n ? (100 * g.cold) / g.n : null, envColdPct: g.envN ? (100 * g.envCold) / g.envN : null })),
    survival,
  }
}

if (require.main === module) {
  const args = process.argv.slice(2)
  const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args.splice(i, 2)[1] : d }
  const region = opt('--region', process.env.AWS_REGION ?? 'eu-north-1')
  const days = Number(opt('--days', 7))
  const to = Date.now(), from = to - days * 86_400_000
  const rowsByFn = Object.fromEntries(args.map((g) => [g.replace('/aws/lambda/', ''), collect(g, region, from, to)]))
  const { perFn, gaps, survival } = analyze(rowsByFn)
  const r1 = (v) => (v === null ? '-' : v.toFixed(1))
  console.table(perFn.map((r) => ({ ...r, coldPct: r1(r.coldPct), invPerColdStart: r1(r.invPerColdStart) })))
  console.table(gaps.map((g) => ({ gapMin: g.gapMin, n: g.n, coldPct: r1(g.coldPct), envN: g.envN, envColdPct: r1(g.envColdPct), envReused: g.envReused })))
  console.log(`longest idle gap an environment was reused after: ${survival.longestIdleMinReused.toFixed(1)} min; reuses after 10+ min: ${survival.reusesAfter10Min}`)
}

module.exports = { analyze, collect }

Eğriden çok bir dashboard sayısı önemliyse Powertools for AWS Lambda (TypeScript) tek satırlık bir middleware ile EMF üzerinden ColdStart metriği yayıyor. Lambda Insights ise kendi init’ini getiren ücretli bir extension olarak init_duration metriğini ekliyor. İkisi de boşluk eğrisini vermiyor; ikisi de bir alarmın izleyebileceği bir sayı veriyor. Parçada routerHandler, router’ın Lambda handler’ı; Hono’da handle(app). Powertools sıcak çağrılarda ColdStart için 0 yaymadığından bir oran, Invocations metriğine karşı metric math gerektiriyor.

import { Metrics } from '@aws-lambda-powertools/metrics'
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware'
import middy from '@middy/core'

const metrics = new Metrics({ namespace: 'fleet', serviceName: 'orders' })

export const handler = middy(routerHandler).use(logMetrics(metrics, { captureColdStartMetric: true }))

Konsolidasyondan sonra endpoint’i geri almanın iki yolu var. Router’ın içinde eşleşen route’u bir metrik boyutu ya da yapılandırılmış log alanı olarak ekleyin; granülerlik yazısının yeniden kurulabilir gözlemlenebilirlik kriteri tam olarak bunu istiyor. Router’ın dışında $context.path ve $context.integrationLatency ile HTTP API access log’larını açın; bunlar koda dokunmadan {proxy+} arkasında path başına gecikme veriyor. $context.routeKey proxy route’unu gösterdiğinden endpoint’i taşıyan alan $context.path; bu alan ID’leri içeren somut path olduğundan şablonlanmış bir biçim üzerinden gruplayın ve metotları ayırmak için $context.httpMethod ekleyin. Access log’larını geçişten önce açın ki iki pencerede de bulunsun.

Eğriyi Yanlış Okumanın Yolları#

7 günlük bir pencereyi 1 günlük bir pencereyle karşılaştırmak. Ham rapor orders p50’sini geçiş öncesi 70 ms, geçiş sonrası 166 ms gösteriyordu; hafta içi saatleri eşleştirmek geçiş öncesi rakamı 109 ms’ye çekti. Her zaman aynı hafta içi saatleri karşılaştırın.

İki basamaklı bir sayımdan yüzde vermek. Tracking’in geçiş sonrası %14,5’i, geçiş öncesindeki 10’a karşı 25 soğuk başlangıç. Değişim yok deyin ve sayıları gösterin.

Zaman damgalarına bakmadan bir domain’in soğuk başlangıçlarını deploy’a yüklemek. Geçiş sonrası pencere içindeki tam production deploy’u router başına tam olarak bir soğuk başlangıca mal oldu, 6.905 çağrıda 7. Tracking’in oranının bununla ilgisi yoktu; zaman damgaları bunu bir dakikada gösterdi.

Çağrı hatalarından sonra soğuk başlangıçları saymak. Bir çökme ya da timeout sonrası bastırılmış init, Init evresini raporlamadan yeniden çalıştırıyor; ek süre Init Duration’a değil Duration’a yazılıyor, bu yüzden hata yoğun bir fonksiyon soğuk başlangıçlarını eksik sayıyor. Telemetry API bunları evresi invoke olan init olayları olarak raporluyor; REPORT satırı raporlamıyor.

Soğuk Başlangıç Başına İsteğe Göre Karar Kuralları#

Granülerlik yazısının varsayılanı yerinde duruyor: tek amaçlı fonksiyonlar, hak edilmiş istisna olarak tek domain’li bir router. İlk istisna kriterinin artık arkasında bir sayı var: Sorgu 1’den fonksiyon başına, soğuk başlangıç başına istek.

Soğuk başlangıç başına istekBağlamKarar
yaklaşık 20’nin üstüherhangiFonksiyona dokunmayın; tabanın birkaç puan yakınında
2-20paylaşılan IAM kapsamı, tek deploy ritmiBir domain router’ında havuzlayın; oranın router’ın trafik seviyesine doğru düşmesini bekleyin
2’nin altıizole IAM ihtiyacı ya da reserved concurrencyFonksiyonu tutun ve oranı kabul edin ya da gecikmeye duyarlıysa provisioned concurrency ekleyin (taktikler)
herhangitek bir endpoint domain’i zaten taşıyorHavuzlama soğuk başlangıçta bir şey kazandırmıyor; IAM ve deploy gerekçesiyle karar verin
herhangirouter init’i yaklaşık 1,5 s’nin üstündeÖnce ağır modülleri lazy import edin; domain’i bölmek ikinci sırada
herhangiaynı dağılımla ayda kabaca 1M isteğin üstüHavuzlama oranı artık pek oynatmıyor; kota, IAM, deploy ve bundle gerekçesiyle karar verin

Varsayılan, tek bir endpoint’in domain’ini zaten taşıdığı ya da her endpoint’in günde birkaç yüz istek gördüğü yerde geçerli; orada havuzlama hiçbir şeyi değiştirmedi ve kararı IAM kapsamı, ortak deploy ve bundle boyutu tek başına veriyor. İstisna ince kuyruk: IAM kapsamını ve deploy ritmini paylaşan, günde kabaca 30 isteğin altındaki endpoint’ler; orada domain başına bir router soğuk başlangıç oranını ve istek başına soğuk maliyeti yarıya indirdi, havuzlanmış p50’ye dokunmadı. Herhangi bir hesap için sonraki adım, son yedi gün üzerinde soğuk başlangıç başına isteğe göre sıralanmış Sorgu 1.

Kaynaklar#

İlgili yazılar