メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

数億データを処理する仕組みを提供する gem 『MedPipe』 を OSS として公開しました

こんにちは。サーバーエンジニアの佐藤太一(@teach_kaiju)です。
本記事では社内で開発した、数億のデータを処理する仕組みを提供する gem MedPipe を紹介します。

MedPipe とは

「Log のデータを全て取得し、フォーマットして tsv として S3 にアップロードする」という要件があったとします。
この要件を実現するために、例えば以下のような実装を考えることができます。

upload_file_name = "hoge_logs.csv"
# 1. S3にアップロードするための file を用意
Tempfile.create do |file|
  # 2. Log のデータを DB から取得
  HogeLog.find_each do |log|
    # 3. フォーマット処理
    formatted_data = format(log)
    # 4. ファイルに書き込み
    line = CSV.generate_line(formatted_data, col_sep: "\t")
    file.puts(line)
  end

  # 5. S3にアップロード
  upload_s3(file, upload_file_name)
end

def format(log)
  # 処理
end

def upload_s3(file, upload_file_name)
  # 処理
end

それに対して、MedPipe を使うと以下のように記述できます。

upload_file_name = "hoge_logs.csv"
pipeline = MedPipe::Pipeline.new
pipeline.apply(PipelineTask::HogeLogReader.new) # 1. Log のデータを DB から取得
        .apply(PipelineTask::HogeLogFormatter.new) # 2. フォーマット処理
        .apply(MedPipe::PipelineTask::TsvGenerater.new) # 3. ファイルに書き込み
        .apply(PipelineTask::S3Uploader.new(upload_file_name)) # 4. S3にアップロード
pipeline.run

このように、MedPipe を使うことで処理の流れが明確になり、可読性を向上させることができます。

それに加えて以下のような機能を容易に実装することができます。

  • 並列処理
  • クエリ最適化のための、in_batches を用いない独自データ取得処理
  • 件数のカウント
  • アップロードするファイルサイズの保存

Ruby エンジニアにとっては Dataflow 等の大規模データ処理ツールと比べて学習コストが低いため、導入を比較的容易に行うことができます。

コンセプト

MedPipe Concept

MedPipe では Pipeline に PipelineTask を登録し、それを順番に実行します。
PipelineTask はやりたいことそのものであるため、独自で実装する必要があります。
PipelineTask が実装する必要のあるメソッドは call のみで非常にシンプルです。

def call(context, prev_result)
  yield "次のTaskの第二引数に渡す値"
end

ただし、大量のデータを扱う際には全部のデータをメモリにのせて次の Task に渡すわけにはいきません。
そこで、基本的には Enumerable::Lazy を後続 Task に渡します。
(lazy で Enumerable を Enumerable::Lazy に変換できます)

例

def call(_context, _)
  yield HogeLog.find_each.lazy
end

後続 Task は Enumerable::Lazy を受け取り、map で処理を挟むことで Enumerable::Lazy を維持できます。

  def call(_context, records)
    yield records.map { |record| format_line(record) }
  end

PipelineTask の他にも PipelineTask を Pipeline に登録する処理など使う準備は必要です。

Usageとサンプルを参考にしてください。

DB からのデータ取得方法

実務で find_each を使う場合には 2 つの問題がありました。

  1. ActiveRecord のメモリ使用量が多い
  2. クエリが最適化されない

1 に関しては in_batches + pluck を使うことで解決できますが、2 に関しては解決できません。
参考: Railsでin_batches使うととても遅い

これを解決するために、MedPipe では BatchReader というクラスを開発しました。

使用例:

  def call(_context, _)
    yield MedPipe::BatchReader.new(
      HogeLog,
      scope: HogeLog.where(created_at: @target_date.all_day),
      pluck_columns:,
      batch_size: BATCH_SIZE
    ).each.lazy
  end

これによって find_each のように1件ずつ、pluck_columns で pluck されたデータを後続 Task に渡すことができます。

プロファイリングの仕方

実務では memory_profiler を用いて、以下のようなコードでプロファイリングを行いました。 ※ 執筆にあたり一部修正しています。

module Profiler
  class << self
...
    def report(&block)
      start_time = Time.current
      result = MemoryProfiler.report(&block)
      elapsed_time = Time.current - start_time

      puts "\n\n===== Profiler Report ====="
      puts "Total allocated: #{bytes_to_mb(result.total_allocated_memsize)} MB (#{result.total_allocated} objects)"
      puts "Total retained: #{bytes_to_mb(result.total_retained_memsize)} MB (#{result.total_retained} objects)"
      puts "Elapsed time: #{elapsed_time.round(2)} sec"
    end
...
    private

...
    # bytes to MB 小数点第二位まで
    def bytes_to_mb(bytes)
      (bytes / 1024.0 / 1024.0).round(2)
    end
  end
end
class PipelineTask::Profiler
  def call(_context, input)
    Profiler.report do
      # Lazy の場合、測定するために発火する
      input.force if input.is_a?(Enumerator::Lazy)

      yield(input)
    end
  end
end
pipeline.apply(PipelineTask::Profiler.new)

既存のスクリプトを修正することなく、プロファイリングを行うことができます。

おわりに

本記事では、MedPipe の紹介を行いました。本 gem は弊社初のオープンソースの gem です。
普段様々な OSS のお世話になっているため、提供する側として業界に貢献できることを嬉しく思います。
OSS として世に出すことを許可していただいた会社や一緒に開発した同僚の近藤さん(@tetetratra)に感謝です!
実装が参考になったり、使ってみてよかった場合は、ぜひ MedPipe の GitHub リポジトリにスターをいただけると励みになります。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

