It event poll

小小网站,记录你的点滴知识!

  1. 检查每个case语句
  2. 如果有任意一个chan是send or recv read,那么就执行该block
  3. 如果多个case是ready的,那么随机找1个并执行该block
  4. 如果都没有ready,那么就block and wait
  5. 如果有default block,而且其他的case都没有ready,就执行该default block

最近被日志是折腾得死去活来,写文件无疑效率是最高的,但是分布式又成问题,虽然稍微折腾一下配合NFS,还是可以搞一搞的,但是始终语言设计没有那么方便。

最终决定用redis,换了redis以为就好了,因为内存运行嘛,谁知道tcp连接开销大得一塌糊涂,服务器负载一下子高了许多,使用netstat -an 查看发现一堆的 TIME_WAIT,连ssh到服务器都巨慢无比,所谓天下武功唯快不破,这么慢80岁老太太跳一支广场舞都能给灭了吧。

既然 tcp连接开销这么大,当然首要任务就是解决连接问题,明显一个请求一次连接是很不靠谱的,还不如直接往硬盘写日志呢,当然写日志第一段也说了,不支持分布式,业务分配没那么好。

那么,能不能先定只用一个连接呢,这显然是不行的,一个连接多个php-fpm互掐也会造成瓶颈,那如果是一个php-fpm一个连接呢?显然这是可取的,于是用了php的redis长连接,php-fpm.ini 配置如下: 

pm = static 
pm.max_children = 400 
pm.max_requests = 10240

php使用 pconnect 来代替 connect

$redis = new redis();
$redis->pconnect('192.168.0.2', 6379); // 内网服务器
$redis->lpush('list', 'Just a test');

上面的配合意思是默认开启400个php-fpm来处理nginx反向代理过来的请求,一个php-fpm处理10240个请求,也就是说,处理10240个请求只需要连接redis一次,显然对于一个请求连接一次redis的开销要小N倍,压测效果也如上所述,从原本的每秒处理几千次请求一下子涨到了1W多次请求。

压测指令也贴上吧,其实还是有许多人不知道的

ab -n 100000 -c 200 要压测的url  // 并发200个客户端请求100000次要压测的url

当然要验证nginx是否真的只开了400个php-fpm,可以用以下代码验证

$pid = getmypid();
touch("pids/".$pid);

执行上面的压测指令,看看pids目录下是不是生成了400个以当前php-fpm进程号为名称的文件,当然不是刚好400个,因为还有一两个是manager进程嘛,呵呵。

压测效果很理想,那是不是问题就解决了呢,当然,PHP没你想的那么美好,举个例子,Mysql的长连接默认是8小时,redis没有了解过,我们就当他也8小时,那8小时一个php-fpm处理10240个请求肯定早就处理完啦,那这个长连接还不关闭,又不能重用,随着时间的推移,连接数不是只增不减,总有一天会内存溢出?

有人可能会说,我测一下10240个请求需要多长时间,redis长连接的超时时间设置接近的数字就好,问题是网络环境哪有想象那么美好,假如网络不好造成10240个请求还没处理完连接超时了呢?加入网络太好处理了好几轮10240次请求连接越来越多了呢?

不过redis好就好在他是单进程单线程IO多路复用的,所以连接一直不关闭也不会有什么大的影响,Mysql是多线程的,线程开多了不关,问题肯定还是比较严重的。

那有没有办法可以弄一个来管理这些长连接的,让他一直不要关闭,用完就给另一个新开的php-fpm,答案就是连接池。

网上照抄一下原理:

    连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等。也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。

按照上面的思想设计一个连接池显然对PHP是莫大的挑战,因为php-fpm之间内存是不共享的,于是我选择了Go语言来干这事,原因很简单,Go的写法和PHP一样简单,java太复杂配置环境也很麻烦,最后被我抛弃了,也不能说抛弃吧,太菜了用不来。

Go连接redis有很多库,最终选择了github.com/garyburd/redigo/redis,然后参考网上的文章写了一个连接池的例子:

package main
import (
    "net/http"
    "runtime"
    "io"
    "fmt"
    "log"
    "time"
    "github.com/garyburd/redigo/redis"
)
// 连接池大小
var MAX_POOL_SIZE = 20
var redisPoll chan redis.Conn
func putRedis(conn redis.Conn) {
    // 基于函数和接口间互不信任原则,这里再判断一次,养成这个好习惯哦
    if redisPoll == nil {
        redisPoll = make(chan redis.Conn, MAX_POOL_SIZE)
    }
    if len(redisPoll) >= MAX_POOL_SIZE {
        conn.Close()
        return
    }
    redisPoll <- conn
}
func InitRedis(network, address string) redis.Conn {
    // 缓冲机制,相当于消息队列
    if len(redisPoll) == 0 {
        // 如果长度为0,就定义一个redis.Conn类型长度为MAX_POOL_SIZE的channel
        redisPoll = make(chan redis.Conn, MAX_POOL_SIZE)
        go func() {
            for i := 0; i < MAX_POOL_SIZE/2; i++ {
                c, err := redis.Dial(network, address)
                if err != nil {
                    panic(err)
                }
                putRedis(c)
            }
        } ()
    }
    return <-redisPoll
}
func redisServer(w http.ResponseWriter, r *http.Request) {
    startTime := time.Now()
    c := InitRedis("tcp", "192.168.0.237:6379")
    dbkey := "netgame:info"
    if ok, err := redis.Bool(c.Do("LPUSH", dbkey, "yanetao")); ok {
    } else {
        log.Print(err)
    }
    msg := fmt.Sprintf("用时:%s", time.Now().Sub(startTime));
    io.WriteString(w, msg+"\n\n");
}
func main() {
    // 利用cpu多核来处理http请求,这个没有用go默认就是单核处理http的,这个压测过了,请一定要相信我
    runtime.GOMAXPROCS(runtime.NumCPU());
    http.HandleFunc("/", redisServer);
    http.ListenAndServe(":9527", nil);
}

上面看似实现了连接池,实际压测效果很不理想,不但请求数低,还报错。

请求数底是因为没有解决连接开销,上面是第一次来的时候开一个go协程去连接20/2就是10次redis,然后放到channel里面去,实际上就是队列了,channel就是消息队列,然后当这10个连接被请求用完了,就又生成10个,实际上tcp连接数一个没少,多少个请求就多少个连接数,只是把连接时间片给移了一下,移给刚好10次连接过后那个倒霉蛋。

错误是因为我内网测试,请求太快了,第一波10个连接还没连接好,第二波又来了,没错,又一大波僵尸,然后几波的goroutine就开始互掐,导致连接错误了

还是没有达到连接池的效果。

连接池的概念是先生成默认的连接,例如40个,那么所有请求过来,都是用这40个连接来处理。

当40个连接被用完的时候,要么排队,要么自增多几个来处理。

这40个连接和新生成的连接,假如生成多5个,那么这45个连接是不会关闭的,用完就放回池里,其实也就是队列里,等待其他请求来使用他,达到连接复用的效果。

也就是说这些连接一定是长连接,一直连着不断开。

上面明显是短连接,我试过放到全局变量,每个连接处理六七个请求之后,就断开了,程序提示连接不可用,go要如何达到长连接的效果呢,最后在老外的文章找到了这个库的实现方式,代码如下:

package main
import (
    "net/http"
    "runtime"
    "io"
    "fmt"
    "log"
    "time"
    "github.com/garyburd/redigo/redis"
)
// 重写生成连接池方法
func newPool() *redis.Pool {
    return &redis.Pool{
        MaxIdle: 80,
        MaxActive: 12000, // max number of connections
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", "192.168.0.2:6379")
            if err != nil {
                panic(err.Error())
            }
            return c, err
        },
    }
}
// 生成连接池
var pool = newPool()
func redisServer(w http.ResponseWriter, r *http.Request) {
    startTime := time.Now()
    // 从连接池里面获得一个连接
    c := pool.Get()
    // 连接完关闭,其实没有关闭,是放回池里,也就是队列里面,等待下一个重用
    defer c.Close()
    dbkey := "netgame:info"
    if ok, err := redis.Bool(c.Do("LPUSH", dbkey, "yangzetao")); ok {
    } else {
        log.Print(err)
    }
    msg := fmt.Sprintf("用时:%s", time.Now().Sub(startTime));
    io.WriteString(w, msg+"\n\n");
}
func main() {
    runtime.GOMAXPROCS(runtime.NumCPU());
    http.HandleFunc("/", redisServer);
    http.ListenAndServe(":9527", nil);
}

压测了一下,上面的代码往redis里面插数据跟只是输出字符串HelloWorld到客户端几乎一样快,检查了一下redis里面netgame:info这个队列数据,一条数据都没丢,这才是我真正要的效果啊,好吧,一个牛逼哄哄的go+redis日志系统真的要诞生了,哇哈哈。

参考:

一个老外问把连接放到全局变量搞不定,当然搞不定,短连接会超时的嘛

官方的实现源码

连接池概念


package main

import (
    "fmt"
)

// 声明数据结构
// 大写变量、方法为公有变量和方法。小写为私有。
type User struct {
    Name    string
    Age     uint
    Address string
    Sex     bool
    Money   float64
}

// 为数据结构添加方法
func (u User) Display() string {
    var sex string
    if u.Sex {
        sex = "boy"
    } else {
        sex = "gril"
    }
    return fmt.Sprintf("Name : %s\nAge : %d\nAddress : %s\nSex : %s\nMoney : %.2f", u.Name, u.Age, u.Address, sex, u.Money)
}

func main() {
    // 使用方式1
    var u1 User
    u1.Name = "user1"
    u1.Money = 10245.236
    fmt.Println(u1.Display())
    fmt.Println("===============================")
    // 使用方式2
    u2 := new(User)
    u2.Name = "小明"
    u2.Money = 235423.2
    fmt.Println(u2.Display())
    fmt.Println("===============================")
    // 使用方式3
    u3 := User{"小花", 23, "上海", false, 23134.234}
    fmt.Println(u3.Display())
    fmt.Println("===============================")
    // 使用方式4
    u4 := User{Name: "小李", Money: 235435.3434, Sex: true}
    fmt.Println(u4.Display())
}

所谓反射,就相当于物理的反射,你通过镜子,可以看到自己的摸样。

函数通过反射,可以获得想要的信息,例如变量的类型和值,对象的属性和方法。

在golang的反射包reflect中,用 TypeOf() 和 ValueOf() 轻松得到对象的类型和值,用 Type() 和 Value() 可以改变反射回来变量的值

下面是一个简单例子:

package main
import (
    "fmt"
    "reflect"
)
type User struct {
    Id int
    Name string
    Age int
    Title string
}
func(this User)Test()string{
    return this.Name
}
func main(){
    // interface可以接受任何类型的参数,如果不用接口,就只能用 := 让go自己去判断类型了
    // o := &User{1,"Tom",12,"nan"}
    var o interface {} = &User{1,"Tom",12,"nan"}
    v := reflect.ValueOf(o)
    fmt.Println(v)
    m := v.MethodByName("Test")
    rets := m.Call([]reflect.Value{})
    fmt.Println(rets)
}

参考:http://blog.sina.com.cn/s/blog_9e14446a01018wzz.html


在golang里面获取时间戳并不难。只要加载time包。然后time.Now().Unix(),就可以了,但接下来转成string就麻烦了

本来,加载strconv的话,用strconv.Itoa也可以解决,但unixtime的时间戳是int64, itoa只能转32位的。这时候就只有一个恶心的办法了。

fmt.Sprintf("%d",int64),这个是肯定可以转,。。。。我现在就是用这种办法的

做个笔记

--EOF--

后记,在群里问了一下,结果asta谢就说了。明明有strconv.FormatInt,用godoc看了一下,居然没看到。可能我的版本旧了。

strconv.Format(int64 , 10) ,后面的参数是2~36,简单就是php的base_convert的go版本。看来,go做tinyurl也是用这个函数了。哈哈

感谢群友们。

---next

自此,go语言的int转换成string有3种方法

1、int32位,strconv.Itoa

2、大于32位,strconv.FormatInt()

3、万恶的fmt.Sprintf...好吧,这个我在php里是经常用来做格式化

转自:http://www.neatstudio.com/show-2428-1.shtml