Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営する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の担当は平田哲也さんです。お楽しみに。

トピックモデルを使って問い合わせ内容を分析した話

この記事はClassi developers Advent Calendar 2021の18日目の記事です。
昨日は基盤インフラチームのめるさんによる「バックエンドエンジニアが基盤インフラチームに異動して半年ほど経った話」でした。

こんにちは、データAI部でデータサイエンティストをしている高木です。
弊社では顧客である先生、生徒、保護者からClassiの機能や契約に関する問い合わせを日々頂いております。
これらの問い合わせの内容を分析し、Classiの現状の課題や今後解決していくための施策などを社内で検討しています。

今回は問い合わせ内容を言語処理技術の一つであるトピックモデルを使って分析した内容についてご紹介します。

なぜ分析する必要があったのか?

Classiへの問い合わせやその対応の内容は、担当者によってテキスト化された状態で管理されています。
弊社のカスタマーサポート・カスタマーサクセスチームは、これらの内容を基に次年度の施策を検討したり、実施した施策がどうだったかという効果を分析しています。

年度が変わる3月〜4月での問い合わせが増加傾向にあり、その中でも学校全体の利用に関わる設定・登録の問い合わせが最も多くなっています。
毎年、これらの問い合わせを目視でチェック・分類し、施策を検討している状態だったのですが、作業には膨大な時間と労力がかかってしまいます。

そこで、ある程度自動的に分析・分類できないかという必要性が出てきており、今回トピックモデルを使った分析を試みました。

トピックモデルとは?なぜ使ったのか?

トピックモデルは、文書集合をそれらに含まれる単語の共起性から、どんなトピック(話題や分野など、大体の「意味」のようなもの)が存在するのか、各文書はどのトピックについてのものか、を自動で推定することができます。

これまで様々な手法が提案されており、文書の検索や分類、また、音楽の歌詞探索*1やゲームのデッキアーキタイプの抽出*2など、様々な分野へ応用されています。
私もテスト問題に応用した研究をしています*3

以下は代表的なトピックモデルです。

トピックモデル 特徴 参考文献 参考コード
LSI: Latent Semantic Indexing ・特異値分解による文章の圧縮
・1文書に1トピック
Deerwester et al (1990)*4 Gensim
PLSI: Probabilistic Latent Semantic Indexing ・LSIの確率モデル化
・1文書に複数トピック
Hofmann (1999)*5 PyPI
LDA: Latent Dirichlet Allocation ・PLSIのベイズ化
・新規文書のトピック推定が可能
Blei et al (2003)*6 Gensim
HDP: Hierechical Dirichlet Process ・LDAのノンパラメトリック化
・トピック数が自動決定可能
Teh et al (2006)*7 Gensim
DTM: Dynamic Topic Model ・時系列データへの適用 Blei et al (2006)*8 Gensim
BTM: Biterm Topic Model ・短い文章への適用 Yan et al (2013)*9 GitHub

一方で、問い合わせの内容にはClassiの機能や契約に関する複数の質問が同時に含まれることがあります。
そして、その内容は時期によっても変化します。

そこで今回は、(1)自動で手軽に内容を把握できる、(2)同一の問い合わせから複数のトピックを抽出することができる、(3)内容の時系列変化を追うことができる、 という点を考慮しDTM(Dynamic Topic Model)を使用しました。

下図はDTMによるトピックの推定と時系列変化の例です。
時刻tにおける、ある文書集合のトピックに出現する単語(横軸)と、それらの単語がトピックに出現する確率(縦軸)を表しています。
また、各文書でそれぞれのトピックが出現する確率も推定されます。

f:id:ttakagi1021:20211216200222p:plain
DTMによるトピックの推定と時系列変化のイメージ

DTMによる問い合わせ内容の分析

概要

2020年と2021年の3月〜4月に寄せられた設定・登録についての問い合わせをトピック分析し、内容の変化を考察しました。
DTMを使ったトピック分析の手順と目的は以下になります。
なお、すべての分析はPythonで行いました。

  1. DTMによるトピックの推定
    • 2020年と2021年のそれぞれの問い合わせのトピックとその時系列変化を推定するため
  2. トピックへのラベル付け
    • トピックの内容を把握しやすくするため
  3. トピックごとの問い合わせ件数を集計
    • どのトピックの問い合わせが多いか把握するため
  4. 2020年と2021年のトピック間類似度の計算
    • 類似度の低いトピック(各年の特有なトピック)を抽出するため
  5. ダッシュボードの作成
    • 手順1〜4の結果を可視化するため

分析手順の詳細

1. DTMによるトピックの推定

