перейти к содержанию

Как отладить бесконечный цикл в рабочем коде Node.js

Бесконечные циклы могут остановить работу любого приложения Node.js. В одну минуту ваша служба работает, а следующая загрузка процессора достигает 100%, и время выполнения запросов истекает. Каждый разработчик боится, что бесконечный цикл приведет к сбою производства.

В этом подробном руководстве вы узнаете, как устранять эти сложные проблемы:

  • Подключение отладчика для печати трассировок стека запущенных процессов Node.
  • Использование тестов активности Kubernetes для автоматического запуска отладки
  • Сохранение трассировок стека путем подключения сетевого хранилища

Следуйте инструкциям, и вы приобретете навыки диагностики и устранения даже самых упорных бесконечных циклов в вашей производственной среде Node.js.

«Кошмар: бесконечный цикл» останавливает производство

Недавно наша команда столкнулась с бесконечным циклом, вызвавшим хаос в нашей производственной среде.

Мы запускали экспресс-приложение Node.js в контейнерах Docker без сохранения состояния, организованных Kubernetes. Трафик проходил через вход NGINX.

     Internet
        |
|------------|
|  NGINX LB  |
|------------|
          | 
       [PODs]
     /      \
   Pod 1   Pod 2 
   App 1    App 2

Наша производственная архитектура до инцидента

Внезапно мы заметили скачок задержки балансировщика нагрузки. Приложение все еще отвечало на проверки работоспособности, но время ожидания запросов потребителей истекло.

При проверке модулей выяснилось, что загрузка ЦП одного контейнера достигла 100%. Бесконечный цикл!

| Looping  |  
 |  Pod 1  | 
| <CPU 100%> |   
|           |

Бесконечный цикл привел к сбою пода 1

Наши обычные инструменты отладки, такие как --prof и --cpu-prof было недостаточно — цикл был вызван недетерминированно, поэтому нам нужно было копать глубже.

Нам нужно было найти способ поставить точку останова в работающем контейнере и распечатать трассировку стека именно в том месте, где происходит цикл.

Отладка с GDB

Решением оказался отладчик GNU (GDB). Присоединив его к нашему процессу Node, мы могли бы:

  • Точка останова в работающем коде JavaScript
  • Распечатайте трассировку стека для диагностики цикла

Вот как можно использовать GDB для отладки Node.js в рабочей среде:

1. Установите ГБД

Сначала установите gdb в вашем приложении Dockerfile:

RUN apt-get update && apt-get install -y gdb

Это связывает его с образом контейнера вашего приложения.

2. Подключите GDB к процессу вашего узла.

Затем выполните команду в работающем контейнере и прикрепите gdb:

docker exec -it <container> bash
gdb -p <PID>

Замените <container> с вашим идентификатором или именем контейнера, и <PID> с вашим идентификатором процесса Node.

Присоединение открывает интерактивный сеанс GDB, подключенный к вашему работающему процессу Node.

3. Установите точку останова при переполнении стека

В GDB установите точку останова, чтобы приостановить выполнение при переполнении стека V8:

break v8::Isolate::HandleStackOverflow
continue

Это останавливает процесс Node сразу в тот момент, когда бесконечная рекурсия вызывает переполнение стека.

4. Распечатайте трассировку стека

При приостановке выполнения распечатайте трассировку стека, чтобы увидеть, где в вашем коде возникает цикл:

bt

Обратная трассировка покажет стек вызовов, ведущий к бесконечному циклу.

Например, если ваш код имеет:

function leakMemory() {
  while(true) {
    // Infinite loop!
  }
}

leakMemory();

Ваш сеанс GDB будет захватывать:

#0 loop (cpu=100%) 
#1 leakMemory()
#2 main() 

Теперь вы точно определили, откуда берется петля!

Быстрый пример GDB

