技術は私たちの力。技術は私たちの楽しみ。 Creative Developer BLOG 技術部ブログ
Technology is our strength. Technology is what we enjoy.

AmazonLinux2023で最新のPerl(5.38)を動かしてみた話し

2023-11-13 勉強会
システム部の菊田です。

前回発表のAIによる自動アプリ生成機能が最速でリリースされました
https://prtimes.jp/main/html/rd/p/000000075.000048404.html
ありがたいことに、あちこちから注目いただき大きな反響をいただいております。

近年、サービスにAIが組み込まれるようになりはじめてからというもの、処理に時間がかかるリクエストが増えてきました。
せっかくサービスに対して、可能性を感じていただき、利用を開始してくださるお客様がいるのに、サーバーがダウンしてしまっては水の泡です。

便利な機能は、いつでも快適に利用できるからこそ、価値は継続するものだと思うので、あらためてインフラの性能をあげるべく、awsの最新のOSであるAmazon Linux2023に、最新バージョンのPerlをインストールして使う方法について取り組んでみました。

①Amazon Linux 2023での環境構築

すでにAmazon Linux 2023については、様々なサイトで特徴や構築にまつわるさまざな情報が公開されてますので、詳細は割愛しますが、今回は社内の勉強会なので、OSやディストリビューションの全体像がわかるようなイメージを作ってみました

ベースがFedoraベースなので、少々使えるコマンドが違う点がまず最初に直面する問題です

※GPTに協力してもらって、できるだけわかりやすい図を作ろうとしましたが、きっちり親子関係になってるわけでもないものも多く
(この影響を受けている、、、みたいなものが親子関係かというとそういうわけでもなかったり)
あくまでもイメージの共有という形で割り切りで

MySQLがない

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/USER_ConnectToInstance.html
AWSの公式が紹介しているのは、MySQLと互換性のあるMariaDBをインストールして使ってねということなのですが、後述の通り、現状データベースとの接続モジュールはMySQLを前提とした各種コマンドも多数存在しているため、これらをすべてMariaDBに対応させていくのも手間だったので、自前で入れることにしました
 
配布されてるバイナリには、

Amazon Linux用のものがないので(Amazon Linux2の時はExtra Packageがあったのでそちらで入れられた)、一見、Amazon Linux2023の元になってるFedora用のものがいけそうでしたが、欲してるライブラリが一致せずダメなようでした

