Movatterモバイル変換


[0]ホーム

URL:


LoginSignup
562

Go to list of users who liked

570

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

月額2650円でDBアクセス込み秒間214リクエスト捌くWebサーバ構築事例

Last updated atPosted at 2015-12-21

Python3.5,Flask,Gunicorn,nginx,CentOS7.1,MySQL5.7.1の構成でサーバ構築したら思った以上にスループットが出たので方法を共有します。インフラ屋ではないので、至らぬ設定が多々あると思います。

ソフトウェア構成

ソフトウェア用途
CentOS7.1OS
Python3.5プログラミング言語
FlaskPython Web フレームワーク
GunicornPython WSGI HTTP Server for UNIX
nginxリバースプロキシ
MySQL5.7.1データベース

クラウド破産しないためのサービス選び

同じゲームを作った仲間がクラウド破産しそうになりました。個人で破産したくなかったのでこの時点で従量課金制であるAWSとGoogleCloudは除外。さくらかConoHaかなと思っていたのですが、ConoHaがSSDプランを格安で始めていたのでConoHaを選択しました。昨年お仕事で使ってたAWS-RDSのHDDをSSDに切り替えたらCPU使用率とスループットが大幅に改善したのでSSD万能説を信奉することにしました。

サーバ構成をどう設計するか

オールインワンかDB+APPサーバ構成にするか。サーバを分割した場合DBとAPP間の通信レイテンシが気になります。サーバが異なっていてもconnection poolingをちゃんと設定していれば1-5msで応答が返ってきます。オールインワンで構築すると将来DBサーバとAPPサーバを分割するときDB移管作業がとっても大変になりそうだったので2台構成にしました。

スクリーンショット 2015-12-21 12.39.19.png

サーバスペックと維持費

ConoHaだとサーバ停止→メモリやCPU増設→サーバ起動でスケールアップできるので、困ったらお金で解決出来る点がポイント高いです。計2650円/月。

分類月額CPUメモリ
DB900円/月21GByte
APP1,750円/月32GByte

静的コンテンツの配信

画像データ, css, js といったファイルは動的に変化しないのでユーザの手元にキャッシュさせたいです。HTTP通信時のHEADERにCache-Control: public と Cache-Control: max-age=XXXXXXXXXsec を付与すればブラウザ側で勝手にキャッシュしてくれるのでサーバ負荷の低減に繋がります。HEADERの付与はnginxをリバースプロキシとして動作させて実現しました。

curlコマンドで画像のHTTP-HEADERの検証
>>> curl--head https://www.destinythegame.com/content/dam/atvi/bungie/dtg-comet/home/hero/debris_planet_ground.pngHTTP/1.1 200 OKServer: Apache/2.2.15 (Red Hat)Last-Modified: Tue, 16 Jun 2015 01:56:37 GMTETag: "23f40-274485-51898e2845ef1"Accept-Ranges: bytesContent-Length: 2573445Content-Type: image/pngCache-Control: max-age=3600Date: Mon, 21 Dec 2015 03:52:54 GMTConnection: keep-alive

アプリ側からのDBアクセスを最適化する設計

DBアクセスはローカルネットワーク経由で通信するため、速度がミリ秒単位で非常に遅い世界です。そのため段階別のキャッシュを活用して高速化を計りました。DBアクセスをいかに削減するかが高速化の大きなポイントになると思います。

キャッシュ生存期間キャッシュ先保存内容
request毎メモリ上計算に時間が掛かる処理の結果
無期限にキャッシュgunicornのワーカープロセス上のThreadLocalStorageDB上のマスターデータ

サーバ構築

CentOS7.1, MySQL5.7.1, python3.5, Gunicorn, nginxの順に構築していきました。

CentOS7.1のはまりどころ

2015年3月31日頃にCentOS7.1がリリースされました。iptables が無くなっていたりサービス起動がsystemctl になっていたりと色々ハマりました。ConoHaだと最初ほとんどの通信をfirewalld が止める安全な設定になっていたので、一旦停止して疎通確認してからFWを再設定しました。CentOS6 と勝手が違ったところだけメモしておきました。詳細は専門ブログみた方が安全です。

#外部アクセス制御しているFireWallの停止systemctl stop firewalld#nginxの再起動systemctl restart nginx#サーバ再起動時にサービスを有効にするsystemctl enable nginx#nginxのサービス起動時の設定ファイル/usr/lib/systemd/system/nginx.service

MySQL5.7.1のはまりどころ

2015/10/20にリリースされたMySQL5.7 です。一部機能が5.6系より3倍速になっているらしいですが、罠が多いと悪名高いことでも有名です。詳細は:MySQL 5.7の罠があなたを狙っている

簡単に言うとパスワードポリシーが厳格になって、定期的に変更しないとパスワードが無効になって突然死亡するようになっています。個人PJだったのでパスワードポリシーを無効にし、IPによるアクセス制限を掛けることでセキュリティを担保して回避しました。

/etc/my.conf
# 文字コード設定character-set-server = utf8# パスワードポリシーの無効化validate_password= OFF

※my.confには、これだけ設定して後はデフォルトで動かしています。チューニング余地ありそうです。

罠:mysqlの最初のログインパスワードがわからない

mysql -u root -p したときにパスワードが判らないときの対策

パスワードは起動時にログに記録されています
>>>cat /var/log/mysqld.log |grep password2015-12-17T09:06:57.427343Z 1 [Note] A temporary password is generated for root@localhost: 8#0Ehxxxxxx

こちらの記事が参考になりました。MySQL5.7で遊んでみよう

mysqlで外部からのアクセスを有効にする方法

デフォルトだと外部からアクセスできないようになっているので、ローカルネットワークからのアクセスを許可していきます。パスワードリセット方法がgrant文になっていたのを知らなくて半日ハマりました。辛い。

パスワード無効化とIPによるネットワーク制限
#接続mysql -u root -p#ユーザ一覧select user,host from mysql.user;#root のパスワードと192.168.0.%からのアクセスを有効に設定するgrant all privileges on youre_dbname.* to root@"192.168.0.%" identified by 'パスワード' with grant option;#ユーザ削除drop user 'testuser'@'192.168.0.%';#password resetlocalset password=‘’;#password resetforhostgrant all privileges on youre_dbname.* to root@"192.168.0.1" identified by '' with grant option;grant all privileges on youre_dbname.* to root@"192.168.0.%" identified by '' with grant option;

Python3.5のインストール

2.7構築したときとビルド方法変わっていなかったので、比較的楽に構築できました。

Python3.5構築
#python3.5ビルド準備yum groupinstall "Development tools"yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-develyum install openssl-devel#python3.5installwget https://www.python.org/ftp/python/3.5.0/Python-3.5.0rc4.tar.xztar xf Python-3.5.0rc4.tar.xzcd Python-3.5.0rc4./configure --prefix=/usr/local --enable-unicode=ucs4 --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"make && make altinstall/usr/local/bin/python3.5
Python3.5でvirtualenv環境構築
sudo easy_install pipsudo easy_install virtualenvsudo easy_install virtualenvwrapperpip install pbrsudo easy_install virtualenvwrapperexport WORKON_HOME=$HOME/.virtualenvssource `which virtualenvwrapper.sh`mkvirtualenv --no-site-packages --python=/usr/local/bin/python3.5 your_virtual_env_name

FlaskとGunicornとnginxを繋ぎ込む

nginxでリバースプロキシ立てて、バックエンドでGunicornのWSGI HTTPサーバが応答している構成です。

Flaskと Gunicornを繋ぐ

Gunicorn 、 ‘Green Unicorn’ は、UNIX用のWSGI HTTPサーバーです。 RubyのUnicornプロジェクトから派生したpre-fork workerモデルを採用しています。Flaskでのwsgi対応は簡単なwsgi.py を書くだけと、とっても簡単です。Gunicornはワーカーモデルで動作するためメモリ効率が悪いですが、ThreadLocalStorageをキャッシュに利用しているプログラムを組んでいると、キャッシュがワーカー毎に独立するため設計が単純になります。

wsgi.py
# -*- coding: utf-8 -*-fromappimportcreate_appapp=create_app()if__name__=="__main__":app.run(debug=False)
gunicornの起動コマンド
gunicorn wsgi:app -c .guniconf.py
guniconf.py
importmultiprocessing# Server Socketbind='unix:/run/gunicorn.sock'backlog=2048# Worker Processesworkers=multiprocessing.cpu_count()*2+1# worker数はコア数*2が最適worker_class='sync'worker_connections=1000max_requests=10000# メモリリーク対策 特定リクエスト毎にワーカー再起動timeout=10keepalive=3debug=Truespew=False# Logginglogfile='/var/log/gunicorn/app.log'loglevel='debug'logconfig='/xxxx/config/gunicorn/gunicorn-log.conf'# Process Nameproc_name='gunicorn_app'

■ CentOS7.1の罠 UNIXドメインソケットの置き場所
/tmp/xxxx.sockに設置する事例が多く見受けられますが、CentOS7.1 で/tmp 配下にUNIXドメインソケットを設置するとセキュリティ違反で通信してくれません。正しくは/run/xxxx.sockに設置するのが正解みたいです。この罠のせいで日曜日がつぶれたので一生忘れないと思います。

Gunicornとnginxを繋ぐ

nignx をリバースプロキシとし動作させて、UNIXドメインソケットを使ってgunicornと繋ぎます。gzip圧縮有効, /static 配下の静的コンテンツにはブラウザキャッシュ有効にするためにHTTP HEADERにCache-Control: max-age=2592000 を付与しています。

nginxの起動コマンド
#普通に起動nginx --conf-path ./conf.d/my.conf#systemctlから起動systemctl start nginx.service#停止方法 次の3つのうちどれかnginx --conf-path ./conf.d/my.conf -s stopsystemctl stop nginx.serviceps -ac|grep nginx して killする。
/etc/nginx/nginx.conf
# For more information on configuration, see:#   * Official English Documentation: http://nginx.org/en/docs/#   * Official Russian Documentation: http://nginx.org/ru/docs/user nginx;worker_processes auto;pid /run/nginx.pid;events {    worker_connections 1024;}http{    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '                      '$status $body_bytes_sent "$http_referer" '                      '"$http_user_agent" "$http_x_forwarded_for"';    upstream app_server {        server unix:/run/gunicorn.sock fail_timeout=0;        # For a TCP configuration:    }    server {        client_max_body_size 1m;        server_name *****your domain name*****;        charset utf-8;        keepalive_timeout 10;        sendfile        on;        tcp_nopush     on;        #gzip        gzip_static on;        gzip on;        gzip_http_version 1.0;        gzip_vary on;        gzip_comp_level 1;        gzip_types text/plain                   text/css                   text/xml                   text/javascript                   application/json                   application/javascript                   application/x-javascript                   application/xml                   application/xml+rss;        gzip_disable "MSIE [1-6]\.";        gzip_disable "Mozilla/4";        gzip_buffers 4 32k;        gzip_min_length 1100;        gzip_proxied off;        #open_file_cache        open_file_cache max=1000 inactive=20s;        open_file_cache_valid 30s;        open_file_cache_min_uses 2;        open_file_cache_errors on;        error_log /var/log/nginx/error.log;        access_log /var/log/nginx/access.log;        # Flask static file        location /static/ {            try_files $uri @proxy_to_app_static;        }        # static proxy        location @proxy_to_app_static {            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;            proxy_set_header Host $http_host;            proxy_redirect off;            proxy_pass   http://app_server;            # APPサーバから帰ってきたデータにHEADERを付与            expires 1M;  # 静的コンテンツのブラウザキャッシュ1ヶ月            access_log off;            add_header Cache-Control "public";        }        location / {            # checks for static file, if not found proxy to app            try_files $uri @proxy_to_app;        }        location @proxy_to_app {            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;            proxy_set_header Host $http_host;            proxy_redirect off;            proxy_pass   http://app_server;        }    }}

■ nginx設定ファイルのさらなる改善案
nginxを静的コンテンツ配布サーバとして運用すると、静的ファイル配信するたびにファイルIOが発生しなくなるため、より高速化するみたいです。設定方法がわかりませんでした...

supervisord でGunicorn をデーモン化する

gunicorn -D wsgi:app でデーモン化できますが、プロセスをkill すると止まってしまいます。gunicornプロセス死亡時に自動で復旧させるためにsupervisord をインストールしてgunicorn をデーモン化しました。supervisord は Python3系では動作しないので、virtualenvをdeactivate で解除してsupervisordを利用しています。

supervisordの基本操作
# supervisorctlコマンド長いのでエイリアスを生成alias sctl='/usr/bin/supervisorctl -c /etc/supervisord.conf'# プロセスの再読み込みsctl reloadsctl reread# 全プロセスの開始、再起動、停止sctl start allsctl restart allsctl stop all# gunicornプロセスの起動sctl start gunicorn

supervisordにgunicornプロセスを追加する設定

/etc/supervisord.d/gunicorn.ini
[program:gunicorn]command=sh /var/hogexxxxx/scripts/production_server.shuser=rootautorestart=truestdout_logfile=/var/log/supervisor/gunicorn-supervisord.log ; 標準出力ログstdout_logfile_maxbytes=1MBstdout_logfile_backups=5stdout_capture_maxbytes=1MBredirect_stderr=true

Flaskをgunicornで起動するためのスクリプト

production_server.sh
#!/bin/shGUNICORN=/root/.virtualenvs/***virtual_env_name***/bin/gunicornPROJECT_ROOT=/var/hogeAPP=wsgi:appcd $PROJECT_ROOTexec $GUNICORN-c$PROJECT_ROOT/config/gunicorn/guniconf.py$APP

supervisord をサーバ再起動時に自動起動するよう設定する

systemctl にsupervisord を登録して有効にします。

supervisord登録用の設定ファイル作る
cd /usr/lib/systemd/systemtouch supervisord.service
supervisord.service
[Unit]Description=Process Monitoring and Control DaemonAfter=rc-local.service[Service]Type=forkingExecStart=/usr/bin/supervisord -c /etc/supervisord.confExecReload=/usr/bin/supervisorctl reloadExecStop=/usr/bin/supervisorctl shutdownSysVStartPriority=99[Install]WantedBy=multi-user.target
supervisordを自動起動するように登録
#設定反映systemctl daemon-reload#起動systemctl start supervisordsystemctl status supervisord.service#サーバ再起動時に立ち上がるように設定systemctl enable supervisord

ベンチマークと負荷試験を実施する

期待通りの性能がでるかApache Benchを利用してベンチマークと負荷試験を実施します。ベンチマークでのスループットも重要ですが、ベンチマークで負荷掛かっている際に、実際にブラウザで自サイトを見て快適に閲覧できるかの観点で確認することが重要です。

またApache Bench側が限界 でサーバの能力を引き出せないことも多いです。2台の端末から同時に負荷を掛け結果がどのように変化する見たり、大規模用のLocust みたいな負荷試験ツールを利用するとよいです。

スクリーンショット 2015-12-21 18.19.09.png

DB UPDATEするviewだと、スループットが伸び悩んでいます。

