Rails 5.2以降のRails 5系でDBに都度接続している場合においてはdatabase.ymlで `reaping_frequency: null` を設定する
Rails 5.2以降でDBに都度接続している場合においてはdatabase.ymlで reaping_frequency: null
を設定しないと、スレッドが枯渇してサーバーにsshすらできなくなりサーバーの再起動が必要になります。
sshはこんなエラーになります。
shell request failed on channel 0
またサーバーに入っていると下記のようなエラーがでて何もできなくなります。
-bash: fork: retry: 子プロセスがありません -bash: fork: リソースが一時的に利用できません
スレッドが枯渇しているサーバーは外部からみると、CPU、メモリなどの負荷がかかっているようには見えず、再起動して入ってみてもOOM-Killerなどが発動しておらず、何が起こっているかわかりにくいのが特徴です。
1プロセスあたりのファイルディスクリプタ数の上限も十分にあり、tcp周りの設定( net.ipv4.tcp_tw_reuse
とか) もしてあり、file descriptorの枯渇もみられなかったので、原因がわからず焦りました。
ちなみに tcp_tw_recycle
は廃止されたそうです。
原因
下記のPRにより、 reaping_frequency
が設定されていないとデフォルトの60秒が設定されるようになりました。
これはReaperがDBへの接続がハングしているか死んでいるかをチェックする間隔になります。
Reaperは別スレッドを立てて定期的に起き上がって確認する実装になっています。
Reaperは、establish_connectionが実行され、コネクションプールが生成されるたびにスレッドを作成します。
がこのスレッドは特にkillもされずに残る実装になっていそうです。
2020/08/31時点のedgeを見ると、ちゃんと止める実装も入っている気がします。
で、activerecord-refresh_connectionなどを使って都度接続などを行っていると、コネクションを貼るたびにスレッドが作られ増え続けます。
スレッドが増える様子は pstree
コマンドで確認できます。(下記の中で数字がスレッド数です)
# pstree sh---bundle-+-bundle---73*[{bundle}] |-bundle---49*[{bundle}] |-bundle---178*[{bundle}] |-bundle---106*[{bundle}] |-bundle---122*[{bundle}] |-bundle---98*[{bundle}] |-bundle---130*[{bundle}] |-bundle---138*[{bundle}] `-74*[{bundle}] // アクセスする # pstree sh---bundle-+-bundle---73*[{bundle}] |-bundle---49*[{bundle}] |-bundle---186*[{bundle}] |-bundle---106*[{bundle}] |-bundle---122*[{bundle}] |-bundle---98*[{bundle}] |-bundle---130*[{bundle}] |-bundle---138*[{bundle}] `-74*[{bundle}]
そうすると、スレッド数がもりもり増えていき、thread-maxに到達して、サーバーにアクセスできなくなります。
対応方法
reaping_frequency
を明示的に null ないしは 0で指定すればそもそもReaperはスレッドを作らなくなります。
さらに都度接続しているのであれば、そのリクエスト内でコネクションがおかしくなったらエラーになりますが、次のリクエストを処理するときは再接続するのでエラーが出続けることはないので、上記の設定でも問題ないと考えています。
参照
thread-maxの設定
bcryptの仕組み(とbcrypt-rubyの中身を見る)
Ruby(主にRails)でパスワードを扱う際のハッシュ化には bcrypt-ruby を使うことがほとんどだと思います。
bcryptについて勉強しつつ、さらに実装を確認してみようと思います。
bcrypt-ruby
は v3.1.15
を利用しています。
bcrypt
説明はwikipediaにあります。
https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf
Blowfishの鍵セットアップ関数に Eksblowfish
を使っていて、このセットアップにcostとソルトとパスワードを利用しています。
セットアップ後、 OrpheanBeholderScryDoubt
という24 byteの文字列を64回繰り返し暗号化しバイト列を生成します。
bcrypt-ruby
ではここで定義されています。
そして、costとソルトと暗号化して生成されてバイト列を結合したものがbcryptがハッシュ値として生成する文字列になります。
bcrypt-ruby
でハッシュ化された文字列
bcrypt-ruby
でハッシュ化に使われるバージョンは 2a
のみぽいです。
生成されるハッシュは接頭辞は $2a$
になっています。
irb(main):002:0> my_password = BCrypt::Password.create("my password") => "$2a$12$zafAQ3jKUpR7TXwYRm8nmeO.3p/1iZN9a09F7cFs7M6kNZxnbvf9C" irb(main):003:0> my_password.version => "2a"
ただし、他のバージョンでハッシュ化されたものも読み込めそうです。
irb(main):006:0> new_password = BCrypt::Password.new("$2y$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e") => "$2y$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e" irb(main):007:0> new_password.version => "2y"
ハッシュ化されている文字列はいくつかの区分に分けられます。
まず 最初の $ $
で 囲われている部分はバージョンです。
次に $ $
で囲われているのは、コストの部分です。
これは、2を基数とした指数となり、この数の2の累乗がEksblowfishでのセットアップ中での繰り返し回数になります。(ストレッチング)
この回数が多ければ、ブルートフォース攻撃になどに対する対抗が強くなりますが、その分計算量が増えて計算に時間がかかるようになります。
bcrypt-ruby
では BCrypt::Engine.calibrate()
といいメソッドが用意されており、どれくらいのコストになるかをざっくりと計算できます。
irb(main):002:0> BCrypt::Engine.calibrate(5000) # この場合は5000ms以内で終わる最大のコストを算出してくる => 16
bcrypt-ruby
ではハッシュ化のcostを指定できます。
irb(main):004:0> my_password = BCrypt::Password.create("my password", cost: 4) => "$2a$04$KV38A7SDgPKjmVi7ir59muRmquFxR/KmX/HtgWY97qC1tBryajHp."
ただし、4以下の場合は全て4に置き換えられます。
https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/engine.rb#L67-L69
irb(main):005:0> my_password = BCrypt::Password.create("my password", cost: 1) => "$2a$04$vLCmqS2n9tnYpnKT.G9wJuhBPjtKakH2/9zOQuU1IIv.HIDj6q27e" # コストが04になっている
また、costが31より大きいと ArgumentError
の例外が発生します。
https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L45
コスト以降の部分はソルトとチェックサムになります。
128ビットのソルトになるのですが、Radix-64エンコードされて22バイトになっています。
チェックサムも184ビットなのですが、Radix-64エンコードされて31バイトになっています。
パスワードの確認する際は、 bcrypt-ruby
ではハッシュ化された文字列のバージョンやコストも含む先頭から29バイトをソルトとして渡して処理をしていきます。
ハッシュ化
bcrypt-ruby
ではソルトの生成におけるランダムな16バイトを生成するために OpenSSL::Random.random_bytes()
を利用しています。
https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/engine.rb#L74
生成されたソルトを利用してハッシュ化をします。
https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L46
同じソルトを使えば、同じ文字列からは同じハッシュ値が生成されます。
irb(main):032:0> my_password = BCrypt::Password.create("my password") => "$2a$12$VoGypMhauXeq4LZcLMbk6uGo/moLdWs/OAGUNbq9gLe9p.YZyOqXm" irb(main):033:0> BCrypt::Engine.hash_secret("my password", "$2a$12$VoGypMhauXeq4LZcLMbk6u") => "$2a$12$VoGypMhauXeq4LZcLMbk6uGo/moLdWs/OAGUNbq9gLe9p.YZyOqXm"
bcrypt-ruby
の利用で気をつけること
Nullバイトの対応
bcrypt-ruby
は Nullバイトの対応ができてないです。
irb(main):039:0> password = BCrypt::Password.create("foo\0bar") => "$2a$12$YPoekx0N24xH50Z0y9JfUeBpEBvYLNISVaSaH4oWJKAWnoTHa46RO" irb(main):040:0> password == "foo" => true
PRはあるのですが、mergeされていませんね、、
なので、下記2つのブログで言及されているようなことで問題になります。
パスワードの文字数は72文字まで
72文字以降は切り捨てられます。
irb(main):043:0> password = BCrypt::Password.create("a" * 73) => "$2a$12$CnOALTas/7Yj0JuzBW3GuOE1rKm3DZfN7N6OLwEGH8lnELV7H3iEC" irb(main):044:0> password == "a" * 72 => true
これは他の言語の実装でも同じような挙動になることが多いとのことです。
SecretSalt
bcryptでは OrpheanBeholderScryDoubt
という文字列を暗号化することですが、この固定の文字列も変えてハッシュ値を取るのが SecretSalt
になります。
Railsのforce_sslとssl_optionsについて
Railsの force_ssl
と ssl_options
について知らないことが多かったのでまとめてみます。
force_ssl
force_ssl
を有効にすると、ActionDispatch::SSL
がミドルウェアとして差し込まれます。
rails/default_middleware_stack.rb at 5394c94a2b8fa3b24893f00b2fc20d6554bf31c9 · rails/rails · GitHub
これにより、 全てのリクエストにHTTPSプロトコルが強制されます。
ちなみに、ActionDispatch::SSL
はその他にも色々やってくれます。
rails/ssl.rb at 5394c94a2b8fa3b24893f00b2fc20d6554bf31c9 · rails/rails · GitHub
で、内部だけで使うヘルスチェックとかのエンドポイントに対してHTTPS強制されるとそれはそれで面倒です。
それを回避するために、ssl_options
を使います。
ssl_options
ssl_options
の中の redirect
の設定の中で、HTTP => HTTPSのリダイレクトの対象から除外する exclude
オプションがあります。
上記のページにもありますが、ヘルスチェックなどのパスを除外することができます。
config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
他にもHSTSやCookieのSecure属性に対するオプションもあります。
参考
ブラウザのXSSフィルタはどうなったのか(2020/08/14 時点)
ブラウザのXSSフィルタを有効にするために、 X-XSS-Protection
レスポンスヘッダーでブラウザの設定を上書きして有効にするという内容が徳丸本のp 135にでてきます。
XSSフィルターってどういう機能なんだろうと調べてみたところ、下記記事にある通り、2020/08/14 現在だとほとんどのブラウザでなくなったりしていることがわかりました。
Chrome
ChromeではXSS Auditorという名前でXSSフィルターを実装していましたが、Chrome 78でこの機能が削除されました。
で、この機能の代わりにChrome 83で Trusted Types
という新しい機能をリリースしています。
使い方は下記を参考にするとよさそうです。
Firefox
Firefoxは X-XSS-Protection
レスポンスヘッダーを使ったXSSフィルターは実装していないですね。
Safari
Safariは2020/08/14でも使えてそうですね。
IE Edge
VS Codeでフォルダの階層をまとめない
Compact Folders を off
or
settings.jsonで下記を設定
"explorer.compactFolders": false,
https://code.visualstudio.com/updates/v1_41#_compact-folders-in-explorer
いつも忘れるのでいい加減メモっておく
React Navigationを4系から5系にバージョンアップする
ReactNative は 0.63.2
まで更新してある状態で、React Navigationのバージョンを4系から5系にバージョンアップしました。
その時の対応をまとめます。
React Navigation 5のアップデート内容
下記にまとまっています。
ここで取り上げられているテーマは下記の通り
- React コンポーネントを使った設定
- 新しいフック
- コンポーネントからのオプションの更新
- 新しいテーマAPI
- TypeScript化(型を第一級に扱う)
- Redux DevTools との統合
- ネイティブなStack Navigator
- ネイティブなMaterial top tab navigator
npm packageの更新
https://reactnavigation.org/docs/upgrading-from-4.x#package-names
使っていたライブラリについて、下記のように変更
react-navigation -> @react-navigation/native react-navigation-stack -> @react-navigation/stack react-navigation-tabs -> @react-navigation/bottom-tabs
@react-navigation/material-top-tabs
と @react-navigation/material-bottom-tabs
は今のプロジェクトでは不要だったのでインストールしませんでした。
また、v4との互換性を保つための仕組みを提供してくれる @react-navigation/compat
を使いました。
=> 最終的に使うのをやめました。
バージョンアップの方針
下記に4系からのアップデートについての方法が公式ページにあります。
Upgrading from 4.x | React Navigation
ここでもある通り、React Navigation 4
のメンテナンスは当面は継続して行い、React Native
の最新バージョンにも追従すると書かれています。
いつまでメンテしてくれるかはなんか不安ですが、今すぐに開発が止まるということがなさそうなので、すぐに無理にバージョンアップしなくてもよさそうです。
今のプロジェクトではアップデートする時間を作れたのでアップデートを実施しました。
@react-navigation/compat
を使う @react-navigation/compatは使わずにアップデートしました
@react-navigation/compat
を使うReact Navigation 5では4とはほぼ互換性がない状態です。
なので、いきなり全部対応しようとすると量が膨大になるので、まずはReact Navigationが用意した @react-navigation/compat
を利用し、変更を最小限にしました。
今後の運用も考えて @react-navigation/compat
は使わずにアップデートを行いました。
変更箇所
createStackNavigator
createBottomTabNavigator
を使ったNagvigator
v5での createXNavigator
は 大幅に変更されており、React コンポーネントとしてnavigatorを設定するようになっています。
https://reactnavigation.org/docs/upgrading-from-4.x/#configuring-the-navigator
createStackNavigator | React Navigation
なので、ここは一旦 @react-navigation/compat
の createCompatNavigatorFactory
を使って既存の呼び出しをラップしました。
Navigatorの数が少なかったのと今後の運用を加味して、v5の方式に移行しました。
v5ではScreenで柔軟にオプションを指定できるようになったため、同時にNavigatorの階層を見直し階層を減らしています。
下記に書いてあるとおり、今回のアップデートではかなり大きな方針変更がありました。
https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator
v4以前では、遷移は静的に定義されていたのに対し、v5では動的に設定されるようになりました。
v4以前では、再帰的にScreenを探しにいきますが、v5ではそれがなくなりました。
v5では、現在のNavigator内で遷移できなければ、一階層上の親のNavigatorから探しに行きますが、それより上には行けないようです。
そのため、Navigatorの階層は2つくらいに抑えることが推奨されています。
When nesting multiple stack navigators, we recommend nesting at most 2 stack navigators, unless absolutely necessary.
Nesting navigators | React Navigation
createSwitchNavigator
は @react-navigation/compat
のものを使う createSwitchNavigatorは利用をやめました
createSwitchNavigator
は @react-navigation/compat
のものを使うReact Navigation 5
では、React コンポーネントを使うようになったため、条件分岐なども書けるようになりました。
そのため、NavigatorのScreen定義を、動的に定義して変更することができるので、Switch Navigatorが不要になります。
なので、createSwitchNavigator
は廃止されているのですが、 今回修正範囲が大きかったので、一旦 @react-navigation/compat
にある、createSwitchNavigator
を使いました。
それに従い、 createStackNavigator
に置き換えて対応しました。
NavigationStackScreenProps
を StackNavigationProp
に置き換える
v5では NavigationStackScreenProps
は StackNavigationProp
となりました。
v4
https://reactnavigation.org/docs/4.x/typescript/#type-checking-all-props-for-a-screen
v5
https://reactnavigation.org/docs/typescript/#type-checking-screens
これにより、Screenに対するparameterを型チェックできるようになります。
修正はこんな感じでした。
navigation.navigate
を使った遷移にも型チェックが効くようになっています。
下記のような感じで全般的に書き直しました。
Screen名 + ParamList というtypeを用意しつつ、exportしています。
import { BarParamList } from 'src/screens/Bar' export type FooParamList = { Foo: {} } interface Props { navigation: StackNavigationProp<FooParamList & BarParamList> } export function Foo({ navigation }: Props) { useEffect(() => { if (!isLoading && isLoggedIn) { navigation.navigate('Bar', {}) } }, [isLoading, isLoggedIn, navigation]) return ( <Wrapper> <FooContent /> </Wrapper> ) }
useNavigation
は @react-navigation/native
のものを使う
これはimportの変更のみです。
-import { useNavigation } from 'react-navigation-hooks' +import { useNavigation } from '@react-navigation/native'
Sceenには前のScreenから navigationとrouteを受け取るようになりました。
https://reactnavigation.org/docs/params#passing-params-to-a-previous-screen
そのため、Screenでは useNavigation
を使わず、渡ってくるnavigationを使うようにしました。
パラメーターの受け渡しの変更、 getParam
の廃止
https://reactnavigation.org/docs/upgrading-from-4.x/#the-navigation-prop
React Navigation 5
では、navigation
propは2つのpropに分割されています。
navigation
prop :navigate
やgoBack
などのヘルパーメソッドを含むroute
prop : 現在の画面のデータを含みます。 これは 4とかだとnavigation.state
を使っていたものです。
また、getParam
は廃止されました。
getParam
は主に2つの役割がありました。
params
がundefined
になることを防ぐparams.someParam
が未定義または null であった場合においてデフォルト値を設定する
これらは、TypeScriptの optional chaining
と nullish coalescing operators
で同等のことができます。
Passing parameters to routes | React Navigation
v5ではこのようになりました。
export function Foo({ route, navigation }: Props) { const uniqueKey = route.params.uniqueKey return ( <ScrollView> <Block> <FooContetn uniqueKey={uniqueKey} /> </Block> </ScrollView> ) }
createAppContainer
から NavigationContainer
を使うように変更
React Navigation 5
では createAppContainer
は廃止されました。
下記のドキュメントのように、NavigationContainer
を使うように修正しただけでした。
https://reactnavigation.org/docs/upgrading-from-4.x#navigation-container
参考資料
Upgrading from 4.x | React Navigation
Compatibility layer | React Navigation
Passing parameters to routes | React Navigation