Serverless DeepDive #12:Telemetry APIで遅延の区間を探る
正常実行、初期化失敗、handler例外とresetを比べ、Telemetry APIで遅延の区間と観測の限界を探ります。
Series: Serverless DeepDive (13 articles)
- …
- 13Serverless DeepDive #12:Telemetry APIで遅延の区間を探る This article
遅延を観測する
requestやevent単位で、処理が実行時間の上限に収まるとき、Lambdaが候補になります。
実行環境の起動や、失敗後の再初期化をAWSへ任せられるためです。
Invoke中の再初期化は、REPORTのDurationにも含まれます。
業務が完了したかは利用者のコードが記録します。
常駐processの寿命を自分で制御したい用途は別の実行基盤を考えます。
実行環境の起動や、失敗後の再初期化をAWSへ任せられるためです。
Invoke中の再初期化は、REPORTのDurationにも含まれます。
業務が完了したかは利用者のコードが記録します。
常駐processの寿命を自分で制御したい用途は別の実行基盤を考えます。
REPORTはLambdaが各Invokeの終わりに出す標準log行です。
handlerが約150 msで終わった呼び出しでも、REPORTは約430 msでした。
process終了やtimeoutの後に、初期化がやり直された呼び出しです。
handlerのlogだけを見ていると、この時間を処理の遅さと考えてしまいます。
handlerが約150 msで終わった呼び出しでも、REPORTは約430 msでした。
process終了やtimeoutの後に、初期化がやり直された呼び出しです。
handlerのlogだけを見ていると、この時間を処理の遅さと考えてしまいます。
Extensionは関数と同じ環境で動いてInvoke eventを受ける別processです。
前回の#11ではExtensionの時間と費用を比べました。
今回はTelemetry APIで、初期化・runtime・Extensionの区間を照合します。
Telemetry APIはLambdaの実行イベントをExtensionへ送るAPIです。
数字を集めるだけで、失敗や遅延を説明できるかも確かめます。
前回の#11ではExtensionの時間と費用を比べました。
今回はTelemetry APIで、初期化・runtime・Extensionの区間を照合します。
Telemetry APIはLambdaの実行イベントをExtensionへ送るAPIです。
数字を集めるだけで、失敗や遅延を説明できるかも確かめます。
観測の構成
4つの関数を比べました。
各関数に、#11と同じGoの外部Extensionを置いています。
Extensionは環境内にHTTP listenerを立て、Lambdaが送るTelemetryのイベントを受け取ります。
受け取ったイベントに受信時刻を付け、JSONで標準出力へ出します。
normalは正常実行、initfailはmodule初期化失敗です。handlerfailはhandlerの例外、resetはprocess終了とtimeoutを試します。各関数に、#11と同じGoの外部Extensionを置いています。
Extensionは環境内にHTTP listenerを立て、Lambdaが送るTelemetryのイベントを受け取ります。
受け取ったイベントに受信時刻を付け、JSONで標準出力へ出します。
| 条件 | 設定 |
|---|---|
| 実行方式 | on-demand、同期Invoke、各関数を逐次実行 |
| runtime | Node.js 26、nodejs26.x、実測はv26.8.2 |
| 資源 | arm64、512 MB、timeout 2秒、reserved concurrency 1 |
| module初期化 | 100 ms待つ |
| handler | 正常・例外・process終了は150 ms待つ。timeout入力は2500 ms |
| Extension | Invoke event受信から200 ms待って、次のイベントを要求 |
| Telemetry | schema 2025-01-29、platformとfunction、buffer timeout 25 ms |
| 呼び出し | 比較16回、受信の確認4回、合計20回。SDKの自動retryは0 |

検証時の構成です。各関数にそれぞれ実行環境があります。
紫はLambda platformが生成したTelemetryの配信です。
handlerはstdoutへfunction logを出し、Lambdaが取り込みます。
緑はstdoutと、CloudWatchへの標準log・受信logの出力です。
Extension自身のlog streamは購読していません。
中継したlogを再び受けるloopを避けるためです。
紫はLambda platformが生成したTelemetryの配信です。
handlerはstdoutへfunction logを出し、Lambdaが取り込みます。
緑はstdoutと、CloudWatchへの標準log・受信logの出力です。
Extension自身のlog streamは購読していません。
中継したlogを再び受けるloopを避けるためです。
正常実行の区間
最初の正常呼び出しでは、初期化が324.607 msでした。
同じ関数の2回目には、新しいInitイベントはありませんでした。
同じ関数の2回目には、新しいInitイベントはありませんでした。
| 区間 | 最初の呼び出し | 2回目 |
|---|---|---|
| handlerが測った時間 | 151.020 ms | 150.517 ms |
platform.runtimeDoneのduration | 155.420 ms | 152.706 ms |
platform.reportのduration | 200.969 ms | 202.002 ms |
extensionOverhead span | 45.186 ms | 49.026 ms |
| Init duration | 324.607 ms | なし |

