Re-arquitetando o Re-arquitetando o Stack Overflow Stack Overflow - - PowerPoint PPT Presentation

re arquitetando o re arquitetando o stack overflow stack
SMART_READER_LITE
LIVE PREVIEW

Re-arquitetando o Re-arquitetando o Stack Overflow Stack Overflow - - PowerPoint PPT Presentation

Re-arquitetando o Re-arquitetando o Stack Overflow Stack Overflow ou como construmos o Stack Overflow for Teams Roberta Arcoverde 1 /whois /whois recifense programadora h 15 anos principal software developer na stack overflow


slide-1
SLIDE 1

Re-arquitetando o Re-arquitetando o Stack Overflow Stack Overflow

  • u como construímos o Stack Overflow for Teams

Roberta Arcoverde

1

slide-2
SLIDE 2

/whois /whois

recifense programadora há 15 anos principal software developer na stack

  • verflow

co-host do hipsters.tech @rla4

2

slide-3
SLIDE 3

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

3

slide-4
SLIDE 4

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

4

slide-5
SLIDE 5

https://stackoverflow.com/c/demo

5

slide-6
SLIDE 6

https://stackoverflow.com/c/demo

5

slide-7
SLIDE 7

https://stackoverflow.com/c/demo

5

slide-8
SLIDE 8

https://stackoverflow.com/c/demo

5

slide-9
SLIDE 9

https://stackoverflow.com/c/demo

5

slide-10
SLIDE 10

https://stackoverflow.com/c/demo

5

slide-11
SLIDE 11

https://stackoverflow.com

6

slide-12
SLIDE 12

https://stackoverflow.com

6

slide-13
SLIDE 13

>170 sites >170 sites

7

slide-14
SLIDE 14

números do dia 03/05 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

8

slide-15
SLIDE 15

9 WEB SERVERS 4 SQL SERVERS

LIVE HOT STANDBY LIVE HOT STANDBY

9

Stack Exchange, Meta, Talent Stack Overflow

~350 req/s

por servidor

528 M

queries/dia

498 M

queries/dia

~5% CPU

slide-16
SLIDE 16

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

10

slide-17
SLIDE 17

11

slide-18
SLIDE 18

como? como?

spoilers: é boring

12

slide-19
SLIDE 19

performance performance é uma é uma feature feature

13

slide-20
SLIDE 20

tech stack tech stack

c# asp.net mvc* sql server dapper, ef core typescript vanilla redis elasticsearch ha proxy *migrando pra .NET Core

14

slide-21
SLIDE 21

15

slide-22
SLIDE 22

15

slide-23
SLIDE 23

multi tenant application multi tenant application

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

16

slide-24
SLIDE 24

17

slide-25
SLIDE 25

https://nickcraver.com/blog/2016/02/03/stack-overflow-a-technical-deconstruction/

18

slide-26
SLIDE 26

Q&A pra dados Q&A pra dados privados? privados?

19

slide-27
SLIDE 27

(o nome original do SO for Teams era Channels)

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

20

slide-28
SLIDE 28

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

21

slide-29
SLIDE 29

[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); } 1 2 3 4 5 6 7 8 9 10 11 12 13

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

22

slide-30
SLIDE 30

Modelo Modelo

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

23

slide-31
SLIDE 31

Modelo Modelo Escalabilidade Escalabilidade

evitar forks, DRY, minimizar alterações no core do projeto capacity planning, o que acontece se tivermos 1k, 10k, 100k times?

23

slide-32
SLIDE 32

Modelo Modelo Segurança Segurança Escalabilidade 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?

23

slide-33
SLIDE 33
  • 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 Plano A: um banco para cada Team cada Team

24

slide-34
SLIDE 34
  • 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 Plano B: um banco para todos os Teams todos os Teams

25

slide-35
SLIDE 35
  • 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 Plano C: um schema por time no mesmo banco time no mesmo banco

26

slide-36
SLIDE 36

27

slide-37
SLIDE 37

28

slide-38
SLIDE 38

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 Escalabilidade

29

slide-39
SLIDE 39
  • nde 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 Segurança

30

slide-40
SLIDE 40

31

slide-41
SLIDE 41

como as redes se como as redes se comunicam? comunicam?

32

slide-42
SLIDE 42

Proxying 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

33

slide-43
SLIDE 43

[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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

34

slide-44
SLIDE 44

[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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [StackRoute("c/{slug}")] [StackRoute("c/{slug}/{*pathInfo}")] 1 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33

34

slide-45
SLIDE 45

[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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [StackRoute("c/{slug}")] [StackRoute("c/{slug}/{*pathInfo}")] 1 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33 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(); } [StackRoute("c/{slug}")] 1 [StackRoute("c/{slug}/{*pathInfo}")] 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 10 11 12 13 14 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33

34

slide-46
SLIDE 46

[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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [StackRoute("c/{slug}")] [StackRoute("c/{slug}/{*pathInfo}")] 1 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33 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(); } [StackRoute("c/{slug}")] 1 [StackRoute("c/{slug}/{*pathInfo}")] 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 10 11 12 13 14 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33 return await this.BlindProxy(channelSite, path); [StackRoute("c/{slug}")] 1 [StackRoute("c/{slug}/{*pathInfo}")] 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33

34

slide-47
SLIDE 47

[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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [StackRoute("c/{slug}")] [StackRoute("c/{slug}/{*pathInfo}")] 1 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33 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(); } [StackRoute("c/{slug}")] 1 [StackRoute("c/{slug}/{*pathInfo}")] 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 10 11 12 13 14 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33 return await this.BlindProxy(channelSite, path); [StackRoute("c/{slug}")] 1 [StackRoute("c/{slug}/{*pathInfo}")] 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 25 } 26 27 // BlindProxy: 28 // valida a requisição (authorization); 29 // constrói um Request; 30 // envia via HTTP para o Team app; 31 // retorna o resultado 32 // profit :D 33 // BlindProxy: // valida a requisição (authorization); // constrói um Request; // envia via HTTP para o Team app; // retorna o resultado // profit :D [StackRoute("c/{slug}")] 1 [StackRoute("c/{slug}/{*pathInfo}")] 2 public async Task<ActionResult> Proxy(string slug) 3 { 4 if (!Current.Settings.Channels.Enabled) 5 { 6 return PageNotFound(); 7 } 8 ... 9 if (Current.Request.IsProxied()) 10 { 11 // yo dawg, I heard you like proxies so we put a proxy in your proxy 12 // so you can channel yo inner channels... Let's not allow this 13 return PageNotFound(); 14 } 15 16 var returnUrl = Current.Request.Url.PathAndQuery; 17 if (!Current.SiteChannels.Contains(channelSite.Id)) 18 { 19 // user does not have access to this channel 20 return RedirectToJoinPage(); 21 } 22 ... 23 24 return await this.BlindProxy(channelSite, path); 25 } 26 27 28 29 30 31 32 33

34

slide-48
SLIDE 48

// 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 wil // - 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); } } // ... }

35

slide-49
SLIDE 49

lições 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

36

slide-50
SLIDE 50
  • utras palestras
  • utras 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)

37

slide-51
SLIDE 51
  • brigada!
  • brigada!

rla4 roberta at stackoverflow.com rla4.com hipsters.tech

38

slide-52
SLIDE 52

instância privada, standalone do Stack Overflow SLA, priority support single sign-on

  • n premise ou Azure

releases trimestrais completamente customizável apropriado para grandes empresas $$$

39