Optimización para PostgreSQL que sirve la aplicación Rails

Como ingeniero de software sénior en la empresa que desarrolla la plataforma de mensajería para la industria de la salud, soy responsable, incluidas otras tareas, del rendimiento de nuestra aplicación. Desarrollamos un servicio web bastante estándar utilizando la aplicación Ruby on Rails para lógica de negocios y API, React + Redux para la aplicación de una sola página de los usuarios, como base de datos que usamos PostgreSQL. Las razones comunes para los problemas de rendimiento en pilas similares son consultas pesadas a la base de datos y me gustaría contar la historia de cómo aplicamos optimizaciones no estándar pero bastante simples para mejorar el rendimiento.


Nuestro negocio opera en los EE. UU., Por lo que debemos cumplir con HIPAA y seguir ciertas políticas de seguridad, la auditoría de seguridad es algo para lo que siempre estamos preparados. Para reducir los riesgos y los costos, confiamos en un proveedor especial de la nube para ejecutar nuestras aplicaciones y bases de datos, muy similar a lo que hace Heroku. Por un lado, nos permite centrarnos en la construcción de nuestra plataforma, pero por otro lado agrega una limitación adicional a nuestra infraestructura. Hablando en breve: no podemos escalar infinitamente. Como un inicio exitoso, duplicamos el número de usuarios cada pocos meses y un día nuestro monitoreo nos dijo que estábamos excediendo la cuota de E / S del disco en el servidor de la base de datos. AWS subyacente comenzó a estrangularse, lo que resultó en una degradación significativa del rendimiento. La aplicación Ruby no era capaz de atender todo el tráfico entrante porque los trabajadores de Unicorn estaban esperando demasiado tiempo para recibir la respuesta de la base de datos, los clientes no estaban contentos.


Soluciones estándar


Al comienzo del artículo, mencioné la frase "optimizaciones no estándar" porque todas las frutas bajas ya fueron recogidas:


  • Eliminamos todas las consultas N + 1. Ruby gem Bullet fue la herramienta principal
  • se agregaron todos los índices necesarios en la base de datos, se eliminaron todos los no necesarios, gracias a pg_stat_statements
  • Algunas consultas con múltiples combinaciones fueron reescritas para una mejor eficiencia
  • separamos las consultas para obtener colecciones paginadas de las consultas de decoración. Por ejemplo, inicialmente agregamos un contador de mensajes por diálogo uniendo tablas, pero se reemplazó con una consulta adicional para aumentar los resultados. La siguiente consulta hace Index Only Scan y es realmente barata:

SELECT COUNT(1), dialog_id FROM messages WHERE dialog_id IN (1, 2, 3, 4) GROUP BY dialog_id; 

  • Agregó algunas cachés. En realidad, esto no funcionó bien porque como aplicación de mensajería tenemos muchas actualizaciones

Todos estos trucos hicieron un gran trabajo durante un par de meses hasta que volvimos a enfrentar el mismo problema de rendimiento: más usuarios, mayor carga. Estábamos buscando algo más.


Soluciones avanzadas


No queríamos usar artillería pesada e implementar la desnormalización y el particionamiento porque estas soluciones requieren un conocimiento profundo en las bases de datos, cambiar el enfoque del equipo de implementar características al mantenimiento y al final queríamos evitar la complejidad en nuestra aplicación. Por último, utilizamos PostgreSQL 9.3 donde las particiones se basan en disparadores con todos sus costos. Principio de KISS en acción.


Soluciones personalizadas


Comprimir datos


Decidimos centrarnos en el síntoma principal: el disco IO. A medida que almacenamos menos datos, a medida que necesitamos menos capacidad de E / S, esta fue la idea principal. Comenzamos a buscar oportunidades para comprimir datos y los primeros candidatos fueron columnas como user_type provistas de asociaciones polimórficas por ActiveRecord. En la aplicación utilizamos muchos módulos que nos llevan a tener cadenas largas como Module::SubModule::ModelName para asociaciones polimórficas. Lo que hicimos: convertir todos los tipos de columnas de varchar a ENUM. La migración de Rails se ve así:


 class AddUserEnumType < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up ActiveRecord::Base.connection.execute <<~SQL CREATE TYPE user_type_enum AS ENUM ( 'Module::Submodule::UserModel1', 'Module::Submodule::UserModel2', 'Module::Submodule::UserModel3' ); SQL add_column :messages, :sender_type_temp, :user_type_enum Message .in_batches(of: 10000) .update_all('sender_type_temp = sender_type::user_type_enum') safety_assured do rename_column :messages, :sender_type, :sender_type_old rename_column :messages, :sender_type_temp, :sender_type end end end 

