2022-01-10 Unix设计哲学概览
Unix设计哲学在程序员的圈子里经久不衰,备受追捧。而Go
语言背后有很多Unix
与C语言
的影子,三位创始人Rob Pike(罗伯. 派克),Ken Thompson(肯. 汤普森)和Robert Griesemer(罗伯特. 格利茨默)都是这两块领域的泰山北斗。了解Unix的设计哲学,对写出优秀的代码很有帮助。
- 英文资料 - https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch01s06.html
- 中文wiki - https://zh.wikipedia.org/wiki/Unix%E5%93%B2%E5%AD%A6
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对这块的解释重点很具有管道特色。这里,我们聚焦于一个点:做好一件事。
将其拆分一下,主要分为两块:
- 做好。做一件事,可能只需要10分的能力;而做好,则需要100分的能力(但
做好
这个标准,尽可能地按照自己的认知上限去做,才可能有成长)。 - 一件事。怎么定义一件事呢?边界的定义就难倒了很多人。这个非常考验能力,更具体可能需要
case by case
去看。
Rob Pike的总结
- 你永远不会知道你的程序会在什么地方耗费时间。程序的瓶颈常常出现在意想不到的地方,因此在你确信找到瓶颈后再动手优化代码吧。
- 测试代码。只有在你详细测试了代码,并且发现一部分代码耗费了绝大部分的运行时间时再对程序作速度优化。
- 功能全面的算法(fancy algorithm)在处理小规模问题时效率很低,这是因为算法时间效率中的常量很大,而问题往往规模很小。除非你知道你遇到的常常是复杂的情况,否则就让代码丑陋但是简单而高效吧。(即使问题规模确实很大,也首先尝试第二条规则。)
- 功能全面的算法比简单的算法更容易产生bug,更难实现。尽量使用简单的算法和数据结构。
- 数据决定一切。如果选择的数据结构能很好的管理数据,算法部分往往不言自明。记住,数据结构,而非算法,才是编程的关键。
- 没有第六条规则。
按照wiki上的说法,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
的更多细节,对我们来说暂时没有更大的意义。我们可以尝试着从其理念中得到以下启发:
- 对接各类云厂商是复杂的。尽管
CNCF
统一了大致上的理念,但细节上的实现各有不同;尤其是结合了账号、权限、资源分配等各类特性后,对接起来的复杂度很高。 - 如果一层不能解决,那就加一层。 直接对接各类公有云复杂度很高,也会为
Kubernetes
这个核心项目带来大量侵入性极强的代码。这时,引入另一个项目作为防腐层,很具有扩展意义。 - 一个核心项目不一定需要做成大型平台,更重要的是它的特性符合整个生态的发展理念。
2022-01-12 微服务架构特征
今天的话题将围绕着一篇谈论微服务架构的文章展开。下面给出原文链接,以及一个翻译的版本:
- 原文 - https://martinfowler.com/articles/microservices.html
- 中文翻译 - https://blog.51cto.com/u_15127669/4175353
文中给出了以下九个微服务特征:
- Componentization via Services 通过服务来实现组件化
- Organized around Business Capabilities 围绕业务能力构建
- Products not Projects 面向产品,而不是面向项目
- Smart endpoints and dumb pipes 终端智能化,通讯轻量化
- Decentralized Governance 分散治理
- Decentralized Data Management 数据去中心化管理
- Infrastructure Automation 基础设施自动化
- Design for failure 容错性设计
- Evolutionary Design 演进式设计
这些点,每个抛出来都可以写一篇长文。建议有兴趣的各位可以阅读原文,结合自己的实践多多思考。
这里,我选择三个最近感触比较深的点,自己也曾经陷入过的认知误区,在这里和大家聊聊:
围绕业务能力构建
业务能力的概念很抽象,虽然我们会经常提及,但在实践过程中又往往容易忽略。
从系统的角度来看,业务能力往往就是对外呈现的功能,对应到内部的技术模块,往往已经决定了七七八八。如何将这些技术模块做合理的拆分与合并,就是微服务架构需要考量的点。这里我谈谈最近比较有心得的三个考量点:
- 拆分高频变化与低频变化
- 分离计算密集型和IO密集型
- 基础能力尽早引入业界的通用模块
当然,还有更多的内容,需要大家在实践中摸索。
终端智能化,通讯轻量化
这一点在云原生的服务中体现得淋漓尽致:以RPC、Service Mesh、服务发现等技术为代表。
终端体现在Pod
这一层,也就是对一个具体运行的App来说,通过Istio、CoreDNS等技术将分布式的服务做到和单体应用一致,然后通过轻量级的通讯方案,如HTTP进行交互。这种方式的优点很明显:
- 分布式服务之间的通讯复杂度最高的部分,由专用的、成熟的组件,引入到Pod层面完成;
- 开发者的代码实现只需要关心RPC的数据出入,复杂度大幅度降低;
目前云原生的Service Mesh技术还未完全形成行业标准,相信很快随着它的落地,将迎来微服务的又一波热潮。
容错性设计
容错性设计,也就是为错误而设计,这一点很反直觉。
作为一名开发者,我们实现功能的思路往往是按照顺序的逻辑步骤;一个一个步骤的串联,才能保证最后的功能实现。但这个时候,如果要我们去关注各类错误的发生,小到网络波动、程序崩溃,大到机房断电,很容易无所适从。
这里,我谈谈自己的理解:主要从发生的概率与影响的严重程度来思考,不要过度追求细节。这里有一个很重要的权衡点 - **健壮性 **与 简单性 :一般来说,要保证程序足够健壮,会引入各种异常的容错性设计,增加系统的复杂度,但这一点并不是绝对的。
从系统整体功能的维度,虽然看起来增加了复杂度,但通过分层、模块化、服务拆分等方式,分而治之 - 一些简单的模块用简单的规则组合成一个大模块,可维护性远远高于一个复杂的模块。
2022-01-13 CNCF-CoreDNS
CoreDNS
是CNCF全景图中 协调与服务发现 模块的核心项目,在Kubernetes
1.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 | example.org:1053 { |
- 当访问
example.org:1053
时,依次触发 file、transfer、errors、log四个插件 - 访问其余DNS时,依次触发 any、forward、errors、log四个插件
上面的语法与nginx
非常类似,而作者实际上是参考Caddy
软件进行设计的,有兴趣的可以查阅相关资料,例如这个 博客。
链式调用是一种表述形式非常强的语法:它以一个大众容易接受的顺序逻辑,讲述了一个完整的调用过程,将各个细节也描述地很清晰。
既然链式调用的描述方式那么棒,为什么目前没有大规模地推广到各类工具上呢?这里,我谈谈个人的三个理解:
- 核心模块支持插件化 - 链式调用需要动态加载各类插件,这一点对核心的模块要求很高,很多软件设计之初就决定了很难走插件化的道路;
- 无需兼容历史问题 - 很多的工具都存在大量的历史版本,很难做到完全兼容这种表达形式;而
CoreDNS
在设计之初就强制采用这种规范,没有历史包袱; - 每个插件的输入与输出保持一致 - 作为一种链式调用,为了保证前者输入可以作为后者输出,两者支持的数据格式必须一样(可以参考Unix中的管道)。DNS服务的功能比较简单,可以保证一致;
同样的,Corefile这种声明方式,也或多或少带来了一些问题,例如:
- 文件解析的复杂性(这点与链式调用本身无关) - 比如数据类型的问题,很难确定文件中的
5
是数字还是字符串; - 链式调用很难解决前后有依赖的情况 - 如调用A插件的结果有个特别的输出,用于插件B的输入时,很难解决;
- 长链式调用的复杂度 - 如果链式调用过长,一方面带来了异常情况下排查问题的复杂度,另一方面很容易出现性能问题;
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.
从开发者的角度来看,不可变基础设施是一个很棒的概念。如果用一个词总结它的优点,那就是 时间与空间上的一致性。
如果有一个传统应用希望改造成适配 不可变基础设施 的场景,那么会有哪些常见的改造点呢?
- 将传统应用的运行环境打造成一个具体的服务器,例如虚拟机、容器;
- 重点分析该应用的输出形式,让其与 服务器无关;
第二点有些抽象,这里我举三个具体的例子:
- 本地缓存 - 转移到分布式缓存服务;
- 本地保存的文件 - 转移到分布式存储服务中;
- 本地日志 - 将原来打印到本地文件的日志服务,重定向到标准输出,由日志采集的side-car收集后统一汇总;
但在实际的工程中,追求 不可变基础设施 很难完全落地,我们可以适当地做一些权衡:
- 如果日志不允许落盘对部分程序的改造成本很高,那么我们可以用ELK等一套日志收集方案做准实时的同步,保证日志可丢失;
- 如果完全依赖分布式缓存对性能压力过大,那么就建立一套分布式缓存与本地缓存的自动同步机制,保证重启后本地缓存丢失,仍可以恢复;
不难看出,只要我们保证应用在基础设施上产生的数据 可在任意时间丢失,就能实现了一定程度上 应用无状态化,也能保证了不可变基础设施的落地。
不可变基础设施是一种理念,具体落地的技术非常依赖容器或虚拟机,以及分布式存储等配套设施。我们没有必要把它作为一种技术标准去强制执行,而应该结合现状,选择性地朝着这个方向不断优化。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding