从业二十余载,我观察到众多公司在系统架构方面面临的问题。在与这些企业沟通、交流,并参与实际方案比选与实施的过程中,我逐渐构建起一套自身独特的逻辑与方法论。我撰写此文,旨在分享这些个人经验与见解,希望能为业界提供参考,共同推动更优秀的架构设计。
我提出的这些思维方式和原则,主要针对当前市场中普遍存在的不合理架构与方案,可视为一种“纠正”。不过需注意,这些原则主要适用于业务逻辑较为复杂的系统;对于访问量较小、功能简单的应用,可能得出截然相反的结论。
**原则一:聚焦实际收益,而非纯粹技术**
在我看来,软件架构首要考虑的是其带来的实际收益。若仅仅为了技术而技术,失去了收益考量,便毫无意义可言。以下几点收益尤为关键:
系统架构能否有效降低技术门槛,加速团队整体开发进程?能够支持并行开发、上线和运维,避免团队成为瓶颈,是软件工程一直追求的目标。即便组织结构存在问题,我们的系统架构设计也应力求并行化。
系统架构能否提升整体稳定性?为提高系统的服务等级协议(SLA),必须为计划内和计划外停机制定完善的解决方案。
系统架构能否通过简化和自动化来降低运营成本?人力成本是最高昂的成本,不仅体现在高昂与缓慢,更在于频繁的人为失误。如果一个架构设计反而需要更多人力,那它无疑是失败的。此外,还需考虑时间成本和资金成本。若一个系统架构无法在这三方面发挥作用,那么它的价值便大打折扣。
**原则二:以应用服务和API为核心视角,摒弃资源与技术中心主义**
国内企业通常存在精细分工,如运维与开发(又细分为基础运维、应用运维;基础核心开发、业务开发)。不同的分工造就了迥异的视角。例如,基础运维与开发者可能更关注资源利用率和性能,而应用运维与业务开发者则侧重于应用和服务本身。分布式架构的演进模糊了传统界限,服务治理、Kubernetes等技术融合了底层基础与上层应用。这体现了DevOps的理念——当技术组件不再能明确划归Dev或Ops时,便需要融合。组织的整体提升,已无法通过优化单一分工或组件实现,而需要自上而下、统一规划的设计。为实现这一目标,所有参与者都需拥有统一的视角和目标,我认为这个目标就是——站在服务和对外API的视角审视问题,而非仅仅关注技术与底层实现。
**原则三:优先选择主流且成熟的技术**
技术选型的正确与否至关重要,一旦选错,可能导致整个架构的痛苦调整。我见过许多案例,随着系统复杂度上升,用户不得不将PHP、Python、.NET或Node.js架构迁移至Java + Go。当系统变得越发庞大和复杂,一些“玩具”技术便不再适用,取而代之的是工业级技术。
务必优先选择更成熟、更工业化的技术栈,而非仅仅是自己熟悉的技术。所谓工业化技术栈,可参考大型互联网、金融、电信等公司广泛采用的技术。这些公司拥有雄厚的技术投入和大规模生产需求,其选择的技术往往具有工业化特性。进行技术选型时,切勿被“某视频公司也在用此技术”的言论,或论坛上无数据支撑的个人喜好所左右,应关注主流公司的实际实践。
选择全球流行的技术,而非仅限于国内流行的技术。技术本身具有全球性。此外,警惕那些“特别案例”,即便它们看起来很“性感”,关键在于其解决问题的思路和采用的技术是否具有普适性。唯有普适性技术才拥有更强的生命力。
最大限度地利用主流技术的红利,避免重复造轮子,更不要魔改。我曾目睹一些公司魔改开源软件,结果“魔改”出了另一个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,再通过响应体指出是否出错。这样做最大的问题是——监控系统将低效运行。监控系统必须解析每个网络请求包才能判断是否出错,且无法区分是客户端还是服务端错误,导致重试或熔断等控制系统无从下手(4xx错误重试或熔断无意义,仅5xx有意义)。有时我感觉仿佛在倒退,错误码设计这种基础技能何以被遗弃?一家公司竟听任这种混乱行为?这些基本技能怎会失传?
我还见过一些公司,其组织内部缺乏统一的用户ID设计,各系统通过用户身份证ID(真实的身份证号)同步用户数据,甚至网关的用户白名单也使用身份证ID。我对该公司在用户隐私管理方面的隐患深感忧虑。一个企业、一个组织,若无标准、规范和抽象,必将乱象丛生。
以下列举一些需要关注的标准和规范(包括但不限于):
服务间调用协议标准与规范:如Restful API路径、HTTP方法、状态码、标准头、自定义头、返回数据JSon Schema等。
命名标准与规范:如用户ID、服务名、标签名、状态名、错误码、消息、数据库等。
日志与监控规范:如日志格式、监控数据、采样要求、报警设置等。
配置规范:如操作系统配置、中间件配置、软件包等。
中间件使用规范:如数据库、缓存、消息队列等。
软件与开发库版本统一:整个组织应每年至少进行一次软件或开发库版本升级评审,确保各团队版本统一,极大简化系统架构的复杂度。
此处重点强调两点:
Restful API规范:这极其重要。PayPal和Microsoft的规范范例我认为最为优秀。Restful API有统一标准的最大优势在于,监控系统可轻松进行统计分析,控制系统可便捷地进行流量编排与调度。
服务调用链追踪:通常参考Google Dapper论文,现有多种实现。Zipkin是其中最严谨的之一,也是Spring Cloud Sleuth的底层实现。Zipkin之所以贴近Google Dapper论文,在于其无状态特性,能快速发送Span,不占用服务应用的内存和CPU。这意味着监控系统宁愿自身故障,也绝不干扰实际应用。
软件升级:我发现许多公司包括BAT在内,缺乏体系化的软件升级活动,完全依赖开发者自发行为。然而,这种系统性活动绝不可能仅凭大众自发形成。公司应至少每年进行一次软件版本升级评审,并推动版本统一,这会极大简化系统架构的复杂性。
**原则六:重视架构扩展性与可运维性**
我 observed 很多架构师只顾当下,不考虑系统的未来扩展性和可运维性。这种“只生不养”的态度,如同生下一个有缺陷的婴儿,未来难以打理。架构与软件并非一劳永逸,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的通信协议以及过时模型等。
不合理设计:例如在网关中写入大量业务逻辑、单体架构、数据与业务逻辑深度耦合、错误的系统架构(将缓存当作数据库、用消息队列同步数据)等。
缺乏配套设施:例如没有自动化测试、良好的软件文档、高质量代码、标准和规范等。
许多人向我寻求技术帮助,我总是苦口婆心地告诉他们:“若您只求‘头痛医头脚痛医脚’的解决方案,我兴趣不大。因为,奢望将一辆夏利车简单 S 改造成法拉利,或扶正一栋地基不稳的歪楼,是不现实的。过去欠下的技术债,终究是要还的。地基没打好的要重打,配套设施缺失的要新建。若基础设施未能按科学正确的方式建立,便不可能拥有完善系统,我也无法逐一解决问题。”起初他们都表示愿意偿还技术债,但当发现债务过于庞大而难以承受时,便开始原形毕露。
他们开始为“欠下的技术债”寻找各种合理化借口——解释各种历史原因和不得已而为之的理由。这让我感觉,他们期望在不改变、不付出的前提下获得进步,甚至宁愿让新技术迁就这些技术债,使其被滥用得一塌糊涂。曾有一家公司,其系统架构和技术选型基本错误,采用错误的模型构建系统,导致性能极差,数据量仅千万级。但他们想的不是还债,不是夯实基础、完善配套,而是要盖更高的楼、上更多的系统——他们觉得现有系统不错,性能问题仅因缺乏大数据平台,所以要建设大数据平台。
我见过太多公司,包括BAT这样的大企业,都在原有技术债上继续建设,结果技术债越滚越大,利息压得喘不过气,最终沦为高利贷,再也无法偿还。我曾在《开发团队的效率》一文中提到WatchDog架构模式:一个系统烂了,不是去修改它,而是在旁边建一个系统来监控它。我很难理解这种逻辑,或许是为了创造更多就业机会。
在此,我有几个坚持的原则和方法分享给大家:
与其花费巨大精力迁就技术债务,不如直接偿还。所谓长痛不如短痛。
建设没有技术债的“新城区”,并通过“防腐层”架构模型,阻止技术债侵蚀“新城区”。
**原则九:依赖数据和学习,摒弃个人经验主义**
许多人向我咨询技术问题,期望我能给出答案。我通常需要先了解他们现有系统的情况,进行诊断。只有获取到这些数据,我才能理解真正原因,才能提供较好的技术方案。我个人认为这是对对方负责任的态度,因为技术手段众多,各有适用场景和权衡。医生看病也一样,确诊病因不能仅凭经验,仍需依赖诊断数据。在科学面前,所有经验都需数据支撑。