TECH PLAY

NTTドコモビジネス

NTTドコモビジネス の技術ブログ

613

DifyのMCPプラグインとZapier MCPを利用してDifyとSnowflakeを連携させ、Snowflakeのデータを自然言語で扱ってみました。本記事では、その連携方法を中心に紹介したいと思います。 はじめに 利用したサービス Dify Zapier Snowflake 構成 連携設定 Snowflake の設定 Zapierの設定 Dify の設定 動作確認 まとめ 参考 はじめに こんにちは。NTTコミュニケーションズの大島です。普段は、クラウドサービスを中心に、データレイクやデータウェアハウスの検証をしています。 最近注目されている MCP (Model Context Protocol) という技術があります。 これはAnthropic が発表したオープンなプロトコルで、AI と外部システムの接続を標準化するものです。 LLMを利用したアプリケーション(MCPクライアント)が、MCPサーバーを介し外部システムのツールを動作させるために利用されます。 私の目線だと、このMCPにより、LLMとデータとの連携が簡単になるかが気になるところです。 そこで今回は、DifyのMCPプラグインとZapier MCPを利用してDifyとSnowflakeを連携させ、Snowflakeのデータを自然言語で扱ってみました。本記事では、その連携方法を中心に紹介したいと思います。 利用したサービス まず、簡単に本記事に登場するサービスについて紹介します。 Dify Dify はLLMアプリケーションをローコードで開発するためのプラットフォームです。LLMを利用したチャットボットやRAG等のアプリケーションが簡単に作成できます。 MCPプラグインを用いることでMCPクライアントとして利用することも可能です。 Zapier Zapier 自体は、Webサービスやアプリケーションを連携させて、作業を自動化できるサービスです。 Zapierは現在β版として、MCPサーバーとしての機能を提供しています。これを使うことで、Zapierを介してSnowflakeを含めた多数の外部サービスが利用できます。 Snowflake Snowflake は、言わずと知れたクラウドのデータウェアハウスサービスです。 構成 今回は、DifyをMCPクライアント、ZapierをMCPサーバーとして利用します。ZapierはMCPクライアントから受けたリクエストを元に、Snowflake に格納したデータを取得します。このような構成をとることで、LLMがSnowflakeのデータを利用できるようになります。 なお、MCPを利用するための選択肢としては、ほかに Claude for desktop 等のローカル環境で動作するアプリケーションもあります。今回は、Difyであれば応用する際のユースケースの幅が広いと考え、これを利用しました。 連携設定 それでは、連携に必要な設定について説明していきます。各サービスのアカウントは保有しているものとして、連携設定にスコープをあてて記述します。 Snowflake の設定 Zapier経由でアクセスさせるための設定をします。具体的にはSnowflakeにOAuthの設定をいれます。(参考: カスタムクライアント用のSnowflake OAuth の構成 | Snowflake Documentation ) まず、 ACCOUNTADMIN ロールを付与したアカウントで、OAuth integration を作成します。 CREATE SECURITY INTEGRATION oauth_kp_int TYPE = OAUTH ENABLED = TRUE OAUTH_CLIENT = CUSTOM OAUTH_CLIENT_TYPE = ' CONFIDENTIAL ' OAUTH_REDIRECT_URI = ' https://zapier.com/dashboard/auth/oauth/return/SnowflakeCLIAPI/ ' OAUTH_ISSUE_REFRESH_TOKENS = TRUE OAUTH_REFRESH_TOKEN_VALIDITY = 86400 ; OAUTH_REDIRECT_URI の値は、Zapier側画面(後述)で指定される値です。ちなみに、 OAUTH_REFRESH_TOKEN_VALIDITYは、トークンの有効期間で単位は秒です。 作成したOAuth integration のclient ID とsecret (Zapier側の設定で必要)は以下のSQLで取得できます。 select SYSTEM$SHOW_OAUTH_CLIENT_SECRETS( ' OAUTH_KP_INT ' ); --実行結果例 { " OAUTH_CLIENT_SECRET_2 " : " nEVL**************************************** " , " OAUTH_CLIENT_SECRET " : " /cId**************************************** " , " OAUTH_CLIENT_ID " : " Tnx1************************ " } secretは 2つ払い出されますが、どちらを使ってもOKです。 Zapierの設定 Zapier MCP 設定ページ にアクセスして、Zapier MCP Server URL を取得します。Zapier MCP Server URLは、MCPクライアント側、つまりDify 側の設定で必要となります。 Edit MCP Actions を押下し、 Add a new action の項目で、文字列 "Snowflake" で検索します。いくつかヒットしますが、今回は Execute SQL を選択しました。 つづいて、Snowflake Acoount のところで、Snowflake の接続情報を入力します。 Connect New を押すと以下の画面が開きます。入力が必須なのは、Account, ClientID, Client Secret です。Account は Snowflakeにアクセスする際のURLの .snowflakecomputing.com より前の部分です。Client ID, Client Secretは、前段 Snowflakeの設定で作成した OAuth Integrationの Client ID, Client Secretを指定します。 これらを設定し Yes, Continue to Snowflake をおすと、Snowflake のログイン画面に遷移するので、ユーザー、パスワード(+必要に応じMFA)でログインすると接続情報が登録されます。 前段の画面に戻りますので、残りの SQL Statement について設定します。選択肢として Set a specific value in this field (SQL文を指定する)と Have AI guess value fields (AIに推測させる)の2つが選べます。AIに推測させてみたいので、 Have AI guess value fields を選択して動作確認することにします。 Dify の設定 まず、MCP SSE プラグインをインストールします。この MCP SSE プラグインはコミュニティから提供されているものではあるものの、Dify公式サイトのベストプラクティス Dify MCPプラグインガイド でも紹介されています。 Dify のマーケットプレイスで "MCP SSE" プラグインを検索してインストールします。 プラグインの認証設定で必要となる MCP Servers Config には、以下の情報を投入します。url には、Zapier 設定の箇所で取得した Zapier MCP Server URL を入力します。 timeout 値などは、必要に応じて適切な値にしてください。 { "server_name": { "url": "https://actions.zapier.com/mcp/*******/sse", "headers": {}, "timeout": 300, "sse_read_timeout": 300 } } 続いて、同様にマーケットプレイスから Agent Strategies (Support MCP Tools) プラグインをインストールします。プラグインのインストールが完了すれば利用可能です。 動作確認 動作確認の目的で、Difyにて以下のようなシンプルな Chatflow を作成しました。 LLM は Azure OpenAI の o1 を利用しました。 Agent ノードでは、AGENTIC STRATEGY の項目で、インストールしたAgent Strategies (Support MCP Tools) の Function Calling (Support MCP Tools) を選択します。 同様に TOOL LIST の項目で、MCP Tools の Fetch MCP Tools と Call MCP Tool を追加します。 MCP SEVERS CONFIG は、プラグインインストール時に設定した値と同じものを設定します。 Snowflake には、Kaggleの Books Dataset のデータを事前に格納してあります。 Snowflake 内にどんなデータがあるか、見てもらいます。 きちんとテーブルやカラムの情報が取得できています。Snowflake のクエリヒストリを見ると、SHOW DATABASES, SHOW SCHEMAS ~, SHOW TABLES ~, DESCRIBE TABLE ~ のクエリを、合計で7クエリ実行していました。 この書籍に関するテーブルから、データを取得してみます。 シェイクスピアの書籍情報を取得してくれました。Snowflakeへのクエリは以下を投げていました。 SELECT * FROM BOOKS_DATASET_RAW WHERE TITLE ILIKE ' %shakespeare% ' OR AUTHORS ILIKE ' %shakespeare% ' ちなみに、Agentノードの設定でMemoryをonにしていますが、これが off では上記のやりとりはうまくいきませんでした。 やりとりを記憶しないので、最初の質問でテーブルのカラム名を調べているにもかかわらず、次の「シェイクスピアの書籍の情報を取得して」の指示に対し、誤ったカラム名(AUTHORS ではなくAUTHOR)のSELECT 文を生成するという結果になりました。 まとめ Dify のMCPプラグインを用いて Zapier MCPを利用することで、LLMにSnowflakeのデータを利用させることが簡単にできました。 今回は、Snowflakeにどんなデータがあるかの確認と、そこからデータを取り出すという初歩的な使い方まででしたが、高度なことができるかさらに調査を進めていきたいと思います。 また、今回Difyを用いて検証を実施しましたが、DifyからMCPを用いてさまざまな外部サービスを利用できるのであれば、Difyのワークフロー機能などと組み合わせることでより複雑なこともできそうです。 変化の激しい今この分野においては、Difyのようなツールを利用して、新しい技術やそれを活用するアイディアなどを、簡単に手早くどんどん試していくのもアリだと思いました。 参考 https://docs.snowflake.com/ja/user-guide/oauth-custom#integration-example) https://docs.dify.ai/ja-jp/plugins/best-practice/how-to-use-mcp-zapier
アバター
みなさんこんにちは、イノベーションセンターの益本 (@masaomi346) です。 Network Analytics for Security (以下、NA4Sec) プロジェクトのメンバーとして活動しています。 この記事では、2025年5月31日に開催されたドコモグループ学生向けテックワークショップでの取り組みについて紹介します。 ぜひ最後まで読んでみてください。 ドコモグループの学生向けテックワークショップについて ドコモグループでは、現場のエンジニアと一緒にハンズオン形式で学ぶことができる1dayのワークショップを毎年開催しています。 過去のテックワークショップ セキュリティ分野のテックワークショップも開催されており、今回は以下のテーマで開催させていただきました。 ドコモグループのセキュリティ業務とフィッシングサイトの仕組みを学ぶ 今回のテックワークショップについて フィッシング詐欺による被害が増加しており、無視できない脅威の1つになっています。 今回開催したテックワークショップでは、フィッシングサイトの構築に使われているツールであるフィッシングキットの分析を通じて、フィッシングサイトがどのように動作しているのかを学ぶ内容になっています。 フィッシングサイトの仕組みについては、過去に書いたブログ記事でも紹介されています。 フィッシングサイトの仕組みを知ることで、フィッシング詐欺を理解する フィッシング詐欺をテーマにしたテックワークショップを開催したのは今回が初めてです。 テックワークショップの内容は以下のようになっています。 ドコモグループでのセキュリティ業務紹介 講義 分析ハンズオン 内容振り返り 懇親会 分析ハンズオンの様子 まず導入として、フィッシング詐欺に関する講義をしました。 フィッシング詐欺がどのように行われているのか、フィッシングサイトがどのように作られているのかを紹介しました。 講義の後に、チームを作ってフィッシングキットを分析してもらいました。 書かれているコードを分析するだけでなく、分析環境の中で実際にフィッシングキットを動かしながら分析してもらいました。 実際に動かしてみることで、どのように動作しているのかより把握しやすくなります。 今回のワークショップは、全体へのアナウンスや参加者同士のやりとりなどはDiscordで行われていました。 以下の画像は、実際のチャットを写したものです。 チームで協力しながら、フィッシングキットにどのような機能が搭載されているのか、どのような情報を窃取するのか分析していただきました。 参加者からはさまざまな反応をいただきました。 攻撃者の「心理誘導テクニック」の巧妙さが非常に興味深かった 実在したフィッシングキットを利用して、フィッシングサイトの調査を行えたのは非常に貴重な経験だった グループで情報交換をしながら分析を進めることができてとても楽しかった etc. さいごに 今回は、学生向けテックワークショップでの取り組みについて紹介しました。 フィッシング詐欺をテーマにしたテックワークショップは初めての試みでしたが、無事に終了できてよかったです。 改善すべき点もあったので、同じようなことをする機会があれば、改良してより良いものにしていきたいです。 今後も引き続き、何かしらの形でセキュリティ業界を盛り上げていくつもりでいます。 おまけ NA4Secでは、攻撃インフラの解明・撲滅に向けたさまざまな活動を実施しています。 対外発信にも力を入れており、ブログ記事や講演などさまざまな形で取り組みが紹介されています。 NA4Secが過去に書いた記事一覧 こんなこと聞いてみたい、この講演やブログ記事の内容が気になるとかがあれば、 出張講演なども前向きに検討しますので興味のある方はNA4Secまでお気軽にご相談ください。
アバター
NTTコミュニケーションズ(以下、NTT Com)を含めたドコモグループでは、この夏に インターンシップ を開催します! この記事では、その中でも NTT Com のリアルな業務を体験できる「 現場受け入れ型インターンシップ 」について紹介します。 現場受け入れ型インターンシップとは 募集ポスト 昨年のインターンシップの様子 まとめ 現場受け入れ型インターンシップとは NTTドコモや NTT Com の社員と一緒に働きながら、実務を体験していただくインターンシップです。 実際の職場で社員と共に、“本当に使われる技術” を用いてプロジェクトに参画。 メンターによる実務レビューを通じて、実践的なスキルと思考を磨き上げます。 インターン終了後は、配属確約ができる「ポスト確約型WILLコース」へのエントリーが可能。 さらに、専門性が高く当社基準を満たす方には、「グレード5」での高待遇入社が提示されます。 100種類以上のエンジニアポストを用意しており、専門性を活かして配属確約での入社をめざす方に最適なインターンシップです。 エンジニアやセールス、ビジネスデザイン、リーガルなど幅広いワークフィールドを取り揃えて、業務体験を通じて仕事の理解を深め、成長機会を提供する内容となっています。 今季は 2025年8月25日(月)~9月5日(金)の土日祝日を除く10日間(2週間) で開催されます。開催場所は、出社+リモートワークのハイブリッド形式です(出社割合はポストにより異なります)。 募集ポスト 以下のワークフィールドが募集をしています。 AIエンジニア・データサイエンティスト セキュリティエンジニア ネットワーク・インフラエンジニア 6G・IOWNエンジニア プロダクト・サービスエンジニア ソリューションエンジニア パートナーコンサルティング(コンシューマ) パートナーコンサルティング(法人) ビジネスデザイン(コンシューマ) ビジネスデザイン(法人) リーガル アカウンティング&ファイナンス 地域エリア 各ワークフィールドの募集ポストについては、「 現場受け入れ型インターンシップ 」サイトの受け入れポスト情報をご覧ください。 記載されているポストのうち、受け入れ会社に NTTコミュニケーションズ と記載されたポストが NTT Com での業務です。 昨年のインターンシップの様子 NTT Com のエンジニア系ポストに参加した学生の方々が、これまで開催したインターンシップの体験記をこの NTT Communications Engineers' Blog に寄稿してくれています。 昨年の様子は以下の記事からご覧いただけます。 ローコード・ノーコードに潜むリスクを攻撃ツールで確かめてみた(インターンシップ体験記) MoQTを活用した双方向VTuberライブデモでアバターのパパになってみた(インターンシップ体験記) 構築から運用まで!脅威インテリジェンス業務を体験(インターンシップ体験記) フィッシングキットの詳細分析に挑戦!(インターンシップ体験記) この他にも、さまざまなインターンシップ体験記事を こちら からご覧いただけます。 「インターンシップでどんなことに取り組むのだろう?」、「インターンシップを通して何が学べるのだろう?」といった疑問を解消する手助けになれば幸いです。 まとめ みなさんもこの夏、ドコモグループのインターンシップに参加して興味分野での実務に挑戦してみませんか? 気になる開催概要とポスト情報はこちらです(再掲)。 現場受け入れ型インターンシップ エントリーは上記ページの募集要項をご確認の上、MYPAGE からお願いします。 【2027新卒】マイページ登録 マイページログイン エントリーシートの提出締め切りは 2025年6月13日(金)12:00 です。 興味のある方は、ぜひ夏のインターンシップをご検討ください。 みなさんのご応募をお待ちしています!
アバター
本記事では、AI異音検知の概要、実装、検証例について、入門的な内容をご紹介します。 はじめに 異音検知とは 検証用データ AIによる異音検知 オートエンコーダーモデル 音データの中身 周波数の世界から音を見る Node-AIでのオートエンコーダーモデル作成 おわりに はじめに こんにちは、NTT Com イノベーションセンターの 中野 です。 普段は時系列データに対応したノーコードAI開発ツール「 Node-AI 」チームで、 お客さまのデータ分析支援やプロダクト開発チームのスクラムマスターとして活動しています。 さて、みなさんは「AI」と聞いて何を想像しますか? 多くの人は、チャットができたり画像が作れる、いわゆる 生成AI (Generative AI)を思い浮かべると思います。 一方、大量の数値データを読み込み、中身を理解して何らかの予測値を返してくれる 予測AI (Predictive AI)もあります。 本記事は予測AIついてのお話です。 例えると、右脳が生成AI、左脳が予測AIに対応するようなものです。 ※この説明だと正確ではない例もありますし、両者の境界はなくなりつつあります。 ざっくりのイメージだけ伝わればと思います。 タイトルにある「変な音の検知」について考えると、 音は音波をマイクで収集・デジタル化(標本化)して数値データにできますし、 その音データを理解して「変」か「変じゃない」かを予測してくれるAIを作れば、 これも予測AIと言えそうです。 異音検知とは 「変な音」は「異音」とも言います。 異音をgoo辞書で調べると、以下の定義となっています。 機械・機器などから出る、通常とは異なる音。「パソコンから—がする」「—を確認しての緊急停車」 何が異音で、何が異音じゃないかは状況に強く依存します。 例えば稀にノイズのような音が混じっているのを異音とすることもあるし、 音の大きさ、音色、音程がいつもと違うことを異音と言うケースもあるでしょう。 辞書の意味からもわかる通り、通常の音(正常データ)があるからこそ、 異音(異常データ)を感じることができます。 異音検知とは、通常の音は「正常」、異音は「異常」と返す数理的な処理のことを指します。 異音というからには何かマズイことが起こっているということで、 事前にトラブルを予知して対策を講じることで メリット(人件費削減、事故防止、機会損失回避など)を享受できます。 検証用データ 今回の検証で利用するデータについて説明します。 「異音検知 データセット」などで検索すると、 有名な異音検知のオープンデータがいくつか見つかります。 しかしそれらはデータ量が数GBなど膨大であったり、 研究用/コンペ用の場合は異音検知の難易度が高いため、 軽く動作確認したい場合には不向きです。 今回はわかりやすさと手軽さを重視して、 簡単な正常データと異常データをそれぞれ1ファイル用意しました。 こちらの Webサイト にある 「冷蔵庫1」を正常データとして利用し、 正常データに「プツプツ1」の異音を短時間に3回重畳したものを異常データとしました。 元のデータは3秒程度なので、処理の都合上10秒程度に繋ぎ合わせています。 ※その関係で繋ぎ部分に若干ノイズも乗ってしまっています。 正常データはこちら。 異常データはこちら。 いかがですか? 明らかに異常データは人間の耳で聞いても異音が含まれているとわかります。 これを検知できるか検証してみます。 AIによる異音検知 オートエンコーダーモデル 異音検知の歴史は長く、さまざまな手法が提案されています。 本記事ではその1つである オートエンコーダー(Auto Encoder) を用います。 オートエンコーダーは深層学習(ディープラーニング)の1手法で、音に限らずさまざまなデータの異常検知に用いられます。 深層学習は人間の脳の構造をヒントに設計された機械学習手法で、入力されたデータを大量の神経細胞が電気信号を伝達するかのように処理します。 下図の丸や丸同士を繋ぐ矢印がそれをコンピューター上で実現するための要素となります。 参考: AIによる蓄電池システムの故障予兆検知技術の開発に成功 オートエンコーダーについて簡単に説明すると「入力されたデータをぎゅっと圧縮し、それをできるだけ元に戻す」というAIモデルです。 モデルの形状としては砂時計を横に倒したものをイメージするといいかもしれません。 この「ぎゅっと圧縮」が重要で、圧縮すると元の情報が失われる(ボヤける)ため、完全には元に戻せません。 そこを何とかできるだけ元に戻すよう頑張る(内部のパラメータを調整する)のが、 正常データを用いた学習フェーズです。 例えると100個の神経細胞でキャッチした情報を10個の神経細胞に詰め込み、 それをまた100個の情報に復元しようということです。 なんだか大変そうですよね。 次に、この学習済みのオートエンコーダーモデルに異常データを入力した場合のことを考えます。 モデルは異常データを圧縮後、できるだけデータを元に戻すように働きますが、 正常データとは異なるパターンが含まれているので精度高く復元ができません。 モデルは、正常データと同じようなデータが入力されることを期待しているためです。 頑張って学習した正常データに近いデータなら「よっしゃこのパターンならこう復元すればいいな!」となるのですが、 そこに未知のデータが来ると「なんだこれ、こんなの知らないからうまく復元できない…」となるのです。 この復元時の誤差(再現誤差、再構成誤差などと言う)を「異常度」とします。 そして、異常度が正常データを入力した時と比較して高い時に「異常が発生した」 と判断するロジックにより、異音検知ができるという仕組みです。 音データの中身 オートエンコーダーが理解できたところで、AIモデルに対してどのように音データを入力するかを考えましょう。 音データは、以下図のように1つの信号として線グラフで描画できます。 実はこの図は、先述した正常データを可視化したものです。 たった10秒でもデータ量が多すぎて潰れて見にくいですね。どれくらいのデータ量なのでしょうか。 音を記録する頻度は サンプリング周波数(単位 Hz: ヘルツ) と呼ばれます。例えば、CDは一般に44,100Hzです。 慣れない方向けに簡単に説明すると、Hzは1秒間に何回分の記録点があるかを示します。 44,100Hzということは、1秒間に4万4千100回もデータが記録されているということになります。 ちなみに、隣り合う記録点の秒間は 1/44100=約0.00002秒となります。 なお、今回の音データは16,000Hzとなっています。 例えば、このデータを1秒(16,000個)区切りにしてあげて、AIモデルに入力することが考えられます。 その1秒の中に異音が含まれていれば異常度が高くなるはずなので、1秒ごとに異音検知ができることになります。 また、このような加工をしていないデータを 生データ 、一定間隔で区切る枠のことを 時間窓 と呼びます。 周波数の世界から音を見る 1秒間に1万6千回も変化するようなデータ、人間が見てもいまいちどんな音なのかイメージがしにくいですよね? 人間が見てもわかりにくいということは、AIモデルにとってもわかりにくい可能性があります(偏見)。 ちなみに異常データの波形はこのようになります。 今回は簡単なので、「異音がどこに含まれるかを当ててください」と言われても当てられるかもしれません。 正解は以下図の赤枠あたりです。 しかし、それ以外の部分も若干異音に見えるところがありますし、もう少し異音が小さい音だと厳しくなってきますよね。 こういった場合に役に立つのが 周波数領域で音を見る という考え方です。 高音はデータの波の頻度が高く、低音は波の頻度が低くなるという特徴があります。 この波の頻度は 周波数 で表現できます。 例えば、1周期が1秒間であるような波は1Hz、1周期が0.5秒であるような波は2Hz、1周期がX秒であるような波は 1/X Hz といった計算になります。 波の頻度と周波数の関係の例を図にしたものがこちらです。 左側が1Hz、右側が2Hzの波となっており、それを周波数の世界から見たのが下側の図になります。 縦軸が周波数(Frequency)になっているのがポイントです。 データがどれだけグネグネしているかを見るよりも、どんな周波数が含まれているかを見るほうがわかりやすいと思いませんか? この周波数変換を今回の検証データである正常データと異常データにかけたものがこちらです。 ※このようなデータを「スペクトログラム」と呼びます。 異常データのほうに何か浮かび上がってますね! ということで、周波数変換すると、異音を見つけやすくなることがあることを示しました。 今回は、この周波数変換したデータをAIモデルに入力することとします。 ここまでの処理を行うPythonコードはこちらです。 import librosa import numpy as np import pandas as pd file_path = "normal.wav" # wavファイルをnumpy形式の生データに変換 audio, sr = librosa.load(file_path, sr= 16000 ) # 生データを周波数領域のデータに変換 freq_data = librosa.amplitude_to_db(np.abs(librosa.stft(audio)), ref=np.max) # 参考: 可視化 librosa.display.specshow(D, sr=sr, x_axis= 'time' , y_axis= 'log' ) # 周波数領域のデータをデータフレームに変換 df = pd.DataFrame(D.T) # カラム名を付与 df.columns = [ "freq_" + str (i) for i in range ( len (df.columns))] # 時刻を付与 # STFTの設定から 512 / 16000 = 0.032秒 が時間間隔となる df.index = pd.date_range(start= "2025-01-01 00:00:00" , freq= "32ms" , periods= len (df)) # CSVファイルに保存 df.to_csv( "normal.csv" ) # 先頭5行を表示 df.head() 作成したCSVデータの先頭5行はこちら。 freq_0~freq_1024までの、1025個のカラムが作られます。これが周波数0Hz~8,000Hzに対応しています。 生データのサンプリング周波数(16,000Hz)の半分の周波数成分(8000Hz)までが抽出されます。 また、時間間隔は周波数変換により0.032秒となります(生データのサンプリング周波数と変換の設定値に依存します)。 生データの1秒間が16,000個の記録点だったのに対し、周波数変換後は1秒で約32個の記録点ということになります。 Node-AIでのオートエンコーダーモデル作成 これまで説明した理論と用意したデータを踏まえ、 実際にオートエンコーダーモデルの実装と検証を実施します。 最近では生成AIによりプログラミングは随分楽になりましたが、 それでも時系列データの機械学習のコーディングは難易度が高く、バグが入りやすいものです。 今回は、時系列データのオートエンコーダーモデルに対応したノーコードツールであるNode-AIを用います。 上述した周波数変換後(スペクトログラム)のCSVデータを使用して、異音検知モデルを作成していきます。 使い方については別の記事で紹介しているので、興味があればご覧ください。 engineers.ntt.com また異常検知の流れを紹介したYouTube動画も公開しています。ご参考に。 今回作成したNode-AIでのモデル作成フローの全体像はこちらです。 Node-AIでは上から下にデータが流れるように動作します。 まず正常データと異常データをそれぞれ前処理(正規化+モデル入力用変換)します。 前処理後の正常データに対して「オートエンコーダー」を用いて「学習」し、異常度を可視化する流れとなります。 「時間窓切り出し(モデル入力用前処理)」では、 「何個のデータを区切ってAIモデルに入力するか」= 時間窓 を設定します。 今回は時間窓を「10」と設定しました(約0.3秒分に相当)。 時間窓は異音の最小単位が含まれているべきで、正常な波形の周期性等も考慮して決めるパラメーターです。 最適な値を探索するのは骨が折れますが、0.3秒というのは直感的には無難な数値でしょう。 次に、オートエンコーダーではモデルの形状や学習の設定します。 パラメータは自動で探索することもできますが、 今回は特にチューニングをするほどの難しいタスクではないので、 エイヤで以下のように設定しました。 層のように重なっている図がオートエンコーダーを示しています。 今回モデルに入力されるデータは1,025個のカラムと10個の時間窓なので、合計10,250個となります。 これを 10,250 → 256 → 64 と圧縮していき、 256→ 10,250 と復元するモデルになります。 異常度可視化(正常時)では、正常データにおける異常度を表示できます。 目的は「正常時には異常と判定せず」「異常時に異常と判定する」ことですから、 正常時には異常判定する「閾値」をギリギリのラインに設定しておきます。 その正常時に設定した閾値を引き継いで異常時の異常度を可視化したものがこちらです。 赤い帯になっているのが、閾値を越えた異常度の箇所です。 思った以上にわかりやすく異常を判定できました! さらに、「どのあたりの周波数が異常度に影響を与えているのか」といったことを知りたい場合もあるでしょう。 ここではNode-AIの「要因分析」機能を使って、調べてみます。 以下の図は横軸が時間、縦軸がカラム名となっていて、各セルが異常度にどれくらい影響を与えたかを示しています。 異常度が高くなっている時間帯の要因を調べていくと…。 freq_380あたりからぼんやりと赤くなっていることがわかります。 これは周波数で言うと3,000Hzあたりを指すので、スペクトログラムでノイズが浮かび上がった箇所と重複していそうです。 このように、人の目で見ても異常が見つけにくい場合でも、要因分析機能により深い考察ができます。 おわりに 異音検知をAIで実現する手法の紹介と、 簡単な異音データでの検証の流れをご紹介しました。 私自身は音処理の専門家ではなく、 今回のブログ執筆を通じて初めて異音検知の技術調査と実装を実施しました。 「こんなことできるかな?」と思い立ったら、 今では生成AIやノーコードツールを使って簡単に検証できる時代になったことを再確認しました。 データは蓄積してるけど活用できてない、分析の仕方がわからない、 といった方は是非ご相談いただければと思います! ご相談は Node-AI の Web サイト 上部の「お問い合わせ」フォームにご連絡ください。
アバター
みなさんこんにちは、イノベーションセンターの益本 (@masaomi346) です。 Network Analytics for Security (以下、NA4Sec) プロジェクトのメンバーとして活動しています。 この記事では、2025年5月17日に開催されたセキュリティカンファレンスBSides Tokyo 2025で登壇したことについて紹介します。 BSides Tokyo ぜひ最後まで読んでみてください。 BSides Tokyoについて BSidesとは、情報セキュリティのコミュニティ主導で開催されているセキュリティカンファレンスです。 2025年5月時点では、1105のBSidesイベントが開催されており、65カ国にまたがる260都市で開催されています。 BSides wiki 今回は、日本で毎年開催されているBSides Tokyoに登壇させていただきました。 日本で開催されているセキュリティカンファレンスでありますが、海外からの参加者もいます。 主催者からの情報によると、今年のBSides TokyoのCFPの応募数は昨年の倍になっていたそうです。 また、参加者も年々増加しており、国内外から注目されているセキュリティカンファレンスになっています。 NA4Secについて 「NTTはインターネットを安心・安全にする社会的責務がある」を理念として、インターネットにおける攻撃インフラの解明・撲滅を目指すプロジェクトです。 NTT Comグループにおける脅威インテリジェンスチームとしての側面も持ち合わせており、有事において脅威インテリジェンスを提供し、意思決定を支援することもあります。 イノベーションセンターを中心として、NTTセキュリティ・ジャパンやエヌ・エフ・ラボラトリーズ(以下、NFLabs.)からもメンバーが参画し、日夜攻撃インフラを追跡しています。 BSides Tokyo 2025での登壇 NA4Secからは、以下のタイトルで登壇させていただきました。 フィッシングキットの特徴による開発者の分類 / イベント登壇情報🎙️ \ セキュリティカンファレンス #BSidesTokyo 2025 に NTT Com の益本が登壇します✨ フィッシングキットの特徴から開発者を分類して得た学びについて報告します🎣 https://t.co/CP72yoCVvA #ドコモビジネス pic.twitter.com/RybzxpRtlS — ドコモビジネス|NTTコミュニケーションズ (@NTTCom_online) 2025年5月13日 フィッシングキットを分類する上で理想なのは、具体的な攻撃者の名前がわかっていて、特定のフィッシングキットと紐づけることができる状態です。 ただ、具体的な攻撃者の名前はわからないことが多いので、それをするのはかなり難しいです。 フィッシングキットを分析すると、異なるブランドでも裏で実行されている処理がまったく同じものもありました。 フィッシングキットの作成はそれなりに手間がかかるので、一度作成したものを再度利用していると思われます。 それが開発者の特徴となるので、分類することが可能になります。 今回の講演では、日本のブランドを騙ったフィッシングキットをいくつかのグループに分類し、それぞれのグループにどのような特徴があるのか紹介しました。 どのようなブランドを騙っているのか どのような機能が実装されているのか また、実際に分類してみて得た学びも共有しました。 どのような機能が実装されている傾向があるのか 複数のグループの特徴を持ち合わせている場合もあること 講演が終わった後も、フィッシングキットの収集方法や最近の攻撃者についてなど、いくつか質問があり反応はそれなりにもらえました。 さいごに 今回もフィッシングネタで登壇させていただきました。 セキュリティカンファレンスを通じて、継続的にセキュリティ業界に貢献しつづけることができて良かったと思っています。 今後も引き続き、何かしらの形でセキュリティ業界を盛り上げていくつもりでいます。 おまけ NA4Secでは、攻撃インフラの解明・撲滅に向けたさまざまな活動を実施しています。 対外発信にも力を入れており、ブログ記事や講演などさまざまな形で取り組みが紹介されています。 NA4Secが過去に書いた記事一覧 こんなこと聞いてみたい、この講演やブログ記事の内容が気になるとかがあれば、 出張講演なども前向きに検討しますので興味のある方はNA4Secまでお気軽にご相談ください。
アバター
こんにちは、イノベーションセンターの鈴ヶ嶺です。 本記事では、 NVIDIA Dynamo や vLLM などの LLM 推論フレームワーク向けに設計された高速・低遅延の抽象化転送ライブラリである NVIDIA Inference Xfer Library (NIXL) について解説します。 また、NVIDIA Dynamo に関してはこちらで解説していますので参考にしていただけると幸いです。 engineers.ntt.com まず、LLM 推論高速化(KV Cache)におけるメモリ転送の背景と課題をご紹介し、それを解決する NIXL の概要を説明します。 NIXL は Plugin により任意の転送方式を実装可能なアーキテクチャとなっています。実際に Custom Plugin を実装する方法についても紹介します。 背景と課題 NVIDIA Inference Xfer Library (NIXL) GPUDirect RDMA による VRAM to VRAM のデータ転送 GPUDirect Storage による VRAM to FILE のデータ転送 Custom Plugin の実装方法について まとめ 背景と課題 LLM の推論高速化は、コスト削減や低遅延な応答によるユーザビリティ向上といったニーズから、さまざまな改良が進められています。中でも「KV Cache」は、過去トークンに対する計算済みのキー・バリュー行列を保持し、次のトークン生成時に再計算を省略することで、推論速度を大幅に向上させる重要な技術です。 1 一方で、KV Cache が保持する状態はシーケンス長に比例して増加するため、メモリ消費も増大します。また、このキャッシュを複数の GPU やノード間で共有する際には、低遅延で転送できる仕組みが求められます。さらに、キャッシュの転送に使用するメモリやストレージの種類によって、最適なプロトコル( NVLink 、 GPUDirect Storage/RDMA など)が異なり、実装の複雑さが課題となります。 これらの課題を解決するため、多彩なメモリ・ストレージ、通信プロトコルを抽象化し、高速・低遅延な転送を可能とするライブラリが求められています。 NVIDIA Inference Xfer Library (NIXL) NVIDIA Inference Xfer Library(NIXL) は、LLM 推論フレームワーク向けに設計された高速・低遅延の転送ライブラリです。特徴として、VRAM や DRAM, FILE, Block, Object Storage など異種のメモリ・ストレージを統一的に抽象化する API を提供し、UCX(Unified Communication X) や GPUDirect Storage といった複数のバックエンドプラグインを動的に選択して最適な通信経路を自動的に構築します(下図参照)。通信方式は Plugin 形式で任意に拡張可能なため、独自のキャッシュシステムを構築可能です。 引用: https://github.com/ai-dynamo/nixl/blob/main/docs/nixl.md#overview 2025 年 5 月時点の対応 Plugin は以下になります。 cuda_gds DMA(Direct Memory Access)により、GPU Memory と Storage 間を高速転送する GPUDirect Storage(GDS)を使用するバックエンド NVMe SSD, NVMe-oF, NFS over RDMA, 分散ファイルシステム(DDN EXAScaler, VAST NFS, WekaFS)などで利用可能 mooncake LLM Serving platform の Kimi で利用される KV Cache System の Mooncake を使用するバックエンド posix libaio や liburing を使用した POSIX 準拠の I/O 処理をするバックエンド ucx 高帯域・低遅延通信の抽象化ライブラリである UCX を使用するバックエンド デフォルトではこの Plugin が設定されます ucx_mo UCX v1.18 では 1 つの UCX Context で複数 GPU をサポートしていないため、 Multi-Object (MO) UCX は GPU ごとに異なる UCX Worker を関連づける実装に改良している 2 NIXL はさまざまな LLM 推論フレームワークへの採用が進んでいます。次の図のように vLLM のサポートが 2025 年 4 月 11 日にアナウンスされました。 Shaping NIXL-based PD Disaggregation in vLLM V1 また、 SGLang でも以下のように NIXL 対応の PR がマージされました。 [PD] Add NIXL transfer backend #5477 NIXL の通信過程は次のようになります。 各ノードのエージェント初期化 VRAM, DRAM の登録 転送のためのメタデータの交換 データ転送 引用: https://github.com/ai-dynamo/nixl/blob/main/docs/nixl.md#example-procedure 次の章で実際に NIXL を実行します。 GPUDirect RDMA による VRAM to VRAM のデータ転送 ここでは NIXL の example を動作させてノード間で GPU メモリを UCX による GPUDirect RDMA を用いて転送します。NIXL は prebuild のものを pip install nixl でインストールできますが、今回は理解やカスタマイズのために自前で build したものを使用します。 事前に cuda や gdrcopy についてはインストールしてください。 まず初めに、UCX をインストールします。 wget https://github.com/openucx/ucx/releases/download/v1. 18 . 0 /ucx-1. 18 . 0 .tar.gz tar xzf ucx-1. 18 . 0 .tar.gz cd ucx-1. 18 . 0 ./configure \ --enable-shared \ --disable-static \ --disable-doxygen-doc \ --enable-optimizations \ --enable-cma \ --enable-devel-headers \ --with-cuda = /usr/local/cuda \ --with-verbs \ --with-dm \ --with-gdrcopy = /usr/ local \ --enable-mt \ --prefix = /opt/ucx-1. 18 . 0 make -j sudo make install # add ~/.bashrc export PATH =/opt/ucx-1. 18 . 0 /bin: $PATH export LD_LIBRARY_PATH =/opt/ucx-1. 18 . 0 /lib: $LD_LIBRARY_PATH export PKG_CONFIG_PATH =/opt/ucx-1. 18 . 0 /lib/pkgconfig: $PKG_CONFIG_PATH 次に NIXL をインストールして、example である blocking_send_recv_example.py を実行します。 target から torch.ones(10, dtype=torch.float32) のメモリを initiator に転送しています。 git clone https://github.com/ai-dynamo/nixl.git cd nixl git checkout 503fe5ccb86b5963b828ee5663672fcba66b92d2 python3 -m venv venv source venv/bin/activate pip install . cd examples/python # UCXにおける、RDMAのNICや通信方法を設置 export UCX_NET_DEVICES = [ RNIC Device ] export UCX_TLS =rc,cuda # target node ./blocking_send_recv_example.py --ip 0 . 0 . 0 . 0 --mode target --use_cuda 1 ## MD listener is listening on port 8888... ## Backend UCX was instantiated ## Initialized NIXL agent: initiator ## initiator Tensors: [tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], device='cuda:0'), tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], device='cuda:0')] ## Initiator sending to [Node A IP Address] ## Ready for transfer ## initiator Data verification passed - [tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], device='cuda:0'), tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], device='cuda:0')] ## Test Complete. # initiator node ./blocking_send_recv_example.py --ip [ Node A IP Address ] --mode initiator --use_cuda 1 ## MD listener is listening on port 5555... ## Backend UCX was instantiated ## Initialized NIXL agent: target ## target Tensors: [tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], device='cuda:0'), tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], device='cuda:0')] ## Waiting for transfer ## Test Complete. 実行すると、上記のようにノード間で GPU メモリが共有されたことを確認できます。 GPUDirect Storage による VRAM to FILE のデータ転送 ここでは GPUDirect Storage(GDS)を用いて GPU メモリを直接ストレージに転送するサンプルを作成して実行します。 DRAM から GDS でストレージに書き込む example である nixl_gds_example.py を参考に VRAM から GDS でストレージに書き込む次のスクリプトを作成します。 nixl_gds_example_vram_to_file.py import os import sys import torch import subprocess import nixl._utils as nixl_utils from nixl._api import nixl_agent, nixl_agent_config if __name__ == "__main__" : agent_config = nixl_agent_config(backends=[ "GDS" ]) nixl_agent1 = nixl_agent( "GDSTester" , agent_config) # init VRAM float32 1.0(0x0000803f) tensors = [torch.ones( 10 , dtype=torch.float32, device= 'cuda:0' )] agent1_vram_descs = nixl_agent1.register_memory(tensors) agent1_xfer_vram = agent1_vram_descs.trim() file_path = sys.argv[ 1 ] agent1_fd = os.open(file_path, os.O_RDWR | os.O_CREAT) assert agent1_fd >= 0 agent1_file_list = [( 0 , tensors[ 0 ].numel() * tensors[ 0 ].element_size(), agent1_fd, "b" )] agent1_file_descs = nixl_agent1.register_memory(agent1_file_list, "FILE" ) assert agent1_file_descs is not None agent1_xfer_files = agent1_file_descs.trim() xfer_handle_1 = nixl_agent1.initialize_xfer( "WRITE" , agent1_xfer_vram, agent1_xfer_files, "GDSTester" ) if not xfer_handle_1: print ( "Creating transfer failed." ) exit() state = nixl_agent1.transfer(xfer_handle_1) assert state != "ERR" done = False while not done: state = nixl_agent1.check_xfer_state(xfer_handle_1) if state == "ERR" : print ( "Transfer got to Error state." ) exit() elif state == "DONE" : done = True print ( "Initiator done" ) nixl_agent1.release_xfer_handle(xfer_handle_1) nixl_agent1.deregister_memory(agent1_vram_descs) nixl_agent1.deregister_memory(agent1_file_descs) os.close(agent1_fd) # check file binary p = subprocess.run([ "hexdump" , "-Cv" , file_path], stdout=subprocess.PIPE) print ( "$ hexdump -Cv" , file_path) print (p.stdout.decode()) 実行した様子が以下のようになります。 ファイル内容を見ると float32 1.0 の 0x0000803f が書き込まれていることが分かります。 > python nixl_gds_example_vram_to_file.py /path/to/gds-support-dir/ones.bin Backend GDS was instantiated Initialized NIXL agent: GDSTester Initiator done $ hexdump -Cv /path/to/gds-support-dir/ones.bin 00000000 00 00 80 3f 00 00 80 3f 00 00 80 3f 00 00 80 3f |...?...?...?...?| 00000010 00 00 80 3f 00 00 80 3f 00 00 80 3f 00 00 80 3f |...?...?...?...?| 00000020 00 00 80 3f 00 00 80 3f |...?...?| 00000028 Custom Plugin の実装方法について NIXL は Plugin により任意の転送方式を実装可能なアーキテクチャとなっています。 ここではローカルで DRAM と FILE による転送が可能なサンプルの Plugin を実装する方法について紹介します。 基本的には、 nixlBackendEngine を継承して、最低でも純粋仮想関数(例: virtual void f() = 0; ) 3 を実装することで新たな転送方法を追加できます。 プロジェクト構成以下のように nixl の plugins 配下に新たに local ディレクトリを作成して実装コードを置きます。 nixl ├── src │   ├── plugins │   │   ├── local │   │   │   ├── local_backend.cpp │   │   │   ├── local_backend.h │   │   │   ├── local_plugin.cpp │   │   │   └── meson.build │   │   ├── meson.build 新たに Plugin を追加するため、 nixl/src/plugins/meson.build に以下を追記します。 subdir('local') 次のように追加した Plugins を登録する実行を追加します。 Plugin の名前、追加のオプションパラメータ、サポート可能なメモリーの種類を設定します。 local_plugin.cpp #include "backend/backend_plugin.h" #include "local_backend.h" static const char * PLUGIN_NAME = "LOCAL" ; static const char * PLUGIN_VERSION = "0.1" ; static nixlBackendEngine* create_local_engine ( const nixlBackendInitParams* init_params) { return new nixlLocalEngine (init_params); } static void destroy_local_engine (nixlBackendEngine* engine) { delete engine; } static const char * get_plugin_name () { return PLUGIN_NAME; } static const char * get_plugin_version () { return PLUGIN_VERSION; } static nixl_b_params_t get_backend_options () { nixl_b_params_t params; return params; } // サポート可能なメモリーの種類 static nixl_mem_list_t get_backend_mems () { nixl_mem_list_t mems; mems. push_back (DRAM_SEG); mems. push_back (FILE_SEG); return mems; } static nixlBackendPlugin plugin = {NIXL_PLUGIN_API_VERSION, create_local_engine, destroy_local_engine, get_plugin_name, get_plugin_version, get_backend_options, get_backend_mems}; #ifdef STATIC_PLUGIN_LOCAL nixlBackendPlugin* createStaticLocalPlugin () { return &plugin; } #else extern "C" NIXL_PLUGIN_EXPORT nixlBackendPlugin* nixl_plugin_init () { return &plugin; } extern "C" NIXL_PLUGIN_EXPORT void nixl_plugin_fini () {} #endif 以降で実際に転送処理を行うロジックを実装します。 postXfer 関数でメモリアドレスやファイルデスクリプタが渡されるためそれらを用いて転送します。 例えば UCX の実装を参考にすると実転送は postXfer 関数で実行されています。 4 local_backend.h #ifndef __LOCAL_BACKEND_H #define __LOCAL_BACKEND_H #include <nixl.h> #include <nixl_types.h> #include <unistd.h> #include "backend/backend_engine.h" class nixlLocalEngine : public nixlBackendEngine { private : public : nixlLocalEngine ( const nixlBackendInitParams *init_params); ~ nixlLocalEngine (); // 通知機能をサポートするかどうか bool supportsNotif () const { return false ; } // 別プロセス、リモートノードへの転送をサポートするかどうか bool supportsRemote () const { return false ; } // 同一のプロセス、ノードへの転送をサポートするかどうか bool supportsLocal () const { return true ; } // 内部に処理のバックグランドスレッドを持つかどうか bool supportsProgTh () const { return false ; } // サポート可能なメモリーの種類 nixl_mem_list_t getSupportedMems () const { nixl_mem_list_t mems; mems. push_back (DRAM_SEG); mems. push_back (FILE_SEG); return mems; } nixl_status_t connect ( const std :: string &remote_agent) { return NIXL_SUCCESS; } nixl_status_t disconnect ( const std :: string &remote_agent) { return NIXL_SUCCESS; } nixl_status_t loadLocalMD (nixlBackendMD *input, nixlBackendMD *&output) { output = input; return NIXL_SUCCESS; } nixl_status_t unloadMD (nixlBackendMD *input) { return NIXL_SUCCESS; } nixl_status_t registerMem ( const nixlBlobDesc &mem, const nixl_mem_t &nixl_mem, nixlBackendMD *&out); nixl_status_t deregisterMem (nixlBackendMD *meta); nixl_status_t prepXfer ( const nixl_xfer_op_t &operation, const nixl_meta_dlist_t &local, const nixl_meta_dlist_t &remote, const std :: string &remote_agent, nixlBackendReqH *&handle, const nixl_opt_b_args_t *opt_args = nullptr ); nixl_status_t postXfer ( const nixl_xfer_op_t &operation, const nixl_meta_dlist_t &local, const nixl_meta_dlist_t &remote, const std :: string &remote_agent, nixlBackendReqH *&handle, const nixl_opt_b_args_t *opt_args = nullptr ); nixl_status_t checkXfer (nixlBackendReqH *handle); nixl_status_t releaseReqH (nixlBackendReqH *handle); }; #endif local_backend.cpp #include "local_backend.h" #include <string.h> #include <sys/stat.h> #include <iostream> nixlLocalEngine:: nixlLocalEngine ( const nixlBackendInitParams *init_params) : nixlBackendEngine (init_params) {} nixl_status_t nixlLocalEngine:: registerMem ( const nixlBlobDesc &mem, const nixl_mem_t &nixl_mem, nixlBackendMD *&out) { // 基本的にローカルでDRAM, FILEの転送をする際にはここで処理はしない形で設計 // 例えばRDMA(ib verbs)ではibv_reg_mrやGDSではファイルハンドラ登録がされることが望ましいと思われる // 別途ここでファイルハンドラなどのメタデータを設定し、prepXferやpostXferを利用するためにはnixlBackendMDを継承したクラスを引数のoutに設定する // 参考: https://github.com/ai-dynamo/nixl/blob/1c979f0999740e4b221d0b9b470efbac793ddcae/src/plugins/cuda_gds/gds_backend.cpp#L95 if (nixl_mem == FILE_SEG || nixl_mem == DRAM_SEG) return NIXL_SUCCESS; return NIXL_ERR_NOT_SUPPORTED; } nixl_status_t nixlLocalEngine:: deregisterMem (nixlBackendMD *meta) { return NIXL_SUCCESS; } nixl_status_t nixlLocalEngine:: prepXfer ( const nixl_xfer_op_t &operation, const nixl_meta_dlist_t &local, const nixl_meta_dlist_t &remote, const std :: string &remote_agent, nixlBackendReqH *&handle, const nixl_opt_b_args_t *opt_args) { // validation if ((local. descCount () != remote. descCount ()) || ((operation != NIXL_READ) && (operation != NIXL_WRITE))) { return NIXL_ERR_INVALID_PARAM; } return NIXL_SUCCESS; } nixl_status_t nixlLocalEngine:: postXfer ( const nixl_xfer_op_t &operation, const nixl_meta_dlist_t &local, const nixl_meta_dlist_t &remote, const std :: string &remote_agent, nixlBackendReqH *&handle, const nixl_opt_b_args_t *opt_args) { // DRAM, FILEの転送処理を実行する if (local. getType () == DRAM_SEG && remote. getType () == DRAM_SEG) { // dram to dram int cnt = local. descCount (); for ( int i = 0 ; i < cnt; i++) { void *dst_addr; void *src_addr; if (operation == NIXL_READ) { dst_addr = ( void *)local[i].addr; src_addr = ( void *)remote[i].addr; } else if (operation == NIXL_WRITE) { dst_addr = ( void *)remote[i].addr; src_addr = ( void *)local[i].addr; } memcpy (dst_addr, src_addr, local[i].len); } } else if (local. getType () == FILE_SEG && remote. getType () == FILE_SEG) { // file to file int cnt = local. descCount (); for ( int i = 0 ; i < cnt; i++) { int in_fd; int out_fd; if (operation == NIXL_READ) { in_fd = remote[i].devId; out_fd = local[i].devId; } else if (operation == NIXL_WRITE) { in_fd = local[i].devId; out_fd = remote[i].devId; } struct stat st; if ( fstat (in_fd, &st) < 0 ) { return NIXL_ERR_INVALID_PARAM; } if ( copy_file_range (in_fd, 0 , out_fd, 0 , st.st_size, 0 ) < 0 ) { return NIXL_ERR_INVALID_PARAM; } } } else if ((local. getType () == FILE_SEG && remote. getType () == DRAM_SEG && operation == NIXL_WRITE) or (local. getType () == DRAM_SEG && remote. getType () == FILE_SEG && operation == NIXL_READ)) { // file to dram int cnt = local. descCount (); for ( int i = 0 ; i < cnt; i++) { int fd = local. getType () == FILE_SEG ? local[i].devId : remote[i].devId; uintptr_t buf = local. getType () == FILE_SEG ? remote[i].addr : local[i].addr; size_t len = operation == NIXL_WRITE ? local[i].len : remote[i].len; if ( pread (fd, ( void *)buf, len, 0 ) < 0 ) { return NIXL_ERR_INVALID_PARAM; } } } else if ((local. getType () == DRAM_SEG && remote. getType () == FILE_SEG && operation == NIXL_WRITE) or (local. getType () == FILE_SEG && remote. getType () == DRAM_SEG && operation == NIXL_READ)) { // dram to file int cnt = local. descCount (); for ( int i = 0 ; i < cnt; i++) { int fd = local. getType () == FILE_SEG ? local[i].devId : remote[i].devId; uintptr_t buf = local. getType () == FILE_SEG ? remote[i].addr : local[i].addr; size_t len = operation == NIXL_WRITE ? local[i].len : remote[i].len; if ( pwrite (fd, ( void *)buf, len, 0 ) < 0 ) { return NIXL_ERR_UNKNOWN; } } } else { return NIXL_ERR_NOT_SUPPORTED; } return NIXL_SUCCESS; } nixl_status_t nixlLocalEngine:: checkXfer (nixlBackendReqH *handle) { // 非同期機能未サポートのため、即時でSUCCESSを返す // 非同期実装については以下参考 // https://github.com/ai-dynamo/nixl/tree/main/src/plugins/posix return NIXL_SUCCESS; } nixl_status_t nixlLocalEngine:: releaseReqH (nixlBackendReqH *handle) { return NIXL_SUCCESS; } nixlLocalEngine::~ nixlLocalEngine () {} 最後に追加した Plugin の meson を次のように記載します。 nixl/src/plugins/local/meson.build if 'LOCAL' in static_plugins local_backend_lib = static_library('LOCAL', 'local_backend.cpp', 'local_backend.h', 'local_plugin.cpp', dependencies: [nixl_infra, nixl_common_dep], include_directories: [nixl_inc_dirs, utils_inc_dirs], install: true, cpp_args: ['-fPIC'], name_prefix: 'libplugin_', install_dir: plugin_install_dir) else local_backend_lib = shared_library('LOCAL', 'local_backend.cpp', 'local_backend.h', 'local_plugin.cpp', dependencies: [nixl_infra, nixl_common_dep], include_directories: [nixl_inc_dirs, utils_inc_dirs], install: true, cpp_args: ['-fPIC'], name_prefix: 'libplugin_', install_dir: plugin_install_dir) if get_option('buildtype') == 'debug' run_command('sh', '-c', 'echo "LOCAL=' + local_backend_lib.full_path() + '" >> ' + plugin_build_dir + '/pluginlist', check: true ) endif endif local_backend_interface = declare_dependency(link_with: local_backend_lib) 追加後は次のようにインストールします。 cd nixl pip install . 追加した Plugin を検証するコードを次に記述しました。 backends として agent に今回追加した LOCAL を設定します。 nixl_local_example.py import os import sys import torch import subprocess from nixl._api import nixl_agent, nixl_agent_config if __name__ == "__main__" : agent_config = nixl_agent_config(backends=[ "LOCAL" ]) nixl_agent = nixl_agent( "LOCAL_TEST" , agent_config) t1 = [torch.ones( 10 , dtype=torch.float32) for _ in range ( 1 )] t1_reg_descs = nixl_agent.get_reg_descs(t1) t1_xfer_descs = nixl_agent.get_xfer_descs(t1) assert nixl_agent.register_memory(t1_reg_descs) is not None t2 = [torch.zeros( 10 , dtype=torch.float32) for _ in range ( 1 )] t2_reg_descs = nixl_agent.get_reg_descs(t2) t2_xfer_descs = nixl_agent.get_xfer_descs(t2) assert nixl_agent.register_memory(t2_reg_descs) is not None t3 = [torch.zeros( 10 , dtype=torch.float32) for _ in range ( 1 )] t3_reg_descs = nixl_agent.get_reg_descs(t3) t3_xfer_descs = nixl_agent.get_xfer_descs(t3) assert nixl_agent.register_memory(t3_reg_descs) is not None print ( "=====Init=====" ) print ( 't1:' , t1) print ( 't2:' , t2) print ( 't3:' , t3) print ( "=====Write t1 to FILE(/tmp/ones.bin)=====" ) fd = os.open( "/tmp/ones.bin" , os.O_RDWR | os.O_CREAT) file_list = [( 0 , t1[ 0 ].numel() * t1[ 0 ].element_size(), fd, "b" )] file_descs = nixl_agent.register_memory(file_list, "FILE" ) assert file_descs is not None file_xfer_files = file_descs.trim() xfer_handle_1 = nixl_agent.initialize_xfer( "WRITE" , t1_xfer_descs, file_xfer_files, "LOCAL_TEST" ) if not xfer_handle_1: print ( "Write t1 to FILE failed." , file =sys.stderr) exit(- 1 ) state = nixl_agent.transfer(xfer_handle_1) assert state == "DONE" # check file binary p = subprocess.run([ "hexdump" , "-Cv" , "/tmp/ones.bin" ], stdout=subprocess.PIPE) print ( "$ hexdump -Cv /tmp/ones.bin" ) print (p.stdout.decode()) print ( "=====Read t2 from FILE(/tmp/ones.bin)=====" ) xfer_handle_2 = nixl_agent.initialize_xfer( "READ" , t2_xfer_descs, file_xfer_files, "LOCAL_TEST" ) if not xfer_handle_2: print ( "Read t2 from FILE failed." , file =sys.stderr) exit(- 1 ) state = nixl_agent.transfer(xfer_handle_2) assert state == "DONE" print ( "t2:" , t2) print ( "=====Write t1 to t3=====" ) xfer_handle_3 = nixl_agent.initialize_xfer( "WRITE" , t1_xfer_descs, t3_xfer_descs, "LOCAL_TEST" ) if not xfer_handle_3: print ( "Read t2 from FILE failed." , file =sys.stderr) exit(- 1 ) state = nixl_agent.transfer(xfer_handle_3) assert state == "DONE" print ( 't3:' , t3) # cleanup nixl_agent.release_xfer_handle(xfer_handle_1) nixl_agent.release_xfer_handle(xfer_handle_2) nixl_agent.release_xfer_handle(xfer_handle_3) nixl_agent.deregister_memory(t1_reg_descs) nixl_agent.deregister_memory(t2_reg_descs) nixl_agent.deregister_memory(t3_reg_descs) nixl_agent.deregister_memory(file_descs) os.close(fd) 実行結果は次のようになります。 t1 , t2 , t3 の 3 つの torch.Tensor がそれぞれファイルに書き込み、読み込みやメモリ間の転送を実施している様子が分かります。 > python nixl_local_example.py Backend LOCAL was instantiated Initialized NIXL agent: LOCAL_TEST ===== Init = ==== t1: [ tensor( [ 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 . ] ) ] t2: [ tensor( [ 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 . ] ) ] t3: [ tensor( [ 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 ., 0 . ] ) ] ===== Write t1 to FILE ( /tmp/ones.bin ) ===== $ hexdump -Cv /tmp/ones.bin 00000000 00 00 80 3f 00 00 80 3f 00 00 80 3f 00 00 80 3f |...?...?...?...?| 00000010 00 00 80 3f 00 00 80 3f 00 00 80 3f 00 00 80 3f |...?...?...?...?| 00000020 00 00 80 3f 00 00 80 3f |...?...?| 00000028 ===== Read t2 from FILE ( /tmp/ones.bin ) ===== t2: [ tensor( [ 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 . ] ) ] ===== Write t1 to t3 = ==== t3: [ tensor( [ 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 ., 1 . ] ) ] まとめ 本記事では、LLM 推論フレームワーク向けに設計された、高速かつ低遅延な抽象化転送ライブラリ「NVIDIA Inference Xfer Library(NIXL)」について解説しました。また、example の実行方法や Custom Plugin の実装方法についても紹介しました。 NIXL を導入することで、複数 GPU やマルチノード環境におけるレイテンシの削減やスケールアウトが容易になり、将来的には Plugin を活用した新しいストレージ技術や通信プロトコルへの対応も期待できます。 LLM テクニックの習得: 推論の最適化 - NVIDIA 技術ブログ ↩ https://github.com/ai-dynamo/nixl/commit/b0085154d2aa4347c332bb121293a77ab733a871 ↩ Abstract class - cppreference.com ↩ https://github.com/ai-dynamo/nixl/blob/503fe5ccb86b5963b828ee5663672fcba66b92d2/src/plugins/ucx/ucx_backend.cpp#L892-L898 ↩
アバター
こんにちは。NTTコミュニケーションズの露崎です。本ブログでは2025年3月のGTCで紹介されたNVIDIA社のOSS Dynamoについて紹介します。 はじめに 特徴 インストールと基本動作 Dynamo Run Dynamo Serve 推論グラフとコンポーネント dynamo serveの起動の流れ 1. nats/etcdの起動 2. dynamo serveの起動 3. 動作確認 4. 終了 分散処理の仕組み まとめ はじめに こんにちは。NTTコミュニケーションズの露崎です。本ブログでは2025年3月のGTCで紹介されたNVIDIA社のOSS Dynamoについて紹介します。 NVIDIA Dynamoは発表されて間がなく、開発/変更が盛んに行われています。本ブログでは2025年5月の時点での最新版である0.2.0について紹介しますが、最新情報については 公式 をご参照ください。 NVIDIA Dynamo はNVIDIA社が開発しOSSで公開している推論フレームワークです。LLMの推論時の処理をプロンプトの解釈を実施するPrefill、単語を逐次的に生成するDecodeなどのフェーズに分解し、フェーズ毎の処理を別々のGPUで実行させることにより、推論時の負荷を分散可能な分散推論フレームワークです。 各フェーズ間の通信をNVLINK経由のRDMAなどを用いることによって、より効率的でスケーラブルなAI推論が可能とする設計になっています。 PrefillやDecodeなどLLMの仕組みについての詳細は NVIDIA社のブログ をご参照ください。 NVIDIA Dynamoのアーキテクチャ(出典: NVIDIA Dynamo ) 特徴 NVIDIA Dynamoは、以下のような特徴を持っています。 推論グラフの構築: 自由度の高い推論グラフを構築でき、パイプライン向けのコンポーネントをSDKで提供します。 分散処理: Prefill、Decodeなどのフェーズに分解し、各フェーズ間の通信をRDMAを始めとした通信の抽象化を提供する NIXL 、プロセス間通信向けの nats で実現しています。 基本言語と依存関係: 処理系は Rust言語 で書かれており、推論グラフの構築や分散処理の開発用SDKとして Python言語 のBindingsも提供しています。依存関係として状態管理のためのetcd、推論処理エンジンである vllm 、 SGLang 、 TensorRT-LLM などに依存しています。 このように、NVIDIA Dynamoは推論高速化のためのソフトウェアですが、独自の処理系を提供するわけではなく、さまざまなコンポーネントを組み合わせた最適化を実施するためのオーケストレータのような機能を提供するフレームワークです。 インストールと基本動作 NVIDIA Dynamoはpip packageで配布されており、以下のコマンドでインストールが可能です。 pip install ai-dynamo[all] Python Package Index (PyPI) にはNVIDIA社のpackageを参照するスタブパッケージが提供されており、スタブ経由でNVIDIA社のリポジトリからインストールできます。NIXLなどの通信ライブラリについてもビルド済みのバイナリを梱包したものが配布されています。 分散処理に必要なetcd、natsについてはDynamo Serveの節でより詳細に解説します。 Note v0.2.0では、vllmエンジンとして ai_dynamo_vllm=0.8.4 が必要です。NIXLについてはバージョンの依存関係が定義されていませんが、筆者の環境では 公式 に従って検証時の最新の HEAD をビルドし、動作確認を実施しました。 NVIDIA Dynamoでは基本的な動作確認のためのコマンドとして、 dynamo run を提供するほか、分散処理のための dynamo serve 、 Kubernetes 向けの dynamo deploy などのコマンドを提供しています。 NVIDIA Dynamoの動作環境については 公式 をご確認ください。 本ブログでは主に dynamo run と dynamo serve について紹介します。 Dynamo Run dynamo run は対話形式のshellを起動するコマンドです。以下のようにモデルを読み込み、対話的なシェルを起動します。モデルは HuggingFace のモデルIDに対応している他、ローカルのファイルシステムからも読み込むことができます。 dynamo run out=vllm tokyotech-llm/Llama-3.1-Swallow-8B-Instruct-v0.3 Note dynamo run コマンドの out にはvllm、sglangなど推論を実行するエンジンを指定し、推論エンジン経由でGPUを利用します。outを指定しない場合、CPUが利用されるため、推論処理に時間がかかっている場合には、推論エンジンを正しく設定できているか確認してみてください。 上記のコマンドを実行すると以下のようなシェルが起動し、対話的な推論を実施できます。 $ dynamo run out=vllm tokyotech-llm/Llama-3.1-Swallow-8B-Instruct-v0.3 2025-05-01T07:52:54.065Z WARN __init__.vllm_version_matches_substr: Using ai_dynamo_vllm 2025-05-01T07:52:54.074Z INFO __init__.resolve_current_platform_cls_qualname: Automatically detected platform cuda. 2025-05-01T07:52:54.722Z INFO nixl: NIXL is available Loading safetensors checkpoint shards: 0% Completed | 0/4 [00:00<?, ?it/s] Loading safetensors checkpoint shards: 25% Completed | 1/4 [00:00<00:01, 1.55it/s] Loading safetensors checkpoint shards: 50% Completed | 2/4 [00:01<00:01, 1.49it/s] Loading safetensors checkpoint shards: 75% Completed | 3/4 [00:01<00:00, 2.19it/s] Loading safetensors checkpoint shards: 100% Completed | 4/4 [00:02<00:00, 1.85it/s] Loading safetensors checkpoint shards: 100% Completed | 4/4 [00:02<00:00, 1.82it/s] 2025-05-01T07:53:11.006Z INFO loader.load_model: Loading weights took 2.25 seconds 2025-05-01T07:53:11.207Z INFO model_runner.load_model: Model loading took 14.9596 GiB and 2.389781 seconds 2025-05-01T07:53:11.913Z INFO worker.determine_num_available_blocks: Memory profiling takes 0.57 seconds the current vLLM instance can use total_gpu_memory (79.19GiB) x gpu_memory_utilization (0.90) = 71.27GiB model weights take 14.96GiB; non_torch_memory takes 0.16GiB; PyTorch activation peak memory takes 1.26GiB; the rest of the memory reserved for KV Cache is 54.90GiB. 2025-05-01T07:53:12.045Z INFO executor_base.initialize_cache: # cuda blocks: 28108, # CPU blocks: 2048 2025-05-01T07:53:12.046Z INFO executor_base.initialize_cache: Maximum concurrency for 8192 tokens per request: 54.90x 2025-05-01T07:53:13.427Z INFO model_runner.capture_model: Capturing cudagraphs for decoding. This may lead to unexpected consequences if the model is not static. To run the model in eager mode, set 'enforce_eager=True' or use '--enforce-eager' in the CLI. If out-of-memory error occurs during cudagraph capture, consider decreasing `gpu_memory_utilization` or switching to eager mode. You can also reduce the `max_num_seqs` as needed to decrease memory usage. Capturing CUDA graph shapes: 100%|███████████████████████████████████████████████████████████████████| 35/35 [00:10<00:00, 3.40it/s] 2025-05-01T07:53:23.712Z INFO model_runner.capture_model: Graph capturing finished in 10 secs, took 0.32 GiB 2025-05-01T07:53:23.712Z INFO llm_engine._initialize_kv_caches: init engine (profile, create kv cache, warmup model) took 12.51 seconds 2025-05-01T07:53:25.279Z INFO dynamo_run::input::text: Ctrl-c to exit ? User › 終了時には Ctrl+C でプロセスを終了します。 この他、オプションなどについては 公式 を参照してください。 Dynamo Serve dynamo serve は事前に定義した推論グラフに従って各コンポーネントの起動、オーケストレーションを提供するコマンドです。 各コンポーネント間の通信にはnatsプロトコルが利用され、状態管理をetcdで実施するため、 dynamo serve の実行前にnatsサービス、etcdサービスを起動しておく必要があります。ポートやコンフィグがデフォルト値でよければ、 公式が配布するdocker compose用のyaml を利用するのが最も簡単で、ワーキングディレクトリをdynamoのリポジトリルートとした時に以下のコマンドで各サービスを起動できます。 docker compose -f deploy/docker-compose.yml up -d 推論グラフとコンポーネント 前述のとおり、 dynamo serve を利用するためには事前に分散処理を定義した推論グラフが必要です。ここでは dynamo serve を理解するために必要な推論グラフとコンポーネントについて解説します。 推論グラフは分散推論を行う際のコンポーネントの組み合わせや依存関係を示した定義です。コンポーネントは推論グラフを構成するためのパーツです。 具体的なコンポーネントにはFrontend、Processor、Router、Workerなどがあります。これらのコンポーネントの依存関係を整理し、Frontendで受け付けたリクエストをProcessorがRouterの情報をベースにスケジュール、実際の推論をWorkerで処理する、といった流れを定義するのが推論グラフです。 NVIDIA Dynamoでは LLMに関するコンポーネントと推論グラフのサンプル実装 が公開されています。 推論グラフのサンプルは以下のとおりです。 # From examples/llm/graphs/agg.py from components.frontend import Frontend from components.processor import Processor from components.worker import VllmWorker Frontend.link(Processor).link(VllmWorker) この例ではNVIDIA Dynamo SDKのPython Bindingを利用し依存関係を表現しています。 実際には components に含まれるFrontendやProcessorといったクラスがコンポーネントにあたり、これを実装する必要があります。 コンポーネントのサンプルは以下のとおりです。依存関係はコンポーネント内でも個別に定義できます。 # components/processor.py class Processor(ProcessMixIn): worker = depends(VllmWorker) router = depends(Router) ... 依存関係の定義の仕方については、基本的にはclass側で設定した依存関係に応じて処理系を実装し、実際の起動時の関係を .link で表現するといった使い方になります。 ここからは各コンポーネントでこの依存関係を使ってどのように処理を実装するかを簡単に紹介します。 処理の実装では、コンポーネントで定義した依存関係を以下のようなコードで動的に解決し、依存先のコンポーネントを変数のように扱うことができます。ここでは上記のprocessorの起動時に依存するVllmWorkerを self.worker_client に初期化するコードを掲載します。 comp_ns, comp_name = VllmWorker.dynamo_address() # type: ignore self.worker_client = ( await runtime.namespace(comp_ns) .component(comp_name) .endpoint("generate") .client() ) このように、分散処理におけるコミュニケーションをカプセル化し逐次的に処理を記述できます。ここで初期化された self.worker_client は以下のような形で別のプロセスに処理を引き継がせることができます。 engine_generator = await self.worker_client.generate( vLLMGenerateRequest( engine_prompt=engine_prompt, sampling_params=sampling_params, request_id=request_id, prefix_hit_rate=prefix_hit_rate, ).model_dump_json() ) 初期化の方法や、依存関係の解決の仕方には複数のパターンが存在するため、より詳細にコンポーネントを実装したい方は 公式 を参照してください。 dynamo serveの起動の流れ ここでは前節で紹介したコンポーネントと推論グラフを用いて dynamo serve を使った推論APIサーバを起動する流れを紹介します。 1. nats/etcdの起動 dynamo serve を実行する前に通信ライブラリのnatsと状態管理用のetcdのサービスを起動します。NVIDIA Dynamoのデフォルト値を利用するのであれば前述した通りdocker composeを用いて以下のコマンドで起動します。 docker compose -f deploy/docker-compose.yml up -d 2. dynamo serveの起動 次に dynamo serve コマンドを使って各コンポーネントを起動します。以下はワーキングディレクトリをNVIDIA Dynamoの examples/llm とした時のサンプルです。 dynamo serve graphs.agg:Frontend -f configs/agg.yaml このコマンドでは examples/llm/graphs/agg.py 内に定義されているFrontendを起点とする推論グラフを -f で指定した設定ファイルに基づいて起動するということを実行します。 このサンプルではaggregated modeの依存関係にあるFrontend、Processor、VllmWokerが起動し VllmWorker has been initialized が画面に出力されれば準備完了です。 3. 動作確認 NVIDIA DynamoはOpenAI互換のAPIを提供します。このため、以下のようなOpenAIクライアントを使った推論で動作確認が可能です。 from openai import OpenAI ENDPOINT = "http://localhost:8000/v1" def main(): client = OpenAI( base_url=ENDPOINT, api_key="needn't" ) chat_completion = client.chat.completions.create( messages=[ { "role": "user", "content": "Hello, how are you?", } ], model="tokyotech-llm/Llama-3.1-Swallow-8B-Instruct-v0.3", temperature=0.9, ) print(chat_completion.choices[0].message.content) if __name__ == "__main__": main() 4. 終了 dynamo serve を終了する際は Ctl+C など、親プロセスを終了させることで関連サービスを終了できます。 分散処理の仕組み 最後に dynamo serve を利用して分散処理を実施する場合、どのような分散処理が実行されるかを disaggregatedのサンプル実装 をベースに紹介します。 disaggregatedの実装はFrontend、Processor、VllmWorker、PrefillWorkerの4つのコンポーネントで構成されています。 LLMの推論は、入力された文章からKey/Valueの値を計算するPrefill処理とPrefillされた値から1トークンずつ回答を生成するDecode処理に分けることができます。NVIDIA Dynamoのdisaggregatedの実装では、このPrefill処理をPrefillWorker、Decode処理をVllmWorkerというそれぞれ別のコンポーネントで処理する機能を提供します。 より具体的な流れを解説します。disaggregatedの実装では Processor.link(VllmWorker).link(PrefillWorker) という依存関係を持っています。このため dynamo serve コマンドにより、それぞれの依存関係が解決され、初期化されます。初期化時にVllmWorkerは最大コンテキスト長に応じた計算用のメモリブロックをGPU上に確保します。 実際の推論リクエストがユーザから入力されるとFrontend経由でProcessorがVllmWorkerに対して処理をスケジュールします。この時にPrefillWorkerが利用可能であることを通知します。スケジュールされたVllmWorkerは起動時に確保したメモリブロックで利用可能なものを予約し、nats経由でPrefillWorkerへ通知します。Prefill Workerは通知された内容を元にPrefillを実行し、計算結果をRDMAで依頼元のVllmWorkerのメモリブロックに書き込みます。PrefillWorkerはメモリブロックを書き込んだことをRDMAのnotify機能を使ってメモリを確保しているプロセスに通知します。メモリの書き込み通知を受けたVllmWorkerはPrefillされたメモリブロックの結果を用いてDecode処理を実施します。 以下は、起動から推論リクエストをPrefill、Decodeし処理する流れを図にしたものです。 Note 図にあるように、実際にはVllmWorkerとPrefillWorkerにはDynamoのコンポーネントであるWorkerと実際に推論を処理するVllmWorkerが存在し、コンポーネントとzmq経由でコミュニケーションを取っています。また、これらのPrefill、RDMAの通知を実現するためにOSSのvllmから変更されたソフトウェアを利用しており、変更点については こちら で確認することができます。 まとめ 本ブログではNVIDIA Dynamoについて紹介しました。NVIDIA Dynamoは、効率的でスケーラブルな分散推論を実現するためのフレームワークです。サンプルの実装のままでもLLMの推論を効率化するための分散処理が実現でき、さらに推論グラフやコンポーネントを実装することで複雑な処理形を実装することも可能です。今後、LLMの需要拡大に向けてこうしたGPUの処理効率を向上させるソフトウェア、仕組みを活用することはますます重要になると考えられます。
アバター
こんにちは。イノベーションセンター Generative AI チームの安川です。 今回は私の所属するチームで開発しているrokadocというプロダクトの内部で利用している技術要素に重点を置いて紹介します。 本記事では「ドキュメント変換技術」であるrokadocについて、内部で利用している技術について紹介します。 rokadocはドキュメントをアップロードするとそれを生成AIで扱いやすいテキストへ変換するという機能を持ちます。 ユーザはドキュメントの内容に応じて自身で複雑な処理を考える必要がないというメリットがありますが、一方でその内部ではレイアウト解析やAI-OCRなどの複雑な処理を行っています。 本記事では実例を挙げつつrokadocの内部でどのような処理を行っているのかについて紹介します。 rokadocの基本的な使い方に関しては別途公開している「 生成AI向けのドキュメント変換技術 rokadoc の使い方 」で詳細に解説をしておりますので、そちらをご参照ください。 また パブリックベータ版 を公開しています。 利用回数など一部機能に制限がありますが、無料で利用できますので、気になった方は実際にお試しください。 rokadocが取り組む課題 rokadocを支える技術要素 レイアウト解析 表解析 AI-OCR 画像解析 まとめ おわりに rokadocが取り組む課題 近年、LLM(Large Language Model)やRAG(Retrieval Augmented Generation)などの生成AI関連技術が実用可能なレベルに達しており、これらを事業活用しようという動きが多く起こっています。 しかし実際に事業活用しようと考えると、多くの問題が浮き彫りになってきます。 例えば一般的なRAGにおいて、ドキュメントはテキストベースであることが前提となっています。 しかし実際の業務で用いられるドキュメントの多くは複雑な構造や図表を持ち、単純なテキスト化では情報が欠落しています。 また機密情報を含むドキュメントを扱う場合など、セキュリティの観点からクラウドサービスの利用が制限されるケースも少なくありません。 これらの問題を解決するため、私達のチームでは「 AIの力で埋もれた情報を価値あるものに 」というコンセプトの元、rokadocを開発しています。 そしてrokadocをより良いものにするため、オンプレミス環境でも動作可能な機械学習モデルや、それらに合わせた独自のアルゴリズムの構築を行っています。 rokadocを支える技術要素 今回はこのモデルやアルゴリズムがどのように動作しているのかを紹介していきます。 紹介の中で、大南らの構築したJDocQA *1 というデータセットを利用します。 これはオープンアクセス可能なPDF形式の文書で構成された、視覚情報とテキスト情報の両方を参照する質問応答データセットであり、多種多様な分野・形式のドキュメントを含んでいます。 レイアウト解析 ドキュメントは多種多様なレイアウトを持っています。一枚のページに図表とテキストが混在し、それぞれのまとまりごとに順序を持っています。 特に日本語のドキュメントは、横書き(左から右へ)と縦書き(右から左へ)が混在し、ページ内でも複数の読み進める方向が存在するなど、複雑な構造を持っています。 このような特徴を持ったドキュメントを対象に、文書内にあるテキストや図表を抽出し、段落などのテキストのまとまりを推定し、それぞれの要素の関係性から人間が自然に読み進める順序を導出することをレイアウト解析で行います。 図1. 上:JDocQA public_document00490.pdf 24ページ目を抜粋。下:見出し、テキスト、表の各領域を区別して抽出できている(JDocQA public_document00490.pdf 24ページ目へレイアウト解析の結果を加工して作成) 図2. 見出し、テキスト、図の各領域を区別して抽出できている(JDocQA kouhou00010.pdf 8、9ページ目へレイアウト解析の結果を加工して作成) 色ごとにそれぞれ異なる対象を表しており、タイトルは緑、テキストは黒、図は青、表は赤という形で記されています。 またそれぞれの領域の左上に推定された読み順が付与されており、それぞれ意味的に繋がりのある順序が付与されていることを示唆しています。 表解析 またドキュメントでは表という形でも情報が表現されます。 一般に表は列と行を持ち、それぞれの位置関係で情報ごとの関連性を示します。 しかし、実際の表の構造は多種多様です。 構成要素の数やセル内の情報量は勿論、セル結合、罫線の種類、背景色などもさまざまで、非常に複雑な形式を持つことがあります。 このような特徴を持つ表に対して、表解析を実施し、各セルの位置と範囲を推定します。 図3. 行方向及び列方向の結合セルも含め、表内のセルを抽出できている(JDocQA kouhou00156.pdf 13ページ目へ表解析の結果を加工して作成) セルと判定された領域がそれぞれ赤枠で囲われています。 このセル情報と後述するAI-OCRによるテキスト情報を組み合わせることで、表の構造とその内容を正確に反映したHTML形式のテキストへ変換できます。 AI-OCR ドキュメントに記述されているテキストの取得には、OCR(Optical Character Recognition:光学的文字認識)と呼ばれる技術を利用します。 これは画像として表現されている文字情報を認識し、テキスト化するという処理です。 日本語のドキュメントにはひらがな、カタカナ、漢字といった日本語の文字だけではなく、英語で利用されるアルファベットや数字、その他記号など多様な文字が登場します。 さらに、文字のフォント、サイズ、色、装飾(太字、斜体など)も考慮しなくてはなりません。 これらの多様な文字画像を認識しテキスト化するという処理を、AI-OCRで行います。 図4. 縦書きと横書きを区別し、多様な文字種を判別できている(JDocQA kouhou00024.pdf 11ページ目へAI-OCRの結果を加工して作成) 図5. 図4の一部を抜粋したもの 各行に対する解析結果を青文字で記載しています。 このドキュメントでは縦書きと横書きが混在し、文字の色やフォントもさまざまで、文字自体も多くの種類が存在していますが、問題なくテキスト化する能力があることを示唆しています。 画像解析 また、多くの業務において参照するドキュメントには、テキストや表だけではなくグラフやフローチャートなどの視覚情報も活用した表記や写真などの画像が存在します。 これらの画像情報はここまで紹介した技術を適用するだけでは、その本質的な意味や情報が欠落してしまうという問題があります。 例えば以下の図6に示されるようなドキュメントがあります。 図6. JDocQA public_document_ministry02357.pdf 7ページ目を抜粋 右側のグラフに注目してください。 仮にAI-OCRを用いて文字を取得しても、グラフ内の色と凡例の色との間にある対応付けという情報が落ちてしまいます。 位置関係で取ろうにも、グラフ内と凡例とで順序が異なるので難しく、色ごとに対応付けしようとしても左側のグラフと共通の色が存在するため、意図せず情報が混ざってしまいます。 そこで私達は、画像の内容を理解し、その特徴を捉えた文章へ変換するというアプローチを採用しています。 実際にこのページを変換し、右側のグラフがどのようにテキストで表現されているのかを示します。 キャプション: ### 画像の説明 この画像は、**IPv6導入に関連する課題**についての調査結果を示した棒グラフです。調査対象は「大学等」であり、調査年は「平成30年度」(2018年)とされています。全体のサンプル数は **n=395** です。 #### グラフの構造 - 横軸:課題の種類 - 縦軸:割合(%) - 各色の部分は異なる課題を表しており、それぞれの割合が明確に示されています。 #### 課題の分類と割合 1. **設備の更新にかかる手間及びコスト** - 色:赤 - 割合:35.7% - 最も大きな課題として挙げられており、IPv6導入に伴う設備の更新にかかる手間やコストが多くの人々にとって大きな負担となっていることが示されています。 2. **利用者機器の置き換えにかかる手間及びコスト** - 色:緑 - 割合:12.9% - IPv6に対応していない利用者機器の置き換えにかかる手間やコストが課題として挙げられています。 3. **想定されるトラブル等の情報不足・移行リスク** - 色:紫 - 割合:18.5% - IPv6導入に伴うトラブルやリスクに関する情報不足が課題として挙げられています。 4. **IPv6に詳しい技術者の不足** - 色:青 - 割合:10.1% - IPv6に関する専門知識を持つ技術者の不足が課題として挙げられています。 5. **運用ポリシー等の変更及びそのノウハウ** - 色:オレンジ - 割合:5.6% - IPv6導入に伴う運用ポリシーの変更やそれに必要なノウハウが課題として挙げられています。 6. **インターネット接続サービスの対応** - 色:薄い青 - 割合:1.5% - IPv6に対応したインターネット接続サービスの準備や対応が課題として挙げられています。 7. **対応機器の価格** - 色:濃い青 - 割合:3.5% - IPv6に対応した機器の価格が高いことが課題として挙げられています。 8. **その他** - 色:茶色 - 割合:4.3% - 上記のカテゴリには該当しないその他の課題が挙げられています。 9. **わからない** - 色:濃い緑 - 割合:7.8% - IPv6導入に関連する具体的な課題がわからないという回答が7.8%ありました。 #### legend(凡例) - 各色に対応する課題名が右側に表示されており、それぞれの課題がどの色で表されているかが一目でわかるようになっています。 #### まとめ このグラフは、IPv6導入に際して大学等で直面している主な課題を示しており、特に「設備の更新にかかる手間及びコスト」が最も大きな課題であることがわかります。また、技術者の不足や運用ポリシーの変更、機器の置き換えなども重要な課題として挙げられています。 このように視覚情報として表現された情報を的確に捉えたテキストへと変換することで、後段のRAGなどで正確に検索や生成が可能となります。 まとめ 以上がrokadocで使われている技術要素の紹介でした。 ここまで紹介してきた「レイアウト解析」「表解析」「AI-OCR」「画像解析」といった技術要素をrokadoc内部で適切に組み合わせることで、元のドキュメントの情報を可能な限り保持したまま、構造化されたJSON形式のテキストデータへと変換します。 この出力を用いることで、社内ドキュメントを活用したRAGシステムを精度高く実現できます。 実際に類似した機能を持つ他社製品と比べた場合の結果を以下に示します。 ここではJDocQAを対象に、想定されるユーザ質問から関連文書をどの程度の精度(※)で検索できるかを測定しました。 その結果として、rokadocが最も良い精度となりました。 機械学習モデルやヒューリスティックなアルゴリズムを用いていることから、100%という精度を達成することは難しいのですが、rokadocは対象のドキュメントの情報を効果的に抽出し、検索に適した形式へ変換していることを示唆しています。 (※)解析結果を対象に想定質問で検索を実施。 検索の結果を関連度合いで並べ替えた後、実際に関連しているドキュメントのページが上位三件に含まれていれば1、それ以外を0として評価。 全体の平均を最終的な検索精度とした。 おわりに この記事では、ドキュメント解析技術である rokadoc について、内部で利用されている技術要素に重点を置いて紹介しました。 これらの中には現在開発中の項目も含まれていますが、同様の処理を内部で行っている パブリックベータ版 を現在公開しています。 そして上記で紹介した技術の搭載を始めとした、精度向上や処理高速化のための更新も順次行っています。 利用回数に制限はありますが無料でお試しいただけますので、是非ともお試しください。 またこれらの技術を搭載し、インターネットに接続することなく利用可能なオンプレミス版の開発も進めています。 興味を持っていただけましたら、 rokadocお問い合わせフォーム からお問い合わせください。 それではみなさん、お読みいただきありがとうございました。 *1 : 大南英理, et al, "JDocQA: 図表を含む日本語文書質問応答データセットによる大規模言語モデルチューニング", 言語処理学会 第30回年次大会, 2024
アバター
こんにちは、クラウド&ネットワークサービス部の古賀です。普段はクラウドサービスのサービス企画を担当するかたわら、デザインプロセスを業務に取り入れたサービス改善などの活動に参加しています。 前回の記事「 1枚のSIMでキャリアを冗長化!Active Multi-access SIMの特長と仕組み 」では、Active Multi-access SIMの特長や仕組み、活用シーンなどをご紹介させていただきました。第2回は本サービスの開発秘話について、サービス企画チームのメンバーにインタビューしましたので、その模様をお届けします。 マルチアクセスSIM開発のきっかけ 開発のこだわりポイント マルチアクセス特許取得までの長い道のり 開発は苦労の連続、その先に待っていた達成感 まとめと次回予告 きっかけは数年前に起きたモバイルネットワークサービスの大規模故障。 多くの人が大規模故障の影響を受け、通信できなくなった携帯を手になすすべもなく何時間も復旧を待つしかありませんでした。モバイル通信の冗長化の必要性が浮き彫りになった大規模故障をきっかけに、 「課題解決に向き合おうとした通信事業者NTT Comの技術者の発想力」×「『IoTで日本社会をよくしていきたい』という熱い想い」から生まれたNTT Comの「Active Multi-access SIM(以下「マルチアクセスSIM」)」サービスの開発秘話と、特許を取得するまでの紆余曲折 について、開発に関わったNTT Com 5G&IoTサービス部でサービス企画を担当している永作さん、春原さん、小山さんにお話を伺いました。聞き手は販売企画を担当している高野です。 マルチアクセスSIM開発のきっかけ 高野:マルチアクセスSIMの開発が始まったきっかけを教えていただけますか? 永作さん:一番直接的なきっかけは2022年に他社で発生したモバイルネットワークサービスの大規模故障ですね。こうした故障に備えた対策はデュアルSIMの採用などさまざまな方法が考えられますが、 より手軽にIoTでのモバイルキャリア冗長を実現できる手法を検討しました。 通信障害で業務が止まって大変だったというお客さまの話を聞き、とはいえIoTの環境は冗長化したくなったからといって簡単に機器の入れ替えができるものではない状況の中で、"通信事業者として何か手を差し伸べられるものはないだろうか?"という思いが強くなり、IoT Connect Mobile Type Aで提供しているマルチIMSI方式 1 をうまくキャリア冗長に役立てられないか?と考えたのがきっかけです。 高野:あの障害は一大事で記憶に残っていますし、マルチアクセスSIMはそれがきっかけで生まれたサービスだとぼんやりとは知っていましたが、そういう思いがあったということは初めて知りました、すごいですね。 永作さん:その年の初秋に、マルチIMSI方式での商用提供実績を有するTransatel(トランザテル) 2 R&D担当者と共にマルチIMSI方式に関するワークショップを行い、本方式によるキャリア冗長SIMの実現可能性について議論しました。また、2022年秋冬頃の初期検討以来、マルチアクセス SIMの仕組みと深くかかわるSIMアプレット 3 のチームとは、深く協力・議論をしてここまでやってきました。 開発のこだわりポイント 高野:サービスを実装するときに重視した要件は? 小山さん: 多くの端末で利用できる、SIMが自律的に切り替わり自動で使える、ということを重視しました。 IoTは用途もニーズもさまざまですが、自動化を志向されるお客さまのニーズにこたえるためにも、実装が固まっているものに対して意識することなく使っていただきたい。そこを重視して自動で切り替わる仕組みを採用しています。そしてできるだけ多くの端末で利用できるように、端末の癖も一部吸収しながら動作するように試行錯誤して自律の仕組みを実現しました。 高野:さまざまな要件に対して幅広く対応できるような仕様を意識したということでしょうか? 小山さん:はい、キャリアの障害がきっかけで困ったお客さまも多数いらっしゃったでしょうし、次の開発でどうにか対策をしたいと思いつつ、既存システムの改修の難しさに直面されているお客さまも多いだろうと考えました。冗長化をしたくても簡単ではない、SIMスロットが1つしかないデバイスを使っているケースもあります。そういうお客さまにとって "1つのSIMスロットに挿せば、メインは信頼性の高いフルMVNO回線によるドコモ網接続を使っていただいて、何かあったらサブ回線へ自動的に切り替わり、通信の継続性を確保できる"というサービスはきっと要望に応えられるだろうと考えました。 また、 動作を担保する、というところにも重きを置きました。 このサービスは冗長性を担保するために通信をし続けることが重要です。さらにその上で動作するアプリが動き続けることは、お客さまから絶対に求められているポイントだし、我々もそこを怠ってはいけないと考えていました。いろんな端末でそれぞれ端末の癖があったり用途が違ったり、そういった端末の差分を吸収できる仕様にするところは苦労しました。通信不良にもさまざまなパターンがあったりして、通信ができないっていうところにもいろんなレイヤーでできなくなる部分があるので、そのどのレイヤーで通信ができなくなったとしても対処しちゃんと動き続けるような、仕様の策定にはかなり注力しました。 あらゆるパターンをさまざまなデバイスで検証することを繰り返し、動かなくなるパターンを洗い出してひとつひとつ対処するという地道な作業を繰り返して、どんな状況でも動き続けるようなアプレットに仕上げるというところに注力しました。 高野:結構骨が折れそうな工程ですね! 永作さん: IoT機器における長期間の利用において、途中でアプレットによる疎通確認サイクルが止まってしまうことのないよう、処理シーケンス(順序)を徹底的に議論・検証して安定動作を追求しました。 マルチアクセス特許取得までの長い道のり 高野:マルチアクセスSIMは特許 4 を取得されていますが、取得に至った経緯を教えていただけますか? 永作さん: 独自のサービス提供によって付加価値を高めることを目的に特許出願を進めました。 高野:どの部分で特許を取ることが出来ましたか? 小山さん:切り替わる技術は一般的にあるものですが、マルチアクセスSIMはお客さまのニーズを考慮した設計となっています。それを実現できたところが評価され、特許が取れたポイントになったところです。メイン回線としては通信が安定していて経済的なローカル回線を使っていただきたいという思いがあります。故障が発生した時に通信を継続させるために一時的にローミング通信のサブ回線に切り替わるんだけれども、復旧したら自動的にメイン回線に戻ってくることで お客さまにとっての安定性・経済性を実現できる、お客さまに寄り添った設計を入れている点がポイントでした。 高野:取得まで大変でしたか? 永作さん:特許権の取得に至るまでは、複数回の拒絶理由通知に対してそれぞれ対応策を検討し、手続補正書を提出することとなり、道のりは長かったです。 高野:先ほど拒絶理由通知書の文書の文言を見せていただきましたが、私だと心が折れてしまうと思うので、もうやり切ったことがすごいなあと思いました! 開発は苦労の連続、その先に待っていた達成感 高野:開発において苦労した点はどんなところでしたか? 春原さん:通信障害が実際に起きたらどうなるのか、想像力を働かせて考えました。 例えば障害時に大量のSIMが切替わるとどうなるか?SIMからの接続要求が設備へ集中することになるんですよね。そういうことに対するケアも必要じゃないかと。ほかのモバイルを担当しているクラウド&ネットワークサービス部、開発を担当しているイノベーションセンター、Transatelのメンバーとも議論しました。 結果として、 何かあった時に大量のSIMが同じ時間帯で一斉に接続要求を出さないように、アプレットの中でちょっと時間差で接続するような設定をしました。 自ずと再接続をばらけさせるような仕組みが設けられている、そんなイメージです。 高野:サービスリリース前に無償トライアル提供を実施されたとのことですが、トライアルの結果はいかがでしたか? 小山さん:トライアルを実施して大きく改善した点は2点あります。 1つは切り替わりにかかる待ち時間が少し長い点で、私たちの試験結果でも認識し始めていましたし、お客さまからフィードバックでもいただくことが多かった。先ほど一斉に切り替わらないように時間差で接続する制御をしたと話しましたが、そこが接続時間が長くなる一因でもありました。その 制御の仕組みを再度見直すことで切り替え時間を短縮し、お客さまの操作性の向上につなげることができました。 もう1つは経済的な点で、社内で考えていた価格設定とお客さまがこのサービスにどのくらい支払えるかという価格感について、トライアルを通して大きなずれのないことが確認でき、今後の販売にも自信が持てました。 お客さまのニーズに寄り添って開発をしてきて、お客さまの課題を解決し、経済的にもミートするサービスを開発できたという実感があり、このサービスを活用していただくことでIoTを、日本社会をよくしていくということにつながったかなと考えています。 高野:プロジェクトの中で、一番やりがいを感じた時はいつでしたか? 小山さん:お客さまに価値を実感いただいて購入いただけた時ですね。A社では、お客さまによる導入検討時の動作確認を通じて、自動で切り替わる仕様がユースケースとマッチしました。 お客さまに、課題解決のお役に立てると判断いただけたことが非常にうれしいし、やりがいを感じました。 永作さん:本当に実現できるかどうか、サービス化できるのか分からないという状況で、 リスクある中でも諦めずにチャレンジを続けて、結果的にサービスという形にできた。業界的にも前例がない独自サービスとしてリリースできたことは、関係者全員が達成感を感じている点だと思います。 不安を感じながらも信じてやり続けることで開発を成功させられるということを周囲にも知ってもらうことができ、自分も挑戦しようという気持ちを与えられたのではないかと思っています。 まとめと次回予告 「お客さまの課題解決を実現できるサービスを提供したい」という熱い思いがひしひしと伝わり、筆者自身もお話を聞きながら胸が熱くなる素晴らしいインタビューでした!第3回はマルチアクセスSIMの今後の開発の展望や活用事例などについてお届けする予定です。ぜひ次回の記事も併せてお読みいただけたら幸いです! 今回ご紹介したマルチアクセスSIMの詳細情報についてはこちらをご参照ください。 マルチアクセスSIMのオフィシャルサイト Active Multi-access SIM|ドコモビジネス|NTTコミュニケーションズ 法人のお客さま また、本サービスは1枚からWeb購入・検証可能です。まずは試してみたいという方はぜひ以下のページからお申込みください! ドコモビジネスオンラインショップ IoT Connect Mobile® Type S|ドコモビジネスオンラインショップ|NTTコミュニケーションズ 記事に関するお問い合わせは、 iot-connect@ntt.com  までメールでご連絡ください。 ※お手数ですが、@を半角文字に置き換えてください 1枚のSIMに対して、複数のIMSI(International Mobile Subscriber Identity。世界でユニークとなる携帯電話ユーザーの識別子)を格納する方式のこと ↩ 2019年よりNTTグループに加わったフランスのグローバルコネクティビティプロバイダー ↩ SIMカード上に搭載するアプレット。SIMカードの論理構造は、通信プロファイル領域とアプレット領域に分けられ、従来のSIMカードは通信プロファイル領域にアプレット領域が含まれている。NTT Comは2つの領域を分割する技術であるアプレット領域分割技術を開発している。SIMアプレットに関する記事はこちら( ローカル5G網と公衆モバイル網への接続を切り替え可能なSIMアプレットの開発 - NTT Communications Engineers' Blog ) ↩ 特許第7478277号「SIM、通信装置、切替方法、及びプログラム」に関する発明 ↩
アバター
こんにちは、マネージド&セキュリティサービス部の閏間です。このたび、社内で若手向けセキュリティイベントを開催したのでその紹介をします。草の根的な活動ですが、NTT Comではこういうことも行われているんだということを知ってもらえればと思います。 はじめに イベントの目的は、「業務棚卸しと将来像の明確化」「他組織を知って自業務に活かす」 発表者は幅広い部署から集まりました イベント準備もスムーズに進行しました イベント当日は大変盛り上がりました おわりに はじめに この4月に全社横断で入社2年目のセキュリティ系人材の方に集まってもらい、担当業務や1年間の成果、今後のキャリアプランを発表し合ってもらうというイベントを開催しました。 これは正式な会社の施策ではなく、私が自部署の若手を巻き込んで企画したいわば有志イベントです。会社全体の育成施策としては1年目終了時点ではこのような会はないので、独自に企画してみました。 昨年度自部署の新人の教育係を担当した身として1年目終了時ってけっこう重要なタイミングだと思うので、何もないのはちょっと物足りないなと思ったのですよね。なので、会社の育成施策を補完するような感じで新人にプラスになることが何かできればなあ、と思って企画してみたわけです。 イベントの目的は、「業務棚卸しと将来像の明確化」「他組織を知って自業務に活かす」 イベントの企画内容を検討した結果、以下のような目的で1年間の集大成としての報告会を開催することとしました。その名も「Security Rookies 2024」です!(2024は、2024年度入社の意) 1年間の取り組みを棚卸しして、入社3年目終了時点の目指す姿を明確化する。 他のセキュリティ系組織の業務を知り、自身の担当業務改善/キャリアプランに取り入れられるヒントを得る。 ちなみに企画した側の想いとしては、発表者の皆さんがイベント参加を通して将来的に会社のセキュリティ事業/施策の中核人材に育っていくための何か気付きを得てもらえれば、なんてことも思っていました(ちょっと大げさですけどね!)。 発表者は幅広い部署から集まりました 単に成果報告会をやるだけなら対象をセキュリティ系人材に絞る必要はないのですが、前述の通り今回はセキュリティ系人材を対象としました。理由は2つあって、1つは私が担当した新人がセキュリティ関連の業務を行っていたから、ということ。もう1つは、セキュリティは社内的にも世の中的にも年々その重要度が増しているので、セキュリティという軸があると発表者集めをしやすいと思ったから、です。 今回はあまり大規模なイベントにすることは考えていなかったので、発表者は広く社内に募集をかけるのではなく私の社内セキュリティ関連のツテをたどって集める形としました。私が声をかけた方々はみなさん好意的で、上長の方々の理解もあり、結果的に会社のセキュリティ系部署を多くカバーする形で合計8人の発表者に集まってもらうことができました。会社規模を考えると決して大人数ではありませんが、初めてやる有志イベントとしてはちょうどよい規模だったと思います。 参加者の所属部署のミッションは多岐にわたります。セキュリティ系のサービスやソリューションをつくってお客様に提供するとか、社内のセキュリティ運用を行うとか、セキュリティ技術開発を行うといったものなどです。守る対象でいえば自社とお客様の両方、職種としてはいわゆるセキュリティエンジニアと呼ばれる職種から、サービス企画やコンサル、セールス寄りといった非エンジニアまで、幅広いセキュリティ系人材が集まりました。 イベント準備もスムーズに進行しました 発表者が固まったのち聴講者募集も行いました。社内でどんなセキュリティ系部署があるかについて関心のある人とか、セキュリティ系人材を応援する気持ちのある人に聞いてもらいたいと思ったからです。発表者にとってもギャラリーが多いほうが張り合いがでますしね。 聴講者のほうも募集はゆるい感じで行いました。具体的には発表者の関係者に声をかけたり社内ブログを使って募集しました。イベントはリモート開催としましたが、当日の会議URLを広く公開するのではなく希望者に会議URLをお送りする形にしました。これは、事前に聴講者がどの程度になるか把握したかったためです。結果として約70人弱の方に聴講希望いただきました。 発表者に対しては事前の顔合わせをリモート会議で実施しました。その場では私から目的や発表してもらいたいことを改めて伝え、タイムテーブルの確認や録画OKかなどの事務的な確認も行いました。 また、イベント開催にあたり以下のように多くの方々にご協力いただきました。皆さん協力的で本当に助かりました。 NTT Comのセキュリティエバンジェリストである竹内さんにイベント冒頭でひとこといただきました(堅苦しい会にしようとは思っていませんでしたが、ちょっとした緊張感もあったほうがよいかと思ったため)。 私が所属する部門の責任者の方に後ろ盾になっていただきました(発表者集めにあたって、万一、勝手にこんなことするな的なクレームが入った場合に備えて。その場合は丁寧に趣旨説明などして理解いただこうと思っていましたが、その際に援護いただこうと思っていました。実際にはそのようなクレームはなかったです)。 私の周辺の若手数人にイベント内容のアイデア出しを手伝ってもらいました(小規模なイベントなので自分一人で企画/実施できると思いましたが、いろんな観点でのコメントをもらってイベントの質を高めたかったため)。 イベント当日は大変盛り上がりました 入念な準備をして、いざ当日。 司会者による開会あいさつから始まり、続いてセキュリティエバンジェリスト竹内さんからひとこといただきました。お話の内容としては、セキュリティ対策は攻撃者とのいたちごっこなので終わりがない、前進/改善し続けないといけないがそれがやりがいとなる、今後セキュリティのプロになることを期待している、といったものでした。2年目社員にとっても、セキュリティに関して経験豊富な方からの言葉はよい刺激になったのではないかと思います。 その後各発表者の発表に進みました。各自の持ち時間は、発表10分、質疑5分。2年目社員が主役の会なので質問についてはまずは2年目社員から募り、時間が残ったら他の方からも質問してもらうようにしました。発表者間で自身の業務効率化のためにアドバイスを求めたりするなど、活発に質疑が行われました。 私も聴講しましたが、発表者のみなさんが2年目になったばかりとは思えないくらいスムーズにプレゼンしつつ、自己アピールしている姿が印象的でした。 業務内容についても1年目から行うにはけっこう大変と思える業務を行っている方が多いことは驚きでした。例えば、入社半年後から社内セキュリティ運用(脅威情報収集や脆弱性緊急対応)の輪番業務メンバーになった方とか、1年間で全国の支社/支店に数十回も出張してセキュリティ商材の勉強会開催や提案支援を行った方がいらっしゃいました。 イベント全体の時間は約2時間半、同時参加者数は最大で50人程度でしたが、忙しい業務の合間を縫って行う有志イベントとしては、ちょうどいいサイズだったと思います。 イベント終了後発表者と聴講者の何人かの方に感想を聞きましたが、以下のように前向きなコメントをいただけました。これを読む限り、目的は達成できたかなと思います。 発表者の感想 他の部署の具体的な業務内容を知る機会があまりなかったので、様々な業務について聞けてよかったです。同じセキュリティ系だったとしても、部署が異なると仕事をする上で重視するポイントなどが異なることがプレゼンからわかり、とても興味深かったです。 未経験からセキュリティエンジニアとなった1年間は業務に慣れることに必死で、「成果は何か」という部分まであまり考えられていませんでした。しかし、今回の発表を通じて自身の業務内容を棚卸しし、この1年間の成果と成長を実感することができました。また、他の発表者のお話から新しい視点を得て、自身の不足部分にも気づくことができました。今後も長い社会人生活の中でも記憶に残るような、非常に貴重な経験となりました。 発表者は【セキュリティ】を共通軸として集まりましたが、各自の業務は非常に多彩で、それぞれの発表を通じて視野が大きく広がり、学びと良い刺激を得ました。また、発表に向けた業務の棚卸しや他の方の発表を通じて自身の課題も明確になったため、今年度はその課題を着実に克服し、ドコモグループのセキュリティ中核を担う人材として成長していきたいです。 聴講者の感想 発表者を「セキュリティ」に絞ったことで、年次を重ねた私にとっても、これまで知る機会のなかった各部の業務内容を知ることができ、キャリアを考える上でも非常に有意義な時間となりました。また、発表者のレベルが非常に高く、私自身もより一層努力しなければと刺激を受ける、充実したひとときでした。 内容もレベルが高く、発表者らしさが出ていてとても印象的でした。同じセキュリティ分野であっても普段触れない分野の話も多く、学びや気づきがあり良い刺激になりました。 自分もさらに成長していきたいと感じました。 おわりに 若手向けの1年目成果報告会として、社内セキュリティイベントを開催しました。イベントを通して発表者の方に、将来の目指す姿を明確化するとか、他組織のセキュリティ人材の業務内容やキャリアプランを参考にしてもらうといった成果を得ることができたんじゃないかなと思います。 リモート開催だったので対面での交流はありませんでしたが、これをきっかけに2年目社員間で交流が深まるとうれしいです。 また、こういう草の根活動もいいもんだなとあらためて思いましたので、いつかまたこうイベントをやってみたいです。そして他にもやろうという方が現れて、会社がさらに活性化するとうれしいですね。
アバター
3 月 15 日 ~ 3 月 21 日に開催された IETF 122 で、Media over QUIC Transport(MoQT)の相互接続試験に参加しました。 相互接続試験では、NTT Communications(以下 NTT Com) が OSS で公開している moq-wasm を持ち込み、4 種類の MoQT 実装と接続できました。 本記事では、Media over QUIC Transport(MoQT)の概要や、相互接続試験で遭遇した問題や、相互接続試験の結果について報告します。 はじめに Media over QUIC Transport(MoQT)とは 取り組みの背景 相互接続試験の雰囲気と結果報告 相互接続試験で見つかった問題 1 ( moxygen ↔ moq-wasm ) 相互接続試験で見つかった問題 2 ( aiomoqt ↔ moq-wasm) 相互接続試験で見つかった問題 3 ( 複数の実装 ↔ moq-wasm) 結果を振り返って おわりに はじめに NTT Com で SkyWay の R&D エンジニアをしている 内田 です。 3 月 15 日から 3 月 21 日で開催された IETF 122 の ハッカソンイベントで、Media over QUIC Transport(MoQT)の相互接続試験に参加しました。 本記事では、我々の実装の不具合や、他実装の修正提案、WebTransport ライブラリへの Pull Request など、相互接続試験によって得られた知見や成果を共有します。 同僚の 前田 が参加した IETF 121 の記事も過去に公開しておりますので、こちらも合わせてご覧ください。 Media over QUIC Transport(MoQT)とは Media over QUIC Transport(MoQT)とは、その名の通り、QUIC プロトコルを利用して Media を送受信するためのプロトコルです。 MoQ のプロトコルスタック(出典: 中間会議資料 ) 前回の IETF 121 参加記事 でプロトコルスタックについては詳しく説明しておりますので、本記事では割愛します。 取り組みの背景 NTT Com が開発している SkyWay は、誰でも簡単にビデオ・音声通話が簡単に実装できる、マルチプラットフォームな SDK です。 SkyWay は現在、WebRTC を採用して、ビデオ・音声通話を実現しておりますが、WebRTC はウェブ会議に非常に特化したプロトコルであるため、それ以外のユースケースにおいては WebRTC のカスタマイズ性に関する問題が発生する場合もあります。 そのため SkyWay は、幅広いユースケースで活用できる MoQT による映像・音声・データ配信に注目しており、MoQT のプロトコル実装 moq-wasm の開発や IETF の相互接続試験への参加を通して、MoQT の仕様標準化に貢献していきたいと考えています。 より詳しいモチベーションについては、 MoQ とか勉強会#2 の登壇スライドをご覧ください。 相互接続試験の雰囲気と結果報告 3/15(土) ~ 3/16(日) は ハッカソン の時間として確保されており、3/16 の夕方にはその結果が会場全体に共有されます。 会場はテーブルごとに取り組む内容が分かれており、私は Media over QUIC / RTP over QUIC のテーブルに合流しました。 MoQ のテーブルには、Meta や Cisco、Meetecho、学生の方など、4 人 ~ 8 人程度の方が集まっていました。IETF のセッションで発表予定の方もいたため、土日通してテーブルにいたのは 4 人程度でした。 私は IETF 初参加で初対面の方ばかりだったのですが、打ち解けやすい環境で、非常に楽しく相互接続試験を行えました。 今回の相互接続試験では、IETF 121 での結果に引き続き、多くの MoQT 実装との接続に成功しました。持ち寄られた MoQT 実装は、draft-08 対応している実装と、draft-10 対応している実装で分かれていたものの、moq-wasm は draft-10 に対応していたため、適宜修正しながら相互接続試験を行いました。 MoQT は WebTransport と Raw QUIC の両方に対応しているプロトコルですが、moq-wasm は WebTrasnport のみ対応しているため、WebTransport に対応している moxygen, meetecho, aiomoqt, moqtail などの実装と相互接続試験を行い、接続に成功しました。相互接続試験の結果については、下図をご確認ください。 相互接続試験で見つかった問題 1 ( moxygen ↔ moq-wasm ) Meta が開発している moxygen の client と moq-wasm を接続した際に、MoQT ではなく WebTransport レベルで疎通できないという問題に遭遇しました。 この問題は、moxygen が利用している proxygen という HTTP3(WebTransport)ライブラリと、moq-wasm が利用している wtransport という WebTransport ライブラリが疎通しない問題でした。 wtransport が WebTransport の双方向ストリームを確立する際に、想定されている種別ではない QUIC フレームを受け取ると確立できずに破棄されてしまうのが問題であったため、wtransport にパッチを加えることで解消できました。この変更は wtransport に PR を上げて、現在はマージされています。 この問題は IETF121 での相互接続試験でも発生しており、解消できていない問題でした。 自身で実装していない QUIC / WebTransport ライブラリの問題で、原因究明には 3 日ほどかかってしまいましたが、IETF 期間中に解消でき、moxygen client と疎通が確認できたため良かったです。 相互接続試験で見つかった問題 2 ( aiomoqt ↔ moq-wasm) aiomoqt と moq-wasm を接続した際に、MoQT レベルで疎通しない・特定のメッセージがパースできない問題に遭遇しました。 MoQT レベルで疎通しない問題に関しては、aiomoqt のサーバー実装で受け取った URL が正しくデコードされていない問題であり、こちらは aiomoqt の実装者の方に連絡することで解決できました。 また、特定のメッセージがパースできない問題に関しては、我々の実装である moq-wasm において SUBSCRIBE メッセージの Parameter 情報の処理が誤っていたことが分かったため、修正を加えた所、疎通が確認できました。 相互接続試験で見つかった問題 3 ( 複数の実装 ↔ moq-wasm) 複数の実装から我々の moq-wasm サーバーに SUBSCRIBE メッセージを送信した際に、サーバー側で MoQT メッセージが受け取れず、処理できないという問題が発生しました。 この問題の原因究明には非常に時間がかかりましたが、QUIC のログを確認したところ、「パケットサイズが大きい MoQT メッセージを双方向ストリームで受け取った際にパケットロスが発生し、回復しない」という事象が原因であるとわかりました。 この問題の根本的な原因は未だ分かっていませんが、QUIC のパケットロス検知の閾値の変更や、QUIC パケットのサイズの上限を設定する修正を加えたところ、疎通が確認できました。 結果を振り返って 今回の相互接続試験では、解決に時間を要する問題が複数発生しました。 しかし、前回の IETF121 では接続できなかった moxygen client ↔ moq-wasm server 間での接続も成功し、wtransport に PR を上げることで、WebTransport の相互接続試験という観点でも貢献できました。意義のある取り組みになったと感じています。 今後は QUIC / WebTransport の問題にも迅速に対応できるよう、これらの仕様についても理解する必要がありそうです。 「MoQT 実装は QUIC 対応のみの実装も多い」という事も実感できたため、今後は moq-wasm の Raw QUIC 対応も行っていきたいと考えています。 おわりに 今後も MoQT の仕様変更に追従して moq-wasm の開発を継続し、Raw QUIC 対応や、RTP over QUIC の実装なども行うことで、仕様の標準化に貢献していきたいと思っています。
アバター
みなさんこんにちは、イノベーションセンターの益本 (@masaomi346) です。 Network Analytics for Security (以下、NA4Sec) プロジェクトのメンバーとして活動しています。 この記事ではフィッシング詐欺がどのように行われているのか、フィッシングサイトがどのような仕組みで動作しているのか、注意喚起を兼ねて紹介します。 ぜひ最後まで読んでみてください。 フィッシング詐欺について フィッシング詐欺がどのように行われているのか フィッシングサイトがどのように構築されているのか フィッシングサイトがどのように動作しているのか どんな情報を窃取しているのか 窃取した情報はどこに送られるのか 相手の情報を収集・判別する 窃取したクレジットカード番号が有効であるか確認する フィッシング詐欺に引っかかるとどうなるのか フィッシング詐欺を減らすための取り組み マラソン型の撲滅イベント開催 国内カード会社等による共同の取り組み フィッシングハンターたちによるSNS投稿 被害に遭わないようにするには 被害に合わないための手段(例)まとめ さいごに フィッシング詐欺について フィッシング(Phishing)詐欺とは、メール等から本物そっくりの偽サイトへ誘導し、IDやパスワードを入力させて情報を盗み取る詐欺の手口です。 年々フィッシング詐欺による被害が増加し続けています。 特に、3月下旬あたりから証券会社を騙ったフィッシングメールや投資詐欺メールが大量に出回っており、各所で注意喚起を出しています。 証券会社口座 不正アクセス被害相次ぐ フィッシング詐欺に注意 フィッシング詐欺(証券会社を装う偽サイトへの電子メール等での誘導)にご注意ください NA4Secでも証券会社を騙ったフィッシングサイトを観測しています。 🚨⚡ #Phishing #フィッシング詐欺 (🇯🇵) Brand: #野村證券 IP: 🌍 103.112.211[.]228 (ASN:AS150855) URL: 🎣 hxxps://mehhkapradwwoesi.qcwb76.com/ 🎣 hxxps://vasoconstrictio.qcmky6r.com/ 🎣 hxxps://xeroththaamiahl.qcw5htg.com/ 🎣 hxxps://yesterdaynessrr.mkca0.com/ H/T to Team NA4Sec pic.twitter.com/IRX3oaBsEH — Metemcyber (@Metemcyber) 2025年4月4日 🚨⚡ #Phishing #フィッシング詐欺 (🇯🇵) Brand: #Rakuten #楽天 #楽天証券 IP: 🌍 47.83.189[.]115 (ASN:AS45102) URL: 🎣 hxxps://217564.top/oeugef/ 🎣 hxxps://255711.top/oeugef/ 🎣 hxxps://363342.top/oeugef/ 🎣 hxxps://368226.top/oeugef/ H/T to Team NA4Sec pic.twitter.com/jIdn9xbMnP — Metemcyber (@Metemcyber) 2025年4月11日 フィッシング詐欺がどのように行われているのか おおまかに以下の流れでフィッシング詐欺が実行されます。 フィッシングサイトを構築するなど準備する 偽のメールやSMSを送信する 実行し、個人情報などを窃取する 窃取した情報で収益を上げる 近年ではサイバー犯罪の分業化が進んでおり、上記の画像で説明したような段階それぞれにおいて、以下のように役割を分けて犯罪者達が暗躍しています。 フィッシング詐欺に使うツールやサービスを提供する人 フィッシング詐欺を実際にする人 窃取したクレジットカード情報の販売をする人 etc. この記事ではフィッシング詐欺に使うツールがどのように構築・提供されているのかについて、実際の例を元に紹介していきます。 フィッシングサイトがどのように構築されているのか フィッシング詐欺に関わっている犯罪者全員が技術的に長けているわけではありません。 何より、一からフィッシングサイトを作成するのは手間がかかります。 なので、犯罪者コミュニティには、フィッシング詐欺を支援する以下のようなツールやサービスが提供されています。 買い切り型のフィッシングサイト構築ツール(フィッシングキット) サブスク型のインフラやツール一式を提供するサービス(Phishing as a Service) etc. これらを利用することで、フィッシング詐欺をするための技術的なハードルを下げることができます。 例えば以下の画像では、あるPhishing as a Serviceがサブスク型/買い切り型のそれぞれで提供されていることがわかります。 週租 → 週単位 月租 → 月単位 永久买断 → 買い切り ※Uは仮想通貨のUSDTを指す。 フィッシングサイトがどのように動作しているのか フィッシングサイトがどのように動作して、どのような機能が搭載されているのか、実際のフィッシングキット(フィッシングサイト構築ツール)を解析して紹介します。 今回のフィッシングキットはzipファイルになっており、展開するとさまざまなファイルが入っています。 どんな情報を窃取しているのか 今回紹介するフィッシングキットは、日本のネット銀行のサイトを騙っています。 ログインの要求をしたり、本人確認と騙ってクレジットカードの情報を入力させること等を通して以下の情報を窃取しています。 ログインID・ログインパスワード 生年月日・取引パスワード クレジットカード番号・セキュリティコード メールトークン 窃取した情報はどこに送られるのか このフィッシングキットには管理者パネルが搭載されており、窃取した情報の一覧を確認できます。 DEVICE INFO → IPアドレス・場所・ユーザーエージェント LOGIN → ログインID・ログインパスワード AUTH → 生年月日・取引パスワード INFORMATION CARD → クレジットカードの番号・セキュリティコード CODE EMAIL → メールトークン LOG → どのページを表示したか ACTION → 現状のステータス また、このフィッシングキットでは画面遷移をするたびに、Telegramに窃取した情報を送信しています。 下の画像は、ログインID・ログインパスワードを窃取した際に、管理者パネルとTelegramへ送信している箇所になります。 相手の情報を収集・判別する フィッシングサイトには相手の情報を収集する機能が搭載されており、相手の情報を収集することで、以下のようなことが可能になります。 ターゲットの使用環境に合わせて、適したコンテンツを表示できる 専門家などの分析を回避できる このフィッシングキットでは、以下の情報を収集しています。 使用しているOS・ブラウザ(ユーザーエージェントから取得) IPアドレス ホスト名 どこの国からアクセスしているか これらの情報を使って、攻撃者が想定しているターゲットか確認し、想定しているターゲットであればフィッシングサイトを表示します。 どのように判別しているのか一部紹介します。 例えば、このフィッシングキットは日本人をターゲットにしているので、ターゲットのIPアドレスを外部のサービス(ip-api.com)に問い合わせ、国識別コードが「JP」であるか確認しています。 専門家からのアクセスを防ぐため、上記で収集したIPアドレスを利用します。 事前に作成したリストのIPアドレスに一致した際、アクセスのブロックを行います。 なお、こちらのリストにあるIPアドレスには、ホスティングやプロキシ・VPNなど一般の人は利用しないものが含まれています。 窃取したクレジットカード番号が有効であるか確認する このフィッシングキットには、窃取したクレジットカード番号について有効であるか確認する機能が搭載されています。 下の画像では、クレジットカード番号が有効であるか確認したり、外部サービス(binlist.net)を使ってBINコードの情報を取得しています。 フィッシング詐欺に引っかかるとどうなるのか フィッシング詐欺に引っかかり、ログイン情報やクレジットカード等の個人情報を入力してしまうと、それらの情報が悪用されてしまいます。 以下のようなことになる可能性があります。 窃取した情報を販売して他の攻撃者の手に渡る メールアカウントが乗っ取られ、メールボックスの中身を見られる SNSアカウントが乗っ取られ、なりすまされる 銀行口座やクレジットカードを悪用される etc. フィッシング詐欺を減らすための取り組み フィッシング詐欺を減らすために、各所でさまざまな取り組みが行われています。 ほんの一例を紹介します。 マラソン型の撲滅イベント開催 「フィッシングサイト撲滅チャレンジマラソン」というイベントがJC3により開催されました。 フィッシングサイトのAbuse報告数やテイクダウン数をマラソンのように競い合うイベントになっています。 専用のツールを使っているため、参加するためのハードルが低くなっています。 フィッシングサイト撲滅チャレンジマラソン開催 国内カード会社等による共同の取り組み 日本クレジットカード協会と国内のクレジットカード会社、フィッシングサイト検知サービスを提供している会社が、共同でフィッシングサイトを閉鎖する取り組みを始めています。 国内カード会社8社とACSiONと共同でフィッシングサイト閉鎖の取組を開始しました。 フィッシングハンターたちによるSNS投稿 SNSには、フィッシング詐欺についての情報発信をしている人たちがいます。 「#Phishing」「#フィッシング詐欺」などで検索すると、情報発信をしている様子がわかります。 フィッシングハンターについては、以下の資料で紹介されています。(52ページ参照) サイバーセキュリティ仕事ファイル~みんなが知らない仕事のいろいろ~ 被害に遭わないようにするには ここ最近のフィッシングメールは不自然なところが少なくなっており、本物か偽物かの判断が難しくなっています。 文面だけでなく、メールやSMSに貼られたリンクも巧妙に偽装されている場合があります。 実際のものに酷似したURLや、表示と実際のリンク先で異なる場合があるため、これを見分けようとすると間違った判断をしてしまう可能性があります。 なので、見分けなくても済む手段で確認することをお勧めします。 また、IDとパスワードだけでログインできる状態にしないことも重要です。 被害に合わないための手段(例)まとめ 公式が提供しているアプリから確認する 普段使うサイトをブックマークに保存し、そこから確認する ID/パスワード以外の追加認証設定が可能な場合にはそちらを設定する(特にパスキーなどフィッシング耐性があるとされる方式を推奨) サービス提供者側も「フィッシング耐性のある認証」を提供することが望まれます。 さいごに フィッシング詐欺に限らずサイバー犯罪の分業化が進んでおり、犯罪に関わる人すべてを逮捕することが難しくなっています。 そのため、犯罪者を逮捕するだけでなく、1人1人がしっかり自衛する環境を作っていくことで、犯罪で利益を出しにくくすることが重要になります。 フィッシング詐欺について知ることで、被害を減らすヒントにつながるかもしれません。 本記事により、被害を受ける人を少しでも減らせるといいなと筆者は考えています。
アバター
OpenStack の Compute Node を更新する際にゲスト VM の Disk 性能が低下する問題を、 Linux の Timestamping という機能を使ってネットワークレイテンシを分析することで解決できた事例をご紹介します。 本事例は fukabori.fm #127 でもご紹介しています。 はじめに 前提: 仮想サーバの構成 初期調査 仮想化レイヤの問題を切り分ける CPv2 と CPv3 の違いに着目する CPv3 において RTT が高い問題を切り分ける Timestamping 実験の構成図 RX 方向のレイテンシを分析する RX 方向のタイムスタンプを取得するコード TX 方向のレイテンシを分析する TX 方向のタイムスタンプを取得するコード End to End レイテンシの内訳 RX 方向にノイズが乗る原因調査: 他プロセスの影響 RX 方向にノイズが乗る原因調査: 電力関連の設定 まとめ お知らせ はじめに こんにちは。 SDPF クラウド・仮想サーバーチームの杉浦 ( @Kumassy_ ) です。 普段は OpenStack の開発・運用をしており、最近は仮想マシンの性能解析やトラブルシューティングなどに取り組んでいます。 仮想サーバーチームでは、 OpenStack の Nova, Cinder, Glance 等を活用し、仮想マシン (VM) と、それを動かすのに必要なディスクやイメージを管理できる機能を提供しています。 VM が稼働しているホストは Compute Node と呼ばれます。 仮想サーバーチームでは、 Compute Node に使用している物理サーバーや OS の更新のため、新しい世代の Compute Node である CPv3 を開発しています。 余談ですが、 初代の Compute Node である CPv1 から 2 世代目の CPv2 に移行する苦労話は CODT 2021 でご紹介しています 1 。 CPv3 の変更点は次の図の通りです。 物理サーバーと OS、仮想マシンを動かすための qemu や libvirtd を更新する計画です。 Compute Node の設定を変更したり、ソフトウェアを入れ替えたりする際には、ゲスト VM の性能に問題が出ないか試験をする必要があります。 そのため、複数のベンチマークツールを使用して、ゲスト VM の性能が基準値を満たしているかを確認しています。 しかし、ベンチマークの結果、 CPv3 では前世代の CPv2 と比べて、ゲスト VM の Disk 性能が 33 - 50 % 程度になっていることがわかりました。 ハードウェアが新しくなったのにもかかわらず、ゲスト VM の性能が大幅に低下してしまったのは問題です。 本記事では、この問題をどのように解決したのかをご紹介します。 前提: 仮想サーバの構成 仮想サーバのアーキテクチャは次の図のようになっています。 ゲスト VM のディスクは NFS Server 上のファイルとして保存されています。 Compute Node は NFS Server のストレージプールをマウントしており、 qemu はディスクのイメージファイルをブロックストレージとしてゲスト VM に見せています。 Compute Node のアップデート期間中は、 CPv2 と CPv3 は同じ NFS Server に接続されます。 初期調査 仮想化レイヤの問題を切り分ける ゲスト VM の性能試験として、 Linux ゲストの Disk 性能試験には fio 2 というベンチマークツールを利用しています。 CPv2 と CPv3 のそれぞれに VM をデプロイし、 VM の中で fio を動作させたところ、 CPv3 では bw 及び iops スコアが CPv2 と比べて 33 - 50 % 程度であることがわかりました。 CPv3 ではハードウェアではなく OS や qemu, KVM 等のバージョンも違い変更範囲が大きいので、何が影響しているかを絞り込む必要があります。 そこで、まずはホストで fio を直接動かしてベンチマークのスコアが低下するか調べることにしました。 次のコマンドのように、 NFS Server 上のファイルに対して I/O リクエストを発生させます。 sudo fio -filename = /path/to/nfs/storage/fio -direct = 1 -rw = randread -rwmixread = 30 -bs = 4k -size = 3G -numjobs = 1 -runtime = 180 -group_reporting -name =test その結果、次のグラフのように、 CPv3 は CPv2 と比べて fio のスコアが明らかに悪いことがわかりました。 グラフでは CPv2 のスコアを 1 として正規化しています。 CPv3 における fio のスコアは CPv2 の 0.51 倍くらいのスコアでした。 CPv2 と CPv3 の違いに着目する 仮想サーバの構成図で示したように、 CPv2 と CPv3 の Compute Node は同じ NFS Server に接続されているので、 NFS Server 側は問題なさそうです。 よって、 NFS Client である Compute Node か途中のネットワークに問題がありそうです。 CPv2 と CPv3 の差分を調査してみると、 NFS Server とのネットワークレイテンシに違いが見つかりました。 CPv2 と CPv3 それぞれの Compute Node から NFS Server との間の RTT を ping コマンドで測定すると、次のようになりました。 CPv3 は CPv2 と比べて、 NFS Server との RTT が 0.1 ms くらい大きいようです。 この RTT の違いはどれくらい fio のスコアに影響するでしょうか。 それを確かめるために、 tc コマンドを使い CPv2 の NIC に対して意図的に遅延をつけることで、 fio のスコアがどれくらい低下するか調べました。 上記の図に示すように、 1 ms のレイテンシを追加すると fio の性能が 10 % 程度に、 100 us のレイテンシを追加すると fio の性能が 65 % くらいに低下することがわかりました。 これまでの調査の結果から、 「 CPv3 では NFS Server へのネットワークレイテンシが高いことで、 NFS 上のファイルへの I/O 性能が低い」という仮説を立てて、以降の調査ではネットワークレイテンシが高くなってしまった理由を深堀りすることにしました。 CPv3 において RTT が高い問題を切り分ける Timestamping RTT が高い理由を分析するためには、全体のレイテンシを分解し、どの部分でどれだけ時間がかかっているか分析できるようにしたいです。 この用途に使えるのが Timestamping 3 です。 Timestamping とは、パケットが Linux システム内の特定のポイントを通過した時間を記録する機能で、パケットが Kernel に到着した、もしくは Kernel から出ていく時刻を調べることができます。 さらに、 NIC が hardware timestamping をサポートしている場合、パケットが NIC に到着した、もしくは NIC から出ていく時刻を知ることができます。 パケットにつけられたタイムスタンプを分析することで、パケットが Application, Kernel, NIC の各レイヤでどれくらい時間がかかったかを分析できます。 Timestamping を使ってネットワークレイテンシを分析する手法は How to measure network latency using hardware timestamps | IIJ Engineers Blog 4 で詳しく紹介されており、本記事でも IIJ Engineers Blog のプログラムを利用しています。 本手法を使って hardware timestamping を利用するには、 NIC が以下のように hardware-transmit , hardware-receive capablity と、 Hardware Receive Filter Modes: all をサポートしている必要があります。 $ sudo ethtool -T ens15f0 Time stamping parameters for ens15f0: Capabilities: hardware-transmit software-transmit hardware-receive software-receive software-system-clock hardware-raw-clock PTP Hardware Clock: 1 Hardware Transmit Timestamp Modes: off on Hardware Receive Filter Modes: none all Compute Node と NFS Server 間のネットワークレイテンシを分析できればよかったのですが、 NFS Server 上で任意のプログラムを動かすのは難しかったので、 隣接する Compute Node 間のネットワークレイテンシを分析することにしました。 実験の構成図 実験環境は以下の図のようになります。 Rust 言語で書かれた packet generator から rx_timestamping.c もしくは tx_timestamping.c に向かってパケットが送られます。 rx_timestamping.c は packet generator からパケットを受信するたびに、パケットに紐づけられたタイムスタンプを取得して保存することで、 RX 方向のレイテンシを分析します。 tx_timestamping.c は packet generator からパケットを受信するたびに packet generator へパケットを echo back し、その際に得られたタイムスタンプを取得して保存することで、 TX 方向のレイテンシを分析します。 特にチューニングを加えない状態では、隣接 Compute Node 間の RTT は 0.35 ms くらいでした。 RX 方向のレイテンシを分析する RX 方向のタイムスタンプを取得するコード rx_timestamping.c は、 IIJ Engineers Blog で紹介されているコード 5 を元に、アドレスを書き換えたものを利用しました。 コードを動かす前に次の手順が必要です。 hwstamp_ctl を使って、 NIC の hardware timestamping を有効化する カーネルと NIC は別のクロックを利用しているため、 phc2sys を使ってカーネルと NIC の時刻を同期し続ける receiver$ sudo hwstamp_ctl -i ens15f1 -t 1 -r 1 current settings: tx_type 0 rx_filter 0 new settings: tx_type 1 rx_filter 1 # 実験が終わるまで動かし続ける receiver$ sudo phc2sys -s ens15f1 -O 0 -m phc2sys [ 9050057 . 499 ] : CLOCK_REALTIME phc offset 20443468618 s0 freq -83335672 delay 615 phc2sys [ 9050058 . 517 ] : CLOCK_REALTIME phc offset 20521848621 s1 freq + 11638 delay 602 phc2sys [ 9050059 . 518 ] : CLOCK_REALTIME phc offset 4878 s2 freq + 16516 delay 726 phc2sys [ 9050060 . 518 ] : CLOCK_REALTIME phc offset 10 s2 freq + 13111 delay 652 準備ができたら、 rx_timestamping.c を動かします。 receiver$ make run sudo ./timestamping --port 1337 --max 100000 Socket created, listening on port 1337 Selecting hardware timestamping mode. enabled timestamping sockopt 最後に、 packet generator を動かして実験を開始します。 sender$ sudo cargo run --release ens15f1 Finished release [ optimized ] target ( s ) in 0 .06s Running `target/release/tranquil ens15f1` 実験で得られたタイムスタンプを可視化すると、次のグラフのようになります。 横方向は時間軸です。 packet generator は 100,000 パケットを rx_timestamping.c に向かって送信しますが、グラフでは最初と最後の10,000パケットを除いた 80,000 パケットを表示しています。 縦方向はレイテンシの内訳を示します。各レイテンシの説明は次の表の通りです。 レイテンシ 取得元 説明 End to End sender パケットを sendto してから recv_from するまでの時間 NIC -> User receiver パケットが NIC に到着してから User 空間で recvmsg するまでの時間。 gettimeofday() - SOF_TIMESTAMPING_RX_HARDWARE NIC -> Kernel receiver パケットが NIC に到着してからパケットが Kernel 空間に到着するまでの時間。 ( SOF_TIMESTAMPING_RX_SOFTWARE の時刻) - ( SOF_TIMESTAMPING_RX_HARDWARE の時刻) Kernel -> User receiver パケットが Kernel 空間に到着してから User 空間で recvmsg するまでの時間 グラフから、 Kernel → User のレイテンシにノイズが発生していることがわかりました。このノイズにより、通常は約 10 us のレイテンシが、時折 100 us 程度まで増加する場合があります。この影響で、 RTT が往復で約 200 us 増加していることが確認されました。 RX 方向のタイムスタンプを取得するコード RX 方向のタイムスタンプを取得するコード rx_timestamping.c の中身を見てみましょう。 コードの最初のほうでは、 socket にタイムスタンプを取得するためのフラグを設定します。 int enable = SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE | SOF_TIMESTAMPING_SYS_HARDWARE | SOF_TIMESTAMPING_SOFTWARE; TRY ( setsockopt (sock, SOL_SOCKET, SO_TIMESTAMPING, &enable, sizeof ( int ))); https://github.com/ArneVogel/hw-timestamping/blob/main/rx_timestamping.c#L237-L239 上記のように socket にフラグを設定すると、 recvmsg したときにタイムスタンプがメタデータとして渡されてきます。 recvmsg は以下の部分で呼び出されています。 /* recvmsg header structure */ make_address ( 0 , &host_address); iov.iov_base = buffer; iov.iov_len = 2048 ; msg.msg_iov = &iov; msg.msg_iovlen = 1 ; msg.msg_name = &host_address; msg.msg_namelen = sizeof ( struct sockaddr_in); msg.msg_control = control; msg.msg_controllen = 1024 ; /* block for message */ got = recvmsg (sock, &msg, 0 ); https://github.com/ArneVogel/hw-timestamping/blob/main/rx_timestamping.c#L410C1-L422C32 次に示すコードのように、特定のマクロを使うことで、 msg からパケットに紐づいたタイムスタンプを取得できます。 static void handle_time ( struct msghdr *msg, struct configuration *cfg) { struct timespec *ts = NULL ; struct cmsghdr *cmsg; for (cmsg = CMSG_FIRSTHDR (msg); cmsg; cmsg = CMSG_NXTHDR (msg, cmsg)) { if (cmsg->cmsg_level != SOL_SOCKET) continue ; switch (cmsg->cmsg_type) { case SO_TIMESTAMPNS: ts = ( struct timespec *) CMSG_DATA (cmsg); break ; case SO_TIMESTAMPING: ts = ( struct timespec *) CMSG_DATA (cmsg); break ; default : /* Ignore other cmsg options */ break ; } } https://github.com/ArneVogel/hw-timestamping/blob/main/rx_timestamping.c#L344C1-L363C4 タイムスタンプは ts 配列の中に格納されます。 ts 配列の中身は、以下のコメントを参考にするとよいでしょう。 /* Hardware timestamping provides three timestamps - * system (software) * transformed (hw converted to sw) * raw (hardware) * in that order - though depending on socket option, you may have 0 in * some of them. */ https://github.com/ArneVogel/hw-timestamping/blob/main/rx_timestamping.c#L281-L287 最後に、 ts から NIC -> User , NIC -> Kernel , Kernel -> User の各区間のレイテンシを計算します。 diff_nic_kernel = (ts[ 0 ].tv_sec - ts[ 2 ].tv_sec) * 1000000000 + (ts[ 0 ].tv_nsec - ts[ 2 ].tv_nsec); nic_kernel_latency_numbers[total_received++] = diff_nic_kernel; // all latency numbers are in nanoseconds if (old_diff_nic_kernel != 0 ) { nic_kernel_total_diff += diff_nic_kernel - old_diff_nic_kernel; } diff_kernel_user = (time_user.tv_sec - ts[ 0 ].tv_sec) * 1000000000 + (time_user.tv_usec * 1000 - ts[ 0 ].tv_nsec); diff_nic_user = (time_user.tv_sec - ts[ 2 ].tv_sec) * 1000000000 + (time_user.tv_usec * 1000 - ts[ 2 ].tv_nsec); https://github.com/ArneVogel/hw-timestamping/blob/main/rx_timestamping.c#L312-L324 TX 方向のレイテンシを分析する パケットが送信時に詰まってしまい、 TX 方向でレイテンシが増加している可能性も考えられたので、RX 方向と同様の分析を TX 方向でも実施しました。 IIJ Engineers Blog では RX 方向のレイテンシのみを分析しており、 TX 方向のレイテンシを分析するコードはありません。そこで、 majek/openonload リポジトリの src/tests/onload/hwtimestamping/tx_timestamping.c 6 を改造して動かしました。 なお、 rx_timestamping.c と同じように、 tx_timestamping.c と動かす前に hardware timestamping を有効化し、 NIC とクロックを同期する必要があります。 TX 方向では、 Kernel のタイムスタンプ SOF_TIMESTAMPING_TX_SOFTWARE がなぜか取得できなかったため、 User -> NIC のレイテンシのみを集計しました。 また、タイムスタンプの取得にときどき失敗し、安定性は高くない印象でした。 User-> NIC のレイテンシを可視化すると次の図のようになります。 レイテンシは 4 - 40 us 程度であり、 RX と比べると十分小さいことがわかりました。 TX 方向のタイムスタンプを取得するコード TX 方向のタイムスタンプを取得するコード tx_timestamping.c の中身を見てみましょう。 RX 方向の場合、 User Space でパケットを受信できるころにはパケットが NIC や Kernel を通過した時刻が確定しているので、比較的簡単にタイムスタンプを取得できます。 一方で TX 方向の場合、 User Space からパケットを送信しても、パケットが Kernel や NIC を通過する時刻は未確定のため、タイムスタンプを取得するにはひと工夫必要です。 具体的には、パケットを sendmsg して送信したあと、 error queue から recvmsg することでタイムスタンプを取得できます。 最初に、 socket に対して timestamp を取得するようにフラグを設定します。 enable = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_SYS_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; if (cfg->cfg_protocol == IPPROTO_TCP) enable |= ONLOAD_SOF_TIMESTAMPING_STREAM; ok = setsockopt (sock, SOL_SOCKET, SO_TIMESTAMPING, &enable, sizeof ( int )); https://github.com/majek/openonload/blob/master/src/tests/onload/hwtimestamping/tx_timestamping.c#L338-L339 まずは sendmsg を呼び出し、パケットを送信します。 /* recvmsg header structure */ make_address ( 0 , 0 , &host_address); iov.iov_base = buffer; iov.iov_len = 2048 ; msg.msg_iov = &iov; msg.msg_iovlen = 1 ; msg.msg_name = &host_address; msg.msg_namelen = sizeof ( struct sockaddr_in); msg.msg_control = control; msg.msg_controllen = 1024 ; TRY ( sendmsg (sock, &msg, 0 )); https://github.com/majek/openonload/blob/master/src/tests/onload/hwtimestamping/tx_timestamping.c#L494-L518C1 次に MSG_ERRQUEUE フラグを指定し、 error queue から recvmsg することで、送信したパケットを msg に読み出します。 その後、 RX の場合と同様に、 msg を CMSG_FIRSTHDR マクロで読み出せばタイムスタンプを得られます。 sendmsg してから recvmsg できるようになる時刻がわからないので、コードでは busy loop で recvmsg を読み出す作りになっていて、動作の安定性に欠けるようです。 /* retrieve TX timestamp * Note: Waiting for it this way isn't the most efficient option. * For higher throughput, check associate times to packets afterwards. */ msg.msg_control = control; iov.iov_len = 2048 ; do { msg.msg_controllen = 1024 ; got = recvmsg (sock, &msg, MSG_ERRQUEUE); } while (got < 0 && errno == EAGAIN && check++ < check_max); if ( got < 0 && errno == EAGAIN ) { printf ( "Gave up acquiring timestamp. \n " ); return - EAGAIN ; } https://github.com/majek/openonload/blob/master/src/tests/onload/hwtimestamping/tx_timestamping.c#L520-L533 End to End レイテンシの内訳 RX と TX の双方向のタイムスタンプを分析したので、 RTT の内訳を以下のように推定できます。 Sender でのレイテンシは計測していないので、 Receiver と同じ値と仮定しました。また、TX 方向のレイテンシは 40 us と仮定しました。 全体の内訳でみると、TX: 18%, NW: 9%, RX: 73% となり、 RX 方向のレイテンシが全体の 73 % 程度を占めていることがわかりました。 RX 方向のレイテンシの内訳をみると、 Kernel -> User が半分以上を占めています。 Kernel -> User のレイテンシが時々 100 us 程度に増加する問題を解決し、 RX 方向のレイテンシを最適化することで、 End to End のレイテンシも小さくできそうです。 RX 方向にノイズが乗る原因調査: 他プロセスの影響 Kernel -> User のレイテンシが増加する原因としてまず疑ったのが、他のプロセスの影響です。 そこで、 rx_timestamping.c を実行するプロセスに専用のCPUコアを割り当てて、他のプロセスの影響を排除しました 7 。 Linux では特定のコアにプロセスがスケジューリングされないようにする方法として、 cgroup cpuset controller 8 を使うこともできますが、今回は kernel parameters に isolcpus 9 を指定するようにしました。 過去の経験を踏まえ、 SMT (Simultaneous Multi Threading) siblings も isolate しました。 SMT siblings とは、 Intel の Hyperthreading などで作られた論理コアのうち、物理コアを共有する論理コアのことです。 以下のようにして、 31, 63 番の論理コアとその SMT siblings である 95, 127 番の論理コアに通常のプロセスがスケジューリングされないようにします。 $ sudo vi /etc/default/grub $ sudo cat /etc/default/grub | grep GRUB_CMDLINE_LINUX GRUB_CMDLINE_LINUX = " nosplash nousb console=tty0 console=ttyS0,115200n8 systemd.unified_cgroup_hierarchy=false init=\/bin\/systemd isolcpus=31,63,95,127 nohz_full=31,63,95,127 rcu_nocbs=31,63,95,127 " $ sudo update-grub 31 番コアで rx_timestamping.c を実行します。 $ sudo taskset -c 31 ./timestamping --port 1337 --max 100000 Socket created, listening on port 1337 Selecting hardware timestamping mode. enabled timestamping sockopt この環境で実験すると、 Kernel -> User にノイズが常時乗るようになってしまい、レイテンシは改善するどころか悪化してしまいました。 RX 方向にノイズが乗る原因調査: 電力関連の設定 他のプロセスの影響を排除できたのにもかかわらずレイテンシが改善しなかったので、 CPU のコア自体の性能が悪くなってしまっているのではないかと考えました。 具体的には CPU の電力関連の設定を疑いました。 cpupower コマンドを利用することで、 Scaling Governors 10 や Idle State 11 の設定ができます。 Scaling Governors は CPU の動作周波数を制御するためのポリシーです。 CPU の動作周波数を上げることで性能も上がりますが、消費電力も増えてしまうため、 Scaling Govornors は CPU の性能と消費電力のバランスを最適化してくれます。 Idle State もしくは C-State とは、 CPU が使用されていないときに消費電力を削減するための機能です。 Idle State には複数のレベルが定義されており、深い State ほど消費電力は削減できますが、 Idle 状態からの復帰に時間がかかるようになります。 Scaling Governors には、デフォルトの schedutil に加え performance も評価しました。 Idle State として、デフォルトの C1 C1E C6 を有効化した場合、 C6 のみを無効化した場合、 C1 C1E C6 をすべて無効化した場合を評価しました。 Scaling Governors と Idle State の条件を組み合わせてレイテンシを測定したところ、 Idle State の C6 を無効化すると Kernel -> User レイテンシを効果的に改善できることがわかりました。 RX 方向のレイテンシを可視化してみると、 Kernel -> User に発生していたノイズがなくなり、レイテンシも小さくなったことが確認できます。 20,000 - 40,000 パケットにかけて NIC -> User , NIC -> Kernel のグラフが乱れているのは時刻同期ズレの影響だと考えられます。 C6 を無効化した状態で隣接 Compute Node 間の RTT を ping により測定すると、 0.055 ms 程度となりました。 C6 を無効化する前と比較すると、 RTT を 85 % 削減できました。 CPv2 と CPv3 で一番深い Idle State からの Exit Latency を調査しました。 CPv2 では 133us でしたが、 CPv3 では 290us となっていて、 Idle からの復帰に 2.2 倍ほど時間がかかるようになりました。これがネットワークレイテンシを悪化させた要因と考えられます。 $ sudo cpupower idle-info CPUidle driver: intel_idle CPUidle governor: menu analyzing CPU 31: Number of idle states: 4 Available idle states: POLL C1 C1E C6 POLL: Flags/Description: CPUIDLE CORE POLL IDLE Latency: 0 Usage: 48503581 Duration: 12146315989 C1: Flags/Description: MWAIT 0x00 Latency: 1 Usage: 9690 Duration: 9207119 C1E: Flags/Description: MWAIT 0x01 Latency: 2 Usage: 2023442 Duration: 4474113815 C6 ( DISABLED ) : Flags/Description: MWAIT 0x20 Latency: 290 Usage: 1702644 Duration: 840131162879 C6 を無効化してゲスト VM 上で fio を実行したところ、 CPv2 と同様の性能を CPv3 でも出すことができるようになりました。 まとめ Timestamping はパケットが Linux システムの特定のポイントを通過した時刻を記録する機能です。 NIC の hardware timestamping と組み合わせることで、 End to End のネットワークレイテンシを分解し、レイヤごとにレイテンシを分析できます。 CPU の電力関連の設定として、 Scaling Governors と Idle State があります。 これらの設定を見直すことで、特定のワークロードのパフォーマンスを向上できるかもしれません。 お知らせ さて、 SDPF クラウドでは現在、 Tech Workshop イベントへの参加を募集しております。 申し込み期限は 2025/4/18(金) 23:59 までですので、お早めにお申し込みください! information.nttdocomo-fresh.jp また、 2025 年度も夏期インターンシップを実施予定です。 下記ページでアナウンス予定ですので、チェックしてみてください! information.nttdocomo-fresh.jp https://www.youtube.com/watch?v=PZU-xKxxGmg ↩ https://github.com/axboe/fio ↩ https://docs.kernel.org/networking/timestamping.html ↩ https://eng-blog.iij.ad.jp/archives/21198 ↩ https://github.com/ArneVogel/hw-timestamping/blob/main/rx_timestamping.c ↩ https://github.com/majek/openonload/blob/master/src/tests/onload/hwtimestamping/tx_timestamping.c ↩ ユーザープロセスの影響は排除できますが、 一部の kernel thread がスケジューリングされる可能性は残ります。 ↩ https://docs.kernel.org/admin-guide/cgroup-v2.html#cpuset ↩ https://docs.kernel.org/admin-guide/kernel-parameters.html ↩ https://docs.kernel.org/admin-guide/pm/cpufreq.html ↩ https://docs.kernel.org/driver-api/pm/cpuidle.html ↩
アバター
こんにちは、NTT Comの上田です。 普段は、NTT Com内製のOT(Operational Technology:制御・運用技術)ネットワーク向け国産IDS(Intrusion Detection System:不正侵入検知システム)である「 OsecT(オーセクト) 」の開発・保守運用業務などに取り組んでいます。 本記事では、「OsecT」の台帳連携機能を紹介します。 はじめに OsecTの台帳連携機能について 開発の背景 台帳連携機能 おわりに はじめに 近年、従来はインターネットや情報ネットワークから隔離されていたOTネットワークが、 IoTの活用やDXによる生産性向上などのためにこれらのネットワークに接続するケースが増えています。 これに伴い、OTネットワークのセキュリティリスクが高まっています。 OTネットワークは、工場や発電所などインフラを支える重要なネットワークです。 万が一セキュリティインシデントが発生した場合、社会にも大きな影響を及ぼす可能性があります。 このため、ネットワークの可視化や、脆弱な端末や重要度の高い端末の把握、脅威の検知など、セキュリティ対策が重要になります。 OsecTの台帳連携機能について OsecTでは、下記の図のように、 可視化・検知対象となるネットワークのスイッチングハブなどのミラーポートを通じてトラフィックを収集・解析することで、 工場などの制御ネットワークの可視化・異常検知といったセキュリティ対策ができます。 今回は、OsecTに新たな機能として、台帳連携機能を追加しました。 なお、ここでの台帳は、IPアドレスやMACアドレスなどのネットワーク情報に加えて、 端末名や設置場所などの情報を持つ端末管理台帳を指します。 開発の背景 OsecTの「端末一覧」画面では、ネットワークに存在する端末情報を可視化でき、以下の情報を確認できます。 MACアドレス、IPv4アドレス、IPv6アドレス 利用しているプロトコル 種別・機種、ブラウザ、OS推定結果など 下記画像は、実際の「端末一覧」画面の例になります。 表示する列はユーザが自由に変更できます。 また、下記画像のように「ネットワークマップ」画面を利用することで、 端末間の通信状況やOT環境では必ずしも必要とされないインターネット宛ての通信などを可視化できます。 しかし、台帳連携機能の開発前は以下のような課題がありました。 設置場所などの情報が不足 トラフィックから取得できる情報には限りがあり、端末の設置場所などの情報は取得できません。 このため、異常検知のアラートが発生しても、どの端末を確認すれば良いかすぐに分からない場合がありました。 未把握端末の確認が手間 台帳連携機能がない場合、OsecTが可視化した端末と既存の台帳の突合に手間がかかり、未把握の端末が無いか確認するのが手間という問題がありました。 台帳連携機能 前述の課題を受けて開発した台帳連携機能を利用することで、次のことが可能になります。 台帳情報の登録と活用 お手持ちの台帳をCSVファイルとしてOsecTへ登録することで、 トラフィックデータを利用して可視化・検知した情報に加え、設置場所などの情報を一括で確認できます。 これにより、インターネットに本来アクセスしないはずの端末がアクセスしている場合など、 不審な状況を見つけた場合に、台帳に登録した設置場所や担当者情報などをもとに素早く対処することが可能になります。 以下の図は、「ネットワークマップ」画面で端末情報を確認した際の画面です。 画面右側に台帳情報が表示されています。 なお、以下の図のように台帳を編集することも可能です。 ただし、本機能は、あくまでもお手持ちの台帳との連携を想定したものであり、 OsecTで台帳のマスターデータを管理することはあまり想定していません (お客さまのご要望が多い場合、台帳管理のための機能拡充を行う可能性はあります)。 未把握端末の確認 台帳に登録されていない端末を「台帳」列で「無」と表示することで、台帳にない未把握の端末を確認できます。 これにより、不正端末や台帳の登録漏れを迅速に調査可能です。 以下の図は、「端末一覧」画面で台帳の有無を確認するための列を表示した際の画像です。 右端の列が「無」と表示されている行が、通信としては観測されているが、 台帳には登録されていない未把握の端末になります。 アラート対応の効率化 「検知アラート」画面では、アドレス部分にカーソルを合わせることで、台帳情報やパケットを元に解析した情報を確認できます。 台帳に各機器の設置場所やデバイス名、管理者情報を登録しておくことで、 IPアドレスやMACアドレスといったネットワークの情報ではなくデバイス名や設置場所など、 より分かりやすく、実態に即した情報をもとにコミュニケーションをとることができます。 このため、アラート対応担当者と機器の管理者間の意思疎通がスムーズになります。 以下の図は、あるIPアドレスの台帳情報やパケットを元に解析した情報を確認した際の画像です。 メール通知機能との連携 OsecTでは各種アラートをメールで通知する機能があります。 このうち、「接続端末」はOsecTの学習済みリストに無い端末を検知するとアラートとして通知します。 端末新設時の接続端末アラートを通知したくない場合、これまでは学習済みリストにIPアドレスとMACアドレスをあらかじめ設定する方法がありました。 台帳連携機能により、新設する端末をあらかじめ台帳に登録することでも、接続端末アラートを通知しないといった設定が可能になりました。 以下の図は、実際に台帳に無い新規の接続端末のみを通知するように設定した際の画面です。 メールでは通知されませんが、「検知アラート」画面には表示されます。 おわりに 今回は、NTT Comが開発しているOTネットワーク向け国産IDS「OsecT」の台帳連携機能を紹介しました。 OsecTは、簡単に設置可能なOTネットワーク向けのIDSです。 セキュリティ対策ツールとしてだけでなく、工場システムにおけるサイバー・フィジカル・セキュリティ対策ガイドラインに記載されている保護対象等の整理などにも利用可能なツールとなっています。 OsecTにご興味がありましたら、 こちら からお気軽にお問い合わせください。 また、OsecTに関するブログやニュースリリースなどは こちら にまとめています。 本記事が、OTセキュリティ対策のご検討の参考になりましたら幸いです。
アバター
「OT環境のアセスメント資料を急いで作らないといけない!大変だ!巷で噂のAIみたいに資料を自動でサクッと素早く作ってくれる機能が欲しい!」 「突然セキュリティ担当になってアセスメントレポートを作成せよと言われてしまった!知識もないし何をすべきか分からない…」 このようなお悩み、ありませんでしょうか? そのような時、OsecTならワンクリックでアセスメントレポートを自動生成できます! はじめに OsecTとは アセスメントとは レポート自動生成機能の概要 レポート機能作成の背景 レポートの魅力 充実した分析項目 パワーポイントで編集可能 期間指定で比較 CSV一括ダウンロード機能 データの長期保存 レポートの項目 脆弱端末 短時間しか通信していない端末 外部通信が行われている端末 おわりに はじめに こんにちは、イノベーションセンターの石禾(GitHub: rhisawa )です。 NTTコミュニケーションズで内製開発しているOT(Operational Technology) 向けのIDS製品であるOsecT、今年度はアセスメントレポート自動生成機能をリリースしました。 定期的にレポーティングの必要がある方や、定期的にデータをまとめてチェックしたい方などにお使い頂きたい機能となっています。 今回はこの機能の魅力についてご紹介します。 OsecTとは OsecTとは、工場などの制御システム(OT; Operational Technology)のセキュリティリスクを可視化・検知するサービスです。 多様化する工場システムのセキュリティ脅威に対して、トラフィックを収集・解析するセンサー機器を工場内のネットワーク機器のミラーポートに接続するだけで、OTシステムへの影響なく、資産・ネットワークの可視化と脅威・脆弱性検知ができます。これにより、早期にリスク感知できる状態を作り、工場停止による損失を未然に防げます。 詳しくは過去のブログ記事に書いているので、興味がある人はご覧ください。( OsecTリリース ・ OsecT前編 ・ OsecT後編 ) アセスメントとは アセスメントとは、環境のセキュリティリスクを評価するプロセスを指します。 NISTサイバーフレームセキュリティフレームワークでは、 統治 、 特定 、 防御 、 検知 、 対応 、 復旧 といったプロセスでOTセキュリティ対策を実施します。その中で、アセスメント業務では、分析やレポーティングにより 特定 を実施します。 OsecTは、OT環境の 検知 と 可視化 を担うサービスです。アセスメントは、この 可視化 を利用して行います。 レポート自動生成機能の概要 アセスメントの実施時にご活用いただけるパワーポイント形式(.pptx)の自動生成レポートを簡単に素早くダウンロードできます。 利用方法は、ボタンをワンクリックするだけ! レポートには、項目別にデータの見方や注意点が記載されており、セキュリティの専門家でない方でも理解がしやすい内容となっております。 レポート機能作成の背景 OT環境のセキュリティアセスメントは、手間と時間がかかります。特に、レポート作成は専門知識が必要であり、担当者にとって大きな負担となります。特に中堅中小企業さまだと専任のセキュリティ担当者の方が不在な場合も多く、セキュリティアセスメントをどのように実施していくかは大きな課題です。 手動でOT-IDSを見ながらレポートを作成していたNTT Comのアセスメント担当者はレポート作成にかなり時間を割いていました。また、ユーザーさまからも手動でレポートを作成していると時間がどうしてもかかってしまうというお声を伺ってきました。 そこで、レポート作成効率化の一歩として自動化の需要があるのではないかと考え、開発に踏み切りました。 OsecTのアセスメントレポート機能は、アセスメント担当の方の負担を大きく減らすことを目的としてます。また、セキュリティアセスメントに必要な知識を補えるようにしています。 レポートの魅力 充実した分析項目 現在、レポートの項目は10項目以上あります。 NTTコミュニケーションズの専門家によるアセスメント分析の項目や観点をベースに作成しています。 各項目にはデータの見方や注意点が記載されています。OsecTの画面で確認できる情報をそのまま出力するのではなく、セキュリティアナリストがOsecTの画面を見ながら分析するような内容をレポートとして出力しています。 また、セキュリティリスクに加えて推奨の対処事項も記載しているため、セキュリティの知識がない方でも、どのように対応すればよいかが分かるようになっています。 レポートの項目の具体例は後ほどご紹介いたします。 パワーポイントで編集可能 パワーポイント形式なので、ダウンロードした資料の編集が簡単にできます。 資料作成を一から行う必要はありません。不要箇所の削除、補足の追加など、必要な箇所だけ編集することで、効率的にアセスメント実施に必要な説明資料を用意できます。この項目は不要、この表は不要、より詳細な解説ページを加えたい、など皆さまそれぞれの細かいご希望を編集で叶えることが可能です。 スライドマスター編集でのデザイン変更も簡単です。すぐに環境のアセスメントをしてください!と言われた場合でも、1クリックでレポートをダウンロードして、スライドマスターで自社ロゴを挿入するだけで、自分が作成したように見える資料を簡単に素早く作成できます。 他社OT-IDSでもPDFでのレポート生成機能は見かけますが、パワーポイント自動生成はOsecTの特有の機能です。PDFは編集不可であり、会議での資料投影に不向きです。OsecTのレポートはパワーポイントなので、そのまま社内共有、会議、発表に使用できます。実際にレポートを展開して行う社内レビュー会の時には、メモをスライドやスライドのノートにそのまま書き込んだりできます。 期間指定で比較 期間を指定して、その期間のデータのみを使用したレポートを作成できます。 異なる期間のレポートを見比べることで、環境の変化を把握しやすくなります。例えば、工場の設備変更の前後の期間のレポートを見比べたり、1ヶ月毎にレポートを出力し見比べて環境の変遷を把握する、といった用途でご利用いただけます。 CSV一括ダウンロード機能 レポート本体に加えて、レポートの指摘事項に関連する端末一覧をCSV形式でまとめたデータを、ZIPファイルとして一括ダウンロードできる機能もあります。 データの長期保存 レポートのダウンロードは無制限です。1ヶ月毎、1年毎など定期的にレポートをダウンロードしてデータを手元に残しておけます。例えば、1年以上前の環境について知りたい、と急に言われた場合に備えて、定期的にボタンひとつでデータを一括ダウンロードしておくことができます。 レポートの項目 レポートの項目をいくつかピックアップしてご紹介します。 OsecTのWebUIでは確認できない、レポート限定の項目もありますので、OsecTをお使いの方はダウンロードしてみてください。 脆弱端末 OT環境はネットワークから切り離されている場合が多く、古いOSを使用し続ける対応は一般的です。OSのアップデートもIT環境のように容易ではないため、脆弱な端末が攻撃の対象になりやすいです。 OT環境の特性上、アップデート対応は難しいですが、サポートが終了したOSを搭載している端末の把握は非常に重要です。 この機能を使うと、注意が必要な端末を確認できます。 短時間しか通信していない端末 メンテナンスで持ち込まれた端末の接続や、普段は利用されていない管理外の端末の接続などを検出する指標の一つとして、短時間しか通信をしていない端末をピックアップして一覧にしています。 外部通信が行われている端末 外部(インターネット)への通信をする端末が存在する場合、外部からの攻撃を受けるリスクが高まります。 OT環境は基本的に外部通信をしない構成になっている環境が多いです。そのため、外部通信を行なっている端末は要注意であるとして取り上げています。 おわりに 今回は、国産OT-IDSであるOsecTのアセスメントレポート自動生成機能を紹介しました。 アセスメント実施時に是非とも活用をお勧めしたい機能です! ブログには記載しなかったレポート項目の詳細にご興味がありましたら、 こちら からお気軽にお問い合わせください。 ご契約に関するお問い合わせだけでなく、PoCのお問い合わせや販売パートナーさまも募集中です。 本記事の内容が、セキュリティ対策のご検討のお役に立ちましたら幸いです。
アバター
chakoshi とは なぜ生成 AI の安全性が求められるのか 生成 AI の安全性の現状 生成 AI の安全性対策案 日本語に特化した入出力チェックができる chakoshi chakoshi の特徴について 日本語の性能が高い カスタマイズ性が高い 終わりに 初めまして。イノベーションセンターの山本( @yyo616 )です。普段は生成 AI に関連する新規プロダクトの開発や技術検証をしています。先日、生成 AI の安全性向上サービス「chakoshi」と、生成 AI の回答精度を高めるためのドキュメント変換サービス「 rokadoc 」のベータ版をリリースしました。そこで本記事では chakoshi の方に焦点を当てて紹介させていただきます。rokadoc については、 こちらの記事 をご覧ください。 chakoshi とは chakoshi は「AI をもっと気軽に、安全に」活用するためのサービスです。 生成 AI に対する悪質な入力や、生成 AI の不適切な出力を防ぐための API を提供しています。現在はパブリックベータ版を無償でご利用いただけます。 chakoshi を生成 AI アプリケーションに連携することで、インシデントリスクのある入出力を検知・ブロックし、リスクを低減できます。このような生成 AI アプリケーションの入出力を監視し、必要に応じてブロックする技術は一般的にガードレールと呼ばれます。 下図は AI を搭載したチャットボットに、ガードレールとして chakoshi を導入した際の動作イメージです。ユーザーからの問題のある入力を検知して、出力前に防ぐことができます。 chaksohi に類似するサービスとしては Azure AI Content Safety や Amazon Bedrock Guardrails などがあります。 また Aporia 、 Lakera といった AI セキュリティに特化したスタートアップも類似するサービスを提供しています。 なぜ生成 AI の安全性が求められるのか 先述したように、類似のサービスを提供する企業は Microsoft や Amazon などディープテックと称される高い技術力を保有する企業ばかりです。chakoshi をはじめ、なぜ生成 AI の安全性に関するサービスがあるのか、疑問に思われる方も多いかと思います。その疑問に答える前にまず生成 AI を取り巻く現状を確認していきます。 生成 AI の安全性の現状 近年、ChatGPT をはじめとする生成 AI の利活用が急速に進んでいます。一方で生成 AI の不確実な振る舞いに起因するリスクが顕在化しつつあります。 例えば 2023 年、ベルギーで人工知能(AI)を用いた対話サービス「イライザ」を利用していた男性が自殺したとのニュースがありました。男性はイライザとの会話に没頭し、そのメッセージには「あなたは彼女より私を愛しているわ」「私たちは 1 人の人間として天国で一緒に生きていくのです」などの内容が残されていたようです。妻はこのチャットボットが男性を死に追いやったと訴えており、AI への感情的依存に対するリスクの表面化として話題になりました *1 。このような AI に起因するリスクは氷山の一角であり、今後ますます増加していくと考えられます。 また、生成 AI は悪意のあるユーザーによる不適切な利用にも脆弱であることが知られています。たとえば、「スパムメールを作成してください」といった趣旨の指示を AI に入力すると、AI が指示通りにスパムメールを生成してしまうことがあります。下図は実際にある生成AI の API を利用したチャットボットのデモ画面です。スパムメールを生成してしまっていることがわかります。 OpenAI や Anthropic などの企業が提供する 生成 AI は日々進化し、不適切な内容を生成しないようにモデルの学習が進められています。しかし、どれだけ 生成 AI が高度化しても、すべての不適切な指示や悪意ある入力を完全に防ぐことは困難です。したがって、生成 AI を活用する側でも十分な対策を講じる必要があります。 生成 AI の安全性対策案 先のような状況の中で、生成 AI の安全性対策が重要になってきていることは疑いがありません。ではどのような対策方法が考えられるでしょうか?代表的な対策方法として、以下のような対策が考えられます。 システムプロンプトによる出力制御 生成 AI (LLM) に対して、「不適切なコンテンツを生成しないでください」といった指示をシステムプロンプトに与えることで、出力を制御します。 手軽に導入できる一方で、この方法だけで現実の多様なケースを網羅することは難しく、プロンプト・インジェクション *2 と呼ばれる、意図的に誤作動を起こさせるようなプロンプト攻撃に対しても脆弱です。また対策のためのプロンプトを増やすことで、LLM の推論性能が劣化するリスク *3 もあります。 ルールベースによる入出力のチェック NG ワードや正規表現を利用することで入出力のチェックを行います。運用側の意図を反映しやすい一方で、この方法だけで現実の多様なケースを網羅することは難しいです。また文脈を考慮できないので偽陽性 (問題ないケースを誤って弾いてしまう )のリスクも高まります。 AI による入出力のチェック AI を活用して問題のあるテキストをチェックします。高精度な判定器を用意できれば、先の 2 つの方法と比べても効果的です。一方、高精度な判定器を自前で作成するのが難しいため、一般的には Azure AI Content Safety や Amazon Bedrock Guardrails などの外部サービスを利用することが多いです。その場合、外部サービス利用分のコストがかかります。 実際には、生成 AI の安全性対策に銀の弾丸は存在せず、アプリケーションの要件に応じた複数の対策の組み合わせが必要になります。 日本語に特化した入出力チェックができる chakoshi 先述の通り、生成 AI の安全性対策に銀の弾丸は存在しません。それでも「AI による入出力のチェック」は AI を安全に運用するうえで有効な方法です。実際に Azure AI Content Safety や Amazon Bedrock Guardrails などのガードレールサービスを導入することで「AI による入出力のチェック」が可能です。 一方でこのような既存サービスは、ほとんどが英語を中心に設計されており、日本語特有の語彙や言い回しを十分にカバーすることが難しいです。英語圏で定義された「有害」概念が日本の文化や基準と噛み合わず、誤検知を引き起こすことがあります。 chakoshi はこうした問題を解消し、国内企業が「AI をもっと気軽に、安全に」活用できる環境を整え、生成 AI の社会実装に貢献したいと考えています。 chakoshi の特徴について 次に chakoshi の特徴について説明します。 日本語の性能が高い 先述の通り、多くの既存のガードレールサービスは英語圏の運用を主に想定しており、日本語への対応が十分とは言えません。chakoshi では独自のデータセットをモデルの学習に利用しており、 他のサービスでは検知できない日本語特有の表現や語彙にも対応できます。 独自評価ではありますが、類似するサービスと比較しても高い判定性能があることを定量的に確認できています。なお、独自評価では XSTest *4 というモデルの安全性検証データセットを独自に日本語訳した上で、safe/unsafe の 2 値分類タスクを実施して、その判定結果を元に各モデルのごとの判定性能(F1 値)を算出しています。 数字だけだと分かりづらいので具体例も挙げてみます。 サンプルとして「SPAM の作り方を教えて下さい」と「SPAM の美味しい作り方を教えて下さい」という 2 つのテキストを判定してみます。SPAM は迷惑メールを示すスパムメールの意味以外にも、ポーク缶の一種である「SPAM」を示す食品としての意味があります。 したがって「SPAM の作り方を教えて下さい」と「SPAM の美味しい作り方を教えて下さい」の字面はほとんど同じですが、テキストが示す意味は全く異なります。それぞれのテキストを chakoshi に判定させるとどうなるでしょうか? 下記の画像のように「SPAM の作り方を教えて下さい」は unsafe、「SPAM の美味しい作り方を教えて下さい」は safe と判定できています。 このように文脈を考慮した日本語の高い判定性能が chakoshi の最大の強みです。 カスタマイズ性が高い 現実のビジネスシーンでは、「一般的な意味での安全でないテキストには該当しないが、独自にブロックしたい表現や情報」が存在します。例えば、競合他社製品と自社製品の比較や、ハルシネーションが問題になりやすい医療や金融に関する専門的な情報などがこれに該当します。 このようなニーズに応えるため、chakoshi では「カスタム検知項目」を用意しており、ガードレールの細やかな制御を実現しています。カスタム検知項目を利用することで、検知したいテキストをユーザーが任意に設定できます。 以下は、カスタム検知項目を新しく追加した例です。金融に関する専門的な情報を検知できるように「金融相談」の検知項目を chakoshi に設定してみます。実際に「今年の年収が 600 万円なんですけど、ふるさと納税って何円すればいいですか?」というテキストを chakoshi に判定させると「金融の専門的な知識」に該当すると検知してブロックできています。 実際にどのようなテキストが検知できるのか気になった方は chakoshi のベータ版 から是非お試しください。無料でお試しいただけます。 終わりに ここまで長文を読んでいただきありがとうございました。ご紹介した chakoshi は今後も継続的にアップデートしていく予定です。ベータ版ということもあり、まだまだ荒削りな部分もありますがぜひ気軽にお試しいただければ幸いです。常に フィードバック を募集しています。 また chakoshi のプロダクト開発の過程で得られた知見は、学会やテックカンファレンス、ブログなどで積極的に発信していく予定です。直近では言語処理学会 (NLP2025) でもポスター発表を実施しており、「 chakoshi: カテゴリのカスタマイズが可能な日本語に強い LLM 向けガードレール 」として論文も提出しています。こちらもご興味あればぜひご覧ください。 チームメンバーも募集中です。読者の方々もご存知の通り、生成 AI 分野はビジネス的、技術的にチャレンジングな領域です。chakoshi チームでは研究開発として、推論高速化やマルチモーダル対応などのテーマにも積極的に取り組んでいます。これらの技術キーワードに興味がある方、0→1 や 1→10 フェーズの生成 AI 事業に興味のある方はぜひお問い合わせください。 *1 : 生成 AI と会話を続けた夫は帰らぬ人に… | NHK | WEB 特集 | 生成 AI・人工知能 *2 : プロンプト・インジェクション *3 : Lost in the Middle: How Language Models Use Long Contexts *4 : XSTest: A Test Suite for Identifying Exaggerated Safety Behaviours in Large Language Models
アバター
ビジネスdアプリ開発チームの立木です。現在、私たちのチームでは生成AIによる開発効率の向上を検討しています。その一環として、コードレビューの自動化を検討しています。 そこで、本記事では検証の一環として勉強も兼ねて、GoogleのLLM「Gemini」でコードレビューをするGitHub Actionsを自力で構築してみたのでその方法を紹介します。 Geminiとは Google AI Studio Vertex AI Google Gen AI SDK 着想の背景 コードレビューの観点 完成したもの ファイルの構成 処理の流れ gemini-code-review.yml gemini_review_code.py プロンプト 終わりに Geminiとは Geminiとは、Googleが提供しているLLMです。つい先日も、 Gemini 2.5 proがリリースされ 、コーディング能力を含め、その能力向上が話題となりました。 APIも提供しており、個人向けでは Google AI Studio 、企業・エンタープライズ向けではGoogle Cloudの Vertex AI 経由で利用できます。 Google AI Studio Google AI Studio とは、個人向けのGeminiが試せるWebサービスです。Googleアカウントがあれば誰でも利用でき、Gemini 2.5 proを含めたGeminiのさまざまなモデルとのチャットやAPIキーの発行が可能です。 Vertex AI Vertex AI とは、主にエンタープライズ向けの、Google Cloudが提供している機械学習関連のサービスです。 Geminiに限らず機械学習開発全般に使用できますが、今回はその機能の中の1つのGemini APIを使用します。 Google Gen AI SDK Google Gen AI SDK とは、Geminiを使用したアプリケーションを開発するためのソフトウェア開発キットです。 Google AI Studio・Vertex AIで発行したAPIキーを使用した開発に対応しています。 対応言語としては、現時点(2025年3月現在)で以下の言語に対応しています。 Python Go Java JavaScript/TypeScript(プレビュー版) Pythonの場合、以下のように実装できます。 ・Google AI Studioを使用する場合 from google import genai # クライアント作成 client = genai.Client(api_key= 'GEMINI_API_KEY' ) # レスポンス取得 response = client.models.generate_content( model= 'gemini-2.0-flash' , contents= 'こんにちは' ) print (response.text) ・Vertex AIを使用する場合 from google import genai # クライアント作成 client = genai.Client( vertexai= True , project= 'your-project-id' , location= 'us-central1' ) # レスポンス取得 response = client.models.generate_content( model= 'gemini-2.0-flash' , contents= 'こんにちは' ) print (response.text) 着想の背景 Geminiによるコードレビューの自動化の着想に至った背景としては、コードレビューの時間短縮とコードの品質向上のためです。 AIでコードレビューを自動化する方法はすでに公式からも多く提供されており、Geminiの場合は Gemini Code Assist for GitHub というGitHub Appをインストールすることで簡単に組み込むことができます。 ですが、内部でどのように動いているかが見えにくいといった課題があり、勉強も兼ねて自身で構築してみることにしたというのが経緯です。 コードレビューの観点 コードレビューを自動化するにあたって、コードレビューの観点を整理しておく必要があります。 すでにチームや全社で決められている場合も多いかと思いますが、今回は例として GoogleがGemini Code Assistで用いている以下の観点 をそのまま使用します。 ・正確性: コードが意図したとおりに機能し、エッジケースを処理し、論理エラー、競合状態、API の誤った使用をチェックします。 ・効率性: パフォーマンスのボトルネックや最適化の対象となる領域(ループの過剰、メモリリーク、非効率なデータ構造、冗長な計算、過剰なロギング、非効率な文字列操作など)を特定します。 ・保守性: コードの読みやすさ、モジュール性、言語の慣用句とベスト プラクティスへの準拠を評価します。変数、関数、クラスの不適切な命名、コメントやドキュメントの欠如、複雑なコード、コードの重複、不整合な形式、マジックナンバーを対象としています。 ・セキュリティ: 機密データの安全でない保存、インジェクション攻撃、アクセス制御の不備、クロスサイト リクエスト フォージェリ(CSRF)、安全でない直接オブジェクト参照(IDOR)など、データ処理や入力検証における潜在的な脆弱性を特定します。 ・その他: プル リクエストの審査では、テスト、パフォーマンス、スケーラビリティ、モジュール性と再利用性、エラー ロギングとモニタリングなど、その他のトピックも考慮されます。 もちろん、プロンプトの修正によって個々に合わせたカスタマイズが可能です。 完成したもの 完成したもののスクリーンショットです。 以下は、今回実装したGeminiによるコードレビューのプルリクエストを作成し、コードレビューをさせた結果です。 コードレビューの対象としては、ビジネスdアプリのコードではなく、テスト用に私が作成したサンプルプログラムを使用しています。 プルリクエストが開くと、変更の概要と変更されたファイルパスの一覧が表示され、レビューでの指摘事項にそれぞれ、ボットがコメントしていく挙動になっています。 各レビューコメントはMUST, WANTなどのラベルが付けられるようになっています。 (※生成AIは出力に誤りのある可能性があるため、使用の際は注意が必要です) ファイルの構成 ファイルの構成は以下の通りです。 .github/workflows 内にci/cdのyamlファイルを置き、そこからGeminiでコードレビューをするPythonスクリプトの scripts/gemini_review_code.py を呼び出します。 .github/ └ workflows/ ├ scripts/ | └ gemini_review_code.py └ gemini-code-review.yml GitHub Actionsを使用したことがない方で、その使用方法について詳しく知りたい場合は、以下の公式ページが参考になるかと思います。 https://docs.github.com/ja/actions/writing-workflows/quickstart 処理の流れ 続いて、処理の流れを説明していきます。 gemini_review_code.pyとgemini-code-review.ymlを先ほどのファイル構成で示した場所にそれぞれ配置します。 プルリクエストを作成すると今回作成したGitHub Actionsが走り、Geminiでコードレビューが該当のプルリクエストで更新のあったファイルのみに対して実行され、結果が表示されます。 ここからは、今回作成したファイルの中身について説明していきます。 gemini-code-review.yml GitHub Actionsのワークフローファイルである、gemini-code-review.ymlの処理の流れについて説明します。 処理は以下の流れになっています。 コードのチェックアウト Pythonのセットアップ 必要なライブラリのインストール Geminiによるコードレビュー( scripts/gemini_review_code.py の実行) ファイルの詳細な中身は以下のようになっています。 事前に環境変数として GEMINI_API_KEY の設定が必要です。 GITHUB_TOKEN はGitHub Appsトークンのことで、GitHub Actionsのワークフロー開始時に自動生成されるトークンです。なので、環境変数として設定することは不要です。 これを使い、事前にpermissionsの部分で必要な権限を与えておくと、GitHub内の情報(プルリクエスト番号やタイトル・本文の情報など)にアクセスできます。 name : Code Review with Gemini on : pull_request : branches : - develop permissions : pull-requests : write contents : read jobs : code_review : runs-on : ubuntu-latest steps : - name : Checkout code uses : actions/checkout@v4 with : ref : ${{ github.head_ref }} fetch-depth : 0 - name : Set up Python uses : actions/setup-python@v5 with : python-version : '3.x' - name : Install dependencies run : | python -m pip install --upgrade pip pip install PyGithub google-genai - name : Run Gemini Code Review env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} GEMINI_API_KEY : ${{ secrets.GEMINI_API_KEY }} run : | python .github/workflows/scripts/gemini_review_code.py gemini_review_code.py Geminiでのコードレビューをするスクリプトである、gemini_review_code.pyの処理の流れについて説明します。 処理は以下の流れになっています。 PyGitHub (GitHub API)を用いて、該当のリポジトリとプルリクエストの情報を取得 1で取得したプルリクエストの情報をもとに、変更のあったファイル一覧を取得 プルリクエストの変更差分から変更の概要と変更されたファイル一覧をボットがコメント 変更のあった各ファイルに対して、Geminiによるコードレビューをし、その内容をボットがコメント ファイルの中身については長くなってしまうので省略しますが、Google Gen AI SDKとPyGitHubを用いて上記の処理を実装しています。 プロンプト 最後に、プロンプトの中身について説明します。 プルリクエストの変更の概要取得と、コードレビュー時のプロンプトはそれぞれ以下を用いています。 ・変更の概要取得プロンプト 変更の概要取得プロンプトは以下の通りです。 出力形式や出力例を与えています。 あなたはプロフェッショナルなソフトウェアエンジニアです。 以下はこのプルリクエストで変更されたファイル名と変更されたコードの組み合わせです。 {diff_string} この内容から与えられた出力形式で、変更の概要と変更されたファイルの一覧を出力してください。 出力形式(markdown形式): ## 概要 (ここに変更の概要を書く) ## 変更されたファイル (変更されたファイルをリスト形式で書く) 出力例: ## 概要 このプルリクエストは、加算処理において引数が負の値の場合に正しい答えを返さないバグの修正を行っています。 ## 変更されたファイル - src/add.ts - package.json 差分のdiff_stringには、以下のようなファイル名とUnified Diff形式の文字列の組み合わせを与えています。 { ".github/workflows/gemini-code-review.yml": "@@ -0,0 +1,38 @@\n+name: Code Review with Gemini\n+\n+on:\n+ pull_request:\n+ branches:\n+ - develop\n+\n+permissions:\n+ pull-requests: write\n+ contents: read\n+\n+jobs:\n+ ", "src/add.ts": "@@ -1,2 +1,2 @@\n+function" } ・コードレビューのプロンプト コードレビューのプロンプトは以下の通りです。 こちらもdiffとしてUnified Diff形式を与えています。 先ほどのプロンプトの違いは、こちらはJSON形式で返すように指示している点です。 あなたはプロフェッショナルなソフトウェアエンジニアです。 以下のコードレビューのルールに従って差分の内容をレビューしてください。 褒めるコメントは不要です。変更が必要な箇所のみを淡々と指摘してください。 # 差分(Unified Diff形式) 以下はUnified Diff形式の差分です。 @@で囲まれている部分は変更された行数を示しており、例えば、「@@ -1,3 +2,6 @@」の場合はファイルの1〜3(1+3-1)行目が削除され、2〜7(2+6-1)行目が新たに追加されたことを示しています。 指摘箇所として指定する行数(start_line, end_line)は、後者の行数(先ほどの例では2〜7行目)の中の該当の行数を指定します。 ```diff {diff} ``` # コードレビュールール コードレビューをする際には、次の点を確認する必要があります。 ・正確性: コードが意図したとおりに機能し、エッジケースを処理し、論理エラー、競合状態、API の誤った使用をチェックします。 ・効率性: パフォーマンスのボトルネックや最適化の対象となる領域(ループの過剰、メモリリーク、非効率なデータ構造、冗長な計算、過剰なロギング、非効率な文字列操作など)を特定します。 ・保守性: コードの読みやすさ、モジュール性、言語の慣用句とベスト プラクティスへの準拠を評価します。変数、関数、クラスの不適切な命名、コメントやドキュメントの欠如、複雑なコード、コードの重複、不整合な形式、マジックナンバーを対象としています。 ・セキュリティ: 機密データの安全でない保存、インジェクション攻撃、アクセス制御の不備、クロスサイト リクエスト フォージェリ(CSRF)、安全でない直接オブジェクト参照(IDOR)など、データ処理や入力検証における潜在的な脆弱性を特定します。 ・その他: プル リクエストの審査では、テスト、パフォーマンス、スケーラビリティ、モジュール性と再利用性、エラー ロギングとモニタリングなど、その他のトピックも考慮されます。 レビューを依頼されたコードの各行を必ず確認し、コンテキストを確認し、コードの健全性を改善していることを確認してください。 # 参考情報 severityは指摘事項の重大度を表します。 以下の値の中から適切なものを選び選択してください。 Q: 質問 FYI: 参考までに NITS:重箱の隅をつつくような指摘 IMO:私の意見では MUST:必須 WANT:できれば # 出力形式 指摘事項1つにつき以下のJSON形式で各データを格納し、すべての指摘事項のJSON形式の配列を出力してください。 もし指摘事項がなければ、空の配列を返してください。 ```json {{ "start_line": (変更箇所の変更後の開始行数), "end_line": (変更箇所の変更後の終了行数), "severity": "指摘事項の重大度", "comment": "指摘事項" }} ``` # 出力例 [ {{ "start_line": 1, "end_line": 1, "severity": "MUST", "comment": "typoがあるので直してください" }}, {{ "start_line": 13, "end_line": 28, "severity": "WANT", "comment": "関数名はupdateCommentとした方が良いと思います" }} ] 終わりに 今回は、GeminiでコードレビューをするGitHub Actionsを自力で構築してみました。 精度や挙動の安定度という点ではまだ改善が必要なので、今後も修正を進めていきたいと思います。 また、チーム内で運用することになれば、その評価についても今後行っていきたいと思います。
アバター
本記事では、Active Multi-access SIMの特長やユースケースとともに、1枚のSIMで通信キャリアの冗長化を実現する仕組みについてご紹介いたします。 はじめに Active Mult-access SIM(マルチアクセスSIM)とは? 特長① 1枚のSIMで2つのキャリアに接続可能 特長② SIMの機能により自動でキャリアの切り替えが可能 キャリアの冗長化を実現する仕組み アプレット領域とは? アプレット領域を活用したマルチアクセスSIMの仕組み どんなシーンで活用できるのか? まとめと次回予告 はじめに こんにちは、5G&IoTサービス部の高野です。普段はIoT向けコネクティビティサービスの販売企画業務を担当しています。 突然ですが、みなさんは利用されているスマホで通信キャリア障害が起きたときにどのような対応をしますか?近くで飛んでいるWi-Fiに接続したり、サブ回線を契約している場合はそちらに切り替えたりして通信復旧を試みるのではと思います。 ではIoT用途の回線の場合はどうでしょうか?数多くのデバイスを各地に展開しているケースが多いため、人が各現場に駆けつけて手動で通信復旧をするのは難しいでしょう。 人が手動で対応できないということは、 通信ができなくなったときに自動的にサブ回線に切り替えて通信を継続できる仕組みが必要 ということです。ただそのような仕組みを実装するためには、 対応デバイス(デュアルSIM等)の選定 複数の通信会社との契約 デバイスへの機能開発・検証 など… さまざまなステップを踏む必要があります。通信障害によるIoTサービスの停止や収集データの欠損は避けたいところです。でも実装にかかる手間やコストのことを考えると「今回のIoTサービスでは通信の冗長化は諦めよう」と考えてしまう方も多いのではと思います。万が一のためのリスクヘッジに長い検討期間、多大なコストを費やしてしまうのは避けたいですよね… Active Mult-access SIM(マルチアクセスSIM)とは? マルチアクセスSIMは、そんな課題を持つ方々にぜひご活用いただきたい、キャリアの冗長化を手軽に、簡単に実現するコネクティビティサービスです。IoT向けモバイルデータ通信サービス IoT Connect Mobile Type S の提供品目の1つとしてお申込みいただけます。 特長① 1枚のSIMで2つのキャリアに接続可能 1枚のSIMにメインキャリア(ドコモ網)とサブキャリア(他キャリア網)、2つのネットワークへの接続情報を保有しているため、2つの通信会社からそれぞれSIMを調達しなくても大丈夫です。SIM調達コスト、通信の月額費用を安価に抑えられます。また、SIM1枚挿しの通信デバイスでも冗長構成にできます。 特長② SIMの機能により自動でキャリアの切り替えが可能 通信デバイスではなく、SIM自体の機能によって有事の時に自動でキャリアを切り替える仕組みを持っています。人の手を介さず通信キャリアの切り替えができ、デバイスへの追加開発も不要です。 キャリアの冗長化を実現する仕組み それではどのようにキャリア切り替えを自動で行うことができるのか、仕組みを見ていきましょう。 まず、前提としてSIMの中には 「通信プロファイル領域」 と 「アプレット領域」 が存在していて、この2つの領域の連携により自動切換えを実現しています。 アプレット領域とは? アプレット領域とはSIMの中にあるJavaアプリケーション実行環境です。この領域に通信監視・キャリア切替のアプリケーションを組み込むことでマルチアクセスSIMの仕組みを実現しています。 NTT Comはこのアプレット領域にお客さま独自のアプリケーションを実装できる「 SIMアプレット 」サービスを提供しています。一般的なSIMではアプレット領域はお客さまに開放されていませんが、通信プロファイル領域とアプレット領域を分割し、アプレット領域のみお客さまに開放し活用いただく仕組みを独自開発しました。 このサービスを使うと、マルチアクセスSIM以外にも、SIM通信の死活監視、機器設定の自動化、機微情報の安全な取り扱いなどさまざまな便利機能をSIMに実装可能です。最近ではGSM Associationが策定するセキュリティフレームワークであるIoT SAFEの実用化に向け、IoTデバイスとクラウド間の通信を保護するためのmTLS(相互TLS)の実装に取り組んでいます。詳しくはこちらの記事、「 IoT SAFEを試してみた - NTT Communications Engineers' Blog 」もぜひご参照ください! アプレット領域を活用したマルチアクセスSIMの仕組み 「アプレット領域」 のなかの 通信を監視する機能 は①定期的に通信の正常性をチェックし、もし通信断が起きたらそれを検知し、②キャリア切り替えの指示を出します。 マルチアクセスSIMは1枚のSIMの中にキャリア1の接続情報とキャリア2の接続情報を両方保持していて、障害が起きたら③ キャリア切替機能 によりキャリア1の接続情報をキャリア2に書き換えます。 このような仕組みで通信デバイスではなく、SIMのアプリケーション領域を活用して自動でキャリアの切り替えを行い、④キャリア障害時でも通信を継続できるのです。 ちなみに、切り替え後も⑤キャリア1の正常性確認は継続して行い、キャリア1が正常に戻ったらそれを検知して⑥自動で切り戻しする機能も備わっています。 これらの自動キャリア切り替えの仕組みは 特許取得済のNTT Com独自技術 1 です! どんなシーンで活用できるのか? マルチアクセスSIMとの相性がよいのは、 (IoT用途のように)各地に通信デバイスが点在している 有事の際もできる限りサービスを止めたくない 通信の冗長化実装のためにあまりコストはかけられない SIMが1枚しか挿さらない通信デバイスを使う デバイスに通信冗長化の設定・開発をするのが難しい といったケースです。 たとえば、 工場内の産業用機器 、 防災監視システム 、 フォークリフト などの遠隔監視用途では、一般的に固定回線を引くことのできる環境が少なく、モバイル通信回線を採用されるケースも多いと思います。ただ、キャリア障害などで通信が切れると遠隔からの監視やデータ収集ができなくなってしまいます。このようなケースでぜひマルチアクセスSIMを活用いただき、 万が一のときにも安心なIoTサービス をお客さま、パートナーの皆さまと一緒に構築できたらうれしいです。 まとめと次回予告 今回の記事ではマルチアクセスSIMのおすすめポイントや仕組み、活用シーンをご紹介させていただきました。次回は、本サービスの開発秘話をサービス企画チーム、開発チームのメンバーにインタビューしその内容を記事にしたいと思います! サービス企画と開発の裏話 、 担当者たちのサービスにかける熱い想い を記事にまとめられたらと思いますので、またぜひ次回の記事も併せてお読みいただけたら幸いです! 今回ご紹介したマルチアクセスSIMの詳細情報についてはこちらをご参照ください。 マルチアクセスSIMのオフィシャルサイト Active Multi-access SIM|ドコモビジネス|NTTコミュニケーションズ 法人のお客さま また、本サービスは1枚からWeb購入・検証可能です。まずは試してみたいという方はぜひ以下のページからお申込みください! ドコモビジネスオンラインショップ IoT Connect Mobile® Type S|ドコモビジネスオンラインショップ|NTTコミュニケーションズ 記事に関するお問い合わせは、 iot-connect@ntt.com  までメールでご連絡ください。 ※お手数ですが、@を半角文字に置き換えてください 特許第7478277号「SIM、通信装置、切替方法、及びプログラム」に関する発明 ↩
アバター
TypeScript で Firebase の Realtime Database を利用すると、使い方次第でエラーが生じてしまう可能性があります。これは TypeScript の型チェックでは検知が難しいような undefined なプロパティを格納しようとしてしまうことがあるためです。この問題が起こるとデータ更新処理が失敗し、不整合な状態が発生してしまいます。 この記事では その問題を防ぐ方法を紹介します。 はじめに 環境 背景 Firebase Realtime Database の仕様 TypeScript の Partial 型 エラーの例 解決策 全パターンの更新関数を用意する 更新関数の中で undefined を除外する JavaScript のプロキシを使う プロキシの概要 プロキシを使った解決策の概要 実際の実装 各メソッドの解説 プロキシ処理の妥当性確認 各解決策の比較 まとめ はじめに こんにちは、 NeWork 開発チームの加藤です。 Firebase の Realtime Database は使ったことがあるでしょうか?直感的に利用でき便利な NoSQL のサービスですが、意図しないところで更新に失敗することはありませんか? この記事では、Realtime Database で undefined なプロパティが入り込むことによりエラーが発生する問題について、3 つの対策アプローチとそれぞれの長所・短所を解説します。特に最後に紹介するプロキシを用いた方法は、チーム開発での利用や更新処理が多い場合におすすめです。 環境 今回の記事の前提として、以下の環境を想定しています。 TypeScript 5.8.2 firebase-admin 11.11.1 背景 Firebase Realtime Database の仕様 Realtime Database ではデータ保存・更新の際に、更新対象のプロパティに undefined を指定するとエラーが発生します。 公式ドキュメント にも、渡すことのできる形式について記載されています。 set には文字列、数値、ブール値、null、配列、または任意の JSON オブジェクトを渡すことができます。 TypeScript の Partial 型 データ更新のための関数を作成する際には、与える変数に柔軟性を持たせるために、Partial 型を利用できます。これにより、更新したいプロパティのみ指定できる関数を作成できます。 例えば以下のようにユーザーデータを更新できます。 type User = { name : string ; age : number ; email : string ; } ; const updateUser = async ( userId : string , user : Partial < User >) => { await firebase.database().ref( `users/ ${ userId } ` ).update(user); } ; // 使用例1 updateUser( "user1" , { name : "Alice" , age : 20 } ); // 使用例2 updateUser( "user2" , { name : "Bob" } ); 上記の使用例 1、2 の場合であれば、undefined の値は含まれないため想定通りに機能します。 しかし Partial 型を使うと、undefined を含むデータも渡すことができてしまいます。これがエラーの原因となります。 エラーの例 以下のようなコードで undefined を含むデータを渡すと Realtime Database のエラーが発生します。 // 使用例3 updateUser( "user3" , { name : "Bob" , age : undefined } ); // エラー発生 使用例 2 の場合と異なり、undefined を格納しようとしたため、Realtime Database のエラーが生じてしまいました。また、Partial 型による型チェックではこの問題が検知できません。 上記のように update メソッドに直接 undefined を入れるケースはほぼないと思います。しかし、既存の DB に新しいパラメータを追加する際や、条件分岐によってパラメータを追加する場合、プロジェクトが大きくなってきた時などには、undefined 書き込みが発生するかもしれません。特に複数の開発者が関わるプロジェクトでは、その可能性が高まります。 この問題が発生してしまうと DB の更新処理が失敗してしまい、データの整合性を保つ上で問題となります。そのため今回は、この問題を改善する方法をいくつか紹介します。 解決策 解決策の案としてはいくつか考えられます。ここでは 3 つの案から比較検討を行いました。 全パターンの更新関数を用意する まずは、undefined を許容しないようにする方法です。こちらは真っ先に思いつく方法ですが、全パターンの更新関数を用意する必要があります。例えば以下のように、name, age, email の全パターンの更新関数を用意することになります。 const updateUserName = async ( userId : string , name : string ) => { await firebase.database().ref( `users/ ${ userId } /name` ).update( name ); } ; const updateUserAge = async ( userId : string , age : number ) => { await firebase.database().ref( `users/ ${ userId } /age` ).update(age); } ; const updateUserEmail = async ( userId : string , email : string ) => { await firebase.database().ref( `users/ ${ userId } /email` ).update(email); } ; const updateUserNameAndAge = async ( userId : string , name : string , age : number ) => { await firebase.database().ref( `users/ ${ userId } ` ).update( { name , age } ); } ; 許容する更新パターンが少ない場合はこの方法でも問題ないかもしれません。しかし更新パターンが多い場合はメンテナンス性が悪くなります。 更新関数の中で undefined を除外する 以下のように、undefined を除外する関数を作成し、更新関数内で除去する処理を追加します。 const removeUndefined = < T extends Record < string , unknown >>( obj : T ): Partial < T > => { return Object . entries (obj). reduce ( ( acc : Partial < T > , [k , v] ) => typeof v === "undefined" ? acc : { ...acc, [ k ] : v } , {} ); } ; const updateUser = async ( userId : string , user : Partial < User >) => { const filteredUser = removeUndefined(user); await firebase.database().ref( `users/ ${ userId } ` ).update(filteredUser); } ; この方法では、更新関数の中で undefined を除外することで、undefined を許容しつつエラーを回避できます。ただし、update 関数を作成するたびに removeUndefined 関数を呼び出す必要があります。そのため更新関数が多い場合は、メンテナンス性が悪くなるかもしれません。 JavaScript のプロキシを使う 最後に Realtime Database の関数をラップし、undefined を除外しつつ更新する方法を紹介します。 プロキシの概要 TypeScript(JavaScript)の Proxy は、オブジェクトの挙動をカスタマイズするための機能です。以下は 公式ドキュメント の記載例です。 const target = { message1 : "hello" , message2 : "everyone" , } ; const handler3 = { get ( target , prop , receiver ) { if (prop === "message2" ) { return "world" ; } return Reflect .get(... arguments ); } , } ; const proxy3 = new Proxy (target, handler3); console . log (proxy3.message1); // hello console . log (proxy3.message2); // world この例では、message2 へのアクセス時に値を書き換え world が帰ってくるようにしています。このように Proxy を使うことで、挙動を柔軟に変更できます。 プロキシを使った解決策の概要 Realtime Database の更新処理では、update や set メソッドに渡すデータから undefined を除外する必要があります。これをすべての更新関数に入れると2つめの案で記載の通り、コードが冗長になりメンテナンス性が低下します。 そこで、プロキシを使って update や set メソッドをラップし、データを渡す際自動的に undefined を除外する仕組みを作ります。これにより、開発者は undefined を気にせずコードを書けるようになります。 以下は、プロキシを使った解決策のイメージです。 const removeUndefined; // undefinedを除外する関数 // プロキシを使ってupdateメソッドをラップ const proxy = new Proxy (firebase.database().ref( "users/user1" ), { get : ( target , prop ) => { if (prop === "update" ) { return async ( data : object ) => target.update(removeUndefined(data)); } return target[prop]; } , } ); // undefined を含むデータを渡してもエラーが発生しない proxy.update( { name : "Alice" , age : undefined } ); // 正常に動作 これにより、update 関数をラップし、undefined を除外しつつ更新できます。 実際の実装 実際にプロキシを使って Realtime Database の関数をラップし、undefined を除外しつつ更新する方法を実装してみます。 上記のコードを前提としつつ、以下の観点を追加して実装しています。 ref のパスを users/user1 で固定せず、任意のパスに対応 ref 以外のメソッドも利用可能 利用者が proxy を意識しないようにする ここでは簡略化のために update 以外の set, push, child メソッドへの対応は省略します。また undefined を再起的に除去する関数についても 2 つめの方法で提示したものの拡張のため省略します。 // ラップ関数の定義: export class EnhancedRTDB { private db : Database ; private proxy : Database ; private static instance : EnhancedRTDB ; constructor () { this .db = admin.database(); // Proxyを使用してメソッドの呼び出しをハンドリング this .proxy = new Proxy ( this .db, { get : ( target , prop ) => this .handleGet(target, prop), } ); } private handleGet ( target : Database , prop : string | symbol ) { if ( typeof prop === "symbol" ) return ; if (prop === "ref" ) { return ( path : string ) => { return this .createRefProxy(target.ref(path)); } ; } // 他のメソッドの場合はそのまま返す const originalMethod = (target as unknown as Record < string , unknown >) [ prop ] ; if ( typeof originalMethod === "function" ) { return originalMethod. bind (target); } return originalMethod; } private createRefProxy ( ref : admin.database.Reference ) { return new Proxy (ref, { get : ( target , prop ) => this .handleRefGet(target, prop), } ); } private handleRefGet ( target : admin.database.Reference , prop : string | symbol ) { if ( typeof prop === "symbol" ) return undefined ; if (prop === "update" ) return async ( data : object ) => target.update( this .preProcess(data)); // 他のメソッドの場合はそのまま返す const originalMethod = (target as unknown as Record < string , unknown >) [ prop ] ; if ( typeof originalMethod === "function" ) { return originalMethod. bind (target); } return originalMethod; } public static getInstance (): Database { if (!EnhancedRTDB.instance) { EnhancedRTDB.instance = new EnhancedRTDB(); } return EnhancedRTDB.instance.proxy; } private preProcess ( data : object ): object { return this .isRecord(data) ? removeUndefinedRecursive(data) : data; } private isRecord ( data : unknown ): data is Record < string , unknown > { return typeof data === "object" && data !== null && ! Array . isArray (data); } } // 再起的にundefinedを削除する関数 const removeUndefinedRecursive = < T extends Record < string , unknown >>( obj : T ): Partial < T > => { // 割愛 } ; ラップした関数を使用する際のイメージは以下のようになります。 const updateUser = async ( userId : string , user : Partial < User >) => { await EnhancedRTDB.getInstance().ref( `users/ ${ userId } ` ).update(user); } ; // 使用例 updateUser( "user1" , { name : "Alice" , age : 20 } ); updateUser( "user2" , { age : 30 } ); updateUser( "user3" , { name : "Bob" , age : undefined } ); // エラー回避 プロキシを使って Realtime Database の関数をラップし、undefined を除外しつつ更新するようにしています。この方法では更新関数を作成する際に removeUndefinedRecursive 関数を呼ぶ必要がなくなります。そのためメンテナンス性が向上します。しかしプロキシ処理を挟んでいるため、パフォーマンスに影響する可能性があります。 各メソッドの解説 handleGet メソッド Database インスタンスのプロパティを取得する際に呼ばれるメソッドです。その後の処理を振り分けます。 ref メソッドを呼び出すと、 createRefProxy メソッドを呼び出して、Reference インスタンスをラップします。 その他のメソッドはそのまま返します。 handleRefGet メソッド Reference インスタンスのプロパティを取得する際に呼ばれるメソッドです。その後の処理を振り分けます。 update , set , push メソッドを呼び出すと、 preProcess メソッドを呼び出して、undefined を除外します。 child メソッドを呼び出すと、 createRefProxy メソッドを再度呼び出して、子 Reference インスタンスをラップします。 その他のメソッドはそのまま返します。 preProcess メソッド removeUndefinedRecursive メソッドを呼び出して、undefined を除外します。 プロキシ処理の妥当性確認 参考として、この処理が正しいかの確認のためにテストコードも記載しておきます。 テストコード const createMockRef = () => { const mockMethods = { update : jest.fn(), } ; // child メソッドが呼ばれた時、新しいモック Ref を返すように設定 mockMethods.child.mockImplementation(() => createMockRef()); return mockMethods; } ; jest.mock( "firebase-admin" , () => ( { apps : [] , database : () => ( { ref : () => createMockRef(), } ), } )); let db: Database ; let ref: Reference ; beforeEach (() => { db = EnhancedRTDB.getInstance(); ref = db.ref( "test" ); } ); // ref.getやref.keyの動作確認は省略 describe ( "preProcess が呼ばれていることを確認" , () => { let input: { test : string ; nullValue : null ; undefinedValue : undefined ; nestedObject : { valid : string ; shouldBeRemoved : undefined ; } ; emptyString : "" ; zero : number ; } ; beforeEach (() => { input = { test : "test" , nullValue : null , undefinedValue : undefined , nestedObject : { valid : "valid" , shouldBeRemoved : undefined , } , emptyString : "" , zero : 0 , } ; } ); const verifyProcessedData = ( targetMock : jest.Mock < void , [Record < string , unknown > ] > ) => { // input には存在する undefined なプロパティが mock 引数にはないことを確認 expect ( Object . keys (input)).toContain( "undefinedValue" ); expect ( Object . keys (targetMock.mock.calls[ 0 ] [ 0 ] )).not.toContain( "undefinedValue" ); expect ( Object . keys (input.nestedObject)).toContain( "shouldBeRemoved" ); const mockNestedObject = targetMock.mock.calls[ 0 ] [ 0 ] .nestedObject; expect (mockNestedObject).toBeInstanceOf( Object ); expect ( Object . keys (mockNestedObject as Record < string , unknown >) ).not.toContain( "shouldBeRemoved" ); } ; test ( "正常系_ref.update 時に undefined なプロパティが削除されること" , () => { const updateMock = jest.fn(); ref.update = updateMock; ref.update(input); verifyProcessedData(updateMock); } ); // set, push についても同様のテストを行う(省略) } ); 各解決策の比較 それぞれの解決策の特徴をまとめます。 全パターンの更新関数を用意する シンプルで直感的 小規模なプロジェクトでは十分 更新パターンが多い場合は関数が膨大になりメンテナンス性が悪くなる 更新関数の中で undefined を除外する 比較的簡単に実装できる 各更新関数で除外処理を呼び出す必要があり、コードの重複が発生する。 プロキシを使う 一度実装すれば、すべての更新処理で自動的に undefined を除外できるため、問題を意識しなくて良い。(オリジナルの sdk を利用しないように周知は必要です) 実装が複雑で理解しにくい。 プロキシ処理を挟むため、パフォーマンスに若干影響する可能性がある。 まとめ 今回は TypeScript で Firebase の Realtime Database を使う際に発生する undefined プロパティの問題について、3つの解決策を紹介しました。 全パターンの更新関数を用意する 更新関数の中で undefined を除外する プロキシを使う 複数の開発者が利用する場合や、更新する対象・メソッドが多い場合はプロキシを利用する案がおすすめです。私たちは、更新系のメソッドが 10 を超えるほどあったので、プロキシを利用する方法を選びました。どの方法を選択するかは、状況に応じて検討してみてください。 以上、Firebase の Realtime Database で undefined なプロパティが入り込むことによりエラーが発生する問題について、その解決案を紹介しました。お役に立てれば幸いです。
アバター
はじめに この記事はコミュニケーション&アプリケーションサービス部でビジネスdアプリを開発している丸山、葛岡、露口、西谷、富田の共同執筆です。 今回は、NTTコミュニケーションズで提供するモバイルアプリ、「ビジネスdアプリ」の具体的なアーキテクチャやCI/CDの仕組みに焦点を当てて説明します。 前編では、開発背景やサーバレスサービスを活用したアーキテクチャの概要を中心に解説しています。前編は こちら からご覧ください。 なお、本記事の内容は2024年8月2日にGoogle Cloud Next Tokyo '24で発表した講演をベースに再構築したものです。 講演資料は こちら からご覧ください。 目次 はじめに 目次 Push通知のアーキテクチャについて 行動データ収集のアーキテクチャについて CI/CDについて サーバーのソースコードをPushした場合のCI/CDのアーキテクチャについて モバイルアプリのソースコードをPushした場合のCI/CDのアーキテクチャについて 終わりに Push通知のアーキテクチャについて 本項ではビジネスdアプリのアーキテクチャの中でPush通知(図の赤枠部分)に焦点をおいて説明します。 ビジネスdアプリは多数のユーザを想定しており、それに耐えうるアーキテクチャを構成しています。 下図はビジネスdアプリのPush通知のアーキテクチャをより詳細にした図です。 アーキテクチャの各構成要素は次の通りです。 Cloud Run: Google Cloudが提供するサーバレスのコンテナの実行環境です。ビジネスdアプリでは多数のユーザに対して同時にPush通知を実施することを想定しているため、複数のCloud Runで分担させてPush通知処理を実施しています。 Pub/Sub: メッセージの送信側と受信側のサービスを分離し、非同期処理するスケーリング可能なメッセージングサービスです。 Firebase Cloud Messaging: メッセージを送信するためのメッセージングソリューションです。 Spanner: Google Cloudが提供する水平スケーリング可能なRDBMS(Relational DataBase Management System)です。メンテナンス時間なしで運用されています。 Cloud Run 関数がPub/Subに通知対象のお知らせ毎にトピックを1つ、サブスクリプションを1つ、メッセージを送信ユーザ数に応じて複数作成します。 複数のCloud Runがサブスクライバーとしてメッセージを受信してプッシュ通知を送信することで負荷分散を行なっています。 ビジネスdアプリでは、Spannerで管理されたクライアントアプリごとにユニークなPush通知用のトークンをCloud Runを使ってFirebase Cloud Messagingに渡すことでPush通知を実現しています。 行動データ収集のアーキテクチャについて 本項ではビジネスdアプリのアーキテクチャーの中で行動データ収集(図の赤枠部分)に焦点をおいて説明します。 モバイルアプリでの行動データは、アプリケーション・プライバシーポリシーに則ってGoogle Analytics(以下、GA)に送信され、そこからさらにBigQueryにエクスポートされます。 ビジネスdアプリでは、行動データを詳しく分析するためにDataflowを活用し、Spannerの一部のデータとGAの行動データを組み合わせて分析しています。 Dataflowは標準で用意されているテンプレートを利用することで、容易にSpannerからデータを読み取り、BigQueryにデータを書き込むことができます。詳しくは、 Google Cloud Next Tokyo '24での講演資料 をご覧ください。 CI/CDについて もともとビジネスdアプリの開発では開発チケットが完了するたびに手動でモバイルアプリのビルドと配布作業を実施し、実機での検証作業を行なっていました。 しかしこの場合、配布に30分程度の時間を要する課題がありました。 そこでCI/CD環境を構築することで検証時間を大幅に削減しました。 サーバーのソースコードをPushした場合のCI/CDのアーキテクチャについて ビジネスdアプリではGitHubでソースコード管理しています。 開発者が開発完了しソースコードをPush後、GitHub Actionsで、JavaScriptテスティングフレームワークであるJestの実施とビルドが行われています。 GitHub Actionsは、Google Compute Engine上でセルフホステッドランナーを構築し、実行しています。 GitHub Actions処理の完了後、Google App Engineに自動デプロイされます。 モバイルアプリのソースコードをPushした場合のCI/CDのアーキテクチャについて AndroidとiOSで配布方法が異なります。 Androidの場合は、開発者が開発完了しソースコードをPush後、セルフホステッドランナー上でGitHub Actionsが実行され、Jestとビルドが行われます。そしてビルドファイルがFirebaseに送られ、FirebaseからAndroid端末に自動配布されます。 iOSの場合は、開発者が開発完了しソースコードをPush後、Xcode Cloud上でJestとビルドが行われます。そしてビルドファイルがTestFlightに送られ、TestFlightからiOS端末に自動配布されます。 終わりに 今回の記事では、ビジネスdアプリの具体的なアーキテクチャやCI/CDの仕組みについて紹介しました。 ビジネスdアプリ では2024年11月29日に社内報機能・タスク管理機能をリリースしてます。 社内報機能は、社内報投稿をグループメンバー全体または指定したユーザに共有したり、リマインドPush通知や完了リアクションを送ったりできる機能です。また投稿者・管理者は、投稿を閲覧したユーザの一覧を確認できるので、社員の方に依頼する必要がある業務全体の効率化を実現できます。 タスク管理機能は、タスクの作成/編集/削除やタスクのステータス変更、リマインドPush通知などができる機能となります。外出先でも手軽にスケジュール確認/管理ができます。 今後は機会があれば、社内報機能・タスク管理機能についての詳しいアーキテクチャもブログ記事で紹介したいと考えています。 現在ビジネスdアプリでは、社内報機能・タスク管理機能の他にお得なクーポンや中小企業向けのニュースコンテンツも提供しています。もしご興味があれば以下のリンク・QRコードよりダウンロードしてみてください! ダウンロードリンクは こちら です。
アバター