标签 Trace 下的文章

Go 1.22新特性前瞻

本文永久链接 – https://tonybai.com/2023/12/25/go-1-22-foresight

美国时间2023年12月20日,Go官方宣布Go 1.22rc1发布,开启了为期2个多月的、常规的公测之旅,Go 1.22预计将于2024.2月份正式发布!

除了在官网下载Go 1.22rc1版本进行新特性体验之外,我们还可以通过在线的Go Playground选择“Go dev branch”来体验(相比下载安装,在线版本体验会有一些局限):

注:关于Go的多种安装方法,《Go语言第一课》专栏有系统全面的讲解,欢迎订阅阅读。

本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早,下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中,所以切记一切变更点要以最终Go 1.22版本发布时为准。

1. 语言变化

Go 1.22的语言特性变化主要是围绕for loop的

1.1 loopvar试验特性转正

Go 1.21版本中,作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥,我们来看一下下面这个最能说明问题的示例:

// go1.22-foresight/lang/for-range/for_range.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i, v := range sl {
        wg.Add(1)
        go func() {
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}

我们分别用Go 1.22rc1和Go 1.21.0来运行上面这段代码:

// 使用go 1.22rc1的运行结果:

$go run for_range.go
4 : 15
1 : 12
0 : 11
3 : 14
2 : 13

// 使用go 1.21.0的运行结果:
$go run for_range.go
4 : 15
4 : 15
4 : 15
4 : 15
4 : 15

之所以存在差异,是因为Go 1.22版本开始,for range语句中声明的循环变量(比如这里的i和v)不再是整个loop一份(loop var per loop),而是每次iteration都会有自己的变量(loop var per-iteration),这样在Go 1.22中,for range中的goroutine启动的闭包函数中捕获的变量是loop var per-iteration,这样才会输出5个不同的索引值和对应的切片值。

注:关于Go 1.22版本之前的for range的坑,《Go语言第一课》专栏有图文并茂的原理讲解,欢迎订阅阅读。

那传统的3-clause的for loop呢?其中的循环变量的语义是否也发生变化了呢?我们看下面示例:

// go1.22-foresight/lang/for-range/classic_for_loop.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i := 0; i < len(sl); i++ {
        wg.Add(1)
        go func() {
            v := sl[i]
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}

我们依然分别用Go 1.22rc1和Go 1.21.0版本运行这段代码,得到的结果如下:

// 使用go 1.22rc1的运行结果:

$go run classic_for_loop.go
0 : 11
4 : 15
2 : 13
3 : 14
1 : 12

// 使用go 1.21.0的运行结果:

$go run classic_for_loop.go
panic: runtime error: index out of range [5] with length 5

goroutine 20 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
panic: runtime error: index out of range [5] with length 5

goroutine 19 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
exit status 2

从输出结果来看,3-clause的for语句中声明的循环变量也变成了loop var per-iteration了。

在Go 1.22之前,go vet工具在遇到像上面代码那样在闭包中引用循环变量的情况时会给出警告,但由于Go 1.22的这个语义修正,go vet对于Go 1.22及以后版本(根据go.mod中的指示)的类似Go代码将不再报错。

不过就像Russ Cox在spec: less error-prone loop variable scoping这一issue中提及那样,该特性落地可能会带来不兼容问题,即对存量代码行为的破坏性改变。为此Go团队提供了一个名为bisect的工具,该工具可以检测出存量代码在for loop语义发生变更后是否会导致问题。不过该工具似乎只能与go test一起使用,也就是说你只能对那些被Go测试覆盖到的for loop进行检测。

目前spec: less error-prone loop variable scoping这一issue还处于open状态,也没有放入Go 1.22 milestone中,不知道后续是否还会存在变数!

1.2 range支持整型表达式

在Go 1.22版本中,for range后面的range表达式除了支持传统的像数组、切片、map、channel等表达式外,还支持放置整型表达式,比如下面这个例子:

// lang/range-expr-support-integer/main.go

func main() {
    n := 5
    for i := range n {
        fmt.Println(i)
    }
}

我们知道:for range会在执行伊始对range表达式做一次求值,这里对n求值结果为5。按照新增的for range后接整型表达式的语义,对于整数值n,for range每次迭代值会从0到n-1按递增顺序进行。上面代码中的for range会从0迭代到4(5-1),我们执行一下上述代码就可以印证这一点:

$go run main.go
0
1
2
3
4

如果n <= 0,则循环不运行任何迭代。

这个新语法特性,可以理解为是一种“语法糖”,是下面等价代码的“语法糖”:

for i := 0; i < 5; i++ {
    fmt.Println(i)
}

不过,迭代总是从0开始,似乎限制了该语法糖的使用范围。

1.3 试验特性:range-over-function iterators

在for range支持整型表达式的时候,Go团队也考虑了增加函数迭代器(iterator),不过前者语义清晰,实现简单。后者展现形式、语义和实现都非常复杂,于是在Go 1.22中,函数迭代器以试验特性提供,通过GOEXPERIMENT=rangefunc可以体验该功能特性。

在没有函数迭代器之前,我们实现一个通用的反向迭代切片的函数可能是像这样:

// lang/range-over-function-iterator/backward_iterate_slice_old.go

func Backward(s []E) func(func(int, E) bool) {
    return func(yield func(int, E) bool) {
        for i := len(s)-1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
        return
    }
}

下面是在Go 1.21.0版本中使用上面Backward函数的方式:

// lang/range-over-function-iterator/backward_iterate_slice_old.go

func main() {
    sl := []string{"hello", "world", "golang"}
    Backward(sl)(func(i int, s string) bool {
        fmt.Printf("%d : %s\n", i, s)
        return true
    })
}

我们用Go 1.21.0运行一下上述示例:

$go run backward_iterate_slice_old.go
2 : golang
1 : world
0 : hello

在以前版本中,这种对切片、数组或map中进行元素迭代的情况在实际开发中非常常见,也比较模式化,但基于目前语法,使用起来非常不便。于是Go团队提出将它们与for range结合在一起的提案。有了range-over-function iterator机制后,我们就可以像下面这样使用Backward泛型函数了:

// lang/range-over-function-iterator/backward_iterate_slice_new.go

func main() {
    sl := []string{"hello", "world", "golang"}
    for i, s := range Backward(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }
}

相比于上面的老版本代码,这也的代码更简洁清晰了,使用Go 1.22rc1的运行结果也与老版本别无二致:

