前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >基于十余年实践的现代软件架构探讨

基于十余年实践的现代软件架构探讨

原创
作者头像
Tiger 张虎
修改2024-12-24 20:18:04
修改2024-12-24 20:18:04
6040
举报

引言

多年的软件从业经验,给了我一个清晰的结论,好的软件是设计出来的。

当然,不只是好的软件,好的产品也是设计出来的。

对于一个大规模的软件系统,软件的架构设计有可能是整个系统中最重要的部分,系统的性能、可维护性、可扩展性等等,都直接取决于软件的架构设计。

基于我过去的经验,特别是极光推送、云巴物联网平台等项目的实践,我想分享一些现代软件架构设计的原则、挑战和未来发展方向。

软件架构的基本概念

根据我们软件提供的功能和服务,每个软件根据实际情况,设计一个合适的架构。

比如,一个复杂的桌面应用程序,往往采用的是一个单体架构,所有的功能,通过动态链接库或者插件的方式,集成到一个程序中。那就需要有一个架构,来管理这些插件的生命周期和互相的调用关系。

而对于一个大型的互联网应用,更重要的是,如何设计一个架构,来支持海量用户的访问,如何保证系统的稳定性和可扩展性。

比如一个电商网站,如果大部分流量来自于商品详情的浏览,那么我们就要考虑设计一个架构,来支持这种高并发、低延迟的访问。通过 CDN,让数据尽可能靠近用户;通过缓存,减少数据的访问压力;通过负载均衡,把请求分散到不同的服务器上。

极光推送的架构设计实践

极光推送应该是国内最早,也是最大的 App 消息服务提供商。

早在安卓刚被推出的时候,我当时就觉得,安卓的原生消息推送,有在国内用不了的风险,国内的安卓手机厂商,也没有这个意愿和能力解决这个问题,国内是很有可能需要一个第三方的消息推送服务的。

后来,根据对市场的观察和一些机缘巧合,我们开始做极光推送这个事情。

极光推送面临的挑战包括:维持海量的长连接、支持高并发的消息下发,以及保证消息的实时性和可靠性。特别是安卓系统,那时候都是我们自己来维持长连接,系统的能力、SDK 的功耗,都是我们需要考虑的问题。而 iOS 要解决跟苹果的推送服务的集成问题。

根据这个情况,我们当时设计了一个比较简单的架构:

  • 接入服务器。用于维持长连接,跟客户端收发消息;
  • 全局路由表。用于记录每个客户端连接的接入服务器和在线状态;
  • 消息服务器。用于管理客户端的标签、别名、消息的下发等;
  • 负载均衡器。用于分配客户端连接的接入服务器。

这套架构,一开始为了快速上线,考虑到团队的技术能力,部分继承了之前一个项目的代码,所以有一些不够优雅的地方,整套系统几乎都是用 C 语言或者 C + Class 的 C++ 语言写的。

一开始,全局路由表是一个基于共享内存的单体服务。负载均衡的逻辑,也是简单的根据客户端的用户 ID,取模分配。

随着用户量的增长,几乎整个架构都被重构:

  • 全局路由表从单体服务,改成了集群;
  • 负载均衡器,加入了对接入服务器的健康检查;
  • 加入了分布式消息队列,用于解耦消息的下发和接收;

当然,这个过程也发现了很多问题,比如:

  • 用 C 语言来写高并发的 IO 系统,开发效率非常低。当时为了加一个分布式队列的支持,花了 3 个多月的时间,最后的效果也不是很理想。写业务逻辑,开发效率也不高;
  • 分布式的缓存是一个很大的挑战。Redis 的集群,数据的一致性有问题。集群的扩容、缩容,都是一个很大的问题;
  • 关系型数据库也是一个很大的挑战。当时我们用的是 MySQL,数据量大了之后,读写性能都有问题。如果自己做分表、分库,又带来很多其他问题。也基本没什么可维护性。

云巴物联网平台的架构设计实践

到做云巴物联网平台的时候,一方面考虑到物联网场景对通信的要求更高,加上之前对架构的一些反思,我们一开始就对架构做了完全的重新设计。制定了一些原则:

  • 所有的服务都能够水平扩展。不能出现单点失效,能够随着用户量的增长,线性扩展;
  • 服务之间的通信,采用异步的消息队列为主。这样,服务之间只通过消息队列耦合,开发测试、在线扩容、回滚等等都会变得非常简单;

开发语言上,我们也做了一些尝试。第一个版本,我们尝试用了当时很流行的 Node.js,后来发现 Node.js 在高压力下,GC 的问题很严重。后来又大量的使用了 Erlang,Erlang 的轻量级进程,运行的效果和开发效率都非常高。后来,为了减低运营成本,又用 Rust 重构了一个版本。

架构上,我们采用了微服务架构。每个服务都是一个独立的进程,通过异步消息队列来通信。

架构设计中的关键决策

在多年的实践中,我发现有几个关键决策会深刻影响整个系统的发展方向:

状态管理的选择

在设计分布式系统时,状态管理是一个核心问题。我们在极光推送初期选择了共享内存的方案,这个决策导致后期扩展遇到了很大的困难。而在云巴平台中,我们采用了完全无状态的设计,所有状态都保存在分布式存储中,这让系统的扩展变得简单了很多。

但是无状态设计也带来了新的挑战:频繁的状态读写会给存储系统带来很大压力。为此,我们设计了多级缓存机制,在内存中保留热点数据,同时通过异步更新来保证数据的最终一致性。

通信协议的取舍

在物联网平台中,通信协议的选择至关重要。我们最初选择了 MQTT 协议,因为它是一个轻量级的发布/订阅协议,非常适合物联网场景。但随着业务的发展,我们发现原生 MQTT 协议在某些场景下存在局限性,比如:

  • 不支持请求/响应模式
  • QoS 2 级别的实现成本过高
  • 协议扩展性有限

为此,我们在 MQTT 的基础上做了一些扩展,增加了自定义的消息类型和头部字段,使其更好地满足我们的业务需求。这个决策证明是正确的,它既保持了与标准 MQTT 客户端的兼容性,又满足了我们特殊的业务需求。

监控和可观测性

在架构设计中,经常被忽视但同样重要的是监控和可观测性的设计。我们在极光推送的经历让我深刻认识到这一点。当时系统出现问题时,往往需要登录到服务器上查看日志,这种方式在系统规模扩大后变得非常低效。

在云巴平台中,我们从一开始就建立了完整的监控体系:

  • 分布式追踪:使用 OpenTelemetry 实现请求的全链路追踪
  • 指标收集:每个服务都暴露标准的 Prometheus 指标
  • 集中式日志:使用 ELK 栈进行日志收集和分析
  • 告警系统:基于多维度的指标设置智能告警阈值

这套系统帮助我们快速定位问题,提前发现潜在风险,大大提高了运维效率。

架构设计的挑战

多年的实践,我们也发现了一些问题,在架构设计层面解决不了。

物联网设备的通信,往往是不可靠的,消息可能会丢失,可能会重复。为了解决这个问题,我们引入了消息的去重和消息的重发机制。这需要我们在云端维护每一条消息的状态,这就要求用一个读写性能都很好的数据库,强一致性,高可用性,还能支持线性扩展。而当时,我们能用的数据库,不论是关系型数据库,还是 NoSQL 数据库,或者 Cache 系统,都不能完全满足我们的需求。而为了更好的解决这类问题,真正需要的是一个针对这种场景的新的数据库系统。比如,针对这种操作频繁,但是总数据量不大的场景,我们可以用一个分布式的内存数据库,把数据都放在内存中,然后通过快照和日志的方式,来保证数据的持久化。

架构的选择,有时候跟采用的技术栈有关。比如,我们用了 Erlang,就很好的解决了高并发、低延迟的问题。但是,Erlang 的 CPU 利用率不高,内存占用也比较高。我们就需要用更大规模的微服务,来支持更多的用户。而用 Rust 重构之后,CPU 和内存的使用率都大幅提高,不再需要部署那么多的微服务,我们就可以把很多服务合并到一个进程中,整个系统架构的复杂度就大大降低了。

未来的思考

经过这些年的实践,我对软件架构有了一些新的思考:

  1. 简单性是最重要的原则:复杂的架构往往会带来更多的问题。在云巴平台后期,我们通过合并服务、简化流程,反而获得了更好的性能和可维护性。
  2. 技术选型要考虑长期成本:新技术可能带来短期优势,但要考虑长期维护成本。比如我们使用 Erlang 的经历,虽然开发效率很高,但招聘和培训成本也很高。
  3. 架构要适应业务发展:好的架构应该能够随着业务的发展而演进。这需要在初期就预留足够的扩展性,同时保持架构的简单性。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 软件架构的基本概念
  • 极光推送的架构设计实践
  • 云巴物联网平台的架构设计实践
  • 架构设计中的关键决策
    • 状态管理的选择
    • 通信协议的取舍
    • 监控和可观测性
  • 架构设计的挑战
  • 未来的思考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档