Des actualités personnelles sous un style impersonnel, et inversement.
Follow @Thomas_Jannaud
J'ai récemment décidé de faire tourner mon site web sur AppEngine, ce qui a été l'occasion d'un redesign complet, tant au niveau interface que l'architecture. Je tenais à écrire sur Appengine pour faire les présentations si ce n’était déjà fait, pour donner un maximum d’information afin que vous puissiez faire un choix de migration ou de premier site, et finalement pour décrire des solutions à des problèmes techniques que vous pourriez rencontrer lors du bulkupload.
AppEngine est un service de Google. Ils hébergent gratuitement votre site tant qu'il reste "petit" (jusqu'à 1 million de visites par mois grossomodo), ensuite on paye en fonction de l'utilisation des ressources. Si votre site devient très populaire du jour au lendemain vous ne devriez pas avoir de problèmes car l’infrastructure de google est derrière. Il faut toutefois toujours payer pour avoir son propre nom de domaine (ex: "bleezworld.com"), mais ce n’est pas obligatoire.
J'avais à la base un site hébergé chez 1&1.net. J'avais acheté le "pack promotionnel" en 2010, à 10€ la première année environ, sans vraiment connaitre le coût pour les autres années. Ensuite je recevais une facture de 20€ de temps en temps. Cela semblait peu, erreur. Décidant de faire le compte un jour, j'ai vu que j'en avais pour 60€ par an. Belle stratégie marketing.
Connectez-vous sur AppEngine avec votre compte gmail, créez une application et vous avez aurez un site avec pour nom lenomchoisi.appspot.com. Si vous avez un nom de domaine (comme bleezworld.com) vous pouvez le faire pointer sur cette adresse appspot.com. Ainsi vous ne payez que 6€ par an à 1&1.net pour le nom de domaine et vous êtes hébergés gratuitement chez Google.
J’ai eu assez peur de ce que je lisais sur AppEngine, essentiellement au sujet du coût et des problèmes d’upload de données. Et c’est vrai, ils existent, mais tout dépend de vos besoins.
Datastore est gratuit jusqu’à 50 000 lectures/écritures par jour, au delà c’est 1$ pour 1 million, mais le quota est très vite atteint d'autant que ce n’est pas très simple d’estimer le nombre d’opérations qu’il faudra. 10Mo de données au format csv est un bon ordre de grandeur de la limite gratuite journalière (upload/download/suppression/...)
Si MySQL est vraiment une nécessité, que vous devez manipuler fréquemment la base de données, ou si elle est conséquente (> 50 Mo) et doit souvent être réinitialisée ou téléchargée, alors AppEngine n’est sûrement pas pour vous.
Si par contre vous avez une grosse base de données qui n’est jamais modifiée (exemple : un dictionnaire), alors Appengine n’est pas contre indiqué. Pour ma base de données cela m’aura coûté 10$ environ pour l’uploader. C’est cher mais maintenant je ne vais plus y toucher et j’économise 50 euros par an sur l’hébergement. Si votre base de données est plus grosse, il faut faire le calcul. Il faut aussi se demander si vous ne pouvez pas avoir une base de données SQLite, mais je n’y ai pensé qu’après. Comme vous n’avez qu’un accès en lecture seule aux fichiers, vous ne pourrez rien modifier dans SQLite.
Je me suis d’abord lancé sur un petit site pour mettre en avant une iphone app pour apprendre le japonais (JapanEasy) "pour voir" et apprivoiser AppEngine. C’est un petit site statique, sans base de données, avec seulement 3 pages, où il n’y a pas possibilité de mettre des commentaires, … parfait pour débuter et ébaucher son infrastructure python/jinja.
Second site, celui avec le dictionnaire japonais. Encore un modèle assez statique avec en plus des requêtes au datastore. C’est l’occasion de voir que le datastore d’AppEngine est utilisable simplement au travers des API fournies. C’est un modèle de base de données clé -> valeur avec des requêtes simples (SELECT * WHERE ... ORDER BY ...).
Une fois le site fait il faut uploader toutes les données (le dictionnaire) ce qui n’est pas si simple et c’est l’objet de la prochaine section.
Voici l’interface d’administration : on peut ajouter, modifier ou supprimer des données une à une mais pour faire des requêtes ou modifier beaucoup de données d’un coup, ce n’est possible ni de l’interface ni depuis le code. Rappelez vous, AppEngine a été conçu avec l’idée que votre site web contiendra des teraoctets de données, même si en somme tout le monde l’utilise sûrement en mode "site php mysql gratuit. Il faudra écrire un programme complet pour modifier beaucoup de données à la fois (MapReduce) mais je n’ai encore pas eu à m’en servir.
Après avoir passé des heures à trimer sur internet pour voir comment uploader ses données (csv vers AppEngine par exemple) tout en effectuant quelques manipulations parfois complexes (définir la clé que l’entité aura sur AppEngine en fonction des données, ...), je me suis dit qu’il était juste de rendre la pareille.
On l’a dit, le coût et l’inconvénient principal d'AppEngine réside dans le transfert de données. Sur 1&1 comme chez d'autres fournisseurs, on peut uploader/downloader une base de données de 100Mo MySQL en quelques minutes, gratuitement. Ça va vite, on peut faire toutes les requêtes que l'on veut dessus, ...
Sur AppEngine c'est lent, c'est compliqué et ça coûte cher. Donc autant ne pas vous tromper !
Voici quelques statistiques d'upload de la base de données du dictionnaire français japonais dont je parlais : plusieurs millions de lignes, au moins 100Mo...
Il aura fallu 60 000 secondes = 1000 minutes = 15 heures pour transférer 600 Mo, quand il ne me fallait que quelques minutes sur un site basé sur MySQL !
D'autre part tout cela a un coût. Dès que l'on dépasse 50 000 opérations "écriture" on paye, au prix de 1$ les 1 millions d'opérations. Difficile d'estimer précisément le nombre d'opérations nécessaire pour une base de données donnée mais je m'en suis tiré pour 12$ avec ça.
Il faut à cela ajouter le fait que l'on paye si l’on a plus de 1Go de données, et on paye chaque semaine. Comme chez les commerçants où il est interdit de payer par carte bleue en deça de 15€, Google nous interdit d'en avoir moins de 2.5$ par semaine (si payant). Donc si on en a pour 5 centimes... on en a pour 2.5$.
Bref, interdit de se tromper avec ses données ! Il vous en coûtera le prix fort.
L’arnaque c’est que la base de données que j’avais faisais 200Mo. Et une fois sur AppEngine il y en avait pour 1.5 Go !!! Voici les 2 choses à savoir pour ne pas exploser votre quota.
Il faut faire très attention au moment de définir ses entités et mettre indexed=False sur les champs que l'on ne souhaite pas indexer, c’est à dire ceux sur lesquels on ne mettra pas de condition WHERE …
Les champs indexés occupent environ 6 fois plus de mémoire que les champs non indexés, et par défaut, tous les champs sont indexés. Si vous avez un fichier index.yaml, j’imagine que c’est la même chose.
La seconde chose à savoir c’est que ce n’est pas dit que l’outil de bulkload fourni par AppEngine utilise bien le modèle d’entités que l’on a défini. J’ai mis indexed=False dans beaucoup de mes champs et je me retrouve à avoir des données d’index 10 fois plus grosses que mes données, ce n’est pas normal. Il faut utiliser la deuxième technique d’upload dont je vais parler pour être sûr.
AppEngine recalcule les stats/quota utilisés de votre app tous les jours. Si vous uploadez (3h), attendez de voir si tout a fonctionné, voyez qu’il y a eu un problème, supprimez, reuploadez, ça vous prendra donc 2 jours. C’est la plus grosse (et la seule jusqu’à présent dans mon expérience) pénibilité à travailler avec AppEngine. Encore une fois, vous n’êtes concernés que si vous avez beaucoup de données, pas juste 1 millier de commentaires.
Le fichier bulkload.yaml de ce site pour transférer les commentaires MySQL de l’ancien site vers AppEngine. Il faut d'abord exporter en csv (d’autres formats comme le xml sont aussi supportés).
article,prenom,date,message
my-iphone-apps,Tom,2013-03-14 20:37:41,"Super applications j’adore"
danemark,Julien,2012-08-07 16:08:02,"Beau voyage !"
…
Le fichier page_handler.py, où est défini le modèle des commentaires. Notez que j'importe ce fichier dans bulkloader.yaml
class Comment(db.Model):
url = db.StringProperty(indexed=True)
date = db.DateTimeProperty(indexed=True)
auteur = db.StringProperty(indexed=True)
message = db.TextProperty(indexed=False)
Dans app.yaml, ajouter :
builtins:
- remote_api: on
python_preamble:
- import: page_handler
- import: google.appengine.api.datastore
- import: google.appengine.ext.bulkload.transform
- import: google.appengine.ext.db
transformers:
- kind: Comment
connector: csv
connector_options:
encoding: utf-8
columns: from_header
property_map:
- property: url
external_name: article
- property: auteur
external_name: prenom
- property: date
external_name: date
import_transform: transform.import_date_time('%Y-%m-%d %H:%M:%S')
- property: message
external_name: message
import_transform: db.Text
Les noms des colonnes du fichier csv correspondent aux champs de l’entité, et on définit le mapping colonne <-> champ de l’entité dans le "property_map" du fichier bulkloader.yaml.
Si votre site est dans le dossier /.../...monsite, voici les commandes (bash) à exécuter pour uploader les données en local :
cd /.../.../monsite; PYTHONPATH=. && echo "" | appcfg.py upload_data --filename=/path/to/csvfile.csv --kind=Comment --url=http://localhost:8081/_ah/remote_api --application=dev~qqchose --log_file=/dev/null --num_threads=1 --config_file=bulkloader.yaml ./
et en "prod" :
cd /.../monsite; PYTHONPATH=. && appcfg.py upload_data --config_file=bulkloader.yaml --filename=/path/to/csvfile.csv --kind=Comment --url=http://qqchose.appspot.com/_ah/remote_api --application=s~qqchose --log_file=/dev/null --num_threads=1 ./
Il y a quelques petites différences. Pour "prod", on vous demandera votre email (celui de votre compte appengine) et votre mot de passe. On ne vous demande que l’email pour l’upload de données en local et ne rien mettre suffit (c’est ce à quoi echo "" |
sert, pour aller plus vite)
Parfois on souhaite transformer ses données de manière un peu compliquée ou bien on souhaite définir soit même les clés des entités (ex: pour le dictionnaire français-japonais). On ne peut pas tout configurer via bulkloader.yaml mais on peut écrire son propre script d’import.
Le fichier dictionnaire_handler.py contient l’entité WordInfo.
class WordInfo(db.Model):
mot = db.StringProperty(indexed=False)
ids = db.ListProperty(long, indexed=False)
definition = db.TextProperty(indexed=False)
Dans app.yaml, ajouter :
builtins:
- remote_api: on
Dans un fichier data_importer.py
import dictionnaire_handler
from google.appengine.api import datastore
from google.appengine.ext import db
from google.appengine.tools import bulkloader
_UNUSED = lambda x: None
def FilterEmptyList(t):
"""x.split(',') renvoie [''] si x == '', alors qu'on voudrait []."""
if len(t) == 1 and not t[0]:
return []
return t
class WordInfoLoader(bulkloader.Loader):
def __init__(self):
bulkloader.Loader.__init__(self, 'WordInfo', [
('__unused__', _UNUSED),
(‘mot’, _UNUSED),
('ids', _UNUSED),
(‘definition’, _UNUSED),
])
def create_entity(self, values, key_name=None, parent=None):
vs = [x.decode('utf-8') for x in values]
return dictionnaire_handler.WordInfo(
key_name=vs[1],
mot=vs[1],
ids=[int(y) for y in FilterEmptyList(vs[2].split(','))],
definition=db.Text(vs[3])
)
# Si vous avez d’autres loaders, mettez les dans ce tableau
loaders = [WordIndexLoader]
Si votre nom d’application est "qqchose", voici les commandes pour uploader toutes vos données en local :
cd /path/to/site; PYTHONPATH=./; appcfg.py upload_data --config_file=data_importer.py --filename=/path/to/fichier.csv --kind=WordInfo --url=http://localhost:8081/_ah/remote_api --application=dev~qqchose --log_file=/dev/null --num_threads=1 ./
et en "prod" :
cd /path/to/site; PYTHONPATH=./; appcfg.py upload_data --config_file=data_importer.py --filename=/path/to/fichier.csv --kind=WordInfo --application=s~qqchose --log_file=/dev/null --url=http://qqchose.appspot.com/_ah/remote_api ./
Si vous utilisez Python et Jinja (pas testé sur d’autres mais j’imagine que c’est la même chose), vous ne devriez jamais avoir d’erreur utf-8. Vous aurez des erreurs si vous faites des choses du genre welcome_string = "bonjour stéphane"
et que vous tentez d’afficher dans le template, à cause du ‘é’.
N’oubliez pas de mettre # -*- coding: utf-8 -*-
tout en haut de vos fichiers python, et quand vous devez utiliser des chaines utf-8, mettez un u devant : welcome_string = u"bonjour stéphane"
.
Il n’y a que lors de l’upload de données que l’on a besoin de spécifier précisément .decode("utf-8"), dans WordInfoLoader -> create_entity, sinon on n’en a jamais besoin, donc ne commencez pas à mettre des encode(‘utf-8’) et decode(‘utf-8’) partout !
Tout ce que j’ai écrit ici fonctionne chez moi, donc s’il y a des choses que j’ai oubliées de mentionner n’hésitez pas à laisser un commentaire !
Laissez un commentaire !
Pas besoin de vous connecter, commencez à taper votre nom et une case "invité" apparaîtra.