maybe daily dev notes

私の開発日誌

会社をマイクロサービスアーキテクチャのシステムとして捉えたら

はじめに

会社組織をマイクロサービスアーキテクチャとして捉える見方があります。この記事ではその概要、またそうした場合に考察される「良い」チームのあり方についてまとめます。

マイクロサービスの要点

ここでいうマイクロサービスとは、技術分野におけるアーキテクチャパターンのこと。マイクロサービスアーキテクチャ内の各サービスは以下の特徴を持ちます。なお、ここでのユーザーはそのマイクロサービスのユーザーを指します (エンドユーザーや他のマイクロサービスなど)。

  1. 各サービスはAPIを公開し、ユーザーはそれを通してのみサービスを利用できる
    • サービスの内部実装をユーザーに意識させてはならない
    • 注) ここでのAPIはHTTPでJSONを云々などの話に限らず、サービスを利用するインターフェースの決め事全般を指す
  2. APIはユーザーとの契約なので、基本的には破壊的変更を加えてならない
    • APIは変更しない限りは、マイクロサービス内部の実装は自由に変更できる
  3. サービスは満たすべきサービスのレベルをユーザーと合意し (Service Level Agreement, SLA)、そのレベルを満たすように努力する

他にも色々あるでしょうが、今日の話に関連する部分だけピックアップしました。 上記の特徴により、各マイクロサービスは独自に意思決定をし、迅速な改善を実現することができます。

マイクロサービスの例

マイクロサービスの代表的な例の一つとして、AWSの各サービス群が挙げられるでしょう。Amazon S3やSQSなど、それぞれのサービスは独自にAPIを定義し、そのAPIに破壊的変更を加えない範囲で継続的に改善することに成功しています。

Amazon CEO(当時)のジェフ・ベゾスが社内の開発者達に通達したとされるBezos Mandateはあまりにも有名です。2002年の話ながら、現代のマイクロサービスに必要な特徴と多くの点で共通しているので、ぜひ見てみると良いでしょう。

会社とマイクロサービスアーキテクチャの類似点

会社の各チーム(部署)は、それぞれがマイクロサービスであるとみなすことができます。 例えば、経理部のあるチームは経費精算申請のリクエストを送ると払い戻しが返ってくるサービスのようなものですし、特定分野のエンジニアが集うチーム(コミュニティ)は関連技術の質問を投げると答えが返ってくるサービスとしてみなすこともできるでしょう。

このように考えると、上記で上げたマイクロサービスの特徴を、会社内の各チームにも適用することができます。以下では、それぞれの類似点とその考え方をチームの運用にどう活かすかを考えてみました。

定められたAPIを通したサービスの利用

マイクロサービスは、ユーザーと取り決めたAPIを経由した利用しか許可されません。ここでいうAPIとは、そのサービスを利用する上での決められたインターフェースです。会社のチームの場合でも、例えば決められたツールでのみ経費申請を受け付けるとか、質問を受ける際は一定のフォーマットを強いるなどといった、ある種の「入力」に関してユーザーにルールを課すことがあります。

これにより、各入力を標準化された方法で処理することができます。入力のバリデーションや担当者の割り振りなどを自動化することも見据えることができ、非常に望ましい状態と言えるでしょう。

合意のないインターフェースを介した利用、例えばチーム内のメンバーに直接DMするといった方法はこの規則に反します。これは以下の点で望ましくありません:

  • ユーザーがチーム内の構成員というある種の内部実装を意識している
    • チーム内の内部実装の変化、例えば構成員の移動などが発生した場合に、ユーザーはこれまでの方法が使えなくなります
  • 上記の自動化プロセスを無視するため、サービス側の効率が低下

もちろん、これはシステム間の連携というよりは人間同士の繋がりの話なので、例外はあるでしょう。しかしながら、基本的には定められたAPIを通してやり取りをすることが望ましいのは共通です。

APIに破壊的変更を加えない

上記のAPIに破壊的変更が発生した場合、そのサービスに依存するすべてのユーザーは変更に適応する必要性が生じます。例えば、経費申請で入力すべき情報が増える、そもそも経費精算で使うツールが変わるなどといった変更です。

変更への適応には、確実にコストが伴います。こちらも状況にはよりますが、必要のない限りはAPIに破壊的変更を加えないというのも、同様に当てはまるでしょう。

また、APIに変更を加えないという前提であれば、それ以外の変更は自由に可能です。構成員の変更も内部の業務プロセスの改善も、APIが変わらないのであれば個別ユーザーへの相談は不要です。このためチーム内で意思決定を迅速にできるということが大きな利点となるでしょう。

サービスのSLA

会社の各部署でも、サービスのレベル(品質)についてユーザーと合意を得るべきです。技術文脈でSLAと言えば可用性のパーセンテージがよく想像されますが、実際はサービスの品質に関するあらゆる項目が含めることができます。例えば、経費申請してからN日以内に振り込むだとか、質問には平日の9〜17時JSTのみ回答しますというのも一つのサービスレベルでしょう。とにかく、ユーザーに提供するサービスの品質について、一定の計測可能なレベルを決め、ユーザーと合意を得ます。

チームはこのSLAを達成するための努力をすべきです。誰が担当するかやメンバーの急な欠勤などには左右されない、常にSLAを満たせる仕組みを作ることに尽力しましょう。

一方で、SLAを超える部分については、ある種の安全余裕です。SREの分野にもError budgetという考え方があり、その余裕を使ってリスクのある実験をするということもできます。チームの運用においても、案件の対応を適度に遅らせながら、合間に他の重要なタスクをこなすといったことが可能でしょう。

SLAを定めることで、サービス運用側は運用状況の良し悪しについて指標を得ることができますし、またサービス利用側はサービスに対して必要十分な期待で利用でき、計画も立てやすくなりますね。

SLAを超える仕事

まれに、SLAを超えた仕事をするメンバーがいる場合もあります。例えば、9〜17時JSTの対応で合意を得ている状況で、その人だけ深夜にも対応を行うなどです。この行為自体はユーザーからも歓迎されるものですし、問題ないでしょう。しかしながら、この活動によって、ユーザーが本来のSLAを勘違いしてしまうリスクがあります。「XXさんは深夜対応してくれたのにあなたはしてくれないのか?」などといったクレームが想像できますね。

このような状況を回避するためには、以下が重要と思われます:

  • SLAを超える対応をする人は、それが本来のSLAを超えた対応であること・他の対応者には同じ対応を期待してはいけないことを、ユーザーに対して強調する
  • サービスの運用側は、現在のSLAをわかりやすい形で常に告知する

また、SLAを超えた仕事をするメンバーは、それが本当にユーザーにとって価値のあることだと信じているのであれば、それをチーム全体のサービスレベルとして引き上げることを目指しても良いかもしれません。チームメンバーに働きかけ、SLA自体を向上させるわけです。

いずれにしても、こういった考慮をせずにSLAを超えた対応をするのは、実は周囲に思わぬ影響をもたらす可能性があるということは認識すると良いでしょう。

この例えの限界

実際の組織の意思決定プロセスは、上位のチームの承認が必要な場合があります。ある種、そのサブ組織のツリー内で分散モノリスができてしまったような状態と言えるでしょうか。この場合は意思決定を迅速にするというメリットは失われるかもしれません。

また、この話はチームの定型業務をサービスとしてみなした場合のみ成立すると考えられます。非定型業務で厳密なAPISLAを定めるのは非効率なためです。あえて例えに含めるなら、非定型業務はチームが新たに価値を発揮できる場所を探すPoCのようなものかもしれません。

まとめ

ということで、会社をマイクロサービスとしてみる話でした。この視点から見ると、技術的に良いマイクロサービスとはという話をチームの運用にも流用できることが分かります。あなたのチームはどのような役割を担うサービスでしょうか?

個々のチームの改善は自ずと会社全体の改善に繋がります。今回紹介したような考え方も使いながらチームを運用して、改善を加速しましょう。

AWSでn秒ごとのループ処理、どうする?

はじめに

サービスを開発していると、n秒ごとに何らかの処理を定期実行したい要件が見つかることがあります。 例えば、10秒ごとにあるAPIエンドポイントにアクセスして結果を保存したいなどです。この記事ではこのような機能の実装方法について考えます。

EventBridge + Lambda の限界

n > 60、つまり1分間以上の間隔の場合は、EventBridge + Lambdaによる処理が最適解となる場合が多いでしょう。

しかしながら、EventBridgeのスケジュール式の最低分解能は1分なので、それよりも短い間隔で呼び出すには適しません。

docs.aws.amazon.com

1分未満の間隔でループを回す場合は、次の方法を取ることが多いです。

方法1. ECSのサービスとして実装

ループ処理を適当な言語で実装し(以下はPythonの例)、ECSのサービスとして起動します。

from time import sleep

def main():
  # 10秒ごとにdoSomething関数を実行する
  while True:
    doSomething()
    sleep(10)

main()

この方法のメリット・デメリットは以下のようになるでしょう:

メリット

  • コンテナを1つ起動するだけなので、実装は直観的でシンプル
  • サーバーレスの基盤であるFargateを使えば運用もある程度楽
  • 処理が常時起動・自動復旧することをECSが保証してくれる

デメリット

  • ECSなのでVPCが必要になり、サーバーレス性はやや低い
  • sleep中もコンピュートリソースを消費するので、コストが高いかも?

類似の方法としてLambdaを使って同じことをする方法もあります。Lambdaは最大15分で強制終了されるため、定期的にLambdaを呼び出しつつ、呼び出したLambdaの中でsleepしながら処理をループする方法です。ただし、今回主眼としたいコスト面についてはECS Fargateを用いるこちらの方法と同等になると考えられるため、この記事では省略します。

方法2. Step Functionsのステートマシンとして実装

別の方法は、Step Functionsを使う方法。基本的にはWaitしながらLambdaを呼び出すことをループします。ただし、Step Functionsは1回のExecutionあたり履歴が25,000件を超えるとエラーになるので、適当な回数で再起動する必要があります。それらを考慮すると、以下のような定義になるしょう。

