在逾二十年的职业生涯中,我见证了众多企业的系统架构实践,也观察到不少突出问题。在与这些公司进行交流、方案探讨乃至具体实施的过程中,我积累了丰富的经验,并逐步形成了自己独特的方法论。本文旨在分享这些个人洞见,希望能为技术同行带来启发,助力大家构建更卓越的架构。本文所阐述的原则主要针对市场中存在的诸多不合理架构,可视为一种“纠正”与优化思路。需要注意的是,这些架构原则普遍适用于业务复杂且有一定规模的应用场景;对于较简单或访问量不大的系统,可能会得出不同的结论。
**原则一:以实际收益为导向,而非盲目追求技术本身**
对于软件架构而言,其首要价值在于能够带来实际收益。脱离收益谈技术,将沦为无意义的“为了技术而技术”。在我看来,关键的技术收益体现在以下几个方面:
降低技术门槛,加速开发流程:一个优秀的系统架构应能促进团队并行开发、上线与运维,避免成为效率瓶颈,从而加快工程进度,快速交付产品。即使组织架构是导致效率低下的原因,我们仍应致力于设计出能够支持并行工作的系统。
提升系统稳定性:为确保系统高稳定运行并提高SLA,必须针对有计划和无计划的停机场景提供相应的解决方案,例如高可用架构设计。
通过简化与自动化降低成本:成本优化最优先考虑的是人力成本,因为人力不仅昂贵且效率可能不高,还容易出现人为错误。如果一个架构设计反而导致需要更多人力,那它无疑是失败的。其次是时间成本和资金成本。
如果一个系统架构无法在这三个方面发挥积极作用,那么它的存在便没有太大意义。
**原则二:聚焦应用服务与API,而非资源与技术栈**
国内企业的职能分工通常较为细致,如运维分为基础运维和应用运维,开发分为基础核心开发和业务开发。不同的分工导致了视角和关注点的差异。例如,基础运维和核心开发倾向于关注资源利用率和性能,而应用运维和业务开发则更关注应用和服务层面。过去二者界限分明,但随着分布式架构的演进,许多技术已模糊了基础层与应用层的界限。例如,服务治理既包含底层技术,也需要业务团队配合;Kubernetes也涉及底层网络技术,同时需要业务应用提供readiness和liveness健康检查,并利用ConfigMap等功能。
这种融合趋势让我意识到,DevOps的兴起正是因为许多技术和组件已不再能清晰地归类为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语言,但其电商和大数据部分却是Java。如果你了解数据分析,建议在各大招聘网站搜索Java职位数量,便可知其主流和热门程度。
**原则四:完备性优先于性能**
我发现,许多架构师在设计系统时,往往将架构能否支撑海量流量作为首要考量,却忽视了系统的完备性和扩展性。我多次见到这样的案例:初期直接采用MongoDB等非关系型数据库,或将数据直接置于Redis中,从而放弃了关系型数据库的数据完备性模型。然而,当后续需要在数据上进行关系查询时,发现NoSQL数据库在Join操作上表现极差,于是开始各种“飞线”操作,为了避免Join而冗余数据,却又无法有效维护冗余数据带来的一致性问题,最终导致数据错乱和丢失。
因此,我提出以下建筑原则:
以最科学严谨的技术模型为主,并辅以不严谨的模型。针对上述案例,这意味着——始终使用完全支持ACID的关系型数据库,并以NoSQL作为补充,而非彻底放弃关系型数据库。这里的原则是“先紧后松”:初期严格,未来可以逐步放宽;若初期就放任自流,后期则难以再收紧。
性能问题总有解决之道。我多年经验表明,性能问题往往有多种解决方案,且手段多样,相比架构的完备性和扩展性,性能问题无需过度担忧。
为了追求所谓的性能而牺牲整个系统的完备性,是极其得不偿失的。
**原则五:制定并遵循标准、规范与最佳实践**
这一原则至关重要,因为只有遵循标准,你的架构才具备更好的扩展性。例如,我经常看到一些公司的系统既不遵循业界标准,也未形成内部标准,整个团队显得杂乱无章。最典型的例子便是HTTP调用的状态返回码。业界标准规定200表示成功,3xx跳转,4xx表示调用端错误,5xx表示服务端错误。然而,许多团队无论成功与否都习惯返回200,然后在body中指示是否出错。几年前,我曾在某知名互联网老兵的公众号上看到推荐这种做法,再三确认后,我深感这种架构思想误人不浅。这样做最大的问题在于——监控系统将在低效状态下工作。监控系统需要逐一解析所有网络请求包才能判断是否出错,且无法区分是调用端错误还是服务端错误。这导致重试或熔断等控制系统无法正常运作(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等巨头,都缺乏系统的软件升级活动,完全依靠开发人员自发。然而,这种体系化的活动,绝不可能仅凭大众自发形成。一个公司至少应每年进行一次软件版本升级审查,并形成软件版本的统一和一致性,这将极大简化系统架构的复杂度。
**原则六:重视架构的扩展性与可运维性**
在许多架构设计中,技术人员往往只考虑当前需求,而忽视了系统的未来扩展性和可运维性。这就像“只生不养”。如果生下的“孩子”先天不足,未来将很难发展。因为架构和软件并非一蹴而就,它需要持续修改和维护。软件成本的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等技术实现。
监控数据收敛:包括日志、指标、调用链等,主要通过标准主流探针,配合后台数据清洗和存储来完成,最好采用无侵入式技术。监控数据必须统一汇集关联,才能产生有效信息。
资源调度与应用部署收敛:涵盖计算、网络和存储的收敛,主要通过容器化方案(如Kubernetes)完成。
中间件收敛:包括数据库、消息队列、缓存、服务发现、网关等。这类收敛方式通常需要在企业内部统一建立共享的云化中间件资源池。
对此,我的原则是:
选择易于业务逻辑和控制逻辑分离的技术:Java的JVM+字节码注入+AOP式的Spring开发框架在这方面具有显著优势。
选择能够享受“前人种树,后人乘凉”技术红利的技术:例如,拥有庞大社区且相互兼容的技术,如Java、Docker、Ansible、HTTP、Telegraf/Collectd等。
中间件应选用支持HA集群和多租户的技术:目前主流中间件普遍支持HA集群模式。
**原则八:不迁就老旧系统的技术债务**
我发现许多公司积压了巨大的技术债务,具体表现如下:
采用过时技术:例如,使用HTTP1.0、Java 1.6、Websphere、ESB、基于Socket的通信协议、过时的模型等。
不合理的设计:例如,在网关中编写大量业务逻辑,采用单体架构,数据与业务逻辑深度耦合,错误的系统架构(如将缓存用作数据库,使用消息队列同步数据)等。
配套设施缺失:例如,缺乏自动化测试、优质软件文档、标准规范,代码质量低下等。
前来寻求技术帮助的人士都有各种各样的问题。我总是苦口婆心地对他们重复同样的话:“如果你只是希望我逐个解决眼前的问题,我兴趣不大。因为,你千万不要指望能够轻而易举地将一辆夏利车改装成法拉利跑车,或是将一座地基不稳的歪楼扶正。以前欠下的技术债,终究是要还的。没打好的地基要重新打,缺失的配套设施都要补齐。如果这些基础设施不以科学正确的方式建立,你就不可能拥有一个好的系统,我也无法帮你逐个解决问题……”起初,他们都会表示“没问题,我们就是要还债”,但最终发现需要偿还的债务过多,有点力不从心,便开始原形毕露。
他们开始为自己“欠下的技术债”寻找各种合理化理由——解释各种历史原因和不得已而为之的苦衷。谈话间,我常有一种感觉——他们渴望在不做出任何改变、不付出任何代价的情况下实现进步,甚至宁愿让新技术迁就于这些技术债务,将新技术滥用得一塌糊涂。曾有一家公司,其系统架构和技术选型基本都是错误的,使用了错误的模型构建系统,导致整个系统性能奇差,即使只有几千万条数据。但他们想的不是还债,不是把地基和配套设施建好,而是想把“楼”修得更高,上更多的系统——他们认为现有系统挺好,性能问题的原因是他们缺乏一个大数据平台,所以要建大数据平台……
我见过很多公司,包括像BAT这样的大公司,都会在原有的技术债务上继续进行建设,结果技术债务越积越多,利息也越来越高,最终变成一笔高利贷,再也无法偿还(我在《开发团队的效率》一文中曾提及一个WatchDog的架构模式,一个系统烂了,不是去修改这个系统,而是在旁边再建一个系统来监控它。我很难理解这种逻辑,或许是为了创造更多就业……)
在此,我坚守以下几个原则和方法,分享给大家:
与其花费巨大精力迁就技术债务,不如直接偿还技术债务。这便是所谓的长痛不如短痛。
建设没有技术债务的“新城区”,并通过“防腐层”的架构模型,阻止技术债务侵蚀“新城区”。
**原则九:依赖数据和学习,避免仅凭经验判断**
许多人向我咨询技术问题,希望我能给出答案。我通常会说,我需要先了解他们现有系统的情况,进行诊断。只有获取到这些数据,我才能理解真正的原因,才可能给出更优的技术方案。我个人认为这是一种负责任的态度,因为技术手段繁多,每种技术都有其适应场景和各种权衡取舍,因此只有经过调研才能做出决策。这如同医生诊断病情,确诊不能仅凭经验,仍需依靠诊断数据。在科学面前,一切经验都应以数据为支撑。