ActiveRecord クエリキャッシュのメモリ使用量と無効化

こんにちは。サーバーエンジニアの佐藤太一(@teach_kaiju)です。
本記事では、クエリキャッシュのメモリ使用量と有効/無効の切り替え方法について紹介します。

クエリキャッシュとは

Active Recordのクエリキャッシュは、1つのリクエストまたはジョブの実行中に同じSQLクエリが複数回実行された場合、2回目以降のクエリの実行を省略し、最初の結果をメモリ上にキャッシュして再利用する機能です。

# 1回目のクエリ実行時
Book.first
# Book Load (2.9ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1

# 2回目のクエリ実行時
Book.first
# CACHE Book Load (0.1ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1

※ 本記事で用いるモデル名(Book)は執筆にあたって差し替えたものであり、実際に使用したモデル名とは異なります。

キャッシュが使用される場合には発行されたクエリのログの先頭にCACHEと付いています。

2024/11 時点で、Sidekiq および SolidQueue ではクエリキャッシュがデフォルトで有効になっていること、 Rails コンソールでは無効になっていることを確認しました。

クエリキャッシュのメモリ消費量

クエリキャッシュはその性質上、大量のレコードを扱うジョブではメモリ使用量が膨大になりえます。

では、クエリキャッシュで実際どの程度メモリが圧迫されるのでしょうか? memory_profiler を用いて計測しました。 ※ モデル名は実際のものから差し替えています。

計測用コード

# 渡されたブロック内のメモリ消費および時間を出力
def report(&block)
  start_time = Time.current
  result = MemoryProfiler.report(&block)
  elapsed_time = Time.current - start_time

  puts "\n\n===== Profiler Report ====="
  puts "Total allocated: #{bytes_to_mb(result.total_allocated_memsize)} MB (#{result.total_allocated} objects)"
  puts "Total retained: #{bytes_to_mb(result.total_retained_memsize)} MB (#{result.total_retained} objects)"
  puts "Elapsed time: #{elapsed_time.round(2)} sec"
  puts "Query cache: #{Book.connection.query_cache.size} queries"
end

# bytes to MB 小数点第二位まで
def bytes_to_mb(bytes)
  (bytes / 1024.0 / 1024.0).round(2)
end

結果

データ数: 50 万
取得カラム: 数値と日付、合計 6 つ
実装: batch_size 1 万で上記のデータを取得する
※ find_each 等クエリキャッシュをスキップするメソッドは使用しません(後述)

クエリキャッシュ無効 クエリキャッシュ有効
Total allocated 281.03 MB (3510676 objects) 272.02 MB (3512618 objects)
Total retained 3.83 MB (71 objects) 99.24 MB (1500823 objects)
Query cache: 56 queries
Elapsed time 17.46 sec 20.83 sec

考察

allocated の差は誤差です。クエリキャッシュの有効/無効でアロケーション数はそんなに変わらないでしょう。
retained (使用中のメモリ) はキャッシュ分大幅に増加しています。

状況によって大きく差が出るため参考程度ですが、 50 万のデータでおよそ 100MB 程度のメモリを確保することがわかりました。
時間は誤差かもしれませんが、クエリキャッシュが無効なほうが少し高速なようです。

もし batch_size が 1 万ではなく 1000 であれば実行するクエリの数は 500 を超えます。Rails 7.1 以上であればクエリキャッシュの数の制限 (default 100) を超えるため、その分 retained は大幅に減少するでしょう。

find_each 等ではクエリキャッシュが無効になる

find_each、find_in_batchesそしてin_batchesではクエリキャッシュが無効になります。

def batch_on_unloaded_relation(relation:, start:, finish:, load:, cursor:, order:, use_ranges:, remaining:, batch_limit:)
...
  relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching

batches.rb

したがって、バッチ処理で上記メソッドを使用する分にはクエリキャッシュを気にする必要はあまりありません。 最適化のために上記メソッドを使わずにバッチ処理を行うときに気をつける必要があります。

クエリキャッシュを無効化する方法

ActiveRecordのモデル.uncachedを使うのがおすすめです。ドキュメント

Model.uncached do
  # この中ではクエリキャッシュが無効になる
end

ActiveRecordのモデル.uncachedを使うと、リードレプリカ等の別DBを参照した場合でもクエリキャッシュを無効化することができます。

切り替え検証

※ Rails コンソールではキャッシュがデフォルト無効なため、無効 -> 有効の切り替えを行なっています

# 通常 (Rails コンソールのためキャッシュがデフォルト無効)
Book.first
Book.first
#  Book Load (2.2ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
#  Book Load (0.4ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1

# キャッシュを有効化
ActiveRecord::Base.cache do
  Book.first
  Book.first
end
#  Book Load (2.9ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
#  CACHE Book Load (0.1ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1

# 別DBに接続 (キャッシュが有効にならない)
ApplicationRecord.connected_to(role: :primary_replica) do
  ActiveRecord::Base.cache do
    Book.first
    Book.first
  end
end
#  Book Load (3.9ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
#  Book Load (1.9ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1

# 別DBでキャッシュを有効化する
ApplicationRecord.connected_to(role: :primary_replica) do
  Book.cache do
    Book.first
    Book.first
  end
end
#  Book Load (2.8ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
#  CACHE Book Load (0.2ms)  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1

本検証ではログにCACHEと付くかどうかを見ていますが、以下でも確認可能です

