kdoc - ウェブアプリケーション開発メモ

  • 作成日:2012-02-23 16:27:42
  • 修正日:2015-10-27 10:18:22

ローカル環境

↑ページトップへ

PSGI/Plack

↑ページトップへ

PSGI/Plack - Perl Superglue for Web Frameworks and Web Servers

  • PSGI…Perl Web Server Gateway Interface。ウェブサーバとPerlアプリケーションをつなぐインタフェースの仕様。
  • Plack…PSGIという仕様を実装したソフトウェア。

対応しているウェブサーバと、対応しているPerlアプリケーションは以下に。

PSGIによるHello World

my $app = sub {
    my $env = shift;
    return [
        200,
        [ 'Content-Type' => 'text/plain' ],
        [ "Hello World" ],
    ];
};

クライアントのIPを表示するPSGIアプリケーション

my $app = sub {
    my $env = shift;
    return [
        200,
        [ "Content-Type", "text/plain" ],
        [ "Hello $env->{REMOTE_ADDR}" ],
    ];
};

PSGIサーバ

plackup
Plack::Server::Standalone::Prefork::Server::Starter(PSSPSS)…「Plack::Server::Standalone ベースのマルチプロセス httpd である Plack::Server::Standalone::Prefork に、完全無停止での更新機能を追加した httpd」

Starlet…シンプル
Starman…Net::Server::PreForkをベースにしたHTTPサーバ。HTTP1.1対応、UNIXソケットへのバインド。

プリフォークサーバ…予め一定数の子プロセスをforkしておき(プリ/フォーク)、並列に処理を行うタイプのサーバ(Starmanはデフォルトで5つ、Starletは10)。

「プリフォークサーバでは、コネクションの維持イコールプロセスの占有なので、HTTPのKeepAliveは無効にするのが一般的」
(Apache2.0系ではKeepAliveTimeoutは15秒、2.2.9では5秒)

Starman

↑ページトップへ

Server::Starter

↑ページトップへ

ホットデプロイとは、「再起動の時にリクエストの処理を続けながら、変更の内容を反映するための手段」。
Server::Starterはこれを可能にするcpanモジュール。

kill -HUPシグナルを受け取ると、新しいサーバプログラムを立ち上げる(古いのも立ち上がったまま)。
新しいサーバプログラムが正常に立ち上がったら、古いプログラム対しkillを実行。

Server::StarterがHUPシグナルを受け取った際、--interval の間にサーバがエラー終了しないと、サーバの立ち上げに成功したと見なして新しいサーバにディスパッチを開始してしまいます*1。よって、立ち上げに1秒以上時間がかかるサーバを運用する場合、このオプションを指定していないとエラーの検知ができない上に、接続ができない時間が発生してしまいます。

Monit

↑ページトップへ

プロセス監視。Daemontools, Supervisorに代わって。

SSH の監視設定例

PID ファイルのパスと起動・停止方法を設定する。

check process sshd with pidfile /var/run/sshd.pid
start program = "/etc/init.d/sshd start" with timeout 3 seconds
stop  program = "/etc/init.d/sshd stop"

Nginx

↑ページトップへ

ディレクトリ構成

↑ページトップへ

MacBook@yusuke % tree -L 1 -F
.
├── Makefile.PL
├── cli/……………………コマンドラインツール、cronから等。
├── config/………………「development.pl」と「production.pl」とか。
├── deploy/………………Cinnamonの設定ファイル。
├── etc/…………………「どこに入れよっかなー?って迷った感じのファイル」
├── lib/……………………「lib/MyApp/Web/Controller」
├── log/…………………
├── psgi/…………………
├── public/………………静的コンテンツ(Mojolicious標準で使われているディレクトリ構造)。
├── sandbox/…………実験スクリプト(レポジトリには入れない)。
├── script/……………?
├── t/……………………テストファイル。
└── templates/…………Mojolicious標準テンプレートファイル。

Makefile.PLの代わりにcpanfile+Carton?

$ mojo generate app MyApp ではなく、
$ mojo generate app MyApp::Web
# Web層だけではないから。

├─ cpanfile
├─ cli/
├─ config/
├─ deploy/
├─ etc/
├─ lib/
│  └─ MyApp/
│      ├─ Web.pm
│      ├─ DB.pm
│      ├─ DB
│      │  └─ Schema.pm
│      ├─ Model
│      │  └─ ModelA.pm
│      └─ Web
│          ├─ User
│          │  └─ Xxx.pm
│          ├─ Admin
│          │  └─ Xxx.pm
│          └─ Develop
│              └─ Xxx.pm
├─ log/
├─ psgi/
├─ public/
│  ├─ css/
│  ├─ favicon.ico
│  ├─ images/
│  └─ js/
├─ sandbox/…………実験スクリプト(レポジトリには入れない)。
├─ script/
├─ t/
└─ templates/
    ├─ layouts/
    │  └─ default.html.eq
    └─ root/
        └─ index.html.eq

デプロイ

↑ページトップへ

定番のCapistarano(Ruby)。軽量なCinnamon(Perl)。

Perl/モジュール管理

↑ページトップへ

plenvとcartonとcpanmはグローバルにインストール。

plenv

Perlbrewじゃない方。

# インストール可能なPerl一覧
$ plenv install -l
# インストール
$ plenv install 5.18.1
# インストール済みのPerl一覧
$ plenv versions
# グローバル設定
$ plenv global 5.18.1
# グローバル設定の確認
$ plenv global
# 現在有効になっているPerl
$ plenv version
$ which perl

# カレントディレクトリに'.perl-version'というファイルを作成し、バージョンを記述する
# ※カレントディレクトリ以下に採用される
$ plenv local 5.5.8
# 無効にする('.perl-version'を削除)
$ plenv local --unset

# .perl-version記載のバージョンのPerlで実行
$ plenv exec XXXX

# 特定Perlのアンインストール
$ plenv uninstall 5.18.1

# cpnamのインストール
$ plenv install-cpanm

# インストール済みのモジュール一覧
$ plenv list-modules

cpanm

  • cpanm -l … 足りないモジュールだけ指定パスにインストール。
  • cpanm -L … コアモジュール以外を指定パスにインストール。

Carton

cpanfile(というファイル)を元にPerlモジュールを管理。

requires 'Module::Name';
requires 'Module::Name', '1.0037'; # 1.0037以上
requires 'Module::Name', '>= 1.0037, < 2.00'; 1.0037以上でメジャーバージョンアップ前
requires 'Module::Name', '== 1.0037'; # 固定
# /path/toディレクトリに用意したcpanfileを元にモジュールインストール
# → /path/to/localにインストールされる
$ cd /path/to/myapp
$ carton install

# /path/to/localを使って起動(「 -- 」に続けてコマンド)
$ carton exec -- XXXXX

# DBD::mysqlは外部ライブラリに依存しているので、以下をしてから。
$ export PATH=/path/to/mysql/bin:$PATH

(gitからはlocal/を除外しておいた方がいいよね)

echo local/ >> .gitignore
  • 実際にインストールされたモジュールのバージョンはcarton.snapshotに記述される。
    • carton installだとcpanfileを読みにいく。
    • carton install --deploymentだとcarton.snapshotを優先して読みにいく。

/20131127a/app.psgiが動いてて、/20131128a/app.psgiなディレクトリに新しい環境作って、切り替えて、だめだったら戻す、みたいなこと。

その他

↑ページトップへ

プロセス監視
Jenkines(設定ファイルとかテストとか)?
fluentd(ログ集積)?
キャッシュ

Supervisord?
Carton(Perl)? Bundler(Ruby)?(モジュール管理?)
Chef / chef-solo?

Keep-alive

↑ページトップへ

ハートビート信号を送って生存を伝えるキープアライブ機能。

HTTP/1.1でHTTP Keep Aliveがサポートされた。
1回のTCP接続で複数のHTTPリクエストを処理可能。

1ページにたくさんの画像がある等の場合に便利(効果あり)。
コネクションを張り直す必要がない。

  • MaxKeepAliveRequests: 接続してから切断するまでに受け付けるリクエストの数
  • KeepAliveTimeout: 接続しているセッションからのリクエストが来なくなってから切断するまでの待ち時間

さくらVPS

↑ページトップへ

Mojolicious

↑ページトップへ

nginx + Starman + Mojolicious

↑ページトップへ

MacでApacheとnginxと切り替え

↑ページトップへ

Apache2は、MacPortsでインストールしたもの。

# Mac起動時に apache を自動起動させる場合
sudo launchctl load -w /Library/LaunchDaemons/org.macports.apache2.plist
# 起動を一時停止する時のコマンド
sudo launchctl unload /Library/LaunchDaemons/org.macports.apache2.plist

# Mac起動時に apache を自動起動させさない場合
sudo launchctl unload -w /Library/LaunchDaemons/org.macports.apache2.plist
# 手動で起動させる時のコマンド
sudo launchctl load -F /Library/LaunchDaemons/org.macports.apache2.plist

# Mac起動時に nginx を自動起動させる場合
sudo launchctl load -w /Library/LaunchDaemons/org.macports.nginx.plist
# 起動を一時停止する時のコマンド
sudo launchctl unload /Library/LaunchDaemons/org.macports.nginx.plist

# Mac起動時に nginx を自動起動させさない場合
sudo launchctl unload -w /Library/LaunchDaemons/org.macports.nginx.plist
# 手動で起動させる時のコマンド
sudo launchctl load -F /Library/LaunchDaemons/org.macports.nginx.plist

# 普通の起動
sodo nginx
# 普通の停止
sudo nginx -s stop

gzip設定

↑ページトップへ

AutoPatchWorkのサーバー周りのこと - 0xFFより。

gzファイルを生成して、.htaccessで以下を指定。

RewriteEngine on
# Accept-Encodingにgzipが含まれていて、
RewriteCond %{HTTP:Accept-Encoding} gzip
# リクエストされたファイル名+.gzなファイルが存在したら、
RewriteCond %{REQUEST_FILENAME}\.gz -s
# URLを.gzつきに書き換え、
RewriteRule ^(.+)$ $1.gz
# ファイルタイプをjsonとして強制する
AddEncoding x-gzip .gz

パスワード

↑ページトップへ

現在パスワードの保存方法のベストプラクティスはソルト付きハッシュ + ストレッチングということになっいるので、それに従うのが無難かと思います。

メール運用

↑ページトップへ

さくらVPSの標準CentOSは最初からpostfixサーバが動いており、試用期間が過ぎれば特別な設定をしなくてもPHP等からメール送信できます。

もろもろ

↑ページトップへ

  • フロントエンドWeb戦略室: 第1回 外部サイトに貼り付けるJavaScriptの作法―ポリシー,速度,セキュリティ,プライバシー(1)|gihyo.jp … 技術評論社
    http://gihyo.jp/dev/serial/01/front-end_web/000101

Devel::KYTProf

ワールドワイド

↑ページトップへ

URL

アップル

  • http://www.apple.com/
  • http://www.apple.com/jp/
  • http://www.apple.com/ca/
  • http://www.apple.com/ca/fr/
  • http://www.apple.com/cn/
  • http://www.apple.com/tw/

マイクロソフト

  • http://www.microsoft.com/en-us/
  • http://www.microsoft.com/ja-jp/
  • http://www.microsoft.com/en-ca/
  • http://www.microsoft.com/fr-ca/
  • 中国: http://www.microsoft.com/zh-cn/
  • 台湾: http://www.microsoft.com/zh-tw/

オラクル

  • http://www.oracle.com/ (http://www.oracle.com/us/)
  • http://www.oracle.com/jp/
  • http://www.oracle.com/ca-en/
  • http://www.oracle.com/ca-fr/
  • http://www.oracle.com/cn/
  • http://www.oracle.com/tw/

Facebook

  • https://www.facebook.com/
  • https://ja-jp.facebook.com/
  • https://fr-ca.facebook.com/
    • https://fr-ca.facebook.com/ ---> https://www.facebook.com/

ちょっと関係ないけど

↑ページトップへ

Amazon

Geo

データベース ORM

↑ページトップへ

「TengはDBIx::Skinnyの後継バージョンと捉えていただいて結構です」

  1. Schemaクラス(Schema.pm)を作成。

基本

# 通常のINSERT
my $row = $teng->insert('user', +{id => 1, name => 'nekokak'});
# last insert idだけを受け取るFAST INSERT
my $id = $teng->fast_insert('user', +{id => 2, name => 'xaicron'});
# 一気にINSERT
$teng->bulk_insert('user', [
    +{ id => 3, name => 'zigorou'},
    +{ id => 4, name => 'hidek48'},
]);

# idから取得
my $row = $teng->single('user', +{id => 1});
$row->id;   # 1
$row->name; # nekokak
# 複数カラムなら、
my $row = $teng->single('user', +{id => 1, name => 'nekokak'});
# 特定のカラムだけ取得
my $row = $teng->single('user', +{id => 1}, +{columns => [qw/id/]});

# inflateを行わないナマのデータを取得
my $row = $teng->single('user', +{id => 1});
$row->get_column('name');
# 複数カラムなら、
$row->get_columns;

# Rowオブジェクトから直接update
$row->update(+{name => 'inukaku'});
# 実際にどういう値でデータベースに登録されたかを確認したいなら、refetch
my $new_row = $row->refetch();
# 更新カラムを先に作ってから
$row->set_column(name => 'inukaku');
$row->set_column(id   => 10);
$row->update();
# あるいは、
$row->set_column( +{
  id   => 10,
  name => 'inukaku',
});
$row->update();

# Rowオブジェクトからdelete
$row->delete();

SQL文で

# 自身でSQLを
my $itr = $teng->search_by_sql('select * from user where id = ?', [qw/1/]);
# イテレータから取得されるRowオブジェクトをどのtableに紐付けるかがむずかしくなる場合、
# 第三引数にRowオブジェクトに紐付けしたいtable名を指定
my $itr = $teng->search_by_sql('SELECT * FROM user WHERE id = ?', [qw/1/], 'user');
# プレースホルダを「?」ではなく名前付けするならsearch_named
my $itr = $teng->search_named('SELECT * FROM user WHERE id = :user_id', +{user_id => 1});

イテレータ

# next
while (my $row = $itr->next) {
  $row->id;
}

# all
my @rows = $itr->all;

# Rowオブジェクトを作りたくない(コストカット)なら、
$itr->suppress_object_creation(1);
while (my $row = $itr->next) {
  $row->{id}; # ただのhashrefでありRowオブジェクトのメソッドはcallできない
}
# (Teng自体に…)
my $teng = Teng->new(suppress_object_creation => 1, ....);
# (もしくは)
$teng->suppress_object_creation(1);

search

※TengのsearchメソッドはSQL::Makerのselectメソッドのラッパー

my $itr = $teng->search('user', +{name => 'nekokak'}, +{order_by => 'id'});
my $itr = $teng->search('user', +{name => 'nekokak'}, +{limit => 1, offset => 1,});
# 以下のように受け取ることも可能
my @rows = $teng->search('user', +{name => 'nekokak'}, +{order_by => 'id'});

update / delete

返り値は、実際に更新/削除したレコード数。

$teng->update('user', +{name => 'inukaku'}, +{id => 1});
# 以下なら全レコード対象
my $update_count = $teng->update('user', +{name => 'inukaku'});

my $delete_count = $teng->delete('user', +{name => 'inukaku'});

Rowクラスの拡張

TengではProj::DB::Row::Userを定義する必要はない。
メソッドを追加したい場合に定義。

package Proj::DB::Row::User;
use strict;
use warnings;
use parent 'Teng::Row';

sub special_method { q{I'm special!} }

1;

namespaceを変更したいなら、Schemaで指定。

table {
    name 'user';
    pk   'id';
    columns qw/id name/;
    row_class 'Proj::Api::User'; # ココにRowクラスを指定する
};

inflate / deflate

データベースへ登録する時、取り出す時にルールを適用する。

table {
    name 'user';
    pk   'id';
    columns qw/id name created_on updated_on/;
    # inflate / deflateの定義は正規表現でcolumnを指定できるので
    # .+_atにマッチするカラムに以下のinflate / deflateが適用される
    # dbから取得したunixtimeをDateTimeオブジェクトにinflateする
    inflate qr/.+_at/ => sub {
        my ($col_value) = @_;
        DateTime->from_epoch($col_value);
    };
    # プログラムから渡されたDateTimeオブジェクトをunixtimeにdeflateする
    deflate qr/.+_at/ => sub {
        my ($col_value) = @_;
        $col_value->epoch;
    };
};
# 全てのテーブルに共通に追加
for my $table_name (keys %{$teng->schema->tables}) {
    my $table = $teng->schema->get_table($table_name);
    $table->add_inflator('name' => sub { warn Dumper \@_; });
    $table->add_deflator('name' => sub { warn Dumper \@_; });
}

その他もろもろ

# スキーマクラスファイルを作るために…
#! perl
use strict;
use warnings;
use DBI;
use Teng::Schema::Dumper;

my $dbh = DBI->connect('dbi:SQLite:./sample.db','','');
print Teng::Schema::Dumper->dump(
    dbh       => $dbh,
    namespace => 'Proj::DB',
), "\n";

# トランザクション
$teng->txn_begin;
my $row = $teng->insert('user', +{id => 1, name => 'nekokak'});
$teng->txn_commit;

プラグインで

# Count
my $count = $teng->count('user', '*', {type => 2})

# あれば取ってきて、なければINSERT
my $row = $teng->find_or_create('user',{name => 'lestrrat'});

# あれば更新して、なければINSERT(REPLACE INTO)
my $row = $teng->replace('user',{name => 'lestrrat'});

メモ

NOW()とかの場合は、スカラリファレンスで渡す。
※実際には、NOWでやらずに、自分で生成した現在時刻を渡した方がいい場合は多そうだけど。

$teng->insert('my_table', {
  my_name => 'NAME',
  created_at => \'NOW'
});

# timestampになってるupdated_atをなんらかの理由で更新しない場合。
$teng->update('my_table', {
  my_name => 'NAME',
  updated_at => \'updated_at'
});

以外

↑ページトップへ

ぼくのさいきょうツール :: [ FLAT ] Develop | ONO TAKEHIKO from aguije inc.
http://flat.is/develop/2014/02/ultimate/

Solarized - Ethan Schoonover(開発環境カラーリング)

モダンPerlなら、(DateTimeじゃなくて)Time::Piece。

  • FormValidator::Lite
  • HTML::FillInForm::Lite
  • Data::Validator
  • Mouse
  • SQL::Maker
  • Devel::NYTProf / Devel::KYTProf
  • Test::mysqld
  • Harriet
  • Test::More / Test::Mojo

テスト

Jenkins

もろもろ

↑ページトップへ