Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Neste artigo falaremos de como e por que nos desenvolvemos Sistema de interacción – un mecanismo que transfire información entre as aplicacións cliente e os servidores 1C:Enterprise, desde a definición dunha tarefa ata a reflexión sobre a arquitectura e os detalles de implementación.

O sistema de interacción (en diante denominado SV) é un sistema de mensaxería distribuído tolerante a fallos con entrega garantida. SV está deseñado como un servizo de alta carga con alta escalabilidade, dispoñible tanto como un servizo en liña (proporcionado por 1C) como como un produto producido en masa que se pode despregar nas súas propias instalacións de servidor.

SV usa almacenamento distribuído avellana e buscador Elasticsearch. Tamén falaremos de Java e de como escalamos horizontalmente PostgreSQL.
Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Declaración de problemas

Para que quede claro por que creamos o Sistema de interacción, falareivos un pouco sobre como funciona o desenvolvemento de aplicacións empresariais en 1C.

Para comezar, un pouco de nós para os que aínda non saben o que facemos :) Estamos a facer a plataforma tecnolóxica 1C:Enterprise. A plataforma inclúe unha ferramenta de desenvolvemento de aplicacións empresariais, así como un tempo de execución que permite que as aplicacións empresariais se executen nun ambiente multiplataforma.

Paradigma de desenvolvemento cliente-servidor

As aplicacións empresariais creadas en 1C:Enterprise funcionan en tres niveis cliente-servidor arquitectura “DBMS – servidor de aplicacións – cliente”. Código de aplicación escrito linguaxe 1C incorporado, pódese executar no servidor de aplicacións ou no cliente. Todo o traballo con obxectos da aplicación (directorios, documentos, etc.), así como a lectura e escritura da base de datos, realízase só no servidor. A funcionalidade de formularios e interface de comandos tamén está implementada no servidor. O cliente realiza a recepción, apertura e visualización de formularios, “comunicación” co usuario (avisos, preguntas...), pequenos cálculos en formularios que requiren unha resposta rápida (por exemplo, multiplicando o prezo por cantidade), traballo con ficheiros locais, traballar con equipos.

No código da aplicación, as cabeceiras dos procedementos e funcións deben indicar explícitamente onde se executará o código, utilizando as directivas &AtClient / &AtServer (&AtClient / &AtServer na versión inglesa do idioma). Os desenvolvedores de 1C agora corrixiránme dicindo que as directivas son realmente máis que, pero para nós isto non é importante agora.

Podes chamar ao código do servidor desde o código do cliente, pero non podes chamar ao código do cliente desde o código do servidor. Esta é unha limitación fundamental que fixemos por varias razóns. En particular, porque o código do servidor debe escribirse de forma que se execute da mesma forma sen importar onde se chame: dende o cliente ou dende o servidor. E no caso de chamar ao código do servidor desde outro código do servidor, non hai cliente como tal. E porque durante a execución do código do servidor, o cliente que o chamou podería pecharse, saír da aplicación e o servidor xa non tería a quen chamar.

Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast
Código que manexa un clic de botón: chamar a un procedemento de servidor desde o cliente funcionará, chamar a un procedemento de cliente dende o servidor non

Isto significa que se queremos enviar algunha mensaxe desde o servidor á aplicación cliente, por exemplo, que rematou a xeración dun informe "de longa duración" e se pode ver o informe, non dispoñemos deste método. Ten que usar trucos, por exemplo, sondear periodicamente o servidor desde o código do cliente. Pero este enfoque carga o sistema con chamadas innecesarias e, en xeral, non parece moi elegante.

E tamén hai unha necesidade, por exemplo, cando chega unha chamada telefónica SIP- ao facer unha chamada, notifícalo á aplicación cliente para que poida utilizar o número da persoa que chama para atopalo na base de datos da contraparte e mostrarlle ao usuario a información sobre a contraparte que chama. Ou, por exemplo, cando un pedido chega ao almacén, notifícalo á aplicación cliente do cliente. En xeral, hai moitos casos nos que tal mecanismo sería útil.

A produción en si

Crear un mecanismo de mensaxería. Rápido, fiable, con entrega garantida, coa capacidade de buscar mensaxes de forma flexible. En función do mecanismo, implementa un mensaxeiro (mensaxes, videochamadas) que se executa dentro das aplicacións 1C.

Deseña o sistema para que sexa escalable horizontalmente. A carga crecente debe cubrirse aumentando o número de nós.

Implantación

Decidimos non integrar a parte do servidor de SV directamente na plataforma 1C:Enterprise, senón implementala como un produto separado, cuxa API se pode chamar desde o código das solucións de aplicación 1C. Fíxose por varias razóns, a principal das cales quería facer posible o intercambio de mensaxes entre diferentes aplicacións 1C (por exemplo, entre Xestión comercial e Contabilidade). Diferentes aplicacións 1C poden executarse en diferentes versións da plataforma 1C:Enterprise, estar situadas en diferentes servidores, etc. En tales condicións, a implementación de SV como un produto separado situado "ao lado" das instalacións 1C é a solución óptima.

Entón, decidimos facer SV como un produto separado. Recomendamos que as pequenas empresas utilicen o servidor CB que instalamos na nosa nube (wss://1cdialog.com) para evitar os gastos xerais asociados á instalación e configuración local do servidor. Os grandes clientes poden considerar recomendable instalar o seu propio servidor CB nas súas instalacións. Usamos un enfoque similar no noso produto SaaS na nube 1cFresco – prodúcese como un produto producido en masa para a instalación nos sitios dos clientes e tamén se desprega na nosa nube https://1cfresh.com/.

App

Para distribuír a tolerancia á carga e a fallos, non implementaremos unha aplicación Java, senón varias, cun equilibrador de carga diante delas. Se precisas transferir unha mensaxe de nodo a nodo, utiliza publish/subscribe en Hazelcast.

A comunicación entre o cliente e o servidor realízase mediante websocket. É moi axeitado para sistemas en tempo real.

Caché distribuída

Escollemos entre Redis, Hazelcast e Ehcache. É 2015. Redis acaba de lanzar un novo clúster (demasiado novo, asustado), hai Sentinel con moitas restricións. Ehcache non sabe como montar nun clúster (esta funcionalidade apareceu máis tarde). Decidimos probalo con Hazelcast 3.4.
Hazelcast está montado nun racimo fóra da caixa. No modo de nodo único, non é moi útil e só se pode usar como caché: non sabe como volcar datos ao disco, se perde o único nodo, pérdese os datos. Implementamos varios Hazelcasts, entre os que realizamos copias de seguridade dos datos críticos. Non facemos unha copia de seguridade da caché, non nos importa.

Para nós, Hazelcast é:

  • Almacenamento de sesións de usuario. Leva moito tempo ir á base de datos para unha sesión cada vez, polo que poñemos todas as sesións en Hazelcast.
  • Caché. Se está a buscar un perfil de usuario, comprobe a caché. Escribiu unha mensaxe nova: colócaa na caché.
  • Temas para a comunicación entre instancias de aplicación. O nodo xera un evento e colócao no tema Hazelcast. Outros nodos de aplicación subscritos a este tema reciben e procesan o evento.
  • Bloqueos de cluster. Por exemplo, creamos unha discusión usando unha clave única (discusión individual dentro da base de datos 1C):

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

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

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

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

Comprobamos que non hai canle. Collemos o peche, revisámolo de novo e creámolo. Se non verificas o bloqueo despois de tomar o bloqueo, entón existe a posibilidade de que outro fío tamén se comprobe nese momento e agora intente crear a mesma discusión, pero xa existe. Non podes bloquear usando o bloqueo java sincronizado ou normal. A través da base de datos - é lento, e é unha mágoa para a base de datos; a través de Hazelcast - é o que necesitas.

Selección dun DBMS

Temos unha ampla e exitosa experiencia traballando con PostgreSQL e colaborando cos desenvolvedores deste DBMS.

Non é doado cun clúster PostgreSQL, hai XL, XC, Citus, pero en xeral estes non son NoSQL que escalan fóra da caixa. Non consideramos NoSQL como o almacenamento principal; abondou con que tomamos Hazelcast, co que non traballaramos antes.

Se precisa escalar unha base de datos relacional, isto significa fragmentación. Como sabedes, co sharding dividimos a base de datos en partes separadas para que cada unha delas poida colocarse nun servidor separado.

A primeira versión do noso sharding asumiu a capacidade de distribuír cada unha das táboas da nosa aplicación en diferentes servidores en diferentes proporcións. Hai moitas mensaxes no servidor A; por favor, movemos parte desta táboa ao servidor B. Esta decisión simplemente gritaba sobre unha optimización prematura, polo que decidimos limitarnos a un enfoque multi-inquilino.

Podes ler sobre multi-inquilino, por exemplo, no sitio web Citus Data.

SV ten os conceptos de aplicación e subscritor. Unha aplicación é unha instalación específica dunha aplicación empresarial, como ERP ou Contabilidade, cos seus usuarios e datos empresariais. Un subscritor é unha organización ou individuo en cuxo nome a aplicación está rexistrada no servidor SV. Un subscritor pode ter varias aplicacións rexistradas e estas aplicacións poden intercambiar mensaxes entre si. O abonado converteuse nun inquilino no noso sistema. As mensaxes de varios subscritores pódense localizar nunha base de datos física; se vemos que un subscritor comezou a xerar moito tráfico, movémolo a unha base de datos física separada (ou incluso a un servidor de bases de datos separado).

Temos unha base de datos principal onde se almacena unha táboa de enrutamento con información sobre a localización de todas as bases de datos de subscritores.

Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Para evitar que a base de datos principal sexa un pescozo de botella, mantemos a táboa de enrutamento (e outros datos que se necesitan con frecuencia) nunha caché.

Se a base de datos do abonado comeza a ralentizarse, cortarémola en particións no seu interior. Noutros proxectos que utilizamos pg_pathman.

Dado que perder mensaxes dos usuarios é malo, mantemos as nosas bases de datos con réplicas. A combinación de réplicas síncronas e asíncronas permítelle asegurarse en caso de perda da base de datos principal. A perda de mensaxes só ocorrerá se a base de datos principal e a súa réplica síncrona fallan simultáneamente.

Se se perde unha réplica síncrona, a réplica asíncrona pasa a ser síncrona.
Se se perde a base de datos principal, a réplica síncrona pasa a ser a base de datos principal e a réplica asíncrona pasa a ser unha réplica síncrona.

Elasticsearch para busca

Dado que, entre outras cousas, SV tamén é un mensaxeiro, require unha busca rápida, cómoda e flexible, tendo en conta a morfoloxía, utilizando coincidencias imprecisas. Decidimos non reinventar a roda e utilizar o buscador gratuíto Elasticsearch, creado a partir da biblioteca Lucena. Tamén implantamos Elasticsearch nun clúster (mestre – datos – datos) para eliminar problemas en caso de falla dos nodos da aplicación.

En github atopamos Plugin de morfoloxía rusa para Elasticsearch e utilízao. No índice Elasticsearch almacenamos raíces de palabras (que determina o complemento) e N-gramas. A medida que o usuario introduce texto para buscar, buscamos o texto escrito entre N-gramos. Cando se garde no índice, a palabra "textos" dividirase nos seguintes N-gramos:

[esos, tek, tex, texto, textos, ek, ex, ext, textos, ks, kst, ksty, st, sty, ti],

E tamén se conservará a raíz da palabra "texto". Este enfoque permítelle buscar ao principio, no medio e ao final da palabra.

Imaxe xeral

Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast
Repita a imaxe do comezo do artigo, pero con explicacións:

  • Balancer exposto en Internet; temos nginx, pode ser calquera.
  • As instancias de aplicacións Java comunícanse entre si a través de Hazelcast.
  • Para traballar cun socket web que usamos Netty.
  • A aplicación Java está escrita en Java 8 e consta de paquetes OSGi. Os plans inclúen a migración a Java 10 e a transición a módulos.

Desenvolvemento e probas

No proceso de desenvolvemento e proba do SV, atopamos unha serie de características interesantes dos produtos que usamos.

Probas de carga e fugas de memoria

O lanzamento de cada versión SV implica probas de carga. Ten éxito cando:

  • A proba funcionou durante varios días e non houbo fallos no servizo
  • O tempo de resposta para as operacións clave non superou un limiar cómodo
  • O deterioro do rendemento en comparación coa versión anterior non supera o 10%

Enchemos a base de datos de proba con datos; para iso, recibimos información sobre o subscritor máis activo do servidor de produción, multiplicamos os seus números por 5 (o número de mensaxes, discusións, usuarios) e probamos así.

Realizamos probas de carga do sistema de interacción en tres configuracións:

  1. proba de esforzo
  2. Só conexións
  3. Rexistro de abonado

Durante a proba de esforzo, lanzamos varios centos de fíos, e eles cargan o sistema sen parar: escribir mensaxes, crear discusións, recibir unha lista de mensaxes. Simulamos as accións dos usuarios comúns (obter unha lista das miñas mensaxes non lidas, escribir a alguén) e solucións de software (transmitir un paquete cunha configuración diferente, procesar unha alerta).

Por exemplo, isto é o aspecto da parte da proba de esforzo:

  • O usuario inicia sesión
    • Solicita as túas discusións non lidas
    • 50 % de probabilidades de ler mensaxes
    • 50 % de probabilidades de enviar mensaxes de texto
    • Próximo usuario:
      • Ten un 20 % de posibilidades de crear unha nova discusión
      • Selecciona aleatoriamente calquera das súas discusións
      • Vai dentro
      • Mensaxes de solicitudes, perfís de usuario
      • Crea cinco mensaxes dirixidas a usuarios aleatorios desta discusión
      • Deixa o debate
      • Repítese 20 veces
      • Pecha sesión, volve ao inicio do guión

    • Un chatbot entra no sistema (emula a mensaxería do código da aplicación)
      • Ten un 50 % de posibilidades de crear unha nova canle para o intercambio de datos (debate especial)
      • 50 % de probabilidades de escribir unha mensaxe en calquera das canles existentes

O escenario "Só conexións" apareceu por un motivo. Hai unha situación: os usuarios conectaron o sistema, pero aínda non se involucraron. Cada usuario acende o ordenador ás 09:00 da mañá, establece unha conexión co servidor e permanece en silencio. Estes mozos son perigosos, hai moitos deles: os únicos paquetes que teñen son PING/PONG, pero manteñen a conexión co servidor (non poden mantelo; que pasa se hai unha mensaxe nova). A proba reproduce unha situación na que un gran número destes usuarios tentan iniciar sesión no sistema en media hora. É semellante a unha proba de esforzo, pero o seu foco está precisamente nesta primeira entrada - para que non haxa fallos (unha persoa non usa o sistema, e xa se cae - é difícil pensar en algo peor).

O script de rexistro de subscritores comeza desde o primeiro lanzamento. Levamos a cabo unha proba de esforzo e estabamos seguros de que o sistema non se ralentizaba durante a correspondencia. Pero chegaron os usuarios e o rexistro comezou a fallar debido a un tempo de espera. Ao rexistrar utilizamos / dev / random, que está relacionado coa entropía do sistema. O servidor non tivo tempo para acumular entropía suficiente e cando se solicitou un novo SecureRandom, conxelouse durante decenas de segundos. Hai moitas maneiras de saír desta situación, por exemplo: cambiar ao /dev/urandom menos seguro, instalar un taboleiro especial que xere entropía, xerar números aleatorios con antelación e almacenalos nun pool. Pechamos temporalmente o problema coa piscina, pero dende entón realizamos unha proba separada para rexistrar novos subscritores.

Usamos como xerador de carga JMeter. Non sabe como traballar con websocket; precisa un complemento. Os primeiros resultados da busca para a consulta "jmeter websocket" son: artigos de BlazeMeter, que recomenda plugin de Maciej Zaleski.

Por aí decidimos comezar.

Case inmediatamente despois de comezar as probas serias, descubrimos que JMeter comezou a perder memoria.

O complemento é unha gran historia separada; con 176 estrelas, ten 132 forks en github. O propio autor non se comprometeu con iso desde 2015 (tomámolo en 2015, despois non levantou sospeitas), varios problemas de github sobre fugas de memoria, 7 solicitudes de extracción sen pechar.
Se decides realizar probas de carga usando este complemento, presta atención ás seguintes discusións:

  1. Nun ambiente multiproceso, utilizouse unha LinkedList normal e o resultado foi NPE en tempo de execución. Isto pódese resolver cambiando a ConcurrentLinkedDeque ou mediante bloques sincronizados. Escollemos a primeira opción para nós (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Fuga de memoria; ao desconectar, a información de conexión non se elimina (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. No modo de transmisión (cando o websocket non está pechado ao final da mostra, pero se usa máis tarde no plan), os patróns de resposta non funcionan (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Este é un dos que hai en github. O que fixemos:

  1. Tomaron garfo Elyran Kogan (@elyrank): soluciona os problemas 1 e 3
  2. Problema resolto 2
  3. Embarcadoiro actualizado do 9.2.14 ao 9.3.12
  4. SimpleDateFormat envolto en ThreadLocal; SimpleDateFormat non é seguro para fíos, o que provocou NPE no tempo de execución
  5. Corrixiuse outra fuga de memoria (a conexión pechouse incorrectamente ao desconectarse)

E aínda flúe!

A memoria comezou a esgotarse non nun día, senón en dous. Non quedaba absolutamente tempo, así que decidimos lanzar menos fíos, pero en catro axentes. Isto debería ser suficiente durante polo menos unha semana.

Pasaron dous días...

Agora Hazelcast está quedando sen memoria. Os rexistros mostraron que despois dun par de días de probas, Hazelcast comezou a queixarse ​​da falta de memoria e, despois dun tempo, o clúster desmoronouse e os nodos continuaron morrendo un por un. Conectamos JVisualVM a hazelcast e vimos unha "serra ascendente": chamábase habitualmente ao GC, pero non podía borrar a memoria.

Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Resultou que en hazelcast 3.4, ao eliminar un mapa/multiMap (map.destroy()), a memoria non está completamente liberada:

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

O erro agora está solucionado en 3.5, pero era un problema daquela. Creamos novos multiMaps con nomes dinámicos e eliminámolos segundo a nosa lóxica. O código parecía algo así:

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();
    }
}

Comentario:

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

multiMap creouse para cada subscrición e eliminouse cando non era necesario. Decidimos que comezaríamos Map , a chave será o nome da subscrición, e os valores serán os identificadores de sesión (desde os cales pode obter identificadores de usuario, se é necesario).

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

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

Os gráficos melloraron.

Como e por que escribimos un servizo escalable de alta carga para 1C: Enterprise: Java, PostgreSQL, Hazelcast

Que máis aprendemos sobre as probas de carga?

  1. JSR223 debe escribirse en groovy e incluír caché de compilación - é moito máis rápido. Ligazón.
  2. Os gráficos de Jmeter-Plugins son máis fáciles de entender que os estándar. Ligazón.

Sobre a nosa experiencia con Hazelcast

Hazelcast era un produto novo para nós, comezamos a traballar con el a partir da versión 3.4.1, agora o noso servidor de produción está a executar a versión 3.9.2 (no momento de escribir este artigo, a última versión de Hazelcast é a 3.10).

Xeración de ID

Comezamos con identificadores enteiros. Imaxinemos que necesitamos outro Long para unha nova entidade. A secuencia da base de datos non é adecuada, as táboas están implicadas no sharding - resulta que hai unha mensaxe ID=1 en DB1 e unha mensaxe ID=1 en DB2, non podes poñer este ID en Elasticsearch, nin en Hazelcast , pero o peor é se queres combinar os datos de dúas bases de datos nunha soa (por exemplo, decidir que unha base de datos é suficiente para estes subscritores). Podes engadir varios AtomicLongs a Hazelcast e manter o contador alí, entón o rendemento de obter un novo ID é incrementAndGet máis o tempo para unha solicitude a Hazelcast. Pero Hazelcast ten algo máis óptimo: FlakeIdGenerator. Ao contactar con cada cliente, dáselles un rango de identificación, por exemplo, o primeiro, de 1 a 10, o segundo, de 000 a 10, etc. Agora o cliente pode emitir novos identificadores por si mesmo ata que remate o intervalo emitido. Funciona rapidamente, pero cando reinicias a aplicación (e o cliente Hazelcast), comeza unha nova secuencia, de aí os saltos, etc. Ademais, os desenvolvedores non entenden realmente por que os ID son enteiros, pero son tan inconsistentes. Pesámolo todo e cambiamos aos UUID.

Por certo, para aqueles que queiran ser como Twitter, hai unha biblioteca de Snowcast: esta é unha implementación de Snowflake enriba de Hazelcast. Podes velo aquí:

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

Pero xa non chegamos a iso.

TransactionalMap.substituír

Outra sorpresa: TransactionalMap.replace non funciona. Aquí tes unha proba:

@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

Tiven que escribir o meu propio substituto 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);
}

Proba non só estruturas de datos habituais, senón tamén as súas versións transaccionais. Ocorre que IMap funciona, pero TransactionalMap xa non existe.

Insira un novo JAR sen tempo de inactividade

En primeiro lugar, decidimos gravar obxectos das nosas clases en Hazelcast. Por exemplo, temos unha clase de aplicación, queremos gardala e lela. Gardar:

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

Lemos:

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

Todo está funcionando. Entón decidimos crear un índice en Hazelcast para buscar por:

map.addIndex("subscriberId", false);

E ao escribir unha nova entidade, comezaron a recibir ClassNotFoundException. Hazelcast intentou engadir ao índice, pero non sabía nada da nosa clase e quería que se lle proporcionara un JAR con esta clase. Fixemos iso, todo funcionou, pero apareceu un novo problema: como actualizar o JAR sen deter completamente o clúster? Hazelcast non recolle o novo JAR durante unha actualización nodo por nodo. Neste punto decidimos que podíamos vivir sen buscar índices. Despois de todo, se usas Hazelcast como unha tenda de valores clave, todo funcionará? En realidade non. Aquí de novo o comportamento de IMap e TransactionalMap é diferente. Cando IMap non lle importa, TransactionalMap lanza un erro.

IMap. Escribimos 5000 obxectos, lemos. Todo se espera.

@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 non funciona nunha transacción, obtemos unha 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();
        }
    });
}

Na versión 3.8, apareceu o mecanismo de implementación da clase de usuario. Pode designar un nodo mestre e actualizar o ficheiro JAR nel.

Agora cambiamos completamente o noso enfoque: serializamos nós mesmos en JSON e gardámolo en Hazelcast. Hazelcast non precisa coñecer a estrutura das nosas clases e podemos actualizar sen tempo de inactividade. A aplicación controla a versión dos obxectos do dominio. Pódense executar diferentes versións da aplicación ao mesmo tempo, e é posible unha situación cando a nova aplicación escribe obxectos con novos campos, pero a antiga aínda non coñece estes campos. E ao mesmo tempo, a nova aplicación le os obxectos escritos pola antiga aplicación que non teñen novos campos. Manexamos este tipo de situacións dentro da aplicación, pero para simplificar non cambiamos nin eliminamos campos, só ampliamos as clases engadindo novos campos.

Como aseguramos un alto rendemento

Catro viaxes a Hazelcast - boas, dúas á base de datos - malas

Ir á caché para buscar datos sempre é mellor que ir á base de datos, pero tampouco queres gardar rexistros non utilizados. Deixamos a decisión sobre que almacenar na caché ata a última etapa de desenvolvemento. Cando se codifica a nova funcionalidade, activamos o rexistro de todas as consultas en PostgreSQL (log_min_duration_statement a 0) e realizamos probas de carga durante 20 minutos. Usando os rexistros recollidos, utilidades como pgFouine e pgBadger poden crear informes analíticos. Nos informes, buscamos principalmente consultas lentas e frecuentes. Para consultas lentas, construímos un plan de execución (EXPLAIN) e avaliamos se tal consulta se pode acelerar. As solicitudes frecuentes dos mesmos datos de entrada encaixan ben na caché. Intentamos manter as consultas "planas", unha táboa por consulta.

Explotación

SV como servizo en liña púxose en funcionamento na primavera de 2017 e, como produto separado, SV lanzouse en novembro de 2017 (naquel momento en estado de versión beta).

En máis dun ano de funcionamento, non houbo problemas graves no funcionamento do servizo en liña CB. Seguimos o servizo en liña a través de Zabbix, recoller e despregar desde Bambú.

A distribución do servidor SV ofrécese en forma de paquetes nativos: RPM, DEB, MSI. Ademais, para Windows fornecemos un único instalador en forma dun único EXE que instala o servidor, Hazelcast e Elasticsearch nunha única máquina. Inicialmente referímonos a esta versión da instalación como a versión "demo", pero agora quedou claro que esta é a opción de implementación máis popular.

Fonte: www.habr.com

Engadir un comentario