commit 4381350286e1e5ac15e582161b9602cb7e2b1a17 Author: Sergey Antropoff Date: Fri Oct 9 07:08:09 2026 +0300 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b5ba856 --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# Скопируйте в .env: cp .env.example .env +# Либо запустите make up — цель создаст .env сама. + +NEBULA_VERSION=v3.8.0 +# Studio 3.10 — выпуск, который документация NebulaGraph 3.8 ставит рядом с графом 3.8. +STUDIO_VERSION=v3.10.0 +ES_VERSION=7.17.29 + +# Пароль встроенного пользователя elastic. Только буквы и цифры: +# он подставляется в nGQL SIGN IN TEXT SERVICE. +ELASTIC_PASSWORD=NebulaEsLab2026 + +# Пароль контейнера PKCS#12 для транспортного TLS Elasticsearch. +ES_CERT_PASSWORD=NebulaCertLab2026 + +# 512m на узел — запас для Docker Desktop. На сервере можно -Xms1g -Xmx1g. +ES_JAVA_OPTS=-Xms512m -Xmx512m + +# true имеет смысл на Linux, где memlock разрешён. На Docker Desktop чаще false. +ES_MEMORY_LOCK=false + +# false не даёт Elasticsearch остановить запись, если диск хоста почти полон. +# На сервере со свободным местом поставьте true. +ES_DISK_THRESHOLD=false + +# Подсеть моста Docker. Если адрес занят, смените подсеть и все IP ниже. +DOCKER_SUBNET=172.30.80.0/24 +METAD0_IP=172.30.80.11 +METAD1_IP=172.30.80.12 +METAD2_IP=172.30.80.13 +STORAGED0_IP=172.30.80.21 +STORAGED1_IP=172.30.80.22 +STORAGED2_IP=172.30.80.23 +GRAPHD_IP=172.30.80.31 +LISTENER_IP=172.30.80.41 +ES01_IP=172.30.80.51 +ES02_IP=172.30.80.52 +ES03_IP=172.30.80.53 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15735a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +# NebulaGraph 3.8 + Elasticsearch 7 + Nebula Studio +# Рецепты — однострочные команды docker compose. +# На Windows запускайте из WSL или Git Bash. Те же команды есть в README. + +COMPOSE = docker compose + +.PHONY: help env up init verify console ps logs es-health check config down clean + +help: + @echo "make up поднять NebulaGraph, Elasticsearch и Studio" + @echo "make init ADD HOSTS и SIGN IN TEXT SERVICE" + @echo "make verify init, listener, полнотекстовый индекс и поиск" + @echo "make console интерактивная консоль nGQL" + @echo "make ps статус контейнеров" + @echo "make logs последние логи" + @echo "make es-health состояние кластера Elasticsearch" + @echo "make check vm.max_map_count внутри Docker" + @echo "make down остановить, тома сохранить" + @echo "make clean остановить и удалить тома" + +.env: + cp .env.example .env + +env: .env + +up: .env + $(COMPOSE) up -d + +init: up + $(COMPOSE) --profile ops run --rm --no-deps -e OPS_COMMAND=init ops + +verify: up + $(COMPOSE) --profile ops run --rm --no-deps -e OPS_COMMAND=verify ops + +console: .env + $(COMPOSE) --profile ops run --rm --no-deps console + +ps: + $(COMPOSE) ps -a + +logs: + $(COMPOSE) logs --tail=80 + +es-health: .env + $(COMPOSE) exec -T es01 sh -c 'curl -fsS -u "elastic:$$ELASTIC_PASSWORD" http://127.0.0.1:9200/_cluster/health?pretty' + +check: + docker run --rm alpine sysctl vm.max_map_count + +config: .env + $(COMPOSE) config --quiet && echo "compose config ok" + +down: + $(COMPOSE) --profile ops down + +clean: + $(COMPOSE) --profile ops down -v diff --git a/README.md b/README.md new file mode 100644 index 0000000..25249ea --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# NebulaGraph 3.8 + Elasticsearch 7 + Studio + +## Быстрый запуск + +```bash +make verify +make es-health +make ps +``` + +Первый `make verify` сам создаёт `.env` из `.env.example`, поднимает контейнеры и проводит проверку. + +| Команда | Что делает | +| --- | --- | +| `make verify` | Поднимает стек и проверяет регистрацию storage, вход в Elasticsearch, listener, полнотекстовый индекс и поиск `doc-001`. | +| `make es-health` | Показывает состояние кластера Elasticsearch. Ожидается `green` и три узла. | +| `make ps` | Печатает статус контейнеров. `es-certs` в `Exited (0)` значит, что сертификат уже создан. | + +Локальный кластер: + +| Сервис | Число | Адрес в сети Docker | Порт | +| --- | --- | --- | --- | +| metad | 3 | `172.30.80.11`–`.13` | 9559 | +| storaged | 3 | `172.30.80.21`–`.23` | 9779 | +| graphd | 1 | `172.30.80.31` | 9669 | +| storaged-listener | 1 | `172.30.80.41` | 9789 | +| Elasticsearch 7.17 | 3 | `172.30.80.51`–`.53` | 9200 | +| Nebula Studio 3.10 | 1 | имя `studio` | 7001 | + +С хоста опубликованы только `127.0.0.1:9669` (graphd), `127.0.0.1:9200` (Elasticsearch) и `127.0.0.1:7001` (Studio). + +Адреса заданы в `.env` и совпадают на macOS, Linux и Windows: контейнеры видят одну сеть `nebula-stack-net`, а не localhost хоста. + +## Что нужно + +- Docker Engine или Docker Desktop и Docker Compose v2 (`docker compose`). +- Для `make`: macOS, Linux, либо Windows через WSL или Git Bash. +- Память Docker: комфортно от 8 ГБ. В `.env` у каждого узла Elasticsearch куча 512 МБ. +- Место на диске под образы (около 3–4 ГБ). + +На Linux перед первым запуском ядро должно разрешать Elasticsearch много memory map: + +```bash +sudo sysctl -w vm.max_map_count=262144 +echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-elasticsearch.conf +sudo sysctl --system +``` + +На macOS и Windows это настройка виртуальной машины Docker Desktop, обычно уже `262144`. Проверка из любого хоста: + +```bash +make check +``` + +Число в ответе должно быть не меньше `262144`. + +## Запуск + +```bash +cp .env.example .env # make up сделает это сам +make up +make init # ADD HOSTS и SIGN IN TEXT SERVICE +make verify # listener, индекс и поиск по тестовым вершинам +``` + +`make verify` включает `make init`. Контейнер `es-certs` после успеха в `make ps` будет `Exited (0)`: сертификат транспортного TLS уже записан в том. + +Studio: + +| Поле | Значение | +| --- | --- | +| Graphd IP | `172.30.80.31` | +| Port | `9669` | +| Username | `root` | +| Password | `nebula` | + +Адрес `127.0.0.1` в Studio не подходит: запрос к graphd уходит из контейнера Studio. `172.30.80.31` — адрес graphd в сети Docker. Авторизация NebulaGraph в этом стенде выключена, пароль `nebula` принимает консоль и Studio. + +Интерактивная консоль: + +```bash +make console +``` + +Остановить, не удаляя данные: `make down`. Удалить тома: `make clean`. + +### Windows без make + +В PowerShell из каталога проекта: + +```powershell +Copy-Item .env.example .env +docker compose up -d +docker compose --profile ops run --rm --no-deps -e OPS_COMMAND=init ops +docker compose --profile ops run --rm --no-deps -e OPS_COMMAND=verify ops +docker compose --profile ops run --rm --no-deps console +``` + +### Полезные команды + +```bash +make ps +make logs +make es-health +docker compose logs -f storaged-listener +``` + +Проверка Elasticsearch с хоста (пароль из `.env`): + +```bash +curl -u "elastic:NebulaEsLab2026" "http://127.0.0.1:9200/_cluster/health?pretty" +curl -u "elastic:NebulaEsLab2026" "http://127.0.0.1:9200/_cat/indices?v" +``` + +Если меняли `ELASTIC_PASSWORD` в `.env`, подставьте его. + +## Как подключён listener + +Полнотекстовый индекс NebulaGraph пишет в Elasticsearch не сам storaged, а отдельный процесс Raft Listener. Это тот же бинарник `nebula-storaged`, но со своим портом и каталогом WAL. В compose у `storaged-listener` свой entrypoint: флаги listener не смешиваются с обычным `nebula-storaged.conf`. + +Слушает он `172.30.80.41:9789` (`LISTENER_IP` в `.env`). В кластер хранения через `ADD HOSTS` его не добавляют. Для каждого графового пространства listener регистрируется один раз, все адреса — одним оператором `ADD LISTENER`. Пока `SHOW LISTENER` не покажет `ONLINE` по всем партициям, полнотекстовый индекс не создают. + +Учётные данные Elasticsearch глобальные для кластера, не для пространства: один `SIGN IN TEXT SERVICE` на все space. Повторный вход делается после `SIGN OUT TEXT SERVICE`. + +`make init` и `make verify` выполняют команды ниже сами и повторяют попытку, пока метаданные и listener не станут готовы. Вручную между `CREATE SPACE`, `CREATE TAG` и индексом нужно подождать одно-два сердцебиения meta (около 20 секунд): иначе пространство или тег ещё не видны. + +Пароль в примерах — значение `ELASTIC_PASSWORD` из `.env`. Адреса — значения по умолчанию из `.env.example`. + +### 1. Узлы хранения + +```ngql +ADD HOSTS + "172.30.80.21":9779, + "172.30.80.22":9779, + "172.30.80.23":9779; +SHOW HOSTS; +``` + +В `SHOW HOSTS` три строки `ONLINE`. До этой команды контейнеры storaged уже запущены, но метаданные ещё не отдают им партиции. `make ps` может показывать их unhealthy, пока `ADD HOSTS` не пройдёт. + +### 2. Elasticsearch + +Транспорт между `es01`–`es03` зашифрован общим сертификатом, HTTP оставлен без TLS и закрыт basic-авторизацией. NebulaGraph ходит по HTTP, поэтому в `SIGN IN` схема `HTTP`, пользователь `elastic` и пароль из `.env`. + +```ngql +SIGN IN TEXT SERVICE + (172.30.80.51:9200, HTTP, "elastic", "NebulaEsLab2026"), + (172.30.80.52:9200, HTTP, "elastic", "NebulaEsLab2026"), + (172.30.80.53:9200, HTTP, "elastic", "NebulaEsLab2026"); +SHOW TEXT SEARCH CLIENTS; +``` + +Ожидаются три строки `ELASTICSEARCH`. Клиент один на весь кластер. Сброс: `SIGN OUT TEXT SERVICE;`. + +### 3. Пространство и listener + +```ngql +CREATE SPACE IF NOT EXISTS demo ( + partition_num = 12, + replica_factor = 3, + vid_type = FIXED_STRING(64) +); + +USE demo; +ADD LISTENER ELASTICSEARCH "172.30.80.41":9789; +SHOW LISTENER; +``` + +`USE` и `ADD LISTENER` должны быть в одной сессии консоли. `SHOW LISTENER` — тоже после `USE demo`. Для 12 партиций будет 12 строк. Ждите `ONLINE` в каждой. Снять listener с пространства: `REMOVE LISTENER ELASTICSEARCH;`. + +### 4. Схема, индекс и данные + +```ngql +USE demo; +CREATE TAG IF NOT EXISTS document ( + title STRING, + content STRING +); +CREATE FULLTEXT TAG INDEX document_search + ON document(title, content) + ANALYZER = "standard"; +SHOW FULLTEXT INDEXES; +``` + +В NebulaGraph 3.8 у `CREATE FULLTEXT TAG INDEX` нет `IF NOT EXISTS`. Повторный запуск отвечает `Existed!`, `make verify` это учитывает. + +```ngql +INSERT VERTEX document(title, content) VALUES + "doc-001":("Kubernetes", "Container orchestration platform"), + "doc-002":("NebulaGraph", "Distributed graph database"); +``` + +Индексация идёт асинхронно через listener. Для уже лежащих в пространстве данных: + +```ngql +USE demo; +REBUILD FULLTEXT INDEX; +``` + +Проверка со стороны NebulaGraph — запрос в Elasticsearch через индекс: + +```ngql +USE demo; +LOOKUP ON document + WHERE ES_QUERY(document_search, "Kubernetes") + YIELD id(vertex); +``` + +В ответе должен быть `doc-001`. Анализатор `standard` приводит слово к нижнему регистру, поэтому в Elasticsearch документ ищется и по `Kubernetes`. + +Появление индекса снаружи: + +```bash +curl -u "elastic:NebulaEsLab2026" "http://127.0.0.1:9200/_cat/indices?v" +``` + +## Если адреса другие + +Поменяйте подсеть и IP в `.env`, затем `make clean && make up && make verify`. Команды nGQL и поле Graphd IP в Studio берите уже из своего `.env`: listener и `ADD HOSTS` должны указывать те же адреса, что `--local_ip` у контейнеров. + +На сервере со свободным диском в `.env` поставьте `ES_DISK_THRESHOLD=true`. Кучу Elasticsearch увеличивают через `ES_JAVA_OPTS`. `ES_MEMORY_LOCK=true` включайте на Linux, где для контейнера разрешён memlock. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..41b31de --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,442 @@ +name: nebula-stack + +x-logging: &logging + driver: json-file + options: + max-size: "10m" + max-file: "3" + +x-nebula: &nebula + restart: unless-stopped + cap_add: + - SYS_PTRACE + logging: *logging + +x-es-env: &es-env + cluster.name: nebula-elasticsearch + discovery.seed_hosts: ${ES01_IP}:9300,${ES02_IP}:9300,${ES03_IP}:9300 + cluster.initial_master_nodes: es01,es02,es03 + bootstrap.memory_lock: "${ES_MEMORY_LOCK}" + network.host: "0.0.0.0" + xpack.security.enabled: "true" + xpack.security.http.ssl.enabled: "false" + xpack.security.transport.ssl.enabled: "true" + xpack.security.transport.ssl.verification_mode: certificate + xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12 + xpack.security.transport.ssl.truststore.path: certs/elastic-stack-ca.p12 + xpack.security.transport.ssl.keystore.password: ${ES_CERT_PASSWORD} + xpack.security.transport.ssl.truststore.password: ${ES_CERT_PASSWORD} + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD} + ES_JAVA_OPTS: ${ES_JAVA_OPTS} + TAKE_FILE_OWNERSHIP: "true" + cluster.routing.allocation.disk.threshold_enabled: "${ES_DISK_THRESHOLD}" + ingest.geoip.downloader.enabled: "false" + +x-es-node: &es-node + image: docker.elastic.co/elasticsearch/elasticsearch:${ES_VERSION} + user: "0:0" + restart: unless-stopped + logging: *logging + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + depends_on: + es-certs: + condition: service_completed_successfully + healthcheck: + test: + - CMD-SHELL + - 'curl -fsS -u "elastic:$$ELASTIC_PASSWORD" http://127.0.0.1:9200/_cluster/health >/dev/null' + interval: 15s + timeout: 10s + retries: 20 + start_period: 90s + +services: + es-certs: + image: docker.elastic.co/elasticsearch/elasticsearch:${ES_VERSION} + user: "0:0" + restart: "no" + logging: *logging + environment: + ES_CERT_PASSWORD: ${ES_CERT_PASSWORD} + ES01_IP: ${ES01_IP} + ES02_IP: ${ES02_IP} + ES03_IP: ${ES03_IP} + entrypoint: ["bash", "-ec"] + command: + - | + if [ -s /certs/elastic-certificates.p12 ] && [ -s /certs/elastic-stack-ca.p12 ]; then + exit 0 + fi + rm -f /certs/elastic-certificates.p12 /certs/elastic-stack-ca.p12 + bin/elasticsearch-certutil ca \ + --silent \ + --out /certs/elastic-stack-ca.p12 \ + --pass "$$ES_CERT_PASSWORD" + bin/elasticsearch-certutil cert \ + --silent \ + --ca /certs/elastic-stack-ca.p12 \ + --ca-pass "$$ES_CERT_PASSWORD" \ + --pass "$$ES_CERT_PASSWORD" \ + --name nebula-es \ + --dns es01,es02,es03 \ + --ip "$$ES01_IP,$$ES02_IP,$$ES03_IP,127.0.0.1" \ + --out /certs/elastic-certificates.p12 + chown 1000:0 /certs/elastic-certificates.p12 /certs/elastic-stack-ca.p12 + chmod 600 /certs/elastic-certificates.p12 /certs/elastic-stack-ca.p12 + volumes: + - es_certs:/certs + networks: + - backend + + es01: + <<: *es-node + hostname: es01 + environment: + <<: *es-env + node.name: es01 + network.publish_host: ${ES01_IP} + ports: + - "127.0.0.1:9200:9200" + volumes: &es-volumes + - esdata01:/usr/share/elasticsearch/data + - es_certs:/usr/share/elasticsearch/config/certs:ro + networks: + backend: + ipv4_address: ${ES01_IP} + + es02: + <<: *es-node + hostname: es02 + environment: + <<: *es-env + node.name: es02 + network.publish_host: ${ES02_IP} + volumes: + - esdata02:/usr/share/elasticsearch/data + - es_certs:/usr/share/elasticsearch/config/certs:ro + networks: + backend: + ipv4_address: ${ES02_IP} + + es03: + <<: *es-node + hostname: es03 + environment: + <<: *es-env + node.name: es03 + network.publish_host: ${ES03_IP} + volumes: + - esdata03:/usr/share/elasticsearch/data + - es_certs:/usr/share/elasticsearch/config/certs:ro + networks: + backend: + ipv4_address: ${ES03_IP} + + metad0: + <<: *nebula + image: vesoft/nebula-metad:${NEBULA_VERSION} + hostname: metad0 + environment: + USER: root + command: &meta-cmd + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${METAD0_IP} + - --ws_ip=${METAD0_IP} + - --port=9559 + - --ws_http_port=19559 + - --data_path=/data/meta + - --logtostderr=true + - --redirect_stdout=false + healthcheck: &meta-health + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${METAD0_IP}/9559'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - meta0:/data/meta + networks: + backend: + ipv4_address: ${METAD0_IP} + + metad1: + <<: *nebula + image: vesoft/nebula-metad:${NEBULA_VERSION} + hostname: metad1 + environment: + USER: root + command: + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${METAD1_IP} + - --ws_ip=${METAD1_IP} + - --port=9559 + - --ws_http_port=19559 + - --data_path=/data/meta + - --logtostderr=true + - --redirect_stdout=false + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${METAD1_IP}/9559'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - meta1:/data/meta + networks: + backend: + ipv4_address: ${METAD1_IP} + + metad2: + <<: *nebula + image: vesoft/nebula-metad:${NEBULA_VERSION} + hostname: metad2 + environment: + USER: root + command: + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${METAD2_IP} + - --ws_ip=${METAD2_IP} + - --port=9559 + - --ws_http_port=19559 + - --data_path=/data/meta + - --logtostderr=true + - --redirect_stdout=false + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${METAD2_IP}/9559'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - meta2:/data/meta + networks: + backend: + ipv4_address: ${METAD2_IP} + + storaged0: + <<: *nebula + image: vesoft/nebula-storaged:${NEBULA_VERSION} + hostname: storaged0 + environment: + USER: root + command: &storage-cmd + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${STORAGED0_IP} + - --ws_ip=${STORAGED0_IP} + - --port=9779 + - --ws_http_port=19779 + - --data_path=/data/storage + - --logtostderr=true + - --redirect_stdout=false + depends_on: &meta-ready + metad0: + condition: service_healthy + metad1: + condition: service_healthy + metad2: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${STORAGED0_IP}/9779'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - storage0:/data/storage + networks: + backend: + ipv4_address: ${STORAGED0_IP} + + storaged1: + <<: *nebula + image: vesoft/nebula-storaged:${NEBULA_VERSION} + hostname: storaged1 + environment: + USER: root + command: + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${STORAGED1_IP} + - --ws_ip=${STORAGED1_IP} + - --port=9779 + - --ws_http_port=19779 + - --data_path=/data/storage + - --logtostderr=true + - --redirect_stdout=false + depends_on: *meta-ready + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${STORAGED1_IP}/9779'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - storage1:/data/storage + networks: + backend: + ipv4_address: ${STORAGED1_IP} + + storaged2: + <<: *nebula + image: vesoft/nebula-storaged:${NEBULA_VERSION} + hostname: storaged2 + environment: + USER: root + command: + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${STORAGED2_IP} + - --ws_ip=${STORAGED2_IP} + - --port=9779 + - --ws_http_port=19779 + - --data_path=/data/storage + - --logtostderr=true + - --redirect_stdout=false + depends_on: *meta-ready + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${STORAGED2_IP}/9779'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - storage2:/data/storage + networks: + backend: + ipv4_address: ${STORAGED2_IP} + + graphd: + <<: *nebula + image: vesoft/nebula-graphd:${NEBULA_VERSION} + hostname: graphd + environment: + USER: root + command: + - --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 + - --local_ip=${GRAPHD_IP} + - --ws_ip=${GRAPHD_IP} + - --port=9669 + - --ws_http_port=19669 + - --logtostderr=true + - --redirect_stdout=false + depends_on: *meta-ready + ports: + - "127.0.0.1:9669:9669" + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${GRAPHD_IP}/9669'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + networks: + backend: + ipv4_address: ${GRAPHD_IP} + + storaged-listener: + image: vesoft/nebula-storaged:${NEBULA_VERSION} + hostname: storaged-listener + restart: unless-stopped + cap_add: + - SYS_PTRACE + logging: *logging + environment: + USER: root + entrypoint: ["bash", "-ec"] + command: + - | + mkdir -p /data/listener /logs + exec /usr/local/nebula/bin/nebula-storaged \ + --daemonize=false \ + --containerized=true \ + --meta_server_addrs=${METAD0_IP}:9559,${METAD1_IP}:9559,${METAD2_IP}:9559 \ + --local_ip=${LISTENER_IP} \ + --ws_ip=${LISTENER_IP} \ + --port=9789 \ + --ws_http_port=19789 \ + --heartbeat_interval_secs=10 \ + --listener_path=/data/listener \ + --data_path=/data \ + --log_dir=/logs \ + --logtostderr=true \ + --redirect_stdout=false + depends_on: *meta-ready + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/${LISTENER_IP}/9789'"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 20s + volumes: + - listener0:/data + networks: + backend: + ipv4_address: ${LISTENER_IP} + + studio: + image: vesoft/nebula-graph-studio:${STUDIO_VERSION} + hostname: studio + restart: unless-stopped + logging: *logging + depends_on: + - graphd + ports: + - "127.0.0.1:7001:7001" + networks: + - backend + + console: + image: vesoft/nebula-console:${NEBULA_VERSION} + profiles: ["ops"] + entrypoint: ["nebula-console"] + command: ["-addr", "graphd", "-port", "9669", "-u", "root", "-p", "nebula"] + stdin_open: true + tty: true + networks: + - backend + + ops: + image: vesoft/nebula-console:${NEBULA_VERSION} + profiles: ["ops"] + entrypoint: ["sh", "-c"] + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD} + STORAGED0_IP: ${STORAGED0_IP} + STORAGED1_IP: ${STORAGED1_IP} + STORAGED2_IP: ${STORAGED2_IP} + ES01_IP: ${ES01_IP} + ES02_IP: ${ES02_IP} + ES03_IP: ${ES03_IP} + LISTENER_IP: ${LISTENER_IP} + OPS_COMMAND: verify + command: + - | + sed 's/\r$$//' /scripts/cluster.sh | sh -s "$${OPS_COMMAND:-verify}" + volumes: + - ./scripts/cluster.sh:/scripts/cluster.sh:ro + networks: + - backend + +networks: + backend: + name: nebula-stack-net + driver: bridge + ipam: + config: + - subnet: ${DOCKER_SUBNET} + +volumes: + es_certs: + esdata01: + esdata02: + esdata03: + meta0: + meta1: + meta2: + storage0: + storage1: + storage2: + listener0: diff --git a/scripts/cluster.sh b/scripts/cluster.sh new file mode 100644 index 0000000..97cb011 --- /dev/null +++ b/scripts/cluster.sh @@ -0,0 +1,213 @@ +#!/bin/sh +# Регистрация NebulaGraph и проверка полнотекстового индекса. +# Запускается внутри контейнера ops, поэтому одинаково работает на macOS, Linux и Windows. +set -eu + +: "${ELASTIC_PASSWORD:?ELASTIC_PASSWORD is required}" +: "${STORAGED0_IP:?}" +: "${STORAGED1_IP:?}" +: "${STORAGED2_IP:?}" +: "${ES01_IP:?}" +: "${ES02_IP:?}" +: "${ES03_IP:?}" +: "${LISTENER_IP:?}" + +if ! command -v nebula-console >/dev/null 2>&1; then + if [ -x /usr/local/bin/nebula-console ]; then + PATH="/usr/local/bin:${PATH}" + else + found=$(find / -name nebula-console -type f 2>/dev/null | head -n 1 || true) + if [ -z "$found" ]; then + echo "nebula-console binary not found" + exit 1 + fi + PATH="$(dirname "$found"):${PATH}" + fi +fi + +NGQL_OUT= + +run_ngql() { + echo "" + echo "+ $1" + set +e + NGQL_OUT=$(nebula-console -addr graphd -port 9669 -u root -p nebula -e "$1" 2>&1) + NGQL_RC=$? + set -e + printf '%s\n' "$NGQL_OUT" + if [ "$NGQL_RC" -eq 0 ] && ! printf '%s\n' "$NGQL_OUT" | grep -q '\[ERROR'; then + return 0 + fi + return 1 +} + +already_exists() { + printf '%s\n' "$NGQL_OUT" | grep -Eqi 'exist' +} + +wait_until() { + attempts=$1 + shift + n=0 + while [ "$n" -lt "$attempts" ]; do + n=$((n + 1)) + if "$@"; then + return 0 + fi + echo "retry ${n}/${attempts}" + sleep 5 + done + return 1 +} + +console_up() { + run_ngql "SHOW HOSTS;" +} + +hosts_online() { + run_ngql "SHOW HOSTS;" || return 1 + on=$(printf '%s\n' "$NGQL_OUT" | grep -c ONLINE || true) + off=$(printf '%s\n' "$NGQL_OUT" | grep -c OFFLINE || true) + [ "$on" -ge 3 ] && [ "$off" -eq 0 ] +} + +listener_online() { + run_ngql "USE demo; SHOW LISTENER;" || return 1 + on=$(printf '%s\n' "$NGQL_OUT" | grep -c ONLINE || true) + off=$(printf '%s\n' "$NGQL_OUT" | grep -c OFFLINE || true) + [ "$on" -ge 12 ] && [ "$off" -eq 0 ] +} + +add_hosts() { + stmt="ADD HOSTS \"${STORAGED0_IP}\":9779, \"${STORAGED1_IP}\":9779, \"${STORAGED2_IP}\":9779;" + if run_ngql "$stmt"; then + return 0 + fi + already_exists +} + +sign_in() { + stmt="SIGN IN TEXT SERVICE (${ES01_IP}:9200, HTTP, \"elastic\", \"${ELASTIC_PASSWORD}\"), (${ES02_IP}:9200, HTTP, \"elastic\", \"${ELASTIC_PASSWORD}\"), (${ES03_IP}:9200, HTTP, \"elastic\", \"${ELASTIC_PASSWORD}\");" + if run_ngql "$stmt"; then + return 0 + fi + if already_exists; then + return 0 + fi + run_ngql "SHOW TEXT SEARCH CLIENTS;" || return 1 + clients=$(printf '%s\n' "$NGQL_OUT" | grep -c ELASTICSEARCH || true) + [ "$clients" -ge 3 ] +} + +do_init() { + echo "=== wait for graphd ===" + wait_until 40 console_up + + echo "=== ADD HOSTS ===" + wait_until 12 add_hosts + echo "=== wait until storage is ONLINE ===" + wait_until 36 hosts_online + + echo "=== SIGN IN TEXT SERVICE ===" + wait_until 24 sign_in + run_ngql "SHOW TEXT SEARCH CLIENTS;" +} + +create_space() { + run_ngql "CREATE SPACE IF NOT EXISTS demo (partition_num = 12, replica_factor = 3, vid_type = FIXED_STRING(64));" +} + +create_tag() { + run_ngql "USE demo; CREATE TAG IF NOT EXISTS document (title STRING, content STRING);" +} + +add_listener() { + if run_ngql "USE demo; ADD LISTENER ELASTICSEARCH \"${LISTENER_IP}\":9789;"; then + return 0 + fi + already_exists +} + +create_index() { + if run_ngql "USE demo; CREATE FULLTEXT TAG INDEX document_search ON document(title, content) ANALYZER = \"standard\";"; then + return 0 + fi + if printf '%s\n' "$NGQL_OUT" | grep -q 'SyntaxError'; then + echo "fatal nGQL syntax error" + exit 1 + fi + already_exists +} + +insert_docs() { + if run_ngql "USE demo; INSERT VERTEX document(title, content) VALUES \"doc-001\":(\"Kubernetes\", \"Container orchestration platform\"), \"doc-002\":(\"NebulaGraph\", \"Distributed graph database\");"; then + return 0 + fi + already_exists +} + +lookup_doc() { + run_ngql "USE demo; LOOKUP ON document WHERE ES_QUERY(document_search, \"Kubernetes\") YIELD id(vertex);" || return 1 + printf '%s\n' "$NGQL_OUT" | grep -q 'doc-001' +} + +es_has_kubernetes() { + if ! command -v bash >/dev/null 2>&1 || ! command -v base64 >/dev/null 2>&1; then + echo "bash/base64 are not in the console image; LOOKUP already queried Elasticsearch" + return 0 + fi + ES01_IP="$ES01_IP" ELASTIC_PASSWORD="$ELASTIC_PASSWORD" bash -c ' + auth=$(printf "%s" "elastic:${ELASTIC_PASSWORD}" | base64 | tr -d "\n\r") + exec 3<>"/dev/tcp/${ES01_IP}/9200" + printf "GET /_search?q=Kubernetes HTTP/1.0\r\nHost: elasticsearch\r\nAuthorization: Basic %s\r\nConnection: close\r\n\r\n" "$auth" >&3 + resp=$(cat <&3 || true) + printf "%s\n" "$resp" + printf "%s\n" "$resp" | grep -q Kubernetes + ' +} + +do_verify() { + echo "=== CREATE SPACE demo ===" + wait_until 24 create_space + + echo "=== CREATE TAG document ===" + wait_until 24 create_tag + + echo "=== ADD LISTENER ELASTICSEARCH ===" + wait_until 12 add_listener + + echo "=== wait until listener is ONLINE ===" + wait_until 36 listener_online + + echo "=== CREATE FULLTEXT TAG INDEX ===" + wait_until 24 create_index + run_ngql "USE demo; SHOW FULLTEXT INDEXES;" + + echo "=== INSERT VERTEX ===" + wait_until 12 insert_docs + run_ngql "USE demo; REBUILD FULLTEXT INDEX;" || true + + echo "=== LOOKUP via Elasticsearch ===" + wait_until 24 lookup_doc + + echo "=== direct Elasticsearch search ===" + wait_until 12 es_has_kubernetes + + echo "" + echo "VERIFY OK" +} + +mode="${1:-verify}" +case "$mode" in + init) + do_init + ;; + verify) + do_init + do_verify + ;; + *) + echo "usage: sh cluster.sh init|verify" + exit 2 + ;; +esac