Tous les produits
Search
Centre de documentation

ApsaraDB for ClickHouse:Compatibility and performance bottleneck analysis and solutions for self-managed ClickHouse migration to ApsaraDB for ClickHouse

Dernière mise à jour :Aug 11, 2026

Après la migration des données d'un cluster ClickHouse autogéré vers le cloud, vous pouvez rencontrer des problèmes de compatibilité et de performance. Pour garantir une migration fluide, nous vous recommandons d'effectuer cette analyse dans un environnement de test avant de basculer le trafic de production.

Informations contextuelles

Les clusters ClickHouse autogérés migrés vers ApsaraDB for ClickHouse peuvent rencontrer les problèmes suivants :

  • Problèmes de compatibilité des versions :

    • Problèmes de compatibilité du moteur MaterializedMySQL

    • Problèmes de compatibilité SQL

  • Épuisement des ressources CPU et insuffisance de mémoire après la migration vers ApsaraDB for ClickHouse

Pour résoudre ces problèmes, concentrez-vous sur la compatibilité et les performances lors de la migration.

Ce guide couvre les points suivants :

  1. Compatibilité des paramètres — comparez les configurations entre les clusters et alignez-les

  2. Compatibilité de MaterializedMySQL — gérez les différences de moteur après la migration

  3. Vérification de la compatibilité SQL — testez par lots vos requêtes existantes sur le nouveau cluster

  4. Analyse des goulots d'étranglement de performance — identifiez les tables et les requêtes à l'origine des problèmes de CPU ou de mémoire

  5. Optimisation SQL — appliquez des correctifs ciblés après avoir identifié la cause racine

Analyse de compatibilité

Compatibilité des paramètres

Exécutez cette requête sur votre cluster autogéré et sur votre cluster ApsaraDB for ClickHouse afin d'exporter tous les paramètres de configuration :

SELECT
    name,
    groupArrayDistinct(value) AS value
FROM clusterAllReplicas(`default`, system.settings)
GROUP BY name
ORDER BY name ASC

Utilisez ensuite un outil de comparaison (tel que la fonction de comparaison de fichiers intégrée à VS Code) pour identifier les divergences. Pour tout paramètre différent, mettez à jour le cluster ApsaraDB for ClickHouse afin qu'il corresponde à la valeur du cluster autogéré.

Paramètres influant sur la compatibilité :

  • compatibility

  • prefer_global_in_and_join

  • distributed_product_mode

Paramètres influant sur les performances :

  • max_threads

  • max_bytes_to_merge_at_max_space_in_pool

  • prefer_global_in_and_join

image.png

Compatibilité de MaterializedMySQL

Si votre cluster autogéré synchronise les données depuis MySQL à l'aide du moteur MaterializedMySQL, tenez compte de la manière dont ApsaraDB for ClickHouse gère cette situation après la migration.

La version communautaire du moteur MaterializedMySQL n'est plus maintenue. ApsaraDB for ClickHouse utilise Data Transmission Service (DTS) pour synchroniser les données MySQL. DTS crée une table locale ReplacingMergeTree sur chaque nœud du cluster, ainsi qu'une table distribuée qui achemine les écritures vers ces nœuds, remplaçant ainsi entièrement la table MaterializedMySQL.

Cette modification architecturale entraîne deux problèmes courants :

Problème 1 : Les requêtes IN et JOIN échouent sur la table distribuée

Dans le cluster autogéré, MaterializedMySQL réplique les données directement sur chaque shard. Après la migration, les données transitent par une table distribuée, ce qui modifie la résolution des sous-requêtes IN et JOIN. Pour plus de détails et de solutions, consultez la section Que faire en cas d'erreur lors de l'utilisation d'une sous-requête sur une table distribuée ?.

Problème 2 : Données en double dans les résultats de requête

La table ReplacingMergeTree fusionne les lignes en double de manière asynchrone en arrière-plan. Si les fusions prennent du retard, les requêtes renvoient plus de doublons que le cluster autogéré. Deux solutions sont disponibles :

Solution 1 : Forcer la déduplication au moment de la requête

Exécutez la commande suivante pour activer globalement le modificateur final. Cela déduplique les résultats lors de chaque requête, mais augmente l'utilisation du CPU et de la mémoire :

SET global final = 1;

Solution 2 : Ajuster la fréquence de fusion sur la table cible

