その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

MacでErlang/OTPをソースコードからビルドする時にNo usable OpenSSL foundって言われるんですけどって時

前提

OpenSSLがbrewで入っている.

結論

こうすれば良い (brew環境下あるある);

$ ./configure --with-ssl=$(brew --prefix openssl)

Ref. Erlang -- Building and Installing Erlang/OTP

所感

わざわざソースコードからビルドしなくても,brew使ってるんだったらbrewerlang入れたら良い.

サービスを提供するにあたって取得されたくないアカウント名が集まっているライブラリが欲しいんですけど〜みたいな時

例えば `logout` みたいなユーザ名を取得されてしまうと,ユーザ側からすると不気味に見えるし,URL設計が終了している時などに脆弱性になり得る (とは言うものの,そもそもそういった脆弱性は根本的に防ぐべきだし,URL設計を終了させてはならない).
ので,タイトルのようなことをpostしたら知見がモリモリ集まってきた.ありがとうございます.

めっちゃ便利!!!!!

YAPC::Kansai 2017 OSAKAで喋ってきました

タイトルは「Webアプリケーションのキャッシュ戦略とそのパターン 」です.

speakerdeck.com

告知で書いたように,ここ1・2年は規模感のあるWebアプリケーションを開発していて,なおかつキャッシュ周りの設計・開発・運用をモリモリやっていたので,その関連で学んだこと,感じたことをまとめて発表したという感じです.聞きに来てくださった皆さんありがとうございます.内容についてはスライドをご覧いただければご理解頂けるかと存じます.

ところでトーク中に言い忘れたこととして,「ランキングの構造を返すJSON」みたいなものはえてして大きくなりがち,かつランキングをバッチで構築している場合は或る単位時間内に変化することが少ない (あるいは無い) ので Cache-Control を付けてJSONを返してしまうと負荷が大きく下がって便利,みたいな話題もありました.しっかりした原稿を作っていないとこういううっかりがあるものですね.反省します.

Varnishについて

トーク中に「Varnishを使ってないんですすみません」という話をしましたところ,懇親会等でVarnish周りで色々なフィードバックを頂戴することが出来てたいへん有益でした.
ざっくりとまとめると;

  • 古来,Varnishの利用に際して孕んでいた問題としてHTMLをゴリッとキャッシュして配信していると「他のユーザのログイン済み画面を誤って表示してしまう」と言うものがあり,このリスクが大きかった
  • 近年ではユーザがログイン済みかどうかはJSでハンドリングすることが出来る (そしてそういう手法を採ることが多い)
    • そして「ログイン済みの時に表示するコンテンツ」はJSがAPIを叩いて取得してくることが出来る (そしてそういう手法を採ることが多い)

という感じで,つまり

  • 大枠のコンテンツ (つまりあらゆるユーザに返しても問題のないコンテンツ) を持っているHTMLについてはVarnishで返してしまう
  • ログイン済みかどうか,あるいはどのユーザか,といったクライアント側に紐づくコンテンツに関しては別口でAPI越しに返してやる

といった手法が現代では可能なので,割と安全にVarnishを活用できるのだなあという認識に落ち着いてきました.あとはVCLが便利とか.Varnish自体に関しては利用できる箇所であれば利用したいところなので,今後適所に取り入れていきたいと思います.

トークをするとこういった情報が集まってきて嬉しいですね.ありがとうございます.

YAPC::Kansai 2017 OSAKAで「Webアプリケーションのキャッシュ戦略とそのパターン」と言うタイトルで喋ります

表題のとおりです.喋ります.詳細は以下のリンクを参照ください.

http://yapcjapan.org/2017kansai/talks.html#talk-22

ここ1,2年ずっとキャッシュと密にやっていく仕事 (サービス) をやっていて,それにより給料と言う名のキャッシュを得て生活していたわけですが,ここらでそこらへんの知見らしきものをまとめ,ついでにアウトプットしようという狙いでトーク応募をしましたら採択されましたから喋ります.役に立つ話が出来ると嬉しいなあと思っています.ぜひ来てください!!!

あとdankogaiさんのライブコーディングの実況・解説という珍ロールで登壇もします.こちらも絶対面白いので来てください!!!

mcrouterはget-multiリクエストを個別のgetリクエストに分割する

mcrouterという,Facebookが作っているmemcachedの為のルータがあります.「ミクルーター」と発音するようです *1

mcrouterは多機能なルータであり,シンプルなルーティング (例えば乱択やhash basedなど) からfailoverを前提とした大規模なクラスタのルーティングまで様々なルーティングを行うことが可能です (Facebook内の数千台規模のmemcachedクラスタでも運用されているようです.参照: Facebookの数千台規模のmemcached運用について - ゆううきブログ).

