Conhecendo o kernel Linux pelo /proc (parte 4) – comportamentos da memória virtual

Conhecendo o kernel Linux pelo /proc (parte 4) – comportamentos da memória virtual

No post anterior vimos como o kernel mantem os mapeamentos de memória virtual para memória física e como essa tradução é realizada em tempo de execução pela MMU. Neste post iremos abordar alguns comportamentos relacionados ao uso de memória de virtual, como a “sobre-alocação” de memória, uso de SWAP, estouro de memória RAM e o que pode ocorrer em máquinas com uma grande quantidade de memória RAM.

Posso alocar mais memória do que o total disponível?

Como vimos anteriormente, os processos possuem um espaço de endereçamento virtual que é muito maior e independente do total de memória RAM disponível, isso, em teoria, permite que um processo realize alocação de um espaço muito maior do que o disponível em memória RAM. No entanto, esse comportamento, conhecido como “overcommit memory”, é controlado pelo kernel através do valor definido no arquivo /proc/sys/vm/overcommit_memory onde existem 3 opções:

  • 0 – Permite o “overcommit” com uma heurística que evita alocações “massivas” e possui tratamento diferenciado para o usuário root. É a opção padrão da maioria das distribuições.
  • 1 – Permite o “overcommit” de maneira irrestrita.
  • 2 – Não permite o “overcommit”, e o tamanho máximo de memória alocada não pode ultrapassar o total de SWAP + uma porcentagem da memória RAM (definida em no arquivo /proc/sys/vm/overcommit_ratio)

Nosso primeiro programa de teste, o overcommit-alloc.c, realiza a alocação de uma área única de memória com tamanho definido através do primeiro parâmetro passado no momento da execução:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
        char  *ptr;
        if(argc < 2){
                printf("Usage: <Size allocation (megabytes)>");
        }
        int megabytes = atoi(argv[1]);
        ptr=malloc(1024*1024* (unsigned long) megabytes);

        if(ptr==NULL){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }

        printf("Memoria alocada, pressiona qualquer tecla para desalocar\n");
        getchar();
        free(ptr);
        printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n");
        getchar();
        return 0;
}

Para ver na prática como isso funciona vamos executar nossos exemplos em uma VM com 512MB de RAM utilizando Vagrant (o Vagrantfile encontra-se disponível no repositório do Github) conforme as instruções abaixo:

$ gcc overcommit-alloc.c -o overcommit-alloc.o
$ vagrant up
$ vagrant ssh
vagrant@contrib-stretch:~$ free -m
              total        used        free      shared  buff/cache   available
Mem:            492          33         299           2         159         443
Swap:          1020           0        1020
vagrant@contrib-stretch:~$ cat /proc/sys/vm/overcommit_memory 
0

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 512
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 1024
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 1500
Falha ao alocar esta quantidade de memoria

Em um segundo terminal vamos acompanhar o campo “Committed_AS” do arquivo /proc/meminfo, que exibe o total de memória alocado em todo sistema:

root@contrib-stretch:/home/vagrant# cat /proc/meminfo | grep 'Committed_AS' ; # Antes da primeira execucao
Committed_AS:      78808 kB
root@contrib-stretch:/home/vagrant# cat /proc/meminfo | grep 'Committed_AS' ; # Durante a primeira execucao
Committed_AS:     604036 kB
root@contrib-stretch:/home/vagrant# cat /proc/meminfo | grep 'Committed_AS' ; # Durante a segunda execucao
Committed_AS:    1128168 kB

Neste exemplo estamos com o comportamento de overcommit padrão (0), temos apenas 443MB de memória disponível para uso e podemos ver que conseguimos alocar mais memória do que o disponível quando realizamos a primeira execução com 512MB e quando realizamos a segunda execução com o dobro do total de memória RAM (1024MB). Somente quando tentamos realizar uma alocação mais abusiva (1500MB) a heurística do kernel negou a alocação.

Agora vamos repetir este mesmo exemplo com o arquivo overcommit-multiple-alloc.o, que realiza duas alocações de memória com o tamanho informado na execução:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
	char  *ptr1,*ptr2;
	if(argc < 2){
		printf("Usage: <Size allocation (megabytes)>");
	}
	int megabytes = atoi(argv[1]);
	ptr1=malloc(1024*1024* (unsigned long) megabytes);

	if(ptr1==NULL){
                printf("Falha ao realizar a primeira alocação\n");
                return 1;
        }

	ptr2=malloc(1024*1024* megabytes);
	if(ptr2==NULL){
                printf("Falha ao realizar a segunda alocação\n");
                return 1;
        }


	printf("Memoria alocada, pressiona qualquer tecla para desalocar\n");
	getchar();
	free(ptr1);
	free(ptr2);
	printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n");
	getchar();
	return 0;
}
$ gcc overcommit-multiple-alloc.c -o overcommit-multiple-alloc.o
$ vagrant ssh
vagrant@contrib-stretch:~$ /vagrant/overcommit-multiple-alloc.o 512
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-multiple-alloc.o 1024
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-multiple-alloc.o 1500
Falha ao realizar a primeira alocação

Em um segundo terminal vamos acompanhar a alocação total do sistema:

# cat /proc/meminfo | grep 'Committed_AS' ; # Antes da primeira execucao
Committed_AS:      79916 kB
root@contrib-stretch:/home/vagrant# cat /proc/meminfo | grep 'Committed_AS' ; # Durante a primeira execucao
Committed_AS:    1129068 kB
root@contrib-stretch:/home/vagrant# cat /proc/meminfo | grep 'Committed_AS' ; # Durante a segunda execucao
Committed_AS:    2176748 kB

Através deste segundo exemplo podemos ver que a heurística de controle de overcommit do kernel atua sobre cada alocação de forma isolada, pois nesta execução nosso programa conseguiu alocar dois blocos de 1024MB que somados atingem 2GB, ultrapassando os 1500MB que foram negados no nosso programa anterior, no entanto, também não conseguiu realizar a primeira alocação de 1500MB.

Agora vamos refazer nossos testes alterando o overcommit_memory para 1, ou seja, a alocação deve ser irrestrita:

$ vagrant ssh
vagrant@contrib-stretch:~$ sudo su
root@contrib-stretch:/home/vagrant# echo 1 > /proc/sys/vm/overcommit_memory 
root@contrib-stretch:/home/vagrant# exit
vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 1500
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 4096
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 16032
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 32096
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

Neste exemplo iniciamos com a alocação de 1500MB, que já ultrapassava o limite de alocação única no modo heurístico e seguimos alocando 4GB, 16GB, 32GB, e poderíamos continuar alocando memória sem maiores problemas.

Apesar deste modo parecer desnecessário e perigoso para a maioria dos casos, existem cenários onde ele é útil. A própria documentação do kernel cita seu uso com aplicações científicas que precisam realizar alocação de longos arrays esparsos, na qual a utilização de memória efetiva é muito pequena.

Agora vamos realizar novamente nossos testes, porém no modo 2, ou seja, não permitindo a realização do overcommit.

$ vagrant ssh
$ vagrant@contrib-stretch:~$ sudo su
root@contrib-stretch:/home/vagrant# echo 2 > /proc/sys/vm/overcommit_memory 
root@contrib-stretch:/home/vagrant# cat /proc/sys/vm/overcommit_ratio 
50
root@contrib-stretch:/home/vagrant# exit
vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 512
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 1024
Memoria alocada, pressiona qualquer tecla para desalocar
Memoria desalocada, pressiona qualquer tecla para encerrar

vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc.o 1500
Falha ao alocar esta quantidade de memoria

Na execução acima, mesmo no modo 2, ainda conseguimos alocar mais memória (1024MB) do que o total de memória RAM (512MB). Parece um comportamento inesperado, mas não é. Isso acontece devido ao kernel levar em consideração também o tamanho de swap disponível no sistema. A conta realizada pelo kernel para definir o limite de alocação é: TOTAL_SWAP + ( overcommit_ratio/100 * RAM), que neste caso é 1024MB + (50/100 * 512MB) = 1280MB, por isso conseguimos alocar os 1024MB.

Outro ponto importante é que neste modo, o kernel impõe este limite de forma global, ou seja, se executarmos o programa presente em overcommit-multiple-alloc.o passando o valor de 1024MB como parâmetro, ele deve falhar na segunda alocação:

$ vagrant ssh
vagrant@contrib-stretch:~$ /vagrant/overcommit-multiple-alloc.o 1024
Falha ao realizar a segunda alocação

Quando estamos trabalhando com o kernel neste modo, podemos acompanhar através do /proc/meminfo no campo “CommitLimit” e “Committed_AS” o limite de alocação e o total alocado atualmente:

vagrant@contrib-stretch:~$ cat /proc/meminfo | grep 'CommitLimit'
CommitLimit:     1297556 kB

A saída acima mostra o limite total de alocação 1297556kB (~1267MB), que é muito próximo do valor que calculamos anteriormente (1280MB).

E se a memória acabar?

Neste caso o kernel irá utilizar o SWAP, que é uma área em disco (podendo ser uma partição ou um arquivo do sistema de arquivos) destinada a armazenar páginas de memória não utilizadas no momento.

O SWAP pode parecer uma necessidade somente de ambientes com pouca memória RAM e muitas pessoas acreditam que quando temos algum uso de SWAP estamos com falta de memória RAM. No entanto, isso nem sempre é verdade, pois o uso de SWAP pode ocorrer em cenários onde existem áreas de memória não utilizadas por um longo período, onde o kernel acaba priorizando a memória RAM para outros fins e acaba movendo essas áreas para SWAP. Esse comportamento acaba sendo benéfico para aplicações que utilizam grandes quantidades de memória em sua inicialização e dificilmente voltam a utilizar a mesma área novamente.

O que realmente indica a falta de memória e representa um problema de desempenho é um fluxo de páginas entrando e saindo do SWAP, pois isso indica que temos processos requisitando páginas em SWAP, o que leva a leitura de disco e o que por sua vez é muito mais lento que o acesso a memória (mesmo com discos de alto desempenho) e com certeza irá degradar a desempenho do sistema.

Outro indício de falta de memória é ver a thread do kernel kswapd constantemente ativa e consumindo CPU, já que ela só deve estar ativa em situações de esgotamento de memória, pois em condições normais o Kernel realiza a liberação de memória através dos seu algoritmos baseados em LRU.

Agora vamos ver como o /proc pode nos mostrar como está o estado do SWAP do nosso sistema, utilizando o overcommit-alloc-initialize.c para estressar o sistema alocando e utilizando uma grande quantidade de memória. O overcommit-alloc-initialize.c é uma variação do overcommit-alloc.c que utiliza a memória alocada preenchendo com o caracter “a”.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]){
	char  *ptr, *tmp_ptr;
	unsigned long i;
	if(argc < 2){
		printf("Usage: <Size allocation (megabytes)>");
	}
	int megabytes = atoi(argv[1]);
	ptr=malloc(1024*1024* (unsigned long) megabytes);

	if(ptr==NULL){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }
	tmp_ptr=ptr;
	for(i=0;i<1024*1024* (unsigned long) megabytes; i++){
                strcpy(tmp_ptr,"a");
		tmp_ptr++;
	}

	printf("Memoria alocada, pressiona qualquer tecla para desalocar\n");
	getchar();
	free(ptr);
	printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n");
	getchar();
	return 0;
}
$ vagrant ssh
vagrant@contrib-stretch:~$ sudo su
root@contrib-stretch:/home/vagrant# echo 0 > /proc/sys/vm/overcommit_memory
root@contrib-stretch:/home/vagrant# exit
vagrant@contrib-stretch:~$ /vagrant/overcommit-alloc-initialize.o 1024
Memoria alocada, pressiona qualquer tecla para desalocar

Em outro terminal vamos consultar o estado da máquina:

vagrant@contrib-stretch:~$ cat /proc/meminfo | egrep 'SwapTotal|SwapFree|MemTotal|MemFree|MemAvailable|Cached'
SwapTotal:       1045500 kB
SwapFree:         120524 kB
MemTotal:         504112 kB
MemFree:          305312 kB
MemAvailable:     319176 kB
Buffers:            3380 kB
Cached:            15520 kB
vagrant@contrib-stretch:~$ free -m
              total        used        free      shared  buff/cache   available
Mem:            492         167         298           0          26         311
Swap:          1020         903         117

Na execução acima, inicialmente garantimos que o kernel volte para o modo padrão de “overcommit” (0), e em seguida iniciamos o nosso o
overcommit-alloc-initialize.o solicitando a alocação e utilização de 1GB.

Como nossa máquina tem apenas 512MB de RAM, é certo que essa execução irá utilizar SWAP, o que podemos confirmar através do /proc/meminfo através da diferença entre os campos SwapTotal e SwapFree, o que resulta em 903MB utilizados de SWAP e que ainda existe por volta de 311MB disponíveis para uso em RAM (MemAvailable), o que nos leva a crer que quase todo o conteúdo gerado pelo nosso programa encontra-se em SWAP.

Para checar se é realmente o nosso processo que está consumindo o SWAP, podemos consultar o arquivo /proc/PID/status nos campos VmSwap e VmRSS:

vagrant@contrib-stretch:~$ cat /proc/$(pgrep -f overcommit-alloc-initialize.o)/status | egrep 'VmSwap|VmRSS'
VmRSS:	  141872 kB
VmSwap:	  906776 kB

De acordo com a saída acima podemos concluir que quase 100% do uso de SWAP está sendo utilizado pelo nosso processo (885MB) e que ele apenas está consumindo 138MB de memória RAM, o que para esta aplicação é um comportamento extremamente benéfico, já que esta área de memória nunca mais será utilizada.

Agora vamos utilizar uma variação desse programa que realiza a leitura dessa área de memória posteriormente à inicialização, o overcommit-alloc-read.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]){
	char  *ptr, *tmp_ptr;
	unsigned long i;
	char str[50];
	if(argc < 2){
		printf("Usage: <Size allocation (megabytes)>");
	}
	int megabytes = atoi(argv[1]);
	ptr=malloc(1024*1024* (unsigned long) megabytes);

	if(ptr==NULL){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }
	tmp_ptr=ptr;
	for(i=0;i<1024*1024* (unsigned long) megabytes; i++){
                strcpy(tmp_ptr,"a");
		tmp_ptr++;
	}

	printf("Memoria alocada, pressiona qualquer tecla para iniciar a leitura\n");
	getchar();


	tmp_ptr=ptr;
        for(i=0;i<1024*1024* (unsigned long) megabytes; i++){
                sprintf(str,"Aqui: %c",*tmp_ptr);
                tmp_ptr++;
        }


	free(ptr);
	printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n");
	getchar();
	return 0;
}

Neste exemplo vamos utilizar três terminais, uma para execução do overcommit-alloc-read.o, outro para visualizar o resultado do comando top e um último para monitorar o arquivo /proc/vmstat:

Terminal 1:

vagrant@contrib-stretch:~$ time /vagrant/overcommit-alloc-read.o 1024
Memoria alocada, pressiona qualquer tecla para iniciar a leitura

Memoria desalocada, pressiona qualquer tecla para encerrar


real	1m40.074s
user	0m56.712s
sys	0m3.660s

Terminal 2:
top-kswapd
Terminal 3:

vagrant@contrib-stretch:~$ cat /proc/vmstat | egrep 'pswpin|pswpout' #antes da execução
pswpin 796519
pswpout 2249486
vagrant@contrib-stretch:~$ cat /proc/vmstat | egrep 'pswpin|pswpout' #com a memoria alocada
pswpin 797068
pswpout 2399606
vagrant@contrib-stretch:~$ cat /proc/vmstat | egrep 'pswpin|pswpout' #apos a execucao 
pswpin 1059810
pswpout 2664169

Na execução acima realmente sentimos o impacto do uso de SWAP, ou seja, nossa aplicação precisou utilizar uma área de memória virtual que estava armazenada em SWAP, o que elevou nosso tempo de execução por volta de 1min e 40 segundos. Além disso podemos ver no segundo terminal, através do comando top, que a thread do kernel kswapd precisou entrar em ação e que tivemos 4% de uso de CPU por sistema e 4% de espera por IO.

No terceiro terminal, podemos observar através do arquivo /proc/vmstat, nos campos pswpin e pswpout, a quantidade de páginas recuperadas do swap e a quantidade de páginas enviadas para swap respectivamente. Neste caso vemos que logo após a alocação e utilização da área de memória temos 150120 (2399606−2249486) páginas sendo enviadas para SWAP e quando terminamos de ler esta área de memória novamente o sistema precisou carregar 262742 páginas de disco para memória, que é o que realmente impacta na execução do nosso programa.

E se o swap acabar?

Se a memória RAM acabar, a área SWAP acabar e o “overcommit memory” estiver permitido é muito provável que ocorra o famoso OOM (Out Of Memory) Killer, que é um recurso do kernel ativado em situações críticas, para matar processos e liberar memória evitando um possível crash completo do sistema operacional como um kernel panic.

Ao contrário do que possa parecer, o OOM Killer possui um heurística interna para definir qual o processo deve ser morto baseado em um score que leva em conta a memória consumida, o tempo de execução, privilégios (capabilities) e prioridade do processo. De maneira geral, podemos dizer que o objetivo do OOM Killer é tentar obter a maior quantidade de memória matando o menor número de processos possível.

Para entender como isso funciona vamos utilizar o oom-killer.c,  que é bem similar ao código anterior, apenas acrescentando algumas confirmações para que possamos monitorá-lo antes, durante e depois da memória ser alocada e utilizada:

oom-killer.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]){
	char  *ptr, *tmp_ptr;
	unsigned long i;
	if(argc < 2){
		printf("Usage: <Size allocation (megabytes)>");
	}
	int megabytes = atoi(argv[1]);

        printf("Pressione qualquer tecla para alocar a memoria:\n");
        getchar();

	ptr=malloc(1024*1024* (unsigned long) megabytes);

	if(ptr==NULL){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }

	printf("Pressione qualquer tecla para inicializar a memoria:\n");
        getchar();

	tmp_ptr=ptr;
	for(i=0;i<1024*1024* (unsigned long) megabytes; i++){
                strcpy(tmp_ptr,"a");
		tmp_ptr++;
	}

	printf("Memoria alocada, pressiona qualquer tecla para desalocar\n");
	getchar();
	free(ptr);
	printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n");
	getchar();
	return 0;
}

Neste exemplo vamos utilizar três terminais, um para execução do primeiro oom-killer.o, outro para o /proc e um último para executar um segundo oom-killer.o:

$ gcc oom-killer.c -o oom-killer.o
$ vagrant ssh
vagrant@contrib-stretch:~$ sudo su
root@contrib-stretch:/home/vagrant# echo 1 > /proc/sys/vm/overcommit_memory
root@contrib-stretch:/home/vagrant# exit 
vagrant@contrib-stretch:~$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:

Pressione qualquer tecla para inicializar a memoria:

Memoria alocada, pressiona qualquer tecla para desalocar

Agora no segundo terminal vamos consultar o score atual para OOM Killer do Kernel (quanto maior o número, maior a probabilidade do processo ser morto) através do /proc/PID/oom_score:

vagrant@contrib-stretch:~$ cat /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score #Sem alocar memoria
0
vagrant@contrib-stretch:~$ cat /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score #Com memoria alocada
0
vagrant@contrib-stretch:~$ cat /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score #Com memoria inicializada
678

No terceiro terminal vamos executar mais um oom-killer.o tentando alocar mais 1GB de memória:

$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:

Pressione qualquer tecla para inicializar a memoria:

Memoria alocada, pressiona qualquer tecla para desalocar

Agora vamos ver o aconteceu com o primeiro terminal:

vagrant@contrib-stretch:~$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:

Pressione qualquer tecla para inicializar a memoria:

Memoria alocada, pressiona qualquer tecla para desalocar
Killed

Na execução acima podemos ver que o score que define a probabilidade de um processo ser morto, em caso de falta de memória, aumenta consideravelmente conforme o uso residente de memória, afinal o score aumentou de 0 para 678 assim que sua memória foi efetivamente utilizada. Também podemos ver que devido ao alto score do primeiro processo, ele foi o escolhido para o “sacrifício” quando executamos o segundo processo em paralelo.

Outro ponto que podemos observar quando ocorre um OOM Killer são os logs do sistema, que normalmente trazem o processo que estava requerendo memória, a pilha de execução do momento e a listagem de processos do momento do incidente com seu consumo de memória:

root@contrib-stretch:/home/vagrant#  tail -n 300 /var/log/syslog
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435776] oom-killer.o invoked oom-killer: gfp_mask=0x24280ca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO), nodemask=0, order=0, oom_score_adj=0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435777] oom-killer.o cpuset=/ mems_allowed=0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435782] CPU: 0 PID: 28628 Comm: oom-killer.o Tainted: G           O    4.9.0-12-amd64 #1 Debian 4.9.210-1
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435782] Hardware name: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435784]  0000000000000000 ffffffff92736bfe ffffab79c0387cf0 ffff8d57dd355000
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435786]  ffffffff92609adb 0000000000000000 0000000000000000 0000000c92755c09
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435788]  ffff8d57daa9d0c0 ffffffff9258d587 0000004200000000 0000000000000001
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435790] Call Trace:
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435815]  [] ? dump_stack+0x66/0x88
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435819]  [] ? dump_header+0x78/0x1fd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435821]  [] ? get_page_from_freelist+0x3f7/0xb20
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435823]  [] ? oom_kill_process+0x22a/0x3f0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435824]  [] ? out_of_memory+0x111/0x470
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435826]  [] ? __alloc_pages_slowpath+0xa1f/0xb30
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435827]  [] ? __alloc_pages_nodemask+0x201/0x260
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435829]  [] ? alloc_pages_vma+0xaa/0x280
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435831]  [] ? handle_mm_fault+0x10ff/0x1350
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435832]  [] ? __do_page_fault+0x255/0x4f0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435836]  [] ? schedule+0x32/0x80
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435838]  [] ? page_fault+0x28/0x30
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435867] Mem-Info:
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435870] active_anon:58278 inactive_anon:58312 isolated_anon:0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435870]  active_file:11 inactive_file:17 isolated_file:0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435870]  unevictable:0 dirty:0 writeback:7 unstable:0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435870]  slab_reclaimable:1927 slab_unreclaimable:2281
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435870]  mapped:30 shmem:28 pagetables:1353 bounce:0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435870]  free:1131 free_pcp:60 free_cma:0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435872] Node 0 active_anon:233112kB inactive_anon:233248kB active_file:44kB inactive_file:68kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:120kB dirty:0kB writeback:28kB shmem:112kB shmem_thp: 0kB shmem_pmdmapped: 0kB anon_thp: 0kB writeback_tmp:0kB unstable:0kB pages_scanned:3372 all_unreclaimable? yes
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435873] Node 0 DMA free:1908kB min:88kB low:108kB high:128kB active_anon:13716kB inactive_anon:184kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:15992kB managed:15908kB mlocked:0kB slab_reclaimable:0kB slab_unreclaimable:12kB kernel_stack:0kB pagetables:84kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435876] lowmem_reserve[]: 0 455 455 455 455
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435878] Node 0 DMA32 free:2616kB min:2684kB low:3352kB high:4020kB active_anon:219388kB inactive_anon:233068kB active_file:44kB inactive_file:68kB unevictable:0kB writepending:28kB present:507840kB managed:488204kB mlocked:0kB slab_reclaimable:7708kB slab_unreclaimable:9112kB kernel_stack:1456kB pagetables:5328kB bounce:0kB free_pcp:240kB local_pcp:240kB free_cma:0kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435881] lowmem_reserve[]: 0 0 0 0 0
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435884] Node 0 DMA: 1*4kB (U) 0*8kB 1*16kB (M) 1*32kB (M) 1*64kB (U) 2*128kB (UM) 2*256kB (UM) 0*512kB 1*1024kB (U) 0*2048kB 0*4096kB = 1908kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435892] Node 0 DMA32: 116*4kB (UME) 71*8kB (E) 9*16kB (E) 11*32kB (UME) 1*64kB (U) 0*128kB 0*256kB 2*512kB (UM) 0*1024kB 0*2048kB 0*4096kB = 2616kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435901] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435902] 373 total pagecache pages
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435903] 300 pages in swap cache
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435904] Swap cache stats: add 4816960, delete 4816660, find 1408928/1597656
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435904] Free swap  = 0kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435904] Total swap = 1045500kB
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435905] 130958 pages RAM
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435905] 0 pages HighMem/MovableOnly
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435906] 4930 pages reserved
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435906] 0 pages hwpoisoned
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435907] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435910] [  188]     0   188    13890       31      23       3       95             0 systemd-journal
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435912] [  197]     0   197    11515        0      23       3      296         -1000 systemd-udevd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435913] [  347]     0   347    11625       24      29       3      115             0 systemd-logind
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435915] [  348]     0   348     7409        8      19       3       55             0 cron
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435916] [  349]   105   349    11278        0      26       3      120          -900 dbus-daemon
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435918] [  361]     0   361    62528       18      29       3      206             0 rsyslogd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435919] [  376]     0   376     3631        0      12       3       37             0 agetty
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435921] [  404]     0   404    17489        0      39       3      206         -1000 sshd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435922] [  437]     0   437    78758        0      30       3      174             0 VBoxService
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435923] [  671]   107   671    14038        0      31       3      191             0 exim4
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435925] [  761]     0   761     5089        0      14       3      258             0 dhclient
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435927] [20620]  1000 20620    16210        0      35       3      216             0 systemd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435928] [20621]  1000 20621    20618        0      41       3      400             0 (sd-pam)
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435930] [28561]     0 28561    23795        1      49       4      241             0 sshd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435931] [28567]  1000 28567    23871       43      48       4      229             0 sshd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435932] [28568]  1000 28568     5228        0      14       3      414             0 bash
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435934] [28603]     0 28603    23795        1      50       3      241             0 sshd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435935] [28609]  1000 28609    23864       28      49       3      242             0 sshd
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435936] [28610]  1000 28610     5249      210      15       3      213             0 bash
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435938] [28620]  1000 28620   263185     5435     520       4   256726             0 oom-killer.o
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435939] [28628]  1000 28628   263185   110295     225       4        0             0 oom-killer.o
Oct 14 02:54:23 contrib-stretch kernel: [1291138.435940] Out of memory: Kill process 28620 (oom-killer.o) score 678 or sacrifice child
Oct 14 02:54:23 contrib-stretch kernel: [1291138.436071] Killed process 28620 (oom-killer.o) total-vm:1052740kB, anon-rss:21740kB, file-rss:0kB, shmem-rss:0kB

No entanto, existem alguns parâmetros que nos permitem alterar o processo de escolha do OOM Killer, o primeiro parâmetro que vamos ver é definido no /proc/PID/oom_score_adj e nos permite somar ou reduzir o score do processo em questão.

Terminal 1:

vagrant@contrib-stretch:~$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:
Pressione qualquer tecla para inicializar a memoria:
Memoria alocada, pressiona qualquer tecla para desalocar

Terminal 2:

vagrant@contrib-stretch:~$ cat /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score
678
vagrant@contrib-stretch:~$ sudo su
root@contrib-stretch:/home/vagrant# echo '-100' > /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score_adj
root@contrib-stretch:/home/vagrant# cat /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score
578
root@contrib-stretch:/home/vagrant# echo '-800' > /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score_adj
root@contrib-stretch:/home/vagrant# exit
vagrant@contrib-stretch:~$ cat /proc/$(pgrep -f /vagrant/oom-killer.o)/oom_score
0

Terminal 3:

vagrant@contrib-stretch:~$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:
Pressione qualquer tecla para inicializar a memoria:
Killed

Neste exemplo acima podemos ver que com o parâmetro oom_score_adj do primeiro processo definido para -800, seu score caiu para 0, o que levou o OOM Killer selecionar o segundo processo para o “sacrifício”.

O outro parâmetro que podemos ajustar é o /proc/PID/oom_adj que tem efeito similar ao oom_score_adj, no entanto ele encontra-se depreciado desde a versão 2.6.36 do kernel.

Por fim ainda temos a possibilidade de desabilitar completamente o OOM Killer através do /proc/sys/vm/panic_on_oom, no entanto isso não é algo muito recomendado, pois pode levar a um kernel panic com crash total do sistema, o que pode ser visto na execução abaixo:

Terminal 1:

$ vagrant shh
vagrant@contrib-stretch:~$ sudo su
root@contrib-stretch:/home/vagrant# echo 1 >  /proc/sys/vm/panic_on_oom
root@contrib-stretch:/home/vagrant# exit
vagrant@contrib-stretch:~$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:
Pressione qualquer tecla para inicializar a memoria:
Memoria alocada, pressiona qualquer tecla para desalocar

Terminal 2:

$ vagrant ssh
vagrant@contrib-stretch:~$ /vagrant/oom-killer.o 1024
Pressione qualquer tecla para alocar a memoria:
Pressione qualquer tecla para inicializar a memoria:

Apos esse momento a máquina congela, e só podemos ver o que ocorreu através do console do VirtualBox:

Como podemos ver, ocorreu um kernel panic e agora somente um reboot forçado para retomar o acesso a máquina, o que reforça a recomendação de manter o OOM Killer sempre ativo, e se esse comportamento do OOM Killer for inadequado para o seu ambiente é preferível ajustar o “overcommit_memory” para o modo 2 (modo restritivo), onde o kernel não vai permitir a super-alocação de memória, o que por sua vez diminui drasticamente a probabilidade de um evento de falta de memória ocorrer.

E se tiver muita memória RAM?

Ter uma grande quantidade de memória RAM no sistema pode parece ser uma excelente ideia para minimizar a probabilidade do uso SWAP e evitar situações de estouro de memória e ativação do OOM Killer. No entanto, quando se trabalha com grandes quantidades de memória RAM passamos a enfrentar novos desafios relacionados a gerência de um número excessivo de páginas de memória.

Imagine um servidor com 1TB de memória RAM, onde temos processos que utilizam mais de 100GB de memória, realizando um cálculo rápido, levando em conta uma página de memória de 4KB, esses processos teriam de lidar com algo em torno de 26 milhões de páginas cada. Isso pode impactar diretamente no desempenho do sistema, pois o kernel teria de gerenciar tabelas de mapeamento com milhões de páginas em diversos processos e levando em conta a capacidade do TLB de um processador moderno (Intel Sky Lake/Core I3/5/7 de 6 geração) de 128 entradas para instruções, 64 entradas para dados, 1536 entradas compartilhadas de segundo nível, seria inevitável o esgotamento do TLB, o que iria forçar a MMU percorrer a PGD constantemente para encontrar as páginas solicitadas.

Felizmente o kernel possui um recurso para minimizar este problema, o “HugePages”, que basicamente é a capacidade de trabalhar com páginas de memória maiores do que o normal, no caso da arquitetura x86-64 e além das páginas de 4KB, temos a possibilidade de trabalhar com páginas de 2MB e 1GB.

De maneira geral o HugePages tem o objetivo de:

  • Diminuir o overhead do kernel sobre a manutenção das tabelas de mapeamento de memória dos processos
  • Aproveitar melhor o TLB, que é um recurso escasso e de fundamental importância para o desempenho dos processos

Atualmente o kernel permite a utilização explícita de HugePages através do “Hugetlbpage” e a utilização de forma transparente, que não requer alterações nas aplicações, através do THP (Transparent Huge Pages).

Hugetlbpage

O Hugetlbpage permite o uso de HugePages de forma explícita pelas aplicações através de chamadas “mmap” ou através do uso de memória compartilhada. No entanto, para ambos os métodos é necessário indicar previamente para o kernel o número de HugesPages a ser reservado na memória RAM através do /proc/sys/vm/nr_hugepages e uma quantidade adicional de páginas que o kernel tentará alocar, caso seja necessário, através do /proc/sys/vm/nr_overcommit_hugepages.

Vale lembrar que as páginas reservadas para HugePages não podem ser utilizadas para outros fins e também não podem ser enviadas para swap, então o ideal é não exagerar nesta configuração e utilizá-la de acordo com o necessário especificado pelas aplicações.
O tamanho da HugePage padrão pode ser consultado através do /proc/meminfo no item “Hugepagesize” e só pode ser alterado no boot através do parâmetro default_hugepagesz.

Para realizar a alocação através da chamada mmap temos duas opções: a primeira é realizar a chamada requerendo memória anonima com a flag MAP_HUGETLB e a segunda é realizar a chamada requerendo o mapeamento de arquivo em memória que resida em um ponto de montagem do tipo hugetlbfs, que deve ser montado previamente.

Para alocação através do uso de memória compartilhada (shmget e shmat) é necessário o uso da flag SHM_HUGETLB e que o usuário que é dono processo pertença ao grupo definido no arquivo /proc/sys/vm/hugetlb_shm_group.

Para ver como tudo isso tudo funciona na prática, preparamos dois programas que alocam e utilizam uma quantidade de memória via MMAP, sendo que um utiliza HugePages e o outro não:

hugepage-off.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main(int argc, char* argv[]){
	char  *ptr, *tmp_ptr;


	unsigned long i;
	if(argc < 2){
		printf("Usage: <Size allocation (megabytes)>");
	}
	int megabytes = atoi(argv[1]);
        ptr=mmap(0,1024*1024* (unsigned long) megabytes,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);

	if(ptr==MAP_FAILED){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }
	tmp_ptr=ptr;
	for(i=0;i+1<1024*1024* (unsigned long) megabytes; i++){
                strcpy(tmp_ptr,"a");
		tmp_ptr++;
	}

	printf("Memoria alocada\n");
	munmap(ptr,1024*1024* (unsigned long) megabytes);
	printf("Memoria desalocada\n");
	return 0;
}

hugepage-on.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main(int argc, char* argv[]){
	char  *ptr, *tmp_ptr;


	unsigned long i;
	if(argc < 2){
		printf("Usage: <Size allocation (megabytes)>");
	}
	int megabytes = atoi(argv[1]);
        ptr=mmap(0,1024*1024* (unsigned long) megabytes,PROT_WRITE|PROT_READ,MAP_HUGETLB|MAP_PRIVATE|MAP_ANONYMOUS,-1,0);

	if(ptr==MAP_FAILED){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }
	tmp_ptr=ptr;
	for(i=0;i-1<1024*1024* (unsigned long) megabytes; i++){
                strcpy(tmp_ptr,"a");
		tmp_ptr++;
	}

	printf("Memoria alocada\n");
	munmap(ptr,1024*1024* (unsigned long) megabytes);
	printf("Memoria desalocada\n");
	return 0;
}

Inicialmente vamos tentar iniciar o hugepage-on.o com 200MB de memória:

$ ./hugepage-on.o 200
Falha ao alocar esta quantidade de memoria

Isso acontence devido a não existirem HugePages disponíveis para alocação, o que pode ser visto através do /proc/meminfo nos campos HugePages_Total e HugePages_Free:

# cat /proc/meminfo  | grep '^Huge'
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

A saída acima indica que não existem HugePages disponíveis e que não existe nenhuma reserva de memória para HugePages, o que pode ser verificado e alterado através do /proc/sys/vm/nr_hugepages:

# cat /proc/sys/vm/nr_hugepages
0
# echo 200 > /proc/sys/vm/nr_hugepages
# cat /proc/meminfo  | grep '^Huge'
HugePages_Total:      78
HugePages_Free:       78
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

Apesar de parecer um comportamento inesperado, solicitar a reserva de uma quantidade de páginas para HugePages no nr_hugepages e visualizar uma quantidade menor no /proc/meminfo, é um comportamento perfeitamente normal, pois é comum que o kernel não consiga reservar a quantidade de memória solicitada devido a fragmentação da memória RAM. A única forma de garantir a reserva é incluir o parâmetro hugepages=N no boot do kernel.
No nosso caso, o kernel só conseguiu reservar 78 páginas, o que daria por volta de 156MB, por isso vamos executar nossos testes alocando 140MB:

 
# time ./hugepage-off.o 140
Memoria alocada
Memoria desalocada

real	0m0,445s
user	0m0,392s
sys	0m0,045s
# time ./hugepage-on.o 140
Memoria alocada
Memoria desalocada

real	0m0,004s
user	0m0,001s
sys	0m0,003s

Na execução acima podemos ver que, mesmo com uma quantidade pequena de memória, ou seja. “140MB”, temos uma diferença de desempenho considerável, por volta de 400ms para a versão sem HugePages e 4ms para versão com HugePages.

Para ver a diferença entre as execuções vamos utilizar a ferramenta perf-stat:

# perf stat -d ./hugepage-off.o 140
Memoria alocada
Memoria desalocada

 Performance counter stats for './hugepage-off.o 140':

        372,114365      task-clock (msec)         #    0,991 CPUs utilized          
                10      context-switches          #    0,027 K/sec                  
                 0      cpu-migrations            #    0,000 K/sec                  
            35.891      page-faults               #    0,096 M/sec                  
     1.288.594.148      cycles                    #    3,463 GHz                      (48,50%)
     1.729.499.101      instructions              #    1,34  insn per cycle           (61,32%)
       162.942.345      branches                  #  437,882 M/sec                    (61,31%)
           199.318      branch-misses             #    0,12% of all branches          (62,29%)
       762.267.431      L1-dcache-loads           # 2048,476 M/sec                    (63,37%)
         2.971.196      L1-dcache-load-misses     #    0,39% of all L1-dcache hits    (64,40%)
            95.096      LLC-loads                 #    0,256 M/sec                    (50,60%)
            36.332      LLC-load-misses           #   38,21% of all LL-cache hits     (49,53%)

       0,375599296 seconds time elapsed

# perf stat -d ./hugepage-on.o 140
Memoria alocada
Memoria desalocada

 Performance counter stats for './hugepage-on.o 140':

          1,622347      task-clock (msec)         #    0,647 CPUs utilized          
                 0      context-switches          #    0,000 K/sec                  
                 0      cpu-migrations            #    0,000 K/sec                  
                53      page-faults               #    0,033 M/sec                  
           902.901      cycles                    #    0,557 GHz                      (29,37%)
           732.331      instructions              #    0,81  insn per cycle         
           143.505      branches                  #   88,455 M/sec                  
             7.045      branch-misses             #    4,91% of all branches        
           200.320      L1-dcache-loads           #  123,475 M/sec                  
            16.217      L1-dcache-load-misses     #    8,10% of all L1-dcache hits  
           LLC-loads                                                     (0,00%)
           LLC-load-misses                                               (0,00%)

       0,002507633 seconds time elapsed

A saída do perf-stat mostra que a execução da versão sem HugePages gastou um número muito maior de ciclos de CPU do que a versão com HugePages, ~1 bilhão de ciclos e 900 mil ciclos respectivamente. Outro ponto que podemos ver, é que o número de “page faults” também é muito maior na versão sem HugePages, 35mil page faults vs 53 page faults na versão com HugePages.

THP – Transparent Huge Pages

O THP permite o uso de HugePages de maneira transparente para as aplicações que utilizam memória anônima (mmap), tmpfs ou memória compartilhada. Quando habilitado, a cada minor page fault o kernel tentará alocar uma HugePage de 2MB, porém, devido ao THP não possuir uma reserva de memória como o Hugetlbpage, não existem garantias de que essa alocação terá sucesso. Quando essa tentativa de formar uma HugePage falha devido a fragmentação, ocorre o fallback para tamanho de página normal, de forma transparente para a aplicação e assim que páginas forem liberadas e a desfragmentação ocorrer, o khugepaged move as páginas comuns de maneira automática para formar uma HugePage.

Vale salientar que, com o passar do tempo, mesmo alocações de pequenos blocos de memória, com tamanho inferior a uma HugePage podem ser movidos para áreas contíguas de memória física e serem transformadas em HugePages pelo khugepaged.

O khugepaged é uma thread do kernel que é executado a cada X milisegundos e varre uma quantidade Y de páginas, cujo objetivo é unir páginas simples e transformá-las em HugePages. Opcionalmente ele também pode realizar a desfragmentação da meméria. Sua configuração é realizada através dos seguintes arquivos:

  • /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs: define o intervalo entre as rodadas do khugepaged (X)
  • /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan: define o número máximo de páginas a serem varridas por rodada (Y)
  • /sys/kernel/mm/transparent_hugepage/khugepaged/defrag: permite habilitar ou desabilitar a desfragmentação pelo khugepaged

Um efeito colateral do uso do THP é um possível disperdício de memória, pois caso as aplicações realizem alocações de memória onde só se utilizam alguns bytes efetivamente, a alocação física de memória será de 2MB ao invés de apenas 4KB. Outro efeito colateral é relacionado a um possível uso excessivo de CPU para desfragmentação dependendo do workload da aplicação. Normalmente bancos de dados acabam sofrendo destes efeitos colaterais e não recomendam o uso do THP.

Por esses motivos o kernel permite que o THP seja desativado completamente ou ser ativado somente quando indicado pela aplicação, através da chamada madvise(MADV_HUGEPAGE).

Essa configuração é realizada através do /sys/kernel/mm/transparent_hugepage/enabled e pode conter os seguintes valores:

  • always – O THP estará sempre habilitado, toda alocação de memória anônima de qualquer processo estará sujeita a utilização de HugePages
  • madvise – O THP estará habilitado, porém, somente as alocações de processos que efetuarem a chamada madvise(MADV_HUGEPAGE) estarão sujeitas a se tornar uma HugePage. Esta é a opção padrão das versões atuais do kernel
  • never – O THP estará completamente desabilitado

Outro ponto que podemos ajustar, é o comportamento da desfragmentação da memória, através do /sys/kernel/mm/transparent_hugepage/defrag. Nele indicamos como o kernel deve proceder quando o ocorre uma tentativa de alocação de uma HugePage onde não existe memória contígua suficiente para formar uma HugePage e que permite o seguintes comportamentos:

  • Always: o kernel irá aguardar a tentativa de “reivindicar” e compactar memória para dar espaço para uma HugePage
  • Defer: o kernel opta pela alocação de uma página simples e em seguida acorda o Kswapd e o Kcompactd para desfragmentar memória. Assim que houver espaço disponível o khugepaged irpa mover as páginas simples para uma HugePage
  • Defer+MAdvise: terá o mesmo comportamento do “always” para chamadas com “madvise” e para o restante utilizara o “defer”
  • Madvise: somente chamadas com “madvise” vão reivindicar memória para HugePages, que utilizará o mesmo comportamento do “always”
  • Never: nunca tenta realizar o defrag

Lembrando que estes parâmetros não são do proc, então para persistir após uma reinicialização é necessário alterá-los nos parâmetros do kernel no momento do boot.

Através do /proc podemos monitorar o uso geral do THP através do /proc/meminfo nos campos “AnonHugePages” e “ShmemHugePages”, onde podemos visualizar a quantidade total de memória utilizada para memória anônima e para memória compartilhada respectivamente. Alem do meminfo também temos o /proc/vmstat onde podemos visualizar em detalhes o comportamento do THP através dos seguintes campos:

  • thp_fault_alloc – É um contador que é incrementado toda vez que uma HugePage é alocada com sucesso
  • thp_collapse_alloc – É um contador que é incrementado toda vez que o khugepaged identifica uma área de memória que pode ser transformada em uma HugePage
  • thp_fault_fallback – É um contador que é incrementado toda vez que a alocação de uma HugePage falha e o kernel utiliza uma página simples.
  • thp_split_page – É um contador que é incrementado toda vez que uma HugePage é divida em páginas comuns e acontece com páginas antigas por conta de reivindicação de memória.
  • compact_stall – É um contador que é incrementado toda vez que um processo aguarda a compactação para liberação de uma HugePage
  • compact_pages_moved – É um contador que é incrementado toda vez que uma página precisa ser movida de uma região de memória para compactar a memória e liberar espaço para uma HugePage. Observar esse item é importante, pois um grande aumento neste item indica que está ocorrendo um grande número de operações de cópia de conteúdo entre regiões de memória, comportamento que pode impactar no desempenho e minimizar o benefício do uso do THP.

Para realizar testes em cima do THP desenvolvemos o thp-test.c onde alocamos uma área de memória anônima baseada no primeiro parâmetro passado na execução, e em seguida preenchemos esta área de memória com o caracter “A” através de acessos randômicos seguidos de acessos sequenciais de 8MB.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main(int argc, char* argv[]){
        char  *ptr, *tmp_ptr;


        unsigned long i,n,total_size,offset;

        if(argc < 2){
                printf("Usage: <Size allocation (megabytes)>");
        }
        int megabytes = atoi(argv[1]);
        total_size = 1024*1024* (unsigned long) megabytes;
        ptr=mmap(0,1024*1024* (unsigned long) megabytes,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);

        if(ptr==MAP_FAILED){
                printf("Falha ao alocar esta quantidade de memoria\n");
                return 1;
        }

        tmp_ptr=ptr;
        for(i=0;i+1<  (unsigned long) megabytes; i++){
                offset=((unsigned long) random()) % (total_size + 1);
                for(n=0;n<8388608;n++){
                        if(tmp_ptr+offset+n >= (tmp_ptr+total_size)-1 ){
                                break;
                        }
                        strcpy(tmp_ptr+offset+n,"a");
                }
        }

        printf("Memoria alocada\n");
        munmap(ptr,1024*1024* (unsigned long) megabytes);
        printf("Memoria desalocada\n");
        return 0;
}
$ gcc thp-test.c  thp-test.o
$ sudo su
# cat /proc/meminfo | egrep 'AnonHugePages|ShmemHugePages'
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
# cat /sys/kernel/mm/transparent_hugepage/enabled 
always [madvise] never
# perf stat  -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses  -e cycles     ./thp-test.o 200
Memoria alocada
Memoria desalocada

 Performance counter stats for './thp-test.o 200':

    16.534.503.996      dTLB-loads                                                  
            89.952      dTLB-load-misses          #    0,00% of all dTLB cache hits 
     3.324.761.221      dTLB-stores                                                 
           664.147      dTLB-store-misses                                           
    17.053.408.632      cycles                                                      

       4,897094809 seconds time elapsed

# cat /proc/vmstat | egrep 'thp'
thp_fault_alloc 0
thp_fault_fallback 0
thp_collapse_alloc 0
thp_collapse_alloc_failed 0
thp_file_alloc 0
thp_file_mapped 0
thp_split_page 0
thp_split_page_failed 0
thp_deferred_split_page 0
thp_split_pmd 0
thp_split_pud 0
thp_zero_page_alloc 0
thp_zero_page_alloc_failed 0
thp_swpout 0
thp_swpout_fallback 0
# echo 'always' > /sys/kernel/mm/transparent_hugepage/enabled 
# perf stat  -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses  -e cycles     ./thp-test.o 200
Memoria alocada
Memoria desalocada

 Performance counter stats for './thp-test.o 200':

    16.507.602.661      dTLB-loads                                                  
            31.461      dTLB-load-misses          #    0,00% of all dTLB cache hits 
     3.308.502.249      dTLB-stores                                                 
           223.286      dTLB-store-misses                                           
    16.816.673.023      cycles                                                      

       4,834108739 seconds time elapsed
# cat /proc/vmstat | egrep 'thp'
thp_fault_alloc 103
thp_fault_fallback 0
thp_collapse_alloc 0
thp_collapse_alloc_failed 0
thp_file_alloc 0
thp_file_mapped 0
thp_split_page 0
thp_split_page_failed 0
thp_deferred_split_page 103
thp_split_pmd 0
thp_split_pud 0
thp_zero_page_alloc 0
thp_zero_page_alloc_failed 0
thp_swpout 0
thp_swpout_fallback 0

Na execução acima podemos ver que:

  • Não existia uso de THP no momento, o que pode ser visto através do /proc/meminfo
  • No /sys/kernel/mm/transparent_hugepage/enabled vimos que o THP estava configurado para ser utilizado somente caso a aplicação assim indicasse com o madvise
  • Após a primeira execução do thp-test.o vimos no /proc/vmstat que nenhum recurso de THP havia sido usado até então
  • Sabendo que nossa aplicação não chama o madvise, alteramos o /sys/kernel/mm/transparent_hugepage/enabled para always
  • Executamos nossa aplicação novamente, o que resultou no uso de aproximadamente 103 HugePages, conforme observado no /proc/vmstat

Apesar do tempo de execução dos dois testes serem muito próximos, o perf stat mostra claramente o melhor aproveitamento do TLB na execução com THP, através de um número muito menor “dTLB-load-misses” e “dTLB-store-misses” assim como na quantidade de ciclos de CPU utilizados.

Próximos passos

No próximo post iremos abordar mais algumas funcionalidades da memória virtual, como memória compartilhada e page cache etc.

Até lá!

 

 

Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:

CURSOSCONSULTORIA

Anterior Guia sobre como criar um cluster com o centralizador de logs Graylog 3.3
Próxima Dominando o Docker Compose: Guia completo para gerenciamento de multi-contêineres

About author

william.welter
william.welter 5 posts

Formado em ciência da computação, com as certificações LPIC-3, LPI DevOps Tools Engineer, LFCS, LFCE, ZCE-PHP5.3, ZFCA, PP9A, desenvolvedor PHP e C, com foco em Linux e software livre. Atualmente é gerente e líder técnico do time de consultoria e suporte na 4Linux, a qual atua principalmente com ferramentas DevOps, containers, cloud, monitoramento e sistemas de missão critica.

View all posts by this author →

Você pode gostar também

Infraestrutura TI

Entenda o Log Binário do MySQL e suas aplicações práticas

O log binário do MySQL é, por vezes, mal compreendido, principalmente por usuários de outros bancos de dados. Nesta postagem pretendo abordar alguns aspectos desse importante mecanismo. Write-ahead logging? Também

DevOps

Docker: Ferramenta essencial para desenvolvedores e gestão de aplicativos

O que é o Docker O Docker é uma ferramenta de código aberto para conteinerização que agiliza a criação e a implantação de aplicativos por meio do uso de contêineres.

Infraestrutura TI

Como implementar a funcionalidade de autocompletar com Elasticsearch

A função de autocompletar presente na maioria das plataformas que utilizamos no dia a dia, como serviços de busca, plataformas de streaming e lojas online, já se provou uma excelente