Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

go generateを忘れていないかCIで検出する

こんにちは・こんばんは・おはようございます 開発支援部基盤バックエンドチームのid:aerealです。

今回は小粒でピリリと辛いちょっとしたテクニックをお届けします。

Goとコード生成

Goとコード生成は密接な関係にあります。 なにしろ go generate というサブコマンドが提供されているほどです。

Classiが開発しているGoを採用したミドルウェア・dronにおいてもgomockによるモック実装の生成やOpenAPI仕様からGoの型の生成などで利用しています。

参考: dron: クラウドネイティブなcron代替の紹介 - Classi開発者ブログ

コード生成とRenovate

コード生成結果に係る変数は主に2つあります。

  1. 入力となるソースコード
  2. 生成するツール

ソースコードは言うまでもありません。リポジトリで管理されるソースコードは常に変更される可能性があります。

忘れられがちなのがコードを生成するツールです。

Go製の開発用ツールもバージョン管理している場合、dependabotやRenovateで他のライブラリ同様に更新がPull Requestで送られます。

アプリケーション内から使用されるライブラリであれば、十分なカバレッジを伴うテストコードがあればマージ前に非互換な挙動を発見できます。

ではコード生成に使うツールはどうでしょうか。ツールの更新に伴い生成結果が変わることもあります。 当然、テスト実行時にはコード生成を伴わないので発見できません。

性質が悪いことにコード生成 (go generate) はそう頻繁に実行するものではないので発見が遅れがちです。 その結果、入力となるソースコードも変更されてしまい一体どんな結果があるべきなのかわからなくなる……ということが考えられます。

では生成するツールを古いバージョンで固定するしかないのでしょうか。

生成したコードの差分をCIで検出する

前述の通りアプリケーション内から利用しているライブラリをある程度の自信をもって更新できるのは自動テストがあるからでした。 コード生成の結果を検証するステップをCIに組み込めばツールの更新が破壊的変更を含まないことを確かめられ、安心してツールの更新ができるというものです。

上記のdronを例にとると以下のstepを記述しています。

  • go generate を実行する
  • その後のワークツリーで git diff --exit-code を実行する

go generate は自明として、 git diff --exit-code について補足します。

--exit-code Make the program exit with codes similar to diff(1). That is, it exits with 1 if there were differences and 0 means no differences.

以上、git-diffのmanより。

差分があれば1を、無ければ0を終了ステータスに設定します。

シェルにおいて非ゼロの終了ステータスは異常を指すので「差分があれば失敗する」という挙動になります。

GitHub Actionsでは非ゼロの終了ステータスを補足するとデフォルトでそのジョブは失敗と報告されるので「go generate して差分があればジョブを失敗させる」ことができます。

おまけ: 自動でコミット・プッシュするというアイデア

GitHub ActionsはGitHubのAPIへアクセスする権限を持ったトークンが特別の設定なく与えられるという強みを持っているので、単純に差分が生じただけであれば git commit && git push させることも可能です。

マーケットプレイスにはAdd & commitのようなワークフローも公開されていますし、似たようなstepを書いてもよいです。

なおオーガニゼーションの設定や将来的なGitHub全体の変更によりトークンにデフォルトで付与される権限が狭められる可能性を考慮し、明示的に permissions を指定するのが望ましいでしょう。

参考: Automatic token authentication - GitHub Docs

むすび

コード生成した結果に差分が生じていないかをCIで検証する方法について紹介しました。

コード生成を活用するGoならではのモチベーションではありますが、一般的なアプリケーション開発に応用できるトピックではないでしょうか。

ClassiではPull Requestのコメント欄でよく見かける指摘を自動化させる気概のあるエンジニアを歓迎します!

コロナ禍で失われた交流と挑戦の機会を回復するためハードル低めの社内LT会を半年運営してみた話

 みなさんはじめまして。Classi で開発支援部に所属するイノウエと申します。

 当社は教育事業を手掛けているため、教育に関心のあるメンバーが集まっており、ジュニアメンバーに対する社内教育にも力を入れています。巷を騒がす感染症の流行により、当社でもリモート勤務体制がはじまったことで個人としては働きやすさが向上したものの、社内教育や他チームとの支援・交流といった面に目を向けると決して良いことばかりではありません。また、以前では盛んに開催されていたウェブ開発者による勉強会コミュニティの縮小傾向も仕方のないこととはいえ無念に感じています。

 このような問題意識に対してなにかできることはないかと思い、「プチトーク会」なる社内イベントを企画し、運営してみたところ比較的感触がよく、まずは半年間続けることができました。この記事ではプチトーク会の目的と設計、運用してみた感想について記したいと思います。

f:id:ClassiJP:20220201112504p:plain

目的

  • 開発者メンバー同士の交流を促すこと
    • リモートワークが中心となった結果、他のチームとの交流の機会が格段に減ったことに対して手を打ちたい
    • 以前であれば立ち話や偶発的に発生していた機会を創出する
  • 発表の場を提供すること
    • コロナ禍で勉強会コミュニティの縮小によりイベントに行きづらくなった
      • 打席に立つことで初めて得られる経験もある
    • 発表の準備を行うことで自分自身の体験・知識が整理される
      • 他者に教えることこそ最高の学習法
    • 他者に対して何かを伝えるということは本質的に難しいということを身をもって学ぶ
      • 聞き手・受け手としての成長にも繋がる

背景と要件

  • 自発的に発表・参加するタイプのカジュアル勉強会は既に社内に存在する
    • 内容は面白く充実しているが、正直参加率が芳しくない
      • 毎回参加する人とまったく参加しない人の二分化
    • 挙手制ではうまく行かない?
      • 他にも自主的に参加する系の集まりがいくつかあるがメンバーが偏る傾向がある(輪読会など)
      • 発表側に事前準備があったり、繁忙期に参加することについて内心チームメンバーに引け目を感じているのでは?
  • 運営・発表・聴衆の三方の負担をほんとうに最小限にすること
    • 私も開発者としての業務のかたわらで運営する必要がある
    • 発表者は事前の準備(スライド作りとか)が大変だとやはり負担になる
      • 指名制なので特に気を使わないといけない
    • 聴衆側も頻度が高いと別の予定と被りやすく負担になる
  • 半年に1度ぐらいは何か発表してほしい
    • 希望を言えばもっと高頻度だと嬉しいけど、業務の片手間としたらこのくらいがちょうどよさそう
    • 開発者メンバーは100名弱なので年間200コマ弱必要

設計

  • 開催スケジュール
    • 30分枠で隔週開催
      • 年52週なので26回開催
    • 30分を1人あたり4分として7コマに分割
      • 7コマ × 26回 = 年182コマで要件に対応
  • 発表レギュレーション
    • 1トークあたり発表3分+質問応答1分で時間厳守(LT大会のドラ方式)
      • 最悪3分であれば当日15分ぐらいあればババっと書ける(と思う)
      • 時間制約上本当に3分で切らないと間に合わないので容赦なく切る
      • 質問は1回のみで質問時間も含め1分以内でバッサリ切る(質問できなかった場合は esa のコメント欄に誘導する)
    • 自己紹介禁止
      • 経験則で発表慣れしてない人ほど自己紹介に時間を半分以上使ってしまい本題に入れないため
      • どうしても入れたければ発表の一番最後にしてもらう
    • テーマは完全自由
      • 自分で決められない人向けのお題ガチャもある(趣味or技術)
      • 私は庭の雑草対策の話をしました
    • 発表スライドは esa のプレゼンテーション機能を使う
      • Markdown から自動変換で作成されるため華美で複雑なスライドを作れない、作れないということは見た目を気にする必要がないので時短◎
      • 制約により内容を簡潔にする必要があるため資料準備の時短◎
      • 自然と esa の1記事となるので情報がストック(蓄積)されて、フィードバックにコメント機能が使える
  • 抽選による指名で発表者を決定する
    • 一巡するまで発表者のかぶりがないようにする

