Go语言Web编程有哪些优势?

1.Go语言之前编程的痛点

(1)为什么会设计Go语言?

我们先来了解一下Go的作者和主要核心开发者们:Robert Griesemer, Rob Pike 和 Ken Thompson。设计Go语言是为了解决当时Google开发遇到的以下这些问题:

  • 大量的C++代码,同时又引入了Java和Python;
  • 成千上万的工程师;
  • 数以万计行的代码;
  • 分布式的编译系统;
  • 数百万的服务器;

其主要有以下几个方面的痛点:

  • 编译慢;
  • 失控的依赖;
  • 每个工程师只是用了一个语言里面的一部分;
  • 程序难以维护(可读性差、文档不清晰等);
  • 更新的花费越来越长;
  • 交叉编译困难;

所以,他们当时设计Go的目标是为了消除各种缓慢和笨重、改进各种低效和扩展性。Go是由那些开发大型系统的人设计的,同时也是为了这些人服务的;它是为了解决工程上的问题,不是为了研究语言设计;它还是为了让我们的编程变得更舒适和方便。

但是结合Google当时内部的一些现实情况,如很多工程师都是C系的,所以新设计的语言一定要易学习,最好是C-like的语言;因为有太多的分布式系统、太多的开发者,所以新的语言一定要可以Scale,这个包括开发、工程师、代码、部署和依赖;20年没有出新的语言了,所以新设计的语言必须是现代化的(例如内置GC)等情况,他们觉得要实现这个目标就需要Go成为一个大家都认可的语言。

最后根据实战经验,他们向着目标设计了Go这个语言,其主要的特色有:

  • 没有继承的OO;
  • 强一致类型;
  • Interface但是不需要显示申明(Duck Type);
  • Function 和Method;
  • 没有异常处理(Error is value);
  • 基于首字母的可访问特性;
  • 不用的Import或者变量引起编译错误;
  • 完整而卓越的标准库包;

Go发布之后,很多公司特别是云计算公司开始用Go重构他们的基础架构,很多都是直接采用Go进行了开发。这几年火到爆的Docker、Kubernetes就是采用Go开发的。我们来看看目前为止采用Go的一些国内外公司,国外的如Google、Docker、Apple、Cloud Foundry、CloudFlare、Couchbase、CoreOS、Dropbox、MongoDB、AWS等公司,国内的如阿里云CDN、百度、小米、七牛云、PingCAP、华为、金山软件、猎豹移动、饿了么等公司。

(2)Go主要应用的系统。

上面那些基本上就是Go的历史背景和设计初衷,那么目前Go主要应用于哪些系统呢?

目前Go主要应用在下面这些系统:

①Web服务器编程,以前你如果使用C或者C++做的那些事情,用Go来做很合适,例如处理日志、数据打包、虚拟机处理、文件系统等。

②容器开发:Docker,使开发高效且可预测;Kubernetes,也称为K8s,是一个开源系统,用于自动化容器化应用程序的部署,扩展和管理。

③微服务、分布式系统、数据库代理器等,例如gRPC、Etcd等。

④Web网络编程,这一块目前应用最广,包括Web应用、API应用、下载应用,而且Go内置的net/http包足够强大,基本就涵盖了常用的Web应用方法了。

⑤数据库,前一段时间Google开发的Groupcache,Couchbase的部分组建,Tidb,Cockroachdb,Influxdb等。

⑥云平台,目前国外很多云平台在采用Go开发,CloudFoundy的部分组建,前VMare的技术总监自己出来搞的Apcera云平台。

(3)为什么选择使用Go语言?

国内很多云创业公司都会选择把Go作为首要语言,例如七牛云等。为什么会选择Go呢?与其他语言的应用相比,它有什么优点呢?

①简单,学习曲线平稳;

它包含了类C语法、GC内置和工程工具。这一点非常重要,因为Go语言容易学习,所以一个普通的大学生花一个星期就能写出来可以上手的、高性能的应用。在国内大家都追求快,这也是为什么国内Go流行的原因之一。

②极致效率;

Go拥有接近C的运行效率和接近PHP的开发效率,这就很有利的支撑了上面大家追求快速的需求。

③Google公司开发;

之所以说Go出身名门,是因为我们知道Go语言出自Google公司,这个公司在业界的知名度和实力自然不用多说。Google公司聚集了一批牛人,在各种编程语言称雄争霸的局面下推出新的编程语言,自然有它的战略考虑。而且从Go语言的发展态势来看,Google对它这个新的宠儿还是很看重的,Go自然有一个良好的发展前途。我们看看Go语言的主要创造者,血统纯正这点就可见端倪了。

④自由高效:组合的思想、无侵入式的接口;

Go语言可以说是开发效率和运行效率二者的完美融合,天生的并发编程支持。Go语言支持当前所有的编程范式,包括过程式编程、面向对象编程以及函数式编程。程序员们可以各取所需、自由组合、想怎么玩就怎么玩。

⑤强大的标准库;

这包括互联网应用、系统编程和网络编程。Go里面的标准库基本上已经是非常稳定了,特别是我这里提到的三个,网络层、系统层的库非常实用。

⑥部署方便:二进制文件、Copy部署;

这一点是很多人选择Go的最大理由,因为部署太方便了,所以现在也有很多人用Go开发运维程序。

⑦简单而强大的并发处理能力;

它包含了降低心智的并发和简易的数据同步,我觉得这是Go最大的特色。之所以写正确的并发、容错和可扩展的程序如此之难,是因为我们用了错误的工具和错误的抽象,Go可以说这一块做的相当简单。

⑧规范,不会写出垃圾代码;

Go拥有强大的编译检查、严格的编码规范和完整的软件生命周期工具,具有很强的稳定性,稳定压倒一切。那么为什么Go相比于其他程序会更稳定呢?这是因为Go提供了软件生命周期(开发、测试、部署、维护等等)的各个环节的工具,如go tool、gofmt、go test等。

2.Go语言Web编程的优势

通过上面的介绍我们知道,Go语言Web开发领域有得天独厚的优势!Go语言被称为云计算时代的C语言。我们知道,在Web开发的世界里,敏捷才是王道。谁能使用更少的费用和资源来更快地完成网站和网络应用,谁就获得更多的竞争优势。此外,对于编程者而言,不仅希望快速完成Web开发,对可用性和用户体验的要求也很高。

这需要开发更多的功能和高级编程语言来开发网站功能,例如Go语言。本文着重介绍了Go语言 Web开发的好处,并将Go语言 Web编程与其他高度流行的语言进行了比较,例如 Python。

让我们看一下Python的特性,以及为什么它能够成为Web开发领域的主流。

(1)Python vs Go语言:优点与缺点

Python自20世纪80年代就出现了,但直到近些年它才变得流行。事实上,Python受欢迎程度已经连续6年增长,现在是最常用的编程语言之一。在Web开发中,它主要用于后端编程,但它也用于前端任务。最终,Python在Web开发场景中占优势的主要原因是它帮助开发人员解决敏捷性的迫切需求。它易于学习,简单易用,有助于快速完成复杂的目标。Python是灵活的,语法也很简单。

Go语言现在被认为是Python、Java等传统语言的替代品,关于Go语言 Web开发与Python、Java的讨论不胜枚举。

虽然简单是Python的主要优点,但也有一些缺点。许多开发人员发现,虽然对于一些简单和基本的开发Python很实用,但如果要构建更复杂的系统和定制模块,使用Python可能会变得很复杂。此外,经常出现令人沮丧的编码错误。在HekReNo.com文章中“从Python转到Go语言的五个原因”Tigran Bayburstyan说:“Python是一个伟大而有趣的语言,但有时你会遇到异常,因为你试图使用一个变量作为一个整数,但事实证明它是一个字符串。”Go会让你在编译时消除这些问题。

总而言之,在许多用例中,Go语言 Web开发已被证明比使用Python更快地完成同一类任务。最终,Go语言是为那些想要完成任务的人快速而有效地完成任务,而不需要进入编程语言的微妙之处。

(2)Python vs Go语言:哪一个更好?

那么,Go语言是否会在不久的将来取代Python呢?开发者社区正在讨论两种编程语言的优缺点。如果您正在寻找用于Web编程、移动开发、微服务和ERP系统的强大工具,我们相信,您应该切换到Go语言的原因是:

①简单性;

如果Python是迄今为止最容易学习的编程语言,那么Go语言甚至更简单。学习Go语言要比学习Python快得多。一些开发人员声称Go语言几乎和JavaScript一样简单。

②高级编译能力;

Go语言是一个编译执行语言,它能够直接将代码编译成可执行的二级制机器码。Go语言静态地将所有依赖库和模块编译到到一个二进制文件中。你不再需要在服务器上安装任何依赖项——你所需要做的就是上传一个编译的文件,你的应用就可以简单的运行了。

③并发和性能;

Go语言并发模型能够确保更高的性能(性能甚至 2x、3x的提升)。大多数现代编程语言都支持并发,但是Go语言采用了更节省资源的方法。相对于Python,Go语言提供更高性能的goroutine模型来支持多线程,goroutine能够更加节省CPU和内存资源。所以Go语言更有助于降低成本和资源。

④框架和库的本地支持;

Go语言 sdk包提供了功能丰富的API,因此不用过多的依赖第三方库就能很好的使用Go语言。当然,如果你需要的话,你可以下载很多工具和框架(Go语言周围的社区已经很强大很完善了),但是常用的API都已经内置到Go语言核心库中了。这样可以加快整个Go语言 Web开发过程的速度,并且使您无需寻找任何第三方依赖。

⑤顶级IDE和调试;

Go语言创作者在创建具有先进调试工具和插件的最先进的集成开发环境方面做得很好。这可以说是编程中最关键的方面,因为IDE会严重阻碍或加速开发过程。今天,当敏捷性给软件公司带来竞争优势时,伟大的IDE和调试工具是一个非常重要的优势。

⑥清晰的语法;

另一个有助于Go语言 Web编程出色的简单性和易用性的是其清晰的语法,它包含零不必要的组件。Go是建立在实用的头脑中的:而不是必须深入研究语言结构,开发者现在可以自由地专注于开发本身。

总结,通过上面的对比我们发现,Go语言在各方面基本都已经超过Python,尽管Python社区仍然超过Gophers,Go倡导者的数量每天都在增加,相信Go语言接近并超过Python、Java是大势所趋。

有了充分的理由,Go语言证明了“先进”不再等同于复杂、缓慢和昂贵。我们可以在不牺牲质量和大量投资的情况下实现显著的开发速度。此外,如果你决定使用Go,那么已经有许多Go语言网络框架供你选择。

3.Go语言Web开发常用框架

Go语言被称为云计算时代的C语言,它以其独特的优势逐渐被越来越多的公司所关注和使用。为了充分利用Go语言的Web开发优势,有必要熟悉一下Go语言的Web框架。

(1)Beego (http://beego.me/)

Beego是一个完全的MVC框架,你可以使用你的Go语言专业技术构建你的web应用程序。Beego 是用Go 语言开发的高效的 HTTP 框架,可以用来快速开发 API、Web应用及后端服务等各种应用。Beego是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架。它还结合了 Go语言自身的一些特性(接口、结构体嵌入等)。

1)Beego 架构简介。

Beego 是基于多个独立模块构建的,是一个高度解耦的框架。最初在设计 Beego 时就考虑到了功能模块化,用户即使不适用 Beego 的HTTP逻辑,也可以独立使用这些模块(例如可以使用cache模块来处理的缓存逻辑,使用日志模块来记录操作信息,使用config模块来解析各种格式的文件)。

Beego各模块的功能,以及使用方法会在接下来逐一介绍。

2)Beego 执行逻辑。

既然Beego 是基于模块构建的,那么它的执行逻辑是怎么样的呢?Beego 是一个典型的MVC框架,其执行逻辑如下图所示。

图1

执行逻辑可以拆分以下几段:

①main文件监听启动端口接收请求。

②请求经过路由和参数过滤功能被转发给绑定URL的控制器处理。

③控制器(Controller)调用辅助工具包、Model、Session管理、日志处理、缓存处理模块进行相应的业务处理。其中,模型(Model)通过ORM直接操作数据库。

④业务处理完成,返回响应或视图(View)给请求方。

(2)Gin(https://gin-gonic.github.io/gin/)

Gin是一个基于 Go 语言编写的 Web 框架。Gin框架拥有很好的性能,其借助高性能的httprouter,运行速度得到了极大提升。目前的 Gin 框架是1.x版本。

①安装。

下载并安装Gin:

$ go get -u github.com/gin-gonic/gin

②第一个Gin示例。

安装完成后,让我们开启Gin之旅。

package main

import (

    “github.com/gin-gonic/gin”

)

func main() {

    // 创建一个默认的路由引擎

    r := gin.Default()

    // GET:请求方式;/hello:请求的路径

    // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数

    r.GET(“/hello”, func(c *gin.Context) {

        // c.JSON:返回JSON格式的数据

        c.JSON(200, gin.H{

            “message”: “Hello world!”,

        })

    })

    // 启动HTTP服务,默认在0.0.0.0:8080启动服务

    r.Run()

}

运行以上代码,然后使用浏览器打开127.0.0.1:8080/hello即可看到一串JSON字符串。

(3)Revel(http://revel.github.io/)

Revel能让Go语言的web开发如虎添翼,大大提高你的开发效率。

(4)Martini (http://martini.codegangsta.io)

Martini是一个受到Sinatra (一个Ruby 框架)启发而开发的Go语言web框架。使用Martini,你可以快速写你的模块化应用或者服务。

(5)Traffic(https://github.com/pilu/traffic)

Traffic 同样也是受Sinatra的regexp/pattern 互斥思想启发下开发的web框架。它是一个小框架,并且是轻量型的。因此,它具有很好的灵活性,扩展性。

(6)Goji(https://goji.io/)

Goji是一个轻量型的web框架,具有简约性和灵活性的多路路由请求特性,正如它所标榜自己的那样。

4.Go语言Web编程书籍

(1)《Go Web编程实战派从入门到精通》廖显东 著 2021年4月出版

采用Go最新版本编写,拒绝纯理论,直接实战!实战!实战!重要的事说3遍!本书聚焦Go Web开发领域,对Go Web知识进行全面深入地讲解。货真价实高质量精品图书!本书有如下特色:

①一线技术,突出实战;

本书以实战为核心,贯穿整本书。所有代码采用Go最新版本编写。

②精雕细琢,阅读性强;

全书的语言经过多次打磨,力求精确。同时注重阅读体验,让没有任何基础的读者也可以很轻松的读懂书中的知识。

③零基础入门,循序渐进,实现快速从菜鸟向实战派高手迈进;

本书以Go入门级程序员为主要对象,初、中、高级程序员都可以从书中学到干货。先从Go的基础学起,然后学习Go核心的技术,再学Go的高级应用,然后再进行项目实战,最后介绍应用程序的Docker实战部署。全书从最基础的知识讲解一步一步到最核心的B2C电子商务系实战开发,真正帮助读者从基础入门向开发高手迈进。

④极客思维,极致效率;

本书以极客思维、深入Go语言底层进行探究,帮助读者了解背后原理。全书言简意赅,以帮助读者提升开发效率为导向,同时尽可能帮助读者缩短阅读本书的时间。

⑤由易到难,重难点标注并重点解析;

本书编排由易到难,内容基本覆盖Go Web的主流前沿技术。同时对重难点进行重点讲解,对易错点和注意点进行了提示说明,帮助读者克服学习过程中的困难。

⑥突出实战,快速突击;

本书的实例代码是绝大部分都是来自于最新的企业实战项目。对于购买本书的读者,所有的源代码均可以通过网上下载,直接下载即可运行,让读者通过实践来加深理解。

⑦实战方案,可直接二次开发进行实战部署;

本书全书以实战为主,所有的示例代码,拿来即可运行。特别是第9章,购买本书的读者可以直接获得B2C电子商务系统的全部源代码。可以直接作为电商项目进行二次开发,用于读者的项目。读者购买本书不仅可以学习本书的各种知识,也相当于购买一个最新版的Go语言电商系统解决方案及项目源码。

(2)《Go Web编程》【新加坡】郑兆雄 著 2017年12月出版

该书的优点:基础入门,出版较早。

该书的缺点:

①太过基础,并且没有循序渐进,需要一定Go语言基础。

②开发例子是国内已经过时的论坛系统,不符合当前市场需求。

③开发的部署是以Heroku、GoogleAppEngine、DigitalOcean等云平台上部署Go Web应用,不符合国内的具体情况。

④缺乏系统深入的知识体系。

⑤缺乏高级实战教程及国内大型架构经验的介绍,无法满足读者向高级迈进。

Go语言 同步sync包简介

Sync包简介

1. 什么是Sync包?

Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.

Values containing the types defined in this package should not be copied.

这句话大意是说:
Sync包同步提供基本的同步原语,如互斥锁。 除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。 通过Channel和沟通可以更好地完成更高级别的同步。并且此包中的值在使用过后不要拷贝。

从描述中可以看到的是,Go语言并不推荐这个包中的大多数并发控制方法,但还是提供了相关方法,主要原因是Go语言中提倡以共享内存的方式来通信:

不要以共享内存的方式来通信,作为替代,我们应该以通信的手段来共享内存

共享内存的方式使得多线程中的通信变得简单,但是在并发的安全性控制上将变得异常繁琐。
正确性不是我们唯一想要的,我们想要的还有系统的可伸缩性,以及可理解性,我觉得这点非常重要,比如现在广泛使用的Raft算法。

2. 包中的Type

包中主要有: Locker, Cond, Map, Mutex, Once, Pool,
RWMutex, WaitGroup

type Locker interface {
        Lock()
        Unlock()
}
type Cond struct {
        // L is held while observing or changing the condition
        L Locker
}

3. 什么是锁,为什么需要锁?

锁是sync包中的核心,他主要有两个方法,加锁和解锁。
在单线程运行的时候程序是顺序执行的,程序对数据的访问也是:
读取 => 一顿操作(加减乘除之类的) => 写回原地址
但是一旦程序中进行了并发编程,也就是说,某一个函数可能同时被不同的线程执行的时候,以时间为维度会发生以下情况:

可以看到的是,A地址的数字被执行了两次自增,若A=5,我们在执行完成后预期的A值是7,但是在这种情况下我们得到的A却是6,bug了~
还有很多类似的并发错误,所以才有锁的引入。若是我们在线程2读取A的值的时候对A进行加锁,让线程2等待,线程1执行完成之后在执行线程2,这样就能够保证数据的正确性。但是正确性不是我们唯一想要的。

4.写更优雅的代码

在很多语言中我们经常为了保证数据安全正确,会在并发的时候对数据加锁

Lock()
doSomething()
Unlock()

Go语言在此包中也提供了相关的锁,但是标明了”most are intended for use by low-level library routines” 所以我这里只对 Once and WaitGroup types做简述。

5.Once 对象

Once 是一个可以被多次调用但是只执行一次,若每次调用Do时传入参数f不同,但是只有第一个才会被执行。

func (o *Once) Do(f func())


    var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }

如果你执行这段代码会发现,虽然调用了10次,但是只执行了1次。BTW:这个东西可以用来写单例。

6. WaitGroup

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()

wait group 用来等待一组goroutines的结束,在主Goroutine里声明,并且设置要等待的goroutine的个数,每个goroutine执行完成之后调用 Done,最后在主Goroutines 里Wait即可。下面是个官方的例子:

var wg sync.WaitGroup
var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/",
}
for _, url := range urls {
        // Increment the WaitGroup counter.
        wg.Add(1)
        // Launch a goroutine to fetch the URL.
        go func(url string) {
                // Decrement the counter when the goroutine completes.
                defer wg.Done()
                // Fetch the URL.
                http.Get(url)
        }(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()

7. 总结

Go语言中高级的并发可以通过channel来实现,这是Go语言所倡导的,但是Go语言也提供了锁等先关操作。

恭喜老詹,35岁的詹姆斯,依旧狂奔在追逐乔丹的路上!


在去年7月,这支湖人刚刚组建完成时,有几个人愿意相信他们能够成功地走到最后呢?在那些质疑者的眼里,这是一支危机四伏,随时都有可能被推翻重来的球队。
他们认为湖人的管理团队太过混乱,缺少成为一支伟大球队所需要的稳定性。
他们虽然认可球队的教练组豪华万分,但却认为由三名前任NBA主帅搭建起来的核心构架,更有可能发生的是暗流涌动,各自心怀鬼胎。
他们认为湖人的阵容缺乏深度,虽有詹眉打底,但却没有稳定可靠的第三点,远不及其他的竞争对手。
他们认为詹姆斯老了,即将年满35周岁的他,很有可能再次被伤病给盯上,一个无法全力输出的詹姆斯,必定无法统帅一支成功的球队。
但,他们错了。
在走完了这段长达一年多的漫长旅途后,洛杉矶湖人,时隔十年再度踏上了世界之巅。至少在这个夜晚,那些质疑者们可以闭嘴了。
湖人这一次的成功,属于队里的每一个人。

他们低估了湖人的教练组。
事后来看,这是一个出色的团队,初期的试探,赛间的调整,甚至有魄力在赛中对阵容的搭配进行临时的修整。他们的准备之充分,应对手法之深厚,显然超过了外界最初的预期。
他们低估了湖人的角色球员与年轻人,能在一年的时间里所收获的成长。
这些失意的,不被人看好的,无人问津的球员,在洛杉矶重新收获了信任,他们打出了强硬的防守表现,留下了无数闪耀的瞬间。

他们低估了安东尼-戴维斯的价值。
在来到这之前,他是众人嘴中调侃的“美国之子”,现役最著名的“荣誉摇摆人”,名大于实的代表人物。
但在来到这之后,他用行动证明了自己的价值,证明了为什么他是这个时代最好最全能的内线球员。他不仅构建起了球队的防守体系,更是他们在进攻端最锋利的刺刃,因为浓眉的存在,让洛杉矶人拥有了更多破解防守与应对进攻的办法,他是球队能够登顶的重要功臣与关键之一。

当然,被他们所低估的,还有勒布朗-詹姆斯的领导力与决心。
的确,他35岁了,跑的没有以前那么快了,跳的没有年轻时那么高了,身体也会受到一些伤病的困扰了,可他追逐伟大的心,依旧炽热。
“我努力奋斗的所有动力,都是为了追逐那个曾经在芝加哥打球的幽灵。”
4年前,刚刚率领着克利夫兰骑士打破52年魔咒,逆天登顶的勒布朗,在接受时任《体育画报》记者的詹金斯采访时,说出了这句话。而在时隔4年之后今天,他依旧在用行动践行着自己曾经许下的诺言。
每当詹姆斯赢得一次总冠军,关于他能否超越乔丹这个议题,就会重新开始被人热议。对此,我也听过不少的“狠话”,总结着来讲,大概意思就是:“从从前,到现在,乃至将来,‘乔丹是至高无上的篮球之神’这件事,永远都不会发生改变。”
我当然也跟大多数人一样,非常认同乔丹在篮球世界里如象征图腾一般的地位。他所取得的荣誉是无上的,他所做到过的那些事情是极富传奇色彩的,他配得上任何人为他送上的任何赞美,哪怕距离他完全退役已过去近20年之久,迈克尔-乔丹这个名字也依旧还在影响着这项运动,正如我们每个人在最初遇见篮球时所知道的第一个名字,永远属于他一样。
但是,仅就我个人而言,我并不相信所谓“真理的永恒”。
乔丹之所以被称为篮球之神,绝非靠的一手虚无的赞美,而是因为那一连串的数字实在太过耀眼——10届得分王,3届抢断王,1个DPOY,5个MVP,6次夺得总冠军,6次拿下FMVP,10个年度一阵,9个年度一防,外加一票多到数不清,哪哪都有他的数据纪录——他的伟大,无需吹捧,也经得起任何比较。
但这并不意味着,当这些数字开始受到别人挑战时,他依旧能靠着所谓“神性”,永远高枕无忧地坐在那个位子上。
虽然詹姆斯目前还不足以威胁到乔丹,但显然,他已经拉近了自己与少年偶像的距离。只是偏见与固有看法的存在,让人或多或少地低估了眼前人所创造的那些传奇成就。

比如有的人会用总决赛胜率来贬低詹姆斯。
的确,40%的总决赛胜率在“乔丹不打第七场”和“乔丹从未输过总决赛”面前,显得有些脆弱不堪。
但又有多少人忽略了,那可是长达10年的总决赛征战啊,更何况我们在评判一件事物的好与坏时,真的该用如此一刀切的方式么?
2007年,四年级的新星勒布朗-詹姆斯独自率领着克利夫兰骑士,史无前例地杀进了总决赛,然后败给了无论阵容,或是核心都要更加强大且老辣的圣安东尼奥马刺,这样的失败,难道不该被加上引号么;
2015年,乐福与欧文相继倒下,詹姆斯带着82万年薪的先发控卫,总价不过2120万美元的核心6人组,与勇士厮杀了6场最终抱憾收场的失败,也应该受人唾弃么;
2018年,欧文出走,球队中期地震式动荡,在经历了两轮大清洗之后,不仅丢掉了明星实力,也彻底失去了自己的阵容深度。整个季后赛,除詹姆斯能每场稳定贡献34分之外,仅乐福一人场均得分上双(14.9分),且命中率只有惨淡的39.2%。但就是在这种极端的情况下,他依旧扛着骑士踏上了总决赛的舞台。的确,在强悍的勇士面前,他一场都没赢,可单场51分拼到加时的神勇,你真的会觉得这是一种耻辱么?
我永远不会这么认为,我只会觉得这是詹姆斯将一支本不属于总决赛的球队,带到了那个位置上,我更愿意将其视作是个人的一次巨大成功。

再比如,有人认为詹姆斯的数据积累之所以如此骇人,纯粹只是因为他打的比较多而已。
但近在眼前的例子——金州勇士核心成员的遭遇——告诉我们:长时间高强度的征战,所能带来的东西绝非只有数据与荣誉,更有身体上的超负荷运转。
17年的职业生涯,有超过一半的时间,詹姆斯都打进了总决赛,他在季后赛的出场时间超过了1万分钟,排名NBA历史第一,几乎是现役排行老二的凯文-杜兰特的一倍之多。
他能够扛住如此长时间的高强度对抗,除了得益于他惊人的身体天赋之外,更重要的,是数年如一日的努力,是对饮食与生活的严苛管理,以及对自身身体的精心照料。
我们早已习惯了他展现在世人面前的优秀,却总是会忽略掉掩藏在这一切背后的血与泪。
打的比赛多,是努力付出之后的回报,更是能力的体现。这本该受到褒奖,但在詹姆斯身上,却无奈地成了另一种被人看低的方式。
这一切,真的就本该如此么?

我不想说詹姆斯未来必定还能取得怎样的荣誉,或是数据积累。毕竟谁都无法确定没有发生过的事。但在今天,在詹姆斯拿下自己职业生涯第4个总冠军和第4座总决赛MVP的奖杯后,他已经成为了NBA历史上仅次于迈克尔-乔丹的球员——在这之前,除了乔丹,没人拿到过超过3个FMVP——他理应获得足够的尊重。
就像他在捧起奖杯时说的那番话一样:“我想得到的,是尊重。”
在他出现在这之前,我们甚至没有见过一个哪怕能够接近乔丹的人,而在这之后,我们也无从确认,下一个拥有这等能力的天才,会在何年何月再次降临。
第17个赛季落幕,年近36岁的詹姆斯,依旧狂奔在这条追赶伟大幽灵的路上。他或许可以成功,又或许不能。但我想,当你有机会去见证这样一段跨越历史的较量时,期待与祝福,终归是要强过诋毁与谩骂的。
在这样一个时刻,在所有人在高呼着“湖人总冠军”的时候,我更想把我的心里话,送给这个时代最伟大的篮球运动员:
“恭喜你,勒布朗-詹姆斯,在经历了17年的漫长旅途之后,你终于获得了挑战神的资格。这个画面,是自你踏入联盟的第一天起,就被世人所寄予的期待。就现在,抬起头看看,上帝的神像就矗立在那座山峰之上,你能看见他,他不远了,去挑战他吧!”

kubectl 技巧总结

Kubectl 是 Kubernetes 最重要的命令行工具。在 Flant,我们会在 Wiki 和 Slack 上相互分享 Kubectl 的妙用(其实我们还有个搜索引擎,不过那就是另外一回事了)。多年以来,我们在 kubectl 方面积累了很多技巧,现在想要将其中的部分分享给社区。

我相信很多读者对这些命令都非常熟悉;然而我还是希望读者能够从本文中有所获益,进而提高生产力。

下列内容有的是来自我们的工程师,还有的是来自互联网。我们对后者也进行了测试,并且确认其有效性。

现在开始吧。

获取 Pod 和节点

  1. 我猜你知道如何获取 Kubernetes 集群中所有 Namespace 的 Pod——使用 --all-namepsaces 就可以。然而不少朋友还不知道,现在这一开关还有了 -A 的缩写。
  2. 如何查找非 running 状态的 Pod 呢? kubectl get pods -A --field-selector=status.phase!=Running | grep -v Complete顺便一说,--field-selector 是个值得深入一点的参数。
  3. 如何获取节点列表及其内存容量: kubectl get no -o json | \
    jq -r '.items | sort_by(.status.capacity.memory)[]|[.metadata.name,.status.capacity.memory]| @tsv'
  4. 获取节点列表,其中包含运行在每个节点上的 Pod 数量: kubectl get po -o json --all-namespaces | \
    jq '.items | group_by(.spec.nodeName) | map({"nodeName": .[0].spec.nodeName, "count": length}) | sort_by(.count)'
  5. 有时候 DaemonSet 因为某种原因没能在某个节点上启动。手动搜索会有点麻烦: $ ns=my-namespace
    $ pod_template=my-pod
    $ kubectl get node | grep -v \"$(kubectl -n ${ns} get pod --all-namespaces -o wide | fgrep ${pod_template} | awk '{print $8}' | xargs -n 1 echo -n "\|" | sed 's/[[:space:]]*//g')\"
  6. 使用 kubectl top 获取 Pod 列表并根据其消耗的 CPU 或 内存进行排序: # cpu
    $ kubectl top pods -A | sort --reverse --key 3 --numeric
    # memory
    $ kubectl top pods -A | sort --reverse --key 4 --numeric
  7. 获取 Pod 列表,并根据重启次数进行排序:kubectl get pods —sort-by=.status.containerStatuses[0].restartCount当然也可以使用 PodStatus 以及 ContainerStatus 的其它字段进行排序。

获取其它数据

  1. 运行 Ingress 时,经常要获取 Service 对象的 selector 字段,用来查找 Pod。过去要打开 Service 的清单才能完成这个任务,现在使用 -o wide 参数也可以: $ kubectl -n jaeger get svc -o wide
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
    jaeger-cassandra ClusterIP None <none> 9042/TCP 77d app=cassandracluster,cassandracluster=jaeger-cassandra,cluster=jaeger-cassandra
  2. 如何输出 Pod 的 requests 和 limits $ kubectl get pods -A -o=custom-columns='NAME:spec.containers[*].name,MEMREQ:spec.containers[*].resources.requests.memory,MEMLIM:spec.containers[*].resources.limits.memory,CPUREQ:spec.containers[*].resources.requests.cpu,CPULIM:spec.containers[*].resources.limits.cpu'
    NAME MEMREQ MEMLIM CPUREQ CPULIM
    coredns 70Mi 170Mi 100m <none>
    coredns 70Mi 170Mi 100m <none>
    ...
  3. kubectl run(以及 createapplypatch)命令有个厉害的参数 --dry-run,该参数让用户无需真正操作集群就能观察集群的行为,如果配合 -o yaml,就能输出命令对应的 YAML: $ kubectl run test --image=grafana/grafana --dry-run -o yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    creationTimestamp: null
    labels:
    run: test
    name: test
    spec:
    replicas: 1
    selector:
    matchLabels:
    run: test
    简单的把输出内容保存到文件,删除无用字段就可以使用了。1.18 开始 kubectl run 生成的是 Pod 而非 Deployment。
  4. 获取指定资源的描述清单: kubectl explain hpa
    KIND: HorizontalPodAutoscaler
    VERSION: autoscaling/v1
    DESCRIPTION:
    configuration of a horizontal pod autoscaler.
    FIELDS:
    apiVersion <string>
    ...

网络

  1. 获取集群节点的内部 IP: $ kubectl get nodes -o json | jq -r '.items[].status.addresses[]? | select (.type == "InternalIP") | .address' | \
    paste -sd "\n" -
    9.134.14.252
  2. 获取所有的 Service 对象以及其 nodePort $ kubectl get -A svc -o json | jq -r '.items[] | [.metadata.name,([.spec.ports[].nodePort | tostring ] | join("|"))]| @tsv'

    kubernetes null
    ...
  3. 在排除 CNI(例如 Flannel)故障的时候,经常会需要检查路由来识别故障 Pod。Pod 子网在这里非常有用: $ kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' | tr " " "\n" fix-doc-azure-container-registry-config ✭
    10.120.0.0/24
    10.120.1.0/24
    10.120.2.0/24

日志

  1. 使用可读的时间格式输出日志: $ kubectl logs -f fluentbit-gke-qq9w9 -c fluentbit --timestamps
    2020-09-10T13:10:49.822321364Z Fluent Bit v1.3.11
    2020-09-10T13:10:49.822373900Z Copyright (C) Treasure Data
    2020-09-10T13:10:49.822379743Z
    2020-09-10T13:10:49.822383264Z [2020/09/10 13:10:49] [ info] Configuration:
  2. 只输出尾部日志: kubectl logs -f fluentbit-gke-qq9w9 -c fluentbit --tail=10
    [2020/09/10 13:10:49] [ info] ___________
    [2020/09/10 13:10:49] [ info] filters:
    [2020/09/10 13:10:49] [ info] parser.0
    ...
  3. 输出一个 Pod 中所有容器的日志:kubectl -n my-namespace logs -f my-pod —all-containers
  4. 使用标签选择器输出多个 Pod 的日志:kubectl -n my-namespace logs -f -l app=nginx
  5. 获取“前一个”容器的日志(例如崩溃的情况):kubectl -n my-namespace logs my-pod —previous

其它

  1. 把 Secret 复制到其它命名空间: kubectl get secrets -o json --namespace namespace-old | \
    jq '.items[].metadata.namespace = "namespace-new"' | \
    kubectl create-f -
  2. 下面两个命令可以生成一个用于测试的自签发证书: openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=grafana.mysite.ru/O=MyOrganization"
    kubectl -n myapp create secret tls selfsecret --key tls.key --cert tls.crt

相关链接

本文没什么结论,但是可以提供一个小列表,其中包含本文相关的有用链接。

  1. Kubernetes 官方文档:https://kubernetes.io/docs/reference/kubectl/cheatsheet/
  2. Linux Academy 的入门参考:https://linuxacademy.com/blog/containers/kubernetes-cheat-sheet/
  3. Blue Matador 分类整理的命令列表:https://www.bluematador.com/learn/kubectl-cheatsheet
  4. 另一个命令指南,部分内容和本文重复:https://gist.github.com/pydevops/0efd399befd960b5eb18d40adb68ef83
  5. kubectl 别名搜集:https://github.com/ahmetb/kubectl-aliases

高并发架构实践

高并发经常会发生在有大活跃用户量,用户高聚集的业务场景中,如:秒杀活动,定时领取红包等。
为了让业务可以流畅的运行并且给用户一个好的交互体验,我们需要根据业务场景预估达到的并发量等因素,来设计适合自己业务场景的高并发处理方案。

在电商相关产品开发的这些年,我有幸的遇到了并发下的各种坑,这一路摸爬滚打过来有着不少的血泪史,这里进行的总结,作为自己的归档记录,同时分享给大家。

服务器架构

业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务。 
一个可以支持高并发的服务少不了好的服务器架构,需要有均衡负载,数据库需要主从集群,nosql缓存需要主从集群,静态文件需要上传cdn,这些都是能让业务程序流畅运行的强大后盾。

服务器这块多是需要运维人员来配合搭建,具体我就不多说了,点到为止。
大致需要用到的服务器架构如下:

  • 服务器
    • 均衡负载(如:nginx,阿里云SLB)
    • 资源监控
    • 分布式
  • 数据库
    • 主从分离,集群
    • DBA 表优化,索引优化,等
    • 分布式
  • nosql
    • redis
      • 主从分离,集群
    • mongodb
      • 主从分离,集群
    • memcache
      • 主从分离,集群
  • cdn
    • html
    • css
    • js
    • image

并发测试

高并发相关的业务,需要进行并发的测试,通过大量的数据分析评估出整个架构可以支撑的并发量。

测试高并发可以使用第三方服务器或者自己测试服务器,利用测试工具进行并发请求测试,分析测试数据得到可以支撑并发数量的评估,这个可以作为一个预警参考,俗话说知己自彼百战不殆。

第三方服务:

  • 阿里云性能测试

并发测试工具:

  • Apache JMeter
  • Visual Studio性能负载测试
  • Microsoft Web Application Stress Tool

实战方案

通用方案

日用户流量大,但是比较分散,偶尔会有用户高聚的情况;

通用

场景: 用户签到,用户中心,用户订单,等
服务器架构图: 

说明:

场景中的这些业务基本是用户进入APP后会操作到的,除了活动日(618,双11,等),这些业务的用户量都不会高聚集,同时这些业务相关的表都是大数据表,业务多是查询操作,所以我们需要减少用户直接命中DB的查询;优先查询缓存,如果缓存不存在,再进行DB查询,将查询结果缓存起来。

更新用户相关缓存需要分布式存储,比如使用用户ID进行hash分组,把用户分布到不同的缓存中,这样一个缓存集合的总量不会很大,不会影响查询效率。

方案如:

  • 用户签到获取积分
    • 计算出用户分布的key,redis hash中查找用户今日签到信息
    • 如果查询到签到信息,返回签到信息
    • 如果没有查询到,DB查询今日是否签到过,如果有签到过,就把签到信息同步redis缓存。
    • 如果DB中也没有查询到今日的签到记录,就进行签到逻辑,操作DB添加今日签到记录,添加签到积分(这整个DB操作是一个事务)
    • 缓存签到信息到redis,返回签到信息
    • 注意这里会有并发情况下的逻辑问题,如:一天签到多次,发放多次积分给用户。
  • 用户订单
    • 这里我们只缓存用户第一页的订单信息,一页40条数据,用户一般也只会看第一页的订单数据
    • 用户访问订单列表,如果是第一页读缓存,如果不是读DB
    • 计算出用户分布的key,redis hash中查找用户订单信息
    • 如果查询到用户订单信息,返回订单信息
    • 如果不存在就进行DB查询第一页的订单数据,然后缓存redis,返回订单信息
  • 用户中心
    • 计算出用户分布的key,redis hash中查找用户订单信息
    • 如果查询到用户信息,返回用户信息
    • 如果不存在进行用户DB查询,然后缓存redis,返回用户信息
  • 其他业务
    • 上面例子多是针对用户存储缓存,如果是公用的缓存数据需要注意一些问题,如下
    • 注意公用的缓存数据需要考虑并发下的可能会导致大量命中DB查询,可以使用管理后台更新缓存,或者DB查询的锁住操作。
    • 我的博文[大话Redis进阶]对更新缓存问题和推荐方案的分享。

以上例子是一个相对简单的高并发架构,并发量不是很高的情况可以很好的支撑,但是随着业务的壮大,用户并发量增加,我们的架构也会进行不断的优化和演变,比如对业务进行服务化,每个服务有自己的并发架构,自己的均衡服务器,分布式数据库,nosql主从集群,如:用户服务、订单服务;

消息队列

秒杀、秒抢等活动业务,用户在瞬间涌入产生高并发请求

消息队列

场景:定时领取红包,等
服务器架构图:

说明:

场景中的定时领取是一个高并发的业务,像秒杀活动用户会在到点的时间涌入,DB瞬间就接受到一记暴击,hold不住就会宕机,然后影响整个业务;

像这种不是只有查询的操作并且会有高并发的插入或者更新数据的业务,前面提到的通用方案就无法支撑,并发的时候都是直接命中DB;

设计这块业务的时候就会使用消息队列的,可以将参与用户的信息添加到消息队列中,然后再写个多线程程序去消耗队列,给队列中的用户发放红包;

方案如:

  • 定时领取红包
    • 一般习惯使用 redis的 list
    • 当用户参与活动,将用户参与信息push到队列中
    • 然后写个多线程程序去pop数据,进行发放红包的业务
    • 这样可以支持高并发下的用户可以正常的参与活动,并且避免数据库服务器宕机的危险

附加: 
通过消息队列可以做很多的服务。 
如:定时短信发送服务,使用sset(sorted set),发送时间戳作为排序依据,短信数据队列根据时间升序,然后写个程序定时循环去读取sset队列中的第一条,当前时间是否超过发送时间,如果超过就进行短信发送。

一级缓存

高并发请求连接缓存服务器超出服务器能够接收的请求连接量,部分用户出现建立连接超时无法读取到数据的问题;

因此需要有个方案当高并发时候时候可以减少命中缓存服务器;

这时候就出现了一级缓存的方案,一级缓存就是使用站点服务器缓存去存储数据,注意只存储部分请求量大的数据,并且缓存的数据量要控制,不能过分的使用站点服务器的内存而影响了站点应用程序的正常运行,一级缓存需要设置秒单位的过期时间,具体时间根据业务场景设定,目的是当有高并发请求的时候可以让数据的获取命中到一级缓存,而不用连接缓存nosql数据服务器,减少nosql数据服务器的压力

比如APP首屏商品数据接口,这些数据是公共的不会针对用户自定义,而且这些数据不会频繁的更新,像这种接口的请求量比较大就可以加入一级缓存;

通用

服务器架构图:

合理的规范和使用nosql缓存数据库,根据业务拆分缓存数据库的集群,这样基本可以很好支持业务,一级缓存毕竟是使用站点服务器缓存所以还是要善用。

静态化数据

高并发请求数据不变化的情况下如果可以不请求自己的服务器获取数据那就可以减少服务器的资源压力。

对于更新频繁度不高,并且数据允许短时间内的延迟,可以通过数据静态化成JSON,XML,HTML等数据文件上传CDN,在拉取数据的时候优先到CDN拉取,如果没有获取到数据再从缓存,数据库中获取,当管理人员操作后台编辑数据再重新生成静态文件上传同步到CDN,这样在高并发的时候可以使数据的获取命中在CDN服务器上。

CDN节点同步有一定的延迟性,所以找一个靠谱的CDN服务器商也很重要

其他方案

  • 对于更新频繁度不高的数据,APP,PC浏览器,可以缓存数据到本地,然后每次请求接口的时候上传当前缓存数据的版本号,服务端接收到版本号判断版本号与最新数据版本号是否一致,如果不一样就进行最新数据的查询并返回最新数据和最新版本号,如果一样就返回状态码告知数据已经是最新。减少服务器压力:资源、带宽

分层,分割,分布式

大型网站要很好支撑高并发,这是需要长期的规划设计 
在初期就需要把系统进行分层,在发展过程中把核心业务进行拆分成模块单元,根据需求进行分布式部署,可以进行独立团队维护开发。

  • 分层
    • 将系统在横向维度上切分成几个部分,每个部门负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和调度组成一个完整的系统
    • 比如把电商系统分成:应用层,服务层,数据层。(具体分多少个层次根据自己的业务场景)
    • 应用层:网站首页,用户中心,商品中心,购物车,红包业务,活动中心等,负责具体业务和视图展示
    • 服务层:订单服务,用户管理服务,红包服务,商品服务等,为应用层提供服务支持
    • 数据层:关系数据库,nosql数据库 等,提供数据存储查询服务
    • 分层架构是逻辑上的,在物理部署上可以部署在同一台物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,分别部署在不同的服务器上,使网站可以支撑更多用户访问
  • 分割
    • 在纵向方面对业务进行切分,将一块相对复杂的业务分割成不同的模块单元
    • 包装成高内聚低耦合的模块不仅有助于软件的开发维护,也便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展
    • 比如用户中心可以分割成:账户信息模块,订单模块,充值模块,提现模块,优惠券模块等
  • 分布式
    • 分布式应用和服务,将分层或者分割后的业务分布式部署,独立的应用服务器,数据库,缓存服务器
    • 当业务达到一定用户量的时候,再进行服务器均衡负载,数据库,缓存主从集群
    • 分布式静态资源,比如:静态资源上传cdn
    • 分布式计算,比如:使用hadoop进行大数据的分布式计算
    • 分布式数据和存储,比如:各分布节点根据哈希算法或其他算法分散存储数据
image

网站分层-图1来自网络

集群

对于用户访问集中的业务独立部署服务器,应用服务器,数据库,nosql数据库。 核心业务基本上需要搭建集群,即多台服务器部署相同的应用构成一个集群,通过负载均衡设备共同对外提供服务, 服务器集群能够为相同的服务提供更多的并发支持,因此当有更多的用户访问时,只需要向集群中加入新的机器即可, 另外可以实现当其中的某台服务器发生故障时,可以通过负载均衡的失效转移机制将请求转移至集群中其他的服务器上,因此可以提高系统的可用性

  • 应用服务器集群
    • nginx 反向代理
    • slb
    • … …
  • (关系/nosql)数据库集群
    • 主从分离,从库集群
image

通过反向代理均衡负载-图2来自网络

异步

在高并发业务中如果涉及到数据库操作,主要压力都是在数据库服务器上面,虽然使用主从分离,但是数据库操作都是在主库上操作,单台数据库服务器连接池允许的最大连接数量是有限的 
当连接数量达到最大值的时候,其他需要连接数据操作的请求就需要等待有空闲的连接,这样高并发的时候很多请求就会出现connection time out 的情况 
那么像这种高并发业务我们要如何设计开发方案可以降低数据库服务器的压力呢?

  • 如:
    • 自动弹窗签到,双11跨0点的时候并发请求签到接口
    • 双11抢红包活动
    • 双11订单入库
  • 设计考虑:
    • 逆向思维,压力在数据库,那业务接口就不进行数据库操作不就没压力了
    • 数据持久化是否允许延迟?
    • 如何让业务接口不直接操作DB,又可以让数据持久化?
  • 方案设计:
    • 像这种涉及数据库操作的高并发的业务,就要考虑使用异步了
    • 客户端发起接口请求,服务端快速响应,客户端展示结果给用户,数据库操作通过异步同步
    • 如何实现异步同步?
    • 使用消息队列,将入库的内容enqueue到消息队列中,业务接口快速响应给用户结果(可以温馨提示高峰期延迟到账)
    • 然后再写个独立程序从消息队列dequeue数据出来进行入库操作,入库成功后刷新用户相关缓存,如果入库失败记录日志,方便反馈查询和重新持久化
    • 这样一来数据库操作就只有一个程序(多线程)来完成,不会给数据带来压力
  • 补充:
    • 消息队列除了可以用在高并发业务,其他只要有相同需求的业务也是可以使用,如:短信发送中间件等
    • 高并发下异步持久化数据可能会影响用户的体验,可以通过可配置的方式,或者自动化监控资源消耗来切换时时或者使用异步,这样在正常流量的情况下可以使用时时操作数据库来提高用户体验
    • 异步同时也可以指编程上的异步函数,异步线程,在有的时候可以使用异步操作,把不需要等待结果的操作放到异步中,然后继续后面的操作,节省了等待的这部分操作的时间

缓存

高并发业务接口多数都是进行业务数据的查询,如:商品列表,商品信息,用户信息,红包信息等,这些数据都是不会经常变化,并且持久化在数据库中
高并发的情况下直接连接从库做查询操作,多台从库服务器也抗不住这么大量的连接请求数(前面说过,单台数据库服务器允许的最大连接数量是有限的)
那么我们在这种高并发的业务接口要如何设计呢?

  • 设计考虑:
    • 还是逆向思维,压力在数据库,那么我们就不进行数据库查询
    • 数据不经常变化,我们为啥要一直查询DB?
    • 数据不变化客户端为啥要向服务器请求返回一样的数据?
  • 方案设计:
    • 数据不经常变化,我们可以把数据进行缓存,缓存的方式有很多种,一般的:应用服务器直接Cache内存,主流的:存储在memcache、redis内存数据库
    • Cache是直接存储在应用服务器中,读取速度快,内存数据库服务器允许连接数可以支撑到很大,而且数据存储在内存,读取速度快,再加上主从集群,可以支撑很大的并发查询
    • 根据业务情景,使用配合客户端本地存,如果我们数据内容不经常变化,为啥要一直请求服务器获取相同数据,可以通过匹配数据版本号,如果版本号不一样接口重新查询缓存返回数据和版本号,如果一样则不查询数据直接响应
    • 这样不仅可以提高接口响应速度,也可以节约服务器带宽,虽然有些服务器带宽是按流量计费,但是也不是绝对无限的,在高并发的时候服务器带宽也可能导致请求响应慢的问题
  • 补充:
    • 缓存同时也指静态资源客户端缓存
    • cdn缓存,静态资源通过上传cdn,cdn节点缓存我们的静态资源,减少服务器压力
image

面向服务

  • SOA面向服务架构设计
  • 微服务更细粒度服务化,一系列的独立的服务共同组成系统

使用服务化思维,将核心业务或者通用的业务功能抽离成服务独立部署,对外提供接口的方式提供功能。
最理想化的设计是可以把一个复杂的系统抽离成多个服务,共同组成系统的业务,优点:松耦合,高可用性,高伸缩性,易维护。
通过面向服务化设计,独立服务器部署,均衡负载,数据库集群,可以让服务支撑更高的并发

  • 服务例子:
    • 用户行为跟踪记录统计
  • 说明:
    • 通过上报应用模块,操作事件,事件对象,等数据,记录用户的操作行为
    • 比如:记录用户在某个商品模块,点击了某一件商品,或者浏览了某一件商品
  • 背景:
    • 由于服务需要记录用户的各种操作行为,并且可以重复上报,准备接入服务的业务又是核心业务的用户行为跟踪,所以请求量很大,高峰期会产生大量并发请求。
  • 架构:
    • nodejs WEB应用服务器均衡负载
    • redis主从集群
    • mysql主
    • nodejs+express+ejs+redis+mysql
    • 服务端采用nodejs,nodejs是单进程(PM2根据cpu核数开启多个工作进程),采用事件驱动机制,适合I/O密集型业务,处理高并发能力强
  • 业务设计:
    • 并发量大,所以不能直接入库,采用:异步同步数据,消息队列
    • 请求接口上报数据,接口将上报数据push到redis的list队列中
    • nodejs写入库脚本,循环pop redis list数据,将数据存储入库,并进行相关统计Update,无数据时sleep几秒
    • 因为数据量会比较大,上报的数据表按天命名存储
  • 接口:
    • 上报数据接口
    • 统计查询接口
  • 上线跟进:
    • 服务业务基本正常
    • 每天的上报表有上千万的数据

冗余,自动化

当高并发业务所在的服务器出现宕机的时候,需要有备用服务器进行快速的替代,在应用服务器压力大的时候可以快速添加机器到集群中,所以我们就需要有备用机器可以随时待命。 最理想的方式是可以通过自动化监控服务器资源消耗来进行报警,自动切换降级方案,自动的进行服务器替换和添加操作等,通过自动化可以减少人工的操作的成本,而且可以快速操作,避免人为操作上面的失误。

  • 冗余
    • 数据库备份
    • 备用服务器
  • 自动化
    • 自动化监控
    • 自动化报警
    • 自动化降级

通过GitLab事件,我们应该反思,做了备份数据并不代表就万无一失了,我们需要保证高可用性,首先备份是否正常进行,备份数据是否可用,需要我们进行定期的检查,或者自动化监控, 还有包括如何避免人为上的操作失误问题。(不过事件中gitlab的开放性姿态,积极的处理方式还是值得学习的)

总结

高并发架构是一个不断衍变的过程,冰洞三尺非一日之寒,长城筑成非一日之功 
打好基础架构方便以后的拓展,这点很重要

image

这里重新整理了下高并发下的架构思路,举例了几个实践的例子,如果对表述内容有啥意见或者建议欢迎留言。

Mac下安装Rust

Mac 上有两种安装方式,一种是官方的命令行运行:

 $ curl https://sh.rustup.rs -sSf | sh

可以参考: Wiki:Rust 语言环境安装:Linux 开发环境(Ubuntu 18)

另一种是使用 Homebrew,这里我们使用此方法来安装,还没有安装 Homebrew 的用户需先自行安装。

以下操作使用 macOS Mojave (10.14.2) 系统,其他比较新的 Mac 系统应该也类似。

# 编辑文件
$ vim ~/.bashrc
# 在文件中加入以下两句
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
# 接下来运行命令使文件生效
$ source ~/.bashrc
# 然后就可以进行安装
curl -sSf https://mirrors.ustc.edu.cn/rust-static/rustup.sh | sh -s
# 进行最后的配置,config可能不存在,创建就完了
$ cd /root/
$ mkdir .cargo
$ cd .cargo 
$ vim config
# 在文件中填入以下内容
[registry]
index = "https://mirrors.ustc.edu.cn/crates.io-index/"
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "https://mirrors.ustc.edu.cn/crates.io-index/"

brew install rust

命令行:

$ brew install rust

会安装下载已经编译好的二进制文件,总共大几百兆,安装成功后测试下:

$ rustc --version

$ cargo --version
Mac 开发环境

Hello World

创建 hello.rs 文件,内容如下:

hello.rs

// This is a comment
// hello.rs

// main function
fn main() {

    // Print text to the console
    println!("Hello World!");
}

命令行 rustchello.rs 编译为可自行文件:

$ rustc hello.rs

接下来运行可执行文件:

$ ./hello
Hello World!
Mac 开发环境

Linux背后的思想

01Linus Torvalds
Linus Torvalds两次改变了技术,第一次是Linux内核,它帮助互联网的发展;第二次是Git,全球开发者使用的源代码管理系统。在一次TED的采访中,Torvalds以极其开放的态度讨论了他独特的工作方式和性格特点。Torvalds说:“我不是一个空想家,我是一名工程师,我非常乐意跟梦想家在一起,他们行走四方,仰望苍穹,看着满天星辰说,“我想到那儿去。”但我是低头看路的那种人,我只想填好眼前这个坑,不让自己掉进去,这就是我。”

02关于开源
Linus TorvaldsLinux并不是一个合作的产物,它是我一系列项目中的一个,纯粹出于自己当时的需要,部分原因是我需要得到结果,但更重要的原因是我享受编程。这段旅程的终点,在25年后的今天(2016),我们仍未达到。当年我只是想做一个完全属于自己的项目,我压根就没想过开源这件事。但在那之后,随着项目越来越大, 你会开始想让别人知道。感觉就像“哇,快来看看我的成果!”
成千上万的人想参与进来(Linux内核项目),但很多时候,我成为了那个断点,我无法让自己跨出那一步,同上千人合作。
因此Git是我的第二个大项目,它存在的意义就是维护我的第一个大项目。事实上这就是我的工作方式。我编程并不是为了… ,我编程是因为好玩,但我也想做一些有意义的事情,因此我设计每一个程序仅仅是因为我自己需要。
而我喜欢开源软件的一点就是,它能让形形色色的人在一起合作。我们不必相互喜欢,有时候我们甚至互相讨厌。是真的,我们经常吵得不可开交。
科学界的开源显然是一种回归,科学最初是开源的。但之后变得越来越封闭,只存在那些昂贵的科学期刊上。开源让科学回归了,我们有了arXiv和开放期刊。小编有话说

Linux并不是选择了开源,只是因为开源恰好是Linux需要的。就如Linus Torvalds所说:“纯粹出于自己当时的需要。”

Linus Torvalds是睿智的,做好自己能控制的。

开源不仅仅代表源代码的开放,开源更是一种工作方式,一种教育方式。因为有了开源,我们多了一种更好的合作共赢的工作方式;因为有了开源,让更多从业者和学生能够学习到更好的技术。
03代码的品味Linus Torvalds有时候你可以换个角度看问题,重写代码,排除特例,完美覆盖所有情况,这就是好的代码。同时也很简单,这是最基本的原则。细节非常重要。对我来说,我愿意与之共事的人,必须有好的品位。

大牛们总是对自己严格要求,不仅仅是要实现功能,并且要优雅的实现。下面我们来看看采访中Linus Torvalds对比的两段代码:

1. 不怎么漂亮的代码

remove_list_entry(entry)
{ prev = NULL;
walk = head;
// Walk the list
while (walk != entry) {

prev = walk;
walk = walk->next;   
}    
// Remove the entry by updating the
// head or the previous entry
if(!prev) {
head = entry->next;
} else {
prev->next = entry->next;   
}
}

上面的代码,需要区分要移除的成员是否为链表的头一个成员。需要单独处理特例情况(要移除的成员为链表的头一个成员)。这个函数比较好理解,这里小编就不做更多的解释了,如有疑问,请添加小编微信交流。

2. 好的代码

remove_list_entry(entry){    
// The "indirect" pointer points to the
// *address* of the thing we'll update
indirect = &head;
// Walk the list, looking for the thing that

// points to the entry we want to remove
while ((*indirect) != entry))
{
indirect = &(*indirect)->next; }
// .. and just remove it

*indirect = entry->next;
}

这个代码完全不需要单独处理特例情况,程序整体更加整洁、优雅。其实现原理为:指针变量indirect保存的是链表成员结构体中的next成员的地址(head指针也可这样看),如下图所示:

所以变量*indirect就相当于是前一个链表成员的next成员(相对于要移除的成员来说)。当找到要移除的成员后,进行如下操作即可:

*indirect = entry->next;

行业内关于智能客服、聊天机器人的应用和架构、算法分享和介绍

阿里巴巴

小蜜

通用领域对话问答

非技术推广

其他

天猫

蚂蚁

闲鱼

云问(拼多多、当当)

携程

去哪儿

京东

Uber

58同城

饿了么

美团

滴滴

瓜子

小冰

苏宁

贝壳

第四范式

腾讯

其他

聊天机器人介绍

其他

汇总

对话管理

知识图谱

智能客服

智齿

智齿 | 机器人来当客服 会是什么样?

知识库

APP架构设计经验谈:接口的设计

App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉。

安全机制的设计

现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:

  1. 用户用密码登录成功后,服务器返回token给客户端;
  2. 客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
  3. 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
    • token错误,这时需要用户重新登录,获取正确的token
    • token过期,这时客户端需要再发起一次认证请求,获取新的token

然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。

如何优化呢?第一种解决方案是采用HTTPS。HTTPS在HTTP的基础上添加了SSL安全协议,自动对数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,安全性可以提高很多。不过,SSL也不是绝对安全的,也存在被劫持的可能。另外,服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式。

我们目前的做法是给每个接口都添加签名。给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证。类似的实现可参考OAuth1.0的签名算法。这样,黑客不知道密钥,不知道签名算法,就算拦截到登录接口,后续请求也无法成功操作。不过,因为签名算法比较麻烦,而且容易出错,只适合对内的接口。如果你们的接口属于开放的API,则不太适合这种签名认证的方式了,建议还是使用OAuth2.0的认证机制。

我们也给每个端分配一个appKey,比如Android、iOS、微信三端,每个端分别分配一个appKey和一个密钥。没有传appKey的请求将报错,传错了appKey的请求也将报错。这样,安全性方面又加多了一层防御,同时也方便对不同端做一些不同的处理策略。

另外,现在越来越多App取消了密码登录,而采用手机号+短信验证码的登录方式,我在当前的项目中也采用了这种登录方式。这种登录方式有几种好处:

  1. 不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;
  2. 用户不再需要记住密码了,也不怕密码泄露的问题了;
  3. 相对于密码登录其安全性明显提高了。

接口数据的设计

接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:

  • Number:整数或浮点数
  • String:字符串
  • Boolean:true 或 false
  • Array:数组包含在方括号[]中
  • Object:对象包含在大括号{}中
  • Null:空类型

所以,传输的数据类型不能超过这六种数据类型。以前,我们曾经试过传输Date类型,它会转为类似于”2016年1月7日 09时17分42秒 GMT+08:00″这样的字符串,这在转换时会产生问题,不同的解析库解析方式可能不同,有的可能会转乱,有的可能直接异常了。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。

另外,以前的项目中还出现过字符串的”true”和”false”,或者字符串的数字,甚至还出现过字符串的”null”,导致解析错误,尤其是”null”,导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理。

服务器返回的数据结构,一般为:

{
    code:0,
    message: "success",
    data: { key1: value1, key2: value2, ... }
}
  • code: 返回码,0表示成功,非0表示各种不同的错误
  • message: 描述信息,成功时为”success”,错误时则是错误信息
  • data: 成功时返回的数据,类型为对象或数组

不同错误需要定义不同的返回码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:

  • 0:成功
  • 100:请求错误
  • 101:缺少appKey
  • 102:缺少签名
  • 103:缺少参数
  • 200:服务器出错
  • 201:服务不可用
  • 202:服务器正在重启

错误信息一般有两种用途:一是客户端开发人员调试时看具体是什么错误;二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。

data字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:

// 正确
data: { token: 123456 }

// 错误
data: 123456

接口版本的设计

接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:

  • 数据的变化,比如增加了旧版本不支持的数据类型
  • 参数的变化,比如新增了参数
  • 接口的废弃,不再使用该接口了

为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:

  1. 每个接口有各自的版本,一般为接口添加个version的参数。
  2. 整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2。

大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。

如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。

有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。

写在最后

关于接口设计,暂时想到的就这么多了。各位看官看完觉得有遗漏或有哪些需要优化的欢迎提出一起讨论。

持续交付2.0:业务引领的DevOps精要

经典图书《持续交付》已出版8年,一直受到软件行业从业者的关注。书中的软件开发原则和实践也随着商业环境VUCA特性的明显增强而逐渐受到软件技术人员的认可。

VUCA是volatility(易变性)、uncertainty(不确定性)、complexity(复杂性)和ambiguity(模糊性)的首字母缩写。VUCA这个术语源于军事用语,在20世纪90年代开始被普遍使用,用来描述冷战结束后的越发不稳定的、不确定的、复杂、模棱两可和多边的世界。在2001年9月11日恐怖袭击发生之后,这一概念和首字母缩写才真正被确定。随后,VUCA被战略性商业领袖们用来描述已成为“新常态”的、混乱的和快速变化的商业环境。

然而,在应用这些为达成持续交付目标所需的软件技术相关原则与实践时,我们会遇到很多难题。例如,业务压力太大,没有时间改进;开发和测试的时间被压缩得太少了,没有时间这么干;这么做的风险太高了,质量很难保障。而这些难题正是由于软件工程的发展惯性带来的,是到了改变的时候了。

1.1 软件工程发展概述

“软件工程”这一学科出现于1968年,当时正值第一次软件危机。第一次软件危机是落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。人们试图借鉴建筑工程领域的工程方法来解决这一问题,以实现“按预算准时交付所需功能的软件项目”的愿望。

1.1.1 瀑布软件开发方法

瀑布软件开发模型由Dr. Winston W. Rovce在1970年发表的“Managing the development of large software systems”一文中首次提出,如图1-1所示。它将软件开发过程定义为多个阶段,每个阶段均有严格的输入和输出标准,项目管理者希望通过这种重计划、重流程、重文档的方式来解决软件危机。很多人将具有以上3个特征的软件开发方法统称为“重型软件开发方法”。

1-1{65%}

图1-1 瀑布软件开发模型

在20世纪,瀑布软件开发模型的每个阶段都需要花费数月的时间。在写出第一行产品代码之前,甲乙双方需要花费大量精力确定需求范围,审核比《新华字典》厚得多的软件需求规格说明书。即便如此,双方还是要为“是否发生了需求范围的变更”“是否准时交付了软件”“交付的软件是否满足了预先设定的业务需求”而纠缠不清。

1.1.2 敏捷软件开发方法

从20世纪80年代开始,微型计算机开始快速普及。20世纪90年代,人们对软件的需求迅速扩大。然而,Standish Group的Chaos Report 1994显示,软件交付项目的失败或交付困难的比率仍旧很高(成功率只有16.2%,受到挑战的项目占比52.7%,而失败率为31.1%)。此时,很多优秀的软件工作者不满意瀑布软件开发方法的交付成果,并在各自工作实践中总结了各种新的软件开发方法,例如我们现在经常听说的Scrum和极限编程,都是在那个时代涌现出来的软件开发方法。

2001年,17位软件大师齐聚加拿大的一个小镇——雪鸟(Snowbird),总结了当时涌现的这些轻量级软件开发方法所具备的特点,共同发表了“敏捷宣言”,提出敏捷软件开发方法应该遵循的十二原则,见附录A。与会人员一致同意,凡符合这一宣言所倡导的价值观且遵循十二开发原则的方法均可被认为是“敏捷软件开发方法”。因此,“敏捷软件开发方法”这一说法从其诞生开始就是一簇软件开发方法的代名词,而不是特指某一种软件开发方法。

敏捷软件开发方法强调发挥人的主观能动性,提倡面对面沟通、拥抱变化、通过迭代和增量开发尽早交付有价值的软件。此时,很多团队已认识到,软件开发实际上是一个不断迭代学习的过程,即软件工程师需要快速学习并理解领域知识,并将其转化成数字世界的表达形式,通过与业务专家的交流讨论来学习并持续迭代这个过程。一个软件交付计划被划分成多个迭代,强调在每个迭代结束时应该得到可运行的软件,如图1-2所示。

1-2{80%}

图1-2 迭代开发,多批次部署发布

与瀑布软件开发方法只在项目交付后期才能看到可运行的软件相比,敏捷软件开发方法在这方面有很大的进步。“持续集成”作为敏捷开发方法中的一个工程实践,率先被更广泛的IT组织所接受,即便那些没有采纳敏捷开发方法的团队也会使用它,因为其强调的频繁自动化构建和自动化测试减少了质量保障团队的重复工作量,也排除了开发团队与质量保障团队之间的沟通障碍。

当时的主流软件需求仍旧是来自企业级定制软件开发。虽然敏捷开发方法使用的是迭代模型,但两次软件发布之间的间隔时间仍旧较长(通常是数月,甚至一年以上)。因此,业务人员与研发团队之间关于需求变更和研发效率的矛盾仍旧是主要矛盾。系统的部署发布工作在整个发布周期中所占用的时间和成本相对较小,部署和运维工作还不是突出矛盾。另外,部署活动通常由专门的技术运维团队执行,产品研发团队对其无感。

在这一时期,无论瀑布开发还是敏捷开发,在软件行业中最重要的关注点都是可交付的软件包本身,即如何快速地将软件需求变为可交付的软件包。

1.1.3 DevOps运动

DevOps的萌芽源于2008年8月敏捷大会多伦多站的一个临时话题“敏捷基础设施(Agile Infrastructure)”。当时比利时独立IT咨询师Patrick Debois非常感兴趣,并且分享了关于“将敏捷实践应用于运维领域”。2009年10月,Patrick在比利时的根特组织了一个“DevOpsDays”社区会议,并正式启用了“DevOps”这个术语。

2010年“The Agile Admin”网站上发表的一篇题为“What is DevOps”(什么是DevOps)的文章中指出:“DevOps是一组概念集合的代称,虽然并非全部都是新概念,但它们已经催化为一种运动,并迅速在整个技术社区中传播。”同时,该文章也给出了其原始定义,即“DevOps是运维工程师和开发工程师参与整个服务生命周期(从设计到开发再到生产支持)的一组实践”,并提出,DevOps应该倡导运维人员更多地使用和开发人员使用的相同技术来进行系统运维工作。

DevOps在维基百科上的定义也在随着时间的推进而不断变化着。截止到2017年,其定义为:

DevOps是一种软件工程文化和实践,旨在统一整合软件开发和软件运维。DevOps运动的主要特点是强烈倡导对构建软件的所有环节(从集成、测试、发布到部署和基础架构管理)进行全面的自动化和监控。DevOps的目标是缩短开发周期,提高部署频率和更可靠地发布,与业务目标保持一致。

事实上,到写作本书之时,业界对DevOps并没有统一的标准定义。正如“敏捷”一样,每一位从业者、每一个企业都有自己所理解的DevOps。有些人认为DevOps是敏捷的一个子集,有些人认为“敏捷做对了,就是DevOps”,还有人则将DevOps看作围绕自动化实施的一套实践,或多或少与敏捷有些关联性。

从历史时间点上来看,DevOps源于敏捷思想和实践在运维领域的应用,但当时的实操指导性不足,而近乎同期出版的《持续交付》一书使其更加具象化,在企业实践方面更具有可操作性。书中给出了一系列的原则、方法与实践,使DevOps运动的参与者有线索可循,例如持续交付中强烈倡导的“一切皆代码,自动化一切,部署流水线尽早反馈”等。

DevOps并非一个标准、一种模式或者一套固定方法,而是一种IT组织管理的发展趋势,也就是说,通过多种方式打破IT职能部门之间的隔阂,改变IT组织内部的原有合作模式,使之更紧密结合,从而促进业务迭代速度更快。这种发展趋势将会引起IT组织内部原有角色与分工的变化,甚至范围更大,会影响到相关的业务组织。对互联网公司来说,其软件产品对业务发展起到极其关键的作用,业务结果与IT效能强关联,因此顺应这一发展趋势的动力更加明显和迫切。

既然DevOps是一种组织管理的发展趋势,那么它就是IT领域普适的。对于不同行业、不同企业中的IT组织,需要根据其所在行业的行业特点以及企业实际状况进行一系列的管理定制。

1.1.4 持续交付1.0

2006年,Jez Humble、Chris Read和Dan North共同发表了一篇题为“The Deployment Production Line”(部署生产线)的文章。文中讨论了软件部署带来的生产效率问题,并首次提出“部署生产线”模式:

……测试和部署是软件开发过程中最困难且耗时的阶段。即使团队已经使用自动构建完成了代码的测试工作,也需要几天时间做生产部署的情况仍旧很常见……我们描述的原则和实践使你可以一键创建、配置和部署新的环境。

……通过多阶段自动化工作流程,测试和部署过程可以完全自动化。利用这种“部署生产线”,可以将已经过验证的代码快速部署到生产环境中,并且一旦发生问题,就可以轻松地回退到以前的版本。

该文章的3位作者当年均就职于ThoughtWorks公司,而该公司是敏捷软件开发方法的践行者、倡导者和推广者。该公司使用的软件开发方法也源自极限编程方法。作者通过各自的实践总结出了“部署流水线”模式的雏形,并且对如何使自动化部署活动更轻松给出了以下4条指导原则。

(1)每个构建阶段都应该交付可工作的软件,即对于中间产物的生成(例如搭建软件框架)不应该是一个单独的阶段。

(2)用同一个制品(artifacts)向不同类型的环境部署,即将其与运行时配置分开管理。

(3)自动化测试和部署,即根据测试目的,分成几个独立的质量关卡。

(4)这个部署生产线设计也应该随着你的应用程序的发展而不断演进。

2007年,ThoughtWorks公司的Dave Farley也发表了一篇题为“The Deployment Pipeline – Extending the range of Continuous Integration”(部署流水线——持续集成的延伸)的文章。文中指出,部署流水线就是通过自动化方式将多个质量验证关卡及其中的验证内容联系在一起,如图1-3所示。

1-3{80%}

图1-3 Dave Farley在2007年定义的部署流水线

同年12月,ThoughtWorks在北京正式组建了产品研发团队,启动了以这种部署流水线为指导思想的软件持续发布管理工具的研发。当时Jez Humble是产品经理,我负责该产品的交付与业务分析。该产品的第一个版本发布于2008年7月,取名为Cruise(现名为GoCD)。其最重要的一个功能特性就是“部署流水线”(deployment pipeline)。而且很多特性设计(包括内置的制品仓库、多阶段之间的制品引用)也体现了“一次构建,多次使用”的原则。

在开发GoCD期间,Jez Humble和Dave Farley合著了《持续交付》(Continuous Delivery

)一书,英文版于2010年正式出版,该书于2011年获得Jolt杰出大奖,中文版也于2011年在国内上市,这标志着“持续交付”这一术语的正式诞生。Jez Humble说:“持续交付是一种能力,也就是说,能够以可持续方式,安全快速地把代码变更(包括特性、配置、缺陷和试验)部署到生产环境上,让用户使用。”本书将这一定义称为“持续交付1.0”。

在《持续交付》一书中,讲述了持续交付1.0所遵循的理念、原则和众多方法与实践,并在该书最后一章指出:“它不仅仅是一种新的软件交付方法论,而且对依赖软件的业务来说,是一个全新的范式[1]
。”

持续交付1.0提供的很多原则及方法是DevOps运动的具体实操指引,它们可以为企业的IT团队将DevOps运动落地实施提供非常具体的指导,如图1-4所示。

1-4{80%}

图1-4 持续交付将发布权交还给业务方

从所涉及的协作角色来看,敏捷开发更多地涉及产品需求方、软件开发工程师和软件测试工程师。在历史上,DevOps更多地涉及软件研发团队(包括开发工程师和测试工程师)与运维工程师,而持续交付1.0涉及产品需求方、软件研发团队和运维工程师,如图1-5所示。

1-5{60%}

图1-5 相关概念在组织角色的主要触达点

持续部署与持续交付

 

很多人认为,持续部署(continuous deployment)是持续交付(continuous delivery)的进阶状态,是指代码提交后一旦成功通过所有质量验证,就立即自动部署到生产环境中,不需要任何人的审批。事实上,“部署”与“交付”这两个主干词的意义并不相同。

“部署”是一种技术领域的操作,也就是说,从某处获取软件包,并按照预先设计的方案将其安装到计算节点上,并确保系统可以正常启动,但它并不一定意味着“必须包含业务功能的发布或交付”。“交付”则是一个业务决策活动,通常也被称为“发布”,也就是说,如果将新构建的特性交付到客户(用户)手中,用户就可以看到并使用它们。

之所以“部署”与“发布”几乎成为等价词,是历史原因造成的。很久以前,软件的发布周期较长,每次新功能部署之后就会立即发布。久而久之,“部署”就成了“发布”的代名词。为了保证软件质量,IT部门通常不允许无关代码(即与本次发布新特性集合无关,例如未开发完成的功能、不完善的功能集)被带到生产环境中。因此,每次部署就一定是重大功能的发布。

随着互联网软件的出现,“部署”和“发布”内容与频率不同的情况也是很常见的。我们可以向环境多次部署,但只有当业务需要时才向用户发布,如图1-4中的箭头所示。例如,Facebook公司的发布工程师Chuck Rossi在2011年该公司举办的技术分享会上指出:“我们现在每天会对Facebook网站进行一次部署操作……Facebook公司在半年后将要主推的功能特性,现在已经上线了,只是用户看不到而已。”

1.2 持续交付2.0

持续交付1.0关注于“从提交代码到产品发布”的过程,如图1-6所示,并且提供了一系列工作原则和优秀的实践方法,可以提升软件开发活动的效率。

1-6{55%}

图1-6 持续交付1.0的关注点

但是,我在实际咨询过程中发现,一些软件功能在开发完成之后,对用户或者业务来说,并没有产生什么影响,有些功能根本没有用户来使用。可是,这些功能的确花费了团队的很多精力才设计实现。这是一种巨大的浪费。这种“无用”的功能生产得越多,浪费就越大。我们是否可以找到一些方法,让我们付出的努力对业务改善更加有效,或者只用很少的成本就可以验证对业务无效呢?

1.2.1 精益思想

2011年出版的《精益创业》一书给了我一些启示。其核心思想是,开发新产品时,先做出一个简单的原型——最小化可行产品(Minimum Viable Product,MVP),这个原型的目标并不是马上生产出一个完美的产品,而是为了验证自己心中的商业假设。得到用户的真实反馈后,从每次试验的结果中学习,再快速迭代,持续修正,在资源耗尽前从迷雾中找到通往成功的道路,最终适应市场的需求。

Eric Rise在书中强调,精益创业就是一个“开发—测量—认知”的验证学习过程,如图1-7所示。也就是说,把创意快速转化为产品,衡量顾客的反馈,然后再决定是改弦更张,还是坚守不移。

该书主要关注于创业初始阶段,将精益思想贯穿于产品“从0到1”的过程。事实上,它也可以用于产品“从1到n
”的过程中。

1996年,Womack、Jones和Roos在《精益思想》一书中指出,精益思想是指导企业根据用户需求,定义企业生产价值,按照价值流来组织全部生产活动,使价值在生产活动之间流动起来,由需求拉动产品的生产,从而识别整个生产过程中不经意间产生的浪费,并消除之。

1-7{45%}

图1-7 “开发—测量—认知”环

在精益管理理论中,“浪费”是指从客户角度出发,对优质产品与良好服务不增加价值的生产活动或管理流程。并指出,业务生产中所有活动都可以归结为以下两种活动,也就是增加价值的活动和不增加价值的活动,而不增加价值的活动就是浪费。在被归类为“浪费”的活动中,又可以分为必要的非增值活动和纯粹的浪费。必要的非增值活动是指从客户的角度看虽没有价值,却可以避免(潜在的)更大的浪费或降低系统性风险的活动,如图1-8所示。

1-8{65%}

图1-8 软件产品开发中的活动浪费

例如,生产流水线上的装配工作是增值活动,质量检查是必要的非增值活动,而因材料供应不足产生的生产等待以及因质量缺陷导致的返工都被认为是不必要的浪费。在软件产品服务的全生命周期中,也同样包含多种“浪费”,例如,无效果的功能特性、各生产环节中的等待(如图1-9所示)、没人看的文档、软件缺陷、机械性的重复工作等。

1-9{75%}

图1-9 用户视角的增值活动与浪费

尽管“消除所有浪费”几乎是不可能的,但是,我们仍旧要全面贯彻“识别和消除一切浪费”的理念,持续不断地优化流程与工作方式,达到高质量、低成本、无风险地快速交付客户价值的目的。

1.2.2 双环模型

自2009年Flickr(一个聚合全球知名热门图片分享网站)声称其网站每天部署10次之时起,“主干开发+持续集成+持续发布”已成为硅谷知名互联网公司应对VUCA环境的一种主流软件研发管理模式。这种变化的原动力并不是来自技术团队本身,而是来自业务与产品方的诉求。为了在VUCA环境中更快地了解海量用户,快速验证大量业务假设和解决方案,他们改变了业务探索的模式,并催生了软件研发管理模式的改变,两种模式相互促进,从而形成了互联网软件产品研发管理的双环模式,即“持续交付2.0”,如图1-10所示。

1-10{65%}

图1-10 持续交付2.0的双环模型

“持续交付2.0”是一种产品研发管理思维框架。它将精益创业与持续交付1.0相结合,强调业务与IT间的快速闭环,以“精益思想”为指导,全面贯彻“识别和消除一切浪费”的理念,通过一系列工作原则与实践,帮助企业以一种可持续方式,高质量、低成本、无风险地快速交付客户价值。

对企业来说,开发软件产品的目标是创造客户价值。因此,我们不应该仅仅关注快速开发软件功能,同时还应该关注我们所交付的软件的业务正确性,以及如何以有限的资源快速验证和解决业务问题。也就是说,不断探索发现真正要解决的业务问题,提出科学的目标,设计最小可行解决方案。通过快速实现解决方案并从真实反馈中收集数据,以验证该问题是否得以解决。这是一个从业务问题出发,到业务问题解决的完整业务闭环,简称为持续交付“8”字环。

它由两个相连的环组成:第一个环为“探索环”,其主要目标是识别和定义业务问题,并制订出最小可行解决方案进入第二个环;第二个环为“验证环”,其主要目标是以最快速度交付最小可行方案,可靠地收集真实反馈,并分析和验证业务问题的解决效果,以便决定下一步行动,如图1-10所示。

探索环包含4个可持续循环步骤,分别是提问、锚定、共创和精炼。

(1)提问,即定义问题。通过有针对性的提问,找出客户的具体需求,并找出具体需求后的原因,即具体需求后要解决的根本问题。在提问中形成团队期望达成的业务目标或者想要解决的业务问题。如果问题无法清晰定义,那么找到的答案自然就会有偏差。因此,在寻找答案之前,应该先清晰地定义问题。

(2)锚定,即定义结果目标指示器。针对问题进行信息收集,经过分析,去除干扰信息,识别问题假设,得到适当的衡量指标项,并用其描述现在的状况,同时讨论并定义我们接下来的行动所期望的结果。

(3)共创,即共同探索和创造解决或验证该问题的多种具有可行性的解决方案。

(4)精炼,即对所有的可行试验方案进行选择,找到最小可行性解决方案,它既可能是单个方案,也可能是多个方案的组合。

验证环也包含4个可持续循环的步骤,分别是构建、运行、监测和决策。

(1)构建:是指根据非数字化描述,将最小可行性解决方案准确地转换成符合质量要求的软件包。

(2)运行:是指将达到质量要求的软件包部署到生产环境或交到用户手中,并使之为用户提供服务。

(3)监测:是指收集生产系统中产生的数据,对系统进行监控,确保其正常运行。同时将业务数据以适当的形式及时呈现出来。

(4)决策:是指将收集到的数据信息与探索环得出的对应目标进行对比分析,做出决策,确定下一步的方向。

探索环就像是一部车子的前轮,把握前进方向。验证环则像车子的后轮,使车子平稳且驱动快速前进。它们之间相互促进,探索环产生的可行性方案规模越小,越能够提高验证环的运转速度;如果价值验证环能够提高运转速度,则有利于探索环尽早得到真实反馈,有利于快速决策,及时对前进方向进行验证或调整。

1.2.3 4个核心原则

“持续交付2.0”是指企业能够以可持续发展的方式,在高质量、低成本及无风险的前提下,不断缩短持续交付“8”字环周期,从而与企业外部频繁互动,获得及时且真实的反馈,最终创造更多客户价值的能力。下面逐一介绍缩短持续交付“8”字环周期的4个核心工作原则。

1.坚持少做

在咨询的过程中,最常听到的一句话就是:“我们最大的问题是人力不足。”无论公司实力如何,想做的事情永远超过自己的交付能力,需求永远做不完。然而,做得多就一定有效吗?我们应该抵住“通过大量计划来构建最佳功能”的诱惑,坚持少做,想办法对新创意尽早验证。

Moran在Do It Wrong Quickly
一书中写道:“Netflix认为,他们想做的事情中,可能有90%是错误的。”Ronny Kohavi等共同发布的文章“Online Experimentation at Microsoft”中也指出,在微软,“那些旨在改进关键指标而精心设计和开发的功能特性中,只有1/3左右成功地改进了关键指标”。

正如当年Mike Krieger(Instagram的联合创始人兼CTO)被问及“5个工程师如何支持4000万用户”时所说的那样——“少做,先做简单的事情”。

2.持续分解问题

复杂的业务问题中一定会包含很多不确定因素,它们会影响问题解决的速度和质量。在实施解决方案之前,通过对问题的层层分解,可以让团队更了解业务,更早识别出风险。企业应该坚信,即便是很大的课题或者大范围的变更,也可以将其分解为一系列小变更,快速解决,并得到反馈,从而尽早消除风险。与其设计一大堆特性,再策划一个持续数月的一次性发布,不如持续不断地尝试新想法,并各自独立发布给用户。

3.坚持快速反馈

当把问题分解以后,如果我们仍旧只是一味地埋头苦干,而忽视对每项已完成工作的结果反馈,那么就失去了由问题分解带来的另一半收益,确认风险降低或解除。只有通过快速反馈,我们才能尽早了解所完成工作的质量和效果。

4.持续改进并衡量

无论做了什么样的改进,如果无法以某种方式衡量它的结果,就无法证明真的得到了改进。在着手解决每个问题之前,我们都要找到适当的衡量方式,并将其与对应的功能需求放在同等重要的位置上,一起完成。

某数据公司就曾因无度量数据,而无法提出有效改进方向的情况。该系统是一个数据标注志愿者招募考试系统。虽然它被分成多个迭代,每个迭代都发布了很多功能,但是,由于没有实现产品人员所关注的数据收集与统计分析功能,使得团队仅知道人们可以使用这个系统完成工作,却无法知道是否能够高效完成工作,也很难提出下一步的产品优化方向。

1.2.4 持续交付七巧板

我们讨论了“持续交付2.0”的指导思想、工作理念和核心原则。大家很容易意识到,它对适应快速变化的市场环境和激烈的市场竞争是非常有效的。那么,企业如何让“持续交付2.0”成为一种组织能力,成为组织的DNA,持续发挥作用,从而领先竞争对手,成为自己的一种竞争优势呢?

持续交付双环模型的实施与改进将涉及企业内的多个部门与不同的角色,无法由某个部门独立实施,必须在整个组织范围内贯彻执行“持续交付2.0”的思想、理念与原则。企业需要在组织管理机制、基础设施以及软件系统架构3个方面付诸行动,而每一个方面都包含多项内容,如图1-11所示。

1-11{55%}

图1-11 持续交付七巧板

条条大路通罗马,而且,罗马也不是一天建成的。每个企业的实施路径可能各不相同,所需要的周期也各有长短,对各方面的能力需求也不完全一致。正如中国传统玩具七巧板一样,每个企业都应根据自己的意愿和诉求,拼出属于自己的持续交付实践地图。

1.3 小结

“持续交付2.0”建立在“持续交付1.0”的“可持续地快速发布软件服务”及精益创业的“最小化可行产品”两种理念基础之上,强调要以业务为导向,从一开始就将业务问题进行分解,并通过不断的科学探索与快速验证,减少浪费的同时,快速找到正确的业务前进方向,简称为“双环模型”。因此,其涉及组织中的多个团队,需要各个团队之间紧密合作,才能缩短“8”字环的周期,如图1-12所示。

1-12{70%}

图1-12 持续交付2.0的相关角色

“持续交付2.0”的4个核心工作原则是坚持少做、持续分解问题、坚持快速反馈和持续改进并衡量。只有这样,才能不断缩短持续交付“8”字环的运行周期,提升用户反馈速度,从而提高业务的敏捷性。这要求管理者跳出原有软件交付管理思维模式,摆脱“害怕失败”的恐惧感,拥抱“科学探索—快速验证”思维方法,快速试错,提升持续交付能力,进而发展现有业务,并快速开创新业务。