Métricas con el Odroid-N2

Ubuntu minimal instalado en un disco de 60Gb solo para testing 😁 no lo recomiendo.

elodroidn2

El Odroid

El odroid n2 es una placa bastante buena, aunque descontinuada ya, por su jefe el odroid n2+ aún asi era una placa que en su momento superaba con creces a la rasberry-py, y claramente por eso la tenemos aquí

odroidn2 odroidn2+

ON/OFF

  • Para apagarlo se puede hacer desde la interface del Sistema.

  • Via infrarojo.

  • Customizarle un botón para ON/OFF.

  • O un esp( pero es muy exagerado 😅)

Cpu benchmark

El Odroid tiene buen rendimiento, Pobre raspi, igualmente esto no es de mi autoria pero no tiene pinta que sea falso o alterado para ofrecer ventaja.

benchmark cpu

Cable para conexión SDD

En un inicio las estabilidad del SO era poca, pero se debía a que el disco ssd sufria algún tipo de desconexión, lentidud etc…​ lo cual producia que la pantalla se quedara congelada.

Este adaptador es muy estable, sin que se frizee el S0 en este caso Ubuntu Mate.

cable estable odroid ssd

odroid@odroid:/$ lsusb (1)
Bus 002 Device 003: ID 174c:1153 ASMedia Technology Inc. ASM1153 SATA 3Gb/s bridge (2)
Bus 002 Device 005: ID 174c:1153 ASMedia Technology Inc. ASM1153 SATA 3Gb/s bridge
Bus 002 Device 002: ID 05e3:0620 Genesys Logic, Inc. USB3.1 Hub
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 002: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
1 Ejecutamos lsusb
2 Tenemos el chipset ASM1153 correcto y estable, permitiendo una velocidad mayor IO de disco.
Info de usuarios que recomienda su uso.

Instalando Ubuntu 22.04 LTS

Instalación de docker y fixeo de arranque

Este script da todo masticado ya, no tenemos que hacer practicamente nada.

Sí, al querer hacer un hello world con docker tenemos este error

Odroid n2 OCI runtime create failed: runc create failed: unable to start container process: error during container init: error setting cgroup config for procHooks process: bpf_prog_query(BPF_CGROUP_DEVICE) failed: invalid argument: unknown

Editaremos el /media/boot/boot.ini

Es porque nos falta añadir la línea siguiente:

# Boot Args
setenv bootargs "root=UUID=e139ce78-9841-40fe-8823-96a304a09859 rootwait rw ${condev} ${amlogic} no_console_suspend fsck.repair=yes net.ifnames=0>
setenv bootargs "${bootargs} systemd.unified_cgroup_hierarchy=0" (1)
1 Justo esta, en el fichero /media/boot/boot.ini, y reiniciamos.

Deberíamos poder ver la línea esa en:

root@odroid:~# cat /proc/cmdline
root=/dev/sda2 rootwait rw console=ttyS0,115200n8  no_console_suspend fsck.repair=yes net.ifnames=0 elevator=noop hdmimode=custombuilt cvbsmode=576cvbs max_freq_a53=1896 max_freq_a73=1800 maxcpus=6 voutmode=hdmi modeline=2560,1080,185580,66659,60,2560,2624,2688,2784,1080,1083,1093,1111,0,0,1 disablehpd=false cvbscable=0 overscan=100  monitor_onoff=false logo=osd0,loaded hdmitx=cec3f sdrmode=auto consoleblank=0 enable_wol=0
(1)
cgroup_hierarchy=1 systemd.unified_cgroup_hierarchy=0
1 Listo, ya podriamos hacer un hello-world con docker.

Docker compose en el Odroid

Necesitamos docker compose, entonces buscamos la última release y es ésta v2.24.0-birthday.10

sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.0-birthday.10/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Grafana

logo horizontal dark

