大型网站系统与 Java 中间件开发实践

阿姆达尔定律(Amdahl’s law)告诉我们,程序中可并行代码的比例决定你增加处理器所能带来的速度提升上限,是否能达到这个上限,还取决于很多其他因素。

例如,当 P=0.5 时,我们可以计算出速度提升的上限就是 2。而如果 P=0.2,速度提升的上限就是1.25。可见,在多核的时代,并发程序的开发或者说提升程序的并发性是多么重要。

$$S(N) = \frac{1}{(1-P)+\frac{P}{N}}$$

其中,P 指的是程序中科并行部分的程序在单核上执行时间的占比,N 表示处理器的个数(总核心数)。S(N) 是指程序在 N 个处理器相对在单个处理器中的速度提升比。


对于单机系统来说,我们如果不使用多进程方式的话,基本不会遇到独立的故障。就是说在单机系统上的单进程程序,如果是机器问题、OS问题或者程序自身的问题,基本的结果就是我们的程序整体不能用了,不会出现一些模块不行另一些模块可以的情况。而在分布式系统中,整个系统的一部分有问题而其他部分正常是经常出现的情况,我们称之为故障独立性。我们在实现分布式系统的时候,必须要找到应对和解决故障独立性的办法。


搜索集群(Search Cluster)的使用方式和读库的使用方式是一样的。只是构建索引的过程基本都是需要我们自己来实现的。可以从两个维度对于搜索系统构建索引的方式进行划分,一种是按照全量/增量划分,一种是按照实时/非实时划分。全量方式用于第一次建立索引(可能是新建,也可能是重建),而增量方式用于在全量的基础上持续更新索引。当然,增量构建索引的挑战非常大,一般会加入每日的全量作为补充。实时/非实时的划分方式则体现在索引更新的时间上了。我们当然更倾向于实时的方式,之所以有非实时方式,主要是考虑到对数据源头的保护。


在 Java 中,我们主要使用的线程池就是 ThreadPoolExecutor,此外还有定时的线程池 ScheduledThreadPoolExecutor。需要注意的是对于 Executors.newCachedThreadPool() 方法返回的线程池的使用,该方法返回的线程池是没有线程上限的,在使用时一定要当心,因为没有办法控制总体的线程数量,而每个线程都是消耗内存的,这可能会导致过多的内存被占用。建议尽量不要用这个方法返回的线程池,而要使用有固定线程上限的线程池。


与 synchronized 及 ReentrantLock 等提供的互斥相比,volatile 只是提供了变量的可见性支持。同一个变量线程间的可见性与多个线程中操作互斥是两件事情,操作互斥是提供了操作整体的原子性,千万不要混淆了。

volatile 可以立刻在其他线程看到新的值,因为 volatile 保证了只有一份主存中的数据。synchronized 调用后必须在修饰的方法或代码块中读取才可以看到最新值,因为 synchronized 不仅会把当前线程修改的变量的本地副本同步给主存,还会从主存读取数据更新本地副本,这样 synchronized 保证了代码块的串行执行。


服务注册查找中心并不处在调用者和服务提供者之间,服务注册查找中心对于调用者来说,只是提供可用的服务提供者的列表,这有点像日常生活中类似 114 的查号服务。不过处于效率的考虑,我们并不是在每次调用远程服务前都通过这个服务注册查找中心来查找可用地址,而是把地址缓存在调用者本地,当有变化时主动从服务注册查找中心发起通知,告诉调用者可用的服务提供者列表的变化。


在不靠升级硬件的情况下,能够想到的处理方案就是给现有数据库减压。减压的思路有三个,一是优化应用,看看是否有不必要的压力给了数据库(应用优化);二是看看有没有其他方法可以降低数据库的压力,例如引入缓存、加搜索引擎等;最后一种思路就是把数据库的数据和访问分到多台数据库上,分开支持,这也是我们的核心思路和逻辑。

数据库拆分有两种方式,一个是垂直拆分,一个水平拆分。垂直拆分就是把一个数据库中不同业务单元的数据分到不同的数据库里面,水平拆分是根据一定的规则把同一业务单元的数据拆分到多个数据库中。先论是垂直拆分还是水平拆分,最后的结果都是将原来在一个数据库中的数据拆分到了不同的数据库中。所以原来单机数据库可以支持的特性现在就未必支持了。

垂直拆分会带来如下影响:

  • 单机的 ACID 保证被打破了。数据到了多机后,原来在单机通过事务来进行处理逻辑会受到很大的影响。我们面临的选择是,要么放弃原来的单机事务,修改实现,要么引入分布式事务。
  • 一些 join 操作会变得比较困难,因为数据可能已经在两个数据库中了,所以不能很方便地利用数据库本身 join 了,需要应用或者其他方式来解决。
  • 靠外键去进行约束的场景会受影响。

水平拆分会带来如下影响:

  • 同样有可能有 ACID 被打破的情况。
  • 同样有可能有 join 操作被影响的情况。
  • 靠外键去进行约束的场景会受影响。
  • 依赖单库的自增序列生成唯一 ID 会受影响。
  • 针对单个逻辑意义上的表的查询要跨库了。

PS: ACID,指数据库事务正确执行的四个基本要素的缩写。包含: 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。


一致性哈希所带来的最大变化是把节点对应的哈希值变为了一个范围,而不再是离散的。在一致性哈希中,我们会把整个哈希值的范围定义得非常大,然后把这个范围分配给现有的节点。如果有节点加入,那么这个新节点会从原有的某个节点上分管一部分范围的哈希值;如果有节点退出,那么这个节点原来管理的哈希值会给它的下一个节点来管理。

新增一个节点时,除了新增的节点外,只有一个节点受影响,这个新增节点和受影响的节点的负载是明显比其他节点低的;减少一个节点时,除了减去的节点外,只有一个节点受影响,它要承担自己原来的和减去的节点的工作,压力明显比其他节点要高。这似乎要增加一倍节点或减去一半节点才能保持各个节点的均衡负载。如果真是这样,一致性哈希的优势就不明显了。

为了应对上述问题,我们引入虚拟节点的概念。即多个物理节点可以变为很多个虚拟节点,每个虚拟节点支持连续的哈希环上的一段。而这时如果加入一个物理节点,就会相应加入很多虚拟节点,这些新的虚拟节点是相对均匀地插入到整个哈希环上的,这样,就可以很好地分担现有物理节点的压力了;如果减少了一个物理节点,对应的很多虚拟节点就会失效,这样就会有很多剩余的虚拟节点来承担之前虚拟节点的工作,但是对于物理节点来说,增加的负载相对是均衡的。所以可以通过一个物理 节点对应非常多的虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布的方式来解决增加或减少节点时负载不均衡的问题。


上一篇
Docker 中 NGINX 挂载应用的静态文件 Docker 中 NGINX 挂载应用的静态文件
在 Docker 中 NGINX 如何获得应用的静态文件呢? 一般来说这种共享文件的需求,我们需要使用 volumes 假如静态文件的地址是 /home/app/app_name/public 那么我们在中 docker-compose.y
2018-11-01
下一篇
Ruby thread pool Ruby thread pool
有时候会遇到一个情况需要在多个远程服务获取数据。 例如服务 a 需要 2 秒,服务 b 需要 5 秒,按照正常处理总共就需要 2+5 总共 7 秒了。 如果 a 和 b 之间并没有相互依赖关系,我们可以使用 thread pool 来并发获
2015-04-10