TDC 2015

Matando um Monolítico Django: de Pluggable Apps aos Microservices


Bernardo Fontes


São Paulo/SP

25 de Julho de 2015

Olar!

twitter.com/bbfontes

github.com/berinhard

garimpo.fm

pessoas.cc

bernardoxhc@gmail.com

berinhard.github.io/talks/


slideshare.net/bernardofontes


Quem são vocês?

## Roteiro - ### Nosso Cenário - ### Microservices - ### Domain Driven Design - ### Implantação - ### Dúvidas
## Nosso Cenário ### Sistema para um empresa de **medicina do trabalho** realizar atendimentos de **medicina ocupacional** por todo o Brasil e fornecer uma **análise inteligente** sobre o perfil dos colaboradores de uma empresa.

Maaaaaaas...

Tudo começou só com uma filinha:

Hoje em dia...

Até envia email

## Histórico do Projeto - ### + de **5 anos** - ### + de **10 devs** passaram - ### Python e **Django 1.4** - ### Hoje: 4 devs e **únicos Pythonistas** - ### Empresa com **12 devs**
## Histórico do Projeto - ### Em **Outubro de 2014** - ### 3 devs - ### Django com **uma única app** - ### 3k LOC de **models.py** - ### ~3k LOC de **views.py** - ### ~2,5k LOC de **services.py**
## Menos de 50% de coverage ![No tests](images/no_test.jpg)
## Infra do Projeto - ### 9 filials == 9 máquinas - ### 1 ambiente de clientes - ### 1 única máquina do banco - ### **11 máquinas**
## $ fab all_hosts deploy ![XKCD](images/deploying.png)
## Cliente - ### **Novos módulos** no sistema - ### **Melhorias** nos antigos - ### Correção de **bugs**
![Work](images/working.png)
### Estudamos ![Library](images/library.jpg)

Estudamos

## Microservices Um estilo arquitetural para o desenvolvimento de **serviços enxutos** em que cada um possa ser **executado em um processo póprio** e se comunicando através de mecanismos de fácil implementação como o protocolo HTTP. Esses serviços são **construídos focando capacidades de negócio** e devem poder ser **deployados independentemente** através de um processo automatizado. A **necessidade de gestão centralizada deve ser mínima** para os serviços visando viabilizar a independência entre eles.
## Componentes por Serviços - ### Componentes: unidades de software **independentes** - ### Serviços: **componentes externos** com comunicação por API
## Foco em Capacidades do Negócio - ### Serviços limitados ao **contexto do problema** - ### Equipe precisa saber só do **contexto específico** e não mais do todo
## Pensamento em ~~Projeto~~ Produto - ### Fim da **Lei de Conway** - ### Domínio menor == entendimento mais simples - ### Entendimento mais simples == equipe responsável por **todo o serviço** - ### Deploy independentemente
## Smart Endpoints & Dumb Pipes - ### Foco em **coesão** e **desacoplamento** - ### Chamadas por métodos em memória viram **chamadas ao serviço** - ### HTTP >> **síncrono** - ### Mensageria >> **assíncrono**
## Governança Descentralizada - ### **Decisões específicas** para o microservice - ### **Right tool** for the job - ### **Independência** no processo de desenvolvimento da equipe
## Armazenamento de Dados Descentralizados - ### Dados sendo visualizados de **acordo com o contexto** - ### **Perde-se** a gestão de transações automática
## Design orientado a Falhas - ### **Falhas de comunicação** são sempre reais - ### A comunicação deve ser sempre **desenvolvida pensando o cenário de falha** - ### Código **mais estável**
## Conclusões do que poderíamos - ### Desenvolver em **outras tecnologias** - ### Ter rotinas de **deploy mais simples** - ### Envolver **mais devs** no projeto - ### Entregar **mais rápido** - ### Gerar software de **maior qualidade** - ### Focar em entregar **mais valor** para o cliente
## Nossa reação ![Mind](images/mind.gif)
## E começamos... - ![Dog](images/no_Idea.png)
## Trade-offs que Encontramos - ### Aumento da **complexidade operacional** - ### Ambiente de dev mais burocrático (resolvemos com o Docker) - ### Diferentes processos de deploy - ### Problemas para garantir a **consistência dos dados** - ### Todos os overheads de **comunicação em sistemas distribuídos**
## Killer Problems - ### Nossas regras de negócios estavam **completamente acopladas** entre os módulos services.py, models.py e views.py - ### A **modelagem acoplada** do nosso banco de dados - ### Impossível de mudar
![DatabaseFull](images/whole_db.jpg)
![Database](images/database.jpg)
## Solução - ![Legacy](images/giphy.gif)

Então estudamos mais...

## Focos no Mindset - ### Definição dos **Bounded Context** dos serviços - ### Utilização de **Domain Objects** - ### Criação de **Use Cases** orquestrando a troca de mensagem entre os objetos - ### Implementação de **Anti-Corruption Layer** nos serviços
## Implementação em Etapas
## 1º Caso: Isolar lógica de um domínio (sem microservice) - Models continuaram no legado - Nova app somente lidando com um tipo de atendimento - Já existiam testes - Migramos as views, services, urls e testes isolando a app - Ponto de acoplamento: import dos models do legado - Objetivo: aprender a isolar a lógica
```python # urls.py do legado (r'^clientes/absenteismo/', include('abs_homolog.urls', namespace='abs_homolog')) # urls.py da app urlpatterns = patterns('abs_homolog.views', url(r'^graficos/consolidado/$', 'absenteeism_report', name='absenteeism_consolidated'), url(r'^graficos/cat/$', 'absenteeism_cat_report', name='absenteeism_cat_chart'), ) ```
## 2º Caso: Nova funcionalidade com baixo acoplamento (sem microservice) - Models, views, forms e urls próprios em nova app - Utilização de Signals para não mexer no meio legado - Integração através de import em listeners no legado - Ponto de acoplamento: único import para o model legado de Paciente - Objetivo: aprender a organizar o código para ser consumido por clientes externos
```python # models.py do legado created_medical_file = Signal(providing_args=['medical_file']) created_medical_file.connect( receivers.populate_medical_file_reports, dispatch_uid='medical_file_creation' ) # views.py do legado created_medical_file.send( sender='atendimento_ocupacional', medical_file=ficha ) # listener.py no legado def populate_medical_file_reports(sender, medical_file, *args, **kwargs): from epidemiologic.models import EpidemiologicRecord EpidemiologicRecord.objects.create_entry( **medical_file.get_report_data() ) ```
## 3º Caso: Nova funcionalidade apenas acoplando o banco (microservice interno) - App plugável do Django (estratégias de settings) - Criação de UC para encapsular lógica de domínio - Utilização de Signals para não mexer no meio legado - Integração através de disparo de mensagens via RabbitMq - Disparo encapsulado em objetos de fronteira - Tarefas fazendo apenas a limpeza de dados e delegando aos UC - Ponto de acoplamento: apenas utilizaram o mesmo banco de dados - Objetivo: implementar um microservice sem o overhead dois ambientes
```python def populate_patient_peridocal_info(sender, patient, *args, **kwargs): from brmed_site.utils import PeriodicalDatesPatientProxy patient_proxy = PeriodicalDatesPatientProxy(patient) patient_proxy.send() class PeriodicalDatesPatientProxy(BaseProxy): def __init__(self, patient): super(PeriodicalDatesPatientProxy, self).__init__() self.patient = patient def send(self): expiration_days = self.patient.days_until_due_date if not isinstance(expiration_days, int): return kwargs = { 'patient_id': self.patient.id 'last_exam_date': self.patient.get_data_ultimo_exame().isoformat(), 'expiration_days': expiration_days, } return self.send_task(settings.UPDATE_PERIODICAL_LAST_EXAM_DATE, kwargs=kwargs) # use_cases.py no microservice chamada pela task class UpdatePeriodicalDatesExamInfo(object): @classmethod def execute(cls, patient_id, last_exam_date, expiration_days): entry = PeriodicalDates.objects.get_or_prepare(patient_id=patient_id) entry.update_last_exam_date(last_exam_date) entry.set_expiration_date(expiration_days) entry.save() ```
## 4º Caso: Nova funcionalidade agnóstica (microservice interno) - App plugável do Django (estratégias de settings) - Criação de UC para encapsular lógica de domínio - Utilização de Signals para não mexer no meio legado - Integração através de disparo de mensagens via RabbitMq - **Modelos sem semântica alguma do domínio do legado** - **Banco de dados próprio (máquina própria pro banco)** - Objetivo: ser possível deployar isoladamente no futuro
```python class ReportsRouter(object): def db_for_read(self, model, **hints): if model._meta.app_label == 'reports': return settings.REPORTS_DB_KEY return None def db_for_write(self, model, **hints): if model._meta.app_label == 'reports': return settings.REPORTS_DB_KEY return None def allow_relation(self, obj1, obj2, **hints): if obj1._meta.app_label == 'reports' and obj2._meta.app_label == 'reports': return True return None def allow_syncdb(self, db, model): if model._meta.app_label == 'south': return True if db == settings.REPORTS_DB_KEY: return model._meta.app_label == 'reports' elif model._meta.app_label == 'reports': return False return None # no settings.py DATABASE_ROUTERS = ['reports.router.ReportsRouter'] ```
## 5º Caso: Autenticação como Microservice - App SSO em Sinatra (Ruby) - **Todo** o ambiente isolado - No legado mudamos o Middleware e Backends de autenticação - Objetivo: validar participação de outros devs no projeto
## 6º Caso: Novo módulo em Microservice - **App Flask** de ambiente isolado - Utilização de comunicação por RabbitMq **e** API Rest - Tivemos que desenvolver uma API para expor o legado - Objetivo: implementar microservices por completo
## Conclusões - ### Tem **muitos** trade-offs para serem pensados - ### Não usamos microservices para tudo - ### Foco em **modularização** - ### Diminuímos o **tempo de entrega** - ### Diminuímos a **barreira de entrada** no projeto

III Encontro PythOnRio

11ª PythonBrasil

Obrigado!

Bernardo Fontes

twitter.com/bbfontes

github.com/berinhard

garimpo.fm

pessoas.cc

bernardoxhc@gmail.com