认识到微服务开发的复杂性

  1. 本地开发要有一个好的开发机器,糟糕的开发机器将会导致糟糕的开发实践
  2. 确保所有服务都使用构建工具,能在一台新机器上构建整个应用程序,而不需要进行太多配置,让开发人员能轻松地在本地运行应用程序的各个部分
  3. 使用Kubernetes,利用Telepresence以便轻松调试 Kubernetes 集群中的应用程序

如果对微服务开发的复杂性缺乏理解,那么团队速度将会随着时间的推移而下降

及时将库和工具更新到最新版

所有服务的依赖项版本保持同步,为依赖升级创建技术债务项,并应作为会议的一部分加以讨论,并定期予以解决,将所有依赖项更新到最新版本

不止包括升级依赖包的版本,还包括架构修正,如当引入一个新工具时,可能可以替换掉原先多个工具,有助于降低复杂性

几年前,许多团队开始将 Spring Cloud Netflix OSS 项目用于微服务。他们使用像 Kubernetes 这样的容器编排工具,但是因为是从 Netflix OSS 开始的,所以他们没有使用 Kubernetes 提供的所有功能。当 Kubernetes 内置了服务发现时,他们仍然使用 Eureka 作为服务发现

避免使用共享服务来做本地开发

举例共享数据库

  1. 一个开发人员可以删除其他开发人员为他们工作编写的数据
  2. 开发人员害怕实验,因为他们的工作会影响其他团队成员
  3. 很难单独测试更改。你的集成测试将变得不可靠,从而进一步降低开发速度
  4. 容易出现不一致和不可预测的状态,开发人员希望在表是空的时候测试边缘情况,但其他开发人员需要一个表来记录
  5. 只有共享数据库拥有系统工作所需的所有数据,团队成员失去了更改的可追溯性
  6. 如果未连接到网络,就很难开展工作

它也可以是消息队列、集中缓存(如 Redis)或任何其他可以发生改变的服务

代码托管平台的可见性

在托管平台按产品、服务对微服务进行分组,方便了解代码库结构

服务有明确定义

避免拆分过多服务,会导致管理成本直线上升,

应用单一责任原则(Single Responsibility Principle)来了解微服务是否变得过大,做的事情是否过多

任何服务都不应该直接与其他服务的数据库通信

服务通信是微服务系统性能低下的首要原因: 如果两条信息相互依赖,那么它们应该属于同一个服务,服务的自然边界应该是其数据的自然边界

明确代码重用策略

对处理相同问题的代码,使用包封装,发布在包管理平台,并通过构建工具来让依赖的微服务及时更新

没有适合的工具和自动化的情况下,使用微服务会导致灾难

多语言编程设计

如果你的开发人员还不够成熟的话,那么无论你使用什么编程语言,你开发的都将是糟糕的产品

一个组织可以指定2、3个语言列表,并列出语言的优势

在选择一门语言前,应该考虑以下一些问题:

  1. 找到成熟的开发人员有多容易
  2. 重新培训开发人员掌握新技术有多容易?我们发现 Java 开发人员可以相对容易地学习 Golang。
  3. 代码的可读、可维护性
  4. 就工具和库的方面而言,生态系统有多成熟?

不仅仅局限于编程语言,也适用于数据库领域, 要始终考虑使用多种技术的维护和操作方面

人员的依赖性

大多数团队专注于他们的特定服务,因此他们并不了解完整的生态系统

确保所有团队都有一个架构团队的代表,使每个团队与整个架构的路线图和目标保持一致(人月神话中有提过,外科手术团队,只需要协调各团队中的架构师)

维护文档

需要维护的文档:

  1. 设计文档
  2. C4 模型中的上下文和容器图
  3. 以架构决策记录的形式跟踪关键架构决策
  4. 开发人员入门指南

在Gitlab中维护所有的文档

功能不要超过平台成熟度

微服务要比传统的单体式应用更为复杂

需要考虑分布式跟踪、可观察性、混沌测试、函数调用与网络调用、服务间通信的安全服务、可调试性等等。这需要在构建正确的平台和工具团队方面付出认真的努力和投资

自动化测试

微服务架构为测试地点和测试方式提供了更多选择

微服务测试的金字塔

Resize icon

