続 カッコの付け方

AWSを始めとしたクラウドコンピューティング全般と、唯一神emacsにおける()の付け方についてだらだら書きます

jsonの戻り値とか、複雑なオブジェクトからスマートにデータを抽出[JMESPath 最強説]

「パイセン、JSONで帰ってくる複雑な構造のオブジェクトをいい感じにするやつしらないっすか?」
「ああ、複雑な構造ね。しらん、専用の関数用意するとか?」
「やっぱ、そっすよねー」
全国津々浦々でこんな会話が繰り広げられてると思います。私も2回ぐらいこのやり取りをしました。この長年の問題に終止符、JMESPath最強じゃね?

JMESPathとは

以前、一回書いてます iga-ninja.hatenablog.com

この時私は勘違いしておりました、JMESPathはJSONだけを捌くライブラリではなく、JSONから素直にコンバート出来る各言語のオブジェクト を捌くことが出来ます。pythonで言うなら list と dictで出来てるものなら JMESPathでクエリ出来ます。# setとかは知りません。

似たようなのに jq コマンドがありますが、JMESPathのほうがいいです、多分。jq 詳しく知らないので、機能・性能面では評価出来ないですが、どう考えても動作環境の広さから言えばこっちが上です。能書きは最後の方に書きます。

JMESPathでこんなことができる!

前回も書きましたが

JMESPath Tutorial — JMESPath

ここ見てもらえばだいたい分かります。しかも試せます。そして私も勘違いしてましたが、このXPathぽい表記が JSON文字列ではなく、各言語のオブジェクト(JSONに変換出来る)に対して通用します。READMEの抜粋ですがこれは、 pythonのdictに対して問い合わせしています。

>>> import jmespath
>>> path = jmespath.search('foo.bar', {'foo': {'bar': 'baz'}})
'baz'

チュートリアルは、ナナメ読みせず、きちんと読んだほうがいいです。

もっと難しい問い合わせ

もっと複雑な問い合わせについて、ちょっと解説します。
http://jmespath.org/examples.html#working-with-nested-data

ここ見てください。awsの ec2_describe_instances の戻りっぽいJSONですが

