在Spring Boot中使用流的Reactive Kafka

390 阅读13分钟

DZone>微服务区>在Spring Boot中使用流的Reactive Kafka

在Spring Boot中使用流的反应式Kafka

如何将Spring Boot和Kafka与流整合到一个反应式解决方案中。

Sven Loesekann user avatar 通过

读者:Sven Loesekann

-

6月28日,22 - 微服务区 -教程

喜欢 (1)

评论

保存

鸣叫

144次浏览

加入DZone社区,获得完整的会员体验。

免费加入

AngularAndSpring项目使用Kafka进行新用户的分布式签到和已注销用户的分布式令牌撤销。

系统架构

AngularAndSpring项目需要能够横向扩展,每个项目都有自己的数据库。为了实现这一点,签到需要被传播到所有的实例中。Kafka作为一个中央事件流平台来发送签到事件。Kafka可以横向扩展到高事件负载,并且可以被设置为高可用

用户签到架构

用户签到会创建一个新的用户,该用户必须在AngularAndSpring项目部署的所有实例中创建。为了做到这一点,Kafka被用来将签到事件流向所有部署的AngularAndSpring实例,在那里它们被存储。在签到实例处理完签到响应后,签到就完成了。然后,登录可以在任何AngularAndSpring实例上完成。

用户注销架构

用户注销是在本地处理的,用户会得到显示在用户界面上的注销。注销过程也向Kafka发送一个注销事件,以撤销该用户登录的JWT。然后,Kafka启动一个有状态的流(可能是在超时后),聚合过去120秒的所有注销的令牌。Kafka流的处理是在AngularAndSpring实例上进行的。撤销的令牌列表被返回到Kafka,并由Kafka发送给所有AngularAndSpring实例。AngularAndSpring实例更新其内存中的撤销的令牌列表。这可以防止令牌更新。

部署架构

该系统是用Kubernetes中的Helm图部署的。目前的Kafka版本需要Zookeeper。AngularAndSpring项目从Kafka发送/接收事件,在MongoDB中读取/存储数据。Zookeeper和Kafka是Kubernetes的部署,有服务来访问它们。AngularAndSpring有一个Kubernetes部署和一个带有NodePort的服务来访问用户界面,MongoDB有一个Kubernetes部署和一个在Kubernetes卷/volumeclaim中坚持数据的服务。

用户登录

AngularAndSpring项目使用Spring Boot的反应式API来访问MongoDB。Spring Boot的反应式API也被用于Kafka。

KafkaConfig类中,Kafka消费者/生产者/主题被配置或创建。

Java

@Configuration
@Profile("kafka | prod")
@EnableKafka
@EnableKafkaStreams
public class KafkaConfig {
...
   @PostConstruct
   public void init() throws ClassNotFoundException {
      String bootstrap = this.bootstrapServers.split(":")[0].trim();
      if (bootstrap.matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$")) {
         DefaultHostResolver.IP_ADDRESS = bootstrap;
      } else if (!bootstrap.isEmpty()) {
         DefaultHostResolver.KAFKA_SERVICE_NAME = bootstrap;
      }
      LOGGER.info("Kafka Servername: {} Kafka Servicename: {} 
        Ip Address: {}", DefaultHostResolver.KAFKA_SERVER_NAME,
	DefaultHostResolver.KAFKA_SERVICE_NAME, 
        DefaultHostResolver.IP_ADDRESS);
      Map<String,Object> props = new HashMap<>();
      props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
      props.put(ProducerConfig.ACKS_CONFIG, "all");
      props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
      props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, 
         this.compressionType);
      props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 
         Class.forName(this.producerKeySerializer));
      props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 
         Class.forName(this.producerValueSerializer));
      this.senderOptions = SenderOptions.create(props);
      props = new HashMap<>();
      props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
      props.put(ConsumerConfig.GROUP_ID_CONFIG, "all");
      props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, 
         this.consumerAutoOffsetReset);        
      props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
         Class.forName(this.consumerKeySerializer));
      props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
         Class.forName(this.consumerValueSerializer));
      this.receiverOptions = ReceiverOptions.create(props);
}

注释使KafkaConfig成为Profiles('kafka'或'prod')的配置类,Kafka与KafkaStreams被启用。

init()方法用于设置Kafka消费者和生产者的属性。Kafka API的DefaultHostResolver类需要被改写,以便在Minikube集群中使用Kafka部署。Kafka部署的配置可以在这个目录中找到

  • 消费者/生产者共享BOOTSTRAP_SERVERS_CONFIG,它必须是IpAdress:Port(比如:192.168.1.1:9092)或servicename:Port(比如:kafkaservice:9092)
  • producerConfig.ACKS_CONFIG = 要求所有的消费者都已经收到该事件。
  • producerConfig.ENABLE_IDEMPOTENCE_CONFIG = true可以重试。
  • producerConfig.COMPRESSION_TYPE_CONFIG = 'gzip' 启用gzip压缩消息(其他类型也可)。
  • producerConfig.KEY_SERIALIZER_CLASS_CONFIG/VALUE_SERIALIZER_CLASS_CONFIG为事件的key/value设置序列化。
  • consumerConfig.GROUP_ID_CONFIG设置了普通消费者的groupId。
  • consumerConfig.AUTO_OFFSET_RESET_CONFIG配置了新的消费者应该从哪里开始处理。
  • consumerConfig.KEY_DESERIALIZER_CLASS_CONFIG/VALUE_DESERIALIZER_CLASS_CONFIG设置事件key/value的解序列器。

爪哇

@Bean
public ReceiverOptions<?, ?> kafkaReceiverOptions() {
   return this.receiverOptions;
}

@Bean
public KafkaSender<?, ?> kafkaSender() {
   return KafkaSender.create(this.senderOptions);
}

@Bean
public NewTopic newUserTopic() {
   return TopicBuilder.name(KafkaConfig.NEW_USER_TOPIC)
      .config(TopicConfig.COMPRESSION_TYPE_CONFIG,   
         this.compressionType).compact().build();
}

方法kafkaReceiverOptions()提供了在init()方法中创建的receiverOptions,供Spring注入。

方法kafkaSender提供了一个KafkaSender,其senderOptions是在init()方法中创建的,供Spring注入。

方法newUserTopic()在启动时创建一个新的Kafka Topic,并设置压缩类型。

这就是Spring Boot为Kafka签到所需的设置。

发送事件

EventProducer类将事件发送至Kafka。

Java

@Profile("kafka | prod")
@Service
public class EventProducer implements MyEventProducer {
   private static final Logger LOGGER = 
      LoggerFactory.getLogger(EventProducer.class);
   private final KafkaSender<String, String> kafkaSender;
   private final EventMapper eventMapper;

   public EventProducer(KafkaSender<String, String> kafkaSender, 
      EventMapper eventMapper) {
      this.kafkaSender = kafkaSender;
      this.eventMapper = eventMapper;
   }

   public Mono<MyUser> sendNewUser(MyUser dto) {
      String dtoJson = this.eventMapper.mapDtoToString(dto);
      return this.kafkaSender.createOutbound()
         .send(Mono.just(new ProducerRecord<>(KafkaConfig.NEW_USER_TOPIC, 
            dto.getSalt(), dtoJson))).then()
               .doOnError(e -> LOGGER.error(
                  String.format("Failed to send topic: %s value: %s", 
                     KafkaConfig.NEW_USER_TOPIC, dtoJson), e))
                        .thenReturn(dto);				
   }

MessageProducer有一个Profile注释,当Profile'kafka'或'prod'被添加时就会使用。

构造函数注入了kafkaSender和EventMapper

方法sendNewUser(...)使用EventMapper将MyUser类变成一个字符串来发送。kafkaSender将dtoJson发送至'NEW_USER_TOPIC',事件键为'dto.getSalt()'。该方法返回一个Mono以支持反应式发送。

接收事件

EventConsumer类从Kafka接收事件。

Java

@Profile("kafka | prod")
@Service
public class EventConsumer {
   private static final Logger LOGGER = 
      LoggerFactory.getLogger(EventConsumer.class);
   private final ReceiverOptions<String, String> receiverOptions;
   private final KafkaReceiver<String, String> userLogoutReceiver;
   private final KafkaReceiver<String, String> newUserReceiver;
   private final MyUserServiceEvents myUserServiceEvents;
   private final EventMapper eventMapper;
   @Value("${spring.kafka.consumer.group-id}")
   private String consumerGroupId;

