Service Fault Tolerant Component of Spring Cloud Alibaba - Sentinel [Code]

Posted by megosh on Fri, 02 Aug 2019 10:14:15 +0200

Communication Principle between Sentinel and Control Station

stay Basic Chapter We learned how to integrate Sentinel for the project, and built a visual console for Sentinel. We introduced and demonstrated various rules configuration methods supported by Sentinel. Sentinel is further introduced in this paper.

First, let's see how the console can get the monitoring information of the micro-service.

Sentinel needs to add spring-cloud-starter-alibaba-sentinel dependency, which includes sentinel-transport-simple-http module. After integrating the module, the micro-service registers itself on the Sentinel console through the connection address configured in the configuration file, and informs the surviving status through the heartbeat mechanism. Thus, Sentinel implements a set of service discovery mechanism.

As follows:

Through this mechanism, the communication address and port number of Sentinel client (micro service) can be seen from the list of machines in Sentinel console:

In this way, the Sentinel console can communicate with the micro-service. When it needs to acquire the monitoring information of the micro-service, the Sentinel console will call the monitoring API exposed by the micro-service regularly, so that the monitoring information of the micro-service can be acquired in real time.

Another question is how does the console send rules to individual microservices when using the console to configure rules? Similarly, if you want to push the configuration rules to the micro service, you only need to call the API that receives the push rules on the micro service.

We can view the APIs exposed to Sentinel console calls by accessing http://{ip address of microservice registration}:8720/api interface, as follows:

Related source code:

  • Registration/heartbeat mechanism: com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeat Sender
  • Communication API: Implementation class of com.alibaba.csp.sentinel.command.CommandHandler

Use of Sentinel API

This section briefly describes how to use Sentinel API in your code. Sentinel has three main APIs:

  • SphU: Add resources that need to be monitored and protected by sentinel
  • Tracer: Statistics business exceptions (non-BlockException exceptions)
  • ContextUtil: A context tool class, usually used to identify the source of the call

The sample code is as follows:

@GetMapping("/test-sentinel-api")
public String testSentinelAPI(@RequestParam(required = false) String a) {
    String resourceName = "test-sentinel-api";
    // try-with-resources is not used here because Tracer.trace does not count exceptions
    Entry entry = null;
    try {
        // Define a sentinel protected resource named test-sentinel-api
        entry = SphU.entry(resourceName);
        // Identify the source of the call to test-sentinel-api as test-origin (for "source-specific" configuration in flow control rules)
        ContextUtil.enter(resourceName, "test-origin");
        // Simulating the time-consuming execution of protected business logic
        Thread.sleep(100);
        return a;
    } catch (BlockException e) {
        // If the protected resource is limited or degraded, a BlockException is thrown
        log.warn("Resources are limited or degraded", e);
        return "Resources are limited or degraded";
    } catch (InterruptedException e) {
        // Statistics of business anomalies
        Tracer.trace(e);
        return "Happen InterruptedException";
    } finally {
        if (entry != null) {
            entry.exit();
        }

        ContextUtil.exit();
    }
}

Explain a few possible doubts:

  • Resource Name: It can be filled in arbitrarily as long as it is unique. Usually the interface name is used.
  • ContextUtil.enter: In this example, the source used to identify the call to test-sentinel-api is test-origin. For example, if the resource is invoked using postman or other requests, its source will be identified as test-origin.
  • Tracer.trace: In the rule of descent, the threshold of abnormal proportion or number can be degraded. Sentinel only counts BlockException and its subclasses. Other abnormalities are not in the scope of statistics, so it is necessary to use Tracer.trace manual statistics. Version 1.3.1 begins to support automatic statistics, which will be introduced in the next section.

Relevant official documents:

@ Sentinel Resource Annotation

From the code examples in the previous section, you can see that the Sentinel APIs are not very elegant in their use, which is somewhat similar to the feeling of using the I/O stream API, and the code is bloated. Fortunately, the @Sentinel Resource annotation has been supported in entinel version 1.3.1, which allows us to avoid writing such bloated code. But even so, it is necessary to learn how Sentinel APIs are used, because the bottom of Sentinel APIs still need to be implemented through these APIs.

Learning an annotation relieves the need to know what it can do, as well as the role of the attributes it supports. The following table summarizes the attributes of the @SentinelResource annotation:

attribute Effect Is it necessary?
value Resource Name yes
entryType entry type, marking the direction of traffic, value IN/OUT, default is OUT no
blockHandler The name of the function that handles BlockException no
blockHandlerClass Classes that store blockHandler. The corresponding processing function must be static ally modified, otherwise it cannot be parsed. no
fallback Used to provide fallback processing logic when an exception is thrown. The fallback function can handle all types of exceptions except those excluded from exceptions to Ignore no
fallbackClass [1.6 support] The class that stores fallback. The corresponding processing function must be static ally modified, otherwise it cannot be parsed. no
defaultFallback [1.6 support] For general fallback logic. The default fallback function can handle all types of exceptions except those excluded from exceptions to Ignore. If fallback and defaultFallback are configured at the same time, take fallback as the criterion. no
Exceptions to Ignore [1.6 support] Specifies which exceptions are excluded. Excluded exceptions will not be counted in the exception statistics, nor will they enter fallback logic, but will be thrown as they are. no
exceptionsToTrace Exceptions requiring trace Throwable

blockHandler, which handles the requirements of BlockException functions:

  1. Must be public
  2. The return type is the same as the original method.
  3. The parameter type needs to match the original method and add the BlockException type parameter at the end.
  4. The default requirement is in the same class as the original method. If you want to use functions of other classes, configure blockHandlerClass and specify the methods in blockHandlerClass

The fallback function requires:

  1. The return type is the same as the original method.
  2. The parameter type needs to match the original method, starting with Sentinel 1.6, or adding Throwable type parameters at the end of the method.
  3. The default requirement is in the same class as the original method. If you want to use functions of other classes, you can configure fallbackClass and specify the methods in fallbackClass.

The defaultFallback function requires:

  1. The return type is the same as the original method.
  2. Method parameter list is empty, or has a Throwable type parameter
  3. The default requirement is in the same class as the original method. If you want to use functions of other classes, you can configure fallbackClass and specify the methods in fallbackClass.

Now we have a comprehensive understanding of the @SentinelResource annotation. Next, we use the @SentinelResource annotation to refactor the code before refactoring, and intuitively understand what conveniences this annotation brings. The refactored code is as follows:

@GetMapping("/test-sentinel-resource")
@SentinelResource(
        value = "test-sentinel-resource",
        blockHandler = "blockHandlerFunc",
        fallback = "fallbackFunc"
)
public String testSentinelResource(@RequestParam(required = false) String a)
        throws InterruptedException {
    // Simulating the time-consuming execution of protected business logic
    Thread.sleep(100);

    return a;
}

/**
 * Functions handling BlockException (handling current limitation)
 */
public String blockHandlerFunc(String a, BlockException e) {
    // If the protected resource is limited or degraded, a BlockException is thrown
    log.warn("Resources are limited or degraded.", e);
    return "Resources are limited or degraded";
}

/**
 * 1.6 Pre-processing demotion
 * 1.6 Start with all types of exceptions (except those excluded from exceptions to Ignore)
 */
public String fallbackFunc(String a) {
    return "Something unusual has happened.";
}

Note: @SentinelResource annotation does not currently support identifying the source of the call

Tips:

Pre-1.6.0 versions of fallback functions only deal with DegradeException, not business exceptions.

If both blockHandler and fallback are configured, they will be degraded by current restriction and will only enter when BlockException is thrown out.
BlockHandler processing logic. If blockHandler, fallback and defaultFallback are not configured, the BlockException will be thrown directly when the current limit is degraded.

Beginning with version 1.3.1, annotations define resources to support automatic statistics of business exceptions without the need to manually call Tracer.trace(ex) to record business exceptions. Previous versions of Sentinel 1.3.1 needed to call Tracer.trace(ex) on their own to record business exceptions.

@ Sentinel Resource annotation related source code:

  • com.alibaba.csp.sentinel.annotation.aspectj.AbstractSentinelAspectSupport
  • com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect

Relevant official documents:

RestTemplate integrates Sentinel

If you know Hystrix, you should know that Hystrix is fault-tolerant not only for the interface of the current service, but also for the interface of the service provider (the callee). So far, we have only introduced how to add relevant rules to the interface of the current service in the entinel console for fault tolerance, but not how to make the interface of the service provider fault tolerant.

In fact, with the forerunner in mind, it's easy to implement fault tolerance for service providers'interfaces. We all know that in Spring Cloud system, communication between micro services can be achieved through RestTemplate or Feign. So you just need to do an article on RestTemplate or Feign. This section first takes RestTemplate as an example to show how to integrate Sentinel to implement fault tolerance for service providers'interfaces.

Simply add the @Sentinel RestTemplate annotation to the method of configuring RestTemplate, using only one annotation. The code is as follows:

package com.zj.node.contentcenter.configuration;