交流的缺乏导致了争辩、沮丧和群体猜忌。很快,部落开始分裂——大家选择了孤立,而不是互相争吵

缺少相互沟通会导致进度灾难、功能的不合理和系统缺陷

对程序功能的修改要从系统角度来考虑和衡量,及时做公开,告知

项目工作手册

是项目文档的集合,包括目的、外部规格说明、接口说明、 技术标准、内部说明和管理备忘录,帮助后来者了解产品设计

事先将项目工作手册设计好,能保证文档的结构本身是规范的

控制信息发布:确保信息能到达所有需要它的人的手中,对所有的备忘录编号或者树状的索引结构

记录修订日期记录和标记变更标识条

编程人员仅了 解自己负责的部分,而不是整个系统的开发细节时,工作效率最高,先决条件是 精确和完整地定义所有接口

大型编程项目的组织架构

团队组织的目的是减少不必要交流和合作的数量

减少交流的方法是人力划分和限定职责范围

树状组织架构:

  1. 任务(a mission)
  2. 产品负责人(a producer)
  3. 技术主管和结构师(a technical director or architect)
  4. 进度(a schedule)
  5. 人力的划分(a division of labor)
  6. 各部分之间的接口定义(interface definitions among the parts)

产品负责人:

  1. 主要的工作是与团队外部,向上和水平地沟通,建立团队内部 的沟通和报告方式
  2. 组建团队,划分工作及制订进度表,要求资源,确保进度目标的实现,根据环境的变化调整资源和团队的构架

技术主管:

  1. 提供整个设计的一致性和概念完整性,控制系统的复杂程度
  2. 对设计进行构思,确定内部结构。
  3. 提供技术问题的解决方案,或者根据需要调整系统设计。
  4. 他的沟通交流在团队中是首要的。他的工作几乎完全是技术性的

产品负责人和技术主管的组合方式

产品负责人和技术主管是同一个人:

  1. 只适用于几个人的小团队开发
  2. 同时具有管理技能和技术技能的人很难找到,大型项目中,每个角色都必须全职工作无法保证还能抽空做别的

产品负责人作为总指挥,技术主管充当其左右手:

  1. 实现难度在建立技术决策上的权威
  2. 必须对技术主管的技术才能表现出尊重,支持他的技术决定
  3. 在主要的技术问题出现之前,私下讨论它们,达到在基本的技术理论上具有相似观点
  4. 一些技巧 通过一些微妙状态特征暗示来(如,办公室的大小、地毯、装修、复印机等等)体现技术主管的威信
  5. 这种组合可以使工作很有效 项目经理可以使用并不很擅长管理的技术天才来完成工作

技术主管作为总指挥,产品负责人充当其左右手:

  1. 小型的团队是最好的选择
  2. 参考 外科手术队伍

对于真正大型项目中的一些开发队伍,产品负责人作为管理者是更合适的安排

之前的问题

在做反向计算时,主要涉及到区域数据的比较,通过查出所有子集再循环剔除,

数据量、大运算慢,频繁根据region_start_id查数据库,返回结果因为展开到商品,对反向涉及到的商品都会带上庞大的区域数据

插曲

深拷贝优化: 展开的授权数据,要从原始授权继承区域,需要深拷贝,直接深拷贝Object太慢,通过immutable做了优化,但涉及到的商品区域数据最后返回时要toJS,速度较慢

region缓存: region_start_id查数据库,将查询id数组长为1的做了lru,目的是缓存区域层级高、重复多的数据,在原始授权数据很多时有较大的速度提升

改造

根本问题

计算区域时,无法通过regionID直接知道层级,并做层级运算(会有子父级数据的剔除和补充)

实现方案

受树形数据加编号启发,通过一个层级key来表示regionID的层级信息,直接对key做运算

将原先展开循环的层级运算,变成字符串比较

缓存:

  • cacheRegionKeyObj key对应region记录
  • cacheRegionIdObj regionId对应key
  • cacheRegionKeysRank key对应子集keys,同层级数据groupby加列转行

在做层级运算时,通过字符串运算即可得到上下级key再到缓存里换出子集keys,再对展开结果做排除即可完成层级运算

