巨人の足元でたじlog

そうして言葉を軽んじるから――― 君は私の言葉を聞き逃す

docker-composeのコンテナ内でcronを動かせたけど、railsのrakeタスクが実行できない

昨日、コンテナ内でcronを実行すること自体は成功したのだが、どうもrailsアプリのrakeタスクを実行する際に、(おそらく)権限の問題で実行できていなかったので、それを修正する。

目標は1時間以内(本当は30分で終わらせたいところ)

まずは現状を確認。 containerにexecする。ホストにて。

$ docker-compose exec app bash

rootでbashにログインできました。私はrootです。

# whoami
root

crobntabを確認する

crontab -e 

→なにもない。(コメントがありますが、有効な記述はない。)

次に、/etc/cron系を調べてみます。

/etc/crontab

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --reporrt /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --reporrt /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --reporrt /etc/cron.monthly)

これはデフォルトで入っていたやつでした。

他の/etc/cron系のファイルを見てみても、(見てないですが)なさそうだったので、特にcron系は設定されていない状態なはずです。

ここから、一つずつ動作を確かめていきませう。

進め方を決めます。
テスト環境にて
1. crontabの動作を確認する(権限必要ないファイルに書き込み)
2. railsのdummyのrakeタスクを実行.ログをファイル出力
3. 本番のrakeタスクを実行。
4. Dockerfile変更→build→run→cronの動作確認
5. 本番に反映

テスト(ローカル)環境と本番環境をごっちゃにしていたので、どこのcrnotabが有効かわからなくなってしまっていた。

テスト環境のコンテナのcronの状況確認

crontab -l
* * * * *   echo "Hello $(date)" >>/var/log/cron.log 2>&1
* * * * *   /usr/local/bundle/bin/rake status:testman  >>/var/log/cron.log 2>&1
* * * * *   pwd >> /var/log/cron.log 2>&1

一番上を確認する。 これ以外は一旦コメントアウトにする。

* * * * *   echo "Hello $(date)" >>/var/log/cron.log 2>&1

出力先をきれいにしておきます

rm -rf /var/log/cron.log

cron実行想定時間後

# cat  /var/log/cron.log
Hello Wed Sep  5 14:59:01 UTC 2018

OKです。ファイルの書き込み(なければ作成)もOKでした。 以下のcronだけを残します。

* * * * *   /usr/local/bundle/bin/rake status:testman  >>/var/log/cron.log 2>&1

ちなみにrakeタスクのstatus:testmanは以下

desc "test"
task :testman => :environment do
    File.open("db/hoge.csv","a") do |f|
    f.puts(Time.now)
    end
end

コレ自体はちゃんと動作することは確認した。

しかし、cronが動かない。 出力先の/var/log/cron.logを見てみると、

/usr/bin/env: 'ruby': No such file or directory

予想: rakeタスクのパスがなんかどっかおかしいんだろ?

それっぽいstack overflow
やっぱりそれっぽいことを言われている。
$HOMEにpathを追加すれば行けるよ的なこと言っているが、あんまりいじりたくない。いじったらその分Dockerfileもいじらなくてはいけなくなるので、厄介。絶対厄介。

まずコンテナ内のrubyのパスを確認してみる。

# which ruby
/usr/local/bin/ruby

...しかしこれがわかって何になるのか。


bundle execすればいい話?
ちょっとdocker-composeのマウントとかにいろいろ紛れてvendorの場所をあまり意識していなかったが、それか?

printenvで表示されるpathに
/usr/local/binは含まれているので、rubyを探せる。

あ、よし。
cronにprintenvさせればいいのだな。

HOME=/root
LOGNAME=root
PATH=/usr/bin:/bin
SHELL=/bin/sh
PWD=/root

やはり、pathがなかった。
正当な解決までの道のりは遠そうだったので、次の方針で進める。
cronでsh /opt/hoge.sh >> /var/log/cron.log
として、
/opt/hoge.shファイルにて

export PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/local/bundle/bin/rake status:testman

これで行けそう。

# sh /opt/cron.sh

これは成功した!
次に、cronから呼び出す。

だめ。
/var/log/cron.log

rake aborted!
No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)

おっとこれは、rakeファイルを実行する場所が悪かったようでした。
/opt/cron.sh

export PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /webapp
/usr/local/bundle/bin/rake status:testman

場所を移動して実行するように変更.

rake aborted!
Bundler::GemNotFound: Could not find rake-12.3.1 in any of the sources
/webapp/config/boot.rb:3:in `<top (required)>'
/webapp/config/application.rb:1:in `require_relative'
/webapp/config/application.rb:1:in `<top (required)>'
/webapp/rakefile:4:in `require_relative'
/webapp/rakefile:4:in `<top (required)>'
(See full trace by running task with --trace)

わからん。
bundle execしないとだめってこと?
一旦それでやってみるよ?
/opt/cron.shを以下に変更

export PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /webapp
/usr/local/bin/bundle exec rake status:testman

結果、だめ。
/var/log/cron.log

bundler: failed to load command: rake (/usr/local/bundle/bin/rake)
Bundler::GemNotFound: Could not find rake-12.3.1 in any of the sources
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/spec_set.rb:91:in `block in materialize'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/spec_set.rb:85:in `map!'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/spec_set.rb:85:in `materialize'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/definition.rb:171:in `specs'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/definition.rb:238:in `specs_for'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/definition.rb:227:in `requested_specs'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/runtime.rb:108:in `block in definition_method'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/runtime.rb:20:in `setup'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler.rb:107:in `setup'
  /usr/local/lib/ruby/site_ruby/2.5.0/bundler/setup.rb:20:in `<top (required)>'
  /usr/local/lib/ruby/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
  /usr/local/lib/ruby/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'

これはなんだ。

これは/otp/cron.shが悪い。
そもそも、bashからsh /opt/cron.shを実行しても同様のエラーになるので、cornの罠とかじゃなく、bundle exec とかは必要ないはず。

ってかそもそもbundle execなしで普通にbashからrakeタスク実行できていたので、同じrootユーザーなのでできるはず。
考えよう。

考えてもわからん。
無能すぎて泣ける。
明日は時間トレなさそうなのでもう諦める。
むしろ一回一回コンテナを起動してしまうことは甘んじて受け入れて、ホストのcronで不なコンテナを消し込みに行こうかな。

全然解決できなさすぎて、セーブポイントから結構進めてたどり着く最初の中ボスみたいなの10回くらい倒せなくて諦めた小5くらいにやってた犬夜叉のゲーム思い出したわ。

docker-composeのコンテナ内でcronを動かす

nginx+rails+PostgreSQLなアプリをdocker-composeで作っていた。
railsのアプリケーションでcronで実行したいrakeタスクがあった。
このタスクのcron化自体はrails(ruby)のgemで行うのだけど、うまく行かなかった。
原因としては、そのgemも結局は、railsアプリ内ではなくそれがのっているホスト(この場合はコンテナ)のcronの仕組みを使うためのラッパーのようなものなので、そもそもコンテナでcronが使えなきゃしょうがない。
結局調べてみたところ、rubyのimageはdebianがベースイメージとして使われていて、そのベースイメージではcronはなかった。
なので、apt-get installして使える必要にしてから、rails側でもcron化するgemを使用すべきなのだけど、面倒くさかったので強硬策に出ていた次第だ。
その方法とは、コンテナのホストから、コンテナごとcronで起動してしまう方法。
これでなんとか凌いでいたのだが、問題が発生。
ホストからcronのためだけにいちいちコンテナを起動して、終わったらそのままにしていたので、使い終わったコンテナがたくさん溜まっていくという現象が発生してしまった。 あくる日容量エラーになってしまったのだった。。。

これは良くないと思い、ちゃんとcronを使っていく。

まずは、動作確認のために、コンテナ上においてcronをinstallして使えるかどうか試してみる。

docker-compose.ymlはこんな感じ。 railsのコンテナでcronを使いたいので、以下のコマンドでシェルに接続で(言葉あってるのか?)

$ docker-compose run app bash

まずはcronをinstallする

# apt-get install cron

install成功したので、早速使ってみる。

# crontab -e
no crontab for root - using an empty one
update-alternatives: error: no alternatives for editor
/usr/bin/sensible-editor: 25: /usr/bin/sensible-editor: editor: not found
/usr/bin/sensible-editor: 28: /usr/bin/sensible-editor: nano: not found
/usr/bin/sensible-editor: 31: /usr/bin/sensible-editor: nano-tiny: not found
/usr/bin/sensible-editor: 34: /usr/bin/sensible-editor: vi: not found
Couldn't find an editor!
Set the $EDITOR environment variable to your desired editor.
crontab: "/usr/bin/sensible-editor" exited with status 1

おっと、editorがない。そんなことがあるもんなんだな。 サクッとインスコしてみる。 適当にそれっぽいコマンドを想像して叩く。

# apt-get install vim

インスコできました。

1 * * * * echo hoge >> /var/log/cron/echo

と書いて保存。
しかし、そもそも/var/log/cronというディレクトリがなかった。

mkdir /var/log/cron

作ってみるも、なしのつぶて。 誰が悪いのか。

と、単に自分のcronがうんこだっただけだった。 以下のcronで。

* * * * *   echo "Hello $(date)" >>/var/log/cron.log 2>&1

いやしかし、これも動かない。 どうやらcrontab -eに問題がありそうだ。

/etc/crontabを編集 以下を追加

* * * * *   echo "Hello $(date)" >>/var/log/cron.log 2>&1

これもだめ、そもそもcronのサービスが起動していないんじゃないか。 それを調べよう。
redhut系で言うところのserviceはdebianでいうなんだっけか。
と思い調べ、結局わからず

# service cron status

と適当に売ってみたら、

[FAIL] cron is not running ... failed!

と表示された。
なんだdebianもserviceで良かったのか。

startさせます。

# service cron start
[ ok ] Starting periodic command scheduler: cron.

よっしゃ成功! 1分待ってcronの実行を確認します。

実行されていました。
くぅ〜これにて完了です。

と言いたいところだが、これをdocker-compose.ymmlに書く。

としたいが、タイムオーバーで、これは明日にやることにする
ここまではローカルのdocker環境で実行していたので、これか本番で一旦cronをコンテナー内に仕込む

そして明日Dockerfileを変更する

laravelのFacadeとserviceクラスがわからない

永遠にわからない。

Ruby on Rails出身の僕。
flaskはわかった。というより、むしろflaskを勉強したことでRuby on Railsの理解も深まった。
flaskだとスクラッチに近い形で書かないと行けないところを、Ruby on Railsだとそこらへんをフレームワーク側ですでに提供してくれている。
これはなんて便利なものなんだと改めて感じた。
flaskではMVCの全部を1つのファイルに収めることもできる。しかしそれは全体が見にくくなるので、普通に作るならやらない。
MVCのありがたみみたいなものも深く理解できた気がする。

しかし、laravelおまえときたら。
Facadeとは何ぞや。Serviceクラスとはなんぞや。Serviceコンテナーとはなんぞや。
新しい概念が多くて納得感がないまま早2ヶ月くらい。
コントローラーが肥大化してきてロジックの流れが見えづらくなるので、部分的に切り出して外にまとめる。という考え方はわかる。
この考え方がFacadeに近いはずだ。
これにあとはDIとかが入ってきて、より便利にラップしてくれて、呼び出し側からすれば単一のシンプルなインターフェースで呼び出せて、裏側ではよしなにやってくれている。
自分が集めた情報だとこんな感じだ。
それはわかるんだが、Serviceクラスとかをかませる理由がわからない。
Laravelのフレームワークがそういう仕様だといえばそれまでだが、そうなのだとしてなぜそんな仕様にしているのか。そうすることで何が嬉しいのか。これが腑に落ちない。 肥大化したコントローラーを切り出して、Facadeにまとめる。それをコントローラーから呼び出す。
これだけでいいじゃないか。なぜServiceというものを挟む必要があるのか。

そもそもFacadeを呼び出す際にいろいろ準備?が必要ということか?
ちゃんとテストコードを書いたことがないために、テストのために想定される実装・機能についてのイメージが湧かないことがこの非理解の原因だろうか。
テストをしやすいようにもっくとかすたぶとかをうまいことやる必要がある。らしい。 それらを呼び出したりするのがServiceクラスということになるのだろうか。

うーむ。来月あたりの自分に、この頃の自分頭わっるぅと言わしめたい。
ということでわからない備忘録終了。

プロセスとスレッドとかタイムスライスとかスケジューリングがわからない

nginxをかませると何がいいのか? その答えを探して、先輩に聞いてみたところキーワードとして「プロセス、スレッド、cpuコア」みたいな話がふんわりと出てきて、しかし自分の方での知識がなさすぎ説明の土台に立てていなかったので、一旦そこら編を整理しておく。

プロセスらへんについては、「ふつうのLinuxプログラミング」という本で読んで、メモリとかうまいことやってくれているというふうに理解した覚えがある。
それぞれのプロセス同士が使うメモリを競合しないように、うまいことやっていると。
一つのプロセスがメモリを使う際には、実際の物理メモリアドレスはどこなのかを意識しない、自分だけのものとして使っていいと、それを誰かが調整してくれている。みたいな認識だった。

http://moro-archive.hatenablog.com/entry/2014/09/11/013520

を読んで、あーそんなこと言ってたなそういえば。と思い出した。

マルチプロセスは、メモリ空間とかごと別の環境ができると。
そのぶん、マルチプロセス内で処理を切り替えるときにはプロセスごとにいちいち仮想アドレスと物理アドレスマッピングをしなきゃいけないと。
なので、その分オーバーヘッドが生じて遅くなる...? 一方でマルチスレッドの場合は、スレッド間でメモリのデータセグメントは共有されている?ので、その分いちいちマッピングをする必要はないので、オーバーヘッドはなくなる?みたいな?

しかしそもそもプロセスなんてのはサーバー上に走っているのでどっちにせよマッピングとかはプロセスが切り替わるごとにやらなきゃいけないんじゃないのか?

いやーわかんない。

そもそも、2つのリクエストがほぼ同時に来たときに、レスポンスを返すための処理がとても重いとする。それをマルチスレッドでさばくと、全体としてのレスポンスの速さは変わるの?
というもの、スレッドが複数個走っているときにも、プロセスはひとつなので、スケジューリングされるタイムスライスの時間は変わらない?はず。とすれば、同じ計算能力に対して仕事が2倍になるのだから、それぞれの仕事の進捗の速さは2分の一になってしまうじゃないか。
というかタイムスライスはどうやって決まるんだ?マルチスレッドで2つのスレッドがたった時点で、タイムスライスが2倍になってくれば問題はないのだが、そこらへんまで踏み込むのは今はやりすぎな気がしている。

明日聞いてみよう。

やっぱりもうちょっと調べてみた。

タイムスライスの長さをシステム負荷に応じて動的に調整します まず、全タスクを一周動作させ るまでのスケジューリング周期を求めます。この値は単一プロセッサ時の周期 (20msec) に CPU 数によって 重み付けをした値です

CPU 数が増える程スケジューリング周期は長く、プロセスが増える程タイムスライスは短くなりま す。これは、CPU 数が増えれば一度に実行出来るプロセス数が増えるのでスケジューリング頻度を増やす必 要が無くなる為です。

https://syuu1228.github.io/process_management_and_process_schedule/process_management_and_process_schedule.pdf

この説明では、プロセスごとにタイムスライスを多めに割り振る方法とかはない。
しかし、topコマンドで負荷を見ながら、yesコマンドを打つと大量にCPUが食われているのがわかる。
ということはやはり処理に応じてタイムスラスは割り振られているんだなぁ。

nginxとかrailsとかdocker-composeの備忘録

nginxのインストール

$ sudo yum update -y
$ sudo yum install -y nginx
$ nginx -v 
nginx version: nginx/1.12.1
$ sudo service nginx status
nginx is stopped
$  sudo service nginx start
Starting nginx:                                            [  OK  ]

xxx.xxx.xxx.xxxにアクセス。
(セキュリティグループで80番ポートを開放しておく)

無事あのページが表示されました。
f:id:benzenetarou:20180728001121p:plain

コンテンツの変更

nginxのドキュメントルートはここらしい /usr/share/nginx/html

参考: [備忘録]nginxのドキュメントルート

ということで、ドキュメントルートのコンテンツを変更してみる。

$ ls -al /usr/share/nginx/html/
合計 24
drwxr-xr-x 2 root root 4096  7月 27 15:06 .
drwxr-xr-x 3 root root 4096  7月 27 15:06 ..
-rw-r--r-- 1 root root 3696  9月 11  2017 404.html
-rw-r--r-- 1 root root 3738  9月 11  2017 50x.html
-rw-r--r-- 1 root root 3770  9月 11  2017 index.html
-rw-r--r-- 1 root root  368  9月 11  2017 nginx-logo.png
lrwxrwxrwx 1 root root   32  7月 27 15:06 poweredby.png -> /usr/share/pixmaps/poweredby.png

特にパスを指定しない場合は、index.htmlが表示される。

$ sudo vi /usr/share/nginx/html/hoge.html

このファイルにhogeと書き込んでおく。
そしてxxx.xxx.xxx.xxx/hoge.htmlにアクセスする。

すると、hogeと表示された。

さて、これで /usr/share/nginx/html/hoge.htmlがドキュメントルートということが確認できたが、この設定ファイルはどこに書いてあるんだろうか。

設定ファイルを探してみる。 どうやらたくさんあるみたいだ。

# ls -al /etc/nginx
total 76
drwxr-xr-x  4 root root 4096 Jul 27 15:06 .
drwxr-xr-x 79 root root 4096 Jul 27 15:06 ..
drwxr-xr-x  2 root root 4096 Jul 27 15:06 conf.d
drwxr-xr-x  2 root root 4096 Sep 11  2017 default.d
-rw-r--r--  1 root root 1077 Sep 11  2017 fastcgi.conf
-rw-r--r--  1 root root 1077 Sep 11  2017 fastcgi.conf.default
-rw-r--r--  1 root root 1007 Sep 11  2017 fastcgi_params
-rw-r--r--  1 root root 1007 Sep 11  2017 fastcgi_params.default
-rw-r--r--  1 root root 2837 Sep 11  2017 koi-utf
-rw-r--r--  1 root root 2223 Sep 11  2017 koi-win
-rw-r--r--  1 root root 3957 Sep 11  2017 mime.types
-rw-r--r--  1 root root 3957 Sep 11  2017 mime.types.default
-rw-r--r--  1 root root 3734 Sep 11  2017 nginx.conf
-rw-r--r--  1 root root 2656 Sep 11  2017 nginx.conf.default
-rw-r--r--  1 root root  636 Sep 11  2017 scgi_params
-rw-r--r--  1 root root  636 Sep 11  2017 scgi_params.default
-rw-r--r--  1 root root  664 Sep 11  2017 uwsgi_params
-rw-r--r--  1 root root  664 Sep 11  2017 uwsgi_params.default
-rw-r--r--  1 root root 3610 Sep 11  2017 win-utf

