このエントリーをはてなブックマークに追加

はじめての Gatling

Gatling を、とある環境のベンチマークに使ってみたので、入門向けとして「はじめての Gatling」としてまとめてみました。

Gatling は、Scala でベンチマークのシナリオを書くけれど、Scala 力が低いても、かなり手軽にシナリオを記述できるところが便利ですね。

このエントリーをはてなブックマークに追加

はじめての Datadog

本番環境に Datadog を導入し始めていますが、Datadog はじめての方の向けに簡単にスライドとして作成してみました。
Datadog に関するカジュアル会があれば、そこで発表したい内容でしたが、スライドを公開しておきます。

今回は、「はじめての Datadog」ということで Datadog の入門編として作成しましたが、応用編も本番環境へ導入して運用している中で知見がたまったところで公開する予定です。

Datadog すこしクセがあるところもありますけれど、とても便利なモニタリングサービスだと思います。

このエントリーをはてなブックマークに追加

お手軽に Java プログラムを起動できる javactl の紹介

Java プログラムをコンパイルしたあと、jar なファイルを実行するためには java コマンドでいろいろな引数を設定して起動することがよくあります。

そんなとき、javactl を使うとプログラムを簡単に起動することができます。
おもな特長は、次のとおりです。

  • pip コマンドで簡単インストールすることができる
  • 起動するための設定ファイルは、YAML 形式で記述することができる

まず、インストールは、pip コマンド一発です。


$ pip install javactl

インストールすると、javactl というプログラムがインストールされます。


$ which javactl
/usr/bin/javactl
$ /usr/bin/javactl -h
Usage: javactl [options...] [args...]

Options:
--version show program's version number and exit
-h, --help show this help message and exit
--check dry-run mode
--debug debug mode

使い方は、とても簡単で設定ファイル名パスを渡すだけです。


$ javactl foo.yaml

設定ファイルの例は、こちらをみると、一目瞭然だと思います。

設定ファイルには、おもに次のような内容を記述します。

  • 対象の Java アプリケーション名 app)
  • アプリケーションの実行ユーザ名
  • 環境変数を定義したい場合は、その設定 (os.env)
  • java のヒープサイズなどのメモリ設定 (java.memory)
  • JMX がある場合、その設定 (java.jmx)
  • java の定義名 (java.env)
  • その他の Java オプション (java.option)
  • ログ(GC)ファイルの設定 (log)

Java に特化した起動パラメータをほぼすべて指定できるので Java/Scala アプリケーションをとりあえずさくっと起動したいには手軽なツールです。

僕の場合は、この javactl を使って Supervisor から Scala アプリケーションを実行するようにしています。

このエントリーをはてなブックマークに追加

Packer を使って EC2 AMI をビルドする

AWS EC2 AutoScaling するときなどには、AMI を作成しておくと、早く本番環境に投入できてとても便利です。

AMI を作成するには、packer を使うと、とても簡単にビルドすることができます。

今回は、公式の CentOS 7 の AMI をベースに、次の AMI を順番に作成してみることにしました。
Packer のサンプルテンプレートを GitHub においておきました。

  1. CentOS 7 AMI 最新版
  2. サービス共通の AMI(上の AMI をベースにして作成する)
  3. サービスのロール別の AMI(上の AMI をベースにして作成する)

CentOS 7 公式の AMI イメージ ID は、次のaws-cli コマンドで取得することが出来ます。(若干時間がかかります)

$ aws --output text ec2 describe-images \
    --owners aws-marketplace \
    --filters \
      Name=architecture,Values=x86_64\
      Name=virtualization-type,Values=hvm \
      Name=hypervisor,Values=xen \
      Name=owner-alias,Values=aws-marketplace \
      Name=root-device-name,Values=/dev/sda1 \
      Name=root-device-type,Values=ebs \
      Name=image-type,Values=machine \
      Name=state,Values=available \
      Name=description,Values="CentOS Linux 7 x86_64 HVM EBS 20150928_01" \
    --query 'Images[].ImageId'

テンプレートの変数は、すべて環境変数で渡すようになっています。

まず、CentOS 7 AMI 最新版 では、次のようなことをやっています。

  1. ルートディスク 30G、EBS 100GB にする
  2. タグを Name、ベースになった Base_AMI で設定する
  3. SELinux を無効にする(石川さんごめんなさい
  4. EPEL と必要な Ansible などをインストールする
  5. CentOS を最新にアップグレードする
  6. タイムゾーンを東京に変更する

残りのサービスごと、サービスロールごとのテンプレートは、ec2-userdata.sh というシェルスクリプトでそれぞれ必要な処理(Ansible によるプロビジョニングなど)を実行するようにします。

さらに今回は CircleCI を使っているので、CircleCI 上で特定のブランチのときのみ AMI を順番にビルドするように設定しました。
cirlce.yml に、例えば次のように記述をするとできます。

...
deployment:
  packer:
    branch: develop-packer
    commands:
      - ci-deploy-packer.sh

この例では、develop-packer ブランチに push されたとき packer を使って AMI を作成する ci-deploy-packer.sh スクリプトを実行するという意味になります。

実際のプロジェクトでは、CentOS 7 共通 1 つ、サービス共通 1 つ、ロールごと 2 つ、を直列で作成して、約 1 時間ほどかかりました。

このエントリーをはてなブックマークに追加

GitHub 上などにある Gem をインストールする方法

GitHub 上にあるとあるフォークした Gem(Rubygems にはリリースされていない)をインストールすることになりました。

普通なら、Gemfile を書いて bundle 経由でインストールするのが一般的だと思いますが、1つの Gem だったので、何か他の方法がないかなと調査したところ、specific_install というコマンドを使うとできました!

使い方は、とても簡単です。

$ sudo gem install --no-document specific_install
$ sudo specific_install -l https://github.com/<リポジトリ>.git -b <ブランチ名>

という感じでインストールすることができます!
今回、td-agent の flunetd plugin で特別に1つカスタマイズされたものがあって、無事 GitHub 上にあるとあるフォークした fluentd plugin をインストールすることができました。
td-agent の場合は、gem コマンドの代わりに td-agent-gem を使えば良いので、とても便利でした。

このエントリーをはてなブックマークに追加

Ansible 2.0 にアップグレードした

Ansible 2.0 がようやくリリースされましたね。さっそく、既存の Playbook をアップグレードしてみました。公式の移行ガイドは、こちらにあります。

まず、実行してみると、次のような警告が表示されました。

[DEPRECATION WARNING]: Instead of sudo/sudo_user, use become/become_user and make sure become_method is 'sudo' (default).
This feature will be removed in a future release. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.

今まで、実行ユーザを指定するとき sudo と sudo_user を使っていましたが、1.9 の頃にはすでに become / become_user になっていたので、sudo / sudo_user から、例えば centos というユーザで実行したいとき、次のように置き換える必要があります。

become: yes
become_user: centos

Ansible 2.0 では、数多くのモジュールが追加されていますが、この中で AWS ELB に SSL 証明書を追加するためのモジュール iam_cert を手動で組み込んでいましたが、これも自分のリポジトリからは削除することができてさっぱりしました。

AWS S3 モジュールをけっこう使っていますが、この中で mode=list というバケットの一覧表示するモードが追加されたのは便利です。ローカルファイルをまとめて同期するための mode=sync もほしいところです。余裕があったらフィードバックしてみたいと思います。

1.9 では、lookup(‘file’) に不具合があって実質使えない状態になっていましたが、2.0 になって無事直って使えるようになりました。

公式移行ガイドにあったエスケープ問題に遭遇しました。僕の場合は、Supervisor の設定ファイルを生成しているところで問題がありました。
具体的には、次のように Supervisor の設定ファイルを生成していました。

# supervisor.yml
- name: Configure program
  ini_file: dest={{ supervisor.config.dir }}/{{ program_name }}.ini
            section=program:{{ program_name }}
            option={{ item.option }}
            value={{ item.value }}
            owner=root
            group=supervisor
            mode=0644
  notify:
    - reload supervisord
  with_items:
    - { option: "command", value: "\"{{ program_command }}\"" }

使う側は、次のような感じです。

- name: install supervisor configuration - foo
  include: supervisor.yml
  vars:
    program_command: "foo bar"

1.9 のときは、この状態だと、program_command = foo bar という内容で出力されていましたが、2.0 になると program_command = “foo bar” という内容で出力されて Supervisor が動作しませんでした。
次のように書き換えました。

# supervisor.yml
- name: Configure program
  ini_file: dest={{ supervisor.config.dir }}/{{ program_name }}.ini
            section=program:{{ program_name }}
            option={{ item.option }}
            value={{ item.value }}
            owner=root
            group=supervisor
            mode=0644
  notify:
    - reload supervisord
  with_items:
    - { option: "command", value: "{{ program_command }}" }

使う側も次のように書き換えました。

- name: install supervisor configuration - foo
  include: supervisor.yml
  vars:
    program_command: foo "bar"

これで、program_command = foo “bar” と出力されて、Supervisor が無事動かすことができました。エスケープまわりは、2.0 になると要注意なところの1つです。

あと、ある playbook から相対パスで他の playbook を include していて template を include 側で指定している場合、include 元からの相対パスを指定しないといけなくなりました。

例えば、次のような Datadog agent の設定ファイルを生成している task conf.yml があります。

# roles/datadog/tasks/conf.yml
- name: copy agent configuration - {{ name }}
  template: src={{ template }}.yaml.j2 dest=/etc/dd-agent/conf.d/{{ name }}.yaml mode=0640 owner=dd-agent group=root
  notify:
    - restart datadog-agent
  tags: datadog

これを別の playbook から、次のように読み込んでいます。

# roles/foo/tasks/main.yml
- name: install datadog-agent - postfix
  include: ../datadog/tasks/conf.yml
  vars:
    name: postfix
    template: postfix

1.9 のときは、このときは、roles/datadog/templates/postfix.yaml.j2 が conf.yml で参照されますが、2.0 では正しく参照されませんでした。次のように書き換える必要があります。(この変更が一番個人的には大きかったです)

# roles/foo/tasks/main.yml
- name: install datadog-agent - postfix
  include: ../datadog/tasks/conf.yml
  vars:
    name: postfix
    template: ../datadog/templates/postfix

あと、うれしい点としては、今まで name に次のように変数で書いた場合、出力内容に変数名のまま出力されていましたが、2.0 になって変数は正しく展開されて表示されるようになりました。

- name: set javactl_heap by {{ ansible_ec2_instance_type }} - {{ app_name }}

最後に human_log.py という Ansible の出力結果を見やすくするプラグインを愛用していますが、2.0 になってプラグイン API の仕様が変更されて動作しなくなりました。human_log.py は、実際にはいくつか不具合が直っているこちらのものを使っています。

human_log.py を使うと、次のように出力が見やすくなるので、おすすめのプラグインです。

TASK: [command echo "TEST"] ***************************************************
changed: [127.0.0.1] => {"changed": true, "cmd": ["echo", "TEST"], "delta": "0:00:00.067706", "end": "2016-01-19 17:50:37.486635", "rc": 0, "start": "2016-01-19 17:50:37.418929", "stderr": "", "stdout": "TEST", "warnings": []}

cmd: echo TEST

start: 2016-01-25 07:50:37.418929

end: 2016-01-25 07:50:37.486635

delta: 0:00:00.067706

stdout: TEST

Ansible 2.0 プラグインの変更点はドキュメントで見つからなかったので、ソースコードを参照してみました。
human_log.py を Ansible 2.0 に対応したバージョンは、こちらにおいておきました。

human_log.py は、次のようにインストールします。CentOS 7 で Ansible 1.9 の RPM をインストールした上で、pip install -U ansible している環境上が前提になります。

$ wget -N \
    https://raw.githubusercontent.com/n0ts/ansible-human_log/master/human_log.py \
    -P /usr/share/ansible_plugins/callback_plugins/
$ sudo sed -i 's/#library        = \/usr\/share\/my_modules\//library =        \/usr\/share\/ansible\/modules/g' /etc/ansible/ansible.cfg

一点問題があって、yum で http 経由の場合、なぜか “found available, installed or updated” エラーが発生するので ignore_errors を追加しています。
次のような感じのとき、パッケージは正しくエラーになります。

- name: install packages
  yum: name="https://packages.exameple.com/{{ item }}.el7.x86_64.rpm" state=present
  with_items:
    - foo-1.1.1-1
    - bar-1.2.1-1
  ignore_errors: yes

こんな感じで無事 Ansible 1.9 から、2.0 へアップグレードすることができました。

参考

このエントリーをはてなブックマークに追加

Rocketeer を投入したお話

PHP という技術制約がある案件で、Rocketeer という一言で言ってしまうと Capistrano の PHP 版になります。
Slack とも連携できていい感じです。

Rocketeer のおもな特長は、次のとおりです。

  • GIT/SVN リポジトリに対応
  • SSH の場合、SSH キーパスフレーズにも対応
  • 任意のデプロイタイミングで、特定の処理を実行することができる

今回は、次のような感じでデプロイするのに使いました。

  • 複数のサーバに SSH 経由でログインする
  • Private Git リポジトリを ssh 経由(SSH キーパスフレーズ付き) clone する
  • デプロイ前後で Slack の専用チャンネルに通知する

実際に使ったところ、いくつか問題があったので、本家にフィードバックはしましたが、今日現在になってもマージされていません。

1. 複数のサーバがデプロイ対象のとき、正しいリビジョン番号でロールバックできない(PR #591

2. Rocketeer の実行ディレクトリがデプロイ対象の git リポジトリでないとき、正しく git のリビジョンを取得できない(PR #592

その他、不具合のではないですが、次の PR もしました。

1. デプロイ対象のユーザが分かるように Notify プラグインに –user の追加(PR #593

2. 各デプロイタスクの前に before / after フックイベントを追加(PR #594

この他、実際に運用を初めて見て1つ次のような問題が発覚しました。

1. 同じサーバ上で、同時に複数のプロジェクトをデプロイする場合、$HOME/.deploy.json ファイルを作って Rocketeer はデプロイ対象サーバを管理しているのですが、なんだか混ざってしまい正しい対象サーバへデプロイすることができない

この問題はさすがに致命的でした。おそらく、そもそも同じサーバ上で同時に複数のプロジェクトをデプロイすることができないことは考えていなかったのではないかと思われます。

この件は、PR をしようかなと思ったのですが、先の PR がすっかり無視されてしまう状態だったので、自分のもの方で修正しました。修正内容としては、デプロイスクリプト上の実行ディレクトリに .deploy.json ファイルを置くように develop ブランチで簡易改良しました。

ということで、この改善を行った結果、デプロイ自体は問題なくすることができました。

頑張って PR したのですが、どうも積極的には開発にはもうあまり取り組んでいない様子なので、Rocketeers の選択はもしかしたら失敗したかもしれませんが、どうしても PHP だけで簡潔したいという場合には、実際には問題なくデプロイできているので大丈夫だと思います。

Rocketeers の設定ファイルは、けっこうめんどくさいので、テンプレートを作ってみました。
このテンプレートを使えば簡単に Rocketeers を使うことができると思います。
テンプレートを clone したあと、僕オリジナルのカスタマイズが入った Rocketeers をインストールします。


$ composer install

設定ファイルは、.rocketeer ディレクトリの中にあります。簡単ですが設定ファイルの説明は、次のとおりです。

  • config.php: デプロイ先のサーバを設定します
  • connections: デプロイ環境ごとの設定ファイルです、テンプレートではローカルホスト環境用の設定ファイルが入っています
  • hooks.php: デプロイの前後のフックの設定ファイルです、デプロイ前後に何らかの処理を追加した場合にはここに設定します
  • paths.php: デプロイ時に利用する各種パスです、基本的には変更する必要はありません
  • plugins: プラグインの設定ファイルです、テンプレートでは Slack プラグインの設定ファイルが入っています
  • remote.php: デプロイ先のディレクトリの設定ファイルです、ファイルのパーミッションなどを変更するときに設定します
  • scm.php: デプロイリポジトリの設定ファイルです、リポジトリの設定をします
  • stages.php: デプロイステージの設定ファイルです
  • strategies: この設定ファイルはよく分かっていません
  • starateies.php: デプロイタスクごとに何を使用するのか設定するファイルです、デプロイ時の動作を変更したいときに設定します

デプロイするコマンドは、次のとおりです。

$ ./vendor/bin/rocketeer deploy --on=local --user=< デプロイユーザ名> -vv

–on はデプロイ環境、–user はデプロイユーザ名(Slack にもこのユーザ名として通知されます)、-vv はデバッグオプション、になります。
–pretend を指定するといわゆる DRY RUN モードになります。

ロールバックするには、次のようになります。

$ ./vendor/bin/rocketeer rollback --on=production --user=< デプロイユーザ名>

このように Capistrano と同じ感じで利用できると思います。

デプロイしたときのログは、.rocketeer/logs/ ディレクトリ以下に出力されます。

Rocketeer ですが、現在はあまりメンテナンスされていない様子ですが、さくっと Capistrano のようなデプロイスクリプトを作りたいときには、便利なツールの1つだと思います。

このエントリーをはてなブックマークに追加

Amazon Web Services 実践入門を読んだ

遅くまきながら、大人の事情で献本していただけなかった(笑)「Amazon Web Services 実践入門」を買って読みました。

タイトルのとおり、まさに AWS を実際に業務で使う上で必須の本になります。

内容的には、アカウント開設から、EC2、Route53、VPC、S3、RDS、ELB、CloudWatch、IAM、といったウェブサービスを AWS 上で展開するにあたっての必須となるサービスについて、ウェブコンソール上での操作方法、コマンドラインでの操作方法、それぞれまとまっており、とても分かりやすい内容でした。出力画面数も、大変数が多く、解説もとても分かりやすい内容でした。

普段から AWS を使っている人も、きっと改めての再確認やもしかしたら知らないこと・忘れていることを再確認できる本になると思います。
ちなみに僕は、この本を読んで 次のことを改めて知りました。

  • CloudWatch で EC2 t2 シリーズの CPU クレジット状況を取得できることが分かった(というか忘れていました)ので、さっそく設定しました!
  • EC2 は、stop / start すると、物理的には違うサーバが起動する

おそらく、この時期ちょうどウェブコンソールが日本語化されて大変だっただろうと推測されます。執筆者の皆様、大変お疲れ様でした。今年、入門の次の続編が出ることを期待しています!!!

AWS の進化は、ご存じのとおりとても早いですが、この本の内容は今らでも買っておいても損はないと思います。

Amazon Web Services実践入門 (WEB+DB PRESS plus)
舘岡 守 今井 智明 永淵 恭子 間瀬 哲也 三浦 悟 柳瀬 任章
技術評論社
売り上げランキング: 7,067
このエントリーをはてなブックマークに追加

ある PHP 案件の振り返り

2015 年、僕がメインで担当したとある PHP 案件の振り返りを行ってみようと思います。おもに技術面から、設計前に想定したこと、実際に導入してうまくいったこと・いかなかったことを振り返ってみたいと思います。

技術的な環境は、次のとおりです。

  • インフラ環境: オンプレミス(だいだい6台くらい、このときのためにほぼハードウェアを新規に調達しました)
  • OS: CentOS 7.0
  • 言語: PHP 5.6
  • フレームワーク: FluelPHP 1.7.x
  • データベース: Postgresql 9.3
  • ミドルウェア
    • ロードバランサー冗長化: Keepalived(新規)
    • ウェブサーバ: Apache から Nginx に変更、PHP は FPM
    • キャッシュ: Redis(新規)
    • 検索: Elasticsearch(新規)
    • ログ: Fluentd(新規)
    • 監視: Nagios & Cacti & Munin(従来と同じですが、新規にセットアップしています)

案件内容的には、とある既存の Java だったサイトを PHP にてフルリニューアルするというものです。

初回の設計時の構成は、次のとおりです。僕の担当は、全体のアーキテクチャ設計と実装とインフラ構築と切り替え時の運用、そしてすべての引き継ぎです。

  • 納期がかなりタイトだったため、データベースは既存の構成のままとしました(テーブル構成・SQL チューニングなどは、フルリニューアル後にお任せすることにしました)
  • サイトのデザインは、そのままの形で移行しますが、あまりにも不要な機能名は移行しない(デザインは、PC、スマフォ、フィーチャーフォン版の3種類ありました)
  • リニューアル後、すぐに引き継ぐため、なるべく新しいミドルウェアは導入しない(あまり技術的な進化はしないですが、業務なので仕方がない面が多いのも事実です)

サーバ構成を図にすると、次のような感じになります。

構成図

まず、インフラ側から、各コンポーネントについて説明します。

OS

  • オンプレのため、OS をインストールが必要ですが、オンプレのホスティング先に設定内容をまとめて依頼することにしました(ネットワーク構成や IPMI になりますが、コストは多少かかりますが個人的にはかなり信頼できるホストティング先でこの面はとても安心できました)
  • ミドルウェア以上の導入は、すべてこちらの管轄として、責任分解点を明確にしました

ロードバランサー

  • オンプレなので、いつもなら Linux のカーネルの変更(LVS ハッシュサイズ変更、TCP WAIT_TIME 変更)+ LVS (DSR) + Keepalived の構成をとりかったですが、引き継ぎ先のエンジニアのスキルセットとそれほどの規模ではなかったので、カーネルの変更は行いませんでした
  • Keepalived + Nginx の Active / Standby 構成としました
  • Keepalived は、VIP のみをもつ設定として、ローカルの Nginx へリクエストを行い、 Nginx をロードバランサーとして各ウェブサーバへプロキシする構成としました
  • L7 Nginx にしましたが、ページの静的ファイルなども、すべてウェブサーバへリクエストを振り分けました(※ロードバランサーにアプリケーションプログラムをデプロイするか、Nginx の静的ファイルキャッシュを使う選択肢も当時は考えましたが、静的ファイルのリクエストはそれほど多くなかったのでキャッシュは不要と判断しました)
  • なので、当然 DSR ではなく、NAT になります

ウェブ

  • ウェブサイトのページを表示するサーバです
  • 一般的な Nginx + PHP FPM の構成としました
  • ページを表示するために必要なデータは、すべてウェブサーバごとに Redis へキャッシュする方針としました(かなり当時の SQL が重かったので、別に PHP でクローラーとして実装してキャッシュしました)
  • ウェブサーバの Redis は、各ウェブサーバごとにキャッシュとしてデータをもっているため、レプリケーションは行っていません)
  • 今後の緊急サーバ増強にあわせて、AWS EC2 のハイブリッド構成も視野に入れていたので、awscli は最初から導入しておきました
  • PHP は、remi にある PHP 5.6 をそのまま導入しました
  • この他に、php-mecabphp_qr を PHP 5.6 で動くようにパッチをあてて使うことにしました

検索

  • 今まで SQL like 的な感じだったので、思い切って Elasticsearch を導入しました
  • 定期的にデータベースから Elasticsearch へデータを入れるプログラムを cron で動作させるようにしました

キャッシュ

  • ウェブサーバごとの Redis とは別に共通に必要なデータを Redis を、別のキャッシュサーバとして設けました
  • キャッシュ1の Redis をマスターとして、各ウェブサーバに別の Redis でスレーブとしました(ページを表示するために共通のキャッシュを取得するためにウェブサーバのローカルのスレーブ Redis から取得する構成としました)

データベース

  • 一般的な Postgresql 9.3 のマスター x スレーブ x 1 構成になります
  • リプレイス後、スレーブを 1 台追加、Keepalived を使って HA 構成としました

監視まわり

  • Sensu & Grafana & Kibana の構成で考えましたが、引き継ぎ先のエンジニアのスキルセットを考慮して見送りました
  • Munin は、別の非エンジニアチームの方が使っていたため、そのまま引き継いで導入しました
  • Mackrel を無料の範囲で導入しました
  • Cacti は、引き継ぎ先のエンジニアのスキルセットを考えて導入しましたが、個人的にはかなり久しぶりだったのでかなり設定に手間取りました

その他のツール

  • 構成管理ツールは、Ansible にしました(本当は Itamae にしたかったですが、Ruby だったので厳しかったです・・・)、Nagios の設定ファイルも自動生成したりしていました
  • Serverspec も導入したかったのですが、Ruby になってしまうため、こちらも引き継ぎ先のエンジニアのスキルセットを考慮して見送りました
  • 案件管理は Backlog、コード管理も Backlog の git、チャットは Slack(※もちろん有料!)、共通系は Qiita Team、を導入していただきました
  • コードのデプロイは、PHP 縛りということで Capistrano ではなく、同等の Rocketeer を使いました、このツールいろいろと罠があったので、別の機会にブログにまとめておきたいと思います

と、こんな感じです。

インフラのオンプレサーバは、サーバ選定からすべて僕一人で行いました。(※ラッキングと OS のインストールは、前述のとおりすべてホスティング先にお任せしました。)

サーバの選定にあたっては、台数が増えると運用コストが増えるため、できるかぎり予算の範囲でスペックの高いサーバとしていますが、次のようなスペックにしました。NIC は、安定の Intel 製にしました。

  • ロードバランサー: CPU 8 コア、メモリ 16GB、ディスク SAS 300GB RAID 1
  • ウェブ: CPU 32 コア、メモリ 64GB、ディスク SAS 300GB x 4、RAID10
  • キャッシュ: CPU 32 コア、メモリ 64GB、ディスク SSD 120GB x 4、RAID10
  • データベース: ioDrive サーバを導入できる見込みもありましたが、コストの問題、そこまでデータベースネックではない(SQL をちゃんとチューニングすれば問題ないはず)様子だったので、既存のサーバをそのまま転用しました、ただしディスク 4 台 SAS 300GB x 2 RAID1 + Spare 構成だったので RAID10 構成に統一しました

次に PHP の部分です、PHP の実装は、引き継ぎ先のエンジニアにも最初から担当していただきました。

  • フレームワークは、CodeIgniter か Laravel かどうか迷いましたが、けっこうシンプルな FuelPHP にしました(どうも現在 FuelPHP は、作者の人が病気?のため、かなり開発が停滞してしまっているようなので、もしかしたら選定に失敗したかもしれません)
  • FuelPHP は、基本的に MySQL での用途となっていたので、Model 部分を頑張って Postgresql に対応しました(今回の案件は画像のデータはそのまま Postgresql に格納されていて、その部分の Model 対応が一番大変でした)
  • テストも同梱されていることが魅力的で、僕の実装ではほぼ PHPunit によるユニットテストを書きましたが、他のエンジニアの方までテストを書く体制がとれなかったです
  • なので、非エンジニアが行うブラックボックステスト QA にみの品質管理体制となっていました(エンジニア側での動作確認は、すべてのブラウザを F5 リロードになっていました)
  • Redis へキャッシュするプログラムは、バッチ処理のように記述してデータベースの変更にあわせてほぼリアルタイムに更新できるようしました(そのため、FuelPHP の tasks + Supervisord の組み合わせにしました)

本番リリース前には、wrk を使って、それぞれの UserAgent で簡易ベンチマークを行いました。そのといは結果的にギリギリのパフォーマンスでした。

リリース後、いくつか問題が発生しました。。。

まずロードバランサー側で iptables の ip_conntack 設定ミス問題が発生しました。これ完全に僕の設定ミスだったので、すぐに設定を変更しました。Nagios での監視も漏れてもれていたことが原因でした。やはり、Serverspec を導入しておけよかったと思います。

Ansible 的には、次の設定を追加しました。

- sysctl: name="{{ item.name }}" value="{{ item.value }}" sysctl_file=/etc/sysctl.d/10-lb.conf
  with_items:
  - { name: net.nf_conntrack_max, value: 2097152 }

次に、それほどアクセスが多くないページの部分的な、キャッシュを使っていない部分でデータベースのボトルネックが発生しました。すぐにすべてキャッシュに入れる構成に変更して再リリースしました。納期的な問題から、あとで対応しようと考えていたのですが、やはりかなり甘い考えだったようです。

さらに PHP でキャッシュデータを生成していたクローラーデーモンがメモリリークして、OOM Killer を抑制していたため、サーバに SSH できない問題が発生しました・・・。調べてみると、どうも FuelPHP の Model まわりで発生しているようで、かなり根本解決が難しかったため、一定時間おきに PHP クローラーデーモンの内部で実際にキャッシングしている処理を別の PHP で実行する方式に変更しました。
クローラーデーモンは、Supervisor 経由で FuelPHP の task を、次のような設定に変更しました。次の設定では、FuelPHP tasks の crawler::run_fork を実行して、foo_cache という crawler に定義されている関数を別 PHP で 10 秒おきに実行するという意味になります。


[program:crawler]
command= /opt/remi/php56/root/usr/bin/php oil refine crawler:run_fork foo_cache --wait=10

そして、キャッシュの方のパフォーマンス問題がピーク時間帯やピークを少し越えたときに発生してしまいました。原因とすると、キャッシュ構造性の設計が起因していました。具体的には、ページのリクエストおきに ZSET なキーを取得して、そのキーをもとにすべての HASH を取得してキャッシュデータとデータベースから取得したデータを作成していましたが、この構成だとパフォーマンス的な限界があったようです。
この修正はかなり時間がかなりそうだったので、取り急ぎ PHP から Redis へアクセスする部分を Redis_db から phpredis に変更しましたが、大きくパフォーマンスは改善しませんでした。

この改善は、キャッシュデータを構造を大幅に見直して、PHP Serialize したデータのみにアクセスする構成にしましたが、結局時間がかかってしまい、引き継ぎもあってこの変更は受け入れられませんでした。。。

結局時間がかかったこともあって、ロードバランサー側にすべての動的なページを 10 分間キャッシュするように設定変更することになかったようです。パフォーマンス問題があったとはいえ、個人的にはちょっとありえない施策だったと思います。

こうしていくつかの問題があったのですが、引き継ぎもすべて完了して、この案件は僕としては終了しました。個人的な反省は、次のとおりです。

  • PHP を常時起動しているクローラーとして実装しない方がよい: やはり Apache の MaxRequestsPerChild や PHP FPM の pm.max_requests にみるように PHP は一定量処理したあとは再起動するのが鉄板だったようです、Supervisor を使っていたので、わざとデーモンプログラムを終了するようにすれば一定時間おきに起動することができました
  • キャッシュ構造の設計ミス: 今回二段階のキャッシュの構造でパフォーマンス問題がありました、二段階にした理由はキャッシュデータ量を減らすためそうしたのですが、キャッシュする量が対象増えても一段階に最初からするべきでした

あわせていくつか課題が残りました。

  • PHP ユニットテスト: 僕が実装した範囲はほぼテストコードを書いていましたが、その他のところはまったくテストコードがない状態となってしまいました、今回の案件の場合すべてのテストを書く必要はないと思いますが、最低限 Model と Controller の部分はテストコードを書いておけば QA するときの工数がかなり下がるため、総合的にみると工数は減るはずです、ヒアリングしてみるとテストコードを書いた経験がないため難しいと言っておられましたが、誰も最初は初めてなのでテストコードを書いてほしかったと思います
  • インフラの運用: オンプレなので、日々の運用で PHP や Nginx やセキュリティパッチをあてていく運用が必要となってきます、必要なものはすべて Qita Team にまとめておきましたが、PHP のバージョンが 3 つくらい古い様子なので、ちゃんと引き継ぎできているのか心配です

ということで、貴重な体験を振り返ってみました。
2016 年も始まっているので、引き続きウェブサービスを開発していきたいと思います。

このエントリーをはてなブックマークに追加

Homebrew cask の仕様がすこし変わった話

OS X 使いの皆さん定番の homebrew-cask ですが、おそらくクリスマス頃に仕様が少し変わったようです。

詳細のチケットは、こちらですが、要約すると、次のような感じになります。

  • 今まで ~/Applications へはすべてリンクだったが、移動するようになった
  • アプリケーションディレクトリは、すべて ~/Applications ではなく、/Applications になる
  • :target はしばらく動作するが、そのうち link に変更になる
  • Caskroom は、デフォルトで /opt/homebrew-cask/Caskroom となる
  • 上記のような理由で Caskroom 以下のバージョン番号のディレクトリは存在しなくなる

これはとても嬉しい変更です、例えば Google Chrome を homebrew-cask 経由でインストールした場合、今までだと 1Password が symlink のため動作しなかった問題があったので、これを根本的に解決できることになります。今までは、個人的にはまだ boxen を使っているので自動的に移動させるようなマニフェストを書いていましたが、これをしなくても済みます。

また、こちらのチケットにあるとおり、tap コマンドを使ったもののみに変更するようです。今までは、brew-cask というパッケージが必要でこれを更新するようになっていましたが、これから brew update コマンドのみで homebrew + homebrew-cask どちらもまとめて更新できるようになります。これはうれいしですね。

新しいシステムに切り替えるには、次のコマンドを実行するだけです。


$ brew uninstall --force brew-cask; brew update

簡単ですね、少し既存のもので試してみましたが、例えば google-chome をみると、次のようになっていますね。


$ brew cask info google-chrome
google-chrome: latest
Google Chrome
https://www.google.com/chrome/
/opt/boxen/homebrew-cask/Caskroom/google-chrome/latest (208 files, 184M)
https://github.com/caskroom/homebrew-cask/blob/master/Casks/google-chrome.rb
==> Contents
Google Chrome.app (app)
==> Caveats
The Mac App Store version of 1Password won't work with a Homebrew-Cask-linked Google Chrome. To bypass this limitation, you need to either:

+ Move Google Chrome to your /Applications directory (the app itself, not a symlink).
+ Install 1Password from outside the Mac App Store (licenses should transfer automatically, but you should contact AgileBits about it).

すべて同じようになっているのかなと思い、他のアプリでも確認してみましたが、なっていないようですね。


$ brew cask info flux
flux: 36-5
f.lux
https://justgetflux.com/
/opt/boxen/homebrew-cask/Caskroom/flux/36-5 (105 files, 2.7M)
https://github.com/caskroom/homebrew-cask/blob/master/Casks/flux.rb
==> Contents
Flux.app (app)

おそらく、順次変更しているのか、いつか内部的にまとめて切り替えるのかもしれません。

 

homebrew-cask を使っている人は、この機会に新しいアップデートに対応してみてはいかがでしょうか?