{
  "reservations": [
    {
      "instances": [
        {"type": "small",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "large",
         "state": {"name": "stopped"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }, {
      "instances": [
        {"type": "medium",
         "state": {"name": "terminated"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "xlarge",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["DB"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }
  ]
}

reservations[].instances[].[tags[?Key=='Name'].Values[] | [0], type, state.name]

[
  [
    "Web",
    "small",
    "running"
  ],
  [
    "Web",
    "large",
    "stopped"
  ],
  [
    "Web",
    "medium",
    "terminated"
  ],
  [
    "DB",
    "xlarge",
    "running"
  ]
]

が帰ってきます。最後の [tags[?Key==`Name`].Values[] | [0], type, state.name] が特にヤヴァイ。select name, type, state from .. みたいに、欲しい項目を複数抜き出してます。プロジェクションってやつです。昔、情報処理とかで勉強した 射影 ですな。 ちょっと気おつけて欲しいのは このサンプルでは ?Key=='Name' と シングルクオートでNameを囲っているが、pythonとかで使うときは

?Key==`Name`

バッククオートで区切ること

pythonから使ってみる

import jmespath

dt = {
  "reservations": [
    {
      "instances": [
        {"type": "small",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "large",
         "state": {"name": "stopped"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }, {
      "instances": [
        {"type": "medium",
         "state": {"name": "terminated"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "xlarge",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["DB"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }
  ]
}


jmespath.search(
    'reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name]',
    dt
)

>>[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 ['DB', 'xlarge', 'running']]

pythonでのサンプルの解説

まず、

reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name]

について解説します。(後でアレンジも試します)

最初の部分

reservations[].instances[]

は特に問題無いと思います。実行したらこんな感じ

In [33]: jmespath.search('reservations[].instances[]', dt)
Out[33]:
[{'state': {'name': 'running'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'small'},
 {'state': {'name': 'stopped'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'large'},
 {'state': {'name': 'terminated'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'medium'},
 {'state': {'name': 'running'},
  'tags': [{'Key': 'Name', 'Values': ['DB']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'xlarge'}]

次の構文ですが、最初にこのクエリのやりたいことは、 instances配下にある、

  • tagsのNameタグのValue(配列なので Key=Nameのもの)
  • type
  • stateのname(dict なので、pythonで書くと state['name']の値)

だけを抜き出したいということです。チュートリアルを終わった段階で、いきなりこのクエリを書ける人はまず、いないと思います。
問題は、複数の要素を抜き出したいが、すべての要素がフラットに並んでいないという点です。フラットに並んでいるとは、わかりやすい例だとこんな感じ

{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
  ]
}

こういう例だと、そもそもJMESPath要らなくね? という話ですが、 (people 以外にもう一個ゴミ要素があるとかなら意味ある) people[*].[first,last] とでもすれば複数のキーの値を抜くことは出来ます。

では、まず失敗例から。tagsの下の Key=name のうちの Values (配列) に着目し、これを狙い打ちます。

In [39]: jmespath.search('reservations[].instances[].tags[?Key==`Name`].Values[]', dt)
Out[39]: [['Web'], ['Web'], ['Web'], ['DB']]

これだと tags.Key='Name'のValuesは引けますが、 reservations[].instances[].tags まで掘り下げているので、どうやっても typeやstateは出てきません。

ちなみに.Valuesのケツの[] は flattenです。無いとこうなります。

In [40]: jmespath.search('reservations[].instances[].tags[?Key==`Name`].Values', dt)
Out[40]: [[['Web']], [['Web']], [['Web']], [['DB']]]

じゃあ、どうすればいいか。reservations[].instances[].tags まで掘り下げないで reservations[].instances[...] でマルチセレクト処理すれば良いということです! お手本の
[tags[?Key=='Name'].Values[] | [0], type, state.name] に着目、 これは

  • tags は tags[?Key==`Name`].Values[] | [0]
  • type は 素直に type
  • state は state.name

抜いています。更に解説を進めますと。
tags[?Key==`Name`].Values[] | [0] のパイプよりも前はもう説明いらないとおもいますが、なんでわざわざ | [0] としているかに注目。Valuesはlist型なので、 ['Web','01'] みたいな値が帰ってくることもありえます、その場合先頭を抜くという意味になります。(これはサンプルなので、参考程度に) あとの type, stateは解説不要ですね!

ちょっとアレンジ

In [46]: jmespath.search('reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name ]', dt)
Out[46]:
[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 ['DB', 'xlarge', 'running']]

これでも十分なんですが、どうせなら dict の listがかえってくるようにしたいです。どうやればよいのか、、って簡単ですよね!今までの解説を読んでたら、linstance[]の下からマルチセレクトしているだけなので、ここで{}で囲って適当にkeyをつければ終わりです。

In [48]: jmespath.search('reservations[].instances[].{_name: tags[?Key==`Name`].Values[] | [0], _type: type, _state: state.name }', dt)
Out[48]:
[{'_name': 'Web', '_state': 'running', '_type': 'small'},
 {'_name': 'Web', '_state': 'stopped', '_type': 'large'},
 {'_name': 'Web', '_state': 'terminated', '_type': 'medium'},
 {'_name': 'DB', '_state': 'running', '_type': 'xlarge'}]

このサンプルの落とし穴

instances[] からの マルチセレクトであるため、 Nameタグが存在しない場合、除外する という動きになっていません。試しにデータの一部の tag.Key Nameを NNameとかにいじってみてください。多分こうなるはずです。

In [54]: dt2
Out[54]:
{'reservations': [{'instances': [{'state': {'name': 'running'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'small'},
    {'state': {'name': 'stopped'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'large'}]},
  {'instances': [{'state': {'name': 'terminated'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'medium'},
    {'state': {'name': 'running'},
     'tags': [{'Key': 'NName', 'Values': ['DB']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'xlarge'}]}]}

In [55]: jmespath.search('reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name ]', dt2)Out[55]:
[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 [None, 'xlarge', 'running']]

最後がNone で抜けてきてます。これを避けるには、更にパイプを使って listの最初の要素がNoneで無いものを抽出するようにするか、これぐらいならpython側でやっちゃうかの2択かなと思います。ココらへんを選べるのがプログラム言語から使える利点ですね。

能書き

対応言語

github.com

python, ruby, php, js, go, lua LWの主要言語ほぼ全部やないか! 言語オタクもここまで来たら凄いわ! しかも jp というコマンドまであるし、goでね。去年の記事では rubyは対応してなかったけど、対応してる。

あとは c/c++, C#, java, swift とかの heavy系
Rust, Elixir とかの 変態関数型言語

ここまで来たら言語オタクから言語変態紳士へクラスチェンジ。

動作環境の優位性

AWSを使っている人なら、少なくとも pythonのJMESPathは絶対に入っているといえる。なぜなら aws-cliが依存しているから。また、Rubist曰くもAWS-SDK2が依存しているらしく、AWSに携わっている人なら、まずJMESPathは入ってる、はず。

ただ、非AWSな人にとっても、各言語でネイティブ対応できていて、バイナリ依存とかも無いので、インストール自体はさくっと簡単に出来る、はず。

なんでこんなにAWSなの?と思ったけど、作者がAWSの中の人?(オーガには入ってる)なので、AWSとは切っても切れない。けどAWS関係なしにこのライブラリはイケてる。

jqとの比較

jq のほうが歴史は古いと思いますが、JMESPathは各言語におそらく言語ネイティブ(俗にゆう pure-javaみたいな)で対応している点が優れていると思います。

あと、作者が一人であるという点も、私はプラスに考えています。なぜならこのJMESPathの仕様自体は、彼がこの世で一番知っているのだから。もちろんバグもどこかにあるでしょうが、JMESPathの仕様に対する実装レベルが(どの言語でも)一定であると想像します。

まとめ & 上達のコツ

  • AWS人ならJMESPath 最高、嫌でも入ってるし!
  • プログラマなら jq より jp のほうが明るい未来(と思う)
  • 学習は、とにかく叩いて試してみる。遅くてもチュートリアルは完全に理解するほうが、結果お得
  • 正規表現と同じ、1手ですべてを解決しようとしない。ちょっとずつ書き足す
  • パイプは素直にパイプ、わからなければパイプがなんとかしてくれる!
  • 今回触れなかったが、UDFも作れるらしい!

個人的な感想ですが、今更 jq を使う理由が見つからないです。過去の遺産(資産)とか関係なしのはなしですよ、もちろん。

GCE Autoscaling in 2016夏

Google Cloud Platform Japan 公式ブログ: GCE ユーザーに高可用性を提供する Regional Managed Instance Groups がベータに 最近 Regional Managed Instance Groups という機能がβになりました。Regional とはなんぞや?ということですが、

この機能のもとでマルチゾーンの設定を行うと、Compute Engine が自動的に VM インスタンスを同一リージョン内の 3 つのゾーンに均等に分散させます。こうしておけば、1 つのゾーンの機能停止といった稀な障害が発生した場合でも、残る 2 つのゾーンに配置された VM インスタンスはサービスの提供を続けることができます。

ということです。逆に言うと今まではゾーン毎にManaged Instance Group (長いので MIGと略)を作る必要があったのですが、本機能でだいぶ楽になります。この機能とAutoscalingについて書きます。

f:id:iga-ninja:20160807111047j:plain

Autoscaling

GCEのAutoscalingは、AWSのそれとはかなり異なります。他のパブリッククラウドの知識は理解の妨げになるので、真っさらな頭でやりましょう。

ASの構造

  1. インスタンステンプレート
  2. インスタンスグループ
  3. (Cloud Monitoring)
  4. AutoScaler

この順番で成り立っている感じです。(Cloud MonitoringはCustom Metricsを使う場合)AWSの場合は

  1. AutoScaling Group launch config
  2. AutoScaling Group
  3. CloudWatch

です。GCEのAutoScalerはそれ自身が閾値基準によるScale-out/in判定を行っている感じです。一個ずつ軽く説明

インスタンステンプレート

AWS launch config 相当です。ようは Scale-out時に立ち上がってくるインスタンスの設定 (Network, Tag, Image, MetaData等) を指定しておく物です。インスタンステンプレートでは リージョンは指定しません。

また、Image (OSイメージ)ですが、これはかなり楽できます。まず、AWSと異なり、Imageは全リージョン共通ですので、1イメージを全世界に撒く必要すらありません。さらにImageには family という属性があり、これを一致させてImage作成すると、 familyでイメージを指定した場合、そのfamilyの中の常に最新版のImageを指す 事ができます。
つまり、AWSだと、AMI作成してから Launch Config のイメージ差し替えが必須でしたが、その1手が全く不要になります。というより、API的にImageだけ差し替えとか出来ないので、これを使わないとかなり面倒な話になります。

なお、family で Imageを指定する方法は、残念ながら Developers Consoleでは存在しないようです。gcloud 必須です。下記の --image-family を指定します。

gcloud compute instance-templates create  |  Cloud SDK  |  Google Cloud Platform

インスタンスグループ

タイトルに出てきた Regional Managed Instance Groupsの話です。インスタンスグループは、インスタンステンプレートを参照します。また AotoScalerも参照?しているはずです。Developers Consoleからみるとこんな感じです。

f:id:iga-ninja:20160807111156p:plain

今回の変更で マルチゾーンが可能になりました。GCEのゾーンはAWSと異なり、2msのペナが無いので、使わない理由がありません。

f:id:iga-ninja:20160807111221p:plain

ヘルスチェックについて

自分も誤解していたので、ヘルスチェックについて。まずAWS識者の為の一言ですが
ELBベースのヘルスチェックは、GCEには存在しません。またヘルスチェックを行うのはLBではありません。

GCEのヘルスチェックはそれ単体で独立しています。HTTPの場合は、ローカルリンクアドレス (169.254,169.254) から飛んで来ます。 よって、ヘルスチェックを使い回すことが出来ます。

AutoScaler

驚くほど少ないパラメータしか指定出来ません。まず現状では出来ないこと

gcloud compute instance-groups managed set-autoscaling  |  Cloud SDK  |  Google Cloud Platform

  • スケール Out/In のルールを分離出来ない
  • スケール Inしないという戦略はとれない
  • 閾値超過したとき、何台追加するかを指定できない (step)

出来ることは

  • インスタンス最大/最小台数指定
  • CoolDown期間の指定 (AS発動間隔)
  • CPUベースのAS発動
  • カスタムメトリックスベースの発動
  • LB利用率ベースの..

次に、それらをGCEはどうさばいているか。

  • スケール Out/In のターゲットとして CPU 60% とだけ指定する
  • CPU 60% を超えた場合、上昇速度に応じて Setpを決める ( 緩やかな上昇なら1台、急激に上昇したら一気に5台とか多分そんな実装っぽい)
  • CPU 60% を下回ったら即座に スケール Inとか、そんなにバカじゃない。ある程度余裕をもって勝手にスケールIn

一言でいうと こまけぇこたぁいいんだよ!! いい感じにやっとくから

デプロイのやり方

Image差し替えの方法です。

新規Image作成

Image作成は結構面倒です。GCEは動いているDisk -> Imageが出来ないので、

  1. Snapshot
  2. Snapshot -> Disk作成
  3. Disk -> Image作成

という結構な面倒なことになります。この時、前述の Family を指定していれば、インスタンステンプレートの差し替え不要。

デプロイ前の AutoScaling Stop

他所でも言及されていますが、 Stopは出来ますが、再開は出来ません。set-autoscaling で再設定が必要です。

まとめ

  • GCEのAutoScalingは細かくユーザーが指定出来ないが、動かしてみたところかなり賢い。正に こまけぇこたぁいいんだよ!!
  • Regional Managed Instance GroupsでZone毎にいちいち作らなくても良くなった。
  • Image の Familyは使うべき。でもgcloudでのみ指定可能。

今回のアップデートでかなり使いやすくなりました。また、AutoScaling自体、かなり大きな変更が入っているはずなので、他所の古い記事とは整合しない部分が多数あります。あとは、AWSのようにLifeCycle-Hookなどが現状存在せす、Shutdown-ScriptというOS外で指定できる機能があります(SLAは無い、走る保証は無い)。細かい色々不足している機能はありますが、ユーザーがいじれる部分を極限まで減らし、本当に必要なものだけを高いレベルで実現出来ているのは、さすがGoogleだと思います。

nginx + php-fpm の組み合わせのErrorログ出力を理解する

ngix + php-fpm の組み合わせで特に Errorログについて整理しときます。

初心者向け補足

プール向け設定ファイル = /etc/php-fpm.d/www.conf
全体の設定ファイル = /etc/php-fpm.conf

php-fpmのログまとめ

error.log

php-fpm 全体で出す Errorログです。これは global ディレクティブでしか設定出来ません。 このファイルは、デフォルトでは各プールの標準エラーなどを出力されることはありません。各プールのエラーを出力したいならば catch_workers_output = yes を、プールの設定ファイルに記載します。 ただし、このパラーメータには注釈があります。

; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Note: on highloaded environement, this can cause some delay in the page
; process time (several ms).
; Default Value: no

パフォーマンスに若干影響するということなので、気になる人はデフォルトの noが良いでしょう。

このファイルはphp-fpm マスタープロセスがつかみます。ので、logrotate時にシグナルを打つ必要があります。

$pool.access.log

プール毎 に設定できる access.logです。 このファイルはphp-fpm ワーカープロセスがつかみます。ので、logrotate時にシグナルを打つ必要があります。

xxx-slow.log

プール毎 に設定できる slow-logです。 php-fpm ワーカープロセスが出しているはずですが、ファイルはつかんでいません。

www-error.log

rpmデフォルトの設定を参考に書きます。プール毎 に設定できるのですが、これは php-fpm が出力しているのではなく、言語としての php が出力しています。 プール毎に設定できるので、これを使うのがベストプラクティスだと思います。

ファイルの出力は、エラーが発生したタイミングで発生します。また、php-fpmが出力していないので、出力するユーザーはワーカプロセスの実行ユーザー = プール向け設定ファイルで設定するユーザー&グループとなります。例えば nginx + php-fpm の組み合わせ centosなどの場合、 /var/log/php-fpm の所有権が apache となっているため、 www.confで 実行ユーザーをnginxなどとしていると ファイル出力出来ません。

nginx側のログ

ワーカープロセスの標準エラーは、実は nginxのerror.logにそのまま出力されます。しかし、日付等が記載されず、そのままなので、その点不便です。

おまけ php-fpm のログ出力パーミッションを変更したい

/var/log/php-fpm/xxx.log のうち、php-fpm自身が出力するものはすべて 600で吐き出され、しかも rootがオーナーです。これを設定ファイルでかえることは残念ながら出来ないようです。パッチは出ています。

https://github.com/sandyskies/php-filemode-patch

JSON中にDot(.)が含まれたキー名を変換する

最近なんでもかんでもJSONですが、MongoDB/ElasticsearchにJSONを突っ込もうとして問題にぶつかりました。具体的には表題の通りで、 キーにDot(.)を含むものが上手く動かない という点です。現状は キーにDotを使うな、もしもデータファイルに入っていたらコンバートしろ ということなので、コンバートフィルタを書きました。

どうして困るのか?

{
  "id": "hogehoge",
  "members": [
    "hisatoshi",
    "toshihisa"
  ],
  "detail": [
    {
      "name": "toshihisa"
    },
    {
      "name": "imaoka",
      "org": {
        "addr.name": "hogehoge",
        "tel.num": "fugafuga"
      }
    }
  ]
}

こういうJSONの場合、MongoDBやElasticsearchに突っ込んだ後、検索する場合は addr.name というキーは detail[1].org.addr.name と表現します。この場合 キーに含まれるDotが、階層を表すDotと競合してしまい、まともに動かないという現象が表れます。

例がよくなかった、途中で配列入っているの気にしないで。要は Object in Object な JSON

  • MongoDBの場合 Dotが含まれていようが、JSONインポートが出来ます。が、検索が出来ないので意味なし。

  • Elasticsearchの場合 インポートの段階で明確に蹴られます。

JSONキー置換フィルタの構想

最初は、正規表現の置換で良いかと思いましたが。JSONの中に JSON-textを含むような JSON-in-JSONのような構成をとられるとムダに破壊するかもと思い止めました。が、よく考えたら JSONのテキスト要素として JSONを表現する場合は \ でエスケープするので、正規表現だけでもいけますね、多分。。。

JSON吸って、pythonのオブジェクトにして、JSONで出す

多分正規表現のみでも行けそうと今、書いていて感じてますが、上記の方針で実装しました。 要は泥臭く実装するってことです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import json


def conv_key(obj):
    if isinstance(obj, dict):
        for org_key in obj.keys():
            new_key = org_key
            if '.' in org_key:
                new_key = org_key.translate({ord("."): ord("_")})
                obj[new_key] = obj.pop(org_key)

            conv_key(obj[new_key])

    elif isinstance(obj, list):
        for val in obj:
            conv_key(val)
    else:
        pass


def main():
    for line in sys.stdin:
        my_json = json.loads(line)
        conv_key(my_json)
        print json.dumps(my_json)


if __name__ == '__main__':
    main()

使い方

./conv_json_invalid_keys.py < hoge.json

実装の説明

  • 再起です。むちゃくちゃ深い JSONだとスタック足らないかもしれません。(そんなJSONで変換が必要とかどうかしている!)
  • JSON->Pythonの対応として、受けうる型は list , dict, 普通の値の3種類のはず
  • list は dictの listかもしれないので、そのままもう一回解析

気づき

  • CSVのように使うJSONは、1行1JSONで書く必要がある。醜いからといってjqで整形してしまうとインポート出来ない
  • type(obj) とかやって比較しようとしていたけど、そんなの時代遅れらしい、 isinstance 使え!
  • tr相当の処理 string.translateunicodeかstrかで違うらしい、この実装は unicode向け
  • json vs simplejson はほとんど変わらんらしいが、 enc/dec がそれぞれ対照的に速い・遅いらしい

GCE HTTP(S) 負荷分散と Cloud CDNを使う

すでに何度が触れていますが、GCE HTTP(S) ロードバランサー が更にアップデートされました。同じく、 HTTP(S) ロードバランサー限定で Cloud CDN というサービスも有効となりました。

GCEでHTTPS負荷分散 SSL Termination - 続 カッコの付け方

GCP リージョン跨ぎロードバランサーの破壊力 - 続 カッコの付け方

HTTP(S) ロードバランサー追加機能

以前の記事で

と書きましたがこれは解消?されているようです。この場合、エフェメラルではなく、静的なIPを1つ取得して、 80にも443にも指定すればOKです。これでよくある(AWS ELBのように) 1つのELBで 80/443 を両方通す設定が可能です。

f:id:iga-ninja:20160626102416p:plain

また、この変更?により、http/httpsの違いによる x-forwarded-for の差は無くなりました(私の記憶が確かであれば)。かならず
[HTTP(S)バランサのIP] -> [多分SSLを解くナニカ] -> [自分でたてたinstance]
となります。この[SSLを解くナニカ]ですが、毎度アクセスするたびにIPアドレスが変わります。これから推測するに、かなりの数のインスタンスSSL解きをやっているのだと思います。イメージはこんな感じ。

f:id:iga-ninja:20160626115810p:plain

ネットワークロードバランサはDSRであるため、技術的に暖気不要なことは説明がつきます。
対して HTTP(S)はリバースプロキシ型となったため、HTTP/HTTPSいずれもターミネートする存在が必要です。ここかショボイと詰まってしまいますが、圧倒的な物量でカバーしていると推測します。

Cloud CDN

Cloud CDNとは、HTTP(S) ロードバランサー専用のCDN機能です。が、これは Google Cloud Storage (GCS)単体でも実現出来る、CDN機能をGCEにも応用したようなものらしいです。なので、

  • 静的コンテンツ(ccs,js,image)はGCSから配信(キャッシュのメタデータもちゃんとセットしている)
  • 動的コンテンツのみGCE + LBで配信

しているような健全なサイトであれば、正直微妙な機能です。GCS使ってれば、静的コンテンツは自動的にCDN配信ですから。

GCE側の実装

GCE側の設定としては超簡単です。

f:id:iga-ninja:20160626105256p:plain

これで終了

キャッシュの有効期限とか

設定できません。オリジンがはき出すヘッダを厳守します。逆に言うと CloudFrontのように、オリジンヘッダを無視して勝手にキャッシュ保持期間を設定したり出来ません。

サーバ側の設定

問題はこれからです。下記はドキュメントの抜粋
https://cloud.google.com/cdn/docs/caching#cacheability

  • 下記条件を満たしていること
    • backend service が caching有効になってること、(上記のGCE側設定)
    • GET だけ
    • HTTPレスポンスコードは 200, 203, 300, 301, 302, 307, 410 のどれか
    • レスポンスヘッダに Cache-Control: public
    • レスポンスヘッダに Cache-Control: s-maxage, Cache-Control: max-ageExpires
    • まともな Date ヘッダ 未来の時間とかダメ
    • content-Length か、chunkなら Transfer-Encoding
  • 下記条件に当てはまるなら、キャッシュしない
    • Set-Cookie
    • データ部分が 4 MB超え
    • VaryAccept, Accept-Encoding, Origin 以外
    • Cache-Control:no-store とか no-cache とか private
    • リクエストヘッダの時点で Cache-Control: no-store?

これらを満たす動的コンテンツってある?
ですがとりあえずやってみます。簡単なので一気に書くと apacheの設定で無理やり Cache-Controlヘッダを付けてやりました。

Header set Cache-Control "public, max-age=600"

apache再起動でOK。ものがphpだろうがなんだろうが、 Cache-Controlを付けます。検証用なので超雑。

動作確認

curl -i で試します。私の試したところ、エッジサーバ毎にAgeを個別に持っているように思います。ですので、数回叩かないとAgeが帰ってこないかもしれません。

$ curl -i http://xxx.yyyy.xxx.zzz/index.html
HTTP/1.1 200 OK
Date: Sun, 26 Jun 2016 02:15:35 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Sat, 25 Jun 2016 20:51:47 GMT
ETag: "21549-9-5362074a6e070"
Accept-Ranges: bytes
Content-Length: 9
Content-Type: text/html; charset=UTF-8
Via: 1.1 google
Cache-Control: public, max-age=600

hello

$ curl -i http://xxx.yyyy.xxx.zzz/index.html
HTTP/1.1 200 OK
Date: Sun, 26 Jun 2016 02:15:33 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Sat, 25 Jun 2016 20:51:47 GMT
ETag: "21549-9-5362074a6e070"
Accept-Ranges: bytes
Content-Length: 9
Content-Type: text/html; charset=UTF-8
Via: 1.1 google
Age: 5           <---- Ageきたー
Cache-Control: public, max-age=600

hello

invalidation

CDNといえば invalidationですが、Cloud CDNの場合はどうやるのでしょうか? GCSでは、GCS上のファイルを削除する・更新すると、CDN側のキャッシュも消えるらしい(あくまでも伝聞)のですが、GCEというかサーバではそんなこと出来ません。ので、ちゃんと invalidationの機能はあります。

f:id:iga-ninja:20160626114650p:plain

パスに http://とか打つと

パスの先頭は「/」にしてください。「*」が使用できるのは、末尾の「/」の後のみです。また、パスには「?」と「#」を含めることはできません。

と教えてくれます。ワイルドカードもいけるんだ、ふーん。

まとめ

GCEのSubNetwork対応による、変更点とAWSとの違い

昨年の12月頃らしいですが、GCEでもSubnetがサポートされました。最初、これを知った時 なんて無駄な機能をつけたんだ!Googleのパワーをこんなしょうもないことに使うな と思いましたが、調べてみるとGoogle流の考慮は入っていました。
ひとまず、このエントリに引っかかった人は、最後まで読んでください。tl;dr とかでは表せませんが、無理やり概要をまとめると、 GCEもsubnetという太古のダサい技術をサポートしてしまったが、他のパブリック・クラウドとは一味ちがうから安心して! となります。

f:id:iga-ninja:20160625192154j:plain

今までのGCE:Network

GCPドキュメント上では、Legacy mode と書いていますが、こちらのほうが よっぽど先鋭的 です。簡単にまとめると

  • ローカルIPアドレスの CIDRだけ指定する
  • そのCIDRは全リージョンにまたがる

たったこれだけです。AWSで言い換えると

  • 全リージョンをまたぐVPCが1つ
  • VPCのなかにsubnetは存在しない。言い換えるならVPC全体が1つのsubnet

これでほぼすべてを賄っていました。これで十分なんです。

Subnet対応でできるようになったこと

一応メリットはあります

  • ローカルIPアドレスを手動固定できる
  • SubnetのCIDR単位でFW ルールを指定できる (ただし AWSのような ルートテーブル - サブネットのひも付けはない)

正直、どうでもいい。。

Legacy Network におけるローカルIPアドレスは、すべてDHCPで、ユーザーで指定出来ませんでした。嘘でした、今確認したらLegacyでもできるようになってました。できると嬉しい!と思うひともいるかもしれませんが、GCEはAWSと異なり、起動時に決めるホスト名で内部DNSが自然に登録されるため、ローカルIP同士をIPアドレスで叩く必要はほぼ皆無です。AWSのような、IPアドレスベースの使いにくいFQDNではなく、ホスト名のみで、/etc/hostsとか登録無しでOKです。

次に SubnetのCIDR単位でFW ルールを指定できる点ですが、これもAWSとは考え方が全く違います。手っ取り早く説明するため、AWSの Public-Subnet / Private-subnet で説明します。
どこかのブログのエントリでもありましたが、このGCE:subnet をつかったとしても Public-subnet / Private-subnet は実現出来ません、が、GCEは AWS流の考え方ではなく、別の方法でこれを実現しています。私の感覚では、GCEの実現方法の方がよりスマート です。

AWSの考え方

旧来のネットワークに近い考え方
Public-Subnet: グローバルIPアドレスをもち、インターネットから直接アクセスできる subnet
Private-Subnet: ローカルIPアドレスしか持たず、インターネットから直接アクセスできない subnet

GCEの考え方

インスタンスタグで、FWのみならず、ルートテーブルも管理すればいい (下記名称は私の造語)
Internet-faced-Instance: グローバルIPアドレスをもち、インターネットから直接アクセスできる instance
Local-only-Instance: グローバルIPアドレスをもたず、インターネットから直接アクセスできない instance

双方の違いとメリット

つまり、ネットワーク経路に関することは subnet単位でやるのがAWS流。 subnetなんて元々存在しないので、インスタンスタグでインスタンス単位管理するのがGCE流。 この考え方は Legacy でも Subnetでも変わっていません

また、VPNを張った場合ですが、この場合はSubnetで切れたほうが(全リージョンにアクセス可能な状態がNGならば)メリットはあります。ただし、必須ではないですね、そこもインスタンスタグで制御すればいいので、所詮気分の問題でどうでもいいです。

新しいのGCE:subnet

厳密には subnet対応にも二種類存在します
1. autoモード
2. customモード

autoモード

自動設定するウィザードではありません customとは全然違います。

  • 各リージョンに1つのSbunetが自動生成
  • Subnetの追加・削除は一切できない
  • 既存SubnetのCIDRの変更できない
  • (多分) 新リージョンが増えたら勝手に新しいCIDRが追加される

f:id:iga-ninja:20160625191119p:plain

サブネットは存在こそしますが、ユーザーはメンテナンス・オペレーション出来ません。 これにより、おそらくですが、新リージョンがリリースされると、勝手にSubnetは追加されていくと思います。

customモード

完全に Subnetをユーザーがコントロール出来ます。つまり

  • 各リージョンに Subnetを1つずつ作るか否かはユーザー次第
  • 1リージョンに Sbunetが2つとかもユーザー次第
  • CIDRも自由自在、ユーザー次第
  • (多分) 新リージョンが増えても勝手に新しいCIDRは作られない

f:id:iga-ninja:20160625191209p:plain

どうしてもSubnetを弄りたい人は custom一択です。

customモードの説明

customの説明を詳しく書きます。

CIDRの考え方

ドキュメントの絵を見るのが早いですが、1つのネットワークの中に 10.1.0.0/16 と 192.168.0.0/16 が混在出来ます。

Using Subnetworks  |  Compute Engine Documentation  |  Google Cloud Platform

AWSの場合、Subnetの前にVPCが存在し、VPCのCIDRの範囲内にSubnetを指定する必要があります。(この成約があるので、最初にネットワーク設計をしないと、手戻りが発生してしまいます。)
しかしGCEには VPCに相当するものはNetworkで、Network自体はCIDRを持ちません。よってこんなことが出来ます。

各Subnet間の通信は?

AWSのNetworkACLのように、Subnetに対してのみ効果を発揮するACLは存在しませんAWS的に言うなれば、SGしか存在しません。
各Subnet間の通信も、FW Ruleで、IPアドレス単位での指定に過ぎません。また、デフォルトでは各Subnet間での疎通は出来ません。同Subnet内でも同じく、デフォルトでは別マシンと疎通出来ません
FW ルールとしては下記のようなものを設定すれば、内部通信は全疎通となります。

f:id:iga-ninja:20160625191310p:plain

f:id:iga-ninja:20160625191319p:plain

Subnet間は疎通ルールを足していないが、インスタンスタグで疎通させている場合はどっちが適応?

  • Subnet A: 192.168.1.0/24
  • Subnet B: 192.168.2.0/24

Subnetレベルで疎通はNGとしているが、各サブネットにインスタンスを立てて、そこに疎通させるためのタグをつけたらどうなるのか?試してみました。

f:id:iga-ninja:20160625191344p:plain

ここで作成した internal タグを、互いのインスタンスにつけて、pingを打ちます。  結果は予想通り疎通OKとなります。デフォルトが暗示Deny、明示Denyできないので、Allowが1つでもあるとそれが適応されます。

全リージョンにSubnetを作ることは必須ではない

作りたければ使いたい時、使いたいだけ作れ!というスタンス 私は大好きこの思想。

あとからCIDRは変更できる?

できない。Subnetは新規作成か削除のみ操作可能

Zoneについて

SubnetはZoneには縛られません。 ただし、リージョンを跨ぐことは出来ません。

一先ずここだけでも安心しました。AWS VPCの最大の設計ミス、Subnetは AZを跨げない という悪手は踏襲していません。

まとめ

  • 基本的なACLの考え方は昔から変わっていない。
  • 今までのNetworkは Legacyとか言われるようになって(気に入らない)、作成するには gcloud コマンド経由となった。
  • Auto の Subnet は今までのNetworkとほとんど変わらない。ただ意味もなくリージョン毎にSubnetを切るだけで、追加も削除もできない。
  • Custom の Subnet は自由にSubnetを切れる
    • Classをあわせる必要すらない
    • 大きなアドレス空間を細切れにしていく設計も不要
    • SubnetはRegionを跨げない
    • SubnetはZoneを跨ぐ、絞りたければインスタンスを建てなきゃいいだけ
    • Subnet毎にルートテーブルとか、ダサい仕様もない
  • Subnet内にインスタンスを構築するとき、ローカルIPアドレスを指定しなくとも、連番で若番から使う

なんでこんなにSubnetを嫌うのか

AWS-VPCを触ったあとに、初めてGCEを触った時に、ネットワークの簡素さに不安を覚えました。
「え、こんなに簡素でいいの?これしかできないの?」
しかし、インスタンスタグレベルでサーバ間通信、ルート、セグメントを分割できると知ったとき
「なんて賢いんだ!さすがGoolge!」
と思いました。私はネットワーク屋ではありませんが、オンプレ時代、というかクラウドコンピューティングが存在しなかった頃のインフラ屋を知っています。その概念から考えると

  • 各セグメントはSubnetで切るものだ!
  • セグメント毎にFWでコントロールするものだ!

という固定観念が多くの人にあるはずです。しかし、ほとんどのパブリック・クラウドはブロードキャストはおろか、ARPすら無いネットワークで、Subnetでセグメントを割る必要すら本当は無いのです。
ACLをサブネットに求める必要すらなく、他の選択肢が存在するので、そちらを選べば良いのです。はっきり行って、Subnetにこだわるのは時代遅れです。
GCEは私が知る限り、最後発だと思いますので、先人(AWS達)が継承した 古臭い固定概念 をバッサリ捨てました。正しい判断だと思います。

だいぶネガティブな論調で書きましたが、今回のSubnet対応により、Subnetを切らないと気がすまない という 意味のないこだわり は実現出来ます。ただしGCEのNetworkの本質は今のところ変わっていないので安心しました。

CloudFront ワイルドカードキャッシュクリアの不具合(修正済)

CloudFront(以下CFと略)を始め、CDNを利用するにあたり、まず注意することは、キャッシュを削除する手段を確立することです。CFはinvalidationをURL毎に発行することにより、このキャッシュクリアを実現していましたが、昨年、ワイルドカード (e.g /image/*) で一気にURLを指定し、クリアすることができるようになりました、が、一部の環境において不具合があり、それをAWSサポートに連絡、修正してもらいました。今はすでに治っているので問題ありませんが、CFの理解のために記述しておきます。

不具合ってどこよ?

https://blog.cloudpack.jp/2014/11/06/etcetera-to-update-header-on-cloudfront/

この問題を 勝手に古いヘッダのキャッシュ問題 と呼称します。
発生のメカニズムは

  1. CFにキャッシュされる、このとき、レスポンスヘッダもキャッシュされる
  2. TTLが切れて、オリジンにリクエストが飛ぶ
  3. オリジンのヘッダだけが変わっていて、コンテンツに変化がない場合、オリジンは304を返す
  4. CFは304を受けると、1. でキャッシュしたデータをつかいまわす

これに対して invalidationをかければOK!なんですが、ワイルドカードでinvalidationを掛けた場合、上手くクリアできません。

ヘッダだけが変わるってどういう状況?

CORSです。CORSの設定を忘れて、CFにキャッシュされたあとに、オリジンにCORS向けヘッダを指定したらこの状況になります。

CORSってなんや?

長くなるので適当に AJAXなどで、動的に別のURLの画像やコンテンツを取得し、使う場合に発動するブラウザ側のセキュリティーチェックと考えてください。
「えっ、Yahooとかでも外部サイトの広告とか出てるやん」と思うでしょうが、あくまでも動的にやる場合です、HTMLにベタでimageタグ書く場合はCORS関係ないです。ちょー適当にいうとWeb2.0です。知らないと生きていけない知識ですので、はじめて知った人はググってください。

今回はGETのことだけ考えていますが、POSTの場合でもCORSはあります。プリフライトリクエストでググってください。こちらはちょっとむずかしいですので、私のボケ防止にいつか書く。

CFのキャッシュの一生

CFは特定のURLリクエストを受けると、下記の動作をとります。

  • CFにキャッシュが無い場合は、オリジンに取りに行く Miss from cloudfront
  • CFにキャッシュがあり、且つTTLが切れていない場合は、キャッシュから返す Hit from cloudfront
  • CFにキャッシュがあるが、TTLが切れている場合は、再度オリジンに取りに行く RefreshHit from cloudfront

上の2つはだいたいわかると思いますが、最後の RefreshHit from cloudfront の時に 古いヘッダのキャッシュ問題は発生します。

TTLが切れたらキャッシュは破棄されるのではないのか?

破棄されません。破棄されるとおもいますよねーふつう。原理的にこれがあるので、古いヘッダのキャッシュ問題が発生するのです。

ということは 古いヘッダのキャッシュ問題は放っておいたらずっと解消しない?

確率としては極めて低いですが、解消する可能性はあります。
CFは RefreshHit from cloudfront 状態で実はキャッシュは捨てていないのですが、キャッシュ対象のURLに一定時間以上、アクセスが発生しない場合は、完全にキャッシュ削除されるタイミングがあります。しかしこのタイミングは完全にAWS様の機嫌次第です。何分とか明言されておりません。またTTLが1日とかの場合は挙動が変わるかもしれません(試してないです)

検証

状況を整理しておきます。

  • 古いヘッダのキャッシュ問題そのものは修正されていない。個人的にはTTL切れ = キャッシュ破棄としてもらうほうがいいので、改善要望は出した。
  • 古いヘッダのキャッシュ問題のW/Aは、invalidationすること。
  • しかし 個別URLで invalidation した場合は綺麗に Miss from cloudfrontになるが、ワイルドカードでinvalidation した場合は、RefreshHit from cloudfrontになる(キャッシュ破棄が発生しない)。

今回の問題は、ワイルドカード invaliだと古いヘッダのキャッシュ問題のW/Aが効かない(かった)、ということです。しかもこの問題はTTLが十分に長い設定(e.g 1day)などの場合は発生しません。私の検証では、Hit from cloudfront状態つまり、TTLが切れていない状態で、ワイルドカード invaliを掛けた場合は、なぜかMiss from cloudfrontになりますつまりW/Aが効いて、キャッシュが破棄される。TTLを1分とか短い設定にしていると、必然的にRefreshHit from cloudfrontになりやすく、この状態でのW/A(ワイルドカードでinvaliをかける)が効かないということがわかっています(た)。

手順概要

  1. S3にダミーコンテンツをぶち込む
  2. S3にCORSの設定をしない
  3. CFをS3オリジンで作る、TTLは30秒とか短くする
  4. CFにキャッシュを吸わせる = 古いヘッダのキャッシュ を作る
  5. S3にCORSの設定をする
  6. CFに問い合わせて古いヘッダのキャッシュ問題の発生を確認
  7. ワイルドカードで invaliかける
  8. CFに問い合わせてMiss from cloudfrontを確認 (不具合修正の確認)

検証スクリプト

S3にダミーコンテンツをぶち込むやつ

#!/bin/env ruby

require 'aws-sdk'
BACKET_NAME = 'YOUR Bucket Name'

def put_3k_files (d, ver)
  s3 = Aws::S3::Client.new(region: 'YOUR Region')
  for num in 0..1000 do
    key = d + sprintf("/%04d.txt",num)
    s3.put_object(bucket: BACKET_NAME, key: key, body: 'this is ' + key + 'ver ' + ver)
  end
end

put_3k_files 'js', '1'
put_3k_files 'js/001', '1'
put_3k_files 'js/001/a', '1'
put_3k_files 'js/001/b', '1'
put_3k_files 'sound/prd', '1'
put_3k_files 'asset', '1'

S3を突っつくやつ

#!/bin/env ruby

require 'net/http'
require 'uri'
require 'resolv'

def get_url(ac_url)
  url = URI.parse(ac_url)
  req = Net::HTTP::Get.new(url.path)
  req.add_field 'Origin', 'http:example.com'
  req.add_field 'Host', 'CF FQDN'
  res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req)}

  ret_arr = [7]
  ret_arr[0] = ac_url
  ret_arr[1] = res.code
  ret_arr[2] = "None"
  res.each do |n, v|
    if n == 'access-control-allow-origin'
      ret_arr[2] = v
    end
    if n == 'content-length'
      ret_arr[3] = v
    end
    if n == 'x-cache'
      ret_arr[4] = v
    end
    if n == 'etag'
      ret_arr[5] = v
    end
    if n == 'age'
      ret_arr[6] = v
    end
  end
  return ret_arr.join("\t")
end

rslv = Resolv::DNS.new()
ip_list = rslv.getaddresses('CF FQDN')

dirs = ['js','js/001','js/001/a','js/001/b','sound/prd','asset']
#dirs = ['js']

for num in 0..1000 do
  key = ARGV[0] + sprintf("/%04d.txt",num)
  full_url = 'http://' + 'CF FQDN' + "/" +  key
  print get_url(full_url) + "\n"

#    ip_list.each do |ip|
#      full_url = 'http://' + ip.to_s + "/" +  key
#      print get_url(full_url) + "\n"
#    end
end

コードは散らかっているが、引数に js とか asset とか指定すると、その配下の1ファイルを突っつきます。出力の3カラム目に *がきていれば CORS用のヘッダが来ています。コメントアウトしてますが、DNSラウンドロビンで返してくるIP全てに対しても打つ事ができます。(あたりまえですが、めちゃ時間かかります)興味ある人は試してみてください。

検証結果

問題は確かに解消していました。

まとめ

ワイルドカードによるinvalidationの問題は解消されましたが、基本的にinvalidation自体が悪手です。さらに、前述してますが、TTLが切れたらキャッシュも削除される という認識が根本的に間違っています(いいか悪いかを別にして、CFにおいては)。さらにさらに、TTLさえ短ければキャッシュ残りは心配しなくていいという考えも間違っています、レスポンスヘッダとコンテンツをCFはごちゃまぜでキャッシュしています。
よって、古典に習い

  • 先頭URLをバージョンとかにして、バージョンごとにURLを使い捨てる
  • QueryString等でごまかす

を使うのがベストです。別URLはCFは別キャッシュだと判断するので、オリジンが同じであろうと関係なく、URLが違えば別キャッシュとして扱う と覚えておきましょう。ちなみにリクエストヘッダもCFが同じキャッシュとみなすか否かに含まれているようです(試してみてください)。少し話がそれましたが、invalidationはおまけぐらいに心構えるのが良いです。

.. あと、この手の検証をやる人は、ブラウザ側のキャッシュを強制無効にしてやるのがいいです。