SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

データ指向アプリケーションデザイン 勉強会メモ

データ指向アプリケーションデザイン 勉強会メモ

勉強会の内容+ α(メモ)

ref:

データ指向

  • オブジェクト指向:オブジェクトを中心にプログラムを設計
  • データ指向:「データ量」「データの複雑さ」「データの変化速度」を中心に考える設計

データ表現

データ量少ない場合:データの表現、生成しやすさを重視

  • テキストデータフォーマット:CSV, JSON, XML など
  • バイナリデータフォーマット:MessagePack, Thrift, Protocol Buffers, Apache Avro
    • (データとして完結している)自己記述型(self-describinig)データフォーマット
      • 例外:ProtocolBuffers はデータ自体にスキーマを含まない(self-describinig ではない)

データ量多い場合(データ基盤):省メモリ、圧縮しやすさ、ストレージの格納しやすさを重視 データを節約してコンパクトにする方法

  • テーブルフォーマット
  • 行指向(row-oriented)フォーマット
    • RDBMS でよく使われる
    • 1行ずつ表現
    • レコード単位で更新しやすい
  • 列指向(column-oriented)フォーマット
    • 同じ型のデータを集めると圧縮しやすい
    • 分析クエリに特化したフォーマット
      • 高速(キャッシュに乗りやすい)
      • SIMD 演算(1つの命令で複数演算器を動かすコンピュータ演算のやり方)も活用しやすい
    • 単一レコードを複数のカラムブロックで表現するため更新が大変(オーバヘッドも高)
    • 例:Apache Request

データアクセス頻度

読込が頻繁

  • 変化しない = 不変データ(イミュータブル)
    • 蓄積されるログ(列指向フォーマットと相性〇)
  • 読み込み特化型 (read-intensive)
    • データへの高速なアクセス、分析の速さが問われる
    • 高速化手法(データを分散)
      • パーティショニング(シャーディング)
        • データを時間範囲(1h, 1d 等)、キー範囲などで分割
        • 必要なデータのみにアクセスが済む、別領域に分かれるため並列アクセス可(高速化)
      • レプリケーション
        • 同じデータを複数個所(マシン、ディスク)に配備し、並列アクセス&耐障害性を確保
      • 実体化ビュー(Materialized View)
        • 一度計算したクエリ結果を保存、再利用可能にする

更新が頻繁(データが 1 か所の場合)

  • 書き込み特化型(write-intensive)

  • 更新対象を素早く見つけられる必要がある = 高速検索

    • → 解決策:インデックス
      • ディスク上のデータを高速に見つけるためのデータ構造
      • B-Tree、Hash index、SSTable(Sorted String Table)
B-Tree
  • ディスク上のページに、次ページへのルックアップテーブルを格納
    • RDBMS では 基本 CREATE INDEX 命令で作られる
    • ※ 1 回の CREATE INDEX で 1 つの B-Tree が作られる。B-Tree を複数作ると更新時にそれらすべて更新する必要があるのでその分更新速度が落ちる
  • 高い更新性能を発揮。空き領域にデータ挿入しやすいため
  • ページ数が増えても木の深さはあまり増えないため、ディスクのランダムアクセスを減らせる=アクセスの回数を減らすのに役立つ
発展:Log-Structured Merge (LSM) Tree

さらに更新が頻繁な場合にも対応できる。(最近よく使われている)

  • レコードの追加、コンパクション操作(恐らくメモリコンパクション=空き領域の断片化を解消して連続した広い空間に再編。メモリ上を整理して合成する操作)を分離
    • ランダムアクセスに強いメモリ上にどんどん追加。更新される旅にメモリ上をソート
    • バックグラウンドで、定期的にソート済みのデータと合成(marge sort)して次レイヤーに保存(L0,L1,… = ディスク、外部ストレージなど)
  • 性能特性
    • 更新時のランダムアクセスを減らせる(シーケンシャルなアクセスなため)
    • 各種オーバヘッドがある
      • 読み込み時に各レイヤーにアクセスする必要がある
      • 書き込みの増幅(Write Amplification)が生じる (次レイヤーに書き出すため同じデータの書き込みが複数回生じる)

例:LevelDB、RocksDB(C++)、AWS S3(Rust) など

分散データ(データが複数個所)

データを複数個所(複数ノード)に分散する場合、考えるべき問題の複雑さが数段上がる

  • ノード間でデータを送受信するために、分散システムが必須
    • 分散ステートマシン=各ノードが同じ状態になる必要がある(membership)
    • (サーバ間のデータを整える目的で) サーバ・クライアント間でのデータ通信も必要(HTTP 上で実装されることが多い)
  • 複数のノードで頻繁に更新がある場合
    • 同期(ロック取得、タイムアウトトランザクション、ログのリプレイ)
    • どのノードの状態が正解かを決めるための合意(consensus)プロトコルが必要
      • (基本的には) 多数決(Quorum)で決める。 Paxos、2PC など
      • あるいは、そもそも調整を避ける(conrdination avoidance)手法もある
  • 障害対応
    • エラーハンドリング
    • エラー時のリトライ
    • 冪等性(同じ操作を 2 回繰り返しても大丈夫なように設計)
    • 障害からの復旧・リカバリ
    • ノードが嘘を付く(間違ったデータを返す)ビザンチン障害と呼ばれる

以上を踏まえて、システムが正しく動作するように設計する必要がある

24 時間 365 日稼働するサービス設計

  • 稼働率:許容されるダウンタイム
    • 99.9 % availability = 8.7 時間/年
    • 99.99 % availability = 53 分/年
    • 99.999 % availability = 5 分/年
  • アプリケーションのデプロイ (その際には止める必要があるが)
    • サービスが動き続ける必要がある
      • =複数バージョンのアプリケーションが同時稼働する(できる必要がある)
    • デプロイ中、あるいはクラッシュ時に通信が遮断される
      • (そのため) クライアント側でリトライ(再実行)処理を実装する必要がある
        • アップグレード中にリクエストが失敗してもリトライすることで次バージョンにトラフィックを流すことができる
  • 冪等性
    • リトライが起こる前提で設計
      • リクエストが成功したがレスポンスが通信障害などによりクライアントに届かなかった場合に、リトライされても問題ないようにする必要がある
      • =同じ操作(リクエスト)が繰り返されても、データを重複登録させない
    • リクエストに UUID などを付けて重複チェック
Compute - Storage の分離 (常にデプロイし続けるための設計)
  • 近年の標準
  • 常にデプロイし続けられるよう、クエリ実行(Compute)とストレージ(Storage)を分離
    • クエリエンジンとストレージを別々にスケールしたり、ローリングアップグレード(同じ機能を持った複数サーバ構成のシステムをアップデートする手法の一つで、システムの稼働状態を維持しながら1台ずつ順番にアップデートしていく方法)

例:SnowflakeAmazon Redshift(データウェアハウスサービス)など

分散トランザクション

そもそもトランザクション処理
  • ACID :Atomicity(不可分性)、Consistency(一貫性)、Isolation(独立性/分離性)、Durability(永続性)
  • 分離性(Isolation)が最も重要
    • 直列化可能性(Serializability)が基本
      • トランザクションが1つずつ実行されたのと変わらない状態を維持
      • オーバヘッドが大きいため現実的には使われにくい(使う場合は考える必要がある)
    • 解消する手段:弱い分離性
      • Read-Committed、Snapshot Isolation、MVCC(Multi-version concurrency contorol)
    • 妥協しないアプローチ:Serializable Snapshot Isolation (SSI)
      • トランザクション異常(anomaly)の種類
        • ファントム(まだ存在しないレコードに、どうロックをかけるか)※SSI だと防げない
        • Write Skew(同じスナップショットを読んで、違う場所に書き込む)
      • Repeatable Read (ダーティリード:なし、ノンリピータブル:なし、ファントム:あり)とは違う
分散トランザクションの世界
  • 複数ノード間、または複数システム間でトランザクションを実装
    • 一貫性、合意、耐障害性
    • 線形化可能性(linearizability)が重要
      • レプリカが複数あっても、ユーザにはコピーが 1 つしかないように見せる
      • 更新操作が終了した途端、全ノードが同じデータを見られるように保証する
    • 実装するために必要な技術要素 (本 9 章)
      • リーダー選出(leader election)
      • サービスディスカバリ
      • メンバーシップ管理
    • 使用例:Zookeeper、etcd(Kubernetes 内部で使用)
    • 異なった方向性のアプローチ(2022 ~)
極み:Amazon Aurora (SIGMOD 2018)

AWS Aurora: AWS Aurora とは?その特徴とは?

  • クラウド向けリレーショナルデータベース
  • 分散版 MySQL
    • 6つのレプリカを作成
    • 2 copy × 3 AZs 。AZ が一つ落ちても quorum (多数決) が取れる = 4/6
  • MySQL のデータベース操作を「ログ書き込み」のみに特化して簡略化
    • ストレージノードがログを分配、各インスタンスでログをリプレイして同じデータを持つ
      • データのコピーには gossip プロトコル(ランダムにノードを選んで配信、同期システムなしに実装できる)
    • さらに、トランザクション処理も同期をなるべく取らずに処理(Coordination Avoidance)
      • 各ノードの返信を待つ 2 相コミット(2PC)を使わなくても良いよう、MySQL の裏側のストレージ全体を再設計

データの複雑さ

テーブルの意味の変化

  • 従来:RDBMS にある最新データ(Snapshot)
  • 現代:
    • 時間とともに変化するデータ、そこから表す派生するデータ(derived data)を表す
  • 列指向クエリエンジンの発展により、分析クエリが容易になり導出データ(derived data)が大量に
  • バッチ処理とストリーム処理の区別がなくってくる位いろんなデータが作られる

導出データ (derived data)

  • 1つデータから数千の導出データが作られることも (実例:Treasure Data 社)
  • クエリで生成されるデータの依存関係や、データ履歴を管理する必要がある
    • 解決法:
      • dbt:クエリの依存関係を記述できる SQL compiler
      • 新しい Table Format:Delta Lake、iceberg、Apache Hudi など
        • テーブルの更新履歴の確認やスナップショットの管理(time travel)ができる

分散バッチ処理

バッチ処理Unix コマンドでのデータ処理に近い(コマンドのパイプ繋ぎ)

分散(複数ノード)で実行するアイディア

  • Spark (2009)
    • Microsoft DryadLINQ (2008) のアイディアがベース
    • UNIX コマンドのような命令(小さいプログラム)を並列・分散実行するイメージ
  • MapReduce (Google 2004, Hadoop 2006)
    • Mapper (key-value)のペアを出力。
    • Reducer:同じ Key に属する Value を集めて集計結果を出力
    • フレームワーク側が自動で分散実行
      • MapSide Join、Broadcast/Hash Join など様々なテクニックが誕生。SQL も実行可(Hive)

ストリーム処理

  • バッチ処理:保存されているデータに対してクエリ実行
  • ストリーム処理:
    • クエリをデータの入力側に送り、常時クエリを実行し続ける
    • あるいはデータの変更をとらえ処理する。 マイクロバッチ、CDC(Charge Data Capture)
  • (上記の概念を上手にまとめたのが )Dataflow Model の概念が必要
    • 時系列データ処理方法の分類・パターンの定義
    • 遅れてやってくる(late arraival)データの処理を埋め合わせる必要性を提示
      • event time(データを捉えた時刻), processing time(データがシステムで見えるようになった時刻)を 2 次元の watermark で管理
      • 2 次元の軸を分割してもれなくデータを処理する

SLO:システムパフォーマンスのはかり方

事業者が定義・合意した SLA を履行するために、サーバーやネットワーク、ストレージなどの各領域の稼働率、性能、可用性、セキュリティ、サポートといった項目ごとに、パフォーマンスの目標値

  • 平均値を見るだけでは、データ規模が大きく性能が遅くなりやすい重要顧客の様子が見えてこない
  • p99.99 の極端な領域での改善は、コストの割りに利益を生まないこともある

まとめ

  • データ指向アプリケーションデザイン
    • 分散データシステム入門の決定版
    • より深い世界に踏み込むための手引き
    • 最新は自分で情報収集する必要あり。英語論文など

urql による Github & Gitlab の マルチクライアント 実現(サンプル)

urql による Github & Gitlab の マルチクライアント 実現(サンプル)

はじめに

(こんな要件は早々ないだろうが)異なる2つのデータソースから GraphQL によりデータを取得し1画面に表示する

例:Github と Gitlab の両方からデータを取得し、データを統合して画面表示

この例を実現するためのサンプルを記載する

urql でのマルチクライアントの実現

useClient() を使用して client を複数用意しようと思うと、そのうちどの client を使用するか区別する方法がよく分からず。

以下の通り、useQuery() のみを使い、複数のカスタムフックを用意してあげれば、マルチクライアントを実現できる。

ref: urql - multiple clients (Github Discussions)

注意点 (上記リンクにも記載されているが見落としやすいので記載):

import { useMemo } from "react";
import { AnyVariables, useQuery, UseQueryArgs, UseQueryResponse } from "urql";

const buildContext = (url: string, accessToken: string) => {
  return useMemo(() => {
    return {
      url: url,
      fetchOptions: {
        headers: {
          authorization: `Bearer ${accessToken}`,
        },
      },
    };
  }, []);
};

export const useGithubQuery = <
  Data,
  Variables extends AnyVariables = AnyVariables
>(
  args: UseQueryArgs<Variables, Data>
): UseQueryResponse<Data, Variables> => {
  const context = buildContext(
    "https://api.github.com/graphql",
    import.meta.env.DEV ? import.meta.env.VITE_GITHUB_TOKEN : "" // 仮
  );
  return useQuery<Data, Variables>({
    ...args,
    context,
  });
};

export const useGitLabQuery = <
  Data,
  Variables extends AnyVariables = AnyVariables
>(
  args: UseQueryArgs<Variables, Data>
): UseQueryResponse<Data, Variables> => {
  const context = buildContext(
    `${import.meta.env.VITE_GITLAB_URL}/api/graphql`,
    import.meta.env.DEV ? import.meta.env.VITE_GITLAB_TOKEN : "" // 仮
  );
  return useQuery<Data, Variables>({
    ...args,
    context,
  });
};

実例

クエリ:

const GET_GITHUB_REPOSITORIES = gql`
  query ($owner: String!) {
    user(login: $owner) {
      repositories(last: 10) {
        nodes {
          name
          url
        }
      }
    }
  }
`;

export const GET_GITLAB_PROJECTS = gql`
  query ($groupFullPath: ID!) {
    group(fullPath: $groupFullPath) {
      id
      name
      projects {
        nodes {
          name
          webUrl
        }
      }
    }
  }
`;

コンポーネント

export const GitRepositories = () => {
  const [githubResult, reexecuteGithubQuery] = useGithubQuery<RepositoryData>({
    query: GET_GITHUB_REPOSITORIES,
    variables: {
      owner: "Symthy",
    },
  });

  const [gitlabResult, reexecuteGitlabQuery] = useGitLabQuery<Group>({
    query: GET_GITLAB_PROJECTS,
    variables: {
      groupFullPath: "gitlab-instance-b5710bbc",
    },
  });

  const {
    data: githubData,
    fetching: githubFetching,
    error: githubError,
  } = githubResult;
  const {
    data: gitlabData,
    fetching: gitlabFetching,
    error: gitlabError,
  } = gitlabResult;
  /* 以下仮実装 start */
  if (githubFetching || gitlabFetching) return <p>Loading...</p>;
  if (githubError) {
    return <div>An error occurred! {githubError.toString()}</div>;
  }
  if (gitlabError) {
    return <div>An error occurred! {gitlabError.toString()}</div>;
  }
  /* end */

  const githubRepositories = githubData?.user.repositories.nodes.map((repo) => {
    return {
      ...repo,
      source: "Github",
    };
  });
  const gitlabRepositories = gitlabData?.group.projects.nodes.map((repo) => {
    return {
      name: repo.name,
      url: repo.webUrl,
      source: "Gitlab",
    };
  });

  const repositories: { name: string; url: string; source: string }[] = [];
  if (githubRepositories) {
    repositories.push(...githubRepositories);
  }
  if (gitlabRepositories) {
    repositories.push(...gitlabRepositories);
  }

  return (
    <>
      <p>Repositories</p>
      <table border={1}>
        <thead>
          <tr>
            <th>Source</th>
            <th>Name</th>
            <th>URL</th>
          </tr>
        </thead>
        {repositories.map((repo) => (
          <tbody>
            <tr>
              <td align="left">{repo.source}</td>
              <td align="left">{repo.name}</td>
              <td align="left">{repo.url}</td>
            </tr>
          </tbody>
        ))}
      </table>
    </>
  );
};

表示結果:

補足

Gitlab は docker で立てることができる

docker run --detach \
  --hostname gitlab.example.com \
  --publish 443:443 --publish 80:80 --publish 22:22 \
  --name gitlab \
  --restart always \
  --volume $GITLAB_HOME/config:/etc/gitlab \
  --volume $GITLAB_HOME/logs:/var/log/gitlab \
  --volume $GITLAB_HOME/data:/var/opt/gitlab \
  --shm-size 256m \
  gitlab/gitlab-ce:latest

ref: https://docs.gitlab.com/ee/install/docker.html

Java - HttpClient への SSL 実装での javax.net.ssl プロパティについて (+Quarkus 少々)

Java - HttpClient への SSL 実装での javax.net.ssl プロパティについて (+Quarkus 少々)

前提知識メモ

  • トラストストア

    • 自らが信頼する CA(Certificate Authority:認証局)のルート証明書または中間証明書を保存する場所(ファイル)。
    • クライアント側が認証するサーバー側の証明書を格納するファイル?
    • (トラストストアを持つ側=クライアントが SSL 接続を行い)、SSL の通信先がサーバー証明書を送信してきた際に、トラストストアに保存されている証明書によって署名がなされているかどうかによって認証の可否を判断。
  • キーストア

ref: Java の SSLSocket で SSL クライアントと SSL サーバーを実装する

以下、簡単な実装例(トラストストア、キーストアのパスは直指定)

ref: 【Java】SSL 通信を実装する

javax.net.ssl に関して

■ javax.net.ssl.SSLContext

SSLSocketFactory.getDefault () で使用される SSLContext と SSLContext.getDefault()で得られる SSLContext には、以下のシステムプロパティが影響する (※SSLContext.setContext()が行われた倍は除く)

  • javax.net.ssl.keyStore
  • javax.net.ssl.keyStorePassword
  • javax.net.ssl.trustStore
  • javax.net.ssl.trustStorePassword

ref: Get SSLContext for default system truststore in Java(JSEE) - StackOverflow

■ javax.net.ssl.TrustManagerFactory & javax.net.ssl.X509TrustManager

注: null の KeyStore パラメータが SunJSSE の「PKIX」または「SunX509」TrustManagerFactory に渡される場合、ファクトリは次の手順でトラストデータを検索します。

  1. システムプロパティ javax.net.ssl.trustStore が定義されている場合、TrustManagerFactory は、このシステムプロティーで指定したファイル名を使ってファイルを検索し、このファイルをキーストアで使用

refs:

ドキュメントの日本がおかしく正確なところは分からないが、恐らく TrustManagerFactory.getInstance() でアルゴリズムに「PKIX」または「SunX509」を指定した場合、javax.net.ssl.trustStore 等のシステムプロパティが指定されていればそれを優先して使用されると思われる

また、以下の通り

デフォルトのトラストマネージャーのアルゴリズムは「PKIX」です。

デフォルトアルゴリズムは「PKIX」と思われるため TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) で取得した TrustManagerFactory にはjavax.net.ssl.trustStore 等が適用されると思われる

HTTP Client ライブラリ編

Java の HTTP Client はいくつかあるが、今回は google-client-java-api を見ていく(理由は特にない。業務で使われていたため)

google-client-java-api

サンプル

SSLFactory sslFactory = SSLFactory.builder()
    .withIdentityMaterial("identity.jks", "password".toCharArray())
    .withTrustMaterial("truststore.jks", "password".toCharArray())
    .build();
NetHttpTransport httpTransport = new NetHttpTransport.Builder()
    .setSslSocketFactory(sslFactory.getSslSocketFactory())
    .setHostnameVerifier(sslFactory.getHostnameVerifier())
    .build();

// ref: https://sslcontext-kickstart.com/client/google.html

HttpTransport にはいくつか種類がある。

  • NetHttpTransport:JDK の HttpURLConnection がベース (今回はこちらを見る)
  • ApacheHttpTransport:Apache HttpClient がベース
  • 他にもいくつか

NetHttpTransport.Builder を使用して証明書を設定できるようである。

特になにもセットしなければ、sslSocketFactory, hostnameVerifier は null となる。これらは null でなければ NetHttpTransport.buildRequest で、HttpURLConnection (https の場合は HttpsURLConnection) にセットされる。

javax.net.ssl.HttpsURLConnection に関しては以下の通り

このクラスでは、HostnameVerifier と SSLSocketFactory を使用します。どちらのクラスにも、デフォルトの実装が定義されています。 これらの実装は、クラスごと(static)またはインスタンスごとに置き換えることもできます。すべての新しい HttpsURLConnection のインスタンスには、生成時にデフォルトの static 値が割り当てられます。

SSLSocketFactory のデフォルトは、以下で取得できるものと思われる

javax.net.ssl.SSLSocketFactory.getDefault() に関しては以下の通り

このメソッドがはじめて呼び出されると、セキュリティ・プロパティ ssl.SocketFactory.provider が検査されます。null 以外の場合、その名前のクラスがロードされ、インスタンス化されます。 それ以外の場合、このメソッドは SSLContext.getDefault().getSocketFactory()を返します。この呼出しに失敗した場合は、使用できないファクトリが返されます。

SSLContext.getDefault() は、前述の通り javax.net.ssl.* システムプロパティが影響すると思われるため

