2018 年总结 · 初窥门径

一转眼 2018 年就过去了,回想了一下今年确实做了非常多的事情,从零搭建交易所服务到负责基础架构以及微服务治理,整个 2018 年也是作者服务端技术逐渐成熟的一年,对于未来的职业规划和方向也变得越来越清晰,到了年末的时候也确实开始回忆和反思这一年个人到底有哪些提升和进步以及有哪些地方能够做得更好。

思维转变

在 2018 年,作者其实不止接触了计算机科学/软件工程这一个领域的知识,其实在业余时间也学习了其他领域的内容,通过对多个领域知识的学习和反思,作者也在尝试学习从实现细节中跳出来,从不同的角度和高度看待当前的问题,对自己的知识体系和架构也有一些反思和感悟,总得来说有两点比较有趣,在这里分享给大家。

永远都没有最优解

过去一年中,作者设计和实现了非常多的软件系统,在思考如何设计和定义一个架构或者服务时,经常会陷入纠结和『自闭』,因为很多设计其实并没有绝对的最优解,每种方案都会有它的优点和缺点。所以方案之间的讨论和争辩其实就是对各种方案利弊的权衡,每个人对于系统未来的设计和规划理解可能会有不同,这也是争执发生的主要原因。

在实现某一个特定的需求时,我们其实更需要列出已有的解法并为所有的解法给出优缺点,随后我们就需要预测业务的变化和增长,根据当前和未来的状况选择相对合适的方案,不过在选择技术方案时经常会遇到以下的问题:

  1. 了解当前的业务需求是相对比较容易的,而预测业务的变化和增长却非常困难
    • 预测业务就跟 CPU 的分支预测一样,一旦出现错误和偏差可能会导致在未来付出比较大的工程代价,整个系统中的一部分代码会迅速变成技术债务拖累整个项目的进度;
  2. 多数考虑未来业务发展的技术方案往往都有比较长的开发周期和较为复杂的工程实现
    • 一个项目中投入的人力和时间经常都是有限的,复杂的工程实现经常不能按照规划完成,所以妥协和降低要求是难以避免的;

如果我们把做软件和系统上的设计与做一些具体的工程实现相比较,就会发现实现一块逻辑时比较容易出现『绝对的』正确和错误,不过系统设计很多时候都是需要权衡的,做系统设计时也没有编译器来告诉我们哪里做错了,为什么做错了,也正是因为没有最优解,所以权衡(tradeoff)成为了软件系统设计时需要深思熟虑的事情。

软件设计中的模式和思想

除了认识到软件系统的设计是没有最优解之外,另一个比较深的感触就是我们应该对软件设计中的模式和思想更加重视,然而思想其实都是抽象的因为它们在抽象和形成的过程中失去了细节,也正是因为它们有比较少的细节,一些工程师才会认为这些思想难以使用,也就是『这些东西听起来都对,但是没什么用』,我们用下面的这句话举个例子:

软件设计要解决的最根本的问题就是控制复杂性。

这一句话描述了在软件工程中非常重要的思想,但是却很难直接应用,不过下面的几个具体例子就能够为我们提供控制复杂度的方法:

  1. 不要在未出现性能瓶颈时对代码进行优化;
  2. 不要在设计时引入不必要的中间层;

这些具体的例子相比抽象的思想多了很多的细节,也让我们可以不太需要思考而直接应用,但是无论是从抽象到具体,还是从具体到抽象的过程都需要不断思考才能成为自己知识的一部分,并在之后的工作和学习中快速应用。

abstract-and-concrete

一些我们觉得没有用或者讲地太泛的书籍可能就是因为它们传达的其实是某个领域中的思想,而不是具体细节,对于大多数人来说这些思想听起来非常浅显易懂不过没有办法使用,只有真正经过思考和内化才能内化成我们的经验。

技术成长

在这一年中,作者涉猎的技术也比较广泛,上半年的时间使用的比较多的是 Elixir 这门编程语言,同时做了一些区块链底层交易序列化和签名的工作,随后负责区块链交易系统的开发,最开始接触的技术主要还是 Elixir 和 Phoenix,之前虽然也学过函数式编程语言 Schema、Haskell,但是这段直接在生产环境中使用函数式编程语言的经历让我对这类语言有了更深的理解;除此之外。由于业务涉及一些一致性需求较强的业务,所以对分布式事务相关的知识进行了比较全面的学习。

下半年的时间主要做的就是 Kubernetes、Linkerd 以及微服务治理这些基础架构相关的事情,主要的开发语言也换成了 Golang,从搭建微服务的服务配置中心、监控报警系统到服务网格的调试和上线让我对于微服务治理和云原生有了一些初步的认识和理解。

在这里用一个矩阵来简单介绍这半年时间技术栈到底发生了怎样的变化:

2018-tech-stack

从这个技术栈迁移的图表出发,这里将这一年中使用的技术分成四个模块分别介绍作者对它们的看法以及具体学习了哪些内容。

区块链

年初的时候做了一些区块链相关的开发,主要是做一些主链的交易序列化、签名以及广播交易到主网的一些工作,在这段时间接触到了很多区块链的相关知识,包括 Bitcoin、Ethereum 这些常见公链的一致性协议、交易结构和签名算法,除此之外还学习了一些智能合约的原理和 Ethereum 上用于编写智能合约的编程语言 Solidity:

对区块链知识的深入学习其实帮助作者更好地理解分布式系统如何实现拜占庭容错,在一定程度上补充了已有的分布式数据库知识,也加深了对对称和非对称加密算法的掌握。

smart-contract

总得来看,区块链相关知识的学习对于个人知识体系的构建还是利大于弊的,但是学习的过程还是走了很多岔路,到目前来看个人认为学习智能合约的编写是一件投入产出比非常低的事情,在现有的区块链基础设施建设下,智能合约的应用范围非常狭窄,学习相关知识也只能用于开阔眼界,如果并不想在未来主要从事区块链相关的行业,还是不要浪费这些时间来学习智能合约的编写了。

区块链行业目前还是处于一个比较早期的阶段,很多基础设施的建设还并不完善,虽然由于近两年虚拟货币的火热带起了很多的热度,但是应用场景还是偏少,比较热门的一些落地应用基本都是博彩相关的场景,除此之外就是面向 B 端金融行业做一些结算的业务,整个行业还需要等待一些真正成熟并且能够落地的项目出现。

分布式系统

除了接触和了解区块链的一些知识,过去一年的时间让作者对分布式系统有了很多正确和错误的观念。毕业之前的几年时间一直做得都是客户端上的开发,所以有时也会将单体的客户端应用和分布式的后端服务作比较,看看这两者有什么异同。

首先,无论是客户端的应用还是后端服务,都是由多个组件(或服务)构成的软件系统,从作者的角度来看,这两者的主要区别就是后端服务往往都是由一组独立的服务器通过网络连接构成的分布式系统,我们可以从这一个根本的区别推断出以下的差异:

  1. 组件(或服务)之间的通信的可靠性;
  2. 部署和发布方式的差异性;

通信可靠性

组件之间的通信方式的不稳定会带来非常多的问题,在一个分布式系统中,多个部署在不同机器上的服务会通过网络进行通信,很多人会认为依靠网络进行通信并不会有太大的问题,不过在分布式系统的几大谬误中,网络是可靠的就是第一大谬误,但是如果像客户端系统只运行在一个进程中,我们往往可以认为在进程内的通信是可靠的,而组件间通信基本上就是靠共享内存和方法调用了。

分布式系统与消息的投递 文章中,作者曾经介绍过分布式系统消息投递的多种语义和可能出现问题的情况。

通信的不可靠会让我们在做一个软件系统时需要考虑非常多的问题:

  1. 如何在网络出现超时是进行重试?
  2. 如何在重试时保证下游业务的幂等性?
  3. 如何在一个涉及链路较长的请求中找到问题?
  4. 如何观测整个系统的运行状况?
  5. 如何在复杂的网络链路中快速定位问题?
  6. 如何避免由于个别服务出错或超时带来的长尾效应?

这些问题不仅要求我们在一个不可靠的通信环境中实现事务,即分布式事务,还需要对系统的指标和网络状态进行监控,对服务(或组件)之间的关系进行梳理。

network

单体的客户端系统由于运行的基本都是单进程,所以基本不会遇到方法调用失败的情况,如果真出现这种问题,那么很多时候意味着整个进程已经崩溃了,通过调用栈大都能直接找到出现问题的组件,一般不需要额外构建复杂的监控和报警系统,但是因为客户端系统运行在用户的终端设备上,所以需要额外的错误上报和日志收集工具。

部署方式

客户端和后端在分发和部署方式上也有着非常大的差别,客户端的应用作为终端应用是要通过渠道分发到使用者的终端设备上的,而后端的服务一般都是部署在公司或者个人管理的物理机或者云服务器上,这也就导致了如下的几个问题:

  1. 如何快速更新或者回滚版本?
  2. 如何分配和使用宿主机的物理资源?
更新技术

对于客户端来说,每次更新需要重新打包、上传并通知用户主动更新是一件非常麻烦的事情,一旦端上出现了非常严重并且紧急的问题,通过这种方式去更新线上应用的周期可能需要以小时或者天来计算,这对于很多公司和业务来讲是不现实的,所以会出现热更新技术动态地下发脚本来解决这个问题。

continuous-delivery

而服务端其实就没有这样的烦恼,因为所有的机器上都是掌握在自己手中的,如何部署、何时部署都能自己控制,就没有类似客户端热更新技术的出现,每次发版只需要打一个镜像然后向编排器提交一个指令就可以部署或者回滚了。

计算资源

客户端应用使用的计算资源就是用户手中的终端设备的物理资源,这些资源在大多数时候我们都可以认为是『无限』的,也就是不需要考虑资源的消耗,但是在绘制一些复杂的图形界面或者运行在一写比较老的设备上时,如何降低资源的消耗并合理的分配 CPU 和内存让我们运行在终端设备上的应用更稳定是客户端研发需要解决的问题。

对于服务端来说,由于服务都是部署在自己掌握的机器上,这些机器需要承载所有客户端发来的流量,计算资源和压力就都集中到了后端的集群中,在这时很多服务就没有办法通过单一的节点承载外部的请求,而是需要部署在多台机器上,那么如何在流量高峰时增加计算资源对集群进行扩容并在低谷时进行收缩,同时在面对海量节点时仍然能够快速扩容和缩容就是一个比较大的问题。

小结

从更高的层次来看,客户端和服务端的应用在本质上来讲都是一个软件系统,虽然它们由于一些原因会有比较大的实现差异,待解决的问题领域也不一样,但是作为软件来说它们总有一些相互可以学习和借鉴的地方。

云原生

云原生(Cloud Native)是一个 2018 年非常热门的概念,对云原生的定义大都来自于 Pivotal 发布的 Migrating to Cloud-Native Application Architectures,书中提到了云原生应用的五个特征:

cloud-native-architecture

正是因为没有明确的定义和解释,不同的人对于云原生的理解可能非常不同,作者对这件事情的理解就是它是一个设计和运行应用的特定方法,使用这种方法需要我们在设计软件系统时考虑到运行当前应用的基础设施,通过在一些通用的抽象和基础设施上构建应用程序,例如 Kubernetes,这样的中间层能够帮助我们降低开发和运维的成本。

2015 年 Google 和 Linux 基金会成立的 CNCF(Cloud Native Computing Foundation)其实就在行业中推动容器编排和面向微服务的开源技术,帮助开发者高效地研发云原生架构的应用程序。

过去的一年时间,作者使用了很多 CNCF 的技术来构建服务端的软件系统,包括 Kubernetes、Helm、gRPC、Linkerd 和 Prometheus,这些 CNCF 基金会孵化的技术有些已经非常成熟,有些还在孵化阶段,不过大多数技术都能够在生产环境中稳定使用,例如整个 2018 年非常热门的容器编排技术 Kubernetes 和 RPC 框架 gRPC,在这里就简单介绍容器编排和服务网格两部分比较有趣的内容。

容器编排

过去的一段时间一直与 Kubernetes 打交道,我们在生产环境中使用了 Kubernetes 作为容器编排技术通过 YAML 对线上的环境进行定义,包括服务的实例数、服务之间的拓扑关系和水平扩展规则。

kubernetes

Kubernetes 模糊了开发和运维之间的边界,帮助我们团队做了非常多的事情,例如:

  1. 发布时提供滚动更新功能,保证线上环境不会因为发布而抖动;
  2. 自动故障重启,当一些 Pod 因为各种原因宕机时会重新创建新的 Kubernetes 对象;
  3. 根据 CPU 的使用量进行水平扩容;

Kubernetes 的使用让研发人员也能通过 YAML 直接修改和定义线上环境,减少了非常多的运维工作,完全不需要关心底层的基础设施和机器的物理资源。

Borg 作为 Kubernetes 的前身,本身就承担了 Google 内部管理海量集群的职责,而 Kubernetes 其实吸取了 Borg 构建过程中的一些经验和教训避免了很多的错误,虽然目前来看 Kubernetes 1.11 版本只支持 5000 个节点的集群,但是对于绝大多数的应用场景都是能够胜任的。

在学习 Kubernetes 的过程中,其实首先需要阅读的就是 Kubernetes 的官方文档,然后看了 Kubernetes in Action,这本书的质量还是非常高的,推荐所有对 Kubernetes 感兴趣的读者都读一下,除此之外还了解了 Kubernetes 一些重要模块的实现原理,写了一些相关的内容:

这些文章大概覆盖了一些 Kubernetes 常见对象的实现原理和顶层的架构设计,然而 Kubernetes 作为一个非常有趣的项目,本身还有非常多值得学习的内容,随后也会去研究其他一些有意思的主题。

社区围绕 Kubernetes 构建了非常多有趣的东西,例如另一个 CNCF 项目 Helm 以及服务网格 Istio,这些项目跟 Kubernetes 的生态结合的非常紧密,为一些中小型的公司提供了比较完善的解决方案。

服务网格

正是因为引入 Kubernetes 为我们的项目节省了大量的人力和物力,所以我们也尝试使用一些其他的开源项目来解决微服务架构带来的服务治理的需求,跟身边的一些朋友交流之后,发现了服务网格这个概念。

service-mesh

服务网格在作者的理解中它就是服务之间的一个中间层,我们将微服务中服务监控打点、路由、限流和熔断等通用能力抽象出来,统一通过这个中间层负责,这就能够减少每一个微服务内部的逻辑,让服务专注于处理内部的业务逻辑,更好更快地为业务提供支持。它的主要作用就是作为基础设施层处理服务之间的通信,集群接入服务网格往往都不需要对原有的代码进行修改,对代码没有侵入性,同时也能够支持多种编程语言,降低了使用的技术门槛。

在今年年中的几个月时间,我们在生产环境上线了 Linkerd,主要承载的功能就是服务的发现和路由,在线上的集群中,我们使用 Linkerd-to-Linkerd 的模式进行部署,也就是所有的流量都会由 Linkerd 进行转发,转发的规则都配置在 Linkerd 的 dtab 中,但是在实际使用 Linkerd 的过程中发现了非常多的问题:

  1. 官方文档其实并不够详细;
    • 对于一个不了解 Linkerd 配置的人来说,尝试写一个可用的配置非常的困难,需要在环境中不断地调试并分析一些 dtab 中一些内置方法和函数;
  2. 默认的一些熔断规则对于小规模的集群不是特别友好;
    • 如果一个微服务只运行了一个实例,那么当前实例如果连续出现 5 次错误后在默认的配置下会被标记成 dead,Linkerd 不会再向该实例转发任何请求,同时如果集群中没有第二个实例,那么就会导致请求无法处理;
    • 如果当前实例在成功启动之前就被 Linkerd 发现并路由请求,那么一旦出现 5 个连续的错误请求,当前节点就会被标记为 dead,所以一定要注意 Probe 的配置;
    • 对于这两个问题,我们找到的最终解决办法就是关闭 Linkerd 的断路和熔断功能,然而由于文档的不够详细,我花了很长时间才搞清楚应该如何配置来才能关闭 Linkerd 的断路和熔断;
  3. 在生产环境中使用 gRPC Stream 会导致内存泄漏;

