一文搞懂后台高性能服务器设计的常见套路, BAT 高频面试系列

微信搜索🔍「编程指北」,关注这个写干货的程序员,回复「资源」,便可获取后台开发学习路线和书籍

前言

金九银十,又是一年校招季。前端

经历过,才深知不易。最近,和做为校招面试官的同事聊了聊,问他们是如何去考察一个学生的,我简单归为如下几点:程序员

  1. 聪明、反应快,这点自没必要说,聪明意味着学习能力、适应力强,可以快速胜任工做。
  2. 算法不错,代码基本功好,这点其实考察的是算法能力和代码是否写得优雅。
  3. 基础过硬,技术岗面试最核心的仍是考察「技术储备」,包括了语言基本功,操做系统、网络、体系结构、系统设计。
  4. 语言组织和表达能力,这点很重要,不少同窗懂得某个知识点,却很难用简洁准确的语言表述出来。

想必有不少同窗在刷题、刷面经,不过我想说“面经虽好,不要贪杯哦~”,面经能够刷,看看面试官都是怎么提问的,但不要寄但愿于原题。
由于面试过程当中的问题每每是一环扣一环的,这意味着你须要有足够的技术深度,将知识由点链接成面,而不是停留在相互孤立的知识点上。面试

因此仍是建议系统性的看书,若是以为时间不够,能够关注书里的重点章节。至于看哪些书?后面也会列一个个人书单和阅读建议。在【编程指北】后台回复【书单】便可获取算法

那么回到技术面试上,除了算法和网络、操做系统这种基础以外,还有一类系统设计和优化的问题。这类问题须要你有一个全局的技术视野,以及熟悉一些经常使用的系统优化方法论,也就是工程上的一些 Best Practice,而不至于本身临时拍脑壳瞎设计。数据库

在互联网公司,常常面临一个“三高”问题:编程

  • 高并发
  • 高性能
  • 高可用

这篇文章将总结一下后台服务器开发中有哪些经常使用的解决“三高”问题的方法和思想。后端

但愿这些知识,可以给你一丝启发和帮助,助力你收割 各大公司 Offer~缓存

先上本文思惟导图:安全

如何解决三高

正文

1、缓存

什么是缓存?看看维基百科怎么说:服务器

In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.

在计算机中,缓存是存储数据的硬件或软件组件,以即可以更快地知足未来对该数据的请求。 存储在缓存中的数据多是以前计算结果,也多是存储在其余位置的数据副本

缓存本质来讲是使用空间换时间的思想,它在计算机世界中无处不在, 好比 CPU 就自带 L一、L二、L3 Cache,这个通常应用开发可能关注较少。可是在一些实时系统、大规模计算模拟、图像处理等追求极致性能的领域,就特别注重编写缓存友好的代码。

什么是缓存友好?简单来讲,就是代码在访问数据的时候,尽可能使用缓存命中率高的方式。这个后面能够单独写一篇 CPU 缓存系统以及如何编写缓存友好代码的文章。

1.1 缓存为何有效?

缓存之因此可以大幅提升系统的性能,关键在于数据的访问具备局部性,也就是二八定律:「百分之八十的数据访问是集中在 20% 的数据上」。这部分数据也被叫作热点数据。

缓存通常使用内存做为存储,内存读写速度快于磁盘,但容量有限,十分宝贵,不可能将全部数据都缓存起来。

若是应用访问数据没有热点,不遵循二八定律,即大部分数据访问并无集中在小部分数据上,那么缓存就没有意义,由于大部分数据尚未被再次访问就已经被挤出缓存了。每次访问都会回源到数据库查询,那么反而会下降数据访问效率。

1.2 缓存分类

  • 1. 本地缓存:

    使用进程内成员变量或者静态变量,适合简单的场景,不须要考虑缓存一致性、过时时间、清空策略等问题。

    能够直接使用语言标准库内的容器来作存储。例如:

本地缓存

  • 2. 分布式缓存:

    当缓存的数据量增大之后,单机不足以承载缓存服务时,就要考虑对缓存服务作水平扩展,引入缓存集群。

    将数据分片后分散存储在不一样机器中,如何决定每一个数据分片存放在哪台机器呢?通常是采用一致性 Hash 算法,它可以保证在缓存集群动态调整,不断增长或者减小机器后,客户端访问时依然可以根据 key 访问到数据。

    一致性 Hash 算法也是值得用一篇文章来说的,若是暂时还不懂的话能够去搜一下。

    经常使用的组件有 MemcacheRedis Cluster 等,第二个是在高性能内存存储 Redis 的基础上,提供分布式存储的解决方案。

1.3 缓存使用指南

1. 适合缓存的场景:

  • 读多写少:

    好比电商里的商品详情页面,访问频率很高,可是通常写入只在店家上架商品和修改信息的时候发生。若是把热点商品的信息缓存起来,这将拦截掉不少对数据库的访问,提升系统总体的吞吐量。

    由于通常数据库的 QPS 因为有「ACID」约束、而且数据是持久化在硬盘的,因此比 Redis 这类基于内存的 NoSQL 存储低很多。经常是一个系统的瓶颈,若是咱们把大部分的查询都在 Redis 缓存中命中了,那么系统总体的 QPS 也就上去了。

  • 计算耗时大,且实时性不高:
    好比王者荣耀里的全区排行榜,通常一周更新一次,而且计算的数据量也比较大,因此计算后缓存起来,请求排行榜直接从缓存中取出,就不用实时计算了。

2. 不适合缓存的场景

  • 写多读少,频繁更新。
  • 对数据一致性要求严格: 由于缓存会有更新策略,因此很难作到和数据库实时同步。
  • 数据访问彻底随机: 由于这样会致使缓存的命中率极低。

1.4 缓存更新的策略

如何更新缓存其实已经有总结得很是好的「最佳实践」,咱们按照套路来,大几率不会犯错。

主要分为两类 Cache-AsideCache-As-SoR。 SoR 即「System Of Record,记录系统」,表示数据源,通常就是指数据库。

一、Cache-Aside:

Cache-Aside架构图

这应该是最容易想到的模式了,获取数据时先从缓存读,若是 cache hit 则直接返回,没命中就从数据源获取,而后更新缓存。

写数据的时候则先更新数据源,而后设置缓存失效,下一次获取数据的时候必然 cache miss,而后触发回源

直接看伪代码:

Cache-Aside 代码示范

能够看到这种方式对于缓存的使用者是不透明的,须要使用者手动维护缓存。

二、Cache-As-SoR:

Cache-As-SoR架构图

从字面上来看,就是把 Cache 看成 SoR,也就是数据源,因此一切读写操做都是针对 Cache 的,由 Cache 内部本身维护和数据源的一致性。

这样对于使用者来讲就和直接操做 SoR 没有区别了,彻底感知不到 Cache 的存在。

CPU 内部的 L一、L二、L3 Cache 就是这种方式,做为数据的使用方应用程序,是彻底感知不到在内存和咱们之间还存在几层的 Cache,可是咱们以前又提到编写 “缓存友好”的代码,不是透明的吗?这是否是冲突呢?

其实否则,缓存友好是指咱们经过学习了解缓存内部实现、更新策略以后,经过调整数据访问顺序提升缓存的命中率。

Cache-As-SoR 又分为如下三种方式:

  • Read Through:这种方式和 Cache-Aside 很是类似,都是在查询时发生 cache miss 去更新缓存,可是区别在于 Cache-Aside 须要调用方手动更新缓存,而 Cache-As-SoR 则是由缓存内部实现本身负责,对应用层透明。
  • Write Through: 直写式,就是在将数据写入缓存的同时,缓存也去更新后面的数据源,而且必须等到数据源被更新成功后才可返回。这样保证了缓存和数据库里的数据一致性
  • Write Back:回写式,数据写入缓存便可返回,缓存内部会异步的去更新数据源,这样好处是写操做特别快,由于只须要更新缓存。而且缓存内部能够合并对相同数据项的屡次更新,可是带来的问题就是数据不一致,可能发生写丢失。

2、预处理和延后处理

预先延后,这实际上是一个事物的两面,无论是预先仍是延后核心思想都是将原本该在实时链路上处理的事情剥离,要么提早要么延后处理。下降实时链路的路径长度, 这样能有效提升系统性能。

