返回 登录
0

浅析:如何构建稳定的系统

作者:Jesper L. Andersen
原文:How to build stable systems
译者:孙薇

准备工作

第一个决策是最简单却最为重要的,属于意识形态的一种:那就是软件是由开发者控制的。开发者需要控制软件,而不是反过来,让管理者、产生负责人控制软件。 唯一能控制软件的人就是编写它们的人。

第二个决策就是必须拥有能够掌控的小型工作单元。先解决整个问题的一小部分,并部署到生产环境中,显然比让整个大型项目挂掉要好得多。将初期的小型工作单元作为后面探索的测试平台。

开发者有责任一直掌控软件,这是关键,其它都是手段。一旦出现bug,就会影响到开发者的工作日程(修复bug!);此外最后期限也会确定开发者的工作日程和软件质量(在此之前完成工作!); 这样等到最后期限到来时,开发者需要对尽在掌控的那部分软件实施部署,而将不在控制的那部分回滚。

对软件所做的任何变更都应该是简洁快速的,并且是将系统从一个稳定点移动到另一个稳定点。宁可少完成一些内容,但要保证完成的部分质量优秀。一旦部署的内容中有错误,就会影响到生产数据,修复起来代价极高。最糟的情况下,甚至必需重置多年来的数据,这样十分浪费时间。

软件是以一系列项目的形式写就。每个项目都是很小的、独立的,并有确定的完成时限。每个项目的人数都很少,不超过6个;完成时间都不会超过2个半月;并有确定的成功条件。项目的成功标准就是:该项目能够完成自己的工作。

每个项目都始于24-72小时全神贯注的基础工作,这段时间为整个项目奠定了核心,为系统可行性描绘出了原型。如果基础工作失败了,这个项目就会被放弃,同时让开发者从中学到经验。所有工作都能被简化或者切分为项目的核心工作,这部分工作的目标就是为了查看整体项目是否可行,并建立所有人对这个项目的信心。

每个项目通常都是一次赌博:做从未做过的事或者高风险/高回报就是赌博。选择新编程语言是赌博,使用新框架也是赌博,采用新的应用部署还是赌博,了解哪些地方是在冒险,哪些是软件的稳定因素,这是我们控制风险时需要知道的。准备回滚也是出于负面因素而进行的赌博。

每个项目始于“在这个项目中我们不会解决的问题”列表,列表中的很多东西似乎很有诱惑力,限定范围会帮助我们设定需求重点,定义哪些是未来需要拓展的,并将这些内容放在以后的其他项目列表中。

了解项目在整体中所处的位置:越是核心的组件就需要更多测试、开发速度越慢、对错误关注度也越高。需要了解公司的一般测试水平,否则要在添加更多测试以前,先准备好冒烟测试。测试的核心组件莫过于leaf组件。

了解你的实验,或者整个项目就是一场实验。——Mike Williams

进行实验:在开始项目之前,先进行小规模分析,将其标注为真正项目开始前的预备分析工作,让大家知道你的研发是朝向正确的解决方向的。

了解你要交互的每一部分的代码质量,仔细警惕,找出故障API。了解你要交互的数据质量,如果在使用前这些数据需要多次清洗,也许在清洗干净前不应在项目中使用这些数据。

任何建立在已有系统顶层的项目都需要过渡方案:我们如何逐渐从现有的点过渡到新的系统?大规模部署往往伴随有很多风险,在稳定的环境中,不要冒这种风险。了解数据源是怎样更新的:如何从一个数据源过渡到另一个。连接到多个数据源,并按需进行移植显然是选择之一。开发者必须总能掌控这些问题。

过程与人

人们总会选择对团队有用的方案,比如Agile/XP/SCRUM/Kanban/ThisYearsFad,当然也可以将其全部否定掉。

开发是数字化的,在家办公或者在办公室办公效率相同,需要避免那些有办公室才能执行的方法。5年来大家都在全世界范围内雇用人员,有的公司在印度、德国和SF有3个办公室。