たくさんあって、どこがそれらしいか探すのが面倒臭そうだ。
とりあえず現在は/usr/share/nginx/html/がドキュメントルートになっているので、その文字列をこのディレクトリ以下から再帰的に検索して探す。

【Linux】複数あるファイルの中から特定の文字列を検索するコマンド | かわたま.net より、いい感じのコマンド。

# find ./* -type f -print | xargs grep 'nginx/html'
./nginx.conf:        root         /usr/share/nginx/html;
./nginx.conf:#        root         /usr/share/nginx/html;

見つかった。
結局モロに一番怪しそうな/etc/nginx/nginx.confにあった。 コメント行をエスケープして見てみると、そこまでバカ多くなさそうで安心。

$ cat nginx.conf | grep -v -E "^\s*#" | grep -v -E "^\s*$"
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
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"';
    access_log  /var/log/nginx/access.log  main;
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;
    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    include /etc/nginx/conf.d/*.conf;
    index   index.html index.htm;
    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /usr/share/nginx/html;
        include /etc/nginx/default.d/*.conf;
        location / {
        }
        error_page 404 /404.html;
            location = /40x.html {
        }
        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}

試しにlisten 80 default_server;の部分を80→8081に変えてみる。

sudo service nginx configtest
sudo service nginx restart

すると、ポート指定なしでアクセスできず、8081ポートでアクセスできた。なるほど。

nginxとプロセスとかの話

nginxをかませることのメリットは、大量のアクセスをさばけるだとか、リダイレクトやらなんやらができるみたいな認識だ。
聞きかじった知識では、プロセスがコアの数だけうんぬんみたいな話だった。
まずは、nginxを挟まないとプロセスが1個だけだよ〜ってのを確認したい。
pythonの軽量フレームワークのflaskで確認する。

これの環境構築は

benzenetarou.hatenablog.com

ここにサクッと書いてある。
今回は、hello.pyを以下のようにする。

import time
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello'

@app.route('/sleep')
def sleep():
    time.sleep(2)
    return 'sleep'

if __name__ == '__main__':
    app.run(host='0.0.0.0')

これで、起動する。

python36 hello.py
 * Serving Flask app "hello" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

これをローカルからcurlする。 2つのシェルスクリプトを用意する。
sleep.sh

echo `date '+%y/%m/%d %H:%M:%S'`
echo ""
for i in `seq 0 99`
do
    curl http://xxx.xxx.xxx.xxx:5000/sleep
done
echo ""
echo `date '+%y/%m/%d %H:%M:%S'`

loop.sh

echo `date '+%y/%m/%d %H:%M:%S'`
echo ""
for i in `seq 0 99`
do
  curl http://xxx.xxx.xxx.xxx:5000
done
echo ""
echo `date '+%y/%m/%d %H:%M:%S'`

これをターミナルを2つ開いて同時に実行させる。 ちなみにloop.shだけを実行すると、

18/07/28 01:39:14

HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello
18/07/28 01:39:20

となった。(使っているwifiは36.4Mbpsをマークしていた)

もしflaskが1リクエストずつしか返せないとしたら、sleep()の処理をしている間はhello()の処理もできないので、sleepの遅さに引きずられてloopも遅くなるはず。

実行結果は、 sleepがちんたらやっている間、loopはloopで勝手にリクエスト&レスポンスを繰り返していて、通常時と殆ど変わらない速さで終了した。

と、いうことは? Flaskはnginxとかかまさなくても、複数の処理を同時にできる。と。
あっれーnginxいらなくね?ってなってしまったので、ちゃんと調べる。

qiita.com

オイオイオイオイホントかよ。。

この記事が言うことにゃあ "デフォルトでは"一つのリクエストのレスポンスを返すまでは、次のリクエストの処理を始められないとな。
ソースコードまんま引用させていただきます。

from flask import Flask, make_response, jsonify
import time

# flask
app = Flask(__name__)

# rest api
@app.route('/hello/<string:value>', methods=['GET'])
def hello(value):
    result = 'hello {0}'.format(value)
    print('[start] ' + result)

    time.sleep(10)

    print('[end] ' + result)
    return(make_response(result))

# main
if __name__ == "__main__":
    # 同時アクセスができない(並列処理が不可能)
    # app.run(host='localhost', port=3000)

    # 同時アクセスができる(並列処理が可能)
    app.run(host='localhost', port=3000 threaded=True)

オイオイオイオイ、あいつ死ぬぜ。
結果、どうやら仕様が変わったようで、デフォルトではthreaded=Trueになっているようでした。
逆にthreaded=Trueと指定することで、リクエストからレスポンスを返すまでが詰まると、どんどんキューが溜まっていくという挙動を確認できた。

ただ、これにnginxをかませてもボトルネックはアプリケーション・サーバーのflaskになると思うので、nginxをかませる利点がまだ腑に落ちない。
本でも買ってみるかな。

と、そもそもなんでこんなnginxについて調べているのかを見失っていた。。。

いいのや、別にC10K問題とか、個人開発では直面せんやろ…
直面できたらラッキーだわむしろ。
ということで、これはタイムカプセル案件にしておく。

本来やりたかったことは、
nginx + unicorn + rails (puma?)をdocker-composeでec2上でデプロイすること。
その前段として、dockerを使わないで上記のデプロイ環境を構築すること。
そして、その前段としてnginxの概要を理解することだった。
いままでapacheですら簡単にしか触ったことなかったし、その使い方も、適当にドキュメントルートをルートディレクトリとして、localのmac動かす感覚でrails -s コマンド叩くみたいな多分間違っているやり方だったっと思う。
ので、ちゃんとウェブサーバーとアプリケーションサーバーの間の設定とか理解しときたい。
一旦、本来やりたいnginx + unicorn + railsをやりにいく。

実際にインストール

https://qiita.com/eighty8/items/0288ab9c127ddb683315 とりあえずこの手順をほとんど思考停止設定していけば、「nginx + unicorn + rails + mysql on docker」が構築できて、実際にサーバーにポート指定なしでアクセスできて、scaffoldまでできる。

これで実行環境は良しとして、課題は
1. 現行動いているソースコードをどうやってここに反映するか
2. この手順ではmysqlを使っているが、現行のシステムではpostgresqlを使っているので、その差異をどうやって吸収するか
3. そもそもDBをどうやって移行するか

この3点を解決したい。
といったところで、明日にする。

railsのactiverecordでlastの一つ手前を取得したい。secondの逆をやりたい。

$ rails c
# ユーザー取得
u = User.find(1)

# ユーザーのtweetを取得したい
u.tweets.content

# ユーザーのtweetの最新を取得したい
u.tweets.last.content

# ユーザーのtweetの最初のを取得したい
u.tweets.first.content

# ユーザーのtweetの最初2個目を取得したい
u.tweets.second.content

# ユーザーのtweetの最新の一つ前を取得したい
u.tweets.last(2)[0].content

u.tweets.last(2).content だと構文エラーになる罠があるので注意。
u.tweets.last(2)で取得できるのは、tweetオブジェクトの配列なので、要素指定してあげないとだめ。

railsのbelongs_toとhas_manyとreferencesの使い方について整理する

ややこしいので、一気に整理する。

空のrailsプロジェクトを立ち上げて、

$ be rails g model user name:text

※ alias be='bundle exec'

userテーブルができました。

$ be rails g model tweet content:text references:users

tweetテーブルができました。

こいつらモデルのクラスを確認してみると、 app/models/user.rb

class User < ApplicationRecord
end

app/models/tweet.rb

class Tweet < ApplicationRecord
end

dbマイグレートする

$ be rake db:migrate
== 20180717181025 CreateTweets: migrating =====================================
-- create_table(:tweets)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

undefined method `user' for #<ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition:0x00007ff17cfc1488>
.
,

と、エラーになる。

これは、もしかして順番がよろしくなかったか?

もう一度やり直す。

$ be rails g model user name:text
$ be rake db:migrate
$ be rails g model tweet content:text references:user
$ be rake db:migrate
== 20180717181557 CreateTweets: migrating =====================================
-- create_table(:tweets)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

undefined method `user' for #<ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition:0x00007fc20b59a978>
.
.
.

順番なんとか関係なかったです、すみません。
たぶんuserを複数形にしたら行ける気がする。
そのために、一度2つ目のmigrationファイルはなかったことにする。

$ be rails destroy model tweet
$ be rails g model tweet content:text references:users
$ be rake db:migrate
.
.
undefined method `users' for #<ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition:0x00007f8ab5104ea0>

うーむ。先にreferencesを使わない方法を実装して確認する。

もう一度もとに戻します。

$ be rails destroy model tweet

シンプルなやつ

$ be rails g model tweet content:text user_id:integer
$ be rake db:migrate

成功。
しかしこれでは、ただ単にuser_idというたまたまuserテーブルと関係ありそうな名前のカラムがあるだけで、実際には無関係です。
2つのファイルを次のように変更 app/models/user.rb

class User < ApplicationRecord
  has_many :tweets
end

app/models/tweet.rb

class Tweet < ApplicationRecord
  belongs_to :user
end

そして、rails consoleにて試してみる。
まずは一通り。

be rails c -s
Loading development environment in sandbox (Rails 5.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0> u = User.create(name:"hoge")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "hoge"], ["created_at", "2018-07-17 18:34:37.448049"], ["updated_at", "2018-07-17 18:34:37.448049"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">
irb(main):002:0>
irb(main):003:0>
irb(main):004:0>
irb(main):005:0>
irb(main):006:0> t = Tweet.create(content:"aaaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.2ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaaaaa"], ["user_id", 1], ["created_at", "2018-07-17 18:35:22.904090"], ["updated_at", "2018-07-17 18:35:22.904090"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">
irb(main):007:0> t2 = Tweet.create(content:"bbbbbaaaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.2ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "bbbbbaaaaaaaa"], ["user_id", 1], ["created_at", "2018-07-17 18:35:29.800640"], ["updated_at", "2018-07-17 18:35:29.800640"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 2, content: "bbbbbaaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:29", updated_at: "2018-07-17 18:35:29">
irb(main):008:0>
irb(main):009:0>
irb(main):010:0>
irb(main):011:0>
irb(main):012:0>
irb(main):013:0>
irb(main):014:0> u.tweets
  Tweet Load (0.2ms)  SELECT  "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">, #<Tweet id: 2, content: "bbbbbaaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:29", updated_at: "2018-07-17 18:35:29">]>
irb(main):015:0>
irb(main):016:0>
irb(main):017:0>
irb(main):018:0>
irb(main):019:0> t.user
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">

sqlをちょっと読んでみる。 まずは一つ目。Userの作成。

u = User.create(name:"hoge")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "hoge"], ["created_at", "2018-07-17 18:34:37.448049"], ["updated_at", "2018-07-17 18:34:37.448049"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">

特に難しいことはしていない。ここは至ってシンプル。
ただ、(?, ?, ?)とかの部分はなんかエスケープしてるんだろうくらいの雰囲気で流している。

続いてTweetの作成。

t = Tweet.create(content:"aaaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.2ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaaaaa"], ["user_id", 1], ["created_at", "2018-07-17 18:35:22.904090"], ["updated_at", "2018-07-17 18:35:22.904090"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">

なるほど、一度Userの情報を探している。
ちなみに、createの際にuser_idを指定しなかったり、usersテーブルのidに存在しない値を指定すると、失敗し、rollbackする。

userに紐づくtweetを取得する

u.tweets
  Tweet Load (0.2ms)  SELECT  "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">, #<Tweet id: 2, content: "bbbbbaaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:29", updated_at: "2018-07-17 18:35:29">]>

なるほど、かってにuserテーブルのidをtweetテーブルのuser_idに変換してwhereの条件をかけているわけか。

最後に、Tweetに紐付いたuserの情報

t.user
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">

はっ!? SQLは発行されないのか!! これはー多分最初にtを作成したときにuser_idを指定していて、そのときにuserの情報がloadされていたので、それを利用しているぽい。

なるほどね、SQLの中身がわかってちゃんと落ち着いて読めばそんなに対してことやってないってことがわかった。
ただし、userテーブルのidをtweetテーブルのuser_idに変換するとか、名前で制約をつけていることが多くて、そこ離れるまでは大変そうだと思ったよ。

referenceを理解する

これと同じことをreferencesを使ってやってみる。
ちょっと改めて調べたところ、さっき自分がやっていたことはとんでもないウンコマンなことだった。

改めて空のrailsプロジェクトから

$ be rails g model user name:text
$ be rails g model tweet content:text user:references
$ be rake db:migrate

userという名前の型がreferencesというわけだ。
先程までのエラーは、referencesというカラムをuserという型で定義しようとしていたために発生したエラーだったのだった。。。ゴミすぎる。笑
モデルのクラスのファイルを見てみる。

app/models/user.rb

class User < ApplicationRecord
end

app/models/tweet.rb

class Tweet < ApplicationRecord
  belongs_to :user
end

おっと、このままの状態だと、Userクラスに何も書いてないが、果たしてこれで動くのか?
rails consoleで確認。

be rails c -s
Loading development environment in sandbox (Rails 5.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0> u = User.create(name: "hoge")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "hoge"], ["created_at", "2018-07-18 14:01:54.105305"], ["updated_at", "2018-07-18 14:01:54.105305"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "hoge", created_at: "2018-07-18 14:01:54", updated_at: "2018-07-18 14:01:54">
irb(main):002:0>
irb(main):003:0>
irb(main):004:0>
irb(main):005:0>
irb(main):006:0> t = Tweet.create(content: "aaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.3ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaaaa"], ["user_id", 1], ["created_at", "2018-07-18 14:02:19.665391"], ["updated_at", "2018-07-18 14:02:19.665391"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 1, content: "aaaaaaa", user_id: 1, created_at: "2018-07-18 14:02:19", updated_at: "2018-07-18 14:02:19">
irb(main):007:0>
irb(main):008:0>
irb(main):009:0>
irb(main):010:0>
irb(main):011:0> u
=> #<User id: 1, name: "hoge", created_at: "2018-07-18 14:01:54", updated_at: "2018-07-18 14:01:54">
irb(main):012:0> u.tweets
Traceback (most recent call last):
        1: from (irb):12
NoMethodError (undefined method `tweets' for #<User:0x00007f9dc8987f48>)
irb(main):013:0>
irb(main):014:0>
irb(main):015:0>
irb(main):016:0>
irb(main):017:0>
irb(main):018:0> t.user
=> #<User id: 1, name: "hoge", created_at: "2018-07-18 14:01:54", updated_at: "2018-07-18 14:01:54">
irb(main):019:0>

userからtweetを取得するのができない。
やはりuser has_many tweetと定義していないからだ。

しかし、tweet belongs_to userなので、tweetからuserを取得することはできる。

なので、相互からアクセスしたいときには、
app/models/user.rb

class User < ApplicationRecord
  has_many :tweets
end

と、has_manyを追加する必要がある。

ところで、型をreferencesとして定義するメリットは何なんだろう?
確かに、belongs_toは自動で書いてくれたけど、結局has_manyを自分で書かないといけないんだったら、そんなに旨味はないように感じるのだが。。。

外部キーをreferences型カラムで保存する
によると、references型はbelongs_toと同じ意味だと。
references型でテーブル定義をすると、hoge_idというように"_id"をつけなくてもいいらしい。
しかし、これがメリットなのか?

【Rails入門】has_many、belongs_toの使い方まとめ | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト

また、referencesを使った場合はapp/model/castle.rb にbelongs_to :ownerが自動で追加されます。 ですので、使えるなら、いちいち手動で設定するよりもこちらのコマンドを使うよにしてください。

この記事のニュアンスだと、別にreferences使わないとだめってことはないみたい。
そうか、別にreferencesじゃなくてもいいのか、ただreferences使ったほうがちょっと便利でラクできるよ!ってことらしい。

そういう理解にしておいて先に進もう。
と、最後にdb/schema.rbだけは確認しておきたい。 referencesを使ってmigrationファイルを作成した場合。

ActiveRecord::Schema.define(version: 2018_07_17_194545) do

  create_table "tweets", force: :cascade do |t|
    t.text "content"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_tweets_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.text "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

なるほど、tweetテーブル側でuser_idにindex的なものがあるなぁ。
しまった、sqlのindexについての知識が乏しい。。。
なんか早くなるっぽいくらいの認識しかない。辛み。

一旦保留にして、referencesじゃなくて、それぞれ別のものとしてuserテーブルとtweetテーブルを作ったものを見てみる。

be rails g model user name:text
be rails g model tweet content:text user_id:integer
be rake db:migrate

db/schema.rb

ActiveRecord::Schema.define(version: 2018_07_18_142813) do

  create_table "tweets", force: :cascade do |t|
    t.text "content"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.text "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

うーむ、tweetsテーブルのuser_idはusersテーブルとは無関係のように見えるな。
これは適当だがreferencesを使ったほうがindex的なものを使うからテーブル感のリレーションを使いたいときには高速。的なことがありそうな気がしてきた。がわからない。
そんなゴリゴリにsqlチューニングする必要ある勢じゃないから、いいや、気にしない。タイムカプセルに入れておこう。

と、やはり気になってteratailにアカウント作って質問の文章を書いている最中にダメ押しでもう少し調べてみたら、多分理解した。

Ruby on Rails - railsのindexとforeign_keyについておしえてください。(63406)|teratail

やっぱり普通にindexを使っているんですね。
そしてindexも概念は理解しました。
sqlのindexの説明で「複製」とかっていう単語を使っていたので、腑に落ちていなかったが、どうやらこういうことらしい。

indexについて理解があいまいなのですが、 --------+--------+---------+

| id | name | shoku |

|1 | taro | ramen |

| 2 | ken | somen |

こんなデータがあったとして、nameフィールドにindexをつけたとすると、よみだすときこのデータ全体を読み込むのではなく、nameフィールドだけを絞って読み込むということですか? だとしたら find_by があるのでindexが不要だと思ったのですが。。。

という質問に対して

たとえば、ユーザーのデータが数百万件を超えるような膨大な数になったとしましょう。 その中から、「メールアドレスがhoge@example.comのデータを探す」となれば、インデックスがない場合だと「全部のデータを当たって、一致するメールアドレスを探す」という処理が必要になります。一方、インデックスでは「B木」のような完全に同一のデータ、あるいはデータ範囲を検索しやすいような特殊な構造でデータを入れてあるので、メールアドレスが一致するデータを、全探索より遥かに高速に引き出すことができます。

Ruby on Rails - railsのindexとforeign_keyについておしえてください。(63406)|teratail

なるほど、普通に複製するだけじゃなくて、B木とかの検索しやすいデータ構造にしておくのか。
いやーCS力足りないなぁ。。。
ということは、結局テーブル同士のリレーションは結構な数の呼び出しが起こりそうな気がしているので、indexを貼ってくれているrefernces型を使うのがbetterという結論でよろしいですかね。