やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

明日から使えるgRPC入門

業務でgRPCを使う機会があるのですが、個人で軽く触ったことがある程度で正しく理解できているか不安だったので、改めてインプットし直しました。この記事はさくらインターネットさんが公開されている記事から特に重要だと感じた箇所を抽出して、自身の備忘録として短くまとめたものです。

knowledge.sakura.ad.jp

実は前回も読んでいるはずなのですが、頭から抜けている箇所が多くありました...。

そもそもRPCって何

RPCとは通信プロトコルの1つで「遠隔手続き呼び出し」と訳せるようにクライント・サーバーモデルであり、どこかにあるサーバーに定義された関数をクライアントが呼び出すことで実現されます。 ここでいう通信プロトコルとは「HTTPHTTPSのような通信に使用されるプロトコルよりも上位の概念」だと認識しておくと理解がしやすいと思います。私はプロトコル・通信プロトコルという言葉が同じように使用されていて、とても混乱しました。

意外にもRPCの歴史は古く1976年にはRFCが発表されていたそうです。

遠隔手続き呼出し (RPC) の考え方は、少なくともRFC 707が発表された1976年まで遡る。
最初にRPCを商用に実用化したのはゼロックスの「Courier」であり、1981年のことであった。

遠隔手続き呼出し - Wikipedia

gRPCについて

本題のgRPCはRPC(Remote Procedure Call)を実現する方法の1つです。
元々はGoogleが自社向けに開発していたものを、改良・オープンソース化した技術であり、従来のXMLやJSONベースのRPCではプレーンのテキスト情報をやり取りするためにオーバーヘッドが大きくなるという欠点がありました。そこで、gRPCではバイナリデータをベースにしてRPCを実現することで、この問題を解消しています。

gPRCではデータのやり取り(表現・シリアライズ)にはProtocol Buffersを使用します。
Protocol BuffersもGoogleが開発したもので、特定の言語に依存しないプロトコル定義ファイルを.proto拡張子を用いて定義します。JSONやYMLのような独自の文法をサポートしており、実際の通信時にはバイナリ形式のデータがやり取りされますが、プロトコル定義ファイルは人間でも簡単に読み書き可能なファイルです。
このプロトコル定義ファイルを専用のprotocというツールを使用して変換することで、各言語に必要なファイル(クラスや型)が出力されます。つまりプロトコル定義ファイルがgRPCの実装においてコアになるということです。

syntax = "proto3";

package user;

message User {
  int32 id = 1;
  string name = 2;
}

また、プロトコルにはHTTP/2を使用します。
従来のXML・JSONベースのREST APIが使用することがあるHTTP/1と比べて通信のパフォーマンスが良いです。HTTPの歴史が紹介されているページも合わせて読んでみたのですが、面白かったです。

developer.mozilla.org

プロトコル定義ファイルの書き方

ファイルトップにはsyntaxを記述して、使用するバージョンをします。
またプロトコル定義ファイルはパッケージング(階層化もOK)がサポートされており、名前衝突を防ぐことができます。
他の.protoファイルに定義されたメッセージなどをインポートすることができるので、積極的にファイルを分割していきたいところです。

syntax = "proto3";

package article;

import "picture.proto";

プロトコル定義ファイルではデータはメッセージ(message)という名前で定義されます。
TypeScriptでいうtypeに該当するもので、データの型をmessage 型名を記述することでメッセージを定義します。 メッセージは複数のフィールドを持つことが可能であり型 フィールド名 フィールド番号の順に記述します。他のメッセージをフィールドに定義することもできます。

/* メッセージ名はキャメルケース */
message Article {
  /* 型 フィールド名 = フィールド番号; */
  uint32 id = 1;
  string title = 2;
  string body = 3; /* フィールド名はスネークケース */
}

message PurchasedArticle {
  Article article = 1;
  string purchased_at = 2;
}

使用可能なデータ型

フィールド番号はフィールドを特定するための識別子の役割を持つため、重複はできません。
また過去に定義したフィールド番号へ新たなフィールドを割り当てるようなことをすると、プロトコル定義ファイルの整合性がとれなくなるため、注意が必要です。互換性についてはトピックが長くなってしまうため、割愛します。

