Cómo se hizo la retransmisión en directo de la DotNetConference 2016

El pasado miércoles 24 de febrero tuvo lugar la conferencia para desarrolladores de .NET organizada por Microsoft España, que tuvo como invitado especial al CEO de Microsoft Satya Nadella. El evento lo pudieron disfrutar aproximadamente 1700 personas de forma presencial, pero también se ofreció la posibilidad de ver uno de las sesiones desde internet con unas 5000 personas online. En este artículo vamos a comentar la arquitectura de la solución para hacer que la retransmisión fuera un éxito.

Diagrama de arquitectura

Antes de continuar este es el detalle de la arquitectura de servicios de Azure que se usaban para hacer el streaming como tal. Aquí no se incluyen las cámaras y la ingesta del video en directo.

image001

Cómo se puede observar en el datacenter de West Europe era donde estaban la mayoría de los servicios que se necesitaban para el streaming. El otro datacenter de North Europe era un backup para el streaming del video por si alguna de las fases de la ingesta de video fallaba.

Azure Media Services

Para hacer la retransmisión del video se ha utilizado el servicio de Azure Media Services en la modalidad de directo. Para ello lo que hay que hacer es crear un canal en directo desde el portal clásico especificando que se quiere codificación del video en directo y que la ingesta utiliza el protocolo RTMP. Así de esta manera el servicio ofrece una configuración predeterminada de codificación en directo con una calidad adaptativa máxima de 1280x720 a 3,500 Mbps.

image003

Una vez que se tiene configurado el servicio de ingesta del directo el siguiente paso es configurar los programas. Estos permiten definir ventanas de tiempo para grabar diferentes programas del directo y así poder archivarlo de forma independiente.

image004

En el caso del evento se tenían 8 programas configurados, incluyendo las sesiones y las dos sesiones principales. Además, había una sesión configurada llamada CoreStreaming que era la que se estaba viendo en la web de Channel9 que contenía todo lo retransmitido pues la ventana configurada era de 16 horas máximas.

Todo esto además se tenía configurado en el datacenter del norte de Europa emitiendo a la vez para tener alta redundancia en caso de caída.

Cámaras y hardware de emisión

Como se ha comentado anteriormente en la configuración de la emisión en directo se tenía por duplicado, así que en la sala 6 del Kinepolis se tenían dos cámaras conectadas a un pc emitiendo a West Europe y otras dos cámaras conectadas emitiendo a North Europe. Todo ello conectado por SDI, tanto la imagen de la cámara como la imagen que viene del proyecto de la sala de cine, que en ese caso es la imagen de la pantalla del ponente. Eso se producía en directo en los dos sistemas y se emitía en directo. De esta manera se tiene la misma señal por duplicado en dos datacenter para poder tener un backup en caso de que pase cualquier cosa a las cámaras/software/pc/red, ect.

Tecnología de reproductor

Detrás del reproductor de Channel9 está Azure Media Player que ya tienen todo lo necesario para poder reproducir el contenido en todos los dispositivos móviles, tables y pcs. Al hacer la ingesta y la codificación en directo con calidad adaptativa, Azure Media Services es capaz de empaquetar al vuelo en los diferentes formatos que ofrece, MPEG-DASH, Smooth Straming y Apple HLS, además de hacer fallback a reproductores en HTML5 (MSE/EME), Flash o Silverlight.

Recordando la arquitectura de la solución, uno de los componentes que se usaba para servir el HTML de reproductor es un servicio de Redis que se encarga de sacar la url de endpoint donde está el flujo en directo que el reproductor necesita. Esta URL define el programa que se quiere reproducir y es importante poder cambiar esos valores al vuelo.

Para ello cada vez que carga la página se comprueba el valor de la llave en el servidor de Redis para saber que url hay que conectarse. Aunque el servicio de Redis es muy rápido y están localizado físicamente en el datacenter de West Europe, consultar cada vez ese valor sería muy costoso. Así que la aproximación a este problema ha sido algo intermedio.

El sistema lo que hace es disponer de un diccionario donde se almacenan los valores de las llaves del servidor de Redis, un se tiene un temporizador que cada 500 milisegundos obtiene de nuevo todas las llaves del servidor y actualiza las que ha cambiado, de esta manera, por cada una de las máquinas virtuales del App Service Plan (4 en total de tamaño P3) hay una conexión al servidor de Redis en vez de por cada página que se sirva con el reproductor.

El siguiente paso es actualizar este diccionario de forma segura frente a subprocesos, para ello la clave es usa una clase de .NET framework que se llama ReaderWriterLockSlim que permite tener un objeto de bloqueo para multiples lectores y un solo escritor. Que es justamente el escenario que aquí se necesita, muchas de las peticiones serán bloqueos de lectura, y solo cuando se haga una modificación de la cache entonces se modificará la estructura.

Lecturas

Para hacer las lecturas se utiliza el objeto antes mencionado para hacer un bloque de lectura. Se comprueba que existe el elemento en el diccionario y se obtiene su valor.

public string GetKeyCore(string key)

