微服务hystrix-javanica详解 翻译

方嘉志
2023-12-01

hystrix-javanica

Java语言比其他语言(如反射和注解)具有很大的优势。所有现代框架,如Spring,Hibernate,myBatis等都力求最大限度地利用这一优势。
在Hystrix中引入注解的想法是改进的明显解决方案。目前使用Hystrix涉及编写大量代码,这是快速开发的障碍。您可能花费大量时间编写Hystrix命令。
通过引入支持注解,Javanica项目使Hystrix更容易使用。

首先,为了使用hystrix-javanica,您需要在项目中添加hystrix-javanica依赖关系。

Maven样例:

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-javanica</artifactId>
    <version>x.y.z</version>
</dependency>

在项目中实现AOP功能可使用了AspectJ库。如果在您的项目中已经使用AspectJ,那么需要在aop.xml中添加hystrix切面,如下所示:

<aspects>
        ...
        <aspect name="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"/>
        ...
</aspects>

关于AspectJ配置的更多信息请读这里

如果在项目中使用Spring AOP,则需要使用Spring AOP命名空间添加特定配置,以使Spring能够管理使用AspectJ编写的切面,并将HystrixCommandAspect声明为Spring bean,如下所示:

<aop:aspectj-autoproxy/>
<bean id="hystrixAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"></bean>

或者如果您使用的是Spring java配置:

@Configuration
public class HystrixConfiguration {

  @Bean
  public HystrixCommandAspect hystrixAspect() {
    return new HystrixCommandAspect();
  }

}

在Spring中使用哪种方法来创建代理并不重要,javanica适用于JDK和CGLIB代理。如果您为aop使用另一个支持AspectJ的框架,并使用其他lib(例如Javassist)创建代理,
那么让我们知道您用于创建代理的lib,我们将在不久的将来尝试添加对该库的支持。

更多关于Spring AOP AspectJ知识请读这里

切面织入

Javanica支持两种编织模式:编译时和运行时。目前加载时织入没有经过测试,但它应该工作。

如何使用

Hystrix命令

同步执行

要以Hystrix命令同步运行方法,您需要使用@HystrixCommand注解来注释方法:

public class UserService {
...
    @HystrixCommand
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }
}
...

在上面的例子中,getUserById方法将在新的Hystrix命令中同步处理。默认情况下,command key的名称是命令方法名称:getUserById
默认group key 名称是被注释方法的类名称:UserService。当然,您也可以使用@HystrixCommand的属性更改它:

    @HystrixCommand(groupKey="UserGroup", commandKey = "GetUserByIdCommand")
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

设置threadPoolKey可使用@HystrixCommand#threadPoolKey()

异步执行

要异步处理Hystrix命令,您应该在命令方法中返回AsyncResult的实例,如下所示:

    @HystrixCommand
    public Future<User> getUserByIdAsync(final String id) {
        return new AsyncResult<User>() {
            @Override
            public User invoke() {
                return userResource.getUserById(id);
            }
        };
    }

命令方法的返回类型应为Future,表示应该以异步方式执行命令

响应式执行

要想“Reactive Execution”,您应该在命令方法中返回一个Observable的实例,如下例所示:

    @HystrixCommand
    public Observable<User> getUserById(final String id) {
        return Observable.create(new Observable.OnSubscribe<User>() {

                @Override
                public void call(Subscriber<? super User> observer) {
                    try {
                        if (!observer.isUnsubscribed()) {
                            observer.onNext(new User(id, name + id));
                            observer.onCompleted();
                        }
                    } catch (Exception e) {
                        observer.onError(e);
                    }
                }

            });
    }

命令方法的返回类型应该是Observable

HystrixObservable接口提供了两种方法:observe() - 与HystrixCommand#queue()HystrixCommand#execute()行为一样,立即开始执行命令;
toObservable() - 一旦Observable被订阅,懒惰地开始执行命令。
为了控制这种行为,并且在两种模式之间切换,@HystrixCommand提供了名为observableExecutionMode的特定属性。
@HystrixCommand(observableExecutionMode = EAGER)表示应该使用observe()方法执行observable命令;
@HystrixCommand(observableExecutionMode = LAZY)表示应该使用toObservable()方法来执行observable命令。