Grafana permite vizualizar métricas en tiempo real, y además es muy customizable, es necesario que use un datasource, en este caso usaremos prometheus que bajo fondo usa PromQL siendo compatible con más de 100 apis.

En el odroid por alguna razón tuve ciertos problemas de permisos simplemente, faltaba el id del usuario correcto con el comando:
id -u

Y el mío era el 1000 🔥 este debemos usarlo en el docker-compose.yml

Lo ejecutamos con:

docker-compose -f docker-compose.yml up -d (1)
1 En el mismo directorio.
networks:
  monitoring:
    driver: bridge
volumes:
  prometheus_data: {}
services:
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    ports:
      - 9100:9100
    networks:
      - monitoring
  prom-pushgateway:
    image: prom/pushgateway
    container_name: prom-pushgateway
    ports:
      - 9091:9091
    networks:
      - monitoring
  prometheus:
    image: prom/prometheus:latest
    user: "1000" (1)
    environment:
      - PUID=1000 (2)
      - PGID=1000 (3)
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - /home/odroid/Documents/metricas/prometheus.yml:/etc/prometheus/prometheus.yml
      - /home/odroid/Documents/metricas:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--web.enable-lifecycle'
    ports:
      - 9090:9090
    networks:
      - monitoring
  grafana:
    image: grafana/grafana:latest
    user: "1000" (4)
    container_name: grafana
    ports:
      - 3000:3000
    restart: unless-stopped
    volumes:
      - /home/odroid/Documents/metricas/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
      - /home/odroid/Documents/metricas/grafana:/var/lib/grafana
    networks:
      - monitoring
1 El user id se debe ajustar
2 Igual en este punto
3 Igual
4 Igual
los volumes deben ajustarse correctamente, pero nada complicado.

Posible error de Iptable

En caso de querer iniciar con docker compose y tengamos est error

Failed to program FILTER chain: iptables failed: iptables --wait -I FORWARD -o br-7657c02c7a4e -j DOCKER: iptables v1.8.7 (nf_tables):  RULE_INSERT failed (Invalid argument): rule in chain FORWARD
 (exit status 4)

Podemos hacer lo siguiente

sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
sudo update-alternatives --set arptables /usr/sbin/arptables-legacy
sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy

Reiniciar docker y network manager

sudo systemctl restart NetworkManager
sudo systemctl restart docker

Y probar nuevamente a ejecutar el fichero .yml con docker compose

Reset de password

Sí, típica historia, pasara que en el algún momento perdamos el acceso al grafana, pero podemos resolverlo con:

docker exec -ti e3e338f0bfae \ (1)
grafana cli admin reset-admin-password \
newPassw0rd (2)
1 Le pasamos el id del container de grafana.
2 Esta será la nueva password
root@odroid:/media/share-nas/dockerfiles++# docker exec -ti e3e338f0bfae grafana cli admin reset-admin-password odroidPassw00rd
INFO [01-05|23:01:35] Starting Grafana                         logger=settings version= commit= branch= compiled=1970-01-01T00:00:00Z
INFO [01-05|23:01:35] Config loaded from                       logger=settings file=/usr/share/grafana/conf/defaults.ini
INFO [01-05|23:01:35] Config overridden from Environment variable logger=settings var="GF_PATHS_DATA=/var/lib/grafana"
INFO [01-05|23:01:35] Config overridden from Environment variable logger=settings var="GF_PATHS_LOGS=/var/log/grafana"
INFO [01-05|23:01:35] Config overridden from Environment variable logger=settings var="GF_PATHS_PLUGINS=/var/lib/grafana/plugins"
INFO [01-05|23:01:35] Config overridden from Environment variable logger=settings var="GF_PATHS_PROVISIONING=/etc/grafana/provisioning"
INFO [01-05|23:01:35] Target                                   logger=settings target=[all]
INFO [01-05|23:01:35] Path Home                                logger=settings path=/usr/share/grafana
INFO [01-05|23:01:35] Path Data                                logger=settings path=/var/lib/grafana
INFO [01-05|23:01:35] Path Logs                                logger=settings path=/var/log/grafana
INFO [01-05|23:01:35] Path Plugins                             logger=settings path=/var/lib/grafana/plugins
INFO [01-05|23:01:35] Path Provisioning                        logger=settings path=/etc/grafana/provisioning
INFO [01-05|23:01:35] App mode production                      logger=settings
INFO [01-05|23:01:35] Connecting to DB                         logger=sqlstore dbtype=sqlite3
INFO [01-05|23:01:35] Starting DB migrations                   logger=migrator
INFO [01-05|23:01:35] migrations completed                     logger=migrator performed=0 skipped=523 duration=1.624073ms
INFO [01-05|23:01:35] Envelope encryption state                logger=secrets enabled=true current provider=secretKey.v1

