EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

Dockerについて

初めまして、EatSmartの新人エンジニアが初ブログを更新したいと思います。
入社してちょうど1ヶ月が経ちました。

現在、社内向けのツールを開発中でその際にdockerで仮想環境を構築しました。
初めてDockerを動かすことになったので調べた内容をまとめてみました。


Dockerとは

コンテナ型の仮想環境を構成、管理するソフトウェアでLinuxカーネルが持つ「コンテナ」機能などを使って実行環境を他のプロセスから隔離しその中でアプリケーションを動作させる技術のこと

VMVMwareのようなサーバー仮想化との違いは扱う単位がマシンではなくプロセスであること

サーバー仮想化ではホストマシン上でゲストマシンが動き、ゲストマシン内で様々なプロセスが動きます。一方のコンテナ型の仮想化ではホストOS上にコンテナという隔離された空間が作成されその中でプロセスが実行されます

Dockerマシンは何が良いのか

■軽量で高速に起動、停止などが可能であること

DockerはLinuxのコンテナ技術を使ったものでよく仮想マシンを比較させます、Virtualboxなどの仮想マシンではホストマシン上でハイパーバイザを利用しゲストOSを動かしその上でミドルウェアなどを動かす。それに対しコンテナはホストマシンのカーネルを利用しプロセスやユーザーなどを隔離することであたかも別のマシンが動いているかのように動かすことができる

■安定した開発を進めたり、リリースサイクルの改善に役立つ

コード化したもの(Dockerfile)を構成管理し、それをCIツール(JenkinsやCircleCI)でビルド(Dockerイメージの作成)、デプロイ(コンテナの起動)、環境テスト(Serverapec)、画面試験(Selenium)を毎日実行すれば環境面の確認も含めて安心して開発を進められるようになる

Docker登場の背景

アプリ開発者にとってハードウェアの調達やメンテナンスは開発以外での作業工程が増え足かせになっていた
サービスモデルの一つであるlaasをより効率よく活用するために必要最低限のリソースでアプリケーションを実行できる環境を提供するコンテナ型サービスが注目を集めている

Dockerの実行環境について

今回はLinux CentOS 7 64bitを使用しましたがその他にも
Docker for Windows / Docker for Mac などのソフトウェアが開発されている


導入 コマンド

# yum install docker
#chkonfig docker on
#service docker start

Docker操作の基本
Dockerサーバ(コンテナ実行用のエンジン)とクライアント(管理ツール)に分かれたツールである

イメージの取得 → コンテナの起動 → コンテナの停止 → イメージの作成 → コンテナの削除 → イメージの削除 といった順で操作していきます



DockerHubで管理

作成したコンテナイメージはDockerHubといったコンテナイメージ登録や共有ができるサービスサイトで管理ができる。このようなサイトにDockerが構成されたサーバーなどの環境からアクセスできれば作成したコンテナイメージをアップロードやダウンロードし利用できる


参考資料

http://docs.docker.jp/
http://knowledge.sakura.ad.jp/tech/2210/

Postgresql DB移行

インフラの見直しに伴いDB移行を行う事になりましたが、フルダンプの移行だと数日かかることが判明した為、いくつか時間短縮の施策を検討してみました。

移行環境

(移行前)
postgresql8.3.5
※移行前の容量は400G程度
(移行後)
postgresql9.3.5

施策1

まず、フルダンプの移行をやめて、テーブル単位の移行に変更しました。また、テーブルの中でも、変更が発生しないマスター系のテーブルと、変更が発生するトランザクション系のテーブルを精査して、マスター系のテーブルは事前移行する事にし、トランザクション系のテーブルのみをサービス停止して移行する事にしました。
後、トランザクション系のテーブルは、移行の必要が無い不要レコードを出来る限り削除しました。

施策2

「施策1」を実施して、再度移行時間を計測してみましたが、それでも数十時間かかることが判明しました。そこで、その中でも移行に時間がかかるテーブルに着目し、時間短縮できないか検討してみました。

施策3

「施策2」で、エクスポート/インポート/index作成等を細分化して計測してみた所、インデックス作成に時間が掛かっていることが分かりました。
そこで、index作成を高速化できないか調べてみた所、DBのパラータ[maintenance_work_mem]を増やすことで高速化できることが分かりました。
https://www.postgresql.jp/document/9.3/html/populate.html

