打造千万级流量系统——秒杀系统(项目初始化)

 技术选型:如何选择满足“三高”要求的技术?

技术选型原则

以我的经验,做技术选型,最好先以一些标准或原则来判断,这样会省力许多。下面是我实践中总结的 14 个字原则——简单易用可扩展,稳定高效成本低,希望对你有多帮助。

简单” 并不是指所用技术实现简单,而是指该技术逻辑和架构清晰明了。它可能是个很庞大的系统,但内部功能逻辑划分清晰,耦合度低,每一部分都可以独立使用。比如 Go Web 框架中的 Beego 框架,虽然功能很多,但每个功能都比较独立,且文档比较全,能让你很快明白。

易用” 主要是指上手容易,它是基于简单这个原则上的。除了功能逻辑耦合度低外,易用还要求所选技术的文档齐全,看文档或者命令提示就能学会如何用。另外,它还要求该技术对环境没有太多的依赖,社区也比较活跃,即使遇到问题也能随时找到人沟通问题。比如 Go 语言,就具备这些优点,有编程基础的程序员,看文档自学不到一周便能上手。

可扩展” 同样是基于简单这一原则之上。当你发现所选组件有些功能无法满足你的需求时,该组件容许你快速扩展功能。比如,你可以通过修改组件的源码,或者实现中间件注入它的框架中来实现扩展。像 Gin 框架和 gRPC 框架,都支持编写中间件注入框架中,以便扩展自定义的功能。

稳定” 是指什么呢?它意味着该技术已有功能变动小,Bug 少,生产环境运行可用性高,有固定的开发者持续维护等。比如 Go 语言从第一版到现在,基本做到了版本前向兼容,10 年前的代码用现在版本的 Go 编译器来编译,无须改动或者极少改动就能编译成功。在运行方面,Go 语言编写的程序也非常稳定,这得益于 Go 工具支持在开发阶段做各种代码检查,确保代码质量。

高效” 是从哪些方面来看的呢?主要有两方面:开发效率和运行效率。开发效率关系着项目的时间成本,一个需要快速上线的 API 项目,用 C++ 开发可能需要两周,用 Go 语言开发一周搞定,你会选哪个?

运行效率也就是性能,关系着项目的复杂度,如果一个项目用 Python 做可能需要 10 台机器,要配置负载均衡,需要用分布式锁,而用 Go 只需要一台机器,连负载均衡都可以不要,分布式锁也省了,你选哪个?相信在这两个场景中你都会选择 Go。

成本低” 主要是考虑长期维护成本,比如机器成本、运维成本、功能扩展成本等。其中,机器成本受单机性能影响,运维成本受架构复杂度影响,功能扩展成本受可扩展性影响。

假如用 1 台机器能搞定的事情,绝对不要用 10 台同等配置机器来做,否则机器成本和架构复杂度都会增加。如果一个框架可以通过注入插件的方式来扩展,另一个框架需要修改自身代码来扩展,那你应该优先选择前者,因为通常加代码要比改代码容易。

以上便是我从多年工作经历中总结出来的技术选型原则。但在实际当中,有些原则不能兼得,比如开发效率和性能之间就比较难兼顾,很多脚本语言开发效率高,但性能差。此时,就需要你活学活用,根据具体场景来分析,不能生搬硬套。

秒杀系统技术选型

前面是技术选型原则,那到具体场景当中该怎么做呢?接下来我们看秒杀系统如何做技术选型。总的来说,秒杀系统技术选型分三部分:语言环境、基础组件、设计模式。

语言环境选型

首先,开发一个系统必定需要用到某种计算机语言。语言的选型要考虑到很多方面,比如,该语言是否匹配研发团队的技术栈和能力,能否被团队新人快速掌握,也就是说能否满足“简单易用”的原则;该语言编写的程序能否在当前系统架构中稳定运行,能否扛住业务流量,能否快速定位问题,也就是能否满足“稳定高效”的原则。

