pagecache引起的生产速度变慢?

Kafka消息堆积时生产速度低问题分析

前言

去年在准备写书,很长一段时间没有更新文章,但最终还是因为自己的原因没有坚持写完,在这之后又有一段时间没有找到动力(实际上是想躺平~),所以没有任何输出。

最近突然看到自己的公众号开通了留言功能,所以还是打算重新进行一些整理输出(白话:想躺躺不平),一方面是方便自己回溯,另一方面是也可以就总结的内容进行交流,今天就来分享下最近深入分析的一个有关kafka的问题。

问题背景

在我们的实际使用场景中,在消息没有堆积的情况下(消费者速度能更上生产者的速度,即消费者组的lag几乎始终为0),生产和消费速度能同时达到一个较高的值。然而,当消息存在一定滞后时(例如先停止消费一段时间,再开启消费者),在很长一段时间内生产的速度是非常非常低的,这可能会导致消息在生产者客户端一侧因为不能及时发送而出现“丢”消息的情况。

pagecache的瓶颈?

对于生产者发送的消息,在kafka broker端的处理,就是调用系统的write接口,将消息写入操作系统的虚拟文件系统层(内部先将内容写入pagecache,然后再刷入到磁盘中);而对于消费者消费的消息,broker则是调用系统的read接口从虚拟文件系统读取(先判断所要读取的内容是否缓存在pagecache中,如果是则直接从pagecache读取,这也称之为缓存命中,如果没有则需要从磁盘读取并缓存到pagecache中)。

在无堆积的场景中,通过cachestat命令可以直观的看到系统pagecache的命中情况,基本维持在98%以上。

de6cfede22b6448d7c97294024c63048.jpeg

也就是说这种情况下, 消费者消费的消息都是缓存在pagecache中,fetch时直接从pagecache读取即可,整个过程没有磁盘的读IO。

a10f02b0f66dc2266fe4c1ed64a43877.jpeg

注:尽管broker内部用于存储消息的log文件是通过mmap进行映射的,但最终写入时依旧是将消息写入了pagecache。

了解到这些后,我们一开始的怀疑方向是:在有堆积的情况下, 由于大量滞后的消息从磁盘读取到pagecache中,是否pagecache的脏页dirty page存在瓶颈,会阻塞业务层的写操作,最终导致生产速度的变慢。

通过调整如下几个脏页相关的系统参数后:

// 开始触发脏页回写的阈值,当前设置为1280MB
vm.dirty_background_bytes = 1342177280
// 脏页的最大阈值,达到这个值后会阻塞io操作,当前设置为30GB
vm.dirty_bytes = 32212254720
// 数据在脏页中的过期时间,当前设置为30s
vm.dirty_expire_centisecs = 3000
// 检测周期,用于将过期数据回写磁盘,默认5s
vm.dirty_writeback_centisecs = 500

再次来测试,发现其生产速度并没有变化。同时,通过"sar -B"命令观察系统脏页情况,发现其脏页始终在1.5GB左右,远没有达到最大阈值,也就是说,不可能出现阻塞业务接口的情况,这也就能说明,在这个场景里,系统脏页大小与磁盘并非瓶颈所在。

接近真相

通过上面的测试证明了系统的pagecache与磁盘都不存在瓶颈,那么写入慢的原因只可能是在业务层,单位时间内写入的次数变少了,从而出现了整体生产速度变低的情况。为了验证这一点,我们分别在无堆积和有消息堆积的场景下,通过strace追踪broker进程内部对系统write的调用次数,与每次调用的耗时,其统计情况如下表所示:


无堆积
有堆积
单次write耗时0.000121397s0.000109609s
每秒write次数93986240

从单次write的耗时的对比也可以间接证明,pagecache层与磁盘是不存在瓶颈的。但从每秒的write次数则可以看出来,在堆积情况下的write次数远小于无堆积情况下的次数,这应该就是生产速度变慢的原因了。

找到了问题的方向后,我们又通过jmx分析了生产请求在broker内部的耗时情况,发现堆积情况下的生产请求总耗时明显大于无堆积情况下的耗时。

f86d86f5371073c4d3bcfe5f3699721d.jpeg

进一步分析生产请求在各个阶段的耗时之前,我们先通过一张图了解下,生产者请求在broker内部处理的大概逻辑。

