Kafka如何实现高性能I/O
使用批量消息提升服务端处理能力Kafka提供了单词发送一条消息的send方法,但实际上,Kafka的客户端在实现消息发送时,采用了异步批量发送的机制。也就是说,当你调用send时,它并不会立即将消息发出去,而是放在缓存当中,到合适的时机把缓存中消息组成一批一次性发送给Broker。
而接收端,Kafka不会把一批消息还原成多条,而是每条消息都当作批消息来处理,在Broker整个处理流程中,这些被组合在一起的消息始终都是一个整体,不会被拆开。
使用顺序读写提升磁盘 I/O性能对于磁盘来说,它有一个特性,就是顺序读写的性能要远远好于随机读写。因为操作系统每次从磁盘读写数据的时候,需要先寻址,也就是先要找到数据在磁盘上的物理位置,然后再进行数据读写。所以随机读写会花费大量时间在寻址上,而顺序读写只需要找到第一个位置,接着往下写久可以了。
Kafka存储设计非常简单,对于每个分区,它把从Producer 收到的消息,顺序地写入对应的 log 文件中,一个文件写满了,就开启一个新的文件这样顺序写下去。消费的时候,也是从某个全局的位置开始,也就是某一个 log 文件中的某个位置开始,顺序 ...
StampedLock
StampedLock 支持的三种锁模式StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。
不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。示例如下:
1234567891011121314151617final StampedLock sl = new StampedLock(); // 获取/释放悲观读锁示意代码long stamp = sl.readLock();try { //省略业务相关代码} finally { sl.unlockRead(stamp);}// 获取/释放写锁示意代码long stamp = sl.writeLock();try { //省略业务相关代码} finally { sl.unlockWrite(st ...
消息队列问题答疑
网关如何接收服务端的秒杀结果这里只是一个很简单的例子,省去了很多的细节,不同的系统也有不同的设计,思路仅供参考。
网关在收到 APP 的秒杀请求后,直接给消息队列发消息。如果发送消息失败,可以直接给 APP 返回秒杀失败结果,成功发送消息之后,线程就阻塞等待秒杀结果。这里不会无限等待,会设置一个超时事件。等待结束之后,去存放秒杀结果的 Map 中查询是否有返回的秒杀结果,如果有就构建Response,给 APP 返回秒杀结果,如果没有,按秒杀失败处理。
RocketMQ 和 Kafka 的消息模型现在,假如有一个主题 MyTopic,我们为主题创建 5 个队列,分布到 2 个 Broker 中。有三个生产者Produer0,Produer1 和Producer2。
这三个生产者与2个Broker可以随便对应,也就是说可以轮询发消息,或者说一个生产者的消息全部发送到一个Broker中。
至于消费端,有消费组、消费者和队列这几个概念。
每个消费组就是一份订阅,它要消费主题 MyTopic 下,所有队列的全部消息。这里要注意,消费了的消息并不会从队列中删除,只是从队列中读取了消息。
多个消 ...
如何处理消息积压
优化性能来避免消息积压在使用消息队列时,我们主要考虑消息的发送方和接收方这两部分的处理,而不需要关注消息队列的处理能力,因为业务逻辑往往复杂于消息队列的处理,而且这两者并不是一个量级。
所以,对于消息队列的性能优化,我们更关注的是,在消息的收发两端,我们的业务代码怎么和消息队列配合,达到一个最佳的性能。
1. 发送端性能优化发送端可能存在发送速率没有设置好,从而导致消息处理太慢。这里的一个解决办法就是并发的进行消息发送,或者说批量的进行发送,都可以提高发送端的性能。
2. 消费端性能优化当消费端的处理能力长时间低于发送端的发送能力时,就会导致消息积压。会导致两种结果,消息队列填满,无法对外服务,或者消息丢失,这都是比较严重的事故。
消费端的优化,除了优化业务逻辑外,还可以水平扩容,增加消费端的并发数来提升总体的消费性能。特别需要注意的一点是,在扩容 Consumer 的实例数量的同时,必须同步扩容主题中的分区(也叫队列)数量,确保 Consumer 的实例数和分区数量是相等的。如果 Consumer 的实例数量超过分区数量,这样的扩容实际上是没有效果的。因为对于消费者来说,在每个分区上 ...
TCP拥塞控制
TCP必须使用端到端拥塞控制而不是使网络辅助的拥塞控制,因为IP层不向端系统提供显式的网络拥塞反馈。TCP所采用的方法是让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。如果感觉到没什么拥堵,就加快发送速度,如果有拥堵,就减少。
怎么限制发送速率:TCP发送方的拥塞控制需要一个额外的变量,拥塞窗口。发送方中已发送但是未被确认的数据量不会超过拥塞窗口(cwnd)和rwnd的最小值。该约束限制了发送方中未被确认的数据量,因此间接地限制了发送方的发送速率。
如何感知拥堵:TCP发送方的丢包事件定义是,要么超时,要么收到接收方3个冗余的ACK。当出现过度拥堵时,路径上的一台或多台路由器缓存会溢出,引起数据包被丢弃,然后发送方会得知丢包事件,然后就会认为出现了拥塞。
简单来说,当正确收到接收方的ACK时,会认为网络没有拥堵,慢慢调大拥塞窗口,增加发送速率。当出现丢包时,就认为网络拥堵,减小拥塞窗口,减小发送速率。
TCP拥塞控制算法包括三部分:
慢启动
拥塞避免
快速恢复
慢启动在刚开始建立连接时,拥塞窗口的大小比较小,而每当正确接收到ACK确认时,就增加拥塞窗口的大小 ...
拥塞控制原理
拥塞原因与代价情况1:两个发送方和一台具有无穷大缓存的路由器两台主机(A和B)都有一条连接,且这两条连接共享源与目的地之间的单跳路由,如下图所示:
由于路由器的缓存是无限制的,那么发送方的发送速率达到一定程度,路由器中的平均排队分组数就会无限增长,源与目的地之间的平均时延也会变成无穷大。
情况2:两个发送方和一台具有有限缓存的路由器这里对情况1稍微做一些修改,假定路由器的缓存容量是有限的。这种假设的结果是,当分组到达一个已满的缓存时会被丢弃。然后我们假设连接可靠,即分组在路由器中被丢弃时,会重发。
假设发送方知道路由中缓存容量,只有在路由未满时才发送,那么就不会导致分组丢失。
但一种更为真实的情况是,发送方并不知道,那么在缓存满时发送分组,带来的代价就是需要重新发送。而且存在一种情况,分组发出但是未丢失,还在排队当中,发送方因超时重新发送了分组,那么就会导致接收方需要丢弃一个重传分组,带来了不必要的开销。
这里就看出拥塞的两个代价,发送方需要重传丢失的分组,而且发送方可能重传不必要的分组。
情况3: 4个发送方和具有有限缓存的多台路由器及多跳路径考虑下图的情况:
由于路由器R2 ...
如何利用事务消息实现分布式事务
什么是分布式事务?首先事务是要保证我们对一系列数据进行一些操作,这些操作要么都成功,要么都失败。一个严格意义的事务实现,应该具有 4 个属性:原子性、一致性、隔离性、持久性。
但是,对于分布式系统来说,严格的实现 ACID 这四个特性几乎是不可能的。一般会采用一些“妥协”的方案,比如说顺序一致性或者最终一致性。
消息队列是如何实现分布式事务的?一个订单和购物车的模型如下图:
这里的半消息,并不是只发送数据信息的一半,而是说发送全部的数据,但是在事务提交之前,消费者是无法看到这条数据的。
这里,发送半消息后,就可以继续执行创建订单,如果订单创建成功,则提交事务,那么消费者,也就是购物车模块可以看到这条消息,然后从购物车删除对应订单的物品。如果订单创建失败,则消费者无法看到消息,也就不会导致创建订单失败,但是购物车删除物品这种情况。
但是还存在一个问题:如果提交事务消息失败时,还是会存在一定的问题,针对这个问题,Kafka 和 RocketMQ给出了不同的解决办法。Kafka 的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。我们可以在业务代码中反复重试提交,直到提交成功,或者删除之 ...
缓存如何做到高可用
分布式缓存的高可用方案有以下三类:
1、客户端方案:在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。
2、中间代理层方案:在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。
3、服务端方案:Redis 2.4 版本后提出的 Redis Sentinel 。
客户端方案在该方案中,需要注意缓存的读写。写需要将数据分片,而读可以用多组缓存做容错,提升系统的可用性。
缓存数据分片当单一机器无法存储所有的数据时,我们就需要将缓存分配到不同的机器上,每个节点上存储部分数据。
一般来讲,分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。
1、Hash分片Hash分片就是对key做哈希计算,然后对总的缓存节点取余。该算法优点是简单,缺点是当增加或者减少分片节点数量时,计算的方式也要发生变化。该方案适合缓存命中率下降不敏感的业务。
2、一致性Hash一致性 Hash 算法可以很好地解决增加和删减节点时,命中率下降的问题。
该算法中,我们将整个Hash值空间 ...
如何正确的选择缓存读写策略
Cache Aside(旁路缓存)策略读策略为:
从缓存中读取数据,命中则直接返回
缓存未命中,则从数据库查询,然后写入缓存,并返回给用户
写策略为:
更新数据库中的数据
删除缓存的数据
先删除缓存再删数据或者先删数据库再删除缓存都会导致一定的问题,分析比较简单,不做具体说明。
存在的问题如果新注册了一个用户,然后立刻发起了查询(此时缓存无法命中),如果查询走的是从库,而且存在一定的时延,那么会有可能查询不到个人信息。
解决办法就是在特定的场景下,我们可以修改后将修改的信息写入缓存当中,而不是删除。
而且该策略对于频繁的修改会导致缓存中的数据被频繁的清理,造成缓存命中率低。
两种解决办法:
一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务的影响也是可以接受。
Read/Write Through(读穿 ...
TCP连接的管理
建立连接过程TCP建立连接的过程如下:
第一步:客户端的TCP首先向服务器端的TCP发送一个特殊的TCP报文段。该报文段中不包含应用层数据。该报文段会将首部中的SYN置为1,这个特殊报文段被称为SYN报文段。
客户会随机地选择一个初始序号(client_isn),并将此编号放置于该起始的TCP SYN报文段的序号字段中。该报文段会被封装在一个IP数据报中,并发送给服务器。
第二步:一旦包含TCP SYN报文段的IP数据报到达服务器主机,服务器会从该数据报中提取出TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段。这个允许连接的报文段也不包含应用层数据。
在报文段的首部包含3个重要的信息。首先,SYN比特被置为1。其次,该TCP报文段首部的确认号字段被置为client _ isn + 1。最后,服务器选择自己的初始序号(server_isn),并将其放置到TCP报文段首部的序号字段中。
这个允许连接的报文段实际上表明了: “我收到了你发起建立连接的SYN分组,该分组带有初始序号client_isn。我同意建立该连接。我自己的初始序号是serve ...