APIs e Microsserviços

16 set, 2016

Mergulhando fundo na API da Riot Games

Publicidade

Olá a todos, sou Leigh Estes, mais conhecido como RiotSchmick. Eu sou um engenheiro de software da Riot Games trabalhando na iniciativa Service Availability. O nosso primeiro artigo sobre a API cobriu nossos objetivos, as responsabilidades das equipes Developer Relations e Developer Platform no ecossistema API, e alguns detalhes de alto nível sobre a tecnologia que usamos na construção de nossa solução de API. A comunidade League of Legends criou muitas ferramentas úteis e sites no backend da API Riot Games, que vão desde sites de estatísticas, como LoLKing.com e OP.GG, até sites que fornecem aos jogadores características complementares, como replay.gg. Neste artigo, como prometido, vamos nos aprofundar na tecnologia e na arquitetura da plataforma API Riot Games, no que se refere à infraestrutura de ponta, e não aos serviços de proxy subjacentes. Primeiro, vou delinear algumas das escolhas tecnológicas fundamentais que fizemos quando decidimos construir uma plataforma API. Então, eu poderei mergulhar em componentes específicos que construímos como parte dessa plataforma. Em seguida, vou dar uma visão geral das tecnologias utilizadas para construir os componentes. No final, eu vou cobrir como nossa infraestrutura é implantada na AWS.

Escolhas fundamentais

Como mencionado em nosso artigo anterior, nós inicialmente usamos plataformas de gerenciamento API existentes, como apiGrove e Repose, mas decidimos construir a nossa própria solução que poderia ser personalizado para funcionar com todas as diferentes regiões, parceiros e tecnologias no ecossistema Riot Games. Depois de fazer alguma pesquisa sobre que soluções de código aberto estavam disponíveis para suportar o tráfego na nossa escala, decidimos utilizar o servidor proxy Zuul desenvolvido pela Netflix, bem como alguns outros projetos da Netflix, incluindo Archaius, Ribbon, Hystrix e Eureka. As pessoas inteligentes e talentosas da Netflix construiram sua infraestrutura para lidar com uma enorme quantidade de tráfego constante e com alta largura de banda, portanto, sabíamos que podíamos confiar que o código foi massivamente testado, além de ter sido testado em produção. O Netflix Zuul fornece uma dependência de núcleo JAR que lida com solicitações HTTP e executa cadeias de filtro. Usando essa dependência como base, nós escrevemos algumas classes adicionais para incluir os recursos que necessitávamos, como a verificação automatizada de saúde, gestão de pool de conexão e monitoramento, processamento de métricas etc. No topo desse servidor proxy núcleo, implementamos filtros personalizados para lidar com coisas como autorização, autenticação, limite de taxa, roteamento dinâmico, balanceamento de carga, coleta de métricas, lista branca, lista negra, e muito mais. O núcleo do servidor proxy, escrito em Java, e filtros personalizados, escrito em Groovy, são construídos em um WAR e implantados na produção em execução no container Jetty servlet. Os componentes adicionais da nossa arquitetura de núcleo, com os quais nossos proxies Zuul se comunicam, são serviços distintos que lidam com limitação de taxa, lista negra, e coleta de métricas e processamento.

Edge Service Rate Limiter

Edge Service Rate Limiter, pronunciado Ezreal, lida com a limitação de velocidade e funções de lista negra. Ele fornece dois endpoints, um que usa algo semelhante a um algoritmo de leaky bucket para gerenciar os limites de velocidade para as chaves de API, e que verifica se uma chave de API está na lista negra com base no número de violações de limite de taxa que tenha incorrido ao longo de um determinado período de tempo. Uma meta com o ESRL foi manter os tempos de resposta incrivelmente rápidos para que nós não tivéssemos gargalo em nossos pedidos durante a tentativa de fazer o gerenciamento de limitação da taxa. Alcançamos esse objetivo com ambos os endpoints que têm tempos de resposta, em média, com menos de 2 ms, como mostrado no gráfico abaixo, usando Redis para todas as operações do ESRL. Cada endpoint executa um script Lua em uma única chamada para Redis para executar todas as operações desejadas em uma conexão e, em seguida, utiliza a resposta script para determinar o que deve retornar para o cliente.

riot-1Métricas Riot API

