gRPC & Python

Денис Катаев

tinkoff.ru

Сервисы

REST vs SOAP vs RPC

SOAP

и WSDL друганя его

 

RPC

XML-RPC

JSON-RPC

REST

и многообразие HTTP методов

 

А что не так то?

уже немного БЕСИТ

  • Микросервисы
  • Вездесущий JSON (конвертация в datetime, ...)
  • Аргументы (GET | POST | JSON in Body)
  • Ошибки (HTTP status | response)
  • Стриминг (Web Sockets | SSE)

gRPC

Поверх HTTP/2

Бинарный протокол сериализации

Protobuf

.proto файлы

Apache Avro

  • dynamic schema
  • schema registry

Apache Thrift

RPC protocol

 

Protobuf vs Avro

vs Thrift

Голосовой помощник
Олег

Сервис склейки

template = "Итак, мы переводим {sum} {name}"
slots = {"sum": "1000", "name": "Азамату"}
prefix.wav + azamat.wav + 1000_rub.wav

Protobuf

Protocol Buffers

syntax = "proto3";

.proto

text to speech

Message

syntax = "proto3";

message TemplateInput {
    string template           = 1;
    map<string, string> slots = 2;
    string backup_phrase      = 3;
};
t = TemplateInput(
    template='Итак, мы переводим {sum} {name}', 
    slots={'sum': '1000', 'name': 'Азамату'}
)
b = t.SerializeToString()

b'\n.\xd0\x98\xd1\x82\xd0\xb0\xd0\xba, \xd0\xbc\xd1\x8b \xd0\xbf\xd0\xb5\xd1\x80\xd0\xb5\xd0\xb2\xd0\xbe\xd0\xb4\xd0\xb8\xd0\xbc {sum} {name}\x12\x16\n\x04name\x12\x0e\xd0\x90\xd0\xb7\xd0\xb0\xd0\xbc\xd0\xb0\xd1\x82\xd1\x83\x12\x0b\n\x03sum\x12\x041000'
t = TemplateInput(
    template='Итак, мы переводим {sum} {name}', 
    slots={'sum': '1000', 'name': 'Азамату'}
)
b = t.SerializeToString()
t = TemplateInput(
    template='Итак, мы переводим {sum} {name}', 
    slots={'sum': '1000', 'name': 'Азамату'}
)
b = t.SerializeToString()

Scalar

  • bool
  • bytes & string
  • double & float
  • int32 & int64
  • uint32 & uint64
  • sint32 & sint64
  • fixed32 & fixed64 (+s)

Enum


enum AudioEncoding {
    ENCODING_UNSPECIFIED = 0;
    LINEAR16 = 1;
    reserved "FLAC";
    reserved 2;
}

OneOf

Сахар

message Foo {
    string test = 1;
}
message Bar {
    string test = 1;
}

message Result {
    string type = 1;
    oneof object {
        Foo foo = 2;
        Bar bar = 3;
    }
}

Repeated

Массивы

message Voice {
    repeated string language_codes = 1;
    string name = 2;
}

Maps

message TemplateInput {
    string template = 1;
    map<string, string> slots = 2;
    string backup_phrase = 3;
};

Packages & imports

package tinkoff.tts;


import "google/protobuf/duration.proto";

Code Generation

Protoc

🤢 .gitmodules 🤢

Docker

FROM znly/protoc as proto

FROM python:3.7 as build_proto
COPY --from=proto /protobuf/google/ /protobuf/google/
RUN pip install --no-cache grpcio-tools
COPY ./tts.proto /source/tts.proto

FROM python:3.7-alpine
COPY --from=build_proto \
   /source/output/tts_pb2.py \
   /your_project/tts_pb2.py

Docker compose run

copy compiled data from docker for local develop

protobuf:
    command: sh -c "cp -r /your_project/tts_pb2.py /mnt"
    build:
      context: protobuf
      dockerfile: Dockerfile
    volumes:
      - ./:/mnt

Code gen?

Python модуль


_TEMPLATEINPUT = _descriptor.Descriptor(
  name='TemplateInput',
  full_name='tinkoff.cloud.tts.v1.TemplateInput',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='template', full_name='tinkoff.TemplateInput.template', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='slots', full_name='tinkoff.TemplateInput.slots', index=1,
      number=2, type=11, cpp_type=10, label=3,
      has_default_value=False, default_value=[],
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='backup_phrase', full_name='tinkoff.TemplateInput.backup_phrase', index=2,
      number=3, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
  ],
  extensions=[
  ],
  nested_types=[_TEMPLATEINPUT_SLOTSENTRY, ],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto3',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=366,
  serialized_end=531,
)

Классы

from tts_pb2 import (
    TemplateInput,
    LINEAR16
)

Как использовать

from tts_pb2 import TemplateInput

tmpl = TemplateInput(
    template='some str',
    slots={}
)

Как использовать

from tts_pb2 import TemplateInput

tmpl = TemplateInput()
# 👇 не работает!!!
tmpl.slots = {} # 👈 не работает!!!

# Только изменение
tmpl.slots.update(test=1)

Как использовать

from tts_pb2 import TemplateInput

tmpl = TemplateInput()
tmpl.no_field = 1 # raises AttributeError
tmpl.template = 1234 # raises TypeError

Как использовать

from tts_pb2 import TemplateInput

tmpl.IsInitialized()
tmpl.__str__()
tmpl.CopyFrom(other_mgs)
tmpl.Clear()

Байты

from tts_pb2 import TemplateInput

tmpl = TemplateInput(template='test')
blob: bytes = tmpl.SerializeToString()

tmpl = TemplateInput()
tmpl.ParseFromString(data: bytes)

pip install grpcio

поверх HTTP/2

Binary

Bi-direction

Secure

Fast

С++ over Cython на борту

gRPC service

syntax = "proto3";

package tinkoff.tts;

import "google/api/annotations.proto";

service TextToSpeech {
    rpc Synthesize (SynthesizeRequest) 
        returns (SynthesizeResponse);
    }
    rpc StreamingSynthesize (SynthesizeRequest) 
        returns (stream StreamingSpeechResponse);
}
message TemplateInput {
    string template = 1;
    map<string, string> slots = 2;
    string backup_phrase = 3;
};

message SynthesizeSpeechRequest {
    SynthesisInput input = 1;
    VoiceSelectionParams voice = 2;
    AudioConfig audio_config = 3;
    TemplateInput template_input = 4;
}

Code Gen

*_pb2_grpc.py

from tts_pb2_grpc import TextToSpeechServicer

class Servicer(TextToSpeechServicer):
    def Synthesize(self, request, context):
        pass

    def StreamingSynthesize(self, request, context):
        pass
from grpc import ServicerContext

from tts_pb2 import (SynthesizeRequest, 
                     SynthesizeResponse,
                     StreamingSynthesizeResponse)
from tts_pb2_grpc import TextToSpeechServicer


class Servicer(TextToSpeechServicer):
    def Synthesize(
            self, request: SynthesizeRequest, 
            context: ServicerContext
    ) -> SynthesizeResponse:
        return SynthesizeResponse()

    def StreamingSynthesize(
            self, request: SynthesizeRequest, 
            context: ServicerContext
    ) -> StreamingSynthesizeResponse:
        yield StreamingSynthesizeResponse()

Server run

from concurrent import futures

import grpc

from tts_pb2_grpc import (
    add_TextToSpeechServicer_to_server)

from code import Servicer


servicer = Servicer()

pool = futures.ThreadPoolExecutor()
server = grpc.server(pool)
add_TextToSpeechServicer_to_server(
    servicer, server)
server.add_insecure_port(address)
import signal
import time
import sys

server.run()


def stop(timeout=None):
    server.stop(timeout)
    pool.shutdown(wait=bool(timeout))
    sys.exit()

signal.signal(signal.SIGTERM, lambda *args: stop(15))
signal.signal(signal.SIGINT, lambda *args: stop())

while True:
    time.sleep(_ONE_DAY_IN_SECONDS)

Many services

add_TextToSpeechServicer_to_server(
    servicer, server)
add_SpeechToTextServicer_to_server(
    servicer, server)

Client

import gprc
from tts_pb2_grpc import TextToSpeechStub

endpoint = 'localhost:50051'
channel = grpc.insecure_channel(endpoint)
stub = TextToSpeechStub(channel)

Stub

from tts_pb2 import (
    SynthesizeRequest, SynthesizeResponse
)

request = SynthesizeRequest()
response = stub.Synthesize(request)
assert isinstance(response, SynthesizeResponse)

RPC

Deadlines & timeouts

Metadata

headers

Auth

jwt

awesome-grpc

  • grpcurl
  • grcpui
  • gh2-bench
  • purerpc
from hypothesis_protobuf import modules_to_strategies
import tts_pb2

strategies = modules_to_strategies(tts_pb2)


@given(instant_message=strategies)
def test_instant_message_processor(instant_message):
    assert process_message(instant_message)

Тесты

pip install pytest-grpc

    def _end_unary_response_blocking(state, call, with_call, deadline):
        if state.code is grpc.StatusCode.OK:
            if with_call:
                rendezvous = _Rendezvous(state, call, None, deadline)
                return state.response, rendezvous
            else:
                return state.response
        else:
>           raise _Rendezvous(state, None, None, deadline)
E           grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
E           	status = StatusCode.UNKNOWN
E           	details = "Exception calling application: Some error"
E           	debug_error_string = "{"created":"@1552312084.410564000","description":"Error received from peer","file":"src/core/lib/surface/call.cc","file_line":1039,"grpc_message":"Exception calling application: Some error","grpc_status":2}"
E           >

Traceroute

Работа напрямую

по желанию

grpc_stub = <stub.test_pb2_grpc.EchoServiceStub object at #>

    def test_example(grpc_stub):
        request = EchoRequest()
>       response = grpc_stub.error_handler(request)

test_example.py:35:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../pytest_grpc/plugin.py:79: in fake_handler
    return real_method(request, context)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def error_handler(self, request: EchoRequest, context):
>       raise RuntimeError('Some error')
E       RuntimeError: Some error

src/servicer.py:10: RuntimeError
import pytest

from stub.test_pb2 import EchoRequest


@pytest.fixture(scope='module')
def grpc_add_to_server():
    from stub.test_pb2_grpc import (
        add_EchoServiceServicer_to_server
    )

    return add_EchoServiceServicer_to_server
@pytest.fixture(scope='module')
def grpc_servicer():
    from servicer import Servicer

    return Servicer()


@pytest.fixture(scope='module')
def grpc_stub_cls(grpc_channel):
    from stub.test_pb2_grpc import EchoServiceStub

    return EchoServiceStub
def test_some(grpc_stub):
    request = EchoRequest()
    response = grpc_stub.handler(request)

    assert response.name == f'test-{request.name}'

def test_example(grpc_stub):
    request = EchoRequest()
    response = grpc_stub.error_handler(request)

    assert response.name == f'test-{request.name}'

Два режима запуска

# с тредами и ближе к проду
$ py.test


# без транспорта, вызовы напрямую
# но чище трейсы
$ py.test --grpc-fake-server 

gRPC

Хорошая альтернатива

  • telegram: @kataev
  • denis.a.kataev@gmail.com

Вопросы?

gRPC & python

By Denis Kataev

gRPC & python

  • 1,403