2.1 预处理

举个咱们团队实际中遇到的问题:

前两个月支付宝联合杭州市政府发放消费劵,可是要求只有杭州市常驻居民才能领取,那么须要在抢卷请求进入后台的时候就判断一下用户是不是杭州常驻居民。

而判断用户是不是常驻居民这个是另一个微服务接口,若是直接实时的去调用那个接口,短时的高并发颇有可能把这个服务也拖挂,最终致使整个系统不可用,而且 RPC 自己也是比较耗时的,因此就考虑在这里进行优化。

那么该怎么作呢?很简单的一个思路,提早将杭州全部常驻居民的 user_id 存到缓存中, 好比能够直接存到 Redis。大概就是千万量级,这样,当请求到来的时候咱们直接经过缓存能够快速判断是否来自杭州常驻居民。若是不是则直接在这里返回前端。

这里经过预先处理减小了实时链路上的 RPC 调用,既减小了系统的外部依赖,也极大的提升了系统的吞吐量。

预处理在 CPU 和操做系统中也普遍使用,好比 CPU 基于历史访存信息,将内存中的指令和数据预取到 Cache 中,这样能够大大提升Cache 命中率。 还好比在 Linux 文件系统中,预读算法会预测即将访问的 page,而后批量加载比当前读请求更多的数据缓存在 page cache 中,这样当下次读请求到来时能够直接从 cache 中返回,大大减小了访问磁盘的时间。

2.2 延后处理

仍是支付宝,上栗子:

集五福活动

这是支付宝春节集五福活动开奖当晚,不过,做为非酋的我通常是不屑于参与这种活动的。

你们发现没有,这类活动中奖奖金通常会显示 「稍后到帐」,为何呢?那固然是到帐这个操做不简单!

到帐即转帐,A 帐户给 B 帐户转钱,A 减钱, B 就必需要同时加上钱,也就是说不能 A 减了钱但 B 没有加上,这就会致使资金损失。资金安全是支付业务的生命线,这可不行。

这两个动做必须一块儿成功或是一块儿都不成功,不能只成功一半,这是保证数据一致性。 保证两个操做同时成功或者失败就须要用到事务

若是去实时的作到帐,那么大几率数据库的 TPS(每秒处理的事务数) 会是瓶颈。经过产品提示,将到帐操做延后处理,解决了数据库 TPS 瓶颈。

延后处理还有一个很是著名的例子,COW(Copy On Write,写时复制)。 Linux 建立进程的系统调用 fork,fork 产生的子进程只会建立虚拟地址空间,而不会分配真正的物理内存,子进程共享父进程的物理空间,只有当某个进程须要写入的时候,才会真正分配物理页,拷贝该物理页,经过 COW 减小了不少没必要要的数据拷贝。

3、池化

后台开发过程当中你必定离不开各类 「池子」: 内存池、链接池、线程池、对象池......

内存、链接、线程这些都是资源,建立线程、分配内存、数据库链接这些操做都有一个特征, 那就是建立和销毁过程都会涉及到不少系统调用或者网络 IO。 每次都在请求中去申请建立这些资源,就会增长请求处理耗时,可是若是咱们用一个 容器(池) 把它们保存起来,下次须要的时候,直接拿出来使用,避免重复建立和销毁浪费的时间。

3.1 内存池

在 C/C++ 中,常用 malloc、new 等 API 动态申请内存。因为申请的内存块大小不一,若是频繁的申请、释放会致使大量的内存碎片,而且这些 API 底层依赖系统调用,会有额外的开销。

内存池就是在使用内存前,先向系统申请一块空间留作备用,使用者须要内池时向内存池申请,用完后还回来。

内存池的思想很是简单,实现却不简单,难点在于如下几点:

  • 如何快速分配内存
  • 下降内存碎片率
  • 维护内存池所需的额外空间尽可能少

若是不考虑效率,咱们彻底能够将内存分为不一样大小的块,而后用链表链接起来,分配的时候找到大小最合适的返回,释放的时候直接添加进链表。如:

空闲链表

固然这只是玩具级别的实现,业界有性能很是好的实现了,咱们能够直接拿来学习和使用。

好比 Google 的 「tcmalloc」 和 Facebook 的 「jemalloc」。

限于篇幅咱们不在这里详细讲解它们的实现原理,若是感兴趣能够搜来看看,也推荐去看看被誉为神书的 CSAPP(《深刻理解计算机系统》)第 10 章,那里也讲到了动态内存分配算法。

3.2 线程池

线程是干吗的?线程就是咱们程序执行的实体。在服务器开发领域,咱们常常会为每一个请求分配一个线程去处理,可是线程的建立销毁、调度都会带来额外的开销,线程太多也会致使系统总体性能降低。在这种场景下,咱们一般会提早建立若干个线程,经过线程池来进行管理。当请求到来时,只需从线程池选一个线程去执行处理任务便可。

线程池经常和队列一块儿使用来实现任务调度,主线程收到请求后将建立对应的任务,而后放到队列里,线程池中的工做线程等待队列里的任务。

线程池实现上通常有四个核心组成部分:

  • 管理器(Manager): 用于建立并管理线程池。
  • 工做线程(Worker): 执行任务的线程。
  • 任务接口(Task): 每一个具体的任务必须实现任务接口,工做线程将调用该接口来完成具体的任务。
  • 任务队列(TaskQueue): 存放还未执行的任务。

线程池模型

线程池在 C、C++ 中没有具体的实现,须要应用开发者手动实现上诉几个部分。

在 Java 中 「ThreadPoolExecutor」 类就是线程池的实现。后续我也会写文章分析 C++ 如何写一个简单的线程池以及 Java 中线程池是如何实现的。

3.3 链接池

顾名思义,链接池是建立和管理链接的。

你们最熟悉的莫过于数据库链接池,这里咱们简单分析下若是不用数据库链接池,一次 SQL 查询请求会通过哪些步骤:

  1. 和 MySQL server 创建 TCP 链接:

    • 三次握手
  2. MySQL 权限认证:

    • Server 向 Client 发送 密钥
    • Client 使用密钥加密用户名、密码等信息,将加密后的报文发送给 Server
    • Server 根据 Client 请求包,验证是不是合法用户,而后给 Client 发送认证结果
  3. Client 发送 SQL 语句
  4. Server 返回语句执行结果
  5. MySQL 关闭
  6. TCP 链接断开

    • 四次挥手

能够看出不使用链接池的话,为了执行一条 SQL,会花不少时间在安全认证、网络IO上。

若是使用链接池,执行一条 SQL 就省去了创建链接和断开链接所需的额外开销。

还能想起哪里用到了链接池的思想吗?我认为 HTTP 长连接也算一个变相的连接池,虽然它本质上只有一个链接,可是思想却和链接池不谋而合,都是为了复用同一个链接发送多个 HTTP 请求,避免创建和断开链接的开销。

池化其实是预处理和延后处理的一种应用场景,经过池子将各种资源的建立提早和销毁延后。

4、同步变异步

对于处理耗时的任务,若是采用同步的方式,那么会增长任务耗时,下降系统并发度。

能够经过将同步任务变为异步进行优化。

举个例子,好比咱们去 KFC 点餐,遇到排队的人不少,当点完餐后,大多状况下咱们会隔几分钟就去问好了没,反复去问了好几回才拿到,在这期间咱们也无法干活了,这时候咱们是这样的:

同步写法

这个就叫同步轮训, 这样效率显然过低了。

服务员被问烦了,就在点完餐后给咱们一个号码牌,每次准备好了就会在服务台叫号,这样咱们就能够在被叫到的时候再去取餐,中途能够继续干本身的事。

这就叫异步,在不少编程语言中有异步编程的库,好比 C++ std::future、Python asyncio 等,可是异步编程每每须要回调函数(Callback function),若是回调函数的层级太深,这就是回调地狱(Callback hell)。回调地狱如何优化又是一个庞大的话题。。。。

这个例子至关于函数调用的异步化,还有的是状况是处理流程异步化,这个会在接下来消息队列中讲到。

5、消息队列

