Если вы используете микро-сервисную архитектуру, то скорее всего знаете, что накладные расходы на коммуникацию между сервисами часто становятся значительной проблемой и если Вы сталкивались с этой проблемой, то скорее всего начали использовать один из RPC фреймворков поверх Protobuf, например, gRPC от Google или Go-Kit от Peter Bourgon или что-то другое. Пересказывать что это и как этим пользоваться нет смысла, все достаточно хорошо описано до меня. Я сам активно использую gRPC в своих проектах, но тут Twich решил выпустить свою реализацию protobuf Twirp. Если Вам интересно, зачем им это было нужно или чем она отличается проходите под кат.

Прежде всего давайте посмотрим на причины заставившие Twich релизить свою собственную версию ProtoBuf:

  • Отсутствие поддержки HTTP 1.1. gRPS опирается на HTTP-трейлеры и полно-дуплексные потоки (full-duplex streams). Twirp поддерживает и HTTP 1.1 и HTTP/2, что очень важно потому что большое количество load-balancer-ов (как хардварных, так и софтверных) поддерживают только HTTP 1.1 — включая AWS Elastic Load Balancer. Но в отличии от gRPC Twirp не поддерживает стримингового RPC, что в случае когда Ваш API построен по принципу Request-Response и не требуется.
  • Cложность реализации библиотеки grpc-go. Библиотека включает полную реализации HTTP/2, независимую от стандартных библиотек, что делает сложным ее пониманию и анализ возникающих ошибок.
  • Cовместимость версий gRPC. В силу того, что gRPC довольно сложен, генерируемый Go код довольно простой и все запросы перенаправляются в grpc-go. Такая связанность приводит к тому, что клиент вынужден использовать ту же самую версию, что и сервер. И если у Вас большое количество клиентов и сервис взаимодействую друг с другом, то версия между ними должна быть идентичная. Понятно, что это приводит к сложностям в деплойменте и развертыванию микросервисов.
  • Также Twitch указывают, что grpc-go требует определенную версию protobuf — github.com/golang/protobuf. Но для меня эта проблема кажется надуманной, так как protobuf имеет только один релиз версии v1.0.0, который используется всеми версиями grpc-go.
  • gRPC поддерживает только бинарную форму сообщений и сниффинг сообщений очень сложным для анализа. Twirp поддерживает как бинарную форму сообщения в формате protobuf, так и в небинарные в формате JSON. Это вам дает преимущество, скажем если вы хотите взаимодействовать с сервисом через обычный HTTP Request посредством JSON

Как видите, простота это основновная причина, по которой Twich решили написать свою реализацию Protobuf.

Теперь давайте посмотрим, как же использовать эту библиотеку.

Если у вас уже настроена среда разработки на Go, то Вам нужно установить следующие пакеты

go get github.com/twitchtv/twirp/protoc-gen-twirp
go get github.com/golang/protobuf/protoc-gen-go

Для пример напишем простой сервис, который инкрементит значение переданное в качестве параметра.

syntax = "proto3";

service Service {
   rpc Increment(Request) returns (Response);
}

message Request {
   int32 valueToIncrement = 1; // must be > 0
}

message Response {
   int32 IncrementedValue = 1; // must be > 0
}

Сгенерируем код для нашего клиента выполнив следующую команду

protoc --proto_path=$GOPATH/src:. --twirp_out=. --go_out=. ./paperclips.proto

В результате буду созданы два файла

  • Increment.pb.go — содержит кодо-генерацию для сообщений
  • Increment.twirp.go — содержит интерфейсы и функции сервиса

Дальше добавим реализацию нашего сервиса

package main

import (
  "fmt"
  "log"
  "net/http"
  "context"
  pb "TwirpSample/Server/Twirp"
)

// Server implements the Increment service
type Server struct {
  value int32
}

// NewServer creates an instance of our server
func NewServer() *Server {
  return &Server{
     value: 1,
  }
}

// Increment returns the incremented value of request.ValueToIncrement
func (s *Server) Increment(ctx context.Context, request *pb.Request) (*pb.Response, error) {
  return &pb.Response{
     IncrementedValue: request.ValueToIncrement + 1,
  }, nil
}

func main() {
  fmt.Printf("Starting Increment Service on :6666")

  server := NewServer()
  twirpHandler := pb.NewServiceServer(server, nil)

  log.Fatal(http.ListenAndServe(":6666", twirpHandler))
}

Теперь, если вы запустите клиента командой go run main.goм к сервису можно будет обратиться как по HTTP:

curl --request "POST"      --location "http://localhost:6666/Service/Increment"      --header "Content-Type:application/json"      --data '{ValueToIncrement: 0}'      --verbose


Output:
{"IncrementedValue":1}

Или в бинарном формате

package main

import
  (
  "fmt"
  rpc "TwirpSample/Server/Twirp"
  "net/http"
  "context"
)

func main() {

  fmt.Println("Twirp Client Example.")

  client := rpc.NewServiceProtobufClient("http://localhost:6666", &http.Client{})
  v, err := client.Increment(context.Background(), &rpc.Request{ValueToIncrement: 11})

  if err != nil {
     fmt.Println(err.Error())
  }
  fmt.Printf("Value: %d", v.IncrementedValue)
}


Output:
Twirp Client Example.
Value:  11

В целом сам фреймворк практически идентичен по подходам с gRPC, но прост в реализации и с одновременной поддержкой HTTP 1.1. На мой взгляд его применимость, если вам необходим RPC сервис, с который вы планируем одновременно взаимодействовать с UI посредством HTTP и между сервисами посредством Protobuf.

Ссылки:

Комментарии (17)


  1. Dmitri-D
    01.05.2018 03:08

    gRPC тоже не менялся и остается совместимым с самим собой старых версий. Так что не вижу проблем в этом плане. Про балансер — да, согласен. Но они и не заботились, что у AWS что-то там не сделано.
    Если говорить о проблемах в использовании gRPC, когда много разных клиентов сидят на разных версиях, вероятно это девелопер создавал сам себе такие проблемы. Наиболее простой способ выстрелить себе в ногу — это выпустить обновление на сервер, и там поменять тэги существующих полей или, например, добавить обязательное поле, о котором старые клиенты ничего знают и послать в запросе не могут. Поэтому правила очень простые — поля только добавляются, и теги только инкрементируются, все новые поля — необязательные. Если любое из этих условий выполнить невозможно, то создается новая точка RPC (т.е. новая процедура или метод), но старая должна продолжать работать пока есть хотя бы один клиент ее вызывающий.


    1. BOOTLOADER Автор
      01.05.2018 18:19

      У меня лично опыт негативного нет gRPC. Но у меня все инфраструктура новая на одной версии. Я пытался с ними связаться, чтобы получиться какие-то точные пояснения ошибок, но пока безуспешно. Насчет добавления удаления полей есть Reserved Fields которые вам позволяют избежать ошибок, но как я сказал — пока нет информации были ли причины именно в этом.


      1. Dmitri-D
        02.05.2018 05:18

        Reserved Fields которые вам позволяют избежать ошибок

        Ошибка возникнет, если например вы использовали необязательное поле «customer_id» с тегом 1. А затем вы удалили это поле из выдачи, а потом ввели поле допустим «order_id» и присвоили ему «свободный» тэг 1. Все старые клинеты начнут читать order_id думая что читают customer_id.
        Что бы этого не произошло вы добавляете поля с новыми тегами, и никогда не переиспользуете удалённые ранее. Так же никогда не удаляете то, что было хоть 1 раз объявлено как обязательное. Вот и вся премудрость. С протоколом там всё в порядке.
        Не в порядке с асинхронным режимом. Впрочем, я интенсивно тестировал прошлым летом. Может, с тех пор что-то и изменилось.


        1. BOOTLOADER Автор
          02.05.2018 18:55

          В спецификации так и написано, если вы удаляете или комментируете поле, используете reserved fields, чтобы избежать колизий.

          Reserved Fields

          If you update a message type by entirely removing a field, or commenting it out, future users can reuse the tag number when making their own updates to the type. This can cause severe issues if they later load old versions of the same .proto, including data corruption, privacy bugs, and so on. One way to make sure this doesn't happen is to specify that the field tags (and/or names, which can also cause issues for JSON serialization) of your deleted fields are reserved. The protocol buffer compiler will complain if any future users try to use these field identifiers.

          message Foo {
          reserved 2, 15, 9 to 11;
          reserved «foo», «bar»;
          }


          1. Dmitri-D
            03.05.2018 03:24

            Да, это об одном и том же. Просто reserved — это способ показать остальным _пользователям_, не gprs, что тег некоего поля был ранее использован.
            Но, если вы сам девелопер некоей rpc коммуникации, т.е. вы пишете классы-сообщения, — ваши пользователи и так не смогут переиспользовать удаленные теги. Поэтому вы можете с тем же успехом и не объявлять их reserved, а просто сам с собой договориться, что пользовать те номера уже нельзя. Как это сделать наиболее простым способом? Инкрементируя некий счётчик тегов. Т.е. для новых тегов использовать последний номер + 1, о чем я и говорил в начале.


  1. spein
    01.05.2018 10:50

    Используем go-micro в проекте, как по мне, очень неплохая альтернатива grpc, если считать его альтернативой, ведь он поддерживает grpc. Стандартно работает по http/1.1 и обладает всеми перечисленными преимуществами даже более: github.com/micro/go-micro


    1. awesomer
      01.05.2018 12:05
      -1

      gRPC — это протокол обмена данными.
      go-micro — это фреймворк для создания микросервисов, который берет на себя решение многих и многих проблем, в том числе и страшно далеких от протокола обмена данными.

      go-micro — это не альтернатива gRPC.

      Это инструмент для других целей.


      1. spein
        01.05.2018 13:42

        godoc.org/google.golang.org/grpc
        Я не спорю, что grpc это название протокола, но все-же это и библиотека одноименная. Я говорил о библиотеке и об ее функционале. В статье вроде не протокол же рассматривается?


        1. awesomer
          01.05.2018 16:54

          А че, в Go есть и альтернативные реализации gRPC?
          go-micro использует всю ту же библиотеку, что вы и напрямую бы стали.


  1. imanushin
    01.05.2018 11:51

    А есть сравнение — какая разница в скорости работы простых http вызовов против gRPC/Twirp ?


    Еще интересно — какой вообще порядок величин, по сравнению с дальнейшей десериализацией?


    Интересует именно цифры, естественно.



  1. awesomer
    01.05.2018 11:57

    то скорее всего начали использовать Protobuf и и его реализацию от Google gRPC


    Protobuf — и есть реализация Google.

    А gRPC — это протокол построенный поверх Protobuf


    1. BOOTLOADER Автор
      01.05.2018 18:24

      Да, вы правы — получить не совсем корректно. Просто есть реализация от Google, есть вот от Twich и от многих других: github.com/google/protobuf/blob/master/docs/third_party.md


      1. awesomer
        01.05.2018 21:28

        gRPC — это вовсе не «реализация протокола Protobuf»

        Написано неграмотно.
        Ну чтобы вам было понятно, это как сказать: «http — это реализация протокола TCP/IP».
        То, что TCP/IP лежит в основе http не делает http его реализацией. «Реализовать http на основе TCP/IP» — так сказать можно.


        1. BOOTLOADER Автор
          01.05.2018 21:56

          Да, gRPC использует Protobuf как язык описание интерфейсов, но такая форма не совсем понятная, поэтому для просто я использовал как «реализация протокола Protobuf».


          1. slonpts
            02.05.2018 03:46
            +1

            Статья хороша, но awesomer верно заметил, что фраза

            Protobuf и и его реализацию от Google gRPC или Go-Kit от Peter Bourgon
            будет путать следующие поколения программистов еще много лет. Наверное, лучше заменить на
            RPC фреймворк поверх Protobuf, например, gRPC от Google или Go-Kit от Peter Bourgon


            1. BOOTLOADER Автор
              02.05.2018 03:46

              Поправил. Спасибо.