Давайте рассмотрим простой практический пример:

  1. Создайте бесконечный цикл в Node:

     // index.js
    
     const infiniteLoop = () => {
       while(true) {}
     }
    
     infiniteLoop();
  2. Запустите его в Docker:

     docker build -t debug-demo .
     docker run -it debug-demo
  3. Подключите GDB в другом терминале:

     docker ps # Get container ID
     docker exec -it <ID> bash
    
     gdb -p <PID>
     break v8::Isolate::HandleStackOverflow
     continue 
    
     bt # Print stack trace

Как только вы установите точку останова и bt, GDB напечатает что-то вроде:

#0 v8::Isolate::HandleStackOverflow 
#1 infiniteLoop() at index.js:4
#2 main() at index.js:8

GDB приостанавливает выполнение и печатает стек вызовов, показывая infiniteLoop вызывает проблему.

Теперь у вас есть практический опыт отладки процессов Node.js с помощью GDB! Давайте посмотрим, как применить эти методы в масштабе Kubernetes.

Использование зондов жизнеспособности

С помощью GDB мы можем прерывать процессы отдельных узлов. Но как автоматически выполнять отладку в кластере контейнеров?

Ответ кроется в Kubernetes зонды жизнеспособности. Объединив проверки работоспособности с нашим сценарием GDB, мы можем автоматически отлаживать бесконечные циклы между модулями.

Вот как это настроить:

1. Понимание датчиков активности

Kubernetes использует проверки работоспособности для отслеживания работоспособности контейнера. Например:

livenessProbe:
  httpGet:
    path: /healthz
    port: 3000
  periodSeconds: 5 # Check every 5 sec
  failureThreshold: 4 # Allow 3 failures 

Это делает HTTP GET /healthz каждые 5 секунд, допуская 3 сбоя перед перезапуском модуля.

Когда происходит четвертый сбой, Kubernetes считает под «неработоспособным» и убивает его.

2. Использование сбоев зондирования для запуска GDB

Мы можем использовать эти события сбоя для автоматического запуска GDB!

Например, создайте скрипт-оболочку для вашего зонда:

#!/bin/bash

