BI框架

BI(Business Intelligence)框架是一种软件工具集合,用于帮助企业从大量的数据中提取、分析和可视化有关业务运营的洞察。它提供了一套功能强大的工具和技术,用于数据仓库、数据处理、数据可视化、报表和仪表盘等方面的业务分析。

Quant

统计学

梯度(斜率)

import numpy as np

np_test1 = np.array([0,0,0,0,0,0])
print("梯度1",np_test1,np.gradient(np_test1))
np_test2 = np.array([0,1,2,3,4,5])
print("梯度2",np_test2,np.gradient(np_test2))
np_test3 = np.array([0,1,2,1,0,-1])
print("梯度3",np_test3,np.gradient(np_test3))
np_test4 = np.array([-1,-1,-1,-1,-1,-1])
print("梯度4",np_test4,np.gradient(np_test4))
np_test5 = np.array([0,-1,-3,-6,-10,-15,-18,-20,-21,-21,-20,-18])
print("梯度5",np_test5,np.gradient(np_test5))
np_test6 = np.array([0,-1,-3,-6,-10,-15,-18,-20,-21,-21,-20,-18,-20])
print("梯度6",np_test6,np.gradient(np_test6))
np_test7 = np.array([0,-1,-3,-6,-10,-15,-18,-20,-21,-21,-20,-18,-18])
print("梯度7",np_test7,np.gradient(np_test7))
np_test8 = np.array([0,-1,-3,-6,-10,-15,-18,-20,-21,-21,-20,-18,-16])
print("梯度8",np_test8,np.gradient(np_test8))

OUT:

梯度1 [0 0 0 0 0 0] [0. 0. 0. 0. 0. 0.]
梯度2 [0 1 2 3 4 5] [1. 1. 1. 1. 1. 1.]
梯度3 [ 0  1  2  1  0 -1] [ 1.  1.  0. -1. -1. -1.]
梯度4 [-1 -1 -1 -1 -1 -1] [0. 0. 0. 0. 0. 0.]
梯度5 [  0  -1  -3  -6 -10 -15 -18 -20 -21 -21 -20 -18] [-1.  -1.5 -2.5 -3.5 -4.5 -4.  -2.5 -1.5 -0.5  0.5  1.5  2. ]
梯度6 [  0  -1  -3  -6 -10 -15 -18 -20 -21 -21 -20 -18 -20] [-1.  -1.5 -2.5 -3.5 -4.5 -4.  -2.5 -1.5 -0.5  0.5  1.5  0.  -2. ]
梯度7 [  0  -1  -3  -6 -10 -15 -18 -20 -21 -21 -20 -18 -18] [-1.  -1.5 -2.5 -3.5 -4.5 -4.  -2.5 -1.5 -0.5  0.5  1.5  1.   0. ]
梯度8 [  0  -1  -3  -6 -10 -15 -18 -20 -21 -21 -20 -18 -16] [-1.  -1.5 -2.5 -3.5 -4.5 -4.  -2.5 -1.5 -0.5  0.5  1.5  2.   2. ]

标准差

import numpy as np

np_test1 = np.array([0,0,0,0,0])
print("标准差1",np_test1,np_test1.std())
np_test2 = np.array([0,1,0,0,0])
print("标准差2",np_test2,np_test2.std())
np_test3 = np.array([0,0,0,1,0])
print("标准差3",np_test3,np_test3.std())
np_test4 = np.array([0,0,1,0,0])
print("标准差4",np_test4,np_test4.std())
np_test5 = np.array([1,1,1,1,1])
print("标准差5",np_test5,np_test5.std())
np_test6 = np.array([1,1,0,0,0])
print("标准差6",np_test6,np_test6.std())
np_test7 = np.array([1,1,0,0,0,0])
print("标准差7",np_test7,np_test7.std())
np_test8 = np.array([1,1,1,0,0,0])
print("标准差8",np_test8,np_test8.std())
np_test9 = np.array([0.5,0.5,0.5,0,0,0])
print("标准差9",np_test9,np_test9.std())
np_test10 = np.array([0.25,0.25,0.25,0,0,0])
print("标准差10",np_test10,np_test10.std())
np_test11 = np.array([0.75,0.75,0.75,0,0,0])
print("标准差11",np_test11,np_test11.std())
np_test12 = np.array([0.75,0.75,0.75,1,1,1])
print("标准差12",np_test12,np_test12.std())

OUT:

标准差1 [0 0 0 0 0] 0.0
标准差2 [0 1 0 0 0] 0.4000000000000001
标准差3 [0 0 0 1 0] 0.4
标准差4 [0 0 1 0 0] 0.4000000000000001
标准差5 [1 1 1 1 1] 0.0
标准差6 [1 1 0 0 0] 0.48989794855663565
标准差7 [1 1 0 0 0 0] 0.47140452079103173
标准差8 [1 1 1 0 0 0] 0.5
标准差9 [0.5 0.5 0.5 0.  0.  0. ] 0.25
标准差10 [0.25 0.25 0.25 0.   0.   0.  ] 0.125
标准差11 [0.75 0.75 0.75 0.   0.   0.  ] 0.375
标准差12 [0.75 0.75 0.75 1.   1.   1.  ] 0.125

机器学习框架环境

BTGYM

https://github.com/Kismuz/btgym

tensortrade

https://github.com/tensortrade-org/tensortrade

Baseline backtest performance

test

Environment configuration [M4]

model name : AMD Ryzen 7 6800H with Radeon Graphics  [16 core]
MemTotal:       28610872 kB
MemFree:        26584252 kB
SwapTotal:       7340032 kB
SwapFree:        7340032 kB
system: Ubuntu 20.04.5 LTS
kernel: Linux 5.15.79.1-microsoft-standard-WSL2 x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.6.47
pandas                 1.5.2
pandas-ta              0.3.14b0
pymongo                4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] 抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    1m20.179s
user    1m18.912s
sys     0m1.421s

Environment configuration [M4]

model name : AMD Ryzen 7 6800H with Radeon Graphics  [16 core]
MemTotal:       58455856 kB
MemFree:        50016704 kB
SwapTotal:       8388608 kB
SwapFree:        8388608 kB
system: NT
kernel: MINGW64_NT-10.0-22621 3.3.6-341.x86_64 unknown
Python 3.9.13
backtrader                    1.9.76.123
ccxt                          2.6.39
pandas                        1.4.4
pandas-ta                     0.3.14b0
pymongo                       4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] 抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    1m43.855s
user    0m0.015s
sys     0m0.000s

Environment configuration [m3]

model name : Intel(R) Xeon(R) CPU E5-2660 v2 @ 2.20GHz  [40 core]
MemTotal:       131970928 kB
MemFree:        124979156 kB
SwapTotal:       2097148 kB
SwapFree:        2097148 kB
system: Ubuntu 20.04.3 LTS
kernel: Linux 5.15.0-46-generic x86_64
Python 3.8.10
backtrader              1.9.76.123
ccxt                    2.5.46
pandas                  1.4.1
pandas-ta               0.3.14b0
pymongo                 4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] [主数据库:mongodb://192.168.1.20]
抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    5m10.304s
user    5m1.097s
sys     0m5.967s

Environment configuration [E14]

model name : AMD Ryzen 5 4500U with Radeon Graphics  [6 core]
MemTotal:       12451164 kB
MemFree:        11482124 kB
SwapTotal:       4194304 kB
SwapFree:        4194304 kB
system: Ubuntu 20.04.1 LTS
kernel: Linux 5.4.72-microsoft-standard-WSL2 x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.4.27
pandas                 1.5.2
pandas-ta              0.3.14b0
pymongo                4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] 抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    1m59.407s
user    1m57.261s
sys     0m1.480s

Environment configuration [M1]

model name : Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz  [4 core]
MemTotal:       33452176 kB
MemFree:        18736776 kB
SwapTotal:      12582912 kB
SwapFree:       12463216 kB
system: NT
kernel: MINGW64_NT-10.0-19044 3.1.4-340.x86_64 unknown
Python 3.7.6
backtrader                         1.9.76.123
ccxt                               2.2.17
pandas                             1.3.5
pandas-ta                          0.3.14b0
pymongo                            4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] 抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    4m22.050s
user    0m0.015s
sys     0m0.016s

Environment configuration [M1]

model name : Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz  [4 core]
MemTotal:       26198024 kB
MemFree:        23249568 kB
SwapTotal:       7340032 kB
SwapFree:        7340032 kB
system: Ubuntu 20.04.1 LTS
kernel: Linux 4.19.128-microsoft-standard x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.5.30
pandas                 1.4.4
pandas-ta              0.3.14b0
pymongo                4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] 抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    2m27.018s
user    2m23.037s
sys     0m1.120s

Environment configuration [BRIC]

model name : Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz  [4 core]
MemTotal:       13008184 kB
MemFree:        12005880 kB
SwapTotal:       4194304 kB
SwapFree:        4194304 kB
system: Ubuntu 20.04.4 LTS
kernel: Linux 5.10.16.3-microsoft-standard-WSL2 x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.5.30
pandas                 1.4.4
pandas-ta              0.3.14b0
pymongo                4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] [主数据库:mongodb://192.168.3.32]
抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00
胜率结果:  {'all_num': 0, 'win_num': 0, 'los_num': 0, 'win_p': 0, 'los_p': 0}

real    3m40.160s
user    3m37.635s
sys     0m3.841s

Environment configuration [M2]

model name : Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz  [16 core]
MemTotal:       13022068 kB
MemFree:        10488912 kB
SwapTotal:       4194304 kB
SwapFree:        4194304 kB
system: Ubuntu 20.04.3 LTS
kernel: Linux 5.10.16.3-microsoft-standard-WSL2 x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.6.47
pandas                 1.5.3
pandas-ta              0.3.14b0
pymongo                4.3.3
[Init] 策略: 测试信号_test 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] [主数据库:mongodb://192.168.1.20]
抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 14_200 [测试信号_test] 最终 1000000.00

real    4m17.342s
user    4m9.496s
sys     0m2.987s

m16_1

Environment configuration [M1]

model name : Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz  [4 core]
MemTotal:       26198024 kB
MemFree:        21258576 kB
SwapTotal:       7340032 kB
SwapFree:        7340032 kB
kernel: Linux 4.19.128-microsoft-standard x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   1.93.108
pandas                 1.4.4
pandas-ta              0.3.14b0
pymongo                4.2.0
[Init] 策略: 微震荡_m16_1 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] 抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 10_600_9_65_9_29_5_3_66_6_20_25_16_4_30_1_1_27_27 [微震荡_m16_1] 最终 983616.87
胜率结果:  {'all_num': 1338, 'win_num': 677, 'los_num': 661, 'win_p': 0.51, 'los_p': 0.49}

real    123m18.071s
user    122m40.520s
sys     0m39.400s

Environment configuration [BRIC]

model name : Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz  [4 core]
MemTotal:       13008184 kB
MemFree:        12119408 kB
SwapTotal:       4194304 kB
SwapFree:        4194304 kB
kernel: Linux 5.10.16.3-microsoft-standard-WSL2 x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.2.17
pandas                 1.4.4
pandas-ta              0.3.14b0
pymongo                4.2.0
[Init] 策略: 微震荡_m16_1 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
[Init] [主数据库:mongodb://192.168.3.32]
抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 10_600_9_65_9_29_5_3_66_6_20_25_16_4_30_1_1_27_27 [微震荡_m16_1] 最终 983616.87
胜率结果:  {'all_num': 1338, 'win_num': 677, 'los_num': 661, 'win_p': 0.51, 'los_p': 0.49}

real    109m52.703s
user    109m23.917s
sys     0m28.641s

Environment configuration [M2]

model name : Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHz  [16 core]
MemTotal:       13022068 kB
MemFree:        12341284 kB
SwapTotal:       4194304 kB
SwapFree:        4194304 kB
kernel: Linux 5.10.16.3-microsoft-standard-WSL2 x86_64
Python 3.8.10
backtrader             1.9.76.123
ccxt                   2.2.5
pandas                 1.4.2
pandas-ta              0.3.14b0
pymongo                4.3.3
[Init] 策略: 微震荡_m16_1 币种: BTC/USDT:USDT
[Init] 初始投资: --money 1000000.00
[Init] 自动切换到1%下注模式!
[Init] 下注方式: --sizer=1 --maxsizer=1 %
[Init] 设置佣金: --commission 0.001
抓取范围: --fromdate --todate
2020-01-01 00:00 open:7199.9    high:7199.9     low:7189.4      close:7198.2    volume:48238.0
2023-01-01 00:00 open:16591.6   high:16593.8    low:16591.6     close:16593.8   volume:2349.0
总数据量: 1578241 时间跨度: 1096.00天
-pd 10_600_9_65_9_29_5_3_66_6_20_25_16_4_30_1_1_27_27 [微震荡_m16_1] 最终 983616.87
胜率结果:  {'all_num': 1338, 'win_num': 677, 'los_num': 661, 'win_p': 0.51, 'los_p': 0.49}

real    233m6.286s
user    224m16.812s
sys     0m27.302s

时序数据库

InfluxDB

InfluxDB是一个开源分布式时序、时间和指标数据库,使用 Go 语言编写,无需外部依赖。

文档:https://docs.influxdata.com/influxdb/v2.0/

Docker下安装InfluxDB

docker run -p 8086:8086 -v /app/data/influxdb:/var/lib/influxdb2 influxdb

PHP使用InfluxDB

composer require influxdata/influxdb-client-php

项目地址:https://github.com/influxdata/influxdb-client-php

TimescaleDB

TimescaleDB是一个基于PostgreSQL分布式时间序列数据库,可扩展到每秒超过1000万个指标,支持本机压缩,处理高基数,并提供本机时间序列功能,例如数据保留策略,连续聚合视图,下采样,数据填充和内插。

TimescaleDB还支持完整的SQL,各种数据类型(数字,文本,数组,JSON,布尔值)和ACID语义。操作上成熟的功能包括高可用性,流式备份,随时间推移升级,角色和权限以及安全性。

文档:https://docs.timescale.com/latest/main

Docker下安装TimescaleDB

docker run -d --name some-timescaledb -p 5432:5432 timescale/timescaledb

项目地址:https://github.com/timescale/timescaledb-docker

亿级数据处理

MongoDB中存在一张数据量过亿的表

目前文档数量 232,656,863

文档容量:46.35Gb

运行环境:

操作系统:Ubuntu 20.04.1 LTS
CPU: i5-4590 @ 3.30 Ghz
内存: 32G
硬盘:M.2 SSD

已在2个字段上创建索引,命中索引的话响应3ms,没命中索引80s以上。

批量导入优化

有百万条数据需导入,为加快导入速度,我们执行批量导入,一次1万条试试:

导入: 610000 [run 5309.4ms]
导入: 620000 [run 5290.9ms]
导入: 630000 [run 4837.2ms]
导入: 640000 [run 4927.6ms]
导入: 650000 [run 5749.3ms]
导入: 660000 [run 4754.5ms]
导入: 670000 [run 5378.4ms]
导入: 680000 [run 4548.4ms]
导入: 690000 [run 4558.4ms]

可以看到1万条一次需要大概5秒。

如果把索引去掉:

导入: 1200000 [run 52.5ms]
导入: 1210000 [run 53.6ms]
导入: 1220000 [run 52.5ms]
导入: 1230000 [run 53.7ms]
导入: 1240000 [run 53ms]
导入: 1250000 [run 52.8ms]
导入: 1260000 [run 60.8ms]
导入: 1270000 [run 52.7ms]
导入: 1280000 [run 55.3ms]
导入: 1290000 [run 55.3ms]

快了近100倍。

像一些冷数据,导入量比较大时,把索引去除加快导入速度。等导入完,重建索引,效率反而提升。

大量数据查询优化

当数据量返回过大时,除了必要的索引优化外,对数据集返回数的限制尤为必要。

请使用skip和limit做数据限制。

MongoDB性能分析和优化

explain

在查询语句后面跟上explain()能获取查询相关诊断信息

比如:

> db.getCollection("m_pass_base").find({_id:{$regex:/^malu/}}).explain(true)

返回:

{
    "queryPlanner": {
        "plannerVersion": NumberInt("1"),
        "namespace": "d1.m_pass_base",      // 查询的集合
        "indexFilterSet": false,            // 索引过滤
        "parsedQuery": {                    // 查询条件
            "_id": {
                "$regex": "^malu"
            }
        },
        "winningPlan": {                    // 最佳执行计划
            "stage": "FETCH",
            "inputStage": {
                "stage": "IXSCAN",
                "keyPattern": {
                    "_id": NumberInt("1")
                },
                "indexName": "_id_",
                "isMultiKey": false,
                "multiKeyPaths": {
                    "_id": [ ]
                },
                "isUnique": true,
                "isSparse": false,
                "isPartial": false,
                "indexVersion": NumberInt("2"),
                "direction": "forward",
                "indexBounds": {           // 当前查询具体使用的索引
                    "_id": [
                        "[\"malu\", \"malv\")",
                        "[/^malu/, /^malu/]"
                    ]
                }
            }
        },
        "rejectedPlans": [ ]               // 拒绝执行计划
    },
    "executionStats": {                    // executionStats会返回最佳执行计划的一些统计信息
        "executionSuccess": true,          // 是否执行成功
        "nReturned": NumberInt("466"),     // 返回结果数
        "executionTimeMillis": NumberInt("0"),
        "totalKeysExamined": NumberInt("467"),   // 索引扫描数
        "totalDocsExamined": NumberInt("466"),   // 文档扫描数
        "executionStages": {
            "stage": "FETCH",                    // 扫描方式
            "nReturned": NumberInt("466"),
            "executionTimeMillisEstimate": NumberInt("0"),
            "works": NumberInt("468"),
            "advanced": NumberInt("466"),
            "needTime": NumberInt("1"),
            "needYield": NumberInt("0"),
            "saveState": NumberInt("3"),
            "restoreState": NumberInt("3"),
            "isEOF": NumberInt("1"),
            "invalidates": NumberInt("0"),
            "docsExamined": NumberInt("466"),
            "alreadyHasObj": NumberInt("0"),
            "inputStage": {
                "stage": "IXSCAN",
                "nReturned": NumberInt("466"),
                "executionTimeMillisEstimate": NumberInt("0"),
                "works": NumberInt("468"),
                "advanced": NumberInt("466"),
                "needTime": NumberInt("1"),
                "needYield": NumberInt("0"),
                "saveState": NumberInt("3"),
                "restoreState": NumberInt("3"),
                "isEOF": NumberInt("1"),
                "invalidates": NumberInt("0"),
                "keyPattern": {
                    "_id": NumberInt("1")
                },
                "indexName": "_id_",
                "isMultiKey": false,
                "multiKeyPaths": {
                    "_id": [ ]
                },
                "isUnique": true,
                "isSparse": false,
                "isPartial": false,
                "indexVersion": NumberInt("2"),
                "direction": "forward",
                "indexBounds": {           // 当前查询具体使用的索引
                    "_id": [
                        "[\"malu\", \"malv\")",
                        "[/^malu/, /^malu/]"
                    ]
                },
                "keysExamined": NumberInt("467"),
                "seeks": NumberInt("2"),
                "dupsTested": NumberInt("0"),
                "dupsDropped": NumberInt("0"),
                "seenInvalidated": NumberInt("0")
            }
        },
        "allPlansExecution": [ ]     // 所有执行计划
    },
    "serverInfo": {
        "host": "M1",
        "port": NumberInt("27017"),
        "version": "3.6.8",
        "gitVersion": "8e540c0b6db93ce994cc548f000900bdc740f80a"
    },
    "ok": 1
}

扫描方式stage有如下几种:

COLLSCAN:全表扫描
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_FILTER:通过mongos对分片数据进行查询
COUNT:利用db.coll.explain().count()之类进行count运算
COUNTSCAN:count不使用Index进行count时的stage返回
COUNT_SCAN:count使用了Index进行count时的stage返回
SUBPLA:未使用到索引的$or查询的stage返回
TEXT:使用全文索引进行查询时候的stage返回
PROJECTION:限定返回字段时候stage的返回

所以对于查询优化,我们希望看到stage的组合是(查询的时候尽可能用上索引):

Fetch+IDHACK
Fetch+ixscan
Limit+(Fetch+ixscan)
PROJECTION+ixscan
SHARDING_FITER+ixscan
COUNT_SCAN

而不希望看到包含如下的stage:

COLLSCAN(全表扫描)
SORT(使用sort但是无index)
不合理的SKIP
SUBPLA(未用到index的$or)
COUNTSCAN(不使用index进行count)

hint

hint 可以强制 MongoDB 使用一个指定的索引,一般我们在联合索引上做优化。

hint({“$natural”:true}) 可以强制查询走全表扫描,这种情况适合在返回数据集很大的时候,不走索引反而效率更高。

MongoDB慢查询

官方文档:https://docs.mongodb.com/manual/reference/database-profiler/

开启慢查询Profiling

Profiling级别说明

0:关闭,不收集任何数据。
1:收集慢查询数据,默认是100毫秒。
2:收集所有数据

方式一:配置文件开启Profiling

修改启动mongo.conf,插入以下代码

#开启慢查询,200毫秒的记录
profile = 1
slowms = 200

方式二:通过命令开启

注意该方式只保留在内存中,重启mongo将失效

#查看状态:级别和时间
drug:PRIMARY> db.getProfilingStatus()   
{ "was" : 1, "slowms" : 100 }

#查看级别
drug:PRIMARY> db.getProfilingLevel()    
1

#设置级别
drug:PRIMARY> db.setProfilingLevel(2)
{ "was" : 1, "slowms" : 100, "ok" : 1 }

#设置级别和时间
drug:PRIMARY> db.setProfilingLevel(1,200)
{ "was" : 2, "slowms" : 100, "ok" : 1 }

修改“慢查询日志”的大小

#关闭Profiling
drug:PRIMARY> db.setProfilingLevel(0)
{ "was" : 0, "slowms" : 200, "ok" : 1 }

#删除system.profile集合
drug:PRIMARY> db.system.profile.drop()
true

#创建一个新的system.profile集合
drug:PRIMARY> db.createCollection( "system.profile", { capped: true, size:4000000 } )
{ "ok" : 1 }

#重新开启Profiling
drug:PRIMARY> db.setProfilingLevel(1)
{ "was" : 0, "slowms" : 200, "ok" : 1 }

慢日志示例:

{
    "op": "command",                      // 操作类型,有insert、query、update、remove、getmore、command   
    "ns": "d1.m_pass_base",               // 操作的集合
    "command": {                          // 查询语句
        "aggregate": "m_pass_base",
        "pipeline": [
            {
                "$match": {
                    "_id": /^1/
                }
            },
            {
                "$group": {
                    "_id": NumberInt("1"),
                    "n": {
                        "$sum": NumberInt("1")
                    }
                }
            }
        ],
        "allowDiskUse": false,
        "cursor": { },
        "$db": "d1",
        "lsid": {
            "id": UUID("e4f7b72e-b69a-42a9-91e5-ec85c7c11f2a")
        }
    },
    "keysExamined": NumberInt("77810366"),
    "docsExamined": NumberInt("0"),
    "cursorExhausted": true,
    "numYield": NumberInt("607894"),
    "locks": {
        "Global": {
            "acquireCount": {
                "r": NumberLong("1215792")
            }
        },
        "Database": {
            "acquireCount": {
                "r": NumberLong("607896")
            }
        },
        "Collection": {
            "acquireCount": {
                "r": NumberLong("607896")
            }
        }
    },
    "nreturned": NumberInt("1"),
    "responseLength": NumberInt("111"),
    "protocol": "op_msg",
    "millis": NumberInt("42190"),                  // 消耗的时间(毫秒)
    "planSummary": "IXSCAN { _id: 1 }",
    "ts": ISODate("2021-04-06T13:43:56.792Z"),     // 语句执行的时间
    "client": "192.168.50.1",
    "allUsers": [ ],
    "user": ""
}

日常使用的查询

#返回最近的10条记录
db.system.profile.find().limit(10).sort({ ts : -1 }).pretty()

#返回所有的操作,除command类型的
db.system.profile.find( { op: { $ne : 'command' } } ).pretty()

#返回特定集合
db.system.profile.find( { ns : 'mydb.test' } ).pretty()

#返回大于5毫秒慢的操作
db.system.profile.find( { millis : { $gt : 5 } } ).pretty()

#从一个特定的时间范围内返回信息
db.system.profile.find(
                       {
                        ts : {
                              $gt : new ISODate("2021-04-06T03:00:00Z") ,
                              $lt : new ISODate("2021-04-06T03:40:00Z")
                             }
                       }
                      ).pretty()

#特定时间,限制用户,按照消耗时间排序
db.system.profile.find(
                       {
                         ts : {
                              $gt : new ISODate("2021-04-06T03:00:00Z") ,
                              $lt : new ISODate("2021-04-06T03:40:00Z")
                              }
                       },
                       { user : 0 }
                      ).sort( { millis : -1 } )

可逆加密算法

不可逆信息摘要算法(也称Hash算法)主要有:MD5,SHA系列,HMAC系列。

可逆加密算法分 对称式 和 非对称式,

对称式:DES,3DES,AES系列

非对称式:RSA,ECC椭圆曲线加密相关算法

基础可逆加密算法

简单的对称可逆加密算法原理,我们可以把它理解为一道数学题:

假设A有一个数字 88,接下来A想对它加密,拿一个密钥数7来加密,比如把它相加得到密文 88+7=95

A把加密后的95传输给B,B手上有协商好的密钥数7,同时也知道解密算法,那么B只要把密文减去密钥即可得到原来数字 95-7=88

以上传输过程中,只传递了密文95,原数字和密钥数都没被传输,也就实现了加密效果。

现实过程中,我们把这个数字看成计算机中的一个存储单位,比如字节(一个字节8比特),我们只要把这个字节做一遍数学运算,就能实现加密效果。

算法实现

根据以上思路,用PHP来实现对称加解密函数:

/**
 * 加密函数 
 * By: Malu
 * @param $data
 * @param $key
 * @return string
 */
public function encrypt($data, $key)
{
    $key = md5($key);
    $x = 0;
    $len = strlen($data);
    $l = strlen($key);

    $char = "";
    $str = "";
    // 循环拼接私钥md5后的字符,组装到待加密字串长度
    for ($i = 0; $i < $len; $i++) {
        if ($x == $l) {
            $x = 0;
        }
        $char .= $key[$x];
        $x++;
    }
    for ($i = 0; $i < $len; $i++) {
        // ord() 函数返回字符串的首个字符的 ASCII 值。
        // 给每个字符循环 加上 私钥md5后的 ASCII 与 256 求模后的值(求模是防止长度越界,比如中文字符)
        // 最后把 ASCII 值转成字符
        $str .= chr((ord($data[$i]) + ord($char[$i])) % 256);
    }
    return base64_encode($str); // 用基础的64个字符替换
}

/**
 * 解密函数
 * By: Malu
 * @param $data
 * @param $key
 * @return string
 */
public function decrypt($data, $key)
{
    $key = md5($key);
    $x = 0;
    $data = base64_decode($data);
    $len = strlen($data);
    $l = strlen($key);

    $char = "";
    $str = "";
    // 循环拼接私钥md5后的字符,组装到加密字串长度一样长
    for ($i = 0; $i < $len; $i++) {
        if ($x == $l) {
            $x = 0;
        }
        $char .= substr($key, $x, 1);
        $x++;
    }
    for ($i = 0; $i < $len; $i++) {
        if (ord(substr($data, $i, 1)) < ord(substr($char, $i, 1))) {
            // 如果加密字串ASCII小于密文ASCII,表示长度已越界,需要补256
            // 那么把 加密字串ASCII + 256 - 私钥md5后的字符串ASCII
            $str .= chr((ord(substr($data, $i, 1)) + 256) - ord(substr($char, $i, 1)));
        } else {
            $str .= chr(ord(substr($data, $i, 1)) - ord(substr($char, $i, 1)));
        }
    }
    return $str;
}

升级算法

以上代码在PHP下运行正常,但是在异构代码下运行呢?

比如PHP项目与Java项目加密传输,标准ASCII码共定义了128个字符,而以上算法用到了256位;

为了标准化,我把它限制到128个字符,我们来改进一下算法:

1.加密原文前做一次base64,把原文降为64个字符集。

2.把加法运算改成异或运算,这样就不用做越界处理。

下面是PHP实现的升级版可逆加密算法:

//url base64编码
public function urlsafe_b64encode($string)
{
    $data = base64_encode($string);
    $data = str_replace(array('+', '/', '='), array('-', '_', ''), $data);
    return $data;
}

//url base64解码
public function urlsafe_b64decode($string)
{
    $data = str_replace(array('-', '_'), array('+', '/'), $string);
    $mod4 = strlen($data) % 4;
    if ($mod4) {
        $data .= substr('====', $mod4);
    }
    return base64_decode($data);
}

/**
 * 加密函数 V2
 * By: Malu
 * @param $data
 * @param $key
 * @return string
 */
public function encrypt_v2($data, $key)
{
    $key = md5($key);
    $x = 0;

    $data = $this->urlsafe_b64encode($data);

    $len = strlen($data);
    $l = strlen($key);  // 32

    $char = "";
    $str = "";
    // 循环拼接私钥md5后的字符,组装到待加密字串长度
    for ($i = 0; $i < $len; $i++) {
        if ($x == $l) {
            $x = 0;
        }
        $char .= $key[$x];
        $x++;
    }
    for ($i = 0; $i < $len; $i++) {
        // ord() 函数返回字符串的首个字符的 ASCII 值。
        // 给每个字符循环 私钥md5后的ASCII 与 与原文处理后的字符做异或运算
        // 最后把 ASCII 值转成字符
        $str .= chr(ord($data[$i]) ^ ord($char[$i]));
    }
    $str = $this->urlsafe_b64encode($str); // 可以在URL安全传输

    return $str;
}

/**
 * 解密函数v2
 * By: Malu
 * @param $data
 * @param $key
 * @return string
 */
public function decrypt_v2($data, $key)
{
    $key = md5($key);
    $x = 0;
    $data = $this->urlsafe_b64decode($data);

    $len = strlen($data);
    $l = strlen($key);

    $char = "";
    $str = "";
    // 循环拼接私钥md5后的字符,组装到加密字串长度一样长
    for ($i = 0; $i < $len; $i++) {
        if ($x == $l) {
            $x = 0;
        }
        $char .= substr($key, $x, 1);
        $x++;
    }
    for ($i = 0; $i < $len; $i++) {
        // 把加密字串ASCII 与 私钥md5后的字符串ASCII 做异或运算
        // 最后把 ASCII 值还原成字符
        $str .= chr(ord(substr($data, $i, 1)) ^ ord(substr($char, $i, 1)));
    }
    $str = $this->urlsafe_b64decode($str);
    return $str;
}

下面是Java实现的升级版可逆加密算法:

// ord() 函数返回字符串的首个字符的 ASCII 值
public static int ord(String s) {
    return s.length() > 0 ? (s.getBytes(StandardCharsets.UTF_8)[0] & 0xff) : 0;
}

/**
 * 加密函数 V2
 * By: Malu
 *
 * @param data
 * @param key
 * @return
 */
public String encrypt(String data, String key) {
    key = md5(key);

    final Base64.Encoder encoder = Base64.getUrlEncoder();
    byte[] textByte = new byte[0];
    try {
        textByte = data.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    String encodedText = encoder.encodeToString(textByte);

    Integer x = 0;
    Integer len = encodedText.length();
    Integer l = key.length();

    String char_tmp = "";
    String str = "";
    // 循环拼接私钥md5后的字符,组装到待加密字串长度
    for (Integer i = 0; i < len; i++) {
        if (x == l) {
            x = 0;
        }
        char_tmp += key.substring(x, x + 1);
        x++;
    }

    for (Integer i = 0; i < len; i++) {
        // ord() 函数返回字符串的首个字符的 ASCII 值。
        // 给每个字符循环 私钥md5后的ASCII 与 与原文处理后的字符做异或运算
        // 最后把 ASCII 值转成字符
        str += (char) (ord(encodedText.substring(i, i + 1)) ^ ord(char_tmp.substring(i, i + 1)));
    }

    try {
        textByte = str.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    encodedText = encoder.encodeToString(textByte);

    return encodedText.replace("=", ""); // 可以在URL安全传输
}

/**
 * 解密函数 V2
 * By: Malu
 *
 * @param data
 * @param key
 * @return
 */
public String decrypt(String data, String key) {
    key = md5(key);
    Integer x = 0;
    String data_tmp = "";

    try {
        byte[] decodedBytes = Base64.getUrlDecoder().decode(data);
        data_tmp = new String(decodedBytes, "utf-8");
    } catch (Exception $e) {

    }

    Integer len = data_tmp.length();
    Integer l = key.length();

    String char_tmp = "";
    String str_tmp = "";
    // 循环拼接私钥md5后的字符,组装到加密字串长度一样长
    for (Integer i = 0; i < len; i++) {
        if (x == l) {
            x = 0;
        }
        char_tmp += key.substring(x, x + 1);
        x++;
    }
    for (Integer i = 0; i < len; i++) {
        // 把加密字串ASCII 与 私钥md5后的字符串ASCII 做异或运算
        // 最后把 ASCII 值还原成字符
        str_tmp += (chr(ord(data_tmp.substring(i, i + 1)) ^ ord(char_tmp.substring(i, i + 1))));
    }

    try {
        byte[] decodedBytes = Base64.getUrlDecoder().decode(str_tmp);
        data_tmp = new String(decodedBytes, "utf-8");
    } catch (Exception $e) {

    }

    return data_tmp;
}

算法加强

以上实现了基础的加密解密过程,我们可以自由改进其算法,比如把md5替换成其他哈希算法,也可以加盐,双重md5,甚至可以通过另外接口做成协商密钥,让密文随机变化,来加强加密强度。

PHP加密传输库

加解密库 php-encrypted-transmission 添加到 composer.json 配置文件

$ composer require malu/php-encrypted-transmission

升级 composer

$ composer update

使用示例

// If you installed via composer, just use this code to require autoloader on the top of your projects.
require 'vendor/autoload.php';

// Using Medoo namespace
use Malu\Encrypted\Encrypted;

$data = ["hello","malu","bbq"];

// 加密输出
$encrypt_data = Encrypted::encrypt(json_encode($data), "34f7e6dd6acf03192d82f0337c8c54ba");
echo $encrypt_data;

// 解密输出
echo Encrypted::decrypt($encrypt_data, "34f7e6dd6acf03192d82f0337c8c54ba");

JWT攻击

JWT是JSON Web Token的缩写,可用于身份认证,会话状态维持以及信息交换等任务。

JWT由三部分构成,分别称为 Header ,Payload 和 Signature ,各部分用“.”相连构成一个完整的Token,形如xxxxx.yyyyy.zzzzz。

遵循 RFC 7519 规范

使用一个JSON格式字符串声明令牌的类型和签名用的算法等,形如:

{
  "alg": "HS256",
  "typ": "JWT"
}

该字符串经过Base64Url编码后形成JWT的第一部分xxxxx。

Header声明一些标准:

Token Description Format
typ 令牌类型 (JWT/JWE/JWS等) string
alg 用于签名或加密的算法 string
kid Key ID - 用作查找 string
x5u x509证书的URL URL
x5c 用于签名的x509证书(作为嵌套的JSON对象) JSON object
jku JWKS格式键的URL URL
jwk 用于签名的JWK格式密钥(作为嵌套的JSON对象) JSON object

Payload

Payload 部分也是一个 JSON 对象,同样的,该字符串经过Base64Url编码形成JWT的第二部分yyyyy。

Payload7个官方字段:

Payload Key Description Format
iss 签发人 (issuer) string
sub 主题 (subject) string
aud 受众 (audience) string
exp 过期时间 (expiration time) Date
nbf 生效时间 (Not Before) Date
iat 签发时间 (Issued At) Date
jti 编号 (JWT ID) string

当然也可以使用私有字段。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

将xxxxx.yyyyy使用alg指定的算法加密,然后需要指定一个私有密钥(secret),再使用 Header 里面指定的签名算法(默认是 HMAC SHA256)得到JWT的第三部分zzzzz。

签名算法:

HMACSHA256(
  base64UrlEncode(header) + "." +  base64UrlEncode(payload),
  secret
)

JWT 的几个特点

1.JWT 不仅用于认证,也携带了Payload信息。对于服务端来说这个Payload可以直接拿来使用,可降低查询数据库的次数。同时也是一种便捷的Auth0解决方案。

2.JWT 由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务端专门部署额外的逻辑。

3.JWT 最大的缺陷是认证私钥,存在于签名里,存在暴力破解可能性,私钥一旦泄露,任何人都可以获得该令牌的所有权限。(该问题可以通过RSA非对称密钥来解决)

JWT攻击

JWT存在如下几个问题

1.敏感信息泄露

由于Header和Payload部分是使用可逆base64方法编码的,因此任何能够看到令牌的人都可以读取数据。

2.算法修改攻击

JWT支持将算法设定为 None 。如果 alg 字段设为 None ,那么签名会被置空,这样任何 token 都是有效的。

如果签名算法为 RS256,那么会选择用私钥进行签名,用公钥进行解密验证。如果服务端不严谨,我们拿到了泄露的公钥 pubkey。此时我们可以尝试将 header 的 alg 算法从 RS256 改为 HS256 ,此时即非对称密码变为对称加密,如果后端的验证也是根据 header 的 alg 选择算法,那么显然正中下怀。

3.密钥可控(SQL注入)

假如header头:

{
    "alg":"SH256",
    "typ":"JWT",
    "kid":"111"
}

其中kid为密钥key的编号id,类似逻辑:

select * from table where kid=$kid

如果在这里对 $kid 进行恶意篡改,例如:

kid = 0 union select 555

这样查询出来的结果为555,这样等同于我们控制了密钥key,拥有了密钥key,即可伪造认证。

4.暴力破解

我们知道 Signature 算法里有私钥,如果这个私钥的复杂度不够,那么显然可以通过暴力破解来攻击。

比如现成的JWT暴力破解工具:https://github.com/brendan-rius/c-jwt-cracker

生成RSA公私钥

命令行:

openssl genrsa -out rsa_private_key.pem 1024
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

在线生成:

https://www.bejson.com/enc/rsa/

注意事项

1.secret base64 encoded 对应处理办法

如果在 https://jwt.io/ 网站上勾选 secret base64 encoded,那么意味着secret密钥是经过 base64 encode 的,所以需要先 base64_decode 原来的 secret 密钥,再传入。

比如PHP里:

JWT2::encode($payload, base64_decode($secret));