$GOEXPERIMENT=rangefunc  go run backward_iterate_slice_new.go
2 : golang
1 : world
0 : hello

但代价就是要理解什么样原型的函数才能与for range一起使用实现函数迭代,这的确有些复杂,本文就不展开说了,有兴趣的童鞋可以先看看有关range-over-function iterator的wiki先行了解一下。

2. 编译器、运行时与工具链

2.1 继续增强PGO优化

Go 1.20版本引入PGO(profile-guided optimization)后,PGO这种优化技术带来的优化效果就得到了持续的提升:Go 1.20实测性能提升仅为1.05%;Go 1.21版本发布时,官方的数据是2%~7%,而Go 1.21编译器自身在PGO优化过后编译速度提升约6%。

在Go 1.22中,官方给出的数字则是2%~14%,这14%的提升想必是来自Google内部的某个实际案例。

2.2 inline和devirtualize

在Go 1.22中,Go编译器可以更灵活的运用devirtualize和inline对代码进行优化了。

在面向对象的编程中,虚拟函数是一种在运行时动态确定调用的函数。当调用虚拟函数时,编译器通常会为其生成一段额外的代码,用于在运行时确定要调用的具体函数。这种动态调度的机制使得程序可以根据实际对象类型来执行相应的函数,但也带来了一定的性能开销。通过devirtualize优化技术,编译器会尝试在编译时确定调用的具体函数,而不是在运行时进行动态调度。这样可以避免运行时的开销,并允许编译器进行更多的优化

对应到Go来说,就是在编译阶段将使用接口进行的方法调用转换为通过接口的实际类型的实例直接调用该方法

注:我的《Go语言精进之路》一书中有对通过接口调用方法的原理的详尽说明,欢迎阅读。

关于内联优化,今年Austin Clements发起了inline大修项目,对Go编译器中的内联优化过程进行全面调整,目标是在Go 1.22中拥有更有效的、具有启发能力的内联,为后续内联的进一步增强奠定基础。该大修的成果目前以GOEXPERIMENT=newinliner试验特性的形式在Go 1.22中提供。

2.3 运行时

运行时的变化主要还是来自GC

Go 1.22中,运行时会将基于类型的垃圾回收的元数据放在每个堆对象附近,从而可以将Go程序的CPU性能提高1-3%。同时,通过减少重复的元数据的优化,内存开销也将降低约1%。不确定减少重复元数据(metadata)这一优化是否来自对unique包的讨论

2.4 工具链

在Go工具链改善方面,首当其冲的要数go module相关工具了。

在Go 1.22中,go work增加了一个与go mod一致的特性:支持vendor。通过go work vendor,可以将workspace中的依赖放到vendor目录下,同时在构建时,如果module root下有vendor目录,那么默认的构建是go build -mod=vendor,即基于vendor的构建。

go mod init在Go 1.22中将不再考虑GOPATH时代的包依赖工具的配置文件了,比如Gopkg.lock。在Go 1.22版本之前,如果go module之前使用的是类似dep这样的工具来管理包依赖,go mod init会尝试读取dep配置文件来生成go.mod。

go vet工具取消了对loop变量引用的警告,增加了对空append的行为的警告(比如:slice = append(slice))、增加了deferring time.Since的警告以及在log/slog包的方法调用时key-value pair不匹配的警告。

3. 标准库

最后,我们来看看标准库的变化。每次Go发布新版本,标准库都是占更新的大头儿,这里无法将所有变更点一一讲解,仅说说几个重要的变更点。

3.1 增强http.ServerMux表达能力

Go内置电池,从诞生伊始就内置了强大的http库,不过长期以来http原生的ServeMux表达能力比较单一,不支持通配符等,这也是Go社区长期以来一直使用像gorilla/muxhttprouter等第三方路由库的原因。

今年log/slog的作者Jonathan Amsterdam又创建了新的提案:net/http: enhanced ServeMux routing,提高http.ServeMux的表达能力。在新提案中,新的ServeMux将支持如下路由策略(来自http.ServeMux的官方文档):

  • “/index.html”路由将匹配任何主机和方法的路径”/index.html”;
  • “GET /static/”将匹配路径以”/static/”开头的GET请求;
  • “example.com/”可以与任何指向主机为”example.com”的请求匹配;
  • “example.com/{$}”会匹配主机为”example.com”、路径为”/”的请求,即”example.com/”;
  • “/b/{bucket}/o/{objectname…}”匹配第一段为”b”、第三段为”o”的路径。名称”bucket”表示第二段,”objectname”表示路径的其余部分。

下面就是基于上面的规则编写的示例代码:

// lib/servemux/main.go

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/index.html", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match /index.html`)
    })
    mux.HandleFunc("GET /static/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "GET /static/"`)
    })
    mux.HandleFunc("example.com/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/"`)
    })
    mux.HandleFunc("example.com/{$}", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/{$}"`)
    })
    mux.HandleFunc("/b/{bucket}/o/{objectname...}", func(w http.ResponseWriter, req *http.Request) {
        bucket := req.PathValue("bucket")
        objectname := req.PathValue("objectname")
        fmt.Fprintln(w, `match /b/{bucket}/o/{objectname...}`+":"+"bucket="+bucket+",objectname="+objectname)
    })

    http.ListenAndServe(":8080", mux)
}

我们使用curl对上述示例进行一个测试(前提是在/etc/hosts中设置example.com为127.0.0.1):

$curl localhost:8080/index.html
match /index.html

$curl example.com:8080/static/abc
match "example.com/"

$curl localhost:8080/static/abc
match "GET /static/"

$curl example.com:8080/
match "example.com/{$}"

$curl example.com:8080/b/mybucket/o/myobject/tonybai
match "example.com/"

$curl localhost:8080/b/mybucket/o/myobject/tonybai
match /b/{bucket}/o/{objectname...}:bucket=mybucket,objectname=myobject/tonybai

从测试情况来看,不同路由设置之间存在交集,这就需要路由匹配优先级规则。新版Go ServeMux规定:如果一个请求有两个或两个以上的模式匹配,则更具体(specific)的模式优先。如果P1符合P2请求的严格子集,也就是说,如果P2符合P1及更多的所有请求,那么P1就比P2更具体。

举个例子:”/images/thumbnails/”比”/images/”更具体,因此两者都可以注册。前者匹配以”/images/thumbnails/”开头的路径,后者则匹配”/images/”子树中的任何其他路径。