O serviço de métricas da API da Riot lida com coleta e tratamento de métricas. Tal como acontece com ESRL, queríamos ter certeza de que a coleta de métricas não teria gargalo em nossos pedidos. Para ajudar nesse objetivo, nós dissociamos a coleção de métricas em tempo real a partir de solicitações HTTP síncronas para Zuul. Os pedidos para o proxy desencadeiam um filtro de métricas que simplesmente adiciona as informações de métricas a uma fila na memória. Podemos no futuro mudar essa fila de execução em Redis ou algum outro cache fora da memória, mas inicialmente queríamos limitar as dependências do Zuul em tempo de execução do serviço REST apenas para chamadas, sem construção de terceiros que não as dependências de bibliotecas centrais do Netflix. Com um pool de threads para processar a fila, conseguimos com sucesso mantê-la na memória por enquanto. Em cada thread no pool é executado um loop que puxa um item de métrica para fora de uma coleção thread-safe, submete ao serviço de métricas da API da Riot e, se a chamada falhar, o readiciona à coleção do thread-safe para que ele possa ser reprocessado mais tarde. Se não houver métricas na fila, a thread vai dormir para evitar um loop ocupado que consome ciclos de CPU. O serviço de métricas da API da fornece um endpoint para postar informações de métricas, que simplesmente o adiciona a uma fila no Redis e, em seguida, retorna imediatamente para o cliente. Enquanto esses pedidos estão sendo apresentados de forma assíncrona pelo Zuul, ainda queremos que os tempos de resposta sejam os mais rápidos possíveis para que o tamanho da fila em-memória não cresça sem limites para que o processamento possa alcançar a taxa de transferência. As médias desse endpoint quanto ao tempo de resposta são menores que 2 ms também, como mostrado no gráfico abaixo. O serviço de métricas também tem um pool de threads que executa processamento em segundo plano para ler itens da fila, agrupar métricas, e salvar os dados de algumas maneiras diferentes em um banco de dados MySQL para que possamos fazer consultas relacionais a ele.

riot-2

Portal do desenvolvedor

Como mencionado em nosso artigo anterior, construímos um portal de desenvolvedor público para facilitar a interação e conversas em curso com desenvolvedores de terceiros. A fim de simplificar o desenvolvimento, aproveitando um framework MVC existente, nós construímos o portal do desenvolvedor como uma aplicação Grails escrita em Java e Groovy que roda em Tomcat e utiliza Bootstrap para o front-end. Quando os usuários fazem o primeiro login no portal do desenvolvedor, uma conta de desenvolvedor é criada para eles. Um aplicativo de desenvolvimento, com uma chave de API associada, é criada automaticamente para eles e é associado à sua conta. Se um limite de taxa mais elevado é necessário para um aplicativo de produção, os usuários podem registrar o seu projeto para o acesso a uma chave de API de produção. Aplicações estão associadas a uma camada, que é uma taxa de definição de limites. Por exemplo, uma camada de demonstração pode ser definida como 10 chamadas por 10 segundos e 500 chamadas por 10 minutos. Tendo ambos os limites da frequência vinculados a um único nível significa que a camada pode gerenciar chamadas em rajadas e contínuas. Os aplicativos também estão ligados a políticas, que definem um conjunto de endpoints de API que um aplicativo pode acessar. Além dessa configuração do aplicativo, a plataforma de API depende da configuração que descreve os serviços disponíveis para proxy e seus endpoints físicos (isto é, endereços IP ou DNS). Uma UI administrativa permite configurar o serviço, as APIs que oferece, e os endpoints físicos que servem essas APIs. As APIs podem ser adicionadas ou removidas de políticas, e políticas de aplicação e camadas das aplicações podem ser modificadas também. Atualmente, na produção, a configuração de níveis, políticas, serviços, endpoints e APIs é feita manualmente. No futuro, estaremos fazendo a transição para um framework que usa a descoberta de serviço para automatizar grande parte dessa configuração até o limite.

Configuração de propagação

Todos os dados de configuração acima descritos para a plataforma de API, incluindo contas geradas automaticamente, aplicações e chaves, bem como os níveis configurados manualmente, políticas, serviços, endpoints e APIs, são armazenados em um esquema em um banco de dados MySQL. Um serviço leve chamado de API Configuration Service é responsável por fornecer informações de configuração, como uma lista de chaves de API válida, uma lista de URIs que o servidor de ponta deve considerar válido, os endpoints físicos a que essas URIs devem ser encaminhadas, a lista de URIs em cada política, as definições de cada limite de camada de taxa etc.

Tanto Zuul quanto ESRL precisavam ler essas propriedades regularmente e alterar dinamicamente seus valores. O projeto Netflix Archaius foi escrito para fornecer exatamente essa funcionalidade, na qual uma lista de fontes de propriedade podem ser sondadas para atualizar as propriedades dinâmicas na memória, então isso foi um ajuste perfeito e nós o integramos ao ESRL e ao Zuul. Inicialmente, nós configuramos as fontes de propriedade do Archaius para bater nos endpoints REST fornecidos pelo API Configuration Service que devolveu as propriedades relevantes. No entanto, uma série de questões combinadas tornam esse mecanismo não confiável para atualizações de propriedades dinâmicas frequentes. Primeiro, o número de itens de configuração no banco de dados cresceu ao longo do tempo, à medida que nossa plataforma cresceu em escopo e base de usuários. Isso se traduz para consultas de banco de dados cada vez mais longas, bem como respostas cada vez maiores do terminal REST. Há, por vezes, problemas de rede dentro de nosso VPC, o que tornou as frequentes tentativas de baixar grandes arquivos ainda mais problemáticas.

