组内的数据系统在承接一个业务需求时无法满足性能需求,于是针对这个场景做了一些优化,在此写篇文章做记录。

业务场景是这样:调用方一次获取某个用户的几百个特征(可以把特征理解为属性),特征以 redis hash 的形式存储在持久化 KV 数据库中,特征数据以天级别为更新粒度。要求 95 分位的延迟在 5ms 左右。

这个数据系统属于无状态的服务,为了增大吞吐量和降低延迟,从存储和代码两方面进行优化。

存储层面

存储层面,一次调用一个用户的三百个特征原方案是用 redis hash 做表,每个 field 为用户的一个特征。由于用户单个请求会获取几百个特征,即使用hmget做合并,存储也需要去多个 slot 中获取数据,效率较低,于是对数据进行归一化,即:把 hash 表的所有 filed 打包成一个 json 格式的 string,举个例子:

// 优化前的特征为 hash 格式
hash key : user_2837947
127.0.0.1:6379> hgetall user_2837947
1) "name"    // 特征1
2) "薯条"     // 特征1的值
3) "age"    // 特征2
4) "18"     // 特征2的值
5) "address" // 特征3
6) "China"   // 特征3的值// 优化后的特征为 string json格式
string key: user_2837947
val:
{"name":"薯条","age":18,"address":"China"
}

特征进行打包后解决了一次请求去多个 slot 获取数据时延较大的问题。但是这样做可能带来新的问题:若 hash filed 过多,string 的 value 值会很大。目前想到的解法有两种,一种是按照类型将特征做细分,比如原来一个 string 里面有 300 的字段,拆分成 3 个有 100 个值的 string 类型。第二种是对 string val 进行压缩,在数据存储时压缩存储,读取数据时在程序中解压缩。这两种方法也可以结合使用。

如果这样仍不能满足需求,可以在持久化 KV 存储前再加一层缓存,缓存失效时间根据业务特点设置,这样程序交互的流程会变成这样:

代码层面

接着来优化一下代码。首先需要几个工具去协助我们做性能优化。首先是压测工具,压测工具可以模拟真实流量,在预估的 QPS 下观察系统的表现情况。发压时注意渐进式加压,不要一下次压得太死。

然后还需要 profiler 工具。Golang 的生态中相关工具我们能用到的有 pprof 和 trace。pprof 可以看 CPU、内存、协程等信息在压测流量进来时系统调用的各部分耗时情况。而 trace 可以查看 runtime 的情况,比如可以查看协程调度信息等。本次优化使用 压测工具+pprof 的 CPU profiler。

下面来看一下 CPU 运行耗时情况:

右侧主要是 runtime 部分,先忽略

火焰图中圈出来的大平顶山都是可以优化的地方,

这里的三座平顶山的主要都是json.Marshaljson.Unmarshal操作引起的,对于 json 的优化,有两种思路,一种是换个高性能的 json 解析包 ,另一种是根据业务需求看能否绕过解析。下面分别来介绍:

高性能解析包+一点黑科技

这里使用了陶师傅的包github.com/json-iterator/go。看了他的 benchmark 结果,比 golang 原生库还是要快很多的。自己再写个比较符合我们场景的Benchmark看陶师傅有没有骗我们:

package mainimport ("encoding/json"jsoniter "github.com/json-iterator/go""testing"
)var s = `{....300多个filed..}`func BenchmarkDefaultJSON(b *testing.B) {for i := 0; i < b.N; i++ {param := make(map[string]interface{})_ = json.Unmarshal([]byte(s), &param)}
}func BenchmarkIteratorJSON(b *testing.B) {for i := 0; i < b.N; i++ {param := make(map[string]interface{})var json = jsoniter.ConfigCompatibleWithStandardLibrary_ = json.Unmarshal([]byte(s), &param)}
}

运行结果:

这个包易用性也很强,在原来 json 代码解析的上面加一行代码就可以了:

var json = jsoniter.ConfigCompatibleWithStandardLibrary
err = json.Unmarshal(datautil.String2bytes(originData), &fieldMap

还有一个可以优化的地方是string[]byte之间的转化,我们在代码里用的参数类型是string,而 json 解析接受的参数是[]byte,所以一般在json解析时需要进行转化:

err = json.Unmarshal([]byte(originData), &fieldMap)

那么string转化为[]byte发生了什么呢。

package mainfunc main(){a := "string"b := []byte(a)println(b)
}

我们用汇编把编译器悄悄做的事抓出来:

来看一下这个函数做了啥:

这里底层会发生拷贝现象,我们可以拿到[]bytestring的底层结构后,用黑科技去掉拷贝过程:

func String2bytes(s string) []byte {x := (*[2]uintptr)(unsafe.Pointer(&s))h := [3]uintptr{x[0], x[1], x[1]}return *(*[]byte)(unsafe.Pointer(&h))
}func Bytes2String(b []byte) string {return *(*string)(unsafe.Pointer(&b))
}

下面写 benchmark 看一下黑科技好不好用:

package mainimport ("strings""testing"
)var s = strings.Repeat("hello", 1024)func testDefault() {a := []byte(s)_ = string(a)
}func testUnsafe() {a := String2bytes(s)_ = Bytes2String(a)
}func BenchmarkTestDefault(b *testing.B) {for i := 0; i < b.N; i++ {testDefault()}
}func BenchmarkTestUnsafe(b *testing.B) {for i := 0; i < b.N; i++ {testUnsafe()}
}

运行速度,内存分配上效果都很明显,黑科技果然黑:

加 cache,空间换时间

项目中有一块代码负责处理 N 个请求中的参数。代码如下:

for _, item := range items {var params map[string]stringerr := json.Unmarshal([]byte(items[1]), &params)if err != nil {...}
}

在这个需要优化的场景中,上游在单次请求获取某个用户300多个特征,如果用上面的代码我们需要json.Unmarshal300多次,这是个无用且非常耗时的操作,可以加 cache 优化一下:

paramCache := make(map[string]map[string]string)for _, item := range items {var params map[string]stringtmpParams, ok := cacheDict[items[1]]// 没有解析过,进行解析if ok == false {err := json.Unmarshal([]byte(items[1]), &params)if err != nil {...}cacheDict[items[1]] = params} else {// 解析过,copy出一份// 这里的copy是为了预防并发问题params = DeepCopyMap(tmpParams)}}

这样理论上不会存在任何的放大现象,读者朋友如果有批处理的接口,代码中又有类似这样的操作,可以看下这里是否有优化的可能性。

for {dosomething()
}

替换耗时逻辑

火焰图中的 TplToStr 模板函数同样占到了比较大的 CPU 耗时,此函数的功能是把用户传来的参数和预制的模板拼出一个新的 string 字符串,比如:

入参:Tpl: shutiao_test_{{user_id}} user_id: 123478
返回:shutiao_test_123478

在我们的系统中,这个函数根据模板和用户参数拼出一个 flag,根据这个 flag 是否相同作为某个操作的标记。这个拼模板是一个非常耗时的操作,这块可以直接用字符串拼接去代替模板功能,比如:

入参:Tpl: shutiao_test_{{user_id}} user_id: 123478
返回:shutiao_test_user_id_123478

优化完之后,火焰图中已经看不到这个函数的平顶山了,直接节省了 5%的 CPU 的调用百分比。

prealloc

还发现一些 growslice 占得微量 cpu 耗时,本以为预分配可以解决问题,但做 benchmark 测试发现 slice 容量较小时是否做预分配在性能上差异不大:

package mainimport "testing"func test(m *[]string) {for i := 0; i < 300; i++ {*m = append(*m, string(i))}
}func BenchmarkSlice(b *testing.B) {for i := 0; i < b.N; i++ {b.StopTimer()m := make([]string, 0)b.StartTimer()test(&m)}
}func BenchmarkCapSlice(b *testing.B) {for i := 0; i < b.N; i++ {b.StopTimer()m := make([]string, 300)b.StartTimer()test(&m)}
}

对于代码中用到的 map 也可以做一些预分配,写 map 时如果能确认容量尽量用 make 函数对容量进行初始化。

package mainimport "testing"func test(m map[string]string) {for i := 0; i < 300; i++ {m[string(i)] = string(i)}
}func BenchmarkMap(b *testing.B) {for i := 0; i < b.N; i++ {b.StopTimer()m := make(map[string]string)b.StartTimer()test(m)}
}func BenchmarkCapMap(b *testing.B) {for i := 0; i < b.N; i++ {b.StopTimer()m := make(map[string]string, 300)b.StartTimer()test(m)}
}

这个优化还是比较有效的:

异步化

接口流程中有一些不影响主流程的操作完全可以异步化,比如:往外发送的统计工作。在 golang 中异步化就是起个协程。

总结一下套路:

代码层面的优化,是 us 级别的,而针对业务对存储进行优化,可以做到 ms 级别的,所以优化越靠近应用层效果越好。对于代码层面,优化的步骤是:

  1. 压测工具模拟场景所需的真实流量

  2. pprof 等工具查看服务的 CPU、mem 耗时

  3. 锁定平顶山逻辑,看优化可能性:异步化,改逻辑,加 cache 等

  4. 局部优化完写 benchmark 工具查看优化效果

  5. 整体优化完回到步骤一,重新进行 压测+pprof 看效果,看 95 分位耗时能否满足要求(如果无法满足需求,那就换存储吧~。

另外推荐一个不错的库,这是 Golang 布道师 Dave Cheney 搞的用来做性能调优的库,使用起来非常方便:https://github.com/pkg/profile,可以看 pprof和 trace 信息。有兴趣读者可以了解一下。


最后,给自己打个广告

欢迎加入 随波逐流的薯条 微信群。

薯条目前有草帽群、木叶群、琦玉群,群交流内容不限于技术、投资、趣闻分享等话题。欢迎感兴趣的同学入群交流。

入群请加薯条的个人微信:709834997。并备注:加入薯条微信群。

一个95分位延迟要求5ms的场景,如何做性能优化相关推荐

  1. 智能音箱场景下的性能优化

    QCon是由InfoQ主办的综合性技术盛会,今年是Qcon举办的第10个年头,半吊子全栈工匠有幸作为演讲嘉宾分享一个近两年来的实践经验--智能音箱场景下的性能优化,隶属于曾波老师出品的"场景 ...

  2. UE4 大世界场景制作与性能优化

    一.使用工具 1.1使用 HLOD 由于HLOD是减少DrawCall的方案,所以其实他会对渲染性能的提高能够起到一定的帮助作用.因为它是将场景中互相独立的静态Mesh通过顶点和材质合并的方式聚合在一 ...

  3. ES在几十亿数据量级的场景下的性能优化

      es性能优化没有什么银弹.不要指望调一个参数,就可以万能的应对所有场景. 1.性能优化杀手锏-filesystem cache   ES数据检索的流程如上所示,第一次检索一个数据时是从磁盘里读的, ...

  4. jquery全局变量_jQuery源码一个小细节,却很好地体现了性能优化的思想,很优秀...

    写在前面 听闻大佬们在写一些框架或者库的时候,到处都隐藏了一些细节,所以阅读他们的源代码,无论从性能优化.还是JS API的学习.亦或是代码风格等方面给到我们很多启发.这两天我翻看了一下jQuery1 ...

  5. etcd 在超大规模数据场景下的性能优化

    2019独角兽企业重金招聘Python工程师标准>>> 作者 | 阿里云智能事业部高级开发工程师 陈星宇(宇慕) 概述 etcd是一个开源的分布式的kv存储系统, 最近刚被cncf列 ...

  6. Flink 新场景:OLAP 引擎性能优化及应用案例

    摘要:本文由阿里巴巴技术专家贺小令(晓令)分享,主要介绍 Apache Flink 新场景 OLAP 引擎,内容分为以下四部分: 背景介绍 Flink OLAP 引擎 案例介绍 未来计划 一.背景介绍 ...

  7. 性能优化:一个 Flink 参数节省了 50% 的 CPU 消耗

    本文属于 Flink 在生产环境的大规模 CPU 优化实战,大并发任务预计节省 30~50% 的 CPU 消耗.下文会详细分析优化相关的实现原理.问题定位以及优化过程.往往在做性能优化时就会发现:当已 ...

  8. editor编辑器为什么头部信息会不见_简单聊一聊一个前端编辑器的性能优化

    最近项目一直在使用 Monaco Editor 这个库. 在我加了一个新功能之后, 整个编辑器开始变的非常卡, 我试图解决这个性能问题. 但是发现有一些棘手... 评论以及文末有更新 场景 其实我加的 ...

  9. 开源了个人开发的一个微信小程序,提供共享场景服务,支持图片和小视频

    初衷 个人开发这个小程序源自一个想法:小时候看葫芦娃,每个葫芦娃都有自己的本领,其中一个"千里眼"的本领可真大.参加工作后,可能有些怀旧,想看看大学学习的地方.住过的宿舍 或者曾经去过的小地方,但并不 ...

最新文章

  1. pandas isnull() 返回bool
  2. setting an array element with a sequence
  3. Codeforces 724 G Xor-matic Number of the Graph 线性基+DFS
  4. 手机技巧:是否应该一次性给手机充电到100%?看完你就明白了!
  5. 想学好数学,请收好这份宝典!
  6. 程序媛如何自我突破?
  7. Jmeter之json条件提取实战(三)
  8. reflect动画_3DSMAX制作超时空未来动画场景-3D建模场景模型教程
  9. 33 计算机维修,33.计算机硬件检测维修与数据恢复竞赛规程(修改)全解.doc
  10. [转载]Linux Shell 的快捷键
  11. 用es5实现es6的promise,彻底搞懂promise的原理
  12. java DTO循环_Java Stream与for循环比较
  13. node.js 快速入门
  14. MATLAB的输入与输出
  15. Win10磁盘占用100%的解决办法
  16. 计算机里的音乐怎么设置,声音和音频设备在电脑上如何设置 电脑无声的情况如何解决【详解】...
  17. 监控freeswitch sip信令
  18. DEDECMS织梦后台更新网站栏目无反应一键更新无响应的解决方法
  19. 4G远程智能巡检摄像机解决方案对比
  20. 欧姆龙服务器显示oE,在公网如何配置OE客户端

热门文章

  1. 消控中心人员配置_关于2018年度环创中心楼宇物业综合管理考评情况的通报
  2. Polly 重试策略
  3. UCloud想吃科创板的“第一个螃蟹”
  4. sudo with no password
  5. 【译】UNIVERSAL IMAGE LOADER. PART 3---ImageLoader详解
  6. ExcelJS —— Node 的 Excel 读写扩展模块2
  7. 【转载】eclipse常用插件在线安装地址或下载地址
  8. Intent中各种常见的Action
  9. Tomcat配置虚拟主机的两种方式
  10. 如何在DC机上禁用成员机的本地账号