avatar

聚焦Java性能优化 打造亿级流量秒杀系统【学习笔记】03_分布式扩展

本章目标

  • nginx反向代理均衡
  • 分布式会话管理
  • 使用redis实现分布式会话存储

4-1 Nginx反向代理

上一章我们看到当单机系统时候,容量有限,响应时间变长TPS上不去的问题。nginx反向代理的功能就是代理后端Tomcat服务器集群,以统一域名方式来访问

  • 单机容量问题,水平扩展
  • nginx反向代理
  • 负载均衡配置

当我们用top -H命令查询cpu运行状况时,单机运行很卡,表象是cpu使用率增高,内存memory占用增加,网络带宽使用增加,有几个参数值得注意

  • cpu us : 用户空间cpu使用情况(用户态进程占比)
  • cpu sy:内核空间cpu使用情况(核心态进程占比)
  • load average:1分钟/5分钟/15分钟负载load平均值,跟着核系数变化,0表示正常,1表示cpu打满,1+代表等待阻塞
  • memory:free 空闲内存,used使用内存

4-2 单机容量问题,水平扩展

这次的水平扩展指的是对应用系统程序扩展,而mysql数据库只有一个,用来开放远端连接(mysql读写分离、分库分表等方法可实现mysql水平扩展,这里没讨论),服务端实现水平对称部署,最后验证访问

Nginx系统框图如下:

我们在阿里云就需要4台服务器(其中1台用作数据库,2台用作应用进程,1台用作nginx反向代理)

4-3修改前端资源用于部署nginx

nginx有三种用途:

  1. 使用nginx作为web服务器(静态资源访问)
  2. 使用nginx作为动静分离服务器
  3. 使用nginx作为反向代理服务器(动态资源请求)

整个项目前端H5请求的类型有两种,一种是静态资源,一种是ajax请求动态资源。对于ajax向域名miaoshaserver请求时,mginx会作为反向代理部署到不同miaosha项目jar包下;而对于静态资源(static,HTML,CSS等)访问域名miaoshaserver/resources时,nginx会向本地磁盘请求资源(企业级应用通常使用的是NAS

那先部署静态资源请求:

在static静态资源目录下新建gethost.js,用来方便配置修改远端连接地址,然后在每个页面上对应修改,然后将静态资源上传到服务器上

4-4 部署Nginx OpenResty

使用Nginx的框架OpenResty来开发配置Nginx,OpenResty是基于NGINX和LuaJIT的动态Web平台。优点是可以支持lua的一些开发。

安装OpenResty

将nginx指定成web服务器

  • location节点path :指定url映射key
  • location节点内容:root指定location path后对应的根路径,index指定默认的访问页
  • sbin/nginx -c conf/nginx.conf启动
  • 修改配置后直接sbin/nginx -s reload无缝重启

访问nginx服务器ip地址,出现welcome to OpenResty 则成功:smile:

4-5/6 前端资源部署

  1. scp 指令将htmlStable文件夹上传到//usr/local/openresty/nginx/html目录下

  2. 修改本地hosts文件指定nginx的IP地址域名为miaoshaserver

  3. 修改nginx目录下conf文件:

当我们浏览器访问miaoshaserver/resources/..时,访问的是/usr/local/openresty/nginx/html/resources/目录下的静态资源文件,做到H5请求静态资源到nginx服务器

  1. 使用sbin/nginx -s reload无缝重启,指的是修改nginx配置文件后不需要重启nginx服务器,连接不会断,只变化了进程号

可以看到执行命令前后 master进程不改变,worker进程的进程号变化

4-7 Nginx配置动静分离服务器

location 节点path特定resources : 静态资源路径

location节点其他路径:动态资源用

  • 设置upstream server

  • 设置动态请求location为proxy pass 路径

  • 开启tomcat access log 验证

设置upstream server

修改/usr/local/openresty/nginx/conf/nginx.conf文件

添加以下内容:配置两个应用服务器以轮询的方式 权重1:1来执行负载均衡

添加其他路径访问,设置动态请求location为proxy pass 路径

开启tomcat access log来验证反向代理是否成功,虽然开启日志要消耗一定的性能,但tomcat采用的是异步队列

在application.properties中添加:

%h表示远端host地址(远端IP地址)

%l 默认返回-

%u 表示远端主机user

%t 表示处理时间

%r 打印请求的方法/url

%s HTTP返回状态码

%b 表示请求response的大小

%D 表示处理请求时长

在miaosha/下面新建文件夹tomcat,授权所有权限sudo chmod 777 tomcat/,这个文件夹作为nginx代理请求的日志文件地址

刷新网页,发现多了一个access_log文件 tail -f access_log查看

表示 远端主机为172,26.241.152(应用服务器2)的请求,GET请求,返回码200,返回response大小32,处理时间4ms

4-8 分布式扩展后的性能压测

分布式扩展后,我们测试一下压测前后的性能,设置线程数1000,循环20次,启动时间15s进行压测

请求直接打在数据库服务器上:

nginx负载均衡服务器上:

解决了单机的容量瓶颈问题,可以做水平扩展

让我们回顾整个秒杀系统架构:

考虑整个系统优化的地方:

1)miaosha.jar包从mysql本地中分离出来,涉及到局域网的通信,而连接数据库的datasouce采用阿里巴巴druid数据库连接池,所以延迟低。

2) nginx和H5客户端采用了tomcat长连接,降低了开销

3)nginx服务器和应用服务器miaosha.jar采用的是短连接方式,可以优化

修改/usr/local/openresty/nginx/conf/nginx.conf文件

因为nginx服务器和应用服务器的通信采用HTTP1.0协议,要配置HTTP1.1才支持长连接

重新nginx服务器,压测性能:

4.9 Nginx高性能原因—epoll多路复用

  • epoll多路复用

    采用epoll多路复用机制完成非阻塞IO操作

  • master worker进程模型

    允许进程平滑重启以及平滑的加载配置保证不断开与客户端连接,可以依赖进程模型完成对应的操作

  • 协程机制

​ 基于协程的非阻塞式编程的一套机制来完成单线程、单进程的机制,却又支持并发的编程调用接口

epoll多路复用

将epoll多路复用之前,我们先要知道bio模型以及select模型

  • java bio 模型,阻塞进程式的模型

bio模型建立在java C/S基础上,客户端和服务端通过socket进行连接,一对一建立,java client只有等socket.write()所有字节流输出到TCP/IP缓存区之后,才返回,如果网络传输很慢,TCP/IP缓冲区被塞满,java client只有等信息传输过去,缓冲区有空间后,才能写入

  • linux select模型,变更触发轮询查询,有1024数量限制

select模型监听客户端连接的socket,将所有监视的socket加入阻塞队列中,阻塞自己进程,如果有连接收到数据,就遍历整个队列,将该连接移除阻塞队列,加入工作队列中

  • epoll模型,变更触发回调直接读取,理论上无上限

epoll类似于select模型,先将自己阻塞,监听客户端连接,但为了避免select遍历队列,设置回调函数,如果连接发生变化就唤醒自己并直接执行回调函数

详细可以看这篇文章,epoll原理

master worker进程模型

master-worker模型,有一个master进程,管理多个worker模型。master进程和worker进程是父子进程的方式,通过fork的方式创建,master进程可以管理worker内部的内存空间。

nginx的master进程负责启动、停止服务、重载配置文件、平缓升级等,worker进程负责于Client端socket的send和receive。

nginx创建master进程后,会创建一个socket文件句柄,用于listen()在对应端口上,等待客户端发起Connect连接请求(这里用到了epoll多路复用技术),当Client发起请求时,epoll会执行对应的回调函数。

socket建立连接三次握手:首先服务端socket先bind()在80端口,然后监听listen()端口,客户端发起Connect请求连接到80端口,在端口上的进程执行accecpt方法,进行三次握手,连接建立起socket句柄,socket可以实现socket.read()和write()的方法,这个过程就对应了request的read和response的write。

问题1:当请求端口连接时,worker进程负责执行accept方法,那么有多个worker进程,哪一个会执行?

这里nginx采用的是锁竞争的原理,互斥体,哪个进程先获得锁,就优先执行。

问题2:执行sbin/nginx -s reload语句是无缝重启,不会断开连接,原理是什么?

sbin/nginx -s reload执行前后master进程不变,worker进程改变

执行语句后,master进程发送信号给对应worker进程,要求把所有socket句柄交给master管理,master重启配置文件,生成新的worker进程,将socket句柄交由新的worker进程来管理(内存操作很快),这样就不用再进行客户端和nginx重连接

问题3:为什么worker进程是单线程,不采用多线程呢?

单线程不涉及线程间的上下文切换和通信,不会造成阻塞,更高效,而因为与client端有epoll多路复用,实现了并发管理。

协程机制

  • 依附于线程的内存模型,切换开销小

一个线程可以有多个协程,不需要cpu切换的开销,只要内存切换。

  • 遇阻塞及归还执行权,代码同步
  • 无需加锁

nginx开发不太去用c/c++,而用lua协程机制,也是基于协程机制(有点不太懂这里。。)

4-12 分布式会话管理

谈到会话管理,首先我们要先了解几个知识:

  1. 什么是会话?

    ==会话==简单理解就是 用户打开浏览器,点击很多超链接,访问多个服务器资源,然后关闭浏览器,整个过程就称之为一个会话。

  2. 保存会话的两种方式是?

    1. 基于cookie的管理方式。实现会话的方式如下:

    1)用户发起登录请求,服务端根据传入的用户密码之类的信息,验证用户,然后创建一个登陆凭证(包含Id之类的信息,还有过期时间,创建时间)

    2)服务端对登陆凭证做数字签名,对称加密,加密后写入cookie。cookie名称必须固定(比如ticket),后面再获取时还得根据名字来获取cookie值

    3)用户下次登陆时,服务端根据上一次登陆凭证的cookie名字找到相关的cookie值,然后做解密处理再做数字签名的认证,验证是否过期等等

    1. 基于token的管理方式。实现会话的方式如下:

    1)基于token的方式和基于cookie类似,区别是token返回给客户端之后,后面每次请求,都会主动把token加到http header里或者url中对token进行验证

两种目前流行的session实现方式:

  • 基于cookie传输sessionid:java tomcat容器session实现

目前我们项目使用的是springboot内嵌tomcat容器里的HTTPsession机制,是基于cookie传输sessionid的一个机制,用来标识一个用户会话项目中用到的地方是登陆/注册功能:

  • 基于token传输类似sessionid:java代码session实现

有些在移动端开发过程中,不适合用cookie开发

但以上两种不适合分布式会话,因为比如登录用于用户每次路由请求因为分布式系统导致每次请求不到一个服务器中,需要改变:

  • 基于cookie传输sessionid:java tomcat容器session实现迁移到redis
  • 基于token传输类似sessionid:java代码session实现迁移到redis

4-13 分布式会话实现(上)

pom文件添加以下两个依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>

下载redis数据库,赋予压缩包权限chmod -R 777 redis-.tar.gz,解压tar -xvzf redis..,在redis文件夹内执行命令make,进入src文件夹,执行./redis-server &后台启动redis,./redis-cli启动redis数据库

4-14 分布式会话实现(中)

在配置application.properites中添加对redis的依赖:

1
2
3
4
5
6
7
8
9
#配置springboot对redis的依赖
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=10
#spring.redis.password=

#设置jedis连接池
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.min-idle=20

更改前端gethost.js中host:从”miaoshaserver”到”localhost:8080”

测试登录界面,将UserModel模型实现序列化接口,这样session的信息才能写入redis数据库中

4-15 分布式会话实现(下)

修改redis配置文件redis.conf,指定redis的访问:添加bind redis数据库的内网IP地址

启动redis : src/redis-server ./redis.conf &

查看6379端口情况 netstat -an | grep 6379 地址正常即成功

切换到应用服务器1,修改配置文件application.properties,

增加一句 spring.redis.host = redis服务器内网IP

重新部署jar包,成功

检查6379端口,出现下面的信息即为成功

测试打开登陆界面,登陆用户后下单,因为同一个登陆用户的session,可重复下单

4-16 基于token的分布式会话实现(上)

上次我们完成了基于cookie传输sessionid的实现,现在来做基于token的分布式会话的实现。

token类似于服务端下发sessionid植入到cookie,下发令牌,交给前端,等登陆请求时,把token加入到http header或url中。

修改UserController.java

在前面添加@Autowired private RedisTemplate redisTemplate; tomcat内嵌redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


//将登录凭证加入到用户登陆成功的session内

//修改成若用户登陆验证成功后将对应的登陆信息和登陆凭证一起存入redis中

//生成登陆凭证token,UUID
String uuidToken = UUID.randomUUID().toString();
uuidToken = uuidToken.replace("-","");
//建立token和用户登录态之间的联系
redisTemplate.opsForValue().set(uuidToken,userModel);
//设置过期时间1h
redisTemplate.expire(uuidToken,1, TimeUnit.HOURS);
//
// this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true);
// this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel);

//下发token
return CommonReturnType.create(uuidToken);

修改OrderController.java,加入redisTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


//Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
String token = httpServletRequest.getParameterMap().get("token")[0];

if(StringUtils.isEmpty(token)) {
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"请登录后下单");
}
//获取用户的登陆信息
UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
if(userModel == null) {
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"请登录后下单");
}

//UserModel userModel = (UserModel) httpServletRequest.getSession().getAttribute("LOGIN_USER");

修改对应前端页面

点击下单,请求url中出现token,说明session加入到了token中

文章作者: SkironYong
文章链接: https://skironyong.github.io/SkironYong.github.io/posts/75e99a1b.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SkironYong
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论