PGO应用框架即"Pinguo GO application framework",是Camera360广告服务端团队研发的一款简单、高性能、组件化的GO应用框架。受益于GO语言高性能与原生协程,业务从php+yii2升级到PGO后,线上表现单机处理能力提高10倍。PGO吸收了php-yii2/php-msf/go-gin等框架的设计思想,平衡了开发效率和运行性能,使用PGO可快速地开发出高性能的web应用程序。
参考文档:pgo-docs
应用示例:pgo-demo
主要测试PGO框架与php-yii2,php-msf,go-gin的性能差异。
说明:
- 测试机为4核8G虚拟机
- php版本为7.1.24, 开启opcache
- go版本为1.11.2, GOMAXPROCS=4
- swoole版本1.9.21, worker_num=4, reactor_num=2
- 输出均为字符串{"code": 200, "message": "success","data": "hello world"}
- 命令: ab -n 1000000 -c 100 -k 'http://target-ip:8000/welcome'
分类 | QPS | 平均响应时间(ms) | CPU |
---|---|---|---|
php-yii2 | 2715 | 36.601 | 72% |
php-msf | 20053 | 4.575 | 73% |
go-gin | 41798 | 2.339 | 55% |
go-pgo | 33902 | 2.842 | 64% |
结论:
- pgo相比yii2性能提升10+倍, 对低于php7的版本性能还要翻倍。
- pgo相比msf性能提升70%, 相较于msf的yield模拟的协程,pgo协程理解和使用更简单。
- pgo相比gin性能降低19%, 但pgo内置多种常用组件,工程化做得更好,使用方式类似yii2和msf。
- GO 1.10+
- Make 3.8+
- Linux/MacOS/Cygwin
- Glide 0.13+ (建议)
- GoLand 2018 (建议)
规范:
- 一个项目为一个独立的目录,不使用GO全局工作空间。
- 项目的GOPATH为项目根目录,不要依赖系统的GOPATH。
- 除GO标准库外,所有外部依赖代码放入"src/vendor"。
- 项目源码文件与目录使用大写驼峰(CamelCase)形式。
<project>
├── bin/ # 编译程序目录
├── conf/ # 配置文件目录
│ ├── production/ # 环境配置目录
│ │ ├── app.yaml
│ │ └── params.yaml
│ ├── testing/
│ ├── app.yaml # 项目配置文件
│ └── params.yaml # 自定义配置文件
├── makefile # 编译打包
├── runtime/ # 运行时目录
├── public/ # 静态资源目录
├── view/ # 视图模板目录
└── src/ # 项目源码目录
├── Command/ # 命令行控制器目录
├── Controller/ # HTTP控制器目录
├── Lib/ # 项目基础库目录
├── Main/ # 项目入口目录
├── Model/ # 模型目录(数据交互)
├── Service/ # 服务目录(业务逻辑)
├── Struct/ # 结构目录(数据定义)
├── Test/ # 测试目录
├── vendor/ # 第三方依赖目录
├── glide.lock # 项目依赖锁文件
└── glide.yaml # 项目依赖配置文件
建议使用glide做为依赖管理工具(类似php的composer),不使用go官方的dep工具
安装(mac):brew install glide
使用(调用目录为项目的src目录):
glide init # 初始化项目
glide get <pkg> # 下载pkg并添加依赖
--all-dependencies # 下载pkg的所有依赖
glide get <pkg>#v1.2 # 下载指定版本的pkg
glide install # 根据lock文件下载依赖
glide update # 更新依赖包
-
拷贝makefile
非IDE环境(命令行)下,推荐使用make做为编译打包的控制工具,从pgo或pgo-demo将makefile复制到项目目录下。
make start # 编译并运行当前工程 make stop # 停止当前工程的进程 make build # 仅编译当前工程 make update # 更新glide依赖(递归更新) make install # 安装glide.lock文件锁定的依赖包 make pgo # 安装pgo框架到当前工程 make init # 初始化工程目录 make help # 输出帮助信息
-
创建项目目录(以下三种方法均可)
- 执行
make init
创建目录 - 参见《项目目录》手动创建
- 从pgo-demo克隆目录结构
- 执行
-
修改配置文件(conf/app.yaml)
name: "pgo-demo" GOMAXPROCS: 2 runtimePath: "@app/runtime" publicPath: "@app/public" viewPath: "@app/view" server: httpAddr: "0.0.0.0:8000" readTimeout: "30s" writeTimeout: "30s" components: log: levels: "ALL" targets: info: class: "@pgo/FileTarget" levels: "DEBUG,INFO,NOTICE" filePath: "@runtime/info.log" error: class: "@pgo/FileTarget" levels: "WARN,ERROR,FATAL" filePath: "@runtime/error.log" console: class: "@pgo/ConsoleTarget" levels: "ALL"
-
安装PGO(以下两种方法均可)
- 在项目根目录执行
make pgo
安装PGO - 在项目根目录执行
export GOPATH=$GOPATH:$(pwd) && cd src && glide get github.com/pinguo/pgo && glide update
- 在项目根目录执行
-
创建Service(src/Service/Welcome.go)
package Service import ( "fmt" "github.com/pinguo/pgo" ) type Welcome struct { pgo.Object } // 框架自动调用的构造函数(可选) func (w *Welcome) Construct() { fmt.Printf("call in Service/Welcome.Construct\n") } // 框架自动调用的初始函数(可选) func (w *Welcome) Init() { fmt.Printf("call in Service/Welcome.Init\n") } func (w *Welcome) SayHello(name string, age int, sex string) { fmt.Printf("call in Service/Welcome.SayHello, name:%s age:%d sex:%s\n", name, age, sex) }
-
注册Service(src/Service/Init.go)
package Service import "github.com/pinguo/pgo" func init() { container := pgo.App.GetContainer() // 注册类 container.Bind(&Welcome{}) // 除控制器目录外,其它包的init函数中应该只注册该包的类, // 而不应该包含子包。 }
-
创建控制器(src/Controller/WelcomeController.go)
package Controller import ( "Service" "net/http" "github.com/pinguo/pgo" ) type WelcomeController struct { pgo.Controller } // 默认动作为index, 通过/welcome或/welcome/index调用 func (w *WelcomeController) ActionIndex() { w.OutputJson("hello world", http.StatusOK) } // URL路由动作,根据url自动映射控制器及方法,不需要配置. // url的最后一段为动作名称,不存在则为index, // url的其余部分为控制器名称,不存在则为index, // 例如:/welcome/say-hello,控制器类名为 // Controller/WelcomeController 动作方法名为ActionSayHello func (w *WelcomeController) ActionSayHello() { ctx := w.GetContext() // 获取PGO请求上下文件 // 验证参数,提供参数名和默认值,当不提供默认值时,表明该参数为必选参数。 // 详细验证方法参见Validate.go name := ctx.ValidateParam("name").Min(5).Max(50).Do() // 验证GET/POST参数(string),为空或验证失败时panic age := ctx.ValidateQuery("age", 20).Int().Min(1).Max(100).Do() // 只验证GET参数(int),为空或失败时返回20 ip := ctx.ValidatePost("ip", "").IPv4().Do() // 只验证POST参数(string), 为空或失败时返回空字符串 // 打印日志 ctx.Info("request from welcome, name:%s, age:%d, ip:%s", name, age, ip) ctx.PushLog("clientIp", ctx.GetClientIp()) // 生成clientIp=xxxxx在pushlog中 // 调用业务逻辑,一个请求生命周期内的对象都要通过GetObject()获取, // 这样可自动查找注册的类,并注入请求上下文(Context)到对象中。 svc := w.GetObject("Service/Welcome").(*Service.Welcome) // 添加耗时到profile日志中 ctx.ProfileStart("Welcome.SayHello") svc.SayHello(name, age, ip) ctx.ProfileStop("Welcome.SayHello") data := pgo.Map{ "name": name, "age": age, "ip": ip, } // 输出json数据 w.OutputJson(data, http.StatusOK) } // 正则路由动作,需要配置Router组件(components.router.rules) // 规则中捕获的参数通过动作函数参数传递,没有则为空字符串. // eg. "^/reg/eg/(\\w+)/(\\w+)$ => /welcome/regexp-example" func (w *WelcomeController) ActionRegexpExample(p1, p2 string) { data := pgo.Map{"p1": p1, "p2": p2} w.OutputJson(data, http.StatusOK) } // RESTFULL动作,url中没有指定动作名,使用请求方法作为动作的名称(需要大写) // 例如:GET方法请求ActionGET(), POST方法请求ActionPOST() func (w *WelcomeController) ActionGET() { w.GetContext().End(http.StatusOK, []byte("call restfull GET")) }
-
注册控制器(src/Controller/Init.go)
package Controller import "github.com/pinguo/pgo" func init() { container := pgo.App.GetContainer() container.Bind(&WelcomeController{}) }
-
创建程序入口(src/Main/main.go)
package main import ( _ "Controller" // 导入控制器 "github.com/pinguo/pgo" ) func main() { pgo.Run() // 运行程序 }
-
编译运行
make update make start curl http://127.0.0.1:8000/welcome
参见pgo-docs