概ね開発の日々


オヤジニア

 342  views 

昼間、喫茶店への道すがら見つけた鮫洲八幡神社。色々あるけどできることをやるしかないんですよね。なんてことを考えつつお参り。新旧のお客さんへのサポートの合間を縫って、サイトのオープンに向けた細かい修正を。まずは運営者のページを直す。現時点では単独スポンサーにて運営(うちだけとも言うw)。次に連絡先のページの内容も修正して、問合せフォームからメールが飛んでくるようにする。買ったテンプレートに入っていたTMform.jsを使ってみる。TemplateMonsterで販売されているBootstrap用テンプレートで使われているらしく説明ページもある。フロントエンドでちょっとした入力チェックをしてくれるが、スクリプトサイズが大きくミニファイかけてもほとんど小さくならない。そのままでは使えないので書き換えてしまおう。でもライセンスがどこにも書いてないんだよね… 他に、送信用のPHPスクリプトも付いているが、こちとらNodeJSなのでそれは使わずに、DBにPOSTしてからメール送信するよう、バックエンド側に追加の処理を組み込む。Nodemailerを使って、別のメールサーバにSMTPS経由で送信してもらおう。あれ?バージョンアップして別サーバを使うにはnodemailer-smtp-transportが必要になったらしい。少しハマった。Gmailだけモジュールなしでもいけるんだとか。うーん、世の中はここまでGoogle化してたんだね… ということで、以下はNodeJSから自前のサーバに、ダイレクトにメールを送信したい奇特な人向けのメモw

こんな感じでモジュールを追加。ついでに本体もアップデートしておく。

# npm install nodemailer-smtp-transport
# npm update nodemailer

書き方が変わったところはこんな感じ。起動時に送信用の口(transporter)を作っておく。

var nodemailer = require('nodemailer');
var smtpTransport = require('nodemailer-smtp-transport');
var transporter = nodemailer.createTransport(smtpTransport({
    host: '[メールサーバ]',
    secure: true,
    port: 465,
    tls: { rejectUnauthorized: false },
    auth: {
        user: '[認証用のユーザ]',
        pass: '[パスワード]'
    }
}));

その口を使って送信するところ。本文が「body」から「text」に変わっているので注意。「html」にするとタグ付きメールで送れるようだが、そんなつもりは毛頭ないのでスルー。

transporter.sendMail({
    from: '[送信するメールアドレス]',
    to: '[受信するメールアドレス]',
    subject: 'お問い合わせフォームより',
    text: '[前もって生成しておいたお問い合わせ内容を含むメールの本文]'
});

あとはMarkdownの書き方ページを仕上げる。さて、記事も書いたし、更新して寝ようかと思ったら、記事のタグ件数が合ってない。よくよく探すと、根深いところにバグがある。しばらく格闘するも抜本的な改善に至らず…頭が回ってないな。少し寝ておこう…

 47  views 

データベースの入れ替え後、そのままにしていたバックアップ側のサーバ。3日前に大きめのセキュリティアップデートが出たので(所謂GHOST)ついでに他も一緒に作業してしまおう。2台であれこれ試してから、残りもがさっと入れ替え。終わって夕食を挟んでCSRF対策の実装へ。一般公開に向けてこれは押さえとかないとね。でもcsurfのおかげであっさり終了。次は、気になっていたパフォーマンス向上策。フロントエンド寄りの実装で、サーバ側は軽いんだけど、見かけの動作がもたつく。こんな感じでsiegeで負荷をかけてみる。以下は、コマンドラインの例。-c 100 が同時接続数、-r 10 がリクエスト回数、--time=10S が負荷をかける時間で、最後にURL。URLを列挙したファイルを読み込ませてランダムにアクセスされることもできるようだ。

siege -c 100 -r 10 --time=10S [負荷をかけたいURL]

