株式会社ヘンリー エンジニアブログ

株式会社ヘンリーのエンジニアが技術情報を発信します

Server-Side Kotlinで書かれたCloud Runサービスのコールドスタートレイテンシを短縮する

株式会社ヘンリーでSREなどをしている戸田(id:eller)です。先日弊社のエンジニアが登壇したサーバーサイドKotlin LT大会 vol.11でSansan社の柳浦様がServer-Side KotlinアプリのCloud Run コールドスタートレイテンシを改善した話をされていました。

Server-Side Kotlin アプリのCloud Run コールドスタート レイテンシを改善した話 - Speaker Deck

本件はCloud Runを使ってServer-side Kotlinを運用している弊社でも関心が高い内容です。そこで弊社の事例も紹介させていただければと思います。

動機:コールドスタートレイテンシを改善すると何が嬉しいか

Cloud Runはスケールアウト時に10秒までリクエストを滞留させます。そして10秒でインスタンスが用意できなかった場合は429エラーが発生する可能性があります。エラーの発生はユーザ体験を悪化させるため、常に多めにインスタンスを起動しておくか、コンテナを10秒以内に起動させる必要があるわけです。

常に多めにインスタンスを起動させる方法は容易ではありますが、ランニングコストを増大させます。コールドスタートレイテンシを改善できればスケールアウトに頼った運用も採用しやすくなり、ランニングコスト圧縮効果を期待できます。

採用した施策

DB接続確立の遅延

弊社ではJDBC接続のプールにHikariCPを採用しています。HikariCPはデフォルトの設定では、DataSource作成時に接続の確立まで処理をブロックします。

設定を変更することでDataSource作成をすぐに終了して後続処理を進めることができますが、この場合はDataSourceからConnectionを取得する際に例外が出る可能性があります。このためstartup probeが呼び出されたときにデータベースに接続できることを確認する処理を入れておくことが良いでしょう。

logbackの設定を動的に組み立てるのをやめる

弊社ではそこまでlogbackの設定を複雑にしていませんが、それでも起動時に ch.qos.logback.classic.util.DefaultJoranConfigurator が1秒弱の処理時間を持っていっていました。これはXMLの解析や各種インスタンスの作成・設定に時間がかかっているからと思われます。

この問題を解決するためのパッケージが logback-tyler として公開されています。まだ安定バージョンに到達していませんが、Javaコード生成ツールなので生成されたコードに自分で責任を持てば良いだけです。

また生成されたコード自身は logback-tyler に依存しないため、ランタイムの依存は追加する必要ありません。むしろjaninoへの依存を削れるので、1MiB程度ですがコンテナも小さくできます。設定時間は弊社事例では100msと、10倍高速化まで持っていくことができました。

利用しているConfiguratorは以下のようなものです。生成されたコードは変数名などに課題が多く、またLogstashEncoderなどに対応していなかったため、けっこう手を加えています。

import ch.qos.logback.classic.AsyncAppender
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.spi.Configurator
import ch.qos.logback.classic.spi.Configurator.ExecutionStatus
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.classic.tyler.TylerConfiguratorBase
import ch.qos.logback.core.Appender
import ch.qos.logback.core.ConsoleAppender
import net.logstash.logback.encoder.LogstashEncoder
import net.logstash.logback.stacktrace.ShortenedThrowableConverter
import kotlin.system.measureTimeMillis

class TylerConfigurator : TylerConfiguratorBase(), Configurator {
    override fun configure(loggerContext: LoggerContext): ExecutionStatus {
        context = loggerContext
        val elapsed = measureTimeMillis {
            val asyncAppender = setupAsyncAppender()
            val loggerRoot = setupLogger("ROOT", "INFO", null)
            loggerRoot.addAppender(asyncAppender)
        }
        println("TylerConfigurator.configure() call lasted $elapsed milliseconds.")
        return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY
    }

    private fun setupAsyncAppender() = AsyncAppender().apply {
        context = this@TylerConfigurator.context
        name = "ASYNC"
        queueSize = 1024

        addAppender(setupStdoutAppender())
    }.also {
        it.start()
    }

    private fun setupStdoutAppender() = ConsoleAppender<ILoggingEvent>().apply {
        context = this@TylerConfigurator.context
        name = "STDOUT"
        encoder = setupLogstashEncoder()

        addFilter(HealthCheckLogFilter())
    }.also {
        it.start()
    }

    private fun setupLogstashEncoder() = LogstashEncoder().apply {
        context = this@TylerConfigurator.context
        throwableConverter = ShortenedThrowableConverter().apply {
            context = this@TylerConfigurator.context
            maxDepthPerThrowable = 50
            shortenedClassNameLength = 20
        }
        fieldNames.apply {
            level = "severity"
            logger = "loggerName"
            thread = "threadName"
            levelValue = "[ignore]"
        }
    }
}

JITコンパイラ最適化

Sansan様の資料にも記載されているものです。Cloud Runのドキュメントでも紹介されていますので、すでに試された方も多いかもしれません。

k6による負荷テストを行ったところ、弊社サービスではパフォーマンスへの影響も限定的と判断できました。これにより1秒近い高速化効果が得られました。

CPU boost

Sansan様の資料にも記載されているとおり、起動時にCPUを追加するCPU boostがとても強力です。JavaやKotlinのサーバはDB接続の確立やDIコンテナの初期化を行ってはじめて接続を受け付けることになりますが、DIコンテナの初期化すなわち依存関係の算出やインスタンス生成はCPUを多く使う処理です。

とはいえ普通にコードを書いていてはCPUコア数増大のメリットは受けにくいようにも感じます。JITやGCでの活用はできているはずですが…。本当はDIコンテナによるインスタンス生成をcoroutineを使って並列化したいのですが、弊社で使っているKoinにはまだこうした機能がまだありません。 lazyModule を使った遅延初期化がexperimentalな機能として実装中とのことで期待しています。

また弊社事例では該当しませんでしたが、CPUコア数が動的に変化するということは Runtime.availableProcessors() を参照して動的になにかを決定するような実装がある場合には注意が必要かもしれません。

適用を見送った手法

コンテナを小さくする

ベースイメージにalpineを使ったり、jlinkで不要な機能を削ぎ落としたJVMを作ったりしてコンテナを小さくできます。場合によっては大きな高速化効果を得られることもありますが、今回のケースでは0.5秒程度の短縮にとどまったため、ビルド工程をシンプルにするために採用しませんでした。

OpenTelemetryの自動計装を諦める

Sansan様の資料にはSplunkのagentの設定変更で充分に回避できると記載されていましたが、弊社はopentelemetry-javaagentを使っている関係からか、必要な自動計装だけを有効にしても10秒の壁を超えられませんでした。-Dotel.javaagent.debug=true オプションを有効にして調査したところ、bytecode manipulationの準備のために編集対象となるClassを探しているようで、ここがかなり遅そうでした。TypeInstrumentationのJavadocによるとクラス名以外の実装や親クラス・インタフェースによって挙動を変えるinstrumentationの場合にここが遅くなるようです。

公式GitHub Issuesにも似たような指摘と議論が複数存在します。こちらのIssueからリンクされているので、気になる方はご覧ください。昔は40秒とかかかってたんですね、さすがに今はそういったレベルではないですがパフォーマンスに課題があるのは変わらないようです。

一応extensionを用意して特定のクラスを対象から外すことはできるようですが、有効にしたい計装があまり多くないことからjavaagentによる自動計装を諦めることも充分に検討できそうです。

AppCDS

javaagentの利用を諦めるなら、AppCDSも検討できます。今回はAppCDS無しでも目標を達成できたので、ビルド工程をシンプルにするために採用しませんでした。

CRaC

JVMアプリケーションコンテナの起動高速化と言えばCRaC、という雰囲気はありますが、弊社で使っているgRPCサーバなどのフレームワークでは公式の対応を謳っているものがなく、検証工数が大きくなると判断して採用していません。Cloud RunでCRaCを使うこと自体は可能なようなので、こちらの記事などを参考に挑戦しても良いかもしれません。

まとめ

Cloud Runのコールドスタートレイテンシを改善すると何が嬉しいかについて述べ、弊社で採用した施策をいくつか紹介いたしました。よく紹介されている手法でもあまり効果がなかったり、新しく開発された技法が有効だったりと、検証のしがいがある技術領域だと感じます。特に logback-tyler はまだ情報が少ないので、この記事が皆さんの参考になれば幸いです。

弊社ではこれらの施策により、18秒近くかかっていたコールドスタートレイテンシが10秒程度に短縮されています。もっと短縮したいとは思っていますが、OpenTelemetry agentによる自動計装、CRaCなどの基盤技術に手を入れないと難しそうです。JVMアプリケーションやServer-side Kotlinの高速化なら任せろという方、ぜひ弊社の採用サイトにも足を伸ばしていただけると幸いです。

jobs.henry-app.jp

ヘンリー版 エンジニア社外登壇 How-To

ヘンリーで SRE をやっている id:nabeop です。

昨年からオフラインイベントも活況になってきています。登壇の準備段階や登壇時に気をつけておきたいポイントについてまとめて、技術勉強会 (ギベン)1でエンジニアが社外登壇するときに気をつけたいことの Tips 集みたいな感じで発表してみました。このときの発表の評判がよくて社外向け版もつくってみない?という声があったので書いてみました。


  1. ギベンについては過去にエントリがあります。
続きを読む

Apollo Serverをv4にアップグレードしました

株式会社ヘンリーでSREなどをやってる戸田(id:eller)です。弊社サービスはBackend for FrontendとしてApollo Serverを採用しています。

先月まではApollo Server v3を利用していましたがEOLが今年の10月に迫っていたため、v4へのアップグレードを実施しました。この記事では移行時に実施したことをご紹介いたします。

なお公式ドキュメントに主な変更点がまとまっていますので、類似の作業を予定されている方はそちらをまずご確認ください。

GraphQL関係の依存を更新する

Apollo Server v4は graphql 16.7.0以降の利用を推奨しています。もしいま依存しているバージョンが古いなら、その更新から始めることをおすすめします。

弊社では graphql に加えて @graphql-tools/load@graphql-tools/schema も使っており、更新が必要でした。これらの変更はApollo Serverの更新とは独立して事前に行ない、動作確認をしています。幸いにも性能への悪影響は観測されませんでした。

@apollo/server を導入する

v3のときは以下のように数多くの依存を package.json に記述する必要がありました:

  • apollo-server
  • apollo-server-core
  • apollo-server-errors
  • apollo-server-express
  • apollo-server-plugin-base
  • apollo-server-types

しかしv4からは @apollo/server ひとつで足りるようになっています。コンテナの大きさには変化はなかったものの、依存管理コストが下がってとても嬉しい変更です。

なお @apollo/server-plugin-landing-page-graphql-playground に含まれる ApolloServerPluginLandingPageGraphQLPlayground はマイグレーション用に提供されているだけだそうです。弊社ではドキュメントに従って ApolloServerPluginLandingPageLocalDefault に置き換えました。

エラーの扱いを1本化する

v3では ApolloErrorGraphQLError の2つのエラーが存在しましたが、v4では GraphQLError に1本化されています。独自のエラー型を提供しなくなったわけですね。

弊社では ApolloError をcatchしていたり AuthenticationError を投げていたりしている箇所のコード書き換えを行っています。

ヘルスチェックを変更する

v3では/.well-known/apollo/server-health を使ったHTTPレベルのヘルスチェックが提供されていましたが、v4ではこれがなくなりました。GraphQLレベルのヘルスチェックを使ったうえで apollo-require-preflight: true HTTPヘッダを設定する必要があります。

弊社ではCloud Runのstartup probeないしliveness probe、ならびにUptime Checkで利用するURLを変更し、GraphQLレベルに加えてDBサーバへの接続性なども確認しています。

まとめ

Apollo Serverをv3からv4にアップグレードした際に実施したことを紹介しました。マイグレーションガイドが親切で、おおむね書いてある手順に従うだけで完了できました。性能上の問題も特に確認できておらず、更新後2週間近く経ちましたが安定して稼働できています。

この記事がApollo Server v3をご利用中の方の背中を押すきっかけになれれば幸いです。

GitHub ActionsでNextJSアプリのビルドとCloud Runへのデプロイを組む

株式会社ヘンリーでSREなどをやってる戸田(id:eller)です。最近の仕事のテーマはリスクコミュニケーションとサイト信頼性です。

弊社のビルドとデプロイは長らくCircle CIを使ってきました。一方でGitHub Actionsも強力なRunnerを使うハードルが下がったり、Circle CIのcontextsよりも使いやすいvariablesやsecretsの管理ができるようになってきたりしています。特にNodeJS開発界隈はGitHub ActionsがメジャーなCI/CD環境になってきている感触もあります。

今回は既存デプロイパイプライン整理のため、NextJSプロジェクトのデプロイパイプラインをGitHub Actionsで組み直しました。要点をご紹介いたしますので、どなたかの参考になれば幸いです。

要件

  • ビルドとデプロイを分離すること。コンテナイメージとアセットをビルドのタイミングで作成しておき、デプロイでは gcloud run deploygcloud storage cp を実行するだけにして、変更のリードタイムを短縮する一環とします。
  • 動作確認環境や本番環境など、複数の環境へのデプロイを統一的に扱えること。
  • ランニングコストがCircle CIと大きく乖離しないこと。将来的にCircleCIの利用を止められればユーザごと料金を大きく削れるため、多少のランニングコスト増は許容範囲とします。

課題

ビルドとデプロイを分離するために、以下のようなワークフローが必要になります。弊社の組み方ですと対象環境ごとにコンテナイメージを作る必要があったため、ビルドマトリックスを利用しています。

graph LR
  テスト
  subgraph ビルド
    development
    staging
    production
  end
  subgraph デプロイ
    d-development[development]
    d-staging[staging]
    d-production[production]
  end
  development --> d-development
  staging --> d-staging
  production --> d-production

また弊社はアセットをCloud Storageにアップロードして、Cloud Load Balancingを経由してエンドユーザーに配布しています。Cloud Storageへのアップロードが即エンドユーザーに対する公開となるため、コンテナイメージはビルドフェーズにpushし、アセットはデプロイフェーズにアップロードさせたいところです。しかしどちらも next build コマンドによって作成するものであるため、アセットのアップロードをビルドフェーズではなくデプロイフェーズまで遅延させる必要がありました。

graph LR
  subgraph ビルド
    direction TB
    build[job] --コンテナイメージ--> ar[(Artifact Registry)]
  end
  ビルド --アセットの受け渡し--> デプロイ
  subgraph デプロイ
    direction TB
    deploy[job] --アセット--> gcs[(Cloud Storage)]
  end

最後に、Pull Request(PR)作成時のワークフローではテストやビルドは行いたいもののコンテナイメージのアップロードはしたくない、またデプロイフェーズは実行する必要がないという特徴があります。こうした制御によってランニングコストを下げるとともに、エンジニアの開発体験を改善することができます。

ビルドマトリックス間の依存関係を表現する

今回の要件はほぼGitHub Actionsの基本機能で実現可能ですが、ビルドフェーズとデプロイフェーズの双方でビルドマトリックスを使っているところだけ注意が必要です。ジョブ同士の依存には jobs.*.needs を使いますが、ここでは matrix を参照できないからです。

jobs:
  build:
    strategy:
      matrix:
        env:
          - development
          - staging
          - production
  # ...
  deploy:
    strategy:
      matrix:
        env:
          - development
          - staging
          - production
    needs:
      - build[matrix.env] # このようには書けない

この問題に対応するため、今回は cloudposse/github-action-matrix-outputs-writeを採用しました。ビルドフェーズに含まれる各々のジョブからひとつずつアーティファクトをアップロードし、これを統合するジョブをデプロイフェーズの前に挟むことで、ビルド用ジョブとデプロイ用ジョブの間に依存関係を持たせつつ、不必要なデプロイ用ジョブを実行しない仕組みを実現しています:

jobs:
  build:
    # ...
      - uses: cloudposse/github-action-matrix-outputs-write@928e2a2d3d6ae4eb94010827489805c17c81181f # v0.4.2
        if: steps.trigger-release.outputs.result == 'true' # リリースが必要な場合。後述
        with:
          matrix-step-name: ${{ github.job }}
          matrix-key: ${{ matrix.env }}
          outputs: |-
            include: true

  # デプロイが必要な環境をリストアップしてmatrix用のJSONを出力する
  prepare:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: cloudposse/github-action-matrix-outputs-read@ea1c28d66c34b8400391ed74d510f66abc392d5e # v0.1.1
        id: read
        with:
          matrix-step-name: build
      - uses: actions/github-script@v7
        id: set-result
        with:
          script: |
            const input = JSON.parse(${{ toJSON(steps.read.outputs.result) }});
            return input.include ? Object.keys(input.include) : [];
    outputs:
      result: "${{ steps.set-result.outputs.result }}"

  deploy:
    needs: prepare
    runs-on: ubuntu-latest
    if: join(fromJSON(needs.prepare.outputs.result), '') != ''
    strategy:
      matrix:
        env: ${{ fromJson(needs.prepare.outputs.result) }}
    environment: ${{ matrix.env }}

ジョブの依存関係は以下のようになります。

graph LR
  テスト
  subgraph ビルド
    development
    staging
    production
  end
  subgraph デプロイ
    d-development[development]
    d-staging[staging]
    d-production[production]
  end
  development & staging & production --> prepare --> d-development & d-staging & d-production

ビルドマトリックス間のファイル受け渡し

