Replicacion
KVNode logra alta disponibilidad mediante un modelo de replicación líder-seguidor. Los cambios escritos en un nodo principal (líder) se transmiten en tiempo real a los nodos réplica (seguidores) a través de un protocolo de replicación basado en NDJSON.
Cómo Funciona la Replicación
- Un cliente escribe un par clave-valor en cualquier nodo del clúster.
- El nodo receptor persiste la escritura en su WAL y la aplica al almacenamiento en memoria.
- Si el nodo tiene un padre configurado, reenvía la escritura upstream mediante el endpoint
/v1/replicate. - Si el nodo tiene hijos, transmite la escritura a todas las réplicas conectadas a través del flujo de replicación.
- Cada réplica aplica el cambio en su propio almacenamiento, manteniendo una copia eventualmente consistente.
Protocolo de Replicación
El endpoint de replicación (POST /v1/replicate) usa NDJSON (JSON Delimitado por Nueva Línea) como formato de transmisión. El protocolo soporta estos tipos de comando:
| Comando | Descripción |
|---|---|
set |
Una clave fue creada o actualizada |
delete |
Una clave fue eliminada |
BASELINE_BEGIN |
Inicio de una instantánea completa de colección |
BASELINE_END |
Fin de una instantánea completa de colección |
ping |
Heartbeat de mantenimiento |
Ejemplo: Flujo de Replicación
1{"type":"BASELINE_BEGIN","collection":"my-collection","snapshotSeq":1}
2{"type":"SET","collection":"my-collection","key":"usuario:alice","value":{"rol":"admin"},"version":1,"timestamp":1700000000}
3{"type":"BASELINE_END","collection":"my-collection","seq":1}
4{"type":"SET","collection":"my-collection","key":"usuario:bob","value":{"rol":"usuario"},"version":1,"timestamp":1700000001}
5{"type":"PING","timestamp":1700000010}
Estado del Nodo
El endpoint /v1/status expone la topología de replicación:
curl "https://api.hola.cloud/v1/status" \
-H "apikey: tu-api-key" \
-H "secret: tu-api-secret"GET /v1/status HTTP/1.1
Host: api.hola.cloud
apikey: tu-api-key
secret: tu-api-secret
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://api.hola.cloud/v1/status", nil)
if err != nil {
panic(err)
}
req.Header.Set("apikey", "tu-api-key")
req.Header.Set("secret", "tu-api-secret")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(responseBody))
}
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.hola.cloud/v1/status',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'apikey: tu-api-key',
'secret: tu-api-secret',
],
]);
$response = curl_exec($ch);
if ($response === false) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
echo $response;
import requests
headers = {
"apikey": "tu-api-key",
"secret": "tu-api-secret",
}
response = requests.request(
"GET",
"https://api.hola.cloud/v1/status",
headers=headers
)
print(response.text)
const response = await fetch("https://api.hola.cloud/v1/status", {
method: "GET",
headers: {
"apikey": "tu-api-key",
"secret": "tu-api-secret"
}
});
console.log(await response.text());
const response = await fetch("https://api.hola.cloud/v1/status", {
method: "GET",
headers: {
"apikey": "tu-api-key",
"secret": "tu-api-secret"
}
});
const text = await response.text();
console.log(text);
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.hola.cloud/v1/status"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.header("apikey", "tu-api-key")
.header("secret", "tu-api-secret")
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}Respuesta:
1{
2 "node": "node-1",
3 "role": "primary",
4 "parent": "",
5 "collections": {
6 "mi-coleccion": {
7 "keys": 42,
8 "lastSeq": 128,
9 "walSizeBytes": 65536
10 }
11 },
12 "children": 2,
13 "uptimeSeconds": 86400,
14 "replication": {
15 "enabled": true,
16 "parent_connected": false
17 }
18}
Métricas del Nodo
Monitorea el rendimiento con el endpoint /v1/metrics:
curl "https://api.hola.cloud/v1/metrics" \
-H "apikey: tu-api-key" \
-H "secret: tu-api-secret"GET /v1/metrics HTTP/1.1
Host: api.hola.cloud
apikey: tu-api-key
secret: tu-api-secret
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://api.hola.cloud/v1/metrics", nil)
if err != nil {
panic(err)
}
req.Header.Set("apikey", "tu-api-key")
req.Header.Set("secret", "tu-api-secret")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(responseBody))
}
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.hola.cloud/v1/metrics',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'apikey: tu-api-key',
'secret: tu-api-secret',
],
]);
$response = curl_exec($ch);
if ($response === false) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
echo $response;
import requests
headers = {
"apikey": "tu-api-key",
"secret": "tu-api-secret",
}
response = requests.request(
"GET",
"https://api.hola.cloud/v1/metrics",
headers=headers
)
print(response.text)
const response = await fetch("https://api.hola.cloud/v1/metrics", {
method: "GET",
headers: {
"apikey": "tu-api-key",
"secret": "tu-api-secret"
}
});
console.log(await response.text());
const response = await fetch("https://api.hola.cloud/v1/metrics", {
method: "GET",
headers: {
"apikey": "tu-api-key",
"secret": "tu-api-secret"
}
});
const text = await response.text();
console.log(text);
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.hola.cloud/v1/metrics"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.header("apikey", "tu-api-key")
.header("secret", "tu-api-secret")
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}Respuesta:
1{
2 "writes_total": 15000,
3 "reads_total": 82000,
4 "replication_commands_sent_total": 15000,
5 "replication_commands_received_total": 0,
6 "children_connected": 2,
7 "parent_connected": false
8}
Verificaciones de Disponibilidad
El endpoint /readyz verifica que un nodo esté listo para recibir tráfico. Para las réplicas, esto significa que la conexión con el padre debe estar establecida y el WAL completamente reproducido. Para los primarios, la disponibilidad es inmediata después del inicio.
curl "https://api.hola.cloud/readyz"GET /readyz HTTP/1.1
Host: api.hola.cloud
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://api.hola.cloud/readyz", nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(responseBody))
}
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.hola.cloud/readyz',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
]);
$response = curl_exec($ch);
if ($response === false) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
echo $response;
import requests
response = requests.request(
"GET",
"https://api.hola.cloud/readyz"
)
print(response.text)
const response = await fetch("https://api.hola.cloud/readyz", {
method: "GET"
});
console.log(await response.text());
const response = await fetch("https://api.hola.cloud/readyz", {
method: "GET"
});
const text = await response.text();
console.log(text);
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.hola.cloud/readyz"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}Respuesta cuando no está listo:
1{"ok":false,"node":"node-2","role":"replica","ready":false,"checks":{"wal_replayed":true,"parent_connected":false}}
Backends de WAL Intercambiables
KVNode soporta múltiples backends de WAL, seleccionables al iniciar:
| Backend | Descripción | Caso de Uso |
|---|---|---|
| Memoria | Búfer en proceso (no durable) | Desarrollo, pruebas, cachés efímeras |
| Kafka | Log durable via Apache Kafka | Despliegues de producción de alto rendimiento |
| PostgreSQL | WAL almacenado en tablas de PostgreSQL | Entornos que ya usan Postgres |
| Redis | WAL almacenado en streams de Redis | Arquitecturas de baja latencia centradas en Redis |
| MongoDB | WAL almacenado en colecciones de MongoDB | Despliegues centrados en MongoDB |
Cada backend implementa la misma interfaz de WAL, por lo que cambiar entre ellos no requiere modificaciones a nivel de aplicación.
Comentarios