あいつの日誌β

働きながら旅しています。

ISUCON7 に参加して相変わらず予選敗退しました。

相変わらず予選落ちしたのですが来年も ISUCON あるならきっと出場するので一年後の自分に向けて記録を残します。

事前にやったこと

ISUCON6 の予選を一度振り返って Nginx の設定や Perl のインストール方法を確認しました。

今回のお題: チャットアプリ

今回のお題は WEB2 + DB1 の構成のチャットアプリでした。

最初に SSH でログインしてよく使うコマンドをインストールしました。で cat /proc/version, cat /proc/cpuinfo, cat /proc/meminfo を実行したらどのサーバーもそんなに性能高くない。前回のようにアプリケーション側がボトルネックになるのかな、と思いました。

で、最初は WEB1 + DB1 でベンチまわしながら改修してやる事がなくなったら WEB2 + DB1 にしてベンチを回す事にしました。

使用言語: Perl

チャットツールなので Node.js にしようか迷ったんですが、まあ改善していてそっちがいいなら後で乗り換える事にして使用言語を Perl にしました。

Perl にして自前のProfiler を使ってボトルネックを探す。GET /icons/:file_nameGET /fetch に問題があることがわかったのでここから着手。

$VAR1 = { 
          'Isubata::Web => GET /add_channel' => '0.000207901000976562',
          'Isubata::Web => GET /profile/:user_name' => '0.0258147716522217',
          'Isubata::Web => POST /login' => '0.0591163635253906',
          'Isubata::Web => GET /message' => '0.823127031326294',
          'Isubata::Web => POST /profile' => '0.0322229862213135',
          'Isubata::Web => GET /icons/:file_name' => '42.7363834381104',
          'Isubata::Web => POST /register' => '0.0101079940795898',
          'Isubata::Web => GET /channel/{channel_id:[0-9]+}' => '0.0515058040618896',
          'Isubata::Web => GET /fetch' => '20.0142226219177',
          'Isubata::Web => POST /message' => '0.0495190620422363'
        };  

top -c で WEB と DB を見てたら DB が悲鳴を上げている。コードを見たら画像データが DB に入っている。普通に考えたら Nginx に静的ファイルとして返すようにさせるべきなのですがとりあえず手っ取り早く WEB 側に Redis を入れることにしました。

これで 9000 点ぐらい。

fetch に関しては Index を貼るだけ。ちなみに sleep(1) という関数があったのでよくわからずコメントアウトしてしまいました。これは後々意味を持つ関数だったようです。私はそこまでたどり着けませんでしたが...

これで 20000 点ぐらい。

ベンチが静的ファイルを返すのが遅いというので Nginx が返すように設定すると今度は GET /icons/:file_nameタイムアウトするというメッセージ

なんだけどマシーンのリソースはまだ限界じゃない。どうしたらいいんだろう...と停滞。

304 ではないだろうか?

ベンチを回しながらブラウザで GET /icons/:file_name を連打してみたら確かに遅い。なんだけど違和感。ブラウザがキャッシュしていない。

そうだ304だ。ベンチマークが Requset ヘッダーに 'if-modified-since' をつけるんじゃないかと思い、ネットで検索した適当な日付を入れてみた。

$c->res->header('Last-Modified','Sat,22 Feb 2015 12:01:19 GMT');

ただし、これではベンチマークif-modified-since を送ってくれなかった。ブラウザでアクセスすると送ってくれるのですが...という事で 304 で対処できないと思いこむ。

競技終了後で他の人の話しを聞いてところどうやら response の時に DateLast-Modified の整合性がとれてないと if-modified-since を送っていないのかも。後日要検証。

そんなこんなで 304 で対処する以外に道があるのか!?いや、なくない?いや、きっとある!みたいなせめぎ合いを一人で始めて急にネットで画像配信の記事を読んだりして他に方法がないのか模索開始してそのまま迷宮入り。

あとは WEB2 + DB1 の構成にしてベンチをとって 30000点 ぐらい。拡張子の処理を正規表現で書き換えたりして少し早くしようとしましたが焼け石に水

そして競技終了。

感想

