乐于分享
好东西不私藏

Go调度器深度解析:吃透这篇Go调度器源码剖析,才算真正跨过架构师门槛

Go调度器深度解析:吃透这篇Go调度器源码剖析,才算真正跨过架构师门槛

📑 文章目录

  1. 引言
  2. 编译与Go运行时
  3. 原始调度器
  4. 调度器增强
  5. GMP模型详解
  6. 程序引导
  7. 创建Goroutine
  8. 调度循环
  9. 查找可运行Goroutine
  10. Goroutine抢占
  11. 处理系统调用
  12. 网络I/O与文件I/O
  13. Netpoll工作原理
  14. 垃圾收集器
  15. 常用函数
  16. Go运行时API

1. 引言

Go(或称Golang)自2009年问世以来,作为构建并发应用的编程语言,其受欢迎程度稳步增长。它被设计为简单、高效且易于使用。

Go的并发模型围绕 goroutine 这一概念构建,这是由Go运行时在用户空间管理的轻量级用户线程。Go提供了有用的同步原语(如channel),帮助开发者轻松编写并发代码。它还使用了复杂的技术使I/O密集型程序保持高效。

理解Go调度器对于Go程序员编写高效的并发程序至关重要。它也有助于我们更好地排查性能问题或调优Go程序的性能。

在本文中,我们将探讨Go调度器是如何随时间演进的,以及我们编写的Go代码在底层究竟发生了什么。

2. 编译与Go运行时

本文包含大量源码走读,因此最好先对Go代码如何编译和执行有一个基本的了解。

当构建一个Go程序时,会经历三个阶段:

  1. 编译(Compilation)
    :Go源文件(*.go)被编译成汇编文件(*.s)。
  2. 汇编(Assembling)
    :汇编文件(*.s)随后被汇编成目标文件(*.o)。
  3. 链接(Linking)
    :目标文件(*.o)被链接在一起,生成单个可执行二进制文件。
Go 代码是如何转化为可执行二进制文件的
要理解Go调度器,首先必须理解Go运行时(Runtime)。

Go运行时是编程语言的核心,提供调度、内存管理和数据结构等基本功能。它本质上就是一组让Go程序得以运行的函数和数据结构的集合。Go运行时的实现可以在 runtime 包中找到。

Go运行时由Go语言和汇编代码混合编写而成,其中汇编代码主要用于处理寄存器等底层操作。

Go 运行时的角色

在编译时,Go编译器会将某些关键字和内置函数替换为Go运行时的函数调用。例如,用于生成新goroutine的 go 关键字会被替换为对 runtime.newproc 的调用;或者用于分配新对象的 new 函数会被替换为对 runtime.newobject 的调用。

你可能会惊讶地发现,Go运行时中的某些函数根本没有Go语言实现。例如,像 getg 这样的函数会被Go编译器识别,并在编译期间替换为底层汇编代码。其他函数(如 gogo)则是平台相关的,完全用汇编实现。将这些汇编实现与其Go声明连接起来是Go链接器的责任。

在某些情况下,一个函数在其包中似乎没有实现,但实际上是通过 //go:linkname 编译器指令链接到了Go运行时中的定义。例如,常用的 time.Sleep 函数实际上链接到了其在 runtime.timeSleep 中的真实实现。

3. 原始调度器(Primitive Scheduler)

⚠️ Go调度器并不是一个独立的对象,而是一组促进调度的函数集合。此外,它并不运行在专用线程上;相反,它运行在与goroutine相同的线程上。随着你阅读完本文的其余部分,这些概念会变得更加清晰。

如果你曾从事过并发编程,可能对多线程模型比较熟悉。它规定了用户空间线程(Kotlin/Lua中的协程或Go中的goroutine)如何复用到单个或多个内核线程上。通常有三种模型:

多对一(N:1)

一对一(1:1)

多对多(M:N)

Go选择了 多对多(M:N) 线程模型,允许多个goroutine复用到多个内核线程上。这种方法以复杂性为代价,利用多核系统的优势并使Go程序在系统调用方面保持高效,解决了N:1和1:1模型的问题。

由于内核不知道什么是goroutine,只向用户空间应用程序提供线程作为并发单元,因此是内核线程负责运行调度逻辑、执行goroutine代码并代表goroutine进行系统调用。

在早期,特别是在1.1版本之前,Go以一种朴素的方式实现了M:N多线程模型。当时只有两个实体:goroutine(G)和内核线程(M,即Machine)。

一个单一的全局运行队列用于存储所有可运行的goroutine,并使用锁来防止竞态条件。运行在每个线程M上的调度器负责从全局运行队列中选择一个goroutine并执行它。

原始调度器

如今,Go以其高性能的并发模型而闻名。遗憾的是,早期的Go并非如此。Go的核心贡献者之一 Dmitry Vyukov 在他著名的《可扩展Go调度器设计》文档中指出了该实现的多个问题:“总的来说,调度器可能会阻碍用户在性能关键场景中使用惯用的细粒度并发。”

让我详细解释一下他的意思。

首先,全局运行队列是性能瓶颈。当创建一个goroutine时,线程必须获取锁才能将其放入全局运行队列。同样,当线程想要从全局运行队列中获取goroutine时,也必须获取锁。众所周知,加锁不是免费的,锁竞争会带来开销。锁竞争会导致性能下降,尤其是在高并发场景中。

其次,线程频繁地将其关联的goroutine移交给另一个线程。这导致局部性差和过多的上下文切换开销。子goroutine通常需要与其父goroutine通信。因此,让子goroutine在与父goroutine相同的线程上运行性能更高。

第三,由于Go一直使用 Thread-caching Malloc (TCMalloc),每个线程M都有一个线程本地缓存 mcache,可用于分配或持有空闲内存。虽然 mcache 仅由执行Go代码的M使用,但它甚至附加在阻塞于系统调用的M上,而这些M根本不使用 mcache。一个 mcache 最多占用2MB内存,并且在线程M销毁之前不会被释放。由于运行Go代码的M与所有M的比例可能高达1:100(太多线程阻塞在系统调用中),这可能导致资源消耗过多和数据局部性差。

4. 调度器增强

既然你已经了解了早期Go调度器的问题,让我们审视一些增强提案,看看Go团队如何解决这些问题,从而拥有了今天高性能的调度器。

提案1:引入本地运行队列

每个线程M配备一个本地运行队列来存储可运行的goroutine。当线程M上正在运行的goroutine G使用 go 关键字生成一个新的goroutine G1时,G1会被添加到M的本地运行队列中。如果本地队列已满,G1则被放入全局运行队列。

在选择要执行的goroutine时,M首先检查其本地运行队列,然后再查询全局运行队列。因此,该提案解决了上一节描述的第一个和第二个问题。

提案1

然而,它无法解决第三个问题。当许多线程M阻塞在系统调用中时,它们的 mcache 仍然保持附加状态,导致Go调度器本身的内存使用率很高,更不用说我们Go程序员编写的程序的内存使用了。

它还引入了另一个性能问题。为了避免饿死阻塞M(如上图中的M1)本地运行队列中的goroutine,调度器应该允许其他线程从中窃取goroutine。但是,如果有大量阻塞线程,扫描所有线程以找到非空运行队列的代价就变得非常高昂。

提案2:引入逻辑处理器

该提案在《可扩展Go调度器设计》中有所描述,其中引入了 逻辑处理器 P 的概念。所谓“逻辑”,意味着P假装执行goroutine代码,但实际上是由与P关联的线程M来真正执行。

线程的本地运行队列和 mcache 现在归P所有。该提案有效地解决了上一节中的遗留问题。

由于 mcache 现在附加到P而不是M,并且当G进行系统调用时M会与P分离,因此当大量M进入系统调用时,内存消耗保持在较低水平。此外,由于P的数量是有限的,工作窃取(stealing) 机制也是高效的。

提案2

随着逻辑处理器的引入,多线程模型仍然是M:N。但在Go中,它被特别称为 GMP模型,因为存在三种实体:Goroutine、Machine(线程)和Processor(处理器)。

5. GMP模型详解

Goroutine: g

当 go 关键字后跟一个函数调用时,会创建一个 g 的新实例,称为G。G是一个代表goroutine的对象,包含元数据,如其执行状态、栈以及指向相关函数的程序计数器。执行一个goroutine仅仅意味着运行G引用的函数。

当goroutine完成执行时,它不会被销毁;相反,它变为 dead 状态并被放入当前处理器P的空闲列表中。如果P的空闲列表已满,死亡的goroutine将被移动到全局空闲列表。当创建新的goroutine时,调度器首先尝试从空闲列表中复用一个,然后才从头分配一个新的。这种回收机制使得goroutine的创建比创建新线程便宜得多。

下表描述了GMP模型中goroutine的状态机(为简洁起见省略了部分状态和转换):

状态
描述
Idle
刚刚创建,尚未初始化
Runnable
当前在运行队列中,即将执行代码
Running
不在运行队列中,正在执行代码
Syscall
正在执行系统调用,未在执行代码
Waiting
未在执行代码,也不在运行队列中,
例如等待channel
Dead
当前在空闲列表中,刚退出或刚开始初始化

GMP 模型中 Goroutines 的状态机

Thread: m

所有的Go代码——无论是用户代码、调度器还是垃圾收集器——都运行在由操作系统内核管理的线程上。为了让Go调度器在GMP模型中良好地工作,引入了代表线程的 m 结构体,m 的实例称为M。

M维护着对当前goroutine G、当前处理器P(如果M正在执行Go代码)、前一个处理器P(如果M正在执行系统调用)以及下一个处理器P(如果M即将被创建)的引用。

每个M还持有一个特殊goroutine g0 的引用,它运行在 系统栈 上——即内核提供给线程的栈。与普通goroutine的动态大小栈不同,系统栈用于栈增长/收缩、垃圾收集以及 parking goroutine 等操作,因为这些操作本身需要一个有效的栈来运行。每当线程执行此类操作时,它会切换到系统栈并在g0的上下文中执行。

与goroutine不同,线程在M创建后立即运行调度器代码,因此M的初始状态是 running。当M被创建或唤醒时,调度器保证总是有一个 idle 处理器P可以与之关联以运行Go代码。如果M正在执行系统调用,它将与P分离(详见“处理系统调用”章节),P可能会被另一个线程M1获取以继续工作。

如果M无法从其本地运行队列、全局运行队列或netpoll中找到可运行的goroutine,它会保持 spinning 状态,再次尝试从其他处理器P和全局运行队列中窃取goroutine。注意,并非所有M都会进入spinning状态,只有当spinning线程数少于繁忙处理器数的一半时才会发生。当M无事可做时,它不会被销毁,而是进入睡眠状态,等待稍后被另一个处理器P1获取。

状态
描述
Running
正在执行Go运行时代码或用户Go代码
Syscall
当前正在执行(阻塞于)系统调用
Spinning
正在从其他处理器窃取goroutine
Sleep
睡眠中,不消耗CPU周期

GMP 模型中 Threads 的状态机

Processor: p

p 结构体在概念上代表一个用于执行goroutine的物理处理器。p 的实例称为P,它们在程序引导阶段创建。虽然创建的线程数量可能很大(Go 1.24中为10000),但处理器的数量通常很少,并由 GOMAXPROCS 决定。无论状态如何,恰好有 GOMAXPROCS 个处理器。

为了最小化全局运行队列的锁竞争,Go运行时中的每个处理器P都维护一个本地运行队列。本地运行队列不仅仅是队列,还由两部分组成:runnext(持有一个优先goroutine)和 runq(goroutine队列)。这两者都是P的可运行goroutine来源,但 runnext 专门作为一种性能优化而存在。

Go调度器允许P从其他处理器P1的本地运行队列中窃取goroutine。只有在前三次尝试从P1的 runq 窃取失败后,才会查询P1的 runnext。因此,当P想要执行goroutine时,如果先从自己的 runnext 查找可运行goroutine,锁竞争会更少。

P的 runq 组件是一个基于数组的、固定大小的循环队列。基于数组且固定大小为256个槽位,这允许更好的缓存局部性并减少内存分配开销。固定大小对于P的本地运行队列是安全的,因为我们还有全局运行队列作为备份。循环特性允许高效地添加和删除goroutine,无需移动元素。

mcache 作为TCMalloc模型的前端,被P用于分配微小和小对象。另一方面,pageCache 使内存分配器能够在不获取堆锁的情况下获取内存页,从而提高高并发下的性能。

为了使Go程序能很好地配合sleep、timeout或interval,P还管理着由 最小堆(min-heap) 数据结构实现的定时器,最近的定时器位于堆顶。在寻找可运行goroutine时,P还会检查是否有定时器已过期。如果是,P将对应的带定时器的goroutine添加到其本地运行队列,给该goroutine运行的机会。

状态
描述
Idle
未执行Go运行时代码或用户Go代码
Running
与正在执行用户Go代码的M关联
Syscall
与正在执行系统调用的M关联
GCStop
与因垃圾收集而STW(Stop-The-World)的M关联
Dead
不再使用,等待在 GOMAXPROCS 增加时被复用

GMP 模型中 Processors 的状态机

在Go程序执行的早期,有 GOMAXPROCS 个处理器P处于 idle 状态。当线程M获取处理器运行用户Go代码时,P转换为 running 状态。如果当前goroutine G进行系统调用,P与M分离并进入 syscall 状态。在系统调用期间,如果P被 sysmon 抢占(见“非协作式抢占”),它首先转换为 idle,然后被移交给另一个线程(M1)并进入 running 状态。否则,一旦系统调用完成,P重新附加到上一个M并恢复 running 状态。

当发生STW垃圾收集时,P转换为 gcStop 状态,并在STW结束后返回之前的状态。如果在运行时减少 GOMAXPROCS,多余的处理器转换为 dead 状态,并在以后 GOMAXPROCS 增加时被复用。

6. 程序引导(Program Bootstrap)

为了启用Go调度器,必须在程序引导期间对其进行初始化。此初始化通过汇编函数 runtime·rt0_go 处理。在此阶段,创建线程 M0(代表主线程)和goroutine G0(M0的系统栈goroutine)。主线程的线程本地存储(TLS)也被设置,G0的地址存储在此TLS中,允许稍后通过 getg 检索。

引导过程随后调用汇编函数 runtime·schedinit,其Go实现位于 runtime.schedinit。该函数执行各种初始化,最显著的是调用 procresize,它将设置多达 GOMAXPROCS 个逻辑处理器P处于 idle 状态。

主线程M0随后与第一个处理器关联,将其状态从 idle 转换为 running 以执行goroutine。之后,创建主goroutine来运行 runtime.main 函数,这是Go运行时的入口点。在 runtime.main 函数内部,创建一个专用线程来启动 sysmon(将在“非协作式抢占”部分描述)。注意,runtime.main 不同于我们编写的main函数;后者在运行时中以 main_main 出现。

主线程随后调用 mstart 开始在M0上执行,启动 调度循环 以获取并执行主goroutine。在 runtime.main 中,经过额外的初始化步骤后,控制权最终移交给用户定义的 main_main 函数,程序开始执行用户Go代码。

值得注意的是,主线程M0不仅负责运行主goroutine,还负责执行其他goroutine。每当主goroutine被阻塞时(例如等待系统调用或等待channel),主线程会寻找另一个可运行的goroutine并执行它。

总结一下,当程序启动时,有一个goroutine G执行 main 函数;两个线程——一个是主线程M0,另一个是为启动 sysmon 而创建的;一个处理器P0处于 running 状态,以及 GOMAXPROCS−1 个处理器处于 idle 状态。主线程M0最初与处理器P0关联以运行主goroutine G。

GMP 模型下的程序启动流程

值得一提的是,在引导阶段,运行时还会生成几个与内存管理相关的其他goroutine,如标记、清除和scavenging。然而,我们将把它们排除在本文范围之外。它们将在未来的文章中更详细地探讨。

7. 创建Goroutine

Go为我们提供了一个简单的API来启动并发执行单元:go func() { ... }()。在底层,Go运行时做了大量复杂的工作来实现这一点。

go 关键字只是Go运行时 newproc 函数的语法糖,该函数负责调度新的goroutine。这个函数本质上做三件事:初始化goroutine,将其放入调用者goroutine所在处理器P的运行队列,唤醒另一个处理器P1。

初始化Goroutine

当调用 newproc 时,只有在没有空闲goroutine可用时才会创建新的goroutine G。Goroutine在执行返回后变为空闲。新创建的goroutine G初始化为2KB栈,由Go运行时中的 stackMin 常量定义。此外,goexit(处理清理逻辑和调度逻辑)被压入G的调用栈,以确保在G返回时执行。初始化后,G从 dead 状态转换为 runnable 状态,表示已准备好被调度执行。

将Goroutine放入队列

如前所述,每个处理器P都有一个由两部分组成的运行队列:runnext 和 runq。当创建新的goroutine时,它被放入 runnext。如果 runnext 已经包含一个goroutine G1,调度器会尝试将G1移动到 runq(前提是 runq 未满),并将G放入 runnext。如果 runq 已满,G1连同 runq 中一半的goroutine将被移动到全局运行队列,以减轻P的工作负载。

唤醒处理器

当创建新的goroutine且我们希望最大化程序并发时,运行goroutine的线程会尝试通过 futex 系统调用唤醒另一个处理器P。为此,它首先检查是否有空闲处理器。如果有空闲处理器P可用,则创建一个新线程或唤醒一个现有线程进入 调度循环,在那里它将寻找可运行的goroutine来执行。创建或复用线程的逻辑在“启动线程”部分描述。

如前所述,GOMAXPROCS(活动处理器P的数量)决定了可以并发运行多少goroutine。如果所有处理器都很忙且不断生成新的goroutine,既不会唤醒现有线程也不会创建新线程。

整体流程梳理下方的图示说明了 Goroutine 的创建过程。为简化说明,我们假设 GOMAXPROCS=2,此时处理器 P1 还未进入调度循环,而 main 函数的唯一动作就是不断派生(spawn)新的 Goroutine。因为这些 Goroutine 不涉及系统调用(关于系统调用的处理,我们将在后续的“处理系统调用”章节探讨),所以系统恰好会创建一个额外的线程 M2,用来与处理器 P1 进行绑定关联。

GMP 模型下 Goroutine 的创建机制

8. 调度循环(Schedule Loop)

Go运行时中的 schedule 函数负责查找并执行可运行的goroutine。它在多种场景下被调用:当创建新线程时、当调用 Gosched 时、当goroutine被park或抢占时,或者在goroutine完成系统调用返回后。

选择可运行goroutine的过程很复杂,将在“查找可运行Goroutine”部分详细说明。一旦选择了goroutine,它就会从 runnable 转换为 running 状态,表明已准备好运行。此时,内核线程调用 gogo 函数开始goroutine执行。

但为什么称之为循环?如“初始化Goroutine”部分所述,当goroutine完成时,会调用 goexit 函数。该函数最终导致调用 goexit0,它为终止的goroutine执行清理并重新进入 schedule 函数——使 调度循环 回归。

Go Runtime 中的调度循环

但如果主线程卡在调度循环中,进程如何退出?只需查看Go运行时中的 main 函数,它由主goroutine执行。在 main_main(Go程序员编写的main函数的别名)返回后,调用 exit 系统调用终止进程。这就是进程能够退出的方式,也是主goroutine不等待由 go 关键字生成的goroutine的原因。

9. 查找可运行Goroutine

线程M有责任找到合适的可运行goroutine,以便最小化goroutine饥饿。此逻辑在 findRunnable 中实现,由 调度循环 调用。

线程M按以下顺序查找可运行goroutine,如果找到则停止链式查找:

  1. 检查 trace reader goroutine的可用性(用于“非协作式抢占”部分)。
  2. 检查 垃圾收集worker goroutine的可用性(在“垃圾收集器”部分描述)。
  3. 1/61的概率,检查 全局运行队列
  4. 如果M正在spinning,检查关联处理器P的 本地运行队列
  5. 再次检查 全局运行队列
  6. 检查 netpoll 以获取I/O就绪的goroutine(在“Netpoll工作原理”部分描述)。
  7. 从其他处理器P1的 本地运行队列 中窃取。
  8. 再次检查 垃圾收集worker goroutine的可用性。
  9. 如果M正在spinning,再次检查 全局运行队列

步骤1、2和8仅供Go运行时内部使用。步骤1中,trace reader用于跟踪程序执行。步骤2和8允许垃圾收集器与普通goroutine并发运行。虽然这些步骤不涉及“用户可见”的进度,但它们对Go运行时的正常运作至关重要。

步骤3、5和9不仅仅获取一个goroutine,而是尝试批量获取以提高效率。批量大小计算为 (global_queue_size/number_of_processors)+1,但受限于几个因素:不超过指定的最大参数,且不超过P本地队列容量的一半。确定获取数量后,它弹出一个goroutine直接返回(将立即运行),并将其余的放入P的本地运行队列。这种批处理方法有助于跨处理器负载均衡并减少全局队列锁的竞争。

步骤4稍微复杂一些,因为P的本地运行队列包含两部分:runnext 和 runq。如果 runnext 不为空,返回其中的goroutine。否则,检查 runq 是否有可运行goroutine并将其出队。

步骤6将在“Netpoll工作原理”部分详细描述。

步骤7是过程中最复杂的部分。它尝试最多四次从另一个处理器(称为P1)窃取工作。在前三次尝试中,它仅尝试从P1的 runq 窃取goroutine。如果成功,P1的 runq 中一半的goroutine被转移到当前处理器P的 runq。在最后一次尝试中,它首先尝试从P1的 runnext 槽位窃取(如果可用),然后再回退到P1的 runq

注意,findRunnable 不仅查找可运行goroutine,还在步骤1发生前唤醒之前进入睡眠的goroutine。一旦goroutine被唤醒,它将被放入执行它的处理器P的本地运行队列,等待被某个线程M拾取并执行。

如果在步骤9之后仍未找到goroutine,线程M会在 netpoll 上等待,直到最近的 timer 过期——例如当goroutine从睡眠中唤醒时(因为在Go中睡眠内部会创建一个定时器)。为什么netpoll与定时器有关?这是因为Go的定时器系统严重依赖netpoll。

在netpoll返回后,M重新进入 调度循环 再次搜索可运行goroutine。findRunnable 的前述两种行为允许Go调度器唤醒沉睡的goroutine,使程序能够继续执行。它们解释了为什么包括主goroutine在内的每个goroutine在入睡后都有机会运行。

如果P没有 timer,其对应的线程M将变为idle。P被放入idle列表,M通过调用 stopm 函数进入睡眠。它保持睡眠直到另一个M1线程唤醒它,通常是在创建新goroutine时。一旦被唤醒,M重新进入 调度循环 以搜索并执行可运行goroutine。

findRunnable 的前述两种行为,使得 Go 调度器能够唤醒处于休眠状态的 Goroutine,从而保证程序继续执行。这也解释了为什么所有的 Goroutine(包括 main Goroutine)在休眠之后,都能重新获得运行的机会。至于下面这段 Go 程序具体是如何运作的,我们留到下一篇文章中再来探讨。 

package mainimport "time"funcmain() {    go func() {        time.Sleep(time.Second)    }()    time.Sleep(2*time.Second)}

当 P 上没有任何定时器时,与其绑定的线程 M 就会转入空闲状态。此时,P 会被归入空闲列表(idle list),而 M 则通过调用 stopm 函数进入休眠。它会一直沉睡,直到被其他线程(如 M1)唤醒——这种情况通常发生在新协程被创建的时候(具体细节我们在后面的‘唤醒处理器’章节探讨)。一旦苏醒,M 就会重新切入调度循环,继续寻找并执行就绪的 Goroutine。

10. Goroutine抢占

抢占是暂时中断goroutine执行以允许其他goroutine运行的行为,防止goroutine饥饿。Go中有两种抢占类型:

  • 非协作式抢占(Non-cooperative preemption)
    :运行时间过长的goroutine被强制停止。
  • 协作式抢占(Cooperative preemption)
    :goroutine自愿让出其处理器P。

非协作式抢占

让我们通过一个例子来理解非协作式抢占的工作原理。在这个程序中,我们有两个计算斐波那契数的goroutine,这是一个包含CPU密集操作的紧密循环。为了确保一次只能运行一个goroutine,我们在运行程序时使用 GOMAXPROCS 将逻辑处理器的最大数量设置为1:GOMAXPROCS=1 go run main.go

package mainimport (    "runtime"    "time")funcfibonacci(n int) int {    if n <= 1 {        return n    }       previous, current := 01    for i := 2; i <= n; i++ {        previous, current = current, previous+current    }    return current}funcmain() {    go fibonacci(1_000_000_000)    go fibonacci(2_000_000_000)    time.Sleep(3*time.Second)}

因为只有一个处理器P,可能会发生多种情况。第一,两个goroutine都不运行,因为main函数控制了P。第二,一个goroutine运行而另一个饿死。第三,不知何故两个goroutine并发运行——几乎像魔法一样。

幸运的是,Go支持我们通过工具了解调度发生了什么。runtime/trace 包包含一个理解和排查Go程序的强大工具。为了使用它,我们需要在main方法中添加插桩以将trace导出到文件。

func main() {    file, _ := os.Create("trace.out")    _ = trace.Start(file)    defer trace.Stop()    ...}

程序运行完成后,我们使用命令 go tool trace trace.out 可视化trace。在下图中,横轴表示在给定时间哪个goroutine在P上运行。正如预期的那样,由于 GOMAXPROCS=1,只有一个名为“Proc 0”的逻辑处理器P。

程序启动时的 Trace 可视化

通过放大时间线的开始部分,你可以看到进程始于 main.main(main包中的main函数),它运行在主goroutine G1上。几微秒后,仍在Proc 0上,goroutine G10被调度执行fibonacci函数,接管处理器并抢占了G1。

非协作式抢占发生时的 Trace 可视化

通过缩小并稍微向右滚动,可以观察到G10后来被另一个goroutine G9取代,它是下一个运行fibonacci函数的实例。该goroutine也在Proc 0上执行。注意图中的 runtime.asyncPreempt:47,我稍后会解释这一点。

从演示中可以得出结论,Go能够抢占CPU绑定的goroutine。但为什么这是可能的?如果一个goroutine持续占用CPU,它如何被抢占?这是一个难题,Go issue tracker上曾有过长期的讨论。这个问题直到Go 1.14才得到解决,当时首次引入了异步抢占。

在Go运行时中,有一个守护进程运行在没有P的专用线程M上,称为 sysmon(即系统监视器)。当sysmon发现一个goroutine使用P超过10ms(Go运行时中的 forcePreemptNS 常量)时,它通过执行 tgkill 系统调用向线程M发送信号,强制抢占正在运行的goroutine。是的,你没看错。根据Linux手册页,tgkill 用于向线程发送信号,而不是杀死线程。信号是 SIGURG

收到SIGURG后,程序的执行转移到信号处理程序,该处理程序在线程初始化时通过调用 initsig 函数注册。注意,信号处理程序可以与goroutine代码或调度器代码并发运行。从主程序到信号处理程序的执行切换由内核触发。

信号传递与处理程序的执行

在信号处理程序中,程序计数器被设置为 asyncPreempt 函数,允许goroutine被挂起并为抢占创造空间。在 asyncPreempt 函数的汇编实现中,它保存goroutine的寄存器并在第47行调用 asyncPreempt2 函数。这就是可视化中出现 runtime.asyncPreempt:47 的原因。

在 asyncPreempt2 中,线程M的goroutine g0将进入 gopreempt_m 以解除goroutine G与M的关联并将G入队到全局运行队列。然后线程继续 调度循环,寻找另一个可运行goroutine并执行它。

由于抢占信号由sysmon触发,但实际抢占直到线程收到抢占信号才发生,因此这种抢占是异步的。这就是为什么goroutine实际上可以运行超过10ms的时间限制,就像示例中的goroutine G9一样。

GMP 模型下的非协作式抢占

早期Go中的协作式抢占

在Go的早期,Go运行时本身无法抢占像上面示例那样具有紧密循环的goroutine。作为Go程序员,我们必须通过在循环体中调用 runtime.Gosched() 来告诉goroutine协作地放弃其处理器P。从程序员的角度来看,这非常繁琐且容易出错,并且在实际中确实存在一些性能问题。因此,Go团队决定实现一种巧妙的方法,由运行时本身抢占goroutine。

Go 1.14以来的协作式抢占

你是否想知道为什么我没有在每次迭代中使用 fmt.Printf 并检查终端以查看两个goroutine是否有机会运行?那是因为如果我那样做了,它就会变成协作式抢占,而不是非协作式抢占了。

为了更好地理解这一点,让我们编译程序并分析其汇编代码。由于Go编译器应用了各种优化,可能会使调试更具挑战性,我们需要在构建程序时禁用它们。可以通过 go build -gcflags="all=-N -l" -o fibonacci main.go 完成。

为了便于调试,我使用Delve(Go的强大调试器)反汇编fibonacci函数。你会发现汇编代码中包含栈保护检查。

      main.go:11      0x1023e8890     900b40f9        MOVD 16(R28), R16      main.go:11      0x1023e8894     f1c300d1        SUB $48, RSP, R17      main.go:11      0x1023e8898     3f0210eb        CMP R16, R17      main.go:11      0x1023e889c     090c0054        BLS 96(PC)      ...      main.go:17      0x1023e8910     6078fd97        CALL runtime.convT64(SB)      ...      main.go:17      0x1023e895c     4d78fd97        CALL runtime.convT64(SB)      ...      main.go:20      0x1023e8a18     c0035fd6        RET      main.go:11      0x1023e8a1c     e00700f9        MOVD R0, 8(RSP)      main.go:11      0x1023e8a20     e3031eaa        MOVD R30, R3      main.go:11      0x1023e8a24     dbe7fe97        CALL runtime.morestack_noctxt(SB)      main.go:11      0x1023e8a28     e00740f9        MOVD 8(RSP), R0      main.go:11      0x1023e8a2c     99ffff17        JMP main.fibonacci(SB)

MOVD 16(R28), R16 加载寄存器R28偏移16处的值,R28持有goroutine数据结构 g,并将该值存储在寄存器R16中。加载的值是 stackguard0 字段,它充当当前goroutine的栈保护。

栈保护是放置在栈末尾的特殊值。当栈指针达到此值时,Go运行时检测到栈几乎已满并需要增长。SUB $48, RSP, R17 加载goroutine的栈指针并减去48。CMP R16, R17 比较栈保护和栈指针,如果栈指针小于或等于栈保护,BLS 96(PC) 分支到程序中 ahead 96条指令的位置。

为什么是小于或等于(≤)而不是大于或等于(≥)?因为栈向下增长,栈指针总是大于栈保护。

你有没有想过为什么这些指令不出现在Go代码中但仍然出现在汇编中?这是因为在编译时,Go编译器自动在函数 序言(prologue) 中插入这些指令。这适用于每个函数,如 fmt.Println,而不仅仅是我们的fibonacci。

前进96条指令后,执行到达 MOVD R0, 8(RSP) 指令,然后继续 CALL runtime.morestack_noctxt(SB)runtime.morestack_noctxt 函数最终会调用 newstack 来增长栈,并可选地进入 gopreempt_m 触发抢占。

协作式抢占的关键点是进入 gopreempt_m 的条件,即 stackguard0 == stackPreempt。这意味着每当goroutine想要扩展其栈时,如果其 stackguard0 之前被设置为 stackPreempt,它将被抢占。

stackPreempt 可以由sysmon设置(如果goroutine运行超过10ms)。如果goroutine进行函数调用,它将被协作式抢占;或者被线程的信号处理程序非协作式抢占,以先发生者为准。它也可以在goroutine进入或退出系统调用时或在垃圾收集的跟踪阶段设置。

重新运行程序(确保设置了 GOMAXPROCS=1)并查看trace:

协作式抢占发生时的 Trace 可视化

你可以清楚地看到goroutine仅在几十微秒后就放弃了逻辑处理器——不像非协作式抢占那样可能保留超过10毫秒。值得注意的是,G9的栈跟踪在循环体内的 fmt.Printf 处结束,展示了函数序言中的栈保护检查。这精确地说明了协作式抢占,即goroutine自愿让出处理器。

GMP 模型下的协作式抢占

11. 处理系统调用

系统调用是内核提供的服务,用户空间应用程序通过API访问。这些服务包括基本操作,例如读取文件、建立连接或分配内存。在Go中,你很少需要直接与系统调用交互,因为标准库提供了简化这些任务的高级抽象。然而,理解系统调用的工作原理对于深入了解Go运行时、标准库内部以及性能优化至关重要。

系统调用分类

在Go运行时中,内核系统调用有两个包装函数:RawSyscall 和 Syscall。我们编写的Go代码使用这些函数调用系统调用。

Syscall 通常用于持续时间不可预测的操作,如读取文件或写入HTTP响应。由于这些操作的持续时间是不确定的,Go运行时需要考虑它们以确保资源的有效利用。该函数协调goroutine G、线程M和处理器P,允许Go运行时在阻塞系统调用期间保持性能和响应能力。

然而,并非所有系统调用都是不可预测的。例如,获取进程ID或获取当前时间通常是快速且一致的。对于这些类型的操作,使用 RawSyscall。由于不涉及调度,当进行原始系统调用时,goroutine G、线程M和处理器P之间的关联保持不变。

在内部,Syscall 委托给 RawSyscall 执行实际的系统调用,但用额外的调度逻辑包装它。

funcSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) {    runtime_entersyscall()    r1, r2, err = RawSyscall6(trap, a1, a2, a3, 000)    runtime_exitsyscall()}

Syscall过程中的调度

调度逻辑分别在 runtime_entersyscall 函数和 runtime_exitsyscall 函数中实现,位于实际系统调用之前和之后。在底层,这些函数实际上是 runtime.entersyscall 和 runtime.exitsyscall

在实际系统调用之前,Go运行时记录调用goroutine不再使用CPU。goroutine G从 running 状态转换为 syscall 状态,其栈指针、程序计数器和帧指针被保存以便稍后恢复。线程M和处理器P之间的关联随后被暂时分离,P转换为 syscall 状态。

有趣的是,sysmon 不仅监视运行goroutine代码的处理器(P处于 running 状态),还监视正在进行系统调用的处理器(P处于 syscall 状态)。如果P保持 syscall 状态超过10ms,而不是非协作式抢占运行中的goroutine,会发生 处理器移交(processor handoff)。这保持了goroutine G和线程M之间的关联,并将另一个线程M1附加到此P,允许可运行goroutine在该M1线程上运行。显然,由于P现在正在执行代码,其状态是 running 而不是之前的 syscall

注意,即使系统调用仍在进行中且无论sysmon是否抢占了P,goroutine G和线程M之间的关联仍然存在。为什么?因为Go程序(包括Go运行时和我们编写的Go代码)只是用户空间进程。内核提供给用户空间进程的唯一执行手段是线程。线程有责任运行Go运行时代码、用户Go代码并进行系统调用。线程M代表某个goroutine G进行系统调用,这就是为什么它们之间的关联保持不变。

因此,即使P被sysmon抢占,M仍然保持阻塞,等待系统调用完成,然后才能调用 runtime.exitsyscall 函数。另一个重要点是,每当处理器P处于syscall状态时,除非sysmon抢占它或系统调用完成,否则它不能被另一个线程M获取以执行代码。因此,如果有多个系统调用同时发生,程序(不包括系统调用)不会取得任何进展。这就是为什么Dgraph数据库将 GOMAXPROCS 硬编码为128以“允许更多磁盘I/O调用被调度”。

如 runtime.exitsyscall 所述,系统调用完成后调度器可以采取两条路径:快路径和慢路径。后者仅在前者不可能发生时才发生。

快路径 发生在有处理器P可用于执行刚完成系统调用的goroutine G时。这个P可以是之前执行G的那个(如果它仍处于 syscall 状态,即未被sysmon抢占),或者是任何其他当前处于 idle 状态的处理器P1——以先找到的为准。在快路径退出之前,G从 syscall 状态转换为 running 状态。

当 sysmon 未接管处理器 P 时的系统调用快速路径

当 sysmon 接管处理器 P 时的系统调用快速路径

在 慢路径 中,调度器再次尝试检索任何空闲处理器P。如果找到,goroutine G被调度在该P上运行。否则,G被入队到全局运行队列,关联的线程M被 stopm 函数停止,等待被唤醒以继续 调度循环

12. 网络I/O与文件I/O

调查显示75%的Go用例是Web服务,45%是静态网站。这并非巧合,Go被设计为对I/O操作高效,以解决臭名昭著的C10K问题。

HTTP服务器底层原理

在Go中,启动HTTP服务器非常简单。

package mainimport "net/http"funcmain() {    http.HandleFunc("/"func(w http.ResponseWriter, r *http.Request) {        w.WriteHeader(200)    })    http.ListenAndServe(":80"nil)}

像 http.ListenAndServe() 和 http.HandleFunc() 这样的函数看起来极其简单,但在底层,它们抽象了大量低级网络复杂性。Go依赖许多基本的socket操作来管理网络通信。

流式套接字(Stream Sockets)常用系统调用概述

具体来说,http.ListenAndServe() 利用 socket()bind()listen()accept() 系统调用创建TCP socket,本质上是文件描述符。它将TCP监听socket绑定到指定地址和端口,监听传入连接,并创建新的已连接socket来处理客户端请求。这一切都无需你编写socket处理代码。

同样,http.HandleFunc() 注册你的处理函数,抽象了诸如使用 read() 系统调用读取数据和 write() 系统调用向网络socket写入数据等低级细节。

Go 对系统调用进行了抽象,从而为 HTTP 服务器提供了简洁的接口

然而,HTTP服务器要高效处理数万个并发请求并非那么简单。Go采用了多种技术来实现这一点。

阻塞I/O、非阻塞I/O与I/O多路复用

I/O操作可以是阻塞或非阻塞的。当线程发出阻塞系统调用时,其执行被挂起直到系统调用完成并返回请求的数据。相比之下,非阻塞I/O不挂起线程;相反,如果数据可用则返回请求的数据,如果数据尚未准备好则返回错误(EAGAIN 或 EWOULDBLOCK)。

阻塞I/O实现简单但效率低下,因为它要求应用程序为N个连接生成N个线程。相比之下,非阻塞I/O更复杂,但如果实现正确,可以显著提高资源利用率。

阻塞IO模型

非阻塞IO模型

另一种值得一提的I/O模型是I/O多路复用,其中使用 select 或 poll 系统调用等待一组文件描述符中的一个变为I/O就绪。在此模型中,应用程序阻塞在这些系统调用之一上,而不是实际的I/O系统调用上。

IO多路复用模型

Go中的I/O模型

Go结合使用非阻塞I/O和I/O多路复用来高效处理I/O操作。由于 select 和 poll 的性能限制,Go避免使用它们,转而使用更具可扩展性的替代方案:Linux上的 epoll,Darwin上的 kqueue,以及Windows上的 IOCP

Go引入了 netpoll,这是一个抽象这些替代方案的函数,为不同OS上的I/O多路复用提供统一接口。

13. Netpoll工作原理

使用netpoll需要4个步骤:在内核空间创建epoll实例,向epoll实例注册文件描述符,epoll轮询文件描述符的I/O,以及从epoll实例注销文件描述符。

创建epoll实例并注册Goroutine

当TCP监听器接受连接时,使用 SOCK_NONBLOCK 标志调用 accept4 系统调用,将socket的文件描述符设置为非阻塞模式。随后,创建几个描述符以集成Go运行时的netpoll。

创建 net.netFD 实例以包装socket的文件描述符。当 net.netFD 实例初始化时,调用 epoll_create 系统调用创建epoll实例。epoll实例在 poll_runtime_pollServerInit 函数中初始化,该函数包装在 sync.Once 中以确保只运行一次。因此,Go进程中只存在单个epoll实例,并在进程的整个生命周期中使用。

在 poll_runtime_pollOpen 内部,Go运行时分配一个 runtime.pollDesc 实例,其中包含调度元数据和对涉及I/O的goroutine的引用。然后使用 epoll_ctl 系统调用和 EPOLL_CTL_ADD 操作将socket的文件描述符注册到epoll的兴趣列表中。由于epoll监视文件描述符而不是goroutine,epoll_ctl 还将文件描述符与 runtime.pollDesc 实例关联,允许Go调度器在报告I/O就绪时识别应恢复哪个goroutine。

创建 poll.FD 实例以管理支持轮询的读写操作。它通过 poll.pollDesc 间接持有对 runtime.pollDesc 的引用。

⚠️ Go确实存在单个epoll实例的问题。关于Go应该使用单个还是多个epoll实例,甚至使用另一种I/O多路复用模型如 io_uring,已有讨论。

基于此模型在网络I/O上的成功,Go也利用epoll进行文件I/O。一旦打开文件,调用 syscall.SetNonblock 函数启用文件描述符的非阻塞模式。随后,初始化 poll.FDpoll.pollDesc 和 runtime.pollDesc 实例以将文件描述符注册到epoll的兴趣列表中,允许文件I/O也被多路复用。

Go 语言中描述符(Descriptors)的关联与机制

轮询文件描述符

当goroutine从socket或文件读取时,它最终调用 poll.FD 的 Read 方法。在此方法中,goroutine进行 read 系统调用以从文件描述符获取任何可用数据。如果I/O数据尚未准备好(即返回 EAGAIN 错误),Go运行时调用 poll_runtime_pollWait 方法来 park goroutine

当goroutine写入socket或文件时行为类似,主要区别在于 Read 被 Write 替换,read 系统调用被 write 替换。

现在goroutine处于 waiting 状态,netpoll有责任在goroutine的文件描述符I/O就绪时向Go运行时展示该goroutine,以便它可以被恢复。

在Go运行时中,netpoll不过是一个同名函数。在 netpoll 函数中,使用 epoll_wait 系统调用在指定时间内监视多达128个文件描述符。该系统调用返回之前为每个变为就绪的文件描述符注册的 runtime.pollDesc 实例。最后,netpoll从 runtime.pollDesc 提取goroutine引用并将其移交给Go运行时。

但 netpoll 函数何时被实际调用?当线程寻找可运行goroutine执行时触发,如 调度循环 中所述。根据 findRunnable 函数,只有当当前P的本地运行队列或全局运行队列中没有可用goroutine时,Go运行时才会查询netpoll。这意味着即使其文件描述符I/O就绪,goroutine也不一定立即被唤醒。

如前所述,netpoll可以阻塞指定时间,这由delay参数决定。如果delay为正,它阻塞指定的纳秒数。如果delay为负,它阻塞直到I/O事件就绪。否则,当delay为零时,它立即返回当前就绪的任何I/O事件。在 findRunnable 函数中,delay传递为0,这意味着如果一个goroutine正在等待I/O,另一个goroutine可以被调度在同一内核线程上运行。

注销文件描述符

如上所述,epoll实例监视多达128个文件描述符。因此,当不再需要文件描述符时注销它们很重要,否则某些goroutine可能会饿死。当文件或网络连接不再使用时,我们应该通过调用其Close方法关闭它。

在底层,调用 poll.FD 的 destroy 方法。该方法最终调用Go运行时中的 poll_runtime_pollClose 函数,使用 EPOLL_CTL_DEL 操作执行 epoll_ctl。这将从epoll的兴趣列表中注销文件描述符。

综合解析下图展示了 Go Runtime 中 netpoll 处理文件 I/O 的完整工作流程。网络 I/O 的处理过程与此类似,只是额外增加了一个用于接受连接的 TCP 监听器(Listener)以及关闭连接的环节。为了便于理解,图中省略了诸如 sysmon 和其他空闲处理器 P 等其他组件。

netpoll 在 GMP 模型中是如何工作的

14. 垃圾收集器

你可能知道Go包含一个垃圾收集器(GC)来自动回收未使用对象的内存。然而,如“程序引导”部分所述,当程序启动时,最初没有线程可用于运行GC。那么GC到底在哪里运行?

Go使用跟踪式垃圾收集器,通过从一组根引用开始遍历分配的对象图来识别存活和死亡对象。从根可达的对象被视为存活;不可达的对象被视为死亡并有资格被回收。Go的GC实现了带有弱引用支持的三色标记算法。这种设计允许垃圾收集器与程序并发运行,显著减少STW暂停并提高整体性能。

Go垃圾收集周期可分为4个阶段:

  1. 第一次STW
    :暂停进程,以便所有处理器P进入安全点。
  2. 标记阶段
    :GC goroutine短暂占用处理器P以标记可达对象。
  3. 第二次STW
    :再次暂停进程,允许GC完成标记阶段。
  4. 清除阶段
    :取消暂停进程,并在后台回收不可达对象的内存。

注意,在步骤2中,垃圾收集worker goroutine与常规goroutine在同一处理器P上并发运行。findRunnable 函数不仅查找常规goroutine,还查找GC goroutine(步骤1和2)。

15. 常用函数

获取Goroutine: getg

在Go运行时中,有一个常用函数用于检索当前内核线程中正在运行的goroutine:getg()。看一眼源代码,你会发现这个函数没有实现。这是因为在编译时,编译器将此函数的调用重写为从线程本地存储(TLS)或寄存器获取goroutine的指令。

当前goroutine何时存储在线程本地存储中以便稍后检索?这发生在 gogo 函数中的goroutine上下文切换期间,该函数由 execute 调用。它也发生在调用信号处理程序时,在 sigtrampgo 函数中。

Goroutine Parking: gopark

这是Go运行时中常用的过程,用于将当前goroutine转换为等待状态并调度另一个goroutine运行。

funcgopark(unlockf func(*g, unsafe.Pointer) bool, ...) {    ...    mp.waitunlockf = unlockf    ...    releasem(mp)    ...    mcall(park_m)}

在 releasem 函数内部,goroutine的 stackguard0 被设置为 stackPreempt 以触发最终的协作式抢占。然后控制权转移给属于当前运行goroutine的同一线程的 g0 系统goroutine,以调用 park_m 函数。

在 park_m 内部,goroutine状态设置为 waiting,goroutine与线程M之间的关联被丢弃。此外,gopark 接收一个 unlockf 回调函数,在 park_m 中执行。如果 unlockf 返回false,被park的goroutine立即变为可运行并使用 execute 在同一线程M上重新调度。否则,M进入 调度循环 以选取goroutine并执行。

启动线程: startm

该函数负责调度一个 M(Machine/线程)来执行指定的 P(Processor)。下图展示了该函数的执行流程,其中 M1 是 M2 的父线程。

startm 函数

如果P为nil,它尝试从全局idle列表中检索空闲处理器。如果没有空闲处理器可用,函数直接返回。如果找到空闲处理器(或已提供P),函数要么创建新线程M1(如果没有空闲的),要么唤醒现有的空闲线程来运行P。一旦唤醒,现有线程M继续在 调度循环 中。如果创建了新线程,则通过 clone 系统调用完成,以 mstart 为入口点。mstart 函数随后转入 调度循环,寻找可运行goroutine执行。

停止线程: stopm

此函数将线程M添加到idle列表并使其进入睡眠。stopm 不会返回,直到M被另一个线程唤醒,通常是在创建新goroutine时。这是通过 futex 系统调用实现的,使M在等待时不消耗CPU周期。

处理器移交: handoffp

handoffp 负责将处理器P的所有权从阻塞在系统调用中的线程M转移到另一个线程M1。在某些条件下,P将与M1关联以通过调用 startm 取得进展:如果全局运行队列不为空,如果其本地运行队列不为空,如果有跟踪工作或垃圾收集工作要做,或者如果当前没有线程处理netpoll。如果这些条件都不满足,P被返回到处理器idle列表。

16. Go运行时API

Go运行时提供了几个API与调度器和goroutine交互。它还允许Go程序员根据其应用程序的特定需求调整调度器和其他组件(如垃圾收集器)。

GOMAXPROCS

此函数设置Go运行时中处理器P的数量,从而控制Go程序中的并行级别。GOMAXPROCS 的默认值是 runtime.NumCPU 函数的值,该函数查询Go进程的操作系统CPU分配。

GOMAXPROCS 的默认值可能会有问题,特别是在容器化环境中。目前有一个正在进行的提案,使 GOMAXPROCS 尊重CPU cgroup配额限制,改善其在此类环境中的行为。在未来的Go版本中,GOMAXPROCS 可能会过时。

一些I/O密集型程序可能受益于比默认值更多的处理器P。例如,Dgraph数据库将 GOMAXPROCS 硬编码为128以允许更多I/O操作被调度。

Goexit

此函数优雅地终止当前goroutine。所有deferred调用在终止goroutine之前运行。程序继续执行其他goroutine。如果所有其他goroutine都退出,程序崩溃。Goexit 应用于测试而非实际应用,当你希望提前中止测试用例(例如,如果不满足前置条件)但仍希望运行deferred清理时。

结论

Go调度器是一个强大且高效的系统,通过goroutine实现轻量级并发。在这篇博客中,我们探讨了它的演进——从原始模型到GMP架构——以及关键组件,如goroutine创建、抢占、syscall处理和netpoll集成。

希望这些知识能帮助你编写更高效、更可靠的Go程序。如果你有任何疑问,欢迎留言评论。


参考资料

  1. kelche.co. Go Scheduling.
  2. unskilled.blog. Preemption in Go.
  3. Ian Lance Taylor. What is system stack?
  4. Michael Kerrisk. The Linux Programming Interface.
  5. W. Richard Stevens. Unix Network Programming.
  6. zhuanlan.zhihu.com. Golang program startup process analysis.
  7. Madhav Jivrajani. GopherCon 2021: Queues, Fairness, and The Go Scheduler.
  8. Abraham Silberschatz, Peter B. Galvin, Greg Gagne. Operating System Concepts.
基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-06-06 12:16:47 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/713663.html
  2. 运行时间 : 0.102106s [ 吞吐率:9.79req/s ] 内存消耗:4,752.34kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=d9f003567a16899564c1a758a2053697
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000633s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000935s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000384s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000297s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000541s ]
  6. SELECT * FROM `set` [ RunTime:0.000216s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000500s ]
  8. SELECT * FROM `article` WHERE `id` = 713663 LIMIT 1 [ RunTime:0.002747s ]
  9. UPDATE `article` SET `lasttime` = 1780719407 WHERE `id` = 713663 [ RunTime:0.004651s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000262s ]
  11. SELECT * FROM `article` WHERE `id` < 713663 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000445s ]
  12. SELECT * FROM `article` WHERE `id` > 713663 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000938s ]
  13. SELECT * FROM `article` WHERE `id` < 713663 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000680s ]
  14. SELECT * FROM `article` WHERE `id` < 713663 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.002393s ]
  15. SELECT * FROM `article` WHERE `id` < 713663 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.005579s ]
0.103734s