注意:默认情况下使用EAGER模式

Fallback

可以通过在@HystrixCommand中声明`fallbackMethod`来实现正常退化,像下面这样:

 @HystrixCommand(fallbackMethod = "defaultUser")
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

    private User defaultUser(String id) {
        return new User("def", "def");
    }

重要的是要记住,Hystrix命令和回退方法应该放在同一个类中并具有相同的方法签名(执行失败的异常(诱发服务降级的异常)为可选参数)。回退方法可以有任何访问修饰符。

方法defaultUser将用于在任何错误的情况下处理回退逻辑。如果您需要将fallback methoddefaultUser作为单独的Hystrix命令运行,那么您需要使用HystrixCommand注释对其进行注释,如下所示:

    @HystrixCommand(fallbackMethod = "defaultUser")
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

    @HystrixCommand
    private User defaultUser(String id) {
        return new User();
    }

如果回退方法标记为@HystrixCommand,那么这种回退方法(defaultUser)也可以有自己的回退方法,如下例所示:

    @HystrixCommand(fallbackMethod = "defaultUser")
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

    @HystrixCommand(fallbackMethod = "defaultUserSecond")
    private User defaultUser(String id) {
        return new User();
    }

    @HystrixCommand
    private User defaultUserSecond(String id) {
        return new User("def", "def");
    }

Javanica提供了在执行fallback中获取执行异常(导致命令失败抛出的异常)的能力。你可以使用附加参数扩展fallback方法签名,以获取命令抛出的异常。
Javanica通过fallback方法的附加参数来公开执行异常。执行异常是通过调用方法getExecutionException()在vanilla hystrix中得到的。

示例:

    @HystrixCommand(fallbackMethod = "fallback1")
    User getUserById(String id) {
        throw new RuntimeException("getUserById command failed");
    }

    @HystrixCommand(fallbackMethod = "fallback2")
    User fallback1(String id, Throwable e) {
        assert "getUserById command failed".equals(e.getMessage());
        throw new RuntimeException("fallback1 failed");
    }

    @HystrixCommand(fallbackMethod = "fallback3")
    User fallback2(String id) {
        throw new RuntimeException("fallback2 failed");
    }

    @HystrixCommand(fallbackMethod = "staticFallback")
    User fallback3(String id, Throwable e) {
        assert "fallback2 failed".equals(e.getMessage());
        throw new RuntimeException("fallback3 failed");
    }

    User staticFallback(String id, Throwable e) {
        assert "fallback3 failed".equals(e.getMessage());
        return new User("def", "def");
    }

    // test
    @Test
    public void test() {
        assertEquals("def", getUserById("1").getName());
    }

如你所见,附加的Throwable参数不是强制性的,可以省略或指定。fallback可以得到父层方法执行失败的异常,因此fallback3会收到fallback2抛出的异常,而不是getUserById命令中的异常。

Async/Sync fallback

回退可以是异步或同步,在某些情况下,它取决于命令执行类型,下面列出了所有可能的使用:

支持

case 1: 同步 command, 同步 fallback

   @HystrixCommand(fallbackMethod = "fallback")
    User getUserById(String id) {
        throw new RuntimeException("getUserById command failed");
    }

    @HystrixCommand
    User fallback(String id) {
        return new User("def", "def");
    }

case 2: 异步 command, 同步 fallback

    @HystrixCommand(fallbackMethod = "fallback")
    Future<User> getUserById(String id) {
        throw new RuntimeException("getUserById command failed");
    }

    @HystrixCommand
    User fallback(String id) {
        return new User("def", "def");
    }

case 3: 异步 command, 异步 fallback

    @HystrixCommand(fallbackMethod = "fallbackAsync")
    Future<User> getUserById(String id) {
        throw new RuntimeException("getUserById command failed");
    }

    @HystrixCommand
    Future<User> fallbackAsync(String id) {
        return new AsyncResult<User>() {
            @Override
            public User invoke() {
                return new User("def", "def");
            }
        };
    }

不支持(禁止)

case 1: 同步 command, 异步 fallback。这种情况是不支持的,因为在本质上,一个调用者执行getUserById方法不会得到Future的结果,并且fallback方法提供的Future结果对调用者根本不可用。
因此执行命令会在调用者获得结果之前强制完成fallbackAsync,已经说过,事实证明,async fallback执行没有任何好处。但是,如果同步和异步命令同时使用某个fallback,这可能是很方便,
如果你看到这种情况是非常有帮助的,那么请来创建问题以便我们很好支持。

    @HystrixCommand(fallbackMethod = "fallbackAsync")
    User getUserById(String id) {
        throw new RuntimeException("getUserById command failed");
    }

    @HystrixCommand
    Future<User> fallbackAsync(String id) {
        return new AsyncResult<User>() {
            @Override
            public User invoke() {
                return new User("def", "def"); // 实际期望获取这个结果,但是被强制执行返回了Future<User>结果,该结果对调用者不可用
            }
        };
    }

case 2: 同步 command, 异步 fallback, 与案例1相同的原因,不支持这种情况。

    @HystrixCommand(fallbackMethod = "fallbackAsync")
    User getUserById(String id) {
        throw new RuntimeException("getUserById command failed");
    }

    Future<User> fallbackAsync(String id) {
        return new AsyncResult<User>() {
            @Override
            public User invoke() {
                return new User("def", "def");
            }
        };
    }

在javanica中使用observable功能有相同的限制。

类或具体命令的默认回退

此功能允许为整个类或具体命令定义默认回退。如果您有一批具有完全相同的回退逻辑的命令,您仍然必须为每个命令定义回退方法,因为回退方法应该具有与命令完全相同的签名,请考虑以下代码:

    public class Service {
        @RequestMapping(value = "/test1")
        @HystrixCommand(fallbackMethod = "fallback")
        public APIResponse test1(String param1) {
            // some codes here
            return APIResponse.success("success");
        }

        @RequestMapping(value = "/test2")
        @HystrixCommand(fallbackMethod = "fallback")
        public APIResponse test2() {
            // some codes here
            return APIResponse.success("success");
        }

        @RequestMapping(value = "/test3")
        @HystrixCommand(fallbackMethod = "fallback")
        public APIResponse test3(ObjectRequest obj) {
            // some codes here
            return APIResponse.success("success");
        }

        private APIResponse fallback(String param1) {
            return APIResponse.failed("Server is busy");
        }

        private APIResponse fallback() {
            return APIResponse.failed("Server is busy");
        }

        private APIResponse fallback(ObjectRequest obj) {
            return APIResponse.failed("Server is busy");
        }
    }

默认回退功能允许采用DRY原则(Don’t Repeat Yourself,不要重复你自己)并摆脱冗余:

    @DefaultProperties(defaultFallback = "fallback")
    public class Service {
        @RequestMapping(value = "/test1")
        @HystrixCommand
        public APIResponse test1(String param1) {
            // some codes here
            return APIResponse.success("success");
        }

        @RequestMapping(value = "/test2")
        @HystrixCommand
        public APIResponse test2() {
            // some codes here
            return APIResponse.success("success");
        }

        @RequestMapping(value = "/test3")
        @HystrixCommand
        public APIResponse test3(ObjectRequest obj) {
            // some codes here
            return APIResponse.success("success");
        }

        private APIResponse fallback() {
            return APIResponse.failed("Server is busy");
        }
    }

默认的回退方法不应该有任何参数,除了可以附加获取执行异常参数,不应该抛出任何异常。以降序优先级列(如果设置1,则不执行2)出如下:

  1. 使用@HystrixCommand的fallbackMethod属性定义命令回退
  2. 使用@HystrixCommand的defaultFallback属性定义命令默认回退
  3. 使用@DefaultProperties的defaultFallback属性定义类默认回退

错误传播

基于此描述,@HystrixCommand具有指定应被忽略的异常类型的能力。

    @HystrixCommand(ignoreExceptions = {BadRequestException.class})
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }
  • 1
  • 2
  • 3
  • 4

