6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SRAAdvent Calendar 2019

Day 21

PostgreSQLの統計情報を可視化(フロントエンド編)

Last updated at Posted at 2019-12-17

#はじめに
「PostgreSQLの統計情報を可視化」の3章です。この章では2章の「バックエンド編」で作成したAPIを使ってPostgreSQLの統計情報をグラフ化する、フロントエンドにフォーカスして話を進めます。

フロントエンドのモジュール構成

Nuxt.jsのフロントエンド側を大雑把に分けると、画面構成モジュールと、データ更新・保持モジュールがあります。まずはインターフェース仕様が決まっている「データ更新・保持モジュール」から作成します。

データ更新・保持モジュール

データを更新・保持するモジュールには、動的なデータの読み出し、書込み、保持を定義するstoreモジュール、静的なデータを保持するassetsモジュールがあります
バックエンドとの動的なデータ送受信にはaxiosを使います。
動的なデータの読み書き、保持を行いますので、storeモジュールに記述します。
###storeモジュール
storeモジュールは以下の4つの定義から構成されます。

名称 役割
state 保持するデータを定義
getters データ読み出し処理
mutations データ書込み処理
actions データ取得・操作

バックエンドのAPIを呼び出して統計情報を取得・保持するsoteモジュールのプログラムは以下の様になります。

store/statistics.js
/**
* 統計情報取得Storeモジュール
*/
import Vue from 'vue'
import Vuex from 'vuex'
import * as api from './api'
Vue.use(Vuex)

export const state = () => ({
  dbSize: [],         // DBサイズ
  slowQuery: [],      // スロークエリ
  sqlCall: null       // SQL実行回数
})
// Getters
export const getters = {
  getDbSize (state) {
    return state.dbSize
  },
  getSlowQuery (state) {
    return state.slowQuery
  },
  getSqlCall (state) {
    return state.sqlCall
  }
}
// Mutations
export const mutations = {
  setDbSize (state, dbSize) {
    state.dbSize = dbSize
  },
  setSlowQuery (state, slowQuery) {
    state.slowQuery = slowQuery
  },
  setSqlCall (state, sqlCall) {
    state.sqlCall = sqlCall
  }
}
// Actions
export const actions = {
  // DBサイズ取得
  async doGetDbSize (params = null) {
    const res = await api.getApi('/statistics/getDbSize')
    if (res.status === 200) {
      this.commit('statistics/setDbSize', res.data.value)
    } else {
      this.commit('statistics/setDbSize', [])
    }
  },
  // SQL実行回数取得
  async doGetSqlCall (params = null) {
    const res = await api.getApi('/statistics/getSqlCalls')
    if (res.status === 200) {
      this.commit('statistics/setSqlCall', res.data)
    } else {
      this.commit('statistics/setSqlCall', null)
    }
  },
  // スロークエリ取得
  async doGetSlowQuery (params = null, data) {
    const threshold = data.threshold || 1000
    const limit = data.limit || 20
    const arg = 'threshold=' + threshold + '&limit=' + limit
    const res = await api.getApi('/statistics/getSlowQuery', arg)
    if (res.status === 200) {
      this.commit('statistics/setSlowQuery', res.data.value)
    } else {
      this.commit('statistics/setSlowQuery', [])
    }
  }
}

ここではaxsiosを使ってAPIの送受信処理を、共通モジュールのapi.jsとして定義しています。

store/api.js
/**
* API共通モジュール
*   POSTとGETのインターフェース共通化
*/
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)

// POST呼び出し共通処理
export const postApi = async function(uri, data = null) {
  try {
    var res = await axios.post(process.env.apiUrl + uri , data)
    return res
  } catch (e) {
    return {status: 500}
  }
}

//GET呼び出し共通処理
export const getApi = async function(uri, options = null) {
  var param = (options != null) ? '?' + options : ''
  try {
    var res = await axios.get(process.env.apiUrl + uri + param)
    return res
  } catch (e) {
    return {status: 500}
  }
}

##画面を構成するモジュール
さて、いよいよ画面作成です。
Nuxtの画面を構成するモジュールには、画面の共通的な外観を定義するlayoutsモジュール、画面の構成を定義するpagesモジュール、画面の共通部品を定義するcomponentsモジュールがあります。
今回は可視化の為にグラフ作成を行いますので、グラフ描画コンポーネントを自動読込するためにpluginsモジュールも使います。

描画コンポーネントにはマテリアルコンポーネントフレームワークのvuetify.jsを使います。
・Vuetifyの公式サイト https://vuetifyjs.com/ja/

vuetifyのマテリアルデザインアイコンには種類が豊富なMaterial Design Iconsを使います。
・Material Design Iconsの公式サイト https://materialdesignicons.com/

グラフ作成のパッケージは色々ありますが、今回は直感的に使いやすいApexchartsとvue用のコンポーネントであるVue-Apexchartsを使います。
・Apexchartsの公式サイト https://apexcharts.com/
・Vue Apexchartsの公式サイト https://vuejsprojects.com/vue-apexcharts