Em última análise, tornou-se muito pouco fiável e demorado transferir as propriedades por meio de um endpoint REST através da rede interna. Assim, decidimos que precisávamos de uma solução do tipo CDN na qual poderíamos baixar arquivos grandes de forma confiável pela Internet a partir de um limite geograficamente próximo. S3 forneceu exatamente o que precisávamos, então nós atualizamos API Configuration Service para ler as informações do banco de dados e escrever as principais informações em arquivos de propriedades em buckets S3. Então Zuul e ESRL foram atualizados para que as suas fontes de propriedade Archaius apontassem para os buckets S3 em vez do terminal API Configuration Service do REST. Esse modelo se provou confiável e de alto desempenho.

Visão geral da tecnologia

riot-3

ESRL, Métricas Riot API e API Configuration Service são escritos em Java. Inicialmente, eles foram escritos usando um framework chamado dropwizard, que permite executar serviços Jersey REST como um executável JAR sem a necessidade de implementar em um container servlet usando Jetty incorporado. Recentemente, nós reimplantamos esses serviços utilizando um framework construído internamente que chamamos de Hermes. Em um artigo anterior, Cam Dunn discutiu como a Riot utiliza um processo de RFC para o projeto técnico. Hermes é a implementação de uma especificação chamada Ambassador que começou como um RFC (na verdade, a captura de tela na parte superior do artigo dele mostra os RFCs Ambassador!). Reimplementar nossos projetos usando uma tecnologia como Hermes, que foi construída em casa e tem sido amplamente adotada, oferece vários benefícios, incluindo o seguinte.

  1. Quando surgem erros ou problemas de desempenho, temos o suporte interno para nos ajudar.
  2. Como os usuários que têm uma participação no Hermes trabalham bem, podemos contribuir de volta para a base de código, e ajudar a todos na Riot, tornando-a ainda melhor.
  3. Quando outros desenvolvedores precisam olhar para os nossos sistemas ou código, estamos usando tecnologia comum, o que lhes dá uma vantagem para compreender o que está acontecendo.
  4. Como os usuários têm contribuído para a base de código regularmente e têm acesso direto aos autores originais, nós entendemos melhor a nós mesmos quando rastreamos problemas.

Nós já encontramos bugs e problemas de desempenho relativos ao Hermes, os corrigimos, enviamos pull requests e coordenamos novos lançamentos de todo o framework por conta própria desde a sua adoção. Assim, sentimos os dividendos que vieram da adoção do framework, o que valeu a pena para a nossa equipe e para a Riot como um todo.

Mencionei anteriormente que aproveitamos Hystrix em nossa ponta. Essa tecnologia fornece a capacidade de detectar quando chamadas para um serviço subjacente estão lentas, tal como definido por um limite configurado, e curto-circuito em todas as chamadas para esse serviço, imediatamente retornando um 503 sem tentar procurar a chamada. Nós fizemos algum trabalho para colocar o Hystrix integrado ao nosso código Zuul customizado, mas nunca conseguimos fazer com que ele funcionasse de forma satisfatória, por isso ainda não o implementamos em produção. Ainda está em nosso mapa integrar o Hystrix corretamente ou implementar a nossa própria solução similar. Nossa configuração da plataforma de API permite a configuração de tempos de espera para conectar e ler os os serviços de proxy para evitar serviços consistentemente lentos e degradar o desempenho na ponta. No entanto, um mecanismo de curto-circuito como o descrito acima reduziria ainda mais o efeito dos serviços consistentemente lentos, impedindo que os pedidos afetados mantivessem o trabalhador Jetty threads ocupado ou fazendo backup do Jetty queue.

Nós inicialmente usamos Ribbon para fazer o balanceamento de carga do software através de um grupo de servidores para cada serviço dos proxies Zuul. Quando reformulamos os nossos projetos dropwizard para executar como servidores Hermes, também reformulamos o Zuul para usar o cliente Hermes, em vez de Ribbon para fazer o balanceamento de carga do software. Da mesma forma, inicialmente usamos Archaius para lidar com toda a configuração dinâmica, incluindo a configuração da plataforma de API discutida anteriormente, bem como a configuração do aplicativo específico do ambiente para o proxy. Recentemente, decidimos mudar para outra solução in-house chamada Configurous para a configuração da aplicação específica do ambiente. Já enumerei acima os benefícios de alavancar nossas próprias tecnologias, por isso não vou falar novamente aqui. Como o Hermes, o Configurous foi conceituado no processo de RFC e acabou por ser implementado e adotado por muitas equipes de toda a empresa. A intenção do Configurous é gerenciar facilmente a configuração de aplicativos em diferentes ambientes, incluindo atualizações dinâmicas. No entanto, já que o escopo do Configurous é gerenciar a configuração do aplicativo, ele suporta apenas pequenos valores de propriedade e alterações relativamente pouco frequentes para esses valores. Assim, continuamos a usar Archaius para as grandes e continuamente alteradas propriedades que definem a configuração da plataforma de API.

