selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

slackからAWS Lambdaを起動するために、Outgoing WebHooksではなくて Slash commandを使った理由

後で勉強会等で発表するための、自分用メモ。

TL;DR

  • Outgoing WebHooks は private channelでは使うことが出来ない
  • slash commandは private channel でも利用可能
  • private channel でも利用されることを考慮して slackからAPI Gatewayへの連携に slash command を採用した

やりたいこと

  • slack上でURLを入力したら botが起動する
  • botは入力されたURLについて、パフォーマンス観点で修正できるところを教えてくれる
  • botは誰でも利用することが出来る

やったこと

https://dev.classmethod.jp/cloud/aws/slack-integration-blueprint-for-aws-lambda/ 基本的に、class method さんのこのブログを見ながら手を動かした。 ここで、Outgoing WebHooksではなくて、Slash commandを使っている理由を諸々考えたので、その内容をメモった。

構成

SlashCommand(slack) => API Gateway => lambda => slack

slash commandを採用した理由

 slackから lambdaを起動するには、Outgoing WebHooksと、slash commandの2つの方法がある。Outgoing Webhookは、特定のchannelでの発言内容に、特定の語句が含まれていたときに、任意のURLにPOSTリクエストを飛ばすことが出来る。slash commandは、/remind などの、slack上で実行できる slash のコマンドを追加できる機能だ。指定した slash commandが実行されたときに、任意のURLにGET/POSTリクエストを実行することが出来る。slash コマンドでの実行は、他のユーザーにはコマンドが見えない。それは実行することが共有・教育につながるという chatopsの考え方からすると、少しばかりデメリットである。

 しかしながら、今回は slash commandを slack から Lambdaを起動するための interfaceとして利用した。その理由は、Outgoing Webhookは privateなchannelでは起動できないためである。実行コマンドを見えるようにするのは、botからslackに通知する内容で担保することにする。

参考資料

Outgoing Webhooks | Slack

The outgoing webhook integration is only available in public channels. If you would like to get data out of private groups and DMs in real-time, try a slash command.

Slash Commands | Slack

CircleCI 2.0 をlocalで動かす

概要

自分がいるプロダクトでCircleCI 2.0 の導入をすることになった。 恥ずかしいことにCircleCIの設定を自分でしたことが無かったので、設定を簡単にする方法を探した。 公式ドキュメントを読んでいたら、circleci をlocalで動かす方法が見つかったのでやってみた。 その結果、10minくらいで簡単に動かせることが分かった。

このドキュメントには、circleciをlocalで動かす方法と、localで動かす際の注意点を記載する。 基本的には、このドキュメントからの抜粋なので、疑問に思ったらそっちを見て頂けると :pray:

手順

  • circleciコマンドのインストール
  • circleci 2.0用の設定ファイルを書く
  • local環境でcircleciを動かす

circleciコマンドのインストール

curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && chmod +x /usr/local/bin/circleci
circleci update

これだけで完了

circleci 2.0用の設定ファイル

今回利用した circleci設定ファイルはこちら。

#.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/node:8.9
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          - v1-dependencies-
      - run: yarn install
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      - run:
          name: Running tests
          command: yarn jest
      - run:
          name: tslint
          command: yarn tslint -c tslint.json 'src/**/*.ts'

ここでは、この設定の説明はしない。下記のページにconfigの書き方が記載されているので、確認して欲しい。

Configuring CircleCI - CircleCI

local環境でcircleciを動かす

# 設定した config.ymlが正しいか確認する
$ circleci config validate -c .circleci/config.yml 
config file is valid
# circleciをlocalで動かす
$ circleci build .circleci/config.yml

====>> Spin up Environment
Build-agent version 0.0.4536-a668371 (2017-11-15T20:13:56+0000)
Starting container circleci/node:8.9
  using image circleci/node@sha256:80714f293dfc6fc72c815a5bc1dc35bf33b3b8e0ebfaec0b4a64c567bbd1e766
....
$ /home/circleci/repo/node_modules/.bin/jest
 PASS  __tests__/parse.spec.ts
 PASS  __tests__/checker.spec.ts

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        3.074s
Ran all test suites.
Done in 3.73s.
Success!
...

これで circleciをlocalで動かすことができた。 続いて、circleciをlocalで動かすときの注意点を記載していく。

注意点

  • 環境変数を指定するときは -e オプションを使うこと
    • circleci build .circleci/config.yml -e VAR=VAL みたいに書く必要がある
  • circleci CLIは Workflowsに対応していない
  • restore_cache, save_cache が使えない
# restore_cacheのエラーログ
====>> Restoring Cache
Error: Skipping cache - error checking storage: not supported

Step failed

最後に

circleciの設定をlocalで確認できるようになると、ちょっと書き換えるときにも色々と捗るので便利そう!

lighthouseの資料を読み込む【Offscreen Images】

TL;DR

  • 画像は必要になったタイミングで読み込もう
  • そのためには、IntersectionObserverが便利だよ

Why the audit is important

Offscreen imageは、フォールドの下に現れる画像である。 usersはoffscreenの画像は見れないので、offscreenの画像を最初のページ読み込み時において、DLする理由はない。言い換えると、offscreenの画像読み込みを延期することは、pageのload timeと操作可能になる時間を短縮できることを表している。

How to pass the audit

この監査を通すためには、最初のリクエストにおいては、トップページの表示されている部分のみDLすること。この戦略を適用することによって、あなたのJS、HTML、CSSは速度を上げることができる。 Critical Rendering Pathを読んでください

offscreen imagesのlazy-loadするタイミングを賢く決定するために、IntersectionObserverの利用を検討してください。

例えば、とても長いページの下の方に、いくつか画像があると仮定する。IntersectionObserverを使うと、あなたはスクロールして必要になった画像のみを読み込むことができる。

読みたい追加資料

lighthouseの資料を読み込む【Properly Size Image】

モチベーション

developers.google.com

最近、webサービスの高速化に向けて、色々と資料を読み込んでいる。Lighthouseを計測ツールとして使っているので、Lighthouse上でどのような指標を、なぜ計測しているのか把握する。

TL;DR

  • screenに表示されるものよりも、大きい画像は配信しない
  • そのために、一つの画像に対して複数のバージョンの画像を生成し
  • クライアントの端末や、用途に応じて適切な画像を自動で参照する必要がある

Why the audit is important

理想的に言うと、あなたのページはscreenに表示されるものより、大きい画像は配信しない方が良い。それより大きなものは、無駄なbytesとpage load timeの低下という結果をもたらす。

How to pass the audit

このauditの結果において、lightouseは全ての失敗したimageの一覧を出す。auditをパスできるように、imageのserve方法をrefactorしてほしい。

主な戦略は、"responsive images"と呼ばれる適切なサイズの画像を配信することだ。

responsive imagesにおいて、あなたは複数のバージョンの画像をそれぞれのimage毎に作り、メディアクエリ、viewport dimensionsを利用して、HTMLやCSSで使うための特定のバージョンを指定する。もう一つの戦略は、SVGのようなvectorベースの画像を利用すること。特定の状況下では、SVG imageはどんなサイズにもスケールできる。

もう一つの戦略は "client hints"と呼ばれるものである。client hintsにおいて、browserは、画像のリクエストを飛ばすときにそのviewport dimensionsとdevice pixel ratioを示し、serverが正しいサイズの画像を配信できているか注意するものである。

しかしながら、browserのサポートは限定的である。Automating Resource Selectionについては、ここから勉強しなさい。

gulp-responsiveやresponsive-images-generatorを見て、imageを幾つかのフォーマットに自動変換する方法を学びましょう。

読み込んだ方が良さそうな資料

AWS Startup Tech で話してきた

10/19 にAWS Startup Tech で30分間話してきました。

内容は Kinesis Firehose, Athena, QuickSightで行動ログ分析できるようにしたというお話しです。Kinesis Firehoseは、スケーラビリティに着目されることが多いですが、簡便性やコストの安さ価値の一つだよ!ということをお話ししてきました。

Kinesis Firehose、Athena、QuickSightについてお金が掛かるポイントをまとめたので、よかったら見てやってください。 Kinesis Analyticsや Glueについては、今後検討していきたいっすな。

speakerdeck.com

browserify 及び gulpの知識整理

最初に

将来的に、browserify => webpackに移行するだろうけれども、 現在の構成を正しく把握するために、メモとして残しておく。

browserifyとは

browserifyは、JavaScriptにおいて依存関係の解決を行うツール。 ES6においては、modulesの仕様が策定されているが、 対応している環境は少ないのでそれをなんとかするために使う必要がある。

用いるコード

