1 of 22

IoTプラットフォーム開発におけるRustの活用

Rust.Tokyo 2023 (2023-10-21)

Takumi Shimada (@garasubo)

2 of 22

自己紹介

  • 名前: Takumi Shimada
  • GitHub: garasubo
  • Rustは2018年ごろから使用開始
    • 趣味で組込みOSを書いたり、各種コンテストに出たりなど
  • 現在はIdein株式会社に所属
    • RustなどでIoTプラットフォームを開発している会社

3 of 22

目次

2017年からIdein社内でどのようにRustが活用を紹介

  • Actcastの概要
  • Rustの変遷とプロダクトへの影響
  • 現実に起きた問題と対処方法

4 of 22

Actcastの概要

5 of 22

Actcast

6 of 22

Actcastとは

  • エッジAIデバイス(Raspberry Pi)を管理するためのプラットフォーム
  • エッジデバイス側のファームウェア(エージェント)が指定されたAIアプリを動かす
  • Web UI経由でエッジデバイスの状態確認や設定変更を行う

7 of 22

アーキテクチャ概観

8 of 22

アーキテクチャ概観

Rustで書かれたclientプログラム

一部がRust製

Rustで書かれたAPI Server

9 of 22

Rustの変遷とプロダクトへの影響

10 of 22

Rust導入の経緯

デバイス側ファームウェアの実装言語としてRustを導入(2017年)

  • ランタイムが不要で高速・軽量
  • 強力な型システム
    • ファームウェアのバグでデバイスが応答不能になると問題
  • マルチスレッド処理が安全に書ける
    • ネットワーク処理・OSコマンドは並行実行したい

など

その後、APIサーバーなどのプラットフォーム側の実装でも利用

11 of 22

Rustの変遷

2017年から現在に至るまでRustとそのエコシステムは大きく変わった

  • async/awaitの登場
  • failureからanyhow/thiserrorによるエラー処理
  • サードパーティーライブラリの変遷

12 of 22

async/awaitの導入

ファームウェアではネットワーク通信・OSコマンドの実行を並行して行うので非同期処理が必須

  • 最初のリリース時点(2020年1月)ではスレッドを組み合わせて非同期処理を実現
    • async/awaitが正式に利用可能になった直後のタイミングだった
  • ネットワーク処理やOSコマンド実行を徐々にasync/awaitに置き換え
    • ライブラリ側が対応している場合はライブラリの更新で対応
    • そうでない場合はblockingなどのクレートを使い、別スレッド処理にする

13 of 22

async/await対応の難しさ

もともとasync/awaitを使うことを前提に書かれていないコードが多いので対応が難しい

  • 関数をasync/awaitにするとasync/awaitが伝搬していき書き換え量が大きくなる
  • 部分的な置き換えをしていたため、その過程でtokio/futures/blockingなど非同期ラインタイムが入り混じりカオス化
  • tokioランタイムを前提とした関数をtokioランタイム外で呼び出して実行時にエラーになる

などなど

14 of 22

failureからanyhow/thiserrorに

  • エラー型の実装には当時主流だったfailureを用いていた
  • その後anyhow/thiserrorが登場し、failureはメンテナンス中止に
  • RustのstdのErrorトレイトも改善が進み、使い勝手がよくなった
    • sentryのようなエラー報告ツールが利用しやすくなる
    • ボイラープレートが減った

15 of 22

failureからanyhow/thiserrorに

failureで定義した巨大なエラー型つくり広いコンポーネントで使っていた

  • 共通のエラー型を定義していたためバリアントが50以上に
  • failure型を前提とした多くのエラー処理が実装されていた
  • 一度に全部置き換えは難しいので部分的に置き換えていく
    • 新しいエラー型には旧エラー型への変換処理を実装することで変更を最小限に

16 of 22

サードパーティーライブラリの変遷

  • rusotoからaws-sdkに変遷
    • 非公式ライブラリから公式ライブラリへ
    • 未だベータ版であるので過信は出来ない
  • その他メンテナンスが中止になったり破壊的変更が入るライブラリが少なくない
    • actixのメンテナが当然レポジトリの内容を全消去した

17 of 22

現実に起きた問題と対処方法

18 of 22

Rustでもメモリーリークはおきる

Rustにおけるメモリー安全性はメモリーリークは保証できていない

例:std::mem::forgetは明示的にメモリーリークを起こすがsafeである

例:std::rc::Rcで循環参照をつくると解放されない領域ができる

例:joinしないthreadは生き残り続けるかもしれない

19 of 22

リークが起きた例

  • wait関数は無限ループでsleepするスレッドをつくる
  • wait2関数はすぐに終了するスレッドをつくる
  • tokio::selectでwaitかwait2のうちどちらかが終了するのを待つ、というのを繰り返す
  • wait2はすぐに終了して次のループに入るが、wait関数の呼び出しによってつくられたスレッドは生き残り続ける
  • ループごとに終了しないスレッドが増加していく

20 of 22

対策

Rustはバグが少ないと言っても過信は禁物

  • テストの充実化
    • ユニットテスト、結合テスト、テスト環境での長期間のテストなど
  • 各種メトリクスの監視
    • CPU使用率、メモリ使用率
    • ログを横断的に分析して本番環境の問題を早期発見
  • 定期的なリファクタリング
    • 複雑なコードは問題を起こしやすい

他の言語でも有効な対策はRustでも大事

21 of 22

開発の工夫

  • CI/CDの時間が長くなりがち
    • ビルドキャッシュの適切な設定(sccacheやSwatinem/rust-cache)
    • cargo build –timingsで出力したレポートをみて重くなる原因を特定
    • cargo nextestでテストケースの実行時間やflaky testを検知する
  • stdと同じ名前の構造体が多く混同しやすい
    • 例: std::sync::Mutexとtokio::sync::Mutex
    • 可能な限りフルパスで書くようにしたり、renameして実体をわかりやすくする

22 of 22

まとめ

Rustを用いた開発の歴史と困難とそれに対する工夫を紹介

  • Rustは2017年から大きく進歩し、プロダクトも影響を受けた
  • 他のプログラミング言語同様、テストやプロダクトの監視は大事
  • Rustで苦しんだ点もあるが、上手に活用すれば非常に便利