Book.connection.query_cache_enabled

おわりに

本記事では ActiveRecord のクエリキャッシュについて紹介しました。メモリ使用量が気になる方は、ぜひクエリキャッシュの無効化を検討してみてください。その際に本記事の内容が参考になれば幸いです。

参考文献

Sidekiq: Problems and Troubleshooting

ShakaCode: Rails 7.1 makes ActiveRecord query cache an LRU


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Vue Fes Japan 2024 After Meetupを開催しました!

こんにちは!メドピアの福田(@Yusa136)です。

2024年11月01日(金)に弊社オフィスにて、MNTSQ株式会社、STORES株式会社と3社でVue Fes Japan 2024 After Meetupを合同開催しました!

Vue Fes Japan 2024 の感想や思い出を語り合いました。この記事では当日の様子やセッションの内容をお届けします。

LT

「VitePressで見つけたアウトプット習慣」

トップバッターは弊社メドピアの岡澤さんです!

VitePressを活用したアウトプット習慣の作り方についての話が印象的でした。「飽き性だけど続けられる」という岡澤さんの言葉の示す通り、VitePressはフロントエンドだけでブログやドキュメントを簡単に作成でき、Vue.jsを使って気軽に情報発信ができるツールです。

なぜVitePressなのか?

岡澤さんがあげた理由は以下の通りです。

  • Markdownで簡単に書ける点
  • フロントエンドで完結
  • Vueチーム推奨のSSG
  • Vueで遊びたい!

VitePressはファイルベースルーティングを採用しています、必要なパッケージも少なく、設定がシンプルな点も良い点として挙げられていました。また、簡単にダークテーマも切り替えられるなど、カスタマイズの自由度も魅力的ですね!

私としてもアウトプットできる場を求めていたので、VitePressを通してVue.jsを学び直すのも良いと思いました!みなさんもぜひこの機会にVitePress触ってみてはいかがでしょうか?

「ReactからVueへの転向:思考の変化とアプローチの違い」

speakerdeck.com

続いては、MNTSQ株式会社の森山凪さんのLTでした!

ReactとVue.jsのライフサイクル表現の変化についてのお話が印象的でした!両者のアプローチの違いに注目してお話しされています。

1. ライフサイクルの違い

Reactのライフサイクル

useEffectを使うと、更新(update)、マウント(mount)、アンマウント(unmount)の処理をまとめて管理できます。例えば初回のマウント時や、ある状態が更新されたときにAPIコールを行うといったことが可能です。この凝集性の高さから、関連する処理を一つのuseEffect内に完結させやすいのが特徴みたいです。 useEffectは再利用しやすいことが大きなメリットですね!

メリットが大きいのは理解できましたが、やはりuseEffectは難しいですね。。。 森山さんも「lifecycleを理解している人でないと(useEffect)難しい」とおっしゃってしました。

2. Vue.jsのライフサイクルフック

一方でライフサイクルフックはどうでしょうか?

Vue.jsではmountedやbeforeUnmountといったフックで、それぞれのタイミングに応じた処理を明確に分けています。機能が時間ごとに分かれるため、コードの流れが直感的に理解しやすいのが利点みたいです!

ReactとVue.jsは機能的な凝集性と時間的な凝集性で異なるみたいです。

Vue.jsでも機能的な凝集性が欲しい場面が出てくると思います。そこでVue.jsのComposableでライフサイクルフックを使うと機能的な凝集性が高いライフサイクルフックが実装できる「いいとこどり!」だと紹介されていました。

そのほかにも、ReactのJSXとVue.jsのSFCについてどちらも「関心の分離」を重視していることをお話しされていました!

「1つのtsxコンポーネントをVueとReact向けにビルドする」

vue-fes-after-meetup-2024-ushironokos-projects.vercel.app

最後のLTはSTORES株式会社のushironokoさんです!

Vue.jsとReactの両方に対応できるtsxコンポーネントについて話されました!TypeScriptで書いたコンポーネントをどちらのフレームワークでも活用できる点が魅力的ですね

tsxとは?

TypeScriptを使ってテンプレートを定義できるファイル形式で、tsxだけではコンポーネントに状態を保つことはできないみたいです。

tsx では Vue.js も書くことができ、Babelのプラグインを用いることでtsxに変換できることを紹介されていました。 レンダー関数と JSX | Vue.js

Reactでは独自にtsxを解釈、ReactのランタイムでJSを生成するとお話しされていました。最終的にはcreateElementになりますが、オプションを組み合わせることで、ビルド後のjsx構文を誰に・どのように解釈させるかを指示できるらしいです。

tsxからVue.jsとReact向けにビルドする

状態管理が不要な部分はtsxのみで対応し、工夫が必要な部分については「unbuild」などのビルドツールで補完すると良いみたいです。

stateを持たせるとReactかVue.jsのランタイムに依存してしまうようです。なので、stateを持たなければ、ReactとVue.jsで使えるコンポーネントになるみたいです。

Vue.jsには固有の問題と解決方法について紹介されていました。

  • esbuildのオプションでtsconfigを拡張し、tsx:"preserve",jsxFactory: "h"を追加する
  • hが参照エラーにならないようにビルド結果にimport{ h }from "vue";を追加する
  • 元コードがClassNameを用いている場合は、classに変換する。

VueとReactのどちらのエコシステムでも活用できるtsxコンポーネントの可能性が広がるお話でした。

パネルディスカッション

「Vue Fes LGTM」

