PHP 各框架下和 Go 的性能比较

在制作 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: 200max_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);
        }
    }
}
相关推荐
名字还没想好☜1 小时前
Python 字符编码实战:encode/decode、UnicodeDecodeError 与 open 的 encoding 坑
开发语言·后端·python·编程语言
Csvn1 小时前
TypeScript 类型性能:让类型体操不再卡死编译器
前端
大勇前进1 小时前
Java 线程创建的 4 种方式,优缺点对比,开发推荐写法
后端
明月_清风1 小时前
Foundry Web3 测试框架入门:从 0 开始写 Solidity 测试
后端·web3·solidity
万少1 小时前
等不到 Apple 的折叠 iPhone,我用 DeepV4.1Flash + workBuddy 一句话自己造了一台
前端·javascript·后端
卷无止境1 小时前
AI Agent编程中,重构节奏与安全检查的门道
后端·python
梦曦i1 小时前
uni-router v0.2.0重磅发布:全链路拦截+重复导航优化
前端·uni-app
数据知道1 小时前
AES 加密实战——模式选择(ECB/CBC/GCM)与安全陷阱
前端·网络·安全
卷无止境2 小时前
AI编程时代,代码复杂性正在悄悄失控
后端·python