高性能网关设计实践

前言

以前的高性能短链设计一文颇受你们好评,共被转载 「47」 次,受宠若惊,在此感谢你们的承认!在文末简单提了一下 OpenResty,一些读者比较感兴趣,恰好咱们接入层网关也是用的 OpenResty,因此但愿经过对网关设计的介绍来简单总结一下 OpenResty 的相关知识点,争取让你们对 OpenResty 这种高性能 Web 平台有一个比较全面的了解。本文会从如下几个方面来说解。java

  • 网关的做用
  • 接入层网关架构设计与实现
  • 技术选型
  • OpenResty 原理剖析

网关的做用

网关做为全部请求的流量入口,主要承担着安全,限流,熔断降级,监控,日志,风控,鉴权等功能,网关主要有两种类型mysql

  • 一种是接入层网关(access gateway),主要负责路由,WAF(防止SQL Injection, XSS, 路径遍历, 窃取敏感数据,CC攻击等),限流,日志,缓存等,这一层的网关主要承载着将请求路由到各个应用层网关的功能nginx

  • 另外一种是应用层网关,好比如今流行的微服务,各个服务多是用不一样的语言写的,如 PHP,Java 等,那么接入层就要将请求路由到相应的应用层集群,再由相应的应用层网关进行鉴权等处理,处理完以后再调用相应的微服务进行处理,应用层网关也起着路由,超时,重试,熔断等功能。程序员

目前市面上比较流行的系统架构以下web

能够看到接入层网关承载着公司的全部流量,对性能有很高的要求,它的设计决定着整个系统的上限。因此咱们今天主要谈谈接入层网关的设计。面试

接入层网关架构设计与实现

首先咱们要明白接入层网关的核心功能是:「根据路由规则将请求分发到对应的后端集群」,因此要实现以下几个功能模型 。redis

一、 路由:根据请求的 host, url 等规则转发到指定的上游(相应的后端集群) 二、 路由策略插件化:这是网关的「灵魂所在」,路由中会有身份认证,限流限速,安全防御(如 IP 黑名单,refer异常,UA异常,需第一时间拒绝)等规则,这些规则以插件的形式互相组合起来以便只对某一类的请求生效,每一个插件都即插即用,互不影响,这些插件应该是「动态可配置」的,动态生效的(无须重启服务),为啥要可动态可配置呢,由于每一个请求对应的路由逻辑,限流规则,最终请求的后端集群等规则是不同的sql

如图示,两个请求对应的路由规则是不同的,它们对应的路由规则(限流,rewrite)等经过各个规则插件组合在一块儿,能够看到,光两个请求 url 的路由规则就有挺多的,若是一个系统大到必定程度,url 会有很多,就会有很多规则,这样每一个请求的规则就必须「可配置化」「动态化」,最好能在管理端集中控制,统一下发。数据库

三、后端集群的动态变动apache

路由规则的应用是为了肯定某一类请求通过这些规则后最终到达哪个集群,而咱们知道请求确定是要打到某一台集群的 ip 上的,而机器的扩缩容实际上是比较常见的,因此必须支持动态变动,总不能我每次上下线机器的时候都要重启系统让它生效吧。

四、监控统计,请求量、错误率统计等等

这个比较好理解,在接入层做全部流量的请求,错误统计,便于打点,告警,分析。

要实现这些需求就必须对咱们采用的技术:OpenResty 有比较详细的了解,因此下文会简单介绍一下 OpenResty 的知识点。

技术选型

有人可能第一眼想到用 Nginx,没错,因为 Nginx 采用了 epoll 模型(非阻塞 IO 模型),确实能知足大多数场景的需求(通过优化 100 w + 的并发数不是问题),可是 Nginx 更适合做为静态的 Web 服务器,由于对于 Nginx 来讲,若是发生任何变化,都须要修改磁盘上的配置,而后从新加载才能生效,它并无提供 API 来控制运行时的行为,而如上文所述,动态化是接入层网关很是重要的一个功能。因此通过一番调研,咱们选择了 OpenResty,啥是 OpenResty 呢,来看下官网的定义:

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建可以处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不只仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

