gRPC և Protocol Buffers
Արագ, տիպավորված կանչեր ծառայությունների միջև՝ գեներացված client-ներով և streaming-ով HTTP/2-ի վրա։
30 վայրկյանում
gRPC-ն եղանակ է, որով ծառայությունները ցանցով կանչում են միմյանց ֆունկցիաները, կարծես դրանք լոկալ լինեին։ API-ն նկարագրում եք մեկ անգամ .proto ֆայլում, և գործիքները բազմաթիվ լեզուներով գեներացնում են տիպավորված client և server կոդ։ Հաղորդագրությունները կոդավորվում են Protocol Buffers-ով՝ կոմպակտ բինար ֆորմատով, և փոխանցվում HTTP/2-ով, որը թույլ է տալիս streaming երկու ուղղությամբ։ Այն փայլում է բեքենդի ներսում՝ միկրոծառայությունների միջև. բրաուզերներին լրացուցիչ շերտ է պետք։
Իմ ամենօրյա stack-ում չէ․ գիտեմ հիմնական գաղափարները և մանրամասները կսովորեմ, երբ նախագիծը դա պահանջի։
Ինչու է սա կարևոր
- Մեկ .proto պայմանագիրը գեներացնում է տիպավորված client-ներ Go-ի, Java-ի, Python-ի, Node.js-ի, C#-ի, Kotlin-ի և այլ լեզուների համար, ինչը վերացնում է թիմերի միջև ինտեգրացիոն բագերի մի ամբողջ դաս։
- Բինար Protobuf հաղորդագրությունները JSON-ից ավելի փոքր են և ավելի արագ են վերլուծվում, ինչը կարևոր է ներքին ծառայությունների միջև բարձր ծավալի հարցումների դեպքում։
- Ներկառուցված streaming-ը, deadline-ները և ստանդարտ status code-երը այն դարձնում են ուժեղ ընտրություն real-time հոսքերի և service mesh-երի համար։
- Խոշոր պլատֆորմներ, ինչպիսիք են Kubernetes-ը (etcd-ի և CRI-ի միջոցով), Envoy-ն և բազմաթիվ cloud API-ներ, օգտագործում են gRPC, ուստի արժե այն հասկանալ, երբ նախագիծը աճում է բազմաթիվ ծառայությունների։
Ինչպես է աշխատում
01Գաղափարը՝ remote procedure call-եր պայմանագրով
REST-ի դեպքում մտածում եք ռեսուրսներով և HTTP բայերով՝ GET /orders/42։ gRPC-ի դեպքում մտածում եք ֆունկցիաներով՝ OrderService.GetOrder(id)։ Սա կոչվում է RPC՝ remote procedure call։ Կարևոր տարբերությունը պայմանագիրն է։ gRPC-ում API-ն գրվում է .proto ֆայլում Protocol Buffers-ով (Protobuf)՝ ինտերֆեյսի սահմանման լեզվով։ Կոմպիլյատորը (protoc կամ Buf CLI) այդ ֆայլը վերածում է կոդի՝ հաղորդագրությունների կլասներ, client stub, որը կանչում եք սովորական մեթոդի պես, և server ինտերֆեյս, որը դուք իրականացնում եք։ Եթե սերվերը փոխում է դաշտի տիպը, client-ները, որոնք վերագեներացնում են կոդը, ստանում են կոմպիլյացիայի սխալ՝ production-ում անակնկալի փոխարեն։ .proto ֆայլը պատկերացրեք որպես իրավաբանորեն պարտադիր մենյու, որին պետք է հետևեն և՛ խոհանոցը, և՛ հաճախորդները։
- Protobuf. սխեմայի լեզուն և բինար կոդավորումը։
- Stub. գեներացված client օբյեկտ, որի մեթոդները ցանցային կանչեր են կատարում։
- Channel. երկարատև կապ սերվերի հասցեի հետ։
02.proto ֆայլի գրում
.proto ֆայլը հայտարարում է package, message-ներ (տվյալների կառուցվածքներ) և service-ներ (RPC մեթոդների խմբեր)։ Յուրաքանչյուր դաշտ ունի տիպ և դաշտի համար (field number)։ Ցանցով փոխանցվում է համարը, ոչ թե անունը, ուստի հրապարակելուց հետո այն երբեք չպետք է փոխվի կամ կրկին օգտագործվի։ proto3 շարահյուսությունը տարածված ընտրությունն է. Protobuf-ի ավելի նոր «editions»-ները feature flag-երով միավորում են proto2-ի և proto3-ի վարքագիծը, բայց proto3 ֆայլերը մնում են լիովին աջակցվող։ Ամսաթվերի համար օգտագործեք well-known տիպեր, օրինակ՝ google.protobuf.Timestamp, և յուրաքանչյուր մեթոդի համար պահեք մեկ request և մեկ response message, որպեսզի հետագայում կարողանաք դաշտեր ավելացնել՝ առանց signature-ները կոտրելու։
syntax = "proto3";
package shop.orders.v1;
import "google/protobuf/timestamp.proto";
service OrderService {
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc WatchOrders(WatchOrdersRequest) returns (stream OrderEvent);
}
message GetOrderRequest { string order_id = 1; }
message GetOrderResponse { Order order = 1; }
message WatchOrdersRequest { string customer_id = 1; }
message OrderEvent { Order order = 1; string type = 2; }
message Order {
string id = 1;
int64 total_cents = 2;
google.protobuf.Timestamp created_at = 3;
}
03Ներքուստ. HTTP/2, framing և status code-եր
gRPC-ն աշխատում է HTTP/2-ի վրա։ Յուրաքանչյուր կանչ HTTP/2 stream է ընդհանուր TCP կապի վրա, ուստի բազմաթիվ կանչեր մուլտիպլեքսավորվում են առանց HTTP մակարդակում head-of-line blocking-ի։ Հարցումը POST է /package.Service/Method հասցեին՝ content-type application/grpc-ով։ Յուրաքանչյուր հաղորդագրություն երկարության նախածանցով բինար Protobuf է։ Metadata-ն (header-ների նման) կրում է auth token-ներ և tracing ID-ներ։ Արդյունքներն ավարտվում են trailer-ներով, որոնք պարունակում են grpc-status և grpc-message։ gRPC-ն ունի իր սեփական status code-երը՝ HTTP-ից առանձին. OK, INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS, PERMISSION_DENIED, UNAUTHENTICATED, RESOURCE_EXHAUSTED, UNAVAILABLE, DEADLINE_EXCEEDED և այլն։ Client-ները պետք է կրկնեն միայն անվտանգ code-երի դեպքում, օրինակ՝ UNAVAILABLE, և միայն idempotent մեթոդների համար։
POST /shop.orders.v1.OrderService/GetOrder HTTP/2
content-type: application/grpc
te: trailers
authorization: Bearer <token>
grpc-timeout: 2S
<5-byte prefix><binary GetOrderRequest>
HTTP/2 200
content-type: application/grpc
<5-byte prefix><binary GetOrderResponse>
grpc-status: 0
04RPC-ի չորս տեսակները
gRPC-ն աջակցում է կանչի չորս ձև։ Unary-ն մեկ հարցում և մեկ պատասխան է՝ սովորական ֆունկցիայի պես։ Server streaming-ը ուղարկում է մեկ հարցում և ստանում պատասխանների հոսք. օգտակար է պատվերների live թարմացումների կամ մեծ արդյունքների համար։ Client streaming-ը ուղարկում է հարցումների հոսք և ստանում մեկ պատասխան. օգտակար է telemetry կամ chunk-եր վերբեռնելու համար։ Bidirectional streaming-ը թույլ է տալիս երկու կողմերին էլ ինքնուրույն ուղարկել նույն կանչի վրա. օգտագործվում է chat-ի, համատեղ խմբագրման կամ real-time կառավարման համար։ Մեկ կանչի ներսում stream-երը կարգավորված են, իսկ flow control-ը կատարում է HTTP/2-ը։ Երկարատև stream-երին պետք են keepalive-ներ և վերամիացման տրամաբանություն, որովհետև proxy-ներն ու load balancer-ները փակում են անգործուն կապերը։
- Unary: rpc Get(Req) returns (Res)
- Server streaming: rpc Watch(Req) returns (stream Event)
- Client streaming: rpc Upload(stream Chunk) returns (Summary)
- Bidirectional: rpc Chat(stream Msg) returns (stream Msg)
05Node.js server և client գործնականում
Node.js-ում պաշտոնական իրականացումը @grpc/grpc-js-ն է։ Կարող եք .proto-ն բեռնել runtime-ում @grpc/proto-loader-ով, ինչպես ստորև, կամ նախապես գեներացնել TypeScript կոդ Buf-ով և ts-proto կամ protobuf-es plugin-ներով՝ լիարժեք type safety-ի համար։ Յուրաքանչյուր client կանչ պետք է փոխանցի deadline. առանց դրա կախված սերվերը կարող է ռեսուրսները հավերժ պահել։ Production սերվերներն օգտագործում են TLS (createSsl) կամ plaintext-ով աշխատում են միայն mTLS ապահովող mesh-ի ներսում։
import * as grpc from "@grpc/grpc-js";
import * as loader from "@grpc/proto-loader";
const def = loader.loadSync("orders.proto", { keepCase: false, longs: String });
const pkg = grpc.loadPackageDefinition(def) as any;
const Svc = pkg.shop.orders.v1.OrderService;
const server = new grpc.Server();
server.addService(Svc.service, {
getOrder: (call: any, cb: any) => {
if (!call.request.orderId) return cb({ code: grpc.status.INVALID_ARGUMENT, details: "orderId required" });
cb(null, { order: { id: call.request.orderId, totalCents: "4200" } });
},
});
server.bindAsync("0.0.0.0:50051", grpc.ServerCredentials.createInsecure(), () => {});
const client = new Svc("localhost:50051", grpc.credentials.createInsecure());
client.getOrder({ orderId: "42" }, { deadline: Date.now() + 2000 }, console.log);
06gRPC-ն բրաուզերում և edge-ում
Բրաուզերները չեն կարող լիովին կառավարել HTTP/2 frame-երը կամ fetch-ով կարդալ trailer-ները, ուստի չեն կարող խոսել native gRPC-ով։ Կա երկու տարածված լուծում։ gRPC-Web-ը մի փոքր տարբեր wire ֆորմատ է, որը proxy-ն (Envoy) թարգմանում է իրական gRPC-ի. այն աջակցում է միայն unary և server streaming։ Connect-ը Buf-ի ավելի նոր պրոտոկոլների ընտանիք է, որի սերվերները միաժամանակ խոսում են gRPC, gRPC-Web և պարզ, HTTP/JSON-ի համար հարմար Connect պրոտոկոլով, ուստի բրաուզերը կարող է կանչել նույն ծառայությունը սովորական fetch-ով։ Շատ թիմեր փոխարենը պահում են REST կամ GraphQL հանրային edge-ում և gRPC-ն օգտագործում միայն դրա հետևում՝ grpc-gateway-ի կամ Envoy-ի նման gateway-ով, որը JSON-ը տրանսկոդավորում է gRPC-ի։
07Գործիքներ և production checklist
Օգտագործեք Buf CLI-ն՝ .proto ֆայլերը lint անելու, CI-ում կոտրող փոփոխությունները հայտնաբերելու և buf.gen.yaml-ից կոդը վերարտադրելի գեներացնելու համար։ Տարբերակավորեք package-ները անվան մեջ (orders.v1, orders.v2)։ Միացրեք ստանդարտ health checking ծառայությունը, որպեսզի load balancer-ները և Kubernetes probe-երը կարողանան ստուգել պատրաստվածությունը, իսկ ոչ production միջավայրերում՝ server reflection, որպեսզի grpcurl-ի նման գործիքները կարողանան ուսումնասիրել API-ն։ Փոխանցեք deadline-ները և tracing metadata-ն (OpenTelemetry) կանչերի միջով։ Հաղորդագրության չափի սահմանները դրեք գիտակցաբար. իրականացումների մեծ մասում ստացման լռելյայն առավելագույն չափը 4 MB է։
# Lint, check for breaking changes vs main, then generate code
buf lint
buf breaking --against ".git#branch=main"
buf generate
# Call a running server (needs reflection or the .proto)
grpcurl -plaintext -d '{"order_id":"42"}' localhost:50051 shop.orders.v1.OrderService/GetOrder
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
Օգտագործեք, երբ
- Ներքին կանչեր ծառայությունների միջև բազմալեզու միկրոծառայությունային համակարգում, որտեղ կարևոր է ընդհանուր, պարտադրվող պայմանագիրը։
- Բարձր throughput-ի կամ ցածր latency-ի ուղիներ, որտեղ JSON-ի վերլուծությունն ու payload-ի չափը չափելի ծախսեր են։
- Real-time հոսքեր բեքենդների միջև. telemetry, գնային հոսքեր, job-երի առաջընթաց, երկկողմանի կառավարման ալիքներ։
- Մոբայլ կամ IoT client-ներ սահմանափակ ցանցերում, որտեղ օգնում են կոմպակտ բինար payload-ները և գեներացված SDK-ները։
Մի՛ օգտագործեք, երբ
- Հանրային API-ներ երրորդ կողմերի համար, որոնք սպասում են curl-ի համար հարմար JSON, ընթեռնելի փաստաթղթեր և հեշտ debugging. REST-ն ավելի բարեկամական է։
- Պարզ վեբ հավելվածներ, որտեղ բրաուզերը հիմնական client-ն է, և մեկ բեքենդ է այն սպասարկում. proxy-ի և codegen-ի overhead-ը չարժե։
- Թիմեր առանց CI-ի և սխեմայի էվոլյուցիայի համար գործիքային կարգապահության. չկառավարվող .proto փոփոխությունները լուռ կոտրում են client-ները։
Այլընտրանքներ
- REST + OpenAPI
Ունիվերսալ, քեշավորվող, մարդու համար ընթեռնելի JSON. OpenAPI սպեցիֆիկացիաներից հնարավոր է client-ներ գեներացնել։
- GraphQL
Client-ի կողմից կառավարվող հարցումներ մեկ endpoint-ով. ավելի լավ է ֆրոնտենդի ճկուն տվյալների կարիքների համար, քան ծառայությունների միջև RPC-ի։
- Connect RPC
gRPC-համատեղելի պրոտոկոլ, որն աշխատում է նաև պարզ HTTP/JSON-ով և բրաուզերներում՝ առանց թարգմանող proxy-ի։
- Message queue-եր (Redis/BullMQ, Kafka, NATS)
Ասինխրոն իրադարձություններ, երբ կանչողին անմիջական պատասխան պետք չէ. ծառայությունները ժամանակի մեջ անջատում են իրարից։
Լավագույն պրակտիկաներ
- Երբեք մի փոխեք և կրկին մի օգտագործեք դաշտերի համարները. հեռացված դաշտերը նշեք որպես reserved, որպեսզի ոչ ոք պատահաբար դրանք չօգտագործի։
- Յուրաքանչյուր RPC-ի տվեք իր սեփական Request և Response message-ը, նույնիսկ եթե դրանք այսօր փոքրիկ են։
- Client-ում միշտ սահմանեք deadline-ներ և մնացած deadline-ը փոխանցեք downstream կանչերին։
- CI-ում գործարկեք buf lint և buf breaking, որպեսզի անհամատեղելի սխեմայի փոփոխությունները ձախողեն build-ը։
- Վերադարձրեք ճշգրիտ gRPC status code-եր՝ օգտակար մանրամասներով. դաշտերի սխալների համար օգտագործեք rich error model-ը (google.rpc.Status)։
- localhost-ից դուրս ամենուր օգտագործեք TLS կամ mTLS, իսկ auth token-ները ուղարկեք metadata-ում, ոչ թե message-ներում։
- Տրամադրեք ստանդարտ health ծառայությունը և ավելացրեք OpenTelemetry interceptor-ներ tracing-ի և մետրիկաների համար։
Տարածված սխալներ
- gRPC-ի առջև պարզ layer-4 (TCP) load balancer դնելը. երկարատև HTTP/2 կապերն ամբողջ տրաֆիկը կապում են մեկ բեքենդին։
- Deadline-ների բացակայություն, ինչի պատճառով մեկ դանդաղ կախվածությունը զբաղեցնում է thread-երն ու կապերը ամբողջ կանչերի շղթայում։
- Ոչ idempotent մեթոդները, ինչպես CreatePayment-ը, UNAVAILABLE-ի դեպքում կրկնելը՝ առանց idempotency key-ի։
- Դաշտերը վերանվանելը կամ վերահամարակալելը, կամ տիպերը փոխելը, ինչը լուռ փչացնում է տվյալները հին client-ների համար։
- Native gRPC-ն ուղղակիորեն բրաուզերներին տրամադրելը և հետո բացահայտելը, որ այն չի աշխատում առանց gRPC-Web-ի կամ Connect-ի։
Խորը վերլուծություն
08Protobuf-ի wire ֆորմատը և ինչու է այն փոքր
Յուրաքանչյուր դաշտ կոդավորվում է որպես tag (դաշտի համարը՝ երեք բիթ ձախ տեղաշարժված, գումարած wire type), որին հաջորդում է արժեքը։ Ամբողջ թվերն օգտագործում են varint. փոքր թվերը զբաղեցնում են մեկ բայթ, մեծերը՝ ավելի շատ։ Ահա թե ինչու են 1-ից 15 դաշտերի համարները թանկարժեք. դրանց tag-ը տեղավորվում է մեկ բայթում, ուստի պահեք դրանք հաճախակի դաշտերի համար։ Բացասական թվերը int32/int64-ում միշտ զբաղեցնում են տասը բայթ. հաճախ բացասական արժեքների համար օգտագործեք sint32/sint64 (zigzag կոդավորում)։ String-երը, bytes-ը և ներդրված message-ները length-delimited են։ Լռելյայն արժեքները (0, դատարկ string, false) proto3-ում ընդհանրապես չեն գրվում, եթե դաշտը նշված չէ optional, ահա թե ինչու առանց optional-ի կամ wrapper տիպերի չեք կարող տարբերել «չսահմանված»-ը «զրո»-ից։ Repeated scalar-ները proto3-ում լռելյայն packed են։
message Reading {
int32 sensor_id = 1; // tag 0x08, varint
sint32 delta = 2; // zigzag: -1 encodes as 1 byte
optional double value = 3; // presence tracked: unset != 0.0
repeated int32 samples = 4; // packed into one length-delimited block
reserved 5, 6;
reserved "legacy_name";
}
09Load balancing, name resolution և xDS
gRPC channel-ը մեկ կապ չէ. այն կառավարիչ է, որը թիրախային անունը (dns:///orders.internal:50051) լուծում է հասցեների, բացում է subchannel-ներ դրանցից յուրաքանչյուրի հետ և ըստ load-balancing policy-ի յուրաքանչյուր կանչի համար ընտրում մեկը։ Լռելյայն pick_first-ը ամեն ինչ ուղարկում է առաջին առողջ հասցեին. round_robin-ը կանչերը բաշխում է բոլորի միջև։ Service config-ը, որը տրվում է DNS-ով կամ սահմանվում կոդում, կարգավորում է policy-ները, կրկնակի փորձերը և ըստ մեթոդի timeout-ները։ Ավելի մեծ մասշտաբով gRPC client-ները կարող են խոսել xDS-ով՝ Envoy-ի control-plane պրոտոկոլով, և endpoint-ները, կշիռներն ու routing կանոնները ստանալ անմիջապես control plane-ից՝ ստանալով proxyless service mesh-ի վարքագիծ։ Սա խուսափում է sidecar-ի լրացուցիչ hop-ից, բայց client-ներին կապում է mesh-ի control plane-ին։
// Node.js: client-side round robin + retry policy via service config
const serviceConfig = JSON.stringify({
loadBalancingConfig: [{ round_robin: {} }],
methodConfig: [{
name: [{ service: "shop.orders.v1.OrderService", method: "GetOrder" }],
timeout: "2s",
retryPolicy: { maxAttempts: 3, initialBackoff: "0.1s", maxBackoff: "1s",
backoffMultiplier: 2, retryableStatusCodes: ["UNAVAILABLE"] },
}],
});
const client = new Svc("dns:///orders.internal:50051", creds,
{ "grpc.service_config": serviceConfig });
10Streaming մասշտաբով. flow control և backpressure
HTTP/2 flow control-ը յուրաքանչյուր stream-ի և կապի համար տալիս է բայթերի պատուհան, որը ստացողը պատրաստ է ընդունել։ Եթե client-ը server stream-ը դանդաղ է կարդում, պատուհանը լցվում է, և սերվերի գրառումները արգելափակվում կամ բուֆերացվում են. սա backpressure-ն է, որն աշխատում է ըստ նախագծի։ Խնդիրներ առաջանում են, երբ սերվերի կոդը անտեսում է դա և անսահմանափակ հաղորդագրություններ է բուֆերացնում հիշողությունում։ Node.js-ում հարգեք call.write()-ի վերադարձվող արժեքը և սպասեք 'drain'-ին։ Երկարատև stream-երին պետք են նաև keepalive ping-եր, որպեսզի NAT gateway-ները և proxy-ները լուռ չկտրեն անգործուն կապերը, բայց չափազանց ագրեսիվ keepalive-ների դեպքում client-ը մերժվում է GOAWAY 'too_many_pings'-ով։ Stream-երը նախագծեք վերսկսելի. ներառեք cursor կամ հաջորդականության համար, որպեսզի վերամիացող client-ը կարողանա շարունակել այնտեղից, որտեղ կանգնել էր։
11Անվտանգություն. mTLS, auth և interceptor-ներ
Ծառայությունների միջև mutual TLS-ը տալիս է և՛ գաղտնագրում, և՛ ինքնություն. յուրաքանչյուր workload ունի սերտիֆիկատ, որը հաճախ ավտոմատ թողարկվում է mesh-ի կամ SPIFFE/SPIRE-ի կողմից, և սերվերները կանչողներին թույլտվություն են տալիս ըստ նրանց սերտիֆիկատի ինքնության։ Օգտատիրոջ ինքնությունը փոխանցվում է առանձին՝ որպես bearer token metadata-ում, և վալիդացվում server interceptor-ում, որը gRPC-ի middleware-ն է։ Interceptor-ներում եք ավելացնում նաև լոգավորումը, մետրիկաները, tracing-ը, rate limiting-ը և panic recovery-ն՝ միատեսակ բոլոր մեթոդների համար։ Production-ում անջատեք կամ պաշտպանեք server reflection-ը, սահմանափակեք հաղորդագրությունների առավելագույն չափը՝ հիշողության չարաշահումը կանխելու համար, և մուտքը վալիդացրեք բացահայտ. protovalidate-ը թույլ է տալիս .proto-ում հայտարարել կանոններ, ինչպես string-ի երկարությունը կամ email ֆորմատը, և դրանք պարտադրել գեներացված կոդում։
Իրական իրավիճակներ
Մեկ բեքենդ pod-ը 100% CPU-ի վրա է, մինչ մյուսները պարապ են
Խնդիր
gRPC ծառայությունն աշխատում է հինգ replica-ով Kubernetes-ում՝ ClusterIP Service-ի հետևում։ Մետրիկաները ցույց են տալիս, որ մեկ pod-ը մշակում է API gateway-ից եկող գրեթե ամբողջ տրաֆիկը։
Ինչպես լուծել
ClusterIP Service-ը բալանսավորում է TCP կապերը, իսկ gateway-ը պահում է մեկ երկարատև HTTP/2 կապ, ուստի բոլոր կանչերը ընկնում են մեկ pod-ի վրա։ Լուծեք ըստ հարցման. կամ անցեք headless Service-ի և client-ը կարգավորեք dns:/// թիրախներով ու round_robin policy-ով, կամ ուղու վրա դրեք L7 proxy կամ service mesh (Envoy, Linkerd, Istio)։ Նաև սերվերում սահմանեք կապի առավելագույն տարիք (grpc.max_connection_age_ms), որպեսզի client-ները պարբերաբար վերամիանան և scale-up-ից հետո վերաբալանսավորվեն։ Փոփոխությունից հետո ստուգեք հարցումների հաճախականությունը ըստ pod-ի։
Դաշտի վերանվանումը կոտրեց մոբայլ հավելվածը
Խնդիր
Բեքենդ ծրագրավորողը .proto ֆայլում վերանվանեց և վերահամարակալեց դաշտը։ Մոբայլ հավելվածի հին տարբերակները, որոնք դեռ օգտագործվում են, հիմա ցույց են տալիս դատարկ գներ։
Ինչպես լուծել
Անմիջապես վերականգնեք դաշտի սկզբնական համարն ու տիպը. դա վերադարձնում է հին client-ներին, որովհետև wire ֆորմատը կրկին համընկնում է։ Եթե նոր անուն է պետք, վերանվանեք դաշտը, բայց պահեք համարը, կամ ավելացրեք նոր դաշտ նոր համարով և անցումային շրջանում լրացրեք երկուսն էլ։ Հեռացված ամեն ինչի համար ավելացրեք reserved գրառումներ։ Հետո կանխեք կրկնությունը. CI-ում գործարկեք buf breaking main branch-ի դեմ, որպեսզի wire-անհամատեղելի փոփոխությունները ձախողեն build-ը, և գիտակցված կոտրող փոփոխությունների համար պահանջեք տարբերակի բարձրացում (orders.v2), որը մատուցվում է զուգահեռ, մինչև հավելվածի հին տարբերակները դուրս գան շրջանառությունից։
Ներքին gRPC ծառայության տրամադրում վեբ dashboard-ին
Խնդիր
Թիմն ունի ներքին ծառայություններ gRPC-ով և ուզում է, որ React dashboard-ը դրանցից մի քանիսը կանչի ուղղակիորեն, ներառյալ live առաջընթացի stream-ը։
Ինչպես լուծել
Ձեռքով գրված REST շերտի փոխարեն օգտագործեք Connect. իրականացրեք կամ փաթեթավորեք ծառայությունը Connect-համատեղելի սերվերով, որպեսզի բրաուզերը այն կանչի գեներացված protobuf-es client-ով fetch-ի միջոցով, և server streaming-ը աշխատի առաջընթացի համար։ Եթե ծառայությունը պետք է մնա մաքուր gRPC, գործարկեք Envoy grpc_web ֆիլտրով և օգտագործեք gRPC-Web client։ Երկու դեպքում էլ authentication-ը դրեք edge-ում՝ կարճատև token-ներով metadata-ում, CORS-ը կարգավորեք միայն dashboard-ի origin-ի համար և տրամադրեք միայն ընթերցման և առաջընթացի մեթոդները, որոնք պետք են dashboard-ին, ոչ թե ամբողջ ներքին մակերեսը։
Արագ ուղեցույց
- Հեռացված դաշտը reserve անել
reserved 4; reserved "old_field";- Proto-ների lint
buf lint- Ձախողել CI-ը կոտրող փոփոխությունների դեպքում
buf breaking --against ".git#branch=main"- Կոդ գեներացնել buf.gen.yaml-ից
buf generate- Սերվերի ծառայությունների ցուցակը
grpcurl -plaintext localhost:50051 list- Unary մեթոդի կանչ
grpcurl -plaintext -d '{"order_id":"42"}' localhost:50051 pkg.Svc/Method- Health ստուգում
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check- Client-ի deadline (Node.js)
client.getOrder(req, { deadline: Date.now() + 2000 }, cb)- Debug լոգեր (grpc-js)
GRPC_TRACE=all GRPC_VERBOSITY=DEBUG node server.js
Հարցեր և պատասխաններ
Q1gRPC ընդդեմ REST-ի. որո՞նք են իրական տարբերությունները։
REST-ը ռեսուրսակենտրոն է, սովորաբար JSON HTTP/1.1-ի կամ HTTP/2-ի վրա, մարդու համար ընթեռնելի և աջակցվում է ամեն գործիքի, բրաուզերի և քեշի կողմից։ gRPC-ն contract-first RPC է. .proto ֆայլը գեներացնում է տիպավորված client-ներ և server-ներ, payload-ները բինար Protobuf են, այն պահանջում է HTTP/2 և native կերպով աջակցում է streaming-ի չորս ռեժիմ։ gRPC-ն հաղթում է լեզուների միջև type safety-ով, payload-ի չափով և ծառայությունների միջև streaming-ով։ REST-ը հաղթում է հասանելիությամբ, debug անելու հեշտությամբ, HTTP քեշավորմամբ և հանրային developer experience-ով։ Տարածված ճարտարապետությունն է REST կամ GraphQL edge-ում՝ բրաուզերների և գործընկերների համար, և gRPC՝ ներքին ծառայությունների միջև։
Q2Ինչպե՞ս է աշխատում հետադարձ համատեղելիությունը Protobuf-ում։
Ցանցով դաշտերը նույնականացվում են համարով և տիպով, ոչ թե անունով։ Decoder-ը բաց է թողնում անհայտ դաշտերի համարները, իսկ բացակայող դաշտերը ստանում են լռելյայն արժեքներ։ Դա նշանակում է, որ կարող եք անվտանգ ավելացնել նոր դաշտեր նոր համարներով, և հին client-ները դրանք կանտեսեն։ Կարող եք վերանվանել դաշտը՝ առանց բինար ֆորմատը կոտրելու, թեև դա կոտրում է գեներացված կոդը և JSON mapping-ը։ Համատեղելիությունը կոտրում է համարի կրկնակի օգտագործումը, դաշտի տիպի անհամատեղելի փոփոխությունը կամ որոշ դեպքերում դաշտը singular-ից repeated դարձնելը։ Դաշտը հեռացնելիս դրա համարն ու անունն ավելացրեք reserved հայտարարության մեջ։ Buf-ի breaking-change ստուգումը CI-ում պարտադրում է այս կանոնները։
Q3Ի՞նչ են deadline-ները և ինչո՞ւ են կարևոր։
Deadline-ը բացարձակ ժամանակն է, մինչև որը client-ին պատասխան է պետք։ Այն ուղարկվում է grpc-timeout header-ով, և երբ անցնում է, client-ը ստանում է DEADLINE_EXCEEDED, իսկ սերվերը կանչը տեսնում է որպես չեղարկված։ Deadline-ները կարևոր են, որովհետև ծառայությունների շղթայում մեկ դանդաղ կախվածությունը այլապես կարող է ռեսուրսներ պահել բոլոր upstream ծառայություններում և առաջացնել կասկադային ձախողումներ։ Լավ պրակտիկա է deadline սահմանել յուրաքանչյուր կանչի համար, downstream կանչերին փոխանցել մնացած բյուջեն՝ նոր timeout սկսելու փոխարեն, և երկար server handler-ներում ստուգել չեղարկումը, որպեսզի դրանք դադարեցնեն աշխատանքը, որին ոչ ոք չի սպասում։
Q4Կարելի՞ է gRPC-ն օգտագործել բրաուզերից։
Ոչ native կերպով։ Բրաուզերները չեն տրամադրում HTTP/2 framing-ը և trailer-ները, որոնց վրա հենվում է gRPC-ն։ Տարբերակներն են gRPC-Web-ը, որն օգտագործում է փոփոխված պրոտոկոլ և պահանջում է proxy, օրինակ՝ Envoy, ստանդարտ gRPC-ի թարգմանելու համար, և աջակցում է միայն unary և server streaming, կամ Connect-ը, որտեղ սերվերն ինքը խոսում է gRPC, gRPC-Web և Connect պրոտոկոլներով, ուստի բրաուզերը կանչում է այն fetch-ով, իսկ նույն handler-ները սպասարկում են բեքենդի gRPC client-ներին։ Այլընտրանքորեն՝ պահեք REST կամ GraphQL API բրաուզերի համար և gateway-ում թարգմանեք gRPC-ի, ինչը հաճախ ամենապարզն է հանրային պրոդուկտների համար։
Q5Ինչպե՞ս եք կատարում gRPC-ի load balancing։
gRPC-ն պահում է երկարատև HTTP/2 կապեր և մեկի վրա մուլտիպլեքսավորում է բազմաթիվ կանչեր։ Layer-4 load balancer-ը բալանսավորում է կապերը, ոչ թե հարցումները, ուստի մեկ client-ը կարող է իր ամբողջ տրաֆիկը ուղարկել մեկ բեքենդի, մինչ մյուսները պարապ են։ Լուծումներն են layer-7 proxy-ները, որոնք հասկանում են HTTP/2 և բալանսավորում են ըստ հարցման, օրինակ՝ Envoy, NGINX grpc_pass-ով կամ service mesh, ինչպես Istio-ն կամ Linkerd-ը, կամ client-side load balancing, որտեղ client-ը DNS-ի կամ xDS-ի միջոցով ստանում է բեքենդի բոլոր հասցեները և օգտագործում round_robin policy։ Kubernetes-ում սովորական պատասխանն է headless Service գումարած client-side balancing կամ mesh։
Q6Ինչպե՞ս եք մշակում սխալները gRPC-ում։
Յուրաքանչյուր կանչ ավարտվում է ֆիքսված հավաքածուից status code-ով և ընտրովի հաղորդագրությամբ։ Դոմենային սխալները համապատասխանեցնում եմ գիտակցաբար. վալիդացիայի ձախողումները՝ INVALID_ARGUMENT, բացակայող գրառումները՝ NOT_FOUND, auth-ը՝ UNAUTHENTICATED կամ PERMISSION_DENIED, rate limit-երը՝ RESOURCE_EXHAUSTED, իսկ ժամանակավոր ձախողումները՝ UNAVAILABLE, որպեսզի client-ները իմանան, որ կարող են կրկնել։ Կառուցվածքային մանրամասների համար richer error model-ը կցում է google.rpc.Status մանրամասներ, օրինակ՝ BadRequest դաշտերի խախտումներով կամ RetryInfo։ Երբեք stack trace-եր չեմ բացահայտում հաղորդագրություններում, իսկ interceptor-ները լոգավորում են սխալները trace ID-ներով, որպեսզի client-ի սխալը հնարավոր լինի հետևել ծառայությունների միջով։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս կնախագծեիք API-ի տարբերակավորումն ու governance-ը gRPC օգտագործող 50 ծառայությունների համար։
Բոլոր .proto ֆայլերը կդնեի schema registry-ում՝ կամ առանձին ռեպոզիտորիայում, կամ Buf Schema Registry-ում, package-ներով, որոնց անվան մեջ տարբերակ կա (billing.v1)։ CI-ը գործարկում է buf lint՝ միատեսակ ոճի համար, և buf breaking վերջին թողարկման դեմ, որպեսզի wire-անհամատեղելի փոփոխությունները չկարողանան merge լինել։ Ավելացնող փոփոխությունները մնում են v1-ում. իսկական կոտրող փոփոխությունները ստեղծում են v2, որը մատուցվում է զուգահեռ՝ deprecation պատուհանով և մետրիկաներով, որոնք ցույց են տալիս, թե որ client-ներն են դեռ կանչում v1։ Գեներացված SDK-ները հրապարակվում են որպես տարբերակավորված փաթեթներ, որպեսզի սպառողները թարմացնեն գիտակցաբար։ Սեփականությունը յուրաքանչյուր package-ի համար բացահայտ է CODEOWNERS-ի միջոցով։ Ընդհանուր տիպերը, ինչպես Money-ն կամ Address-ը, ապրում են փոքր common package-ում և պահվում են կայուն, որովհետև այնտեղ ամեն փոփոխություն տարածվում է ամենուր։
S2Sidecar service mesh, թե՞ proxyless gRPC. որո՞նք են փոխզիջումները։
Sidecar mesh-ը (Istio, Linkerd) mTLS, կրկնակի փորձեր, տրաֆիկի բաժանում և telemetry է տալիս յուրաքանչյուր ծառայությանը՝ անկախ լեզվից և առանց կոդի փոփոխության։ Գինը յուրաքանչյուր կանչի համար լրացուցիչ ցանցային hop-ն է, sidecar-ի CPU-ն ու հիշողությունը յուրաքանչյուր pod-ում և շահագործման բարդությունը։ Proxyless gRPC-ն xDS-ն օգտագործում է gRPC գրադարանի ներսում, ուստի client-ները endpoint-ները, load balancing-ը և routing-ը ստանում են անմիջապես control plane-ից. ավելի ցածր latency և ռեսուրսների օգտագործում, բայց միայն gRPC տրաֆիկի համար, միայն հասուն xDS աջակցությամբ լեզուներում, և mesh-ի հնարավորությունները կապված են գրադարանի տարբերակներին։ Ambient կամ node-level mesh-երը միջին տարբերակ են։ Ես կսկսեի sidecar կամ ambient mesh-ով՝ միատեսակության համար, և proxyless-ը կօգտագործեի միայն latency-ի համար կրիտիկական թեժ ուղիներում։
S3Ինչպե՞ս եք կրկնակի փորձերն անվտանգ դարձնում gRPC ծառայությունների շղթայում։
Առաջինը՝ կրկնակի փորձերը սահմանափակեք այն code-երով, որոնք նշանակում են, որ հարցումը հավանաբար չի մշակվել կամ կարող է կրկնվել, հիմնականում UNAVAILABLE, և միայն idempotent մեթոդների համար։ Փոփոխող մեթոդների համար պահանջեք idempotency key հարցման մեջ և սերվերում պահեք արդյունքներն ըստ դրա, որպեսզի կրկնված CreatePayment-ը վերադարձնի սկզբնական արդյունքը։ Երկրորդը՝ խուսափեք retry amplification-ից. եթե երեք շերտերից յուրաքանչյուրը երեք անգամ է կրկնում, մեկ օգտատիրոջ հարցումը կարող է դառնալ 27 բեքենդ կանչ։ Կրկնեք մեկ շերտում, իդեալում՝ edge-ում կամ ձախողմանն ամենամոտ client-ում, օգտագործեք exponential backoff jitter-ով և gRPC-ի retry throttling-ը, որն անջատում է կրկնակի փորձերը, երբ ձախողումների հարաբերակցությունը բարձր է։ Երրորդը՝ փոխանցեք deadline-ները, որպեսզի կրկնակի փորձերը երբեք չգերազանցեն սկզբնական բյուջեն։
Բառարան
- RPC
- Remote procedure call. այլ մեքենայի վրա աշխատող ֆունկցիայի կանչ, կարծես այն լոկալ լիներ։
- Protocol Buffers
- Google-ի սխեմայի լեզուն և կոմպակտ բինար կոդավորումը, որոնցով gRPC-ն սահմանում և ուղարկում է հաղորդագրությունները։
- Stub
- Գեներացված client օբյեկտ, որի մեթոդները սերիալիզացնում են հարցումները, ուղարկում ցանցով և վերադարձնում պատասխանները։
- Deadline
- Բացարձակ ժամանակը, մինչև որը կանչը պետք է ավարտվի. դրանից հետո կանչը չեղարկվում է DEADLINE_EXCEEDED-ով։
- Metadata
- Կանչի հետ ուղարկվող key-value զույգեր՝ HTTP header-ների նման, որոնք օգտագործվում են auth token-ների և trace ID-ների համար։
- Interceptor
- Middleware, որը client-ում կամ server-ում փաթաթում է յուրաքանչյուր կանչ՝ լոգավորման, auth-ի, մետրիկաների կամ կրկնակի փորձերի համար։
- gRPC-Web
- gRPC-ի բրաուզերի հետ համատեղելի տարբերակ, որին պետք է proxy՝ ստանդարտ gRPC-ի թարգմանելու համար։
- Field number
- Protobuf դաշտի թվային ID-ն. այն նույնականացնում է դաշտը ցանցով փոխանցելիս և երբեք չպետք է փոխվի։
- xDS
- Envoy-ի control-plane API-ները. gRPC client-ները կարող են դրանցով ստանալ endpoint-ներ և routing՝ առանց sidecar-ի։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։