   public EventConsumer(MyUserServiceEvents myUserServiceEvents, 
      ReceiverOptions<String, String> receiverOptions, 
         EventMapper eventMapper) {
      this.receiverOptions = receiverOptions;
      this.userLogoutReceiver = KafkaReceiver			
         .create(this.receiverOptions(List
            .of(KafkaConfig.USER_LOGOUT_SINK_TOPIC)));
      this.newUserReceiver = KafkaReceiver.create(this
         .receiverOptions(List.of(KafkaConfig.NEW_USER_TOPIC)));
      this.myUserServiceEvents = myUserServiceEvents;
      this.eventMapper = eventMapper;
   }

   private ReceiverOptions<String, String> receiverOptions(
      Collection<String> topics) {
      return this.receiverOptions
         .addAssignListener(p -> LOGGER.info("Group {} partitions assigned  
             {}", this.consumerGroupId, p))
             .addRevokeListener(p -> LOGGER.info("Group {} partitions 
                 revoked {}", this.consumerGroupId, p))
                 .subscription(topics);
   }

   @EventListener(ApplicationReadyEvent.class)
   public void doOnStartup() {
      this.newUserReceiver.receiveAtmostOnce().flatMap(myRecord -> 
          this.myUserServiceEvents
             .userSigninEvent(this.eventMapper.
                 mapJsonToObject(myRecord.value(), 
                 MyUser.class))).subscribe();
      this.userLogoutReceiver.receiveAtmostOnce().flatMap(myRecord -> 
         this.myUserServiceEvents.logoutEvent(this.eventMapper
            .mapJsonToObject(myRecord.value(),  
                RevokedTokensDto.class))).subscribe();
   }	
}

EventConsumer类在配置文件 "kafka "或 "prod "上启用了配置文件注解。构造函数设置MyUserServiceEvents服务、EventMapper和newUserReceiver。

方法doOnStartup()是由Spring在启动完成前运行的。该方法创建了一个Spring Reactor Streams,用来接收newUser/logout事件,并通过MyUserService处理这些事件然后,这些流被订阅()并在应用程序的正常运行时间内运行。

处理事件

MyUserServiceEvents类处理签入事件。

Java

@Profile("kafka | prod")
@Service
public class MyUserServiceEvents extends MyUserServiceBean 
   implements MyUserService {
   private static final Logger LOGGER = LoggerFactory.getLogger(
      MyUserServiceEvents.class);
   private final MyEventProducer myEventProducer;
   private final Sinks.Many<MyUser> myUserSink = Sinks.many().multicast()
      .onBackpressureBuffer();
   private final ConnectableFlux<MyUser> myUserFlux = this.myUserSink
      .asFlux().publish();

   public MyUserServiceEvents(JwtTokenProvider jwtTokenProvider, 
      PasswordEncoder passwordEncoder, PasswordEncryption passwordEncryption,   
      MyMongoRepository myMongoRepository, MyEventProducer myEventProducer) {
      super(jwtTokenProvider, passwordEncoder, passwordEncryption, 
         myMongoRepository);
      this.myEventProducer = myEventProducer;
   }

   @Override
   public Mono<MyUser> postUserSignin(MyUser myUser) {
      Mono<MyUser> myUserResult = this.myUserFlux.autoConnect()
         .filter(myUser1 -> myUser.getUserId().equalsIgnoreCase(
             myUser1.getUserId())).shareNext();
      return super.postUserSignin(myUser, false, true).flatMap(dto -> 
         this.myEventProducer.sendNewUser(dto)).zipWith(myUserResult, 
            (myUser1, msgMyUser1)-> msgMyUser1).flatMap(myUser1 -> {
               return Mono.just(myUser1);
            });
   }

   public Mono<MyUser> userSigninEvent(MyUser myUser) {
      return super.postUserSignin(myUser, true, false).flatMap(myUser1 -> {
         if (this.myUserSink.tryEmitNext(myUser1).isFailure()) {
            LOGGER.info("Emit to myUserSink failed. {}", myUser1);
         }
      return Mono.just(myUser1);
   });
}

MyUserServiceEvents类在配置文件 "kafka "或 "prod "上用配置文件注解启用。构造函数注入了一个MyEventProducer来发送事件到Kafka。

myUserSink创建一个可以发送Spring ReactorMyUser事件的sink。

myUserFlux创建了一个Spring Reactor Flux,可以接收MyUser事件。

postUserSignin(...)方法被休息控制器MyUserController调用,并创建一个连接到myUserFlux的Mono,以等待这个签到的匹配事件。postUserSignin(...)方法检查签到实体,但不存储它。在flatMap(...)中,签到实体与MyEventProducer被发送到Kafka。然后,zipWith(...)被用来等待在myUserResult中收到的myUserFlux的结果,并返回结果Mono。

方法userSigninEvent(...)被EventConsumer调用,并调用postUserSignin(...)方法来存储该事件。然后,myUserSink被用来发出Spring Reactor事件,使postUserSignin(...)方法完成并返回结果Mono。

结论 签到

在本系列的第一部分,展示了设置和反应式发送/接收事件。Spring使得使用Kafka只需很少的代码和努力,而Reactor对Kafka的支持使得在保持代码可读性的同时实现了高效/反应式发送和接收事件。

注销

AngularAndSpring项目使用Kafka来实现新用户的分布式登录和已注销用户的分布式令牌撤销。这篇文章展示了登录/注销如何使用有状态的Kafka流来实现注销时的令牌撤销。该架构在登录部分显示。

用户登录

登录是由MyUserServiceBean处理的。登录过程对于基于Kafka和MongoDB的认证是相同的。

爪哇

public Mono<MyUser> postUserLogin(MyUser myUser) throws 
   NoSuchAlgorithmException, InvalidKeySpecException {
   Query query = new Query();		   
   query.addCriteria(Criteria.where("userId").is(myUser.getUserId()));
   return this.myMongoRepository.findOne(query, 
      MyUser.class).switchIfEmpty(Mono.just(new MyUser()))
     .delayElement(Duration.ofSeconds(3L))
     .map(user1 -> loginHelp(user1, myUser.getPassword()));
   }

private MyUser loginHelp(MyUser user, String passwd) {
   if (user.getUserId() != null) {
      if (this.passwordEncoder.matches(passwd, user.getPassword())) {
        String jwtToken = this.jwtTokenProvider.createToken(user.getUserId(),
           Arrays.asList(Role.USERS));
        user.setToken(jwtToken);
        user.setPassword("XXX");
        return user;
      }
   }
   return new MyUser();
}

postUserLogin方法使用Spring Reactor的反应式API。

该查询在文档中搜索 "userId","userId "是唯一的。

findOne(...) "调用搜索查询中的MyUser文档。如果没有找到,会返回一个空的MyUser文档。该文档的返回被延迟3秒,以限制每个用户的登录次数。方法loginHelp检查是否找到了MyUser Document,检查密码,然后检查是否是一个带有有效令牌的MyUser Document返回,或者是一个空的MyUser Document。

用户注销事件

Logout事件是在MyUserServiceEvents类中的postLogout(...)方法中为Kafka创建的。

爪哇

public Mono<Boolean> postLogout(String token) {
   String username = this.getTokenUsername(token);
   String uuid = this.getTokenUuid(token);
   List<RevokedToken> revokedTokens = new ArrayList<>();
   revokedTokens.add(new RevokedToken(null, username, uuid, 
      LocalDateTime.now()));
   return revokedTokens.stream().map(myRevokedToken -> 
      this.myEventProducer.sendUserLogout(myRevokedToken)
         .flatMap(value -> Mono.just(value != null)))
         .reduce((result1, result2) -> 
            Mono.just(result1.block() == true && result2.block() == true))
               .orElse(Mono.just(Boolean.FALSE));
}

首先,"用户名 "和 "uuid "被从token中检索出来。uuid "是每个登录者唯一的,而不是token。

然后,为这个登录创建一个带有RevokedToken的RevokedTokens列表。

RevokedToken列表被用来创建一个流,将RevokedToken发送到带有myEventProducer的Kafka流。因为结果可能有重复的,所有的结果都被检查为真,并返回结果。

发送事件

注销事件是在EventProducer类中的sendUserLogout方法中发送的。

Java

public Mono<RevokedToken> sendUserLogout(RevokedToken dto) {
   String dtoJson = this.eventMapper.mapDtoToString(dto);
   return this.kafkaSender.createOutbound().send(Mono.just(
      new ProducerRecord<>(KafkaConfig.USER_LOGOUT_SOURCE_TOPIC, 
         dto.getName(), dtoJson))).then()
           .doOnError(e -> LOGGER.error(
               String.format("Failed to send topic: %s value: %s", 
                  KafkaConfig.USER_LOGOUT_SOURCE_TOPIC, dtoJson), e))
           .thenReturn(dto);
}

EventMapper用于将RevokedToken DTO变成一个JSON字符串。

kafkaSencer创建一个出站连接,并将Kafka的ProducerRecord发送到USER_LOGOUT_SOURCE_TOPIC,用户名为键,JSON字符串为内容。错误被记录下来,在事件被发送后,DTO被返回。

Kafka注销流

处理revokedTokens的有状态Kafka流在KafkaStreams类中。

Java

@Profile("kafka | prod")
@Component
public class KafkaStreams {
   private static final Logger LOGGER =   
      LoggerFactory.getLogger(KafkaStreams.class);
   private static final long LOGOUT_TIMEOUT = 120L;
   private static final long GRACE_TIMEOUT = 5L;
   private ObjectMapper objectMapper;

   public KafkaStreams(ObjectMapper objectMapper) {
      this.objectMapper = objectMapper;
   }

   @Bean("UserLogoutTopology")
   public Topology userLogout(final StreamsBuilder builder) {
      builder.stream(KafkaConfig.USER_LOGOUT_SOURCE_TOPIC, 
         Consumed.with(Serdes.String(), Serdes.String())).groupByKey()
           .windowedBy(SlidingWindows
           .ofTimeDifferenceAndGrace(Duration.ofSeconds(
              KafkaStreams.LOGOUT_TIMEOUT),
              Duration.ofSeconds(KafkaStreams.GRACE_TIMEOUT)))
              .aggregate(LinkedList<String>::new, (key, value, myList) -> {
                 myList.add(value);
                 return myList;
              }, Materialized.with(Serdes.String(), 
                 Serdes.ListSerde(LinkedList.class, Serdes.String())))
                    .toStream().mapValues(value -> 
                       convertToRevokedTokens((List<String>) value))
                       .to(KafkaConfig.USER_LOGOUT_SINK_TOPIC);
      Properties streamsConfiguration = new Properties();   
      streamsConfiguration.put(
         StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG,
            LastlogoutTimestampExtractor.class.getName());
      streamsConfiguration.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG,  
         1000L);
      return builder.build(streamsConfiguration);
}

KafkaStreams类是用'kafka'或'prod'配置文件激活的。

方法userLogout(...)为Kafka流创建 "UserLogoutTopology",该流以 "USER_LOGOUT_SOURCE_TOPIC "作为源。该流有groupByKey()来按userId对事件进行分组。流有windowedBy(...),为注销事件提供了一个125秒的持久窗口,之后事件就会从窗口中消失。windowBy(...)持久化事件,并返回窗口中userId的事件的聚合。事件键和值是Materialized(stateful) Kafka Topic中的字符串。在Materialized.with(...)中是映射在RevokedToken对象列表中的值,并发送至'USER_LOGOUT_SINK_TOPIC'。然后是两个Kafka流配置属性的设置和构建器返回拓扑结构。

有状态的Kafka主题在收到10MB的数据或超时30秒后被处理。

接收事件

Kafka事件是通过EventConsumer接收的。

Java

@EventListener(ApplicationReadyEvent.class)
public void doOnStartup() {
   this.newUserReceiver.receiveAtmostOnce().flatMap(myRecord -> 
      this.myUserServiceEvents.userSigninEvent(this.eventMapper.
         mapJsonToObject(myRecord.value(), MyUser.class))).subscribe();
   this.userLogoutReceiver.receiveAtmostOnce().flatMap(myRecord -> 
      this.myUserServiceEvents.logoutEvent(this.eventMapper.
          mapJsonToObject(myRecord.value(), 
             RevokedTokensDto.class))).subscribe();
	}	

ApplicationReadyEvent "是在启动完成之前发送的。userLogoutReceiver "用EventMapperMyUserServiceEvents类的logoutEvent(..)方法处理Json(String)事件。subscribe()方法激活了Kafka接收器。

处理事件

RevokedTokensDto类在MyUserServiceEvents类的logoutEvent(...)中被处理。

爪哇

public Mono<Boolean> logoutEvent(RevokedTokensDto revokedTokensDto) {
   return Mono.just(this.updateLoggedOutUsers(revokedTokensDto
      .getRevokedTokens()));
}

public Boolean updateLoggedOutUsers(List<RevokedToken> revokedTokens) {
   this.jwtTokenProvider.updateLoggedOutUsers(revokedTokens);
   return Boolean.TRUE;
}

方法中调用方法 updateLoggedOutUsers(...),该方法用于更新在每次请求中检查的RevokedToken DTO的内存列表。

结论 Logout

Kafka能够轻松实现Jwt令牌的撤销。如果有状态的Kafka Topic Streams的限制不是一个障碍,他们可以减少应用程序中这个功能的逻辑量。Kafka是一个非常强大的工具,可以做很多事情,但确实需要时间来加速使用它。

开发/系统部署

AngularAndSpring项目使用Kafka来实现新用户的分布式登录,以及已注销用户的分布式令牌撤销。本文展示了Kafka的开发部署和Kafka/Zookeeper/MongoDb/AngularAndSpring的系统部署。

为Kafka的DNS名称解析打补丁

Spring Kafka客户端会检查Kafka实例的DNS名称。为了禁用DNS检查,DefaultHostResolver类被覆盖。

爪哇

public class DefaultHostResolver implements HostResolver {
   public static volatile String IP_ADDRESS = "";
   public static volatile String KAFKA_SERVER_NAME = "";
   public static volatile String KAFKA_SERVICE_NAME = "";
   
   @Override
   public InetAddress[] resolve(String host) throws UnknownHostException {
      if(host.startsWith(KAFKA_SERVER_NAME) && !IP_ADDRESS.isBlank()) {
         InetAddress[] addressArr = new InetAddress[1];
         addressArr[0] = InetAddress.getByAddress(host, 
            InetAddress.getByName(IP_ADDRESS).getAddress());
         return addressArr;
      } else if(host.startsWith(KAFKA_SERVER_NAME) && 
         !KAFKA_SERVICE_NAME.isBlank()) {
         host = KAFKA_SERVICE_NAME;
      }
      return InetAddress.getAllByName(host);
   }
}

KAFKA_SERVER_NAME'包含Kafka服务器的DNS名称开头的字符串。IP_ADDRESS'包含Kubernetes集群(本例中为Minikube)的IP地址。KAFKA_SERVICE_NAME'包含必须用于连接Kubernetes集群中Kafka的服务名称。

如果设置了'KAFKA_SERVER_NAME'和'IP_ADDRESS',如果DNS名称以'KAFKA_SERVER_NAME'开头,就会返回'IP_ADDRESS'。这样就可以在开发部署中启用Kafka的DNS解析。

如果 "KAFKA_SERVER_NAME "和 "KAFKA_SERVICE_NAME "被设置,并且Kafka的DNS名称以 "KAFKA_SERVER_NAME "开头,"KAFKA_SERVICE_NAME "被设置为Kafka的DNS名称,以便在Kubernests集群中访问Kafka。

开发部署

对于开发来说,需要一个本地的Kafka实例,并且Kafka和Zookeeper需要一起部署。这可以通过Docker Compose或Kubernetes完成。Kubernetes用于系统部署,所以它也用于开发。

部署的Helm图可以在kafka目录中找到。values.yaml看起来是这样的。

YAML

kafkaName: kafkaapp
zookeeperName: zookeeperserver
kafkaImageName: bitnami/kafka
kafkaImageVersion: latest
zookeeperImageName: bitnami/zookeeper
zookeeperImageVersion: latest
kafkaServiceName: kafkaservice
zookeeperServiceName: zookeeperservice
volumeClaimName: mongo-pv-claim
persistentVolumeName: mongo-pv-volume

secret:
  name: app-env-secret
  nameKafka: kafka-env-secret
  nameZookeeper: zookeeper-env-secret

envZookeeper:
  normal: 
    ALLOW_ANONYMOUS_LOGIN: yes
  secret:
   ZOOKEEPER_TICK_TIME: "2000"
   
envKafka:
  normal: 
    KAFKA_BROKER_ID: "1"
    KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
    KAFKA_CFG_LISTENERS: PLAINTEXT://:9092
    KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://:9092
    KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: false
    ALLOW_PLAINTEXT_LISTENER: yes
  secret:
    KAFKA_ZOOKEEPER_CONNECT: "zookeeperservice:2181"    

首先,所有的Helm变量都被设置。secret包含Kafka和Zookeeper的编码秘密值。

'envZookeeper'和'envKafka'部分有正常的环境变量和秘密变量。秘密变量由_helpers.tpl脚本处理,并作为Kubernetes的秘密提供。

Helm模板 看起来像这样。

YAML

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Values.secret.nameZookeeper }}
type: Opaque
data:
  {{- range $key, $val := .Values.envZookeeper.secret }}
  {{ $key }}: {{ $val | b64enc }}
  {{- end}}
---
apiVersion: v1
kind: Secret
metadata:
  name: {{ .Values.secret.nameKafka }}
type: Opaque
data:
  {{- range $key, $val := .Values.envKafka.secret }}
  {{ $key }}: {{ $val | b64enc }}
  {{- end}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.zookeeperName }}
  labels:
    app: {{ .Values.zookeeperName }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Values.zookeeperName }}
  template:
    metadata:
      labels:
        app: {{ .Values.zookeeperName }}
    spec:
      containers:
      - name: {{ .Values.zookeeperName }}
        image: "{{ .Values.zookeeperImageName }}:{{ .Values.zookeeperImageVersion }}"
        resources:
          limits:
            memory: "768M"
            cpu: "0.5"
          requests:
            memory: "512M"
            cpu: "0.5"
        env:
          {{- include "helpers.list-envZookeeperApp-variables" . | indent 10 }}
        ports:
        - containerPort: 2181
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.zookeeperServiceName }}
  labels:
    app: {{ .Values.zookeeperServiceName }}
spec:
  ports:
  - port: 2181
    protocol: TCP
  selector:
    app: {{ .Values.zookeeperName }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.kafkaName }}
  labels:
    app: {{ .Values.kafkaName }}    
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Values.kafkaName }}
  template:
    metadata:
      labels:
        app: {{ .Values.kafkaName }}
    spec:
      securityContext:
        runAsUser: 0
        runAsGroup: 0
        fsGroup: 0
      containers:
      - name: {{ .Values.kafkaName }}
        image: "{{ .Values.kafkaImageName }}:{{ .Values.kafkaImageVersion }}" 
        imagePullPolicy: Always
        resources:
          limits:
            memory: "1G"
            cpu: "1.5"
          requests:
            memory: "768M"
            cpu: "1.0"
        env:
          {{- include "helpers.list-envKafkaApp-variables" . | indent 10 }}
        ports:
        - containerPort: 9092
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.kafkaServiceName }} 
  labels:
    run: {{ .Values.kafkaServiceName }} 
spec:
  type: NodePort
  ports:
  - port: 9092
    nodePort: 9092
    protocol: TCP
  selector:
    app: {{ .Values.kafkaName }}

前两节为Kafka和Zookeeper的秘密创建了不透明的秘密,这些秘密是用base64编码的。

接下来的两部分是Zookeeper的部署/服务。内存被限制在 "768M",CPU被限制在 "0.5"。这一行 "env: {{- include "helpers.list-envZookeeperApp-variables" .| 缩进10 }}'包括配置Docker镜像的变量和秘密的值.yaml。

接下来的两节有Kafka的部署/服务。需要'securityContext'参数来使Kafka镜像以root身份运行。内存被限制在'1G',CPU被限制在'1.5'。env: {{- include "helpers.list-envKafkaApp-variables" .|缩进10 }}'包括配置Docker镜像的变量和秘密的值.yaml。该服务有一个NodePort,可以从Kubernetes集群的外部访问9092端口。

设置/启动Minikube集群的命令可以在minikubeSetup.sh文件中找到。

启动命令是:"minikube start -extra-config=apiserver.service-node-port-range=1024-65535"。

Helm命令可以在helmCommand.sh文件中找到。Helm部署命令是'helm install kafka ./kafka',卸载命令是'helm uninstall kafka'。

系统部署

系统部署是Helm图表angularandspringkafka的组合。它通过Zookeeper部署Kafka,通过MongoDB部署AngularAndSpring,并提供一个NodePort来访问AngularAndSpring的用户界面。 Helm图表可以在angularandspringwithkafka中找到。values.yaml_helpers.tpl是扩展的,template.yaml是其他模板的组合。

系统部署的values.yaml看起来像这样。

YAML

webAppName: angularandspring
dbName: mongodb
webImageName: angular2guy/angularandspring
webImageVersion: latest
dbImageName: mongo
dbImageVersion: 4.4
volumeClaimName: mongo-pv-claim
persistentVolumeName: mongo-pv-volume

