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.
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 dk | 21.546 | %2,2 | 6.372 | %2,4 |
| 1-3 dk | 2.115 | %2,5 | 389 | %1,8 |
| 3-5 dk | 265 | %4,9 | 25 | %0,0 |
| 5-7 dk | 119 | %44,5 | 16 | %31,3 |
| 7-10 dk | 147 | %83,7 | 16 | %50,0 |
| 10-15 dk | 106 | %98,1 | 13 | %84,6 |
| 15-30 dk | 191 | %100 | 18 | %100 |
| 30+ dk | 707 | %100 | 49 | %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şluk | Soğuk, fonksiyon boşluğuna göre | Soğuk, ortam boşluğuna göre | Bu kadar boşluktan sonra yeniden kullanılan ortam |
|---|---|---|---|
| 5-7 dk | %44,5 | %30,8 | 119 |
| 7-10 dk | %83,7 | %69,1 | 55 |
| 10-15 dk | %98,1 | %95,4 | 5 |
| 15-30 dk | %100 | %100 | 0 |
| 30+ dk | %100 | %100 | 0 |
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 istek | Fonksiyon | Çağrı | Soğuk | Soğuk başlangıç başına istek |
|---|---|---|---|---|
| 1’in altı | 26 | 83 | %78,3 | 1,3 |
| 1-3 | 15 | 196 | %63,3 | 1,6 |
| 3-10 | 18 | 604 | %45,9 | 2,2 |
| 10-30 | 9 | 1.196 | %32,4 | 3,1 |
| 30-100 | 7 | 3.181 | %15,5 | 6,5 |
| 100-300 | 1 | 1.124 | %1,6 | 62 |
| 300 üstü | 2 | 18.890 | %2,2 | 45 |
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.549 | 6.905 |
| Soğuk başlangıç oranı | %7,9 | %3,8 |
| İstek başına soğuk başlangıç maliyeti (oran × init p50) | 94 ms | 53 ms |
| Süre p50 | 71 ms | 72 ms |
| Süre p90 | 488 ms | 250 ms |
| Süre p99 | 3.264 ms | 3.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:
| Domain | Soğuk önce | Soğuk sonra | İstek başına maliyet önce | İstek başına maliyet sonra |
|---|---|---|---|---|
| fleet | %12,7 | %6,6 | 154 ms | 80 ms |
| driver | %9,4 | %6,8 | 110 ms | 87 ms |
| auth | %17,0 | %12,1 | 200 ms | 153 ms |
| orders | %20,4 | %3,9 | 243 ms | 70 ms |
| notification | %0,8 | %0,5 | 10 ms | 6 ms |
| tracking | %11,4 | %14,5 | 132 ms | 181 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ış | Fonksiyon | Soğuk başlangıç | Init p50 | Init p90 |
|---|---|---|---|---|
| 1,0-1,5 MB, tek amaçlı | 42 | 1.615 | 1.209 ms | 1.289 ms |
| 1,5-2 MB, tek amaçlı | 8 | 93 | 1.282 ms | 1.402 ms |
| 2-3 MB, tek amaçlı | 2 | 24 | 1.275 ms | 1.330 ms |
| 1,6-2,3 MB, beş router | 5 | 192 | 1.246-1.333 ms | 1.312-1.425 ms |
| 5,5 MB, customers router | 1 | 30 | 1.835 ms | 1.930 ms |
| 5,8 MB, orders router | 1 | 39 | 1.909 ms | 2.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 istek | k | Tek amaçlı filo, soğuk | Router’lar, soğuk | Gü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 |
| 1M | 10 | %2,9 | %2,2 | %1,1 |
| 10M | 100 | %2,3 | %2,2 | %0,1 |
| 100M | 1.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:
| Durum | Bö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ımda | Böl |
| Kota ve soğuk kuyruk birlikte | Grupla, 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 istek | Bağlam | Karar |
|---|---|---|
| yaklaşık 20’nin üstü | herhangi | Fonksiyona dokunmayın; tabanın birkaç puan yakınında |
| 2-20 | paylaşılan IAM kapsamı, tek deploy ritmi | Bir 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 concurrency | Fonksiyonu tutun ve oranı kabul edin ya da gecikmeye duyarlıysa provisioned concurrency ekleyin (taktikler) |
| herhangi | tek bir endpoint domain’i zaten taşıyor | Havuzlama soğuk başlangıçta bir şey kazandırmıyor; IAM ve deploy gerekçesiyle karar verin |
| herhangi | router 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 |
| herhangi | aynı 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#
- Understanding the Lambda execution environment lifecycle (yeni sekmede açılır) - Init, Invoke ve Shutdown evreleri, 10 saniyelik Init sınırı ve AWS’nin ortamların “for some time” tutulup birkaç saatte bir yenilendiğine dair ifadesi. Tipik soğuk başlangıç oranının %1’in altında olduğu cümlesini, bastırılmış init notunu ve INIT faturalama bağlantısını da taşıyor.
- Log and monitor Node.js Lambda Event Functions (yeni sekmede açılır) -
REPORTsatırının alanları;Init Durationneyi ölçer ve ne zaman görünür. - Supported logs and discovered fields in CloudWatch Logs Insights (yeni sekmede açılır) - Logs Insights’ın Lambda için otomatik keşfettiği alanlar ve soğuk başlangıçların neden
@messageiçinden parse edildiği. - Logging and metrics for AWS Lambda (AWS Prescriptive Guidance) (yeni sekmede açılır) - Fonksiyon örneği başına bir log stream ve stream adlandırma biçimi; ortamları
@logStreamile saymanın dayanağı. - CloudWatch Logs Insights
limitcommand (yeni sekmede açılır) - Bir sorgunun döndürdüğü varsayılan ve en fazla olay sayısı. - CloudWatch Logs Insights
statscommand (yeni sekmede açılır) - İki sorguda kullanıldığı haliylecount,count_distinctvepct;count_distinct’in yalnızca çok yüksek kardinalitede yaklaşık olduğu notuyla. - Metrics collected by Lambda Insights (yeni sekmede açılır) -
init_durationmetriği veLambdaInsightsad alanının geri kalanı. - Powertools for AWS Lambda (TypeScript): Metrics (yeni sekmede açılır) -
logMetricsmiddleware’i ve decorator’ı üzerindekicaptureColdStartMetricileColdStartmetriği. - Customize HTTP API access logs (yeni sekmede açılır) - Proxy route arkasında endpoint başına gecikme için
$context.path,$context.integrationLatencyve$context.routeKey. - Quotas for configuring and running an HTTP API in API Gateway (yeni sekmede açılır) - API başına route (artırılabilir), API başına entegrasyon (artırılamaz) ve domain başına çok seviyeli API mapping için 200 sınırı (tek seviyeli mapping’ler sayılmıyor); konsolidasyonu zorlayan kota ve gateway bölmenin uyması gereken kota.
- Amazon API Gateway quotas (yeni sekmede açılır) - HTTP, REST ve WebSocket API’ler arasında Region başına paylaşılan hesap düzeyindeki throttle kotası.
- Map API stages to a custom domain name for HTTP APIs (yeni sekmede açılır) - Tek bir custom domain’in base path’leri birden fazla HTTP API’ye yönlendirmesi; gateway’i bölme alternatifi.
- AWS Lambda standardizes billing for INIT Phase (yeni sekmede açılır) - Ağustos 2025’ten beri on-demand yönetilen runtime fonksiyonlarında faturalanan INIT süresi; bundle boyutunun para cinsinden maliyetine dair kısa notun dayanağı.
- AWS Lambda: Tek Amaçlı Fonksiyonlar mı, Lambdalith mi? - Tek amaçlı fonksiyonların yapısal gerekçesi ve burada ölçülen istisna kriterleri.
- Tek Amaçlı Lambda’ları Hono Lambdalith’e Route Route Taşımak - Burada izlenen route route geçiş sırası; o yazının router’ı her handler’ın çekirdeğini ortak bir fonksiyona çıkarırken buradaki router mevcut handler’ı çağırıyor.
- Init Amortismanına Göre Kod Mimarisi - Modül kapsamındaki init’in neyden oluştuğu; 1,2 saniyelik tabanın yaşadığı yer.
- AWS Lambda Cold Start Optimizasyonu: Production Dersleri - Soğuk başlangıca duyarlı kalması gereken fonksiyonlar için provisioned concurrency ve init hijyeni.
İlgili yazılar
AWS Lambda fonksiyonlarını nasıl bölmeli: varsayılan single-purpose, single-domain Lambdalith'i hak edilmiş istisna gör, kararı veren platform güçlerini tanı.
lambda · serverless · architecture +2
DI container'lar, monolitik SDK'lar, god-handler'lar, modül üstü secret çağrıları ve ağır ORM'ler: soğuk başlatmada bedeli ve yerine geçen fonksiyonel yapı.
lambda · typescript · serverless +2
Bun ve Deno'yu AWS Lambda'da custom runtime ile çalıştırma: performans benchmark'ları, maliyet analizi ve production deployment pattern'leri.
lambda · javascript · serverless +2
Production ortamlarından öğrenilen AWS Lambda cold start optimizasyon stratejileri. Runtime seçimi, provisioned concurrency ve pratik optimizasyon teknikleri.
lambda · serverless · cold-start +3
Bir Lambda filosu Middy'nin statik middleware modelini ne zaman aşar, projeye özel bir motor istek başına konfigürasyonu nasıl çözer, bakımı neye mal olur
lambda · middleware · performance +6