在二十多年的职业生涯中,我接触了各式各样的公司系统架构,也发现了诸多亟待解决的问题。在与这些企业进行沟通、讨论、实施以及方案选型时,我经历了无数次的权衡与妥协。随着经验的积累,我逐步形成了自己独特的逻辑和方法论,并希望通过此文总结这些心得,以期为同行提供参考借鉴,共同构建更为出色的系统架构。值得注意的是,我的这些思考和原则主要针对当前市场上普遍存在的不合理架构和方案,可视为一种“纠偏”。
需要强调的是,本文所阐述的架构原则主要适用于业务相对复杂的系统。对于那些功能简单、访问量不大的应用,遵循这些原则可能会得出相反的结论。
**原则一:以实际收益为导向,而非盲目追求技术本身**
对于软件架构而言,其首要价值在于能否带来实实在在的收益。脱离收益,单纯为了技术而技术,将毫无意义。在我看来,以下几点是衡量技术收益的关键维度:
架构能否降低技术门槛,加速团队整体开发进程?工程实践持续追求的目标是提升团队效率、实现快速发布。因此,系统架构应支持并行开发、并行上线与并行运维,避免任何团队成为瓶颈。即便团队效率受限于组织结构,我们仍应设计支持并行操作的系统架构。
架构能否有效提升系统稳定性?为增强系统整体稳定性,提高服务等级协议(SLA),必须针对计划内和计划外停机制定完善的解决方案。
架构能否通过简化与自动化降低运营成本?最高效的成本优化是人力成本,因为人为操作不仅慢、贵,还易产生失误。如果架构设计无法降低人力成本,反而需要更多投入,那它无疑是失败的。此外,时间成本和资金成本也同样重要。
若系统架构无法在上述三方面发挥积极作用,其存在的意义便值得商榷。
**原则二:以应用服务和API为核心视角,而非资源或技术**
国内许多公司常按职能进行细致划分,如运维(含基础运维与应用运维)和开发(含基础核心开发与业务开发)。不同的分工导致了视角和关注点的差异。例如,基础运维和开发者更侧重资源利用率和性能,而应用运维和业务开发者则更关注应用和服务层面。过去两者相对独立,但随着分布式架构的演进,一些系统组件已难以界定其归属。例如,服务治理既包含底层基础技术,也需业务团队配合;Kubernetes同样如此,既涉及网络等底层技术,也要求业务配合readiness、liveness等健康检查,以及使用ConfigMap等。这些现象让我深刻体会到,DevOps的兴起正是因为许多技术和组件已不再纯粹属于Dev或Ops范畴,亟需融合。当前,组织和架构的优化,已无法通过单一职能或单一组件的调优获得显著提升。它需要自顶向下的整体规划与统一设计,方能实现全局性改善。就像城市交通优化一样,当城市规模达到一定程度时,仅优化几条道路或街区无济于事,必须对整个城市的功能体进行整体规划才能提高效率。为实现整体提升,所有参与者必须保持统一的视角和目标。近几年我发现,这个共同的目标就是——始终以服务和对外API的视角审视问题,而非仅仅关注技术细节或底层实现。
**原则三:选用主流且成熟的技术栈**
技术选型至关重要,一旦失误,可能导致整个架构的痛苦调整。过去几年,我多次见到公司将PHP、Python、.NET或Node.js架构迁移至Java + Go架构的案例。这个过程虽然艰难,但在系统日益复杂和庞大时,你就不能再使用“玩具”技术,而必须转向更具工业化水准的技术。
应尽可能采用成熟且工业化的技术栈,而非仅限于自己熟悉的技术。所谓工业化技术栈,可参考互联网、金融、电信等大型公司普遍采用的技术。大公司投入更多,规模生产需求更高,故其技术通常更具工业化特性。技术选型切忌被“某视频公司也在用此技术”或论坛上基于个人喜好而非数据的吐槽所左右,应以主流公司实际应用为更可靠的依据。
优先选择全球流行的技术,而非仅限于地域性流行的技术。技术本身是全球性的,而非局限性的。因此,国际化的技术生命力更强。同时,切勿被某些公司的“特殊案例”所迷惑,即便案例再吸引人,关键在于解决问题的思路和所用技术是否具有普适性,唯有普适性技术才具备更强的生命力。
尽可能利用主流技术的红利,避免重复造轮子或进行“魔改”。我曾见过不少公司魔改开源软件,比如有公司魔改Mesos,结果改出来一个类似Kubernete的系统。我也见过很多公司或技术团队热衷于发明自己的“专用轮子”,但最终常被主流开源软件取代。这完全没有必要。不重复造轮子和不魔改,并非因为自身技术能力不足,而是因为当今时代已不再是事事亲力亲为的时代,而是要努力融入并与整个产业、技术社区融合协作,才能获得最大收益。那些试图为特殊情况而自成一套的方法,短期或可行,但长期来看,我并不看好。
在绝大多数情况下,除非有非常特殊的需求,选择Java几乎不会出错。一方面,Java在业务开发方面拥有出色的生产力,并且在Spring框架的保障下,代码质量不易劣化。另一方面,Java社区极其成熟,各种所需架构和技术均易于获取,技术红利巨大。这种运行在JVM上的语言具备诸多优势。从长远来看,基于Java技术栈的架构,其风险和成本(包括人力、时间与资金)都是最优的。
在我所接触的公司中,不乏架构决策受技术负责人个人喜好、专长和经验绑架的情况,未能从客观角度进行技术选型。的确,在从0到1的起始阶段,任何技术都可能适用。若仅是开发一个简单的应用,无需事务处理或复杂交易流程,如论坛、社交类应用,任何语言皆可胜任。然而,当系统日益复杂、需要处理大量交易、规模从小变大、开发团队不断扩张时,你可能会发现,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中指示是否出错(前两年我在微信公众号上还看到一位小有名气的互联网老兵推荐这种无论对错都返回200的做法,后台再三确认后,我深感此类架构师贻害不浅)。这种做法的最大问题在于,监控系统将在低效状态下运作。监控系统需要逐一解析所有网络请求包才能判断是否出错,且完全无法区分是客户端还是服务器端错误,从而导致重试或熔断等控制系统无从下手(若是4xx错误,重试或熔断毫无意义;只有5xx才有意义)。有时,我真有一种越活越退步的感觉,错误码设计这种最基本的东西为何会缺失?一个公司竟会任由大家随意乱来?这些基础技能怎么就这样丢掉了?
此外,我还见过某些公司,其整个组织缺乏统一的用户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等架构最佳实践,以及分布式系统架构相关实践。
**原则七:全面收敛控制逻辑**
所有程序皆包含两种逻辑:业务逻辑与控制逻辑。业务逻辑旨在完成具体业务功能,而控制逻辑则为辅助性,例如多线程、分布式技术、数据库或文件存储的选择、配置、部署、运维、监控、事务控制、服务发现、弹性伸缩、灰度发布、高并发等。这些皆属控制逻辑,与业务逻辑无直接关联。控制逻辑的技术深度通常高于业务逻辑,门槛也更高。因此,最好由专业的程序员负责控制逻辑的开发,进行统一规划、管理与收敛。这包括:
流量收敛。涵盖南北向和东西向流量的调度,主要通过流量网关、开发框架SDK或Service Mesh等技术实现。
服务治理收敛。包括服务发现、健康检查、配置管理、事务、事件、重试、熔断、限流等,主要通过开发框架SDK(如Spring Cloud)或服务网格Service Mesh等技术实现。
监控数据收敛。包括日志、指标、调用链等,主要通过标准主流的探针以及后台的数据清洗和存储来完成,最好采用无侵入式技术。监控数据必须统一在一个地方进行关联,才能产生有效信息。
资源调度与应用部署的收敛。包括计算、网络和存储的收敛,主要通过容器化方案,如Kubernetes来完成。
中间件收敛。包括数据库、消息队列、缓存、服务发现、网关等。这类收敛方式通常需要在企业内部统一建立共享的云化中间件资源池。
对此,我的原则是:
选择易于实现业务逻辑与控制逻辑分离的技术。在这方面,Java的JVM结合字节码注入和AOP式的Spring开发框架,将带来巨大优势。
选择能够享受“前人种树,后人乘凉”技术红利的技术。例如,拥有庞大社区且相互兼容的技术,如Java、Docker、Ansible、HTTP、Telegraf/Collectd等。
中间件应选用支持高可用集群和多租户技术。目前,绝大多数主流中间件已支持HA集群模式。
**原则八:不迁就陈旧系统的技术债务**
我发现许多公司背负着沉重的技术债务,具体表现如下:
技术老旧。例如,仍在使用HTTP 1.0、Java 1.6、Websphere、ESB,基于Socket的通信协议,以及过时的模型等。
设计不合理。例如,网关中编写大量业务逻辑、单体架构、数据与业务逻辑深度耦合、错误的系统架构(如将缓存当数据库使用,或用消息队列同步数据)等。
配套设施缺乏。例如,没有自动化测试、良好的软件文档、高质量代码、标准和规范缺失等。
求助者带着形形色色的问题来寻求技术支援。我总是苦口婆心地对他们重复同样的话:“如果你们只是想寻求个案式的解决方案,我兴致不大。因为,你们绝不应指望能轻易地将一辆夏利车改造成法拉利跑车,或是扶正一栋地基不牢的歪楼。过去欠下的技术债,终究是要还的;没打好的地基要重新夯实,缺失的配套设施都要补齐。如果这些基础设施未能按照科学正确的方式建立,你们不可能拥有一个良好的系统,我也无法为你们提供个案式的解决方案……”起初,他们都会信誓旦旦地说,没问题,我们就是要还债。然而,当他们发现需要偿还的债务之多,有些难以承受时,便会原形毕露。
他们开始为“欠下的技术债务”寻找各种合理的理由——向你解释各种历史原因和不得已而为之的苦衷。谈及此处,我有一种感觉——他们似乎希望不付出任何改变和努力就能进步,甚至宁愿让新技术迎合这些技术债务,使其被滥用得一塌糊涂。曾有一家公司,其系统架构和技术选型基本都是错误的,使用了错误的模型构建系统,导致整个系统性能奇差,即使数据量只有几千万条。然而,他们想的并非还债,并非夯实地基和完善配套设施,而是要将楼建得更高,上马更多系统——他们认为现有系统挺好,性能问题的原因是缺乏大数据平台,所以要建设大数据平台……
我见过很多公司,包括像BAT这样的大企业,都在原有技术债务的基础上进行更多建设,结果技术债务越积越多,利息也越来越高,最终变成一笔无法偿还的高利贷。在我看来,一个系统如果破败了,正确的做法不是去修修补补,而是在旁边建立一个新系统来取代它。我很难理解为什么会有这种逻辑,也许是为了创造更多的就业机会吧。
关于此点,我坚持以下几个原则和方法,分享给大家:
与其费尽心力迁就技术债务,不如直接偿还。所谓长痛不如短痛。
建设无技术债务的“新城区”,并通过“防腐层”的架构模型,阻止技术债务渗透至“新城区”。
**原则九:依赖数据和学习,而非个人经验**
曾有不少人向我咨询他们的技术难题,希望能得到一个确切答案。我常回应说,我需要先了解他们现有系统的具体情况,即进行一番诊断。唯有获取这些数据后,我才能明晰问题的真正根源,进而提出一套相对完善的技术方案。我个人认为,这是一种对求助者负责的态度。因为技术手段千变万化,每种技术都有其适用的场景和相应的权衡取舍,所以,唯有经过充分调研才能做出最终决定。这就像医生看病一样,诊断病因不能仅凭经验,更要依赖诊断数据。在科学面前,一切经验都需数据支撑。