Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

examples: add example to show how to use the health service #3381

Merged
merged 8 commits into from
Apr 8, 2020
64 changes: 64 additions & 0 deletions examples/features/health/README.md
Original file line number Diff line number Diff line change
@@ -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)`.
85 changes: 85 additions & 0 deletions examples/features/health/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
*
* Copyright 2018 gRPC authors.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update to 2020

*
* 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(fmt.Sprintf("UnaryEcho: _, %v", err))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified a bit:

fmt.Printf("UnaryEcho: _, %v\n", err)
or
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)
}
}
87 changes: 87 additions & 0 deletions examples/features/health/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
*
* Copyright 2018 gRPC authors.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update date.

*
* 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)
}
}