Skip to content

Commit

Permalink
Merge pull request #10048 from MPMG-DCC-UFMG/dev
Browse files Browse the repository at this point in the history
Integra sistema distribuído à Master
  • Loading branch information
rennancl authored Mar 3, 2023
2 parents 9f35eae + 03e6fb2 commit 03eccc0
Show file tree
Hide file tree
Showing 276 changed files with 12,988 additions and 12,745 deletions.
2 changes: 0 additions & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ source_pkgs =
crawled_request_filter
cssify
entry_probing
formparser
param_injector
range_inference
scrapy_puppeteer
step_crawler

[report]
Expand Down
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.git
venv/
env/
mpmg*/
mpmg*/
data/
4 changes: 2 additions & 2 deletions .github/workflows/continuous-integration-pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
build:
strategy:
matrix:
python-version: [3.7, 3.8]
python-version: [3.7]
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -16,7 +16,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python install.py
python web_install.py
- name: Test with pytest
run: |
coverage run -m pytest
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,16 @@ cython_debug/
kafka_*/
temp_description_folder/
redis-*/
crawlers/log/
crawler_manager/log/

.vscode/

main/staticfiles/json/step*_signature.json

utils/tests/
utils/examples/
main/migrations/

local-chromium/
# Kafka interface log files
broker_interface/log
19 changes: 0 additions & 19 deletions Dockerfile

This file was deleted.

210 changes: 157 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,200 @@
# C01
Desenvolvimento de ferramentas para construção e manutenção de coletores de páginas da Web.

Existem coletores bases, que podem ser personalizados através da interface feita em django. Eles são capazes de coletar:

- Páginas estáticas
- Páginas dinâmicas ou onde é necessário interagir com formulários.
- Arquivos
- Conjunto de arquivos
## Status atual

Os coletores são desenvolvidos em Scrapy em conjunto com Pyppeteer para o caso de páginas dinâmicas. O gerenciamento dos coletores é feito com o Scrapy-cluster.
- Distribuição de spiders (funcionam cooperativamente em máquinas distintas)
- Problemas de escalabilidade no módulo de salvamento de coletas/arquivos
- Suporte a páginas estáticas e downloads de arquivos
- Suporte parcial a páginas dinâmicas (com bugs)
- Suporte a download de arquivos em páginas dinâmicas. Mas caso haja mais de uma instância de `spider_manager` executando em máquinas distintas, os arquivos serão salvos em locais diferentes.
- Logs são transmitidos distribuidamente via Kafka
- Monitoramento parcial de coletas/coletores
- É possível acompanhar o andamento da coleta de páginas/arquivos
- Não é possível acompanhar a "saúde" dos `workers`/`spiders`

## TODOs

Dentre as funcionalidades disponíveis para os coletores, pode-se se citar, por exemplo:
A lista atualizada pode ser vista [aqui](https://github.com/MPMG-DCC-UFMG/C01/issues?q=is%3Aissue+is%3Aopen+label%3A%22sistema+distribu%C3%ADdo%22):

- Mecanismos para camuflagem dos coletores, como rotação de endereço de IP e gerenciamento de cookies.
- Ferramentas para gerar endereços automaticamente através de templates
- Os coletores também podem ser gerenciados através de uma API RESTful.
- [ ] Suporte total a páginas dinâmicas (há certos bugs na execução de coletas)
- [ ] Download centralizado de arquivos em páginas dinâmicas
- Provavelmente será necessário suporte a sistema de arquivos distribuídos
- [x] Suporte a mecânismos de passos (páginas dinâmicas)
- [ ] Resolver problemas de escalabilidade (módulos `writer` e `link_generator`)
- Será necessário suporte à sistema de arquivos distribuídos para o módulo `writer`
- [ ] Melhorias no sistema de acompanhamento de andamento de coletas (o atual não esté "calibrado")
- [ ] Tela de monitoramento de coletas, como saúde dos `workers`/`spiders`
- [ ] Realização de testes, inclusive de robustez

Para que seja possível utilizar o sistema, e consequentemente configurar e executar coletores, é necessário inicialmente instalar a aplicação. Essa página se refere a essa etapa inicial. Preferencialmente, a instalação deve ser feita nativamente em sistemas baseados em Linux, contudo, através do Docker, é possível instalar o sistema em outros SO, como Windows. Portanto, se esse for o seu caso, foque nas instruções de instalação no final da página, na seção "Execução com Docker (standalone)". Futuramente, será possível realizar uma instalação distribuída do sistema, que ainda está sendo desenvolvida.
## Como executar

Temos 4 módulos que devem funcionar de maneira independente: `Crawler Manager (CM)` (que inicia junto com o servidor), `Link Generator (LG)`, `Spider Manager (SM)` e `Writer (W)`.

## Instalação
A primeira etapa para poder instalar o sistema é realizar o donwload de seu código-fonte. Para isso, utilize as ferramentas do GitHub para baixar o repositório localmente.
Esses módulos, devidademente configurado os `hosts` do `Zookeeper`, `Kafka` e `Redis`, em um arquivo `settings.py` na respectiva pasta deles, podem ser executados em máquinas diferentes com as seguintes condições:

Em seguida, é importante notar que para usar o programa é necessário um _virtualenv_ ou uma máquina apenas com **Python 3.7+**, de maneira que os comandos _"python"_ referencie o Python 3.7+, e _"pip"_ procure a instalação de pacotes também do Python 3.7+. Assim, configure esse ambiente. Para mais informações de como criar e manter _virtualenvs_, o [seguinte material](https://docs.python.org/pt-br/3/library/venv.html) pode ajudar.
- Cada um devem ser iniciados manualmente, ainda não foram dockerizados.
- Deve haver apenas uma instância em execução dos seguintes módulos:
- `Crawler Manager`: Naturalmente terá apenas uma instância em execução, pois é um processo filho do servidor.
- `Link Generator`: Mais de uma instância em execução resultará em duplicação de requisições de coletas. Isso porque cada instância irá ler o comando de geração de requisições, então **é necessário adaptar a leitura de mensagens do Kafka para que apenas um consumer leia cada mensagem**.
- `Writer`: Esse módulo tem o problema de escalabilidade de `Link Generator` (Kafka consumers lendo a mesma mensagem, portanto, gerando processamento duplicado), mas mais que isso, ter mais de uma instância em execução resultará em `fragmentação da coleta`. Isso pois cada módulo escreve dados no sistema de arquivos local de cada máquina. Portando, é `necessário suporte a sistema de arquivos distribuídos`.
- Podem haver várias instâncias de `Spider Managers` em execução, inclusive na mesma máquina. Ao contrário dos problemas de escalabilidade relatado nos outros módulos que se comunicam via `Kafka`, a fila de coletas é gerenciado via `Redis`, de modo que não há processamento de uma mesma mensagem por consumidores diferentes.
- No momento, um `Spider Manager` por gerir apenas um spider de uma mesma coleta.

Além disso, alguns serviços necessitam que o Java esteja rodando no sistema, que pode ser instalado por
> **Obs.**: Zookeeper, Kafka e Redis devem estar em execução e seus hosts e portas configuradas nos arquivos settings.py dos módulos.
### Iniciando Kafka e Redis

Ao executar `python install.py` na raiz do projeto, tanto o `Kafka` quanto o `Redis` são baixados automaticamente. Para executá-los, execute os seguintes comandos (considerando que a pasta corrente é a raiz do projeto):

Inicie o `Zookeeper` para que o `Kafka` funcione corretamente:
```bash
cd kafka_2.13-2.4.0/
bin/zookeeper-server-start.sh config/zoo.properties
```
sudo apt install default-jre

Abra um outro terminal e inicie o Kafka:

```bash
cd kafka_2.13-2.4.0/
bin/kafka-server-start.sh config/server.properties
```
Caso seu sistema não seja Linux, siga as instruções de instalação da Java Runtime para o seu sistema, as quais podem ser encontradas nesse [link](https://www.java.com/en/download/manual.jsp).

Para instalar todos os programas e suas dependências execute o script install.py.
```
python install.py
Abra outro terminal e inicie o `Redis`:

```bash
cd redis-5.0.10/
./src/redis-server
```
Esse script deve ser executado a partir da raiz do repositório. No final da execução, logs semelhantes a esses devem aparecer no seu terminal:

<img src="https://drive.google.com/uc?export=view&id=1DXXS-CQyXThC4xlPYp-zquUctdDhphac" >
### Interface/servidor Django

Se deseja instalar apenas algum dos módulos implementados, por exemplo, o módulo de extração de parâmetros de formulários, navegue até a pasta do módulo e execute pip install:
```
cd src/form-parser
pip install .
```
Instale as dependências:

Caso pretenda utilizar o Tor para rotacionar IPs, é necessário configurá-lo por meio dos seguintes comandos:
```bash

python3.7 -m venv venv
source venv/bin/activate
python install.py
```
chmod 744 src/tor_setup/setup.sh
./src/tor_setup/setup.sh

Coloque-o para rodar:

```bash
python manage.py runserver --noreload
```

Um script de instalação diferente foi necessário pois para instalar o Tor é necessário ser superusuário.
> Note a flag `--noreload`, ela é importante para que o servidor não reinicie durante sua execução e crie mais consumidores Kafka que o necessário (gerando duplicação de `logs`, por exemplo)
## Execução
### Link Generator

Para execução da interface basta executar o seguinte comando:
```
python run.py
```
E em seguida acessar _http://localhost:8000/_
Vá para a pasta do módulo e instale suas dependências:

Se quiser acessar o programa através da rede, execute:
```bash
cd link_generator/
python3.7 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
python run.py 0.0.0.0:8000
Execute-o:

```bash
cd src/
python main.py
```
E então use o IP da máquina onde a interface está sendo executada para acessá-la. Por exemplo, se a máquina onde você rodou o comando acima tem endereço de IP _1.2.3.4_, e esse endereço é visível para sua máquina através da rede, você pode acessar _http://1.2.3.4:8000/_. Essa execução só dará certo se a máquina estiver com o acesso externo desse IP liberado, caso contrário, não será possível acessar o sistema remotamente.

Assim que tiver executado e acesso pelo navegador, a seguinte página aparecerá:
### Spider Manager

Vá para a pasta do módulo e instale suas dependências:

<img src="https://drive.google.com/uc?export=view&id=1pfvTCtLBCk7SIu8SprEL8cD5FQcojlZl" >
```bash
cd spider_manager/
python3.6 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Mais informações de como utilizar a interface pode ser encontrado nas próximas páginas dessa Wiki.
```

## Execução com Docker (standalone)
> Note que é usado python3.6. Isso é um requisito necessário, pois o módulo utiliza recursos do Scrapy-Cluster que só a suporte a essa versão do python.
Antes de tudo, assegure-se de que o Docker está devidamente instalado no seu computador. Caso precise de instruções de como fazer isso, o seguinte link pode auxiliar nesse processo: https://docs.docker.com/get-docker/
Crie quandos processos desejar de Spider Managers, repetindo o comando abaixo:

Para instalação do sistema é necessário montar a imagem a partir do Dockerfile, para isso execute o seguinte comando a partir da raiz do repositório:
```
sudo docker build -t c01 .
cd src/
python comand_listener.py
```
### Writer

Vá para a pasta do módulo e instale suas dependências:

Para conseguir executar a imagem, é preciso criar o diretório "data" a partir da raiz do repositório, para isso, execute o comando:
```
mkdir data
cd writer/
python3.7 -m venv venv
source venv/bin/activate
```
Obs: Caso seu sistema não seja Linux, utilize o comando equivalente para criar diretório.

Em seguida, é necessário executar a imagem. Ainda na raiz do respositório execute o comando responsável por isso:
```
sudo docker run --mount type=bind,source="$(pwd)/data",target=/data -p 8000:8000 -t c01
Execute-o:

```bash
cd src/
python writer.py
```
## Arquitetura atual da solução distribuída

![Arquitetura](sist_dist_diagram.png)

O sistema possui 4 módulos principais:

- **Gerenciador de coletas** (`crawler_manager`)
- Módulo acoplado ao servidor.

- É responsável por realizar a interface entre a aplicação Django e os módulos de processamento de coletas.

- Recebe status de spiders (criação e encerramento) vindos de gerenciador de spiders. Com isso, é capaz de saber e informar ao servidor quando uma coleta terminou.

- Também repassa os logs das coletas ao servidor, bem como o atualiza sobre o andamento das coletas, em geral.

- **Gerador de requisições de coletas** (`link_generator`, *um melhor nome pode ser atribuído)
- Responsável por gerar as requisições iniciais de coletas.

- Útil para o caso de URLs parametrizadas não sobrecarreguem o servidor.

- **Funcionamento atual**

- O módulo recebe a configuração de um coletor e gera todas requisições de coletas possíveis. Um dos possíveis impactos disso é "inundar" o Redis.

- **Funcionamento desejado/futuro**

- Geração requisições de coletas sob demanda.

- **Gerenciador de spiders** (`spider_manager`)
- Responsável por gerenciar o ciclo de vida dos spiders, bem como informar ao gerenciador de coletas sobre o status dos mesmos e das coletas.

- **Funcionamento atual**

- Há suporte há apenas um spider por coleta para cada gerenciador de spiders. Mas podem haver múltiplos spiders gerenciados, desde que sejam de coletas diferentes.

- Spiders são criados no início do processo de coleta, e, caso um spider fique ocioso por determinado tempo, ele é automaticamente encerrado.

- **Funcionamento desejado/futuro**

- Suporte a múltiplos spiders de um mesmo coletor.

- Balanceamento de carga. Criação e encerramento de spiders a qualquer momento, de acordo com a necessidade.

- **Escritor** (`writer`)
- Responsável por persistir as coletas, bem como baixar e salvar os possíveis arquivos encontrados nela.

- **Funcionamento atual**

- Só é suportado que apenas um deste módulo execute ao mesmo tempo. Pois os dados coletados são salvos diretamente no sistema de arquivo da máquina hospedeira.

- Mais de um deste módulo em execução implicaria em dados salvos em máquinas diferentes ou possíveis conflitos de arquivos.

- Isso reflete em um possível gargalo ao salvar os dados coletados, como os dados coletados são transmitidos por um único tópico Kafka e o processamento por um única instância deste módulo.

- **Funcionamento desejado/futuro**

O comando acima garante que o container terá acesso ao disco da máquina, e esse aceso foi feito através da ligação da raiz do respositório com a raiz da imagem. Ou seja, ao configurar coletores com o seguinte caminho "/data/nome_coletor", os dados estarão sendo salvos na verdade no seguinte diretório da máquina: "caminho_da_raiz_repositório>/data/nome_coletor". É possível alterar o diretório na máquina hospedeira, para isso, basta alterar o trecho "$(pwd)" do comando para o diretório desejado.
- Suporte a sistema de arquivos distribuídos e/ou salvamento dos dados coletados em banco de dados (para o caso das páginas), possibilitando múltiplas instâncias do módulo executando ao mesmo tempo.
48 changes: 48 additions & 0 deletions broker_interface/file_description_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import json
import os

from kafka import KafkaConsumer

class FileDescriptionConsumer():
TOPIC_NAME = 'file_description'

def __init__(self, kafka_host: str, kafka_port: str):
"""
FileDescriptionConsumerConsumer constructor.
:param kafka_host: location of the host machine running kafka
:param kafka_port: port used to interact with kafka in the host machine
"""

self.kafka_consumer = KafkaConsumer(
FileDescriptionConsumer.TOPIC_NAME,
bootstrap_servers=f'{kafka_host}:{kafka_port}'
)

def description_consumer(self):
"""
Writes the description of the items stored in Kafka to the correct
files. Items must be utf-8 encoded json strings in the format described
in feed_description.
"""
try:
for item_json in self.kafka_consumer:
item = json.loads(item_json.value.decode('utf-8'))
self.write_description(item)
finally:
self.kafka_consumer.close()

def write_description(self, item: dict):
"""
Writes the description of items loaded from the Kafka topic to the
correct file.
:param item: A dictionary in the format described in feed_description
"""

file_address = os.path.join(item["destination"],
"file_description.jsonl")

with open(file_address, "a+") as f:
f.write(json.dumps(item["description"]))
f.write("\n")
Loading

0 comments on commit 03eccc0

Please sign in to comment.