CS-Cart – HTTP 500 エラー対策

目次

1. 本記事について
2. 環境情報
3. 問題発生時のリソース状況
4. 原因
5. 対処内容
6. 対処後のリソース状況

1. 本記事について


CS-Cart のレスポンス(サイト応答)が極端に劣化し、HTTP 500 エラーが発生するケースがあったため、確認すべき箇所と、その対処方法を記事にします。

本記事は CS-Cart 導入済みの方向けのため、
CS-Cart についての説明は割愛致します。
詳しくは公式サイトをご確認ください。

公式サイト
開発者ドキュメント

2. 環境情報


– OS/ソフトウェア
EC2(Amazon Linux 2, SSD, 64 ビット, t3.large)
RDS(MySQL 8.0.17, マルチAZ, db.r3.large)

CS-Cart マーケットプレイス版(4.9.2_JP_1)
PHP(7.2.30)
PHP-fpm(7.2.30)
Apache(2.4.39)

– AWS環境(ロードバランサ + EC2 + RDS のシンプル構成です)

3. 問題発生時のリソース状況


以下は CloudWatch ダッシュボードで表示したメトリクスです。
※ 表示されている値は 5 分間隔の平均値です。

問題が発生した 19:00 前後の状況を確認すると、

①EC2
 1. CPUUtilization(CPU 使用率)は特に問題なし
 2. CpuWait(CPU 待ち時間)が上がっている

②ApplicationELB(ロードバランサ)
 1. TargetResponseTime(ユーザへリクエストが返却されるまでの時間)が上がっている
 2. NewConnectionCount(新規接続数)は遅延が発生した時間帯以外と比較する限り問題なし

③RDS
 1. DatabaseConnections(DBコネクション数)が上がっている
 2. CPUUtilization(CPU 使用率)は特に問題なし
 3. ReadLatency(SelectSQLの待ち時間)が上がっている

上記のため、

・WEB サーバが忙しい訳ではない。
・DB コネクション数が上がっている時間帯に処理待ちが多く発生し、レスポンスタイムが遅くなっている。

という状況でした。


4. 原因


RDS の Performance Insights を確認したところ、問題が発生した時間帯で「Table_locks_waited(テーブルロックによる処理待ち)」が多数発生していました。

「Table_locks_waited MySQL」で調べると、テーブルのストレージエンジンが「MyISAM(マイアイサム)」の場合は テーブルロック方式という記述が、、

CS-Cart テーブルのストレージエンジンを確認したら「lock_keys」テーブルを除き、全て「MyISAM(マイアイサム)」でした。

※ 詳しい内容は後述致します。

テーブルのストレージエンジンを確認するためのSQLは以下です。

select TABLE_NAME, ENGINE from information_schema.TABLES where TABLE_SCHEMA like '%cscart%';

テーブルロックの原因となっているストレージエンジンの変更を検討したいですが、CS-Cart が MyISAM 以外に対応していない可能性があるため、「CS-Cart MyISAM」で調べたところ、
CS-Cart は InnoDB(イノディービー) にも対応しているので問題ない。という情報と、
CS-Cart 開発者が高速化の方法について説明している記事の中で InnoDB への変換を推奨している情報が見つかりました。

MySQLストレージエンジン
How to speed up CS-Cart. Server details

これで裏付けが取れましたので、以降の対処を行いました。


5. 対処内容


それぞれ以下の作業を行いました。

①RDS
 CS-Cart テーブルのストレージエンジンを「MyISAM(マイアイサム)」から「InnoDB(イノディービー)」へ変更

②EC2
 OPcache と APCu を導入
 ※ こちらはテーブルロックへの対処ではないですが、高速化の観点で CS-Cart 開発者から推奨されているため、実施しました。

①RDS

CS-Cart テーブルのストレージエンジンを「MyISAM(マイアイサム)」から「InnoDB(イノディービー)」へ変更

MyISAM はテーブルロックの方式なので、参照クエリや更新クエリが多くなるにつれ、処理待ちが発生します。

共有ロック
(READロック)
他のプロセスは読み込みはできるが、書き込みができなくなる
排他ロック
(WRITEロック)
他のプロセスは読み込みも書き込みもできない

本件の問題が発生した際はテーブルロックが多く発生しておりましたので、
処理待ちが発生した結果、ユーザへ HTTP 500 エラーが返却されてしまいました。

この問題を解消するため、テーブルエンジンを InnoDB へ変更します。

InnoDB を採用した理由は行ロック方式であることと、
CS-Cart は InnoDB でも適切に動作する旨、公式サイトに明記されているためです。
MySQL のテーブルエンジンは MyISAM や InnoDB の他にもありますが、特殊なユースケースで使用するタイプのため検討しておりません。
気になる方は公式サイトの「第16章代替ストレージエンジン」リンクを辿ってください。

公式サイト
第16章代替ストレージエンジン


MyISAM から InnoDB への変更は、

ALTER TABLE cscart_addon_descriptions ENGINE=InnoDB;

このように「ALTER TABLE」SQL で行います。
ひとつひとつ SQL を書くのが大変なので、以下のように一括で作成します。

select
concat('ALTER TABLE ', TABLE_NAME, ' ENGINE=InnoDB;') as 'sql'
from
information_schema.tables
where
table_schema like '%cscart%'
and
engine = 'MyISAM';

CS-Cart の全テーブルを MyISAM から InnoDB へ変更したら RDS への対処は完了です。

②EC2

OPcache と APCu を導入

PHP はインタプリタ言語のため、呼び出される度にプログラムの解析/解釈が行われ、オーバーヘッドがあります。
今回はクライアントへのレスポンス速度を改善する必要があるため、PHP の速度改善に寄与する「OPcache」と「APCu」をインストールします。

— 解説
OPcache を導入すると、コンピュータが解析済みの「バイトコード」をキャッシュして再利用してくれるため、処理が高速になります。
APCu は PHP が動的に作成したデータをキャッシュしてくれるので、ページの表示速度が改善されます。

本環境の PHP は「remi」リポジトリでインストールされているため、
インストールコマンドは以下になります。

$ sudo yum install --enablerepo=remi,remi-php72 php-opcache php-pecl-apcu

※ OPcache が php-opcache です。
※ APCu が php-pecl-apcu です。

インストールが終わりましたら、Apache を再起動しましょう。

$ sudo systemctl restart httpd

以下の表示となれば完了です。

OPcache(with Zend OPcache … の記述が追加されます)

$ php -v

PHP 7.2.30 (cli) (built: May 5 2020 18:04:45) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.30, Copyright (c) 1999-2018, by Zend Technologies

APCu(apcu が表示されます)

$ php -i | grep apc
/etc/php.d/40-apcu.ini,
apcu
MMAP File Mask => /tmp/apc.XXXXXX
apc.coredump_unmap => Off => Off
apc.enable_cli => Off => Off
apc.enabled => On => On
apc.entries_hint => 4096 => 4096
apc.gc_ttl => 3600 => 3600
apc.mmap_file_mask => /tmp/apc.XXXXXX => /tmp/apc.XXXXXX
apc.preload_path => no value => no value
apc.serializer => php => php
apc.shm_segments => 1 => 1
apc.shm_size => 32M => 32M
apc.slam_defense => On => On
apc.smart => 0 => 0
apc.ttl => 0 => 0
apc.use_request_time => On => On
apc.writable => /tmp => /tmp

本来、性能問題を解決するにあたっては、1つ1つ対応を試し、原因箇所の特定をし、対策をし、効果を確認するのが鉄則ですが、本記事では「それぞれどの程度効果があったのか」についての測定結果がありません。
急を要したため、テーブルのストレージエンジン変更とPHPのキャッシュ化を同時に行い解決を優先しました。


