Pular para o conteúdo

Dgraph para os preguiçosos do meu time

Postado em 7 minutos de leitura

O que é?

Dgraph é um banco de dados distribuído com estrutura nativa de grafo e suporte à linguagem GraphQL.
Sua arquitetura em cluster requer ao menos uma instância do tipo Zero e uma do tipo Alpha. Um cluster Dgraph possui os componentes:

  • Zero: controla e armazena dados sobre o cluster. Responsável também por mover dados entre réplicas Alpha.
  • Alpha: Servidor que recebe requisições dos clients, executa as queries e mutations e armazena os dados.
  • Ratel: Interface web dos Alpha que permite executar queries e mutations usando a linguagem DQL.

Como funciona?

É obrigatório ter ao menos um Zero e um Alpha para o cluster funcionar. Ratel é opcional.
O Alpha possui uma porta HTTP para receber requisições GraphQL e uma porta gRPC para requisições dos clients.
Requisições via gRPC utilizam a linguagem DQL (Dgraph Query Language), uma linguagem própria do Dgraph inspirada no GraphQL.

O Alpha trabalha no esquema de Leader/Follower. Quando existe mais de um Alpha, um se torna leader e os outros followers. As requisições podem ser feitas para qualquer Alpha pois os dados são replicados entre eles.

É possível configurar um load balancer utilizando o Nginx para evitar sobrecarregar um determinado Alpha.

As portas utilizadas são:

  • Zero: 5080 para comunicação interna do cluster e 6080 para operações administrativas.
  • Alpha: 7080 para comunicação interna do cluster, 9080 para comunicação gRPC com os clients e 8080 para acesso HTTP.
  • Ratel: 8000 como servidor web do front-end.

Como rodar via Docker?

Standalone

Cria um container rodando um Zero, um Alpha e o Ratel. Em Julho/2021 o Ratel deixou de ser embutido na imagem standalone. Você pode acessar via https://play.dgraph.io/?latest. Não é recomendado para produção.

docker run -d --name dgraph -p 6080:6080 -p 8080:8080 -p 9080:9080 -v $HOME/dgraph/standalone:/dgraph dgraph/standalone

Cluster

Cria um cluster mínimo contendo um Zero, três Alphas e o Ratel, todos rodando como container Docker.
Todos os containers estão rodando na mesma máquina, nesse exemplo. É recomendado que cada um rode em uma máquina separada em produção.

# Cria uma rede virtual para os containers
docker network create dgraph_default

# Executa 1 Zero
docker run -d --name zero -p 5080:5080 -p 6080:6080 --network dgraph_default --hostname zero -v $HOME/dgraph/zero:/dgraph dgraph/dgraph dgraph zero --my=zero:5080 --telemetry sentry=false --replicas 3

# Cria 3 containers do Alpha
for i in `seq 1 3`; do
  docker run -d --name alpha${i} \
  -p 908${i}:9080 \
  -p 708${i}:7080 \
  -p 808${i}:8080 \
  --network dgraph_default \
  --hostname alpha${i} \
  -v $HOME/dgraph/alpha${i}:/dgraph \
  dgraph/dgraph dgraph alpha --zero zero:5080 --my=alpha${i}:7080 --logtostderr --cache size-mb=2048 --telemetry sentry=false --telemetry reports=false --security whitelist=0.0.0.0/0 --limit query-timeout=500ms
done

# Ratel
docker run -d --name ratel -p 8000:8000 --hostname ratel dgraph/ratel dgraph-ratel -addr localhost:8081 -listen-addr 0.0.0.0

Segurança

O Dgraph open source não possui um mecanismo de autenticação baseado em usuário e senha, como na maioria dos bancos de dados. É possível passar o parâmetro --security token=senha ao criar o Alpha, para restringir as operações administrativas, mas é ineficiente contra manipulação de dados. Nesse caso, as requisições gRPC precisam conter o header auth-token e as requisições HTTP o header X-Dgraph-AuthToken.

Diretiva @auth

Dgraph permite utilizar ACLs e a diretiva @auth em consultas GraphQL. Saiba mais em:

Checklist AWS:

  • Bloquear via Security Group o acesso público às portas 5080, 6080, 7080, 8000, 8080 e 9080.
  • Liberar via Security Group as portas apenas para o Security Group da aplicação.
  • Configurar um Nginx/Caddy com Auth Basic e SSL para fazer proxy com o Ratel e os Alphas.
  • Configurar SSL para comunicação entre o client e o Alpha.

Monitoramento

É importante manter o monitoramento do banco para verificar qualquer problema. Dgraph possui uma API que retorna métricas no formato do Prometheus. Basta configurar o Prometheus para obter essas métricas e um dashboard no Grafana.

O exemplo abaixo executa o Prometheus, na mesma máquina que o cluster, para obter métricas do Zero e do Alpha:

cat > prometheus.yml <<EOF
scrape_configs:
  - job_name: "dgraph"
    metrics_path: "/debug/prometheus_metrics"
    scrape_interval: "2s"
    static_configs:
    - targets:
      - zero:6080      # For Dgraph zero, 6080 is the http endpoint exposing metrics.
      - alpha1:8080    # For Dgraph alpha, 8080 is the http endpoint exposing metrics.
EOF
docker run -d --name prometheus --network dgraph_default --hostname prometheus -p 9999:9090 -v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

Existe um template de dashboard para o Grafana. Basta executar o container do Grafana, criar um datasource apontando para o IP e porta do Prometheus e importar o template.
O download do template pode ser feito em https://raw.githubusercontent.com/dgraph-io/benchmarks/master/scripts/grafana_dashboard.json.

docker run -d --name grafana --network dgraph_default --hostname grafana -p 3000:3000 grafana/grafana

Tanto o Zero como Alpha expõem APIs para informações sobre o cluster:

curl "localhost:6080/state"       # Zero
curl "localhost:8081/health?all"  # Alpha

Também é possível obter algumas informações via GraphQL no endpoint /admin:

# POST http://localhost:8081/admin
query {
  health {
    instance
    address
    version
    status
    lastEcho
    group
    uptime
    ongoing
    indexing
  }
}

Gerenciamento do Cluster

Os comandos mais básicos para gerenciar um cluster são:

# Para desligar um Alpha
curl "localhost:8082/shutdown"

# Remove um Alpha do cluster
curl "localhost:6080/removeNode?id=2&group=1"

# Remove todos os dados e schemas
curl localhost:8080/alter -d '{ "drop_all": true }'

DQL e Schemas

Dgraph suporta criação de schemas e manipulação de dados usando GraphQL ou DQL. Vou focar na segunda, que é utilizada quando se usa o client oficial conectando via gRPC.
Sugiro utilizar GraphQL apenas para projetos que não necessitam de um back-end, onde as requisições partem direto do front-end para o banco de dados. É uma boa solução para criar protótipos interativos ou validar idéias.

Schema

Operações relacionadas à schemas, queries e mutations, são executadas na instância Alpha.
Uitlizamos o endpoint /alter para manipular schemas na linguagem DQL.

Esse é um schema básico de ecommerce para demonstrar o uso de grafos:

cat > schema.dql <<EOF
name: string @index(term) .
amount: int .
price: int .
age: int .
country: uid @reverse .
products: [uid] @reverse .
customer: uid @reverse .

type Country {
    name
}

type User {
    name
    age
    country
}

type Product {
    name
    price
}

type Order {
    products: Product
    customer: User
    amount
}
EOF

curl -XPOST "localhost:8080/alter" --data-binary '@schema.dql'

Mutations

Vou utilizar um dataset simulando um e-commerce bem simples.
Dgraph cria automaticamente os valores UID que identificam um nó no grafo. Podemos criar nós contendo um alias para o UID. O formato é _:nome_do_alias".

Reparem que Dgraph permite executar múltiplas queries e mutations na mesma requisição. Utilize a interface web Ratel ou curl via terminal:

curl -H "Content-Type: application/json" -XPOST "localhost:8080/mutate?commitNow=true" -d '{
    "set": [
    	{
        	"dgraph.type": "Country",
        	"uid": "_:brasil",
        	"name": "Brasil"
        },
        {
        	"dgraph.type": "Country",
        	"uid": "_:argentina",
        	"name": "Argentina"
        },
        {
        	"dgraph.type": "Country",
        	"uid": "_:uruguai",
        	"name": "Uruguai"
        },
        {
        	"dgraph.type": "User",
        	"uid": "_:gustavo",
        	"name": "Gustavo Henrique",
        	"age": 37,
        	"country": ":_brasil"
        },
        {
        	"dgraph.type": "User",
        	"uid": "_:carol",
        	"name": "Carol",
        	"age": 33,
        	"country": ":_argentina"
        },
        {
        	"dgraph.type": "Product",
        	"uid": "_:playstation5",
        	"name": "Playstation 5",
        	"price": 49900
        },
        {
        	"dgraph.type": "Product",
        	"uid": "_:system76",
        	"name": "Laptop System76 Lemur Pro 14\"",
        	"price": 129900
        },
        {
    		"dgraph.type": "Order",
    		"customer": { "uid": "_:gustavo" },
    		"products": [
    			{ "uid": "_:playstation5" },
    			{ "uid": "_:system76" }
    		],
    		"amount": 179800
    	},
    	{
    		"dgraph.type": "Order",
    		"customer": { "uid": "_:carol" },
    		"products": [ { "uid": "_:playstation5" } ],
    		"amount": 49900
    	}
    ]
}'

Queries

Exemplos de queries utilizando DQL.

Pedidos do usuário com UID 0x77

curl -H "Content-Type: application/dql" -XPOST "localhost:8080/query" -d $'{
    orders(func: uid(0x77)) {
        uid
        name
        country {
		    name
        }
        order: ~customer {
            uid
            amount
            products {
                name
                price
            }
        }
    }
}'

Total de vendas do produto com UID 0x72

curl -H "Content-Type: application/dql" -XPOST "localhost:8080/query" -d $'{
    sales(func: uid(0x72)) {
        uid
        name
        total_orders: count(~products)
    }
}'

Países onde foram vendidos o produto com UID 0x72

curl -H "Content-Type: application/dql" -XPOST "localhost:8080/query" -d $'{
    sales(func: uid(0x72)) {
        uid
        name
        orders: ~products {
            customer {
                country {
                    name
                }
            }
        }
    }
}'

Total de vendas do país com UID 0x74

curl -H "Content-Type: application/dql" -XPOST "localhost:8080/query" -d $'{
    var(func: uid(0x74)) {
        ~country {
            orders as count(~customer @filter(gt(amount, 0)))
        }
    }
    sales() {
        total: sum(val(orders))
    }
}'

Pedidos de clientes do Brasil

curl -H "Content-Type: application/dql" -XPOST "localhost:8080/query" -d $'{
    orders_from_brazil(func: type(Order)) @cascade {
        uid
        amount
        products { name }
        customer {
            name
            country @filter(eq(name, "Brasil")) {
                name
            }
        }
    }
}'

Outros produtos comprados por quem comprou o Playstation 5:

curl -H "Content-Type: application/dql" -XPOST "localhost:8080/query" -d $'{
    PRODUCT as var(func: eq(name, "Playstation 5"))
    var(func: type(Order)) @cascade {
        products @filter(uid(PRODUCT))
        also_bought: products @filter(NOT uid(PRODUCT)) {
            PRODUCT_ID as uid
            name
        }
    }

    products(func: uid(PRODUCT_ID)) {
        name
    }
}'

Conclusão

Um banco de dados em grafo fornece flexibilidade para evolução de um projeto e fácil escalabilidade, sendo ideal para aplicações modernas ou sistemas distribuídos.

Dgraph é escrito em Go e seus clients utilizam conexão gRPC. Isso permite que o banco de dados rode com boa performance em máquinas com uma configuração modesta.

Agora deixo com vocês a tarefa de fazer um teste de carga e executar queries mais complexas para avaliar a performance e velocidade.