LXC: primeros pasos

Hola a todos. Llevo un tiempo dándole vueltas a dar un salto más en la virtualización. Teóricamente con KVM conseguimos muy buen rendimiento, pero con contenedores reduciríamos al máximo la diferencia entre la máquina anfitriona y sus invitados.

Ahora todos los hispters gritarán de alegría coreando al unísono Docker! Docker! Docker!, pues guardad los fulares porque por ahora simplemente vamos a realizar una pequeña prueba y lo más natural es probar la tecnología que es más parecida a mi forma de administrar los sistemas, LXC. Para los más viejos del lugar, LXC vendría a ser como un chroot hipervitaminado. También hay que decir que Docker se basó inicialmente en LXC para su desarrollo y que su concepto de instancia es un poco diferente.

En un horizonte más lejano tenemos OpenStack, pero cualquier tecnología de virtualización que aprendamos nos servirá en ese ecosistema por lo que es tiempo bien aprovechado.

Para esta pequeña prueba vamos a crearnos un par de máquinas, una con Debian (Stretch, que es la testing actualmente) y otra con Ubuntu (Xenial, es decir la reciente 16.04), evidentemente no utilizaremos dos máquinas físicas, bastará con que sean dos máquinas virtuales con *KVM, sí, habéis oído bien, contenedores dentro de máquinas virtuales, una locura.

Crearé estas dos instancias porque quiero comparar la configuración de ambas aproximaciones para ver cuál es más cómoda de administrar. Ambas distribuciones tienen el paquete LXC en la versión 2.0 por lo que la comparación será justa. Una vez que tengamos listos estos servidores crearemos un contenedor en cada una de ellas, con la misma configuración y dentro de ellos haremos una instalación rápida de PostgreSQL para comparar su rendimiento.

En este punto supondremos que tenemos ambas distribuciones limpias y funcionando, ya que la configuración de KVM la tenemos más que superada por posts anteriores. En orden de antigüedad: 1, 2, 3, 4, 5 y 6.

La configuración de la base de LXC es similar tanto en Debian como en Ubuntu, así que estos comandos podemos ejecutarlos en ambos sistemas. Primero instalamos el paquete principal:

# apt-get install lxc

Ahora, para asegurarnos que todo funciona correctamente disponemos de una orden que se instala junto con la base:

# lxc-checkconfig
Kernel configuration not found at /proc/config.gz; searching...  
Kernel configuration found at /boot/config-4.4.0-31-generic  
--- Namespaces ---
Namespaces: enabled  
Utsname namespace: enabled  
Ipc namespace: enabled  
[...]

Si la instalación ha sido exitosa todos los parámetros nos deben aparecer con un tranquilizador enabled y en un agradable verde lima. Listo, ya podemos crear nuestro primer contenedor:

# lxc-create -t download -n ubuntu-test
Setting up the GPG keyring  
Downloading the image index

---
DIST    RELEASE ARCH    VARIANT BUILD  
---
alpine    3.0 amd64   default 20160630_17:50  
alpine    3.0 i386    default 20160630_17:50  
[...]
ubuntu    yakkety s390x   default 20160717_03:49  
---

Distribution: ubuntu  
Release: xenial  
Architecture: amd64

Downloading the image index  
Downloading the rootfs  
Downloading the metadata  
The image cache is now ready  
Unpacking the rootfs

---
You just created an Ubuntu container (release=xenial, arch=amd64, variant=default)

To enable sshd, run: apt-get install openssh-server

For security reason, container images ship without user accounts  
and without a root password.

Use lxc-attach or chroot directly into the rootfs to set a root password  
or create user accounts.  

Como véis hemos elegido Ubuntu para nuestro contenedor y en la versión estable, la 16.04, Xenial, por suspuesto la de 64 bits. Cuando lancemos el contenedor veremos una pequeña diferencia entre los dos anfitriones, primero Debian:

# lxc-start -n ubuntu-test
# lxc-info -n ubuntu-test
Name:           ubuntu-test  
State:          RUNNING  
PID:            2878  
CPU use:        0.51 seconds  
BlkIO use:      8.00 KiB  
# lxc-ls -f
NAME        STATE   AUTOSTART GROUPS IPV4 IPV6  
ubuntu-test RUNNING 0         -      -    -  