indexページのベンチマーク結果
#同時100並列で1万回アクセスしてベンチマークを取得>>> ab-n 10000-c 100 http://your-site-url/Server Software:        nginx/1.6.3Document Length:        43093 bytesConcurrency Level:      100Time taken for tests:   46.575 secondsComplete requests:      10000Failed requests:        9940   (Connect: 0, Receive: 0, Length: 9940, Exceptions: 0)Write errors:           0Total transferred:      534711748 bytesHTML transferred:       532891566 bytesRequests per second:    214.71 [#/sec](mean)Time per request:       465.750 [ms] (mean)Time per request:       4.657 [ms] (mean, across all concurrent requests)Transfer rate:          11211.59 [Kbytes/sec] receivedConnection Times (ms)              min  mean[+/-sd] median   maxConnect:        2   48 206.2     11    2269Processing:    41  412 328.4    346   14057Waiting:       20  173 205.2    135   13702Total:         46  460 389.5    366   14061
updateページのベンチマーク結果
#同時100並列で1万回アクセスしてベンチマークを取得>>> ab-n 10000-c 100 http://your-site-url/updateServer Software:        nginx/1.6.3Document Length:        52976 bytesConcurrency Level:      100Time taken for tests:   230.742 secondsComplete requests:      10000Failed requests:        9998   (Connect: 0, Receive: 0, Length: 9998, Exceptions: 0)Write errors:           0Total transferred:      531787037 bytesHTML transferred:       529967037 bytesRequests per second:    43.34 [#/sec](mean)Time per request:       2307.418 [ms] (mean)Time per request:       23.074 [ms] (mean, across all concurrent requests)Transfer rate:          2250.67 [Kbytes/sec] receivedConnection Times (ms)              min  mean[+/-sd] median   maxConnect:        2    3  29.7      2    1207Processing:   160 2294 219.1   2269    3416Waiting:      147 2280 217.2   2256    3369Total:        163 2297 220.9   2271    3684

■ 限界まで負荷を掛けると応答速度は大幅に劣化する結果となった
Time per request: 465.750 [ms] (mean)
1並列実行であればTime per request が 50ms前後ですが、100並列では応答速度が465ms と10倍劣化する結果となりました。

■ ApacheBench のFailed requestsについて
Failed requests: 9998
(Connect: 0, Receive: 0, Length: 9998, Exceptions: 0)

失敗理由がLength になっています。これはサイトのコンテンツ長が一致しているかで判定しています。たとえばサイトのコンテンツを動的に変化させているページでは、コンテンツ長が一致しないため失敗扱いとカウントされてしまいます。

deployコマンドを開発する

手動deploy辛いのでコマンド1つでdeploy出来るようにしました。deploy時の試験には、ページ内のリンク切れ確認を行うテストコード を利用しています。

deploy.sh
# !/bin/sh# エラーなら停止set-eu# deployecho"deploy start"ssh-l root conoha"date"ssh-l root conoha"cd /var/hoge && git pull origin master"ssh-l root conoha"/usr/bin/supervisorctl -c /etc/supervisord.conf restart gunicorn"echo"~~~~~~~~~~~~"echo"deploy finish"echo"~~~~~~~~~~~~"# http status check/hoge/.virtualenvs/env_name/bin/py.test /hoge/tests/tests_deploy.pyecho"~~~~~~~~~~~~"echo"deploy test finish"echo"~~~~~~~~~~~~"

HTMLを最適化する

さいごにGoogle DevelopersのPageSpeed Insights でサイトを解析してHTML、CSS、JSのレイヤでページを最適化します。といってもBootstrapといったWebフレームワークを利用していれば、そうそう悪い点数にはならないと思います。

スクリーンショット 2015-12-22 0.40.02.png

まとめ

平均応答速度50msのサイトを自分のスマホで触ってみると、体感できるレベルで応答が早い!速度は正義だと実感できました。格安でSSDサーバを借りられるなんて良い時代になりましたね。さくらクラウドとConoHaクラウドの値段設定はほぼ同じです。今後も利益が出る範囲で価格競争をして頂ければと思います。

今回は応答速度第一で開発を進めてみました。commit時にローカル環境でApacheBenchが自動で走る設定を組み、応答時間が20msを超えないよう気をつけていました。Flaskは何かと物足りないWebフレームワークなので使っているとあれも足りないこれも足りないとなってしまいましたが、応答速度を考慮しながら足りない機能を追加する過程はパズルみたいで楽しかったです。

参考

562

Go to list of users who liked

570
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
562

Go to list of users who liked

570

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?


[8]ページ先頭

©2009-2025 Movatter.jp