Por último, integramos o proxy com Eureka, que havia sido definido como parte do ecossistema “Stack” da Riot e estava sendo colocado em todos os ambientes. Essa integração nos permitiu começar a transição anteriormente mencionada para autodescobrir e configurar serviços na ponta. Além da autodescoberta de serviços subjacentes que querem estar no proxy, ele permite que cada proxy autolocalize o ESRL e os serviços de métricas da API da Riot com as quais devem ter comunicação. Essa funcionalidade está atualmente apenas testada e ativada em ambientes internos, mas funciona bem e economiza um monte de etapas manuais como caixas que são giradas para cima e para baixo. Eureka foi eventualmente substituído por outra tecnologia da Riot chamada Discoverous. Discoverous foi outro produto do processo RFC e sua implementação foi baseada em Eureka, mas acrescentou algumas funcionalidades personalizadas que o complexo ecossistema da Riot tinha necessidade. É totalmente compatível com Eureka, portanto nenhuma alteração foi exigida do nosso lado quando a mudança foi feita.

Implantação AWS

Os servidores proxy Zuul são implantados em instâncias da AWS 3 VPCs em Tóquio, Irlanda e NorCal. Existem 4 instâncias do servidor proxy, 2 em cada zona de disponibilidade em cada VPC atrás de um ELB. Cada VPC também tem um conjunto de instâncias para as métricas da API da Riot e ESRL que os Zuuls usa na VPC. O serviço de métricas da API da Riot e a ESRL usam cada uma seu próprio Redis, uma instância AWS ElasticCache, em cada VPC. O serviço de métricas da API da Riot em NorCal é considerada a métrica “mestre” e tem o suporte de um banco de dados MySQL, uma instância AWS RDS. O serviço de métricas da API da Riot na Irlanda e em Tóquio são consideradas “escravas” e quando as suas threads executam a etapa de persistência para salvar os dados de métricas, eles fazem isso através de uma chamada REST para o mestre com o DTO serializado em JSON. O mestre, em seguida, salva os dados no banco de dados. Esse modelo nos permite salvar todos os dados de métricas de todas as regiões em um único banco de dados no qual podemos consultar. Quando o mestre executa a etapa de persistência, ele salva os DTOs diretamente no banco de dados. O API Configuration Service é executado somente no NorCal VPC e grava os arquivos necessários em buckets S3 em todas as regiões. Cada Zuul e ESRL lê a partir do bucket S3 na sua própria região para reduzir a latência.

riot-4

Conclusão

O serviço de ponta e a infraestrutura relacionada tem evoluído continuamente ao longo dos anos desde que a API foi lançada. Fizemos melhorias no desempenho e confiabilidade, amadurecemos a tecnologia de ligar as diferentes peças, e a produção melhorou toda a arquitetura. Daqui para frente, a infraestrutura de ponta será implantada em todos os data centers da Riot para ajudar a gerenciar ACLs e limites de taxa, fornecer um ponto único de fácil acesso para todas as APIs sendo compartilhadas entre os serviços, e alavancar todas as outras características e benefícios que os serviços de ponta fornecem. À medida que liberamos o framework para permitir que a ponta autodescubra e configure os serviços, ela se torna um mecanismo ainda mais poderoso para permitir que os serviços falem uns com os outros de uma forma controlada, previsível e confiável. Esse framework permitirá que as equipes se movam ainda mais rápido e concentrem-se em fornecer o valor do negócio em vez de terem que se preocupar com como resolver os mesmos problemas repetidamente quando eles escrevem novos serviços.

Isso nasceu como parte de uma solução focada para o problema específico de raspagem na nossa plataforma e cresceu e amadureceu a tal ponto que ele está posicionado para ser uma peça fundamental da arquitetura de microsserviços que a Riot está construindo. Estamos muito animados para ver o quão longe ele irá, e estamos ansiosos para os desafios de oferecer uma solução de ponta para toda a empresa. Obrigado pela leitura.

***

Artigo publicado pela Riot Engineering. A tradução foi feita com autorização pela Redação iMasters. Você pode conferir o original em https://engineering.riotgames.com/news/riot-games-api-deep-dive.