共计 5634 个字符,预计需要花费 15 分钟才能阅读完成。
我的项目背景
分享之前,先来简略介绍下该我的项目在流式日志解决链路中所处的地位。
流式日志网关的次要性能是提供 HTTP 接口,接管 CDN 边缘节点上报的各类日志(拜访日志 / 报错日志 / 计费日志等),将日志作预处理并分流到多个的 Kafka 集群和 Topic 中。
越来越多的客户要求提供实时日志反对,业务量的减少让机器资源的耗费也一劳永逸,最先暴露出了流式日志解决链路的一大瓶颈——带宽资源。
能够通过给集群裁减更多的机器来晋升集群总传输带宽,但基于老本考量,重中之重是先优化网关程序。
旧版网关我的项目
我的项目代号 Chopper,其基于另一个外部 OpenResty 我的项目框架来开发的。其亮点性能有:反对从 Consul、Redis 等其余内部零碎热加载配置及动静失效;可能加载 Lua 脚本实现灵便的日志预处理能力。
其 Kafka 生产者客户端基于 doujiang24/lua-resty-kafka 实现。通过实际考验,Chopper 的吞吐量是满足现阶段需要的。
存在的问题
- 要害依赖库的社区活跃度低
lua-resty-kafka 的社区活跃度较低,至今依然处在试验阶段;而且它用作 Kafka 生产者客户端目前没有反对消息压缩性能,而这在其余语言实现的 Kafka 客户端中都是规范的选项。
- 内存应用不节制
单实例部署配置 4 核 8 G,仅大量申请拜访后,内存占用就稳固在 2G 而没有开释。
- 配置文件可维护性差
理论线上用到 Consul 作为配置核心,采纳篇幅很长的 JSON 格局配置文件,不利于运维。另外在 Consul 批改配置没有回退性能,是一个高风险操作。
好在目前日志网关的性能并不简单,所以咱们决定重构它。
新我的项目启动
家喻户晓,Go 语言领有独特的高并发模型、较低的上手难度和丰盛的第三方生态。而且咱们小组成员都有 Go 我的项目的开发教训,所以咱们抉择应用基于 Go 语言的技术栈来从新构建 Chopper 我的项目,所以新我的项目命名为 chopper-go。
需要梳理及概要设计
从新构建一个线上我的项目的根本准则是,性能上要齐全兼容,最好可能实现线上服务的无缝降级替换。
原版外围模块的设计
Chopper 的外围性能是将接管到的 HTTP 申请分流到特定 Kafka 集群及其 Topic 中。
一、HTTP 接口局部
只凋谢了惟一一个对外的 API,性能很简略:
申请形式:POST 申请门路:/log/repo/{repo\_name} 申请体: 多行日志,满足 JSONL 格局(即每行一条 JSON,多行按换行符 \n 分隔)。相应状态码:- 200:投递胜利。- 5xx:投递失败须要重试。参数解释: – repo\_name: 对应 repo 配置名称。
二、业务配置局部
每一类业务形象为一个 repo 配置。Repo 配置由三局部形成:constraint、processor、kafka。constraint 是一个对象,能够配置对日志字段的一些约束条件,不满足条件的日志会被抛弃。processor 是一个列表,能够组合多个解决模块,程序将按程序顺次对申请中的每条日志进行解决。实现了如下几种 processor 类型:
- decoder , 配置原始数据按哪种格局反序列化到 Lua table,但只实现了 JSON decoder。
- splitter,配置分隔日志字段的字符。
- assigner,配置一组字段名映射关系,须要与 spliter 配合。
- executer,配置额定的 lua 脚本名称,通过动静加载其余 lua 脚本实现更灵便的解决逻辑。
kafka 是一个对象,能够配置以后业务相关联的 Kafka 集群名,默认投递的 Topic,以及生产者客户端的工作模式(同步或者异步)。
新版本的改变 HTTP
接口沿用原先的设计,在业务配置局部做了一些改变:
- processor 改名为 executers,实现几个通用性能的日志解决模块,不便组合应用。
- kafka 配置中关联的不再是集群名,而是 Kafka 生产者客户端的配置标签。
- 原先保留 kafka 集群连贯配置信息的配置块,改为保留 kafka 生产者客户端的配置块,对立在一个配置块区域初始化所有用到的 kafka 生产者客户端。
一点斗争(做减法)
为了缩短新我的项目的开发周期,对原始我的项目的一些不太重要的个性咱们做了一些取舍。
勾销动静脚本性能
Go 是动态语言没有 Lua 动静语言那么灵便,要加载执行动静脚本有肯定的实现难度,且日志解决性能没有保障。线上只有极少数业务在 processor 中配置了 executor,且这些 executor 的 Lua 脚本实现相近,齐全能够抽取出通用的代码。
不反对内部配置核心
为了让公布和回退有记录可回溯,从 Consul 等配置核心热加载服务配置的性能咱们也去掉了。利用好容器平台的金丝雀公布性能,就能将服务更新的影响降到最低。
不反对简单的路由重写
OpenResty 我的项目内置 Nginx 能够利用 Nginx 弱小的配置实现丰盛的路由 rewrite 性能,就具体应用场景而言,咱们只须要简略的路由映射即可。况且更简单的需要也能够由上一级网关实现。
抉择适合的开源库
Web 框架的抉择
应用 Go 开发 Web 利用很快捷。咱们参考了如下文章:
- 《超全的 Go Http 路由框架性能比拟》(https://colobu.com/2016/03/23/Go-HTTP-request-router-and-web-…
- 《iris 真的是最快的路由框架吗?》(https://colobu.com/2016/04/01/Is-iris-the-fastest-golang-rout…
- https://github.com/kataras/server-benchmarks
下列几款 Star 较多的 Go Web 框架都能满足咱们需要:
- kataras/iris
- gin-gonic/gin
- go-chi/chi
- labstack/echo
他们性能都很好,最终咱们抉择了 Gin。起因是用得多比拟熟,而且文档看着难受。
Kafka 生产者客户端的抉择
社区中热度最高的几款 Go Kafka 客户端库:
- segmentio/kafka-go
- Shopify/sarama
- confluentinc/confluent-kafka-go
实际上三款客户端库咱们在历史我的项目中都应用过,其中 kafka-go 的 API 是三者中最简洁易用的,咱们的多个生产端程序都是基于它实现的。
然而在 chopper-go 中仅须要用到生产者客户端,咱们没有抉择 kafka-go。那是因为咱们做了一些基准测试(https://github.com/sko00o/benchmark-kafka-go-clients),发现 kafka-go 的生产者客户端存在性能危险:启用 async 模式时只管音讯发送特地快,然而内存占用也增长特地快。通过浏览源码我也找到了起因并向官网提了 issue(<https://github.com/segmentio/kafka-go/issues/819),然而作者感觉这设计没故障,所以就不了了之了。
最终咱们抉择 sarama,一方面是性能很稳固,另一方面是它凋谢的 > API 较多,然而用起来的确有点吃力。
测试框架的抉择
程序的可靠性,肯定须要测试来保障。除了编写小模块中编写单元从测试,咱们对整个日志网关服务还要做集成测试。集成测试波及到一些内部服务依赖,此我的项目中次要的内部依赖是 Kafka 和 Zookeeper。
利用 Docker 能够很不便的拉起测试环境,咱们留神到了两款能够用来在 Go test 中编写集成测试的库:
- ory/dockertest
- testcontainers/testcontainers-go
应用下来,咱们最终抉择了 testcontainers-go,简略介绍下起因:
在编写集成测试时,咱们须要有个期待机制来确保依赖服务的容器是否准备就绪,并以此管制测试流程,以及测试完结后须要把测试开启的长期容器都清理洁净。
testcontainers-go 的设计要优于 dockertest。testcontainers-go 提供一个 wait 子包,能够配置多种期待策略来确保依赖服务就绪,以及测试完结时它会调用一个非凡的名为 Ryuk 的容器来确保测试容器都被敞开。相对而言,dockertest 要简陋不少。
须要留神的是,在 CI 环境运行集成测试都须要确保 ci-runner 反对 DinD,否则运行 go test 会失败。
我的项目开发
我的项目开发过程中根本依照需要来实现没有太多难点。这里分享踩到的几个坑。
循环中变量的援用问题
在测试中发现,Kafka 生产者没有按冀望把音讯投递到指定的 Kafka 集群。
通过排查到如下代码:
func New(cfg Config) (*Manager, error) {var newProducers = make(NewProducerFuncs)
for name, kCfg := range cfg.Mapping {newProducers[name] = func() (kafka.Producer, error) {return kafka.New(kCfg) }
}
// 略
}
其作用是将配置每个 Kafka 生产者配置先保留为一个函数闭包,待后续初始化 repo 的时候再初始化生产者客户端。
经验丰富的同学能够发现,for 循环的 kCfg 变量其实是指向迭代对象的地址,整个循环下来所有的函数闭包中用到的 kCfg 都指向 cfg.Mapping 的最初一个迭代值。
解决办法很简略,先做一遍变量拷贝即可:
func New(cfg Config) (*Manager, error) {var newProducers = make(NewProducerFuncs)
for name, kCfg := range cfg.Mapping {newProducers[name] = func() (kafka.Producer, error) {return kafka.New(kCfg) }
}
// 略
}
这是个挺容易碰到的问题,参考 https://colobu.com/2022/10/04/redefining-for-loop-variable-se…
Go 也有可能在将来将循环变量的语义从 per-loop 改成 per-iteration。
Sarama 客户端的一点坑
对于重要的日志数据,咱们心愿在 HTTP 申请返回时明确反馈是否胜利写入 Kafka。那么最好将 Kafka 生产者客户端配置为同步模式。
而同步模式的生产者要进步吞吐量,批量发送是必不可少的。
批量发送的配置位于 sarama.Config.Producer.Flush
cfg := sarama.NewConfig()
// 单次申请中音讯数量的相对下限
cfg.Producer.Flush.MaxMessages = batchMaxMsgs
// 可能触发申请收回的音讯数量阈值
cfg.Producer.Flush.Messages = batchMsgs
// 可能触发申请收回的音讯字节大小阈值
cfg.Producer.Flush.Bytes = batchBytes
// 批量申请的触发间隔时间
cfg.Producer.Flush.Frequency = batchTimeout
实际中发现,如果配置了 Flush.Bytes 而没有配置 Flush.Frequency 就存在问题。如果音讯大小始终未达阈值就不会触发批量申请,故 HTTP 申请就会阻塞直到客户端申请超时。
所以在配置参数的读取上,咱们把这两个配置项做了关联,只有配置了 Flush.Frequency 能力让 Flush.Bytes 的配置失效。
我的项目上线
容器平台上的灰度技巧
本来图不便咱们的路由转发规定配置的是全副路由间接转给同一组 Chopper 实例。
后面介绍了,每一个业务对应一个 repo,也就对应一个独立的申请门路。如果要灰度新的服务,须要对不同业务独自灰度,所以咱们须要将不同业务的流量去离开。
好在容器平台的 k8s-ingress 应用的是 APISIX 作为接入网关,其路由匹配的优先级是:相对匹配 > 前缀匹配。
只须要针对特定业务减少一条相对匹配规定,就能够拆散出特定业务的流量。
举个例子:本来的转发规定是:/* -> workers-0
咱们新建一条转发规定:/log/repo/cdn-access -> workers-1
workers-0 和 workers-1 两组服务的配置完全相同。
而后咱们对 workers-1 这组服务灰度公布新版程序。
逐渐扩充
每灰度一条路由,咱们能够从监控 Dashboard 上察看 HTTP 申请是否有异样,察看 Kafka 对应的 topic 的写入速率是否有异样抖动。
一旦观测到异样,立刻进行灰度,而后检查程序运行日志,修改问题后从新开始灰度。
如果无异样,则逐渐扩充灰度比例,直到实现服务更新。
总结起来就是灰度、观测、回退、批改循环推动,确保降级对每个业务都无感知。
实现公布
比照服务端资源占用状况
旧版 chopper (4C8G x 20) 灰度比例
10% -> 50%
chopper-go (4C4G x 20)
10% -> 50%
50% -> 100%
论断:新版日志网关的内存和 CPU 的资源应用都有显著升高。
服务端程序的资源占用状况
旧版 chopper 的 Kafka 客户端不反对消息压缩,chopper-go 公布中就配置了 Kafka 生产者音讯的性能。压缩算法抉择 lz4,察看两组生产服务的资源实用率的变动:生产服务 0
- 内存使用率 27% -> 40%
- 网络流入 253Mbps -> 180Mbps
生产服务 1
- 内存使用率 28% -> 39%
- 网络流入 380Mbps -> 267Mbps
论断:开启消息压缩性能后,生产实例的内存使用率广泛有增长,但内网传输带宽占用升高约 30%
更新打算
重构后的流式日志网关,尚有许多可优化空间,例如:
- 采纳更节俭带宽的日志传输格局;
- 进一步细化 Kafka topic 的分流粒度;
- 日志音讯解决阶段多级解决执行器之间减少缓存进步字段访问速度等等。
在丰盛开源生态的加持下,该项目标优化迭代也将井井有条地进行。