什么是服务的状态

即我们会在业务服务里保存一些本地数据即本地缓存,游戏服务端就是典型的状态服务

ServerLess

一直以来,无状态的服务被当成分布式服务设计的最佳实践和铁律。因为无状态的服务对于扩展性和运维实在是太方便了。没有状态的服务,可以随意地增加和减少结点,同样可以随意地搬迁。而且,无状态的服务可以大幅度降低代码的复杂度以及 Bug 数,因为没有状态,所以也没有明显的“副作用”。

有状态的服务 Stateful

题外话

游戏有状态服务器如何做到伸缩自如,这不得不提到我实习的youzu公司用到的akka框架了

幂等性:一次和多次请求某一个资源应该具有同样的副作用

为什么会有幂等性问题?上游重试导致的,下游得做好幂等

幂等分为请求幂等和业务幂等,业务幂等是人为定义的,这种东西没有讨论性

全局ID

ID由谁来分配是个问题?

分配中心?那么每一次交易都需要找那个中心系统来。 这样增加了程序的性能开销。还是上游?上游的话就可能出来重复ID,因为它一般是个集群,每台机器都承担相同的功能

还有就是我们需要一些不冲突的算法

UUID,是个字符串,占用空间大,索引的效率非常低,生成的 ID 太过于随机,完全不是人读的,而且

没有递增,如果要按前后顺序排序的话,基本不可能

雪花算法的生成原理,生成的东西是一个 long类型的数

41bit 为毫秒数

5bit为数组中心ID(需用用户配置)

5bit为机器ID(需要用户配置)

12bits 作为毫秒内的序列号。一毫秒可以生成 4096 个序号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;

public class Test {
public static void main(String[] args) {
// datacenterId = 1, workerId = 1
Snowflake snowflake = IdUtil.getSnowflake(1, 1);

for (int i = 0; i < 10; i++) {
long id = snowflake.nextId();
System.out.println(id);
}
}
}

幂等性处理流程

一锁二判三通过

or 通过mysql 的 insert,update,但其实归结到底它其实还是 一锁二判三通过,因为update会做上行锁

但我们希望我们有一个标准的方式来做这个事,所以,最好还是用一个 ID。

因为我们的幂等性服务也是分布式的,所以,需要这个存储也是共享的。这样每个服务就变成没有状态

的了。但是,这个存储就成了一个非常关键的依赖,其扩展性和可用性也成了非常关键的指标。

HTTP 的幂等性

幂等:get,head,delete,put(创建or更新,URL是一样,故为幂等)

不幂等:post

防止表单提交重复性做法

1.表单提交时提交一个Token,这个Token可以是前端生成好携带过来的,然后做下校验Token是否之前就存

在了。用于防止用户多次点击了表单提交按钮,而导致后端收到了多次请求,却不能分辨是否是重复的

提交。我觉得这应该是产品设计

2.前端按钮做好置灰

3.更稳妥的做法是,后端成功后向前端返回302状态码跳转,把用户的post请求变成get请求,把刚才post的

东西展示出来。如果是web的话,就把刚才的提交页面置为过期,防止回退,这种模式叫做PRG模式

前面所说的隔离设计通常都需要对系统做解耦设计,而把一个单体系统解耦,不单单是把业务功能拆分出来,正如上面所说,拆分完后还会面对很多的问题。其中一个重要的问题就是这些系统间的通讯

通讯一般来说分同步和异步两种。同步通讯就像打电话,需要实时响应,而异步通讯就像发邮件,不需要马上回复。各有千秋,我们很难说谁比谁好。但是在面对超高吐吞量的场景下,异步处理就比同步处理有比较大的优势了,这就好像一个人不可能同时接打很多电话,但是他可以同时接收很多的电子邮件一样。

异步不一定是多线程!!!,如akka的信箱,它异步可以把消息重新发到自己的信箱,但还是单线程的

但异步也不是完全的好,时延还是不如同步的

但同步有以下问题的

  1. 同步调用需要被调用方的吞吐不低于调用方的吞吐。否则会导致被调用方因为性能不足而拖死调用方。换句话说,整个同步调用链的性能会由最慢的那个服务所决定,发送方会被接收方限制死
  2. 同步调用会导致调用方一直在等待被调用方完成,如果一层接一层地同步调用下去,所有的参与方会有相同的等待时间。这会非常消耗调用方的资源(因为调用方需要保存现场(Context)等待远端返回,所以对于并发比较高的场景来说,这样的等待可能会极度消耗资源)。还是那句话,发送方会被接收方限制死
  3. 接受发崩了,发送方也得跟着崩
  4. 很难做到一对多,跟打电话一样,注意不是微信电话 狗头

所以,异步比同步好的原因,除了提高调用方的吞吐量最大的一个好处是其可以让服务间的解耦更为彻底,系统的调用方和被调用方可以按照自己的速率而不是步调一致,从而可以更好地保护系统,让系统更有弹力

异步通信几种方式

请求响应式

这种模式下,调用方直接请求被调用方,然后被调用方回复—收到,正在处理

一般分为两种模式,一种是调用方去轮询询问干没干哦。还有一种是调用方写个回调方法,然后被

调用方处理好请求后通过这个方法回调,游戏支付领域就是这么干的,支付完成后支付中台会回调

游戏服务端,前端回调的这个http / rpc 接口游戏服务端会交给支付中台。

但以上两种还是会有一定耦合的

订阅方式

pub/sub,各个服务之间依靠事件交互,但接收方还是依赖于发送方

服务肯定是无状态好,运维会方便很多

broker模式

即消息中间件

这种模式就做到了发送和接受完全解耦

但你broker要做到三高,高性能,高可靠,高可用

事件驱动架构

订阅和broker其实都是事件驱动架构

正如前面所说,事件驱动最好是使用 Broker 方式,服务间通过交换消息来完成交流和整个流程的驱

动。

例如:

好处就是 运维测试很容易,故障不容易扩散,服务治理比较容易,吞吐量互不影响

但任何技术都有它不好的一面

可观察性变差了

:::color4
ps:NIO/Epoll + Eventloop 本身就是事件驱动的设计

:::

总结

首先为什么要有异步通信

异步通信需要注意哪些地方

好了,我们来总结一下今天分享的主要内容。首先,同步调用有四点问题:影响吞吐量、消耗系统资

源、只能一对一,以及有多米诺骨牌效应。于是,我们想用异步调用来避免该问题。异步调用有三种

方式:请求响应、直接订阅和中间人订阅。最后,我介绍了事件驱动设计的特点和异步通讯设计的重点

如同船舱隔离

对于系统的分离有两种方式,一种是以服务的种类来做分离,一种是以用户来做分离

  1. 以服务做隔离,多种业务从接入到应用到持久化层都做隔离,且做到物理上的隔离,如商品,评,论….。这也是现在微服务的常规做法,但这样有个问题,性能会降低,因为你查询可能涉及到多个服务的调用,在网络中转增多了,这里的性能指的是时延,而不是吞吐量,这种场景下吞吐量其实是会增加。对于这样的问题,一般来说,我们需要小心地设计用户交互,最好不要让用户在一个页面上获得所有的数据。对于目前的手机端上来说,因为手机屏幕尺寸比较小,所以,也不可能在一个屏幕页上展示太多的内容。
    以及还有跨业务板块请求链路中单点故障导致整条链路走不下去问题,跨板块交换复杂的问题(引入消息中间件,且该中间件可pub/sub,持久化),分布式事务问题(在亚马逊中,使用的是 Plan – Reserve – Commit/Cancel 模式)
1
2
3
4
5
6
7
8
9
10
用户请求资源

Plan ──> 检查可用性

Reserve ──> 临时锁定资源

┌───┴──────┐
Commit Cancel
│ │
资源最终占用 资源释放回滚

可见,隔离了的系统在具体的业务场景中还是有很多问题的,是需要我们小心和处理的。对此,我们不

可掉以轻心。根据我的经验,这样的系统通常会引入大量的异步处理模型。

  1. 用户隔离,即多租户

三种方式,但没有绝对,一般是这种。服务共享,数据独立,若比较重要的客户就做到完全独立

然而,在虚拟化技术非常成熟的今天,我们完全可以使用“完全独立”(完全隔离)的方案,通过底层

的虚拟化技术(Hypervisor 的技术,如 KVM,或是 Linux Container 的技术,如 Docker)来实现物

理资源的共享和成本的节约。

:::color4
ps:docker它是内核空间共享,但用户空间隔离,容器里安装的只是操作系统的一部分即用户空间,内核空间还是宿主机的

:::

创建型设计模式,就是造模型

为什么需要创建型模式?