###pluginsモジュール
画面描画用のvuetifyとvue-apexchartsのコンポーネント読み込み用のコードをプラグインに作成します。

plugins/vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import '@mdi/font/css/materialdesignicons.css'

Vue.use(Vuetify)

export default (ctx) => {
  const vuetify = new Vuetify({
    theme: {
      dark: false // darkのテーマも格好いいですが、グラフの色が見づらくなるのでfalseにしました
    },
    icons: {
      iconfont: 'mdi' // iconは種類が豊富なmdiを使います
    }
  })

  ctx.app.vuetify = vuetify
  ctx.$vuetify = vuetify.framework
}
plugins/vue-apexcharts.js
import Vue from 'vue'
import VueApexCharts from 'vue-apexcharts'

Vue.use({
  install (Vue, options) {
    Vue.component('apexchart', VueApexCharts)
  }
})

作成したプラグインをnuxt.config.jsに登録します。

nuxt.config.js
省略
  plugins: [
    '@plugins/vuetify',
    {
      src: '@plugins/vue-apexcharts',
      ssr: false
    }
  ],
省略

###layoutsモジュール
全画面共通のレイアウトに以下のメニューを用意して画面が切り替わるようにしたいと思います。

メニュー mdiアイコン 表示ページ
データベース容量 mdi-database /db_size
アクセス負荷 mdi-align-vertical-bottom /access_load
スロークエリ mdi-tortoise /slow_query
layouts/monitor.vue
<template>
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      :mini-variant="miniVariant"
      :clipped="clipped"
      fixed
      app
    >
      <v-list>
        <v-list-item
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          router
          exact
        >
          <v-list-item-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <v-app-bar
      :clipped-left="clipped"
      fixed
      app
    >
      <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-toolbar-title v-text="title" />
    </v-app-bar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
    <v-footer
      :fixed="fixed"
      app
    >
      <span>&copy; 2019</span>
    </v-footer>
  </v-app>
</template>
<script>
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
Vue.use(Vuetify)
export default {
  data () {
    const $t = this.$t.bind(this)
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      miniVariant: false,
      right: true,
      rightDrawer: false,
      title: $t('label.pgbmon'),
      items: [
        {
          icon: 'mdi-database',
          title: $t('label.dbSize'),
          to: '/db_size'
        },
        {
          icon: 'mdi-align-vertical-bottom',
          title: $t('label.accessLoad'),
          to: '/access_load'
        },
        {
          icon: 'mdi-tortoise',
          title: $t('label.slowQuery'),
          to: '/slow_query'
        }
      ]
    }
  }
}
</script>

国際化対応ファイルに日本語の文字列を登録します。

lang/ja.json
{
  "label": {
    "accessLoad": "アクセス負荷",
    "db": "データベース",
    "dbSize": "データベース容量",
    "homePage": "トップページ",
    "host": "ホスト",
    "otherError": "エラー",
    "pageNotFound": "ページはありません",
    "pgbmon": "Pg バランスモニター",
    "slowQuery": "スロークエリ"
  },
  "button": {},
  "tooltip": {
    "menu": "メニュー"
  },
  "message": {
    "otherError": "エラーが発生しました",
    "pageNotFound": "指定のページはありません"
  }
}

こんな感じのメニューになります。
menu.JPG

pagesモジュール

とりあえず、空のindx.vueを用意します。

pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <v-card />
    </v-flex>
  </v-layout>
</template>

<script>
export default {
  layout: 'monitor'
}
</script>

###DBサイズの可視化画面
ここでは、個々のDBサイズを見るのではなく、突出して肥大化したDBはないか確認のため、DB間のサイズのバランスを可視化したいと思います。

pages/db_size.vue
<template>
  <v-container fluid fill-height>
    <v-layout
      align-center
      row wrap
    >
      <v-flex xs12 md6>
        <v-card class="ma-2 pa-3">
          <apexchart type="donut" height="380" :options="chartOptions" :series="series" />
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
export default {
  name: 'DbSizeChart',
  layout: 'monitor',
  data () {
    const $t = this.$t.bind(this)
    return {
      series: [],
      chartOptions: {
        labels: this.getLabel(),
        dataLabels: {
          enable: false
        },
        plotOptions: {
          pie: {
            donut: {
              labels: {
                show: true,
                value: {
                  formatter: function (val) {
                    return String(val).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')
                  }
                }
              }
            }
          }
        },
        title: {
          text: $t('label.dbSize')
        },
        responsive: [{
          breakpoint: 480,
          options: {
            chart: {
              width: 200
            },
            legend: {
              position: 'bottom'
            }
          }
        }]
      }
    }
  },
  async created () {
    await this.initialize()
  },
  methods: {
    async initialize () {
      await this.$store.dispatch('statistics/doGetDbSize')
      this.series = this.getVal()
      this.chartOptions.labels = this.getLabel()
    },
    getVal () {
      const dbSize = this.$store.getters['statistics/getDbSize']
      const val = []
      for (let i = 0; i < dbSize.length; i++) {
        val.push(Number(dbSize[i].result[0].pg_database_size))
      }
      return val
    },
    getLabel () {
      const dbSize = this.$store.getters['statistics/getDbSize']
      const label = []
      for (let i = 0; i < dbSize.length; i++) {
        label.push(dbSize[i].db + '@' + dbSize[i].host)
      }
      return label
    }
  }

}
</script>

こんな感じでDB間のDBサイズがドーナツグラフで表示されます。
db_size.JPG

多少、偏りが見られるようです。。。

###クエリの種類と数の可視化画面
次に、DB間でSELECT/INSERT/UPDATE等のクエリに偏りがないか、確認するためのグラフを作成したいと思います。

pages/access_load.vue
<template>
  <v-container fluid fill-height>
    <v-layout
      align-center
      row wrap
    >
      <v-flex v-for="(item, i) in series"
              :key="i"
              xs12 md4
      >
        <v-card class="item ma-2 pa-3">
          <apexchart type="radar" height="350" :options="chartOptions[i]" :series="item" />
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
export default {
  name: 'AccessLoadChart',
  layout: 'monitor',
  data () {
    return {
      // グラフの色を5種類用意します。
      color: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0'],
      series: [],
      chartOptions: []
    }
  },
  async created () {
    await this.initialize()
  },
  methods: {
    async initialize () {
      await this.$store.dispatch('statistics/doGetSqlCall')
      const data = this.$store.getters['statistics/getSqlCall']
      var series = []
      const label = []
      for (let i = 0; i < data.value.length; i++) {
        label.push(data.value[i].db + '@' + data.value[i].host)
        const val = data.value[i].result
        for (let j = 0; j < val.length; j++) {
          var call = []
          if (Array.isArray(series[val[j].sql])) {
            call = series[val[j].sql]
          }
          if (val[j].call !== null) {
            call.push(Number(val[j].call))
          } else {
            call.push(0)
          }
          series[val[j].sql] = call
        }
      }
      this.series = []
      this.chartOptions = []
      var i = 0
      for (var key in series) {
        var val = series[key]
        var index = i %  this.color.length
        this.series.push([{ name: key, data: val }])
        var option = {
          title: { text: key },
          labels: label,
          fill: {
            colors: [this.color[index]]
          }
        }
        this.chartOptions.push(option)
        i++
      }
    }
  }
}
</script>

こんな感じでDB間のSELECT/INSERT/UPDATEクエリの実行数がレーダーチャート表示されます。
access_cnt.JPG
クエリはバランスよく分散されているようです。

###スロークエリの可視化
最後にDBによって処理負荷に偏りがないか、DB間のスロークエリを比較するグラフを作成したいと思います。

pages/slow_query.vue
<template>
  <v-container fluid fill-height>
    <v-layout align-center row wrap>
      <v-flex xs12 md12>
        <v-card class="item ma-2 pa-3">
          <p class="font-weight-black">{{ title }}</p>
          <apexchart type="bar" height="400" :options="chartOptions" :series="series" />
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>
<script>
export default {
  layout: 'monitor',
  data () {
    return {
      threshold: process.env.threshold,
      limit: process.env.limit,
      series: [],
      chartOptions: {
        plotOptions: {
          bar: {
            barHeight: '100%',
            horizontal: false
          }
        },
        dataLabels: {
          enabled: false
        },
        xaxis: {
          labels: { show: false }
        }
      },
      title: 'slow query'
    }
  },
  async created () {
    await this.initialize()
  },
  methods: {
    async initialize () {
      this.series = []
      await this.$store.dispatch('statistics/doGetSlowQuery', { threshold: this.threshold, limit: this.limit })
      const data = this.$store.getters['statistics/getSlowQuery']
      for (let i = 0; i < data.length; i++) {
        var maxTime = []
        var label = []
        const val = data[i].result
        for (let j = 0; j < val.length; j++) {
          maxTime.push(Math.round(val[j].max_time))
          label.push(String(val[j].calls))
        }
        var name = data[i].db + '@' + data[i].host
        this.series.push({ name: name, data: maxTime })
      }
    }
  }
}

</script>

こんな感じで登録したDBのスロークエリーの時間が棒グラフで表示されます。
slow_query.JPG

全部バランスよく、遅いクエリーがあるようです。。。

##まとめ
ここまで簡単ですが、PostgreSQLの統計情報を可視化するコードを書いてみました。
統計情報の可視化は以前から興味がありましたので、軽い気持ちで始めてみましたが、思いのほか簡単に綺麗なグラフが出る事に驚きました。
今後、DBのデモなどに使えると良いなと思っています。

単体テストをやってみたい方は次の章にお進みください。
>>PostgreSQLの統計情報を可視化(単体でスト編)

なお、コード全体をご覧になりたい方はGitHubからダウンロードできるようにしてみましたので、ご利用ください。
GitHub https://github.com/at-mitani/pgbmon

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?