Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >gRPC 网关,针对 HTTP 2.0 长连接性能优化,提升吞吐量

gRPC 网关,针对 HTTP 2.0 长连接性能优化,提升吞吐量

作者头像
微观技术
发布于 2021-07-05 07:34:24
发布于 2021-07-05 07:34:24
4.2K00
代码可运行
举报
文章被收录于专栏:微观技术微观技术
运行总次数:0
代码可运行

大家好,我是Tom哥~

最近要搞个网关GateWay,由于系统间请求调用是基于gRPC框架,所以网关第一职责就是能接收并转发gRPC请求,大致的系统架构如下所示:

简单看下即可,由于含有定制化业务背景,架构图看不懂也没关系,后面我会对里面的核心技术点单独剖析讲解

为什么要引入网关?请求链路多了一跳,性能有损耗不说,一旦宕机就全部玩完了!

但现实就是这样,不是你想怎么样,就能怎么样!

有时技术方案绕一个大圈子,就是为了解决一个无法避开的因素。这个因素可能是多方面:

  • 可能是技术上的需求,比如要做监控统计,需要在上层某个位置加个拦截层,收集数据,统一处理
  • 可能是技术实现遇到巨大挑战,至少是当前技术团队研发实力解决不了这个难题
  • 可能上下文会话关联,一个任务要触发多次请求,但始终要在一台机器上完成全部处理
  • 可能是政策因素,为了数据安全,你必须走这一绕。

本文引入的网关就是安全原因,由于一些公司的安全限制,外部服务无法直接访问公司内部的计算节点,需要引入一个前置网关,负责反向代理、请求路由转发、数据通信、调用监控等。

1、问题抽象,技术选型

上面的业务架构可能比较复杂,不了解业务背景同学很容易被绕晕。那么我们简化一些,抽象出一个具体要解决的问题,简化描述。

过程分为三步:

1、client端发起gPRC调用(基于HTTP2),请求打到gRPC网关

2、网关接到请求,根据请求约定的参数标识,从Redis缓存里查询目标服务器的映射关系

3、最后,网关将请求转发给目标服务器,获取响应结果,将数据原路返回。

gRPC必须使用 HTTP/2 传输数据,支持明文和TLS加密数据,支持流数据的交互。充分利用 HTTP/2 连接的多路复用和流式特性。

技术选型

1、最早计划采用Netty来做,但由于gRPC的proto模板不是我们定义的,所以解析成本很高,另外还要读取请求Header中的数据,开发难度较大,所以这个便作为了备选方案。

2、另一种改变思路,往反向代理框架方向寻找,重新回到主流的Nginx这条线,但是nginx采用C语言开发,如果是基于常规的负载均衡策略转发请求,倒是没什么大的问题。但是,我们内部有依赖任务资源关系,也间接决定着要依赖外部的存储系统。

Nginx适合处理静态内容,做一个静态web服务器,但我们又看重其高性能,最后我们选型 Openresty

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

2、Openresty 代码 SHOW

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
http {
    include       mime.types;
    default_type  application/octet-stream;
    access_log  logs/access.log  main;
    sendfile        on;
    keepalive_timeout  120;
    client_max_body_size 3000M;
    server {
        listen   8091   http2;
        location / {
            set $target_url  '' ;
            access_by_lua_block{
                local headers = ngx.req.get_headers(0)
                local jobid= headers["jobid"]
                local redis = require "resty.redis"
                local red = redis:new()
                red:set_timeouts(1000) -- 1 sec
                local ok, err = red:connect("156.9.1.2", 6379)
                local res, err = red:get(jobid)
                ngx.var.target_url = res
            }
            grpc_pass   grpc://$target_url;
        }
    }
}

3、性能压测

1、Client 端机器,压测期间,观察网络连接:

结论:

并发压测场景下,请求会转发到三台网关服务器,每台服务器处于TIME_WAIT状态的TCP连接并不多。可见此段连接基本能达到连接复用效果。

2、gRPC网关机器,压测期间,观察网络连接情况:

有大量的请求连接处于TIME_WAIT状态。按照端口号可以分为两大类:637940928

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
LISTEN 2
ESTABLISHED 6
TIME_WAIT 27500

通过linux shell 统计命令,172.16.66.46服务器有27500个TCP连接处于 TIME_WAIT

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | grep 6379 |awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
ESTABLISHED 1
TIME_WAIT 13701

其中,连接redis(redis的访问端口 6379) 并处于 TIME_WAIT 状态有 13701 个连接

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | grep 40928 |awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
ESTABLISHED 2
TIME_WAIT 13671

其中,连接后端Server目标服务器 并处于 TIME_WAIT 状态有 13671 个连接。两者的连接数基本相等,因为每一次转发请求都要查询一次Redis。

结论汇总:

1、client端发送请求到网关,目前已经维持长连接,满足要求。

2、gRPC网关连接Redis缓存服务器,目前是短连接,每次请求都去创建一个连接,性能开销太大。需要单独优化

3、gRPC网关转发请求到目标服务器,目前也是短连接,用完即废弃,完全没有发挥Http2.0的长连接优势。需要单独优化

4、什么是 TIME_WAIT

统计服务器tcp连接状态处于TIME_WAIT的命令脚本:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
netstat -anpt | grep TIME_WAIT | wc -l 

我们都知道TCP是三次握手,四次挥手。那挥手具体过程是什么?

1、主动关闭连接的一方,调用close(),协议层发送FIN包,主动关闭方进入FIN_WAIT_1状态

2、被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close()操作

3、被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;

4、主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态

5、等待 2MSL(Maximum Segment Lifetime, 报文最大生存时间),主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

2MSL到底有多长呢?这个不一定,1分钟、2分钟或者4分钟,还有的30秒。不同的发行版可能会不同。在Centos 7.6.1810 的3.10内核版本上是60秒。

来张TCP状态机大图,一目了然:

为什么一定要有 TIME_WAIT ?

虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接到CLOSED状态。但是网络是不可靠的,发起方无法确保最后发送的ACK报文一定被对方收到,比如丢包或延迟到达,对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文。所以TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

简单讲,TIME_WAIT之所以等待2MSL的时长,是为了避免因为网络丢包或者网络延迟而造成的tcp传输不可靠,而这个TIME_WAIT状态则可以最大限度的提升网络传输的可靠性。

注意:一个连接没有进入 CLOSED 状态之前,这个连接是不能被重用的!

如何优化 TIME_WAIT 过多的问题

1、调整系统内核参数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将 TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中 TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout =  修改系统默认的 TIMEOUT 时间
net.ipv4.tcp_max_tw_buckets = 5000 表示系统同时保持TIME_WAIT套接字的最大数量,(默认是18000).TIME_WAIT连接数量达到给定的值时,所有的TIME_WAIT连接会被立刻清除,并打印警告信息。但这种粗暴的清理掉所有的连接,意味着有些连接并没有成功等待2MSL,就会造成通讯异常。一般不建议调整
net.ipv4.tcp_timestamps = 1(默认即为1)60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。也就是说服务器打开了 tcp_tw_reccycle了,就会检查时间戳,如果对方发来的包的时间戳是乱跳的或者说时间戳是滞后的,那么服务器就会丢掉不回包,现在很多公司都用LVS做负载均衡,通常是前面一台LVS,后面多台后端服务器,这其实就是NAT,当请求到达LVS后,它修改地址数据后便转发给后端服务器,但不会修改时间戳数据,对于后端服务器来说,请求的源地址就是LVS的地址,加上端口会复用,所以从后端服务器的角度看,原本不同客户端的请求经过LVS的转发,就可能会被认为是同一个连接,加之不同客户端的时间可能不一致,所以就会出现时间戳错乱的现象,于是后面的数据包就被丢弃了,具体的表现通常是是客户端明明发送的SYN,但服务端就是不响应ACK,还可以通过下面命令来确认数据包不断被丢弃的现象,所以根据情况使用

