I'm trying to speed up a Python script that reads a large log file (JSON lines, 50gb+) and filter out results that match 1 of 2000 CIDR ranges.
Logfile
20 million lines
{"ip":"xxx.xxx.xxx.xxx","timestamp":"2017-05-27T04:00:35-04:00","data":{},"error":"EOF","error_component":"banner"}
{"ip":"xxx.xxx.xxx.xxx","timestamp":"2017-05-27T04:00:35-04:00","data":{"banner":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","ehlo":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","starttls":"500 Unknown command\r\n"},"error":"Bad return code for STARTTLS","error_component":"starttls"}
{"ip":"xxx.xxx.xxx.xxx","timestamp":"2017-05-27T04:00:35-04:00","data":{},"error":"EOF","error_component":"banner"}
{"ip":"xxx.xxx.xxx.xxx","timestamp":"2017-05-27T04:00:35-04:00","data":{"banner":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","ehlo":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","starttls":"502 No such command\r\n"},"error":"Bad return code for STARTTLS","error_component":"starttls"}
CIDR file
2,000 lines
86.80.0.0/12
77.160.0.0/12
188.200.0.0/13
178.224.0.0/13
84.24.0.0/13
Script
import sys
import json
from netaddr import *
reload(sys)
sys.setdefaultencoding('utf-8')
filename = 'results.json'
filename = unicode(filename, 'utf-8')
cidr_filename = 'cidr.txt'
rowcount = 0
count = 0
# Load CIDR ranges
with open(cidr_filename, 'r') as f:
cidr = [line.strip() for line in f]
# Load JSON line by line
with open(filename) as f:
for line in f:
output = json.loads(line)
rowcount += 1
# Match if IP is in CIDR ranges
if all_matching_cidrs(output['ip'], cidr):
if 'banner' in output['data']:
print(output['ip'] + '\t' + output['data']['banner'])
count += 1
print('---------------------------------------')
print('LINES: {rowcount}')
print('RESULTS: {count}')
print('---------------------------------------')
Current results
Parsing an example set of 100,000 rows takes now 8 minutes using:
- Pypy
- MacBook Pro with 2.8 GHz Intel Core i7, 16Gb RAM, SSD
Parsing the complete set of 20,000,000 rows would take a staggering 26 hours.
---------------------------------------
LINES: 100000
RESULTS: 1243
---------------------------------------
real 7m57.739s
user 7m52.127s
sys 0m4.177s
The bottleneck is the number of CIDR ranges to search within, when I run an example set of 100,000 row against 1 CIDR range it takes only 1.2 seconds.
---------------------------------------
LINES: 100000
RESULTS: 4
---------------------------------------
real 0m1.201s
user 0m1.095s
sys 0m0.090s
Is there a faster way of accomplishing this? Would Multithreading/Multiprocessing speed things up? Any help or other feedback would be much appreciated!
Things I've done:
- Using Pypy, this is 9x(!) faster than Python 2.7 for this job.
- Tried using Tim Bray's Widefinder but couldn't make it work as it focuses on regex searches IMHO.
UPDATE
rolfl's solution brought my times to parse 20,344,457 rows from ±26 hours to 4.5 minutes!
---------------------------------------
LINES: 20344457
RESULTS: 130863
---------------------------------------
real 4m27.661s
user 3m55.171s
sys 0m26.793s
TemporalWolf's advice to cProfile my code showed that indeed json.loads() was a bottleneck:
ncalls tottime percall cumtime percall filename:lineno(function)
16607394 131.960 0.000 131.960 0.000 {_pypyjson.loads}
Following his advice to slice the IP address natively instead of loading each line as JSON it was 2.5x times faster!
---------------------------------------
LINES: 20344457
RESULTS: 130863
---------------------------------------
real 1m40.548s
user 1m13.885s
sys 0m22.664s
all_matching_cidrs(output['ip'], cidr):
do? \$\endgroup\$50GB
...JSON
... dear god... \$\endgroup\$json.loads()
takes a significant amount of time and most lines don't match your cidr list, naively slicing for IP addresses (and processsing for shorter than max length) takes about 1/20th the time ofjson.loads()
, which you only need to do for lines you are processing... a 95% reduction in cost for lines which are skipped. Is that significant? You won't know until you profile. \$\endgroup\$