前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >鹅厂千亿级流量监控平台背后的技术干货~

鹅厂千亿级流量监控平台背后的技术干货~

作者头像
腾讯云可观测平台
发布2023-04-11 11:10:43
4120
发布2023-04-11 11:10:43
举报

张翔

腾讯云高级工程师。前端性能监控(RUM)产品核心开发,主要负责前端性能监控系统中的上报服务层模块的设计与实现。

| 导语ReadinessProbe(就绪探针) 和 LivenessProbe (存活探针)为 K8s 中的健康检查探针,如果设置不当,可能会给服务带来反作用,甚至会短时间内让服务宕机。RUM 是如何设置,减少超高突发流量带来的不必要麻烦。

前言

腾讯云可观测—前端性能监控的接入层服务目前部署在腾讯云容器服务(TKE)之上,随着业务的增长,在超高突发流量面前出现了几次服务级联故障,在复盘的过程中,我们仔细排查并做了一份总结,供大家参考。

在超高突发流量下,部署在容器服务 TKE 上面的服务会出现级联故障,所谓的级联故障有三个特征:

  1. 会短时间内让服务宕机。
  2. 受影响的服务会逐渐恶化最后需要依赖人为干预。
  3. 最坏情况下,发生速度快,可能未来得及产生告警,因为负载和故障是迅速发生的。

对于比较严重的情况,是当流量突然上涨,原本的 pod 瞬间减少甚至减半,随后开始逐渐变得更少,最后整个服务集群“挂掉”的情况。分析下来发现原因是,由于某些 pod 挂掉重启,剩余的 pod 承受上涨的超高流量,然后扩容不及时加上由于 pod 大量宕掉导致 HPA 计算并不准确,触发了集群内 pod 逐渐减少的情况。

按照上述情况,我们思考是不是做好冗余就可以了呢?经过实战,发现做好冗余,效果并不是很理想。因此开始深入地分析为什么会出现这样的情况?

首先在流量超高的情况下,pod 是不是真的“挂掉”了,从而影响了 HPA 的计算无法扩容?

而 pod 是否挂掉是通过健康检查进行判断的,TKE 服务都是使用 TCP 端口检查进行容器的健康检查的,难道问题出现在这里?

偶然间翻阅资料,发现大型企业对于 Readiness 和 Liveness 的设置相当慎重,原因是该处设置不当,会导致服务出现级联故障,后续还可能会导致故障恶化。

为什么健康检查会导致服务级联故障?

LivenessProbe 的存在的意义是:如果容器中的服务出现了死锁等卡住的情况,重启恢复原有的良好状态,而不是使用 LivenessProbe 用于进程守护类似的工作。

项目上的配置是使用 TCP 探针方式并且设置不健康阈值为 1,其实这是一个非常危险的操作

因为在高流量的情况下,随着并发数越大,其实响应时间也会越来越大,但对于健康检查的配置来说:

1. 健康检查的间隔配置与超时配置是一个固定的值,而接口的响应时间是一个动态的值,在并发高的情况下,响应时间随之增大但是服务是可以正常返回的,此时健康检查的返回可能超过设定的门槛,不健康阈值一旦设置得过低,那么就很有可能“误伤”,将一个响应较慢但正常的服务 kill 掉,这种误伤判断可能是雪崩的开始。因此,不健康阈值是建议按照默认的配置 3 次或以上,除非服务有特别的需求进行调低。

2. TCP 端口检查这种探针方式,并不适合去探测 HTTP 服务甚至是 RPC 服务,TCP 的探测并不能真实反馈服务的实时吞吐量,很容易让服务处于过载状态。那为什么 TCP 探针方式不能真实反馈服务真实的吞吐量,我们一起来探索 kubelet 探针实际的原理是怎么样的。

PS:

想了解更多的 Liveness 和 Readiness 相关信息,可查看官方推荐的文档:https://srcco.de/posts/kubernetes-liveness-probes-are-dangerous.html。

Kubelet 的探针原理

一、探讨探针源码

对于 TCP 的探针,我们可以看下具体 Kubelet 的探针源码:

代码语言:javascript
复制
.....
if p.TCPSocket != nil {        port, err := probe.ResolveContainerPort(p.TCPSocket.Port, &container)        if err != nil {            return probe.Unknown, "", err        }        host := p.TCPSocket.Host        if host == "" {            host = status.PodIP        }        klog.V(4).InfoS("TCP-Probe", "host", host, "port", port, "timeout", timeout)        return pb.tcp.Probe(host, port, timeout)}
.....
// Probe checks that a TCP connection to the address can be opened.func (pr tcpProber) Probe(host string, port int, timeout time.Duration) (probe.Result, string, error) {    return DoTCPProbe(net.JoinHostPort(host, strconv.Itoa(port)), timeout)}
// DoTCPProbe checks that a TCP socket to the address can be opened.// If the socket can be opened, it returns Success// If the socket fails to open, it returns Failure.// This is exported because some other packages may want to do direct TCP probes.func DoTCPProbe(addr string, timeout time.Duration) (probe.Result, string, error) {    d := probe.ProbeDialer()    d.Timeout = timeout    conn, err := d.Dial("tcp", addr)    if err != nil {        // Convert errors to failures to handle timeouts.        return probe.Failure, err.Error(), nil    }    err = conn.Close()    if err != nil {        klog.Errorf("Unexpected error closing TCP probe socket: %v (%#v)", err, err)    }    return probe.Success, "", nil

上述为 Kubelet 实现 TCP 探针的源码,实际上就是通过实现一个 TCP 客户端向目标端发起 TCP 连接,如果在超时时间内连接成功的话,就视为健康的。那问题来了,连接成功是依据什么判断的?是需要完全完成 TCP 的三次握手和四次挥手才算吗?我们看下 ProbeDialer 的具体实现代码:

代码语言:javascript
复制
func ProbeDialer() *net.Dialer {    dialer := &net.Dialer{        Control: func(network, address string, c syscall.RawConn) error {            return c.Control(func(fd uintptr) {                // fd 这里是指句柄                syscall.SetsockoptLinger(int(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 1, Linger: 1})            })        },    }    return dialer}

结合上面 DoTCPProbe 的实现代码来看,可以发现探测端是主动断开连接的一方,而在 TCP 四次挥手的过程中,如果是主动断开的一方,会保留 socket 一段时间(2 个最长报文寿命)。

但是探测发起端也就是 Kubelet 为了减少这种 time wait 状态的 socket,使用了 socket lingering,也就是使用一个阻塞型的 socket(即便使用 non-blocking socket 也会导致应用程序阻塞),周期性地将发送缓冲的数据发出并等待服务端 ACK,如果服务端 ACK 后正常四次挥手关闭 socket。

可以看到,探针实现里,通过获取 TCP 连接的句柄,调用系统原语,修改 TCP 的 linger 配置,改变默认等待时间 60s 为 1s。

二、TCP 的半连接与全连接队列

从上面的分析可以知道,其实 Kubelet 的探测是一次完整的 TCP 握手挥手过程。但是 TCP 握手是在 Linux 内核层面就能完成的操作,并不需要到上层业务逻辑层,换言之,TCP 探测反应不出业务真实吞吐量,为什么?

因为 TCP 三次握手在 Linux 内核中,会维护两个队列,分别是半连接队列(SYN 队列),全连接队列(Accept 队列)

服务端在收到客户端发起的 SYN 请求,内核就会将该连接存到半连接队列里面,然后向客户端回复 SYN + ACK,在客户端回复 ACK 之后,即服务端收到第三次握手的 ACK 后,内核会将连接从半连接队列移除,创建新的连接添加到全连接队列里面,然后用户态的进程调用 Accept 将连接取出使用。

结合上面的探针实现可以知道,Socket 连接在达到 Established 状态之后就会调用 close 方法进入四次挥手流程,也就是说TCP 探针的探测完全是在内核中就能完成,反馈不出上层业务服务是否拥挤。

另外半连接队列与全连接队列都有最大的队列长度限制,只有当达到最大长度限制之后,TCP 的连接才会被丢弃,也就是 TCP 的探测是在全连接队列满队时才会出现超时或者连接失败的情况。

为了验证这种情况,我们写了代码例子下列代码,该例子可真实反应 TCP 探测时全连接队列的情况,可供大家参考:

代码语言:javascript
复制
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        // 为模拟服务的拥挤,10 秒后再返回        time.Sleep(time.Second * 10)        w.WriteHeader(http.StatusOK)    }))defer server.Close()
.....// 这里的 dialer 就是 kubelet 的探针真实实现dialer := net.Dialer{    Control: func(network, address string, c syscall.RawConn) error {        return c.Control(func(fd uintptr) {            syscall.SetsockoptLinger(int(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{                Onoff:  1,                Linger: 1,            })        })    },}dialStartTime := time.Now()conn, err := dialer.Dial("tcp", net.JoinHostPort(tHost, strconv.Itoa(tPort)))if err != nil {    log.Printf("connect tcp err: %v\n", err)    return}err = conn.Close()if err != nil {    log.Printf("close connect tcp err: %v", err)    return}log.Printf("check success, cost:%v\n", time.Since(dialStartTime))

执行效果如下:执行前服务端口的全连接队列长度为 128,队列中暂无等待的 Socket。

然后执行 client 逻辑,瞬间建立 1000 个连接:

可以看到连接建立的时候是比较快的,即便服务端是每 10 秒才获取一次 Socket,而当全连接队列满队之后,探测才慢下来:

所以,TCP 探针是不太能反馈真实服务的吞吐量和处理能力的。

对于LivenessProbe 来说,如果此时服务只是响应慢 (tcp 握手慢),就很容易误伤一个存活的服务。

对于 Readiness probe 来说,如果无法判断服务是否拥挤,ReadinessProbe 就会一直把流量分发到服务,直到完全崩溃。

很多时候,服务只是反应慢,暂时不要将流量分发到负载均衡,先让服务处理内存中的任务,相当于一个冷却期,处理完再回到负载均衡队列里,如果服务本身不是有问题,那这个时间一般都会比重新起一个 pod 要快。

实际上,像 HTTP 服务和 RPC 服务(基于 HTTP/2)都不是特别推荐 TCP 方式的探测,像 gRPC 在 K8s 的云原生中有自己独特的探针实现,而不是直接使用 TCP 探针。

那 TCP 探针是什么时候用会比较合适???

一般是使用 TCP 协议直接作为服务传输协议会比较适合,例如 FTP 协议服务。

不使用 TCP 探针,应该使用什么呢?

HTTP 探针进行服务的 

结合之前提到的文章还有上面的验证,RUM 接入层已经切换使用 HTTP 探针进行服务的 Readiness 和 Liveness probe 的探测方式,也就是实现一个 health_check 接口,并且这个接口是与真实服务同端口但不同路径,这样才能真实反应到服务的真实情况。

此外,RUM 在后续会引入守护进程与提高服务单测覆盖率来保证服务的可靠性,避免单一依赖 Liveness。

为什么不要把 ReadinessProbe 与 LivenessProbe 探针设置同一频率?

除了不使用 TCP 探测,ReadinessProbe 和 LivenessProbe 的探测频率最好不要相同,因为两个探针是并行的,如果同频容易出现上一秒 ReadinessProbe 为健康,下一秒 LivenessProbe 为不健康导致接口服务出现问题,LivenessProbe 的不健康次数应该设置得更宽容一些,例如上述文章中说的 10 次,RUM 服务是设置的 5 次。

总结

优化上述服务探针设置问题后,服务运行确实更稳定了。但是级联错误是一个系统问题,需要优化服务多个方面,例如镜像大小,启动速度,sidecar 镜像设置等等。上述仅仅是 RUM 接入层优化的一小部分经验,希望能给大家带来一些服务优化上的参考,RUM 后续优化也将会给大家总结分享一些经验。感谢大家对 RUM 的支持~

联系我们

如有任何疑问,欢迎扫码进入官方交流群~


欢迎关注腾讯云可观测,了解最新动态

👇点击阅读原文立了解前端性能监控(RUM)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云可观测 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二、TCP 的半连接与全连接队列
  • 不使用 TCP 探针,应该使用什么呢?
  • 为什么不要把 ReadinessProbe 与 LivenessProbe 探针设置同一频率?
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档