6. 対処後のリソース状況


※ 表示されている値は 5 分間隔の平均値です。

対応前後の状況を比較すると、

– ApplicationELB(ロードバランサ)
 1. TargetResponseTime(ユーザへリクエストが返却されるまでの時間)
   6.87 → 0.8
 2. NewConnectionCount(新規接続数)
   6.87 → 395

レスポンスタイムが改善されていることが確認できます。



本記事は以上です。
問題が発生した際に CS-Cart についての情報をインターネットで検索したのですが、なかなか欲しい情報が見つからず苦労したため、掲載致しました。
解決への一助となりましたら幸いです。

Dockerを使ったMagentoの開発環境構築方法

1. 概要

今回はECサイト構築プロダクトの代表例であるMagentoプラットフォームの開発環境をローカルでDockerを使って構築する方法を紹介します。Magentoとは利用者にユーザ管理、カートシステム、及び決済方法をはじめ、高度な機能と柔軟な変更性を提供したECサイトのプラットフォームです。本記事では、Magentoをダウンロードし、インストールする方法をまとめています。環境構築をポータブルで、扱いやすくするためにdocker-composeを使っています。

本記事内容

  • Magentoとは
  • 本記事を読む前提知識
  • 構築環境準備
  • 環境のデザイン・設計
  • 実装
  • 構築手順
  • 考察・まとめ

2. 本記事を読む前提知識

本記事は以下のような読者の方に向けて書きました。

  • ECサイトの構築、開発等に興味があり、新しいプラットフォームを探している人
  • 新しいフレームワーク、プラットフォーム勉強が好きな人
  • Magentoの開発、自作モジュールを勉強したい人

事前知識

以下の情報・知識について理解があることを前提としています

  • linuxの基本的な知識(ターミナル上コマンドで基本的な操作できる)
  • docker、 docker-composeコンテナー技術
    • dockerは、仮想化・インフラ分野で注目されている技術
    • docker-composeは複数のdockerコンテナーを一斉に管理できるようなツールです。複数のdockerコンテナーから構成される複雑システムをdockerのコマンドだけで操作・管理するのは膨大な作業になってしまう可能性があります。そこで、docker-composeがよく利用されている。docker-composeを使ったら1つのファイルに必要なコンテナーを記述して、docker-compose upという1つのコマンドを実行させるだけで環境が構築されます。
  • nginx、php、composer
    • nginxはサーバ上でウェブを立てるサービス
    • composerはphp依存関係ライブラリを管理するシステム
  • redis
    • インメモリーデータベースシステムで、アクセス性能がいいのでキャッシュ用に使用されていることが多いです。
  • elasticsearch
    • 大量のデータ中で早く検索できるような技術
  • microservice (マイクロサービス)
    • dockerといったコンテナー技術の流行により、大きなシステムを構築する際に、複数の小さいサービスの組み合わせで構築する考え方が注目されています。これをマイクロサービスとよんでいます。マイクロサービスの利点として、開発・変更にはシステム全体ではなくて一部のサービスだけが対象となり、アジャイル的な開発に向いています。

3. Magentoとは

MagentoはPHPで開発され、Symfony、Laminas (バージョン2.3.4まではZend)等を利用した大規模ECサイト向けのプラットフォームです。本プラットフォームはオープンソースプロジェクトで、2018年にAdobeに買収されました。ライセンスとして無料版(Community version)と有料版(Enterprise version)があり、有料版の方にセキュリティー、性能性、利用性の観点からモジュールが追加されています。BuiltWith®サイトの調査によるとMagentoは世界中で利用されているプラットフォームのうち3番目になっています。

Magentoの主なメリットとデメリットは以下の通りです。
【利点】
・大規模性
・ECサイトに必須な機能の全てが揃っています
・要件によって柔軟に変更できます
・オープンソースなので、Communityバージョンを無料でも利用可能
【弱点】
・小規模なデータの扱いには向いていません
・非常に複雑なプラットフォームなので習得に時間がかかります

4. 構築環境準備

  • dockerとdocker-composeのインストール
    • このサイトのガイドに沿ってdockerとdocker-composeのインストール
  • Magentoマーケットプレイスでアカウント作成
  • 作成したアカウントのアクセスキーを作成
    • ログインした後に、このサイトにアクセスし、アクセスキーを作成

5. 環境のデザイン・設計

MagentoのECサイトを構築する際、以下のサービスを立てる必要があります:

  • webサーバ構築に必要なサービス
    • nginxサービス
    • phpサービス
    • データベースサービス
  • Magentoサイトに利用するサービス
    • redis:キャッシュサービス、セッション用とキャッシュ用で2つ必要
    • elasticsearch:検索エンジン

したがって、今回はnginx、php、データベース、redis-session、redis-cache、elasticsearchといった6つのサービスをdockerで立てます。以下は環境のデザイン図になります。

以下にはブラウザからデータがどうやって流れて処理されていく手順を説明します:

  • ユーザはブラウザからアクセスするときに、リクエストはnginxに80ポートで送信されます。
  • nginxは、phpでの処理が必要なら(動的なコンテンツ)、phpに9000ポートで依頼を送信します。
  • Magentoのほとんどの処理がphpコンテナー上で行われるが、処理中にredis-session、redis-cache、elasticsearch、データベースに接続します。
  • 処理されたレスポンスがユーザのブラウザへ返却されます。

magentoのソースコードとデータベースのファイルをコンテナーと開発パソコンの間に共有した理由を以下に説明します:

  • 開発中に編集したファイルをすぐにMagentoのdocker環境に反映させ、テストするのは開発の流れである。したがって、magentoソースコードをnginx、phpコンテナーと共有しました。
  • データベースのボリュームを開発パソコンと共有することによって、この開発環境にデータベースも含めて一緒に他の開発者のパソコンに写したい場合、ファイルとしてコピーするだけで済みます。

6. 環境構築のdocker-composeファイル

本記事で使ったMagento開発環境構築のソースコードはこのgithubからアクセスできる。v0.1タグはちょうど本記事で利用したソースコードとなっています。上に書いてあるように、複数のdockerコンテナーを管理するためにdocker-composeを利用しました。以下に、docker-compose.ymlファイルを表示しています。

version: '3'
services:
  # magentoをダウンロードするため
  composer-install:
    build: ./composer
    volumes:
      - ./html:/html:delegated
      - ./config/composer/auth.json:/root/.composer/auth.json
  # ウェブサーバ
  nginx:
    image: nginx:1.12
    ports: 
      - 80:80
    # magentoコードとnginx設定ファイルの共有
    volumes:
      - ./html:/var/www/html:delegated
      - ./config/nginx/magento.conf:/etc/nginx/conf.d/default.conf:cached
    links:
      - php
    depends_on:
      - php
  # phpサーバ
  php:
    build: ./php
    volumes:
      - ./html:/var/www/html:delegated
    links:
      - db
      - redis-cache
      - redis-sessions
      - elasticsearch
    depends_on:
      - db
  db:
    image: mysql:5.7.31
    volumes:
      - ./db/data:/var/lib/mysql:delegated
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: magento
  elasticsearch:
    image: elasticsearch:7.9.0
    environment:
      - discovery.type=single-node
  redis-cache:
    image: redis:alpine
  redis-sessions:
    image: redis:alpine

nginxサービス

ベースはnginx:1.12イメージで、開発パソコンから80ポートで接続できるようにしています。magentoソースコード(html)をコンテナー上では/var/www/htmlにマウントしています。さらに、このコンテナーはphpコンテナーに接続します(links: -php)。

phpサービス

ベースはphp/Dockerfileになっています。自作のDockerfileでコンテナーを作成している理由は、magentoを動かすために複数のphpパッケージが必要となります。したがって、Dockerfileでphp:7.2-fpmコンテナー上にmagento動作に必要なパッケージをインストールしています。このコンテナーにもmagentoソースコードをマウントしています。さらに、phpコンテナーはdb、redis-cache、redis-sessions、elasticsearchコンテナーに接続します。

elasticsearch、redis-cache、redis-sessionsサービス

基本的にはデフォルトの設定で使うので、定義がベースとなるイメージを設定するだけで良いです。

7. 構築手順

本記事の作業は以下のようなラップトップを開発パソコンとして利用しました。

パソコンモデルMacBook Pro
プロセッサー2.3 GHz Dual-Core Intel Core i5
メモリー16 GB
オペレーティングシステムMacOS Catalina Version 10.15.7
Dockerアプリケーションdocker desktop v2.3.0.5

Magentoソースコードのダウンロード

まずは、このgithubから本記事で利用したソースコードをクローン・ダウンロードします。以下にはソースコードを置いた場所を「ルートフォルダ」とします。

以下には色々なdocker、docker-compose、シェルスクリプトのコマンドをターミナル上で動かす必要があるが、それらをターミナル上でcdコマンドでgithubからのソースコードをおいた「ルートフォルダ」の中で実行しています。したがって、読者はcdコマンドを使って予め、ターミナル上で現在フォルダを「ルートフォルダ」に変更しておけば良いです。

$ cd ルートフォルダ ← 現在フォルダをルートフォルダに変更します

1.magentoのソースコードをcomposerを使って「ルートフォルダ/html」の下にダウンロードします。composerを実行させるためのコンテナーをdocker-composeの中に準備してあります。composerの実行には認証が必要なので、「ルートフォルダ/config/composer/auth.json」ファイルの中身に4.で作成したアクセスキーをコピーする必要があります。

{
    "http-basic": {
        "repo.magento.com": {
            "username": "ここにパブリックキーをコピー",
            "password": "ここにプライベートキーをコピー"
        }
    }
}

2.以下のコマンドをターミナル上で「ルートフォルダ」の下に実行させてmagento 2.3.5バージョンをcomposerを使ってダウンロードできます。このスクリプトの中にはcomposer-installというdockerコンテナーをたてその上にcomposerコマンドでmagentoソースコードをダウンロードするコードが入っています。
※magentoは大きいプラットフォームなので、コマンド実行には数分ぐらいかかります

$ sh magento_download.sh

Magentoのインストール

ダウンロードした後、ターミナル上で「ルートフォルダ」の下にdocker-compose upコマンドで環境を立てます。

$ docker-compose up

その後、magentoをインストールする必要があります。そのためには、phpコンテナー上でmagentoインストールコマンドを実行させる必要があるが、そのためにmagento_install.shスクリプトを準備しています。以下のようにコマンドをターミナル上で「ルートフォルダ」の下に実行させれば、magentoのインストールができます。

$ sh magento_install.sh

シェルの中身は以下のようになっています。そこで、アドミン画面にログインする情報を自分で設定場合は、ADMIN_EMAILとADMIN_PASSWORDの値を変更すれば良いです。

#!/bin/sh

# 各サービスの名前の指定
# もし,.envファイルの中身を変更したら
# こちらも変更する必要がある
PHP=magento_php_1
# DATABASE=magento_db_1

# admin画面にログインするための情報
ADMIN_USER_NAME='admin'
ADMIN_PASSWORD='Password1234'
ADMIN_EMAIL='huchka@sbworks.jp'

docker exec -it ${PHP} php -d memory_limit=-1 /var/www/html/bin/magento setup:install \
  --db-host db --db-name magento --db-user root --db-password root --timezone 'Asia/Tokyo' --currency JPY --use-rewrites 1 --cleanup-database \
  --backend-frontname admin --admin-firstname AdminFirstName --admin-lastname AdminLastName --admin-email ${ADMIN_EMAIL} \
  --admin-user ${ADMIN_USER_NAME} --admin-password ${ADMIN_PASSWORD} --base-url 'http://127.0.0.1/' --language en_US \
  --session-save=redis --session-save-redis-host=redis-sessions --session-save-redis-port=6379 --session-save-redis-db=0 --session-save-redis-password='' \
  --cache-backend=redis --cache-backend-redis-server=redis-cache --cache-backend-redis-port=6379 --cache-backend-redis-db=0 \
  --page-cache=redis --page-cache-redis-server=redis-cache --page-cache-redis-port=6379 --page-cache-redis-db=1 \

# elasticsearchの設定
docker exec -it ${PHP} php /var/www/html/bin/magento config:set catalog/search/engine 'elasticsearch7'
docker exec -it ${PHP} php /var/www/html/bin/magento config:set catalog/search/elasticsearch7_server_hostname 'elasticsearch'

テスト、動作確認

以上で、magentoのダウンロードとインストールが完了したので、ブラウザで127.0.0.1にアクセスしたら以下のような画面が表示されるはずです。

さらに、admin画面に入りたい場合127.0.0.1/adminにアクセスし、インストール時に設定したユーザ名(ADMIN_USER_NAME)とパスワード(ADMIN_PASSWORD)でログインできます。

Magentoはindexer管理、キャッシュ管理、デプロイモード管理等の設定を楽にするためにcliコマンドを標準で提供しています。しかし、標準コマンドをphpコンテナーに入って(docker exec -itコマンドで)実行させる必要があり、一々コンテナーに入るのは無駄な作業になります。したがって、magento_command.shというシェルを本記事の作業で用意しました。例えば、キャッシュの状況を表示するmagentoのコマンドは”php bin/magento cache:status”です。これを実行させるためには以下のコマンドを打つだけで済みます。以下の結果から、全ての種類のデータをキャッシュするように設定されていることが分かります。

$ sh magento_command.sh cache:status
Current status:
                        config: 1
                        layout: 1
                    block_html: 1
                   collections: 1
                    reflection: 1
                        db_ddl: 1
               compiled_config: 1
                           eav: 1
         customer_notification: 1
            config_integration: 1
        config_integration_api: 1
                google_product: 1
                     full_page: 1
             config_webservice: 1
                     translate: 1
                        vertex: 1

8. 考察・まとめ

Magentoはサイズ的に膨大なphpスクリプトから構成されている大きなプラットフォームです。したがって、開発中や本番ライブでの性能が問題となってくることが多いです。性能を上げるためには、redis、varnish、elasticsearch等のキャッシュサービスと検索エンジンを利用することを強くお勧めします。さらに、CPU周波数が高くて、SSDを持っているサーバ、パソコンを利用した方が良いです。例えば、AWS上で本番のサーバを構築しているならC5系のインスタンスが適しています。magentoの処理は多数のphpファイルを渡って実行されるように設計されており、例えば一つのレスポンスに対し、100以上のphpスクリプトが実行されているということもあるからです。

ラップトップ上のdockerでMagentoの開発環境を何も考えずに構築すると、性能がとても低くなり、開発・作業できないぐらいなものになるケースもあります。そのときに最初に確認するべきは、dockerのリソースの設定になります。本記事で使った設定は以下の写真のように、CPUとMemoryはラップトップの半分ずつにしています。
次に、dockerコンテナーへのマウント(共有)する方法について確認します。つまり、htmlフォルダをどんなモードでマウントしているかということです。基本的には、volumeのマウントをdelegatedにした方が良いです。この設定をしたら、dockerコンテナーの方がファイル書き込みがホストのファイルに反映されるのに遅延を入れて性能をあげています。

本記事ではMagentoプラットフォームの開発環境を構築する方法をまとめました。環境構築にはマイクロサービスとして流行っているdocker (docker-compose)を利用しました。

tensorflowを使って気温を予測してみた

こんにちは。最近気象データを用いて、発電所の発電量を予測できるのかということを調査していました。調査中に得た知見(必要なデータの取得や環境構築、予測手順)についてまとめました。

目次

  • 概要
  • はじめに
  • キーワード
  • 構成内容
  • 今回使用するデータ
  • データ取得先
  • そもそもgrib2ファイルとは
  • 動作環境(grib2ファイルから気温を取得するための環境)
  • grib2ファイルから気温を取得する
  • 動作環境(tensorflowを実行するための環境)
  • tensorflowを用いてlstmネットワークを構築し、予測する
  • 考察、まとめ

概要

気象庁が提供している気象データを用いて、将来の気温をtensorflowを用いて予測しました。使用したデータと予測結果は下記の通りになりました。
・使用したデータについて
 1時間毎の気温データを、教師データ用に1年間、評価データ用に1ヶ月
・予測結果
 教師データから予測モデルを作成し、未来1ヶ月を予測した結果、下記のような高い精度で予測できました。
  全てのデータは3%以内の誤差(※1絶対温度換算)
  90%のデータは3%以内の誤差、9%のデータは、10%以内の誤差、1%のデータは、20%以内の誤差(※1摂氏換算)

 結果をグラフ化したものが下記になります。
※1 絶対温度とは、原子・分子の熱運動がほとんどなくなる温度を0K(単位:ケルビン)とする温度のことで、絶対温度T(K)と摂氏温度t(℃ )は、T=t+273(K)という関係が成り立つ。

未来1ヶ月の実データと予測データ

 このブログでは予測するまでに必要なデータの取得や環境構築、予測手順についてまとめました。

はじめに

1.tensorflowとは
 tensorflowとはGoogleが開発している、機械学習に用いられるソフトウェアライブラリです。詳しい使い方やチュートリアルはこちらを参考にしてください。

2.予測方法について
 tensorflow内で、long short term memory(lstm)というリカレントニューラルネットワーク(RNN)のモデルを使って予測を行なっています。RNNとは、脳内にある神経細胞のつながりを数式的なモデルで表現したものです。lstmはRNNのモデルの中の一つで、長期間の情報を学習することが可能になっています。
予測を行う上で、モデルの詳細を理解しなくても実装することは可能ですが、概要程度は理解している方が、記事の後半に書いてあるソースコードを理解しやすくなるので、時間のある人は下記記事を参考にしてください。
ニューラルネットワークの基礎知識 – 1
13. ニューラルネットワークの基礎
リカレントニューラルネットワーク_RNN (Vol.17)
わかるLSTM ~ 最近の動向と共に
RNNとLSTMを理解する

キーワード

  • python
  • grib2
  • tensorflow
  • long short time memory(lstm)
  • docker

構成内容

本記事では、下記環境を前提に検証を行っています。
OS:macOS Sierra 10.12.6
Docker Desktop:19.03.02
Python:3.5.2(docker内のpythonのバージョン)

今回使用するデータ

 気象庁が気象データをgrib2というファイル形式で提供しています。今回は下記のファイルを使用します。

データ取得先

今回は検証のため、こちらからデータを取得してきます。企業活動で頻繁にデータを利用する方は、※気象業務支援センターからデータを直接購入してください。

※気象業務支援センターでは教育研究機関向けにデータを提供しています.企業活動等のためにデータを頻繁に必要とされる方は,気象業務支援センターからデータを直接購入し,データ提供スキーム全体の維持発展にご協力ください.
http://database.rish.kyoto-u.ac.jp/arch/jmadata/

そもそもgrib2ファイルとは

 grib2とは、世界気象機関(WMO)が定める形式で書かれたファイルで、気象データを扱うために世界中で使用されています。grib2ファイルには、地表面を格子状に分割した各地点の気象データが含まれています。
 grib2について詳しく知りたい方は、こちらを参照してください。

動作環境(grib2ファイルから気温を取得するための環境)

 grib2ファイルを読み込むためにpygribを使います。環境はdockerで準備しました。
(dockerはこちらからインストールしました。Download for Macボタンを押して、dmgファイルをダウンロードして、インストールしてください。設定は全てデフォルトでOKです。)

FROM ubuntu:latest

ENV DEBIAN_FRONTEND=noninteractive

# パッケージのインストールとアップデート
RUN apt-get update && apt-get -y upgrade
RUN apt-get -y install build-essential
RUN apt-get -y install git vim curl wget
RUN apt-get -y install zlib1g-dev \
libssl-dev \
libreadline-dev \
libyaml-dev \
libxml2-dev \
libxslt-dev \
libncurses5-dev \
libncursesw5-dev 

# pyenv のインストール
RUN git clone git://github.com/yyuu/pyenv.git /root/.pyenv
RUN git clone https://github.com/yyuu/pyenv-pip-rehash.git /root/.pyenv/plugins/pyenv-pip-rehash
ENV PYENV_ROOT /root/.pyenv
ENV PATH $PYENV_ROOT/bin:$PATH
RUN echo 'eval "$(pyenv init -)"' >> .bashrc

# anaconda のインストール
ENV ANACONDA_VER 4.1.1
ENV LD_LIBRARY_PATH=/lib/x86_64-linux-gnu:$PYENV_ROOT/versions/anaconda3-$ANACONDA_VER/lib
RUN pyenv install anaconda3-$ANACONDA_VER
RUN pyenv global anaconda3-$ANACONDA_VER
ENV PATH $PYENV_ROOT/versions/anaconda3-$ANACONDA_VER/bin:$PATH

# ライブラリのアップデート
RUN conda update -y conda
RUN pip install --upgrade pip
RUN conda install -c conda-forge pygrib=2.0.2

RUN conda install -c conda-forge/label/gcc7 jpeg

RUN pip install numpy --upgrade
RUN pip install pandas --upgrade

RUN mkdir /temp

上記のDockerfileをbuildします

docker build -t pygrib_ubuntu .

データの取得

データ取得するためのソースコードは下記です。

# -*- coding: utf-8 -*-
from datetime import timedelta
import pygrib
import pandas as pd
import sys
import numpy as np
import os

# 取得したい場所の緯度経度
latitude = 34.123456
longitude = 134.123456

# ファイル内の時刻はUTCなので、JSTに変換
time_diff = timedelta(hours=9)

def _read(srcfn):
    src = pygrib.open(srcfn)
    return src

def _close(src):
    src.close()

def create_csv(parameter_name, file_name):
    selected_item = gpv_file.select(parameterName = parameter_name )

    df = pd.DataFrame({
        "validDate" : [item.validDate + time_diff for item in selected_item],
        file_name : [
            item.data(
                lat1 = latitude - 0.025,
                lat2 = latitude + 0.025,
                lon1 = longitude - 0.03125,
                lon2 = longitude + 0.03125,
            )[0][0][0] for item in selected_item
        ]
    })

    df.to_csv( file_name + '/' + os.path.basename(sys.argv[1]) + '_' + file_name + '.csv', index=False, columns=["validDate", file_name])

# ファイル読み込み    
gpv_file = _read(sys.argv[1])