Lambda関数の中でループ回数をカウントアップして、カウントが一定数を超えたらステートマシンを再起動します。

def handler(event, context):
  count = event.get['count'] or 0
  doSomething()
  return { 'count' : count + 1 }

この方法のメリット・デメリットは以下のようになるでしょう:

メリット

  • VPCいらずで、よりサーバーレス

デメリット

  • マネージドにステートマシンの常時起動を保証できないので、別途監視する必要あり
  • Step Functionsのステート遷移がコスト高いかも?

ということで、2つの方法を紹介しました。

上記の方法1と2では、色々と違いはありますが、多くはケースバイケースの判断となるでしょう。そのような中でも絶対的な指標となる(低ければ低いほど良い)、コストについてまず注目してみます。

コストの比較

方法1と2のコスト主要因は、以下となります:

  • 方法1: ECS Fargateの利用時間
  • 方法2: Lambdaの呼び出し・利用時間、Step Functionsの状態遷移回数

具体的に見ていきましょう。ここで、諸々のパラメータを定義します :

  •  T : ループの処理間隔 [s]
  •  T_c : 1ループ当たりのLambda処理時間 [s]
  •  v_{cpu} : FargateのCPU数 [vCPU]
  •  v_{ram} : FargateのRAMサイズ [GB]
  •  l_{ram} : LambdaのRAMサイズ [GB]
  •  C_{fc} : Fargateの1vCPUあたりの料金 [USD/h]
  •  C_{fm} : Fargateの1GB RAMあたりの料金 [USD/h]
  •  C_{li} : Lambdaの呼び出しあたりの料金 [USD/invocation]
  •  C_{lt} : Lambdaの利用時間あたりの料金 [USD/GB/s]
  •  C_{s} : Step Functionsの状態遷移あたりの料金 [USD/遷移]

方法1のコストは非常に簡単です。一時間あたりの料金は以下になります。

 C_1 = v_{cpu}C_{fc} + v_{ram} C_{fm} [USD/h]

CPUとRAMの料金を足すだけです。単純ですね。

方法2の1時間あたりのコストは、以下です。

 C_2 = \frac{3600}{T}(C_{li}+ l_{ram}T_c C_{lt}   + 3C_{s}) [USD/h]

  \frac{3600}{T} は1時間あたりのループ回数です。1ループあたりはLambdaの料金に加えて、Step Functionsの遷移回数3回分 (上図を参照) の料金が課金されます。 また、厳密にはステートマシンの再起動による遷移料金も含まれますが、軽微なため無視しています。

では、各変数に2022/6現在の値を代入して、計算してみました。 ここからパラメータをいじることができます。

それぞれのコストは諸々のパラメータによるのですが、いくつかの例を見てみましょう。特定の例における比較があたかも一般論として独り歩きしてしまうのは避けたいです。以下はあくまでも特定の例における比較であることに注意してご覧ください。

一例

以下のようにパラメータを設定した場合です:

  •  T_c= 100 ms : 1ループ当たりのLambda処理時間 [s]
  •  v_{cpu}= 0.25 vCPU : FargateのCPU数 [vCPU]
  •  v_{ram}= 256 MB : FargateのRAMサイズ [GB]
  •  l_{ram}= 256 MB : LambdaのRAMサイズ [GB]

このときのグラフはこうなります。

青線が ECS Fargateのコスト ( C_1)、赤線がStep Functions + Lambdaのコスト ( C_2)です。ループの処理間隔  T を横軸の変数として、コストをプロットしています。

これより、処理間隔がおよそ20秒を超える場合は、Step Functionsのほうが安上がりなことが分かります。ちなみに緑のラインは、処理時間が1分を超える場合のみ利用できる、EventBridge + Lambdaの場合です。不必要なコンピュートリソースを使わない分、圧倒的に安いことが分かりますね。

繰り返しになりますが、これはあくまでも一例です。このページから、パラメータを色々と変更して動きを確認してみてください。また、計算式に間違いがある可能性もあるため、ご自身で検算した上で参考にしていただければ幸いです。

もう少し考慮点を

2つの方法では、コスト面で違いがあることが分かりました。特に処理間隔が10秒以下になる場合は、Step Functionsだとやや高くなる印象です。とはいえ、これ単一のコストは1時間で1円ちょっとなので、1ヶ月でも1000円程度でしょう。状況によっては、コストはこのアーキテクチャを決める支配的な要因にならないかもしれません。

その他のメリットデメリットとしては、上記に挙げたとおりです。特にVPCの有無や、Step Functionsで処理が実行されていることを監視する必要性などは、重要な考慮点となるでしょう。 許容される複雑度を考えながら、そのシステムに最適な技術選定をしてください。

まとめ

AWSで1分未満の間隔の定期的な処理を実行する方法について考えました。やはりいろいろな考慮点があるので、最適な技術選定をするためには、それぞれのメリットデメリットを把握する必要があります。

