Rumah >pembangunan bahagian belakang >Golang >Penstriman gRPC: Amalan Terbaik dan Cerapan Prestasi

Penstriman gRPC: Amalan Terbaik dan Cerapan Prestasi

Patricia Arquette
Patricia Arquetteasal
2024-12-06 11:32:12759semak imbas

gRPC Streaming: Best Practices and Performance Insights

pengenalan

Penstriman gRPC membolehkan mesej protobuf distrim dari klien ke pelayan, pelayan ke klien atau secara dwiarah.
Ciri berkuasa ini boleh digunakan untuk membina aplikasi masa nyata seperti aplikasi sembang, papan pemuka pemantauan masa nyata dan banyak lagi.

Dalam artikel ini, kami akan meneroka cara menggunakan penstriman gRPC dengan betul.

Prasyarat

  • Pengetahuan asas gRPC
  • Pengetahuan asas bahasa pengaturcaraan Go (Kod sampel ditulis dalam Go, tetapi konsep itu boleh digunakan pada bahasa lain juga)
  • Contoh kod tersedia di GitHub

Amalan Baik

Mari kita semak amalan baik untuk menggunakan penstriman gRPC:

Gunakan permintaan unary untuk permintaan unary

Satu kesilapan biasa ialah menggunakan penstriman untuk permintaan yang tidak sama.
Sebagai contoh, pertimbangkan definisi perkhidmatan gRPC berikut:

service MyService {
  rpc GetSomething (SomethingRequest) returns (stream SomethingResponse) {}
}

Jika pelanggan hanya perlu menghantar satu permintaan dan menerima satu respons,
Anda tidak perlu menggunakan penstriman. Sebaliknya, kami boleh menentukan perkhidmatan seperti berikut:

service MyService {
  rpc GetSomething (SomethingRequest) returns (SomethingResponse) {}
}

Dengan menggunakan penstriman untuk permintaan unary, kami menambah kerumitan yang tidak perlu
kepada kod, yang boleh menjadikannya lebih sukar untuk difahami dan dikekalkan dan bukan
mendapat sebarang faedah daripada menggunakan penstriman.

Contoh kod Golang membandingkan permintaan unary dan permintaan penstriman:

Permintaan tanpa syarat:

type somethingUnary struct {
    pb.UnimplementedSomethingUnaryServer
}

func (s *somethingUnary) GetSomething(ctx context.Context, req *pb.SomethingRequest) (*pb.SomethingResponse, error) {
    return &pb.SomethingResponse{
        Message: "Hello " + req.Name,
    }, nil
}

func TestSomethingUnary(t *testing.T) {
    conn := newServer(t, func(s grpc.ServiceRegistrar) {
        pb.RegisterSomethingUnaryServer(s, &somethingUnary{})
    })

    client := pb.NewSomethingUnaryClient(conn)

    response, err := client.GetSomething(
        context.Background(),
        &pb.SomethingRequest{
            Name: "test",
        },
    )
    if err != nil {
        t.Fatalf("failed to get something: %v", err)
    }

    if response.Message != "Hello test" {
        t.Errorf("unexpected response: %v", response.Message)
    }
}

Menstrim permintaan unari:

type somethingStream struct {
    pb.UnimplementedSomethingStreamServer
}

func (s *somethingStream) GetSomething(req *pb.SomethingRequest, stream pb.SomethingStream_GetSomethingServer) error {
    if err := stream.Send(&pb.SomethingResponse{
        Message: "Hello " + req.Name,
    }); err != nil {
        return err
    }

    return nil
}

func TestSomethingStream(t *testing.T) {
    conn := newServer(t, func(s grpc.ServiceRegistrar) {
        pb.RegisterSomethingStreamServer(s, &somethingStream{})
    })

    client := pb.NewSomethingStreamClient(conn)

    stream, err := client.GetSomething(
        context.Background(),
        &pb.SomethingRequest{
            Name: "test",
        },
    )
    if err != nil {
        t.Fatalf("failed to get something stream: %v", err)
    }

    response, err := stream.Recv()
    if err != nil {
        t.Fatalf("failed to receive response: %v", err)
    }

    if response.Message != "Hello test" {
        t.Errorf("unexpected response: %v", response.Message)
    }
}

Seperti yang kita lihat, kod untuk permintaan unary adalah lebih mudah dan lebih mudah difahami
daripada kod untuk permintaan penstriman.

Menghantar berbilang dokumen sekaligus jika kita boleh

Mari bandingkan dua definisi perkhidmatan ini:

service BookStore {
  rpc ListBooks(ListBooksRequest) returns (stream Book) {}
}

service BookStoreBatch {
  rpc ListBooks(ListBooksRequest) returns (stream ListBooksResponse) {}
}

message ListBooksResponse {
  repeated Book books = 1;
}

BookStore menstrim satu buku pada satu masa, manakala BookStoreBatch menstrim berbilang buku secara serentak.

Jika pelanggan perlu menyenaraikan semua buku, lebih cekap menggunakan BookStoreBatch
kerana ia mengurangkan bilangan perjalanan pergi dan balik antara pelanggan dan pelayan.

Jom lihat contoh kod Golang untuk Kedai Buku dan BookStoreBatch:

Kedai Buku:

type bookStore struct {
    pb.UnimplementedBookStoreServer
}

func (s *bookStore) ListBooks(req *pb.ListBooksRequest, stream pb.BookStore_ListBooksServer) error {
    for _, b := range bookStoreData {
        if b.Author == req.Author {
            if err := stream.Send(&pb.Book{
                Title:           b.Title,
                Author:          b.Author,
                PublicationYear: int32(b.PublicationYear),
                Genre:           b.Genre,
            }); err != nil {
                return err
            }
        }
    }
    return nil
}

func TestBookStore_ListBooks(t *testing.T) {
    conn := newServer(t, func(s grpc.ServiceRegistrar) {
        pb.RegisterBookStoreServer(s, &bookStore{})
    })

    client := pb.NewBookStoreClient(conn)

    stream, err := client.ListBooks(
        context.Background(),
        &pb.ListBooksRequest{
            Author: charlesDickens,
        },
    )
    if err != nil {
        t.Fatalf("failed to list books: %v", err)
    }

    books := []*pb.Book{}
    for {
        book, err := stream.Recv()
        if err != nil {
            break
        }
        books = append(books, book)
    }

    if len(books) != charlesDickensBooks {
        t.Errorf("unexpected number of books: %d", len(books))
    }
}

Kedai BukuBatch:

type bookStoreBatch struct {
    pb.UnimplementedBookStoreBatchServer
}

func (s *bookStoreBatch) ListBooks(req *pb.ListBooksRequest, stream pb.BookStoreBatch_ListBooksServer) error {
    const batchSize = 10
    books := make([]*pb.Book, 0, batchSize)
    for _, b := range bookStoreData {
        if b.Author == req.Author {
            books = append(books, &pb.Book{
                Title:           b.Title,
                Author:          b.Author,
                PublicationYear: int32(b.PublicationYear),
                Genre:           b.Genre,
            })

            if len(books) == batchSize {
                if err := stream.Send(&pb.ListBooksResponse{
                    Books: books,
                }); err != nil {
                    return err
                }
                books = books[:0]
            }
        }
    }

    if len(books) > 0 {
        if err := stream.Send(&pb.ListBooksResponse{
            Books: books,
        }); err != nil {
            return nil
        }
    }

    return nil
}

func TestBookStoreBatch_ListBooks(t *testing.T) {
    conn := newServer(t, func(s grpc.ServiceRegistrar) {
        pb.RegisterBookStoreBatchServer(s, &bookStoreBatch{})
    })

    client := pb.NewBookStoreBatchClient(conn)

    stream, err := client.ListBooks(
        context.Background(),
        &pb.ListBooksRequest{
            Author: charlesDickens,
        },
    )
    if err != nil {
        t.Fatalf("failed to list books: %v", err)
    }

    books := []*pb.Book{}
    for {
        response, err := stream.Recv()
        if err != nil {
            break
        }
        books = append(books, response.Books...)
    }

    if len(books) != charlesDickensBooks {
        t.Errorf("unexpected number of books: %d", len(books))
    }
}

Daripada kod di atas, ia perlu dijelaskan mana yang lebih baik.
Jom jalankan penanda aras untuk melihat perbezaannya:

Tanda aras Kedai Buku:

func BenchmarkBookStore_ListBooks(b *testing.B) {
    conn := newServer(b, func(s grpc.ServiceRegistrar) {
        pb.RegisterBookStoreServer(s, &bookStore{})
    })

    client := pb.NewBookStoreClient(conn)

    var benchInnerBooks []*pb.Book
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        stream, err := client.ListBooks(
            context.Background(),
            &pb.ListBooksRequest{
                Author: charlesDickens,
            },
        )
        if err != nil {
            b.Fatalf("failed to list books: %v", err)
        }

        books := []*pb.Book{}
        for {
            book, err := stream.Recv()
            if err != nil {
                break
            }
            books = append(books, book)
        }

        benchInnerBooks = books
    }

    benchBooks = benchInnerBooks
}

Tanda aras BookStoreBatch:

func BenchmarkBookStoreBatch_ListBooks(b *testing.B) {
    conn := newServer(b, func(s grpc.ServiceRegistrar) {
        pb.RegisterBookStoreBatchServer(s, &bookStoreBatch{})
    })

    client := pb.NewBookStoreBatchClient(conn)

    var benchInnerBooks []*pb.Book
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        stream, err := client.ListBooks(
            context.Background(),
            &pb.ListBooksRequest{
                Author: charlesDickens,
            },
        )
        if err != nil {
            b.Fatalf("failed to list books: %v", err)
        }

        books := []*pb.Book{}
        for {
            response, err := stream.Recv()
            if err != nil {
                break
            }
            books = append(books, response.Books...)
        }

        benchInnerBooks = books
    }

    benchBooks = benchInnerBooks
}

Hasil penanda aras:

BenchmarkBookStore_ListBooks
BenchmarkBookStore_ListBooks-12                      732           1647454 ns/op           85974 B/op       1989 allocs/op
BenchmarkBookStoreBatch_ListBooks
BenchmarkBookStoreBatch_ListBooks-12                1202            937491 ns/op           61098 B/op        853 allocs/op

Sungguh peningkatan! BookStoreBatch lebih pantas daripada BookStore dengan faktor 1.75x.

Tetapi mengapa BookStoreBatch lebih pantas daripada BookStore?

Setiap kali pelayan menghantar aliran mesej.Hantar() kepada klien, perlu
mengekod mesej dan menghantarnya melalui rangkaian. Dengan menghantar berbilang dokumen
sekali gus, kami mengurangkan bilangan kali pelayan perlu mengekod dan menghantar
mesej, yang meningkatkan prestasi bukan sahaja untuk pelayan tetapi juga
untuk pelanggan yang perlu menyahkod mesej.

Dalam contoh di atas, saiz kelompok ditetapkan kepada 10, tetapi pelanggan boleh melaraskannya berdasarkan keadaan rangkaian dan saiz dokumen.

Gunakan penstriman dua arah untuk mengawal aliran

Contoh kedai buku mengembalikan semua buku dan menamatkan strim, tetapi jika pelanggan
perlu menonton acara dalam masa nyata (cth., penderia), penggunaan dwiarah
penstriman ialah pilihan yang tepat.

Strim dua arah agak rumit kerana kedua-dua pelanggan dan pelayan
boleh menghantar dan menerima mesej pada masa yang sama. Semoga golang dipermudahkan
untuk bekerja dengan konkurensi seperti ini.

Seperti yang dinyatakan, penderia boleh menjadi contoh terbaik penstriman dua arah.
Fungsi jam tangan membolehkan pelanggan memutuskan penderia yang hendak ditonton dan diminta
nilai semasa jika perlu.

Mari kita lihat definisi protobuf berikut:

service MyService {
  rpc GetSomething (SomethingRequest) returns (stream SomethingResponse) {}
}

Mesej permintaan bukan sahaja aliran mesej tetapi juga mesej yang boleh
mengandungi pelbagai jenis permintaan. Arahan satu daripadanya membolehkan kami mentakrifkan
medan yang boleh mengandungi hanya satu daripada jenis yang ditentukan.

Kod golang untuk penderia akan diabaikan, tetapi anda boleh menemuinya di sini

serverStream membungkus strim dan data penderia untuk menjadikannya lebih mudah untuk digunakan.

service MyService {
  rpc GetSomething (SomethingRequest) returns (SomethingResponse) {}
}

Seperti yang dinyatakan sebelum ini, pelayan boleh menghantar dan menerima mesej pada masa yang sama, satu
fungsi akan mengendalikan mesej masuk dan fungsi lain akan mengendalikan
mesej keluar.

Menerima mesej:

type somethingUnary struct {
    pb.UnimplementedSomethingUnaryServer
}

func (s *somethingUnary) GetSomething(ctx context.Context, req *pb.SomethingRequest) (*pb.SomethingResponse, error) {
    return &pb.SomethingResponse{
        Message: "Hello " + req.Name,
    }, nil
}

func TestSomethingUnary(t *testing.T) {
    conn := newServer(t, func(s grpc.ServiceRegistrar) {
        pb.RegisterSomethingUnaryServer(s, &somethingUnary{})
    })

    client := pb.NewSomethingUnaryClient(conn)

    response, err := client.GetSomething(
        context.Background(),
        &pb.SomethingRequest{
            Name: "test",
        },
    )
    if err != nil {
        t.Fatalf("failed to get something: %v", err)
    }

    if response.Message != "Hello test" {
        t.Errorf("unexpected response: %v", response.Message)
    }
}

Pernyataan suis digunakan untuk mengendalikan pelbagai jenis permintaan dan membuat keputusan
apa yang perlu dilakukan dengan setiap permintaan. Adalah penting untuk meninggalkan fungsi recvLoop sahaja
untuk membaca dan tidak menghantar mesej kepada pelanggan atas sebab ini kami mempunyai sendLoop
yang akan membaca mesej daripada saluran kawalan dan menghantarnya kepada pelanggan.

Menghantar mesej:

type somethingStream struct {
    pb.UnimplementedSomethingStreamServer
}

func (s *somethingStream) GetSomething(req *pb.SomethingRequest, stream pb.SomethingStream_GetSomethingServer) error {
    if err := stream.Send(&pb.SomethingResponse{
        Message: "Hello " + req.Name,
    }); err != nil {
        return err
    }

    return nil
}

func TestSomethingStream(t *testing.T) {
    conn := newServer(t, func(s grpc.ServiceRegistrar) {
        pb.RegisterSomethingStreamServer(s, &somethingStream{})
    })

    client := pb.NewSomethingStreamClient(conn)

    stream, err := client.GetSomething(
        context.Background(),
        &pb.SomethingRequest{
            Name: "test",
        },
    )
    if err != nil {
        t.Fatalf("failed to get something stream: %v", err)
    }

    response, err := stream.Recv()
    if err != nil {
        t.Fatalf("failed to receive response: %v", err)
    }

    if response.Message != "Hello test" {
        t.Errorf("unexpected response: %v", response.Message)
    }
}

Fungsi sendLoop membaca kedua-dua saluran kawalan dan saluran data serta menghantar
mesej kepada klien. Jika strim ditutup, fungsi akan kembali.

Akhir sekali, ujian laluan gembira untuk perkhidmatan sensor:

service BookStore {
  rpc ListBooks(ListBooksRequest) returns (stream Book) {}
}

service BookStoreBatch {
  rpc ListBooks(ListBooksRequest) returns (stream ListBooksResponse) {}
}

message ListBooksResponse {
  repeated Book books = 1;
}

Daripada ujian di atas, kita dapat melihat bahawa pelanggan boleh membuat, membatalkan dan mendapatkan semasa
nilai sensor. Pelanggan juga boleh menonton berbilang penderia pada masa yang sama.

Cabar Diri Anda

  • Laksanakan aplikasi sembang menggunakan penstriman gRPC.
  • Ubah suai perkhidmatan penderia untuk menghantar berbilang nilai sekaligus untuk menjimatkan perjalanan pergi dan balik.
  • Hidu trafik rangkaian untuk melihat perbezaan antara permintaan unary dan permintaan penstriman.

Kesimpulan

Penstriman gRPC ialah alat yang serba boleh dan berkuasa untuk membina aplikasi masa nyata.
Dengan mengikuti amalan terbaik seperti menggunakan penstriman hanya apabila perlu, mengumpulkan data dengan cekap dan memanfaatkan penstriman dua arah dengan bijak, pembangun boleh memaksimumkan prestasi
dan mengekalkan kesederhanaan kod.
Walaupun penstriman gRPC memperkenalkan kerumitan, faedahnya jauh mengatasi cabaran
apabila diterapkan dengan penuh pertimbangan.

Kekal berhubung

Jika anda mempunyai sebarang soalan atau maklum balas, sila hubungi saya di LinkedIn.

Atas ialah kandungan terperinci Penstriman gRPC: Amalan Terbaik dan Cerapan Prestasi. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn