Un largo artículo de diez mil caracteres detalla la exploración y práctica de ClickHouse en datos en tiempo real de Jingxingda | Equipo técnico de JD Cloud

1. Introducción

El departamento de tecnología de Jingxingda adopta la arquitectura JDQ+Flink+Elasticsearch en el escenario de compra del grupo comunitario para crear informes de datos en tiempo real. Con el desarrollo del negocio, Elasticsearch comenzó a exponer algunas desventajas. No es adecuado para consultas de datos a gran escala. La exportación de paginación profunda de alta frecuencia conduce al tiempo de inactividad de ES, no puede deduplicar estadísticas con precisión y el rendimiento cae significativamente cuando se usan múltiples campos. se agregan y calculan. Así que se introdujo ClickHouse para hacer frente a estos inconvenientes.

El enlace de escritura de datos es que los datos comerciales (binlog) se procesan y se convierten en mensajes MQ en un formato fijo.Flink se suscribe a diferentes temas para recibir datos de tablas de diferentes sistemas de producción, realiza asociación, cálculo, filtrado, datos básicos complementarios, etc. .table y, finalmente, escriba el flujo de datos DataStream procesado en ES y ClickHouse. El servicio de consulta está expuesto al exterior a través de JSF y la puerta de enlace de logística para visualización.Dado que ClickHouse usa toda su potencia informática para una consulta, no es bueno en consultas de alta concurrencia. Agregamos cachés a algunas interfaces de indicadores de agregación en tiempo real, o consultamos los indicadores de cálculo de ClickHosue en tareas programadas y los almacenamos en ES. Algunos indicadores ya no verifican ClickHouse en tiempo real, sino que verifican los indicadores calculados en ES para resistir la concurrencia, y este método puede ser muy mejorado Mejore la eficiencia del desarrollo, el fácil mantenimiento y puede unificar el calibre del índice.

He experimentado varias dificultades en el proceso de introducción de ClickHouse y gasté mucha energía para explorarlas y resolverlas una por una. Las registraré aquí y espero brindar alguna orientación para los estudiantes que no han estado en contacto con ClickHouse. evite los desvíos Si hay errores en el texto, espero que se incluyan más instrucciones, y todos son bienvenidos a discutir temas relacionados con ClickHouse. Este artículo es largo pero está lleno de productos secos, espere de 40 a 60 minutos para leerlo.

2 problemas encontrados

Como se mencionó anteriormente, nos hemos encontrado con muchas dificultades.Los siguientes problemas son el enfoque de este artículo.

  • ¿Qué motor de tabla deberíamos usar?
  • Cómo escribe Flink en ClickHouse
  • ¿Por qué es 1~2 minutos más lento consultar ClickHouse que consultar ES?
  • Si escribir en una tabla distribuida o en una tabla local
  • ¿Por qué el uso de la CPU de un solo fragmento es alto?
  • Cómo ubicar qué SQL consumen CPU, tantos SQL lentos, ¿cómo sé qué SQL lo está causando?
  • SQL lento encontrado, cómo optimizarlo
  • Cómo resistir la alta concurrencia y garantizar la disponibilidad de ClickHouse

3 Esquema de consulta y selección del motor de tablas

Antes de elegir un motor de tabla y un esquema de consulta, primero aclare los requisitos. Como se mencionó en el prefacio, estamos construyendo tablas anchas en Flink, lo que implicará operaciones de actualización de datos en el negocio, y el mismo número de pedido comercial se escribirá en la base de datos varias veces. El upsert de ES admite este tipo de operación que necesita sobrescribir los datos anteriores. No hay upsert en ClickHouse, por lo que es necesario explorar una solución que pueda admitir upsert. Con este requisito, echemos un vistazo al motor de tablas y el esquema de consulta de ClickHouse.

ClickHouse tiene muchos motores de tablas, y el motor de tablas determina cómo se almacenan los datos, cómo se cargan y qué características tiene la tabla de datos. En la actualidad, el motor de tablas de ClickHouse se divide en cuatro series, a saber, Log, MergeTree, Integration y Special.

  • Serie de registros: adecuado para escenarios con una pequeña cantidad de datos (menos de un millón de filas) y no admite índices, por lo que no es eficiente para consultas de rango.
  • Serie de integración: se utiliza principalmente para importar datos externos a ClickHouse, u operar directamente datos externos en ClickHouse, compatible con Kafka, HDFS, JDBC, Mysql, etc.
  • Serie especial: por ejemplo, la memoria almacena datos en la memoria interna y los datos se perderán después de reiniciar. El rendimiento de la consulta es excelente y el archivo usa directamente archivos locales como almacenamiento de datos, etc. La mayoría de ellos están personalizados para escenarios específicos.
  • Serie MergeTree: la familia MergeTree tiene una variedad de variantes de motor. Como el motor más básico de la familia, MergeTree proporciona capacidades tales como indexación de clave principal, partición de datos, copia de datos y muestreo de datos, y admite cantidades extremadamente grandes de escritura de datos. Otros motores de la familia están basados ​​en el motor MergeTree, cada uno tiene sus propias fortalezas.

Log, Special e Integration se utilizan principalmente para fines especiales y los escenarios son relativamente limitados. Entre ellas, las características de rendimiento de ClickHouse son MergeTree y su motor de tabla familiar, que también es el motor de almacenamiento principal oficial. Es compatible con casi todas las funciones básicas de ClickHouse, y esta serie de motores de tabla se utilizará en la mayoría de los escenarios de la producción. ambiente. Nuestro negocio no es una excepción y requiere el uso de índices de clave principal. El incremento diario de datos es de más de 25 millones de incrementos, por lo que la serie MergeTree es el objetivo que debemos explorar.

El motor de tablas de la serie MergeTree está diseñado para insertar una gran cantidad de datos. Los datos se escriben rápidamente uno por uno en forma de fragmentos de datos. Para evitar demasiados fragmentos de datos, ClickHouse los fusionará en segundo plano de acuerdo con ciertas reglas para formar nuevos segmentos.En comparación con la modificación continua de los datos ya almacenados en el disco al insertarlos, esta estrategia de fusionar tras insertar y luego fusionar es mucho más eficiente. Esta característica de la fusión repetida de fragmentos de datos también es el origen del nombre de la serie MergeTree (familia de árboles de fusión). Para evitar la formación de demasiados fragmentos de datos, se requieren escrituras por lotes. La serie MergeTree incluye los motores MergeTree, ReplacingMergeTree, CollapsingMergeTree, VersionedCollapsingMergeTree, SummingMergeTree y AggregatingMergeTree.Estos motores se presentan a continuación.

3.1 MergeTree: fusionar árboles

MergeTree admite toda la sintaxis SQL de ClickHouse. La mayoría de las funciones son similares a MySQL con las que estamos familiarizados, pero algunas funciones son bastante diferentes, como la clave principal. La clave principal de la serie MergeTree no se usa para la deduplicación. En MySQL, no puede haber dos datos con la misma clave principal en una tabla, pero en ClickHouse está permitido.

En la declaración de creación de la tabla a continuación, se definen el número de pedido, la cantidad del producto, el tiempo de creación y el tiempo de actualización. Los datos se dividen de acuerdo con el tiempo de creación, orderNo se usa como clave principal (clave principal) y orderNo también se usa como clave de clasificación (ordenar por). De forma predeterminada, la clave principal y la clave de clasificación son las mismas. En la mayoría de los casos, no es necesario especificar la clave principal, en este ejemplo, solo para ilustrar la relación entre la clave principal y la clave de clasificación. Por supuesto, la clave de clasificación puede ser diferente del campo de clave principal, pero la clave principal debe ser un subconjunto de la clave de clasificación, como la clave principal (a,b), la clave de clasificación debe ser (a,b, , ), y los campos que componen la clave principal deben estar en el campo de clave de clasificación más a la izquierda en .

CREATE TABLE test_MergeTree (  orderNo String,  number Int16,  createTime DateTime,  updateTime DateTime) ENGINE = MergeTree()PARTITION BY createTimeORDER BY  (orderNo)PRIMARY KEY (orderNo);insert into test_MergeTree values('1', '20', '2021-01-01 00:00:00', '2021-01-01 00:00:00');insert into test_MergeTree values('1', '30', '2021-01-01 00:00:00', '2021-01-01 01:00:00');

