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

lxyuma BLOG

開発関係のメモ

unityで2Dゲームのコマ送りアニメはSprite入れ替えで自作した方が良い

表題の通り。

書籍に書いてあるようなUnity本体の

Animator+Controller+Animation等使わずに、

SpriteRendererのSprite切り替えるだけ。

ちゃんと作れば、draw callも減らせる

効果

  • 何よりFPSが劇的に改善。本体アニメの1.5〜2倍のFPS。軽い。

  • アニメ作成/画像管理も簡単で公式より手間もかからない(勿論作り次第だが)

  • 作ってみると、思ったより大変ではなかった (土台は1日もかからない)

ここまでに至った経緯を書く。

概要

  • 2Dアニメ描くための選択肢
  • 重くてlicenseも邪魔だったNGUI
  • NGUI Sprite Anime vs 本体アニメのperformance 比較
  • 単にSprite切替だけでoverspecだった公式アニメ
  • 用途によって使えない3rd party lib/std asset
  • 1objのmesh renderer内で頑張る手間
  • 最終的にアニメ自作した

2Dアニメ描くための選択肢

Unityで2D game作る時、

キャラ動かすようなコマ送りのアニメの実現には色んな方法がある。

  • NGUI Sprite Animation
  • Unity公式のアニメ
  • Standard AssetのSprite Anime
  • その他の選択肢(Sprite Studio? etc)
  • Originalで作っちゃう

NGUI SpriteAnime

元々、Unity UIの実装が面倒でuGUIも無かった事もあり、今も根強く多いであろうNGUI。

名前の通り、本来、GUIのためのツールだが、

なんだかんだで、ゲームのキャラ等で使ってるprj多いかと。

メリットとしては、アニメ系の実装が簡単ではある(特にTween系)

こういうのはuGUIでも無いので助かるのだが、まあ自分で実装しても大した量では無い。

画像圧縮もunity本体にあるので、もうNGUI要らない気もする。

冷静に考えるとそんなにメリットないのだが、なによりデメリットが多かった。

デメリットは、

  • 1)NGUI Sprite Animeの初期処理Start()が高負荷
  • 2)何かあった時の改修がライセンス上、でき無い
  • 3)限界まで行くと、描画をしてくれ無い

1)NGUI Sprite Animeの初期処理Start()が高負荷

NGUI sprite animationを使うとコマ送りのアニメが簡単に作れる。

が、これ、開始時のStart()処理が大変、高負荷だった。

何やってるか辿ると、単にatlasから指定したprefixのspriteを見つけてるだけ。

多分、ちょっとしたUIのアニメはそれでいいのだろうが、

キャラで使うと、(そもそもUIでないし使い方間違っているのだが)

方向やアクションやら複数あり、ドローコールも減らしたいので、

どうしても1枚のatlasに大量の画像を埋め込んでしまう。

この量的な問題でStart()処理が重くなる。

初めに生成しておいて使いまわせば初めは回避できるが、

キャラの方向変える途中で画像prefix変えてもStart()処理呼ばれてしまう。

初めから全方向全アクションのSpriteAnime準備すればいいが、

構造が大分いびつで、確かメモリ使用も多かった。

2)何かあった時の改修がライセンス上、でき無い

NGUIは明確な利用規約がない(ほとんどのアセットもそうなのだが)

そうなるとUnityのEULAに沿う事になるのだが、

そこに、アセットのmodifyや派生物の準備が禁止されている。

具体的に言うと、直接コードを改変してはいけないし、

継承して拡張クラス作るのも危ないということ。

なので、ちょっと使いづらいと思っても気軽に変更できない。

もし、必要以上に細かく作られていれば、使い方工夫すればいいので、

それでもいいのだが、

NGUI Sprite Animeは作りがシンプル過ぎるので、出来ない事が諸々でてくる。

本体改修に継承避けて色々工夫すれば実装できるのだが、

これもどんどん歪になってくる。

3)限界まで行くと、描画してくれ無い

パフォーマンス気になって以下の検証をしてみた。

画像32×64の2コマ画像を縦横に複数並べてアニメ再生してみて、

・公式アニメで作ったもの

・NGUIで作ったもの

で比較。

・(10x10)100 obj程度では差異がでなかったので、

・(50x50)2500個位に一気に増やすと

1つ面白い違いが出てきた。

  • FPS自体は平均値はだいたい一緒(ややNGUIが高い程度)
    • 但し、公式アニメの方がFPSが不安定になった
  • この時、公式アニメは全部表示されるのに対して、
    • NGUIは表示は1列だけで、残りは真っ白で描画しなくなった

ここから考えられるのは、

  • NGUIは、何かしら閾値を超えて限界になると、一部の描画を諦める(のでfpsは安定する)
  • 公式アニメは画像をそのまま指定しているだけで、馬鹿正直に描画しようとするので、限界超えるとFPS不安定になる

UIのためのアニメなので、これでいいのだろうが、ちょっと、色々制御できない分、困る。

これらの理由から、NGUIは使わない事に。

本体アニメ

Unity本体にも当然2D画像のキャラをコマ送りのアニメ表示させる仕組みがある。