如果两者都不更具体,那么模式就会发生冲突。为了向后兼容,这一规则有一个例外:如果两个模式发生冲突,而其中一个模式有主机(host),另一个没有,那么有主机的模式优先(比如上面测试中的第二次curl执行)。如果通过ServeMux.Handle或ServeMux.HandleFunc设置的模式与另一个已注册的模式发生冲突,这些函数就会panic。

增强后的ServeMux可能会影响向后兼容性,使用GODEBUG=httpmuxgo121=1可以保留原先的ServeMux行为。

3.2 增加math/rand/v2包

在日常开发中,我们多会在生成随机数的场景下使用math/rand包,其他时候使用的较少。但Go 1.22中新增了math/rand/v2包,我之所以将这个列为Go 1.22版本标准库的一次重要变化,是因为这是标准库第一次为某个包建立v2版本包,按照Russ Cox的说法,这次v2包的创建,为标准库中的其他可能的v2包树立了榜样。创建math/rand/v2可以使Go团队能够在一个相对不常用且风险较低的包中解决工具问题(如gopls、goimports等对v2包的支持),然后再转向更常用、风险更高的包,如sync/v2或encoding/json/v2等。

新增rand/v2包的直接原因是清理math/rand并修复其中许多悬而未决的问题,特别是使用过时的生成器、慢速算法以及与crypto/rand冲突的问题,这里就不针对v2包举具体的示例了,对该包感兴趣的同学可以自行阅读该包的在线文档,并探索如何使用v2包。

同时,该提案也为标准库中的v2包的创建建立了一种模式,即v2包是原始包的子目录,并且以原始包的API为起点,每个偏离点都要有明确的理由。

想当初,go module刚落地到Go中时,Go module支持两种识别major的两种方式,一种是通过branch或tag号来识别,另外一种就是利用vN目录来定义新包。当时还不是很理解为什么要有vN目录这种方式,现在从math/rand/v2包的增加来看,足以体现出当初module设计时的前瞻性考量了。

3.3 大修Go execution tracer

Go Execution Tracer是解决Go应用性能方面“疑难杂症”的杀手锏级工具,它可以提供Go程序在一段时间内发生的情况的即时视图。这些信息对于了解程序随时间推移的行为非常宝贵,可辅助开发人员对应用进行性能改进。我曾在《通过实例理解Go Execution Tracer》中对其做过系统的说明。

不过当前版本的Go Execution Tracer在原理和使用方面还存在诸多问题,Google的Michael Knyszek在年初发起了Execution tracer overhaul的提案,旨在对Go Execution Tracer进行改进,使Go Execution Tracer可扩展到大型Go部署的Go执行跟踪。具体目标如下:

  • 使跟踪解析所需的内存占用量仅为当前的一小部分。
  • 支持可流式传输的跟踪,以便在无需存储的情况下进行分析。
  • 实现部分自描述的跟踪,以减少跟踪消费者的升级负担。
  • 修复长期存在的错误,并提供一条清理实现的路径。

在近一年的时间里,Knyszek与Felix Geisendorfer、Nick Ripley、Michael Pratt等一起实现了该提案的目标。

鉴于篇幅,这里就不对新版Tracer的使用做展开说明,有兴趣的童鞋可结合《通过实例理解Go Execution Tracer》中的使用方法自行体验新版Tracer。

注:新版Tracer的设计文档 – https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md

3.4 其他

Go 1.4版本以来,syscall包新特性就已经被冻结,并在Go 1.11版本中被标记为不推荐使用(deprecate)。Go团队推荐gopher使用golang.org/x/sys/unix或golang.org/x/sys/windows。syscall包的大多数功能都能被golang.org/x/sys包替代,除了下面这几个:

syscall.SysProcAttr(类型os/exec.Cmd.SysProcAttr)
syscall.Signal(参考文献os.Signal)
syscall.WaitStatus(参考文献os.(*ProcessState).Sys)
syscall.Stat_t
... ...

由于syscall包已经弃用,IDE等工具在开发人员使用上述内容时总是得到警告!这引发了众多开发人员的抱怨。为此,在Go 1.22版本中,syscall取消了弃用状态,但其功能特性依旧保持冻结,不再添加新特性。

  • TCPConn to UnixConn:支持zerocopy

gnet作者Andy Pan的提案:TCPConn to UnixConn:支持zerocopy在Go 1.22落地,具体内容可以看一下原始提案issue

  • 新增go/version包

在Go 1.21版本发布后,Go团队对Go语言的版本规则做了调整,并明确了Go语言的向前兼容性和toolchain规则,Go 1.22中增加go/version包实现了按照上述版本规则的Go version判断,这个包既用于go工具链,也可以用于Gopher自行开发的工具中。

4. 小结

Go 1.22版本具有至少两点重要的里程碑意义:

  • 通过对loopvar语义的修正,开启了Go已有“语法坑”的fix之路
  • 通过math/rand/v2包树立了Go标准库建立vN版本的模式

“语法坑”fix是否能得到社区正向反馈还是一个未知数,其导致的兼容性问题势必会成为Go社区在升级到Go 1.22版本的重要考虑因素,即便决定升级到Go 1.22,严格的代码审查和测试也是必不可少的。

最后,感谢Go团队以及所有Go 1.22贡献者做出的伟大工作!

文本涉及的源码可以在这里下载。

5. 参考资料

-Go 1.22 Milestone – https://github.com/golang/go/milestone/298


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Service Weaver:以单体形式编码,以微服务形式部署

本文永久链接 – https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices

分布式应用的主流架构模式演化为微服务架构已经有些年头了。微服务、DevOps、持续交付和容器技术(k8s)是构成最初云原生概念的核心要素。它们相生相拌,共同演进,并推动了云计算全面进入云原生时代

云原生应用普遍采用微服务架构,遗留的单体应用程序会逐步演进并拆分为多个微服务,新应用则会直接采用微服务架构进行设计与实现。微服务的好处是显而易见的:

  • 每个微服务都编译为一个二进制文件并独立部署和扩展,可以提高资源利用率;
  • 一个微服务的崩溃不会影响到其他微服务,限制了错误的传播半径,从而提高了容错能力;
  • 改善了抽象的边界。微服务需要清晰明确的API,降低了代码纠缠不清的可能性;
  • 灵活部署,不同微服务的二进制文件可以以不同频率发布,从而实现更敏捷的代码升级。
  • … …

不过做过微服务的朋友都知道,微服务架构带来的不仅仅是好处,还有很多挑战:

  • 单体应用内的模块间可通过内存直接交互,而在微服务架构的应用中,多个微服务需要进行跨进程跨机器的通信,对数据的序列化和反序列化操作必不可少,其开销很难避免,对应用性能是有较大损耗的;
  • 研究表明,三分之二的故障是由于不同版本的微服务之间的交互引发的,这会损害应用的正确性;
  • 每个微服务开发人员都有自己的发布和管理计划,而无法像单体应用那样使用单个二进制文件来统一构建、测试和部署,这给微服务开发管理带来了很高的复杂性;
  • API管理变得复杂。一旦某个微服务发布了,它的API很难在不影响其他使用该API的服务的情况下进行变更,新老API同时存在是常态;
  • 减慢了应用程序开发的速度。在进行会影响多个微服务的更改时,开发人员无法原子地实现和部署这些更改。他们必须仔细计划如何根据自己的发布时间表在n个微服务中引入变更;
    … …

由此可见,微服务并非“银弹”,人们在消除微服务的缺点方面做了很多工作,不可谓不努力,但收效甚微,甚至出现了回归monolith(大单体)的现象

今年年初Google发布了一个在这方面的探索成果:Service Weaver。Service Weaver不仅仅是一个分布式应用的开发框架,更是一个旨在减少或消除微服务弊端的探索实验的结论。

Service Weaver到底有何与众不同?它的核心抽象是什么?它的最大优点又是什么呢?在这一篇文章中,我就和大家一起来学习和了解一下Service Weaver这个开发框架。

1. Service Weaver简介

Service Weaver是Google开源的一个编程框架(programming framework) ,用于编写、部署和管理用Go开发的分布式应用程序。

注:随着Service Weaver的演进,后续可能会有其他语言的版本。

使用Service Weaver,你可以像编写在本地机器上运行的传统单进程Go可执行文件一样编写应用程序。然后,将其部署到云中,该框架会将其分解为一组微服务,并将其与云提供商(主要是k8s)集成(如监控、跟踪、日志等)。简单来说,就是“以单体形式编码,以微服务形式部署”

开篇提过,Google开源的Service Weaver本就是为解决微服务架构在实践中出现的诸多问题而提出的创新思路与实验,为此它提出并实现了三个核心原则

  • 在构建阶段,开发人员只需编写模块化的单体程序;
  • 在首次部署和运行阶段,Service Weaver会将逻辑组件分配给物理进程,可以是本地的一个进程,也可以是多个进程,当然最主流的还是分配给运行在公有云提供商k8s的不同pod;
  • 以原子方式升级变更应用,彻底杜绝应用的不同版本间的交互。

这么说依然很抽象,闻名不如见面,接下来我们就用一些例子来看一下Service Weaver是如何践行这三个原则的。

我们先来看看用Service Weaver开发的“Hello, World”程序长什么样子。

2. Hello, World

安装Service Weaver很简单,只需执行下面命令:

$go install github.com/ServiceWeaver/weaver/cmd/weaver@latest

$weaver
USAGE

  weaver generate                 // weaver code generator
  weaver version                  // show weaver version
  weaver single    <command> ...  // for single process deployments
  weaver multi     <command> ...  // for multiprocess deployments
  weaver ssh       <command> ...  // for multimachine deployments
  weaver gke       <command> ...  // for GKE deployments
  weaver gke-local <command> ...  // for simulated GKE deployments
  weaver kube      <command> ...  // for vanilla Kubernetes deployments

DESCRIPTION

  Use the "weaver" command to deploy and manage Weaver applications.

  The "weaver generate", "weaver version", "weaver single", "weaver multi", and
  "weaver ssh" subcommands are baked in, but all other subcommands of the form
  "weaver <deployer>" dispatch to a binary called "weaver-<deployer>".
  "weaver gke status", for example, dispatches to "weaver-gke status".

注:Weaver要求Go版本高于1.21。另外在MacOS上安装使用时,官方文档提到要开启export CGO_ENABLED=1; export CC=gcc; 不过CGO_ENABLED=1通常是默认的。另外我使用CC=clang也可以正常安装和使用weaver。

安装完Weaver后,我们就来看一个基于Weaver的Hello, World示例,了解一下基于Weaver框架开发的应用的基本结构。

我们创建一个hello目录,然后在hello下面使用go mod init hello来初始化一个go module。这个例子非常简单,hello目录下只有一个main.go:

// serviceweaver-examples/hello/main.go

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/ServiceWeaver/weaver"
)

func main() {
    if err := weaver.Run(context.Background(), serve); err != nil {
        log.Fatal(err)
    }
}

// app is the main component of the application. weaver.Run creates
// it and passes it to serve.
type app struct {
    weaver.Implements[weaver.Main]
}

// serve is called by weaver.Run and contains the body of the application.
func serve(context.Context, *app) error {
    fmt.Println("Hello, World")
    return nil
}

我们看到:示例导入了weaver包,然后在main函数中调用weaver.Run函数。Run函数的原型如下:

// github.com/ServiceWeaver/weaver/weaver.go
func Run[T any, P PointerToMain[T]](ctx context.Context, app func(context.Context, *T) error) error

weaver充分利用了Go 1.18引入的泛型,Run就是一个泛型函数,它的第二个参数为app,这是一个函数类型的参数。顾名思义,app这个函数封装了整个应用的主运行逻辑。在hello这个示例中,我们为Run的第二个参数传入的是serve。而serve的逻辑非常简单,就是输出“Hello, World”,然后就返回nil了,返回nil表示正常退出。weaver.Run会处理应用的生命周期,比如优雅关闭等,serve函数就只需要关心业务逻辑即可,通过这种方式,通用的服务框架代码和业务代码便分离开来,降低了耦合,提高可维护性。

到这里,很多读者可能注意到了:由于示例过于简单,serve函数并没有使用传入的第二个参数(类型为*app),但在用Weaver开发的实用程序中,Run的第二个参数是整个应用的核心,并且app这个类型恰好就是weaver.Run泛型函数中T的类型实参(type argument)。

Run函数的注释中明确说明:T类型(app)必须是一个struct类型且包含一个weaver.Implements[weaver.Main]的嵌入字段,在该示例中app类型的定义恰是如此:

