Nothing here requires action to keep working — no migration, no import path change. One behaviour change is worth reading first.
Behaviour change
Rate-limited requests now return HTTP 429. They previously returned HTTP 200 with code: 500 in the body, so a load balancer, an uptime monitor and a benchmark all counted a rejection as a success — the earlier load tests here reported over ten times the real throughput before a status distribution gave it away. The threshold moves to extend.rateLimit.inboundQPS, defaulting to the 200 that was hardcoded, so the limit itself is unchanged; set it to 0 to switch the limiter off behind a gateway that already rate-limits. #881
- 🔒 Every tenant after the first was authorized against another tenant's policy. go-admin-core cached the casbin enforcer under a
sync.Once, and this repository passed the same empty key for every configured database. In the multi-tenant setup — one database per host — every host after the first was handed the enforcer built from the first host's database and never loaded its owncasbin_ruletable: a permission granted in one tenant applied in the others, and one denied there stayed denied however that tenant was configured. Both halves are needed, and both land here. #885 core#141 - 🔒 The captcha endpoint logged its own answer. Anyone who could read the application log could log in without solving it. #881
- 🐛 The shipped configuration had no connection pool settings, so Go's defaults applied — and
MaxIdleConnsdefaults to 2. Under load almost every request opened a TCP connection and closed it immediately, exhausting local ports:can't assign requested address, every request failing. Not slower, unavailable. The templates now setmaxIdleConns: 20,maxOpenConns: 100andconnMaxLifeTime: 3600, with the reasoning next to them —maxOpenConnsmultiplies by instance count against the database'smax_connections, andconnMaxLifeTimehas to stay under itswait_timeout. #881 - 🐛 The memory queue's buffer was 100, which is where it starts dropping rather than how much it processes at once:
Appenddiscards the message and returns an error instead of waiting. Each stream has one consumer goroutine, and the login and operation log consumers write to the database, so a burst above that write rate has only the buffer to absorb it. Under load 100 dropped more than 60% of messages; 1000 dropped none. Applies whenlogger.enableddbis on. #881 - 👌 The authorization path no longer recompiles a regexp per policy and per exclusion entry. casbin's
util.KeyMatch2callsregexp.MatchString, which compiles every time; the exclusion list is walked per request (32 entries, about 2,566 allocations) and the matcher runs once per permission the role holds. ServingGET /api/v1/deptto a non-admin whose role holds 201 permissions goes from 2,933 req/s to 13,882 at 256 concurrent, with p99 falling from 343ms to 39ms — throughput had been flat from 16 to 256 concurrent, the process saturated compiling patterns. Note thatadminskips the permission check entirely, so none of this is visible when testing with an admin token. #885 core#141 - 🔧 go-admin-core moves from v2.1.0 to v2.3.0, which is where several of the above come from and brings more besides: the in-memory queue lost about one message in seven when consumers registered while the first requests arrived — that queue carries the login and operation logs;
ResolveSearchQuerypanicked on an unexported field, so a single one on a search DTO took down the whole list endpoint; counter updates were lost under concurrency; and a cache sweep held one lock across the whole map, stalling reads for 16ms once a minute at a million entries. #881 #885 - ✅ Adds a load-test harness reporting latency percentiles and the status-code distribution across a concurrency sweep. It is skipped unless
GOADMIN_BENCH_ADDRpoints at a running server, sogo test ./...is unaffected. The status distribution is what caught the 429 problem above. #881 - 📝 Adds Traditional Chinese and Japanese READMEs, repoints the build badge — it had reported failing for years because it named a workflow that no longer exists — and unifies the documentation links. #882 #883
- 🔧 Documentation-only changes no longer trigger the deploy workflow. #884
Throughput figures were measured against MySQL on one machine, load generator and server sharing it. Read them as relative changes rather than numbers your hardware will reproduce.
本次发布没有任何需要动手才能继续工作的变更 —— 不用迁移,不用改导入路径。有一处行为变更值得先读。
行为变更
被限流的请求现在返回 HTTP 429。 此前返回的是 HTTP 200,只在 body 里写 code: 500,于是负载均衡、可用性监控和压测都把一次拒绝算成了一次成功 —— 本仓库早前的压测因此报出了十倍于真实值的吞吐,直到打印状态码分布才发现。阈值移到 extend.rateLimit.inboundQPS,默认值就是此前写死的 200,所以限流阈值本身没有变化;部署在自带限流的网关后面时,填 0 可以关闭限流。#881
- 🔒 第一个之后的所有租户,都在用别的租户的策略做鉴权。 go-admin-core 用
sync.Once缓存 casbin enforcer,而本仓库对每个已配置的数据库都传了同一个空 key。在多租户配置下(每个 host 一个库),第一个之后的每个 host 拿到的都是用第一个 host 的库构建的 enforcer,从未加载过自己的casbin_rule表:一个租户里授予的权限会在其他租户生效,而那里被拒绝的,无论该租户怎么配都仍然被拒绝。两侧改动缺一不可,本次一并发布。#885 core#141 - 🔒 验证码接口把自己的答案写进了日志。任何能读到应用日志的人都可以不解验证码直接登录。#881
- 🐛 随仓库分发的配置里没有连接池设置,于是走 Go 的默认值 —— 而
MaxIdleConns默认只有 2。高负载下几乎每个请求都新建一条 TCP 连接、用完立刻关闭,本机端口迅速耗尽:can't assign requested address,请求全部失败。不是变慢,是不可用。 配置模板现在给出maxIdleConns: 20、maxOpenConns: 100、connMaxLifeTime: 3600,并在旁边写清了取值依据 ——maxOpenConns要乘以实例数再跟数据库的max_connections比,connMaxLifeTime必须小于它的wait_timeout。#881 - 🐛 内存队列的缓冲长度是 100,而这个值是「从哪里开始丢」而不是「一次处理多少」:队列满时
Append直接丢弃该消息并返回错误,不会阻塞等待。每个 stream 只有一个消费 goroutine,而登录日志、操作日志的消费要写数据库,突发流量高于这个写入速度时,缓冲区是唯一的缓解手段。压测中 100 的丢弃率超过 60%,1000 为 0。仅在logger.enableddb开启时生效。#881 - 👌 鉴权路径不再为每条策略、每个排除项重新编译一次正则。 casbin 的
util.KeyMatch2调用的是regexp.MatchString,每次都重新编译;而排除列表每请求遍历一次(32 项,约 2,566 次内存分配),匹配器则对角色持有的每条权限各跑一次。对持有 201 条权限的非 admin 用户提供GET /api/v1/dept,256 并发下从 2,933 req/s 提升到 13,882,p99 从 343ms 降到 39ms —— 改动前吞吐从 16 到 256 并发是持平的,进程已被正则编译打满。注意admin会完全跳过权限校验,所以用 admin 账号测试看不到这里的任何变化。#885 core#141 - 🔧 go-admin-core 从 v2.1.0 升到 v2.3.0,上面几条中有几条正来源于此,此外还带来:内存队列在「首批请求到达的同时注册消费者」时约每七条丢一条 —— 而登录日志和操作日志走的就是这条队列;
ResolveSearchQuery遇到未导出字段会 panic,搜索 DTO 上只要有一个,整个列表接口就崩;计数器在并发下丢更新;缓存清理持有单一锁遍历整张表,百万条目下每分钟让读取停顿 16ms。#881 #885 - ✅ 新增压测工具,在一轮并发扫描中输出延迟分位数和状态码分布。未设置
GOADMIN_BENCH_ADDR指向运行中的服务时自动跳过,不影响go test ./...。上面那个 429 的问题,正是靠状态码分布发现的。#881 - 📝 新增繁体中文与日文 README,修正构建徽章 —— 它多年来一直显示 failing,因为指向的工作流早已不存在 —— 并统一了文档站链接。#882 #883
- 🔧 只改文档时不再触发部署工作流。#884
吞吐数据在单台机器上对 MySQL 实测,压力生成器与服务端共用同一台机器。请作为相对变化幅度参考,而非你的硬件上可复现的绝对值。