Admin password changed successfully ✔

El fichero prometheus.yml

Este fichero lo necesita prometheus si o si, para el intervalo de scraping, además las url’s de pushgateway ( que no la usare de momento aquí), pero si la de node-exporter.

En caso de que el arranque del contenedor de prometheus falle, este fichero debemos también editarle.

Además borrar y crear el contenedor de prometheus nuevamente.

docker-compose stop (1)
docker rm id_contenedor_prometheus (2)
docker-compose -f docker-compose.yml up -d (3)
1 Parando contenedores, grafana, prometheus, node, pushgateway, sin recrear ninguno.
2 Removiendo container, a través de su id.
3 Recreando container de prometheus, los demás contenedores se reutilizaran.
global:
  scrape_interval: 1m

scrape_configs:
  - job_name: 'prometheus'
    scrape_interval: 1m
    static_configs:
      - targets: ['prometheus:9090'] (1)

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100'] (2)

  - job_name: dev-push-gateway
    metrics_path: /metrics
    scheme: http
    static_configs:
      - targets: ['prom-pushgateway:9091'] (3)
        labels:
          service: 'prom-pushgateway'
1 container_name de prometheus
2 container_name de node-exporter
3 container_name de push-gateway

Añadiendo el datasource

Como lo indica el input sera la url del servidor de prometheus más su puerto.

grafana datasource
Aquí en wsl sería http://prometheus:9090

Por último confirmamos el datasource de prometheus, esta notificación es la que debemos tener.

Save and test
datasource añadido correctamente

Pushgateway

Util para métricas que se hacen difíciles de scrapear para prometheus, entonces pushgateway suele trabajar en conjunto para ese tipo de métricas.

Un caso de uso será de load-testing por ejemplo con Artillery

Prometheus

prometheus logo
interface targets prometheus

Panel de node-exporter

Node exporter, nos permite exponer métricas del sistema operativo, por lo cual prometheus puede scrapear para que grafana las grafique, lo interesante es que existen ya varios paneles que podemos reutilizar.

Importando panel node-exporter

Añadimos un nuevo dashboard

new dashboard
importando panel node exporter
grafana

Artillery

artillery logo

Pero para instalarlo necesitamos node en este caso estoy usando la última versión, y npm que es como maven pero decentralizado

Instalamos nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

Lo exportamos a nuestro profile o bashrc

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

Actualizamos sin reiniciar

source ~/.bashrc

Instalamos la version latest de Node.js

nvm install node

Usamos la version instalada

nvm use node

La establecemos por defecto

nvm alias default node
npm install -g artillery (1)
1 Instalamos artillery
rubn ⲁƛ ▸ npx artillery dino
(node:2692684) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created) (1)
 ------------
< Artillery! >
 ------------
          \
           \
               __
              / _)
     _/\/\/\_/ /
   _|         /
 _|  (  | (  |
/__.-'|_|--|_|
1 warning de algo obsoleto
rubn ⲁƛ ▸ artillery version
(node:2692881) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)

        ___         __  _ ____
  _____/   |  _____/ /_(_) / /__  _______  __ ___
 /____/ /| | / ___/ __/ / / / _ \/ ___/ / / /____/
/____/ ___ |/ /  / /_/ / / /  __/ /  / /_/ /____/
    /_/  |_/_/   \__/_/_/_/\___/_/   \__  /
                                    /____/


VERSION INFO:

Artillery: 2.0.14
Node.js:   v22.2.0
OS:        linux

Script para Node, Artillery, nvm

#!/bin/bash (1)

# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

# Set up NVM environment variables
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

# Load nvm script to use it in the current shell session
source ~/.bashrc

# Install the latest version of Node.js
nvm install node

# Use the installed Node.js version
nvm use node

# Set the installed Node.js version as the default
nvm alias default node

# Install Artillery globally
npm install -g artillery

# Verify installations
echo "Node.js version: $(node -v)"
echo "npm version: $(npm -v)"
echo "Artillery version: $(artillery -V)"
1 En windows si nos da error al ejecutar -bash: ./node-npm-artillery.sh: /bin/bash^M: bad interpreter: No such file or directory podriamos resolverlo con sed -i -e 's/\r$//' scriptname.sh, cosa que lanza si creamos un .txt y se renombra a .sh

Dashboard para Artillery

Config de Artillery con plugin para conexión con prometheus

config:
  target: "http://localhost:8080"
  phases:
    - duration: 1
      arrivalRate: 100
  http:
    timeout: 20
  plugins:
    publish-metrics:
      - type: prometheus
        pushgateway: 'http://192.168.1.6:9091' (1)
        tags:
          - 'test_id:myTest'
          - 'type:loadtest'

scenarios:
  - name: "Request"
    flow:
      - get:
          url: "/example-url"
1 Esta es la url del servidor donde esta instalado prometheus, en mi caso en mi odroid.

Al ejecutar el artillery run artillery.yml se pushearan a prometheus, el fichero artillery.yml puedo tenerlo donde quiera pero se debe ajustar siempre la url del target y tener instalado artillery.

La gráfica se ve llena porque he generado varios reportes.

artillery http metrics

Es importante que las variables que tenemos en grafana coincidan también con las que tenemos en nuestro .yml
variables grafana

Con eso tendriamos nuestros combos con la data para filtrar mejor.

combo grafana cargados correctamente

Gráfica de latencia

Para la gráfica de latencia es mejor sumar usando los operadores que nos da grafana.

grafana lantencia sumarizacion

Ahora así no se repite la etiqueta de la Legend .

lantency panel fixed

R2DBC vs Synchronous programming

Estoy probando ejemplos de código de r2dbc con mariadb, un ejemplo ya existente de Alejandro Duarte, y me doy cuenta que en mi localhost, de manera reactiva si se procesan el doble de request que de manera síncrona, es decir doble de rendimiento/throughput

Como dicho proyecto usa la jdk 21, use un newVirtualThreadPerTaskExecutor y boundedElastic de project reactor

return wordRepository.findWords(limit)
        .publishOn(Schedulers.boundedElastic()) (1)
        .map(this::fillData);
1 Por lo visto ya en nuevas version de project reactor, el boundedElastic se integra con los virtual threads
return wordRepository.findWords(limit)
        .publishOn(Schedulers.fromExecutorService(Executors.newVirtualThreadPerTaskExecutor()))
        .map(this::fillData);

Y este es el Summary report

Java síncrona

All VUs finished. Total time: 32 seconds

--------------------------------
Summary report @ 01:32:59(+0200)
--------------------------------

http.codes.200: ................................................................ 10 (1)
http.codes.500: ................................................................ 90
http.downloaded_bytes: ......................................................... 1232349
http.request_rate: ............................................................. 23/sec
http.requests: ................................................................. 100
http.response_time:
  min: ......................................................................... 10004
  max: ......................................................................... 30128
  mean: ........................................................................ 11993.3
  median: ...................................................................... 9999.2
  p95: ......................................................................... 30040.3
  p99: ......................................................................... 30040.3
http.responses: ................................................................ 100
vusers.completed: .............................................................. 100
vusers.created: ................................................................ 100
vusers.created_by_name.Request word: ........................................... 100
vusers.failed: ................................................................. 0
vusers.session_length:
  min: ......................................................................... 10006.8
  max: ......................................................................... 30148.6
  mean: ........................................................................ 11999.1
  median: ...................................................................... 9999.2
  p95: ......................................................................... 30040.3
  p99: ......................................................................... 30040.3
1 De locos, a penas procese 10 request, un poco extraño, quizas sea configuración de mi local ?

R2dbc

Summary report @ 09:00:08(+0200)
--------------------------------

errors.ETIMEDOUT: .............................................................. 100
http.codes.200: ................................................................ 20 (1)
http.request_rate: ............................................................. 45/sec
http.requests: ................................................................. 100
http.response_time:
  min: ......................................................................... 9208
  max: ......................................................................... 18431
  mean: ........................................................................ 13866.8
  median: ...................................................................... 9607.1
  p95: ......................................................................... 18588.1
  p99: ......................................................................... 18588.1
http.responses: ................................................................ 20
vusers.created: ................................................................ 100
vusers.created_by_name.Request word: ........................................... 100
vusers.failed: ................................................................. 100
⠸
1 20 request con http 200, nada mal.

Artillery cloud

Aquí una metrica compartida bastante fácil de hacer, tan simple como ejecutar

 artillery run artillery.yml --record --key your_api_key

artillery cloud

Poco espacio en disco

Mirando métricas

Llega un momento que el disco del Odroid se quedo corto, pero con un USB adicional podemos añadir otro disco más grande

En el panel de node exporter se mostrará el disco nuevo.

newdisk2TB

Usando NFS del NAS

Otra opción interesate es usar el NFS que hemos activado en el NAS, este protocolo tiene un buen rendimiento y no es muy difícil de usar, nos permitirá tener un directorio en nuestro host que apunta a otra dirección en la red.

Lo presentado aquí, en local es inseguro, dado que no tengo seteada la seguridad como debería, pero para un testeo rápido viene bien.

Entonces instalamos:

sudo apt update && sudo apt install nfs-common (1)
sudo mount -t nfs 192.168.1.250:/mnt/pool/ /media/share-nas (2)
Created symlink /run/systemd/system/remote-fs.target.wants/rpc-statd.service → /lib/systemd/system/rpc-statd.service.
1 Actualizamos e instalamos el nfs-common
2 Montamos la unidad externa en nuestro host
nfs disco unidad mas espacio
Las rutas anteriores son de esto:

/media/usb0 ruta del Odroid con menos de 50Gb.
/media/share-nas ruta del NFS del NAS con mucho mas espacio para simple testeo.

Montado automático

sudo nano /etc/fstab
# UNCONFIGURED FSTAB FOR BASE SYSTEM
LABEL=BOOT /media/boot vfat umask=0077 0 1
UUID=e139ce78-9841-40fe-8823-96a304a09859 / ext4 errors=remount-ro 0 1
192.168.1.250:/mnt/pool/mariadb /media/share-nas nfs defaults 0 0 (1)
1 La ruta NFS nueva.
Si el fichero fstab es editado mal, no podremos arrancar el Ubuntu del Odroid.

En caso de que no tengamos acceso a la ruta nfs y tengamos este tipo de error

Error con ruta nfs

d????????    ??   ??   /media/share-nas (1)
1 Este directorio estaba montado, pero con alguna update del servidor externo, ya no tenemos acceso.

Entonces la solución de momento fue editar el fstab con una nueva ruta y vulgarmente reiniciar el odroid, pero esta solución parece mejor

mount -o remount /share/