MaxCompute facture en fonction du volume de données analysées et des ressources de calcul consommées. Deux situations expliquent la majorité des pics de facturation inattendus : les tâches SQL qui analysent plus de données que nécessaire et les tâches MapReduce qui allouent davantage de ressources que la charge de travail ne l'exige. Cette rubrique explique comment diagnostiquer et résoudre ces deux problèmes.
Estimer les coûts avant d'exécuter les tâches
Utilisez les outils TCO pour estimer les coûts de calcul avant de soumettre les tâches. Pour les tâches SQL, utilisez CostSQL afin de prévisualiser le coût d'une requête avant son exécution. Configurez des alertes de consommation de ressources pour détecter rapidement toute augmentation inattendue des coûts.
Réduire les coûts de calcul SQL
Éviter les analyses complètes de table
Les analyses complètes de table constituent la principale cause de coûts de calcul SQL élevés. MaxCompute facture en fonction du volume de données analysées : analyser une table entière alors que seul un sous-ensemble est nécessaire multiplie votre facture.
Désactivez les analyses complètes de table au niveau de la session ou du projet :
-- Disable for the current session
set odps.sql.allow.fullscan=false;
-- Disable for the entire project
SetProject odps.sql.allow.fullscan=false;
**Élaguez les colonnes — évitez l'instruction SELECT * :**
L'instruction SELECT * déclenche systématiquement une analyse complète de la table. Sélectionnez uniquement les colonnes dont vous avez besoin :
-- Table T has columns a, b, c, d, e.
-- This query reads only a, b, and e — skipping c and d entirely.
SELECT a, b FROM T WHERE e < 10;
Élaguez les partitions — filtrez sur les colonnes clés de partition :
En spécifiant une clé de partition dans la clause WHERE, vous indiquez à MaxCompute d'ignorer les partitions non pertinentes et d'analyser uniquement les données correspondantes :
SELECT a, b FROM T WHERE partitiondate = '2017-10-01';
Sans filtre de partition, une opération JOIN ou SELECT sur une table partitionnée revient à effectuer une analyse complète de la table. Élaguez toujours les partitions avant d'effectuer des jointures. Pour les cas où l'élagage des partitions ne s'applique pas, consultez la section Scénarios où l'élagage des partitions ne s'applique pas.
Réécrire les modèles SQL coûteux
Certains mots-clés SQL déclenchent des opérations de brassage et de tri supplémentaires, ce qui consomme des ressources de calcul. La cause profonde réside dans les mouvements de données : lorsque le moteur doit réorganiser les données entre les nœuds pour satisfaire une requête, il consomme du processeur et des E/S proportionnellement au volume de données déplacé. La réécriture de ces modèles réduit ou élimine ces mouvements.
Remplacer FULL OUTER JOIN par UNION ALL
FULL OUTER JOIN oblige le moteur à faire correspondre chaque ligne des deux tables, générant un important brassage intermédiaire. UNION ALL élimine totalement la jointure en complétant les valeurs manquantes par des zéros :
-- Original: FULL OUTER JOIN
SELECT COALESCE(t1.id, t2.id) AS id, SUM(t1.col1) AS col1, SUM(t2.col2) AS col2
FROM (
SELECT id, col1 FROM table1
) t1
FULL OUTER JOIN (
SELECT id, col2 FROM table2
) t2
ON t1.id = t2.id
GROUP BY COALESCE(t1.id, t2.id);
-- Optimized: UNION ALL (no join, no data movement overhead)
SELECT t.id, SUM(t.col1) AS col1, SUM(t.col2) AS col2
FROM (
SELECT id, col1, 0 AS col2 FROM table1
UNION ALL
SELECT id, 0 AS col1, col2 FROM table2
) t
GROUP BY t.id;
Déplacer GROUP BY en dehors de UNION ALL
Placer GROUP BY dans chaque branche d'un UNION ALL conduit le moteur à effectuer deux agrégations. Effectuez l'agrégation une seule fois après l'union :
-- Original: GROUP BY inside each branch (double aggregation)
SELECT t.id, SUM(t.val) AS val
FROM (
SELECT id, SUM(col3) AS val FROM table3 GROUP BY id
UNION ALL
SELECT id, SUM(col4) AS val FROM table4 GROUP BY id
) t
GROUP BY t.id;
-- Optimized: single aggregation after union
SELECT t.id, SUM(t.val) AS val
FROM (
SELECT id, col3 AS val FROM table3
UNION ALL
SELECT id, col4 AS val FROM table4
) t
GROUP BY t.id;
Remplacer DISTINCT par GROUP BY
L'utilisation de DISTINCT sur de grands ensembles de données nécessite un tri complet. L'instruction GROUP BY permet d'obtenir le même résultat avec moins de surcharge :
-- Original: DISTINCT (full sort)
SELECT COUNT(DISTINCT id) AS cnt FROM table1;
-- Optimized: GROUP BY (more efficient deduplication)
SELECT COUNT(1) AS cnt
FROM (
SELECT id FROM table1 GROUP BY id
) t;
Autres modèles à éviter :
Utilisez l'instruction
INSERT INTOavec un champ de partition plutôt que d'insérer sans partitionnement. Cela réduit la complexité SQL et diminue les coûts.Triez les données exportées temporairement à l'aide d'outils externes tels qu'Excel au lieu d'utiliser ORDER BY. L'instruction ORDER BY dans MaxCompute déclenche un tri global sur l'ensemble du jeu de données.
Contrôler la fréquence de planification
MaxCompute est conçu pour le traitement par lots de grande ampleur, et non pour les requêtes en temps réel. Planifier des tâches SQL à intervalles courts (toutes les quelques secondes ou minutes) accumule des files d'attente de tâches soumises à une facturation au paiement à l'utilisation, ce qui peut entraîner un pic inattendu sur la facture du lendemain.
Exécutez CostSQL pour estimer le coût des tâches planifiées fréquemment avant de définir leur cadence. Pour les charges de travail nécessitant des résultats quasi instantanés, privilégiez un service de calcul en temps réel dédié plutôt que MaxCompute.
Prévisualiser les données de table sans exécuter de code SQL
L'exécution de l'instruction SELECT * FROM table LIMIT 10 consomme des ressources de calcul. Utilisez plutôt la fonctionnalité intégrée de prévisualisation de table : elle lit les données directement depuis le stockage sans déclencher de tâche de calcul.
DataWorks : Ouvrez la page Data Map, recherchez la table et utilisez l'onglet de prévisualisation. Consultez la section Afficher les détails d'une table.
MaxCompute Studio : Double-cliquez sur une table dans l'arborescence des objets pour prévisualiser ses données.
Adapter l'outil à la charge de travail
MaxCompute renvoie les résultats en quelques minutes, et non en millisecondes. C'est l'outil approprié pour l'analytique par lots de grande ampleur, mais il convient mal aux requêtes frontales qui exigent des réponses instantanées.
Pour les requêtes frontales (tableaux de bord, résultats de recherche, recherches de lignes), utilisez une base de données relationnelle telle qu'ApsaraDB RDS. Stockez-y les résultats agrégés de MaxCompute et servez les requêtes depuis la base de données. Les requêtes frontales dépourvues de clause WHERE, n'effectuant aucune agrégation et ne joignant aucun dictionnaire seront systématiquement lentes dans MaxCompute.
Réduire les coûts de calcul MapReduce
Configurer la taille des splits et le nombre de reducers
Deux paramètres de configuration ont l'impact le plus important sur la consommation de ressources MapReduce.
La taille des splits contrôle le nombre de mappers créés. La valeur par défaut est de 256 Mo par split. Une taille de split plus petite crée davantage de mappers, augmentant ainsi le parallélisme mais aussi l'utilisation des ressources. Utilisez JobConf#setSplitSize pour ajuster cette valeur en fonction du coût de calcul de votre logique map : si chaque enregistrement est coûteux à traiter, réduisez la taille du split pour créer plus de mappers et répartir la charge.
Le nombre de reducers est par défaut égal au quart du nombre de mappers et peut être défini sur n'importe quelle valeur comprise entre 0 et 2 000. Un nombre plus élevé de reducers consomme davantage de ressources. Définissez uniquement le nombre de reducers requis par votre charge de travail d'agrégation et configurez explicitement le compte à l'aide de jobconf.setNumReduceTasks(num).
Fusionner les tâches sérielles à l'aide du mode pipeline
Lorsque plusieurs tâches MapReduce sont chaînées (la sortie d'une tâche alimentant la suivante), chaque tâche intermédiaire écrit les résultats sur disque puis les relit. Ces E/S disque s'accumulent tout au long de la chaîne.
Le mode pipeline fusionne les tâches MapReduce sérielles en une seule tâche, éliminant ainsi les lectures et écritures intermédiaires sur disque. Cela réduit à la fois les coûts et la surcharge de planification. Pour des exemples d'implémentation, consultez la section Exemples de pipeline.
Élaguer les colonnes dans les tables d'entrée
Lorsqu'un mapper lit une table d'entrée mais n'a besoin que de quelques-unes de ses colonnes, lire la ligne entière gaspille des E/S. Spécifiez les colonnes requises lors de l'ajout d'une table d'entrée :
InputUtils.addTable(TableInfo.builder().tableName("wc_in").cols(new String[]{"c1","c2"}).build(), job);
Après cette configuration, le mapper lit uniquement les colonnes c1 et c2. Les données accessibles par nom de colonne ne sont pas affectées ; les données accessibles par index peuvent se comporter différemment.
Lire les ressources lors de l'étape setup
Chaque appel de lecture d'une ressource engendre une surcharge, et vous pouvez lire des ressources jusqu'à 64 fois. La lecture de la même ressource au sein d'une fonction map ou reduce entraîne sa relecture pour chaque enregistrement. Lisez plutôt les ressources une seule fois lors de l'étape setup. Pour des exemples d'utilisation, consultez la section Exemple d'utilisation des ressources.
Éviter de construire des objets dans la fonction map ou reduce
Les objets Java construits à l'intérieur d'une fonction map ou reduce sont reconstruits à chaque invocation d'enregistrement. Déplacez la construction des objets vers l'étape setup :
Record word;
Record one;
public void setup(TaskContext context) throws IOException {
// Construct once in setup — not on every map call
word = context.createMapOutputKeyRecord();
one = context.createMapOutputValueRecord();
one.set(new Object[]{1L});
}
Utiliser un combiner lorsque la sortie map contient des clés dupliquées
Un combiner pré-agrège la sortie d'une tâche map avant qu'elle ne soit envoyée aux reducers, réduisant ainsi le volume de données transférées sur le réseau pendant la phase de brassage.
N'utilisez un combiner que lorsque la sortie map contient plusieurs enregistrements avec la même clé, par exemple dans une tâche de comptage de mots. Si les clés de sortie map sont uniques, un combiner ajoute une surcharge sans apport bénéfique.
Le combiner suivant additionne les valeurs pour les clés correspondantes :
/**
* A combiner class that combines map output by summing values.
*/
public static class SumCombiner extends ReducerBase {
private Record count;
@Override
public void setup(TaskContext context) throws IOException {
count = context.createMapOutputValueRecord();
}
@Override
public void reduce(Record key, Iterator<Record> values, TaskContext context)
throws IOException {
long c = 0;
while (values.hasNext()) {
Record val = values.next();
c += (Long) val.get(0);
}
count.set(0, c);
context.write(key, count);
}
}
Prévenir le skew des données avec des colonnes de partition ou un partitionneur personnalisé
Par défaut, les reducers reçoivent les données en fonction du hachage du schéma de clé complet. Si certaines valeurs de clé sont beaucoup plus fréquentes que d'autres, certains reducers reçoivent significativement plus de données. Il s'agit d'un problème de longue traîne où certains reducers terminent bien après les autres, bloquant l'ensemble de la tâche.
Pour distribuer les données plus uniformément entre les reducers, spécifiez les colonnes clés de partition à l'aide de JobConf#setPartitionColumns. Les données sont routées vers les reducers en fonction du hachage de ces colonnes plutôt que de la clé complète :
jobconf.setPartitionerClass(MyPartitioner.class)
jobconf.setNumReduceTasks(num)
Pour un contrôle plus fin, implémentez un partitionneur personnalisé :
import com.aliyun.odps.mapred.Partitioner;
public static class MyPartitioner extends Partitioner {
@Override
public int getPartition(Record key, Record value, int numPartitions) {
// numPartitions is the total number of reducers.
// Route each key to a reducer based on key length.
String k = key.get(0).toString();
return k.length() % numPartitions;
}
}
Maintenir la mémoire JVM dans un ratio CPU/mémoire de 1:4
La configuration standard prévoit 1 cœur de processeur et 4 Go de mémoire, avec odps.stage.reducer.jvm.mem réglé sur 4006. Allouer de la mémoire au-delà d'un ratio de 1:4 par rapport aux cœurs de processeur augmente la facturation. Ajustez odps.stage.reducer.jvm.mem pour correspondre à votre charge de travail réelle plutôt que de surprovisionner.
Étapes suivantes
Pour optimiser les coûts de stockage, consultez la section Optimiser les coûts de stockage.
Pour réduire les coûts de chargement et de téléchargement des données, consultez la section Optimiser les coûts des chargements et téléchargements de données.
Pour analyser et résoudre les anomalies de facturation, consultez la section Gérer les coûts.
Pour générer un plan d'optimisation des ressources, consultez la section Générer un plan d'optimisation des ressources de calcul.