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

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

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

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

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

规格说明的风格

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

形式化定义

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

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

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

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

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

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

周会和大会

周会

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

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

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

大会

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

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

原因

  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. 产品陈旧问题,实现落后与否的判断应根据其它已有的系统,而不是未实现的概念。在现有的时间和有效的资源范围内,寻找解决实际问题的切实可行方案

rpc

使用rpc的目的: 让远程调用服务就像调本地服务一样简单

比http的优势:协议传输效率较低

sofarpc与grpc

性能: sofarpc好,grpc性能一般(单线程 30kqps,小于8k包时)
易用性: Http2出现较晚,迁移转换成本高,sofarpc支持的协议多
可行性: sofarpc的注册中心目前还没有正式支持SOFAMesh

服务发现

zookeeper

  1. 健康检查依赖TCP长链接活性探测不准确
  2. 需要在服务中集成sdk
  3. 更复杂的服务治理

参考

sofa-rpc-node: 目前只支持zk

k8s service

服务注册和发现,转发,负载均衡能力

缺点: 同一个应用的不同的实例提供了不同的服务会出错,只能在同集群内使用

istio

弥补k8s service的不足
可配置的服务发现(从k8s获取服务注册表),由Sidecar保持长连接,转发,负载并提供熔断、限流降级、调用链治理等能力

SOFAMesh

基于istio,还在测试中

Resize icon

  • 采用 Golang 编写的 MOSN 取代 Envoy(sidecar)
  • 合并 Mixer 到数据平面以解决性能瓶颈
  • 增强 Pilot 以实现更灵活的服务发现机制(主动告诉对应的 Sidecar,它需要发布哪些服务,主动告诉对应的 Sidecar,它需要订阅哪些服务),增加数据同步模块,以实现多个服务注册中心之间的数据交换

事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务支持是在引擎层实现的

ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)

InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的

隔离性与隔离级别

读未提交(read uncommitted) 一个事务还没提交时,它做的变更就能被别的事务看到

读提交(read committed)一个事务提交之后,它做的变更才会被其他事务看到,查询只承认在语句启动前就已经提交完成的数据

可重复读(repeatable read) 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,未提交变更对其他事务也是不可见的,查询只承认在事务启动前就已经提交完成的数据

串行化(serializable )同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行

长事务和回滚日志

回滚日志: 在MySQL中,每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值

在查询这条记录的时候,不同时刻启动的事务会有不同的read-view,要获取旧的视图,必须将当前值依次执行回滚操作得到

当没有事务再需要用到这些回滚日志时,就是当系统里没有比这个回滚日志更早的read-view的时候,回滚日志会被删除

长事务的影响系统里面会存在很老的事务视图,在这个事务提交之前数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间

set autocommit=0 这个命令会将这个线程的自动提交关掉,只执行一个select语句,这个事务就启动了,而且并不会自动提交。持续存在直到你主动执行commit 或 rollback 语句,或者断开连接

commit work and chain 提交事务并自动启动下一个事务,省去了再次执行begin语句的开销

查找持续时间超过60s的事务

1
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间

监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill

设置innodb_undo_tablespaces(默认值为0,表示不独立设置undo的tablespace,默认记录到ibdata系统表空间中,否则,则在undo目录下创建这么多个undo文件)成2(或更大的值),如果真的出现大事务导致回滚段过大方便清理

可重复读和MVCC

MVCC 数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id,旧版本需要根据当前版本和undo log计算出来

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句(第一个快照读语句),事务才真正启动。如果想要马上启动一个事务,可以使用start transaction with consistent snapshot1

InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交,数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view),只有版本已提交,而且是在视图创建前提交的才可见

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read) set k=k+1会拿最新的值做更新,更新后的trx_id是自己的更新,可以直接使用,如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待

redo log

InnoDB引擎实现

在MySQL里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高

WAL技术,WAL的全称是Write-Ahead Logging,它的关键点就是先写日志,再写磁盘 顺序i/o

当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面

redo log是物理日志,记录的是“在某个数据页上做了什么修改”

InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写。write pos是当前记录的位置,checkpoint是当前要擦除的位置也是往后推移并且循环的,擦除记录前要把记录更新到数据文件

有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

bin log

Server层日志

binlog存在的原因 redolog只有InnoDB有,别的引擎没有,edolog是循环写的,不持久保存,binlog的“归档”这个功能,redolog是不具备的。

binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”,binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

  • statement:基于SQL语句的模式,某些语句中含有一些函数,例如 UUID NOW 等在复制过程可能导致数据不一致甚至出错。
  • row:基于行的模式,记录的是行的变化,很安全。但是 binlog 的磁盘占用会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。
  • mixed:混合模式,根据语句来选用是 statement 还是 row 模式。

两阶段提交

由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

引擎将新数据更新到内存中,同时将更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务

执行器生成操作的binlog,并把binlog写入磁盘

行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成

undo log

每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值

当没有事务再需要用到这些回滚日志时,回滚日志会被删除,长事务会导致保存很多回滚日志占用大量存储空间