DZone>微服务区>在Spring Boot中使用流的Reactive Kafka
在Spring Boot中使用流的反应式Kafka
如何将Spring Boot和Kafka与流整合到一个反应式解决方案中。
通过
-
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 "用EventMapper和MyUserServiceEvents类的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图表angularandspring和kafka的组合。它通过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上的热门文章
评论