Hystrix component analysis

Having finished Feign and Ribbon, today we will study another class library developed by Netflix team - Hystrix.

At an abstract level, Hystrix is a protector. It can protect our application from down due to a dependent failure.

At present, the official has stopped iterating with Hystrix. On the one hand, it is considered that Hystrix is stable enough. On the other hand, it has turned to more flexible protectors (instead of enabling protection according to pre configuration), such as resilience4j. Of course, stopping iteration does not mean that Hystrix has no value, and many of its ideas are still worth learning and using for reference.

 

  As before, the Hystrix studied in this paper is native, not encapsulated by Spring.

What problems did Hystrix solve

Officials have given detailed answers to this question (see the official wiki at the end of the article). Here I combine to give some of my own understanding (the following figure is also borrowed from the official).

Our applications often need to call some dependencies. The dependency here is generally remote service, so why not directly say remote service? Because the applicable scenarios of Hystrix are broader, when we finish learning Hystrix, we will find that even ordinary methods called in the application can be regarded as dependency.

 

When calling these dependencies, you may encounter exceptions: call failure or call timeout.

Let's talk about the call failure. When a dependency goes down, our application will fail to call it. In this case, we will consider fast failure, so as to reduce the overhead of a large number of call failures.

 

Besides, the call timed out. Unlike the call failure, the dependency is still available at this time, but it takes more time to get what we want. When the traffic is large, the thread pool will soon be exhausted. In large-scale projects, the impact of a dependency timeout will be amplified, and even lead to the paralysis of the whole system. Therefore, call failure also needs to fail quickly.

For the above exceptions, Hystrix can isolate the failed dependencies in time, and subsequent calls will fail quickly until the dependencies return to normal.

How

After the call fails or the timeout reaches a certain threshold, the protector of Hystrix will be triggered to open.

Before invoking dependency, Hystrix will check whether the protector is turned on. If it is turned on, it will go directly to fall back. If it is not turned on, it will execute the calling operation.

In addition, Hystrix will regularly check whether the dependency has been restored. When the dependency is restored, the protector will be closed and the whole call link will return to normal.

Of course, the actual process is more complex, and it also involves caching, thread pool, etc. The official provided a picture and gave a more detailed description.

How to use

Here I use specific examples to illustrate the logic of each node. See the link at the end of the article for the project code.

Package as command

First, to use Hystrix, we need to wrap the call request for a dependency into a command by inheriting Hystrix command   or   Package the HystrixObservableCommand. After inheritance, we need to do three things:

  1. Specify commandKey and commandGroupKey in the construct. It should be noted that commands with the same commandGroupKey will share a thread pool, and commands with the same commandKey will share a protector and cache. For example, we need to obtain user objects from UC service according to user id. we can make all UC interfaces share a commandGroupKey, and different interfaces use different commandkeys.
  2. Override the run or construct method. In this method, we call a dependent code. I can put the code calling the remote service or print a sentence at will. Therefore, as I said earlier, the definition of dependency can be broader, not limited to the remote service.
  3. Override the getFallback method. This method is used when a quick failure occurs.
java
public class CommandGetUserByIdFromUserService extends HystrixCommand<DataResponse<User>> {
    
    private final String userId;
    
    public CommandGetUserByIdFromUserService(String userId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // The same command group shares a ThreadPool
                .andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// The same command key shares a CircuitBreaker and requestCache
               );
        this.userId = userId;
    }

    /**
     * Execute the final task. If you inherit the HystrixObservableCommand, override construct()
     */
    @Override
    protected DataResponse<User> run() {
        return userService.getUserById(userId);
    }
    
    /**
     * This method is called in the following scenarios
     * 1. An exception is thrown when the final task is executed;
     * 2. Final task execution timeout;
     * 3. Request short circuit when the circuit breaker is opened;
     * 4. Connection pool, queue, or semaphore exhausted
     */
    @Override
    protected DataResponse<User> getFallback() {
        return DataResponse.buildFailure("fail or timeout");
    }
}

Execute command

Then, only execute the command, the above figure will "move". There are four methods to execute commands. Calling execute() or observe() will execute immediately, while calling queue() or toObservable() will not execute immediately. It will not be executed until future.get() or observable.subscribe().

java
    @Test
    public void testExecuteWays() throws Exception {
        
        DataResponse<User> response = new CommandGetUserByIdFromUserService("1").execute();// execute()=queue().get() synchronize
        LOG.info("command.execute():{}", response);
        
        Future<DataResponse<User>> future = new CommandGetUserByIdFromUserService("1").queue();//queue()=toObservable().toBlocking().toFuture() synchronize
        LOG.info("command.queue().get():{}", future.get());
        
        Observable<DataResponse<User>> observable = new CommandGetUserByIdFromUserService("1").observe();//hot observable asynchronous
        
        observable.subscribe(x -> LOG.info("command.observe():{}", x));
        
        Observable<DataResponse<User>> observable2 = new CommandGetUserByIdFromUserService("1").toObservable();//cold observable asynchronous
        
        observable2.subscribe(x -> LOG.info("command.toObservable():{}", x));
    }

