Skip to content

Latest commit

 

History

History
926 lines (712 loc) · 34.2 KB

File metadata and controls

926 lines (712 loc) · 34.2 KB

Developing fault-tolerant microservices with Istio Retry and MicroProfile Fallback

Note
This repository contains the guide documentation source. To view the guide in published form, view it on the Open Liberty website.

Explore how to manage the impact of failures by using MicroProfile and Istio Fault Tolerance to add retry and fallback behaviours to microservices.

What you’ll learn

You will learn how to combine MicroProfile Retry and Fallback policies with Istio Retry to make your microservices more resilient to common failures, such as network problems.

Microservices that are created using Eclipse MicroProfile can be freely deployed in a service mesh to reduce the complexity associated with managing microservices. Istio is a service mesh, meaning that it’s a platform for managing how microservices interact with each other and the outside world. Istio consists of a control plane and sidecars that are injected into application pods. The sidecars contain the Envoy proxy. You can think of Envoy as a sidecar that intercepts and controls all the HTTP and TCP traffic to and from your container. If you would like to learn more about Istio, check out the Managing microservice traffic using Istio guide.

MicroProfile and Istio both provide simple and flexible solutions to build fault-tolerant microservices. Fault tolerance provides different strategies for building robust behaviour to cope with unexpected failures. A few fault tolerance policies that MicroProfile can offer include Retry, Timeout, Circuit Breaker, Bulkhead, and Fallback. There is some overlap that exists between MicroProfile and Istio Fault Tolerance, such as the Retry policy. However, Istio does not offer any fallback capabilities. To view the available fault tolerance policies in MicroProfile and Istio, refer to the comparison between MicroProfile and Istio fault handling.

Use retry policies to fail quickly and recover from brief intermittent issues. An application might experience these transient failures when a microservice is undeployed, a database is overloaded by queries, the network connection becomes unstable, or the site host has a brief downtime. In these cases, rather than failing quickly on these transient failures, a retry policy provides another chance for the request to succeed. Simply retrying the request might be all you need to do to make it succeed.

Fallback offers an alternative execution path when an execution does not complete successfully. You will use the @Fallback annotation from the MicroProfile Fault Tolerance specification to define criteria for when to provide an alternative solution for a failed execution.

You will develop microservices that demonstrate MicroProfile Fault Tolerance with Istio fault handling. Both MicroProfile and Istio can be used when you want your microservices to have a service mesh architecture with Istio, and use MicroProfile to provide the extra fault tolerance policies that do not exist within Istio.

The application that you will be working with is an inventory service, which collects, stores, and returns the system properties. It uses the system service to retrieve the system properties for a particular host. You will add fault tolerance to the inventory service so that it reacts accordingly when the system service is unavailable.

Enabling MicroProfile Fault Tolerance

Navigate to the guide-microprofile-istio-retry-fallback/start directory to begin.

The MicroProfile Fault Tolerance API is included in the MicroProfile dependency that is specified in your pom.xml file. Look for the dependency with the microprofile artifact ID. This dependency provides a library that allows you to use the fault tolerance policies in your microservices.

The InventoryResource.java file makes a request to the system service through the MicroProfile Rest Client API. If you want to learn more about MicroProfile Rest Client, you can follow the Consuming RESTful services with template interfaces guide.

pom.xml

link:finish/inventory/pom.xml[role=include]

InventoryResource.java

link:finish/inventory/src/main/java/io/openliberty/guides/inventory/InventoryResource.java[role=include]

Adding the MicroProfile @Retry annotation

To simulate that your system service is temporarily down due to brief intermittent issues, you will pause the pod that is associated with your system service, then try to send requests to the service. When the system pod is paused, requests to the service return a 503 status code, and the systemClient.getProperties() in InventoryResource.java throws a WebApplicationException.

To retry the requests to your system service after a WebApplicationException has occurred, add the @Retry annotation.

Update the InventoryResource.java file.
inventory/src/main/java/io/openliberty/guides/inventory/InventoryResource.java

To retry the service request a maximum of 3 times, only when a WebApplicationException occurs, add the @Retry annotation before the getPropertiesForHost() method.

InventoryResource.java

link:finish/inventory/src/main/java/io/openliberty/guides/inventory/InventoryResource.java[role=include]

A request to a service might fail for many different reasons. The default Retry policy initiates a retry for every java.lang.Exception. However, you can base a Retry policy on a specific exception by using the retryOn parameter. You can identify more than one exception as an array of values. For example, @Retry(retryOn = {RuntimeException.class, TimeoutException.class}).

You can set limits on the number of retry attempts to avoid overloading a busy service with retry requests. The @Retry annotation has the maxRetries parameter to limit the number of retry attempts. The default number for maxRetries is 3 requests. The integer value must be greater than or equal to -1. A value of -1 indicates to continue retrying indefinitely.

Building and running the application

Navigate to the guide-microprofile-istio-retry-fallback/start directory and run the following command to build your application and integrate the Retry policy into your microservices:

mvn package

Next, run the docker build commands to build container images for your application:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.

The -t flag in the docker build command allows the Docker image to be labeled (tagged) in the name[:tag] format. The tag for an image describes the specific image version. If the optional [:tag] tag is not specified, the latest tag is created by default.

To verify that the images for your system and inventory microservices are built, run the docker images command to list all local Docker images.

docker images

Your two images system and inventory should appear in the list of all Docker images:

REPOSITORY         TAG
inventory          1.0-SNAPSHOT
system             1.0-SNAPSHOT

To deploy your microservices to the Kubernetes cluster, use the following command:

kubectl apply -f services.yaml

You will see an output similar to the following:

gateway.networking.istio.io/sys-app-gateway created
service/system-service created
service/inventory-service created
deployment.apps/system-deployment created
deployment.apps/inventory-deployment created

The traffic.yaml file contains two virtual services. A virtual service defines how requests are routed to your applications.

Deploy the resources defined in the traffic.yaml file:

kubectl apply -f traffic.yaml

Run the following command to check the status of your pods:

kubectl get pods

If all the pods are healthy and running, you will see an output similar to the following:

NAME                                    READY     STATUS    RESTARTS   AGE
inventory-deployment-645767664f-nbtd9   2/2       Running   0          30s
system-deployment-6bd97d9bf6-4ccds      2/2       Running   0          30s

Check that all of the deployments are available. You need to wait until all of your deployments are ready and available before making requests to your microservices.

kubectl get deployments
NAME                     READY     UP-TO-DATE   AVAILABLE   AGE
inventory-deployment     1/1       1            1           1m
system-deployment        1/1       1            1           1m

You will make a request to the system service from the inventory service to access the JVM system properties of your running container. The Istio gateway is the entry point for HTTP requests to the cluster. As defined in the services.yaml file, the gateway is expecting the Host header of your system service and inventory service to be system.example.com and inventory.example.com, respectively. However, requests to system.example.com and inventory.example.com won’t be routed to the appropriate IP address. To ensure that the gateway routes your requests appropriately, ensure that the Host header is set appropriately. You can set the Host header with the -H option of the curl command.

services.yaml

link:finish/services.yaml[role=include]

traffic.yaml

link:finish/traffic.yaml[role=include]

Make a request to the service by using curl:

curl -H Host:inventory.example.com http://localhost/inventory/systems/system-service -I

If the curl command is unavailable, then use Postman. Postman enables you to make requests using a graphical interface. To make a request with Postman, enter http://localhost/inventory/systems/system-service into the URL bar. Next, switch to the Headers tab and add a header with key of Host and value of inventory.example.com. Finally, click the blue Send button to make the request.

export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
curl -H Host:inventory.example.com http://`minikube ip`:$INGRESS_PORT/inventory/systems/system-service -I

You will see the following output:

HTTP/1.1 200 OK
x-powered-by: Servlet/4.0
content-type: application/json
date: Mon, 19 Aug 2019 19:49:47 GMT
content-language: en-US
x-envoy-upstream-service-time: 4242
server: istio-envoy
transfer-encoding: chunked