我们使用的 Linkerd 1.4.6 版本有非常多需要解决的问题,不过从官方 1.5.1 版本的 CHANGELOG 中我们发现它们已经对 HTTP/2.0 内存泄漏的问题进行了修复,但是这个时候我们已经将它从生产环境下线了。

  • HTTP/2
    • Fixes an HTTP/2 issue that causes Linkerd to stop processing incoming frames on an HTTP/2 connection after Linkerd sends a RST_STREAM frame to its remote peer. This was causing gRPC clients to experience timeout errors intermittently because connections between Linkerd and its remote peers weren’t being closed properly.
    • Sets the maxConcurrentStreamsPerConnection config value for the h2 router to 1000 by default to prevent Linkerd from running out of memory when HTTP/2 clients leak connection streams.

现在的 Linkerd 在作者心目中也不是特别的成熟,但是不可否认的是这是一个非常有想法的项目,确实能够帮助我们解决服务发现和路由的问题,也有一些更高级的限流模块,不过 Google 带头搞出来的 Istio 其实更被大家关注,由于它引入了控制面板的概念,所以我们一般会认为它是下一代的服务网格。

istio

虽然 Istio 背后有 Google 等一些大公司参与,但是目前的版本无法在生产环境中使用,问题也非常的多,所以如果只是在测试环境中尝试一下还是没什么问题的。

从 Kubernetes 解决了容器编排,提供了云原生的操作系统之后,建立在服务网格之上的 Istio 也尝试去解决微服务的治理问题,这种通过中间层来解决问题的方式其实非常有生命力,作者觉得在 2019 年服务网格技术也会日益成熟能帮帮助非常多的团队降低微服务的治理成本。

编程语言

今年使用的编程语言主要是两个,上半年使用 Elixir 比较多,下半年就都在使用 Golang 了,在这里作者可以简单介绍一下这两门语言的特点以及作者对这些语言的体会和理解。

Elixir

Elixir 是一个基于 Erlang 虚拟机的函数式编程语言,它能够非常好的支持分布式、高容错和高实时性要求的软件系统,但是它的特性和背景就决定了这门编程语言不会特别的热门。

elixir-programming-language

我们最开始在生产环境中大规模使用 Elixir 来构建我们的 Web 服务,但是最后发现很多场景并不是非要使用 Elixir,值得肯定的是作为运行在 Erlang 虚拟机上的编程语言,它的并发模型能够非常好地支持长连接。

每一个 Elixir 的进程其实都非常的轻量级,开销非常小,我们可以在一个普通的机器上创建几十万个 Elixir 进程来处理外部的请求,能够提供比较好的并发性能,这与 Golang 中的 Goroutine 非常相似;除了轻量级的进程之外,Elixir 基于 Erlang 的虚拟机还能提供热更新的功能,保证线上正在运行的进程不会因为更新而停止工作,这也是使用 Erlang 带来的特性 - 『永不停机』。

不过虽然从语言层面 Elixir 确实很优秀,并且有很多非常好用的特性:管道、轻量级进程和模式匹配,但是它的背景相比目前比较热门的语言没有那么好,整个生态环境也比较差,gRPC 和 protobuf 这种级别的框架目前都没有官方提供的库,其他的框架就更不用多说了,所以在微服务架构中使用 Elixir 并不是一个比较好的选择,目前来看应用场景主要也是即时通信、长连接推送等等;不过哪怕真正遇到了这门语言的适合的场景,还是需要考虑招聘的难度社区的状况再慎重做决定。

Golang

相比与非常小众的 Elixir 语言,Golang 的社区支持就很好,一方面却是因为 Google 作为 Golang 的亲爹在大力推广,另一方面是因为这两年 Golang 语言日渐成熟,性能和可维护性都非常优异。

golang

Docker、Kubernetes、Prometheus 和 etcd 等热门的开源项目都是用 Golang 作为开发语言,这也为 Golang 的社区带来了非常正面的作用。

Golang 语言的学习曲线非常平缓,一个有其他语言经验的工程师从学习这门语言到开始写可能也只需要几天的时间,语言原生支持的 Goroutine 协程和 Channel 让并发编程变得非常的简单,虽然在错误处理上经常被人诟病,但是依然阻挡不了工程师对这门语言的追捧。

虽然 Golang 的代码一般都极其冗长,代码中充斥着对错误的判断,每当需要处理错误的时候都很头疼,但是它的生态环境却非常的好,生态中该有的框架基本上都非常完善,除了这个优点之外,编译非常方便、工具链也相对比较成熟也是很重要的特点,未来的几年时间作者也会把 Golang 当做工作上主要使用的编程语言。

阅读书籍

2018 年算下来一共读了 13 本书,大多数书籍的质量其实都非常一般,在技术上感觉没有让人特别印象深刻的书,觉得写得比较好的就是上面提到过的《Kubernetes in Action》,想要学习 Kubernetes 的读者一定不要错过。

  1. 亿级流量网站架构核心技术
  2. 微观经济学
  3. 精通比特币
  4. Rework
  5. OKR 工作法
  6. 硝烟中的 Scrum 和 XP
  7. Go 语言实战
  8. etcd 技术内幕
  9. Go 编程语言
  10. Kubernetes in Action
  11. 好好说话
  12. 小岛经济学
  13. Elixir 程序设计

业余时间也读了一些经济学相关的教材,了解经济学的知识对作者的影响还是比较大的,虽然很多经济学的结论都能够根据直觉推测出来,但是有一些现象和规则学习起来还是非常有趣。

明年可能还是会花一些时间来阅读经济学相关的书籍,同时也希望减少技术书籍的阅读量,目前市面上真正值得多次阅读的技术书籍真的非常的少,想要静下心来好好学习一下更抽象的知识,帮助自己在软件设计上的理解更进一步。

社交网络

今年一年时间总共写了 17 篇博客,上半年的几个月事情比较多,所以一直没有写博客,下半年写的大多数文章都比较满意,GraphQL 在微服务架构中的实践 记录了服务架构的迁移和一些对 GraphQL 的思考和实践,剩下的两篇与分布式系统理论相关的文章作者也比较满意。

  1. 分布式事务的实现原理
  2. 分布式系统与消息的投递

博客今年的访问量也接近了 100w,大多数的流量都来自于搜索引擎,在这里也非常感谢各位读者的支持;因为发现经常有人要在公众号里转载博客的文章,之前也遇到过很多人在微信公众号里声明原创盗用我的文章,所以年底的时候开了一个微信公众号,公众号中的大多数的内容应该会跟博客同步,主要会涉及服务端技术,同时包含以下三个主题:

  1. 云原生技术
  2. 微服务架构
  3. 编程语言

wechat-account-qrcode

想要订阅博客的读者,既可以通过 RSS 的方式,也欢迎大家通过微信公众号的方式进行订阅。

总结

每当到了年末对这一年进行总结的时候,都会发现过去一年做了很多事情,不过发现自己需要学习的知识更多,往往都会觉得时间不够用,所以只能尽量合理分配自己的时间,减少不必要的投入。

今年最大的成长就是对微服务和云原生架构的理解有着比较大的提升,在服务端这个领域打下了比较扎实的基础,经过对过去的反思和总结,对未来一段时间的技术栈也有了比较清晰的规划,希望明年能够在技术方面获得更多的成长,也欢迎各位读者关注作者的 GitHubWeibo

相关文章

关于图片和转载

知识共享许可协议
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。

微信公众号

wechat-account-qrcode

关于评论和留言

如果对本文 2018 年总结 · 初窥门径 的内容有疑问,请在下面的评论系统中留言,谢谢。

原文链接:2018 年总结 · 初窥门径 · 面向信仰编程

Follow: Draveness · GitHub

Draveness

Go / Rails / Rust

Beijing, China draveness.me