gRPC
Rapira обслуживает унарные вызовы RPC из PHP. Один слушатель принимает вызовы gRPC, gRPC-Web и Connect по HTTP/1.1 или незашифрованному HTTP/2 через TCP или Unix-сокеты. Унарный вызов содержит одно сообщение запроса и одно сообщение ответа.
Пул gRPC использует режим Dispatcher. Rapira управляет транспортом и передаёт PHP бинарные сообщения protobuf. Каждый PHP-воркер обрабатывает один вызов за раз. Потоковые методы не поддерживаются. Слушатель не завершает TLS.
Запуск эхо-сервиса
Установите Rapira. Установите buf для сборки набора дескрипторов и grpcurl для выполнения клиентских команд. Этот пример возвращает байты запроса в качестве ответа. Для него не нужны сгенерированные классы PHP или расширение gRPC для PHP.
Создайте следующую структуру каталогов:
app/
├── proto/
│ └── echo.proto
├── grpc.php
└── rapira.tomlОпределение сервиса
Сохраните эту схему в proto/echo.proto:
syntax = "proto3";
package example.v1;
option php_namespace = "Example\\V1";
option php_metadata_namespace = "Example\\Metadata";
service Echo {
rpc Echo(EchoMessage) returns (EchoMessage);
}
message EchoMessage {
string text = 1;
}Маршрут имеет вид /example.v1.Echo/Echo. Контекст PHP предоставляет имя метода example.v1.Echo/Echo без начальной косой черты.
Сборка набора дескрипторов
Rapira читает схему из набора дескрипторов: бинарного google.protobuf.FileDescriptorSet, который содержит все импортируемые файлы. Соберите его из app/:
buf build proto --as-file-descriptor-set -o api.binpbЕго также может собрать protoc. Добавьте --include_imports, потому что без этого флага protoc не включает импортируемые файлы:
protoc --include_imports --descriptor_set_out=api.binpb -I proto proto/echo.protoИсполняемый файл protoc не нужен Rapira во время работы.
Диспетчер PHP
Сохраните этот скрипт в grpc.php:
<?php
use Rapira\Exception\ClosedException;
use Rapira\Exception\WorkDiscardedException;
$dispatcher = Rapira\get_dispatcher();
try {
while (true) {
$call = $dispatcher->receive();
try {
$metadata = $call->getResponseMetadata();
$metadata->addHeader('x-worker', (string) getmypid());
$metadata->addTrailer('x-result', 'echoed');
$call->respond($call->getMessage());
} catch (WorkDiscardedException) {
continue;
}
}
} catch (ClosedException) {
return;
}Запрос и ответ используют один тип сообщения, поэтому обработчик может вернуть байты напрямую. ClosedException завершает цикл при остановке. WorkDiscardedException означает, что хост уже отменил вызов.
Настройка слушателя и пула
Сохраните эту конфигурацию в rapira.toml:
[grpc]
listen = "127.0.0.1:50051"
descriptor_set = "api.binpb"
reflection = true
[grpc.pool]
entrypoint = "grpc.php"
mode = "dispatcher"
processes = 2descriptor_set использует каталог файла конфигурации как базовый. Конфигурации только для gRPC не нужна секция [http].
Запустите сервер из app/:
rapira serve rapira.tomlВызов сервиса
Получите список сервисов из другого терминала:
grpcurl -plaintext 127.0.0.1:50051 listВызовите эхо-метод:
grpcurl -plaintext -d '{"text":"hello"}' 127.0.0.1:50051 example.v1.Echo/EchoОтвет:
{
"text": "hello"
}grpcurl получает схему через рефлексию. Добавьте -v перед адресом, чтобы посмотреть заголовки и трейлеры ответа.
Клиент Connect может отправить JSON. Rapira преобразует JSON-запрос в бинарный protobuf до того, как его получит PHP, и преобразует бинарный ответ обратно в JSON:
curl -H 'Content-Type: application/json' -d '{"text":"hello"}' http://127.0.0.1:50051/example.v1.Echo/EchoИспользование сгенерированных сообщений PHP
Сгенерируйте классы PHP, если обработчику нужно читать или изменять поля сообщения. Установите protoc и Composer для этого этапа сборки.
Выполните эти команды из app/:
composer require google/protobuf
mkdir -p generated
protoc --proto_path=proto --php_out=generated proto/echo.protoДобавьте эти сопоставления пространств имён в объект autoload.psr-4 файла composer.json:
{
"autoload": {
"psr-4": {
"Example\\V1\\": "generated/Example/V1/",
"Example\\Metadata\\": "generated/Example/Metadata/"
}
}
}Обновите автозагрузчик:
composer dump-autoloadПодключите его перед циклом диспетчера в grpc.php:
require __DIR__ . '/vendor/autoload.php';Замените строку с respond() внутри вложенного блока try следующим кодом:
$message = new Example\V1\EchoMessage();
$message->mergeFromString($call->getMessage());
$message->setText(strtoupper($message->getText()));
$call->respond($message->serializeToString());Перезапустите сервер примера. Теперь тот же клиентский вызов возвращает {"text":"HELLO"}. В обработчике приложения перехватывайте исключения разбора protobuf и отправляйте StatusCode::InvalidArgument.
Руководство по сгенерированному коду PHP описывает методы доступа к полям и сериализацию. Для этого серверного API не требуется расширение gRPC для PHP.
Контракт диспетчера
В воркере gRPC функция Rapira\get_dispatcher() возвращает Rapira\Grpc\GrpcDispatcher. Инициализируйте автозагрузчик и общие сервисы приложения до цикла.
| API | Поведение |
|---|---|
name() | Возвращает "grpc". |
getInfo(): GrpcDispatcherInfo | Возвращает снимок счётчиков этого воркера. pendingCount() считает ожидающие вызовы. activeCount() равен 0 или 1. |
receive(int $timeout = -1) | Возвращает следующий UnaryCall. Время ожидания задаётся в микросекундах. -1 означает неограниченное ожидание. При истечении времени выбрасывает Rapira\Exception\TimeoutException. |
tryReceive() | Возвращает UnaryCall или null, если ожидающих вызовов нет. Не ждёт. |
getServices() | Возвращает список обслуживаемых сервисов, их методов, входных и выходных типов и видов методов. Потоковые методы входят в список. Доступен до первого вызова. |
$call->getContext() | Возвращает метод, метаданные, адрес клиента, протокол, время получения и предельный срок. |
$call->getMessage() | Возвращает сообщение запроса как бинарный protobuf в строке PHP. |
$call->respond(string $message) | Завершает вызов одним сериализованным ответом protobuf. |
$call->fail(Status $status) | Завершает вызов со статусом ошибки gRPC. |
$call->isCancelled() | Сообщает об отмене клиентом, закрытии соединения или истечении предельного срока. |
$call->isFinalized() | Сообщает, завершён ли вызов. |
Завершите текущий вызов перед получением следующего. Пока вызов открыт, receive() выбрасывает \Error. Повторное завершение выбрасывает Rapira\Exception\AlreadyFinalizedError. Ответ после отмены выбрасывает WorkDiscardedException.
Вызов, который PHP не завершил, теряется. Тогда клиент получает INTERNAL с сообщением internal error. Неперехваченное исключение тоже приводит к потере вызова, и клиент не видит его сообщение.
Используйте $call->getContext()->method для выбора обработчика, если схема определяет несколько методов. $call->getContext()->protocol имеет значение Grpc, GrpcWeb или Connect. Очищайте состояние приложения, относящееся к вызову, перед следующей итерацией. Диспетчер не заполняет суперглобальные переменные HTTP.
Возврат ошибок и подробностей
Используйте fail() для ожидаемой ошибки приложения. Выполните этот код в обработчике текущего вызова:
$call->fail(new Rapira\Grpc\Status(
Rapira\Grpc\StatusCode::InvalidArgument,
'text is required',
));StatusCode содержит 16 кодов ошибок gRPC. В нём нет варианта Ok. Успешный respond() отправляет статус OK. fail() - единственный способ отправить статус ошибки.
Необязательный третий аргумент Status - список объектов Rapira\Grpc\ErrorDetail. Каждый объект содержит URL типа protobuf и байты сериализованного сообщения. Для gRPC и gRPC-Web Rapira отправляет подробности в grpc-status-details-bin. Для Connect она отправляет их в теле ошибки JSON.
Rapira не перехватывает Rapira\Grpc\Exception\GrpcException. Перехватите его и передайте его свойство $status в fail().
Клиент получает UNAVAILABLE, если Rapira отклоняет вызов до того, как его получит PHP. Это происходит, когда очередь воркеров остаётся заполненной 30 секунд, когда пул останавливается или когда загрузка PHP в воркере завершилась ошибкой. Интерцептор тоже может отклонить вызов до того, как его получит PHP. Статус задаёт интерцептор, например UNAUTHENTICATED. См. раздел Интерцепторы.
Метаданные
Читайте метаданные запроса из $call->getContext()->metadata. values($name) возвращает все значения для имени в порядке поступления без учёта регистра имени. Массив entries, доступный только для чтения, хранит имена в нижнем регистре.
Rapira удаляет из метаданных запроса транспортные имена, например grpc-timeout, content-type и te. Она отбрасывает текстовое значение, которое содержит не только печатные символы ASCII. Для имени с суффиксом -bin Rapira разделяет значение по , и декодирует каждую часть из base64. PHP получает необработанные байты. Часть, которая не декодируется, отбрасывается.
Добавляйте метаданные ответа перед respond() или fail():
$requestId = $call->getContext()->metadata->values('x-request-id')[0] ?? '';
$metadata = $call->getResponseMetadata();
$metadata->addHeader('x-request-id', $requestId);
$metadata->addBinaryHeader('x-token-bin', "\x00\xff");
$metadata->addTrailer('x-result', 'completed');addHeader() и addTrailer() принимают значения из печатных символов ASCII. Пустое значение допустимо. При отправке Rapira удаляет начальные и конечные пробелы текстового значения. Для бинарных значений используйте addBinaryHeader() или addBinaryTrailer(). Имя бинарного значения должно заканчиваться на -bin.
Rapira переводит имена метаданных ответа в нижний регистр. После этого имя может содержать только 0-9, a-z, _, - и ., как требует протокол gRPC. Транспортное имя, некорректное имя или некорректное значение выбрасывает \ValueError. Текстовые методы отклоняют имена с суффиксом -bin. Повторное имя добавляет ещё одно значение.
headers() и trailers() возвращают снимки. Для Connect каждый трейлер передаётся как заголовок с префиксом trailer-. Передайте метаданные запроса через параметр grpcurl -H 'x-request-id: demo-1'.
Интерцепторы
Интерцептор проверяет вызов до того, как его получит PHP. Перечислите интерцепторы в grpc.interceptors в порядке цепочки. У каждого интерцептора есть своя таблица. В Rapira есть один интерцептор: auth.
Аутентификация
auth принимает вызов, только если вызов содержит настроенный Bearer-токен. Запишите токены в файл, по одному токену в строке:
# rapira gRPC tokens
2f1c9a7e4b0d4c8f9e3a
ci.deploy-tokenRapira пропускает пустые строки и строки, которые начинаются с #. Каждый токен состоит из символов A-Z, a-z, 0-9, -, ., _, ~, + и /, с необязательными символами = в конце. Затем включите интерцептор:
[grpc]
descriptor_set = "api.binpb"
interceptors = ["auth"]
[grpc.auth]
tokens_file = "grpc-tokens"Путь файла токенов использует каталог конфигурационного файла как базовый. Правила ключей описаны в разделе Конфигурация.
Клиент передаёт ровно одно значение метаданных authorization в виде Bearer <token>:
grpcurl -plaintext -H 'authorization: Bearer ci.deploy-token' 127.0.0.1:50051 listСлушатель не шифрует трафик, поэтому токен передаётся по сети в открытом виде. Если клиенты подключаются через сеть, которой вы не доверяете, поставьте TLS-прокси перед слушателем. См. RFC 6750 §5.3.
Вызов без действительного токена получает UNAUTHENTICATED. Унарный вызов Connect получает HTTP-статус 401 с заголовком WWW-Authenticate: Bearer. Rapira не читает тело запроса, и PHP не получает вызов.
grpc.health.v1.Healthне требует токена, потому что gRPC-пробы Kubernetes не могут передавать метаданные.- Рефлексия требует токена.
- Вызов неизвестного метода без токена получает
UNAUTHENTICATED, а неUNIMPLEMENTED. - PHP по-прежнему получает значение
authorizationвызова, который прошёл проверку.
Мастер читает файл токенов при запуске. Файл, который Rapira не может прочитать, файл без токенов и некорректный токен останавливают запуск. Перезапустите Rapira, чтобы загрузить изменённый файл токенов. Перезагрузка сохраняет старые токены.
Предельные сроки и отмена
Клиент задаёт таймаут через grpc-timeout (gRPC и gRPC-Web) или connect-timeout-ms (Connect). grpc.default_timeout_secs задаёт таймаут вызова, для которого клиент не задал таймаут. grpc.max_timeout_secs уменьшает более длинный таймаут клиента до своего значения. По умолчанию оба ключа не заданы, поэтому у вызова без таймаута клиента нет предельного срока. Такой вызов также читает сообщение запроса без ограничения времени. Задайте оба ключа, если клиенты не являются доверенными.
$call->getContext()->deadline содержит предельный срок как отметку времени Unix в секундах или null. receivedAt содержит время, когда Rapira прочитала сообщение запроса полностью.
Например, задайте клиенту предельный срок в две секунды:
grpcurl -plaintext -max-time 2 -d '{"text":"hello"}' 127.0.0.1:50051 example.v1.Echo/EchoКогда предельный срок истекает, клиент получает DEADLINE_EXCEEDED, а isCancelled() возвращает true. Rapira не может остановить код PHP, поэтому PHP продолжает обработку вызова. Проверяйте isCancelled() во время длительных операций. Перехватывайте WorkDiscardedException при вызове методов ответа, поскольку отмена может произойти после проверки.
Таймаут receive() задаёт, сколько PHP ждёт новую работу. Он не связан с предельным сроком вызова. grpc.pool.request_terminate_timeout_secs контролирует время работы процесса. Он заменяет воркер, если вызов выполняется дольше лимита.
Сервисы и рефлексия
Мастер загружает набор дескрипторов до создания воркеров через fork. Некорректный набор, отсутствие импортируемых файлов или неизвестный сервис в конфигурации не позволяют серверу запуститься. Перезапустите Rapira после изменения набора дескрипторов. Перезагрузка сохраняет загруженный набор.
По умолчанию пул обслуживает сервисы тех файлов, которые не импортирует ни один другой файл набора. Файл, импортируемый другим файлом, является зависимостью, например google/longrunning/operations.proto. Его сервисы не обслуживаются. Задайте grpc.services, чтобы указать обслуживаемые сервисы, например ["billing.v1.InvoiceService"]. Используйте этот ключ, если один набор используют несколько экземпляров Rapira, или чтобы обслуживать сервис импортируемого файла.
Потоковый метод, метод сервиса, который пул не обслуживает, и неизвестный метод возвращают UNIMPLEMENTED. При запуске Rapira записывает предупреждение для каждого потокового метода обслуживаемого сервиса.
Рефлексия по умолчанию отключена. При reflection = true Rapira обслуживает grpc.reflection.v1 и grpc.reflection.v1alpha. ListServices возвращает обслуживаемые сервисы. Все файлы и символы набора дескрипторов доступны, поэтому каждый клиент может прочитать весь набор.
При reflection = false передайте схему клиенту:
grpcurl -plaintext -import-path proto -proto echo.proto -d '{"text":"hello"}' 127.0.0.1:50051 example.v1.Echo/EchoПроверки состояния
Rapira обслуживает протокол проверки состояния gRPC (grpc.health.v1.Health) в каждом воркере. Check и Watch сообщают SERVING для пустого имени "" и для каждого обслуживаемого сервиса. Во время остановки они сообщают NOT_SERVING.
Сервис проверки состояния не проверяет PHP. Воркер, в котором загрузка PHP завершилась ошибкой, сообщает SERVING, а его вызовы получают UNAVAILABLE. grpc.services не может указывать сервисы проверки состояния или рефлексии, потому что Rapira обслуживает их сама.
Рефлексия не показывает сервис проверки состояния. Запросу Connect в формате JSON схема не нужна:
curl -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:50051/grpc.health.v1.Health/CheckПротоколы и ограничения
- Используйте бинарный gRPC-Web (
application/grpc-web+proto). Текстовый режим gRPC-Web не поддерживается и возвращаетUNIMPLEMENTED. - Для браузерных клиентов с другого origin настройте CORS на прокси. Rapira не обслуживает предварительные запросы CORS.
- Запрос Connect в формате JSON, который не декодируется, возвращает
INVALID_ARGUMENT, и PHP не получает вызов. Декодер JSON игнорирует неизвестные поля. Он не игнорирует имя значения перечисления, которое набор дескрипторов не объявляет. - Метод с
option idempotency_level = NO_SIDE_EFFECTS;также принимает запрос Connect GET. - Сообщения могут использовать сжатие gzip. Запрос с другим кодированием сообщения возвращает
UNIMPLEMENTED. - Запросы к PHP-сервисам имеют лимит тела 4 МиБ и отдельный лимит распакованного сообщения 4 МиБ. Более крупный запрос возвращает
RESOURCE_EXHAUSTED. Ключей TOML для изменения этих лимитов нет. - Ответ PHP, который не удаётся преобразовать в Connect JSON, возвращает
INTERNALи записывает предупреждение в журнал. Ответы в бинарном protobuf не используют это преобразование. - Декодер JSON не ограничивает память, которую используют элементы запроса. Запрос Connect в формате JSON размером 4 МиБ с большим числом мелких элементов может заставить воркер использовать несколько сотен МиБ для полей repeated или map. Для полей
google.protobuf.StructилиListValueворкер может использовать больше 1 ГиБ. Ограничение 4 МиБ действует после распаковки. Если клиенты не являются доверенными, поставьте перед слушателем прокси, который ограничивает размер распакованного запроса. $call->getContext()->tlsвсегда равенnull. Если клиентам нужен TLS, поставьте TLS-прокси перед слушателем. Для нативного gRPC прокси должен использовать HTTP/2 для соединения с Rapira.- Rapira отправляет HTTP/2 keepalive PING соединению, которое остаётся неактивным в течение
grpc.keepalive_interval_secs. Она закрывает соединение, если PING не получает ответа в течениеgrpc.keepalive_timeout_secs. По умолчанию оба ключа равны 10 секундам. В HTTP/1.1 нет PING, поэтому эти ключи не действуют для клиентов gRPC-Web или Connect по HTTP/1.1.
Одно соединение использует один воркер
Каждое соединение обслуживает один процесс воркера. Клиент gRPC обычно отправляет все вызовы канала по одному соединению HTTP/2. Такой клиент получает пропускную способность одного воркера при любом размере пула. Чтобы использовать больше воркеров, откройте несколько соединений или используйте балансировщик нагрузки L7, который распределяет вызовы.
Совместная работа HTTP и gRPC
Одна конфигурация может содержать и [http], и [grpc]. Каждый плагин имеет собственный слушатель, входной PHP-скрипт и пул воркеров. Мастер контролирует оба пула. Пул gRPC поддерживает те же настройки числа воркеров и замены, что и пул HTTP, с mode = "dispatcher".
Все настройки gRPC описаны в разделе Конфигурация, а надзор за пулами - в разделе Модель процессов.
Windows
Сборка для Windows обслуживает тот же слушатель gRPC и тот же API PHP. Действуют следующие отличия:
grpc.listenпринимает только адрес TCP.grpc.interceptorsи таблица[grpc.auth]недоступны.- Пул gRPC - статический пул потоков интерпретатора PHP в одном процессе.
grpc.pool.processesзадаёт число потоков. getmypid()возвращает один и тот же идентификатор процесса в каждом интерпретаторе.- Сбой загрузки PHP в любом из пулов останавливает сервер с кодом выхода 70.
Как применяются ограничения размера входящих данных?
Лимит тела запроса включает пятибайтовый заголовок конверта унарного запроса с фреймингом. Проверка состояния и рефлексия ограничивают каждое сообщение запроса 16 КиБ. Эти входящие лимиты не задают лимит размера ответа.
Какие ограничения действуют для содержимого Any в JSON-ответах?
Преобразование в JSON декодирует содержимое каждого google.protobuf.Any с бюджетом 32 МиБ для декодированных элементов. Превышение бюджета возвращает INTERNAL и записывает предупреждение в журнал. Клиенты бинарного protobuf не используют это преобразование.