画面にあるのはInit・runtimeDone・reportです。
表のhandler時間と
event schema reference には、reportのspansの構造が示されています。
このschema資料だけでは
表のhandler時間と
extensionOverheadは保存した原イベントから採りました。extensionOverheadはplatform.reportのrecord.spansにある区間です。event schema reference には、reportのspansの構造が示されています。
このschema資料だけでは
extensionOverheadの開始・終了の定義を確定できませんでした。初回のInit(324.607 ms)は、reportのdurationとは別の
この初回Initは後述するInvoke中の再初期化と同じようには足し引きしません。
init_ms列に載っています。この初回Initは後述するInvoke中の再初期化と同じようには足し引きしません。
Extensionの200 msはhandlerが戻った後からではなく、Invoke eventを受けた直後から待つ設定です。
その待機はhandlerの150 msと重なります。
今回の
この条件では、runtime終了後に残った待機の区間と読んでいます。
監視Extensionの一般的な費用を測った数字ではありません。
今回は、区間を確かめるために意図して待機を入れています。
その待機はhandlerの150 msと重なります。
今回の
extensionOverhead約45〜49 msはExtensionの待機200 msとruntimeDoneとの差に近い値でした。この条件では、runtime終了後に残った待機の区間と読んでいます。
監視Extensionの一般的な費用を測った数字ではありません。
今回は、区間を確かめるために意図して待機を入れています。
REPORTが長いときは、Init・handler・Extensionのどの区間が大きいかを調べます。
初期化の失敗
初期化失敗の関数はmodule読み込みの途中で例外を出します。
最初の呼び出しでは、次の区間が記録されました。
最初の呼び出しでは、次の区間が記録されました。
| 記録 | phase | status | duration |
|---|---|---|---|
platform.initReport | init | error | 385.746 ms |
platform.initReport | invoke | error | 272.707 ms |
platform.runtimeDone | — | error | 0.000 ms |
platform.report | — | error | 294.167 ms |

最初の呼び出しでは、初期化が最初のInitとInvoke中のInitで記録されています。
これは送信側がInvokeを再送した結果ではありません。
今回の2回の比較呼び出しと受信確認の1回は、すべて明示的に送信した別々の呼び出しです。
採取した3回のlogに、handlerのbeginはありませんでした。
これは送信側がInvokeを再送した結果ではありません。
今回の2回の比較呼び出しと受信確認の1回は、すべて明示的に送信した別々の呼び出しです。
採取した3回のlogに、handlerのbeginはありませんでした。
APIの応答はHTTP 200でしたが、
応答のerrorTypeは
名前が違うため、集計では同じ分類として扱いません。
FunctionError=Unhandledでした。応答のerrorTypeは
Error、TelemetryはRuntime.Unknownです。名前が違うため、集計では同じ分類として扱いません。
標準logの
Telemetryの385.746 msと272.707 msを丸めた値です。
この失敗は標準logでも、調べる箇所を初期化へ絞れます。moduleの読み込みから調べます。
INIT_REPORTにも、init 385.75 msとinvoke 272.71 msがありました。Telemetryの385.746 msと272.707 msを丸めた値です。
この失敗は標準logでも、調べる箇所を初期化へ絞れます。moduleの読み込みから調べます。

保存済みの標準log5行を、そのまま転記した実Console画面です。
元のLambda実行時刻ではありません。
最後のREPORT2行は次に見るprocess終了後とtimeout後の正常呼び出しです。
この画面の5行だけでは、元の実行順や他のlogの有無は分かりません。
copied_event_timeは2026年10月11日07:43:47 UTCの転記時刻です。元のLambda実行時刻ではありません。
init_msとduration_msの単位はmsです。空欄は原文にfieldがない行です。INIT_REPORT自体にRequestIdはありません。最後のREPORT2行は次に見るprocess終了後とtimeout後の正常呼び出しです。
この画面の5行だけでは、元の実行順や他のlogの有無は分かりません。
再初期化の区間
#09では、失敗後にglobal stateが初期化される条件を比べました。
今回は、その初期化に使った時間も追います。
今回は、その初期化に使った時間も追います。
| 区間 | process終了の後 | timeoutの後 |
|---|---|---|
| handlerが測った時間 | 150.907 ms | 150.852 ms |
Init phase=invoke | 218.702 ms | 224.641 ms |
platform.runtimeDoneのduration | 153.956 ms | 153.621 ms |
extensionOverhead span | 46.973 ms | 47.921 ms |
| Init+runtimeDone+extensionOverhead | 419.631 ms | 426.183 ms |
platform.reportのduration | 426.136 ms | 432.703 ms |
| 上の合計とreportの差 | 6.505 ms | 6.520 ms |

画面のhandler行の時刻はepoch msで、他の行はISO形式です。
行は時系列順に並んでいません。照合には元の時刻を使っています。
表のruntimeDoneとextensionOverheadは同じRequestIdの原イベントから採りました。
行は時系列順に並んでいません。照合には元の時刻を使っています。
表のruntimeDoneとextensionOverheadは同じRequestIdの原イベントから採りました。
moduleの初期化UUIDは4e845592…→ee343e31…→3173a2b3…へ変わりました。
最初の値はreset関数の最初のhandler logで確認しました。
画面にある2つの値はprocess終了後とtimeout後の値です。
Telemetryには
phaseは最初の
これはsuppressed initと呼ばれる、Invokeに含まれる再初期化です。
実行環境のLifecycle にも、この区間が説明されています。
最初の値はreset関数の最初のhandler logで確認しました。
画面にある2つの値はprocess終了後とtimeout後の値です。
Telemetryには
initStart、initRuntimeDone、initReportがありました。phaseは最初の
initからinvokeへ変わっています。これはsuppressed initと呼ばれる、Invokeに含まれる再初期化です。
実行環境のLifecycle にも、この区間が説明されています。
InitイベントにはRequestIdがありません。
今回は同じ関数を逐次呼び、Initイベントの時刻とphaseを、続く
今回は同じ関数を逐次呼び、Initイベントの時刻とphaseを、続く
platform.startと照合しました。今回の
この差の内訳は測っていません。3区間の和が
reset関数のlog groupから採取した111件のeventに、成功した再初期化の
標準
Telemetryでは、そのreportに含まれたInit 218.702 msと224.641 msを分けて観測できました。
handlerを変える前に初期化のやり直しを調べる根拠になります。
platform.reportのdurationはInit+runtimeDone+extensionOverheadより、6.505 msと6.520 ms長くなりました。この差の内訳は測っていません。3区間の和が
platform.reportのdurationになるとは言い切れません。reset関数のlog groupから採取した111件のeventに、成功した再初期化の
INIT_REPORTはありませんでした。標準
REPORTの426.14 msと432.70 msは、表のreportを小数2桁へ丸めた値です。Telemetryでは、そのreportに含まれたInit 218.702 msと224.641 msを分けて観測できました。
handlerを変える前に初期化のやり直しを調べる根拠になります。
handler例外とstatus
handlerで例外を出した2回も、応答は
一方、今回のTelemetryでは
FunctionError=Unhandledでした。一方、今回のTelemetryでは
runtimeDoneもreportもstatus=successでした。| 判定する記録 | handlerで例外 | 次の正常呼び出し |
|---|---|---|
| Invoke応答のFunctionError | Unhandled | なし |
| handlerのoutcome log | intentional-error | success |
| Telemetry runtimeDoneのstatus | success | success |
| module初期化UUID | 7f763d47… | 7f763d47… |

例外の後も、今回のmodule識別値は変わりませんでした。
初期化の観測結果もprocess終了とtimeoutの後とは違いました。
初期化の観測結果もprocess終了とtimeoutの後とは違いました。
Telemetryのstatusを業務の成功件数へ直接使うのは避けたいところです。
少なくとも今回は、その集計では2回の例外を見落とします。
呼び出し側のFunctionErrorと、handlerが記録する完了条件も照合します。
業務の完了判定には利用者のコードが残す結果を使います。
少なくとも今回は、その集計では2回の例外を見落とします。
呼び出し側のFunctionErrorと、handlerが記録する完了条件も照合します。
業務の完了判定には利用者のコードが残す結果を使います。
イベントの受信時刻
正常関数の4回目は、通常のREPORTが返っています。
しかし、最初に採取したlogには、そのTelemetry reportがありませんでした。
受信確認のために同じ関数をもう1回呼ぶと、4回目のreportが届きました。
しかし、最初に採取したlogには、そのTelemetry reportがありませんでした。
受信確認のために同じ関数をもう1回呼ぶと、4回目のreportが届きました。
| 4回目のreportに残った時刻 | 値 |
|---|---|
Telemetryのtime | 03:01:55.457 UTC |
listenerのreceived_msをUTCへ変換 | 03:03:02.738 UTC |
| 次のhandlerが始まった時刻 | 03:03:02.740 UTC |

正常1〜3回目のreportも次の
受信時刻は03:01:54.768、55.017、55.254 UTCです。
handler例外の関数やresetの途中のreportも、後のInvokeに合わせて受信しました。
reset後には、新しいExtensionの初期化中に前のreportを受けています。
Init失敗の2回も含め、16件すべて生成時刻と受信時刻を分けて記録しました。
platform.startの時刻に受信しました。受信時刻は03:01:54.768、55.017、55.254 UTCです。
handler例外の関数やresetの途中のreportも、後のInvokeに合わせて受信しました。
reset後には、新しいExtensionの初期化中に前のreportを受けています。
Init失敗の2回も含め、16件すべて生成時刻と受信時刻を分けて記録しました。
4関数の比較の最後のreportも、次のInvoke付近で届きました。
生成から受信までの差は約64〜72秒です。配信が遅れた原因は測っていません。
正常4回目の差67.281秒は次のInvoke開始までの間隔です。
network転送だけに67秒かかったと測った数字ではありません。
関数ごとに1回ずつ追加し、比較16回分のreportをすべてRequestIdで照合できました。
その追加4回自身の最後のreportは、次の配信まで採取できていません。
生成から受信までの差は約64〜72秒です。配信が遅れた原因は測っていません。
正常4回目の差67.281秒は次のInvoke開始までの間隔です。
network転送だけに67秒かかったと測った数字ではありません。
関数ごとに1回ずつ追加し、比較16回分のreportをすべてRequestIdで照合できました。
その追加4回自身の最後のreportは、次の配信まで採取できていません。
Telemetry API の
listenerの受信時刻とは別に記録します。
同資料は、bufferや処理能力によってlogがdropされる場合も説明しています。
生成時刻・受信時刻・RequestIdを残すと、実行の遅さと受信の待ち時間を分けられます。
timeは、platformがeventを生成した時刻です。listenerの受信時刻とは別に記録します。
同資料は、bufferや処理能力によってlogがdropされる場合も説明しています。
生成時刻・受信時刻・RequestIdを残すと、実行の遅さと受信の待ち時間を分けられます。
再現する
測定binaryに使ったGoは1.27.1でした。原本の全build flagsは不明です。
共通labは実測に使った非公開の検証ツールです。
実測はinline配備の
共通labは実測に使った非公開の検証ツールです。
setup.py、run.mjs、run-with-deadline.pyは構文確認だけで、AWSでは未実行です。実測はinline配備の
index.jsでした。再構築例はzip内のindex.cjsを使います。ファイルを置く
掲載コードを次のpathへ保存します。
Layerの
Extensionsの説明 にある配置です。
Layerの
extensions/lab-extensionは/opt/extensions/へ展開されて起動します。Extensionsの説明 にある配置です。
1
2
3
4
5
6
7
handlers/extensions/main.go
handlers/extensions/layer/extensions/lab-extension
handlers/telemetry/index.cjs
scenarios/telemetry-events.json
setup.py
run.mjs
run-with-deadline.pyExtensionを登録する
main.goは、INVOKEとSHUTDOWNを登録します。listenerを起動し、登録で得たidentifierでTelemetryを購読してから、Nextを呼びます。
Goのコードでは、受信payloadについてのcomment一行だけ直しました。動作は原測定と同じです。
bufferの仕様 は
1 MiBはこのlistenerのHTTP body上限です。
bufferの仕様 は
2 × maxBytes + metadataBytesを見込むよう説明します。1 MiBはこのlistenerのHTTP body上限です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
var outputMu sync.Mutex
var sequence atomic.Uint64
var initID string
func emit(record map[string]any) {
record["extension_init_id"] = initID
record["function_id"] = os.Getenv("LAB_FUNCTION_ID")
outputMu.Lock()
defer outputMu.Unlock()
_ = json.NewEncoder(os.Stdout).Encode(record)
}
func setting(name string, fallback, minimum, maximum int) int {
value := os.Getenv(name)
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil || parsed < minimum || parsed > maximum {
panic(fmt.Sprintf("invalid non-secret setting %s", name))
}
return parsed
}
func api(client *http.Client, method, endpoint, identifier string, body any) (*http.Response, error) {
var reader io.Reader
if body != nil {
encoded, err := json.Marshal(body)
if err != nil {
return nil, err
}
reader = bytes.NewReader(encoded)
}
request, err := http.NewRequest(method, endpoint, reader)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
if identifier == "" {
request.Header.Set("Lambda-Extension-Name", "lab-extension")
} else {
request.Header.Set("Lambda-Extension-Identifier", identifier)
}
return client.Do(request)
}
func subscribe(client *http.Client, base, identifier string) error {
listener, err := net.Listen("tcp", ":4243")
if err != nil {
return err
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {
writer.WriteHeader(http.StatusMethodNotAllowed)
return
}
var events []struct {
Type string `json:"type"`
Time string `json:"time"`
Record json.RawMessage `json:"record"`
}
// Payload can reach 2*maxBytes plus metadata; this listener caps each HTTP body at 1 MiB.
if err := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1024*1024)).Decode(&events); err != nil {
writer.WriteHeader(http.StatusBadRequest)
return
}
for _, event := range events {
emit(map[string]any{
"phase": "telemetry-event", "type": event.Type, "time": event.Time,
"record": event.Record, "received_ms": time.Now().UnixMilli(),
"seq": sequence.Add(1),
})
}
writer.WriteHeader(http.StatusOK)
})
server := &http.Server{Handler: mux, ReadHeaderTimeout: time.Second}
go func() { _ = server.Serve(listener) }()
response, err := api(client, http.MethodPut, base+"/2022-07-01/telemetry", identifier, map[string]any{
"schemaVersion": "2025-01-29", "types": []string{"platform", "function"},
"buffering": map[string]int{"maxItems": 1000, "maxBytes": 262144, "timeoutMs": setting("EXT_BUFFER_MS", 1000, 25, 30000)},
"destination": map[string]string{"protocol": "HTTP", "URI": "http://sandbox.localdomain:4243"},
})
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("telemetry subscribe HTTP %d", response.StatusCode)
}
emit(map[string]any{"phase": "extension-subscribed", "schema": "2025-01-29"})
return nil
}
func main() {
random := make([]byte, 16)
if _, err := rand.Read(random); err != nil {
panic(err)
}
initID = hex.EncodeToString(random)
started := time.Now().UnixMilli()
base := "http://" + os.Getenv("AWS_LAMBDA_RUNTIME_API")
client := &http.Client{} // Extension Next intentionally has no request timeout.
response, err := api(client, http.MethodPost, base+"/2020-01-01/extension/register", "", map[string]any{"events": []string{"INVOKE", "SHUTDOWN"}})
if err != nil {
panic(err)
}
identifier := response.Header.Get("Lambda-Extension-Identifier")
response.Body.Close()
if response.StatusCode != http.StatusOK || identifier == "" {
panic("extension registration failed")
}
if os.Getenv("EXT_TELEMETRY") == "1" {
if err := subscribe(client, base, identifier); err != nil {
panic(err)
}
}
initMS := setting("EXT_INIT_MS", 100, 0, 500)
time.Sleep(time.Duration(initMS) * time.Millisecond)
emit(map[string]any{"phase": "extension-init", "started_ms": started, "finished_ms": time.Now().UnixMilli(), "requested_ms": initMS})
for {
response, err := api(client, http.MethodGet, base+"/2020-01-01/extension/event/next", identifier, nil)
if err != nil {
panic(err)
}
var event struct {
EventType string `json:"eventType"`
RequestID string `json:"requestId"`
DeadlineMS int64 `json:"deadlineMs"`
ShutdownReason string `json:"shutdownReason"`
}
err = json.NewDecoder(io.LimitReader(response.Body, 16384)).Decode(&event)
response.Body.Close()
if err != nil || response.StatusCode != http.StatusOK {
panic("extension Next failed")
}
if event.EventType == "SHUTDOWN" {
emit(map[string]any{"phase": "extension-shutdown", "reason": event.ShutdownReason, "at_ms": time.Now().UnixMilli()})
return
}
invokeStarted := time.Now().UnixMilli()
postMS := setting("EXT_POST_MS", 150, 0, 500)
markerMS := int64(0)
if os.Getenv("EXT_WAIT_FOR_MARKER") != "0" {
markerPath := "/tmp/lab-extension-" + event.RequestID + ".json"
for time.Now().UnixMilli() < event.DeadlineMS {
data, readErr := os.ReadFile(markerPath)
if readErr == nil {
var marker struct {
PostMS int `json:"post_ms"`
FinishedMS int64 `json:"finished_ms"`
}
if json.Unmarshal(data, &marker) == nil && marker.PostMS >= 0 && marker.PostMS <= 500 {
postMS = marker.PostMS
markerMS = marker.FinishedMS
_ = os.Remove(markerPath)
break
}
}
time.Sleep(5 * time.Millisecond)
}
}
emit(map[string]any{"phase": "extension-post-start", "request_id": event.RequestID, "invoke_started_ms": invokeStarted, "marker_ms": markerMS, "post_started_ms": time.Now().UnixMilli(), "requested_ms": postMS})
time.Sleep(time.Duration(postMS) * time.Millisecond)
emit(map[string]any{"phase": "extension-done", "request_id": event.RequestID, "invoke_started_ms": invokeStarted, "marker_ms": markerMS, "finished_ms": time.Now().UnixMilli(), "requested_ms": postMS})
}
}type/time/recordは受信値、received_ms/seqはExtensionの記録です。並行に受信するため、process内のseqでイベントが発生した順序をすべて決めることはしません。
markerは#11でhandlerの終了を知らせたファイルです。
今回のhandlerはそのファイルを書きません。
markerを待たず、Invoke受信から200 ms待ちます。
handlerで失敗を分ける
index.cjsは、実測とbyteが同じ72行です。moduleで100 ms待ち、初期化UUIDを記録します。
UUIDはmoduleの識別値です。
TELEM_INIT_FAIL=1ならhandlerより前に例外を出します。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const { randomUUID } = require("node:crypto");
const { writeSync } = require("node:fs");
const { setTimeout: sleep } = require("node:timers/promises");
// This identifier is a module-initialization marker, not a host identifier.
const initId = randomUUID();
const initStartedMs = Date.now();
const initClock = process.hrtime.bigint();
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
const initWorkMs = Number(process.hrtime.bigint() - initClock) / 1e6;
writeSync(
1,
`${JSON.stringify({
phase: "telemetry-init",
init_id: initId,
function_id: process.env.LAB_FUNCTION_ID,
init_started_ms: initStartedMs,
init_work_ms: initWorkMs,
init_fail: process.env.TELEM_INIT_FAIL === "1",
node: process.version,
})}\n`,
);
if (process.env.TELEM_INIT_FAIL === "1") {
throw new Error("lab module initialization failure");
}
let calls = 0;
function identifier(value, field) {
if (typeof value !== "string" || !/^[a-z0-9-]{1,64}$/.test(value)) {
throw new TypeError(`${field} must match [a-z0-9-]{1,64}`);
}
return value;
}
exports.handler = async (event, context) => {
if (!event || typeof event !== "object" || Array.isArray(event)) {
throw new TypeError("event must be a JSON object");
}
const mode = event.mode ?? "normal";
if (!["normal", "throw", "exit", "timeout"].includes(mode)) {
throw new TypeError("unknown mode");
}
const id = identifier(event.id ?? "work", "id");
const runId = identifier(event.run_id ?? "manual", "run_id");
calls += 1;
const startedMs = Date.now();
const startedClock = process.hrtime.bigint();
const base = {
id,
run_id: runId,
mode,
request_id: context.awsRequestId,
init_id: initId,
count: calls,
function_id: process.env.LAB_FUNCTION_ID,
node: process.version,
started_ms: startedMs,
};
writeSync(1, `${JSON.stringify({ phase: "telemetry-handler-begin", ...base })}\n`);
await sleep(mode === "timeout" ? 2500 : 150);
const result = {
phase: "telemetry-handler-end",
...base,
outcome: mode === "normal" ? "success" : "intentional-error",
handler_ms: Number(process.hrtime.bigint() - startedClock) / 1e6,
finished_ms: Date.now(),
};
writeSync(1, `${JSON.stringify(result)}\n`);
if (mode === "throw") throw new Error("lab handler failure");
if (mode === "exit") process.exit(1);
return result;
};入力を揃える
実測scenarioには4関数の設定と16回の呼び出し順を記録しています。
下の
storage、handler、layersなどは共通labの設定です。下の
setup.pyは関数のidとenvironmentだけを読み、他の設定はcode内に書いています。run.mjsはmeasure.stepsから送信順と入力を読みます。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
{
"schemaVersion": 1,
"id": "telemetry-events",
"description": "Correlate platform telemetry with controlled initialization, normal invocation, handler error, init error and runtime reset",
"storage": {
"enabled": false
},
"functions": [
{
"id": "normal",
"memoryMb": 512,
"timeoutSeconds": 2,
"reservedConcurrency": 1,
"asyncRetryAttempts": 0,
"maxEventAgeSeconds": 60,
"environment": {
"EXT_TELEMETRY": "1",
"EXT_WAIT_FOR_MARKER": "0",
"EXT_INIT_MS": "0",
"EXT_POST_MS": "200",
"EXT_BUFFER_MS": "25",
"TELEM_INIT_FAIL": "0"
},
"handler": {
"kind": "nodejs-inline",
"file": "handlers/telemetry/index.cjs",
"runtime": "nodejs26.x",
"entrypoint": "handler",
"s3Access": "none"
},
"layers": [
{
"path": "handlers/extensions/layer"
}
]
},
{
"id": "initfail",
"memoryMb": 512,
"timeoutSeconds": 2,
"reservedConcurrency": 1,
"asyncRetryAttempts": 0,
"maxEventAgeSeconds": 60,
"environment": {
"EXT_TELEMETRY": "1",
"EXT_WAIT_FOR_MARKER": "0",
"EXT_INIT_MS": "0",
"EXT_POST_MS": "200",
"EXT_BUFFER_MS": "25",
"TELEM_INIT_FAIL": "1"
},
"handler": {
"kind": "nodejs-inline",
"file": "handlers/telemetry/index.cjs",
"runtime": "nodejs26.x",
"entrypoint": "handler",
"s3Access": "none"
},
"layers": [
{
"path": "handlers/extensions/layer"
}
]
},
{
"id": "handlerfail",
"memoryMb": 512,
"timeoutSeconds": 2,
"reservedConcurrency": 1,
"asyncRetryAttempts": 0,
"maxEventAgeSeconds": 60,
"environment": {
"EXT_TELEMETRY": "1",
"EXT_WAIT_FOR_MARKER": "0",
"EXT_INIT_MS": "0",
"EXT_POST_MS": "200",
"EXT_BUFFER_MS": "25",
"TELEM_INIT_FAIL": "0"
},
"handler": {
"kind": "nodejs-inline",
"file": "handlers/telemetry/index.cjs",
"runtime": "nodejs26.x",
"entrypoint": "handler",
"s3Access": "none"
},
"layers": [
{
"path": "handlers/extensions/layer"
}
]
},
{
"id": "reset",
"memoryMb": 512,
"timeoutSeconds": 2,
"reservedConcurrency": 1,
"asyncRetryAttempts": 0,
"maxEventAgeSeconds": 60,
"environment": {
"EXT_TELEMETRY": "1",
"EXT_WAIT_FOR_MARKER": "0",
"EXT_INIT_MS": "0",
"EXT_POST_MS": "200",
"EXT_BUFFER_MS": "25",
"TELEM_INIT_FAIL": "0"
},
"handler": {
"kind": "nodejs-inline",
"file": "handlers/telemetry/index.cjs",
"runtime": "nodejs26.x",
"entrypoint": "handler",
"s3Access": "none"
},
"layers": [
{
"path": "handlers/extensions/layer"
}
]
}
],
"measure": {
"steps": [
{
"id": "normal-1",
"function": "normal",
"input": {
"mode": "normal"
}
},
{
"id": "normal-2",
"function": "normal",
"input": {
"mode": "normal"
}
},
{
"id": "normal-3",
"function": "normal",
"input": {
"mode": "normal"
}
},
{
"id": "normal-4",
"function": "normal",
"input": {
"mode": "normal"
}
},
{
"id": "initfail-1",
"function": "initfail",
"input": {
"mode": "normal"
}
},
{
"id": "initfail-2",
"function": "initfail",
"input": {
"mode": "normal"
}
},
{
"id": "handlerfail-1",
"function": "handlerfail",
"input": {
"mode": "normal"
}
},
{
"id": "handlerfail-2",
"function": "handlerfail",
"input": {
"mode": "throw"
}
},
{
"id": "handlerfail-3",
"function": "handlerfail",
"input": {
"mode": "normal"
}
},
{
"id": "handlerfail-4",
"function": "handlerfail",
"input": {
"mode": "throw"
}
},
{
"id": "handlerfail-5",
"function": "handlerfail",
"input": {
"mode": "normal"
}
},
{
"id": "reset-1",
"function": "reset",
"input": {
"mode": "normal"
}
},
{
"id": "reset-2",
"function": "reset",
"input": {
"mode": "exit"
}
},
{
"id": "reset-3",
"function": "reset",
"input": {
"mode": "normal"
}
},
{
"id": "reset-4",
"function": "reset",
"input": {
"mode": "timeout"
}
},
{
"id": "reset-5",
"function": "reset",
"input": {
"mode": "normal"
}
}
]
}
}各入力にstepの
idとrun_idを足します。最初の実測入力は次の値でした。1
{"id":"normal-1-1","mode":"normal","run_id":"dd12-r1"}実行と採取
関数を用意する
setup.pyは再構築例です。各関数にscoped Logsだけを許すroleを作ります。環境変数はscenarioの値に
LAB_FUNCTION_IDとLAB_DEPLOYMENT_IDを足した全8個です。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import base64
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import time
import zipfile
profile, account, deployment, output = sys.argv[1:]
if not re.fullmatch(r"[0-9]{12}", account):
raise ValueError("account must have 12 digits")
if not re.fullmatch(r"[a-z0-9-]{1,24}", deployment):
raise ValueError("invalid deployment")
region = "ap-northeast-1"
root = Path.cwd()
out = Path(output).resolve()
out.mkdir(parents=False, exist_ok=False)
scenario = json.loads(Path("scenarios/telemetry-events.json").read_text())
base = ["aws", "--profile", profile, "--region", region, "--no-cli-pager"]
call_number = 0
def aws(*args):
global call_number
call_number += 1
result = subprocess.run(base + list(args), capture_output=True, text=True,
timeout=60, env={**os.environ, "AWS_MAX_ATTEMPTS": "1"})
(out / f"cli-{call_number:03d}.stdout").write_text(result.stdout)
(out / f"cli-{call_number:03d}.stderr").write_text(result.stderr)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return json.loads(result.stdout) if result.stdout.strip() else {}
def json_file(name, value):
path = out / name
path.write_text(json.dumps(value))
return "file://" + str(path)
if aws("sts", "get-caller-identity")["Account"] != account:
raise RuntimeError("account mismatch")
layer_binary = root / "handlers/extensions/layer/extensions/lab-extension"
layer_zip = out / "layer.zip"
with zipfile.ZipFile(layer_zip, "w", zipfile.ZIP_DEFLATED) as archive:
info = zipfile.ZipInfo("extensions/lab-extension")
info.create_system = 3
info.external_attr = 0o100755 << 16
info.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(info, layer_binary.read_bytes())
function_zip = out / "function.zip"
with zipfile.ZipFile(function_zip, "w", zipfile.ZIP_DEFLATED) as archive:
archive.write("handlers/telemetry/index.cjs", "index.cjs")
layer = aws("lambda", "publish-layer-version", "--layer-name", "lambda-lab-" + deployment,
"--zip-file", "fileb://" + str(layer_zip),
"--compatible-runtimes", "nodejs26.x", "--compatible-architectures", "arm64")
trust = {"Version": "2012-10-17", "Statement": [{"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}
state = {"profile": profile, "account": account, "deployment": deployment,
"region": region, "layer_arn": layer["LayerVersionArn"], "functions": {}}
for fn in scenario["functions"]:
name = "lambda-lab-" + deployment + "-" + fn["id"]
group = "/aws/lambda/" + name
role_name = name + "-role"
aws("logs", "create-log-group", "--log-group-name", group)
aws("logs", "put-retention-policy", "--log-group-name", group, "--retention-in-days", "1")
role = aws("iam", "create-role", "--role-name", role_name,
"--assume-role-policy-document", json_file(fn["id"] + "-trust.json", trust))
policy = {"Version": "2012-10-17", "Statement": [{"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": f"arn:aws:logs:{region}:{account}:log-group:{group}:*"}]}
aws("iam", "put-role-policy", "--role-name", role_name, "--policy-name", "own-logs",
"--policy-document", json_file(fn["id"] + "-policy.json", policy))
env = {**fn["environment"], "LAB_FUNCTION_ID": fn["id"], "LAB_DEPLOYMENT_ID": deployment}
args = ["lambda", "create-function", "--function-name", name,
"--runtime", "nodejs26.x", "--architectures", "arm64", "--handler", "index.handler",
"--role", role["Role"]["Arn"], "--zip-file", "fileb://" + str(function_zip),
"--memory-size", "512", "--timeout", "2", "--layers", state["layer_arn"],
"--environment", json_file(fn["id"] + "-environment.json", {"Variables": env})]
for attempt in range(6):
try:
aws(*args)
break
except RuntimeError as error:
if "cannot be assumed by Lambda" not in str(error) or attempt == 5:
raise
time.sleep(2 ** attempt)
aws("lambda", "wait", "function-active-v2", "--function-name", name)
aws("lambda", "put-function-concurrency", "--function-name", name,
"--reserved-concurrent-executions", "1")
aws("lambda", "put-function-event-invoke-config", "--function-name", name,
"--qualifier", "$LATEST", "--maximum-retry-attempts", "0", "--maximum-event-age-in-seconds", "60")
state["functions"][fn["id"]] = {"name": name, "group": group,
"role_arn": role["Role"]["Arn"], "role_name": role_name, "environment": env,
"code_sha256": base64.b64encode(hashlib.sha256(function_zip.read_bytes()).digest()).decode()}
(out / "state.json").write_text(json.dumps(state, indent=2))Invokeと採取
run.mjsも再構築例です。実測と同じくmaxAttempts=1、同期Invoke、SDK期限7秒です。callerの経過時間は
performance.now()、照合用時刻はDate.now()で記録します。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { performance } from "node:perf_hooks";
import { LambdaClient, InvokeCommand, GetFunctionConfigurationCommand,
GetFunctionConcurrencyCommand, GetFunctionEventInvokeConfigCommand } from "@aws-sdk/client-lambda";
import { CloudWatchLogsClient, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs";
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
const [statePath, action, runId, output, selection] = process.argv.slice(2);
if (!["baseline", "drain", "collect"].includes(action)) throw new Error("invalid action");
if (!/^[a-z0-9-]{1,64}$/.test(runId)) throw new Error("invalid run_id");
const state = JSON.parse(await readFile(statePath, "utf8"));
const scenario = JSON.parse(await readFile("scenarios/telemetry-events.json", "utf8"));
const config = { region: state.region, profile: state.profile, maxAttempts: 1 };
const lambda = new LambdaClient(config);
const logs = new CloudWatchLogsClient(config);
const sts = new STSClient(config);
const bounded = (client, command, ms = 7000) => client.send(command, { abortSignal: AbortSignal.timeout(ms) });
const save = (name, value) => writeFile(output + "/" + name, JSON.stringify(value, null, 2));
const same = (a, b) => JSON.stringify(Object.entries(a).sort()) === JSON.stringify(Object.entries(b).sort());
await mkdir(output);
const startTime = Date.now();
await save("run-start.json", { startTime, action, run_id: runId });
try {
const identity = await bounded(sts, new GetCallerIdentityCommand({}));
if (identity.Account !== state.account) throw new Error("account mismatch");
for (const target of Object.values(state.functions)) {
const c = await bounded(lambda, new GetFunctionConfigurationCommand({ FunctionName: target.name }));
const rc = await bounded(lambda, new GetFunctionConcurrencyCommand({ FunctionName: target.name }));
const asyncConfig = await bounded(lambda, new GetFunctionEventInvokeConfigCommand({ FunctionName: target.name, Qualifier: "$LATEST" }));
if (c.Runtime !== "nodejs26.x" || c.Handler !== "index.handler" || c.MemorySize !== 512 || c.Timeout !== 2 ||
c.Architectures?.[0] !== "arm64" || c.Role !== target.role_arn || c.CodeSha256 !== target.code_sha256 ||
c.Layers?.length !== 1 || c.Layers[0].Arn !== state.layer_arn ||
rc.ReservedConcurrentExecutions !== 1 || asyncConfig.MaximumRetryAttempts !== 0 ||
asyncConfig.MaximumEventAgeInSeconds !== 60 || !same(c.Environment?.Variables ?? {}, target.environment)) {
throw new Error("function configuration mismatch: " + target.name);
}
}
if (action === "collect") {
const since = JSON.parse(await readFile(selection, "utf8")).startTime;
if (!Number.isSafeInteger(since) || since > startTime) throw new Error("invalid startTime");
for (const [id, target] of Object.entries(state.functions)) {
let nextToken;
for (let page = 1; page <= 20; page += 1) {
const result = await bounded(logs, new FilterLogEventsCommand({ logGroupName: target.group,
startTime: since, endTime: startTime, nextToken, limit: 10000 }));
await save("logs-" + id + "-page-" + page + ".json", result);
if (!result.nextToken || result.nextToken === nextToken) break;
if (page === 20) throw new Error("log page limit reached");
nextToken = result.nextToken;
}
}
} else {
const steps = action === "baseline" ? scenario.measure.steps : scenario.measure.steps.filter(s => s.id === selection);
if (steps.length !== (action === "baseline" ? 16 : 1)) throw new Error("invalid step selection");
for (const step of steps) {
const target = state.functions[step.function];
const input = { id: step.id + "-1", ...step.input, run_id: runId };
const startedMs = Date.now();
const startedMono = performance.now();
const response = await bounded(lambda, new InvokeCommand({ FunctionName: target.name,
InvocationType: "RequestResponse", LogType: "Tail", Payload: Buffer.from(JSON.stringify(input)) }));
const endedMs = Date.now();
const elapsedMs = performance.now() - startedMono;
const payload = Buffer.from(response.Payload ?? []).toString("utf8");
const { Payload, ...headers } = response;
await save("invoke-" + step.id + ".json", { input, response: headers, client: {
startUtc: new Date(startedMs).toISOString(), endUtc: new Date(endedMs).toISOString(), elapsedMs } });
await writeFile(output + "/invoke-" + step.id + ".payload", payload);
await writeFile(output + "/invoke-" + step.id + ".log.txt", Buffer.from(response.LogResult ?? "", "base64"));
if (response.$metadata.attempts !== 1) throw new Error("unexpected SDK attempts");
}
}
} finally {
lambda.destroy(); logs.destroy(); sts.destroy();
}run-with-deadline.pyで、processの外側にも期限を置きます。下の120秒と60秒は再構築例の設計値です。元の送信記録には外側期限の値がありません。
1
2
3
4
5
6
7
8
import subprocess
import sys
seconds = int(sys.argv[1])
if seconds < 1 or seconds > 120:
raise ValueError("deadline must be 1..120 seconds")
result = subprocess.run(sys.argv[2:], timeout=seconds)
raise SystemExit(result.returncode)実行の順序
AWS CLI v2、Go、Node.js、Python 3が必要です。実験用accountで使います。
profile、account、固有のdeployment名とrepo外の保存先を置き換えます。
以下のbuildとSDK callerは再構築用で、掲載の実測値は共通labから得た値です。
profile、account、固有のdeployment名とrepo外の保存先を置き換えます。
以下のbuildとSDK callerは再構築用で、掲載の実測値は共通labから得た値です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lab_profile='<実験用profile>'
lab_account='<12桁のaccount ID>'
lab_deployment='<小文字・数字・hyphenの固有名>'
lab_output='<repo外の新規directoryの絶対path>'
mkdir "$lab_output"
mkdir -p handlers/extensions/layer/extensions
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -o handlers/extensions/layer/extensions/lab-extension handlers/extensions/main.go
chmod 755 handlers/extensions/layer/extensions/lab-extension
python3 setup.py "$lab_profile" "$lab_account" "$lab_deployment" "$lab_output/setup"
npm init -y
npm install @aws-sdk/client-lambda @aws-sdk/client-cloudwatch-logs @aws-sdk/client-sts
python3 run-with-deadline.py 120 node run.mjs "$lab_output/setup/state.json" baseline dd12-r1 "$lab_output/baseline"
python3 run-with-deadline.py 120 node run.mjs "$lab_output/setup/state.json" collect dd12-r1 "$lab_output/collect1" "$lab_output/baseline/run-start.json"
python3 run-with-deadline.py 60 node run.mjs "$lab_output/setup/state.json" drain dd12-drain "$lab_output/drain-normal" normal-4
python3 run-with-deadline.py 60 node run.mjs "$lab_output/setup/state.json" drain dd12-drain "$lab_output/drain-handlerfail" handlerfail-5
python3 run-with-deadline.py 60 node run.mjs "$lab_output/setup/state.json" drain dd12-drain "$lab_output/drain-reset" reset-5
python3 run-with-deadline.py 60 node run.mjs "$lab_output/setup/state.json" drain dd12-drain "$lab_output/drain-initfail" initfail-2
python3 run-with-deadline.py 120 node run.mjs "$lab_output/setup/state.json" collect dd12-r1 "$lab_output/collect2" "$lab_output/baseline/run-start.json"16回→採取→4関数へ各1回→再採取で、合計20回です。
追加はすべてmode=normalで、Init失敗の関数はmoduleの例外になります。
送信する間隔で受信までの時間も変わるため、67.281秒の再現を期待する手順ではありません。
追加はすべてmode=normalで、Init失敗の関数はmoduleの例外になります。
送信する間隔で受信までの時間も変わるため、67.281秒の再現を期待する手順ではありません。
実験が終わったら、
収集したlogは先に保存し、関数、Layer version、log group、role内のpolicy、roleの順で片付けます。
state.jsonに記録した4関数・4つのrole・4つのlog group・Layer versionを削除します。収集したlogは先に保存し、関数、Layer version、log group、role内のpolicy、roleの順で片付けます。
まとめ
Durationや初期化失敗を調べるなら、まず標準の
成功した再初期化や、runtime後に残るExtensionの区間を分けたいとき、Telemetry購読を足します。
今回の約430 msもhandlerの約150 msだけでは説明できませんでした。
InitとExtensionを分けると、調べる対象を絞れます。
INIT_REPORTとREPORTを使います。成功した再初期化や、runtime後に残るExtensionの区間を分けたいとき、Telemetry購読を足します。
今回の約430 msもhandlerの約150 msだけでは説明できませんでした。
InitとExtensionを分けると、調べる対象を絞れます。
subscriberの起動やbufferの扱い、配信の遅れへの対応、記録の照合は利用者の担当です。
4関数の最後のreportは次のInvoke付近まで受信できませんでした。
この構成で即時の集計を前提にはできません。遅れの原因は未測定です。
購読の有無によるDuration、Max Memory Used、課金の差も測っていません。
4関数の最後のreportは次のInvoke付近まで受信できませんでした。
この構成で即時の集計を前提にはできません。遅れの原因は未測定です。
購読の有無によるDuration、Max Memory Used、課金の差も測っていません。
対象はNode.js 26 previewの少数の逐次Invokeです。
他runtime、通常のon-demand以外の実行方式、高log量のdrop、listener故障は測っていません。
Lambdaへ起動と再初期化を任せても、業務の完了は利用者のコードで確かめます。
他runtime、通常のon-demand以外の実行方式、高log量のdrop、listener故障は測っていません。
Lambdaへ起動と再初期化を任せても、業務の完了は利用者のコードで確かめます。
Series: Serverless DeepDive (13 articles)
- …
- 13Serverless DeepDive #12:Telemetry APIで遅延の区間を探る This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article