Tenga en cuenta que el número de pedido de la clave principal de los dos datos escritos aquí es 1. En este escenario, primero creamos un pedido y luego actualizamos la cantidad de producto del pedido a 30 y el tiempo de actualización. el negocio es 1, y las piezas del producto La cantidad es 30.

La inserción de datos con la misma clave principal no causará conflictos, y ambos datos con la misma clave principal existen en los datos de consulta. La siguiente figura es el resultado de la consulta. Dado que cada inserción formará una parte, la primera inserción genera un archivo de partición de datos 1609430400_1_1_0 y la segunda inserción genera un archivo de partición de datos 1609430400_2_2_0. El fondo aún no ha activado la fusión, por lo que Clickhouse- cliente Los resultados de la pantalla se separan en dos tablas (las herramientas de consulta gráfica DBeaver y DataGrip no se pueden ver como dos tablas, y el entorno ClickHouse se puede construir a través de la ventana acoplable para ejecutar declaraciones a través del modo cliente, y hay un documento de entorno CK en el final del artículo).

El resultado esperado debería ser que el número se actualice de 20 a 30, y el tiempo de actualización también se actualice al valor correspondiente. Solo hay una fila de datos para la misma clave principal comercial, pero al final se conservan dos. Esta lógica de procesamiento en Clickhouse hará que los datos que consultamos sean incorrectos. Por ejemplo, cuente la cantidad de pedidos por deduplicación, cuente (número de pedido) y cuente la cantidad de pedidos realizados suma (número).

Intentemos fusionar dos filas de datos.

Después de la fusión forzada de segmentos, todavía hay dos datos, que no son los datos que esperábamos conservar el último elemento con una cantidad de 30. Pero las dos filas de datos se fusionan en una tabla, la razón es que el ID de partición de 1609430400_1_1_0 y 1609430400_2_2_0 son iguales y se fusionaron en un archivo de 1609430400_1_2_1. Una vez completada la fusión, 1609430400_1_1_0 y 1609430400_2_2_0 se eliminarán en segundo plano después de un período de tiempo determinado (8 minutos de forma predeterminada). La siguiente figura muestra las reglas de nomenclatura del archivo de partición, ID de partición: 1609430400 = 2021-01-01 00:00:00, MinBolckNum, MaxBolckNum: son el bloque de datos más pequeño y el bloque de datos más grande, y son un número entero de incremento automático . Nivel: 0 puede entenderse como el número de veces que se ha fusionado la partición. El valor predeterminado es 0, y se agregará 1 a la nueva partición generada después de cada fusión.

Con base en lo anterior, se puede ver que aunque MergeTree tiene una clave principal, no es similar a la función de deduplicación única que usa MySQL para mantener registros. Solo se usa para la aceleración de consultas. Incluso después de la fusión manual, las filas de datos con el Aún existe la misma clave principal y no puede ser utilizada por las empresas. La desduplicación de documentos conduce a resultados incorrectos de conteo (número de pedido) y suma (número), que no se aplican a nuestras necesidades.

3.2 Reemplazo del árbol de combinación: reemplazar el árbol de combinación

Aunque MergeTree tiene una clave principal, no puede deduplicar datos con la misma clave principal.Nuestros escenarios comerciales no pueden tener datos duplicados. ClickHouse proporciona el motor ReplacingMergeTree para la deduplicación, que puede eliminar datos duplicados al fusionar particiones. Entiendo que la deduplicación se divide en dos aspectos, uno es la deduplicación física, es decir, los datos duplicados se eliminan directamente, y el otro es la deduplicación de consulta, que no procesa datos físicos, pero los resultados de la consulta ya filtraron datos duplicados.

El ejemplo es el siguiente, el método de creación de la tabla ReplacingMergeTree no es particularmente diferente de MergeTree, pero el ENGINE se cambia de MergeTree a ReplacingMergeTree([ver]), donde ver es la columna de versión, que es un elemento opcional. el sitio web oficial es UInt, Date o DateTime, pero también experimenté con el tipo Int (ClickHouse 20.8.11). ReemplazoMergeTree deduplica los datos físicos durante la fusión de datos, y la estrategia de deduplicación es la siguiente.

  • Si no se especifica la columna de versión, la última fila insertada se mantiene entre las filas con la misma clave principal.
  • Si se ha especificado la columna de versión, el siguiente ejemplo especifica la columna de versión como la columna de versión, y la deduplicación conservará la fila con el valor de versión más grande, independientemente del orden de inserción de datos.

<!---->

CREATE TABLE test_ReplacingMergeTree (  orderNo String,  version Int16,  number Int16,  createTime DateTime,  updateTime DateTime) ENGINE = ReplacingMergeTree(version)PARTITION BY createTimeORDER BY  (orderNo)PRIMARY KEY (orderNo);1) insert into test_ReplacingMergeTree values('1', 1, '20', '2021-01-01 00:00:00', '2021-01-01 00:00:00');2) insert into test_ReplacingMergeTree values('1', 2, '30', '2021-01-01 00:00:00', '2021-01-01 01:00:00');3) insert into test_ReplacingMergeTree values('1', 3, '30', '2021-01-02 00:00:00', '2021-01-01 01:00:00');-- final方式去重select * from test_ReplacingMergeTree final;-- argMax方式去重select argMax(orderNo,version) as orderNo, argMax(number,version) as number,argMax(createTime,version),argMax(updateTime,version) from test_ReplacingMergeTree;

La siguiente figura muestra los resultados de tres consultas después de la ejecución de las dos primeras declaraciones de inserción. Ninguno de los tres métodos de consulta tiene ningún impacto en los datos almacenados físicamente. Los métodos final y argMax solo eliminan los duplicados de los resultados de la consulta.

  • Consulta ordinaria: los resultados de la consulta no se desduplican, los datos físicos no se desduplican (los archivos de partición no se fusionan)
  • consulta de deduplicación final: el resultado de la consulta se ha deduplicado, pero los datos físicos no se han deduplicado (los archivos de partición no se han fusionado)
  • Consulta de deduplicación argMax: el resultado de la consulta se ha deduplicado, pero los datos físicos no se han deduplicado (los archivos de partición no se han fusionado)

Entre ellos, los métodos de consulta final y argMax filtran los datos duplicados. Nuestros ejemplos son todas operaciones basadas en tablas locales. No hay diferencia en los resultados entre final y argMax. Sin embargo, si el experimento se basa en tablas distribuidas y los dos datos caen en diferentes fragmentos de datos (tenga en cuenta que esto no es una partición de datos ), entonces los resultados finales y argMax serán diferentes. El resultado final no se deduplicará, porque final solo puede deduplicar la tabla local y no puede deduplicar los datos de fragmentos cruzados, pero se deduplica el resultado de argMax. argMax extrae los datos más recientes que queremos consultar comparando el tamaño de la segunda versión del parámetro para lograr el propósito de filtrar datos duplicados. El principio es poner los datos de cada fragmento en la memoria del mismo fragmento para compararlos y calcularlos. , por lo que admite la deduplicación entre fragmentos.

Debido a que la combinación en segundo plano se realiza en un momento incierto, ejecute el comando de combinación y luego use la consulta normal para encontrar que el resultado son datos deduplicados, versión = 2, número = 30 son los datos que queremos conservar.

Ejecute la tercera declaración de inserción. La clave principal de la tercera declaración es la misma que las dos primeras, pero el campo de partición createTime es diferente. Las dos primeras son 2021-01-01 00:00:00 y la tercera es 2021 -01-02 00 :00:00, de acuerdo con el entendimiento anterior, los datos con la versión = 3 se conservarán después de la reunión de fusión forzada. Después de realizar una consulta común, encontramos que los datos con la versión = 1 y 2 se fusionaron y desduplicaron, y se retuvo la 2, pero aún existían los datos con la versión = 3. La razón de esto es que ReemplazarMergeTree elimina los datos duplicados en unidades de particiones Los campos de partición createTime y particiónID de las dos primeras inserciones son iguales, por lo que se fusionan en el archivo de partición 1609430400_1_2_1, y la tercera inserción es inconsistente con las dos primeras y no se puede fusionar en un archivo de partición, y la deduplicación física no se puede realizar. logrado. Finalmente, a través de la consulta de deduplicación final, se encuentra que se puede admitir la deduplicación y argMax también tiene el mismo efecto, que no se muestra.