其他优化:
net.ipv4.ip_local_port_range = 1024 65535 ,增加可用端口范围,让系统拥有的更多的端口来建立链接,这里有个问题需要注意,对于这个设置系统就会从1025~65535这个范围内随机分配端口来用于连接,如果我们服务的使用端口比如8080刚好在这个范围之内,在升级服务期间,可能会出现8080端口被其他随机分配的链接给占用掉
net.ipv4.ip_local_reserved_ports = 7005,8001-8100 针对上面的问题,我们可以设置这个参数来告诉系统给我们预留哪些端口,不可以用于自动分配。

2、将短连接优化为长连接

短连接工作模式:连接->传输数据->关闭连接

长连接工作模式:连接->传输数据->保持连接 -> 传输数据-> 。。。->关闭连接

5、访问 Redis 短连接优化

高并发编程中,必须要使用连接池技术,把短链接改成长连接。也就是改成创建连接、收发数据、收发数据... 拆除连接,这样我们就可以减少大量创建连接、拆除连接的时间。从性能上来说肯定要比短连接好很多

在 OpenResty 中,可以设置set_keepalive 函数,来支持长连接。

set_keepalive 函数有两个参数:

  • 第一个参数:连接的最大空闲时间
  • 第二个参数:连接池大小
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
local res, err = red:get(jobid)
// redis操作完后,将连接放回到连接池中
// 连接池大小设置成40,连接最大空闲时间设置成10秒
red:set_keepalive(10000, 40)

reload nginx配置后,重新压测

结论:redis的连接数基本控制在40个以内。

其他的参数设置可以参考: https://github.com/openresty/lua-resty-redis#set_keepalive

6、访问目标 Server 机器短连接优化

nginx 提供了一个upstream模块,用来控制负载均衡、内容分发。提供了以下几种负载算法:

  • 轮询(默认)。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
  • weight(权重)。指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
  • ip_hash。每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
  • fair(第三方)。按后端服务器的响应时间来分配请求,响应时间短的优先分配。
  • url_hash(第三方)。按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

由于 upstream提供了keepalive函数,每个工作进程的高速缓存中保留的到上游服务器的空闲保持连接的最大数量,可以保持连接复用,从而减少TCP连接频繁的创建、销毁性能开销。

缺点:

Nginx官方的upstream不支持动态修改,而我们的目标地址是动态变化,请求时根据业务规则动态实时查询路由。为了解决这个动态性问题,我们引入OpenRestybalancer_by_lua_block

通过编写Lua脚本方式,来扩展upstream功能。

修改nginx.confupstream,动态获取路由目标的IP和Port,并完成请求的转发,核心代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 upstream grpcservers {
    balancer_by_lua_block{
      local balancer = require "ngx.balancer"
      local host = ngx.var.target_ip
      local port = ngx.var.target_port
      local ok, err = balancer.set_current_peer(host, port)
      if not ok then
         ngx.log(ngx.ERR, "failed to set the current peer: ", err)
         return ngx.exit(500)
      end
    }
    keepalive 40;
 }

修改配置后,重启Nginx,继续压测,观察结果:

TCP连接基本都处于ESTABLISHED状态,优化前的TIME_WAIT状态几乎没有了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[root@tf-gw-64bd9f775c-qvpcx nginx]#  netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
LISTEN 2
ESTABLISHED 86
TIME_WAIT 242

写在最后

本文主要是解决gRPC的请求转发问题,构建一个网关系统,技术选型OpenResty,既保留了Nginx的高性能又兼具了OpenResty动态易扩展。然后针对编写的LUA代码,性能压测,不断调整优化,解决各个链路区间的TCP连接保证可重复使用。


关于我:前阿里架构师,出过专利,竞赛拿过奖,CSDN博客专家,负责过电商交易、社区生鲜、营销、金融等业务,多年团队管理经验,爱思考,喜欢结交朋友

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微观技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
nginx使用长连接代理grpc流量
Nginx在1.13.10版本支持了对grpc流量的反向代理,恰好业务有需求,要在sidecar容器中代理grpc流量。因此参考指引文档进行了配置。但是并未如预期般顺利运行,按照示例配置后,nginx与后端的grpc服务并非长连接,导致了一系列问题,在此做个记录,也给有需要的读者做一个参考,对具体过程不感兴趣的可直接跳到最后查看完整配置。
我有一只萌妹子
2022/10/09
3.8K3
如何优化高并发TCP链接中产生的大量的TIME_WAIT的状态
线上有几台QPS每秒几万请求的服务器,大致访问链路如下:client -> nginx -> web 服务器,由于每台机器上混布了多个web服务并通过nginx反向代理统一分发请求,在服务升级的时候经常出现端口被占用的情况,排查问题时,发现系统过存在几万多个 time_wait状态。统计命令如下:
我是攻城师
2020/02/25
27.7K3
time_wait 详解和解决方案
TCP 连接关闭时,会有 4 次通讯(四次挥手),来确认双方都停止收发数据了。如上图,主动关闭方,最后发送 ACK 时,会进入 TIME_WAIT 状态,要等 2MSL 时间后,这条连接才真正消失。
蘑菇先生
2020/04/13
3K0
玩转CVM之tw_reuse和tw_recycle 罪与罚
1.CVM ping测试正常,但使用TCP连接,偶尔出现超时或延时较大,而此时网络并没有发生抖动。
苏欣
2019/07/24
8.5K0
tcp如何维护长连接
上次提到tcp数据流无边界特点 还有一个特点那就是 TCP有长连接和短连接之分 目录结构: tcp连接的终止 — 01 — socke正常关闭 流程: 被动关闭一方接受完毕数据 然后发送
早起的鸟儿有虫吃
2018/04/13
2.9K0
tcp如何维护长连接
Time Wait的作用、原因、影响和如何避免
TIME_WAIT状态存在的理由: 1)可靠地实现TCP全双工连接的终止 在进行关闭连接四次挥手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,服务器将重发最终的FIN, 因此客户端必须维护状态信息允许它重发最终的ACK。如果不维持这个状态信息,那么客户端将响应RST分节,服务器将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。 因而,要实现TCP全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的客户端必须维持状态信息进入TIME_WAIT状态。
全栈程序员站长
2022/09/06
2.5K1
Time Wait的作用、原因、影响和如何避免
TCP连接的TIME_WAIT和CLOSE_WAIT 状态解说-运维笔记
相信很多运维工程师遇到过这样一个情形: 用户反馈网站访问巨慢, 网络延迟等问题, 然后就迫切地登录服务器,终端输入命令"netstat -anp | grep TIME_WAIT | wc -l " 查看一下, 接着发现有几百几千甚至几万个TIME_WAIT 连接数. 顿时慌了~
洗尽了浮华
2018/11/21
3.3K0
记一次Redis连接池问题引发的RST
如图所示,服务器发送了大量的 reset,在我 watch 的时候还在发,多半有问题。
LA0WAN9
2021/12/14
1.2K0
记一次Redis连接池问题引发的RST
你所不知道的TIME_WAIT和CLOSE_WAIT
然后打开Google,输入关键词:too many timewait。一定能找到解决方案,而排在最前面或者被很多人到处转载的解决方案一定是:
sunsky
2020/08/20
3.1K0
你所不知道的TIME_WAIT和CLOSE_WAIT
tomcat大量time wait问题
在服务端访问量大的时候检测到大量的time wait,并且接口请求延时较高。 执行 netstat -n |awk ‘/^tcp/{++S[$NF]}END{for(m in S) print m,S[m]}’ 这个shell命令的意思是把netstat -n 后结果的最后一条放到S[]数组中,如果相同则执行+1操作。 此时能看到TCP各种状态下的连接数量,示例
全栈程序员站长
2022/08/14
1.1K0
tomcat大量time wait问题
重传问题四阶段优化分享
使用wrk模拟http压力打nginx时,发现压测过程中持续出现重传现象,而且在高压下和低压下都会出现不同程度的重传。
mingjie
2022/05/12
1.1K0
重传问题四阶段优化分享
Redis连接数为何会偏高
本文介绍了ThinkPHP和YII2两个框架中对于redis的典型使用场景,通过连接数偏高的现象引出了长连接与短连接的概念,并且简单描述了几种网络连接状态,包括TIME_WAIT,ESTABLISHED,同时介绍了应用开发中Socket与TCP UDP的关联关系。
needrunning
2019/07/04
5.1K0
Redis连接数为何会偏高
服务端不回应客户端的syn握手,三次握手失败原因排查
测试环境有一个后台服务,部署在内网服务器A上(无外网地址),给app提供接口。app访问这个后台服务时,ip地址是公网地址,那这个请求是如何到达我们的内网服务器A呢,这块我咨询了网络同事,我画了简图如下:
低级知识传播者
2023/08/30
3.1K1
服务端不回应客户端的syn握手,三次握手失败原因排查
TCP连接中time_wait在开发中的影响-搜人以鱼不如授之以渔
  根据TCP协议定义的3次握手断开连接规定,发起socket主动关闭的一方socket将进入TIME_WAIT状态,TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),TIME_WAIT状态下的socket不能被回收使用. 具体现象是对于一个处理大量短连接的服务器,如果是由服务器主动关闭客户端的连接,将导致服务器端存在大量的处于TIME_WAIT状态的socket, 甚至比处于Established状态下的socket多的多,严重影响服务器的处理能力,甚至耗尽可用的socket,停止服务. TIME_WAIT是TCP协议用以保证被重新分配的socket不会受到之前残留的延迟重发报文影响的机制,是必要的逻辑保证。
intsmaze-刘洋
2018/08/29
1.1K0
TCP连接中time_wait在开发中的影响-搜人以鱼不如授之以渔
Linux下TCP连接过程总结
一、Linux服务器上11种网络连接状态:       图:TCP的状态机 通常情况下,一个正常的TCP连接,都会有三个阶段:1、TCP三次握手; 2、数据传送; 3、TCP四次挥手 注:以下说明最
猿人谷
2018/01/17
5K0
Linux下TCP连接过程总结
性能优化|Tomcat 服务优化
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
民工哥
2020/09/16
9640
gRPC 长连接在微服务业务系统中的实践
作者 | 张琦 长连接和短连接哪个更好, 一直是被人反复讨论且乐此不疲的话题。有人追求短连接的简单可靠, 有人却对长连接的低延时趋之若鹜。那么长连接到底好在哪里, 它是否是解决性能问题的银弹? 本文就
深度学习与Python
2020/11/16
4K0
gRPC 长连接在微服务业务系统中的实践
vivo AI计算平台 Kubernetes集群Ingress网关实践
vivo 人工智能计算平台小组从 2018 年底开始建设 AI 计算平台至今,已经在 kubernetes 集群、以及离线的深度学习模型训练等方面,积累了众多宝贵的开发、运维经验,并逐步打造出稳定的基础容器平台 - AI 容器平台(VContainer)。为了支撑公司 AI 在线业务的发展,满足公司对算力资源的高效调度管控需求,需要将在线业务,主要包括 C 端、推理等业务,由原来的虚拟机或物理机迁移至 AI 容器平台。于是小组从 2020 年初开始,基于在线业务的需求对 AI 容器平台进行进一步建设,并将平台与公司的 CMDB、CICD 等基础模块进行打通,使在线业务能够顺利从虚拟机、物理机迁移至 AI 容器平台。
深度学习与Python
2020/12/18
7370
大量的 TIME_WAIT 状态连接怎么处理?(文末有福利)
Nginx 作为反向代理时,大量的短链接,可能导致 Nginx 上的 TCP 连接处于 time_wait 状态:
范蠡
2020/08/07
8.7K0
大量的 TIME_WAIT 状态连接怎么处理?(文末有福利)
网关 - OpenResty
互联网公司,不论体量大小如何,其内部的技术架构基本都是相似的,体现在以下几个方面:
iginkgo18
2021/11/24
1.4K0
相关推荐
nginx使用长连接代理grpc流量
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验