Augmentez la fréquence à laquelle ApsaraDB for ClickHouse fusionne les parties dans la table ReplacingMergeTree :

ALTER TABLE <AIM_TABLE>
    MODIFY SETTING
        min_age_to_force_merge_on_partition_only = 1,
        min_age_to_force_merge_seconds = 60;
Paramètre Description Valeur par défaut
min_age_to_force_merge_on_partition_only Lorsqu'il est défini sur 1, force la fusion des données au sein des partitions 0 (désactivé)
min_age_to_force_merge_seconds Intervalle de temps entre les fusions forcées de parties (en secondes) 3600

Vérification de la compatibilité SQL

Utilisez un script Python pour extraire les instructions SELECT du journal des requêtes de votre cluster autogéré et rejouez-les sur ApsaraDB for ClickHouse. Les requêtes ayant échoué indiquent des problèmes de compatibilité SQL à résoudre avant la migration.

Prérequis

Avant de commencer, assurez-vous que :

Vérifier la compatibilité SQL

  1. Installez la bibliothèque cliente Python pour ClickHouse :

    pip3 install clickhouse_driver
  2. Créez un fichier Python contenant le script suivant. Remplacez les valeurs d'espace réservé par vos détails de connexion réels :

    from clickhouse_driver import connect
    import datetime
    import logging
    # pip3 install clickhouse_driver is required.
    
    # Connection settings for the self-managed cluster
    host_old = 'HOST_OLD'          # VPC endpoint
    port_old = TCP_PORT_OLD        # TCP port
    user_old = 'USER_OLD'          # Username
    password_old = 'PASSWORD_OLD'  # Password
    
    # Connection settings for ApsaraDB for ClickHouse
    host_new = 'HOST_NEW'          # VPC endpoint
    port_new = TCP_PORT_NEW        # TCP port
    user_new = 'USER_NEW'          # Username
    password_new = 'PASSWORD_NEW'  # Password
    
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    def create_connection(host, port, user, password):
        """Establish a connection to ClickHouse."""
        return connect(host=host, port=port, user=user, password=password)
    
    def get_query_hashes(cursor):
        """Query the query_hash list of the last two days."""
        get_queryhash_sql = '''
        select distinct normalized_query_hash from system.query_log
        where type='QueryFinish'
          and `is_initial_query`=1
          and `user` not in ('default', 'aurora')
          and lower(`query`) not like 'select 1%'
          and lower(`query`) not like 'select timezone()%'
          and lower(`query`) not like '%dms-websql%'
          and lower(`query`) like 'select%'
          and `event_time` > now() - INTERVAL 2 DAY;
        '''
        cursor.execute(get_queryhash_sql)
        return cursor.fetchall()
    
    def get_sql_info(cursor, queryhash):
        """Query the SQL information for the specified query_hash in the last 2 days (search range is 3 days)."""
        get_sqlinfo_sql = f'''
        select `event_time`, `query_duration_ms`, `read_rows`, `read_bytes`, `memory_usage`, `databases`, `tables`, `current_database`, `query`
        from system.query_log
        where `event_time` > now() - INTERVAL 3 DAY and `type`='QueryFinish' and `normalized_query_hash`='{queryhash}' limit 1
        '''
        cursor.execute(get_sqlinfo_sql)
        sql_info = cursor.fetchone()
        if sql_info:
            return [info.strftime('%Y-%m-%d %H:%M:%S') if isinstance(info, datetime.datetime) else info for info in sql_info[:-1]], sql_info[-1]
        return None
    
    def execute_sql_on_new_db(cursor, current_database, query_sql, execute_failed_sql):
        """Execute the SQL query on the new cluster and record any failures."""
        try:
            cursor.execute(f"USE {current_database};")
            cursor.execute(query_sql)
        except Exception as error:
            logging.error(f'query_sql execute in new db failed: {query_sql}')
            execute_failed_sql[query_sql] = error
    
    def main():
        conn_old = create_connection(host=host_old, port=port_old, user=user_old, password=password_old)
        conn_new = create_connection(host=host_new, port=port_new, user=user_new, password=password_new)
    
        cursor_old = conn_old.cursor()
        cursor_new = conn_new.cursor()
    
        # Extract query hashes and SQL from the self-managed cluster
        old_query_hashes = get_query_hashes(cursor_old)
        old_db_execute_dir = {}
        for queryhash in old_query_hashes:
            sql_info, query = get_sql_info(cursor_old, queryhash[0])
            if sql_info:
                old_db_execute_dir[query] = sql_info
    
        cursor_old.close()
        conn_old.close()
    
        # Replay queries against ApsaraDB for ClickHouse
        execute_failed_sql = {}
        keys_list = list(old_db_execute_dir.keys())
        for query_sql in old_db_execute_dir:
            position = keys_list.index(query_sql)
            current_database = old_db_execute_dir[query_sql][-1]
            logging.info(f"new db test the {position + 1}th/{len(old_db_execute_dir)}, running sql: {query_sql}\n")
            execute_sql_on_new_db(cursor_new, current_database, query_sql, execute_failed_sql)
    
        # Collect execution results from ApsaraDB for ClickHouse
        new_query_hashes = get_query_hashes(cursor_new)
        new_db_execute_dir = {}
        for queryhash in new_query_hashes:
            sql_info, query = get_sql_info(cursor_new, queryhash[0])
            if sql_info:
                new_db_execute_dir[query] = sql_info
    
        cursor_new.close()
        conn_new.close()
    
        # Log results
        for query_sql in new_db_execute_dir:
            if query_sql in old_db_execute_dir:
                logging.info(f'succeed sql: {query_sql}')
                logging.info(f'old sql info: {old_db_execute_dir[query_sql]}')
                logging.info(f'new sql info: {new_db_execute_dir[query_sql]}\n')
    
        for query_sql in execute_failed_sql:
            logging.error('\033[31m{}\033[0m'.format(f'failed sql: {query_sql}'))
            logging.error('\033[31m{}\033[0m'.format(f'failed error: {execute_failed_sql[query_sql]}\n'))
    
    if __name__ == "__main__":
        main()
  3. Exécutez le script et examinez la sortie. Chaque requête ayant échoué est enregistrée en rouge avec son message d'erreur. Analysez et corrigez les échecs en fonction de l'erreur spécifique avant de procéder à la migration de production.

Analyse et optimisation des performances

Après le basculement du trafic vers ApsaraDB for ClickHouse, vous pouvez constater une saturation du CPU ou un épuisement de la mémoire. Les étapes ci-dessous vous aident à localiser systématiquement la cause racine, depuis l'identification des tables problématiques jusqu'à l'isolement des requêtes spécifiques, en passant par leur analyse et leur correction.

Si vous savez déjà quelle table ou quelle requête est à l'origine du problème de performance, passez directement à l'Étape 3 : Analyser les performances SQL.

Étape 1 : Identifier les tables à l'origine du goulot d'étranglement des performances

Deux approches sont possibles : les graphiques en flammes (flame graphs) et l'analyse de query_log. Les graphiques en flammes nécessitent plus de configuration, mais offrent une ventilation visuelle et intuitive de l'utilisation du CPU. L'analyse de query_log est plus simple et ne nécessite aucun outil externe, mais vous devez interpréter les données vous-même. Utilisez les deux méthodes conjointement pour obtenir une image aussi complète que possible.

Créer un graphique en flammes

  1. Connectez-vous à ApsaraDB for ClickHouse en utilisant clickhouse-client. Pour les instructions de connexion, consultez la section Se connecter à un cluster ClickHouse via l'interface de ligne de commande.

  2. Exportez le trace_log pour la fenêtre temporelle durant laquelle le problème de performance s'est produit. Remplacez <IP>, <port> et les valeurs de event_time par vos valeurs réelles :

    Paramètre Description
    <IP> Endpoint Virtual Private Cloud (VPC) d'ApsaraDB for ClickHouse
    <port> Port TCP d'ApsaraDB for ClickHouse
    # trace_type = 'CPU' collects CPU call stacks.
    # trace_type = 'Real' collects wall-clock call stacks.
    /clickhouse/bin/clickhouse-client -h <IP> --port <port> -q \
      "SELECT arrayStringConcat(arrayReverse(arrayMap(x -> concat( addressToLine(x), '#', demangle(addressToSymbol(x)) ), trace)), ';') AS stack, count() AS samples \
      FROM system.trace_log \
      WHERE trace_type = 'CPU' \
        AND event_time >= '2025-01-08 19:31:00' \
        AND event_time < '2025-01-08 19:33:00' \
      GROUP BY trace \
      ORDER BY samples DESC \
      FORMAT TabSeparated \
      SETTINGS allow_introspection_functions=1" > cpu_trace_log.txt
  3. Installez clickhouse-flamegraph. Pour les instructions de téléchargement et d'installation, consultez clickhouse-flamegraph.

  4. Générez le graphique en flammes :

    cat cpu_trace_log.txt | flamegraph.pl > cpu_trace_log.svg

    Ouvrez le fichier SVG pour identifier les fonctions consommant le plus de CPU. Par exemple, si vous voyez ReplacingSortedMerge apparaître de manière prédominante, concentrez votre enquête sur les requêtes adressées aux tables ReplacingMergeTree.

    image

Analyser query_log

Exécutez ces requêtes simultanément sur les deux clusters pour effectuer une comparaison Top N. Recherchez les tables dont l'utilisation du CPU ou de la mémoire est significativement plus élevée sur ApsaraDB for ClickHouse que sur le cluster autogéré ; ce sont vos candidates.

Une utilisation élevée du CPU ou de la mémoire sur une table n'entraîne pas toujours une dégradation globale du cluster. Cela suggère simplement que cette table est plus susceptible d'être à l'origine du problème. Comparez les résultats entre les deux clusters pour confirmer.

Localiser les tables présentant des problèmes de CPU

SELECT
    tables,
    first_value(query),
    count() AS cnt,
    groupArrayDistinct(normalizedQueryHash(query)) AS normalized_query_hash,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS sum_user_cpu,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS sum_system_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS avg_user_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS avg_system_cpu,
    sum(memory_usage) AS sum_memory_usage,
    avg(memory_usage) AS avg_memory_usage
FROM clusterAllReplicas(default, system.query_log)
WHERE (event_time > '2025-01-08 19:30:00') AND (event_time < '2025-01-08 20:30:00') AND (query_kind = 'Select')
GROUP BY tables
ORDER BY sum_user_cpu DESC
LIMIT 5

Localiser les tables présentant des problèmes de mémoire

SELECT
    tables,
    first_value(query),
    count() AS cnt,
    groupArrayDistinct(normalizedQueryHash(query)) AS normalized_query_hash,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS sum_user_cpu,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS sum_system_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS avg_user_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS avg_system_cpu,
    sum(memory_usage) AS sum_memory_usage,
    avg(memory_usage) AS avg_memory_usage
FROM clusterAllReplicas(default, system.query_log)
WHERE (event_time > '2025-01-08 19:30:00') AND (event_time < '2025-01-08 20:30:00') AND (query_kind = 'Select')
GROUP BY tables
ORDER BY sum_memory_usage
LIMIT 5

Remplacez les valeurs de event_time par la fenêtre temporelle durant laquelle le problème de performance s'est produit.

Étape 2 : Identifier les requêtes à l'origine du goulot d'étranglement des performances

Après avoir identifié la table problématique à l'étape 1, exécutez les requêtes suivantes pour isoler les instructions SQL spécifiques à l'origine du problème. Remplacez <AIM_TABLE> par le nom de la table issu de l'étape 1 et mettez à jour event_time pour correspondre à votre fenêtre temporelle cible.

Localiser les requêtes SQL à l'origine des problèmes de CPU

SELECT
    tables,
    first_value(query),
    count() AS cnt,
    normalizedQueryHash(query) AS normalized_query_hash,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS sum_user_cpu,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS sum_system_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS avg_user_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS avg_system_cpu,
    sum(memory_usage) AS sum_memory_usage,
    avg(memory_usage) AS avg_memory_usage
FROM clusterAllReplicas(default, system.query_log)
WHERE (event_time > '2025-01-08 19:30:00') AND (event_time < '2025-01-08 20:30:00') AND (query_kind = 'Select') AND has(tables, '<AIM_TABLE>')
GROUP BY
    tables,
    normalized_query_hash
ORDER BY sum_user_cpu DESC
LIMIT 5

Localiser les requêtes SQL à l'origine des problèmes de mémoire

SELECT
    tables,
    first_value(query),
    count() AS cnt,
    normalizedQueryHash(query) AS normalized_query_hash,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS sum_user_cpu,
    sum(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS sum_system_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')]) AS avg_user_cpu,
    avg(ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')]) AS avg_system_cpu,
    sum(memory_usage) AS sum_memory_usage,
    avg(memory_usage) AS avg_memory_usage
FROM clusterAllReplicas(default, system.query_log)
WHERE (event_time > '2025-01-08 19:30:00') AND (event_time < '2025-01-08 20:30:00') AND (query_kind = 'Select') AND has(tables, 'AIM_TABLE')
GROUP BY
    tables,
    normalized_query_hash
ORDER BY sum_memory_usage
LIMIT 5

Étape 3 : Analyser les performances SQL

Utilisez EXPLAIN PIPELINE et system.query_log pour comprendre pourquoi une requête spécifique est lente.

Analyser le plan d'exécution :

EXPLAIN PIPELINE <SQL with performance issues>

Pour plus de détails sur la lecture de la sortie EXPLAIN, consultez la documentation ClickHouse EXPLAIN.

Analyser les métriques d'exécution depuis query_log :

SELECT
    hostname() AS host,
    *
FROM clusterAllReplicas(`default`, system.query_log)
WHERE
    event_time > '2025-01-18 00:00:00'
    AND event_time < '2025-01-18 03:00:00'
    AND initial_query_id = '<INITIAL_QUERY_ID>'
    AND type = 'QueryFinish'
ORDER BY query_start_time_microseconds

Remplacez <INITIAL_QUERY_ID> par l'ID de la requête que vous souhaitez examiner et mettez à jour event_time pour couvrir la fenêtre temporelle pertinente.

Concentrez-vous sur ces champs dans la sortie :

Champ Éléments à rechercher
ProfileEvents Compteurs détaillés pour le temps CPU, les E/S disque, l'allocation mémoire et le réseau. Permet de comparer l'utilisation des ressources entre les nœuds. Consultez la référence des événements système.
Settings Paramètres de configuration actifs pendant la requête. Aide à identifier les paramètres pouvant causer un comportement inattendu. Consultez la référence des paramètres.
query_duration_ms Durée totale d'exécution de la requête. Si une sous-requête est anormalement lente, utilisez son query_id pour trouver ses détails d'exécution dans le journal. Pour ajuster la verbosité du journal, consultez Configurer les paramètres du fichier config.xml.

Si les métriques seules n'expliquent pas la dégradation des performances, extrayez les informations d'exécution de la même requête depuis le cluster autogéré et comparez-les côte à côte avec les résultats d'ApsaraDB for ClickHouse. Les différences dans les valeurs de ProfileEvents révèlent souvent la cause racine.

Étape 4 : Optimiser le SQL

Après avoir identifié la requête lente, appliquez les optimisations pertinentes ci-dessous. La plupart des régressions de performances après une migration ont une cause racine spécifique ; concentrez-vous sur la technique correspondant à vos constatations plutôt que d'appliquer toutes les optimisations en même temps.

Optimisation Description
Optimisation des index Identifiez les colonnes fréquemment utilisées dans les clauses WHERE et créez un index pour celles-ci.
Types de données Utilisez les types de données les plus appropriés pour réduire la surcharge de stockage et accélérer les lectures.
**Éviter SELECT *** Sélectionnez uniquement les colonnes nécessaires pour réduire les E/S et le transfert réseau.
Utiliser la clé primaire et les colonnes indexées dans WHERE Filtrez sur les colonnes indexées pour permettre l'élagage (pruning).
Utiliser PREWHERE Déplacez les conditions de filtre sélectives vers PREWHERE pour réduire le volume de données traité par les étapes suivantes.
Utiliser GLOBAL JOIN pour les tables distribuées Remplacez JOIN standard par GLOBAL JOIN pour éviter l'inefficacité de la diffusion par shard.
Ajuster max_threads L'augmentation de max_threads utilise plus de cœurs CPU, mais une valeur trop élevée provoque une contention des ressources. Consultez la référence des paramètres.
Créer des vues matérialisées Pour les requêtes complexes fréquemment exécutées avec des résultats stables, pré-agrégez les données à l'aide d'une vue matérialisée.
Ajuster la compression des données ClickHouse compresse les données par défaut. L'ajustement de l'algorithme et du niveau de compression peut améliorer l'efficacité du stockage et la vitesse des requêtes.
Étendre le cache de requêtes Pour les requêtes stables et fréquemment répétées, augmentez uncompressed_cache_size pour réduire le recalcul. Pour les instructions de modification des paramètres, consultez Configurer les paramètres du fichier config.xml.