优势:

  1. 深拷贝只有层级key数组
  2. 层级运算不涉及数据库
  3. 返回结果只有展开的层级key,前端通过缓存cacheRegionKeyObj换算即可

总结

  1. 深拷贝处理可以尝试immutable
  2. 对于层级数据的上下计算,考虑转成平铺的形式,同时保留层级信息

文档化的规格说明——手册

  • 产品的外部规格说明,它描述和规定了用户所见的每一个细节,也是结构师主要的工作产物

随着系统的使用和反馈,规格说明中难以使用的地方也不断地被修改,对实现人员而言,修改需要阶段化,有进度时间表和版本

实现人员的设计和创造不应该被手册限制,手册要避免描述内部实现

体系结构设计人员必须为自己描述的特性准备一种实现方法,但是他不应该试图支配具体的实现过程。

规格说明的风格

  • 清晰、完整和准确,每条说明都必须重复所有的基本要素,所有文字都要相互一致
  • 由一两个人将结论转换成书面规格说明,保持文字和产品之间的一致性

形式化定义

  • 使用形式化标记方法达到所定义需要的精确程度

优缺点 精确完整,差异得更加明显,可以更快地完成。缺点是不易理解 需要记叙性文字的辅助,才能使内容易于领会和讲授

决不要携带两个时钟出海,带一个或三个 如果同时使用形式化和记叙性定义,则必须以一种作为标准,另一种作为 辅助描述,并照此明确地进行划分

形式化定义仅仅用于外部功能说明它们是什么 有时会通过一段实现该功能的程序来定义,但只是说明功能,不能限定体系结构

设计实现可以作为一种形式化定义的方法

优点所有问题可以通过试验清晰地得到答案,从来不需要争辩和商讨,回答是快捷迅速的
缺点 可能过度地规定了外部功能。有时会给出未在计划中的意外答案;特别容易引起混淆,当实现充当标准时,还必须防止对实现的任何修改

周会和大会

周会

每周半天的会议,所有的结构师,加上硬件和软件实现人员代表和市场计划人员参与,由首席系统结构师主持

在会议之前分发建议、会议内容,解决方案会被传递给结构师并做记录,当决策没有达成共识时由首席结构师来决定

周会优势: 相同小组每周交流,对相关内容比较了解,不需要安排额外时间培训;深刻理解所面对的问题,并且与产品密切相关;正式的书面建议集中了注意力,强制了决策的制订,避免了会议草稿纪要方式的不一致;清晰地授予首席结构师决策的权力,避免了妥协和拖延

大会

随着时间的推移,一些决定、一些小事情并没有被某个参与者真正地 接受。对于这些问题,有时周例会没有重新考虑,慢慢地, 很多小要求、公开问题或者不愉快会堆积起来。通过年度大会解决这些堆积起来的问题

大多数条目的规模很小,每个不同的声音都有机会得到表达。然后会制订出决策,每个人都在倾听、参与,每个人对复杂约束和决策之间的相互关系有了更透彻的理解,使决策更容易被接受

分布式任务调度

原因

  1. 存在一些耗时较长的运算,需要从web服务中移出,比如缓存计算、报表导出
  2. 管控力度:原先的运算集群采用mq通信方式,master对worker的管控力度不够,会出现一个任务重复失败拖垮集群的现象
  3. worker之前不能区分,原因还是没有直接由master派发任务,也无法做跨语言调度

目标

master

  1. 稳定、高可用,不执行具体运算
  2. 接收、记录待执行的任务,按任务优先级派发,记录任务执行结果,支持超时、重试、失败状态
  3. 能管控worker生命,知道它的运行状态和任务进度,根据worker状态派发任务
  4. 向业务方通知运行结果

worker

  1. 启动时向master注册自己的环境和算力
  2. 定时汇报自己的运行状态
  3. 支持多语言

实现

通信

基于grpc的stream方式通信,worker端目前采用重启方式重连,master端可通过end事件清除失效worker

1
2
3
4
5
6
7
service Greeter {
rpc Command (stream body) returns (stream body) {}
}
message body {
string body = 1;
}

master和worker的结构

  • 基于egg,风格结构与后端业务框架基本相同
  • 主要分为job和life_cycle,分管任务和生命周期

Resize icon

