Въведение в Hystrix

1. Общ преглед

Типичната разпределена система се състои от много услуги, които си сътрудничат.

Тези услуги са склонни към неуспешни или забавени отговори. Ако услугата се провали, това може да повлияе на други услуги, засягащи производителността и евентуално да направи други части на приложението недостъпни или в най-лошия случай да свали цялото приложение.

Разбира се, има налични решения, които помагат приложенията да станат устойчиви и устойчиви на грешки - една такава рамка е Hystrix.

Рамковата библиотека Hystrix помага да се контролира взаимодействието между услугите, като осигурява толерантност към грешки и толерантност към латентността. Той подобрява цялостната устойчивост на системата, като изолира неуспешните услуги и спира каскадния ефект на отказите.

В тази поредица от публикации ще започнем, като разгледаме как Hystrix идва на помощ, когато услуга или система се провали и какво може да постигне Hystrix при тези обстоятелства.

2. Прост пример

Начинът, по който Hystrix осигурява толерантност към грешки и латентност, е да изолира и приключи повикванията към отдалечени услуги.

В този прост пример обгръщаме повикване в метода run () на HystrixCommand:

class CommandHelloWorld extends HystrixCommand { private String name; CommandHelloWorld(String name) { super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.name = name; } @Override protected String run() { return "Hello " + name + "!"; } }

и ние изпълняваме повикването, както следва:

@Test public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){ assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!")); }

3. Настройка на Maven

За да използваме Hystrix в проекти на Maven, трябва да имаме зависимост hystrix-core и rxjava-core от Netflix в проекта pom.xml :

 com.netflix.hystrix hystrix-core 1.5.4  

Най-новата версия винаги може да бъде намерена тук.

 com.netflix.rxjava rxjava-core 0.20.7 

Най-новата версия на тази библиотека винаги може да бъде намерена тук.

4. Настройка на отдалечена услуга

Нека започнем със симулиране на пример от реалния свят.

В примера по-долу класът RemoteServiceTestSimulator представлява услуга на отдалечен сървър. Той има метод, който отговаря със съобщение след дадения период от време. Можем да си представим, че това изчакване е симулация на отнемащ време процес в отдалечената система, водещ до забавен отговор на повикващата услуга:

class RemoteServiceTestSimulator { private long wait; RemoteServiceTestSimulator(long wait) throws InterruptedException { this.wait = wait; } String execute() throws InterruptedException { Thread.sleep(wait); return "Success"; } }

И ето нашият примерен клиент, който извиква RemoteServiceTestSimulator .

Извикването към услугата е изолирано и обвито в метода run () на HystrixCommand. Това е опаковането, което осигурява устойчивостта, която засегнахме по-горе:

class RemoteServiceTestCommand extends HystrixCommand { private RemoteServiceTestSimulator remoteService; RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) { super(config); this.remoteService = remoteService; } @Override protected String run() throws Exception { return remoteService.execute(); } }

Извикването се изпълнява чрез извикване на метода execute () на екземпляр на обекта RemoteServiceTestCommand .

Следният тест показва как се прави това:

@Test public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2")); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(), equalTo("Success")); }

Досега видяхме как да обгръщаме повиквания за отдалечени услуги в обекта HystrixCommand . В раздела по-долу нека разгледаме как да се справим със ситуация, когато отдалечената услуга започне да се влошава.

5. Работа с дистанционно обслужване и защитно програмиране

5.1. Защитно програмиране с изчакване

Обща практика за програмиране е да задавате таймаути за разговори до отдалечени услуги.

Нека започнем, като разгледаме как да зададете таймаут на HystrixCommand и как това помага чрез късо съединение:

@Test public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4")); HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter(); commandProperties.withExecutionTimeoutInMilliseconds(10_000); config.andCommandPropertiesDefaults(commandProperties); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); }

В горния тест забавяме отговора на услугата, като задаваме времето за изчакване на 500 ms. Ние също така определяме времето за изпълнение на HystrixCommand на 10 000 ms, като по този начин даваме достатъчно време за отговор на отдалечената услуга.

Сега нека видим какво се случва, когато времето за изчакване на изпълнението е по-малко от извикването за изчакване на услугата:

@Test(expected = HystrixRuntimeException.class) public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5")); HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter(); commandProperties.withExecutionTimeoutInMilliseconds(5_000); config.andCommandPropertiesDefaults(commandProperties); new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute(); }

Забележете как сме свалили лентата и сме задали времето за изпълнение на 5000 ms.

Очакваме услугата да реагира в рамките на 5000 ms, докато сме настроили услугата да реагира след 15 000 ms. Ако забележите, когато изпълнявате теста, тестът ще излезе след 5000 ms, вместо да чака 15 000 ms и ще хвърли HystrixRuntimeException.

Това показва как Hystrix не чака по-дълго от конфигурираното време за изчакване за отговор. Това помага системата да бъде защитена от Hystrix по-отзивчива.

В секциите по-долу ще разгледаме задаването на размер на пула от нишки, който предотвратява изчерпването на нишките, и ще обсъдим неговата полза.

5.2. Дефанзивно програмиране с ограничен брой нишки

Задаването на таймаути за повикване за обслужване не решава всички проблеми, свързани с отдалечените услуги.

Когато отдалечена услуга започне да реагира бавно, типично приложение ще продължи да извиква тази отдалечена услуга.

Приложението не знае дали отдалечената услуга е здрава или не и новите нишки се раждат всеки път, когато влезе заявка. Това ще доведе до използване на нишки на вече борещ се сървър.

We don't want this to happen as we need these threads for other remote calls or processes running on our server and we also want to avoid CPU utilization spiking up.

Let's see how to set the thread pool size in HystrixCommand:

@Test public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted _thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool")); HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter(); commandProperties.withExecutionTimeoutInMilliseconds(10_000); config.andCommandPropertiesDefaults(commandProperties); config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withMaxQueueSize(10) .withCoreSize(3) .withQueueSizeRejectionThreshold(10)); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); }

In the above test, we are setting the maximum queue size, the core queue size and the queue rejection size. Hystrix will start rejecting the requests when the maximum number of threads have reached 10 and the task queue has reached a size of 10.

The core size is the number of threads that always stay alive in the thread pool.

5.3. Defensive Programming With Short Circuit Breaker Pattern

However, there is still an improvement that we can make to remote service calls.

Let's consider the case that the remote service has started failing.

We don't want to keep firing off requests at it and waste resources. We would ideally want to stop making requests for a certain amount of time in order to give the service time to recover before then resuming requests. This is what is called the Short Circuit Breaker pattern.

Let's see how Hystrix implements this pattern:

@Test public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker")); HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter(); properties.withExecutionTimeoutInMilliseconds(1000); properties.withCircuitBreakerSleepWindowInMilliseconds(4000); properties.withExecutionIsolationStrategy (HystrixCommandProperties.ExecutionIsolationStrategy.THREAD); properties.withCircuitBreakerEnabled(true); properties.withCircuitBreakerRequestVolumeThreshold(1); config.andCommandPropertiesDefaults(properties); config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withMaxQueueSize(1) .withCoreSize(1) .withQueueSizeRejectionThreshold(1)); assertThat(this.invokeRemoteService(config, 10_000), equalTo(null)); assertThat(this.invokeRemoteService(config, 10_000), equalTo(null)); assertThat(this.invokeRemoteService(config, 10_000), equalTo(null)); Thread.sleep(5000); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); }
public String invokeRemoteService(HystrixCommand.Setter config, int timeout) throws InterruptedException { String response = null; try { response = new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(timeout)).execute(); } catch (HystrixRuntimeException ex) { System.out.println("ex = " + ex); } return response; }

In the above test we have set different circuit breaker properties. The most important ones are:

  • The CircuitBreakerSleepWindow which is set to 4,000 ms. This configures the circuit breaker window and defines the time interval after which the request to the remote service will be resumed
  • The CircuitBreakerRequestVolumeThreshold which is set to 1 and defines the minimum number of requests needed before the failure rate will be considered

With the above settings in place, our HystrixCommand will now trip open after two failed request. The third request will not even hit the remote service even though we have set the service delay to be 500 ms, Hystrix will short circuit and our method will return null as the response.

We will subsequently add a Thread.sleep(5000) in order to cross the limit of the sleep window that we have set. This will cause Hystrix to close the circuit and the subsequent requests will flow through successfully.

6. Conclusion

В обобщение Hystrix е проектиран да:

  1. Осигурява защита и контрол върху грешки и латентност от услуги, които обикновено се осъществяват по мрежата
  2. Спрете каскадирането на откази в резултат на отпадане на някои от услугите
  3. Неуспешно бързо и бързо възстановяване
  4. Деградирайте грациозно, когато е възможно
  5. Мониторинг и предупреждение в реално време на командния център за повреди

В следващия пост ще видим как да комбинираме предимствата на Hystrix с Spring структурата.

Пълният код на проекта и всички примери могат да бъдат намерени в проекта github.