消息队列示意图

这是一个很是简化的消息队列模型,上游生产者将消息经过队列发送给下游消费者。在这之间,消息队列能够发挥不少做用,好比:

5.1 服务解耦

有些服务被其它不少服务依赖,好比一个论坛网站,当用户成功发布一条帖子有一系列的流程要作,有积分服务计算积分,推送服务向发布者的粉丝推送一条消息..... 对于这类需求,常见的实现方式是直接调用:

直接调用

这样若是须要新增一个数据分析的服务,那么又得改动发布服务,这违背了依赖倒置原则即上层服务不该该依赖下层服务,那么怎么办呢?

发布订阅模式

引入消息队列做为中间层,当帖子发布完成后,发送一个事件到消息队列里,而关心帖子发布成功这件事的下游服务就能够订阅这个事件,这样即便后续继续增长新的下游服务,只须要订阅该事件便可,彻底不用改动发布服务,完成系统解耦。

5.2 异步处理

有些业务涉及到的处理流程很是多,可是不少步骤并不要求实时性。那么咱们就能够经过消息队列异步处理。好比淘宝下单,通常包括了风控、锁库存、生成订单、短信/邮件通知等步骤。可是核心的就风控和锁库存, 只要风控和扣减库存成功,那么就能够返回结果通知用户成功下单了。后续的生成订单,短信通知均可以经过消息队列发送给下游服务异步处理。大大提升了系统响应速度。

这就是处理流程异步化。

5.3 流量削峰

通常像秒杀、抽奖、抢卷这种活动都伴随着短期海量的请求, 通常超事后端的处理能力,那么咱们就能够在接入层将请求放到消息队列里,后端根据本身的处理能力不断从队列里取出请求进行业务处理。

就像最近长江汛期,上游短期大量的洪水汇聚直奔下游,可是经过三峡大坝将这些水缓存起来,而后匀速的向下游释放,起到了很好的削峰做用。

起到了平均流量的做用。

5.4 总结

消息队列的核心思想就是把同步的操做变成异步处理,异步处理会带来相应的好处,好比:

  • 服务解耦
  • 提升系统的并发度,将非核心操做异步处理,不会阻塞住主流程

可是软件开发没有银弹,全部的方案选择都是一种 trade-off。 一样,异步处理也不全是好处,也会致使一些问题:

  • 下降了数据一致性,从强一致性变为最终一致性
  • 有消息丢失的风险,好比宕机,须要有容灾机制

6、批量处理

在涉及到网络链接、IO等状况时,将操做批量进行处理可以有效提升系统的传输速率和吞吐量。

在先后端通讯中,经过合并一些频繁请求的小资源能够得到更快的加载速度。

好比咱们后台 RPC 框架,常常有更新数据的需求,而有的数据更新的接口每每只接受一项,这个时候咱们每每会优化下更新接口,

使其可以接受批量更新的请求,这样能够将批量的数据一次性发送,大大缩短网络 RPC 调用耗时。

7、数据库

咱们常把后台开发调侃为「CRUD」,数据库在整个应用开发过程当中的重要性不言而喻。

并且不少时候系统的瓶颈也每每处在数据库这里,慢的缘由也有不少,好比多是没用索引、没用对索引、读写锁冲突等等。

那么如何使用数据才能又快又好呢?下面这几点须要重点关注:

7.1 索引

索引多是咱们平时在使用数据库过程当中接触得最多的优化方式。索引比如图书馆里的书籍索引号,想象一下,若是我让你去一个没有书籍索引号的图书馆找《人生》这本书,你是什么样的感觉?固然是怀疑人生,同理,你应该能够理解当你查询数据,却不用索引的时候数据库该有多崩溃了吧。

数据库表的索引就像图书馆里的书籍索引号同样,能够提升咱们检索数据的效率。索引能提升查找效率,但是你有没有想过为何呢?这是由于索引通常而言是一个排序列表,排序意味着能够基于二分思想进行查找,将查询时间复杂度作到 O(log(N)),快速的支持等值查询和范围查询。

