作者:ZhiYan,Jack47

转载请保留作者和原文出处

Update:

2018.8.8 在无锁小节增加了一些内容

性能优化,优化的东西一定得在主路径上,结合测量的结果去优化。不然即使性能再好,逻辑相对而言执行不了几次,其实对提示性能的影响微乎其微。记得抖哥以前说多隆在帮忙查广告搜索引擎的问题,看到了一处代码,激动的说这里用他的办法,性能可以提升至少10倍。但实际上,这里的逻辑基本走不到 face_palm。

性能优化的几个跟语言无关的大方向:

减少算法的时间复杂度

例子1

我们实现了一个CallBack的机制,一段执行流程里,会有多个plugin,每个plugin可以添加callback,每个callback有唯一的名字;添加callback时,需要注意覆盖的问题,如果覆盖了,需要返回老的callback。一开始我们的实现机制是使用数组,这样添加时,需要挨个遍历,查看是否时覆盖的情况。Update操作的时间复杂度为O(n);后来我们添加了一个辅助的Map,用来存储 <name, callbackIdx>的映射关系。Update的平均时间复杂度降低为O(1)

例子2

在我们的pipeline场景里,类似net/http里的context,我们有个task的概念的。每个阶段(plugin)都可以向里面塞数据,一开始为了支持cancel某个阶段,重新执行这个阶段的功能,我们是使用嵌套,类似递归的方式。这样就可以很方便的撤销某个阶段放入的数据。但是这种设计,如果要从里面取数据,需要层层遍历,类似递归一样,时间复杂度为O(n);因为每个plugin都会与task打交道,所以这里 task里数据的存取是高频操作,而且我们后来经过权衡,觉得支持取消掉某个阶段对task的操作,不是必须的,不支持也没关系,所以后来简化了task的设计,直接用一个map来做,这样时间复杂度又降下来了。

根据业务逻辑,设计优化的数据结构

我们有个场景,是要对URL执行类似归一化的操作,把里面重复的\字符删掉,比如 \\ -> \。这个逻辑对于网关,是高频逻辑,因为每个请求来了,都需要判断,但是真正要删掉重复的\的操作,其实比较少,大部分场景是检查完,发现正常,不需要做修改。

一开始我们的实现是把url字符挨个检查,没问题的放入 bytes.Buffer 中,最终返回 buffer.String();后来我们优化了一下,采用了标准库中 path/path.go 中的 Lazybuf 的方式,LazyBuf中发现要写入的字符和基准的字符不一样时,才分配内存来存储修改后的字符串,不然最终还是基准的字符,直接返回就行,避免了无谓的内存拷贝操作。

这里其实体现了一个小技巧,尽量想想自己需要的操作,是否标准库里有,同时也要多看看标准库的实现,吸取经验。

尽量减少磁盘IO次数

IO操作尽量批量进行。比如我们的网关会记录访问日志,类似Nginx的access.log。在生产环境/压测环境下,会生成大量的日志,虽然操作系统写入文件是有缓冲的,但是这个缓冲机制我们应用程序没法直接控制,而且写入文件时调用系统API,也比较耗时。我们可以在应用层面,给日志留缓冲区(buffer),定时或达到一定量(4k,跟虚拟文件系统的块大小保持一致)时调用操作系统IO操作来写入日志。

总结一下,就是写入日志是异步的,同时是攒够一批之后,再调用操作系统的写入

具体实现:进来的数据,先放到一个2048字节大小的channel里,由一个固定的go routine负责不断的从channel里读取数据,写入到buffered io里。这里2048字节的channel,类似队列一样,是有削峰作用的。当有大批日志写入时,channel可以暂时缓冲一下,降低 buffer.io 真正flush的频率。;写入文件时,套上一个 bufio.Writer(size=512),即内部是有512字节大小的缓冲区,满了才使用整块数据调用Write();

尽量复用资源

资源的申请和释放,跟内存(也是一种资源)的申请和释放其实是一样的,尽量复用,避免重复/频繁申请;

比如下面的这个time.Tick,适用于使用者不需要关闭它,即非频繁调用的情况。使用它很方便,但是要注意,它没法关闭,所以垃圾回收器也没法回收它。来看一下下面的这段代码修改记录:

+	ticker := time.NewTicker(time.Second)
+ defer ticker.Stop()
+
for {
select {
- case <-time.Tick(time.Second):
+ case <-ticker.C:

修改前,for循环里会频繁创建time.Ticker,但都没有回收机制。改动后,for循环里复用同一个time.Ticker,而且会在当前函数执行结束时释放time.Ticker。

sync.Map的使用

其实看清楚map.go里的注释,注意使用场景。

sync.Map适合两种用途:

  1. 指定的key,value只会被写入一次,但是会被读取很多次
  2. 多个goroutine读取、写入、覆盖的数据都是没有交集的

只有上述情况下,sync.Map才能相比Go map搭配单独的Mutex或RWMutex而言,显著降低锁的竞争,均摊复杂度是常数(amortized constant time)

大部分情况下,应该用 map ,然后用单独的锁或者同步机制,这样类型安全,而且可以有其他的逻辑

锁相关

Mutexes

锁在满足以下条件的情况下,是很快的:

  • 没有其他人竞争 (想象为挤公交车,此时没人跟你抢,你直接上车)
  • 锁覆盖的代码,执行时间非常快 (想象为挤公交车,大家速度都很快,嗖嗖就上去了,下一个人等待上一个人挤上去的时间很短)

当竞争越激烈,锁的性能下降的越厉害。

Reference:

locks aren't slow, lock contention is

锁的粒度尽量小

比如我们的pipeline生命周期的管理,一开始是通过一把大锁来控制并发的,后续优化时,发现里面可以细分成两块,各自可以用一把锁来控制,这样锁的粒度变小,并发程度会提高。

这里比较好的例子是BigCache的实现。它使用分片(sharding)的方式,

跟Java 7里的concurrent hash map的实现类似,对数据进行分片,分片之间是独立的,可以并发的进行写操作。对细分后的分片进行并发控制,这样能有效减小锁的粒度,让并发度尽可能高。

Reference: Writing Fast Cache Service in Go

RWMutexes

  1. 是否有多读少写的场景,如果是,尽量用读写锁;这样尽量把写锁的粒度缩小,能用读锁解决的,就不需要用写锁,真正需要修改结果时,才使用写锁。

比如:

func (b *DataBucket) QueryDataWithBindDefault(key interface{}, defaultValueFunc DefaultValueFunc) (interface{}, error) {
先上读锁,看key是否存在,如果存在,就返回 // 大部分情况下是这样,所以这个优化肯定很有意义
否则,上写锁,把默认值加上 // 这种情况只会发生一次
}

尽量使用无锁的方式:

是否真的需要使用加锁的方式来保证整个代码块是互斥的?是否能用原子操作(CAS)来代替锁?

原子操作和锁的主要区别在于锁的粒度,使用锁,可以让锁保护的整个代码块是互斥的,使用原子操作,只能让操作的这个变量是互斥的。所以原子操作适合修改某个值的情况。

例如:

利用 atmoic int stopped = 0/1 来代表是否停止,需要停止时,设置为1。

golang里Atomic操作有:Atomic.CompareAndSet, LoadInt(), StoreInt()

如果利用某个变量代表现在是否在干活,close时需要等别人干完活,那么在close时,需要通过spin的方式等待干活的人结束:

for atomic.LoadInt(&doing) > 0 {
sleep(1ms)
}

关于原子操作,可以看看 JDK 7 里 ConcurrentHashMap的实现,大量使用了原子操作,保证是无锁,非阻塞的。比如里面segments[]是懒初始化,如果需要writer初始化一个segment,赋值给segments时,就可以用CompareAndSet。

内存相关

减少内存分配的次数

生成字符串时,尽量写入 bytes.Buffer, 而不是用 fmt.Sprintf()

+	var repeatingRune rune
- result := string(s[0])
+ result := bytes.NewBuffer(nil)
for _, r := range s[:1] {
- result = fmt.Sprintf("%s%s", result, string(r))
+ result.WriteRune(r)
+ }

数据结构初始化时,尽量指定合适的容量

比如Java或者Go里面,如果数组,Map的大小已知,可以在声明时指定大小,这样避免后续追加数据时需要扩展内部容量,造成多次内存分配

-	eventStream := make(chan cluster.Event)
+ eventStream := make(chan cluster.Event, 1024)

语言(Go)相关

语言相关的其实还有很多,但是随着语言的发展,基本上都会被解决掉,所以这里只提一下下面的这个,对Go语言感兴趣的同学,可以看So You Wanna Go Fast

避免内存拷贝

如下的代码,两者有什么区别?

-	for _, bucket := range s.buckets {
- bucket.Update(v)
+ for i := 0; i < len(s.buckets); i++ {
buckets[i].Update(v)

修改前的这种方式,bucket是通过拷贝生成的临时变量;而且这种方式下,由于操作的是临时变量,所以 s.buckets并不会被更新!

Go routine虽好,也有代价

我们的网关,一开始的时候,由于大家也都是刚接触Go语言,用Go routine用的也顺手,所以很喜欢用Go routine;比如我们的主流程里,需要记录本次请求的一些指标,为了不影响主流程的执行,这些记录指标的逻辑都是启动一个新的go routine去执行的。后来发现我们在一台机器上,一个程序里,某一时刻启动了十万计的go routine,而这些go routine生命周期很短,会不断的销毁和创建。我也简单的用Go Benchmark测试模拟了一个场景,测试了之后发现go routine数量上去后,性能下降很大,说明此时的调度开销也比较大了。后来我们修改了设计,让大家把需要更新的数据放到channel里,启动固定的go routine去做更新的事情,这样可以避免频繁创建go routine的情况。

使用多个http.Client来发送请求

一开始我们是通过一个http.Client来发送同一个API的请求,后来担心这里可能存在并发的瓶颈,尝试了创建多个http.Client,发送时随机使用某一个发送的机制,发现性能提升了。其实性能有多少提升,取决于使用场景的,还是得实际测量,用数值说话,我们的方法不一定对你们有用!

Go语言在benchmark方面,提供了很多强有力的工具,可以参加下面的文章:

High performance go workshop

An Introduction to go tool trace

Writing and Optimizing Go code

Go tooling essentials

好了,以上就是所有内容了,欢迎留下你的性能优化的思路和方法!


如果您看了本篇博客,有所收获,请点击右下角的“推荐”,让更多人看到!

打赏也是对自己的肯定
微信打赏

web server性能优化浅谈的更多相关文章

  1. App性能优化浅谈

    前言 前段时间给公司的小伙伴们进行了关于app性能优化的技术分享.这里我稍微整理一下也给大家分享一下.关于性能优化这个话题非常大,涉及面能够非常广,也能够非常深入.本人能力有限,不会给大家讲特别难懂, ...

  2. sql性能优化浅谈

    sql性能优化总结: 最近随着数据越来越多,数据库性能问题暴露的越来越严重.几百万,上千万,甚至过亿的数据处理速度会非常的慢. 下面对工作中遇到的问题做下总结,希望以后能对日后的工作有所帮助. 不同的 ...

  3. MYSQL优化浅谈,工具及优化点介绍,mysqldumpslow,pt-query-digest,explain等

    MYSQL优化浅谈 msyql是开发常用的关系型数据库,快速.稳定.开源等优点就不说了. 个人认为,项目上线,标志着一个项目真正的开始.从运维,到反馈,到再分析,再版本迭代,再优化… 这是一个漫长且考 ...

  4. pb传输优化浅谈

    在正式切入今天要谈的优化之前,先碎碎念一些自己过去这几年的经历.很久没有登录过博客园了,今天也是偶然兴起打开上来看一下,翻看了下自己的随笔,最后一篇原创文章发布时间是2015年的4月,今天是2017年 ...

  5. Web前端性能优化全攻略

    网页制作poluoluo文章简介:Web 前端性能优化是个大话题,是个值得运维人员持续跟踪的话题,是被很多网站无情忽视的技术. Web 前端性能优化是个大话题,是个值得运维人员持续跟踪的话题,是被很多 ...

  6. Web前端性能优化全攻略[转载]

    1. 尽量减少 HTTP 请求 (Make Fewer HTTP Requests) 作为第一条,可能也是最重要的一条.根据 Yahoo! 研究团队的数据分析,有很大一部分用户访问会因为这一条而取得最 ...

  7. Web前端性能优化进阶——完结篇

    前言 在之前的文章 如何优化网站性能,提高页面加载速度 中,我们简单介绍了网站性能优化的重要性以及几种网站性能优化的方法(没有看过的可以狂戳 链接 移步过去看一下),那么今天我们深入讨论如何进一步优化 ...

  8. web前端性能优化指南(转)

    web前端性能优化指南 概述 1. PC优化手段在Mobile侧同样适用2. 在Mobile侧我们提出三秒种渲染完成首屏指标3. 基于第二点,首屏加载3秒完成或使用Loading4. 基于联通3G网络 ...

  9. Web前端性能优化教程09:图像和Cookie优化

    本文是Web前端性能优化系列文章中的第九篇,主要讲述内容:图像和Cookie优化.完整教程可查看:  一. 图像优化 图像基础知识 gif: 适用于动画效果,例如提示的滚动条图案 jpg: 是一种使用 ...

随机推荐

  1. 对try-catch-finally异常处理的最新理解

    try{ ...... }catch(......){ }finally{ ...... } 这个结构是用来处理Java所有可能出现的异常的,这个我很早其实就已经学过,不过最近看了个视频,感觉自己虽然 ...

  2. 虚拟机搭建CentOS主机win10通过xshell连接

    目标:主机是win10系统,虚拟机搭建CentOS,在主机上通过XShell连接操作. 第一步 主机上安装虚拟机 第二步 下载CentOS 下载地址http://101.110.118.69/isor ...

  3. Linux kernel的中断子系统之(三):IRQ number和中断描述符

    返回目录:<ARM-Linux中断系统>. 总结: 二描述了中断处理示意图,以及关中断.开中断,和IRQ number重要概念. 三介绍了三个重要的结构体,irq_desc.irq_dat ...

  4. Azure Go Management SDK 中国版使用示例

    简介 刚学习go几天,尝试调用Azure的SDK进行管理API的操作,基本思路是基于注册的AD Application信息生成token,然后再使用Token生成serviceClient,然后再进行 ...

  5. Debian虚拟机安装VirtualBox增强功能

    作者:荒原之梦 原文链接:http://zhaokaifeng.com/?p=573 本文中使用的Debian是安装在VirtualBox中的虚拟机,具体参数如下: Debian版本:Linux de ...

  6. 拇指玩」制作的「谷歌安装器」app

    作者:匿名用户链接:https://www.zhihu.com/question/57468448/answer/153000587来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...

  7. 找不到 blog.csdn.net 的服务器 DNS 地址

    找不到 blog.csdn.net 的服务器 DNS 地址 csdn的博客用win7的电脑打不开是怎么回事?手机可以正常打开,csdn的bbs 下载什么的都可以正常使用. blog.csdn.net显 ...

  8. BZOJ_1598_[Usaco2008 Mar]牛跑步_A*

    BZOJ_1598_[Usaco2008 Mar]牛跑步_A* Description BESSIE准备用从牛棚跑到池塘的方法来锻炼. 但是因为她懒,她只准备沿着下坡的路跑到池塘, 然后走回牛棚. B ...

  9. nginx用户认证与htpasswd命令

    最近在搭建ELK,然后ELK的kibana界面想添加一个访问限制,看到kibana有个插件x-pack,本来想用用,发现是收费的,就放弃了,然后就想着想配置下nginx的认证访问来实现简单的访问登陆. ...

  10. 基于Mycat实现读写分离

    随着应用的访问量并发量的增加,应用读写分离是很有必要的.当然应用要实现读写分离,首先数据库层要先做到主从配置,本人前一篇文章介绍了mysql数据库的主从配置方式即:<mysql数据库主从配置&g ...