# Do normal health check 
response=$(curl -s http://localhost:3000/healthz)

if [ $? != 0 ]; then
  failures=$((failures+1))

  if [ "$failures" -ge 4 ]; then
     # Attach GDB on 4th failure
     gdb -p <PID> -x /gdbscript.gdb
  fi
fi
  • Пройдите обычную проверку здоровья
  • Если это не удалось, увеличьте счетчик
  • При 4 неудачах запустите сценарий GDB.

Теперь сбои проверки работоспособности заставляют GDB подключать и отлаживать ваше приложение!

3. Обновление определения датчика жизнеспособности

Укажите свой livenessProbe к этому сценарию-оболочке:

livenessProbe:
  exec:
    command:
      - /bin/bash
      - /wrapperscript.sh 
  # Other probe parameters

Теперь Kubernetes запустит ваш скрипт, содержащий логику отладки GDB.

Всякий раз, когда цикл вызывает сбой, GDB запускается автоматически!

Полный пример

Вот полный пример livenessProbe упаковка отладки GDB:

gdbscript.gdb:

break v8::Isolate::HandleStackOverflow
continue
bt

живость.sh:

#!/bin/bash

response=$(curl -s http://localhost:3000/healthz || exit 1)

if [ $? != 0 ]; then
  failures=$((failures+1))

  if [ "$failures" -ge 4 ]; then  
     gdb -p <PID> /gdbscript.gdb
  fi
fi

развертывание.yaml:

livenessProbe:
  exec:
    command:
      - /bin/bash
      - /liveness.sh
  periodSeconds: 5
  failureThreshold: 4 

Теперь ваш зонд живучести автоматически отлаживает любые бесконечные циклы!

Сохраняющиеся следы стека

При описанной выше настройке, если ваше приложение входит в бесконечный цикл, проверка работоспособности запустит GDB и распечатает трассировку стека.

Но поскольку вскоре после этого модуль завершается, выходные данные трассировки стека теряются при перезапуске контейнера.

Чтобы сохранить следы, нам нужно хранить их снаружи, используя сетевое хранилище. Kubernetes предлагает несколько отличных вариантов подключения общих томов для сохранения ваших данных.

Использование постоянных томов

A Постоянный том (PV) предоставляет сетевое хранилище для модулей Kubernetes. Некоторые варианты включают в себя:

  • Тома EBS (хранилище эластичных блоков)
  • EFS (эластичная файловая система)
  • ISCSI
  • NFS (сетевая файловая система)

Например, чтобы смонтировать том EFS:

  1. Создайте файловую систему EFS
  2. Создайте PV и PersistentVolumeClaim (PVC).
  3. Установите ПВХ в капсулу
apiVersion: v1
kind: PersistentVolume
metadata:
  name: gdb-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  storageClassName: efs
  csi:
    driver: efs.csi.aws.com
    volumeHandle: <file-system-id>

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gdb-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: efs 
  resources:
    requests:
      storage: 1Gi

---  

# Pod spec
volumes:
  - name: debugger-storage
    persistentVolumeClaim:
      claimName: gdb-pvc

Теперь ваш модуль может монтировать PV в качестве внешнего хранилища!

Сохранение трассировок стека

Подключив сетевое хранилище, вы можете сохранять следы GDB:

# liveness.sh

gdb -p <PID> /gdbscript.gdb | tee /debugger/stacktrace.txt 

Это перенаправляет вывод GDB на внешний stacktrace.txt.

При следующем сбое ваш датчик работоспособности:

  1. Присоедините GDB и распечатайте стек
  2. Сохраните трассировку на свой постоянный том.

Таким образом, даже когда модуль перезапускается, вы сохраняете данные отладки!

Пример из реального мира

Давайте рассмотрим реальный пример того, как мы использовали эти методы:

Наш кластер Kubernetes размещался на AWS, поэтому в качестве хранилища мы использовали тома EFS.

Dockerfile

RUN apt-get update && apt-get install -y gdb

живость.sh

#!/bin/bash

response=$(curl -s http://localhost:3000/healthz || exit 1)

if [ $? != 0 ]; then

  # Increment failure counter
  ...

  # On 4th failure, attach GDB
  if [ "$failures" -ge 4 ]; then

    gdb -p <PID> /gdbscript.gdb | tee /efs/stacktrace.txt

  fi

fi

развертывание.yaml

// Container spec

volumeMounts:
  - name: debugger-efs 
    mountPath: /efs

// Pod spec 

volumes:
  - name: debugger-efs
    persistentVolumeClaim: 
      claimName: efs-pvc # EFS storage

// Liveness probe  

livenessProbe:
  exec:
    command: 
      - /bin/bash
      - /liveness.sh
  failureThreshold: 4

Это дало нам возможность автоматической отладки на основе проверок работоспособности. Всякий раз, когда цикл вызывал сбой, он:

  1. Запустить GDB с помощью трассировки стека
  2. Направьте вывод в постоянное хранилище EFS.

В конце концов мы обнаружили, что виноват неправильный импорт библиотеки. После исправления зависимости наш кошмар с бесконечным циклом был наконец решен!

Основные выводы

Вот ключевые методы отладки бесконечных циклов в Node.js:

  • Используйте ГБД для точки останова и печати трассировок стека запущенных процессов Node
  • Используйте зонды жизнеспособности для автоматического запуска GDB в случае сбоя
  • Сохранять данные путем передачи трассировок в сетевые тома, такие как EFS

Хотя в примерах используются Docker и Kubernetes, те же принципы применимы к любой производственной среде:

  • Подключите отладчик, например GDB, для приостановки и проверки.
  • Автоматизация отладки с помощью проверок работоспособности
  • Экстернализация данных, чтобы они сохранялись при перезапусках

С помощью этих стратегий вы сможете быстро диагностировать и устранить даже самые болезненные бесконечные циклы. Больше никаких сбоев в производстве!

Я надеюсь, что эти методы помогут вам отладить сложные бесконечные циклы. Дайте мне знать в комментариях, если у вас есть еще какие-либо советы или рекомендации!

Присоединяйтесь к беседе

Ваш электронный адрес не будет опубликован. Обязательные поля помечены * *