Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast

En este artículo hablaremos sobre cómo y por qué desarrollamos Sistema de interacción - un mecanismo que transfiere información entre aplicaciones cliente y servidores 1C: Enterprise, desde establecer una tarea hasta pensar en la arquitectura y los detalles de implementación.

El Sistema de Interacción (en adelante, CB) es un sistema de mensajería distribuida tolerante a fallos con entrega garantizada. CB está diseñado como un servicio de alta carga con alta escalabilidad, disponible como servicio en línea (proporcionado por 1C) y como producto de producción en masa que puede implementarse en sus propias instalaciones de servidor.

SW utiliza almacenamiento distribuido Hazelcast y motor de búsqueda Elasticsearch. También hablaremos de Java y de cómo escalamos PostgreSQL horizontalmente.
Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Formulación del problema

Para dejar claro por qué creamos el Sistema de Interacción, les contaré un poco cómo funciona el desarrollo de aplicaciones comerciales en 1C.

Primero, un poco sobre nosotros para aquellos que aún no saben lo que hacemos :) Estamos desarrollando la plataforma tecnológica 1C:Enterprise. La plataforma incluye una herramienta de desarrollo de aplicaciones empresariales, así como un tiempo de ejecución que permite que las aplicaciones empresariales funcionen en un entorno multiplataforma.

Paradigma de desarrollo cliente-servidor

Las aplicaciones empresariales creadas en 1C:Enterprise funcionan en tres niveles Servidor de cliente arquitectura "DBMS - servidor de aplicaciones - cliente". Código de aplicación escrito en lenguaje incorporado 1C, puede ejecutarse en el servidor de aplicaciones o en el cliente. Todo el trabajo con los objetos de la aplicación (directorios, documentos, etc.), así como la lectura y escritura de la base de datos, se realiza únicamente en el servidor. La funcionalidad de interfaz de comandos y formularios también se implementa en el servidor. En el cliente se reciben, abren y muestran formularios, “comunicación” con el usuario (avisos, preguntas...), pequeños cálculos en formularios que requieren una respuesta rápida (por ejemplo, multiplicar el precio por la cantidad), trabajar con Archivos locales, trabajo con equipos.

En el código de la aplicación, los encabezados de los procedimientos y funciones deben indicar explícitamente dónde se ejecutará el código, utilizando las directivas &AtClient / &AtServer (&AtClient / &AtServer en la versión en inglés del idioma). Los desarrolladores de 1C ahora me corregirán diciendo que las directivas son en realidad más, pero para nosotros ahora no es importante.

Puede llamar al código del servidor desde el código del cliente, pero no puede llamar al código del cliente desde el código del servidor. Esta es una limitación fundamental, impuesta por nosotros por varias razones. En particular, porque el código del servidor debe escribirse de tal manera que se ejecute de la misma manera, sin importar desde dónde se llame: desde el cliente o desde el servidor. Y en el caso de una llamada al código de servidor desde otro código de servidor, no existe un cliente como tal. Y porque durante la ejecución del código del servidor, el cliente que lo llamó podría cerrarse, salir de la aplicación y el servidor no tendría a quién llamar.

Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast
Código que maneja un clic en un botón: llamar a un procedimiento de servidor desde el cliente funcionará, llamar a un procedimiento de cliente desde el servidor no

Esto significa que si queremos enviar algún mensaje desde el servidor a la aplicación cliente, por ejemplo, que la formación de un informe "de larga duración" ha finalizado y el informe se puede ver, no tenemos esa manera. Debe utilizar trucos, por ejemplo, sondear periódicamente el servidor a partir del código del cliente. Pero este enfoque carga el sistema con llamadas innecesarias y, en general, no parece muy elegante.

Y también es necesario, por ejemplo, cuando un teléfono SIP-llamar, notificar a la aplicación cliente sobre esto para que pueda encontrarlo en la base de datos de la contraparte por el número de la persona que llama y mostrar al usuario información sobre la contraparte que llama. O, por ejemplo, cuando llegue un pedido al almacén, notificarlo a la aplicación cliente del cliente. En general, hay muchos casos en los que un mecanismo de este tipo sería útil.

realmente configurando

Crea un mecanismo de mensajería. Rápido, confiable, con entrega garantizada, con posibilidad de búsqueda flexible de mensajes. Según el mecanismo, implemente un mensajero (mensajes, videollamadas) que funcione dentro de las aplicaciones 1C.

Diseñar el sistema horizontalmente escalable. Una carga creciente debería cubrirse aumentando el número de nodos.

implementación

Decidimos no integrar la parte del servidor CB directamente en la plataforma 1C:Enterprise, sino implementarla como un producto separado, cuya API se puede llamar desde el código de las soluciones de la aplicación 1C. Esto se hizo por varias razones, la principal de las cuales fue permitir el intercambio de mensajes entre diferentes aplicaciones 1C (por ejemplo, entre el Departamento de Comercio y Contabilidad). Diferentes aplicaciones 1C pueden ejecutarse en diferentes versiones de la plataforma 1C:Enterprise, ubicarse en diferentes servidores, etc. En tales condiciones, la solución óptima es implementar CB como un producto separado, ubicado "en el costado" de las instalaciones 1C.

Entonces, decidimos hacer CB como un producto separado. Para empresas más pequeñas, recomendamos utilizar el servidor CB que instalamos en nuestra nube (wss://1cdialog.com) para evitar la sobrecarga asociada con la instalación y configuración del servidor local. Los grandes clientes, sin embargo, pueden considerar conveniente instalar en sus instalaciones su propio servidor CB. Utilizamos un enfoque similar en nuestro producto SaaS en la nube. 1cfresco – se lanza como un producto de producción para que lo instalen los clientes y también se implementa en nuestra nube https://1cfresh.com/.

solicitud

Para la distribución de carga y la tolerancia a fallas, implementaremos no una aplicación Java, sino varias, y les colocaremos un equilibrador de carga delante. Si necesita enviar un mensaje de un nodo a otro, utilice publicar/suscribir en Hazelcast.

Comunicación entre el cliente y el servidor - a través de websocket. Es muy adecuado para sistemas en tiempo real.

Caché distribuido

Elija entre Redis, Hazelcast y Ehcache. Afuera en 2015. Redis acaba de lanzar un nuevo clúster (demasiado nuevo, aterrador), hay un Sentinel con muchas restricciones. Ehcache no sabe cómo agruparse (esta funcionalidad apareció más tarde). Decidimos probar con Hazelcast 3.4.
Hazelcast está agrupado fuera de la caja. En el modo de un solo nodo, no es muy útil y solo puede funcionar como caché: no sabe cómo volcar datos al disco; si se pierde el único nodo, los datos se pierden. Implementamos varios Hazelcasts, entre los cuales realizamos copias de seguridad de datos críticos. No hacemos una copia de seguridad del caché, no sentimos lástima por él.

Para nosotros, Hazelcast es:

  • Almacenamiento de sesiones de usuarios. Se necesita mucho tiempo para acceder a la base de datos para una sesión, por lo que colocamos todas las sesiones en Hazelcast.
  • Cache. Buscando un perfil de usuario: consulte el caché. Escribí un mensaje nuevo; guárdalo en el caché.
  • Temas para la comunicación de instancias de aplicaciones. El nodo genera un evento y lo coloca en un tema de Hazelcast. Otros nodos de aplicación suscritos a este tema reciben y procesan el evento.
  • cerraduras de racimo. Por ejemplo, creamos una discusión mediante una clave única (discusión-singleton en el marco de la base 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Comprobamos que no hay ningún canal. Tomaron la cerradura, la revisaron nuevamente y la crearon. Si no verifica después de tomar el bloqueo, existe la posibilidad de que otro hilo también haya verificado en ese momento y ahora intente crear la misma discusión, y ya existe. Es imposible realizar un bloqueo mediante un bloqueo java sincronizado o normal. A través de la base, lentamente, y la base es una pena, a través de Hazelcast, lo que necesitas.

Elegir un DBMS

Tenemos una amplia y exitosa experiencia con PostgreSQL y cooperación con los desarrolladores de este DBMS.

Con un clúster, PostgreSQL no es fácil: hay XL, XC, citus, pero, en general, no es noSQL lo que se escala de forma inmediata. NoSQL no se consideró como el almacenamiento principal, fue suficiente que tomáramos Hazelcast, con el que no habíamos trabajado antes.

Dado que necesita escalar una base de datos relacional, significa fragmentación. Como sabe, al fragmentar, dividimos la base de datos en partes separadas para que cada una de ellas pueda colocarse en un servidor separado.

La primera versión de nuestro sharding asumió la capacidad de distribuir cada una de las tablas de nuestra aplicación a diferentes servidores en diferentes proporciones. Hay muchos mensajes en el servidor A. Movamos parte de esta tabla al servidor B. Esta decisión simplemente gritaba sobre una optimización prematura, por lo que decidimos limitarnos a un enfoque multiinquilino.

Puede leer sobre multiinquilino, por ejemplo, en el sitio web Datos de Citus.

En SV existen conceptos de aplicación y suscriptor. Una aplicación es una instalación específica de una aplicación empresarial, como ERP o Contabilidad, con sus usuarios y datos empresariales. Un suscriptor es una organización o un individuo en cuyo nombre se registra la aplicación en el servidor CB. Un suscriptor puede tener varias aplicaciones registradas y estas aplicaciones pueden intercambiar mensajes entre sí. El suscriptor se convirtió en inquilino de nuestro sistema. Los mensajes de varios suscriptores pueden ubicarse en una base física; Si vemos que algún suscriptor ha comenzado a generar mucho tráfico, lo movemos a una base de datos física separada (o incluso a un servidor de base de datos separado).

Contamos con una base de datos principal donde se almacena la tabla de enrutamiento con información sobre la ubicación de todas las bases de datos de suscriptores.

Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Para evitar que la base de datos principal sea un cuello de botella, mantenemos la tabla de enrutamiento (y otros datos solicitados con frecuencia) en la caché.

Si la base de datos del suscriptor comienza a ralentizarse, la cortaremos en particiones internas. En otros proyectos, para particionar tablas grandes, usamos pg_pathman.

Dado que perder mensajes de usuarios es malo, realizamos copias de seguridad de nuestras bases de datos con réplicas. La combinación de réplicas sincrónicas y asincrónicas le permite asegurarse contra la pérdida de la base de datos principal. La pérdida de mensajes ocurrirá solo en caso de una falla simultánea de la base de datos principal y su réplica sincrónica.

Si se pierde la réplica sincrónica, la réplica asincrónica se vuelve sincrónica.
Si se pierde la base de datos principal, la réplica sincrónica se convierte en la base de datos principal y la réplica asincrónica se convierte en una réplica sincrónica.

Elasticsearch para buscar

Dado que, entre otras cosas, CB también es un mensajero, aquí necesitamos una búsqueda rápida, cómoda y flexible, teniendo en cuenta la morfología, de coincidencias inexactas. Decidimos no reinventar la rueda y utilizar el motor de búsqueda gratuito Elasticsearch, creado a partir de la biblioteca. Lucene. También implementamos Elasticsearch en un clúster (maestro - datos - datos) para eliminar problemas en caso de falla de los nodos de la aplicación.

En github encontramos Complemento de morfología rusa para Elasticsearch y úselo. En el índice de Elasticsearch, almacenamos raíces de palabras (que define el complemento) y N-gramas. Cuando el usuario ingresa texto para buscar, buscamos el texto escrito entre N-gramas. Cuando se guarde en el índice, la palabra "textos" se dividirá en los siguientes N-gramas:

[te, tech, tex, text, texts, ek, eks, ext, exts, ks, kst, ksty, st, sty, you],

Y también se guardará la raíz de la palabra "texto". Este enfoque le permite buscar al principio, en el medio y al final de la palabra.

Imagen general

Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast
Repitiendo la imagen del principio del artículo, pero con explicaciones:

  • Balancer expuesto a Internet; Tenemos nginx, puede ser cualquiera.
  • Las instancias de aplicaciones Java se comunican entre sí a través de Hazelcast.
  • Para trabajar con un socket web, utilizamos Netty.
  • Aplicación Java escrita en Java 8, consta de paquetes. OSGi. Los planes son migrar a Java 10 y cambiar a módulos.

Desarrollo y pruebas

Durante el desarrollo y las pruebas del CB, encontramos una serie de características interesantes de los productos que utilizamos.

Pruebas de carga y pérdidas de memoria.

La liberación de cada versión CB es una prueba de carga. Pasó exitosamente cuando:

  • La prueba funcionó durante varios días y no hubo denegaciones de servicio.
  • El tiempo de respuesta para operaciones clave no superó un umbral cómodo
  • La degradación del rendimiento en comparación con la versión anterior no supera el 10%

Llenamos la base de datos de prueba con datos; para esto, obtenemos información sobre el suscriptor más activo del servidor de producción, multiplicamos sus números por 5 (la cantidad de mensajes, discusiones, usuarios) y así lo probamos.

Realizamos pruebas de carga del sistema de interacción en tres configuraciones:

  1. prueba de estrés
  2. Solo conexiones
  3. Registro de suscriptor

Durante una prueba de estrés, lanzamos varios cientos de hilos y cargan el sistema sin parar: escriben mensajes, crean discusiones, reciben una lista de mensajes. Simulamos las acciones de usuarios comunes (obtener una lista de mis mensajes no leídos, escribirle a alguien) y decisiones de programa (transferir un paquete a otra configuración, procesar una alerta).

Por ejemplo, así es como se ve parte de la prueba de estrés:

  • El usuario inicia sesión
    • Solicita tus hilos no leídos
    • 50% de posibilidades de leer mensajes
    • 50% de posibilidades de escribir mensajes
    • Siguiente usuario:
      • 20% de posibilidades de crear un nuevo hilo
      • Selecciona aleatoriamente cualquiera de sus discusiones.
      • viene adentro
      • Solicita mensajes, perfiles de usuario.
      • Crea cinco mensajes dirigidos a usuarios aleatorios de este hilo.
      • Fuera de discusión
      • Se repite 20 veces
      • Cierra la sesión y vuelve al principio del script.

    • Un chatbot ingresa al sistema (emula la mensajería a partir del código de las soluciones aplicadas)
      • 50% de probabilidad de crear un nuevo canal de datos (discusión especial)
      • 50% de posibilidades de escribir un mensaje en cualquiera de los canales existentes.

El escenario "Sólo conexiones" apareció por una razón. Hay una situación: los usuarios han conectado el sistema, pero aún no han participado. Cada usuario por la mañana a las 09:00 enciende la computadora, establece una conexión con el servidor y guarda silencio. Estos tipos son peligrosos, hay muchos: solo tienen PING / PONG de los paquetes, pero mantienen la conexión con el servidor (no pueden mantenerla, y de repente aparece un nuevo mensaje). La prueba reproduce la situación en la que un gran número de estos usuarios intenta iniciar sesión en el sistema en media hora. Parece una prueba de estrés, pero se centra precisamente en esta primera entrada, para que no haya fallas (una persona no usa el sistema, pero ya se está cayendo; es difícil encontrar algo peor).

El escenario de registro de suscriptores se origina desde el primer lanzamiento. Realizamos una prueba de estrés y estuvimos seguros de que el sistema no se ralentizaba en la correspondencia. Pero los usuarios fueron y el registro comenzó a fallar por tiempo de espera. Al registrarnos utilizamos / dev / random, que está ligado a la entropía del sistema. El servidor no tuvo tiempo de acumular suficiente entropía y, cuando se solicitó un nuevo SecureRandom, se congeló durante decenas de segundos. Hay muchas formas de salir de esta situación, por ejemplo: cambiar a un /dev/urandom menos seguro, instalar una placa especial que genere entropía, generar números aleatorios por adelantado y almacenarlos en el grupo. Cerramos temporalmente el problema con el grupo, pero desde entonces hemos estado realizando una prueba separada para registrar nuevos suscriptores.

Como generador de carga utilizamos JMeter. No sabe trabajar con un websocket, se necesita un complemento. Los primeros en los resultados de búsqueda para la consulta "jmeter websocket" son artículos con BlazeMeteren el que recomiendan complemento de Maciej Zaleski.

Ahí es donde decidimos empezar.

Casi inmediatamente después del inicio de pruebas serias, descubrimos que comenzaron las pérdidas de memoria en JMeter.

El complemento es una gran historia separada, con 176 estrellas tiene 132 bifurcaciones en github. El propio autor no se ha comprometido con él desde 2015 (lo tomamos en 2015, luego no despertó sospechas), varios problemas de github sobre pérdidas de memoria, 7 solicitudes de extracción no cerradas.
Si elige cargar la prueba con este complemento, tenga en cuenta las siguientes discusiones:

  1. En un entorno de subprocesos múltiples, se utilizó la LinkedList habitual, como resultado, obtuvimos NPE en tiempo de ejecución. Se resuelve cambiando a ConcurrentLinkedDeque o mediante bloques sincronizados. Elegimos la primera opción para nosotros.https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Pérdida de memoria, la información de conexión no se elimina al desconectarse (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. En el modo de transmisión (cuando el websocket no se cierra al final de la muestra, pero se usa más en el plan), los patrones de respuesta no funcionan (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Este es uno de los que hay en github. Lo que hicimos:

  1. Han tomado tenedor de Elyran Kogan (@elyrank): soluciona los problemas 1 y 3
  2. Problema resuelto 2
  3. Embarcadero actualizado del 9.2.14 al 9.3.12
  4. SimpleDateFormat envuelto en ThreadLocal; SimpleDateFormat no es seguro para subprocesos y conduce a NPE en tiempo de ejecución
  5. Se corrigió otra pérdida de memoria (la conexión se cerró incorrectamente al desconectarse)

¡Y sin embargo fluye!

La memoria empezó a acabarse no en un día, sino en dos. No hubo tiempo en absoluto, decidimos ejecutar menos hilos, pero con cuatro agentes. Esto debería haber sido suficiente para al menos una semana.

Han pasado dos días...

Ahora Hazelcast se está quedando sin memoria. Los registros mostraron que después de un par de días de pruebas, Hazelcast comienza a quejarse de falta de memoria y, después de un tiempo, el clúster se desmorona y los nodos continúan muriendo uno por uno. Conectamos JVisualVM a Hazelcast y vimos la "sierra hacia arriba": regularmente llamaba al GC, pero no podía borrar la memoria de ninguna manera.

Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Resultó que en Hazelcast 3.4, al eliminar un mapa/multiMap (map.destroy()), la memoria no se libera por completo:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

El error ahora está solucionado en 3.5, pero era un problema en aquel entonces. Creamos un nuevo mapa múltiple con nombres dinámicos y lo eliminamos de acuerdo con nuestra lógica. El código se parecía a esto:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Llamar:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap se creó para cada suscripción y se eliminó cuando no era necesario. Decidimos que comenzaríamos un Mapa. , la clave será el nombre de la suscripción y los valores serán identificadores de sesión (mediante los cuales podrá obtener identificadores de usuario, si es necesario).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Los gráficos han mejorado.

Cómo y por qué escribimos un servicio escalable altamente cargado para 1C: Enterprise: Java, PostgreSQL, Hazelcast

¿Qué más hemos aprendido sobre las pruebas de carga?

  1. JSR223 debe estar escrito en maravilloso e incluir caché de compilación; es mucho más rápido. Enlace.
  2. Los gráficos de Jmeter-Plugins son más fáciles de entender que los estándar. Enlace.

Sobre nuestra experiencia con Hazelcast

Hazelcast era un producto nuevo para nosotros, comenzamos a trabajar con él desde la versión 3.4.1, ahora tenemos la versión 3.9.2 en nuestro servidor de producción (al momento de escribir este artículo, la última versión de Hazelcast es 3.10).

generación de identificación

Comenzamos con identificadores enteros. Imaginemos que necesitamos otro Long para una nueva entidad. La secuencia en la base de datos no es adecuada, las tablas están involucradas en la fragmentación; resulta que hay un ID de mensaje = 1 en DB1 y un ID de mensaje = 1 en DB2, no puede colocar este ID en Elasticsearch, ni tampoco en Hazelcast. pero lo peor es si quieres reducir los datos de dos bases de datos a una (por ejemplo, decidir que una base de datos es suficiente para estos suscriptores). Puede tener varios AtomicLongs en Hazelcast y mantener el contador allí, luego el rendimiento de obtener una nueva ID es incrementaAndGet más el tiempo para consultar en Hazelcast. Pero Hazelcast tiene algo más óptimo: FlakeIdGenerator. Al contactar, a cada cliente se le proporciona un rango de ID, por ejemplo, el primero, de 1 a 10 000, el segundo, de 10 001 a 20 000, y así sucesivamente. Ahora el cliente puede emitir nuevos identificadores por sí solo hasta que finalice el rango emitido. Funciona rápido, pero al reiniciar la aplicación (y el cliente Hazelcast) se inicia una nueva secuencia, de ahí los saltos, etc. Además, los desarrolladores no tienen muy claro por qué los ID son números enteros, pero están muy en desacuerdo. Pesamos todo y cambiamos a UUID.

Por cierto, para aquellos que quieran ser como Twitter, existe una biblioteca Snowcast: esta es una implementación de Snowflake sobre Hazelcast. Puedes ver aquí:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Pero aún no hemos llegado a eso.

TransactionalMap.reemplazar

Otra sorpresa: TransactionalMap.replace no funciona. Aquí tienes una prueba:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Tuve que escribir mi propio reemplazo usando getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Pruebe no sólo las estructuras de datos habituales, sino también sus versiones transaccionales. Sucede que IMap funciona, pero TransactionalMap ya no existe.

Adjunte un nuevo JAR sin tiempo de inactividad

Primero, decidimos escribir objetos de nuestras clases en Hazelcast. Por ejemplo, tenemos una clase de Aplicación, queremos almacenarla y leerla. Ahorrar:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Leer:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Todo está funcionando. Luego decidimos crear un índice en Hazelcast para buscarlo:

map.addIndex("subscriberId", false);

Y al escribir una nueva entidad, comenzaron a recibir una ClassNotFoundException. Hazelcast intentó agregar algo al índice, pero no sabía nada sobre nuestra clase y quería poner un JAR con esta clase. Hicimos precisamente eso, todo funcionó, pero apareció un nuevo problema: ¿cómo actualizar el JAR sin detener completamente el clúster? Hazelcast no recoge un nuevo JAR en una actualización por nodo. En este punto, decidimos que podíamos vivir sin búsquedas de índices. Después de todo, si usa Hazelcast como almacén de valores clave, ¿todo funcionará? No precisamente. Aquí nuevamente, comportamiento diferente de IMap y TransactionalMap. Cuando a IMap no le importa, TransactionalMap arroja un error.

IMap. Anotamos 5000 objetos, leemos. Se espera todo.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Pero no funciona en una transacción, obtenemos una ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

En 3.8, apareció el mecanismo de implementación de clases de usuario. Puede designar un nodo maestro y actualizar el archivo JAR en él.

Ahora hemos cambiado completamente nuestro enfoque: nosotros mismos serializamos en JSON y guardamos en Hazelcast. Hazelcast no necesita conocer la estructura de nuestras clases y podemos actualizarlas sin tiempo de inactividad. La aplicación controla el control de versiones de los objetos del dominio. Se pueden iniciar diferentes versiones de la aplicación al mismo tiempo, y es posible que una nueva aplicación escriba objetos con nuevos campos, mientras que la anterior aún no conoce estos campos. Y al mismo tiempo, la nueva aplicación lee los objetos escritos por la aplicación antigua que no tienen campos nuevos. Manejamos este tipo de situaciones dentro de la aplicación, pero por simplicidad no cambiamos ni eliminamos los campos, solo ampliamos las clases agregando nuevos campos.

Cómo ofrecemos alto rendimiento

Cuatro viajes a Hazelcast son buenos, dos viajes a la base de datos son malos

Siempre es mejor buscar datos en la caché que en la base de datos, pero tampoco conviene almacenar registros no reclamados. Decidir qué almacenar en caché se deja para la última etapa del desarrollo. Cuando se codifica la nueva funcionalidad, habilitamos el registro de todas las consultas en PostgreSQL (log_min_duration_statement a 0) y ejecutamos pruebas de carga durante 20 minutos. Utilidades como pgFouine y pgBadger pueden crear informes analíticos basados ​​en los registros recopilados. En los informes, buscamos principalmente consultas lentas y frecuentes. Para consultas lentas, creamos un plan de ejecución (EXPLICAR) y evaluamos si dicha consulta se puede acelerar. Las solicitudes frecuentes de la misma entrada encajan bien en la memoria caché. Intentamos mantener las consultas "planas", una tabla por consulta.

explotación

CB como servicio en línea se lanzó en la primavera de 2017, mientras que en noviembre de 2017 se lanzó un producto CB independiente (en ese momento en estado beta).

Durante más de un año de funcionamiento, no ha habido problemas graves en el funcionamiento del servicio CB online. Monitorizamos el servicio online a través de Zabbix, recopilar y desplegar desde Bamboo.

La distribución del servidor CB viene en forma de paquetes nativos: RPM, DEB, MSI. Además, para Windows, proporcionamos un único instalador en forma de un único EXE que instala el servidor, Hazelcast y Elasticsearch en una máquina. Al principio llamamos a esta versión de la instalación "demostración", pero ahora ha quedado claro que esta es la opción de implementación más popular.

Fuente: habr.com

Añadir un comentario