ISUCON13の記録
今年も来ましたISUCONの季節。遅くなりましたがブログ記事となります。 いつもの頼れる後輩syusui、abcangとチーム7年目として出場し、全体12位 + 株式会社アークエッジ・スペース様の打ち上げ成功賞をいただきました! 株式会社アークエッジ・スペース様の打ち上げ成功賞はなんと衛星開発現場見学ツアーに参加させていただけるということで、かなり激アツな賞なのではないでしょうか。 開始前のスポンサー賞発表の時にこれ普通に行きてぇ~ってみんなで言ってたので本当にうれしいです。
今年は予選本戦の区分けがなくなりいきなり全員とバトルということで当初の目標はこれまでの予選突破ラインの30位以内としていましたが、これを無事達成しつつ賞までいただいてめでたい結果になったかなと思います。
大体の流れは abcang が書いてくれているのでそちらへのリンクを張っておくのと、僕のやったことを少しだけ書いておこうと思います。 abcang.hatenablog.com
リポジトリはこちら github.com
やったこと
最初の準備
準備されたマシンに置いてあるソースコード、publicファイル(フロントエンド)、SQLファイルなどをリポジトリに詰め込んでdeploy scriptをおきました。
pt-query-digestを見ながらスロークエリに対してインデックスを張る
シンプルですね。変に新しいツールを突然使うとほぼ間違いなく確実にしにます。 来年あるなら事前に準備してなんかモダンなやつを使ってみてもいいかもしれない。 performance_schema を使っている例も見ましたのでそのへんとか勉強して準備しておこうかなと思いました。
github.com github.com github.com
スロークエリをベースにテーブルの修正を行う
アクセスされるたびに、全テーブルなめて数を数えたり毎回ランキング生成をしている部分を事前に計算したり、情報が更新されるタイミングで計算しておいたりすることで毎回全部計算しなくてもいいようにしました。
github.com github.com github.com
SetMaxOpenConns
abcangくんの記事でも述べられていますが、何故かHTTPリクエストが全然来なくなってスコアが上がらないという現象が発生していました。 特にCPUが詰まっていて、アクセスが受けられていないわけでもなく、CPU idleはむしろ100ぐらいみたいな状況が続きかなり困った状態でした。 あと30分ぐらいしか時間がねえみたいな状況下でもうヤケクソじゃ!!!って値を変えたところめちゃくちゃスコアが上がりひっくり返りました。
SetMaxOpenConnsは初期実装のデフォルトで10が設定されており、DBコネクションが一瞬で埋まり、CPUとしては余裕がある状態にもかかわらず、 接続は受け付けたけどDBコネクションがidleになるまでまっているリクエストばかりになり、止まっているように見えたというオチでした。
自分で最初にこれは最後広げないとね~って言ってたんですが、うっかり忘れていたのでチームメンバーには大変なご迷惑をおかけしました。。。 今回設定した100という値もあと10分しかねぇみたいな状況下で、かなり適当で設定していたので、本当は何回かベンチ回したりして様子見しながら設定すればもっとコネクション開けたはずで、そうなるともっとスコアが上がったはずなので 大変悔しい思いがあります。仕事でも似たような調整したのに全くわかってなかったぜ。
おそらくではありますが、今どれだけのリクエストが来ていて、どれだけ処理しているのかみたいなメトリクスがあればもう少し気がつけたのかなと思います。 なんかさすがに処理できている量が少なくないか、とかアクセス自体は来ているなみたいなのがわかればここまでハマらずにすんだと思います。 次回以降の課題とさせていただきます。
まあこれにはまったおかげで急激に点数が伸び結果的に打ち上げ成功賞をいただけたので、ま、ええか!
終わりに
今年はちゃんと手を動かしていろんな改善を行えていた(例年何もわからんっていって手が止まったりしてた)ので全然点数上がらなかったときは大変大変苦しい時間を過ごしましたが、最終的にそれなりの点数になってよかったですね。 これも諦めずに全員で改修を積み重ねた結果だと思います。
毎年そろそろ作問大変そうだよなぁ~っていいながら毎年あーこうきたかみたいな驚きを与えてくれる運営陣には頭が上がりません。次回もまた期待しています!ありがとうございました。
最後終了30秒前に滑り込みで点数が出た様子
vscodeのruby extensionでsyntax highlightが死ぬ問題について
最近は仕事でVSCodeを使っているんだけど、Rubyの拡張 Ruby - Visual Studio Marketplace のアップデートの際に突然syntax highlightがぶっ壊れて仕事の効率が大幅ダウンしていたので、調べて直してみることにした。
issueを眺めていると問題のバージョン 0.16.0で問題があるらしくとりあえずダウングレードで対応しようぜってあったので対応した。
- 一度Ruby extensionをuninstallしてreload
https://rebornix.gallery.vsassets.io/_apis/public/gallery/publisher/rebornix/extension/Ruby/0.15.0/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage
ここから0.15.0をダウンロード (参考:https://code.visualstudio.com/docs/editor/extension-gallery#_common-questions
)- 落としてきたファイルの拡張子を
VSIX
に変更 - VSCodeのInstall Extension from VSIXをコマンドパレットから起動
- VSIXを選んでインストール
するとダウングレードできてなんとかもとに戻った。
react-resizable使った話
これの続きです。
研究で色々あってreactを使う判断をしました。
研究ではAndroidのセンサログ情報やら開発者が実装した行動認識を行うクラスを可視化して想定通りに行動認識アルゴリズムが動作しているかの調査や、そもそもどういったセンサデータが用いれるかを調べられるWebベースのデバッグツールを作っていました。 本デバッグツールでは様々なセンサデータを可視化することとなるのですが、その際には、グラフなどを自由にサイズ変更できる必要があるかと思います。
そこで STRML/react-resizable: A simple React component that is resizable with a handle. を使いました。 名前の通りリサイズに対応したReactコンポーネントです。
インストール方法とインポート方法はREADMEを参照。
function onResize(e, {element, size}){ this.setState({ viewSize: size }); } <ResizableBox width={this.state.viewSize.width} height={this.state.viewSize.height} onResize={onResize}> <div> Resizable </div> </ResizableBox>
簡単ですね。
ただリサイズできるようにするためのハンドルも設定しないといけません。
.react-resizable { position: relative; } .react-resizable-handle { position: absolute; width: 20px; height: 20px; bottom: 0; right: 0; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); background-position: bottom right; padding: 0 3px 3px 0; background-repeat: no-repeat; background-origin: content-box; box-sizing: border-box; cursor: se-resize; }
cssにこれを書く必要があります。backgroundには 」
型の画像が仕込まれていてここをドラッグすることでリサイズできるような仕組みです。
これもreact-draggableのときと同じようにコンポーネントのサイズを保持したい場合はlocalStorageなどを活用すればよいでしょう。
this.state = { viewSize: { width: parseInt(localStorage.getItem("width"), 10) || 400, height: parseInt(localStorage.getItem("height"), 10) || 400, } }; function onResize(e, {element, size}){ this.setState({ viewSize: size }); localStorage.setItem(`width`, size.width); localStorage.setItem(`height`, size.height); } <ResizableBox width={this.state.viewSize.width} height={this.state.viewSize.height} onResize={onResize}> <div> Resizable </div> </ResizableBox>
さらにreact-draggableと連携すればドラッグ可能でリサイズ可能なウインドウっぽい何かが完成します。
// constructorなどで実行 this.state = { componentPosition: { x: parseInt(localStorage.getItem(`position_x`), 10) || 0, y: parseInt(localStorage.getItem(`position_y`), 10) || 0 }, viewSize: { width: parseInt(localStorage.getItem("width"), 10) || 400, height: parseInt(localStorage.getItem("height"), 10) || 400, } }; function onDrag(e, position){ const {x, y} = position; this.setState({ position: {x, y} }); } function onStop(e, position){ const {x, y} = position; localStorage.setItem("position_x", x); localStorage.setItem("position_y", y) } function onResize(e, {element, size}){ this.setState({ viewSize: size }); localStorage.setItem(`width`, size.width); localStorage.setItem(`height`, size.height); } <Draggable handle="handle" position={this.state.componentPositon} onDrag={onDrag} onStop={onStop}> <div className="handle"> ここをドラッグ! </div> <ResizableBox width={this.state.viewSize.width} height={this.state.viewSize.height} onResize={onResize}> <div> Resizable </div> </ResizableBox> </Draggable>
こういうことをしてなんとか修論を乗り越えました。裏ではWebsocketやらReduxやら色々使っているので元気が出たらそのあたりも頑張って記事にします。
golangのparseライブラリで別サーバに向ける方法
我らがParse.comさんが本格的にサービス終了されました。それにより前に作ったinstall数集計golangアプリが動作しなくなりました。
今回の環境ではOSSのparse-serverへの移行を成功させているので、回避策としては単純なもので、向き先サーバを変えるだけだったけどろくなドキュメントがなくて大変だったので書いときます。
結論
まず結論です。最初のParse Clientの作成部分は以下のようになっています。
// Client インストール数問い合わせのクライアント type Client struct { parseClient parse.Client } // NewClient Client生成 func NewClient(applicationID string, restAPIKey string) *Client { return &Client{ parseClient: parse.Client{ Credentials: parse.RestAPIKey{ ApplicationID: applicationID, RestAPIKey: restAPIKey, }, }, } } client := parseclient.NewClient(defaultApplicationID, defaultRestAPIKey)
それをこうすれば他の実装を変えなくてもいいです。
// Client インストール数問い合わせのクライアント type Client struct { parseClient parse.Client } // NewClient Client生成 func NewClient(applicationID string, restAPIKey string) *Client { return &Client{ parseClient: parse.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, Credentials: parse.RestAPIKey{ ApplicationID: applicationID, RestAPIKey: restAPIKey, }, BaseURL: &url.URL{ Scheme: "https", Host: "parse.server.example.com", Path: "", }, }, } } client := parseclient.NewClient(defaultApplicationID, defaultRestAPIKey)
Transport
についてですが、今回建てたparse-sever側のSSL証明書をLet’s encryptにしていましてその証明書に対応しておらず認証失敗となるので、SSLの確認を回避することでとりあえずの回避となっています。これはちゃんとした鍵を置くとかすれば消してもいいでしょう(未検証)。
ソースレベルのお話
https://github.com/facebookgo/parse/blob/master/parse.go
アクセス先のサーバを決めているのは、この https://github.com/facebookgo/parse/blob/master/parse.go#L198 RoundTrip関数
func (c *Client) RoundTrip(req *http.Request) (*http.Response, error) { req.Proto = "HTTP/1.1" req.ProtoMajor = 1 req.ProtoMinor = 1 if req.URL == nil { if c.BaseURL == nil { req.URL = &defaultBaseURL } else { req.URL = c.BaseURL } } else { if !req.URL.IsAbs() { if c.BaseURL == nil { req.URL = defaultBaseURL.ResolveReference(req.URL) } else { req.URL = c.BaseURL.ResolveReference(req.URL) } } } if req.Host == "" { req.Host = req.URL.Host } if req.Header == nil { req.Header = make(http.Header) } var userAgent string if c.UserAgent == "" { userAgent = defaultUserAgent } else { userAgent = c.UserAgent } req.Header.Add(userAgentHeader, userAgent) if c.Credentials != nil { if err := c.Credentials.Modify(req); err != nil { return nil, err } } res, err := c.transport().RoundTrip(req) if err != nil { return res, err } if res.StatusCode > 399 || res.StatusCode < 200 { body, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { return res, err } if len(body) > 0 { var apiErr Error if json.Unmarshal(body, &apiErr) == nil { return res, &apiErr } } return res, &RawError{ StatusCode: res.StatusCode, Body: body, } } return res, nil }
最初の部分だけ見れば良い
if req.URL == nil { if c.BaseURL == nil { req.URL = &defaultBaseURL } else { req.URL = c.BaseURL } } else { if !req.URL.IsAbs() { if c.BaseURL == nil { req.URL = defaultBaseURL.ResolveReference(req.URL) } else { req.URL = c.BaseURL.ResolveReference(req.URL) } } }
req.URL
は Get
、 Post
などを呼び出す際に渡した http.Request
なので基本的にはnilになるはず。
でc
ことclientのBaseURL
の存在を見ています。
defaultBaseURL
は
defaultBaseURL = url.URL{ Scheme: "https", Host: "api.parse.com", Path: "/1/", }
こうなっているので同じような構造のものをクライアントに渡せば良いことがわかります。
まとめ
自前のparse-serverではlimitに制限がないのでガンガンAPI Callできていいですね。
golangで書いたスクリプトまとめ
Golangの勉強で書いたコードについて挙げます。
これは画像アップローダーです。短いコードなのでガッと載せます。
package main import ( "crypto/rand" "encoding/base64" "io" "log" "net/http" "os" "path/filepath" "strings" "github.com/gin-gonic/gin" ) func main() { port := os.Getenv("PORT") if port == "" { log.Fatal("$PORT must be set") } router := gin.Default() router.Static("/", "./images") router.POST("/upload", func(c *gin.Context) { file, header, err := c.Request.FormFile("image") fileExt := filepath.Ext(header.Filename) b := make([]byte, 40) rand.Read(b) randomName := base64.URLEncoding.EncodeToString(b) out, err := os.Create("./images/" + randomName + strings.ToLower(fileExt)) if err != nil { log.Print(err) c.Err() } defer out.Close() _, err = io.Copy(out, file) if err != nil { log.Print(err) c.Err() } c.String(http.StatusOK, randomName+strings.ToLower(fileExt)) }) router.Run(":" + port) }
まず、 router
には2つのエンドポイントが設定されます。一つは /upload
です。これは名前の通りPOSTで画像を受けて実行しているサーバの ./images/
以下に適当なハッシュをつけて保存するエンドポイントです。 画像を見たいときはおもむろに /<hash>
でアクセスすると見れます。ここはStaticとして設定をして実現をしていますが、サーバが画像を返すのは現実的によろしくないので、今後の課題とさせていただきます。
これはwebsocketでいい感じにチャットできるgolang webアプリです。 実装は、
- 作者: Mat Ryer,鵜飼文敏,牧野聡
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/01/22
- メディア: 大型本
- この商品を含むブログ (2件) を見る
これをパクって参考にしてやりました。
これはアクセス元IPを出すgolang webアプリです。 VPNなどを構築した後に本当にVPN越しに行けているのかの確認用に作りましたが、本当は 診断くん でなんとかしています。
package main import ( "fmt" "log" "net" "net/http" "os" ) func doko(w http.ResponseWriter, r *http.Request) { var ip string ip = r.Header.Get("X-Forwarded-For") if ip == "" { var err error ip, _, err = net.SplitHostPort(r.RemoteAddr) if err != nil { log.Println("SplitHostPort error: ", err) fmt.Fprint(w, "IP: ???\nHost: ???") return } } log.Println("IP: ", ip) host, err := net.LookupAddr(ip) if err != nil { log.Println("LookupAddr error: ", err) fmt.Fprintf(w, "IP: %s\nHost: ???", ip) return } log.Println("Host: ", host) fmt.Fprintf(w, "IP: %s\nHost: %s", ip, host[0]) } func main() { port := os.Getenv("PORT") if port == "" { log.Fatal("$PORT must be set") } http.HandleFunc("/", doko) err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
func doko
内でアクセス元の情報をもとに返してあげる実装になっています。簡単でした。
まとめと感想
Webアプリをガッツリ書くよりもAPIのためとかコマンドとして書くみたいな方が書きやすかった。一時期RailsみたいなフルスタックWAFとして使おうとしたけどなんか違う気がする。
ssh設定
sshはすごい便利ですね。中でも気に入っているのが .ssh/config
です。一番好きな設定ファイルです。
知らない人に便利さを伝えるときにここを見ろとするために書きます。
普通のログイン
host1.example.com
にアクセスしたいときを想定します。
$ ssh <ユーザ名>@host1.example.com
初回の接続時には、以下のような確認メッセージが出ます。
The authenticity of host 'host1.example.com' can't be established. RSA key fingerprint is xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx. Are you sure you want to continue connecting (yes/no)?
yes
とすれば良いです。この確認メッセージはサーバの秘密鍵フィンガープリントになるのですが、もしDNSキャッシュポイズニングなどで接続先などが書き換えられて悪意のあるサーバにアクセスさせられかけたときに、あれ?フィンガープリント変わってるけど大丈夫???って聞いてくれる仕組みです。
次にパスワードが問い合わされるので答えてあげましょう。 認証が通れば晴れてログインです。
鍵認証
毎回パスワードを入力するのはセキュリティリスクが高くあまり推奨されません、それに面倒ですね。 そこで一般的には公開鍵認証というのが用いられます。公開鍵暗号の仕組みそのものはwikiに説明を譲ります。
まずは自分の秘密鍵と公開鍵を生成する必要があります。
$ ssh-keygen
すると
Generating public/private rsa key pair. Enter file in which to save the key (/home/you/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/you/.ssh/id_rsa. Your public key has been saved in /home/you/.ssh/id_rsa.pub. The key fingerprint is: xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx you@your_host
このように出ます。最初の Enter file in which to save the key
では鍵の位置が聞かれていますが、デフォルトでいいでしょう。
次の Enter passphrase (empty for no passphrase)
と Enter same passphrase again
では秘密鍵に対するパスワードを問われています。
秘密鍵は名の通り秘密にしなくては行けない鍵で、万が一漏れた際には登録しているサーバに問答無用で入られてしまうので鍵をかけたほうがセキュリティは安全です。
が、面倒な場合はかけなくても良いです。そうした場合は鍵の取扱には十分注意してください。
このコマンドのあと ~/.ssh/id_rsa
と ~/.ssh/id_rsa.pub
というファイルが生成されています。
~/.ssh/id_rsa
が秘密鍵、 ~/.ssh/id_rsa.pub
が公開鍵となります。
サーバに公開鍵を配置すると、自動的に公開鍵認証が用いられます。
サーバサイドの ~/.ssh/authorized_keys
に公開鍵を追加します。
$ mv id_rsa.pub .ssh # id_rsa.pubはコピーしてきた公開鍵 $ cd .ssh $ cat id_rsa.pub >> authorized_keys
.ssh/config
~/.ssh/config
というファイルに設定を書くことで接続先に名前をつけたり、接続先固有の設定などができたりします。
例えば上の例をもとにすると、
Host host1 HostName host1.example.com User <ユーザ名>
こうすると、 ssh host1
だけで、User名とHostNameが設定されます。便利。
以下追記していく
react-draggable使った話
研究で色々あってreactを使う判断をしました。
研究ではAndroidのセンサログ情報やら開発者が実装した行動認識を行うクラスを可視化して想定通りに行動認識アルゴリズムが動作しているかの調査や、そもそもどういったセンサデータが用いれるかを調べられるWebベースのデバッグツールを作っていました。 本デバッグツールでは様々なセンサデータを可視化することとなるのですが、その際には、ツール利用者が自由にグラフ配置できるような仕組みが求められるかと思います。 なにぶん、昨今Androidのセンサは増えていますし、行動認識と一口に言っても様々なパラメータや計算過程の値が跋扈するわけです、大量のグラフを並べたいように並べる仕組みが必要かと思います。
そこで mzabriskie/react-draggable: React draggable component というライブラリを使いました。 名前の通りドラッグ可能にする便利コンポーネントです。
インストール方法と設定については公式READMEをご覧ください。
ドラッグ可能にする
ドラッグ可能にしたい要素を Draggable
コンポーネントで囲います。
<Draggable> <div> ドラッグできるよ </div> </Draggable>
これで div
要素はドラッグ可能になりました。簡単ですね。Reactのコンポーネントでも同様にできます。
ウインドウっぽくしたい
現状の実装だと要素内のどこをドラッグしてもドラッグされます。できればウインドウのUIっぽく上の方だけクリックしたら移動みたいな実装にしたいですね。
それには handle
属性を利用すれば良いです。 handle
で指定したclassの要素をドラッグすることで移動ができるようになります。
<Draggable handle="handle"> <div className="handle"> ここをドラッグ! </div> こっちではドラッグできないよ </Draggable>
位置を保存したい
今回のツールはブラウザベースで開発しているのですがリロードのたびに配置が変わるのは好ましくありません。 そこで位置を保存して再読込の際にはロードされるようにしましょう。
// constructorなどで実行 this.state = { componentPosition: { x: parseInt(localStorage.getItem(`position_x`), 10) || 0, y: parseInt(localStorage.getItem(`position_y`), 10) || 0 } }; function onDrag(e, position){ const {x, y} = position; this.setState({ position: {x, y} }); } function onStop(e, position){ const {x, y} = position; localStorage.setItem("position_x", x); localStorage.setItem("position_y", y) } <Draggable handle="handle" position={this.state.positon} onDrag={onDrag} onStop={onStop}> <div className="handle"> ここをドラッグ! </div> こっちではドラッグできないよ </Draggable>
こうすると、初回は position
属性によって位置が決められる。ドラッグ時には onDrag
が呼び出され、 state.position
が更新される。
ドラッグ停止時には onStop
が呼び出され localStorage
への保存が行われる。