ReemplazoMergeTree tiene las siguientes características

  • Utilice la clave principal como clave única para juzgar los datos duplicados y admita la inserción de datos con la misma clave principal.
  • La lógica para eliminar datos duplicados se activará al fusionar particiones. Pero el momento de la fusión es incierto, por lo que puede haber datos duplicados al realizar consultas, pero eventualmente se eliminarán los duplicados. Puede llamar a optimizar manualmente, pero provocará una gran cantidad de lectura y escritura de datos, por lo que no se recomienda para uso en producción.
  • Los datos duplicados se eliminan en unidades de particiones de datos. Cuando las particiones se combinan, los datos duplicados en la misma partición se eliminarán y los datos duplicados en diferentes particiones no se eliminarán.
  • Puede usar los métodos final y argMax para deduplicar consultas. De esta manera, puede obtener resultados de consulta correctos independientemente de si los datos se han combinado o no.

Mejor uso de ReemplazoMergeTree

  • Consulta de selección ordinaria: para consultas fuera de línea con poca puntualidad, ClickHouse se puede usar para fusionar y cooperar automáticamente, pero es necesario asegurarse de que el mismo documento comercial se encuentre en la misma partición de datos, y también se debe garantizar que la tabla distribuida sea en el mismo fragmento El método de consulta más eficiente y que ahorra recursos computacionales.
  • Consulta de modo final: puede usar final para consultas en tiempo real. Final es deduplicación local. Es necesario asegurarse de que los mismos datos de clave principal caigan en el mismo fragmento (Fragmento), pero no es necesario que caigan en los mismos datos. partición. Este método es menos eficiente, pero en comparación con la selección ordinaria, consumirá algo de rendimiento. Si la condición where golpea bien el índice de clave principal, el índice secundario y los campos de partición, entonces la eficiencia se puede usar por completo.
  • Consulta argMax: puede usar argMax para consultas en tiempo real. El requisito de argMax es el más bajo y puede deduplicar cualquier consulta. Sin embargo, debido a su método de implementación, la eficiencia será mucho menor y consumirá una gran cantidad de Rendimiento No se recomienda su uso. Más adelante en 9.4.3, los datos de la prueba de presión se compararán con los finales.

Entre los tres esquemas de uso anteriores, ReemplazarMergeTree con consulta de modo final está en línea con nuestras necesidades.

3.3 CollapsingMergeTree/VersionedCollapsingMergeTree: colapso del árbol de combinación

El colapso y la fusión de árboles ya no se ilustran con ejemplos. Puede consultar el ejemplo en el sitio web oficial.

CollapsingMergeTree registra el estado de la fila de datos definiendo un campo de bit de signo. Si el bit de signo es 1 (línea de "estado"), significa que esta es una línea de datos válida, y si el bit de signo es -1 (línea de "cancelar"), significa que esta línea de datos debe eliminarse. Cabe señalar que solo se pueden plegar los datos con la misma clave principal.

  • Si sign=1 tiene al menos una fila más que sign=-1, mantenga la última fila con sign=1.
  • Si sign=-1 es al menos una fila más que sign=1, mantenga la primera fila con sign=-1.
  • Si sign=1 tiene tantas filas como sign=-1, y la última fila es sign=1, conserve los datos de la primera fila de sign=-1 y la última fila de sign=1.
  • Si sign=1 tiene tantas líneas como sign=-1 y la última línea es sign=-1, no conserve nada.
  • En otros casos, ClickHouse no reportará un error pero imprimirá un registro de alarmas, en este caso, el resultado de la consulta es incierto e impredecible.

Preste atención al usar CollapsingMergeTree

1) Al igual que ReemplazarMergeTree, los datos plegables no se activan en tiempo real, sino solo cuando se fusionan las particiones. Los datos duplicados aún se consultarán antes de la fusión. hay dos soluciones

  • Use la optimización para forzar la fusión, y tampoco se recomienda usar la fusión forzada que es extremadamente ineficiente y consume recursos en un entorno de producción.
  • Vuelva a escribir el método de consulta utilizando group by con una columna de signo firmado. Este enfoque aumenta el costo de codificación de usar

2) En términos de escritura, el programa que necesita escribir datos registra los datos en la línea "Estado" eliminando o modificando datos a través de la línea "Cancelar", lo que aumenta considerablemente el costo de almacenamiento y la complejidad de la programación. Flink volverá a ejecutar los datos cuando se conecte o, en algunos casos, y las filas de datos registradas en el programa se perderán. Puede causar que el signo = 1 y el signo = -1 sean inconsistentes y no se puedan fusionar. Esto es un problema inaceptable para nosotros.

CollapsingMergeTree también tiene una desventaja. Tiene requisitos estrictos en el orden de escritura. Si está escrito en el orden normal, primero escriba la línea con signo = 1 y luego escriba la línea con signo = -1, y se puede fusionar normalmente. Si se invierte el orden, entonces no se puede fusionar normalmente. ClickHouse proporciona VersionedCollapsingMergeTree, que resuelve el problema del orden aumentando el número de versión. Pero otras funciones son exactamente las mismas que CollapsingMergeTree y no pueden satisfacer nuestras necesidades.

3.4 Resumen del motor de tabla

Presentamos en detalle los cuatro motores de tablas de la serie MergeTree, MergeTree, ReplacingMergeTree, CollapsingMergeTree y VersionedCollapsingMergeTree. También hay SummingMergeTree y AggregatingMergeTree que no se presentan. SummingMergeTree es un motor de tablas diseñado para no preocuparse por los datos detallados, sino solo por los datos agregados. . MergeTree también puede cumplir con este requisito de centrarse solo en datos resumidos. Se puede satisfacer mediante el uso de group by con funciones de agregación de suma y conteo, pero realizar una agregación en tiempo real para cada consulta aumentará mucho la sobrecarga. Tenemos requisitos de datos detallados y requisitos de indicadores de resumen, por lo que SummingMergeTree no puede satisfacer nuestras necesidades. AggregatingMergeTree es una versión mejorada de SummingMergeTree, que es esencialmente lo mismo. La diferencia es que SummingMergeTree realiza la agregación de suma en columnas de clave no principal, mientras que AggregatingMergeTree puede especificar varias funciones de agregación. Tampoco puede satisfacer la demanda.

Al final, elegimos el motor ReplacingMergeTree. La tabla distribuida se fragmenta a través de la clave principal comercial sipHash64 (docId) para garantizar que los datos de la misma clave principal comercial se encuentren en el mismo fragmento. Al mismo tiempo, la creación del documento comercial time se utiliza para particionar por mes/día. Coopere con las consultas finales para deduplicar. Durante el período de Double Eleven, los datos de esta solución aumentaron en 3000 W por día, y la  tasa de utilización de la CPU del clúster de la base de datos empresarial pico QPS93, 32C 128G 6 segmentos y 2 copias fue de hasta el 60 %, y el sistema se mantuvo estable como un entero. Todas las optimizaciones prácticas a continuación también se basan en el motor ReemplazoMergeTree.

4 Cómo escribe Flink en ClickHouse

4.1 Problema con la versión de Flink

Flink admite la escritura de datos en la base de datos JDBC a través del conector JDBC, pero los métodos de escritura del conector JDBC de las diferentes versiones de Flink son bastante diferentes. Porque Flink hizo una importante refactorización del conector JDBC en la versión 1.11:

  • El nombre del paquete antes de la versión 1.11 era flink-jdbc
  • El nombre del paquete después de la versión 1.11 (incluida) es flink-connector-jdbc

Los dos soportes para escribir en ClickHouse Sink de diferentes maneras en Flink son los siguientes:

Al principio usamos la versión 1.10.3 de Flink, y flink-jdbc no admite la escritura de flujo de datos. Necesitamos actualizar la versión de Flink a 1.11.x y superior para usar flink-connector-jdbc para escribir datos en ClickHouse.

4.2 Construcción del fregadero ClickHouse

