在制作 AI GO ADMIN 的接口节流功能的时候,需要跨平台进行测试,突发奇想,何不找个全新的线上服务器,压测一下常见 PHP 框架和 Go 的来个对比,看看性能差距到底如何。
ai-go-admin 是一款主打可视化 CRUD 和极简内核的开源后台系统,完全诞生于 AI 时代,技术栈是 Golang + VUE3,最近刚做完,完全开源免费商用,欢迎体验:github.com/ai-go-hub/a...
测试环境
- CPU:4核 AMD EPYC 9K65 192-Core Processor
- 内存:8G
- 硬盘:通用 SSD(50 GB)
- 操作系统:Ubuntu Server 26.04 LTS 64位
- 基础资源占用:CPU 约 1%,内存约 12%
测试代码
代码直接让 AI 写的,目检了一下没啥大问题,放在文末。
测试代码规划纯凭感觉,一是需要含数据库业务,统一使用 PostgreSQL 数据库,同一个表,其中预设了 10 行数据,测试程序需要查询所有数据,然后直接输出数据条数。
人工确定了查库的逻辑是存在(生性多疑的我怕 AI 直接给我输出 10 😂),然后是支持常驻内存的,需要对数据库连接句柄进行维护,而不是每次都重新连接。
不返回所有数据内容,只是单纯的返回数据条数;不返回 JSON,大家都直接返回纯文本即可。
所有框架都没有引入多余依赖,做最简实现。
| 框架/语言 | 版本 | 说明 |
|---|---|---|
| Go | 1.27.0 | net/http、pgx 连接池 |
| php-fpm | 8.2 | 使用了 PHP 自带的 pgsql 扩展 |
| thinkphp | 8.0 | think-orm |
| webman | 2.1 | 使用了 PHP 自带的 pgsql 扩展 |
| ai go admin | 1.26.6 | 这份是功能齐全的完整系统,关闭节流后进行测试,全套生产级依赖,含:Gin、GORM 等 |
测试流程
首先将 pgsql 服务性能开到最高,最大连接数搞到最大,再对以上五份代码逐一、单独测试。
开始测试前总是确保服务器处于空闲状态,然后直接压测,确定已经将服务器资源全部占满且压测无任何报错为止。
测试结果
| 框架/语言 | RPS |
|---|---|
| 原生 PHP | 1363.16 |
| Thinkphp | 142.88 |
| Webman | 58084.70 |
| 原生 Go | 51993.83 |
首先是 ThinkPHP 属实是拉完了,PHP-FPM 也不太给力。
PHP 这边的大亮点是 Webman,它比原生 Go 还多点,从另外的角度证明 PHP 确实还有一战之力;
另外值得解释一下的是:Webman 基于多进程模型,单个请求发生 IO 阻塞时会占用整个工作进程,导致该进程上的全部请求被阻塞(以上压测体现不出来);而 Go 的 Goroutine 调度可以做到 M:N 调度隔离,单个请求阻塞不会影响其他请求。同时 Go 的内存管理心智负担小得多,所以并不是 Go 拉胯,应该是有所取舍?
根据 workerman 官方的数据,使用 ORM 的话每秒请求数大概要降一半,也就是大概 2.9W,Go 加上 Gin + GORM + 生产全套依赖 后还有 3.3W 左右(见后续 AI GO ADMIN 压测),个人感觉综合下来两者并发性能其实大差不差,实在要对比语言的话还是得看其他方面的差异,不再本文讨论范围内。
以下依次列出了具体压测数据
原生 PHP
bash
wrk -t4 -c100 -d30s http://127.0.0.1:8081/index.php
Running 30s test @ http://127.0.0.1:8081/index.php
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 73.84ms 21.63ms 427.87ms 84.75%
Req/Sec 342.38 37.92 480.00 70.83%
40941 requests in 30.03s, 7.77MB read
Requests/sec: 1363.16
Transfer/sec: 264.91KB
Thinkphp
bash
wrk -t4 -c100 -d30s http://127.0.0.1:8082/bench
Running 30s test @ http://127.0.0.1:8082/bench
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 691.23ms 121.58ms 1.27s 71.48%
Req/Sec 36.56 18.96 121.00 70.87%
4291 requests in 30.03s, 838.09KB read
Requests/sec: 142.88
Transfer/sec: 27.91KB
Webman
bash
wrk -t4 -c100 -d30s http://127.0.0.1:8787/bench
Running 30s test @ http://127.0.0.1:8787/bench
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.20ms 3.33ms 129.03ms 94.51%
Req/Sec 14.63k 7.28k 56.68k 62.20%
1748345 requests in 30.10s, 175.07MB read
Requests/sec: 58084.70
Transfer/sec: 5.82MB
原生 Go
bash
wrk -t4 -c100 -d30s http://127.0.0.1:8090/bench
Running 30s test @ http://127.0.0.1:8090/bench
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.26ms 3.20ms 77.06ms 97.91%
Req/Sec 13.07k 1.81k 16.08k 92.67%
1560300 requests in 30.01s, 175.59MB read
Requests/sec: 51993.83
Transfer/sec: 5.85MB
ai-go-admin 压测
首先得关闭接口节流(config/throttle.yaml),然后调高数据库连接池配置(config/config.yaml 里边的 max_open_conns: 200 和 max_idle_conns: 100)。
先做了个不带数据库业务的
bash
wrk -t4 -c100 -d30s http://127.0.0.1:8080/ping
Running 30s test @ http://127.0.0.1:8080/ping
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.05ms 1.36ms 28.50ms 88.09%
Req/Sec 36.98k 3.51k 50.69k 72.75%
4416345 requests in 30.01s, 690.73MB read
Requests/sec: 147154.69
Transfer/sec: 23.02MB
带数据库业务
bash
wrk -t4 -c100 -d30s http://127.0.0.1:8080/bench
Running 30s test @ http://127.0.0.1:8080/bench
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.33ms 2.79ms 57.06ms 78.72%
Req/Sec 8.47k 675.08 10.81k 75.58%
1010958 requests in 30.00s, 176.43MB read
Requests/sec: 33694.62
Transfer/sec: 5.88MB
测试代码
pgsql 初始化
sql
-- 测试表:5 个字段
CREATE TABLE test_users (
id SERIAL PRIMARY KEY,
username VARCHAR(64) NOT NULL,
email VARCHAR(128) NOT NULL,
age INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 插入 10 行测试数据
INSERT INTO test_users (username, email, age, created_at) VALUES
('alice01', 'alice01@example.com', 21, '2026-01-01 09:00:00'),
('bob02', 'bob02@example.com', 22, '2026-01-02 09:00:00'),
('carol03', 'carol03@example.com', 23, '2026-01-03 09:00:00'),
('dave04', 'dave04@example.com', 24, '2026-01-04 09:00:00'),
('eve05', 'eve05@example.com', 25, '2026-01-05 09:00:00'),
('frank06', 'frank06@example.com', 26, '2026-01-06 09:00:00'),
('grace07', 'grace07@example.com', 27, '2026-01-07 09:00:00'),
('henry08', 'henry08@example.com', 28, '2026-01-08 09:00:00'),
('iris09', 'iris09@example.com', 29, '2026-01-09 09:00:00'),
('jack10', 'jack10@example.com', 30, '2026-01-10 09:00:00');
Golang 原生
go
// Go 基准测试程序:net/http + pgx 连接池
// 每次请求:从连接池取连接 -> 查询 test_users 全表 -> 返回行数
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/jackc/pgx/v5/pgxpool"
)
// 数据库配置(硬编码,与其它测试程序保持一致)
const (
dbHost = "127.0.0.1"
dbPort = 5432
dbName = "phpgo_bench"
dbUser = "bench"
dbPassword = "postgres"
// 连接池大小:默认等于 GOMAXPROCS,压测时可按 CPU 核数调整
poolMaxConns = 8
listenAddr = ":8090"
)
var pool *pgxpool.Pool
func benchHandler(w http.ResponseWriter, r *http.Request) {
var n int
err := pool.QueryRow(r.Context(),
"SELECT count(*) FROM (SELECT id, username, email, age, created_at FROM test_users) t",
).Scan(&n)
if err != nil {
http.Error(w, "query failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, strconv.Itoa(n))
}
func main() {
dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s pool_max_conns=%d",
dbHost, dbPort, dbName, dbUser, dbPassword, poolMaxConns)
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
log.Fatalf("parse dsn: %v", err)
}
pool, err = pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
log.Fatalf("create pool: %v", err)
}
if err := pool.Ping(context.Background()); err != nil {
log.Fatalf("ping db: %v", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/bench", benchHandler)
mux.HandleFunc("/", benchHandler)
fmt.Printf("go bench listening on %s\n", listenAddr)
if err := http.ListenAndServe(listenAddr, mux); err != nil {
log.Fatalf("listen: %v", err)
os.Exit(1)
}
}
php-fpm
php
<?php
/**
* PHP-FPM 基准测试脚本(PHP 8.2,零依赖,仅用 ext-pgsql)
* 每次请求:连接 pgsql -> 查询 test_users 全表 -> 输出行数
*/
$host = '127.0.0.1';
$port = 5432;
$dbname = 'phpgo_bench';
$user = 'bench';
$password = 'postgres';
$conn = pg_connect(
"host={$host} port={$port} dbname={$dbname} user={$user} password={$password}",
PGSQL_CONNECT_FORCE_NEW
);
if ($conn === false) {
http_response_code(500);
echo 'pg_connect failed';
exit;
}
$result = pg_query($conn, 'SELECT id, username, email, age, created_at FROM test_users');
if ($result === false) {
http_response_code(500);
echo 'pg_query failed: ' . pg_last_error($conn);
exit;
}
header('Content-Type: text/plain; charset=utf-8');
echo pg_num_rows($result);
pg_free_result($result);
pg_close($conn);
ThinkPHP
php
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
/**
* 基准测试控制器:查询 pgsql test_users 全表并返回行数
*/
class Bench extends BaseController
{
public function index()
{
$rows = Db::query('SELECT id, username, email, age, created_at FROM test_users');
return (string) count($rows);
}
}
Webman
php
<?php
namespace app\controller;
use support\Request;
use Throwable;
/**
* 基准测试控制器:查询 test_users 全表并返回行数
*
* webman 是常驻内存进程,pg_connect 对同一连接串会复用本进程内
* 已建立的连接,因此压测时每个 worker 进程只有一条数据库连接。
*/
class BenchController
{
public function index(Request $request)
{
$conn = @pg_connect(
'host=127.0.0.1 port=5432 dbname=phpgo_bench user=bench password=postgres'
);
if ($conn === false) {
return response('pg_connect failed', 500);
}
try {
$result = pg_query($conn, 'SELECT id, username, email, age, created_at FROM test_users');
if ($result === false) {
return response('pg_query failed: ' . pg_last_error($conn), 500);
}
return response((string) pg_num_rows($result) . 'wm');
} catch (Throwable $e) {
// 连接可能已被服务端断开,丢弃后下个请求自动重连
pg_close($conn);
return response('error: ' . $e->getMessage(), 500);
}
}
}