Archivo por meses: diciembre 2012

¿Cómo ejecutar Apache SOLR en Windows Azure?

Trabajando con una startup en Argentina me plantearon la pregunta sobre cómo ellos podían llevar su solución a Windows Azure y así obtener todas las ventajas de una solución cloud. Ellos utilizaban una aplicación Web con base de datos SQL, nada exótico por lo cual el movimiento era transparente, a excepción de un detalle. Esta compañía utiliza SOLR como motor de búsqueda. SOLR es un buscador empresarial que corre en java http://lucene.apache.org/solr/

Esa pieza es la parte más entretenida de la migración, cómo correr Apache Solr sobre Windows Azure. Bueno, este post explica cómo paso a paso.

Escenario
El escenario a implementar es un Sitio Web (Web Role) que juega el papel de Front End, el cual utiliza el servicio de búsqueda de Solr (Worker Role) que juega el papel de backend. Las llamadas entre el FrontEnd y SOLR son llamadas HTTP a un puerto interno. El escenario se muestra en el siguiente diagrama. La idea de usar un puerto interno es para que sólo nuestro Front End pueda utilizar el motor de Solr.

Prerrequisitos
Los prerrequisitos para desarrollar este tutorial son los siguientes:

  1. Tener instalado Windows Azure SDK for .NET
  2. Tener un archivo ZIP con la java virtual machine y SOLR
  3. Tener la librería ICSharpCode.SharpZipLib.dll para trabajar archivos ZIP

Como parte del código fuente de la solución, ambos archivos vienen incluidos. El SDK lo deben descargar directamente desde http://www.windowsazure.com/en-us/develop/net/#

Los pasos para construir el proyecto son los siguientes:

  1. Crear un proyecto del tipo Cloud
  2. Agregar la librería ICSharpCode.SharpZipLib.dll
  3. Agregar el archivo ZIP que contiene JRE y SOLR
  4. Agregar los Script de configuración
  5. Agregar la tarea de inicio del Worker Role
  6. Comprobar que SOLR se inicia junto con el Worker Role
  7. Configurar el puerto de comunicación entre el Web Role y el worker Role
  8. Crear página de búsqueda en el Web Role
  9. Implementar consulta de versión de Java a SOLR.
  10. Comprobar la consulta de versión de Java a SOLR.
  11. Implementar comando Ping y búsqueda a SOLR
  12. Comprobar el resultado de los comandos Ping y búsqueda

Paso 1: Crear un proyecto del tipo Cloud
En visual Studio 2012 crear un proyecto del tipo Cloud y agregarle dos roles:

  1. Web Role llamado FrontEndDolr
  2. Worker Role llamado SolrService

La siguiente figura muestra los dos roles a crear.

Paso 2: Agregar la librería ICSharpCode.SharpZipLib.dll
Esta biblioteca la utilizaremos para descomprimir el archivo ZIP que contiene JRE y SOLR. Podemos encontrar la librería en

http://www.icsharpcode.net/opensource/sharpziplib/

La agregamos en una carpeta llamada lib en el Worker Role, solo para mantener el orden del proyecto. Un detalle muy importante es que una vez agregada la libraría al proyecto vayamos a sus propiedades y configuremos «Copy to Output Directory» con el valor «Copy Always«

Paso 3: Agregar el archivo ZIP que contiene JRE y SOLR
Como SOLR corre en java es necesario tener la JavaTM Standard Edition Runtime Environment (JRE). La distribución de la JRE es muy simple, es cosa de copiar todo el contenido del directorio en el disco local del Worker Role antes de utilizarlo. JRE se puede descargar desde

http://www.java.com/es/download/index.jsp

De manera similar, SOLR, también puede ser distribuido copiando todo el contenido del directorio en el Worker Role. SOLR puede ser descargado desde

http://www.apache.org/dyn/closer.cgi/lucene/solr/4.0.0

Una vez que obtiene los dos componentes, la idea es copiar los directorios que los contienen y comprimirlos en un archivo ZIP, de nombre javasolr. Una vez hecho esto, debemos agregar el Zip al proyecto dentro de una carpeta que llamaremos java. Una vez agregado el archivo ZIP al worker Role se configura en sus propiedades «Copy to Output Directory» con el valor «Copy Always«.

Paso 4: Agregar los Script de configuración
Ahora debemos agregar los script que se ejecutaran al momento de iniciar el Worker Role. La idea es que al inicio del mismo, se realicen las siguientes tareas:

  1. Descomprimir JRE y SOLR
  2. Ejecutar SOLR

Para esto usaremos el siguiente script de PowerShell.

function unzip ($zip, $destination) {

Add-Type -Path ((Get-Location).Path + ‘\lib\ICSharpCode.SharpZipLib.dll’)

$fs = New-Object ICSharpCode.SharpZipLib.Zip.FastZip

$fs.ExtractZip($zip, $destination, »)

}

#Paso 1: descomprimier JRE y SOLR

unzip ((Get-Location).Path + «\java\javasolr.zip») (Get-Location).Path

#Paso 2: ejecutar SOLR

cd «.\javasolr\Solr»

..\..\jre7\bin\java.exe -jar start.jar

Este script lo agregamos al Worker Role en un archivo llamado Launch.PS1. Una vez agregado script al worker Role se configura en sus propiedades «Copy to Output Directory» con el valor «Copy Always«. Es importante grabar este archivo con la codificación para Windows y no UTF-8 que utiliza Visual Studio por defecto para archivos de texto. Esto se hace en el menú File, Advance Save Options y seleccionar una codificación para Windows como la siguiente.

Ahora, vamos a agregar el archivo Run.cmd que será el que ejecutará el script de PowerShell. Run.cmd contiene la siguiente línea de código

powershell -executionpolicy unrestricted -file .\Launch.ps1

Esta línea de comandos, está ejecutando el script de PowerShell sin restricciones. Una vez agregado el archivo Run.cmd al worker Role no olvidar configurar en sus propiedades «Copy to Output Directory» con el valor «Copy Always«. Si esto no se realiza, entonces no se copia el script por lo cual la tarea de inicio fallará. Por último, grabar este archivo con la codificación para Windows y no UTF-8, de la misma forma como se hizo para el Script de PowerShell.

Paso 5: Agregar la tarea de inicio del Worker Role

El archivo llamado ServiceDefinition.csdef es el que contiene todas las definiciones del servicio que estamos creando. En él está definido el Web Role y el Worker Role. Es aquí donde vamos a agregar la tarea de inicio del Worker Role, para que descomprima el archivo ZIP que contienen la JRE y SOLR, para luego levantar el servicio de SOLR en el puerto 8983.

Para ellos debemos modificar la definición del Worker Role de la siguiente manera

<WorkerRole name="SolrServicevmsize="Small">
    <Startup>
      <Task commandLine="Run.cmdexecutionContext="limitedtaskType="background" />
    </Startup>
    <Imports>
      <Import moduleName="Diagnostics" />
    </Imports>
    <Endpoints>
      <InternalEndpoint name="httpSolrprotocol="tcpport="8983" />
    </Endpoints>
  </WorkerRole>

El tag Startup que contiene TASK define que al momento de iniciar el Worker Role, se ejecute el comando Run.cmd

Paso 6: Comprobar que SOLR se inicia junto con el Worker Role
Ahora vamos a comprobar que todo está funcionado correctamente con nuestro proyecto. Paro esto vamos a ejecutar en modo debug. Obtenemos la siguiente página Web en nuestro Browser.

Ahora, vamos a ver si SOLR está funcionando. Hay que tener en cuenta que el Worker Role al iniciarse, descomprime el archivo ZIP y eso toma un tiempo. SOLR funciona por defecto en el puerto 8983, entonces cargamos la siguiente URL http://localhost:8983/solr/admin/ y vemos

