创业公司的技术挑战
托尔斯泰说:“幸福的家庭都是相同的,不幸的家庭各有各的不幸。”互联网创业公司也一样。大部分互联网创业公司,都会碰到以下几个技术挑战。
- 如何快速、低成本的搭建系统,同时确保安全稳定?
- 如何快速的构建和发布应用,满足业务需求?
- 如何提高团队开发效率,确保开发质量?
这个列表肯定不完整,但这三个应该是创业公司技术团队都会面临的共通的问题。当然杏仁不能说完全解决了这几个问题,但还是取得了一些进展。我们接下来简单介绍下,我们杏仁是怎么应对这些挑战的,以及容器又可以带来什么?
该系列文章会分为三篇。第一篇介绍容器化之前,杏仁技术架构的发展历史。第二篇介绍容器以及杏仁的容器化方案。第三篇最后总结为什么我们认为创业公司应该用容器,以及为什么容器可以帮助我们应对这三个挑战。
杏仁早期
在 2012 年以前,大部分互联网公司包括创业公司,都是直接购买服务器,租用 IDC 机房的机架部署的。应用是直接运行在物理机上的,要扩展必须购买新服务器。IDC 经常出各种故障,如果碰到 IDC 迁移的话,就更痛苦,必须半夜搬机器,天亮前上线。总之对创业公司的成本、服务稳定性、工作效率都是有很大的消耗。
不过杏仁医生很幸运,正好赶上了公有云的成熟,所以一开始就是基于公有云搭建的。杏仁医生最早期的架构如下:
这个架构非常简单,其中负载平衡、数据库都是基于腾讯云的。然后腾讯云也提供了一些基础的监控、告警和安全服务。然后就是两个应用,一个移动后端 API,一个运营平台,都是基于 Play 的 Scala 应用。
很多人可能会好奇为什么选择 Scala/Play 进行开发,毕竟 Scala 在国内应该用的不多。这里一方面因为杏仁医生是继承了看处方的架构,当初看处方就是基于 Scala/Play 开发的, 团队对这一套方案比较熟悉。我们需要快速的构建杏仁医生,自然就会选择最熟悉的语言和框架。而且对于中小规模的应用,Scala/Play 的开发效率的确非常高。Scala 本身的表达能力非常强,是一门很有意思的语言。很多好学的工程师,也会对新的语言也会比较感兴趣。
应用拆分和CI/CD
经过一年多快速演进,整个应用越来越庞杂。所以我们对应用进行了拆分,并且随着业务扩张,应用也越来越多,例如 HIS、CRM 等。所以我们的架构变成了这个样子。
这时 Scala 最大的问题开始体现出来了,那就是编译速度的问题。那时候我们的应用部署方式也很原始,必须登陆服务器运行一个 Shell 脚本,它会拉取代码,然后编译、打包、运行,整个过程需要耗费5~10分钟。而我们 API 应用的节点后来增加到 5、6 台,即使两台同时发布,也需要 20 分钟才能全部发布完成。如果发布后出了问题,那就吐血了,因为回滚也是一样的流程,又需要 20 分钟。
有一次我们做了一个送 Apple Watch 的活动,半夜 12 点开始。我们出了个很低级的 BUG,活动一开始就蹭蹭蹭的一分钟送好几个 Apple Watch。我们创业公司也没多少钱,每一个都是白花花的银子,心痛啊。修复很简单,但发布或者回滚都得先编译,太慢了,于是我们一狠心把服务器给停了,几分钟后才部署上了新的代码。
这是我们觉得必须要有一个自动化的发布系统了。其实在几年以前,发布都是需要运维执行的,研发提交给运维,运维手动部署。那自然发布不可能很频繁,并且对开发和运维都是很大的负担。但渐渐的敏捷和 Devops 的文化成为主流,持续集成和发布(CI/CD)成为一项基础设施。
我们第一版 CI/CD 很简单,是基于 Jenkins 的,通过脚本进行编译、打包,然后拷贝到服务器上发布。因为只要打包一次即可,缓解了部署慢的问题。但还是存在几个问题。
- 首先,没有应用仓库。打包是一次性的,部署的时候会备份当前应用目录,用于回滚,所以只能回滚到上一个版本。
- 其次,健康检比较简单,只能检测应用是否启动。我们遇到过应用启动了,测活也没问题,但服务还是有严重问题、基本不可用的情况。
- 最后,不支持灰度发布,出问题只能回滚。
期间我们有一次较大的故障,也是因为这几个因素,花了很长时间才恢复。痛定思痛,于是我们又开发了 Frigate 发布系统,它的架构大致如下图。
- Frigate 有一个应用仓库,即 App Repository。应用仓库会保存发布的应用版本,回滚的时候可以指定版本。
- Watcher 组建实现了比较强大的应用检测功能。除了一般的 HTTP 检测,还可以从日志、监控里获取数据,可以根据异常数、错误率等进行进一步的健康检测。
- Frigate 支持分组和分阶段发布。例如现发布2台机器,然后健康检查,或者中间可以有一些人工检查,然后再发布剩余的机器。
后来回头看,Frigate 虽然没有使用容器,但其实是实现了容器编排的很多功能。Frigate 发布的截图如下,这是基于 Jenkins Pipeline 的。
微服务化
系统多了,依赖复杂、数据没有隔离、逻辑重复,接下来一个必然的方向就是微服务。关于微服务,我们公众号有两篇文章(乐高式微服务化改造(上)、乐高式微服务化改造(下))对此有比较详细的分析说明,这里就简单介绍一下。
我们的服务注册和发现是基于 Consul 的,负载平衡是通过 Nginx 实现的。下图是整个服务注册和发现的过程:
有几点是值得一提的。
首先,我们的微服务是基于 HTTP 和 Json 的,没有采用二进制的协议如 Protobuf、Thrift 等。其实 HTTP 和二进制协议的性能差别,并没有很多人想的那么大,一般也就2、3倍的差距(没有亲测)。对大部分企业,这个差别根本就不是瓶颈,特别是现在还有 HTTP2。如果真的有需要,还可以在 HTTP2 上跑二进制协议,通过框架在服务端和客户端加一层就可以实现。
其次,我们的微服务对应用是无侵入的。我们没有采用常见的 Dubbo、SpringCloud 框架。一方面我们服务调用方有 Java 应用也有 Scala 应用,要接入还是要花点功夫。另一方面,我们认为微服务框架发展的未来方向是非侵入性的独立的微服务基础设施层。其实这和容器编排的理念是一致的,并且最近提出的 Service Mesh 概念,就是进一步的延伸,我们认为这才是微服务的未来。
最后,我们每个微服务都会生成一个 SDK,便于调用方调用。SDK 集成了熔断、异步、分布式追逐(开发中)等功能。
搭建了微服务基础框架后,我们开发了好几个微服务,有业务的例如订单、预约等,有基础设施的例如推送、短信等。当然其实有些并不算“微”。
但是我们发现,整个体系依然存在不少问题
- 基于云服务,成本低了,效率高了。但运维还是面向资源的,并且资源利用率不高。
- 有了持续集成和部署的能力。但新增节点、新建服务等,依然需要大量人工运维,并且扩展并不方便。
- 实践微服务,改进了应用架构。但依赖管理、监控等尚未完善,稳定性仍然不够。
下一篇,我们将介绍杏仁是怎么使用容器和容器编排来优化这些问题的。