さて,mcrouterの詳細や使い方の説明については他の資料に譲るとして,本記事ではmcrouterがmemcachedget-multi リクエストを各々複数の get リクエストに分解して宛先のmemcachedに送る挙動をするという話をします.

どういうことなのか

Multi-key GET Behavior · Issue #40 · facebook/mcrouter · GitHub

このissueそのままではあるんですが,検証してみましょう.

{
    "pools": {
        "sample": {
            "servers": [
                "localhost:11211"
            ]
        }
    },
    "route": {
        "type": "AllAsyncRoute",
        "children": [
            "PoolRoute|sample"
        ]
    }
}

この設定ファイルを食わせてmcrouterを起動して (e.g. mcrouter -f ./config.json -p 22122),更に宛先のmemcachedを起動します.memcachedmemcached -vvというふうに起動すると検証が楽 (Ref: memcached おすすめ起動オプションまとめ - blog.nomadscafe.jp).

しかる後に以下のようなコードを流し込んでみる.

use IO::Socket;

# mcrouter に get-multi
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 22122,
    Proto    => 'tcp',
);

my $keys = join " ", map { $_ } 1..5; # 5件multi-get
$socket->print("get $keys\r\nquit\r\n");
my @lines = $socket->getlines;
$socket->close;

さてこの時にmemcachedのログを確認してみると

<30 get 1
>30 END
<30 get 2
>30 END
<30 get 3
>30 END
<30 get 4
>30 END
<30 get 5
>30 END

という感じで,get-multiリクエストではなく個別のgetリクエストが来ていることがわかります.

一方,memcachedに直接繋いだ時は当然get-multiでリクエストが飛びます.

use IO::Socket;

# memcached に get-multi
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

my $keys = join " ", map { $_ } 1..5; # 5件multi-get
$socket->print("get $keys\r\nquit\r\n");
my @lines = $socket->getlines;
$socket->close;
<31 get 1 2 3 4 5
>31 END

つまりmcrouterはget-multiリクエストを個別のgetリクエストにバラしてから宛先memcachedに送りつけているのだ!! (正確にはget-multiに限らず,multi系のオペレーションは全部分解される)

内部的にどうなっているのか?

主に関連する部分

詳細な実装に踏み込んで説明すると大変なので端折りますが

  • multi operationなリクエストはそれぞれバラしてcontextという単位にする (get-multiだったら個別のget contextにする)
  • 1リクエストあたりのcontext群はシリアライズする
  • シリアライズしたコマンド列をソケットに流し込む

という挙動をしているようです.

つまり先程の例 (1から5のkeyをget-multiする場合) では get 1\r\nget 2\r\nget 3\r\nget 4\r\nget 5\r\n というコマンドの塊をソケット越しで送るという動きになるようです.

懸念

memcachedのconn_yieldsはどういう時に増えるのか - その手の平は尻もつかめるさ

前回の記事で書いたように,1回のソケットで大量のコマンドを発行するとしきい値を超えた場合にはconn_yieldsが発生します.conn_yieldsが発生するとそのバッチリクエストの性能が劣化するという懸念があります.
従ってmemcached client側がget-multiで送っていると思い込んでいたコマンドは,実はmcrouter側で個別のgetリクエストに変換されており,key数が多い場合はconn_yieldsが多発するというような予期せぬ事態に陥ることがあります.




さてconn_yieldsによって本当に性能劣化するのか? という検証については次回行うこととします.

memcachedのconn_yieldsはどういう時に増えるのか

memcachedconn_yieldsはどのような時に増えるのかという話です.なおmemcachedはテキストプロトコルかつデフォルトの設定で実行しているものとします (なお,この記事で重要なのは -R がデフォルト,つまり20ということです).

memcached の conn_yields について - tokuhirom's blog

1つのコネクションでコマンドを発行しまくっている場合にここに到達するようだ

と,上記記事にあるとおりなんですが,実際にmemcachedのコードを読みながら試してみたという記録です.

TL;DR

1回のソケットで大量のコマンドを発行している場合にincrementされる.

getコマンドを何度も発行してみる

get 1get 2get 3とクエリを何度も発行していくパターン.

use IO::Socket;
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

$socket->print("get $_\r\nquit\r\n") for 1..100;

my @lines = $socket->getlines;

$socket->close;

この時は conn_yields がincrementされない.

multi-getで大量のエントリを取得してみる

get 1 2 3...と1つのgetで大量のエントリをひいてくるパターン.

use IO::Socket;
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

my $keys = join ' ', map { $_ } 1..100;
$socket->print("get $keys\r\nquit\r\n");

my @lines = $socket->getlines;

$socket->close;

この時も conn_yields はincrementされない.

1回のソケットで大量のコマンドを発行してみる

get 1\r\nget 2\r\nget 3\r\n...と1回のリクエストに複数コマンドを詰めるパターン.

use IO::Socket;
my $socket = IO::Socket::INET->new(
    PeerAddr => 'localhost',
    PeerPort => 11211,
    Proto    => 'tcp',
);

my $gets = join "\r\n", map {"get $_"} 1..100;
$socket->print("$gets\r\nquit\r\n");

my @lines = $socket->getlines;

$socket->close;

この場合は conn_yields はincrementされる (memcachedのデフォルトの最大コマンド数の閾値は20なので,その場合は (100 - 20)/20 = 4 ということで conn_yields は4増える).

コードを読んでみる

基本的に memcached.c を読むとわかる.

memcached/memcached.c at 12ea2e4b50a3222412e9e1cffb1253d907c56cd5 · memcached/memcached · GitHub

注目すべきはこのwhileループconn_yields はこのループ内のこの部分でしかincrementされない.
で,どういう時に conn_yields がincrementされるかというと,

  • nreqsという変数が0を下回っていて
  • なおかつstateが conn_new_cmd の時にループが回った

という時にされる (このnreqsというのは「1つのコネクションで何個のコマンドを許容するか」という閾値memcachedコマンドの -R オプション相当).
nreqs はstateが conn_new_cmdの時にループが回るとdecrement される(実装はこの辺).

つまりざっくり言うと,stateが conn_new_cmd の時に何度もループが回ると conn_yields が増加するという事になる.
stateが conn_new_cmd になるのはどういう時かというと色々あるのだけれど,頻度が多いものとしては「コマンド実行後 (正確には結果出力後)」がある.

1コネクション中で大量のコマンドが送られてきた場合,

1. 送られてきた文字列をバッファしつつ読み込みparseする
2. コマンドを実行
3. stateがconn_new_cmdになる (nreqsのdecrement,条件を満足している時は conn_yields のincrement)
4. まだ読み込んでいない文字列がある場合は1に戻る

という挙動をする (実装はこの辺).
例として get 1\r\nget2\r\n という文字列が送られてきた時は get 1\r\n を読み込み・実行した後に get 2\r\n を読み込み,実行する.この時コマンドは2度実行されるので, nreqs-2 されている.

上記の不発だった例のように「getコマンドを何度も発行してみる」という場合はそれぞれのコマンド実行が別のコネクションになっているので,conn_new_cmd時のループは高々1度しか回らない.よってnreqsは-1しかされないので conn_yields はincrementされない.

また「multi-getで大量のエントリを取得してみる」という場合は少し特殊で,送られてきた文字列を読み込む時にバッファ内に収まるのであればそれは通常のコマンド1発のように扱われる.バッファ内に収まらない時は一回読み込んでからstateを conn_waiting に遷移し,更にそこから conn_read に遷移することで続きの文字列を読み込む (読み込み切るまでこれを繰り返す).いくら文字列が長くてもコマンド1発としてみなされるので nreqs は-1しかされない.よって conn_yields はincrementされないということになる (実装はこの辺).




認識が足りなくて,getコマンドを何度も発行してみたり,multi-getで大量のエントリを取得してみたりした時でも conn_yields がincrementされると勘違いしていて,動作検証時にハマったので調べてみました.

所感

Perlの場合,Cache::MemcachedCache::Memcached::Fastを使っていれば1つのコネクションで複数コマンドを発行するケースは無さそうなので, conn_yields のことは考えなくても良さそう?

Test::Fluent::Logger書いた

表題の通りです.Perl5の話です.

CPANにもあげてあります.

https://metacpan.org/pod/Test::Fluent::Logger

何をするモジュールかというと,useするだけでFluent::Loggerのpost及びpost_with_timeの内容をインターセプトして,本来送るべき宛先には送らずに内部のリストにそれを貯めるという動作をするモジュールです.そしてそのリストをgetしてきて中身を確認したり,またリストを空にする (clear) こともできます.
使い方についてはSYNOPSISを読んでいただければわかると思います.とくに難しいことはありません.

本来はMockみたいな名前をつけるべきだったのかもしれませんが,こんなのテスト用途以外に誰が使うんだということでこの名前になっています.
毎度毎度この手のコードを書いていて,良い加減飽きたなという動機でこのたびモジュール化した次第.久々にCPANに新しいモジュールをアップロードしたんですが,昔のあの日のまま変わっていなくて少し安心しました.
ご利用くださいませ.