発明のための再発明

Webプログラマーが、プログラムの内部動作を通してプログラムを作る時の参考になるような情報を書くブログ(サーバーサイドやDevOpsメイン)

grpcの動き

grpcがどんな風に動いているかを覗いてみます。

grpcとは

下の画像のようにgoogle製のrpcフレームワーク言語をまたいで扱えるのが特徴です。

f:id:mrasu:20180407195152p:plain

クライアントは言語ごとに作られていて、grpcのリポジトリに直接入っているものと、別リポジトリに分けられているものが有ります。
基本的には、protocolbufferとhttp2を利用したRPCで、cncfの各プロジェクトやコンテナ系のミドルウェアでかなり使われています。

wiresharkで見るgrpcの動き

grpc-goのサンプルコードを基に、通信内容を見てみます。
動いている内容は、

  1. クライアントが、SayHello({Name: "world"})というメソッドを関数を実行する
  2. サーバーがResponse{Message: "Hello world"}と返す

というシンプルな動作で、最もシンプルな Unary RPC を使用しています。

wiresharkで除いて見ると、下のような流れを見ることが出来ます。 (クライアントのポートが42874、サーバーのポートが50051)

f:id:mrasu:20180407194834j:plain

http2を使用していることがわかります。

詳細に見てみると、
クライアントからのDATAはHEADERSと一緒に送られていて、下のような内容が送信されています。

HEADERS内容: 
    :method: POST
    :scheme: http
    :path: /helloworld.Greeter/SayHello
    content-type: application/grpc

DATA内容:
    hexでの表記: 00000c00010000000100000000070a05776f726c64
    最後に、"world"(77 6f 72 6c 64)があるのがわかります

対して、サーバーからのHEADERSとDATAの下のような内容です。

HEADERS内容: 
    :status: 200
    content-type: application/grpc

DATA内容:
    hexでの表記: 000012000000000001000000000d0a0b48656c6c6f20776f726c64
    最後に、"Hello world"(48 65 6c 6c 6f 20 77 6f 72 6c 64)があるのがわかります

つまり、

  • 呼び出したいメソッドはHEADERS
  • protocolbufferの内容はDATA

に書かれているとわかります。

つまり、http2で動いているだけなので、grpcのライブラリを使わずとも、リクエストを送るクライアントが(大変ですが)一応、書けます。

func main() {
    log.Println("start client")

    conn, err := net.Dial("tcp", "localhost:50051")
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }

    magic := "505249202a20485454502f322e300d0a0d0a534d0d0a0d0a"
    settings := "000000040000000000"
    headers := "00005c010400000001838645956272d141fc1eca245f15852a4b631b87eb1968a0ff418ba0e41d139d09b8d800d87f5f8b1d75d0620d263d4c4d65647a8a9acac8b4c7602bb2b83f40027465864d833505b11f40899acac8b24d494f6a7f867df7db7416ff"
    data := "00000c00010000000100000000070a05776f726c64"

    packets := []string{magic, settings, headers, data}

    for _, p := range packets {
        raw, err := hex.DecodeString(p)
        if err != nil {
            log.Fatalf("cannot decode: %v", p)
        }

        if _, err := conn.Write(raw); err != nil {
            log.Fatalf("cannot write: %v", p)
        }
    }

    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            log.Fatalf("cannot read from socket: %v", err)
        }
        log.Println(string(buf[:n]))
        log.Println(hex.EncodeToString(buf[:n]))
    }
}

更に詳細な送信内容は、github内のドキュメントにまとめられています

コードから見る動き

grpc(Unary RPC)の動きをgoのコードを基におってみてみます。

クライアントから見ると、

  1. ユーザーがprotoファイルで定義した関数を呼び出す。
  2. 引数の値をprotocolbufferを基にシリアライズして、DATAにする
  3. pathに呼び出したい関数をセットする
  4. サーバーに送信
  5. 受信を待つ
  6. 受信したデータをデシリアライズして、ユーザーに伝える

という流れです。
逆にサーバーから見ると、

  1. サーバーを起動する
  2. 呼び出される関数を登録する
  3. リクエストが来たら、DATAをデシリアライズし、pathの値を基に登録した関数を呼び出す
  4. 実行した戻り値をクライアントへ送信する

以上が、grpcの簡単な流れとなっています。

Dapperを読んだ

Dapperとは

googleが社内で利用している、分散トレーシングツール https://research.google.com/pubs/pub36356.html

この論文から出来たOSSとしてはtwitterによるzipkin(github)、uberによるjaeger(github)が有る。

内容

マイクロサービスだと1つのリクエストに対して複数のサービスが連携するので、一貫した記録が必要になる。 だから、リクエストの中でどのサービスにどれくらい時間がかかったかを記録したかった。 f:id:mrasu:20180401215816p:plain

