読者です 読者をやめる 読者になる 読者になる

続 カッコの付け方

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

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はおまけぐらいに心構えるのが良いです。

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

Google Cloud SQL 2nd GenerationのFailOverなど

GAEのために出来た、という歴史的な背景からもあまりGCEから使うこともなかった Cloud SQL (1st generation)ですが、昨年 2nd generation がベータとなりました。パフォーマンスの高さについてはアナウンスされていますが、この記事はFailOverを中心に、2nd generationを探っていきます。

Cloud SQLとは

AWSで言うところのRDS、マネージドSQL(RDB もっと言うと MySQL)です。VMインスタンスに自前でMySQLを入れるよりも勿論割高ですが、バックアップとかフェイルオーバとかの面倒をGoogleが見てくれるというものです。

最初に結論

  • アクセスIPアドレスは、相変わらず Global IPのみ
  • アクセス元IPアドレスで制限
  • GCEの内部ネットワークに作成は出来ないが、GCE内から接続する分には、ネットワーク速度的なペナルティーはない(多分)
  • RDSのMulti-AZに相当する機能はある、が、そもそもGCEはZone (AZ) がどれ位離れているかとか明言されていない
  • Multi-AZと書いたが、Active/Standbyではない、Active/Active(RO) なので、どちらかと言うと AWS Auroraに近い (語弊を生む乱暴な言い方だが、構成としては)
  • FailOverの機能はある、が、自動的にエンドポイントを書き換える機能は無い
  • レプリケーション構成(semisync)

構成と役割

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

1つのDBクラスタとして管理します。各ロールについて

  • マスタサーバ
    文字通り
  • フェイルオーバレプリカ(RO)
    レプリカですが、フェイルオーバ時にマスタ昇格の対象となるものです。 必ずマスタサーバと同じインスタンスタイプとなります

  • リードレプリカ(RO)
    文字通りのレプリカ、マスタが死んでもこいつはマスタ昇格しない。マスタサーバと異なるインスタンスタイプでもOKです

フェイルオーバレプリカ はAuroraのReaderに近い存在です。それに加えて、絶対にマスタ(Writer)に昇格しないノードがリードレプリカです。

実践

Developers Consoleからポチポチしていれば出来ます。が、注意点のみ記載します

1) GCEからの接続前提であれば、勿論リージョンをあわせること

2) マスタのバックアップを取らないと、レプリカが作れない これは フェイルオーバレプリカでもリードレプリカでも同じ

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

接続確認

mysqlコマンドで接続できます。初期ユーザは root & パス無しなので、とっととパスワードを指定しましょう。

レプリケーションの様子

  • マスターノード
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000006 |      120 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.08 sec)

mysql> select @@binlog_format;
+-----------------+
| @@binlog_format |
+-----------------+
| ROW             |
+-----------------+
1 row in set (0.09 sec)
  • フェイルオーバレプリカ
mysql> show slave status \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: [master global ip]
                  Master_User: cloudsqlreplica
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000006
          Read_Master_Log_Pos: 120
               Relay_Log_File: relay-log.000005
                Relay_Log_Pos: 283
        Relay_Master_Log_File: mysql-bin.000006
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 120
              Relay_Log_Space: 613
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: Yes
           Master_SSL_CA_File: master_server_ca.pem
           Master_SSL_CA_Path: /mysql/datadir
              Master_SSL_Cert: replica_cert.pem
            Master_SSL_Cipher:
               Master_SSL_Key: replica_pkey.pem
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: [master server id]
                  Master_UUID: 5c40936b-cca9-11e5-8416-0242ac110009
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
1 row in set (0.14 sec)

mysql> select @@read_only;
+-------------+
| @@read_only |
+-------------+
|           1 |
+-------------+
1 row in set (0.12 sec)
  • リードレプリカ
# show slave statusはほぼ同じ

mysql> select @@read_only;
+-------------+
| @@read_only |
+-------------+
|           1 |
+-------------+
1 row in set (0.12 sec)

FailOver

2016/02/06現在、gcloiudコマンドでもFailOver発動のコマンドはありません。ただし、Cloud SQL APIを叩けば発動出来ます。 https://cloud.google.com/sql/docs/high-availability#test

FailOver手動発動

curlで叩けと書いてありますが、ちょっとむずかしいので API Exprolerで簡単に打ちます。 まず、Cloud SQL API を有効化させます。

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

ご丁寧に誘導リンクがあるので、そのままGo

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

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

