Corría el año 2011 cuando Netflix se encontraba migrando sus servicios a la nube cuando detectó que era difícil probar/testear el comportamiento de la plataforma ante situaciones inesperadas. Por ello diseñaron un sistema al que llamaron Chaos Monkey que se encargaba de introducir errores de forma aleatoria, buscando “perturbar” el buen funcionamiento de la plataforma ante fallos.

Posteriormente Netflix fue añadiendo más y más componentes, especializados en distintos escenarios cada uno, por lo que pasaron a denominar “Simian Army” al conjunto de las herramientas:

 

Siguiendo esta línea de pensamiento podríamos plantearnos crear algo parecido a nivel de base de datos SQL Server para mejorar nuestra tolerancia a fallos y perturbaciones. Tenemos que tener en cuenta que, a día de hoy, aún muchos entornos ejecutan en servidores dedicados físicos. Con el paso a la virtualización se añadió cierta incerdidumbre y variabilidad en base al “ruido” generado por otras VMs. Con el salto a cloud, especialmente si es en entornos PaaS, debemos estar preparados para tener un menor control y por tanto una mayor aleatoriedad respecto a cuando ocurren problemas y también sobre la naturaleza de éstos.

Podemos ver cómo Microsoft, en el caso de Azure, intenta exponer en la documentación que este tipo de problemas son inherentes en los entornos cloud: https://docs.microsoft.com/en-us/azure/architecture/best-practices/transient-faults#why-do-transient-faults-occur-in-the-cloud. AWS también en su whitepaper sobre reliability (https://d1.awsstatic.com/whitepapers/architecture/AWS-Reliability-Pillar.pdf) hace hincapié en que debemos testear nuestras aplicaciones tanto ante fallos de pocos segundos como de horas y hacerlo en un entorno con carga (para no desvirtuar los cálculos de posibles RTOs bajo carga real)

En el caso de SQL Database, por poner algún ejemplo concreto, algunos de nuestros clientes nos han preguntado sobre este error:

“Error code 40613: «Database <x> on server <y> is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them the session tracing ID of <z>»”

La realidad es que ese es solo uno de los muchos errores posibles que Microsoft considera como “transient” (https://docs.microsoft.com/en-us/azure/sql-database/sql-database-develop-error-messages#transient-fault-error-codes) y por tanto considera que somos nosotros responsables de “reintentar” ante su ocurrencia puntual. La mayor parte de código existente, como mucho, gestiona el típico error 1205 (deadlock), o el timeout, pero no está preparado para el resto de errores.

Por tanto, creemos que una posible forma de ver cómo de preparadas están nuestras aplicaciones para responder ante fallos sería implementar en nuestros entornos procesos que simularan fallos de forma aleatoria a nivel de base de datos. Algunas situaciones que se nos ocurren que podrían ser interesantes de simular serían:

  • Cortes abruptos de conectividad aleatorios.
  • Generación de situaciones de saturación de recursos (memoria, disco, CPU, network…)
  • Ralentización de procesos por bloqueos
  • Rollbacks forzados en transacciones abiertas

Plantear una suite completa de simulación problemas está fuera del alcance de una entrada en un blog, por lo que vamos a centrarnos en un par de problemáticas.

La primera de ellas sería la de los bloqueos para lo que generaremos un procedimiento almacenado que, de forma aleatoria, bloquee una tabla de nuestro sistema durante un número de milisegundos aleatorio. Por ejemplo, podríamos hacer algo así:

use AdventureWorks2017
go
create or alter procedure SQLMonkey_tablocks (
  @min_lock_duration_ms int, 
  @max_lock_duration_ms int
)
AS
BEGIN
  SET NOCOUNT ON;
  IF (@min_lock_duration_ms is null or @max_lock_duration_ms is null)
  BEGIN
    RAISERROR ('@min_lock_duration_ms or @max_lock_duration_ms is null',16, 1 )
    RETURN -1
  END

  IF (@min_lock_duration_ms <=0 or @max_lock_duration_ms <=0)
  BEGIN
    RAISERROR ('@min_lock_duration_ms or @max_lock_duration_ms <=0',16, 1 )
    RETURN -2
  END

  IF (@min_lock_duration_ms > @max_lock_duration_ms)
  BEGIN
    RAISERROR ('@min_lock_duration_ms > @max_lock_duration_ms',16, 1 )
    RETURN -3
  END
  -- Get a random duration between min and max
  DECLARE @lock_duration_ms int = 
    FLOOR(RAND()*(@max_lock_duration_ms-@min_lock_duration_ms+1))+@min_lock_duration_ms;
  
  -- Get a random table name
  DECLARE @table varchar(max) = 
  (select top(1) QUOTENAME(schema_name(schema_id)) + '.' + QUOTENAME(name) from sys.tables order by newid())

  -- Lock table, wait @lock_duration_ms milliseconds and release
  RAISERROR ('Locking %s for %i ms...',10,1,@table,@lock_duration_ms)
  DECLARE @SQL varchar(max) = '
    BEGIN TRAN; 
    SELECT count(*) from ' + @table + ' WITH (TABLOCK, XLOCK, HOLDLOCK) WHERE 0=1; 
    DECLARE @d DATETIME = DATEADD(MILLISECOND, ' + convert(varchar(max),@lock_duration_ms) + ', GETDATE())
    WAITFOR TIME @d
    COMMIT'
  EXEC (@SQL)
  RAISERROR ('Unlocking %s after %i ms...',10,1,@table,@lock_duration_ms)
END

Para ponerlo en marcha simplemente tendremos que lanzar el procedimiento bien dentro de un job, con un bucle con una espera entre bloqueos, o de cualquier otra forma que se nos ocurra. Por ejemplo, si lo lanzamos desde SSMS podemos generar 100 bloqueos con una duración entre 2 y 5 segundos de esta forma:

exec SQLMonkey_tablocks 2000,5000
go 100
Beginning execution loop
Locking [Production].[ProductReview] for 3097 ms...
Unlocking [Production].[ProductReview] after 3097 ms...
Locking [Production].[WorkOrder] for 4084 ms...
Unlocking [Production].[WorkOrder] after 4084 ms...
Locking [Purchasing].[PurchaseOrderDetail] for 3105 ms...
Unlocking [Purchasing].[PurchaseOrderDetail] after 3105 ms...
Locking [Sales].[CountryRegionCurrency] for 4484 ms...
Unlocking [Sales].[CountryRegionCurrency] after 4484 ms...
(…)
Locking [Sales].[SalesTerritory] for 3272 ms...
Unlocking [Sales].[SalesTerritory] after 3272 ms...
Locking [Purchasing].[PurchaseOrderHeader] for 3081 ms...
Unlocking [Purchasing].[PurchaseOrderHeader] after 3081 ms...
Batch execution completed 100 times.

Otro procedimiento de tipo “mono loco” que podemos crear con facilidad es uno que mate procesos/sesiones aleatoriamente. En el ejemplo mostrado a continuación filtraremos aquellas sesiones que sean de usuario, y no de sistema, ya que el objetivo es probar cómo se comportan nuestras aplicaciones ante kills aleatorios, no el propio SQL Server. Añadiremos también un parámetro para indicar si queremos que mate únicamente sesiones con peticiones en marcha (running) o también en cualquier estado. Esto incluiría sesiones en sleeping por ejemplo, que están simplemente conectadas sin ejecutar nada, lo que nos ayudaría a verificar que las aplicaciones son capaces de reconectar sin problemas:

use AdventureWorks2017
go
create or alter procedure SQLMonkey_killer (
  @only_running int =0
)
AS
BEGIN
  SET NOCOUNT ON;
  IF (@only_running not in (0,1))
  BEGIN
    RAISERROR ('@only_running should be 0 or 1',16, 1 )
    RETURN -1
  END

  -- Get a random process
  DECLARE @SPID int
  IF (@only_running=0)
    SET @SPID= (select TOP (1) session_id from sys.dm_exec_sessions where is_user_process=1 and session_id <> @@spid order by newid())
  ELSE 
    SET @SPID= (select TOP (1) session_id from sys.dm_exec_sessions where is_user_process=1 and session_id <> @@spid and status='running' order by newid())

  IF @SPID is null 
  BEGIN
    RAISERROR ('No processes available to be killed...',10,1)
    RETURN -2
  END
  
  -- Kill session
  RAISERROR ('Killing session %i',10,1,@SPID)
  DECLARE @SQL varchar(max) = 'KILL ' + convert(varchar(max),@SPID)
  EXEC (@SQL)
  RAISERROR ('Session %i killed',10,1,@SPID)
END
go

Si queremos lanzar 3 “matados” aleatorios cada 2 segundos y que solo afecten a procesos en marcha podemos lanzarlo desde SSMS de la siguiente forma:

exec SQLMonkey_killer @only_running=1

waitfor delay '00:00:02'

go 3


Beginning execution loop

Killing session 61

Session 61 killed

Killing session 77

Session 77 killed

No processes available to be killed...

Batch execution completed 3 times.

Si queremos que afecten a cualquier sesión de usuario existente lo lanzaremos sin el parámetro @only_running o con dicho parámetro a valor 0.

exec SQLMonkey_killer @only_running=0

waitfor delay '00:00:02'

go 3


Beginning execution loop

Killing session 67

Session 67 killed

Killing session 51

Session 51 killed

Killing session 77

Session 77 killed

Batch execution completed 3 times.

En conclusión, creemos que una arquitectura basada en un “punto único de fallo” como puede ser una base de datos en una instancia de SQL Server, independientemente del mecanismo de HA que utilicemos, es demasiado arriesgada para sistemas críticos ya que fallos a nivel de base de datos o instancia siempre podremos tener. Por tanto, es fundamental que si la continuidad de nuestro negocio es crítica para nosotros tengamos redundancia en forma de múltiples instancias, múltiples bases de datos, ubicadas en distintos entornos, distintos datacenters, etc. que sean capaces de asumir la carga de otras instancias que puedan tener problemas puntuales. Idealmente deberíamos poder al menos dar un «servicio parcial» al mayor número de clientes posibles durante las incidencias de forma que el impacto no sea global. Si queremos conseguir buenos resultados nuestras aplicaciones deberían ser «tolerantes» con ciertos fallos, incluir reintentos, reconexiones automáticas, etc. para que la experiencia global desde el lado del usuario sea la mejor posible.

Rubén Garrigós

Mentor at SolidQ
I am an expert in high-availability enterprise solutions based on SQL Server design, tuning, and troubleshooting. Over the past fifteen years, I have worked with Microsoft data access technologies in leading companies around the world.

Nowadays, I am a Microsoft SQL Server and .NET applications architect with SolidQ. I am certified by Microsoft as a Solution Expert on the Microsoft Data Platform (MSCE: Data Platform) and as a Solution Expert on the Microsoft Private Cloud (MSCE: Private Cloud). As a Microsoft Certified Trainer (MCT), I have taught multiple official Microsoft courses as well as other courses specializing in SQL Server. I have also presented sessions at official events for various Microsoft technologies user groups.
Rubén Garrigós