TECH PLAY

アプトポッド

アプトポッド の技術ブログ

252

マニュアル等のドキュメント制作を担当している私は、世の中の機械翻訳エンジンの進歩を日々驚きながら観察しています。 実際に業務で使うかは別途判断するとして、「この文をこの機械翻訳エンジンに与えたら、どんな訳文が得られるのだろうか」と、試してみることもあります。 この記事では、そんなお試しの例として、Google Cloud Translation APIの「用語集」の機能を使ってみます。 aptpod Advent Calender 2020 の18日目、テクニカルライターの篠崎が担当します。 (なお、この記事で扱うのは、プログラムから利用する機械翻訳ウェブAPI Google Cloud Translation API です。ウェブブラウザーから利用する機械翻訳サービス Google Translate ではありません。) この記事について Google Cloud Translationの用語集機能とは 試してみる 準備 お試しPythonコード 得られた翻訳結果 おわりに 参考文献 この記事について この記事は、軽く「試してみる」だけの内容です。機械翻訳一般やGoogle Cloud Translation APIの性能や有用性を検証するためのものではありません。 2020年12月に試してみた結果に基づいています。翻訳エンジンは日々更新されると思われます。また、用語集機能もアップデートされるかもしれません。 Google Cloud Translation APIで翻訳を実行するには料金がかかります。また、以下に挙げるコードではGoogle Cloud Storageのバケットに用語集を保存しますが、それに対しても料金が発生します。 日本語から英語への翻訳を例とします。 あくまで1つの例としてご覧ください。 Google Cloud Translationの用語集機能とは Google Cloud Translation APIには、 用語集(Glossary)の機能 が存在します。 対訳形式の用語集をあらかじめ登録しておくことで、指定された訳語を訳文の中で使用してもらうことができます。 対訳用語集の例 Google Cloud Translation APIの公式ドキュメント では、用語集を使用する場合の例として、「商品名」、「あいまいな単語」(意味が全く違うのに同じつづりの語)、「借用語」が挙げられています。 実際に用語集を使用するとどのような翻訳結果が得られるのか、試してみました。 試してみる 以下では、必要な準備を行ったうえで、実際に用語集を使って機械翻訳をしてみます。 準備 手元のPCからGoogle Cloud Translation APIを使用するには、一般に、以下の準備が必要です(Google Cloud Platformのアカウントは取得済みとします)。 これらについては、 公式ドキュメント に詳しく説明されています。 Google Cloudコンソール(以下「GCPコンソール」)で新しいプロジェクトを作る。 GCPコンソールで、プロジェクトのCloud Translation API機能を有効にする。 GCPコンソールで、APIを使用するためのアカウントである「サービスアカウント」を作り、適切な権限を設定する。 GCPコンソールで、サービスアカウントのキー(JSONファイル)を作成し、手元のPCにダウンロードする。 サービスアカウントのキーのファイルを、環境変数 GOOGLE_APPLICATION_CREDENTIALS で指定する。 クライアントライブラリー(Python用ならば google-cloud-translate )をインストールする。 Cloud SDK をインストールする。 また、この記事で紹介する「お試しPythonコード」を実行するには、以下の準備も必要です。 使用するサービスアカウントには「Cloud Translation API 編集者」( roles/cloudtranslate.editor )と、「Storage Object管理者」( roles/storage.objectAdmin )のロールを持たせる。 サービスアカウントに設定されたロール 用語集のアップロード先としてGoogle Cloud Storage(GCS)バケットを作っておく。 GCSバケットを作っておく お試しPythonコード 上記の準備をしたうえで、実際に翻訳を試してみます。 このコードには、用語集データのアップロードから後片付けまでの手順が入っています(エラー処理は省略しています)。また、用語集とテスト用の原文もコード内に入っています。 from google.cloud import storage from google.cloud import translate_v3 as translate # 1. 各種設定 # 日英対訳用語集(公式ドキュメントにある「単一言語ペアの用語集」の形式で作成) # ここでは、簡単のためにCSV形式の1つの文字列として用意しました。 GLOSSARY = ''' \ "メールアドレス","email address" "健司","Takeshi" "国立","Kunitachi" "試験","test" "選ぶ","select" ''' # テスト翻訳対象の原文 SOURCE_TEXTS = [ 'メールアドレスを入力してください。' , 'メールアドレス設定画面' , '健司さんは国立の病院に行きます。' , '学校で試験があります。' , '3つの選択肢から選ぶことができます。' , '彼が選ぶものはいつも同じです。' , '3つの選択肢から選ばなければならなないため、Aを選んだ。' , '3つの選択肢から選ぶよう指示されたため、Aを選ぶことにした。' ] # GCPのプロジェクト情報(プロジェクトは事前に作成済みとする) PROJECT_ID = '<プロジェクトID>' # ロケーションはいまのところus-central1のみ対応とのこと LOCATION = 'us-central1' # 用語集CSVのアップロード先バケット名(バケットは事前に作成済みとする) BUCKET_NAME = '<GCSバケット名>' # バケットに用語集を保存するときのオブジェクト名 GLOSSARY_OBJECT_NAME = 'test_ja_en_glossary.csv' # 上記により、バケット内のCSVファイルオブジェクトへのURIが決まる。 GLOSSARY_URI = 'gs://' + BUCKET_NAME + '/' + GLOSSARY_OBJECT_NAME # 新しく作成する用語集のID GLOSSARY_ID = 'test_ja_en_glossary' # 2. 用語集をアップロードして、GSCバケット内にCSVファイルとして保存する # GCSクライアントを作成する storage_client = storage.Client() # 用語集のアップロード先バケットと、新たに作るオブジェクト名を指定 bucket = storage_client.bucket(BUCKET_NAME) glossary_blob = bucket.blob(GLOSSARY_OBJECT_NAME) # 用語集CSVを文字列として送信し、GCS上のオブジェクトにする glossary_blob.upload_from_string(GLOSSARY) # 3. GCSにアップロードされたCSVをもとにして、 # Google Cloud Translate用の「用語集リソース」を作る # Google Cloud Translate v3 APIのクライアントを作成 trans_client = translate.TranslationServiceClient() # 用語集リソースのパスを決める glossary_name = trans_client.glossary_path( PROJECT_ID, LOCATION, GLOSSARY_ID) # ソース言語とターゲット言語のペアを指定する language_pair = translate.types.Glossary.LanguageCodePair( source_language_code= 'ja' , target_language_code= 'en' ) # 先ほどGCSにアップロードした用語集CSVを指定する gcs_source = translate.types.GcsSource(input_uri=GLOSSARY_URI) input_config = translate.types.GlossaryInputConfig(gcs_source=gcs_source) # 上記で準備した情報を組み合わせて、用語集の情報を準備する glossary = translate.types.Glossary( name=glossary_name, language_pair=language_pair, input_config=input_config) project_location_path = trans_client.location_path(PROJECT_ID, LOCATION) # 用語集リソースを作成する glossary_create_operation = trans_client.create_glossary( parent=project_location_path, glossary=glossary) # 用語集リソースの作成完了を待つ glossary_create_result = glossary_create_operation.result(timeout= 90 ) # 4. 翻訳してみる # 使用する用語集リソースを指定する glossary_config = translate.types.TranslateTextGlossaryConfig( glossary=glossary_name) # 翻訳対象、言語名、用語集リソース等を指定して翻訳する response = trans_client.translate_text( SOURCE_TEXTS, 'en' , project_location_path, source_language_code= 'ja' , glossary_config=glossary_config) # 結果を出力する for (source, trans, trans_with_glossary) in zip ( SOURCE_TEXTS, response.translations, response.glossary_translations): print ( '原文: ' + source) print ( '用語集なし: ' + trans.translated_text) print ( '用語集あり: ' + trans_with_glossary.translated_text) print () # 5. 後片付け # 用語集リソースを削除する glossary_delete_operation = trans_client.delete_glossary(name=glossary_name) # 用語集リソースの削除完了を待つ glossary_delete_result = glossary_delete_operation.result(timeout= 90 ) # GCS内のCSVファイルを削除する glossary_blob.delete() 得られた翻訳結果 以下のような結果が得られました。 原文: メールアドレスを入力してください。 用語集なし: Please enter your e-mail address. 用語集あり: Please enter your email address. ↑ 用語集で指定したとおり、ハイフンなしの"email address"となりました。 原文: メールアドレス設定画面 用語集なし: Email address setting screen 用語集あり: email address setting screen ↑ 文書内でタイトルとして使うことを想定した名詞句です。 先頭は大文字がよいのですが、用語集に入れたハイフンなしの"email address"に置換され、 小文字になってしまいました。 なお、「メールアドレス」という1つの原語に対して、"email address"と"Email address"のような2種類の訳語を登録することはできません("Can't create glossary. There are conflicting entries."というエラーになります)。 原文: 健司さんは国立の病院に行きます。 用語集なし: Kenji goes to a national hospital. 用語集あり: Takeshi goes to a Kunitachi hospital. ↑ 「たけし」さんと「くにたち」の話であることがあらかじめ分かっているときは、このような置換は効果を発揮しそうです。 ただし、"hospital in Kunitachi"のようにはならず、単純な置き換えになっているようです。 原文: 学校で試験があります。 用語集なし: I have an exam at school. 用語集あり: I have an test at school. ↑ ここでは、単純に"exam"が"test"に置換された結果、"a test"ではなく"an test"となってしまいました。 原文: 3つの選択肢から選ぶことができます。 用語集なし: You can choose from three options. 用語集あり: You can select from three options. ↑ 動詞の例です。「選ぶ」が"select"に置換されました。(このような一般的な動詞は用語集に入れないほうが良いと思いますが、試しにやってみました。) 原文: 彼が選ぶものはいつも同じです。 用語集なし: The one he chooses is always the same. 用語集あり: The one he select is always the same. ↑ 単純に"select"に置換された結果、"he selects"ではなく、"he select"となってしまいました。 原文: 3つの選択肢から選ばなければならなないため、Aを選んだ。 用語集なし: I chose A because I had to choose from three options. 用語集あり: I chose A because I had to choose from three options. ↑ 「選ぶ」を用語集に入れましたが、活用形である「選ば」、「選ん」は用語集に入れていないので置換されないようです。 原文: 3つの選択肢から選ぶよう指示されたため、Aを選ぶことにした。 用語集なし: I was instructed to choose from three options, so I decided to choose A. 用語集あり: I was instructed to select from three options, so I decided to select A. ↑ 「選ぶ」に完全に一致する文字列(連体形)の場合は置換されました。 おわりに 用語集の効果がどのようなものか、軽く試してみることができました。 用語集の項目の立て方を工夫して、他のケースも試してみたいですが、 上記のようなコードを用意しておけば、用語集のアップロードから、翻訳結果取得、用語集削除まで、いっぺんにできるので便利です。 なお、Cloud Translation APIには、対訳コーパスを使ってカスタム翻訳モデルをトレーニングし、 それを使って翻訳を行う AutoML Translation という機能もあります。 こちらも、いつか試してみたいところです。 参考文献 Google Cloud Translation公式ドキュメント 特に、 用語集の作成と使用(高度な機能) Python Client for Google Cloud Translation公式ドキュメント Improving Machine Translation with the Google Translation API Advanced (Translation APIの使用法がよく分かるチュートリアルです。) GoogleのCloud Translation API v3を触ってみる (つまづきがちな、サービスアカウントのロール(役割)の設定についても解説されています。)
Advent Calendar 2020 17 日目を担当します、 SRE チームの川又です。 SRE チームでは自社開発プロダクトである intdash のサーバサイドインフラにおいて、主に以下の職務を行なっています。 設計・構築・運用 可用性・パフォーマンスの向上のための改善 セキュリティの維持 今回は、エッジロケーションにおけるセキュリティを維持したパフォーマンス向上のために、サイト間VPN 接続を用いて弊社 intdash のトラフィックフローを効率化した話をご紹介します。 はじめに 課題 解決策 エッジ側vRouter の構成 結果 おわりに はじめに 弊社では年に1 回程度、サーキットを1日貸し切って、自社プロダクトを用いた車両走行・データ計測を行う イベント を行います。 本イベントは以下を目的としています。 自社プロダクトに触れる (ドッグフーディング) デモ用データ取得 自社新規プロダクトの検証 通算何回か行なっている本イベントですが、 現地サーキットの通信環境の悪さを原因とする課題 を抱えていました。 この記事では、ネットワーク技術でその課題の解決を試みた話をご紹介します。 ※ この実験を行ったのは昨年度です。今年度は、主に感染症対策のため本イベントは実施しておりません。 課題 例年先述のイベントを行なっているサーキットはモバイル網の通信速度が遅く、さらには、サーキット内にモバイル網がエリア外の敷地もありました。 したがって、インターネット通信を行える帯域が小さく 満足にクラウド側とデータ通信ができない課題 がありました。 弊社の intdash Server は多様なデータパイプラインを備えており、車両等の計測対象からアップストリームされるデータをリアルタイムに取得することも可能です。また、同じく弊社のWebベースダッシュボードアプリケーション Visual M2M Data Visualizer を用いることでデータの多彩な可視化が行えます。 イベント当日は、サーキットに居る弊社社員のほぼ全員が Visual M2M Data Visualizer で車両計測データの確認を行うことが予測されました。ですが、先述の通りデータ通信帯域が限られているため 何らかの対策 を行う必要がありました。 本ネットワーク環境の概要図を以下に示します。 ネットワーク環境概要図 何らかの対策を実施しなければ、下の図の様にクライアントにあたる各人の Visual M2M Data Visualizer がそれぞれインターネットを通してクラウド上の intdash Server と通信を行い、帯域を逼迫させてしまいます。 問題点のイメージ図 解決策 この課題を解決するために、エッジロケーションにあたるサーキット側にも intdash Server を設置し(以下、これを エッジサーバ と呼びます)、インターネット上ではサーバ間のみで通信を行う様に環境を構築しました。この場合、 クライアントはエッジサーバのみと通信を行う ため、インターネット側の通信量は削減されます。 問題解決構成のイメージを以下の図に示します。 解決構成のイメージ図 弊社 intdash Server はリアルタイムストリームのPub/Sub メッセージングに NATS が利用可能です。この NATS を用いてクラウド側の intdash Server とエッジ側の intdash Server で連携を行い、クライアントへのリアルタイムストリームを提供可能にしました。概念としては、CDN( Content Delivery Network) におけるキャッシュサーバに近いです。 サーバ間の通信に関しては、以下の理由からサイト間VPN接続としました。 セキュアに通信を行う サーバ側に追加設定を必要としない サイト間VPN接続には以下を用いました。 クラウド側: AWS 仮想プライベートゲートウェイ (VGW) エッジ側: vRouter VyOS 概要図を以下に示します。 解決構成概要図 エッジ側vRouter の構成 エッジ側のvRouter ではクライアントがサーバの接続先を意識しないで良い様に以下の機能を提供させました。 DHCP サーバ ( Dynamic Host Configuration Protocol ) DNS サーバ ( Domain Name System ) NAT ( Network Address Translation ) ルーティング(静的) VPN ゲートウェイ エッジ側ネットワークの機能およびリソースをvRouter の管理下に置くことで、クライアント側が接続先サーバのドメイン名を意識せずアクセス可能な構成としました。 結果 結果としては、理論通りの挙動をさせることには成功しました。ただし、他の検証施策もあった兼ね合いで、全てのクライアント側で同時にエッジサーバを利用して貰うことが叶わず、効果測定としては満足に行うことが出来ませんでした。次回、同じ構成を試す機会があれば、インターネット通信量の削減効果を測定してみたいと思います。 おわりに 今回は、インターネット通信環境が十分でないエッジロケーションでの弊社 intdash の利用に際し、エッジの intdash Server とサイト間VPN接続を用いてトラフィックフローの効率化を行なった話をご紹介しました。 インフラのエンジニアリングとしては、利用する製品の挙動を理解した上で、状況・要件に応じた設計・構築、可用性の選択が重要だと考えます。課題に対して柔軟に対応できる様に努めていきたいです。 最後までご覧くださいまして、ありがとうございました。
Advent Calendar 2020 16日目担当の ソリューションアーキテクト の岩坪です。 aptpodのソリューションアーキテクトという役割は、自社プロダクトであるIoTプラットフォーム intdash をベースに、お客様の課題解決やDX(Digital Transformation)実現に向けたソリューション提案、システム全体のアーキテクチャ設計を行い、納品までのプロジェクトリードを行っていきます。 今回は弊社の intdash と、近年ロボット開発だけでなく自動運転車両開発など様々な産業で採用されている ROS(Robot Operating System ) との連携についてご紹介します。 はじめに intdashのシステム構成 『intdash x ROS』で実現できること 『intdash x ROS』のユースケース例 おわりに はじめに ソリューションアーキテクトである我々の仕事は お客様の課題に対して、いかに自社プロダクトintdashを効果的かつ効率的に活用した解決手段をご提案できるか に尽きると考えています。 お客様のニーズ、課題、悩みは様々ですが、それぞれに対して全てをただ求められるままに対応していたのでは、費用と期間がいくらあっても足りませんし、出来上がったものもお客様にとって本当に望んでいたものになるとは限りません。 そこで提案に当たっては次のようなことを考えていく必要があります。 お客様に対してintdashが最大限に価値提供できるのはどの領域であるか 要件に対してintdashの特長が当てはまる領域はどこであるか 短期間でお客様の求めるシステムを提供する為に自社とお客様側システム/機器との境界をどこに持つべきか その産業のプロフェッショナルであるお客様に実現頂くべき領域はどこまでの範囲であるか 自社以外の技術/サービスで優れている領域があれば、その技術/サービスといかに連携を行うべきか その道のエキスパートの会社があるのであればその会社の技術に頼るべき これらを考える上で重要なのは、自社の技術/自社のプロダクトの特長を意識しつつ、 お客様 ↔︎ aptpod、世の中の技術 ↔︎ intdashの技術 との適切な境界を考え、また、その境界をいかにスムーズに繋ぐことができるか?ということです。 境界をスムーズに繋ぐ為には、自社プロダクト以外との連携を行いやすいように、プロダクトとして 世の中の標準的な技術 への対応を進めていく必要があります。それにより、新規にお客様へシステム提供するに当たってのコスト、リードタイムは大幅に削減できます。弊社のintdashもこういった進化を徐々に行っています。 前置きが長くなってしまいましたが、今回は自社プロダクト intdash と、 近年、ロボット開発だけでなく自動運転車両開発などでも採用されている ROS(Robot Operating System ) との連携について自社の取り組みをご紹介したいと思います。 intdashのシステム構成 弊社のプロダクトであるIoTプラットフォーム intdash の特長としては以下があげられます。 高いリアルタイム性でデータ伝送できること サーバを介しても低遅延でデータを届けることができる 伝送時にパケロスしたデータの完全回収ができること 不安定な通信環境化においても全てのデータを回収できる 多種多様なデータに対して時刻を同期してデータ管理が行えること エッジサイドでデータに対してタイムスタンプを付与することで種々のデータを同一の時間軸で扱うことができる サーバに保存されたデータを利活用できること リアルタイムのデータ活用だけでなく、保存された過去データも容易に分析、学習などに利活用できる intdashのシステムでは、データが実際に流れている自動車、ロボット、建機などの ターゲットデバイスに intdash Server と接続するためのゲートウェイデバイスを配置して、ゲートウェイデバイス上に自社プロダクトであるエッジ向けのミドルウェア intdash Edge を搭載することで、デバイスに流れるデータを intdash Serverまで伝送することが可能になります。 また、intdash Serverに伝送されたデータを、Webベースのダッシュボードアプリケーションである Visual M2M Data Visualizer を使用することで、多彩に可視化することができます。 intdash Edgeが扱うデータはお客様側のシステム、デバイスにより様々であり、自動車業界におけるCANなど、intdash Edgeが標準で対応しているデータ形式もあれば、お客様ごとにカスタム開発することで対応するケースもあります。 intdash Edgeでは、デバイスとデータをやり取りする機能部位を、 Device Connector と呼んでいます。上記の カスタム開発 の対応を少しでも減らして、お客様に取って 短期・低コスト でシステムに導入ができるものを提供する為には、世の中で求められるデータ形式に対してプロダクトとして標準対応を進めておく必要があります。 そして近年、ROSを取り扱うシステムが多く見られるようになってきたこともあり、 弊社のプロダクトとしてintdash Edgeにおいて、 Device ConnectorがROSに標準対応 しました。 ROS nodeとしての ROS用のDevice Connector ができたことで、お客様のROSを取り扱うシステムに、intdash Edge搭載のゲートウェイデバイスを接続し、 容易に、すぐに、低コスト でintdash ServerへのROSメッセージのリアルタイム伝送が可能となりました。 また、複数台のデバイス上にROS用のDevice Connecterを使用したintdash Edgeを搭載することで、 遠隔のデバイス間におけるintdash Serverを介したROSメッセージのやり取りも可能 になります。 これは、 遠隔制御、協調制御、遠隔監視や、遠隔におけるROSのデータ利用 などのユースケースが考えられ、ROSを取り扱うシステムの用途を大きく広げることができます。 『intdash x ROS』で実現できること これまでにご紹介の通り、 intdash EdgeのROS用のDevice Connector を使用することでROSを取り扱うシステムの様々な連携の可能性を広げることができます。 一般的にROSを取り扱うシステムのアプリケーション開発のワークフローは、下記のような流れになると思います。 特徴としては、開発しているアプリケーションを シミュレータ上で検証 を行い、ソフトウェアの修正を進めます。さらに、実機を用いて フィジカル環境における検証 を行い、またここでのフィードバックをソフトウェアに反映します。これらの作業を繰り返すことで、開発が進み、アプリケーションが成熟していきます。 このワークフローの中には下記のような要素が必要となると考えられます。 要素として、 「可視化」「シミュレーション」「テスト」「学習」「エッジ管理」 など多岐に渡りますが、これらに共通することは、エッジサイドのデバイス性能には限界があることから、 様々なクラウドサービス等と連携してこれらの要素を実現する必要がある ことです。また、クラウド上のシミュレーション環境と連携して実機を開発・検証することで、フィジカル(実機)とバーチャル(シミュレーション)のデータを統合管理できるようになり、 フィジカルとバーチャルの比較や相互フィードバックが容易になる 利点もあります。 そして、これらクラウドサービスとの連携には、 システム間のスムーズなデータ伝送なしには実現が難しい という課題があげられます。 『intdash x ROS』ではまさにこの「 スムーズなデータ伝送 」の手段を提供できます。 『intdash x ROS』で提供できる価値は具体的には下記の内容があげられます。 遠隔のROS空間を繋ぐ 同一のROS空間同様にintdash Serverを介してROSメッセージのやり取りを実現することが可能 リアルタイムデータ伝送 リアルタイムの遠隔制御、遠隔監視などの用途に耐えうる低遅延によるデータ伝送を実現することが可能 ROSのデータ利活用 intdash Serverに保存したROSメッセージを可視化・分析・学習・シミュレーションなどに活用することが可能 『intdash x ROS』のユースケース例 『intdash x ROS』の具体的なシステム構成の一例として、こちらの記事 『AWS re:Invent 2019 で AWS RoboMakerとintdash によるTurtlebot3の遠隔制御の展示を行いました!』 でも紹介している「intdash x AWS RoboMaker」による Turtlebot3の遠隔制御、デジタルツイン環境のシステム構成を紹介します。 実際にTurtlebot3とAWS RoboMakerのシミュレーションが動いている動画はこちらです。 後ろのモニタに表示されているのが、Visual M2M Data Visualizerと、AWS RoboMaker上のGazeboの画面表示です。 Visualize remote control TurtleBot3 with AWS RoboMaker+intdash RoboMakerのシミュレーション環境(Gazebo)と、実環境のController、Turtlebot3を各デバイスに搭載されたintdash Edgeがintdash Serverを介して接続することで、 実環境のTurtlebot3の遠隔制御、データ可視化だけでなく、シミュレーション環境のロボットに対する遠隔制御、データ可視化 までを行うことができます。 一例ではありますが、『intdash x ROS』が実環境とシミュレーション環境である他システムを スムーズなデータ伝送により連携 できることがお分かり頂けたのではないでしょうか。 おわりに 今回は弊社のIoTプラットフォーム intdash と ROS の連携についてご紹介しました。 繰り返しになりますが、『intdash x ROS』で解決することは下記です。 遠隔のROS空間を繋ぐことができる リアルタイムデータ伝送することができる ROSのデータ利活用ができる ROSを取り扱うシステムを開発される中で課題をお持ちで、『intdash x ROS』の世界観に興味を 持たれた方はぜひaptpodへ コンタクト 下さい。 弊社では、intdashを AWSマーケットプレイス でも順次提供していきますので、興味ある方には 容易にお試しいただける環境をご用意できます。 『アプトポッド、高速IoTプラットフォーム「intdash」を AWS Marketplaceで提供開始』 また、ロボティクスでバリバリROSを扱っています!や、ROSって興味あったんだよね。というような方やintdashの技術、価値に興味を持って頂けたエンジニアの方は是非、こちらの 弊社採用ページ もご覧になって頂けましたらと。 aptpodと一緒にリアルタイムデータ伝送が実現する明るい未来をめざしましょう。
自己紹介 はじめまして。株式会社アプトポッド ビジネスデベロップメントグループの小宮です。 aptpod Advent Calendar 2020 の15日目を担当します。 ビジネスデベロップメントグループとは、 個別の商談からイベント出展まで、対外的な情報発信の取りまとめ 製品の価格決めから売り方検討 納品物のデリバリーまでの支援 その他ビジネスサイドのプロセスの穴埋めと補強 などをしているチームです。要するに営業サイドの何でも屋さんです。 実は私、そろそろアプトポッドに入社して、ちょうど一年が経つところです。まだ「例年のアプトポッド」がどのようなものかわからないのですが、それでも今年はお客様との接点が例年より少なかった一年でした。 そんな中、今年は9本のプレスリリースを出しまして、その半分が製品のアップデートに関するものです。総括すると、2020年は 今後増えるであろう様々なスケールの案件に最適に対応するための、準備を色々と仕込んだ年 だったと言えます。 コロナ禍の折、なかなかちゃんとご説明する機会もないので、今年のプレスリリースのうち製品に関わるものが、弊社製品のどの部分をどうアップデートしたものだったのか、という観点でご説明させてください。 自己紹介 はじめに:アプトポッドは産業用IoTプラットフォームを提供している会社です intdashの構成 プレスリリース振り返り プレスリリース1:intdash 2.0リリース プレスリリース2:テラデータ様との協業 プレスリリース3:SDKの提供開始 プレスリリース4:AWS Marketplaceでのintdashのご提供開始 おわりに はじめに:アプトポッドは産業用IoTプラットフォームを提供している会社です アプトポッドは、産業向けに強みを持つIoTプラットフォーム”intdash”をご提供している会社です。 IoTに必要な、データ伝送やデータ管理などのコア機能は、上図上側のサーバーサイドで動く ミドルウェア に含まれています。さらに、上図下側の、「 エッジ 」と呼ばれる各種産業機器に取り付けるハードウェアや、エンドユーザーが直接操作する、時系列データの可視化ツールや各種ユーティリティ類も一気通貫してご提供しています。 このように、サーバーサイド、フロントエンド、ハードウェアの機械系から組み込みソフト、さらにはスマホアプリまで、いわゆるフルスタックに対応できる点が弊社の強みであり、このテックブログで取り扱うテーマがめちゃくちゃ幅広い所以でもあります。 intdashの構成 改めて弊社の製品群を、データ活用の流れに沿って分解したのが上図です。 一番左、つまり一番エッジに近いところから行くと、エッジのセンサーやアクチュエータと通信する ペリフェラル があり、エッジとクラウドの間のゲートウェイの役割を果たす ターミナルシステム があります。これらが弊社で取り扱う ハードウェア になります。 ターミナルシステム上で動く組み込みソフトも、一連の仕組み”intdash”に対応したものである必要があります。この組み込みソフト(エッジサイドのミドルウェア)を intdash Edge 、そしてそのうち特にintdashの特徴である、データの完全回収やリアルタイム伝送など、データ伝送機能を担うソフトウェアを intdash SDK for Edge Device (intdash Edge Agent) と呼びます。 続いて真ん中、サーバーサイドで動くミドルウェアの部分を、 intdash Server と呼びます。またこのミドルウェアには外部連携するためのAPIをご用意しており、 intdash SDK for Python を用いてカスタムの後処理やデータ分析などを手軽に行うことができます。 最後、一番右側の、データ活用の手段として、データ可視化ツールである Visual M2M Data Visualizer をご用意しております。こちらはブラウザ上で手軽に時系列データを可視化する、デザイン性に優れたツールです。 Visual M2M Data Visualizer DEMO 今年の夏に、弊社のコーポレートサイトのデザインが一新されたのをご存知でしょうか。この「PRODUCTS」以下には、弊社製品がカテゴリごとにならんでいます。各製品の詳細に興味がある方は、ぜひ コーポレートサイト も覗いていただけると幸いです。 プレスリリース振り返り プレスリリース1:intdash 2.0リリース 今年の製品関連のプレスリリース第1弾は、7月28日の「 DX プラットフォームintdash 2.0 をリリース 」でした。これは、intdash Serverのメジャーアップデートであり、本運用などにも使えるスケーラブルな環境を提供できるようになったことを指します。まず真ん中のコアの部分を、様々な規模の案件に対応できるようにした、ということです。 プレスリリース2:テラデータ様との協業 続いて第2弾は、8月4日の「 アプトポッドとテラデータ、自動車開発向けDXソリューション提供で協業 」です。これは、intdashで取得したデータの活用の手段を広げるため、日本テラデータ様のもつ次世代分析基盤「 Teradata Vantage 」と連携できるようになったことを指します。Visual M2M Data Visualizerが扱えるのは時系列データの分析のみであり、統計処理をやろうと思うと何かしらのカスタムコードをPythonで書く必要があったのですが、Vantageとの連携により、データ取得から分析まで、エンドユーザー側はコーディングすることなく、本来やるべき作業に集中できるようになりました。 プレスリリース3:SDKの提供開始 第3弾は、9月30日の「 高速IoTプラットフォーム「intdash」 アプリケーション開発キット「intdash SDK」の提供を開始 」です。上のintdashの構成で、しれっと2つのSDK: intdash SDK for Edge Device と intdash SDK for Python 「があります」とご紹介しましたが、これらは今年新規にリリースされたものです。 intdash SDK for Pythonについては以下のウェブサイトをご参照ください。 https://pypi.org/project/intdash/ また以下の情報もご参照ください。 ドキュメント サンプルコード(GitHub) intdash SDK for Edge Deviceについては以下のデベロッパーガイドをご参照ください。こちらは誰でも入手可能というわけではなく、ご利用される方に都度アクセス情報を払い出す形でご提供しています。 intdash Edge Agentデベロッパーガイド これまでのintdashの標準構成では、弊社の指定するターミナルシステム(エッジとクラウドのゲートウェイとなるコンピュータのこと)と、その下にぶら下がる弊社製のペリフェラルでしかシステムを構築できませんでした。しかしintdash SDK for Edge Deviceにより、ターミナルシステムとして扱えるコンピュータの種類が広がり *1 、システム構築の自由度が一つ上がりました。またこの開発環境を公開したことにより、弊社のエンジニアでなくとも、エッジサイドの組み込みソフトや、データ分析用のツールなどを作ることができるようになりました。 プレスリリース4:AWS Marketplaceでのintdashのご提供開始 続いて第4弾は、同じく9月30日の「 アプトポッド、高速IoTプラットフォーム「intdash」を AWS Marketplaceで提供開始 」です。これにより、お客様の持つAWS環境内に簡単にintdashを構築することが可能になりました *2 。先にご説明した各種SDKと組み合わせると、弊社の直接のご支援なくとも、弊社が構築するのと同機能のintdash環境をお客様の手で構築することができるようになります。 おわりに 以上、各プレスリリースの位置づけをざっと説明してきました。さらにざっくりまとめると、 データ活用の流れの上流から下流まで、弊社自身のマンパワーが制約にならないように、要所要所を強化、または汎用化してきた 、ということになります。 intdashの正式リリースは2018年5月 で、まだリリースから2年半の比較的若い製品です。実はこの後も、年始にかけていくつか製品関連のプレスリリースを予定しております。本投稿が「このプレスリリースはどの部分をどう強化することを意図しているのか」の理解につながれば幸いです。 *1 : 現時点で対応しているプラットフォームは、AMD64 アーキテクチャー上の Linux、Raspberry Pi 上の Raspbian、NVIDIA Jetson 上の NVIDIA L4Tのみになります。 *2 : 2020/12時点ではAWS Marketplaceは一般公開ではなく、Private Offerのみ
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の14日目を担当します。 研究開発グループでは、TCP/QUIC/UDP などのトランスポートプロトコルの製品適用に向けた検証を行っています。 今回の記事は前回の「5Gのネットワークを計測してみた」の続きになります。 tech.aptpod.co.jp 今回の記事では、以下の2つを紹介します。 モバイルルーターを Wi-Fi STATION SH-52A に固定して行った4G/5G のping/iperf3 の計測結果 4G/5G でのTCP/QUIC/UDP の計測結果 計測機材の紹介 計測場所の紹介 ネットワーク計測の結果 pingの計測 iperf3の計測 UDPの結果 TCPの結果 トランスポートプロトコルの評価 実験内容 評価結果 TCPの結果 QUICの結果 UDPの結果 考察 まとめ 計測機材の紹介 計測機材は前回と同じ機材を使用しています。 計測用のクライアントは組み込み製品を想定しRaspberry Pi4を使用しました。計測用のサーバは、AWS上でEC2のm5.xlargeのUbuntuインスタンスを使用しました。Raspberry Pi側はUbuntu 20.04、サーバ側はUbuntu 18.04 を使用しています。 また、5G/4Gの接続にはDocomoから販売されている モバイルルーター Wi-Fi STATION SH-52A を使用しています。SH-52Aは「5G/4G/3G」で接続するモードと「4G/3G」で接続するモードを選択可能です。「5G/4G/3G」モードでは5Gの接続に固定されるわけではありませんが、今回計測を行った場所では安定して5Gに接続が可能でした。 以下のような構成で計測をします。 前回の記事の再掲になりますが、計測に使った機器は以下のように接続しています。 計測場所の紹介 前回の記事で計測を行った場所と同じ日産スタジアム東ゲート付近の広場で、12/4に計測を行いました。 ネットワーク計測の結果 pingの計測 pingの計測では、pingコマンドを使って1秒間隔で60秒間RTT (Round Trip Time)を計測しました。 比較のために、12/4 に計測した結果と前回の記事の計測結果の両方を載せます。前回の記事では4Gの計測は モバイルWi-FiルーターL-01G というモバイルルーターを使っていました。 以下の表には、前回の結果と今回の計測結果の、平均値、10パーセンタイル値、50パーセンタイル値、90パーセンタイル値を載せています。 4G(2020/12/4計測) 5G(2020/12/4計測) 4G(2020/12/2掲載) 5G(2020/12/2掲載) 平均値 40.3msec 43.3msec 74.5msec 38.5msec 10パーセンタイル値 31.7msec 32.9msec 65.2msec 30.4msec 50パーセンタイル値 40.5msec 44.0msec 75.6msec 39.15msec 90パーセンタイル値 47.9msec 52.12msec 83.9msec 46.09msec 前回の記事では、5Gの方が4GよりもRTTが小さいと紹介しましたが、モバイルルーターをSH-52Aに統一した結果を見ると、4Gと5GのどちらもRTTには差分がなさそうです。 L-01G を使用した場合のみ大きく違う値が出ていますが、この原因は今後調査を行い機会があればお伝えしたいと思います。 iperf3の計測 iperf3の計測では、UDPとTCP(輻輳制御はBBRを使用)のそれぞれについて、クライアントからサーバに20秒間データを送信する計測を3回行いました。 UDPの結果 UDPの計測は律速するまで帯域を上げています。4Gは25Mbps、5Gは50Mbpsを設定しています。 4G(2020/12/4計測) 5G (2020/12/4計測) 4G(2020/12/2掲載) 5G (2020/12/2掲載 1回目 20.9 Mbps 45.2 Mbps 17.4Mbps 45.2Mbps 2回目 21.2 Mbps 18.4 Mbps 24.6Mbps 47.0Mbps 3回目 20.7 Mbps 39.4 Mbps 25.0 Mbps 47.0 Mbps このように、おおむね5Gの方が高いスループットを得られています。 UDPの2回目の時は、未到達のパケットがかなり多かったため4G側より低い値になっていますが、ネットワークの変動によるものではないかと思います。 TCPの結果 TCPの場合はビットレートを指定しなくても流せる上限まで流そうとするのでビットレートの指定はしません。 計測結果は以下のようになりました。 4G(2020/12/4計測) 5G (2020/12/4計測) 4G(2020/12/2掲載) 5G (2020/12/2掲載 1回目 10.3 Mbps 40.3 Mbps 9.33Mbps 15.9Mbps 2回目 13.6 Mbps 43.4 Mbps 11.1Mbps 17.8Mbps 3回目 16.2 Mbpbs 34.2 Mbps 12.3 Mbps 34.4 Mbps おおむね5Gの方が高いスループットが出ています。ただし、5Gの結果は前回の記事の結果と今回の結果で大きく差が出ています。したがって、変動の影響はありうると考えるのがよさそうです。 トランスポートプロトコルの評価 ここからは、トランスポートプロトコルの評価についてお伝えします。 実験では、 TCP(輻輳制御はCUBICを使用) QUIC(輻輳制御はCUBICを使用) UDP をそれぞれ4G/5Gで評価しています。 計測は4G/5Gに対してそれぞれ5回ずつ行いました。 実験内容 実験は、高頻度なデータの塊をクライアントからサーバに伝送するユースケースを設定します。 送信するデータは1unit (データの単位)を8byteとします。これを1000 unit/secで送信します。そのために1msec毎に1unitのデータをクライアント側で生成します。 弊社製品のintdashでは、IPやTCPヘッダのオーバーヘッドを低減するために、一定期間のユニットをバッファリングして送信する仕組みを導入しています。 なので、それに倣って10個の連続したデータを一つの塊として送信します。このバッファリングしたデータ単位はflush *1 と呼ばれています。 以下の図のように、送信時刻までの10msec分のデータが1つのflushとしてまとめて送信されます。 flush クライアント-サーバ間の送信遅延を評価するために、データがクライアントで生成された時刻とサーバ側で受信した時刻の差分を遅延時間として定義します。 サーバでは1flush分のデータをまとめて受信します。そのため、flushの先頭のunitは、ネットワークの上りの遅延+10msec程度の遅延が発生します。 クライアントとサーバ間のデータのやり取り この場合、遅延時間は以下のようになります。 生成時刻 受信時刻 1unit 0msec ネットワークの上りの遅延 + 10msec 2unit 1msec ネットワークの上りの遅延 + 9msec 3unit 2sec ネットワークの上りの遅延 + 8msec ... ... ... 10unit 9msec ネットワークの上りの遅延+1msec ここから、理想的な状態では「ネットワークの上りの遅延+10msec」の範囲に、遅延時間が収まることが期待されます。 評価結果 それでは実際に結果を見ていきます。 TCPの結果 ここでは5回の計測結果の中から一つの結果を取り上げていますが、他の結果も同様の傾向になっています。 まずはヒストグラムです。横軸は、遅延時間、縦軸は頻度になっています。 TCP ヒストグラム 理想的には10msec以内に収まってほしいのですが、5Gの場合は20msec、4Gの場合は40msec の範囲にデータが収まっていることがわかります。pingの結果が4G/5Gで差異がなかったことを考えると、4G/5Gの回線のみを変えて結果に差分が出た点は疑問が残ります。 原因は、クライアントからサーバの間のどこかで想定外のバッファリングが行われたのではないかと予想しています。 ヒストグラムだけだと理解が難しいので、時系列でのプロットも見てみます。 まずは、5Gの結果です。以下のグラフは横軸が10秒間に送信される10000unitのデータの番号で、縦軸がそれぞれの遅延時間になります。5Gの場合は19msec~36msec 辺りに収まっていることがわかります。 TCP(5G)の結果 次に4Gの結果です。4Gの場合は多くの値が、10msec~50msecの間にあることがわかります。 TCP(4G)の結果 原因は調査が必要ですが、ヒストグラムの違いの理由は明らかになりました。 QUICの結果 QUICの結果のヒストグラムは以下のようになります。 QUIC ヒストグラム ここでも、理想的には10msec以内に収まっていてほしいものが、5Gの場合は20msec、4Gの場合は40msec の範囲に収まっていることがわかります。 5Gの場合はTCPと同じような結果になっています。一方で4Gの場合は2つの山が見える結果になっています。 TCPの時と同様に、時系列のプロットを見てみます。 5Gの結果は以下のようになります。ヒストグラムで見たように、18msec~38msecの間にデータが収まっていることがわかります。 QUIC(5G)の結果 次に、4Gの結果です。4Gの場合は、18msec~34msecの範囲と、15msec~45msecの二つのパターンがあることがわかります。これはネットワーク上で別の経路を通った結果が反映されている可能性がありそうです。この結果、ヒストグラムには二つの山ができていると考えられます。 QUIC(4G)の結果 UDPの結果 UDPの結果のヒストグラムは以下のようになります。 UDP ヒストグラム TCP/QUICの結果と異なり、5Gの方が遅延時間のとる範囲が大きくなっています。 ここでも時系列のデータを見てみます。 5Gの結果はこれまでと異なり、値のとる範囲が安定していないことがわかります。その結果がヒストグラムにも反映されていたようです。 5G(UDP)の結果 逆に4Gの結果は、最初の2秒(2000unit)の振れ幅は大きいですが、それ以降は 19msec~29msec 辺りに落ち着いているデータが多いように見えます。 4G(UDP)の結果 考察 疑問が多く残る結果になりましたが、少なくとも以下の3点は言えそうです。 10unit以上のデータがまとまって届いているため、経路上のどこかで予期していないキューイングが発生している可能性が考えられる ネットワーク上の経路の違いが遅延時間に影響を与えている TCP/QUIC/UDPすべての結果を踏まえると、4G/5Gの違いがあると言い切れる根拠は今回の実験では得られなかった 疑問が多く残っているため、今後も調査・検証を続けていこうと思います。 まとめ 今回の記事では「5Gでのトランスポートプロトコルの評価」について紹介しました。 ネットワークの再計測の結果やトランスポートプロトコルの評価結果は疑問が多く残る結果となりました。今後も調査・検証を継続、新しい事実が判明したらお届けしたいと思います。 最後までご覧いただきありがとうございました。 *1 : https://www.aptpod.co.jp/products/white-paper/iscp.pdf
みなさま、こんにちは。研究開発グループと製品開発グループ に兼務で所属しております、きしだです。 aptpod Advent Calendar 2020 11日目を担当します。今回は機械学習に関わるエンジニア向けに、最近AWSがリリースしたAWS Lambdaの新機能を利用して、サクッと推論APIを作るネタをご紹介します。 aws.amazon.com 推論をすばやくAPI化する意義 その前に、推論箇所をAPIとしてすばやく用意できる必要性について簡単に触れたいと思います。 機械学習関係の案件では、お客様側も理解できるKPIを立てることが非常に重要視されています。例えば「モデルの正解率を〜にしたい」や「モデルの動く速さを〜にしたい」などですね。これらの具体的な数値はお客様と議論を重ね、お互いにしっくりくる数値に落とし込む必要があるのですが、これがとてもむずかしいのです。なぜでしょうか。 お客様と開発側で共に運用イメージがわかず、モデルに対して運用時の影響力がピンとこない 具体的な運用イメージをすべて仮説ベースで作り上げるため、仮説にほころびが生まれやすく議論が錯綜する 上記に心当たりがある方もいらっしゃるのではないのでしょうか。これらはいずれも「システム運用を前提としたモデルの使い方をお客様側でイメージできていない」ことに起因します。このときにAPIまでを用意してお客様に共有すると、以下のようなことが期待できます。 APIを叩くだけで推論結果が返ってくるので、お客様側もモデルを試しやすい APIとして用意されているので一時的にシステムに組み込むことができ、試験運用が可能になる 必要最低限の費用で、お客様側で運用しながらモデルをさわることができる ガチガチにシステムを作るより、まずは小さく簡単なシステムから始めることでイテレーションを回せる いいこと尽くめですね。お客様側もモデルにふれる機会がグッと増え、議論を活発にまわすことができ、より納得感をもってすすめることができます。そのためにモデル開発に留めず運用側に一歩踏みこむ姿勢が大事になり、モデルの推論箇所をAPIとしてすばやく用意することが必要になります。 今回の記事はその一歩のヒントになるよう、私が検証した内容の一部をご紹介するものです。 AWS Lambda コンテナサポートの嬉しいポイント AWS Lambdaといえば、 「サーバーのことを意識しないサーバーレスコンピューティングを提供してくれるサービス」で、アプリケーションをすばやく実行環境に持っていきたい時に重宝する素晴らしいサービスです。それだけでなく、Amazon API Gateway などの他サービスをトリガーにしたイベント定義も楽々できることで有名ですよね。 機械学習の実行環境もAWS Lambda上で動かせたら楽だろうなぁと思う時がありつつも、以下の制約により断念していました。 パッケージの割り当て量が 250MB Lambda向けにパッケージを作る際、依存パッケージが多いと手間がかかる ローカルテスト/デバッグはAWS Lambdaだけでお手軽にできない とくに容量制限が厳しく、機械学習の場合はTensorFlowやらKerasなどの大きめのパッケージを使用することが多いため、到底250MBに抑えるなんてことはできません。 しかしながら冒頭に記載のAWSのリリース内容では、 大きく以下のような内容がピックアップされていました。 コンテナイメージをそのままLambda関数としてデプロイできる コンテナイメージは 10GB までならデプロイ可能 ローカルで実行できるLambdaのRuntime APIツール Lambda Runtime Interface Emulator がオープンソース化される パッケージの容量が大きく緩和され、コンテナイメージ向けのローカルテストツールも提供されるとのことです。これは、従来のLambdaに対して抱いていた課題感をすべて払拭してくれる予感がしますね。 前置きが長くなってしまいましたが、試してみないわけにはいかない! ということで、実際に動かして見ましょう。 構成 今回は、 こちら のTensorFlowを使用した「車両を検出するモデル」を、AWS Lambda上で動かしてみます。 *1 以下のように、Amazon API Gatewayと連携させてクライアントPCから画像をPOSTすると、車両の検出結果を返すAPIを作ります。 構成図 TensorFlowで作成されたモデルが動くコンテナイメージを用意しAmazon ECRにPushします AWS Lambda上でLambda関数を作成、Amazon API Gatewayと紐付けて画像のPOST用APIを用意します コンテナイメージを作ってローカルでテストする まずはコンテナイメージを作ってみましょう。 手元にあるコンテナをそのまま使えばよいのではなく、AWS Lambda向けにコンテナを作りなおす必要があります。 理由としては、Lambda Runtime API を実装する Lambda Runtime Interface Clients を取り込む必要があるためです。このクライアントを実行するためにいくつか依存ライブラリをインストールしなければいけません。 上記をふまえ、以下のようなDockerfileを準備します。 # Define function directory ARG FUNCTION_DIR="/function" FROM tensorflow/tensorflow:1.14.0-py3 # Install aws-lambda-cpp build dependencies RUN apt-get update && \ apt-get install -y \ g++ \ make \ cmake \ unzip \ libcurl4-openssl-dev \ libsm6 \ libxrender1 \ libxtst6 \ # Install for vehicle detection python3-dev \ libsm6 \ libxext6 \ libxrender-dev \ libgl1-mesa-dev \ python3-tk # Include global arg in this stage of the build ARG FUNCTION_DIR # Create function directory RUN mkdir -p ${FUNCTION_DIR} # Copy function code COPY app/app.py ${FUNCTION_DIR}/app.py COPY app/lib/ ${FUNCTION_DIR}/lib/ # other python file RUN mv ${FUNCTION_DIR}/lib/* ${FUNCTION_DIR} # Install the runtime interface client & other python package COPY requirements.txt / RUN pip install --upgrade pip && \ pip install awslambdaric && \ pip install --target ${FUNCTION_DIR} --no-cache-dir -r requirements.txt # Set working directory to function root directory WORKDIR ${FUNCTION_DIR} # (optional) for TEST COPY aws-lambda-rie /usr/bin/ RUN chmod 755 /usr/bin/aws-lambda-rie ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ] いくつかポイントを記載します。 # (optional) for TEST COPY aws-lambda-rie /usr/bin/ RUN chmod 755 /usr/bin/aws-lambda-rie コンテナをローカルでテストする際は、冒頭で紹介の通り Runtime interface emulator を使います。ローカルでテストしたい場合は AWSの公式ドキュメント に従い、インストールしておきましょう。 (※コンテナにいれなくても、コンテナ実行時にマウントする方法もあるのでここは任意です) ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ] 実行時は、 Lambda Runtime Interface Client により対象の関数が呼び出されます。上記を実行するため ENTRYPOINT でクライアントの実行コマンド、 CMD で対象関数を指定します。 (今回は app.pyの handler 関数を対象) 上記の準備ができたら、buildします。 docker build -t serverless-function:latest . buildできたら、まずはローカルで動作するか確認します。 以下を実行します。 docker run -p 9000:8080 --entrypoint /usr/bin/aws-lambda-rie --name serverless --rm serverless-function:latest /usr/local/bin/python -m awslambdaric app.handler すると、以下のようにコンテナ内のLambda Runtime APIが動作します。 time="2020-12-08T07:30:14.459" level=info msg="exec '/usr/local/bin/python' (cwd=/function, handler=app.handler)" 早速エミュレーターが提供しているエンドポイントを叩いて見ましょう。 以下の画像をエンドポイントに投げてみます。 車の画像 以下はクライアント側をPythonで簡単に書いたものです。 # Amazon API Gatewayの代わりに base64にencode data = base64.b64encode(f.read()) data = { "body" :data} response = requests.post( "http://localhost:9000/2015-03-31/functions/function/invocations" , json=data) contents = json.loads(response.content) imageBody = base64.b64decode(contents[ "body" ]) Image.open(io.BytesIO(imageBody)) 上記を実行すると、コンテナ側のログが出力されていました。 START RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 Version: $LATEST /usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:516: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. ・・・ 2020-12-08 07:30:45.707671: W tensorflow/compiler/jit/mark_for_compilation_pass.cc:1412] (One-time warning): Not using XLA:CPU for cluster because envvar TF_XLA_FLAGS=--tf_xla_cpu_global_jit was not set. If you want XLA:CPU, either set that envvar, or use experimental_jit_scope to enable XLA:CPU. To confirm that XLA is active, pass --vmodule=xla_compilation_cache=1 (as a proper command-line flag, not via TF_XLA_FLAGS) or set the envvar XLA_FLAGS=--xla_hlo_profile. END RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 REPORT RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 Init Duration: 2.54 ms Duration: 7020.34 ms Billed Duration: 7100 ms Memory Size: 3008 MB Max Memory Used: 3008 MB Jupyter notebook側でもデータが返ってきました。 推論もできている様子です👍 推論結果 ECRにプッシュし、AWS Lambda上で実行してみる いよいよ実環境へのデプロイです。 ECRに先ほど作成したコンテナイメージをpushします。(docker loginが必要ですのでご注意ください) docker tag serverless-function:latest {AWS_ACCOUNT_NO}.dkr.ecr.ap-northeast-1.amazonaws.com/vehicle-detection-api:latest docker push {AWS_ACCOUNT_NO}.dkr.ecr.ap-northeast-1.amazonaws.com/vehicle-detection-api:latest コンソールを見ると無事pushできていました。 ECRの様子 次に Lambda関数を作成してみます。関数の作成時、「コンテナイメージ」を選択するとECRにコミットされているコンテナイメージを選択することができます。 関数の作成 これで無事作成できました。 次にAmazon API Gateway を連携させて、推論したい画像データをPOSTするためのAPIを構築します。 構築後のURL 無事構築できたので、ローカルの時と同様に画像を送付してみます。  ローカルテストと同様に、クライアント側として以下のコードを用意します。 response = requests.post( "https://{API_GATEWAY_CODE}.execute-api.ap-northeast-1.amazonaws.com/default/vehicle-endpoint-test" , data=data, headers=header ) imageBody = base64.b64decode(response.content) Image.open(io.BytesIO(imageBody)) 上記を実行すると....以下のように車が推論された結果が返ってくることが確認できました! 推論の結果 予想より簡単に推論エンドポイントを立てることができました! 🎉 使ってみた感想 一通り試してみた感想をまとめておきます。 良かった点 予め用意したコンテナをベースにLambda向けにビルドし直せばいいので、環境の作り方が楽! AWS Lambda Runtime API との連携テストもローカルで実施しやすくなり、環境準備がしやすい! 今後の課題となりそうな点 基本Lambda関数の実行時の仕様は変わらず。AWS Lambdaの仕様でデータのキャッシュはできないので、推論する場合毎度モデルをロードする必要がある ローカルでテストできるのは Lambda Runtime API経由のみなので、Amazon API Gatewayなどの他サービスとの連携までを見据えたテストまで完結することはむずかしい まとめ AWS Lambda上に推論環境として構築したコンテナイメージをデプロイし、Lambda関数として動かすことができました。外部に共有できるAPIとしてはまだ課題はありそうですが、とても簡単に試すことができるのでケースに当てはまりそうな方がいたらぜひ活用してみてください! ちなみに余談ですが、弊社製品のintdashというデータストリーミングプラットフォームと今回の推論APIのような機械学習ツールと連携すると、データの収集から推論まで行えるシステムをサクッと作れます。 もしまた機会があれば、今年リリース発表された intdash SDK for Python を使って、今回構築したAPIとintdashを連携したパターンもやってみたいと思います。 intdash SDK for Python のリリース発表はこちら www.aptpod.co.jp ご覧いただきありがとうございました! *1 : https://github.com/ahmetozlu/vehicle_counting_tensorflow より一部引用
はじめに Advent Calendar 2020 10 日目を担当する、SRE チームの柏崎です。 nginx をお使いのみなさま、TLS 導入が当然となっている現代、どんな設定をされていますでしょうか。 NGINX Config や SSL Server Test のようなサイトを利用し、セキュリティに気を配っている方も多いと思います。 そんな nginx ユーザのみなさま、最近 nginx 1.19.4 がリリースされ、 イケてる TLS 設定ができるようになった事はご存じでしょうか? ChaCha20-Poly1305 について まず前提知識として、ChaCha20-Poly1305 について、ほんの少し書いておきます。 ChaCha20-Poly1305 は、ストリーム暗号である ChaCha20 とメッセージ認証符号である Poly1305 を組み合わせた、認証付き暗号です。 RFC 7905 (2016/06) で TLS における利用が標準化され、TLS 的には比較的新しい暗号方式となっています。 OpenSSL だと 1.1.0 以降で利用可能です。 これまで、現実的に使える暗号方式として一択だった AES-GCM に加え、ChaCha20-Poly1305 が加わった形になります。 クライアントによる 2 方式の性能差 ChaCha20-Poly1305 はソフトウェア処理に向いた、簡潔なアルゴリズムです。 一方 AES-GCM は、ソフトウェア処理ではあまり性能が出ませんが、ハードウェア処理が利用できる環境ではとても高速です。 Intel や AMD の CPU には AES をハードウェア処理できる AES-NI という拡張機能が載っていますが、ARM 等には載っていません。 つまり、AES-GCM と ChaCha20-Poly1305 のどちらが性能が高いかは、環境によって違ってくるということです。 Android スマートフォンや Raspberry Pi、最近発売された M1 Mac など、ARM で動くデバイスは意外と多いです。 特に、弊社の製品のエッジ向けミドルウェア intdash Edge は、Raspberry Pi を含め様々なデバイスで利用されるので、この性能差は気になるところです。 実際に、openssl speed コマンドを使って、8192 バイトブロックにおける、AES-GCM と ChaCha20-Poly1305 の暗号化・復号化の性能を測ってみました。 ざっくりとした性能差を知るのが目的のため、それぞれ 1 回ずつの計測です。 利用したコマンドは下記です。 ### aes-128-gcm の暗号化 $ openssl speed -evp aes-128-gcm ### aes-256-gcm の暗号化 $ openssl speed -evp aes-256-gcm ### chacha20-poly1305 の暗号化 $ openssl speed -evp chacha20-poly1305 ### aes-128-gcm の復号化 $ openssl speed -evp aes-128-gcm -decrypt ### aes-256-gcm の復号化 $ openssl speed -evp aes-256-gcm -decrypt ### chacha20-poly1305 の復号化 $ openssl speed -evp chacha20-poly1305 -decrypt 確かに、Intel では AES-GCM の方が高性能ですが、ARM では逆転して ChaCha20-Poly1305 が高性能ですね。 クライアントの暗号スイートリスト TLS クライアントによっては、ネゴシエーション時の暗号スイートリストが、前述の性能差を考慮したものになっています。 クライアントの暗号スイートリストは Qualys SSL Labs の SSL Client Test で確認することができるので、実際に Intel MacBook と M1 MacBook で比較をしてみました。 Intel 系 MacBook 上の Chrome M1 MacBook 上の M1 用 Chrome 確かに、Intel MacBook では AES-GCM が優先されているのに対し、M1 MacBook では ChaCha20-Poly1305 が優先されていますね。 これまでの課題 TLS のネゴシエーションにおける暗号スイートは、クライアントとサーバの暗号スイートリストを比較することで、1 つに決定されます。 しかし、一般的な TLS サーバは、「サーバ側の暗号スイートリストを優先して」暗号スイートを決定するような設定がされています。 つまり、TLS クライアントが適切な優先度で暗号スイートリストを送ってきたとしても、サーバ側の優先度により決まった一つに決定されてしまうのです。 例えば、Raspberry Pi は ChaCha20-Poly1305 を使ってラクしたいのに、限られたリソースのなか苦手な AES-GCM で頑張らないといけない、みたいな事が起きてしまいますね。 nginx 1.19.4 でどうなる? nginx 1.19.4 では、 ssl_conf_command ディレクティブ を使って、OpenSSL の設定を行うことができるようになりました。 下記の設定を追加すると、「クライアントが ChaCha20 の暗号スイートを優先している場合はサーバも優先する」という動作をさせることができます。 イケてる! ssl_conf_command Options PrioritizeChaCha; この設定は、 僕が勝手に nginx の実験場にしている アプトポッドのコーポレートサイト にも適用しています。 Chrome でアクセスして開発ツールを開くと、下記の画像のように環境によって適切な暗号方式でつながるはずです。 Intel 系 MacBook 上の Chrome M1 MacBook 上の M1 用 Chrome さいごに ちょっとマニアックになってしまいましたが、nginx の TLS 設定についてお話をしました。 ちなみに M1 Macbook でのスクショは、同僚に協力していただきました。ありがとうございます!M1 羨ましい! nginx 1.19.4 は mainline のバージョンなので、intdash のサービス用サーバでは、まだこの設定を適用できておりません。 stable 版の 1.20.0 が待ち遠しいですね。 この記事は、筆者の個人ブログの記事をベースに加筆修正したものです nginx 1.19.4 以降で ChaCha20-Poly1305 の運用が現実的になった件
aptpod Advent Calender 2020 、 9日目の記事です。 本日の担当は、組込み開発チームでFW開発を担当している矢部です。 はじめに 組込み機器の開発に関わって1x年ですが、入出力の自動化が難しい機器も多く、結局手作業になって無駄にボタン押下やUI操作の速度が洗練されたりします。私の場合、ゲームをよくやっていたからか、効率よく操作させることができるとちょっと嬉しかったりもします。 とはいえリグレッションテストなどを考えると極力自動化したいところなので、過去いろいろと試行錯誤してきました。 今回は、その中でも比較的簡単な、USB通信をインターフェースに持つ機器のテストを自動化した際の手法について簡単に解説します。 前提知識 今回の記事で詳しくは解説しない部分をさらっと。 BDDとは 振る舞い駆動開発(Behavior Driven Development)と呼ばれるもので、TDDの一種。 システムに期待する挙動や制約などを自然言語に近い形式で記述し、テストを実行できるようにしたもの。自然言語で書けるため、非開発者であってもシステムの仕様がわかるように書ける(らしい)。 テストファーストな開発だと「テストコードは仕様である」という考え方になるので、それを突き詰めたものがBDDかな、と思っています。 BDDのフレームワーク 主要な言語であれば、たいていBDDを実現するためのフレームワークが存在します。 Ruby: RSpec Python: Behave C/C++: Catch2 etc... 私が最初に触れたのはRSpecですが、最近はPythonを利用する機会が多いため、今回はBehaveで実装します。 Pythonの開発環境 色々ありますが、最近はpipenvを使っています。設定や実行方法など、バランスが取れていて使いやすい。 公式: https://pipenv-ja.readthedocs.io/ja/translate-ja/ Pipenvを使ったPython開発まとめ: https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a USB機器をテストする 実行環境 Ubuntu MATE 20.04 今回はLinuxのデバイスドライバを利用する都合上、テスト環境はLinuxになります。 Python環境構築 テスト用のディレクトリに、pipenvを利用してBehaveをインストールします。 $ mkdir usbtest $ cd usbtest $ pipenv --python 3 $ pipenv install behave テスト対象 今回は私が開発に関わった、aptpodの CAN-USB Interface - AP-CT2A 1 を対象にします。実際に AP-CT2A ではこの方法で全機能の自動テストコードを書いています。なお、2020/12 時点ではデバイスドライバなどのコードを公開していないため、細かいところはぼかしたり改変しています。 PythonでUSBデバイスを操作する 方法はいくつかあると思いますが、今回はデバイスドライバがあるため、 ioctl を利用して操作します。 基本的な手順は以下になります。 デバイスファイルをopenする。 ファイルディスクリプタを利用して、ioctlでIO制御する。 ioctlのリクエストは汎用的なものであればPython側にありますが、今回のように特殊なものは自前で設定する必要があります。 リクエストの種別として IO、 IOR、_IOWなど 2 があり、C/C++であれば以下のように実装されます。 struct fw_data_s { int value; }; typedef struct fw_data_s fw_data_t; #define CMD1 _IO( 'A' , 0x1 ) #define CMD2 _IOR( 'A' , 0x2 , fw_data_t) #define CMD3 _IOW( 'A' , 0x3 , fw_data_t) 上記のコマンドがデバドラ側にあると仮定して、デバイス操作のためのクラスをPython上で実装すると以下のようになります。 __make_xxx のメソッドが、リクエストの生成部分になります。 import ctypes import fcntl import logging import os class FwData (ctypes.Structure): _fields_ = [( "value" , ctypes.c_int32)] class ApCt2a : def __init__ (self, path): self.__path = path def open (self): self.__fd = os.open(self.__path, os.O_RDWR | os.O_NONBLOCK) if self.__fd == - 1 : logging.critical( "[APTTRX] failed open path:{0}" .format(self.__path)) def close (self): os.close(self.__fd) self.__fd = None def cmd1 (self): fcntl.ioctl(self.__fd, self.__make_io_req( 0x1 )) @ property def data (self): data = FwData() fcntl.ioctl(self.__fd, self.__make_ior_req( 0x02 , ctypes.sizeof(data)), data) return data.value @ data.setter def data (self, value): data = FwData() data.value = value fcntl.ioctl(self.__fd, self.__make_iow_req( 0x03 , ctypes.sizeof(data)), data) def __make_io_req (self, nr): return ord ( "A" ) << 8 | nr def __make_iow_req (self, nr, size): return 1 << 30 | ord ( "A" ) << 8 | nr | size << 16 def __make_ior_req (self, nr, size): return 2 << 30 | ord ( "A" ) << 8 | nr | size << 16 テストする仕様 AP-CT2Aのメイン機能は、入力したCAN 3 データに時刻情報を付与してホストに渡すというものです。さらに複数台あった場合、同期用のケーブルで機器間を接続することで、付与する時刻を同期させることができます。 AP-CT2A接続図 上の写真で、機器間を繋いでいるのが同期ケーブルです(開発用の機材なので汚いのはご容赦を)。今回は、この同期機能を確認するためのテストを例に取ります。 テストしたいシーケンス 2台のAP-CT2Aを接続する 2台のAP-CT2Aを時刻同期する CANデータを500kbpsで受信できるよう設定する CANデータを入力する 2台のAP-CT2A受信したCANデータの時間が一致していることを確認する 実際に書いてみる ファイルは以下のような形で構成されます。 .feature にテストシナリオ、 steps/ 以下に実際にシナリオ内で実行される処理(step)を実装します 4 。 features/ features/everything.feature features/steps/ features/steps/steps.py 今回の例だと、まずテストシナリオをこのように書きます 5 。 Feature: AP-CT2A テスト Scenario: 複数台で時間同期する Given 2台のAP-CT2Aを用意する And AP-CT2Aを時刻同期する And AP-CT2Aを500kbps入力に設定する When AP-CT2AにCANデータを500kbpsで10個入力する Then AP-CT2AがCANデータを10個取得している And AP-CT2Aが受信したCANデータの時刻情報が一致している そして、各stepの処理について、以下のように書きます。 @~ のところで対応するstepを指定し、変数として利用したい箇所は {} で記載します 6 。 from behave import given, step, then, when @ given ( "AP-CT2Aを時刻同期する" ) def step_impl (context): for d in context.devices: d.sync() @ when ( "AP-CT2AにCANデータを{baudrate:d}kbpsで{num:d}個入力する" ) def step_impl (context, baudrate, num): send_candata(baudrate, num) @ then ( "AP-CT2AがCANデータを{num:d}個取得している" ) def step_impl (context, num): for d in context.devices: data = d.receive() assert len (data) == num 実行例 上のシナリオを実行した例。 $ pipenv shell $ behave Feature: AP-CT2A テスト # features/ap_ct2a.feature:1 Scenario: 複数台で時間同期する # features/ap_ct2a.feature:3 Given 2台のAP-CT2Aを用意する # features/steps/ap_ct2a_steps.py:6 0.001s And AP-CT2Aを時刻同期する # features/steps/ap_ct2a_steps.py:11 0.001s And AP-CT2Aを500kbps入力に設定する # features/steps/ap_ct2a_steps.py:16 0.000s When AP-CT2AにCANデータを500kbpsで10個入力する # features/steps/ap_ct2a_steps.py:21 0.000s Then AP-CT2AがCANデータを10個取得している # features/steps/ap_ct2a_steps.py:26 0.000s And AP-CT2Aが受信したCANデータの時刻情報が一致している # features/steps/ap_ct2a_steps.py:31 0.000s 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 6 steps passed, おわりに 今回はUSB機器を例に取りましたが、何らかのプロトコルで通信することで動作するものについてはほぼ自動テスト化できるものと思っています。 外部からの操作を必要とするものに関しては(例えばUSBの挿抜とか)なかなか自動化は難しいですが、aptpodには優秀なHWエンジニアも在籍しており、テスト用の治具も自作したりします。 組み込み機器であっても、既存のフレームワークをうまく使ったり、アイデア次第で煩雑なテスト業務を軽減させることができますので、トライしてみてはいかがでしょうか。 https://www.aptpod.co.jp/products/hardware/ のペリフェラルデバイスの項に載っています。 ↩ https://www.quora.com/What-is- IO- IOR-and-__IOW-in-ioctl ↩ https://ja.wikipedia.org/wiki/Controller_Area_Network ↩ https://behave.readthedocs.io/en/latest/tutorial.html#features ↩ 普段は日本語では書きません。マルチバイト文字怖い。 ↩ https://behave.readthedocs.io/en/latest/tutorial.html#python-step-implementations ↩
Advent Calendar 2020 8日目 を担当します。ハードウェアグループの加藤です。 私は2020年2月にハードウェアエンジニアとして入社しました。10か月間業務を行って思ったこと、感じたことなどを書きます。 自己紹介 ハードウェアエンジニアの仕事 アプトポッドで仕事して思ったこと、感じたこと 社員に対する信頼がある 社員同士の互いの信頼や尊重もある。 やらされている感がない 事業に無理がない 技術的に本質的な方向を向いている 業務環境、開発に使用しているツール アプトポッドに入社してみた結果 仕事に対するモチベーションが大いに上がった 未経験の業務もやらせてもらえるのでスキルアップ 妻が喜んだ 最後に 自己紹介 簡単に私がアプトポッドに入社するまでに勤めた会社についてご説明します。特に意識はしていませんが、入社して思ったこと感じたことというのはこれまで勤めた会社との比較になっていると思います。 ゲーム機等の会社 1994年に入社。ゲームセンター向けのゲーム機器を6年間、海外カジノ向けのスロットマシンを13年間、業務用フィットネスバイクの開発を2年間、各グループ会社の開発部門でハードウェアエンジニアとして商品開発を行いました。海外カジノ向けスロットマシンのグループ企業は、米国のラスベガスに本社を設置するところからのスタートでいろいろ大変でした。 業務用フィットネスバイクの企画から販売まで行うスタートアップ企業 設立されたばかりの会社に入社しました。社員数は10名以下。ハードウェアもソフトウェアも自分以外はわかる人はおらず大変でした。業務用フィットネスバイクというのは、スポーツクラブ等においてある自転車型の運動機器です。この会社では大学・企業・プロの運動部にターゲットを絞った製品の開発を行いました。ここでは4年ちょっと勤めて、立ち上げを成功させることができました。この後、アプトポッドに入社しました。 ハードウェアエンジニアの仕事 アプトポッドのハードウエアエンジニアの業務内容はこんな感じです。 ハードウェア製品の企画、投資(費用・工数)回収計画の立案 設計、開発、評価等 規格・認証試験の対応 製造会社の選定、その他いろいろも含めて量産できるようにする。 量産対応(生産を継続する。生産効率や品質の向上も。) 顧客案件のHWカスタマイズ対応 企画から量産まで全部ですね。 製造そのものは必要な設備等を有する会社に委託しています。そうは言っても部品のEOL対応などは自分達で判断する必要があります。設計や開発は都度終わらせることができますが、生産はそうはいきません。製品の種類が増えてきた時にどうするかはこれからの課題となっています。それでも自分たちで効率よく行っていくのが最も良いのかなぁ。と考えています。 アプトポッドで仕事して思ったこと、感じたこと 社員に対する信頼がある パソコンを自宅に持ち帰ることができます。 ”何言っているの?”という人の方が多いでしょうか? 私がこれまで勤めていた会社では平社員はパソコンの持ち出しができませんでした。 オフィスには、お菓子コーナーがあってお菓子食べ放題です。カップ麺も栄養ドリンクも置いてあります。コーヒーも飲めます。自動販売機の飲み物も1本30円です。 しかし、無茶苦茶なことをする人はいません。無茶苦茶な事というのは、本当に食べ放題食べてしまったり、カップ麺や自販機の飲み物を大量に持ち出して外で売りさばいたりということです。 新型コロナウィルス騒ぎ以降、会社全体として在宅勤務が主体となったのですが、「在宅勤務だからやらないといけないこと」というのもありません。 第一印象は、「会社は社員を信頼していて、社員のモラルも高い。すごいなこの会社。」でした。 社員同士の互いの信頼や尊重もある。 社内でなにか人にお願いする時、人からお願いされる時、期限を設定しないのです。誰もがすぐに対応します。なので、必要最低限の時間で事が進んでいきます。いやぁ、すごいです。 打ち合わせを設定する時は、参加者のカレンダーを確認して空いてさえいれば、好きなところに入れられます。極端な話、今すぐ!でもOKです。それで注意されたり怒られたりすることはありません。 やらされている感がない 上司の都合で書類を作成したり提出したりを求められることがありません。業務のスケジュールも自分で立てますし、レビューも自分で設定します。準備ができたらカレンダーに設定するだけなので、参加者の都合の調整などは発生しません。 事業に無理がない ハードウェアは量産時に材料費がかかります。販売価格の設定に無理があるとずーっと赤字のままになってしまいます。しかし、競合他社と同じ内容の製品を販売することになってしまう場合、営業部門の人は価格を安く設定することを求めざるを得なくなります。 アプトポッドの場合、独自性と技術力の高い製品・サービスを提供することができるので、販売価格や原価で無理を求められることはありません。 一般的に、独自性の高い商品であったとしても、ハードウェア単体だと同等品は簡単に作られてしまうので、売れて目立ってしまうとすぐ価格競争になってしまいます。アプトポッドの場合、そういう心配も不要であり、設計・開発に集中できるのはいいなぁと感じています。 技術的に本質的な方向を向いている 設計開発部門が向いている技術的な方向性に対して、私個人が安心感を得られるかどうかということです。主観なので正しい/間違っているはないのですが、自分にとっては大事に考えていることです。何かというと、例えば掃除機を作っている会社があったとして、吸引力に優れた掃除機を作れている会社であるなら安心で、(お客さんの要望だったとしても)本体の重量が軽いとか紙パックの容量が大きいといった掃除機を売ったり売れちゃったりしている会社であれば不安といった具合です。ディスプレイだったら、画面大きく、解像度高く、発色に優れた製品を出せていれば大丈夫で、”専用の眼鏡をかけると立体に見えます。”ということだと、「大丈夫かな?」って感じです。 データをクラウドに保存し、そこから閲覧する製品・サービス( IoTプラットフォーム intdash )を行っている会社で「データの完全回収」「低遅延」「高速」(これだけではありませんが)に対する方向性が社内全体で見受けられ、これに対し私は安心感を得ました。 業務環境、開発に使用しているツール 出社する/自宅で仕事するは本人の自由 オフィスは新宿区四谷四丁目 勤務時間も完全に裁量労働制 社内コミュニケーションはSlack 打ち合わせはGoogleMeetが基本、GoogleCalendarで互いの予定を確認 ドキュメント作成はConfluence、課題管理はJIRA 基板CADはQuadcept、筐体設計CADはFusion360 CADは「必要な機能が一通り揃っており手頃な価格帯のもの」を使用していると思います。選定する人の好みや経験に影響されやすいと思うのですが、こんなところにも柔軟で合理的な会社の色がでているように感じました。 アプトポッドに入社してみた結果 仕事に対するモチベーションが大いに上がった 成功体験がないとなかなか大きい成長が得られない。そして、成長市場でないと成功体験を得るのは難しい。さらに、成長市場であっても人や組織や方向性がしっかりと確立されていないと長続きしない。アプトポッドは全てを満たしており、そんな中で仕事をできることに幸せ(と書くと胡散臭いですが)を感じ、大いにモチベーションをもって仕事に取り組むことができるようになりました。 未経験の業務もやらせてもらえるのでスキルアップ 必要とされる業務であれば、やりたいと言えばやらせてもらえます。私の今年の業務の場合、全く経験はありませんでしたが3DCADで基板筐体用のデータや加工用の図面を作成したりといった事をやらせてもらえました。今回のような記事の投稿というのも初めての体験でした。ただ、「やって良い」というだけでなく、周りの多くの方々に教えていただきながら業務を進めることが出来るので、自身のスキルアップが実現できました。 妻が喜んだ アプトポッドに勤めることになって、私の妻も「よかったわぁ」と大変喜んでいます。主な理由はふたつです。 ほとんど在宅勤務なので、「いつもそばにいるので安心」「家なので食事も制御可能」 私の妻の場合、「私が見える場所にいる。自分が帰ってきた時に私が家にいる」のが安心ということでした。 新型コロナウィルス騒ぎ以降9.5カ月間で出社したのは9日程度、EMC試験の為の外出が5日程度でした。 「将来性のある業種なので安心」「社員数も多い会社なので安心」 思うところはありますが、まぁ喜んでくれるのはありがたいことです。 社員数はその会社の成長フェーズによって変化するもので、多ければ良いというものではないと思います。しかし、社員数10名以下の中小企業で社員1人が頑張っているだけでは、なかなか会社も個人も成長は難しく、妻の言っていることも100%間違っているというわけでもないかな。とも思います。人数が少ない(特に1人の場合)と得られる情報が少なく進歩に時間がかかるのでキツイです。 アプトポッドよりも社員数の多い会社は多数あるでしょうが、各社員が優秀であることと効率の高いツールを使いこなしていることから、共有している情報の質と量は高いレベルにあるのだろうなと思っています。 最後に 社外の方は、良い事ばっかり書いてあるようにお感じになる方もいらっしゃるかもしれません。しかし、今回の記事は私が思ったこと・感じたことで嘘はありません。 もしアプトポッドに興味を持っていただけたら、 弊社の採用ページ に足を運んでいただけるとありがたく思います。
Advent Calendar 2020 7日目担当デザイン室の上野です。 デザイン室では製品や案件のUIデザインを行っています。 弊社のWebベースのダッシュボードアプリケーション Visual M2M Data Visualizer で可視化できるVisual Partsには3Dモデルを表示するパーツも存在します。 そこで用いられる3Dモデル作成も担当しています。 パーツをパネルに貼り付けてデータを疎通させると以下の画像のようにモデルがサーバに送られてきたデータと同期してリアルタイムに動くようになっています。 こういった3Dモデルでのデータ可視化は直感的でわかりやすいため、製造業のお客様の現場で需要があります。 今回はそこで使われている3Dモデルの作成フローと気をつけている3つのポイントを書いていこうと思います。 モデリングツールについて 3Dモデルの作成フロー モデリング時に気をつけている3つのポイント ちょうど良いくらいにポリゴン数を調整する 法線の向きを正しく ゴミを残さない モデリングツールについて 弊社では Maya LT を主に使用しています。 MAYA LTは元々MAYAと呼ばれるピクサーなどが使っている有名な3DCGソフトがあり、そこからレンダリングなど映像に特化した機能を制限し、ゲームなどをはじめとするリアルタイムに描画するモデリングに特化したソフトウェアになっています。 MAYA自体が業界標準的な側面もあり、ドキュメントがとても豊富です。 他には Blender というツールも使い始めています。 こちらは無料で間口が広く様々なユーザーがおり拡張機能も豊富なのが魅力です。 UIなどが独特で一昔前は使いにくい印象でしたが、最近は大きいアップデートがあり有料ソフトウェアと遜色ないほど進化してきました。 書き出しできるフォーマットやそれぞれ特化した機能の違いがあるので要件によって使い分けています。 3Dモデルの作成フロー モデルの作成は0から作るパターンと、CADなどの3Dデータをいただいて作り直すパターンがあります。 0から3Dモデリングを作る場合のフローは以下の3つのステップになります。 ① 下準備として三面図を用意 ② Maya上に三面図を引いてモデリング ③ 完成 CADなどの3Dデータをいただいて作り直す場合は、そのままのフォーマットではブラウザで表示することはできないので、一度MAYA LT上に取り込みます。 その際にそのまま書き出そうとするとポリゴン数があまりにも膨大(データ量も肥大)なので、ガシガシ減らして動作に耐えうるくらいまでモデルを修正していきます。 モデルが完成したら、そこからOBJファイルを書き出し、Photoshopに読み込んでサムネイルを作成します。 ここまでがデザイン室の作業となります。 あとはOBJファイルとサムネイル画像をエンジニアに渡し、実装をお願いしていく形になります。 他にもボーンを入れてアニメーションできるようにFBXファイルというフォーマットで実装した例もあります。 アバターロボットを遠隔でリアルタイム制御する未来へ 〜Aptpod×MELTIN MMI エンジニアクロストークインタビュー〜 こちらのロボットの3Dモデルは、アバターロボットの操作側から上がってくるデータで各関節にあるボーンの回転を制御しました。 こういったアニメーションをする3Dモデルは、外観ができたあと内側にボーンを配置していき、各ポリゴンの頂点のウェイトを紐付けしていく作業があります。 モデリング時に気をつけている3つのポイント 自分がモデリングをする際に気をつけているポイントは ポイント1 ちょうど良いくらいにポリゴン数を調整 ポイント2 法線の向きを正しく ポイント3 ゴミを残さない の3つです。 ちょうど良いくらいにポリゴン数を調整する 実はこちらの3Dパーツですがマテリアルは全てThree.jsでマテリアルを制御してライン描画するようになっています。 リアルタイムで描画するにあたってポリゴン数が多いと動作が重くなります。 しかし、ポリゴンが極端に少なすぎると密度がスカスカになり形がわかりにくくなってしまいます。 動作を保ちながら形も分かりやすいようにちょうどいいポリゴン数になるように心がけて作っています。 ポリゴン数は ディスプレイ -> ヘッドアップディスプレイ -> ポリゴン数 にチェックを入れることでビューポートに表示されるようになります。 作る対象によって差はありますが多くても7000ポリゴン以内を目安に作っています。 (今回例にだしている飛行機のモデルは汎用的なものになるので、特徴を出さずに△1028ポリゴンと少なめに作っています) お客様からCADベースのモデルデータを表示したいと依頼されるケースがあります。 ポリゴン数50万〜データ容量も50MBなど、Webブラウザで扱うには重たいデータなので、扱えるようにローポリ化します。 例えば下図のように約70万ポリゴンを2500ポリゴンに減らすとこれだけ軽くなり、パフォーマンスを保ってブラウザで表示できます。 法線の向きを正しく 法線とはポリゴンのフェースの向きを表します。 緑の線が飛び出ている方向がフェースの向きになります。 左側が上向きのフェース、右側が下向きのフェースです。向きによって描写が変わるのがわかりますね。 メッシュの結合やポリゴンの穴を埋めたりしているとこの向きが反転してしまうことがあります。 そのままだと描画がおかしくなるため対象のフェースを選び メッシュ -> 法線 -> 反転 で直していきます。 確認方法としては、対象のオブジェクトを選択し、 ディスプレイ -> ポリゴン -> フェース法線 をONにすることで画面のように表示されます。 ゴミを残さない ポリゴンの結合などを繰り返したりしていると気づかないうちに不整合な要素ができてしまいます。 そういった要素も描画時に問題がおきる元なのでそちらもしっかり取り除いて綺麗なデータを渡すようにしています。 メッシュ -> クリーンアップオプション を開き該当項目にチェックして適用を押すと該当するフェースなどを綺麗にしてくれます。 他にもヒストリの削除など作業中に色々整理しながら作っていきます。 以上がWebアプリケーション用の3Dモデルの作成フローと気をつけている3つのポイントになります。 ありがとうございました!
aptpod Advent Calendar 2020 の4日目を担当します、研究開発グループの大久保です。 2日目 では、Rustと Quinn でechoサーバを作成しました。今回は応用として、Quinnを使って大量のデータを送信し、パフォーマンス評価をしてみたいと思います。弊社内のユースケースとして、エッジ側で溜まったデータをサーバに送信したい、という状況が考えられるため、それを想定した評価となります。 実装と検証 1プロセス内でサーバ、クライアント両方立てます。 Cargo.toml に追記する依存関係は以下のようになります。 [dependencies] anyhow = "1" futures = "0.3" quinn = "0.6" rand = "0.7" rcgen = "0.8" tokio = { version = "0.2", features = ["full"] } 評価用のコードは以下のようになります。 use anyhow :: * ; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, ClientConfigBuilder, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use rand :: {thread_rng, Rng}; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: time :: Instant; // 送信用のデータ(10KiB) const MSG_SIZE: usize = 1024 * 10 ; #[tokio::main] async fn main () -> Result < () > { let cert = rcgen :: generate_simple_self_signed ([ "localhost" . to_owned ()])?; let cert_der = cert. serialize_der ()?; let priv_key = cert. serialize_private_key_der (); let cert_der_clone = cert_der. clone (); // 送信用の適当なデータを乱数で用意する let mut send_data = vec! [ 0u8 ; MSG_SIZE]; thread_rng (). fill ( &mut send_data[..]); // サーバを動かす tokio :: spawn (async move { run_server (cert_der_clone, priv_key).await. unwrap (); }); // クライアントを動かし、所要時間(ミリ秒)を表示する let start = Instant :: now (); run_client (cert_der, & send_data).await?; let elapsed = start. elapsed (); println! ( "Elapsed time: {} ms" , elapsed. as_secs () * 1000 + elapsed. subsec_millis () as u64 ); Ok (()) } // サーバを動かす async fn run_server (cert_der: Vec < u8 > , priv_key: Vec < u8 > ) -> Result < () > { let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); let cert = Certificate :: from_der ( & cert_der)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & priv_key)?, )?; let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), 33333 ); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); while let Some (conn) = incoming. next ().await { tokio :: spawn (async { match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // サーバへの接続を扱う async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); while let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; tokio :: spawn (async { let _data = uni_stream. read_to_end (MSG_SIZE).await. unwrap (); }); } println! ( "connection closed from {}" , connection. remote_address ()); Ok (()) } // クライアントを動かす async fn run_client (cert_der: Vec < u8 > , send_data: & [ u8 ]) -> Result < () > { let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & cert_der)?)?; let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 )), 33333 ); let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; let NewConnection { connection, .. } = endpoint. connect ( & addr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); for _ in 0 .. 1000 { let connection = connection. clone (); let mut send_stream = connection. open_uni ().await?; send_stream. write_all (send_data).await?; send_stream. finish ().await?; } connection. close ( 0u8 . into (), & []); endpoint. wait_idle ().await; Ok (()) } echoサーバと基本的に同じように構築していきますが、いくつかの変更点があります。 let cert = rcgen :: generate_simple_self_signed ([ "localhost" . to_owned ()])?; let cert_der = cert. serialize_der ()?; let priv_key = cert. serialize_private_key_der (); main 関数のこちらの部分では、 rcgen クレートを用いて、自己署名証明書を作成しています。テスト目的ならこれで十分でしょう。 while let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; tokio :: spawn (async { let _data = uni_stream. read_to_end (MSG_SIZE).await. unwrap (); }); } handle_connection 関数では、サーバに対して張られた単方向ストリームを読み込みます。読み込むだけで返信は行いません。 for _ in 0 .. 1000 { let connection = connection. clone (); let mut send_stream = connection. open_uni ().await?; send_stream. write_all (send_data).await?; send_stream. finish ().await?; } run_client 関数では、用意したデータ(10KiB)を1000回ループを回して送信します。 こちらを実行すると、以下のような出力になりました。 listeing on 0.0.0.0:33333 connected: addr=127.0.0.1:33333 connected from 127.0.0.1:47158 closed by peer: 0 Elapsed time: 131 ms 1000回送信したので、1回あたり0.13ms程度で10KiBを送信していたことが分かります。 送信タスクを非同期にする せっかく tokio で非同期にコードを書いているので、 FuturesUnordered を使って送信タスクを非同期にしてみます。その場合、送信を1000回行っているfor文を次のように書き換えます。 let mut tasks = futures :: stream :: FuturesUnordered :: new (); for _ in 0 .. 1000 { let connection = connection. clone (); let send_data = send_data. to_vec (); let task = async move { let mut send_stream = connection. open_uni ().await. unwrap (); send_stream. write_all ( & send_data).await. unwrap (); send_stream. finish ().await. unwrap (); }; tasks. push (task); } while let Some (_) = tasks. next ().await {} この場合の実行結果は以下のようになりました。 listeing on 0.0.0.0:33333 connected: addr=127.0.0.1:33333 connected from 127.0.0.1:42291 closed by peer: 0 Elapsed time: 83 ms 1回あたり0.083msで、すこし短縮されます。 なお、送信するデータを10KiBから100KiBにした場合、1回送信ごとにブロッキングする実装で663ms、非同期で668msと、ほとんど差が無くなります。1ストリームあたりに流す量に応じて使い分けるのが良さそうです。 最後に 今回は、RustとQuinnを使って、大量のデータを送信した場合のパフォーマンスを調査しました。Quinnは、tokioをベースとして実装されているため、tokioやfuturesといったRustの非同期機能と組み合わせて使うことができ、コネクションやストリームを開くのも非常に直感的にできます。今後もこれらの機能を活用しながら、引き続きQUICの利用法を調査していく予定です。
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の3日目を担当します。 研究開発グループでは、TCP/QUIC/UDP などのトランスポートプロトコルの製品適用に向けた検証を行っています。 aptpodの製品は車載などの移動体の組み込み機器もターゲットにしているため、これらのトランスポートプロトコルは4G/5G 上での使用が想定されます。 そこで、5Gを使用して、ネットワークの計測やトランスポートプロトコルを評価しようということになりました。 今回の記事では、5GのネットワークでGoogleのスピードテスト/ping/iperf3などの計測を行った結果を紹介します。 なお、トランスポートプロトコルの評価については、12/15 「5G上でのトランスポートプロトコルの評価」にて投稿する予定です。 計測用機材の紹介 Wi-Fi 接続の影響について 計測場所について 都庁周辺の調査 新宿区立新宿中央公園 新宿住友ビル1F フリースペース 新宿三井ビル前のオープンスペース 渋谷ストリーム 日産スタジアム周辺 計測結果 googleのスピードテスト 計測場所① 計測場所② 計測場所③ pingの計測 iperf3の計測 UDPの結果 TCPの結果 まとめ 計測用機材の紹介 まずは計測用機材を紹介します。 Googleスピードテストは、 Xperia XZ2 SO-03K で計測しています。 pingとiperf3の計測用のクライアントは組み込み製品を想定しRaspberry Pi4を使用しました。pingとiperf3計測用のサーバーは、AWS上でEC2のm5.xlargeのUbuntuインスタンスを使用しました。Raspberry Pi側はubuntu 20.04、サーバー側はubuntu 18.04 を使用しています。 以下のような構成で計測をします。 構成図 5Gへの接続には、Docomoから販売されているモバイルルーター Wi-Fi STATION SH-52A を使用しています。 5G接続時には、画面が青く光り表示が「5G」に代わるため5Gに接続しているのかどうかの判別が可能です。 5G接続時 Raspberry PiとSH-52Aは有線で以下のように接続します。Raspberry Piと赤いケーブルで接続されているのはモバイルバッテリーです。 5G を計測する機器の構成 Wi-Fi STATION SH-52A では、手動で5Gと4Gを選択することはできない 1 ため、4G計測用には モバイルWi-FiルーターL-01G を使用しています。 L-01Gは有線接続がないためRaspberry Piとモバイルルーター間はWi-Fiで接続しました。 4G 計測用機器の構成 Wi-Fi 接続の影響について 公正に評価できるように、Raspberry PiとWi-Fi接続影響について調べました。 まずはRaspberry PiとL-01G間のRTTについてです。 Raspberry PiからL-01Gに対して、1秒間隔で1分間の設定でpingコマンドを実行しました。RTTの値は最小値3.1msec、平均値 7.952msec、最大値26.8msecでした。状況による変動はあると思いますが、今回は4GでpingによるRTTの計測を行うと8msec 程大きい値が出ると想定します。 次に、Wi-Fiの帯域確認のために、Raspberry PiとパソコンをそれぞれL-01G経由で接続しiperf3を使って両者の間の帯域を調べました。 結果は、TCP 31.5Mbps、UDP 32.1Mbpsとなりました。もし4Gの計測を行ったときにこの値で律速した場合は、Wi-Fiの影響があるかもしれません。 計測場所について ドコモが5Gを提供しているエリアは、 サービスエリアマップ から確認することが出来ます。 最初に会社のオフィスの近くにある新宿区の都庁周辺と渋谷ストリームで計測ができないか確認に行きました。下記の画像の赤い部分が5Gが入るといわれている場所です。 都庁周辺の5Gエリアマップ 渋谷ストリーム周辺の5Gエリアマップ 両者ともあまり芳しくなかったため、周辺に大きなビルなどがない日産スタジアムを調査対象に選びました。 日産スタジアム周辺の5Gエリアマップ 都庁周辺の調査 都庁周辺は後述する3か所で確認をしました。5Gが入りますが、すぐに4Gに切り替わってしまい計測を行えるほど安定した接続は難しかったです。 新宿周辺は高いビルなどが多いため、5Gの直進性が高く減衰しやすいという特性による影響があったのかもしれません。 新宿区立新宿中央公園 新宿中央公園はエリアマップの範囲から少し外れていますが、時々5Gが入ることがありました。 新宿中央公園 新宿住友ビル1F フリースペース 新宿住友ビルの1Fのフリースペースは屋内ですが、5Gが入りました。コンクリートではなくガラス張りだったのが影響しているかもしれないです。 新宿三井ビル前のオープンスペース ここは上記の2か所よりは安定して5Gが拾えました。しかし、4Gに切り替わることが多かったため計測には不向きと考えられます。 新宿三井ビル前 渋谷ストリーム 渋谷ストリームはイベントホール内での接続が可能のようです。イベントホールには入れませんでしたが、4Fで一瞬だけ5Gを拾えました。しかしそれ以降5Gは拾えませんでした。 日産スタジアム周辺 都庁周辺・渋谷では厳しそうだったので、日産スタジアムを訪問しました。 日産スタジアム 日産スタジアムの周りは高いビルなどもなく、5Gの接続状況はかなり良かったです。 ということで、日産スタジアム周辺で計測を行いました。 計測結果 googleのスピードテスト スピードテストはgoogleが提供している回線速度計測のサービスです。上りと下りのスループットを計測することが出来ます。 スピードテストはSH-52A、L-01G をXperia XZ2 SO-03KとWi-Fiで接続して行いました。 場所による変化を見るために、スタジアムの東ゲート付近の3か所で計測しています。 計測場所① Smile Table という建物の脇の場所で計測しました。 日産スタジアム計測場所① 5Gの結果は、ダウンロードが76.1Mbps、アップロードが63.4Mbpsでした。 日産スタジアム場所①5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが7.38Mbps、アップロードが18.6Mbpsになりました。 日産スタジアム場所①4Gのスピードテストの結果 計測場所② 次に計測したのは東ゲート前です。 日産スタジアム場所② 5Gの結果は、ダウンロードが50.7Mbps、アップロードが25.1Mbpsでした。 日産スタジアム計測場所② 5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが4.18Mbps、アップロードが17.5Mbpsになりました。 日産スタジアム計測場所② 4Gスピードテストの結果 計測場所③ 3か所目は、東ゲート付近の広場です。 日産スタジアム計測場所③ 5Gの結果は、ダウンロードが92.7Mbps、アップロードが36.7Mbpsでした。 日産スタジアム計測場所③ 5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが16.5Mbps、アップロードが27.3Mbpsになりました。 日産スタジアム計測場所③ 4Gスピードテストの結果 ばらつきはありますが3か所すべてでダウンロード・アップロードともに5Gの方が4Gよりもスループットが高い結果になりました。 pingの計測 pingの計測は、計測場所③で行っています。 pingコマンドを使って1秒間隔で60秒間RTTを計測しました。 Raspberry PiからAWS上のEC2インスタンスまで60秒の間に1秒間隔で計測しています。 ヒストグラムのプロット結果は以下のようになります。(外れ値は除いています) pingの結果 RTTにはばらつきがありますが、傾向として5Gの方が小さい値になっています。 2 また、最小値、平均値、最大値は以下のようになりました。 最小値 平均値 最大値 5G 26.8msec 38.5msec 47.9msec 4G 58.3msec 74.8msec 93.9msec 参考までに、L-01GとのRaspberry Pi間のRTTの平均で8msecを引いてプロットしても、傾向は変わらないと言えそうです。 pingの結果 4GはWi-Fi分8msecを引いた iperf3の計測 iperf3の計測も計測場所③で行っています。 ここでは、UDPとTCP(輻輳制御はBBRを使用)それぞれ20秒間の上りの計測を3回行いました。 UDPの結果 UDPの計測は律速する上限まで帯域を上げてみて計測をしてみました。5Gは50Mbps、4Gは25Mbpsを設定しています。 計測結果は以下のようになりました。 1回目 2回目 3回目 5G 45.2Mbps 47.0Mbps 47.0 Mbps 4G 17.4Mbps 24.6Mbps 25.0 Mbps L-01Gの上限は30Mbpsと思われるので、4GはWi-Fiの接続で律速されているわけではなさそうです。 TCPの結果 TCPの場合はビットレートの指定をしないと上限まで流そうとするので、ビットレートの指定はしません。 計測結果は以下のようになりました。 1回目 2回目 3回目 5G 15.9Mbps 17.8Mbps 34.4 Mbps 4G 9.33Mbps 11.1Mbps 12.3 Mbps 同じ計測場所③で計測したスピードテストとは差分がありますが、これは計測方法の違いが影響している可能性がありそうです。 iperf3の結果からUDP/TCPともに5Gの方が4Gよりもスループットが高いことが期待出来そうです。 まとめ 今回は「5Gのネットワークを計測してみた」について紹介しました。 今回の計測結果から5GによるRTTやスループットの改善には期待が出来そうです。 3 次回は、12/15 に 「5Gでのトランスポートプロトコルの評価」についてご紹介する予定です。 最後までご覧いただきありがとうございました。 この記事を書いているときに気が付きましたが実際には「5G/4G/3G」モードと「4G/3G」モードを選べるようです。12/15 投稿予定の「5G上でのトランスポートプロトコルの評価」の記事は4Gの計測もSH-052Aを利用して計測した結果を載せたいと思います ↩ モバイルルーターを統一した結果、RTTの値はほぼ同じ値になることを確認しました。詳細は次の記事をご確認ください。: 5Gでのトランスポートプロトコルの評価 - aptpod Tech Blog ↩ 2 と同上 ↩
aptpod Advent Calendar 2020 の2日目を担当します、研究開発グループの大久保です。 弊社では、新しいプロトコルであるQUICの利用法を調査しています。そこで今回は、RustのQUIC実装の1つである Quinn を用いて、受け取ったリクエストをそのままクライアントへ返送するechoサーバを実装してみます。RustのQUIC実装には、他に quiche というものもありますが、Quinnは tokio の上に実装されているため、Rustのasync機能を活用して楽に書くことができます。 構成 quinn-echo-server と quinn-echo-client という2つのクレートを作り、それぞれの Cargo.toml に以下の依存関係を追記します。 [dependencies] anyhow = "1" clap = "3.0.0-beta.2" futures = "0.3" quinn = "0.6" tokio = { version = "0.2", features = ["full"] } QUICを使うのに quinn 、async周りの機能のために futures と tokio が必要です。また、エラー処理には、最近おなじみになった anyhow を、CLI引数のパースには clap を入れておきます。 サーバ側 quinn-echo-server は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] port: u16 , #[clap(short, long)] ca: PathBuf, #[clap(long)] privkey: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); // 証明書の設定 let cert = Certificate :: from_der ( & std :: fs :: read (opts.ca)?)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & std :: fs :: read (opts.privkey)?)?, )?; // QUICを開く let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), opts.port); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); // クライアントからの接続を扱う while let Some (conn) = incoming. next ().await { tokio :: spawn (async { // クライアントとの処理を行い、エラーが起きたら表示 match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // echoの処理をする関数 async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); // 受信用のストリームを開く if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; // ストリームを読み出す let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); // 送信用のストリームを開く let mut send_stream = connection. open_uni ().await?; // 返信を書き込む send_stream. write ( & data).await?; send_stream. finish ().await?; connection. close ( 0u8 . into (), & []); } else { bail! ( "cannot open uni stream" ); } println! ( "closed" ); Ok (()) } 下準備と接続を開くまで main 関数で行っています。コマンドライン引数で渡されたポート番号と、証明書ファイルのパスを基にサーバを立ち上げ、クライアントとの接続が起きたら handle_connection 関数に渡します。tokioランタイムの上で動作するので、 main 関数には #[tokio::main] 属性を追加しておきます。 handle_connection 関数では、受信用に単方向ストリームを開き、内容を全て読み出します。その後、送信用の単方向ストリームを開き、受け取った内容をそのまま書き出したら接続を終了します。 クライアント側 quinn-echo-client は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: {Certificate, ClientConfigBuilder, Endpoint, NewConnection}; use std :: net :: SocketAddr; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] ipaddr: SocketAddr, #[clap(short, long)] ca: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & std :: fs :: read ( & opts.ca)?)?)?; let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; // サーバへ接続 let NewConnection { connection, mut uni_streams, .. } = endpoint. connect ( & opts.ipaddr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); // メッセージの書き込み let msg = "hello" ; let mut send_stream = connection. open_uni ().await?; send_stream. write (msg. as_bytes ()).await?; send_stream. finish ().await?; println! ( "sent \" {} \" " , msg); // 返信の読み込み if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); } else { bail! ( "cannot open uni stream" ); } // 終了 endpoint. wait_idle ().await; Ok (()) } main 関数で接続、書き込み、返信の読み込みまで行います。QUICの接続を確立したら、送信用の単方向ストリームを開き、文字列 "hello" を書き込みます。その後、受信用の単方向ストリームを開き、文字列に変換して表示したら終了します。 実行 適当なDER形式の証明書ファイル cert.der と秘密鍵 priv.der を用意した場合、サーバを次のように立ち上げます。 quinn-echo-server --port 33333 --ca cert.der --privkey priv.der このサーバに接続するクライアントは、次のように実行します。 quinn-echo-client --ipaddr 127 . 0 . 0 .1:33333 --ca cert.der すると次のような出力が得られます。 サーバ側 listeing on 0.0.0.0:33333 connected from 127.0.0.1:32820 received "hello" closed クライアント側 connected: addr=127.0.0.1:33333 sent "hello" received "hello" これでechoが返ってくることを確認できました。 最後に 今回は、tokioをベースとして構築されたQuinnをつかって、簡単なQUICのechoサーバを構築してみました。別のQUIC実装であるquicheの方は、自分でイベントを扱うループを書く必要があったりしますが、Quinnの方はRustのasync/await機能の基本が分かっていれば、比較的簡単に使いこなすことができます。コネクションやストリームを開いたり、ストリームの読み書きも、とてもRustらしい書き方で行えます。今回実装したのはechoサーバですが、これでQUICの特徴の1つである ストリームの使い方がわかるので、ここから応用することもできるでしょう。 新しいプロトコルであるQUICも、Rustの知識がある程度あればQuinnで簡単に使えるので、ぜひお試し下さい。
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の1日目を担当します。 今回の記事では、ROSに対応している3つのオープンソースの自動運転シミュレータと弊社製品の intdash を連携させて、自動運転シミュレータの画像やセンサー情報をウェブブラウザ上で可視化してみました。 自動運転シミュレータは、「 LGSVL 」、「 CARLA 」、「 AirSim 」の3つを試してみました。 まずはそれぞれのシミュレータとintdashを連携させた動画をご覧ください。 動画では左に自動運転シミュレータ、右側にウェブブラウザ上で可視化を行う弊社製品の「 Visual M2M Data Visualizer 」を配置しています。 intdash LGSVL連携 youtu.be intdash CARLA連携 youtu.be intdash AirSim連携 youtu.be 動画上では撮影のために自動運転シミュレータとウェブブラウザを単一のPCで実行しています。 実際には、以下の図のように可視化しているセンサー情報や画像はクラウドを経由してウェブブラウザに届いています。 自動運転シミュレータ上の映像とウェブブラウザ上の映像を比較すると、クラウドを経由していますが大きな遅延がないことが確認できると思います。 このように、弊社の製品群を活用することで、ROSに対応した様々な自動運転シミュレータとの連携が実現できます。 ちなみに、今回は可視化を行っていますが、intdashを使うとクラウドにデータを保存することも可能です。保存されたデータをダウンロードして解析するなどの活用も可能です。 ここからはintdashとそれぞれの自動運転シミュレータをどのように連携させたかについて触れたいと思います。 intdashとROSの連携 LGSVLとROSの連携 CARLAとの連携 AirSimとの接続 まとめ intdashとROSの連携 最初に、3つの自動運転シミュレータに共通している部分について説明します。 ROSとintdashを連携させてウェブブラウザ上で可視化を行うまでは3つの自動運転シミュレータに共通しています。 それぞれの自動運転シミュレータ *1 がROSトピックをpublishします。 ここで、発行されたROSトピックのうち画像以外のセンサー情報はintdash bridge という弊社製品のROSノードが、画像はConverter *2 というROSノードがsubscribeします。 ROSトピックをウェブブラウザで可視化するためにintdash bridgeはROSトピックをJSON *3 に、ConverterはROSトピックから画像のバイナリのみを取り出します。 JSONとバイナリのデータはintdash Edge Agentに渡されます。そしてintdash Edge Agent がクラウドにデータをアップロードします。 ウェブブラウザ上のVisual M2M Data Visualizerは、クラウドのintdash ServerからJSONと画像のバイナリを受け取り、可視化します。 このように、intdashを使用して、自動運転シミュレータのセンサー情報や画像をウェブブラウザ上で可視化しています。 LGSVLとROSの連携 LGSVLはLGエレクトロニクス社が開発しているUnityベースの自動運転シミュレータで、自動運転ソフトウェアのAutowareと連携できるシミュレータとしても知られています。 LGSVLをROSと連携させるためには、 rosbridge server が必要になります。 intdashとLGSVLを連携させたときの構成図は以下のようになります。 intdashとLGSVLの連携 LGSVLは内部にrosbridge serverと接続するためのrosbridge clientを内包しています。 rosbridge clientにrosbridge server と接続するための設定をしてからLGSVLを起動するとrosbridge serverに接続します。 rosbridge clientは接続後に、JSON形式の情報をrosbridge serverに渡します。 rosbridge serverはrosbridge clientから受け取ったJSONをROSトピックに変換してpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとLGSVLの連携が可能になります。 CARLAとの連携 CALRAはUnreal Engineベースの自動運転シミュレータで、intel社やToyota Research Instituteなどがスポンサーを行っています。 intdashとCARLAを連携させたときの構成図は以下のようになります。 intdashとCARLAの連携 CARLAはROSと連携するために carla_ros_bridge というROSノードを提供しています。 このcarla_ros_bridgeがCARLAからセンサー情報と画像を取得しROSトピックとしてpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとCARLAの連携が可能になります。 AirSimとの接続 AirSimはUnrealEngineベースのドローンや車の自動運転シミュレータで、Microsoft社が開発しています。 intdashとAirSimを連携させたときの構成図は以下のようになります。 intdashとAirSimの連携 AirSimはROSと連携するために、 airsim_ros_pkgs を提供しています。 airsim_ros_pkgsに含まれる airsim_node というROSノードをROSとの連携のために使います。 airsim_ros_nodeは、AirSimのシミュレータからセンサー情報と画像を取得しROSトピックとしてpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとAirSimの連携が可能になります。 まとめ 今回は「intdashと自動運転シミュレータを連携させてみた」について紹介しました。 このように、弊社の製品群を活用することで、ROSに対応した3つのオープンソースの自動運転シミュレータとの連携が簡単に実現できました。 それぞれのシミュレータとの連携で見ていただいたように、弊社製品部分の構成は変えずにそのまま使用しています *4 。 研究開発グループではROSに限らず、プロトコルや機械学習に関連したテーマなど様々な技術テーマの調査・検証を進めています。今後も継続的に調査・検証の結果を記事として投稿できればと思います。 最後までご覧いただきありがとうございました。 *1 : 実際には後述する通り自動運転シミュレータの情報をpublishするROSのノードがいます。 *2 : 製品ではないですが自作のROSノードです *3 : ROSトピックをバイナリのまま送信することも可能です。現状ではウェブブラウザ上での可視化にはJSONが使われているのでJSONに変換しています。 *4 : subscibeするROSトピック名などの設定ファイルは変更しています
弊社では最近、PDF形式のドキュメントを作成するために Sphinx を使っています。 1 Sphinxを使って、reStructuredTextからPDFを作成する 例えば、弊社では先日AWS Marketplaceにて intdash LE All-in-One をリリースしましたが、そのドキュメントはSphinxで作成しました。 intdash概要 AMIを使ったintdashサーバー構築手順 Sphinxは非常に使いやすいツールですが、本格的に使うようになってから気づいた、使いこなしのコツやノウハウがいくつかあります。 その1つとして今回は、表紙タイトルの折り返しについてご紹介したいと思います。テクニカルライターの篠崎がお届けします。 長い文書タイトルで発生する折り返し 表紙テンプレートをカスタマイズする (準備)表紙のテンプレートを確認する 表紙テンプレートを再定義し、改行コマンドの入ったテキストを挿入する まとめ―Sphinxの柔軟性 長い文書タイトルで発生する折り返し 文書のレイアウトを自動化するとき、表紙も自動で作るか、または、別途手作業で作った表紙をあとで結合するか(例えばAdobe Illustratorで1枚のグラフィックとして作るというのがよくある手法でしょうか)は、判断の分かれるところだと思います。 【後日付記】 この記事の内容は、PDFの表紙のカスタマイズの方法としては有効ですが、表紙タイトルの改行位置を調整するだけであれば、Sphinxプロジェクトの設定ファイル conf.py で latex_documents を設定するほうが簡単です。 latex_documents による設定例: latex_documents = [('index', 'test.tex', r'長い\\タイトルは\\改行できます', author, 'manual')] 設定 latex_documents には、タプルのリストを指定します。タプル内の3番目(インデックス2)の要素で、PDF出力時の表紙タイトルを設定することができます。文字列はそのままLaTeXソースに書き込まれますので、上の例のように \\ を入れると改行することができます。 詳細については、公式ドキュメントの latex_documents についての説明 をご覧ください。 レイアウトを自動化するからには、表紙も自動で作成したいところです。一方、表紙は読者の目に最初に触れるものですから、できるだけ格好よくしたいと考えます。 そこで、バランスのよいレイアウトのテンプレートを作っておいて、決められた位置にテキストや画像を当てはめるのが定番だと思います。 表紙レイアウトのテンプレート例 それでも、大きな文字で長いタイトルを入れると、行末で折り返しが発生し、見栄えが悪くなることがあります。 英語であれば基本的に単語の区切り(スペース)でしか折り返されませんが、日本語では区切りのスペースを入れませんので、良いところで折り返されるとは限りません。 例として、長いタイトルを持つ文書をSphinxで作ってみます。Sphinxでは、プロジェクト設定ファイル conf.py の変数 project の値がPDFの表紙タイトルになります。以下のようにしてみました。 # 最小限の設定をしたconf.py project = 'とても長いタイトルを持ち秋の夜に世界の片隅で作成されたPDF出力テストのためのドキュメント' author = 'test-author' language = 'ja' これでPDFを生成すると以下のようになりました。 行末で折り返された長いタイトル あまり格好よくないですね。 Sphinxを使い始めたころは、PDF出力後に編集ツールを使って手作業で改行を入れていたのですが、毎回このような修正をするのは大変です。 Acrobatで手動で改行を入れる やはり、自動でレイアウトしているのだから、できるだけ自動化したいですよね。 そこで以下のような方法を採ることにしました。方法はいろいろあると思いますので、一例としてお読みください。 使用した環境は以下の通りです。 Windows 10 Sphinx v3.3.1 TeX Live 2020(pLaTeXとdvipdfmxを使用) 表紙テンプレートをカスタマイズする 適切なレイアウトを実現するため、表紙テンプレートを新しく作ります。そこに、折り返し位置を指定したテキストを埋め込むことにします。 2 なお「表紙テンプレート」と呼んでいるものは、実体はLaTeXコマンドです。 以下の(A)~(C)を行います。 タイトル内の適切な折り返し位置にLaTeXの改行コマンド \\ を入れておく(A) 表紙テンプレートを再定義する(B) 表紙テンプレート(B)には、改行コマンドが入ったタイトル(A)を挿入する(C) (準備)表紙のテンプレートを確認する 先まわりして、 (B) の表紙テンプレートから考えてみます。 SphinxでPDFを出力する際に使用されるデフォルトの表紙テンプレートは、 \sphinxmaketitle というLaTeXコマンドです。 Sphinxソースの中の sphinx/texinputs/sphinxmanual.cls にあります。 \newcommand { \sphinxmaketitle }{ % \let\sphinxrestorepageanchorsetting\relax \ifHy @pageanchor \def\sphinxrestorepageanchorsetting { \Hy @pageanchortrue } \fi \hypersetup { pageanchor = false } % avoid duplicate destination warnings \begin { titlepage } % \let \footnotesize\small \let\footnoterule\relax \noindent\rule { \textwidth }{ 1pt } \par \begingroup % for PDF information dictionary \def\endgraf { } \def\and {\& } % \pdfstringdefDisableCommands { \def \\{ , }} % overwrite hyperref setup \hypersetup { pdfauthor = { \@author } , pdftitle = { \@title }} % \endgroup \begin { flushright } % \sphinxlogo \py @HeaderFamily { \Huge \@title \par } % <--- ここで、タイトル\@titleが出力されています ... \end { flushright } %\par ... \end { titlepage } % ... } タイトルの文字列は最終的に \@title コマンドにバインドされ、このテンプレートを使って出力されることが分かります。 表紙テンプレートを再定義し、改行コマンドの入ったテキストを挿入する そこで、デフォルトのテンプレートに少しだけ変更を加えて、改行コマンドの入ったタイトルを出力できるようにします。 Sphinxのたくさんのパラメーターの1つ latex_elements で preamble に文字列を設定すると、その文字列はLaTeXファイルのプリアンブルに書き出されます。この仕組みを利用して、テンプレートを再定義( \renewcommand )します (B) 。詳細についてはこの下のコード例をご覧ください。 また、タイトルには折り返し位置を指定するための改行コマンドを入れます (A) 。設定ファイル conf.py はPythonコードであるため、設定ファイル内で文字列操作も行うことができます。これを表紙テンプレートに挿入します (C) 。 # タイトルの折り返し位置を指定したconf.py author = 'test-author' # 変更なし language = 'ja' # 変更なし # タイトルは分割された状態で用意 document_title_lines = [ 'とても長いタイトルを持ち' , '秋の夜に世界の片隅で作成された' , 'PDF出力テストのためのドキュメント' ] # 改行コマンド `\\` を挟んで連結する(A) # バックスラッシュが4つなのは、エスケープのため my_latex_title_lines = ' \\\\ ' .join(document_title_lines) # => とても長いタイトルを持ち\\秋の夜に世界の片隅で作成された\\PDF出力テストのためのドキュメント # LaTeX出力の設定 latex_elements = { 'preamble' : r''' % my_latex_title_linesをLaTeXの世界に持ち込む \newcommand{\mylatextitlelines}{''' + my_latex_title_lines + r'''} % 表紙テンプレート内でアットマークが使われているため、アットマークを通常の文字として扱う \makeatletter % 表紙テンプレートを再定義(B) \renewcommand{\sphinxmaketitle}{% \let\sphinxrestorepageanchorsetting\relax \ifHy@pageanchor\def\sphinxrestorepageanchorsetting{\Hy@pageanchortrue}\fi \hypersetup{pageanchor=false}% avoid duplicate destination warnings \begin{titlepage}% \let\footnotesize\small \let\footnoterule\relax \noindent\rule{\textwidth}{1pt}\par \begingroup % for PDF information dictionary \def\endgraf{ }\def\and{\& }% \pdfstringdefDisableCommands{\def\\{, }}% overwrite hyperref setup \hypersetup{pdfauthor={\@author}, pdftitle={\@title}}% \endgroup \begin{flushright}% \sphinxlogo \py@HeaderFamily {\Huge \mylatextitlelines \par} % <--- ここで\mylatextitlelinesを使用(C) {\itshape\LARGE \py@release\releaseinfo \par} \vfill {\LARGE \begin{tabular}[t]{c} \@author \end{tabular}\kern-\tabcolsep \par} \vfill\vfill {\large \@date \par \vfill \py@authoraddress \par }% \end{flushright}%\par \@thanks \end{titlepage}% \setcounter{footnote}{0}% \let\thanks\relax\let\maketitle\relax %\gdef\@thanks{}\gdef\@author{}\gdef\@title{} \clearpage \ifdefined\sphinxbackoftitlepage\sphinxbackoftitlepage\fi \if@openright\cleardoublepage\else\clearpage\fi \sphinxrestorepageanchorsetting } % 表紙スタイル終わり % アットマークを特殊文字に戻す \makeatother ''' } # 変数`project`は、各行をそのまま連結したもの # PDFのメタ情報には正しいタイトルが入る project = '' .join(document_title_lines) # => とても長いタイトルを持ち秋の夜に世界の片隅で作成されたPDF出力テストのためのドキュメント これで、PDFを出力してみます。 指定どおりに折り返されたタイトル 指定した位置で折り返されています。これで、PDF生成のたびに手作業で折り返し位置を修正する必要はなくなりました。 3 新しい表紙テンプレート \sphinxmaketitle は、他のドキュメントにも使いまわしができます。 この説明ではテンプレートを conf.py に直接書き込みましたが、LaTeXのスタイルファイル(.sty)として分離することで、より使いまわしを楽にすることも可能です。 まとめ―Sphinxの柔軟性 Sphinxは非常に柔軟にカスタマイズできるツールです。柔軟性を高めている特徴として、以下があります: たくさんの パラメーター が変更可能であること(しかも、痒い所に手が届くパラメーターが存在すること) レイアウトのテンプレートがモジュール化されており、それぞれ再定義可能であること 設定ファイル conf.py がPythonコードであり、ドキュメント生成時に評価されるものであること 今回の例ではこれらの特徴を活用し、パラメーター内で新たにテンプレートを定義しました。また、改行コマンドを挿入するという単純な操作ではありますが、PythonでLaTeXコードの断片を生成しました。Sphinxの持つ柔軟性の一端をご紹介できたかと思います。 今後もうまく使いこなしたいと考えています。 Sphinxを採用した背景や、LuaLaTeXによる日本語PDFの作成方法については、以前のエントリー SphinxとLuaLaTeXで、日本語PDFマニュアルを作る - aptpod Tech Blog にて公開しています。 ↩ 折り返し位置を指定するために、変数 project にLaTeXの改行コマンド \\ を挿入し、 project = 'とても長いタイトルを持ち\\\\秋の夜に世界の片隅で作成された\\\\PDF出力テストのためのドキュメント' のようにしてもうまくいきません。Sphinxの処理のなかでバックスラッシュがエスケープされるためです。 ↩ この新しい表紙テンプレートのコードは、 { 、 } 、 $ 、 % のようなLaTeXの特殊文字を最初から含むタイトルには対応していません。 ↩
はじめに こんにちは、SRE チームの柏崎です。 先日、 intdash が AWS Marketplace にて提供開始 されました。 これを期に、サーバサイドアプリをパッケージングするために、 fpm というツールを使う機会がありました。 すっかりコンテナな世の中には地味めな話題ですが、今回は fpm について、rpm パッケージの作成例を交えながら紹介したいと思います。 経緯 弊社では、intdash を組み合わせた PoC プロジェクトが多くあり、製品である intdash 自体のカスタマイズがよく行われています。 その昔、intdash のサーバサイドアプリはモノリシックアーキテクチャだったため、カスタマイズのために製品本体に手を入れなければならず、派生製品が乱立していました。 故にデプロイにおいては、派生製品ごとの実行バイナリを管理する手間を減らすため、「都度サーバ上でビルドを行う」という、ちょっと斜め上の方式をとっていました。せっかく Go で作っているのにもったいないですね…。 現在ではマイクロサービスアーキテクチャへの移行が進み、製品に手を入れずに柔軟なカスタマイズが可能な構成になっていますが、デプロイは従来の方式を踏襲してしまっており、下記のようなたくさんの課題を抱えています。 Ansible タスクでビルドしているので、冪等性があいまい Ansible の実行が長時間になりがち サーバがそれぞれビルドのためのソースを持つので、ディスクスペースが無駄 お客様管理のサーバへのデプロイも、アプトポッドエンジニアがやらなければならない そんなとき intdash の AMI 公開の話が持ち上がり、「こんなダサい方式でデプロイされた AMI を晒すのは恥ずかしい」と重い腰を上げたのでした。古いバージョンの利用者にアップデート手段を提供しないといけないですしね。 fpm とは? 目的 Linux (に限らずですが) のほとんどのディストリビューションはパッケージ管理システムを利用しています。 RHEL 系で rpm 形式を扱う yum、Debian 系で deb 形式を扱う apt がメジャーですね。 パッケージ管理システムによって、利用者はソフトウェア一式の追加削除や、依存関係の解決などを手軽に行うことができます。 便利なパッケージ管理システムですが、パッケージの作成者にとってはちょっと大変です。 様々な形式に対応するために、パッケージ作成手順を形式ごとに学習していかなければなりません。 rpm ひとつとっても、spec ファイルの複雑な書式や rpmbuild コマンドの使い方など、覚える事が多いです。 fpm は、覚えなければならない事を極力省き、シンプルに様々な形式のパッケージを作ることを目的として作られています。 どうやって動くのか fpm は、様々な形式から入力と出力を指定すると、その間の変換を行ってくれます。 README にも書いてありますが、様々な入力形式・出力形式に対応しています。 npm モジュールから rpm を作ったり、deb から rpm を作ったり、色々な用途が思いつきますね。 使いかた それでは、実際にパッケージの作成を行いながら、使い方を見ていきましょう。 入力形式として dir を、出力形式として rpm を使い、intdash の認証認可を担当するマイクロサービス「auth」の rpm パッケージを作ります。 構成ファイルの準備 入力形式 dir は、ディレクトリ配下に配置されたファイル一式をパッケージの構成ファイルとして扱ってくれます。 必要なファイルを用意し、ディレクトリ buildroot 配下に配置します。 buildroot/ usr/ bin/ authd # 実行バイナリ lib/ systemd/ system/ intdash-service-auth.service # systemd ユニットファイル share/ doc/ intdash-service-auth-1.6.0/ copyright # コピーライトファイルなど ... etc/ intdash/ authd.conf # 設定ファイル また、出力形式 rpm は、ヘルパユーティリティとしてインストール時やアンインストール時に実行されるスクリプトを指定できます。 これらをディレクトリ rpm_helper 配下に配置します。 rpm_helper/ pre.sh # インストール前に実行される post.sh # インストール後に実行される preun.sh # アンインストール前に実行される postun.sh # アンインストール後に実行される 例として、よくある rpm のヘルパユーティリティを以下に書いておきます。 サービスの動作に必要なユーザの作成や systemd 関連の操作などを行っています。 ### rpm_helper/pre.sh getent group intdash >/dev/null 2>&1 || \ groupadd -r intdash getent passwd intdash >/dev/null 2>&1 || \ useradd -r -g intdash -d /var/lib/intdash -s /sbin/nologin intdash exit 0 ### rpm_helper/post.sh if [ $1 -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 ||: fi ### rpm_helper/preun.sh if [ $1 -eq 0 ]; then systemctl --no-reload disable intdash-service-auth.service >/dev/null 2>&1 ||: systemctl stop intdash-service-auth.service >/dev/null 2>&1 ||: fi ### rpm_helper/postun.sh if [ $1 -eq 0 ]; then systemctl daemon-reload >/dev/null 2>&1 ||: fi 準備はこれだけです。 スクリプト周りで rpm のちょっとした知識は必要ですが、簡単ですね! パッケージング それでは、いざパッケージングをしていきましょう。 コマンド一発です。 $ fpm \ --output-type rpm \ --input-type dir \ --chdir ./buildroot \ --name intdash-service-auth \ --version 1.6.0 \ --iteration 1 \ --architecture x86_64 \ --license Unspecified \ --maintainer product-support@aptpod.co.jp \ --vendor "aptpod, Inc." \ --url https://www.aptpod.co.jp/ \ --rpm-summary "intdash Auth Service" \ --description "This package contains the intdash Auth Service." \ --rpm-os linux \ --depends shadow-utils \ --depends systemd \ --before-install ./rpm_helper/pre.sh \ --after-install ./rpm_helper/post.sh \ --before-remove ./rpm_helper/preun.sh \ --after-remove ./rpm_helper/postun.sh \ --directories /etc/intdash \ --directories /usr/share/doc/intdash-service-auth-1.6.0 \ --config-files /etc/intdash/authd.conf {:timestamp=>"2020-11-05T08:27:27.428159+0000", :message=>"Created package", :path=>"intdash-service-auth-1.6.0-1.x86_64.rpm"} ポイントをいくつか書いておきます。 アンインストール時にディレクトリが残ってしまわないように、 --directories オプションを忘れないようにしましょう。 --config-files を使って特定のファイルが設定ファイルであることを明示しておくと、編集済みの設定ファイルがパッケージアップデート時に上書きされたり、アンインストール時に削除されたりするのを防ぐことができます。 --iteration では、 1.6.0-1 の 1 ようなバージョンの後ろに付く文字列を指定できます。 パッケージ自体の更新で 2 3 と増やしたり、プレリリースバージョンにて 0.1.rc1 0.2.rc2 のように付与することで、パッケージ管理システムにより適切に新旧バージョンの比較が行われます。 以上で、rpm が出来上がりました。 お手軽ですね! おわりに fpm について、実例を交えながら紹介しました。 AMI は無事、恥ずかしくない状態で公開することができました。 引き続き、社内へのデプロイへの適用など、質の高い構築運用に活かしていくつもりです。 ここでは rpm の作成例を紹介しましたが、ほとんどの内容は deb など他の形式にも使い回せます。 パッケージングでお悩みの方の参考になれば幸いです。
はじめに ハードウェアグループのおおひらです。 所謂コロナ禍と呼ばれる状況になってはや半年が過ぎましたが、ハードウェア設計に携わっている皆様はいかがお過ごしでしょうか。 弊社では今年の2月中旬から原則リモートワークの勤務態勢になり、緊急事態宣言が終了したあともオフィスの出社人数を制限するよう3密を回避する対策が続けられています。ハードウェアグループのメンバーも自宅と会社での作業のバランスをとりつつ業務にあたっています。 私自身、各種計測器や試作基板を自宅に持ち帰ったり、前々から個人で購入しようとしていた機材を揃えたりして自宅の作業環境を充実させております。最近では協力会社や商社の方々との打合せがWeb会議で行われることも多く、意図的に自室の作業デスクがカメラに写り込むようにして会話ネタの提供に一役買ったり…😊 さて、与太話はこれぐらいにして、本記事ではハードウェア製品を商品化するうえで地味で大変だけれど避けては通れない、 部品表の管理 について書きたいと思います。 はじめに 部品表の管理は大変という話 OpenBOMの紹介 できること ライセンス形態 使ってみた おわりに 部品表の管理は大変という話 部品表 : BOM (Bills of Materials) という言葉は、製造業に従事されている方には馴染み深いと思います。ハードウェア製品を構成する部品の一覧のことで、各部品の属性として例えば下記のようなものが挙げられます。 品名 各部品の部品番号 (型番/型式) メーカー名 メーカー型番 リファレンス番号 単価 数量 (員数) 環境規制情報 (RoHS) 図面の情報・リンク etc... この部品表を元に調達・発注作業をしたり、また、各種製品コンプライアンスを遵守するために部品変更に対するプロセスを定めて情報追跡を行ったりします。 例えば米国仕向けのためにFCCの認証を取ったり、欧州仕向けのためにCEマーキングの自己宣言をしたり、更には車載機器としての欧州認証であるECE Regulation No.10 (通称Eマーク)の認証を取ったりと、各国と地域で適切な法規制を遵守する必要がありますよね。(弊社でも絶賛試験中です) 製品の出荷時点でこのような認証を受けることができたとしても、その後の部品の生産中止や仕様変更によって製品のスペックや構成物質が変わってしまうことは避けられません。こういった品質と製品コンプライアンスの管理・対応のために多くのメーカーでは4M変更プロセスが定められており、その活動の軸になる重要な要素が部品表と言えます。 品質管理における「4M」とは?効果を発揮するための変更管理 | デジタルトランスフォーメーション チャンネル 実際に特定の製品の部品表を作成する際には、そのフォーマットとしてスプレッドシート (エクセル)を使われる方が多いのではないかと思います。電気部品や半導体部品を実装した基板だけの製品を出荷するのであれば回路CADと統合されている専用のBOM管理ツールを使うことも考えられるのですが、多くの製品はメカ部品や副資材、シールや梱包箱、取扱説明書(紙)など、様々なマテリアルから成る部品の階層構造を持ちますので汎用的なフォーマットで作業できたほうが望ましいですよね。 しかしこれには色々と問題があって、 階層構造を作りづらい バージョン管理できない 横展開が面倒 (ある製品の一部品を、他の製品で流用しようとしたときに頑張ってコピペする) などなど、手作業および謎のエクセルマクロを含み、誰も管理できない秘伝のタレみたいなファイルが出来上がる…という未来が容易に想像できます。 前職で謎の社内イントラシステムに苦しめられた経験もあり(←製造業の方なら同意してくれる人も多いはず)、スプレッドシートで管理したくないな…と思っていろいろ調べた結果、OpenBOMというクラウドベースの部品表管理サービスに行き着きました。 OpenBOMの紹介 www.openbom.com ぱっと見て良い感じですね。 できること 上記のページにも書かれていますが、特徴は以下のとおりです。 スプレッドシートのように直感的に作業できる 画像や3D CAD(Autodesk, Solidworks)のデータを取り込んで各部品をビジュアルで見ることができる 複数人で同時にBOM編集できる ERPシステム(Netsuite)などとの統合 リビジョン管理できる 在庫管理できる クラウドサービスとしての基本的な機能はもちろん、在庫管理や経費との連携もできるということです。 ライセンス形態 www.openbom.com 個人ユーザーであれば無償、もしくは月25ドルのプロフェッショナルユーザーのライセンスがあります。違いは階層ごとのコスト計算機能の有無や、ベンダー管理機能の有無といったところでしょうか。 チームであれば月125ドルから。 企業ライセンスは月375ドルで、部品番号を割り振ったり、4M変更に対応しようとするとこのライセンス契約が妥当かなと思います。 www.openbom.com 使ってみた 企業ライセンスで14日間の無償トライアルをしてみました。 基本的にはカタログを作成して、そのカタログをもとに各製品単位のBOMを作っていく仕組みになっています。カタログというのは、例えば電気部品だったり、半導体部品だったり、または機構部品(板金)、機構部品(ネジ)といった区分の単位で整理される部品のリストのことです。これをベースに各製品のBOMを組立てていくことで、各部品の再利用がしやすくなります。 カタログとBOMの関係 個人的にGood!と感じたところは以下のとおりです。 電気CAD等、外部のCADの部品表からインポート可能 企業ライセンスだと各部品に自動的にユニークな部品番号を振ってくれる機能がある 部品が画像として可視化されていて直感的 部品の属性としてベンダーを紐付けられ、購買・発注時に複数のベンダーを比較することができる BOMの階層ごとにコスト算出できる 在庫管理ができ、試作支給部品や量産部品の管理が楽。営業・管理側と開発側との連携のハードルが下がる。 月375ドルでこれであれば使いたい…ということでこれから稟議申請書を書こうと思います。 おわりに クラウドベースのBOM管理ツール:OpenBOMを紹介させていただきました。 弊社のように小規模な組織では専任の調達/購買メンバーがいないこともあると思います。我々エンジニアとしては、OpenBOMのようなツールを効果的に使って業務効率化を進めていけると本来やりたい開発・設計業務に集中できて幸せになれるのではないでしょうか。 ※ ちなみに無償トライアルに申し込むとCEOのOlegさんから営業メールがバンバン飛んできます^^; 営業熱心でスゴイ。。。
エンベデッドチーム 久保田です。 開発環境をWSL2 (Windows Subsystem for Linux)へ移行しました。 タイミングよく、「WSL2でUSBデバイスを使ってみよう」という記事が出回っていたので、 aptpod CAN-USB Interface (AP-CT2A) もWSL2で動かせるのではないかという期待から intdash Edge Agent 含めて動作環境を整えてみましたのでご紹介します。 元記事 Adding USB support to WSL2 https://github.com/rpasek/usbip-wsl2-instructions USB support to WSL2 http://ktkr3d.github.io/2020/07/06/USB-support-to-WSL2/ intdash Edge Agent とは intdash Edge Agentは、intdash Edgeの基本機能を提供するエッジデバイス用のエージェントソフトウェアです。ユーザーは、接続するデバイスに応じたプラグインであるDevice Connectorを実装するだけで、様々なデバイスをintdashに接続することができます。このソフトウェアを使用すれば、intdash Edge が提供する自動再送、データ流量の制御といった、エッジ側の基本機能を利用したクライアントアプリケーションを、最小限の追加実装で開発することが可能となります。 aptpod CAN-USB Interface - AP-CT2A とは CANバスをターミナルアプライアンスに接続し、CAN、J1939などの制御信号を取得するためのインターフェイスデバイス 分岐線を使用して同時に2チャンネルのCANバスに接続可能 CANデータに対する高精度なハードウェアタイムスタンピングが可能 クロックの共有機構により、複数のCAN-USB Interface間のタイムスタンプを同期 外部計測装置などへの同期信号の出力が可能 動作構成 動作構成 WSL2 (Ubuntu 20.04)には、intdash Edge Agent, AP-CT2A device driverをbuild, installします。 WSL2は現時点(2020/10)で、残念ながらUSBデバイスの直接接続をサポートしていませんので、USB over IP経由でWindows側のAP-CT2Aを接続してみます。 WSL2とは WSL2は、Linux 用 Windows サブシステムが Windows 上で Linux バイナリを実行できるようにしたLinux 用 Windows サブシステム アーキテクチャの新しいバージョンです。 WSL1との違いは、ファイル システムのパフォーマンスを向上すること、システム コールの完全な互換性を追加することです。 WSL2 の初期リリースでは、ハードウェア アクセスのサポートが制限され、GPU、シリアル、USB デバイスに直接アクセスすることはできません。 図. WSL1 と WSL2 の命令実行の比較 「WSLのアーキテクチャ https://roy-n-roy.github.io/Windows/WSL%EF%BC%86%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A/Architecture/ 」から USB over IPとは USB over IPは、USBコマンドをIPパケットによってカプセル化するデバイス制御方法です。 USB/IP Design 「USB/IP PROJECT http://usbip.sourceforge.net/ 」から 手順 Windows側 USB/IPのstubドライバを配布サイト ( https://github.com/cezanne/usbip-win ) からダウンロードします。 https://github.com/cezanne/usbip-win/releases/tag/v0.1.0 PS C:\usbip-win\0.1.0> dir ディレクトリ: C:\usbip-win\0.1.0 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2020/07/11 14:05 551472 usb.ids -a---- 2020/07/11 14:05 1434112 usbip.exe -a---- 2020/07/11 14:07 1364992 usbipd.exe -a---- 2020/07/11 14:05 762 usbip_stub.cer -a---- 2020/07/11 14:05 1951 usbip_stub.inx -a---- 2020/07/11 14:07 58384 usbip_stub.sys -a---- 2020/07/11 14:05 2486 usbip_test.pfx -a---- 2020/07/11 14:05 2316 usbip_vhci.cat -a---- 2020/07/11 14:05 762 usbip_vhci.cer -a---- 2020/07/11 14:05 1960 usbip_vhci.inf -a---- 2020/07/11 14:05 87344 usbip_vhci.sys バイナリをダウンロードしたディレクトリでstubドライバをインストールします。 PS C:\usbip-win\0.1.0> .\usbip.exe install AP-CT2AをWindowsのUSBポートに接続すると、usbipコマンドでUSB接続が表示されます。 PS C:\usbip-win\0.1.0> .\usbip.exe list -l - busid 1-123 (32b2:0100) unknown vendor : unknown product (32b2:0100) <= これ - busid 1-138 (8087:0a2b) Intel Corp. : unknown product (8087:0a2b) - busid 1-99 (093a:2510) Pixart Imaging, Inc. : Optical Mouse (093a:2510) - busid 1-236 (5986:111c) Acer, Inc : unknown product (5986:111c) usbipコマンドでUSB/IPの接続登録を実行します。 PS C:\usbip-win\0.1.0> .\usbip.exe bind -b 1-123 usbip: info: bind_device: bind device on busid 1-123: complete usbipdコマンドでWSL2側からの接続待ちにします。 PS C:\usbip-win\0.1.0> .\usbipd.exe -d -4 usbipd: info: starting usbipd (usbip 1.0.0) usbip: debug: C:\work\usbip-win\userspace\src\usbipd\usbipd_sock.c:38:[build_sockfd] opening 0.0.0.0:3240 usbip: info: listening on 0.0.0.0:3240 WSL2側 Windows側で待ち受けているポートへ接続します。 $ sudo usbip attach --remote=192.168.1.8 --busid=1-123 接続が完了すると、WSL2側にAP-CT2Aが現れます $ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 002: ID 32b2:0100 aptpod, Inc. AP-CT2A Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 動作結果 動作結果 CANを流してみると、無事受信できました。 USB/IP経由することでCPU負荷が大きくなるかと考えていましたが、PCがCore i7なので余裕でした。 ・・・が、何かのきっかけで受信が0になります。エラーとなるわけではないので、USB/IPドライバの不具合かと。正常に動かすには調整が必要なようです。 まとめ WSL2でのintdash Edge Agentの動作手順を簡単に紹介しました。 結果AP-CT2Aは正常に継続動作しませんでしたが、intdash Edge Agentの動作は問題ありません。 CPUがパワフルなので、AP-CT2AのWindowsネイティブドライバがあれば、CANデータをTCPで転送してDevice Connectorで回収する手段も取れなくはなさそうです。 WSL2のUSBデバイス正式対応に期待しましょう。 参考 (WSL2の導入) WSL2導入|WinアップデートからWSL2を既定にするまでのスクショ https://qiita.com/tomokei5634/items/27504849bb4353d8fef8 WSL2におけるVcXsrvの設定 https://qiita.com/ryoi084/items/0dff11134592d0bb895c WSL2によるホストのメモリ枯渇を防ぐための暫定対処 https://qiita.com/yoichiwo7/items/e3e13b6fe2f32c4c6120
研究開発グループの大久保です。 当社の製品の中にはC/C++で書かれたものが存在し、その中には独自のバイナリフォーマットを取り扱うものが存在します。既存のコードとやり取りするようなRustのプロジェクトを起こすためには、その独自のバイナリフォーマットをRustで取り扱えるようにしなければなりません。しかしながら、Rustの標準ライブラリの機能だけでは、バイナリの読み書きは意外と面倒になります。そのため、今回はRustでバイナリを扱うのならぜひ知っておきたいクレートを3つご紹介します。 byteorder byteorder はその名の通り、バイトオーダ、つまりエンディアンを扱うためのクレートです。使い方はシンプルで、 ByteOrder トレイトと、 BigEndian, LittleEndian, NativeEndian のうち自分が扱いたいエンディアンをインポートすれば、バッファと数値型の間で読み書きを行うことができます。 例えば、長さ4バイトのバッファから32bit整数型を読み出す場合、次のようになります。 use byteorder :: {BigEndian, LittleEndian, NativeEndian, ByteOrder}; fn main () { let buf = [ 0 , 0 , 0 , 42 ]; let a = LittleEndian :: read_u32 ( & buf); assert_eq! (a, 704643072 ); let a = BigEndian :: read_u32 ( & buf); assert_eq! (a, 42 ); let a = NativeEndian :: read_u32 ( & buf); assert_eq! (a, 704643072 ); } 32bit整数型の書き込みは次のようになります。 use byteorder :: {BigEndian, LittleEndian, NativeEndian, ByteOrder}; fn main () { let mut buf = [ 0 ; 4 ]; LittleEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 42 , 0 , 0 , 0 ]); BigEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 0 , 0 , 0 , 42 ]); NativeEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 42 , 0 , 0 , 0 ]); } NativeEndian はこれを実行しているプラットフォームのエンディアンを示します。 bytes bytes は、Rust用の非同期ライブラリ tokio で使われているバイナリ操作用のクレートです。 Buf と BufMut というトレイトを導入することで、バイナリ読み書きのためのメソッドを利用することができます。 例として、用意したデータの先頭から順に整数を読み出していきます。 use bytes :: Buf; fn main () { let data = [ b'a' , 0 , 33 , 42 , 0 ]; let mut p = & data[..]; assert_eq! (p. get_u8 (), b'a' ); // 0バイト目を8bit整数として読み出し assert_eq! (p. get_u16 (), 33 ); // 1〜2バイト目をビッグエンディアン16bit整数として読み出し assert_eq! (p. get_u16_le (), 42 ); // 3〜4バイト目をリトルエンディアン16bit整数として読み出し } Vec に順番に整数を書き込んでいくこともできます。 use bytes :: BufMut; fn main () { let mut buf = Vec :: new (); buf. put_u8 ( b'r' ); // 8bit整数を書き込み buf. put_u8 ( b'u' ); buf. put_u8 ( b's' ); buf. put_u8 ( b't' ); buf. put_u16 ( 0xFFEE ); // ビッグエンディアンとして16bit整数を書き込み buf. put_u16_le ( 0x1122 ); // リトルエンディアンとして16bit整数を書き込み assert_eq! (buf, [ b'r' , b'u' , b's' , b't' , 0xFF , 0xEE , 0x22 , 0x11 ]); } 読み書きどちらも関数名の後ろに _le を付けるとリトルエンディアン扱いになります。バッファの先頭から読み書きしていくようなデータ構造の場合、bytesはなかなか便利なクレートと言えるでしょう。 nom nom はRust用のパーサコンビネータライブラリです。nomが提供するパース用の関数を組み合わせて、対象となるフォーマット用のパーサを作り上げるようにして使います。nomのexampleに示されているのは、テキスト( &str )のパースですが、バイナリ( &[u8] )のパースにも使えます。 例えば、先頭から順にバイナリを読んでいき、結果を MyData 構造体に格納していく場合は次のようになります。 use nom :: IResult; use nom :: number :: complete :: {be_u8, be_u32}; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; Ok ((input, MyData { a, b, c, d })) } fn main () { let data = [ 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , } ); } 基本的にnomにおけるパーサ関数は、パースしたい領域のスライスを受け取り、読み残しのスライスと読み取った結果のタプルを返します。そのため、返り値のスライスを読み取れば、先頭から順に値を読み込んでいくことができます。 先頭から順に読んでいくだけならbytesでも可能ですが、nomの関数を使えば複雑な構造のバイナリを読み取ることも可能です。例えば、先頭に mydata というマジックナンバーがついているバイナリをパースしたい場合は、 tag を使うことができます。 use nom :: bytes :: complete :: tag; use nom :: number :: complete :: {be_u32, be_u8}; use nom :: IResult; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, _) = tag ( b"mydata" )(input)?; // inputの先頭6バイトが"mydata"かどうか確かめる let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; Ok ((input, MyData { a, b, c, d })) } fn main () { let data = [ b'm' , b'y' , b'd' , b'a' , b't' , b'a' , 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 , ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , } ); } また、このデータの末尾に、ヌル終端のASCII文字列が格納されていた場合を考えてみます。この場合、 take_until を使うことで、0が現れるまでのスライスを取得することができます。 use nom :: bytes :: complete :: {tag, take_until}; use nom :: number :: complete :: {be_u32, be_u8}; use nom :: IResult; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , id: Vec < u8 > , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, _) = tag ( b"mydata" )(input)?; let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; let (input, id) = take_until ( & b" \0 " [..])(input)?; // ヌル文字が現れるまでのデータを取得 Ok (( input, MyData { a, b, c, d, id: id. to_vec (), }, )) } fn main () { let data = [ b'm' , b'y' , b'd' , b'a' , b't' , b'a' , 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 , b'a' , b'b' , b'c' , b'd' , b'e' , b'f' , 0 , ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , id: b"abcdef" . to_vec (), } ); } 他にもnomにはいろいろな機能が用意されていますので、うまく使えばもっと多様なフォーマットにも対応できます。 最後に Rustでちょっと凝ったことをすると、標準ライブラリ以外のクレートが必要になることが多く、適したクレートを探すのは少し大変です。そのため、今回はバイナリを扱う場合に必須になりそうなクレートをご紹介しました。Rustはその適用範囲上、バイナリを扱うことも多いかと思いますので、この記事がご参考になれば幸いです。
はじめに こんにちは、製品開発グループの落合です。主に エッジサイドミドルウェア(intdash Edge) の開発を担当しています。このintdash EdgeはC++で作成しているのですが、言語が何であろうと「面倒な事は自動化したい」ですよね。そして、特に面倒なのは「テスト」じゃないでしょうか? そんな訳で、intdash Edgeのプロジェクトで使用している「CIでの動的テスト」を紹介させて頂こうと思います。「CIでテストなんて当たり前でしょ」と言われる気もしますが、 clang の sanitizer を使っている記事は意外と少ない気がするので今回記事にしてみました。 え、なんで valgrind ではなく sanitizer を使っているかですか?単純に検知できるエラーが多いのが理由です。 はじめに CIで行っているテスト 開発環境の準備 動的テスト Address Sanitizer Undefined Behavior Memory Sanitizer Coverage コーディングスタイルチェック スタイルの規定 フォーマッターの実行 まとめ CIで行っているテスト intdash Edgeのプロジェクトで行っているCIのテストは主に下記3つです。 コーディングスタイルチェック(clang-format) 静的解析(CppCheck) 動的テスト(calng sanitizer)← 今回の記事の主題 それぞれの費用対効果を(完全に主観で)表すと、こんな感じです。 テスト項目 バグの検知 コスト感と効果 スタイルチェック × フォーマッターが自動で行ってくれる。 予めチームでのフォーマットを決めておくことで、レビューなどでの非生産的な論争を避けられる。 静的テスト △~○ テストコードを書かなくても良い。 実装した関数が仕様通りに動くかのチェックはできない。 見つかるバグはツールの性能に大きく左右される。 動的テスト ◎ テストコードが必要。 動かしたコードに対して未初期化・メモリ関連のチェックができる。 継続的にメンテするプロジェクトなら、断然ユニットテストを書いて動的テストまで取り入れるべきだと思います。 なぜなら、ユニットテストをCIに取り入れることで、テスト対象が仕様通りに動作することを保証できるので、簡単にデグレを抑止できます。 さらに、動的テストを足す事で、テストで動かしたコードのメモリエラーをチェックできます。 さらにさらに、カバレッジも出力すればテストケースの考慮漏れも防げます。 では、本題のclangのscanitizerで動的テストを行ってみましょう。。。と行きたいのですが、その前に、clangの入った開発環境をささっとdockerで作りましょう。 開発環境の準備 dockerがインストールされている環境で下記コマンドを実行してください。 $ docker run --rm -it debian:10-slim /bin/bash # apt-get update # apt-get install -y gpg wget # wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor >/usr/share/keyrings/llvm-snapshot.gpg # echo "deb [signed-by=/usr/share/keyrings/llvm-snapshot.gpg] http://apt.llvm.org/buster/ llvm-toolchain-buster-9 main" >> /etc/apt/sources.list # apt-get update # apt-get install -y clang-9 clang-format-9 ささっとできましたね。エディタはお好きなモノを入れてください。 動的テスト intdash Edgeのプロジェクトでは、動的テストに、 clang の sanitizer を使用しています。 valgrindではなくsanitizerを使っている理由は、前述の通り、検知できるエラーが多いからです。ただ、やり方によってはvalgrindでチェックできていた項目(UMR: uninitialized memory reads)がチェックされなくなってしまうので、この点の対応も紹介します(超単純ですが)。 Address Sanitizer では、clangのsanitizerを使ってみましょう。 まずは、エラーの発生するコードを書いてみます( こちらのページのIntroduction に様々なパターンのエラーを発生するコードがあります)。 main.cc #include <stdlib.h> void *p; int main() { p = malloc(7); p = 0; // The memory is leaked here. return 0; } 次に、sanitizerを実行してみます。 # clang-++9 -fsanitize=address -g main.c # ./a.out ================================================================= ==4219==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4961dd in malloc (/root/a.out+0x4961dd) #1 0x4c58b8 in main /root/main.c:6:7 #2 0x7f44ae33709a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). リークを検知できましたね。 Undefined Behavior では、次に不定の動作、Undefined Behaviorの検知もしてみましょう。 main.cc #include <stdlib.h> void *p; int main(int argc, char **argv) { p = malloc(7); p = 0; // The memory is leaked here. int k = 0x7fffffff; k += argc; // 2147483647 + 1 = Undefined behavior return 0; } # clang-++9 -fsanitize=undefined,address -g main.c # ./a.out main.cc:9:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior main.cc:9:5 in ================================================================= ==4240==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4961dd in malloc (/root/a.out+0x4961dd) #1 0x4c8227 in main /root/main.cc:6:7 #2 0x7f60b15bb09a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). Undefined BehaviorとLeakの両方検知できてますね。今回はビルド時のオプション -fsanitize に undefined が足されている点に注意してください。 Memory Sanitizer では次は、AddressSanitizerは対応していない、未初期化メモリ(UMR: uninitialized memory reads)の検知をするためにMemorySanitizerを足してみましょう。 main.cc #include <stdlib.h> #include <stdio.h> void *p; int main(int argc, char **argv) { p = malloc(7); p = 0; // The memory is leaked here. int k = 0x7fffffff; k += argc; // 2147483647 + 1 = Undefined behavior int* a = new int[10]; a[5] = 0; if (a[argc]) // Uninitialized memory read printf("xx\n"); return 0; } # clang++-9 -fsanitize=undefined,address,memory -g main.cc clang: error: invalid argument '-fsanitize=address' not allowed with '-fsanitize=memory' あらら、ビルドエラーが出てしまいましたね。 実はエラーの内容の通り、残念なことに、AddressSanitizerとMemorySanitizerは同時には設定できません。 なので、おとなしく2回実行しましょう。 # clang++-9 -fsanitize=undefined,address -g main.cc -o address.out # clang++-9 -fsanitize=memory -g main.cc -o memory.out # ./address.out; ./memory.out main.cc:11:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior main.cc:11:5 in xx ================================================================= ==4260==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4961dd in malloc (/root/address.out+0x4961dd) #1 0x4c822d in main /root/main.cc:7:7 #2 0x7fe83d8cc09a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). ==4263==WARNING: MemorySanitizer: use-of-uninitialized-value #0 0x49a8d3 in main /root/main.cc:15:7 #1 0x7f595dd7109a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) #2 0x41f269 in _start (/root/memory.out+0x41f269) SUMMARY: MemorySanitizer: use-of-uninitialized-value /root/main.cc:15:7 in main Exiting 二回実行したとしても 処理速度はvalgrindより早い ので、こちらの方が良いと思うのですがいかがでしょうか? Coverage そうそう、カバレッジの出力も忘れてはいけませんね(カバレッジ計測の仕組みについて興味がある方は こちらの記事 も見てみてください)。 # clang++-9 -fsanitize=undefined,address -g main.cc -o address.out # clang++-9 -fsanitize=memory -fprofile-instr-generate -fcoverage-mapping -g main.cc -o memory.out # ./address.out; ./memory.out ...出力は省略... # llvm-profdata-9 merge -sparse default.profraw -o default.profdata # llvm-cov-9 show -format=html -output-dir=coverage-report -instr-profile=default.profdata memory.out coverage-reportフォルダに結果のhtmlページが作成されます。 これでカバレッジも計測できるようになりました。 コーディングスタイルチェック ここまでで、 clang を使っての sanitizer のチェックを紹介しましたが、ついでなのでフォマッターも紹介します。 「そもそもフォーマッターなんていらないでしょ」と言われるかもしれませんが、コーディングスタイルを統一し、何も考えずにフォーマッターに任せることで、生産性の低い悩みが出る可能性は減らせると思います。 intdash Edgeのプロジェクトでの具体的な使用方法は、 開発者はコードエディタにclang-formatを適用して自動フォーマット(を推奨) CIではclang-formatを実行しフォーマット通りかチェック となっています。 それではスタイルを規定して、フォーマッターを使ってみましょう。 (開発環境のセットアップ方法は こちら で紹介しています) スタイルの規定 clang-formatのスタイルは 様々な設定 ができますが、導入しやすいのはベースとなるスタイルを選び、そこから必要な箇所だけ変更する方法です。 ベースとなるスタイル LLVM A style complying with the LLVM coding standards Google A style complying with Google’s C++ style guide Chromium A style complying with Chromium’s style guide Mozilla A style complying with Mozilla’s style guide WebKit A style complying with WebKit’s style guide Microsoft A style complying with Microsoft’s style guide intdash Edge のプロジェクトでは、ベーススタイルは mozilla を使用し、そこから下記変更を行っています。 switch ブロック内の case X: 文をインデント:しない インデントに使用する列数 :4 アクセス修飾子(public: protected: private:)のインデント:しない このフォーマットを設定ファイルにしたものは下記になります。 .clang-format --- BasedOnStyle: mozilla IndentCaseLabels: false IndentWidth: 4 AccessModifierOffset: -4 ... フォーマッターの実行 では、フォーマッターを下記コードに対して実行してみましょう。 main.cc #include <stdio.h> #include <stdlib.h> class Class {private: Class();}; int main(int argc, char **argv) { switch (argc) { case 1: printf("hello"); break; } return 0; } # clang-format-9 -style=file -i main.cc # cat main.cc #include <stdio.h> #include <stdlib.h> class Class { private: Class(); }; int main(int argc, char** argv) { switch (argc) { case 1: printf("hello"); break; } return 0; } フォーマットされましたね。includeは名前順に変更され、classやswitchは指定したフォーマットで整形されています。 まとめ C++で開発している intdash Edge のプロジェクトで使用しているCIから、clangのsanitizerによる動的テストと、clang-formatによるフォーマットを紹介しました。 sanitizerは今回紹介した以外にも様々な機能があります。ぜひぜひ、導入を検討してみてください。