SolrCloud, podobnie jak większość systemów rozproszonych podlega pewnym zasadom. Np. CAP mówi o tym, iż rozproszony system jest w stanie zapewnić dwie z trzech wymienionych funkcjonalności w tym samym czasie – dostępność, spójność, odporność na rozłączanie sieci. Oczywiście nie będziemy rozmawiać o podstawach systemów rozproszonych, ale skupimy się jak możemy kontrolować tolerancję zapisu i odczytu w SolrCloud.
Tolerancja zapisu
Tolerancja zapisu to dość skomplikowany temat. Po pierwsze, wraz z wprowadzeniem Solr 7.0 dostaliśmy różne rodzaje replik. Mamy repliki typu NRT, które zapisują dane do loga transakcyjnego, a indeksowanie odbywa się na każdej z replik. Mamy repliki typu TLOG, gdzie nowe dane zapisywane są do loga transakcyjnego, a samo indeksowanie nie następuje i występuje tylko binarna replikacja. W końcu repliki typu PULL, gdzie Solr nie korzysta z loga transakcyjnego replikując segmenty od lidera.
Nie będziemy się dzisiaj jednak zajmować dokładną analizą, jak działają poszczególne typy replik i skupimy się na domyślnym typie, czyli replikach NRT. Repliki te były z nami od początku SolrCloud i na szczęście są dalej 😉
W przypadku replik NRT procedura indeksowania danych działa następująco – na początku lider przyjmuje dane, zapisuje je w logu transakcyjnym i wysyła do swoich replik (zakładamy, że wszystkie są typu NRT). Każda z replik zapisuje dane w logu transakcyjnym. W tym momencie nasze dane są już bezpieczne i Solr może zwrócić potwierdzenie przyjęcia danych. Oczywiście, gdzieś w między czasie, w zależności od konfiguracji tworzony jest odwrócony indeks. Co się stanie kiedy nie wszystkie shardy będą dostępne? Indeksowanie nie powiedzie się. Aby to przetestować wystarczy, że uruchomimy dwie instancje Solr następującymi komendami:
$ bin/solr start -c
$ bin/solr start -z localhost:9983 -p 6983
A następnie stworzymy kolekcję:
$ bin/solr create_collection -c test_index -shards 2 -replicationFactor 1
Zabijemy jedną z instancji:
$ bin/solr stop -p 6983
I spróbujemy zaindeksować dane:
$ curl -XPOST -H 'Content-type:application/json' 'localhost:8983/solr/test_index/update' -d '{
"id" : 2,
"name" : "Test document"
}'
Oczywiście, tak jak mogliśmy się spodziewać Solr zwróci bład:
{
"responseHeader":{
"status":503,
"QTime":4011},
"error":{
"metadata":[
"error-class","org.apache.solr.common.SolrException",
"root-error-class","org.apache.solr.common.SolrException"],
"msg":"No registered leader was found after waiting for 4000ms , collection: test_index slice: shard2 saw state=DocCollection(test_index//collections/test_index/state.json/8)={\n \"pullReplicas\":\"0\",\n \"replicationFactor\":\"1\",\n \"shards\":{\n \"shard1\":{\n \"range\":\"80000000-ffffffff\",\n \"state\":\"active\",\n \"replicas\":{\"core_node3\":{\n \"core\":\"test_index_shard1_replica_n1\",\n \"base_url\":\"http://192.168.1.11:8983/solr\",\n \"node_name\":\"192.168.1.11:8983_solr\",\n \"state\":\"active\",\n \"type\":\"NRT\",\n \"force_set_state\":\"false\",\n \"leader\":\"true\"}}},\n \"shard2\":{\n \"range\":\"0-7fffffff\",\n \"state\":\"active\",\n \"replicas\":{\"core_node4\":{\n \"core\":\"test_index_shard2_replica_n2\",\n \"base_url\":\"http://192.168.1.11:6983/solr\",\n \"node_name\":\"192.168.1.11:6983_solr\",\n \"state\":\"down\",\n \"type\":\"NRT\",\n \"force_set_state\":\"false\",\n \"leader\":\"true\"}}}},\n \"router\":{\"name\":\"compositeId\"},\n \"maxShardsPerNode\":\"-1\",\n \"autoAddReplicas\":\"false\",\n \"nrtReplicas\":\"1\",\n \"tlogReplicas\":\"0\"} with live_nodes=[192.168.1.11:8983_solr]",
"code":503}}
W tym wypadku nie jesteśmy w stanie zrobić nic – nie chcemy, aby nasze dane wylądowały gdziekolwiek. Musimy czekać, aż Solr powróci do stanu używalności, albo sami go do niego doprowadzimy 😉
Co jednak w przypadku, kiedy mamy wiele replik i tylko niektóre z nich nie są dostępne? W tym wypadku zapis się powiedzie, a w najnowszych wersjach Solr poinformuje nas o stanie replikacji poprzez umieszczenie w odpowiedzi parametru rf, czyli replication factor.
Stwórzmy sobie zatem jeszcze jedną kolekcję, tym razem składającą się z jednego lidera i jego repliki:
$ bin/solr create_collection -c test_index_2 -shards 1 -replicationFactor 2
Jeżeli spróbujemy zaindeksować dane takim samym poleceniem jak wcześniej (oczywiście zmieniając nazwę kolekcji) odpowiedź Solr w wersji 7.6.0 będzie wyglądała następująco:
{
"responseHeader":{
"rf":2,
"status":0,
"QTime":316}}
Jak widać parametr rf ustawiony został na wartość 2, co mówi nam, że indeksowanie powiodło się zarówno na liderze, jak i jego replice. Jeżeli zatrzymalibyśmy instancję Solr działającą na porcie 6983 i spróbowali ponowić indeksowanie Solr poinformuje nas, iż tylko jedna replika danych została zapisana:
{
"responseHeader":{
"rf":1,
"status":0,
"QTime":4}}
We wcześniejszych wersjach Solr, aby otrzymać tą informację należało dodać parametr min_rf większy od 1 do żądania zawierającego indeksowanie, aby dostać informację, jaki poziom replikacji został osiągnięty przez Solr.
Tolerancja odczytu
W przypadku odczytu sprawy mają się trochę inaczej. Brak spójności danych, np. w przypadku braku jednego lub kliku shardów spowoduje, iż Solr zwróci błąd. Możemy to bardzo prosto pokazać – tworzymy dwie instancje Solr:
$ bin/solr start -c
$ bin/solr start -z localhost:9983 -p 6983
Następnie tworzymy prostą kolekcję:
$ bin/solr create_collection -c test -shards 2 -replicationFactor 1
A teraz zatrzymujemy jedną z instancji:
$ bin/solr stop -p 6983
I teraz wystarczy zadać proste zapytanie:
http://localhost:8983/solr/test/select?q=*:*
W odpowiedzi zamiast pustej listy dokumentów dostaniemy błąd:
{
"responseHeader":{
"status":503,
"QTime":6,
"params":{
"q":"*:*"}},
"error":{
"metadata":[
"error-class","org.apache.solr.common.SolrException",
"root-error-class","org.apache.solr.common.SolrException"],
"msg":"no servers hosting shard: shard2",
"code":503}}
Czasami jednak chcielibyśmy zaprezentować wyniki wyszukiwania pomimo braku części informacji. Nie jest to oczywiście idealna sytuacja, ale czasem lepiej pokazać mniej danych, niż nie pokazać nic, oczywiście jeżeli zdajemy sobie sprawę co robimy. W tym celu przychodzą nam z pomocą dwa parametry: shards.tolerant oraz shards.info. Oba powinny zostać ustawione na wartość true, czyli nasze zapytanie przyjmie następującą formę:
http://localhost:8983/solr/test/select?q=*:*&shards.tolerant=true&shards.info=true
Tym razem Solr nie zwróci już błędu, a nagłówek odpowiedzi będzie wyglądał następująco:
{
"responseHeader":{
"zkConnected":true,
"partialResults":true,
"status":0,
"QTime":45,
"params":{
"q":"*:*",
"shards.tolerant":"true",
"shards.info":"true"}},
"shards.info":{
"":{
"error":"org.apache.solr.common.SolrException: no servers hosting shard: ",
"trace":"org.apache.solr.common.SolrException: no servers hosting shard: \n\tat org.apache.solr.handler.component.HttpShardHandler.lambda$submit$0(HttpShardHandler.java:165)\n\tat java.util.concurrent.FutureTask.run(FutureTask.java:266)\n\tat java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\n\tat java.util.concurrent.FutureTask.run(FutureTask.java:266)\n\tat com.codahale.metrics.InstrumentedExecutorService$InstrumentedRunnable.run(InstrumentedExecutorService.java:176)\n\tat org.apache.solr.common.util.ExecutorUtil$MDCAwareThreadPoolExecutor.lambda$execute$0(ExecutorUtil.java:209)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat java.lang.Thread.run(Thread.java:748)\n",
"time":0},
"http://192.168.1.11:8983/solr/test_shard1_replica_n1/":{
"numFound":0,
"maxScore":0.0,
"shardAddress":"http://192.168.1.11:8983/solr/test_shard1_replica_n1/",
"time":18}},
"response":{"numFound":0,"start":0,"maxScore":0.0,"docs":[]
}}
Jak widać, działa to tak jakbyśmy chcieli. Nie dostaliśmy od Solr błędu, a odpowiedź. Dodatkowo widzimy, iż zwrócone są częściowe rezultaty (partialResults ustawione na true), co pozwala nam lub naszej aplikacji na stwierdzenie, że coś jest nie tak. Oprócz tego opcja shards.info=true pozwoliła Solr zwrócić informacje których shardów brakuje.