NetHttpTransport.Builder に何もセットせず、すぐさま build() して得られる NetHttpTransport から生成する HttpRequest (HttpsURLConnection を保持) には、javax.net.ssl.* のシステムプロパティが適用されると思われる。

※推測の域を出ないため要確認

追記:javax.net.ssl.* のシステムプロパティでトラストストアは適用できた。

■ 蛇足:Apache HttpComponents HttpClient

ちょっと参考になりそうなのを見つけたためリンクのみ添付

Quarkus についてメモ

本件に関して調べ始めた発端。

Quarkus アプリケーション内 から 外部の HTTPS リソースにアクセスする場合は、以下等の quarkus のプロパティ(application.conf) は効かない

  • quarkus.http.ssl.certificate.key-store-file
  • quarkus.http.ssl.certificate.key-store-password
  • quarkus.http.ssl.certificate.trust-store-file
  • quarkus.http.ssl.certificate.trust-store-password

これらはあくまで、SSL を使用して Quarkus アプリケーション (サーバ) を公開する際に使用するためのプロパティ (ブラウザ等から Quarkus アプリケーションにアクセスする際に使用されるもの)

ref: プロパティ quarkus.http.ssl.certificate.key-store-file が機能せず、要求されたターゲットへの有効な証明書パスが見つからない

故に、実行時に javax.net.ssl.* のシステムプロパティでトラストストア等を適用する必要がありそう。 ※それでもダメだった場合はソース上でトラストストア等を直接読み込んで、上記で触れた google-client-java-api では NetHttpTransport.Builder にセットすれば適用できそう。

追記:javax.net.ssl.* のシステムプロパティでトラストストアは適用されたため、トラストストアを読み込んで NetHttpTransport.Builder にセットするような処理を自前で用意する必要はなかった。

また、認証局OpenID Connect の認証を行うための仕組みが用意されており、OpenID Connect 時に使用する証明書を指定するためのプロパティが上記とは別に用意されている。

OPENID CONNECT (OIDC) AUTHORIZATION CODE FLOW MECHANISM

ただ、こちらのプロパティを使用すると、パスワードの指定も必須になってしまう。

OIDC の際も、上記プロパティの指定が無ければ、javax.net.ssl.* のシステムプロパティで適用されたトラストストアを使用し、こちらのケースではパスワード指定必須でないため、パスワード指定を避けたい場合はシステムプロパティを使用すると良い。

さいごに

SSL の実装を行ったことがこれまでなく、参考になりそうなとあるサービスのコードを見ていて、証明書検証有効時には特に証明書を読み込むような処理もなく、無効時には https://gist.github.com/kazuhira-r/bb3ca27bc6194ff4900e のような実質検証を行わないような実装となっており、Why? というところからスタートしてからここまで調べ、恐らく javax.net.ssl.truststore 等で証明書適用できる、できなかった場合の代替案としてどういう風にすれば適用できるかまで見れたのでひとまず良しとする。

推測があっていたかは後日追記するかもしれない。

追記:推測は合っていた

Poke Battle Integration App 再設計&今後の方針

Poke Battle Integration App 再設計&今後の方針

旧名:PokeRest から改名(リポジトリhttps://github.com/Symthy/poke-battle-data-mgmt-serv

ポ〇モン対戦のためのサービス開発

半年近くに渡っての開発での失敗、それを踏まえての構成再設計&今後の方針を定義。合わせて何に立ち向かうのかを記載する。

背景/向き合う課題

ポ〇モン対戦は、シンプルなように見えて実に奥が深く、調整次第では(限度はあるが)相性の有利不利を覆すことが可能なのは魅力の1つではないかと思う。

元1プレイヤーとして感じている課題は色々あるが、ここでは個人的に感じているものかつ今回主に焦点を当てるものに留める。

ポ〇モン対戦に関する現状感じている課題

  • ツールがバラバラ ※スマホアプリであれば複数兼ねているものもある

    • 図鑑(タイプ、種族値、覚える技などの基本情報)
    • ダメージ計算
    • 育成個体管理
    • パーティ管理
    • etc.
  • 1 ターンの持ち時間 45 秒 の短い間で考えることが多い

    • その判断材料に「ダメージ量」は重要なファクターの1つ。(上位プレイヤーでも場面によっては正確性のためにダメージ計算を行う程)
      • ダメージ計算を行うには必要な項目が多くあり、慣れた人でも (ツール使用で) 20 秒前後は要しているように感じる
      • 練度が足りない人は、45 秒あっても足りない場合も…
  • 火力調整、耐久調整を細かくやろうと思うと、多くのダメージ計算を回す必要がある

    • 自身で調整を考える際、基本的には対応範囲を広くするためにも、複数の仮想敵に対して火力/耐久ラインの調整を行う。
    • つまり、1 vs 多 の計算を行う必要がある。
      • 大体のダメージ計算ツールは 1 vs 1 形式(知る限りでは良くて 1 vs 2 まで)。
      • 1 vs 多のものはないため、1 vs 1 形式に都度入力値を変えながら調整を考えるのが現状。
  • 1 試合 1 試合を記録し、分析できるようなツールは(恐らく)ほぼない

    • パーティ構築するには環境を知り、タイプ相性以上の個々の相性を知り、自身のパーティについて知り、上位を目指すなら流行りを知る、など研究/分析要素がある。
    • 猛者は対戦をこなして環境や自分でパーティの弱みを見極めて改善を行うが、初心者には敷居が高い
  • 1 シリーズ(1ヶ月)終了毎に、それなりの方が投稿する(パーティの)構築記事を書く手間は大きい

    • パーティコンセプト、それぞれの個体の調整意図や仮想敵、基本選出、動かし方 etc.

要件とアプローチ

ざっくり箇条書き。

  • 1 つのツール(サービス)で、パーティ構築~対戦までが全て完結すること。概ね必要になるのは以下
    • 基本情報の参照・検索
    • 育成のための調整(それに必要となるダメージ計算)
    • (調整 or 育成済み)個体&パーティ管理
    • 対戦時のダメージ計算
    • パーティと戦績の出力
  • 調整を考えるための過程を(可能な限り)スムーズにする
    • 基本情報参照中の(気になった)ポ〇モンに対して(情報引き継いで)ダメージ計算にシフト可能
    • 1 vs 複数の同時ダメージ計算を可能にする
  • 調整したものをそのまま登録できる、パーティ登録もできる
    • ダメージ計算の結果もセットで登録できる
    • 登録した個体からパーティを編成できる。コンセプト等追加情報を記録できる
    • 個体の有利不利を記録できる。パーティ全体の有利不利ポ〇モンを俯瞰視できる
    • パーティメンバーの一部入れ替えを行う際、元のパーティを継承できる
    • 登録したものはエクスポートできる
  • 対戦時のダメージ計算を(可能な限り)短縮する(それにより対戦中1ターンの考察時間を増やす)
    • 登録した個体やパーティの情報を引き継いでダメージ計算を行える
    • 相手の場に出ているポ〇モンだけでなく、控えのポ〇モンに対しても同時にダメージ計算ができる(= 1 vs 6 の計算が実施可能)
    • 入力を最小限で済むよう調整のデフォルトセットを用意する(とりあえずざっくり計算したい時に1クリックで調整の指定を可能にする)
  • 対戦結果を記録できる
    • ダメージ計算で入力した自パーティと相手パーティをそのまま記録できる
    • パーティの戦績として記録可能、エクスポートもできる
    • 対戦結果から得意・不得意の傾向分析ができる
    • (可能なら) 環境(どのポ〇モンのどういった型が多いか)分析ができる

まとめると、

  • 登録/入力したものは引き継いで使用可能(例:個体やパーティ → ダメージ計算)にしたり、入力補助を行うことで入力の簡素化
  • パーティ構築の過程(調整検討など)~結果(戦績)までを記録できるかつ、それを出力できる(構築記事の一部にそのまま使えるのがベスト)
  • (どこまでできるかは分からないが) 自パーティの分析や環境の分析が行える

設計

これまで(失敗)

主だった失敗点としては以下の通り。

  • サーバサイドをモノリスで作ろうとしたこと
  • 転〇時のポートフォリオにしようとし、作ることを急ぐあまりテストを疎かにしたこと
  • 機能単位ではなく部品単位で作っていたがために全体から見ると中途半端になっていること
  • 動く状態にない
  • REST API サーバとして開発していたが、APIユースケースベースに寄っていて設計に歪みがある
  • ベースとなるデータを最初から DB に入れることを考えていたが、ある1テーブルだけで凡そ 50 ~ 100 × 1000 (= 5 万~ 10 万) レコード必要。それに加えてユーザの各種データを持つとなると個人開発でリソースが限られるために厳しく、それに対する対策を出せなかったこと

そもそも何故こんな自体になったのか。

時間がない中、転〇時のポートフォリオにしようという焦りから 完成させるに至らずとも、テストを後回しにしてでも土台となる部分と一番メインとなる機能を作り切り、転〇活動をしながら各 API の実装とテストを進める

という歪んだ方針で開発を押し進めた結果、総 Step 数が1万を越えた辺りから、開発のしづらさを感じた出したことと、上記案図のように分割した方が良いのではないかという考えもあり、完成まで現状の2~3倍実装する必要があるというのがなんとなく見えているが、完成後にサービスを分割するのか?という迷いと、このまま押し通してリリースにこぎつけたとしても機能追加をスムーズに行えない未来がなんとなく見え、モチベーションが続かなくなり失速して頓挫した。

そのため、一度距離を置くとともに、当時はフロントエンドを開発できる程の技量がなかったため、後述のフロントエンドとバックエンドを並行して開発できるようにするために、React の学習に半年程費やして、現在(再設計/再計画)に至る。

目指す姿(再設計過程)

考える中で二転三転したため、その過程も含めて記載する。

前提とする考え

独立したベースデータを保持するキャッシュサービス(以降、「ベースデータキャッシュサービス」と記述)を用意する

細かい変更点は以下の通り。

  • ベースデータに関しては、自身での全て管理するの厳しいため、一旦諦めて外部リソースを頼る => PokeAPI
    • ベースデータの参照頻度には偏りがある(対戦でよく使われるものは参照頻度が高く、それ以外は参照頻度が低い)はず。
    • 全てを均一の速さで取得するよりも、参照頻度が高いものをより速く取得できるようにした方が、ユーザビリティも良くなるはず
    • 故に、ベースデータキャッシュサービスを用意する => 利用 OSS 候補:go-cache
    • そうすることで、速さだけでなく、頼る外部リソースが仮にダウンしたとしても利用中のユーザをしばらくは救えるはず(それだけでは不完全なため代替手段は用意する必要があると思われるが優先度は低め)
    • ただ、最も要となる部分だが外部に頼るという不安要素も抱えることになる
      • 自身でベースデータを管理する算段が立てば、将来的に丸ごとリプレイスする可能性もある。
      • そうなった場合にも、運用を継続しながらリプレイスを容易に進められるよう、独立したサービスに分ける。
    • ベースデータキャッシュサービスの通信インターフェースは Connect を利用する予定
  • 上記以外のユーザデータに関する部分(個体やパーティの情報等)は REST API を止め、GraphQL を使用する
  • ベースデータキャッシュサービスを除いてバックエンドは無くてもフロントエンドのみで利用可能なサービスとする
    • ユーザデータ(個体やパーティの情報等)をどこかに預けず自身で管理したいユーザが一定数いるのではないかと考えているため。また個人開発のためバックエンドが不安定な要素になる懸念があるため、ほぼフロントエンドのみで使用できるようにする必要がある。

構成検討:過程

上記を根底に置き、構成について考えた。バックエンドは2案を検討。

  • フロントエンド(共通。詳細はブラッシュアップ予定)

  • バックエンド案1:責務毎にサービス分割

サービス指向アーキテクチャに則り、サーバサイドは役割毎にサービスを分割する可用性重視の案。

  • バックエンド案2:モジュラモノリスベース

そもそも案1のように細かく分ける必要がないのではないかと考えて出した案。

案1、2共通の部分を除いて評価した。

案1を基準とした時の相対評価(勝る:◎、同等:〇、劣る:△)。項目はひとまずソフトウェア品質特性ベース+ α で評価(機能性/使用性は評価できないため除外)

# 案 1 案 2 評価結果の理由
信頼性 サービスが分かれるため一部サービスがダウンしてもその他は継続運用可能
効率性 サービス分ける分サービス間で通信が必要になる等サーバの資源使用量は増(可用性向上のためにコンテナ化するとなおさら)
保守性 完成直後はおそらくそれほど変わらないと思われる。規模がそれなりに大きくなれば案1に軍配が上がるかもしれない
移植性 単純に別サーバに引っ越しであれば案2の方が分かれていない分容易なように思うが CI/CD の仕組を整えればさほど優劣は付かないと考える。いざというときに分散配置など柔軟に対処が可能なため案 1 に軍配が上がると考える
コスト サービスを分ける分実装コストは案 2 の方が大
アーキテクチャ コスト的に 1度バラしたものを統合/再編 > モジュラモノリスを分解/再編 と考える

(個人開発のためリソースの資源が限られることから) 少なくともバックエンドは 1 サーバに全て入れる予定である。故に、サーバが壊れれば信頼性は 0、かつ 案 2(モジュラモノリスベース)でも、不具合により一部モジュールが機能不全を起こしたとしても、サービスをダウンさせず他モジュールは継続利用可能となるよう作り込めば、案 1 と同等の信頼性は得られるのではないかと推測。

以上より、案2 > 案1と判断する。

構成検討:結論

そこまで考えたところで、1つ考慮が必要な点に気づいた。

  • ダメージ計算ロジックは、フロントエンドとバックエンド両方に必要。

1 vs 多のダメージ計算をフロントエンドで行うのは負荷が大きい可能性があり、基本的には(ユーザの環境に依存して性能差があまり出ないよう)バックエンドで引き受けたい。

だが、バックエンドが一時的に利用不可となっても、フロントエンドのみで(可能な限り)サービス利用を可能としたいため、フロントエンドとバックエンド両方に同じロジックを持たせたい。

となると、フロントエンドは TypeScript を用いて開発することから、共通化しようとするとバックエンドも TypeScript である必要がある。

バックエンドのメイン言語は Golang を採用しているが、ダメージ計算ロジックをユーザデータを扱うサービス内に一緒に含める必要があるかというと、ユーザデータと依存関係を持たせる必要がないため、No。

故にダメージ計算用のモジュールを独立させても問題ない =フロントエンドとソース共通利用のため独立させる。(※ ダメージ計算用のモジュールには、呼び出す導線は GUI のみ、GUI がある前提の機能である、GUI と密接な関係でも問題ない点から tRPC の利用が良いのではないかと考えている)

結論、案1と案2の間のような構成とすることとした。

今後の方針

これまで開発してきた以下ソースについては、不要になる部分もあるが、使える部分も多くある。また、完全ではないが、一部コードの自動生成の仕組みも用意している。

まずは、

  • ベースデータキャッシュサービスの開発に取り組むとともに
  • (過去の失敗を活かすべく) テストによる動作担保に取り組む(特に DB 操作)
  • モジュール単位で構成を再編。コード自動生成の仕組強化にも取り組む。

基本的にフロントエンド~バックエンドまでをユースケース単位で開発を進め、できたところから公開する。(公開することを前提に進めてこなかった点への反省と戒めを込めて)

DB に関しては postgresql 使用予定から sqlite に変更予定。postgresql が使える良いサービスが見つかっていない、かつ まずはユーザのデータはユーザに持ってもらうような運用にするため、持つデータは多くない。DB 移行が必要になった際に痛みを伴うが、まずは確実に運用できる形から始める(過去の失敗を踏まえて)。

Git Review Comment Acumulator 企画/設計

Git Review Comment Acumulator 企画/設計

背景

チームメンバーの技術力や自走力の低さが根底の問題として存在している。

一時期、その解消目的で私が 7 ~ 8 割り主体で勉強会を開催していたが、一方通行ではうまくいかず(全く結果に結びつかず、やるだけ無駄という判断を下さざるを得なくなった)、止めた。

代わりとなるものを探していたが、中々良いものが見つからなかった

他でうまくいっている方法として、読書会や業務で使えそうなツールを探して共有等の方法があるが、制約が大きいこともありやろうとはならず。

ただ、他でうまくいっている方法をいくつか見る中で、共通点はアウトプットサイクルを回すことだと推測した。

恐らく外でそのための題材やアイディアを探しても、現環境に適用できるものは見つからないと見切りをつけ、内部にアウトプットサイクルを回せる題材がないかと探した。

それで辿り着いたのが、コードレビューの指摘であった。

コードレビュー指摘であれば毎 Sprint 誰かが何かしら指摘をもらう。それを次に繋げるためにどうすればよいかを考え調べ振り返ってもらうだけでなく、チームメンバーにも共有することでアウトプットし、(恐らく主に私が行うことになるが)何かしらのフィードが行えれば、アウトプットサイクルを構築できると考えた(更に指摘が特定の人以外に共有されないという問題も一緒に解決できると踏んでいる)。

だがそこには障害があり、コードレビューの指摘(PR のコメント)を収集するには多少なりとも時間がかかることから、そこに各メンバーの時間を使わせたくないと、上司に巻き取られるも行われず自然消滅させられた。

裏を返せば、コメント収集が簡単に行われれば実行可能であると判断。

過去に pythonGithub からコメントを収集するための CLI ツールを作るも、開発環境面の問題から実行に多少の敷居があるために誰にも使われず。

故に、GUI ツールを作るしかないため、ブラウザで使えるよう Web アプリ(SPA)を作れば敷居0にでき、実行に移せると考えて開発を行うこととした。

要件

  • 画面で容易に操作できる(CLI では使われない)
  • Github と Gitlab による差異は最小限にし共通のインターフェースとしたい
    • Github のみ、Gitlab のみを使用している環境でも使える
  • Github の PR と Gitlab の MR の両方からコメントを取得&閲覧できる
    • 所属する組織の Github と Gitlab の両方のリポジトリの一覧が見れる
    • 両方から複数の PR/MR を複数同時選択して閲覧したい
    • Github の PR の Conversation のようにコメント対象ソース周辺もセットで見れる
    • 特定のコメントに対してメモを付けてストックできるようにしたい
      • ストックしたものは保存でき、いつでも閲覧できるようにしたい
      • ストックする際にカテゴリ等で分類できるようにしたい
      • ストックしたものを検索できる、言語別にも見れる
    • 収集するコメントはフィルタリングできるようにしたい
      • PR/MR 作成者、レビュー者など
  • Web ブラウザのみでも使用可能とする

設計

リポジトリ、PR/MR、コメントの情報取得には以下を利用する(どちらも同様の情報が取得できそうである)

大まかな構成は以下の通り。

データ(ストックするコメント)の保持は以下のような方針とする。

  • Web ブラウザのみで使用できるよう、データはブラウザ上で保持する(保存先候補:IndexedDB)。
  • データはインポート/エクスポートできるようにする(データが飛ぶ可能性があるのでユーザに保持してもらう)。
  • 上記が面倒なユーザのために、バックエンドを提供する(Docker で環境提供してユーザの環境で立ててもらうことを想定)。バックエンドを使用するか否かはユーザが自由に選択できるようにする。

SPA 開発には React を使用する。

開発計画

まずは、Github と Gitlab から 指定した PR/MR からコメントを取得して表示できるようにする(これをできるようにするにするだけでやりたいことは実施できるため)。

そこから、ユーザビリティ向上のため、複数 PR/MR から一括でコメントを取得/表示できるようにし、 ストックしておきたいコメントを保存&エクスポート/インポートできるようにしていく(他にも追加した方が良いものが見つかれば追加していく)。

バックエンドのサポートは最後の予定とする。

個人的には、このアプリから Github/Gitlab のレビュー&コメント投稿をできるようにすれば、今後参考になる/なりそうだとストックしておいた過去の指摘を素早く引き出して、そのまま流用するといったことができるようになり、多少レビューのスムーズ化に繋がったりしないかと妄想している(やってみなければ分からないし、やるなら他との明確な差別化が必要と思われる)。

リポジトリ

2022 振り返り

2022 振り返り

2022 年を振り返る

  • アウトプット
  • 業務面&個人面
  • 2023 年の目標

アウトプット面

3年目了。なんだかんだ1年に1回は振り返りを行ってはやり方や使用媒体を変更し、多少なりとも改善を図っている。

1年前の改善事項

今回どうするかを触れる前に1年前を振り返る。

1年前は、以下自身のドキュメント管理用ツール(以降、個人ツールと記載)を作ってしまえと考え、2022 年頭に開発して1年間はこれを使ってきた。

https://github.com/Symthy/docs-and-blog-enty-manager

作った経緯:個人用ドキュメント&ブログ管理ツール(ガチめ)作成記

(上記ツールにより「管理」という煩わしさからある程度解放されたことによる恩恵かはわからないが)アウトプットについての自身のスタイルのようなものがようやく見えてきた。

今回の改善事項

VSCode× 自作ツールを使う中で、いくつかの気づきがあった。

  • VSCode よりは Notion の方が書き味がいい(というよりは書いたものがそのまま見た目になるため見やすい。Markdown で書くとプレビュー表示が必要になる)
  • ある程度自動化したとはいえ、ちょっとした小さいものを書くには VSCode +自作ツールではまだ敷居(というよりは多少の煩わしさ)を感じている。
  • それ故か、色々記事を見たりして、Twitter ではリツして疑似ピン留めや、はてなブックマークで後で読むに入れる等しているが、それらはそれ限りで埋もれ OUTPUT に繋がっていない(自作ツール導入により解決を期待したができていなかった)

一度 Notion の使用をやめようと思ったが、Notion のデータベース機能が何かを Stock していくには便利である点。

そこで、個人的な目標としている INPUT:OUTPUT = 1:1 以上 を成立させるためにも、以下の「Link Stock」テーブルのようなスタイルで、気になった or 調べて見つけた記事等を(URL と Summary 記載の上)Stock していくのが良いと考えた

https://protective-metatarsal-484.notion.site/Link-TechTips-Stack-bbcc6dfc9bd44256afc542cf9858ad52

また、Notion を再度利用するにあたっては以下を参考にし、個人技術メモの導入と、タグライブラリーを導入することとした。

そうすると、今度はブログをどう使うかが曖昧になってしまったが、以下のような使い分けをするのが良いと考えた。

たまたま、上記 Connect の記事を書いたことで、自身のアウトプットのスタイルとして何かを小出しにするよりも調べまくってまとめて1つのでか物を出す方が性に合っていると認識した。

Notion に個人的なメモを溜め何か1つのテーマについて記事を書く際にそのメモを材料にしつつまとめてブログにアウトプットするというのが現時点で考える理想の形である。

個人ツールの今後についてのアイディア

追加したい機能はいくつかあり、優先度的に後回しになりがちではあるが

obsidian との併用をできるように改修を行うのが良さそうに思っている。

obsidian のベースとなっている手法/考え方を理解する必要があるが、その一部に個人ツールの根底にある考えと重なる部分もあること、取り入れることで記事数が多くなった際に個人ツールで衝突するであろう課題も解消できると共に、ツールをより昇華させれると考える。

Obsidaian 参考

業務面&個人面

これまでと 2022 年

3 年前の転〇活動でどん底な現状を認識し、3 年かけてでも再起すると誓い、その 3 年が経過した今。業務面の目標、そして最低限ではあるが個人面での目標をクリアした。

  • 業務面
    • 1つ以上自身の実力でもって自身に自信を持てるだけの成果を作る ⇒ 達成
      • 数少ないチャンスを掴み取って遂に今年達成した(しかも2つ作れた)
  • 個人面

業務面での成果は、この3年間色々な技術要素に触れ学び、技術力や対応力等を地道に上げ続けたからこそ達成できたと思う。たかが3年されど3年。今度は専門性を築くためにもまだまだ研鑽を積む必要がある。

個人面では、今年は主に以下2つの学習を進めた。

(他に細かいものを上げると gRPC、GraphQL、AWS 等も)

React はまだ学習が必要だが、某社フロントエンド試験を題材とした学習/開発を通して、自身で企画したものを(時間はかかるだろうが)作り上げる自信が付いたのは個人的に大きな収穫であった。

Golang に関してもポートフォリオ開発には失敗しているものの、そのために Golang を半年近く触り続け多少なりとも理解が進んだことが業務の成果に繋がったことは何よりも大きかった。

今年(2022/12/30 時点)の Github は以下の通り。(これを見て振り返ると業務高負荷により何度かプチバーンアウト(特に 4~5 月が酷かった)を起こしたり今年も体重をロストして過去最低更新する等散々だったなぁと…)

2023 年目標

なんとなく目指したい方向も見え、そこを目指すためにも、更に技術力等を上げるためにも

  • 転〇し、次なるスタートを切る

これは絶対条件。また React 学習を優先したため後回しにした

  • AWS 学習(資格取得)

また、技術面に関しては次なるスタートのためにもポートフォリオ開発のため以下の学習が中心となると思われる。

そして、引き続きポートフォリオ開発を進める。現在開発中のものは構想も大きいため今考えているものだけでも数年がかりとなると推測しているが地道に進めていく。加えて業務の改善と個人的にも欲しいためにもう1つ 2023 年に開発を行う。

他にも細かいことを上げれば色々あるが、転〇先によってこの辺りは変わるだろう。なによりもまずは転〇のため今できることを全力で行おう。

たとえ進む速度が遅くとも地道に足掻き続けるしか道はない故にその道を行く。

以上

【マイクロサービス】可観測性と OpenTelemetry (基本のみ)

【マイクロサービス】可観測性と OpenTelemetry (基本のみ)

OpenTelemetry の以下ドキュメントの内容をまとめた。

可観測性のコア概念

OpenTelemetry を知るうえで押さえておくべき概念のためまとめる。

可観測性(オブザーバビリティ)

内部の仕組を知らなくてもシステムについて質問(「なぜこれが起こっているのか?」など)できるようにすることで、システムを外部から理解できるようにする(簡単にトラブルシューティングして処理できるようにする)。

信頼性と指標

テレメトリ

システムから送信された、その動作に関するデータ(Traces、Metrics、Logs)

信頼性

サービスはユーザーが期待していることを実行しているか?」という質問に答える。100%稼働していても、出力が期待通りでなければ「信用できない」となる。

メトリクス (指標)

インフラストラクチャまたはアプリケーションに関する一定期間の数値データの集計です。

例:システム エラー率、CPU 使用率、特定のサービスの要求率など

SLI (Service Level Indicator)

サービスの動作の測定値を表す。ユーザーの観点からサービスを測定。

SLI の例:Web ページの読み込み速度

SLO (Service Level Objective)

組織や他のチームに信頼性を伝達する手段。

1 つ以上の SLI をビジネス価値に関連付けることによって実現される。

分散トレース

基本要素

  • ログ:サービスまたはその他のコンポーネントによって発行されるタイムスタンプ付きのメッセージ。(ただし Traces とは異なり、それらは必ずしも特定のユーザー要求またはトランザクションに関連付けられているわけではない)

  • スパン:作業または操作の単位を表す。リクエストが行う特定の操作を追跡し、その操作が実行されたときに何が起こったのかを把握するためのもの(名前、時間関連のデータ、 構造化されたログ メッセージ、および その他のメタデータ (つまり、属性)が含まれ、追跡する操作に関する情報を提供)ref: スパン属性

分散トレース

  • マイクロサービスやサーバーレスアプリケーションなどのマルチサービスアーキテクチャを介して伝播するときに (アプリケーションまたはエンドユーザーによって作成された) リクエストが辿るパスを記録 = 追跡(分散システムにおけるパフォーマンスの問題の原因特定に用いる)

  • トレースは、分散システムを通過するときにリクエスト内で何が起こるかを分析することで、分散システムのデバッグと理解を容易にする。また、アプリケーションまたはシステムの状態の可視性が向上し、ローカルでの再現が困難な動作をデバッグできるようになる

  • 多くのオブザーバビリティバックエンドは、トレースを以下のようなウォーターフォールダイアグラムとして視覚化する

OpenTelemetry とは

データを取り込み&変換し、Observability バックエンド (つまり、オープン ソースまたは商用ベンダー) に送信するための、標準化されたベンダーに依存しない SDKAPI、およびツールのセットを提供する。

背景

分散されているシステムが拡張されるにつれて、開発者が自分のサービスが他のサービスにどのように依存しているか、または他のサービスにどのように影響しているかを確認することがますます困難になった。(特に速度と精度が重要なデプロイ後または停止中)

システムを監視可能にするには、インストルメント化する必要がある。つまり

  • コードは trace、 metrics、 logs を発行する必要がある
  • 計測されたデータを Observability バックエンドに送信する必要がある

が、Observability バックエンド は様々あり、Observability バックエンドに送信するデータ(テレメトリ)の形式は標準化されていない = Observability バックエンドの切り替えには、コード再実施や新しい Agent を立てる必要がでてくる(データの移植性がない)。

標準化のためにできた2つの OSS(OpenTracing、OpenCensus)が統合されてできたのが、OpenTelemetry。

Getting Started (チュートリアル)

チュートリアルを通してみた。コードは以下のままのため部品のみまとめる。

https://opentelemetry.io/docs/instrumentation/go/getting-started/

  • スパン
    • これによりスパンが決まる
newCtx, span := otel.Tracer(name).Start(ctx, "<SpanName>")
// :
span.End()

Getting Started のコードの App.Run()のループにおける各実行のトレース

Run
├── Poll
└── Write
    └── Fibonacci
  • コンソールエクスポーター
    • テレメトリを OpenTelemetry API からエクスポーターに接続する。
    • エクスポーターは、テレメトリ データをコンソール or リモートシステム or コレクターに送信するためのもの。
    • 後で SDK を構成してテレメトリ データを SDK に送信するときに使用
func newExporter(w io.Writer) (trace.SpanExporter, error) {
    return stdouttrace.New(
        stdouttrace.WithWriter(w),
        // Use human-readable output.
        stdouttrace.WithPrettyPrint(),
        // Do not print timestamps for the demo.
        stdouttrace.WithoutTimestamps(),
    )
}
  • リソース
    • テレメトリデータがどのサービスからまたはどのサービスインスタンスから来ているかを識別する方法が必要
    • テレメトリを生成するエンティティ = Resource
    • SDK が処理するすべてのテレメトリ データに関連付けたい情報は、以下により返された Resource に追加される。Resource を TracerProvider を登録することで、それがなされる
    • ここで設定した情報が、出力の InstrumentationLibrary になる。
func newResource() *resource.Resource {
    r, _ := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String("fib"),  // どのサービス(自分)からなのかを指定
            semconv.ServiceVersionKey.String("v0.1.0"),
            attribute.String("environment", "demo"),
        ),
    )
    return r
}
  • トレーサープロバイダー
    • トレーサーをトレーサープロバイダーに登録
    • trace.WithBatcher() = データのバッチ処理は良い方法であり、下流のシステムに過負荷をかけないようにするのに役立つ
    • 以下単純な例では、グローバルプロバイダーを使用する方が理にかなっているが、より複雑なコードベースや分散コードベースでは、これらの他の方法で TracerProviders を渡す方が理にかなっている = otel.SetTracerProvider() は グローバルプロバイダー?他にも渡す方法がある。
l := log.New(os.Stdout, "", 0)

// Write telemetry data to a file.
f, err := os.Create("traces.txt")
if err != nil {
    l.Fatal(err)
}
defer f.Close()

exp, err := newExporter(f)
if err != nil {
    l.Fatal(err)
}

tp := trace.NewTracerProvider(
    trace.WithBatcher(exp),  // トレーサー登録
    trace.WithResource(newResource()),
)
defer func() {
    if err := tp.Shutdown(context.Background()); err != nil {
        l.Fatal(err)
    }
}()
otel.SetTracerProvider(tp)
  • エラー
// func Fibonacci()
if n > 93 {
    return 0, fmt.Errorf("unsupported fibonacci number %d: too large", n)
}
// func (a *App) Write
f, err := Fibonacci(n)
if err != nil {
    span.RecordError(err)
    span.SetStatus(codes.Error, err.Error())
}
return f, err

動作

$ go run cmd/main.go
What Fibonacci number would you like to know:
11
Fibonacci(11) = 89

出力

{
    "Name": "Poll",
    "SpanContext": {
        "TraceID": "7fa9acb4bf524b96c5c7241c96d71cf1",
        "SpanID": "adf6231c571ca0c2",
        "TraceFlags": "01",
        "TraceState": "",
        "Remote": false
    },
    "Parent": {
        "TraceID": "7fa9acb4bf524b96c5c7241c96d71cf1",
        "SpanID": "9b4bc8636e7cfdb7",
        "TraceFlags": "01",
        "TraceState": "",
        "Remote": false
    },
    "SpanKind": 1,
    "StartTime": "0001-01-01T00:00:00Z",
    "EndTime": "0001-01-01T00:00:00Z",
    "Attributes": [
        {
            "Key": "request.n",
            "Value": {
                "Type": "STRING",
                "Value": "11"
            }
        }
    ],
    "Events": null,
    "Links": null,
    "Status": {
        "Code": "Unset",
        "Description": ""
    },
    "DroppedAttributes": 0,
    "DroppedEvents": 0,
    "DroppedLinks": 0,
    "ChildSpanCount": 0,
    "Resource": null,
    "InstrumentationLibrary": {
        "Name": "fib",
        "Version": "",
        "SchemaURL": ""
    }
}
{
    "Name": "Fibonacci",
    "SpanContext": {
        "TraceID": "0083174227c68c673cf9c5c98e3d3b54",
        "SpanID": "50df65de267b17c1",
        "TraceFlags": "01",
        "TraceState": "",
        "Remote": false
    },
    "Parent": {
        "TraceID": "0083174227c68c673cf9c5c98e3d3b54",
        "SpanID": "6cb2740b4be9a347",
        "TraceFlags": "01",
        "TraceState": "",
        "Remote": false
    },
    "SpanKind": 1,
    "StartTime": "0001-01-01T00:00:00Z",
    "EndTime": "0001-01-01T00:00:00Z",
    "Attributes": null,
    "Events": null,
    "Links": null,
    "Status": {
        "Code": "Unset",
        "Description": ""
    },
    "DroppedAttributes": 0,
    "DroppedEvents": 0,
    "DroppedLinks": 0,
    "ChildSpanCount": 0,
    "Resource": null,
    "InstrumentationLibrary": {
        "Name": "fib",
        "Version": "",
        "SchemaURL": ""
    }
}
{
    "Name": "Write",
    "SpanContext": {
        "TraceID": "0083174227c68c673cf9c5c98e3d3b54",
        "SpanID": "6cb2740b4be9a347",
        "TraceFlags": "01",
        "TraceState": "",
        "Remote": false
    },
    "Parent": {
        "TraceID": "00000000000000000000000000000000",
        "SpanID": "0000000000000000",
        "TraceFlags": "00",
        "TraceState": "",
        "Remote": false
    },
    "SpanKind": 1,
    "StartTime": "0001-01-01T00:00:00Z",
    "EndTime": "0001-01-01T00:00:00Z",
    "Attributes": null,
    "Events": null,
    "Links": null,
    "Status": {
        "Code": "Unset",
        "Description": ""
    },
    "DroppedAttributes": 0,
    "DroppedEvents": 0,
    "DroppedLinks": 0,
    "ChildSpanCount": 1,
    "Resource": null,
    "InstrumentationLibrary": {
        "Name": "fib",
        "Version": "",
        "SchemaURL": ""
    }
}
{
    "Name": "Run",
    "SpanContext": {
        "TraceID": "7fa9acb4bf524b96c5c7241c96d71cf1",
        "SpanID": "9b4bc8636e7cfdb7",
        "TraceFlags": "01",
        "TraceState": "",
        "Remote": false
    },
    "Parent": {
        "TraceID": "00000000000000000000000000000000",
        "SpanID": "0000000000000000",
        "TraceFlags": "00",
        "TraceState": "",
        "Remote": false
    },
    "SpanKind": 1,
    "StartTime": "0001-01-01T00:00:00Z",
    "EndTime": "0001-01-01T00:00:00Z",
    "Attributes": null,
    "Events": null,
    "Links": null,
    "Status": {
        "Code": "Unset",
        "Description": ""
    },
    "DroppedAttributes": 0,
    "DroppedEvents": 0,
    "DroppedLinks": 0,
    "ChildSpanCount": 1,
    "Resource": null,
    "InstrumentationLibrary": {
        "Name": "fib",
        "Version": "",
        "SchemaURL": ""
    }
}