DTMによるトピックの推定は、Gensimのmodels.ldaseqmodelというライブラリを使用しました。
トピックの推定で使用するメソッドと入力パラメータは以下になります。

dtm_model = LdaSeqModel(corpus, id2word, time_slice, num_topics)
  • corpus:コーパス
    • 文書毎の単語IDとその出現回数を持つタプルリスト
  • id2word:辞書
    • 単語とそれを一意に識別するIDの辞書
  • time_slice:タイムスライス
    • 指定した「期間」ごとの文書数のリスト
  • num_topics:トピック数
    • トピックの数は可変

辞書やコーパスの詳細については省略しますが、これらを作成する際に重要となる単語の抽出処理について後述します。

タイムスライスとは、トピックの変化を指定した「期間」で分割するためのもので、今回はその「期間」を月曜日〜日曜日の1週間ごとにしました。
週毎の問い合わせ件数が以下の場合、time_slice = [3, 5, 1]となります。

  • 2021/03/01(月)〜03/07(日):3件
  • 2021/03/08(月)〜03/14(日):5件
  • 2021/03/15(月)〜03/21(日):1件

さらに、トピック数は分析や分類したい粒度によって手動で設定する必要があります。
今回は以下の理由で両年ともトピック数を10としました。

  • 2020年の問い合わせを調査した際、約10個の重要なカテゴリに分類された
  • 多すぎるとトピックの意味が細かく分散しチェックするのが大変

単語の抽出処理について

トピックモデルではトピックの推定精度を向上させるために、関連のある単語の共起性を高めることが重要になってきます。
一方で、日本語には次のような性質があります。

単名詞Nが対象分野の重要な概念を表しているなら、書き手はNを頻繁に単独で使うのみならず、新規な概念を表す表現としてNを含む複合名詞を作りだすことも多い。*10

この「単名詞N」と「単名詞Nを含む複合名詞」は関連する概念を表していることが多いです。

例えば、Classiのコミュニケーション機能である「校内グループ」では、「グループ」という単語を中心に、「自動作成グループ」、「任意作成グループ」などの機能が存在し、同一の問い合わせ内で出現することが多いです。

この例の場合、「単名詞N」と「単名詞Nを含む複合名詞」は以下になります。

  • 単名詞:校内、グループ、自動、作成、任意
  • 複合名詞:校内グループ、自動作成グループ、任意作成グループ

これらの単語はすべて関連しており、同一のトピックに分類されることが望まれるため、このような単名詞複合名詞をすべて抽出するようにしました。
抽出処理には、形態素解析器のMeCabと形態素解析器の結果を基に複合名詞を抽出することが可能なTermExtractを使用しています。

2. DTMによるトピックの推定

手順1で作成されたdtm_modelにより、特定の期間のトピックに出現する単語と出現確率をリストで出力することができます。
そのメソッドと入力パラメータは以下になります。

dtm_model.print_topic(topic, time, top_terms)
  • topic
    • トピックのID(トピック数10の場合0~9)
  • time
    • タイムスライスで設定した「期間」の順番
  • top_terms
    • 出力する単語の数

このメソッドによる出力結果を用いて、トピックをワードクラウドで可視化しました。
そして、比較的大きく表された単語を基にラベルを付与しました。

最初の週(time=0)のトピックのワードクラウドは以下になります。

f:id:ttakagi1021:20211216184956p:plain
2020年3月 第1週のワードクラウド

f:id:ttakagi1021:20211216185035p:plain
2021年3月第1週のワードクラウド

3. トピックごとの問い合わせ件数集計

dtm_modelでは、問い合わせごとにトピックの出現確率も出力することができます。
そのメソッドと入力パラメータは以下になります。

dtm_model.doc_topics(doc_number)
  • doc_number
    • コーパスや辞書を作成する際に入力した文書の順番

問い合わせごとに最も出現確率の高いトピックをその問い合わせにおけるトピックとし、トピックごとの問い合わせ件数を集計しました。
以下の表は件数の多い順にトピックを並べた結果になります。

2020年3月〜4月

トピックNo ラベル
2 年度更新の各ステップ作業
10 生徒情報の登録
1 生徒や先生の情報登録, ExcelファイルのUPL/DL
6 IDとPW, ログイン, 招待コード, 無償提供
4 管理責任者, 管理者の権限, 生徒カルテ
3 校内グループの設定や配信, 授業の登録
7 学習動画, 学習記録, ダミー生徒
9 継続, 利用停止, 保護者
5 識別番号, 留年や進学した生徒の登録, 人数確定
8 模試データ連携

2021年3月〜4月