またフィールド番号は、15までは1バイト(0から15を表現可能)にエンコードされます。
16番以降は2バイト以上になるため、頻繁に使用するフィールドは15番までに定義しておくのが良いそうです。

You should use the field numbers 1 through 15 for the most-frequently-set fields.
Lower field number values take less space in the wire format.
For example, field numbers in the range 1 through 15 take one byte to encode.
Field numbers in the range 16 through 2047 take two bytes.

Language Guide (proto 3) | Protocol Buffers Documentation

enumと配列・map

メッセージ以外にもenumを定義することができます。
フィールドは0始まりにする必要があり、0の数値を割り当てているフィールドがデフォルト値になります。

enum Status {
  Draft = 0;
  Private = 1;
  Public = 2;
}

message Article {
  uint32 id = 1;
  Status status = 2;
}

他にも配列とmap(連想配列)がサポートされています。
Protocol Buffersでは直和型という表現はしないようですが、oneofを使うことでAかBのフィールドのどちらかが存在することを定義できます。

/* 配列 */
message Tag {
  uint32 id = 1;
  string tag_name = 2;
}

message Article {
  uint32 id = 1;
  repeated Tag tags = 4;
}

/* 連想配列(map) */
message ArticleResponse {
  bool error = 1;
  oneof result {
    Article article = 2;
    string error_message = 3;
  }
}

/* oneof */
message UserResult {
  oneof result {
    User user = 1;
    string error = 2;
  }
}

サービスの定義

ここではメッセージ(データ型)についての定義でした。
RPCではサーバーにどのようなサービス(プロシージャ群)が存在しているのか、どのようなリクエストを受けてレスポンスを返すのかを定義する必要があります。こちらもプロトコル定義ファイルに同じように定義することができます。サービスの定義にはserviceを記述してプロシージャの定義にはrpcを記述します。

service Article {
  /* rpc プロシージャ名 (リクエスト型) returns (レスポンス型 */
  rpc GetArticle (ArticleRequest) returns (ArticleResponse);
}

記事情報を取得するRPCの一連の定義は以下のようになります。
一例ではありますが、1つのサービス・プロシージャ定義が完了しました。

syntax = "proto3";

package article;

message Tag {
  uint32 id = 1;
  string tag_name = 2;
}

message Article {
  uint32 id = 1;
  string title = 2;
  string body = 3;
  repeated Tag tags = 4;
  enum Status {
    Draft = 0;
    Private = 1;
    Public = 2;
  }
  Status status = 5;
}

message ArticleRequest {
  uint32 id = 1;
}

message ArticleResponse {
  bool error = 1;
  oneof result {
    Article article = 2;
    string error_message = 3;
  }
}

ファイルの出力

プロトコル定義ファイルが完成したらprotocを使って、各ファイルを出力します。
言語によって実行するコマンドが異なるので、詳細は公式サイトのLanguagesを参照してください。

grpc.io

自分の場合はkotlinを使っているのでgradleコマンドを実行することで各ファイルが出力されました。
コマンドの内部処理までは理解できていませんが、基本的にはprotocが内部で実行されているのだと思います。

あとはサーバーサイドの処理を記述すれば完成です。

internal class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
  override suspend fun sayHello(request: HelloRequest) =
    helloReply {
      message = "Hello ${request.name}"
    }
}

まとめ

  • RPCは通信プロトコルの1つで、リモートに定義されたプロシージャを呼び出すための技術
  • gRPCはRPCを実現する技術の1つでGoogleが社内向けに開発したものが改良・オープンソース化されたもの
  • gRPCでは通信のオーバーヘッドを減らすためにProtocol Buffersというバイナリ形式のデータをやり取りする
  • Protocol Buffersではプロシージャ・データ型の情報をプロトコル定義ファイル(.proto)に記述する
  • プロトコル定義ファイルにはメッセージ・サービスといった情報を定義する
  • protocというツールを使うことで、言語に対応したファイル(クラスや型)を出力する
  • 出力したファイルにサーバー側の処理などを実装する

今回は紹介しませんでしたが、gRPCはリクエスト・レスポンスといわれる1つのリクエストに対して1つのレスポンスを返す形式のみでなく、複数のリクエストに対して複数のレスポンスを返したり、リクエストをキャンセルさせたりといったことも可能です。
マイクロサービスでよく問題になるAへのリクエストは成功したけど、Bへのリクエストに失敗して、Cへのリクエストをキャンセルしたいようなケースに柔軟に対応することができそうです。

