Инструменты сисадмина: Perl и Golang
- пятница, 30 июня 2023 г. в 00:00:17
В статье отражен опыт применения языков 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
), делающая замер потребления памяти (первая колонка) три раза, вывод которой так же будет показан после примеров кода.
#!/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;
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 |
#!/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";
}
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 |
#!/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
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 |
Эта задача родилась уже после того, как я написал тот самый скрипт, который меня попросили. Дело тут в том, что Perl просто берет json строку и парсит ее целиком в свои структуры (массивы, хеши) и дальше ты работаешь уже с ними. В Golang все немного сложнее, прежде чем парсить нужно самому описать весь json (все объекты!) в типах, с которыми дальше удобно работать. Вот тут есть статья, описывающая нюансы парсинга json в Golang, прочитав которую можно понять весь масштаб трагедии для человека писавшего всю жизнь на языке с динамическими типами. Хорошо, что в моем скрипте мне не надо было глубоко парсить json, а хватило лишь разбить верхний json массив на строки, которые представляют json объект. В приведенных ниже скриптах показан пример такого подхода, сначала на Golang, а уже потом, ради интереса, повторенный на Perl, поэтому сначала представлен скрипт на Golang, а уже потом на Perl, в отличие от других задач, в которых последовательность написания скриптов была другой.
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"}
#!/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 как скриптовый язык.