go语言对象池设计模式 golang对象

golang sync.pool对象复用 并发原理 缓存池 在go http每一次go serve(l)都会构建Request数据结构 。在大量数据请求或高并发的场景中,频繁创建销毁对象,会导致GC压力 。解决办法之一就是使用对象复用技术 。在http协议层之下,使用对象复用技术创建Request数据结构 。在http协议层之上,可以使用对象复用技术创建(w,*r,ctx)数据结构 。这样即可以回快TCP层读包之后的解析速度 , 也可也加快请求处理的速度 。
先上一个测试:
结论是这样的:
貌似使用池化,性能弱爆了???这似乎与net/http使用sync.pool池化Request来优化性能的选择相违背 。这同时也说明了一个问题,好的东西,如果滥用反而造成了性能成倍的下降 。在看过pool原理之后,结合实例,将给出正确的使用方法,并给出预期的效果 。
sync.Pool是一个 协程安全 的 临时对象池。数据结构如下:
local 成员的真实类型是一个 poolLocal 数组,localSize 是数组长度 。这涉及到Pool实现,pool为每个P分配了一个对象,P数量设置为runtime.GOMAXPROCS(0) 。在并发读写时,goroutine绑定的P有对象,先用自己的,没有去偷其它P的 。go语言将数据分散在了各个真正运行的P中,降低了锁竞争 , 提高了并发能力 。
不要习惯性地误认为New是一个关键字 , 这里的New是Pool的一个字段,也是一个闭包名称 。其API:
如果不指定New字段 , 对象池为空时会返回nil,而不是一个新构建的对象 。Get()到的对象是随机的 。
原生sync.Pool的问题是,Pool中的对象会被GC清理掉,这使得sync.Pool只适合做简单地对象池,不适合作连接池 。
pool创建时不能指定大?。挥惺肯拗?。pool中对象会被GC清掉,只存在于两次GC之间 。实现是pool的init方法注册了一个poolCleanup()函数,这个方法在GC之前执行,清空pool中的所有缓存对象 。
为使多协程使用同一个POOL 。最基本的想法就是每个协程,加锁去操作共享的POOL , 这显然是低效的 。而进一步改进,类似于ConcurrentHashMap(JDK7)的分Segment,提高其并发性可以一定程度性缓解 。
注意到pool中的对象是无差异性的,加锁或者分段加锁都不是较好的做法 。go的做法是为每一个绑定协程的P都分配一个子池 。每个子池又分为私有池和共享列表 。共享列表是分别存放在各个P之上的共享区域,而不是各个P共享的一块内存 。协程拿自己P里的子池对象不需要加锁,拿共享列表中的就需要加锁了 。
Get对象过程:
Put过程:
如何解决Get最坏情况遍历所有P才获取得对象呢:
方法1止前sync.pool并没有这样的设置 。方法2由于goroutine被分配到哪个P由调度器调度不可控,无法确保其平衡 。
由于不可控的GC导致生命周期过短 , 且池大小不可控,因而不适合作连接池 。仅适用于增加对象重用机率,减少GC负担 。2
【go语言对象池设计模式 golang对象】 执行结果:
单线程情况下,遍历其它无元素的P,长时间加锁性能低下 。启用协程改善 。
结果:
测试场景在goroutines远大于GOMAXPROCS情况下 , 与非池化性能差异巨大 。
测试结果
可以看到同样使用*sync.pool,较大池大小的命中率较高,性能远高于空池 。
结论:pool在一定的使用条件下提高并发性能 , 条件1是协程数远大于GOMAXPROCS,条件2是池中对象远大于GOMAXPROCS 。归结成一个原因就是使对象在各个P中均匀分布 。
池pool和缓存cache的区别 。池的意思是,池内对象是可以互换的,不关心具体值,甚至不需要区分是新建的还是从池中拿出的 。缓存指的是KV映射,缓存里的值互不相同 , 清除机制更为复杂 。缓存清除算法如LRU、LIRS缓存算法 。
池空间回收的几种方式 。一些是GC前回收 , 一些是基于时钟或弱引用回收 。最终确定在GC时回收Pool内对象,即不回避GC 。用java的GC解释弱引用 。GC的四种引用:强引用、弱引用、软引用、虚引用 。虚引用即没有引用,弱引用GC但有空间则保留,软引用GC即清除 。ThreadLocal的值为弱引用的例子 。
regexp 包为了保证并发时使用同一个正则,而维护了一组状态机 。
fmt包做字串拼接,从sync.pool拿[]byte对象 。避免频繁构建再GC效率高很多 。
golang sync.Pool的用法及实现正如sycn.Pool的名字所示 , 这是go中实现的一个对象池,为什么要有这个池呢?首先go是自带垃圾回收机制(也就是通常所说的gc) 。gc会带来运行时的开销,对于高频的内存申请与释放,如果将不用的对象存放在一个池子中,用的时候从池子中取出一个对象,用完了再还回去,这样就能减轻gc的压力 。
对于池这个概念,之前可能听说过连接池 。能否用sync.Pool实现一个连接池呢?答案是不能的 。因为对于sync.Pool而言,我们无法保证每次放回去再取出来的对象是与之前一致的,对象的内存存在着呗销毁的可能 。因此 , 这个sync.Pool的存在仅仅是为了减缓gc的压力而生的 。
定义sync.Pool的时候只需要设置一个New成员,它是一个函数,类型为func() interface{},当池子中没有空闲的对象时就会调用New函数生成一个 。由于pool中对象的数量不可控,因此并没有传递任何与对象数量有关的参数 。
然后 , 调用调用Get函数就可以取出一个对象,调用Put函数就可以将对象归还到池子中 。
Go语言——goroutine并发模型参考:
Goroutine并发调度模型深度解析手撸一个协程池
Golang 的 goroutine 是如何实现的?
Golang - 调度剖析【第二部分】
OS线程初始栈为2MB 。Go语言中,每个goroutine采用动态扩容方式,初始2KB,按需增长,最大1G 。此外GC会收缩栈空间 。
BTW,增长扩容都是有代价的,需要copy数据到新的stack,所以初始2KB可能有些性能问题 。
更多关于stack的内容,可以参见大佬的文章 。聊一聊goroutine stack
用户线程的调度以及生命周期管理都是用户层面,Go语言自己实现的 , 不借助OS系统调用,减少系统资源消耗 。
Go语言采用两级线程模型,即用户线程与内核线程KSE(kernel scheduling entity)是M:N的 。最终goroutine还是会交给OS线程执行,但是需要一个中介,提供上下文 。这就是G-M-P模型
Go调度器有两个不同的运行队列:
go1.10\src\runtime\runtime2.go
Go调度器根据事件进行上下文切换 。
调度的目的就是防止M堵塞,空闲 , 系统进程切换 。
详见Golang - 调度剖析【第二部分】
Linux可以通过epoll实现网络调用,统称网络轮询器N(Net Poller) 。
文件IO操作
上面都是防止M堵塞,任务窃取是防止M空闲
每个M都有一个特殊的G,g0 。用于执行调度,gc,栈管理等任务,所以g0的栈称为调度栈 。g0的栈不会自动增长,不会被gc,来自os线程的栈 。
go1.10\src\runtime\proc.go
G没办法自己运行,必须通过M运行
M通过通过调度,执行G
从M挂载P的runq中找到G,执行G
如何实现支持数亿用户的长连消息系统此文是根据周洋在【高可用架构群】中的分享内容整理而成go语言对象池设计模式,转发请注明出处 。周洋,360手机助手技术经理及架构师,负责360长连接消息系统,360手机助手架构的开发与维护 。不知道咱们群名什么时候改为“Python高可用架构群”go语言对象池设计模式了,所以不得不说,很荣幸能在接下来的一个小时里在Python群里讨论golang....360消息系统介绍360消息系统更确切的说是长连接push系统,目前服务于360内部多个产品 , 开发平台数千款app,也支持部分聊天业务场景,单通道多app复用,支持上行数据,提供接入方不同粒度的上行数据和用户状态回调服务 。目前整个系统按不同业务分成9个功能完整的集群,部署在多个idc上(每个集群覆盖不同的idc),实时在线数亿量级 。通常情况下 , pc , 手机,甚至是智能硬件上的360产品的push消息,基本上是从go语言对象池设计模式我们系统发出的 。关于push系统对比与性能指标的讨论很多同行比较关心go语言在实现push系统上的性能问题,单机性能究竟如何,能否和其他语言实现的类似系统做对比么go语言对象池设计模式?甚至问如果是创业,第三方云推送平台,推荐哪个?其实各大厂都有类似的push系统,市场上也有类似功能的云服务 。包括我们公司早期也有erlang,nodejs实现的类似系统 , 也一度被公司要求做类似的对比测试 。我感觉在讨论对比数据的时候,很难保证大家环境和需求的统一 , 我只能说下我这里的体会 , 数据是有的,但这个数据前面估计会有很多定语~第一个重要指标go语言对象池设计模式:单机的连接数指标做过长连接的同行,应该有体会,如果在稳定连接情况下,连接数这个指标 , 在没有网络吞吐情况下对比,其实意义往往不大,维持连接消耗cpu资源很小,每条连接tcp协议栈会占约4k的内存开销,系统参数调整后,我们单机测试数据 , 最高也是可以达到单实例300w长连接 。但做更高的测试,我个人感觉意义不大 。因为实际网络环境下,单实例300w长连接,从理论上算压力就很大:实际弱网络环境下,移动客户端的断线率很高,假设每秒有1000分之一的用户断线重连 。300w长连接,每秒新建连接达到3w,这同时连入的3w用户,要进行注册,加载离线存储等对内rpc调用 , 另外300w长连接的用户心跳需要维持,假设心跳300s一次,心跳包每秒需要1w tps 。单播和多播数据的转发,广播数据的转发,本身也要响应内部的rpc调用,300w长连接情况下,gc带来的压力,内部接口的响应延迟能否稳定保障 。这些集中在一个实例中,可用性是一个挑战 。所以线上单实例不会hold很高的长连接,实际情况也要根据接入客户端网络状况来决定 。第二个重要指标:消息系统的内存使用量指标这一点上,使用go语言情况下,由于协程的原因,会有一部分额外开销 。但是要做两个推送系统的对比,也有些需要确定问题 。比如系统从设计上是否需要全双工(即读写是否需要同时进行)如果半双工,理论上对一个用户的连接只需要使用一个协程即可(这种情况下,对用户的断线检测可能会有延时),如果是全双工 , 那读/写各一个协程 。两种场景内存开销是有区别的 。另外测试数据的大小往往决定我们对连接上设置的读写buffer是多大,是全局复用的,还是每个连接上独享的 , 还是动态申请的 。另外是否全双工也决定buffer怎么开 。不同的策略,可能在不同情况的测试中表现不一样 。第三个重要指标:每秒消息下发量这一点上 , 也要看我们对消息到达的QoS级别(回复ack策略区别),另外看架构策略,每种策略有其更适用的场景,是纯粹推?还是推拉结合?甚至是否开启了消息日志?日志库的实现机制、以及缓冲开多大?flush策略……这些都影响整个系统的吞吐量 。另外为了HA,增加了内部通信成本,为了避免一些小概率事件 , 提供闪断补偿策略,这些都要考虑进去 。如果所有的都去掉,那就是比较基础库的性能了 。所以我只能给出大概数据,24核,64G的服务器上 , 在QoS为message at least,纯粹推,消息体256B~1kB情况下,单个实例100w实际用户(200w )协程,峰值可以达到2~5w的QPS...内存可以稳定在25G左右,gc时间在200~800ms左右(还有优化空间) 。我们正常线上单实例用户控制在80w以内,单机最多两个实例 。事实上,整个系统在推送的需求上,对高峰的输出不是提速,往往是进行限速,以防push系统瞬时的高吞吐量 , 转化成对接入方业务服务器的ddos攻击所以对于性能上,我感觉大家可以放心使用,至少在我们这个量级上,经受过考验 , go1.5到来后,确实有之前投资又增值了的感觉 。消息系统架构介绍下面是对消息系统的大概介绍,之前一些同学可能在gopher china上可以看到分享,这里简单讲解下架构和各个组件功能 , 额外补充一些当时遗漏的信息:架构图如下,所有的service都 written by golang.几个大概重要组件介绍如下:dispatcher service根据客户端请求信息,将应网络和区域的长连接服务器的 , 一组IP传送给客户端 。客户端根据返回的IP,建立长连接,连接Room service.room Service,长连接网关,hold用户连接,并将用户注册进register service,本身也做一些接入安全策略、白名单、IP限制等 。register service是我们全局session存储组件,存储和索引用户的相关信息,以供获取和查询 。coordinator service用来转发用户的上行数据 , 包括接入方订阅的用户状态信息的回调,另外做需要协调各个组件的异步操作,比如kick用户操作,需要从register拿出其他用户做异步操作.saver service是存储访问层,承担了对redis和mysql的操作,另外也提供部分业务逻辑相关的内存缓存,比如广播信息的加载可以在saver中进行缓存 。另外一些策略 , 比如客户端sdk由于被恶意或者意外修改,每次加载了消息,不回复ack,那服务端就不会删除消息,消息就会被反复加载,形成死循环,可以通过在saver中做策略和判断 。(客户端总是不可信的) 。center service提供给接入方的内部api服务器 , 比如单播或者广播接口 , 状态查询接口等一系列api,包括运维和管理的api 。举两个常见例子 , 了解工作机制:比如发一条单播给一个用户,center先请求Register获取这个用户之前注册的连接通道标识、room实例地址,通过room service下发给长连接 Center Service比较重的工作如全网广播,需要把所有的任务分解成一系列的子任务 , 分发给所有center,然后在所有的子任务里,分别获取在线和离线的所有用户,再批量推到Room Service 。通常整个集群在那一瞬间压力很大 。deployd/agent service用于部署管理各个进程,收集各组件的状态和信息,zookeeper和keeper用于整个系统的配置文件管理和简单调度关于推送的服务端架构常见的推送模型有长轮训拉取,服务端直接推送(360消息系统目前主要是这种) , 推拉结合(推送只发通知,推送后根据通知去拉取消息).拉取的方式不说了,现在并不常用了,早期很多是nginx lua redis,长轮训,主要问题是开销比较大 , 时效性也不好,能做的优化策略不多 。直接推送的系统,目前就是360消息系统这种 , 消息类型是消耗型的,并且对于同一个用户并不允许重复消耗,如果需要多终端重复消耗,需要抽象成不同用户 。推的好处是实时性好,开销小 , 直接将消息下发给客户端,不需要客户端走从接入层到存储层主动拉取.但纯推送模型,有个很大问题,由于系统是异步的 , 他的时序性无法精确保证 。这对于push需求来说是够用的,但如果复用推送系统做im类型通信,可能并不合适 。对于严格要求时序性,消息可以重复消耗的系统 , 目前也都是走推拉结合的模型,就是只使用我们的推送系统发通知,并附带id等给客户端做拉取的判断策略 , 客户端根据推送的key,主动从业务服务器拉取消息 。并且当主从同步延迟的时候,跟进推送的key做延迟拉取策略 。同时也可以通过消息本身的QoS , 做纯粹的推送策略,比如一些“正在打字的”低优先级消息,不需要主动拉取了,通过推送直接消耗掉 。哪些因素决定推送系统的效果?首先是sdk的完善程度,sdk策略和细节完善度,往往决定了弱网络环境下最终推送质量.SDK选路策略,最基本的一些策略如下:有些开源服务可能会针对用户hash一个该接入区域的固定ip , 实际上在国内环境下不可行,最好分配器(dispatcher)是返回散列的一组,而且端口也要参开 , 必要时候,客户端告知是retry多组都连不上,返回不同idc的服务器 。因为我们会经常检测到一些case,同一地区的不同用户,可能对同一idc内的不同ip连通性都不一样,也出现过同一ip不同端口连通性不同,所以用户的选路策略一定要灵活,策略要足够完善.另外在选路过程中,客户端要对不同网络情况下的长连接ip做缓存,当网络环境切换时候(wifi、2G、3G) , 重新请求分配器,缓存不同网络环境的长连接ip 。客户端对于数据心跳和读写超时设置,完善断线检测重连机制针对不同网络环境,或者客户端本身消息的活跃程度,心跳要自适应的进行调整并与服务端协商,来保证链路的连通性 。并且在弱网络环境下 , 除了网络切换(wifi切3G)或者读写出错情况,什么时候重新建立链路也是一个问题 。客户端发出的ping包,不同网络下,多久没有得到响应,认为网络出现问题,重新建立链路需要有个权衡 。另外对于不同网络环境下 , 读取不同的消息长度,也要有不同的容忍时间,不能一刀切 。好的心跳和读写超时设置 , 可以让客户端最快的检测到网络问题,重新建立链路,同时在网络抖动情况下也能完成大数据传输 。结合服务端做策略另外系统可能结合服务端做一些特殊的策略,比如我们在选路时候,我们会将同一个用户尽量映射到同一个room service实例上 。断线时,客户端尽量对上次连接成功的地址进行重试 。主要是方便服务端做闪断情况下策略 , 会暂存用户闪断时实例上的信息,重新连入的 时候,做单实例内的迁移,减少延时与加载开销.客户端保活策略很多创业公司愿意重新搭建一套push系统,确实不难实现,其实在协议完备情况下(最简单就是客户端不回ack不清数据),服务端会保证消息是不丢的 。但问题是为什么在消息有效期内,到达率上不去?往往因为自己app的push service存活能力不高 。选用云平台或者大厂的,往往sdk会做一些保活策略 , 比如和其他app共生,互相唤醒,这也是云平台的push service更有保障原因 。我相信很多云平台旗下的sdk , 多个使用同样sdk的app,为了实现服务存活,是可以互相唤醒和保证活跃的 。另外现在push sdk本身是单连接,多app复用的,这为sdk实现,增加了新的挑战 。综上 , 对我来说,选择推送平台,优先会考虑客户端sdk的完善程度 。对于服务端,选择条件稍微简单,要求部署接入点(IDC)越要多,配合精细的选路策略 , 效果越有保证,至于想知道哪些云服务有多少点,这个群里来自各地的小伙伴们,可以合伙测测 。go语言开发问题与解决方案下面讲下,go开发过程中遇到挑战和优化策略,给大家看下当年的一张图,在第一版优化方案上线前一天截图~可以看到 , 内存最高占用69G,GC时间单实例最高时候高达3~6s.这种情况下,试想一次悲剧的请求,经过了几个正在执行gc的组件,后果必然是超时... gc照成的接入方重试 , 又加重了系统的负担 。遇到这种情况当时整个系统最差情况每隔2 , 3天就需要重启一次~当时出现问题,现在总结起来,大概以下几点1.散落在协程里的I/O,Buffer和对象不复用 。当时(12年)由于对go的gc效率理解有限,比较奔放,程序里大量short live的协程,对内通信的很多io操作,由于不想阻塞主循环逻辑或者需要及时响应的逻辑,通过单独go协程来实现异步 。这回会gc带来很多负担 。针对这个问题,应尽量控制协程创建,对于长连接这种应用,本身已经有几百万并发协程情况下,很多情况没必要在各个并发协程内部做异步io,因为程序的并行度是有限 , 理论上做协程内做阻塞操作是没问题 。如果有些需要异步执行,比如如果不异步执行,影响对用户心跳或者等待response无法响应,最好通过一个任务池,和一组常驻协程,来消耗,处理结果,通过channel再传回调用方 。使用任务池还有额外的好处 , 可以对请求进行打包处理,提高吞吐量,并且可以加入控量策略.2.网络环境不好引起激增go协程相比较以往高并发程序,如果做不好流控,会引起协程数量激增 。早期的时候也会发现,时不时有部分主机内存会远远大于其他服务器,但发现时候,所有主要profiling参数都正常了 。后来发现,通信较多系统中,网络抖动阻塞是不可免的(即使是内网),对外不停accept接受新请求,但执行过程中,由于对内通信阻塞,大量协程被 创建 , 业务协程等待通信结果没有释放,往往瞬时会迎来协程暴涨 。但这些内存在系统稳定后,virt和res都并没能彻底释放,下降后,维持高位 。处理这种情况,需要增加一些流控策略,流控策略可以选择在rpc库来做,或者上面说的任务池来做,其实我感觉放在任务池里做更合理些,毕竟rpc通信库可以做读写数据的限流,但它并不清楚具体的限流策略,到底是重试还是日志还是缓存到指定队列 。任务池本身就是业务逻辑相关的,它清楚针对不同的接口需要的流控限制策略 。3.低效和开销大的rpc框架早期rpc通信框架比较简单,对内通信时候使用的也是短连接 。这本来短连接开销和性能瓶颈超出我们预期,短连接io效率是低一些,但端口资源够,本身吞吐可以满足需要,用是没问题的,很多分层的系统,也有http短连接对内进行请求的但早期go版本,这样写程序,在一定量级情况,是支撑不住的 。短连接大量临时对象和临时buffer创建,在本已经百万协程的程序中,是无法承受的 。所以后续我们对我们的rpc框架作了两次调整 。第二版的rpc框架 , 使用了连接池 , 通过长连接对内进行通信(复用的资源包括client和server的:编解码Buffer、Request/response) , 大大改善了性能 。但这种在一次request和response还是占用连接的,如果网络状况ok情况下,这不是问题,足够满足需要了,但试想一个room实例要与后面的数百个的register,coordinator,saver,center,keeper实例进行通信,需要建立大量的常驻连接,每个目标机几十个连接,也有数千个连接被占用 。非持续抖动时候(持续逗开多少无解) , 或者有延迟较高的请求时候 , 如果针对目标ip连接开少了,会有瞬时大量请求阻塞,连接无法得到充分利用 。第三版增加了Pipeline操作,Pipeline会带来一些额外的开销,利用tcp的全双特性,以尽量少的连接完成对各个服务集群的rpc调用 。4.Gc时间过长Go的Gc仍旧在持续改善中,大量对象和buffer创建,仍旧会给gc带来很大负担,尤其一个占用了25G左右的程序 。之前go team的大咖邮件也告知我们,未来会让使用协程的成本更低,理论上不需要在应用层做更多的策略来缓解gc.改善方式,一种是多实例的拆分 , 如果公司没有端口限制,可以很快部署大量实例,减少gc时长,最直接方法 。不过对于360来说,外网通常只能使用80和433 。因此常规上只能开启两个实例 。当然很多人给我建议能否使用SO_REUSEPORT,不过我们内核版本确实比较低,并没有实践过 。另外能否模仿nginx,fork多个进程监控同样端口,至少我们目前没有这样做,主要对于我们目前进程管理上,还是独立的运行的,对外监听不同端口程序,还有配套的内部通信和管理端口,实例管理和升级上要做调整 。解决gc的另两个手段,是内存池和对象池,不过最好做仔细评估和测试,内存池、对象池使用,也需要对于代码可读性与整体效率进行权衡 。这种程序一定情况下会降低并行度,因为用池内资源一定要加互斥锁或者原子操作做CAS,通常原子操作实测要更快一些 。CAS可以理解为可操作的更细行为粒度的锁(可以做更多CAS策略,放弃运行,防止忙等) 。这种方式带来的问题是,程序的可读性会越来越像C语言,每次要malloc,各地方用完后要free,对于对象池free之前要reset,我曾经在应用层尝试做了一个分层次结构的“无锁队列”上图左边的数组实际上是一个列表,这个列表按大小将内存分块,然后使用atomic操作进行CAS 。但实际要看测试数据了,池技术可以明显减少临时对象和内存的申请和释放,gc时间会减少,但加锁带来的并行度的降低,是否能给一段时间内的整体吞吐量带来提升,要做测试和权衡…在我们消息系统,实际上后续去除了部分这种黑科技,试想在百万个协程里面做自旋操作申请复用的buffer和对象,开销会很大,尤其在协程对线程多对多模型情况下,更依赖于golang本身调度策略,除非我对池增加更多的策略处理,减少忙等 , 感觉是在把runtime做的事情,在应用层非常不优雅的实现 。普遍使用开销理论就大于收益 。但对于rpc库或者codec库,任务池内部,这些开定量协程,集中处理数据的区域,可以尝试改造~对于有些固定对象复用,比如固定的心跳包什么的,可以考虑使用全局一些对象,进行复用,针对应用层数据 , 具体设计对象池 , 在部分环节去复用 , 可能比这种无差别的设计一个通用池更能进行效果评估.消息系统的运维及测试下面介绍消息系统的架构迭代和一些迭代经验,由于之前在其他地方有过分享,后面的会给出相关链接,下面实际做个简单介绍,感兴趣可以去链接里面看架构迭代~根据业务和集群的拆分,能解决部分灰度部署上线测试,减少点对点通信和广播通信不同产品的相互影响,针对特定的功能做独立的优化.消息系统架构和集群拆分,最基本的是拆分多实例,其次是按照业务类型对资源占用情况分类 , 按用户接入网络和对idc布点要求分类(目前没有条件,所有的产品都部署到全部idc)系统的测试go语言在并发测试上有独特优势 。对于压力测试,目前主要针对指定的服务器,选定线上空闲的服务器做长连接压测 。然后结合可视化,分析压测过程中的系统状态 。但压测早期用的比较多,但实现的统计报表功能和我理想有一定差距 。我觉得最近出的golang开源产品都符合这种场景,go写网络并发程序给大家带来的便利,让大家把以往为了降低复杂度,拆解或者分层协作的组件,又组合在了一起 。QAQ1:协议栈大小,超时时间定制原则?移动网络下超时时间按产品需求通常2g,3G情况下是5分钟,wifi情况下5~8分钟 。但对于个别场景,要求响应非常迅速的场景,如果连接idle超过1分钟 , 都会有ping,pong,来校验是否断线检测,尽快做到重新连接 。Q2:消息是否持久化?消息持久化,通常是先存后发,存储用的redis,但落地用的mysql 。mysql只做故障恢复使用 。Q3:消息风暴怎么解决的?如果是发送情况下,普通产品是不需要限速的,对于较大产品是有发送队列做控速度,按人数,按秒进行控速度发放,发送成功再发送下一条 。Q4:golang的工具链支持怎么样?我自己写过一些小程序千把行之内 , 确实很不错,但不知道代码量上去之后 , 配套的debug工具和profiling工具如何,我看上边有分享说golang自带的profiling工具还不错,那debug呢怎么样呢,官方一直没有出debug工具,gdb支持也不完善,不知你们用的什么?是这样的,我们正常就是println,我感觉基本上可以定位我所有问题,但也不排除由于并行性通过println无法复现的问题,目前来看只能靠经验了 。只要常见并发尝试,经过分析是可以找到的 。go很快会推出调试工具的~Q5:协议栈是基于tcp吗?是否有协议拓展功能?协议栈是tcp,整个系统tcp长连接 , 没有考虑扩展其功能~如果有好的经验,可以分享~Q6:问个问题,这个系统是接收上行数据的吧,系统接收上行数据后是转发给相应系统做处理么,是怎么转发呢,如果需要给客户端返回调用结果又是怎么处理呢?系统上行数据是根据协议头进行转发 , 协议头里面标记了产品和转发类型 , 在coordinator里面跟进产品和转发类型,回调用户,如果用户需要阻塞等待回复才能后续操作,那通过再发送消息,路由回用户 。因为整个系统是全异步的 。Q7:问个pushsdk的问题 。pushsdk的单连接 , 多app复用方式 , 这样的情况下以下几个问题是如何解决的:1)系统流量统计会把所有流量都算到启动连接的应用吧?而启动应用的连接是不固定的吧?2)同一个pushsdk在不同的应用中的版本号可能不一样 , 这样暴露出来的接口可能有版本问题,如果用单连接模式怎么解决?流量只能算在启动的app上了,但一般这种安装率很高的app承担可能性大,常用app本身被检测和杀死可能性较少,另外消息下发量是有严格控制 的 。整体上用户还是省电和省流量的 。我们pushsdk尽量向上兼容,出于这个目的,push sdk本身做的工作非常有限,抽象出来一些常见的功能,纯推的系统,客户端策略目前做的很少,也有这个原因 。Q8:生产系统的profiling是一直打开的么?不是一直打开,每个集群都有采样,但需要开启哪个可以后台控制 。这个profling是通过接口调用 。Q9:面前系统中的消息消费者可不可以分组?类似于Kafka 。客户端可以订阅不同产品的消息,接受不同的分组 。接入的时候进行bind或者unbind操作Q10:为什么放弃erlang,而选择go,有什么特别原因吗?我们现在用的erlang?erlang没有问题,原因是我们上线后 , 其他团队才做出来,经过qa一个部门对比测试,在没有显著性能提升下,选择继续使用go版本的push,作为公司基础服务 。Q11:流控问题有排查过网卡配置导致的idle问题吗?流控是业务级别的流控,我们上线前对于内网的极限通信量做了测试,后续将请求在rpc库内 , 控制在小于内部通信开销的上限以下.在到达上限前作流控 。Q12:服务的协调调度为什么选择zk有考虑过raft实现吗?golang的raft实现很多啊 , 比如Consul和ectd之类的 。3年前,还没有后两者或者后两者没听过应该 。zk当时公司内部成熟方案,不过目前来看,我们不准备用zk作结合系统的定制开发,准备用自己写的keeper代替zk,完成配置文件自动转数据结构 , 数据结构自动同步指定进程,同时里面可以完成很多自定义的发现和控制策略,客户端包含keeper的sdk就可以实现以上的所有监控数据,profling数据收集 , 配置文件更新,启动关闭等回调 。完全抽象成语keeper通信sdk,keeper之间考虑用raft 。Q13:负载策略是否同时在服务侧与CLIENT侧同时做的 (DISPATCHER 会返回一组IP)?另外,ROOM SERVER/REGISTER SERVER连接状态的一致性可用性如何保证? 服务侧保活有无特别关注的地方? 安全性方面是基于TLS再加上应用层加密?会在server端做,比如重启操作前,会下发指令类型消息 , 让客户端进行主动行为 。部分消息使用了加密策略,自定义的rsa des,另外满足我们安全公司的需要,也定制开发很多安全加密策略 。一致性是通过冷备解决的,早期考虑双写 , 但实时状态双写同步代价太高而且容易有脏数据,比如register挂了,调用所有room,通过重新刷入指定register来解决 。Q14:这个keeper有开源打算吗?还在写,如果没耦合我们系统太多功能,一定会开源的 , 主要这意味着,我们所有的bind在sdk的库也需要开源~Q15:比较好奇lisence是哪个如果开源?
使用Go实现一个数据库连接池开始本文之前go语言对象池设计模式,go语言对象池设计模式我们看一段Go连接数据库的代码:
本文内容我们将解释连接池背后是如何工作的,并 探索 如何配置数据库能改变或优化其性能 。
转自:
整理:地鼠文档:
那么sql.DB连接池是如何工作的呢?
需要理解的最重要一点是,sql.DB池包含两种类型的连接——“正在使用”连接和“空闲”连接 。当您使用连接执行数据库任务(例如执行SQL语句或查询行)时 , 该连接被标记为正在使用,任务完成后,该连接被标记为空闲 。
当您使用Go执行数据库操作时 , 它将首先检查池中是否有可用的空闲连接 。如果有可用的连接,那么Go将重用这个现有连接,并在任务期间将其标记为正在使用 。如果在您需要空闲连接时池中没有空闲连接 , 那么Go将创建一个新的连接 。
当Go重用池中的空闲连接时,与该连接有关的任何问题都会被优雅地处理 。异常连接将在放弃之前自动重试两次,这时Go将从池中删除异常连接并创建一个新的连接来执行该任务 。
连接池有四个方法,我们可以使用它们来配置连接池的行为 。让我们一个一个地来讨论 。
SetMaxOpenConns()方法允许您设置池中“打开”连接(使用中 空闲连接)数量的上限 。默认情况下 , 打开的连接数是无限的 。
一般来说,MaxOpenConns设置得越大,可以并发执行的数据库查询就越多 , 连接池本身成为应用程序中的瓶颈的风险就越低 。
但让它无限并不是最好的选择 。默认情况下,PostgreSQL最多100个打开连接的硬限制,如果达到这个限制的话 , 它将导致pq驱动返回”sorry, too many clients already”错误 。
为go语言对象池设计模式了避免这个错误,将池中打开的连接数量限制在100以下是有意义的,可以为其他需要使用PostgreSQL的应用程序或会话留下足够的空间 。
设置MaxOpenConns限制的另一个好处是 , 它充当一个非常基本的限流器,防止数据库同时被大量任务压垮 。
但设定上限有一个重要的警告 。如果达到MaxOpenConns限制,并且所有连接都在使用中,那么任何新的数据库任务将被迫等待,直到有连接空闲 。在我们的API上下文中 , 用户的HTTP请求可能在等待空闲连接时无限期地“挂起” 。因此,为了缓解这种情况,使用上下文为数据库任务设置超时是很重要的 。我们将在书的后面解释如何处理 。
SetMaxIdleConns()方法的作用是:设置池中空闲连接数的上限 。缺省情况下 , 最大空闲连接数为2 。
理论上,在池中允许更多的空闲连接将增加性能 。因为它减少了从头建立新连接发生概率—,因此有助于节省资源 。
但要意识到保持空闲连接是有代价的 。它占用了本来可以用于应用程序和数据库的内存,而且如果一个连接空闲时间过长,它也可能变得不可用 。例如,默认情况下MySQL会自动关闭任何8小时未使用的连接 。
因此,与使用更小的空闲连接池相比 , 将MaxIdleConns设置得过高可能会导致更多的连接变得不可用,浪费资源 。因此保持适量的空闲连接是必要的 。理想情况下,go语言对象池设计模式你只希望保持一个连接空闲 , 可以快速使用 。
另一件要指出的事情是MaxIdleConns值应该总是小于或等于MaxOpenConns 。Go会强制保证这点,并在必要时自动减少MaxIdleConns值 。
SetConnMaxLifetime()方法用于设置ConnMaxLifetime的极限值,表示一个连接保持可用的最长时间 。默认连接的存活时间没有限制 , 永久可用 。
如果设置ConnMaxLifetime的值为1小时,意味着所有的连接在创建后,经过一个小时就会被标记为失效连接,标志后就不可复用 。但需要注意:
理论上,ConnMaxLifetime为无限大(或设置为很长生命周期)将提升性能 , 因为这样可以减少新建连接 。但是在某些情况下,设置短期存活时间有用 。比如:
如果您决定对连接池设置ConnMaxLifetime,那么一定要记住连接过期(然后重新创建)的频率 。例如,如果连接池中有100个打开的连接,而ConnMaxLifetime为1分钟,那么您的应用程序平均每秒可以杀死并重新创建多达1.67个连接 。您不希望频率太大而最终影响性能吧 。
SetConnMaxIdleTime()方法在Go 1.15版本引入对ConnMaxIdleTime进行配置 。其效果和ConnMaxLifeTime类似,但这里设置的是:在被标记为失效之前一个连接最长空闲时间 。例如,如果我们将ConnMaxIdleTime设置为1小时,那么自上次使用以后在池中空闲了1小时的任何连接都将被标记为过期并被后台清理操作删除 。
这个配置非常有用,因为它意味着我们可以对池中空闲连接的数量设置相对较高的限制,但可以通过删除不再真正使用的空闲连接来周期性地释放资源 。
所以有很多信息要吸收 。这在实践中意味着什么?我们把以上所有的内容总结成一些可行的要点 。
1、根据经验 , 您应该显式地设置MaxOpenConns值 。这个值应该低于数据库和操作系统对连接数量的硬性限制,您还可以考虑将其保持在相当低的水平,以充当基本的限流作用 。
对于本书中的项目 , 我们将MaxOpenConns限制为25个连接 。我发现这对于小型到中型的web应用程序和API来说是一个合理的初始值 , 但理想情况下,您应该根据基准测试和压测结果调整这个值 。
2、通常 , 更大的MaxOpenConns和MaxIdleConns值会带来更好的性能 。但是,效果是逐渐降低的,而且您应该注意,太多的空闲连接(连接没有被复用)实际上会导致性能下降和不必要的资源消耗 。
因为MaxIdleConns应该总是小于或等于MaxOpenConns,所以对于这个项目,我们还将MaxIdleConns限制为25个连接 。
3、为了降低上面第2点的风险,通常应该设置ConnMaxIdleTime值来删除长时间未使用的空闲连接 。在这个项目中 , 我们将设置ConnMaxIdleTime持续时间为15分钟 。
4、ConnMaxLifetime默认设置为无限大是可以的,除非您的数据库对连接生命周期施加了硬限制,或者您需要它协助一些操作 , 比如优雅地交换数据库 。这些都不适用于本项目,所以我们将保留这个默认的无限制配置 。
与其硬编码这些配置,不如更新cmd/api/main.go文件通过命令行参数读取配置 。
ConnMaxIdleTime值比较有意思,因为我们希望它传递一段时间,最终需要将其转换为Go的time.Duration类型 。这里有几个选择:
1、我们可以使用一个整数来表示秒(或分钟)的数量,并将其转换为time.Duration 。
2、我们可以使用一个表示持续时间的字符串——比如“5s”(5秒)或“10m”(10分钟)——然后使用time.ParseDuration()函数解析它 。
3、两种方法都可以很好地工作,但是在这个项目中我们将使用选项2 。继续并更新cmd/api/main.go文件如下:
File: cmd/api/main.go
go语言对象池设计模式的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于golang对象、go语言对象池设计模式的信息别忘了在本站进行查找喔 。

    推荐阅读