Re-arquitetando o Stack Overflow

ou como construímos o Stack Overflow for Teams

Roberta Arcoverde

/whois 

  • recifense
  • programadora há 15 anos
  • principal software developer na stack overflow
  • co-host do hipsters.tech
  • @rla4
  • desde 2008
  • 50+ milhões de usuários únicos/mês
  • 18 milhões de perguntas
  • 27 milhões de respostas
  • top 50 sites mais acessados do mundo
  • 3k Teams criados, 50k usuários
  • 10 meses em desenvolvimento
  • lançado em maio/2018
  • equipe tinha originalmente 3 devs, agora são 7
    • melhor nome de time da história: Teams Team 😎

https://stackoverflow.com/c/demo

https://stackoverflow.com

>170 sites

números do dia 03/05

  • 278.912.108 HTTP requests
  • 67.188.355 page views
  • 3.506.670.995.363 bytes (3.5 TB) enviados
  • 953.860.308 SQL queries executadas
  • 5.250.697.564 redis hits
  • 600.000 websockets ativos
  • 19ms de tempo de renderização da Question page
    • 54.290.431 page views, ou 80% do total
  • 123ms de tempo de renderização geral

9 WEB SERVERS

4 SQL SERVERS

LIVE

HOT STANDBY

LIVE

HOT STANDBY

Stack Exchange, Meta, Talent

Stack Overflow

~350 req/s

por servidor

528 M queries/dia

498 M queries/dia

~5% CPU

imagem gentilmente cedida por Marco (@sklivvz) em http://www.slideshare.net/howtoweb/marco-cecconi-stack-overflow-architecture

como?

 

spoilers: é boring

performance é uma feature

tech stack

  • c#
  • asp.net mvc*
  • sql server
    • dapper, ef core
  • typescript
    • vanilla
  • redis
  • elasticsearch
  • ha proxy

*migrando pra .NET Core

🤷‍♀️

multi tenant application

  • um único app pool para todos os sites
  • roteado via host headers

Q&A pra dados privados?

(o nome original do SO for Teams era Channels)

nasce uma ideia! (sim, o screenshot é legítimo)

times são sites que existem dentro do Stack Overflow

tratá-los como se fossem novos sites na rede, porém visíveis apenas a partir do 

public class Post {

    public int Id { get; }
    public string Title { get; }
    
    public int? TeamId { get; }
    ...
}

// reusar banco
// criar novo código
public class Post {

    public int Id { get; }
    public string Title { get; }
    ...
}



// criar novo banco
// reusar código
[StackRoute("help/search-inline")]
public async Task<ActionResult> SearchInline(string q)
{
    var searchSite = GetSearchSite();
    var results = await searchSite.HelpPostIndex.SearchAsync(searchSite, q);
    var sm = new SearchModel
    {
        SearchString = q,
        Results = results
    };

    return PartialView("~/Views/Help/SearchInline.cshtml", sm);
}

https://stackoverflow.com/help/search-inline

https://askubuntu.com/help/search-inline

https://stackoverflow.com/c/demo/help/search-inline

Modelo

Segurança

Escalabilidade

evitar forks, DRY, minimizar alterações no core do projeto

default private, mudança de mindset, crash na aplicação > vazamento de dados

capacity planning, o que acontece se tivermos 1k, 10k, 100k times?

👍

  • Bases isoladas entre Teams
  • Dados isolados dos dados públicos
  • Mínimo de alterações no código (usar modelo existente pra novos sites)

👎

  • Escalabilidade. AG distribuídos começam a degradar rapidamente a partir de 1k bancos

  • Hardware e instrumentação para gerenciar milhares de bases de dados

Plano A: um banco para cada Team

👍

  • Escalabilidade
  • Dados isolados dos dados públicos

👎

  • Sem isolamento entre Teams
  • Reescrever boa parte das consultas
  • Consultas não são mais as mesmas para sites vs Teams

Plano B: um banco para todos os Teams

👍

  • Dados isolados entre Teams
  • Dados isolados dos dados públicos
  • Escalabilidade é... decente

  • Baixo custo de reescrita

👎

  • Precisamos escrever infra de provisionamento dinâmico

Plano C: um schema por time no mesmo banco

basicamente: saindo de 170 para 10k+ sites

  • SQL Server
    • 1 banco per-site
    • 1 banco pra todos os Teams, 1 schema per-Team
  • Elasticsearch
    • 1 índice per-site
    • 1 índice per-team, até 5k
  • Provisionamento
    • tarefa agendada cria sempre um buffer de 100 schemas para futuros Teams

Escalabilidade

  • onde manter os dados dos Teams?
  • como comunicar o site público com o Team?
  • migrar *tudo* pra lugares seguros
    • notificações
    • emails
    • monitoramento
    • internal API
    • websockets
    • tags

Segurança

como as redes se comunicam?

Proxying

  • Já usávamos no /jobs
  • Requisição é "clonada" e enviada para a CFZ
  • Response é jogada direto no stream de saída
  • 800 LoC
  • Por que não usar APIs/serviços?
    • custo de serialização
    • mais código, menos uniformidade
[StackRoute("c/{slug}")]
[StackRoute("c/{slug}/{*pathInfo}")]
public async Task<ActionResult> Proxy(string slug)
{
    if (!Current.Settings.Channels.Enabled)
    {
        return PageNotFound();
    }    
    ...
    if (Current.Request.IsProxied())
    {
        // yo dawg, I heard you like proxies so we put a proxy in your proxy
        // so you can channel yo inner channels... Let's not allow this
        return PageNotFound();
    }

    var returnUrl = Current.Request.Url.PathAndQuery;
    if (!Current.SiteChannels.Contains(channelSite.Id))
    {
        // user does not have access to this channel
        return RedirectToJoinPage();
    }
    ...

    return await this.BlindProxy(channelSite, path);
}

// BlindProxy: 
//    valida a requisição (authorization);
//    constrói um Request;
//    envia via HTTP para o Team app;
//    retorna o resultado
//    profit :D




    // No, you can't:
    // - Use a CookieCollection (it'll get headers, but not pass them here)
    // - Set the Set-Cookie header on the response (ASP.Net strips it)
    // - Set an additional Set-Cookie (also stripped)
    // - Take the raw header and pass it (comma delimited, only the first cookie will set)
    // - Use Headers.GetValues(string) (it screws up on commas)
    // - Maintain your sanity working with ASP.Net and cookie headers
    // Fun fact: half of the cookie BS here is supporting IIS6 and IE5. Not kidding.
    if (cResponse.Headers["Set-Cookie"].HasValue())
    {
        var nvc = cResponse.Headers;
        var result = new List<string>();
        for (var i = 0; i < nvc.Count; i++)
        {
            if (nvc.GetKey(i) == "Set-Cookie")
            {
                // Don't ask. You'll cry.
                var vals = nvc.GetValues(i);
                if (vals != null) result.AddRange(vals);
            }
        }
        // ...
    }




lições

  • entenda seus cenários de escalabilidade
  • quando não souber: capacity planning
  • segurança vai além de proteger dados de acesso externo

outras palestras

  • instrumentação
    • adaptamos todos os nossos sistemas de monitoramento pra incluir Teams
  • proxy v2
    • protobuf
    • grpc
    • structured model
  • single sign-on
    • re-arquitetando o modelo de autenticação e autorização
  • modelo de segurança
    • dados (perguntas, respostas, tags)
    • metadados (traffic logs, IPs, urls)
    • external endpoints (ads, APIs, emails)

obrigada!

rla4

roberta at stackoverflow.com

rla4.com

hipsters.tech

  • instância privada, standalone do Stack Overflow
  • SLA, priority support
  • single sign-on
  • on premise ou Azure
  • releases trimestrais
  • completamente customizável
  • apropriado para grandes empresas
  • $$$

QCon SP 2019

By rla4

QCon SP 2019

  • 302