また、postgresqlの移行高速化方法を調べていく中で、WALの書き込み停止(full_page_writes=false/fsync=false)、pg_dump/pg_restoreを並列で実行するオプション[jobs]が有効であることも分かりました。
WALの書き込み停止
https://www.postgresql.jp/document/9.3/html/runtime-config-wal.html

並列で実行するオプション[jobs]
https://www.postgresql.jp/document/9.3/html/app-pgrestore.html
https://www.postgresql.jp/document/9.3/html/app-pgdump.html

移行リハーサル

「施策3」を一通り実装し移行リハーサルを実施した所、エクスポート/インポートトータルで10時間以内で終える事ができました。
本内容を踏まえ、本番移行の準備を行いました。

本番移行

本番当日、サービス停止している事を考慮し[maintenance_work_mem][jobs]のパラメータの値を増やして実施しました。
エクスポート迄は[jobs]のパラメータの値を増やした影響で、スケジュールより前倒しで完了しました。ところが、インポートの処理が、予定より数時間遅延しました。
原因としては、[maintenance_work_mem]の値を増やしすぎたことにより、swapが発生した事が考えられます。

最後に

スケジュールは遅延しましたが、何度かリハーサルを行っていたので、ある程度の遅延時間の予測が立てれた為、切り戻しを実施せず完了する事が出来ました。改めて、リハーサルの重要性を感じました。

データセンターからクラウドへの移行

これまでイートスマートでは、サービスの大部分をデータセンターを利用して提供してきました。 データセンターの利用を開始してから年月が経つことで、ネットワーク機器・サーバの故障や容量不足が目立つようになりました。 データセンターを利用しているなかで問題となったのは、主に以下の3つです。

  • ネットワーク機器・サーバの老朽化によるトラブル
  • 上記要因による保守作業の増加
  • サーバのリソース不足

ハードウェアの老朽化により、故障が発生するリスクが高くなり、部品の交換にも支障をきたすようになりました。 また、サービスが成長するに伴いサーバのメモリ等の容量不足が目立つようになりました。 そこで昨年から、データセンターからクラウドへの移行を計画しました。

クラウドへの移行計画

移行を計画するにあたり、現状・移行のそれぞれのメリット・デメリット、それに伴うリスクを検討しました。 検討内容を踏まえ、データセンターから既に利用実績のあるさくらインターネットへ移行することになりました。 またこのタイミングで、既に一部のサービスの本番環境で稼働実績のあるDockerを全面的に利用することになりました。 これに伴い、アプリケーションのビルド・デプロイ・監視・ログ収集等の仕組みを作り直しました。

アプリケーションのビルド・デプロイ

アプリケーションのビルド・デプロイには、引き続きJenkinsを利用しました。 これまでは、VCSからチェックアウトしたソースをビルドし、rsyncでデプロイを行っていました。 Dockerを利用するにあたり、ビルド後にDockerイメージを作成し、Docker Registryへpushし、Dockerホストでpull/runを行うようにしました。

監視

監視には、こちらも引き続きZabbixを引き続き利用しました。 Dockerホストを含む各サーバへZabbix Agentを導入し、Zabbixから各種リソース・サービスの状態を取得しました。 Javaアプリを動かすDockerコンテナは、Zabbix Java Gatewayを利用してJMXの状態を取得しました。

ログ

ログはこれまでは各サーバに書き出し、バッチでファイルサーバへ集約していました。 これを、fluentdを利用してほぼリアルタイムでファイルサーバへ集約するようにしました。 これにより、今まで各サーバにsshでログインしていたログの確認が、ファイルサーバで行えるようになりました。

バッチの移植

バッチ処理はこれまで必要なミドルウェアや設定が行われてた専用のサーバで処理を行っていました。 バッチもwebアプリケーションと同様にDockerイメージ化することで、リソース管理が柔軟になりました。 このタイミングでバッチの洗い出しを行いました。移行当日に停止した場合の影響範囲の調査と、手動実行が必要か等を確認しておきます。

データベースのレプリケーション

これまでも一部サービスでは導入していたデータベースのレプリケーションを、全てのサービスで導入しました。

クラウドへの移行準備