隣のサーバから色々と突っついて、キャッシュの有効期限を調整したり、コードを書き換えたり。ボトルネックになっている場所を探す。やっぱり、データベースのところだよなぁ。30倍ぐらい遅くなるところがある…比較のために突っついていたサーバにWordpressが入っていたので、逆から同じ負荷テストをかけてみたら、中断する間も無くコケた…大事なメールが来るかもしれないはずだったので、ちょっと焦ったがなんとか再起動。うーん、Wordpressより軽いのはわかったから、これで一旦良しとするか。最低限の機能も整ったし、来週から公開準備に入ろうか。 おっと、あとひとつ。一昨日の交流会で翻訳システムの現状について聞かれた翻訳についての調査。やはり、このあたりは Google が一番進んでいるか。有償化されたりバージョンアップで仕様が変わったりして、てっきり使えないものと思ってたが、ウェブサイト翻訳ツールを使ってあっさり設置完了。とりあえず、翻訳精度がわからないので、日本語と英語だけにしておこう。ちょうどいい「ぼた餅」の画像がなかったので、亀の形のシュークリームでもどうぞ。

 51  views 

過去に開発したブログを含むウェブアプリケーションで、対処療法に終始したデータベースのスケールアウト問題。今回のバックエンド(サーバ側)では多少の負荷がかかっても、分散データベース構成にして回避できるようにしてある。ただし、非ロックで更新を行う設計となっているため、処理落ちなどの際にデータのロールバックができない問題を含んでいた。これを擬似的に回避する実装を半年前に思いつき、外注したまま忘れかけていたものが納品されるとのことで、あがってきたソースコードを新しいバックエンドに組み込んで色々とテストしてみる ついでに、大雑把に組んだままの、フロントエンド側のエラーハンドリングを少々リファクタリング。残っていたパスワード変更の実装もやっつけておく。機能面の実装はこれで一段落?あ、別の不具合を見つけたw 少し時間がかかりそうだったので、腹ごなし(写真のモツ鍋)。戻って少し休憩、仕上げる 引き続き、バックアップ環境の整備とCSRF対策。続きは明日にしよう

 28  views 

昨日見ていたドメイン取得サービスのサイトで、Bootstrapの標準メニューがデザインとしてうまく馴染んでいた。そんなこんなで、アカウント毎のページのテンプレートを修正して、表示をBootstrap標準に近づけた。より使いやすくするために、リロードや記事の登録・編集、プロフィール変更へのリンクの位置の調整や、記事の保存時にフォームをマスクしてボタンの二度押しを防いだり。他に、バックエンドの認証セッションの延長処理、認証がタイムアウトで切れている際のエラーのハンドリングや、記事の(Markdown記法で書かれたテキストの)プレビュー、アップロード済みの記事画像の削除など、ぼちぼちと細かな実装をこなす。これで、だいぶブログっぽくなってきた

写真は昼間にふらっと立ち寄った品川神社で見つけた大黒様。お札買ったけど儲かりますかねw

 37  views 

アカウントごとに異なる JavaScriptやCSS、画像などのコンテンツを置けるように、アプリケーションサーバ側の設定を調整。個別のテンプレートを仕掛けられるようにするための前段となる実装を。 ついでに、おもしろいものを見つけたのでアップしておく。HTML5だけでできている3D表示。プラグインを使わずにここまでできる。当たり前っちゃ〜当たり前だが、今更ながら置いてみると面白い。ちなみに FirefoxF, Safari, Chrome のみ。IEとandroidはだめw

あと、記事中にHTMLを直接表示させるため Markdown 設定からHTMLのサニタイズを外した。こうしなきゃどうしようもないんだけど、広告を無効化したりスパイウェアを置くこともできる。さて、どうしたものやら…

 32  views 

引き続き、改修

開発環境は概ね Mac。中でも。Magic Mouse は歴代のマウスの中でもかなり気に入ってる。特にブラウザを使っていてマウスの表面を左や右に撫でるだけで、ページを行ったり来たりできるのは、もう病みつきだ。ただ、ウェブサイトに直接記事を書き込んでいて、指が当たってページ移動してしまい入力内容が飛んでしまうこともよくある。これを防ぐため普段は文章はテキストエディタを使って保存しながら書き、それを投稿の時にコピペしているが、普通のユーザはわざわざそこまではやらないだろう。 Bootstrap のモーダルフォームでは、フォームが表示されると背景をグレーアウトしてくれて、一見移動を抑制してくれそうなものだが、そこまではやってくれない。ExtJSなどでもこれは同じだ。しかし、各サービスは工夫を凝らしており、ユーザの気付かないところで入力内容をセーブしておいてページが戻ってきたときに再現してくれたり、編集中にページ移動を確認するダイアログを出すことで対応している。さっき、書きかけの記事をひとつ飛ばしてしまったこともあり、先にこの対応をしておこうw

$( '#dialog' ).bind( 'show.bs.modal', function() {
    $( window ).on( 'beforeunload', function() {
        return '記事が保存されていません。このまま移動しますか?';
    });
});
$( '#dialog' ).bind( 'hide.bs.modal', function() {
    $( window ).off( 'beforeunload' );
});

こんな感じのスクリプトを、ページが表示された後に $( window ).load( function() {…} 読み込んでおく。前半部分で、Bootstrapのモーダルフォームであれば、移動を確認するダイアログが表示されて、誤操作で記事が消えることもなくなる。後半部分はフォームが非表示になった時に確認ダイアログを出す処理を上書きしている。フォームが閉じると確認ダイアログは出なくなる。showとhideにイベントを割り当てられるのでは Bootstrap のおかげ。ありがたや m( )m なお、本来であれば、フォームの個々の入力欄のdefaultValueとvalueの値を比較するなどして、入力内容が変更された場合のみ確認表示をセットすべきである。しかし、元々寄り道的な対応でもあり他にも課題はまだまだ残っている。入力フォームの生成方法やバックエンドとの接続、使用していない入力コンポーネントなどへの対応も必要なので、後に調整することにして今はここまでにしておこう

次は、記事(ARTICLE)にタグ付けができるようにする。 フロントエンドにタグをスペース区切りで編集できるよう入力欄を追加。あわせて、バックエンドのスキーマを調整して登録できるようにする。 タグは一発で見たいので、別テーブル(TAB)に集約して格納しつつ、記事との関連を保存するテーブル(TAG2ARTICLE)と二段階で保存するようにしておく。所謂、正規化

+---------+     +-------------+     +-----+
|  記 事  | <--> |  記事とタグ  | <--> | タグ |
| ARTICLE | 1:n | TAG2ARTICLE | n:1 | TAG |
+---------+     +-------------+     +-----+

こうしておくと、一応SQL一発でタグのリストを引っ張り出せる

select [TAGのキー],COUNT([TAGのキー]) from TAG2ARTICLE GROUP BY [TAGのキー] ORDER BY COUNT([TAGのキー]) DESC;

ただ、バックエンドがTinyなMVCを意識して開発しているので、今の実装だと複雑なクエリーを通せず、集合関数た集合クエリはうまく扱えないんだった。うーんどうするかな…半日考えたがこれといった妙案も浮かばず。もう夜か。酒でもかっくらおうかと思ったが、風呂がてらジムに行って運動して、串カツでも食って帰ろうw

3時間ほどでオフィスに戻る。集合関数ライクの動作を実装する方法は思いつかなかったが、リフレッシュのおかげか、コードの断片をいくつか思いつく。大がかりな実装はひとまず置いといて、TAG2ARTICLE を JSON形式で取得する際に、パラメータをつけると集合関数で処理されたっぽい結果が返ってくるようにしてみた。もう少し捻ればきちんと組み込めな感じだし、SQLじゃなく配列で処理したので、データベースを差し替えても使えるだろう。これで、本来のタグ表示と検索の下準備は完了。あとはフロントエンドとバックエンドの調整。まずは、フロントエンド側でタグのリストを取得して表示するようにする。次に、タグで検索されたら(#をつけるとタグ検索)バックエンドの挙動が変わるようにしておく。あとは表示されているタグがクリックされたら、検索テキストに放り込んでバックエンドを呼び出すようにしてできあがり。後半はコードを書いたというよりは、いじってるうちに自然にできた感じ。ふふふ。もう朝か。それじゃ、今日はこのあたりで…

 24  views 

Facebookでシェアすると画像が出るが、Twitterには画像が出てない? ということで、軽いノリでTweetに画像が載るようテンプレートの修正をはじめる

以下のメタタグをヘッダに追加。ページの内容が入るようにしておく

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="[Twitterのアカウント]" />
<meta name="twitter:url" content="[ページのURL]" />
<meta name="twitter:title" content="[ページのタイトル]" />
<meta name="twitter:description" content="[ページの本文(あまり長すぎないように)]" />
<meta name="twitter:image" content="[画像のパス]" />

最近流行りらしい summary_large_image(大きな画像)にしてみた。Twitterにログイン済みのブラウザで検証ページ(Card Validator)にアクセス。Twitterのアカウントは開発者として登録されていなければいけないようだ(※ 事前にフォーラムにログインすれば開発者になれる) Card URL の入力欄に試したいページのURLを入れて「Preview Card」ボタンを押して、うまく表示されればオッケー 早速、記事からツイートボタンを押してみる。画像と本文がツイートに出てくるはずなんだが出ない。画像が大きすぎる訳でもなさそうだし、うーむ… もしかして、個人アカウントのせいかな?サイト用のアカウントを作ろうとしたがなぜか新規登録ができない。詰んだ〜と思って、しばらくして登録してみたらできた。新しいアカウントに変えメタタグを揃えて再度ツイート。でも画像は出ない。以前は登録に時間がかかったようなので、1日ほど(次の記事を投稿するまで)待ってみるか

そうそう。オウンドメディア用のTwitterアカウントは@ownd_meにした。どう使うかはこれから考えよう。アカウントを作ったついでに、ポータルのトップページにタイムラインのパーツを貼ってみた(右側もしくは下段)。アカウント用の適当な画像がないので、まずはそのあたりからだな

さて、タバコが切れた。買いに行こう 戻ってきたが気になるw

開発者サイトのドキュメント曰く、幅280px、高さ150px以上で、かつサイズは1MB以下にしろと。ドキュメントは最初によく読みましょうね<自分。サムネイル画像の出方を調整して再度ツイートすると右下に「概要を表示」リンクが現れる。さっきは無かったような(ウロ覚え)…クリックするとサムネイル画像とタイトル、本文が表示されました(拍手) でも、デフォルトで画像は表示されないみたい。twitter:cardのパラメータがsummary_large_image以外なのか、はたまたリツイートでは表示されるとか。もしかして、公式アカウントじゃないとだめなのか?うーん…

 188  views 

昨日はアマゾンからの荷物が届くので、配送待ちついでに朝まで開発

Bootstrapでレスポンシブな表示に対応したが、画像のサムネイル自体は複数サイズを作成するようにしてある サーバサイドで幅を判断してimgタグのsrcの中を切り替えると、後のトラフィックとサーバ負荷増につながるので、ブラウザ側でネイティブに対応できないか考える html5.1のドラフトにあった、pictureタグとimgタグのsrcsetを使って試行錯誤してみようか

まずは pictureタグ を使ってみる

<picture>
    <source media="(max-width: 639px)" srcset="./img/picture_w640.jpg">
    <source media="(min-width: 640px) and (max-width: 767px)" srcset="./img/picture_w768.jpg">
    <source media="(min-width: 768px) and (max-width: 991px)" srcset="./img/picture_w992.jpg">
    <source media="(min-width: 992px)" srcset="./img/picture_w1200.jpg">
    <img class="img-responsive" src="./img/picture.jpg">
</picture>

もひとつ。imgタグのsrcset を使う方法

<img class="img-responsive"
    src="./img/picture.jpg"
    sizes="100vw"
    srcset="./img/picture_w640.jpg 640w,
            ./img/picture_w768.jpg 768w,
            ./img/picture_w992.jpg 992w,
            ./img/picture_w1200.jpg 1200w"
>

Chrome でデバッグ。ブラウザ幅を変更すると切り替わってにんまり 途中で Safari に変更。ん?切り替わらない。Firefoxもだめ…iOSは動いてる?(やや適当) Webkitで実装されたのがだいぶ前なので、てっきりSafariは動くと思ったが、やや時期尚早だったかw Chromeもよく見るとキャッシュが効かない。半日の作業が無駄に… せっかくなので、Nginxのキャッシュを使ったり、NodeJSのミドルウェアを入れてみたりして、バックエンド側でのキャッシュを試す いろいろ試しながらだったせいか、一時ファイルが変なところにできてる。再現しないので寝ぼけてたかな?もう、日が高い Nginx+NodeJS(Express)のキャッシュ使用の最適解っぽいものは見つかったが、色々細かな設定も必要なので、元に戻してしばし寝る…

起きる。もう午後w お客さんのサポートや庶務をこなしつつ、デバッグがてらスマホで画像をアップしてみるも、記事間の移動が面倒w 開発順序を入れ替えて、記事下の前後の記事へのリンクを追加することに すぐにできたが、後の記事は取れるのに、前の記事がどうにも取れずにハマる… フロントエンドからバックエンドまで順に追っかけた結果 SELECT * FROM [テーブル] WHERE [キー] < [値] ORDER BY [キー] DESC でデータが取れない。 正順(ASC)だと取れるデータが逆順(DESC)だと取れない。Mronngaのサイトで少し前のバグだった。CentOS6 標準の MySQL5.1 に Mroonga のモジュールを被せ、古いバージョンだったのが原因 MySQLを5.5に入れ替えて Mroonga を最新版にしようと思ったが、すこし時間がかかりそうだし、寝ておくか

起きる。お客さんのサポートとまた庶務。それにしても、経理処理になんでこんなに時間が食われるんだ?うーん… MySQLを入れ替える。こんな感じ

データベースのバックアップ

# mysqldump -u root -p [データベース名] > [ダンプファイルのパス]

MySQL5.1の削除

# yum remove mysql*

連なって、cronie cronie-anacron crontabs munin-node postfix sysstat yum-cron あたりのパッケージが消される。後で戻そう

MySQL5.5(SCL)のインストール

# yum install -y centos-release-SCL
# yum install -y http://packages.groonga.org/centos/groonga-release-1.1.0-1.noarch.rpm
# yum makecache
# yum install -y mysql55-mysql-server
# yum install -y groonga-tokenizer-mecab

# /sbin/service mysql55-mysqld start
# chkconfig mysql55-mysqld on

つづいて、削除されたパッケージの再インストール

# yum install cronie cronie-anacron crontabs munin-node postfix sysstat yum-cron

mysql56u-common mysql56u-libs が入ったが気にしないw munin-node だけ、設定を戻しておく

# mv /etc/munin/munin-node.conf.rpmsave /etc/munin/munin-node.conf
# /etc/init.d/munin-node start

MySQLの再設定 まずはmy.cnfの変更

# vi /opt/rh/mysql55/root/etc/my.cnf

このあたりを追加しておく

[mysqld]
skip-character-set-client-handshake
character-set-server=utf8

[mysqldump]
default-character-set=utf8

[mysql]
default-character-set=utf8

ちゃんとチューニングしなくちゃな…

SCL版のMySQLだと設置ディレクトリが変わるのでパスを食わせないとコマンドが動かない /opt/rh/mysql55/enable にパス類の設定が書いてあるので、~/.bashrcに追記しておく

# vi ~/.bashrc

/opt/rh/mysql55/enable の内容

export PATH=/opt/rh/mysql55/root/usr/bin${PATH:+:${PATH}}
export LIBRARY_PATH=/opt/rh/mysql55/root/usr/lib64${LIBRARY_PATH:+:${LIBRARY_PATH}}
export LD_LIBRARY_PATH=/opt/rh/mysql55/root/usr/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
export MANPATH=/opt/rh/mysql55/root/usr/share/man:${MANPATH}
export CPATH=/opt/rh/mysql55/root/usr/include${CPATH:+:${CPATH}}
# source ~/.bashrc

やっと、MySQLの初期設定

# /opt/rh/mysql55/root/usr/bin/mysql_secure_installation

こんな感じで回答

Change the root password? [Y/n] Y # ルートアカウントのパスワードを設定
Remove anonymous users? [Y/n] Y # anonymousアカウントを削除
Disallow root login remotely? [Y/n] Y # リモートからの使用を停止
Remove test database and access to it? [Y/n] Y # テスト用データベースを削除

ここで Mroonga(ストレージ版)をインストール

# yum install -y mysql55-mroonga

先にやっておくと、うまく動かなかったが、再起動を忘れたせいかもしれない

最後にデータベースの投入

# mysql -u root -p

mysql> CREATE DATABASE [データベース名];
mysql> quit;

# mysql -u root -p [データベース名] < [ダンプファイルのパス]

写真は一昨日打ち合わせでご来社された方からの高価ないただきもの。 香りが最高♪ 蔵が目に浮かぶよう(行ったことないけどねw)

 31  views 

風邪も治って、夕方から検索機能を実装

まずは、MySQLにMroongaのプラグインを追加

インストール前にデータベースを丸ごとダンプ(バックアップ)

# mysqldump -u [MySQLのユーザ] -p [データベース名] > [ダンプファイルのパス]

次にMroongaをインストール

バックアップのダンプファイルを開いて CREATE TABLE の末尾にある、ENGINEをMroongaに修正

# vi [ダンプファイルのパス]
元:
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
先:
) ENGINE=Mroonga DEFAULT CHARSET=utf8;

一旦、データベースを再作成しダンプを戻す(リストア)

# mysql -u root -p

mysql> DROP DATABASE [データベース名];
mysql> CREATE DATABASE [データベース名];
mysql> quit

# /usr/bin/mysql -u [MySQLのユーザ] -p [データベース名] < [ダンプファイルのパス]

これで完了。

データベースのスキーマをほぼそのまま全文検索ができるようになった Mroongaの中の人たちありがとう m(__)m

このあたりでホスティング先のプロバイダからレプリケーションが動いとらんぞと連絡… 既に本稼働用の環境で動いてるんだが、物理サーバ2台にマスターとバックアップのVPSを分散してあって、冗長化してある (多少、値は張るが、サーバ監視が無いと寝たいときに寝られるので安心なんだよな) データベースもレプリケーション機能を使ってバックアップしてるが、モジュールを追加したせいなのか、データベースを作り直したせいでレプリケーションが止まったらしい 開発中のせいか手元にあるサーバの感覚で連絡し忘れてた。申し訳ない… 作業手順を連絡。復旧よろしくです ;-)

引き続き、バックエンド側の検索処理まわりを直す

具体的にはWHERE句を以下のような感じで修正

SELECT * FROM [テーブル名] WHERE [カラム名] LIKE '%[検索キーワード]%';

SELECT * FROM [テーブル名] WHERE MATCH([カラム名]) AGAINST("+[検索キーワード]" in boolean mode);

データがまだ少ないので速度検証は行わず。ブラウザのデバッガを見ながらF5攻撃してみる。体感上は特に問題なし あと、キーワードが複数の時に思ったような結果が得られないが、バックエンド側の設計を見直す必要があるので後日対応へ持ち越し

その後、ポータルとアカウント用ページのフロントエンドの検索フォームを見えるようにする 検索の負荷を考えながら、それぞれバックエンド側との通信手順を多少変えて実装する

nサムネイル画像がブラウザ幅に追従しておらず、越えてしまうことに気づく… ここは Bootstrap で対応。imgタグのclassに"img-responsive"を書けばオッケ サムネイル画像の表示に背景画像を使ったせいで画像のクリックができないのも改めて見ると不便。あちこち参考にしつつ background-size:cover と似た描画に変えておく

HTML

<div class="frame">
    <a href="[画像のリンク先]">
        <img class="img-responsive" src="[画像のパス]">
    </a>
</div>

CSS

div.frame {
    height:120px;
    line-height:120px;
    overflow:hidden;
    position:relative;
}
div.frame img {
    position:absolute;
    top:50%;left:0;
    -webkit-transform:translate(0,-50%);
    transform:translate(0,-50%);
}

途中、iPadやiPhoneで横向きに撮った画像が時計回りに90度回転して表示されるのが気になったのであわせて直す。ここも convert のパラメータに -auto-orient を追加するだけ

後は、デバッグしつつリファクタリングを少々。メール対応と今後の進め方の検討など。デバッグもそろそろ自動化しないとなと思いつつ、今日はここまで

写真は途中で作った夜食。100均の安売りおでんダネに出汁と具を少々追加。 さすがに、全部は食べてないw

 26  views 

サイト名、オーナー名、プロフィール文を変更できるようにした ついでに画像もアップできるように。テンプレートに表示するかどうかは検討中 技術的に特筆すべきことは無い 画像は本日の寝酒@コンビニ。週明けの気付け薬としてw