原来 goim 是这样实现高并发

本章将从架构和程序设计两个方面来阐述goim 高并发的实现原理。

架构

首先从架构来说 goim 分为三层 comet、logic 和 job。

comet 属于接入层,非常容易扩展,直接开启多个 comet 节点,前端接入可以使用 LVS 或者 DNS来转发。

logic 属于无状态的逻辑层,可以随意增加节点,使用 nginx upstream 来扩展 http 接口,内部 rpc 部分,可以使用 LVS 四层转发。

job 用于解耦 comet 和 logic。

系统使用 kafka 作为消息队列,可以通过 kafka 使用多个 broker 或者多个 partition 来扩展队列。使用 redis 作为元数据、节点心跳信息等维护。

 

程序设计

其次在程序设计上,一是尽可能的拆分锁的粒度,来减少资源竞态。二是在内存管理方面,通过申请一个大内存,然后拆成所需的数据类型,自己进行管理,来减少频繁申请与销毁内存操作对性能的损耗。三是充分利用 goroutine 和 channel 实现高并发。四是合理应用缓冲,提供读写性能。

拆分锁的粒度

比如 comet 模块,通过 bucket 来拆分 TCP 链接,每个 TCP 链接根据一定的规则划分到不同的  bucket 中进行管理,而不是集中到单个大而全 bucket中,这样锁的粒度更小,资源竞态几率就更低,性能也能更好的提升,不需要将时间花费的等锁上。

//internal/comet/server.go
//初始化 Server,生成多个 bucket.
func NewServer(c *conf.Config) *Server {
....
	s.buckets = make([]*Bucket, c.Bucket.Size)
	s.bucketIdx = uint32(c.Bucket.Size)
	for i := 0; i < c.Bucket.Size; i++ {//生成多个bucket
		s.buckets[i] = NewBucket(c.Bucket)
	}
	...
}
//根据 subKey 获取 bucket,将不同的 TCP 分配到不同的 bucket 进行管理。 
func (s *Server) Bucket(subKey string) *Bucket {
	idx := cityhash.CityHash32([]byte(subKey), uint32(len(subKey))) % s.bucketIdx
	if conf.Conf.Debug {
		log.Infof("%s hit channel bucket index: %d use cityhash", subKey, idx)
	}
	return s.buckets[idx]
}
/*
 广播消息通过循环 Bukets, 每个 bucket 有自己的锁,通过拆分锁的粒度,来减少锁的竞态,挺高性能。
*/
func (s *server) Broadcast(ctx context.Context, req *pb.BroadcastReq) (*pb.BroadcastReply, error) {
....
	go func() {
		for _, bucket := range s.srv.Buckets() {
			bucket.Broadcast(req.GetProto(), req.ProtoOp)
			if req.Speed > 0 {
				t := bucket.ChannelCount() / int(req.Speed)
				time.Sleep(time.Duration(t) * time.Second)
			}
		}
	}()
	...
}

 

内存管理

在 comet 模块中,在 round(internal/comet/round.go) 中,会一次性申请足够的读缓冲和写缓存以及定时器,通过一个空闲链表中进行维护。每个 TCP 链接需要的时候,从这些空闲链表获得,使用完之后放回去。对于 TCP 读 goroutine 中,每个 TCP 有一个 proto 缓冲(ring),通过环形数组实现。

//internal/comet/server.go
//NewRound 根据配置,提前申请各种数据类型,以备使用。
func NewServer(c *conf.Config) *Server {
	s := &Server{
		c:         c,
		round:     NewRound(c),
		rpcClient: newLogicClient(c.RPCClient),
	}
...
}
//每个 tcp 连接从 round 获取一个 Timer、Reader、Writer
func serveTCP(s *Server, conn *net.TCPConn, r int) {
	var (
		tr = s.round.Timer(r)
		rp = s.round.Reader(r)
		wp = s.round.Writer(r)
...
	)
	s.ServeTCP(conn, rp, wp, tr)
}

//每个 tcp 连接通过 ring(internal/comet/ring.go) 生成一个 proto 类型的环形数组,用于读取数据。
func (s *Server) ServeTCP(conn *net.TCPConn, rp, wp *bytes.Pool, tr *xtime.Timer) {
	...
var (	
		ch      = NewChannel(s.c.Protocol.CliProto, s.c.Protocol.SvrProto) //环形数组
	)
...
//读取数据过程,先从环形数组中获得一个 proto,然后将数据写入 proto
if p, err = ch.CliProto.Set(); err != nil {
	break
}
if err = p.ReadTCP(rr); err != nil {
	break
}
}


//internal/comet/round.go
type Round struct {
	readers []bytes.Pool
	writers []bytes.Pool
	timers  []time.Timer
	options RoundOptions
}

func NewRound(c *conf.Config) (r *Round) {
....
	// reader
	r.readers = make([]bytes.Pool, r.options.Reader) //生成 N 个缓存池
	for i = 0; i < r.options.Reader; i++ { 
r.readers[i].Init(r.options.ReadBuf, r.options.ReadBufSize)
	}
	// writer
	r.writers = make([]bytes.Pool, r.options.Writer)
	for i = 0; i < r.options.Writer; i++ {
		r.writers[i].Init(r.options.WriteBuf, r.options.WriteBufSize)
	}
	// timer
	r.timers = make([]time.Timer, r.options.Timer)
	for i = 0; i < r.options.Timer; i++ {
		r.timers[i].Init(r.options.TimerSize)
	}
	...
}

goroutine 和 channel 实现高并发

比如 comet 对于推送 room 消息, 每个 bucket 将推送通道分成32个,每个通道1024长度。每个通道由一个 goroutine 进行消费处理。推送 room  消息的时候,依次推送到这32个通道中。这样做提高 bucket 内部的并发度,不至于一个通道堵塞,导致全部都在等待。 

//internal/comet/bucket.go
//每个bucket 生成 RoutineAmount 个 Channel, 每个 Channel 由一个 roomproc 处理。
func NewBucket(c *conf.Bucket) (b *Bucket) {
	b.routines = make([]chan *pb.BroadcastRoomReq, c.RoutineAmount)
	for i := uint64(0); i < c.RoutineAmount; i++ {
		c := make(chan *pb.BroadcastRoomReq, c.RoutineSize)
		b.routines[i] = c
		go b.roomproc(c)
	}
	return
}
func (b *Bucket) roomproc(c chan *pb.BroadcastRoomReq) {
	for {
		arg := <-c
		if room := b.Room(arg.RoomID); room != nil {
			room.Push(arg.Proto)
		}
	}
}

//将消息轮询发送到 routines 中。
func (b *Bucket) BroadcastRoom(arg *pb.BroadcastRoomReq) {
	num := atomic.AddUint64(&b.routinesNum, 1) % b.c.RoutineAmount
	b.routines[num] <- arg
}

同时 Job 中也充分利用 goroutine 和 channel,在 Job 中 每个 comet 区分不同的消息推送通道。

1.pushChan:推送单聊消息的通道,分为 N 组,依次将消息推送的 N 组中,每个组有自己的 goroutine, 来提高并发性

2.roomChan:推送群聊消息的通道,分为 N 组,依次将消息推送的 N 组中,每个组有自己的 goroutine, 来提高并发性

3.broadcastChan:广播消息

4.开启 N 个 goroutine, 每个 goroutine,接收单聊、群聊和广播消息。

合理应用缓冲,提供读写性能

在 job 推送 room 消息时,并非在收到消息的时候就推送到 comet,是是通过一定的策略实现批量推送,来提高读写性能。 

对于某个 room 消息而言,会开启一个 goroutine 来处理并开启了写缓冲机制,按批次进行发送(消息个数)。接收到消息之后,不是马上发送,而是进行缓冲,等待一段时间,看看还有没有消息。推送的条件一是已经达到了最大批次数,二是超时。如果长时间没有消息,会销毁这个 room。

具体参考 internal/job/room.go pushproc 实现。

总结

阅读了 goim 源码之后,对与如何设计一个高并发的服务,个人认为主要体现在几个方面,一是拆分职能、可以做到各个模块的扩缩容。拆分粒度,减少竞态和性能损耗。二是更巧妙的方式去使用内存来减少频繁申请和销毁内存的性能损耗。三是充分利用语言特性实现高并发。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值