実装

  • Google カレンダーで最大公約数的に比較的予定が入っていなさそうな30分枠を見極めて隔週30分の繰り返し設定で予定を突っ込んでしまう
    • 先に枠を抑えておけば将来的にかぶらなくなっていく
  • Google スプレッドシートを作成して先程のカレンダーから生成された日付を突っ込む
  • 従業員名簿に適当に序数を振った上でかぶりなし乱数を生成して1開催日あたり7人になるように割り振る
  • 運営用の Slack チャンネルを作って次回開催日の発表者にメンションで事前お知らせをする
    • 都合が悪ければ当事者に日時の交換を調整してもらう、交換の調整を行うこと自体も交流のひとつになるので運営者はあまり介入しない
  • 開催日当日は直前にお知らせ用チャンネルで呼び込みする
  • 通話ツールは Google Meet を使い、発表者がそれぞれ資料を画面共有する方式
    • 普段は画面共有機能を使わない人もよい練習になる
  • タイムキープはスマートフォンのストップウォッチで行う
  • ドラ(3分強制終了)の音はいいアイデアがないので私の声(じゃ〜ん)でやっている(恥ずかしい・・・)

結果と感想

 運営者は先程の Google スプレッドシートにたまに乱数を生成して埋めるだけでいいので、ほぼ手間なしで半年間運用することができました。(発表者が一巡しました)

 オーディエンス参加者も平均全体の7〜8割程度でかなり好調に推移しており、普段話したことがない同僚から思わぬ話が飛び出したりすることで、その後の交流やアクションに繋がっているようで、おおむね期待の通りの成果を出せているかなと思います。

 運営者の負担がほとんどかからない(当日の司会業ぐらい)ことから、当社だけでなく他社にも輸出できるフォーマットだと思いましたので、同じような課題感をお持ちの方がいればぜひ参考にしていただけると幸いです。

 続けてきた中でのマイナーチェンジとしては、やはり30分7枠だと時間の余裕が本当にないため、(いい質問してるな〜)と思ったタイミングでも強制終了せざるを得ないのがもったいなく感じ、若干時間オーバーしてもよい余地を持たせるために30分6枠にしたり、つい最近入ってきたばかりの新人メンバーを手厚く歓迎したいので質問時間をやや長めにしたりという工夫をしたりしていますが、ここは司会進行者のクセ次第だと思うのでまずはやってみて感触を確かめながら変えていくと良いと思います。

 また、コロナ禍が落ち着きを見せてコミュニティで発表する機会が復活した折には、飛び込みでLTを募集されたときなどにこのプチトーク会に向けて準備した内容や資料をそのまま流用することもできるはずなので、当社で自然と働いているうちに将来のチャンスに向けた資産を気づかないうちに蓄積できている、ということもウラの狙いの一つです。なかなか普段から意識的に行うのは難しいと思いますので、そういった側面からもメンバーに対する支援になれば良いと思っています。

 最後に宣伝ですが、 Classi では子どもの無限の可能性を引き出すために学校に特化したサービスを提供しています。これまでのキャリアで培った経験を子どもたちの未来のために活かしてみませんか?興味がある方はぜひこちらから応募をお願いします。

新入生募集中

「Python FlaskによるWebアプリ開発入門」の紹介

こんにちは、データAI部Pythonエンジニアの工藤( id:irisuinwl )です。 今回は弊社Pythonエンジニアである平田さんが著者の一人であるPython FlaskによるWebアプリ開発入門のご紹介をいたします。

私もレビューに関わり献本頂いたので、その魅力をお伝えできればと思います。

目次

  • 本の概要
    • 内容紹介
    • 誰向け?
    • ここが嬉しい
  • 各部の説明
    • 第0部 イントロダクション
    • 第1部 Flask入門
    • 第2部[Flask実践1]物体検知アプリの開発
    • 第3部[Flask実践2]物体検知機能のAPI化/デプロイメント
    • 第4部 機械学習APIの開発

本の概要

内容紹介

「Python FlaskによるWebアプリ開発入門」はタイトルの通り、Webアプリケーションを作りながらPythonのAPIフレームワークであるFlaskを学んでいく本です。
Flaskの概要と使い方を紹介した後に実践的にWebアプリケーションを作成していきます。

この本で作るアプリケーションとしては以下になります。

  • 認証機能のある簡単な問合せアプリケーション
  • 物体検知アプリケーション
    • アプリケーション実装
    • 機械学習APIの実装
    • テスト作成
  • 機械学習モデルの検討、データ分析コードを移行したWebアプリケーション

様々な機能のアプリケーションを作ることで実践的なFlaskの使い方を学べる非常に良い本だと感じました。

誰向け?

この本を読む上で向いている読者は以下のように思いました

  • 向いてる人

    • Pythonの文法を抑えているが、アプリケーションを作るためのHTTPの知識やインフラの知識が薄く、アプリケーション開発をしてみたい人
    • Pythonでモデル開発をしているが、プロトタイピングのためにAPIとWebアプリケーションを作成したいと思っているデータサイエンティスト
  • 向いてない人

    • Pythonを全く触ったことのない方

ここが嬉しい

この本を読んで嬉しいポイントを紹介します

  • 様々なフレームワークと比較してFlaskを使う利点が紹介されている
  • APIのコードだけでなく、APIを動かすインフラ、テスト、機械学習モデル開発といったWebアプリケーション開発に関連する項目を広範に扱っている
  • 説明が丁寧
    • APIを作る上で必要なHTTPの知識(Cookieやセッションなど)
    • 関連するPEPの提示
    • 関連するコードのディレクトリ構成が明示されていて分かりやすい

非常に説明が丁寧でアプリケーション開発に関わることをなるべく省略せず解説しているため、Pythonを使ってアプリケーション開発を始めようという人にとって非常に学びの深い本であると感じました。

続きを読む

完走したClassi developers Advent Calendar 2021を振り返って今年を締めよう!

挨拶

こんにちは・こんばんは・おはようございます。開発者ブログ編集部長のid:aerealです。

時は師走、ソフトウェアテック界隈ではおなじみとなったアドベントカレンダーの季節でした。

当社もClassi developers Advent Calendar 2021と銘打ち、25本の記事が集まりました。

これまでもClassiでアドベントカレンダーはやっていたのですが、今年は開発者ブログ編集部を立ち上げてから始めてアドベントカレンダーの企画・編集を編集部が執り行いました。 編集部員が5名に拡大したことにより25日間に渡る怒涛のレビューや進捗管理に挑戦できるだろうと踏んでのことでした。

今年は執筆者のユニークユーザー拡大を目標にこれまでアウトプットの経験が少なかったメンバーを中心に声をかけました。 おかげでアドベントカレンダーをきっかけに始めて記事を書いたよというメンバーも増えて何よりです。

今回は無事にアドベントカレンダーを完走したことを祝して編集部で改めて投稿された記事を振り返り、一言ずつコメントで紹介することにしました。 もしまだご覧になっていない記事があったらぜひこの機会に読んでみてください。

振り返り

12/1 id:tetsuro-ito: 開発本部長になってやったこと - tetsuroitoのブログ

id:ClassiJPより

職位の大きな変更に際し、短い期間で何を考えどう動いたかがまとめられています。見返しててあらためて思ったのですが、1ヶ月で100名弱のメンバー全員との1on1をやったり新部署を作ったり、ここに書かれていることだけでもタフなムーブすぎて驚きです。

12/2 沼沢一樹: GitHub に AWS 認証情報を持たせずに、Actions で S3 Backend な Terraform の plan を実行する - Qiita

id:aerealより

ホットな AssumeRoleWithExternalIdentity の話題でした。

こういった認可まわりを変える際って一般的な書き方になっていて文書から仕様を読み取りづらかったり、試行錯誤するのも大変だったりするので身近な人の事例がまとまっているととても助かります。

余談ですがJWTの sub にリポジトリが入っていてtrust policyでそれを参照できるの、JWTの良いところが出ているなあと思います。こういうところが好きです。

12/3 id:tkdn: JSConf JP に参加してきました - Classi開発者ブログ

id:kiryuanzuより

今年の11月に開催された JS Conf についての感想が書かれた記事です。発表を聞いてアクセシビリティについて改めて学んだ話や OSS やソフトウェアが成立するためのプラットフォームの話など、多方面な発表が行われたようで、自分自身は未参加でしたが大変興味をそそられました。年末年始にアーカイブの方で聴講したいと思います!

12/4 Sab: iOS15からのキーボード回避[UIKit]

id:tkdnより

OS のキーボードが表示された際にどうするか、iOS 15 から実装可能になった UIKeyboardLayoutGuide を紹介していただきました。単に機能紹介ではなく、じゃあこれはどうなの?といった気になるところまで掘った良記事でしたね

12/5 id:lime1024: ecspresso をより安全に使うために - らいむぎばたけ

id:tetsuro-itoより

ecspressoを安全に使うためのモチベーションや実際にハマったポイントなどをコードとセットで紹介していて、他の人の参考にもなりそうでした。

12/6 youichiro ogawa: Next.js, Prisma, Apollo GraphQL, Nexusで作るシンプルTODOアプリ

id:ClassiJPより

実際に作りながら、各種ライブラリはどうして、何に使うのかが丁寧に記載してあってとても参考になります。GraphQL Nexusは知らなかったけど便利そう!って思ったのであとで触ってみようと思いました。

12/7 id:kazumeat: リモートワークのための質問力向上研修を実施しました - Classi開発者ブログ

id:kiryuanzuより

今回のアドベントカレンダーで最多ブックマークを記録した記事です🎉

社内で実施された質問力向上研修について大変丁寧かつ分かりやすくまとめていただきました。質問をする際や回答する際に「この伝え方で大丈夫かな?」と困ったらこの記事を見返したいと思います。

12/8 id:kiryuanzu: Hardening 2021 Active Fault 参加レポート - 桐生あんずです

id:aerealより

id:kiryuanzuさんのこの記事をきっかけにHardeningという競技を知ったのですが、実際の業務にかなり近い……というかほぼそのままといって良い内容で精巧さに驚きました。

「セキュリティホール90%オフ」というチーム名はとんちが効いていて好きです。

12/9 id:ruru8net: Amazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話 - Classi開発者ブログ

id:tkdnより

Datadog 日本の公式 Twitter にも捕捉されていました。バッチの監視といったところだけではなく、社内の Datadog 勉強会でもカスタムスパンを使ったトレーシングの知見も発表していた id:ruru8net のナイス監視実践記事。

2 年前の記事もリンクされており若手の成長を感じる良い記事ですね。

12/10 id:tomoyanamekawa: Cloud Composer 2へのupgradeでどハマりした話 - Classi開発者ブログ

id:testuro-itoより

最近GAになったCloud Composer2のアップグレードに際して、ハマりやすいポイントを解説していて、良かったです。ドキュメントもまだ少ないので、貴重な記事でした。

12/11 id:onigra: EC2からECSへ移行する道のり - Classi開発者ブログ

id:ClassiJPより

成長機会を積極的に取りに行く若手メンバーとそれを導くムーブができる人がいてめちゃめちゃいい会社じゃん〜と思ったら弊社でした。僕も横で見てましたがすごい勢いで作業が進み若者が成長していく様子は、見ててとても気持ちがよかったですしめちゃめちゃ刺激になりますね!

12/12 北原幹也: デザイナーとエンジニアの対話で、UX/UI の意思決定のボトルネックを解消する - Qiita

id:aerealより

「対話がうまくいっていない」という出発点から始まり、物の本を当たりながら「アンチパターンを踏み抜いていた」「ユーザビリティとはなんなのかの定義が揃っていなかった」「デザイナー視点での評価基準をエンジニアが把握できていなかった」と現状理解を深めひとつずつ解決していく丁寧なストーリーでした。

12/13 id:tasmaniadecoco: IAM Policy Simulator で「必要な権限足りてる?」を確かめる - Classi開発者ブログ

id:kiryuanzuより

IAM Policy Simulator についてはよく知らなかったのですが、自分も記事内の同じ課題に直面していたことが少し前にあったので大変参考になりました🙏

レビュー依頼時のコミュニケーション課題に対して問題意識を抱えて解決しようとする姿勢にも真摯な印象を受け、振る舞いや考え方に対しても参考にしていきたいと思える記事でした。

12/14 id:khaigo: UIKitでDesign Systemを実装する - Classi開発者ブログ

id:tkdnより

アプリにおけるデザインシステムを UIKit で実践する記事です。Swift のケースではどうなるかも記載があって静的型付けしたいよな〜わかる〜という感想が湧いてきました笑 今後の伸長に期待ですね!

tetoru(テトル) に関わる内容が開発者ブログでもちらほらと… id:sasata299エンジニアに役割を変えた記事でも触れています。

12/15 おかじ: 入社前に不安だったリモートワークでのコミュニケーションは実際どうだったのか

id:tetsuro-itoより

リモートワークにおいて、コミュニケーションが重視される中で、とても示唆に富む記事でした。リモート時に入社したことがある人とそうじゃない人のナラティブに端を架けてくれる記事で、いろんな人に読んでもらいたいですね。

12/16 id:seiga_hayashi: 育休を取得することにしたらチームの状態が改善した話 - 目だ!目を狙え!

id:ClassiJPより

とても大事な育児休業についてと、それを取得するために社内でどのような準備をしたかについて解説しています。これから育児休業を取る方の参考になる点もあるのではないでしょうか。

12/17 id:c5meg1012: バックエンドエンジニアが基盤インフラチームに異動して半年ほど経った話 - めるノート

id:aerealより

id:c5meg1012さんとは隣のチームで仕事しているのですが、産休・育休から復帰のタイミングでキャリアチェンジも図るというかなり意欲的な態度で驚くと同時に着実にキャッチアップされていて負けていられないなという気持ちになります。 ちなみにその後、AWS認定 ソリューションアーキテクト アソシエイトに合格されたそうです。

12/18 id:ttakagi1021: トピックモデルを使って問い合わせ内容を分析した話 - Classi開発者ブログ

id:kiryuanzuより

Classi を利用されているお客様からの問い合わせ内容を言語処理技術の一つであるトピックモデルを使って分析した記事です。分析の流れをわかりやすく述べており、その分析結果から今の課題や施策の効果などの考察について説明されています。 データ分析の活動から効果的な施策を検討していく流れの一例を知ることができ、大変面白い内容でした。

12/19 横田貴之: SwiftLintをGithubActionsのmacOSではなくubuntuで実行する

id:tkdnより

GitHub Actions の OS イメージが macOS になることのコスト感どこも課題に感じることはありますね。SwiftLint を動作させるためのステップが詳しく紹介されています。記事内に登場した GitHub Actions におけるトークンのパーミッションの制御は今年できるようになったんですよね。

id:aerealハマっていたようなので GitHub Actions の実行権限について考える場合はご注意を!

12/20 陳巍: 伝わるAPIドキュメントを作成する - Qiita

id:tetsuro-itoより

OpenAPIで伝わるための工夫を書いてくれました。育児でなかなか時間を作れない中でとてもよかったし、こういった工夫の繰り返しが円滑なAPI設計やコミュニケーションの流通を促しますね。

12/21 id:irisuinwl: FirestoreのCRUD APIを作って、負荷試験をする - irisuinwl’s diary

id:ClassiJPより

自作の負荷テスト用アプリケーションの解説にとどまらず、Firestoreとはどんなものかや負荷テストする上での考慮点等にも触れられていてとてもよかったです!

関係ないですが、本記事を読んでこういうツールがあるんだ〜と思ってlocustで検索したら虫の画像が出てきたのでちょっと慌てました。

12/22 id:nakaearth: 推理小説をグルーピングするために特徴語を抽出してみる - nakaearthの日記

id:aerealより

特徴語抽出して遊ぶシリーズは定番ともいえますが、ミステリ小説をかけてみるという発想がおもしろいですね。 word2vecなんかを使って意味・構文埋め込みをして叙述トリックが使われている作品とそれ以外の作品でなんらかの特徴が浮かんでくるんだろうか? という着想を得ました。

