Как стать автором
Обновить

Притча о семи с половиной языках

Время на прочтение11 мин
Количество просмотров7.7K

Дисклеймер.

Статья написана исключительно в развлекательных целях.

Как все начиналось

Любой администратор любой информационной системе знает, что мест для автоматизации в его работе бесконечное множество. Какая огромная ниша для автоматизации прячется в задачах обработки логов, сбора статистики, реакции на события систем.

Вот однажды и моего коллегу утомило обрабатывать гигабайты логов 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. Поэтому придется обходится «построчной» записью. Это конечно намного медленнее, но для наших нужд вполне подойдет. Для работы с JSON будем применять 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<BsonDocument> 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<BsonDocument>("sample_collection_cs");
        }


        public void SaveManyDoc(List<BsonDocument> 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<BsonDocument> Docs = new List<BsonDocument>();


                foreach (var item in JA1)
                {
 
                    if (item.Value<int>("MessageLatencyType") != 0)
                    {
                        var i = item.Value<JObject>("MessageLatency");
                        item["MessageLatency"] = i.Value<int>("TotalMilliseconds");
                        
                    }

                    BsonDocument BDoc = BsonSerializer.Deserialize<BsonDocument>(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<String> Files;
    public FileHarvester(String Path) {
        File _Path = new File(Path);
        this.Files = new ArrayList<String>();
        search(".*\\.json", _Path, Files);
    }

    public static void search(final String pattern, final File folder, List<String> 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<Document> jsonList = new ArrayList<Document>();
        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

  • работа с БД

На каждую часть повесили по таймеру и стали наблюдать. Оказалось, что практически в любом языке самым медленным является работа с объектами JSON.

Замена целого объекта MessageLatency на поле типа int самая долгая операция в любом языке и библиотеке по работе с JSON, кроме JS. Не представляю, как JS работает с объектами JSON, но выглядит так как будто он вообще не заморачивается десериализацией, и работает с ними как с массивами строк.

P.S.:

А вот внеконкурсные результаты работы в несколько потоков на языке Ruby.

Многопоточный Ruby обошел Go по скорости. Хотя это конечно читерство, но все равно приятно ))

Просто мысли вслух

Как я и говорил в начале, статья написано исключительно в развлекательных целях. Думаю, она вполне подойдет для легко чтива под чашку чая.

Исходя из вышесказанного не стоит искать здесь какой-то истины, ровно как и не стоит принимать полученные данные в качестве призыва к выбору инструмента разработки.

Что касается меня, то я сделал то что давно хотел - в первый раз попробовал Ruby и Go, а также посмотрел по пристальней на Java. Ruby оказался очень интересным языком, вероятно я продолжу общение с ним. Мой основной инструмент работы (PS7) оказался в топе, жалко что с конца :-)

Надеюсь кому-то это было интересно :-)

Всем спасибо за внимание.

Теги:
Хабы:
+24
Комментарии23

Публикации