もし他にもこのような方法があるよ、このような考慮点があるよなどありましたら、教えていただければ幸いです。

Slack 6種のチャンネル仕分け!

Slackのチャンネルは6種類に大別できる。これを理解して設定を使い分けることで、Slackの通知地獄から開放される第一歩となろう。

なお有料版限定の機能を使う場合の話。

これが6種類だ!

Slackのチャンネルは、見え方について以下3つの直交する設定が可能:

  1. チャンネルを含むセクションを折りたたむか開くか

    右向き三角は折りたたみ、下向き三角は展開されたセクション
    セクションを折りたたむことで、画面を占有する高さを大きく節約することができる。一方で折りたたんだセクションの中のチャンネルにアクセスするためには、2クリック必要になる。このため、基本的には折りたたんだセクションには訪れる頻度の低いチャンネルを入れる。

  2. セクション内のチャンネルの表示設定: すべて表示 or 未読のみ表示

    表示設定
    未読のみ表示もまた、画面を占有する高さを節約するための機能。折りたたみとの違いは、隠されたチャンネルにアクセスするには Cmd + T でチャンネルに直ジャンプするしかないこと。すなわち、折りたたみよりもアクセスのコストが高い。一方で、表示されたチャンネルには1クリックでアクセス可能という優位性もある。

  3. チャンネルをミュートするか否か

    薄字がミュートされたチャンネル
    チャンネルをミュートすることで、新着メッセージへの通知が無効になる。基本的には、新着メッセージのたびに通知があると煩わしいチャンネルに対してこの機能を利用する。ただしこれは2の設定と組み合わせると面白くて、セクションを未読のみ表示にするとミュートしたチャンネルも表示される。このため、ミュートしていてもなお、通知はないが新着メッセージに気づくことができるのだ。
    ミュートするとこの通知が来ない

これらの設定に応じて23=8種類だが、このうち、1を折りたたむにして2を未読のみ表示にする設定は意味をなさない (併用しても特段のメリットがない) ので、有効な選択は6種類あることになる。

ミュートしない ミュート
セクション展開 & すべて表示 1 2
セクション展開 & 未読のみ表示 3 4
セクション折りたたみ & すべて表示 5 6
セクション折りたたみ & 未読のみ表示 - -

使い分け

では、この6種類をどう使い分けるか説明しよう!

  1. セクション展開 & すべて表示 & ミュートしない 自分にとって最も重要なチャンネル。常に1クリックでアクセスできるので能動的にメッセージを送信しやすいし、新着に対して通知もされる。

  2. セクション展開 & すべて表示 & ミュート 重要だがメッセージの受信頻度が高く煩わしいチャンネル。チャンネルを開けば大体新着メッセージがあるので、通知の必要を感じないときに。

  3. セクション展開 & 未読のみ表示 & ミュートしない 自分から能動的にメッセージを送ることは稀だが、メッセージはタイムリーに追いたいチャンネル。

  4. セクション展開 & 未読のみ表示 & ミュート 自分から能動的にメッセージを送ることは稀だし、メッセージもあとからまとめて読めば十分なチャンネル。

  5. セクション折りたたみ & すべて表示 & ミュートしない 3に近いが、これは新着があるときもチャンネルの表示に2クリック必要。メッセージが送られてくる事自体稀で、ほとんど放棄されている。ただし、稀に利用されるので、そのときは確実に気づきたいようなチャンネル。

  6. セクション折りたたみ & すべて表示 & ミュート これをするのはチャンネルから退室するのとほぼ同義だが、退室のログが残るのも忍びないようなチャンネル。

まとめ

以上、6種のチャンネルの声を聞き分ければ、君もSlackマスターだ!

趣味Webサービスをサーバーレスで作る ― 格安編

はじめに

最近AWSのサーバーレスサービスで作るWebサービスの雛形を公開してみた*1。小規模サービスならかなり安く (ほぼ無料になるケースも多そう) で運用できるので、割と良い選択になる場面も多いと考えている。今日はその布教記事。

github.com

全体構成

全体の構成は下図のとおり。特徴としては以下が挙げられる:

  • CDNでフロントエンド (React SPA) の静的ファイルを配信
  • バックエンドAPIAPI Gateway + Lambda (Express.js)
  • データベースはDynamoDB (詳細は後述)
  • ユーザー登録が必要なサービスのため、Eメール認証も付けている
  • ありがちな非同期ジョブ、cronジョブ実行の仕組みも用意
  • AWS CDKで一発デプロイ

architecture

こういう構成を趣味サービスで採ることのおそらく一番のデメリットは、学習コストだろう (VMを使った構成とはかなり違うので。) このサンプルを参考に実装したり、一旦そのまま使ったりすることで、その初期学習コストを回避させることができれば良いなと思う。

他にもVMやコンテナ系サービスと比較したサーバーレス特有の考慮点がいくつかあるので、次の節にまとめる。

サーバーレスの考慮事項

AWSのサーバーレスサービスを使う時に考慮すべきことをまとめる。

メリット

サーバーレスのメリットは枚挙にいとまがないが、趣味サービスという観点で実用的なのは以下のようなところ:

  • 初期コストが安い。基本的にリクエスト量に応じた従量課金なので、ユーザーが少ないうちはほぼ無料で運用可能。
  • サーバーの運用が不要。VMの面倒を見るような作業から開放される。
  • 流行の技術なので、業務での活用も期待できる。

一方でもちろんAWSマネージドになる分考えないといけないポイントもあり、以下にそれらをまとめた。他にもあれば教えてください。

タイムアウトの短さ

Amazon API Gatewayの仕様上、Webリクエストは30秒でタイムアウトする。つまり、リクエストを送ってからレスポンスに30秒以上かかるようなAPIは作ることができない。実際レスポンスに1分もかかるようなAPIがあると、ユーザー側も不安になる・UX下がることもあろうから、妥当な制約ではある。

この制約を回避するための常套手段は、非同期ジョブ化である。APIではジョブをエンキューだけしてレスポンスを返し、非同期でジョブを実行する。今回のサンプルではAmazon SQSにエンキューしAWS Lambdaでデキュー・ジョブ実行する形で、これを実現している。

DynamoDBの特性

小規模サービスで最もコストがかかるのは大抵データベース。例えばAmazon RDS Auroraを使うとしたら、最安のインスタンス (db.t3.small) 1台でも毎月5000円くらいかかる。個人サービスにはなかなか厳しい値だろう。

一方で今回使うDynamoDBはリクエスト量に応じた課金なので、小規模ならかなり格安に使うことができる。具体的には、100万リクエストごとに1.5 USDとかそれくらいのコスト感。もし毎月1万アクセスのサービスなら、1円程度で済む計算。

このようにコスト効率の良いサービスではあるのだが、いかんせんNoSQLなのでRDMBSとは使い勝手が違う。具体的にはJOINができないため、情報を1つのリクエストで一括取得したい場合はテーブル設計にそれなりの工夫が必要になる。正直設計だけでも結構難しいし、DynamoDB Streamsなどを使ったそれなりの仕組みを実装する必要もあったりする。

serverlessfirst.com

とはいえ小規模サービスなら、強いて1リクエストで必要な情報をすべて取得することにこだわらなくても良いのでは。上記のように、1万アクセスで1円程のコスト感である。仮に1アクセスごとのリクエスト数が2倍になったとしても2円。1円のためにDynamoDBまわりの設計を複雑化するのは非合理じゃないだろうか。ここの最適化にこだわらなければ、意外とDynamoDBも素直に使える*2

将来的にもしサービスの規模が大きくなったら、その時にリファクタすれば良い。DynamoDBはテーブルをリファクタする仕組みが十分に整っている。もちろん非常に簡単な作業というではないが、それほどの規模になっているのであればモチベーションを湧かせるのは難しくないはず。

コストの上限

サーバーレス系のサービスでよく言われる気がするのは、なまじスケールアウト性能が高いため、もしDDoS攻撃を受けた時にコストが青天井になるという不安。この点について考えてみる。

architecture

まずフロントエンドの静的アセットを配信しているCDN (Amazon CloudFront) だが、ここをDDoSするのは攻撃者視点であまりメリットがないと思われる。CDNに対しての攻撃は多くの場合直接サービス品質に影響を与えられないし、課金攻撃だとしてもデータの転送量に対する支払いとなるため、攻撃にかかるコストとサービス運用者に支払わせるコストがトントンになって効率が悪いだろう。

このため、より気にすべきはバックエンドAPIの方だろう。例えば非同期Jobを走らせるエンドポイントだと1回のリクエストでジョブをエンキューできるので、重いジョブの場合は攻撃の効率が良さそうだ。

しかしこれも運用側で対策ができて、LambdaのReserved Concurrencyを設定すれば大体OK。これはLambdaの同時実行数に上限を設定する機能で、要は無限にスケールアウトするような事態を防止できる。このため、攻撃を受けても一定のコスト増加やサービス品質の低下はあるものの、無限にコストが増えることはない。あとはAWSの請求額を監視するSlack通知でも設定しておけば、早めに異常に気づいてそこから対策を考えることができる。

dev.classmethod.jp

これらのことはDDoS攻撃だけでなく急にサービスが流行りだしたときもおなじことが言えるかもしれないが、その場合は実ユーザーをお金に変換する仕組み (広告など) を事前に仕込んでおけば、運用コスト以上にリターンがあるはず。

とはいえ多くの個人サービスは攻撃を受けることも急に流行りだすこともないだろうから、アーキテクチャをシンプルに保つ意味でAWS WAFなどは入れていない。ここは考え方次第なので、最初からそういう対策を入れておくのも全然アリだと思う。趣味なのでこだわりたいところをこだわろう。

ローカルでの開発しやすさ

Lambdaを使うとローカルで開発しづらいんじゃないという声もよく聞く気がする。実はそうでもなくて、というのは今回 serverless-express というExpress.jsのサーバーをほぼそのままLambda上で動かせる仕組みを使っているため。 これにより、ローカルではただのExpressサーバーとして検証し、そのコードをそのままLambdaにデプロイするということが可能*3

SQSなどのサービスが絡む部分は、実際ローカルで検証しづらい。ここは自分だとAWS上のリソースを検証用途でも使うようにしている。最近は cdk watchcdk hotswap といったCDKの機能で、AWS上のリソースが絡む検証も非常に高速にイテレーションを回せるようになった。このおかげで待ち時間も短くストレスもない。コストに関しても今回のサーバーレス系サービスなら無視できるレベル。

dev.classmethod.jp

ちなみにフロントエンドは、SPAの仕組み上CDNで配信してもローカルで動かしても同じ動作になるので、何も考えずにローカルで動作検証できる。そんな感じで、システム全体をローカルから開発している。

塩漬けの可能性

サーバーレスに代表されるAWSマネージドのサービスは、基本的に塩漬け運用はしづらいと言われる。例えば最近だとPython 3.6のEOLに伴ってLambdaの同ランタイムはサポートが終わったので、開発者たちは対応に奔走したものだ (一応、Lambdaがサポート終了した後も関数実行自体は可能なので動作はする。)

ただし、実は今回利用したサービスの場合はそれほどユーザー影響のある変更があるとは考えづらい。AWSサービスは内部仕様は変わることはあれど、ユーザーの目にするAPI後方互換性を保たずに変更されることは少ないためだ*4。 基本的にAPIはユーザーとの契約なので、みだりに破壊的な変更は加えられないものなのである。このように、完全にサーバーレスのサービスを利用していれば案外塩漬け性能は高いかもしれない。

念のため、一般論として塩漬けは推奨されない (脆弱性に対応できない、サービスを改善しづらくなるなどが理由) ので、モチベーションが消え去ったけどサービス自体は提供し続けたいような場合の最終手段と捉えておくと良さそう。

システム全体のコストは?

AWSは無料利用枠があり、今回使うサービスの多くは毎月一定までは無料で使える (例えばAPI Gatewayは毎月100万リクエストまで無料など)。つまり、小規模な趣味サービスならほぼ永久無料で運用できる。

aws.amazon.com

無料利用枠の使い果たしたあとも、基本的には単純な従量課金なので、リクエスト量がわかれば見積もりは容易。DynamoDBの項でも書いたが、大抵100万リクエストにつき100円とかのオーダーなので、そんなもんだと思っておけば良い。VMとは異なり使ってない計算リソースが維持され続けることもないので、特に利用者数が時刻により変動するようなサービスは効率よく運用できる。

実際運用してみて

まだ限定公開ながら、この技術スタックで稼働しているサービスがある。 今はかなり小規模なので、目論見通りほぼ無料枠に収まっている。

一点想定外だったのは、Amazon Elastic Container Repository (ECR)の利用料金。素のLambdaランタイムではデプロイの際Node.jsのバンドリングに時間がかかるなど不便だったので、この時はDocker Lambdaを使っていた。 CDKだと cdk watch という機能を使って、ローカルのファイルに変更があるたびに自動で新しいDockerイメージをビルドしてデプロイするということができるのだが、これによりECRへの保存量が多めになっていた模様。

結果的には、1ヶ月で10GB程度のストレージを消費していたため100円くらい課金された。やっぱり運用してみるまで分からないこともあるね。ここはユーザーの増加に伴って増える金額ではないので、許容している。

まとめ

趣味サービス、AWSのサーバーレスで作ってみても良いんじゃないという話をした。他の手段と同様に考慮すべき事項はあるが、かなり有効な選択だと思う。サンプルも用意しているのでぜひお試しください。

*1:私のチームではこのように再利用可能なサンプルを公開することが奨励されている

*2:今はサーバーレスのRDBMSサービスも世の中にあるので、DBだけはそれを使うというのもアリだと個人的には思う。AWS外だとIaCしづらいとかそういう実用的なデメリットはあるだろうが。

*3:実際これでは1つのLambdaのコードが肥大化して良くないという話もあるのだが、一旦シンプルさのためこうしている。APIルートごとにLambdaを分けるということは容易なので、コールドスタートが長いなどの問題が表面化してきたらリファクタしよう。

*4:というよりそんな例あったんだろうか?知らない。

MariaDBコントリビューション録その5 - MDEV-18873

前回のあらすじ

MDEV-24582に取り組むうちに、GDBが仲間に加わった。コードを実行しながら変数の中身も見えるすごいやつである。強力な仲間たちとともに、今日もIssueに立ち向かう。

tmokmss.hatenablog.com

とはいえ、とりあえず気持ちを変えてみる

ここ最近取り組んでいるMDEV-24582はなかなかの難敵なので、修正方針についてメンテナに合意を得てから実装に着手することにしよう。修正を追えてPRを出したあとに根本的な指摘をされるのはお互いに避けたいだろう。

該当のチケットに方針に関するたたき台を投稿したので、しばし返信を待つ。そしてこのままではブログネタがなくなるので、今日は別のIssueに取り組むことにする。

今回のIssue

