package org.apache.solr.cloud.yarn; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.security.SecureRandom; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.yarn.api.ApplicationConstants; import org.apache.hadoop.yarn.api.records.Container; import org.apache.hadoop.yarn.api.records.ContainerId; import org.apache.hadoop.yarn.api.records.ContainerLaunchContext; import org.apache.hadoop.yarn.api.records.ContainerStatus; import org.apache.hadoop.yarn.api.records.FinalApplicationStatus; import org.apache.hadoop.yarn.api.records.LocalResource; import org.apache.hadoop.yarn.api.records.LocalResourceType; import org.apache.hadoop.yarn.api.records.LocalResourceVisibility; import org.apache.hadoop.yarn.api.records.NodeReport; import org.apache.hadoop.yarn.api.records.Priority; import org.apache.hadoop.yarn.api.records.Resource; import org.apache.hadoop.yarn.client.api.AMRMClient.ContainerRequest; import org.apache.hadoop.yarn.client.api.NMClient; import org.apache.hadoop.yarn.client.api.async.AMRMClientAsync; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.util.ConverterUtils; import org.apache.hadoop.yarn.util.Records; import org.apache.log4j.Logger; /** * Launches Solr nodes in SolrCloud mode in YARN containers and then waits for shutdown. */ public class SolrMaster implements AMRMClientAsync.CallbackHandler { public static Logger log = Logger.getLogger(SolrMaster.class); public static void main(String[] args) throws Exception { (new SolrMaster(SolrClient.processCommandLineArgs(SolrMaster.class.getName(), getOptions(), args))).run(); } CommandLine cli; Configuration conf; NMClient nmClient; int numContainersToWaitFor; int memory; int port; int nextPort; boolean isShutdown = false; String randomStopKey; Map<String, Set<Integer>> solrHosts = new HashMap<String, Set<Integer>>(); String inetAddresses; public SolrMaster(CommandLine cli) throws Exception { this.cli = cli; Configuration hadoopConf = new Configuration(); if (cli.hasOption("conf")) { hadoopConf.addResource(new Path(cli.getOptionValue("conf"))); hadoopConf.reloadConfiguration(); } conf = new YarnConfiguration(hadoopConf); nmClient = NMClient.createNMClient(); nmClient.init(conf); nmClient.start(); numContainersToWaitFor = Integer.parseInt(cli.getOptionValue("nodes")); memory = Integer.parseInt(cli.getOptionValue("memory", "512")); port = Integer.parseInt(cli.getOptionValue("port")); nextPort = port; SecureRandom random = new SecureRandom(); this.randomStopKey = new BigInteger(130, random).toString(32); this.inetAddresses = getMyInetAddresses(); } protected String getMyInetAddresses() { Set<String> ipAddrs = new HashSet<String>(); try { Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); while (ifaces.hasMoreElements()) { NetworkInterface next = ifaces.nextElement(); for (InterfaceAddress addr : next.getInterfaceAddresses()) { InetAddress inetAddr = addr.getAddress(); String ipAddr = inetAddr.getHostAddress(); if (ipAddr != null && ipAddr.length() > 0) { // todo: use a regex if (!"127.0.0.1".equals(ipAddr) && (ipAddr.indexOf(".") != -1 && ipAddr.indexOf(":") == -1)) ipAddrs.add(ipAddr); } } } } catch (Exception exc) { throw new RuntimeException("Failed to get address(es) of SolrMaster node due to: " + exc, exc); } StringBuilder addrs = new StringBuilder(); for (String ip : ipAddrs) { if (addrs.length() > 0) addrs.append(","); addrs.append(ip); } return addrs.toString(); } public Configuration getConfiguration() { return conf; } public void run() throws Exception { int virtualCores = Integer.parseInt(cli.getOptionValue("virtualCores", "1")); AMRMClientAsync<ContainerRequest> rmClient = AMRMClientAsync.createAMRMClientAsync(100, this); rmClient.init(getConfiguration()); rmClient.start(); // Register with ResourceManager rmClient.registerApplicationMaster("", 0, ""); // Priority for worker containers - priorities are intra-application Priority priority = Records.newRecord(Priority.class); priority.setPriority(0); // Resource requirements for worker containers Resource capability = Records.newRecord(Resource.class); capability.setMemory(memory); capability.setVirtualCores(virtualCores); // Make container requests to ResourceManager for (int i = 0; i < numContainersToWaitFor; ++i) rmClient.addContainerRequest(new ContainerRequest(capability, null, null, priority)); log.info("Waiting for " + numContainersToWaitFor + " containers to finish"); while (!doneWithContainers()) Thread.sleep(10000); log.info("SolrMaster application shutdown."); // Un-register with ResourceManager try { rmClient.unregisterApplicationMaster(FinalApplicationStatus.SUCCEEDED, "", ""); } catch (Exception exc) { // safe to ignore ... this usually fails anyway } } public synchronized boolean doneWithContainers() { return isShutdown || numContainersToWaitFor <= 0; } public synchronized void onContainersAllocated(List<Container> containers) { String zkHost = cli.getOptionValue("zkHost"); String solrArchive = cli.getOptionValue("solr"); String hdfsHome = cli.getOptionValue("hdfs_home"); Path pathToRes = new Path(solrArchive); FileStatus jarStat = null; try { jarStat = FileSystem.get(conf).getFileStatus(pathToRes); } catch (IOException e) { throw new RuntimeException(e); } LocalResource solrPackageRes = Records.newRecord(LocalResource.class); solrPackageRes.setResource(ConverterUtils.getYarnUrlFromPath(pathToRes)); solrPackageRes.setSize(jarStat.getLen()); solrPackageRes.setTimestamp(jarStat.getModificationTime()); solrPackageRes.setType(LocalResourceType.ARCHIVE); solrPackageRes.setVisibility(LocalResourceVisibility.APPLICATION); Map<String, LocalResource> localResourcesMap = new HashMap<String, LocalResource>(); localResourcesMap.put("solr", solrPackageRes); String acceptShutdownFrom = "-Dyarn.acceptShutdownFrom=" + inetAddresses; log.info("Using " + acceptShutdownFrom); String dasha = ""; if (hdfsHome != null) { dasha += " -a '-Dsolr.hdfs.home=" + hdfsHome + " -Dsolr.directoryFactory=HdfsDirectoryFactory -Dsolr.lock.type=hdfs %s'"; } else { dasha += "-a '%s'"; } dasha = String.format(dasha, acceptShutdownFrom); String command = "/bin/bash ./solr/bin/solr -f -c -p %d -k %s -m " + memory + "m -z " + zkHost + dasha + " -V"; for (Container container : containers) { ContainerId containerId = container.getId(); // increment the port if running on the same host int jettyPort = nextPort++; String jettyHost = container.getNodeId().getHost(); Set<Integer> portsOnHost = solrHosts.get(jettyHost); if (portsOnHost == null) { portsOnHost = new HashSet<Integer>(); solrHosts.put(jettyHost, portsOnHost); } portsOnHost.add(jettyPort); log.info("Added port " + jettyPort + " to host: " + jettyHost); try { // Launch container by create ContainerLaunchContext ContainerLaunchContext ctx = Records.newRecord(ContainerLaunchContext.class); ctx.setLocalResources(localResourcesMap); String cmd = String.format(command, jettyPort, randomStopKey); log.info("\n\nRunning command: " + cmd); ctx.setCommands(Collections.singletonList( cmd + " >" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout 2>&1" )); log.info("Launching container " + containerId); nmClient.startContainer(container, ctx); } catch (Exception exc) { log.error("Failed to start container to run Solr on port " + jettyPort + " due to: " + exc, exc); } } } public void onContainersCompleted(List<ContainerStatus> containerStatuses) { for (ContainerStatus status : containerStatuses) { log.info("Completed container " + status.getContainerId()); synchronized (this) { numContainersToWaitFor--; } } } public void onShutdownRequest() { log.info("onShutdownRequest ... shutting down: " + solrHosts); for (String host : solrHosts.keySet()) { for (Integer port : solrHosts.get(host)) { stopJettyOnHost(host, port, randomStopKey); } } isShutdown = true; } protected void stopJettyOnHost(String host, int jettyPort, String stopKey) { String commandUrl = "http://" + host + ":" + jettyPort + "/shutdown?token=" + stopKey; log.info("Sending stop command to " + commandUrl); HttpMethod method = null; try { HttpClient client = new HttpClient(); method = new GetMethod(commandUrl); client.executeMethod(method); } catch (Exception exc) { log.warn("Failed to send shutdown command to " + commandUrl + " due to: " + exc, exc); } finally { method.releaseConnection(); } } public void onNodesUpdated(List<NodeReport> nodeReports) { for (NodeReport report : nodeReports) { log.info("Node: " + report); } } public float getProgress() { return 0; } public void onError(Throwable throwable) { log.error(throwable); } private static Options getOptions() { Options options = new Options(); Option[] opts = getSolrMasterOptions(); for (int i = 0; i < opts.length; i++) options.addOption(opts[i]); return options; } public static Option[] getSolrMasterOptions() { return new Option[]{ OptionBuilder .withArgName("HOST") .hasArg() .isRequired(true) .withDescription("Address of the Zookeeper ensemble; defaults to: localhost:2181") .create("zkHost"), OptionBuilder .withArgName("ARCHIVE") .hasArg() .isRequired(true) .withDescription("tgz file containing a Solr distribution.") .create("solr"), OptionBuilder .withArgName("PORT") .hasArg() .isRequired(false) .withDescription("Solr port; default is 8983") .create("port"), OptionBuilder .withArgName("PATH") .hasArg() .isRequired(false) .withDescription("Solr HDFS home directory; if provided, Solr will store indexes in HDFS") .create("hdfs_home"), OptionBuilder .withArgName("INT") .hasArg() .isRequired(false) .withDescription("Memory (mb) to allocate to each Solr node; default is 512M") .create("memory"), OptionBuilder .withArgName("INT") .hasArg() .isRequired(false) .withDescription("Virtual cores to allocate to each Solr node; default is 1") .create("virtualCores"), OptionBuilder .withArgName("INT") .hasArg() .isRequired(true) .withDescription("Number of Solr nodes to deploy; default is 1") .create("nodes"), OptionBuilder .withArgName("PATH") .hasArg() .isRequired(false) .withDescription("YARN ResourceManager address") .create("conf") }; } }