WESEEK Tech Blog

WESEEK のエンジニアブログです

Dev in Container on WSL2 でボリュームのパフォーマンスを比べてみる

f:id:yukitakei:20200613040453j:plain

こんにちは。武井です。

2020年5月末に待望の Windows 10 May 2020 Update が公開され、WESEEK, Inc. で開発中の GROWI でも WSL2 と Docker Desktop を利用して Dev in Container を実現した新しい開発スタートアップ を採用しました。

本日のエントリーでは、その開発時に利用するストレージ(Docker volume)の種類によってパフォーマンスがどれくらい変わるのかを実験した際の結果を紹介します。

続きを読む

Raspberry Piと温度・湿度・気圧センサーと天気予報APIでペット環境を見守る

こんにちは、 takayuki です。

最近、知り合いのインコ部屋にRaspberry Piと温度・湿度・気圧センサーを設置し、天気予報APIも使用して、お留守番中の環境を確認できるようにしました。

動機

インコはもともと暖かく湿気のある地域に生息しており、寒さに弱い生き物です。
日中に家を空けていると、インコ部屋の温度・湿度が保たれているか、心配になります。
以前、部屋の温度を数日低くしてしまったことで、インコが体調を崩してしまったことがありました。

現在は、家電メーカーから発売されているペットカメラを設置していますが、これは微妙にかゆいところに手が届かないと感じていました。
ペットカメラには、標準カメラ、赤外線カメラ、温度センサー、動作センサー、音センサーが搭載されています。

標準カメラ、赤外線カメラは、外出先でも様子が見られるため、非常に便利です。
温度センサーは、ペットカメラの画角の関係で距離を取る必要があり、インコ部屋の外に置いています ※。
そのため、計測される温度は人がいるところの室温で、インコが過ごしているケージ内の室温はわかりません。また、温度センサーしかないため、湿度はわかりません。

※インコ部屋はアクリルケージで囲い、ペット用のヒーターで暖めています。

そこで今回、温度・湿度・気圧センサーをインコ部屋の中に設置し、環境を計測できるようにしました。

構成

今回構築した構成は下記の通りです。

f:id:Aqutam:20200303020524p:plain

ハードウェア

# 品名 数量
1 Raspberry Pi 4 Mlodel B 1
2 OKdoブラック2ピース構造スライドケース 1
3 TOSHIBA microSDHC EXCERIA 16GB 100MB/s 1
4 BME280 温湿度・気圧センサ 2
5 タカチ SWプラケース SW-50B 2
6 6極4芯モジュラーケーブル 2m 3
7 モジュラーローゼット MJ-4S-MG 1
8 モジュラーソケット6極4芯 TM5RJ3-64 3

ソフトウェア

ネットワーク

  • インコの家は、 LTE を経由してインターネットに接続しています。
  • インコの家と自宅は相互に通信できるよう、 IPsec で拠点間接続しています。

センサーの製作

温度・湿度・気圧センサーは、秋月電子通商で販売されている BME280 を使用しています。このセンサーは、I2Cで制御できます。

Raspberry Pi と BME280 を下記のように配線していきます。

f:id:Aqutam:20200309234805p:plain

BME280は、ジャンパを設定することで、 0x760x77 の2つのI2Cアドレスを使用できます。
今回、2つセンサーを接続するため、それぞれのアドレスになるようにジャンパをはんだ付けします。

I2Cは、電源に2本、信号に2本 (SCL, SDA) の合計4本の配線が必要になります。
Raspberry PiとセンサーをつなぐI2Cバスは、容易に取り回しができるようにしたいと考え、4芯モジュラーケーブルを採用しました。
モジュラーケーブルは家電量販店で調達できるものなので、最適な長さのものに交換して、センサーの位置を自由に変更できます。

タカチ SWプラケースを削り、センサーと2つのモジュラージャックを収めました。
2つのモジュラージャックは導通しているため、他のセンサーをデイジーチェーンできるようにしています。
写真では穴が空いていませんが、温度・湿度・気圧を計測できるよう、ケースの前面には通気孔を空けています。

f:id:Aqutam:20200302232643j:plain f:id:Aqutam:20200302232624j:plain

Raspberry Pi からはI2Cで使用する4本の線を引き回し、モジュラーローゼットを配置しています。

f:id:Aqutam:20200302232435j:plain f:id:Aqutam:20200302232512j:plain

設置

下記の写真のように、インコ部屋に温度センサーを設置しています。
手作業で穴を空けたため、ガタガタになってしまっています。

f:id:Aqutam:20200302232558j:plain f:id:Aqutam:20200302232609j:plain

実装

センサーからのデータ取得

センサーからのデータの取得は、こちらのコードを参考にさせていただきました。 samplecodes/BME280 at master · SWITCHSCIENCE/samplecodes · GitHub

こちらのコードをベースに、下記CSVを出力するように実装します。

Date,GR Temp,GR Press,GR Hum,OR Temp,OR Press,OR Hum
2020-03-01 17:00:00,23.94,1004.81,53.46,21.53,1003.36,62.13
2020-03-01 17:30:00,25.75,1005.25,50.85,22.10,1003.73,62.95
2020-03-01 18:00:00,26.55,1005.34,48.78,22.22,1003.83,62.06
2020-03-01 18:30:00,26.58,1005.70,47.49,22.00,1004.25,62.20
2020-03-01 19:00:00,26.41,1005.88,42.79,21.68,1004.29,50.83
2020-03-01 19:30:00,26.18,1006.09,40.66,21.65,1004.61,51.54

天気予報APIからの実績と予報データの取得

天気予報データは、 Rakuten RapidAPI を使用し、このマーケットプレイスで公開されている、 Dark Sky API を使用しました。
Rakuten RapidAPI は、1つのAPIキーを使用して世界中の様々なAPIを利用できるようにした、プロキシサービスです。

様々なAPIをWebで簡単にテストでき、Pythonをはじめいろいろな言語に対応したAPIをコールするためのコードスニペットも用意されているため、結構便利です。

f:id:Aqutam:20200310003313p:plain

Pythonコードスニペットをダウンロードし、パースして下記CSVを出力するためのコードを追加実装しました。

Date,WT Temp,WT Press,WT Hum,WT Sum,WT PrecipIntensity,WT PrecipProbability,WT ApparentTemperature,WT WindSpeed,WT WindBearing,WT CloudCover
2020-03-01 17:00:00,14.79,1015.4,42.0,曇り,0,0,14.79,2.66,167,77
2020-03-01 17:30:00,14.2,1015.7,44.0,曇り,0,0,14.2,2.74,180,90
2020-03-01 18:00:00,13.65,1016,47.0,曇り,0,0,13.65,2.89,191,94
2020-03-01 18:30:00,13.14,1016.6,50.0,曇り,0,0,13.14,3.13,187,96.0
2020-03-01 19:00:00,12.73,1017.2,52.0,曇り,0,0,12.73,3.51,180,92.0
2020-03-01 19:30:00,12.46,1017.6,55,曇り,0,0,12.46,3.93,177,91.0

グラフの描画

グラフの描画は、まず Jupyter Notebook で行いました。
Jupyter Notebook はブラウザ上で Python のコードを記述でき、グラフなど視覚的な結果を確認しながらプログラミングできるツールです。

センサーからのデータ取得天気予報APIからの実績と予報データの取得 で出力していた2つのCSVを読み込ませ、 Pandas で加工していきます。
そのデータを Matplotlib でグラフを描画するようにしました。

f:id:Aqutam:20200310004559p:plain f:id:Aqutam:20200310004610p:plain

Slackへのアップロード

Jupyter Notebook でグラフが描画できたら、ここから Python のコードに落とし込みます。
Pythonコード上で、 Slack files.upload API を使用して、Slack の特定 channel にグラフをアップロードするようにします。
cron に登録し、定期的にグラフをアップロードするようにします。

動作画面

下記のように、定期的に1週間ごと、1日ごとの温度・湿度・気圧の推移をグラフに描画しています。
緑色、黄色の線はセンサーのデータで、グレーの線は DarkSky API から取得した天気データです。

f:id:Aqutam:20200314120352p:plain

DarkSky API は、1時間ごとの天気予報も取得することが可能です。
これを使用して、1日先の1時間ごとの温度・湿度・気圧・降水量・降水確率を取得して、下記のようにグラフをプロットしています。
毎晩1回Slackにアップロードし、翌日のインコ部屋の温度管理の計画を立てるための情報として使用しています。

f:id:Aqutam:20200314120359p:plain

使用してみての感想

今回は温度・湿度・気圧センサーと天気予報APIから取得したデータをグラフに描画する仕組みを構築しました。

使用を開始してからは1ヶ月弱が過ぎました。
今までは、インコ部屋に温湿度計を立てて、ペットカメラ越しに確認しに行っていたのですが、このシステムを構築してからは定期的にSlackに情報が通知されるため、だいぶ把握が楽になりました。
湿度・温度・気圧・降水確率・降水量のグラフは、それぞれ別の画像で書き出しSlackにアップロードしているのですが、短時間に連続してアップロードしているためか、たまにアップロードに失敗します。
そんなに頻度は多くないので、失敗したときは手動でグラフ描画を再実行しています。

製作時の感想は、ケースの加工がなかなか大変でした。バンドソーやボール盤など電動工具がなく手作業で削ったり穴を空けていたので結構疲れました。
ソフトウェアの実装は、参考にできるコードがすでにあったり、便利なAPIサービスがあったりと、すんなり実装できました。

インコ部屋の環境は、今後下記のように改良してみたいと考えています。

  • Prometheus・Grafana 環境の構築
  • 温度のしきい値に応じて Slack への通知 (Prometheus Alertmanager)
  • 温度に応じて赤外線でエアコンの電源ONにする
  • I2Cバスのノイズ対策

CircleCI 2.1 による kubernetes で動作するアプリケーションの CI/CD 事始め (Ruby on Railsアプリケーション)

はじめに

この記事は CircleCI Advent Calendar 2019 の 19 日目の記事です。

拙稿となりますが Ruby on Rails, Vue.js によるモダン WEB アプリケーション 実践編 (その2) にて GitHub Action を使って k8s 上で動作する Ruby on Rails アプリケーションを CI/CD する Workflow を作ったので、同じ機能を CircleCI で作ってみることにします。

尚、Rails アプリケーションを前提としてますが、k8s の deployment manifest で定義されたアプリケーションであれば概ね流用できると思います。

なぜ CircleCI?

アドベントカレンダーのネタとして何かないかと探したことがきっかけです。(笑)

Wercker, CircleCI, GitHub Actions, GitLab CI/CD ではどれも YAML を定義することで CI/CD 環境を作ることが出来ます。 (CI/CD をオンプレ環境で構築する場合は Jenkins を使う環境があると思います)

そこで、1つ処理の実例を題材として GitHub Actions, CircleCI のそれぞれで違いを探してみようと思い立ったため CircleCI を使うことにしました。

前提事項

CircleCI が VCS リポジトリと連携できるよう初期設定が既に完了していることを前提としています。

初めて CircleCI の Workflow を追加する場合は、Getting Started を参考にして Workflow を設定してみて下さい。

構築する CI/CD の動作説明

CI では VCS レポジトリの全ブランチを対象に、コミットされた時点のコードに対してテストを実行するものとします。

CD では VCS リポジトリのデプロイ用ブランチを対象に、アプリケーションの Docker container image をビルド及び、k8s へデプロイするものとします。

CI の動作説明

CIの動作図
CIの動作図

CI の動作は上図にあるように次のとおりです。

  1. 開発者VCSリポジトリ(ex.GitHub) に git push を実行
  2. VCSリポジトリ(ex.GitHub)CIツール(ex.CircleCI) に PUSHイベントを通知
  3. CIツール(ex.CircleCI) がテストを実行
  4. CIツール(ex.CircleCI)VCSリポジトリ(ex.GitHub) CI結果(テスト結果)を通知
  5. (opt) CIツール(ex.CircleCI)開発者 にCI結果(テスト結果)を通知

※ 今回「5. CIツール(ex.CircleCI)開発者 にCI結果(テスト結果)を通知」は設定しません。

CD の動作説明

CDの動作図
CDの動作図

CD の動作は上図にあるように次のとおりです。

  1. 開発者VCSリポジトリ(ex.GitHub) にてPR/MRをマージ
  2. VCSリポジトリ(ex.GitHub)CDツール(ex.CircleCI) にマージ(又はブランチ更新)イベントを通知
  3. CDツール(ex.CircleCI) にてDockerイメージをビルド
  4. CDツール(ex.CircleCI)Dockerイメージレジストリ(ex.Docker Hub) にDockerイメージをプッシュ
  5. CDツール(ex.CircleCI)Kubernetesk8sマニフェストを更新
  6. Kubernetes にてローリングアップデート
  7. CDツール(ex.CircleCI)Kubernetes にてローリングアップデートの成功を確認
  8. (opt) CIツール(ex.CircleCI)開発者 にCI結果(テスト結果)を通知

CircleCI の Workflow / Job を作成する

GitHub Actions から CircleCI に移行する際に参考になるドキュメントが migrating-from-github にあります。

GitHub Actions では 1 Workflow は 1 ファイルであり、Workflow の中では複数の Job が定義できます。Workflow 設定は .github/workflows 配下に保存します。 一方で CircleCI では複数の Workflow が 1 ファイルに保存され、複数の Job が定義できます。Workflow 設定は .circleci/config.yml に保存します。

CI 環境

まずは CI 環境を構築することにします。

GitHub Actions の CI Workflow

# .github/workflows/test.yml
name: Test

on: push

jobs:

  test:

    runs-on: ubuntu-latest
    container: ruby:2.5.3

    steps:
    - uses: actions/checkout@v1

    - name: Initialize
      env:
        RAILS_ENV: test
        DISABLE_SPRING: 1
      run: |
        # install tools
        curl -sL https://deb.nodesource.com/setup_10.x | bash -
        apt-get install -y nodejs
        npm install yarn@1.13.0
        gem install bundler -v 1.17.3
        # initialize DB
        bundle install
        bundle exec rails db:migrate

    - name: Test
      run: bundle exec rails test

CircleCI の CI Workflow

# .circleci/config.yml
version: 2.1
jobs:
  test:
    docker:
      - image: ruby:2.5.3

    steps:
      - checkout

      - run:
          name: Initialize
          environment:
            RAILS_ENV: test
            DISABLE_SPRING: 1
          command: |
            # install tools
            curl -sL https://deb.nodesource.com/setup_10.x | bash -
            apt-get install -y nodejs
            npm install yarn@1.13.0
            gem install bundler -v 1.17.3
            # initialize DB
            bundle install
            bundle exec rails db:migrate

      - run:
          name: Test
          command: bundle exec rails test

workflows:
  ci:
    jobs:
      - test

GitHub Actions と CircleCI の比較

GitHub Actions と CircleCI のそれぞれの Workflow で CI 環境を実現する YAML ファイルを見てみましたが、内容はほぼ同じに記載することが出来ました。

記載内容に違いが出るのは、並列実行、キャッシュ利用、実行ホストのOS/リソース指定などを行おうとした時だと思われます。 CI として紹介した Workflow は非常に単純なものであったため差異は出ませんでした。

CD 環境

次に CD 環境を構築することにします。

GitHub Actions の CD Workflow

# .github/workflows/docker_build_and_push.yml
name: Docker Image CI

on:
  push:
    branches:
    - stable

jobs:

  docker_build_and_push:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Build the Docker image
      env:
        # [TODO] ${{ github.repository }} から repository 名だけ抽出する
        IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
      run: docker build . --file Dockerfile --tag $IMAGE_NAME

    - name: Login to Docker hub
      run: docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }}

    - name: Push the Docker image to Docker hub
      env:
        # [TODO] ${{ github.repository }} から repository 名だけ抽出する
        IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
      run: docker push $IMAGE_NAME
# .github/workflows/deploy.yml
name: Deploy docker container to kubernetes

on:
  release:
    types: [published]

jobs:

  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@master

    - name: migrate on cluster
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
      with:
        args: >
          run tmp-migrate -i --generator=run-pod/v1 --rm
          --image ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
          --overrides='{
            \"spec\":{
              \"containers\":[{
                \"name\":\"app\",
                \"image\":\"${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}\",
                \"command\":[\"bash\"],
                \"args\":[\"-c\",\"rails db:migrate SECRET_KEY_BASE=$(rails secret)\"],
                \"envFrom\":[{\"secretRef\":{\"name\":\"vue-practice\"}}]
              }]
            }
          }'

    - name: deploy to cluster
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
        # [TODO] ${{ github.repository }} から repository 名だけ抽出する
        IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
        K8S_NAMESPACE: vue-practice
        K8S_DEPLOYMENT_NAME: vue-practice
        K8S_CONTAINER_NAME: app
      with:
        args: set image --namespace $K8S_NAMESPACE --record deployment/$K8S_DEPLOYMENT_NAME $K8S_CONTAINER_NAME=$IMAGE_NAME

    - name: verify deployment
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
        K8S_NAMESPACE: vue-practice
        K8S_DEPLOYMENT_NAME: vue-practice
      with:
        args: rollout status --namespace $K8S_NAMESPACE deployment/$K8S_DEPLOYMENT_NAME

CircleCI の CD Workflow

Settings >> BUILD SETTINGS >> Environment Variables にて、環境変数を以下のとおり設定する必要があります。

名前 説明
DOCKER_HUB_USERNAME Docker Hub アカウントのユーザ名
DOCKER_HUB_PASSWORD Docker Hub アカウントのパスワード
KUBECONFIG_STR ※1 kubectl config の内容を base64 変換したもの ※2

※1 circleci/kubernetes の install-kubeconfig コマンドでは設定ファイルの内容を KUBECONFIG 環境変数で設定することが default なのですが、kubectl は KUBECONFIG 環境変数を設定ファイルへのパスとして使用するため KUBECONFIG 以外の名前にしましょう。(参考)

※2 echo -n "$KUBECONFIG" | base64 -d により復元できる値にすること

# .circleci/config.yml
version: 2.1

orbs:
  docker-orb: circleci/docker@0.5.19
  kube-orb: circleci/kubernetes@0.10.1

commands:
  migrate_vue_practice:
    parameters:
      image_name:
        type: string
    steps:
      - run: |
          kubectl run tmp-migrate -i --generator=run-pod/v1 --rm -n vue-practice --image << parameters.image_name >> \
            --overrides="'{ \
              \"spec\":{ \
                \"containers\":[{ \
                  \"name\":\"app\", \
                  \"image\":\"<< parameters.image_name >>\", \
                  \"command\":[\"bash\"], \
                  \"args\":[\"-c\",\"rails db:migrate SECRET_KEY_BASE=\$(rails secret)\"], \
                  \"envFrom\":[{ \
                    \"secretRef\":{ \
                      \"name\":\"vue-practice\" \
                    } \
                  }] \
                }] \
              } \
            }'"

jobs:
  test:
    docker:
      - image: ruby:2.5.3

    steps:
      - checkout

      - run:
          name: Initialize
          environment:
            RAILS_ENV: test
            DISABLE_SPRING: 1
          command: |
            # install tools
            curl -sL https://deb.nodesource.com/setup_10.x | bash -
            apt-get install -y nodejs
            npm install yarn@1.13.0
            gem install bundler -v 1.17.3
            # initialize DB
            bundle install
            bundle exec rails db:migrate

      - run:
          name: Test
          command: bundle exec rails test

  docker_build_and_push:
    machine:
      image: ubuntu-1604:201903-01

    steps:
      - checkout

      - run:
          name: Initialize
          command: |
            GIT_HASH=$(git rev-parse HEAD)
            echo "export GIT_HASH=${GIT_HASH}" >> $BASH_ENV
            echo "export IMAGE_NAME=${DOCKER_HUB_USERNAME}/vue_practice_app" >> $BASH_ENV
            source $BASH_ENV

      - run:
          name: docker login
          command: echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USERNAME} --password-stdin

      - docker-orb/build:
          image: $IMAGE_NAME
          tag: $GIT_HASH

      - docker-orb/push:
          image: $IMAGE_NAME
          tag: $GIT_HASH

  deploy:
    machine:
      image: ubuntu-1604:201903-01

    steps:
      - checkout

      - run:
          name: Initialize
          command: |
            GIT_HASH=$(git rev-parse HEAD)
            echo "export IMAGE_NAME=${DOCKER_HUB_USERNAME}/vue_practice_app:${GIT_HASH}" >> $BASH_ENV
            source $BASH_ENV

      - kube-orb/install-kubectl

      - kube-orb/install-kubeconfig:
          kubeconfig: KUBECONFIG_STR

      - migrate_vue_practice:
          image_name: $IMAGE_NAME

      - kube-orb/update-container-image:
          namespace: vue-practice
          resource-name: deployment/vue-practice
          container-image-updates: app=$IMAGE_NAME
          record: true

      - kube-orb/get-rollout-status:
          namespace: vue-practice
          resource-name: deployment/vue-practice

workflows:
  ci:
    jobs:
      - test

  cd:
    jobs:
      - test:
          filters:
            branches:
              only: stable
      - docker_build_and_push:
          requires:
            - test
      - deploy:
          requires:
            - docker_build_and_push

CircleCI Orbs を使う方法には .circleci/config.yml にて以下のようにします。

  • version: 2.1 以上にする
  • orbs の値として使用したい Orb を Hash 形式で指定する
    • Hash の key は job 内で使用する名前となる
    • Hash の value は公開された Orb のパスを指定する
      • @ をつけると Orb のバージョンを指定できる
    • 例) docker-orb: circleci/docker@0.5.19 を指定すると circleci/docker の version 0.5.19 が定義したコマンドが使えるようになり、Job の中で docker-orb/build のようにコマンドが指定できる

また、commands も version: 2.1 から使えるようになった設定です。 Job の中で run 等の代わりに使えるコマンドを定義できます。 今回 commands を使った理由は、kubectl run の override させる JSON パスに $IMAGE_NAME を展開しつつ生成できなかったため使うことにしました。

GitHub Actions と CircleCI の比較

GitHub Actions と CircleCI のそれぞれの Workflow で CD 環境を実現する YAML ファイルを見てみましたが、CI と同様に内容はほぼ同じに記載することが出来ました。

GitHub Actions も CircleCI も外部の Workflow/Job を流用できるため、同様の機能であれば、それらの使い方の違いはあるもののほぼ同じに記載することが出来ました。

作成した CI/CD の改善ポイント

  • npm, gem パッケージインストール後にキャッシュを有効にする
  • Docker イメージのビルドキャッシュを有効にする
  • CI/CD の実行結果をメールの代わりに Slack 通知する
  • CD にてデプロイする前に継続して実行するかどうかを人に尋ねる

キャッシュを有効にすることで Workflow を実行する時間が短縮できるようになることが期待できます。

最後に - GitHub Actions と CircleCI の違いについての所感

今回はせっかくなので version 2.1 から使えるようになった機能である CircleCI Orbs, Commands を使ってみました。

CircleCI の Marketplace と同様の仕組みとして CircleCI Orbs なるものがあると記事を執筆していて知りました。

CircleCI Orbs を知る前は GitHub Actions との大きな差異として、外部の Workflow が利用できる点だと思っていたので、version 2.1 の新機能を知ったことで GitHub Actions とはほぼ差異はなく移行できる印象を持ちました。

敢えて違いとなることを挙げると次のとおりかと感じてます。

GitHub Actions の Marketplace の手軽さ、GitHub Actions では JavaScript を使ってアクションを実装することが出来るため自由度やメンテナンス製が高いように感じます。

一方で CircleCI には CLI があるため、開発をする際にはわざわざ VCS リポジトリに commit/push せずに開発・デバッグ出来るメリットがあると思います。

イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips (2018年版-後編)

こちらは 「イマドキの JavaScript 開発で使える、リモートデバッグとロガーの Tips (2018年版-後編)」からの転載です。

SEROKU の開発を例に、弊社で使っているリモートデバッグとロガーの Tips をご紹介します。 当記事は 2018 年、と過去の記事ですが、現在でも応用可能な Tips になっています。



案件としても OSS 成果物としても、JavaScript を利用するシチュエーションは増え続けています。まだまだ枯れた言語とは言い難い状況で、使われるバージョンも ES5 から ES7 まで進化を続け、新しい文字列リテラルや async/await のような「イマドキの JavaScript の書き方」を紹介する記事は多い中、デバッグはこうあるべきという情報は比較的少ないように思います。

本記事の前編、中編では、システム開発に於けるデバッガ、ロガーの大切さと、他の言語・フレームワークと比べた際の JavaScript 開発環境に於けるビハインドについて説明し、実際に理想的なロガーを利用する為の設定方法を紹介してきました。

本記事では JavaScript 開発時のデバッガー利用のための具体的な設定方法を紹介します。

理想の世界

まずは前編で掲げたゴールの再掲です。

対象システム

以下の3つのシナリオをカバーすることにします。

  • シナリオA
    • node.js (Express)
  • シナリオB
    • node.js (Express) + webpack/babel によるクライアントビルド
  • シナリオC
    • next.js on Express

やりたいこと

  • SourceMap の利用
    • ブラウザの開発者ツールでエラーを追う場合、トランスパイル前のソースの行数が分かる
  • リモートデバッグ

Let's Try! ― シナリオA

シナリオAは、node.js (Express) 環境向けのロガー設定です。
必要なことは以下 2 つです。

npm 経由のプロセスをデバッグ

node.js のプロセスは、 node コマンドに --inspect あるいは --inspect-brk を渡すことでリモートデバッグに必要な inspector の利用が可能になります。node コマンドをそのまま叩くことは少ないので、まずは npm 経由で立ち上げた node プロセスをデバッグできるようにしましょう。

VSCode公式ページによれば、以下のようなコードで、npm から起動したプロセスをデバッグできるようになります。

(..snip..)

"scripts": {
  "debug": "node --nolazy --inspect-brk=9229 app.js"
},

(..snip..)

9229 ポートは inspector 利用時のデフォルトポートなので、単に --inspect でも構いません。

次に VSCode の設定ファイルです。

launch.json

(..snip..)

{
    "name": "Launch via NPM",
    "type": "node",
    "request": "launch",
    "cwd": "${workspaceFolder}",
    "runtimeExecutable": "npm",
    "runtimeArgs": [
        "run-script", "debug"
    ],
    "port": 9229
}

(..snip..)

これだけで、VSCode のデバッガが利用可能になります。

個人的には以下のように若干カスタムしたものを使っています。

launch.json

(..snip..)

{
    "type": "node",
    "request": "launch",
    "name": "Debug: Server",
    "runtimeExecutable": "npm",
    "runtimeArgs": [
      "run", "debug"
    ],
    "port": 9229,
    "restart": true,
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"
},

(..snip..)

node-dev を使おう

さて、debug script では node をそのまま起動していますが、開発中にソースコード変更を検知して Express サーバーが再起動してくれると更に楽です。巷では nodemon が有名ですが、筆者が推すのは断然 node-dev です。コンフィグレスで require されたものだけを watch してくれる上に、デスクトップ通知があって再起動したことがわかりやすいのが特徴です。

debug script を修正し、以下のようにしておきましょう。

(..snip..)

"scripts": {
  "debug": "node-dev --nolazy --inspect app.js"
},

(..snip..)

追加の watch 設定

開発環境で、js だけではなく json 等が変更されたときでも node-dev による再起動が働いて欲しい場合もあると思います。そんなときは、追加で require する機構を用意し、 development 環境でのみ働くようにしておきましょう。

dev.js

const fs = require('fs');
const path = require('path');

const localeDir = 'src/locales';

class Dev {

  init() {
    this.requireForAutoReloadServer();
  }

  /**
   * require files for node-dev auto reloading
   */
  requireForAutoReloadServer() {
    // load all json files for live reloading
    fs.readdirSync(localeDir)
      .filter(filename => {
        return fs.statSync(path.join(localeDir, filename)).isDirectory();
      })
      .map((dirname) => {
        require(path.join(localeDir, dirname, 'translation.json'));
      });
  }

}

module.exports = Dev;

app.js

(..snip..)

if (self.node_env === 'development') {
  const Dev = require('./dev');
  const dev = new Dev();
  dev.init();
}

(..snip..)

上記のサンプルコードは、 src/locales/* 下にある translation.json ファイルが変更された場合に再起動を行います。

Let's Try! ― シナリオB

シナリオBは、node.js (Express) + webpack/babel によるクライアントビルド両方で利用できる設定です。

ここまでに作ったファイルは完全に流用できます。
webpack の設定と、VSCode の設定を追加しましょう。

webpack.js

(..snip..)

devtool: 'cheap-module-eval-source-map',

(..snip..)

devtool にどのような値を設定可能かは webpack の公式ドキュメントを参照してください。ここでは速度を重視し、 cheap-module-eval-source-map を選びました。

次に VSCode の設定ファイルです。

launch.json

(..snip..)
{
    "type": "chrome",
    "request": "launch",
    "name": "Debug: Chrome",
    "sourceMaps": true,
    "webRoot": "${workspaceFolder}",
    "sourceMapPathOverrides": {
      "webpack:///*": "${workspaceFolder}/*"
    },
    "url": "http://localhost:3000",
}
(..snip..)

特に重要なのは webRootsourceMapPathOverrides です。

webRoot は、URL ベースで root がどこを指し示すか、
sourceMapPathOverrides はブラウザの開発者ツールで source map の参照先の webpack:/// から始まるパスが、VSCodeワークスペース上でどこを指し示すべきかのマッピングを行います。ブレークポイントがうまく設定できない場合は、この2つを見直しましょう。

Let's Try! ― シナリオC

シナリオCは、next.js 向けの設定です。ディレクトリ構成がほぼ決まっているので、シナリオBより簡単かもしれませんね。

launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Next: Node",
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run",
        "debug"
      ],
      "port": 9229,
      "restart": true,
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "sourceMapPathOverrides": {
        "webpack:///*": "${workspaceFolder}/*"
      },
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Next: Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}",
      "sourceMapPathOverrides": {
        "webpack:///*": "${webRoot}/*"
      },
    }
  ],
  "compounds": [
    {
      "name": "Next: Both",
      "configurations": ["Next: Node", "Next: Chrome"]
    },
  ],
}

まとめ

後編ではロギングに関して実際のコードを紹介し、デバッガ利用時の「理想の世界」を実現するコードを紹介しました。デバッガや node-dev のような自動で再起動するツールを使うことで、開発時の効率は数倍になります。

関連記事(前編・中編)と合わせて、是非自身とチームの開発をより効率的に、より楽しいものにしていただければと思います。

関連記事

tech.weseek.co.jp

tech.weseek.co.jp

ゲーム開発のために Cocos Creator 触ってみた! 〜その1〜

みなさんこんにちは。突然ですが Cocos Creator 知ってますか!? cocos といってもファミリーレストランではありません(笑)現在ゲームのアプリを作りたいとなった場合、いわゆるゲームエンジンを何を使うかというと、 Unity と Cocos2d-x の二つが人気の上位を占めると言える状態です。

Cocos Creator はその Cocos2d-x をベースとした、スマホ用(Web)ゲーム開発アプリ統合開発ツールです。統合開発ツールってなんのこっちゃ、って思う人は Unity と Cocos2d-x を足して2で割ったものだと思ってください(笑)

何ができるの?

上記の説明でピンとこない人に(当たり前や)、統合開発ツールって実際なんなんだってところなんですが、元々の Cocos2d-x はあくまでコードベースで開発するスタイルなので視覚的にわかりづらい、とっつきにくい部分が少なからずありました。 一方で、 Unity の場合は(こちらは統合開発ツール)、アプリケーションを開くと画面の UI 操作で制作するゲームのシーンを作成できたり、 UI を編集できたり、アニメーションを設定できたり、コーディング(別途 Editor の設定が必要)やビルドができたり、ざっくり言ってしまえばそれをインストールすればそれだけでアプリが作れちゃうんですね。

なので、 Cocos Creator は Cocos2d-x がなんか複雑すぎーって諦めちゃった人たちにはかなり有用なツールになりえるかと思います。 また、アプリケーションの UI は Unity をかなり参考にしているっぽいので、Unity を触っていた人たちはすんなり入れるかと思います。

コードを書く上で、言語ってとっても大事だと思うんですけど、 Cocos Creator では 2018/08/29 現在、JavaScript, TypeScript で書くことができます。

インストールしてみる

本家のサイトからまずダウンロードページに飛びましょう。(デフォが中国語なのでびっくりしないように)

最新バージョンは2系ですが、触ってみた感じちょっとバグ多めなので安定している 1.9 か 1.10 系をお勧めします(^_^;)

DL したセットアップファイルを開けば自動的にインストールが始まるので OK を押していけば基本的に大丈夫です。

トップページにあるダウンロードリンクだと最新バージョンを落とすことになるので、バージョン指定したい場合は上記のダウンロードページに直接飛んでください。

起動してみる

インストールが正常に完了すると、起動できる状態になります。 ?? みたいなアイコンなので早速起動してみましょう。

起動すると、まずはログイン画面が表示されます。Cocos Developer アカウントが必要なので、先に登録しておくか、もしくはログイン画面の Sign up をクリックして登録しましょう!

登録が完了した上で、ログインフォームに情報を入力して Login ボタンを押すと、正常に処理が進めばプロジェクト作成画面に切り替わるので、この状態でプロジェクトが作れるようになります!

プロジェクトを作成してみる

ログインが正常に済むと、Dashboard がプロジェクト作成の画面に切り替わります。

f:id:weseek:20191114182705p:plain

Cocos Creator では、「Recent Projects」タブから既存のプロジェクトを(一度開いたプロジェクトはここにリスティングされます)、「New Project」タブからは、いくつかのサンプルをベースに作るか、空の Project を、 Open Other… タブでは PC 上にある Cocos Creator のプロジェクトを選択形式でそれぞれプロジェクトを開くことができます。

せっかくなので Empty Project で新規に作成したいと思います。

画面の説明

プロジェクトが立ち上がると以下のような画面が開きます。

f:id:weseek:20191114182724p:plain

Unity を知ってる人は、その類似具合に驚くかと思います(笑)

ざっと解説していきます。

Node Tree

  • シーンに配置される Node が階層構造で表示される
  • 並列に Node が置かれた場合は、画面上で 下 に表示されているものが画面では全面に来る
  • Node は階層構造として表現できるので、Node の子、孫・・・のような作り方も可能(グルーピング)

Asset

  • プロジェクトを通して使われるアセットファイル、スクリプトファイルが表示される
  • プロジェクト作成時に作られる assets フォルダの中身がそのまま表示される

Scene

  • 名前の通り、シーン(画面の単位)を編集することができる

Properties

  • Node Tree 上で選択した Node のプロパティを確認(調整)することができる

Console

  • ログを確認することができる

Timeline

  • アニメーションを生成することができる

Node Library

いざ開発!

開発の流れとしては、基本的には、 Scene 上、または Node Tree 上にコンポーネントをペタペタ貼っていき UI を組み上げていきます。

音声や画像、フォント等のアセットを使用したい場合には Assets に適宜追加して、それを同じ要領で Scene または Node Tree 上に配置していく。という感じで画面を作っていきます。

ある程度画面が作れてきたらスクリプトを定義し、ボタンなどに対してイベントを仕掛け、処理を書いていく、というフローになるかと思われます。

また画面上部にある三角ボタンを押すと その状態でのデバッグができるので(デフォルトだとブラウザが開かれる)ちょこちょこ確認しながら作成することができます!

まとめ

今回は、 Cocos Creator という統合開発環境の簡単な説明と使い方を書いていきました。

前身となる Cocos Studio というものが Deprecated になってしまったり、 Cocos 陣営の戦略がなかなか不透明だったり、とにかくサンプルが少なくて「こういうときどうやればいいんだろう?」みたいなことが結構あったりするんですが、それでも手頃にゲームアプリが作れちゃうところはとっても魅力だと思います!

次回からは、なにか目的をもってゲームを作り上げていこうと思いますのでお楽しみに!

Go 言語での開発始めてみる〜開発環境を作る編〜

こちらは 「Go言語での開発始めてみる〜開発環境を作る編〜 」からの転載です。



前回の続き Go言語での開発を試してみる 〜調べる編〜 で調べたものを組み合わせて実際に Go 言語を使って Web システムを作ってみようと思います。今回はフレームワークの起動までです。

開発環境構築

OS は Windows 10 にて行っていきます。

Go 自体のバージョン

この前入れた通り下記で行こうと思います。こちらもそこまで意味は無いです。

> go version
go version go1.10.3 windows/amd64

開発エディタ

特に選定したわけではなく、普段使い慣れている Visual Studio Code を使って開発をしようと思います。 とりあえず拡張機能をインストール

f:id:weseek:20191107165922p:plain

Ctrl + Shift + p でコマンドパレットを開き、 Go: Install/Update tools を実行

f:id:weseek:20191107165939p:plain

ツールをインストールしていきます。

    Installing github.com/nsf/gocode SUCCEEDED
    Installing github.com/uudashr/gopkgs/cmd/gopkgs SUCCEEDED
    Installing github.com/ramya-rao-a/go-outline SUCCEEDED
    Installing github.com/acroca/go-symbols SUCCEEDED
    Installing golang.org/x/tools/cmd/guru SUCCEEDED
    Installing golang.org/x/tools/cmd/gorename SUCCEEDED
    Installing github.com/fatih/gomodifytags SUCCEEDED
    Installing github.com/haya14busa/goplay/cmd/goplay SUCCEEDED
    Installing github.com/josharian/impl SUCCEEDED
    Installing github.com/davidrjenni/reftools/cmd/fillstruct SUCCEEDED
    Installing github.com/rogpeppe/godef SUCCEEDED
    Installing golang.org/x/tools/cmd/godoc SUCCEEDED
    Installing github.com/sqs/goreturns SUCCEEDED
    Installing github.com/golang/lint/golint SUCCEEDED
    Installing github.com/cweill/gotests/... SUCCEEDED
    Installing github.com/derekparker/delve/cmd/dlv SUCCEEDED

    All tools successfully installed. You're ready to Go :).

これらがインストールされました。
すると。。。

f:id:weseek:20191107170012p:plain

わーすごい!色もついたしブレイクポイントを設定して F5 を押すことでデバッグもできるようになりました!

  • 若干詰まった点
    • setting.json を作業ディレクトリ配下に置き下記のように gopath を設定しなければいけないようです

        {
          "go.gopath": "/path/to/go/src/beegotest"
        }
      

こんな感じにフルパスで指定してあげるとインストール直下以外でも動きました。

実際にやってみた

それではここから本格的な(とはいっても大層なものを作るわけではないですが)開発に入って行こうと思います。
せっかくなので前回調べた中のフレームワークを使ってみようと思います。

今回は GitHub上の Star 数が一番多いという理由だけで beego を使ってみようと思います。

パッケージ管理

特にこだわりがないのでパッケージ管理は dep を使おうと思います。vgo (Versioned Go) も公式ではありますがまだ人類には早いという噂を聞いたので無難な選択をしますw
とりあえず go get で取得し、init して ensure で /vendor 配下にパケージをダウンロードするように設定します。

> go get -u github.com/golang/dep/cmd/dep
> dep init
> dep ensure

どのコマンドもうんともすんとも言わず不安ですが、指定したフォルダの配下に下記ができていれば大丈夫のようです。

> dir


    ディレクトリ: go\src\beegotest


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2018/08/20     19:32                vendor
-a----       2018/08/20     19:32            222 Gopkg.lock
-a----       2018/08/20     19:32            655 Gopkg.toml
-a----       2018/08/20     19:20             55 settings.json

beego インストール

dep の準備が整ったら実際に beego をインストールしていきましょう。

> dep ensure -add github.com/astaxie/beego
Fetching sources...

"github.com/astaxie/beego" is not imported by your project, and has been temporarily added to Gopkg.lock and vendor/.
If you run "dep ensure" again before actually importing it, it will disappear from Gopkg.lock and vendor/.

bee ツールもご一緒に

> dep ensure -add github.com/beego/bee

と思ったんですが、どうやら依存関係のせいで dep でインストールができませんでした。。。 しかたがないので go get で取得します。ソースコードとしては直接関係ないのでまぁ問題ないと判断します。

> go get github.com/beego/bee

無事インストールできました

> bee version
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0

├── Beego     : Beego is not installed. Please do consider installing it first: https://github.com/astaxie/beego
├── GoVersion : go1.10.3
├── GOOS      : windows
├── GOARCH    : amd64
├── NumCPU    : 4
├── GOPATH    : C:\Path\to\go\
├── GOROOT    : C:\Go\
├── Compiler  : gc
└── Date      : Monday, 20 Aug 2018

プロジェクト作成

適当に bee ツールにてプロジェクトを作成してみます。

> bee new haruch
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2018/08/20 20:13:05 INFO     0001 Creating application...
        create   C:\Path\to\go\src\beegotest\haruch\
        create   C:\Path\to\go\src\beegotest\haruch\conf\
        create   C:\Path\to\go\src\beegotest\haruch\controllers\
        create   C:\Path\to\go\src\beegotest\haruch\models\
        create   C:\Path\to\go\src\beegotest\haruch\routers\
        create   C:\Path\to\go\src\beegotest\haruch\tests\
        create   C:\Path\to\go\src\beegotest\haruch\static\
        create   C:\Path\to\go\src\beegotest\haruch\static\js\
        create   C:\Path\to\go\src\beegotest\haruch\static\css\
        create   C:\Path\to\go\src\beegotest\haruch\static\img\
        create   C:\Path\to\go\src\beegotest\haruch\views\
        create   C:\Path\to\go\src\beegotest\haruch\conf\app.conf
        create   C:\Path\to\go\src\beegotest\haruch\controllers\default.go
        create   C:\Path\to\go\src\beegotest\haruch\views\index.tpl
        create   C:\Path\to\go\src\beegotest\haruch\routers\router.go
        create   C:\Path\to\go\src\beegotest\haruch\tests\default_test.go
        create   C:\Path\to\go\src\beegotest\haruch\main.go
2018/08/20 20:13:05 SUCCESS  ? 0002 New application successfully created!

いろいろと自動作成されました。

起動

それでは早速起動します!

> cd haruch
> bee run
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2018/08/20 20:16:35 INFO     ? 0001 Using 'haruch' as 'appname'
2018/08/20 20:16:35 INFO     ? 0002 Initializing watcher...
beegotest/vendor/github.com/astaxie/beego/config
beegotest/vendor/gopkg.in/yaml.v2
beegotest/vendor/github.com/astaxie/beego/utils
beegotest/vendor/github.com/astaxie/beego/logs
beegotest/vendor/github.com/astaxie/beego/grace
beegotest/vendor/github.com/astaxie/beego/session
beegotest/vendor/github.com/astaxie/beego/toolbox
beegotest/vendor/golang.org/x/crypto/acme
beegotest/vendor/github.com/astaxie/beego/context
beegotest/vendor/golang.org/x/crypto/acme/autocert
beegotest/vendor/github.com/astaxie/beego/context/param
beegotest/vendor/github.com/astaxie/beego
beegotest/haruch/controllers
beegotest/haruch/routers
beegotest/haruch
2018/08/20 20:16:40 SUCCESS  ? 0003 Built Successfully!
2018/08/20 20:16:40 INFO     ? 0004 Restarting 'haruch.exe'...
2018/08/20 20:16:40 SUCCESS  ? 0005 './haruch.exe' is running...
2018/08/20 20:16:40.951 [I] [asm_amd64.s:2361]  http server Running on http://:8080

起動した!!!

f:id:weseek:20191107170043p:plain

試しにいじってみる

普通の自動生成されたファイルを眺めてみると、普通の MVC フレームワークのようなので試しに View を編集してみましょうか。
/views/index.tpl というファイルを開いてみると想像通り html ファイルが入っていました。 なので下記のように編集してみます(抜粋)

<body>
  <header>
    <h1 class="logo">haruhikonyan 参上</h1>
    <div class="description">
      go 及び beego を試してみてるマン
    </div>
  </header>
  <footer>
    <div class="author">
      Official website:
      <a href="http://{{.Website}}">{{.Website}}</a> /
      Contact me:
      <a class="email" href="mailto:{{.Email}}">{{.Email}}</a>
    </div>
  </footer>
  <div class="backdrop"></div>

  <script src="/static/js/reload.min.js"></script>
</body>

f:id:weseek:20191107170109p:plain

無事編集できたのと、ライブリロードこそ対応してないものの、ソースを保存してブラウザを更新すればサーバの再起動無しにコードは反映されました。本日はこれくらいで。

まとめ

まだまだ Go言語たるものの真髄には全然触れられてはいないとは思いますが、 VSCode でのエディタの整備も特に苦も無く整備でき、デバッグ環境もすぐ整えられました。また、ちょこっと試してみた Go の Web フレームワークである beego も Rails のようなフルスタックフレームワークも体感的には Ruby で開発環境を整えるよりも楽に行えた気がしたのでこれはアリだなと思いました。

次回はこの beego を使ってもっと Web サービスっぽくするのと、他の機能も使ってみようと思います。

関連記事

tech.weseek.co.jp

アプリケーション開発におけるロックの重要性と ORM におけるロックの実現例 〜楽観的ロックの紹介〜

こちらは 「アプリケーション開発におけるロックの重要性と ORM におけるロックの実現例〜楽観的ロックの紹介〜」からの転載です。



前回、「アプリケーション開発におけるロックの重要性と ORM におけるロックの実現例」ではロックについて掘り下げ、トランザクションについてとその特徴を紹介し、その中で楽観的ロックの存在を挙げました。

そこで、今回は楽観的ロック及び悲観的ロックについて紹介したいと思います。

まずは、データの不整合が発生する状態とトランザクションの分離レベルについて詳細を紹介し、続いて楽観的ロックと悲観的ロックについて紹介していきます。

データの不整合

ロックによりデータの不整合が発生することを防ぐことができますが、不整合が発生するケースは様々です。

データの不整合を全て防ごうとすると可用性や性能が著しく低下するため、どの程度まで不整合を許容するかを考え、それに見合ったトランザクションの分離レベルを選択する必要があります。

データの不整合が発生した状態

整合性に問題が発生した状態として、ANSI/ISO 標準 SQL と論文「A critique of ansi sql isolation levels」から取り上げると次の項目が挙げられます。

「※」が付いた不整合状態は、コミット前の値は参照できない前提で考えると問題の本質が分かりやすいと思われます。(それでも防げない状態であるため)

リードスキューとライトスキューを除く各状態について、以下に例を用いて問題が発生するまでのシーケンスを図示します。

ダーティライト

ダーティライトとは「複数のトランザクションが同じエンティティを更新した後、あるトランザクションロールバックした場合に戻すべき値が不明となった状態」です。

f:id:weseek:20191105181121p:plain

図のように、Transaction A が Michael のニックネームを Mick から Mike へ変更したことにより、Transaction B がロールバックする際に Mick に戻すべきか、Mike へ戻すべきか判断できない状態となります。

ロストアップデート(Lost Update)

ロストアップデートとは「とあるトランザクションが書き込んだ値が、他のトランザクションにより上書きされた状態」です。

f:id:weseek:20191105181144p:plain

図のように、Transaction A が Michael のニックネームを Mick から Mike へ変更し、その後に Transaction B が Michael のニックネームを Mick から Mickey へ変更する操作を行うと、Transaction A の更新が失われてしまいます。

ダーティリード

ダーティリードとは「とあるトランザクションが更新した値 A' を他のトランザクションが参照した後に、更新された値がロールバックされると、読み取った値 A' がロールバックされずにトランザクションに利用されることになってしまった状態」です。

f:id:weseek:20191105181210p:plain

図のように、Transaction A がコミットする前に Michael のニックネームを Mick から Mike へ変更した内容をTransaction B が読み取ってしまうと、Transaction A がロールバックしても Transaction B はニックネーム Mike を保持し続けてしまいます。

ファジーリード / 非再現リード / ノンリピータブルリード

ファジーリードとは「とあるトランザクションが読み込んだ値 A が、他のトランザクションにより A' に更新されてコミットされると、値 A が二度と呼び出せなくなってしまった状態」です。

ダーティリードを防ぐために、他のトランザクションがコミットする前のエンティティは参照できないようにしても発生します。

f:id:weseek:20191105181233p:plain

図のように、Transaction A が処理中に読み込んだエンティティは Michael の Age が 12 であった値が、Transaction B によって Age が 13 に変更された上でコミットされると、再度 Transaction A が読み取った値は Age 13 になります。

ファントムリード

ファントムリードとは「とあるトランザクションがテーブルを読み込んだ後に、他のトランザクションによりエンティティが挿入された場合、再度テーブルを読み込むと挿入されたエンティティが参照できてしまう状態」です。

ファジーリードを防ぐために、トランザクション処理中に読み取ったエンティティの値は常に同じとなるようにしても発生します。

f:id:weseek:20191105181257p:plain

図のように、Transaction B は Prize winners テーブルから読み取ったレコード数は 2 つであるため、David を当選者へ追加しようとしますが、Transaction A により Charlie が当選者に追加された上でコミットされると、Transaction B では新しい当選者 Charlie が追加されたテーブルが読み込まれるようになります。

トランザクションの分離レベル

データの不整合が発生するケースを紹介しましたが、極端な話ではトランザクションを並列で実行せずに直列(シリアル) に実行することで全てのケースを防ぐことが出来ます。

しかしそれでは非効率なので、次に示すトランザクションの分離レベルが ANSI/ISO 標準 SQL によって定義されています。(前回記事で紹介した通り、これらの分離レベルは MySQL 等多くの DBMS で実装されています)

  • READ UNCOMMITTED
    • コミットされていない値も読み取ることが出来るよう分離する
  • READ COMMITTED
    • 読み取った値は必ずコミットされた値となるよう分離する
  • REPEATABLE READ
    • とあるトランザクション処理の間、同じエンティティであればいつ読み取っても同じ値となるよう分離する
  • SERIALIZABLE
    • 複数のトランザクション処理結果が、シリアルに実行された場合と同じ結果になるよう分離する

それぞれの分離レベルにおいてデータ不整合発生可否をまとめると次のようになります。

分離レベル Dirty Write Dirty Read Non-Repeatable Read Phantom Read
READ UNCOMMITTED × × ×
READ COMMITTED × ×
REPEATABLE READ ×
SERIALIZABLE

<凡例>×…発生する、〇…発生しない

一方で、論文「A critique of ansi sql isolation levels」では上記の ANSI/ISO SQL 標準における分離レベルを再定義・拡張させた分離レベルが定義されています。
(参考: A critique of ansi sql isolation levels 解説公開用)

この定義の中では ANSI/ISO 標準 SQLトランザクション分離レベルを明確に定義し、それらの分離レベルでは防ぐことのできないデータ不整合が発生する状態を追加し、それらに含むトランザクション分離レベル毎の発生可否を整理しています。

新たに追加されたトランザクション分離レベルは次の 2 つであり、Snapshot Isolation が Serialize に近い分離レベルを保つことができ、並列で実行することのできるレベルであると述べています。(Snapshot Isolation は InterBase, Firebird, Oracle, PostgreSQL, SQL Anywhere, MongoDB, Microsoft SQL Server (2005 and later) で実装されています。但し、Oracle では Snapshot Isolation を Serializable と呼ぶなど、DBMS によって分離レベル名が異なっていたりするようです。※参考)

  • Cursor Stability
    • SQL カーソルにおけるロック動作を踏まえた拡張により、READ COMMITTED では解決できない問題を防げるよう分離する
  • Snapshot Isolation
    • とある時点において取得したスナップショットに対してトランザクション操作を行うことで、ファントムリードを防げるよう分離する

分離レベルの再定義は割愛して、トランザクションの分離レベルとデータの不整合発生可否をまとめると次のようになります。

分離レベル Dirty Write Dirty Read Lost Update Non-Repeatable Read Phantom Read Read Skew Write Skew
READ UNCOMMITTED × × × × × ×
READ COMMITTED × × × × ×
Cursor Stability × ×
REPEATABLE READ ×
Snapshot Isolation ×
SERIALIZABLE

<凡例>×…発生する、〇…発生しない、△…一部発生する

分離レベルの選択

トランザクションの分離レベルの種類と、それに応じて防ぐことのできるデータ整合性について紹介しましたが、結論としてはどのような目的・環境においても最適となる分離レベルは存在せず、可用性と性能のトレードオフで選択することになります。

参考までに、いくつかの DBMS におけるデフォルトの分離レベルを紹介します。

DBMS デフォルトの分離レベル 参考情報
MySQL 8.0(5.6も同じ)
(InnoDB)
REPEATABLE READ 8.0, 5.6
PostgreSQL 10(9.6も同じ) READ COMMITTED 9.6, 10
Oracle Database 18c(12cも同じ) READ COMMITTED 18c, 12c
Microsoft SQL Server 2017(2016も同じ) READ COMMITTED 2017, 2016
MongoDB 4.0 READ UNCOMMITTED 4.0
※v4.0からマルチドキュメントのトランザクションが対応された

ロックの有効期間と楽観的アプローチ

トランザクションの分離レベル以外の観点として、ロックの有効期間の違いによってもデータの不整合状態を防げる可能性の違いと性能の違いが生まれます。

例えばトランザクションが開始されてから終了するまでの間ずっとロックを行うことで不整合を防げる可能性は高まりますが、ロックが解放されるまでの待ち時間が増えることになります。

一方で、読込・更新操作の間だけロックをかけることで待ち時間は少なくなりますが、今度は不整合が発生する可能性が高まります。

そこで、不整合が発生するような更新が同時に行われる頻度によるアプローチの違いを紹介します。

悲観的ロック(Pessimistic Locking)

概要

悲観的ロックは更新が同時に行われる頻度が高いことを想定しており、読込・更新処理が開始された時点で他の処理を排除するロック方式です。

アプリケーションレベルでも悲観的ロックを行うことは可能ですが、一般的に DB レベルで行なわれます。

書き込み操作が主に行われる用途に対して適したロック方式です。

デメリット

悲観的ロックはロックが解除されるまでトランザクションの待ち時間が長く発生する可能性があること、明示的な開放が必要となることから、読み取り操作が主であり、ステートレスな通信である HTTP 等との相性が悪い(ロックかけっぱなしが発生しうる)とされています。

楽観的ロック(Optimistic Locking)

概要

楽観的ロックはレコードに対する書き込みを禁止するためのロック方式の 1 つです。

更新が同時に行われる頻度は低いだろうという楽観的な考えに基づくロック方式です。 ロックと言いつつも、データに対してのロックは行わずに競合の検証のみを行います。

悲観的ロックの対となる方式です。

ロックの仕組み

楽観的ロックは ActiveRecord や GORM 等、O/R マッパーによりアプリケーションレベルで実装されています。

ロックをかける実装の単位としては、エンティティ単位(RDB におけるテーブルの 1 レコード単位) であることが一般的のようです。 (単位は O/R マッパーの実装に依存するとは思いますが、そもそも同時更新が行われる頻度が低い前提なので、カラム単位で設定するメリットが少ないのだと思います)

ロックの仕組みは次のとおり単純なものです。

  1. エンティティを更新する前にエンティティ毎に設定したバージョンを読み取る
  2. エンティティの更新処理が完了したらバージョンが読み取った時から変わっていないか検証する
  3. 【バージョン変化なしの場合】
    競合がなかったと判断して、バージョンをカウントアップしてエンティティの更新処理を行う
  4. 【バージョン変化ありの場合】
    競合が発生したと判断して、トランザクション処理を失敗させる

ここで、上記説明の中で事前の定義なく「バージョン(を示すカラム)」と記載しましたが、楽観ロックを使うためにはテーブルのレコードにバージョンを示すカラムを用意する必要があります。

このカラムは DB のテーブル作成/マイグレーション時に必要であり、O/R マッパーの使い方によって具体的な方法は変わりますが、例えば Ruby on RailsActiveRecord ではマイグレーションファイルでモデルに lock_version カラムを追加するマイグレーションファイルを作成してマイグレーションを実行することになります。

ロック動作のシーケンス

下記にロック未使用時と楽観的ロック使用時のシーケンス図を示します。

ロック未使用時

f:id:weseek:20191105181328p:plain
ロストアップデート状態となる

楽観的ロック使用時

f:id:weseek:20191105181350p:plain
更新処理が競合したことを検出してトランザクション B が失敗する

図のように、楽観的ロック使用時には更新処理が競合したことが検出され、トランザクション B は失敗して Optimistic Lock Exception(OLE) が発生します。

このように、楽観的ロックはエンティティに対するロックは行わず、更新処理の開始時点とコミット時点のバージョンを比較することで更新処理が競合したことを検証します。

防ぐことのできるデータ不整合

楽観的ロックではダーティリードの発生を防ぐことが出来ますが、ファジーリード、ファントムリードは防ぐことができません。

メリット

悲観的ロックに比べてロック待ちが発生しない分、トランザクション完了までの時間は短くなります。 また、ロックによるブロックと解放(Two phase locking) を行う必要がありません。

従って、高速な応答が求められ、ステートレスである HTTP と相性が良いとされています。

デメリット

潜在的に、ロック待ちが発生しない分、不整合が発生する可能性が高まるデメリットが存在します。

但し、前提として更新が同時に行われる頻度が少ない環境を想定していることから、このデメリットは無視できます。

また、楽観的ロックは DB レベルで提供される機能ではないため、アプリケーション側でバージョンフィールドを用意して、バージョン比較による検証や、バージョン書き込み、OLE の発生などを実装する必要があります。

但し、先に述べたように O/R マッパーにより楽観的ロック機能が提供されるため、O/R マッパーを使う限りはこのデメリットも無視できます。

まとめ

データの不整合が発生した状態は多く存在し、トランザクションの分離レベルを可用性と効率とのトレードオフで選択する必要があることを紹介しました。

また、楽観的な考えに基づくアプローチである楽観的ロックについて紹介し、高速な読み取り処理が必要とされるステートレスな処理である HTTP において有効であると言えることを紹介しました。

次回は楽観的ロックを実装したフレームワークや O/R マッパーについて具体例を紹介していきたいと思います。

関連記事

tech.weseek.co.jp