トランザクション中にMySQLに再接続するということについて
問題
Ruby on Railsではコネクションプーリングが基本的に有効になっていて、コネクションが使い回されるという仕組みになっています。 Auroraにフェイルオーバーが発生し、マスターがレプリカに降格した場合は コネクションがレプリカに向き続けてしまってWriteを発行したらエラーが発生してしまうという問題があります。 この問題に対処するためにアプリケーション側でよしなにフェイルオーバーに気づき、再接続する仕組みを入れないといけないのです。
一般的に ActiveRecord::StatementInvalid
や Mysql2::Error
をキャッチして、--read-only
なエラーだったら
コネクションを再接続するというgemを使ったアプローチが取られることが多いようですが、これは本当に適切なのでしょうか? :thinking_face:
トランザクション内で再接続された場合はどうなるんだろうと思い、調べてみました。
ActiveRecord::Base.transaction do foo = User.new(name: "foo") foo.save! ActiveRecord::Base.connection.reconnect! // 状況を再現しやすいようにわざと再接続してみる bar = User.new(name: "bar") bar.save! end puts User.count # 2か0を期待したいが、1が返ってくる
上記の場合ですと、コネクションが切断された段階でデータベースはロールバックされ、ロックが解除されたりします。
しかし、ActiveRecord::Base.transaction
は例外を検知しているわけではないので、後続の処理が実行されます。
後続の処理はauto-commit modeで実施されるため、不整合が発生する可能性があります。
これはおそらくdatabase.yml
に書く reconnect
のオプションと実質同じなんじゃないかと思っています。
解決策
- トランザクション外のどこかのライフサイクルでフェイルオーバーを検知し、再接続する機構を入れる(トランザクション内の場合なら切断して、例外を上げる)
- トランザクションが失敗した場合は抜けて再びトランザクションを貼り直して再試行する
が正しいと思っています。
前者の仕組みとして、active_record_mysql_xverify
というgemがあるようです。
active_record_mysql_xverify - so what
GitHub - winebarrel/active_record_mysql_xverify
このgemはコネクションをプールからcheckoutするときに
前回エラーが発生したというフラグを持っているかつinnodb_read_only
が有効だったら、再接続するというアプローチを取っています。
むやみに毎回チェックするわけではないのが良いですね。
ですが、unicornみたいなprefork型のサーバーだと1つのプロセスに対して、1つのコネクションが使われ、そもそもプールに返却されることがないっぽい(?)ので
上記のgemは効果的な解決策にならないみたいです。
また、Delayed JobのバックエンドとしてActive Recordを使っている場合はどこかで再接続する仕組みを用意しなければならず面倒ですね。
Delayed Jobにはlifecycleというフックポイントがいくつか用意されており、これを活用するといいんじゃないかという結論に至りました。
lifecycle.before(:error)
と書くことでジョブが再試行される前にコネクションが再接続されると思います。
class Delayed::Plugins::ClearConnectionsOnError < Delayed::Plugin callbacks do |lifecycle| lifecycle.before(:error) do |_worker, _job| is_read_only = ActiveRecord::Base.connection.execute('SHOW GLOBAL VARIABLES LIKE "innodb_read_only"').first.fetch(1) == "ON" if is_read_only ActiveRecord::Base.clear_all_connections! end end end end Delayed::Worker.plugins << Delayed::Plugins::ClearConnectionsOnError
終わりに
Railsノウハウ難しい・・・ 間違っていることを書いていたら、教えていただけると助かります。
合わせて読みたい
MySQL :: MySQL 5.6 リファレンスマニュアル :: 23.7.16 自動再接続動作の制御
mysql等の接続を司る下層のクラスオブジェクトは自動再接続をしちゃいけない。それを利用するトランザクション管理クラスが困る。切れても再接続して切れてないように見せ掛けたければ、その上層で別途用意すればいい。
— s/v (@sockety_v) 2011年11月30日
良い子のみんなは、MySQL Connector/Jで意味も解らずautoReconnectを使っちゃイケないよ!!切断時の例外処理をちゃんと実装するんだ!!
— Mikiya Okuno (@nippondanji) 2019年3月20日
GitHub ActionsでECSのタスク定義のみを更新する
GitHub ActionsでECSサービスをデプロイするときによく使われる aws-actions/amazon-ecs-deploy-task-definition ですが、 タスク定義のみを更新することもできます。READMEのサンプルコードにはその例がないので、気づきにくいです。
inputs: task-definition: description: 'The path to the ECS task definition file to register' required: true service: description: 'The name of the ECS service to deploy to. The action will only register the task definition if no service is given.' required: false
inputsにtask-definitionだけ入力してあげるだけでOKです。
# 中略 - name: register new task definition family uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.render-container.outputs.task-definition }}
ECS Scheduled Taskでバッチ処理するタスク定義の更新などで活用できると思います。
参考
GitHub Actionsがdependabotで発火した場合のみ挙動を変えたい
DependabotのPRを元に発火するGitHub Actionsはread-onlyなGITHUB_TOKEN
のみを扱うことができるため、step内で
Secretsを参照している場合はfailしてしまいます。
【Github actions】DependabotのPull RequestでSecretsが参照できずワークフローがFailになった場合の対処 - Simple minds think alike
failしていると煩わしいので、dependabotで発火した場合はそもそもジョブを実行しないようにするか pull_request_target
、workflow_run
を使うようにすると良さそうです。
今回はdependabotで発火した場合は、ジョブを実行しないようにするワークフローを例として残しておきます。
name: example on: push: jobs: verify-actor: runs-on: ubuntu-latest outputs: actor: ${{ steps.echo.outputs.actor }} steps: - name: echo actor id: echo run: | echo "::set-output name=actor::${{ github.actor }}" trigger_deploy: runs-on: ubuntu-latest needs: [verify-actor] if: needs.verify-actor.outputs.actor != 'dependabot[bot]' steps: - uses: actions/checkout@v2 - name: something run: | echo "hey"
一つのジョブ内で条件分岐をしようとすると、全てのstepにifを入れないといけません。
そのため、最初に条件分岐をするためだけのジョブを作り、後続のジョブの if
で needs
の outputs
を参照することで
条件文の記述を1回にしています。
stepの出力を後続stepから参照する場合は、echo "::set-output name=key::value
形式で参照できるようにしておく必要がありますが、
jobの出力を後続jobから参照する場合は、jobのoutputs
と needs
を使う必要があります。(ややこしい)
参考
【Github actions】DependabotのPull RequestでSecretsが参照できずワークフローがFailになった場合の対処 - Simple minds think alike
ECSでunicornを動かすときの注意点
ECSはタスクが停止すると、各コンテナにまず SIGTERM
を送ります。
しかし、Railsと一緒によく使われるunicornは SIGTERM
を受け取ると、gracefulに終了してくれません。
graceful shutdownさせるためには QUITシグナルをmasterプロセスに対して送る必要があります。 Signal handling
デフォルトの停止シグナルは SIGTERM ですが、これは Dockerfile に STOPSIGNAL ディレクティブを追加することによってオーバーライドできます。この停止シグナルは、シャットダウンの命令をアプリケーションに通知します。
やり方としてはshell scriptのハンドラーを書いたり、STOPSIGNAL
ディレクティブをDockerfileに追加する方法もあると思いますが、今回の場合は
unicorn.rb
に Signal.trap
して自身のmasterプロセスに対してシグナルを送るのが一番簡単だと思います。
before_fork do |server, worker| Signal.trap 'TERM' do Process.kill 'QUIT', Process.pid end
参考
Nginx の limit_req
Nginxの流量制限する場合に使用される limit_req の使い方について調べてみました。 NGINX Rate Limiting の簡単なメモになります。
limit_req
Nginxのrate limitでは、パケット通信の帯域制限などで使われているleaky bucket algorithmに従ってキューイングします。
基本的な設定
http { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; ... server { ... location /search/ { limit_req zone=one burst=5; }
ひとつずつ設定を見ていきます。
まず、limit_req_zone
directive で httpリクエストに対する制限を定義できます。
ここではあくまでも定義しているだけなので、実際に実装する場合は location
ディレクティブ内などに設定を書く必要があります。
limit_req_zone
は key、zone、rateという3つのパラメーターを持ちます。
key
その右にある $binary_remote_addr
はクライアントのIPアドレスを表現するNginxの変数です。
zone
アクセス制限のかけたURLに対して頻繁にアクセスしてくるそれぞれのIPアドレスを記録するためのメモリについての設定です。 これはNginxのワーカープロセス間で共有されます。 目安として1メガバイトには約16,000のIPアドレスを記録することが可能です。 Nginxが新しいIPアドレスを記録する必要があるときにメモリが枯渇している場合は、最も古いIPアドレスを削除し、メモリを開放します。 それでもまだスペースが足りず不十分な場合は503を返します。 また、上記の枯渇問題を防ぐためにNginxは新しいIPアドレスを登録するたびに過去60秒間に使用されていない最大2つのエントリを削除します。
rate
最大のリクエストレートを設定します。 上記の例では、1秒あたり1リクエストを超えることができないことを表していますが、 実際にはNginxはミリ秒の粒度でリクエストを追跡するため100msごとに1つのリクエストに対応します。
burst
burst パラメーターをzoneで指定されたrateを超えてクライアントが実行できるリクエストの数を定義できます。 burstを超えたリクエストは全てキューイングされます。
例: 20スロットが空でリクエストが21個飛んできた場合は、1つをアップストリームに流す。 次に残りの20リクエストをキューイングし、100ミリ秒ごとにデキューする。
burst with nodelay
nodelay が設定された場合は空きスロットがない状態は503、 空きスロットがある場合はリクエストは処理され、スロットが使用済みになります。
例: 20スロットが空でリクエストが21個同時に飛んできた場合は、21リクエスト全て処理をし、20個のスロットを確保する。 その後、100ミリ秒ごとにデキューして消化。
まとめ
burstなし、burstあり、burstあり+nodelayの3パターンあります。
本番環境で使う場合は nodelay をつける場合が多そう。
参考
Goで自作パッケージを作るときのちょっとした動作確認
SongmuさんのGo Conference 2019 Summerでの発表資料に
パッケージ内の関数の動作確認とかは gore -pkg . が異常に便利です
と書いてあって、使ってみたら結構便利だったので備忘録として残しておきます。
-pkg string the package where the session will be run inside
合わせて読みたい
RFC違反のメールアドレス
日本の大手キャリアであるdocomo、auが2009年ごろまでに作成したメールアドレスはRFCに準拠していないものも作成可能でした。 今日はRFC違反のメールアドレスを考慮した正規表現とAmazon SES、SendGridの対応状況について調べてみました。
RFC違反のメールアドレスを考慮した正規表現
https://qiita.com/sakuro/items/1eaa307609ceaaf51123
HTML5の input[type=email]
と同じ正規表現をかけると良いです。
Amazon SES
検証結果を見る限り、RFC違反のメールアドレスでも送れるものと送れないものがあるみたいです。
SendGrid
2013年頃はRFC違反のメールアドレスでも一部送れるようだったが、2019年に仕様が代わり ローカルパートをダブルクォーテーションで囲まないといけないらしいです。