Junedayday Blog

六月天天的个人博客

0%

2022-02

2022-02-07 CNCF-CloudEvents

今天,我们一起来看CloudEvents,并不是一款成熟的软件系统,而更像是一种协议与标准。不过,它提出的相关概念,对我们开发与设计软件系统时,很有参考意义。

官方定义

CloudEvents is a vendor-neutral specification for defining the format of event data.

顾名思义,CloudEvents项目旨在定义 云时代的事件。事件是一个很广的定义,在不同的软件系统里有不同的表现形式。

想要将所有软件系统里的事件进行标准化,这里面的工作量与难度可想而知,在很长一段时间内很难落地。在我看来,这个项目的意义是长期的 - 先提供一套切实可行的标准与SDK,再尝试结合云原生生态的在核心项目中落地,最后再大规模推广

核心概念

事件里涉及到了很多概念,我选择其中的核心概念,并将其分成了两类。

完整的内容可以参考: https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#notations-and-terminology

数据类:

  1. Occurrence - 发生(客观事实)
  2. Data - 数据
  3. Context - 上下文
  4. Event - 事件

传输类:

  1. Producer - 生产者
  2. Intermediary - 中介
  3. Consumer - 消费者
  4. Event Format - 事件格式
  5. Message - 消息
  6. Protocol - 协议

关键字段

CloudEvents给出了规范的同时,也给出了多语言的SDK。我们可以参考它的命名方式,引入到自己的开发系统中。

必填字段:

  1. id - string
  2. source - URI-reference
  3. specversion - string
  4. type - string

保证 id+source 全局唯一

可选字段:

  1. datacontenttype - string
  2. dataschema - string
  3. subject -string
  4. time - timestamp,推荐RFC-3339

小结

CloudEvents 目前仍处于非常早期的阶段,有兴趣的朋友,可以尝试引入其SDK,将内部的RPC、MQ等通信数据统一起来。

从长期来看,将一个系统中的事件格式统一起来,对整个系统的帮助是很大的。比如说,我们完全可以将服务注册、服务发现等功能认为是一种事件,要求EtcdZookeeperConsul等均支持该方式,就能有利于相关功能的标准化。

2022-02-08 CNCF-NATS

作为CNCF中消息系统的核心项目,NATS受到了各大公司的青睐,近年来使用量也在逐步提升。有不少同学对消息系统的认识还比较模糊,今天我们就借NATS的核心模型,对消息系统有进一步的认识。

官网 - https://nats.io/

Github - https://github.com/nats-io/nats-server

三种消息传递模型

发布-订阅模式:类似于广播模式

publish-subscribe

请求-响应模式:对应关系可自行调整,请求者必须等待到响应才认为是成功

request-reply

队列-订阅模式:分布式系统中非常重要的消息队列功能,实现消息分发

queue-groups

分布式系统中的消息系统

了解上面三种消息传递模型后,你可能仍不清楚它们的适用场景。我建议大家深入地了解这三种模型的本质,这样更方便记忆与理解。其实,在分布式系统中,最核心的是 队列-订阅 模式,其余两种模式意义并不大。

  1. 发布订阅 只是 队列订阅 的一种特殊的广播模式;
  2. 请求响应 更多地应结合服务发现能力,在RPC框架中进行实现;

第一点的使用场景不多见,举个例子:

服务2有多个实例,本地内存里保存了一些信息;现在服务1要更新所有服务2中内存的信息,就需要采用发布-订阅模式,否则会导致服务间数据不一致。

如果服务2引入了分布式缓存,那就是队列-订阅模式。

那么,队列-订阅模式 对分布式系统来说有什么意义呢?这其实就是消息队列的价值,我这里列举最关键的两点:

  1. 削峰填谷:针对分布式系统中的性能问题,通过队列的形式,将高峰期的msg积压到Queue中,在低峰期时交给消费者处理。
  2. 解耦强依赖:从调用关系可以看到,其实Publisher是要将信息传递给Subscriber;但增加了Queue后,Publisher只与Queue交互,Subscriber也只与Queue交互。可以想象,即便Subscriber短暂地挂了,重启后依旧可以正常使用。

分布式的消息队列还有很多注意点,这里我就不一一列举了,更多的资料大家可以自行搜索。

小结

虽然从生产环境的应用范围来看,NATS仍与老牌的重量级消息队列Kafka有相当大的差距,尤其是在大数据的系统中。但对比RocketMQ、RabbitMQ等轻量级产品,NATS的优势已经越来越明显,尤其是在性能与多语言的SDK上,建议有条件的朋友可以尝试使用。

2022-02-09 CNCF-Helm

Helm在整个云原生平台中扮演了重要角色。值得注意的是,Helm自身的复杂度并不高,它更多依赖的是优秀的设计理念与当前包含大量软件的生态。

官方的定义很简洁,即Kubernetes中的包管理者,即一个公共的软件仓。

The Kubernetes Package Manager

使用Helm

类似于Dockerhub,Helm的一大特色就是使用起来非常简单,可快速地在Kubernetes环境中安装软件。

以Kubernetes中的证书管理为例,我们可以参考链接 - https://artifacthub.io/packages/helm/cert-manager/cert-manager ,可以快速地通过几个命令就能完成下载与部署。

我希望大家能注意到:低门槛是吸引用户的重要因素,但真正决定软件长期走向的,是它自身的核心功能。所以,Helm中的软件有三点需要特别关注:

  1. 契合Kubernetes平台:许多软件原生并不支持Kubernetes,需要做一定的改造;
  2. 保证常规功能:如安装时要判断依赖项、卸载时清理哪些数据、升降版本兼容性等等,都是很琐碎、又是很重要的事情;
  3. 人工维护问题:软件是高频迭代的,尤其是在云原生环境下,核心项目往往要大量的人力投入到 FAQ、配置参数说明、兼容性问题的处理;

这三点给Helm带来的是一种滚雪球效应,即越滚越大、越难以被替代;而这种雪球最终能支撑多大的市场,非常依赖Helm内部的核心设计,尤其是扩展性部分。

Charts

Helm称自己是Kubernetes平台中的包管理器,而这个包的格式被称为Charts,我们一起来看看一个官方示例:

1
2
3
4
5
6
7
8
9
10
11
wordpress/
Chart.yaml # A YAML file containing information about the chart
LICENSE # OPTIONAL: A plain text file containing the license for the chart
README.md # OPTIONAL: A human-readable README file
values.yaml # The default configuration values for this chart
values.schema.json # OPTIONAL: A JSON Schema for imposing a structure on the values.yaml file
charts/ # A directory containing any charts upon which this chart depends.
crds/ # Custom Resource Definitions
templates/ # A directory of templates that, when combined with values,
# will generate valid Kubernetes manifest files.
templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes

关键在于三个目录:

  1. charts - 保存当前chart的依赖子chart
  2. crds - 这是chart依赖Kubernetes实现软件运行的关键(CRD是k8s可扩展性的一大特色)
  3. templates - 用来绑定chart自定义参数

换一个视角,这三个文件夹体现了三种能力:

  1. charts - package能力复用
  2. crds - 自定义对接Paas平台(k8s)
  3. templates - 定制化参数

小结

Helm是Kubernetes使用人员需要非常重视的一个产品,它能快速地帮助我们安装与部署软件。

不过,我不建议大家去阅读它的相关源码,它的代码并不优秀;相反地,我更建议大家可以去尝试自己做一个chart(最好能结合自己开发的程序+依赖的中间件,如go程序+redis),这样既能结合Helm实现应用程序的快速部署,又能去实践Kubernetes的CRD。

2022-02-10 CNCF-Buildpacks

Buildpacks是一款对标Docker的镜像打包工具,虽然在CNCF中作为核心项目,但在目前的主流开发场景中用到的并不多。

我们不妨来思考一下Buildpacks与竞品的核心优势:

Buildpacks官网介绍自身的核心特性为3个:ControlComplianceMaintainability。我们今天挑选两个关键性的特征来聊一聊。

Control - Balanced control between App Devs and Operators.

平衡开发者与运维人员。这个也是Buildpacks对标Docker的最大优势。

刚熟悉Dockerfile的同学,会觉得体验很棒,只需要少数几行就能快速制作出一个镜像;但是,如果你是重度使用的用户,就会有不一样的体验:

  1. 多应用的Dockerfile中有大量重复、但又有少量定制化的内容(如依赖的软件)
  2. 由于定制化的内容存在,往往需要开发工程师编写Dockerfile

所以,维护Dockerfile成为了开发工程师很琐碎的工作,而Buildpacks则是希望将部分工作转移给运维人员。但在我看来,这个收益并不明显:

  1. 现状:大部分的公司会封装一些基础镜像,在基础镜像上的Dockerfile所需要的命令已经很少了,整体的复杂度不会很高;
  2. 工作平衡:平衡的意义并没有减少整体的工作量,两种角色的人数总量仍不会有大的变化;
  3. 责任明确:目前大型公司的运维人员越来越少,更强调的是开发人员自己管理应用的整个生命周期;

但换一个角度,Buildpacks理念是可以降低开发人员对Dockerfile这块的门槛,更专注于业务代码的开发。但是,编写Dockerfile这项技能本身难度不高,而且有利于研发自行排查问题,我个人是非常建议开发人员去学习的。

Maintainability - Perform upgrades with minimal effort and intervention.

这一点是Buildpacks的一大特色。

如果你对Docker的镜像底层有一定的了解,会清楚一个镜像就是一层层layer的堆叠;从最上层来看,就是一个完整的操作系统。但如果只对某个layer进行更改,就得销毁老容器、再起一个新的。而Buildpacks则提供了rebase的能力,也就是在运行中的容器中做到快速替换某个layer,而不需要整个重建。

举一个例子,当前运行的容器有层layer是设置环境变量(参考Dockerfile中的ENV指令),我们要进行增加或者更改参数,就能快速实现rebase。当然,rebase肯定是有不少限制条件的,尤其是rebase中的内容不能影响到程序的运行。

我们不妨发散地思考这个特性的价值:由于它核心解决了无需重启整个容器的作用,所以对启动成本比较大的程序,它的意义是很大的,尤其是Java程序。

小结

Buildpacks在社区中活跃度并不高,这也间接证明了Docker的统治地位,而它则需要一个合适的契机才可能得到大幅度的应用。这也提醒了我们,不要一味地追求新的技术,更应该结合现状理性分析。

2022-02-11 CNCF-Operator Framework

Operator Framework是为了降低Kubernetes中Operator开发门槛,而由CNCF社区提供的一套框架。由于这一整套的解决方案门槛很高,需要使用者对Kubernetes的原理有相当的基础,所以今天我们不会深入其细节,而是通过借由这个项目更好地理解Kubernetes。

Controller的工作原理

Operator本质上,是一种定制化的Controller;而控制器的核心思想,是根据期望状态与当前状态,管理k8s中的资源。我们这边可以结合下面这张图,来了解Controller的工作原理。

k8s-controller

  1. client-go是k8s提供的代码生成工具,相关的代码会自动生成;而controller-specific是自行开发的内容;
  2. 期望状态与当前状态的对比逻辑,决策的结果是 新增、更新、删除对应的资源,触发对应的callbacks;
  3. 具体的执行工作,交给Worker执行,而结果如果未达到预期,依然会再次触发整个流程;

关于k8s中的controller,源码分析可以参考我之前的一篇博客 https://junedayday.github.io/2021/02/18/k8s/k8s-012/

三大组件

  1. Operator SDK - 快速生成Operator相关代码
  2. Operator Lifecycle Manager - k8s中的生命周期管理
  3. Operator Metering - 监控

其中监控部分很重要,能帮助使用人员在复杂的K8s系统中排查问题。

小结

目前Operator Framework虽然在社区比较受欢迎,但使用者往往仅限于k8s的深度用户;而许多大型公司又往往会自行封装k8s,不能完美兼容Operator Framework,导致它的推广很受限。

我个人有三点建议:

  1. 优先去Helm里搜索成熟应用,不要自行开发Operator;
  2. 如果有切实的使用需求,优先去公开库 - https://operatorhub.io/ 搜索;
  3. k8s深度玩家可忽略以上两点~

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码

go-tip

go-zero概况

go-zero是当前处于CNCF孵化中的一个Goz语言框架项目,在Github上的star数目前达到14.3K。

作为一款起源于国内的项目,go-zero的中文资料比较齐全,对国内开发者相对友好。但前景如何,还需要进一步的观察。今天我们一起来了解这个项目。

阅读全文 »

2022-01

2022-01-24 CNCF-Linkerd

今天我们来看 Orchestration & Management 编排和管理 层最后一个核心项目 - Linkerd。从严格意义上来说,我们应称它为Linkerd2,区别于原来的1.0版本。

Linkerd是Service Mesh的第一个产品,但在Google的Istio入场后在功能与性能上完全超越。这一段的历史很有意思,大家可以自行搜索了解。

关于Service Mesh,我们已经聊过两款CNCF中的软件了 - Envoy/Contour,这个Linkerd是两者的结合。我们来看一下它的架构示意图,整体来说分为三块:

Linkerd2
  1. CLI - 客户端,对Linkerd2进行管理
  2. Control Plane 控制平面
    1. destination 获取各类信息,如服务发现、网络策略、性能和监控指标
    2. indentity 主要是TLS安全相关
    3. proxy-injector 是一种Kubernetes的Admission Controller,用于对初始化pod注入linkerd相关的信息
  3. Data Plane 数据平面
    1. linkerd-proxy 核心的功能实现,包括代理、路由、TLS、限流等等
    2. linkerd-init 是一种Kubernetes的Init Containter,用iptables的特性将Pod的流量都导向linkerd-proxy

Linkerd的架构非常清晰明了,与Kubernetes的特性紧密结合。我们也不难看到,它的核心能力非常依赖linkerd-proxy这个组件。linkerd-proxy采用了Rust语言编写,而对应的Envoy使用的是C++,从性能来看两者相差无几,更多的是语言生态上的选择不同。

我们再一起读一段Linkerd官方对Service Mesh的定义

A service mesh like Linkerd is a tools for adding observability, security, and reliability features to “cloud native” applications by transparently inserting this functionality at the platform layer rather than the application layer.

  • observability - 可观察性:logging、metrics、tracing

  • security - 安全性:TLS等特性

  • reliability - 可靠性:体现在对网络层的统一管理

从目前来看,Linkerd仍处于一个比较早期的阶段,对标Istio还有大量的功能缺失,我在短期内不太看好。不过它引入了Rust语言有可能吸引一批优秀的人才,成为突破口。

2022-01-25 Go1.18的两个教程

在1月初,我们已经一起看了Go官方对1.18的新特性讲解,想回顾的朋友可以点击这个链接:Go1.18概览。前几天,官方又发布了对泛型和Fuzzing的两个教程,我们再一起浏览下,查漏补缺。

Generics

1
2
3
4
5
6
7
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
  • comparable 是个关键词,指的是支持操作符==!=
  • int64 | float64 则用简洁的语法表示了两种支持的类型

但第二点,如果支持的类型太多,就需要做一次抽象,如

1
2
3
4
5
6
7
type Number interface {
int64 | float64
}

func SumNumbers[K comparable, V Number](m map[K]V) V {
//
}

Go语言的泛型表示方法非常简单,其支持的能力也很有限。相对于C++与JAVA中的泛型,无疑逊色了很多。我们可以简单地归纳Go泛型的使用场景:用于 基础类型 的通用操作,如int/int32/int64/float64等这种重复性很高的基本运算。

作为一种标准,Go的泛型落地非常坎坷,短期内官方也不太可能在这块扩增新的特性,所以Go的泛型适用性会比较窄。

随着1.18的完全落地,我们可以在很多基础库中看到泛型的实践,到时候我们再可以根据具体case进行了解。

Fuzzing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}

相对于传统的单元测试,Fuzzing Test更强调一种 不确定性的输入 理念 - 由于输入的数据是随机的,输出往往是不确定的,那我们最好可以通过一定的操作,减少甚至消除输出的不确定性,才能保证测试的完备性:

比如说,示例中对字符串的反转,转变成了两个测试点:

  1. Reversing a string twice preserves the original value 即两次反转后成为原字符串,
  2. The reversed string preserves its state as valid UTF-8 字符串依然为UTF-8编码格式

从输入和输出来看,如果每个输入都对应枚举出一个输出,那就是单元测试;而Fuzzing Test的理念是尽可能地将输出做到可控,更方便地写各种测试。

在实际工程中,能用到Fuzzing特性的地方很少,更多的还是依赖简单地单元测试保障我们的代码质量。

2022-01-26 如何避免分布式事务

最近,有朋友和我交流分布式事务的实践心得,而我的建议是:尽量避免分布式事务

这里的避免并非完全的不要使用,毕竟像金融场景中,这还是一个必要的特性。但对于绝大多数系统,分布式事务带来的复杂度是非常高的,也需要很高的维护成本与理解成本,远超其收益,我不太建议大家刻意地使用这个技术。

举一个简单的case - 用户下了一个订单,经过如下步骤:

  1. 订单服务生成订单
  2. 库存服务扣去库存
  3. 付费服务完成扣款
  4. 用户积分服务增加积分

这时,最直观的解法是要有一套成熟的分布式事务的方案。但事实上,我更推荐在工程上采用下面两种解决方案,而其中的关键词就是 - 补偿

在MQ中重试

我们经常会利用MQ来解耦服务,那么自然会用它来驱动大量的消息。

例如,我们将扣款请求放到MQ里,扣款服务处理成功后通过另一个MQ通知成功。而当扣款服务出现问题时、也就是扣款失败,常见的有2种选择:

  1. 如果要求是必须成功的,消费时就不要返回成功,在服务中反复重试,即便MQ积压产生告警、再人工恢复;
  2. 如果允许失败,那就设置一个最大重试次数,超过最大重试次数则通知给对应的补偿服务;

利用trace-id+ELK

trace-id是分布式链路追踪的关键信息,用于串联信息;而ELK又通过日志收集系统,将这块收集到了一个系统。

我们可以在生成订单时,同时记录这个关键性的trace-id,然后调用各个服务。有任何一个服务失败,我们就将订单状态修改为失败或超时;而数据不一致的问题,就由对应的补偿服务,根据这些有问题的订单的trace-id去分析。

其实可以从这个方案延伸出类似的,比如直接将错误通过trace-id+信息发送给补偿服务,统一收集。

注意点

  1. 补偿不代表只能手动,我们可以在补偿服务内根据错误码,实现一定的自动化;
  2. 补偿更多体现的是一种最终一致性的思想,会有延时,我们要保证中间状态的数据不会污染系统;

在微服务+云原生时代,我们非常提倡 面向错误编程,正是为了能更好地面对各种不确定的异常case。分布式事务带来了大量的复杂度,目前也没有一套跨语言、跨组件的通用解决方案,目前主流几个方案对应用的侵入性很强,所以我不太建议大部分朋友在生产环境使用,而花更多时间学习相关理论、应付面试就行了。

2022-01-27 CNCF-TiKV

了解完核心的 调度与管理 相关的软件后,我们接下来开始接触 应用定义与开发 的相关软件,这部分与我们实际开发接触最为紧密,也更容易理解。

官方的定义为:

TiKV provides both raw and ACID-compliant transactional key-value API, which is widely used in online serving services, such as the metadata storage system for object storage service, the storage system for recommendation systems, the online feature store, etc.

也就是TiKV支持 简单的与满足ACID事务性的KV存储,被应用在各种存储系统上,如关系型数据库、非关系型数据库、分布式文件系统,最具有代表性的即同属一个公司的TiDB。按官方的定义,我们可以将它对标Redis。

我们结合TiKV的核心特性来看看。

Low and stable latency

RawKV’s average response time less than 1 ms (P99=10 ms).

延迟是IO相关的软件很重要的特性。但对于这个特性,我们要注意两点:

  1. 只针对简单KV,而不针对事务
  2. 真实延迟很依赖存储介质

从这点来看,在TiKV层面引入事务的特性前,需要我们要斟酌一下它对延迟的影响。

High scalabilit

With the Placement Driver and carefully designed Raft groups, TiKV excels in horizontal scalability and can easily scale to 100+ terabytes of data. Scale-out your TiKV cluster to fit the data size growth without any impact on the application.

强调了高扩展性,可支持100TB+的数据。

TiKV采用了Raft作为分布式一致性的协议,这一点与Etcd一致。关于Raft这块是目前工程化的主流,相对于Paxos更容易落地。不过,各家在实现Raft时都或多或少有一些变种,这块我们暂时不细聊。

Consistent distributed transactions

Similar to Google’s Spanner, TiKV (TxnKV mode) supports externally consistent distributed transactions.

支持一致性的分布式事务。

分布式事务对强一致性的业务非常有价值,但它的实现必然会带来一定的性能问题,尤其体现在延迟上。以金融服务为例,分布式事务能保证资金的一致性,不产生资损;但延迟问题又会带来一些异常case,所以需要做好权衡。

Adjustable consistency

In RawKV and TxnKV modes, you can customize the balance between consistency and performance.

对简单KV模式与事务性的KV模式,提供了可调节的一致性功能。

这就是一致性与性能上的权衡。关于这点,大家可以了解一下ACID与BASE对业务的价值。从我的观察来看,目前越来越多的服务倾向于最终一致性,主要有以下优点:

  1. 对外部服务来说视角清晰,更容易理解 - 从外部服务视角来看,本服务最终会趋于一致,而不需要关心各种异常的中间状态,这非常有助于微服务的边界划分;
  2. 服务更具健壮性 - 软件系统的不稳定因素很多,最终一致性可以更好地处理这些异常。

当然,对应的代价是该服务需要引入重试、幂等、异步校验、状态机、恢复日志等特性,自身的复杂度是比较高的。这些技术我也会在后面和大家分享。

2022-01-28 CNCF-Vitess

今天我们来聊聊一款和关系型数据库相关的产品 - VitessVitess的定位很简洁:

A database clustering system for horizontal scaling of MySQL

我们直接从架构图入手,来了解它是怎么实现 MySQL横向扩展 的。

我们关注最核心的两个模块:

VTTablet

A tablet is a combination of a mysqld process and a corresponding vttablet process, usually running on the same machine. Each tablet is assigned a tablet type, which specifies what role it currently performs.

一个Tablet对应到一个具体的MySQL实例,类似于sidecar模式。我之前基于VTTablet做过一定的二次开发,和大家分享一下我对这块的认识:

