apisix 遇到的一些问题

背景

大约十年前,部门内部有一套 ESB, 一套网关服务,当时网关服务用的 Python (Django 框架), 处理了一些基本的认证/流控逻辑,但是无法支持高并发并且经过网关的性能损耗很大

在 2019 年,我们使用 Golang 重写了网关服务,性能大约是原来的 34 倍

至于选择 Golang 的原因,主要是因为:

  • 团队内部有 Golang 的开发经验,当时基于 Golang 的开源网关比较多,例如 janus, KrakenD, tyk 等等
  • 相反,团队内部没有 OpenResty 的开发和运维经验 (当时 OpenResty 及 Kong 比较火,并且当时 Apisix 并不火)

(我们的网关上线一年多之后,Apisix 团队有到我们内部做过分享,当时切到其他项目救火了并没有去听)

当时技术选型没有使用 gin, 而是 chi (轻量,100% compatible with net/http ) + std reverse proxy + dbless 模型 (借鉴 Kong 的概念)

做了两个大版本:

  • 第一个版本,底层数据不变,完全实现了原来的功能,发布上线 (此时新老版本都可以运行,灰度过去)
  • 第二个版本,重新设计了底层数据结构,重做了管理端功能,数据迁移发布切换到新版本

(这个版本的一直非常稳定,并且性能比 apisix 版本还好。得益于 goroutines / channels 以及无处不在的缓存)

然后在 2022 年,我们又遇到下一个问题:扩展性,我们有越来越多的需求

但是

  • Golang 要实现插件的成本太高了,并且缺乏生态,很多很常见的特性都需要自己实现
  • 无法支持自定义插件的场景
  • 无法支持热更新 (在内存中全量重建 radixtree 之后替换掉旧的)

在 2019 我切换去做权限系统,搞完之后又去接用户系统,到了 2023 四月份,由于某些原因,切回来接手新版的基于 apisix 的网关

接近八个月时间,很大部分时间在做架构调整以及重构,并且花费了很长一段时间验证新版本的稳定性,遇到了蛮多问题

最近在整理工作文档,汇总一下

简单梳理总结一下

架构调整和代码重构

原先的项目拆分的非常细碎,通过 SDK 共享代码,通过 API 做数据交互

  • 合并管理端的项目到一个单体项目,去掉 SDK 及交互 API
  • 删除过度设计的模块,使用更直接/务实的方式实现
  • 去除 基于 apisix-go-plugin-runner 实现的权限数据同步/鉴权模块,使用 HTTP + lrucache 方式实现 (简单,不用考虑数据一致性)
  • 去除 基于 apisix-go-plugin-runner 实现的基于漏桶算法的频率控制,使用原生插件实现 (fixed-windows, 务实,虽然效果不如漏桶)
  • 去反向依赖
  • 对代码进行重构,模块及分层,提升可读性; 同时增加单元测试,引入 test-nginx
  • 集成测试左移
  • 对 dockerfile, helm-charts, 各种运维脚本进行优化,优化出包流水线
  • 增加多套环境用于不同场景的测试
  • ……

(这块花了很多时间,才做到 production ready, 主要是又得在开飞机的时候换引擎)

Apisix 相关的问题

升级:2.15 升级到 3.2.1

基本按照 Upgrade Guide 阅读,评估后直接升级到 3.2.1

主要的变更还是配置文件,顺带还把原先配置文件的生成方式重写,直接 helm 生成最终用的文件,抹掉了在容器启动时各种 sed/subst 之类的操作

其实后来还尝试升级到 3.2.2, 当时以为是一个小版本,结果合入了一个 pr #9456 feat(config_etcd): use a single long http connection to watch all resources, 导致了 bug: route 404 after upgrade to 3.2.2, 原因是etcd prefix中带了-, 在这个 fix: can’t sync etcd data if key has special character 中修复了

带了 9456 这个特性的版本都需要关注下这个 PR 后面带的一批 bugfix (应该有四五个了,并且都比较重要,所以建议用最新版)

prometheus 的问题

radixtree 最长路径匹配问题

发布后,有业务反馈其原先正常的路由目前被匹配到了另一个路由上

apisix:
  router:
    http: radixtree_uri_with_parameter

在这种类型的 radixtree 中,最长路径匹配实际上并不是确定的,跟路径被加入树中的顺序有关,bug: Adding routes to a Radix Tree in a different order can lead to the same URL matching the first added route instead of the longest path match

这个问题目前官方还没定位修复,我们目前是通过写入路由配置时,计算 priority,使得其看起来像最长路径匹配 通过 priority 解决最长路径匹配失败:calculateMatchSubPathRoutePriority

DNS 服务出问题,恢复后,apisix 无法从解析失败中恢复

DNS 解析失败之后,有报错日志; 然后 DNS 服务恢复了,apisix 也一直无法将请求进行转发

报错:

2023/04/20 09:08:02 [error] 68#68: *151026556 [lua] resolver.lua:47: parse_domain(): failed to parse domain: aaa.com, error: failed to query the DNS serve
r: dns server error: 2 server failure, client: 1.1.1.1, server: _, request: "POST /api/v1/xxxxx HTTP/1.0", host: "aaa.com"
2023/04/20 09:08:02 [error] 68#68: *151026556 [lua] upstream.lua:79: parse_domain_for_nodes(): dns resolver domain: aaa.com error: failed to query the DNS
 server: dns server error: 2 server failure, client: 1.1.1.1, server: _, request: "POST /api/v1/xxxxx  HTTP/1.0", host: "aaa.com"
2023/04/20 09:08:02 [error] 68#68: *151026556 [lua] init.lua:540: http_access_phase(): failed to set upstream: no valid upstream node, client: 1.1.1.1, server: _, req
uest: "POST /api/v1/xxxxx HTTP/1.0", host: "aaa.com"

触发条件:route 有 upstream(upstream 中是域名), 并且 router 关联到了 service, 并且 service 中有 upstream

相关 issue: bug: dns resolution did not resume immediately after the dns server resume

这个问题官方暂时没有定位修复,解决方法:自行 patch parse_domain_for_nodes

并且切换到更为稳定的 DNS 服务

管理端频繁变更带来的性能抖动

我们有几万个 route, 变更非常频繁,我们发现在管理端变更时,会导致 apisix 的性能抖动 help request: Is the radix tree rebuilt every time any route is updated?

后来通过阅读源码发现,每次 watch 到变更,都会导致 radixtree 重建,并且这个重建是同步的,会导致性能抖动

官方后来有个 feat: increment route update for radixtree host uri, radixtree uri and radi… 做增量更新优化

我们暂时的解决办法:如果有变更,定期重建 radixtree(而不是实时), 并且打散到一定范围的时间内,避免整体服务性能抖动,具体参考 radixtree_uri_with_parameter_rebuild_with_interval

url 路径参数中带中文会导致 404

相关 issue help request: when use Chinese word as uri parameter got 404

apisix 上下文中使用的路径是decode过的,但是下层 radixtree 使用的是encode过的,会导致这类路径匹配 404

目前我们暂时用 10561 这种方式修复,但是这样做看着比较低效 (我们压测完发现差别不大,线上直接先 patch 了)

zero downtime deployment

发布时,正在处理的请求会受到影响 Is the apisix support preStop hook? for graceful shutdown

除了这个,还遇到 ipvs+linux 内核版本会导致滚动更新时,还会有持续的流量进入到正在 terminate 的 pod 中

etcd compacted 导致 apisix 全量拉取

生产环境上线后,etcd 会定期出现内存使用量告警,突然飙升,伴随着大量的 read 行为

查看 etcd 配置以及阅读 apisix 源码发现,etcd 当前配置 5 分钟定时 compacted 一次,存在发布变更的话,apisix watch 到 event 发现 err=compacted, 会导致 apisix 全量拉取数据,此时会导致大量的 read 行为,导致 etcd 内存飙升

所以建议 apisix 依赖的 etcd 配置 compaction mode 改成 revision, 相关文档

变更 etcd 配置:

auto-compaction-mode: revision
auto-compaction-retention: "1000"

file-logger 的性能问题

自定义插件,通过增加 batch-processor 解决 (本质上就是将 json encode 转移到了 nginx timer 里)

queryString 中使用分号作为分隔符

灰度过程中,有用户反馈拼接的参数没有生效,而在原来 Golang 版本网关中是正常的

排查发现,我们使用的是 Golang 1.6, 这个版本中,支持同时使用&;作为 QueryString 的分隔符,然后这个特性在 Golang 1.7 中被去除了 Go 1.17 Release Notes: URL query parsing

所以,存量的请求中,原先有一些请求使用的是;作为分隔符,灰度到 Apisix 后就失效了

最后我们通过一个自定义插件,向前兼容了这批路由,原理很简单,把 ; 换成 & 再放回去解析

local new_args = string_replace(ctx.var.args, ";", "&")
core.request.set_uri_args(ctx, new_args)

偶发 502

有应用反应原先使用 nginx 作为反向代理的时候正常,接入 apisix 之后偶发 502

nginx 默认的配置 proxy_http_versionHTTP/1.0;

即没有配置的情况下,默认是 HTTP/1.0

但是 apisix 的默认配置

proxy_http_version 1.1;
keepalive_timeout 60s;

这就意味着,如果后端服务开启了 keepalive, 但是配置的 timeout 小于 60s, 那么就会出现偶发 502

实际验证中,对于 gunicorn 以 gevent/gthread 方式启动的服务,将默认的2s改成65s也不能规避这个问题,只能通过 --keep-alive 0 关闭 keepalive; 相关文档