特定のクエリを実行した際に、MariaDBサーバーがクラッシュするというバグ。クラッシュした際のスタックトレースが添付されているので、これを参考に見ていこう。

[MDEV-18873] Server crashes in Compare_identifiers::operator or in my_strcasecmp_utf8 upon ADD PERIOD IF NOT EXISTS with empty name - Jira

まずは例によって、問題が再現するクエリを書いたテストケースを作成し、実行してみる。

# mysql-test/suite/innodb/t/MDEV-18873.test
ALTER TABLE t ADD PERIOD IF NOT EXISTS FOR `` (s,e);

# run test
./mysql-test-run.pl innodb.MDEV-18873 --manual-gdb

ADD PERIOD という命令は初見だと思ったら、MariaDB特有の機能らしい。正直ほぼMySQL == MariaDBだと思っていたのだが、まれによく相違点があって面白い。

原因の特定

Issueのスタックトレースから Compare_identifiers::operator() でエラーが起きていることが分かるので、そこにブレークポイントを配置する。

#3  <signal handler called>
#4  0x000056225dec77ca in Compare_identifiers::operator() (this=0x7fd1a807a41f, a=..., b=...) at /data/src/10.4/sql/vers_string.h:42
#5  0x000056225ded034f in Lex_cstring_with_compare<Compare_identifiers>::streq (this=0x7fd198005e80, b=...) at /data/src/10.4/sql/vers_string.h:91
#6  0x000056225e0ec019 in LEX::add_period (this=0x7fd198004960, name=..., start=..., end=...) at /data/src/10.4/sql/sql_lex.h:4363
...
struct Compare_identifiers
{
  int operator()(const LEX_CSTRING& a, const LEX_CSTRING& b) const
  {
    DBUG_ASSERT(a.str[a.length] == 0);
    DBUG_ASSERT(b.str[b.length] == 0);
    return my_strcasecmp(system_charset_info, a.str, b.str);
  }
};

デバッガで追うと、 DBUG_ASSERT(a.str[a.length] == 0); でエラーが発生することが分かる。DebugビルドとReleaseビルドで挙動が異なるのも、このためだろう。今回のクエリでは、おそらくテーブル t が存在しないために、 a がNULLポインタになる模様。このため、 a.str がSegmatation faultとなり、サーバーがクラッシュする。

Releaseビルドでは、 my_strcasecmp の中でエラーが発生するが、これも a がNULLであることに起因するようなので、同じ原因と言えるだろう。

対処方法 - 1st trial

a.str を参照する前に、 a がNULLかどうか確認するのが最低限の対応となるだろう。以下のような判定を追加した。これまでは str がNULLの場合は必ず実行時エラーになっていたはずのため、この判定を追加しても既存の動作に悪影響はないはずだ。

  bool streq(const Lex_cstring_with_compare& b) const
  {
-    return Lex_cstring::length == b.length && 0 == Compare()(*this, b);
+    return Lex_cstring::length == b.length && str != NULL && 0 == Compare()(*this, b);
  }

やや対症療法的な感じはするが、シンプルで機能する修正ではあり、実際今回のエラーは解消する。一旦PRを作成して、レビュワーに確認してみることにする。

github.com

レビュー

半日程度でレビューが返ってきた。早い。実は今回のレビュワーは全く面識のないメンテナである。この方の視点では、謎の日本人からパッチが送られてきても、修正が妥当なのか不安に思うだろう。レビューのコメントも、修正の妥当性を説明することを求めているようだ。

たしかに言われてみれば、修正してはみたもののよくわからない点も多々あることに気付かされた。グローバルの強い開発者にコメントをもらえるのはOSSコントリビューションの醍醐味の一つと言える。その中で自身の理解の解像度を高めていけるのだから、これはとても良いことだ。

時間を割いてくれたレビュワーに感謝、不完全なPRを出した自分を陳謝しつつ、再度調査をしてみよう。

次回に続く。

C# 開発を始める on Visual Studio Code + MacOS

はじめに

自分は今までC#Windows上のVisual Studioか、Unityでしか開発したことがない。この2つはいずれも開発環境が完全自動で整備されるので、非常に楽に開発を始めることができたものだ。しかし、今回はVisual Studio Code、しかもMacで開発したいので、手動で環境を整備する必要があった。この手順をまとめる。

インストール

インストールすべきは以下の2つ:

このページが公式原典なので、最新の情報はそちらを参照すること。

ディレクトリ構成

C#リポジトリは、基本的にはソリューション→プロジェクトの入れ子構造を取る。これらは、以下のコマンドで作成することができる。

dotnet new sln
dotnet new <プロジェクトタイプ> --name=<プロジェクト名>
dotnet sln add <プロジェクト名>

プロジェクトはビルドの単位で、例えば作りたいアプリが1つのプロジェクト、その単体テストがもう1つのプロジェクト、といった感じ。ソリューションはプロジェクトを束ねる単位。MSBuildでビルドするために必要な設定ファイルと考えても差し支えない。この辺りに詳細が書いてある

dotnet new コマンドは、テンプレートに沿ってプロジェクトの雛形を作成するコマンド。プロジェクトタイプはここを参考に指定する。

次に sln add コマンドで、作成したプロジェクトをソリューションに追加している。 この辺りをGUIで操作できるのがVisual Studioの良さだったんだなと実感する。とはいえCLIでもそれほど大変ではないことが分かった。

この状態でVSCodeをリロードすると、C# Extensionが良い感じに設定を生成してくれて、ビルドやデバッグができるようになる。

ちなみにこれよりも具体的なディレクトリ構造については、C#は特にデファクトスタンダードもなく自由にして良い模様。とりあえず src ディレクトリに全て入れていくようにする。困ったらまた考えよう。

テストプロジェクトの追加

これを参考にC#ではテストが別のビルド単位となるので、新規プロジェクトとして追加する。

dotnet new xunit --name=test
dotnet sln add test
cd test
dotnet add reference ../<テスト対象>/<テスト対象>.csproj

単体テストフレームワークは、 xunit, mstest, nunit のうちどれかを生成できるが、市井の情報を見る限りはxunitが一番流行っているらしい。

テストは以下のコマンドで実行できる:

dotnet test

Visual Studioのようにテスト結果をGUIで表示できないか調べたが、VSCodeだと現状できない模様。改めて考えると自分はNode.js開発するときもCLIでテスト結果を見ていたので、ここは妥協する。

これで一通り開発の準備が整った。

余談: C#のバージョン

ここまでC#のバージョンを全く意識してなかったのだが、 .NET 6.0 ではC# 10がデフォルトで利用されるらしい。

C# language versioning - C# Guide | Microsoft Learn

自分が最後に触ったのはUnity 2019の頃で、そのときはC# 6とか7だった記憶。ずいぶんと進んだものだ。最近の更新履歴はこの辺りから確認できる

ちなみにテンプレートを console にすると、以下のようなファイルがエントリポイントとして生成される。クラスもなにもないのでびっくりするが、C# 9.0からクラス定義なしの記述がサポートされたらしい

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

まとめ

上記の手順で、C# on VSCode + Macの開発環境が整う。Visual Studioとは違ってCLIベースなのでややつらいが、言語ごとにエディタをインストールするのもまたつらいので、一旦我慢して使ってみることにする。

リアル脱出ゲーム 初参加 Lessons learned

はじめに

先日新宿近辺のリアル脱出ゲームに行ってきた。めちゃくちゃ面白かったし、また行きたい。次こそ良い成果を残せるように、今回得られた教訓をまとめる。

怪しげな雑居ビルの中で開催された

教訓

基本的にゲームマスターは人の盲点を突いてくる。盲点をなくすには、普段無意識で行っている脳内枝刈りを見直す必要があるぞ。

1. 一度使った小道具は再利用されるものと心得よ

一度使った手がかりは忘れがち。でもそこが盲点です!

一度使った手がかりを再利用することは普通にある。テレビゲームの常識だと大抵一度使ったキーアイテムは捨てても良いので、まさに盲点になりがち。脱出ゲームにおいては、常に頭の片隅に置いておくか、むしろ再利用を第2候補くらいで疑ってかかるべき。

2. 掛け算が潜んでいるぞ

ばつ印がでてきたら、アルファベットのXやバツはさることながら、乗算記号も疑うべし。文字列の中に急に出てくるとパッとは気づけないので、ゲームマスターに好まれてそう。

あとはN桁のシリンダー錠など数字の暗証番号が頻出するので、ただ数字をN個羅列するよりはひねりを持たせたいのかも。あるいは、僕のように筆算だけが得意な人間のため、均等に活躍の機会を生み出すための工夫なのだろう。

多分横棒―を減算記号、十を加算記号と読ませるような問題も今後は出てくるだろう。同様に対応したいところ。

3. 物理的に不可能な仕掛けはない

バーチャルのゲームではないので、物理的に不可能な仕掛けはありえない。このことは時に大きなヒントとなる。例えばそもそも謎解きの結果をどこにどのように入力すればよいのかすら分からない場合、物理的に検出が不可能・困難な方法は選択肢から外すことができる。

この時、電子工作で入手可能なセンサーを知ってると仕掛けを想像できるだろう。メタな推論だと萎えるかもしれないが、手段を選べるほど余裕はないことを心得よ。

akizukidenshi.com

4. 形や大きさが一致するペアを探せ

何かアイテムが登場したら、その形や大きさに一致するペアが周りに存在しないか確認せよ。大抵、対応する箇所にはめ込むとか、そのような仕掛けがあることが多い。

5. 背景や装飾に仕掛けを溶け込ませるテクニック

手がかりを背景や装飾に溶け込ませて、わかりづらくする手法がある。 このテクニックを知っていれば、逆に背景から手がかりを手早く見出すことができるかもしれない。

これはほぼズルいクイズのやり口なので、たつなみさんのTwitterを見て訓練すると良いだろう。

以上。今日の自分は昨日の自分よりも強くなっている。次回の脱出に期待。