valid,invalid

関心を持てる事柄について

Airflow webserverのUIがなぜ2種類存在するのか

AirflowでDAG実行時にGUI, CLI, REST APIからパラメータを渡す - valid,invalid件の調査をしていて知ったのだが、Airflow webserverのUIには2種類のUIが存在する。

利用できる機能に差分があるうえに、Airflow本体のリリースフローも初見ではわからず、ややこしかったので整理する。

2種類のUI

1. flask-admin based web UI

1系ではデフォルトで使用されるUI。名前の通りflask-adminベースで作られている。

2. FAB based web UI

Roll Based Access Controlを提供しているのでドキュメントではRBAC UI と表記されることもある。 突然の略称のため名前からはわかりづらいがFlask AppBuilderベースで作られている。

1.10.0以降で利用可能 (changelog) であり、airflow.cfg[webserver] rbac = Trueを設定するとこちらになる。

なお、flask-admin based web UIはLegacy UIと表され、2系ではFAB based web UIのみの提供となる予定とのこと。


一部の機能は現時点で最新である1.10.12でも FAB-based web UI にしか存在しない。 同じバージョンでもviewに差分がある例↓(GUI から DAG Run に conf を渡すフィールドがFAB-basedの方にしか存在しない)

歴史的経緯

Improving Airflow UI Securityで明晰に語られている。

2015年のAirflowはセキュリティに関する機能が少なかった

誰もがメタデータDBを見られる、グローバルに共有されているオブジェクトを編集できる、DAG を開始または停止したり失敗した TaskInstance を成功マークにしたり、その逆をしたりすることができる…といった具合。

Nginxなど別の機構でGUIへのアクセスを制御することはできたがかなり不便だったのでRBAC UIに取り組み始めた。

Airflow webserverはFlask Adminで構築されており、RBAC UIを実装するために4つの可能性のあるアプローチが検討されていた

  1. Django への移行
    • ビルトインのユーザー認証システムと、より成熟した拡張機能のエコシステムを備えている
    • 移行作業の量が利益に比べて限定的となる
  2. クラッチ
    • Airflow に合わせてカスタマイズされた RBAC システムを構築することができ、この機能を時間をかけて進化させる柔軟性が得られる
    • 利用できる資産を利用しないので多大なコスト
    • 想定しているRBACセキュリティモデルはかなり汎用的なものであるため、ゼロからの実装は魅力的でなかった
  3. Flask extension
    • Flask-RBACやFlask-Principalなど、RBACを扱うために特別に書かれたflask拡張機能
    • 拡張機能はいずれも事前に定義されたロールを用いる必要があるが、Airflowではconfigurableであってほしい
  4. Flask-Appbuilderに切り替える
    • Flask-AppBuilder (FAB) は Flask-Admin に似た小さいフレームワーク
    • ビルトインの RBAC システムがconfigurable
      • セッション管理、様々な認証バックエンドとの統合ができる
    • Apache-Superset] で使用されていて実績がある

最終的にFABが採用された。メンテナは機能を統合して Airflow をshipすることに集中し、FABがほとんどを処理してくれることを期待した。

実際の移行作業

どちらもDjangoインスパイアのフレームワークであり、慣習も踏襲されていたので良かった。ORMのSQLAlchemyサポートを両者ともしてたのでモデル層は全く手を入れずに済んだ。

たくさん手を入れたのはViewであり、いくつかの機能が足りなかったぶんはFABにパッチを送った。

レポジトリ戦略

もともと別レポジトリで概念実証のために開発していた。

https://github.com/wepay/airflow-webserver 今は404

おかげで検証や開発は高速で進んだが、Airflowの早い進化に追従するには本体にマージしなければいけない。代替UIのための別レポジトリを維持するコストもかかるし、Airflow本体と誤認されたり、間違ってインストールされたりしたくないため。

互換性

