taka_rock’s blog

エンジニア大学生の勉強記録

ISUCON10本戦に参加してきた話

ISUCON10 本戦に参加してきた話

ISUCON10予選に参加してきた話の続き。 予選敗退、、、悔しい。って気持ちだったのですが、ISUCON本戦2週間前になんだかんだあって本戦出場できることになりました。歓喜

メンバー

予選と一緒

本戦まで

事前練習

reudがVultrでインスタンスを4台用意してくれました。
予選ではhtopとnew relicでほぼ雰囲気で計測していたため、計測ツールの導入等練習しました。
具体的には

の導入の練習をしました。

また、予選の環境をインスタンス上に構築する際に運営側がAnsibleで用意してくれていた為、Ansibleを初めて触りました。触った所感として、「Ansibleめっちゃ便利だな」とチームメンバー全員でAnsibleに感動し、計測ツールの導入もAnsibleで行おうという話になり、reudがAnsible周りの用意をしてくれました。神。

後、この記事を読んで

主にはベンチ実行後、alp + pt-query-digestの結果をNginxのドキュメントルート配下にhtml出力し、そのURLをSlack通知するという方法で確認できるようにしていました。 この形式だと結果を中途半端にSlack通知するより視認性も高く、後から見返す時もわかりやすいのでオススメです

とあり、便利そう!ということでalp + pt-query-digestの出力結果をNginxで配信できるように準備しておきました。
その内容としては 定期実行処理としてcrontabに

crontab -e

こんな感じでnginxのアクセスログをalpに通し、sedでmdのいー感じの形式にし、md形式にて出力しました。

*/1 * * * *  cat /var/log/nginx/access.log | alp ltsv -r | sed -e 's/\+/|/g' -e '1d' | head -n -1 > /home/isucon/logs/nginx.md

また、pt-query-digestの方も

*/1 * * * *  sudo pt-query-digest /var/log/mysql/mysql-slow.log > /home/isucon/logs/slow.txt

のように出力しました。

本戦始まってから気づいたのですが、cronでこんなに解析かけるとサーバーになかなか負荷がかかります。その為本戦ではcron外しました、、、良い子はめんどくさがらずにちゃんとベンチマークの実行に合わせて解析をかけるようにしましょう。

景品

当日前に本戦出場記念の景品が届きました! 物が届くと嬉しいですね!

f:id:taka_rock:20201007212936j:plain
f:id:taka_rock:20201007213047j:plainf:id:taka_rock:20201007213040j:plain

当日

やぱったの家にメンバー3人で集まり、オフラインで本戦に挑みました!

f:id:taka_rock:20201007213019j:plain

本戦開始

10:00の開始と共に問題とレギュレーションを読み始めました。文章量かなりあった、、、

概ね読み終えた所でインスタンスに接続し、まずはミドルウェア周りの確認を行いました。

mysqlは8系が入っていることはすぐに分かったのですが、Webサーバーにnginxが用いられていると思いきや入っていない、、、

sudo lsof -i:443

で確認してみるとenvoyが動いている、、、存在は知ってたけど触ったことは無かったのでconfigのymlを雰囲気で読みました。

確認後はmysqlの設定でslow-queryのログをオンにし、pt-query-digestで解析できるようにしたり、別インスタンスからのアクセスを許可したり、軽くindexを貼ったりしました。

メンバーの方もUbuntuのバージョンによる違いによりansibleが上手く動かない等、かなり手こずっていました

また、alpでアクセスログの解析を行おうとしたものの、envoyのログをalpで欲しい形式で出力するのに時間がかかってしまい、結局やめました。(ltsvで食わせて上げる必要があると思っていたのですがalpのREADMEをちゃんと読んでみると解析に必要な情報が書いてありますね、、、本番中にも読んだのですが、焦っていて読み飛ばしていました)

サーバー構成

reudがneofetchを用いてサーバー3台のスペックを確認してくれて、3台ともスペックが違う事が判明しました。(3台のスペックが違うなんて思ってもいなかった)

  • サーバー1
    • CPU 2コア
    • メモリ 1GB
  • サーバー2
    • CPU 2コア
    • メモリ 2GB
  • サーバー3
    • CPU 4コア
    • メモリ 1GB