Ahora, si ejecuto la búsqueda siguiente obtengo http://localhost:8983/solr/select/?q=usb&version=2.2&start=0&rows=10&indent=on

Nota aclaratoria: el paquete de SOLR que estoy utilizando ya está indexado para efecto de pruebas, si ustedes usan el paquete por defecto, no podrán buscar hasta realizar el indexado. Un tutorial de SOLR muy rápido de leer, donde pueden ver como indexar, está en http://www.solrtutorial.com/solr-in-5-minutes.html

Paso 7: Configurar el puerto de comunicación entre el Web Role y el Worker Role
Para establecer la comunicación entre el Web Role, que será nuestro Front End, y el Worker Role, donde está corriendo el servicio SOLR vamos a declarar un EndPoint en el Worker Role. Este Endpoint será del tipo interno ya que su objetivo es solo permitir que el Web Role haga consultas a SOLR. Para ellos vamos a las propiedades del Worker Role, en la sección de EndPoint agregamos uno como se muestra en la siguiente figura.

Paso 8: Crear página de búsqueda en el Web Role
Ahora vamos a trabajar en el Front End, es decir en el Web Role donde crearemos una página para poder realizar las búsquedas. En el proyecto FrontEndSolr agregar un nuevo Web Form que se llame busqueda.aspx. Marcar está nueva página como la página de inicio, para ello con el botón derecho sobre la misma seleccionar Set as Start Page.

A la página de búsqueda le agregamos 5 controles. Una caja de texto para las búsquedas, 3 botones para buscar, consultar la versión de java y hacer un ping respectivamente. Por último, el control literal para poder ver las respuestas de SOLR. Estos son los controles a agregar.

<strong>Buscador SOLR</strong><br />
        <asp:TextBox ID="TextBox1" runat="server" Width="303px"></asp:TextBox>
        <br />
        <asp:Button ID="btBuscar" runat="server" Text="Buscar" OnClick="btBuscar_Click" />
        <asp:Button ID="btJava" runat="server"  Text="Java Version" OnClick="btJava_Click" />
        <asp:Button ID="btPing" runat="server"  Text="Ping" OnClick="btPing_Click" />
        <br />
        <br />
        <asp:Literal ID="Literal1" runat="server" EnableViewState="False" Mode="PassThrough"></asp:Literal>

Paso 9: Implementar consulta de versión de Java a SOLR.
Vamos a partir con una consulta sencilla a SOLR, le vamos a preguntar desde el FrontEnd qué versión de Java está corriendo. Para ellos debemos enviar la siguiente petición a SOLR

http://localhost:8983/solr/admin/get-properties.jsp

Ahora la dirección 127.0.0.1 puerto 8983 no podemos utilizarla ya que cuando hagamos deploy de nuestra solución Web Role y Worker Role corren en VM diferentes por lo que tienen direcciones diferentes. Ahora, nosotros no sabemos las direcciones que nos van asignar por lo cual debemos implementar un método que nos permita obtener la dirección de SOLR de manera dinámica. Este método lo implementaremos en WebRole.cs ya que utiliza RoleEnviroment para acceder a la información de los roles e instancias que se están ejecutando.

public static string getSolServiceAddress()
{
    return RoleEnvironment.Roles["SolrService"].Instances[0].InstanceEndpoints["httpSolr"].IPEndpoint.ToString();
}

Ahora podemos implementar la consulta de la versión de java de esta manera

protected void btJava_Click(object senderEventArgs e)
{
    //Dirección de SolrService
    string urlTarget = WebRole.getSolServiceAddress();
    string solrURL = string.Format("http://{0}/solr/admin/get-properties.jsp"urlTarget);
    //Ejecuta la llamada HTTP
    string respuesta = this.CommandSolr(solrURL);
    Literal1.Text = string.Format("<H3>{0}</h3><PRE>{1}"urlTargetServer.HtmlEncode(respuesta));
}

Donde CoomandSolr es un método auxiliar que realiza la llamada a SOLR, utilizando la clase WebRequest. El método es el siguiente y se utilizará para todas las consultas

