• 打造千万级流量秒杀第二十九课 预热和压测:SLB 预热和压测的意义及方法


    前面我为你介绍了秒杀系统的开发和本地测试,测试通过后,我们还需要将服务部署到线上环境,并在活动开始前一天进行 SLB 预热和压力测试。为什么呢?这是因为大型促销活动流量非常大,需要确保线上环境的各资源能扛住压力,从而保障活动期间不出故障。但由于秒杀系统能承载的 QPS 太高了,在压力测试中普通的压测工具根本无法满足要求,于是就需要用到分布式压测工具了。

    所以,接下来我就为你详细介绍下如何做 SLB 预热和分布式压测。

    分布式压测

    上一讲我实现了一个 bench 命令用于压测秒杀 API 服务,不过它是单节点压测工具,性能比较有限,无法压测秒杀集群,我们需要将它改造成分布式压测系统。

    什么是分布式压测系统呢?简单来说,分布式压测系统是利用多个服务器节点,在集群管理单元的协调下,同时向被测服务发起压力测试,以便给被测服务制造超高压力。

    分布式压测系统原理

    分布式压测系统需要满足什么样的基本条件呢?

    第一,并发请求能力,这是压测工具最基本的功能。

    第二,数据统计能力。压测工具在发起并发请求后,需要收集各请求的性能指标,比如请求延迟、QPS、错误率等。

    第三,水平扩展能力。分布式压测系统要能压测不同并发能力的被测系统,这要求它必须能够通过水平扩展来控制自身并发能力。

    第四,集群管理能力。分布式压测系统本身是有多个节点的,它需要有一个控制中心来统一控制各节点的任务下发和数据统计,以便让各节点同时开始任务,并在任务结束后上报统计结果。

    分布式压测系统设计与实现

    基于前面我提到的四点要求,一个分布式压测系统该如何设计呢?分布式压测系统重点体现在“分布式”上,它主要包括任务管理单元和任务执行单元。其中,任务管理单元负责配置任务,并下发给各个任务执行单元。这个任务下发其实就是配置下发,也就是说我们也可以用 ETCD 来实现配置同步。

    具体同步哪些配置呢?前面我实现的 bench 命令主要用了并发数、请求数、url、keepalive 这 4 个参数,但一个通用的压测系统还有更多的参数。比如当压测系统需要测一个 POST 接口时,它需要指定数据以及数据类型,比如指定数据类型为 "application/json"。另外,每次下发任务需要有一个唯一的版本号,以便各执行单元能按照版本号汇总测试结果。

    由于参数较多,而且需要通过 ETCD 同步配置,因此我们需要定义一个结构体来保存这些参数。比如我定义了一个 Task 结构体,它包含这几个字段:

    1. ID 字段,用于表示唯一任务,我们可以简单地用时间戳当任务 ID,毕竟同一时间不会进行两场压力测试。

    2. Servers 字段,表示被测服务的地址。它有可能是个域名(如 "event.test.com"),也有可能是一批 IP 加端口的列表(如:"192.168.1.2:8080,192.168.1.3:8080"),使用它压测服务既能通过 DNS 压测,又能绕过 DNS 压测。

    3. Path 字段,表示被测接口的路径,也就是哪个接口,如 /event/list。

    4. Method 字段,表示被测接口方法名 ,它的值可能是 POST、GET 还是 PUT 方法等。

    5. Data 字段,表示压测接口需要的数据,如 json 格式的数据 {"event_id: 123,"goods_id":456}。

    6. ContentType 字段,用于表示压测接口需要的数据格式,如 "application/json"。

    7. Concurrency 字段,用于表示并发数,如 100。

    8. Number 字段,用于表示请求数的,如 1000000(一百万)。

    9. Duration 字段,用于表示压测时长的,如 300(秒)。

    10. Status 字段,用于表示任务状态,0 为未开始,1 为执行中,2 为停止中,3 为已停止。

    具体代码如下所示:

    type Task struct {
       ID          int      `json:"id"`
       Servers     []string `json:"servers"`
       Path        string   `json:"path"`
       Method      string   `json:"method"`
       Data        string   `json:"data"`
       ContentType string   `json:"content_type"`
       Concurrency int      `json:"concurrency"`
       Number      int      `json:"number"`
       Duration    int      `json:"duration"`
       Status      int      `json:"status"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    为了便于任务管理单元将结构体编码成 json 字符串存放到 ETCD,然后执行单元从 ETCD 中取出配置并从 json 字符串解码到结构体,我给结构体中每个字段都加上了 json tag。

    接下来,我们就可以实现从 ETCD 读写配置的代码。

    读配置的代码实现比较简单,之前我们已经实现了秒杀集群的配置同步逻辑,我们直接复制过来修改下即可。大致实现逻辑如下:

    1. 主要是将 ETCD key 改成压测系统用的 key /bench/task/config;

    2. 将 tmpConfig 类型改成 Task;

    3. 给函数加一个参数 callback 用于通知程序在配置更新后执行相应操作,比如结束当前正在进行的测试并开始新的测试;

    4. 移除启动时从 ETCD 初始化配置的代码,避免启动时执行任务。

    5. 最终我们得到一个用于从 ETCD 中同步压测系统配置的函数 watchTask。

    代码示例如下所示:

    func watchTaskConfig(callback func(cfg Task)) error {
       var err error
       cli := etcd.GetClient()
       key := "/bench/task/config"
       update := func(kv *mvccpb.KeyValue) (bool, error) {
          if string(kv.Key) == key {
             var tmpConfig Task
             err = json.Unmarshal(kv.Value, &tmpConfig)
             if err != nil {
                logrus.Error("update bench config failed, error:", err)
                return false, err
             }
             logrus.Info("update bench config ", tmpConfig)
             callback(tmpConfig)
             return true, nil
          }
          return false, nil
       }
       watchCh := cli.Watch(context.Background(), key)
       for resp := range watchCh {
          for _, evt := range resp.Events {
             if evt.Type == etcdv3.EventTypePut {
                if ok, err := update(evt.Kv); ok {
                   break
                } else if err != nil {
                   break
                }
             }
          }
       }
       return nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    接下来我们需要改造 doBench 函数,支持结束当前任务,开始执行新任务。具体思路是让其成为 Task 的方法,在函数里面判断 Status 的值。如果 Status 值不为 1,则退出当前任务并更新 Status 值为 3。代码示例如下:

    func (t *Task) doBench() {
       wg := &sync.WaitGroup{}
       wg.Add(t.Concurrency)
       for i := 0; i < t.Concurrency; i++ {
          go func() {
             for atomic.LoadInt32(&t.Status) == 1 {
                // do test
             }
             atomic.StoreInt32(&t.Status, 3)
             wg.Done()
          }()
       }
       wg.Wait()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    然后,我们需要有一个任务管理器 TaskManager,它有一个字段 task 用于表示当前正在执行的任务,以及一个互斥锁用于更新当前任务。它还需要有一个 onConfigChange 方法用于处理配置变更,如果当前任务 Status 为 1,需要将其修改为 2,并等待状态变为 3 后开始执行新的任务。具体代码如下:

    type TaskManager struct {
       sync.Mutex
       task Task
    }
    func (tm *TaskManager) onConfigChange(task Task) {
       if atomic.LoadInt32(&tm.task.Status) == 1 {
          atomic.StoreInt32(&tm.task.Status, 2)
       }
       for atomic.LoadInt32(&tm.task.Status) == 2 {
          time.Sleep(time.Second)
       }
       tm.Lock()
       tm.task = task
       tm.Unlock()
       if task.Status == 1 {
          tm.task.doBench()
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    最后,我们就可以在 Run 方法中添加一行代码 watchTaskConfig((&TaskManager{}).onConfigChange) 来监控配置变更并更新任务状态。

    至于将配置写入到 ETCD 的方法,有很多种。可以用 etcdctl 命令,可以用代码来控制。当然,比较好的方式还是用代码实现一个管理后台,以便后续可以扩展更多方便的功能。

    SLB 预热

    SLB 作为 DNS 后第一层负载均衡器,它的性能在整个系统中非常关键,通常是由云厂商提供。比如 AWS 的弹性负载均衡器 ELB 就是 SLB,分为 NLB 和 ALB,它会根据流量大小自动扩容、缩容。

    为什么需要在活动前对 SLB 做预热呢?那是因为平常流量小,云厂商会为 SLB 分配比较小的资源,当大流量来的时候,才会逐渐扩容。但是,扩容是需要时间的,像秒杀这么大的流量,扩容时间可能持续好几分钟。而秒杀的大流量通常是在秒杀活动开始前后 1 分钟内,此时扩容速度是远赶不上流量增长速度的。如果流量超过 SLB 承载能力,就会触发 SLB 的限流,影响活动效果。因此,通常需要提前制造压力触发 SLB 自动扩容,这便是 SLB 预热。

    SLB 预热需要注意哪些事情呢?

    首先需要跟云厂商确认两个参数:

    1. 流量降下来多久后 SLB 会自动缩容,以便确认预热时间安排在活动开始前多少时间,以防预热过早导致后续流量下降后 SLB 缩容;

    2. 单个 SLB 集群最多能承载多少流量,以便确认活动期间是否要采用多个 SLB,以防单个 SLB 容量不足。

    其次,需要分别压测以下几个场景:

    1. 带 DNS 通过 SLB 压测秒杀 API 服务;

    2. 不带 DNS 通过 SLB 压测秒杀 API 服务;

    3. 直接压测秒杀 API 服务。

    之所以要测这几个场景,主要是为了对比看性能瓶颈在什么地方。理想情况下,直接压测秒杀 API 服务应该是性能最好的,其次是不带 DNS 压测,但这三种场景的结果不应该相差很大。如果第一种相比第二种 QPS 小很多,那么瓶颈在 DNS,需要想办法优化它。同理,如果第 2 种比第 3 种小很多,那么瓶颈在 SLB,同样需要想办法优化。如果是 DNS 的瓶颈,则需要考虑使用 CDN 做就近解析。如果是 SLB 的瓶颈,则需要考虑使用多个 SLB,使用 DDNS 轮询做负载均衡。

    Drawing 0.png

    小结

    这一讲我给你介绍了分布式压测系统的原理、压测过程的注意事项以及 SLB 预热,这是本专栏秒杀系统的最后一讲,也是秒杀系统项目最后一步。希望你在学完这一讲后,能自己手动实现一个简单的分布式压测系统。在实现分布式压测系统的过程,你将会加深对“三高”架构的理解。

    接下来给你出个思考题:如果要绕过 SLB 直接压测秒杀系统多个节点,压测工具该如何实现呢?期待你在留言区讨论哦!

    好了,这一讲就到这里了。下一讲作为结束语,我将结合自己的经历,和你聊聊程序员成长之路。希望对你的职业发展有所帮助。

    源码地址:
    https://github.com/lagoueduCol/MiaoSha-Yiletian/blob/main/cmd/bench.go


    你好!很高兴你坚持下来学完了本专栏。“三高”架构对于程序员和架构师来说,是升职加薪必不可少的技能,接下来你要如何规划自己的职业发展,掌握更多这方面的技能呢?

    你还记得吗?我在开篇词里提到过,我已经工作十多年了。我于 2008 年毕业, 2010 年正式踏入软件行业,到 2021 年已经有 11 年软件开发从业经验了。我曾经在2017年加入小米,深度参与了国际小米网秒杀系统的性能优化,并在后面晋升为国际小米网基础服务技术负责人。

    可能你会好奇,为何我毕业后不是直接从事软件行业。实际上,我的大学专业跟计算机毫不沾边,我是中途转行当程序员的。那么,我是如何一步一步成为资深工程师,并积累了丰富的“三高”架构经验呢?

    接下来,我想以自己的经历,跟你分享下我对程序员这一职业的一些感想和经验,希望对你的职业发展有所帮助。

    决定转行,制定目标

    当我刚从大学毕业时,我对未来是有些迷茫的。因为所学专业就业面比较窄,毕业后从事的并不是自己喜欢的工作,薪资也比较低。由于我大学期间比较喜欢编程,刚好对毕业后的工作并不是很满意,于是在工作两年后作出了决定,走上了程序员之路。

    但是,这条路该怎么走,其实我一开始也不是很清楚。不过,当我了解到程序员是个高薪行业时,我给自己定了个简单粗暴的目标:薪资每两年翻一番

    可能有人觉得这样的目标有点夸张,但有句话说得好:谋其上者取其中,谋其中者取其下。什么意思呢?意思是如果设置一个高目标,通过努力,你至少能达到中等水平,但如果你定了个中等目标,可能只会达到低等水平。因此,当你定目标的时候,眼光要方长远点,不要定一个踮起脚就能够着的目标,而是要定一个需要跳起来才能达成的目标。比如当我定了每两年薪资翻一番的目标后,我就知道自己需要为这个目标付出多少努力,我需要思考如何规划自己的职业。

    秒杀系统 结束语--金句.png

    实际上,我的工作前 8 年薪资基本上是按照每两年半翻一番的。虽然没达到两年翻一番,但也算很不错了。如果我的目标定的不是两年,而是三年,我可能需要花三年半才能让薪资翻番。

    持之以恒,抓紧学习

    当定好目标后,就需要付诸行动了。作为非科班出身的我,很清楚自己跟科班生的差距。于是,我开始给自己制定学习计划了。

    决定转行当程序员的第一年,当时我对 Linux 非常感兴趣,于是给自己制定了为期三个月的高强度学习目标,主要包括各种 Linux 命令、《UNIX 环境高级编程》(简称 APUE)。靠这三个月的学习成果,我找到了一份 Linux 系统编程的工作,薪资是我第一份工作的两倍,算是正式踏上了程序员之路了。

    之后我在学习上并没有松懈,仍然持之以恒抓紧时间学习。除了在工作时间内研究各种开源软件代码外,我还坚持用半年时间在公交、地铁上看完了《大话设计模式》《代码整洁之道》《代码大全》等经典书籍。这些书籍对我提升编码能力起了很大作用,因为它们介绍的是编程思想和理念,其作用要比学一门语言或者一个工具大得多。后续在涉及“三高”的项目中,这些知识为我编写高质量代码提供了强大的理论支撑。

    学以致用,快速成长

    知识要学以致用,如果只是学了但不去用,就容易荒废。软件开发更是讲究动手能力的职业,需要积累大量的编码经验。

    我还记得我在三年前优化秒杀性能的时候,遇到一个秒杀 CPU 打满的问题。当时有同事排查了一天也没排查出原因,找我一起排查。由于之前看过秒杀服务用的 Beego 框架的源码,知道 Beego 框架的路由用到了正则匹配,我初步判断性能瓶颈是在路由正则匹配上。后来我用 pprof 这个性能分析大杀器,提取压测时的性能数据,通过分析证实了我的判断。于是我们修改代码,将请求过滤器放到路由前面提前拦截请求,一举解决了 CPU 打满的问题。

    在后续的工作中,我都始终学以致用,不放过任何一个可以实践的机会。比如我严格遵守代码命名规则、最小改动原则,以及面向对象的五大原则:单一职责原则、开闭原则、接口隔离原则、里氏替换原则和依赖倒置原则。

    这些原则有多重要呢?很多工作五六年的程序员和工作三年的差不多,写的代码极难维护,主要原因就是没有对这些原则学以致用。他们总是会以项目时间紧为借口,直接采用快捷键 Ctrl+C 复制然后 Ctrl+V 粘贴的方式编写代码,人称“CV 工程师”。

    定期总结,经验内化

    除了学以致用外,定期总结和分享也很重要。总结是提炼核心知识的主要方式,而给人分享的过程也是自我总结的过程。

    总结的方式有很多种,可以是写一篇博客,画一张脑图,给团队做一次内部分享,或者写一个开源软件。**定期总结能将你学的知识沉淀下来成为自己的经验。**如果你能建立自己的知识体系,你的工作效率也会得到极大提升。

    就我个人感受来说,工作越久越能体会到定期总结的好处。因为随着工作时间越来越长,你所接触的知识越来越多,对这些知识做总结就好比给一个数据库加索引,能提升查询效率。比如我在做秒杀系统设计之前,就用到了脑图总结秒杀系统的功能清单和所用到的技术。因为学的知识越多越难整理,而脑图比较简洁,对整理核心知识非常有帮助,为后面的方案设计和代码实现提供了很多灵感。

    另外,随着你做的总结和分享越来越多,你也能在技术圈里提升自己的影响力,这在你的职业发展中将会有很大帮助。

    极客精神,追求极致

    当你做到了前面四点后——制定高目标、持续学习、学以致用、定期总结,你的工作效率相比刚毕业的两年内会有很大提升,知识面也越来越全。但是,当你会的越多,你会看到你不会的更多了,因为以前有些东西你压根不知道它的存在。有时候你会感觉自己的成长变慢了,会焦虑。这个时候,你需要做的不是马上去学更多的东西,因为毕竟精力有限。你要做的是在众多技术中选一座山,努力爬上它的顶端,成为这个方向的专家。

    要做到这点并不容易,你需要有极客精神,需要非常强的专注力,对做的事情追求极致。举个例子,你好不容易花了一个月开发了一套可用的系统,能打 80 分,你是否愿意再花一个月时间去改善它的性能、用户体验,完善剩下的 20 分呢?很多人做不到这一点,因为他们会因为各种原因而不愿意把时间花在一个已经能用的系统上。但是,刚好是这 20 分决定了普通程序员跟专家的差距。

    好了,本专栏有关打造千万级流量秒杀系统到这里就结束了。但我希望这是你学习和掌握秒杀系统高可用、高性能、高并发的起点,就像我前面提到的那样,要学以致用,定期总结,持续成长,正如乔布斯那句名言那样:“Stay hungry, Stay foolish”。

    最后,我邀请你为本专栏进行一个结课评价,因为你的每一份回复和评价,都是我和拉勾教育最想了解,以及将来努力的方向。


    精选评论

    **施:

    当听到老师转行都能这样坚持时,我一个大学计算机的为什么还有这么多借口推脱!好好反省自己吧!力争上游!冲冲冲!!!卷不动都是假的!!!!

    XX:

    感谢老师奉献了自己的3高宝贵经验,收获很大!

  • 相关阅读:
    数据库中间件-mycat-1-搭建
    15.前端笔记-CSS-PS切图
    11.22IG客户情绪报告: 黄金、原油、澳元、日元、欧元、英镑
    【LearnOpenGL基础入门——2】搭建第一个OpenGL窗口
    有点奇怪!访问目的网址,主机能容器却不行
    qDebug() 显示行号
    如何使用 MySQL 做全文检索这件事
    flask学习
    Android 启动service(Kotlin)
    tcpreplay命令后加上“--maxsleep=num“,num表示最大延迟时间(单位毫秒)
  • 原文地址:https://blog.csdn.net/fegus/article/details/126378359