Recientemente, algunos procesos comerciales deben monitorearse de extremo a extremo. Estos negocios están compuestos por varios microservicios. Todos los microservicios están escritos en Java Spring. Necesitamos comprender las estadísticas de tráfico y el estado de rendimiento de cada módulo involucrado en todo el negocio. , como un total de Cuántas llamadas de solicitud comercial, cuántas respuestas exitosas o fallidas, cuánto tiempo lleva cada paso, etc. Por lo tanto, también estudié cómo generar indicadores estadísticos en aplicaciones Java Spring, recopilar indicadores de manera uniforme a través de Prometheus y presentar esta información a través de diferentes informes en Grafana.
Primero, definamos un proceso comercial simple. Supongamos que tenemos dos aplicaciones Spring. Una es una llamada HTTP que proporciona una interfaz de solicitud comercial. Después de recibir la solicitud comercial, la información contenida en ella se envía a Kafka. Otra aplicación es suscribirse a los mensajes de Kafka, obtener los datos comerciales enviados por la Aplicación 1 y procesarlos.
aplicación uno
Cree una nueva aplicación en el sitio web start.spring.io, el nombre del artefacto es kafka-sender-example y seleccione Apache kafka para spring, Actuator, Spring Web en Dependencias. Abra el archivo de proyecto generado, agregue una clase llamada RemoteCommandController, implemente una interfaz http, el código es el siguiente:
package cn.roygao.kafkasenderexample;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
@RestController
public class RemoteCommandController {
@Autowired
private KafkaTemplate<Integer, String> template;
private final static Logger LOGGER = Logger.getLogger(RemoteCommandController.class.getName());
@PostMapping("/sendcommand")
public ResponseEntity<Map<String, Object>> sendCommand(@RequestBody JSONObject commandMsg) {
String requestId = UUID.randomUUID().toString();
String vin = commandMsg.getString("vin");
String command = commandMsg.getString("command");
LOGGER.info("Send command to vehicle:" + vin + ", command:" + command);
Map<String, Object> requestIdObj = Collections.singletonMap("requestId", requestId);
ProducerRecord<Integer, String> record = new ProducerRecord<>("remotecommand", 1, command);
try {
System.out.println(System.currentTimeMillis());
template.send(record).get(10, TimeUnit.SECONDS);
}
catch (ExecutionException e) {
LOGGER.info("Error");
LOGGER.info(e.getMessage());
}
catch (TimeoutException | InterruptedException e) {
LOGGER.info("Timeout");
LOGGER.info(e.getMessage());
}
return ResponseEntity.accepted().body(requestIdObj);
}
}
Este código es muy simple. Proporciona una interfaz POST/sendcommand. El usuario llama a esta interfaz y proporciona el número VIN del vehículo y la información del comando que se enviará. Después de recibir la solicitud, la información de la solicitud comercial se reenviará al mensaje. tema de Kafka. KafkaTemplate se usa aquí para enviar mensajes. Para hacer esto, defina una clase de configuración llamada KafkaSender con el siguiente código:
package cn.roygao.kafkasenderexample;
import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.IntegerSerializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
@Configuration
public class KafkaSender {
@Bean
public NewTopic topic() {
return TopicBuilder.name("remotecommand")
.build();
}
@Bean
public ProducerFactory<Integer, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// See https://kafka.apache.org/documentation/#producerconfigs for more properties
return props;
}
@Bean
public KafkaTemplate<Integer, String> kafkaTemplate() {
return new KafkaTemplate<Integer, String>(producerFactory());
}
}
El código define la dirección del servidor Kafka, los temas de los mensajes y otras configuraciones.
Ejecute ./mvnw clean package para compilar y empaquetar.
aplicación dos
Cree una nueva aplicación en el sitio web start.spring.io, el nombre del artefacto es kafka-sender-example y seleccione Apache kafka para spring, Actuator in Dependencies. Abra el archivo de proyecto generado y cree una nueva clase llamada RemoteCommandHandler para realizar la función de recibir información de Kafka. El código es el siguiente:
package cn.roygao.kafkareceiverexample;
import java.util.concurrent.TimeUnit;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.listener.adapter.ConsumerRecordMetadata;
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
@Component
public class RemoteCommandHandler {
private Timer timer;
public RemoteCommandHandler(MeterRegistry registry) {
this.timer = Timer
.builder("kafka.process.latency")
.publishPercentiles(0.15, 0.5, 0.95)
.publishPercentileHistogram()
.register(registry);
}
@KafkaListener(id = "myId", topics = "remotecommand")
public void listen(String in, ConsumerRecordMetadata meta) {
long latency = System.currentTimeMillis()-meta.timestamp();
timer.record(latency, TimeUnit.MILLISECONDS);
}
}
El constructor de esta clase debe pasar un objeto MeterRetistry y luego crear un nuevo objeto Timer, que es una de las cuatro métricas proporcionadas por Micrometer y se puede usar para registrar información de duración. Registre este temporizador en MeterRegistry.
En el método de escucha, se define suscribirse al mensaje del tema del mensaje de Kafka, obtener la marca de tiempo de la hora de generación en los metadatos del mensaje y compararla con la hora actual, calcular el tiempo que lleva desde la generación del mensaje hasta el mensaje. consumo, y luego use el temporizador para calcular. Timer realizará estadísticas de distribución de diferentes intervalos percentiles según la definición anterior.
Del mismo modo, también necesitamos definir una clase de configuración de Kafka, el código es el siguiente:
package cn.roygao.kafkareceiverexample;
import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
@Configuration
@EnableKafka
public class KafkaConfig {
@Bean
KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(3);
factory.getContainerProperties().setPollTimeout(3000);
return factory;
}
@Bean
public ConsumerFactory<Integer, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}
@Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put("key.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
return props;
}
}
Agregue la siguiente configuración al archivo application.properties:
spring.kafka.consumer.auto-offset-reset=earliest
server.port=7777
management.endpoints.web.exposure.include=health,info,prometheus
management.endpoints.enabled-by-default=true
management.endpoint.health.show-details: always
Luego ejecute ./mvnw clean package para compilar y empaquetar.
Iniciar Kafka
Aquí uso Docker para iniciar Kafka. El contenido del archivo de redacción es el siguiente:
---
version: '2'
services:
zookeeper:
image: confluentinc/cp-zookeeper:6.1.0
hostname: zookeeper
container_name: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
broker:
image: confluentinc/cp-server:6.1.0
hostname: broker
container_name: broker
depends_on:
- zookeeper
ports:
- "9092:9092"
- "9101:9101"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1
KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9101
KAFKA_JMX_HOSTNAME: localhost
KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081
CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092
CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1
CONFLUENT_METRICS_ENABLE: 'true'
CONFLUENT_SUPPORT_CUSTOMER_ID: 'anonymous'
schema-registry:
image: confluentinc/cp-schema-registry:6.1.0
hostname: schema-registry
container_name: schema-registry
depends_on:
- broker
ports:
- "8081:8081"
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'broker:29092'
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
connect:
image: cnfldemos/cp-server-connect-datagen:0.4.0-6.1.0
hostname: connect
container_name: connect
depends_on:
- broker
- schema-registry
ports:
- "8083:8083"
environment:
CONNECT_BOOTSTRAP_SERVERS: 'broker:29092'
CONNECT_REST_ADVERTISED_HOST_NAME: connect
CONNECT_REST_PORT: 8083
CONNECT_GROUP_ID: compose-connect-group
CONNECT_CONFIG_STORAGE_TOPIC: docker-connect-configs
CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1
CONNECT_OFFSET_FLUSH_INTERVAL_MS: 10000
CONNECT_OFFSET_STORAGE_TOPIC: docker-connect-offsets
CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1
CONNECT_STATUS_STORAGE_TOPIC: docker-connect-status
CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1
CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter
CONNECT_VALUE_CONVERTER: io.confluent.connect.avro.AvroConverter
CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry:8081
# CLASSPATH required due to CC-2422
CLASSPATH: /usr/share/java/monitoring-interceptors/monitoring-interceptors-6.1.0.jar
CONNECT_PRODUCER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringProducerInterceptor"
CONNECT_CONSUMER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringConsumerInterceptor"
CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components"
CONNECT_LOG4J_LOGGERS: org.apache.zookeeper=ERROR,org.I0Itec.zkclient=ERROR,org.reflections=ERROR
control-center:
image: confluentinc/cp-enterprise-control-center:6.1.0
hostname: control-center
container_name: control-center
depends_on:
- broker
- schema-registry
- connect
- ksqldb-server
ports:
- "9021:9021"
environment:
CONTROL_CENTER_BOOTSTRAP_SERVERS: 'broker:29092'
CONTROL_CENTER_CONNECT_CLUSTER: 'connect:8083'
CONTROL_CENTER_KSQL_KSQLDB1_URL: "http://ksqldb-server:8088"
CONTROL_CENTER_KSQL_KSQLDB1_ADVERTISED_URL: "http://localhost:8088"
CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081"
CONTROL_CENTER_REPLICATION_FACTOR: 1
CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1
CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1
CONFLUENT_METRICS_TOPIC_REPLICATION: 1
PORT: 9021
ksqldb-server:
image: confluentinc/cp-ksqldb-server:6.1.0
hostname: ksqldb-server
container_name: ksqldb-server
depends_on:
- broker
- connect
ports:
- "8088:8088"
environment:
KSQL_CONFIG_DIR: "/etc/ksql"
KSQL_BOOTSTRAP_SERVERS: "broker:29092"
KSQL_HOST_NAME: ksqldb-server
KSQL_LISTENERS: "http://0.0.0.0:8088"
KSQL_CACHE_MAX_BYTES_BUFFERING: 0
KSQL_KSQL_SCHEMA_REGISTRY_URL: "http://schema-registry:8081"
KSQL_PRODUCER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringProducerInterceptor"
KSQL_CONSUMER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringConsumerInterceptor"
KSQL_KSQL_CONNECT_URL: "http://connect:8083"
KSQL_KSQL_LOGGING_PROCESSING_TOPIC_REPLICATION_FACTOR: 1
KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: 'true'
KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: 'true'
ksqldb-cli:
image: confluentinc/cp-ksqldb-cli:6.1.0
container_name: ksqldb-cli
depends_on:
- broker
- connect
- ksqldb-server
entrypoint: /bin/sh
tty: true
ksql-datagen:
image: confluentinc/ksqldb-examples:6.1.0
hostname: ksql-datagen
container_name: ksql-datagen
depends_on:
- ksqldb-server
- broker
- schema-registry
- connect
command: "bash -c 'echo Waiting for Kafka to be ready... && \
cub kafka-ready -b broker:29092 1 40 && \
echo Waiting for Confluent Schema Registry to be ready... && \
cub sr-ready schema-registry 8081 40 && \
echo Waiting a few seconds for topic creation to finish... && \
sleep 11 && \
tail -f /dev/null'"
environment:
KSQL_CONFIG_DIR: "/etc/ksql"
STREAMS_BOOTSTRAP_SERVERS: broker:29092
STREAMS_SCHEMA_REGISTRY_HOST: schema-registry
STREAMS_SCHEMA_REGISTRY_PORT: 8081
rest-proxy:
image: confluentinc/cp-kafka-rest:6.1.0
depends_on:
- broker
- schema-registry
ports:
- 8082:8082
hostname: rest-proxy
container_name: rest-proxy
environment:
KAFKA_REST_HOST_NAME: rest-proxy
KAFKA_REST_BOOTSTRAP_SERVERS: 'broker:29092'
KAFKA_REST_LISTENERS: "http://0.0.0.0:8082"
KAFKA_REST_SCHEMA_REGISTRY_URL: 'http://schema-registry:8081'
Ejecute nohup docker compose up > ./kafka.log 2>&1 & para comenzar. Ingrese localhost:9021 en el navegador para ver información relacionada con Kafka en la interfaz de la consola.
Ejecute la aplicación 1 y la aplicación 2 respectivamente, y luego llame a la interfaz POST http://localhost:8080/remotecommand para enviar solicitudes comerciales, como los siguientes comandos:
curl --location --request POST 'http://localhost:8080/sendcommand' \
--header 'Content-Type: application/json' \
--data-raw '{
"vin": "ABC123",
"command": "engine-start"
}'
En la consola de Kafka, puede ver que hay un tema de mensaje de comando remoto y se envía y consume un mensaje.
Inicio Prometeo y Grafana
También use docker compose para comenzar, el contenido del archivo de redacción es el siguiente:
services:
prometheus:
image: prom/prometheus-linux-amd64
#network_mode: host
container_name: prometheus
restart: unless-stopped
volumes:
- ./config:/etc/prometheus/
command:
- '--config.file=/etc/prometheus/prometheus.yaml'
ports:
- 9090:9090
grafana:
image: grafana/grafana
user: '472'
#network_mode: host
container_name: grafana
restart: unless-stopped
links:
- prometheus:prometheus
volumes:
- ./data/grafana:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- 3000:3000
depends_on:
- prometheus
Cree un nuevo directorio de configuración en el directorio de archivos de redacción, que almacena el archivo de configuración de Prometheus, el contenido es el siguiente:
scrape_configs:
- job_name: 'Spring Boot Application input'
metrics_path: '/actuator/prometheus'
scrape_interval: 2s
static_configs:
- targets: ['172.17.0.1:7777']
labels:
application: 'My Spring Boot Application'
La configuración de objetivos aquí es la dirección expuesta por la aplicación 2, y metrics_path es la ruta para recopilar métricas.
Cree un nuevo directorio de datos/grafana en el directorio de archivos de redacción y móntelo en el directorio de archivos de Grafana. Tenga en cuenta que debe usar chmod 777 para modificar los permisos del directorio, de lo contrario, Grafana informará un error de permiso.
Ejecute nohup docker compose up > ./prometheus.log 2>&1 y ejecute.
Abra localhost: 9090 para acceder a la página de Prometheus, y luego podemos ingresar kafka para buscar, y podemos ver los datos del índice kafka_process_latency informados por la aplicación 2, y se llevan a cabo las estadísticas de los tres intervalos percentiles de 0.15, 0.5 y 0.95 según nuestra definición.
Abra localhost:3000 para acceder a la página de Grafana, configure la fuente de datos, seleccione la dirección del contenedor de Prometheus y luego guarde y pruebe. Luego, puede crear un nuevo tablero y luego mostrar el gráfico indicador de kafka_process_latency en el informe.
[Continuará] Es necesario agregar la métrica Counter para llamar a la interfaz Http y definir más informes en Grafana, incluidos otros indicadores de servicio, etc.