Wraz z publikacją wersji 8.7 Solr w nasze ręce dostaliśmy funkcjonalność circuit breaker. Ten wzorzec projektowy i funkcjonalność go implementująca pozwala na automatyczne odrzucenie dane żądania, kiedy spełnione są zdefiniowane kryteria. Na przykład, kiedy użycie pamięci jest większe, lub load jest zbyt wysoki. Spójrzmy zatem na to, co dostępne jest w Solr 8.7.
Nowa Funkcjonalność
Nowa funkcjonalność wprowadzona do Solr stara się dostarczyć możliwości przerwania wykonywania w chwili kiedy dana instancja Solr przekracza zdefiniowane limity. Na przykład, kiedy wykorzystanie pamięci JVM dochodzi do 75% możemy chcieć przestać akceptować kolejne żądania, aby zakończyć przetważanie obecnych.
W tym właśnie celu do Solr 8.7 została dodana funkcjonalność circuit breaker z następującymi implementacjami:
- Wykorzystanie pamięci JVM
- Użycie procesora
Czyli kiedy skorzystać z nowej funkcjonalności? Odpowiedź jest dość prosta – wtedy kiedy stawiamy stabilność instancji Solr ponad wydajność. Jeżeli chcemy większą stabilność – korzystamy z circuit breakerów, jak chcemy pełną wydajność – nie korzystamy z nich.
Konfiguracja
Konfiguracja circuit breaker powinna być umieszczona w sekcji circuitBreaker w pliku solrconfig.xml:
<circuitBreaker class="solr.CircuitBreakerManager" enabled="true">
...
</circuitBreaker>
Atrubut enabled kontroluje, czy funkcjonalność jest włączona, czy nie. W przypadku ustawienia jego wartości na true circuit breaker jest włączony, w przypadku ustawienia na false jest wyłączony.
W chwili pisania tego tekstu mogliśmy korzystać z następujących implementacji:
- Circuit breakera opartego o wykorzystanie procesora
- Circuit breakera opartego o wykorzystanie pamięci JVM
Zacznijmy od tego pierwszego. W przypadku zdefiniowania circuit breakera opartego o wykorzystanie procesora Solr sprawdza limit w oparciu o średnie wykorzystanie w ciągu ostatniej minuty. Jeżeli zdefiniowany limit zostanie przekroczony kolejne żądania nie będą przetwarzane. Aby zdefiniować taki circuit breaker do sekcji circuitBreaker w pliku solrconfig.xml dodajemy:
<str name="cpuEnabled">true</str>
<str name="cpuThreshold">75</str>
Powyższa konfiguracja mówi Solr, iż ma sprawdzać wykorzystanie procesora, a limit, który powinien powodować odrzucanie kolejnych żądań to 75% wykorzystania w trakcie ostatniej minuty.
Kolejna implementacja to ta oparta o średnie wykorzystanie pamięci JVM. W przypadku kiedy średnie zużycie będzie wyższe od zdefiniowanego limitu obliczanego na podstawie maksymalnej wielkości stosu kolejne żądania będą odrzucane. Aby zdefiniować taki circuit breaker do sekcji circuitBreaker w pliku solrconfig.xml dodajemy:
<str name="memEnabled">true</str>
<str name="memThreshold">80</str>
Powyższa konfiguracja mówi Solr, aby odrzucać kolejne żądania w chwili kiedy wykorzystanie JVM będzie równe lub większe od 80% maksymalnej wielkości stosu JVM. Czy w przypadku, kiedy Xmx dla procesu JVM ustawione byłoby na wartość 10G, a wykorzystanie wyniesie 8G lub więcej żądania będą odrzucane.
Warto pamiętać, że wartość memThreshold może przyjąć wartości pomiędzy 50, a 95.
Finalna konfiguracja zawierająca obie funkcjonalności wyglądałaby następująco:
<circuitBreaker class="solr.CircuitBreakerManager" enabled="true">
<str name="memEnabled">true</str>
<str name="memThreshold">80</str>
<str name="cpuEnabled">true</str>
<str name="cpuThreshold">75</str>
</circuitBreaker>
Działanie
Po stworzeniu kolekcji z naszą konfiguracją i w chwili kiedy jeden z limitów został przekroczony Solr odpowie następująco:
{
"responseHeader":{
"status":503,
"QTime":0,
"params":{
"json":"{\n\t\"query\": \"*:*\",\n\t\"facet\": {\n\t \"test\": {\n\t \"terms\": {\n\t \"field\": \"text\"\n\t }\n\t }\n\t}\n}"}},
"status":"FAILURE",
"error":{
"metadata":[
"error-class","org.apache.solr.common.SolrException",
"root-error-class","org.apache.solr.common.SolrException"],
"msg":"Circuit Breakers tripped Memory Circuit Breaker triggered as JVM heap usage values are greater than allocated threshold.Seen JVM heap memory usage 503369312 and allocated threshold 429496729\n",
"code":503}}
Kod odpowiedzi w tym przypadku to 503 wraz z informacją dlaczego Solr odrzucił dane żądanie. Sugerowanym działaniem w takim wypadku jest odczekanie przed kolejną próbą tego samego żądania. Dodatkowo dobrym pomysłem jest zwiększanie czasu pomiędzy kolejnymi powtórzeniami, tak aby nie wysyłać zbyt dużej liczby żądań do Solr. Sama funkcjonalność circuit breaker nie wpływa znacząco na zasoby Solr, aczkolwiek wiele żądań, które będą wysłane w tym samym czasie może spowodować jeszcze większe wykorzystanie zasobów.
Minusy
Posiadanie konfiguracji circuit breaker w naszym pliku solrconfig.xml nie oznacza, że nasza instancja Solr jest całkowicie chroniona przed problemami takimi, jak OutOfMemory. Pokaże na to przykładzie wykorzystującym Solr 8.7 oraz circuit breaker działający na podstawie wykorzystania pamięci JVM. Cały przykład dostępny jest na Githubie.
Zaczynamy od domyślnej instancji Solr 8.7, działającym jako klaster SolrCloud i posiadająym następującą konfiguracje circuitBreaker:
<circuitBreaker class="solr.CircuitBreakerManager" enabled="true">
<str name="memEnabled">true</str>
<str name="memThreshold">80</str>
<str name="cpuEnabled">true</str>
<str name="cpuThreshold">75</str>
</circuitBreaker>
Następnie stworzyłem kolekcję circuit i zaindeksowałem milion dokumentów korzystając z prostego skryptu w języku Python. Struktura danych była bardzo prosta – składała się z dwóch pól:
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" docValues="true" />
<field name="text" type="text_ws" indexed="true" stored="true" multiValued="false" />
Sam skrypt stara się, aby w polu text było jak najwięcej unikalnych termów.
Następnie zadałem zapytanie, które wygląda jak to poniżej:
curl -XGET 'localhost:8983/solr/circuit/select?q=*:*&rows=0'
Odpowiedź od Solr była taka, jakiej byśmy się spodziewali:
{
"responseHeader":{
"zkConnected":true,
"status":0,
"QTime":60,
"params":{
"q":"*:*",
"rows":"0"}},
"response":{"numFound":1000000,"start":0,"numFoundExact":true,"docs":[]
}}
Drugie zapytanie, które zadałem wyglądało następująco:
curl 'http://localhost:8983/solr/circuit/query' -d '{
"query": "*:*",
"limit": 0,
"facet": {
"test": {
"terms": {
"field": "text"
}
}
}
}'
A odpowiedź Solr była już inna:
{
"responseHeader":{
"zkConnected":true,
"status":500,
"QTime":6893,
"params":{
"json":"{\n \"query\": \"*:*\",\n \"limit\": 0,\n \"facet\": {\n \"test\": {\n \"terms\": {\n\t\"field\": \"text\"\n }\n }\n }\n}"}},
"response":{"numFound":1000000,"start":0,"numFoundExact":true,"docs":[]
},
"error":{
"metadata":[
"error-class","org.apache.solr.common.SolrException",
"root-error-class","java.lang.OutOfMemoryError"],
"msg":"Exception occured during uninverting text",
"trace":"org.apache.solr.common.SolrException: Exception occured during uninverting text\n\tat org.apache.solr.search.facet.UnInvertedField.rethrowAsSolrException(UnInvertedField.java:681)\n\tat org.apache.solr.search.facet.UnInvertedField.getUnInvertedField(UnInvertedField.java:621)\n\tat org.apache.solr.search.facet.FacetFieldProcessorByArrayUIF.findStartAndEndOrds(FacetFieldProcessorByArrayUIF.java:43)\n\tat org.apache.solr.search.facet.FacetFieldProcessorByArray.calcFacets(FacetFieldProcessorByArray.java:116)\n\tat org.apache.solr.search.facet.FacetFieldProcessorByArray.process(FacetFieldProcessorByArray.java:94)\n\tat org.apache.solr.search.facet.FacetRequest.process(FacetRequest.java:454)\n\tat org.apache.solr.search.facet.FacetProcessor.processSubs(FacetProcessor.java:477)\n\tat org.apache.solr.search.facet.FacetProcessor.fillBucket(FacetProcessor.java:433)\n\tat org.apache.solr.search.facet.FacetQueryProcessor.process(FacetQuery.java:65)\n\tat org.apache.solr.search.facet.FacetRequest.process(FacetRequest.java:454)\n\tat org.apache.solr.search.facet.FacetModule.process(FacetModule.java:150)\n\tat org.apache.solr.handler.component.SearchHandler.handleRequestBody(SearchHandler.java:360)\n\tat org.apache.solr.handler.RequestHandlerBase.handleRequest(RequestHandlerBase.java:214)\n\tat org.apache.solr.core.SolrCore.execute(SolrCore.java:2627)\n\tat org.apache.solr.servlet.HttpSolrCall.execute(HttpSolrCall.java:795)\n\tat org.apache.solr.servlet.HttpSolrCall.call(HttpSolrCall.java:568)\n\tat org.apache.solr.servlet.SolrDispatchFilter.doFilter(SolrDispatchFilter.java:415)\n\tat org.apache.solr.servlet.SolrDispatchFilter.doFilter(SolrDispatchFilter.java:345)\n\tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)\n\tat org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)\n\tat org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)\n\tat org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)\n\tat org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1610)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)\n\tat org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1300)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)\n\tat org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)\n\tat org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1580)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)\n\tat org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1215)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)\n\tat org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:221)\n\tat org.eclipse.jetty.server.handler.InetAccessHandler.handle(InetAccessHandler.java:177)\n\tat org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)\n\tat org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)\n\tat org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)\n\tat org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)\n\tat org.eclipse.jetty.server.Server.handle(Server.java:500)\n\tat org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)\n\tat org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)\n\tat org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)\n\tat org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:273)\n\tat org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)\n\tat org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)\n\tat org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)\n\tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)\n\tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)\n\tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)\n\tat org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:135)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)\n\tat java.base/java.lang.Thread.run(Thread.java:832)\nCaused by: java.lang.OutOfMemoryError: Java heap space\n\tat org.apache.solr.search.facet.UnInvertedField.visitTerm(UnInvertedField.java:136)\n\tat org.apache.solr.uninverting.DocTermOrds.uninvert(DocTermOrds.java:350)\n\tat org.apache.solr.search.facet.UnInvertedField.<init>(UnInvertedField.java:205)\n\tat org.apache.solr.search.facet.UnInvertedField.lambda$getUnInvertedField$1(UnInvertedField.java:613)\n\tat org.apache.solr.search.facet.UnInvertedField$$Lambda$658/0x000000080116cc40.apply(Unknown Source)\n\tat org.apache.solr.util.ConcurrentLRUCache.lambda$computeIfAbsent$1(ConcurrentLRUCache.java:227)\n\tat org.apache.solr.util.ConcurrentLRUCache$$Lambda$659/0x000000080116c040.apply(Unknown Source)\n\tat java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)\n\tat org.apache.solr.util.ConcurrentLRUCache.computeIfAbsent(ConcurrentLRUCache.java:226)\n\tat org.apache.solr.search.FastLRUCache.computeIfAbsent(FastLRUCache.java:258)\n\tat org.apache.solr.search.facet.UnInvertedField.getUnInvertedField(UnInvertedField.java:610)\n\tat org.apache.solr.search.facet.FacetFieldProcessorByArrayUIF.findStartAndEndOrds(FacetFieldProcessorByArrayUIF.java:43)\n\tat org.apache.solr.search.facet.FacetFieldProcessorByArray.calcFacets(FacetFieldProcessorByArray.java:116)\n\tat org.apache.solr.search.facet.FacetFieldProcessorByArray.process(FacetFieldProcessorByArray.java:94)\n\tat org.apache.solr.search.facet.FacetRequest.process(FacetRequest.java:454)\n\tat org.apache.solr.search.facet.FacetProcessor.processSubs(FacetProcessor.java:477)\n\tat org.apache.solr.search.facet.FacetProcessor.fillBucket(FacetProcessor.java:433)\n\tat org.apache.solr.search.facet.FacetQueryProcessor.process(FacetQuery.java:65)\n\tat org.apache.solr.search.facet.FacetRequest.process(FacetRequest.java:454)\n\tat org.apache.solr.search.facet.FacetModule.process(FacetModule.java:150)\n\tat org.apache.solr.handler.component.SearchHandler.handleRequestBody(SearchHandler.java:360)\n\tat org.apache.solr.handler.RequestHandlerBase.handleRequest(RequestHandlerBase.java:214)\n\tat org.apache.solr.core.SolrCore.execute(SolrCore.java:2627)\n\tat org.apache.solr.servlet.HttpSolrCall.execute(HttpSolrCall.java:795)\n\tat org.apache.solr.servlet.HttpSolrCall.call(HttpSolrCall.java:568)\n\tat org.apache.solr.servlet.SolrDispatchFilter.doFilter(SolrDispatchFilter.java:415)\n\tat org.apache.solr.servlet.SolrDispatchFilter.doFilter(SolrDispatchFilter.java:345)\n\tat org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)\n\tat org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)\n\tat org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)\n\tat org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)\n\tat org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)\n",
"code":500}}
Jak widać powyżej Solr odpowiedział błędem 500, a powodem tego błędu był wyjątek java.lang.OutOfMemoryError. Oczywiście jest to przykład dość ekstremalny, ale należy pamiętać, iż tak może się zdarzyć. W chwili kiedy sprawdzane było wykorzystanie pamięci nie sięgało one 80% i wyglądało następująco:
I dlatego też Solr powzolił na wykonanie zapytania.
Podsumowanie
Pomimo tego, że nie wszystko jest idealne, a Solr nie dokonuje żadnych obliczeń i przewidywania wykorzystania pamięci przez dane żądanie uważam iż funkcjonalność circuit breaker to krok w dobrym kierunku. Mam także nadzieję, iż w niedalekiej przyszłości dostaniemy kolejne implementacje. Na przykład takie dedykowane tylko niektórym funkcjonalnościom – zapytanion, indeksowaniu, czy facetingowi. A jaka jest wasza opinia na ten temat?