Metricbeatの古いインデックスを自動クローズする


# Metricbeatって？

サーバのCPU負荷状況やメモリ使用状況や通信量やプロセス一覧とかの統計情報を時系列で収集する可視化ツール。データはElasticsearchに蓄積し、Kibanaとセットで可視化できる。
ある時刻にバースト的に負荷かかってますよ、がグラフひと目でわかるのがうれしくて導入した。
https://www.elastic.co/jp/beats/metricbeat


# Elasticsearchのヒープメモリ枯渇問題

Metricbeatは自分のデータをElasticsearchに保存するにあたり、`metricbeat-6.7.1-2021.04.16`てな名前で1日に1つインデックスを自動作成する。
うちの環境だと1インデックスあたり約13万ドキュメント、約30MB。1ヶ月で900MBちょっと、1年で約11GB、5年で約55GB。
今どきのディスク容量ならたいしたもんではないのだが、これだけのインデックスをopenにし続けているとElasticsearchのヒープメモリを食い尽くす可能性がある。Elasticsearchに割り当てられたヒープを使い切ればGC地獄に陥り致命的事態になる。
http://n-agetsuma.hatenablog.com/entry/2017/12/06/000000

稼働から数ヶ月経ったところでこれに引っかかり、Elasticsearchが正常に応答を返さなくなる事態に見舞われた。その時は古いMetricBeatインデックスをクローズしてElasticsearchサービスを再起動し直すことができた。

# インデックスの自動管理をしたい

再発防止のため、古いMetricBeatインデックスを自動でクローズすることにする。普段見るのは過去1週間くらいだし、時々見る範囲でも1ヶ月前の同日付程度である。そんなに前まですぐ見れる状態である必要はない。

## 環境

サーバー：WindowsServer2016
Elasticsearch：6.7.1
MetricBeat：6.7.1

Windowsなので慣れたPowerShellを使う。

## インデックスのクローズ操作

Close index APIでクローズする。
https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-close.html

```powershell:PowerShellでインデックスクローズ操作
$Param = @{
  Method = "Post"
  ContentType = "application/json"
  Uri = "http://ServerName:9200/IndexName/_close"
}
Invoke-RestMethod @Param
```

`metricbeat-6.7.1-2021.04.16`とMetricBeat＋バージョン＋日付で名付けられているので、例えば3ヶ月前の日付を生成してインデックス名を指定してクローズするのを毎日実行すればいい。


## 日付生成

```powershell:フォーマット指定して日付を生成
$Today = Get-Date
$IndexDate_Close = ($Today.AddDays(-90)).ToString("yyyy.MM.dd")
#2021.04.16 の形で出る
```


## 対象インデックスの存在確認

MetricBeatは常時動作していて毎日インデックスを生成しているから「その日付のインデックス無いよ」は無いはず…だけどこういう確認を普段なんのかの理屈つけてサボっているので、後学のため書く。

```powershell:HTTP応答チェック
#渡されたUrlの存在確認する
function Get-HttpStatusCode($Url){
  try{
      $Resp = Invoke-WebRequest -Method "Head" -Uri $Url
      $StatusCode = $Resp.StatusCode
  }
  catch{
      $StatusCode = $_.Exception.Response.StatusCode.value__
      #インデックスオープンなら200、存在しないと404
  }
  Write-Output $StatusCode #戻り値
}
```
PowerShellの戻り値は以下の記事がわかりやすかった。

PowerShellにおける"戻り値"と"Return"について
https://blog.shibata.tech/entry/2015/07/05/114903

>なんつーか、"式を評価して最終的な値を返す"ってより、"式を評価して随時値をストリームに放流する"って感じがしっくりくる。

#古いインデックスをクローズするスクリプト
これを仕掛けてしばらく様子を見る。
ディスク容量は余裕があるので今の所デリートは行わない。必要になったら同じしくみでできるだろう。

```powershell:IndexManage.ps1
#渡された文字列をログ追記するだけ
function Out-OperationLog([string]$Line){
  Out-File -InputObject $Line -FilePath ./IndexClose.log -Encoding default -Append
}

#渡されたUrlの存在確認する
function Get-HttpStatusCode($Url){
  try{
      $Resp = Invoke-WebRequest -Method "Head" -Uri $Url
      $StatusCode = $Resp.StatusCode
  }
  catch{
      $StatusCode = $_.Exception.Response.StatusCode.value__
      #インデックスオープンなら200、存在しないと404
  }
  Write-Output $StatusCode #戻り値
  #200、404以外ならエラー文まるごと返してログに書くのがいいかも…
}

#Metricbeat特定日インデックスクローズ
function Close-MetricbeatIndex ($Date) {
  $ApiUrl = "http://${ServerName}:9200/${IndexNamePrefix}${Date}/_close"
  $Param_ESClose = @{
    Method = "Post"
    ContentType = "application/json"
    Uri = $ApiUrl
  }
  try{
    $Resp = Invoke-RestMethod @Param_ESClose
    Out-OperationLog "`"${ApiUrl}`",$(${Resp}.acknowledged)" #成功ならTrue
  }
  catch{
    Out-OperationLog "`"${ApiUrl}`",IndexOperation Error"
  }
}

#対象サーバー等の設定値
$ServerName = "ServerName"
$IndexNamePrefix = "metricbeat-6.7.1-"

#日付生成
$Today = Get-Date
$IndexDate_Close = ($Today.AddDays(-90)).ToString("yyyy.MM.dd")
#$IndexDate_Delete = ($Today.AddDays(-120)).ToString("yyyy.MM.dd") #今後使う予定

#対象インデックスの存在確認して処理分岐
$IndexUrl = "http://${ServerName}:9200/${IndexNamePrefix}${IndexDate_Close}"
$HttpResp = Get-HttpStatusCode $IndexUrl
switch ($HttpResp) {
  200 { Close-MetricbeatIndex $IndexDate_Close }
  404 { Out-OperationLog "`"${IndexUrl}`",Index 404" }
  Default { Out-OperationLog "`"${IndexUrl}`",HttpResponse Unknown Error"}
}
#switchとGet-HttpStatusCodeの役割分担がごちゃっている…
```

# （別解）Curator

Elasticsearch公式提供ツールに、インデックスの管理を支援するCuratorがある。ymlで設定を書いてインデックスを操作できる。
https://www.elastic.co/guide/en/elasticsearch/client/curator/5.8/index.html

これあんまり更新頻度が高くなくて（それほど更新する内容もないんだろうが）、ほんとに続くのかな？これ覚えるより自力で書くこと覚えたほうがいいんじゃねーの？と思ってる。