kafkaName: kafkaapp
zookeeperName: zookeeperserver
kafkaImageName: bitnami/kafka
kafkaImageVersion: latest
zookeeperImageName: bitnami/zookeeper
zookeeperImageVersion: latest
kafkaServiceName: kafkaservice
zookeeperServiceName: zookeeperservice

secret:
  name: app-env-secret
  nameKafka: kafka-env-secret
  nameZookeeper: zookeeper-env-secret

envApp:
  normal: 
    MONGODB_HOST: mongodb
    CPU_CONSTRAINT: true
    SPRING_PROFILES_ACTIVE: prod
    KAFKA_SERVICE_NAME: kafkaService
  secret:
    JWTTOKEN_SECRET: secret-key1234567890abcdefghijklmnopqrstuvwxyz
    
envZookeeper:
  normal: 
    ALLOW_ANONYMOUS_LOGIN: yes
  secret:
   ZOOKEEPER_TICK_TIME: "2000"
   
envKafka:
  normal: 
    KAFKA_BROKER_ID: "1"
    KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
    KAFKA_CFG_LISTENERS: PLAINTEXT://:9092
    KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://:9092
    KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: false
    ALLOW_PLAINTEXT_LISTENER: yes
  secret:
    KAFKA_ZOOKEEPER_CONNECT: "zookeeperservice:2181"  

第一部分是AngularAndSpring/MongoDb/Storage的Helm变量的定义。Docker镜像的名称和版本被设置,persistentVolume/persistentVolumeClaim得到一个名称。

下一部分是Kafka/Zookeeper的Helm变量,由Docker镜像名称和版本以及服务名称定义。

下一部分是为AngularAndSpring/Kafka/Zookeeper定义的秘密。

在'envApp'/'envZookeeper'/'envKafka'中是为Docker镜像配置定义的变量/秘密。

_helpers.tpl看起来像这样。

YAML

{{/*
Create envApp values
*/}}
{{- define "helpers.list-envApp-variables"}}
{{- $secretName := .Values.secret.name -}}
{{- range $key, $val := .Values.envApp.secret }}
- name: {{ $key }}
  valueFrom:
    secretKeyRef:
      name: {{ $secretName }}
      key: {{ $key }}
{{- end}}
{{- range $key, $val := .Values.envApp.normal }}
- name: {{ $key }}
  value: {{ $val | quote }}
{{- end}}
{{- end }}

{{/*
Create envZookeeper values
*/}}
{{- define "helpers.list-envZookeeperApp-variables"}}
{{- $secretName := .Values.secret.nameZookeeper -}}
{{- range $key, $val := .Values.envZookeeper.secret }}
- name: {{ $key }}
  valueFrom:
    secretKeyRef:
      name: {{ $secretName }}
      key: {{ $key }}
{{- end}}
{{- range $key, $val := .Values.envZookeeper.normal }}
- name: {{ $key }}
  value: {{ $val | quote }}
{{- end}}
{{- end }}

{{/*
Create envKafka values
*/}}
{{- define "helpers.list-envKafkaApp-variables"}}
{{- $secretName := .Values.secret.nameKafka -}}
{{- range $key, $val := .Values.envKafka.secret }}
- name: {{ $key }}
  valueFrom:
    secretKeyRef:
      name: {{ $secretName }}
      key: {{ $key }}
{{- end}}
{{- range $key, $val := .Values.envKafka.normal }}
- name: {{ $key }}
  value: {{ $val | quote }}
{{- end}}
{{- end }}

该脚本为AngularAndSpring/Zookeeper/Kafka部署创建值和秘密。helpers.list-envApp-variables "是作为一个例子使用的。其他脚本的工作原理与此类似。

helpers.list-envApp-variables "被定义。

秘密名称被设置,然后脚本在秘密值上进行迭代。对于每个秘密都有一个名称和secretKeyRef,其中有创建的名称和密钥。这将秘密作为一个不透明的配置值提供给Docker镜像。

然后脚本遍历正常值。对于每个值,都会创建一个名称和一个值。

这些值和定义在template.yaml中的Secrets就是配置的Docker镜像。

总结 部署

用于开发的Kubernetes设置已经为创建系统部署奠定了基础。如果Kafka的版本发布后不再需要Zookeeper,Kafka就可以作为Docker镜像启动。Helm图可以扩展到部署/配置几个系统。 在这种情况下,有4个镜像。一个Kafka集群可以得到它自己的Helm图来进行部署,一个中央/可扩展/容错的Kafka集群可以被可以访问该集群的应用程序使用。应用(微服务)和它们的数据库如果相互依赖,应该一起部署。

Kubernetes 应用 Docker(软件) 事件 kafka 内存(存储引擎) Spring Boot 流(计算) 字符串 数据类型

DZone撰稿人所表达的观点属于他们自己。

DZone上的热门文章


评论

微服务 合作伙伴资源