各サービスはspanという開始と終了時間とリクエストid、annotationという任意のマークを持っている。 サービスが別サービスを呼び出すとspanは入れ子になる。

各spanは一度ローカルに保存され、その後中央(googleだから、bigtable)に送られる。 f:id:mrasu:20180401215825p:plain Dapperにはオーバーヘッドがあるので、サンプリングしている。 全部のリクエストを記録しなくても、トレーシングとしては、十分把握出来る

Amazonに見るi18n -「国際化対応」とは何を変える事か

サービスを海外展開したり日本にいる外国人にアピールしたくなった時、国際化対応が必要になります。
この記事を通して、「国際化対応したい」という話が出た時に「どこをどのように」対応することになるのか、対応範囲や見積などがなんとなく伝わればと思います。
以前の記事(i18nの書き方 from JavaScript)ではコードについて書きましたが、この記事ではAmazonを例に、開発者にとって国際化対応が「i18nのライブラリ入れるだけ」ではないこと、他にどのような対応が必要になるかという雰囲気を伝えられればと思います。

※国際化対応という言葉は色々な文脈で使用されますが、この記事では「現在想定している言語の話者以外(このブログを見ている人の大半にとっては日本人以外)にサービスを使用してもらうための対応一般」程度の雑な意味で使用します。

なぜAmazon

国際化対応には、大別して「言語を変える」という基本的な対応と「各文化に沿って表示やフローを変更する」という複雑な対応の2種類が存在します。

驚くべきことに、Amazonはこの2種類の対応を同時に行っているのです。
amazon.co.jpのフッターから、「言語選択」と「国の選択」ができるようになっています。
言語変更は別言語を使う人向けの対応であり、国の変更では販売商品やサービスなど大きく変わります。

下のURLがそれぞれのページです。

この仕組みのおかげで、国際化対応にあたり「どこが変わっているのか」、「なぜ妥協しているのか」を実感しやすいため、この記事ではAmazonを例としています。

※調査日は2018年3月19日

トップページ

まずは、トップページの国際化対応を見てみましょう

  • 日本用、日本語
    f:id:mrasu:20180331001348p:plain
  • 日本用、英語
    f:id:mrasu:20180331001343p:plain
  • 米国用、英語
    f:id:mrasu:20180331001204p:plain

ルーセル

まず目につくのは巨大なカルーセル部分(prime videoの宣伝画像)でしょうか。
ここでは、

という対応がされていることがわかります。つまり、プログラム的には大きな変更は行っていません
リンク先を画像毎に変えるというのはカルーセル実装時に対応していることであるので、開発にとっては「表示内容の変更」でしかありません。
また、トップページ全体を通してもナビゲーションの位置やレコメンドの配置場所など、レイアウトに大きな違いはないため、各国で共通したテンプレートを使用していることが伺えます。
つまり、言語や国などの条件によって受けの良いデザインを使用することで、各対象への受けを狙っているものの、あくまで部分的対応で実現しているということです。

このカルーセルのように、メイン画像を変えることは費用対効果が大きく、プログラムとしてもその場しのぎの簡単な対応で実現可能なので導入しやすい国際化対応となります。

部分的な表示・非表示

次に、最上部に米国用のみ「NEW&INTERESTING FINDS」と題した宣伝があるところを見ると、

  • 言語・地域によって表示するかしないか分かれる場所がある
  • この場所を表すdivタグ(id=nav-upnav)は日本用にも存在するが中のhtmlはなし
  • (米国用サイトであってもスペイン語では表示していない)

とわかります。カルーセルでは画像を変更していましたが、ここでは「表示・非表示」を通して表示内容を変更しています
非表示にするには、div部分だけ残して中身のhtmlを消すか、display:noneで消すか、divごとhtmlから消すかは悩みどころですが、好みの問題でしょう

さて、この対応が実施される典型は、「リンク先を隠す」という対応です。
「メインコンテンツについては国際化対応を行ったが、小さなコンテンツやセール、リンク先の別サービスでは対応していない」という時に、リンク先を隠すことで国際化対応していないコンテンツの存在を許容することが出来ます。
このような対応は移行時期だけではなく、サイト全体の国際化対応が完了した後にも、ターゲットの違いや費用対効果の面から、国際化対応しないと決定されるコンテンツがある限り存在し続けます。
そのため、プログラムを書く際は、表示非表示のロジックは共通化しておくのが良いでしょう。

レコメンド内容

トップページの分析の最後に商品の宣伝部分(おもちゃのレコメンドや"Deals recommended for you"以下)を見てみましょう

  • 日本用は言語が違ってもレコメンド内容は同一
  • 英語であっても日本語書籍を紹介するなど、言語差は考慮していない?
  • 米国用の"Deals recommended for you"は金額が出ていたり、カテゴリ横断の宣伝がされているなど大きく違う

ことがわかります。
つまり、日本サイトではログイン前に表示するレコメンドでは言語を意識していないのです。
推測ですが、ログインして使用するユーザーが多かったり、日本用サイトを見ているからには日本のコンテンツを理解できるはずだという想定をしたりしていて、差別化する必要性を強く感じていないのかもしれません。

まぁ、どのような理由にしろ、妥協可能だという判断がされたのでしょう。
国際化対応を考え始めるとあらゆる場所に対応しようと考えてしまいますが、妥協出来る部分は存在します。Amazonであっても妥協できる部分を上手く見つけて、妥協しているのです。
また、"Deals recommended for you"では金額を出していることから、部分的には差を出そうと努力していることもわかります。

他にもトップページには、メニュー項目が違ったり、英語表示なのに日本語広告があったり、中国大陸用サイトではテンプレートが違うなど、色々と見つかるので探してみると面白いものですが、長くなったのでここまでとして、商品画面を見てみましょう

商品画面

商品画面は下のような表示になっています(商品は洋書の「Refactoring」)

  • 日本用、日本語
    f:id:mrasu:20180331001518p:plain
  • 日本用、英語
    f:id:mrasu:20180331001522p:plain
  • 米国用、英語
    f:id:mrasu:20180331001524p:plain

URL

最初に注目するところはURLです。
Amazonは世界共通のDBを使用しているのか、同一商品に対しては同一idが付与されているようです。
たとえば、画像の例では「Refactoring」を使用していますが、日本と米国のURLは

と、idは共通しています。
同一商品には同一idが振られると使い回しが楽で良いのでしょう。
ただし、同一DBを使用しているかどうかははっきりしません。
日本特有の商品に対するidの振り方が違ったり、タイトルが微妙に違っていたりする所をみると、idのみ共通化し、各項目(商品名や著者、値段など)は各国が扱いやすいようにテーブルごと分けて管理しているかもしれません。
商品内容を各国が管理することで、DBの全内容を共有する必要がなくなり、各国が裁量を持ちやすくなるという利点が有ります。

ただ、DBやテーブルを別にするというのはかなり思い切った構成になるので、初期段階では同一データを保管しつつ、別表示が必要な項目のみを各言語・地域用に管理するのが無難でしょう。

レイアウト

画面構成についてみると、

  • 日本用の日本語と英語では言語が変わってるだけでレイアウトは同一
  • 米国用では「kindle,hardcover,paperback,other sellers」というタブで表示が分けられている

というように、明らかにレイアウトが別です。
htmlでみても、div(id=dp-container)の中が明らかに違っています。更に、dp-containerの中にjavascriptが埋め込まれていることから、dp-containerを使用するまでは共通のテンプレートが使用され、dp-container内で分けられているということが透けて見えます。

さらに興味深いことに、イタリアフランスAmazonでは日本と共通のレイアウトになっているようです。アメリカで先んじてレイアウトを変更しているようです。
Amazonではアメリカを中心に改良を行い、その成果を各国に配布するというフローなのもしれませんね。

このように、各国に合わせてレイアウトを別にすることで、各文化に対応した修正が可能になります。
しかし、その分翻訳者への伝達など調整コストが高くなってしまうので、日本用の日本語と英語が文章の翻訳のみ行っているようにレイアウトを変更しないという判断も可能です。

金額、在庫

最後に、日本用の英語と日本語での共通項目について見て、この記事を終わりたいと思います。
当たり前に感じますが、

  • 金額は共通(7,464円)
  • 在庫も共通(2点)
  • 配送予定日も共通(3/20)
  • レビューも共通

と、共通部分が多くあります。
外貨対応はクレジットカード任せなのか、ドルで購入できるなどはしていないようです(もしかすると、別に指定できる場所があるのかもしれませんが、あまり表に出していないよう)
日本に配達することは位置情報からわかっているので、在庫や配達予定日も同一のプログラムが動いているでしょう。
レビュー数も同じですし、レビューの内容は訳さずにそのまま表示しています。

他にも日本の商品のタイトルや内容紹介などは、英語で表示しても日本語が表示されます。大きく翻訳を割り切っていることがわかります。

このように、言語・地域に合わせて変更する必要が無い場所や、どうしても対応箇所が多くなってしまう場所では、割りきって翻訳せずに同一内容を表示するということも必要になります。

まとめ

以上、Amazonが行っている国際化対応について見てみました。
Amazonほどであっても全てを国・地域によって変更しているわけではないのです。
どこを対応して、どこを共通化するかを見極める必要が有ります。
金額や文言は別にするのか、どこの画像を変えるか、レイアウトを大きく変える価値があるページはどこなのか。
小さな変更と割りきって強引に対応するのか、DBを作り変えるなど根本的な対応を行うのか。
考える必要が有ります。

この記事が、「国際化対応」で何をする必要があるのか考えるヒントになればと思います。