UDP×低トラフィックIoT機器でOpenTelemetryのE2Eトレーシングに挑戦した
はじめに
IoTシステムの可観測性を向上させたい。そう思ってOpenTelemetryでトレーシングの技術検証を行いました。
ただ、今回対象としたのは普通のWebサービスではなく、TCPコネクションすら張れない、超低トラフィック・低負荷のIoT機器です。
この記事では、そんな制約ある環境でE2Eのトレーシングを実現するために挑戦した(そして一度失敗した)結果と、最終的に辿り着いた「タイムスロット方式」について共有します。
この分野に強いわけではないので、是非ディスカッションをしてみたいと思い、書きました。
対象となるIoT機器の制約
今回対象としたIoT機器は、宅内LANに繋がるものから、山間部などから基地局を通じて繋がるものまで様々です。最終的にクラウドで集約して処理するわけですが、こういった機器のソフトウェアには厳しい制約があります。
必須条件
- 低トラフィックであること(通信速度は100bps〜10kbps程度)
- 低負荷であること(バッテリー駆動のため処理負荷を最小限に)
この二つの条件を満たすために、TCPの3ウェイハンドシェイクのコストや再送制御のオーバーヘッドが大きな課題になります。そのため、必然的にUDPを用いたシステムが対象になるわけです。
システムアーキテクチャの全体像
UDPを使いながらも機器との双方向通信を行うため、機器とクラウド側の双方にUDPクライアントとUDPサーバーを持つ設計にしています。
ここでのポイントは、UDP部分とTCP部分が混在していることです。UDPとTCPの境界となるサーバーが必要で、今回はこれらを以下のように呼ぶことにします:
- Ingressサーバー:上り通信(機器→クラウド)の境界
- Egressサーバー:下り通信(クラウド→機器)の境界

TCP部分にはOpenTelemetry Collectorをホストしてトレースを集約し、Jaegerで可視化できます。しかし、UDP部分にどうやってトレースを紐づけるかが大きな課題でした。
最初のアプローチ:TraceParentをUDPペイロードに埋め込む
まずはOpenTelemetryの標準的な手法に倣って、以下のような設計を考えました。
1. IngressサーバーでTraceParent IDを生成
2. UDPペイロードにTraceParent IDを含める
3. IoT機器が受信して、そのままEgressに返送
4. Egress以降のマイクロサービスでトレースを継続

k8sクラスタ上でOpenTelemetry CollectorとJaegerのホスト、マイクロサービスの計装を行い、実際にデモ構築してみました。その結果、Jaegerでトレースの可視化ができることを確認できました🎉
受け取ったUDPリクエストをAWS Kinesisで処理しているのですが、その処理時間やDBへの書き込み時間も可視化できています。

設計から間違っていた
冷静に考えると、この設計には見落としがありました。いや、考えずに実装して、IoT機器側のメンバーから教えていただいて、初めて気がつきました...。
問題1:通信量の増加
OpenTelemetryのTraceParentは、テキスト形式だと約55バイト、バイナリ形式でも24バイトのデータになります。
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
例えば100bpsの通信だと、ヘッダーを送るだけで約4.4秒も回線を占有してしまいます。これでは実用になりません。
問題2:処理負荷の増加
IoT機器では、バッテリーに残っているエネルギーをどれくらいの速さで使い切るかで動作時間が決まります。TraceParentのパースを毎回の通信で実行するのは、バッテリー駆動のIoT機器には重すぎる処理です。
タイムスロット方式で解決を試みる
試行錯誤の末、現時点で筋が良さそうだと思っているのがタイムスロット方式です。ある期間の通信を、同一トレースとみなします。
具体的には、以下のようなルールでTrace IDを生成します:
trace_id = SHA256(device_id + floor(timestamp / 1e+9))
この方式により、同一デバイスの1秒間のスロット内で発生した通信は、すべて同じTrace IDを持つことになります。
メリット:
- UDPペイロードを一切編集しない
- IoT機器側の処理が不要
- 通信量の増加なし
- サーバー側だけでトレースIDを再構築可能
デメリット:
- 厳密な因果関係の追跡はできない
- 同一スロット内の複数通信が混在する可能性がある
ただし、IoTシステムの通信頻度は比較的低いため、実運用上は十分に有用だと判断しました。
実装
OpenTelemetry Collectorの設定で、タイムスロット方式を実現できます。以下のような設定を記述するだけです:
processors:
transform:
trace_statements:
- context: span
statements:
- set(attributes["minute_slot"], Int(UnixNano(start_time) / 1000000000))
- set(trace_id.string, Substring(SHA256(Concat([attributes["device_id"], String(attributes["minute_slot"])], "")), 0, 32))
この設定により、デバイスIDとタイムスタンプからTrace IDを動的に生成できます。
ハッシュ化に加えて、Substringを追加しています。これによってTraceIDの桁調節をしています。
デモリポジトリ
タイムスロット方式の部分飲み、実際に動作するデモを用意しました:
https://github.com/Melonps/otel-trace-grouping-demo
直接的な伝搬を行わずともトレースの関連付けができることを確認できます。
おわりに
まだまだ試行錯誤も、実践も足りない状態なので、正直外に出さない方がいいんじゃないかと思ってました。ただ、IoTシステムの可観測性向上は、まだまだ発展途上の領域だと感じています。みなさんと一緒に盛り上げられたら嬉しいです。
同じことをCloud Naitive Days Winter 2025のLT枠 でお話ししました。
Discussion