百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

优化 Golang 分布式行情推送的性能瓶颈

toyiye 2024-06-21 12:00 8 浏览 0 评论

责编 | 张红月
出品 | 码农桃花源
最近一直在优化行情推送系统,有不少优化心得跟大家分享下。性能方面提升最明显的是时延,在单节点8万客户端时,时延从1500ms优化到40ms,这里是内网mock客户端的得到的压测数据。
对于订阅客户端数没有太执着量级的测试,弱网络下单机8w客户端是没问题的。当前采用的是kubenetes部署方案,可灵活地扩展扩容。

架构图


push-gateway 是推送的网关,有这么几个功能:第一点是为了做鉴权;第二点是为了做接入多协议,我们这里实现了websocket, grpc, grpc-web,sse的支持;第三点是为了实现策略调度及亲和绑定等。
push-server 是推送服务,这里维护了订阅关系及监听mq的新消息,继而推送到网关。

问题一:并发操作map带来的锁竞争及时延


推送的服务需要维护订阅关系,一般是用嵌套的map结构来表示,这样造成map并发竞争下带来的锁竞争和时延高的问题。
// xiaorui.cc {"topic1": {"uuid1": client1, "uuid2": client2}, "topic2": {"uuid3": client3, "uuid4": client4} ... }
已经根据业务拆分了4个map,但是该订阅关系是嵌套的,直接上锁会让其他协程都阻塞,阻塞就会造成时延高。
加锁操作map本应该很快,为什么会阻塞?上面我们有说过该map是用来存topic和客户端列表的订阅关系,当我进行推送时,必然是需要拿到该topic的所有客户端,然后进行一个个的send通知。(这里的send不是io.send,而是chan send,每个客户端都绑定了缓冲的chan)
解决方法:在每个业务里划分256个map和读写锁,这样锁的粒度降低到1/256。除了该方法,开始有尝试过把客户端列表放到一个新的slice里返回,但造成了 GC 的压力,经过测试不可取。
// xiaorui.cc
sync.RWMutexmap[string]map[string]client
改成这样
m *shardMap.shardMap
分段map的库已经推到github[1]了,有兴趣的可以看看。


问题二:串行消息通知改成并发模式


简单说,我们在推送服务维护了某个topic和1w个客户端chan的映射,当从mq收到该topic消息后,再通知给这1w个客户端chan。
客户端的chan本身是有大buffer,另外发送的函数也使用 select default 来避免阻塞。但事实上这样串行发送chan耗时不小。对于channel底层来说,需要goready等待channel的goroutine,推送到runq里。
下面是我写的benchmark[2],可以对比串行和并发的耗时对比。在mac下效果不是太明显,因为mac cpu频率较高,在服务器里效果明显。
串行通知,拿到所有客户端的chan,然后进行send发送。
for _, notifier := range notifiers { s.directSendMesg(notifier, mesg)}
并发send,这里使用协程池来规避morestack的消耗,另外使用sync.waitgroup里实现异步下的等待。
// xiaorui.cc
notifiers := []*mapping.StreamNotifier{}// conv slicefor _, notifier := range notifierMap { notifiers = append(notifiers, notifier)}

// optimize: direct map structtaskChunks := b.splitChunks(notifiers, batchChunkSize)

// concurrent send chanwg := sync.WaitGroup{}for _, chunk := range taskChunks { chunkCopy := chunk // slice replica wg.Add(1) b.SubmitBlock( func() { for _, notifier := range chunkCopy { b.directSendMesg(notifier, mesg) } wg.Done() }, )}wg.Wait()
按线上的监控表现来看,时延从200ms降到30ms。这里可以做一个更深入的优化,对于少于5000的客户端,可直接串行调用,反之可并发调用。

问题三:过多的定时器造成cpu开销加大


行情推送里有大量的心跳检测,及任务时间控速,这些都依赖于定时器。go在1.9之后把单个timerproc改成多个timerproc,减少了锁竞争,但四叉堆数据结构的时间复杂度依旧复杂,高精度引起的树和锁的操作也依然频繁。
所以,这里改用时间轮解决上述的问题。数据结构改用简单的循环数组和map,时间的精度弱化到秒的级别,业务上对于时间差是可以接受的。
Golang时间轮的代码已经推到github[3]了,时间轮很多方法都兼容了golang time原生库。有兴趣的可以看下。

问题四:多协程读写chan会出现send closed panic的问题

解决的方法很简单,就是不要直接使用channel,而是封装一个触发器,当客户端关闭时,不主动去close chan,而是关闭触发器里的ctx,然后直接删除topic跟触发器的映射。
// xiaorui.cc
// 触发器的结构type StreamNotifier struct { Guid string Queue chan interface{}

closed int32 ctx context.Context cancel context.CancelFunc}

func (sc *StreamNotifier) IsClosed() bool { if sc.ctx.Err() == nil { return false } return true}
...


问题五:提高grpc的吞吐性能


grpc是基于http2协议来实现的,http2本身实现流的多路复用。通常来说,内网的两个节点使用单连接就可以跑满网络带宽,无性能问题。但在golang里实现的grpc会有各种锁竞争的问题。
如何优化?多开grpc客户端,规避锁竞争的冲突概率。测试下来qps提升很明显,从8w可以提到20w左右。
可参考以前写过的grpc性能测试[4]。

问题六:减少协程数量


有朋友认为等待事件的协程多了无所谓,只是占内存,协程拿不到调度,不会对runtime性能产生消耗。这个说法是错误的。虽然拿不到调度,看起来只是占内存,但是会对 GC 有很大的开销。所以,不要开太多的空闲的协程,比如协程池开的很大。
在推送的架构里,push-gateway到push-server不仅几个连接就可以,且几十个stream就可以。我们自己实现大量消息在十几个stream里跑,然后调度通知。在golang grpc streaming的实现里,每个streaming请求都需要一个协程去等待事件。所以,共享stream通道也能减少协程的数量。


问题七:GC 问题


对于频繁创建的结构体采用sync.Pool进行缓存。有些业务的缓存先前使用list链表来存储,在不断更新新数据时,会不断的创建新对象,对 GC 造成影响,所以改用可复用的循环数组来实现热缓存。

后记

有坑不怕,填上就可以了。
参考资料
  • https://github.com/rfyiamcool/ccmap/blob/master/syncmap.go

  • https://github.com/rfyiamcool/go-benchmark/tree/master/batch_notify_channel

  • https://github.com/rfyiamcool/go-timewheel

  • https://github.com/rfyiamcool/grpc_batch_test

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码