Além de todas as discussões recentes sobre o Kubernetes e se você deve ou não Dockerizar seu banco de dados, hoje eu gostaria de mostrar por que essas duas coisas podem ser boas soluções quando a escalabilidade e a elasticidade são um grande requisito em sua arquitetura.
O segredo aqui é simples: Spring Boot com Kubernetes para implantação do aplicativo e do banco de dados usando NoSQL.
Por que NoSQL e Spring Data?
Com os bancos de dados de documentos, é possível evitar muitas junções desnecessárias, pois toda a estrutura é armazenada em um único documento. Portanto, ele terá um desempenho naturalmente mais rápido do que um modelo relacional à medida que seus dados crescerem.
Se você estiver usando qualquer uma das linguagens JVM, o Spring Data e o Spring Boot podem ser algo bastante familiar para você. Assim, você pode começar rapidamente com o NoSQL mesmo sem nenhum conhecimento prévio.
Por que Kubernetes?
O Kubernetes (K8s) permite escalonar para cima e para baixo seu aplicativo sem estado em um ambiente independente de nuvem. Nas últimas versões, o K8s também adicionou a capacidade de executar aplicativos com estado, como bancos de dados, e esse é um dos (muitos) motivos pelos quais ele é um tema tão importante atualmente.
Eu mostrei em Minha postagem anterior no blog como implantar o Couchbase no K8s e como torná-lo "elástico", aumentando e diminuindo a escala facilmente. Se você ainda não o leu, dedique alguns minutos extras à transcrição do vídeo, pois ela é uma parte importante do que falaremos aqui.
Criação de um microsserviço de perfil de usuário
Na maioria dos sistemas, o usuário (e todas as entidades relacionadas) é o dado acessado com mais frequência. Consequentemente, essa é uma das primeiras partes do sistema que precisa passar por algum tipo de otimização à medida que os dados crescem.
Adicionar uma camada de cache é o primeiro tipo de otimização que podemos pensar. No entanto, ainda não é a "solução final". As coisas podem ficar um pouco mais complicadas se você tiver milhares de usuários ou se precisar armazenar entidades relacionadas ao usuário também na memória.
O gerenciamento de grandes quantidades de perfis de usuários é uma boa opção para bancos de dados de documentos. Basta dar uma olhada no Caso de uso do Pokémon Gopor exemplo. Portanto, a criação de um serviço de perfil de usuário altamente dimensionável e elástico parece ser um desafio suficientemente bom para demonstrar como projetar um microsserviço altamente dimensionável.
O que você vai precisar:
- Couchbase
- JDK e o plug-in do Lombok para Eclipse ou Intellij
- Maven
- Um cluster do Kubernetes - estou executando este exemplo em 3 nós no AWS (não recomendo usar o minikube). Se você não sabe como configurar um, assista a este vídeo vídeo.
O Código
Você pode clonar o projeto inteiro aqui:
|
1 |
https://github.com/couchbaselabs/kubernetes-starter-kit |
Vamos começar criando nossa entidade principal chamada Usuário:
|
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 |
@Document @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode public class User extends BasicEntity { @NotNull @Id private String id; @NotNull @Field private String name; @Field private Address address; @Field private List<Preference> preferences = new ArrayList<>(); @Field private List<String> securityRoles = new ArrayList<>(); } |
Nessa entidade, temos duas propriedades importantes:
- securityRoles: Todas as funções que o usuário pode desempenhar no sistema.
- preferências: Todas as possíveis preferências que o usuário possa ter, como idioma, notificações, moeda etc.
Agora, vamos brincar um pouco com nosso Repositório. Como estamos usando o Spring Data, você pode usar praticamente todos os seus recursos aqui:
|
1 2 3 4 5 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = "user") public interface UserRepository extends CouchbasePagingAndSortingRepository<User, String> { List<User> findByName(String name); } |
Se você quiser saber mais sobre o Couchbase e o Spring Data, Confira este tutorial.
Também implementamos dois outros métodos:
|
1 2 3 4 5 6 |
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferences SATISFIES preference.name = $1 END") List<User> findUsersByPreferenceName(String name); @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)") User hasRole(String userId, String role); |
- hasRole: Verifica se um usuário tem uma função especificada:
- findUsersByPreferencyName: Como o nome diz, ele encontra todos os usuários que contêm uma determinada preferência.
Observe que estamos usando a sintaxe N1QL no código acima, pois ela torna as consultas muito mais simples do que o uso de JQL simples.
Além disso, você pode executar todos os testes para se certificar de que tudo está funcionando corretamente:

Não se esqueça de alterar seu application.properties com as credenciais corretas do seu banco de dados:
|
1 2 3 4 |
spring.couchbase.bootstrap-hosts=localhost spring.couchbase.bucket.name=test spring.couchbase.bucket.password=couchbase spring.data.couchbase.auto-index=true |
Para testar nosso microsserviço, adicionei alguns endpoints Restful:
|
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 |
@RestController @RequestMapping("/api/user") public class UserServiceController { @Autowired private UserService userService; @RequestMapping(value = "/{id}", method = GET, produces = APPLICATION_JSON_VALUE) public User findById(@PathParam("id") String id) { return userService.findById(id); } @RequestMapping(value = "/preference", method = GET, produces = APPLICATION_JSON_VALUE) public List<User> findPreference(@RequestParam("name") String name) { return userService.findUsersByPreferenceName(name); } @RequestMapping(value = "/find", method = GET, produces = APPLICATION_JSON_VALUE) public List<User> findUserByName(@RequestParam("name") String name) { return userService.findByName(name); } @RequestMapping(value = "/save", method = POST, produces = APPLICATION_JSON_VALUE) public User findUserByName(@RequestBody User user) { return userService.save(user); } } |
Dockerizando seu microsserviço
Primeiro, altere seu application.properties para obter as credenciais de conexão das variáveis de ambiente:
|
1 2 3 4 |
spring.couchbase.bootstrap-hosts=${COUCHBASE_HOST} spring.couchbase.bucket.name=${COUCHBASE_BUCKET} spring.couchbase.bucket.password=${COUCHBASE_PASSWORD} spring.data.couchbase.auto-index=true |
E agora podemos criar nosso Dockerfile:
|
1 2 3 4 5 6 |
FROM openjdk:8-jdk-alpine VOLUME /tmp MAINTAINER Denis Rosa <denis.rosa@couchbase.com> ARG JAR_FILE ADD ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] |
Em seguida, criamos e publicamos nossa imagem no Docker Hub:
- Crie sua imagem:
|
1 |
./mvnw install dockerfile:build -DskipTests |
- Faça login no Docker Hub a partir da linha de comando
|
1 |
docker login |

- Vamos pegar o imageId de nossa imagem criada recentemente:
1docker images

- Crie sua nova tag usando o imageId:
|
1 2 |
//docker tag YOUR_IMAGE_ID YOUR_USER/REPO_NAME docker tag 3f9db98544bd deniswsrosa/kubernetes-starter-kit |
- Por fim, impulsione sua imagem:
1docker push deniswsrosa/kubernetes-starter-kit
Sua imagem agora deve estar disponível no Docker Hub:

Configuração do banco de dados
Escrevi um artigo inteiro sobre isso aquimas para ser breve. Basta executar os seguintes comandos dentro do diretório do kubernetes.
|
1 2 3 4 |
./rbac/cluster_role.sh kubectl create -f secret.yaml kubectl create -f operator.yaml kubectl create -f couchbase-cluster.yaml |
Depois de algum tempo, todas as três instâncias do nosso banco de dados deverão estar em execução:

Vamos encaminhar a porta do Console da Web para nossa máquina local:
|
1 |
kubectl port-forward cb-example-0000 8091:8091 |
E agora podemos acessar o console da Web em https://localhost:8091. Você pode fazer login usando o nome de usuário Administrador e a senha senha

Ir para Segurança -> ADICIONAR USUÁRIO com as seguintes propriedades:
- Nome de usuário: amostra de couchbase
- Nome completo: amostra de couchbase
- Senha: amostra de couchbase
- Verificar senha: amostra de couchbase
- Funções: De acordo com a imagem abaixo:

OBS: Em um ambiente de produção, não adicione seu aplicativo como administrador
Implantação de seu microsserviço
Primeiro, vamos criar um segredo do Kubernetes onde armazenaremos a senha para nos conectarmos ao nosso banco de dados:
|
1 2 3 4 5 6 7 |
apiVersion: v1 kind: Secret metadata: name: spring-boot-app-secret type: Opaque data: bucket_password: Y291Y2hiYXNlLXNhbXBsZQ== #couchbase-sample in base64 |
Execute o seguinte comando para criar o segredo:
|
1 |
kubectl create -f spring-boot-app-secret.yaml |
O arquivo spring-boot-app.yaml é o responsável pela implantação do nosso aplicativo. Vamos dar uma olhada em seu conteúdo:
|
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 |
apiVersion: apps/v1beta1 kind: Deployment metadata: name: spring-boot-deployment spec: selector: matchLabels: app: spring-boot-app replicas: 2 # tells deployment to run 2 pods matching the template template: # create pods using pod definition in this template metadata: labels: app: spring-boot-app spec: containers: - name: spring-boot-app image: deniswsrosa/kubernetes-starter-kit imagePullPolicy: Always ports: - containerPort: 8080 name: server - containerPort: 8081 name: management env: - name: COUCHBASE_PASSWORD valueFrom: secretKeyRef: name: spring-boot-app-secret key: bucket_password - name: COUCHBASE_BUCKET value: couchbase-sample - name: COUCHBASE_HOST value: cb-example |
Gostaria de destacar algumas partes importantes desse arquivo:
- réplicas: 2 -> O Kubernetes iniciará duas instâncias do nosso aplicativo
- imagem: deniswsrosa/kubernetes-starter-kit -> A imagem do docker que criamos anteriormente.
- contêineres: nome: -> Aqui é onde definimos o nome do contêiner que executa nosso aplicativo. Você usará esse nome no Kubernetes sempre que quiser definir quantas instâncias devem estar em execução, estratégias de escalonamento automático, balanceamento de carga etc.
- env: -> É aqui que definimos as variáveis de ambiente do nosso aplicativo. Observe que também estamos nos referindo ao segredo que criamos anteriormente.
Execute o seguinte comando para implementar nosso aplicativo:
|
1 |
kubectl create -f spring-boot-app.yaml |
Em alguns segundos, você perceberá que as duas instâncias do seu aplicativo já estão em execução:

Por fim, vamos expor nosso microsserviço ao mundo externo. Há dezenas de possibilidades diferentes de como isso pode ser feito. No nosso caso, vamos simplesmente criar um balanceador de carga:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: v1 kind: Service metadata: name: spring-boot-load-balancer spec: ports: - port: 8080 targetPort: 8080 name: http - port: 8081 targetPort: 8081 name: management selector: app: spring-boot-app type: LoadBalancer |
O seletor é uma das partes mais importantes do arquivo acima. É onde definimos os contêineres para os quais o tráfego será redirecionado. Nesse caso, estamos apenas apontando para o aplicativo que implantamos anteriormente.
Execute o seguinte comando para criar nosso balanceador de carga:
|
1 |
kubectl create -f spring-boot-load-balancer.yaml |
O balanceador de carga levará alguns minutos para ser ativado e redirecionar o tráfego para nossos pods. Você pode executar o seguinte comando para verificar seu status:
|
1 |
kubectl describe service spring-boot-load-balancer |

Como você pode ver na imagem acima, nosso balanceador de carga pode ser acessado em ad84a916d65ad11e884a20266aa53c9-1223617270.us-west-2.elb.amazonaws.com, e o targetPort 8080 redirecionará o tráfego para dois endpoints: 10.2.1.6:8080 e 10.2.2.7:8080
Por fim, podemos acessar nosso aplicativo e começar a enviar solicitações a ele:
- Inserção de um novo usuário:

- Busca de usuários:

E quanto a ser elástico?
É aqui que as coisas ficam realmente interessantes. E se precisarmos aumentar a escala de todo o nosso microsserviço? Digamos que a Black Friday esteja chegando e que precisemos preparar nossa infraestrutura para suportar esse fluxo maciço de usuários que chegam ao nosso site. Bem, esse é um problema fácil de resolver:
- Para escalonar nosso aplicativo, basta alterar o número de réplicas no spring-boot-app.yaml arquivo.
12345678910...spec:selector:matchLabels:app: spring-boot-appreplicas: 6 # tells deployment to run 6 pods matching the templatetemplate: # create pods using pod definition in this templatemetadata:labels:...
Em seguida, execute o seguinte comando:
|
1 |
kubectl replace -f spring-boot-app.yaml |
Está faltando alguma coisa? Sim. E quanto ao nosso banco de dados? Devemos ampliá-lo também:
- Altere o atributo size no couchbase-cluster.yaml file:
|
1 2 3 4 5 6 7 8 9 |
... enableIndexReplica: false servers: - size: 6 name: all_services services: - data - index ... |
Por fim, execute o seguinte comando:
|
1 |
kubectl replace -f couchbase-cluster.yaml |
Como posso reduzir a escala?
Reduzir a escala é tão fácil quanto aumentar a escala; você só precisa alterar ambos couchbase-cluster.yaml e spring-boot-app.yaml:
- couchbase-cluster.yaml
|
1 2 3 4 5 6 7 8 9 |
... enableIndexReplica: false servers: - size: 1 name: all_services services: - data - index ... |
- spring-boot-app.yaml:
|
1 2 3 4 5 6 7 8 9 10 |
... spec: selector: matchLabels: app: spring-boot-app replicas: 1 template: metadata: labels: ... |
E execute os seguintes comandos:
|
1 2 |
kubectl replace -f couchbase-cluster.yaml kubectl replace -f spring-boot-app.yaml |
Escalonamento automático de microsserviços no Kubernetes
Vou me aprofundar nesse tópico na parte 2 deste artigo. Enquanto isso, você pode conferir este vídeo sobre dimensionamento automático de pods.
Solução de problemas de sua implantação do Kubernetes
Se seus pods não conseguirem iniciar, há muitas maneiras de solucionar o problema. No caso abaixo, ambos os aplicativos não conseguiram iniciar:

Como eles fazem parte da implantação, vamos descrever a implantação para tentar entender o que está acontecendo:
|
1 |
kubectl describe deployment spring-boot-deployment |

Bem, nada é realmente relevante nesse caso. Então, vamos dar uma olhada em um dos registros do pod:
|
1 |
kubectl log spring-boot-deployment-74649f869d-74sq8 |

Entendi! O aplicativo não foi iniciado porque esquecemos de criar o usuário no Couchbase. Basta criar o usuário para que os pods sejam ativados em alguns segundos:

Conclusão
Os bancos de dados são aplicativos com estado, e dimensioná-los não é tão rápido quanto dimensionar aplicativos sem estado (e provavelmente nunca será), mas se você precisar criar uma arquitetura realmente elástica, deverá planejar o dimensionamento de todos os componentes da sua infraestrutura. Caso contrário, você estará apenas criando um gargalo em outro lugar.
Neste artigo, tentei mostrar apenas uma pequena introdução sobre como você pode tornar elásticos seu aplicativo e seu banco de dados no Kubernetes. No entanto, essa ainda não é uma arquitetura pronta para produção. Ainda há muitos outros aspectos a serem considerados, e abordarei alguns deles nos próximos artigos.
Enquanto isso, se você tiver alguma dúvida, envie-me um tweet para @deniswsrosa ou deixe um comentário abaixo.