Cómo ahorré un 30% de CPU con 2 líneas de código

Establecer la tecnología Didi como " Estrella ⭐️ "

Reciba actualizaciones de artículos lo antes posible

ClickHouse es una base de datos distribuida en columnas de alto rendimiento de código abierto para el análisis de datos en tiempo real. Admite motores de computación vectorizados, computación paralela multinúcleo y altas tasas de compresión. Ocupa el primer lugar en el rendimiento de consultas de una sola tabla entre las bases de datos analíticas. Didi comenzó a presentar Clickhouse en 2020, sirviendo negocios principales como el transporte de automóviles en línea y la recuperación de registros. La cantidad de nodos es de más de 300, los datos de nivel PB se escriben todos los días y se realizan decenas de millones de consultas todos los días. el clúster más grande tiene más de 200 nodos. Este artículo introduce principalmente un punto en la optimización del rendimiento de Clickhouse, desde el descubrimiento de problemas hasta la solución final del problema y la obtención de mejores beneficios.

01

problema encontrado

La carga del nodo en línea es relativamente alta y es necesario ubicar dónde se usa principalmente la CPU. Lo primero que debe confirmar es qué módulo ocupa la CPU En Clickhouse, los módulos que hacen un uso intensivo de la CPU son principalmente consultas, escritura y fusión. Use el comando top para ubicar el proceso con el mayor uso de CPU. Después de ubicar el proceso, use el comando top -Hp pid para ver el subproceso con el mayor uso de CPU, como se muestra en la siguiente figura:

60fcd3e559fba4523cdd54badec95051.png

1. En primer lugar está el subproceso BackgrProcPool, que es responsable de ejecutar las tareas de fusión y mutación de la tabla ReplicatedMergeTree y necesita procesar una gran cantidad de datos.

2. En segundo lugar está el subproceso HTTPHandler, que es responsable de procesar las solicitudes http del cliente, incluidos el análisis de consultas, la optimización y la generación del plan de ejecución. El plan de ejecución físico generado final será ejecutado por el subproceso QueryPipelineEx.

3. Mirando hacia abajo, encontrará que seis subprocesos BackgrProcPool consecutivos ocupan más del 30 % de la CPU. Son los principales responsables del movimiento de datos entre discos. Cuando el uso del disco supera el umbral establecido (90 % de forma predeterminada), el subproceso BgMoveProcPool moverá el archivo de la pieza en el disco a otro disco. Al mismo tiempo, si Move TTL está configurado para la tabla, la pieza se moverá al disco de destino cuando caduquen los datos de la pieza. Se utiliza principalmente para realizar el enfriamiento y el calentamiento de los datos. El número máximo predeterminado de subprocesos en el grupo de subprocesos BgMoveProc es 8, que es responsable de mover datos entre todos los discos de la tabla MergeTree.

4. El subproceso ZookeeperSend restante y el subproceso ZookeeperRecv en la figura son respectivamente responsables de enviar la solicitud de operación a ZK y recibir la respuesta de la operación correspondiente.El mecanismo de sincronización de copia de la tabla ReplicatedMergeTree se basa en ZK para realizar. Hay muchos otros hilos en Clickhouse, así que no los presentaré uno por uno aquí.

El comando top ha estado monitoreando durante un período de tiempo y descubrió que el uso de CPU de estos 8 subprocesos BgMoveProPool está casi siempre en la parte superior. ¿Podría ser que la tasa de uso de algunos discos haya alcanzado el 90% y todos los subprocesos Move están reubicando datos entre discos? Pero cuando el disco en línea se usa hasta el 80%, se activará. ¿Hay algún problema con la alarma?

Use el comando df -h para verificar el uso del disco. Después de la ejecución, se encuentra que la tasa de uso de los 12 discos es de aproximadamente el 50%, lo cual es muy extraño. El espacio en disco es suficiente y el clúster en línea no está configurado con hot y separación en frío. , es razonable que el subproceso BgMoveProcPool no ocupe la CPU, ¿qué está haciendo?

02

confirmar pregunta

Para averiguar qué está ejecutando el subproceso BgMoveProcPool, use el comando pstack pid para tomar la pila en este momento, imprima la pila varias veces y descubra que el subproceso BgMoveProcPool está en el método MergeTreePartsMover::selectPartsForMove, y la pila es como sigue:

#0  0x00000000100111a4 in DB::MergeTreePartsMover::selectPartsForMove(std::__1::vector<DB::MergeTreeMoveEntry, std::__1::allocator<DB::MergeTreeMoveEntry> >&, std::__1::function<bool (std::__1::shared_ptr<DB::IMergeTreeDataPart const> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >*)> const&, std::__1::lock_guard<std::__1::mutex> const&) ()
#1  0x000000000ff6ef5a in DB::MergeTreeData::selectPartsForMove() ()
#2  0x000000000ff86096 in DB::MergeTreeData::selectPartsAndMove() ()
#3  0x000000000fe5d102 in std::__1::__function::__func<DB::StorageReplicatedMergeTree::startBackgroundMovesIfNeeded()::{lambda()#1}, std::__1::allocator<{lambda()#1}>, DB::BackgroundProcessingPoolTaskResult ()>::operator()() ()
#4  0x000000000ff269df in DB::BackgroundProcessingPool::workLoopFunc() ()
#5  0x000000000ff272cf in _ZZN20ThreadFromGlobalPoolC4IZN2DB24BackgroundProcessingPoolC4EiRKNS2_12PoolSettingsEPKcS7_EUlvE_JEEEOT_DpOT0_ENKUlvE_clEv ()
#6  0x000000000930b8bd in ThreadPoolImpl<std::__1::thread>::worker(std::__1::__list_iterator<std::__1::thread, void*>) ()
#7  0x0000000009309f6f in void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, int, std::__1::optional<unsigned long>)::{lambda()#3}> >(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, int, std::__1::optional<unsigned long>)::{lambda()#3}>) ()
#8  0x00007ff91f4d5ea5 in start_thread () from /lib64/libpthread.so.0
#9  0x00007ff91edf2b0d in clone () from /lib64/libc.so.6

El método selectPartsForMove está siendo ejecutado por el subproceso BgMoveProcPool capturado muchas veces, lo que indica que el método selectPartsForMove lleva mucho tiempo. Por el nombre del método, podemos entender que este método es para encontrar la parte que se puede mover y luego consultar el sistema. tabla .part_log para ver el registro MovePart.

SELECT * FROM system.part_log WHERE event_time > now() - toIntervalDay(1) AND event_type = 'MovePart'

Ejecute el SQL anterior para consultar los registros de MovePart en el último día, pero no hay coincidencia. Hasta ahora, casi podemos estar seguros de que el subproceso BgMoveProcPool ha estado consultando las partes que se pueden mover, pero los resultados están todos vacíos y la CPU ha estado haciendo cálculos no válidos. De acuerdo con el análisis anterior, se ha localizado el código problemático y el siguiente paso es estudiar el código fuente de selectPartsForMove, de la siguiente manera:

bool MergeTreePartsMover::selectPartsForMove(MergeTreeMovingParts & parts_to_move, const AllowedMovingPredicate & can_move, const std::lock_guard<std::mutex> & /* moving_parts_lock */) {
    std::unordered_map<DiskPtr, LargestPartsWithRequiredSize> need_to_move;
    ///  1. 遍历所有的disk,将使用率超过阀值的disk添加need_to_move中
    if (!volumes.empty()) {
        for (size_t i = 0; i != volumes.size() - 1; ++i) {
            for (const auto & disk : volumes[i]->getDisks()) {
                UInt64 required_maximum_available_space = disk->getTotalSpace() * policy->getMoveFactor(); /// move_factor默认0.9
                UInt64 unreserved_space = disk->getUnreservedSpace();
                if (unreserved_space < required_maximum_available_space)
                    need_to_move.emplace(disk, required_maximum_available_space - unreserved_space);
            }
        }
    }
    /// 2. 遍历所有的part,首先如果Part的MoveTTL已过期则添加到需要移动的集合parts_to_move中,否则为超过阈值的disk添加候选Part
    time_t time_of_move = time(nullptr);
    for (const auto & part : data_parts) {
        /// 检查该part能否被move, 
        if (!can_move(part, &reason))
            continue;




        /// 检查part的move_ttl
        auto ttl_entry = data->selectTTLEntryForTTLInfos(part->ttl_infos, time_of_move);
        auto to_insert = need_to_move.find(part->volume->getDisk());
        if (ttl_entry) { /// 已过期,则需要移动到目标磁盘
            auto destination = data->getDestinationForTTL(*ttl_entry);
            if (destination && !data->isPartInTTLDestination(*ttl_entry, *part))
                reservation = data->tryReserveSpace(part->getBytesOnDisk(), data->getDestinationForTTL(*ttl_entry));
        }
        if(reservation) /// 需要移动
            parts_to_move.emplace_back(part, std::move(reservation));
        else {  /// 候选Part
            if (to_insert != need_to_move.end())
                to_insert->second.add(part);
        }
    }
    /// 3. 为候选的Part申请空间并添加到需要移动的集合parts_to_move中
    for (auto && move : need_to_move) {
        for (auto && part : move.second.getAccumulatedParts()) {
            auto reservation = policy->reserve(part->getBytesOnDisk(), min_volume_index);
            if (!reservation)
                break;


            parts_to_move.emplace_back(part, std::move(reservation));
            ++parts_to_move_by_policy_rules;
            parts_to_move_total_size_bytes += part->getBytesOnDisk();
        }
    }

El método SelectPartsForMove principalmente hace 3 cosas:

  • Primero recorra todos los discos y agregue discos cuyo uso supere el umbral de need_to_move.

  • Luego, recorra todas las partes. Primero, si el MoveTTL de la parte ha expirado, agréguelo al conjunto parts_to_move que necesita moverse. De lo contrario, agregue una parte candidata para el disco que excede el umbral.

  • Finalmente, solicite espacio para la Parte candidata y agréguela al conjunto parts_to_move que necesita ser movida.

El paso que consume más tiempo es el segundo paso, que aumentará con el aumento del número de partes en la tabla.Después de consultar system.parts, se encuentra que hay más de 300,000 partes en total, y la tabla más grande tiene más de 60 000 piezas ¿Por qué es así? No es de extrañar que lleve tiempo.

El problema es obvio aquí: el subproceso BgMoveProcPool comprueba constantemente si las más de 300 000 piezas cumplen las condiciones de movimiento, pero cada vez que ninguna pieza cumple las condiciones, ha estado haciendo cálculos no válidos.

03

Resolver el problema

El espacio en disco del nodo en línea es suficiente y la estratificación fría y caliente de datos no está configurada, por lo que no hay necesidad de desperdiciar CPU para verificar cada parte.

Cuando la tasa de uso del disco alcanza el 90 %, el valor de need_to_move obtenido está vacío y la estratificación caliente y fría no está configurada, es decir, move_ttl está vacío.Cuando se cumplen ambas condiciones, ¿puede ahorrar mucho dinero sin verificar todas las partes? El cálculo se repite, por lo tanto, agregue las siguientes dos líneas de código antes de atravesar y verificar la parte.Cuando need_to_move está vacío y move_ttl está vacío, devuelve falso directamente.

if (need_to_move.empty() && !metadata_snapshot->hasAnyMoveTTL())
    return false;

04

efecto real

Publique en el clúster público doméstico y luego use el comando superior para observar la CPU consumida por cada subproceso. Se puede encontrar que el subproceso BgMoveProcPool ya no se encuentra en el frente, y la CPU ocupada por los 8 subprocesos BgMoveProcPool se ha reducido de alrededor del 30% antes a por debajo del 4%.

3e8419e6ca3013cd15dfbcd22c6413dd.png

Echemos un vistazo a la CPU general de la máquina. Se puede ver claramente que la CPU se ha reducido de aproximadamente un 20 % antes de la actualización a aproximadamente un 10 %, y los picos no son tan altos.

1c1e87a68b1eecbc7ef9c4232b43dd88.png

Y aportó esta optimización a la comunidad, se ha fusionado para dominar.

05

pensamientos de seguimiento

En muchos casos, el código no tendrá problemas cuando la cantidad de datos es pequeña y la concurrencia es baja. Una vez que la cantidad de datos y la concurrencia aumenten, surgirán muchos problemas. En el proceso de escritura del código, respete cada línea de código para hacer el programa más robusto. En el futuro, Clickhouse continuará esforzándose en el escenario de recuperación de registros para crear un sistema de recuperación de registros de nivel PB estable, de bajo costo y alto rendimiento. 

Supongo que te gusta

Origin blog.csdn.net/DiDi_Tech/article/details/131566458
Recomendado
Clasificación