喜欢异步交流的话:电邮、IRC、Slack等都能避免干扰,更容易进行同步。在办公室里设定一些隐蔽的角落,让想要不受干扰、安静工作一阵子的人有个去处。

总有人不喝咖啡,尊重他们的选择。一些人喜欢结对编程,用同一个键盘来解决问题;还有人烦透了这些互动。了解在团队中,人们偏好使用代码库的方式有哪些不同。

坐在椅子上并不代表这些时间都花在创造生产力上面了。很多时候,不在电脑前的时候反而能获得解决方案。灵活的工作时间和工作场合对生产力来说必不可少。聪明的人不会在离开工作时马上关闭工作状态的“开关”。所有团队都会有人犯错,这是正常的,从错误中学习,习惯解决问题。

系统设计

系统是为了生产力而构建。也就是说,系统并不是玩具,不能只完成自己那一份,就丢到生产环境不用再操心了。系统是用于生产消耗的:需要考虑如何在生产中配置系统,需要考虑内部依赖,并进行限制,还需要让系统易用、易维护。

系统建立在12-factor理论之上,包括一套松散耦合的模块,每个模块都有一个责任,并为了让软件的其他部分正常工作,而对这个责任进行管理。模块之间通过协议进行松散的通讯,也就是说在通讯中,各方都可以变更,只要仍旧通过相同的方式进行通讯。在设计协议时要考虑到未来的扩展问题,每个模块在设计时都要考虑依赖,各模块都可以随时替换掉,将其放在另一个系统中需要是仍然可用的。

要避免会滋生紧密耦合的深层依赖架构,在模块太过巨大时需要将其作拆分处理,但也要避免一大堆太过细小的模块。要保持复制功能的能力,从而减少依赖;依赖越少,有时候反而会越成功。

在通讯结构中,端点是智能的,中间件只需传递数据。即多层次发掘参数状态:所构建的系统可以接纳任何难解的数据团,并进行传递。避免中间件对数据进行解析和诠释。代码和数据一直在变化,因此在数据上保持参数化会让一切变得简单。

可以准备一个管理或重启策略,如果以Erlang进行编写,这个策略应该已经有了,如果用其他语言,必须在应用内部(细节更好)或者通过操作系统(保证粗粒度)构建这样的基础架构。

系统偏好通过幂等性实现棘轮效应的方法,从已知的稳定状态过渡到计算出的下一步状态:如果成功的话,会对一致性进行验证,然后保持在这种状态中;如果失败的话,就会放弃之前的尝试,再来一次。基本上只有在棘轮侧翼,计算出的系统和有状态的系统之间的系统会没有状态。这点对于分布式系统的尽力交付机制来说特别重要,因为在所有消息中拥有唯一ID,意味着超时状态下可以执行重试,并确保如果在接收系统中拥有执行日志的话,就不会被接收系统重复运行。

构建总是能与状态点及时“同步”的系统,这样就避免了所构建的系统通过单独模式进行在线处理、离线同步,使得代码路径重复存在、非常复杂。

遵循UNIX原则:每个工具只做好一件事情,避免在一个工具中增加更多功能,而是另起一个工具。

预先定义系统的容量:在正常的操作中,打算实现多大的负载?正常运转的负载峰值是多少?

安装工作

首先创建一个空项目,将其加入到持续集成中,再部署到staging环境中。如果是个没有用户的全新项目,也可以直接部署到将要设置生产环境的机器上。一旦运行起来,就可以开始构建应用了。随着需求增加,我们在部署链中也增加必须的配置。

持续集成会产生构件,以独立的方式,不依赖主机环境,保存简单设置的方式来构建代码。设法预先配置系统,在部署时就无需外部依赖了,在以后可以节省大把力气。在staging和生产环境中部署的构件是相同的,根据环境进行配置。可以采用磁盘、consul、etcd、DNS或从S3上下载的某个配置文件。这里宁可使用简单的技术,也不要过早使用高级技术。

构件具有可复制性,将依赖封锁在特定的标签或版本中,让升级的依赖关系成为自己可以掌控的决定,目的就是为了让应用包具有可复制性。避免外部因素对应用突然产生影响,把一切搞的一团糟。

构件包含运行软件所需的一切,或是二叉树,或是包含二进制的目录树。通过静态链接相连,Go binaries、OCaml binaries、Haskell (GHC) binaries或者Erlang/Elixir发布都是优秀的构件样例。部署系统会读取部署信息,而部署信息就填充在这些构件中。

在不到1分钟之内,在第一个实例中点击按钮执行操作(button-push-to-operational-on-the-first-instance)以实施生产环境的部署。

创建每个应用中都会包含的默认数据库。数据库包含debug/追踪组件,收集和输出指标的工具,让应用成为网络聊天机器人等等。在每个应用中使用同一个数据库。

开发工作

代码正确比开发速度更重要,代码优雅比开发速度更重要,代码质量比开发速度更重要,其实速度真的不太重要。

在开发开始前应当定义一个点,号称“足够好”的点,在到达这个点的程度之前都不要进行优化。大多软件的开发无需速度。

在执行算法和数据架构优化前进行测量,了解变更是否起到了想要的作用。构建的系统应当能够在运行时收集自身指标,将这些指标发送给中央点执行进一步分析,查看Gil Tene在HdrHistogram上的工作,并利用这个工具。

竭尽所能使用手边的工具:单元测试、基于属性的测试、类型系统、静态分析以及性能分析。完全没有理由拒绝使用能帮助你早点解决bug的工具。由于对生产数据造成变更,bug越晚发现,修复所花的成本就越大,要成指数倍的增加。

软件在构建时就是为了运行在不同的环境中,特别是UNIX。系统需要适应不同的运行环境,如果锁定特定平台,一般就会出现问题。如果只能运行在Windows上,那就糟糕了。因为锁定单独供应商的话,你的存亡要取决于他们所提供的软件质量。

大多时候对代码格式的讨论是没有意义的。因为标准在制定时也是随意的,之后大家都来效仿。在GO语言中,只要运行“go fmt”,讨论就被终结了。

了解在系统中出错的关键在哪里:哪些地方绝对不能出错,哪些地方对正确性的要求更为宽松。将出错的关键隔离出来,着重进行测试,在系统的边界使用负载调节,以避免过载的情况出现,而不要从内部进行负载调节。如果工作量太大,需要拒绝某些工作,为一些选中的人提供服务,要比想为所有人服务,结果却完不成要好得多。

使用循环切断器来切断级联依赖故障,由于可以手动进行切换,在需要维护时这些更是必不可少的。

选择数据库

默认的文本编辑器是ed(1),默认的数据库是postgresql,除非数据集大于10TB,否则选postgresql就可以了。如果需要MongoDB之类的功能,可以创建jsonb列。将数据放在postgres中进行分析,然后移出。利用Postgres来存储数据,从中输出到elasticsearch中;对postgres中存储的其他数据进行预热,除非负载增加,否则运行postgres的读取副本就可以了:使用pg_bouncer。

大多新的数据库在一致性与安全性保证上都有问题,特别是不够成熟的变体。它们的“call me maybe”运行模式很可能因为意外而导致数据无法存储,特别是在分布式数据库中,通过网络连接的情况下更是如此。咨询Kyle Kingsbury在Jepsen项目中的最新发现,使用它来获得你需要的保证。

很多新数据库性能都很有限:在特定情况下使用良好,一旦超出这个范围,或者将负载/压力增到承受能力之外时,就会惨遭失败。如果开始执行非常复杂的事务时,需要进行切换,我们很难长期抛开数据库设计,特别是在需要分布式操作时。将复杂的事务互动独立出来,只开放少量的存储区域,这一般是出于经济因素。在可能的情况下,寻找等幂的棘轮效应方案。

选择编程语言

想要系统稳健,必须得在系统中某处选择Erlang,除此之外没有其他语言更能满足运行稳定所需要的准则了。

