Cases de Insucesso

investigando outages, bugs e outras falhas

roberta arcoverde

@rla4

rla4.com

/whois 

  • recifense
  • programadora .NET desde 2005
  • tech lead do stack overflow for teams
  • co-host do hipsters.tech
  • microsoft mvp
  • @rla4
  • gosta de minúsculas

por que falar sobre insucessos

dnad '15 - https://www.youtube.com/watch?v=qP4Jb9UBLsQ

 

o que esperar dessa palestra

cases interessantes de outages com diferentes razões (e diferentes soluções)

como monitoramos, identificamos e resolvemos problemas

links pra ferramentas de monitoramento e métricas open source

alívio por saber que você não é a única que já derrubou um site em produção

  • desde 2008
  • 20 devs no core team
    • 10 no Stack Overflow for Teams
  • 54+ milhões de usuários únicos/mês

tech stack

  • .net core 3.1
  • c#
  • asp.net core
  • sql server
    • dapper, ef core
  • typescript
    • design system open source: stackoverflow.design
  • redis
  • elasticsearch
  • ha proxy

9 WEB SERVERS

2 SQL SERVERS

LIVE

HOT STANDBY

Stack Overflow

528 M queries/dia

~5% CPU

ferramentas úteis: OpServer

https://opserver.github.io/Opserver/

https://opserver.github.io/Opserver/

ferramentas úteis: metrics

https://github.com/StackExchange/StackExchange.Metrics

  • cliente .NET para reportar métricas
  • handlers para Bosun, SignalFx, statsd, etc
  • algumas métricas suportadas:
    • estatísticas de GC
      • gen0, gen1, gen2
    • asp.net core
      • req/s
      • failed requests
      • total requests
    • process
      • cpu
      • thread count
      • memória

falha #1: "escalando" migrations

falha #1: "escalando" migrations

  • novos Teams: +1000% nas primeiras 24h
  • CPU tinha picos de 100%
  • o que acontece quando uma build é executada?
    • migrations!

falha #1: "escalando" migrations

  • cada site tem uma tabela de Migrations executadas
  • arquivos .sql numerados
  • step no agente de build
    • varre lista de arquivos
    • se migration não existe no banco, executa
    • salva data de execução, nome e hash do arquivo na tabela
If fnColumnExists('PostVotes','IsInvalidated') = 0
Begin
    Alter Table PostVotes 
    	Add IsInvalidated bit Not Null 
        Constraint DF_PostVotes_IsInvalidated default(0)
End
migrations/987 - add IsInvalidated to PostVotes.sql

falha #1: "escalando" migrations

  • pra cada site "novo", antes de executar os scripts, checamos se a tabela Migrations existe:
IF NOT EXISTS(SELECT 1 FROM sys.tables WHERE [name] = 'Migrations')
BEGIN
	CREATE TABLE Migrations 
	(
		Id int identity primary key, 
		[Filename] nvarchar(260),
		[Hash] varchar(40), 
		[ExecutionDate] datetime,
		[Duration] int		
	)
	CREATE UNIQUE INDEX UQ_Filename ON Migrations([Filename])
	CREATE UNIQUE INDEX UQ_Hash ON Migrations([Hash])
END
  • o acesso a sys.tables é a parte interessante: aparentemente o sql server não gosta muito de milhares de schemas consultando essa view ao mesmo tempo, resultando em deadlocks
  • solução: mover lógica pra criar tabela de Migrations pra tempo de provisionamento, não de build

falha #2: "já sei, vou usar regex"

  • sintomas
    • CPU dos nossos web servers topou em 100%
    • servidores foram tirados de rotação pelo LB e marcados como "unhealthy"
    • homepage estava demorando muito pra responder
      • começamos por aqui! o que temos na homepage?

falha #2: "já sei, vou usar regex"

  • a homepage e o link de uma pergunta específica tinham tempos de resposta bem acima do normal
  • à primeira vista, a pergunta era inofensiva...

falha #2: "já sei, vou usar regex"

public static string TrimUnicode(this string s)
{
    if (s.IsNullOrEmpty()) return s;
    
    // see http://en.wikipedia.org/wiki/Zero-width_non-joiner
    s = Regex.Replace(s, @"^[\s\u200c]+|[\s\u200c]+$", "");
    return s;
}
  • ... mas ela tinha 30 MIL caracteres em branco na descrição
  • código usado pra "normalizar" posts e comentários
  • faz "trim" de uma string, removendo inclusive caracteres em  branco unicode
  • mas o que acontece em strings com muitos espaços em branco no "meio"?
    • backtracking!

falha #2: "já sei, vou usar regex"

  • a pergunta entrou nos "hot network posts", sendo renderizada em nossa homepage
  • a homepage é também usada pra health checks. oops
  • a gente se auto-derrubou por 34 minutos
  • solução: substituir a regex com substring
/// <summary>
/// like .Trim() but MORE AWESOME because it removes unicode fake spaces too
/// </summary>
public static string TrimUnicode(this string s)
{
    if (s.IsNullOrEmpty()) return s;
    var start = 0;
    var len = s.Length;
    while (start < len && (char.IsWhiteSpace(s[start]) || s[start] == '\u200c'))
    	start++;
    var end = len - 1;
    while (end >= start && (char.IsWhiteSpace(s[end]) || s[end] == '\u200c'))
    	end--;
    if (start >= len || end < start)
    	return "";
    return s.Substring(start, end - start + 1);
}

falha #3: migrando pra EF Core

  • em outubro de 2018, migramos de Linq2SQL para EF Core
  • sintomas:
    • sites caíram!
    • no log de top queries, vimos um suspeito "select * from posts" 
      • (yay, SQL Server é super eficiente!)
    • mesmo usando só 4% de CPU, nossos servers de banco saturaram a rede (trazendo todos os posts do banco pros servidores de aplicação), e olha que sao 2x10Gb links

falha #3: migrando pra EF Core

  • uma de nossas consultas fazia um filtro com um new Guid
  • Linq2SQL traduzia isso pra uma query SQL, trazendo ~50 resultados
  • EF Core (via client side evaluation) não sabia traduzir pra SQL, então fazia a query sem filtro e filtrava em memória, trazendo... 140 milhões de resultados
var state = 
db.SessionStates.Single(u => u.GUID == new Guid(sessionGuid));
// EF Core won't translate this inline...
var guidParam = new Guid(sessionGuid);
var state = 
db.SessionStates.Single(u => u.GUID == guidParam);

falha #3: migrando pra EF Core

As a general rule, Entity Framework Core attempts to evaluate a query on the server as much as possible. EF Core converts parts of the query into parameters, which it can evaluate on the client side. The rest of the query (along with the generated parameters) is given to the database provider to determine the equivalent database query to evaluate on the server. EF Core supports partial client evaluation in the top-level projection (essentially, the last call to Select()). If the top-level projection in the query can't be translated to the server, EF Core will fetch any required data from the server and evaluate remaining parts of the query on the client. If EF Core detects an expression, in any place other than the top-level projection, which can't be translated to the server, then it throws a runtime exception. See How queries work to understand how EF Core determines what can't be translated to server.

https://docs.microsoft.com/en-us/ef/core/querying/client-eval

falha #bônus: tamagotchi DDoS

https://github.com/StackExchange/stackegg

https://stackstatus.net/post/115305251014/outage-postmortem-march-31-2015

essa palestra é sobre o quê mesmo?

erros acontecem (e nos tornam desenvolvedores melhores!)

 

observabilidade é importante

 

cuidado com deadlocks

obrigada :)

@rla4

rla4.com