grpc.io

とはいえHTTPのバージョン問題もあり、シンプルなサービスや外部公開向けのをAPIを作るようなケースであればREST APIを採用するのが適切だと思います。またやり取りされるデータ形式はバイナリになるため、直感的にデバッグをするのが難しくなるという欠点もあります。

少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。

参考文献

【書評】データ指向アプリケーションデザインを読了して見える世界

1ヶ月ほど読み進めていた「データ指向アプリケーションデザイン」を読了しました。
オライリーから出版されている本の中でも、かなり分厚い部類の書籍だったのと、章ごとの情報量が凄かったので結構、時間がかかってしまいました。

個人的には難易度の高い書籍だと思うのですが、それでも多くの方からオススメされてきた書籍でもあります。
今まで読んでは飽き...読んでは飽き...を繰り返していましたが、ようやく一貫して読了したので簡単に書評を書いてみたいと思います。

デカい...! 分厚い...!

続きを読む

SELECTの結果から複数のデータを複数INSERTする

SQLで初期データを作成したいというのは、よくあるケースかなと思います。
例えば全ての企業(companies)に対して初期ユーザー(users)を1件登録する必要があるとします。企業とユーザーは1対多の関係にあり、以下のようにINSERTSELECTを組み合わせることで簡単に全ての企業に対して初期ユーザーの追加が完了します。

企業一覧

postgres=# SELECT * FROM companies;
 id | name
----+------
  1 | A社
  2 | B社
  3 | C社
(3 rows)

各企業に初期ユーザーを追加

INSERT INTO users (name, company_id)
SELECT
  '初期ユーザー',
  companies.id
FROM
  companies
;

postgres=# SELECT * FROM users;
 id |     name     | company_id
----+--------------+------------
  1 | 初期ユーザー |          1
  2 | 初期ユーザー |          2
  3 | 初期ユーザー |          3
(3 rows)

ではSELECTの実行結果から複数のデータを複数INSERTするにはどうすれば良いでしょうか。
先ほどの例でいうと、初期ユーザとして「岡部・椎名・橋田」を登録するにはどのようなクエリを記述すれば良いでしょうか。自分が「SELECT INSERT 複数行」といったワードで調べた限り、この問題を解決する方法を紹介している方を発見することはできませんでした。

続きを読む

AWS クラウドプラクティショナー(CLF-C02)に合格しました

先日、AWS公式が開催しているAWS Certified Cloud Practitioner(CLF-C02)に合格しました。
学習期間は1週間ほど。市販のテキストを読みつつ、無料で問題を解くことができるサイトを利用させて頂きました。
AWSに関しては多少の知識はあれど、実務で使ったことはほとんどありません。
というのも自分が所属する企業ではGCPをメインに使っているため、AWSを本格的に使う機会がありませんでした。大学生の時にアカウントだけは作っていて知らぬ間に無料枠を食い潰していたようです。もったいない...。
ただ、GCPとAWSで似たサービスや概念が多くあったので、スムーズに理解することができたと思います。

続きを読む

2023年度のブログ活動の振り返り

あっという間に今年も残り数時間で終わろうとしていますね。
今年度もありがとうございました。今年は多くの方に記事を読んで頂けて嬉しい限りです。
実は報告などはしていなかったのですが、2023年はある目標を立ててブログ(アウトプット)を続けてきました。
もしかしたら、勘付いている方もいるかもしれませんが、その目標とは「毎月10記事以上の記事を1年、投稿し続ける」というものです。

ブログ上部の「運営者について」に月毎の投稿数が分かる情報を表示するようにしているのですが、2023年は全ての月で10記事以上の記事を投稿し続けて、12月度はすでに9件の記事を公開しており、この記事が10個目の記事になります。
1年間で122個の記事を公開したと考えると、よく頑張ったなぁ...と少し誇らしい気持ちになります。

ところで、なぜ毎月10記事の投稿を続けたのかというと、自身のモチベーションに左右されずに継続的にアウトプットをできるようになりたいという思いがあったためです。「忙しいから...」「ネタがないから...」といった、やらないことへの言い訳をできない状況づくりを意識しました。結果的に自分との約束を無事に果たせて良かったです。

続きを読む