{

string result = null;

if (!string.IsNullOrEmpty(key))

{

rwls.EnterReadLock();

try

{

if (items.ContainsKey(key))

{

result = items[key];

}

TrackGetKeyEvent(key);

}

catch (Exception ex)

{

telemetry.TrackException(ex);

}

finally

{

rwls.ExitReadLock();

}

}

return result;

}

Las escrituras son un poco más complicadas, porque primero lo que se hace es una lectura que se puede elevar a escritura en el objeto de bloqueo. Para ello lo que se hace es llamar a EnterUpgradeableReadLock que adquiere un bloqueo de lectura actualizable. En el caso de que se tenga que actualizar entonces se bloquea a escritura llamando a EnterWriterLock y se actualiza la variable del diccionario.

private void UpdateKey(string key, RedisValue value)

{

if (!string.IsNullOrEmpty(key))

{

if (!value.IsNullOrEmpty)

{

string realValue = value;

rwls.EnterUpgradeableReadLock();

bool needUpdate = false;

try

{

if (items.ContainsKey(key) &&
items[key] != realValue)

{

needUpdate = true;

}

if (!items.ContainsKey(key))

{

needUpdate = true;

}

if (needUpdate)

{

rwls.EnterWriteLock();

try

{

if (items.ContainsKey(key))

{

items[key] = realValue;

}

else

{

items.Add(key, realValue);

}

TrackUpdateKey(key, realValue);

}

catch (Exception ex)

{

telemetry.TrackException(ex);

}

finally

{

rwls.ExitWriteLock();

}

}

}

catch (Exception ex)

{

telemetry.TrackException(ex);

}

finally

{

rwls.ExitUpgradeableReadLock();

}

}

}

}

En ambos casos cada vez que se hace cualquier operacion de bloqueo, ya se de lectura, escritura o lectura actualizable, el código siempre se envuelve dentro de un bloque de try catch para en el finalize de ese bloque liberar el bloqueo. En el caso de que no se libere cualquier bloqueo se puede encontrar un bloqueo por espera mutua. Aunque el código que hay entre los bloques try catch es muy sencillo y a simple vista no tiene por qué fallar, es una buena práctica envolver ese código en un try catch.

Temporizador

El temporizador que lanza el proceso de actualización también es especial, ya que al ser una frecuencia de actualización tan alta (500ms) que puede que el tiempo que se tarda en completar la ejecución del código de actualización de las llaves sea superior a este intervalo. Con lo que si esto sucediese se ejecutaría de nuevo el proceso y se tendrías dos hilos a la vez intentando actualizar los valores. Para ello la clase ProtectedTimer, permite ejecutar el código de actualización solamente una vez para ello utiliza clase Interlocked para incrementar de forma segura un entero marcado como volátil en la definición para asegurarse de que solamente un hilo se está ejecutando.

public class ProtectedTimer

{

private Action action;

private TimeSpan time;

private Timer timer;

private TelemetryClient client = new TelemetryClient();

private volatile int isWorking;

public ProtectedTimer(TimeSpan time, Action action, bool
autostart = true)

{

this.action = action;

this.time = time;

if (autostart)

{

Start();

}

}

public void Stop()

{

timer.Dispose();

}

public void Start()

{

timer = new Timer(new TimerCallback(OnTimerTick),
null, 0, (int)time.TotalMilliseconds);

}

public void Start(TimeSpan dueTime)

{

timer = new Timer(new TimerCallback(OnTimerTick),
null, (int)dueTime.TotalMilliseconds, (int)time.TotalMilliseconds);

}

private void OnTimerTick(object arg)

{

Interlocked.Increment(ref isWorking);

if (isWorking == 1)

{

try

{

action();

}

catch (Exception ex)

{

client.TrackException(ex);

Trace.WriteLine("ProtectedTimer: "
+ ex.ToString());

}

isWorking = 0;

}

}

}

Estadísticas de uso de la cache de redis

Con esta infraestructura ninguna las peticiones de una web llegaban a hacer un hit en el servidor de redis, pero el temporizados de 500 milisegundos leía todos los valores. Estas son las estadísticas desde las 8AM hasta las 11PM de la noche.

image006image007

Las conexiones durante todo el evento fueron muy bajas, ya que solo se hace aproximadamente una por cada máquina que estaba en funcionamiento.

image008

Cambio de la URL del reproductor sin actualizar la página

Desde el principio una de las motivaciones del evento era que nada podía fallar. Pero no se pueden controlar todas las variables y desde luego puede pasar cualquier cosa, así que lo mejor de todo es esperar el fallo y estar preparado.

Para ello lo que se hizo fue tener la posibilidad de cambiar de datacenter desde el que se hacía la retransmisión del directo en caliente, pulsando un botón cambiar automáticamente todas las URLs del datacenter de West Europe al de North Europe y al revés.

Para ello se construyó una web paralela con un App Service Plan independiente donde está alojada una web que permite cambiar los valores del servidor de cache Redis en cualquier momento.

image009

La interfaz de usuario es muy sencilla y permite cambiar de datacenter simplemente pulsando un botón, además también ofrece la posibilidad de cambiar alguna de las URLs a mano de la aplicación.

En la parte de cliente existía un JavaScript que se encargaba cada 10 segundos de preguntarle al servidor cual es la url del video para la página actual y en el caso de haya cambiado actualizarla directamente.

(function () {

var checkUrl = function () {

var location;

if (window.location.pathname === "/") {

location = "default";

}

else {

location = window.location.pathname.substring(1,
window.location.pathname.length - 5);

location = location.replace("/",
"_");

location = location.toLowerCase();

}

$.ajax({

url: "/status/" + location +
".aspx"

})

.done(function (data) {

if (data.value !== window._smanifest) {

window._smanifest = data.value;

window.thePlayer.src([{ src: data.value, type:
"application/vnd.ms-sstr+xml" }, ]);

}

});

};

var checkTimeout = setInterval(checkUrl, 1000 *
10);

})();

Detecta de forma automática que localizacion está y consulta una web localizada en el tramo /status/{location}.aspx que contiene la respuesta en formato JSON de la URL. El por qué para cada URL diferente hay una página diferente de estado con la URL del video en directo se comenta más abajo y está relacionado con la cache de Kernel de IIS.

Otras optimizaciones

Cada uno de los programas que se comentó al principio tenía una URL dedicada, es decir, un fichero .aspx que contenía la lógica del player para inicializarse con el canal adecuado. Esto se hizo de esta manera porque en el servidor web se activó la cache de Internet Information Services de kernel para la extensión .aspx.

image011

De esta forma durante 10 segundos las respuestas de todos los ficheros .aspx de la solución son cacheadas por el kernel en el módulo de HTTP.SYS que implementa el driver de HTTP de Windows. El único inconveniente de esta solución es que se cachea la primera respuesta del fichero, sin tener en cuenta parámetro de QueryString de la URL.

Rendimiento final de la infraestructura

A continuación, alguna de las gráficas de rendimiento durante el evento:

CPU del servicio de Web Hosting Plan con 3 máquinas S3, que luego se bajaron a 2 S1

image013

Número de hits en el sitio web

image014

Se tuvo que hace un deployment a eso de las 11 de la mañana y se han perdido los hits antes de esa hora, pero se puede concluir en más de 2.7M en total. Con un consumo de CPU, de media, de 10%.

Telemetría

Para saber cuántos usuarios activos ha habido y cuantas peticiones se han realizado se ha añadido la telemetría de Application Insights tanto en el cliente como en la parte de servidor para saber cuántas peticiones, navegadores y tiempo se ha pasado en la página del player viendo la retransmisión del directo.

Tiempo medio de respuesta del servidor

image015

Tiempo medio de respuesta de la url del player especifica fuero 10.05ms en total y la url de comprobación del player fueron 6.31ms, valores muy buenos de respuesta del HTML del player.

Número de llamadas a la CDN del video

image016

De las cuales solo fallaron

image017

Que representa el 0,1% de las peticiones.

Pruebas de carga.

Antes del evento se realizaron pruebas de carga para ver el rendimiento de las páginas del reproductor y de la característica de cambiar la URL del directo sin actualizar la página web. Para ello se utilizó Visual Studio 2015 y las pruebas de carga web para simular la navegación de todas las páginas web además de las páginas de actualización de la URL del reproductor.

Para hacer las pruebas se provisiono una maquina D4 (8 Cores y 28Gb de memoria) en el mismo datacenter de West Europe. En la gráfica de Azure Web Sites correspondiente al sitio web se llegaron a 1.8M de peticiones web en aproximadamente 30 minutos.

image018

Además, en la parte de la izquierda se puede observar las mismas pruebas, pero leyendo cada vez del servidor de Redis en vez de cachear las peticiones. No solo el rendimiento fue peor en términos de páginas por segundo servidas, sino que el número de errores en el servidor web (HTTP Status Code 500) fue muy grande, siendo en la parte de la derecha cero.

Este es el resultado de alguna de las pruebas que se realizaron en Visual Studio.

image019

Datos curiosos a comentar:

  • De media se recibieron 2533 páginas por segundo, con un máximo de 4117 y un minimo de 80.
  • Se llegó a simular 3700 usuarios de forma concurrente accediendo a la página.
  • El tiempo medio de respuesta de la mayoría de las paginas fue de 0,85 segundos o lo que es lo mismo 850 milisegundos para responder la web, incluyendo tiempo de transferencia del HTML.
  • El número de errores por segundo de la prueba fue cero.

Conclusiones

Teniendo en cuenta que Azure ofrece casi todo lo que se necesita para poder hacer la retransmisión en directo de un evento, lo que hay alrededor también es muy importante. Tener en cuenta la escalabilidad del sistema y los posibles picos es muy importante. Hasta que no te enfrentas a este tipo de evento no te das cuenta de la cantidad de peticiones que se pueden hacer a tu sistema, así que durante la planificación es muy importante hacer muchas pruebas de carga. Nadie puede estimar cuantos usuarios de verdad pueden venir a ver el directo y más teniendo en cuenta la tan honorable visita de nuestro CEO Satya Nadella.

Luis Guerrero.

Technical Evangelist Microsoft Azure.

@guerrerotook