トピックNo ラベル
2 生徒情報の登録, ExcelファイルのUPL/DL
3 年度更新
5 年度更新
4 IDとPW, ログイン, 招待コード
8 模試データ連携
7 利用停止, 学習動画パック
6 校内グループ, アンケート
1 管理責任者, 学習動画, 学習記録, 閲覧権限
10 授業登録, 先生登録, 帳票登録
9 人数確定

4. トピック間類似度の計算

手順2で使用したdtm_model.print_topicを用いて、トピックに含まれる単語とその出現確率を50個出力し、2020年と2021年のトピック間の類似度をコサイン類似度で計算しました。

下図はトピック間の類似度をヒートマップによって可視化した結果です。
行が2020年、列が2021年のトピックを示しており、それぞれの類似度の値と値が大きいほど色が濃く表示されています。

例えば、2021年のtopic9は同年の他トピックに比べ2020年のトピックとの類似度がすべて0.5未満と低くなっています。
このことから、topic9は2021年特有なトピックだということが分かります。

f:id:ttakagi1021:20211216190648p:plain
2020年(行)と2021年(列)のトピック間類似度のヒートマップ

5. ダッシュボードによる可視化

手順1〜4の結果をTableauのダッシュボードで可視化しました。

下図は2021年のダッシュボードです。
このダッシュボードで分かることは以下になります。

  • 各年度のトピックを表す単語やその出現確率(図中①)
  • トピックごとの単語の週次変化(図中②)
  • 各トピックに該当する問い合わせ内容(図中③)
  • トピックごとの問い合わせ件数(図中④)
  • トピック間の類似度(手順4のヒートマップを別ページで作成)

f:id:ttakagi1021:20211216201758p:plain
2021年3月〜4月のトピック分析ダッシュボード

内容変化の考察

以上の分析結果を基に、2020年と2021年の内容の変化を考察しました。
ページの都合上詳細は省きますが、いくつか興味深い結果を以下で紹介します。

「年度更新」における各ステップの問い合わせの減少

手順3の問い合わせ件数の多いトピックを見ると、両年ともに次年度のユーザー情報を更新・登録する「年度更新」や「情報登録」についてのトピックが上位3位までに入っています。

2020年はこれらのトピックで「ステップ」や「STEP」という単語を含む問い合わせが多かったのですが、2021年ではそれらの単語を含む問い合わせが減少していました。

この理由としては、2020年から年度更新の手順をステップごとに説明したガイドが作成されたため、ステップごとに問い合わせをする人が増えたと考えられます。

そして、2021年はこれらのガイドを基にした作業を初めて行う先生の数が減少した(作業を理解している先生が増加した)ため、これらの単語を含む問い合わせが減少したと考えられます。

下図は2020年3月〜4月の「年度更新」に関するトピックの推移です。

f:id:ttakagi1021:20211216201916p:plain
2020年の「年度更新」に関するトピックの推移

「利用生徒人数の登録確定」についての問い合わせの増加

手順4で述べたように、2021年のtopic9は同年の他トピックに比べ2020年のトピックとの類似度が低くなっており、2021年特有なトピックとなっています。

このトピックは利用生徒人数の登録確定に関するもので、その作業方法や締切についての問い合わせが多かったです。

この理由としては、2021年は4月に入ってからこれらの確定作業を完了するよう各学校への連絡が増えていました。
その結果、これらの確定作業に関連する問い合わせが増えたと考えられます。

下図は2021年3月〜4月の利用生徒人数の登録確定に関するトピックの推移になります。
4月以降でこのトピックに関連する単語の出現確率が上昇傾向になっているのが分かります。

f:id:ttakagi1021:20211216202022p:plain
2021年の「利用生徒人数の登録確定」に関するトピックの推移

最後に

今回の分析結果から、現状のClassiの設定・登録に関する課題や施策の効果などを考察することができました。
この結果をチームを超えて見ることによって、今年の施策はどうだったのか、今後どういう施策を打っていくべきかを考えることができます。

例えば、カスタマーサポートチームでは今回の分析で、生徒人数の登録確定や年度更新において各設定の期日が分かりづらいことや、年度更新のSTEP3の問い合わせが多いことが明らかになったため、これらの問い合わせを10%減らすことを目標に改善活動を進めています。

また、PMM(プロダクトマーケティングマネージャー)チームでは顧客課題をプロダクトへ反映するために、今回の分析手順をユーザーから寄せられる機能改善についての要望に応用し、その内容の深堀りや要望を実現するための開発の優先順位付けを検討しています。

このように、今回の取り組みが各チームで進められている施策に少しでも寄与できていることは、データAI部としても嬉しい限りです。
分析結果を毎回真摯に聞いて下さりご意見下さる各チームの皆様にはとても感謝しています。

現在、10月までの問い合わせ内容の分析が完了しており、今後は来年の2月までの問い合わせ内容の分析を実施し、年間の問い合わせの傾向を整理していく予定です。

さらに、来年度以降の問い合わせ内容の継続的な分析や、それらの結果を各チームへ素早く還元できる仕組みを構築していきたいと考えています。

明日のClassi developers Advent Calendar 2021の担当は横田さんです。
よろしくお願いします。

*1:Lyric Jumper: https://lyric-jumper.petitlyrics.com

*2:『逆転オセロニア 』における、機械学習モデルを用いたデッキのアーキタイプ抽出とゲーム運用への活用: https://www.slideshare.net/RyoAdachi/deck-archetype-extraction-cedec2019

*3:高木輝彦,高木正則,勅使河原可海,田中健次: e テスティングにおけるLDAを用いた項目間類似度の算出, 情報処理学会論文誌, Vol.55, No.1, pp.91 - 104, 2014.

*4:Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K. and Harshman, R.: Indexing by Latent Semantic Analysis, Vol. 41, No. 6, pp. 391–407 (1990).

*5:Hofmann, T.: Probabilistic latent semantic indexing, Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval, SIGIR ’99, New York, NY, USA, pp. 50–57 (1999).

*6:Blei, D. M., Ng, A. Y. and Jordan, M. I.: Latent dirichlet allocation, J. Mach. Learn. Res., Vol. 3, pp. 993–1022 (2003).

*7:Teh, Y. W., Jordan, M. I., Beal, M. J. and Blei, D. M.: Hierarchical Dirichlet Processes, Journal of the American Statistical Association, Vol. 101, pp. 1566–1581(2006).

*8:Blei D. M. and Lafferty J. D.: Dynamic topic models, Proceedings of the 23rd international conference on Machine learning, pp. 113-120 (2006).

*9:Yan, X., Guo, J., Lan, Y. and Cheng, X.: A biterm topic model for short texts, Proceedings of the 22nd international conference on World Wide Web, pp. 1445–1456 (2013).

*10:中川裕志,湯本紘彰,森 辰則:出現頻度と連接頻度に基づく専門用語抽出,自然言語処理,Vol. 10, No. 1, pp. 27–45 (2003).

UIKitでDesign Systemを実装する

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

はじめまして。小中事業開発部でモバイルアプリエンジニアをしています拜郷です。
今回は新規開発中サービスのiOSアプリでDesign System1を実装するにあたって考えたことを書いていきます。

Design System導入の背景

小中事業開発部では現在新規サービス tetoru(テトル) のリリースに向けてチーム開発を行なっています。
開発を進めていく中でUXDチームからDesign System導入の検討がされました。
導入の背景としては開発メンバーが増員するタイミングだったのと、デザインツールの移行が決まったのが大きな要因としてありました。
Design Systemはプロダクト、チームそれぞれの視点で以下の目的を実現できると考えています。

  • プロダクトを通してユーザに一貫性のある体験を提供できる
  • チーム内でデザイン原則を共通認識として持つことができる
  • メンテナビリティとスケーラビリティの向上

iOSアプリ開発の背景

iOSアプリは開発初期の段階からUIKitおよびInterface Builder(以下IB)を扱うStoryboard, xibを使用して開発を行なっていました。
Design System導入のためにIBでのレイアウトをやめたりSwiftUIに変更するのは諸々の事情により現実的ではなかったため、前提として開発方針は変えずDesign Systemの実装検討を進めることになりました。
ただしiOSアプリ開発においてUIKitおよびIBはDesign Systemの実装(主にUIコンポーネントの再利用)に適しているとは言えず少々困難です。
以下ではDesign SystemをUIKitで実装するにあたって考えたことを具体例を交えて紹介していきます。
(※今回紹介するソースコードはすべて本記事用のサンプルコードです。)

デザイントークンの実装

まずDesign Systemにおける最小単位であるデザイントークンの実装を考えます。 デザイントークンにはカラー、余白、行間、Elevation(高さ)、タイポグラフィ、シャドウ、アニメーションなど複数のコンポーネントにまたいで使用される情報を定義します。

カラー

カラーの定義はAsset Catalogを使用します。これはXcode9およびiOS11から使える標準機能なので現時点では特に悩まず使えます。
定義したカラーをコード内で使用する場合、UIColorクラスの初期値にAsset Catalogで定義した文字列を指定します。
ここでSwiftGenR.Swiftを使用することでタイプセーフにリソースを扱えるようになり、タイプミスなどで実行時エラーが発生しなくなる等のメリットがあります。
またAsset CatalogのColorはライト/ダークの各モードをそれぞれ定義することができるため、デザイントークンのカラー定義を双方で検討しておくことでダークモード対応が容易に行えると思います。

// 通常コードからカラーを取得する
UIColor(named: "primary")
// SwiftGenを使用する
Asset.Color.primary.color
// R.Swiftを使用する
R.color.primary()

タイポグラフィ

タイポグラフィの定義はUIFontをextensionして使用することにしました。
SystemFontを使用する場合は単純にトークン名に紐づいたサイズとウェイトを指定して定義します。
カスタムフォントを使用する場合はサイズ、ウェイトに加えフォントファミリーを指定します。ここでもSwiftGenやR.Swiftを使用することでタイプセーフにリソースを扱えます。

public extension UIFont {
    static let title: UIFont = .systemFont(ofSize: 44.0, weight: .bold)
    static let body: UIFont = .systemFont(ofSize: 14.0, weight: .bold)
    static let caption: UIFont = .italicSystemFont(ofSize: 11.0)
    static let button: UIFont = .systemFont(ofSize: 14.0, weight: .regular)
    static let swiftGenSample: UIFont = FontFamily.mplus1.regular.font(size: 14.0)  // SwiftGenでカスタムフォントを指定する
    static let rswiftSample: UIFont = R.font.mplus1pRegular(size: 14.0)!            // R.Swiftでカスタムフォントを指定する
    ...
}

レイアウト要素

余白、角丸、高さなどレイアウト要素の数値を扱うデザイントークンはCGFloatをextensionして定義するようにしました。

public extension CGFloat {
    struct spacing {
        static let xxx_small: CGFloat = 2
        static let xx_small: CGFloat = 4
        static let x_small: CGFloat = 8
        ...
    }

    struct cornerRadius {
        static let small: CGFloat = 2.0
        static let medium: CGFloat = 4.0
        static let large: CGFloat = 8.0
        ...
    }
}

ただしIBからここで定義したレイアウト要素の情報を直接参照できないという課題があります。
この課題に対する1つの解決策として、IB上で設定したNSLayoutConstraintをIBOutletを使ってコードと紐づけるという方法があります。
たとえばある画面をレイアウトする際にSafeAreaとコンポーネントの余白に定義したデザイントークンの数値を使いたい場面があるとします。
この場合IB上では仮の値を設定したNSLayoutConstraintをIBOutletに紐づけます。
あとは紐づけたNSLayoutConstraintにデザイントークンとして定義した値を設定することができます。

f:id:khaigo:20211213101536p:plain
IB上で設定したNSLayoutConstraintをIBOutletでコードと接続する

@IBOutlet weak var btnTrailingConstraints: NSLayoutConstraint! {
    didSet {
        btnTrailingConstraints.constant = <デザイントークンとして定義した値>
    }
}

しかしこの方法はデメリットも存在します。
特に下記のデメリットはIBを使用するメリットを打ち消すので、今回こちらの方法は採用しませんでした。

  • すべての制約をIBOutletに紐づけてコード上で管理するとコード量が肥大化する
  • 動的に制約を書き換えることになるのでStoryboard, xib上のレイアウトと実際に表示されるレイアウトに差異が出る

UIコンポーネントの実装

UIコンポーネントはボタンやフォームなどデザインに使用されるUIパーツを定義します。基本的にはここで定義したコンポーネントの組み合わせで各画面のレイアウトが完成します。
実装方針としてはIBDesignableおよびIBInspectableを使用し、UIKitの各パーツをラップしたクラスを定義しそれをIB上でカスタムクラスとして設定することにしました。
カスタムクラスは定義した各デザイントークンを組み合わせることで共通化を図ります。

またUIコンポーネントを定義する上で、コンポーネントを共通化する単位についてデザイナーとしっかり認識を合わせることが重要になります。
ここでエンジニア、デザイナー間で認識齟齬が起きると以下のような問題が発生するかと思います。

  • 持つべき機能が異なるコンポーネントを共通化してしまう
  • 同じ目的のコンポーネントを別コンポーネントとして切り分けてしまう

いずれもUIコンポーネントが二重管理になったり、想定外の画面でハレーションが起きるなどメンテナビリティ、スケーラビリティを低下させる要因になります。

UILabel

基本方針の通り実装します。カスタムクラスにIBDesignableを付与することでIBから適用したUIが確認できます。
ここでのポイントはUILabelを構成する要素はすべてデザイントークンで定義したものを使用している点です。

@IBDesignable
class TitleLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    private func setupAttributes() {
        // デザイントークンで定義した値を設定する
        font = .title
        textColor = R.color.systemGray_800()
    }
}

@IBDesignable
class BodyLabel: UILabel {
    ...
}

@IBDesignable
class CaptionLabel: UILabel {
    ...
}

定義したUILabelのカスタムクラスを設定することでIB上で反映されたスタイルを確認する事ができます(以下サンプル)。

f:id:khaigo:20211213101832p:plain
定義したUILabelのカスタムクラスを設定する

UIButton

こちらも基本方針通りです。
UIButtonの状態に応じてlayoutSubviewsでスタイルを切り替えるようにしています。
その他UIViewを継承するいずれのUIクラス(UITextView, UITextField etc)もこの方針でカスタムクラスを定義してUIコンポーネントを作成するようにします。

@IBDesignable
class FillButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        switch state {
        case .normal:
            backgroundColor = R.color.brandAccessible()
            titleLabel?.textColor = R.color.systemGray_000()
        case .highlighted:
            backgroundColor = R.color.brandAccessibleActive()
            titleLabel?.textColor = R.color.systemGray_000()
        case .disabled:
            backgroundColor = R.color.buttonDisabled()
            titleLabel?.textColor = R.color.disabled()
        default:
            break
        }
    }
    
    private func setupAttributes() {
        layer.cornerRadius = .cornerRadius.medium
        backgroundColor = R.color.brandAccessible()
        titleLabel?.font = .button
        titleLabel?.textColor = R.color.systemGray_000()
    }
}

@IBDesignable
class CancelButton: UIButton {
    ...
}

@IBDesignable
class TextButton: UIButton {
    ...
}

定義したUIButtonのカスタムクラスを設定することでIB上で反映されたスタイルを確認する事ができます(以下サンプル)。

f:id:khaigo:20211213102016p:plain
定義したUIButtonのカスタムクラスを設定する

まとめ

最後までお読みいただきありがとうございます。
まだまだ諸々の事情によりSwiftUIではなくUIKitを使用するプロジェクトは多々あるかと思います。
実装に関しては基礎的な内容だったかも知れませんがDesign Systemを実装する際の考え方の参考になると幸いです。
私自身もDesign Systemとその実装についてまだまだ模索中ではありますが今後もUnlearn & Learnでやっていきたいです!

Classi developers Advent Calendar 2021 の 15日目はおかじさんです。


  1. Design Systemとは諸説ありますが、大まかに言うとプロダクトのデザインにおける一貫性を効率的に保つための仕組みのことです。
    Design Systemを構成する要素としては、概念・原則をまとめたドキュメント、スタイルガイド、UIコンポーネントライブラリ、またそれらを管理・運用するためのルールやツールなどがあげられます。

IAM Policy Simulator で「必要な権限足りてる?」を確かめる

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

こんにちは。開発本部プロダクト開発部学習チームでエンジニアをしています、藤田です。 本記事では AWS の IAM の Policy の定義から、アクセス可能なリソース範囲・許可されるアクション等を事前に検証できる IAM Policy Simulator を紹介します。

続きを読む

EC2からECSへ移行する道のり

開発本部の onigra です。今回の記事は、Classiのアプリケーション実行環境をAmazon EC2からECSに移行しているお話をします。

この記事では「Ruby on RailsのWebアプリケーションをECSに移行する上での技術的なトピック」ではなく、「なぜClassiはEC2からECSに移行する必要があるのか」「どのように移行を進めているのか」についてをお話します。

なお、この記事は Classi developers Advent Calendar 2021 11日目の記事です。昨日は データプラットフォームチームの滑川さんによる Cloud Composer 2へのupgradeでどハマりした話 でした。

なぜEC2からECSに移行する必要があったのか

私が入社した当初、Classiは前述の通りAmazon EC2で稼働していました。それ自体に問題は無いのですが、サーバのプロビジョニングとデプロイが遅く、不安定という課題を抱えていました。

また、普段プロダクトを開発・運用しているチームの中にデプロイ、インフラの知識や、既存の仕組みを把握しているメンバーが少なく、プロビジョニングやデプロイに何か問題があると、私のような組織横断的な技術課題に取り組むメンバーが対応にあたることが多々ありました。

近年、書籍 『LeanとDevOpsの科学』 でも語られているように、変更のリードタイムとデプロイの頻度は組織のパフォーマンスに大きく関わる要素であり、今後価値提供のスピードを上げていくにあたって大きな障害となるため、なんとか全社的に解決したい課題だと考えていました。

そんな中、 2020年の春に不正アクセスが発生し、その後サービスの高負荷によるアクセス障害が続く状態 になりました。

日々対応に追われる中、次から次へとサーバのチューニングやアプリケーションの修正をデプロイしなければならない状況で、デプロイとプロビジョニングの仕組みが大きなボトルネックになっていることをこの時に改めて痛感します。

また、今後Classiがセキュリティ面でもパフォーマンス面でもお客様からの信頼を回復しなければならない中で、今のままの仕組みではとても改善のスピードを上げることはできません。

この時、現状のEC2へのプロビジョニング、デプロイの仕組みを全て捨て、ECSに移行して1から作り直そうという決断をしました。

移行の戦略

最初の移行の成否が今後を左右する

この時点で現場は日々の対応で疲弊していました。そんな状況を打破し、サービスの改善にポジティブな勢いを生むためには、移行する1つめのリポジトリの成果に大きく左右されると考え、確実に成功させる必要がありました。

そのため、最初の移行対象はコンテナ化の難易度が比較的低いWeb APIの機能のみを提供しているリポジトリを選択することにしました。

運用ができるメンバーを増やしつつ、移行する

私はコンテナを普段から利用し、ECSを触った経験があったので、移行する上で知識の問題は無いのですが、コンテナの運用に慣れていないメンバーへレクチャーをしきれるのかという懸念がありました。

また、Classiには多くのリポジトリがあり、それらを1つ1つ移行していたらかなりの期間がかかってしまうことは容易に想像できます。そのため、移行作業と移行のレクチャーができる人員をスケールさせる戦略を練る必要がありました。

そのため、移行作業は以下のようにメンバーを巻き込みつつ、最終的に自分以外のメンバーがレクチャーを行えるような状態を目指して進めることにしました。

  1. 自分で検証環境の移行を行い、必要な作業を把握する
  2. 本番環境の移行に取り掛かる際に一緒に作業するメンバーをアサインし、ペア作業でレクチャーしながら移行を完了させる
  3. 次のリポジトリに取り掛かる際に新たにメンバーをアサインし、2のメンバーにレクチャーしてもらう
  4. 以降、自分はサポート役に回る

また、本番リリース直前は必ず一緒に「あと何をすればリリースできるのか」を確認する機会を設けました。 いわゆるリリーススプリントに臨むための準備です。以下のような内容を確認、レクチャーし、リリースマネジメントを行いました。

  • Datadogの使い方
  • Sentryの使い方
  • 依存するアプリケーションの考慮
  • 本番ネットワーク疎通確認と方法の確認
  • リリースの手段と、ロールバックの仕方
  • 関係各所への連絡と期待値調整
  • EC2環境と、不要になるAWSのリソースを削除する手順

これらを移行作業の中でレクチャーすることによって、今後リポジトリ担当者が自立してサービスを運用できる状態になることを目指しました。移行の完了は終わりではありません、始まりなのです。

結果

2021年12月現在、13のリポジトリが移行完了しています。

何が移行を勢いづけたのか

フォロワーシップを強く発揮するメンバーの存在

同じ課題感を感じていたメンバーがフォロワーシップを発揮し、2つめのリポジトリの移行作業を早々に完了させ、以降に続くメンバーの勧誘とレクチャーを積極的に行ってくれました。

そのおかげで予定よりも早く自分がサポーターの役割に移行でき、アプリケーション固有の問題や例外的な対応をメインに行うことによって、移行プロセスのボトルネックの解消にフォーカスすることができました。

成長機会を貪欲に掴みにきた新卒、若手メンバーの存在

移行するメンバーを募集した際、新卒や若手が積極的に手をあげて参加してくれました。私自身、キャリアの中でもトップレベルに困難な状況と感じていたのに、それを成長機会と捉え、できることをしようとする姿勢には強く勇気づけられました。

会社の危機的な状況に対し、若いメンバーからサービス改善に繋がるポジティブなムーブメントを起こせたことは、非常に大きな意義があったと思っています。

移行に参加したメンバーの1人である小川さんがECS化について書いた記事も是非ご覧ください。

新卒1年目でECS化に取り組んだことを振り返る

移行に関わるメンバー同志のコミュニティのようなつながり

私はチャットへのレスポンスが早い方なのですが、移行作業を行うメンバーが集まるチャネルではよりそれを心がけました。未知の作業を行うことに不安を感じているメンバーに対し、何か書き込めばレスポンスが返ってくる安心感を与えたかったからです。結果的にその行動はチャネルが盛り上がることに繋がり、コミュニティのようになりました。