その為、初期状態でベンチを回した場合もスコアはバラけてました

  • サーバー1
    • 4973
  • サーバー2
    • 5617
  • サーバー3
    • 6887

netdataで負荷を確認しながらベンチを何度か回して、サーバーの構成を入れ替え、DBのCPU負荷が高いため最終的に

  • サーバー1
    • envoy
  • サーバー2
  • サーバー3
    • DB

の構成にしました。 多分この辺でスコアが多分7152点。

TeamCapacity

サーバーを分散し、CPU負荷に余裕があることが確認できた為、何かしらのパラメータでアクセス数のlimitにあたっているのでは無いかと考えました。メンバーに話すとTeamCapacityなるパラメータがある事が判明。 10→50へ上げてもらう。これでスコアが多分10010点。 良さげなTeamCapacityを探すために一旦120にあげて、ベンチを回したりしてみるも通らず60にてfix(負荷がかかりすぎるとenvoyが死んでしまっていたっぽい。open files limitに引っかかってしまっていた??)

WebPush

やぱったとreudが実装してくれました。 途中で実装手伝おうと思いドキュメント読んだものの、しっかり読まないと何も分からなかった為何も手伝えず。実装感謝です。これで多分14314点

Goのコードをい〜かんじに

Goのコードをい〜かんじにしようとしたものの時間が足らずにほぼできませんでした。

ラスト1時間

諸々あって、ベンチが通らなくなってしまいかなり焦りました。

f:id:taka_rock:20201008005658p:plain

終了時間ぎりぎりでベンチが通ったためそこでfinish

結果

failしてました

最後のベンチが通ったのでいけると思ったのですが、ランダムでベンチが落ちる状態だったようです。 出ていたエラーとしては下記

  • あるべき通知を受信していないことが検知されました
  • 最終検証にて Clarification の検証に失敗しました

悔しいです。

振り返り

競技中はやらなきゃいけないことが多すぎて終了時間まであっという間でした。その振り返りとして、やることとやらないことをしっかりと判断するべきだったと感じています。

具体的には、計測ツールの導入で手間取る箇所やenvoyの理解など競技のスコアアップには本質的には繋がらない箇所はある程度割り切って自分たちのできる所で更にスコアアップを狙うべきだったと思います。

できることをやるの大切。

後、マニュアルにキャッシュ可と書いてあったものの最小工数での良い方法が思いつかなかったです。他チームではVarnishというものをenvoyとwebの間でキャッシュしていたそう。知らなかった為、勉強する。

来年は社会人枠で本戦出場目指して頑張ります。

P.S.

メンバーでオフラインで集まって参加するのはとても楽しかったです。 来年はオフラインで開催できてると良いな。

ISUCON10予選に参加してきた話

ISUCON10予選に参加してきた話

ISUCON10予選に参加してきました。 ブログを書くまでがISUCON予選?と言われているので今回はISUCON10予選について振り返っていきたいと思います。

ISUCONとは?

いい感じにスピードアップコンテスト、略してISUCONです。 その名の通り、サーバーの高速化をしたりしていい感じにスピードアップして戦います。

本選へ参加するための予選があり、下記の条件で30チーム選出されます。

  1. 一般枠 (25チーム):予選終了時スコアにおける上位25チーム
  2. 学生枠 (5チーム):学生チームの中で、予選終了時スコアにおける上位5チーム

結果は?

結果は、1122点でした。 Twitterで見た所、学生枠の5位だった方が1240くらいだったそうなので学生枠での本戦出場あと一歩でした、、、
下記が作業レポジトリです。(機密情報が含まれていた為、commitを1つに固めてます) https://github.com/reud/01NOCUSI

誰と出たの?

ワンランク上のジロリアンというチームで3人で出場しました。

メンバーの役割分担としてはそこまで明確には決めてなく、インフラとアプリケーション(Go)のコードを3人でい〜感じに分担して作業する感じでした。
Discordを使って常に通話しながらのフルオンラインで競技に参加しましたが、特に問題なく競技を行えました。

参加までの準備

事前にISUCON9の予選を用いて少し練習していました。
reudがConoHaでVPSを借りてくれたので、そこに諸々立てました。
3人ともNginx, MySQLのチューニング等したことが無かった為、皆で介抱し合いながら色々勉強していました。
VPS借りてくれたのめっちゃありがたかった、、、

  • 皆で勉強したこと
    • N+1の対策方法
    • インデックスの貼りどころ
    • New Relicを導入しての計測
    • Nginxの設定チューニング
    • Mysqlの設定チューニング
    • インスタンスを複数立てた際の設定方法
    • インメモリキャッシュ
    • Goでの非同期処理
    • etc

基本的な所の勉強&参加者のブログを読んで自分たちも実際にやってみたって感じですね。参加ブログ感謝。

当日

開始まで

10:00開始だったものの、朝起きたら12:00開始に変更になっていました。時間の余裕ができた為、10:00からDiscordに集まりながらSlackのワークスペースを作ったり、便利コマンドや困ったときの記事をSlackに貼りまくっていました。開始延期ありがたい。 そんなこんなで12:20開始に変更になり、ゆるゆると開始待機していました。

12:20 競技開始

開始と同時に、チームメンバー全員で予選マニュアルをしっかり読みました。

踏み台経由のSSHをしたこと無かったため、 ssh_configの例がマニュアルに書いてあったのめちゃくちゃ助かりました。

 Host isucon-bastion
   HostName <踏み台用IPアドレス>
   Port 20340
   User isucon

 Host isucon-server
   ProxyJump isucon-bastion
   User isucon
   HostName <自チームサーバのIPアドレス>

インスタンスに接続して最初に、ディレクトリ構成を確認しました。 Webアプリケーションは/home/isucon/isuumo/webapp以下を触れば良さそうだったのでチームメイトにGit管理の追加&Githubのプライベートレポジトリの連携等頼みました。Git管理は必須ですね。 次はNginx, MySQLのデフォルトの設定を確認したりしていました。

とりあえずベンチを走らせようと思ったもののまだ走らせる事ができなかったので、ソースコードの確認やreudがnew relicを導入してくれたりしていました。仕事が早い。

13:30

ベンチを動かせるようになったので動かしました。 493点。なるほど。

13:40

やぱったがインスタンスのメモリを確認してくれました。
2GB、、、?めっちゃ低スペじゃね??って話したりしていた記憶がある。

new relicの計測結果や、ソースコード見た感じでデータベースチューニングゲーか、、、?と強く感じてきた頃。SQLクエリとschemaとにらめっこし始めました。
インデックス貼ろうにもwhere句とORDER BYが多すぎて辛い。

14:00

マニュアルに下記の記述が書いてあったことを思い出しました。

bot からのアクセスはコンバージョンに繋がらないため、弾くことが仕様として決定しましたが、まだ実装されていません。 bot は User-Agent が以下の正規表現にマッチする形式であり、このリクエストに対して 503 Service Unavailable を返すことが許可されています。 これに対するベンチマーカーからの減点は発生しません。

やぱたが正規表現の神(正規表現エンジンとか作ってた)なので該当UAからのアクセスは503を返すようにNginxの設定をお願いしました。/etc/nginx/sites-enabled/isuumo.confに下記を追加していく感じ

if ($http_user_agent ~* "^ISUCONbot(-Mobile)?$") {
  return 503;
}

点数が上がった気がする。(確かこの頃ベンチマークのキューを積めなくてすぐに計測できなかった)

クエリ見てみるページング処理がとLIMIT OFFSETを用いて記述されている。これを良い感じにしようと思ったが、条件が複雑でどうやってページングすればよいのか分からず断念、、、(これどうやってページングするのがベストだったんですかね、、、?)

この辺で、スキーマをデータ構造に最適っぽやつに変更して、インデックスを追加してみた。(これもベンチで計測できなかった為お気持ち早くなった気がする)

Webからアプリを操作していたら、画像ファイルの配信が遅かったのでNginxで静的ファイルのキャッシュをすれば早くなるような気がしたのでキャッシュの設定を追加する。(マニュアルしっかり読んだら、「ベンチマークからのリクエストは、 API に対してのみ行われます。」と書かれているので静的ファイル周りの設定は今回は何も意味ないですね。最後にキャッシュ周りの設定を剥がしました。マニュアルしっかり読みましょう。)

