我做系统架构的一些原则
工作 20 多年了,这 20 来年看到了很多公司系统架构,也看到了很多问题,在跟这些公司进行交流和讨论的时候,包括进行实施和方案比较的时候,都有很多各种方案的比较和妥协,因为相关的经历越来越多,所以,逐渐形成了自己的逻辑和方法论。今天,想写下这篇文章,把我的这些个人的经验和想法总结下来,希望能够让更多的人可以参考和借鉴,并能够做出更好的架构来。另外,我的这些思维方式和原则都针对于现有市面上众多不合理的架构和方案,所以,也算是一种“纠正”……(注意,这篇文章所说的这些架构上的原则,一般适用于相对比较复杂的业务,如果只是一些简单和访问量不大的应用,那么你可能会得出相反的结论)
目录
原则一:关注于真正的收益而不是技术本身
对于软件架构来说,我觉得第一重要的是架构的收益,如果不说收益,只是为了技术而技术,而没有任何意义。对于技术收益来说,我觉得下面这几个收益是非常重要的:
- 是否可以降低技术门槛加快整个团队的开发流程。能够加快整个团队的工程流程,快速发布,是软件工程一直在解决的问题,所以,系统架构需要能够进行并行开发,并行上线和并行运维,而不会让某个团队成为瓶颈点。(注:就算拖累团队的原因是组织构架,也不妨碍我们做出并行的系统架构设计)
- 是否可以让整个系统可以运行的更稳定。要让整个系统可以运行的更为的稳定,提升整个系统的 SLA,就需要对有计划和无计划的停机做相应的解决方案(参看《关于高可用的架构》)
- 是否可以通过简化和自动化降低成本。最高优化的成本是人力成本,人的成本除了慢和贵,还有经常不断的 human error。如果不能降低人力成本,反而需要更多的人,那么这个架构设计一定是失败的。除此之外,是时间成本,资金成本。
如果一个系统架构不能在上面三个事上起到作用,那就没有意义了。
原则二:以应用服务和 API 为视角,而不是以资源和技术为视角
国内很多公司都会有很多分工,基本上都会分成运维和开发,运维又会分成基础运维和应用运维,开发则会分成基础核心开发和业务开发。不同的分工会导致完全不同的视角和出发点。比如,基础运维和开发的同学更多的只是关注资源的利用率和性能,而应用运维和业务开发则更多关注的是应用和服务上的东西。这两者本来相关无事,但是因为分布式架构的演进,导致有一些系统已经说不清楚是基础层的还是应用层的了,比如像服务治理上的东西,里面即有底层基础技术,也需要业务的同学来配合,包括 k8s 也样,里面即有底层的如网络这样的技术,也有需要业务配合的 readniess和 liveness 这样的健康检查,以及业务应用需要 configMap 等等 ……
这些东西都让我感觉到所谓 DevOps,其实就是因为很多技术和组件已经分不清是 Dev 还是 Ops 的了,所以,需要合并 Dev和 Ops。而且,整个组织和架构的优化,已经不能通过调优单一分工或是单一组件能够有很大提升的了。其需要有一种自顶向下的,整体规划,统一设计的方式,才能做到整体的提升(可以试想一下城市交通的优化,当城市规模到一定程度的时候,整体的性能你是无法通过优化几条路或是几条街区来完成的,你需要对整个城市做整体的功能体的规划才可能达到整体效率的提升)。而为了做到整体的提升,需要所有的人都要有一个统一的视角和目标,这几年来,我觉得这个目标就是——要站在服务和 对外API的视角来看问题,而不是技术和底层的角度。
原则三:选择最主流和成熟的技术
技术选型是一件很重要的事,技术一旦选错,那会导致整个架构需要做调整,而对架构的调整重来都不是一件简单的事,我在过去几年内,当系统越来越复杂的时候,用户把他们的 PHP,Python, .NET,或 Node.js 的架构完全都迁移到 Java + Go 的架构上来的案例不断的发生。这个过程还是非常痛苦的,但是你没有办法,当你的系统越来越复杂,越来越大时,你就再也不能在一些玩具技术上玩了,你需要的更为工业化的技术。
- 尽可能的使用更为成熟更为工业化的技术栈,而不是自己熟悉的技术栈。 所谓工业化的技术栈,你可以看看大多数公司使用的技术栈,比如:互联网,金融,电信……等等 ,大公司会有更多的技术投入,也需要更大规模的生产,所以,他们使用的技术通常来说都是比较工业化的。在技术选型上,千万不要被——“你看某个视频公司也在用这个技术”,或是一些在论坛上看到的一些程序员吐槽技术的观点(没有任何的数据,只有自己的喜好)来决定自己的技术,还是看看主流大多数公司实际在用的技术栈,会更靠谱一些。
- 选择全球流行的技术,而不是中国流行的技术。技术这个东西一定是一个全球化的东西,不是一个局域化的事。所以,一定要选国际化的会更好。另外,千万不要被某些公司的“特别案例”骗过去了,那怕这个案例很性感,关键还是要看解决问题的思路和采用的技术是否具有普世性。只有普世性的技术有更强的生命力。
- 尽可能的使用红利大的主流技术,而不要自己发明轮子,更不要魔改。我见过好些个公司魔改开源软件,比如有个公司同魔改mesos,最后改着改着发现自己发明另一个 kubernetes。我还见过很多公司或技术团队喜欢自己发明自己的专用轮子,最后都会被主流开源软件所取代。完全没有必要。不重新发明轮子,不魔改,不是因为自己技术不能,而是因为,这个世界早已不是自己干所有事的年代了,这个时代是要想尽方法跟整个产业,整个技术社区融合和合作,这样才会有最大的收益。那些试图因为某个特例需要自成一套的玩法,短期没问题,但长期来说,我都不看好。
- 绝大多数情况下,如无非常特殊要求,选 Java基本是不会错的。一方面,这是因为 Java 的业务开发的生产力是非常好的,而且有 Spring 框架保障,代码很难写烂,另外,Java 的社区太成熟了,你需要的各种架构和技术都可以很容易获得,技术红利实在是太大。这种运行在JVM上的语言有太多太多的好处了。在 Java 的技术栈上,你的架构风险和架构的成本(无论是人力成本,时间成本和资金成本)从长期来说都是最优的
在我见过的公司中,好些公司的架构都被技术负责人个人的喜好、擅长和个人经验给绑架了,完全不是从一个客观的角度来进行技术选型。其实,从 0 到 1 的阶段,你用什么样的技术都行,如果你做一个简单的应用,没有事务处理没有复杂的交易流程,比如一些论坛、社交之类的应用,你用任何语言都行。但是如果有一天你的系统变复杂了,需要处理交易了,量也上来了,从 1 到 10,甚至从 10 到 100,你的开发团队也变大了,需要构建的系统越来越大,你可能会发现你只有一个选择,就是 Java。想想京东从.NET 到 Java,淘宝从 PHP 到 Java……
注,一些有主观喜好的人一定会对我上述对 Java 的描述感到不适,我还用一些证据说明一下——全中国所有的电商平台,几百家银行,三大电信运营商,所有的保险公司,劵商的系统,医院里的系统,电子政府系统,等等,基本都是用 Java 开发的,包括 AWS 的主流语言也是 Java,阿里云一开始用 C++/Python 写控制系统,后面也开始用 Java ……你可能会说 B站是用 go语言,但是你可能不知道 B 站的电商和大数据是用 Java……懂着数据分析的同学,建议上各大招聘网站上搜一下 Java 的职位数量,你就知道某个技术是否主流和热门……
原则四:完备性会比性能更重要
我发现好些公司的架构师做架构的时候,首要考虑的是架构的性能是否能够撑得住多大多大的流量,而不是考虑系统的完备性和扩展性。所以,我已经多次见过这样的案例了,一开始直接使用 MongoDB 这样的非关系型数据库,或是把数据直接放在 Redis 里,而直接放弃关系型数据库的数据完备性的模型,而在后来需要在数据上进行关系查询的时候,发现 NoSQL 的数据库在 Join 上都表现的太差,然后就开始各种飞线,为了不做 Join 就开始冗余数据,然而自己又维护不好冗余数据后带来的数据一致性的问题,导致数据上的各种错乱丢失。
所以,我给如下的一些如下的架构原则:
- 使用最科学严谨的技术模型为主,并以不严谨的模型作为补充。对于上面那个案例来说,就是——永远使用完备支持 ACID 的关系型数据库,然后用 NoSQL 作补充,而不是完全放弃关系型数据库。这里的原则就是所谓的“先紧后松”,一开始紧了,你可以慢慢松,但是开始松了,以后你想紧再也紧不过来了。
- 性能上的东西,总是有很多解的。我这么多年的经历告诉我,性能上的事,总是有解的,手段也是最多的,这个比起架构的完备性和扩展性来说真的不必太过担心。
为了追求所谓的性能,把整个系统的完备性丢失掉,相当地得不偿失。
原则五:制定并遵循服从标准、规范和最佳实践
这个原则是非常重要的,因为只有服从了标准,你的架构才能够有更好的扩展性。比如:我经常性的见到很多公司的系统既没有服从业界标准,也没有形成自己公司的标准,感觉就像一群乌合之众一样。最典型的例子就是 HTTP 调用的状态返回码。业内给你的标准是 200表示成功,3xx 跳转,4xx 表示调用端出错,5xx 表示服务端出错,我实在是不明白为什么无论成功和失败大家都喜欢返回 200,然后在 body 里指出是否error(前两年我在微信公众号里看到一个有一定名气的互联网老兵推荐使用无论正确还是出错都返回 200 的做法,我在后台再三确认后,我发现这样的架构师真是害人不浅)。这样做最大的问题是——监控系统将在一种低效的状态下工作。监控系统需要把所有的网络请求包打开后才知道是否是错误,而且完全不知道是调用端出错还是服务端出错,于是一些像重试或熔断这样的控制系统完全不知道怎么搞(如果是 4xx错,那么重试或熔断是没有意义的,只有 5xx 才有意义)。有时候,我会有种越活越退步的感觉,错误码设计这种最基本最基础的东西为什么会没有?并且一个公司会任由着大家乱来?这些基础技能怎么就这样丢掉了?
还有,我还见过一些公司,他们整个组织没有一个统一的用户 ID 的设计,各个系统之间同步用户的数据是通过用户的身份证 ID,是的,就是现实世界的身份证 ID,包括在网关上设置的用户白名单居然也是用身份证 ID。我对这个公司的内的用户隐私管理有很大的担忧。一个企业,一个组织,如果没有标准和规范,也就会有抽象,这一定是要出各种乱子的。
下面,我罗列一些你需要注意的标准和规范(包括但不限于):
- 服务间调用的协议标准和规范。这其中包括 Restful API路径, HTTP 方法、状态码、标准头、自定义头等,返回数据 JSon Scheme……等。
- 一些命名的标准和规范。这其中包括如:用户 ID,服务名、标签名、状态名、错误码、消息、数据库……等等
- 日志和监控的规范。这其中包括:日志格式,监控数据,采样要求,报警……等等
- 配置上的规范。这其中包括:操作系统配置、中间件配置,软件包……等等
- 中间件使用的规范。数据库,缓存、消息队列……等等
- 软件和开发库版本统一。整个组织架构内,软件或开发库的版本最好每年都升一次级,然后在各团队内统一。
这里重要说一下两个事:
- Restful API 的规范。我觉得是非常重要的,这里给两个我觉得写得最好的参考:Paypal 和 Microsoft 。Restful API 有一个标准和规范最大的好处就是监视可以很容易地做各种统计分析,控制系统可以很容易的做流量编排和调度。
- 另一个是服务调用链追踪。对于服务调用链追踪来说,基本上都是参考于 Google Dapper 这篇论文,目前有很多的实现,最严格的实现是 Zipkin,这也是 Spring Cloud Sleuth 的底层实现。Zipkin 贴近 Google Dapper 论文的好处在于——无状态,快速地把 Span 发出来,不消耗服务应用侧的内存和 CPU。这意味着,监控系统宁可自己死了也不能干扰实际应用。
- 软件升级。我发现很多公司包括 BAT,他们完全没有软件升级的活动,全靠开发人员自发。然而,这种成体系的活动,是永远不可能靠大众的自发形成的。一个公司至少一年要有一次软件版本升级的review,然后形成软件版本的统一和一致,这样会极太简化系统架构的复杂度。
原则六:重视架构扩展性和可运维性
在我见过很多架构里,技术人员只考虑当下,但从来不考虑系统的未来扩展性和可运维性。所谓的管生不管养。如果你生下来的孩子胳膊少腿,严重畸形,那么未来是很难玩的。因为架构和软件不是写好就完的,是需要不断修改不断维护的,80%的软件成本都是在维护上。所以,如何让你的架构有更好的扩展性,可以更容易地运维,这个是比较重要的。所谓的扩展性,意味着,我可以很容易地加更多的功能,或是加入更多的系统,而所谓可运维,就是说我可以对线上的系统做任意的变更。扩展性要求的是有标准规范且不耦合的业务架构,可运维性要求的则是可控的能力,也就是一组各式各样的控制系统。
- 通过服务编排架构来降低服务间的耦合。比如:通过一个业务流程的专用服务,或是像 Workflow,Event Driven Architecture , Broker,Gateway,Service Discovery 等这类的的中间件来降低服务间的依赖关系。
- 通过服务发现或服务网关来降低服务依赖所带来的运维复杂度。服务发现可以很好的降低相关依赖服务的运维复杂度,让你可以很轻松的上线或下线服务,或是进行服务伸缩。
- 一定要使用各种软件设计的原则。比如:像SOLID这样的原则(参看《一些软件设计的原则》),IoC/DIP,SOA 或 Spring Cloud 等 架构的最佳实践(参看《SteveY对Amazon和Google平台的吐槽》中的 Service Interface 的那几条军规),分布式系统架构的相关实践(参看:《分布式系统的事务处理》,或微软件的 《Cloud Design Patterns》)……等等
原则七:对控制逻辑进行全面收口
所有的程序都会有两种逻辑,一种是业务逻辑,一种是控制逻辑,业务逻辑就是完成业务的逻辑,控制逻辑是辅助,比如你用多线程,还是用分布式,是用数据库还是用文件,如何配置、部署,运维、监控,事务控制,服务发现,弹性伸缩,灰度发布,高并发,等等,等等 ……这些都是控制逻辑,跟业务逻辑没有一毛钱关系。控制逻辑的技术深度会通常会比业务逻辑要深一些,门槛也会要高一些,所以,最好要专业的程序员来负责控制逻辑的开发,统一规划统一管理,进行收口。这其中包括:
- 流量收口。包括南北向和东西向的流量的调度,主要通过流量网关,开发框架 SDK或 Service Mesh 这样的技术。
- 服务治理收口。包括:服务发现、健康检查,配置管理、事务、事件、重试、熔断、限流……主要通过开发框架 SDK – 如:Spring Cloud,或服务网格Service Mesh等技术。
- 监控数据收口。包括:日志、指标、调用链……主要通过一些标准主流的探针,再加上后台的数据清洗和数据存储来完成,最好是使用无侵入式的技术。监控的数据必须统一在一个地方进行关联,这样才会产生信息。
- 资源调度有应用部署的收口。包括:计算、网络和存储的收口,主要是通过容器化的方案,如k8s来完成。
- 中间件的收口。包括:数据库,消息,缓存,服务发现,网关……等等。这类的收口方式一般要在企业内部统一建立一个共享的云化的中间件资源池。
对此,这里的原则是:
- 你要选择容易进行业务逻辑和控制逻辑分离的技术。这里,Java 的 JVM+字节码注入+AOP 式的Spring 开发框架,会带给你太多的优势。
- 你要选择可以享受“前人种树,后人乘凉”的有技术红利的技术。如:有庞大社区而且相互兼容的技术,如:Java, Docker, Ansible,HTTP,Telegraf/Collectd……
- 中间件你要使用可以 支持HA集群和多租户的技术。这里基本上所有的主流中间件都会支持 HA 集群方式的。
原则八:不要迁就老旧系统的技术债务
我发现很多公司都很非常大的技术债务,这些债务具体表现如下:
- 使用老旧的技术。比如,使用HTTP1.0, Java 1.6,Websphere,ESB,基于 socket的通讯协议,过时的模型……等等
- 不合理的设计。比如,在 gateway 中写大量的业务逻辑,单体架构,数据和业务逻辑深度耦合,错误的系统架构(把缓存当数据库,用消息队列同步数据)……等等
- 缺少配套设施。比如,没有自动化测试,没有好的软件文档,没有质量好的代码,没有标准和规范……等等
来找我寻求技术帮助的人都有各种各样的问题。我都会对他们苦口婆心地说同样的一句话——“如果你是来找我 case-by-case 解决问题,我兴趣不大,因为,你们千万不要寄希望能够很简单的把一辆夏利车改成一辆法拉利跑车,或是把一栋地基没打好的歪楼搞正。以前欠下的技术债,都得要还,没打好的地基要重新打,没建配套设施都要建。这些基础设施如果不按照正确科学的方式建立的话,你是不可能有一个好的的系统,我也没办法帮你 case-by-case 的解决问题……”,一开始,他们都会对我说,没问题,我们就是要还债,但是,最后发现要还的债真多,有点承受不了,就开始现原形了。
他们开始为自己的“欠的技术债”找各种合理化的理由——给你解释各种各样的历史原因和不得以而为之的理由。谈着谈着,让我有一种感觉——他们希望得到一种什么都不改什么都不付出的方式就可以进步的心态,他们宁可让新的技术 low 下来迁就于这些技术债,把新的技术滥用地乱七八糟的。有一个公司,他们的系统架构和技术选型基本都搞错了,使用错误的模型构建系统,导致整个系统的性能非常之差,也才几千万条数据,但他们想的不是还债,不是把地基和配套设施建好,而且要把楼修的更高,上更多的系统——他们觉得现有的系统挺好,性能问题的原因是他们没一个大数据平台,所以要建大数据平台……
我见过很多很多公司,包括大如 BAT 这样的公司,都会在原来的技术债上进行更多的建设,然后,技术债越来越大,利息越来越大,最终成为一个高利贷,再也还不了(我在《开发团队的效率》一文中讲过一个 WatchDog 的架构模式,一个系统烂了,不是去改这个系统,而是在旁边建一个系统来看着它,我很难理解为什么会有这样的逻辑,也许是为了要解决更多的就业……)
这里有几个原则和方法我是非常坚持的,分享给大家:
- 与其花大力气迁就技术债务,不如直接还技术债。是所谓的长痛不如短痛。
- 建设没有技术债的“新城区”,并通过“防腐层 ”的架构模型,不要让技术债侵入“新城区”。
原则九:不要依赖自己的经验,要依赖于数据和学习
有好些人来找我跟我说他们的技术问题,然后希望我能够给他们一个答案。我说,我需要了解一下你现有系统的情况,也就是需要先做个诊断,我只有得到这些数据后,我才可能明白真正的原因是什么 ,我才可能给你做出一个比较好的技术方案。我个人觉得这是一种对对方负责的方法,因为技术手段太多了,所有的技术手段都有适应的场景,并且有各种 trade-off,所以,只有调研完后才能做出决定。这跟医生看病是一样的,确诊病因不能靠经验,还是要靠诊断数据。在科学面前,所有的经验都是靠不住的……
另外,如果有一天你在做技术决定的时候,开始凭自己以往的经验,那么你就已经不可能再成长了。人都是不可能通过不断重复过去而进步的,人的进步从来都是通过学习自己不知道的东西。所以,千万不要依赖于自己的经验做决定。做任何决定之前,最好花上一点时间,上网查一下相关的资料,技术博客,文章,论文等 ,同时,也看看各个公司,或是各个开源软件他们是怎么做的?然后,比较多种方案的 Pros/Cons,最终形成自己的决定,这样,才可能做出一个更好的决定。
原则十:千万要小心 X – Y 问题,要追问原始需求
对于 X-Y 问题,也就是说,用户为了解决 X问题,他觉得用 Y 可以解,于是问我 Y 怎么搞,结果搞到最后,发现原来要解决的 X 问题,这个时候最好的解决方案不是 Y,而是 Z。 这种 X-Y 问题真是相当之多,见的太多太多了。所以,每次用户来找我的时候,我都要不断地追问什么是 X 问题。
比如,好些用户都会来问我他们要一个大数据流式处理,结果追问具体要解决什么样的问题时,才发现他们的问题是因为服务中有大量的状态,需要把相同用户的数据请求放在同一个服务上处理,而且设计上导致一个慢函数拖慢整个应用服务。最终就是做一下性能调优就好了,根本没有必要上什么大数据的流式处理。
我很喜欢追问为什么 ,这种追问,会让客户也跟着来一起重新思考。比如,有个客户来找我评估的一个技术架构的决定,从理论上来说,好像这个架构在用户的这个场景下非常不错。但是,这个场景和这个架构是我职业生涯从来没有见过的。于是,我开始追问这个为什么会是这么一个场景?当我追问的时候,我发现用户都感到这个场景的各种不合理。最后引起了大家非常深刻的研讨,最终用户把那个场景修正后,而架构就突然就变成了一个常见且成熟的的模型……
原则十一:激进胜于保守,创新与实用并不冲突
我对技术的态度是比较激进的,但是,所谓的激进并不是瞎搞,也不是见新技术就上,而是积极拥抱会改变未来的新技术,如:Docker/Go,我就非常快地跟进,但是像区块链或是 Rust 这样的,我就不是很积极。因为,其并没有命中我认为的技术趋势的几个特征(参看《Go,Docker 和新技术 》)。当然,我也不是不喜欢的就不学了,我对区块链和 Rust 我一样学习,我也知道这些技术的优势,但我不会大规模使用它们。另外,我也尊重保守的决定,这里面没有对和错。但是,我个人觉得对技术激进的态度比起保守来说有太多的好处了。一方面来说,对于用户来说,很大程度上来说,新技术通常都表面有很好的竞争力,而且我见太多这样成功的公司都在积极拥抱新的技术的,而保守的通常来说都越来越不好。
有一些人会跟我说,我们是实用主义,我们不需要创新,能解决当下的问题就好,所以,我们不需要新技术,现有的技术用好就行了。这类的公司,他们的技术设计第一天就在负债,虽然可以解决当下问题,但是马上就会出现新的问题,然后他们会疲于解决各种问题。最后呢,最后还是会走到新的技术上。
这里的逻辑很简单 —— 进步永远来自于探索,探索是要付出代价的,但是收益更大。对我而言,不敢冒险才是最大的冒险,不敢犯错才是最大的错误,害怕失去会让你失去的更多……