Algunas notas sobre esta migración para personas que no están familiarizadas con Rails:


  • disable_ddl_transaction! deshabilita la migración transaccional. Es muy arriesgado hacerlo, pero queríamos evitar largas transacciones. Asegúrese de no deshabilitar las transacciones en la migración sin necesidad de hacerlo.
  • En el primer paso creamos un nuevo tipo de datos ENUM en PostgreSQL. La mejor característica de ENUM es un tamaño pequeño, muy pequeño en comparación con varchar. ENUM tiene algunas dificultades para agregar nuevos valores, pero generalmente no agregamos nuevos tipos de usuarios a menudo.
  • agregue una nueva columna sender_type_temp con user_type_enum
  • complete los valores en la nueva columna in_batches para evitar un bloqueo prolongado en los mensajes de la tabla
  • el último paso intercambia la columna antigua por una nueva. Este es el paso más peligroso porque si la columna sender_type se convirtiera en sender_type_old pero sender_type_temp no se convirtiera en sender_type tendríamos muchos problemas.
  • safety_assured proviene de la gema strong_migration que ayuda a evitar errores al escribir migraciones. Cambiar el nombre de la columna no es una operación segura, por lo que tuvimos que confirmar que entendemos lo que estábamos haciendo. En realidad, hay una forma más segura pero más larga que incluye múltiples implementaciones.

No es necesario decir que ejecutamos todas las migraciones similares durante los períodos de actividad más bajos con las pruebas adecuadas.


Convertimos todas las columnas polimórficas en ENUM, eliminamos las columnas viejas después de unos días de monitoreo y finalmente ejecutamos VACUUM para disminuir la fragmentación. Esto nos ahorró aproximadamente el 10% del espacio total en disco, pero
¡algunas tablas con algunas columnas se comprimieron dos veces! Lo que era más importante: PostgreSQL comenzó a almacenar en caché algunas tablas en la memoria (recuerde, no podemos agregar fácilmente más RAM) y esto disminuyó drásticamente la E / S de disco requerida.


No confíes en tu proveedor de servicios


Se encontró otra cosa en el artículo Cómo un solo cambio de configuración de PostgreSQL mejoró el rendimiento de la consulta lenta en 50 veces : nuestro proveedor de PostgreSQL realiza una configuración automática para el servidor en función del volumen solicitado de RAM, Disco y CPU, pero por cualquier motivo dejaron el parámetro random_page_cost con el valor predeterminado que es 4 optimizado para HDD. Nos cobran por ejecutar bases de datos en SSD pero no configuraron PostgreSQL correctamente. Después de contactarlos, obtuvimos mejores planes de ejecución:


 EXPLAIN ANALYSE SELECT COUNT(*) AS count_dialog_id, dialog_id as dialog_id FROM messages WHERE sender_type = 'Module::Submodule::UserModel1' AND sender_id = 1234 GROUP BY dialog_id; db=# SET random_page_cost = 4; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- HashAggregate (cost=76405.45..76411.92 rows=647 width=12) (actual time=2428.412..2428.420 rows=12 loops=1) Group Key: dialog_id -> Bitmap Heap Scan on messages (cost=605.90..76287.72 rows=23545 width=4) (actual time=82.442..2376.033 rows=79466 loops=1) Recheck Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Heap Blocks: exact=23672 -> Bitmap Index Scan on index_messages_on_sender_id_and_sender_type_and_message_type (cost=0.00..600.01 rows=23545 width=0) (actual time=76.067..76.068 rows=79466 loops=1) Index Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Planning time: 3.849 ms Execution time: 2428.691 ms (9 rows) db=# SET random_page_cost = 1; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ HashAggregate (cost=21359.54..21366.01 rows=647 width=12) (actual time=97.905..97.913 rows=12 loops=1) Group Key: dialog_id -> Index Scan using index_messages_on_sender_id_and_sender_type_and_message_type on messages (cost=0.56..21241.81 rows=23545 width=4) (actual time=0.058..60.444 rows=79466 loops=1) Index Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Planning time: 0.277 ms Execution time: 98.070 ms (6 rows) 

Alejar datos


Movimos una tabla enorme a otra base de datos. Tenemos que mantener auditorías de cada cambio en el sistema por ley y este requisito se implementa con gem PaperTrail . Esta biblioteca crea una tabla en la base de datos de producción donde se guardan todos los cambios de objetos bajo supervisión. Utilizamos la biblioteca multiverse para integrar otra instancia de base de datos a nuestra aplicación Rails. Por cierto, va a ser una característica estándar de Rails 6. Hay algunas configuraciones:


Describa la conexión en el archivo config/database.yml


 external_default: &external_default url: "<%= ENV['AUDIT_DATABASE_URL'] %>" external_development: <<: *external_default 

Clase base para modelos ActiveRecord de otra base de datos:


 class ExternalRecord < ActiveRecord::Base self.abstract_class = true establish_connection :"external_#{Rails.env}" end 

Modelo que implementa versiones de PaperTrail:


 class ExternalVersion < ExternalRecord include PaperTrail::VersionConcern end 

Caso de uso en el modelo bajo auditoría:


 class Message < ActiveRecord::Base has_paper_trail class_name: "ExternalVersion" end 

Resumen


Finalmente agregamos más RAM a nuestra instancia de PostgreSQL y actualmente consumimos solo el 10% del disco disponible IO. Sobrevivimos hasta este punto porque aplicamos algunos trucos: datos comprimidos en nuestra base de datos de producción, corregimos la configuración y eliminamos los datos no relevantes. Probablemente, esto no ayudará en su caso particular, pero espero que este artículo pueda dar algunas ideas sobre la optimización simple y personalizada. Por supuesto, no olvide pasar por la lista de verificación de problemas estándar enumerados al principio.


PD: recomiendo un gran complemento DBA para psql .

Source: https://habr.com/ru/post/445426/


All Articles