RHEL用のものと互換性があるようなので、こちらで成功しました
(クラスメソッドさんの記事参考にさせていただきましたhttps://dev.classmethod.jp/articles/install-mysql-client-to-amazon-linux-2023/)

MySQLのバージョンアップ

最新のPerlのバージョンに対応した、DBD::mysqlは、MySQL 8.x代のクライアントライブラリを期待して動作しているため

Amazon Linux2023でMySQLに接続する時には、データベースのバージョンもそれにあわせる必用がありました
 Aurora2(MySQL5.7互換性)のサポート来年の10月には切れてしまうので、覚悟決めてバージョンアップもしていきましょう
 
MySQLのバージョンをあげた弊害ですが、今のところ、MySQLのデフォルトのキャラセットがutf8mb4になってるせいか、データベースがutf8のままだと、DBIの接続時にMalformed packetが出てしまうようでエラーを解消できなかったので、データベースをutf8mb4に変更し対策しました

wkhtmltopdfの問題

htmlで作ったページをPDFにしてくれるライブラリのwkhtmltopdfも活用させていただいてるのですが、こちらもAmazon Linux2023に対応したバイナリが配布されてません
このため、以下ビルドツールを取得してビルドしました

https://github.com/wkhtmltopdf/packaging
https://github.com/wkhtmltopdf/qt

READMEの通りで、今はdockerを使ってビルドするんですよね
ビルドに必用なもろもろのライブラリで既存環境が肥大化しないように、同じ環境のコンテナを立ち上げて、そこに大量のライブラリをインストールして
ビルド終わったらコンテナ捨てるってやり方が、とっても合理的だなーってあらためて感じました

指定するOSの指定は以下で見つけたのですが「amazonlinux2023-x86_64」とすることで期待通りにビルドできました
 https://github.com/wkhtmltopdf/packaging/commit/957cfe913a4f2ad14d7b0c272fb7897560ed308b

ちなみに実行コマンドはこんな形で、対象のOSと、最後に引数にwkhtmltopdfの本体のソースコードがあるパスを指定する形
./packaging/build package-docker amazonlinux2023-x86_64 /root/wkhtmltopdf/

ImageMagick問題

デフォルトでインストールできるImageMagickが、ImageMagick7になるですが、どうもcpanmでインストールするPerlのImageMagickは、ImageMagic6を前提にしたビルドになってるようで、コンパイルエラーになってしまう問題があったため、そちらとの整合性をあわせるためにImageMagick6.9.12.98を入れました

その他

あとは、他のサイトで記事になってる通り、rsyslogがデフォルトで無効になってるから、いつもの見慣れたログ(/var/log/messagesとか)がないので有効にしたり、なんやかんやした話しは割愛します

②最新の安定版のPerl5.38をインストール

Amazon Linux2023にインストールできるPerlは、5.32.1です。
Perl7がこのバージョンと互換性があると言われているので、ここに対応しておけば後々Perl7にも対応していけるとは思うのですが

5.32ももう、2年前なんですよね

なので、どうせやるなら、最も新しいPerlにしようってことで、5.38を入れることにしました。

Perlのバージョン切り替えについて

今後はこまめに新しいバージョンのPerlを試しやすくしていくために、バージョンを切り替えて管理できるようにしました
ユーザ単位で切り替えて検証したい時にはPlenvが良さそうだったのですが、サーバ全体で切り替えたい場合は、alternativesのほうが良さそうに思えたので、そちらを採用しました
必用なバージョンのPerlのソースコードを取得し、Configureでインストール先などを設定し、make install後、alternativesにもそのperlを優先順位をつけて登録します

curl -LO https://www.cpan.org/src/5.0/perl-5.38.0.tar.gz
tar -xzvf perl-5.38.0.tar.gz
cd perl-5.38.0
./Configure -des -Dprefix=/usr/local/perl-5.38.0
make
sudo make install
sudo alternatives --install /usr/bin/perl perl /usr/local/perl-5.38.0/bin/perl 300

curl -LO https://www.cpan.org/src/5.0/perl-5.32.0.tar.gz
tar -xzvf perl-5.32.0.tar.gz
cd perl-5.32.0
./Configure -des -Dprefix=/usr/local/perl-5.32.0
make
sudo make install
sudo alternatives --install /usr/bin/perl perl /usr/local/perl-5.32.0/bin/perl 10
そうすると、以下のような形で、どっちのPerlに切り替えるか操作できるようになります

cpanのインストールも以下ような形で、バージョンごとにインストール先を変えてインストールしていきました
cpanm --reinstall -L /usr/local/perl-5.38.0/lib/site_perl/5.38.0 DBI

バージョンアップに伴う弊害

細かな影響まですべて確認できていないのですが、5.26のアップデートで一番影響が大きかった、カレントディレクトリがパスから削除されてしまった問題に比べると今回は、MySQLとの通信まわり以外は、そう大きな混乱なく、表示することができました
Perlは後方互換性が高いと言われてますが、正直、ここまで簡単にバージョンアップ対応できたのは今まで経験がないので、びっくりでした

③Perlの新機能や、試してみたかった機能の検証

Perl5.38の新機能

まだ実験的な機能なので、使う時にはuse feature が必用になりますが、class構文が追加になりました

#!/usr/bin/perl
use feature qw(class try);
no warnings qw(experimental::class experimental::try);
#----------------------------------
# 従業員クラス
class Employee {
field $id :param;
field $name :param;
field $age :param;
# カスタムコンストラクタ
# 直後に呼ばれる。引数とかのチェックとか、初期化処理とか、コンストラクタのカスタマイズしたい時はここで制御する";
ADJUST {
die "Invalid employee ID format\n" unless $id =~ /^[A-Z0-9]+$/;

}
method get_id() {
return $id;
}
method get_name() {
return $name;
}
method set_name($new_name) {
$name = $new_name;
}
method get_age() {
return $age;
}
method set_age($new_age) {
$age = $new_age;
}
}

#----------------------------------
# マネージャークラス
class Manager :isa(Employee) {
field $department :param; # 部署名
}

package main;
try{
my $employee = Employee->new(id=>"12345",name=>"Alice",age=>30);
print "-----------Employee----------------\n";
print "ID: ", $employee->get_id(), "\n";
print "Name: ", $employee->get_name(), "\n";

# 従業員の名前を変更
$employee->set_name("Bob");
print "New Name: ", $employee->get_name(), "\n";
my $manager = Manager->new(department=>'system',id=>"11111",name=>"MG",age=>50);
print "-----------Manager----------------\n";
print "ID: ", $manager ->get_id(), "\n";
print "Name: ", $manager ->get_name(), "\n";
} catch ($e) {
print $e."\n";
}
以下のような従来の書き方よりも直感的に書けるようになれました

#!/usr/bin/perl
#----------------------------------
# 従業員クラス
package Employee;
{
sub new {
my ($class, %args) = @_;
die "Invalid employee ID format\n" unless $args{id} =~ /^[A-Z0-9]+$/;
my $self = {
id => $args{id},
name => $args{name},
age => $args{age},
};
return bless $self, $class;
}
sub get_id {
my $self = shift;
return $self->{id};
}
sub get_name {
my $self = shift;
return $self->{name};
}
sub set_name {
my ($self, $new_name) = @_;
$self->{name} = $new_name;
}
sub get_age {
my $self = shift;
return $self->{age};
}
sub set_age {
my ($self, $new_age) = @_;
$self->{age} = $new_age;
}
}

#----------------------------------
# マネージャークラス
package Manager;
{
use base qw(Employee);
sub new {
my ($class, %args) = @_;
my $self = $class->SUPER::new(%args);
$self->{department} = $args{department};
return $self;
}
}

package main;
eval{
my $employee = Employee->new(id=>"12345",name=>"Alice",age=>30);
print "-----------Employee----------------\n";
print "ID: ", $employee->get_id(), "\n";
print "Name: ", $employee->get_name(), "\n";
$employee->set_name("Bob");
print "New Name: ", $employee->get_name(), "\n";

my $manager = Manager->new(department=>'system',id=>"11111",name=>"MG",age=>50);
print "-----------Manager----------------\n";
print "ID: ", $manager ->get_id(), "\n";
print "Name: ", $manager ->get_name(), "\n";
};
if ($@) {
print $@."\n";
}
Perlの初学者が苦しむ点として、こういう独自構文なクラス構文が長年あったと思うので、こういう一般的な構文が使えるようになる点はとても好ましいと思います
  
次に、無名サブルーチンまわりの処理速度があがったと記載があったので、10万個の無名サブルーチンを生成する簡易プログラムでベンチマークを取ってみたところ良い数値が出てました

PSGIの検証

CGIは、Common Gateway Interfaceの略称で、Webサーバが外部プログラム(PerlやシェルやPythonなどでも)を呼び出す時の標準化された規格です。

CGIモードで動作する場合、Webサーバでは各リクエスト毎に新しいプロセスが生成されるので、プロセス間のデータの競合を意識しなくても良い利点はあるのですが、リクエストごとにプロセスが立ち上がってしまうので、大量のアクセスを捌けないなど、サーバリソースが逼迫しやすい問題があります

長らくこういう、Webサーバ自身にプログラムの仕事を一緒にさせる方式が取られていて、より高速化させるために、FastCGIや、mod_perlなど、Webサーバのモジュールとして、より高速化するための手法が取られてきました。

その後、2009年くらいから、WSGIなどに触発され、PerlをWebサーバから分離させて動かす、PSGI(Perl Web Server Gateway Interface)という方法が登場してきました。

この方法により、Webサーバは、プログラムの実行から解放され、PSGIで動作するアプリケーションサーバとの仲介だけの専念できるようになれるため、マルチスレッドでリクエストを処理できるようになれます。

なかなか難しい概念なので、GPTと相談して、ランチタイムの定食屋さんに来るお客さんと受け皿となるテーブルで例えてみました

WebサーバがCGIを動かそうとすると、このような形で、混みあってる時でも、座席に1人しか通さない形になって、店内の座席を有効活用できない状態になります。
ただし、プライベートな空間を持てるようになれるので、スマホで操作してる画面の内容や、PCを開いて社外秘の情報なども気にせず過ごすことができます

次にCGIを動かさず、マルチスレッドで動かす場合です。
こちらは空いてるスペースにどんどん人を入れていくので、スペースを有効活用できます。
そのかわり、1人1人の空間は限られているので、個人情報などまわりに見られないよう、配慮が必要です。

今回の勉強会では、GPTの画像生成DALL-Eとの対話も楽しい時間でした

では、早速PSGIの応答速度を検証してみます

Starman

cpanでStarmanをインストールするとインストール先に実行ファイルができるので、こちらから起動する形でした

5000番のポートで立ち上げておき、apacheのリバースプロキシでcgiのリクエストの時のみプロキシされるようにしてます

cpanm --reinstall -L /usr/local/perl-5.38.0/lib/site_perl/5.38.0 Starman

/usr/local/perl-5.38.0/lib/site_perl/5.38.0/bin/starman --listen :5000 performance_test.psgi
検証用に作成したプログラムは以下の通りで

performance_test.psgi


use strict;
use warnings;
use Plack::Request;
use File::Temp qw/ tempfile /;
use Time::HiRes qw( gettimeofday tv_interval );

my $app = sub {
my $env = shift;
my $request = Plack::Request->new($env);

my $start_time = [gettimeofday];

# ループ処理のための変数を宣言
my $total = 0;
for (my $i = 0; $i < 10000; $i++) {
$total += $i;
}

# ファイル書き込みと読み取り
my ($fh, $filename) = tempfile();
print $fh "Some data";
seek $fh, 0, 0;
my $file_content = <$fh>;
close $fh;

# メモリ使用量の計測
my $memory_usage = `ps -o rss= -p $$`;

my $end_time = [gettimeofday];
my $elapsed = tv_interval($start_time, $end_time);

return [ 200, ['Content-Type' => 'text/plain'],
["Loop total: $total\nFile content: $file_content\nMemory Usage: ${memory_usage}KB\nElapsed time: ${elapsed}s"] ];
};

$app;
ループ処理やファイル書き込み処理などいくつか処理に時間がかかりそうなものを書いたプログラムで検証してみます。

performance_test.cgi


#!/usr/bin/perl

use strict;
use warnings;
use CGI;
use File::Temp qw/ tempfile /;
use Time::HiRes qw( gettimeofday tv_interval );

my $q = CGI->new;
print $q->header('text/plain');

my $start_time = [gettimeofday];

# ループ処理の変数を宣言
my $total = 0;
for (my $i = 0; $i < 10000; $i++) {
$total += $i;
}

# ファイル書き込みと読み取り
my ($fh, $filename) = tempfile();
print $fh "Some data";
seek $fh, 0, 0;
my $file_content = <$fh>;
close $fh;

# メモリ使用量の計測
my $memory_usage = `ps -o rss= -p $$`;

my $end_time = [gettimeofday];
my $elapsed = tv_interval($start_time, $end_time);

print "Loop total: $total\n";
print "File content: $file_content\n";
print "Memory Usage: ${memory_usage}KB\n";
print "Elapsed time: ${elapsed}s";

負荷テスト

本当はJmetarでcgi以外のリクエスト(画像、js、css)も含めた全リクエストを模倣して性能を検証したかったのですが、すみません、ちょっと時間切れになってしまったので、Apache Benchで簡易的に負荷かけてみます

検証用のサーバがt3.microでスペック低かったので、10人のユーザが10ページづつサイトを閲覧し、合計100リクエストになる程度の負荷をかけてみた場合の比較とします

ab -n 100 -c 10 https://xxxx.xxx.xxx/

psgi環境の結果


$ ab -n 100 -c 10 https://xxxx.xxx.xx/index.cgi
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxx.xxx.xx (be patient).....done


Server Software: Apache/2.4.56
Server Hostname: xxxx.xxx.xx
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384,2048,256
Server Temp Key: X25519 253 bits
TLS Server Name: xxxx.xxx.xx

Document Path: /index.cgi
Document Length: 91 bytes

Concurrency Level: 10
Time taken for tests: 3.232 seconds
Complete requests: 100
Failed requests: 9
(Connect: 0, Receive: 0, Length: 9, Exceptions: 0)
Total transferred: 24391 bytes
HTML transferred: 9091 bytes
Requests per second: 30.94 [#/sec] (mean)
Time per request: 323.156 [ms] (mean)
Time per request: 32.316 [ms] (mean, across all concurrent requests)
Transfer rate: 7.37 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 91 197 144.7 130 806
Processing: 36 60 32.5 52 287
Waiting: 36 58 23.2 51 155
Total: 133 258 142.8 195 846

Percentage of the requests served within a certain time (ms)
50% 195
66% 258
75% 299
80% 357
90% 438
95% 566
98% 842
99% 846
100% 846 (longest request)
合計テスト時間: 3.232秒
リクエストあたりの平均時間: 323.156ミリ秒
コンカレンシーあたりの平均時間: 32.316ミリ秒
1秒あたりのリクエスト数: 30.94
失敗したリクエスト: 9

cgi環境の結果


kikuta@kikuta:~$ ab -n 100 -c 10 https://xxxx.xxx.xx/performance_test.cgi
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking xxxx.xxx.xx (be patient).....done


Server Software: Apache/2.4.56
Server Hostname: xxxx.xxx.xx
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384,2048,256
Server Temp Key: X25519 253 bits
TLS Server Name: xxxx.xxx.xx

Document Path: /performance_test.cgi
Document Length: 91 bytes

Concurrency Level: 10
Time taken for tests: 6.886 seconds
Complete requests: 100
Failed requests: 13
(Connect: 0, Receive: 0, Length: 13, Exceptions: 0)
Total transferred: 26399 bytes
HTML transferred: 9099 bytes
Requests per second: 14.52 [#/sec] (mean)
Time per request: 688.597 [ms] (mean)
Time per request: 68.860 [ms] (mean, across all concurrent requests)
Transfer rate: 3.74 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 107 249 261.9 138 1321
Processing: 111 307 91.1 315 506
Waiting: 92 267 78.0 271 506
Total: 248 557 247.9 486 1552

Percentage of the requests served within a certain time (ms)
50% 486
66% 559
75% 618
80% 630
90% 749
95% 1291
98% 1539
99% 1552
100% 1552 (longest request)
合計テスト時間: 6.886秒
リクエストあたりの平均時間: 688.597ミリ秒
コンカレンシーあたりの平均時間: 68.860ミリ秒
1秒あたりのリクエスト数: 14.52
失敗したリクエスト: 13

検証結果

パフォーマンス: PSGI環境はCGI環境に比べて、リクエスト処理が約2倍速いことがわかりました
安定性: 両環境ともに失敗したリクエストがありましたが、PSGI環境での失敗数が少ないことから、PSGIの方がやや安定していると言えます
負荷耐性: PSGI環境は、より多くのリクエストを短い時間で処理できるため、高負荷状態に対する耐性が高いと言えます

PSGIの有用性は確認できましたが、プロセスが使いまわされる点で、グローバル変数を使ってしまっていたり、mainパッケージが明示的についてないようなコードの場合には意図した動作にならない問題もあったので、どんなコードでもそのままPSGI化して動く、、、ような単純なものではありませんでした(Plack::App::WrapCGIでそのまま動いたらいいなーくらいの安易な検証しかしてないですが)

ただ、有用性はとても感じているので、引き続き活用方法を検証していきたいと思います!

総評

Perlのことを深く知ろうとすると、古い情報から追ってく必用があって、今回、故きを温ねて新しきを知るという言葉の意味を知った気がました。
作者のラリーウォールさんがどういう設計思想でこの言語を開発し、その後、さまざまな技術的な制約を乗り越えて、ユーザに利便性をもたらし
新しい言語の台頭で人気が下火になった今も、世界中には、200を超える地域グループが存在し、日本でも毎年カンファレンスが開かれていて
今もなおPerlを信仰する熱い思いを持った方々がたくさんいる現実もしりました
自分がこれまで出たってきた、Perl好きのエンジニアの方は、本当にみなさん、Perlをこよなく愛してる方ばかりそういう方々によって支えられてるのだなーと感じました
5.38のバージョンではおよそ100名のエンジニアの方が29万行の変更を1年かけて行っていただいたようで、あらためて自分たちのビジネスはこういうコミュニティの努力によって支えられてる感じ、近い将来、恩返しをしていかなければならないと思いました。
 
バージョンアップ対応(リファクタリングなども)は、見た目のふるまいが特にかわるわけでもないのに、大がかりな開発が必要になることも多く、なかなかコストをかけることに躊躇してしまうことも多いものですが、かけなかったコストは、技術的負債という形で借金を追うことになるので、計画的に覚悟をもってこのタスクを遂行していこうと思います
記事一覧へ