楽楽勤怠開発のバックエンドエンジニア

今野裕介氏(以下、今野):はい。では「新機能開発のアーキテクチャ選定」ということで、私、今野のほうから発表を始めたいと思います。よろしくお願いします。では自己紹介からですね。今野と申します。楽楽勤怠開発課に所属していまして、役職としてはバックエンドエンジニア。要するにサーバーサイドのプログラムを作るおじさんです。

実を言うと、入社が2020年7月1日で、ちょうど面接を受けたのが最初の緊急事態宣言のちょっと手前ぐらいだったという、滑り込みした感じの、まだ新入りです。

前職ではレコメンドエンジンという「あなたにおすすめの商品はこれです」みたいな、そういったもののASPサービスの設計や開発、運用、その他諸々全部やっていました。

現在、楽楽勤怠の打刻機能をメインに新機能の開発を担当しています。ちょっとパーソナルな情報ですが、趣味はカメラや城巡りです。

楽楽勤怠の新機能

まずは楽楽勤怠の紹介をします。一番新しい楽楽シリーズのサービスで、クラウド型の勤怠管理サービスです。2020年の10月に初期リリースをしまして、その後ほぼ毎月、新機能をリリースしています。ぜひよろしくお願いします。

で、今回お話ししたいのは、楽楽勤怠におけるICカード打刻機能の話になります。こちらが、2021年2月16日にリリースした機能になりまして。こちらは専用のPitTouchPro2という端末を利用して、ICカード打刻を実現しています。

基本的に楽楽勤怠は、ブラウザーでサイトにアクセスしてもらって、ログインして、ブラウザーで出勤や退勤といった打刻をしますが、この端末を使うと、その必要がなく、専用端末を使うだけで打刻できます。

これは主に工場や、あとは店舗など、PCが持ち込めないような環境でも、インターネットにさえつながれば打刻できる代物になります。端末自体は、弊社から販売したもののみが今のところ利用可能な運用となっています。

PitTouchPro2の概要

では次に、PitTouchPro2の概要をさらっと説明します。打刻ですね。出退勤・休憩・外出とか、あとはカードの紐づけと削除ができます。専用の端末から内部では組み込みのSafariが動いているっぽくて、そこからXHRのLevel2でリクエストしているっぽい感じになっています。

もしこの端末が接続しているネットワークに問題がある場合は、その状態で打刻しても楽楽勤怠のほうに届かないので、その端末内部に、打刻の情報を保持しておいて、ネットワークの問題が解消したら再送するというような機能が存在しています。対応しているICカードはFeliCaとMifareです。

こちら公式の画像から拝借してきたのですが、端末はだいたいこんな外観になっています。ちょっとのっぺりとしているのですが、真ん中の青っぽいところにカードをかざすと打刻できます。こんな感じの端末を使っています。有線のLANをつなぐ場合は、後ろの端子にザクッとこうケーブルを入れて、使っていく感じです。

ICカード打刻の要件整理

では次に、楽楽勤怠におけるICカード打刻の要件を説明します。最初に、これは当然ですが、PitTouchPro2の端末を使って出勤と退勤の打刻ができます。

次に、この端末経由でカードの登録と削除ができる機能です。その登録の削除の際には、端末上から、例えば私であれば、「今野」という従業員のコードを入れることによって、その従業員の情報を参照してから、登録や削除をする機能があります。

あと、これは最後が一番重要なポイントなのですが、楽楽勤怠がバージョンアップする際に、もし不慮の事故で何かダウンしているような状態であっても、PitTouchから送られたデータ、打刻データが失われないのが非常に重要なポイントです。

打刻データの性質

この要件を踏まえた上で、アーキテクチャを新しく作っていくわけですが、まずその前に、打刻データの性質をいったん簡単に定義すると、まず打刻データ自体は、エンドユーザーの行動ログの1つだと考えられます。あるAさんが、出勤打刻を10時にした、退勤打刻を19時にしたなど、そういった行動ログの1つであると。

そして重要な性質として、データは失われてはいけません。そもそも失われていいデータがこの世にどれだけあるのかというのは、ちょっと微妙なところではありますが、データが失われるとどうなるかを、まず整頓して考えます。

もし、本来10時に打刻をしたはずなのに、そのデータが失われてしまうと、出勤の打刻が登録されないので、そのデータが失われたエンドユーザーは、勤怠時刻の申請という、打刻の修正作業が必要になってしまいます。それが頻繁に行われてしまうと「そもそも楽楽勤怠の信頼性ってどうなの?」という話が出てきてしまうと。なので、データが絶対に失われないような機構が必要になってきます。

そして、もう1つ重要な性質があって。もし同じ時刻の打刻が重複したとしても、大きな問題がないというところですね。これは比較して言うと、例えばECの販売データとか、売り上げのデータですね。例えば時計が1個売れたっていうトランザクションがあったとして、それが二重に登録されると、売り上げのデータがずれるので大きな問題になってしまいますが。

打刻に関して言えば、少なくとも出勤打刻が10時というのが何回あったとしても、勤怠計算をする上では、特に影響がないわけです。なぜなら、出勤した時刻と退勤した時刻がわかっていればいいだけなので、同じ時刻の打刻が何回あったとしても、別にその期間は特に変わりはないはずです。

なので計算上は、大きな影響がないのが特徴になります。ただ、だからといって同じ重複データがいっぱいあると、データの量は増えるし、その繰り返し打たれる分だけ無駄なネットワーク消費にもなるので、パフォーマンス的な問題は当然生じてしまいます。

新アーキテクチャ図

先ほどの要件とこの性質を踏まえた上で、非常にざっくりとしたアーキテクチャ図を描いてみました。

一番右側の青いところこちらが勤怠のブラウザーでアクセスしてもらうインフラ系統が載っているところです。サービスが入っているところですね。

今回その楽楽勤怠本体が稼働している場所とは別に、このICカード打刻を専用に受け付けるアプリケーションとインフラ群を構築しました。これが真ん中にあるオレンジのゾーンですね。打刻アプリケーションと書いてあるところです。

PitTouchPro2の端末は、ここに対してリクエストを行うようなかたちになります。で、例えばPitTouchPro2で出勤打刻をすると、1番のところ、矢印で打刻アプリケーションにリクエストが飛ぶと。打刻アプリケーションは、このリクエストを受け付けたら何をするかというと、この右側の楽楽勤怠に、そのデータをいろいろ整形した上で連携します。こちらいずれもHTTPSで連携をしています。

この楽楽勤怠が返したレスポンスをもとに、いったんこの3番は飛ばして、4番で打刻アプリケーションがPitTouchPro2に応答を返すと、PitTouchPro2の端末のその液晶上で「打刻を受け付けました」であったり、あとは「登録されてないカードです」「そんな従業員いません」といった感じのエラーメッセージを出したりしています。

通常はこの1、2、4という流れなんですが、もしこの2番が万が一ダメになってしまった場合のことを考えなければいけないと。それで、このときに打刻データをいったん保持していく場所を用意しました。これが真ん中の「RabbitMQ」というところのメッセージキューになります。

もし2番のほうで通信ができなかったり、メンテナンス中ということを打刻アプリケーションが検知した場合は、3番の処理を実行して、RabbitMQのほうに打刻データを詰める作業をします。

打刻データがこのRabbitMQに詰まった場合は、別途またこのRabbitMQに入ったデータを刈り取るバッチ、バッチといっても常時起動しているプロセスにはなるのですが、こちらを別途用意しておいて、非同期で楽楽勤怠が復帰した時に打刻データを入れるかたちで同期する仕組みになっています。こんなような感じで、緊急時にはRabbitMQを経由することでデータが飛ばないように担保しています。

RabbitMQを採用した理由

まずRabbitMQのお話です。こちらは打刻データを保存するために採用しました。ラクスの社内でRabbitMQを他にも使っているサービスがあるということで、採用の1つのポイントになったかなというところがあります。

あとはRabbitMQ、MQのミドルウェアは数がいっぱいあるのですが、非常に公式のドキュメントが豊富で、だいたい公式の、英語ではあるんですけども、ドキュメントを読んでいれば、一通り基本的な構築や、あとはクライアント側の実装のほうも簡単にできちゃうような、それぐらい情報量が豊富でした。現在勤怠で使っているバージョンのほうが3.8系、一応最新の系統になります。

RabbitMQ自体はErlangのVM上で動くプロダクトなのですけども、このRabbitMQの開発元がErlangのVMの本も出していて、これなんか『Zero-dependency Erlang』っていうやつですね。こちらを採用してます。

あとこちらですね。このRabbitMQ、図の上では1個しかないように見えますが、クラスター構成になっているので、複数のノードで構成されています。そして、フェイルオーバーするようなかたちになっています。

Javaのライブラリ「Resilience4j」の採用

次に、ただこのRabbitMQを使っているだけだと、一応緊急時の経路ができただけなので、それだけでは足りないよねということで、今度フェイルオーバーの仕組みをもう1つ付け加えることにしました。

こちらがResilience4jという、Javaのライブラリの採用です。楽楽勤怠自体、Javaがメインで使われていますが、その中でフォールトトレラント用のライブラリが何かないかなというところで、こちらを採用することにしました。

これ、けっこういろいろなことができて。サーキットブレーカーパターンの実現や、リトライ、あと流量制御。キャッシュなんかもできますかね。今回は全部盛りにしても意味がないので、いっぱいある中でも絞って採用しました。サーキットブレーカーとリトライですね。ちょっと次の図で説明をします。

さっきの図とほぼ同じなんですが、まず青いところからですね。リトライは特に説明をする必要はないと思うのですが、複数回失敗してもやり直すところですね。この3番とBのところ。3番が打刻アプリケーションからRabbitMQに対して、打刻データを入れるところ。ここで万が一失敗してしまうと、本当に最後の砦のためアウトになってしまうので、必ずリトライをするようにしました。

あとはそのMQに入ったデータを刈り取った後ですね。同期バッチが、楽楽勤怠の本体のほうに入れる時にも、リトライを入れてあります。複数回やり直すところですね。

サーキットブレーカーとリトライ

そして、この2番のところ。こちらがサーキットブレーカーとリトライ、両方とも入れています。サーキットブレーカーは、ざっくり言ってしまうと、何回か失敗が記録された場合に、強制的に「そこは落ちているんだ」と考えて、この2番のリクエスト自体をしないで「エラーが起きているよ」というのを迅速に返しちゃう、そういった機構です。

ここは、楽楽勤怠自体がメンテナンスしている場合は動いていないことが確定なので、その場合に何度も何度もリクエストを送るとむだでしかないため、あらかじめ切ってしまって応答を早めるような機構になります。

そしてこの2番については、そのサーキットブレーカーとリトライのパターン両方とも適応することで、フェイルオーバーを考えています。

実際、これらを実現してみて、感想にはなるのですが。図をちょっと出すのも恥ずかしいぐらいシンプルな構成になっていますが、本当に実現すべきこと自体は単純です。ただ、結局そこで事故った時が一番大事になってしまうので、そこをどこまで想定するか、どこまでやり込むかっていうところで、そのバランスが非常に苦労したところですね。

この時に、今回で言えばRabbitMQや、Resilience4jによるリトライ、サーキットブレーカーパターンの実現ということで、落ち着くことになりました。

もちろん正常系のテストは、パターンに「あ、通ったね」だけでいいのですが、MQを介していたり、あとサーキットブレーカーがちゃんと発動したかどうかをチェックする時に、何度も何度も失敗をさせなきゃいけないので、そのあたりの異常系のテストは非常に大変でした。

あとは情報量が、RabbitMQは公式のドキュメントや、英語の文献は非常にあったので、すんなりといけたのですが、逆にResilience4jはほとんどなくて。日本語の文献は、ほぼ「やってみました」で終わっているのしかなくて、公式のサンプルだと一部動かないやつがあって、大変つらかったなと。そこはゴリ押しでなんとか検証して、適応に漕ぎ着けたようなところになります。

こちらで今回、私からの発表は以上です。ご清聴ありがとうございました。

質疑応答

司会者1:ありがとうございます。ではみなさんのコメントを拾っていきますね。

司会者2:今、コメントもチャットのほうからいただいていまして。理論上可能なら、すべてのデータは積み上げ式になっていてほしいよなと。アップデートするのではなくて、というの意見。

それから、カード情報と従業員情報はどこで紐づけしているのか気になる、という感じで。このあたりがきていましたけど、今野さん、いかがですか。

今野:そうですね。そのカード情報を紐づける時に、まずこのPitTouchPro2の端末上からカード登録という機能があって。その際に従業員のコードを入れて、従業員がいるかどうかをまず参照できるんですね。そこで、まず従業員の情報を引っ張ってきます。そこで「この人ですか」というのが合っているのを確認したら、カードをかざすことでカードを紐づけることができるかたちになっています。

司会者1:Webの管理画面で、先にICカードのなんか番号みたいなのと、例えば私だったら、司会者1なので、司会者1は1・2・3・4・5・6・7・8・9・10みたいなものを、先にWebの管理画面で登録しておくっていうことですかね。

今野:そちらも用意して。端末からやるのと、あとはブラウザーでログインして、CSVのインポートにはなりますが、一括で大量の人数インポートする時には、そちらで対応するかたちになります。

司会者2:Twitterもですね。これはどちらかというと共感という部分もありますが、打刻データは難しそうだなというところと、打刻を絶対保存したい部分だけマイクロサービス化させるのがいいな、というようなことですね。ツイートありがとうございます。

司会者1:打刻であれば、初めからRabbitMQ経由での記録処理だけでいいと思うんですが、なぜ同期処理も、2つのルートを用意しているんですか、という質問があります。

今野:はい、もう本当におっしゃるとおりで。理想的なことを言うと、打刻アプリケーションは勤怠の本体にリクエスト飛ばさないで、MQに投げて、あとはもう知らんというふうにやりたかったのですが、実際に、打刻アプリケーションは集計する機能しかないところなので、実際の打刻データを保存するのは、楽楽勤怠の本体になります。

あと、PitTouchPro2上に応答を返さなければいけません。ちゃんと打刻が保存されたのかや、もう本当にだめだったかのを、応答をしっかり返さなきゃいけないので、ちょっと同期処理にせざるを得なかったというのが、ちょっと妥協した点というか、端末の制約上やらざるを得なかったところはあります。

司会者1:ああ、そっか。1回、元のタイミングに。チャンスが2回あるというか、ということなのですかね。RabbitMQに送るだけだったら送って終わりになっちゃうんで、もしRabbitMQが死んでいたら、それはそれで終わっちゃいますよね。

今野:そうですね。MQから刈り取った先でエラーになっていたとしても、それを検知することはできないので。なので、やらなきゃいけなかったんです。本当は、打刻アプリケーションとしては、スケールアウトしたいので、その投げっぱなし打ちっぱなしで終わりたいんですけど、応答としては「打刻が登録されたかもしれません」と返すわけにはいけないので、どうしても同期処理でやらざるを得なかった感じですね。

司会者1:RabbitMQは、まあまあ最後の手段というか、そっちのほうが保険ということですね。

今野:そうですね、保険です。基本的にはそういったことになります。

司会者2:次の質問です。データ同期の際に、どっちのデータが正しいのかの判断はどのように行っていますか。

今野:なるほど…?

司会者1:アレかな。ちょっと前のページの図で、2の処理がうまくいったら、3は発動しないじゃないですか。

今野:はい、その通りですね。なので、基本的には重複はないはずですね。先ほど打刻データの性質のところで重複の話をしたので、ちょっとそのあたりとこんがらがっちゃったかもしれないのですが。基本的に2で成功したら3はないし、逆に言うと3がやられているということは、2で成功していることはあり得ないです、はい。

司会者1:次の質問です。「従業員は何人ぐらいを想定しているでしょうか」。

今野:そうですね。ちょっと詳細な数字がどれだけ出せるのかは微妙なところですが、打刻アプリケーション的には、一応ここ、この真ん中のオレンジのところがいくらでもスケールアウトする仕組みにはなっているので。なので、基本的にはどんとこいっていうようなかんじですね。

司会者2:どんとこい(笑)。ずどんとこいくらいですかね。

今野:そういったところですね。

司会者1:「RabbitMQ経由でエラーデータが渡されるなど、エラーになった場合は何か対処されていますでしょうか。」

今野:はい。そうですね。例えばRabbitMQで(エラーが)いっちゃった時に、登録されていないカードでやってしまうと、本来「そんなカードは登録されていません」みたいなことが起きるのですが。それが、2番が発動した場合はすぐに4で返せるのですが、この同期待ち経由になった場合に、そのエラーが発生した場合には、エラーログが管理画面で見られるようになっていまして、この「何時何分に登録されていないカードから打刻がありました」というような機能を用意してあります。なので、そちらで実際エラーが起きている場合には確認できます。

司会者2:「RabbitMQ経由で非同期になると思いますが、その時のレスポンスや、完了orエラーはどのように端末に返していますか」という質問が。

今野:そうですね。この3番が発動した時に4番という話だと思いますが。こちら、3に入ったことが確定しているのであれば、ちょっとその先のことは見えないので、うまくいかなかったかもしれないといった、ちょっと濁したメッセージが出るようにはなっているので、ちょっとあやふやなかたちのレスポンスしかできない、失敗する可能性もあります、ということですね。

司会者2:含んでいるということですね。はい、わかりました。ありがとうございます。