LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


【インターンレポート】LINEギフトにおけるバッチ処理のKubernetes化とCI改善

はじめに

こんにちは。東京大学大学院情報理工学系研究科修士1年の平岡拓海です。2023年の8/7~9/15の6週間、LINE株式会社の就業型サマーインターンシップに参加しました。期間中、LINEギフトチームに配属され、サーバーサイドエンジニアとして業務に取り組みました。

本インターンでは、主に

  • 一部のバッチ処理をPerlからJavaに書き換え、ジョブをVerda Kubernetes Service (VKS)上で走らせること
  • GitHub ActionsにおけるCI改善

に取り組みました。それぞれを順に説明していきます。

バッチ処理

背景

LINEギフトでは、ログ集計処理や期限が切れたクーポンを削除する処理などをバッチ処理として、一定の期間ごとに走らせています。そのバッチ処理の現状の問題点として

  1. Perlで実装されており、Javaへの移行がまだ進められていない。
  2. それぞれのバッチ処理がロックを取得しながら同期的に、かつ1つのサーバーで実行されているため、あるバッチ処理がメモリを食い潰すとそれ以降のバッチ処理が全て実行できない。
  3. リリース作業時にバッチサーバを再起動する必要があり、バッチ処理の実行とタイミングが衝突することがある。

の3点が挙げられます。上記3点の問題をそれぞれ解消するために

  1. 社内で広く用いられるJavaで実装し直すことで、Javaで既に実装されたLINEギフトモジュールとの相性を良くする。
  2. バッチ処理をVKSのCronJobで管理させることで、バッチ処理で使用するCPU/メモリリソースに対して、動作するWorker Nodeを調整させる。
  3. CronJobの利用により、バッチ起動中においても、バッチを中断することなくリリース作業を行うことができるようにする。

という方法を選択しました。

自分が今回取り組んだバッチ処理内容は、"集計対象の商品に対して、バリエーションごとに配達待ちのオーダーの数をカウントし、ログ集計システムに送信する"処理を10分ごとに行うというものです。

設計

バッチ処理自体は、JavaのSpring Bootアプリケーションとして記述するのですが、それぞれのバッチ処理はKuebernetesのCronJobに管理させます。CronJobは、設定したスケジュールをもとに定期的に処理を実行するよう、それぞれのJobを管理するコンポーネントのことです。Kubernetesの文脈におけるJobは1つ以上のPodを起動させてそれらのPodを管理するコンポーネントのことです。Kubernetes上で動作しているワークロードにあわせて、適切なインスタンスで実行してくれるため、リソースを有効利用することが可能となっています。

LINEではVerdaというプライベートクラウドを運用しており、そのVerda上にKubernetesを導入したVKSも運用しています。今回、そのVKS上でバッチ処理を走らせることにしました。

以下がバッチ処理のシステムフローを表した図です。

図1. バッチ処理のシステムフロー図

実装

既存の実装では、バッチ処理はPerlで書かれていますが、バッチ処理の管理自体はCrontabでLinuxコマンドベースで行われています。以下のようにPerlスクリプトを実行することでバッチ処理が実行されます。

*/10 *    *    *    *   setlock -nx /tmp/lockfile1 ./cron/payment_order_log.pl

*/10 * * * * は、10分ごとに処理を実行することを表しています。また、同じ種類のジョブは同期的に実行される必要があり、setlockコマンドを使うことで引数で指定したファイルにロックをかけ、それに続く引数のコマンドを実行します。その後ロックを解放することで新しく同種類のジョブを実行することが可能となります。

バッチ処理のPerl実装をJavaで実装し直し、バッチ処理を制御するための設定をKubernetesのmanifestを書くことでVKS上で実行できるようにしました。上記のように同期的にバッチ処理を実行する必要があることに関しては、manifest側で concurrencyPolicy: Forbid と設定することで対応できました。

結果

Javaコードとは別にmanifestとしてコードベースでジョブを管理することで、バッチ処理の管理を簡易化しました。また、以下のようにKubernetesのダッシュボードでジョブの状態やログを確認することができるようになり、開発効率の向上に繋がりました。

図2. Kubernetesダッシュボード

新たな問題点

こうしてバッチ処理を実装してPerlと同様の挙動であることを確認できたのですが、作業中にある別の問題に直面しました。それは、CronJobとして起動するDocker Imageのbuildに用いているGitHub Actionsのworkflowの実行時間が遅いという問題です。今までCIをDrone CI上で実行することが多かったのですが、社内で運用されているDrone CIのサポートが終了するため、今回新しくGitHub Actions上でCIを回すことにしました。そのGitHub Actionsのworkflowの内訳としては、

  • generate-project-matrix: プロジェクトごとにmatrixを生成
  • assemble-project: プロジェクトをbuildする
  • check-project: lintチェックの実行
  • test-project: テストの実行
  • publish: Docker Imageのビルドとレジストリへの格納

というものです。以下が実際のGitHub上でのworkflowの様子を表しており、ジョブごとの依存関係がよくわかると思います。

図3. GitHub Actionsにおけるworkflow

このworkflowでは合計17分程度かかっていました。pushするごとにこのworkflowが走るのですが、毎回これほどの時間を要すると開発効率も悪くなってしまいます。そこで私はGitHub ActionsにおけるCI改善のタスクにも取り組みました。

CI改善

背景

現状、LINEギフト内の一部ではCIを回すためにGitHub Actionsを使用しています。またworkflowを実行するためのrunnerにも、VKSを使用しております。私が直面したサーバーサイドのworkflowの実行時間が長いという局所的な問題を解決するだけではなく、ギフト全体のCIにも目を向け、以下の二つの問題点を改善することにしました。

  1. ジョブが立て込むと、workflowが実行途中で落ちてしまうことがある
  2. workflowの実行時間が長い

1について、これが起きる理由の一つは、それぞれのジョブに対して適切なリソース(CPUコア/メモリ)の割り当てができていないことが考えられます。現状、1~3コア、2~6GBでオートスケーリングするPodしか用意しておらず、軽い処理を複数コアで実行してしまい他の処理のためのコアが不足している状況や、重い処理の実行時間が長くなってしまいコアを他の処理に割り当てることができない状況が生じています。

2について、これはバッチ処理の最後でも述べたように、サーバーサイドのGitHub Actions workflowの実行時間が長く開発効率を落としているので改善する必要があります。この実行時間が長い原因は調査の結果以下であることがわかりました。

  • キャッシュが利用されていない
  • Testが並列に実行されていない

設計

1. workflowが落ちる問題

まずworkflowの処理量に対してリソースが足りていない状況であったため、クラスターの増強を行いました。さらにクラスター上で動くKubernetesのPodを複数種類設けることでジョブに応じてPodの種類をworkflow側で指定できるようにしました。具体的には、以下のようなCPU/メモリリソースが異なる4種類のPodを用意しました。

CPU Core
Memory (GiB)
self-hosted-l 6 12
self-hosted-m 2 4
self-hosted-s 1 2
self-hosted-xs 0.5 1

表1. Podの種類ごとのスペック

また細かい設定について、今まではrequest(リソースの最低使用量)とlimit(リソース使用量の上限量)を異なる値にしていました。これは、1種類のPodしか用意していなかったため、ジョブの負荷に応じて自動でリソースの使用量を変更できるようにしたかったからです。この設定の問題点としては、ジョブの実行中にリソース不足で実行が中断されてしまう可能性があるという点とリソース使用量が動的に変化することによってスケジューラのバランシングが難しくなる点などが挙げられます。今回はPodを複数種類用意したことで、動的に使用量を変化させる必要がなくなったため、requestとlimitを同じ値にするようにしました。

2. workflowの実行時間が長い

GitHub Actionsでキャッシュを使う時は、 Cache action を使うことが多いと思います。ただgradleを使うプロジェクトに対しては、 gradle-build-action を使った方がキャッシュを上手く管理してくれるのです。 gradle-build-action はgradleのバージョンを指定してプロジェクトのビルドを実行したり、キャッシュを管理することが可能です。今までもビルドにこのactionを使っていたのですが、キャッシュを利用するために Cache action も利用していました。つまりキャッシュを管理するアクションが2つある状況で、その2つがコンフリクトを起こしていました。実際、公式READMEにもこの2つの同時使用は避けるようにと書かれていました。そのため Cache action の記述を消すことで対応しました。

またTestの並列実行を許可するために、build.gradleファイルでmaxParallelForksの値を設定しました。

さらには1のクラスター増強に関連して、Testのような負荷の大きな処理は高いスペックのPodに割り当て、matrixを生成するような負荷の小さい処理は低いスペックのPodに割り当てることで、実行時間の短縮だけではなく、クラスターの利用効率の向上を実現しました。

結果

最終的に、17分かかっていたサーバーサイドのworkflowの実行時間を12分にまで短縮することができました。実際に実行してみて、ジョブの並列度の向上が時間短縮に大きく貢献しているように感じました。負荷に応じて適切にPodを割り当てることができているため、サーバーサイドのTestのようなある程度タスク数の多い処理には、多くのPodを割り当てることができ、ジョブの並列度を高めることができていました。またクラスターの利用効率が高くなったため、ほぼ必ずworkflowにPodが割り当てられるようになり、workflowが実行できないというようなことはあまり生じなくなりました。

以下が複数種類Podに処理が割り振られている様子です。

図4. 複数種類のPodが起動している様子

開発を通じて得られた気づき

今回、初めてこのような大規模なプロダクト開発に携わることができて、普段の開発では経験できないようなことを色々経験させていただきました。

自分はドメインの変更に対応するタスクも別で行ったのですが、その実装としては該当箇所のURLを変更するだけで簡単です。ただ、その変更によりどれくらい影響範囲が出るかなども調査する必要がありました。これはそのURL変更により、他で想定外の挙動を示してしまうことを防ぐためです。同様の理由でテストもしっかり実装されている必要がありました。このように多くのユーザーに使用され信頼されているサービスであるからこそ、慎重に開発が進められていると感じました。長い工程を経て、自分が変更した箇所がリリースされ、それを自分のスマートフォンのLINEギフトから確認できた時は大きな達成感を感じられました。

また、大規模なプロダクトであるがゆえにチームも細分化されており、チームの人数が少ないので個人の裁量が大きかったのが印象的でした。人数が少ないため、自分のやりたいタスクに積極的に挑戦でき、教え合いが生じやすい環境は、エンジニアとしての成長に適していると感じました。さらにチーム間での情報共有も頻繁に行っており、それぞれのチームが何をやっているかを把握しやすく、プロダクト開発がうまく回っているように感じました。

おわりに

バッチ処理やCI改善のタスクを通して、今まであまり馴染みのなかったKubernetesやGitHub Actionsなどの技術を深いところまで知り、実務として実装に取り組めたのは良い経験でした。サーバーサイドエンジニアとしてインフラ・DevOpsの技術はこれからも深めていきたいです。

自分はインターン生の中では珍しく、積極的に出社をしており、新卒の方々を中心にお話を伺う機会を多く設けてもらっていました。その中で、エンジニアも含めコミュニケーションをとりやすい方が多いと感じました。エンジニアは技術力が高いことももちろん大切ですが、チーム開発をしていく以上、コミュニケーションに無駄なコストをかけずに開発を進めることはさらに大切です。一緒に働きたいと思えるくらいにコミュニケーションがとりやすい方が多かったのが印象的でした。

また社員の多くに感じたのは、自分が携わるプロダクトに対して自信と誇りを持っているということです。同僚に自分の携わるプロダクトの説明をして懸命にユーザーになってもらうおうとする姿や自分が直接的に関わった機能を自信ありげに説明する姿は、とても素敵だと感じました。多くのユーザーに使ってもらって利益を生むという最終的な目標がありますが、それ以前に素直に自分のプロダクトを好きと言えるのは大事なことだと思います。

最後にこのインターンの中で多大なるサポートをしていただいた名田さんと菅家さんに感謝したいです。名田さんには、主に技術的なサポートを多くしていただき、技術への深い理解に繋がりました。また、柔らかい雰囲気でありながらタスクを凄まじいスピードでこなす姿は自分の目指すエンジニア像そのものでした。そして自分のメンターとして付いてくださった菅家さんは、オンボーディングから私のタスクの補助、LINEの文化を知る機会の提供などたくさんのことをしてくださりました。おかげ様でインターン中は何も不自由なくタスクに集中することができました。一つ一つの仕事を高い精度でこなすことや周りの人に最大限還元しようとする姿勢は、プロダクトを間違いなく良い方向に導いてくれるので、自分も見習う必要があると感じました。その他、多くの方がインターン期間中に自分に関わってくださり、充実したインターン生活を送ることができました。ありがとうございました!