書籍やチュートリアル等でも出てくる正当な作り方ではある。

AnimatorにAnimationControllerつけて、

そこでAnimation(Clip)を指定していく、というもの。

本体アニメで作る手間

一番初めに作るのは、ドラッグドロップで簡単なのだが、

アニメクリップの単位でひたすらそれを繰り返す必要があり、

その後のAnimationControllerでの調整も必要で、中々面倒臭い。

ゲーム開発進めていくと、当然数も増えるので、このコストも馬鹿にならない。

これ結局3Dモデル動かすなら、この位がっつりしてるといいのだが、

2Dのキャラアニメ作るには、ややオーバースペックではある。

ステータスをGUIで見れてもそんなに嬉しくない

本体アニメをSprite差し替えScriptとperf比較すると...

イメージ的に、本体アニメは色々最適化が走っていて、

単に画像差し替えるよりめっちゃ早くなるような気がしていたのだが、

これも検証してみたらそうではなかった。

また同じ条件(複数の画像を沢山アニメ表示)で、

  • 本体アニメ
  • 画像切り替えオリジナルscript

で、やや複雑なアニメを500obj程出して

比較検証してみた。

結果。

  • 本体アニメより画像切り替えscriptの方がFPS比で、約1.5倍程上がって軽快になった。 (※一応、ちゃんとdraw call減らした上での数値)

あれ、本体アニメって全然遅い。

本体アニメはSpriteRendererのSprite切り替えてるだけ

objの動き見てたら、単純にSpriteRendererのSpriteを変えてるだけだった。

要するに、本体アニメのやってる事は

scriptからRendererのSprite切り替えてるのだけ。

自作しても同じ作りになる。

本体アニメと自作の違いは、AnimationControllerやらの

管理の有無の差で、本体アニメはそれがある分重い。

2D Gameにはオーバースペックな本体アニメ

この本体アニメ、3Dアニメ作るノリで2Dアニメも作ろうとしていて、

ちょっとオーバースペック。正直、殆ど要らない。

要らないというのも言い過ぎなのだが、

需要と供給の方向性が違っていて、

必要なのはもっとflashっぽいツールで、

それは別のツール頼る事になり、Unityにそこまでは期待はしていない。

このオーバースペックな管理のために作業のパフォーマンス落ちるなら自作した方がいい。

3rd party library

第3の選択肢として他にUnityのアニメ補完するような物がいっぱいある。

Sprite StudioとかSpineとか幾つか見たが、殆どFlash寄りのツールで

パーツ組み立ててリッチに動く日本の伝統的なソシャゲーっぽいゲームなら

あんなのでいいのだが、

単純なコマ送りアニメの2Dゲーム的には殆どオーバースペックだった。

それに、殆どがMeshRendererを1objに突っ込んで複数objを出すような物が多く、

SpriteRendererより重かった。

それ以外の方法

テクスチャ切り貼りして複数画像を1mesh renderer 1objにぶっこむ方法もある。

3Dの話ばかりだが、一応2Dのカメラでもできた。

が、これ、今後の追加改修/管理が大変そう。

やっぱり1obj毎に画像分けたい。

そうなると、やっぱり沢山のmeshより沢山のSpriteRendererの方が

パフォーマンスもまともなので、SpriteRenderer使った方が良さそう。

ということで

SpriteRendererのSprite切り替えるアニメを自作した。

FPS指定して、その逆数のミリ秒待って画像切り替える。基本はそれだけ。

ファイル名ルール決めれば画像を決まったdir入れれば再生できる。

公式の提供しているSpritePacker で一枚絵にしていれば、ドローコールも減る。

UnityのTextureImporterを使うと、このあたりの設定も自動化できた。(ここは俺作ってないけど)

まとめ

作るものにもよるが、

  • 2dで
  • パーツ組み立てでなく、コマ送りアニメで
  • ある程度、量が多くてパフォーマンス気にしていて
  • 管理も無理なく色々自動化していきたいなら、

パフォーマンス面と管理のしやすさから

SpriteRendererでSprite切り替えをしていく処理を書いた方が良さそう。

おしまい。

centos7でunix domain socketが読めない

nginx centos linux

最近、goji(golang web framework)触り始めていて、

centos7のnginxからunix domain socket経由で、

gojiに繋ごうとしても繋がらなかった。

原因はすごい下らない単純な理由だったが、メモ。

※ちなみに、よくあるsocketファイルのuser/権限の話ではない。systemdの話

事象

nginxでdomain socketを指定してbrowserアクセスすると、以下エラー

  • web上
502 Bag Gateway
connect() to 
unix:/tmp/go.sock failed 
(2: No such file or directory) 
while connecting to upstream, 
client: **.**.**.**, server: _, 
request: "GET / HTTP/1.1", 
upstream: "http://unix:/tmp/go.sock:/", 
host: "localhost:8080"

原因

/tmp配下にdomain socket置くのがよくないらしい

詳細

domain socketの記事でよく/tmp/***.sock経由で通信するものがあるが、

最近のfedora系はsecurityの都合上、/tmp経由のsocket通信を禁止してるらしい。

つい少し前のcentosでは/tmpでもdomain socket動いていた記憶があるのだが

いつの間にやら最近のcentosも、同じく/tmpのdomain socketはダメな様子。

nginx の起動ファイル

もう少し調べていくと、具体的にはcentos7のsystemd管理の話らしく、

nginxの以下の起動ファイルのPrivateTmp=trueによる挙動らしい。

# cat /etc/systemd/system/multi-user.target.wants/nginx.service
[Unit]
・・・略・・・
[Service]
・・・略・・・
PrivateTmp=true

[Install]
・・・略・・・

参考記事

対応

参考記事に習って、

/tmpでなく、/runに

domain socket入れたら動いた

補足

ちなみに、/runに手動で追加したdirの内容は再起動するとなくなってしまうので、

以下の手順で、systemdからの登録が必要。

  • 一時ファイル用の設定を追加
$ vim /etc/tmpfiles.d/my_app.conf

# 以下、追加
d /var/run/my_app 0755 user_name user_name -
$ systemd-tmpfiles --create /etc/tmpfiles.d/my_app.conf

$ systemctl daemon-reload

参考記事

ソース

goji

gojiはdocumentどこにあるかわからず(まだ全然ない?)

srcから辿って以下でdomain socket指定した。

goji.ServeListener(bind.Socket("/run/my_app/go.sock"))

nginx

いつものやつ。

upstream backend {
    server unix:/run/my_app/go.sock;
}

server {
    listen  80;
    server_name _;
    root        /usr/share/nginx/html;

        location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;

        proxy_pass http://backend/;
        }
}

varcharとtextの違い(mysql innodb)

mysql

mysqlの可変長文字列を扱う、varchar型とtext型の違いの話。

古い情報が混在していたので、ちょっと整理してメモ。

myisamの頃の話

  • sizeが違う
  • 行の中身がdataか(varchar)、dataへのポインタか(text)
    • 参照挟むので、performanceの違いがあった(varcharが早い)

今 net でぐぐって、ひっかかる情報の大半がこの話。

最近のinnodbの話

  • 最大sizeは一緒。64kb(但し、TINYTEXT型、MEDIUMTEXT型、LONGTEXT型は名前の通り違う)
  • varcharもtextも、中身は同じ仕組み(BLOB field / off page column)
    • 行にdata入れるのも、外部(overflow page)への参照にするのも、行フォーマット次第(row format)
    • 5.6で行formatのdefault は COMPACT
    • COMPACT : カラムの値が小さいとB treeにそのままデータ格納、768byte以上でoverflow page作る。5.0以降のdefaultで互換性維持の為、今もdefault
  • 仕組み一緒なので、performanceも変わらないらしい

違い

type 初期値 最大文字長 最大sizeの数の意味
varchar 指定可 指定可 文字数で指定(日本語でも、最大65535文字まで入る)
text 指定不可 指定不可 byteで指定(1文字3byteのutf8で日本語入れたら65535 の 1/3文字 入る)

※他、sql標準規格に含まれるか?(=varchar)含まれず独自拡張か?(=text)という違いもある

結論

  • ほぼ、varchar一択で良いのでは。
  • text型のメリットは、varchar以上のsizeを入れる型もある事(MEDIUMTEXT/LONGTEXT)と、型で最大文字長が明示できる点?(TINY/MEDIUM...etc)。普通に使う分には要らない。
  • ちなみに、どちらにせよ、innodbは8kbの壁と言われる行サイズの制限にひっかかる可能性がある。その時は、まずは行formatをcompressかdynamicに

参考

androidのcanvasのCORSが動かない

android canvas

経緯

canvasを画像に変換したかった。(toDataURL)

しかし、canvas内の画像をCDN経由にすると、

後述のとおり、ブラウザのsecrity policyにひっかかり画像に変換できない。

そこで、Cross Origin Resource Sharing(CORS)を使って、これを回避したかった。

これは、そのメモ。

結論

先に結論を書いておくと、androidだけ無理だったので諦めた。

canvasでの画像変換でのCORS対応は、まだまだ未整備な感じ。

ブラウザのsame origin policy

ブラウザには、same origin policy 同一生成元ポリシーというルール(RFC)がある。

これは、

  • 同じprotocol
  • 同じポート
  • 同じホスト

ではない、ページ間の操作やアクセスを制限するというもの

xhrやiframeなど、ブラウザの幾つかの機能で、上記の制約が入っているが、

その中に、canvasもこの制約が含まれている。

canvasでのsame origin policy

canvasは、toDataURL()をすると、

canvasの内容を画像データ(base64のdataスキーマuri)に変換できる。

この時、documentと異なるhostから取得した画像をcanvasに入れていると、

ブラウザのsame origin policyに抵触してしまい、

canvasが汚染(taint)して、画像に変換する事ができない。

コンソールにエラーが出てくる。

回避するには

回避するには、

  • Reverse Proxy
  • CORS

といった、やり方があるが、

わざわざこれだけのために、Reverse Proxy組みたくないので、

CORS使う事が一般的。

CORS

CORSは、Cross Origin Resource Sharingの事。

異なるホストのURLを事前に教えてこの制約から特別に回避してしまおうという話。

サーバー側と、クライアント側と両方に対応が必要。

以下、今回の、canvasの画像変換に限って書くと、

CORS サーバー対応

例えば、documentはくhttp://nantoka-host-aと、

その画像をはく、http://nantoka-host-bがあったとして、

画像の方のレスポンスのヘッダーに、

Access-Control-Allow-Origin: *

を追加する。(* がワイルドカード。ここに、host入れる)

他、幾つか、ヘッダがある。

参考

CORS クライアント側対応

画像を追加する時に、crossOrigin属性を指定すれば良い。

var img = new Image();
img.src = “http://nantoka-host-b”
img.crossOrigin = “Anonymous”;

参考

結果

  • PC(chrome)では、エラーでなくなった。
  • iOS系も、エラーでなくなった。
  • android系では、エラーはそのまま、無くならず、まだ出てきた。

androidとCORS

一応、caniuse見ても、android端末は古くからCORS対応されている事になっている。

ところが、2.3系、4.x系の端末で、

サーバー側のレスポンスヘッダー微調整したり、クライアント側の記述もいろいろ微調整して、

試行錯誤してみたが、結局、ずっと、以下のエラーが出る。

DOM Exception: SECURITY_ERR (18)

androidcanvasのCORSは機能していないようにみえる。

※全端末ではないかもしれないが、少なくとも、自分が見た端末は動かなかった。

stack over flow

stack over flow見てみると、やっぱり、皆動いてない。

ちなみに、自分の場合、はじめ古いios(5だったと思う)も動かなかったが、

Access-Control-Allow-MethodsとAccess-Control-Allow-Headersを追加したら、

とりあえず、古いiosも動くようになった。

androidbase64?

ググると、androidは、画像をxhr経由のblob(android2は無理なのだが)で取得して、

それをbase64に変換すると、policy突破できるような記述があり、

試してみると、確かに動く!

のだが、これ、実は、NGらしい。

same origin policyは、uriスキーマの違いもチェックしているので、

documentのhostにあたる、httpと、base64画像の、data uri scheme

異なるスキーマなので、仕様上は、NGにしなくてはいけないらしい。

参考

これをちゃんと実装しているのが、chromeらしく、それ以外の多くが実装されていないみたい。

確かに同じソースをandroidchromeで見ると、エラーになった。

chromeでエラーになる時点でOUTで、デフォルトのブラウザもいつ実装変わるのかわからないので、諦めた。

結局

androidの問題あるので、CORS諦めて、

documentと同じhostから画像をとるように妥協した。

但し、当然、そこだけ負荷かかるので、

ページの作り工夫することにした。

(本当に必要な時だけ、documentから画像を取得したい。。。)

createjs,canvasでハマるところ2014

createjs canvas android javascript

経緯

先日、初めてcreatejs使ったスマホ向けwebアプリ作ってリリースした。

まあ、予想通り、createjs、canvasにいろいろハマった。

はまったところは、gistにメモしておいたので、ここにも整理して乗っけておく。

なお、類似blog記事多数あり、俺もほとんど8割位それらの記事で助かりました。

ここで、参考にlink貼った記事書かれた方々に感謝します。

そして、次にcreatejs使う人のために、

既出の物、あまり他の記事に見ない物も、まとめて書いておきます。

参考までに。

概要

  • 以下の現象の原因と対応について書いていきます
    • タッチイベントがずれる
    • staging環境で画像click時にエラーに
    • retinacanvasがぼやける
    • (android)canvasが2つ出てきた
    • (android)でcanvasに1clickすると2clickした事になってる
    • (android)特定の条件で、eventに対して意図しないオブジェクトが反応してしまう
    • (android) 古い端末でcanvasが重すぎる
    • (android)一部の端末でcanvasではじめに描画した画像の残像が残って画面が崩れる
    • (iOS)iphone4をios7にした端末で、なぜか一部タッチできない

タッチイベントがずれる

  • 現象
    • タッチした時、実際のタッチした箇所と反応している箇所にズレがあった
  • 原因
    • 全体のzoomの割合に合わせてcanvasのzoomとscaleの調整が必要だった
    • よくスマホ向けに偽装userAgentのブラウザpluginとか入れてるとzoom入ってておかしくなる
    • ズレがあったら、まずは、zoomをチェックしよう
  • 対応
    • 全体にかかっているズーム率を取得。その後、canvasに対して以下スタイルをあてる
    • zoom: 1 / ズーム率
    • transform-origin: top left
    • transform: scale(ズーム率, ズーム率)
  • 参考

staging環境で画像click時にエラーに

  • 現象
    • localでは正常に動いたのに、stagingにあげた途端に、画面は描写されるが、タッチなど反応しない。
    • consoleのエラー「Unable to get image data from canvas because the canvas has been tainted by cross-origin data.」
  • 原因
    • canvasのcross domainにひっかかってた
  • 対応
    • mask用のclick領域を準備した
    • 具体的には、hitAreaにmask用のshapeを突っ込んだ
  • 参考

retinacanvasがぼやける

  • 現象
    • iphone5など、retinaな端末で見ると画像がぼやけて見える
  • 原因
    • 当然、canvasも実際のサイズの2倍しないといけない
  • 対応
    • devicePixcelRatioがあるものだけ、幅高さ2倍で、canvasを伸縮する
  • 参考
if (window.devicePixelRatio) {
    var height = canvas.getAttribute('height');
    var width = canvas.getAttribute('width');
    canvas.setAttribute('width', Math.round(width * window.devicePixelRatio));
    canvas.setAttribute('height', Math.round( height * window.devicePixelRatio));
    canvas.style.width = width+"px";
    canvas.style.height = height+"px";
    stage.scaleX = stage.scaleY = window.devicePixelRatio;
}

(android)canvasが2つ出てきた

  • 現象
    • canvasはHTML/DOM上1つしか存在しないが、ブラウザ表示では、その上に、架空のcanvas(表示内容は全く一緒)が出現
    • androidの少し古めの端末で出現(arrowsとか)
  • 原因
  • 対応
    • 親要素のoverflow(or overflow-x):hiddenをvisibleにして解決
    • 不都合あれば、DOMを調整しよう。

(android)でcanvasに1clickすると2clickした事になってる

  • 現象
    • createjsのtouch enableつけて、androidの一部の機種に悪影響
  • 原因
    • どうやら、タッチした時に、touchstartと、mousedownもfireしてるっぽい。
    • androidの一部端末に見られた。(galaxy系とか)
  • 対策
    • 初めにclick時に100ms以内か否かの判定つけたが、他に影響ありすぎた 参考:記事1
    • 結局、click時に毎回、touchstart以外をのぞく設定をした 参考:記事2
    • touchstart/touchend/touchmove全部この対応が必要

(android)特定の条件で、eventに対して意図しないオブジェクトが反応してしまう

  • 現象
    • あるobjをclickすると、そのobjのeventではなく、canvasの最も上のobjのeventをfireする
    • objに対してのあたり判定は成功していて、イベントバブリング、caputringフェーズで、元のobjを見つけられず、最後のobjがイベントの対象と勘違いして、そのobjをfireしてしまう
    • これも、一部のandroid端末だけで発生した。(端末は忘れたが)
  • 原因
    • 対象objの上に別のobjを重ねてtweenアニメしていたのが原因で、元の対象objまでevent callできなくなってる様子
    • 普通にcreatejsのバグっぽい
  • 対応
    • 今回の場合、たまたまanimeしてるobjはtouchする必要がなかったので、animeしてるobjにmouseEnabled = falseすると、解決

(android) 古い端末でcanvasが重すぎる

  • 現象
    • 古いandroidで重すぎて動かない。
    • 反応が遅すぎ
  • 原因
    • 指定のFPSについて来れてない
  • 対応
    • userAgentでandroidだけ分岐して、低めのFPS指定した(setFPS)
    • 参考値:android2,3,4.0まで FPS=5、android4.1,4.2をFPS=10、それ以外を20にした。全体挙動を見て、このあたりが妥当なんじゃないかなー。と。
    • ただ、こうすると、古い端末で自分でos upgradeしてると守りきれない
    • 最悪、アプリの中の設定で、低いFPSに切り替えられるようにしてもいいかもしれない(ゲームとかでこういうのありますよね)

(android)一部の端末でcanvasではじめに描画した画像の残像が残って画面が崩れる

  • 現象
    • androidの一部の端末で、画面の一部の描写がcanvasに残っていて消されなかった
    • 縞状の画面崩れや、画面の隅の1/8ほどの領域の崩れが起こった。
  • 原因
    • 描画についてこれていないっぽい
  • 対応
    • 1)問題端末の9割は、上述の通り、FPSを下げる(5らへん)と解決
    • 2)一部の端末(sony xperiaらへん)は、FPS下げるだけでは解決されず、FPSで自動でupdateのsyncかけていく直前に、手動で初めの一回だけstage.update()したら解決した
    • 3)一部の端末(sharp aquosらへん)は、それでもうまくいかず、調べていくと、android4.1/4.2で、clearRectが失敗してるらしい。4.1か4.2の時だけ、clearRect後に、visibilityをon/off切り替える感じにした。参考記事
// android4.1と4.2の時だけ、毎回のupdate()で、以下。
      this.canvas.style.visibility = 'hidden';
      this.canvas.offsetHeight;
      this.canvas.style.visibility = 'inherit';

(iOS)iphone4をios7にした端末で、なぜか一部タッチできない

結論

  • やっぱり、androidで苦労したなーという印象。

以上。

isucon4予選やってきた

isucon golang

isucon

isucon4予選やった。

言語は、せっかくの機会なので勉強して、Goで。

初めのscoreは1223点で、

最終的に、26631点まで伸ばして、2日目の17位で終わった。

つまり、予選通過ならず、敗退してしまった。

ここまでやった事でlog等から思い出せるだけを記録に残す。

事前準備

  • 予選問題をやった
  • ネットにころがってる、有り難い記事を読んでた(LINEの夏期講習完全版とか、関係者のまとめとか)
  • isuconを良い機会に、golangの勉強した
  • 参加メンバーの役割分担した。(皆がやる事分からないと混乱するので、大体何をするべきかエクセルにまとめた)
    • 俺:Go + nginx + mysql + KVS + infra
    • Aさん:Go + mysql + score算出programチェック周り
    • Bさん:front(cache周り) + infra
    • この担当のイメージで、更に細かいタスクを分けて分担していた
  • gitbucket準備した
  • 一日の流れを決めた
    • 午前中に最低限のinstallと設定と各種指標の調査計測を行って、
    • お昼にTODOを効果とリスク書いて整理して、
    • 午後にTODOを効果順につぶしていく。
    • | TODO内容 | 効果 | リスク | 担当 | みたいな感じ

予定していた構成

  • 過去の問題から、こんなイメージをしていた
  • front
    • varnichかnginxでcache
    • ※今時のサイトは画像とか絶対あるし、frontのcache周りが重要になるような気がしていた。
  • backend
    • nginx + Go(unix domain socket繋ぐ)
    • ※今迄のように生Go(with gorilla)の想定で、なんとなく心の奥底でmartiniかgin出るよ〜と言われてる気がしていたが、封じ込めていた。
  • DB
    • mysql + memcached
    • ※KVSはmemcachedだろうと思ってた。これも心の奥底ではそろそろRedis出るよーと言われてる気がしたが、封じ込めてた。

実際の構成

  • 結局真っ裸でtableも少ないweb appliだった。
    • KVS入ってなくて(これは独自で入れた方が良かったが、結局、私たちは入れなかった)
    • goはmartiniだった。(今後、参考にさせてもらいます。)
    • あと、front周りは静的fileが非常に少なかった。これが分かっていた時点で、早く、皆でbackendに注力するべきだったなー。
    • 初めからnginxだった。apacheをnginxにするだけでscore稼ぎみたいな事は出来なかった。

当日の朝

  • score算出プログラムを読もうとしたが、見当たらない。go binaryだけ?っぽく断念。
  • supervisordでGoに切り替えるのだが、どうやら、うまくいかない。
    • 何だろうと思ったら、元々立ち上がってたrubyunicornがずっと立ち上がりっぱなしだった。どうやら、停止に失敗していた
    • なんか、supervisordのrestartの動きが怪しい。
    • 原因はともあれ、もうruby要らないので、killして、Goを立ち上げ直した。
    • 予選終わった後に、Q&A見てみると同じくsupervisordがruby終わらせてくれてなくて困ってる人いた。皆一緒。
  • nginxの設定空っぽだったので、とりあえず最低限の設定した
    • worker_procesとかworker_connectionとかgzipとかepolとかsendfileとかそういうヤツ
  • /etc/my.cnfも基本的な設定を実施。slow queryとか仕込んで、innotopとpercona tool入れた。
    • でも、結局、mysqldumpslow/show process listしか、まともに使ってないな、そういえば。

domain socket繋ぎ

  • httpやtcp経由でnginx繋いだりmysql繋いだりしてると、3way hand shakeやフロー/輻輳制御する分コストかかるので、unix domain socketに変えていった
  • ただ、localhost内での話なので、scoreへの貢献は少なかったかもしれない
  • Goからnginx とのdomain socket
  //http.ListenAndServe(":8080", m)

  l,err := net.Listen("unix", "/tmp/go.sock")
  if err != nil {
    fmt.Printf("%s\n", err)
    return
  }
  sigc := make(chan os.Signal, 1)
  signal.Notify(sigc, os.Interrupt, os.Kill, syscall.SIGTERM)
  go func(c chan os.Signal){
    sig := <- c
    log.Printf("Caught signal %s: shutting down.", sig)
    l.Close()
    os.Exit(0)
  }(sigc)

  err = http.Serve(l, m)
  if err != nil {
    panic(err)
  } 
  • nginx側もsocketに対しての権限が必要で、Go側で権限を変えるコード書いてみたが動かなかったので、結局、supervisordからGoを実行するUserをnginxユーザーに変えた(権限でどこかのdir等ひっかかると思ったら、それで特に問題なかった)
  • goから繋ぐmysqltcpunix domain socketに変更

benchトラブル

  • この辺りでbenchmarkプログラムからエラーが出るようになった

その1

 type:fail  reason:init script failed  log:exit status 1  message:Do not run benchmark
  • 何これ?
  • benchmarkで使ってるinit.shでエラーになってて、中身を見ると、単に外部からtsvをimportしてるだけだった
  • 結局、一度に大量のデータ送られて困っているようで、以下/etc/my.cnfに書いたら解決した
max_allowed_packet=300M

その2

  • 次にこんな事言われた
02:47:40 type:fail  reason:Get http://localhost/: dial tcp: lookup localhost: too many open files  method:GET  uri:/
  • 確かこれは、いろんな所でfile descriptor上げると解決したと思う

その3

  • その次に更にこんな事言われた
01:31:07 type:fail  reason:Connection timeout  method:GET  uri:/
  • (もしかしたら、違うログだったかもしれないが)確か、これは、nginxがhost書き換えていたとかで、アプリ側で許可してるIPから外れてしまってエラーになっていたヤツだったと思う。
  • nginxの設定を見直して解決

  • ここら辺でお昼くらいになってた。気付けば。

お昼

  • 一旦、サーバーをimageにして皆にmaster AMIとして共有した
  • gitbucketも準備していたが、結局、AMIの共有で連携していた
    • Master AMI作成して皆に共有
    • 各自はmaster AMIを元につくった自分のinstanceで作業。うまくいけば、本番にも同じ適用
    • この適用の時は一部gitbucket使ってたんだけど、んーー。必要だったかな?
  • front任せてた人からgzipかけない方が良いといわれ外してもらった
    • 今回のケースではstatic file少なく、gzipかける前後のsize差がないのに、変換costかかるからscore下がってるっぽかった。

index

  • めっちゃ、遅かったが、ようやくベンチも動いてGoやnginxの基本構成も出来ていたので、ここでmysqldumpslowで遅いquery特定した
    • 明らかにindex不足だった
    • 弊社代表する優秀なエンジニア!にfrontやcache周り任せていたのだが、今回のシチュエーションだと殆ど活躍する機会が皆無で勿体ないので(というか申し訳ないので)、このmysql index周りをお願いした
    • 結局、このindexが一番効いて、一気に1万の台に上がった
    • なんというか、逆にあまり決めないで臨機応変な体制にしても良かったかもしれない。。。(ただ、そうすると、色々ぶつかりそうだったり、漏れがあったり...どうだろう。何がいいんだろう)

n+1問題

  • 所謂n + 1問題(ORMでよくあるLoopで毎回SQL発行するやつ)も見事にあったので、おっしゃ俺やるぜ!と思って頑張った
  • まあ、sqlは一個にまとめてprogramに適用したが、scoreに反映されず、あれ、と思ったらそもそも、このsqlを使っているのは、ルール上scoreに換算されない所だった!!!
    • 見事に1時間くらい無駄にしてしまった><うーーん。。
    • これ、もし罠的な意図無ければ、product codeに入れずに、score算出プログラム側でやってほしかったなーと思ったり
    • そもそも俺がルールよくみないのがいけないですよね。これ実際の現場でもよくありますよね。仕様よくみないとね。
    • 反省。

計測

  • 初め見たときはCPUもmemoryもがら空き状態だった。
    • nginxのconnection数やらGOのruntime.GOMAXPROCS(runtime.NumCPU())やらやってもCPU食ってくれない
    • 結局、IO待ちが多くて、上のindex追加対応とマージするとCPUそこそこ使ってくれるようになった
    • とはいえ、それでも確か8割位でLoadAverageも一桁で、まだまだ使い切れそう
  • process はnginxが複数たってくれなかった。(1台で玉に2,3台でてくる程度)
    • これも、index追加対応でIO待ち解消すると、ちゃんと複数あがってくれた。
    • worker_process autoの時の話だが。あれ、そもそも、ここ、皆固定値入れてるの?
  • あと、memoryが、超がら空きだったのが勿体なかった
    • swapも当然ほとんど発生してない。
    • 余裕有ればKVS入れて、Count処理とかcacheしても良かったなー。
    • KVSといえば、今回のtable構成では、loginのtrackingしてる所があり、単純にinsertしてselectしてるだけなので、これをKVS(redisでsort済みとかね)に突っ込むのやりたかった。ただ、queryが割と柔軟に書かれていたので、この対応いちいち書き換えて行く作業時間が、残り時間に入るか不安で没に。

夕方

  • /etc/my.cnf見直しをエンジニアAに依頼。ここは何をやったかあまり見てない
  • あと、locked usersというreportも気になって見てもらった
    • 今回のアプリはinsertとselectだけで、アプリ側でトランザクションも貼ってないので、どこからもdbのロックかかりそうもない。
    • 調べてもらうとresponseが3sec以上かかったuser?とかだった? ※ちょっと自分は確認してないが
    • なぜ、こんな事起こるか、その時は謎だった。
    • 予選後、反省会で、この話あがり、おそらく、nginx+GOの大きな蛇口に対してmysqlの蛇口が低すぎたんじゃないか、という説がでた。mysql周りで溢れた分が処理しきれていない可能性があるかもしれない。my.cnfもうちょっとしっかり見れば良かった

benchmarkのエラー

  • index追加して、nginx+Goの多重化を整理すると、またbenchmarkのエラーが出てきた
type:fail      reason:Get http://localhost/: dial tcp 127.0.0.1:80: cannot assign requested address    method:GET      uri:/
  • これ、多分、多くの出場者がひっかかっていたのでは?(井戸端でも誰か書いてた)
  • 調べると、goのnet/httpのkeepalive周りの問題っぽい。 ※参考1/2
  • nginxでkeepalive接続をoffにしたら、このエラーでなくなった。
  • こういうのも、現場でよくありますよね。

benchmarkの調整

  • 最後にベンチマークのworkaroundを幾つか指定して適性値を探した。
    • コア数前後が良さそうだが、この時点でまだCPU使い切ってなかったので、ばんばん並列度合い上げてみた
    • 結局、8くらいまであげれた(それ以上あげてもscoreにあまり貢献しなかった)
    • そして、それでも、まだCPU/LoadAvg余裕あった
    • 上にも書いたが、次はmysqlの蛇口を開くべきだったなー。my.cnfの設定に移れば良かった。
    • 反省会で出た内容だが、そもそもmysql冗長化してしまっても良かった気がした。
  • ここら辺で2万くらいのscoreになってた

最後

  • ぎりぎりになって、table設計の怪しそうな所を変えて試したが、score貢献なかった(varchar をintに、tinyint(4)を1にとか)
  • あと、mysqlの余計なlogとかoffにしたり。
  • マージしてマージして、と言っていたが、結局マージ反映全然出来てなくて最後少しバタバタした(何故か終了前にssh繋がらないとか、、、)
  • 17:50くらいに最終score送信して、それで終了

感想

  • 振り返ると、なんか、benchmark周りのエラーを除くのばっかやっていたような印象。
  • メモリーが全然がら空きでmemcache/redisとか突っ込めば良かったなーと
  • あと、初めに担当決め過ぎると、責任感の意識が働いて柔軟に動けず逆に良くなかったなーと
  • なんか、17:50くらいで終わったので、皆で反省会やったけど、なんか暗い雰囲気になってしまったのも良くなかった!
    • 反省会はお酒飲みながら明るくやらないとね!
    • その後、hikarieのおしゃれなお店で酒飲みながら反省会しました。

次回

  • 次回も参加したいっす。

Server-Sent Eventsまとめ。ハイパフォーマンスブラウザネットワーキング16章

まだ、社内でO'Reillyのハイパフォーマンスブラウザネットワーキングの読書会をやっていて、

自分が、16章のserver sent eventsやるので、その下書き。(前の資料もいつかblogにupする)

※ちなみに、書いてる事を単にまとめてるわけではないので、ご注意を。

ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化

ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化

16章 Server Sent Events

  • よく言われるのがSNSのTimelineの更新とかで使える仕組み
  • server sent eventsってwebsocketと被っている事もあって、中々目立たないですよね

server sent eventsの対応状況

  • caniuse
  • websocket同様androidIEが非対応で足ひっぱってきたが
  • androidは4.4から使える
  • IEは11でも未だに「使えない」

有名サイト

  • ちょっと気になったので、
  • 有名サイトでpush通知っぽい事をどういう技術で実現してるか、ざっくり調べてみた。
  • 2014年8月9日の状況
  • ざっくり見た感じなので、間違ってたらごめんなさい。

facebook

twitter

  • Timeline:ajax polling(数十秒)

gmail

  • ajax polling
  • 定期的にmailのpreloadしてて(多分cacheしてそう)、そのtimingでメールあれば、別xhr飛ばして取得してきてる

github

  • server sent events!!
    • 別host(live.github.com)とSSEで繋いでて、push来たら、本hostに通常のxhrでgetしてるっぽい

利用例

  • websocketも、server sent eventも、まだまだ珍しい状況。
  • 結局、いずれもIE/android対応遅かったので多くの機器でpolyfilする必要があり、複雑化を恐れたためか?
  • 今回ピックアップしてみた例の中では、server sent events利用してる所はgithub位だった。

実際、動かしてみる

sse

  • client側
source = new EventSource("stream");
source.addEventListener("message", function(e){
  // メッセージ表示処理
}, false);

sse

  • server側
// これはimageで色々省略してます。
var reqListener = function(req, res){
  res.writeHead(200, {"Content-Type":"text/event-stream"});
  res.write("id: 1234");
  res.write("data: some message");
}
http.createServer(reqListener).listen(8888);

要するに

  • front
    • EventSourceをnewすると、引数のパスにGETして通信開始
    • 後はevent bindingして処理を追記していく
  • back
    • responseのContentTypeにtext/event-stream
    • 決まった書式でresponse返す(data: ***)

Server-Sent Eventsの主要な要素

  • EventSourceインタフェース
    • DOMイベントとして取得できる
  • EventStream
    • 低レイテンシで単一接続で済む(consoleで確認してみよう)
    • sseはxhr上のstreamingでバッファ肥大化しない(通常のxhrと違って、メモリに溜め込まず消去されるらしい)

再接続

  • エラー時、自動再接続する機能
    • 試すと、確かにclientが勝手に再接続した。(復帰迄5,6sec位かかった)
  • 接続再開後、失われたメッセージの再配信する機能
    • これは、Last-Event-IDヘッダーをclientから渡す仕組みは確かにあるが、(p274)
    • それを見て、再送信させる云々はserver sideで自力で書かなくては行けない

polyfilライブラリ

  • 幾つかある様子
  • これとか
  • このpolyfilで決定版みたいなlibraryが無い点は注意(websocketといえば、socket.ioみたいな定番がsseは無い)

制限と課題

  • 制限
    • server -> clientのみ(逆は不可)
    • utf-8 文字のみ(バイナリはエンコードが必要で非効率)
  • 課題
    • NW中間装置がstreamをcacheするかも。そこからレイテンシ増加/接続破壊を引き起こす可能性あるらしい

まとめ

  • textのみの単純なpush通知なら、websocketよりsseの方が気軽(httpでxhrの延長なので)
    • serverも対応状況気にせず、実装もごちゃごちゃしない(さっきの中間装置はどうしようもないが)
  • IEandroidが足を引っ張ているので、polyfilは必須(定番っぽいのが無い)