依存関係が表現できれば、ファイルの受け渡しは難しくありません。actions/upload-artifactactions/download-artifact を利用して、 ${{ matrix.env }} をnameに含むアーティファクトをアップロード&ダウンロードするようにします。アーティファクトは一定時間で削除されますが、デプロイが充分に頻繁であれば問題にならないでしょう。

jobs:
  build:
    steps:
      # ...
      - uses: actions/upload-artifact@v4
        with:
          name: next-static-${{ matrix.env }}
          path: .next/static
  # ...
  deploy:
    steps:
      # ...
      - uses: actions/download-artifact@v4
        with:
          name: next-static-${{ matrix.env }}
          path: .next/static
          merge-multiple: true

コンテナイメージのpushやデプロイフェーズの必要性を判断する

弊社はGitflowを使って開発をしています。コンテナイメージのpushやデプロイフェーズの必要性を整理すると、以下のようになります:

  • developブランチに変更をpushしたら、開発環境にデプロイ
  • releaseブランチからmasterブランチに向けたPRを更新したら、動作確認環境にデプロイ
  • masterブランチに変更をpushしたら、本番環境にデプロイ

これ以外のケース、例えばtopicブランチへの変更のpushやdevelopブランチに向けたPRの更新では、ビルドやテストは必要でもコンテナイメージのpushやデプロイの実行は不要です。この判断をGitHub Actions Workflowのフォーマットで表現することは可能ですが、単体テストを書きたいと考えたためJavaScriptファイルに切り出してactions/github-script で実行することとしました:

      - run: |
          echo "head=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
        id: extract-branch
      - uses: actions/github-script@v7
        id: trigger-release
        with:
          result-encoding: string
          script: |
            const triggerRelease = require(".github/workflows/trigger-release");
            const {HEAD_BRANCH, BASE_BRANCH, APP_ENV} = process.env;
            return triggerRelease(HEAD_BRANCH, BASE_BRANCH, APP_ENV);
        env:
          HEAD_BRANCH: ${{ steps.extract-branch.outputs.head }}
          BASE_BRANCH: ${{ github.base_ref }}
          APP_ENV: ${{ matrix.env }}
// .github/workflows/trigger-release.js
/**
 * @param {string} head PRのHEAD、あるいはPUSHされたブランチの名前
 * @param {string|undefined} base PRのBASEブランチの名前、あるいはundefined
 * @param {string} env development, staging, productionのいずれか
 */
module.exports = (head, base, env) => {
  const isPush = base === undefined || base.length === 0;

  if (env === "production") {
    return isPush && head === "master";
  } else if (env === "staging") {
    return !isPush && head.startsWith("release/");
  } else {
    return isPush && head === "develop";
  }
};

まとめ

弊社のユースケースでは next build はコンテナイメージとアセットの双方を作成するのに必要なコマンドでありビルドフェーズに実行したいものでしたが、アセットのアップロードタイミングはデプロイ時にまで遅延させる必要がありました。またビルドマトリックスを利用するために、ビルドジョブとデプロイジョブの依存関係管理が複雑化していました。アーティファクトを利用することでこの2つの問題が解消できました。

またコンテナイメージのpushやデプロイフェーズの必要性を判断する条件は複雑化しがちですが、JavaScriptに切り出すことでVitestなどによる単体テストを書けるようになります。必要ならTypeScriptで書くこともできるでしょう。複雑化しやすいワークフローを制御するテクニックとして覚えておいて損はないと思います。

OpenTelemetry Collector 自身のモニタリングについて考える

ヘンリーで SRE をやっている id:nabeop です。最近の仕事のテーマはサービスの可観測性の向上と信頼性の計測です。

最近では可観測性の文脈では OpenTelemetry が話題に上がると思いますが、ヘンリーでも OpenTelemetry を導入してテレメトリデータを収集して、各種バックエンドに転送しています。分散トレース周りの話題については、以下のエントリがあります。

ヘンリーではマイクロサービスからのテレメトリデータは Cloud Run で構築した OpenTelemetry Collector で集約し、otelcol のパイプライン中で必要な処理を実施し、バックエンドに転送するアプローチを採用しています。

OpenTelemetry Collector でテレメトリデータを収集している様子

現在は監視基盤の移行期なので、メトリクスが Google Cloud と Datadog の両方に転送されていますが、将来的には Datadog に一本化される見込みです。

今回のエントリでは OpenTelemetry Collector 自体の可観測性をどのように確保しているかについて紹介します。

OpenTemetry Collector の内部メトリクスを Prometheus 形式でエクスポートする

OpenTelemetry Collector のモニタリングについては以下のドキュメントが参考になります。

また、OpenTelemetry Collector 自体の可観測性の考え方についてはこのようなドキュメントがあります。このドキュメントでは実験的なアプローチとして OTLP でテレメトリデータを外部に転送するアプローチが紹介されています。今回は以下の理由から OTLP によるエクスポートを選択せず、Prometheus 方式で OpenTelemetry Collector の内部情報をエクスポートするアプローチを採用しました。

  • OTLP でのエクスポートは実験的という扱いである
  • 前述のモニタリング方法のメトリクスが Prometheus 形式で記述されている

したがって、OpenTelemetry Collector の内部のメトリクスを Cloud Monitoring と Datadog の双方に転送する OpenTelemetry Collector の設定は以下のようになりました。

receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: otel-collector
          scrape_interval: 30s
          static_configs:
            - targets: ['0.0.0.0:8888']

processors:
  batch:
    send_batch_size: 8192
    timeout: 15s
  transform/gcp:
    metric_statements:
    - context: datapoint
      statements:
      - set(attributes["exported_service_name"], attributes["service_name"])
      - delete_key(attributes, "service_name")
      - set(attributes["exported_service_namespace"], attributes["service_namespace"])
      - delete_key(attributes, "service_namespace")
      - set(attributes["exported_service_instance_id"], attributes["service_instance_id"])
      - delete_key(attributes, "service_instance_id")
      - set(attributes["exported_instrumentation_source"], attributes["instrumentation_source"])
      - delete_key(attributes, "instrumentation_source")
      - set(attributes["exported_instrumentation_version"], attributes["instrumentation_version"])
      - delete_key(attributes, "instrumentation_version")

exporters:
  googlecloud:
  datadog:
    api:
      site: datadoghq.com
      key: ${env:DD_API_KEY}

service:
  telemetry:
    metrics:
      address: ":8888"

  pipelines:
    metrics/promethus-for-datadog:
      receivers: [prometheus]
      processors: [batch]
      exporters: [datadog]
    metrics/promethus-for-gcp:
      receivers: [prometheus]
      processors: [batch, transform/gcp]
      exporters: [googlecloud]

service の telemetry.metrics によって OpenTelemetry Collector の内部メトリクスを Prometheus 形式で 0.0.0.0:8888/tcp でエクスポートして、prometheus レシーバーで Prometheus 形式のテレメトリデータを収集しています。

また、Cloud Monitoring にメトリクスを転送しようとした際に「Duplicate label Key eccountered」というエラーが発生し、メトリクスデータの転送に失敗していたので、google exporter の README.md の記述を参考に transform プロセッサーで transform/gcp としてメトリクスの属性を exported_ プレフィックスをつけた属性名に置き換えています。

このようなパイプライによって、Datadog と Google Cloud の Cloud Monitoring の両方で OpenTemetry Collector の内部メトリクスが他のマイクロサービスと同様に観測できるようになりました。

今後の課題

今回のアプローチでは Prometheus 形式でエクスポートしていますが、前述の OpenTelemetry Collector の Observability のドキュメントでは実験的という立ち位置ですが、将来的に OTLP 形式に置き換わることが示唆されています。将来的に OTLP 形式が推奨となり、Prometheus 形式でのエクスポートが非推奨となった場合、メトリクス名もドット区切りの OTLP 形式に置き換わることが予想されるので、メトリクスデータの連続性が失われることが課題になりそうと思っています。

また、OpenTelemetry Collector のモニタリングのドキュメントでは転送時にエラーになった場合は otelcol_processor_dropped_spansotelcol_processor_dropped_metric_points がカウントアップされるとありましたが、我々の環境ではこれらのメトリクスは生成されていませんでした。今は代替として otelcol_exporter_send_failed_spansotelcol_exporter_send_failed_metric_points を監視するようにしています。

We are hiring!!

ヘンリーでは各種エンジニア職を積極的に採用しています。Henry が扱っている医療ドメインは複雑ですが、社会的にもやりがいがある領域だと思っています。複雑な仕組みを実装しているアプリケーションには可観測性は重要な要素です。一緒にシステムの可観測性を向上しつつ、複雑な領域の問題を解決してみませんか?

mablers_JPでドメインエキスパートとQAについて登壇しました

SDET / SREのsumirenです。 2023/12/21に開催されたmablers_JP オフラインMeetUpにヘンリーから登壇しました。その際の登壇内容について、こちらのエントリにサマリを記します。

当日のアーカイブは以下になります。よろしければぜひご覧ください。

youtu.be

イベントを運営・企画いただいた運営の皆様に感謝します。ありがとうございました。

背景

ヘンリーは医療ドメインにディープダイブし、複雑な診療報酬制度に向き合っています。難解なドメインでお客様のペインに正しくアプローチするために、ヘンリーではドメインエキスパートが活躍しています。ヘンリーで活躍しているドメインエキスパートについては、こちらの記事も是非ご覧ください。

ドメインエキスパートの知見をサービスの品質に最大限活かすため、ヘンリーではドメインエキスパートがQAの役割も担っています。これにより、「実際には医療事務さんはこういう使い方をしない」であったり「この診察をしたときには、厳密にはこういう金額計算になる」といった業務への深い洞察をサービスの品質に活かすことを目指しています。

登壇内容

上記のドメインエキスパートの知見を自動テストにおいても活かすため、ヘンリーではローコード自動テストツールであるmablをドメインエキスパート中心で運用しています。

ローコード自動テストツールとはいえ、実際にmablを非エンジニア中心で運用しているケースは少ないのではないかと思います。ヘンリーでは様々な工夫をしながらこうした尖った運用を実現し、自動テストの領域においてもドメインエキスパートの知見を活用することに挑戦しています。

イベントでは、こうしたドメインエキスパート中心のmabl運用について、組織やプロセスの設計における工夫の事例をシェアさせていただきました。

最後に

ヘンリーの組織にはまだまだQAの課題があります。

ドメインへの理解度が高い一方、QA自体の専門性が不足しているため、一般的に検知しやすいバグが摘出できないこともあります。また、QA専任の方はいないため、長期的な品質戦略や計画を立てたり、品質のボトルネックを積極的に特定して課題解決を推進することもできていないと言えます。

制約の中で、ドメインエキスパートやSDET中心で定例や改善活動を回し、今自分たちにできるQAにチームで向き合っているというのが現状です。

そうした背景もあり、ヘンリーでは1人目のQAエンジニアを募集しています。ドメインエキスパートとQAエンジニアの専門性をシナジーさせることでサービスの品質をさらに高めたり、プロアクティブに品質戦略に臨む組織体制を作っていきたいと考えています。

興味を持っていただけましたら、ぜひカジュアル面談でお話させてください。よろしくお願いいたします。

hrmos.co

2023年ヘンリーアドベントカレンダー完走の感想

ヘンリーで SRE をやっている id:nabeop です。

そして、この12月、さらにシェアを増やすべく、有志の企画によりヘンリー史上初のアドベントカレンダーを行うことになりました。 Advent Calendar 2023をやるので2023年を振り返ってみます - 株式会社ヘンリー エンジニアブログ

そろそろ社員数も増えてきたし、アドベントカレンダーができるんじゃないか?という Slack の自分の分報チャンネルでつぶやいたら、あれよあれよという間に全社的な取り組みになって、25日間の枠が埋まりました。

ということで、今年のうちにアドベントカレンダーの感想のエントリを書いてみます。

12/1

アドベントカレンダーを始めるにあたり、せっかく2人の VPoE がいるので、開始と終了はそれぞれに担当してもらいたいと思っていたところでした。この一年で組織とサービス規模の両面が大きくなったというのが改めてわかりました。来年はもっと拡大する見込みなので、引き続きやっていきたいですね。

12/2

双方のソフトスキルに依存する話なので、むずいですよね。チームでも毎朝30分の Standup ミーティングをしていて、タスクの困りとかはそこで会話しているけど、自分が相手の困りを引き出せているかというと自信ないです。

12/3

僕は今年の6月にヘンリーに入社したので今年の後半しかいなかったですが、導入予定の医療機関様がすごいスピードで増えていく様子をみていました。来年も Henry をたくさんの医療機関様に導入いただいて、医療の DX 化を進めていきたいですね。

12/4

大きい会場で緊張しているところをダジャレで助けてもらいました!!だいたい発言から数分で面白いダジャレがついてくるので、瞬発力がすごいなと思っています。

あと、エントリのタイトルで「完走の感想」とダジャレにしていたので、カスタム URL も遊び心が欲しいなーと Slack のチャンネルでつぶやいたらシュッと「otucalendar」といい感じなワードを出してくれたので助かりました。

12/5

浸透圧だけでここまで語れるのすごすぎないですか!?

12/6

弊社の特徴として、Henry を導入いただくにあたって、医療期間様の業務の深いところまで理解して、導入の価値を最大化することを目指しています。今まではカスタマーサクセス (CS) という職種でしたが、より実態に合わせるようにパートナーグロース (PG) と職種名を変えました。その背景と今後の展望まで書かれているので、興味のある方はぜひ読んでもらいたいですね。

12/7

入社していきなりたくさんの医療機関様にインタービューをして、その内容が日々 Slack にながれてきて、とても頼もしいなーと思ってみていました。

12/8

数ヶ月くらい技術勉強会をやってきて、Slack でも「これギベンで話したら面白くないです?」みたいな会話がされていて根付いてきたなーと思っています。こういう取り組みはどんどんやっていきたいですね。

12/9

一緒に OpenTelemetry の導入をしているんですけど、このエントリを読んで思ったよりもハマりポイントがあるなーと改めて思いました。ただ、問題に向き合うことで OpenTelemetry のトレースのプロパゲーション周りの理解が深まってよかったです。

12/10

PC で仕事をすることが普通だと思っていたけど、業種によってはそうではない、ということが驚きでした。Henry を導入していただくことによって、DX 化の促進にもなると嬉しいなと思いました。

12/11

blogsync は個人的にも使っているので、メンテしてもらってありがたい!このブログも blogsync での自動化をしたいなーと思って途中まで作業していたけど、忙しくて手が動かせてないことを思い出しました。

12/12

PG の業務範囲は広いので、キャリアパスが多岐にわたるということですね。この中から各自にあったキャリアパスで成長してもらえそう。

12/13

PG という職種についてのエントリを読んでいたので、PG として活躍されている人の実体験も合わさって、PG という職種の解像度があがりました。

12/14

いつもお世話になっています!!!いろんなところに認知負荷があるんだということを知れました。

12/15

退路を立ちつつ、ちゃんと有言実行していてかっこいい!!!開発チームでレセプト関係の会話を聞くことはありますが、複雑すぎて理解できる気がしないので、プロは凄いなーと思っています。

12/16

一時期は朝にコーヒーを飲まないと夕方には頭痛がするくらいコーヒー好きな状態でしたが、淹れ方はここまでこだわれるとは思ってもなかったです。深い世界だ。

12/17

仕事でも関わることが多くて、関わるたびに圧倒されるくらい優秀な人です。そんな人が「自分がワントップにならなくて済むぐらいに激強メンバーが揃っている」という組織、どこだろう...?

12/18

弊社では Kotlin がメインの言語になっているけど、僕自身はあんまり触れていないので、まずは気軽に Kotlin Scripting で色々試行錯誤してみるのもいいかなーと思いました。

12/19

一年で1.5倍の組織規模になったけど、その時々でより良い組織になるように色々されていてありがたいです。あと、猫が可愛い。

12/20

複雑なドメインを扱っていてつらみはあるけど、課題が複雑であるほど、解いた時の達成感は大きいですよね。SRE としても複雑な問題に日々直面していますが、直面するたびにわくわくしています。

12/21

便利になるように個人的に作っていたツールがプロダクトに取りこまれてめっちゃいいですね。

12/22

会社の DemoDay で実際に動いているとこを見せてもらった時にも思ったけど、技術説明とかも含めて解説されると、これだけのモノを個人でガッと作っちゃうの、すごくない!?となっちゃう

12/23

異なる医療ドメインのキャッチアップをプログラミング言語の習得に例えられてイメージしやすかったです。

12/24

僕も最初に複雑な医療ドメインをキャッチアップすることに苦労したけど、こういうアプローチもあるんですね。とくにチャンキングの件が参考になりました。

12/25

アドベントカレンダーのいくつかのエントリで触れられていた通り、ヘンリーは今年だけで組織が1.5倍になるほど拡大したけど、来年は今年よりも拡大する見込みなので色々考えることがある!自分にとっても経験したことがない規模で拡大するので今からわくわくしています。

アドベントカレンダーを終えて

最初は25日分のうち8割くらい埋まれば御の字かなーと思って発案したけど、蓋を開けてみたら、あっという間に25日分が集まって、2トラック分やるか...?みたいな会話もされてびっくりしました。また、職種にかかわらずいろんな方に参加してもらえたことも嬉しかったです。エントリの内容も趣味から仕事までバラエティに富んでいてヘンリーのメンバーらしいアドベントカレンダーになったのではないかと思っています。

ヘンリーは来年も拡大しつづけているので、来年のアドベントカレンダーは本当に2トラック分になるかもしれないですね。

そんなヘンリーではさまざまな職種で募集をしています。一緒に日本の医療の DX 化を推進していきませんか?