You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 27 | 怎么编写可持续发展的代码?
成功的大公司,也是从小公司起步的。刚开始的时候,软件可能比较简单,用户也比较少,一台廉价的服务器,或者一个简单的虚拟机,甚至几个静态的页面就绰绰有余。
很快辛苦的努力得到回报产品传播速度远超预期用户很喜欢公司的产品或者服务数量大幅增加需求越来越强劲。这时候也是公司最忙碌的时候每个人眼里只有两个字“增长”。用户规模增长随之带来的是软件规模增长运维复杂度增长。这时候廉价的服务器满足不了需求了就需要更多的服务器甚至是各种用途不一样的服务器还需要使用更多的带宽、更多的内存、更多的CPU甚至更多的硬盘。
跟得上增长的公司,用户会越来越喜欢,就会脱颖而出,每一份辛苦都得到了优厚的回报;跟不上增长的,用户会越来越抱怨,公司会被迅速模仿,然后用户被抢走,公司被迅速甩开,每一份辛苦都成了一声叹息。
增长对软件的要求就是要有处理越来越多工作和越来越大规模的能力或者潜力。这种能力通常称之为可伸缩性Scalability
不过要提醒的是也有人使用“可扩展性”这个词表示规模的扩张能力。可扩展性这个词汇很多时候也用于表示功能的扩展Extensibility。这就容易混淆规模扩展和功能扩展这两个完全不一样的概念。如果有人使用了可扩展性这个概念要弄清楚指的是规模还是功能。
为了方便理解减少混淆我们使用更通俗一点的词汇来表达这两个概念。这两个词汇就是规模扩张能力Scalability和功能扩展能力Extensibility
规模扩张能力,是依赖于具体的代码的。不是所有的代码都能适应规模的扩张。这一次,我们就来讨论代码的规模扩张能力,以及一些常见的陷阱。
## 两种规模扩张方式
规模扩张主要有两种方式。一种方式是规模垂直扩张scale in/out另一种是规模水平扩张scale up/down
规模垂直扩张指的是提高同一个处理单元处理更多负载的能力。比如硬件上增加服务器的硬盘、内存和CPU软件上优化算法、程序和硬件的使用等。
规模垂直扩张是传统的提高负载的方式方式方法都比较直观效果也立竿见影。但是规模垂直扩张成本很高而且是非线性的。比如说4倍的CPU可能只提高2倍的负载而8倍的CPU可能只提高2.5倍的负载。
另外规模垂直扩张是有上限的一台服务器的处理能力不是可以无限扩展的。还有硬件的规模扩张可能要更换机器停止软件运行。这种规模扩张方式不太适用于可用性要求高的产品。比如我们常用的微信出现5分钟的停机都是天大的事故。
规模水平扩张,指的是通过增加更多的处理单元,来处理更多的负载。我们常见的例子,就是增加服务器。分布式系统、负载均衡、集群系统这些技术,提供的就是规模水平扩张的能力。
优秀的规模水平扩张技术,可以使用很多廉价的机器,提供大规模的计算能力。一般情况下,规模水平扩张的成本要低于规模垂直扩张。而且,如果其中一个节点出了问题,只要其他节点还在正常工作,整个系统也可以照常运转。如果想要添加一个新节点,系统也不需要停顿。规模水平扩张技术的这些特点,非常适用于高可用性系统。
和规模垂直扩张相比,规模水平扩张的方式方法并不直观。支持规模水平扩张的代码要能够协调地运行在不同的机器上,也就是说要支持分布式计算。很多代码,不是天生就支持分布式计算的,而且修改起来也不容易。
我们常说的优化代码,一般指的是提高每一个处理单元的使用效率,也就是规模垂直扩张能力。其实,我们还要考虑代码是不是能够分布式运行,也就是规模水平扩张能力。
## 麻烦的状态数据
影响代码水平规模扩张的最重要的一个因素就是用户的状态数据。比如用户的登录状态用户购物车里的商品信息HTTP连接里缓存的会话数据等。
如果用户访问一个服务节点时,在节点留下了状态。这个状态就要在多个节点间同步。否则,如果用户下一次访问被分配到不同的服务节点,这个状态就会消失不见。比方说吧,上午,我们在一个网站购物,把待选的商品放到购物车里。这个选择商品的过程,可能是由位于北京南城的数据中心的一台服务器处理的。下午,我们准备结账,重新访问这个购物网站。这时候,服务器可能是由位于贵州的数据中心提供的。如果上午访问和下午访问的服务器之间没有同步购物车数据,下午访问时,购物车里就没有我们想要的信息了。
购物车的状态同步,可以通过分布式数据库来解决。分布式数据库自动处理多个节点之间的数据同步。
现在的软件服务大都是基于HTTP协议提供的Web服务。Web服务本身就是一个无状态的协议。即使可以保持HTTP的连接一般的服务框架也会考虑在连接不能保持情况下的会话管理也就是保存用户状态。HTTP协议层面的状态管理也需要支持分布式计算。搭建支持规模水平扩张的Web服务时要做好Web服务框架的选型。
如果我们的代码里保存了状态数据可能会影响规模水平扩张的能力。比如说下面的这个例子中的sessionCache这个静态变量如果用来保存用户的会话并且使用SessionId匹配用户行为规模水平扩张时就会遇到麻烦。因为这个变量内容的更改只存在于运行它的节点里不能在一个分布式系统的每个节点之间同步。
```
private static final HashMap<SessionId, byte[]> sessionCache = new HashMap();
```
对于规模水平扩张的需求,状态数据是一个很麻烦的存在。甚至,一些基础的,需要保存状态数据的网络协议,在早期的版本中也没有考虑规模水平扩张的问题。这就给规模水平扩张带来了一定的困难。
所以,采用规模水平扩张时,一定要小心代码的状态数据能不能同步。另外,由于软件依赖的基础设施问题,还要测试软件的运行平台是否能够很好地支持规模水平扩张。
## 无状态数据
如果一个服务是无状态的规模水平扩张就会非常顺利。比如说静态的网页静态的图片静态的商品描述静态的JavaScript和CSS文件等等。由于不需要在服务端保留状态这些数据资源就不需要在不同的节点间实时同步。无状态的数据可以降低规模水平扩张的技术复杂性在技术上有了更多的改进空间。
比如说现代的浏览器都可以缓存静态数据比如说静态的JavaScript和CSS文件。如果用户访问的两个不同网站使用了相同的脚本文件。浏览器只需要下载一次脚本文件就可以在两个网站使用。这样缓存的脚本文件就可以加速网页的加载减轻服务器的压力。
![](https://static001.geekbang.org/resource/image/fd/8d/fd94ae22ae2751b87d8e0ca81d2bcf8d.png)
## 分离无状态数据
由于无状态数据有这么多的优点,把无状态数据分离出来,单独提供服务就成了一个常用的解决方案。独立的无状态数据服务,既没有规模水平扩张的羁绊,还能充分利用客户端的缓存。另外,无状态数据和状态数据的分离,可以让状态数据的处理集中资源,也能提高状态数据的处理能力。
比如说一个网站如果使用了共享的jquery.js脚本下载这个脚本就不再占用这个网站的资源了。
所以,如果你要设计一个具有规模扩张能力的软件架构,分离无状态数据和状态数据,既可以提高规模水平能力,也可以提高规模垂直扩张能力。
## 使用用户资源
对静态数据进行缓存是现代浏览器提高服务性能的一个重要办法。除此之外浏览器还可以缓存动态数据比如HTTP的cookie以及HTTPS的安全连接的参数。
鉴于无状态数据的诸多优点一些协议设计开始考虑无状态的服务场景。比如TLS 1.3就加入了对无状态服务的支持。
无状态服务,并不一定都没有服务状态。一个典型的做法是,服务端通过一定的数据保护机制,把服务状态保护起来,发送到客户端。然后,客户端缓存封印的服务状态。下次连接时,客户端把封印的服务状态原封不动地送回到服务端。然后,服务端解封客户端发送的封印服务状态,就获得了需要处理的状态数据。这样,既有了无状态服务的便利,解除了规模水平扩张的限制,又解决了服务需要状态的客观需求。
遗憾的是这种设计能够提供的服务状态数据尺寸比较有限应用场景也比较苛刻而且数据保护机制一般也比较复杂。所以我们一般要在基础架构层面解决掉核心的问题数据保护机制、服务状态封存机制、缓存机制等。然后在应用层面仅仅定制状态的内容比如HTTP的cookie的格式和数据就是可以定制的内容。而HTTP的cookie交换的机制就由HTTP协议负责解决。
## 小结
如果把我们上面讨论的放到一起,就可以得到具有规模扩张能力的软件的一些最佳实践:
1. 把无状态数据分离出来,单独提供无状态服务;
2. 把最基本的服务状态封装起来,利用客户端的缓存,实现无状态服务;
3. 小心使用服务状态,编码时要考虑服务状态的规模水平扩张能力。
基于上述的原则,市场上有很多优秀的解决方案和成熟技术。欢迎你在留言区分享、讨论这些解决方案和技术。
## 一起来动手
这一次的练手题我们要拆解一下Web页面。找一个你常用的Web服务比如说InfoQ或者极客时间。使用浏览器的插件阅读这个HTML页面试着分析下这个页面里哪些可能是动态数据哪些可能是静态数据这个页面是怎么处理这些数据的使用我们今天讨论的基本原则这个页面还有没有优化的空间如果你侧重于服务端的编码你想想服务器端该做什么样的调整
欢迎你在留言区留言,分享你的看法。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起交流一下。