golang IM架构设计(1)

wangshizebin · 2019-04-24 11:31:35 · 4613 次点击 · 预计阅读时间 6 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-04-24 11:31:35 的文章,其中的信息可能已经有所发展或是发生改变。

Written by 王泽宾

1.1 传输协议的选择

  • 项目现状 目前,常见的IM系统传输报文无外乎使用UDP、TCP以及应用层的HTTP这几种协议。市面上象微信、MSN、陌陌、米聊、环信等大多采用TCP协议,只有QQ比较特殊,采用了UDP协议,应该是历史原因造成的,可能与当时的网络条件和初始资源有关。

  • UDP协议 UDP协议提供了一种不可靠的无连接数据包传输服务。它不提供报文到达的确认、排序、及流量控制等功能。本身设计比较简洁,数据包较小,无需确认等特点,所以传输效率极高,比较适合应于流媒体类型的业务,这些业务对于少量数据包的损失不敏感。但对于IM系统来讲,对数据的完整性要求高、传输有序,直接使用UDP协议就不合适。如要使用就必须在UDP协议基础上再增加校验、重发机制。

  • TCP协议 TCP协议提供了一种可靠的有连接数据包传输服务。它解决了UDP协议对于报文到达的确认、排序、及流量控制等方面的缺陷,能够保证可靠地数据传输。但它的缺点是传输效率要比UDP低,对于服务端大量数据的处理,需要耗费较高的资源,尤其是在长连接情况下,如何保证单机服务器高并发量,如何灵活地进行水平扩展都需要做好设计。

  • HTTP协议 有的IM采用了TCP之上的应用层HTTP协议的传输,但一般对接的是Web客户端。还有的是作为一项辅助功能,提供第三方访问的web接口。

  • 结论 我们的IM产品将采用TCP传输协议收发报文,以后会增加HTTP协议作为开放接口。

1.2 基于TCP协议的长连接测试

1.2.1 编程语言

以下的测试工作完全基于golang代码进行的测试。为什么使用golang而不是c/c++或者java呢?选择golang主要基于以下因素的考虑:

  • golang 内置的协程机制,非常适合于开发高并发系统,协程类似于轻量级的线程,对系统资源消耗极少,使用过程比线程简单。golang还为协程之间的通讯专门设计了channel,以及封装了各种锁和其他同步工具,简化了并发系统开发的难度。golang丰富的内置类型,函数多返回值,defer机制等新的语言特色,也极大地方便快速开发。golang的开发速度、编程难度远低于c/c++。
  • golang不是一种解释性语言,它根据不同的操作系统直接编译为二进制运行代码,所以它的运行效率比java,python等解释性语言要高,但比c/c++二进制代码的运行效率要略低。 基于以上两点的权衡,我们选择了golang语言进行系统开发。

1.2.2 测试说明

对于IMServer来讲,首先满足单服务器能够抗住足够多的并发的Tcp长连接。golang 底层通讯模型,windows默认使用了IOCP机制,linux默认使用了epoll模型,均能抗住高并发。

IMClient根据日常使用场景,设计了每1分钟发送500个字节数据,后接收server推送的500个字节数据,以此循环进行,并保持长连接。IMServer接收数据,并原样回送数据。测试环境中,Server使用了一台低配PC,2核CPU,8G内存,操作系统为centos7/Windows。

每个客户端开启了6000个连接,为了模拟更多的并发,同时开启了8-10个客户端,模拟5万长连接用户使用IM服务,进行压力测试。

当所有连接正常开启以后,用telnet单独连接服务器,发送数据和回送数据依然正常,说明在当前并发情况下,系统正常运行,符合预期。

单台低配服务器达到5万已经足够,无需接受更多的长连接。如果用户超过5万长连接,可以采用水平扩展方式,加入更多的服务器,进行负载均衡即可。

单机在linux统计长连接总数: 在这里插入图片描述 加载到5w长连接后,使用telnet连接服务器,查看服务情况,依然正常接受请求,回送数据:

在这里插入图片描述 在这里插入图片描述

客户端代码 im_client.go

package main

import (
    "fmt"
    "net"
    "time"
    "strconv"
)

const (
    //根据自身情况修改服务器ip地址
    serverAddr = "192.168.1.36:120"
)

func main() {
    succCount := 0
    failCount := 0
    for i:=0; i<6000; i++ {
        conn, err := net.Dial("tcp", serverAddr)
        if err != nil {
            failCount++
            fmt.Println("Number of dialing failures:", failCount)
            time.Sleep(time.Second)
            continue
        }
        defer conn.Close()

        fmt.Println("Number of successful connections:", succCount )

        go Client(conn, succCount )

        succCount ++
    }

    //Stop here to prevent the program from exiting
    tick := time.NewTicker(10 * time.Second)
    for {
        select {
            case <-tick.C:
        }    
    }
}

func Client(conn net.Conn, index int) error {
    dataRead := make([]byte, 512)
    dataWrite := strconv.Itoa(index) + 
    "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\n" +
    "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\n" +
    "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\n" +
    "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\n" +
    "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\n"

        for {
            conn.Write([]byte(dataWrite))
            _, err := conn.Read(dataRead)
            if err != nil {
                return err
            }
            time.Sleep(60*time.Second)
    }
    return nil
}

linux下运行前,必须重新设置可打开的最大文件句柄数,默认为1024,也就是最大只能接受1024个连接数。我们设置为65330:

在这里插入图片描述

服务器端代码 im_server.go

package main

import (
    "net"
    "log"
    "time"
)

const (
    SERVER_IP = ""
    SERVER_PORT = "120"
)

type Server struct {

}

func (server *Server) Start() {

    serverAddr := SERVER_IP + ":" + SERVER_PORT
    listener, err := net.Listen("tcp", serverAddr)
    if err != nil {
        log.Println("Failed to execute function net.Listen: ", err.Error())
        return
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
                log.Println("Continue accepting connection because of an temporary error: ", err.Error())
                time.Sleep(1 *time.Second)
                continue
            }

            log.Println("Failed to execute function listener.Accept, exit app because of an error: ", err.Error())
            return
        }

        sess := &Session {
            Conn: conn,
        }
        go sess.Serve()
    }
}

func main() {
    (&Server{}).Start()
}

服务器端代码 im_session.go

package main

import (
    "bufio"
    "net"
    "log"
    "fmt"
)

type Session struct {
    Conn    net.Conn
    Reader    *bufio.Reader
    Writer    *bufio.Writer
}

func (session *Session) Serve() {
    defer session.Close()

    session.Reader = bufio.NewReader(session.Conn)
    session.Writer = bufio.NewWriter(session.Conn)

    for {
        byteData, err := session.Reader.ReadSlice('\n')
        if err != nil {
            log.Println("Error occured in funciton Session.Serve: ",err.Error() )
            return
        }

        line := string(byteData)
        fmt.Println(line)
        session.Send(line)
    }
}

func (session *Session) Close() {
    session.Conn.Close()
}

func (session *Session) write(data string) error {
    if _, err := fmt.Fprint(session.Writer, data); err != nil {
        return err
    }
    return session.Writer.Flush()
}

func (session *Session) Send(data string) error {
    return session.write(data)
}

1.3 后续工作

  • GC调优 golang采用垃圾收集器(GC)自动调度内存垃圾回收工作,目前1.12版本已经做得非常优秀了,垃圾回收过程卡顿不明显,但是作为server需要承载大流量的冲击,我们必须得充分考虑减少GC的次数,降低在堆上反复进行内存的分配和回收,减少内存的使用量。这些工作需要配合golang的调优工具,以及读懂golang的部分源代码。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

4613 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传