我經常看到很多程序員, 運維在代碼搜索上使用ack, 甚至ag(the_silver_searcher ), 而我工作中95%都是用grep,剩下的是ag. 我覺得很有必要聊一聊這個話題. 我以前也是一個運維, 我當時也希望找到最好的最快的工具用在工作的方方面面. 但是我很好奇為什麼ag和ack沒有作為linux發行版的內置部分. 內置的一直是grep. 我當初的理解是受各種開源協議的限制, 或者發行版的boss個人喜好. 後來我就做了實驗, 研究了下他們到底誰快. 當時的做法也無非跑幾個真實地線上log看看用時. 然後我也有了我的一個認識: 大部分時候用grep也無妨, 日志很大的時候用ag.
ack原來的域名是betterthangrep.com, 現在是beyondgrep.com. 好吧. 其實我理解使用ack的同學, 也理解ack產生的原因. 這裡就有個故事.
最開始我做運維使用shell, 經常做一些分析日志的工作. 那時候經常寫比較復雜的shell代碼實現一些特定的需求. 後來來了一位會perl的同學. 原來我寫shell做一個事情, 寫了20多行shell代碼, 跑一次大概5分鐘, 這位同學來了用perl改寫, 4行, 一分鐘就能跑完. 亮瞎我們的眼, 從那時候開始, 我就覺得需要學perl,以至於後來的python.
perl是天生用來文本解析的語言, ack的效率確實很高. 我想著可能是大家認為ack要更快更合適的理由吧. 其實這件事要看場景. 我為什麼還用比較’土’的grep呢? 看一下這篇文章, 希望給大家點啟示
PS: 嚴重聲明, 本實驗經個人實踐, 我盡量做到合理. 大家看完覺得有異議可以試著其他的角度來做. 並和我討論.
我使用了公司的一台開發機(gentoo)
我測試了純英文和漢語2種, 漢語使用了結巴分詞的字典, 英語使用了miscfiles
中提供的詞典
# 假如你是ubuntu: sudo apt-get install miscfiles wget https://raw.githubusercontent.com/fxsjy/jieba/master/extra_dict/dict.txt.big
我會分成英語和漢語2種文件, 文件大小為1MB, 10MB, 100MB, 500MB, 1GB, 5GB. 沒有更多是我覺得在實際業務裡面不會單個日志文件過大的. 也就沒有必要測試了(就算有, 可以看下面結果的趨勢)
cat make_words.py # coding=utf-8 import os import random from cStringIO import StringIO EN_WORD_FILE = '/usr/share/dict/words' CN_WORD_FILE = 'dict.txt.big' with open(EN_WORD_FILE) as f: EN_DATA = f.readlines() with open(CN_WORD_FILE) as f: CN_DATA = f.readlines() MB = pow(1024, 2) SIZE_LIST = [1, 10, 100, 500, 1024, 1024 * 5] EN_RESULT_FORMAT = 'text_{0}_en_MB.txt' CN_RESULT_FORMAT = 'text_{0}_cn_MB.txt' def write_data(f, size, data, cn=False): total_size = 0 while 1: s = StringIO() for x in range(10000): cho = random.choice(data) cho = cho.split()[0] if cn else cho.strip() s.write(cho) s.seek(0, os.SEEK_END) total_size += s.tell() contents = s.getvalue() f.write(contents + '\n') if total_size > size: break f.close() for index, size in enumerate([ MB, MB * 10, MB * 100, MB * 500, MB * 1024, MB * 1024 * 5]): size_name = SIZE_LIST[index] en_f = open(EN_RESULT_FORMAT.format(size_name), 'a+') cn_f = open(CN_RESULT_FORMAT.format(size_name), 'a+') write_data(en_f, size, EN_DATA) write_data(cn_f, size, CN_DATA, True)
好吧, 效率比較低是吧? 我自己沒有vps, 公司服務器我不能沒事把全部內核的cpu都占滿(不是運維好幾年了). 假如你不介意htop的多核cpu飄紅, 可以這樣,耗時就是各文件生成的時間短板:
# coding=utf-8 import os import random import multiprocessing from cStringIO import StringIO EN_WORD_FILE = '/usr/share/dict/words' CN_WORD_FILE = 'dict.txt.big' with open(EN_WORD_FILE) as f: EN_DATA = f.readlines() with open(CN_WORD_FILE) as f: CN_DATA = f.readlines() MB = pow(1024, 2) SIZE_LIST = [1, 10, 100, 500, 1024, 1024 * 5] EN_RESULT_FORMAT = 'text_{0}_en_MB.txt' CN_RESULT_FORMAT = 'text_{0}_cn_MB.txt' inputs = [] def map_func(args): def write_data(f, size, data, cn=False): f = open(f, 'a+') total_size = 0 while 1: s = StringIO() for x in range(10000): cho = random.choice(data) cho = cho.split()[0] if cn else cho.strip() s.write(cho) s.seek(0, os.SEEK_END) total_size += s.tell() contents = s.getvalue() f.write(contents + '\n') if total_size > size: break f.close() _f, size, data, cn = args write_data(_f, size, data, cn) for index, size in enumerate([ MB, MB * 10, MB * 100, MB * 500, MB * 1024, MB * 1024 * 5]): size_name = SIZE_LIST[index] inputs.append((EN_RESULT_FORMAT.format(size_name), size, EN_DATA, False)) inputs.append((CN_RESULT_FORMAT.format(size_name), size, CN_DATA, True)) pool = multiprocessing.Pool() pool.map(map_func, inputs, chunksize=1)
等待一段時間後,目錄下是這樣的:
$ls -lh total 14G -rw-rw-r-- 1 vagrant vagrant 2.2K Mar 14 05:25 benchmarks.ipynb -rw-rw-r-- 1 vagrant vagrant 8.2M Mar 12 15:43 dict.txt.big -rw-rw-r-- 1 vagrant vagrant 1.2K Mar 12 15:46 make_words.py -rw-rw-r-- 1 vagrant vagrant 101M Mar 12 15:47 text_100_cn_MB.txt -rw-rw-r-- 1 vagrant vagrant 101M Mar 12 15:47 text_100_en_MB.txt -rw-rw-r-- 1 vagrant vagrant 1.1G Mar 12 15:54 text_1024_cn_MB.txt -rw-rw-r-- 1 vagrant vagrant 1.1G Mar 12 15:51 text_1024_en_MB.txt -rw-rw-r-- 1 vagrant vagrant 11M Mar 12 15:47 text_10_cn_MB.txt -rw-rw-r-- 1 vagrant vagrant 11M Mar 12 15:47 text_10_en_MB.txt -rw-rw-r-- 1 vagrant vagrant 1.1M Mar 12 15:47 text_1_cn_MB.txt -rw-rw-r-- 1 vagrant vagrant 1.1M Mar 12 15:47 text_1_en_MB.txt -rw-rw-r-- 1 vagrant vagrant 501M Mar 12 15:49 text_500_cn_MB.txt -rw-rw-r-- 1 vagrant vagrant 501M Mar 12 15:48 text_500_en_MB.txt -rw-rw-r-- 1 vagrant vagrant 5.1G Mar 12 16:16 text_5120_cn_MB.txt -rw-rw-r-- 1 vagrant vagrant 5.1G Mar 12 16:04 text_5120_en_MB.txt
確認版本
➜ test ack --version # ack在ubuntu下叫`ack-grep` ack 2.12 Running under Perl 5.16.3 at /usr/bin/perl Copyright 2005-2013 Andy Lester. This program is free software. You may modify or distribute it under the terms of the Artistic License v2.0. ➜ test ag --version ag version 0.21.0 ➜ test grep --version grep (GNU grep) 2.14 Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by Mike Haertel and others, see <http://git.sv.gnu.org/cgit/grep.git/tree/AUTHORS>.
實驗設計
為了不產生並行執行的相互響應, 我還是選擇了效率很差的同步執行, 我使用了ipython提供的%timeit. 上代碼溫馨提示, 這是一個灰常耗時的測試. 開始執行後 要喝很久的茶…
import re import glob import subprocess import cPickle as pickle from collections import defaultdict IMAP = { 'cn': ('豆瓣', '小明明'), 'en': ('four', 'python') } OPTIONS = ('', '-i', '-v') FILES = glob.glob('text_*_MB.txt') EN_RES = defaultdict(dict) CN_RES = defaultdict(dict) RES = { 'en': EN_RES, 'cn': CN_RES } REGEX = re.compile(r'text_(\d+)_(\w+)_MB.txt') CALL_STR = '{command} {option} {word} {filename} > /dev/null 2>&1' for filename in FILES: size, xn = REGEX.search(filename).groups() for word in IMAP[xn]: _r = defaultdict(dict) for command in ['grep', 'ack', 'ag']: for option in OPTIONS: rs = %timeit -o -n10 subprocess.call(CALL_STR.format(command=command, option=option, word=word, filename=filename), shell=True) best = rs.best _r[command][option] = best RES[xn][word][size] = _r # 存起來 data = pickle.dumps(RES) with open('result.db', 'w') as f: f.write(data)
我來秦皇島辦事完畢(耗時超過1一天), 繼續我們的實驗.
我想工作的時候一般都是用到不帶參數/帶-i(忽略大小寫)/-v(查找不匹配項)這三種. 所以這裡測試了:
渲染圖片的gist可以看這裡benchmarks.ipynb. 他的數據來自上面跑的結果在序列化之後存入的文件