/** * 构造Sink * [@param](https://my.oschina.net/u/2303379) clusterPrefix clickhouse 数据库名称 * [@param](https://my.oschina.net/u/2303379) sql   insert 占位符 eq:insert into demo (id, name) values (?, ?) */public static SinkFunction getSink(String clusterPrefix, String sql) {    String clusterUrl = LoadPropertiesUtil.appInfoProcessMap.get(clusterPrefix + CLUSTER_URL);    String clusterUsername = LoadPropertiesUtil.appInfoProcessMap.get(clusterPrefix + CLUSTER_USER_NAME);    String clusterPassword = LoadPropertiesUtil.appInfoProcessMap.get(clusterPrefix + CLUSTER_PASSWORD);    return JdbcSink.sink(sql, new CkSinkBuilder<>(),            new JdbcExecutionOptions.Builder().withBatchSize(200000).build(),             new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()                    .withDriverName("ru.yandex.clickhouse.ClickHouseDriver")                    .withUrl(clusterUrl)                    .withUsername(clusterUsername)                    .withPassword(clusterPassword)                    .build());}

Use la API JdbcSink.sink() de flink-connector-jdbc para construir un sumidero Flink. Los parámetros de entrada de JdbcSink.sink() tienen los siguientes significados

  • sql: instrucciones SQL en forma de marcadores de posición, por ejemplo: insertar en valores de demostración (id, nombre) (?, ?)
  • nuevo CkSinkBuilder<>(): la clase de implementación de la interfaz org.apache.flink.connector.jdbc.JdbcStatementBuilder, que asigna principalmente los datos en el flujo a java.sql.PreparedStatement para construir PreparedStatement, y los detalles no se repetirán .
  • El tercer parámetro de entrada: la estrategia de ejecución de flink sumidero.
  • El cuarto parámetro de entrada: controlador jdbc, conexión, número de cuenta y contraseña.

  • Cuando lo use, simplemente agregue Sink directamente en el flujo de DataStream.

5 Flink escribe la estrategia de ClickHouse

Flink escribe en ES y Clikhouse al mismo tiempo, pero al realizar una consulta de datos, se encuentra que ClickHouse siempre es más lento que ES. Se sospecha que el procesamiento, como la fusión de ClickHouse, llevará algún tiempo, pero estas operaciones de fusión de ClickHouse no afecta la consulta. Más tarde, revisé el código de la estrategia de escritura de Flink y descubrí que había un problema con la estrategia que usamos.

En el código anterior (4.2), new JdbcExecutionOptions.Builder().withBatchSize(200000).build() es la estrategia de escritura. ClickHouse recomienda escribir por lotes no menos de 1000 líneas para mejorar el rendimiento de escritura, o no más de una línea. por segundo solicitud de escritura. La estrategia es escribir 200 000 filas de registros una vez, y Flink también escribirá y confirmará cuando realice Checkpoint. Por lo tanto, cuando la cantidad de datos se acumule a 20 W o al punto de control de la memoria Flink, habrá datos en ClickHouse. Nuestra estrategia de sumidero de ES es de 1000 líneas o 5 segundos para el envío de escritura, por lo que escribir en ClickHouse es más lento que escribir en ES.

Hay una desventaja de enviar cuando se alcanzan los 20 W o el punto de control. Cuando la cantidad de datos es pequeña e inferior a 20 W, el intervalo de tiempo del punto de control es t1 y el tiempo del punto de control es t2. Entonces, el tiempo más largo desde que se recibe el mensaje JDQ hasta que se escribe en ClickHouse El intervalo es t1+t2, completamente dependiente del tiempo del punto de control, a veces la acumulación de datos más lenta es de 1 a 2 minutos. Luego optimice la estrategia de escritura de ClickHouse, el nuevo JdbcExecutionOptions.Builder().withBatchIntervalMs(30 * 1000).build() está optimizado para enviar una vez cada 30 s. De esta manera, si el punto de control es lento, se puede activar la estrategia de envío de los 30. De lo contrario, el envío en el momento del punto de control también es una estrategia relativamente comprometida, que se puede ajustar de acuerdo con las características propias del negocio. tiempo, se encuentra que si el intervalo es demasiado pequeño, la tasa de uso de la cpu de zookeeper aumentará, y la tasa de uso de zk aumentará de menos del 5% a aproximadamente el 10% cuando se envía una vez cada 10 segundos.

La lógica de procesamiento org.apache.flink.connector.jdbc.internal.JdbcBatchingOutputFormat#open en Flink se muestra en la siguiente figura.

6 Si escribir en una tabla distribuida o en una tabla local

Permítanme hablar sobre los resultados primero, estamos escribiendo en la tabla distribuida.
Tanto la información en línea como los colegas del servicio en la nube de ClickHouse sugieren escribir a la mesa local. Una tabla distribuida es en realidad una tabla lógica que no almacena datos físicos reales. Por ejemplo, si consulta una tabla distribuida, la tabla distribuida enviará la solicitud de consulta a la tabla local de cada fragmento para la consulta y luego recopilará los resultados de la tabla local de cada fragmento y los devolverá después de resumir. Al escribir en una tabla distribuida, la tabla distribuida almacenará los datos escritos en diferentes fragmentos según ciertas reglas. Si escribir en la tabla distribuida es solo un reenvío de red puro, el impacto no es grande, pero escribir en la tabla distribuida no es un reenvío puro. La situación real se muestra en la siguiente figura.

Hay tres fragmentos S1, S2 y S3, y el cliente se conecta al nodo S1 para escribir en la tabla distribuida.

  1. Paso 1: escribir 1000 piezas de datos en la tabla distribuida La tabla distribuida distribuirá 300 piezas de datos a S1, 200 piezas a S2 y 500 piezas a S3 de acuerdo con las reglas de enrutamiento.
  2. Paso 2: el cliente envía 1000 piezas de datos, y las 300 piezas de datos que pertenecen a S1 se escriben directamente en el disco, y los datos de S2 y S3 también se escriben en el directorio temporal de S1
  3. Paso 3: S2 y S3 reciben la notificación de cambio de zk, generan una tarea para extraer los datos del directorio temporal correspondientes al fragmento actual en S1, colocan la tarea en una cola y esperan cierto tiempo para extraer los datos por su cuenta. nodo.

Del método de escritura de la tabla distribuida, podemos ver que todos los datos se colocarán en el disco conectado al fragmento por el cliente. Si la cantidad de datos es grande, la E/S del disco provocará un cuello de botella. Además, los motores de la serie MergeTree tienen un comportamiento de fusión, que a su vez tiene amplificación de escritura (una parte de los datos se fusiona varias veces), lo que ocupa una cierta cantidad de rendimiento del disco. Vi en Internet que los casos de escritura en tablas locales son todos incrementos diarios de decenas de miles de millones, cientos de miles de millones. Hay dos razones principales por las que elegimos escribir en la tabla distribuida. Una es simple, porque escribir en la tabla local requiere modificar el código y especificar en qué nodo escribir. La otra es que no hay cuellos de botella serios en el rendimiento al escribir en la mesa local durante el proceso de desarrollo. Durante el período de Double Eleven, el aumento diario de filas de 3000 W (después de la consolidación) no causó presión de escritura. Si se produce un cuello de botella posterior, se puede abandonar la escritura en la tabla distribuida.

7 ¿Por qué solo un cierto fragmento tiene un alto uso de CPU?

7.1 La distribución desigual de datos conduce a una CPU alta en algunos nodos

La imagen de arriba es un problema encontrado en el proceso de conexión a ClickHouse, en el que el uso de la CPU del nodo 7-1 es muy alto y la diferencia entre los diferentes nodos es muy grande. Posteriormente, a través del posicionamiento SQL, se descubrió que la cantidad de datos en diferentes nodos también es muy diferente, y la cantidad de datos en el nodo 7-1 es la más grande, lo que da como resultado la cantidad de filas de datos que el nodo 7-1 necesita para El proceso es muy grande en comparación con otros nodos, por lo que la CPU será relativamente alta. Debido a que usamos el código del sitio de la cuadrícula, la estrategia de fragmentación de datos de la tabla distribuida después del código del contenedor de clasificación se codifica, pero la base del código del contenedor de clasificación y el código del sitio web es relativamente pequeña, lo que resulta en una dispersión insuficiente después del hash, lo que resulta en este fenómeno de sesgo de datos. Posteriormente se utilizó como hash la llave primaria del negocio, lo que resolvió el problema de alta CPU de algunos nodos.

7.2 Un nodo desencadena una fusión, lo que resulta en una alta CPU de este nodo

