多年的软件从业经验,给了我一个清晰的结论,好的软件是设计出来的。
当然,不只是好的软件,好的产品也是设计出来的。
对于一个大规模的软件系统,软件的架构设计有可能是整个系统中最重要的部分,系统的性能、可维护性、可扩展性等等,都直接取决于软件的架构设计。
基于我过去的经验,特别是极光推送、云巴物联网平台等项目的实践,我想分享一些现代软件架构设计的原则、挑战和未来发展方向。
根据我们软件提供的功能和服务,每个软件根据实际情况,设计一个合适的架构。
比如,一个复杂的桌面应用程序,往往采用的是一个单体架构,所有的功能,通过动态链接库或者插件的方式,集成到一个程序中。那就需要有一个架构,来管理这些插件的生命周期和互相的调用关系。
而对于一个大型的互联网应用,更重要的是,如何设计一个架构,来支持海量用户的访问,如何保证系统的稳定性和可扩展性。
比如一个电商网站,如果大部分流量来自于商品详情的浏览,那么我们就要考虑设计一个架构,来支持这种高并发、低延迟的访问。通过 CDN,让数据尽可能靠近用户;通过缓存,减少数据的访问压力;通过负载均衡,把请求分散到不同的服务器上。
极光推送应该是国内最早,也是最大的 App 消息服务提供商。
早在安卓刚被推出的时候,我当时就觉得,安卓的原生消息推送,有在国内用不了的风险,国内的安卓手机厂商,也没有这个意愿和能力解决这个问题,国内是很有可能需要一个第三方的消息推送服务的。
后来,根据对市场的观察和一些机缘巧合,我们开始做极光推送这个事情。
极光推送面临的挑战包括:维持海量的长连接、支持高并发的消息下发,以及保证消息的实时性和可靠性。特别是安卓系统,那时候都是我们自己来维持长连接,系统的能力、SDK 的功耗,都是我们需要考虑的问题。而 iOS 要解决跟苹果的推送服务的集成问题。
根据这个情况,我们当时设计了一个比较简单的架构:
这套架构,一开始为了快速上线,考虑到团队的技术能力,部分继承了之前一个项目的代码,所以有一些不够优雅的地方,整套系统几乎都是用 C 语言或者 C + Class 的 C++ 语言写的。
一开始,全局路由表是一个基于共享内存的单体服务。负载均衡的逻辑,也是简单的根据客户端的用户 ID,取模分配。
随着用户量的增长,几乎整个架构都被重构:
当然,这个过程也发现了很多问题,比如:
到做云巴物联网平台的时候,一方面考虑到物联网场景对通信的要求更高,加上之前对架构的一些反思,我们一开始就对架构做了完全的重新设计。制定了一些原则:
开发语言上,我们也做了一些尝试。第一个版本,我们尝试用了当时很流行的 Node.js,后来发现 Node.js 在高压力下,GC 的问题很严重。后来又大量的使用了 Erlang,Erlang 的轻量级进程,运行的效果和开发效率都非常高。后来,为了减低运营成本,又用 Rust 重构了一个版本。
架构上,我们采用了微服务架构。每个服务都是一个独立的进程,通过异步消息队列来通信。
在多年的实践中,我发现有几个关键决策会深刻影响整个系统的发展方向:
在设计分布式系统时,状态管理是一个核心问题。我们在极光推送初期选择了共享内存的方案,这个决策导致后期扩展遇到了很大的困难。而在云巴平台中,我们采用了完全无状态的设计,所有状态都保存在分布式存储中,这让系统的扩展变得简单了很多。
但是无状态设计也带来了新的挑战:频繁的状态读写会给存储系统带来很大压力。为此,我们设计了多级缓存机制,在内存中保留热点数据,同时通过异步更新来保证数据的最终一致性。
在物联网平台中,通信协议的选择至关重要。我们最初选择了 MQTT 协议,因为它是一个轻量级的发布/订阅协议,非常适合物联网场景。但随着业务的发展,我们发现原生 MQTT 协议在某些场景下存在局限性,比如:
为此,我们在 MQTT 的基础上做了一些扩展,增加了自定义的消息类型和头部字段,使其更好地满足我们的业务需求。这个决策证明是正确的,它既保持了与标准 MQTT 客户端的兼容性,又满足了我们特殊的业务需求。
在架构设计中,经常被忽视但同样重要的是监控和可观测性的设计。我们在极光推送的经历让我深刻认识到这一点。当时系统出现问题时,往往需要登录到服务器上查看日志,这种方式在系统规模扩大后变得非常低效。
在云巴平台中,我们从一开始就建立了完整的监控体系:
这套系统帮助我们快速定位问题,提前发现潜在风险,大大提高了运维效率。
多年的实践,我们也发现了一些问题,在架构设计层面解决不了。
物联网设备的通信,往往是不可靠的,消息可能会丢失,可能会重复。为了解决这个问题,我们引入了消息的去重和消息的重发机制。这需要我们在云端维护每一条消息的状态,这就要求用一个读写性能都很好的数据库,强一致性,高可用性,还能支持线性扩展。而当时,我们能用的数据库,不论是关系型数据库,还是 NoSQL 数据库,或者 Cache 系统,都不能完全满足我们的需求。而为了更好的解决这类问题,真正需要的是一个针对这种场景的新的数据库系统。比如,针对这种操作频繁,但是总数据量不大的场景,我们可以用一个分布式的内存数据库,把数据都放在内存中,然后通过快照和日志的方式,来保证数据的持久化。
架构的选择,有时候跟采用的技术栈有关。比如,我们用了 Erlang,就很好的解决了高并发、低延迟的问题。但是,Erlang 的 CPU 利用率不高,内存占用也比较高。我们就需要用更大规模的微服务,来支持更多的用户。而用 Rust 重构之后,CPU 和内存的使用率都大幅提高,不再需要部署那么多的微服务,我们就可以把很多服务合并到一个进程中,整个系统架构的复杂度就大大降低了。
经过这些年的实践,我对软件架构有了一些新的思考:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。