# CSVファイル作成
create_csv("Temperature","temperature")

 下記コマンドを実行して、任意の時間帯の気温を取得します。
(Z__C_RJTD_20190701000000_MSM_GPV_Rjp_Lsurf_FH00-15_grib2.binはgrib2ファイルのファイル名)

# ローカル上
docker run -it -v /path/to/somepath:/temp pygrib_ubuntu
# docker上
python /temp/get_temperature.py Z__C_RJTD_20190701000000_MSM_GPV_Rjp_Lsurf_FH00-15_grib2.bin

 上記の実行例では、2019/7/1 9:00~2019/7/2 1:00までのデータ、つまり、16時間分のデータがcsvファイルで取得できます。データの単位はケルビン(K)であり、℃ではない点に注意してください。これを学習に使用したい分だけ、取得します。今回の予測には学習用に1年分を、評価用に1ヶ月分を使用しています。

動作環境(tensorflowを実行するための環境)

次に、tensorflowを使用するための環境をdocker上に構築します。

FROM tensorflow/tensorflow:latest-py3

RUN apt-get update && apt-get -y upgrade
RUN pip install pandas --upgrade
RUN pip install matplotlib --upgrade
RUN pip install keras --upgrade
RUN pip install tensorflow --upgrade
RUN pip install sklearn --upgrade
RUN mkdir /temp

上記のDockderfileをbuildします

docker build -t tensor_flow_ubuntu .

予測するためのソースコードは下記です。使用できるモデルはいくつかあるのですが、今回はlong short time memory(lstm)というモデルで予測を行いました。lstmは、時系列データで学習できるようにするネットワークで、今回の予測に適しているので採用しました。

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.pyplot as plt
import math
from keras.models import Sequential
from keras.layers import Dense, LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error

plt.switch_backend('agg')
dataframe = pd.read_csv('learn_temperature.csv', usecols=[1], engine='python')
dataset = dataframe.values
dataset = dataset.astype('float32')
plt.plot(dataset)
 
# 乱数を固定する
np.random.seed(7)
 
# データを正規化する
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)

# 教師データと予測データに分割する
train_size = 8775
# train_size = int(len(dataset) * 0.7)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]

# 配列データを行列データに変換する
def create_dataset(dataset, maxlen):
    #dataX, dataY = [], []
    dataX = []
    dataY = []
    for i in range(len(dataset)-maxlen-1):
        # 時刻t、気温x(t)とすると、x(t),x(t+1)...x(t-maxlen)を入力とする
        a = dataset[i:(i+maxlen), 0]
        dataX.append(a)
        # x(t+maxlen)を出力とする
        dataY.append(dataset[i + maxlen, 0])
    return np.array(dataX), np.array(dataY)
 
# 行列データを作成
maxlen = 24
trainX, trainY = create_dataset(train, maxlen)
testX, testY = create_dataset(test, maxlen)
 
# 行列を[samples, time steps, features]という形に変換する
trainX = np.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = np.reshape(testX, (testX.shape[0], 1, testX.shape[1]))

# LSTMネットワークを構築する
model = Sequential()
model.add(LSTM(4, input_shape=(1, maxlen)))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(trainX, trainY, epochs=10, batch_size=1, verbose=2)

# 予測する
trainPredict = model.predict(trainX)
testPredict = model.predict(testX)

# 正規化したデータを基に戻す
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])

# グラフ化するために、教師データをずらす
trainPredictPlot = np.empty_like(dataset)
trainPredictPlot[:, :] = np.nan
trainPredictPlot[maxlen:len(trainPredict)+maxlen, :] = trainPredict

# グラフ化するために、予測データをずらす
testPredictPlot = np.empty_like(dataset)
testPredictPlot[:, :] = np.nan
print("len(trainPredict),len(dataset)")
print(len(trainPredict),len(dataset))
testPredictPlot[len(trainPredict)+(maxlen*2)+1:len(dataset)-1, :] = testPredict

# データセットと予測値をプロットする
plt.plot(scaler.inverse_transform(dataset), color ="g", label = "row")
plt.plot(trainPredictPlot,color="b", label="trainpredict")
plt.plot(testPredictPlot,color="m", label="testpredict")
 
# グラフを保存
plt.legend()
plt.savefig('result.png')

np.savetxt('test_predict_plot.csv',testPredictPlot,delimiter=',')

下記コマンドを実行して、予測を行います。

# ローカル
docker run -it -v /path/to/somepath:/temp tensor_flow_ubuntu
# docker上
python /temp/predict_temperature.py

 学習に使用したデータ(learn_temperature.csv)と、出力結果(test_predict_plot.csv)は下記です。test_predict_plot.csvは、学習期間の値(1~8789行目)はnanで、8800行目からが予測値になっています。

実際の値と予測値をグラフ化したものが下記です。

未来1ヶ月の実データと予測データ

予測精度については下記の通りとなりました。
 1.全てのデータは3%以内の誤差(ケルビン換算)
 2.90%のデータは3%以内の誤差、9%のデータは、10%以内の誤差、1%のデータは、20%以内の誤差(摂氏換算)

考察、まとめ

 今回はtensorflowを使って気温の予測を行いました。一般的にtensorflowのメリット、デメリットは下記と言われています。
メリット
・利用者が多いので、調べた時に情報を得やすい
デメリット
・計算グラフ構築後の変更が不可能
このくらいの予測では計算グラフを変更することはしなかったので、特に大きなデメリットは感じませんでした。tensorflowを使ってよかったと思います。
 次に、精度についてですが、学習期間のデータを増やす、学習回数を増やす等で精度を上げることは可能です。ただし、昨今温暖化が進んでいる影響で、学習期間のデータを増やしすぎると精度が逆に下がる等の傾向が見られるかもしれないので、調査してみるのも良いかもしれません。
 さらに今後は、このモデルを使って、別の気象データについても予測がうまくいくのか検証していきたいと思います。

SpringBoot、Thymeleafでのページング機能の実装(Java初心者向け)

はじめに

 現在のプロジェクトでは、プログラムはJava(Spring-Boot)、フロントはthymeleaf、Bootstrapを使って開発をしています。
 開発する中で、時間を要したのがページネーション処理です。同環境での実装例も見当たらず、数日かかっての実装となりました。
 そこで、Spring-Boot、Tymeleaf環境でのページネーションの実装方法をご紹介します。同環境でページネーション処理につまずいている方に読んでいただきたいです。

環境

実装ページング機能概要

  • テーブルでData内容を表示する(3件づつ)
  • ページ数の表示、そのページへの遷移が可能
  • 現在のページの所在が分かる。
  • 「次へ」「前へ」を表出させて遷移できる。
  • 「最初」「最後」を表出させて遷移できる。
  • 表示するページ数を制限する(3ページ)

ディレクトリ構成

src/
├ java/
│ ├ sampleController.java
│ ├ sampleForm.java
│ ├ sampleDao.java
│ └ member.java
└ resourse/
└ html/
└ sample.html

1-1 Controller

package jp.sbworks.standard.web.controller;

import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import jp.sbworks.standard.web.dao.SampleDao;
import jp.sbworks.standard.web.db.single.pojo.SSample;

@Controller
public class SampleController {

    /** 1ページの表示数 */
    private final String limit = "3";

    /** ページネーションで表示するページ数 */
    private int showPageSize = 3;

    @Autowired
    private SampleDao sampleDao;