普段リクエストヘッダーの事までは意識しないので今回のお題は大変勉強になりました。競技用のサーバー構築に非常に苦労されたようで、運営の皆様は大変お疲れ様でした。

来年も ISUCON 開催されたら、また挑戦させて頂きたいので何卒よろしくお願い致します。

ISUCON7 に向けて ISUCON6 の予選を復習するために環境を用意する

あらすじ

というわけで ISUCON6 予選の環境を再現したいと思います。当日行われた環境とはディレクトリ構成などが違うと思いますがそのあたりはご容赦ください。

あと PHP はよくわからなかったので除外しました。

Azure で以下の VM を用意

私は今回 Azure を用意しましたが良いスペックの PC をお持ちの方は VirtualBox でも良いのかもしれません。

OS: Ubuntu Server 16.04 LTS
サイズ: Standard DS2 v2
ユーザー名: isucon
IP: 静的に変更
受信規則: HTTP を追加

ansible を読み解く

おそらく起点はこちら。 https://github.com/isucon/isucon6-qualify/blob/master/provisioning/image/init.sh

なんですが https://github.com/isucon/isucon6-qualify/issues/6 にあるように現在は少し Ansible が想定とは違う構造になっているようです。issue にあるように target を dev にする必要があるのと、それでもエラーが出る場所があるので修正をします。

エラーが出る場所1: bin の位置

https://github.com/isucon/isucon6-qualify/blob/master/provisioning/image/ansible/04_deploy.yml#L10

エラーが出る場所2: PHP

https://github.com/isucon/isucon6-qualify/blob/master/provisioning/image/ansible/04_deploy.yml#L10

構築手順

最初に最低限必要なコマンドを入手します。ack-grep は私の趣味です。

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install -y --no-install-recommends ansible git aptitude ack-grep

Ansible

git clone

cd /home/isucon
git clone https://github.com/isucon/isucon6-qualify.git

bin が当初想定していた場所とは異なっているようです。あとPHPがなぜかエラーになる。ということで以下の修正を加えます。

edit isucon6-qualify/provisioning/image/ansible/04_deploy.yml

--- a/provisioning/image/ansible/04_deploy.yml
+++ b/provisioning/image/ansible/04_deploy.yml
@@ -7,7 +7,7 @@
     - dev
   tasks:
     - synchronize: src=../../../webapp/ dest=/home/isucon/webapp owner=no group=no links=yes
-    - synchronize: src=../../../bin/ dest=/home/isucon/bin owner=no group=no
+    - synchronize: src=../../../webapp/bin/ dest=/home/isucon/bin owner=no group=no
     - synchronize: src=../files/env.sh dest=/home/isucon/env.sh owner=no group=no
     - file: path=/home/isucon owner=isucon group=isucon state=directory recurse=yes
       become_user: root
@@ -35,19 +35,19 @@
       shell: PATH=/home/isucon/.local/go/bin:$PATH GOROOT=/home/isucon/.local/go GOPATH=/home/isucon/webapp/go make all
       args:
         chdir: /home/isucon/webapp/go
-    - name: composer

-      get_url:
-        url: http://getcomposer.org/composer.phar
-        dest: /home/isucon/webapp/php/composer.phar
-        mode: 0755
-    - name: php
-      shell: PATH=/home/isucon/.local/php/bin:$PATH php composer.phar install
-      args:
-        chdir: /home/isucon/webapp/php
-    - name: php-fpm (isuda)
-      copy: src=../files/isuda.php-fpm.conf  dest=/home/isucon/.local/php/etc/isuda.php-fpm.conf  owner=isucon group=isucon mode=644
-    - name: php-fpm (isutar)
-      copy: src=../files/isutar.php-fpm.conf dest=/home/isucon/.local/php/etc/isutar.php-fpm.conf owner=isucon group=isucon mode=644
+#    - name: composer
+#      get_url:
+#        url: http://getcomposer.org/composer.phar
+#        dest: /home/isucon/webapp/php/composer.phar
+#        mode: 0755
+#    - name: php
+#      shell: PATH=/home/isucon/.local/php/bin:$PATH php composer.phar install
+#      args:
+#        chdir: /home/isucon/webapp/php
+#    - name: php-fpm (isuda)
+#      copy: src=../files/isuda.php-fpm.conf  dest=/home/isucon/.local/php/etc/isuda.php-fpm.conf  owner=isucon group=isucon mode=644
+#    - name: php-fpm (isutar)
+#      copy: src=../files/isutar.php-fpm.conf dest=/home/isucon/.local/php/etc/isutar.php-fpm.conf owner=isucon group=isucon mode=644
 
 - hosts: all
   gather_facts: no

この状態で ansible を実行します。 target に dev を指定する事によって冒頭に説明した問題を回避しています。

cd ~/isucon6-qualify/provisioning/image/ansible/
PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ansible-playbook -i localhost, *.yml --connection=local -t dev

DB setup

DB を初期化します。

cd ~/isucon6-qualify/provisioning/image/
sh db_setup.sh

この状態で IP を直接叩くと Isuda が表示されると思います。

bench tool を用意

ベンチマークツールを同一ホスト上に設置する

.bashrc に以下を設定します。

echo 'export PATH=~/.local/go/bin:$PATH' >> ~/.bashrc
echo 'export GOROOT=/home/isucon/.local/go' >> ~/.bashrc
echo 'export GOPATH=/home/isucon/gocode' >> ~/.bashrc
echo 'export PATH=$GOROOT/bin:$PATH' >> ~/.bashrc
echo 'export PATH=$GOPATH/bin:$PATH' >> ~/.bashrc
echo 'export PATH=$HOME/isucon6-qualify/webapp/bin:$PATH' >> ~/.bashrc

環境変数を適用させる

exec $SHELL -l

edit ~/.gitconfig

[ghq]
    root = ~/gocode/src

isntall

go get github.com/motemen/ghq
ghq get https://github.com/isucon/isucon6-qualify.git

ビルド

cd /home/isucon/gocode/src/github.com/isucon/isucon6-qualify/bench
go get
go build

最初のベンチ結果

$ ./bench -datadir=data -target=http://localhost
{"pass":false,"score":0,"success":0,"fail":0,"messages":["初期化リクエストに失敗しました"]}

もう一度実行

$ ./bench -datadir=data -target=http://localhost
2017/10/18 15:46:39 start pre-checking
2017/10/18 15:46:45 pre-check finished and start main benchmarking
2017/10/18 15:47:39 benchmarking finished
{"pass":true,"score":3233,"success":1568,"fail":4,"messages":["リクエストがタイムアウトしました (POST /keyword)","リクエストがタイムアウトしました (POST /login)","リクエストがタイムアウトしました (POST /stars)"]}

おしまい。

ubuntu で useradd したときに password をデフォルトで設定したいので expect を使う

ubuntuadduser を実行すると対話モードが発生します。

USER_NAME=okamuuu # 適宜変更してください
sudo adduser $USER_NAME

こんな感じの対話モードが始まります。

Adding user `okamuuu' ...
Adding new group `okamuuu' (1002) ...
Adding new user `okamuuu' (1002) with group `okamuuu' ...
Creating home directory `/home/okamuuu' ...
Copying files from `/etc/skel' ...
Enter new UNIX password:  # パスワード入力
Retype new UNIX password: # パスワード入力(確認)
passwd: password updated successfully
Changing the user information for okamuuu
Enter the new value, or press ENTER for the default
        Full Name []:    # Enter連打
        Room Number []: 
        Work Phone []: 
        Home Phone []: 
        Other []: 
Is the information correct? [Y/n] Y  # Yes
# 終了

こんな感じの createuser.sh を追加するとよいでしょう。

if [ $# -ne 1 ]; then
  echo "実行するには1個の引数が必要です。" 1>&2
  exit 1
fi

USER_NAME=$1
PASSWORD=hogehogehoge

echo ${USER_NAME}

sudo expect -c "
spawn adduser ${USER_NAME}
expect \"Enter new UNIX password:\"
send -- \"${PASSWORD}\n\"
expect \"Retype new UNIX password:\"
send -- \"${PASSWORD}\n\"
expect \"Full Name\"
send -- \"\n\"
expect \"Room Number\"
send -- \"\n\"
expect \"Work Phone\"
send -- \"\n\"
expect \"Home Phone\"
send -- \"\n\"
expect \"Other\"
send -- \"\n\"
expect \"Is the information correct?\"
send -- \"Y\n\"
interact

こんな感じで実行

sh createuser.sh okamuuu

おしまい。

docker build でやたらと時間がかかるのは .wercker が原因だったっていうお話

こんな感じのコマンド実行したら生成される Docker Image のサイズがおかしい

docker build -t myapp .
Sending build context to Docker daemon  4.321GB

echo "node_modules" >> .dockerignore したけど相変わらず Image のサイズが大きい。なんだろうとおもったら wercker-cli 操作した時にできた .wercker が原因だった

% du -d1 -x .
28768   ./.git
16      ./.storybook
9176296 ./.wercker
685064  ./node_modules
72      ./public
360     ./src
16      ./stories
5928    ./storybook-static
9899200 .

du -s * だと dotfiles が表示されないので du -d1 -x . がおすすめです。というお話です。

Nginx + Node.js + React.js. + Mongodb を Docker で構築する

あらすじ

Nginx + Node.js + React.js + Mongodb で Web Application の構築手順を説明する必要があるのですが、Dockerfile をドキュメントとして扱う事になりました。

% docker --version
Docker version 17.06.2-ce, build cec0b72

% node -v
v8.1.0

% create-react-app --version
1.4.0

% mongo --version | head -n 1                                                                           MongoDB shell version v3.4.9

準備

一応 Todos アプリのような感じの Web API を backend で用意して React がそのデータを描画する、までの簡単なサンプルアプリを作成しつつ Docker について説明します。

先ずは以下のコマンドを実行して雛形を作っておきます。

cd 
mkdir practice-docker-provision && cd $_
mkdir backend mongodb mongo_seed nginx
create-react-app front
touch docker-compose.yml

mongodb

create docker-dompose.yml

version: "3"
services:
  mongodb:
    image: mongo:latest
    environment:
      - MONGO_DATA_DIR=/data/db
      - MONGO_LOG_DIR=/dev/null
    ports:
      - 27017:27017
    command:
      - mongod

docker-compose run --build を実行して mongod を起動します。起動したらアクセスできる事を確認します。

% mongo --quiet                                                                                                                   > show dbs
admin  0.000GB
local  0.000GB
> exit

mongo_seed

次に mongod を起動する度に database へ初期データを import するようにします。

mongod を起動した後、mongod へ初期データをインサートします。

mkdir mongo_seed/json
touch mongo_seed/Dockerfile mongo_seed/json/todos.json

create mongo_seed/json/todos.json

[
  { title: "todo 1", done: false},
  { title: "todo 2", done: false},
  { title: "todo 3", done: false}
]

create mongo_seed/Dockerfile

FROM mongo:latest

COPY json json

CMD mongoimport --host mongodb --db myapp --collection todos --drop --jsonArray --file ./json/todos.json

edit docker-compose.yml

     command:
       - mongod
+
+  mongo_seed:
+    build: mongo_seed
+    links:
+      - mongodb
+    depends_on:
+      - mongodb

docker-compose up --build を実行してから動作確認をします。

% mongo --quiet
> show dbs
admin  0.000GB
local  0.000GB
myapp  0.000GB
> use myapp
switched to db myapp
> db.todos.find()
{ "_id" : ObjectId("59d1fd6e73e8aa5ae594a04c"), "title" : "todo 1", "done" : false }
{ "_id" : ObjectId("59d1fd6e73e8aa5ae594a04d"), "title" : "todo 3", "done" : false }
{ "_id" : ObjectId("59d1fd6e73e8aa5ae594a04e"), "title" : "todo 2", "done" : false }
> exit
%

Node.js

cd ~/practice-docker-provision/backend
yarn init -y
yarn add express mongoose --save
touch app.js models.js

create app.js

var express = require('express');
var app = express();
var mongoose = require('mongoose');
var databaseUrl = process.env.MONGO_DATABASE || "mongodb://localhost/myapp"
var Todo = require('./models').Todo;

mongoose.connect(databaseUrl, {useMongoClient: true});

app.get('/api/todos', function(req, res) {
  Todo.find().exec((err, todos) => {
    if (err) {
      res.send(err)
      return
    }   
    res.json(todos)
  })  
});

app.listen(3000);

edit models.js

var mongoose = require('mongoose');

const Todo = mongoose.model('Todo', {
  title: {
    type: String,
    default: "", 
  },  
  done: {
    type: Boolean,
    default: false
  }
});

module.exports = { Todo: Todo }

edit backend/Dockerfile

FROM node:8

WORKDIR /usr/src/app

COPY package.json .
RUN yarn install

COPY . .

edit package.json

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node app.js"
  },  
  "dependencies": {
    "express": "^4.16.1",
    "mongoose": "^4.11.14"
  }
}

edit docker-compose.yml

version: "3"
services:
  mongodb:
    image: mongo:latest
    environment:
      - MONGO_DATA_DIR=/data/db
      - MONGO_LOG_DIR=/dev/null
    ports:
      - 27017:27017
    command:
      - mongod

  mongo_seed:
    build: mongo_seed
    links:
      - mongodb
    depends_on:
      - mongodb

  backend:
    build: "backend"
    environment:
      - NODE_ENV=production
      - MONGO_DATABASE=mongodb://mongodb/myapp
    ports:
      - "3000:3000"
    links:
      - mongodb
    depends_on:
      - mongodb

docker-compose up --build を実行してから動作確認をします。

% curl -s http://localhost:3000/api/todos | jq .                                                 [
  {
    "_id": "59d201da7e61015c1651b11f",
    "done": false,
    "title": "todo 1"
  },
  {
    "_id": "59d201da7e61015c1651b120",
    "done": false,
    "title": "todo 3"
  },
  {
    "_id": "59d201da7e61015c1651b121",
    "done": false,
    "title": "todo 2"
  }
]

React.js + Nginx

React.js を build します。

cd practice-docker-provision/front
yarn run build

front/build にファイルが生成されている事を確認して下さい。

次に Nginx の config と Dockerfile を準備します。

create nginx/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    # include /etc/nginx/conf.d/*.conf;

    server {
      listen 80;

      # server_name localhost;

      gzip on;
      gzip_types *;

      location /api/ {
        proxy_pass http://backend:3000/api/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection ‘upgrade’;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
      }

      location / {
        root /app;
        index index.html;
        try_files $uri $uri/ /index.html;
      }

   }
}

create nginx/Dockerfile

FROM nginx:1.13.0

RUN mkdir /app

COPY ./nginx.conf /etc/nginx/nginx.conf

CMD ["nginx", "-g", "daemon off;"]

edit docker-compose.yml

       - mongodb
     depends_on:
       - mongodb
+
+  nginx:
+    build: "nginx"
+    ports:
+      - "8080:80"
+    volumes:
+      - ./front/build:/app:ro

React.js から backend の API への通信をする

すでに Docker の設定は完了していますが、最後に React.js を修正します

cd ~/practice-docker-provision/front
yarn add axios --save

edit src/App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import axios from "axios";

class App extends Component {

  constructor() {
    super();
    this.state = {
      todos: []
    };
  }

  async componentDidMount() {
    const res = await axios.get("/api/todos")
    this.setState({todos: res.data || []})
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <ul>
        {this.state.todos.map((todo, index) => (
          <li key={index}>{todo.title}</li>
        ))}
        </ul>
      </div>
    );
  }
}

export default App;

再度 yarn build して動作確認で終了です。

Docker で Nginx を起動して proxy させようとしたら host not found in upstream と言われる件

この書き方だとそうなった

FROM nginx:1.13.0

RUN mkdir /app

COPY ./nginx.conf /etc/nginx/nginx.conf

RUN service nginx start

正しくはこう。service は Docker の中で使ったらダメ、絶対。

FROM nginx:1.13.0

RUN mkdir /app

COPY ./nginx.conf /etc/nginx/nginx.conf

CMD ["nginx", "-g", "daemon off;"]

こちらからは以上です。