Classi開発者ブログ

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

フルリモート環境である開発本部の課題を解決するために始めてみたこと

こんにちは。技術戦略室にてエンジニアをしています、中島です。

以前、リモートワーク環境におけるコミュニケーション課題の一つである「質問」についてブログを書かせていたただきました。 tech.classi.jp

弊社では今でも全社的にフルリモートを続けていますが、やはり人と人との接触機会は以前より減っています。そのため、計画的偶発性も少なくなっているのではないか、と考えています。

※ 計画的偶発性とは?

個人のキャリアの8割は予想しない偶発的なことによって決定される。その偶然を計画的に設計し、自分のキャリアを良いものにしていこうという考え方。 計画的偶発性理論 - Wikipedia

今回は、こういった状況を改善するために始めてみた施策について紹介します。

開発本部の課題と仮説

施策の内容へ入る前に、今私が所属している開発本部の抱えている課題と、それに対する仮説をお伝えします。

「Classi」というサービスはユーザー様から見ると1つのサービスですが、その中には先生・生徒・保護者様向けに様々なサービスが存在します。 classi.jp

開発チームはそれぞれのサービス毎に存在していますが、サービス内の機能は他のサービスとも複雑に依存しあっています。そのため、ちょっとした修正、お問い合わせの調査、システムアラート対応などをするだけでも他のチームとのコミュニケーションが必要になります。

しかし、チームを越えてコミュニケーションを取ることが少し難しいという現状が明らかになってきました。 なぜ難しいのでしょうか? リモートワーク以前であれば、コミュニケーションを取るために少し社内を歩き回るだけで、お互いに気軽に話しかけやすいという側面がありました。

ですが、リモートワークでは、まずはSlackでの文字でのやりとり、話すためにはMTGの設定をしなければいけなくなりました。リモートワーク以前に入社している人からすれば、相手を見知っているので気軽さはさほど変わらないのかもしれませんが、リモートワーク以降に入社した人もたくさんいます。

チームを越えた仕事が発生した時に初めて、全く知らない人とコミュニケーションを取るのと、少しでも人となりを知っている人とコミュニケーションを取るのでは、違いがあるのではないでしょうか?

その仮説を基に、チーム外の人とのコミュニケーションを促進できるような施策=雑談をしていこう!となりました。

本当に?

コミュニケーションを取れば、雑談をすれば、本当に課題が全て解決するのでしょうか? そもそも、こういった制度はあまり受け入れられないことも重々承知しています。 なので開発本部の制度として建て付ける際に、それでも意義があると自分自身納得する必要がありました。そんな中、弊社VPoTであるしんぺいさんの記事を読みました。

nekogata.hatenablog.com

私には考えもつかない視点で書かれていてとても好きです。上記の文章とともに、弊社のesaの中ではさらに社内の課題を交えた議論がたくさんの人とされていました。

その中で自分が腑に落ちたことは、雑談してもコミュニケーションがうまくいかないことはあるしプロトコルが合わない人もいる。それをもっと早い段階で(仕事で関係性を持つ前に)認識しておく(器官を育てておく)ことが大事なのではないか。そうすれば本番ではもう少し上手くやれるのではないか?ということです。(上手くとは、無闇に衝突しないなど)

そういった事前の場としても機能するのであればやる意義はあるのかな、と思ったのでした。

新しく始めた制度について

新たに「Zatsu談制度」を始めました。名前にあまり深い意味はありません。

この制度の目的は、開発本部内で横の繋がりを作り、チーム外で気軽に話せる人(場)を作ることです。 開発本部内での、弱い関係斜めの関係を作ることを主目的としています。

弱い関係、斜めの関係ってなんぞや?と思った方は是非上記のリンクを御覧ください。

例えば、同じチーム(強い関係)にしか所属していないと、そのチームにだけ注目してしまい、他のチームから孤立してしまう可能性があります。 考え方・施策・行動など、そのチーム内で個別最適が進むと、いわゆる”サイロ化”が発生します。

「開発本部の課題」にも書いた通り、Classiはサービス(プロダクト)の依存関係が複雑に絡み合った状態で開発されてしまっているため、必然的に外のチームとコラボレーションしながら課題解決に取り組む必要があります。サイロ化した状態ではコラボレーションが進みません。情報伝播や相互理解を促進するためには、弱い関係も必要です。

ちなみに「弱い関係」についてはデメリットもあるそうです。気になる方は「組織デザイン」をご一読ください。

また、斜めの関係性を構築することで、チーム外の関係だからこそ相談できたり、異なる視点を得られたり、新たに別の関係性が生まれたりするのではないでしょうか。

そんな想いでこの制度が立ち上がりました。

Zatsu談制度の運用方法

制度をどのように運用しているかを紹介します。

  • Zatsu談制度は、基本は 1on1 形式で行います
    • 縛っているわけではないので、他のペアと開催するのでもOKとしています
  • ペアの選定仕様は以下となっています
    • 誰かの意図・思惑は入れず、ランダムとする
    • チーム外の人とペアにする
    • 過去にペアになった人と違う人をペアにする
  • ペア選定作業はGoogle Apps Script (GAS) で実現しています
  • 制度の1実施期間を、3ヶ月に固定しています
  • 期間が終了すると、ペアを再選定します
  • 一度のZatsu談期間が終わったら、開発本部メンバー全員にアンケート形式でフィードバックをもらいます
  • 相談窓口として、Google Formsを用意しています
  • 参加が難しいという方もいると思うので、強制はしていません

Zatsu談で何話そう?

突然、雑談してね!と言われても困りませんか?なので色々なZatsu談ネタをesaにまとめて共有しています。

例えば弊社ではチームごとに偏愛マップやモチベーショングラフを作っていたりするので、そのリンクを集めたり、16personalitiesStrength finderの結果を共有したりしています。

conversation topic
雑談ネタの紹介

また、Slackには色々な趣味チャンネルもあるので、そこから話題を広げることもできそうです。

 Recommended Slack channels
おすすめSlackチャンネル

esaには、自己紹介ページを作っている方も多数います。日報を書く方もいて、ペアの日報を読んでいるという方もいました。自分を知ってもらうためには一定の自己開示も大切ですね。

1回目の実施が終わってみて

7月末に1回目のZatsu談期間が終了しました。そのタイミングでアンケートを取ってみたので、一部の内容を紹介します。

Zatsu談回数

  • 複数回実施してくださった方が半数以上を占めました
  • 1回で終わってしまい続かない方が多いのではないかという予想でしたが、定期的な開催が多く見られました

満足度・目的達成度

1: 不満足 5:満足

1: 達成されてない 5:達成されている

  • まだ制度を始めたばかりなので、もっと左に寄るかなと思いきや、満足度・目的達成共に右に寄っていました。一定の評価をいただけてホッとしました

Zatsu談制度があってよかったこと

制度があってよかったことをたくさんいただいたので、一部を紹介します。

- 直接役立ったり効果があるかはわからないが、Zatsu談ペアになった相手とはちょっと気安い雰囲気は生まれると思う
- ペアになった人をハブに、今まで関わったことのない全く知らない人と話す機会を得られました
- 知らない部門の人や業務で関わりがない人と話せるのは良い
- 会社の歴史のシェアや暗黙知のシェアにもなる
- 業務に関係ないプライベートの雑談ができるのが楽しかった!
- 普通に人生の先輩として話すだけでも学びがあった!
- 合同Zatsu談しているところもあって良いと思った!
- 別の領域の人と接点を持てて、その人が別の会で発言されてるのを見ても「Zatsu談で話した人だ!」と顔が浮かんだり、雑談で話したことと紐づいたりして理解が深まった
- 普段絡みの無い人と話をすることができた
- たまに雑談する間柄だったけど、改めて機会ができてよかったと思う。
- 仕事以外のことを気軽に話せて楽しかった。今後、何かあったときに話しかけるハードルは下がりそう。
- 業務であまり話さない人と話すきっかけが強制的にできるのでよかった。
- 通常業務では関われなかった関係ができた。新卒とペアになったので、他の新卒や研修にも興味を持つようになった。
- 普段話さない内容であったりちょっとした相談を気軽にすることができた。
- 恐らく殆ど接点がなかった&これからも少ないであろう方とお話できたのが嬉しかったです。
- 話すきっかけはできました

この他にも「やってみたら楽しかった!」「Zatsu談制度以外のところでもZatsu談してみた!」「チーム内のコミュニケーションも見直してみた」などの嬉しい声を聞くことができました。

しばらくこの制度を続けてみて、いずれまた状況をこちらで報告したいと思います。

他にやろうとしていること

Classiはサービスもシステムも複雑と何度か書いていますが、その影響もあり中途入社した方は入社当初のキャッチアップに苦労している面も見られます。

中途入社者が組織に適応するまでには一定の期間が必要だと言われています。(組織再社会化) 技術戦略室ではこの点をサポートするべく、中途入社者を受け入れるチームと協力して開発本部のオンボーディングを見直す動きも始めています。

まだまだやりたいこと・やれることはたくさんあるので、引き続きみんながごきげんにミッションを達成できるよう、開発本部の課題を解決していきたいと思っています。

RubyとRailsのコミッターである松田明さんの講演でプログラミングを楽しむモチベーションが上がりました

2022年4月から新卒エンジニアとして入社しました daichi ( id:da1chi24 )です。

先日社内でRubyとRailsのコミッターである松田明さんによる特別講義が開催されました。
開催の経緯や感想、当日出た質問を紹介します。

開催の経緯

2022年の新卒研修では、研修内容に関連した話題について自身で調べた内容を発表する機会がありました。
その発表の内容に、OSSのコードを読んだ発表など、それぞれの興味があることを深く掘り下げるものが多くありました。
発表が終わった後、その発表が良かったという話が上がり、OSS活動を積極的に行っている松田明さんをお呼びして講演していただくことになりました。

新卒研修に興味がある方は以下の記事もご覧ください。 tech.classi.jp tech.classi.jp

特別講義のテーマ

特別講義のテーマは、これから職業エンジニアとして人生を進める私たち新卒に向けて、楽しくプログラミングを続けていくためのヒントとなるものでした。

松田さんは20年以上職業プログラマーをされており、OSS活動やコミュニティ運営などもされています。そこで得られた経験や実感についての発表でした。

このような世界で活躍するエンジニアの方から直接話を聞ける機会はすごく貴重なので、楽しみにしていました。

印象に残った話

この講演の中では沢山の興味深い話がありました。 その中でも私個人が特に印象深かった話を紹介します。

プログラミングの最大の楽しみは「コミュニティ」

仕事で楽しめるプログラミングはプログラミングのごく一部でしかなく、最大の楽しみは「コミュニティ」であると話されていました。

松田さんは実際に、Asakusa.rbやRubyKaigiの主催など多くのコミュニティ運営に関わっていたり、世界中のカンファレンスで登壇を行っています。

その中で他のエンジニアとの関わりや新しい発見や経験を得ることができ、その活動がとても充実しているという話でした。

松田さんがコミュニティを大切にしている話は、自分にとって嬉しかったです。
自分もエンジニアコミュニティが好きでこの業界を選んだのですが、技術的に最前線を走るエンジニアの方も同じことを考えていることに親近感を覚えました。

また世界中のカンファレンスでの思い出や、そこでのエンジニアのコミュニティの面白さが語られました。
カジュアルな雰囲気で美味しいご飯やビールを楽しみながら技術の話ができるのは、すごく楽しそうで新鮮でした。

このようなイベントを通じて国内だけでなく世界中に友人ができることが、とても魅力的だと感じました。

OSS活動は義務ではなくて権利

OSS活動は「エンジニアがしなければならないもの」というものではなく、「誰でも参加することができる権利が与えられているもの」なので好きなところからつまみ食いしながら楽しんで欲しいという話がありました。

コード一つで世界中の人と関われるというのは、他にはないエンジニアの特権です。
松田さんも実際にOSSの活動を通じてエンジニアのコミュニティと関わったり、世界の有名なエンジニアと一緒に仕事をしたりしているという話もありました。

業務の中だとその会社のエンジニアとしか関わることができませんが、OSS活動であれば世界中の素晴らしいエンジニアと一緒に開発ができます。
そういった活動での経験がコミュニティの関わりに繋がったり、業務にも良い影響があるのだと感じました。

確かに今まで私は、OSS活動というと貢献しないといけないというイメージが強く、技術的に未熟な状態で貢献できることがあるのかと躊躇していました。

この話を聞いた後、自分にもできるとこから気軽にやっても良いと感じるようになり、敷居が下がりました。
今ではドキュメントの修正のPRを出したりするなど簡単な事から始めるようになったので、考え方が変わるきっかけになったと思います。

実際の自分が出したドキュメントの修正のPR

当日の質問

講義では話されなかった当日出た質問について一部紹介します。

Q. Ruby/Railsにずっと時間を投資できた理由を知りたいです。他の技術に目移りしなかったのですか。

Rubyの次に来る言語とかはひとりのプログラマーとしては気になりますが、自分はRubyやRailsのことが大好きで両足突っ込んでしまっているので、今更他の言語を主戦場にする気にはならないです。
むしろ「RubyやRailsの市場は俺が耕した」と思っているし、それで自分がRubyをやめてしまったら無責任だとも思うので、これからももっとみんなにRubyやRailsを使ってもらうように働きかけていく活動は続けていくと思います。

OSSへの人生最初のパッチはどうやりましたか。良いアプローチの仕方を教えて欲しいです。

僕の場合はあんまり参考にならないかもです。昔はもっと世の中は雑だったし、Railsとかも普通に使っているのに普通にバグっているという状態でした。
仕事で使うにしてもパッチを当てながらでないと使えなかったです。
なので敷居が高くなかったし、自分が困っているからやっていた感じでした。
今は整ってきてしまっているので、そこから問題を探すのは当時より難しそうではあります。
それでも、世の中のOSSからバグが無くなることは絶対にないので、まだまだ解決するべき問題は無限にあるはずです。

[予告] 9月のRubyKaigiに現地で参加します

Classiは2022年のRubyKaigiのスポンサーをしています。スポンサーチケットを頂いたので、新卒を含めたエンジニアと現地に行きます。

今回の松田さんの講演の中で、カンファレンスをより楽しむためには予習をすることが大事だという話がありました。

今私たちはRubyKaigiをより楽しむために、技術顧問のigaigaさんと毎週RubyKaigiのスピーカーセッションの内容などの予習会を行っています。

参加した後に現地のレポートも報告するので楽しみにしていてください。

UNIX 系システムのプロセスに関する社内勉強会に参加しました

こんにちは。新卒 1 年目エンジニアのすずまさです。

先日、弊社 VPoT の id:nkgt_chkonk が執筆した process-book の勉強会を修了しました。

process-book は *nix系のシステムにおけるプロセスやシグナルなどについて説明することを目的に書かれました。「プロセスとかよくわかってないからちゃんと知りたいな」みたいなひとたちが想定読者です。

参加する前は UNIX の基礎的な知識も乏しかった私ですが、学びがたくさんあり毎回楽しく参加できたので紹介します。

process-book の社内勉強会の様子

開催の経緯

Slack でシェルスクリプトの話題で盛り上がったのをきっかけに、プロセスモデルの重要性が話に挙がり、流れでその日のうちに「process-book 読もう会」が誕生しました。

進め方は下記の通りです。

  • 日時: 毎週木曜日 15:00〜15:30
  • 週に 1 章ペースで進める
  • 初めに各章ごと担当者を決めておき、担当者は esa に内容をまとめ、当日司会をする
  • 参加者は各自手を動かして予習しておく
  • 疑問や気付いたことがあれば会の中で発言する

印象的だった章

4 章のファイルディスクリプタの話が印象的でした。

  • OS は、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開きます。
  • OS は、その開いたファイルを表す「番号札」を作成します。
  • OS は、その番号札をプロセスに対して返します。

この「番号札」のことを、「ファイルディスクリプタ」と呼びます

Ruby を使い、実際にファイルディスクリプタを出力してみると "5" が出力されます。

また、標準入力のファイルディスクリプタは 0、標準出力は 1、標準エラー出力は 2 ということを学びます。

ここで、「では 3 と 4 は何を指しているのか?」という疑問が生まれました。

会の中では「lsof コマンドを使うと調べられるよ!」とだけアドバイスを頂けたので、勉強会後に実際に実行してみると、下記のような結果が得られました。

# fd.rb
file = File.open("nyan.txt","w")
puts file.fileno
puts $$
while(1)
  sleep
end
$ ruby fd.rb &
$ 5 # ファイルディスクリプタ
8300 # プロセスID
$ lsof -p 8300
COMMAND  PID     USER   FD      TYPE DEVICE SIZE/OFF   NODE NAME
~~~ 省略 ~~~
ruby    8300 ssm-user    0u      CHR  136,1      0t0      4 /dev/pts/1
ruby    8300 ssm-user    1u      CHR  136,1      0t0      4 /dev/pts/1
ruby    8300 ssm-user    2u      CHR  136,1      0t0      4 /dev/pts/1
ruby    8300 ssm-user    3u  a_inode   0,14        0     13 [eventfd]
ruby    8300 ssm-user    4u  a_inode   0,14        0     13 [eventfd]
ruby    8300 ssm-user    5w      REG  259,1        0 259116 /home/ssm-user/004/nyan.txt

ここで、FD (ファイルディスクリプタ) の 3u と 4u を見れば良さそうだと分かりますが、これだけ見ても実際に何が行われているのかは分かりません。 そこで、man eventfd を実行してみると、下記のような文章が得られます。

eventfd() creates an "eventfd object" that can be used as an event wait/notify mechanism by user-space applications, and by the kernel to notify user-space applications of events.

この文章から、ファイルを読み込んだ後にそのファイルへのアクションを待つのが 3 で、アクションを受け取って通知するのが 4 だと予想できます。

では、その通知はどこに向けてされているのでしょうか?

Ruby の実装を読んでみましょう。

pipes[0] = pipes[1] = eventfd(0, EFD_NONBLOCK|EFD_CLOEXEC); https://github.com/ruby/ruby/blob/b6c1e1158d71b533b255ae7a2731598455918071/thread_pthread.c#L1767

この文が呼び出されているコメントを読むと、"communication pipe with timer thread and signal handler" と書かれています。 つまり、通知を出しているのは Ruby 内部のタイマーで、通知を受け取っているのはシグナルハンドラーなのだと分かります。

上記は勉強会後に Slack で質問したことで得られた学びなのですが、「ファイルディスクリプタの 3 と 4 が何を指しているのか知りたい」という当初のシンプルな疑問から Ruby の実装を読むことになるとは思わなかったので感動しました。

1 つの疑問から新たに疑問が生まれ、それがどんどん解消されていくのが気持ち良くて楽しかったです。

参加して嬉しかったこと

質問が歓迎される

本勉強会では先生役のような詳しい人が何人かおり、その方たちに出てきた疑問をよくぶつけていたのですが、たまに「良い疑問ですね」と言われるのが嬉しくてかなりモチベーションになりました。

疑問をぶつけるとその場で解消され、解消されなかったとしても考察が広がるので、疑問を持てば持つほど楽しい勉強会でした。

man を読むようになった

man はman コマンド名でそのコマンドのマニュアルを読むことができるコマンドです。

正直今まで man コマンドを使ってマニュアルを読んだことがほとんどなかったのですが、今回の勉強会を経て man をしっかり読むようになりました。

man を読むことで疑問が解決できたり、新しく洞察が得られたりすることが多く、改めて公式ドキュメントの重要さを認識しました。

ボリュームがちょうど良かった

文字数が多くないので予習が苦ではありませんでした。 余裕があったため、章の内容通りに手を動かすだけでなく、毎回気になった箇所をじっくり調べることもできました。

感想

最初の方は分からないことが多すぎて議論についていくので精一杯でしたが、途中からわかることが増えて様々な疑問が浮かび、そこから一気に楽しい勉強会になりました。

他の方の鋭い疑問に驚く場面も多く、一人でやっていたらここまで学びは多くなかったと思うので、本当に参加して良かったです。

開催してくださった先輩やサポートしてくださった方々には感謝の気持ちでいっぱいです。

今回学んだ内容を活かした発展的な内容もやりたいという話が挙がっているので、次回があれば是非また参加したいと思います!

Amazon OpenSearch Serviceをアップグレードしました

こんにちは、プロダクト開発部でバックエンドエンジニアをしている望月です。

Classiのサービスでは、先生や生徒がアップロードしたコンテンツファイルやWebテストなどの検索システムにおいてAmazon OpenSearch Service(以下、OpenSearch)を利用しています。
コンテンツファイルのメタデータやWebテストのデータ等はRDBでも保持しているのですが、以前検索におけるパフォーマンスが課題になった際にOpenSearchが導入されました。 ユースケースとしては、Webテストの文章からの全文検索や、ファイル名・(オーナーの)ユーザー名等でのキーワード検索、カテゴリや属性ごとの検索などがあります。

今回はこのOpenSearchのアップグレード実施で行ったことや学びになったことをお話ししていきます。

アップグレード方法

AWSがマネージドサービスとして提供しているインプレースアップグレードがありますが、当時のOpenSearchのバージョンが古く対応していなかったため、新しくOpenSearchドメインを立てて移行する形を取りました。また、同時にOpenSearchを配置するネットワークの移行も実施しました。

今回、新旧OpenSearch間で 移行したドキュメント数は約572万件でした。Classiの中でも比較的データ量の多いシステムで、移行に30時間程度必要という見積もりだったため、定期深夜メンテナンス(5時間)の中でデータ移行を含めた作業を完結させるのは厳しいという判断になりました。

新旧OpenSearchを並行稼働させて両方に書き込みを行う期間をもった上で切り替えるという案もありましたが、この登録処理でパフォーマンスに課題があり、処理を増やしたくないという理由で今回は採用に至りませんでした。

最終的に実施したのは、事前にデータ同期済みの新しいOpenSearchドメインをスタンバイさせ、定期夜間メンテナンスでは新旧OpenSearchの切り替えと、アップグレードに対応したクライアントアプリケーションのリリースのみを行う、という方法でした。

本番オペレーションの手順を詳細に書いてデモンストレーション

本番オペレーションは、以下のような手順で進めました。

  1. 新しいOpenSearchへ、事前に既存のデータを取り込む
    • RDBのデータをもとに、クライアントアプリケーションから 新しいOpenSearchへデータを再登録するスクリプトを実行
  2. 定期深夜メンテナンスまでは通常通りユーザーがサービスを利用しているため、新旧OpenSearch間で発生するデータ差分を定期的に同期
    • RDBのデータの更新履歴をもとに、クライアントアプリケーションから 新しいOpenSearchへデータを更新するスクリプトを定期的に実行
  3. 定期深夜メンテナンスで新旧OpenSearchを切り替え
    • 新しいOpenSearchのバージョンに対応したクライアントアプリケーションのリリース
    • クライアントアプリケーションが新しいOpenSearchへアクセスするよう切り替え
    • QAの実施

大量のデータを扱うことには個人的にこれまで苦手意識があり、検証段階ではデータ再登録・更新時のスクリプトで意図しないタイムアウトが発生するなど苦労もありましたが、本番のオペレーションは大きな問題もなく終わらせることができました。

前述の通り、今回のOpenSearchアップグレードは定期深夜メンテナンスで作業を行う必要がありました。そして、定期深夜メンテナンスはおおむね月に一度の実施となっているため、本番でチャレンジできるタイミングが限られていました。
そのため、事前にオペレーション手順(データ移行・新旧OpenSearch切り替え・クライアントアプリケーションのリリース・ロールバック)をすべてチェックリスト形式で詳細に記述しておき、ステージング環境にてQAを行う際に何度かオペレーションの練習をしました。AWS環境に反映してみて初めて表出した事象もいくつかあったため、都度対応しながらオペレーション手順へ取り込んでいきました。

結果的にロールバックは実施しませんでしたが、手順を詳細に書いておくことで当日に落ち着いて作業を進められました。また、移行後のデータに追加で処理を走らせる際にも役立ちました。

システムと組織の依存関係に合わせてメンバーを巻き込む

今回最も難しかったのは、OpenSearchからユーザーまでの間に複数の機能コンポーネントが存在していて、それぞれ担当するチームが異なっていたことでした。 大規模なシステムの場合、機能コンポーネントを複数扱うプロジェクトを進めていくのは技術的にも困難ですが、関係するメンバーの意向を確認しつつ進めていくことも大きな課題となるのではないでしょうか。

今回のケースですと、各機能コンポーネントを担当するチームは以下の通りでした。

  1. OpenSearchを管轄するチーム
  2. OpenSearchのクライアントアプリケーションを管轄するチーム
  3. ユーザーから見える機能を管轄するチーム

最初私は1のチームメンバーとして作業を進めていましたが、検証環境を作るにあたっては2のチームメンバーに相談相手やレビュワーになってもらいました。また、リリースするにはユーザーから見える機能がちゃんと動いているかどうか確認するプロセスが必要だと思い、3のチームメンバーに協力をお願いしたり、自分がチーム移籍したりして作業を進めました。
また、OpenSearchの切り替え後に発覚したデータ不整合において今回の移行が起因であるかどうかの調査を行うことがあり、こちらもチーム間で連携して過去の経緯を追いつつ調査を行いました。

最後に

以上、OpenSearchのアップグレード実施についてお話しさせていただきました。 今回、自分1人で抱え込まずに適切なチームにヘルプを依頼できたのは、振り返ってみると良かったのではないかと思います。力を貸してくれたメンバーには、本当に感謝しています。

今後もアップグレード対応は継続的に行なっていく必要がありますが、今回実施した際の知見を活かし、よりスムーズに行えるようにしていきたいと思っています。

We are Hiring!

Classiでは今回紹介した以外の機能ですと、学校内でやりとりするメッセージ機能や、ポートフォリオ(活動記録)の検索システムでもOpenSearchを利用しています。 また、他にもECS化やデータ基盤の活用など技術的に面白いトピックも多くあります。詳しくはぜひ、カジュアル面談等でお話ししましょう!

Jest v28 shard オプションを使い、CI でカバレッジを計測できるようにする

こんにちは、ラーニング・学習トレーニングチームの id:tkdn です。 今日は Jest shard オプションを使って CI でどうカバレッジ計測をしたか について書いていきます。

Jest v28 shard オプション

Jest v28 から shard オプションが入りました。このオプションでテスト実行を指定の数で分割することができます。

Jest's own test suite on CI went from about 10 minutes to 3 on Ubuntu, and on Windows from 20 minutes to 7.

公式のアナウンスでもあるとおりですが、Jest 自身の CI でのテストが 20 分から 7 分になったという速度の変わりようです。

私たちのチームではリモートという状況の中で TDD /モブプロを実践しており、コミットによりバトンをつないでいます。そのため CI のスピードアップは開発体験を良くするだけでなく、マクロな視点ではリリースのリードタイムを短くすることにもつながります。

shard オプション、これを使わない手はありません。

shard オプションの使い方

我々は GitHub Actions を使っているので以下のように設定ファイルを書き換えました。特に難しいところはありません。ワンラインの実行オプションについては Jest のドキュメントに記載があります

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}

node_modules のキャッシュなどほかのステップは省いていますが、最小構成であれば上記の設定で動きます。これで testジョブは 4 つ並列で動くことになります。

環境にもよりそうですが、我々の環境では約 500 のテストケースで 5 分弱かかっていたところ、shard で 4 つ並行させると半分の 2 分半に短縮できました。 めちゃくちゃインパクトがあるわけではないのですが、ごくまれにモブプロで全体通してテスト結果みたいねというときや、ちょっとした修正後のデプロイなど、待ち時間が短くてうれしいタイミングはいくらでもあります。

shard オプションで実行した後カバレッジをどうするか問題

ただし問題が出てきます。プロジェクトでは coverageThreshold を設定していたため Jest が機械的に分割したテストのサブセットではグローバルなカバレッジ閾値を満たすことができないという状況が発生しました(分割せずに実行した場合はもちろんカバレッジを満たします)。

並列テスト A のコードパスで通ったモジュールやコンポーネントが必ずしも並列テスト A でテストされている保証はないので、そうなるだろうという予想はなんとなくしていましたが…。

類似した Issue: [Bug]: shard option and global coverageThreshold config · Issue #12751 · facebook/jest

解決策:並列テストのカバレッジを別ジョブで統合する

この問題についてはチームメンバーが解決策を持ってきてくれました。

参考になる Issue: Expose istanbul/nyc's check-coverage functionality in jest · Issue #11581 · facebook/jest

まずは並列で実行したテストのカバレッジを個別で Artifact として持っておきます(Artifact 自身は次のジョブで使います)。

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}
+   - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json
+   - name: Upload Artifact
+     uses: actions/upload-artifact@v3
+     with:
+       name: tmp-coverage
+       path: ./coverage

次に新しいジョブで並列実行により得られた Artifact を集めて nyc を使ってマージし(A)、nyc を使ってカバレッジ計測し(B)、簡易的なスクリプトでカバレッジを満たしていないファイルなどを出力します(C)。

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --coverage --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}
    - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json
    - name: Upload Artifact
      uses: actions/upload-artifact@v3
      with:
        name: tmp-coverage
        path: ./coverage
+ report-coverage:
+   needs:
+     - test
+   runs-on: ubuntu-latest
+   steps:
+     - uses: actions/checkout@v3
+     - uses: actions/download-artifact@v3
+       with:
+         name: tmp-coverage
+         path: tmp-coverage
+     - name: Merge coverage
+       # (A)ここで並列で得られたカバレッジをマージする
+       run: npx nyc merge tmp-coverage/ coverage/coverage.json
+     - name: Check coverage
+       # (B)マージしたカバレッジ計測判定を行う
+       run: npx nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100 -t coverage/
+     - name: Report coverage
+       if: failure()
+       run: npx nyc report --reporter=text -t coverage/ > coverage/coverage-result
+     - name: List Low coverage files
+       if: failure()
+       # (C)CIでの出力のためにスクリプトを実行します
+       run: node .github/tools/extract-low-coverage.js

これで大方やりたいことは実施できました。

注意点

気にしなくてはいけないこととしては、GitHub Actions の従量課金は実行時間の合計が対象となることです。実行時間が半分になり嬉しい限りなのですが、2 分半 * 4 並列が実際の実行時間となる点はしっかり踏まえて、お使いのプラン内の無料枠を考慮しましょう。少なくとも最初から並列する数を多めに設定しすぎてクォータを食いつぶした! なんてことがないようにするとよいですね。

まとめ

Jest v28 から使える shard オプションとそれを利用したカバレッジ計測について書きました。参考記事として以下に挙げた URL で狂喜したのですが、shard で実行後のカバレッジ計測のプラクティスがなかったので今回筆を取らせていただきました。

shard オプションは嬉しいのですが、実際には Jest のカバレッジの仕組みでフォローできるようになると良いですね。

Classi では CI の速度が気になった際にカッとなってやってしまうエンジニアを歓迎しています!!

参考 URL

ClassiのアダプティブラーニングエンジンCALEの品質を高める取り組み

はじめに

こんにちは、Pythonエンジニアをしてます工藤( id:irisuinwl )です。 この度、Classi独自のアダプティブラーニングエンジンである Classi Adaptive Learning Engine (CALE) をリリースしました。

corp.classi.jp

自分は主にCALE開発において、レコメンドを行うエンジン部分のバックエンドを担当しました。 今回の記事では、0からレコメンドシステムを開発し、システムが安定稼働する品質を実現したノウハウを紹介したいと思います。

CALEの概要

アダプティブラーニングとは、個別最適化学習のことを言います。 CALEでは、従来のClassiでのテスト機能である先生から生徒へのテストを配信、テスト解答に加えて、テスト終了後に生徒のそれぞれの理解に応じて問題を出題し、そのテストについて生徒それぞれの理解を深めるための機能を実現しました。

アーキテクチャ

CALEは Google Cloud (Google Cloud Platform, 以下 GCP) 上で構築されており、アプリケーション基盤としてGoogle Kubernetes Engine (以下 GKE) を使っております。

推論するためのモデルとAPIのpodは分離し、モデル開発とAPI開発の責務分割および、将来的に様々なモデルによって推論できるように設計をしました。

レコメンドエンジンのAPI部分はFlaskを用いて実装しており、ストレージには Firestore, Cloud SQL, Cloud Storage を利用しております。

工夫した点

CALEは新規システムであり、個別最適化学習を実現するレコメンドエンジンの要求仕様や、良いレコメンドの体験など、見えない部分が多いです。そのため、システムが安定稼働すること、つまりシステムの品質を守ることを重視しました。 以下ではシステムの品質を高める上で、工夫したTipsを紹介していきます。

本記事で紹介する取り組みを実施することで、CALEでは

  • valid rateが99.99%,
  • 95%ile latencyが779ms

という安定したシステムを実現することができました。

テスティング

テストを書くことは品質を保つ点において重要です。 CALEでは以下のテストをおこなっていました。

  • ユニットテスト
  • インテグレーションテスト
  • 負荷テスト
  • QA

ユニットテスト、インテグレーションテストではPythonのテストフレームワークであるpytestを用いてテストしました。

コードカバレッジはQAチームと相談し、C1 Coverageを選択しました。最終的にカバレッジを95%以上高めることができました。

負荷テストは、LocustというPythonの負荷テストライブラリを用いて実装しております。

各API呼び出しのユースケースに対して、今期想定されるユーザー利用数から負荷のテスト設計・実装を行い、ビジネスサイドに近いプロダクトオーナーを含めてレビューを行いました。 また、レイテンシが高まる観点として、登録したユーザー数、レコメンドされる問題数、蓄積された解答数といった、データ量を観点に入れて負荷テストの設計・実装を行いました。

Locustの詳細な使い方は以前自分が書いた こちらの記事 を参考に頂ければ幸いです。

QAではAutifyを使って、実際にユーザーを想定したユースケースのE2Eテストを定期実行しました。E2Eテストの自動化によって、アジリティ高くデプロイすることが出来ました。

運用・監視

運用・監視はCloud Operationsの内、以下を利用しました。

  • Cloud Logging
  • Cloud Monitoring
  • Cloud Profiler

特にCloud LoggingのPython clientがv3.0.0 になってからさまざまな情報を構造化ロギングすることが出来、使い勝手が良かったです。 以下のようにextra引数に値を入れることでログのjsonPayloadに値を入れることが出来ます。

import logging
from google.cloud.logging.handlers import CloudLoggingHandler
import google.cloud.logging

client = google.cloud.logging.Client(project="test-project")
handler = CloudLoggingHandler(client)
# setup_logging(handler)

cloud_logger = logging.getLogger('cloudLogger')
cloud_logger.setLevel(logging.INFO)
cloud_logger.addHandler(handler)

data_dict = {"hello": "world"}
cloud_logger.info("cloudLogger logging lib test1", extra={"json_fields": data_dict})

上のコードを実行すると、以下のようにログが出力されます。

また、運用と開発を両立するために SLI/SLO を設定しました。 GCP が提供している SLI/SLOに関するドキュメント を参考に、以下の流れで作成しました:

  • 利用ユースケースをまとめる
    • 利用ユースケース (クリティカルユーザージャーニー)の一覧化
    • ユースケースごとの影響度合いを考える
  • データ分析
    • 各ユースケースにおける処理の性能を測る
    • 現状の利用状況に対して、厳しく設定するのか、緩く設定するのかを考える
  • SLIの設計
    • 上記で洗い出したユースケースごとに設計する
    • ユーザーがサービスを問題なく使えるために見るべき数値は何かを指標に落とす
  • SLOの設計
    • ユーザーがサービスを問題なく使えるために指標をどの程度にすれば良いかを考える

そして、考えた基準をCloud Loggingのログベース指標でメトリクスを取得し、Cloud Monitoringのサービス経由でSLI/SLOに設定しました。

Kubernetesやインフラの運用テスト

開発環境で運用のテストも行いました。 テストの手順としては先述したLocustでのシナリオテストを常に行いながら、インフラの構成変更、スケーリングなどをテストし、ダウンタイムが生じるかを確認しました。 基本的には考えられる運用を列挙して、その洗い出した項目をテストをしました。 以下がその項目の一例となります。

  • 高負荷を掛けて、GKEのオートスケーリング時の挙動をテストする
  • クラスタのアップデート
  • ノードのアップデート
  • 誤ったイメージをpush, rollbackする
  • Cloud SQLのフェイルオーバー
  • 稼働中のモデルデプロイ

このテストをすることにより、スケーリング時のダウンタイムが発見でき、Kubernetesの Pod Terminate時のベストプラクティス に従い、対処することが出来ました。

まとめ

アダプティブラーニングエンジンであるCALEの高いシステム品質を実現する取り組みを紹介しました。

100%安定したシステムを実現することは不可能にせよ、今回の記事で紹介した

  • 利用に則したテストを行う
  • SLI/SLOといった品質の基準をビジネスサイドおよび開発者全体で合意をとる

といった取り組みを続けていれば、システムが運用できるか、期待するシステム品質の実現できるかが自ずと分かり、高い品質のシステムの実現を達成できると考えております。

Classi では学校教育の現場で使われる高い品質のサービスを実現していく必要があります。 レコメンドエンジンを初めとした全国の学校で利用される新しい教育×データ活用サービス、そして、教育現場で安定稼働するシステムを一緒に作っていきたいと思った方は是非、Pythonエンジニアにご応募ください!

https://hrmos.co/pages/classi/jobs/0000003hrmos.co

個別学習機能の裏側を紹介: アダプティブラーニングエンジン「CALE」

みなさん、こんにちは。 データAI部でデータサイエンティストをしております廣田と申します。今回は、2022年5月にリリースした「全国模試に対応したAI搭載の個別学習機能」について、ユーザーからのフィードバック及びAIの部分について簡単に紹介していきたいと思います。

弊社ではこのAIを「CALE(Classi Adaptive Learning Engine)」と呼んでおり、本記事でもこちらの呼び方を使っていきたいと思います。

個別学習機能およびCALEについて

まず、個別学習機能について簡単に紹介します。こちらの画像を使いながら説明します。

個別学習機能の全体像(https://corp.classi.jp/news/2710/ より引用)

個別学習機能は

  • STEP 1: 先生によるテスト配信
  • STEP 2: 生徒によるテスト解答

の後のSTEP 3に対応する機能で、以下の流れを繰り返しながら生徒は学習していくことになります:

  • CALEが生徒の解答データを取得し分析
  • CALEが生徒におすすめの問題を複数選び、生徒に提示(推薦ロジックは後述)
  • 生徒は提示された問題の中から1つ問題を選び解答

複数の問題を提示する点が大きなポイントで、生徒の「どういう問題を解いていきたいか」といった意志を反映する余地を確保することで、生徒が納得感を持って取り組めるよう工夫しています。

CALEはこの個別学習機能の中で、生徒に合った問題を推薦する役割を担っています。

ユーザーからのフィードバック

幸いなことに、リリース以後多くの生徒にCALEを利用していただいております。ここでは、CALEを利用していただいた一部の学校の生徒に対して実施したアンケート結果について紹介いたします。

CALEが推薦した問題の難易度について尋ねたところ、一番多いのが「難しかった」で、次点で「ちょうど良かった」となりました。「難しかった」と「ちょうど良かった」には大きな差は無く、総評としては「やや難しい」と言えると思います。

CALEが推薦した問題の難易度についてのアンケート結果

CALEの問題推薦ロジックの基本アイデア

ここではCALEが問題を推薦する際の基本的なアイデアについて紹介します。ポイントは

  • ①問題の難易度調整
  • ②学習単元の遷移

の2点です。

①問題の難易度調整

基本的なアイデアは「難しすぎず易しすぎない問題を推薦する」というものです。易しすぎる問題では学べることも少ないですし、難しすぎる問題の場合はそもそも何かを学び取ることが難しいです。CALEでは「この生徒にこの問題を出した時、何%の確率で正解できそうか」を計算し、その予測正答確率の値に基づき、難しすぎず易しすぎないような問題を選び出しています。予測正答確率の算出には項目反応理論(Item Response Theory)を利用しております。

既に触れましたが、現状のCALEは生徒からは「やや難しい」と評価されております。仮に正答率が高めの問題を選ぶようにCALEの挙動を調整すれば、生徒が「ちょうど良い」と感じる割合が増えると予想されます。しかし単純に難易度を易しくしてしまうと、今度は生徒の学びに繋がる要素が減ることが予想されます。生徒の学習の進めやすさと学習効果、双方のバランスがちょうど良くなるラインを探ることは非常に興味深いテーマで、今も検討を重ねているところです。

②学習単元の遷移

上記のような難易度調整をしても全く問題に正解できないケースが存在します。例えば、現在学習中の単元の前提の単元の理解が不足しており、現状の単元の理解が進まないようなケースです。このようなケースの対応策として、CALEでは学習単元の遷移機能も備えております。

生徒が学習中の単元で全く正解できなかった場合、CALEはその前提知識に相当する単元にさかのぼって問題を選んでくるようになっております。もしさかのぼり先の単元の理解が十分だと判断されれば、元の単元から出題されるようになります。

ただし単元間の依存関係は自明なものではないため、「この単元ができなかった時にどの単元にさかのぼらせるべきか」といった点については今も検討しているところです。

今後の課題

課題は山積みです。例えば現状のCALEの問題推薦ロジックについての課題であれば

  • 出題難易度のバランス調整
  • さかのぼり先の単元の選択方法
  • 予測正答確率の精度向上

などが挙げられますし、さらに大きな枠組みで考えると

  • 現状のロジック以外の問題推薦ロジックの検討
  • 学習効果の測定

などが挙げられます。

学校の先生・生徒の声に耳を傾け、教育工学の知見も踏まえ、1つ1つ着実に解決していきたいと考えております!

さいごに

今回紹介したCALEをはじめとして、弊社ではAI系の機能の研究開発を積極的に行なっております。まだまだ課題が山積みなCALEを一緒に進化させていきたい、教育現場の課題を技術で解決したい、などの気持ちがある方はぜひこちらからご連絡ください!

© 2020 Classi Corp.