16:00

reudがhtopコマンドを用いて負荷計測する事を提案してくれる。
CPU使用率が100%貼り付き、、、
処理の流れ的にもMySQLの負荷が凄い事は確実なのでMySQLインスタンスを増やそうって話になるものの17:00まではインスタンス1台でスコア上げていく方針で作業を進める。

17:00

MySQLインスタンス増やそうにも、構成どうするって話になる。

ここが今回1スコアが上がったターニングポイント

3人とも別の手法提案していてどれを採用するか結構話し合った

こんな感じでどれを採用するかなかなかまとまらない為、とりあえずGoで1台, MySQLで1台の2台構成にして計測してから考えようって話になる。

ここで、Goから別のインスタンスMySQLに接続切り替えるのに結構手間取ってしまった、、、
やったこととしては/etc/mysql/mysql.conf.d/mysqld.cnfへ他インスタンスのIPをbind-address=xx.xxx.xx.xxxで追加して、Goのインスタンスからmysql -u isucon --host=xx.xx.xx.xxx --port=3306 -pで接続できることを確認。
接続できたので問題ないだろうとWebからページの表示を確認した所500が返ってくる、、、みたいな状況になり、色々話しているうちにユーザー+ホストでMySQLユーザー権限の管理がされている事を思い出し、各インスタンスに紐づくユーザーを追加した所、問題なく接続できました。(複数インスタンスでの接続練習しておくべきだった、、、)

やっとGoで1台, MySQLで1台で動作成功し、ベンチを走らせる。
スコアは覚えていないが、あれこの構成でも結構スコア上がってね??ってなった

19:00

インスタンス構成は、色々話しているうちにreudの案を元にNginx+Goで1台,テーブル毎に分割したMySQL2台を用意して、Go側でDB2台の接続情報を持つのが一番楽&安全でスコア上がりそうじゃね??って話になった為その構成に変更。

reudが爆速でGoでの2台DB接続処理を書いてくれました。早すぎて震えた。

ベンチを走らせるとスコア881で学生順位15位くらいに食い込んで、「あれ、本戦出場目指せるんじゃね??」という雰囲気が出て、インスタンス構成はこれで確定。

MySQLinnodb_buffer_pool_sizeを2GBにしたりGo側の処理をいーかんじにしてもらい再度ベンチを走らせる。
スコア1075で学生順位7位で本戦出場が本当に見えてくる。

f:id:taka_rock:20200914035139p:plain
学生7位

20:00

ミドルウェアのログ出力をオフにしたり、new relicをGoのコードから剥がしたりと撤退作業を始めました。
諸々作業完了後に計測すると1122点。他のチームがそこまでスコア上がっていないことを願い、これ以上のスコアUPは諦めました。

21:00

終わり。

結果

学生枠でも本戦出場に食い込めませんでした、、、

振り返り

結果発表待ちの時に気づいたのですが、MySQLの設定のinnodb_buffer_pool_sizeってメモリ上に展開するデータサイズで、それ+query_cache_sizeでクエリの実行結果もどんどんメモリ上にキャッシュしていくことができるんですね、この事を知らずにメモリの空きがある事は確認できていたものの、query_cacheのチューニングを行いませんでした、、、、メモリ限界までキャッシュさせてればもっとスコアあがったと思うとかなり悔しいです。勉強不足です。(再起動テストを行っていなかったため、結局Failで落ちていたっちゃ落ちていたかも)

またスコアに繋がる打ち手を全然自分で出せておらず、あまりチームに貢献できなかったなと感じています。

細かく振り返ってみると次の点が改善できたと思っています。

  • 計測を更に行えるように
    • 計測の練習ほぼしておらず、当日ぶっつけで計測したので、更に良い計測方法があったはず、、、来年は推測せずに、計測します。
  • Goの練習を直前にもっとしておくべきだった
    • Goは書いたことがあるものの、複雑な処理を記述すのには時間がかかってしまう位のレベル感なのでGoの最適化をチームメイトに全て任せる形になってしまいました。問題点見つけて自分で爆速で修正できるレベルまで練習しておくべし。
  • ミドルウェアの設定内容を深く理解しておく
    • query_cacheの件など理解しきれていない点が存在した。細かいパラメータを完全に理解しなくとも頭の中の引き出しは増やしておくべし。

まとめ

ギリギリ、本戦出場できなかったのでかなり悔しいです。精進します。

来年の意気込み

社会人枠で本戦出場目指します。

P.S.

DISCORDに書いてあった他の人の打ち手

  • MySQL5.7 → 8に変更する
    • 8に変更すると早くなるんですね、、、普段5系ばかり使っているので知らなかった
  • generated columns
  • Special index

はじめてのKubernetes

About

Kubernetesをはじめて触ってみた。
その際の記録。

環境構築

Docker for MacではKubernetesがサポートされている。
Docker for MacのPreferenceを開き、KubernetesタブからEnable Kubernetesにチェックを入れればKubernetesのインストールが始まり、少し待つとKubernetesが使用できるようになる。とても簡単!
f:id:taka_rock:20191110183857p:plain kubectl cluster-infoコマンドを実行し、次のような結果が返って来れば導入成功!

Kubernetes master is running at https://kubernetes.docker.internal:6443
KubeDNS is running at https://kubernetes.docker.internal:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

やってみた

Kubernetsでは「マニフェスト」と呼ばれる定義ファイルに「リソース」を記述して登録することで、コンテナを実行する。

  • Word List
    • Pod
      • コンテナの管理に使用するリソース。1つ以上のコンテナのグループを表し、Kubenetsにおけるコンテナ管理の基本単位である。
    • Deployment
      • 実行するPodの数とそのPodの定義テンプレートを含むリソース。複数のPodを管理する為に使用する。

Podの定義例
nginxコンテナを実行するPodの定義

apiVersion: v1
    kind: Pod
    metadata: 
    name: nginx
spec:
    cantainers:
    - name: nginx
        image: nginx:1.15.7
    ports:
    - containerPort: 80

Deploymentの定義例
ngixコンテナを実行するPodを3個起動する定義

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

コンテナを起動してみる

上記Deploymentの定義を記述したマニュフェストファイルをdeployment.yamlとして保存する。 kubectl applyコマンドを実行してKubenetesにマニフェストファイルの内容を実行する。

kubectl apply -f deployment.yaml

kubectl get podsコマンドを実行してKubenetesのクラスタ上にPodが起動しているかを確認する。
下記のような結果が返って来れば実行成功!

NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-6dd86d77d-5sgtm   1/1     Running   0          100m
nginx-deployment-6dd86d77d-cphxc   1/1     Running   0          100m
nginx-deployment-6dd86d77d-k4kkd   1/1     Running   0          100m

Podと通信してみる

Deploymentを用いてPodを起動し、コンテナを建てられたので実際に通信してみる。

  • Word List
    • Service
      • 複数のPodへの負荷分散を実現する為のリソース
        • Serviceの定義を元にロードバランシングできる

Serviceの定義例
先程のngixコンテナを実行するPodへポート8080への通信をPodのポート80へ転送する定義

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80

Deploymentの登録と同様に、上記Serviceの定義を記述したマニュフェストファイルをservice.yamlとして保存し、kubectl applyコマンドを実行してKubenetesにマニフェストファイルの内容を実行する。

kubectl apply -f service.yaml

kubectl get serviceコマンドを実行してServiceの状態を確認する。

NAME             TYPE          CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
kubernetes      ClusterIP      10.96.0.1      <none>        443/TCP    20h
nginx-service   LoadBalancer   10.105.91.94   localhost     8080:31494 TCP   102m

nginx-serviceがlocalhost:8080で待ち構えている事が分かる。
実際にhttp://localhost:8080へアクセスし、nginxの標準ページが表示されればService経由でのPodとの通信が成功! f:id:taka_rock:20191110193025p:plain

感想

Docker for MacKubernetesがサポートされている為、とても簡単にHello worldができ、コンテナの複数起動&ロードバランシングもとても簡単に行えた。
更に触れ、実用的なメリットも学んでいきたい。