private string CommandSolr(string url)
{
    string responseFromServer;
    WebRequest request = WebRequest.Create(url);
    // If required by the server, set the credentials.
    request.Credentials = CredentialCache.DefaultCredentials;
    try
    {
        // Get the response.
        WebResponse response = request.GetResponse();
        // Display the status.
        Console.WriteLine(((HttpWebResponse)response).StatusDescription);
        // Get the stream containing content returned by the server.
        Stream dataStream = response.GetResponseStream();
        // Open the stream using a StreamReader for easy access.
        StreamReader reader = new StreamReader(dataStream);
        // Read the content.
        responseFromServer = reader.ReadToEnd();
        // Clean up the streams and the response.
        reader.Close();
        response.Close();
    }
    catch (Exception X)
    {

        responseFromServer = X.Message.ToString();
    }
    return responseFromServer;

Paso 10: Comprobar la consulta de versión de Java a SOLR. Para comprobar que nuestra implementación está correcta, vamos a ejecutar el proyecto en modo Debug y validar que podemos consultar la versión de Java. Al presionar el botón «Java Versión» de la página busqueda.aspx obtendremos una respuesta como la que se muestra en la siguiente imagen. La dirección que aparece al principio de la respuesta, es la dirección asignada dinámicamente del EndPoint interno que definimos. En este caso 127.255.0.1:8983

Paso 11: Implementar comando Ping y búsqueda a SOLR

La implementación del comando Ping es simplemente llamar a la URL siguiente, por lo que implementaremos un método similar al anterior.

http://localhost:8983/solr/admin/ping

El método que utilizaremos es

protected void btPing_Click(object senderEventArgs e)
{
    //URL de Consulta 
    string urlTarget = WebRole.getSolServiceAddress();
    string solrURL = string.Format("http://{0}/solr/admin/ping"urlTarget);
    string respuesta = this.CommandSolr(solrURL);
    Literal1.Text = string.Format("<H3>{0}</h3><PRE>{1}"urlTargetServer.HtmlEncode(respuesta));
}

Ahora de manera equivalente, para la búsqueda tenemos que insertar el texto a buscar en la URL que se llama.

protected void btBuscar_Click(object senderEventArgs e)
{
    if (TextBox1.Text == "")
    {
        Literal1.Text  = "no puede ser en blanco";
    }
    else
    {
        string urlTarget = WebRole.getSolServiceAddress();
        string solrURL = "http://{0}/solr/select/?q={1}&version=2.2&start=0&rows=10&indent=on";
        string q = Server.HtmlEncode(TextBox1.Text);
        string respuesta = this.CommandSolr(string.Format(solrURL,urlTargetq));
        Literal1.Text = string.Format("<H3>{0}</h3><PRE>{1}"urlTargetServer.HtmlEncode(respuesta));
    }
}

Paso 12: Comprobar el resultado de los comandos Ping y búsqueda
Ejecutamos el proyecto en modo debug, y comprobamos los dos nuevos comandos. La respuesta a Ping es la siguiente.

Por último, la respuesta a la búsqueda de la palabra USB es la siguiente.

Al cierre
La ejecución de servicios como SOLR es perfectamente posible en Windows Azure, utilizando Worker Role. Existen varias consideraciones que tener en cuenta en este tipo de soluciones, por ejemplo como podemos hacer escalar esto. Cómo podemos tener múltiples Worker Role ejecutando Soler y desde el Front End implementar alguna suerte de balanceo de carga para la llamadas. Ese será el próximo post!

Otro ejemplo de servicio Java corriendo en Azure es Deploying Java Applications in Azure de Mario Kosmiskas, donde explican cómo correr JETTY en Azure y que yo leí para resolver la pregunta del post.

El código del proyecto pueden bajarlo desde el siguiente link, este es un ejemplo que tiene el propósito de ilustrar el ejemplo y no es para ser puesto en producción.