import org.springframework.cloud.alibaba.sentinel.annotation.SentinelRestTemplate;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class BeanConfig {

    @Bean
    @LoadBalanced
    @SentinelRestTemplate
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Note: @SentinelRestTemplate annotation contains blockHandler, blockHandler Class, fallback, fallbackClass attributes, which are used in the same way as the @SentinelResource annotation, so we can use these attributes to customize our exception handling logic when triggering current limiting and downgrading.

Then we write a piece of test code to call the service provider's interface. The code is as follows:

package com.zj.node.contentcenter.controller.content;

import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@Slf4j
@RestController
@RequiredArgsConstructor
public class TestController {

    private final RestTemplate restTemplate;

    @GetMapping("/test-rest-template-sentinel/{userId}")
    public UserDTO test(@PathVariable("userId") Integer userId) {
        // The interface for invoking the user-center service (in this case, the user-center is the service provider)
        return restTemplate.getForObject(
                "http://user-center/users/{userId}", UserDTO.class, userId);
    }
}

After writing the above code to restart the project and access the test interface properly, in the cluster link of the entinel console, you can see that the user-center interface has been registered here. Now you only need to add relevant rules to it to realize fault tolerance:

If we don't want Sentinel to be fault-tolerant for the service provider's interface during development, we can switch through the following configuration:

# Open or close the @Sentinel RestTemplate annotation
resttemplate:
  sentinel:
    enabled: true

Sentinel implements source code for integration with RestTemplate:

  • org.springframework.cloud.alibaba.sentinel.custom.SentinelBeanPostProcessor

Feign Integration Sentinel

The previous section introduced RestTemplate's integration with Sentinel, which has already made some preparations. Here's a direct example. First, add the following configuration in the configuration file:

feign:
  sentinel:
    # Open Sentinel's support for Feign
    enabled: true

Define a FeignClient interface:

package com.zj.node.contentcenter.feignclient;

import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

Similarly, write a test code to invoke the service provider's interface. The code is as follows:

package com.zj.node.contentcenter.controller.content;

import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import com.zj.node.contentcenter.feignclient.UserCenterFeignClient;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TestFeignController {

    private final UserCenterFeignClient feignClient;

    @GetMapping("/test-feign/{id}")
    public UserDTO test(@PathVariable Integer id) {
        // The interface for invoking the user-center service (in this case, the user-center is the service provider)
        return feignClient.findById(id);
    }
}

After writing the above code to restart the project and access the test interface properly, in the cluster link of the entinel console, you can see that the user-center interface has been registered here, and the behavior is the same as that of RestTemplate integration Sentinel:

By default, Sentinel's handling throws an exception directly when current limitation and degradation occur. What if you need to customize the exception handling logic when the current limit and downgrade occur instead of throwing the exception directly? @ There is a fallback attribute in the FeignClient annotation that specifies which class to use when a remote call fails. So in this example, we first need to define a class and implement the UserCenter FeignClient interface. The code is as follows:

package com.zj.node.contentcenter.feignclient.fallback;

import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import com.zj.node.contentcenter.feignclient.UserCenterFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient {

    @Override
    public UserDTO findById(Integer id) {
        // Custom current limiting and degrading processing logic
        log.warn("Remote calls are current-limited/Downgraded");
        return UserDTO.builder().
                wxNickname("Default").
                build();
    }
}

Then the fallback property is specified on the @FeignClient annotation of the User Center FeignClient interface, as follows:

@FeignClient(name = "user-center", fallback = UserCenterFeignClientFallback.class)
public interface UserCenterFeignClient {
    ...

Next, let's do a simple test to see if the fallback property calls the method in the implementation class when the remote call fails. Add a flow control rule to the service provider's interface as follows:

With postman, requests frequently occur, and when QPS exceeds 1, the results are as follows:

As you can see, the default values defined in the code are returned. This proves that when the remote call fails due to current limitation, degradation or other reasons, the method implemented in the UserCenterFeignClientFallback class will be invoked.

But there is another problem. This method can not get the exception object, and the console will not output any relevant exception information. What if the business needs to print the exception log or deal with the exception? Another attribute in the @FeignClient annotation, fallbackFactory, is also needed to define a class, except that the interfaces implemented are different. The code is as follows:

package com.zj.node.contentcenter.feignclient.fallbackfactory;

import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import com.zj.node.contentcenter.feignclient.UserCenterFeignClient;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {

    @Override
    public UserCenterFeignClient create(Throwable cause) {

        return new UserCenterFeignClient() {
            @Override
            public UserDTO findById(Integer id) {
                // Custom current limiting and degrading processing logic
                log.warn("Remote calls are current-limited/Downgraded", cause);
                return UserDTO.builder().
                        wxNickname("Default").
                        build();
            }
        };
    }
}

Specify the fallbackFactory property on the @FeignClient annotation of the User Center FeignClient interface, as follows:

@FeignClient(name = "user-center", fallbackFactory = UserCenterFeignClientFallbackFactory.class)
public interface UserCenterFeignClient {
    ...

It should be noted that fallback and fallbackFactory can only choose one from the other and can not be used at the same time.

Repeat the previous test, and the console can output the relevant exception information:

Sentinel implements associated source code for integration with Feign:

  • org.springframework.cloud.alibaba.sentinel.feign.SentinelFeign

Sentinel Code Level Posture Summary

Topics: Java Lombok Attribute Spring REST