登壇者のご紹介

  • メドピア株式会社:小林さん
  • STORES株式会社:ushironokoさん
  • MNTSQ株式会社:安積洋さん

最後のパネルディスカッションでは、小林さんが司会役としてVueのマイグレーションや最新トレンドについてざっくばらんに話し 合いました。今年は新しいことにチャレンジする前向きな話が多く、昨年に比べてバランス良く知見が共有された印象でした。

議論のハイライト

マイグレーションと技術負債の解消

まず話題に上ったのは、Vueのマイグレーションについてです。去年のVue Fesでは、Vue 2からVue 3への移行に関するセッションが多く開催され、技術負債の解消がテーマとして取り上げられました。一方で、今年は次世代ツールチェインや新技術の導入など、未来志向の明るい話も増えており、会場には期待感が漂っていました。

Rustと次世代ツールチェインへの関心

特に注目を集めたのが、JavaScriptと並行して注目を浴びているRustについてです。「われわれはRustを書くしかないのか?」という話も飛び出す中、パフォーマンス向上や開発体験を改善するため、RustのエコシステムをVueと組み合わせて活用する動きが増えているとのことでした。例えば、Rustで書かれた「oxc」などのツールチェインが紹介され、これによりビルド時間やローカル開発の効率化が期待されています。

一方で、Rustを採用するかどうかについては、「事業にどれほどのインパクトがあるか?」という実務面での意見もあり、導入には慎重な検討が必要という話も。VueエコシステムにRustがどう寄与していくのか、今後の動向が注目されると感じました。

ブースのお話

よくわからないトラック名が呼ばれていたと話題にあがりました。Vue Fes Japan 2024でのトラック名、「MNTSQが全ての合意をフェアにするぞ」トラックは私もスタッフとして参加した時に印象に残っています。 STORESさんのブースではコードに改善点として付箋を貼る展示をされていました。コードが見えなくなるほど付箋が貼られていたみたいですね。私が訪れた時にはLGTM付箋も多かった印象です! 弊社メドピアでは握力測定を行いました。脅威の75kgを記録された方がいるみたいです! 当日の参加レポートはこちら Vue Fes Japan 2024に参加しました!#vuefes - メドピア開発者ブログ

懇親会の様子

懇親会ではたくさんのご飯をMNTSQさん、ドリンクはSTORESさんが用意してくださり、皆さんで交流を行いました。

多くの参加者と交流することができ、良い交流ができました。 私としてもすごく刺激になりました。

最後に

登壇者の皆さん、合同開催のMNTSQ株式会社、STORES株式会社の皆さんありがとうございました!

LTやパネルディスカッションでもあった通り明るい話が多かった印象でした。

これからもVue、Nuxtのコミュニティの発展を祈るとともに、貢献していきたいと思います!

来年のVue Fes Japanも楽しみです!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Vue Fes Japan 2024に参加しました!#vuefes

こんにちは。 メドピアの伏見 ( @fussy113 ) です。

2024年10月19日に大手町プレイス ホール&カンファレンスで開催された Vue Fes Japan 2024 に参加してきました! この記事では当日の様子やセッションの内容などをお届けします。

スポンサーとしての取り組み

メドピアはゴールドスポンサー、セッションルームネーミングライツスポンサーとして協賛いたしました。

スポンサーブース

握力で技術的負債を粉砕しよう!
『握力測定 in Vue Fes Japan 2024』

と題して、握力を測定していただきました。

"せっかくなので、両手測ってもいいですか?" という方や、"こんな筈ではない、もう一回!"と再挑戦しに来てくださる方もいて非常に盛り上がりました! スポンサーブース企画として、なかなかユニークだったのではないでしょうか。

TOP3は技術広報のXアカウントにて公開中です。

1位の72.1kg、すごいですね...!

セッションルームネーミングライツスポンサー

「メドピアトラック」という名前のルームを開設しておりました。 キーノートセッションなどもこの「メドピアトラック」で開催され、広いホールの席がほぼ満席で埋まるなど大変な盛り上がりを見せておりました!

セッション

私が参加したセッションのうち、いくつかを紹介させていただきます。

キーノート

vuefes.jp

トップバッターEvan Youさんによるキーノートセッション!

これまでのVueの話から始まり、最新動向の共有がされました。 v3.5のアップデートの話や、v3.6についての言及がありましたね。 v3.6ではSuspenseのstableや、Vapor Modeの試験的導入などを予定しているとのことで、楽しみです!

またViteなど、エコシステム周りの今後についても話がありました。 VoidZeroによって、今後のフロントエンドの開発体験がどう変わっていくのか。 私はエコシステム周りをあまり追えていなかったので、フロントの今後の変化にワクワクしました。

Vaporモードを大規模サービスに最速導入して学びを共有する

vuefes.jp

2024年10月現在、R&Dが進められているVueの新しいコンパイル戦略、Vapor Mode。 vuejs/vue-vaporリポジトリのplayground上に実際にプロダクトレベルのコードを入れて、Vapor Modeによる恩恵がどれだけ得られるかを検証したという内容のセッションでした。

R&Dということもあり、従来のVueのコンポーネントでは動かないところから試行錯誤して動くところまでやり切る、凄まじい熱量を感じました。 また、Vapor Modeをオンにした時、バンドルサイズ、初期描画、更新速度について良い結果が出た点がとても興味深かったです。

Chromeを利用した計測の方法もとても定量的でわかりやすく、今後のVapor Modeへの期待がとても高まりました!

Vue3の一歩踏み込んだパフォーマンスチューニング

vuefes.jp

APIから最大1,000件のデータを取得、リアクティブにデータを編集できるページのパフォーマンス改善をVueの機能を利用して行なっていったという内容のセッションでした。 Vue2、Vue3それぞれで取れるアプローチの方法を紹介してくださっていたのが印象的でした。すぐに取り入れることが出来そうです。

また、Vue3からはリアクティブな追跡について細かいチューニングが出来るようになったようで、そのTipsが多く紹介されました。 進化したリアクティブAPIやVueUse、v-memoなど、新しい機能に対してのキャッチアップの重要さを感じました。

アフターイベント

全てのセッション終了後は、アフターイベントが開催されました。

Vue.jsを愛する人たちが集まって大変な盛り上がりを見せていました。

オリジナルカクテル、会社の人と1杯ずついただきました

私も他社のエンジニアの方と情報交換したりしました。 お話ししてくださった方、ありがとうございました!

参加してのまとめ

"Fes =お祭りのように Vue.js を共に盛り上げ、共に学び、そしてなによりも共に楽しむ"が体現されたイベントだなと肌で感じることができました。 私自身は実務に活かせるような内容のセッションをメインに聴いたこともあり、とても学びもあって非常に良い時間でした。

来年の開催が楽しみですね、また参加するのが今から楽しみです!

Vue Fes Japan 2024 After Meetup やります!

STORES、MNTSQ、メドピアの3社でAfter Meetupを開催します! https://medpeer.connpass.com/event/331417/

イベントタイトル: 『Vue Fes Japan 2024 After Meetup』
開催日時: 2024/11/1(金) 19:00 〜 21:30
会場: メドピア株式会社
東京都中央区築地1-13-1 銀座松竹スクエア8階
※ オフライン開催

各社のエンジニアによるLTやパネルディスカッションを予定しています。 ご興味がある方はぜひご参加ください!
昨年の様子は こちら


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

監査ログの保管先をRDBからS3に移行する

こんにちは。サーバーサイドエンジニアの @atolix_です。

今回はメドピアで運用しているアプリケーションのkakariの監査ログをDB管理からS3管理に移行したので、その方法と手順について紹介したいと思います。

kakari.medpeer.jp

背景

従来kakariではAuditedを用いて、監査ログを専用のauditsテーブルに保管する処理を行っていました。

github.com

# application_record.rb

class ApplicationRecord < ActiveRecord::Base
  ...
  include Auditable
# auditable.rb

module Auditable
  extend ActiveSupport::Concern

  included do
    audited
    ...
  end

しかしレコードの変更の度にauditsテーブルへの書き込みが走る為、DBマイグレーションを行なった際に書き込みのロックが発生して、アプリケーションの動作に影響が出るといったインシデントが発生してしまいました。

今回はこの恒久対応としてDBへの書き込みを廃止してS3に監査ログを保管できるように変更を加えていきます。

実装概要

監査ログを通常のアプリケーションログと分別してS3に保管したいので、以下のような構成を組みます。

アプリケーションログの出力は従来通りCloudWatch Logsに送信、一方で監査ログはKinesis Firehoseを経由してS3に格納できるようにFireLensを間に置いて二箇所に振り分ける想定です。

AuditLoggable

今回は食べチョクさんが作成してくださったAuditLoggableをインストールして、監査ログをファイルに出力するように変更します。 tech.tabechoku.com

Auditedと同様にapplication_record.rbに追記をすることでレコードの変更を追跡することが出来ます。

# application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Auditable
  ...
  extend AuditLoggable::Extension
  log_audit

initializersでaudit.logの書き込み場所を指定します。

# config/initializers/audit_loggable.rb

AuditLoggable.configure do |config|
  if Rails.env.test? || Rails.env.development?
    config.auditing_enabled = false
  else
    config.audit_log_path = Rails.root.join("..", "..", "opt", "audit.log")
  end
end

DBへの書き込みをしていた時と同様のリアルタイム性を保つ為に、audit.logへの書き込みを標準出力に吐き出すようにシンボリックリンクを貼ります。

# Dockerfile
...
RUN ln -sf /dev/stdout /opt/audit.log

標準出力された監査ログを確認すると、以下のようなjsonで出力されました。

{
  "timestamp": "2024-08-06T18:00:59.981+09:00",
  "record": {
    "auditable": {
      "id": 1,
      "type": "ModelType"
    },
    "user": {
      "id": 1,
      "type": "Admin::Account"
    },
    "action": "update",
    "changes": "{\"name\":[\"xxx\",\"yyy\"]}",
    "remote_address": "xxx.xxx.xxx.x",
    "request_uuid": "xxxxxxxxxx"
  }
}

これで監査ログが標準出力に出るようになったので、現在Railsからはアプリケーションログと監査ログの2種類が混ざって出力されることになります。

次はFireLensを通して2種類のログを振り分けて送信できるようにインフラ構成を修正します。

FireLens

AWSではFireLensを使用することでコンテナのログルーティング設定が簡単に実装出来ます。

docs.aws.amazon.com

コンテナ定義内でRailsのログドライバーにFireLensを指定した後、カスタム設定のファイル(今回はfluent-bit/etc/extra.conf)と送信先のKinesis Firehoseを設定します。

