刚结束一个项目的开发,需要做一些压力测试,这时想起来用GO写更合适。
接口是一个TCP协议,没有web接口,大家知道JVM平台的线程跟物理线程是一
比一的关系,切换线程开销比较大。
想在一台PC上模拟比较高的并发,其实是有点不切实际,因为JVM平台已经决定了,要模拟并发,只有不断的往上创建线程,而GOLang在设计之初就考虑到这样的问题,所以它基于协程的思想来支持多任务的执行。
因为之前没有用GO写过东西,所以从最简单的"Hello Wolrd"小程序 开始:
package main
import (
"fmt"
"net"
)
func SimpleTcp() {
tcpAddr, err := net.ResolveTCPAddr("tcp4", "localhost:21211")
conn, err := net.DialTCP("tcp", nil, tcpAddr)
_, err = conn.Write([]byte("order 123 \r\n"))
buf := make([]byte, 6)
_, err = conn.Read(buf)
_ = err
fmt.Println(string(buf))
}
第一次优化
分解 SimpleTcp 函数,提取三个不同的函数
- 创建连接的函数
- 写入tcp指令的函数
- 从网络中读取响应体的函数
func Conn(ip string) (conn *net.TCPConn, err error) {
tcpAddr, err := net.ResolveTCPAddr("tcp4", ip)
if err != nil {
panic("ResolveTCPAddr error,ip:" + ip)
}
conn, err = net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
panic("DialTCP error,ip:" + ip)
}
return conn, err
}
func Write(conn *net.TCPConn, body string) error {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
_, err := conn.Write([]byte("order 123 \r\n"))
return err
}
func Read(conn *net.TCPConn) (res string, err error) {
buf := make([]byte, 6)
_, err = conn.Read(buf)
fmt.Println(string(buf))
return string(buf), err
}
入口方法
func Simpletest(cmd string) {
//创建连接
conn, err := Conn("localhost:21211")
//确保被关闭
defer func() {
conn.Close()
}()
if err != nil {
fmt.Println(err)
return
} else {
fmt.Println("connected", conn)
}
Write(conn, cmd)
res, err := Read(conn)
fmt.Println(res, err)
}
执行结果
start
connected &{{0xc042092000}}
OK
OK<nil>
end
第二次优化
加入并行测试
- 添加channel
- 用WaitGroup等待操作执行结束
const COUNT = 500 * 100
const WORKERS = 40
//启动测试,创建协议,主线程进入等待
func StartWorker(works int) {
var wg sync.WaitGroup
var c1 = make(chan string, WORKERS)
wg.Add(COUNT)
go func() {
for i := 0; i < COUNT; i++ {
fmt.Println(i)
PushChan(c1, "order 123 \r\n", &wg)
}
}()
for i := 0; i < works; i++ {
go PollChan(c1, &wg)
}
wg.Wait()
}
//投递消息
func PushChan(c1 chan string, cmd string, wg *sync.WaitGroup) {
c1 <- cmd
}
//拉取消息
func PollChan(c1 chan string, wg *sync.WaitGroup) {
select {
case cmd := <-c1:
Dispose(wg, cmd)
//递归获取
PollChan(c1, wg)
}
}
//整合处理流程,创建连接读入指令读取响应
func Dispose(wg *sync.WaitGroup, cmd string) (res string, er error) {
var start = time.Now()
var conn, err = Conn("localhost:21211")
defer func() {
wg.Done()
if conn != nil {
conn.Close()
}
}()
if err != nil {
return "", err
} else {
//fmt.Println("connected", conn)
}
Write(conn, cmd)
res, err = Read(conn)
//fmt.Println("res:", res)
if !strings.Contains(res, "STORED") {
fmt.Println("e:", (time.Now().Sub(start) / 1e9), res)
}
return res, err
}
第三次优化
前面的测试都是基于短连接的测试,连接用完就删除了,生产环境用的是长连接,所以后面的测试加入了长连接测试
- 加入sync.Pool用于保存连接实例
var pool = InitPool(0, "localhost:21211")
func InitPool(poolSize int, ip string) *sync.Pool {
pool := &sync.Pool{New: func() interface{} {
conn, _ := Conn(ip)
return conn
}}
return pool
}
func DisposeWithPool(wg *sync.WaitGroup, cmd string) (res string, err error) {
var start = time.Now()
var conn *net.TCPConn = pool.Get().(*net.TCPConn)
defer func() {
pool.Put(conn)
wg.Done()
}()
Write(conn, cmd)
res, err = Read(conn)
if !strings.Contains(res, "STORED") {
fmt.Println("e:", (time.Now().Sub(start)), res)
}
return res, err
}
第四次优化
上面读取响应的时候用的指定的字节数,而实际情况是每个响应都是一行消息。
- 加入bufio,用于优化读取
func Readx(conn *net.TCPConn) (res string, err error) {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
pReader := bufio.NewReader(conn)
line, _, err := pReader.ReadLine()
return strings.TrimSpace(string(line)), err
}
后记:
一直用java做开发,里面的不少内容在学习golang时对照着学习,golang提供了更简洁的api,还比较容易上手。后面会对golang的一些包深入学习。本文里没有给出严格的压测标准和输出,其实就是给自己一个学习使用golang的理由,没有需求驱动很多知识只是知道不能形成完整的知识结构,在需求中会有方向,自己需要什么就去找相关的资料。
目前为止有些go的语法还不是很清楚怎么用,我想通过一个一个实践,都会解决的。