8d47915794e36d3186087d921c00f8fc.jpeg

  • 当生产者或消费者的请求进入到kafka broker时,Processor线程负责从接收缓存区中获取请求并放入RequestQueue中,请求在RequestQueue中放置的时间为RequestQueueTime。

  • RequestHandler线程从RequestQueue中拉取请求并进行处理(将请求写入本地日志,或从本地日志读取内容),请求处理的耗时为LocalTime。

  • 当请求处理完毕后,会将请求传递给DelayedOperationPurgatory,满足指定条件后才发送请求响应,请求在本地处理完等待放到ResponseQueue的时间为RemoteTime。

  • 当满足条件后请求响应会放回到Processor的ResponseQueue中,请求响应在ResponseQueue中放置的时间为ResponseQueueTime。

  • 最后,Processor线程从ResponseQueue中拉取请求响应,并发送给生产者/消费者,其发送的时间为ResponseSendTime。

这几个时间加起来就是TotalTime。

具体来看下各阶段耗时的对比,如下图所示:

RequestQueueTime

757c630679d50fdd02e05e6e44481c2b.jpeg

堆积情况下,请求在队列中的耗时比无堆积情况下的要长,可以理解为无堆积情况下单位时间接收的请求要更多,因此在队列中等待的时间更长,可以说是符合预期的。

LocalTime

c535f28c0b115f3b5a02e5df5c7a77a8.jpeg

两种情况下, 本地的处理耗时几乎是一样的,再次证明在pagecache和磁盘层都不存在瓶颈。

ResponseQueueTime

359b51b7513ef594fb180fa9a5e1db14.jpeg

ResponseSendTime

012c2ca804ed76a4e9a44864b62a1569.jpeg

从这里可以看到, 请求响应在队列中等待的时间明显变长,同时请求响应发送的耗时也明显增加,因此,可以推导出结论,由于单位时间内请求响应等待发送与实际发送的耗时变长,从而导致整体生产速度降低。

问题原因

从JMX暴露的指标,我们看到了请求响应发送耗时明显增加导致了整体生产速度的降低,那为什么会出现这种现象呢?

我们又从jmx的指标里对比分析了两种情况下,单位时间内生产者请求数与消费者请求数:

c4261e89a5726fc33ef83b9ed679e29e.jpeg

08056660994e94832856da19b348c465.jpeg

从图中可以看到, 不管是生产的请求个数,还是消费请求个数,堆积场景下比无堆积情况下都是要少很多的。但是,堆积情况下的消费速度(吞吐量)和无堆积情况下的消费速度却是相差无几的。由此可以推断,在堆积场景下,单个消费请求获取到的数据量是要更多的,通过`wireshark`抓包,也证实了这一点:在无堆积情况下,单个请求响应大小在10KB到1MB之间波动(测试时单条消息在10KB左右),平均大小为几十KB,而在堆积情况下,请求响应大小几乎都是1MB。

而请求响应的大小和传输时间是正相关的,从抓包中可以看到,1MB的数据发送耗时为7ms左右,100KB的数据发送耗时为2ms左右。

在无堆积的情况下,消息几乎都是实时消费掉的,因此消费者每次`fetch`到的消息量是不大的,而堆积情况下, 每次`fetch`的消息是填满一整个`batch`,这就是两种场景下的区别,也就是发送耗时变长的原因。

但是,消费请求响应发送耗时的变大怎么影响到生产请求的呢?这就得梳理kafka broker内部Processor线程的处理逻辑了。

在Processor线程的处理逻辑中,关键在于,每次循环处理时,先处理请求响应:即先将responseQueue中的内容全部取出并发送给客户端(生产者/消费者),然后才从tcp连接的接收缓冲区中获取请求消息,并放到requestQueue中。

af4b1bde2bf8402f6131a103eaee3f38.jpeg

由于broker采用reactor的网络模型,即Acceptor接收到客户端新的tcp连接之后,是轮询将连接分配给Processor,负责后续tcp连接的数据交互,因此,同一个Processor线程中处理的连接可能同时包含生产者和消费者,当消费者fetch请求响应处理耗时变长,也就变相的拉低了整个生产者请求的处理速度。这也就是问题的真正原因。

分析出原因后,我们通过对消费者的速度进行了一定的限制(还是确保消费速度会大于生产速度,这样才能将滞后的消息消费掉),在堆积情况下, 生产的速度立马得到了提升。

小结

本文针对kafka在消息滞后消费(堆积)情况下,生产速度非常低的现象,从系统层到业务层,通过不同手段进行分析,定位到服务端网络处理方式引起的单位时间消费请求响应发送耗时变长变相拉低了生产者的速度。最后,通过对消费者消费速度进行一定的限制来保证堆积情况下具有一定的生产速度。

好了,这就是本文的全部内容,如果觉得本文对您有帮助,请点赞+转发,如果觉得有不正确的地方,欢迎留言交流~

<think>我们正在讨论SQL Server 2008 R2中insert和update操作变慢是否可能由Plan Cache污染引起。 首先,我们需要了解Plan Cache污染通常是指执行计划缓存中积累了大量的低效、未重用或过时的执行计划,导致内存压力增加,进而影响查询性能。但是,insert和update操作(数据修改操作)与select查询有所不同。 分析: 1. insert和update操作通常不会像select那样频繁地重用执行计划,因为每个insert/update语句操作的数据可能不同(尤其是非参数化的情况下),但参数化的insert/update语句可以重用计划。 2. Plan Cache污染的主要表现是内存压力增大,导致频繁编译和重编译,从而增加CPU负担,并可能使其他操作变慢。因此,如果insert/update操作需要编译执行计划,而缓存中大量无效计划导致内存不足,迫使SQL Server频繁清除缓存,那么insert/update语句可能需要重新编译,从而变慢。 3. 另外,insert/update操作变慢更常见的原因可能是: - 锁阻塞(例如,长时间运行的事务阻塞了其他事务) - 索引过多(每次插入需要更新多个索引) - 触发器(如果有,触发器中的代码执行慢) - 统计信息过时导致优化器选择了低效的执行计划(对于update/insert操作,虽然它们本身不涉及复杂计划,但可能涉及子查询或连接) - 磁盘I/O瓶颈(写入速度慢) - 日志文件增长或磁盘空间不足 - 约束检查(如外键、检查约束等) 根据引用[1](深入解析SQL Server 2008 笔记)和引用[3](关于跟踪事件),我们可以通过以下方式检查: 结论: Plan Cache污染可能是导致insert/update变慢的间接原因,但并不是主要原因。更常见的原因需要从其他方面分析。 然而,我们可以这样思考Plan Cache污染的影响: - 如果系统内存被大量无效计划占用,导致SQL Server内存压力增大,可能会使数据缓存(Buffer Pool)被挤压,从而增加磁盘I/O(因为需要从磁盘读取数据页),进而影响insert/update操作(因为写入也需要先读取相关页)。 - 另外,如果insert/update语句本身是动态SQL且没有参数化,那么每次执行都会生成新的执行计划,造成计划缓存膨胀,进一步加剧内存压力。 因此,虽然Plan Cache污染不是insert/update变慢的直接原因,但它可以作为一个系统级因素导致整体性能下降,进而影响所有操作。 建议的排查步骤: 1. 检查当前Plan Cache的使用情况,是否存在大量单次使用的计划(adhoc查询): ```sql SELECT objtype, cacheobjtype, COUNT(*) AS [Number of Plans], SUM(CAST(size_in_bytes AS BIGINT)) / 1024.0 / 1024.0 AS [Size in MB], AVG(usecounts) AS [Avg Use Count] FROM sys.dm_exec_cached_plans GROUP BY objtype, cacheobjtype ORDER BY [Size in MB] DESC; ``` 如果发现大量adhoc计划且使用次数为1,则说明存在计划缓存污染。 2. 检查insert/update语句的实际执行情况: - 使用SQL Server Profiler或扩展事件(Extended Events)捕获这些语句的编译和重编译事件(如SQL:BatchCompleted, SP:StmtCompleted, SQL:StmtRecompile)以及执行时间。 - 引用[3]提到了使用`sp_trace_setevent`来设置跟踪事件,可以捕获相关事件。 3. 检查是否由于参数嗅探导致insert/update语句(如果这些语句包含参数)使用了低效的计划。可以通过查看执行计划的编译参数和运行时参数来比较。 4. 检查系统内存压力: ```sql SELECT * FROM sys.dm_os_performance_counters WHERE counter_name IN ('Page life expectancy', 'Buffer cache hit ratio', 'Page reads/sec'); ``` - 如果Page life expectancy(页生命期)很低(如低于300秒),说明内存压力大,数据页被频繁换出。 - 如果Buffer cache hit ratio(缓存命中率)低于90%,说明物理读很高。 5. 检查阻塞: ```sql SELECT * FROM sys.dm_os_waiting_tasks WHERE blocking_session_id <> 0; ``` 6. 检查I/O性能: ```sql SELECT * FROM sys.dm_io_virtual_file_stats(NULL, NULL); ``` 计算磁盘延迟(avg_read_latency, avg_write_latency)是否在正常范围(通常低于20ms)。 如果发现Plan Cache污染严重,可以采取以下措施: - 强制参数化:`ALTER DATABASE ... SET PARAMETERIZATION FORCED;` - 定期清理计划缓存(谨慎使用,仅作为临时措施): ```sql DBCC FREESYSTEMCACHE ('SQL Plans'); ``` - 使用优化指引(Plan Guide)来固定高效的执行计划。 但请注意,直接清理整个计划缓存会导致所有查询重新编译,可能短时间内增加CPU负担。 总结:insert/update操作变慢可能是由多种原因引起的,Plan Cache污染可能是间接原因之一,但更应关注锁、索引、触发器、I/O等直接因素。建议按照上述步骤进行排查。 §§相关问题§§ 1. 如何判断SQL Server是否存在内存压力? 2. SQL Server中如何监控和优化insert/update操作的性能? 3. 在SQL Server 2008 R2中,如何减少计划缓存中的adhoc查询数量? </think> ### SQL Server 2008 R2中Insert/Update变慢与Plan Cache污染的关系 在SQL Server 2008 R2中,**Insert和Update操作变慢可能与Plan Cache污染有关,但通常不是直接原因**,需结合具体场景分析。以下是关键判断依据和排查步骤: --- ### 一、Plan Cache污染对Insert/Update的**间接影响** 1. **内存资源争用** - 当Plan Cache被大量低效计划占用时(如一次性adhoc查询),会导致内存压力增大 - 数据页缓存(Buffer Pool)可能被挤压,迫使SQL Server频繁读写磁盘 - **影响表现**:Insert/Update需要的数据页无法在内存中命中,磁盘I/O增加(参考引用[2]的I/O优化建议)[^2] 2. **查询编译开销增加** - 如果Insert/Update包含子查询或复杂条件(如`INSERT INTO ... SELECT ... WHERE`),需生成执行计划 - Plan Cache污染可能导致优化器花费更多时间查找/替换计划,增加CPU负载 - **验证方法**: ```sql SELECT * FROM sys.dm_exec_query_stats WHERE sql_handle IN (SELECT sql_handle FROM sys.dm_exec_requests WHERE command LIKE '%INSERT%' OR command LIKE '%UPDATE%') ``` 3. **锁竞争加剧** - 内存压力可能触发更多锁等待(如PAGEIOLATCH等待) - 通过监控等待类型验证: ```sql SELECT wait_type, waiting_tasks_count FROM sys.dm_os_wait_stats WHERE wait_type LIKE 'PAGEIOLATCH%' ``` --- ### 二、Insert/Update变慢的**更常见原因** 1. **索引过多** - 每插入/更新一行需维护所有相关索引,索引数量越多越慢 - **解决方案**:删除未使用的索引,使用`sys.dm_db_index_usage_stats`分析索引利用率 2. **触发器性能问题**(尤其复杂逻辑触发器) - 触发器中的低效SQL会被反复执行,可能受Plan Cache污染影响 - **检查命令**: ```sql SELECT OBJECT_NAME(parent_id) AS TableName, definition FROM sys.sql_modules WHERE OBJECTPROPERTY(object_id, 'IsTrigger') = 1 ``` 3. **日志写入瓶颈** - Insert/Update需写事务日志,磁盘延迟是常见瓶颈 - **监控指标**:`avg_disk_write_latency > 20ms`(通过`sys.dm_io_virtual_file_stats`) 4. **统计信息过时** - 导致优化器为WHERE/JOIN子句生成低效计划(参考引用[1]的统计信息重要性)[^1] --- ### 三、排查Plan Cache污染的**直接证据** 执行以下查询检查污染迹象: ```sql -- 检查占用内存大且重用率低的计划 SELECT TOP 20 objtype, usecounts, size_in_bytes/1024 AS size_kb, text AS query_text FROM sys.dm_exec_cached_plans CROSS APPLY sys.dm_exec_sql_text(plan_handle) WHERE usecounts < 3 -- 低重用 AND size_in_bytes > 102400 -- 大于100KB ORDER BY size_in_bytes DESC; ``` - **污染迹象**:存在大量`objtype='Adhoc'`且`usecounts=1`的大体积计划 --- ### 四、优化建议 1. **强制参数化减少adhoc计划** ```sql ALTER DATABASE YourDB SET PARAMETERIZATION FORCED; ``` 2. **清理无效计划缓存** ```sql -- 定期清理单次使用的计划 DECLARE @plan_handle varbinary(64) SELECT @plan_handle = plan_handle FROM sys.dm_exec_cached_plans WHERE objtype = 'Adhoc' AND usecounts = 1; DBCC FREEPROCCACHE(@plan_handle); ``` 3. **优化触发器与索引** - 禁用非关键触发器测试性能:`DISABLE TRIGGER TriggerName ON TableName` - 使用在线索引重建减少锁争用: ```sql ALTER INDEX ALL ON TableName REBUILD WITH (ONLINE = ON); ``` 4. **配置日志文件最佳实践**(参考引用[2]的存储优化)[^2] - 日志文件与数据文件分盘存储 - 设置合理初始大小避免自动增长 > **结论**:Insert/Update变慢通常需优先排查索引、触发器、I/O等直接因素。Plan Cache污染是次要因素,但当系统出现内存压力时需联合优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值