J'ai couru dans une intéressante limite avec l'App Moteur de base de données. Je suis entrain de créer un handler pour nous aider à analyser des données d'utilisation sur un de nos serveurs de production. Pour effectuer l'analyse j'ai besoin d'interroger et de résumer de+ de 10 000 entités extraites de la banque de données. Le calcul n'est pas difficile, il est juste un histogramme des éléments qui passent par un filtre spécifique de l'utilisation des échantillons. Le problème que je rencontre c'est que je ne peux pas récupérer les données de la banque de données assez rapide pour faire le traitement avant de frapper la requête date limite.
J'ai essayé tout ce que je pense de segmenter la requête en parallèle des appels RPC pour améliorer les performances, mais selon appstats je n'arrive pas à obtenir les requêtes à l'exécution en parallèle. Peu importe la méthode que j'essaie (voir ci-dessous), il semble toujours que le cpr a fait retomber en cascade séquentiel suivant les requêtes.
Remarque: la requête et d'analyse de code fonctionne, il fonctionne lentement parce que je ne peux pas obtenir rapidement des données de la banque de données.
Arrière-plan
Je n'ai pas une version live, je peux partager, mais ici, c'est le modèle de base pour la partie du système dont je parle:
class Session(ndb.Model):
""" A tracked user session. (customer account (company), version, OS, etc) """
data = ndb.JsonProperty(required = False, indexed = False)
class Sample(ndb.Model):
name = ndb.StringProperty (required = True, indexed = True)
session = ndb.KeyProperty (required = True, kind = Session)
timestamp = ndb.DateTimeProperty(required = True, indexed = True)
tags = ndb.StringProperty (repeated = True, indexed = True)
Vous pouvez penser à des échantillons comme des moments où un utilisateur fait usage d'une capacité d'un nom donné. (ex: "systemA.feature_x'). Les balises sont fondées sur des informations du client, système d'information et la fonctionnalité. ex: ['winxp', '2.5.1', 'systemA', 'feature_x', 'premium_account']). Donc les balises forme d'un dénormalisée ensemble de jetons, qui pourraient être utilisés pour trouver des échantillons d'intérêt.
L'analyse que je suis en train de faire consiste à prendre une plage de dates et de demander combien de fois a été une caractéristique de l'ensemble des fonctionnalités (peut-être tous éléments) utilisé par jour (ou par heure) par compte client (entreprise, et non par utilisateur).
Donc l'entrée au gestionnaire être quelque chose comme:
- Date De Début De
- Date De Fin
- Tag(s)
La sortie serait:
[{
'company_account': <string>,
'counts': [
{'timeperiod': <iso8601 date>, 'count': <int>}, ...
]
}, ...
]
Code commun pour les Requêtes
Voici un peu de code en commun pour toutes les requêtes. La structure générale du gestionnaire est un simple gestionnaire d'obtenir l'aide de webapp2 qui définit les paramètres de la requête, exécute la requête, les processus, les résultats, crée des données de retour.
# -- Build Query Object --- #
query_opts = {}
query_opts['batch_size'] = 500 # Bring in large groups of entities
q = Sample.query()
q = q.order(Sample.timestamp)
# Tags
tag_args = [(Sample.tags == t) for t in tags]
q = q.filter(ndb.query.AND(*tag_args))
def handle_sample(sample):
session_obj = sample.session.get() # Usually found in local or memcache thanks to ndb
count_key = session_obj.data['customer']
addCountForPeriod(count_key, sample.timestamp)
Méthodes Essayé
J'ai essayé une variété de méthodes pour tenter d'extraire des données de la banque de données aussi rapidement que possible et en parallèle. Les méthodes que j'ai essayé jusqu'à maintenant:
A. Seule Itération
Il s'agit plus d'une simple base de cas pour comparer par rapport aux autres méthodes. Je viens de créer la requête et itération sur tous les éléments de laisser ndb faire ce qu'il fait pour tirer d'eux l'un après l'autre.
q = q.filter(Sample.timestamp >= start_time)
q = q.filter(Sample.timestamp <= end_time)
q_iter = q.iter(**query_opts)
for sample in q_iter:
handle_sample(sample)
B. Les Grandes Récupérer
L'idée ici était de voir si je pouvais faire un très grand effort.
q = q.filter(Sample.timestamp >= start_time)
q = q.filter(Sample.timestamp <= end_time)
samples = q.fetch(20000, **query_opts)
for sample in samples:
handle_sample(sample)
C. Async extrait dans le temps de la gamme
L'idée ici est de reconnaître que les échantillons sont assez espacés dans le temps afin que je puisse créer un ensemble indépendant de requêtes qui a divisé le temps global de la région en morceaux et essayez d'exécuter chacune de ces en parallèle en utilisant async:
# split up timestamp space into 20 equal parts and async query each of them
ts_delta = (end_time - start_time) / 20
cur_start_time = start_time
q_futures = []
for x in range(ts_intervals):
cur_end_time = (cur_start_time + ts_delta)
if x == (ts_intervals-1): # Last one has to cover full range
cur_end_time = end_time
f = q.filter(Sample.timestamp >= cur_start_time,
Sample.timestamp < cur_end_time).fetch_async(limit=None, **query_opts)
q_futures.append(f)
cur_start_time = cur_end_time
# Now loop through and collect results
for f in q_futures:
samples = f.get_result()
for sample in samples:
handle_sample(sample)
D. Async cartographie
J'ai essayé cette méthode, car la documentation fait sonner comme ndb peut exploiter certaines parallélisme automatiquement lors de l'utilisation de la Requête.map_async méthode.
q = q.filter(Sample.timestamp >= start_time)
q = q.filter(Sample.timestamp <= end_time)
@ndb.tasklet
def process_sample(sample):
period_ts = getPeriodTimestamp(sample.timestamp)
session_obj = yield sample.session.get_async() # Lookup the session object from cache
count_key = session_obj.data['customer']
addCountForPeriod(count_key, sample.timestamp)
raise ndb.Return(None)
q_future = q.map_async(process_sample, **query_opts)
res = q_future.get_result()
Résultat
J'ai testé un exemple de requête pour recueillir des temps de réponse global et appstats traces. Les résultats sont les suivants:
A. Seule Itération
real: 15.645 s
Celui-ci passe successivement à travers de l'extraction de lots, l'un après l'autre, puis récupère chaque session de memcache.
B. Les Grandes Récupérer
real: 12.12 s
Effectivement la même chose que l'option A, mais un peu plus rapide pour une raison quelconque.
C. Async extrait dans le temps de la gamme
real: 15.251 s
Il semble apporter plus de parallélisme au début, mais semble avoir ralenti par une série d'appels à la prochaine au cours d'une itération des résultats. Aussi ne semble pas être en mesure de se chevaucher la session memcache recherches avec les requêtes en cours.
D. Async cartographie
real: 13.752 s
Celui-ci est le plus difficile pour moi de comprendre. On dirait qu'il a q bonne affaire de chevauchement, mais tout semble s'étirer dans une chute d'eau au lieu d'en parallèle.
Recommandations
Basé sur tout cela, ce qui me manque? Suis-je tout simplement de frapper une limite sur App Engine ou est-il une meilleure façon de tirer vers le bas grand nombre d'entités en parallèle?
Je suis à une perte quant à ce qu'à essayer la prochaine. J'ai pensé à la réécriture du client, de faire de multiples demandes d'app engine en parallèle, mais cela semble assez de force brute. Je voudrais vraiment s'attendre à ce que l'application du moteur doit être capable de gérer ce cas d'utilisation, donc je suppose que il y a quelque chose que je suis absent.
Mise à jour
En fin de compte j'ai trouvé que l'option C était le mieux pour mon cas. J'ai pu l'optimiser pour terminer en 6,1 secondes. Pas encore parfait, mais beaucoup mieux.
Après l'obtention de l'avis de plusieurs personnes, j'ai trouvé que les éléments suivants ont été la clé pour comprendre et garder à l'esprit:
- Plusieurs requêtes peuvent s'exécuter en parallèle
- Seulement 10 RPC peut être dans le vol à la fois
- Essayez d'éliminer au point qu'il n'y a pas secondaire requêtes
- Ce type de tâche est mieux à gauche de la carte de réduire et de files d'attente de tâches, pas les requêtes en temps réel
Donc, ce que j'ai fait pour le rendre plus rapide:
- J'ai partitionné la requête de l'espace à partir du début basé sur le temps. (remarque: plus d'égalité les partitions sont en termes d'entités retournées, le mieux)
- Je dénormalisée les données à supprimer la nécessité pour la session secondaire requête
- J'ai fait usage de ndb opérations asynchrones et wait_any() pour chevaucher les requêtes avec le traitement
Je suis toujours pas à obtenir la performance, je attendre ou comme, mais il est réalisable pour l'instant. Je souhaite juste leur a une meilleure façon de tirer un grand nombre de séquentielles des entités en mémoire rapidement dans les gestionnaires.