diff --git a/examples/features/health/README.md b/examples/features/health/README.md new file mode 100644 index 000000000000..f4bb6a1516ba --- /dev/null +++ b/examples/features/health/README.md @@ -0,0 +1,64 @@ +# Health + +gRPC provides a health library to communicate a system's health to their clients. +It works by providing a service definition via the [health/v1](https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto) api. + +By using the health library, clients can gracefully avoid using servers as they encounter issues. +Most languages provide an implementation out of box, making it interoperable between systems. + +## Try it + +``` +go run server/main.go -port=50051 -sleep=5s +go run server/main.go -port=50052 -sleep=10s +``` + +``` +go run client/main.go +``` + +## Explanation + +### Client + +Clients have two ways to monitor a servers health. +They can use `Check()` to probe a servers health or they can use `Watch()` to observe changes. + +In most cases, clients do not need to directly check backend servers. +Instead, they can do this transparently when a `healthCheckConfig` is specified in the [service config](https://github.com/grpc/proposal/blob/master/A17-client-side-health-checking.md#service-config-changes). +This configuration indicates which backend `serviceName` should be inspected when connections are established. +An empty string (`""`) typically indicates the overall health of a server should be reported. + +```go +// import grpc/health to enable transparent client side checking +import _ "google.golang.org/grpc/health" + +// set up appropriate service config +serviceConfig := grpc.WithDefaultServiceConfig(`{ + "loadBalancingPolicy": "round_robin", + "healthCheckConfig": { + "serviceName": "" + } +}`) + +conn, err := grpc.Dial(..., serviceConfig) +``` + +See [A17 - Client-Side Health Checking](https://github.com/grpc/proposal/blob/master/A17-client-side-health-checking.md) for more details. + +### Server + +Servers control their serving status. +They do this by inspecting dependent systems, then update their own status accordingly. +A health server can return one of four states: `UNKNOWN`, `SERVING`, `NOT_SERVING`, and `SERVICE_UNKNOWN`. + +`UNKNOWN` indicates the current state is not yet known. +This state is often seen at the start up of a server instance. + +`SERVING` means that the system is healthy and ready to service requests. +Conversely, `NOT_SERVING` indicates the system is unable to service requests at the time. + +`SERVICE_UNKNOWN` communicates the `serviceName` requested by the client is not known by the server. +This status is only reported by the `Watch()` call. + +A server may toggle its health using `healthServer.SetServingStatus("serviceName", servingStatus)`. diff --git a/examples/features/health/client/main.go b/examples/features/health/client/main.go new file mode 100644 index 000000000000..db4145024fa2 --- /dev/null +++ b/examples/features/health/client/main.go @@ -0,0 +1,85 @@ +/* + * + * Copyright 2020 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "context" + "flag" + "fmt" + "log" + "time" + + "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/features/proto/echo" + _ "google.golang.org/grpc/health" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/resolver/manual" +) + +var serviceConfig = `{ + "loadBalancingPolicy": "round_robin", + "healthCheckConfig": { + "serviceName": "" + } +}` + +func callUnaryEcho(c pb.EchoClient) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.UnaryEcho(ctx, &pb.EchoRequest{}) + if err != nil { + fmt.Println("UnaryEcho: _, ", err) + } else { + fmt.Println("UnaryEcho: ", r.GetMessage()) + } +} + +func main() { + flag.Parse() + + r, cleanup := manual.GenerateAndRegisterManualResolver() + defer cleanup() + r.InitialState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: "localhost:50051"}, + {Addr: "localhost:50052"}, + }, + }) + + address := fmt.Sprintf("%s:///unused", r.Scheme()) + + options := []grpc.DialOption{ + grpc.WithInsecure(), + grpc.WithBlock(), + grpc.WithDefaultServiceConfig(serviceConfig), + } + + conn, err := grpc.Dial(address, options...) + if err != nil { + log.Fatalf("did not connect %v", err) + } + defer conn.Close() + + echoClient := pb.NewEchoClient(conn) + + for { + callUnaryEcho(echoClient) + time.Sleep(time.Second) + } +} diff --git a/examples/features/health/server/main.go b/examples/features/health/server/main.go new file mode 100644 index 000000000000..5a9245dfdbcd --- /dev/null +++ b/examples/features/health/server/main.go @@ -0,0 +1,87 @@ +/* + * + * Copyright 2020 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net" + "time" + + "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/features/proto/echo" + "google.golang.org/grpc/health" + healthpb "google.golang.org/grpc/health/grpc_health_v1" +) + +var ( + port = flag.Int("port", 50051, "the port to serve on") + sleep = flag.Duration("sleep", time.Second*5, "duration between changes in health") + + system = "" // empty string represents the health of the system +) + +type echoServer struct { + pb.UnimplementedEchoServer +} + +func (e *echoServer) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) { + return &pb.EchoResponse{ + Message: fmt.Sprintf("hello from localhost:%d", *port), + }, nil +} + +var _ pb.EchoServer = &echoServer{} + +func main() { + flag.Parse() + + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + s := grpc.NewServer() + healthcheck := health.NewServer() + healthpb.RegisterHealthServer(s, healthcheck) + pb.RegisterEchoServer(s, &echoServer{}) + + go func() { + // asynchronously inspect dependencies and toggle serving status as needed + next := healthpb.HealthCheckResponse_SERVING + + for { + healthcheck.SetServingStatus(system, next) + + if next == healthpb.HealthCheckResponse_SERVING { + next = healthpb.HealthCheckResponse_NOT_SERVING + } else { + next = healthpb.HealthCheckResponse_SERVING + } + + time.Sleep(*sleep) + } + }() + + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +}