TDC 2015
Matando um Monolítico Django: de Pluggable Apps aos Microservices
Bernardo Fontes
São Paulo/SP
25 de Julho de 2015
## 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...
## 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