# container_definition.json

 {
    "name": "rails",
     ...
    "logConfiguration": {
      "logDriver": "awsfirelens" # FireLensを指定
    },
...
},
...
{
    "essential": true,
    "image": "${fluentbit_image}",
    "name": "fluentbit",
    "firelensConfiguration": {
      "type": "fluentbit",
      "options": {
        "config-file-type": "file",
        "config-file-value": "/fluent-bit/etc/extra.conf"
      }
    },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${log_group}",
        "awslogs-region": "${region}",
        "awslogs-stream-prefix": "firelens"
      }
    },
    "environment": [ # 送信先を外部から指定できるようにロググループやリージョンの情報を環境変数に格納する
      {
        "name": "LOG_GROUP",
        "value": "${log_group}"
      },
      {
        "name": "REGION",
        "value": "${region}"
      },
      {
        "name": "TARGET_FIREHOSE",
        "value": "${firehose_name}"
      }
    ]
  }

次にログをCloudWatch LogsとKinesis Firehoseの二箇所に分岐して流せるようにFluent Bitの設定を加えていきます。 先にDockerfileに必要な設定ファイルを記述しておきます。

# Dockerfile

FROM amazon/aws-for-fluent-bit:2.32.2 

COPY extra.conf /fluent-bit/etc/extra.conf
COPY parsers.conf /fluent-bit/etc/parsers.conf
COPY categorize_logs.lua /fluent-bit/etc/categorize_logs.lua
COPY stream-processor.conf /fluent-bit/etc/stream-processor.conf
COPY output.conf /fluent-bit/etc/output.conf

Fluent Bit内の設定詳細

ここからはログの大まかな処理の流れを説明していきます。

最初にRailsコンテナから受け取るログはFireLensを通して*-firelens-*でタグ付けされるので、分かりやすくする為に一旦stream-processor.conf内でcombine.webにタグを集約させます。

# stream-processor.conf
[STREAM_TASK]
    Name web
    Exec CREATE STREAM web WITH (tag='combine.web') AS SELECT * FROM TAG:'*-firelens-*';

combine.webタグが付与されたログはjsonパースされます。

# parsers.conf
[PARSER]
    Name         json
    Format       json
    Time_Key time
    Time_Format %Y-%m-%dT%H:%M:%S.%L%z
    Time_Keep On
    Time_Offset +0900

後に再度stream-processor.confを通った時にタグを書き換えやすくするために、一度luaスクリプトで仮のタグ情報を付与します。

function categorize_logs(tag, timestamp, record)
  if record["record"] ~= nil then
    record["new_tag"] =  "audit"
  else
    record["new_tag"] =  "rails"
  end

  return 2, timestamp, record
end

stream-processor.conf内でアプリケーションログと監査ログを判別できるようにタグを書き換える処理を加えます。

# stream-processor.conf
...

[STREAM_TASK]
    Name  audit
    Exec  CREATE STREAM audit WITH (tag='logs.audit') AS SELECT * from TAG:'*combine.web*' WHERE new_tag = 'audit';

[STREAM_TASK]
    Name  rails
    Exec  CREATE STREAM rails WITH (tag='logs.rails') AS SELECT * from TAG:'*combine.web*' WHERE new_tag = 'rails';

output.confでは先ほどstream-processor.confで書き換えたタグを元に、従来のコンテナログと監査ログをそれぞれCloudWatch Logs, Firehoseに振り分けます。

# output.conf

# 通常のコンテナログは従来通り CloudWatch Logs に送信する
[OUTPUT]
    Name              cloudwatch_logs
    Match             logs.rails # stream-processor.confで書き換えたタグ
    region            ${REGION}
    log_group_name    ${LOG_GROUP}
    log_stream_prefix rails

# 監査ログは Kinesis Firehose に送信する
[OUTPUT]
    Name              kinesis_firehose
    Match             logs.audit
    region            ${REGION}
    delivery_stream   ${TARGET_FIREHOSE}

最終的なextra.confとログの流れは以下のようになります。

# extra.conf

[SERVICE]
    Parsers_file parsers.conf
    Streams_File stream-processor.conf

[FILTER]
    Name         parser
    Match        combine.web
    Key_Name     log
    Parser       json

[FILTER]
    Name         lua
    Match        combine.web
    script       categorize_logs.lua
    call         categorize_logs

@INCLUDE output.conf

Kinesis Firehose

FireLensからS3にログを送信する間にKinesis Firehoseを挟みます。

一応S3プラグインを使って直接バケットに監査ログを送信することも可能ですが、今回はFargateのような永続ディスクのない環境でFluent Bitを実行しているので、突然のコンテナの停止時に監査ログをロストしてしまう可能性が考えられます。

その為に分散バッファーとしてKinesis Firehoseを経由して継続的にログを送信できるように構成しています。

github.com

設定自体は監査ログを格納するS3バケットをdestinationに指定したシンプルな内容です。