就是规范设置属性,其实写代码久了自己会有这种意识,不写光学感受不到,初学者一定要先学一遍,用不用先不说,一定要知道,等觉得需要规范了,重新学一遍就通了。没办法如果不是天才,一定会经历shi山代码

另外builder这个词确实很贴合了,盖楼必须一层一层盖,强调了创建对象赋值过程中的有序性

DMA解放了CPU

有 DMA 时(现代网络发送)

  • DMA(Direct Memory Access) = 让外设(网卡)直接访问内存,不需要 CPU 一直搬数据。
  • 流程是:
    1. 用户调用 send() → 数据先拷贝到 内核 socket buffer
    2. 内核告诉网卡:”这段内存的数据你自己去拿”。
    3. 网卡用 DMA 控制器,直接从内核缓冲区读取数据到网卡缓冲区。
    4. CPU 空出来干别的活。

👉 CPU 只做控制,不做“苦力”搬运工

没有 DMA 时(早期方式)

如果没有 DMA,CPU 就得亲自当“苦力”:

  1. 用户调用 send(),数据进入内核缓冲区。
  2. CPU 一点点把内核缓冲区的数据拷贝到网卡的寄存器/缓冲区
  3. 网卡再把数据发出去。

问题:

  • CPU 必须忙着不断“搬运字节”,效率很低。
  • 数据传输过程中 CPU 不能干别的事,性能瓶颈严重。
  • 吞吐量大时,CPU 可能光搬数据就耗光了算力。

如今网络应用已经从cpu密集型转成了I/0密集型,你CRUD是没有啥cpu消耗的,唯一瓶颈就是多线程,切来

切去的消耗

CS(客户端服务器),BS(浏览器服务器)。BS就是一种特殊的架构

计算机存储器

最低端是与cpu同频的寄存器

容量大,速度快,价格便宜三者无法同时满足

1.第一层寄存器

作用:让 CPU 快速访问和处理数据,避免频繁访问速度较慢的内存,是 CPU 执行指令的核心中转站

2.第二层高速缓存

3.主存

4.磁盘

5.物理内存

6.虚拟内存

没有什么是加一层中间件不能解决,如果有那就再加一层,这是计算机领域几乎真理的话

虚拟内存正是此话的又一个重要实践

操作系统32位操作系统,2的32次方,寻址也就只有4GB啊,那你怎么用上8GB的呢

应用程序是不能直接操作物理内存,都是操作虚拟内存,而虚拟内存有一张映射表,它可能映射到磁盘or内

存,这个就不得而知了

7.MMU(内存管理单元)

作用就是管映射到哪去

如果使用虚拟内存技术的话,cpu则会把这些虚拟内存地址通过地址总线送到内存管理单元MMU,MMU

再将虚拟内存地址转成 物理地址之后再通过内存总线访问物理内存

8.用户态和内核态

Linux安全的一个原因就是区分用户态和内核态,硬件只能内核态来操作,即调操作系统函数委托内核来

帮你干事

在资源和用户做了隔离,你碰不到就是安全的

Linux I/O

1.IO缓冲区

脏不是说数据有问题,只是说没落盘

刷flush了就不脏dity了。只要不是批量写,都有卡的问题

一般数据丢失,都是没刷盘就断电了

2.传统I/O读写模式

Linux的系统模式 read,write

3.zero 拷贝

指cpu不需要先将数据从某个内存复制到另外一个特定区域,这种技术通常用于通过网络传输文件时节省cpu

周期和内存带宽,即不用倒腾数据到用户态了

**3.1 mmap(memory map) **

map就是个映射

用对象,一种是深拷贝(产生新的对象),一种是浅拷贝(用的还是同一个对象)

mmap就是一种浅拷贝

这种方式是zero 拷贝较为简单的实现方式,通过调用Linux的系统函数mmap()替换原先的read()

不用拷贝到内存,直接在内核里面倒腾

两步走:先mmap 再 write,但还是4次切换

以上是老版本的Linux

3.2 sendfile()

这里的就做得更到底了,sendfile()就相当于把mmap 和 write合成了一个内核函数,这么做的好处就是少了

两次切换,因为原来是调两函数,调完后还得切回去继续调另外一个

kafka就是用sendfile来实现零拷贝,rocketmq还是用的mmap

:::color4
零拷贝,零拷贝,其实还得是拷贝的,只是不用拷贝到用户态了,直接在内核态里拷贝,对于用户态是无感知的

:::

总结

零拷贝 约等于 对象浅拷贝,直接返回引用映射