【架构设计】【问题分析】记一次调用内部 es 服务超时问题
业务背景
我们有一个日志服务需要调用数据中台的 es 服务去写入日志数据(这里的技术选型有待商榷,不过作者无力逆转)。当前情况是 mongo 和 es 服务调用的双写,通过开关配置
当前架构
当前是批量写,在内存队列中累积,每个日志类型一个对象,队列大小 1000。两秒一个调度任务去双写,批量调用 es 服务。mongo 则是单点写,与 http 请求绑定的。
问题分析
6.2 上班,同事发现日志服务卡顿,请求响应慢。就让运维重启了容器(没有保留现场,只保留了容器日志)。之后问题依然,于是同事开始看日志,发现 es 服务不可用,于是去找数据中台的人 battle,然后吵起来了。。作者这时候被拉来救火。
因为只有日志了,那就只能通过日志+apm 来切入。大概有如下几种
分词过大
由于数据中台同事第一时间反馈的问题是我们的报文过大,把他们的服务打挂了(这里无力吐槽,做服务提供的,怪调用者把服务调用挂了,怎么说的出口的,脸不会红吗,怎么做的稳定性治理?)。然后甩给我们一个他们日志的报错。又说是我们的报文过大,最大一个报文接近两百兆,但是同事说之所以我们采用这个批量的架构方案,都是数据中台的建议(这里作者持中立意见,都有问题,应该做好自我保护)。
然后作者根据这个报错,去追溯了一下这个
content
字段,业务上就是日志的主体。原来在一个月前,数据中台的同事建议之下,es 的 mapping 类型由默认的 object 修改为了 flattened 类型flattened 相关优化可以参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.15/flattened.html
32766 字节这个限制,是 Lucene 针对一个 term 的限制,对应的是 flattened 的 ignore_above 属性,不过这个属性的单位是字符
ignore_above 参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.15/ignore-above.html
对应的优化方案的话,应该就是对应配置这个 ignore_above 字段,不过会截断日志,当前未使用。
网络通信失败
500 Internal Server Error
Read timed out
内存溢出
写入内存队列的时候
批量写的时候
架构设计
方案一:基于 http 做治理
限制单次请求日志数目
由于数据中台侧服务,兼容大报文,所以路由并没有限制报文大小
数据中台会做自我保护,所以需要尽量控制在数据中台允许范围内,否则会返回错误
目前的方案是 2 秒一次批量插入,所以可能会导致大报文。而且周期长很容易 oom,目前观测日志是有发生的(建议修改)
静态指标:
数据中台接口服务限制的单次报文大小
日志平均大小,建议取 80%
动态指标:
数据中台接口服务限制的单次报文大小
取出队列中所有日志,判断是否超过数据中台单次报文限制,不超过直接发送,超过则二分。剩余的放下次调度或者 mq
更好的方案是重写内存队列,入队总和超过单次报文限制,就串行发送,串行带来的并发压力,交给熔断去处理
非批次:直接采用单条发送
考虑直接自旋处理
同时不同类型的日志也建议采用线程隔离
限制单条日志大小
同样因为数据中台限制了报文大小,所以自然单条报文也受限于此
如果单条超限:见失败策略-failfast
限制并发数目
数据中台给出并发支持的区间
这里的失败策略:
时间窗口内超限则熔断,即引入限流。熔断后走失败策略
超过并发的剩余消息,延迟到下次调度任务写。指标判断也需要依赖熔断器,可以熔断后延迟写
问题在于,目前是批量写,用处有限
方案二:mq 自消费
mq 的消息 payload 都是单条日志消息
单条消息大小小于 mq 限制,rabbit 默认 128m。消费的时候与方案一一样需要处理单条消息的失败
并发通过存储压力分离,消费周期控制。
mq 重试还需要注意投递 dlx 或者 failfast,搭配告警。避免重试超限
资源上 mq 只有 rabbit 可用(哭泣),rabbit 不支持批量生产。
纯用 rabbit,因为没有做隔离,项目组共用,担心互相影响,磁盘压力
并发支持有限,可能要做延迟写,避免 broker 压力,与客户端延迟。
rabbit 不适合大数据场景,并发支持有限。这里去找了一些性能测试的数据。首先我们的诉求是支持秒级别超百兆的并发,性能测试中正好有类似的数据,10k 的数据,1w 的并发。测试数据反馈其中内存占用最高峰的测试项目是
Two publishers and no consumers
的时间段(虽然这里不消费,但是实际就算消费,每秒消费的数据也非常有限),达到了 5g。而且测试的机器是 14c 12g 的配置,我们自己的 broker 服务器配置只有 2c 8g。https://blog.rabbitmq.com/posts/2023/05/rabbitmq-3.12-performance-improvements/
方案三:兼并方案
维持现有架构
减少调度周期,目的是软性的减少单次报文大小
不同类型的日志也建议采用线程隔离
考虑更细粒度的自旋
接收到错误码,或者请求失败。就拆分单条入 mq 重试。这里相当于被动处理错误
主动避免错误,可以在方案一中折中选择
单条限制
动态指标
熔断最后兜底,失败扔 mq
这里相当于是
队列控制报文大小限制,同时也解决了 oom 的问题
熔断控制并发
mq 解决内存重试压力
失败策略
failfast,快速失败。直接丢弃,或者加上告警
failback,故障恢复。
内存重试:
当前线程重试
异步重试
异步排队重试
db 记录失败记录,分布式调度任务处理重试
mq 重试,
幂等问题,仅针对失败的 mq 消息
单条日志入 mq,异步遍历+重试
消费的时候,可以批量请求 es 服务
降级:写 mongo,写文件,但是如果后续不再是双写,读的时候不太好操作
以上策略再失败,则告警转人工
参考资料
https://www.elastic.co/guide/en/elasticsearch/reference/7.15/mapping.html
https://www.rabbitmq.com/admin-guide.html
评论