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, вы можете быстро выявлять узкие места и принимать обоснованные решения по оптимизации.