经过从易用性、性能、稳定性等方面,我对比分析了 Java、C++、Go,最后决定用综合能力比较强的 Go 语言来实现秒杀系统。主要原因就是,它满足“简单易用,稳定高效”的原则。

Go 语言上手很快,有一两年经验的工程师不到一周就能上手写项目。在谷歌和云原生的推动下,Go 语言一直在稳定发展,社区也非常活跃,云原生技术也证明了 Go 语言的稳定和高性能。况且,由于秒杀对并发、性能、可用性的要求都非常高,Go 在这三个方面都非常优秀,这也是我们选择 Go 来实现秒杀系统的主要原因。

语言在各个阶段需要特定的环境,如开发环境、编译打包环境、运行环境等。当我们选定好语言后,需要为它的这几个环境制定相应规范,避免因环境差异而带来代码格式错乱、软件版本不兼容等麻烦。

在秒杀项目中,鉴于服务器运行环境大多是使用 Linux,我建议开发环境使用 Linux 或者 Mac,与线上运行环境保持一致。至于 IDE ,我推荐使用 Goland,搭配 goimports、golint 等工具自动检查代码,这样可以不用把精力浪费在折腾 Vim、VSCode 的插件上。

至于编译打包环境,推荐 GitLab CI 的方式,可以用它的 Pipeline 做单元测试、代码检查等工作,提升工作效率。至于运行环境,我建议直接用云主机部署,系统使用 Linux。因为我们需要为秒杀服务做系统参数优化,以便提供最好的性能。

基础组件选型

秒杀用到的基础组件,主要有框架、KV 存储、关系型数据库、MQ

框架主要有 Web 框架和 RPC 框架。

其中,Web 框架主要用于提供 HTTP 接口给浏览器访问,所以 Web 框架的选型在秒杀服务中非常重要。在这里,我推荐Gin,它的性能和易用性都不错,在 GitHub 上的 Star 达到了 44k。有关各个 Web 框架的性能对比,你还可以参考 GitHub 上的 go-web-framework-benchmark 项目。

你可能要问了,我为什么没有选择性能最好的 fasthttp,而是选择 Gin?这是因为,虽然 fasthttp 在请求延迟低于 10ms 时性能优势明显,但其底层使用的对象池容易让人踩坑,导致其易用性较差。如果没用好,服务在高并发下容易出现奇怪的问题,从而引发故障。

从 go-web-framework-benchmark 项目中的性能测试结果里你也能发现,当请求延迟在 10ms 以上时,Gin 与 fasthttp 的性能差距也越来越小,所以没必要过于追求性能而忽略了稳定性。对于研发人员来说,在技术选型时找好性能与稳定性之间的平衡点是很重要的事情。

至于 RPC 框架,我推荐选用 gRPC,因为它的扩展性和性能都非常不错。在秒杀系统中,Redis 中的数据主要是给秒杀接口服务使用。为了保证缓存功能的高内聚低耦合,我们将用 gRPC 在秒杀接口服务中实现配置同步接口,供秒杀管理后台调用,以便将配置从管理后台同步到 Redis 缓存中。

KV 存储方面,秒杀系统中主要是用 Redis 缓存活动配置,用 etcd 存储集群信息。

关系型数据库中,MySQL 技术成熟且稳定可靠,秒杀系统用它存储活动配置数据很合适。主要原因还是秒杀活动信息和库存数据都缓存在 Redis 中,活动过程中秒杀服务不操作数据库, 使用 MySQL 完全能够满足需求。

MQ 有很多种,其中 Kafka 在业界认可度最高,技术也非常成熟,性能很不错,非常适合用在秒杀系统中。而且,Kafka 支持自动创建队列,秒杀服务各个节点可以用它自动创建属于自己的队列。

介绍完基础组件的选型后,接下来就需要考虑如何写代码了,而这就涉及设计模式的选型了。

设计模式选型

设计模式总共有 23 种,我们该如何选取呢?

在秒杀系统中,我们将使用拦截过滤器模式、组合模式、抽象工厂模式、观察者模式等几种设计模式。 接下来我给你介绍下它们都分别用在什么地方。

拦截过滤器模式的作用,你光看名字就能看出来。没错,我们将在秒杀系统中用它来实现漏斗模型中的流量拦截器。我在上一讲中提到,除了网关和 WAF 的拦截器外,秒杀服务自身有两层拦截器。因此我们需要用拦截过滤器模式将它们组织起来,自上而下形成一条链式处理流程。当请求被链式处理流程中的某个拦截器拦截掉,就表示该请求失败了,不进行后续处理。

组合模式主要用于活动场次信息的逻辑中,因为活动场次信息在 DDD 中是聚合根,聚合了活动专题信息、商品信息、库存信息等数据。

抽象工厂模式在秒杀中有两处作用,一个是用于对多种流量拦截器的抽象设计,另一个是对读写内存队列和读写 MQ 的抽象设计。我们将在秒杀服务中设计出 Interceptor 和 Queue 这两种抽象接口类,并提供对应的构造函数给工厂方法调用。

观察者模式主要用于管理秒杀集群信息的同步逻辑,比如当管理后台修改了限流速度后,需要将限流速度更新到各个 Queue 对象中。每种队列关心的限流配置不一样,因此需要从 etcd 中订阅不同的配置。

以上便是秒杀系统中设计模式的选型。关于设计模式本身的具体内容,网上资料很多,这里不详细介绍。秒杀中各设计模式的代码,我将会在后面的代码实战环节给你详细介绍。

技术选型的最终结果是形成开发视图,这是我们得到的开发视图:

项目规范:Go 项目初始化都有哪些规范?

目录结构规范
很多人会觉得奇怪,目录结构有什么好介绍的,不就是创建个目录嘛?其实,这里面有很大的讲究。因为,规划好目录结构是初始化一个项目的第一步。

众所周知,很多语言都可以通过引入依赖包,也就是公共库的方式来快速实现功能,也可以通过将代码导出为依赖包,实现代码复用。比如 Java、Python、PHP 都有自己的依赖包管理器, Go 也类似。通常 Go 的包名称跟代码所在目录名称是一样的。如果目录结构规划得不好,会出现什么问题呢?

首先,可能会存在依赖关系混乱的问题,比如循环依赖和反向依赖。

什么是循环依赖呢?举个例子,位于同层级中的 A、B、C 三个包,A 依赖 B,B 依赖 C,结果后面写代码的时候发现 C 还要依赖 A,这就造成循环依赖了。在 Go 语言中,循环依赖会导致项目编译失败,编译器会提醒你解决循环依赖的问题。

反向依赖又是怎样的呢?比如 A 和 C 位于第一层,B 和 D 位于第二层,正常情况下 A 依赖 B,C 依赖 D,如果 B 依赖了 C,就构成反向依赖,导致层级关系错乱。

其次,可能会导致阅读代码的时候非常不方便。

假如项目中有 100 个源代码文件,如果都放到一个目录下,用 IDE 看代码的时候,文件列表将会变得很长,需要上下滚动才能找到想看的文件。这就好比你的电脑桌面铺满了各种文件,要从中找出想用的文件将会很费时。

这两个问题该如何解决呢?其实项目的目录结构有一些规范可遵循。通常,可编译运行的开源 Go 项目倾向于使用下面的目录结构:

├── cmd/
├── config/
├── docs/
├── internal/
├── pkg/
├── script/
├── static/
├── go.mod
├── README.md
├── LICENSE.md
├── Makefile

其中 cmd 目录用于存放可执行程序的入口源码文件,config 目录存放配置文件,docs 目录存放文档。internal 目录中存放的是仅用于项目内部的源码,而 pkg 目录存放的则是可以被其他项目引用的源码。script 目录用于存放脚本文件,比如用于编译程序的脚本文件。static 目录中存放的是诸如网页源文件或者图片之类的静态资源文件。

除了以上几个目录外,Go 项目根目录中还有几个常见的文件,主要是 go.mod、README.md、LICENSE.md 和 Makefile 。

其中,go.mod 用于管理外部依赖关系,通常通过go mod init命令生成。假如你的代码中引用了 github.com/hello/world 包,你需要在 go.mod 中添加对这个包的依赖关系。如果你使用 Goland 开发 Go 项目,Goland 将会帮你自动更新 go.mod 文件。注意,别忘了在 Goland 中开启 Go Modules 功能,配置如下:

README.md 和 LICENSE.md 一个是说明文档的入口文件,一个是源码分发许可文件。 最后一个 Makefile 则是执行 make 命令时的配置文件,常用于执行编译、单元测试、运行等操作。

除了开源项目的规范外,还有其他规范吗?有!那就是之前给你介绍的 DDD,它主要用于业务系统项目。如果你的项目按照 DDD 来设计,那么项目的目录结构要体现出 DDD 的思想。

还记得 DDD 的分层设计吗?如果按照 DDD 分层设计,那么目录结构中需要包含以下内容:

├── interfaces/
├── application/
├── domain/
├── infrastructure/

其中infrastructure存放基础设施相关的代码,如秒杀系统中的通用函数、用于访问 Redis 和 MySQL 的代码等。它位于 DDD 分层设计中的最底层,其他层都可以依赖它。

domain中存放的是领域的定义和核心逻辑,类似于 MVC 模型中的 Model,比如秒杀系统中的活动信息定义和活动相关的处理逻辑。它可以引用 infrastructure 从存储中获取数据并进行处理,也可以从上层接收并处理数据,给上层返回处理结果。

application用于存放请求数据的整体控制逻辑,有点类似于 MVC 模型中的 controller,比如秒杀接口服务中获取活动信息的入口函数。它主要接收上游请求,并控制数据的整体处理流程。

interfaces目录主要存放接口定义和初始化代码,比如秒杀接口服务中 HTTP 接口定义和路由初始化逻辑。它主要引用 application,将请求转给 application 处理。

在秒杀项目中,我们将开源 Go 项目和 DDD 项目的特点结合起来,使用 cmd 目录存放秒杀服务的入口代码,提供 api、admin、bench 等子命令。

最终,秒杀系统的目录结构大致如下:

├── README.md
├── go.mod
├── LICENSE
├── Makefile
├── main.go
├── cmd/
│   ├── api.go
│   ├── admin.go
│   ├── bench.go
│   └── root.go
├── interfaces/
│   ├── admin/
│   ├── api/
│   └── rpc/
├── application/
│   ├── api/
│   └── admin/
├── domain/
│   ├── event/
│   ├── user/
│   ├── product/
│   └── stock/
├── infrastructure/
│   ├── utils/
│   ├── logger/
│   ├── metrics/
│   ├── redis/
│   ├── mysql/
│   ├── etcd/
│   └── kafka/
├── config
│   ├── seckill.ini
│   └── seckill.toml
└── docs
   ├── api.md
    ├── admin.md
   └── bench.md

命令行参数规范
前面我提到了使用 cmd 目录存放秒杀程序入口代码,用于管理秒杀程序的子命令。那么,秒杀服务命令行参数应该怎么设计呢?

我们先看下其他程序的命令行参数是如何的。以我电脑上的一个 Go 程序 cobra 为例子,执行 ./cobra -h 后,结果如下:

从截图上可以看到,输出的结果中主要包括三部分:Usage、Available Commands、Flags。其中 Usage 是告诉你该命令总的用法, Available Commands 告诉你有哪些子命令,Flags 则是告诉你可以用哪些参数。

看完后你有没有觉得整个输出结果让人一目了然?

总的来说,一个优秀的程序命令由三部分组成:主命令、子命令、参数。主命令是整个程序的入口,子命令是程序内各种主要的功能,参数则是告诉程序如何执行这些功能。

对应到秒杀服务中,主命令应该是seckill。秒杀服务包括秒杀接口服务和秒杀管理后台后端,它们的子命令分别是api和admin。另外,为了方便做一些比较复杂的压力测试,我们还可以提供一个bench命令。

这些子命令如何管理呢?这里我给你推荐一款非常好用的工具cobra,用它可以生成解析命令行参数的代码。你可以执行go get -u github.com/spf13/cobra安装该工具。

我们该如何使用 cobra 命令生成代码呢?在项目根目录下执行以下命令:

cobra init seckill
cobra add api
cobra add admin
cobra add bench

我们就获得了 cmd/root.go、cmd/api.go、cmd/admin.go、cmd/bench.go。他们分别是主命令 seckill、子命令 api、admin、bench 的入口文件。
打开 cmd/root.go,你将看到如下两段代码

var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
   Use:   "seckill",
   Short: "A brief description of your application",
   Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
   // Uncomment the following line if your bare application
   // has an action associated with it:
   //Run: func(cmd *cobra.Command, args []string) { },
}
func init() {
   cobra.OnInitialize(initConfig)
   // Here you will define your flags and configuration settings.
   // Cobra supports persistent flags, which, if defined here,
   // will be global for your application.
   rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.seckill.yaml)")
   // Cobra also supports local flags, which will only run
   // when this action is called directly.
   rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

其中 rootCmd 是主命令的配置,类型是 *cobra.Command。它的 Use 字段是主命令名称,Short 字段是对命令的简短描述,Long 字段是对命令的详细描述。Short 字段和 Long 字段目前还是默认值,需要修改成秒杀命令的描述。

需要注意的是,rootCmd 中的 Run 字段默认是被注释掉的。该字段是 cobra 解析完命令行参数后正式执行功能的入口函数,你可以开启它,并在其中根据参数做一些初始化的工作。

在 init 函数中,cobra 会执行配置的初始化,并解析命令行参数。在生成的代码中,已经提供了 config 参数,用于指定配置文件路径。通常,命令行参数有长参数和短参数这两种格式,这里的 config 是长参数,在使用的时候是 --config 这种形式,而后面的 c 是短参数,使用的时候是 -c 这种形式。

cmd 目录下的 api.go、admin.go、bench.go 也类似,区别在于各子命令的 cmd 配置中 Run 字段是有个默认函数的。并且,在 init 函数中,会有类似 rootCmd.AddCommand(adminCmd) 的代码将子命令的配置添加到主命令的配置中。

接下来,我们可以在项目的根目录下添加 Makefile,用于编译程序。内容如下:

all: build
build:
   go build -o seckill main.go
clean:
   rm seckill
.PHONY: clean build all

其中 all 指令是 Makefile 的入口,也是执行 make 命令的默认行为,它依赖了 build 指令。build 指令用于编译出可执行文件 seckill ,而 clean 指令则是用于清理编译好的 seckill 文件。 .PHONY 是做什么用的呢?由于 Makefile 中依赖关系默认都是文件间的依赖,因此当目录中存在与 Makefile 中指令相同的文件时,可能会导致 make 命令无法正常执行 Makefile 中的指令,而 .PHONY 则是为了解决这个问题。

有了 Makefile 后,我们就可以在项目根目录下执行 make、make all 或者 make build 来编译出 seckill。接下来,我们执行 ./seckill help 命令便可以看到 seckill 的帮助信息。如下所示:

A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
 seckill [command]
Available Commands:
 admin       A brief description of your command
 api         A brief description of your command
 bench       A brief description of your command
 help        Help about any command
Flags:
 -c, --config string   config file (default is $HOME/.seckill.yaml)
 -h, --help            help for seckill
 -t, --toggle          Help message for toggle
Use "seckill [command] --help" for more information about a command.

引自:拉勾打造千万级流量系统

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值