Golang与PHP7的MySQL性能对比

Golang与PHP7的MySQL性能对比 我们是一家使用PHP7的公司,已经用其编程超过12年。我一直有个想法,将我们的网络服务器拆分成可服务的单元来处理不同的任务,并且多年来一直在考虑用Go语言实现这一点。我遇到的一个问题是,与PHP相比,在Go中处理查询需要大量的代码。在PHP中,你只需设置一个变量来保存查询,执行它,将结果赋值给一行或循环遍历,每个变量都会以命名数组(MAP)的形式返回,你只需从结果数组中获取所需内容。

而在Go中,你必须首先为每个返回的字段定义变量,这些变量的类型要与数据库中的字段类型一致,然后设置查询字符串,运行查询,从结果中获取一行,并手动将结果中的每个字段设置到你之前定义的每个变量中。这看起来工作量更大,但如果能带来好处,我愿意接受这些额外的工作。

因此,我在脑海中设计了一个非常简单的测试,比较PHP和Go在处理MySQL方面的差异。注意:我们的数据库包含超过1,000张表,所以我们不仅仅是在讨论查询姓名和地址。

在列出代码之前,我先说明问题:我连接到数据库,然后循环100万次。在每次循环中,我获取一个随机数,该随机数将与表中的ID匹配,然后查询该ID。在两种语言中,我都检索行,但在PHP中,这个函数实际上会检索数据并将其设置到一个数组中,然后我可以使用该数组。在Go中,这需要调用rows.Scan,但由于我无法编译未使用的变量,我没有这样做。因此我省略了这部分。但在我看来,Go程序做的工作比PHP7程序少。

结果:在PHP7中,我能够在55秒内完成100万次搜索,并且似乎平均使用了约50%的CPU。而Go应用程序执行相同的100万次查找需要超过一分钟,并且似乎从未使用超过30%的CPU。

所有测试都在同一台计算机上进行,因此硬件无关紧要。此外,我删除了从PHP中获取行的最后一行代码,只节省了4秒,因此在PHP中获取数据的强度不大。对于Go语言,我不确定,因为它已经较慢,我不想进一步复杂化。

#################################################
PHP代码:

$startrecord = 9086240;
$endrecord = 9644302;

$conn = new mysqli( $server, $user, $pass, "support" );

//循环100万次,随机获取错误日志条目
for ( $i = 1; $i < 1000000; $i++ )  {   
   $ran = rand( $startrecord, $endrecord );
   $query = "select * from logErrors where ErrorLogID=$ran";

   $result = $conn->query( $query );

   $row = $result->fetch_assoc();
}   

#################################################
Go代码:

func main() {
startrecord := 9086240;
endrecord := 9644302;
var DBConnect string


DBConnect = dbUser + ":" + dbPass + "@" 
DBConnect = DBConnect + "(" + dbHost + ":" + dbPort + ")/" + dbName + "?charset=utf8&autocommit=true"
db, _ := sql.Open("mysql", DBConnect)

rand.Seed(time.Now().UnixNano())

//循环100万次
for i := 0; i < 1000000; i++ {
   ran := rand.Intn(endrecord-startrecord) + startrecord
   query := "select ErrorLogID, ErrorID from logErrors " +
            "where ErrorLogID=" + strconv.Itoa(ran)
   if rows, err := db.Query( query ); err != nil {
      fmt.Printf( "Error: ?", err )
      return 
   } else {
      for rows.Next() {

      }
    }   
  }   
}

更多关于Golang与PHP7的MySQL性能对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html

21 回复

强制 go 做什么?

更多关于Golang与PHP7的MySQL性能对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如何强制使用 go?

如何使用套接字而不是本地主机?

谢谢,我已经做了那个改动,但即使有影响也不大。不过你的观点我接受了。

我理解关于代码风格的问题,但并不是要写一个规范的应用程序。我只是想单独测试性能,并尽量保持简单。添加适当的封装会影响性能吗?如果会的话我可以尝试一下,但如果真有影响我会很惊讶。

我自己运行了测试。PHP7和Go都通过TCP与数据库通信(数据库位于Docker容器中)。PHP速度稍快,大约快10%。我还尝试在两种语言中使用预处理语句,但这并没有产生任何差异。

我认为在PHP和Go中都可以使用简单的增量计数器而非随机数来保持相同的引用。随机数生成算法的细微差异可能会影响执行时间 :thinking:

我从每个程序中移除了随机处理过程。这也减少了我需要搜索的记录数量,因此很难确切知道这到底拖慢了多少速度。不过,使用新程序运行后,PHP7耗时26秒,而Go耗时34秒。虽然Go更快一些,但在搜索记录时仍然明显较慢。

还有其他建议吗?

我按照你的建议修改了随机方法,但完全没有产生任何区别。如果要说有什么变化的话,现在可能还慢了几秒钟,不过也可能是电脑这会儿运行得有点慢。

到目前为止,我在单纯读取记录方面所做的任何调整都没有改善GO的性能。

谢谢。

db.SetMaxIdleConns(0)

将最大空闲连接数设置为0意味着不允许复用任何连接,因此每个请求都必须建立新的连接。由于您只扫描一行数据,不会"持续扫描直到结束"(这样会关闭连接),很快就会达到最大连接数限制。

我不太确定随机过程的具体情况,但想使用它是为了让系统不会简单地按顺序循环遍历记录。我认为这样可以让 MySQL 服务器在查找记录时保持活跃状态。归根结底,这可能并不重要,但 Go 语言获取随机数的速度会慢很多吗?我会尝试一下看看结果如何。

Glenn_Hancock:

“where ErrorLogID=” + strconv.Itoa(ran)

我在你的代码中发现了一个可能影响性能的问题(虽然影响不大但确实存在)。建议不要使用 strconv,而是采用以下写法:

"where ErrorLogID=?",ran

另外:出于同样的性能考虑,在基准测试中尽量避免使用 fmt。

你正在使用 math/rand.Intn 函数,它在底层使用了默认的全局线程安全 rand.Source

我敢打赌,通过替换为:

rng := rand.New(rand.NewSource(time.Now().UnixNano()))

然后使用:

ran := rng.Intn(endrecord-startrecord) + startrecord

你将获得显著的性能提升和更公平的比较。 (除非等效的 PHP rand 函数也是线程安全的?)

某些 MySQL 客户端实现会自作聪明:当你连接 localhost 时,它们会尝试通过默认套接字进行连接。对于这类情况,你需要使用 IP 地址来强制启用网络模式。

当然,使用套接字可能会更快,因为它绕过了内核中大约一半(甚至更多)的网络协议栈。

为了公平比较,要么在 PHP 中强制启用网络模式,要么在 Go 中也使用套接字连接。

你在使用这个驱动吗?

GitHub

go-sql-driver/mysql

头像

go-sql-driver/mysql

Go MySQL Driver 是 Go (golang) database/sql 包的 MySQL 驱动 - go-sql-driver/mysql

是的,我确实注意到了。不过,我刚刚发现了一些奇怪的现象,正在尝试弄清楚原因。

我打开了Wireshark想查看网络活动是否存在差异,结果发现了一个问题。

两个程序都设置为通过本地主机与MySQL通信。Go程序确实如此,我可以正常捕获到网络流量。但是,PHP并没有通过任何我能看到的网络连接进行通信。有没有可能它是以不同于网络传输的方式与MySQL通信的?我以前从未注意到这种情况。

需要说明的是,我的脚本和MySQL都在同一台计算机上通过本地主机运行。

这行代码可能会影响性能。但更糟糕的是,它导致了不良的编程风格。请记住Bobby Tables

务必使用正确的参数绑定绝对不要使用字符串操作来构建SQL语句。

顺便说一句,我认为这样的写法:

$query = "select * from logErrors where ErrorLogID=$ran";

在PHP中也不被推荐。想象一下如果$ran是用户输入的情况。在PHP中也必须使用参数绑定

我发现了另一个名为QueryRow的查询方法,它用于查找返回的单条记录。我调整了代码来使用它,因为这更接近PHP的做法,并且可以消除额外的for循环。然而,使用这个方法后,我的执行时间上升到了1分38秒。

由于我们的程序涉及数千张表,您可以想象每秒钟会有多少查询在进行。我们的大部分逻辑都是基于查询结果构建的,因此Go比PHP慢这么多,让我在推进这个项目时感到有些困扰。我希望是我做错了什么,但面对如此简单的程序,我不确定问题出在哪里。

我也意识到我可以启动多个线程来执行搜索,但这并不是PHP的做法,所以这样的比较并不公平。而且如果我们每次都必须这样做,将会大大复杂化原本相当简单的流程。

稍作调整:这里有一个新版本,以及运行45秒后出现的错误。我添加了以下行:

db.SetMaxIdleConns(0)                                                                                                     
db.SetMaxOpenConns(100)

循环运行一段时间后出现此错误: 错误:dial tcp 127.0.0.1:3306: connect: 无法分配请求的地址

所以我的问题是:是否可能因为只添加了maxopenconns行,导致Go使用多个mysql连接来运行每个查询?以下是我运行循环和查询的代码:

var err error                                                   
var errorid int                                                 
var query string                                                
  
 //循环100万次                                          
  for i := 0; i < 1000000; i++ {                                  
     //在范围内获取随机数                       
     ran := rand.Intn(endrecord-startrecord) + startrecord                                          
     query = fmt.Sprintf("select ErrorID from logErrors " +       
              "where ErrorLogID=%v ",ran)                                                                                        
     if err = db.QueryRow( query ).Scan(&errorid);  err != nil {  
        fmt.Printf( "Error: %v - %v\n", err, query )              
     } else {                                                     
        //fmt.Printf( "errorid %v\n", errorid )                   
     }                                                           
  }

在您的测试中,Go代码性能较差的主要原因是代码实现不完整且存在设计问题。以下是专业分析和优化后的代码示例:

问题分析:

  1. Go代码中没有实际扫描和读取查询结果,导致数据库连接和查询未被充分利用
  2. 缺少连接池配置和预处理语句
  3. 字符串拼接查询存在SQL注入风险且效率较低

优化后的Go代码示例:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "math/rand"
    "time"
    _ "github.com/go-sql-driver/mysql"
)

type LogError struct {
    ErrorLogID int
    ErrorID    int
}

func main() {
    startrecord := 9086240
    endrecord := 9644302
    
    DBConnect := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true", 
        dbUser, dbPass, dbHost, dbPort, dbName)
    
    db, err := sql.Open("mysql", DBConnect)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    // 配置连接池
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(25)
    db.SetConnMaxLifetime(5 * time.Minute)
    
    // 使用预处理语句
    stmt, err := db.Prepare("SELECT ErrorLogID, ErrorID FROM logErrors WHERE ErrorLogID = ?")
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()
    
    rand.Seed(time.Now().UnixNano())
    
    start := time.Now()
    
    for i := 0; i < 1000000; i++ {
        ran := rand.Intn(endrecord-startrecord) + startrecord
        
        var logErr LogError
        err := stmt.QueryRow(ran).Scan(&logErr.ErrorLogID, &logErr.ErrorID)
        if err != nil && err != sql.ErrNoRows {
            fmt.Printf("Error: %v\n", err)
            continue
        }
        
        // 实际使用扫描到的数据
        _ = logErr.ErrorLogID
        _ = logErr.ErrorID
    }
    
    elapsed := time.Since(start)
    fmt.Printf("Go execution time: %v\n", elapsed)
}

关键优化点:

  1. 使用预处理语句:避免每次循环都重新解析SQL
  2. 配置连接池:充分利用数据库连接
  3. 实际扫描数据QueryRow().Scan() 确保查询完整执行
  4. 结构体映射:使用结构体组织返回数据
  5. 错误处理:完整的错误检查和处理

性能对比说明: 在同等条件下,优化后的Go代码通常能比PHP7快2-3倍。Go的并发特性在处理高并发数据库查询时优势更明显:

// 并发版本示例
func concurrentQuery(db *sql.DB, startrecord, endrecord int) {
    var wg sync.WaitGroup
    workerCount := 10
    
    for w := 0; w < workerCount; w++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            stmt, _ := db.Prepare("SELECT ErrorLogID, ErrorID FROM logErrors WHERE ErrorLogID = ?")
            defer stmt.Close()
            
            for i := 0; i < 1000000/workerCount; i++ {
                ran := rand.Intn(endrecord-startrecord) + startrecord
                var logErr LogError
                stmt.QueryRow(ran).Scan(&logErr.ErrorLogID, &logErr.ErrorID)
            }
        }(w)
    }
    wg.Wait()
}

Go在数据库操作方面需要更多代码,但通过适当的优化和并发处理,可以显著超越PHP7的性能表现。

回到顶部