Vespaの冗長化を試してみる

# はじめに
スタンバイアドベントカレンダー4日目です:smiley:

スタンバイでは検索エンジンに[Vespa](https://vespa.ai/)を利用しています！
Vespaはmulti-node構成をとることで冗長化することができます。
スタンバイでも冗長化構成をとっています。

本記事では冗長化構成であるmulti-nodeの[公式サンプル](https://github.com/vespa-engine/sample-apps/tree/master/examples/operations/multinode-HA
)が用意されているので、それを用いて環境構築します。
環境構築後、`content cluster`の冗長化について検証していきたいと思います。`content cluster`はdocumentを管理しています。
※document：Vespaはデータをドキュメントとしてモデル化しています。本記事でデータの意味を表します。

# 各clusterの役割
サンプルを実施する前にVespaの各clusterについて簡単に説明をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567766/f28845fc-a7bb-3e44-00b7-d44b99047cc5.png)
画像出典: [Vespa Overview](https://docs.vespa.ai/en/overview.html)

|cluster|役割|
|:--:|:--:|
|Admin/Config|設定の管理や各clusterの制御の役割|
|Stateless Java container|ユーザからの検索リクエストや更新を受け付ける役割|
|Content|indexを保持し検索・ランキングを実行する役割|


# 公式サンプル実施！

## 環境準備
公式サンプルの最初から[Start the content cluster:](https://github.com/vespa-engine/sample-apps/tree/master/examples/operations/multinode-HA#start-the-content-cluster)まで実施します。ここまで実施すると動作確認できる状態になっています。
※サンプルの中でstatusの確認をする場面が何度かでてきますが、数十秒待たないと反映されないこともあるのでご注意ください。

このサンプルでは、`contaier-cluster`を２つにわけています。`feed（更新）`と`query（検索）`を独立して作成しています。


## 動作確認
### feed（更新）
[documentAPI](https://docs.vespa.ai/en/reference/document-v1-api-reference.html)を利用してdocumentのfeedをします。このサンプルでは5件feedします。

```bash
i=0; (for doc in $(ls ../../../album-recommendation/ext/*.json); \
  do \
    curl -H Content-Type:application/json -d @$doc \
    http://localhost:8080/document/v1/mynamespace/music/docid/$i; \
    i=$(($i + 1)); echo; \
  done)
```

### query（検索）
feedしたdocumentが検索できることを確認します。
```bash
curl --data-urlencode 'yql=select * from sources * where sddocname contains "music"' \
  http://localhost:8082/search/
```

以下のように返却されることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567766/5bd54ae7-2411-b91a-2ae9-496974d03347.png)

# contentの冗長性
contentの冗長性について確認していきます。

デプロイはサンプルにあったコマンドを実行します。
（以降デプロイする時はこのコマンドを実行）
```bash
zip -r - . -x "gke/*" "img/*" "scripts/*" "pki/*" "tls/*" README.md .gitignore "*.yaml" | \
  curl --header Content-Type:application/zip --data-binary @- \
  http://localhost:19071/application/v2/tenant/default/prepareandactivate
```

## content内のドキュメント数
各nodeのドキュメント数を確認するのにVespaが提供する[monitorAPI](https://docs.vespa.ai/en/operations-selfhosted/monitoring.html)を利用します。

サンプルだとnode8とnode9が`content-node`なので対象portにリクエストを投げます。
`content-node`を増やす場合は`ports`を更新してください。
```bash
ports=(20100 20101)

for port in "${ports[@]}"; do
  curl -s "http://localhost:$port/metrics/v2/values" | \
  jq -r '.nodes[] | .hostname as $hostname | .services[] | select(.name == "vespa.searchnode") | .metrics[] | select(.values["content.proton.documentdb.documents.active.last"]) | "\($hostname): \(.values["content.proton.documentdb.documents.active.last"])"'
done
```

実行するとnode8に2件、node9に3件保持していることがわかります。（node8が3件になることもあるかもしれません）

```bash
node8.vespanet: 2
node9.vespanet: 3
```

## min-redundancy
### 概要
サンプルでは`<min-redundancy>2</min-redundancy>`となっています。

`min-redundancy`とはdocumentをコピーして保持する数となります。つまり、いずれかのcontentが壊れてもdocumentを保持します。

以下のパターンを検証します。
- 特定nodeにおけるVespaサービスの停止
- コンテナ停止
- コンテナ削除

### 検証
#### 冗長性あり（min-redundancy：2）
- 特定nodeにおけるVespaサービスの停止
node9のVespaサービスを停止してみます。
```bash
docker exec -it node9 /bin/bash
[vespa@node9 /]$ vespa-stop-services
config-sentinel was running with pid 99, sending SIGTERM
Waiting for exit (up to 15 minutes)
.. DONE
configproxy was running with pid 33, sending SIGTERM
Waiting for exit (up to 15 minutes)
. DONE
```

ドキュメント数を確認してみるとnode8のdocumentが5件になっていることがわかります。なので検索しても5件表示されます。
```bash
node8.vespanet: 5
```

node9のVespaを再開させます
```bash
[vespa@node9 /]$ vespa-start-services
Starting config proxy using tcp/node0.vespanet:19070 and tcp/node1.vespanet:19070 and tcp/node2.vespanet:19070 as config source(s)
Waiting for config proxy to start on 'node9.vespanet'
runserver(configproxy) running with pid: 587
config proxy started after 0s (runserver pid 587)
Starting services for VESPA_HOSTNAME='node9.vespanet'
runserver(config-sentinel) running with pid: 653
```
数秒待つと、ドキュメント数は以前と同じ状態になります。
```bash
node8.vespanet: 2
node9.vespanet: 3
```

- コンテナ停止
```bash
docker stop node9
```
Vespaサービスの停止と同じ動作になります。

- コンテナ削除
```bash
docker rm -f node9
```

削除後にnode9を再度`docker run`し、再度デプロイします。
```bash
docker run --detach --name node9 --hostname node9.vespanet \
    -e VESPA_CONFIGSERVERS=node0.vespanet,node1.vespanet,node2.vespanet \
    --network vespanet \
    --publish 19108:19107 --publish 20101:19092 \
    vespaengine/vespa services
```

Vespaサービスの停止と同じ動作になります。

#### 冗長性なし（min-redundancy：1）
`services.xml`を修正して、デプロイします。
※抜粋
```xml
    <content id="music" version="1.0">
        <min-redundancy>1</min-redundancy>
        <documents>
            <document type="music" mode="index" />
            <document-processing cluster="feed" />            
        </documents>
        <nodes>
            <node hostalias="node8" distribution-key="0" />
            <node hostalias="node9" distribution-key="1" />
        </nodes>
    </content>
```

- 特定nodeにおけるVespaサービスの停止
先ほどと同じようにnode9のVespaサービスを停止して、ドキュメント数を確認します。
```bash
node8.vespanet: 2
```
先ほどと違い冗長化されていないため、node8のdocumentが2件のままとなっています。なので検索しても2件しか返却されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567766/95760668-d390-a99a-388b-2846ccd6136d.png)

- コンテナ停止
Vespaサービスの停止と同じ動作になります。

- コンテナ削除
削除後にnode9を再度`docker run`し、再度デプロイします。
```bash
node8.vespanet: 2
node9.vespanet: 0
```
冗長化していた時と違い、document数が戻りません。

Vespaのdocument情報は[vespa-remove-index](https://docs.vespa.ai/en/operations-selfhosted/vespa-cmdline-tools.html#vespa-remove-index)で記載されている通り`$VESPA_HOME/var/db/vespa/`に格納されているため、コンテナを削除すると削除されます。
Vespaサービスの停止とコンテナ停止は`$VESPA_HOME/var/db/vespa/`の情報が削除されないため、documentが復活するということがわかりました。

## group
先ほどは`content`のnode１台ずつ独立して分散していましたが、
`content-node`をグループ化して分散することも可能です。
content用のコンテナを追加で作成します。
```bash
docker run --detach --name node10 --hostname node10.vespanet \
    -e VESPA_CONFIGSERVERS=node0.vespanet,node1.vespanet,node2.vespanet \
    --network vespanet \
    --publish 19109:19107 --publish 20102:19092 \
    vespaengine/vespa services

docker run --detach --name node11 --hostname node11.vespanet \
    -e VESPA_CONFIGSERVERS=node0.vespanet,node1.vespanet,node2.vespanet \
    --network vespanet \
    --publish 19110:19107 --publish 20103:19092 \
    vespaengine/vespa services
```

`hosts.xml`にnode10とnode11を追加します。
※抜粋
```xml
  <host name="node10.vespanet">
    <alias>node10</alias>
  </host>
  <host name="node11.vespanet">
    <alias>node11</alias>
  </host>
```

`services.xml`にnode10とnode11を追加します。
※抜粋
```xml
    <content id="music" version="1.0">
        <min-redundancy>2</min-redundancy>
        <documents>
            <document type="music" mode="index" />
            <document-processing cluster="feed" />            
        </documents>
        <group>
            <distribution partitions="1|*"/>
            <group name="group0" distribution-key="0">
                <node hostalias="node8" distribution-key="0"/>
                <node hostalias="node9" distribution-key="1"/>
            </group>
            <group name="group1" distribution-key="1">
                <node hostalias="node10" distribution-key="2"/>
                <node hostalias="node11" distribution-key="3"/>
            </group>
        </group>
    </content>
```

この状態でデプロイします。（グループ数とmin-redundancyの数や`distribution partitions`の設定が正しくないとエラーになります）
デプロイ後、各コンテナのdocument数を確認するとグループ単位に冗長化されていることが確認できました。
```bash
node8.vespanet: 2
node9.vespanet: 3
node10.vespanet: 3
node11.vespanet: 2
```

# 最後に
`content cluster`の冗長化について確認し、共有させていただきました。
スタンバイでも`content cluster`のnode数や冗長化構成は負荷に合わせて試行錯誤しながら調整をしています。

Vespaを利用する側として、今後も学んで最適な構成で利用していきたいです！

最後までお読みいただき、誠にありがとうございました。

# 参考
https://github.com/vespa-engine/sample-apps/tree/master/examples/operations/multinode-HA

https://docs.vespa.ai/en/performance/sizing-examples.html