Skip to content

Commit

Permalink
add toc
Browse files Browse the repository at this point in the history
  • Loading branch information
fenixsoft committed Jan 9, 2022
1 parent 1f24861 commit 54cb1bb
Show file tree
Hide file tree
Showing 4 changed files with 4 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

由此可见,一旦在技术根基上出现问题,依赖使用者通过各种 Tricks 去解决,无论如何都难以摆脱“两害相权取其轻”的权衡困境,否则这就不是 Tricks 而是会成为一种标准的设计模式了。

在另一方面,HTTP 的设计者们并不是没有尝试过在协议层面去解决连接成本过高的问题,即使是 HTTP 协议的最初版本(指 HTTP/1.0,忽略非正式的 HTTP/0.9 版本)就已经支持了(连接复用技术在 HTTP/1.0 中并不是默认开启的,是在 HTTP/1.1 中变为默认开启)连接复用技术,即今天大家所熟知的[持久连接](https://en.wikipedia.org/wiki/HTTP_persistent_connection)(Persistent Connection),也称为连接[Keep-Alive 机制](https://en.wikipedia.org/wiki/Keepalive)。持久连接的原理是让客户端对同一个域名长期持有一个或多个不会用完即断的 TCP 连接。典型做法是在客户端维护一个 FIFO 队列,每次取完数据(如何在不断开连接下判断取完数据将会放到稍后[传输压缩](/architect-perspective/general-architecture/diversion-system/transmission-optimization.html#传输压缩)部分去讨论)之后一段时间内不自动断开连接,以便获取下一个资源时直接复用,避免创建 TCP 连接的成本。
在另一方面,HTTP 的设计者们并不是没有尝试过在协议层面去解决连接成本过高的问题,即使是 HTTP 协议的最初版本(指 HTTP/1.0,忽略非正式的 HTTP/0.9 版本)就已经支持了连接复用技术(连接复用技术在 HTTP/1.0 中并不是默认开启的,是在 HTTP/1.1 中变为默认开启),即今天大家所熟知的[持久连接](https://en.wikipedia.org/wiki/HTTP_persistent_connection)(Persistent Connection),也称为连接[Keep-Alive 机制](https://en.wikipedia.org/wiki/Keepalive)。持久连接的原理是让客户端对同一个域名长期持有一个或多个不会用完即断的 TCP 连接。典型做法是在客户端维护一个 FIFO 队列,每次取完数据(如何在不断开连接下判断取完数据将会放到稍后[传输压缩](/architect-perspective/general-architecture/diversion-system/transmission-optimization.html#传输压缩)部分去讨论)之后一段时间内不自动断开连接,以便获取下一个资源时直接复用,避免创建 TCP 连接的成本。

但是,连接复用技术依然是不完美的,最明显的副作用是“[队首阻塞](https://en.wikipedia.org/wiki/Head-of-line_blocking)”(Head-of-Line Blocking)问题。请设想以下场景:浏览器有 10 个资源需要从服务器中获取,此时它将 10 个资源放入队列,入列顺序只能按照浏览器遇见这些资源的先后顺序来决定的。但如果这 10 个资源中的第 1 个就让服务器陷入长时间运算状态会怎样呢?当它的请求被发送到服务端之后,服务端开始计算,而运算结果出来之前 TCP 连接中并没有任何数据返回,此时后面 9 个资源都必须阻塞等待。因为服务端虽然可以并行处理另外 9 个请求(譬如第 1 个是复杂运算请求,消耗 CPU 资源,第 2 个是数据库访问,消耗数据库资源,第 3 个是访问某张图片,消耗磁盘 I/O 资源,这就很适合并行),但问题是处理结果无法及时返回客户端,服务端不能哪个请求先完成就返回哪个,更不可能将所有要返回的资源混杂到一起交叉传输,原因是只使用一个 TCP 连接来传输多个资源的话,如果顺序乱了,客户端就很难区分哪个数据包归属哪个资源了。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public @interface NotConflictAccount {

```

另外一条建议是将不带业务含义的格式校验注解放到 Bean 的类定义之上,将带业务逻辑的校验放到 Bean 的类定义的外面。这两者的区别是放在类定义中的注解能够自动运行,而放到类外面这需要想前面代码那样,明确标出注解时才会运行。譬如用户账号实体中的部分代码为:
另外一条建议是将不带业务含义的格式校验注解放到 Bean 的类定义之上,将带业务逻辑的校验放到 Bean 的类定义的外面。这两者的区别是放在类定义中的注解能够自动运行,而放到类外面这需要像前面代码那样,明确标出注解时才会运行。譬如用户账号实体中的部分代码为:

```java
public class Account extends BaseEntity {
Expand Down
2 changes: 1 addition & 1 deletion distribution/traffic-management/traffic-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

- **每秒事务数**(Transactions per Second,TPS):TPS 是衡量信息系统吞吐量的最终标准。“事务”可以理解为一个逻辑上具备原子性的业务操作。譬如你在 Fenix's Bookstore 买了一本书,将要进行支付,“支付”就是一笔业务操作,支付无论成功还是不成功,这个操作在逻辑上是原子的,即逻辑上不可能让你买本书还成功支付了前面 200 页,又失败了后面 300 页。
- **每秒请求数**(Hits per Second,HPS):HPS 是指每秒从客户端发向服务端的请求数(请将 Hits 理解为 Requests 而不是 Clicks,国内某些翻译把它理解为“每秒点击数”多少有点望文生义的嫌疑)。如果只要一个请求就能完成一笔业务,那 HPS 与 TPS 是等价的,但在一些场景(尤其常见于网页中)里,一笔业务可能需要多次请求才能完成。譬如你在 Fenix's Bookstore 买了一本书要进行支付,尽管逻辑上它是原子的,但技术实现上,除非你是直接在银行开的商城中购物能够直接扣款,否则这个操作就很难在一次请求里完成,总要经过显示支付二维码、扫码付款、校验支付是否成功等过程,中间不可避免地会发生多次请求。
- **每秒查询数**(Queries per Second,QPS):QPS 是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那 QPS 和 HPS 是等价的,但在分布式系统中,一个请求的响应往往要由后台多个服务节点共同协作来完成。譬如你在 Fenix's Bookstore 买了一本书要进行支付,尽管扫描支付二维码时尽管客户端只发送了一个请求,但这背后服务端很可能需要向仓储服务确认库存信息避免超卖、向支付服务发送指令划转货款、向用户服务修改用户的购物积分,等等,这里面每次内部访问都要消耗掉一次或多次查询数。
- **每秒查询数**(Queries per Second,QPS):QPS 是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那 QPS 和 HPS 是等价的,但在分布式系统中,一个请求的响应往往要由后台多个服务节点共同协作来完成。譬如你在 Fenix's Bookstore 买了一本书要进行支付,尽管扫描支付二维码时客户端只发送了一个请求,但这背后服务端很可能需要向仓储服务确认库存信息避免超卖、向支付服务发送指令划转货款、向用户服务修改用户的购物积分,等等,这里面每次内部访问都要消耗掉一次或多次查询数。

以上这三个指标都是基于调用计数的指标,在整体目标上我们当然最希望能够基于 TPS 来限流,因为信息系统最终是为人类用户来提供服务的,用户不关心业务到底是由多少个请求、多少个后台查询共同协作来实现。但是,系统的业务五花八门,不同的业务操作对系统的压力往往差异巨大,不具备可比性;而更关键的是,流量控制是针对用户实际操作场景来限流的,这不同于压力测试场景中无间隙(最多有些集合点)的全自动化操作,真实业务操作的耗时无可避免地受限于用户交互带来的不确定性,譬如前面例子中的“扫描支付二维码”这个步骤,如果用户掏出手机扫描二维码前先顺便回了两条短信息,那整个付款操作就要持续更长时间。此时,如果按照业务开始时计数器加 1,业务结束时计数器减 1,通过限制最大 TPS 来限流的话,就不能准确地反应出系统所承受的压力,所以直接针对 TPS 来限流实际上是很难操作的。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

> **场景二**:假设你现在有两个 Docker 镜像,其中一个封装了 HTTP 服务,为便于称呼,我们叫它 Nginx 容器,另一个封装了日志收集服务,我们叫它 Filebeat 容器。现在要求 Filebeat 容器能收集 Nginx 容器产生的日志信息。
场景二依然不难解决,只要在 Nginx 容器和 Filebeat 容器启动时,分别将它们的日志目录和收集目录挂载为宿主机同一个磁盘位置的 Volume 即可,这种操作在 Docker 中是十分常用的容器间信息交换手段。不过,容器间信息交换不仅仅是文件系统,假如此时我又引入了一个新的工具[confd](https://github.com/kelseyhightower/confd)——Linux 下的一种配置管理工具,作用是根据配置中心(Etcd、ZooKeeper、Consul)的变化自动更新 Nginx 的配置,这里便又会遇到新的问题。confd 需要向 Nginx 发送 HUP 信号以便[通知 Nginx](http://nginx.org/en/docs/control.html)配置已经发生了变更,而发送 HUP 信号自然要求 confd 与 Nginx 能够进行 IPC 通信才行。尽管共享 IPC 名称空间不如共享 Volume 常见,但 Docker 同样支持了该功能,docker run 提供了`--ipc`参数,用于把多个容器挂在到同一个父容器的 IPC 名称空间之下,以实现容器间共享 IPC 名称空间的需求。类似地,如果要共享 UTS 名称空间,可以使用`--uts`参数,要共享网络名称空间的话,就使用`--net`参数。
场景二依然不难解决,只要在 Nginx 容器和 Filebeat 容器启动时,分别将它们的日志目录和收集目录挂载为宿主机同一个磁盘位置的 Volume 即可,这种操作在 Docker 中是十分常用的容器间信息交换手段。不过,容器间信息交换不仅仅是文件系统,假如此时我又引入了一个新的工具[confd](https://github.com/kelseyhightower/confd)——Linux 下的一种配置管理工具,作用是根据配置中心(Etcd、ZooKeeper、Consul)的变化自动更新 Nginx 的配置,这里便又会遇到新的问题。confd 需要向 Nginx 发送 HUP 信号以便[通知 Nginx](http://nginx.org/en/docs/control.html)配置已经发生了变更,而发送 HUP 信号自然要求 confd 与 Nginx 能够进行 IPC 通信才行。尽管共享 IPC 名称空间不如共享 Volume 常见,但 Docker 同样支持了该功能,docker run 提供了`--ipc`参数,用于把多个容器挂载到同一个父容器的 IPC 名称空间之下,以实现容器间共享 IPC 名称空间的需求。类似地,如果要共享 UTS 名称空间,可以使用`--uts`参数,要共享网络名称空间的话,就使用`--net`参数。

以上便是 Docker 针对场景二这种不跨机器的多容器协作所给出的解决方案,自动地为多个容器设置好共享名称空间其实就是[Docker Compose](https://docs.docker.com/compose/)提供的核心能力。这种针对具体应用需求来共享名称空间的方案,的确可以工作,却并不够优雅,也谈不上有什么扩展性。容器的本质是对 cgroups 和 namespaces 所提供的隔离能力的一种封装,在 Docker 提倡的单进程封装的理念影响下,容器蕴含的隔离性也多了仅针对于单个进程的额外局限,然而 Linux 的 cgroups 和 namespaces 原本都是针对进程组而不仅仅是单个进程来设计的,同一个进程组中的多个进程天然就可以共享着相同的访问权限与资源配额。如果现在我们把容器与进程在概念上对应起来,那容器编排的第一个扩展点,就是要找到容器领域中与“进程组”相对应的概念,这是实现容器从隔离到协作的第一步,在 Kubernetes 的设计里,这个对应物叫作 Pod。

Expand Down

0 comments on commit 54cb1bb

Please sign in to comment.