    @RequestMapping(value = "/sample",method = RequestMethod.GET)
    @Transactional(readOnly = true)
    public String index(Model model, @RequestParam HashMap<String, String> params) throws Exception {
        // パラメータを設定し、現在のページを取得する    
        String currentPage = params.get("page");

        // 初期表示ではパラメータを取得できないので、1ページに設定
        if (currentPage == null){
            currentPage = "1";
        }
        // データ取得時の取得件数、取得情報の指定
        HashMap<String, String> search = new HashMap<String, String>();
        search.put("limit", limit);
        search.put("page", currentPage);
    
        int total = 0;
        List<SSample> list = null;
        try {
            // データ総数を取得
            total = sampleDao.getMemberListCount();
            // データ一覧を取得
            list = sampleDao.getMemberList(search);
        } catch (Exception e) {
            return "error/fatal";
        }

        // pagination処理
        // "総数/1ページの表示数"から総ページ数を割り出す
        int totalPage = (total + Integer.valueOf(limit) -1) / Integer.valueOf(limit);
        int page = Integer.valueOf(currentPage);
        // 表示する最初のページ番号を算出(今回は3ページ表示する設定)
        // (例)1,2,3ページのstartPageは1。4,5,6ページのstartPageは4
        int startPage = page - (page-1)%showPageSize;
        // 表示する最後のページ番号を算出
        int endPage = (startPage+showPageSize-1 > totalPage) ? totalPage :    startPage+showPageSize-1;
        model.addAttribute("list", list);
        model.addAttribute("total", total);
        model.addAttribute("page", page);
        model.addAttribute("totalPage", totalPage);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);
        return "html/sample/index";
        }
    }

1-2 DAO

package jp.sbworks.standard.web.dao;

import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;

import org.sql2o.Connection;
import org.sql2o.Query;
import org.sql2o.Sql2o;

import jp.sbworks.standard.web.db.single.pojo.SSample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;


@Repository
public class SampleDao {

    @Autowired
// データベースアクセス用のライブラリ
    private Sql2o sql2o;
 
    // リスト情報を取得する 
    public List<SSample> getMemberList(HashMap<String, String> search) throws SQLException, IOException {
        // Jdbcに接続する
        Connection con = sql2o.open();
        con.getJdbcConnection().setAutoCommit(false);
        // クエリの作成
        Query query = con.createQuery("select ID as id, NAME as name, DEPARTMENT as department, DAY_OF_JOINING as dayOfJoining from SAMPLE limit :limit offset :offset;");
        int limit = Integer.valueOf(search.get("limit"));
        int page = Integer.valueOf(search.get("page")) - 1;
        // 何件情報を取得するかの指定。
        query = query.addParameter("limit", limit);
        // 何件目からの情報を取得するかの指定(※コントローラからパラメータを使って現在のページ数が分かる。それによって何件目からの情報を取得すればいいのかが分かる。)
        query = query.addParameter("offset", limit * page);

        return query.executeAndFetch(SSample.class);
    }
    
    // リスト情報件数を取得する 
    public int getMemberListCount() throws SQLException, IOException {
        Connection con = sql2o.open();
        con.getJdbcConnection().setAutoCommit(false);
        Query query =  con.createQuery("select count(1) from SAMPLE");
        return query.executeAndFetchFirst(Integer.class);
     }
}

1-3 DB

package jp.sbworks.standard.web.db.single.pojo;

import java.io.Serializable;
import java.time.LocalDate;

public class SSample implements Serializable {

    // ID 
    public Integer id;
    // 名前 
    public String name;
    // 部署 
    public Integer department;
    // 入社日
    public LocalDate dayOfJoining;
    // 登録者 
    public String regName;

}

1-4 html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <script type="text/javascript" th:src="@{/webpack-bundled/Sample/bundled.js}"></script>
    </head>
<div class="table-scroll mb-2">
    <table class="table table-bordered table-sm mt-1">
        <thead>
            <tr>
                <th>社員ID</th>
                <th>名前</th>
                <th>部署</th>
                <th>入社日</th>
                <th>登録者</th>
            </tr>
        </thead>
        <tbody>
            <th:block th:each="item, status : ${list}">
                <tr>
                    <td th:text="${item.id}"></td>
                    <td th:text="${item.name}"></td>
                    <td th:text="${item.department}"></td>
                    <td th:text="${item.dayofjoining}"></td>
                    <td th:text="${item.regname}"></td>
                </tr>
            </th:block>
        </tbody>
    </table>
</div>
<!-- ここからページング処理 -->
<nav>
    <ul class="pagination pg-blue justify-content-center">
        <li th:if="${startPage} > 1" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=1'}" tabindex="-2">最初</a>
        </li>
        <li th:if="${page} > 1" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${page-1}}" tabindex="-1">前へ</a>
        </li>
        <th:block th:if="${endPage}<=0">
            <li class="page-item " th:classappend="${i == page} ? active" th:each="i : ${startPage}">
                <a class="page-link" th:href="@{'/sample?page=' + ${i}}" th:text="${i}"></a>
            </li>
        </th:block>
        <!-- StartPageからEndPageまでのページ数を表示する -->
        <th:block th:if="${endPage}>0">
            <li class="page-item " th:classappend="${i == page} ? active" th:each="i : ${#numbers.sequence(startPage, endPage)}">
                <a class="page-link" th:href="@{'/sample?page=' + ${i}}" th:text="${i}"></a>
            </li>
        </th:block>
        <li th:if="${page} < ${totalPage}" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${page+1}}">次へ</a>
        </li>
        <li th:if="${endPage} < ${totalPage}" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${totalPage}}">最後</a>
        </li>
    </ul>
</nav>

現在いるページの所在を分かりやすくするために、Bootstrapを使って色付けをしています。下記は現在表示しているページとして青色で表示する場合の処理を簡単に書きました。

<nav>
    <ul class="pagination pg-blue">
…
<li th:classappend="${i == page} ? active" >

結果

下記のように実装できました。

まとめ

ページングの処理は煩雑なりがちですが、基本的な動きはJavaで実装し、Thymeleafで条件分岐、繰り返し処理を活用することで、より可読性が高く、シンプルなプログラムとなります。
 私がつまづいた箇所は、2ページ以降のデータの取得です。コントローラでパラメータを使って現在のpage番号を取得することで、取得を始めるデータを特定でき、ページごとに正しいデータを取得することに成功しました。
 1ページに表示するデータ数や、表示ページ数の指定をプロジェクト用に沿って替えることで、活用いただければ幸いです。

author/A.Nishihara

開発環境構築について ~ Windows 10 Home + Dockerでmysqlを動かす ~

  • はじめに
    ローカル環境で開発作業をする際、以下のような様々な課題を経験することがあります。
    一例として、
    ・開発環境構築手順が多く、煩雑で環境を整えるのに1日費やす
    ・各自のマシンで若干設定が異なっていることによって、環境依存が生じる
    ・DBが共有化されている(共通のサーバーに有る)ので、データを容易に弄れない(データの洗い替えができないなど)
    etc
    があります。
    そこで、今回は各自のローカル環境構築の手間を減らすことを目的とした、
    Windows Homeでの開発環境の整備について記します。
    ※Windows ProとMacはDocker-toolboxを使わず、Dockerをインストールしてください。
    今回の内容で コマンドだけで、ローカル環境にAmazon Linuxを立ち上げ、その環境でアプリを動かすことができました。また、コンテナ上にMySQL(DB)を用意したことで、自分専用のDBが作れました。

構成内容

  • Windows 10 64bit Home Edition
  • Docker-Toolbox (インストーラver 18.03.0-ce)※基本的にlatestで良いと思います。
    Windows HomeではHyper-vが使えないので、Hyper-Vの代わりにOracle Virtual Boxを利用するDocker Toolboxを使います。
  • Java:java-11-amazon-corretto
  • amalizonlinux:2
  • MySQL:8.0.18

Oracle Virtual Box + Docker Toolboxをインストールする

  1. ダウンロード&インストール
    Docker Toolboxをダウンロード
    基本デフォルトで好きな場所にインストールしてください。
    デスクトップにアイコンは作っておいたほうが後々楽になります。
  2. 起動
    デスクトップに作成されたDocker Quickstart Terminalを起動させる。
    ここに起動後の画像を貼る。
    起動するとDockerでおなじみのクジラが出てくる。
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/

docker is configured to use the default machine with IP 192.168.99.100
For help getting started, check out the docs at https://docs.docker.com


Start interactive shell

上記のメッセージが出ると思うので、IPをどこかに記録しておきます。(後で確認することも可能。)

合わせてDockerで使う必要なコマンドが最低限インストールされています。
docker ps
docker-compose
…etc

今回の構成

directory
| docker-compose.yml
|─Dokcer
|─java
│ Dockerfile(webアプリを動かすサーバー情報を記載)

└─mysql
│ Dockerfile(DBの情報を記載)

├─conf.d
│ my.cnf(mysqlのconf内容を記載)

└─initdb.d
1__init.sql(初期データ等を投入するためのSQL)
バージョン情報
java:java-11-amazon-corretto
amazonlinux:2
mysql:8.0.18

dockerでamazonlinuxを動かす準備

サーバーの構成を記載した、サンプルの設計書を記載します。

FROM amazonlinux:2

#タイムゾーンの設定
RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# x86_64 args
ARG rpm_x64=java-11-amazon-corretto-devel-11.0.5.10-1.x86_64.rpm
ARG path_x64=https://d3pxv6yz143wms.cloudfront.net/11.0.5.10.1
ARG key_x64=13817E35D6AA26BB2D85267712EABAC5209DDBC0

# aarch64 args
ARG rpm_aarch64=java-11-amazon-corretto-devel-11.0.5.10-1.aarch64.rpm
ARG path_aarch64=https://d3pxv6yz143wms.cloudfront.net/11.0.5.10.1
ARG key_aarch64=13817E35D6AA26BB2D85267712EABAC5209DDBC0

# In addition to installing the RPM, we also install
# fontconfig. The folks who manage the docker hub's
# official image library have found that font management
# is a common usecase, and painpoint, and have
# recommended that Java images include font support.
#
# See:
#  https://github.com/docker-library/official-images/blob/master/test/tests/java-uimanager-font/container.java
RUN set -eux; \
    case "$(uname -p)" in \
        x86_64) rpm=$rpm_x64; path=$path_x64; key=$key_x64 ;; \
        aarch64) rpm=$rpm_aarch64; path=$path_aarch64; key=$key_aarch64 ;; \
        *) echo >&2 "Unsupported architecture $(uname -p)."; exit 1 ;; \
    esac; \
    \
    curl -O $path/$rpm \
    && export GNUPGHOME="$(mktemp -d)" \
    && gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys $key \
    && gpg --armor --export $key > corretto.asc \
    && rpm --import corretto.asc \
    && rpm -K $rpm \
    && rpm -i $rpm \
    && rm -r $GNUPGHOME corretto.asc $rpm \
    && yum install -y fontconfig \
    && yum clean all

ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto

# recommended by spring boot
VOLUME /tmp

# create directory for application
RUN mkdir /app
WORKDIR /app

COPY ./build/libs/hogehoge.war hogehoge.war

# EXPOSE は開けるポートを指定する.
EXPOSE 8180

# set entrypoint to execute spring boot application
ENTRYPOINT ["sh","-c","java -Dserver.port=8180 -Dspring.profiles.active=local -jar hogehoge.war"]

今回はDockerfileの中身はdockerhubをもとにしています。
仮想AWS EC2とするためAmazon Linuxを使います。
最後のENTRYPOINTのみ各自が動かす環境に合わせてください。  
※warファイルでの実行を想定したものになります。

DockerでMySQLを動かす準備

DBのサンプル設計書になります。

FROM mysql:8.0.18

#MySQL設定ファイルをイメージ内にコピー
ADD ./conf.d/my.cnf /etc/mysql/conf.d/my.cnf
COPY ./initdb.d/* /docker-entrypoint-initdb.d/

#docker runに実行される
CMD ["mysqld"]

今回は最小限にしています。
※Amazon Linuxはdockerhubからコピーしたものですが、MySQLは自前です。
こういった形で記載することもできます。
MySQLのバージョンは各自が利用している環境のバージョンに合わせてください。latestにて最新を取得することも可能です。

Dockerを動かす準備

上記のAmazon LinuxとMySQLを動かすためのファイル(docker-copose.yml)を用意します。

version: '3.3'
services:
    mysql:
        build: ./docker/mysql
        environment:
            MYSQL_DATABASE: dbName
            MYSQL_USER: user
            MYSQL_PASSWORD: password
            MYSQL_ROOT_PASSWORD: rootpassword
        ports:
            - "3314:3306" #3314はhost側のポート、3306はコンテナ側のポート
    app:
        build:
            context: ./
            dockerfile: ./docker/java/dockerfile
        depends_on:
           - mysql
        ports:
            - "8090:8180" #8090はhost側のポート、8180はコンテナ側のポート
        links:
            - mysql

実際に動かす

  1. docker-compose.ymlが配置してあるパスに移動します。
    例:”C:workspase”
  2. docker-compose.exe build
    を実行する。Dockerfileに記載された内容に問題なければbuildされます。
  3. buildが正常に完了した場合、
    docker-compose.exe up -d
    を実行します。
    ※-dオプションを付けることでバックグランド実行になります。
  4. 実行されていることを確認する。
    docker-compose.exe ps
    を実行すると以下のような状態になり、stateがupになっていればOKです。
Name                       Command               State                 Ports
---------------------------------------------------------------------------------------------------
hogehoge_app_1     sh -c java -Dserver.port=8 ...   Up      0.0.0.0:8090->8180/tcp
hogehoge_mysql_1   docker-entrypoint.sh mysqld      Up      0.0.0.0:3314->3306/tcp, 33060/tcp
  1. コンテナを止めたい場合
    docker-compose stop
    を実行してください。
    ※upの反対でdownコマンドも用意されています。
    ですが、これはコンテナを停止し、そのコンテナとネットワークを削除します。なので、コンテナのDBやアプリに改変を加えていた場合破棄されます。

まとめ

今までのローカル環境構築では、各自が各PJの手順に沿ってDB接続や、アプリの設定等を行っていたと思います。この時点でMacとWindows用で2種類存在することもあるかもしれません。
ですが、用意したDockerfileを配布することで配布された側も配布した側と同じ環境がすぐに作成することができます。 (OSを意識することが少なくなります)
Dockerfileやdocker-compose.ymlもバージョン管理していくことをおすすめします。

ローカル環境にAmazon Linuxを立ち上げ、その環境でアプリが動く用になりました。また、コンテナ上にMySQL(DB)を用意したことで、自分専用のDBが作られました。
これでデータを自由に弄れるようになりますし、何回でも同じデータで復元できます。単体テスト等でデータを作り直すのも容易になるはずです。
  
今後は、nginxを追加しかつSonarQubeによる静的解析等を追加していく予定。

author k.kawai

DBの環境差異調査(mysql編)

システム開発において、
 ・本番
 ・検品(ステージング)
 ・開発
と環境が別れている事は通常よくあることであり、それにともないDBの環境もそれぞれ存在することとなります。

開発体制が小規模であったり、管理ルールが整っていない時などに、それぞれのDB環境に差異が発生している事が多々あります。
その差異を視覚化し、容易に把握するための方法をご紹介します。

※今回使用した環境
 ・mysql : 5.6.40
      https://dev.mysql.com/downloads/windows/installer/5.6.html
 ・ローカル環境:windows 10
 ・比較ツール:WinMerge 2.14.0
        https://winmerge.org/downloads/?lang=ja

1.差分抽出SQL の用意


SELECT '### COLUMN情報 #########################' AS '';
SELECT TABLE_NAME
     , COLUMN_NAME
--   , ORDINAL_POSITION
     , COLUMN_TYPE
     , COLUMN_DEFAULT
     , IS_NULLABLE
     , COLUMN_KEY
     , IFNULL( COLLATION_NAME , '')      COLLATION_NAME
     , IFNULL( CHARACTER_SET_NAME , '')  CHARACTER_SET_NAME
     , COLUMN_COMMENT 
  FROM INFORMATION_SCHEMA.COLUMNS
 WHERE TABLE_SCHEMA = @DB_DATABASE
 ORDER BY TABLE_NAME , COLUMN_NAME
;

SELECT '### INDEX情報 ##########################' AS '';
SELECT TABLE_NAME
     , INDEX_NAME
     , SEQ_IN_INDEX
     , COLUMN_NAME
  FROM INFORMATION_SCHEMA.STATISTICS
 WHERE TABLE_SCHEMA = @DB_DATABASE
 ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX 
;

mysql では、DBの情報を INFORMATION_SCHEMA 内に保持しており、その情報を出力しています。
※上記例では、カラムの順番を考慮せず、カラム名順で評価しています。

2.差分抽出SQL の実行

上記 SQL をファイルに保存して、コマンドで実行します。
※ここでは、「db-columns.sql」と命名します。

mysql -e "set @DB_DATABASE='DB名'; source db-columns.sql ;" > 環境.txt

※接続情報等は、省略させて頂いています。

こちらの実行した結果は以下の通りとなります。
※ mysql のバージョンに関わらず、全てのバージョンで稼働すると思います。


### COLUMN情報 #########################
TABLE_NAME	COLUMN_NAME	COLUMN_TYPE	COLUMN_DEFAULT	IS_NULLABLE	COLUMN_KEY	COLLATION_NAME	CHARACTER_SET_NAME	COLUMN_COMMENT
hoge	birthday	date	NULL	YES				
hoge	key	int(11)	NULL	NO	PRI			
hoge	name	varchar(50)	NULL	YES	MUL	utf8_general_ci	utf8	
hoge	sex	char(1)	0	YES		utf8_general_ci	utf8	

### INDEX情報 ##########################
TABLE_NAME	INDEX_NAME	SEQ_IN_INDEX	COLUMN_NAME
hoge	hoge_i1	1	name
hoge	hoge_i1	2	birthday
hoge	PRIMARY	1	key

3.SQL実行結果の比較

上記、環境毎に出力し、その結果を winmerge 等で比較する事が出来ます。
例えば、ローカルの環境と比較。

別のタブで画像を開くと、大きな画像で参照できます。

上記は、以下の テーブルを比較した結果なのですが、

-- 左のDDL
CREATE TABLE `hoge` (
	`key` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NULL DEFAULT NULL,
	`sex` CHAR(1) NULL DEFAULT '0',
	`birthday` DATE NULL DEFAULT NULL,
	PRIMARY KEY (`key`),
	INDEX `hoge_i1` (`name`, `birthday`)
)

-- 右のDDL
CREATE TABLE `hoge` (
	`key` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NULL DEFAULT NULL,
	`sex` VARCHAR(1) NULL DEFAULT '0',
	`birthday` TIMESTAMP NULL DEFAULT NULL,
	PRIMARY KEY (`key`),
	INDEX `hoge_i1` (`name`)
)

WinMarge 上で、差分行が色分けされているのが判るかと思います。
・DATE型 と TIMESTAMP 型 の差異
・CHAR型 と VARCHAR型 の差異
・INDEX の差異

※Oracle ですと、「USER_TAB_COLUMNS」・「USER_IND_COLUMNS」テーブルを使用すると、同じ情報が取得できます。

author k.tomita

マイクロフレームワークを使ってみた-Spark 編-

本記事の概要

  • Spark Framework チュートリアルやってみた
  • Spark Framework を実際のプロジェクトに使うかを評価してみた

こんにちは。SBWorksです。

突然ですが、みなさま。マイクロフレームワークという言葉を知っていますか?

マイクロフレームワークとは

microframework is a term used to refer to minimalistic web application frameworks. It is contrasted with full-stack frameworks.

出典 https://en.wikipedia.org/wiki/Microframework

簡単に言うと、軽量でフルスタックなウェブフレームワークって感じですね。最近は、急激な環境変化に対応する為に、お互いに疎なサービスを開発し、それらの組み合わせて大きなサービスを実行するという開発がトレンドになってきています。フレームワークもその急激な変化に対応するために、軽量なフレームワークの開発が盛んに行われているようです。色々なフレームワークが開発されている中で、今回はJavaのフレームワークである、Spark Frameworkを使ってみようと思います。(詳しく知りたい人は公式ページのドキュメントを参照してください。)

Spark Framework のチュートリアルやってみた

それでは実際にSpark Frameworkを使ってみましょう。

まずは公式ページへ。

チュートリアルページへ。

今回はBasic webapp structure のチュートリアルをやってみます。

Githubにソースコードが公開されている。チュートリアルが充実してるのはありがたいですね。

javaディレクトリ配下のソースコードはこんな感じ。util + 画面毎に分かれてる感じ。

resourceディレクトリ配下のソースコードはこんな感じ。velocity使ってる。

gradleとmavenがあったので、今回はgradleでビルドしてみます。

$ cd path/to/spark-basic-structure
$ gradle build

起動してみます。

$ gradle run

立ち上がったらログイン画面(http://localhost:4567/login/)へアクセスしてみます。

ひとまず動きました。あとはUsername/Passwordを入力して(UserDao.javaを確認)、書籍一覧画面(localhost:4567/books/)へアクセスしてみると、

書籍一覧も表示できました。

Spark Framework を実プロジェクトに使うかを検証

チュートリアルを動かすことはできたので、これを実プロジェクトで使えるのかを検証していきたいと思います。

まずは本番環境にリリースすることを想定して、jarファイルを作成して起動します。

$ cd path/to/spark-basic-structure
$ gradle build
$ java -jar build/libs/spark-basic-structure-all-1.0-SNAPSHOT.jar

起動したら、先ほどと同じようにログイン画面(http://localhost:4567/login/)へアクセスしてみます。

CSSが効いていない…。ソースコードで該当箇所を調べます。どうやら下記ソースコードがgradle run で起動した時とjarで起動した時で異なるようです。

staticFileLocation("/public");

調べてみると、同じようにrunタスクで起動した時には正常に動くのに、jarで起動すると、静的ファイルが読み込めないという記事がいくつかあったので同じようにハマっている人がいるようです。
公式にissueを投げている人がいて、grade shadowでjarファイル作ったらうまくいくよと書いていたので、試してみるもうまく行きませんでした。

jarでの不具合が出ている問題が解決できないと、本番稼働できないので、今回はSpark Framework の使用は一旦見送ることにしました。

結論

  • Spark Framework でチュートリアルやってみた
  • Spark Framework を実プロジェクトでは使わないことにした

最後に

今後もマイクロフレームワークを検証していきたいと思います。

ありがとうございました!