VTTablet最核心实现,是 模拟一个MySQL,与真正的MySQL进行连接。所以,可以体现如下的特点:

  1. 无侵入式 - 充分利用了MySQL集群间通信的协议,不会侵入原MySQL。这点能衍生出很多价值,例如兼容多版本的MySQL。
  2. 性能较优 - 通过MySQL内部通信的协议交互。
  3. 可扩展性强 - 从原先对大MySQL集群的维护,转变成了相对轻量级的VTTablet集群的维护

VTGate

VTGate is a lightweight proxy server that routes traffic to the correct VTTablet servers and returns consolidated results back to the client. It speaks both the MySQL Protocol and the Vitess gRPC protocol. Thus, your applications can connect to VTGate as if it is a MySQL Server.

VTGate是网关层的角色,主要分三块功能:

  1. 对外暴露出原生的MySQL协议与gRPC协议;
  2. 对内维护与VTTablet集群的连接;
  3. 核心依赖Topology服务中的数据,主要是VTTablet的状态数据和Admin的配置数据

小结

Vitess是用Go语言编写的软件,我比较推荐对数据库原理感兴趣的朋友去阅读VTTablet相关的源码,从中你可以了解到很多MySQL的关键性功能,会比直接阅读MySQL的C++简单很多。比如我曾经做过的:

  1. SQL解析 - 用于自研的查询平台
  2. Binlog同步 - 用于MySQL到异构数据库的同步平台

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码

2022-01

2022-01-17 CNCF-gRPC

今天,我们一起来看CNCF的 Remote Procedure Call - RPC 模块中最具代表性的项目 - gRPC。

gRPC官网的定义很简洁,重点强调了其高性能的特点:

A high performance, open source universal RPC framework.

我们再看一段官方更详细的描述:

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.

除了高性能,这里还提到了两个重要特性:

  • 插件化的支持:负载均衡、tracing、健康监测、认证等
  • 最后一公里:将手机app、浏览器与后端服务器的联通

第一点体现出了gRPC的高度可扩展性。这里的插件化不仅体现在上面列举的这些具体功能,甚至像序列化的方案、底层通信的协议,都可以做到快速替换与迭代。

第二点在实际生产上应用的case并不多,仍需要大量的实践与经验沉淀后,才建议大家尝试。目前,绝大多数的gRPC通信都是发生在后端服务之间。

接下来,我们来谈谈官网列举的四个特性:

Simple service definition

gRPC采用Protocol Buffers进行定义,从整体来说阅读体验确实是比较简洁的,但在实际工程中仍存在两个问题:

  1. 迁移成本大:gRPC没有提供从很多历史RPC解决方案中迁移的途径,如Thrift等,完全是另起炉灶,这对有历史包袱的团队来说很难接受;
  2. 配套开发工具缺失:高频使用Protocol Buffers的朋友都有了解,主流IDE对pb文件的错误提示和文件格式化都有缺失,很多问题都无法在coding的过程中实时提示,而是需要运行二进制命令protoc后才能了解。

Start quickly and scale

快速启动 - 强调的是通过Protocol Buffers生成的代码,可以在各语言内用简洁的代码就可运行(这部分与各语言强相关)

快速扩容 - 这个特性需要与Kubernetes的能力结合。

Works across languages and platforms

跨语言与平台的特性,非常依赖背后的生态 - 需要大量的工作去兼容多语言,也要兼顾性能、稳定性等问题。

所以,这不仅仅是技术上的问题,更需要社区、资金等复杂因素的支持。

Bi-directional streaming and integrated auth

  • 双向流式通信 - 这点是gRPC的核心特性,体现出了与其它RPC方案的差异
  • 集成认证功能 - Auth是gRPC插件化生态中的重要一环,这部分其实与服务网格的功能存在一定的重叠

2022-01-18 CNCF-Envoy

作为云原生Service Mesh代表性的产品之一,Envoy占据了大壁江山。

Envoy is an L7 proxy and communication bus designed for large modern service oriented architectures.

  1. 定位:L7 代理和通信总线
  2. 重点服务对象:SOA面向服务架构(也完全适用于微服务架构)

核心理念:

The network should be transparent to applications. When network and application problems do occur it should be easy to determine the source of the problem.

网络应对应用程序透明。当网络和应用程序出现问题时,应该很容易地确定问题根源。

文档资料:

我们依旧挑选三个核心特性展开:

Out of process architecture 进程外架构

进程外架构,其实就是一种微服务理念的体现,它的利与弊也是和微服务强相关的。

优点很直观 - Envoy可以支持多语言,也可以独立于业务快速迭代与发展。

而弊端在官网上没有体现,最关键的一点是:性能损耗。作为一种side-car模式,Envoy是在本机内、进程间进行通信,远远地比进程内调用的压栈、出栈更花时间。

Service discovery and dynamic configuration 服务发现和动态配置

很多运维同学都会拿Envoy去对比Nginx,确实在很多特性上,两者有很大的相似之处。但在这个特性上,Envoy有得天独厚的优势。

以一个常见的动态负载均衡为例,nginx需要结合consul template来实现,还需要对应用程序做一定的侵入。而Envoy可以与Kubernetes完美契合,大幅度减少维护成本。

Best in class observability 最佳的可观察性

Envoy 的最主要目标是使网络透明,而最佳的可观测性往往会从OpenTelemetry推荐的三点入手:Metrics、Logging、Tracing。

Envoy提供了大量的插件,一方面保证了Envoy内部的问题清晰明了,同时也能快速地协助定位到应用侧的问题。

2022-01-19 CNCF-Contour

作为CNCF中的控制平面的主打产品,Contour的知名度远不如Google的Istio。但由于种种原因,Istio并没有被捐献给云原生基金会,于是CNCF就孵化了Contour。

定义

Contour is an open source Kubernetes ingress controller providing the control plane for the Envoy edge and service proxy. Contour supports dynamic configuration updates and multi-team ingress delegation out of the box while maintaining a lightweight profile.

三大特性

  • Envoy Inside
  • Flexible Architecture
  • TLS Certificate Delegation

从定义与三个特性的描述里,我们很难真实地感受到Contour的特点。

今天我将换一个角度:从控制平面与数据平面进行分析。如果我们能清楚地认识到这两者的定义与边界,那么会对云原生的Service Mesh理念,以及Envoy、Contour、Istio等产品有更深的理解。

这篇博客的定义很具有参考意义:

  • 控制平面 - 通过配置和控制消息来组织编排网络的逻辑,并下发给数据平面
  • 数据平面 - 以 Sidecar 的形式与应用部署在一起,承载其流量的发送与接收

控制平面的关键功能就是控制与管理,重点对象是配置信息

我们可以将数据平面看作为基础的Nginx服务,它是和底层的应用强相关的,解析相关的数据包并实现路由、负载均衡等功能。但Nginx自身也需要有一定的配置信息,比如说:

  • 数据平面的服务发现中心往往是因服务而异的;
  • 数据平面的超时参数会频繁调整;
  • 在做蓝绿测试时,需要频繁调整流量;
  • 认证相关的设置往往需要统一更新,如证书;

在我看来,控制平面更像是数据平面的配置中心,解决的是高频变化或应用间差异性很大的配置信息,让数据平面更稳定地运行。

从架构层面来说,如果一层解决不了问题,那就再抽象一层。

从具体的工程实践来说,数据平面主要负责的是底层实现,在保证稳定性的情况下更要兼具性能,所以不宜有太多丰富的功能。这时,引入了控制平面,我们可以将很多的配置信息通过界面UI方式进行管理,下发到各个控制平面,然后控制平面再针对性地进行转发到数据平面,就能大幅度地提高整个Service Mesh方案的可维护性。

2022-01-20 CNCF-Emissary-Ingress

Emissary-Ingress是CNCF中又一个与网络相关的项目。这款软件的前身叫作Ambassador。但Ambassador是一款商业软件,为了避免相关的问题,更名为Emissary-Ingress后被捐献给了CNCF。因为网上资料更多的都是Ambassador,下面统一用Ambassador对这款软件进行描述。

CNCF上的定义为:

open source Kubernetes-native API gateway for microservices built on the Envoy Proxy

我们很容易将这个产品与Istio、Envoy混淆起来,那它们之间的区别是什么呢?这里有篇文章,介绍了具体的实践,但我们直接可以从标题中得到关键信息:

  • Ambassador - Edge proxy
  • Istio - service mesh

而Envoy+Contour对标的是Istio,所以不难看出,云原生推荐将Ambassador作为一种边缘网关,更多地是作为整体流量的出入口。如果说service mesh是一种对网络的精细化管理,那么边缘网关更多地是对整体流量的管理。

但在实际使用中,两者有大量的重叠功能,如路由、认证、限速等,而我也没有在网上找到相关的Best Practice,这部分就很需要大家的经验与摸索了。从我的角度来看,更倾向于大家多使用Envoy的特性,毕竟它的生态最成熟。即便后续有了替代品,对方也往往会提供对应的兼容方案。

这里,我引用了一个链接,讲述了微服务网关(Emissary-Ingress定位)与传统企业级网关的差异。

Primary Purpose 主要目标

  • 传统企业级网关 - Expose, compose, and manage internal business APIs
  • 微服务网关 - Expose and observe internal business services

微服务网关相对而言会更轻量级,而且对外以服务的维度呈现。

Publishing Functionality 发布功能

  • 传统企业级网关 - API management team or service team registers / updates gateway via admin API
  • 微服务网关 - Service team registers / updates gateway via declarative code as part of the deployment process

微服务网关更倡导自运维的方式。

Handling and Debugging Issues 处理和Debug问题

  • 传统企业级网关 - L7 error-handling (e.g. custom error page or payload). Run gateway/API with additional logging. Troubleshoot issue in staging environment

  • 微服务网关 - Configure more detailed monitoring. Enable traffic shadowing and / or canarying

微服务网关更强调的是网络侧的更细致化的管理,突出了灵活性。

2022-01-21 谈谈Context在Go程序外的传递

这两天,有个读者跟我讨论了一个关于链路追踪的问题 - 他发现通过context传递的trace-id丢失了。从根本上来说,这涉及到了Context在Go程序外的传递的理解,细想的话很有意思,所以我今天专门拿出来讲讲。

如果你对Go语言有一定的基础,会很习惯地使用Context作为上下文信息的传递:无论是内部函数的调用,还是RPC的调用,这就会产生一种错觉:context的传递是水到渠成的

这位同学的调用链路是 Go程序 - MQ - Go程序,发现context中的trace-id丢失了。

trace-id用于分布式的链路追踪

分析这个问题不难,有很多种思路,但我比较建议大家从底层实现去理解:

  1. context的本质,是一种key-value的数据结构;
  2. 服务之间的调用,本质上是一种网络上的二进制数据的传递,而context信息肯定是保存在其中的
  3. HTTP是一种二进制数据上的协议表达格式,我们常见的RESTful API 的 json数据是填入到body中,但HTTP协议还有很多地方可以保存数据,尤其是HTTP Header。context信息大概率是被保存在Header里的;

Go语言的Client将context封装到Header里,Server将context从Header中解析出来

默认的官方库是不会写直接写的,需要做一层浅封装:

发送前,从ctx中将对应的k-v填入到header;

收到后,从header中提取k-v,写入到context。

要验证一下上面的猜想很简单,我强烈大家用最底层的方式:Wireshark抓包。事实也如我们的预期,Go语言的ctx传递正是用这种方式实现的。而MQ这种第三方组件,往往不会去解析这种特定的Header,所以会发生丢失。

这时,我们有什么样的解决方案呢?

  1. 制定标准。这也是OpenTracing等规范推崇的,对广大的开发者来说非常友好,尤其是跨语言的情况,但这块非常依赖生态,需要大量的SDK,整体来说推进进度比较难。(这就体现了Service Mesh的价值)
  2. 显示传递。相当于把关键信息填到HTTP的body里,对具体代码的实现来说侵入性很强,但不失为中小型公司快速落地的方案;遇到一些不支持标准的第三方软件,我们也往往只能采用这种方式。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码

2022-01

2022-01-10 Unix设计哲学概览

Unix设计哲学在程序员的圈子里经久不衰,备受追捧。而Go语言背后有很多UnixC语言的影子,三位创始人Rob Pike(罗伯. 派克),Ken Thompson(肯. 汤普森)和Robert Griesemer(罗伯特. 格利茨默)都是这两块领域的泰山北斗。了解Unix的设计哲学,对写出优秀的代码很有帮助。

Doug McIlroy的总结

Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.

做一件事,做好它。让程序能够互相协同工作。应该让程序处理文本数据流,因为这是一个通用的接口。

作为管道机制的发明者,Doug McIlroy对这块的解释重点很具有管道特色。这里,我们聚焦于一个点:做好一件事

将其拆分一下,主要分为两块:

  1. 做好。做一件事,可能只需要10分的能力;而做好,则需要100分的能力(但 做好 这个标准,尽可能地按照自己的认知上限去做,才可能有成长)。
  2. 一件事。怎么定义一件事呢?边界的定义就难倒了很多人。这个非常考验能力,更具体可能需要case by case去看。

Rob Pike的总结

  1. 你永远不会知道你的程序会在什么地方耗费时间。程序的瓶颈常常出现在意想不到的地方,因此在你确信找到瓶颈后再动手优化代码吧。
  2. 测试代码。只有在你详细测试了代码,并且发现一部分代码耗费了绝大部分的运行时间时再对程序作速度优化。
  3. 功能全面的算法(fancy algorithm)在处理小规模问题时效率很低,这是因为算法时间效率中的常量很大,而问题往往规模很小。除非你知道你遇到的常常是复杂的情况,否则就让代码丑陋但是简单而高效吧。(即使问题规模确实很大,也首先尝试第二条规则。)
  4. 功能全面的算法比简单的算法更容易产生bug,更难实现。尽量使用简单的算法和数据结构。
  5. 数据决定一切。如果选择的数据结构能很好的管理数据,算法部分往往不言自明。记住,数据结构,而非算法,才是编程的关键。
  6. 没有第六条规则。

按照wiki上的说法,1、2可以归纳为 过早的优化是一切罪恶的根源,3、4可以理解为 疑惑不定之时最适合穷举

而第五条就非常有意思,也就是 数据结构比算法更重要

这点,和我们在刷算法题时认知相反 - 在做算法题目时,我们往往已经得到了一个具体的数据结构,要做的更多是根据这个数据结构选择合适的算法。当数据结构确定时,可选择的算法就很有限了,这也大大缩小了题解的范围。

在复杂的场景中,我们首先得确定数据结构,这一步尤为复杂:

  1. 复合 - 数据结构往往是复合的、嵌套的,单一的数据结构很少见;
  2. 抓住核心 - 可用数据结构解往往有多种,关键是识别其中决定性的因素;
  3. 不确定性 - 最优解的数据结构,往往会根据时间变化而变化;
  4. 简单性 - 性能最优解并不一定是最终的解,实际工程中多考虑简单性;

小结

关于Unix的设计哲学还有很多优秀的见解,但综合起来,可以归纳为KISS原则,也就是简单性。希望大家能在工程实践中,多多思考怎么保证简单性,对做出优秀的设计会很有启发。

2022-01-11 CNCF-CrossPlane

今天我们一起来看CNCF中的第二个项目 - CrossPlane 。它位于CNCF全景图中Kubernetes旁,受众比较小。

先看一段来自官网 https://crossplane.io/ 的信息:

Crossplane is an open source Kubernetes add-on that enables platform teams to assemble infrastructure from multiple vendors, and expose higher level self-service APIs for application teams to consume, without having to write any code.

我们依旧抽取其中关键的词进行解析:

  • Kubernetes add-on CrossPlane的定位是Kubernetes的插件,并非一个独立的平台
  • assemble infrastructure from multiple vendors 统一封装多个依赖方的基础设施
  • expose higher level self-service APIs 暴露高层接口

然后,官方描述了五个关键性的特征:

  • Provision and manage cloud infrastructure and services using kubectl
  • There is a flavor of infrastructure for everyone on Crossplane
  • Publish simplified infrastructure abstractions for your applications
  • The Universal Cloud API
  • Run Crossplane anywhere

概括起来,可以定义为:使用 kubectl 封装了各类云的API,来统一管理基础设施、服务和应用

我们从具体的实践角度来看,统一封装接口往往只是加分项,而核心在于 支持的云基础设施与服务的范围

官方文档 可以看到,CrossPlane主要支持了亚马逊云、谷歌云、微软云等厂商。可以看到,这款产品主要面向的是国外的公有云厂商。这其实也决定了CrossPlane更多面向的是国外开发者。

学习CrossPlane的更多细节,对我们来说暂时没有更大的意义。我们可以尝试着从其理念中得到以下启发:

  1. 对接各类云厂商是复杂的。尽管CNCF统一了大致上的理念,但细节上的实现各有不同;尤其是结合了账号、权限、资源分配等各类特性后,对接起来的复杂度很高。
  2. 如果一层不能解决,那就加一层。 直接对接各类公有云复杂度很高,也会为Kubernetes这个核心项目带来大量侵入性极强的代码。这时,引入另一个项目作为防腐层,很具有扩展意义。
  3. 一个核心项目不一定需要做成大型平台,更重要的是它的特性符合整个生态的发展理念。

2022-01-12 微服务架构特征

今天的话题将围绕着一篇谈论微服务架构的文章展开。下面给出原文链接,以及一个翻译的版本:

文中给出了以下九个微服务特征:

  1. Componentization via Services 通过服务来实现组件化
  2. Organized around Business Capabilities 围绕业务能力构建
  3. Products not Projects 面向产品,而不是面向项目
  4. Smart endpoints and dumb pipes 终端智能化,通讯轻量化
  5. Decentralized Governance 分散治理
  6. Decentralized Data Management 数据去中心化管理
  7. Infrastructure Automation 基础设施自动化
  8. Design for failure 容错性设计
  9. Evolutionary Design 演进式设计

这些点,每个抛出来都可以写一篇长文。建议有兴趣的各位可以阅读原文,结合自己的实践多多思考。

这里,我选择三个最近感触比较深的点,自己也曾经陷入过的认知误区,在这里和大家聊聊:

围绕业务能力构建

业务能力的概念很抽象,虽然我们会经常提及,但在实践过程中又往往容易忽略。

从系统的角度来看,业务能力往往就是对外呈现的功能,对应到内部的技术模块,往往已经决定了七七八八。如何将这些技术模块做合理的拆分与合并,就是微服务架构需要考量的点。这里我谈谈最近比较有心得的三个考量点:

  • 拆分高频变化与低频变化
  • 分离计算密集型和IO密集型
  • 基础能力尽早引入业界的通用模块

当然,还有更多的内容,需要大家在实践中摸索。

终端智能化,通讯轻量化

这一点在云原生的服务中体现得淋漓尽致:以RPCService Mesh服务发现等技术为代表。

终端体现在Pod这一层,也就是对一个具体运行的App来说,通过Istio、CoreDNS等技术将分布式的服务做到和单体应用一致,然后通过轻量级的通讯方案,如HTTP进行交互。这种方式的优点很明显:

  1. 分布式服务之间的通讯复杂度最高的部分,由专用的、成熟的组件,引入到Pod层面完成;
  2. 开发者的代码实现只需要关心RPC的数据出入,复杂度大幅度降低;

目前云原生的Service Mesh技术还未完全形成行业标准,相信很快随着它的落地,将迎来微服务的又一波热潮。

容错性设计

容错性设计,也就是为错误而设计,这一点很反直觉。

作为一名开发者,我们实现功能的思路往往是按照顺序的逻辑步骤;一个一个步骤的串联,才能保证最后的功能实现。但这个时候,如果要我们去关注各类错误的发生,小到网络波动、程序崩溃,大到机房断电,很容易无所适从。

这里,我谈谈自己的理解:主要从发生的概率与影响的严重程度来思考,不要过度追求细节。这里有一个很重要的权衡点 - **健壮性 **与 简单性 :一般来说,要保证程序足够健壮,会引入各种异常的容错性设计,增加系统的复杂度,但这一点并不是绝对的。

从系统整体功能的维度,虽然看起来增加了复杂度,但通过分层、模块化、服务拆分等方式,分而治之 - 一些简单的模块用简单的规则组合成一个大模块,可维护性远远高于一个复杂的模块。

2022-01-13 CNCF-CoreDNS

CoreDNS是CNCF全景图中 协调与服务发现 模块的核心项目,在Kubernetes1.12版本之后成为了默认的DNS服务。熟悉CoreDNS是掌握Kubernetes必不可少的技能。

照例,我们先一起看下其核心定义,非常简洁明了:

官网 - CoreDNS: DNS and Service Discovery

CNCF - CoreDNS is a DNS server that chains plugins

今天,我们将围绕一个关键词chains plugins - 链式插件 展开,这也是CoreDNS实现的核心特性。

官方对这个特性的定义如下,

CoreDNS chains plugins. Each plugin performs a DNS function, such as Kubernetes service discovery, prometheus metrics, rewriting queries, or just serving from zone files. And many more.

从中不难看出,CoreDNS将各种DNS的功能抽象成一个插件,进行链式调用。

我们用 官方github上的Corefile 来了解这个特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
example.org:1053 {
file /var/lib/coredns/example.org.signed
transfer {
to * 2001:500:8f::53
}
errors
log
}

. {
any
forward . 8.8.8.8:53
errors
log
}
  1. 当访问example.org:1053 时,依次触发 file、transfer、errors、log四个插件
  2. 访问其余DNS时,依次触发 any、forward、errors、log四个插件

上面的语法与nginx非常类似,而作者实际上是参考Caddy软件进行设计的,有兴趣的可以查阅相关资料,例如这个 博客

链式调用是一种表述形式非常强的语法:它以一个大众容易接受的顺序逻辑,讲述了一个完整的调用过程,将各个细节也描述地很清晰

既然链式调用的描述方式那么棒,为什么目前没有大规模地推广到各类工具上呢?这里,我谈谈个人的三个理解:

  1. 核心模块支持插件化 - 链式调用需要动态加载各类插件,这一点对核心的模块要求很高,很多软件设计之初就决定了很难走插件化的道路;
  2. 无需兼容历史问题 - 很多的工具都存在大量的历史版本,很难做到完全兼容这种表达形式;而CoreDNS在设计之初就强制采用这种规范,没有历史包袱;
  3. 每个插件的输入与输出保持一致 - 作为一种链式调用,为了保证前者输入可以作为后者输出,两者支持的数据格式必须一样(可以参考Unix中的管道)。DNS服务的功能比较简单,可以保证一致;

同样的,Corefile这种声明方式,也或多或少带来了一些问题,例如:

  1. 文件解析的复杂性(这点与链式调用本身无关) - 比如数据类型的问题,很难确定文件中的5是数字还是字符串;
  2. 链式调用很难解决前后有依赖的情况 - 如调用A插件的结果有个特别的输出,用于插件B的输入时,很难解决;
  3. 长链式调用的复杂度 - 如果链式调用过长,一方面带来了异常情况下排查问题的复杂度,另一方面很容易出现性能问题;

CoreDNS的成功,链式调用插件 这个特性只体现了简单性的理念,并不是关键性的原因,而更多地是依赖大量开箱即用的插件

2022-01-14 不可变基础设施

看起来,这功能描述与CICD流程差不多,但使用体验差距很大。在传统的模式下,我们执行的是一个具体的动作,比如扩1个应用、升级2个程序等;而在k8s里,使用者只要声明最终的预期状态,比如5个应用运行v1.0版本的程序,那么整个系统该扩容还是缩容、该升级还是回滚,都由k8s自行根据当前状态进行判断。

ab4ea1c14f669e0e42040689da7aa074c4e479f3

云原生有五大代表性的技术 - 容器、服务网格、微服务、不可变基础设施和声明式 API。相对于其余四种概念,不可变基础设施 - Immutable Infrastructure 更难理解,今天我们来一起看看。

入门可以参考这篇文章 - https://zhuanlan.zhihu.com/p/382203966

网上可搜索到的不可变基础设施定义有很多,这里我选择一个比较有代表性的:

Immutable infrastructure refers to servers (or VMs) that are never modified after deployment.

从开发者的角度来看,不可变基础设施是一个很棒的概念。如果用一个词总结它的优点,那就是 时间与空间上的一致性

如果有一个传统应用希望改造成适配 不可变基础设施 的场景,那么会有哪些常见的改造点呢?

  1. 将传统应用的运行环境打造成一个具体的服务器,例如虚拟机、容器;
  2. 重点分析该应用的输出形式,让其与 服务器无关

第二点有些抽象,这里我举三个具体的例子:

  1. 本地缓存 - 转移到分布式缓存服务;
  2. 本地保存的文件 - 转移到分布式存储服务中;
  3. 本地日志 - 将原来打印到本地文件的日志服务,重定向到标准输出,由日志采集的side-car收集后统一汇总;

但在实际的工程中,追求 不可变基础设施 很难完全落地,我们可以适当地做一些权衡:

  1. 如果日志不允许落盘对部分程序的改造成本很高,那么我们可以用ELK等一套日志收集方案做准实时的同步,保证日志可丢失;
  2. 如果完全依赖分布式缓存对性能压力过大,那么就建立一套分布式缓存与本地缓存的自动同步机制,保证重启后本地缓存丢失,仍可以恢复;

不难看出,只要我们保证应用在基础设施上产生的数据 可在任意时间丢失,就能实现了一定程度上 应用无状态化,也能保证了不可变基础设施的落地。

不可变基础设施是一种理念,具体落地的技术非常依赖容器或虚拟机,以及分布式存储等配套设施。我们没有必要把它作为一种技术标准去强制执行,而应该结合现状,选择性地朝着这个方向不断优化。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码

2022-01

2022-01-04 Go1.18概览

在2021年年底,Go推出了1.18Beta版本。由于正式版本没有完全敲定,普通开发人员没有必要研究到底层实现,但如果能先形成一个全局上的认知,能帮助我们领先一步。

关于1.18的核心改动,是 对泛型(Generics)的支持。Go语言的泛型语法比较简单,如下:

1
2
3
4
5
6
7
8
9
10
type numeric interface {
type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64
}

func min[T numeric](a, b T) T {
if a < b {
return a
}
return b
}

如果要在实际工程上落地,还有很多考量点,我这边重点提三点:

  1. 主流IDE的支持 - 包括Goland与VSCode,尽量帮助开发者能在编码时发现问题;
  2. 历史库的迁移与兼容 - 实现泛型后,许多主流库会有大量的冗余函数,如何提供迁移方案,值得我们去关注;
  3. 泛型的最佳工程实践 - 作为一种新特性,Go的泛型如何应用在工程中、尤其是复杂工程中,需要一定的实践摸索,并总结规律(可借鉴其余支持泛型的语言);

除了泛型,另外一个比较大的特性就是Fuzzy Testing

这个特性是为单元测试提供更全面的数据输入,这样就能覆盖更多的case,提前发现问题。关键词Fuzzy支持的主要特性是将一个输入参数,从具体的值变成范围,如原先输入a=1,现在支持a输入范围为[-10,10]。在跑单元测试时,大量的Fuzzy肯定会带来一定的性能压力,这时可以引入一定的并发特性。

总体来说,Go1.18对工程侧的影响更多地是提高代码的 简洁性。新特性的学习成本很低,我们不用过于急着引入,可以多花时间学习底层原理。

Go Blog - https://go.dev/blog/go1.18beta1

Medium - https://betterprogramming.pub/golang-1-18-what-you-need-to-know-a5701f7e14ab

2022-01-05 CNCF-概览

CNCF作为云原生的代表性组织,提供了大量开源的软件,以及配套的、开箱即用的解决方案。有很多朋友对CNCF和云原生的认识可能仍停留在新闻报道里。今天,我先带大家在整体上入个门,后续选择具有代表性的软件进行分析。

由于篇幅所限,我的分享只会提重点知识,帮大家建立这部分的知识框架,更详细的内容需要大家自行学习。

CNCF的概览可以参考这个全景图 - https://landscape.cncf.io/,更新迭代非常频繁。其中,最核心的为下面五块:

landscape

  • App Definition and Development 应用定义与开发
    • Database 数据库
    • Streaming & Messaging 流处理和消息通信
    • Application Definition & Image Build 应用定义与镜像构建
    • Continuous Integration & Delivery 持续集成与交付
  • Orchestration & Management 编排和管理
    • Scheduling & Orchestration 调度与编排
    • Coordination & Service Discovery 协调与服务发现
    • Remote Procedure Call 远程过程调用
    • Service Proxy 服务代理
    • API Gateway API网关
    • Service Mesh 服务网格
  • RunTime 运行时
    • Cloud Native Storage 云原生存储
    • Container Runtime 容器运行时
    • Cloud Native Network 云原生网络
  • Provisioning 提供者
    • Automation & Configuration 自动化与配置
    • Container Registry 容器注册
    • Security & Compliance 安全与合规
    • Key Management 密钥管理
  • Observability and Analysis 可观察性和分析
    • Monitoring 监控
    • Logging 日志
    • Tracing 跟踪
    • Chaos Engineering 混沌工程

其余还包括Kubernetes的平台提供商、Serverless、成员、认证的服务提供商等周边内容,并不在我们讨论的范围之内。但从基金会来看,它提供了一整套生态,非常有助于落地。

那么,如何认识这五块呢?其实Landscape提供了很好的图形效果,我们只要记住两点:

  1. 应用定义与开发编排和管理运行时提供者 这四块是自上而下的核心链路,下层为上层提供能力支撑
  2. Observability and Analysis 是核心链路旁边的重要支撑

相信到这里,你对CNCF已经有了初步认识。

2022-01-06 《我做系统架构的一些原则》From 陈皓