如果没有选择Erlang,就必须在Weapon-of-Choice™中重新实现Erlang的概念。需要避免单一化,用C#或Java编写全部代码意味着有的项目能够解决地很容易,有的项目就会很难。尝试混用多种不同的语言,以便提供不同的权衡策略;这将会使得选中最切合问题语言的机会最大化。

了解语言的劣势:Python不太适合大规模并发,Erlang不适合需要原始计算能力的问题,OCaml在并行执行上不够成熟,Go不适合需要优秀抽象能力以及复杂故障模式的问题,某种语言只有在你拥有其轻松部署方案时才是成功的。工具部署必须在使用前完成,不管什么语言,所有的项目都使用相同的配置和构建工具:make(1)。

持续集成和部署的常用语言是make(1),在公司的所有项目中都使用make,新人上手简单,还能重复持续集成工具所做的工作,构建软件的方式也有了相关“文档”。

配置工作

构件中有着默认配置,需要变化的内容都是环境相关的。利用“12-factor”,从环境变量中选取配置。持久性数据会存放在专用的地方,位于构件路径以外的地方。这些应用日志记录到默认位置,上限不会超过提前设定的某个磁盘空间常数量,从而使得循环建立起来。

应用不可修改构件路径。

在生产与staging环境中使用不同的凭据,避免误配置,将staging与生产环境的网络分离开来,这样staging的部署就不会影响到生产环境的部署了。不要让开发者用笔记本随意访问生产环境,加上一些限制。

避免过早采用etcd/Consul/chubby等安装,除非规模足够大,否则都无需使用完全动态的配置系统。在引导时使用从S3下载的文件,多数情况下都足够了。

运营工作

为休息时间做出优化,必须尽力避免让工作人员——无论是运营者还是开发者半夜爬起来处理问题。系统必须能够完美降级,部分降级的系统一般还能运行,而无响应的系统则是出现故障。使用这种办法,避免打扰工作人员的休息。

系统可以使用monit、supervise、upstart、systemd、rcNG、SMF等等,但不会使用tmux/screen。运营系统在放弃前能多次重启出现故障的应用。

考虑给软件使用单独的堆栈,而不是借用系统堆栈,以避免不得不使用旧软件的情况,并使得在不同型号的机器间进行堆栈迁移更简单。

kill应用总是很安全的,对于Amazon控制的事件,我们是没有控制权的,必须按照命令,对应用执行停止和启动操作。引导主动请求并不是好选择,为了让工作完成,必须耗费时间按照启动的相反顺序一一关闭内部系统。

开发者一般不对生产环境的主机进行日志记录,每个日志文件都是在系统之外发送和索引的,指标也是这样。开发者在staging主机上工作,这样就可以在大多数情况下(超过90%),不用访问生产环境就能重现错误,并有足够的信息可以发送。

在故障出现之前,指标一般就会指示出故障。剔除用奇怪的方式放弃使用系统的消费者之后,剩下的往往代表了大家使用系统的方式。了解系统为什么在特定消费者那里出现峰值,能够让故障处理成为前瞻性风险。随着负载增加,极端价值成为常见的事情。

唯一能对生产环境主机进行变更的方式就是重新部署,唯一能对staging主机进行变更的方式也是重新部署。

现在我们的世界是弹性的,运转新机器、jail或域名都很便宜,因此可以用在运营中。重建数据中心,利用开关实现负载均衡,让回滚和部署降级变得容易,逐渐切换使用新代码的流量(高风险),在实现全额负载前观察是否适用。为风险部署和问题修复提供充足(超额)机器,然后逐渐向下缩减,观察软件运行是否良好。

人们很容易在生产环境部署出现故障时,通过回滚来处理问题。这样做的风险很有可能失去控制,因此要留好后路。如果想要一天多次部署生产环境,那么手边要准备一组稳定的主机,预备回滚。

截止2016年2月,Docker还不成熟,目前暂且避免在生产环境中使用它。目前它还不能满足需求,不过这种情况总会随着时间改变的,需要继续关注,了解该采用它的时间。


(责编/钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,交流探讨可加微信qshuguang2008,备注姓名+公司+职位)

「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008入群,备注姓名+公司+职位。

评论