// serviceweaver-examples/hello/main.go
type app struct {
    weaver.Implements[weaver.Main]
}

说到这里,就不得不提到Service Weaver的核心抽象:组件(component)了!基于Service Weaver框架开发的应用是由一个组件的集合。实际上,Weaver中的组件就是一个普通Go接口的实现,编写代码时,组件间的交互也是通过接口的方法调用完成的。

那么,上面示例中的组件在哪里呢?上面的示例仅包含一个Weaver应用必须的组件:main组件。app类型就理解为一个main组件,它通过嵌入weaver.Implements[weaver.Main]这个类型实现了weaver.Main接口:

// Main is the interface implemented by an application's main component.
type Main interface{}

对于Weaver应用而言,main组件是不可获取的,如果注释掉app结构体类型中weaver.Implements[weaver.Main]这一行,那么无论执行weaver generate命令还是go run命令,你得到的都会是错误:

$weaver generate .
-: # hello
./main.go:12:22: *app does not satisfy "github.com/ServiceWeaver/weaver".PointerToMain[app] (missing method implements)
/Users/tonybai/Test/Go/service-weaver/hello/main.go:12:12: *app does not satisfy "github.com/ServiceWeaver/weaver".PointerToMain[app] (missing method implements)

$go run .
# hello
./weaver_gen.go:34:40: cannot use (*app)(nil) (value of type *app) as "github.com/ServiceWeaver/weaver".InstanceOf["github.com/ServiceWeaver/weaver".Main] value in variable declaration: *app does not implement "github.com/ServiceWeaver/weaver".InstanceOf["github.com/ServiceWeaver/weaver".Main] (missing method implements)
./weaver_gen.go:37:25: cannot use (*app)(nil) (value of type *app) as "github.com/ServiceWeaver/weaver".Unrouted value in variable declaration: *app does not implement "github.com/ServiceWeaver/weaver".Unrouted (missing method routedBy)
./main.go:12:22: *app does not satisfy "github.com/ServiceWeaver/weaver".PointerToMain[app] (missing method implements)

好了,大致了解Weaver应用的结构后,我们来运行一下这个示例:

$go mod tidy
go: finding module for package github.com/ServiceWeaver/weaver
go: found github.com/ServiceWeaver/weaver in github.com/ServiceWeaver/weaver v0.21.2
go: downloading modernc.org/ccgo/v3 v3.16.13
go: downloading modernc.org/cc/v3 v3.40.0
go: downloading lukechampine.com/uint128 v1.2.0
go: downloading modernc.org/token v1.0.1

$weaver generate .
$go run .
╭───────────────────────────────────────────────────╮
│ app        : hello                                │
│ deployment : ca0fcdf2-d9bc-456b-a668-159688e3cca5 │
╰───────────────────────────────────────────────────╯
Hello, World

我们看到,在go run执行之前,我们通过weaver generate命令生成一些代码,这些生成的代码放在了weaver_gen.go中,有100多行,是weaver应用运行所必须的stub代码。

hello, world虽然简单易懂,但对Weaver的核心抽象:逻辑组件(component)的体现并不明显,我们再来看一个复杂一些的例子。

3. 一个http服务器例子

我们来实现一个http服务器的例子,下面是这个例子的组件逻辑拓扑结构:

从图中可以看到,这个实例程序一共有三个weaver component:main组件(listener)、reverser组件(用于将输入的字符串反转)和converter组件(用于将输入的字符串变成大写字符串)。

reverser组件和converter组件都比较简单,每个组件对应的接口仅有一个方法,它们的代码如下:

// serviceweaver-examples/httpserver/reverser.go

package main

import (
    "context"

    "github.com/ServiceWeaver/weaver"
)

// Reverser component.
type Reverser interface {
    Reverse(context.Context, string) (string, error)
}

// Implementation of the Reverser component.
type reverser struct {
    weaver.Implements[Reverser]
}

func (r *reverser) Reverse(_ context.Context, s string) (string, error) {
    runes := []rune(s)
    n := len(runes)
    for i := 0; i < n/2; i++ {
        runes[i], runes[n-i-1] = runes[n-i-1], runes[i]
    }
    return string(runes), nil
}

// serviceweaver-examples/httpserver/converter.go

package main

import (
    "context"
    "strings"

    "github.com/ServiceWeaver/weaver"
)

// Converter component.
type Converter interface {
    ToUpper(context.Context, string) (string, error)
}

// Implementation of the Converter component.
type converter struct {
    weaver.Implements[Converter]
}

func (r *converter) ToUpper(_ context.Context, s string) (string, error) {
    return strings.ToUpper(s), nil
}

接下来,我们实现这个示例的实现weaver.Main接口的app类型:

// serviceweaver-examples/httpserver/main.go

type app struct {
    weaver.Implements[weaver.Main]
    reverser  weaver.Ref[Reverser]
    converter weaver.Ref[Converter]
    lis       weaver.Listener
}

这里app结构体类型通过weaver.Ref嵌入了实现了另外两个组件接口的组件实例,Ref函数的定义如下:

// Ref[T] is a field that can be placed inside a component implementation
// struct. T must be a component type. Service Weaver will automatically
// fill such a field with a handle to the corresponding component.
type Ref[T any] struct {
    value T
}

// Get returns a handle to the component of type T.
func (r Ref[T]) Get() T { return r.value }

此外,通过泛型类型Ref的Get方法,可以获得对相应组件的访问权。

app结构体类型中还包含了一个weaver.Listener类型的实例,Listener理论上并非组件,而是Weaver框架提供了网络服务端口监听的实现,可以放置在任何提供网络服务的组件实现内部,比如本示例的app这个main组件。app将reverser、converter和listener聚合在一起,为后续的serve函数实现提供支持。

接下来,我们看看serve函数的实现:

// serviceweaver-examples/httpserver/main.go

func serve(ctx context.Context, app *app) error {
    // The lis listener will listen on a random port chosen by the operating
    // system. This behavior can be changed in the config file.
    fmt.Printf("http listener available on %v\n", app.lis)

    // Serve the /reverse endpoint.
    http.HandleFunc("/reverse", func(w http.ResponseWriter, r *http.Request) {
        name := r.URL.Query().Get("name")
        if name == "" {
            name = "World"
        }
        reversed, err := app.reverser.Get().Reverse(ctx, name)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "after reversing, name is %s\n", reversed)
    })
    // Serve the /convert endpoint.
    http.HandleFunc("/convert", func(w http.ResponseWriter, r *http.Request) {
        name := r.URL.Query().Get("name")
        if name == "" {
            name = "World"
        }
        converted, err := app.converter.Get().ToUpper(ctx, name)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "after converting, name is %s\n", converted)
    })
    return http.Serve(app.lis, nil)
}

我们看到serve函数定义了两个端点/reverse和/convert的Handler函数,并通过http.Serve启动了一个http服务器,http服务器返回,应用退出,否则http服务将一直运行。

我们来运行一下这个程序:

$cd serviceweaver-examples/httpserver
$go mod tidy
$weaver generate .
$go run .
╭───────────────────────────────────────────────────╮
│ app        : httpserver                           │
│ deployment : 55827837-896f-4060-88c2-f1f1d953d142 │
╰───────────────────────────────────────────────────╯
http listener available on [::]:59493

我们看到,示例中的httpserver启动后在59493这个端口监听客户端的连接,我们用curl工具来测试一下:

$curl "http://localhost:59493/convert?name=abcdefg"
after converting, name is ABCDEFG
$curl  "http://localhost:59493/reverse?name=abcdefg"
after reversing, name is gfedcba

我们看到,无论是reverser组件还是converter组件工作都正常。

由于我们没有指定端口,59493是一个随机端口。如果要指定监听的地址和端口,我们可以借助weaver提供的toml格式的配置文件来实现:

// weaver.toml
[single]
listeners.lis = {address = "localhost:8080"}

基于weaver.toml配置文件启动httpserver的命令如下:

$SERVICEWEAVER_CONFIG=weaver.toml go run .
╭───────────────────────────────────────────────────╮
│ app        : httpserver                           │
│ deployment : ee49694c-4935-4f44-96f3-cc7d1d0167ae │
╰───────────────────────────────────────────────────╯
http listener available on 127.0.0.1:8080

在这种模式下启动的httpserver,所有组件都会在一个单一的进程中,组件间的通信通过方法调用进行。这种单体程序在单个进程中部署运行的方式称为single process部署模式,十分适合开发者对程序的开发与调试。weaver为这种方式提供了专门的子命令single,我们可以通过single命令在单进程启动httpserver,不过我们要修改一下weaver.toml:

// weaver.toml

[single]
listeners.lis = {address = "localhost:8080"}

[serviceweaver]
binary = "./httpserver"

无论是single子命令,还是后面即将讲到的multi,都是基于一个可执行文件进行的,因此我们要将httpserver这个示例编译为一个可执行文件”httpserver”,我已经将编译命令放入Makefile,大家输入make命令执行即可。

有了可执行的二进制文件httpserver后,我们就可以使用single子命令启动单进程版的httpserver了:

$weaver single deploy weaver.toml
╭───────────────────────────────────────────────────╮
│ app        : httpserver                           │
│ deployment : ad7c0341-d5d2-4182-8944-306d7682e708 │
╰───────────────────────────────────────────────────╯
http listener available on 127.0.0.1:8080

在开篇讲Service Weaver的三个核心原则时提到,基于Weaver的应用既可以跑在一个进程中,也可以部署在多个进程,以及云提供商的k8s环境中,下面我们就来看看weaver应用的部署,先来将单进程部署模式改为本地多进程部署模式。

4. 部署

基于Weaver应用的部署方式与编码完全解耦,我们无需修改源码便可以实现多进程部署。唯一要做的就是改改weaver.toml,新增多进程部署模式下应用的监听地址信息:

// weaver.toml
[single]
listeners.lis = {address = "localhost:8080"}

[serviceweaver]
binary = "./httpserver"

[multi]
listeners.lis = {address = "localhost:8080"} // 新增

接下来使用下面命令,我们就可以将httpserver以多进程的形式启动起来:

$weaver multi deploy weaver.toml
╭───────────────────────────────────────────────────╮
│ app        : httpserver                           │
│ deployment : bd689290-4929-47f1-a0f0-774d5e1a9307 │
╰───────────────────────────────────────────────────╯
S1003 18:51:02.042859 stdout               ac04576d                      │ http listener available on 127.0.0.1:8080
S1003 18:51:02.043210 stdout               c03c4eed                      │ http listener available on 127.0.0.1:8080

weaver multi子命令提供了查看httpserver多进程启动后状态的方法:

$weaver multi status
╭──────────────────────────────────────────────────────────╮
│ DEPLOYMENTS                                              │
├────────────┬──────────────────────────────────────┬──────┤
│ APP        │ DEPLOYMENT                           │ AGE  │
├────────────┼──────────────────────────────────────┼──────┤
│ httpserver │ bd689290-4929-47f1-a0f0-774d5e1a9307 │ 1m3s │
╰────────────┴──────────────────────────────────────┴──────╯
╭───────────────────────────────────────────────────────────────╮
│ COMPONENTS                                                    │
├────────────┬────────────┬──────────────────────┬──────────────┤
│ APP        │ DEPLOYMENT │ COMPONENT            │ REPLICA PIDS │
├────────────┼────────────┼──────────────────────┼──────────────┤
│ httpserver │ bd689290   │ weaver.Main          │ 30194, 30195 │
│ httpserver │ bd689290   │ httpserver.Converter │ 30198, 30199 │
│ httpserver │ bd689290   │ httpserver.Reverser  │ 30196, 30197 │
╰────────────┴────────────┴──────────────────────┴──────────────╯
╭─────────────────────────────────────────────────────╮
│ LISTENERS                                           │
├────────────┬────────────┬──────────┬────────────────┤
│ APP        │ DEPLOYMENT │ LISTENER │ ADDRESS        │
├────────────┼────────────┼──────────┼────────────────┤
│ httpserver │ bd689290   │ lis      │ 127.0.0.1:8080 │
╰────────────┴────────────┴──────────┴────────────────╯

在status输出的信息中,我们能看到deployment(部署)信息、组件(components)信息以及listener信息。从组件信息来看,weaver multi子命令将每个component放入了一个单独进程,包括main component,并且每个component的副本数(replica)为2,即一共启动了6个进程。从下面ps命令的输出结果也能印证这点:

$ps -ef|grep httpserver
  501 30194 30193   0  6:51下午 ttys006    0:00.05 /Users/tonybai/test/go/service-weaver/httpserver/httpserver
  501 30195 30193   0  6:51下午 ttys006    0:00.05 /Users/tonybai/test/go/service-weaver/httpserver/httpserver
  501 30196 30193   0  6:51下午 ttys006    0:00.07 /Users/tonybai/test/go/service-weaver/httpserver/httpserver
  501 30197 30193   0  6:51下午 ttys006    0:00.04 /Users/tonybai/test/go/service-weaver/httpserver/httpserver
  501 30198 30193   0  6:51下午 ttys006    0:00.05 /Users/tonybai/test/go/service-weaver/httpserver/httpserver
  501 30199 30193   0  6:51下午 ttys006    0:00.04 /Users/tonybai/test/go/service-weaver/httpserver/httpserver

在multi process这种模式下,应用的各个组件由于不在同一进程内,它们之间的通信由基于方法调用改为了基于RPC调用的方式。

weaver multi还提供了以web形式查看应用运行状态的命令:dashboard

$weaver multi dashboard
Dashboard available at: http://127.0.0.1:62183

weaver multi dashboard命令会自动打开浏览器并展示httpserver的各种运行信息和状态信息:

点击页面上的Deployment超链接,我们将进入到下面的页面中:

除此之外,页面最下方还有一个展示组件拓扑以及组件间traffic的图:

通过上图我们知道,reverse端点和convert端点分别接到过2次和1次请求。

注:web状态页面上的traces由于没有开启trace,会暂无数据。

和weaver multi一样,weaver ssh可以实现多机器部署,weaver kube实现基于k8s的部署,weaver gke实现在Google Kubernetes Engine上的部署,这里的multi、ssh、kube等都可以称为deployer。single、multi、ssh是weaver内置支持的,而其他weaver 则是调用weaver-完成的,比如:weaver gke status将调用weaver-gke status命令。

注:由于手里没有现成的kubernetes环境,weaver kube命令无法展示了。

到这里,我们已经践行了Service Weaver的两大核心原则:开发阶段以单体程序形式编码开发,以及运行时通过不同deployer(multi、ssh、k8s等)来实现部署环境与代码的解耦。到这里,你是否体会到了本文题目“以单体形式编码,以微服务形式部署”的深意了呢!

下面我们再来看看Weaver核心原则的第3条:原子升级。

5. 升级

对于使用go run或weaver multi deployment部署的应用程序来说,避免升级过程中的跨版本通信是轻而易举的事,因为每个部署都是独立运行的。

我本地没有Kubernetes环境,也没有GKE的账号,那么如何验证weaver的原子升级过程呢?好在weaver提供了gke-local,即在本地建立一个模拟gke环境,我们可以使用这种方式来看看通过weaver如何实现app的原子升级。

首先我们要执行下面命令单独安装weaver-gke-local:

$go install github.com/ServiceWeaver/weaver-gke/cmd/weaver-gke-local@latest

在我的机器和网络环境下,这个安装过程略显“漫长”,因为要拉取很多依赖的go module,还包括像k8s、k8s client这样的go module。

安装好weaver-gke-local后,我们基于httpserver建立一个新module:httpserver-upgrade。然后修改其weaver.toml,增加gke和rollout相关配置:

// serviceweaver-examples/httpserver-upgrade/weaver.toml

[single]
listeners.lis = {address = "localhost:8080"}

[serviceweaver]
binary = "./httpserver"
rollout = "5m" # Perform five minutes slow rollout.

[multi]
listeners.lis = {address = "localhost:8080"}

[gke]
regions = ["us-west1"]
listeners.lis = {public_hostname = "hello.com"}

然后,为了区分不同版本,我在main.go中为各个端点的处理handler加上了一些带有版本信息的日志,并重新执行make构建新的可执行文件。

下面我们就在gke-local环境下首次部署httpserver:

$weaver gke-local deploy weaver.toml
Deploying the application... Done
Version "b343b4de-bb84-4bd7-8bc0-09eb0054b07d" of app "httpserver" started successfully.
Note that stopping this binary will not affect the app in any way.
Tailing the logs...
S1004 06:33:14.621470 stdout               ea68b26c                      │ http v1 listener available on http://localhost:8000
S1004 06:33:14.627226 stdout               be97798d                      │ http v1 listener available on http://localhost:8000

我们可以ctrl+c结束weaver gke-local deploy这个命令的执行,但一旦部署成功,即便这个命令退出,已经部署的程序依然会运行。

^CTo continue watching the logs, run the following command:

    weaver gke-local logs --follow 'version == "b343b4de"'

并且按照上述提示,我们可以继续执行下面命令来tail整个应用的输出日志:

$weaver gke-local logs --follow 'version == "b343b4de"'
S1004 06:33:14.621470 stdout               ea68b26c                      │ http v1 listener available on http://localhost:8000
S1004 06:33:14.627226 stdout               be97798d                      │ http v1 listener available on http://localhost:8000

和multi子命令在本地多进程部署一样,在gke-local下部署后,我们也可以使用status查看应用部署信息和状态:

$weaver gke-local status
╭────────────────────────────────────────────────────────────────────╮
│ Deployments                                                        │
├────────────┬──────────────────────────────────────┬───────┬────────┤
│ APP        │ DEPLOYMENT                           │ AGE   │ STATUS │
├────────────┼──────────────────────────────────────┼───────┼────────┤
│ httpserver │ b343b4de-bb84-4bd7-8bc0-09eb0054b07d │ 4m55s │ ACTIVE │
╰────────────┴──────────────────────────────────────┴───────┴────────╯
╭─────────────────────────────────────────────────────────────────────╮
│ COMPONENTS                                                          │
├────────────┬────────────┬──────────┬──────────────────────┬─────────┤
│ APP        │ DEPLOYMENT │ LOCATION │ COMPONENT            │ HEALTHY │
├────────────┼────────────┼──────────┼──────────────────────┼─────────┤
│ httpserver │ b343b4de   │ us-west1 │ weaver.Main          │ 2/2     │
│ httpserver │ b343b4de   │ us-west1 │ httpserver.Converter │ 2/2     │
│ httpserver │ b343b4de   │ us-west1 │ httpserver.Reverser  │ 2/2     │
╰────────────┴────────────┴──────────┴──────────────────────┴─────────╯
╭─────────────────────────────────────────────────────────────────────────────────────────────╮
│ TRAFFIC                                                                                     │
├───────────┬────────────┬────────────┬────────────┬──────────┬────────────┬──────────────────┤
│ HOST      │ VISIBILITY │ APP        │ DEPLOYMENT │ LOCATION │ ADDRESS    │ TRAFFIC FRACTION │
├───────────┼────────────┼────────────┼────────────┼──────────┼────────────┼──────────────────┤
│ hello.com │ public     │ httpserver │ b343b4de   │ us-west1 │ [::]:62559 │ 0.5              │
│ hello.com │ public     │ httpserver │ b343b4de   │ us-west1 │ [::]:62564 │ 0.5              │
╰───────────┴────────────┴────────────┴────────────┴──────────┴────────────┴──────────────────╯
╭────────────────────────────╮
│ ROLLOUT OF httpserver      │
├─────────────────┬──────────┤
│                 │ us-west1 │
├─────────────────┼──────────┤
│ TIME            │ b343b4de │
│ Oct  3 22:37:59 │ 1.00     │
╰─────────────────┴──────────╯

我们看到整个应用被模拟部署到us-west1 region,每个组件有两个副本,用ps命令查看,我们也能看到6个进程:

$ps -ef|grep httpserver
  501 38480 35224   0  6:33上午 ttys006    0:00.13 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver
  501 38481 35224   0  6:33上午 ttys006    0:00.11 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver
  501 38482 35224   0  6:33上午 ttys006    0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver
  501 38483 35224   0  6:33上午 ttys006    0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver
  501 38484 35224   0  6:33上午 ttys006    0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver
  501 38485 35224   0  6:33上午 ttys006    0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver

现在我们可以使用curl命令来验证一下应用的可用性:

$curl  --header 'Host: hello.com' "http://localhost:8000/reverse?name=abcdefg"
after reversing-v1, name is gfedcba
$curl  --header 'Host: hello.com' "http://localhost:8000/reverse?name=abcdefg"
after reversing-v1, name is gfedcba
$curl  --header 'Host: hello.com' "http://localhost:8000/reverse?name=abcdefg"
after reversing-v1, name is gfedcba
$curl  --header 'Host: hello.com' "http://localhost:8000/convert?name=abcdefg"
after converting-v1, name is ABCDEFG
$curl  --header 'Host: hello.com' "http://localhost:8000/convert?name=abcdefg"
after converting-v1, name is ABCDEFG

可以看到,app工作正常!

此外,我们还可以通过dashboard可以以图形化的方式观测app状态(weaver gke-local dashboard),在后续升级过程中,通过dashboard可以清楚地看到整个升级过程:

注:gke-local会在本地建立一个模拟load balancer,并将发到hello.com主机的请求按Traffic Fraction分发给不同副本。

接下来,我们就来开发httpserver的v2版本,将main.go中的version改为v2,然后重新编译httpserver,执行下面命令部署新版httpserver:

$weaver gke-local deploy weaver.toml
Deploying the application... Done
Version "2ee38e73-323f-4b42-b115-ee5bc40a8c09" of app "httpserver" started successfully.
Note that stopping this binary will not affect the app in any way.
Tailing the logs...
S1004 06:50:12.575585 stdout               702058ba                      │ http v2 listener available on http://localhost:8000
S1004 06:50:12.586352 stdout               ef3d7c3f                      │ http v2 listener available on http://localhost:8000

^CTo continue watching the logs, run the following command:

    weaver gke-local logs --follow 'version == "2ee38e73"'

由于我们配置的rollout为5分钟,所以新版httpserver替换掉旧版httpserver的过程会持续5分钟。而这个过程中load balancer针对新旧两个版本的Traffic Fraction也会动态调整:旧版本会逐渐降低,新版本会逐渐升高:



这时向app发送的请求,既可能由v1版本处理,也可能由v2版本处理:

$curl  --header 'Host: hello.com' "http://localhost:8000/convert?name=abcdefg"
after converting-v1, name is ABCDEFG
$curl  --header 'Host: hello.com' "http://localhost:8000/reverse?name=abcdefg"
after reversing-v1, name is gfedcba
$curl  --header 'Host: hello.com' "http://localhost:8000/convert?name=abcdefg"
after converting-v1, name is ABCDEFG
$curl  --header 'Host: hello.com' "http://localhost:8000/reverse?name=abcdefg"
after reversing-v2, name is gfedcba

最后新版app将全面接手对请求的处理:

之后,旧版的app将被delete掉:

这样新版app的升级部署(rollout)就结束了!rollout后,所有请求将被v2版本处理,应答中将带有v2字样。

在新版本升级的过程中,你如果使用ps查看httpserver进程数量,你会发现数量多出一倍,那是因为整个rollout过程采用的是蓝绿部署方式,即完全部署一套新app,然后通过调整load balancer的分发比例,让新版app逐渐承担全部流量,而在这个过程中,不会存在新老版本组件交互的情况出现。下图展示了这一过程:

注:如果要杀掉app,可以用weaver gke-local kill httpserver命令。

6. 小结

Service Weaver是一个优秀的框架,可以帮助开发人员以单体形式快速构建、以微服务形式快速部署分布式应用,其三个核心原则的创新思路值得我们学习借鉴。

但Service Weaver也不是万能的,Service Weaver主要针对在线的分布式服务系统,即需要在用户请求到达时处理它们的系统,例如网络应用程序或API Server正是此类分布式服务系统。基于Weaver开发这类系统,应用可以轻松获取网络Listener并建立HTTP 服务器,应用可以支持原子升级,且应用组件的副本数量可以根据请求压力的大小自动扩缩(本文并未演示这个特性)。

不过要注意的是:Service Weaver仅仅开源了几个月,其API尚未Stable,本文中的示例基于v0.21.2版本实现,也许在未来的某个时间点,这些示例可能会因API的变化而无法Run起来, status命令和dashboard命令所展现给用户的样式也会发生变化。另外学习weaver本身也是有学习成本的,weaver自身的代码由于采用了泛型和反射,读起来也是很晦涩。

综上,Service Weaver所践行的理念的优秀的,但考虑其成熟度以及Go社区崇尚的“The Best Go framework is no framework”的信条,选择引入Service Weaver框架之前务必要仔细斟酌。

本文涉及的Go源码,可以在这里下载。

7. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

微博(暂不可用):https://weibo.com/bigwhite20xx
微博2:https://weibo.com/u/6484441286
博客:tonybai.com
github: https://github.com/bigwhite
Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats