RPC トーク: シリアライゼーションの問題

RPC トーク: シリアライゼーションの問題

シーケンスとは

コンピュータの場合、すべてのデータはバイナリ シーケンスです。しかし、これらのバイナリ データを人間が判読可能で制御可能な形式で処理するために、プログラマはデータ型と構造の概念を発明しました. データ型は、バイナリ データの断片の解析方法をマークするために使用され、データ構造は次のようになります。複数のセグメント(連続/不連続)をマークするために使用されます)バイナリデータがどのように編成されているか。

たとえば、次のプログラム構造:

type User struct {
    
    
	Name  string
	Email string
}

Name と Email はそれぞれ 2 つの独立した (または連続した、または不連続な) メモリ空間 (データ) を表し、構造体変数自体にもメモリ アドレスがあります。

1 つのプロセスで、構造アドレスを共有することでデータを交換できます。ただし、ネットワークを介して他のマシンのプロセスにデータを送信する場合は、ユーザー オブジェクトのさまざまなメモリ空間を連続したバイナリ表現にエンコードする必要があります。これを「シリアライゼーション」と呼びます。ピア マシンがバイナリ ストリームを受信した後、データをユーザー オブジェクトとして認識し、それをプログラムの内部表現に解析できる必要があります。これを「逆シリアル化」と呼びます。

シリアライゼーションとデシリアライゼーションは、人間の視点と機械の視点の間で同じデータを変換することです。

シリアル化プロセス

ここに画像の説明を挿入

インターフェイス記述 (IDL) の定義

データ記述情報を送信するために、また複数人でのコラボレーションを指定するために、一般的に IDL (Interface Description Languages) で記述された定義ファイルに記述情報を定義します。たとえば、Protobuf の IDL 定義は次のとおりです。

message User {
    
    
  string name  = 1;
  string email = 2;
}

スタブ コードを生成する

どんなシリアル化方法が使用されても、最終的な目標はプログラム内のオブジェクトになることです. シリアル化方法は多くの場合言語に依存しませんが、このセクションではメモリ空間をプログラムの内部表現 (構造体/クラスなど) にバインドします.特定のプロセスは言語に依存するため、多くのシリアライゼーション ライブラリは、IDL ファイルをターゲット言語のスタブ コードにコンパイルするために、対応するコンパイラを提供する必要があります。

通常、スタブ コードの内容は次の 2 つの部分に分けられます。

型構造の生成 (つまり、ターゲット言語の Struct[Golang]/Class[Java])
シリアライゼーション/デシリアライゼーション コードの生成 (バイナリ ストリームをターゲット言語構造に変換)
以下は、Thrift によって生成されるシリアライゼーション スタブ コードです。

type User struct {
    
    
  Name string `thrift:"name,1" db:"name" json:"name"`
  Email string `thrift:"email,2" db:"email" json:"email"`
}

//写入 User struct
func (p *User) Write(oprot thrift.TProtocol) error {
    
    
  if err := oprot.WriteStructBegin("User"); err != nil {
    
    
    return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) }
  if p != nil {
    
    
    if err := p.writeField1(oprot); err != nil {
    
     return err }
    if err := p.writeField2(oprot); err != nil {
    
     return err }
  }
  if err := oprot.WriteFieldStop(); err != nil {
    
    
    return thrift.PrependError("write field stop error: ", err) }
  if err := oprot.WriteStructEnd(); err != nil {
    
    
    return thrift.PrependError("write struct stop error: ", err) }
  return nil
}

// 写入 name 字段
func (p *User) writeField1(oprot thrift.TProtocol) (err error) {
    
    
  if err := oprot.WriteFieldBegin("name", thrift.STRING, 1); err != nil {
    
    
    return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:name: ", p), err) }
  if err := oprot.WriteString(string(p.Name)); err != nil {
    
    
  return thrift.PrependError(fmt.Sprintf("%T.name (1) field write error: ", p), err) }
  if err := oprot.WriteFieldEnd(); err != nil {
    
    
    return thrift.PrependError(fmt.Sprintf("%T write field end error 1:name: ", p), err) }
  return err
}

// 写入 email 字段
func (p *User) writeField2(oprot thrift.TProtocol) (err error) {
    
    
  if err := oprot.WriteFieldBegin("email", thrift.STRING, 2); err != nil {
    
    
    return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:email: ", p), err) }
  if err := oprot.WriteString(string(p.Email)); err != nil {
    
    
  return thrift.PrependError(fmt.Sprintf("%T.email (2) field write error: ", p), err) }
  if err := oprot.WriteFieldEnd(); err != nil {
    
    
    return thrift.PrependError(fmt.Sprintf("%T write field end error 2:email: ", p), err) }
  return err
}

User オブジェクトをバイナリにシリアル化するために、メモリ内の構造全体の構成と順序をハードコーディングし、各フィールドに対して必須の型変換を実行することがわかります。新しいフィールドを追加する場合は、スタブ コードを再コンパイルし、すべてのクライアントを更新する必要があります (もちろん、新しいフィールドを使用して更新する必要はありません)。逆シリアル化の手順は似ています。

上記の長いコードは、デモンストレーションに使用する最も単純なメッセージ構造にすぎませんが、本番環境での実際のメッセージ タイプの場合、このスタブ コードはより複雑になります。

スタブ コードの生成は、言語間の呼び出しの問題を解決するためだけのものであり、必須ではありません。呼び出し元と呼び出し先が両方とも同じ言語であり、将来的に同じ言語であることが保証される場合は、IDL 定義をターゲット言語で直接記述し、コンパイル手順をスキップすることも選択します。 Thrift ドリフト プロジェクトでは、Java を使用して定義ファイルを直接記述します。

@ThriftStruct
public class User
{
    
    
    private final String name;
    private final String email;

    @ThriftConstructor
    public User(String name, String email)
    {
    
    
        this.name = name;
        this.email = email;
    }

    @ThriftField(1)
    public String getName()
    {
    
    
        return name;
    }

    @ThriftField(2)
    public String getEmail()
    {
    
    
        return email;
    }
}

前述したように、シリアライゼーション自体の重要性は、人間と機械の観点から理解できるデータの変換を提供することです。従来の考え方は中間構造によるものであり、これは操作機能を提供することによるものです。

ただし、このタイプの方法に共通する問題は、データを操作する機能しか提供されず、データを自分で管理するプログラマーの利便性が犠牲になることです。たとえば、User 構造体が持つフィールドを知りたい場合、シリアル化およびコンパイルされたコードがこの機能を提供しない限り、バイナリの文字列から始める方法はありません。たとえば、User オブジェクトをいくつかの ORM ツールと組み合わせてデータベースに直接格納する場合は、新しい User 構造体を手動で記述してから、値を 1 つずつ割り当てる必要があります。

このタイプのシリアル化フレームワークは、データベースやメッセージ キューなど、データ定義があまり変わらないコア インフラストラクチャ サービスで主に使用されます。日常のビジネス開発で使用する場合、費用対効果があまり高くない可能性があります。

やっと

インターネット上で、どのシリアライゼーション プロトコルがより優れたパフォーマンスを発揮するかについて議論しているのをよく耳にします。実際、さまざまなシリアライゼーション ソリューションを真剣に検討すると、シリアライゼーション プロトコル自体は単なるドキュメントであり、そのパフォーマンスは実装方法に依存することが簡単にわかります。異なる言語での実装、および同じ言語での異なる方法とメソッドでの実装は、最終的な使いやすさとパフォーマンスに大きな影響を与えます。Flatbuffer を使用して Protobuf プロトコルを実装すると、多くのパフォーマンスを向上させることができますが、それはあなたが望むものではないかもしれません。

パフォーマンスと比較して, どのシリアライゼーションの問題を解決したいのか, どの問題を放棄してもよいのかを理解することがより重要です. 明確な要件がある場合にのみ、適切なシリアライゼーションソリューションを選択できます, そして実際に問題に遭遇することもできます.問題が解決可能かどうか、およびその解決方法をすぐに知ることができます。

おすすめ

転載: blog.csdn.net/kalvin_y_liu/article/details/129995839