Dapr で Invoke 時のメソッド名を可変にする事の落とし穴

Dapr アプリケーションのサイドカーとして導入される ```daprd``` は Invoke 時のメソッド名ごとにメトリクスを作って内部的に保持してしまいます。

そのため、下記ドキュメントのように Invoke 時のメソッド名を ```checkout/{orderId}``` のような可変にすると、メトリクスが増え続けて不都合が生じるように思います。

* https://docs.dapr.io/developing-applications/building-blocks/service-invocation/howto-invoke-discover-services/

例えば、固定であれば ```checkout``` という単一のメトリクスで済んだものを、可変にする事で ```checkout/1``` や ```checkout/2``` のような無意味なメトリクスを次々と増やしていく事になり、消費メモリが徐々に増えてしまうはずです。

そこで、今回は Dapr で Invoke 時のメソッド名を可変にする事で消費メモリにどう影響するのか検証してみました。

# はじめに

下記 2つの Dapr アプリケーション order と checkout を用いて、order から checkout を Invoke する際にメソッド名を可変にするかどうかで order 側の ```daprd``` の消費メモリがどのように変化するのか検証します。

## order アプリケーション

```javascript:order/index.mjs
import { DaprServer, HttpMethod } from '@dapr/dapr'
import { randomUUID } from 'crypto'

const run = async () => {
    const server = new DaprServer()
    const opts = { method: HttpMethod.GET }

    // 固定版（ /checkout ）
    const check1 = (_) => server.client.invoker.invoke('checkout', 'checkout')
    // 可変版（ /checkout/{uuid} ）
    const check2 = (_) => server.client.invoker.invoke('checkout', `checkout/${randomUUID()}`)

    await server.invoker.listen('check1', check1, opts)
    await server.invoker.listen('check2', check2, opts)

    await server.start()
}

run().catch(err => {
    console.error(err)
    process.exit(1)
})
```

```dockerfile:order/Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY ./package*.json ./

RUN npm install

COPY . .

CMD ["node", "index.mjs"]
```

## checkout アプリケーション

```javascript:checkout/index.js
const http = require('http')

const server = http.createServer((req, res) => {
    res.end(`path:${req.url}`)
})

server.listen(3000)
```

```dockerfile:checkout/Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY . .

CMD ["node", "index.js"]
```

# 検証

検証は以下の 3パターンで実施しました。

* (a) 固定のメソッド名を使用
* (b) 可変のメソッド名を使用
* (c) daprd のメトリクスを無効化（注）した状態で可変のメソッド名を使用

```
（注） dapr.io/enable-metrics を false に設定
```

下記スクリプトで 3000 回のアクセスを実施した後、しばらく待ってから ```kubectl top pod --containers``` を実施して daprd の消費メモリを取得します。これを 3回繰り返す事にしました。

##### テスト用スクリプト

```javascript:k6_check1.js（固定メソッド名を使用）
import http from 'k6/http'
import { check, sleep } from 'k6'

export const options = {
    vus: 20,
    iterations: 3000
}

export default function() {
    const r = http.get('http://127.0.0.1:3001/check1')
    
    check(r, { 'status 200': (r) => r.status == 200 })

    sleep(0.1)
}
```

```javascript:k6_check2.js（可変メソッド名を使用）
...省略

export default function() {
    const r = http.get('http://127.0.0.1:3001/check2')
    
    check(r, { 'status 200': (r) => r.status == 200 })

    sleep(0.1)
}
```

Kubernetes マニフェストの内容は次の通りです。
order 側は Heap の情報を取得できるようにプロファイリングを有効化（```dapr.io/enable-profiling: "true"```）しています。

##### Kubernetes マニフェスト

```yaml:order.yaml
kind: Service
apiVersion: v1
metadata:
  name: order
  labels:
    app: order
spec:
  selector:
    app: order
  ports:
  - protocol: TCP
    port: 3001
    targetPort: 3000
  type: LoadBalancer

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order
  labels:
    app: order
spec:
  replicas: 1
  selector:
    matchLabels:
      app: order
  template:
    metadata:
      labels:
        app: order
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "order"
        dapr.io/app-port: "3000"
        dapr.io/enable-metrics: "true"
        dapr.io/metrics-port: "9090"
        dapr.io/enable-profiling: "true"
    spec:
      containers:
      - name: order
        image: order
        env:
        - name: APP_PORT
          value: "3000"
        ports:
        - containerPort: 3000
        imagePullPolicy: Never
```

```yaml:checkout.yaml
kind: Service
apiVersion: v1
metadata:
  name: checkout
  labels:
    app: checkout
spec:
  selector:
    app: checkout
  ports:
  - protocol: TCP
    port: 3002
    targetPort: 3000
  type: LoadBalancer

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  labels:
    app: checkout
spec:
  replicas: 1
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "checkout"
        dapr.io/app-port: "3000"
        dapr.io/enable-metrics: "true"
        dapr.io/metrics-port: "9090"
    spec:
      ...省略
```

## (a) 固定メソッド名

```kubectl apply``` で上記のマニフェストを適用します。

この時点での消費メモリは以下のようになりました。

```sh:開始時
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-lgsf5   checkout   1m           7Mi             
checkout-5b95fbcf-lgsf5   daprd      3m           19Mi            
order-7d8b9fddd9-pb4f8    daprd      2m           19Mi            
order-7d8b9fddd9-pb4f8    order      0m           25Mi
```

```k6_check1.js``` を 3回実行した結果がそれぞれ次のようになりました。

```sh:k6_check1.js 1回目 3000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-lgsf5   checkout   1m           8Mi             
checkout-5b95fbcf-lgsf5   daprd      4m           22Mi            
order-7d8b9fddd9-pb4f8    daprd      5m           28Mi            
order-7d8b9fddd9-pb4f8    order      0m           24Mi 
```

```sh:k6_check1.js 2回目 6000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-lgsf5   checkout   1m           10Mi            
checkout-5b95fbcf-lgsf5   daprd      3m           29Mi            
order-7d8b9fddd9-pb4f8    daprd      3m           26Mi            
order-7d8b9fddd9-pb4f8    order      1m           63Mi 
```

```sh:k6_check1.js 3回目 9000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-lgsf5   checkout   0m           10Mi            
checkout-5b95fbcf-lgsf5   daprd      4m           29Mi            
order-7d8b9fddd9-pb4f8    daprd      3m           25Mi            
order-7d8b9fddd9-pb4f8    order      0m           24Mi  
```

daprd の消費メモリが多少増えた程度で特に問題は無さそうです。

### Heap Profile の取得

pprof で daprd（order側） の Heap 情報を取得できるようにポート ```7777``` へ port-forward します。

```sh:port-forward
$ kubectl port-forward order-7d8b9fddd9-pb4f8 7777
```

pprof で Heap の top を取得したところ、次のような結果となりました。

```sh:
$ go tool pprof -top http://localhost:7777/debug/pprof/heap
...省略
File: daprd
Type: inuse_space
Time: Jun 4, 2023 at 10:22pm (JST)
Showing nodes accounting for 11575.30kB, 100% of 11575.30kB total
      flat  flat%   sum%        cum   cum%
 2056.32kB 17.76% 17.76%  2056.32kB 17.76%  github.com/aws/aws-sdk-go/aws/endpoints.init
 1577.92kB 13.63% 31.40%  1577.92kB 13.63%  github.com/denisenkom/go-mssqldb/internal/cp.init
 1089.33kB  9.41% 40.81%  1089.33kB  9.41%  github.com/valyala/fasthttp/stackless.NewFunc
 1057.98kB  9.14% 49.95%  1057.98kB  9.14%  regexp/syntax.(*compiler).inst (inline)
  578.66kB  5.00% 54.95%   578.66kB  5.00%  google.golang.org/protobuf/internal/strs.(*Builder).AppendFullName
  553.04kB  4.78% 59.72%   553.04kB  4.78%  google.golang.org/protobuf/reflect/protoregistry.(*Types).register
  544.67kB  4.71% 64.43%   544.67kB  4.71%  github.com/xdg-go/stringprep.init
  524.09kB  4.53% 68.96%   524.09kB  4.53%  k8s.io/apimachinery/pkg/runtime.(*Scheme).AddKnownTypeWithName
  516.64kB  4.46% 73.42%   516.64kB  4.46%  runtime.procresize
  514.63kB  4.45% 77.87%   514.63kB  4.45%  math/rand.newSource (inline)
...省略
```

## (b) 可変メソッド名

念の為、```Kubectl delete``` してから再度マニフェストを apply しました。

```sh:開始時
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-rxlck   checkout   1m           7Mi             
checkout-5b95fbcf-rxlck   daprd      3m           19Mi            
order-7d8b9fddd9-lgq7z    daprd      3m           19Mi            
order-7d8b9fddd9-lgq7z    order      1m           24Mi      
```

可変メソッド名で Invoke する ```k6_check2.js``` を 3回実行した結果がそれぞれ次のようになりました。

```sh:k6_check2.js 1回目 3000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-rxlck   checkout   1m           9Mi             
checkout-5b95fbcf-rxlck   daprd      3m           46Mi            
order-7d8b9fddd9-lgq7z    daprd      5m           47Mi            
order-7d8b9fddd9-lgq7z    order      1m           25Mi 
```

```sh:k6_check2.js 2回目 6000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-rxlck   checkout   0m           10Mi            
checkout-5b95fbcf-rxlck   daprd      7m           60Mi            
order-7d8b9fddd9-lgq7z    daprd      2m           69Mi            
order-7d8b9fddd9-lgq7z    order      0m           62Mi 
```

```sh:k6_check2.js 3回目 9000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-rxlck   checkout   1m           10Mi            
checkout-5b95fbcf-rxlck   daprd      3m           75Mi            
order-7d8b9fddd9-lgq7z    daprd      3m           80Mi            
order-7d8b9fddd9-lgq7z    order      1m           62Mi  
```

懸念していた通り、(a) とは違って daprd の消費メモリが増え続ける結果となりました。

基本的には、daprd のコンテナが OOM Killer によって kill される（再起動になる）まで消費メモリが増え続けていくようです。

### Heap Profile の取得

(a) と同様に Heap の top を取得したところ、次のような結果となりました。

```sh:
$ go tool pprof -top http://localhost:7777/debug/pprof/heap
...省略
Showing nodes accounting for 36870.64kB, 100% of 36870.64kB total
      flat  flat%   sum%        cum   cum%
10242.39kB 27.78% 27.78% 10242.39kB 27.78%  go.opencensus.io/stats/view.newDistributionData
 4096.34kB 11.11% 38.89% 18284.11kB 49.59%  go.opencensus.io/stats/view.(*viewInternal).addSample
 3584.34kB  9.72% 48.61%  3584.34kB  9.72%  go.opencensus.io/stats/view.decodeTags
 3433.37kB  9.31% 57.92% 14187.78kB 38.48%  go.opencensus.io/stats/view.(*collector).addSample
 2562.46kB  6.95% 64.87%  2562.46kB  6.95%  github.com/aws/aws-sdk-go/aws/endpoints.init
 1577.92kB  4.28% 69.15%  1577.92kB  4.28%  github.com/denisenkom/go-mssqldb/internal/cp.init
 1024.94kB  2.78% 71.93%  1024.94kB  2.78%  regexp/syntax.(*compiler).inst (inline)
 1024.11kB  2.78% 74.71%  1024.11kB  2.78%  regexp/syntax.(*parser).newRegexp (inline)
 1024.05kB  2.78% 77.49%  4608.38kB 12.50%  go.opencensus.io/stats/view.(*collector).collectedRows
  600.58kB  1.63% 79.12%  1113.21kB  3.02%  github.com/go-playground/validator/v10.init
...省略
```

(a) とは大きく異なり、メトリクス系のデータ（```go.opencensus.io/stats```）に大きく占有されています。

## (c) 可変メソッド名 + メトリクス無効（enable-metrics:"false"）

```dapr.io/enable-metrics``` を ```false``` にした場合に変化があるのかを下記マニフェストを使って検証しました。

```yaml:order_metrics-off.yaml
...省略
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order
  labels:
    app: order
spec:
  replicas: 1
  selector:
    matchLabels:
      app: order
  template:
    metadata:
      labels:
        app: order
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "order"
        dapr.io/app-port: "3000"
        dapr.io/enable-metrics: "false"
        dapr.io/enable-profiling: "true"
    spec:
      ...省略
```

結果は次の通りです。

```dapr.io/enable-metrics``` を ```false``` にしてもメトリクス自体は無効化されず、(b) と同様に消費メモリが増え続けました。

```sh:開始時
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-tj7jt   checkout   1m           7Mi             
checkout-5b95fbcf-tj7jt   daprd      3m           19Mi            
order-956fc649f-7n8sw     daprd      3m           19Mi            
order-956fc649f-7n8sw     order      1m           24Mi 
```

```sh:k6_check2.js 1回目 3000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-tj7jt   checkout   1m           9Mi             
checkout-5b95fbcf-tj7jt   daprd      4m           44Mi            
order-956fc649f-7n8sw     daprd      2m           52Mi            
order-956fc649f-7n8sw     order      1m           25Mi  
```

```sh:k6_check2.js 2回目 6000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-tj7jt   checkout   0m           9Mi             
checkout-5b95fbcf-tj7jt   daprd      3m           60Mi            
order-956fc649f-7n8sw     daprd      3m           71Mi            
order-956fc649f-7n8sw     order      0m           62Mi
```

```sh:k6_check2.js 3回目 9000アクセス後
$ kubectl top pod --containers
POD                       NAME       CPU(cores)   MEMORY(bytes)   
checkout-5b95fbcf-tj7jt   checkout   0m           10Mi            
checkout-5b95fbcf-tj7jt   daprd      3m           73Mi            
order-956fc649f-7n8sw     daprd      3m           86Mi            
order-956fc649f-7n8sw     order      1m           62Mi 
```

### Heap Profile の取得

こちらも (b) と同様の結果となりました。

```sh:
$ go tool pprof -top http://localhost:7777/debug/pprof/heap
...省略
Showing nodes accounting for 44173.14kB, 100% of 44173.14kB total
      flat  flat%   sum%        cum   cum%
16900.43kB 38.26% 38.26% 16900.43kB 38.26%  go.opencensus.io/stats/view.newDistributionData
 4291.71kB  9.72% 47.98% 21704.16kB 49.13%  go.opencensus.io/stats/view.(*collector).addSample
 3584.30kB  8.11% 56.09% 25288.46kB 57.25%  go.opencensus.io/stats/view.(*viewInternal).addSample
 3072.70kB  6.96% 63.05%  3072.70kB  6.96%  go.opencensus.io/stats/view.(*DistributionData).clone
 2366.89kB  5.36% 68.40%  2366.89kB  5.36%  github.com/denisenkom/go-mssqldb/internal/cp.init
 2082.37kB  4.71% 73.12%  2082.37kB  4.71%  regexp/syntax.(*compiler).inst (inline)
 2049.48kB  4.64% 77.76%  2049.48kB  4.64%  github.com/aws/aws-sdk-go/aws/endpoints.init
 1536.06kB  3.48% 81.23%  1536.06kB  3.48%  internal/reflectlite.Swapper
 1028.89kB  2.33% 83.56%  1028.89kB  2.33%  regexp.onePassCopy
 1024.34kB  2.32% 85.88%  1024.34kB  2.32%  google.golang.org/protobuf/internal/impl.legacyLoadMessageInfo
...省略
```

# まとめ

* Invoke 時のメソッド名を可変にするのは避けた方が無難
    * 可変にするとメトリクスが増え、消費メモリが無駄に増え続ける事になりかねない
* dapr.io/enable-metrics を false に設定しても、メトリクス自体は無効化されない