12/23 id:minhquang4334: ISUCON11予選課題の27万点まで練習し新人エンジニアが学んだこと - Classi開発者ブログ

id:kiryuanzuより

2020年秋に新卒として入社された id:minhquang4334 さんによる ISUCON11 の振り返り・復習記事です。ISUCON初参加で100/598位という結果も残されていることも大変凄いのですが、その後さらに知らなかったことに対して満遍なく吸収する姿勢が本当に素晴らしく、自分も見習おうとなりました。

12/24 id:JesseTetsuya: Flask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作ってみたら役立った話 - Classi開発者ブログ

id:tkdnより

Excel 運用されていたデータのマスター管理によって調査やデバッグに時間がかかっていた課題、Excel に RDB のテーブルスキーマが整理されていたのがつらいという課題へソフトウェアを介入させ、まさに「ザ・カッとなってやった課題解決!」といった記事で気持ちがいいですね。

12/25 id:nkgt_chkonk: 2021年Classiに起こった変化の振り返り - Classi開発者ブログ

id:tetsuro-itoより

今年の技術視点でのふりかえり記事でした。読みながら、今年も変化してきたなと内省することができたし、これからの変化の仕込みにも期待ですね!

おわりに

記事投稿時にレビュアーとして目を通す機会もあり、その時に気付いたことがひとつあります。 それはこれまでに投稿した記事へ言及し「今はこうだよ」「これに刺激を受けてやってみたよ」といった内容が多かったことです。

実際、社内でも「○○さんが詳しいよ、前にこういう記事を書いていて──」と紹介するようなシーンがここ1年で増えたという実感があります。

それぞれ別々のチームで働いていてもやっていること・関心のあることに連続性があると改めて実感します。 そしてこの感覚を編集部だけではなく記事を書いたメンバーにもそれぞれ感じてもらえたことで、アウトプットを積み重ねることの大きな意義が少しでも伝わったのであれば編集部冥利に尽きます。

アドベントカレンダーというお祭りはまた1年後までのお預けになりますが、Classiはこれからも着実にアウトプットを通して研鑽していきます。 それではみなさま良いお年を!

2021年Classiに起こった変化の振り返り

VPoTの id:nkgt_chkonk です。この記事は Classi developers Advent Calendar 2021 25日目の記事です。昨日は平田(@JesseTetsuya)さんによるFlask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作ってみたら役立った話でした。

さて、あと少しで2021年も終わってしまいますが、2021年、みなさんはいかがお過ごしでしょうか。Classiにとっては去年から引き続き「変化の年」だったなあ、というのを強く感じています。本記事では、この一年、Classiがどのような課題に直面し、どう変化してきたのかを振り返ってみたいと思います。

年明けから春まで

去年の話になってしまいますが、Classiは2020年4月に、大型アクセス障害を起こしてしまいました。この反省から、同じような事故でユーザーのみなさまにご迷惑をおかけしてしまうような事態を繰り返さないために、対症療法ではなく根治療法として組織の形の変革や仕組みの変更を行いました。その詳細は2020年11月に書いた セキュリティインシデントと大規模障害を経てClassiは開発組織をどう変化させたのか - Classi開発者ブログ に譲ります。

その後、年明けから今年の春に向けて、新しくなった組織で、各チーム、各メンバーはとにかくシステムの課題の改善に必死にとりくんでくれました。わたしにとって印象的だった例としては、「スーパーN+1」「大迂回が必要な改修」や「オブザーバビリティへの継続的な取り組み」があります。

スーパーN+1

まず、「スーパーN+1」です。Classiには、社内で「スーパーN+1」と呼んでいた高いシステム負荷を引き起こす問題が散見されていました。あるユーザーからの1リクエストに対して、パラメータを変えながら何回も内部APIを呼び出します。この時点でAPIに対するN+1が起こっているのですが、なんとその内部APIでも、DBに対してN+1問題を引き起こしているような問題です。

スーパーN+1を改修するためには、APIを呼んでいる側呼ばれている側双方にまたがる改修を行う必要があるため、修正が高コストになりがちです。また、Classiでは、自動テストが不足しており、このこともスーパーN+1を改修するにあたって修正の高コスト化を呼んでいました。それだけではなく、以前のClassiはデプロイが事故りやすかったりロールバックに時間がかかったりというデプロイ上の問題も抱えていました。これも、スーパーN+1を解消するための修正コストを跳ね上げていたひとつの原因です。

そんな中で、デプロイ上の問題を解決するためにECS化を進めたり、それと同時進行でスーパーN+1を解消したりと、自分たちの管理しているサービスに対して適切にシステム上の問題を解決していったメンバーを見ていて、大変心強かったのをよく覚えています。このように、日々きちんとシステム上の問題が解決されていくような光景は、2020年春以前にはなかなか見ることができない光景でした。本当に心強く「Classiって変わったな」と思うことが多い日々でした。

大迂回が必要な改修

次に「大迂回が必要な改修」です。実はClassiには、RDB上でデッドロックを引き起こしてしまう機能が存在していました。その機能はデッドロックを引き起こしてしまうということの他にも、ユーザーにとって不利益となる根本的な仕様上の問題を抱えており「この機能はなくしてしまうべきだ」という判断がなされました。

しかし、その機能を無くしてしまうことにより、不利益を被ってしまうユーザーも当然存在するわけで「いまある機能とは別のやり方で、その機能で実現できていたことを実現させる」という必要が出てきます。つまり「問題を起こしている機能をなくすためには、別の機能をリリースしてからその機能を無くさなければならない」という、迂回が必要になるわけです。

このようなロングスパンの改善も、しっかりとやり切ってくれて「コストがかかるけどわかりやすく新しい価値が生まれるわけではない。しかし問題は抱えている」というような部分の改修をきちんとやりきれたことも、Classiの大きな変化を感じる例でした。

オブザーバビリティへの継続的な取り組み

最後に「オブザーバビリティへの継続的な取り組み」があります。これは id:ruru8net によるAmazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話などがわかりやすいかと思います。

現在のClassiでは、なんらかの障害が起こったあと、レトロスペクティブで「これを検知できていたら傷は浅かった」みたいな項目が見つかるたびに、そのための監視の仕組みが整備されるような動きが継続して行われています。これも「そこに投資しても直接わかりやすく新しい価値を産むわけではないが、将来の障害を減らし、素早くプロダクトを進化させていくためには必要」という種類の投資です。このような投資が当たり前にされるようになったことに、やはり大規模アクセス障害以前との変化を強く感じています。

このような変化をメンバーひとりひとりが自発的に起こしてくれた結果として、今年の春は去年よりも大きなトラフィックがあったにも関わらず、大きな事故なく春のピークを乗り切ることができました。これは本当にメンバーひとりひとりが尽力してくれたおかげだと思っており、とても感謝しています。

とくに、システム改修のために力をたくさん発揮してくれた若手のエンジニアが、今やClassiに欠かすことのできない戦力として活躍してくれていることが本当に嬉しく、心強く思っています。

夏〜秋

さて、春にアクセスのピークを乗り換えたあと、苦しい時期が続いたと思っています。というのも、システム面の問題の改善は、マイナスをゼロに近づけるための仕事です。一方、ユーザーの抱えている課題を解決する為には、システムの機能適合性や使用性の向上のような「ゼロをプラスに」する仕事もしなければならないわけです。

「去年のような事故を起こさない」という大きな目標をひとつ越えましたが、まだまだ直さないといつか爆発してしまうシステム課題も残っています。この課題に取り組みながら、システムの機能適合性や使用性を向上させていく取り組みが、夏〜秋にかけて多くなってきました。

ここで課題になってきたのが、リポジトリ単位でチームが組成されていることと、分断され絡み合ったモノリスになっていることです。

Classiのリポジトリは、基本的には機能単位で分かれているのですが、中央に巨大なDBが存在し、そのDBを各リポジトリで共有しています。一方、「機能単位」とはいえ、Classiというひとつの大きなプロダクトに対して「ひとつの機能」だけで価値を提供することは難しく、さまざまな形でリポジトリ同士が絡まり合っています。

