valid,invalid

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

Idempotency-Key Headerの現状・仕様・実装の理解を助けるリソースまとめ

Idempotency-Key Header に関する調査と実装を半年ぐらい前に行ったので、そのとき参考にしたリソースと 2021 年 9 月時点で得られる最新情報をメモしておく。

前提: Idempotency-Key Header とは

HTTP リクエストのうち冪等ではないとされるリクエス*1を冪等にし、安全なリトライを可能にするための仕組みの 1 つ。

Jayadeba Jena, Sanjay Dalal, Erik Wilde 氏らによって 2021 年 11 月に仕様が提案された。現在は IETF のもと、インターネット標準化過程(Standard Track)にあり、IETF Meeting や GitHub issues にて議論が行われている。real world ではすでに多くの企業で類似する実装が行われている。

現状と今後

GitHub issues を見る限り論点はいくつか残っており、RFC になるにはまだ時間がかかりそう。

また、de facto と言えるほどサーバ側の実装パターンはまだ固まっていない印象。実装する際には後述する Airbnb や Stripe の記事などを参照しつつ、各実装者が多少なり手心を加えて設計をしなければならなさそうだ。

今後の最新議論を追いかけるなら GitHub issues や IETF meeting をウォッチすることになると思う。


ここからリソースまとめ。

仕様を説明するもの

The Idempotency-Key HTTP Header Field

https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-00

2021-07-01 に提出された最新の draft。IETF HTTPAPI WG によって accept されたもの。 ちなみに初出は 2020-11-17*2

解決したい課題、API サーバおよびクライアントに期待する振る舞い、先行する実装等々、全体像を掴むのに十分な情報がある。

POST リクエストを冪等処理可能にする Idempotency-Key ヘッダの提案仕様

https://asnokaze.hatenablog.com/entry/2020/11/19/015243

2020 年 11 月、初期の draft 公開を受けての記事。日本語でわかりやすい簡潔な説明。

2021 年 9 月時点では日本語で "dempotency key header" を検索したときに最上位に来るのが当記事。

ちなみに、そのあと英語の記事がいくつか続いた後に僕の Scrapbox のページが来るぐらいに情報が少ない。英語で検索しよう。

2021-09 時点での最新議論

IETF 111 Meeting の発表資料

https://datatracker.ietf.org/meeting/111/materials/slides-111-httpapi-idempotency-header-00

2021-07-26 の IETF 111 Meeting 資料。現在のステータスと議論の状況をざっと掴める。

Discussion on GitHub

https://github.com/ietf-wg-httpapi/idempotency

draft を受けての各界からの反応・意見・課題について議論している repository。

いくつかななめ読みしてみる。

#2 Clarification for status code for various scenarios

各シナリオで返す status code や命名について。「英語が母国語じゃない自分としては〜」のような意見が寄せられるのも面白い。

#3 Feedback from Google Standard Payments

protocol agnostic な実装であるべきでは?と Googler からの指摘。個人の意見として書いているようだが、gRPC を推す Google 側の position talk と見られる節はある。

#5 How does this header compare with OASIS Repeatable Requests Header?

類似する実装として OASIS (Organization for the Advancement of Structured Information Standards) によるRepeatable Requests Headerが存在するがどう使い分けるか、歩調を合わせるか、はたまた統合の道を辿るか。

各種 SaaS の先行実装

IETF なのですでに実装 (Running code) がある*3

draft の "4. Implementation Status" に記されているものを参照するとよい。

https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-00#section-4

主に決済・金融系の SaaS が公開する API で実装されている。先行実装にはIdempotency-Key の名前を使うものもあれば、Request-Idと呼んでいるものもある。

SaaS で実装されている、いくつか代表的なものを挙げる。

Stripe

https://stripe.com/docs/api/idempotent_requests

動画もあった。

https://www.youtube.com/watch?v=nnMqSQtSZUQ

各言語の client 実装があるが、いずれも利用者が Idempotency Key のことを知らなくても自動でセットするようになっている。知らなくても安全にリトライできる良い設計と思う。

もちろん、key のことをよく知る実装者であれば form 入力値などによって「一意性」を決定し、key を生成できる。

以下は Ruby client の例。

https://github.com/stripe/stripe-ruby/blob/v5.38.0/lib/stripe/stripe_client.rb#L869-L873

      # It is only safe to retry network failures on post and delete
      # requests if we add an Idempotency-Key header
      if %i[post delete].include?(method) && config.max_network_retries > 0
        headers["Idempotency-Key"] ||= SecureRandom.uuid
      end

Amazon Pay

https://developer.amazon.com/ja/docs/amazon-pay-api-v2/idempotency.html

日本語で読める。

Note: 冪等キーは各 Amazon Pay 事業者アカウントに固有のものであり、無期限に保存されます。これは、同じキーを異なる事業者アカウントに使用できることを意味します。

key が無期限に保存されるタイプ。

draft によれば、expiry を設定するかどうかは任意。

2.3. Idempotency Key Validity and Expiry

The resource MAY enforce time based idempotency keys, thus, be able to purge or delete a key upon its expiry. The resource server SHOULD define such expiration policy and publish in related documentation.

PayPal

https://developer.paypal.com/docs/business/develop/idempotency/

Idempotency-Key header の draft を書いたうちの 1 人が PayPal のメンバーなのだが、PayPal 自身はPayPal-Request-Id header を使っているというのが興味深いポイント。PayPalには"歴史"がありそうだ。

実装するときに参考になるリソース

各種 SaaS の仕様は参考になるが、当然ながら、記されているのは外から見た振る舞いの定義のみ。

仕組み自体はシンプルなので draft を読み込めばコア部分はわりとすんなり実装できるものの、細かい点において迷うポイントがあった。key を保存するストレージに何を選ぶか、エラーシナリオで返す status code は何か、など。

いざ自分で実装してみようと思ったときに参考になる記事をいくつか。

Designing robust and predictable APIs with idempotency

https://stripe.com/blog/idempotency

2017 年、@brandur が Stripe 在籍時に書いた記事。分散システムにおいてなぜ idempotent な HTTP リクエストが重要か、といった基本思想のおさらい。

加えて、クライアントと API を設計する際に従うべきいくつかの基本原則。

  • 障害が一貫して処理されるようにする - クライアントにリモートサービスに対する操作を再試行させる。そうしないと、データが一貫性のない状態のままになってしまい、将来的に問題が発生する可能性がある
  • 失敗が安全に処理されるようにする - idempotency および idempotency key を使用して、クライアントが一意の値を渡し、必要に応じてリクエストを再試行できるようにすうる
  • 失敗が責任を持って処理されることを確認する
    • Exponential Backoff や jitter を使って、立ち往生している可能性のあるサーバに配慮する

先述した Stripe の Ruby client の実装を読むと Exponential Backoff や jitter のお手本が見られる。

Implementing Stripe-like Idempotency Keys in Postgres

https://brandur.org/idempotency-keys

これも 2017 年の@brandur の記事。

記事はとても長いのだが Sinatra で書かれた実装が公開されており、この実装をベースに解説しているので非常に参考になる。

https://github.com/brandur/rocket-rides-atomic

同実装では key のストレージとして RDBMS を使用している。これは key の保存とリソースの状態の更新が atomic でなければならないため。リクエスト単位で atomic なのではなく、1 リクエストの中で atomic な処理が複数ある複雑なケースではそういうこともある。

また、リクエストハンドラの中だけでなく idempotency key の reap (expire したレコードの削除) や completer (中途半端に終わったリクエストをあとで非同期に完了させる) の実装も参考になる。

Patterns of Service-oriented Architecture: Idempotency Key

https://multithreaded.stitchfix.com/blog/2017/06/26/patterns-of-soa-idempotency-key/

2017 年に concept を解説している記事。トレーサビリティへの言及が良い。

  • とある idempotency key が既存のトランザクションを見つけるために使用されたログを記録しておく
  • サービスがいつ既存のレコードを見つけたのか知ることができる
  • 不適切に設計された idempotency key algorithm を使っていることを発見できる

Good diagram.

Avoiding Double Payments in a Distributed Payments System

https://medium.com/airbnb-engineering/avoiding-double-payments-in-a-distributed-payments-system-2981f6b070bb

2019 年の Airbnb による記事。分散システムに結果整合をもたらすテクニックのうち、write repair を実現するのが Idempotency-Key である、という導入から始まる。

Airbnb が実装している Orpheus と呼ばれる Java library と設計の基本理念が参考になる。API call を3つのフェーズ(Pre-RPC, RPC, Post-RPC)に分けて考え、API が DB に書き込むのは Pre, Post-RPC フェーズのみとし、APIがさらに downstream や external services (payment processor や bank) にリクエストする部分を RPC とするあたりなど。

他にも、クライアントのリトライや Idempotency-Key の生成方法、key を DB から読み出すときは必ず master を参照する、などのテクニックが詳説されている。

Idempotency-Key IETF standards draft

https://brandur.org/fragments/idempotency-key-draft

2021 年 7 月、 https://news.ycombinator.com/item?id=27729610 を見ての@brandur の記事なので比較的新しい。HackerNews と合わせて読むと面白い。

Googler のコメントでは「ヘッダーではなくペイロードに入れたほうが良い」と主張があり、これは gRPC との相性のためと@brandur は見ている。

@brandur は「リクエストボディと分離しておくことで、クライアントが意識せずに idempotency を実現できる」と反論している。

実際に Stripe の API クライアントはそのように実装されているので説得力がある。

その他

国内ではメルペイから数多くの知見が公開されている。


各リソースを再訪して細かくコメントしていたら本記事が思ったより長くなってしまった。全体をもう少し整理したら薄い本にできるような気もする。

*1:https://datatracker.ietf.org/doc/html/rfc7231 , https://developer.mozilla.org/en-US/docs/Glossary/Idempotent あたり参照

*2:https://datatracker.ietf.org/doc/html/draft-idempotency-header-01

*3:IETF の仕様策定プロセスではラフコンセンサス。仕様より先に複数の独立した実装があることがほとんど。 https://www.nic.ad.jp/ja/tech/ietf/section4.html

OSS contribution の reward として 100 DEV (¥60,000相当) もらった

今年の6月頃にDEV Airdropという「全世界のOSS開発者に総額2億3000万円を配布」するキャンペーンに応募していた*1

OSS contributionの多寡によって対象となるかどうかやトークンの配布量 (≒金額) が決まる仕組みのため、まったく受け取れない可能性あるかなと思っていたが、100DEVトークンを受け取ることができた。

2021-09-05 時点では 1DEV が¥600 ~ 630 相当のため、100DEV は ¥60,000 ~ 63,000 相当*2

ref https://coinmarketcap.com/currencies/dev-protocol/

DEV Airdropとは

キャンペーンページは以下。

airdrop.devprotocol.xyz

DEV Airdropの前に、まず"Dev Protocol"というDeFi protocolがある。その開発母体がリリースした"Stakes.social"というサービスが1周年を迎えた記念としてDEVトークンをOSS開発者に配布しよう、というキャンペーンがDEV Airdrop。

キャンペーンページより抜粋。

"Dev Protocol", a made-in-Japan DeFi protocol, is launching a "GitHub Airdrop" to commemorate its first anniversary of releasing "Stakes.social", a sponsor platform, and as appreciation for your contribution so far.

Stakes.social

stakes.social

Stakes.social はOSS開発者、VRアーティスト、NFTマーケット、グリーンプロジェクト、Vtuberなど様々なクリエイターを応援し、持続的な資金調達を目指すプラットフォーム。ざっくり言うとクラウドファンディング的なやつと理解。

Issue

キャンペーンページより再び抜粋。

It's hard for developers to rely on goodwill from patrons if they wish to continuously develop OSS as well as grow the community itself. We'd like you to use the airdropped DEV tokens to help stake other projects, while focusing on OSS development yourself. By using our Stakes.social platform, you can help other projects in need and have a stream of additional income by doing so.

Stakes.socialの理念とも通じるが、DEV Airdropキャンペーンの背景には「善意に頼ったOSSの継続的開発やコミュニティ維持が困難である」という課題意識がある。

DEV AirdropキャンペーンではOSS開発者を対象にDEVトークンをairdrop (空中投下) するが、他のプロジェクトの支援に使ってもらうのが本意。Stakes.socialプラットフォームを使用することで、資金を必要としている他のプロジェクトを支援でき、支援者はさらに追加の収入を得ることができる*3


また、PR TIMESのプレスリリースも出ているので日本語での情報はそちらを参照。

prtimes.jp

受け取り方

受け取り方については別の記事で詳説したのでそちらを参考にしてほしい。

ohbarye.hatenablog.jp

報酬の使いみち

突然6万円が"降ってきた-airdropped-"わけだが、どうする!?

さっと換金して何かに使っても良いのだが、せっかくなので何かしらのプロジェクト支援に使ってみたいと思う。

時折話題に昇るOSS fatigueやburnoutによる開発停滞・停止等の諸問題は、いちOSS利用者にとっても無視できず、「善意に頼ったOSSの継続的開発やコミュニティ維持が困難である」という課題意識への共感があるため。

OSS関連の話題については過去にいくつか記事を書いている。


キャンペーンに感謝しつつ、もらえるものは喜びつつ、僕よりももっとdeservedな方々もいると思うのでそういう方々にも行き届くと良いなと思う。

DEVトークンのボラティリティも大きく、これからの伸びにも期待している。原資ゼロで降ってきた資金なのでこのままボーナスステージに突っ込むぞ!!

*1:2021-09-05時点ではキャンペーンの応募はすでに終了している

*2:2021年5月には 1DEV で¥2,000 の時期もあった

*3:利益を生む仕組みはよくわかっていない

DEV Airdrop の reward を受け取る方法

Dev Airdrop for GitHub によって配布されるDEV tokenの受け取り方をまとめる。

なお、本記事は暗号通貨の購入・送金、取引所の口座開設等に関する記述を含むものの、暗号通貨のあらゆる取引において生じた不都合・不利益について筆者は一切の責任を追いません。*1

お約束ではありますが自己責任でお願いします。

対象読者

  • DEV Airdropの配布対象者
    • 2021-07-01までにエントリー済
    • その時点でEthereum walletを作っているはず
  • 受け取り (claim) のやり方がわからない
    • walletにgas feeを用意する方法がわからない
    • どこから何を操作してclaimすればいいかわからない
    • claimした結果をどうやって確認すればいいかわからない

配布対象者かどうかの確認方法

"Connect to a wallet" > "Check Airdrop Reward" をクリックする。

以下のように "You can claim now"... と表示されていれば対象者である。

ハマりどころ

"Check Airdrop Reward" をクリックしてもローディングが終わらずに画面が遷移しないことがある。

自分の場合は、以下の手順で解決した。

  1. Chrome でシークレットウィンドウを立ち上げる
  2. "Connect to a wallet" をタップ時に表示されるQRコードスマホで読み取る
    • スマホには Metamask をインストールし、Airdrop にエントリーした時の wallet を設定済とする
  3. 署名するか聞かれるので署名する

手順

ざっくり言うとやることは3つのみ。

  1. Ethereum wallet に gas fee を用意する
  2. iroiro で claim する
  3. MetamaskでDEV tokenの残高を見られるようにする

ちなみに3番目は受け取りのためではなく着金確認のために行う。

なお、いずれもEthreum walletに関する記述はMetamaskでの操作を前提にしている。Metamask以外のwalletでもやることは変わらないはずだが行える操作や手順に差異があるかもしれない。

1. Ethereum wallet に gas fee を用意する

gas fee って何?という方は イーサリアムの「Gas(ガス)」って何?~イーサリアムの仕組みを理解しよう | CRIPCY あたりを参照。

ここではDEV tokenのclaimに伴って発生するEthereum network上での計算量であり、かつ請求する人が負担しなければならない費用、程度に捉えておけばよい。

公式による説明は以下の通り。

What do I need to claim

To create an Ethereum transaction for claiming purposes, a small amount of ETH is consumed as a gas fee. The gas fee will vary depending on Ethereum congestion, but it is safe to set aside around 0.005 ETH.

gas fee は Ethereum network の状況によって変動するものの、0.005ETH (記事執筆時点で2,000円程度) の残高があればよかろうとのこと。

(2021-09-01 22:48 追記)

gas fee の現在値や変動を知りたければ Ethereum Gas Price Charts & Historical Gas Fees – ethereumprice を見ると良い。唐突に高騰している時間帯もあるようだ。

[vim-jp Slack[(https://vim-jp.org/docs/chat.html)で@tsuyoshi_choさんに教えていただいた。

(2021-09-01 22:48 追記終わり)

AirdropのためにEthereum walletを作ったきりで放置している場合、walletの残高は0のままなので、入金なり送金なりで0.005ETHの残高がある状態にする。

いくつか方法があるので順に述べる。

購入する

最も簡易だが手数料などのトータルコストでやや高くつくと思われる。

Apple Payやデビットカードなどで購入し、そのままwalletに入金される。

Metamaskを開いてログインするとどこかに「購入」ボタンがあるのでクリックする。ちなみに、MetamaskのChrome拡張とiOSアプリではアイコンが異なっていて若干戸惑う。

Wyre / Transak

ざっくりいうとWyreやTransakは暗号通貨の購入をサポートする決済ゲートウェイ (決済代行会社) みたいなもの。詳細は省くが、ここにお金を払えばwalletに残高が入金されると思えばよい。

どちらも大差ない気がするが購入にかかる手数料や最低購入金額に差がありそう。

Apple Pay

WyreがApple Payに対応しているのでiOSアプリのMetamaskであればApple Payで支払える。Apple Payを設定済であればこの手段が最も早そう。

ただし最低購入額が$50からとのこと。あとで出金する前提なら多少は多めに入金してもよいと思う。

クレジットカード / デビットカード

Metamask上の説明を真に受けると

ちなみにどちらもプリペイドカードは受け付けていないように見える。

送金する

WyreやTransakは通貨の交換所みたいなもの、取引の中間業者なので当然手数料がかかる (gas feeとは別)。そのため違った形でEthreumを調達して送金することでやや安くなる。

すでに保有しているEthreumがあるならばwalletアドレスに送金するのが最も簡易で早い。

そうでなければ、さらに言えば暗号通貨取引所の口座を持っていなければ、最も手間がかかる。

口座開設

任意の暗号通貨取引所で口座開設する。

正直今回のためだけの用途であればどこでも良いと思う。必須なのは、日本円での取引ができるよう国内の取引所を選ぶこと、ぐらい。次いで、今回のclaimを安く済ませたいなら見るべきは入出金手数料、取引手数料あたりを見ることになる。けど大差なさそう。

ちゃんと比較していないがbitbank, Coincheck, DMM Bitcoinあたりの、入出金や取引の手数料のどこかで安く勝負してるところなら良いのではないか。

過去にbitFlyer, Coincheck, bitbank, Zaif, Binance, Bittrexなど色々な取引所に口座を作ったが結局メインで使っているのはbitbank。熱心なトレーダーではないので特筆する理由もないのだが…UIが全体的にすっきりしてて使いやすい。モバイルアプリも良く出来ている。

ETH購入 & 送金

暗号通貨取引所の口座開設が済んだらETHを購入する。販売所でなく取引所で購入すること。

違いは 【前編】「取引所」「販売所」ってなにが違うの?: 【わかる!暗号資産】初心者向け解説 :START! -基礎から学ぶ、マネー&ライフ-:朝日新聞デジタル などを参照。要はトレーダー間で取引できる取引所のほうが手数料が安く済むということ。

walletに送金する際の手数料がかかるため0.01ETHぐらい買っておく。 購入方法は各取引所のヘルプページを参照。

購入できたら送金する。 送金方法は各取引所のヘルプページを参照。

必要なのはwallet アドレスと数量だけ。walletアドレスはMetamaskなどのwalletから確認する。

送金transactionが完了するまで数分〜数十分かかる。transactionの進捗状況はEtherscanというサイトで確認できる。たいていの取引所では取引履歴からEtherscanへのリンクがあるはず。

2. iroiro で claim する

DEV Airdropのページにて "Claim with iroiro" をクリックする。

"WALLET CONNECT" をクリックする。

Metamaskでのログインを行う。

ここからスクショ取り忘れたが "Claim" 的なボタンが出たのでクリックしたはず。

次にMetamaskでtransactionの承認ダイアログが出たら承認する。

承認後、"You claimed token." と表示されていればOK。

"View on Etherscan" をクリックすることでtransactionの進捗確認ができる。

3. MetamaskでDEV tokenの残高を見られるようにする

Metamask で Dev トークンの残高を確認する - DEV Community に従ってカスタムトークンを追加する。

(2021-09-01 23:07 追記)

DEVのトークコントラクトアドレスの確認方法が迷いやすいので詳説する。

2の手順まで完了していれば、Metamaskからtransactionの履歴が確認できる。

f:id:ohbarye:20210901225929p:plain

f:id:ohbarye:20210901230013p:plain

f:id:ohbarye:20210901230040p:plain

ここからEthrerscanのtransaction詳細画面へ遷移する。かかったtransaction fee, gas price もこの画面で確認できる。

送信されたトークンの詳細には以下のリンクから遷移する。

f:id:ohbarye:20210901230240p:plain

DEV token の詳細画面に表示されている以下の値がトークコントラクトアドレスである。この値を使ってカスタムトークンの追加をMetamaskで行う。

f:id:ohbarye:20210901230406p:plain

(2021-09-01 23:07 追記終わり)

手順がうまくいっていれば以下のように100 DEVを受け取れたことが確認できる。

gas feeは約0.004ETHかかったので、状況によっては0.005ETHで安全ではないかもしれない。

ハマりどころ

カスタムトークン登録時に入力するトークコントラクトアドレスが間違っていると、カスタムトークンとしてDEVを追加できても永遠に着金は確認できない。

僕は以下のような誤りを犯した。

この操作を間違ってもtransactionの結果が失われるわけではなく、walletから見えていないだけなので心配無用。


claim手順に関しては以上。

受け取ったらどうする?

個人の資産として自由に使用すればよい。代表的な用途を2つ紹介する。

1. stakes.socialで任意のプロジェクトにstakeする

DEV protocolの理念・目的がクリエイターの応援、創作の収益化にあるので、もらったDEV tokenをそのままクリエイターにstakeするのも一手。

stakes.social

stakeする、の概念理解が正しいかわからないけど、こんな感じになるらしい。

  • 任意のクリエイターの任意のプロジェクトにお金を預ける
  • クリエイターが頑張る
  • 預けたお金に利子がつく
  • win-win

2. 出金する

walletでDEVをETHに交換する。

ETHを任意の取引所のETH預け入れ用アドレスに送金する。

取引所から銀行口座などに出金する。

うまい飯を食ったりガジェットの購入を通じて活力を得て、さらにOSSに貢献する。

感想

DEV AirdropはOSS contributorに還元するとてもありがたいプロジェクトであり謝意を述べたい、という前提をエクスキューズしつつ...

暗号通貨に馴染みが薄い身からすると、なかなか受け取るまでの手順が煩雑でハードルが高いように感じた。

自分以外にも、せっかくreward対象になったのにclaimのやり方がわからずに放置されている方々を見かけてもったいないと思ったため、本記事にて手順を紹介した。

*1:桁を間違えて購入したとか、トレードにハマって破産したとか

ISUCON11 ソロチームで参加して予選敗退しました

ISUCON11にソロチームBPM200で参加して予選敗退しました。

結果

  • 最終スコア: 13458
  • 最高スコア: 14484
  • 使用言語: Ruby

去年↓よりは1人でも戦えた気がしたものの順位上の結果としては大差なく終わってしまい残念。

ohbarye.hatenablog.jp

使用したツール

  • Vim
  • RubyMine
  • DataGrip
  • NewRelic
  • mysqldumpslow
  • top
  • git / GitHub

ちょっとしたコード変更はサーバ内でVimで書いてベンチマーカー回して良ければコミットして、複雑なコードの変更はRubyMine側でじっくり書いてからコミットするスタイル。

DBの中身を見たりクエリをExplainするのに最初はmariadbのREPLで頑張ってたのですが途中からDataGripでリモート接続して確認することにしました。クエリの補完や情報の見やすさなど、全てが圧倒的に優れているのでめっちゃ良かったです。

NewRelicは普段業務でも使うのでその延長で使わせてもらいました。スポンサー感謝です。

戦略

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

  • 1台構成でできるところまでやる
  • 素振りしてない計測ツールやプロファイラは使わない
  • アプリケーションのすべての仕様を理解しようとしない

振り返り

良かった点

  • 最初の環境構築の延長で使い慣れているツール(特にDataGrip)の設定をやった
  • めんどうなN+1のボトルネックから逃げずにSQLで勝負できた

良くなかった点

  • 全体的にアプリケーションコード側での解決に拘った
    • 12:30に9,000点まで伸ばせて前半の滑り出しは悪くなかったのですがそこからずっと伸び悩み
    • 多少時間をかけてでもNginxのチューニングや複数台構成に挑戦すべきところだったかなと思います(できたかどうかは別)

終了後に学んだこと

追試終了後の日曜以降に残った環境でいろいろ試してました。

  • APIとDBのサーバ分離
    • MySQLの設定周りでハマると思ったら意外とかんたんだったので素振りしておけばよかった
  • 繰り返し実行するコマンドをMakefile
    • サーバのrestartとかlogの削除とかそのへん。一人作業だからいいか…と言い訳して毎回typeしてたのですがMakefile書くぐらいはちゃんとやろう
  • NginxとAPIの通信をUnix domain socket化
    • 数字に効くのかはわからないのですが試しても良かった
  • kataribeやdstatなど使わなかった計測ツールの使い方を調べた
    • なにこれ、めっちゃ便利...ってなった

あと競技そのものとはあまり関係ないですがCloudFormationでの環境構築とても簡単で良かったですね。構成図を見たりしてました。

f:id:ohbarye:20210824001119p:plain
CloudFormationのDesignerから出力した図

すでに$17ぐらいかかっていたので気をつけよう…。


自分の引き出しが「API >> DB >>>>>> Nginx > インフラ周り」と偏り過ぎているのでボトルネックがわかっても打ち手が適切でなかったり、そもそも思いつかなかったりと、総合格闘技的な"力"不足を改めて感じるISUCONでした。

所感

2年連続でソロチームで出た所感としては、やはり、ISUCONはスキルポートフォリオの不足を知るとても良い機会だということです。

チームで出るならほとんどの場合は各自の得意領域で勝負をすることになると思うのですが、ソロなのでなんでも自分でやらなければいけない。そのぶんトラブルシューティング・課題発見・司令塔・問題解決力・実装力などすべてを合わせた総合格闘技力がモロに試されることになります。普段チームで開発しているとなかなか得難い体験だなと思います。

複数人で参加して効率重視で優勝を狙うところとは離れており*1、運営の意図とは異なるのかもしれませんがこのような楽しみ方ができるのもISUCONの好きなところです。

運営の皆様、本当にお疲れさまでした && ありがとうございました。


[おまけ] やったこと

ここからは競技時間内にやったことの羅列です。

環境構築: 1529

一人なのでまずは最初の1時間で落ち着いて開始できるように持っていくところ。

  • CloudFormationでの環境構築
  • サーバへのssh確認
    • 直前にマシンを交換してたので鍵がローカルになくてsshできずとても焦った
  • 環境チェッカーの実行
  • ローカルマシンのブラウザでアプリケーションが動いていることを確認
    • 去年はここを怠ったのでアプリケーションの挙動をコードから特性や仕様を理解しようとして時間を浪費した
  • git initしてGitHubにpush
  • 初期設定のままベンチマーカーを実行する

Ruby実装に切り替える: 4732

なぜか点が伸びた

[11:00] NewRelic導入: 1532

スッ...と下がった

NewRelicでmost time consumingが GET /api/trendとわかりました。

f:id:ohbarye:20210823001832p:plain
初期状態
ここまで一切コードリーディングしてなかったので、対象エンドポイントと周辺を読み始める。

N+1で明らかにマズいのだけどけっこう直すの重そうだな…と日和りましたが、ボトルネックから片付けないといけないことは学んできたので覚悟決めて倒すことに。

[12:30] GET /api/trendのN+1を倒す: 9035

かなり苦戦しつつもGET /api/trendのN+1を消し、クエリ1回で必要な全データを取れるようにしました。

途中クエリをミスってFailするようになってしまったバグの修正でけっこう時間がかかったものの、一気に点数が伸びて昼飯を食べる気が湧きました。

ベンチマーカー走行時に凡ミスでFailするとかなり心臓に悪いので、これ以降の修正は基本的にRubyのREPLでコードをある程度実行してからコミットすることにしました。

また、ボトルネックPOST /api/condition/:jia_isu_uuidになったのでこの辺を読んでいく。

f:id:ohbarye:20210823001858p:plain
ボトルネックが変わった

[13:00] [Revert] drop_probabilityの調整: 8685

「いっぱいデータが貯まったほうがスコアが伸びるんだな?」程度の仕様理解をしていたのでこの値をいじって点数の増減を確かめてみました。

  • drop_probability 0.5: 8685
  • drop_probability 0.7: 8815

結果、下げるほど点数が下がることがわかったので0.9に戻す。

[13:30] POST /api/condition/:jia_isu_uuidのbulk insert化: 9149

コードを読んで遅そうなところはinsertがN回走るところだったのでbulk insert化したところ微増。

NewRelicでは依然としてこのエンドポイントが遅いままだったので「うーん、非同期化しないとダメか...?いや、構成変更の素振りが足りないから手を出すのは危険な気が…」と逡巡。

[14:30] indexをisu_condition (jia_isu_uuid, timestamp)に追加: 9909

NewRelicで見えるAPIレイヤのメトリクスでは手を出せる改善箇所が定まらなくなったため、MySQL slow query logを出力してクエリ傾向を見てみる。

isu_conditionテーブルのRows_examinedの数値が異常に高いクエリがいっぱいあった。初期のベンチマーカー走行時は高々1,000程度しかレコードがなかったもののこの時点で21,118件のレコードが入っていたのでインデックスを貼ることにした。

このあとしばらく迷走します。

[15:00] GET /api/trend のsortで減点されていたのを修正: 6858

deductionが20ほど出てて不穏だったので直した。N+1直したときのクエリにORDER BYDESC付け忘れがあった。

なぜこれで点が下がるの...?と思ったところ、ユーザーが増えたせいみたいだ。

revertするか悩んだが後々活きてくることに期待してそのままいくことにした。

[15:10] 毎回static fileをreadするのをやめる: 6574

ボトルネックじゃないところに手を出し始めているので良くない兆候。しかも点が下がっている

Nginxのtry_files思いつきたかったな〜

[15:30] drop_conditionを思い切って0.1に下げる: 9358

打つ手を見失って実験しただけなのだけど、今度はなぜか伸びている...! のでこのままいく。

[16:30] GET /api/isu のN+1を倒す: 9266

GET /api/trend と同様の倒し方。こちらも結構手こずった

[17:00] GET /api/isu/:id/icon をキャッシュさせる: 9320

画像は変更されないようなのでNgnxで GET /api/isu/:id/iconexpires つけてキャッシュさせるようにした。

[18:10] 計測ツールをオフにする: 13020

しばらく見てないのでNginxのログ出力、MySQL slow query log、NewRelicをオフにしたところようやく10,000点超えた

[18:20] Pumaをcluster modeで動かす: 14328

ずっと1台でRubyプロセスのCPUが100%に張り付いているのは気になっていたが手が回っていなかった...。puma -w 2でcluster modeでPumaを起動したところやや伸び。CPUは2つのRubyプロセスが60%程度になりました。

以下を試したがあまり伸びませんでした。

  • preload: 13542
  • worker数の調整: 12982

[18:40] 祈りのラストベンチマーカー: 14484

特に何をするわけでもなく祈りながらベンチマーカーを実行したところこれまでで最高スコアになりました。

*1:もちろん勝つ気持ちでやってますが

デスクチェアの座面クッションを掃除した

"腰を破壊する自宅デスク環境"を改善して健康被害から卒業した - valid,invalid に書いた通り、先日中古でオカムラのコーラルチェアを購入した。

椅子自体の性能は初日からめちゃくちゃ気に入ったのだが、中古ということもあり 座面がお世辞にもキレイとは言えなかった ため、自分で掃除してみた。

結果

写真の明るさや彩度がブレてしまったが Before / After はこんな感じ。

← Before | After →

多少ヨゴレを強調するように撮ったとはいえ、Beforeを改めて見返すとヤバい。昔に働いていた会社の倉庫に眠ってたボロ椅子を思い出す。二度と座りたくない!!

掃除のやり方

オカムラ公式サイトのやり方やネットで見かけた記事を参考に、以下の道具を用意した。

  • 粘着カーペットクリーナー(通称コロコロ) or 掃除機
  • アルカリ系の汚れ落とし
  • タワシ or 歯ブラシ
  • 吸水タオル
  • (任意)ドライヤー

1. 表面のほこりやチリをコロコロや掃除機で除去する

掃除の邪魔になりそうなほこりやチリには最初に退場いただく

2. アルカリ電解水をかけまくる

ここからが本番

有機物の汚れ(汗、皮脂、食べ物、飲みもののしみなど)はアルカリ電解水で浮かせて拭き取るのが有効らしい

最初は遠慮気味に吹きかけていたが、けっこうビシャビシャになるまでかけないと汚れが浮き出なさそうだったので容赦なく噴射

皮膚や喉が弱い人は手袋やマスクをしながら作業したほうが良さそう

3. タワシや歯ブラシでこする

うちにはタワシがなかったので使い古した歯ブラシで実行

アルカリ電解水を噴射しただけでは汚れが浮いているのかどうかまったくわからないが、歯ブラシでゴシゴシこすり始めるとブラシがみるみる汚れていく…

うわ〜〜〜(自主規制)

4. 吸水タオルで拭き取る

ブラシに付着した汚れを拭き取ったり、座面を直接タオルで吹いたりしていく

何度もこすっていると徐々にタオルが汚れていく

ある程度こすって吸水タオルがビシャビシャになった頃、水場でタオルを絞ると濁った水が…

うわ〜〜〜(自主規制)


2~4を何回か繰り返した後、ドライヤーで乾かして汚れの落ち具合をチェックして良さそうなら終了。

本当にこれで落ちるのか?と半信半疑だったので1周目は半分だけやってみた。すると違いが明らかになってきたので勢いづいて2周やったところ当初よりかなりマシになった。

←1周目 | 2周目→

アルカリ電解水で濡れていると汚れている箇所がわかりづらくなるので、ドライヤーでいったん乾かした後に残っている箇所をさらに重点的に掃除。

最終的には3周したところでこんな感じ。

写真で見るとまだまだ落とせそうな箇所があるが、通常の照明下、肉眼ではほとんど気にならない程度にまでキレイになった。

感想

オフィスバスターズのような専門業者であればある程度クリーニングしてから発送してくれそうな気がするが、個人間取引ということである程度は覚悟して購入したので文句は言えず。まぁ、座面以外は目立った汚れがなかったので良かった。

前提として、オフィスチェアはどんなに良いものでも使用していると必ず汚れたり状態が悪くなっていくので掃除が必要とのこと。ならば新品を購入してもいずれはこの状況に遭遇するはず…それが早いか遅いか、それだけ…

なのではあるが、掃除過程で予想以上の汚れを観測してしまいメンタルにダメージが…。

椅子自体はとても気に入っているので 定期的に汚物は消毒 していきたい。

SQLでパーセンタイル値を求める

SQLでパーセンタイル値を求めたいことがあり、Calculate Percentile Value using MySQL - Stack Overflowを参考に実現できた。

実例

口座 (accounts) テーブルで残高 (balance) column の95パーセンタイル値を求める。

SELECT balance
FROM (SELECT @row_num := @row_num + 1 AS row_num, balance
      FROM accounts t,
           (SELECT @row_num := 0) counter
      ORDER BY balance) temp
WHERE temp.row_num = ROUND(.95 * @row_num)
;

95パーセンタイル以下の値を持つレコードに絞って平均を求める。

SELECT AVG(balance)
FROM (
      SELECT @row_num := @row_num + 1 AS row_num, balance
      FROM accounts t,
           (SELECT @row_num := 0) counter
      ORDER BY balance
     ) temp
WHERE temp.row_num <= ROUND(.95 * @row_num)
;

解説

一番内側のクエリで、指定のカラムを昇順で並べつつ行番号を割り振る。

SELECT @row_num := @row_num + 1 AS row_num, balance
FROM accounts t,
     (SELECT @row_num := 0) counter
ORDER BY balance

以下のような結果が出力される。

row_num balance
1 1232
2 122090
3 134243
4 230909
5 390901

↑のクエリで得られるレコードが100件あるとしたら、95番目の値が95パーセンタイル値になる。

この中から95%目(というのも変な表現だが)の行だけを選択するにはWHERE temp.row_num = ROUND(.95 * @row_num)で絞り込めば良い。

95パーセンタイル値以下のレコードに絞るのであればWHERE temp.row_num <= ROUND(.95 * @row_num)


PERCENTILE_DISCみたいな良い感じのウィンドウ関数があればよいのだが、使えない環境もある。

https://docs.aws.amazon.com/ja_jp/redshift/latest/dg/r_WF_PERCENTILE_DISC.html

環境

zshの起動がいつの間にか速くなっていた

zshの起動が遅かったのでzprofでプロファイリングしながら起動速度を3倍速くした - valid,invalid からしばらく経ち、久々にzshの起動時間を計測してみた。

前回

起動1秒でたまにイラッとする程度。

for i in $(seq 1 10); do time zsh -i -c exit; done
zsh -i -c exit  0.79s user 0.40s system 108% cpu 1.100 total
zsh -i -c exit  0.78s user 0.39s system 108% cpu 1.070 total
zsh -i -c exit  0.77s user 0.39s system 108% cpu 1.069 total
zsh -i -c exit  0.79s user 0.39s system 109% cpu 1.083 total
zsh -i -c exit  0.78s user 0.38s system 108% cpu 1.065 total
zsh -i -c exit  0.80s user 0.40s system 108% cpu 1.099 total
zsh -i -c exit  0.79s user 0.40s system 109% cpu 1.090 total
zsh -i -c exit  0.80s user 0.40s system 107% cpu 1.110 total
zsh -i -c exit  0.79s user 0.40s system 108% cpu 1.100 total
zsh -i -c exit  0.80s user 0.40s system 108% cpu 1.100 total

今回

前回同様まずはおおまかな起動時間を測ってみた。

$ for i in $(seq 1 10); do time zsh -i -c exit; done
zsh -i -c exit  0.49s user 0.25s system 109% cpu 0.676 total
zsh -i -c exit  0.48s user 0.23s system 109% cpu 0.650 total
zsh -i -c exit  0.49s user 0.24s system 109% cpu 0.661 total
zsh -i -c exit  0.48s user 0.24s system 110% cpu 0.658 total
zsh -i -c exit  0.48s user 0.23s system 110% cpu 0.648 total
zsh -i -c exit  0.49s user 0.24s system 111% cpu 0.651 total
zsh -i -c exit  0.48s user 0.24s system 110% cpu 0.649 total
zsh -i -c exit  0.48s user 0.24s system 110% cpu 0.652 total
zsh -i -c exit  0.48s user 0.23s system 110% cpu 0.643 total
zsh -i -c exit  0.49s user 0.24s system 110% cpu 0.654 total

そこまで遅くなかった。というか、計測してなかったけど前回よりいつの間にか早くなっていた。

また、前回ボトルネックの一端であったkubectlの補完(とkubernetes関連のutil)を今はオフれる*1ことに気づいたので、オフにしてみたら、さらに速くなった。

$ for i in $(seq 1 10); do time zsh -i -c exit; done
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.420 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.420 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.421 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.423 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.419 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.419 total
zsh -i -c exit  0.21s user 0.20s system 95% cpu 0.421 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.417 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.419 total
zsh -i -c exit  0.20s user 0.20s system 95% cpu 0.422 total

いつの間にか早くなっていた理由

この2年ぐらいでそんなに.zshrcいじったっけ?と思ってdiffしてみたらけっこう変更してた。

計測してないが、おそらく高速化に寄与したであろう変更は

  • preztoやめてoh-my-zshにした
  • plugin managerをantigenに寄せて、ついでに使ってないpluginを消した
  • antigenをv2にした
  • zshをupdateした

あたり。

定期的にpluginを消したりソフトウェアをupdateするのは大事だなぁと改めて思ったところだが、antigenのメンテナンス止まっているのか... また移行しないと...

環境

*1:kubernetes使っていないため