Y ahora Ubuntu:

# lxc-start -n ubuntu-test
# lxc-info -n ubuntu-test
Name:           ubuntu-test  
State:          RUNNING  
PID:            4459  
IP:             10.0.3.162  
CPU use:        0.71 seconds  
BlkIO use:      8.00 KiB  
Memory use:     12.52 MiB  
KMem use:       0 bytes  
Link:           vethYR5TMW  
 TX bytes:      1.27 KiB
 RX bytes:      1.40 KiB
 Total bytes:   2.67 KiB
# lxc-ls -f
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6  
ubuntu-test RUNNING 0         -      10.0.3.162 -  

La diferencia es que la red no se ha levantado automáticamente en el primer caso. Vamos a arreglar esto. Creamos el archivo /etc/default/lxc-net en nuestro anfitrión Debian con el siguiente contenido:

USE_LXC_BRIDGE="true"  
LXC_BRIDGE="lxcbr0"  
LXC_ADDR="10.0.3.1"  
LXC_NETMASK="255.255.255.0"  
LXC_NETWORK="10.0.3.0/24"  
LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"  
LXC_DHCP_MAX="253"  
LXC_DHCP_CONFILE=""  
LXC_DOMAIN=""  

Y activamos el servicio:

# systemctl enable lxc-net
# systemctl start lxc-net
# reboot

Si ejecutamos un ifconfig vemos que ha aparecido la interfaz de red lxcbr0. Ahora debemos asignarle esta interfaz a nuestro contenedor editando el fichero /var/lib/lxc/ubuntu-test/config en el anfitrión Debian y cambiando las últimas líneas por éstas:

[...]
# Network configuration
lxc.network.type = veth  
lxc.network.link = lxcbr0  
lxc.network.flags = up  
lxc.network.hwaddr = 00:16:3e:43:1a:7e  

Tened en cuenta que la MAC de la tarjeta de red sería recomendable cambiarla si váis a crear varios contenedores. La primera vez que probé a reiniciar el contenedor con la nueva configuración no cogió una ip por lo que opté por la decisión más rápida, reiniciar por completo el anfitrión y una vez arrancado lancé la instancia:

# lxc-stop -n ubuntu-test
# lxc-start -n ubuntu-test
# lxc-info -n ubuntu-test
Name:           ubuntu-test  
State:          RUNNING  
PID:            2813  
CPU use:        0.29 seconds  
BlkIO use:      0 bytes  
Link:           vethP7PUAB  
 TX bytes:      850 bytes
 RX bytes:      634 bytes
 Total bytes:   1.45 KiB

Si alguien sabe porqué LXC no reparte direcciones ip en Debian cambiando el servicio lxc-net, activándolo y reiniciándolo todos le agradeceremos el comentario, de momento seguiremos con nuestra solución salomónica y reiniciaremos todo el anfitrión.

Ahora ya volvemos a tener una parte que podemos aplicar a ambos anfitriones. Lo primero que vamos a hacer es conectarnos a ellos y comprobar que tenemos acceso a internet:

# lxc-attach -n ubuntu-test
root@ubuntu-test:/# apt update  
Hit:1 http://archive.ubuntu.com/ubuntu xenial InRelease  
Get:2 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [95.7 kB]  
[...]

Bien, todo esto está muy bien, pero necesitamos un poco de acción, PostgreSQL estaría bien. Vamos a instalar el servidor en ambos huéspedes y hacer un pequeño test. Los pasos son iguales en ambos contenedores, añadiremos las fuentes oficiales del proyecto e instalaremos el paquete:

root@ubuntu-test:/# echo "deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main" > /etc/apt/sources.list.d/pgdg.list  
root@ubuntu-test:/# apt install wget  
root@ubuntu-test:/# wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -  
root@ubuntu-test:/# apt update  
root@ubuntu-test:/# apt install postgresql-9.5  

Una vez instalado el servidor ya podemos realizar un test de rendimiento pero primero, para facilitar las cosas. vamos a cambiar el fichero /etc/postgresql/9.5/main/pg_hba.conf y ponemos todos los métodos de autenticación a trust:

