mirror of https://github.com/apache/lucene.git
SOLR-6832: Queries be served locally rather than being forwarded to another replica
git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1659748 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
3dc8f4a437
commit
429588097c
|
@ -111,6 +111,9 @@ New Features
|
||||||
* SOLR-7019: Support changing field key when using interval faceting.
|
* SOLR-7019: Support changing field key when using interval faceting.
|
||||||
(Tomás Fernández Löbbe)
|
(Tomás Fernández Löbbe)
|
||||||
|
|
||||||
|
* SOLR-6832: Queries be served locally rather than being forwarded to another replica.
|
||||||
|
(Sachin Goyal, Timothy Potter)
|
||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
@ -40,13 +40,18 @@ import org.apache.solr.common.params.SolrParams;
|
||||||
import org.apache.solr.common.util.NamedList;
|
import org.apache.solr.common.util.NamedList;
|
||||||
import org.apache.solr.common.util.StrUtils;
|
import org.apache.solr.common.util.StrUtils;
|
||||||
import org.apache.solr.core.CoreDescriptor;
|
import org.apache.solr.core.CoreDescriptor;
|
||||||
|
import org.apache.solr.core.SolrCore;
|
||||||
import org.apache.solr.request.SolrQueryRequest;
|
import org.apache.solr.request.SolrQueryRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.ListIterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
@ -63,6 +68,7 @@ public class HttpShardHandler extends ShardHandler {
|
||||||
private Map<String,List<String>> shardToURLs;
|
private Map<String,List<String>> shardToURLs;
|
||||||
private HttpClient httpClient;
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
protected static Logger log = LoggerFactory.getLogger(HttpShardHandler.class);
|
||||||
|
|
||||||
public HttpShardHandler(HttpShardHandlerFactory httpShardHandlerFactory, HttpClient httpClient) {
|
public HttpShardHandler(HttpShardHandlerFactory httpShardHandlerFactory, HttpClient httpClient) {
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
|
@ -101,20 +107,69 @@ public class HttpShardHandler extends ShardHandler {
|
||||||
|
|
||||||
// Not thread safe... don't use in Callable.
|
// Not thread safe... don't use in Callable.
|
||||||
// Don't modify the returned URL list.
|
// Don't modify the returned URL list.
|
||||||
private List<String> getURLs(String shard) {
|
private List<String> getURLs(ShardRequest sreq, String shard) {
|
||||||
List<String> urls = shardToURLs.get(shard);
|
List<String> urls = shardToURLs.get(shard);
|
||||||
if (urls == null) {
|
if (urls == null) {
|
||||||
urls = httpShardHandlerFactory.makeURLList(shard);
|
urls = httpShardHandlerFactory.makeURLList(shard);
|
||||||
|
preferCurrentHostForDistributedReq(sreq, urls);
|
||||||
shardToURLs.put(shard, urls);
|
shardToURLs.put(shard, urls);
|
||||||
}
|
}
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A distributed request is made via {@link LBHttpSolrClient} to the first live server in the URL list.
|
||||||
|
* This means it is just as likely to choose current host as any of the other hosts.
|
||||||
|
* This function makes sure that the cores of current host are always put first in the URL list.
|
||||||
|
* If all nodes prefer local-cores then a bad/heavily-loaded node will receive less requests from healthy nodes.
|
||||||
|
* This will help prevent a distributed deadlock or timeouts in all the healthy nodes due to one bad node.
|
||||||
|
*/
|
||||||
|
private void preferCurrentHostForDistributedReq(final ShardRequest sreq, final List<String> urls) {
|
||||||
|
if (sreq == null || sreq.rb == null || sreq.rb.req == null || urls == null || urls.size() <= 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SolrQueryRequest req = sreq.rb.req;
|
||||||
|
|
||||||
|
// determine if we should apply the local preference
|
||||||
|
if (!req.getOriginalParams().getBool(CommonParams.PREFER_LOCAL_SHARDS, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get this node's base URL from ZK
|
||||||
|
SolrCore core = req.getCore();
|
||||||
|
ZkController zkController = (core != null) ? core.getCoreDescriptor().getCoreContainer().getZkController() : null;
|
||||||
|
String currentHostAddress = (zkController != null) ? zkController.getBaseUrl() : null;
|
||||||
|
if (currentHostAddress == null) {
|
||||||
|
log.debug("Couldn't determine current host address to prefer local shards " +
|
||||||
|
"because either core is null? {} or there is no ZkController? {}",
|
||||||
|
Boolean.valueOf(core == null), Boolean.valueOf(zkController == null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled())
|
||||||
|
log.debug("Trying to prefer local shard on {} among the urls: {}",
|
||||||
|
currentHostAddress, Arrays.toString(urls.toArray()));
|
||||||
|
|
||||||
|
ListIterator<String> itr = urls.listIterator();
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
String url = itr.next();
|
||||||
|
if (url.startsWith(currentHostAddress)) {
|
||||||
|
// move current URL to the fore-front
|
||||||
|
itr.remove();
|
||||||
|
urls.add(0, url);
|
||||||
|
|
||||||
|
if (log.isDebugEnabled())
|
||||||
|
log.debug("Applied local shard preference for urls: {}",
|
||||||
|
Arrays.toString(urls.toArray()));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void submit(final ShardRequest sreq, final String shard, final ModifiableSolrParams params) {
|
public void submit(final ShardRequest sreq, final String shard, final ModifiableSolrParams params) {
|
||||||
// do this outside of the callable for thread safety reasons
|
// do this outside of the callable for thread safety reasons
|
||||||
final List<String> urls = getURLs(shard);
|
final List<String> urls = getURLs(sreq, shard);
|
||||||
|
|
||||||
Callable<ShardResponse> task = new Callable<ShardResponse>() {
|
Callable<ShardResponse> task = new Callable<ShardResponse>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -147,6 +147,7 @@ public class ResponseBuilder
|
||||||
|
|
||||||
public void addRequest(SearchComponent me, ShardRequest sreq) {
|
public void addRequest(SearchComponent me, ShardRequest sreq) {
|
||||||
outgoing.add(sreq);
|
outgoing.add(sreq);
|
||||||
|
sreq.rb = this;
|
||||||
if ((sreq.purpose & ShardRequest.PURPOSE_PRIVATE) == 0) {
|
if ((sreq.purpose & ShardRequest.PURPOSE_PRIVATE) == 0) {
|
||||||
// if this isn't a private request, let other components modify it.
|
// if this isn't a private request, let other components modify it.
|
||||||
for (SearchComponent component : components) {
|
for (SearchComponent component : components) {
|
||||||
|
|
|
@ -49,6 +49,7 @@ public class ShardRequest {
|
||||||
|
|
||||||
public ModifiableSolrParams params;
|
public ModifiableSolrParams params;
|
||||||
|
|
||||||
|
public ResponseBuilder rb;
|
||||||
|
|
||||||
/** list of responses... filled out by framework */
|
/** list of responses... filled out by framework */
|
||||||
public List<ShardResponse> responses = new ArrayList<>();
|
public List<ShardResponse> responses = new ArrayList<>();
|
||||||
|
|
|
@ -805,6 +805,21 @@
|
||||||
<lst name="defaults">
|
<lst name="defaults">
|
||||||
<str name="echoParams">explicit</str>
|
<str name="echoParams">explicit</str>
|
||||||
<int name="rows">10</int>
|
<int name="rows">10</int>
|
||||||
|
<!-- Controls the distribution of a query to shards other than itself.
|
||||||
|
Consider making 'preferLocalShards' true when:
|
||||||
|
1) maxShardsPerNode > 1
|
||||||
|
2) Number of shards > 1
|
||||||
|
3) CloudSolrClient or LbHttpSolrServer is used by clients.
|
||||||
|
Without this option, every core broadcasts the distributed query to
|
||||||
|
a replica of each shard where the replicas are chosen randomly.
|
||||||
|
This option directs the cores to prefer cores hosted locally, thus
|
||||||
|
preventing network delays between machines.
|
||||||
|
This behavior also immunizes a bad/slow machine from slowing down all
|
||||||
|
the good machines (if those good machines were querying this bad one).
|
||||||
|
|
||||||
|
Specify this option=false for clients connecting through HttpSolrServer
|
||||||
|
-->
|
||||||
|
<bool name="preferLocalShards">false</bool>
|
||||||
</lst>
|
</lst>
|
||||||
<!-- In addition to defaults, "appends" params can be specified
|
<!-- In addition to defaults, "appends" params can be specified
|
||||||
to identify values which should be appended to the list of
|
to identify values which should be appended to the list of
|
||||||
|
|
|
@ -220,5 +220,9 @@ public interface CommonParams {
|
||||||
*/
|
*/
|
||||||
public static final String REQUEST_PURPOSE = "requestPurpose";
|
public static final String REQUEST_PURPOSE = "requestPurpose";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When querying a node, prefer local node's cores for distributed queries.
|
||||||
|
*/
|
||||||
|
public static final String PREFER_LOCAL_SHARDS = "preferLocalShards";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,12 @@ package org.apache.solr.client.solrj.impl;
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import static org.apache.solr.cloud.OverseerCollectionProcessor.CREATE_NODE_SET;
|
||||||
|
import static org.apache.solr.cloud.OverseerCollectionProcessor.NUM_SLICES;
|
||||||
|
import static org.apache.solr.common.cloud.ZkNodeProps.makeMap;
|
||||||
|
import static org.apache.solr.common.cloud.ZkStateReader.MAX_SHARDS_PER_NODE;
|
||||||
|
import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
@ -44,6 +50,7 @@ import org.apache.solr.common.params.CommonParams;
|
||||||
import org.apache.solr.common.params.ModifiableSolrParams;
|
import org.apache.solr.common.params.ModifiableSolrParams;
|
||||||
import org.apache.solr.common.params.ShardParams;
|
import org.apache.solr.common.params.ShardParams;
|
||||||
import org.apache.solr.common.util.NamedList;
|
import org.apache.solr.common.util.NamedList;
|
||||||
|
import org.apache.solr.common.util.SimpleOrderedMap;
|
||||||
import org.apache.zookeeper.KeeperException;
|
import org.apache.zookeeper.KeeperException;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
@ -53,7 +60,11 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -115,6 +126,7 @@ public class CloudSolrClientTest extends AbstractFullDistribZkTestBase {
|
||||||
stateVersionParamTest();
|
stateVersionParamTest();
|
||||||
customHttpClientTest();
|
customHttpClientTest();
|
||||||
testOverwriteOption();
|
testOverwriteOption();
|
||||||
|
preferLocalShardsTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testOverwriteOption() throws Exception, SolrServerException,
|
private void testOverwriteOption() throws Exception, SolrServerException,
|
||||||
|
@ -349,6 +361,117 @@ public class CloudSolrClientTest extends AbstractFullDistribZkTestBase {
|
||||||
cloudClient.close();
|
cloudClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if the specification of 'preferLocalShards' in the query-params
|
||||||
|
* limits the distributed query to locally hosted shards only
|
||||||
|
*/
|
||||||
|
private void preferLocalShardsTest() throws Exception {
|
||||||
|
|
||||||
|
String collectionName = "localShardsTestColl";
|
||||||
|
|
||||||
|
int liveNodes = getCommonCloudSolrClient()
|
||||||
|
.getZkStateReader().getClusterState().getLiveNodes().size();
|
||||||
|
|
||||||
|
// For preferLocalShards to succeed in a test, every shard should have
|
||||||
|
// all its cores on the same node.
|
||||||
|
// Hence the below configuration for our collection
|
||||||
|
Map<String, Object> props = makeMap(
|
||||||
|
REPLICATION_FACTOR, liveNodes,
|
||||||
|
MAX_SHARDS_PER_NODE, liveNodes,
|
||||||
|
NUM_SLICES, liveNodes);
|
||||||
|
Map<String,List<Integer>> collectionInfos = new HashMap<String,List<Integer>>();
|
||||||
|
createCollection(collectionInfos, collectionName, props, controlClientCloud);
|
||||||
|
waitForRecoveriesToFinish(collectionName, false);
|
||||||
|
|
||||||
|
CloudSolrClient cloudClient = createCloudClient(collectionName);
|
||||||
|
assertNotNull(cloudClient);
|
||||||
|
handle.clear();
|
||||||
|
handle.put("timestamp", SKIPVAL);
|
||||||
|
waitForThingsToLevelOut(30);
|
||||||
|
|
||||||
|
// Remove any documents from previous test (if any)
|
||||||
|
controlClient.deleteByQuery("*:*");
|
||||||
|
cloudClient.deleteByQuery("*:*");
|
||||||
|
controlClient.commit();
|
||||||
|
cloudClient.commit();
|
||||||
|
|
||||||
|
// Add some new documents
|
||||||
|
SolrInputDocument doc1 = new SolrInputDocument();
|
||||||
|
doc1.addField(id, "0");
|
||||||
|
doc1.addField("a_t", "hello1");
|
||||||
|
SolrInputDocument doc2 = new SolrInputDocument();
|
||||||
|
doc2.addField(id, "2");
|
||||||
|
doc2.addField("a_t", "hello2");
|
||||||
|
SolrInputDocument doc3 = new SolrInputDocument();
|
||||||
|
doc3.addField(id, "3");
|
||||||
|
doc3.addField("a_t", "hello2");
|
||||||
|
|
||||||
|
UpdateRequest request = new UpdateRequest();
|
||||||
|
request.add(doc1);
|
||||||
|
request.add(doc2);
|
||||||
|
request.add(doc3);
|
||||||
|
request.setAction(AbstractUpdateRequest.ACTION.COMMIT, false, false);
|
||||||
|
|
||||||
|
// Run the actual test for 'preferLocalShards'
|
||||||
|
queryWithPreferLocalShards(cloudClient, true, collectionName);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
controlClient.deleteByQuery("*:*");
|
||||||
|
cloudClient.deleteByQuery("*:*");
|
||||||
|
controlClient.commit();
|
||||||
|
cloudClient.commit();
|
||||||
|
cloudClient.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryWithPreferLocalShards(CloudSolrClient cloudClient,
|
||||||
|
boolean preferLocalShards,
|
||||||
|
String collectionName)
|
||||||
|
throws Exception
|
||||||
|
{
|
||||||
|
SolrQuery qRequest = new SolrQuery();
|
||||||
|
qRequest.setQuery("*:*");
|
||||||
|
|
||||||
|
ModifiableSolrParams qParams = new ModifiableSolrParams();
|
||||||
|
qParams.add("preferLocalShards", Boolean.toString(preferLocalShards));
|
||||||
|
qParams.add("shards.info", "true");
|
||||||
|
qRequest.add(qParams);
|
||||||
|
|
||||||
|
// CloudSolrClient sends the request to some node.
|
||||||
|
// And since all the nodes are hosting cores from all shards, the
|
||||||
|
// distributed query formed by this node will select cores from the
|
||||||
|
// local shards only
|
||||||
|
QueryResponse qResponse = cloudClient.query (qRequest);
|
||||||
|
|
||||||
|
Object shardsInfo = qResponse.getResponse().get("shards.info");
|
||||||
|
assertNotNull("Unable to obtain shards.info", shardsInfo);
|
||||||
|
|
||||||
|
// Iterate over shards-info and check what cores responded
|
||||||
|
SimpleOrderedMap<?> shardsInfoMap = (SimpleOrderedMap<?>)shardsInfo;
|
||||||
|
Iterator<Map.Entry<String, ?>> itr = shardsInfoMap.asMap(100).entrySet().iterator();
|
||||||
|
List<String> shardAddresses = new ArrayList<String>();
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
Map.Entry<String, ?> e = itr.next();
|
||||||
|
assertTrue("Did not find map-type value in shards.info", e.getValue() instanceof Map);
|
||||||
|
String shardAddress = (String)((Map)e.getValue()).get("shardAddress");
|
||||||
|
assertNotNull("shards.info did not return 'shardAddress' parameter", shardAddress);
|
||||||
|
shardAddresses.add(shardAddress);
|
||||||
|
}
|
||||||
|
log.info("Shards giving the response: " + Arrays.toString(shardAddresses.toArray()));
|
||||||
|
|
||||||
|
// Make sure the distributed queries were directed to a single node only
|
||||||
|
if (preferLocalShards) {
|
||||||
|
Set<Integer> ports = new HashSet<Integer>();
|
||||||
|
for (String shardAddr: shardAddresses) {
|
||||||
|
URL url = new URL (shardAddr);
|
||||||
|
ports.add(url.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This assertion would hold true as long as every shard has a core on each node
|
||||||
|
assertTrue ("Response was not received from shards on a single node",
|
||||||
|
shardAddresses.size() > 1 && ports.size()==1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Long getNumRequests(String baseUrl, String collectionName) throws
|
private Long getNumRequests(String baseUrl, String collectionName) throws
|
||||||
SolrServerException, IOException {
|
SolrServerException, IOException {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue