SpringOnion: Railsの遅そうなEXPLAINをログ出力するgem

SpringOnionというRailsの遅そうなEXPLAINをログ出力するgemを作りました。

github.com

某kamopoさんのMySQLCasualLog.pmの移植?です。

cf. ふつうのWeb開発者のためのクエリチューニング

以前の移植とは異なって、Arproxyを必要とせず、単体で動きます。

何年前の話だよといわれそうですが、未だにスロークエリと戦っていたりするので…

Rails 3以前に搭載されていた、遅かったクエリを自動的にEXPLAINする機能とは異なり、フィルタで選別されたすべてのクエリに対してEXPLAINを実行し、まずそうなキーワードが出現したらログに出力します。

また、EXPLAINの実行先は開発用のDBだけでなく任意のDBに向けられます(例: ステージング環境のDBなど)。

使い方

GemfileにSpringOnionを追加して

group :development, :test do
  gem 'spring_onion'
end

enviroments/*.rbinitializersにSpringOnionのEXPLAIN実行先DB情報を記述します。

if Rails.env.development? || Rails.env.test?
  # EXPLAINを実行するDB
  SpringOnion.connection = Mysql2::Client.new(host: 'my-host', ...)

  # ActiveRecordの接続先と同じにする場合は↓な感じで
  #SpringOnion.connection = ActiveRecord::Base.connection.raw_connection
end

SpringOnion.enabled = trueにするかSPRING_ONION_ENABLED=1で、遅そうなEPLAINがログ出力されるようになります。

実行例

#!/usr/bin/env ruby
require 'active_record'
require 'spring_onion'

ActiveRecord::Base.establish_connection(
  adapter:  'mysql2',
  host: 'db',
  username: 'root',
  database: 'sakila'
)

SpringOnion.enabled = true # or `SPRING_ONION_ENABLED=1`
SpringOnion.connection = ActiveRecord::Base.connection.raw_connection
SpringOnion.source_filter_re = //

class Actor < ActiveRecord::Base
  self.table_name = 'actor'
  self.primary_key = 'actor_id'
end

Actor.all.to_a
root@d786c737d8a4:/mnt# bundle exec ./test.rb

SpringOnion   INFO  2020-07-19 03:19:05 +0000   {"sql":"SELECT `actor`.* FROM `actor`","explain":[{"line":1,"select_type":"SIMPLE","table":"actor","partitions":null,"type":"ALL","possible_keys":null,"key":null,"key_len":null,"ref":null,"rows":16,"filtered":100.0,"Extra":null}],"warnings":{"line 1":["slow_type"]},"backtrace":["/mnt/lib/spring_onion/explainer.rb:6:in `execute'","/mnt/test.rb:21:in `<top (required)>'"]}

SPRING_ONION_JSON_PRETTY=1で見やすく。

root@d786c737d8a4:/mnt# SPRING_ONION_JSON_PRETTY=1 bundle exec ./test.rb

SpringOnion   INFO  2020-07-19 03:19:43 +0000   {
  "sql": "SELECT `actor`.* FROM `actor`",
  "explain": [
    {
      "line": 1,
      "select_type": "SIMPLE",
      "table": "actor",
      "partitions": null,
      "type": "ALL",
      "possible_keys": null,
      "key": null,
      "key_len": null,
      "ref": null,
      "rows": 16,
      "filtered": 100.0,
      "Extra": null
    }
  ],
  "warnings": {
    "line 1": [
      "slow_type"
    ]
  },
  "backtrace": [
    "/mnt/lib/spring_onion/explainer.rb:6:in `execute'",
    "/mnt/test.rb:21:in `<top (required)>'"
  ]
}

上記の場合、"type": "ALL"が引っかかってwarningが出ています。

warning

https://github.com/winebarrel/spring_onion/blob/d659b2ca4ef2fda68a08179c9a3c8323299f604c/lib/spring_onion/config.rb#L4-L26

warningにはMySQLCasualLog.pmと同じルールを設定していますが、新しく追加することも可能です。

例えば、rowsが10,000行を超えていたら、warningするなど。

SpringOnion.warnings[:too_many_rows] = lambda do |exp|
  exp['rows'] > 10000
end

フィルタ

SQLとbacktraceのソースコードのパスでフィルタリング可能です。

たとえばSPRING_ONION_SQL_FILTER_RE=actor bundle exec ./test.rbと実行すると、actorが含まれるクエリだけEXPLAINします。

また、SPRING_ONION_SOURCE_FILTER_RE=my_path bundle exec ./test.rbと実行すると、パスにmy_pathが含まれるソースコード経由で実行されたクエリだけEXPLAINします。(デフォルトは/app/

その他

ログ出力先は標準出力がデフォルトですが、ファイルなどに変更可能です。

SpringOnion.logger = Logger.new('/foo/bar/zoo.log')

CSVとかTSVをJSON LinesにするツールをGoとRustで書いた

CSVとかTSVをJSON LinesにするツールをGoとRustで書いた。

Go版

github.com

$ printf 'foo,bar\nbar,zoo' | xjsonl -keys a,b
{"a":"foo","b":"bar"}
{"a":"bar","b":"zoo"}

Rust版

github.com

$ printf 'foo,bar\nbar,zoo' | xjr -k a,b
{"a":"foo","b":"bar"}
{"a":"bar","b":"zoo"}

モチベーションとか

qrnを作った関係でテキストをJSON Linesに変換するツールが欲しくなったのだが、あまり上記のようなツールが見つからず、joで頑張るのもめんどくさい、ということで自作した。

かなり小さいツールなので、Rust学習のためにポートするにはちょうどいいかと考えてRust版も作成。

Rust入門

The Rust Programming LanguageRust By Exampleを読んでいたのだけれど、いまいち頭に入らず。 最終的に「プログラミング言語Rust入門」という書籍をKindleで購入。

プログラミング言語Rust入門

プログラミング言語Rust入門

全部がカバーされているわけではないけれど(マクロの書き方とかアトリビュートの詳細とかジェネリクスの細かいところなどはなかった)、足がかりとしてはよい本だったと思う。

Rust版を作りながら思ったこと

  • unwrapまわりが最初わかりにくかった
    • https://qiita.com/nirasan/items/321e7cc42e0e0f238254 がとても参考になった
    • Goから入った人間としては「そんなにパニックしていいのか?」と思ったが、log.Fatalfを使ってたりするので、同じなのかも。unwrapよりはexpectでわかりやすいメッセージを表示した方が良さそうだけど
  • dbg!マクロは最初の方で知りたかった…
    • 参照にしないとたまにコンパイルエラーになったりするあたり、新鮮味を感じた
  • strとStringがあるのがややめんどくさい
    • とりあえずstructに併せてStringを標準的に使うようにしたけれど、正しいのかわからない
      • 引数は&strにしてみた
    • Stringのマクロかリテラルが欲しい
  • ファイル分けの標準がいまいちよくわからなかったので、Goっぽくはしてみた
    • モジュールまわりは解説を読むよりは https://github.com/shadowmint/rust-starter が参考になった
    • main.rsとlib.rsを共存させるよりはmain.rs内でmod hogehoge;と書いてしまった方が、CLIを作るときは楽なんじゃないかなーと思ったけれど、そういう例は見つからず…
    • →素直にlib.rsを書け、という気になってきた
  • ジェネリクスに慣れていないせいで、最初「キャストの方法は?」と探しまくった
  • クロージャがやや使いにくい気がした。定義ごとに個別の型になっているせい?なんとなく、引数で受け取るときはfn()使うよりもFn()を使っておいた方が無難な気がした
    • Golangのノリで、関数を返す高階関数を書こうと思ったけれど、めんどくさい感じになったので諦め。結局、関数ポインタを多めに使うようにした
    • クロージャに限らず、戻り値でのジェネリクスは鬼門感がある
  • テストはモジュール内で定義されていたほうがきれいな気がしたので https://github.com/winebarrel/xjr/tree/master/src/xjr のようにしてみたが、いいのか悪いのかわからず
    • mod.rsに実装書くのはアリなのかな?
  • ロスコンパイルはcrossでサクッとできた
  • splitにややはまった
    • "".split(",") -> ""
    • Rubyが特殊な仕様なのかも
  • trim_rightがdeprecatedでこだわりを感じるなど
  • Goに比べると実行ファイルがとても小さいのでよかった
  • 所有権については難しそうなイメージの割にほとんど考えることはなかった
  • ネストが深くなりがちなのはmatchのせいだろうか?
    • xjrは見直してみるとなんかGoっぽいかんじになってしまった

その他、Rustで参考にしたサイト

Rust所感

  • 個人で書いている分にはとても楽しい。よい言い方ではないけれど、パズル的な楽しみがある
  • プロジェクトで採用するには結構決断力がいりそう。慣れているか高スキルのメンバーが多くない限り、Golang選びそう
    • Goは手の筋力が必要だけど、Rustは脳の筋力が必要な感じ
  • Golangはランタイムの豪華さがやはり最大のメリットだなーと思った。ただ、今回は並行プログラミングまわりを全然比べられていないので、その辺は別途深追い予定
    • 関係ないけど、vlangはランタイムをがんばらないとGoほどのメリットは感じられなさそうな気がした

qrnというDBベンチマークツールを作った

qrnというDBベンチマークツールを作りました。

github.com

これは何?

羅列されたクエリを実行するだけのDBベンチマークツールです。 今のところMySQLにしか対応していませんが、PostgreSQLへの対応はそんなに難しくないと考えています。

羅列したクエリを実行するだけなので、クエリのログ(MySQLならgeneral log)をほぼそのままテストデータにすることができます。 逆に同じクエリを異なるパラメーターで実行するようなことはできないので、そういうむきであればJdbcRunnerなどを使った方がいいと思います。

Installation

https://github.com/winebarrel/qrn/releases から最新版をダウンロードしてください。

Usage

$ echo '{"query":"select 1"}' >> data.jsonl
$ echo '{"query":"select 2"}' >> data.jsonl
$ echo '{"query":"select 3"}' >> data.jsonl
$ qrn -data data.jsonl -dsn root:@/ -nagents 4 -rate 5 -time 10 -histogram
/ run 184 queries (20 qps)

          57µs - 115µs -
         115µs - 173µs -
         173µs - 231µs ---
         231µs - 289µs ------------
         289µs - 346µs ----------
         346µs - 404µs ------------------------------------------
         404µs - 462µs --------------------------------------------------------
         462µs - 520µs ------------------------------
         520µs - 760µs ----------

{
  "Started": "2020-05-13T11:18:14.224848+09:00",
   ...
}

主要なオプション

  • -dsn DSN
  • -data テストデータ。JSON Lines形式
  • -nagents 並列数。1 agentが1コネクション・1 goroutineで実行
  • -rate 1 agentが1秒に実行するクエリ数の上限。たとえば2 agentでrate 5を指定すると、期待されるQPSは10QPS
  • -time テストの実行時間(秒)

テストデータ

上記の通りテストデータはJSON Linesです。 デフォルトで"query"キーのクエリを実行しますが、-keyオプションで変更できます。 クエリ以外のキーは無視します。

テストデータはメモリにロードしないので、大きめのデータでもクライアント側のメモリを圧迫することはないと思います。 逆に、10万qpsとか高いスループットが必要な場合、それが足を引っ張る…かも(そこまで試せていないです)。

MySQLの場合、general logをJSON Linesに変換する必要があるので、以下のような変換ツールを作って、logrotateのタイミングでjsonlに変換するようにしています。

/var/log/general.log {
   # ...
    postrotate
        # ...
       /usr/local/bin/mysql-general-log-parser /var/log/general.log-$(date +%Y%m%d%H) | gzip > /var/log/general.log-$(date +%Y%m%d%H).jsonl.gz'
    endscript
}
#!/usr/bin/env ruby
require 'json'

module MysqlGeneralLogParser
  HEADER_REGEXP = /\A(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[A-Z]+)\s+(?<id>\d+)\s+(?<command>\w+)\b(?<argument>.*)\z/m

  Chunk = Struct.new('Chunk', :time, :id, :command, :argument)

  def parse(io:)
    chunk = nil

    io.each_line do |line|
      if HEADER_REGEXP =~ line
        yield(chunk) if chunk
        chunk = Chunk.new
        chunk.time = Regexp.last_match[:time]
        chunk.id = Regexp.last_match[:id]
        chunk.command = Regexp.last_match[:command]
        chunk.argument = Regexp.last_match[:argument]
      elsif chunk
        chunk[:argument] << line
      end
    end

    yield(chunk) if chunk
  end
  module_function :parse
end

def main
  MysqlGeneralLogParser.parse(io: ARGF) do |chunk|
    puts chunk.to_h.to_json
  end
end

main

結果の出力

テスト結果はJSONで標準出力に出力されます。

{
  "Started": "2020-05-13T11:18:14.224848+09:00",
  "Finished": "2020-05-13T11:18:24.559912+09:00",
  "Elapsed": 10,
  "Queries": 189,
  "NAgent": 4,
  "Rate": 5,
  "QPS": 18.287694303306097,
  "ExpectedQPS": 20,
  "Response": {
    "Time": {
      "Cumulative": "78.389862ms",
      "HMean": "392.47µs",
      "Avg": "414.761µs",
      "P50": "418.565µs",
      "P75": "462.099µs",
      "P95": "532.099µs",
      "P99": "735.68µs",
      "P999": "760.585µs",
      "Long5p": "632.823µs",
      "Short5p": "218.38µs",
      "Max": "760.585µs",
      "Min": "182.384µs",
      "Range": "578.201µs",
      "StdDev": "90.961µs"
    },
    "Rate": {
      "Second": 2411.0260584461803
    },
    "Samples": 189,
    "Count": 189,
    "Histogram": [
      {
        "57µs - 115µs": 1
      },
      {
        "115µs - 173µs": 1
      },
      {
        "173µs - 231µs": 4
      },
      {
        "231µs - 289µs": 14
      },
      {
        "289µs - 346µs": 12
      },
      {
        "346µs - 404µs": 48
      },
      {
        "404µs - 462µs": 63
      },
      {
        "462µs - 520µs": 34
      },
      {
        "520µs - 760µs": 12
      }
    ]
  }
}

よく見そうな値は

  • QPS: クライアント側でのスループット
  • ExpectedQPS: agent数×rateで算出した期待されるQPS。QPSとExpectedQPS乖離が大きいほどキャパオーバーが大きい(と思われる)
  • Response: 実行した全クエリの応答時間の統計値です。

JSON以外のメッセージについては標準エラー出力に出力されます。

-histogramオプションをつけるとヒストグラムが出力されます

          57µs - 115µs -
         115µs - 173µs -
         173µs - 231µs ---
         231µs - 289µs ------------
         289µs - 346µs ----------
         346µs - 404µs ------------------------------------------
         404µs - 462µs --------------------------------------------------------
         462µs - 520µs ------------------------------
         520µs - 760µs ----------

-htmlオプションをつけると、ヒストグラムのHTMLファイルが出力されます。

f:id:winebarrel:20200515140956p:plain

このへんは https://github.com/jamiealquiza/tachymeter の機能ですね。

モチベーションとか

とあるサービスで使われているDBのベンチマークを取ろうとしたとき、人間がテストデータを書くと、データによって性能の偏りが大きく「サービスで使われているDBのワークロードを再現するのは難しい」「general logをそのままテストデータに使えないかな」と思っていたのと、ほかのベンチマークツールを使っているといろいろいじりたいことが多くて(出力結果など)「自前でベンチマークツールを作っておくと便利そう」と思ったあたりがモチベーションです。

misc

  • -rateオプションは身の回りで使った感じではまあまあの精度でqpsを制限できているのですが、もう少し厳密にできればいいんですが、逆にそこまで必要かなとも v1.2.0から、まあまあしっかりするようになりました

  • テストデータはファイル読み込みなのでシャッフルが難しく、いまのところagentごとに読み込み開始位置をランダムにしているだけです。とくに問題はないのですが、もう少し賢くしたいです

追記

スクリプトでのデータ記述に対応しました

https://github.com/winebarrel/qrn#use-script-as-data

$ cat data.js
for (var i = 0; i < 10; i++) {
  query("select " + i);
}

$ qrn  -script data.js -dsn root:@/ -nagents 8 -time 15 -rate 5

追記2

PostgreSQLに対応しました

追記3

general.logをパースしてjsonlにするやつを作りました

github.com

SendGridのv3 Mail Send APIのRuby Clientを書いた

年末で少し時間があったので、書こう書こうと思っていたSendGridのv3 Mail Sendを書きました。

V3 Mail Send API Overview - SendGrid Documentation | SendGrid

github.com

READMEのUsageを見ればわかるとおり、ほぼAPIに即した薄いクライアントです。

とりあえず、現状で公式クライアントは利用しにくいので、自作した感じです。 非公式とはいっても公式がスキーマを公開しているので、rakeタスクで公式のスキーマを取得して、メール送信ポストリクエストのボディーのバリデーションをJSON Schemaで行うようにしています。

require 'kani_laser'

client = KaniLaser::Client.new(api_key: 'ZAPZAPZAP')

# see https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/index.html
client.send_mail(
  personalizations: [
    {
      to: [
        {
          email: 'john@example.com'
        }
      ],
      subject: 'Hello, World!'
    }
  ],
  from: {
    email: 'from_address@example.com'
  },
  content: [
    {
      type: 'text/plain',
      value: 'Hello, World!'
    }
  ]
)

で、公式クライアント

SendGrid公式のRubyクライアントは、あまりインターフェースがよいとは思えなくて

require 'sendgrid-ruby'
include SendGrid

from = SendGrid::Email.new(email: 'test@example.com')
to = SendGrid::Email.new(email: 'test@example.com')
subject = 'Sending with Twilio SendGrid is Fun'
content = SendGrid::Content.new(type: 'text/plain', value: 'and easy to do anywhere, even with Ruby')
mail = SendGrid::Mail.new(from, subject, to, content)

sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
response = sg.client.mail._('send').post(request_body: mail.to_json)
puts response.status_code
puts response.body
puts response.parsed_body
puts response.headers
  • いくつかクラスを利用しているけれどすべてJSON生成のヘルパーで必須ではなく冗長
  • 送信用のメソッドが#_で、ちょっとわかりにくすぎると思う…(メタプログラミングを避けた? あー、sendメソッドが使いにくかったせいか)
  • method_missingを使ったかなり薄いラッパーでメソッドやパラメータのチェックが弱い。ヘルパークラスも空白などを除去しているだけでチェックの役に立っていない
  • 気になる書き方がいくつか(固定文字列のJSONをわざわざパースしてHashにしている・必須引数に空白のデフォルト値を設定している)

といった感じで、うーん…とクビをかしげる状態になっています。 v1からv2への変更で大幅に修正が入っていて、なぜこのようなわかりにくいインターフェースになったのか、ぜんぜん経緯を理解できていないです。v3 APIへの対応リソースが足りなかったんだろうか?

一方でOpen API specificationも公開していて、今回私の書いたKaniLaserはそこからschemaをとってきてリクエストパラメーターのJSONJSON Schemaでパリデーションするようにしています。

リクエストパラメーターのバリデーションができれば、インターフェースはシンプルになりそうですが、公式クライアントはなんか頑張っている?せいで煩雑です。なぜ

API定義はしっかりしているように見えるので、そこからどうとでもクライアントは生成できると思うのですが、なんでこんな状況になっているのかよくわからない…

わからない。全くわからない。


公式がちょちょいとやれば、メタ情報から各種言語のクライアントを自動生成できそうだけれど、なぜやってないんだろう? oas.jsonからクライアントを自動生成できそうだけど。 やっぱりリソースの問題かなぁ、Ruby優先度低いのかなぁ…うーん…

蛇足

https://sendgrid.api-docs.io/ を参照すればちょちょいのちょい…と思っていたんですが、oas.jsondefinitions、足りなくないですかね。email_objectとか。

最終的に https://github.com/sendgrid/sendgrid-oai/ のマスターをスキーマのソースにするようにしたんだけど、これ、正しいのかなぁ

カニレーザー

dic.pixiv.net

ridgepole: v0.7.6でignoreという属性をつけられるようにした

ridgeporeのv0.7.6でignoreという属性をカラム・インデックス・FKにつけられるようにした。

github.com


あんまり周知していなかったせいで、全然知られていないが、ridgepoleでもexecuteがつかえる。

https://github.com/winebarrel/ridgepole#execute

create_table "authors", force: :cascade do |t|
  t.string "name", null: false
end

create_table "books", force: :cascade do |t|
  t.string  "title",     null: false
  t.integer "author_id", null: false
end

add_index "books", ["author_id"], name: "idx_author_id", using: :btree

execute("ALTER TABLE books ADD CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES authors (id)") do |c|
  # Execute SQL only if there is no foreign key
  c.raw_connection.query(<<-SQL).each.length.zero?
    SELECT 1 FROM information_schema.key_column_usage
     WHERE TABLE_SCHEMA = 'bookshelf'
       AND CONSTRAINT_NAME = 'fk_author' LIMIT 1
  SQL
end

冪等性を担保するために少しだけ拡張していて、ブロック内の返り値がtrueの場合のみ、executeを実行するようにしている。 ブロックにはコネクションを渡しているので、存在チェックをすることで重複してexecuteが実行されないようにすることができる。

…が、たとえばN-gramのフルテキストインデックスのように、Railsが対応していないカラムやインデックスをexecuteで作成しようとするとめんどくさいことになる。

例えば

create_table "books", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
  t.string "title", null: false
  t.index ["title"], name: "ft_index", type: :fulltext
end

execute("CREATE FULLTEXT INDEX ft_index ON books(title) WITH PARSER ngram") do |c|
  rows = c.raw_connection.query('SHOW INDEX FROM books', as: :hash)
  !rows.map{|r| r.fetch('Key_name') }.include?('ft_index')
end

のような感じでインデックスを貼ろうと思うと、先に「N-gramではないインデックス」が作成されてしまって、「N-gramのインデックス」はexecuteでは作成されなくなる。

executeのブロック内のクエリで頑張って「N-gramではない場合は再作成」というようにすれば、「N-gramのインデックス」を貼ることはできるが、流石にめんどくさいので、カラムやインデックスにignoreという属性をもたせることにした。

ignoreを使うとこんな感じになる

# -*- mode: ruby -*-
# vi: set ft=ruby :
create_table "books", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
  t.string "title", null: false
  t.index ["title"], name: "ft_index", type: :fulltext, ignore: true
end

execute("CREATE FULLTEXT INDEX ft_index ON books(title) WITH PARSER ngram") do |c|
  rows = c.raw_connection.query('SHOW INDEX FROM books', as: :hash)
  !rows.map{|r| r.fetch('Key_name') }.include?('ft_index')
end

…先ほどとほぼ同じだけれど、ignore: trueがついたカラム・インデックス・FKについては、ridgepoleは無視、全く変更に関与しないようにしている(つもり)。 なので、このスキーマ定義はRailsでは対応していない「N-gramのインデックス」を冪等性を保ったまま作成することができる(はず)。

テストが通ったらリリースする予定。

apexでrubyをデプロイする

ちょうど4日ほど前に、apexにrubyサポートのPRがマージされていた。

github.com

なので、すこし試してみた。

apexのビルド

goのバージョンは1.11.4

GO111MODULE=onでビルド・インストールできた。

(apexのmasterをcloneしてから)
export GO111MODULE=on
make
make local

バージョンが更新されないのは、なんでなんだろう…?

$ apex version
Apex version 1.0.0-rc2

lambdaファンクションのデプロイ

ディレクトリ構成はこんな感じ。

test
├── functions/
│   └── my_ruby/
│       ├── function.json
│       └── lambda.rb
└── project.json

デフォルトのファイル名はlamba.rbでハンドラ名はhandler

def handler(event:, context:)
  {hello: 'world'}
end
$ apex deploy my_ruby
   • config unchanged          env= function=my_ruby
   • updating function         env= function=my_ruby
   • updated alias current     env= function=my_ruby version=1
   • function updated          env= function=my_ruby name=test_my_ruby version=1
$ apex invoke my_ruby
{"hello":"world"}

Gemの同梱

bundlerでpathを指定すれば普通にできた。

source 'https://rubygems.org'

gem 'hashie'
cd functions/my_ruby/
bundle install --path vendor/bundle
test/
├── functions/
│   └── my_ruby/
│       ├── Gemfile
│       ├── Gemfile.lock
│       ├── function.json
│       ├── lambda.rb
│       └── vendor/
└── project.json
require 'hashie'

def handler(event:, context:)
  {test: Hashie::Mash.new(hello: 'world')}
end

require "bundler/setup"とかは不要なよう。

C拡張の同梱

docker run -v $(pwd):/mnt -w /mnt ruby:2.5 bundle install --path vendor/bundle

とかやったらできるかと思ったら、libruby.so.2.5: cannot open shared object fileでエラー。

Amazon Linuxで拡張を作成しないとだめっぽい?ので、こちらを参考にDockerイメージを作成。

FROM amazonlinux

RUN yum -y install \
      gcc-c++ \
      openssl-devel \
      readline \
      libyaml-devel \
      readline-devel \
      zlib \
      zlib-devel \
      git \
      bzip2 \
      tar \
      make

RUN git clone https://github.com/sstephenson/rbenv.git ~/.rbenv && \
    git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build && \
    ~/.rbenv/plugins/ruby-build/install.sh

ENV PATH /root/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN rbenv install 2.5.3
RUN bash -c 'rbenv init - > /etc/profile'
RUN rbenv global 2.5.3
RUN bash -l -c 'gem install bundler'

ENTRYPOINT ["bash", "-l", "-c"]
source 'https://rubygems.org'

gem 'postfix_status_line'
docker run -v $(pwd):/mnt -w /mnt ruby-for-lambda bundle install --path vendor/bundle
require 'postfix_status_line'

def handler(event:, context:)
  status_line = "Feb 27 09:02:37 MyHOSTNAME postfix/smtp[26490]: D53A72713E5: to=<myemail@bellsouth.net>, relay=gateway-f1.isp.att.net[204.127.217.16]:25, conn_use=2, delay=0.57, delays=0.11/0.03/0.23/0.19, dsn=2.0.0, status=sent (250 ok ; id=20120227140036M0700qer4ne)"
  {parsed: PostfixStatusLine.parse(status_line)}
end
$ apex deploy my_ruby
   • config unchanged          env= function=my_ruby
   • updating function         env= function=my_ruby
   • updated alias current     env= function=my_ruby version=3
   • function updated          env= function=my_ruby name=test_my_ruby version=3
$ apex invoke my_ruby
{"parsed":{"time":"Feb 27 09:02:37","hostname":"MyHOSTNAME","process":"postfix/smtp[26490]","queue_id":"D53A72713E5","to":"*******@bellsouth.net","domain":"bellsouth.net","relay":"gateway-f1.isp.att.net[204.127.217.16]:25","conn_use":2,"delay":0.57,"delays":"0.11/0.03/0.23/0.19","dsn":"2.0.0","status_detail":"(250 ok ; id=20120227140036M0700qer4ne)","status":"sent"}}

ただこれは、Amazon Linuxが必要というわけじゃなくて、rbenvでインストールしたC拡張にlibruby.so.2.5へのリンクがないせいじゃないだろうか?

misc

apexのアクティビティをみると、先行き不安な気がする。

f:id:winebarrel:20181217231739p:plain https://github.com/apex/apex/graphs/contributors

前回のリリースは5月だし、https://github.com/apex/up に力を入れているようだし、うーむ…

とはいえCFnを使わないでlambdaだけに焦点をあてたメジャーなツールがapexぐらいしか見つからないので、しばらくはapexだよりかなぁ

active_record_mysql_xverify

active_record_mysql_xverifyというgemファイルを書いた。

github.com

これは何?

RailsでAuroraを使うときに、フェイルオーバー時に旧マスタにつなぎに行く問題をなんとかするやつ。 世間様を見渡すに「コネクションプール使うな。接続を切れ」というのが現状の解決策のようだけど、もうちょっとなんとかしたかったので作成。

使い方

config/environments/production.rbとかに

ActiveRecordMysqlXverify.verify = ->(conn) do
  conn.ping && conn.query('show variables like "innodb_read_only"').first.fetch(1) == 'OFF'
end

または

ActiveRecordMysqlXverify.verify = ActiveRecordMysqlXverify::Verifiers::AURORA_MASTER

と書いておくと、いい感じにしてくれます。

R/W splittingをしして、コネクション毎に有効にするかしないかを分けたい場合はActiveRecordMysqlXverify.handle_ifを使ってください。

ActiveRecordMysqlXverify.handle_if = ->(config) do
  config[:host] == 'my-cluster...'
end

あと、デフォルトではSQLの実行時にエラーが起きた場合、コネクションを検証するようになっているので、すべてのリクエストでコネクションを検証したい場合は

ActiveRecordMysqlXverify.only_on_error = false

としてください。

実装について

Railsはコネクションプールからのチェックアウト時にverify!を実行して*1、検証に失敗すると再接続する*2ようになっているので、verify!を上書きしてinnodb_read_onlyを見るようにしただけです。

すべてのリクエストでshow variablesを実行するのもアレなので、デフォルトではエラー時のみ。

MariaDB Connector/J

これを実装するにあたってMariaDB Connector/Jの実装を読んだり動作検証をしたりしたのだけれど、faster failoverでやっていることは

  1. SQLを実行
  2. エラーをハンドリング
  3. 再接続
  4. SQLを再実行

というのが大きな流れのようだった。

エラーハンドリングの判断にエラーの種別を見たり、別スレッドでコネクションの状態をチェックしていたり*3、といろいろやっているけれど、基本的にはactiverecord-mysql-reconnectと変わらないような気がする。

トランザクションが切れた状態でのSQLの再実行って、トラブルありそうだけどあんまり問題になってないのかな?

ActiveRecord::QueryCache

コネクションプールからのcheckoutとcheckinのタイミングが重要なので、ActiveRecordのコードをちまちま読んでいたんだけれど、Rails5でQueryCacheはRackのミドルウェアじゃなくなってた*4

checkoutは最初にコネクションにアクセスしたタイミングでされるけど*5checkinはQueryCacheに依存している気がする*6

ミドルウェアじゃなくなったのも、その辺の「コネクションプールの管理に必須だから」みたいな判断もあるのかな?あるいは単にAPIの変更の影響だろうか?

QueryCacheのレイヤが完全に削除されるとcheckinが機能しなくなりそうなんだけど、そのへんどうなんだろう?よくわからない。