Página 1 de 1

Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-24 10:13 @467
por Superdri
Saludos.

Estoy haciendo un apartado de mi web para seguir las partidas que se están jugando del WoW a tiempo real a partir de la API que me manda las clasificaciones de los jugadores.

Lo que hago es grabar como partidas de jugadores nuevos las entradas que no tienen un jugador conocido en mi base de datos y grabar como partidas nuevas las entradas de jugadores en mi base de datos que tengan un índice diferente a la última partida de ese jugador.

Tengo básicamente 3 problemas:
  • El rendimiento de mi nube no baja del 70 % mientras estoy siguiendo EU y US (son dos scripts distintos).
  • Los no-éxitos de la petición de la clasificación vía HTTPS, muchas veces el servidor de la API no me responde y no sé por qué.
  • Caracteres mal formados en el json que recibo. De vez en cuando recibo errores tipo: "expected while parsing object/hash, at character offset 87785 (before ",")" al decodificar el json.
Os dejo el script a ver si podéis echarme una mano.
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. use JSON;
  2. use DBD::mysql;
  3. use LWP::UserAgent;
  4. use Daemon::Easy sleep=>1, stopfile=>'stopeu', pidfile=>'pideu', callback=>'worker';
  5.  
  6. sub worker {
  7.         partidas('2v2');
  8.         partidas('3v3');
  9.         partidas('rbg');
  10. }
  11.  
  12. sub partidas {
  13.         my $bracket = shift;
  14.         my ($dbname,$dbhost,$dbuser,$dbpass) = ('blizzardrankings', 'localhost', 'miuser', 'mipass');
  15.         my $season = 's23';
  16.  
  17.         my $ua = LWP::UserAgent->new;
  18.         my $response = $ua->get("https://eu.api.battle.net/wow/leaderboard/$bracket?locale=en_GB&apikey=5gdrgfafqdnkryj8tqafqxsdr4qamdcq");
  19.        
  20.         #muchos unsuccess
  21.         if ($response->is_success) {
  22.                 my $contenido = $response->decoded_content;
  23.                 $contenido =~ s/\n//g;
  24.                 $contenido =~ s/ //g;
  25.                 #a veces falla el decode json
  26.                 my $json = JSON->new->utf8->decode($contenido);
  27.  
  28.                 my $db = DBI->connect("DBI:mysql:$dbname:$dbhost", "$dbuser", "$dbpass") or die "Imposible conectar con la DB";
  29.                 $db->{'mysql_enable_utf8'} = 1;
  30.  
  31.                 my $sth = $db->prepare("SET character_set_results = 'utf8', character_set_client = 'utf8', character_set_connection = 'utf8', character_set_database = 'utf8', character_set_server = 'utf8'");
  32.                 $sth->execute() or die "imposible insertar en la tabla";
  33.                 $sth->finish;
  34.  
  35.                 $sth = $db->prepare("CREATE TABLE IF NOT EXISTS `blizzardrankings`.`wow_eu_partidas_$bracket\_$season`(
  36.                                                                 `ranking` INT(4) NOT NULL,
  37.                                                                 `rating` INT(4) NOT NULL,
  38.                                                                 `name` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  39.                                                                 `realm_id` INT(4) NOT NULL,
  40.                                                                 `realm_slug` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  41.                                                                 `race_id` INT(2) NOT NULL,
  42.                                                                 `class_id` INT(2) NOT NULL,
  43.                                                                 `spec_id` INT(3) NOT NULL,
  44.                                                                 `faction_id` INT(2) NOT NULL,
  45.                                                                 `gender_id` INT(2) NOT NULL,
  46.                                                                 `rating_change` INT(2) NOT NULL,
  47.                                                                 `ranking_change` INT(2) NOT NULL,
  48.                                                                 `date` TIMESTAMP
  49.                                                         ) ENGINE = InnoDB CHARSET=utf8 COLLATE utf8_general_ci"
  50.                 );                             
  51.                 $sth->execute() or die "imposible crear la tabla";
  52.                 $sth->finish;
  53.                
  54.                 foreach my $entradas (@{$json->{'rows'}}) {
  55.                         #hacer un select con todos los jugadores de golpe seria mas rapido? eso incluiria otro bucle para formar el query sql
  56.                         $sth = $db->prepare("SELECT * FROM `wow_eu_partidas_$bracket\_$season` WHERE name = '$entradas->{name}' AND realm_id = $entradas->{realmId} ORDER BY date DESC LIMIT 1");
  57.                         $sth->execute() or die "imposible consultar la tabla";
  58.                         my $data = $sth->fetchrow_hashref();
  59.                         $sth->finish;
  60.                         if ($data->{name}) {
  61.                                 if ($entradas->{rating} != $data->{rating}) {
  62.                                         my $rating_change = $entradas->{rating} - $data->{rating};
  63.                                         my $ranking_change = $entradas->{ranking} - $data->{ranking};
  64.                                         $entradas->{'realmName'} =~ s/'//;
  65.                                         $sth = $db->prepare("INSERT INTO wow_eu_partidas_$bracket\_$season VALUES ('$entradas->{ranking}', '$entradas->{rating}', '$entradas->{name}', '$entradas->{realmId}', '$entradas->{realmSlug}', '$entradas->{raceId}', '$entradas->{classId}', '$entradas->{specId}', '$entradas->{factionId}', '$entradas->{genderId}', '$rating_change', '$ranking_change', CURRENT_TIMESTAMP)");
  66.                                         $sth->execute() or die "imposible inserta en la tabla";
  67.                                         $sth->finish;
  68.                                 }
  69.                         }
  70.                         else {
  71.                                 $entradas->{'realmName'} =~ s/'//;
  72.                                 $sth = $db->prepare("INSERT INTO wow_eu_partidas_$bracket\_$season VALUES ('$entradas->{ranking}', '$entradas->{rating}', '$entradas->{name}', '$entradas->{realmId}', '$entradas->{realmSlug}', '$entradas->{raceId}', '$entradas->{classId}', '$entradas->{specId}', '$entradas->{factionId}', '$entradas->{genderId}', '0', '0', CURRENT_TIMESTAMP)");
  73.                                 $sth->execute() or die "imposible inserta en la tabla";
  74.                                 $sth->finish;
  75.                         }
  76.                 }
  77.  
  78.                 $db->disconnect;
  79.                 print "Succes updated eu $bracket\n";
  80.         }
  81. }
  82.  
  83. run();
  84.  
Coloreado en 0.006 segundos, usando GeSHi 1.0.8.4

Gracias.

Re: Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-24 14:38 @651
por explorer
La carga tan grande es porque le has puesto que se ejecute una vez por segundo (opción sleep=>1). ¿Realmente necesitas tanto nivel de detalle?

El que la API no te responda a veces puede ser debido por ese volumen tan grande de peticiones (3 peticiones de partidas por segundo, son 10800 peticiones por hora). Normalmente, las API tienen un límite de peticiones por día o por hora.

En cuanto a los caracteres mal formados... deberías mirar en la posición que te está indicando JSON, para ver qué pasa, qué tipo de caracteres estás recibiendo. Si ves que llegan caracteres no utf8 aunque la cabecera del json diga que está en utf8, lo que puedes hacer es quitar la llamada al método utf8() de JSON, y luego lo decodificas a mano con el módulo Encode (ejemplo sacado de la sección utf8 del módulo JSON):
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. use JSON;
  2. use Encode;
  3.  
  4. my $objeto = JSON->new->decode( decode_utf8($json, 0) );
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

La clave está en el valor '0' de decode_utf8, que indica que en caso de mala codificación utf8, Encode sacará un mensaje de advertencia, sustituirá el carácter malo con el "carácter de sustitución" genérico, y continuará con el resto. (Más información en perldoc Encode).

Y si necesitas velocidad, cuando hayas comprobado que el módulo JSON funciona bien, sustituye el módulo JSON por JSON::XS.

Re: Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-24 17:45 @781
por Superdri
Gracias por la respuesta, explorer :)

Estoy probando ahora lo del decode() y lleva bastante rato sin salir el error de carácter mal formado. Si veo que sigue así probaré con JSON::XS para más velocidad.

Estoy haciendo un SELECT a la base de datos por entrada, y son muchas. Poner un índice en la tabla podría ser otra solución para que vaya más rápido si no me equivoco.

Sobre las peticiones, no estoy haciendo 3 por segundo, lo que hace es ejecutar cada función que carga las partidas de forma lineal y cada función suele tardar 1 minuto (son 5000 entradas aproximadamente), cuando acaba las 3 funciones es cuando entra ese sleep de 1 segundo. Además ahora implementé una rutina para que en caso de no-éxitos vuelva a cargar la misma en vez de pasar a la siguiente:
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. sub worker {
  2.         while (partidas('2v2') == 0) {
  3.                 sleep 3;
  4.         }
  5.         while (partidas('3v3') == 0) {
  6.                 sleep 3;
  7.         }
  8.         while (partidas('rbg') == 0) {
  9.                 sleep 3;
  10.         }
  11. }
  12.  
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

La función partidas devuelve 1 cuando tiene éxito.

Muchas gracias :)

Re: Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-25 07:54 @371
por explorer
¿Un minuto para 5000 entradas? Entonces el cuello de botella es el suministrador, desde luego. Quizás lo tengan limitado porque habrá mucha gente haciendo lo mismo. Llevo escuchando esto de las posiciones de los jugadores del WoW... desde hace un montón de años.

De todas maneras, comprueba los límites que te da el servidor, por si llegas al límite de peticiones por hora/día.

El código que yo usaría (no probado):
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. sub worker {
  2.         for my $cat (qw( 2v2 3v3 rbg )) {       # para todas las categorías
  3.                 do {
  4.                         sleep 1 + int rand 5;   # esperamos entre 1 y 5 segundos (aleatorio)
  5.  
  6.                 } while ( ! partidas($cat) );   # repetimos mientras no haya partidas de $cat...
  7.         }
  8. }
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4
De esta manera, nos aseguramos que siempre haya esperas entre peticiones, y así no "asustamos" al servidor.

Re: Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-25 08:37 @400
por Superdri
Hola.

He conseguido reducir el tiempo bastante. Ahora me tarda sobre 40 segundos por petición. Lo que más se notó fue meter el índice en mi tabla de MySQL; y la forma de descodificar que me pasaste creo que es más rápida, también. Ahora el consumo de CPU de la nube ya no pasa del 20 %.

Lo malo: que acabo de volver, he mirado el servidor y el demonio se ha parado por un carácter mal formado al descodificar con JSON :(

Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. my $objeto = JSON->new->decode( decode_utf8($json, 0) );
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

El decode_utf8 funciona bien y nunca falla, pero el decode() del módulo JSON es el que está dando el error. Creo que el módulo JSON no tiene ninguna opción para que no pare el programa si encuentra un fallo.

Hasta luego.

Re: Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-25 14:06 @629
por explorer
Lo que sí puedes hacer es usar eval{}, que funciona parecido al try{}catch() de otros lenguajes. Algo así (no probado):
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
  1. my $objeto = eval {
  2.         JSON->new->decode( decode_utf8($json, 0) );
  3. };
  4. if ($@) {
  5.         say "Hubo errores al sacar el json: $@";
  6. }
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

Más información en perldoc -f eval.

Re: Optimizar peticiones HTTPS, decodificar JSON y MySQL

NotaPublicado: 2017-10-25 21:29 @936
por Superdri
Hola de nuevo :)

Ya funciona todo correcto. Cambié el LWP::UserAgent por IO::All e hice una pequeña función que comprueba el código HTTP que devuelve la API para casos de no-éxito y no ha dado un error en muchas horas funcionando el decode de JSON::XS.

¡Hasta otra!