二叉搜索树查询效率无疑是最高的,由于平均来讲每次比较都能缩小一半的搜索范围,可是通常在数据库索引的实现上却会选择 B 树或 B+ 树而不用二叉搜索树,为何呢?

这就涉及到数据库的存储介质了,数据库的数据和索引都是存放在磁盘,而且是 InnoDB 引擎是以页为基本单位管理磁盘的,一页通常为 16 KB。AVL 或红黑树搜索效率虽然很是高,可是一样数据项,它也会比 B、B+ 树更高,高就意味着平均来讲会访问更多的节点,即磁盘IO次数!

根据 Google 工程师 Jeff Dean 的统计,访问内存数据耗时大概在 100 ns,访问磁盘则是 10,000,000 ns。

因此表面上来看咱们使用 B、B+ 树没有 二叉查找树效率高,可是实际上因为 B、B+ 树下降了树高,减小了磁盘 IO 次数,反而大大提高了速度。

这也告诉咱们,没有绝对的快和慢,系统分析要抓主要矛盾,先分析出决定系统瓶颈的究竟是什么,而后才是针对瓶颈的优化。

其实关于索引想写的也还有不少,但仍是受限于篇幅,之后再单独写。

先把我认为索引必知必会的知识列出来,你们能够查漏补缺:

  • 主键索引和普通索引,以及它们之间的区别
  • 最左前缀匹配原则
  • 索引下推
  • 覆盖索引、联合索引

7.2 读写分离

通常业务刚上线的时候,直接使用单机数据库就够了,可是随着用户量上来以后,系统就面临着大量的写操做和读操做,单机数据库处理能力有限,容易成为系统瓶颈。

因为存在读写锁冲突,而且不少大型互联网业务每每读多写少,读操做会首先成为数据库瓶颈,咱们但愿消除读写锁冲突从而提高数据库总体的读写能力。

那么就须要采用读写分离的数据库集群方式,一主多从,主库会同步数据到从库。写操做都到主库,读操做都去从库。

读写分离

读写分离到以后就避免了读写锁争用,这里解释一下,什么叫读写锁争用:

MySQL 中有两种锁:

  • 排它锁( X 锁): 事务 T 对数据 A 加上 X 锁时,只容许事务 T 读取和修改数据 A。
  • 共享锁( S 锁): 事务 T 对数据 A 加上 S 锁时,其余事务只能再对数据 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。

读写分离解决问题的同时也会带来新问题,好比主库和从库数据不一致

MySQL 的主从同步依赖于 binlog,binlog(二进制日志)是 MySQL Server 层维护的一种二进制日志,是独立于具体的存储引擎。它主要存储对数据库更新(insert、delete、update)的 SQL 语句,因为记录了完整的 SQL 更新信息,因此 binlog 是能够用来数据恢复和主从同步复制的。

从库从主库拉取 binlog 而后依次执行其中的 SQL 便可达到复制主库的目的,因为从库拉取 binlog 存在网络延迟等,因此主从数据存在延迟问题。

那么这里就要看业务是否容许短期内的数据不一致,若是不能容忍,那么能够经过若是读从库没获取到数据就去主库读一次来解决。

7.3 分库分表

若是用户愈来愈多,写请求暴涨,对于上面的单 Master 节点确定扛不住,那么该怎么办呢?多加几个 Master?不行,这样会带来更多的数据不一致的问题,增长系统的复杂度。那该怎么办?就只能对库表进行拆分了。

常见的拆分类型有垂直拆分和水平拆分。

考虑拼夕夕电商系统,通常有 订单表、用户表、支付表、商品表、商家表等, 最初这些表都在一个数据库里。
后来随着砍一刀带来的海量用户,拼夕夕后台扛不住了! 因而紧急从阿狸粑粑那里挖来了几个 P八、P9 大佬对系统进行重构。

  1. P9 大佬第一步先对数据库进行垂直分库,