master
  • 通过mq记录任务,并定期读取任务到缓存,按优先级排序,
  • 处理worker连接,定期检查连接情况
  • 派发任务,只在新worker连接和任务完成时执行,定期检查任务执行状态
worker
  • 接受master派发的任务调用本地service/method执行,在表中记录执行结果
  • 定期自检健康发送心跳给master

使用

任务代码的编写

  • 在worker端实现
  • 代码位置在service文件夹
1
2
3
4
5
6
// app/service/authorization.js
const Service = require('@fgrid/egg').Service;
class AuthorizationService extends Service {
async deleteAuthorization(){}
}

代码结构与业务框架一致,业务数据通过rpc获取,原则上不直连业务数据库

业务服务调用任务

  • sdk直接写在fgrid-middleware

调用方式和rpc调用类似

1
2
// 通过mifStart来调用分布式任务,后面接serviceName.methodName
this.ctx.mifStart.authorization.deleteAuthorization()

计划实现功能

  1. master高可用
  2. worker多语言版和算力等属性
  3. 定时任务

结构师的交互准则和机制

成本

  • 结构师能在设计早期从开发那得到成本
  • 开发可以增高或降低估的计成本,来反映对设计的好恶

尽早交流和持续的与开发沟通能使结构师有较好的成本意识,以及使开发人员获得对设计的信心,并且不会混淆各自的责任分工

成本过高时的处理
  • 削减设计
  • 跟开发建议成本更低的实现方法(挑战估算的结果)

第二个是结构师固有的主观感性反应,是在向开发人员的做事方式提出挑战

  1. 开发承担创造性和发明性的实现责任,结构师只能建议,而不能支配
  2. 为所指定的说明提供实现方法,并对改方法保持低调和平静,并接受其他任何能达到目标的方法
  3. 准备放弃坚持所作的改进建议

一般开发人员会反对体系结构上的修改建议,通常他是对的——当正在实现产品时, 某些特性的修改会造成意料不到的成本开销

自律——开发第二个系统所带来的后果

  • 第二个系统是设计师们所设计的最危险的系统

开发第一个系统时,结构师倾向于精炼和简洁。知道自己对正在进行的任务不够了解,所以会谨慎仔细地工作,修饰功能和想法被小心谨慎地推迟,导致过分地设计第二个系统

着手第三、第四个系统时,通过之前的经验得到此类系统通用特性的判断,而且系统之间的差异会帮助他识别出经验中不够通用的部分

开发第二个系统的后果(second-system effect)与纯粹的功能修饰和增强明显不同,由于技术、设计的变化,可能会对已经落后技术进行细化、精炼

  1. 关注系统的特殊危险,避免功能上的修饰;根据系统基本理念及目的,舍弃一些功能
  2. 每个小功能分配一个值: 每次改进,有一个数据指标,比如功能 x 不超 过 m 字节的内存和 n 微秒。能作为决策的向导,在物理实现期间充当指南和对所有人的警示.例如facebook的Messenger,为每个功能设置了代码大小预算,要求工程师负责遵守预算约束,作为功能接受标准的一部分(构建一个系统来计算每个功能的二进制大小权重)

项目经理如何避免画蛇添足(second-system effect)拥有两个系统以上经验的结构师的决定。同时,保持对特殊诱惑的警觉,不断提出正确的问题,确保原则上的概念和目标在详细设计中被完整实现

概念一致性

  • 在系统设计中,概念完整性应该是最重要的考虑因素

大多数统体现出的概念差异和不一致性的原因: 并不是因为它由不同的设计师们开发,是由于设计被分成了由若干人完成的若干任务

概念的完整性要求: 系统只反映唯一的设计理念,用户所见的技术说明来自少数人的思想

为了连贯的设计思路,宁可省略一些不规则的特性和改进,也不提倡独立和无法整合的系统,哪怕包含着许多很好的设计

获得概念的完整性

  • 系统的实现目标:功能性,易用性,功能与理解上复杂程度的比值才是系统设计的最终测试标准
  • 简洁和直白来自概念的完整性
  • 易用性需要设计的一致性和概念上的完整性

每个部分必须反映相同的原理、原则和一致的折衷机制。在语法上, 每个部分应使用相同的技巧;在语义上,应具有同样的相似性

贵族专制统治和民主政治

系统的概念完整性

  • 要求设计必须由一个人,或者非常少数互有默契的人员来实现
  • 决定了使用的容易程度

不能与系统基本概念进行整合的想法和特色,最好放到一边不予考虑。如果出现了很多非常重要但不兼容的构想,就应该抛弃原来的设计,对不同基本概念进行合并,在合并后的系统上重新开始

系统的结构师(产品)

  • 用户的代理人,支持用户的真正利益,而不是维护销售人员所鼓吹的利益
  • 易用性很大程度上依赖结构师

结构师工作产物的生命周期比那些实现人员的产物要长,并且结构师一直处在解决用户问题,实现用户利益的核心地位。如果要得到系统概念上的完整性,那么必须控制这些概念

实现人员

  • 产品的成本性能比很大程度上依靠实现人员

在给定体系结构下的设计实现,同样需要创造性、同样新的思路和卓越的才华

纪律和规则

  • 最差的建筑往往是那些预算远远超过起始目标的项目
  • 外部的体系结构规定实际上是增强,而不是限制实现小组的创造性
  • 创造性活动会因为规范化而得到增强

一旦将注意力集中在没有人解决过的问题上,创意就开始奔涌而出。在毫无限制的实现小组中,在进行结构上的决策时,会出现大量的想法和争议,对具体实现的关注反而会比较少

在等待时,实现人员应该做什么

  • 不能让实现队伍来负责产品设计: 进度推迟,质量更加低劣: 概念完整性的缺乏导致系统开发和 修改上要付出更昂贵的代价

工作的垂直划分(工作被划分成体系结构、设计实现和物理实现)从根本上大大减少了劳动量,结果是使交流彻底地简化,概念完整性得到大幅提高

效率高和效率低的实施者之间具体差别非常大,经常达到了数量级的水平

需要协作沟通的人员的数量影响着开发成本,因为成本的主要组成部分是相互的沟通和交流,以及更正沟通不当所引起的不良结果(系统调试)

队伍以类似外科手术的方式组建,而并非一拥而上由一个人来进行问题的分解,其他人 给予他所需要的支持,以提高效率和生产力

成员分类

首席程序员(外科医生)

对整个业务有完整清楚的理解,负责整体的设计、规范、框架的编码和功能划分,对复杂的模块亲自编码

一个人开发应该很难存在,还有很多scut work需要普通程序员完成,
定义功能和性能技术说明书,设计程序,编制源代码、测试以及书写技术文档。

首席程序员需要极高的天分、丰富的经验和应用数学、业务数据处理或者其他方面的大量系统知识和应用知识。

副手

首席程序员的后备,充当外科医生的保险机制,只是经验较少。

帮助首席程序员思考、讨论和评估,能代表自己的小组与其他团队讨论有关功能和接口问题,了解所有的代码

普通程序员

根据首席程序员的任务划分,按既定规则开发指定的功能模块,提供规范的输入输出参数

管理员

控制财务、人员、工作地点和办公设备,负责与组织中其他管理机构的交流,可以为多个团队服务

编辑

很难存在工作枯燥
提供各种参考信息和书目,对许多个版本进行维护,并监督文档的生成机制
框架、整体的文档由首席程序员给出,各个接口模块由响应的开发给出

两个文秘(不明确)

管理员和编辑每个人需要一个文秘。
管理员的文秘负责非产品文件和使项目协作一致。

程序职员(不明确)

他负责维护编程产品数据库中所有的团队技术记录。该职员接受文秘性质的培训,承担机器码文件和可读文件的相关管理责任。
他还负责记录和更新所有小组成员的工作拷贝,并使用自己的交互式工具来控制产品逐步增长的完整性和有效性。

工具维护人员(运维,组件组)

保证所有基本服务的可靠性,以及承担团队成员所需要的特殊工具的构建、维护和升级责任,常常要开发一些实用程序,编制具有目录的函数库以及宏库。

测试人员

负责计划测试的步骤和为单元测试搭建测试平台

语言专家

寻找一种简洁、有效的使用语言的方法来解决复杂、晦涩或者是棘手的问题,比如DBA
他通常需要对技术进行一些研究(2-3天)。通常一个语言专家可以为2-3个首席程序员服务。

如何运作

系统是一个人或者最多两个人思考的产物,因此客观上达到了概念的一致性

外科手术团队中,不存在利益的差别,观点的不一致由外科医生单方面来统一

让 200 人去解决问题,而仅仅需要 协调 20 个人,即那些“外科医生”的思路

乐观主义

创造性活动分为三个阶段: 构思、实现和交流

只有在实现的过程中,才能发现构思的不完整性和不一致性

由于编程的高可控性,我们会认为实现很顺利,因此造成了乐观主义的弥漫,而构思是有缺陷的,因此总会有bug

大型的编程工作,或多或少包含了很多任务,某些任务间还具有前后的次序,从而一切正常的概率变得非常小,甚至接近于无

人月

用人月作为衡量一项工作的规模是一个危险和带有欺骗性的神话

编程中近乎不可能存在人员数量和时间可以相互替换

  • 任务次序上的限制不能分解

Resize icon

  • 子任务之间需要相互沟通和交流的任务,沟通所增加的负担由两个部分组成,培训和相互交流,培训随人员的数量呈线性变化,相互之间交流按照 n(n-1)/2 递增

Resize icon

所增加的用于沟通的工作量可能会完全抵消对原有任务分解所产生的作用,甚至会导致延长

系统测试

项目排期占比

  • 1/3 计划
  • 1/6 编码
  • 1/4 构件测试和早期系统测试
  • 1/4 系统测试,所有的构件已完成

很少项目允许为测试分配一半的时间,但大多数项目的测试实际上是花费了进度中一半的时间

不为系统测试安排足够的时间简直就是一场灾难因为延迟会发生在项目快完成的时候,坏消息没有任何预兆,很晚才出现,此时的延迟具有不寻常的、严重的财务和心理上的反应,甚至会导致活动延误,付出相当高的商业代价,远远高于其他开销。因此,在早期进度策划时,允许充分的系统测试时间是非常重要的

空泛的估算

现状: 受限于顾客要求的紧迫程度,非阶段化方法的采用,少得可怜的数据支持,加上完全借助软件经理的直觉

解决方案:

  • 开发并推行生产率图表、缺陷率、估算规则等
  • 在基于可靠基础的估算出现之前,项目经理需要挺直腰杆,坚持他们的估计, 确信自己的经验和直觉总比从期望派生出的结果要强得多

重复产生的进度灾难

避免小的偏差(Take no small slips) 在新的进度安排中分配充分的时间,以确保工作能仔细、彻底地完成,从而无需重新确定时间进度表

重复生成的工作量 不论在多短的时间内,聘请到多么能干的n个新员工,都需要接受一位有经验的职员的培训。如果培训需要一个月的时间,那么n+1个人月会投入到原有进度安排以外的工作中。原先划分的任务要重新拆分某些已经完成的工作必定会丢失,系统测试必须被延长

向进度落后的项目中增加人手,只会使进度更加落后。(Adding manpower to a late software project makes it later)

项目的时间估算 任务顺序和子任务的数量产生的人员数量,推算出进度时间表,该表安排的人员较少,花费的时间较长(唯一的风险是产品可能会过时)。分派较多的人手,计划较短的时间,将无法得到可行的进度表

在众多软件项目中,缺乏合理的时间进度是造成项目滞后的最主要原因,它比其他所有因素加起来的影响还要大

一个问题单独看起来很没有困难,但是当它们相互纠缠和累积在一起的时候,团队的行动就会变得越来越慢

Resize icon

职业的苦恼

  1. 学习编程的最困难部分,是将做事的方式往追求完美的方向调整
  2. 由他人来设定目标,供给资源,提供信息,很少能控制工作环境和目标。个人的权威和他所承担的责任是不相配的
  3. 依赖他人的程序是一件非常痛苦的事情。往往这些程序设计得并不合理,实现拙劣,发布不完整(没有源代码或测试用例), 或者文档记录得很糟
  4. 寻找琐碎的 bug,调试和查错往往是线性收敛的,甚至具有二次方的复杂度。寻找最后一个错误比第一个错误将花费更多的时间
  5. 产品陈旧问题,实现落后与否的判断应根据其它已有的系统,而不是未实现的概念。在现有的时间和有效的资源范围内,寻找解决实际问题的切实可行方案