Притча о семи с половиной языках
Дисклеймер.
Статья написана исключительно в развлекательных целях.
Как все начиналось
Любой администратор любой информационной системе знает, что мест для автоматизации в его работе бесконечное множество. Какая огромная ниша для автоматизации прячется в задачах обработки логов, сбора статистики, реакции на события систем.
Вот однажды и моего коллегу утомило обрабатывать гигабайты логов exchange сервера, чтобы найти нужные данные. Вполне здравая мысль посетила его светлый ум — «Куда как проще работать с данными если они лежат в какой ни будь БД». И не мудрствуя лукаво он лихо наваял скрипт на том что было установлено на его свеженьком Linux. Так появился первый скрипт на python который занимался складированием логов в MongoDB.
Через какое-то время в разговоре со мной он вскользь упомянул про этот механизм и изъявил желание переписать его на JS. Мне мысль понравилась, и я предложил написать этот скрипт на всем что есть под рукой. Чем мы и занялись в обеденные перерывы.
Входные данные
Для тестирования мы выбрали 545 json фала общим размером 1Гб. Количество документов в них примерно 687 тысяч.
В качестве сервера БД была выбрана MongoDB.
Дополнительные требования:
Так как экспортер логов exchange-а хранит дату в каком то совершенно непотребном виде («Timestamp»: »\/Date (1649404808029)\/»), необходимо было привести эту запись к тому который понимает Mongo.
Если поле «MessageLatencyType» имеет значение отличное от 0, то заменить значение поля MessageLatency на MessageLatency. TotalMilliseconds.
Вот пример этих полей:
"MessageLatencyType": 1
"MessageLatency": {
"Days": 0,
"Hours": 0,
"Milliseconds": 325,
"Minutes": 0,
"Seconds": 0,
"Ticks": 3250000,
"TotalDays": 3.7615740740740738E-06,
"TotalHours": 9.0277777777777774E-05,
"TotalMilliseconds": 325,
"TotalMinutes": 0.0054166666666666669,
"TotalSeconds": 0.325,
"Sign": 1
}
Стараться использовать только нативные или официальные инструменты для работы с JSON и Mongo.
Без читерства)))
Творческая часть
Что же оказалось у нас под рукой?
Python
В сущности, питон прекрасно подходит как для повседневных административных задач, так и для конкретно этой. В качестве драйвер использовался pymongo.
Hidden text
def get_database():
from pymongo import MongoClient
import pymongo
CONNECTION_STRING = 'mongodb://localhost:27017'
client = MongoClient(CONNECTION_STRING)
return client['sample_database_python']
def convert_datetime(unix_timestamp):
import datetime
milliseconds = 0
if len(unix_timestamp) == 13:
milliseconds = int(unix_timestamp[-3:])
unix_timestamp = float(unix_timestamp[0:-3])
the_date = datetime.datetime.utcfromtimestamp(unix_timestamp)
the_date += datetime.timedelta(milliseconds=milliseconds)
return the_date
def import_file(file_name):
import json
with open(file_name, 'r', encoding='utf8') as sample_file:
sample_data = json.load(sample_file)
for line in sample_data:
line['Timestamp'] = convert_datetime(line['Timestamp'][6:-2])
if (line['MessageLatencyType'] != 0):
line['MessageLatency'] = line['MessageLatency']['Milliseconds']
sample_collection.insert_many(sample_data)
if __name__ == '__main__':
database = get_database()
sample_collection = database['sample_collection']
import os
path = './data/'
for file in os.listdir(path):
if file.endswith('.json'):
import_file(f"{path}{file}")
# print(f"Imported file: {path}{file}")
JS
Прекрасный и очень шустрый язык. Устанавливаем драйвер БД и вперед.
Hidden text
const { MongoClient } = require("mongodb");
const client = new MongoClient("mongodb://127.0.0.1:27017");
const fs = require("fs");
// const path = "./data/";
const path = "/media/user/SOFT/SHARED/MX/";
async function run() {
try {
await client.connect();
const database = client.db("exchange_logs");
const logsCollection = database.collection("records");
const files = fs.readdirSync(path);
for (const file of files) {
await importFile(`${path}${file}`, logsCollection, database);
console.log(`Imported file: ${path}${file}`);
}
} catch (err) {
console.log(err);
} finally {
await client.close();
}
}
async function importFile(fileName, logsCollection, database) {
try {
const sample_data = JSON.parse(fs.readFileSync(fileName, "utf8"));
sample_data.forEach((record) => {
record.Timestamp = new Date(+record.Timestamp.slice(6, 19));
if (record.MessageLatencyType !== 0) record.MessageLatency = record.MessageLatency.Milliseconds;
});
const results = await logsCollection.insertMany(sample_data);
// console.log(results);
await database.collection("logs").insertOne({ FileName: fileName, Timestamp: new Date(), Success: true, InsertedCount: results.insertedCount });
} catch (error) {
await database.collection("logs").insertOne({ FileName: fileName, Timestamp: new Date(), Success: false });
}
}
run().catch(console.dir);
PowerShell 7,5
5:
К сожалению официальных драйверов под PS я не нашел, поэтому пришлось брать то что есть: Install-Module Mdbc.
Данный проект не новый, но он до сих пор не имеет реализации, столь нужного, командлета как InsertMany. Поэтому придется обходится «построчной» записью. Это конечно намного медленнее, но для наших нужд вполне подойдет. Для работы с JOSN будем применять JavaScriptSerializer из стандартной библиотеки System.Web.Script.Serialization.
Hidden text
Import-Module Mdbc
Add-Type -AssemblyName System.Web.Extensions
[string]$MongoUrl = "mongodb://localhost:27017"
[string]$Db = "sample_database_ps"
[string]$CollectionName = "sample_collection_5"
Connect-Mdbc $MongoUrl $Db $CollectionName
[string]$LogPath = "c:\temp\data"
$JS = New-Object System.Web.Script.Serialization.JavaScriptSerializer
$JS.MaxJsonLength = 32108864
$Files = Get-ChildItem -Path $LogPath | Select-Object FullName
foreach ($File in $Files){
$StreamReader = New-Object System.IO.StreamReader($File.FullName)
$Content = $StreamReader.ReadToEnd()
$StreamReader.Dispose()
$JSON = $JS.DeserializeObject($Content)
foreach($Item in $JSON){
if($Item.MessageLatencyType -ne 0){
$Item.MessageLatency = $Item.MessageLatency.TotalMilliseconds
}
}
$JSON | Add-MdbcData
}
7:
Седьмая версия PS обладает всеми недостатками предыдущей версии. Но имеет существенное отличие, а именно собственный конвертер для JSON. Забегая вперед скажу, что он довольно-таки неповоротливый.
Hidden text
Import-Module Mdbc
[string]$MongoUrl = "mongodb://localhost:27017"
[string]$Db = "sample_database_ps"
[string]$CollectionName = "sample_collection_7"
Connect-Mdbc $MongoUrl $Db $CollectionName
[string]$LogPath = "c:\temp\data"
$Files = Get-ChildItem -Path $LogPath | Select-Object FullName
foreach ($File in $Files){
$Reader = New-Object System.IO.StreamReader($File.FullName)
$Content = $Reader.ReadToEnd()
$Reader.Dispose()
$JSON = $Content | ConvertFrom-Json # <--- Вот он
foreach($Item in $JSON){
if($Item.MessageLatencyType -ne 0){
$Item.MessageLatency = $Item.MessageLatency.TotalMilliseconds
}
}
$JSON | Add-MdbcData
}
GoLang
Признаться, я возлагал большие надежды на этот язык. Но о результатах мы порассуждаем чуть позже. А пока немного кода
Hidden text
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"time"
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var collection *mongo.Collection
var ctx = context.TODO()
func main() {
DBinit()
files := []string{}
files = GetFiles("c:\\temp\\data")
for _, item := range files {
WorkJSON(item)
}
}
func GetFiles(Path string) []string {
result := []string{}
files, err := ioutil.ReadDir(Path)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
s := filepath.Join(Path, file.Name())
result = append(result, s)
}
return result
}
func WorkJSON(Path string) {
jsonFile, err := os.Open(Path)
if err != nil {
fmt.Println(err)
}
byteValue, _ := ioutil.ReadAll(jsonFile)
jsonFile.Close()
var result []map[string]interface{}
var docs = make([]interface{}, 0)
json.Unmarshal([]byte(byteValue), &result)
for _, item := range result {
if item["MessageLatencyType"] != 0.0 {
item["MessageLatency"] = item["MessageLatency"].(map[string]interface{})["TotalMilliseconds"]
}
var strjsonDate = item["Timestamp"].(string)[6:19]
strDate, err := strconv.ParseInt(strjsonDate, 10, 64)
if err != nil {
panic(err)
}
tm := time.Unix(strDate, 0)
item["Timestamp"] = tm
docs = append(docs, item)
}
collection.InsertMany(ctx, docs)
}
func DBinit() {
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatal(err)
}
err = client.Ping(ctx, nil)
if err != nil {
log.Fatal(err)
}
collection = client.Database("sample_database_node").Collection("sample_collection_GO")
}
Во время работы на Go меня почему то посетила ностальгия. Вспомнилось как около 25 лет назад меня приводили в замешательство слова про указатели))
Ruby
Оказывается, в руби очень удобно работать с многопоточностью. И не смотря на то что в остальных версиях мы делали все в один поток, на руби я сделал форк который умеет работать в несколько потоков.
И так, не забываем про gem install mongo и вперед:
Hidden text
class FilesHarvester
attr_reader :path, :Files
def initialize (path)
@path = path
self.GetFiles
end
def GetFiles
@Files = Dir.glob(path)
end
end
class JSONWorker
attr_reader :path, :json_data
def initialize(path)
require 'json'
@path = path
json_file = File.open path
@json_data = JSON.load(json_file)
@json_data.each do |jsitem|
if (jsitem["MessageLatencyType"] != 0)
ts = jsitem["MessageLatency"]["TotalMilliseconds"]
jsitem["MessageLatency"] = ts
end
raw_date = jsitem["Timestamp"]
right_data = self.convert_time(raw_date)
jsitem["Timestamp"] = right_data
end
end #init
def convert_time(raw_date)
require 'time'
clear_date = raw_date[6..-3].to_s
reult = DateTime.strptime(clear_date, '%Q')
return reult
end #convert_time
end
class MongoDBClient
attr_reader :collection
def initialize
require 'mongo'
client = Mongo::Client.new([ '127.0.0.1:27017' ], :database => 'sample_database_node')
db = client.database
@collection = client[:sample_collection_ruby]
end
end
fileList = FilesHarvester.new(File.expand_path File.dirname(__FILE__) + '/data/*')
#fileList = FilesHarvester.new('F:/TEMP/Ruby/data_/*')
paths = fileList.Files
client = MongoDBClient.new
paths.each do |path|
jw = JSONWorker.new(path)
client.collection.insert_many(jw.json_data)
end
C#
В качестве сериализатора Json использовался Newtonsoft взятый NuGet-ом. Драйвера для Mongo официальные, взяты оттуда же.
Hidden text
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using MongoDB.Driver;
namespace import_test_cs
{
public class FilesWorker
{
public string[] files;
public FilesWorker(string Path)
{
files = Directory.GetFiles(Path, "*.json",
SearchOption.TopDirectoryOnly);
}
}
public class DBWorker
{
private string connectionString = "mongodb://localhost:27017";
private MongoClient client;
private IMongoCollection DBCollection;
public DBWorker()
{
Setup();
BsonDocument chemp = new BsonDocument();
}
private void Setup()
{
client = new MongoClient(connectionString);
IMongoDatabase database = client.GetDatabase("sample_database_node");
DBCollection = database.GetCollection("sample_collection_cs");
}
public void SaveManyDoc(List bsonArray)
{
DBCollection.InsertMany(bsonArray);
}
}
class Program
{
static DateTime ConvertDateTime(string source)
{
DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dtDateTime = dtDateTime.AddMilliseconds(double.Parse(source));
return dtDateTime;
}
static void Main(string[] args)
{
string[] paths = new FilesWorker(@"c:/temp/data/").files;
DBWorker DBClient = new DBWorker();
foreach (string path in paths)
{
JArray JA1 = JArray.Parse(File.ReadAllText(path));
List Docs = new List();
foreach (var item in JA1)
{
if (item.Value("MessageLatencyType") != 0)
{
var i = item.Value("MessageLatency");
item["MessageLatency"] = i.Value("TotalMilliseconds");
}
BsonDocument BDoc = BsonSerializer.Deserialize(item.ToString());
string DB = BDoc.GetValue("Timestamp").AsString;
DateTime RealTime = DateTime.Parse(DB);
BDoc.Remove("Timestamp");
BDoc.Add(new BsonElement("Timestamp", RealTime));
Docs.Add(BDoc);
}
DBClient.SaveManyDoc(Docs);
}
}
}
}
JAVA
Код на java получился самый объемный. Для работы с json применялась легкая библиотека com.github.cliftonlabs: json-simple
FileHarvester.java
import java.util.ArrayList;
import java.util.List;
import java.io.File;
public class FileHarvester {
private File _Path;
public List Files;
public FileHarvester(String Path) {
File _Path = new File(Path);
this.Files = new ArrayList();
search(".*\\.json", _Path, Files);
}
public static void search(final String pattern, final File folder, List result) {
for (final File f : folder.listFiles()) {
if (f.isDirectory()) {
search(pattern, f, result);
}
if (f.isFile()) {
if (f.getName().matches(pattern)) {
result.add(f.getAbsolutePath());
}
}
}
}
}
DBClient.java
import com.github.cliftonlabs.json_simple.JsonArray;
import com.github.cliftonlabs.json_simple.JsonObject;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class DBClient {
private JsonArray JSON;
private MongoCollection collection;
public DBClient(JsonArray JSON, MongoCollection collection){
this.JSON = JSON;
this.collection = collection;
PutDate();
}
private void PutDate(){
List jsonList = new ArrayList();
for (Object Item: this.JSON) {
Document doc;//= new Document();
String s = ((JsonObject)Item).toJson();
doc = Document.parse(s);
String d = doc.getString("Timestamp");
LocalDateTime date = LocalDateTime.parse(d, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
doc.put("Timestamp", date);
jsonList.add(doc);
}
this.collection.insertMany(jsonList);
}
}
JSONWorker.java
import com.github.cliftonlabs.json_simple.*;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
public class JSONWorker {
public JsonArray JSON;
public String JSONString;
JSONWorker(String FilePath) throws IOException, JsonException {
Reader reader = Files.newBufferedReader(Paths.get(FilePath));
JsonArray TempJSON = (JsonArray) Jsoner.deserialize(reader);
for (Object Item: TempJSON) {
JsonKey Key = Jsoner.mintJsonKey("MessageLatencyType", 0);
int i = ((JsonObject)Item).getInteger(Key);
if(i != 0)
{
JsonKey KeyFromMountain = Jsoner.mintJsonKey("MessageLatency", null);
JsonObject MS = ((JsonObject)Item).getMapOrDefault(KeyFromMountain);
JsonKey KeyFromTotalMilliseconds = Jsoner.mintJsonKey("TotalMilliseconds", 0);
int TotalMilliseconds = MS.getInteger(KeyFromTotalMilliseconds);
((JsonObject)Item).put("MessageLatency", TotalMilliseconds);
}
JsonKey DataKey = Jsoner.mintJsonKey("Timestamp", 0);
String DataString = ((JsonObject)Item).getString(DataKey);
String ShortDataString = DataString.substring(6, 19);
long LongDataString = Long.parseLong(ShortDataString);
Timestamp ts = new Timestamp(LongDataString);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
String my8601formattedDate = df.format(ts);
((JsonObject)Item).put("Timestamp", my8601formattedDate);
} //for
this.JSON = TempJSON;
this.JSONString = TempJSON.toJson();
}
}
ImportTest.java
import com.github.cliftonlabs.json_simple.JsonException;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ImportTest {
public static void main(String[] args) throws JsonException, IOException {
Path path = Paths.get("");
String s = path.toAbsolutePath().toString();
String DataPath = s + File.separator + "data";
System.out.println("Data path:" + DataPath);
FileHarvester FilesHarvest = new FileHarvester(DataPath);
MongoClient mongoClient = new MongoClient();
MongoDatabase database = mongoClient.getDatabase("sample_database_node");
MongoCollection collection = database.getCollection("sample_collection_java");
for (String FilePath : FilesHarvest.Files) {
JSONWorker JSONw = new JSONWorker(FilePath);
DBClient Mongo = new DBClient(JSONw.JSON, collection);
} //for
mongoClient.close();
}
}
Результаты
И так. Для чего же мы все это делали?
На самом деле, ни для чего))). Просто это достаточно легкая задача для того что бы попробовать что-то новое, какой-то новый язык. Понять его удобство, или скорость разработки, или красоту цветовой палитры, ну или любой другой субъективный параметр.
Но коль уж мы потратили столько времени, давайте все-таки сделаем какие-то количественные выводы.
Хм… Что же вызывает такую большую разницу в скорости?
Для ответа на этот вопросы мы условно разделили программу на части:
На каждую часть повесили по таймеру и стали наблюдать. Оказалось, что практически в любом языке самым медленным является работа с объектами JSON.
Замена целого объекта MessageLatency на поле типа int самая долгая операция в любом языке и библиотеке по работе с JSON, кроме JS. Не представляю, как JS работает с объектами JSON, но выглядит так как будто он вообще не заморачивается десериализацией, и работает с ними как с массивами строк.
P.S.:
А вот внеконкурсные результаты работы в несколько потоков на языке Ruby.
Многопоточный Ruby обошел Go по скорости. Хотя это конечно читерство, но все равно приятно))
Просто мысли вслух
Как я и говорил в начале, статья написано исключительно в развлекательных целях. Думаю, она вполне подойдет для легко чтива под чашку чая.
Исходя из вышесказанного не стоит искать здесь какой-то истины, ровно как и не стоит принимать полученные данные в качестве призыва к выбору инструмента разработки.
Что касается меня, то я сделал то что давно хотел — в первый раз попробовал Ruby и Go, а также посмотрел по пристальней на Java. Ruby оказался очень интересным языком, вероятно я продолжу общение с ним. Мой основной инструмент работы (PS7) оказался в топе, жалко что с конца :-)
Надеюсь кому-то это было интересно :-)
Всем спасибо за внимание.