В статье отражен опыт применения языков Perl и Golang в повседневной работе бородатого сисадмина в качестве скриптового языка и показаны примеры использования.
Начало времен
Когда-то давно в молодости я выбирал инструмент, который помог бы мне автоматизировать ручной труд, а именно распарсить лог или конфиг, протестировать коннект к базе данных, собрать ответы от сайтов и т.д. И я выбрал Perl. Он до сих пор является палочкой-выручалочкой. Внятное объяснение этому можно найти в данной заметке, из которой я приведу лишь главные причины актуальности Perl:
-
Он везде установлен по умолчанию, при этом удобен для быстрых сценариев и адаптируется к новым парадигмам.
-
Я могу быть уверен, что сценарий Perl, который я пишу сегодня, будет работать без изменений через 10 лет.
Любой уважающий себя сисадмин должен быть с программистским уклоном. Мне всегда было интересно писать всякого рода прокси, а также скрипты синхронизации баз данных, мониторинга, бэкапа и т.д. Для этого на CPAN можно найти кучу примеров кода с отличной документацией, которая, по моему мнению, является лучшим примером оформления и представления документации к коду.
Что-то новенькое
И вот недавно меня попросили написать скрипт (точнее я сам напросился :-)), который дергает некое api по http и результат (json) складывает в noSQL базу данных, при этом главным условием было то, что нельзя писать на Perl, т.к. в команде программистов никто его не знает. Тогда я предложил написать на Golang, ведь, по моему мнению, именно этот современный язык заслуживает внимания сисадминов. До этого момента я никогда не писал на языках со статической типизацией, да и образование у меня не программистское, но тем интересней мне показалась задача.
Так как Golang для меня новый язык, то прежде чем браться за работу (времени у меня было предостаточно) я решил напиcать скрипты на Perl и Golang для трех распространенных задач и одной не очень распространенной, тем самым сравнив некоторые моменты: скорость написания скриптов, время выполнения, потребление памяти. Вот список задач, который я собрал для примеров кода:
-
Найти 500-е коды ответов в access.log размером ~1G
-
Сделать выборку из sql базы данных (в таблице 12200 строк)
-
Узнать дату выдачи и дату окончания действия ssl сертификата сайта www.example.com
-
По особенному распарсить json
Примеры скриптов
Для замера времени выполнения все скрипты запускались через утилиту time, вывод которой будет показан после примеров кода. В момент работы скриптов в соседнем терминале была запущена команда (for i in $(seq 1 3);do ps -eo rss,command | grep script | grep -vE 'grep|vim|go run'; sleep 1 ;done), делающая замер потребления памяти (первая колонка) три раза, вывод которой так же будет показан после примеров кода.
Задача 1. Найти 500-е коды ответов в access.log размером ~1G
script.pl
#!/usr/bin/env perl use strict; use warnings; use utf8; use open qw(:std :utf8); open FILE, "<", 'access.log' or die $!; while (<FILE>) { my @a = split /\s+/; print if $a[8] eq '500'; } close FILE;
script.go
package main import ( "bufio" "fmt" "log" "os" "regexp" ) func main() { file, err := os.Open("access.log") if err != nil { log.Fatalf("%s", err) } fileScanner := bufio.NewScanner(file) for fileScanner.Scan() { s := regexp.MustCompile(`\s+`).Split(fileScanner.Text(), 10) if s[8] == "500" { fmt.Println(s) } } }
|
|
script.pl |
script.go |
|
Вермя выполнения |
real 0m16,132s |
real 0m24,357s |
|
Потребление памяти (in kilobytes) |
6400 perl ./script.pl |
9608 ./script |
Задача 2. Сделать выборку из sql базы данных (в таблице 12200 строк)
script.pl
#!/usr/bin/env perl use strict; use warnings; use utf8; use open qw(:std :utf8); use DBI; my $dbh = DBI->connect( "dbi:Pg:dbname='exp';host='10.10.10.1';port=5432", 'exp', '111', {AutoCommit => 1, RaiseError => 1, PrintError => 0, pg_enable_utf8 => 1, ShowErrorStatement => 0} ); my $sth = $dbh->prepare('SELECT deal_city_id, "ShortName", "FullName" FROM public.deal_city ORDER BY deal_city_id'); $sth->execute(); while (my $ref = $sth->fetchrow_hashref) { print $ref->{deal_city_id}.' '.$ref->{ShortName}.' '.$ref->{FullName}. "\n"; }
script.go
package main import ( "database/sql" "fmt" "log" _ "github.com/lib/pq" ) func main() { db, err := sql.Open("postgres", "host=10.10.10.1 port=5432 user=exp password=111 dbname=exp sslmode=disable") if err != nil { log.Fatal(err) } rows, err := db.Query("SELECT deal_city_id, \"ShortName\", \"FullName\" FROM public.deal_city ORDER BY deal_city_id") if err != nil { log.Fatal(err) } defer rows.Close() var ( deal_city_id int ShortName string FullName string ) for rows.Next() { err := rows.Scan(&deal_city_id, &ShortName, &FullName) if err != nil { log.Fatal(err) } fmt.Printf("%v %s %s\n", deal_city_id, ShortName, FullName) } }
|
|
script.pl |
script.go |
|
Время выполнения |
real 0m2,885s |
real 0m2,790s |
|
Потребление памяти (in kilobytes) |
15232 perl ./script.pl |
6896 ./script |
Задача 3. Узнать дату выдачи и дату окончания действия ssl сертификата сайта www.example.com
script.pl
#!/usr/bin/env perl use strict; use warnings; use Crypt::OpenSSL::X509; use IO::Socket::SSL; my $client = IO::Socket::SSL->new( PeerHost => "www.example.com", PeerPort => 443, SSL_verify_callback => \&verify_cert, ) or die "error=$!, ssl_error=$SSL_ERROR"; $client->close(); sub verify_cert { return 1 if $_[5] != 0; my $cert_pem = Net::SSLeay::PEM_get_string_X509($_[4]); my $x509 = Crypt::OpenSSL::X509->new_from_string($cert_pem); print $x509->subject() . "\n"; print $x509->notBefore() . "\n"; print $x509->notAfter() . "\n"; return 1; }
вывод скрипта
C=US, ST=California, L=Los Angeles, O=InternetCorporationforAssignedNamesandNumbers, CN=www.example.org Jan 13 00:00:00 2023 GMT Feb 13 23:59:59 2024 GMT
script.go
package main import ( "crypto/tls" "fmt" ) func main() { conn, err := tls.Dial("tcp", "www.example.com:443", &tls.Config{InsecureSkipVerify: true}) if err != nil { panic("failed to connect: " + err.Error()) } defer conn.Close() cs := conn.ConnectionState() for _, cert := range cs.PeerCertificates { fmt.Printf("%v\n", cert.Subject) fmt.Printf("%v\n", cert.NotBefore) fmt.Printf("%v\n", cert.NotAfter) break } }
вывод скрипта
CN=www.example.org,O=Internet Corporation for Assigned Names and Numbers,L=Los Angeles,ST=California,C=US 2023-01-13 00:00:00 +0000 UTC 2024-02-13 23:59:59 +0000 UTC
|
|
script.pl |
script.go |
|
Время выполнения |
real 0m0,531s |
real 0m0,441s |
|
Потребление памяти (in kilobytes) |
22732 perl ./script.pl |
9900 ./script |
Задача 4. По особенному распарсить json
Эта задача родилась уже после того, как я написал тот самый скрипт, который меня попросили. Дело тут в том, что Perl просто берет json строку и парсит ее целиком в свои структуры (массивы, хеши) и дальше ты работаешь уже с ними. В Golang все немного сложнее, прежде чем парсить нужно самому описать весь json (все объекты!) в типах, с которыми дальше удобно работать. Вот тут есть статья, описывающая нюансы парсинга json в Golang, прочитав которую можно понять весь масштаб трагедии для человека писавшего всю жизнь на языке с динамическими типами. Хорошо, что в моем скрипте мне не надо было глубоко парсить json, а хватило лишь разбить верхний json массив на строки, которые представляют json объект. В приведенных ниже скриптах показан пример такого подхода, сначала на Golang, а уже потом, ради интереса, повторенный на Perl, поэтому сначала представлен скрипт на Golang, а уже потом на Perl, в отличие от других задач, в которых последовательность написания скриптов была другой.
script.go
package main import ( "encoding/json" "fmt" ) type Developer struct { RawValue string } func (d *Developer) UnmarshalJSON(data []byte) error { d.RawValue = string(data) return nil } func main() { jsonStr := `[ {"id":1,"name":"Larry"}, {"id":2,"name":"Robert"}, {"id":3,"name":"Rob"}, {"id":4,"name":"Ken"} ]` developers := []Developer{} if err := json.Unmarshal([]byte(jsonStr), &developers); err != nil { panic(err) } for _, d := range developers { //fmt.Printf("%s --- is %T\n", d.RawValue, d.RawValue) fmt.Printf("%s\n", d.RawValue) } }
вывод скрипта
{"id":1,"name":"Larry"} {"id":2,"name":"Robert"} {"id":3,"name":"Rob"} {"id":4,"name":"Ken"}
script.pl
#!/usr/bin/env perl use strict; use warnings; use utf8; use open qw(:std :utf8); use JSON (); my $jsonStr = '[ {"id":1,"name":"Larry"}, {"id":2,"name":"Robert"}, {"id":3,"name":"Rob"}, {"id":4,"name":"Ken"} ]'; my $developers = eval { JSON ->new ->filter_json_object(sub{JSON::encode_json(shift)}) ->decode($jsonStr) }; die $@ if $@; print $_ . "\n" for @$developers;
вывод скрипта
{"id":1,"name":"Larry"} {"id":2,"name":"Robert"} {"name":"Rob","id":3} {"name":"Ken","id":4}
В этой задаче не будет таблицы с замерами, т.к. это бессмысленно. Взглянув на скрипты, можно увидеть, что подходы совершенно разные. Golang не пытается распарсить весь json, а разбивает лишь верхний массив на строки. Perl сначала парсит массив, затем парсит объекты внутри него в свои хеши и уже затем просто заменяет их на то, что ему подсунули, а подсунули ему обратно сериализованный хеш, и поэтому в выводе скрипта ключи внутри некоторых json объектов перемешаны.
Выводы
Начнем с самого простого — скорость написания скриптов. У обоих языков хорошая документация и куча примеров кода на все случаи, поэтому этот критерий зависит только от практики, и зависимость тут линейная — чем больше практики, тем быстрее скорость написания скриптов.
При работе с текстом (задача 1) Perl оказался на высоте: он и памяти меньше потребляет, и быстрее работает. Вот тут есть классные тесты по потреблению памяти, в которых подтверждается превосходство Perl при работе с текстом. Возможно, приведенный мною код на Golang не сильно оптимизированный и можно его улучшить, чтобы сократить отставание. Я буду только рад, если в комментариях предложат вариант пооптимальней. Нужно сделать еще одну оговорку: в данной задаче в варианте на Perl я не использовал ни одного модуля, и если вдруг окажется, что для распарсивания лога будет нужен модуль (например DateTime), то считаю, что Perl сравняется по скорости с Golang и проиграет по памяти, как это происходит в других задачах.
Во всех других задачах Perl чуток уступает по скорости выполнения и сильно проигрывает по памяти, что вполне объяснимо — Golang язык со строгой статической типизацией, а следовательно, должен потреблять меньше памяти.
Главные выводы из всего этого можно сделать такие:
-
изучать Golang после многолетнего использования Perl сложновато, но вполне по силам сисадминам;
-
Golang вполне способен заменить Perl как скриптовый язык.
ссылка на оригинал статьи https://habr.com/ru/articles/744380/
Добавить комментарий