Airflowは下位互換性を重視するため、ユーザーが新しいUIに移行するための十分な時間を確保したかった。レガシーUIを完全に廃する前に、短期間は両方のUIを並行して利用できるようにすることを決めた。

  • 既存の UI 関連のコードはすべて www/ ディレクトリに置く
  • コードベースがif文だらけにならないように、UI関連の変更はすべてwww_rbac/という新しいディレクトリで行い、RBAC専用の別のFlaskアプリを作成した

リリース

RBAC UIは2018-08にAirflow 1.10.0でリリースされた

Changelog — Airflow Documentation

airflow/UPDATING.md at master · apache/airflow · GitHub

今後

Airflow 2.xからはレガシーUIは提供しなくなるため、1系を使っているユーザーにもrbac = Trueで試すことを推奨している。

airflow/UPDATING.md at master · apache/airflow · GitHub

Airflowをベースにしている[Google Clound Composer]ではRBAC UIをサポートしていないため、一部機能は2系になるまで使えないはず。Clound IAMでおおむね権限を制御できるかもしれないがDAGレベルの制御は難しそうかも(触ったことないので不明)。


masterairflow/www/views.pyにmergeされているし、shipされたはずの機能がなぜ手元で表示されないのか…?と悩んでいたら、flask-admin based web UIだった、というハマりがあったので謎が解けてよかった。ちょうど移行の過渡期にAirflowに触り始めてしまっただけだった。

大きなOSSの内部大改装は作業だけでなくストラテジを練ったり広報したりも含めてものすごい大変というのが垣間見えた。Airflowメンテナが、www以下への変更をリリース前にwww_rbacへcherry-pickしていてかなり大変そうだったので早く2系が公開されて負担が減っていくとよいと思う。

AirflowでDAG実行時にGUI, CLI, REST APIからパラメータを渡す

Airflow webserver GUI▶️ 再生アイコンからTrigger DAGをクリックすると事前にDAG定義の内容で[DAG]が実行される。

ただ、以下のようなユースケースのために、実行するDAG(以下、DAG Run)にパラメータを渡したいことがある。

  • バッチによってはパラメータ付きで手動実行したい
    • JenkinsのParameterized Build的な機能がほしい
  • 一時的なデータ投入・更新バッチのために毎回DAG定義を書くのではなく、汎用的なDAGを用意してパラメータで実行するコマンドを動的に切り替えできると便利

Conf option

DAG RunにJSON形式でパラメーターを渡す Conf というオプションがあり、様々なインタフェースから渡せる。

個人的には「2. GUIからパラメータ付き実行」が欲しかったもの。

1. CLIからパラメータ付き実行

CLIでは--conf (-c) optionからJSONを渡すことができる

-c, --conf JSON string that gets pickled into the DagRun’s conf attribute

Command Line Interface Reference — Airflow Documentation

airflow trigger_dag -c '{"key1":1, "key2":2}' dag_id

For Apache Airflow, How can I pass the parameters when manually trigger DAG via CLI? - Stack Overflow

2. GUIからパラメータ付き実行

Trigger DAGでなくAdd DAG RunからもDAGの実行が可能。ヘッダのBrowse -> DAG Runs -> + アイコンクリックで新規DAG Run作成画面に遷移する。

f:id:ohbarye:20201018153053p:plain

この画面にconfフィールドが1.10.8から追加された。ただし、デフォルトのFlask Admin based UIではこの項目は表示されず、Flask AppBuilderベースのRBAC UIでなければ表示されない。

3. REST API

Airflow WebServerはREST API経由でのDAG操作を受け付ける。Experimetalなので今後のversionで更新される可能性は高い。

$ curl 'http://localhost:8080/api/experimental/test'
{"status":"OK"}

API経由でDAGの実行を指示することができ、confをbodyとして渡せばDAGにパラメータを渡せる。

REST API Reference — Airflow Documentation

 $ curl -X POST \
   http://localhost:8080/api/experimental/dags/<DAG_ID>/dag_runs \
   -H 'Cache-Control: no-cache' \
   -H 'Content-Type: application/json' \
   -d '{"conf":"{\"key\":\"value\"}"}'

DAGファイルの記述

DAGファイル内において conf パラメータを参照できる箇所は限られている。環境変数のようにどこでも取得できてしまうとworkflow定義が動的に変わってしまうため制限しているようす。

Operatorへ渡す引数

Operatorへ渡す引数のいくつかは template 機能を活用することができるため、以下のように渡すことができる。

bash_task = BashOperator(
    task_id="bash_task",
    bash_command='echo "Here is the message: '
                 '{{ dag_run.conf["json_key"] }}" ',
    dag=dag,
)

Jinja template による文字列展開を行っているため多少動的に書くこともできる。

f:id:ohbarye:20201018153512p:plain
template を利用できるオプションは API document に `(templated)` の記述がある
template 内で利用できるマクロ一覧はMacros reference — Airflow Documentationを参照

各バッチを実行する command、または environment にこの template 記述を与えることでパラメータ付きの実行が可能になる。

定期実行の場合は conf の中身のJSONは空になるためDAGファイルまたはバッチ側にて、渡したいパラメータが空のケースをサポートする必要がある。

e.g. '{{ dag_run.conf["json_key"] if dag_run else ""}}'

    override = {
        "containerOverrides": [{
            "name": ECS_TASK_ID,
            "command": [
                "bash",
                "-c",
                "bundle exec rake perfect_batch"
                ('TARGET_ID={{ dag_run.conf["TARGET_ID"] if dag_run else ""}}'
                 ' bundle exec rake withdrawal:trigger_gmo_pg_callback')
            ],
        }]
    }

    t2 = ECSOperator(
        overrides=override,
        ...
    )

Python Operator

Python Operatorで実行される関数の引数から参照できる。

def run_this_func(ds, **kwargs):
    print("Remotely received value of {} for key=message".
          format(kwargs['dag_run'].conf['message']))


run_this = PythonOperator(
    task_id='run_this',
    provide_context=True,
    python_callable=run_this_func,
    dag=dag,
)

Custom Operator

自前で書いた Operator の中でも kwargs を介して参照できそう(未検証)。

from airflow.models.baseoperator import BaseOperator
from airflow.utils.decorators import apply_defaults

class HelloOperator(BaseOperator):

    @apply_defaults
    def __init__(
            self,
            name: str,
            *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.name = name

    def execute(self, context):
        message = "Hello {}".format(self.name)
        print(message)
        return message

環境

  • Airflow 1.10.9

参考

Airflow 1.10.6ではweb serverのREST APIがデフォルトで全公開されている

問題

Airflow webserverはREST APIを公開しており、DAGの参照や作成などが可能となっている。

# 疎通確認
$ curl 'https://your.domain/api/experimental/test'
{"status":"OK"}

1.10.6のデフォルトの設定では破壊的操作を含むAPIを認証なしで全公開するinsecureな作りになっていた。

経過

本体でも危険性に気づき、[AIRFLOW-6027] Disable API access by default by mik-laj · Pull Request #6625 · apache/airflow · GitHubdefaultの挙動が修正された。

また、1.10.11でshipされたChange default auth for experimental backend to deny_all by ashb · Pull Request #9611 · apache/airflow · GitHubにより、airflow.cfgの設定がデフォルトでdeny_allとなるように修正された。

推奨される設定

API機能を利用しないAirflow webserverであればairflow.cfgで当機能をオフにしておくのが望ましい。

 [api]
 # How to authenticate users of the API
 -auth_backend = airflow.api.auth.backend.default
 +auth_backend = airflow.api.auth.backend.deny_all

/api/experimental というエンドポイント名、気概を感じる

参考

Sorbetのドキュメント読みつつ型付けの練習

仕事で利用しているライブラリがSorbetを使っており、sorbet-runtimeをupgradeするdependabotのpull requestsがバリバリ供給されてくるのでもう少し中身を知っておきたいと思って公式ドキュメントを拾い読みしたりしたメモと所感。

ドキュメントではStripe社内の実態の話がちょいちょい出てくるのと、Ruby3との関わりあたりが興味深かった。

また、Sorbet関連のプロジェクトを少し触ってみてパッチを送ってみる過程でも型をブッ込んだプロジェクトってこんな感じなのか〜と思うところがあった。

大きくは2つの構成要素

Overview · Sorbet

sorbet gem

  • コードの静的解析を行うCLIツール
  • 実行時には必要ない

sorbet-runtime gem

  • Rubyのコードにtype annotationを加えられるようにする
  • T namespace と sig methodによるsignatureの記述
  • 実行時に動的に型検査を行う

漸進的型付け

Gradual Type Checking & Sorbet · Sorbet

型の恩恵を受けるためだけにすでに存在している巨大なコードベースをrewriteしたり、言語が持つ大きな資産を捨てて別言語に乗り換えるのは現実的ではない。特にRubyにはものすごい量と質の資産がある。

既存資産と型の恩恵の両方を活かすにはTypeScriptが実践して成功を収めたような漸進的型付けが有効となる。

Sorbetでは妥協点として、メソッド呼び出し・メソッド定義・クラス・ファイルそれぞれの単位で型検査をオプトアウトできるようにした。

T.untyped

Sorbetの型システムにおける型の一つ。漸進的型付けという観点でいえばもっとも重要な型。

  • すべての値が T.untyped型であることを主張することができる
  • また、T.untyped型のすべての値は他の型であることを主張することができる
  • 型付けを始めたタイミングではほぼすべてがuntypedである

漸進的型付けに短期的には役立つが長期的にはなくしていく型。

sigil

Enabling Static Checks · Sorbet

ファイルの先頭に記述するマジックコメントのこと。sigilによりsrb tcの型検査のレポート内容を5段階に分けることができる。TypeScriptでstrictnessを調整できるのに似ている。

trueないしstrictあたりが目指すところ。

# typed: ignore

  • Sorbetからはファイルが読まれない
  • Stripeではignoreしているファイルは、無い!

# typed: false

  • syntax、定数名の解決、sigの正しさのみレポートされる。sigは記述しなくてもよい
  • sigilを記述しない場合はデフォルトでこのレベルになる

# typed: true

  • type errorがレポートされるようになる
  • 存在しないメソッド呼び出し、メソッドの不正な呼び出し(引数間違い)、型とマッチしない変数の使い方
  • Stripeではもうすぐ99%のファイルがこのレベルに到達する!*1

# typed: strict

  • sigの記述がすべてのメソッドに必要
  • 定数、変数は明示的な型注釈が必要
  • TypeScriptでいえばnoImplicitAnyに相当する

# typed: strong

  • T.untypedを許容しない
  • ファイル内のすべてのメソッド呼び出しについて静的に型を決定できている
  • Stripeでもほぼ使われない
    • RBIファイルか、ほぼ空のファイルぐらい
  • Sorbetが機能を足すとT.untypedがもたらされることがあるのでstrongのサポートは最小限にとどめておく

実行時検査

Enabling Runtime Checks · Sorbet

sigによりシグネチャが付与されたメソッド呼び出しはラップされ、前後で以下を検査される。

  • 宣言にマッチする引数で呼び出されたか
  • 結果が宣言された戻り値にマッチするか

sigが間違っていたらリファクタリング対象の検出はできない。到達できるコードに到達できないとSorbetは判断してしまうことがある。また、将来的には型情報を利用したRubyの高速化もありえるが、誤った型情報はそのために使えないし、sigで嘘をつくと実際には遅くなることも十分ある。

なぜRuntime checkをするか

エコシステム含めてだいぶ参考にしているであろうTypeScriptとの大きな違いの1つがここだと思うので気になっていた。*2

SorbetがRuntime checkを重視するのは以下のため:

  • 型注釈に信頼が増す
  • 自動テストが型注釈のテストになる
  • ステージングや本番での型検査結果をモニタリングすることにより間違った型がコードベース全体に伝播する前に早期に検出できる
  • 型を付けたコードを型のないコードから守り、型の採用や型付けを促進する

型注釈と実装の一致をランタイムで検証することで乖離を防ぎ、結果として型の恩恵を受けやすくして型付けのインセンティブを増やす、という戦略のようだ。

実行時の挙動をコントロールする方法

.on_failure

sorbet-runtimeT::Configuration moduleを使ってType error発生時にはロギングするだけ、などの制御が可能。個別のsigにsig {params(argv: T::Array[String]).void.on_failure(:log)}のように定義することもできる。

.checked

実行時に型検査するかどうかを選べる。ただ、これを多用するとそもそもランタイムでやる意義が薄れるので推奨はしていないようす。

sig {params(argv: T::Array[String]).void.checked(:never)}

型定義ファイル

インラインでRubyファイルに型を書くこともできるがRBIファイル.rbiにも記述できる。文法上はRubyと全く同じように記述でき、メソッドの中身が空なだけ。

Cで書かれたライブラリに型を与えるためにもこの仕様は必要。

すべてのライブラリ作者が型注釈を書くのは現実的ではないのでDefinitelyTypedのように https://github.com/sorbet/sorbet-typed にライブラリの型を置くことができる。

Ruby3との関連

Types in Ruby 3, RBS, and Sorbet · Sorbet

SorbetはRuby本体の上に構築してきたしこれからもそうする方針であり、Ruby3 or Sorbetではない。

標準ライブラリの型定義を用意するのはとても大事。現在、Sorbetが標準ライブラリの型定義を持っているおかげで、ユーザーがSorbetを使い始めた瞬間にメソッド呼び出しのうち約25%が型情報を持つほどのカバレッジになる。

RBSの生成にはSorbetのRBIが使われるはずだし、Ruby本体が型定義のRBSを持つことでSorbetにもメリットがある。共存前提。

型付けの練習をするには

ちょっと触ってみたいと思ってやったこと

1. Sorbet playgroundで遊ぶ

Sorbet Playground

雰囲気はわかるがリアルワールドの例が見たくなった。

2. 型追加のcontributionを求めているプロジェクトで試す

Add more Sorbet typing to the gem · Issue #178 · Vonage/vonage-ruby-sdk · GitHubとか。

sigilのレベルを引き上げたり、ライブラリのコードを読みつつsigを書いてみることでだいぶ理解が深まる。srb tc -aにより推論されたシグネチャを自動付与したり、生成された定義を見てみるのも面白い。

うまくいったらpull requestを送れて一石二鳥。

3. Sorbet本体を触る

Sorbet本体でも標準ライブラリの型定義が一部足りなかったりする。

C++で書かれていたりビルド・テストツールにBazelを使っていたりとRubyist的にあまり馴染みがないところもあるが.rbiに定義を足したりテストしたりするのはRubyでもなんとかなりそうだったのでちょっとしたパッチを送ってみた。

github.com

最初のビルドには30分かかった。

所感

TypeScriptを時折書いている身としては型注釈を書く体験自体に違和感はなかった。めっちゃ書きたいわけじゃないけど書いてるプロジェクトに入ったなら普通に書ける、みたいな。

ただ、複雑なRubyコードに対する型をまだ書いてないのでリアルワールドでtyped: strictレベルで戦っていける感じはまだしていない。


Vonageのgemを読んでいて思ったのはcontributionのハードルを下げてくれるということ。

  • ライブラリのコードを読むうえで実装者の意図がわかる
  • 静的検査、テストでの実行時型検査でしょぼいミスを防げる

Stripe社内でもこんな感じらしいのでライブラリに限らず、ではあるがOSSプロジェクト的にも嬉しいポイントではありそう。


Runtime checkで受けられる恩恵の本質は実際にSorbetを導入したプロジェクトを運用してみないとなんともいえないかも。

*1:https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet より

*2:TypeScriptの場合はランタイムがJSなので、JSでできないことはできないという制約は別途あるが…。いちおうTypeScriptにもRuntime checkのライブラリがあるけどメインストリームとはいえないと思うのでスルー。Denoだとランタイムの型検査があるのだっけ

最近買って良かった漫画(2020年7月〜9月)

2020年7月~9月あたりで買ってよかった漫画。 元々集めていて続刊が出ただけのやつは除いて、新しく見知った漫画に限定。

葬送のフリーレン

最終回の「数年後─────」みたいなやつに弱いんですよ。

物語としてのクライマックスを超えて"精神の浄化-カタルシス-"を得たあと─────"その後"の世界が描かれることで読者の脳内によりくっきりとキャラクターや世界観が生き生きと印象付けられること、あると思います。

で、葬送のフリーレンなんですが全部"後日譚-それ-"っていう。

突然後日譚を語られても知らんがなって感じなんですが、主人公(ということになっている)フリーレンの視点で現在とオーバーラップされる過去の「思い出」の差し込み方や描き方がとても巧み。

過去の謎を解き明かしつつ現在を語るタイプのストーリーテリングといえば"The Haunting of Hill House"も良かったな〜。

それで「思い出」といえばジョジョの奇妙な冒険第6部 ストーンオーシャンなんだよな…

生きるという事はきっと「思い出」を作る事なのだ・・・ F・Fはそう悟っていた─────

フリーレンは現在の旅を通じて過去の冒険をちゃんと"思い出"にしていく…それ、"生きる"ってことじゃん…

大ダーク

林田球先生はドロヘドロだけじゃなく魔剣Xから追っています(マウント)がストーリーテリングさらに上手くなっているな〜と(尊敬)。

「けっこう行き当たりばったりで作品を描いていく」みたいな話をどこかで先生がされていたと思うのですが、それにしては、謎と解決、新キャラクターの登場、世界観の提示…これらファクターの繋げ方が天才的。

ドロヘドロ同様にグチャッとしたシーンはかなり多いですが『ぼのぼの』なみにほのぼのしているという奇妙な作風も相変わらずで好きです。

斬り介とジョニー四百九十九人斬り

ショートギャグ漫画の専門と思っていた榎本俊二先生の異色の殺陣マンガです。チェンソーマン経由で知りました。そういえばチェンソーマンも"例"のオマージュシーン「クァンシと魔人達四十九人斬り」が出てくる7巻に続いて8巻が発売されましたね。

ストーリーはほぼないようなもので、チャンバラ表現にただただ興奮するための漫画です。

さんかく窓の外側は夜

このマンガがすごい2020で選出された『異国日記』のほうが有名な印象ありますが、個人的には『さんかく窓』推し。

良い作品というのは得てしてジャンルを越境するものですが『さんかく窓』に関して言えばホラー、ミステリ、サスペンス、超能力、BLあたりが綯い交ぜになっているような感じですかね。まぁ正直読んでいるとジャンルなんかどうでも良くなってくるんですが。

青野くんに触れたいから死にたい』あたり好きな人はハマりそうな気がする。

しかしながら『異国日記』といい『さんかく窓』といいこれだけ面白い漫画をよく並行して連載できるな〜。

アンデッドアンラック

やり尽くされていそうな能力バトルや"不死"の設定でも"ここ"まで持っていけるか〜〜〜

キャラクターの登場や展開が早く"引き込み力"がめっちゃ高いところと、各キャラクターの能力が明かされる大ゴマで超太字が入るところがめっちゃ好きですね。読まないと何言ってるかわからないと思うけど…。

新人の作品なんですね、苛烈なジャンプ連載戦争でなんとか生き残ってほしい。

度胸星

最近だと『望郷太郎』や『へうげもの』ですかね、山田芳裕先生といえば。

度胸星』のラストは悲しみの打ち切りという話を聞いていてなかなか手を出せずにいたのですが、読んでみるとなかなかどうして、めっちゃ良い…。

新装版 度胸星(1)

新装版 度胸星(1)

SF要素よりかはヒューマンドラマや友情・努力・勝利の成分が多いのでSFというくくりで敬遠する人がいたらもったいないと思う。

いつか続刊が出てくれないかな〜。

水は海に向かって流れる

子供はわかってあげない』の田島列島先生の新作。このマンガがすごい2020で選出されていたので知っている方も多そう。

つい最近3巻で完結しました。

前作に続いてふつうっぽい人たちがふつうからちょっと外れた人間関係や悩みに向き合っていく話、といえばそれまでなんですが、"向き合うこと"を描くのが本当に上手い。

現実世界において人と向き合うということは物語のようにスッキリと終わることばかりではないし、そもそも人はかんたんに行動には起こせない。そういう前提があってこその細かい描写が良い。

女の園の星

説明すると面白くなくなるタイプの漫画です。

ファーストシングルをリリースしました

昨年バンド活動を始めまして、先日ついにファーストシングルをリリースしました。今回のバンドでは作詞作曲は別メンバーが担当しており、自分は主にベースを弾いています。

配信サービスで聞けます

SpotifyApple Musicを始めとした各種配信サービスで視聴することができます。

各種配信サービスへのリンクは以下から↓

linkco.re

配信代行サービスとの契約周りをリーダーのエア氏が頑張ってくれました。大感謝。

イラスト

ジャケットのイラストはまつもとまち氏に依頼させて頂きました。

ボーカルのちあき氏が「イメージにめちゃめちゃピッタリな絵を描かれる方!お願いしたい!」とワイワイ言いつつ、お仕事を請けていただけるかおそるおそるお尋ねしたところ快諾いただきました。

世界観を汲み取っていただいて本当に大満足です。素敵なイラストをありがとうございます。

レコーディング

コロナ禍で外出を制限していたので今回のレコーディングのほぼすべては各メンバーが自宅で非同期に宅録し、エア氏がミックス〜マスタリングをやってくれました。

やれるもんですね。

タワーレコードの新人発掘的なやつ

タワーレコードの新人発掘的なやつに取り上げていただきました。

今後

昨年はライブを目標に邁進していましたがコロナ禍では厳しいため楽曲のレコーディングと配信が活動の軸になりそうです。

コロナ禍以前にバンドで作っていた楽曲が他にもあるのでそれらも精力的に発表していきたいと考えています。


楽曲の視聴、Twitterアカウントのフォローよろしくお願いします!

"好きなこと"で、"生"きていく !?

ISUCON10 予選敗退した

ISUCON10にソロチームBPM200で参加し、最終スコア770で予選敗退しました。通過スコアには大きく届きませんでした。

戦略

ソロなので時間は絶対に足りない、という前提のもと予めフォーカスするポイントはある程度決めておきました。

大方針としてはなるべく1台で勝負する、ということ。複数台構成に持っていくのは自分のスキル的に時間がかかりそうだし、過去問で素振りしてたとき、問題によってはアプリケーションレイヤでの改善に絞ってもそれなりに伸ばせると思ったため。今回もそうだと良いな〜と思ってましたが…現実は非常。

また、普段使わない解析ツールをインストールしたりログ設定を行ったりするのにかける時間もないため、それなりに手慣れたNewRelicでボトルネックの推移を見ながらチューニング対象を決めていく、というのも予定していました。

やったこと

環境準備

ssh設定確認したりgit initしたりマニュアルを読んだりマシンスペックを確認したり。

bot対策の概念を知ったけどボトルネックなのかどうかわからないので初手では手を出さないことに。

NewRelic導入

素振っていたとおり初手でNewRelicを入れました。導入後、最初に回せたベンチマーカーで出たスコアが280ぐらい。

ベンチマーカー流しつつtopコマンド眺めているとDB, CPU律速で、アプリケーションとしては物件検索と椅子検索がmost time consumingとNewRelicから理解しました。

f:id:ohbarye:20200920145442p:plain
NewRelic便利

インデックス追加

初期実装ではインデックス皆無なので物件検索と椅子検索のコードを読みながらインデックスを追加しました。複雑なパターンすべてに対応はできないだろうと思いながらも、Nginxのアクセスログを見ると単一条件のクエリがそこそこあり、スコアがちょっとだけ伸びました。

このときMySQL5.7の降順インデックス周りの挙動を知らず、EXPLAINしてfilesortになっているのはなんでだろう…とか思ってました。

範囲検索からID検索へ(失敗)

物件検索原因は範囲検索に対して有効ではないアプローチと考え、rentやwidthのidで検索できるように実装を変更。

generated columnにするとかadd columnして一括updateするとかすれば良かったのですが、何を血迷ったか、rent, width, heightのマスタテーブルと中間テーブルを作り、initialize処理の中でレコードをインサートしまくるという方針で実装してしまいました。

その実装に合わせて検索や物件追加のエンドポイントのコードも直す大掛かりな作業になったのですが案の定initializeでタイムアウトする結果になり…泣く泣くrevert。

ぜんぜんISUCON向きの実装ではないし今思えばいろいろツッコミどころがあるのですがここに3時間ぐらい使ってしまったのがこれ以降の思考に大きく影響を与えることに。

Bulk insert

検索周りで数時間溶けた失敗が心に来たので少しでも稼げそうなところを見つけては直してくことにしました。

物件と椅子のN+1インサートはまとめてやることに。このエンドポイントはボトルネックでもなんでもないということを知りつつ…!

物件検索結果をメモリキャッシュ

Nginxのアクセスログからだいたい同じクエリが飛んでくることはわかっていたので、検索条件で結果をキャッシュすることにしました。

これはちょっと効きました。

なぞって検索のN+1なくす(失敗)

最後の2時間ぐらいでもまだ物件検索と椅子検索が支配的ではあったもののなぞって検索が上位に追い上げてきたので見てみることに。わかりやすいN+1があったものの位置情報まわりで何してるかを理解するためにMySQLのドキュメントを読むのにそこそこ時間を使ってしまいました。

f:id:ohbarye:20200920154953p:plain
nazotte

POINT型カラム追加を試したり、クエリをマージしてN+1を直したはずがベンチマーカーが失敗するようになり、デバグを途中で断念…。

ここも成果ゼロにも関わらず2時間ぐらい溶けてました。

low_priced API用のindex追加

安い椅子と物件を検索するAPI (low_priced) もそこそこ時間を食っていてindexが効いてないことを見つけたので追加。

bot対策(失敗)

残り時間がなく、すぐに点数が伸ばせそうなものとしてbot対策があったことを思い出す。Nginxの設定をググりながら入れてみる。

が、スコアがさほど伸びていないため最後の時点でもbotからのアクセスは来てなかったとはず…。明らかに優先度判断ミスっていました。

その他

最後の1時間で細々とした仕上げをしていました。

  • RACK_ENV=productionを設定
  • NewRelicをオフにする
  • Nginxなどの各種ログをオフにする
  • 再起動して問題ないか確認

反省

明らかに点数を稼げる大型の問題には気付いたものの、物件検索の改善アプローチを間違えて時間を溶かしてしまってからは明らかに焦ってしまい、冷静な優先度判断や時間配分ができずに終わってしまった…という感じです。小さい点数稼ぎはできてもブレイクスルーがなかった。

そうなってしまった原因はMySQL周りの力不足。generated column、降順インデックスだけでなく位置検索・空間インデックスあたり(この辺は他の参加者も苦戦していそうでしたが)の知識がなく誤ったアプローチをしてしまったり、DB分割はアイデアすら出てこなかったな〜。

感想

結果は惨敗でしたが自分の力不足を強く感じることができたうえめっちゃ学びがあったので参加できてよかったです。

噛みごたえのある良問を作ってくださったyosuke_furukawaさん、過去にない規模のチーム数にも関わらず捌き切ってくださった運営の皆様、スポンサーその他関わった大勢の方に大感謝です。

来年も楽しみにしています。