Use cache

Then, after entering the command logic, Hystrix will first determine whether to use cache.

By default, caching is disabled and can be enabled by overriding getCacheKey() of command (it will be enabled as long as it returns non empty).

java
    @Override
    protected String getCacheKey() {
        return userId;
    }

It should be noted that when using the cache (HystrixRequestCache), request log (HystrixRequestLog) and batch processing (HystrixCollapser), you need to initialize the HystrixRequestContext and call it in the following try...finally format:

java
    @Test
    public void testCache() {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try {
            CommandGetUserByIdFromUserService command1 = new CommandGetUserByIdFromUserService("1");
            command1.execute();
            // There is no in the cache at the first call
            assertFalse(command1.isResponseFromCache());
            
            CommandGetUserByIdFromUserService command2 = new CommandGetUserByIdFromUserService("1");
            command2.execute();
            // The second call fetches the result directly from the cache
            assertTrue(command2.isResponseFromCache());
        } finally {
            context.shutdown();
        }
        // zzs001
    }

Whether the protector is on

Next, Hystrix will determine whether the protector is on.

Here, I manually create fail or time out in the run method of command. In addition, we can adjust the threshold of protector opening through hystrix command properties.

java
    public CommandGetUserByIdFromUserService(String userId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // The same command group shares a ThreadPool
                .andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// The same command key shares a CircuitBreaker and requestCache
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerRequestVolumeThreshold(10)
                        .withCircuitBreakerErrorThresholdPercentage(50)
                        .withMetricsHealthSnapshotIntervalInMilliseconds(1000)
                        .withExecutionTimeoutInMilliseconds(1000)
                        ));
        this.userId = userId;
    }
    @Override
    protected DataResponse<User> run() {
        LOG.info("To execute the final task, the thread is:{}", Thread.currentThread());
        // Manual manufacturing timeout
        /*try {
            Thread.sleep(1200);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }*/
        // Manual manufacturing exception
        throw new RuntimeException("");
        //return UserService.instance().getUserById(userId);
    }

At this time, when the call fails to reach a certain threshold, the protector is triggered and opened, and subsequent requests will go directly to the fall back.

java
    @Test
    public void testCircuitBreaker() {
        CommandGetUserByIdFromUserService command;
        int count = 1;
        do {
            command = new CommandGetUserByIdFromUserService("1");
            command.execute();
            count++;
        } while(!command.isCircuitBreakerOpen());
        LOG.info("call{}After this time, the circuit breaker opens", count);
        
        // If you call it again at this time, you will go directly to fall back
        command = new CommandGetUserByIdFromUserService("1");
        command.execute();
        assertTrue(command.isCircuitBreakerOpen());
    }

Is the connection pool, queue, or semaphore exhausted

Even if the protector is closed, we can't call the dependency immediately. We need to check whether the connection pool or semaphore is exhausted first (you can configure whether to use thread pool or semaphore through hystrix command properties).

Because the default thread pool is relatively large, I have reduced the thread pool through HystrixThreadPoolProperties.

java
    public CommandGetUserByIdFromUserService(String userId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // The same command group shares a ThreadPool
                .andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// The same command key shares a CircuitBreaker and requestCache
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withCoreSize(2)
                        .withMaxQueueSize(5)
                        .withQueueSizeRejectionThreshold(5)
                        ));
        this.userId = userId;
    }

At this time, when the thread pool is exhausted, subsequent requests will go directly to the fall back, and the protector is not turned on.

java
    @Test
    public void testThreadPoolFull() throws InterruptedException {
        
        int maxRequest = 100;
        
        int i = 0;
        do {
            CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
            command.toObservable().subscribe(v -> LOG.info("non-blocking command.toObservable():{}", v));
            LOG.info("Whether the thread pool, queue, or semaphore is exhausted:{}", command.isResponseRejected());
            
        } while(i++ < maxRequest - 1);
        
        
        // If you call it again at this time, you will go directly to fall back
        CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
        command.execute();
        // The thread pool, queue, or semaphore is exhausted
        assertTrue(command.isResponseRejected());
        assertFalse(command.isCircuitBreakerOpen());
        
        Thread.sleep(10000);
        // zzs001
    }

epilogue

The above is a brief conclusion of Hystrix. After reading the official wiki and combining the above examples, I believe I have a deep understanding of Hystrix.

Posted by shiranwas on Mon, 22 Nov 2021 11:04:58 -0800