上記のドキュメント通りに打つのですが、SettingsVersion ってなんやねん! ということで、これを先に取得します。gcloudで取得可能

gcloud sql instances describe <your master instance-name>

....
  kind: sql#settings
  locationPreference:
    kind: sql#locationPreference
  pricingPlan: PER_USE
  replicationType: SYNCHRONOUS
  settingsVersion: '19'
  tier: db-g1-small
state: RUNNABLE

この場合だと 19です。
これで実行可能です

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

Failoverの挙動

こんな感じです

  1. マスター止まる
  2. フェイルオーバレプリカが昇格
  3. 元マスター復帰
  4. 元マスタ-がマスターに昇格、同時にフェイルオーバレプリカダウン
  5. フェイルオーバレプリカがスレーブに戻る

AWSとは異なり、DNSに依存しないIPベースと言うポリシーは、悪い意味でも一貫しています。あくまで、意図的にフェイルオーバを起こした場合の動きですが、実際の障害時も同じ動作になると思います。
特筆することは、マスターのIPやエンドポイントが一定ではないという点です。上記パターンでもマスターは マスター -> スレーブ -> マスターと切り替わっていますが、エンドポイント(IP)はそれを追従していません。文字通りDBのフェイルオーバのみが実行されています。
この点をどう扱う(える)かが1つの導入への障害となりえます。フェイルオーバの一連の作業の中で、各サーバのマスタ昇格のイベントを捕まえることが出来ないので、HAPoxyの設定を変えてエンドポイントを変更したり、consulとかも難しいと思います。
ただ、これはエンジニアの勘ですが、最近この手のエンドポイント問題は、クライアントライブラリ側で吸収するのが主流となっていく気がします(mongoを除く) 。すでにPHPでもmysqlnd-msなど Read/Write spplitingを含めてHA構成のエンドポイント探しは、ライブラリ側の方が柔軟でかつ賢い実装がすでに出揃いつつあると感じています。

あとから変えられる?

1st に比べてかなりの部分が Pre-Provisionな感じになりましたが。。

ディスク

増やせますが、減らせません。1st-genの時は完全オンデマンドだったので、そちらも選べればよかったのですが。10 -> 15GBしか試していませんが - ダウンタイムはゼロ - ディスク拡張中のパフォーマンスは言及されていない

なお、容量アップにかかる時間は、恐らくもともとのディスク容量に依存するかは試していません。ただし、10 -> 15GB の場合は、一瞬でした。 ですが、問題があります!ここは後述

インスタンスタイプ

変更できます。が、もちろんサービス断有りです。所用時間は 2 - 4 分程度で、結構遅いな-

その他

以外なところではフラグ = サーバパラメータの変更でも再起動が必須な点でしょうか、下記ダイアログが出ます

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

フェイルオーバレプリカと変更

ここが結構問題です。マスターのディスクをでかくしたら、勝手にスレーブ(フェイルオーバレプリカ)もデカくしてくれそうなもんですよね?
インスタンスタイプを変えたらスレーブも変わりそうなもんですよね?最初に一緒にしないとダメとか言うなら。
それが変わらないんですよ。

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

ということは、マスターのスペックを変えたらフェイルオーバレプリカ作り直しということになります。これはイケてないなー
ちなみにこの点はドキュメントと矛盾しています。

After the failover replica is created, you can change all configuration settings of the instance, except that you can not enable backups or change the activation policy.

まとめ Pros/Cons

Pros

  • 1stから、DB限界データサイズが大幅UP
  • リードレプリカ台数調整可能
  • 自動FailOver
  • インスタンスタイプが明確GCEと同じ感覚で指定できる

Cons

  • 1stにあったデータ従量課金が無くなった
  • IP制限がGlobal IPのみなので、タグとかで制限出来ない、具体的にGCEでもエフェメラルIPを使っている場合は、Cloud SQL Proxyというのを使う必要がある。https://cloud.google.com/sql/docs/sql-proxy
  • 自動FailOverするが、エンドポイント書き換えはない
  • フェイルオーバレプリカのスペック変更ができない(ドキュメントはできるっぽいことかいてあるが、、)

Crate.IO クラウドネイティブな新しいデータベースエンジン

Crate.IOという新しいDBエンジンが 2014 TCのアワードをとっていたらしい。それからもう1年近く立っていますが、正直なところ、流行っているのか定かではないです。が、誰もやらないことのほうが面白いのでやります。

Crate.IOとは?

Crate でググると、木の箱の絵が出てきたり、Rustのパッケージマネジャ?とかぶるので、Crate.IOとします。正直、Crateはかなりググりにくい言葉です。ざっとまとめとくと

  • マルチマスタ型
  • ハナからクラウド前提
  • RDBで全部やるのじゃなくて、NoSQLでやろう、それもNoSQLの中でも用途ごとに分割しよう」という最近の風潮を全否定 全部 Crate.IOでやれ!
  • クライアントライブラリはもうほとんどの言語で用意済み
  • サーバ側はゼロコンフィグでいける(ようにしたい)

なかなか魅力的な おしながき でしょう?流行るかどうかはわからないですが。

アプリ開発者視点

Use Cases

インフラ的にいくら面白くとも、アプリ開発上意味がなければわざわざ採用する価値がありません。ましてや新しいDBエンジンです。全体的な印象としては、開発者に優しいと思います。
なんといっても
SQLが通る!

Crate SQL — Crate documentation

やっぱりSQLは強し! 上記マニュアル流し読み。

DDL

普通のRDBDDLとほぼ変わりませんが、シャーディング・ルーティングを意識した構文があります。

DDL デフォルトでシャーディング5本

Crate supports sharding natively, it even uses 5 shards by default if not further defined.

らしいです。5本以外だとDDLで指定すればOK。

Routing デフォルトはPrimary Key

If primary key constraints are defined, the routing column definition can be omitted as primary key columns are always used for routing by default.

Routingというと聞き慣れませんが、所謂シャードキーと捉えれば良さそう。Primary Key無しで、特定のカラムをシャードキー(clusterd by) に指定する。

Replication デフォルトで1レプリカ

Defining the number of replicas is done using the number_of_replicas property.

何箇所にレプリケーションするかを、DDLで指定しちゃいます!マニュアルによると最大4つ。ここまでやれば盤石でしょう。
しかもですよ!

Note The number of replicas can be changed at any time.

なんだと!

DDLまとめ

他にも Full Text や Partitioned Tableなど、ジジイがニヤつく機能やら、聞き慣れない Analyzer という物があったり、マニュアル読むだけでお腹いっぱい。
私が抜粋した分だけでまとめますと。

  • シャーディング・ルーティング・レプリケーション等、本来はウラ(インフラ側)で吸収していた部分が、思いっきりオモテ(=DDL)に出てきた

最初から、いかにスケールさせるかを想定して作られているので、これらの機能をDDLで指定します。ひとことで言うと、 なんかMongoDBっぽい ですが、MongoDBよりももう一歩踏み込んでいる感じはします。インフラ(サーバ)側を知るとそれは明確になります。

Query

https://crate.io/docs/reference/sql/queries.html#object-arrays

基本的なSQLは通りそうですが、その中でも気になったのが、Arrayに対するQueryです。いずれ試してみる。

Types

Object型など、ある程度予想がつくものもありますが、ユースケースにも上げられているジオ系のTypeと関数があります。複雑なことは出来ないですが、lat-longから距離を測ったりとかはできるようです。もっと複雑な機能は自分で拡張しろやってことでしょう。

https://crate.io/docs/reference/sql/data_types.html#geo-point

https://crate.io/docs/reference/sql/scalar.html#geo-functions

Blob

https://crate.io/docs/reference/blob.html

blobテーブルはDDLで定義して、Upload & Download は HTTP経由ですね。ここのDDLも勿論 シャード、レプリケーション数が指定できるようです。

各種言語の対応状況

安心してください、ほぼ全部です。

https://crate.io/docs/getting-started/clients/

万が一見つからなかったとしても。HTTPのRESTを叩くだけっぽいので、多分移植は楽勝。

アプリ側のまとめ

実のところ、インフラ側の方が楽勝ヒャッハーなんで、そこを書こうと思ってましたが力尽きました、次回書きます。
アプリ側も改めて見ると結構いいですね~

  • SQLが使えるよ!
  • シャーディング・ルーティング・レプリケーション、これらすべてDDLで指定する
  • Blobも使えるよ!、Upload/DownloadはHTTP
  • 認証機構は今のところ無い(マニュアル読んだ分だと)

今後Crate.IOの主戦場では、おそらく認証機構は必要となりそう。認証を入れた上で、どれぐらいのスピードが確保できるか。
最近の動向として、ESとの連携(データのマイグレーション?)が話題になっているようです。ココらへんも後ほど書くつもり。