移行セレモニー

社内にポジティブな雰囲気を起こすことと、移行しきったメンバーを称えて成功体験にしてもらうために、移行が完了したら担当したメンバーにオープンチャンネルで移行完了宣言してもらい、スタンプを可能な限りつけて盛大に祝うというセレモニーを必ず行っていました。

f:id:onigra:20211208190426j:plain

その後

運用の知識を身につけたメンバーを触媒に、社内で徐々にサービス運用を改善する動きが浸透していきました。

運用改善を行うWorking Groupの定期開催化

まず、株式会社はてな様で行っている Performance Working Group を自主的に行うチームが現れました。その活動が他のチームに広がり、チームで個別に行うだけではなく、インフラチームが主催で行う組織横断的なPerformance Working Groupも定期開催されるようになりました。

また、定期的にSentryの整理やエラー内容の調査する取り組みも各チームで行われています。アプリケーションやエラー監視ツールはインシデント以前から導入されていたものの、これまではエラーが多すぎたり、使い方がわからないメンバーも多く、放置されがちだった状況から大きく改善されました。

これは、lacolacoさんがSentryについての継続的な活用支援を続けてきてくれたことも大きな後押しであったと感じています。

Sentryを活用するためにやっていること

システムアラート発生時のメンバーのレスポンスが向上した

インシデント以前はシステムアラートの発生に対して、特定のメンバーしか反応しない・できない状態で、反応したメンバーが担当しているチームに「これ大丈夫ですか?」と伝えに行くことも多かったのですが、徐々に反応するメンバーが増えてきて、今では担当チームのメンバーがシステムアラート発生から真っ先に「確認します」と反応することがほとんどになりました。

これにはインフラチームが主体で行っていた、サービスのピークタイムの監視を当番制で行う取り組みが定常化し、インフラチーム以外のメンバーもその当番に参加するようになり、システムアラートへの対応経験を積んだメンバーが徐々に増えていったことも後押しになりました。

今となってはピークタイムの監視当番は、システムアラート対応のオンボーディングの役割も兼ねるようになりました。

インフラに興味を持つメンバーが増えた

移行作業をきっかけにインフラに興味を持ち、自主的にAWSの資格を受験して合格したメンバーも数名います。

AWS 認定 ソリューションアーキテクト - アソシエイト(SAA-C02) 試験に合格したので振り返る

受験費用は会社でサポートしているのですが、最初に受験したメンバーは会社で精算できるということを知らず、たまたまSlackで「この間の試験受かってよかった〜」という発言を見かけて「会社で精算できるから申請してね」と声をかけたこともありました。

また、理解の必要はあるが自分で構築することは少ないAWSのネットワーク(VPC、Subnetなど)について、移行に参加したメンバーが勉強会を開催したいと相談され、一緒に内容を考えてハンズオンを実施したりもしました。

その他

監視についての記事を開発者ブログに執筆してくれるメンバーもいました。

AWS ECS監視のオオカミ少年化を防ぐために考えたちょっとしたこと Amazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話

現状維持を良しとせず、試行錯誤しながらアウトプットする姿勢はとても頼もしく感じます。

反省点

ここまで良かった点ばかり書いていますが、当然省みることもあります。

「移行に関わりたい」というメンバーが想定以上に多く、移行作業が勢いよく進んでしまい、私がコントロール仕切れないこともありました。また、私が移行作業しつつアクセス障害への対応も行なっていたため、当然作業の中でボトルネックになってしまうことも発生し、十分にサポート仕切れないまま進んでしまい、不安を感じさせてしまったメンバーもいました。私がボトルネックになってしまうことは、事前に予測できていたことでした。

移行のふりかえりを行なった際に、「表面的にうまくいってると評価されているECS移行のイメージと参加当初ギャップがあった」というフィードバックをもらいました。これは当時、十分なサポート体制を組めなかった状態を的確に指摘していると感じています。

最後に

結果、Classiでは現在ほとんどのサービスをECSで運用し、デプロイの安定化と頻度の向上に成功しました。また、移行作業を通して若手メンバーを中心に、全社的な運用力の底上げに繋げられたと感じています。最悪の状況から前を向けるようになったのは、間違いなくこの時に成長してくれたメンバーのおかげです。

一方で繰り返しになりますが、移行の完了は終わりではありません、始まりなのです。移行は価値提供のスピードを上げていくにあたっての最低条件でしかありません。また、「ほぼ」と書いた通り移行完了していないリポジトリは残っています。

引き続き、より良いサービスを運用していくために精進していきます。

© 2020 Classi Corp.