如果userResource.getUserById(id);抛出类型为BadRequestException的异常,则此异常将被包装在HystrixBadRequestException中,并重新抛出,而不触发后备逻辑。
你不需要手动执行,javanica会为你做这个。

值得注意的是,默认情况下,一个调用者总是会得到根本原因异常,例如BadRequestException,而不是HystrixBadRequestExceptionHystrixRuntimeException(除非有执行代码显式抛出这些异常的情况)。

可选地,通过使用raiseHystrixExceptions,可以禁用HystrixRuntimeException的拆箱。即所有未被忽略的异常会作为HystrixRuntimeException的cause出现。

    @HystrixCommand(
        ignoreExceptions = {BadRequestException.class},
        raiseHystrixExceptions = {HystrixException.RUNTIME_EXCEPTION})
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

注意:如果命令有一个回退,则只有触发回退逻辑的第一个异常将被传播给调用者。例:

    class Service {
        @HystrixCommand(fallbackMethod = "fallback")
        Object command(Object o) throws CommandException {
            throw new CommandException();
        }

        @HystrixCommand
        Object fallback(Object o) throws FallbackException {
            throw new FallbackException();
        }
    }

    // in client code
    {
        try {
            service.command(null);
        } catch (Exception e) {
        assert CommandException.class.equals(e.getClass())
        }
    }

请求缓存

……

配置

命令属性

命令属性可以使用@HystrixCommand的’commandProperties’设置,如下所示:

    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
        })
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

Javanica使用Hystrix ConfigurationManager动态设置属性。对于上面的例子,Javanica幕后执行的动作:

ConfigurationManager.getConfigInstance().setProperty("hystrix.command.getUserById.execution.isolation.thread.timeoutInMilliseconds", "500");

更多关于Hystrix命令属性commandfallback

ThreadPoolProperties可以使用@HystrixCommand的’threadPoolProperties’设置,如下所示:

    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
        },
                threadPoolProperties = {
                        @HystrixProperty(name = "coreSize", value = "30"),
                        @HystrixProperty(name = "maxQueueSize", value = "101"),
                        @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                        @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                        @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
                        @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
        })
    public User getUserById(String id) {
        return userResource.getUserById(id);
    }

默认属性

@DefaultProperties是类(类型)级别注释,允许设置的默认命令属性,有groupKeythreadPoolKeycommandPropertiesthreadPoolProperties
ignoreExceptionsraiseHystrixExceptions
默认情况下,使用此注释指定的属性将在注释类中定义的每个hystrix命令使用,除非命令明确使用相应的@HystrixCommand参数来指定这些属性(覆盖默认行为)。例:

    @DefaultProperties(groupKey = "DefaultGroupKey")
    class Service {
        @HystrixCommand // hystrix command group key is 'DefaultGroupKey'
        public Object commandInheritsDefaultProperties() {
            return null;
        }
        @HystrixCommand(groupKey = "SpecificGroupKey") // command overrides default group key
        public Object commandOverridesGroupKey() {
            return null;
        }
    }

Hystrix collapser

Suppose you have some command which calls should be collapsed in one backend call. For this goal you can use @HystrixCollapser annotation.

Example:

/** Asynchronous Execution */
    @HystrixCollapser(batchMethod = "getUserByIds")
    public Future<User> getUserByIdAsync(String id) {
        return null;
    }
    
    /** Reactive Execution */
    @HystrixCollapser(batchMethod = "getUserByIds")
    public Observable<User> getUserByIdReact(String id) {
        return null;
    }    
        
    @HystrixCommand
    public List<User> getUserByIds(List<String> ids) {
        List<User> users = new ArrayList<User>();
        for (String id : ids) {
            users.add(new User(id, "name: " + id));
        }
        return users;
    }

    // Async
    Future<User> f1 = userService.getUserByIdAsync("1");
    Future<User> f2 = userService.getUserByIdAsync("2");
    Future<User> f3 = userService.getUserByIdAsync("3");
    Future<User> f4 = userService.getUserByIdAsync("4");
    Future<User> f5 = userService.getUserByIdAsync("5");
    
    // Reactive
    Observable<User> u1 = getUserByIdReact("1");
    Observable<User> u2 = getUserByIdReact("2");
    Observable<User> u3 = getUserByIdReact("3");
    Observable<User> u4 = getUserByIdReact("4");
    Observable<User> u5 = getUserByIdReact("5");
    
    // Materialize reactive commands
    Iterable<User> users = Observables.merge(u1, u2, u3, u4, u5).toBlocking().toIterable();

A method annotated with @HystrixCollapser annotation can return any value with compatible type, it does not affect the result of collapser execution, collapser method can even return null or another stub. There are several rules applied for methods signatures.

  1. Collapser method must have one argument of any type, desired a wrapper of a primitive type like Integer, Long, String and etc.
  2. A batch method must have one argument with type java.util.List parameterized with corresponding type, that's if a type of collapser argument is Integer then type of batch method argument must be List<Integer>.
  3. Return type of batch method must be java.util.List parameterized with corresponding type, that's if a return type of collapser method is User then a return type of batch command must be List<User>.

Convention for batch method behavior

The size of response collection must be equal to the size of request collection.

@HystrixCommand
  public List<User> getUserByIds(List<String> ids); // batch method
  
  List<String> ids = List("1", "2", "3");
  getUserByIds(ids).size() == ids.size();

Order of elements in response collection must be same as in request collection.

 @HystrixCommand
  public List<User> getUserByIds(List<String> ids); // batch method
  
  List<String> ids = List("1", "2", "3");
  List<User> users = getUserByIds(ids);
  System.out.println(users);
  // output
  User: id=1
  User: id=2
  User: id=3

Why order of elements of request and response collections is important?

The reason of this is in reducing logic, basically request elements are mapped one-to-one to response elements. Thus if order of elements of request collection is different then the result of execution can be unpredictable.

Deduplication batch command request parameters.

In some cases your batch method can depend on behavior of third-party service or library that skips duplicates in a request. It can be a rest service that expects unique values and ignores duplicates. In this case the size of elements in request collection can be different from size of elements in response collection. It violates one of the behavior principle. To fix it you need manually map request to response, for example:

// hava 8
@HystrixCommand
List<User> batchMethod(List<String> ids){
// ids = [1, 2, 2, 3]
List<User> users = restClient.getUsersByIds(ids);
// users = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
List<User> response = ids.stream().map(it -> users.stream()
                .filter(u -> u.getId().equals(it)).findFirst().get())
                .collect(Collectors.toList());
// response = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
return response;

Same case if you want to remove duplicate elements from request collection before a service call. Example:

// hava 8
@HystrixCommand
List<User> batchMethod(List<String> ids){
// ids = [1, 2, 2, 3]
List<String> uniqueIds = ids.stream().distinct().collect(Collectors.toList());
// uniqueIds = [1, 2, 3]
List<User> users = restClient.getUsersByIds(uniqueIds);
// users = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
List<User> response = ids.stream().map(it -> users.stream()
                .filter(u -> u.getId().equals(it)).findFirst().get())
                .collect(Collectors.toList());
// response = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
return response;

To set collapser properties use @HystrixCollapser#collapserProperties

Read more about Hystrix request collapsing [here] (https://github.com/Netflix/Hystrix/wiki/How-it-Works#wiki-RequestCollapsing)

Collapser error processing Batch command can have a fallback method. Example:

@HystrixCollapser(batchMethod = "getUserByIdsWithFallback")
    public Future<User> getUserByIdWithFallback(String id) {
        return null;
    }
        
    @HystrixCommand(fallbackMethod = "getUserByIdsFallback")
    public List<User> getUserByIdsWithFallback(List<String> ids) {
        throw new RuntimeException("not found");
    }


    @HystrixCommand
    private List<User> getUserByIdsFallback(List<String> ids) {
        List<User> users = new ArrayList<User>();
        for (String id : ids) {
            users.add(new User(id, "name: " + id));
        }
        return users;
    }

#Development Status and Future Please create an issue if you need a feature or you detected some bugs. Thanks

Note: Javanica 1.4.+ is updated more frequently than 1.3.+ hence 1.4+ is more stable.

It's recommended to use Javanica 1.4.+

原文链接: hystrix-javanica & 推荐阅读

 

 类似资料: