Archive for 12月, 2015

golang recover() 的一个坑,记录一下。

今天碰到了一个 recover() 的一个坑。

有一个操作预期可能出现 panic 而且不需要关心它是否panic.就建立了一个匿名函数将操作包裹起来并 追加了 defer recover。 原来都是 defer func(){_=recover()}(),这次省掉了匿名函数,直接 defer recover() 。

 
package main

func main(){
    defer recover()
    panic("123")
}
/*

panic: 123

goroutine 1 [running]:
main.main()
        D:/golang/src/github.com/gamexg/go-test/鍩烘湰璇硶/recover娴嬭瘯/1/1.g         
o:5 +0x97
exit status 2
*/

结果如上,直接挂了。recover 无效…

查看了 recover 的文档:recover 只能放到 defer 函数里面,不能放到子函数。实测直接 defer recover() 也不行。recover 是内置函数,源码需要到go的实现里面查,懒得去查了。

go 的很多设计感觉都是黑魔法,就像这里,如果 panic 是一个协程本地变量保存的,那么在什么地方调用 recover 都行。但是看情况不是,恐怕和函数调用搅到一起了。

v:=dict[key]、 v,ok:=dict[key] 这个设计也是使用的黑魔法,感觉是一开始只设计了第一种实现,但是由于需要获得是否存在key,就另加了第二个实现,很生硬的感觉。

chan 实现也比较奇怪,连接之类的关闭都是 conn.Close() 实现关闭,chan 却是 close(chan),设计并不统一,很别扭。

Comments (1) »

完整的golang 多协程+信道 任务处理示例

有几个地方需要注意:for i + 协程时如果协程使用 i ,那么需要增加 i:=i 来防止多协程冲突;实际执行任务时需要用一个函数包起来,防止单个任务panic造成整个程序崩溃。

 
package main
import (
    "sync"
    "fmt"
)

/*
一个标准的协程+信道实现

*/

func main() {

    taskChan := make(chan int)
    TCount := 10
    var wg sync.WaitGroup //创建一个sync.WaitGroup

    // 产生任务
    go func() {
        for i := 0; i < 1000; i++ {
            taskChan <- i
        }
        // 全部任务都输入后关闭信道,告诉工作者进程没有新任务了。
        close(taskChan)
    }()

    // 告诉 WaitGroup 有 TCount 个执行者。
    wg.Add(TCount)
    // 启动 TCount 个协程执行任务
    for i := 0; i < TCount; i++ {

        // 注意:如果协程内使用了 i,必须有这一步,或者选择通过参数传递进协程。
        // 否则 i 会被 for 所在的协程修改,协程实际使用时值并不确定。
        i := i

        go func() {

            // 协程结束时报告当前协程执行完毕。
            defer func() { wg.Done() }()

            fmt.Printf("工作者 %v 启动...\r\n", i)

            for task := range taskChan {

                // 建立匿名函数执行任务的目的是为了捕获单个任务崩溃,防止造成整个工作者、系统崩溃。
                func() {

                    defer func() {
                        err := recover()
                        if err != nil {
                            fmt.Printf("任务失败:工作者i=%v, task=%v, err=%v\r\n", i, task, err)
                        }
                    }()

                    // 故意崩溃,看看是不是会造成整个系统崩溃。
                    if task%100==0{
                        panic("故意崩溃啦")
                    }

                    // 这里的 task 并不需要通过参数传递进来。
                    // 原因是这里是同步执行的,并不会被其它协程修改。
                    fmt.Printf("任务结果=%v ,工作者id=%v, task=%v\r\n",task*task,i,task)
                }()
            }

            fmt.Printf("工作者 %v 结束。\r\n", i)
        }()
    }

    //等待所有任务完成
    wg.Wait()
    print("全部任务结束")
}

Comments (2) »

socket.io-python-emitter 发送消息 socket.io-redis 无法收到的问题。

之前将服务迁移到 docker ,结果发现 socket.io-python-emitter 发送的消息全部发送不出去。经检查发现是 socket.io-redis 0.2.0 做了性能优化,与老版本不兼容了,无奈查源码自己打了个补丁(现已合并至主干)。

最开始是发现网站发起的推送延迟很厉害,而且推送 ping 日志显示丢包达到了80%以上。由于所有推送操作同时使用了极光推送及自建的 socket.io 推送。一开始没想到两套推送方案全部挂了,以为是测试设备出了问题,并且测试时设备日志记录了一次极光推送被强制关闭的log。同时发现极光推送控制台日志显示设备不在线(后来发现近期极光推送非常不稳定,这种延迟是常规状态了,由于原来自建推送可用, 所以没有注意)。

但是经检查发现设备没有问题,而且设备状态上报部分同样使用的 socket.io,也很正常。所以又测试了一下python向网页端推送,发现还是收不到推送,确认python发出的推送出现故障了。

无奈进 redis 数据库查看推送是否进入了数据库,结果发现python发出的推送消息的确进入了redis数据库,但是和socket.io本身发出消息的频道名称不一致。找了一下,没有当时的记录了,大体是执行的redis-cli命令,通过 PSUBSCRIBE * 订阅所有频道。

于是觉得可能是频道名字的问题,所以仿着 socket.io 消息的格式通过 PUBLISH 频道名称 消息 发了个消息,无效,并且命令返回了1,只有手工PSUBSCRIBE * 的订阅,也就是 socket.io 过呢本没有接收消息。觉得是 socket.io 有问题,岁查看代码,发现socket.io-redis 会根据房间名订阅消息,并不会订阅 #emitter 频道的消息,就很纳闷之前的系统为什么能工作。检查了下修订历史,发现是 0.1.4 -> 0.2.0 为了性能只订阅相关房间的消息,看了一下发现 python 的代码是2年前了,和当前的实现不兼容…

无奈只能自己修改了,中间又发现新版本同时还修改了包格式,不过都问题不大。

这次修bug理清了大部分 socket.io-redis 的流程。

socket.io-redis 接收消息是通过订阅 redis 上面相关的频道来实现的,会检查 uid 看看是不是本机发出的,如果不是就转到 socket.io 处理。不过这里有个问题,uid 是随机生成的6位字符串,也就是有可能碰到相同的uid,socket.io-redis 会抛弃相同uid的消息造成相同uid的 socket.io 之间无法通行。

发送消息是通过向 redis 发送消息,然后其他 socket.io-redis 订阅并接收实现的。广播消息会直接发送到 prefix + ‘#’ + packet.nsp + ‘#’ ;发送到指定房间的消息会对涉及到的房间都发送一条 prefix + ‘#’ + packet.nsp + ‘#’ + room + ‘#’ 频道的消息。

No comment »