7-4 nodos (nodo maestro y réplica), la CPU es mucho más alta que otros nodos sin ningún signo, después de excluir emergencias como nuevos negocios en línea y grandes promociones, ubique SQL lento y analice la consulta lenta de cada nodo a través de query_log, vea la Sección 8 para oraciones específicas.

Al comparar el SQL lento de los dos nodos, se encuentra que las condiciones de consulta del siguiente SQL son bastante diferentes.

SELECT    ifNull(sum(t1.unTrackQty), 0) AS unTrackQtyFROM    wms.wms_order_sku_local AS t1 FINAL PREWHERE t1.shipmentOrderCreateTime > '2021-11-17 11:00:00'    AND t1.shipmentOrderCreateTime <= '2021-11-18 11:00:00'    AND t1.gridStationNo = 'WG0000514'    AND t1.warehouseNo NOT IN ('wms-6-979', 'wms-6-978', '6_979', '6_978')    AND t1.orderType = '10'WHERE    t1.ckDeliveryTaskStatus = '3'

Pero tenemos una duda, la misma declaración, la misma cantidad de ejecuciones, y no hay diferencia en la cantidad de datos y la cantidad de partes de los dos nodos, por qué la cantidad de filas escaneadas por el nodo 7-4 es 5 veces el del 7-0, y esta razón Si lo encuentra, debería poder localizar la causa raíz del problema.
A continuación, usamos clickhouse-client para realizar consultas SQL, habilitar registros de nivel de seguimiento y ver el proceso de ejecución de SQL. Para conocer los métodos de ejecución específicos y el análisis del registro de consultas, consulte la sección 9.1 a continuación. Aquí analizamos directamente los resultados.

Las dos cifras anteriores se pueden analizar

  • Nodo 7-0: escaneó archivos de partición de 4 partes, con un total de 94 W de líneas, y tardó 0,089 s
  • Nodo 7-4: escaneó archivos de partición de 2 partes, incluida una línea part491W, un total de líneas 502W y tomó 0.439s

Obviamente, la partición 202111_0_408188_322 del nodo 7-4 es anómala, porque estamos particionados por mes, y el nodo 7-4 no sabe por qué se fusionó la partición, lo que provocó que los datos que recuperamos el 17 de noviembre cayesen en esta gran partición. , por lo que la consulta filtrará todos los datos desde principios de noviembre hasta el 18, que es diferente del nodo 7-0. El SQL anterior se consulta a través de la condición gridStationNo = 'WG0000514', por lo que este problema se resuelve después de crear un índice secundario en el campo gridStationNo.

Después de agregar el índice secundario, nodo 7-4: escaneó archivos de partición de 2 partes, con un total de 38 W líneas, y tomó 0.103 s.

7.3 Fallo físico de la máquina

Esto es raro, pero sucedió una vez.

8 Cómo localizar qué SQL está consumiendo CPU

Creo que hay dos formas de solucionar el problema. Una es si la frecuencia de ejecución de SQL es demasiado alta y la otra es juzgar si hay una ejecución lenta de SQL. La ejecución frecuente o la consulta lenta consumirán una gran cantidad de recursos informáticos de la CPU. Los siguientes dos casos se utilizan para ilustrar dos métodos efectivos para solucionar problemas de CPU alta.Aunque los dos métodos siguientes tienen una operación diferente, el núcleo es analizar y localizar mediante el análisis de query_log.

8.1 grafana posicionamiento alta frecuencia ejecución SQL

Algunos requisitos se lanzaron en diciembre. Recientemente, se descubrió que la tasa de uso de la CPU era relativamente alta en comparación con la tasa de uso de la CPU. Es necesario verificar qué SQL están causando el problema.

Se puede ver en el monitoreo de grafana autoconstruido en la figura anterior (documentos de construcción) que varias declaraciones de consulta se ejecutan con una frecuencia muy alta.A través de SQL para ubicar la lógica del código de la interfaz de consulta, se encuentra que un front- solicitud de interfaz final y una interfaz de back-end ejecutarán múltiples condiciones similares.Instrucción SQL, pero el estado comercial es diferente. Este tipo de declaración que necesita contar diferentes tipos y diferentes estados puede optimizarse mediante la agregación condicional, que se describirá en detalle en la Sección 9.4.1. Después de la optimización, la frecuencia de ejecución de las declaraciones se reduce considerablemente.

8.2 Gran número de filas escaneadas/uso elevado de memoria: análisis query_log_all

La sección anterior decía que la alta frecuencia de ejecución de SQL conduce a un alto uso de la CPU. ¿Qué debo hacer si la frecuencia de ejecución de SQL es muy baja pero la CPU sigue siendo alta? La frecuencia de ejecución de SQL es baja y puede haber una gran cantidad de filas de datos escaneados, lo que consume una gran cantidad de recursos, como E/S de disco, memoria y CPU. En este caso, es necesario usar otro método para solucionar este problema. mal SQL (T ⌓T).

ClickHouse en sí tiene una tabla system.query_log, que se utiliza para registrar los registros de ejecución de todas las declaraciones. La siguiente figura muestra información de campo clave de la tabla

-- 创建query_log分布式表CREATE TABLE IF NOT EXISTS system.query_log_allON CLUSTER defaultAS system.query_logENGINE = Distributed(sht_ck_cluster_pro,system,query_log,rand());-- 查询语句select     -- 执行次数    count(), -- 平均查询时间    avg(query_duration_ms) avgTime,    -- 平均每次读取数据行数    floor(avg(read_rows)) avgRow,    -- 平均每次读取数据大小    floor(avg(read_rows) / 10000000) avgMB,    -- 具体查询语句    any(query),    -- 去除掉where条件,用户group by归类    substring(query, positionCaseInsensitive(query, 'select'), positionCaseInsensitive(query, 'from')) as queryLimitfrom system.query_log_all/system.query_logwhere event_date = '2022-01-21'  and type = 2group by queryLimitorder by avgRow desc;

query_log es una tabla local. Es necesario crear una tabla distribuida, consultar los registros de consulta de todos los nodos y luego ejecutar la declaración de análisis de consulta. El efecto de la ejecución se muestra en la siguiente figura. En la figura se puede ver que el el número promedio de filas escaneadas por varias declaraciones ha alcanzado el nivel de 100 millones. Puede haber problemas con esta declaración. Las declaraciones poco razonables, como índices y condiciones de consulta, se pueden analizar escaneando el número de filas. La alta CPU de un cierto nodo en 7.2 se resuelve localizando la declaración SQL problemática de esta manera y luego investigando más.

9 Cómo optimizar consultas lentas

La optimización de SQL de ClickHouse es relativamente simple y la mayor parte del tiempo de consulta se dedica a la E/S del disco. Puede consultar este pequeño experimento para comprenderlo. La dirección principal de optimización es reducir la cantidad de datos procesados ​​por una sola consulta de ClickHouse, es decir, reducir la E/S del disco. A continuación se presenta el método de análisis de consultas lentas, el método de optimización de sentencias de creación de tablas y algunas optimizaciones de sentencias de consulta.

9.1 Usar registros de servicio para análisis de consultas lentas

Aunque ClickHouse ha proporcionado EXPLAIN nativo para ver los planes de consulta después de la versión 20.6, la información proporcionada no es muy útil para optimizar SQL lento. Antes de la versión 20.6, podemos obtener más información utilizando el registro de servicio en segundo plano. Analizamos. En comparación con EXPLAIN, prefiero usar el método de visualización de registros de servicio para el análisis. Este método requiere el uso de clickhouse-client para ejecutar instrucciones SQL. Al final del artículo, hay un documento de entorno CK creado a través de Docker. La versión superior de EXPLAIN proporciona información detallada, como la cantidad de partes escaneadas por declaraciones SQL y la cantidad de filas de datos que ESTIMATE puede consultar. Para el uso de EXPLAIN, consulte la documentación oficial.
Utilice una consulta lenta para el análisis y localice el siguiente SQL lento a través de query_log_all en 8.2.

select    ifNull(sum(interceptLackQty), 0) as interceptLackQtyfrom wms.wms_order_sku_local final    prewhere productionEndTime = '2022-02-17 08:00:00'    and orderType = '10'where shipmentOrderDetailDeleted = '0'  and ckContainerDetailDeleted = '0'

Usando clickhouse-client, el parámetro send_logs_level especifica el nivel de registro como seguimiento.

clickhouse-client -h 地址 --port 端口 --user 用户名 --password 密码 --send_logs_level=trace

