热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

几行代码为老板省百万某高并发服务GOGC及UDPPool优化

20万QPS-臣妾办不到前两天一位同学找我,说他一个需要几百万qps的go重构的服务下周要上线,但随着容器核数增加,不具有线性ScaleUp,他担心上线后扛不住,心里没底.8核(容

20万QPS-臣妾办不到

前两天一位同学找我, 说他一个需要几百万qps的go重构的服务下周要上线, 但随着容器核数增加, 不具有线性Scale Up, 他担心上线后扛不住,心里没底. 8核(容器虚拟核, 可以认为是4个物理核, 以下都为虚拟核)时QPS 2.3万/s, 他单容器加到40核时, qps只有8万(按我看来, 这种单容器搞这么大核, 不太合理) 他的目标是能够大致对标原先的cpp服务, 40核物理核50万/s, 即对应到虚拟核40核, 25万/s, go能跑到15-20万即可(原先cpp数据读共享内存, go读redis).

以下代码均为精简过的示意代码, 与实际略有不同.

屡试不爽GOGC: 8万/s到14万/s

我大概了解了一下他的服务逻辑模型, 接收客户端UDP请求, 请求一次redis, 然后再单向发UDP包给其他机器.

服务的内存占用不大, 200m. 这种情况, 有一种屡试不爽的方法: 在main里面分配一个1024M不用的[]byte, 全局占着, 根据允许的内存情况, 然后把服务GOGC设置大一些.

我这里设置了300, 也就是正常跑的时候, 占用内存4G, 一测QPS从8万到了14万, gc消耗在火焰图中也基本看不见了. 3行代码为马化腾节省上百万, 离目标不远了. 足以看出go的gc在这种高QPS小内存情况下的损耗之大.

UDP问题

接下来跑一发火焰图. 有几处可以优化的地方. 我觉得到20万/s应该没啥问题. 其中有一点比较有意思, UDP单发的client调用, 占用CPU竟然比同样数量的redis调用tcp一来一回的还要多. 不是说UDP简单, 这咋比TCP还耗性能呢?

火焰图中显示占用了竟然达到30%的CPU时间.

RPC框架中UDP发包的代码精简了一下大概是这样. 

火焰图中的1对应net.DialUDP, 2对应udpconn.Write, 3对应conn.Close.

大致浏览了下代码, 其实这里主要是系统调用耗费了时间 DialUDP: SYSSOCKET创建socket fd, SYSFCNTL获取fd属性和设置fd为nonblock, SYSSETSOCKOPT设置socket fd的一些属性, SYSCONNECT绑定远端ip port, 最大头的还是poll.runtimepollOpen里把这个UDP fd放入到epoll中EPOLLCTLADD.

Write: 基本是SYSWRITE.

Close: 基本是SYSCLOSE.

由此看出, 这个代码发一次udp发包, 这么多系统调用. 最大的问题就是每次发包, 都创建一个实例, 然后用一下, 就关掉了. UDP实例能否复用?

UDP实例复用

其实代码思路倒不难, 就是对于同一个对端地址的udp实例, 使用一个sync.Pool保存, 这样同时对一个ip port的请求可以使用不同的实例, 比用一个实例减少锁等待.

不同的地址用一个sync.Map保存各个对应的sync.Pool. 使用时, 从sync.Map获取对应的sync.Pool, pool中有则使用, 没有则创建一个.

使用结束, 放回pool中.

sync.Pool的特性使得在GC时, 可以释放不再使用的fd.

结构体包装定义

Pool代码

这里其实会有一点内存泄露的味道, sync.Map中对端地址对应的sync.Pool实例本身没法释放. 不过这里问题不大, 对于服务端之间调用, 就算你对端节点有几万个, 我估计也就只有几M内存而已.

是否有并发问题, 串包问题?

对于这种并发网络编程, 最怕的就是并发不安全的操作并发了, 状态乱了, 或者串包. 初看起来没啥问题: 不同协程不会同时使用一个UDP实例.

但仔细一思考, 这样的使用方式还是存在一点局限性的, 大家都知道UDP是无状态的, 也就是说, 你用udp往ip1:p1发个包, 那么你本地也必然开了一个端口local:p2, 然后你不收回包了, 但服务端回了包.

接下来, 另一个协程如果也往ip1:p1发包, 正好也用了local:p2, 但它收回包, 这个时候上一次服务端的回包就被这个协程接收了, 导致串包了. 不复用UDP实例, 每次new, write, close, 这种情况都有可能偶尔发生. 那用池子的话, 更加加剧了这种情况. 所以

  1. UDP客户端要和服务端的行为一致, 服务端不回包的话, 客户端就只发不收. 服务端回包的话, 客户端就即发又收.

  2. 如果udp服务端回包, 客户端也不想收, 那么在一个客户端机器上, 不要对同一个对端udp服务, 存在收包和不收包的两种udp client模型.

经过压测, 的确没有串包问题.

UDP改进效果 17万/s

QPS从14万涨到了17万. 之前dial占了绝大部分, 现在已经很少了, close也很少了. 而write在现有代码调用下, 是没减少的.

其他处理

看了下火焰图, 还有其他一些地方可以优化一下.

  1. 日志精简

  2. 上报合并

  3. 客户端服务发现的select node性能需要优化

做了1,2步后, 基本上到QPS到20万问题不大, 后面我也没关注了. 当然正式环境跑的时候, 跑个一半多点的负载就行了.

关于UDP connect

可能没接触过UDP的人会觉得很奇怪, go的udp怎么会有dial呢? 其实底层调的是udp connect, 作用当然不是三次握手, 建立连接, UDP中调用connect内核仅仅把对端ip&port记录下来, 发包时无需指定对端addr, 使用 funcWrite(b[]byte)(int,error), 同一个实例后面多次发包都无需再做connect.

go里面也还有not connect udp, 使用net.ListenPacket创建, 然后发包时指定对端addr, 使用方法 WriteTo(p[]byte,addrAddr)(nint,err error), 这种情况下, 每次发包内核都要绑定对端addr, 发包完, 解绑. 多次发包, 都会进行绑定和解绑.

除此之外, 对于connected udp, 收包的时候也会减少应用层出现串包的问题. 因为它只收connect的对端ip的包返回给应用层.

总结

UDP以前用的多, 框架直接通过配置就能切换udp/tcp, 内网网络环境好, UDP用起来没啥问题, 不过现在用的不多了. 我不太熟, 做这个优化我也是现学的UDP.

抓大放小, 先从参数改起. 20行代码, 帮马化腾节省几百万. 

参考阅读

  • 一文搞懂 etcd 3.5 核心特性

  • 谈谈实体的 ID 与“键”

  • 消息中间件:为什么我们选择 RocketMQ

  • 分布式一致性算法Raft

  • dubbogo 是如何炼成的?

  • 差之毫厘:etcd 3 完美支持 HTTP 访问

原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

2021年GIAC调整到7月30-31日在深圳举行,点击阅读原文了解更多详情。


推荐阅读
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • 2021最新总结网易/腾讯/CVTE/字节面经分享(附答案解析)
    本文分享作者在2021年面试网易、腾讯、CVTE和字节等大型互联网企业的经历和问题,包括稳定性设计、数据库优化、分布式锁的设计等内容。同时提供了大厂最新面试真题笔记,并附带答案解析。 ... [详细]
  • 本文整理了Java中java.lang.NoSuchMethodError.getMessage()方法的一些代码示例,展示了NoSuchMethodErr ... [详细]
  • python中安装并使用redis相关的知识
    本文介绍了在python中安装并使用redis的相关知识,包括redis的数据缓存系统和支持的数据类型,以及在pycharm中安装redis模块和常用的字符串操作。 ... [详细]
  • 本文总结了初学者在使用dubbo设计架构过程中遇到的问题,并提供了相应的解决方法。问题包括传输字节流限制、分布式事务、序列化、多点部署、zk端口冲突、服务失败请求3次机制以及启动时检查。通过解决这些问题,初学者能够更好地理解和应用dubbo设计架构。 ... [详细]
author-avatar
fdsfdsfsfsfsfsfsfsfsafsf
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有