根据业务关联性强弱,将它们分到不一样的数据库, 好比订单库,商家库、支付库、用户库。

  1. 第二步是对一些大表进行垂直分表,将一个表按照字段分红多表,每一个表存储其中一部分字段。 好比商品详情表可能最初包含了几十个字段,可是每每最多访问的是商品名称、价格、产地、图片、介绍等信息,因此咱们将不常访问的字段单独拆成一个表。
  • 因为垂直分库已经按照业务关联切分到了最小粒度,数据量任然很是大,P9 大佬开始水平分库,好比能够把订单库分为订单1库、订单2库、订单3库...... 那么如何决定某个订单放在哪一个订单库呢?能够考虑对主键经过哈希算法计算放在哪一个库。
  • 分完库,单表数据量任然很大,查询起来很是慢,P9 大佬决定按日或者按月将订单分表,叫作日表、月表。

分库分表同时会带来一些问题,好比平时单库单表使用的主键自增特性将做废,由于某个分区库表生成的主键没法保证全局惟一,这就须要引入全局 UUID 服务了。

通过一番大刀阔斧的重构,拼夕夕恢复了往日的活力,你们又能够愉快的在上面互相砍一刀了。

(分库分表会引入不少问题,并无一一介绍,这里只是为了讲解什么是分库分表)

8、具体技法

8.1 零拷贝

高性能的服务器应当避免没必要要数据复制,特别是在用户空间和内核空间之间的数据复制。 好比 HTTP 静态服务器发送静态文件的时候,通常咱们会这样写:

发送文件

若是了解 Linux IO 的话就知道这个过程包含了内核空间和用户空间之间的屡次拷贝:

IO示意图

内核空间和用户空间之间数据拷贝须要 CPU 亲自完成,可是对于这类数据不须要在用户空间进行处理的程序来讲,这样的两次拷贝显然是浪费。什么叫 「不须要在用户空间进行处理」?

好比 FTP 或者 HTTP 静态服务器,它们的做用只是将文件从磁盘发送到网络,不须要在中途对数据进行编解码之类的计算操做。

若是可以直接将数据在内核缓存之间移动,那么除了减小拷贝次数之外,还能避免内核态和用户态之间的上下文切换。

而这正是零拷贝(Zero copy)干的事,主要就是利用各类零拷贝技术,减小没必要要的数据拷贝,将 CPU 从数据拷贝这样简单的任务解脱出来,让 CPU 专一于别的任务。

经常使用的零拷贝技术:

  1. mmap

    mmap 经过内存映射,将文件映射到内核缓冲区,同时,用户空间能够共享内核空间的数据。这样,在进行网络传输时,就能够减小内核空间到用户空间的拷贝次数。

mmap

  1. sendfile

    sendfile 是 Linux2.1 版本提供的,数据不通过用户态,直接从页缓存拷贝到 socket 缓存,同时因为和用户态彻底无关,就减小了一次上下文切换。

    在 Linux 2.4 版本,对 sendfile 进行了优化,直接经过 DMA 将磁盘文件数据读取到 socket 缓存,真正实现了 ”0” 拷贝。前面 mmap 和 2.1 版本的 sendfile 实际上只是消除了用户空间和内核空间之间拷贝,而页缓存和 socket 缓存之间的拷贝依然存在。

8.2 无锁化

在多线程环境下,为了不 竞态条件(race condition), 咱们一般会采用加锁来进行并发控制,锁的代价也是比较高的,锁会致使上线文切换,甚至被挂起直到锁被释放。

基于硬件提供的原子操做 CAS(Compare And Swap) 实现一些高性能无锁的数据结构,好比无锁队列,能够在保证并发安全的状况下,提供更高的性能。

首先须要理解什么是 CAS,CAS 有三个操做数,内存里当前值M,预期值 E,修改的新值 N,CAS 的语义就是:

若是当前值等于预期值,则将内存修改成新值,不然不作任何操做

用 C 语言来表达就是:

CAS

注意,上面 CAS 函数其实是一条原子指令,那么是如何用的呢?

假设我须要实现这样一个功能:

对一个全局变量 global 在两个不一样线程分别对它加 100 次,这里多线程访问一个全局变量存在 race condition,因此咱们须要采用线程同步操做,下面我分别用锁和CAS的方法来实现这个功能。

CAS和锁示范

经过使用原子操做大大下降了锁冲突的可能性,提升了程序的性能。

