Re-arquitetando o Re-arquitetando o Stack Overflow Stack Overflow
- u como construímos o Stack Overflow for Teams
Roberta Arcoverde
1
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
Roberta Arcoverde
1
recifense programadora há 15 anos principal software developer na stack
co-host do hipsters.tech @rla4
2
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
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
https://stackoverflow.com/c/demo
5
https://stackoverflow.com/c/demo
5
https://stackoverflow.com/c/demo
5
https://stackoverflow.com/c/demo
5
https://stackoverflow.com/c/demo
5
https://stackoverflow.com/c/demo
5
https://stackoverflow.com
6
https://stackoverflow.com
6
7
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
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
imagem gentilmente cedida por Marco (@sklivvz) em http://www.slideshare.net/howtoweb/marco-cecconi-stack-overflow-architecture
10
11
spoilers: é boring
12
13
c# asp.net mvc* sql server dapper, ef core typescript vanilla redis elasticsearch ha proxy *migrando pra .NET Core
14
15
♀
15
um único app pool para todos os sites roteado via host headers
16
17
https://nickcraver.com/blog/2016/02/03/stack-overflow-a-technical-deconstruction/
18
19
(o nome original do SO for Teams era Channels)
nasce uma ideia! (sim, o screenshot é legítimo)
20
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
[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
evitar forks, DRY, minimizar alterações no core do projeto
23
evitar forks, DRY, minimizar alterações no core do projeto capacity planning, o que acontece se tivermos 1k, 10k, 100k times?
23
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
Teams Dados isolados dos dados públicos Mínimo de alterações no código (usar modelo existente pra novos sites)
distribuídos começam a degradar rapidamente a partir de 1k bancos Hardware e instrumentação para gerenciar milhares de bases de dados
24
Dados isolados dos dados públicos
Teams Reescrever boa parte das consultas Consultas não são mais as mesmas para sites vs Teams
25
Teams Dados isolados dos dados públicos Escalabilidade é... decente Baixo custo de reescrita
infra de provisionamento dinâmico
26
27
28
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
29
como comunicar o site público com o Team? migrar *tudo* pra lugares seguros notificações emails monitoramento internal API websockets tags
30
31
32
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
[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
[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
[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
[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
[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
// 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
entenda seus cenários de escalabilidade quando não souber: capacity planning segurança vai além de proteger dados de acesso externo
36
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
rla4 roberta at stackoverflow.com rla4.com hipsters.tech
38
instância privada, standalone do Stack Overflow SLA, priority support single sign-on
releases trimestrais completamente customizável apropriado para grandes empresas $$$
39