function foo(){
  return 'foo';
}
module.exports = foo;
function bar(){
  return 'bar';
}
module.exports = bar;
var foo = require('./foo');
var bar = require('./bar');
var el = document.getElementById('box');
el.textConent = foo() + ' ' + bar();

browserifyを使ったビルド方法

# mainとなるコードだけビルドすれば良い。
browserify js/main.js -o bundle.js

すると、下記のコードが生成される。

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// bar.js
function bar() {
  return 'bar!';
}
 
module.exports = bar;
 
},{}],2:[function(require,module,exports){
// foo.js
function foo() {
  return 'foo!';
}
 
module.exports = foo;
 
},{}],3:[function(require,module,exports){
// main.js
var foo = require('./foo');
var bar = require('./bar');
var el = document.getElementById('box');
el.textContent = foo() + ' ' + bar();
 
},{"./bar":1,"./foo":2}]},{},[3]);

ということで、mainとなるコードだけbuildすればrequireしているファイルを全て探し、依存を解決してくれる超便利ツールがbrowserifyである。

参考資料

Gulpとは

開発ワークフローの中にある、時間の掛かる苦痛な作業を自動化してくれるツール。 npm moduleを利用して、2000以上のfile変換のためのpluginsが利用できる。 pluginを使えば CoffeeScriptをES5に置き換えたりすることも可能である。

gulpにおけるtaskの例

gulpはファイルをストリームとして扱いながら処理を行う。 例を下記のコードに示す。

// gulp.taskの第一引数はタスク名
gulp.task('sass', function(){
    var stream = gulp.src('sass/.scss')
        .pipe(sass()) // compile
        .pipe(autoprefixer()) // prefixの付与
        .pipe(minify()) // 圧縮
        .pipe(gulp.dest('css')) // 任意の場所に出力
    return stream;
})

このように .pipe() の中に、行いたい処理をまとめて実行することが可能である。

gulpとbrowserifyを使ったtransformの指定

gulpを利用したCoffeeScript => ES5変換方法は下記の用になる。

'use strict';

const gulp = require('gulp');
const browserify = require('browserify');
const babelify = require('babelify');
const fs = require('fs');

const dir = {
    srcJS: "./frontend/js/",
    distJS: "./app/assets/javascripts/",
};

const jsFiles = [
    'a.js',
    'b.js',
]

gulp.task('debug', ['debug_flag', 'build']);
var debuging = false;
gulp.task('debug_flag', function() { debuging = true } );
gulp.task('build', () => {
    jsFiles.forEach(function (file) {
        browserify(dir.srcJS + file, { debug: debuging } )
        .transform(babelify, { presets: ["es2015"] } )
        .bundle()
        .pipe(fs.createWriteStream(dir.distJS + file));
    })
});

isucon6の過去問を解く

やり方

とりあえず、現状自分たちが考えることができる理想的な方法で、過去問を解いてみる。

参考資料

https://speakerdeck.com/fujiwara3/isucon-summer-school-1 https://speakerdeck.com/fujiwara3/isucon-summer-school-2

環境を作る

https://github.com/matsuu/vagrant-isucon

# 1時間以上かかるので、暇な時にやっておくと良い
➜  isucon git clone git@github.com:matsuu/vagrant-isucon.git
➜  vagrant-isucon git:(master) cd ./vagrant-isucon/isucon6-qualifier-standalone
➜  isucon6-qualifier-standalone git:(master) vagrant up

ベンチマークを取ってみる

➜  isucon6-qualifier-standalone git:(master) vagrant ssh
ubuntu@ubuntu-xenial:~$ sudo -i -u isucon
isucon@ubuntu-xenial:~$ cd isucon6q
isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
2017/10/09 11:35:16 start pre-checking
2017/10/09 11:35:22 pre-check finished and start main benchmarking
2017/10/09 11:36:16 benchmarking finished

nginxのプロファイリング

