查询性能优化
一、单体项目测压
1、云服务器准备一台,阿里云服务器(对于学习用,可以选择按量付费模式)
可有效降低学习成本,不使用的时候停止服务器即可,不收取费用,需要使用的时候开启即可
我的配置
2、在服务器安装相关环境(JDK、MYSQL)
模式为传统单体项目。所有都放在一个服务器中。
3、秒杀项目准备(项目资源可留言)
商品详情页接口展示,之后将主要使用该接口测压、优化。
4、所有环境都准备好后,打包部署(阿里云服务器需配置具体的安全组规则,开放端口,不然无法对外访问)
5、接口访问成功。
6、使用 JMeter 测压
1、线程数量:700,循环次数:10。数据可进行相关调整。通过多次测压吞吐量平均值在 130/s 左右。
2、使用 pstree -p 端口号,查看线程数量
在没有测压情况下的数量:30(tomcat最小核心线程 10 + 其他线程)
3、使用 top -H 查看
cpu us:用户空间的cpu使用情况(用户层代码)
cpu sy:内核空间的cpu使用情况(系统调用)
load average:1,5,15分钟load平均值,跟着核数系数,0代表通常,1代表打满,1+代表等待阻塞i)
memory:free空闲内存,used使用内存
7、配置 springboot tomcat 配置
· 1、查看 springboot 相关 tomcat 配置
搜索 service.tomcat
springboot内置 tomcat 所有相关 默认配置都在这里。
accept-count:默认队列数量 100,
accesslog.enabled:是否开启 tomcat 日志
max-threads:最大线程数量
min-spare-threads:最小核心线程
2、springboot 中添加相关 tomcat 配置:
# 默认队列 server.tomcat.accept-count=1000 # (几核 * 2) *100 最大线程 server.tomcat.max-threads=400 # 最小核心线程 server.tomcat.min-spare-threads=100 # 开启 tomcat 日志 server.tomcat.accesslog.enabled=true # 日志路径 server.tomcat.accesslog.directory=/home/miaosha/tomcat # 日志格式 %h ip地址,%l ,%u ,%t 处理时长, "%r" 请求url, %s 返回状态码, %b 请求 response大小, %D 处理请求时长 server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D
3、配置 tomcat Keepalived 相关配置
/** * @description 当 Spring 容器内没有 TomcatEnbeddedServerFactory 这个 Bean 时,会把此 bean 加载进 Spring 中 * @author: hq * @create: 2022-08-30 23:16 **/ @Component public class WebServerConfig implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> { @Override public void customize(ConfigurableWebServerFactory factory) { // 使用对应工厂类提供的接口,定制化 Tomcat connector ((TomcatServletWebServerFactory)factory).addConnectorCustomizers(new TomcatConnectorCustomizer() { @Override public void customize(Connector connector) { Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); // 定制化 KeepalivedTimeOut,设置 30s 内没有请求则服务端自动断开 Keepalived 连接 protocol.setKeepAliveTimeout(30000); // 当客户端发送超过 1w 个请求则自动断开 Keepalived 连接 protocol.setMaxKeepAliveRequests(10000); // protocol.setMaxConnections(); 最大 soket 连接数 } }); } }
4、重新打包部署测压
启动过后会在设置 Tomcat 日志的目录生成文件夹
在未测压情况下,默认线程数量:120
压力测试时,最大线程数量为420。与未设置 tomcat.max-threads 提升了不少。
吞吐量也提升至 160/s。
查看 tomcat 日志
二、负载均衡、分布式会话
1、准备 3 台服务器(新增)
一台作为 Nginx、两台 Java。环境分别安装好。地区为成都。
开始那台机器地区为上海,之后将作为 Redis、Mysql 服务使用。
2、向两台Java1、Java2 部署程序(两台数据访问Mysql均连接上海区域服务器)
3、配置Nginx
#user nobody; CPU数量 * 2 worker_processes 4; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; pid /usr/local/nginx/logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; upstream tomcatServerPool{ server 172.29.26.12:9020 weight=10; server 172.18.160.168:9020 weight=10; } server { listen 80; server_name localhost; location / { # root html; # index index.html index.htm; proxy_pass http://tomcatServerPool; #用户IP proxy_set_header X-Real-IP $remote_addr; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
4、首先测试单机
修改IP地址
在测压的过程中,吞吐量 66/s 左右,与我们使用上海区域服务器慢了将近一般,这是因为受地区影响,后面新建的3台服务器都是成都区域,java程序数据访问上海区域,受传输距离影响所以速度就变慢了。所以看得出,跨区域太远会影响响应速度。
5、双机,nginx负载均衡测试压力
JMeter 修改IP地址为 nginx 服务器IP,端口还改为 80,线程组不变
可以看到 通过 负载均衡,已将吞吐量提升了一倍多。由 66/s 提升至 143/s 。
三、缓存
1、Redis 缓存
spring.redis.database=1 spring.redis.port=6379 spring.redis.host=******** spring.redis.password=******** spring.redis.jedis.pool.max-active=1000 spring.redis.jedis.pool.max-wait=-1ms spring.redis.jedis.pool.max-idle=16 spring.redis.jedis.pool.min-idle=8
1、修改代码
2、部署压力测试(后面都是双机测试)
通过 Redis 缓存介入,吞吐量翻了几倍。
2、Guava 本地缓存
1、这个时候考虑加入本地缓存,在每台服务器上缓存热点数据,像直接使用 HashMap、ConcurrentHashMap格式的类型还不满足自动失效清除功能,我们可以使用 guava 提供的数据类型 Cache<String, Object>;
但是在修改方面,可以使用 MQ 消息广播,通知每台服务器进行操作,代码复杂度提高了。
2、编写代码
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency>
/** * @description * @author: hq * @create: 2022-08-31 16:21 **/ @Service public class CacheServiceImpl implements CacheService { private Cache<String, Object> cache = null; @PostConstruct public void init() { cache = CacheBuilder.newBuilder() // 设置缓存容器初始容量 .initialCapacity(16) // 设置缓存中最大可以存储 100 个 key,超过100个后按照 LRU 策略移除缓存项 .maximumSize(100) // 设置写缓存多少秒过期 .expireAfterWrite(30, TimeUnit.SECONDS) .build(); } @Override public void setCache(String key, Object obj) { cache.put(key, obj); } @Override public Object getCache(String key) { return cache.getIfPresent(key); } }
3、部署服务
4、压力测试
吞吐量已经提升至 650/s。
3、Nginx 缓存(反向代理废弃版)
1、在整个服务调用运行过程中,nginx 是用户第一道关卡。那么可以在用户调用的时候,先查找 nginx 缓存。
2、配置反向代理缓存
#user nobody; worker_processes 4; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; pid /usr/local/nginx/logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; upstream tomcatServerPool{ server 172.29.26.12:9020 weight=10; server 172.18.160.168:9020 weight=10; keepalive 30; } # proxy_cache_path 设置缓存目录 # levels 表示是两级目录,1和2表示用1位和2位16进制来命名目录名称。在此例中,第一级目录用1位16进制命名,如b;第二级目录用2位16进制命名 # keys_zone 设置共享内存以及占用空间大小 # max_size 最大缓存容量 # inactive 缓存最长时间 proxy_cache_path /usr/local/nginx/upstream_cache levels=1:2 keys_zone=mycache:50m max_size=1g inactive=5m; server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { # root html; # index index.html index.htm; proxy_pass http://tomcatServerPool; #用户IP proxy_set_header X-Real-IP $remote_addr; proxy_cache mycache; # 缓存状态为 200 304 的数据 proxy_cache_valid 200 304 5m; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
3、重启 nginx
4、JMeter 压测
性能提升不是很大,原因是因为 nginx 是将缓存存储在磁盘上的,读取没有内存速度快。
但是它介于用户第一层关卡就返回数据,如果没有这缓存,那么也会有网络上的消耗,毕竟是上海区服务器。
4、Nginx Lua
1、lua携程机制
ngxin有对应的携程机制
携程就是线程空间在内的执行单元,叫做携程。
类似于线程一样,有自己独立的一个运行空间,它这个运行是基于用户态模拟出来的独立运行空间。本身依托于线程,并且也要像线程一样被 CPU 执行。
切换开销小
2、nginx lua 插载点
init_by_lua:系统启动时调用
init_worker_by_lua:worker 进程启动时调用
set_by_lua:nginx 变量用复杂 lua return
rewrite_by_lua:重写 url 规则
access_by_lua:权限验证阶段
content_by_lua:内容输出节点
3、安装 OpenResty (nginx 服务器)
下载地址:https://openresty.org/cn/download.html
安装为默认安装路径:https://www.runoob.com/w3cnote/openresty-intro.html
1、进入 openresty 目录
2、编写测试lua脚本(需将之间运行的 nginx 关闭)
1、创建 lua 文件夹,并进入 lua 文件夹
2、vim init.lua
ngx.log(ngx.ERR,"init lua success");
#调用 nginx 日志方法,打印错误日志
3、返回上一级
4、进入nginx目录 修改配置 vim nginx/conf/nginx.conf
5、启动 nginx
打印出了刚才的那句话
6、将刚才配置干掉,只是测试使用
3、编写 Hello World html文本输出案列
1、vim lua/helloworld.lua
ngx.exec("/item/get?id=7");
2、编辑 nginx 配置
3、启动 nginx
4、测试
像一个代理转发,现在通过 nginx 和 lua 完成了指定某一个固定 URL 的方式。
4、Openresty 实践
shared dic:共享内存字典,所有 worker 进程可见,lru 淘汰
1、配置 shared dic
vim nginx/conf/nginx.conf
2、编写 lua 脚本
vim lua/itemshareddict.lua
function get_from_cache(key) -- 声明一个变量:这个变量就是刚才 ngxin.conf 配置的 my_cache local cache_ngx = ngx.shared.my_cache local value = cache_ngx:get(key) return value end -- 参数 key,value exptime(过期时间) function set_to_cache(key,value,exptime) if not exptime then exptime = 0 end local cache_ngx = ngx.shared.my_cache local succ,err,forcible = cache_ngx:set(key,value,exptime) return succ end -- /item/get?id=7 local args = ngx.req.get_uri_args() -- 取到 id 参数 local id = args["id"] -- "item_"..id 两个..类似于 java 中 string 字符串拼接 + local item_model = get_from_cache("item_"..id) -- 如果缓存取不到 if item_model == nil then -- 发送 http 请求拿到 resp local resp = ngx.location.capture("/item/get?id="..id) -- 获取 body 内容 item_model = resp.body -- 将结果添加至缓存中 1分钟 set_to_cache("item_"..id,item_model,1*60) end -- 不等于 null 直接返回 ngx.say(item_model)
3、配置 nginx URL
location /luaitem/get { default_type "application/json"; content_by_lua_file ../lua/itemshareddict.lua; }
4、浏览器调用测试,访问成功
5、测压
5、Openresty Redis 支持
1、安装 openresty 对 redis 的支持
进入 lualib 目录
cd resty
这里面封装了常用的 redis 的操作
2、编写 itemredis.lua
-- 获取请求参数 local args = ngx.req.get_uri_args() local id = args["id"] -- 引入二次封装 redis local redis = require "resty.my_redis" local self = { ip = "172.29.26.13", port = "6379", db_index = 1, pool_max_idle_time = 30000, pool_size = 100 } local cache = redis.new(self) local item_model = cache:get("miaosha:item:"..id) if item_model == ngx.null or item_model == nil then local resp = ngx.location.capture("/item/get?id="..id) -- 正常操作是获取到数据后,是要存入 redis 中的,但是我们在程序中已经完成了这步操作了 -- 这里就不对 redis 操作了 item_model = resp.body end ngx.say(item_model)
在这里,我找了一个第三方封装好的组件
local redis_c = require "resty.redis" local ok, new_tab = pcall(require, "table.new") if not ok or type(new_tab) ~= "function" then new_tab = function (narr, nrec) return {} end end local _M = new_tab(0, 155) _M._VERSION = '0.01' local commands = { "append", "auth", "bgrewriteaof", "bgsave", "bitcount", "bitop", "blpop", "brpop", "brpoplpush", "client", "config", "dbsize", "debug", "decr", "decrby", "del", "discard", "dump", "echo", "eval", "exec", "exists", "expire", "expireat", "flushall", "flushdb", "get", "getbit", "getrange", "getset", "hdel", "hexists", "hget", "hgetall", "hincrby", "hincrbyfloat", "hkeys", "hlen", "hmget", "hmset", "hscan", "hset", "hsetnx", "hvals", "incr", "incrby", "incrbyfloat", "info", "keys", "lastsave", "lindex", "linsert", "llen", "lpop", "lpush", "lpushx", "lrange", "lrem", "lset", "ltrim", "mget", "migrate", "monitor", "move", "mset", "msetnx", "multi", "object", "persist", "pexpire", "pexpireat", "ping", "psetex", "psubscribe", "pttl", "publish", --[[ "punsubscribe", ]] "pubsub", "quit", "randomkey", "rename", "renamenx", "restore", "rpop", "rpoplpush", "rpush", "rpushx", "sadd", "save", "scan", "scard", "script", "sdiff", "sdiffstore", "select", "set", "setbit", "setex", "setnx", "setrange", "shutdown", "sinter", "sinterstore", "sismember", "slaveof", "slowlog", "smembers", "smove", "sort", "spop", "srandmember", "srem", "sscan", "strlen", --[[ "subscribe", ]] "sunion", "sunionstore", "sync", "time", "ttl", "type", --[[ "unsubscribe", ]] "unwatch", "watch", "zadd", "zcard", "zcount", "zincrby", "zinterstore", "zrange", "zrangebyscore", "zrank", "zrem", "zremrangebyrank", "zremrangebyscore", "zrevrange", "zrevrangebyscore", "zrevrank", "zscan", "zscore", "zunionstore", "evalsha" } local mt = { __index = _M } local function is_redis_null( res ) if type(res) == "table" then for k,v in pairs(res) do if v ~= ngx.null then return false end end return true elseif res == ngx.null then return true elseif res == nil then return true end return false end function _M.close_redis(self, redis) if not redis then return end --释放连接(连接池实现) local pool_max_idle_time = self.pool_max_idle_time --最大空闲时间 毫秒 local pool_size = self.pool_size --连接池大小 local ok, err = redis:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.say("set keepalive error : ", err) end end -- change connect address as you need function _M.connect_mod( self, redis ) redis:set_timeout(self.timeout) local ok, err = redis:connect(self.ip, self.port) if not ok then ngx.say("connect to redis error : ", err) return self:close_redis(redis) end if self.password then ----密码认证 local count, err = redis:get_reused_times() if 0 == count then ----新建连接,需要认证密码 ok, err = redis:auth(self.password) if not ok then ngx.say("failed to auth: ", err) return end elseif err then ----从连接池中获取连接,无需再次认证密码 ngx.say("failed to get reused times: ", err) return end end return ok,err; end function _M.init_pipeline( self ) self._reqs = {} end function _M.commit_pipeline( self ) local reqs = self._reqs if nil == reqs or 0 == #reqs then return {}, "no pipeline" else self._reqs = nil end local redis, err = redis_c:new() if not redis then return nil, err end local ok, err = self:connect_mod(redis) if not ok then return {}, err end redis:init_pipeline() for _, vals in ipairs(reqs) do local fun = redis[vals[1]] table.remove(vals , 1) fun(redis, unpack(vals)) end local results, err = redis:commit_pipeline() if not results or err then return {}, err end if is_redis_null(results) then results = {} ngx.log(ngx.WARN, "is null") end -- table.remove (results , 1) --self.set_keepalive_mod(redis) self:close_redis(redis) for i,value in ipairs(results) do if is_redis_null(value) then results[i] = nil end end return results, err end local function do_command(self, cmd, ... ) if self._reqs then table.insert(self._reqs, {cmd, ...}) return end local redis, err = redis_c:new() if not redis then return nil, err end local ok, err = self:connect_mod(redis) if not ok or err then return nil, err end redis:select(self.db_index) local fun = redis[cmd] local result, err = fun(redis, ...) if not result or err then -- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err) return nil, err end if is_redis_null(result) then result = nil end --self.set_keepalive_mod(redis) self:close_redis(redis) return result, err end for i = 1, #commands do local cmd = commands[i] _M[cmd] = function (self, ...) return do_command(self, cmd, ...) end end function _M.new(self, opts) opts = opts or {} local timeout = (opts.timeout and opts.timeout * 1000) or 1000 local db_index= opts.db_index or 0 local ip = opts.ip or '127.0.0.1' local port = opts.port or 6379 local password = opts.password local pool_max_idle_time = opts.pool_max_idle_time or 60000 local pool_size = opts.pool_size or 100 return setmetatable({ timeout = timeout, db_index = db_index, ip = ip, port = port, password = password, pool_max_idle_time = pool_max_idle_time, pool_size = pool_size, _reqs = nil }, mt) end return _M
3、修改 nginx 配置
4、压力测试
这就很给力了,峰值吞吐量 1000/s 。(从单体的 70/s 到现在的 1000/s。)
5、最后整体架构图
openresty 使用 redis 从节点作为缓存,减少主节点加压力,也可使用 多主多从模式。可扩展。