高并发场景下 disk io 引发的高时延问题

该系统属于长连接消息推送业务,某节假日推送消息的流量突增几倍,瞬时出现比平日多出几倍的消息量等待下推。

事后,发现生产消息的业务服务端因为某 bug,把大量消息堆积在内存里,在一段时间后,突发性的发送大量消息到推送系统。但由于流量保护器的上限较高,当前未触发熔断和限流,所以消息依然在流转。

消息系统不能简单地进行削峰填谷式的排队处理,因为很容易造成消息的耗时长尾,所以在不触发流量保护器的前提下,需要进行的并发并行地去流转消息。

下图是我司长连接消息推送系统的简单架构图,问题出在下游的消息生产方业务端。

其实更重要的原因是前些日子公司做成本优化,把一个可用区里的一波机器从物理机迁移到阿里云内,机器的配置也做了阉割。突然想起曹春晖大佬的一句话:

没钱做优化,有钱加机器。

这样两个问题加起来,导致消息时延从 100ms 干到 3s 左右,通过监控看到高时延问题持续 10 来分钟。

分析问题

造成消息推送的时延飙高,通常来说有几种情况,要么 cpu 负载高?要么 redis 时延高?要么消费rocketmq 慢?或者哪个关键函数处理慢 ?

通过监控图表得知,load 正常,且网络 io 方面都不慢,但两个关键函数都发生了处理延迟的现象,这两个函数内除处理 redis 和 mq 的网络 io 操作外,基本是纯业务组合的逻辑,讲道理不会慢成这个德行。

询问基础运维的同学得知,当时该几个主机出现了磁盘 iops 剧烈抖动, iowait 也随之飙高。

但问题来了,大家都知道通常来说 Linux 下的读写都有使用 buffer io,写数据是先写到 page buffer 里,然后由内核的 kworker/flush 线程将 dirty pages 刷入磁盘,但当脏写率超过阈值 dirty_ratio 时,业务中的 write 会被堵塞住,被动触发进行同步刷盘。

推送系统的日志已经是 INFO 级别了,虽然日志经过特殊编码,空间看似很小,但消息的流程依旧复杂,不能不记录,每次扯皮的时候都依赖这些链路日志来甩锅。

阿里云主机普通云盘的 io 性能差强人意,以前在物理机部署时,真没出现这问题。????

解决思路

通过监控的趋势可分析出,随着消息的突增造成的抖动,我们只需要解决抖动就好了。上面有说,虽然是 buffer io 写日志,但随着大量脏数据的产生,来不及刷盘还是会阻塞 write 调用的。

解决方法很简单,异步写不就行了!!!

  • 实例化一个 ringbuffer 结构,该 ringbuffer 的本质就是一个环形的 []byte 数组,可使用 Lock Free 提高读写性能;

  • 为了避免 OOM, 需要限定最大的字节数;为了调和空间利用率及性能,支持扩缩容;缩容不要太频繁,可设定一个空闲时间;

  • 抽象 log 的写接口,把待写入的数据塞入到ringbuffer里;

  • 启动一个协程去消费 ringbuffer 的数据,写入到日志文件里;

    • 当 ringbuffer 为空时,进行休眠百个毫秒;

    • 当 ringbuffer 满了时,直接覆盖写入。

这个靠谱么?我以前做分布式行情推送系统也是异步写日志,据我所知,像 WhatsApp、腾讯 QQ 和广发证券也是异步写日志。对于低延迟的服务来说,disk io 造成的时延也是很恐怖的。

覆盖日志,被覆盖的日志呢?异步写日志,那 Crash 了呢?首先线上我们会预设最大 ringbuffer 为 200MB,200MB 足够支撑长时间的日志的缓冲。如果缓冲区满了,说明这期间并发量着实太大,覆盖就覆盖了,毕竟系统稳定性和保留日志,你要哪个?

Crash 造成异步日志丢失?针对日志做个 metrics,超过一定的阈值才开启异步日志。但我采用的是跟广发证券一样的策略,不管不顾,丢了就丢了。如果正常关闭,退出前可将阻塞日志缓冲刷新完毕。如果 Crash 情况,丢了就丢了,因为 golang 的 panic 会打印到 stderr。

另外 Golang 的垃圾回收器 GC 对于 ringbuffer 这类整块 []byte 结构来说,扫描很是友好。Ringbuffer 开到 1G 进行测试,GC 的 Latency 指标趋势无异常。

至于异步日志的 golang 代码,我暂时不分享给大家了,不是因为多抠门,而是因为公司内部的 log 库耦合了一些东西,真心懒得抽离,但异步日志的实现思路就是这么一回事。

结论

如有全量详细日志的打印需求,建议分两组 ringbuffer 缓冲区,一个用作 debug 输出,一个用作其他 level 的输出,好处在于互不影响。还有就是做好缓冲区的监控。

下面是我们集群中的北京阿里云可用区集群,高峰期消息的推送量不低,但消息的延迟稳定在 100ms 以内。


让技术也有温度!关注我,一起成长~

资料分享,关注公众号回复指令:

  • 回复【加群】,和大佬们一起成长。

  • 回复【000】,下载一线大厂简历模板。

  • 回复【001】, 送你 Go 开源电子书。

使用 uC/OS-III 的 FatFs 库,你需要修改 `diskio.c` 文件来适配你的硬件和文件系统。 以下是一个示例,假设你的硬件使用 SPI 接口连接 SD 卡,且 FatFs 库的 API 版本为 R0.13a。 ```c #include "ff.h" #include "diskio.h" #include "stm32f4xx_hal.h" #include "stm32f4xx_hal_spi.h" // SPI接口定义 #define SPIx SPI2 #define SPIx_CLK_ENABLE() __SPI2_CLK_ENABLE() #define SPIx_SCK_GPIO_CLK_ENABLE() __GPIOB_CLK_ENABLE() #define SPIx_MISO_GPIO_CLK_ENABLE() __GPIOB_CLK_ENABLE() #define SPIx_MOSI_GPIO_CLK_ENABLE() __GPIOB_CLK_ENABLE() #define SPIx_FORCE_RESET() __SPI2_FORCE_RESET() #define SPIx_RELEASE_RESET() __SPI2_RELEASE_RESET() #define SPIx_SCK_PIN GPIO_PIN_13 #define SPIx_SCK_GPIO_PORT GPIOB #define SPIx_SCK_AF GPIO_AF5_SPI2 #define SPIx_MISO_PIN GPIO_PIN_14 #define SPIx_MISO_GPIO_PORT GPIOB #define SPIx_MISO_AF GPIO_AF5_SPI2 #define SPIx_MOSI_PIN GPIO_PIN_15 #define SPIx_MOSI_GPIO_PORT GPIOB #define SPIx_MOSI_AF GPIO_AF5_SPI2 // SD卡片选引脚定义 #define SD_CS_PIN GPIO_PIN_12 #define SD_CS_GPIO_PORT GPIOD #define SD_CS_GPIO_CLK_ENABLE() __GPIOD_CLK_ENABLE() // 硬件初始化函数 void SPIx_Init(void) { SPI_HandleTypeDef hspi; hspi.Instance = SPIx; hspi.Init.Mode = SPI_MODE_MASTER; hspi.Init.Direction = SPI_DIRECTION_2LINES; hspi.Init.DataSize = SPI_DATASIZE_8BIT; hspi.Init.CLKPolarity = SPI_POLARITY_LOW; hspi.Init.CLKPhase = SPI_PHASE_1EDGE; hspi.Init.NSS = SPI_NSS_SOFT; hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; hspi.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi.Init.TIMode = SPI_TIMODE_DISABLED; hspi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLED; hspi.Init.CRCPolynomial = 7; HAL_SPI_Init(&hspi); } // 初始化SD卡片选引脚 void SD_CS_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SD_CS_GPIO_CLK_ENABLE(); GPIO_InitStruct.Pin = SD_CS_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(SD_CS_GPIO_PORT, &GPIO_InitStruct); HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_SET); } // 硬件SPI传输函数 void SPIx_Transfer(uint8_t *data_out, uint8_t *data_in, uint16_t len) { HAL_SPI_TransmitReceive(&hspi, data_out, data_in, len, 1000); } // 定义FatFs需要的全局变量 static volatile DSTATUS diskStatus = STA_NOINIT; // 硬件初始化函数,由FatFs库调用 DSTATUS disk_initialize(BYTE pdrv) { // 初始化SPI和SD卡片选引脚 SPIx_Init(); SD_CS_Init(); // 将SD卡片选引脚拉低,使SD卡进入SPI模式 HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_RESET); // 等待SD卡启动 HAL_Delay(500); // 获取SD卡状态 if (SD_Init() == SD_OK) { diskStatus &= ~STA_NOINIT; } else { diskStatus |= STA_NOINIT; } // 将SD卡片选引脚拉 HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_SET); return diskStatus; } // 硬件读取函数,由FatFs库调用 DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { // 计算读取地址 DWORD addr = sector * 512; // 将SD卡片选引脚拉低 HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_RESET); // 读取数据 if (SD_ReadBlock(buff, addr, count * 512) == SD_OK) { diskStatus &= ~STA_NOINIT; return RES_OK; } else { diskStatus |= STA_NOINIT; return RES_ERROR; } // 将SD卡片选引脚拉 HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_SET); } // 硬件写入函数,由FatFs库调用 DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { // 计算写入地址 DWORD addr = sector * 512; // 将SD卡片选引脚拉低 HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_RESET); // 写入数据 if (SD_WriteBlock((uint8_t *)buff, addr, count * 512) == SD_OK) { diskStatus &= ~STA_NOINIT; return RES_OK; } else { diskStatus |= STA_NOINIT; return RES_ERROR; } // 将SD卡片选引脚拉 HAL_GPIO_WritePin(SD_CS_GPIO_PORT, SD_CS_PIN, GPIO_PIN_SET); } // 硬件控制函数,由FatFs库调用 DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { // 根据命令执行相应的操作 switch (cmd) { case CTRL_SYNC: return RES_OK; case GET_SECTOR_COUNT: *(DWORD *)buff = SD_GetCapacity() / 512; return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; return RES_OK; case MMC_GET_TYPE: *(BYTE *)buff = 4; return RES_OK; default: return RES_PARERR; } } ``` 你需要根据你的硬件和文件系统的实际情况修改上述代码。例如,如果你的硬件使用的是 I2C 接口连接 EEPROM,那么你需要修改SPI相关的代码,改为I2C相关的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值