# alpのinstall
root@ubuntu-xenial:~# wget https://github.com/tkuchiki/alp/releases/download/v0.3.1/alp_linux_amd64.zip
root@ubuntu-xenial:~# apt install unzip
root@ubuntu-xenial:~# unzip alp_linux_amd64.zip
root@ubuntu-xenial:~# install ./alp /usr/local/bin/
#ベンチマーク実行
isucon@ubuntu-xenial:~/isucon6q$ cd isucon6q/
isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
#alp実行
root@ubuntu-xenial:~# alp --sum -r -f /var/log/nginx/access_log --aggregates='/keywords/.*'
+-------+-------+-------+---------+-------+-------+-------+-------+--------+------------+------------+--------------+------------+--------+--------------------------------------------------------+
| COUNT |  MIN  |  MAX  |   SUM   |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY)  | MAX(BODY)  |  SUM(BODY)   | AVG(BODY)  | METHOD |                          URI                           |
+-------+-------+-------+---------+-------+-------+-------+-------+--------+------------+------------+--------------+------------+--------+--------------------------------------------------------+
|    68 | 1.147 | 7.731 | 323.394 | 4.756 | 1.147 | 4.726 | 7.696 |  1.423 |   7894.000 | 108819.000 |  4688744.000 |  68952.118 | GET    | /                                                      |
|   270 | 0.001 | 4.718 | 274.573 | 1.017 | 0.001 | 0.882 | 4.559 |  0.943 | 106015.000 | 106015.000 | 28624050.000 | 106015.000 | GET    | /css/bootstrap.min.css                                 |
|   135 | 0.001 | 4.710 | 137.316 | 1.017 | 0.001 | 0.856 | 4.154 |  0.942 |  28631.000 |  28631.000 |  3865185.000 |  28631.000 | GET    | /js/bootstrap.min.js                                   |
|   135 | 0.001 | 4.711 | 137.077 | 1.015 | 0.000 | 0.868 | 4.152 |  0.942 |  16849.000 |  16849.000 |  2274615.000 |  16849.000 | GET    | /css/bootstrap-responsive.min.css                      |
|   135 | 0.001 | 4.710 | 136.959 | 1.015 | 0.001 | 0.859 | 4.153 |  0.942 |  86351.000 |  86351.000 | 11657385.000 |  86351.000 | GET    | /js/jquery.min.js                                      |
|   135 | 0.001 | 4.709 | 136.811 | 1.013 | 0.000 | 0.872 | 4.150 |  0.943 |   1092.000 |   1092.000 |   147420.000 |   1092.000 | GET    | /favicon.ico                                           |
|   135 | 0.001 | 4.709 | 136.020 | 1.008 | 0.000 | 0.852 | 4.148 |  0.941 |     93.000 |     93.000 |    12555.000 |     93.000 | GET    | /img/star.gif                                          |
|    57 | 0.003 | 3.001 |  63.052 | 1.106 | 0.003 | 1.046 | 3.000 |  0.802 |     25.000 |    335.000 |     2615.000 |     45.877 | POST   | /stars                                                 |
|    67 | 0.001 | 3.006 |  59.732 | 0.892 | 0.001 | 0.714 | 3.001 |  0.779 |      5.000 |    335.000 |     3615.000 |     53.955 | POST   | /login                                                 |
|    41 | 0.032 | 3.002 |  49.207 | 1.200 | 0.032 | 0.969 | 3.001 |  0.886 |      5.000 |    331.000 |     6374.000 |    155.463 | POST   | /keyword

mysqlのプロファイリング

root@ubuntu-xenial:~# vim /etc/mysql/my.cnf
[mysqld]
slow_query_log  = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0
root@ubuntu-xenial:~# systemctl restart mysql
root@ubuntu-xenial:~# systemctl restart isuda.ruby
root@ubuntu-xenial:~# systemctl restart isutar.ruby
root@ubuntu-xenial:~# head -20 /var/log/mysql/slow.log
/usr/sbin/mysqld, Version: 5.7.19-0ubuntu0.16.04.1-log ((Ubuntu)). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
# Time: 2017-10-09T05:02:00.742015Z
# User@Host: isucon[isucon] @ localhost []  Id:     4
# Query_time: 0.000143  Lock_time: 0.000000 Rows_sent: 0  Rows_examined: 0
use isuda;
SET timestamp=1507525320;
SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY';
# Time: 2017-10-09T05:02:00.746467Z
# User@Host: isucon[isucon] @ localhost []  Id:     4
# Query_time: 0.004247  Lock_time: 0.003866 Rows_sent: 2  Rows_examined: 4
SET timestamp=1507525320;
SELECT * FROM entry
        ORDER BY updated_at DESC
        LIMIT 10
        OFFSET 0;
# Time: 2017-10-09T05:02:00.747545Z
# User@Host: isucon[isucon] @ localhost []  Id:     4
# Query_time: 0.000388  Lock_time: 0.000143 Rows_sent: 2  Rows_examined: 4
root@ubuntu-xenial:~# wget https://www.percona.com/downloads/percona-toolkit/3.0.4/binary/debian/xenial/x86_64/percona-toolkit_3.0.4-1.xenial_amd64.deb
root@ubuntu-xenial:~# apt install libdbd-mysql-perl libdbi-perl libio-socket-ssl-perl libnet-ssleay-perl libterm-readkey-perl
# Profile
# Rank Query ID           Response time Calls R/Call V/M   Item
# ==== ================== ============= ===== ====== ===== ===============
#    1 0x6C987DBB94BEC091 43.9842 96.9%   414 0.1062  0.13 SELECT entry
#    2 0x51C26B78ABE63960  0.6851  1.5%    28 0.0245  0.00 INSERT SELECT UPDATE entry
#    3 0x0C85264D3C24C18B  0.3384  0.7%    44 0.0077  0.09 SELECT entry
#    4 0x782FEBA311BC59ED  0.1392  0.3%   414 0.0003  0.00 SELECT star
#    5 0xF32185B56AEF3A1D  0.0631  0.1%    44 0.0014  0.00 SELECT entry
#    6 0x7E7900C7AF4DD0A4  0.0440  0.1%    23 0.0019  0.00 INSERT star
#    7 0x6FF06CCB4B4E1FAC  0.0261  0.1%    17 0.0015  0.00 INSERT UPDATE entry
#    8 0x41883793B68D128E  0.0235  0.1%    82 0.0003  0.00 SELECT user
#    9 0x16F15936BC5B2382  0.0224  0.0%    70 0.0003  0.00 SELECT entry
#   10 0x87B0C6DEA95CEF2E  0.0201  0.0%    84 0.0002  0.00 SELECT user
# MISC 0xMISC              0.0312  0.1%    59 0.0005   0.0 <32 ITEMS>

上から順番に遅いやつ。

# Query 1: 0.70 QPS, 0.07x concurrency, ID 0x6C987DBB94BEC091 at byte 28601055
# Scores: V/M = 0.13
# Time range: 2017-10-09T05:24:22 to 2017-10-09T05:34:12
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         32     414
# Exec time     96     44s   129us      1s   106ms   266ms   117ms    75ms
# Lock time     39    64ms    40us     5ms   153us   224us   361us    93us
# Rows sent     99   2.70M       1   6.94k   6.67k   6.63k   1.28k   6.63k
# Rows examine  91   5.40M       2  13.89k  13.34k  13.78k   2.66k  13.78k
# Query size     0  23.85k      59      59      59      59       0      59
# String:
# Databases    isuda
# Hosts        localhost
# Users        isucon
# Query_time distribution
#   1us
#  10us
# 100us  ###
#   1ms
#  10ms  ################################################################
# 100ms  ###########################
#    1s  #
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isuda` LIKE 'entry'\G
#    SHOW CREATE TABLE `isuda`.`entry`\G
# EXPLAIN /*!50100 PARTITIONS*/
select * from entry order by character_length(keyword) desc\G

改善作業に入る

step1 不要なカラムを読み込まないようにする

# before
keywords = db.xquery(%| select * from entry order by character_length(keyword) desc |)

# after
keywords = db.xquery(%| select keyword from entry order by character_length(keyword) desc |)
# Query 1: 17.68 QPS, 0.14x concurrency, ID 0x8AC9E716A43D8A44 at byte 610435
# Scores: V/M = 0.00
# Time range: 2017-10-09T05:57:41 to 2017-10-09T05:58:44
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         37    1114
# Exec time     89      9s     5ms   103ms     8ms    16ms     6ms     6ms
# Lock time     34   111ms    45us     4ms    99us   144us   138us    80us
# Rows sent     99   7.55M   6.93k   6.95k   6.94k   6.63k       0   6.63k
# Rows examine  92  15.11M  13.87k  13.90k  13.89k  13.78k       0  13.78k
# Query size    21  70.71k      65      65      65      65       0      65
# String:
# Databases    isuda
# Hosts        localhost
# Users        isucon
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms  ################################################################
#  10ms  ###############
# 100ms  #
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isuda` LIKE 'entry'\G
#    SHOW CREATE TABLE `isuda`.`entry`\G
# EXPLAIN /*!50100 PARTITIONS*/
select keyword from entry order by character_length(keyword) desc\G
isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
2017/10/09 14:57:41 start pre-checking
2017/10/09 14:57:45 pre-check finished and start main benchmarking
2017/10/09 14:58:41 benchmarking finished
{"pass":true,"score":4719,"success":1918,"fail":1,"messages":["リクエストがタイムアウトしました (POST /stars)"]}

step2 何度も実行されないようにする

#before
      entries.each do |entry|
        entry[:html] = htmlify(entry[:description])
        entry[:stars] = load_stars(entry[:keyword])
      end
#after
    keywords = 
    entries.each do |entry|
      entry[:html] = htmlify(keywords, entry[:description])
      entry[:stars] = load_stars(entry[:keyword])
    end
# Query 1: 4.67 QPS, 0.04x concurrency, ID 0x8AC9E716A43D8A44 at byte 687529
# Scores: V/M = 0.00
# Time range: 2017-10-09T06:04:42 to 2017-10-09T06:05:54
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         13     336
# Exec time     69      3s     5ms    54ms     9ms    17ms     5ms     6ms
# Lock time      9    28ms    32us   287us    83us   144us    33us    73us
# Rows sent     99   2.28M   6.93k   6.95k   6.94k   6.63k       0   6.63k
# Rows examine  73   4.56M  13.87k  13.90k  13.89k  13.78k    0.00  13.78k
# Query size     7  21.33k      65      65      65      65       0      65
# String:
# Databases    isuda
# Hosts        localhost
# Users        isucon
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms  ################################################################
#  10ms  ############################
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isuda` LIKE 'entry'\G
#    SHOW CREATE TABLE `isuda`.`entry`\G
# EXPLAIN /*!50100 PARTITIONS*/
select keyword from entry order by character_length(keyword) desc\G
isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
2017/10/09 15:04:42 start pre-checking
2017/10/09 15:04:47 pre-check finished and start main benchmarking
2017/10/09 15:05:42 benchmarking finished
{"pass":true,"score":5051,"success":2093,"fail":1,"messages":["リクエストがタイムアウトしました (POST /keyword)"]}

step3 assetsをnginxで返す

root@ubuntu-xenial:~# alp --sum -r -f /var/log/nginx/access.log --aggregates='/keywords/.*'
+-------+-------+-------+---------+-------+-------+-------+-------+--------+------------+------------+--------------+------------+--------+---------------------------------------------+
| COUNT |  MIN  |  MAX  |   SUM   |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY)  | MAX(BODY)  |  SUM(BODY)   | AVG(BODY)  | METHOD |                     URI                     |
+-------+-------+-------+---------+-------+-------+-------+-------+--------+------------+------------+--------------+------------+--------+---------------------------------------------+
|   114 | 0.645 | 6.417 | 314.101 | 2.755 | 0.645 | 2.723 | 4.654 |  0.862 |   7960.000 |  93210.000 |  4816125.000 |  42246.711 | GET    | /                                           |
|   456 | 0.001 | 3.043 | 278.135 | 0.610 | 0.002 | 0.503 | 2.131 |  0.497 | 106015.000 | 106015.000 | 48342840.000 | 106015.000 | GET    | /css/bootstrap.min.css                      |
|   228 | 0.001 | 3.038 | 140.322 | 0.615 | 0.001 | 0.511 | 2.059 |  0.496 |  28631.000 |  28631.000 |  6527868.000 |  28631.000 | GET    | /js/bootstrap.min.js                        |
|   228 | 0.001 | 3.039 | 140.048 | 0.614 | 0.002 | 0.513 | 2.060 |  0.496 |  86351.000 |  86351.000 | 19688028.000 |  86351.000 | GET    | /js/jquery.min.js                           |
|   228 | 0.002 | 3.044 | 139.345 | 0.611 | 0.002 | 0.505 | 2.062 |  0.497 |  16849.000 |  16849.000 |  3841572.000 |  16849.000 | GET    | /css/bootstrap-responsive.min.css           |
|   228 | 0.001 | 3.038 | 138.529 | 0.608 | 0.001 | 0.509 | 2.058 |  0.497 |   1092.000 |   1092.000 |   248976.000 |   1092.000 | GET    | /favicon.ico                                |
|   228 | 0.001 | 3.027 | 137.308 | 0.602 | 0.001 | 0.498 | 2.057 |  0.495 |     93.000 |     93.000 |    21204.000 |     93.000 | GET    | /img/star.gif

やっぱり / 重いけれども、まずはすぐにできそうな assetsまわりからやってみる。

        location ~ ^/(img|css|js|favicon.ico){
            root /home/isucon/webapp/public;
        }
isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
2017/10/09 15:15:16 start pre-checking
2017/10/09 15:15:20 pre-check finished and start main benchmarking
2017/10/09 15:16:16 benchmarking finished
{"pass":true,"score":5258,"success":2380,"fail":2,"messages":["リクエストがタイムアウトしました (POST /stars)"]}

200 score早くなった。

step4 失敗しているリクエストをなくす

isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
{"pass":true,"score":3779,"success":1851,"fail":4,"messages":["starがついていません (GET /)","リクエストがタイムアウトしました (POST /stars)","讃岐典侍日記 に 1109年 へのリンクがありません (GET /)"]}

これを見ると POST /stars でtimeoutしている。

    post '/stars' do
      keyword = params[:keyword]
      isuda_keyword_url = URI(settings.isuda_origin)
      isuda_keyword_url.path = '/keyword/%s' % [Rack::Utils.escape_path(keyword)]
      res = Net::HTTP.get_response(isuda_keyword_url)
      halt(404) unless Net::HTTPSuccess === res

POST /stars では GET /keyword/:keyword を実行している。 GET /keyword/:keyword の検索がなかなか遅いので、こいつをなんとかしなければならない。

root@ubuntu-xenial:~# alp --sum -r -f /var/log/nginx/access.log --aggregates='/keywords/.*'
|     8 | 1.287 | 3.497 |   19.388 | 2.423 | 1.287 | 2.553 | 2.927 |  0.671 |  11924.000 |  11928.000 |     95408.000 |  11926.000 | GET    | /keyword/巨大基数

なぜ遅いのか見てみる。

mysql> desc star;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| keyword    | varchar(191)        | NO   |     | NULL    |                |
| user_name  | varchar(191)        | NO   |     | NULL    |                |
| created_at | datetime            | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+

keywordにindexが貼られていない。keywordにindexを貼っておく。

isucon@ubuntu-xenial:~/isucon6q$ ./isucon6q-bench -target http://127.0.0.1
2017/10/09 16:01:56 start pre-checking
2017/10/09 16:01:59 pre-check finished and start main benchmarking
2017/10/09 16:02:56 benchmarking finished
{"pass":true,"score":5671,"success":2476,"fail":0,"messages":[]}

300 scoreあがった。

5step get /をなんとかする。

root@ubuntu-xenial:~# alp --sum -r -f /var/log/nginx/access.log --aggregates='/keywords/.*'
+-------+-------+-------+---------+-------+-------+-------+-------+--------+------------+------------+--------------+------------+--------+-------------------------------------------------------+
| COUNT |  MIN  |  MAX  |   SUM   |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY)  | MAX(BODY)  |  SUM(BODY)   | AVG(BODY)  | METHOD |                          URI                          |
+-------+-------+-------+---------+-------+-------+-------+-------+--------+------------+------------+--------------+------------+--------+-------------------------------------------------------+
|   101 | 0.709 | 7.417 | 345.768 | 3.423 | 0.709 | 3.317 | 6.436 |  1.414 |   7960.000 | 162448.000 |  5027564.000 |  49777.861 | GET    | /                                                     |
      keywords = db.xquery(%| select keyword from entry order by character_length(keyword) desc |)
      entries.each do |entry|
        entry[:html] = htmlify(keywords, entry[:description])
        entry[:stars] = load_stars(entry[:keyword])
      end

ここを、下記のようにできるのは無いか。

      keywords = db.xquery(%| select keyword from entry order by character_length(keyword) desc |)
      stars = load_stars(keywords)
      
      entries.each do |entry|
        entry[:html] = htmlify(keywords, entry[:description])
        entry[:stars] = stars[entry[:keyword]]
      end

今日は一旦ここまで

その他

再起動用script

#!/bin/sh

set -e

now=`date +%Y%m%d-%H%M%S`
mv /var/log/nginx/access.log /var/log/nginx/access.log.$now
systemctl restart nginx

mv /var/log/mysql/slow.log /var/log/mysql/slow.log.$now
mysqladmin -uisucon -pisucon flush-logs

systemctl restart isuda.ruby
systemctl restart isutar.ruby

過去問の作業ログ

当日の作業の流れ

環境確認

  • githubでコード管理
  • アプリケーションの動作確認: ブラウザでさわってみる、どんなプロセス動いてるとか
  • 運営側から提供されているベンチマークを実行
  • マシンスペックの確認(core数とか重要そう)

計測