[...]
# Database administrative login by Unix domain socket
local   all             postgres                                trust

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     trust  
# IPv4 local connections:
host    all             all             127.0.0.1/32            trust  
# IPv6 local connections:
host    all             all             ::1/128                 trust  
# Allow replication connections from localhost, by a user with the
[...]

Ya podemos seguir con el test:

root@ubuntu-test:/# systemctl restart postgresql  
root@ubuntu-test:/# su - postgres  
postgres@ubuntu-test:~$ createdb performance_tests  
postgres@ubuntu-test:~$ exit  
logout  
root@ubuntu-test:/# pgbench -i -s 10 -h localhost -U postgres performance_tests  
NOTICE:  table "pgbench_history" does not exist, skipping  
NOTICE:  table "pgbench_tellers" does not exist, skipping  
NOTICE:  table "pgbench_accounts" does not exist, skipping  
NOTICE:  table "pgbench_branches" does not exist, skipping  
creating tables...  
100000 of 1000000 tuples (10%) done (elapsed 0.08 s, remaining 0.70 s)  
200000 of 1000000 tuples (20%) done (elapsed 0.16 s, remaining 0.63 s)  
300000 of 1000000 tuples (30%) done (elapsed 0.37 s, remaining 0.86 s)  
400000 of 1000000 tuples (40%) done (elapsed 0.59 s, remaining 0.88 s)  
500000 of 1000000 tuples (50%) done (elapsed 0.81 s, remaining 0.81 s)  
600000 of 1000000 tuples (60%) done (elapsed 1.02 s, remaining 0.68 s)  
700000 of 1000000 tuples (70%) done (elapsed 1.10 s, remaining 0.47 s)  
800000 of 1000000 tuples (80%) done (elapsed 1.18 s, remaining 0.29 s)  
900000 of 1000000 tuples (90%) done (elapsed 1.26 s, remaining 0.14 s)  
1000000 of 1000000 tuples (100%) done (elapsed 1.34 s, remaining 0.00 s)  
vacuum...  
set primary keys...  
done.  
root@ubuntu-test:/# pgbench -c 16 -j 16 -T 60 -h localhost -U postgres performance_tests  
starting vacuum...end.  
transaction type: TPC-B (sort of)  
scaling factor: 10  
query mode: simple  
number of clients: 16  
number of threads: 16  
duration: 60 s  
number of transactions actually processed: 15284  
latency average: 62.811 ms  
tps = 254.090037 (including connections establishing)  
tps = 254.363806 (excluding connections establishing)  

Podemos tunear un poco la configuración de PostgreSQL pero el tema del post no es ése, vamos a comparar el comportamiento de la base de datos en los anfitriones frente a sus contenedores para comprobar hasta qué punto se pierde rendimiento.

Para instalarlo en los anfitriones el proceso es el mismo que hemos explicado antes. Según mis pruebas, realizadas varias veces, la media de transacciones procesadas estaban en torno a las quince mil, con lo que sí, parece que realmente la penalización en los contenedores es prácticamente nula. Lo ideal sería realizar una batería de pruebas cortesía de los chicos de phoronix, pero este pequeño experimento nos ha servido para confirmar lo que se viene comentando de esta tecnología y para animarnos a seguir investigando por esta vía.

Y a partir de aquí qué?, os preguntaréis, pues yo también. De momento se me ocurre montar una prueba con Docker, sobre este mismo laboratorio de máquinas, ver si es igual de simple de configurar y realizar los tests. Luego tenemos la opción de desarrollar habilidades con alguna herramienta de orquestación de máquinas virtuales/contenedores como puede ser Ansible, Puppet o similares. A partir de ahí el cielo es el límite y una cosa que realmente quiero investigar es el sistema OverlayFS que se utiliza con LXC para permitir que varios contenedores compartan una parte del sistema de ficheros común y así reducir el mantenimiento de este tipo de infraestructuras.

También podríamos hablar de cómo tunear todos estos recursos una vez que ya tenemos los conceptos básicos claros, pero esto ya será en próximos capítulos.

Vamos a despedirnos yéndonos arriba y bailando por la convulsa inglaterra de 1984 junto con el pequeño Billy Elliot al son de The Clash y su perfecta London Calling de su disco homónimo, aunque hoy prefiero aconsejaros la impresionante recopilación hecha para la banda sonora de la película.

Dance!