剛結(jié)束一個項目的開發(fā)葡公,需要做一些壓力測試,這時想起來用GO寫更合適彼水。
接口是一個TCP協(xié)議,沒有web接口极舔,大家知道JVM平臺的線程跟物理線程是一
比一的關(guān)系凤覆,切換線程開銷比較大。
想在一臺PC上模擬比較高的并發(fā)拆魏,其實是有點不切實際盯桦,因為JVM平臺已經(jīng)決定了,要模擬并發(fā)渤刃,只有不斷的往上創(chuàng)建線程拥峦,而GOLang在設(shè)計之初就考慮到這樣的問題,所以它基于協(xié)程的思想來支持多任務(wù)的執(zhí)行卖子。
因為之前沒有用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))
}
第一次優(yōu)化
分解 SimpleTcp 函數(shù),提取三個不同的函數(shù)
- 創(chuàng)建連接的函數(shù)
- 寫入tcp指令的函數(shù)
- 從網(wǎng)絡(luò)中讀取響應(yīng)體的函數(shù)
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) {
//創(chuàng)建連接
conn, err := Conn("localhost:21211")
//確保被關(guān)閉
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)
}
執(zhí)行結(jié)果
start
connected &{{0xc042092000}}
OK
OK<nil>
end
第二次優(yōu)化
加入并行測試
- 添加channel
- 用WaitGroup等待操作執(zhí)行結(jié)束
const COUNT = 500 * 100
const WORKERS = 40
//啟動測試洋闽,創(chuàng)建協(xié)議玄柠,主線程進(jìn)入等待
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)
}
}
//整合處理流程,創(chuàng)建連接讀入指令讀取響應(yīng)
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
}
第三次優(yōu)化
前面的測試都是基于短連接的測試诫舅,連接用完就刪除了羽利,生產(chǎn)環(huán)境用的是長連接,所以后面的測試加入了長連接測試
- 加入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
}
第四次優(yōu)化
上面讀取響應(yīng)的時候用的指定的字節(jié)數(shù)刊懈,而實際情況是每個響應(yīng)都是一行消息这弧。
- 加入bufio娃闲,用于優(yōu)化讀取
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做開發(fā),里面的不少內(nèi)容在學(xué)習(xí)golang時對照著學(xué)習(xí)匾浪,golang提供了更簡潔的api畜吊,還比較容易上手。后面會對golang的一些包深入學(xué)習(xí)户矢。本文里沒有給出嚴(yán)格的壓測標(biāo)準(zhǔn)和輸出玲献,其實就是給自己一個學(xué)習(xí)使用golang的理由,沒有需求驅(qū)動很多知識只是知道不能形成完整的知識結(jié)構(gòu)梯浪,在需求中會有方向捌年,自己需要什么就去找相關(guān)的資料。
目前為止有些go的語法還不是很清楚怎么用挂洛,我想通過一個一個實踐礼预,都會解決的。