Pyroscope — это мощный инструмент непрерывного профилирования, созданный для мониторинга производительности приложений в реальном времени. В этой статье мы рассмотрим, как быстро развернуть Pyroscope, подключить к нему Node.js приложение и проанализировать поведение кода без использования Grafana Alloy.
Что такое Pyroscope?
Pyroscope — это open-source решение от Grafana Labs для профилирования производительности, которое позволяет:
отслеживать использование CPU, памяти и других ресурсов в реальном времени;
поддерживать профилирование для языков Go, Python, Java, Ruby, Node.js и др.;
сравнивать профили за разные промежутки времени;
интегрироваться с Grafana, Kubernetes и другими инструментами мониторинга.
Цель эксперимента
В рамках этого примера мы:
Запустим Pyroscope как в Docker, так и в Kubernetes (включая Yandex Cloud).
Подключим тестовое Node.js приложение с преднамеренно медленной функцией и функцией с утечкой памяти.
Сгенерируем нагрузку и проанализируем результаты профилирования.
Важно: Grafana Alloy в этом примере не используется — данные отправляются напрямую из Node.js приложения в Pyroscope.
Быстрый старт с Docker Compose
git clone https://github.com/patsevanton/profiling-nodejs-app-by-pyroscope-with-sdk docker-compose up -d
Docker-Compose.yaml
# Версия формата файла Docker Compose version: '3' # Определение сервисов/контейнеров services: # Сервис сервера Pyroscope (система для анализа производительности) pyroscope-server: # Используем официальный образ Pyroscope от Grafana с конкретной версией image: grafana/pyroscope:1.13.2 # Пробрасываем порты: порт 4040 контейнера на порт 4040 хоста ports: - "4040:4040" # Сервис Node.js приложения node-app: # Собираем образ из Dockerfile в текущей директории build: . # Пробрасываем порты: порт 3000 контейнера на порт 3000 хоста ports: - "3000:3000" # Переменные окружения для конфигурации приложения environment: # Адрес сервера Pyroscope для отправки данных профилирования PYROSCOPE_SERVER_ADDRESS: "http://pyroscope-server:4040" # Зависимости: этот сервис зависит от pyroscope-server # и будет запущен только после его успешного старта depends_on: - pyroscope-server
Для начала можно запустить нагрузку локально, имитируя активность пользователя:
while true; do \ curl http://localhost:3000/fast; \ curl http://localhost:3000/slow; \ curl http://localhost:3000/leak; \ done
Развёртывание в Kubernetes (Yandex Cloud)
1. Клонируем репозиторий с конфигурацией
git clone https://github.com/patsevanton/profiling-nodejs-app-by-pyroscope-with-sdk cd pyroscope-nodejs
2. Настраиваем инфраструктуру через Terraform
export YC_FOLDER_ID='ваш_folder_id' terraform apply
Terraform код
# Создание внешнего IP-адреса в Yandex Cloud resource "yandex_vpc_address" "addr" { name = "pyroscope-pip" # Имя ресурса внешнего IP-адреса external_ipv4_address { zone_id = yandex_vpc_subnet.pyroscope-a.zone # Зона доступности, где будет выделен IP-адрес } } # Создание публичной DNS-зоны в Yandex Cloud DNS resource "yandex_dns_zone" "apatsev-org-ru" { name = "apatsev-org-ru-zone" # Имя ресурса DNS-зоны zone = "apatsev.org.ru." # Доменное имя зоны (с точкой в конце) public = true # Указание, что зона является публичной # Привязка зоны к VPC-сети, чтобы можно было использовать приватный DNS внутри сети private_networks = [yandex_vpc_network.pyroscope.id] } # Создание DNS-записи типа A, указывающей на внешний IP resource "yandex_dns_recordset" "rs1" { zone_id = yandex_dns_zone.apatsev-org-ru.id # ID зоны, к которой принадлежит запись name = "pyroscope.apatsev.org.ru." # Полное имя записи (поддомен) type = "A" # Тип записи — A (IPv4-адрес) ttl = 200 # Время жизни записи в секундах data = [yandex_vpc_address.addr.external_ipv4_address[0].address] # Значение — внешний IP-адрес, полученный ранее } # Создание DNS-записи типа A, указывающей на внешний IP resource "yandex_dns_recordset" "grafana" { zone_id = yandex_dns_zone.apatsev-org-ru.id # ID зоны, к которой принадлежит запись name = "grafana.apatsev.org.ru." # Полное имя записи (поддомен) type = "A" # Тип записи — A (IPv4-адрес) ttl = 200 # Время жизни записи в секундах data = [yandex_vpc_address.addr.external_ipv4_address[0].address] # Значение — внешний IP-адрес, полученный ранее } # Создание DNS-записи типа A, указывающей на внешний IP resource "yandex_dns_recordset" "nodejs-app" { zone_id = yandex_dns_zone.apatsev-org-ru.id # ID зоны, к которой принадлежит запись name = "nodejs-app.apatsev.org.ru." # Полное имя записи (поддомен) type = "A" # Тип записи — A (IPv4-адрес) ttl = 200 # Время жизни записи в секундах data = [yandex_vpc_address.addr.external_ipv4_address[0].address] # Значение — внешний IP-адрес, полученный ранее }
# Получаем информацию о конфигурации клиента Yandex data "yandex_client_config" "client" {} # Создание сервисного аккаунта для управления Kubernetes resource "yandex_iam_service_account" "sa-k8s-editor" { name = "sa-k8s-editor" # Имя сервисного аккаунта } # Назначение роли "editor" сервисному аккаунту на уровне папки resource "yandex_resourcemanager_folder_iam_member" "sa-k8s-editor-permissions" { role = "editor" # Роль, дающая полные права на ресурсы папки folder_id = data.yandex_client_config.client.folder_id member = "serviceAccount:${yandex_iam_service_account.sa-k8s-editor.id}" # Назначаемый участник } # Пауза, чтобы изменения IAM успели примениться до создания кластера resource "time_sleep" "wait_sa" { create_duration = "20s" depends_on = [ yandex_iam_service_account.sa-k8s-editor, yandex_resourcemanager_folder_iam_member.sa-k8s-editor-permissions ] } # Создание Kubernetes-кластера в Yandex Cloud resource "yandex_kubernetes_cluster" "pyroscope" { name = "pyroscope" # Имя кластера network_id = yandex_vpc_network.pyroscope.id # Сеть, к которой подключается кластер master { version = "1.30" # Версия Kubernetes мастера zonal { zone = yandex_vpc_subnet.pyroscope-a.zone # Зона размещения мастера subnet_id = yandex_vpc_subnet.pyroscope-a.id # Подсеть для мастера } public_ip = true # Включение публичного IP для доступа к мастеру } # Сервисный аккаунт для управления кластером и нодами service_account_id = yandex_iam_service_account.sa-k8s-editor.id node_service_account_id = yandex_iam_service_account.sa-k8s-editor.id release_channel = "STABLE" # Канал обновлений # Зависимость от ожидания применения IAM-ролей depends_on = [time_sleep.wait_sa] } # Группа узлов для Kubernetes-кластера resource "yandex_kubernetes_node_group" "k8s-node-group" { description = "Node group for the Managed Service for Kubernetes cluster" name = "k8s-node-group" cluster_id = yandex_kubernetes_cluster.pyroscope.id version = "1.30" # Версия Kubernetes на нодах scale_policy { fixed_scale { size = 3 # Фиксированное количество нод } } allocation_policy { # Распределение нод по зонам отказоустойчивости location { zone = yandex_vpc_subnet.pyroscope-a.zone } location { zone = yandex_vpc_subnet.pyroscope-b.zone } location { zone = yandex_vpc_subnet.pyroscope-d.zone } } instance_template { platform_id = "standard-v2" # Тип виртуальной машины network_interface { nat = true # Включение NAT для доступа в интернет subnet_ids = [ yandex_vpc_subnet.pyroscope-a.id, yandex_vpc_subnet.pyroscope-b.id, yandex_vpc_subnet.pyroscope-d.id ] } resources { memory = 20 # ОЗУ cores = 4 # Кол-во ядер CPU } boot_disk { type = "network-ssd" # Тип диска size = 128 # Размер диска } } } # Настройка провайдера Helm для установки чарта в Kubernetes provider "helm" { kubernetes { host = yandex_kubernetes_cluster.pyroscope.master[0].external_v4_endpoint # Адрес API Kubernetes cluster_ca_certificate = yandex_kubernetes_cluster.pyroscope.master[0].cluster_ca_certificate # CA-сертификат exec { api_version = "client.authentication.k8s.io/v1beta1" args = ["k8s", "create-token"] # Команда получения токена через CLI Yandex.Cloud command = "yc" } } } # Установка ingress-nginx через Helm resource "helm_release" "ingress_nginx" { name = "ingress-nginx" repository = "https://kubernetes.github.io/ingress-nginx" chart = "ingress-nginx" version = "4.10.6" namespace = "ingress-nginx" create_namespace = true depends_on = [yandex_kubernetes_cluster.pyroscope] set { name = "controller.service.loadBalancerIP" value = yandex_vpc_address.addr.external_ipv4_address[0].address # Присвоение внешнего IP ingress-контроллеру } } # Вывод команды для получения kubeconfig output "k8s_cluster_credentials_command" { value = "yc managed-kubernetes cluster get-credentials --id ${yandex_kubernetes_cluster.pyroscope.id} --external --force" }
# Ресурс для создания сети VPC в Yandex Cloud resource "yandex_vpc_network" "pyroscope" { name = "vpc" # Имя сети VPC } # Ресурс для создания подсети в зоне "ru-central1-a" resource "yandex_vpc_subnet" "pyroscope-a" { v4_cidr_blocks = ["10.0.1.0/24"] # CIDR блок для подсети (IP-диапазон) zone = "ru-central1-a" # Зона, где будет размещена подсеть network_id = yandex_vpc_network.pyroscope.id # ID сети, к которой будет привязана подсеть } # Ресурс для создания подсети в зоне "ru-central1-b" resource "yandex_vpc_subnet" "pyroscope-b" { v4_cidr_blocks = ["10.0.2.0/24"] # CIDR блок для подсети (IP-диапазон) zone = "ru-central1-b" # Зона, где будет размещена подсеть network_id = yandex_vpc_network.pyroscope.id # ID сети, к которой будет привязана подсеть } # Ресурс для создания подсети в зоне "ru-central1-d" resource "yandex_vpc_subnet" "pyroscope-d" { v4_cidr_blocks = ["10.0.3.0/24"] # CIDR блок для подсети (IP-диапазон) zone = "ru-central1-d" # Зона, где будет размещена подсеть network_id = yandex_vpc_network.pyroscope.id # ID сети, к которой будет привязана подсеть }
terraform { required_providers { yandex = { source = "yandex-cloud/yandex" version = ">= 0.72.0" } } required_version = ">= 1.3" }
Получаем доступ к кластеру:
yc managed-kubernetes cluster get-credentials --id xxxx --force
Установка Pyroscope и Grafana через Helm
Добавляем Helm репозиторий Grafana
helm repo add grafana https://grafana.github.io/helm-charts helm repo update
Устанавливаем Pyroscope
helm upgrade -n pyroscope --create-namespace --install pyroscope \ grafana/pyroscope --values values_pyroscope.yaml
values_pyroscope.yaml
# Конфигурация Alloy alloy: enabled: false # Отключаем интеграцию с Alloy # Настройки Ingress для внешнего доступа ingress: enabled: true # Включаем Ingress контроллер className: "nginx" # Указываем использовать nginx ingress controller hosts: - pyroscope.apatsev.org.ru # Доменное имя для доступа к Pyroscope
Устанавливаем Grafana с поддержкой Pyroscope
helm upgrade -n grafana --create-namespace --install grafana \ grafana/grafana -f values_grafana.yaml
values_grafana.yaml
# Настройки окружения Grafana env: # Устанавливаем плагин grafana-pyroscope-app при инициализации Grafana GF_INSTALL_PLUGINS: grafana-pyroscope-app # Включаем анонимный доступ (для демонстрационных/тестовых сред) GF_AUTH_ANONYMOUS_ENABLED: "true" # Устанавливаем роль анонимного пользователя как Admin (полный доступ) GF_AUTH_ANONYMOUS_ORG_ROLE: Admin # Настройки Ingress для доступа к Grafana извне ingress: # Включаем создание Ingress ресурса enabled: true # Указываем, что используем nginx ingress controller ingressClassName: nginx # Доменные имена, по которым будет доступна Grafana hosts: - grafana.apatsev.org.ru # Пути, по которым будет доступен сервис (корень домена) paths: - / # Конфигурация источников данных Grafana datasources: # Файл конфигурации datasources.yaml datasources.yaml: # Версия API конфигурации apiVersion: 1 # Список источников данных datasources: # Конфигурация источника данных Pyroscope - name: Grafana Pyroscope # Отображаемое имя источника type: grafana-pyroscope-datasource # Тип источника данных # URL для подключения к серверу Pyroscope # Формат: http://<service-name>.<namespace>:<port> url: http://pyroscope.pyroscope:4040 # Дополнительные параметры конфигурации jsonData: minStep: '15s' # Минимальный интервал между точками данных (15 секунд)
Развёртывание Node.js приложения
Приложение содержит три эндпоинта:
/fast— быстрый ответ,/slow— медленная функция,/leak— функция с утечкой памяти
NodeJS код
//app.js // Импорт необходимых модулей const Pyroscope = require('@pyroscope/nodejs'); // Модуль для профилирования const http = require('http'); // Нативный HTTP модуль Node.js // Инициализация Pyroscope для сбора метрик производительности Pyroscope.init({ // Адрес сервера Pyroscope (берется из переменных окружения или используется значение по умолчанию) serverAddress: process.env.PYROSCOPE_SERVER_ADDRESS || 'http://pyroscope-server:4040', // Имя приложения для идентификации в Pyroscope appName: 'nodejs-example-app', // Теги для дополнительной классификации данных tags: { environment: 'development', // Среда выполнения (разработка) version: '1.0.0' // Версия приложения }, // Настройки сбора данных о процессорном времени wall: { collectCpuTime: true // Включаем сбор данных о времени CPU } }); // Запуск сбора метрик Pyroscope.start(); // Глобальный массив для демонстрации утечки памяти const memoryLeak = []; // Создаем HTTP сервер с разными эндпоинтами для демонстрации различных сценариев const server = http.createServer((req, res) => { // Маршрутизация запросов if (req.url === '/fast') { fastRoute(req, res); // Быстрый эндпоинт } else if (req.url === '/slow') { slowRoute(req, res); // Медленный эндпоинт (искусственная нагрузка) } else if (req.url === '/leak') { leakMemoryRoute(req, res); // Эндпоинт с утечкой памяти } else { // Дефолтный эндпоинт res.writeHead(200); res.end('Hello from Node.js!\n'); } }); // Обработчик быстрого маршрута function fastRoute(req, res) { res.writeHead(200); res.end('Fast response!\n'); // Простой быстрый ответ } // Обработчик медленного маршрута function slowRoute(req, res) { // Искусственно создаем CPU нагрузку для демонстрации профилирования let sum = 0; for (let i = 0; i < 100000000; i++) { sum += Math.random(); // Тяжелые вычисления } res.writeHead(200); res.end(`Slow response! Sum: ${sum}\n`); } // Обработчик маршрута с утечкой памяти function leakMemoryRoute(req, res) { // Добавляем большие объекты в глобальный массив, который никогда не очищается for (let i = 0; i < 1000; i++) { memoryLeak.push({ id: Date.now(), // Уникальный идентификатор data: new Array(1000).fill('leak-data').join(''), // Большая строка timestamp: new Date().toISOString() // Временная метка }); } res.writeHead(200); res.end(`Added 1000 objects to memory leak. Total: ${memoryLeak.length} objects\n`); } // Запуск сервера на порту 3000 server.listen(3000, () => { console.log('Server running on http://localhost:3000'); });
//package.json { "name": "nodejs-pyroscope-example", "version": "1.0.0", "main": "app.js", "dependencies": { "@pyroscope/nodejs": "0.4.5" } }
Деплой в Kubernetes
kubectl apply -f kubernetes/deployment.yaml kubectl apply -f kubernetes/service.yaml kubectl apply -f kubernetes/ingress.yaml
Скрытый текст
apiVersion: apps/v1 kind: Deployment metadata: name: nodejs-app labels: app: nodejs-app spec: replicas: 2 selector: matchLabels: app: nodejs-app template: metadata: labels: app: nodejs-app spec: containers: - name: nodejs-app image: ghcr.io/patsevanton/pyroscope-nodejs:latest ports: - containerPort: 3000 env: - name: PYROSCOPE_SERVER_ADDRESS value: "http://pyroscope.pyroscope:4040" livenessProbe: httpGet: path: / port: 3000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: / port: 3000 initialDelaySeconds: 5 periodSeconds: 5
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nodejs-app-ingress spec: ingressClassName: nginx rules: - host: nodejs-app.apatsev.org.ru http: paths: - path: / pathType: Prefix backend: service: name: nodejs-app-service port: number: 80
apiVersion: v1 kind: Service metadata: name: nodejs-app-service spec: selector: app: nodejs-app ports: - protocol: TCP port: 80 targetPort: 3000 type: ClusterIP
Генерация нагрузки
После деплоя можно запустить нагрузку через Ingress:
while true; do \ curl nodejs-app.apatsev.org.ru/fast; \ curl nodejs-app.apatsev.org.ru/slow; \ curl nodejs-app.apatsev.org.ru/leak; \ done
Мониторинг использования ресурсов
Проверим, сколько памяти потребляют pod-ы:
kubectl top pod
Пример вывода:
NAME CPU(cores) MEMORY(bytes) nodejs-app-77f7b96899-7cff6 19m 1652Mi nodejs-app-77f7b96899-hvfrl 32m 1676Mi
Анализ профиля
Pyroscope предоставляет как собственный web-интерфейс, так и интеграцию с Grafana. Вот как это выглядит:
Встроенный UI Pyroscope


Pyroscope в Grafana





Что показывает профилирование?
После анализа профиля можно сделать следующие выводы:
Функция
leakMemoryRouteв файле./app.jsна строке 63 потребляет 223 MB — явная утечка памяти.Функция
slowRouteна строке 51 работает почти 7 минут — требует оптимизации.
Заключение
Pyroscope — это отличный инструмент для разработчиков, стремящихся понять и улучшить производительность своих приложений. Благодаря простой интеграции с Node.js и Kubernetes, вы можете быстро выявлять узкие места и принимать обоснованные решения по оптимизации.