Ejecute el SQL lento anterior en el cliente y el servidor imprimirá el registro de la siguiente manera: el volumen del registro es grande y se omiten algunas filas sin afectar la integridad del registro general.

[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.036317 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Debug> executeQuery: (from 11.77.96.163:35988, user: bjwangjiangbo) select ifNull(sum(interceptLackQty), 0) as interceptLackQty from wms.wms_order_sku_local final prewhere productionEndTime = '2022-02-17 08:00:00' and orderType = '10' where shipmentOrderDetailDeleted = '0' and ckContainerDetailDeleted = '0'[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.037876 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> ContextAccess (bjwangjiangbo): Access granted: SELECT(orderType, interceptLackQty, productionEndTime, shipmentOrderDetailDeleted, ckContainerDetailDeleted) ON wms.wms_order_sku_local[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.038239 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Debug> wms.wms_order_sku_local (SelectExecutor): Key condition: unknown, unknown, and, unknown, unknown, and, and, unknown, unknown, and, and[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.038271 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Debug> wms.wms_order_sku_local (SelectExecutor): MinMax index condition: unknown, unknown, and, unknown, unknown, and, and, unknown, unknown, and, and[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.038399 [ 1340 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> wms.wms_order_sku_local (SelectExecutor): Not using primary index on part 202101_0_0_0_3[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.038475 [ 1407 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> wms.wms_order_sku_local (SelectExecutor): Not using primary index on part 202103_0_17_2_22[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.038491 [ 111 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> wms.wms_order_sku_local (SelectExecutor): Not using primary index on part 202103_18_20_1_22..................................省去若干行(此块含义为:在分区内检索有没有使用索引).................................................[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.039041 [ 1205 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> wms.wms_order_sku_local (SelectExecutor): Not using primary index on part 202202_1723330_1723365_7[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.039054 [ 159 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> wms.wms_order_sku_local (SelectExecutor): Not using primary index on part 202202_1723367_1723367_0[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.038928 [ 248 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> wms.wms_order_sku_local (SelectExecutor): Not using primary index on part 202201_3675258_3700711_1054[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.039355 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Debug> wms.wms_order_sku_local (SelectExecutor): Selected 47 parts by date, 47 parts by key, 9471 marks by primary key, 9471 marks to read from 47 ranges[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.039495 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 202101_0_0_0_3, approx. 65536 rows starting from 0[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.039583 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 202101_1_1_0_3, approx. 16384 rows starting from 0[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.040291 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 202102_0_2_1_4, approx. 146850 rows starting from 0..................................省去若干行(每个分区读取的数据行数信息).................................................[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.043538 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 202202_1723330_1723365_7, approx. 24576 rows starting from 0[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.043604 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 202202_1723366_1723366_0, approx. 8192 rows starting from 0[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.043677 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 202202_1723367_1723367_0, approx. 8192 rows starting from 0..................................完成数据读取,开始进行聚合计算.................................................[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.047880 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> InterpreterSelectQuery: FetchColumns -> Complete[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.263500 [ 1377 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> AggregatingTransform: Aggregating[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.263680 [ 1439 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> Aggregator: Aggregation method: without_key..................................省去若干行(数据读取完成后做聚合操作).................................................[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.263840 [ 156 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> AggregatingTransform: Aggregated. 12298 to 1 rows (from 36.03 KiB) in 0.215046273 sec. (57187.69187876137 rows/sec., 167.54 KiB/sec.)[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.264283 [ 377 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> AggregatingTransform: Aggregated. 12176 to 1 rows (from 35.67 KiB) in 0.215476999 sec. (56507.191284950095 rows/sec., 165.55 KiB/sec.)[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.264307 [ 377 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Trace> Aggregator: Merging aggregated data..................................完成聚合计算,返回最终结果.................................................┌─interceptLackQty─┐│              563 │└──────────────────┘...................................数据处理耗时,速度,信息展示................................................[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.265490 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Information> executeQuery: Read 73645604 rows, 1.20 GiB in 0.229100749 sec., 321455099 rows/sec., 5.22 GiB/sec.[chi-ck-t8ebn40kv7-3-0-0] 2022.02.17 21:21:54.265551 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} <Debug> MemoryTracker: Peak memory usage (for query): 60.37 MiB.1 rows in set. Elapsed: 0.267 sec. Processed 73.65 million rows, 1.28 GB (276.03 million rows/s., 4.81 GB/s.)

Ahora analice qué información se puede obtener del registro anterior. En primer lugar, la declaración de consulta no utiliza el índice de clave principal. La información específica es la siguiente

2022.02.17 21:21:54.038239 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} wms.wms_order_sku_local (SelectExecutor): condición clave: desconocido, desconocido y, desconocido, desconocido y, y, desconocido, desconocido, y y

El índice de partición tampoco se usa, la información específica es la siguiente

2022.02.17 21:21:54.038271 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} wms.wms_order_sku_local (SelectExecutor): condición de índice MinMax: desconocido, desconocido y, desconocido, desconocido y, y, desconocido, desconocido , y y

Esta consulta analiza un total de 36 partes y MarkRange 9390. Al consultar la tabla de información de partición del sistema system.parts, se encuentra que la tabla actual tiene un total de 36 particiones activas, lo que equivale a una exploración de tabla completa.

2022.02.17 21:44:58.012832 [ 1138 ] {f1561330-4988-4598-a95d-bd12b15bc750} wms.wms_order_sku_local (SelectExecutor): 36 partes seleccionadas por fecha, 36 partes por clave, 9390 marcas por clave principal, 9390 marcas para leer de 36 rangos

Se leyó un total de 73645604 filas de datos en esta consulta, que también es el número total de filas de datos en esta tabla. La lectura tardó 0,229100749 s y se leyó un total de 1,20 GB de datos.

2022.02.17 21:21:54.265490 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} executeQuery: Leer 73645604 filas, 1,20 GiB en 0,229100749 segundos, 321455099 filas/segundo ., 5,22 GiB/seg.

La memoria máxima consumida por esta instrucción de consulta es 60,37 MB

2022.02.17 21:21:54.265551 [ 618 ] {ea8f56fe-cf2b-4260-8f44-a006458bdab3} MemoryTracker: uso máximo de memoria (para consulta): 60,37 MiB.

Finalmente, se resume la siguiente información: esta consulta tomó un total de 0.267 s, procesó 7365 W de datos, un total de 1.28 GB y proporcionó la velocidad de procesamiento de datos.

1 filas en conjunto. Transcurrido: 0,267 seg. Procesó 73,65 millones de filas, 1,28 GB (276,03 millones de filas/s., 4,81 GB/s.)

Por lo anterior, se pueden encontrar dos serios problemas

  • No se utiliza el índice de clave principal: da como resultado un escaneo completo de la tabla
  • No se utilizan índices particionados: lo que da como resultado un análisis completo de la tabla

Por lo tanto, es necesario agregar un campo de clave principal o un índice de partición a la condición de consulta para la optimización.

shippingOrderCreateTime es la clave de partición, después de agregar esta condición, vea el efecto.

Al analizar el registro, podemos ver que no se usa el índice de clave principal, pero se usa el índice de partición, la cantidad de fragmentos de escaneo es 6, MarkRange 186, se escanea un total de 1,409,001 filas de datos, la memoria utilizada es 40.76 MB, y el tamaño de los datos escaneados se reduce considerablemente para ahorrar una gran cantidad de recursos del servidor.Y se mejora la velocidad de consulta, de 0,267 s a 0,18 s.

9.2 Optimización de la creación de tablas

9.2.1 Trate de no usar tipos anulables

Desde un punto de vista práctico, establecerlo en Nullable tiene poco impacto en el rendimiento, probablemente porque nuestro volumen de datos es relativamente pequeño. Sin embargo, el funcionario ha señalado claramente que trate de no usar el tipo Nullable, porque el campo Nullable no se puede indexar y la columna Nullable tiene un archivo adicional para almacenar la marca Null además de un archivo que almacena valores normales.

El uso de Nullable casi siempre afecta negativamente el rendimiento, tenga esto en cuenta al diseñar sus bases de datos.

CREATE TABLE test_Nullable(  orderNo String,  number Nullable(Int16),  createTime DateTime) ENGINE = MergeTree()PARTITION BY createTimeORDER BY  (orderNo)PRIMARY KEY (orderNo);

Tome la instrucción de creación de tabla anterior como ejemplo, la columna de número generará dos archivos adicionales number.null.*, ocupando espacio de almacenamiento adicional, mientras que la columna orderNo no tiene un archivo de almacenamiento adicional marcado con nulo.

En nuestra aplicación real, inevitablemente encontraremos campos que pueden ser nulos. En este caso, se puede usar un valor imposible como valor predeterminado. Por ejemplo, si el campo de estado es todo 0 o superior, entonces puede establecer Usar - 1 para el valor predeterminado en lugar de anulable.

9.2.2 Granularidad de partición

La granularidad de la partición se establece de acuerdo con las características del escenario empresarial y no debe ser demasiado gruesa ni demasiado fina. Nuestros datos generalmente se dividen estrictamente según el tiempo, por lo que se dividen en particiones por día y mes. Si la granularidad del índice es demasiado fina y se divide por minutos u horas, se generará una gran cantidad de directorios de partición, y mucho menos PARTICIÓN POR create_time directamente, lo que dará como resultado una cantidad asombrosamente grande de particiones.Casi todos los datos tienen una partición. , lo que afectará seriamente el rendimiento. Si la granularidad del índice es demasiado gruesa, el volumen de datos de una sola partición será relativamente grande. El problema de la sección 7.2 anterior también está relacionado con la granularidad del índice. Al particionar por mes, el volumen de datos de una sola partición alcanza los 500 W, y el rango de datos es de 1 a 18. Solo consulta la cantidad de datos para los dos días del 17 y el 18, pero la partición mensual está optimizada. Después de fusionar la partición, los datos adicionales del 1 al 16 que no son relevante tiene que ser procesado Si la partición se divide por día, no habrá aumento de CPU. Por lo tanto, es necesario crear de acuerdo con sus propias características comerciales y mantener el principio de que la consulta solo procesa datos dentro del alcance de esta condición de consulta y no procesa adicionalmente datos irrelevantes.

9.2.3 Selección de tabla distribuida de reglas de fragmentación apropiadas

Tomando el 7.1 anterior como ejemplo, las reglas de particionamiento seleccionadas por la tabla distribuida no son razonables, lo que da como resultado un sesgo de datos grave que cae a unos pocos fragmentos. En lugar de ejercer el poder de cómputo de todo el clúster de la base de datos distribuida, la presión se ejerce sobre una pequeña cantidad de máquinas. De esta manera, el rendimiento del clúster general definitivamente no mejorará, así que elija las reglas de fragmentación apropiadas de acuerdo con el escenario comercial. Por ejemplo, optimizamos sipHash64 (warehouseNo) a sipHash64 (docId), donde docId es el único identificador en el negocio.

9.3 Pruebas de rendimiento, comparación de los efectos de optimización

Antes de hablar sobre la optimización de consultas, permítanme hablar sobre una pequeña herramienta, una herramienta de prueba de rendimiento de referencia de Clickhouse proporcionada por Clickhouse. El entorno es el mismo que se mencionó anteriormente. El entorno CK se crea a través de la ventana acoplable. Los parámetros de prueba de presión pueden referirse a el documento oficial Aquí doy un ejemplo simple de prueba de concurrencia.

clickhouse-benchmark -c 1 -h 链接地址 --port 端口号 --user 账号 --password 密码 <<< "具体SQL语句"

De esta manera, puede comprender la información de QPS y TP99 a nivel de SQL, de modo que pueda probar la diferencia de rendimiento antes y después de la optimización de declaraciones.

9.4 Optimización de consultas

9.4.1 La función de agregación condicional reduce el número de filas de datos escaneados

Suponga que una interfaz quiere contar la "cantidad de piezas entrantes", la "cantidad de pedidos salientes efectivos" y la "cantidad de piezas revisadas" de un día determinado.

-- 入库件量select sum(qty) from table_1 final prewhere type = 'inbound' and dt = '2021-01-01';-- 有效出库单量select count(distinct orderNo) final from table_1 prewhere type = 'outbound' and dt = '2021-01-01' where and status = '1' ;-- 复核件量select sum(qty) from table_1 final prewhere type = 'check' and dt = '2021-01-01';

Para generar tres indicadores desde una interfaz, se requieren las tres declaraciones SQL anteriores para consultar la tabla_1 para completar, pero no es difícil encontrar que dt es consistente, la diferencia radica en las dos condiciones de tipo y estado. Suponiendo que dt = '2021-01-1' cada consulta necesita escanear filas de datos de 100 W, entonces una solicitud de interfaz escaneará filas de datos de 300 W. Después de optimizar la función de agregación condicional, las tres consultas se cambian a una y la cantidad de filas escaneadas se reducirá a filas de 100 W, por lo que los recursos informáticos del clúster se pueden ahorrar en gran medida.

select sumIf(qty, type = 'inbound'), -- 入库件量countIf(distinct orderNo, type = 'outbound' and status = '1'), -- 有效出库单量sumIf(qty, type = 'check') -- 复核件量prewhere dt = '2021-01-01';

La función de agregación condicional es relativamente flexible y se puede usar libremente de acuerdo con su propia situación comercial. Recuerde que uno de los propósitos es reducir el volumen de escaneo general y lograr el propósito de mejorar el rendimiento de las consultas.

9.4.2 Índice secundario

El motor de tablas de la serie MergeTree puede especificar el índice de salto.
El índice de conteo de saltos significa que después de que el segmento de datos se divide en pequeños bloques de acuerdo con la granularidad (index_granularity especificado al crear la tabla), los pequeños bloques del número de granularity_value se combinan en un bloque grande y la información del índice se escribe en estos grandes bloques, lo que es útil para usar Omitir una gran cantidad de datos innecesarios al filtrar dónde, lo que reduce la cantidad de datos que SELECT necesita leer.

CREATE TABLE table_name(    u64 UInt64,    i32 Int32,    s String,    ...    INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3,    INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4) ENGINE = MergeTree()...

El índice del ejemplo anterior permite a ClickHouse reducir la cantidad de datos leídos al ejecutar las siguientes consultas.

SELECT count() FROM table WHERE s < 'z'SELECT count() FROM table WHERE u64 * i32 == 10 AND u64 * length(s) >= 1234

Tipos de índice admitidos

  • minmax: En la unidad de granularidad del índice, almacena los valores mínimo y máximo calculados por la expresión especificada; puede ayudar a omitir rápidamente bloques que no cumplen con los requisitos y reducir IO en consultas de equivalencia y rango.
  • set (max_rows): en la unidad de granularidad del índice, almacene el conjunto de valores distintos de la expresión especificada, que se usa para juzgar rápidamente si la consulta equivalente golpea el bloque y reduce IO.
  • ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed): después de segmentar la cadena en ngrams, se puede crear un filtro de floración para optimizar las condiciones de consulta, como equivalente, me gusta e in.
  • tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed): Similar a ngrambf_v1, la diferencia es que no usa ngram para la segmentación de palabras, pero usa puntuación para la segmentación de palabras.
  • Bloom_filter([falso_positivo]): cree un filtro de floración para la columna especificada para acelerar la ejecución de las condiciones de consulta, como valor igual, me gusta y adentro.

Ejemplo de creación de un índice secundario

Alter table wms.wms_order_sku_local ON cluster default ADD INDEX belongProvinceCode_idx belongProvinceCode TYPE set(0) GRANULARITY 5;Alter table wms.wms_order_sku_local ON cluster default ADD INDEX productionEndTime_idx productionEndTime TYPE minmax GRANULARITY 5;

Reconstruir datos de índice de partición: los datos insertados antes de crear el índice secundario no pueden pasar por el índice secundario, y los datos de índice de cada partición deben reconstruirse para que surtan efecto.

-- 拼接出所有数据分区的MATERIALIZE语句select concat('alter table wms.wms_order_sku_local on cluster default ', 'MATERIALIZE INDEX productionEndTime_idx in PARTITION '||partition_id||',')from system.partswhere database = 'wms' and table = 'wms_order_sku_local'group by partition_id-- 执行上述SQL查询出的所有MATERIALIZE语句进行重建分区索引数据

9.4.3 Final reemplaza argMax para deduplicación

Compare la brecha de rendimiento entre los métodos final y argMax, como se muestra en el siguiente SQL

-- final方式select count(distinct groupOrderCode), sum(arriveNum), count(distinct sku) from tms.group_order final prewhere siteCode = 'WG0001544' and createTime >= '2022-03-14 22:00:00' and createTime <= '2022-03-15 22:00:00' where arriveNum > 0 and test <> '1'-- argMax方式select count(distinct groupOrderCode), sum(arriveNumTemp), count(distinct sku) from (select argMax(groupOrderCode,version) as groupOrderCode, argMax(arriveNum,version) as arriveNumTemp, argMax(sku,version) as sku from tms.group_order prewhere siteCode = 'WG0001544' and createTime >= '2022-03-14 22:00:00' and createTime <= '2022-03-15 22:00:00' where arriveNum > 0 and test <> '1' group by docId)

El modo final de TP99 es obviamente mucho mejor que el modo argMax

9.4.4 predonde en lugar de donde

La sintaxis de ClickHouse admite condiciones de filtro prewhere adicionales, que se evaluarán antes que las condiciones where, que pueden considerarse como un where más eficiente y su función es filtrar datos. Cuando la condición de filtro prewhere se agrega a la condición de filtro sql, el escaneo de almacenamiento se llevará a cabo en dos etapas. Primero, se lee el bloque de almacenamiento de valor de columna que depende de la expresión prewhere para verificar si hay un registro que cumpla con la condición. , y otras columnas que cumplen la condición Léalo, tome el siguiente SQL como ejemplo, donde el método prewhere escaneará primero los campos type y dt, y sacará las columnas que cumplan las condiciones.Cuando ningún registro cumple las condiciones, los datos en otras columnas se pueden omitir y no leer. Es equivalente a reducir aún más el rango de escaneo en base al rango de marcas. En comparación con where, prewhere procesará menos datos y tendrá un mayor rendimiento. Puede que no sea fácil de entender este pasaje,

-- 常规方式select count(distinct orderNo) final from table_1 where type = 'outbound' and status = '1' and dt = '2021-01-01';-- prewhere方式select count(distinct orderNo) final from table_1 prewhere type = 'outbound' and dt = '2021-01-01' where and status = '1' ;

En la sección anterior, hablamos sobre el uso de final para la optimización de la deduplicación. Use final para deduplicar y use prewhere para optimizar las condiciones de consulta. Hay una trampa a tener en cuenta. Prewhere se ejecutará antes que final. Por lo tanto, durante el procesamiento de campos con valores variables como estado, filas de datos en el intermedio se puede consultar el estado, lo que genera incoherencias en los datos finales.

Como se muestra en la figura anterior, los datos comerciales de docId: 123_1 se escriben tres veces, y los datos hasta la versión = 103 son los datos de la última versión. Cuando usamos where para filtrar el campo de valor variable de estado, los resultados de la declaración 1 y el enunciado 2 son los siguientes.

--语句1:使用where + status=1 查询,无法命中docId:123_1这行数据select count(distinct orderNo) final from table_1 where type = 'outbound' and dt = '2021-01-01' and status = '1';--语句2:使用where + status=2 查询,可以查询到docId:123_1这行数据select count(distinct orderNo) final from table_1 where type = 'outbound' and dt = '2021-01-01' and status = '2';

Después de introducir prewhere, el método de escritura de la declaración 3: cuando prewhere filtra el campo de estado, los datos con estado = 1 y versión = 102 se filtrarán, lo que generará resultados de consulta incorrectos. La forma correcta de escribir es la declaración 2, usando prewhere para optimizar los campos inmutables.

-- 语句3:错误方式,将status放到prewhereselect count(distinct orderNo) final from table_1 prewhere type = 'outbound' and dt = '2021-01-01' and status = '1';-- 语句4:正确prewhere方式,status可变字段放到where上select count(distinct orderNo) final from table_1 prewhere type = 'outbound' and dt = '2021-01-01' where and status = '1' ;

Otras restricciones: prewhere actualmente solo está disponible para motores de tablas de la serie MergeTree

9.4.5 Poda de columna, poda de partición

ClickHouse es muy adecuado para tablas anchas que almacenan grandes cantidades de datos, por lo que debemos evitar usar operaciones SELECT *, que son operaciones muy impactantes. Las columnas deben recortarse y seleccionar solo las columnas que necesita, porque cuantos menos campos, menos recursos de IO se consumirán y mayor será el rendimiento.
La eliminación de particiones consiste en leer solo las particiones requeridas y controlar el rango de consulta de los campos de partición.

9.4.6 donde, agrupar por orden

El orden de las columnas en where y group by debe ser coherente con el orden de las columnas en order by en la declaración de creación de la tabla y debe colocarse al principio para que tengan prefijos comunes continuos e ininterrumpidos; de lo contrario, el rendimiento de la consulta se verá afectado.

-- 建表语句create table group_order_local(    docId              String,    version            UInt64,    siteCode           String,    groupOrderCode     String,    sku                String,    ... 省略非关键字段 ...     createTime         DateTime) engine = ReplicatedReplacingMergeTree('/clickhouse/tms/group_order/{shard}', '{replica}', version)PARTITION BY toYYYYMM(createTime)ORDER BY (siteCode, groupOrderCode, sku);--查询语句1select count(distinct groupOrderCode) groupOrderQty, ifNull(sum(arriveNum),0) arriveNumSum,count(distinct sku) skuQtyfrom  tms.group_order finalprewhere createTime >= '2021-09-14 22:00:00' and createTime <= '2021-09-15 22:00:00'and siteCode = 'WG0000709'where arriveNum > 0 and test <> '1'--查询语句2 (where/prewhere中字段)select count(distinct groupOrderCode) groupOrderQty, ifNull(sum(arriveNum),0) arriveNumSum,count(distinct sku) skuQtyfrom  tms.group_order finalprewhere siteCode = 'WG0000709' and createTime >= '2021-09-14 22:00:00' and createTime <= '2021-09-15 22:00:00'where arriveNum > 0 and test <> '1'

La declaración de creación de tabla ORDER BY (siteCode, groupOrderCode, sku), la declaración 1 no cumplió con los requisitos y pasó la prueba de presión QPS6.4, TP99 0.56s, la declaración 2 cumplió con los requisitos y pasó la prueba de presión QPS 14.9, TP99 0.12s

10 Cómo resistir la alta concurrencia y garantizar la disponibilidad de ClickHouse

1) Reducir la velocidad de consulta y aumentar el rendimiento

max_threads: Ubicado en users.xml, indica la cantidad máxima de CPU que puede usar una sola consulta. El valor predeterminado es la cantidad de núcleos de CPU. Si la máquina es 32C, se iniciarán 32 hilos para procesar la solicitud actual. Puede reducir max_threads y sacrificar la velocidad de una sola consulta para garantizar la disponibilidad de ClickHouse y mejorar la concurrencia. Se puede configurar a través de la URL jdbc

La siguiente figura se basa en la configuración 32C128G. Con la condición de que el clúster de CK pueda proporcionar servicios estables y la tasa de uso de la CPU sea del 50 %, se realiza una prueba de presión para max_threads. La prueba de presión de nivel de interfaz ejecuta 5 SQL por solicitud y procesa 508W líneas de datos. Se puede observar que cuanto menor sea el max_threads, mejor será el QPS y peor el TP99. Puede ajustar un valor de configuración apropiado de acuerdo con sus propias condiciones comerciales.

2) La interfaz agrega un caché durante un cierto período de tiempo
3) La tarea asincrónica ejecuta la declaración de consulta, y los resultados del índice agregado se colocan en el ES, y la aplicación consulta los resultados agregados en el ES 4
) La vista materializada resuelve este problema a través de la agregación previa, pero nuestros escenarios comerciales no se aplican

11 recopilación de datos

• Operaciones comoconstruir una base de datos, crear una tabla, crear un índice secundario, etc.

•Cambiar el campo ORDENAR POR, PARTICIÓN POR, datos de copia de seguridad, datos de migración de tabla única y otras operaciones

• Cree un clúster de ck de enlace de clickhouse-clientebasado en docker

• Cree grafanabasado en docker para monitorear la ejecución de SQL

• Entornode prueba para construir clickhouse por sí mismo

Autor: JD Logística Ma Hongyan

Fuente de contenido: comunidad de desarrolladores de JD Cloud

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/9008112
Recomendado
Clasificación