能够简单理解为,OpenResty = Nginx + Lua, 经过 Lua 扩展 Nginx 实现的可伸缩的 Web 平台 。它利用了 Nginx 的高性能,又在其基础上添加了 Lua 的脚本语言来让 Nginx 也具备了动态的特性。经过 OpenResty 中 lua-Nginx-module 模块中提供的 Lua API,咱们能够动态地控制路由、上游、SSL 证书、请求、响应等。甚至能够在不重启 OpenResty 的前提下,修改业务的处理逻辑,并不局限于 OpenResty 提供的 Lua API。

关于静态和动态有一个很合适的类比:若是把 Web 服务器当作是一个正在高速公路上飞驰的汽车,Nginx 须要停车才能更换轮胎,更换车漆颜色,而 OpenResty 中能够边跑边换轮胎,更换车漆,甚至更换发动机,直接让普通的汽车变成超跑!

除了以上的动态性,还有两个特性让 OpenResty 独出一格。

「1.详尽的文档和测试用例」

做为开源项目,文档和测试毫无疑问是其是否靠谱的关键,它的文档很是详细,做者把每一个注意的点都写在文档上了,多数时候只要看文档便可,每个测试案例都包含完整的 Nginx 配置和 lua 代码。以及测试的输入数据和预期的输出数据。

「2.同步非阻塞」

OpenResty 在诞生之初就支持了协程,而且基于此实现了同步非阻塞的编程模型。

「画外音:协程(coroutine)咱们能够将它当作一个用户态的线程,只不过这个线程是咱们本身调度的,并且不一样协程的切换不须要陷入内核态,效率比较高。(通常咱们说的线程是要指内核态线程,由内核调度,须要从用户空间陷入内核空间,相比协程,对性能会有不小的影响)」

啥是同步非阻塞呢。假设有如下两个两行代码:

local res, err  = query-mysql(sql)
local value, err = query-redis(key)

「同步」:必须执行完查询 mysql,才能执行下面的 redis 查询,若是不等 mysql 执行完成就能执行 redis 则是异步。

「阻塞」:假设执行 sql 语句须要 1s,若是在这 1s 内,CPU 只能干等着不能作其它任何事,那就是阻塞,若是在 sql 执行期间能够作其余事(注意因为是同步的,因此不能执行如下的 redis 查询),则是非阻塞。

同步关注的是语句的前后执行顺序,若是上一个语句必须执行完才能执行下一个语句就是同步,若是不是,就是异步,阻塞关注的是线程是 CPU 是否须要在 IO 期间干等着,若是在 IO(或其余耗时操做期间)期间能够作其余事,那就是非阻塞,不能动,则是阻塞。

那么 OpenResty 的工做原理是怎样的呢,又是如何实现同步非阻塞的呢。

OpenResty 原理剖析

工做原理剖析

因为 OpenResty 基于 Nginx 实现的,咱们先来看看 Nginx 的工做原理

Nginx 启动后,会有一个 master 进程和多个 worker 进程 , master 进程接受管理员的信号量(如 Nginx -s reload, -s stop)来管理 worker 进程,master 自己并不接收 client 的请求,主要由 worker 进程来接收请求,不一样于 apache 的每一个请求会占用一个线程,且是同步IO,Nginx 是异步非阻塞的,每一个 worker 能够同时处理的请求数只受限于内存大小,这里就要简单地了解一下 nginx 采用的 epoll 模型:

epoll 采用多路复用模型,即同一时间虽然可能会有多个请求进来, 但只会用一个线程去监视,而后哪一个请求数据准备好了,就调用相应的线程去处理,就像图中所示,如同拨开关同样,同一时间只有一个线程在处理, Nginx 底层就是用的 epoll ,基于事件驱动模型,每一个请求进来注册事件并注册 callback 回调函数,等数据准入好了,就调用回调函数进行处理,它是异步非阻塞的,因此性能很高。

打个简单的比方,咱们都有订票的经验,当咱们委托酒店订票时,接待员会先把咱们的电话号码和相关信息等记下来(注册事件),挂断电话后接待员在操做期间咱们就能够去作其余事了(非阻塞),当接待员把手续搞好后会主动打电话给咱们通知咱们票订好了(回调)。

worker 进程是从  master fork  出来的,这意味着 worker 进程之间是互相独立的,这样不一样 worker 进程之间处理并发请求几乎没有同步锁的限制,好处就是一个 worker 进程挂了,不会影响其余进程,咱们通常把 worker 数量设置成和 CPU 的个数,这样能够减小没必要要的 CPU 切换,提高性能,每一个 worker 都是单线程执行的。那么 LuaJIT 在 OpenResty 架构中的位置是怎样的呢。

首先启动的 master 进程带有 LuaJIT 的机虚拟,而 worker 进程是从 master 进程 fork 出来的,在 worker 内进程的工做主要由 Lua 协程来完成,也就是说在同一个 worker 内的全部协程,都会共享这个 LuaJIT 虚拟机,每一个 worker 进程里 lua 的执行也是在这个虚拟机中完成的。

同一个时间点,worker 进程只能处理一个用户请求,也就是说只有一个 lua 协程在运行,那为啥 OpenResty 能支持百万并发请求呢,这就须要了解 Lua 协程与 Nginx 事件机制是如何配合的了。

如图示,当用 Lua 调用查询 MySQL 或 网络 IO 时,虚拟机会调用 Lua 协程的 yield 把本身挂起,在 Nginx 中注册回调,此时 worker 就能够处理另外的请求了(非阻塞),等到 IO 事件处理完了, Nginx 就会调用 resume 来唤醒 lua 协程。

事实上,由 OpenResty 提供的全部 API,都是非阻塞的,下文提到的与 MySQL,Redis 等交互,都是非阻塞的,因此性能很高。

OpenResty 请求生命周期

Nginx 的每一个请求有 11 个阶段,OpenResty 也有11 个 *_by_lua 的指令,以下图示:

各个阶段 *_by_lua 的解释以下

set_by_lua:设置变量;
rewrite_by_lua:转发、重定向等;
access_by_lua:准入、权限等;
content_by_lua:生成返回内容;
header_filter_by_lua:应答头过滤处理;
body_filter_by_lua:应答体过滤处理;
log_by_lua:日志记录。

这样分阶段有啥好处呢,假设你原来的 API 请求都是明文的


# 明文协议版本
location /request {
content_by_lua '...'; # 处理请求
}

如今须要对其加上加密和解密的机制,只须要在 access 阶段解密, 在 body filter 阶段加密便可,原来 content 的逻辑无需作任务改动,有效实现了代码的解藕。

# 加密协议版本
location /request {
access_by_lua '...'; # 请求体解密
content_by_lua '...'; # 处理请求,不须要关心通讯协议
body_filter_by_lua '...'; # 应答体加密
}

再好比咱们不是要要上文提到网关的核心功能之一不是要监控日志吗,就能够统一在 log_by_lua 上报日志,不影响其余阶段的逻辑。

worker 间共享数据利器: shared dict

worker 既然是互相独立的进程,就须要考虑其共享数据的问题, OpenResty 提供了一种高效的数据结构: shared dict ,能够实如今 worker 间共享数据,shared dict 对外提供了 20 多个 Lua API,都是原子操做的,避免了高并发下的竞争问题。

路由策略插件化实现

有了以上 OpenResty 点的铺垫,来看看上文提的网关核心功能 「路由策略插件化」,「后端集群的动态变动」如何实现

首先针对某个请求的路由策略大概是这样的

整个插件化的步骤大体以下

一、每条策略由 url ,action, cluster 等组成,表明请求 url 在打到后端集群过程当中最终经历了哪些路由规则,这些规则统一在咱们的路由管理平台配置,存在 db 里。

二、OpenResty 启动时,在请求的 init 阶段 worker 进程会去拉取这些规则,将这些规则编译成一个个可执行的 lua 函数,这一个个函数就对应了一条条的规则。

须要注意的是为了不重复去 MySQL 中拉取数据,某个 worker 从 MySQL 拉取完规则(此步须要加锁,避免全部 worker 都去拉取)或者后端集群等配置信息后要将其保存在 shared dict 中,这样以后全部的 worker 请求只要从 shared dict 中获取这些规则,而后将其映射成对应模块的函数便可,若是配置规则有变更呢,配置后台经过接口通知 OpenResty 从新加载一下便可

通过路由规则肯定好每一个请求对应要打的后端集群后,就须要根据 upstream 来肯定最终打到哪一个集群的哪台机器上,咱们看看如何动态管理集群。

后端集群的动态配置

在 Nginx 中配置 upstream 的格式以下

upstream backend {
    server backend1.example.com weight=5;
    server backend2.example.com;
    server 192.0.0.1 backup;
}

以上这个示例是按照权重(weight)来划分的,6 个请求进来,5个请求打到 backend1.example.com, 1 个请求打到 backend2.example.com,若是这两台机器都不可用,就打到 192.0.0.1,这种静态配置的方式 upstream 的方式确实可行,但咱们知道机器的扩缩容有时候比较频繁,若是每次机器上下线都要手动去改,而且改完以后还要从新去 reload 无疑是不可行的,出错的几率很大,并且每次配置都要 reload 对性能的损耗也是挺大的,为了解决这个问题,OpenResty 提供了一个 dyups 的模块来解决此问题, 它提供了一个 dyups api,能够动态增,删,建立 upsteam,因此在 init 阶段咱们会先去拉取集群信息,构建 upstream,以后若是集群信息有变更,会经过以下形式调用 dyups api 来更新 upstream


-- 动态配置 upstream 接口站点
server {
     listen 127.0.0.1:81;
      location / {
          dyups_interface;
     }
}


-- 增长 upstream:user_backend
curl -d "server 10.53.10.191;" 127.0.0.1:81/upstream/user_backend

-- 删除 upstream:user_backend
curl -i -X DELETE 127.0.0.1:81/upstream/user_backend

使用 dyups 就解决了动态配置 upstream 的问题

网关最终架构设计图

经过这样的设计,最终实现了网关的配置化,动态化。

总结

网关做为承载公司全部流量的入口,对性能有着极高的要求,因此技术选型上仍是要慎重,之因此选择 OpenResty,一是由于它高性能,二是目前也有小米,阿里,腾讯等大公司在用,是久通过市场考验的,本文经过对网关的总结简要介绍了 OpenResty 的相关知识点,相信你们对其主要功能点应该有所了解了,不过 OpenResty 的知识点远不止以上这些,你们若有兴趣,能够参考文末的学习教程深刻学习,相信你们会有很多启发的。


往期精选

利用 Sharding-JDBC 解决数据库读写分离后,数据查询延时问题

基于分布式文件系统 FastDFS,利用 Zuul 网关实现滑块验证登陆

月薪8K,java初级程序员须要掌握的一些面试经验

jsp的10年是谁让它如此落幕?

 

福利彩蛋
最近整理一份很全的Java学习资料,感兴趣的老铁能够在微信搜索【 架构荟萃】,后台回复如下关键字,便可免费获取。

回复“ sb”,免费获取 SpringBoot 全套视频教程。

回复“ sc”,免费 获取 SpringCloud 全套视频教程。

回复“面试”,免费获取 Java 面试全套题集。

回复“小程序”, 免费 获取微信小程序开发 全套视频教程。

 
扫描上图二维码关注我,加v:WooolaDunzung 进微信粉丝群。


本文分享自微信公众号 - 架构荟萃(dwooola)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索