Elasticsearch7にしたらtoo_many_buckets_exceptionになってしまった
結論
一回でまとめて取得していたものを数回に分けて取得するようにした。
対象のインデックス
GET logs/_mapping
{ "logs" : { "mappings" : { "properties" : { "action" : { "type" : "text" }, "user_id" : { "type" : "integer" }, "company_id" : { "type" : "integer" }, "request" : { "type" : "nested", "properties" : { "id" : { "type" : "integer" }, "type" : { "type" : "text", "fielddata" : true } } }, "timestamp" : { "type" : "date" } } } } }
too_many_buckets_exceptionとは
Elasticsearchの用語に合わせて説明すると 指定した集約(aggregation)方法で分類(Bucket)された数が 1万を超えたために出たエラーである
SQLに例えて説明すると GROUP BY で出力されたレコードの数が 1万を超えた場合である。
実際のElasticsearch で叩いてた内容
まずこんな検索Queryを作ったことを許してほしい
{ "query": { "bool": { "must": [ { "match": { "company_id": 1 } }, { "range": { "timestamp": { "gte": "now-1y", "lt": "now" } } } ] } }, "aggs":{ "by_user_id":{ "terms":{ "field":"user_id", "size":1000000, "missing":0 }, "aggs":{ "by_month":{ "date_histogram":{ "field":"timestamp", "interval":"month", "format":"yyyyMM" }, "aggs":{ "by_request":{ "nested":{ "path":"request" }, "aggs":{ "by_request_type":{ "terms":{ "field":"request.type" }, "aggs":{ "by_request_id":{ "terms":{ "field":"request.id" } } } } } } } } } } } }
SQLで表現すると
SELECT user_id, DATE_FORMAT(timestamp,'%Y-%m') AS month, request_id, request_type FROM logs WHERE company_id = 1 AND timestamp BETWEEN DATE_SUB(NOW(),INTERVAL 1 YEAR) AND NOW() GROUP BY user_id, DATE_FORMAT(timestamp,'%Y-%m'), request_id, request_type
確かに一年間分となるとそうなるよなと思いつつ
このエラーに対する設定は変えられるのだが、
現時点ではElasticsearch 公式では設定ではなくクエリ自体を修正するように勧めている。
公式通り検索範囲を狭めた
一年で1回ではなく 一ヶ月で12回に分けるようにした。
GET logs/_search
{ "query":{ "bool":{ "must":[ { "match":{ "company_id": 1 } }, { "range":{ "timestamp":{ "gte":"2020-01-01T00:00:00.000+09:00", "lte":"2020-01-31T23:59:59.999+09:00" } } } ] } }, "aggs":{ "by_user_id":{ "terms":{ "field":"user_id", "size":1000000, "missing":0 }, "aggs":{ "by_request":{ "nested":{ "path":"request" }, "aggs":{ "by_request_type":{ "terms":{ "field":"request.type" }, "aggs":{ "by_request_id":{ "terms":{ "field":"request.id" } } } } } } } } } }
あとはサーバーサイド側で整えて今回は乗り越えた。
やはり基礎は学んでおかないと。。。
今回はこの本を読んで基本を抑えました。
Kindle unlimitedの対象となっていたので読んでみましたが
例えや図が多く、わかりやすい内容になっているので急いで学ばなくてはいけない人にもおすすめです
Rails5.2にしてからデプロイが徐々に遅くなっている
結果だけ気になる人のために
まずは結論から
原因
Rails5.2から導入されたgem Bootsnap が
処理を早くするためにデプロイ毎にcacheし
古いのが消えずに残り続けていたから。
解決方法
cache を消した。
cd /var/www/myapp/shared rm -rf tmp/cache/bootsnap*
原因わからず半ベソ編
最初はただのRails バージョンをあげただけだった。。
最近はどこの現場でもRailsのバージョンを最新にしたがる人なので
今回も段階的にRails6にしようと思ってた矢先でした
Rails5.2にしてからだんだんデプロイ時間が遅くなっていることに気付き、
5分もかからなかったデプロイが
三日ほどで10分以上かかるようになってしまいました。。。
グラフを見てもらうとわかるようにデプロイ失敗しているんですが
処理時間が長すぎるために10分応答がなかったので
CircleCIが失敗と判断しデプロイができないようになってしまいました。。
Unicornも勝手に落ちるように。。
挙げ句の果てにUnicornも一日のどこかで
一時的に504エラーを返すようになり
完全に半ベソ状態でした。。
考えつく限り対策したけどダメだった編
以下の三つだけ少し効果があったので紹介します
release以下のRailsを最低限削除
capistranoを使用しているのでreleaseディレクトリに5つのRailsが入っているので
最新から3つほど残し、他は一時的に削除した。
tmp/assets配下にある使われてないjsファイルを削除
こちらもプリコンパイルしたときのjsファイルが昔からずっと残っていたので
config/deploy.rb に以下の設定を加えて古いjsファイルを削除した。
set :keep_assets, 3
スワップ領域を作ってメモリを割り当てた
あと他試したけど全く変わらなかった
- ロードバランサーで一台切り離してから サーバー再起動 & Unicorn再起動
- Unicorn ワーカプロセスを2つから5つに増やす。
- メモリキャッシュ削除
- CircleCI上でassets:compileして転送
やっとわかった編
当時はデプロイが遅いのとUnicornが勝手に止まったのが重なったせいで
いろんな説が飛び交いまして
Unicornの性能が悪い説
CircleCIが勝手に設定変えた説
とかもう結構何が悪いのかわからなくなっていたところ
bootsnap で処理が早くなるという記事を見つけて
「え!実は効いていないとか?有効にすれば元に戻るかも!」
と思い Githubを見ていたらこんなことを書いているのを見つけました。
大事なので一部抜粋すると
If you notice deploys getting progressively slower, this is almost certainly the cause.
もしデプロイが遅くなっているならこれが原因だ的なこと書いてるじゃん!!
ということですぐにcacheを削除しデプロイしてみた。。
嬉しい!!!
13分くらいかかってたのが5分程度に戻ってる!!
てかすみません僕が今回の戦犯でした。。。。
現場の皆様は叱ることなく今回の件を褒めていただきました。。
締め
今回の件でいろいろと学ばせてもらいました。
インフラも少し毛が生える程度に知識を得ることができました。。
雑にRailsのバージョンをあげてるわけではないんですが
もう少し差分があるところはもう少し調べないとなあと思います。。
Rails 更新時にParams Permitを間違えてまるっと子モデルのデータが消えた話
困った問題
なぜか子モデルが消して入れ直す形になってしまったので 忘れないようにメモ
class UsersController < ApplicationController before_action :set_user, only: %i[edit update] def edit; end def update if @user.update(user_params) redirect_to home_path else render :edit end end private def user_params params.require(:user).permit( :email, :password, user_profile_attributes: %i[address nick_name] ) end def set_user @user = User.find(session[:user_id]) end end # 親モデル class User < ApplicationRecord has_one :user_profile, dependent: :destroy accepts_nested_attributes_for :user_profile end # 子モデル class UserProfile < ApplicationRecord belongs_to :user end
そして今回ハマったのがここなんですが user_params
これとは違うControllerで UserProfileを更新することがあるのですが
そうするとUsersController で更新した際に 見事に一回消して入れて行なってしまうため 前の項目がまるっと消されてる! という状況が生まれてしまうのでした。。
Unpermitted parameter: :id (1.6ms) BEGIN UserProfile Load (2.4ms) SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`user_id` = 1 LIMIT 1 SQL (2.2ms) DELETE FROM `user_profiles` WHERE `user_profiles`.`id` = 101 SQL (2.9ms) INSERT INTO `user_profiles` (`user_id`, `address`, `nick_name`, `created_at`, `updated_at`) VALUES (1, 'tokyo', 'hoge', 2020-02-20 13:51:02', '2020-02-20 13:51:02') SQL (2.5ms) UPDATE `users` SET `updated_at` = '2020-02-20 13:51:02' WHERE `users`.`id` = 1 (10.1ms) COMMIT
ですが実はもう答えは書いてあるのです!
そこ! Unpermitted parameter: :id
解決策
子モデルの permit parameter に :id を追加してあげれば ちゃんと更新されるのでした。
def user_params params.require(:user).permit( :email, :password, user_profile_attributes: %i[id address nick_name] ) end
(1.9ms) BEGIN UserProfile Load (2.6ms) SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`user_id` = 1 LIMIT 1 SQL (2.5ms) UPDATE `user_profiles` SET `nick_name` = 'hogehoge', `updated_at` = '2020-02-20 14:01:29' WHERE `user_profiles`.`id` = 102 SQL (2.3ms) UPDATE `users` SET `updated_at` = '2020-02-20 14:01:29' WHERE `users`.`id` = 1 (3.6ms) COMMIT
初歩的なミスですが 重大なバグになりそうなので書いておきました。
Rails5.2とbelongs_toとN+1問題の話
結果
レコードに登録するときに
外部キーをidとして入れると
N+1になる
環境はこちら
rails 5.2.1 activerecord-import 0.27.0 ruby 2.5.3
なにいってんだこいつ的な感じだと思うので説明
class User < ApplicationRecord has_many :items end class Item < ApplicationRecord belongs_to :user validates :name, presence: true end
そしてItemを複数取り込みする処理があるとする
class ItemImport def execute items = User.all.limit(100).each_with_object([]) do |user, items| %w(item_a item_b item_c).each do |name| item = Item.new(user_id: user.id, name: name) next unless item.valid? items << item end end Items.import items end end
ここでN+1発生
>> ItemImport.new.execute (前略) User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 User Load (0.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 User Load (0.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 User Load (0.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 User Load (1.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 User Load (0.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 (後略)
Rails5からbelongs_toで指定した値は
入力必須のため外部IDだけ入れておくと
ちゃんと有効な値か存在チェックをしてくれるという
仕様からうまれた問題だった
item = Item.new(user: user, name: name)
とすることで
チェック不要の有効な値が入っていると認識してくれて
無駄なクエリを発行せず
Insertしてくれるのでした
おしまい!
Railsって結構オンラインプログラミングスクールの
教材として使われるけど
完全にオーバースペックだよね
っていう小話
ファイルを複数コピーするだけのbash
使い方は
cp_files hogehoge.txt
と打つと
同じ階層に
1_hogehoge.txt
2_hogehoge.txt
...
5_hogehoge.txt
と作ってくれます。
cp_files hogehoge.txt 2
と引数にファイル数を指定することもできますが
簡単な処理なので
自分でカスタマイズした方がいいかもしれないです。
あくまで参考程度に
#/bin/bash function cp_files() { if [ -e $1 ]; then local first_path="./" local array=( `echo $1 | tr -s '/' ' '`) local last_index=`expr ${#array[@]}` # ここでエラーがでたら 上を `expr ${#array[@]} - 1` に書き換えてください local file_name=$array[$last_index] if [[ "$1" =~ "^/" ]]; then first_path=/ elif [[ "$1" =~ "^~/" ]]; then first_path=~/ fi for i in `seq 1 "${2:=5}"` do array[last_index]=$i\_$file_name echo "cp $1 $first_path$(IFS=/; echo "${array[*]}")" cp $1 $first_path$(IFS=/; echo "${array[*]}") done else echo "Not Exists File: $1" fi }
Crystal languageをJET BRAINS社のIDEで使いたい
って思い
RubyMine 2018.2
でプラグインをインストールした話
使ってみた結果:
VSCODEを使おう
※2018/10時点
それでも使いたい方はこちらご参考ください
まずこちらURLを見ると
https://plugins.jetbrains.com/plugin/10213-crystal-language-plugin
2018.1.6で終了していることに気づく。。
まじかあと思いつつ
Githubを見てると見事に
開発が止まっていた。。
残念。。
ってことで
Issue, PullRequestを見てるとこんなすばらしいプルリクが!!
https://github.com/crystal-lang-tools/intellij-crystal-lang/pull/5
どうやら動くっぽいけど
Masterにマージしないっぽい。。
じゃあ自分で同じ差分を準備して
Buildするかってことで
git clone
git clone https://github.com/crystal-lang-tools/intellij-crystal-lang.git
cd intellij-crystal-lang
一応念のためブランチ切っておく
git checkout -b feature/2018.2 git fetch origin pull/5/head:feature/2018.2 cat gradle.properties
これで ideaVersion = 2018.2
となっていることを確認する
そしてビルド
(JDK必要です!)
./gradlew idea ./gradlew build
ビルド成功したら
勝手にプラグインが有効になってると思いきや
なってなかった。。(今思うと当たり前か)
ってことで
ここからIDEで操作
Preferences > Plugins > Install plugin from disk...
を選択し
下記パスにあるzipファイルのままでOpenする
intellij-crystal-lang/build/distributions/intellij-crystal-1.3-SNAPSHOT.zip
するとPluginsのリストの中に
Crystal language plugin の項目があるので
Checkして再起動して
無事に動くことを確認!!
使い勝手は色々ともう少し機能があればなと
思うのだが引き続き調査中!
【jQuery】 Gifを出したりしまったりする
app2.html
<html> <head> <title>Gif TEST</title> </head> <body> <h1>Gif TEST</h1> <div> <h3>最初からDOMを用意して display: none; するパターン</h3> <div id="loading1" style="display: none;"> <label>Loading Now!!</label> <label>これだとIEだと表示されない可能性大</label> <img src="loading.gif"><!-- どっかからgifを取ってきてください。。 --> </div> Check this out!!<input type="checkbox" id="checkLoad1"> </div> <div> <h3>JSにDOMを用意させて入れたり取ったりするパターン</h3> <div id="loading2"></div> Check this out!!<input type="checkbox" id="checkLoad2"> </div> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script src="app2.js"></script> </body> </html>
app2.js
$(function() { $('#checkLoad1').on('click', function(){ if ($(this).prop('checked')) { $('#loading1').show(); } else { $('#loading1').hide(); } }) $('#checkLoad2').on('click', function(){ if ($(this).prop('checked')) { var load_tmp = "<label>Loading Now!!</label>" + "<img src=\"loading.gif\">"; $('#loading2').append(load_tmp).show(); // 意外とshowが大事だったりする。 } else { $('#loading2').empty(); } }) })