Para todos aquellos que están inquietos ansiosos, permítanme revelar el secreto profundo y oscuro de todas las optimizaciones de rendimiento en FTS en 6.5.0 es,
" Haga menos y menos a menudo ** "
Y estoy seguro de que estará de acuerdo conmigo cuando se desplace más abajo.
(Para los que se perdieron - Parte 1)
Recopilación dispersa sobre gRPC
Normalmente, un índice FTS se divide en subíndices y las particiones del índice se distribuyen entre los nodos de un clúster de varios nodos. Cualquier consulta de búsqueda entrante típica se dispersa a todos esos nodos/particiones de índice remotas y los resultados de la búsqueda se recogen de diferentes nodos, se fusionan por rango y se envían de vuelta al usuario por el nodo coordinador o el nodo de gestión de consultas.
Hasta el lanzamiento de la versión 6.5.0, toda la dispersión en FTS se realiza a través de http/http2. Alejarse de esta recopilación dispersa basada en REST era un tema pendiente desde hace mucho tiempo en el backlog del equipo. Procrastinar nos ha beneficiado en este caso.
Con la aparición de las funciones de la versión 6.5.0, como la integración de N1QL-FTS, nos hemos dado cuenta de que ha llegado el momento de experimentar con gRPC para la recopilación interna de datos dispersos. Dado que ya se ha hablado mucho sobre los pros de gRPC en toda la web, me abstendré de repetirlo aquí.
Con FTS, nos interesaba sobre todo explorar las capacidades de streaming y multiplexación de peticiones de gRPC. La transmisión de los resultados de búsqueda a los clientes ayuda a resolver el caso de uso de servir un resultado de búsqueda con millones de resultados no clasificados (clasificados de forma constante) de una forma más respetuosa con la memoria.
Desafíos interesantes fueron,
- Obtener las interfaces correctas para permitir que el paquete cbft de nivel superior escriba los resultados de la búsqueda en el flujo del cliente de forma configurable (p. ej.: lote de aciertos o un solo acierto) a medida que los iteradores de la biblioteca de indexación recorren los documentos coincidentes en algún lugar profundo del motor de almacenamiento.
- Definición de los tipos de mensajes protobuf adecuados en el contexto de los tipos de mensajes existentes.
Conseguir el mensaje protobuf adecuado no fue muy sencillo, ya que FTS ya contaba con los tipos de mensaje json establecidos, que estaban estrechamente entretejidos con los tipos Golang internos y todos nuestros clientes los utilizan. Por lo tanto, cualquier nuevo formato de mensaje tenía que tener en cuenta los tipos de mensajes heredados. Esto introduce un nivel extra de esfuerzos de marshal/unmarshal entre esos tipos Go y los nuevos tipos protobuf. Y esto, de hecho, activó el botón de pánico durante las pruebas iniciales debido a las abolladuras significativas en nuestros gráficos de rendimiento.
Inmediatamente abordamos ese problema reduciendo los gastos generales de análisis del mensaje de búsqueda heredado del nuevo tipo de mensaje protobuf. La idea era incrustar todo el mensaje de búsqueda heredado como una porción de byte dentro del nuevo mensaje proto, lo que eliminaba algunos rodeos y creaba mucha menos basura.
Resultado
La recolección dispersa basada en gRPC ha mostrado un 2X para consultas de términos de baja frecuencia en nuestras pruebas internas con un clúster de 3 nodos.
Consulta de rango numérico
La biblioteca de indexación de textos que utilizamos (bleve) almacena los valores de campo numérico en diferentes puntos de precisión, es decir, un único valor de campo numérico se indexa como múltiples tokens codificados en binario desplazado. Aunque este enfoque aumenta el tamaño del índice, a la hora de realizar una consulta, el resultado es un menor número de términos que buscar para un rango numérico determinado, lo que agiliza enormemente las consultas.
Oportunidad
Similar al modus operandi de las consultas geográficas mencionadas en Parte 1 de esta serie, la implementación de la consulta de rango numérico también deriva matemáticamente los términos numéricos exactos (términos candidatos) que se van a buscar. La lógica de filtrado de términos candidatos utilizada para la consulta de rango numérico es similar a la de las consultas difusas/regex. Intentará filtrar los términos candidatos mediante la intersección de los dos DFAs, es decir, la intersección entre el Diccionario de Términos y el del DFA/FST temporal construido a partir de los rangos numéricos creados matemáticamente. Sin embargo, a partir de los perfiles del grafo golang flame, este enfoque resultó ser menos eficiente en memoria y CPU.
Hemos mejorado esta lógica de filtrado con una API de búsqueda de diccionarios de términos más limpia y ligera que ayudó a controlar el uso de memoria y cpu. Y esto eliminó esos picos adicionales que aparecían en los gráficos de llama.
Resultado
Este cambio ha provocado 77% Mejoras de rendimiento en las pruebas de rendimiento internas del FTS, que tuvieron un flujo constante de mutaciones entrantes (10.000 conjuntos/seg).
Prefijo/Consultas difusas/Cartas comodín
Oportunidad
Todos estos tipos de consulta hacen un uso intensivo de los recorridos FST. Las consultas de prefijos aprovechan los iteradores FST basados en rangos, mientras que las consultas comodín/fuzzy utilizan los iteradores basados en autómatas, es decir, un autómata regex para las consultas comodín/regex y un autómata levenshtein para las consultas fuzzy.
Identificamos las estructuras compartibles de forma concurrente en estas consultas y las almacenamos en caché en un segmento para mejorar la reutilización de las consultas. Este método redujo en gran medida la cantidad de basura creada y nos ayudó a ahorrar ciclos de GC.
Resultado
La mejora del almacenamiento en caché y la reutilización ayudaron a mejorar las cifras de rendimiento de -.
Comodín de 25%, Fuzzy by 12%, Prefijo por 9%.
{** Sólo hay tres optimizaciones: "Haz menos. Hazlo menos a menudo. Hazlo más rápido".
Las mayores ganancias proceden de la 1, pero dedicamos todo nuestro tiempo a la 3.- Michael Fromberger}