Redis與服務接口相結合的高並發
Xadserver接口與redis結合實現高並發需要滿足以下三個條件:
- Gevent模擬高並發請求時,服務接口滿足模擬請求的QPS
- Redis可支持的最大連接數滿足高並發的數量
- 本次測試使用連接池,降低客戶端連接redis以及銷毀連接的系統開銷(連接池數量需大於等於高並發的數量,原因下邊會說明)
事前需要關注的三個概念:xadserver並發數,redis支持的最大連接數,連接池的最大連接數(之后如果需要動態設置redis的最大連接數而又不想重啟redis影響線上的服務,可以通過config set maxclients 65535 命令實時設置):
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "2000"
先來查詢一下,我們當前redis支持的最大連接數:
實驗1
為了說明連接池最大連接數與並發數的關系,我們 先做以下這個試驗;
# coding=utf-8
from gevent import monkey
import requests
monkey.patch_all()
import gevent
import redis
import time
import redis
Pool = redis.ConnectionPool(host='127.0.0.1', port=6379, max_connections=10, db=2)
pr = redis.Redis(connection_pool=Pool, decode_responses=True)
# print pr.get('__h5_campaign_info__122671')
import sys
def getFunc(key):
"""取key"""
v = pr.get('__h5_campaign_info__122671')
print v
def call_gevent(count):
"""調用gevent 模擬高並發"""
begin_time = time.time()
run_gevent_list = []
num = 1
for i in range(count):
print('--------------%d--Test-------------' % i)
mykey = 'test' + str(num)
run_gevent_list.append(gevent.spawn(getFunc, mykey))
num = num + 1
gevent.joinall(run_gevent_list)
end = time.time()
print('測試並發量' + str(count))
print('單次測試時間(平均)s:', (end - begin_time) / count)
print('累計測試時間 s:', end - begin_time)
if __name__ == '__main__':
# 並發請求數量
test_count = 20 # 改變並發量查看測試效果。。我這里取7000,10000,20000進行測試。記得將rdis的最大連接數改為30000並重啟redis。
while 1:
call_gevent(count=test_count)
如代碼所示,我們設置的連接池最大連接數是10,而並發數我們設置為20,執行該並發條件下的返回結果,發現出現如下異常;
Traceback (most recent call last):
File "src/gevent/greenlet.py", line 716, in gevent._greenlet.Greenlet.run
File "/Users/liquid/PycharmProjects/aliyun_sls/redis高並發.py", line 19, in getFunc
v = pr.get('__h5_campaign_info__122671')
File "/Users/liquid/PycharmProjects/aliyun_sls/venv/lib/python2.7/site-packages/redis/client.py", line 1207, in get
return self.execute_command('GET', name)
File "/Users/liquid/PycharmProjects/aliyun_sls/venv/lib/python2.7/site-packages/redis/client.py", line 752, in execute_command
connection = pool.get_connection(command_name, **options)
File "/Users/liquid/PycharmProjects/aliyun_sls/venv/lib/python2.7/site-packages/redis/connection.py", line 970, in get_connection
connection = self.make_connection()
File "/Users/liquid/PycharmProjects/aliyun_sls/venv/lib/python2.7/site-packages/redis/connection.py", line 986, in make_connection
raise ConnectionError("Too many connections")
ConnectionError: Too many connections
2019-04-16T06:55:17Z <Greenlet "Greenlet-2" at 0x102051050: getFunc('test13')> failed with ConnectionError
查看redis的當前連接數為11(去除本就存在的1個連接,可知redis當前連接數為10,而更多的並發數並沒有分配到資源)
127.0.0.1:6379> info clients
# Clients
connected_clients:11
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
實驗2
之后我們設置redis連接池的最大連接數為20,再試一次;
測試並發量20
('\xe5\x8d\x95\xe6\xac\xa1\xe6\xb5\x8b\xe8\xaf\x95\xe6\x97\xb6\xe9\x97\xb4\xef\xbc\x88\xe5\xb9\xb3\xe5\x9d\x87\xef\xbc\x89s:', 7.859468460083007e-05)
('\xe7\xb4\xaf\xe8\xae\xa1\xe6\xb5\x8b\xe8\xaf\x95\xe6\x97\xb6\xe9\x97\xb4 s:', 0.0015718936920166016)
可以正常處理並發的請求,並不會報錯
之后再去查看redis的當前連接數為21(去除本就存在的1個連接,可知redis當前連接數為20每一個並發的請求都分配到了的redis連接池中的資源),
127.0.0.1:6379> info clients
# Clients
connected_clients:21
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
實驗3
此時我們修改我們連接池的最大連接數為30,再次執行代碼,代碼依然沒有報錯
測試並發量20
('\xe5\x8d\x95\xe6\xac\xa1\xe6\xb5\x8b\xe8\xaf\x95\xe6\x97\xb6\xe9\x97\xb4\xef\xbc\x88\xe5\xb9\xb3\xe5\x9d\x87\xef\xbc\x89s:', 7.630586624145508e-05)
('\xe7\xb4\xaf\xe8\xae\xa1\xe6\xb5\x8b\xe8\xaf\x95\xe6\x97\xb6\xe9\x97\xb4 s:', 0.0015261173248291016)
結論:
在使用redis的連接池訪問redis里的資源時,連接池數必須大於等於並發數(二者同時小於redis可支持的最大連接數),否則多出來的並發數將會因為分配不到redis的資源而收到報錯信息(參考地址:https://redis.io/topics/clients)
上述三個實驗並沒有與我們的接口服務結合在一起,下面將結合接口服務再次實驗
實驗4:
# coding=utf-8
from gevent import monkey
import requests
monkey.patch_all()
import gevent
import redis
import time
def getFunc(key):
"""取key"""
v = requests.get('http://127.0.0.1:91/sss')
print v
def call_gevent(count):
"""調用gevent 模擬高並發"""
begin_time = time.time()
run_gevent_list = []
num = 1
for i in range(count):
print('--------------%d--Test-------------' % i)
mykey = 'test' + str(num)
run_gevent_list.append(gevent.spawn(getFunc, mykey))
num = num + 1
gevent.joinall(run_gevent_list)
end = time.time()
print('測試並發量' + str(count))
print('單次測試時間(平均)s:', (end - begin_time) / count)
print('累計測試時間 s:', end - begin_time)
if __name__ == '__main__':
# 並發請求數量
test_count = 100 # 改變並發量查看測試效果。。我這里取7000,10000,20000進行測試。記得將rdis的最大連接數改為30000並重啟redis。
while 1:
call_gevent(count=test_count)
接口代碼如下:
Pool = redis.ConnectionPool(host='127.0.0.1', port=6379, max_connections=50, db=2)
# 從池子中拿一個鏈接
pr = redis.Redis(connection_pool=Pool, decode_responses=True)
@app.route("/sss", methods=["GET", "POST"])
def test_concurrent():
try:
pr.get('__h5_campaign_info__111127')
return json.dumps({'code':1})
except:
traceback.print_exc()
return json.dumps({'code':0})
本地機器上,該接口最多支持2600的並發(uwsgi啟動兩個進程處理請求),所以等下模擬請求時,數量並不會太多(100的並發量做測試)
此時並發量為100,而我們設置的redis連接池為50,按照預期,應該是100個請求分攤到連接池的50個資源上,多余的請求資源等待前50個資源的釋放(事實上連接池會在初始化時申請一部分資源,使用完后歸還連接池,從而達到減少連接redis與注銷連接開銷的目的),接下來看接口的響應如何
有一半的請求響應如下(預期中的):
127.0.0.1 - - [16/Apr/2019 15:20:07] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:20:07] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:20:07] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:20:07] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:20:07] "GET /sss HTTP/1.1" 200 -
另一半的請求響應(預期外的):
Traceback (most recent call last):
File "run.py", line 533, in test_concurrent
pr.get('__h5_campaign_info__111127')
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/client.py", line 880, in get
return self.execute_command('GET', name)
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/client.py", line 570, in execute_command
connection = pool.get_connection(command_name, **options)
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 897, in get_connection
connection = self.make_connection()
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 904, in make_connection
raise ConnectionError("Too many connections")
ConnectionError: Too many connections
此時查詢redis中的客戶端連接數為51(去除本就存在的一個連接),數量和連接池申請的資源相匹配
127.0.0.1:6379> info clients
# Clients
connected_clients:51
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
實驗5:
此時修改連接池的最大連接數為100,並發量依然控制在100
全部請求的響應為:
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:30:58] "GET /sss HTTP/1.1" 200 -
此時查詢redis的客戶端連接數(去除本就存在的一個連接),數量和連接池申請的資源完全匹配
127.0.0.1:6379> info clients
# Clients
connected_clients:101
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
實驗6
連接池的最大連接數設置為200,並發量控制在100
全部請求的響應為:
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:35:49] "GET /sss HTTP/1.1" 200 –
此時查詢redis的客戶端連接數(去除本就存在的一個連接),數量和並發數完全匹配
127.0.0.1:6379> info clients
# Clients
connected_clients:101
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
這6個實驗全部都是基於redis可支持的最大連接數大於連接池的最大連接數以及並發數
實驗7:
此時我們修改redis可支持的最大連接數為20,再次實驗(修改命令如下):
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "2000"
127.0.0.1:6379> config set maxclients 20
OK
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "20"
此時修改連接池的數量為30,而我們的並發也控制在30,再次實驗:
返回結果有20個如下:
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 -
127.0.0.1 - - [16/Apr/2019 15:42:32] "GET /sss HTTP/1.1" 200 –
剩余10個請求的返回結果如下:
Traceback (most recent call last):
File "run.py", line 533, in test_concurrent
pr.get('__h5_campaign_info__111127')
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/client.py", line 880, in get
return self.execute_command('GET', name)
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/client.py", line 578, in execute_command
connection.send_command(*args)
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 563, in send_command
self.send_packed_command(self.pack_command(*args))
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 538, in send_packed_command
self.connect()
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 446, in connect
self.on_connect()
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 520, in on_connect
if nativestr(self.read_response()) != 'OK':
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 577, in read_response
response = self._parser.read_response()
File "/Users/liquid/PycharmProjects/Env/lib/python2.7/site-packages/redis/connection.py", line 255, in read_response
raise error
ConnectionError: max number of clients reached
此時可分配資源內的20個請求可以正常返回,而資源外的10個請求則按照redis拒絕請求的信息返回
綜合以上7個實驗,我們得出以下結論:
1, redis可支持的最大連接數必須大於等於連接池設置的最大連接數或者並發數的任意一個數值
2, redis連接池申請資源的數量必須大於等於並發數,否則多余的並發請求將會因為分配不到資源而出現異常
3, 考慮到redis不停創建連接和銷毀連接的系統開銷會影響我們的接口質量,所以我們在xadserver項目中使用redis連接池申請到足夠的資源供並發請求分配調用
仍需確認的點:
1,修改機器的文件描述符以及redis配置的最大連接數超過我們的並發數,redis是否可以按照預期接受並處理請求
2,接口內調用redis實時獲取數據,對接口的響應速度影響有多大(需要完全模擬線上環境測試)