小滴中间件项目大课-自动化云测平台/Spingboot3.X/微服务/Kafka3.x

179 阅读7分钟

在当今快速发展的信息技术时代,中间件技术在企业信息化建设中扮演着至关重要的角色。自20世纪80年代以来,随着企业业务需求的不断变化,企业可能需要同时运行多个不同的业务系统,这些系统可能基于不同的操作系统、数据库和异构网络环境。中间件的出现,正是为了解决这些信息系统如何协同工作的问题,实现企业跨平台、分布式应用的目标。

“夏のke”》 Ukoou·ㄷㅁΜ

中间件的定义与作用

中间件(Middleware)是一种位于操作系统和应用程序之间的软件,有时也被认为是操作系统的一部分。它通过一组集成的中间件构成一个平台,包括开发平台和运行平台。中间件的核心功能是通信,它使得分布式系统中的各个部分能够相互通信。中间件的使用简化了企业应用的复杂性,使得开发人员能够面对一个简单而统一的开发环境,专注于业务逻辑的实现,而不必为程序在不同系统软件上的移植而烦恼。

中间件的特点

中间件应具备以下特点:

  1. 满足大量应用的需要:中间件需要能够支持广泛的应用场景。
  2. 运行于多种硬件和操作系统平台:中间件应具有跨平台的特性。
  3. 支持分布计算:提供跨网络、硬件和操作系统平台的透明性应用或服务的交互。
  4. 支持标准的协议和接口:这是中间件可移植性和互操作性的关键。

中间件的这些特点使其成为许多标准化工作的主要部分,对于应用软件开发来说,中间件的重要性甚至超过了操作系统和网络服务。中间件提供的程序接口定义了一个相对稳定的高层应用环境,保护了企业在应用软件开发和维护中的投资。

消息中间件——Kafka机制

储存模型

Kafka节点上,一个Partition的每个副本对应一个磁盘目录,新的日志,都是直接append到文件末尾,所以不管文件多大,写入总是O(1)的时间复杂度。但如果文件很大,顺序查找的效率也会很低。kafka通过两种方式解决:分段、索引。

分段

比如有100条 Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文 件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段 中。

索引

数据文件分段使得可以在一个较小的数据文件中查找对应offset的 Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立 了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。

索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。

注: index文件中并没有为数据文件中的每条Message建立索引,而是采用了 稀疏存储 的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。但缺点是没有建立索引的 Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。

高性能

顺序读写

kafka采用的磁盘的顺序读写比无序快了太多,这是由操作系统决定的,即使是普通的机械磁盘,顺序访问速率也接近了内存的随机访问速率。

即使是顺序读写,过于频繁的大量小IO操作一样会造成磁盘的瓶颈,此时又变成了随机读写。Kafka的策略是把消息集合在一起,批量发送,尽可能减少对磁盘的访问。所以,Kafka的Topic和Partition数量不宜过多,超过64个Topic/Partition以后,Kafka性能会急剧下降。

零拷贝

Kafka中存在大量的网络数据持久化到磁盘(Producer到Broker)和磁盘文件通过网络发送(Broker到Consumer)的过程。这一过程的性能直接影响Kafka的整体吞吐量。

Linux 2.4+内核通过sendfile系统调用,提供了零拷贝。数据通过DMA拷贝到内核态Buffer后,直接通过DMA拷贝到NIC Buffer,无需CPU拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件-网络发送由一个sendfile调用完成,整个过程只有两次上下文切换,因此大大提高了性能。

Kafka的数据传输通过TransportLayer来完成,其子类PlaintextTransportLayer通过Java NIO的FileChannel的transferTo和transferFrom方法实现零拷贝。

页缓存

Kafka并不太依赖JVM内存大小,而是主要利用Page Cache,如果使用应用层缓存(JVM堆内存),会增加GC负担,增加停顿时间和延迟,创建对象的开销也会比较高。

读取操作可以直接在Page Cache上进行,如果消费和生产速度相当,甚至不需要通过物理磁盘直接交换数据,这是Kafka高吞吐量的一个重要原因。

这么做还有一个优势,如果Kafka重启,JVM内的Cache会失效,Page Cache依然可用。

代码机制

kafka-producer.xml

<!--基本配置 -->
<bean id="producerProperties" class="java.util.HashMap">
    <constructor-arg>
        <map>
            <!-- kafka服务地址,可能是集群-->
            <entry key="bootstrap.servers" value="192.168.62.212:9092,192.168.62.213:9092,192.168.62.214:9092"/>
            <!-- 有可能导致broker接收到重复的消息,默认值为3-->
            <entry key="retries" value="10"/>
            <!-- 每次批量发送消息的数量-->
            <entry key="batch.size" value="1638"/>
            <!-- 默认0ms,在异步IO线程被触发后(任何一个topic,partition满都可以触发)-->
            <entry key="linger.ms" value="1"/>
            <!--producer可以用来缓存数据的内存大小。如果数据产生速度大于向broker发送的速度,producer会阻塞或者抛出异常 -->
            <entry key="buffer.memory" value="33554432 "/>
            <!-- producer需要server接收到数据之后发出的确认接收的信号,此项配置就是指procuder需要多少个这样的确认信号-->
            <entry key="acks" value="all"/>
            <entry key="key.serializer" value="org.apache.kafka.common.serialization.StringSerializer"/>
            <entry key="value.serializer" value="org.apache.kafka.common.serialization.StringSerializer"/>
        </map>
    </constructor-arg>
</bean>
<!-- 创建kafkatemplate需要使用的producerfactory bean -->
<bean id="producerFactory"
      class="org.springframework.kafka.core.DefaultKafkaProducerFactory">
    <constructor-arg>
        <ref bean="producerProperties"/>
    </constructor-arg>
</bean>
<!-- 创建kafkatemplate bean,使用的时候,只需要注入这个bean,即可使用template的send消息方法 -->
<bean id="KafkaTemplate" class="org.springframework.kafka.core.KafkaTemplate">
    <constructor-arg ref="producerFactory"/>
    <!--设置对应topic-->
    <property name="defaultTopic" value="bert"/>
</bean>
// 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:kafka-producer.xml")
public class KafkaTemplateTest {
    @Autowired
    private KafkaTemplate<Integer, String> kafkaTemplate;
    @Test
    public void hello(){
        kafkaTemplate.sendDefault("hello world");
    }
}

kafka-consumer.xml

<bean id="consumerProperties" class="java.util.HashMap">
        <constructor-arg>
            <map>
                <!--Kafka服务地址 -->
                <entry key="bootstrap.servers" value="192.168.62.212:9092,192.168.62.213:9092,192.168.62.214:9092" />
                <!--Consumer的组ID,相同goup.id的consumer属于同一个组。 -->
                <entry key="group.id" value="bert.mac" />
                <!--如果此值设置为true,consumer会周期性的把当前消费的offset值保存到zookeeper。当consumer失败重启之后将会使用此值作为新开始消费的值。 -->
                <entry key="enable.auto.commit" value="true" />
                <!--网络请求的socket超时时间。实际超时时间由max.fetch.wait + socket.timeout.ms 确定 -->
                <entry key="session.timeout.ms" value="15000 " />
                <entry key="key.deserializer"
                    value="org.apache.kafka.common.serialization.StringDeserializer" />
                <entry key="value.deserializer"
                    value="org.apache.kafka.common.serialization.StringDeserializer" />
            </map>
        </constructor-arg>
    </bean>
    <!--指定具体监听类的bean -->
    <bean id="messageListernerConsumerService" class="com.ximalaya.queue.KafkaConsumerListener" />
    <!-- 创建consumerFactory bean -->
    <bean id="consumerFactory" class="org.springframework.kafka.core.DefaultKafkaConsumerFactory">
        <constructor-arg>
            <ref bean="consumerProperties"/>
        </constructor-arg>
    </bean>
    <bean id="containerProperties" class="org.springframework.kafka.listener.config.ContainerProperties">
        <!-- 要消费的 topic -->
        <constructor-arg value="bert"/>
        <property name="messageListener" ref="messageListernerConsumerService"/>
    </bean>
    <bean id="messageListenerContainer" class="org.springframework.kafka.listener.KafkaMessageListenerContainer" init-method="doStart">
        <constructor-arg ref="consumerFactory"/>
        <constructor-arg ref="containerProperties"/>
    </bean>

生产者

  1. 直接使用KafkaProducer, DefaultKafkaProducerFactory 典型的工厂模式, 封装了kafka producer 配置

  2. KafkaTemplate 来了一个 经典的单例模式

     public class KafkaTemplate<K, V> implements KafkaOperations<K, V> {
         private final ProducerFactory<K, V> producerFactory;
         // volatile 保证多线程的可见性
         private volatile Producer<K, V> producer;
         private Producer<K, V> getTheProducer() {
             if (this.producer == null) {
                 synchronized (this) {
                     // 多重检查
                     if (this.producer == null) {
                         this.producer = this.producerFactory.createProducer();
                     }
                 }
             }
             return this.producer;
         }
     }
    

发送逻辑

public ListenableFuture<SendResult<K, V>> send(String topic, V data) {
    ProducerRecord<K, V> producerRecord = new ProducerRecord<>(topic, data);
    return doSend(producerRecord);
}
protected ListenableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord) {
    getTheProducer();
    final SettableListenableFuture<SendResult<K, V>> future = new SettableListenableFuture<>();
    getTheProducer().send(producerRecord, new Callback() {
        public void onCompletion(RecordMetadata metadata, Exception exception) {
            if (exception == null) {
                future.set(new SendResult<>(producerRecord, metadata));
                if (KafkaTemplate.this.producerListener != null
                        && KafkaTemplate.this.producerListener.isInterestedInSuccess()) {
                    KafkaTemplate.this.producerListener.onSuccess(producerRecord.topic(),
                            producerRecord.partition(), producerRecord.key(), producerRecord.value(), metadata);
                }
            }else {
                future.setException(new KafkaProducerException(producerRecord, "Failed to send", exception));
                if (KafkaTemplate.this.producerListener != null) {
                    KafkaTemplate.this.producerListener.onError(producerRecord.topic(),
                            producerRecord.partition(), producerRecord.key(), producerRecord.value(), exception);
                }
            }
        }
    });
    if (this.autoFlush) {
        flush();
    }
    return future;
}
  1. 将KafkaProducer 的send callback 转换为ListenableFuture
  2. 使用 producerListener 将“事件处理”逻辑与发送主流程解耦