华兴证券是 CloudWeGo 企业用户,应用 Kitex 框架实现混合云部署下的跨机房调用。
企业用户如何搭建针对 kitex 的可观测性零碎?如何在 K8s 集群下应用 Kitex ?
华兴证券后端研发工程师,DevOps 负责人张天将从以下 4 个方面介绍 Kitex 在多机房 K8s 集群下的实践经验,包含:
- 针对 Kitex 的可观测性零碎搭建教训;
- 服务压力测试中遇到的问题以及解决方案;
- Kitex 的不同连贯类型在 K8s 同集群 / 跨集群调用下的一些问题和解决方案;
- 实际中遇到的其余问题以及解决方案。
以下内容来自 张天 老师的分享。
Kitex 的可观性零碎搭建
华兴证券 CloudWeGo-Kitex 应用状况
首先介绍下咱们团队的 Kitex 应用状况。去年 6 月 1 日。咱们团队成立。Kitex 在 7 月 12 日公布了首个版本,10 天后咱们就引入了 Kitex。抉择 Kitex 的起因是:咱们团队晚期成员比拟理解 Kitex,为了疾速撑持业务迭代和验证,抉择最相熟的框架,岂但应用上比拟习惯,对性能和性能方面也比拟有把握。起初也撑持了咱们 APP 的疾速上线。大概 4 个月之后就上线了 APP 的第一个版本。
下图是咱们的微服务调用关系图,一共有三十多个微服务,调用链路数超过 70。咱们的服务别离部署在两个机房。外围业务比方交易、行情等部署在公有机房。非核心的业务,比方资讯、股票信息等部署在阿里的金融云,这样可能更好地利用金融云已有的基础设施比方 MySQL、Kafka 等,作为初创团队,可能升高整体的运维压力。思考到性能以及平安方面的因素,两个机房之间专门拉了专线。服务之间存在一些跨机房的依赖。跨机房调用会产生很多问题,后文会具体阐明。
Tracing 选型
服务数多了之后,咱们须要一套链路追踪零碎来描述调用链路每个环节的耗时状况。思考到 Kitex 原生反对 Opentracing,为缩小集成老本,咱们调研了合乎 Opentracing 标准的产品。
排除掉免费的、客户端不反对 Go 之后,就剩阿里云的链路追踪产品和 Uber 公司出品的 Jaeger,思考到公有机房也要部署,最终抉择了 Jaeger。
Kitex 接入 Tracing
选定计划之后,开始对 Kitex 的这个性能进行测试,后果发现过后去年 9 月初的 Kitex 版本并不反对跨服务的 Tracing,起因是调用的时候,没有把 Trace 信息发送给上游,如图所示,这样上下游是两个孤立的 Trace(OpenTracing 标准里称为 Span),于是就无奈通过一个 TraceID 去串起整条链路。过后工作比拟急,于是咱们没有等 Kitex 官网的实现,决定自研。
为了自研,咱们联合 Kitex 的源码,梳理出客户端和服务端的流程。能够看出 Kitex 的上下游都内置了 Tracer 的 Hook。这里咱们要解决的问题是,如何把 Span 信息进行跨服务传输?
经调研,实现透传有三种计划。
第一种是在音讯层搞一个 Thrift 协定的拓展,把 Trace 信息塞进去。起因是 Thrift 自身没有 Header 构造,只能进行协定的拓展。好在 Kitex 反对自定义的协定拓展,因而具备可行性,然而开发成本较高,所以没抉择这种计划。
第二种是在 IDL 里减少通用参数,在字段里存 Trace 信息。毛病是业务无关的字段要在 IDL 里,对性能有肯定的影响。毕竟须要通过 Kitex 的中间件,通过反射来提取。
第三种是利用了 Kitex 提供的传输层透传能力,对业务没有侵入性。最初抉择了这一种计划。
透传计划定了之后,整体的流程就清晰了。首先客户端会在 metaHandler.write
里通过 CTX 获取以后 Span,提取并写入 spanContext
到 TransInfo 中。
而后服务端,在 metaHandler.Read
里读取 spanContext
并创立 ChildOf 关系的 Span,中间件完结时 span.finish()
,最初为了避免产生孤立 Trace,New 服务端时不应用 Kitex 提供的 Tracing 的 Option。
这里是因为同一个服务可能别离作为 Kitex 上下游,Tracer 如果共用,须要别离加非凡逻辑,实现上有点简单。
Tracing 根底库
为了充分利用 Tracing 的能力,除了 Kitex,咱们在根底库中也减少了 Gin、Gorm、Redis、Kafka 等组件的 Tracing。
上面展现理论的一条链路。性能是通过短信验证码进行登录。先是作为 HTTP 服务的 API 入口,而后调用了一个短信的 RPC 服务,RPC 服务外面通过 Redis 来查看验证码。通过之后调用用户服务,外面可能进行一些减少用户的 MySQL 操作。最初把用户登录事件发给 Kafka,而后经营平台进行生产,驱动一些营销流动。能够看出最耗时的局部是对于新增用户的一堆 MySQL 操作。
对谬误的监控
Tracing 个别只关注调用耗时,然而一条链路中可能呈现各种谬误:
1. Kitex
- Kitex RPC 返回的 err(Conn Timeout、Read Timeout 等);
- IDL 里自定义的业务 Code(111: 用户不存在)。
2. HTTP
- 返回的 HTTP 状态码(404、503);
- JSON 里的业务 Code(-1: 外部谬误)。
如何对这类谬误进行监控?次要有以下三种计划:
- 打日志 + 日志监控,而后通过监控组件,这种计划须要解析日志,所以不不便;
- 写个中间件上报到自定义指标收集服务,这种计划长处是足够通用,然而须要新增中间件。同时自定义指标更关注具体的业务指标;
- 利用 Tracing 的 Tag,这种计划通用且集成成本低。
具体实现如下:
- Kitex 的 err、以及 HTTP 的状态码,定义为零碎码;
- IDL 里的 Code 以及 HTTP 返回的 JSON 里的 Code,定义成业务码;
- Tracing 根底库里提取相应的值,设置到
span.tag
里; - Jaeger 的
tag-as-field
配置里加上相应的字段(原始的 Tags,为 es 里的 Nested 对象,无奈在 Grafana 里应用 Group By)。
监控告警
在减少谬误监控的根底上,咱们构建了一套监控告警零碎体系。
这里重点看一下方才的链路追踪相干的内容。首先每个业务容器会把指标发送到 Jaeger 服务里。Jaeger 最终把数据落盘到 es 中。而后咱们在 Grafana 上配置了一堆看板以及对应的告警规定。
触发报警时,最终会发送到咱们自研的 alert-webhook 里。
自研的局部首先进行告警内容的解析,提取服务名等信息,而后依据服务的业务分类,散发到不同的飞书群里,级别高的报警会打加急电话。这里也是用到了飞书的性能。
Grafana 里咱们配置了各类型服务调用耗时、错误码一体化看板,形容了一个服务的方方面面的指标。包含日志监控、错误码监控、QPS 和调用耗时、容器事件监控、容器资源监控等。
下图展现了飞书告警卡片。包含 RPC 调用超时、零碎码谬误、业务码谬误。
这里咱们做了两个简略的工作,一个是带上了 TraceID,不便查问链路状况。另一个是把业务码对应的含意也展现进去,研发收到报警之后就不必再去查表了。
本章小结
- 实现了 Tracing 接入 Kitex,实现跨服务传递;
- 对 Tracing 根底库扩大了其余类型中间件(Gin、Gorm、Redis、Kafka)的反对;
- 对 Tracing 根底库减少了零碎码、错误码实现对谬误的监控;
- 配置了全方位的服务指标看板;
- 联合 es、Grafana、飞书以及自研告警服务,搭建了针对微服务的监控告警零碎。
这样咱们就实现了可观测性体系的搭建。
服务压力测试中遇到的问题以及解决方案
实现了监控告警体系之后,咱们心愿对服务进行压测,来找出性能瓶颈。第二局部介绍一下服务压测中遇到的问题和解决方案。
Kitex v0.0.8:连贯超时问题
首先咱们发现,QPS=150 左右,Kitex 呈现连贯建设超时的谬误。过后咱们查看了下 CPU、网络、内存等均没有达到限度。先是狐疑连接池大小不太够,于是测了下 10 和 1000,如上图所示,后果在报错数目上没有区别。另外察看到的一个景象是,压测期间呈现靠近 5000 的 Time Wait 状态。
5000 的限度,是因为达到了 tcp_max_tw_buckets
的设置的值。超过这个值之后,新的处于 Time Wait 状态的连贯会被销毁,这样最大值就放弃在 5000 了。于是咱们尝试进行排查,但没有思路,于是去翻看 Kitex 的 Issue,发现有人遇到雷同的问题。
原来,v0.0.8 版本的 Kitex,在应用域名的形式来新建 Client 的时候,会导致连接池生效。因为把连贯放回连接池时,用的 Key 是解析之后的 IP,而 GET 的时候,用的是解析前的域名,这样基本 Get 不到连贯,于是不停创立短连贯。这样的两个结果是:一方面建设连贯比拟耗时,另一方面申请执行结束之后都会敞开掉连贯,于是导致了大量的 Time Wait。
为了进行验证,我把测试服务改成了 IP 拜访,而后比拟了 IP 拜访和域名拜访以及不同连接池大小的状况。能够看出 IP 拜访(连接池无效),然而连接池比拟小的状况,呈现缩小的 Timeout。连接池 100,Timeout 隐没。而两头的域名拜访的状况下,呈现大量 Timeout。
Kitex v0.1.3:连接池问题修复
看代码得悉在 Kitex v0.1.3 修复了这个问题。
于是咱们打算降级 Kitex 的版本,因为过后曾经上了生产环境,在降级根底组件之前,须要进行验证,看一下不同连接池大小状态下的体现。还是域名模式,QPS 为 150 的状况下,随着连接池大小的减少,Timeout 的状况逐步变少到隐没。
持续进行压测,咱们发现 QPS=2000 的时候又呈现了报错。联合监控,发现起因是连贯建设的时候超过了默认的 50ms。
咱们探讨了几种解决方案:
1. 批改超时配置。然而,交易日的 9:30-9:35 有⼀堆集中交易申请,突发的流量,耗时长了体验不好,可能会影响 APP 支出,咱们心愿零碎性能保持稳定。
2. 进行连贯耗时的优化。然而 Kitex 曾经应用了 Epoll 来解决创立连贯的事件,作为应用方,进一步优化的难度和老本都太大。
3. MaxidleTimeout 参数改成无限大?比方先创立一个足够大的池,而后随着用户申请,池变得越来越大,最终稳定下来。然而每次服务降级之后,这个池就空了,须要缓缓复原。
4. 进行连贯预热。
其实连贯预热就相当于压测完结之后立马趁热再压一次,如图,能够发现 QPS=2000 的状况下,简直都走了连接池,没有报错。因而,如果服务启动时可能进行连贯预热,就能够省下建设连贯的工夫,使服务的性能保持稳定。
过后 CloudWeGo 团队针对咱们公司建了企业用户交换群,于是咱们就向群里的 Kitex 研发提了连贯预热的需要。其开发之后提供了连贯预热个数的选项。咱们也进行了测试。依照 QPS=2000 进行测试,
- WARM_UP_CONN_NUM=0:大概 1s 报错;
- WARM_UP_CONN_NUM=100:大概 4s 报错;
- WARM_UP_CONN_NUM=1000:大概 4s 报错,但能够看出一开始都无需新建连贯;
- WARM_UP_CONN_NUM=2000:无报错。
本章小结
- Kitex v0.0.8:域名模式下存在连接池生效问题,v0.1.3 中修复;
- Kitex v0.1.3:可进一步通过连贯预热性能进步零碎性能。
Kitex 的不同连贯类型在 K8s 同集群 / 跨集群调用下的一些问题和解决方案
第三局部咱们讨论一下 Kitex 的不同连贯类型在 K8s 同集群 / 跨集群调用下的一些问题和解决方案。
长连贯的问题:跨集群调用
首先是长连贯跨集群调用下的问题。服务在跨集群调用时,其源 IP: 端口为宿主机的,数量无限,而目标 IP: 端口为上游集群的 LB,个别是固定的。
那么,当长连接池数目比拟大(比方数千),且上游较多(各种服务、每个都多正本,加起来可能数十个)的状况下,申请顶峰时段可能导致上游宿主机的源端口不够用。同集群内跨机器调用走了 vxlan,因而没有这个问题。
解决方案有两类:
- 硬件计划:机器;
- 软件计划:对于上游为 Kitex 服务,改用 Mux 模式(这样大量连贯就能够解决大量并发的申请)。上游不是 Kitex 框架,因为 Mux 是公有协定,不反对非 Kitex。此时可思考减少上游服务的 LB 数量,比方每个 LB 上调配多个端口。
比拟起来,革新成 Mux 模式老本最低。
连贯多路复用的问题:滚动降级
然而多路复用模式,在 K8s 场景下,存在一个滚动降级相干的问题。咱们先介绍下 Service 模式,
K8s 的 Service 模式采纳了 IPVS 的 Nat 模式(DR 和隧道模式不反对端口映射),链路为:上游容器←→ClusterIP(服务的虚构 IP)←→上游容器。
而后咱们看看滚动降级流程:
- 新容器启动。
- 新容器 Readiness Check 通过,之后做两件事件:
- 更新 Endpoints 列表:新增新容器,删除旧容器;
- 发送 sigTerm 到旧容器的 1 号过程。
- 因为更新了 Endpoints 列表,Endpoints 列表产生更新事件,立刻回调触发规定更新逻辑(syncProxyRules):
- 增加新容器到 IPVS 的 rs,权重为 1;
- 如果此时 IPVS 的旧容器的中 ActiveConn + InactiveConn > 0(即已有连贯还在),旧容器的权重会改成 0,但不会删除 rs。
通过步骤 3 之后,已有的连贯依然可能失常工作(因为旧容器 rs 未删),但新建的连贯会走到新的容器上(因为旧容器权重 =0)。
在 Service 模式下,上游通过一个固定的 IP: 端口来拜访上游,当上游滚动降级的时候,上游看到的地址并未变动,即无奈感知到滚动降级。于是,上游即便有优雅退出,但上游并不知道上游开始优雅退出了。之后可能的状况是:
- 上游发现连贯忙碌,始终没有被动敞开,导致 K8s 配置的优雅降级工夫超时,强制 Kill 过程,连贯敞开,上游报错;
- 上游发现连贯闲暇,被动敞开,然而客户端在敞开之前恰好拿到了连贯(且认为可用),而后发动申请,实际上因为连贯敞开,发动申请失败报错。
针对此问题,解决方案如下:
- 同集群调用:改用 Headless Service 模式(联合 DNSResolver),通过 DNS 列表的增删来感知上游变动;
- 跨集群调用:借鉴 HTTP2 的 GOAWAY 机制。
具体可采纳如下形式:
- 收到 sigTerm 的上游间接通知上游(通过之前建设的 Conn1),同时上游持续解决发来的申请。
- 上游收到敞开信息之后:
- 新申请通过新建 Conn2 来发;
- 已有的申请依然通过 Conn1,且解决完了之后,等上游优雅敞开 Conn1。
这种形式的长处是同集群跨集群均可应用,毛病是须要 Kitex 框架反对。在咱们找 Kitex 团队探讨之后,他们也提供了排期反对本需要。
连贯多路复用的滚动降级测试:Headless Service 模式
在 Kitex 团队开发期间,咱们测下 Kitex 已有版本对 Headless Service 模式下的滚动降级性能。测试计划如下:
- Kitex 版本 v0.1.3;
- 上下游均为 Mux 模式;
- 上游的加了个自定义 DNSResolver,刷新工夫为 1s,加日志打印解析后果;
- 上游的退出信号处理,收到 sigTerm 之后特意 Sleep 10s(用来排除这个 Case:服务端发现连贯闲暇敞开了,但客户端在敞开之前恰好拿到连贯,接着认为未敞开,实际上曾经敞开,而客户端发动了申请,于是导致报错);
- QPS=100 恒定压上游,而后触发上游滚动降级。
实测报错如下图:
时序剖析如下:
- 旧上游收到 sigTerm,开始 Sleep 10s;
- 上游解析到旧上游的 IP,向旧上游发动申请;
- DNS 规定更新:旧上游 IP 解析项隐没,新上游解析项呈现;
- 上游申请报错;
- 旧上游 sleep 实现,开始退出逻辑。
可见报错时旧上游还未执行退出逻辑,排除旧上游被动敞开连贯。申请旧上游期间,且此时解析到新容器 IP(移除了旧容器 IP),报错是因为还没到退出逻辑的时候。因而揣测,解析条目变动导致了报错。
依据揣测,联合代码(Kitex 客户端局部)剖析,可能呈现以下并发问题:
【协程 1】客户端从 Mux 池里取出 Conn1,行将发动申请(所以没有机会再查看 Conn1 状态了);
【协程 2】DNS 更新,移除了 IP,于是 Clean 办法中敞开了 Conn1;
【协程 1】客户端用 Conn1 发动申请,导致报错 Conn Closed。
于是咱们向 CloudWeGo 提了 Issue,他们很快修复了这个问题。
连贯多路复用的滚动降级测试:Service 模式
同样地,在 Service 模式中,测试计划如下:
- Kitex 版本应用 Feature 分支:mux-graceful-shutdown;
- 上下游均为 Mux 模式、服务发现应用 Service 模式;
- 恒定 QPS=200 压上游,20s 触发上游滚动降级;
- 另外写个服务打印期间的 IPVS 的日志;
- 上游的退出信号处理,收到 SigTerm 之后特意 sleep 10s(保障 IPVS 规定已更新)。
测试后果如下:
报错:INFO[0050] “{\”code\”:-1,\”message\”:\”remote or network error: connclosed\”}”。
时序剖析为:
- 旧上游收到 sigTerm,开始 sleep 10s。
- IPVS 规定变动:
- 新上游 weight=1,ac=0,inac=0;
- 旧上游 weight=0,ac=2,inac=0;
- 旧上游 sleep 实现,进入最长为 15s(WithExitWaitTime)的优雅退出。
- 上游申请报错。
- 旧上游打印了最初一条日志。
- IPVS 规定变动:
- 新上游 weight=1,ac=2,inac=0 => ac=2 阐明上游新建连贯到新容器;
- 旧上游 weight=0,ac=0,inac=2 => inac=2 示意连贯敞开。
- IPVS 规定变动:旧上游的规定被移除。
因而咱们得出结论,报错产生在优雅退出期间。最初一条日志时刻大于报错时刻,因而,排除 K8s 的问题,确认 Conn Closed 是由 Kitex 导致的。之后咱们和 Kitex 研发团队沟通了剖析后果,找到了 Root Cause,是因为假如了新的上游会有一个新的地址(但理论中 Service 模式都是一个地址),导致新申请取到了老申请的连贯并进行敞开。对此进行了修复:
连贯多路复用的问题:上游扩容
如果用 Service 模式(上游看到的上游就是体现为⼀个 IP),创立的 TCP 连贯会在最开始固定的几个上游 POD 上,之后如果扩容减少 POD,新创建的 POD 就不会路由到了,导致扩容实际上有效。
解决方案如下:
1. 同集群调用:可用 Headless Service 模式,因为 DNS 解析可能失去所有 POD,路由没问题。
2. 跨集群调用:不在同集群内,Headless Service 模式有效,思考如下计划:
- 计划 1:批改服务发现机制。
长处:Kitex 无需改变。
毛病:减少依赖项(服务发现组件)。 - 计划 2:上游先降级,之后上游 Redeploy 一下,让连贯散布到上游的各种实例上。
长处:Kitex 无需改变。
毛病:上游可能很多,一一 Redeploy 十分不优雅。 - 计划 3:上游定期把 Mux 给过期掉,而后新建连贯。
长处:彻底解决。
毛病:须要 Kitex 反对。
本章小结
- 首先,针对长连贯模式分析了跨集群时上游源端口数问题,心愿通过多路复用模式解决;
- 其次,针对多路复用模式 + K8s Headless Service 模式的优雅降级,实测报错,剖析定位了起因,Kitex 研发团队及时解决了相应问题;
- 再次,针对多路复用模式 + K8s Service 模式下的优雅降级提出了计划,Kitex 团队实现了实现,迭代了一轮,测试通过;
- 最初,针对多路复用模式 + K8s Service 模式下的上游正本扩容时路由不到的问题剖析了起因,提出了计划,目前计划待实现。
实际中遇到的其余问题以及解决方案
第四局部咱们剖析下实际中遇到的其余问题以及解决方案。
RPC Timeout Context Canceled 谬误
研发同学发现日志呈现 Contexe Canceled 的谬误,剖析日志发现呈现频率低,一天只有几十条,属于偶发报错。
咱们揣测是用户手机因为某种原因敞开了进行中的连贯所导致,对此进行本地验证。三个局部:首先 Gin 客户端设置了 500ms 超时限度,去申请 Gin 服务端接口;其次,Gin 服务端收到申请之后,转而去调用 Kitex 服务;最初,Kitex 服务端 Sleep 1s 模仿耗时超时,保障 Gin 客户端在申请过程中敞开连贯。
实测可能稳固地复现。
咱们梳理了源码逻辑,客户端敞开连贯之后,Gin 读取到 EOF,调用 cancelCtx,被 Kitex 客户端的 rpcTimeoutMW 捕捉到,于是返回了 err。
那么问题就变成,申请未实现时,连贯为何会被敞开?咱们依照设施的 ID 去剖析日志,发现两类状况:一类是报错对应的申请是该设施短期内的最初一条,于是思考 APP 被手动敞开;二是报错对应的申请非短期内的最初一条,客户端研发反馈,有些接口例如搜寻,上一条申请执行中(未返回),且新的申请来时,会 Close 掉上一次申请的连贯。第二种状况比拟确定,对于第一种状况,APP 被敞开时,IOS 和 Android 是否会敞开连贯?客户端同学没有给出必定的回答。
于是咱们思考理论测试一下,两端别离写一个测试的利用,继续发动申请,然而不开释连贯,此时敞开 APP,剖析 TCP 包。实测咱们在两端上均看到了 4 次挥手的 Fin 包。所以这个问题失去了确认。
那么如何进行修复呢?咱们采取在 GIN 的中间件上拦挡掉 Done 办法的形式。
上线之后,再没有呈现这种状况。
还有一个问题,咱们在测试环境发现,跨集群调用的时候,经常出现连贯被重置的问题。生产环境搜日志,无此景象。
咱们剖析了 环境差别:
- 生产环境是专线直连;
- 测试环境,因为专线比拟低廉,机房之前通过公网拜访,两头有个 NAT 设施。
咱们找网络共事征询,得悉 NAT 表项的过期工夫是 60s。连贯过期时,NAT 设施并不会告诉上下游。因而,上游调用的时候,如果 NAT 设施发现表项不存在,会认为是一个生效的连贯,就返回了 rst。于是咱们的解决方案是 Kitex 上游的 MaxIdleTimeout 改成 30s。实测再未呈现报错。
本章小结
- Rpc Timeout:Context Canceled 问题剖析和解决;
- Rpc Error:Connection Reset 问题剖析和解决。
瞻望
将来咱们打算把 Gin 更换为更高性能(QPS/ 时延)的 CloudWeGo-Hertz。因为咱们 K 线服务的 Response Size 比拟大(~202KiB),更换后 QPS 预计可达原先的 5 倍。同时,为回馈开源社区,咱们打算奉献 Tracing 根底库的代码到 Kitex-contrib/Tracer-opentracing。欢送继续关注 CloudWeGo 我的项目,退出社区一起交换。
CloudWeGo 企业用户反对
欢送企业用户扫描二维码,填写企业反对问卷,获取 CloudWeGo 团队企业技术支持。
关上飞书,扫描左侧二维码填写 企业反对问卷 ,扫描右侧二维码可退出 飞书交换群。
我的项目地址
GitHub:https://github.com/cloudwego
官网:www.cloudwego.io