sql >> Base de Datos >  >> RDS >> PostgreSQL

Rendimiento de aplicaciones basadas en PostgreSQL:latencia y retrasos ocultos

Oleoducto Goldfields, por SeanMac (Wikimedia Commons)

Si está intentando optimizar el rendimiento de su aplicación basada en PostgreSQL, probablemente se esté centrando en las herramientas habituales:EXPLICAR (BUFFERS, ANALYZE) , pg_stat_statements , explicación_automática , log_statement_min_duration , etc.

Tal vez esté investigando una disputa de bloqueo con log_lock_waits , monitorear el rendimiento de su punto de control, etc.

Pero, ¿pensaste en la latencia de red? ? Los jugadores conocen la latencia de la red, pero ¿pensaste que era importante para tu servidor de aplicaciones?

La latencia importa

Las latencias típicas de red de ida y vuelta de cliente/servidor pueden oscilar entre 0,01 ms (host local) y ~0,5 ms de una red conmutada, 5 ms de WiFi, 20 ms de ADSL, 300 ms de enrutamiento intercontinental e incluso más para elementos como enlaces satelitales y WWAN .

Un SELECT trivial puede tomar del orden de 0.1ms para ejecutarse del lado del servidor. Un INSERT trivial puede tardar 0,5 ms.

Cada vez que su aplicación ejecuta una consulta, tiene que esperar a que el servidor responda con éxito/fracaso y posiblemente un conjunto de resultados, metadatos de consulta, etc. Esto genera al menos un retraso de ida y vuelta en la red.

Cuando trabaja con consultas pequeñas y simples, la latencia de la red puede ser significativa en relación con el tiempo de ejecución de sus consultas si su base de datos no está en el mismo host que su aplicación.

Muchas aplicaciones, particularmente ORM, son muy propensas a ejecutar muchos de consultas bastante simples. Por ejemplo, si su aplicación Hibernate está recuperando una entidad con un @OneToMany obtenido de forma perezosa relación con 1000 elementos secundarios, probablemente hará 1001 consultas gracias al problema de selección n+1, si no más. Eso significa que probablemente esté gastando 1000 veces la latencia de ida y vuelta de su red simplemente esperando . Puede levantar unirse a la izquierda para evitar eso... pero luego transfiere la entidad principal 1000 veces en la unión y tiene que deduplicarla.

De manera similar, si está llenando la base de datos desde un ORM, probablemente esté haciendo cientos de miles de INSERT triviales. s... y esperando después de todos y cada uno para que el servidor confirme que está bien.

Es fácil tratar de concentrarse en el tiempo de ejecución de la consulta e intentar optimizarlo, pero no hay mucho que pueda hacer con un trivial INSERT INTO ...VALUES ... . Elimine algunos índices y restricciones, asegúrese de que esté incluido en una transacción y ya casi ha terminado.

¿Qué hay de deshacerse de todas las esperas de la red? Incluso en una LAN, comienzan a acumularse en miles de consultas.

COPIAR

Una forma de evitar la latencia es usar COPY . Para usar el soporte COPY de PostgreSQL, su aplicación o controlador debe producir un conjunto de filas similar a CSV y transmitirlas al servidor en una secuencia continua. O se le puede pedir al servidor que envíe su aplicación como un flujo CSV.

De cualquier manera, la aplicación no puede intercalar una COPIA con otras consultas, y las inserciones de copia deben cargarse directamente en una tabla de destino. Un enfoque común es COPIAR en una tabla temporal, luego desde allí haga un INSERTAR EN... SELECCIONAR... , ACTUALIZAR... DESDE.... , ELIMINAR DE... USANDO... , etc. para usar los datos copiados para modificar las tablas principales en una sola operación.

Eso es útil si está escribiendo su propio SQL directamente, pero muchos marcos de aplicaciones y ORM no lo admiten, además, solo puede reemplazar directamente el simple INSERT . Su aplicación, marco o controlador de cliente tiene que lidiar con la conversión para la representación especial que necesita COPY , busque cualquier metadato de tipo necesario, etc.

(Controladores notables que hacen apoyo COPIAR incluyen libpq, PgJDBC, psycopg2 y la gema Pg... pero no necesariamente los marcos y ORM creados sobre ellos).

PgJDBC:modo por lotes

El controlador JDBC de PostgreSQL tiene una solución para este problema. Se basa en el soporte presente en los servidores PostgreSQL desde 8.4 y en las funciones de procesamiento por lotes de la API de JDBC para enviar un lote de consultas al servidor, luego espere solo una vez para confirmar que todo el lote se ejecutó correctamente.

Bueno, en teoría. En realidad, algunos desafíos de implementación limitan esto, de modo que los lotes solo se pueden realizar en fragmentos de unos pocos cientos de consultas en el mejor de los casos. El controlador también puede ejecutar consultas que devuelvan filas de resultados en lotes por lotes solo si puede determinar qué tan grandes serán los resultados antes de tiempo. A pesar de esas limitaciones, el uso de Statement.executeBatch() puede ofrecer un gran aumento de rendimiento a las aplicaciones que realizan tareas como la carga masiva de datos en instancias de bases de datos remotas.

Debido a que es una API estándar, puede ser utilizada por aplicaciones que funcionan en múltiples motores de bases de datos. Hibernate, por ejemplo, puede utilizar el procesamiento por lotes de JDBC, aunque no lo hace de forma predeterminada.

libpq y procesamiento por lotes

La mayoría (¿todos?) de los demás controladores de PostgreSQL no admiten el procesamiento por lotes. PgJDBC implementa el protocolo PostgreSQL de forma completamente independiente, mientras que la mayoría de los demás controladores utilizan internamente la biblioteca C libpq que se proporciona como parte de PostgreSQL.

libpq no es compatible con el procesamiento por lotes. Tiene una API asíncrona sin bloqueo, pero el cliente solo puede tener una consulta "en curso" a la vez. Debe esperar hasta que se reciban los resultados de esa consulta antes de poder enviar otra.

El servidor de PostgreSQL admite el procesamiento por lotes muy bien, y PgJDBC ya lo usa. Así que he escrito soporte por lotes para libpq y lo envió como candidato para la próxima versión de PostgreSQL. Dado que solo cambia el cliente, si se acepta, seguirá acelerando las cosas cuando se conecte a servidores más antiguos.

Estaría realmente interesado en los comentarios de los autores y usuarios avanzados de libpq controladores de cliente y desarrolladores de libpq aplicaciones basadas en El parche se aplica bien sobre PostgreSQL 9.6beta1 si desea probarlo. La documentación es detallada y hay un programa de ejemplo completo.

Rendimiento

Pensé que un servicio de base de datos alojado como RDS o Heroku Postgres sería un buen ejemplo de dónde sería útil este tipo de funcionalidad. En particular, acceder a ellos desde nuestro lado, sus propias redes, realmente muestra cuánto puede doler la latencia.

Con una latencia de red de ~320 ms:

  • 500 inserciones sin procesamiento por lotes:167.0s
  • 500 inserciones con procesamiento por lotes:1.2s

… que es más de 120 veces más rápido.

Por lo general, no ejecutará su aplicación a través de un enlace intercontinental entre el servidor de la aplicación y la base de datos, pero esto sirve para resaltar el impacto de la latencia. Incluso en un socket de Unix para localhost vi una mejora del rendimiento de más del 50 % para 10 000 inserciones.

Lotes en aplicaciones existentes

Desafortunadamente, no es posible habilitar automáticamente el procesamiento por lotes para las aplicaciones existentes. Las aplicaciones tienen que usar una interfaz ligeramente diferente en la que envían una serie de consultas y solo luego solicitan los resultados.

Debería ser bastante sencillo adaptar las aplicaciones que ya usan la interfaz libpq asíncrona, especialmente si usan el modo sin bloqueo y un select() /encuesta() /epoll() /EsperarMúltiplesObjetosEx círculo. Aplicaciones que utilizan libpq síncrono las interfaces requerirán más cambios.

Lotes en otros controladores de clientes

De manera similar, los controladores de cliente, marcos y ORM generalmente necesitarán cambios internos y de interfaz para permitir el uso de procesamiento por lotes. Si ya están usando un bucle de eventos y E/S sin bloqueo, deberían ser bastante simples de modificar.

Me encantaría ver que los usuarios de Python, Ruby, etc. puedan acceder a esta funcionalidad, así que tengo curiosidad por ver quién está interesado. Imagina poder hacer esto:

import psycopg2
conn = psycopg2.connect(...)
cur = conn.cursor()

# this is just an idea, this code does not work with psycopg2:
futures = [ cur.async_execute(sql) for sql in my_queries ]
for future in futures:
    result = future.result  # waits if result not ready yet
    ... process the result ...
conn.commit()

La ejecución por lotes asíncrona no tiene por qué ser complicada a nivel de cliente.

COPIAR es el más rápido

Donde los clientes prácticos aún deberían preferir COPY . Aquí hay algunos resultados de mi computadora portátil:

inserting 1000000 rows batched, unbatched and with COPY
batch insert elapsed:      23.715315s
sequential insert elapsed: 36.150162s
COPY elapsed:              1.743593s
Done.

El trabajo por lotes proporciona un aumento de rendimiento sorprendentemente grande incluso en una conexión de socket local de Unix... pero COPIAR deja a ambos enfoques de insertos individuales muy atrás en el polvo.

Utilice COPIAR .

La imagen

La imagen de esta publicación es de la tubería del Esquema de suministro de agua de Goldfields desde Mundaring Weir cerca de Perth en Australia Occidental hasta los campos de oro del interior (desierto). Es relevante porque tardó tanto en terminarse y fue objeto de críticas tan intensas que su diseñador y principal defensor, C. Y. O'Connor, se suicidó 12 meses antes de que se pusiera en marcha. A nivel local, la gente suele decir (incorrectamente) que murió después la tubería se construyó cuando no fluía el agua, porque tomó tanto tiempo que todos asumieron que el proyecto de la tubería había fallado. Luego, semanas después, se derramó el agua.