Because the system service is available, the request to the service is successful and returns a 200 response code.

To see the number of times that the system service was called, check the logs of the system pod by using the kubectl logs command. Replace [system-pod-name] with the pod name associated with your system service, which you previously saw when running the kubectl get pods command.

kubectl logs [system-pod-name] -c istio-proxy | grep -c system-service:9090
kubectl logs [system-pod-name] -c istio-proxy | find /C "system-service:9090"

You will see that the kubectl logs command returns a value of 1, meaning that 1 request is made to the system service:

1

Now you will make the system service unavailable and observe that MicroProfile’s Retry policy will take effect.

Pause the system service pod to simulate that the service is unavailable. Remember to replace [system-pod-name] with the pod name that is associated with your system service.

kubectl exec -it [system-pod-name] -- /opt/ol/wlp/bin/server pause

You will see the following output:

Pausing the defaultServer server.
Pausing the defaultServer server completed.

Make a request to the service by using curl:

curl -H Host:inventory.example.com http://localhost/inventory/systems/system-service -I

If the curl command is unavailable, then use Postman.

curl -H Host:inventory.example.com http://`minikube ip`:$INGRESS_PORT/inventory/systems/system-service -I

You will see the following output:

HTTP/1.1 503 Service Unavailable
x-powered-by: Servlet/4.0
content-length: 91
content-type: text/plain
date: Thu, 15 Aug 2019 13:21:57 GMT
server: istio-envoy
x-envoy-upstream-service-time: 2929
content-language: en-US

Because the the system service is unavailable, the request returns a 503 response code. However, the request retried several times, as specified by the MicroProfile @Retry annotation.

See the number of times that the service was retried:

kubectl logs [system-pod-name] -c istio-proxy | grep -c system-service:9090
kubectl logs [system-pod-name] -c istio-proxy | find /C "system-service:9090"

You will see the following output:

37

The above command returns 37, because there was a total of 37 requests made to the system service. By default, Istio will retry 2 times to resolve any issues with a 503 response code. Including the initial requests and the retries, the requests made by Istio are multiplied by the requests made by MicroProfile. Hence, the 3 requests from the system service, the 3 requests from the inventory service, and the 4 MicroProfile requests are multiplied together, giving a total of 36 requests. Including the succesful request that you made before you paused the system service, there was a total of 37 requests.

Enabling Istio Fault Tolerance

Previously, you implemented the Retry policy to retry requests to your system service by using MicroProfile Fault Tolerance. This Retry policy can also be implemented with Istio Fault Tolerance.

Update the traffic.yaml file.
traffic.yaml

traffic.yaml

link:finish/traffic.yaml[role=include]

Add the retries field under the route specification in the traffic.yaml file. This tells Istio to retry requests a maximum of 4 times when the request returns any 5xx response code.

The attempts field is required in the configuration of the Istio Retry policy. This field specifies the maximum number of retries that will be attempted for a given request. To retry a request on specific conditions, use the retryOn field. Because your paused system service responds with a 503 response code, you set retryOn to be 5xx. Other retry conditions can also be specified in retryOn. Optionally, the perTryTimeout field can be added to Istio’s Retry policy to specify the amount of time that is allocated to each retry attempt.

After you configure the number of retries that Istio performs, deploy your microservices again:

kubectl replace --force -f services.yaml
kubectl replace --force -f traffic.yaml

Wait until all of your deployments are ready and available. The [system-pod-name] will be regenerated and is different than the one you used previously. Run the kubectl get pods command to get the new [system-pod-name]. Pause the system service pod to simulate that the service is unavailable.

kubectl exec -it [system-pod-name] -- /opt/ol/wlp/bin/server pause

Make a request to the service by using curl:

curl -H Host:inventory.example.com http://localhost/inventory/systems/system-service -I

If the curl command is unavailable, then use Postman.

curl -H Host:inventory.example.com http://`minikube ip`:$INGRESS_PORT/inventory/systems/system-service -I

Because the system service is unavailable, the request still returns a 503 response code. This time, however, Istio retried the request several more times before failing.

See the number of times that the service was retried:

kubectl logs [system-pod-name] -c istio-proxy | grep -c system-service:9090
kubectl logs [system-pod-name] -c istio-proxy | find /C "system-service:9090"

You will see the following output:

60

The above command returns a value of 60, indicating that a total of 60 requests are made to the system service. The 3 default Istio requests for the system service, the 5 requests for the inventory service that you enabled in the traffic.yaml file, and the 4 requests sent by MicroProfile are multiplied together.

Next, you will disable some MicroProfile Fault Tolerance capabilities, so that your system service retries with only Istio’s Retry policy.

Turning off MicroProfile Fault Tolerance

When both MicroProfile and Istio Fault Tolerance capabilities are enabled, there is a compounding effect that may be unexpected. If both MicroProfile and Istio set their own Retry policies on a service, the maximum number of retries that occur is not equivalent to either of the number of retries specified in MicroProfile or Istio. The number of retries set by MicroProfile and Istio are actually multiplied.

If you want to use Istio as your service mesh and only its fault tolerance capabilities, you can turn off MicroProfile Fault Tolerance by adding a property. This configuration avoids any overlap in behaviours.

MicroProfile Fault Tolerance offers a config property MP_Fault_Tolerance_NonFallback_Enabled that disables all MicroProfile Fault Tolerance capabilities except fallback. If MP_Fault_Tolerance_NonFallback_Enabled is set to false, only the @Fallback behaviour is enabled. The other behaviours specified by the MicroProfile Fault Tolerance annotations, including @Retry, won’t take effect.

You will define the MP_Fault_Tolerance_NonFallback_Enabled config property in a ConfigMap. ConfigMaps store configuration settings about a Kubernetes pod. This configuration is loaded into the pod as an environment variable that is used by the pod’s containers. The environment variables are defined in the pod’s specification by using the envFrom field. To learn more about ConfigMaps, check out the Configuring microservices running in Kubernetes guide.

Use the MP_Fault_Tolerance_NonFallback_Enabled config property to disable the retries performed by MicroProfile, so that only Istio performs retries.

Update the services.yaml file.
services.yaml

services.yaml

link:finish/services.yaml[role=include]

Add a ConfigMap into the services.yaml file, and set the MP_Fault_Tolerance_NonFallback_Enabled config property to false. Add the envFrom field to inject the ConfigMap with the MP_Fault_Tolerance_NonFallback_Enabled property into your pods.

The name of the ConfigMap, which is inventory-config, becomes the environment variable name that is specified in the envFrom field.

Deploy your microservices again to turn off all MicroProfile Fault Tolerance capabilities, except fallback:

kubectl replace --force -f services.yaml

Wait until all of your deployments are ready and available. Run the kubectl get pods command to get the new [system-pod-name]. Pause the system service pod to simulate that the service is unavailable:

kubectl exec -it [system-pod-name] -- /opt/ol/wlp/bin/server pause

Make a request to the service by using curl:

curl -H Host:inventory.example.com http://localhost/inventory/systems/system-service -I

If the curl command is unavailable, then use Postman.

curl -H Host:inventory.example.com http://`minikube ip`:$INGRESS_PORT/inventory/systems/system-service -I

Because the system service is unavailable, the request still returns a 503 response code. This time, however, the request was retried several times with Istio, without any retries from MicroProfile.

See the number of times that the service was retried:

kubectl logs [system-pod-name] -c istio-proxy | grep -c system-service:9090
kubectl logs [system-pod-name] -c istio-proxy | find /C "system-service:9090"

You will see the following output:

15

The above command returns 15, indicating that a total of 15 requests are made to the system service. Because MicroProfile’s Retry policy is disabled, only Istio’s retries are performed.

traffic.yaml

link:finish/traffic.yaml[role=include]

Using MicroProfile Fallback

services.yaml

link:finish/services.yaml[role=include]

Since retrying the requests to the system service still does not succeed, you need a "fall back" plan. You will create a fallback method as an alternative solution for when retry requests to the system service have failed.

Although you disabled MicroProfile @Retry and other MicroProfile Fault Tolerance policies using the MP_Fault_Tolerance_NonFallback_Enabled config property, the fallback policy is still available. As mentioned before, Istio does not offer any fallback capabilities, so the MicroProfile Fallback capability can be used to complement it.

The @Fallback annotation dictates a method to call when the original method encounters a failed execution. If your microservices have a Retry policy specified, then the fallback occurs after all of the retries have failed.

Update the InventoryResource.java file.
inventory/src/main/java/io/openliberty/guides/inventory/InventoryResource.java

InventoryResource.java

link:finish/inventory/src/main/java/io/openliberty/guides/inventory/InventoryResource.java[role=include]

Create the getPropertiesFallback() method. Add the @Fallback annotation before the getPropertiesForHost() method, to call the getPropertiesFallback() method when a failure occurs.

The getPropertiesFallback() method, which is the designated fallback method for the original getPropertiesForHost() method, prints out a warning message in the browser that says the system service may not be running.

Rebuild your application to add fallback behaviour to your microservices:

mvn package

Next, run the docker build commands to rebuild the container images for your application:

docker build -t inventory:1.0-SNAPSHOT inventory/.
docker build -t system:1.0-SNAPSHOT system/.

Deploy your microservices again to turn off all MicroProfile Fault Tolerance capabilities, except fallback:

kubectl replace --force -f services.yaml
kubectl replace --force -f traffic.yaml

Wait until all of your deployments are ready and available. Run the kubectl get pods command to get the new [system-pod-name]. Pause the system service pod to simulate that the service is unavailable:

kubectl exec -it [system-pod-name] -- /opt/ol/wlp/bin/server pause

Make a request to the service by using curl:

curl -H Host:inventory.example.com http://localhost/inventory/systems/system-service -I

If the curl command is unavailable, then use Postman.

curl -H Host:inventory.example.com http://`minikube ip`:$INGRESS_PORT/inventory/systems/system-service -I

You will see the following output:

HTTP/1.1 200 OK
x-powered-by: Servlet/4.0
x-from-fallback: yes
content-type: application/json
date: Mon, 19 Aug 2019 19:49:47 GMT
content-language: en-US
x-envoy-upstream-service-time: 4242
server: istio-envoy
transfer-encoding: chunked

You can see that the request is now successful and returns a 200 response code, with a header called x-from-fallback, indicating that the fallback method is called when the system service is not available.

See the number of times that the service is retried before the fallback method is called:

kubectl logs [system-pod-name] -c istio-proxy | grep -c system-service:9090
kubectl logs [system-pod-name] -c istio-proxy | find /C "system-service:9090"

You will see the following output:

3

The above command returns 3, indicating that a total of 3 requests are made to the system service. The Istio retries that you enabled on the inventory service are not performed, because the Fallback policy is enabled. However, the 3 default requests by Istio on the server end are still performed. Because all of these requests failed, the getPropertiesFallback() fallback method is called.

Tearing down your environment

When you are done checking out the MicroProfile and Istio Fault Tolerance features, you might want to tear down all the deployed resources as a cleanup step.

Delete your resources from the cluster:

kubectl delete -f services.yaml
kubectl delete -f traffic.yaml

Delete the istio-injection label from the default namespace. The hyphen immediately after the label name indicates that the label should be deleted.

kubectl label namespace default istio-injection-

Navigate to the directory where you extracted Istio and delete the Istio resources from the cluster:

kubectl delete -f install/kubernetes/istio-demo.yaml

Delete all Istio resources from the cluster:

istioctl uninstall --purge
istioctl uninstall --purge
istioctl uninstall --purge

Perform the following steps to return your environment to a clean state.

  1. Point the Docker daemon back to your local machine:

    eval $(minikube docker-env -u)
  2. Stop and delete your Minikube cluster:

    minikube stop
    minikube delete

Great work! You’re done!

You learned how to build resilient microservices by using Istio Retry and MicroProfile Fallback. You also observed how MicroProfile Fault Tolerance integrates with and complements Istio Fault Tolerance.