除了 CAS,还有一些硬件原子指令:

  • Fetch-And-Add,对变量原子性 + 1
  • Test-And-Set,这是各类锁算法的核心,在 AT&T/GNU 汇编语法下,叫 xchg 指令,我会单独写一篇如何使用 xchg 实现各类锁。

8.3 序列化与反序列化

先看看维基百科怎么定义的序列化:

In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, across a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. For many complex objects, such as those that make extensive use of references, this process is not straightforward. Serialization of object-oriented objects does not include any of their associated methods with which they were previously linked.

我相信你大几率没有看完上面的英文描述,其实我也不爱看英文资料,总以为很慢,可是计算机领域一手的学习资料都是美帝那边的,因此没办法,必须逼本身去试着读一些英文的资料。

实际上也没有那么难,熟悉经常使用的几百个专业名词,句子都是很是简单的一些从句。没看的话,再倒回去看看?

这里我就不作翻译了,主要是水平过低,估计作到「信达雅」的信都很难。

扯远了,仍是回到序列化来。

全部的编程必定是围绕数据展开的,而数据呈现形式每每是结构化的,好比结构体(Struct)、类(Class)。 可是当咱们 经过网络、磁盘等传输、存储数据的时候却要求是二进制流。 好比 TCP 链接,它提供给上层应用的是面向链接的可靠字节流服务。那么如何将这些结构体和类转化为可存储和可传输的字节流呢?这就是序列化要干的事情,反之,从字节流如何恢复为结构化的数据就是反序列化。

序列化解决了对象持久化和跨网络数据交换的问题。

序列化通常按照序列化后的结果是否可读,可分为如下两类:

  • 文本类型:

    如 JSON、XML,这些类型可读性很是好,是自解释的。也经常用在先后端数据交互上,由于接口调试,可读性高很是方便。可是缺点就是信息密度低,序列化后占用空间大。

  • 二进制类型

    如 Protocol Buffer、Thrift等,这些类型采用二进制编码,数据组织得更加紧凑,信息密度高,占用空间小,可是带来的问题就是基本不可读。

还有 Java 、Go 这类语言内置了序列化方式,好比在 Java 里实现了 Serializable 接口即表示该对象可序列化。

说到这让我想起了大一写的的两个程序,一个是用刚 C 语言写的公交管理系统,当时须要将公交线路、站点信息持久化保存,当时的方案就是每一个公交线路写在一行,用 "|"分割信息,好比:

5|6:00-22:00|大学城|南山站|北京站
123|6:30-23:00|南湖大道|茶山刘|世界

第一列就是线路编号、第二项是发车时间、后面就是途径的站点。是否是很是原始?实际上这也是一种序列化方式,只是效率很低,也不通用。并且存在一个问题就是若是信息中包含 “|”怎么办?固然是用转义。

第二个程序是用 Java 写的网络五子棋,当时须要经过网络传输表示棋子位置的对象,查了一圈最后发现只须要实现 Serializable 接口,本身什么都不用干,就能本身完成对象的序列化,而后经过网络传输后反序列化。当时哪懂得这就叫序列化,只以为牛逼、神奇!

最后完成了一个能够网络五子棋,拉着隔壁室友一块儿玩。。。真的是成就感满满哈哈哈。

说来在编程方面,已经好久没有这样的成就感了。

总结

这篇文章主要是粗浅的介绍了一些系统设计、系统优化的套路和最佳实践。

不知道你发现没有,从缓存到消息队列、CAS......,不少看起来很牛逼的架构设计其实都来源于操做系统、体系结构。

因此我很是热衷学习一些底层的基础知识,这些看似古老的技术是通过时间洗礼留下来的好东西。如今不少的新技术、框架看似很是厉害,实则很多都是新瓶装旧酒,每几年又会被淘汰一批。

最后说一句(求关注)

这篇文章写了挺久的,从写文章、画图,调格式每一步都很花时间。若是以为对你有帮助的话,能够点个关注或者在看鼓励下~

文章持续更新,微信搜索「 编程指北 」第一时间获取,回复【资料】有我准备的一线 BAT 大厂面试资料和简历模板。

期待你的关注~
有任何问题,欢迎留言~

相关文章
相关标签/搜索