resource "aws_kinesis_firehose_delivery_stream" "fluentbit" {
  ...
  destination = "extended_s3"

  extended_s3_configuration {
    bucket_arn  = aws_s3_bucket.audit_logs.arn
    buffering_size     = 10  # MB
    buffering_interval = 300 # seconds
    compression_format = "GZIP"
    custom_time_zone   = "Asia/Tokyo"
    ...
}

動作確認

該当のバケットを確認すると監査ログが蓄積されていることが分かります。

念の為1週間ほど従来のDBへの保管とS3への保管を並行で稼働させて、レコード数に差がないことまで確認出来たらDBへの書き込みを停止して完了です。

まとめ

FireLensとKinesis Firehoseを使うことで比較的簡単に監査ログの保存先をS3に移行することが出来ました。 S3に保管した監査ログはAthena等と組み合わせてクエリ検索出来るようにしておくと良さそうです。

一方でDBへの書き込みと比較すると監査ログのロストの確率は少しだけ上がるので、移行する際にはログの欠損が起きないか検証を十分にする必要があります。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

メドピアはVue Fes Japan 2024にゴールドスポンサーとして協賛します!

こんにちは。 10月からメドピアのVPoEになりました保立 ( @purunkaoru ) です。

メドピアは2024年10月19日に大手町プレイス ホール&カンファレンスで開催される Vue Fes Japan 2024 にゴールドスポンサー、セッションルームネーミングライツスポンサーとして協賛、そしてブースの出展も行います!

ブース企画

今回のメドピアブースは

握力で技術的負債を粉砕しよう!
『握力測定 in Vue Fes Japan 2024』

と題し、ブースを訪問いただいた皆さまに、握力測定を行っていただきます。

握力測定に挑戦!

握力測定、最近してますか?
学生以来測ってないという方!めったにないチャンスです!
この機会にぜひ挑戦してみてください。

アンケートに答えてメドピアオリジナルグッズを手に入れよう!

測定結果をアンケートに入力していただくと、メドピア特製アクリルスタンドと大きなビニールバック(通称デカバック)をプレゼント。
また、アンケートに参加いただいた方の中から抽選で、3名の方に豪華景品をプレゼント!

  • アクリルスタンド

  • オリジナルデカバック

この場所でお待ちしています

メドピアは、会場1階 カンファレンス106+107でブース出展しております。

ブースには、メドピアカラーの濃い緑色のTシャツを着たメンバーが立っています。
お気軽に声をお掛けください!

当日皆様にお会いできるのを楽しみにしております!

今年もAfter Meetupを開催します!

STORES、MNTSQ、メドピアの3社でAfter Meetupを開催します! https://medpeer.connpass.com/event/331417/

イベントタイトル: 『Vue Fes Japan 2024 After Meetup』
開催日時: 2024/11/1(金) 19:00 〜 21:30
会場: メドピア株式会社
東京都中央区築地1-13-1 銀座松竹スクエア8階
※ オフライン開催

ご興味がある方はぜひご参加ください!
昨年の様子は こちら


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

小さくはじめる OKR

集合知プラットフォーム事業部・開発部の榎本です。

前回の記事はフロントエンドエンジニアの小林さんによる『小さくはじめる Vue の Composable』でした。

今回は小さくはじめるシリーズ第二弾ということで、今期開発部でOKRを導入してみて、それがいい感じにワークしたので紹介したいと思います。

OKR導入前の課題

私たちの開発部を含む組織図は下図のようになっていました。

flowchart TD
    事業部 --> 開発部
    事業部 --> A部
    事業部 --> B部
    事業部 --> ...

1つの大きな事業部があり、その事業部を構成するユニットの1つとして開発部がある形です。

事業部単位および部署単位でそれぞれ目標を設定しています。しかし、1つ大きな問題がありました。

それは、事業部目標と開発部目標が関連していないことです。つまり、事業部は事業部として達成したい目標がある一方、開発部は開発部として「(事業部とは関係のない)開発部がやりたいこと」をベースに立てた目標が掲げられていました。

その結果、以下の問題が生じていました。

  • 事業部目標と開発部目標がリンクしていない
  • 開発部目標が単なる開発チームのToDoになっている(ToBeではない)

このような状況だったため、私自身も日々の業務をしながら「同じ事業部なんだけど、他の部署とはどことなく違う方向を向いて仕事をしているな〜」という違和感が付いて回っていました。

会社という1つの船は同じ方向(目標)に向かって走るべき

OKRã‚’å°Žå…¥

上記の課題を解決するために、開発部でOKRを導入しました。OKRを導入すれば、「事業部目標と開発部目標が関連しない」問題を解決できると考えたからです。

本来、OKRは組織全体で導入するべきものです。しかし組織全体としては現状運用しているMBOの目標設定があり、その制度はすぐには変えることができなそうだったので、開発部のみで試験的にOKRを導入・運用してみることにしました。

OKRをどう決めるか?

チームのOKRは、そのチームの上位チームのOKRから決めるべきとされています。『Google re:Work - ガイド: OKRを設定する』 には下記のように書かれています。

最初に組織の目標を表明しておくと、チームや個人がそれを考慮して自分たちの目標を設定できます。この方法なら、組織全体の OKR に整合性をもたせることができます。

(中略)

ただしチームの OKR は、組織の OKR の少なくとも 1 つには関係している必要があります。

この「組織の目標」を「事業部の目標」、「組織のOKR」を「事業部のミッション」に置き換えて開発部のOKRを考えてみることにしました。

OKR研修を実施

OKRを決める前に、まずはOKRという目標設定のフレームワークの理解を深める必要があります。

Google re:Work のOKRガイドや、社内のOKR経験者へのヒアリング、OKRに関する書籍を参考に資料にまとめ、マネージャー陣に共有しました。

この時使った資料は、公開用に内容をアップデートして、speakerdeckで公開していますので、よろしければご活用ください。

OKR基本のキ / OKR Basics - Speaker Deck

OKRの決め方

マネージャー陣でディスカッション

OKRの基礎知識をインストールできたら、いざOKRについてのディスカッションです。

開発部のマネージャー陣を招集し、下記の順番でOKRのディスカッションを進めました。

  1. Objective決め
    • 事業部目標をベースにブレスト
    • Objectiveの決定(3つ)
  2. Key Results(以下「KR」と表記します)決め
    • 1で決めたObjectiveを順番にKRをブレスト
    • KRの決定(1つの Objective 毎に約3つずつ)
  3. OKR運用方法決め
    • チームとしてどのようにOKRを運用していくかを決定

マネージャー陣でOKRをディスカッションしている様子

時間はめっちゃかかる

サラッと書きましたが、このディスカッションはとても時間を要しました。参加メンバー全員初めてのOKR、ということもあってか約4時間のミーティングを三回、計12時間以上ディスカッションしました。

大変骨が折れる作業ではありましたが、以下の理由から時間をかけただけの価値は十分にあったと考えています。

  • 事業部目標とリンクする目標設定ができた
  • 「今期、何にフォーカスすべきか?」のコンセンサスが得られた
  • 腹落ちするObjective・ワクワクするObjectiveを設定できた
  • ディスカッションを通してマネージャー同士の相互理解が進み、目線を合わせることができた

工夫した点

KRオーナーを決める

良いOKRを決めたとしても、それを推進する旗振り役がいなければ、なかなか前には進みません。

私たちはそれぞれのKR毎にオーナーを決め、担当者にオーナーシップをもって達成率の向上にコミットしてもらいました。オーナーは個人の場合もありますし、複数名の場合もありますし、チーム名の場合もあります。

オーナーは具体的には下記のことをやってもらいました。

  • KR達成のために、関係者を巻き込み、まとめ上げる
  • KRの達成率の管理
  • 四半期毎の振り返りの実施

また、オーナーにとって下記のような良い副作用もありました。

  • 旗振り役になってもらった人の中には、チームをリードした経験が少ないエンジニアもいたが、OKRのリード経験を通して成長に繋がった
  • KRの達成のためにチームを跨いだ活動も増え、チームを超えた交流の機会になった

週定例で進捗を追う

言うまでもなく、OKRは立てて終わりではなく、達成に向けて動き続けなければなりません。

私たちの現在のObjectiveは何で、Key Results の最新の進捗状況をどれくらいなのかを週次の開発部の定例で確認するようにしました。これによって、メンバー各人がOKRに自覚的になり、達成に向けた動きを加速させることが出来たと感じています。

決まったOKRは一枚のスプレッドシートにまとめて公開し、進捗を誰からも一目瞭然にする

OKRの振り返り

OKRを半年間やってみて、振り返りを実施しました。その結果を一部抜粋して共有します。

よかったこと

多くの開発部メンバーから「やってよかった」「来期もOKRを続けたい」という声を得ることが出来ました。

他にも以下のポジティブな感想を得ることが出来ました。

  • 運用改善KRを推進したチームが運用チームにアンケートを取ったところ、「運用改善の効果を感じられたか?」「来期もこの取り組みを続けていきたい思うか?」という質問に対して、運用チームほぼ全員からポジティブな結果を得られた
  • ストレッチ目標を設定することで、自分のキャパシティを超えた活動まで可能になった
  • オーナーの中には、楽しそうにオーナーを勤めてくれる人もいて頼もしかった
  • 今までよりも費用対効果の高いタスクに優先的に取り組むことができた
  • 同じ事業部の別の部署にもOKR研修を実施し「OKRとは何ぞや」について理解してもらえた
  • 同じKRを追っているメンバーでモブプログラミング・モブレビューを実施し、知見を共有できた

課題に感じたこと

一方で振り返って課題に感じたことも出てきました。

  • 推進力がオーナーの力に依存してしまう
  • 数値に目が行きすぎて、目的を見失うケースが一部あった
  • もっと適切なKR成果指標があるのに、最初に定めたKRで硬直化してしまっていた
  • 「OKRツリー」という言葉があるが、必ずしも全てがツリー状には紐づかない

今後改善したいこと

上課題も踏まえて、次回OKRをもっと上手に回すために、下記の点に気を付けたいと思っています。

  • 上手な進め方をしているKRオーナーをモデルケースとして、ノウハウを横展開する
  • オブザーバーとしてマネージャー陣もOKRのミーティングに参加し、きちんと目的を失わないようにガイドする
    • 「Objective達成のためのKey Result」という意識を持つことが大事
  • KRの成果指標・数値は(適切な理由があれば)変更可能なことをオーナーに伝える
  • 「OKRをツリー状にすること」に拘らないようにする

OKRを成功させるためのポイント

実際にOKRを半年間運用していみて感じる、OKRを成功させるためのポイントは下記だと考えます。

  • 前提として上位組織(今回でいうと事業部)のObjectiveを明確にする
  • 皆が率先して「やりたい!」と思えるようなチャレンジングでワクワクするObjectiveを設定する
  • OKRを全体公開し、定期的に進捗をチェックし、必要であれば見直し・振り返りを行う
  • マネージャー・リーダーがOKRを理解し、達成に向けてコミットする
  • OKRをそのまま従業員評価ツールに使わない
    • 評価を上げるためのいわゆる「数値ハック」を防止
  • ムーンショットを目指すというOKRの考え方を理解し、ストレッチした目標をきちんと設定すること
    • MBOに慣れてしまった脳だと、達成可能な範囲内の数値目標を置いてしまいがち

MBO vs OKR

さいごに

今回は開発部で小さく始めたOKRの例を紹介させていただきました。

OKRは基本的に全社レベルで導入する目標設定フレームワークですが、部単位でOKRを設定しても十分にワークする手応えを得られました。

皆様の目標設定の一助になれば幸いです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp