通过MapReduce降低服务响应时间
在微服务中开发中,api网关扮演对外提供restful api的角色,而api的数据往往会依赖其他服务,复杂的api更是会依赖多个甚至数十个服务。虽然单个被依赖服务的耗时一般都比较低,但如果多个服务串行依赖的话那么整个api的耗时将会大大增加。
那么通过什么手段来优化呢?我们首先想到的是通过并发来的方式来处理依赖,这样就能降低整个依赖的耗时,Go基础库中为我们提供了 WaitGroup 工具用来进行并发控制,但实际业务场景中多个依赖如果有一个出错我们期望能立即返回而不是等所有依赖都执行完再返回结果,而且WaitGroup中对变量的赋值往往需要加锁,每个依赖函数都需要添加Add和Done对于新手来说比较容易出错
基于以上的背景,go-zero框架中为我们提供了并发处理工具MapReduce,该工具开箱即用,不需要做什么初始化,我们通过下图看下使用MapReduce和没使用的耗时对比:

相同的依赖,串行处理的话需要200ms,使用MapReduce后的耗时等于所有依赖中最大的耗时为100ms,可见MapReduce可以大大降低服务耗时,而且随着依赖的增加效果就会越明显,减少处理耗时的同时并不会增加服务器压力
并发处理工具MapReduce
MapReduce是Google提出的一个软件架构,用于大规模数据集的并行运算,go-zero中的MapReduce工具正是借鉴了这种架构思想
go-zero框架中的MapReduce工具主要用来对批量数据进行并发的处理,以此来提升服务的性能

我们通过几个示例来演示MapReduce的用法
MapReduce主要有三个参数,第一个参数为generate用以生产数据,第二个参数为mapper用以对数据进行处理,第三个参数为reducer用以对mapper后的数据做聚合返回,还可以通过opts选项设置并发处理的线程数量
场景一: 某些功能的结果往往需要依赖多个服务,比如商品详情的结果往往会依赖用户服务、库存服务、订单服务等等,一般被依赖的服务都是以rpc的形式对外提供,为了降低依赖的耗时我们往往需要对依赖做并行处理
func productDetail(uid, pid int64) (*ProductDetail, error) {
var pd ProductDetail
err := mr.Finish(func() (err error) {
pd.User, err = userRpc.User(uid)
return
}, func() (err error) {
pd.Store, err = storeRpc.Store(pid)
return
}, func() (err error) {
pd.Order, err = orderRpc.Order(pid)
return
})
if err != nil {
log.Printf("product detail error: %v", err)
return nil, err
}
return &pd, nil
}
该示例中返回商品详情依赖了多个服务获取数据,因此做并发的依赖处理,对接口的性能有很大的提升
场景二: 很多时候我们需要对一批数据进行处理,比如对一批用户id,效验每个用户的合法性并且效验过程中有一个出错就认为效验失败,返回的结果为效验合法的用户id
func checkLegal(uids []int64) ([]int64, error) {
r, err := mr.MapReduce(func(source chan<- interface{}) {
for _, uid := range uids {
source <- uid
}
}, func(item interface{}, writer mr.Writer, cancel func(error)) {
uid := item.(int64)
ok, err := check(uid)
if err != nil {
cancel(err)
}
if ok {
writer.Write(uid)
}
}, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) {
var uids []int64
for p := range pipe {
uids = append(uids, p.(int64))
}
writer.Write(uids)
})
if err != nil {
log.Printf("check error: %v", err)
return nil, err
}
return r.([]int64), nil
}
func check(uid int64) (bool, error) {
// do something check user legal
return true, nil
}
该示例中,如果check过程出现错误则通过cancel方法结束效验过程,并返回error整个效验过程结束,如果某个uid效验结果为false则最终结果不返回该uid
MapReduce使用注意事项
- mapper和reducer中都可以调用cancel,参数为error,调用后立即返回,返回结果为nil, error
- mapper中如果不调用writer.Write则item最终不会被reducer聚合
- reducer中如果不调用writer.Wirte则返回结果为nil, ErrReduceNoOutput
- reducer为单线程,所有mapper出来的结果在这里串行聚合
实现原理分析:
MapReduce中首先通过buildSource方法通过执行generate(参数为无缓冲channel)产生数据,并返回无缓冲的channel,mapper会从该channel中读取数据
func buildSource(generate GenerateFunc) chan interface{} {
source := make(chan interface{})
go func() {
defer close(source)
generate(source)
}()
return source
}
在MapReduceWithSource方法中定义了cancel方法,mapper和reducer中都可以调用该方法,调用后主线程收到close信号会立马返回
cancel := once(func(err error) {
if err != nil {
retErr.Set(err)
} else {
// 默认的error
retErr.Set(ErrCancelWithNil)
}
drain(source)
// 调用close(ouput)主线程收到Done信号,立马返回
finish()
})
在mapperDispatcher方法中调用了executeMappers,executeMappers消费buildSource产生的数据,每一个item都会起一个goroutine单独处理,默认最大并发数为16,可以通过WithWorkers进行设置
var wg sync.WaitGroup
defer func() {
wg.Wait() // 保证所有的item都处理完成
close(collector)
}()
pool := make(chan lang.PlaceholderType, workers)
writer := newGuardedWriter(collector, done) // 将mapper处理完的数据写入collector
for {
select {
case <-done: // 当调用了cancel会触发立即返回
return
case pool <- lang.Placeholder: // 控制最大并发数
item, ok := <-input
if !ok {
<-pool
return
}
wg.Add(1)
go func() {
defer func() {
wg.Done()
<-pool
}()
mapper(item, writer) // 对item进行处理,处理完调用writer.Write把结果写入collector对应的channel中
}()
}
}
reducer单goroutine对数mapper写入collector的数据进行处理,如果reducer中没有手动调用writer.Write则最终会执行finish方法对output进行close避免死锁
go func() {
defer func() {
if r := recover(); r != nil {
cancel(fmt.Errorf("%v", r))
} else {
finish()
}
}()
reducer(collector, writer, cancel)
}()
在该工具包中还提供了许多针对不同业务场景的方法,实现原理与MapReduce大同小异,感兴趣的同学可以查看源码学习
- MapReduceVoid 功能和MapReduce类似但没有结果返回只返回error
- Finish 处理固定数量的依赖,返回error,有一个error立即返回
- FinishVoid 和Finish方法功能类似,没有返回值
- Map 只做generate和mapper处理,返回channel
- MapVoid 和Map功能类似,无返回
本文主要介绍了go-zero框架中的MapReduce工具,在实际的项目中非常实用。用好工具对于提升服务性能和开发效率都有很大的帮助,希望本篇文章能给大家带来一些收获。
项目地址
https://github.com/tal-tech/go-zero
好未来技术
通过MapReduce降低服务响应时间的更多相关文章
- P95、P99.9百分位数值——服务响应时间的重要衡量指标
前段时间,在对系统进行改版后,经常会有用户投诉说页面响应较慢,我们看了看监控数据,发现从接口响应时间的平均值来看在500ms左右,也算符合要求,不至于像用户说的那么慢,岁很费解,后来观察其它的一些指标 ...
- 五、MapReduce 发布服务
是一个并行计算框架(计算的数据源比较广泛-HDFS.RDBMS.NoSQL),Hadoop的 MR模块充分利用了HDFS中所有数据节点(datanode)所在机器的内存.CUP以及少量磁盘完成对大数据 ...
- Mapreduce 历史服务 配置启动查看
如果没有进行配置的话,那个History是不可以进行点击的,点击进去就会报错!所以需要进行配置一下 使用命令启动HistoryServer 就可以查看任务执行的进度了 命令: sbin/mr-jobh ...
- 添加 K8S CPU limit 会降低服务性能
文章转载自:https://mp.weixin.qq.com/s/cR6MpQu-n1cwMbXmVaXqzQ
- 我用go-zero开发了第一个线上项目
作者:结冰 前言 说在最前面,我是一个外表谦让,内心狂热,外表斯文,内心贪玩的一个普通人.我的职业是程序员,是一个golang语言爱好者,一半是因为golang好用,一半是因为其他语言学不好.我是 ...
- 微服务和SOA服务
微服务和SOA都被认为是基于服务的架构,这意味着这两种架构模式都非常强调将“服务”作为其架构中的首要组件,用于实现各种功能(包括业务层面和非业务层面).微服务和SOA是两种差异很大的架构模式,但是他们 ...
- spring-cloud-hystrix服务熔断与降级
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联 ...
- Hbase理论&&hbase shell&&python操作hbase&&python通过mapreduce操作hbase
一.Hbase搭建: 二.理论知识介绍: 1Hbase介绍: Hbase是分布式.面向列的开源数据库(其实准确的说是面向列族).HDFS为Hbase提供可靠的底层数据存储服务,MapReduce为Hb ...
- NOS服务监控实践
本文来自网易云社区 作者:王健 一. 背景 此处所说的服务监控程序,是通过模拟用户的请求,对一个系统的服务质量进行监控的程序.服务监控程序的主要目的是,从用户的角度出发,通过发送端到端的请求,确认系 ...
随机推荐
- 关于pom.xml文件中配置jquery,以及如何在jsp中引入
pom.xml <!-- 对jquery的支持 --> <dependency> <groupId>org.webjars.bower</groupId> ...
- 下载安装gradle
1.登录官网:www.gradle.org,进入到下图的界面: 2.点击Install Gradle,跳转到下一个界面后: 3.下载 4.下载成功后,解压到任意位置,将路径添加到path路径下,选择我 ...
- AES字节数组加密解密流程
AES类时微软MSDN中最常用的加密类,微软官网也有例子,参考链接:https://docs.microsoft.com/zh-cn/dotnet/api/system.security.crypto ...
- linux下redis安装部署
1.获取redis资源 进官网下载https://redis.io/download最新版本后将文件移到linux环境中 或者直接wget http://download.redis.io/relea ...
- oracle之二检查点
检查点(checkpoint) 8.1 什么是checkpointcheckpoint是数据库的一个内部事件,检查点激活时会触发数据库写进程(DBWR),将数据缓冲区里的脏数据块写到数据文件中. 8. ...
- python中的一些内置函数
1.布尔类型 2.求和sum 3.取全局变量和局部变量 4.ascii码和字符集 chr().ord() 5.看某个功能下有哪些方法 help(x).dir(x) 6.exec执行python代码 7 ...
- leetcode1546题解【前缀和+贪心】
leetcode1546.和为目标值的最大数目不重叠非空子数组数目 题目链接 算法 前缀和+贪心 时间复杂度O(n). 1.对nums数组求前缀和: 2.在求前缀和过程中将前缀和sum插入到set集合 ...
- vue-router-next 通过hash模式访问页面不生效,直接刷新页面一直停留在根路由界面的解决办法
vue3中,配合的vueRouter版本更改为vue-router-next通过 npm i vue-router@next 的方式进行引入添加,随后创建 router.js,在main.js里面引入 ...
- java Synchronized集合
在Collections存在相关"Synchronized"支持同步的集合, 在java1.0 也存在"Vector"; 为什么会选择放弃"Vecto ...
- C#实现创建、编辑NX表达式
在NX8.5中使用C#编辑表达式中有个坑,part.Expressions.Edit该方法鲁棒性很差,当表达式有错时也能编辑成功(手动在NX中增加错误表达式会有弹框,无法创建,而该方法却可以,疑是bu ...