移行にあたり、移行先の環境を出来る限り再現したものを用意して検証を行いました。 既存の仕組みをそのまま移行するわけでは無いため、現状の安定性とパフォーマンスを満たすことが出来るかを確認しました。 確認にあたり、主にApache BenchとJMeterを利用しました。 Apache Benchでは、Nginx/Apache/Tomcat等の高負荷状態を作り、安定性の検証を行いました。 JMeterでは、本番環境に近いシナリオを作り、安定性・パフォーマンスの検証を行いました。

検証と並行して、データセンター側の作業も進めました。 移行当日の作業を減らすため、事前にバックエンド系のサービスをクラウドへ移行しました。 また、移行にかかる時間を短縮するため、データの転送に時間がかかるデータベース・ファイルサーバも準備を行いました。 データベースは、事前にベースバックアップを転送し、レプリケーションを行うことが決まりました。 データセンター側をマスタ、クラウド側をスレーブとして移行当日まで運用し、当日にクラウド側をマスタに昇格させることでダウンタイムを短くするためです。 ファイルサーバは、事前にクラウドへ全ファイルの転送を行い、当日まで同期を行うようにしました。

最後に移行テストを実施し、作業内容に見直し等を行いました。

クラウドへの移行実施

移行は3つのフェーズで実施しました。

移行前日

  • 開発中のアプリを凍結し、移行のためのマージ作業
  • IPアドレスが変わるため、反映までの時間を短くするためDNSTTLの変更
  • zabbixによる監視の開始

移行当日

  • サービス停止
  • データベースなどのバックアップの停止
  • バッチ処理の停止
  • データベースの切り替え(マスタ昇格)
  • DNSの切り替え
  • サービスの動作確認
  • サービス再開

移行後

  • ログ等からサービスの切り替えの確認
  • データセンターのデータのバックアップ
  • データセンターの機材の撤収

データセンターを切り替えるための最後の作業を行いました。 準備の段階で大部分の作業を終えているため、前日・当日の作業自体は少なくて済みました。

動作確認が終わりサービスを再開すると、検証時には見つけられなかった問題がいくつか発生しました。 そのなかのひとつが、Dockerホストが不安定になり再起動が必要になってしまったことです。 Swarmモードの利用の停止、Dockerコンテナの再配置、メモリやサービスのチューニングで対処しました。

おわりに

データセンターを利用していて当時の問題は、移行を行うことで解消することが出来ました。 移行直後に一時的に不安定になりましたが、上記の対処を行うことで現在は安定しています。 リソースに余裕が出来たことで、これまで稼働させることが出来なかったものや、冗長構成を取ることが出来るようになりました。 いくつか失敗もしてしまいましたが、普段の業務では出来ない経験をすることが出来ました。

docker buildでURLを指定する

こんにちは。お久しぶりです。

大分長い間ブログを寝かせていましたが、ブログを再開することにしました。 まずはウォーミングアップで、簡単なネタから…。

アプリケーションのビルドとデプロイ

弊社では、ソース管理にgit、アプリケーションの稼働にdockerを使っている環境があります。 (このころdockerを使おうと頑張っていましたね。) eatsmart.hatenablog.com

その環境では、アプリケーションのデプロイには、

  1. git上のソースをビルド
  2. dockerコンテナ構築
  3. デプロイ

という手順を踏むのですが、1について、ビルドサーバー上の適当な場所へgit cloneを行なっていました。

ビルドするための環境

ただ、ビルドサーバー上で"適当な場所"を決めてgit cloneしなければいけないし、それ以前に、実稼働環境のランタイムやライブラリのバージョンがアプリケーションによって異なるので、ビルドサーバー上での環境についてのバージョン管理が複雑になっていました。

ビルド環境のコンテナ化とURL指定

どうにかビルド環境を簡単に管理することができないかと思いながらdockerのドキュメントを見ていたら、

docker build [OPTIONS] PATH | URL | -

URLを指定してdocker buildできることに気づきました。

指定するURLとしてgitのリポジトリを使うことができ、以下のように指定できます。

指定リポジトリ URL buildのルートフォルダ
master myrepo.git masterブランチのルート
ブランチ指定 myrepo.git#branch branchブランチのルート
ディレクトリ指定 myrepo.git#:myfolder masterブランチの/myfolder
ブランチ&ディレクトリ指定 myrepo.git#branch:myfolder branchブランチの/myfolder

