Ubuntu minimal instalado en un disco de 60Gb solo para testing 😁 no lo recomiendo.
|
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í
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.
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.
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
Credenciales
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
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.
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
|
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
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.
-
ID 19230
Importando panel node-exporter
Añadimos un nuevo dashboard
Artillery
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
-
prometheus-grafana-dashboards respositorio ya archivado con el json para el panel de prometheus usando 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. |
Es importante que las variables que tenemos en grafana coincidan también con las que tenemos en nuestro .yml |
Con eso tendriamos nuestros combos con la data para filtrar mejor.
Gráfica de latencia
Para la gráfica de latencia es mejor sumar usando los operadores que nos da grafana.
Ahora así no se repite la etiqueta de la Legend .
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
Enlace al reporte de 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.
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 |
Las rutas anteriores son de esto:
|
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/