あいつの日誌β

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

groonga/docker で http モードで Docker を起動して request を発行すると セマフォが出る

あらすじ

全文検索したくなったので久しぶりに Groonga 使いたいのですが、設定方法を思い出すの面倒なので Docker 化しておこうと思ったら何かがおかしかった。

再現手順

mkdir verify-groonga-dokcer && cd $_
mkdir -p groonga/db
touch docker-compose.yml
touch groonga/Dockerfile

create docker-compose.yml

version: "3" 
services:
  groonga:
    build: "./groonga"
    ports:
      - 10041:10041:wqq

create groonga/Dockerfile

FROM groonga/groonga:latest

RUN mkdir -p /app/groonga/db
WORKDIR /app/groonga

EXPOSE 10041

ENTRYPOINT ["groonga", "--protocol", "http", "-s", -n "./db/groonga.db"]

実行する

% docker-compose build --no-cache 
% docker-compose up --force-recreate
Recreating verifygroongadokcer_groonga_1 ... 
Recreating verifygroongadokcer_groonga_1 ... done
Attaching to verifygroongadokcer_groonga_1

上記の状態で http://localhost:10041/ にアクセスすると GUI の管理画面が表示されるので、そこにで適当に Table を作成して Table を参照しようとすると 139 で groonga が終了してしまう

verifygroongadokcer_groonga_1 exited with code 139

調べていたらこのような issue を発見

どうやら python3.6 manage.py runserver で SIGFAULT が出てるらしい。

https://github.com/docker-library/python/issues/211

そして以下のようにして修正ができるらしい。実際に修正できた。

https://github.com/jubel-han/dockerfiles/blob/master/common/stack-fix.c

解決策: patch を用意して Dockerfile build 時にその patch を適用する

前述の issue と repository を参考に stack-fix.cを用意

#include <dlfcn.h>
#include <pthread.h>
#include <stdio.h>

// THIS IS TO AVOID A SIGFAULT WHEN RUNNING python3.6 manage.py runserver
// This should be fixed at some point by Alpine and/or Python
// Check this issue for more info
// https://github.com/docker-library/python/issues/211
typedef int (*func_t)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg) {

    pthread_attr_t local;
    int used = 0, ret;

    if (!attr) {
        used = 1;
        pthread_attr_init(&local);
        attr = &local;
    }
    pthread_attr_setstacksize((void*)attr, 2 * 1024 * 1024); // 2 MB

    func_t orig = (func_t)dlsym(RTLD_NEXT, "pthread_create");

    ret = orig(thread, attr, start_routine, arg);

    if (used) {
        pthread_attr_destroy(&local);
    }

    return ret;
}

Dockerfile を以下のようにする

FROM groonga/groonga:latest

# Add the patch fix
COPY stack-fix.c /lib/

# Prepare the libraries packages
RUN set -ex \
    && apk add --no-cache  --virtual .build-deps build-base \
    && gcc  -shared -fPIC /lib/stack-fix.c -o /lib/stack-fix.so \
    && apk del .build-deps

# export the environment variable of LD_PRELOAD
ENV LD_PRELOAD /lib/stack-fix.so

RUN mkdir -p /app/groonga/db
WORKDIR /app/groonga

EXPOSE 10041

CMD ["--protocol", "http", "-s", "-n", "./db/groonga.db"]

再び実行して groonga がセマフォを起さない事を確認しました。

% docker-compose build --no-cache 
% docker-compose up --force-recreate

 ところで

こういう問題はすでに解決しているのか、していないのかどこに報告すべきなのかが分かっていませんので有識者の方がいらしたら何卒アドバイスをお願いします。

さくらクラウド上で ISUCON7 予選の環境を構築してみた

あらすじ

ISUCON7 でまたもや予選敗退したんですが来年に備えて復習しておきたいので環境を構築してみました。

やること

[訂正]ディスクプランは HDD ではなくて SSD でした。

さくらのクラウドで 1GB/1仮想コア、ディスクプランは SSD 。これを3台構築。あとスイッチを1台追加してこれとサーバーをつなぎます。host名をそれぞれ app1, app2, app3(db) として、最初に app1 を作ってから app2 と app3 をクローンしていきます。

完成予想図はこんな感じです。さくらのクラウド触った事がある人だったら簡単だと思います。私は初めて触ったのでちょっと手こずりましたが。

f:id:okamuuu:20171024225504p:plain

やった事

ほとんど README.md に書いてあるんですが、まあこの記事は自分の為の備忘録なので...

github.com

ubuntu でログイン

sudo apt update
sudo apt install -y git vim mysql-server nginx
sudo update-alternatives --set editor /usr/bin/vim.basic

isucon ユーザーを作成

isucon ユーザーを作成

sudo adduser isucon

sudo visudo で NOPASS にする。最終行に以下を追記

isucon ALL=NOPASSWD: ALL

isucon ユーザーでログインできるようにする

sudo su isucon
cd
mkdir .ssh/
chmod 700 .ssh
cd .ssh
touch authorized_keys
chmod 600 authorized_keys  # authorized_keys に公開鍵を追加する

ssh秘密鍵でのみログインできるようにする(パスワード・チャレンジレスポンスでの SSH を許可しないにチェックを入れていれば不要)

sudo vi /etc/ssh/sshd_config
#PasswordAuthentication yes
PasswordAuthentication no

ssh 再起動

$ sudo service ssh restart

環境構築

後は以下を参照

https://github.com/isucon/isucon7-qualify

使用言語を Perl に変更

環境が構築できたら使用する言語を systemd に登録します。 Perl の場合は https://github.com/isucon/isucon7-qualify/blob/master/files/app/isubata.perl.service にファイルがあります。

create /etc/systemd/system/isubata.perl.service

[Unit]
Description = isucon7 qualifier main application in perl

[Service]
WorkingDirectory=/home/isucon/isubata/webapp/perl
EnvironmentFile=/home/isucon/env.sh

ExecStart = /home/isucon/local/perl/bin/carton exec plackup -s Starlet -p 5000 app.psgi

Restart   = always
Type      = simple
User      = isucon
Group     = isucon

[Install]
WantedBy = multi-user.target

install cpan modules

cd /home/isucon/isubata/webapp/perl
/home/isucon/local/perl/bin/carton install

[追記]上記の service には /home/isucon/env.sh が存在する事を期待しているので追加しておきます

create /home/isucon/env.sh

ISUBATA_DB_HOST=127.0.0.1 # たぶん後で db に変更する事になる
ISUBATA_DB_USER=isucon
ISUBATA_DB_PASSWORD=isucon

start

sudo systemctl daemon-reload
sudo systemctl enable isubata.perl
sudo systemctl restart isubata.perl

ベンチが動作することを確認したら サーバーを2台 clone します。 clone するときに ファイルを修正する、にするとIPアドレスMACアドレスが変更されます。この時ファイルを修正する、にチェックを忘れると同一のMACアドレスが存在する事になりややこしい事になりますのでご注意ください。もし間違えたらクローンからやり直したほうが簡単だと思います。

プライベートネットワーク用スイッチを作成

スイッチを作成した後サーバーの Nic とスイッチを接続します。 サーバーを停止する必要があります。

ローカルネットワーク側サーバのネットワーク設定

https://knowledge.sakura.ad.jp/1272/

スイッチと接続した後に以下を各サーバーで実行します。

sudo ifconfig eth1 192.168.101.1/24 # app1
sudo ifconfig eth1 192.168.101.2/24 # app2
sudo ifconfig eth1 192.168.101.3/24 # app3 db

[追記]上記だと再起動後に設定が消えてしまうので以下のように /etc/network/interfaces を修正します。

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet static
address 59.106.xxx.xxx
netmask 255.255.255.0
gateway 59.106.xxx.xxx

### 追加する項目
auto eth1
iface eth1 inet static
address 192.168.101.1  # app1 の場合は末尾が1
netmask 255.255.0.0

反映させる

sudo /etc/init.d/networking restart 

各サーバーの /etc/hosts に以下を追記して名前解決できるようにします。

192.168.101.1   app1
192.168.101.2   app2
192.168.101.3   app3 db 

app1 と app 2 の MySQL を停止する。nginx, mysql を disable するとき若干 warning が出ます。

sudo systemctl stop mysql
sudo systemctl disable mysql

app3 の nginx と起動している isubata.perl.service を停止する

sudo systemctl stop nginx
sudo systemctl disable nginx

最後にベンチ

ベンチマークはリモートからだけどここでは app1 で実行する事にします。 注意点として プライベートネットワークではなく、外部IPを指定します。

プライベートとグローバルでは帯域が違うためスコアに影響が出ると思われます。

./bin/bench -remotes=59.106.xxx.xxx,59.106..xxx.xxx -output result.json

スコアだけみたい場合

./bin/bench -remotes=59.106.xxx.xxx,59.106.xxx.xxx -output result.json && cat $_ | jq .score

というわけで

これから予選を突破した人たちのブログを読んでしっかり復習したいと思います。

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)"]}

おしまい。

Bitbukcet で Access key を登録して Read only な repository に対して git clone する

もう何度も同じ失敗をしているのでメモ

鍵の登録は問題ないかを確認

ssh -T git@bitbucket.org

git clone は git@bitbucket.orgユーザ名/リポジトリ名.git: で繋ぐ

git clone git@bitbucket.org:ユーザ名/リポジトリ名.git

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 . がおすすめです。というお話です。