线上问题排查

发布时间 2023-06-19 01:09:52作者: MarkLeeBYR

果园浇水服务作为果园项目的P0级别死保服务,因业务场景需要,具有qps高,功能多,负载大的特点。早先的浇水服务偶有超时现象,发现接口超时大多时候,数据库有建连操作。线上机器31台,YZ和ZW各有15,16台,单实例8c16g。

后续为了响应公司计划,将机器缩容至YZ和ZW机房各4台,单实例4c16g。浇水接口的性能问题,因为缩容而放大了。根据我们的QPS估计,理论上讲,8台机器完全可以承载线上请求。

实际情况是:当核数缩至4c后,cpu不足以处理请求,ktrace和日志中均存在无法追踪的停顿(无日志),高峰期由于请求处理不过来,grpc连接池被撑爆。

在此情况下,为了排查接口问题,并确定问题的产生,过程中对几个资源池进行了优化,优化后接口响应及早晚高峰的情况有所改观,但未能彻底解决早晚高峰请求处理不过来的问题。最后把核数恢复8core。由于grpc线程池不再被撑爆,线程池活跃线程数始终在***以下,于是调整了一下grpc线程池大小,接口响应明显降低。

之后不再出现无法追踪的超时报警,且延迟请求曲线正常情况下表现平缓稳定,接口优化有了阶段性的成果。


 

解决问题

一:合理配置线程池资源

场景

接口超时的突刺经常出现在数据库建立连接时,优先解决这个问题就涉及到了合理配置资源线程池。

通过服务监控发现redis和mysq的连接一直不停在建立,之后排查发现我们一直使用的是默认的线程池。

夸张的情况像这样:

因此对数据库资源池,连接数,keep-alive时长等进行了配置优化,针对不同服务对数据库的使用情况进行了定制化配置。

 

理论

合理配置线程池可以有效地减少内存及CPU开销,当我们和数据库创建连接时,如果每次都需要重新创建线程和连接,不仅会消耗内存,造成不必要的资源浪费,多个线程导致CPU在线程上下文上的切换开销增大,使CPU资源紧张,同时也会增加JVM的回收频率,对程序造成一定的影响。频繁创建&回收连接,会阻塞持续打过来的请求,进而拖慢服务响应。

何为合理配置,背书方式告诉我们区分IO密集型及CPU密集型、混合型。根据不同操作选择:

CPU密集型:线程池核心数 = CPU核心数+1线程

IO密集型:因为处理IO的时间不会占用CPU,因此可以根据使用多配置一些线程。

鉴于我们大多操作既有IO也有CPU,没必要教条跟核心数扯上太多关系。利特尔法则更适合实操场景,利特尔法则提出了这样一个理论:L = λW

L表示一个系统的任务数,λ表示任务入队频率,W代表任务系统中平均的等待时间。

用个知乎上的例子来说明一下:

  • 已知你正在就读的大学每年从全国各地招收2000名学生, 每名学生均需要4年才能毕业离校,请估算目前在你们大学就读的学生总人数。

这道题目的答案是8000 (2000 * 4), 原因是你的大学一直保持着4年招收学生的人数,而每年学校招收2000人,因此得出答案。

所以,我们在估算线程数时,最好是按一段时间内的真实任务数来估算线程池大小。

有个比较常见的场景是,线程池设置得很大,然而线程池过大在一些场景反而会导致程序性能下降。CPU资源、内存资源、I/O资源都是有限的,过大的线程池,过多的负载需求反而可能导致资源临界,导致性能低下;同时,核心线程不会被销毁,大量闲置的线程资源在回收中不停被标记,也会有一定程度影响程序响应。

综上,我们要对默认的连接池进行调整,用以适配真实服务场景,并且对资源连接时间进行优化,能保持合理的数据库连接数不被释放,不用在每日晚上低流量时连接释放,早上流量打进来频繁建连导致性能拖垮。

 

mysql线程池调整

  • 配置
  • 默认配置
  • KDB查看资源配置在这里,这里配置的值是公共配置,如果想对不同服务配置,需要去Kconf中进行灰度配置
  "maxConnection": 100,
  "minConnection": 0,
  "idleTimeoutMs": 1h

从默认配置我们得到一个结论,如果我们使用默认配置:最大连接数是100,最小连接数是0,当1小时连接不被使用,连接就会被释放。同redis配置一样,过一晚上,大部分连接都会失效。

  • 观察监控
  • 以及资源池
  • 配置
  • 方式和redis一样,利用一段时间的total连接数除以实例数量
  • minConnection = total / 实例数量
  • 重复步骤直到高峰期不会频繁建连,如上两张图一样
  • 如果不想连接被释放,minConnection要调大

改造后的P995监控在高峰期没有产生突刺

 

redis线程池适配

  • 配置
  • cronus代码配置类:
  • 服务启动时会根据集群配置对客户端进行初始化
JedisClusterByZooKeeper(AvailableZone az, JedisClusterConfig clusterConfig,Supplier<? extends ILogTopic> oldReplicaTopic) {}
  • 能重写的方法(定制的属性)有限,只能定义operationTimeout即socketTimeoutMs
  • 客户端资源池属性配置类:RedisClientConfig
  • 初始化:这个方法里指定了一种从kconf读取配置方式
static ConfigAdaptor<RedisConfigModel.RedisClientConfig> getCoverRedisClientConfig(JedisClusterConfig config) {}
  • 默认配置:
//从这个方法找到的
static GenericObjectPoolConfig getObjectPoolConfig(@Nullable JedisClientConfig jedisClientConfig) {}

//默认配置是这个
Integer poolMaxTotal = 100;
Integer poolMaxIdle = 100;
Integer poolMinIdle = 0;
Long poolMaxWaitMs = 2s;
Long poolMinEvictableIdleTimeMillis = -1;
Long poolTimeBetweenEvictionRunsMillis = 10min;
Integer connectTimeoutMs = 2000;
Integer socketTimeoutMs = 8000;

从默认配置我们得到一个结论,如果我们使用默认配置:最大连接数是100,最小连接数是0,当10分钟连接不被使用,连接就会被释放。

而浇水服务特点之一就是,qps在夜里达到低谷,早上7:30左右一分钟qps翻倍甚至多倍。那么连接池的大多资源已被释放,在流量激增时,便开始频繁建立连接,占用了本不需要重新分配的IO资源和CPU资源。

 

当时的现场已经在裂变平台侧不好找了,下面是一个使用默认资源池的资源池使用监控

  • 配置

比方我们观察到早高峰有一波建连,或服务的建立连接情况类似于下图

a. 选中建连比较频繁的一段监控(表象明显,从无到有,持续一段时间,然后又变无),如果像上图一样建连很规律很平均,就选择释放连接时间(默认的poolTimeBetweenEvictionRunsMillis = 10min)打开此表的edit页面,选中total配置,查看这段时间总共创建了多少次连接。

b. poolMinIdel = total / 实例个数,其他配置酌情配置,poolMinIdel这个参数的关键处在于,无论空闲多久都不会被释放

c. 创建完后,继续观察监控,一个是redis链接成功次数统计中,建连依然频繁,或是资源池跟上面一样起起伏伏,那么说明poolMinIdel还不够大,我们再继续重复a步骤,直到这样

(以下为浇水服务)

以及

 

Grpc线程池 & 自定义业务线程池配置

 

  • 配置方式
  • 尽可能大地利用线程池,不要浪费资源,使用率留有一定buffer即可,比如浇水服务线程池是这样的,从核心线程数600,队列长度300,改为核心线程数64,队列128

 

改造后接口平均延迟明显下降(在四点半修改后分机房升级后的表现明显优于之前)

 

二:流量治理

场景

调整完线程池后,解决了由于数据库建连导致的突刺,剩余的突刺大多是由于第三方服务可用性下降导致的,这是难以避免的,机房间通信,第三方服务性能本身都可能造成问题。为保障被调用服务的延迟不影响到自己的服务,必要的限流阻断措施是有必要的。

KESS流量治理

  1. 解决
  2. https://kess-test.corp.kuaishou.com/#/strategy/detail
  3. tips:
  4. 请结合实际场景进行配置
  5. 找组内人review
  6. 隔板配置可以先进行测试打点,再进行实际运行
  7. 其他的文档里写的很详细,我也是初次趟趟,感觉还挺方便

 

三:容器管理

CPU Throttled

搞到这里,其实我们还没有解决缩容后带来的问题,为什么超时会增加,为什么早上的流量扛不住了,为什么单个请求,同一个线程中有莫名的停顿,在高峰期达到了5s甚至更高,而这时,我们的CPU逻辑核平均使用率看起来很正常?同时,用top命令能观察到有si和sy的值很高的逻辑核

之后就关注到了CPU Throttled这个指标,发现cpu节流时有发生,因节流而导致的中断时间也很“可观”。如下图的监控:

之后便把视线聚焦到了CPU Throttled这个监控上。CPU Throttled,是k8s借助cgroup和CFS对CPU资源的限制和管控机制。CFS调度器(Completely Fair Scheduler)是Linux的内核调度器,可以理解为调度器维护着一个任务池,确定某一时刻CPU到底应该执行哪个任务。为了尽可能维持Fair,它的模型在轮询调度的基础上增加了针对单个进程的vriture_runtime的计算,利用进程权重配置和实际运行时间计算出来的vruntime最终决定了哪个进程被分配更多的CPU。如果vruntime值越小,说明这个进程该被优先执行(被分配更多的处理器时间片)。

k8s利用cgroup进行资源管理和隔离,我们在为容器分配资源时,比如

    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

单位后缀m表示“千分之一个核心”,所以这个资源对象的容器进程需要250/1000的核心(25%),并且最多使用500/1000的核心(50%)。类似的,2000m表示两颗完整核心,可以用2,2.0来表示

最终会被翻译成cgroups和CFS的参数配置

$ cat cpu.shares
256
$ cat cpu.cfs_quota_us       //cpu.cfs_quota_us:表示该control group限制占用的时间(微秒),默认为-1,表示不限制。如果设为50000,表示占用50000/100000=50%的CPU。
50000
$ cat cpu.cfs_period_us      //cpu.cfs_period_us:cpu分配的周期(微秒),默认为1秒即1000000
100000

还有一个默认参数

$ cat /proc/sys/kernel/sched_latency_ns
24000000

所以在这个节点上,正常压力下,系统的CFS调度周期为24ms,CFS重分配周期为100ms,而该POD在一个重分配周期最多占用50ms的时间,在有压力的情况下,POD可以占据的CPU Share比例是256

有关CFS等姿势可以参照官方指导(链接

找个例子说明一下不同资源需求的POD容器是如何在CFS调度下占用CPU资源的:

在这个例子中,有如下系统配置情况:

  • CFS 调度周期为 10ms,正常负载情况下,进程 ready 队列里面的进程在每 10ms 的间隔内都会保证被执行一次
  • CFS 重分配周期为 100ms,用于保证一个进程的 limits 设置会被反映在每 100ms 的重分配周期内可以占用的 CPU 时间数,在多核系统中,limit 最大值可以是 CFS 重分配周期 * CPU 核数
  • 该执行进程队列只有进程 A 和进程 B 两个进程
  • 进程 A 和 B 定义的 CPU share 占用都一样,所以在系统资源紧张的时候可以保证 A 和 B 进程都可以占用可用 CPU 资源的一半
  • 定义的 CFS 重分配周期都是 100ms
  • 进程 A 在 100ms 内最多占用 50ms,进程 B 在 100ms 内最多占用 20ms

所以在一个 CFS 重分配周期 (相当于 10 个 CFS 调度周期) 内,进程队列的执行情况如下:

  • 在前面的 4 个 CFS 调度周期内,进程 A 和 B 由于 share 值是一样的,所以每个 CFS 调度内 (10ms),进程 A 和 B 都会占用 5ms
  • 在第 4 个 CFS 调度周期结束的时候,在本 CFS 重分配周期内,进程 B 已经占用了 20ms,在剩下的 8 个 CFS 调度周期即 80ms 内,进程 B 都会被限流,一直到下一个 CFS 重分配周期内,进程 B 才可以继续占用 CPU
  • 在第 5-7 这 3 个 CFS 调度周期内,由于进程 B 被限流,所以进程 A 可以完全拥有这 3 个 CFS 调度的 CPU 资源,占用 30ms 的执行时间,这样在本 CFS 重分配周期内,进程 A 已经占用了 50ms 的 CPU 时间,在后面剩下的 3 个 CFS 调度周期即后面的 30ms 内,进程 A 也会被限流,一直到下一个 CFS 重分配周期内,进程 A 才可以继续占用 CPU

另一个简单的例子来说:

假使cpu.cfs_period_us设定为100毫秒(这是全局统一的设置),cpu.cfs_quota_us设定为20毫秒(这是每个应用在K8s自己设定的,当前的配置等同于给Pod分配200millicore)。这意味着CFS会每隔100毫秒会重新分配应用的CPU使用权,而在每个100毫秒内,应用可以占用CPU20毫秒。在物理机上单独部署,由于我们没有使用cgroups,程序可以用尽空闲的CPU。但是假如一个请求在物理机上需要花100毫秒,在刚刚设定的K8s环境下则会发生如下现象:

运行20毫秒 -> 挂起并等待80毫秒(节流) -> 运行20毫秒 -> 挂起并等待80毫秒-> 运行20毫秒 -> 挂起并等待80毫秒-> 运行20毫秒 -> 挂起并等待80毫秒-> 运行20毫秒 -> 请求完成并返回

原本只需100毫秒的请求,现在却花费了420毫秒。

这就是为什么在物理机上部署的程序,放到容器里反而响应变慢的原因。

如果想解决这个问题,我们需要调整节流的限制,由于我们没有权限直接修改,那么增加核心数就可以提高limit的限制。

最后我们从4Core恢复了8Core核心数,修改后效果明显,解决了高峰期需求处理不过来的情况。

接口性能比对,改造前:

改造后:

服务端P995监控:

 

四:监控配置

场景

小伙伴排查时发现我们的报警配置是每次有p995超过1000ms就报警,这会把一些网络抖动也计算进去,太多的偶发性警报,容易让大家忽略掉真正需要处理的报警信息。

Grafana报警配置

  1. tips:配置要容忍可能的网络抖动,可以配置为三分钟采集平均时常的时间大于1s时报警,慎选max()函数
  2. 配置完后是这样

 

 

解决结果

  1. 浇水接口每日早上流量激增时,不再爆grpc线程池
  2. 接口耗时监控 (P995&P99)无异常突刺,曲线平稳
  • 异常突刺不包括:
  • 公司基础架构、KCC、容器、网络等公共组件报警
  • 数据库连接异常中断
  • 具有突发性、偶见性、可追溯的非裂变平台业务导致的性能问题
  • 曲线平稳特征:十分钟内(P995&P99)无超过200ms的异常连续起伏

 

从31台机器缩到现在的8台,性能无明显大的损失

 

此次排查监控一览

  1. JVM GC监控
  2. 风控调用链监控
  3. Klog页面查询有限,能接入kibana也很方便(链接
  4. ktrace就不多说了(链接
  5. GRPC服务端统计(链接)(链接2
  6. Java线程池监控(链接
  7. redis调用统计(链接
  8. RPC Monitor(链接
  9. 资源连接数统计(链接
  10. Falcon(链接