golang

Инструменты сисадмина: Perl и Golang

  • пятница, 30 июня 2023 г. в 00:00:17
https://habr.com/ru/articles/744380/

В статье отражен опыт применения языков Perl и Golang в повседневной работе бородатого сисадмина в качестве скриптового языка и показаны примеры использования.

Начало времен

Когда-то давно в молодости я выбирал инструмент, который помог бы мне автоматизировать ручной труд, а именно распарсить лог или конфиг, протестировать коннект к базе данных, собрать ответы от сайтов и т.д. И я выбрал Perl. Он до сих пор является палочкой-выручалочкой. Внятное объяснение этому можно найти в данной заметке, из которой я приведу лишь главные причины актуальности Perl:

  • Он везде установлен по умолчанию, при этом удобен для быстрых сценариев и адаптируется к новым парадигмам.

  • Я могу быть уверен, что сценарий Perl, который я пишу сегодня, будет работать без изменений через 10 лет.

Любой уважающий себя сисадмин должен быть с программистским уклоном. Мне всегда было интересно писать всякого рода прокси, а также скрипты синхронизации баз данных, мониторинга, бэкапа и т.д. Для этого на CPAN можно найти кучу примеров кода с отличной документацией, которая, по моему мнению, является лучшим примером оформления и представления документации к коду.

Что-то новенькое

И вот недавно меня попросили написать скрипт (точнее я сам напросился :-)), который дергает некое api по http и результат (json) складывает в noSQL базу данных, при этом главным условием было то, что нельзя писать на Perl, т.к. в команде программистов никто его не знает. Тогда я предложил написать на Golang, ведь, по моему мнению, именно этот современный язык заслуживает внимания сисадминов. До этого момента я никогда не писал на языках со статической типизацией, да и образование у меня не программистское, но тем интересней мне показалась задача.

Так как Golang для меня новый язык, то прежде чем браться за работу (времени у меня было предостаточно) я решил напиcать скрипты на Perl и Golang для трех распространенных задач и одной не очень распространенной, тем самым сравнив некоторые моменты: скорость написания скриптов, время выполнения, потребление памяти. Вот список задач, который я собрал для примеров кода:

  1. Найти 500-е коды ответов в access.log размером ~1G

  2. Сделать выборку из sql базы данных (в таблице 12200 строк)

  3. Узнать дату выдачи и дату окончания действия ssl сертификата сайта www.example.com

  4. По особенному распарсить 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
user 0m15,758s
sys 0m0,373s

real 0m24,357s
user 0m24,375s
sys 0m0,877s

Потребление памяти (in kilobytes)

6400 perl ./script.pl
6400 perl ./script.pl
6400 perl ./script.pl

9608 ./script
9820 ./script
9540 ./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
user 0m0,071s
sys 0m0,032s

real 0m2,790s
user 0m0,030s
sys 0m0,044s

Потребление памяти (in kilobytes)

15232 perl ./script.pl
15872 perl ./script.pl
16768 perl ./script.pl

6896 ./script
9036 ./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
user 0m0,051s
sys 0m0,012s

real 0m0,441s
user 0m0,002s
sys 0m0,004s

Потребление памяти (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 как скриптовый язык.