また、「Classi」という大きなひとつのプロダクトで使用性や機能適合性を向上させるためには、探索的にユーザーの課題を解いていく必要があります。そのため、これらの向上のためには、システム課題の改修以上にリポジトリを超えた柔軟な協働が必要となります。

さらに、分断され絡み合ったモノリスの中では、影響範囲が分かりにくい中で、自分のリポジトリだけではなくて全体を見ながらやりたいことを達成するための、技術的な戦術を描けることが必要になります。このような戦略を描くためには、アーキテクトと呼ばれるようなひとたちが持っている、かなり高度かつ広範囲なエンジニアリング能力が必要です。そして、各チームが自律的にその動きをできるようにならないと、Classiの進化をスケールさせることはできません。

また、各チームには各チームのやりたいことがあって、しかし分断され絡み合ったモノリスの中ではそのやりたいことを実現しようとするとどうしても他チームの協力が必要となります。しかしそのチームにもやりたいことはありますので、チーム同士の利害がバッティングしてしまうことが多発します。

こういった技術的な課題、組織的な課題が健在化してきたのが、夏〜秋にかけての苦しい時間だったと感じています。また、この時自分は、まだVPoTとして解くべき課題がわかっていなかった、と反省しきりです。わたしがVPoTとしてすべきことを模索している中で、チームのメンバーは組織に起因する課題にぶつかって日々苦しんでいました。

「みんな全力で頑張ってくれているのに、どうしてもうまく成果につながらない」という時期を過ごすことで「みんなが全力で頑張ってくれたらその分だけちゃんと成果がでる仕組みを作ること」が自分の仕事のひとつなのだ、と理解することができたのですが、それを自分が学んでいる間は、本当にメンバーに苦しい思いをさせてしまいました。

秋〜今にかけて

「みんなが全力で頑張ってくれたらその分だけちゃんと成果がでる仕組みを作ること」が自分のVPoTとしての仕事なのだ、と理解したのち、まずは「なぜ頑張ってくれているのに成果に結びつけることができていないのか、それを解決するためにはどういう構造をとればいいと思っているのか」の自分なりの分析を社内に対して発信し、社内のさまざまな立場の人と対話をし始めました。この発信と対話の中から生まれたひとつの解が「チーム横断バックログ」として現在運用され始めています。

このチーム横断のバックログは、チームの利害関係がバッティングしスタックしてしまう課題に対して「まずはどのチームがどんな解きたい問題を持っていて、そのためにどのチームの協力が必要なのかを可視化、管理する必要がある」と考え、作成しました。このバックログを作るにあたって、作ると決めて考えるまでは簡単だけど、全社の理解を得て実効させていくのはかなりカロリーの高い仕事であるということも学びました。個人的な話になってしまいますが、こういう高カロリーな仕事は片手間ではなかなか進めることはできないので、自分のような責任者がきちんと進めていくことが必要で、それが今現在のClassiでVPoTがやるべき仕事のひとつなのであろうと学んだ時期でもあります。

ただ、このチーム横断のバックログはまだ運用がへろへろで、その改善は継続的に必要だとも考えています。いつか続報をお知らせできたら嬉しいです。

さて、チーム横断のバックログがあることで、チームごとの利害がバッティングしたときの調整は以前よりもうまくできるようになりました。しかし、それだけでは、組織的な問題はある程度解決しても、チームがそれぞれアーキテクト的な動きをできなければ結局物事が進まない問題は未解決となってしまいます。

ここに対しては、ふたつのアプローチを現在考えています。ひとつは、技術的なアプローチ、もうひとつは組織的なアプローチです。

まずは技術的なアプローチに関してです。そもそもなぜこんなにも機能開発やシステム修正の難易度が高いのか。それは、内部品質の低さに依るものです。内部品質が高ければ高いほど、プロダクトイシューを解決するために必要となる技術的難易度は下がっていきます。なので、技術面でいえば、「外部品質や利用時の品質につなげるために、内部品質を向上させること」が必要なわけです。

弊社のプロダクトは、お世辞にも変更容易性やテスト容易性が高いわけではありません。ここに対して、スケジュールを切った上でまずは自動テストの拡充から始めています。今のままだと、高度かつ広範囲なエンジニアリング能力がプロダクトイシューの解決に必要となってしまうわけですが、内部品質を継続的に高めることによって、より多くのメンバーのより多くの力がプロダクトイシューの解決に寄与できるようになることを目論んでいます。

一方、組織的なアプローチとしては「では目下足りていない内部品質に対して、2022年度はどのようなチーム構造を作り、どのような仕事の進め方をすれば、最も素早くプロダクトイシューを解決できるのか」の戦略を各組織長とともに立て、実効させていくための立て付けを、目下構築中です。

まとめ

今年一年を振り返ってみましたが、ひとりひとりのメンバーが課題感を持って動いてくれた結果、Classiは本当に変わったと実感します。結果として、新しい問題が立ちはだかっている状態ではあるのですが、これはつまり「ボトルネックが移動した」というやつだと思っているので、前向きに、新しくでてきたボトルネックに対応し続けていけるVPoTとなっていきたいと思っています。その結果、来年はさらなる変化を起こして、もっとユーザーの感じる価値に真摯に向き合い素早くプロダクトを進化させていけるようになりたいし、そのための仕組みをきちんと作っていくのがぼくの仕事なので、周りに助けを求めながらしっかりその職責を果たしたいと考えています。

最後に、今回の記事で赤裸々に書いた通り、現在のClassiは決して技術的にも組織的にも「理想的な環境」とは言い難いです。しかし、一から綺麗な環境を作るよりも、すでに問題を抱えている環境をより良い環境にしていくことは技術的にたいへんチャレンジングな課題だと私は考えていて「課題解決ジャンキー」にとっては大変に刺激的な組織であると思っています。課題解決がやりたいひとにとっては本当に無限に機会があるので、自分の力で大きな課題を解決したいみなさまはぜひ! 採用情報 | Classi(クラッシー) - 新しい学びが広がる未来の教育プラットフォームを創る からご連絡いただければ幸いです! カジュアルにお話しするところから始めましょう!

Flask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作ってみたら役立った話

この記事はClassi developers Advent Calendar 2021の24日目の記事です。

こんにちは、データAI部でPythonエンジニアをしている平田(@JesseTetsuya)です。普段は、PoCとデータをもってくる、というところ以外全部やる、というスタンスで開発業務を行っています。

今回は、Flask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作っておいたら、諸々あとから役立った話をしていきます。

コンテンツマネジメントシステムとは、WEBテストで利用する一部のコンテンツを管理をする社内用システムになります。メタデータマネジメントシステムとは、データ分析基盤のテーブル情報やカラム情報を管理するシステムになります。

まずは、それらシステムの土台となっているFlask App Builderの説明をしていきます。

Flask App Builderの概要

Flask App Builderは、Flask製のCRUDアプリケーションをさくっと作れるオープンソースです。

Flask App Builderのドキュメントはこちらです。インターフェースは、こんな感じです。

f:id:JesseTetsuya:20211223153926p:plain

この画像は、リポジトリ内にあるサンプルの一つです。コードはここにあります。

上記のサンプル以外にも、それぞれのユースケースごとの機能が実装されたサンプルコードはここにあります。

Flask App Builderって何ができるのかを理解するには、下記のREADMEを読めば概要を理解できるかと思います。

f:id:JesseTetsuya:20211223153955p:plain

下記コマンドでさくっと試してみることができます。 わかりにくい方は、こちらのyoutubeの解説動画をおすすめします。

(venv)$ pip install flask-appbuilder
(venv)$ flask fab create-app
Your new app name: first_app
Your engine type, SQLAlchemy or MongoEngine [SQLAlchemy]:
Downloaded the skeleton app, good coding!

(venv)$ cd first_app
(venv)$ export FLASK_APP=app
(venv)$ flask fab create-admin
Username [admin]:
User first name [admin]:
User last name [user]:
Email [admin@fab.org]:
Password:
Repeat for confirmation:

(venv)$ flask run

データのCRUD検索処理ユーザーの認証権限管理のようなフルスクラッチで実装すると手間がかかる機能はデフォルトで用意されており、UIも見やすい状態になっています。

このFlask App Builderをカスタマイズして、コンテンツマネジメントシステムとメタデータマネジメントシステムの開発からインフラ設計・構築までを一人でさっと作ってみてどのように役に立っているのかを書いていきます。

まずは、コンテンツマネジメントシステムです。

コンテンツマネジメントシステムの概要

作ったものは、こちらです。 コンテンツ情報が一覧できる画面の写真です。他にももっと閲覧できる画面は、あります。

f:id:JesseTetsuya:20211223154944p:plain

最初は、Google Kubernetes Engine(GKE)の1クラスターまるごと使っておりましたが、インフラコスト的に贅沢すぎましたので現在は、他のアプリケーションもデプロイされているGKEクラスターの社内用ツールのnamespaceを切ってそのnamespace内のpodにデプロイしています。

既に機能が揃っているのでさっとモックを作って見せながら要件をヒアリングしつつ、開発していきました。

他チームからデータや機能の要望があれば、改善を都度行っています。

コンテンツのマスターデータ以外にもWEBテスト機能に必要なデータへのCRUDができるようになっています。

では、コンテンツマネジメントシステムを作って何が嬉しかったか、という話をします。

コンテンツマネジメントシステムをつかった課題解決

コンテンツマネジメントシステムをつかった課題解決は、以下です。

課題

  • 生徒が解く問題のマスター情報を別のチームがエクセルで管理しており、最新の状態の中身を確認するのに手間だった。

解決

  • 一通りのWEB上で問題マスターデータに対してCRUD、検索、フィルター、並び替え、ファイルアップロードなどの機能を追加することで、Excelファイルの散在やコミュニケーションコストや日頃の調査やデバック工数を減らすことができた。

開発工数

  • オープンソースなのでアプリケーション自体は、無料.

  • アプリケーション実装に2, 3人日、インフラ構築に2週間ぐらい。

その他役立ったこと

  • QA実施の際にQAチームにデータ確認をしてもらうのに役立った。
  • WEBテスト機能の開発者がデバックするや仕様確認する際に役立った。

本来の調査やデバックならば、エンジニアが踏み台サーバーにはいってそこからmysqlコマンド叩いて確認してなどの作業が必要ですが、このコンテンツマネジメントシステムのおかげで、さくっとテストデータを入れての確認などがしやすくなりました。

現在では、運用保守の状態で必要に応じて機能を追加していくというような状態です。 今後は、エンジニアバックグラウンドのない方でも利用しやすいようなUIにより改善していければと思います。

次に、メタデータマネジメントシステムのほうをみていきます。

メタデータマネジメントシステムの概要

作ったものは、こちらです。 テーブル名やカラム名は、BigQueryのAPIを叩いてデータ分析基盤から自動連携しています。Cloud Runにデプロイして利用しています。

各テーブルの役割情報やカラム情報などは、自分達で記入していきます。

f:id:JesseTetsuya:20211223155013p:plain

f:id:JesseTetsuya:20211223155024p:plain

ログイン数などの統計情報もいい感じにカスタマイズしやすいコードになっています。 しかし、Redashなどのダッシュボードで見てしまうため、あまりつかっていません。

メタデータマネジメントシステムをつかった課題解決

コンテンツマネジメントシステムをつかった課題解決は、以下です。

課題

  • データ分析基盤を開発する前にそもそもの現状のRDBのテーブル・カラム情報が整理されていなかったので、一人で調べたり、各機能の担当者に話しを聞いて情報をかき集めてエクセルにまとめていました。ただただ、それがつらかった。

f:id:JesseTetsuya:20211223155059p:plain

解決

  • データ分析基盤との自動連携を実装することで自分でテーブル名やカラム名やデータ型から記入する必要はなくなりました。検索性やユーザビリティは、あがりました。

開発工数

  • オープンソースなのでアプリケーション自体は、無料.

  • アプリケーション実装に2, 3人日、インフラ構築に2週間ぐらい。

その他役立ったこと

  • 担当以外のデータをデータ分析基盤でみるときや、ダッシュボード作成の際のクエリを書く時にどんなテーブル情報やカラム情報があって、どんな意味があるのかを理解するのに役立つようになった。

  • そもそも、どんなデータがあるのかわからないという人にとって理解の手助けになった。

テーブル情報やカラム情報は、都度入力していく必要があります。

その施策として、もくもく会を開催したり、チームの定例で10分だけもくもくと手を動かす時間をもうけていきました。

進め方は、meetのブレイクアウトセッションを利用して3,4人のグループに分けて、お互に情報をかけ集めながらやっていきます。

9人でもくもく記入していくと10分で大体一つのデータマートの50%ぐらいのテーブル情報とカラム情報は、記入しきることができます。

このチームでは、3つのデータウェアハウス含めたデータセットのテーブル情報とカラム情報を埋めていきました。一つのデータセットあたり20 - 30ぐらいのテーブルがあります。

これら全て埋めるのに、大体10分のもくもく会を5,6回ほどやると必要なテーブル情報とカラム情報をうめることができました

エクセルに私一人で記入している時は、カラム情報まで記入できず、なおかつ2つのデータセットのテーブル情報まで記入するのが限界でした。

まとめ

アプリケーション実装よりもインフラ構築の方が工数がかかっています。

これは、当時の私のGKEとCloud Runの仕様の知識不足も影響しているかと思いますが、全体としても早く安く作れたCRUDアプリケーションなのではないかと思います。

また、世の中には、既製のCMSやメタデータマネジメントシステムのOSSは多くあるかと思いますが、機能が多すぎるとか、クリティカルな制約があったりするものです。必要最低限の機能をさくっと安く作るには、こういったフレームワークは最適です。

FlaskのみでWEBアプリケーションを開発してみたいという方は、2022年1月24日発売の「Python FlaskによるWebアプリ開発入門 物体検知アプリ&機械学習APIの作り方 」という私が執筆した書籍の一読をおすすめします。

www.amazon.co.jp

また、Classiでは、Pythonエンジニア募集中です。ご興味ある方は、下記のリンクからの応募をお待ちしております。

hrmos.co

ISUCON11予選課題の27万点まで練習し新人エンジニアが学んだこと

この記事は Classi developers Advent Calendar 2021 の23日目の記事です。

こんにちは、プロダクト開発部の2年目の@minhquang4334です。 今年の8月に、同じ部で3年目の@henchiyb 先輩と一緒に yasuoチームを作り、ISUCON11 オンライン予選に初めて参加しました。参加するきっかけは弊社に業務委託として来てくださっている@soudaiさんからISUCONの話について聞かれて、面白そうなので、チャレンジしてみました。結果はRubyで4万点まで達成できましたが、全体のチームの100/598 位ぐらいで敗退してしまいました。

オンライン予選が終わった後、数百万点を達成したチームはどうやってそこまで出来たのかとずっと疑問でした。各チームの解説ブログを見てみましたが、目を通しただけですぐ忘れてしまい、知見を深く理解できないと思いました。それで、自分でももっと深くやってみたいという気持ちになったので、ISUCON 運営の方が準備してくれた環境を立ち上げて練習しました。再度の結果はRubyで、27万点 (11/598 位の点数相当) まで伸ばせました。

ISUCON11予選の課題の練習を通じて、Webパフォーマンスチューニングを中心に、いろいろ勉強になりました。その知見は新人エンジニアの時とISUCONに初めて参加する時に早めに知っておくと良さそうだと思ったので、この記事ではそんなエンジニアを対象にISUCON11予選課題の解説を含めて、自分なりの学んだことをまとめて共有します。

前提

推測ではなく測定しよう

パフォーマンスチューニングするとき、どのように進めていますか。今までは、私はよくコードを見て、N+1や不要なループなどといったパフォーマンスの悪いコードを気づいたら、すぐ改善しようと思いました。しかし、ISUCONでそのように時々たくさん改善できても、システムのパフォーマンスが上がらなく、点数もほぼ変わっていません。 理由は解決した問題はシステムのパフォーマンス低下の原因ではないからです。それも、問題を特定するために、システムのメトリクスを見ておらず、自分の推測を信じたからです。 例えば、以下のAPIの実行時間フレームグラフの場合、どれほど 不要なループとN+1を改善しても、そのAPI自体における改善の効果は少ないのではないでしょうか。不要なループとN+1は良くない実装ですが、パフォーマンス低下の原因ではありません。

f:id:minhquang4334:20211222190216p:plain
例:リクエストの実行時間

正しい問題を調べるのがすごく大事で、問題解決のアプローチを変更しなくてはいけません。推測の代わりに、システム運用のメトリクスを監視したり、測定したり、異常なものを問題として抽出したりすることです。そうして適切な問題を取り除き、解決してから、再度その測定・改善プロセスを回します。

ISUCON11予選練習の時に、その測定・改善プロセスを回してみると、徐々に点数が上がってきて、やりながら達成感も感じられました。

測定した例です。

  • ALPを使って、リクエストごとに実行時間を測定する
+-------+--------+------------------------------+-----+------+-----+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+--------------+-----------+
| COUNT | METHOD |             URI              | 1XX | 2XX  | 3XX |  4XX  | 5XX |  MIN  |  MAX  |   SUM    |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |  SUM(BODY)   | AVG(BODY) |
+-------+--------+------------------------------+-----+------+-----+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+--------------+-----------+
| 32656 | POST   | /api/condition/*             |   0 | 7510 |   0 | 25146 |   0 | 0.004 | 0.320 | 3067.503 | 0.094 | 0.060 | 0.100 | 0.100 |  0.018 |     0.000 |     14.000 |       28.000 |     0.001 |
|  2688 | GET    | /isu/*                       |   0 | 2561 |   0 |   127 |   0 | 0.048 | 0.956 |  549.019 | 0.204 | 0.000 | 0.472 | 0.112 |  0.198 |     0.000 | 135259.000 | 42280309.000 | 15729.282 |
|   639 | GET    | /api/trend                   |   0 |  614 |   0 |    25 |   0 | 0.024 | 1.116 |  397.817 | 0.623 | 0.068 | 0.540 | 0.700 |  0.195 |     0.000 |   4637.000 |  2749921.000 |  4303.476 |
+-------+--------+------------------------------+-----+------+-----+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+--------------+-----------+
Count: 68432  Time=0.00s (28s)  Lock=0.00s (0s)  Rows_sent=46.7 (3194156), Rows_examined=397.0 (27169268), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY timestamp DESC

Count: 1089  Time=0.00s (2s)  Lock=0.00s (0s)  Rows_sent=0.0 (0), Rows_examined=0.0 (0), Rows_affected=0.0 (0), isucon[isucon]@localhost
  COMMIT

Count: 1152  Time=0.00s (1s)  Lock=0.00s (0s)  Rows_sent=1.0 (1125), Rows_examined=2725.9 (3140278), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY `timestamp` DESC LIMIT N

Count: 21800  Time=0.00s (1s)  Lock=0.00s (0s)  Rows_sent=3.1 (68432), Rows_examined=0.4 (8204), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu` WHERE `character` = 'S'

Count: 3377  Time=0.00s (0s)  Lock=0.00s (0s)  Rows_sent=0.0 (0), Rows_examined=0.0 (0), Rows_affected=1.0 (3377), isucon[isucon]@localhost
  INSERT INTO `isu_condition` (`jia_isu_uuid`, `timestamp`, `is_sitting`, `condition`, `message`) VALUES ('S', 'S', TRUE, 'S', 'S')
  • top コマンドでCPU 利用率とメモリ利用率を測定する
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   2244 isucon    20   0  784600  74604  12984 S 110.6   2.0   0:24.22 bundle
   2077 mysql     20   0 1714832 109468  18660 S  25.9   2.9   0:29.95 mysqld
   2236 www-data  20   0   13792  10416   5036 S   6.6   0.3   0:01.92 nginx
   2235 www-data  20   0   11428   7992   5036 R   3.3   0.2   0:00.90 nginx

もしDatadogやNew Relicなどが活用できたら、測定がより楽になると思います。

データベースについて学んだこと

Webシステムの世界ではデータベース がパフォーマンス低下の原因となるケースは少なくないようです。それで、ソフトウェアエンジニアとして、データベースの正しい扱い方を学ぶのは損ではないと思います。

LIMIT句が大事だ

You aren't gonna need it https://ja.wikipedia.org/wiki/YAGNI

実際に必要となるまでは追加しないのがよいというプログラミング原則があります。そのような考え方でデータベースからデータを取得する時にも、必要であるデータのみを取得するのが大事です。 LIMIT 句を付けたら、 SELECT 文を実行した時に取得するデータの行数の上限を設定することができます。LIMIT 句を付けないと、無駄なデータを取得し、SQLが実行される時間のほとんどがDisk I/Oになってしまって、CPU利用率なども上がって、システムのボトルネックになるケースが多いです。 ISUCON 11で不要なデータを取得しないようにLIMIT 句を付けたら結構パフォーマンスが上がって、点数も急増できました。正に小さい変更で大きな効果です。

  • LIMIT句を付ける前 MySQL Slow Log
Count: 75480  Time=0.00s (2s)  Lock=0.00s (0s)  Rows_sent=46.9 (3537598), Rows_examined=3.0 (228652), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY timestamp DESC
  • LIMIT句を付ける後 MySQL Slow Log
Count: 226516  Time=0.00s (8s)  Lock=0.00s (1s)  Rows_sent=1.0 (223303), Rows_examined=0.1 (29024), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY timestamp DESC LIMIT N

LIMIT句を付ける後、Rows_sent と Rows_examined の数値が大きく減らすことができました。見なきゃいけないデータが限定されて、データベースの負荷も大きく減らせます。以下は実装・測定の参考となります。

Auto Increment Primary Key を使うべきか

Primary Key カラムにAUTO_INCREMENT をつけると、データを追加した時にカラムに対して現在格納されている最大の数値に 1 を追加した数値を自動で格納することができます。カラムに連続した数値を自動で格納したい場合に便利ですが、パフォーマンス的に問題ないでしょうか。MySQLの前提で考えてみましょう。

MySQLでB-treeインデックスの構造とInnoDBストレージエンジンを使われています。B-treeは以下の写真のような木のデータ構造で、テーブルのIndex (Primary Keyも一つのIndex) をテーブルのデータと別途に管理するものです。テーブルに一つのレコードを追加したら、テーブルにあるIndexのB-treeにも一つのノードが追加されます。B-treeの詳しい解説はこちらをご覧ください。

f:id:minhquang4334:20211222190032p:plain
B-tree インデックス構造

B-treeにノードが追加されるときに、どこで保存できるか既存のノードからそれぞれ比較します。それで、Primary KeyにAUTO_INCREMENTをつけたら、Indexは連続した数値なので、すぐ保存できるポジション (一番右側の下)を見つけられます。その特徴からAUTO INCREMENT Primary Keyを使ったら、Insert文が早く大量のInsert文によって負荷が高くなるシステム (write-heavyシステム) に相応しいと思っている方は多いのではないでしょうか。 しかし、InnoDBではACID特性)を守るため、AUTO_INCREMENT ロックモードがあります。それはテーブルにレコードを書き込むときに、AUTO_INCREMENTのロックをとって、他の書き込むトランザクションが待機させるという仕組みです。

f:id:minhquang4334:20211222190307p:plain
Mysql AUTO_INCREMENT ロックの例

上記の写真のように、同時に複数のトランザクションからAUTO INCREMENT Primary KeyがあるテーブルにInsert文が発行されたら、T1が終わるまで、T2がロックされます。T1が終わったら、T2でロックが 解放されて、T2はAUTO_INCREMENTロックを取得できて、Insert文が発行できます。そのため、大量のInsert文の負荷があるシステムなら、AUTO INCREMENT Primary Keyを使う場合、副作用が出てくる可能性があり、パフォーマンス低下の原因の一つになります。

ISUCON11でIOTシステムみたいな時系列のデータを収集するシステムなので、AUTO INCREMENT Primary Keyのテーブルに同時に大量のInsert文が発行されました。そのテーブルのPrimary KeyをCompound Keyに変更したら、結構パフォーマンスが改善できました。

システムアーキテクチャについて学んだこと

一個一個のAPIやリクエストやクエリなどを改善するというより、システム全体の構成に問題があるのか特定して解決できたら、ISUCONだけではなく実際にシステムのパフォーマンスを最適化するうえで大きなカギになると思います。

コンテンツ配信はNGINXに肩代わりさせる

普通のWeb システムのクライアントに静的なファイルを配信する方法は root/ フォルダーにファイルを置いておき、必要な場合はバックエンドがユーザーの認証やファイルを読み取りをしてから、Webサーバーが配信します。 Rubyの場合は、send_fileというメソッドがよく使われています。

railsdoc.com

指定したパスに存在する画像やファイルを読み込み、その内容をクライアントに送信

send_fileの動きが気になったことはありませんか。これは、ディスクからファイルを読み取る必要があることです。このファイルは、出力バッファーを通過し、Webサーバーにフラッシュされ、クライアントに配信します。大きいファイルの場合、メモリをたくさん使って、memory_limitを超えたケースもあります。ファイル全体をメモリに保存したり、削除したりするプロセスのため、バックエンドが忙しくなって、他のリクエストを処理できなくなります。

そこで、バックエンドの負荷を減らすために、X-Accel-Redirectを使って、認証のみをバックエンドで行い、コンテンツ配信はNginxに肩代わりさせることができます。X-Accel-RedirectはNginxの機能で、バックエンドから返されたヘッダによって決定される場所への内部的なリダイレクトを許可します。それでバックエンドを解放して他のリクエストを処理でき、Nginxが持つ素晴らしいコンテンツ配信の力で、高速配信を実現できます。

f:id:minhquang4334:20211222190559p:plain
X-Accel-Redirectを使った場合

ISUCON11予選を練習するときに、その機能は全く知らず、解説しているブログなどをいくつも読むことで勉強になりました。以下のようにnginx設定ファイルに設定しました。

https://github.com/minhquang4334/isucon11-training/blob/master/isucondition.conf#L64-L68

    location /icon {
        internal;
        alias /home/isucon/tmp; 
        expires 86400s;
    }

location /iconinternalを追加したら、X-Accel-Redirect レスポンスヘッダーを返すことでnginxから静的ファイルをクライアントに配信できます。

リバースプロキシキャッシュを用意する

キャッシングはWebパフォーマンスの最適化最適化におけるポピュラーな手法の一つです。現代のWebシステムにキャッシングシステムはなくてはならない存在になったといった印象もあります。それを用意することで、Webサーバとかデータベースなどの負荷が減らすことができます。 リバースプロキシキャッシュはキャッシングの一つのタイプであり、Webサーバのクライアントの間に置いておくサーバです。その中で、Varnishは一番よく使われているものです。

f:id:minhquang4334:20211222190744p:plain
Varnishはリバースプロキシキャッシュサーバーである

上記の図を使って、Varnishといったリバースプロキシキャッシュを簡単に説明します。

  • 一度目にクライアントからリクエストを送ったら、VarnishはWebサーバにフォワードします。Webサーバはそのリクエストを処理して、データベースや外部APIと接続してから、レスポンスを生成します。そのレスポンス自体はVarnishのキャッシュに保存されて、クライアントに返します。
  • 2度目以降でクライアントからリクエストを受けたら、すでにレスポンスはVarnishのキャッシュにあるので、すぐ返します。
  • Varnishのキャッシュの有効期限も設定でき、有効期限を超えたら、キャッシュにあるレスポンスは無有効に設定されます。

そのフローでバックエンドコードを変更せずに、負荷が凄く減らせます。この仕組みは特に認証とリアルタイムも必要なくページやデータなどに相応しいと思います。

ISUCON11予選の課題では、ユーザーがログインせずサマリの画面をリロードする度にapi/trendが呼ばれて、かなり重いクエリが発行されてしまいます。そのページが遅くて、ユーザーがサイトに興味がなくなるというビジネスの要件もあります。そこで解説ブログを参考してみてから、Varnishをインストールして0.5sの有効キャッシュ応答をするように変更しました。

  • Varnishを活用する前の該当リクエストのALP
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| COUNT | METHOD |             URI              | 1XX | 2XX  | 3XX  |  4XX  | 5XX |  MIN  |  MAX  |   SUM    |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |   SUM(BODY)   | AVG(BODY) |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
|  4499 | GET    | /api/trend                   |   0 | 4482 |    0 |    17 |   0 | 0.004 | 0.180 |  542.084 | 0.120 | 0.080 | 0.116 | 0.124 |  0.015 |     0.000 |   5870.000 |  25317465.000 |  5627.354 |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
  • Varnishを活用する後の該当リクエストのALP
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| COUNT | METHOD |             URI              | 1XX | 2XX  | 3XX  |  4XX  | 5XX |  MIN  |  MAX  |   SUM    |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |   SUM(BODY)   | AVG(BODY) |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| 24461 | GET    | /api/trend                   |   0 | 24454 |   0 |     7 |   0 | 0.000 | 0.020 |    6.500 | 0.000 | 0.000 | 0.000 | 0.000 |  0.001 |      0.000 |   8478.000 | 159572757.000 |   6523.558 |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+

Varnishを使った後、AVG, P1, P50, P99のレスポンスタイムはほぼ0sぐらいになりました。バックエンドにほぼ手を入れずに、簡単な修正で、信じられないパフォーマンス最適化が出来て、すごく感動しました。今回、Varnishを導入するのが以下となります。

location ~ ^/api/trend$ {
    proxy_set_header Host $http_host;
    proxy_set_header Connection "";
    proxy_http_version 1.1;
    proxy_pass http://varnish;
}
sub vcl_backend_response {
    # Happens after we have read the response headers from the backend.
    #
    # Here you clean the response headers, removing silly Set-Cookie headers
    # and other mistakes your backend does.
    set beresp.ttl = 0.5s;
    set beresp.grace = 0.2s;
}

課題の特徴から適切な構成を設計する

ISUCON11予選の課題の特徴として、時系列のデータでありユーザーが増加することにより同時に大量のレコードをデータベースに書き込むといった特性上、どうしてもデータベースの負荷が高くなりパフォーマンスが低下してしまいます。そこで、リクエストごとにデータベースに書き込まずに、書き込むデータをキャッシューに入れ、Scheduled Jobを起動し、定期にキャッシュから取得したデータを一括データベースに書き込むように修正しました。その修正をした後、点数は136226から222174まで急増できました。以下のリンクは今回の修正と測定となります。 https://github.com/minhquang4334/isucon11-training/issues/1#issuecomment-944838242

こちらの改善はパフォーマンスの観点で一番効果が高いと感じています。解決したい課題の特徴を把握して、適切な構成を設計して、実装するのが一番大事かと勉強になりました。それは最初から書いた通りに、正しい課題を解決することだと思います。

終わりに

今回の練習を通じて、Webシステムパフォーマンスチューニングを含めて技術の課題解決についていろいろ勉強になりました。目を通しただけで忘れてしまい、実際にやってみた方がより深く理解できてきたと思っています。その知見を生かして、これからClassiでの業務にも頑張っていきたいです。それとも、今年の予選では敗退してしまいましたが、来年の予選までちゃんと練習して、本選まで挑戦していきたいと思います。💪 💪

自分自身の学んだことや理解したことをこの記事にまとめました。理解が不足している部分もあるかと思います。お気づきの際はぜひコメントしていただきたいです。

明日のClassi developers Advent Calendar 2021の担当は平田哲也さんです。お楽しみに。

© 2020 Classi Corp.