今天,给大家推荐一篇来自左耳朵耗子-陈皓的文章

  1. 关注于真正的收益而不是技术本身
  2. 以应用服务和 API 为视角,而不是以资源和技术为视角
  3. 选择最主流和成熟的技术
  4. 完备性会比性能更重要
  5. 制定并遵循服从标准、规范和最佳实践
  6. 重视架构扩展性和可运维性
  7. 对控制逻辑进行全面收口
  8. 不要迁就老旧系统的技术债务
  9. 不要依赖自己的经验,要依赖于数据和学习
  10. 千万要小心 X – Y 问题,要追问原始需求
  11. 激进胜于保守,创新与实用并不冲突

以上11点,理解会因人而异,我重点挑三个争议性比较大的聊聊,其余的内容建议大家阅读原文。

完备性会比性能更重要

借用书中的一句话:使用最科学严谨的技术模型为主,并以不严谨的模型作为补充,也就是先紧后松。

有不少开发者在实际工程中的实践往往相反:为了追求快速落地,会希望毕其功于一役,引入所谓的“一站式解决方案”(如例子中NoSQL),但实践下来引入大量的问题,让后人叫苦不迭。

关于这个问题,我个人有三个思考:

  • 不断提高自己的基础能力。很多架构上的局限性,往往是设计者停留于自己的舒适区,不愿意往前一步。
  • 分清主次、合理分工。 在设计时,我们要分清楚核心功能和非核心功能,懂得取舍,将功能交由合适的模块或软件。
  • 功能的实现不是对应到单模块,而是整个系统的涌现。性能问题的解法不仅仅限于单个软件,而是有一整套生态,可以多去查一些大厂的分享。

不要迁就老旧系统的技术债务

为了缩小讨论范围,我对这里 技术债务 做一个收口:不仅仅是指有弊端的技术问题,更是需要投入时间精力等成本去维护。有技术债务,不代表就一定要去还,而需要一个契机 - 维护的成本 > 修复的收益

举个例子,某个程序写得很烂,性能很差:

  • 前期可以通过扩容快速解决,上线后业务收益很高,那就是 成本大于收益,不需要排到最高优先级去修复;
  • 但随着业务收益稳定下来,增长只有个位数,但发现在机器上投入的成本很高,通过优化预计能缩容50%,可以让总收益提升20%,那这时还技术债务的优先级就很高了。

还技术债,技术能力只是一个基本,以下两点更为重要:

  • 评估成本与收益,尽可能地做到有数据支撑,有助于决策者下决心还债;
  • 用更长远、更广维度地看待技术债务问题,不要拆东墙补西墙。

激进胜于保守,创新与实用并不冲突

这个观点是很aggressive的,遇到这样的观念冲突时,决策者找不到客观标准去评估,就很难有二义性的定义:要么激进,要么保守;要么创新,要么实用。这种情况下,我遇到过的比较好的解法有两种:

  1. 决策者自身技术能力强,能掌控团队技术的大致走向。
  2. 决策者和执行者之间培养出足够的信任,适当评审与把控,放权实践。

第一种情况在实际工作场景中并不多见,尤其当团队规模很大时,就像CTO往往不是公司技术最强的那位。所以,我更倾向于大家多尝试第二种途径。

当然,我也遇到过很多效果不好的解法,比如说:决策者既然不清楚怎么做才好,那就找2个执行者进行battle,一个代表创新方,另一个代表保守方。也许在少数情况下,最后能帮助决策者找到正确的方向;但更多的实际场景中,会产生如下问题:

  • 两个执行者碰撞越来越激烈,但始终谁也说服不了谁
  • 决策者越听越迷茫,不知道该如何抉择

在我看来,这类决策者往往是偏管理,技术上的掌控力不足,导致在决策时没有足够的倾向性;同时,与执行者之间的信任也不足,就希望把决策这件事下移、尝试着走平衡之道。

我个人的想法是:先选择一个能力相对优秀的执行者,认真评估其方案,然后交由对方执行;最后哪怕失败了,也可以通过复盘改进,想想下次如何更好地决策,更好地把控方案;当然,如果你认为纯粹是执行者的问题,那就换个可信赖的人。

2022-01-07 CNCF-Kubernetes

今天,我们一起来看看CNCF中的最核心项目 - KubernetesKubernetes相关内容非常庞大,我们依旧关注聚焦于核心能力。

Kubernetes 位于CNCF核心的 调度与编排 模块,也就是整个解决方案的基石。在CNCF上的介绍为:

Kubernetes is an open-source system for automating deployment, scaling, and management of containerized applications.

对这个定义,我们关注两个点:

  • automating deployment, scaling, and management 自动化的部署、扩缩容和管理,这是k8s核心能力;
  • containerized applications 容器化应用,这是k8s操作的基本对象;

为了更好地介绍Kubernetes,我对官方首页描述中的关键特性再做一些更详细地说明:

Automated rollouts and rollbacks

自动化扩缩容和升级回滚。这个特性是k8s最核心的,也是大规模推广的根本原因。

看起来,这功能描述与CICD流程差不多,但使用体验差距很大。在传统的模式下,我们执行的是一个具体的动作,比如扩1个机器、升级2个程序等;而在k8s里,使用者只要声明最终的预期状态,比如5台机器运行v1.0版本的程序,那么整个系统该扩容还是缩容、该升级还是回滚,都由k8s自行根据当前状态进行判断。

这个,就是云原生的一大特性:声明式API ,而不是传统上的命令式API

声明式API不一定比命令式API好。在应用程序开发时,命令式API更容易理解。

Service discovery and load balancing

服务发现与负载均衡。这个功能很大程度上减少了分布式软件运行模式的复杂性。

服务发现,以前非常依赖zookeeper/etcd等这类注册中心,往往需要侵入到业务代码;而负载均衡,则很依赖nginx这类软件,并在上面做复杂配置。

当然,k8s给出的只是通用解法,对一些具备很强业务属性的服务发现与负载均衡,仍需要程序自行实现。

Storage orchestration

存储编排。存储的编排是k8s重点演进的功能。

k8s抽象了存储概念,从传统的本地存储扩展为分布式云存储,对上层应用屏蔽了存储这块的复杂度。

Designed for extensibility

为扩展性而设计。

扩展性是k8s非常重视的点,无论是开放出容器、网络、存储等接口规范,还是像自定义资源(CRD)等插件的开放,都体现出了一种开放的精神,也是k8s如今能作为云原生标志性的软件的立足之本。

有一个点希望大家认识到:k8s的成功不是简单地因为开放性,更重要的是,它定义的这些开放性的规范与接口,都是Google经过实践总结出来的经验,符合主流厂商的趋势与开发者的需求。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码

go-tip

Ent概览

Simple, yet powerful ORM for modeling and querying data.

Ent作为一款由Facebook开源的库,官方定义为An entity framework for Go。从整个微服务框架来看,它更准确的定位应是 数据模型层的工具库。了解Ent这款企业级工具的大致实现,不仅有助于我们在技术选型时拓宽视野,也能帮助我们能更好地认识数据模型层。

阅读全文 »

go-tip

Wire概览

在讲解Kratos的过程中,我们引入了google推出的wire这个工具。我们先阅读一下官方的定义:

Wire is a code generation tool that automates connecting components using dependency injection.

从关键词入手:

  • code generation 代码生成,一方面说明了有学习成本,需要了解这个工具的原理;另一方面,也说明了它的目标是消除重复性的coding
  • automates connecting components 自动连接组件,明确了wire工具的目标是将多个对象组合起来
  • dependency injection 依赖注入,指明了wire实现自动连接组件的思想。依赖注入是一个很强大的功能,我会在下面结合具体的case聊一聊

我们从具体的case着手,学习wire这个工具。

阅读全文 »