[Rio-pm] Ajuda com Threads - tempo de abertura

Blabos de Blebe blabos em gmail.com
Domingo Julho 11 08:36:28 PDT 2010


Só que tem umas pegadinhas aí.

Primeiramente seu gargalo não é processamento, é I/O, certo? Já que rede é I/O.

Dado que você executa um ping externo, invariavelmente, seu script vai
fazer um fork para executar esse ping. Então, se você está usando 250
threads ou 250 forks dá no mesmo pois cada thread/fork vai fazer ao
menos um fork adicional.

Aliás se o seu processo externo pode demorar até seg no caso do host
não responder, a diferença entre criação de thread ou fork passa a ser
irrelevante.

Fiz alguns testes aqui na minha rede local (range de 254 ips com 2
ativos - perto do pior caso) com icmp usando:

qx: 56.500s
system: 66.959s
Net::Ping: 26.135s

Fiz os meus testes com o script abaixo:
http://gist.github.com/470412

O que já mostra muita diferença.

Eu observei não-cientificamente o comportamento do meu sistema via top.

Usando qx e system, vi os ontes de processos ping sendo criados e o
meu script não conseguiu passar de 50% de uso da cpu, pois, a thread
dispara o novo processo que vai fazer I/O e ela por tabela espera.

Já com o Net::Ping consegui bater na marca de 71%.

blabos em host:~/workspace/threads$ cat /proc/cpuinfo
processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 13
model name	: Intel(R) Celeron(R) M processor          900MHz
stepping	: 8
cpu MHz		: 900.195
cache size	: 512 KB
fdiv_bug	: no
hlt_bug		: no
f00f_bug	: no
coma_bug	: no
fpu		: yes
fpu_exception	: yes
cpuid level	: 2
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov
clflush dts acpi mmx fxsr sse sse2 ss tm pbe nx up bts
bogomips	: 1800.39
clflush size	: 64
cache_alignment	: 64
address sizes	: 32 bits physical, 32 bits virtual
power management:


Não adianta muito você escolher entre fork ou thread se sua thread vai
fazer fork.

Além do mais deve haver para o seu hardware/SO um limite ideal de
threads/forks para vc minimizar o chaveamento de contexto. Também não
adianta um milhão de threads se você vai passar a maior parte do tempo
chaveando entre elas.

Como vc tem bem mais hosts que isso, procure o número ideal de
threads/processos que minimize a perda por chaveamento de contexto e
use algum módulo de pool de thread ou fork. Sugiro pra fork o
Parallel::ForkManager, mais por facilidade mesmo.

Mas lembre-se, seu gargalo *NÃO* é o sistema de threads.

Quanto ao Net::Ping se enrolar, está fora da minha jurisdição, mas eu
notei um comportamento estranho sim, e adicionei um sleep 3 no final
do for, que aparentemente resolveu. O que eu notei é que para um host
down, as 9 etapas de verificação levavam apenas 6 seg, mesmo com
timeout de 1 seg pra cada. Adicionei o sleep 3 e ficou tudo 'normal'.

Há pessoas muito mais habilitadas na lista do que eu para resolver
isso. Sugiro de antemão, confirmar essa afirmação cientificamente e/ou
então submeter um patch.

Gurus? Agora é com vcs. :)

Abraços



2010/7/10 João André Simioni <jasimioni em gmail.com>:
> Fiz um teste agora colocando um sleep 10 no inicio da função checkIp.
> As threads abrem rápido assim, mas o desempenho ficou ruim. Além
> disso, notei que as threads não utilizam os demais processadores.
>
> Minha solução atual está com um fork no começo, quebrando o código em
> 8 processos distintos (número de processadores), cada processo com um
> sub-grupo e criando suas threads (30 threads por grupo).
>
> Obrigado pela ajuda
>
> João André
>
> 2010/7/10 João André Simioni <jasimioni em gmail.com>:
>> Blabos,
>>
>> o loop interno não é nem usado nesse código, como observado pelo
>> Bruno, porque ele define a quantidade de threads no início. Usualmente
>> eu disparo uma thread para cada elemento que vou gerenciar, e defino
>> um limite de threads abertas no início do código e é por isso que esse
>> loop existe, para controlar o limite de threads (e funciona bem em
>> diversos outros scripts que não têm o mesmo problema de desempenho).
>>
>> Segue os comentários no código:
>>
>>> #!/usr/bin/perl
>>>
>>> use strict;
>>> use DBI;
>>> use Net::Ping;
>>> use Time::HiRes qw/usleep tv_interval gettimeofday/;
>>> use threads ('yield', 'stack_size' => 32*4096, 'exit' =>
>>> 'threads_only', 'stringify');
>>>
>>> my $host = '192.168.160.179';
>>> my $sid  = 'HOMOLOG';
>>> my $user = 'mac_user';
>>> my $pass = 'mac_user';
>>>
>>> my $dbh = DBI->connect("dbi:Oracle:host=$host;sid=$sid", $user, $pass,
>>> { AutoCommit => 1 });
>>>
>>> my $sth = $dbh->prepare('SELECT CPE_ID, CLIENTE_ID, CPE_DESC,
>>> CPE_IP_WAN, CPE_NODE, CPE_IF FROM MAC_CPE WHERE CPE_PING = ? AND
>>> CPE_ATIVO = ?');
>>> $sth->execute('1', 'A');
>>>
>>> my @cpes;
>>>
>>> while (my (@row) = $sth->fetchrow_array) {
>>>     push @cpes, [ @row ];
>>> }
>>>
>>> $dbh->disconnect();
>>
>> Até aqui somente listei os elementos a partir do banco de dados.
>>
>> O código abaixo quebra o numero de elementos em 250 threads.
>>
>>> my $maxThreads = 250;
>>> my $total      = @cpes;
>>>
>>> my $cpePerThread = int($total / $maxThreads);
>>>
>>> my @toMonitor;
>>> my $i = 0;
>>> my $c = 0;
>>>
>>> foreach my $cpe (@cpes) {
>>>     push @{$toMonitor[$i]}, $cpe;
>>>     $c++;
>>>
>>>     if ($c > $cpePerThread) {
>>>         $i++;
>>>         $c = 0;
>>>     }
>>> }
>>>
>>> print "Para $total cpes, e $maxThreads threads, tenho no final $i
>>> grupos, com ", scalar @{$toMonitor[0]}, " elementos cada\n";
>>
>>
>>
>> E imprimo - vou ter o total de elementos dividido em 250 grupos (cada
>> grupo atendido por uma thread)
>>
>>
>>
>>> sub processResult {
>>>     my $r = shift;
>>>     if (ref $r ne 'ARRAY') {
>>>         print STDERR "Erro na thread\n";
>>>         return 0;
>>>     }
>>>     my ($result, $host, $ifs) = @$r;
>>>
>>> }
>>
>> Funcao de processamento do resultado da thread -- aqui não faz nada
>>
>>> sub testeClient {
>>>     my $threadNum = shift;
>>>     my $cpeGroup  = shift;
>>>     foreach my $cpe (@{$cpeGroup}) {
>>>         my ($cpeId, $clientId, $cpeDesc, $cpeIp, $cpeNode, $cpeIf) = @$cpe;
>>>         my $pingOk = &checkIp($cpeIp);
>>>         my $status = $pingOk ? 'RESPONDE' : 'MORTO';
>>>         # print join(", ", $threadNum, $cpeId, $clientId, $cpeDesc,
>>> $cpeIp, $cpeNode, $cpeIf, $status), "\n";
>>>     }
>>>     # print "Finishing thread $threadNum\n";
>>>     return([1, 0]);
>>> }
>>
>> Código da thread - basicamente chama a sub 'checkIp' para ver se o
>> host está vivo.
>>
>> Abaixo o código de criação das threads - para cada grupo, cria uma thread.
>>
>>> my $threadCount = 1;
>>> foreach my $cpeGroup (@toMonitor) {
>>>     print "Trying to create thread";
>>>     my $t0 = [gettimeofday];
>>>
>>>     print " in ", scalar localtime $t0->[0], ".", $t0->[1], "\n";
>>>
>>>     my $thr = threads->create({scalar => '1'}, 'testeClient',
>>> $threadCount, $cpeGroup);
>>>     my $elapsed = tv_interval ( $t0, [gettimeofday]);
>>>     print "Created thread $thr ($threadCount) - took $elapsed seconds
>>> to create\n";
>>>     $threadCount++;
>>
>> Esse bloco pode ser removido, pois nunca entra - foi a primeira coisa
>> que tirei, porque achei que a chamada a threads->list poderia estar
>> influenciando. Esse método retorna em menos de 1ms em todas as
>> execuções. Mas se preferir só ignora.
>>
>>>     while (threads->list() >= $maxThreads) {
>>>         my @joinable = threads->list(threads::joinable);
>>>         for (@joinable) {
>>>             my $r = $_->join();
>>>             &processResult($r);
>>>         }
>>>         usleep(10000);
>>>     }
>>>     my $elapsed = tv_interval ( $t0, [gettimeofday]);
>>
>> Finaliza a criação de threads
>>
>>> }
>>
>> Aguarda as threads concluirem.
>>
>>> while (threads->list()) {
>>>     my @joinable = threads->list(threads::joinable);
>>>     for (@joinable) {
>>>         my $r = $_->join();
>>>         &processResult($r);
>>>     }
>>>     usleep(10000);
>>> }
>>
>> Fim de execução
>>
>>> sub checkIp {
>>>     my $ip = shift;
>>>     my $pingOk = 0;
>>
>>
>> Essa rotina faz 3 testes para cada elemento. Em cada teste, tenta um
>> ping ICMP, um UDP e um TCP. Tenho que executar o ping do S.O. porque
>> senão sou obrigado a rodar o script como root e o Net::Ping faz
>> confusão com o ICMP. Se eu testo um host e ele nao responde no tempo
>> de timeout e eu em seguida testar outro host, se o primeiro responder
>> ele acha que o segundo respondeu.
>>
>>>     eval {
>>>         for (1..3) {
>>>             my $ping = `/bin/ping -c 1 -w 1 $ip`;
>>>
>>>             if (grep { / 0% packet loss/ } $ping) {
>>>                 $pingOk = 1;
>>>                 return 1;
>>>             }
>>>
>>>             if ($pingOk == 0) {
>>>                 my $p = Net::Ping->new('icmp');
>>>                 $p->service_check(1);
>>>                 if ($p->ping($ip, 1)) {
>>>                     $pingOk = 1;
>>>                     return 1;
>>>                 }
>>>             }
>>>
>>>             if ($pingOk == 0) {
>>>                 my $p = Net::Ping->new('udp');
>>>                 $p->service_check(1);
>>>                 if ($p->ping($ip, 1)) {
>>>                     $pingOk = 1;
>>>                     return 1;
>>>                 }
>>>             }
>>>
>>>             sleep 3;
>>>         }
>>>     };
>>>
>>>     return $pingOk;
>>> }
>>>
>>
>> Quanto a usar Java, é que tenho um grupo de desenvolvimento Java do
>> meu lado - eu posso solicitar a eles, mas eu não vejo porque Perl não
>> pode atender bem nesse caso.
>>
>> Obrigado
>>
> _______________________________________________
> Rio-pm mailing list
> Rio-pm em pm.org
> http://mail.pm.org/mailman/listinfo/rio-pm
>


Mais detalhes sobre a lista de discussão Rio-pm