docker build に渡すURLを切り替えることで、branchについてのコンテナも簡単に作ることができそうです。

最後に

git上にDockerfileを格納し、docker build時にgit上のURLを指定することで、専用の環境でビルドとコンテナ構築をまとめてできるようになりました。

もぐナビの検索をリニューアルしました

10月中旬に、もぐナビの検索をリニューアルしました。

mognavi.jp

イートスマートのサービスでは、データベースに主にPostgreSQLを利用しています。
これまで検索を行う場合は、PostgreSQL全文検索を可能にするpg_bigmというモジュールを導入して利用してきました。
今回のリニューアルでは、pg_bigmからElasticsearch+Kuromojiへの移行を行いました。

リニューアルの背景

pg_bigmは名前の通り2-gram(バイグラム)という方法で全文検索を行います。今回のリニューアルでは、2-gramからKuromojiによる形態素解析を利用した全文検索へ変更しました。
2-gramと形態素解析はそれぞれ利点・欠点が挙げられます。今回のリニューアルでは、関係の無いキーワードでヒットすることを減らし検索の精度を上げるために形態素解析の導入を行いました。

もぐナビが扱う商品のメーカー・ブランドには、「ファミリーマート」「ファミマ」や「マクドナルド」「マック」「マクナル」のように複数の呼び方が存在するものがあります。
類義語による検索の実現も、今回のリニューアルで目指しました。

環境構築

今回はdockerを利用して環境を構築しました。

eatsmart.hatenablog.com

環境の構築には公式から提供されたものを利用しました。
これにKuromojiのインストール等の初期設定を行い、オリジナルのイメージを作成しました。
また、既存の辞書へ、メーカー・ブランド名を元に独自に追加も行いました。

課題

辞書のメンテナンス

日々新たな商品が発売されたりブランドが登場します。
これらは既存の辞書では適切に処理出来なくなる事が予想されるため、継続的な辞書のメンテナンスが必要になります。
これには別途集計している検索ログを活用し、ヒットしなかったキーワードの確認と適切なキーワードの登録を行う仕組みづくりに取り組みます。

ひらがな・カタカナによる検索

例えば「チョココロネ」の商品情報を検索するため「ちょこころね」と入力すると、「ちょこ」「ころね」ではなく「こころ」と分解されます。
これではユーザーは目的の商品を見つける事が出来ません。

フロントエンド改善の取り組み

もぐナビではサイトの閲覧を快適にするために、フロントエンドの改善を継続して行っています。

今回は今まで行ったフロントエンド改善の取り組みを振り返りたいと思います。

 

参考にしたもの

サイトを高速化するため、まずは以下の本を読みました。

この本に書かれたルールを元に、改修と計測を繰り返しました。

www.oreilly.co.jp

 

また、改修を行った後の確認には、以下を利用しています。

 

行ったこと

1.画像の最適化

もぐナビでは商品画像を初め、クチコミ画像など大量の画像が表示されます。また、ロゴ画像やUIで利用する画像もあります。

 HTTPリクエストを減らすため、このうちUIで利用する画像をCSSスプライトにしました。合わせて画像の最適化を行い、容量の低減も行いました。

CSSスプライトの作成にはCSS Sprite Generatorというサイトを利用しました。

画像の最適化にはTinyPNG – Compress PNG images while preserving transparencyというサイトを利用しました。

 

それ以外の商品画像やクチコミ画像は、jpegtranを利用して最適化を行っています。

これにより、最大60%程度の容量を削減する事が出来ました。

2.JS/CSSの最適化

画像と同様にHTTPリクエストの数を減らすため、JS/CSSの最適化 も行いました。

こちらはYUI Compressorをantを利用したビルドプロセスへ組み込み自動化しています。

これにより、読み込むJS/CSSが最大10個以上ありましたが、半分以下に減らすことが出来ました。

 

3.JavaScriptの移動

ページの最上部に記述していたJavaScriptの読み込みを、ページの一番下へ移動しました。また、一部はasyncを追加することで非同期にしました。

これにより、ページの読み込みがブロックされる事が無くなり、ページが表示されるまで待たされる時間を短くすることが出来ました。

 注意点として、単純にページの下へ移動してしまうと、<head>タグで記述したスクリプトでエラーが発生する事があります。

このため、DOMContentLoadedでの実行するように変更するなどの対応が必要となりました。

 

4.コンテンツの圧縮

HTMLやJS/CSSの容量を低減を行うため、Apacheでmod_deflateを利用するようにしました。

gzipでコンテンツを圧縮することにより、最大75%程度の容量を削減する事が出来ました。

 

5.コンテンツの有効期限

クライアント側でキャッシュを有効活用するため、画像/JS/CSSに180日の有効期限を設定しました。

これと合わせ、コンテンツの内容を変更した場合にクライアントが再度ダウンロードを行えるよう、「?ts=xxxx」のようなパラメータを追加しています。

 

終わりに

以上の改修を行い、Google Analyticsの「サイトの速度」は9秒台から5秒前半まで改善することが出来ました。

実際には上に挙げた以外の改修も行っています。しかし効果が無かったり、逆に悪くなってしまったものもありました。このため、改修を行ったあとは必ず計測を行い、元に戻す事も必要です。

JAVAのマーカーインターフェースとアノテーション

非キャッシュの仕組み

弊社のシステムはJAVA + strutsで実装されている所が多いのですが、サイト上のページはアクセスの負荷に耐えられるように、WEBサーバーやAPPサーバーから、キャッシュされた情報を返すようにシステムを構築しています。
ただ、ページによっては毎回Actionを実行して、リアルタイムなデータを表示する必要があります。そのために、Actionが特定のマーカーインターフェースかアノテーションを実装していたら、HTTPレスポンスヘッダに

Cache-Control: no-store
Pragma: no-cache

を出力するようにしようと考えました。

疑問点

やりたい事はマーカーインターフェースでもアノテーションでも可能なのですが、実装するにはリフレクション的な事を行うので、どのやり方が一番処理が軽いのか、疑問が浮かびました。
interfaceを実装しているかどうかを調べる方法も、instanceofを使う方法とClass.isAssignableFromを使う方法があるので、それに関しても検証してみたいと思います。

やってみる

で、疑問点を解消するために、実際に実験をしてみました。

  • interface

マーカーインターフェースなので、中身は無いです。

public interface NoCacheInterface {
}
  • annotation

リフレクションするので、RetentionPolicyはRUNTIMEです。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface NoCache {
}
  • Actionクラス

マーカーインターフェースとアノテーションを実装します。
BaseActionはstrutsのActionを継承して、前/後処理(そこでHTTPレスポンスヘッダの出力も行う)等を行います。
この実験では特に意味は無いですが…。

@NoCache
public class TestAction extends BaseAction implements NoCacheInterface {
  • 検証コード

それぞれのやり方でぐるぐるループさせます。
処理の前にgcをして、後に残っているメモリ量も見てみます。

TestAction action = new TestAction();
int count = 100000000;
long start = 0;

-- インターフェース(instanceof)
System.gc();
start =System.currentTimeMillis();
for(int i=0;i<count;i++){
	if(action instanceof NoCacheInterface){
	}
}
System.out.println((System.currentTimeMillis()-start)+"msec");
System.out.println(Runtime.getRuntime().freeMemory());

-- インターフェース(isAssignableFrom)
System.gc();
start =System.currentTimeMillis();
for(int i=0;i<count;i++){
	if(NoCacheInterface.class.isAssignableFrom(action.getClass())){
	}
}
System.out.println((System.currentTimeMillis()-start)+"msec");
System.out.println(Runtime.getRuntime().freeMemory());

-- アノテーション
System.gc();
start =System.currentTimeMillis();
for(int i=0;i<count;i++){
	if(action.getClass().getAnnotation(NoCache.class)!=null){
	}
}
System.out.println((System.currentTimeMillis()-start)+"msec");
System.out.println(Runtime.getRuntime().freeMemory());

結果

以下のようになりました。

4msec
13829000
8msec
13661280
3674msec
13468800

結果は

  1. マーカーインターフェース(instanceof)
  2. マーカーインターフェース(isAssignableFrom)
  3. アノテーション

の順で、処理速度が速く使用メモリが少ないという事になりました。
なので、マーカーインターフェースで実装してinstanceofで確認することにします。
ただ、1億回もループしてこの程度ですけどね。