Translate

2013-03-20

Forked processes and class destructors

Actually, it's universal advice for each language which provides facilities for OOP. Yesterday, I debugged issue appeared during unit-test run. Test just silently died without any verbose information about the reasons of such behavior. Lately, I discovered that test was killed by itself via TERM signal. But let me introduce a bit of pre-history.

Because of the lack of multi-threading support in DBI, as I described in previous post, I decided to move all internal processing machinery in our proprietary Perl module to multi-process model. Module is responsible for invoking a bunch of stored procedures on MySQL side in parallel fashion to utilize all computing power of hardware. There are no synchronization issues with locking in module because work is being spread among threads during creation stage and each thread does its part of work completely independently. Earlier, I had decided to use Perl threads but it was a flop (does somebody wish to bring multi-threading to DBI? I would be really grateful). I have Thread class in my module to represent a part of work which should be done in parallel. Firstly, you fill Thread instance with entities which represent different parts of work which should be performed in serial fashion. Then you invoke ->run method which returns instance of some kind of queue which is being used for synchronization and feedback in main thread/process. Everything is simple.

Troubles arose when I moved to using child processes internally in Thread class instead of Perl threads. The fact is that destructor was introduced in Thread class for finalizing communication pipe and killing child process. And from here, I would recommend to memorize following:

"Instances will be destroyed in each forked process during normal exit() routine."

If you deal with some system wide resources (named semaphores, processes) in your class internally (as I described above, I deal with child processes which I should sanitize appropriately during Thread destruction) you might to get into trouble.

"To avoid undesirable destructor invocation use _exit() routine"

I'm not sure if this routine is present everywhere but it is defined in POSIX. If we are talking about Perl, then you can get it from POSIX package. Something like POSIX::_exit. That's it.

2013-03-16

Blaming Perl and Ruby concurrency issues

I'm totally dissatisfied with modern state of concurrency support in dynamic languages:

About Ruby:

MRI Ruby Threads are native but interpreter itself has blocking nature. The list of really concurrent-powered Ruby implementations is pretty short: #Rubinius (#LLVM Ruby implementation), #JRuby (built on top of #Java VM with using of support for dynamic languages). Not sure about #MacRuby (built on #LLVM also). With MRI problem can be solved with using C binding with calculation-intensive work in combination with rb_thread_blocking_region Ruby C API method.

About Perl:

Perl has native threads inside its Linux port. Moreover, concurrency issues in it is solved with using threads::shared mechanics. threads::shared is pragma-module. Is we don't use threads::shared then all data will be entirely and deeply copied for each thread creation. If we will provide variable :shared attribute then this particular blessed variable's instance will be accessible from all threads. But there are several BUT's:

- GLOBs references (read file descriptors) cannot be shared. Perl will argue then if you've decided to assign shared member or member's element to non-shared object;
- it follows from statement above that entire object must be deeply "shared" (all members should have internal shared attribute);
- several widely-used packages (Perl DBI for example) have issues with concurrency because of internally maintained singleton objects; sometimes they are claimed as "blocking and thread-safe" but I would say "sometimes for particular runs they will be save" from what I've observed recently in one of my unit tests. I would advise to give a chance for sufficient concurrency and you will see the issues.

2013-03-01

File open( ... ) call in dynamic languages

Yesterday, I was digging issue in our program product (just to make things clearer, product is about network automation). Problem was in maintenance Perl script which performs heavyweight calculation and is being run periodically. It seemed like script lived persistently without any wish to finish their work and die appropriately. The rest of the issue investigation's story is commercially valuable information and can't be disclosed. Hopefully, it's irrelevant to the main subject of the post.

This issue wasn't reproduced on the rest of available test environments. Hence there was no chance to re-run this issue under the Perl debugger. The things were given as is. No CPU consumption, no massive IO wait, no swapping hell, database worked fast. Script was just hanged. I decided to drill alive Perl interpreter's process with strace at least to define if there is deadlock/livelock/waiting for resource. Here, I'd like to say that original problem was investigated successfully. What really matters is what I found during my investigation. Here is edited output from strace (with my humble comments):

~# strace -p 8888 ...// it's fine open("/tmp/f", O_RDONLY|O_LARGEFILE)    = 3 // what is it? ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0xbfa77468) = -1 ENOTTY (Inappropriate ioctl for device) ... // it's quite predictable close(3) ... ~#
What I was surprised by is how exactly Perl open function looks at the lowest level of userspace. What does ioctl( ... ) stand for here? Moreover, what does its flags mean in the context?

Just to compare behavior during opening file in another languages, I checked how Ruby works. Here is  the results:

~# ruby -e '1000000.times { File.open( "/tmp/f", "r" ) { |f| f.close; } }' &
[1] 8069
~# strace -p 8069
...
// it's OK; O_LARGEFILE makes sense also

open("/tmp/f", O_RDONLY|O_LARGEFILE)    = 5
// filling internal instance variables, I guess
fstat64(5, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
fstat64(5, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
// what is happening...?
ioctl(5, SNDCTL_TMR_TIMEBASE or TCGETS, 0xbf872138) = -1 ENOTTY (Inappropriate ioctl for device)
// OK; the rest is fine at least
close(5)

...

And I met it again. Since MRI Ruby source code is available on GitHub I decided to dig into File class implementation (Perl source could be chosen too but I'm used to neat UI features of GitHub). Here is what I found out with some inline comments:

Firstly, I tried to find instance initialize( ... ) method:

io.c: ...
void
Init_IO(void)
{
  //...
  Init_File();

  rb_define_method(rb_cFile, "initialize",  rb_file_initialize, -1);
  //...
}
...

Then:
io.c:
...
static VALUE
rb_file_initialize(int argc, VALUE *argv, VALUE io)
{
    if (RFILE(io)->fptr) {
    rb_raise(rb_eRuntimeError, "reinitializing File");
    }
    if (0 < argc && argc < 3) {
    VALUE fd = rb_check_convert_type(argv[0], T_FIXNUM, "Fixnum", "to_int");


    if (!NIL_P(fd)) {
        argv[0] = fd;
        return rb_io_initialize(argc, argv, io);
    }
    }
    rb_open_file(argc, argv, io);


    return io;
}
...

Then:

io.c:
...

static VALUE
rb_open_file(int argc, VALUE *argv, VALUE io)
{
    VALUE fname;
    int oflags, fmode;
    convconfig_t convconfig;
    mode_t perm;


    rb_scan_open_args(argc, argv, &fname, &oflags, &fmode, &convconfig, &perm);
    rb_file_open_generic(io, fname, oflags, fmode, &convconfig, perm);


    return io;
}
...

Then:

io.c:
...

static VALUE
rb_file_open_generic(VALUE io, VALUE filename, int oflags, int fmode, convconfig_t *convconfig, mode_t perm)
{
    rb_io_t *fptr;
    convconfig_t cc;
    if (!convconfig) {
    /* Set to default encodings */
    rb_io_ext_int_to_encs(NULL, NULL, &cc.enc, &cc.enc2);
        cc.ecflags = 0;
        cc.ecopts = Qnil;
        convconfig = &cc;
    }
    validate_enc_binmode(&fmode, convconfig->ecflags,
             convconfig->enc, convconfig->enc2);


    MakeOpenFile(io, fptr);
    fptr->mode = fmode;
    fptr->encs = *convconfig;
    fptr->pathv = rb_str_new_frozen(filename);
    fptr->fd = rb_sysopen(fptr->pathv, oflags, perm);
    io_check_tty(fptr); // Hmm...? Let's get back later...
    if (fmode & FMODE_SETENC_BY_BOM) io_set_encoding_by_bom(io);


    return io;
}
...

Then:

io.c:
...

static int
rb_sysopen(VALUE fname, int oflags, mode_t perm)
{
    int fd;
    struct sysopen_struct data;


    data.fname = rb_str_encode_ospath(fname);
    data.oflags = oflags;
    data.perm = perm;


    fd = rb_sysopen_internal(&data);
    if (fd < 0) {
    if (errno == EMFILE || errno == ENFILE) {
        rb_gc();
        fd = rb_sysopen_internal(&data);
    }
    if (fd < 0) {
        rb_sys_fail_path(fname);
    }
    }
    rb_update_max_fd(fd);
    return fd;
}
...

Then:

io.c:
...


static inline int
rb_sysopen_internal(struct sysopen_struct *data)
{
    int fd;
    fd = (int)rb_thread_blocking_region(sysopen_func, data, RUBY_UBF_IO, 0);
    if (0 <= fd)
        rb_update_max_fd(fd);
    return fd;
}


...

Huh. We need to go deeper... Then:

io.c:
...

static VALUE
sysopen_func(void *ptr)
{
    const struct sysopen_struct *data = ptr;
    const char *fname = RSTRING_PTR(data->fname);
    return (VALUE)open(fname, data->oflags, data->perm);
}


...

That's it. Here is final point in this branch. If I was regular expression in depth-first search mode I would backtrack now. Since I'm human I returned to the point where I saw another interesting call:


io.c:
...

static VALUE
rb_file_open_generic(VALUE io, VALUE filename, int oflags, int fmode, convconfig_t *convconfig, mode_t perm)
{
    rb_io_t *fptr;
    convconfig_t cc;
    if (!convconfig) {
    /* Set to default encodings */
    rb_io_ext_int_to_encs(NULL, NULL, &cc.enc, &cc.enc2);
        cc.ecflags = 0;
        cc.ecopts = Qnil;
        convconfig = &cc;
    }
    validate_enc_binmode(&fmode, convconfig->ecflags,
             convconfig->enc, convconfig->enc2);


    MakeOpenFile(io, fptr);
    fptr->mode = fmode;
    fptr->encs = *convconfig;
    fptr->pathv = rb_str_new_frozen(filename);
    fptr->fd = rb_sysopen(fptr->pathv, oflags, perm);
    io_check_tty(fptr); // Hmm...? Let's get back later...
    if (fmode & FMODE_SETENC_BY_BOM) io_set_encoding_by_bom(io);


    return io;
}
...

Then:

io.c:
...

static void
io_check_tty(rb_io_t *fptr)
{
    if (isatty(fptr->fd))
        fptr->mode |= FMODE_TTY|FMODE_DUPLEX;
}
...

Hmm. int isatty( int ) is POSIX function which tests whether passed descriptor is terminal or not. It looks like something at GLibc level... Hmm... Let's write brief test on pure C:


/tmp/test.c:

#include "~/include/stdio.h"

int main( void ) {
  for( int i = 0; i < 10000000; ++i )
    fclose( fopen( "/tmp/f", "r" ) );
}


Let's compile and run:

~$ gcc -std=c99 /tmp/test.c -o /tmp/test
~$ /tmp/test &
[1] 14939
~$ sudo strace -p 14939

...
// Nothing special...

open("/tmp/f", O_RDONLY)                = 3
close(3)                                = 0
open("/tmp/f", O_RDONLY)                = 3
close(3)                                = 0

...

OK. Then it's not about implementation of open( ... ) POSIX call in Glibc. Let's turn our sight to another player:

/tmp/test.c:

#define _POSIX_SOURCE
#include "~/include/stdio.h"
#include "~/include/unistd.h"

int main( void ) {
  FILE *f;
  for( int i = 0; i < 10000000; ++i ) {
    f = fopen( "/tmp/f", "r" );
    isatty( fileno( f ) );
    fclose( f );
  }
}

Let's test our opaque guessing that there is something in isatty( ... ):

buff@badtop:~$ gcc -std=c99 /tmp/test.c -o /tmp/test
buff@badtop:~$ /tmp/test &
[1] 15046
buff@badtop:~$ sudo strace -p 15032
...
open("/tmp/f", O_RDONLY)                = 3
// GOTCHA!
ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0xbfac3788) = -1 ENOTTY (Inappropriate ioctl for device)
close(3)                                = 0
open("/tmp/f", O_RDONLY)                = 3
ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0xbfac3788) = -1 ENOTTY (Inappropriate ioctl for device)
close(3)                                = 0
...

That's it. It was isatty(...). And this is how it's exactly implemented in Linux. Not sure about other UNIXes. I found via Google a noticeable amount of discussions at forums related to this topic. Authors guessed that there were some problems on the system level after they had investigated behavior of their scripts via strace tool. Just to stop these discussions I'd like to say: there is no problem. It is just like isatty( ... ) POSIX API call implemented in Glibc. In mentioned case, file is regular file and doesn't support terminal ioctls. But really wondering what sound-related SNDCTL_TMR_TIMEBASE ioctl stands for. Probably, we need to go deeper into Ulrich Drepper castle.

PS: Don't be surprised by strange #include directives. Angled brackets don't work. Probably, I should have to learn HTML basics before publishing.

2011-03-15

ReiserFS vs Ext4 - Ext4 win !

Ну что же. Вот и закончилось тестовое испытание на маленьком сервере для 25 машин файловой системы ext4.

Надо сказать в заданном test-case'е система показала выигрыш по ключевым характеристикам, важным для данного случая.

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

В общем падение НОДовского сервера (тот который большой и да, он упал) пришлось очень кстати. Сейчас можно разом переехать на ext4.

2011-03-11

ReiserFS vs Ext4 - посмотрим на схватку !

Недавно проявилась одна проблемка с файловой системой ReiserFS (не 4).

Я её выбрал, как универсальную файловую систему для всех задач когда-то давным-давно. И по умолчанию ставил на все подконтрольные мне машины (Debian Lenny/Squeeze).

Здесь следует, наверное, осветить выполняемые задачи.

Один сервер (Debian Lenny) работает, как сетевое NFS/CIFS/FTP хранилище. Количество клиентов ко всему этому делу - ~200 штук. Подключённые клиенты используют в основном CIFS (win-шара по-простому) и причём используется он достаточно интенсивно (работает старая DOS-ERP по схеме с общей шарой). Между делом он же является VPN (OpenVPN) шлюзом с NAT и NTP-сервером.

Второй сервер (Debian Lenny) примерно такой же, только количество клиентов ~25, причём с той же задачей (CIFS + DOS-ERP) только 5 штук. Остальные используют сетевые ресурсы периодически. Дополнительно, этот же сервер работает, как Ethernet-bridge (совсем немного правил), VPN (OpenVPN) шлюз с NAT, кэширующий proxy для системы пакетов APT (approx), сервер сетевой (DHCP+TFTP) загрузки, WEB-портал (Zabbix+OCSInventory) и NTP-сервер (вроде всё сказал).

Проблема начала проявлятся на первом из двух, когда я решил слить 5 сетевых CIFS-ресурсов, лежащих на отдельных разделах в единый МЕГА-раздел с сохранением этих самых логических ресурсов. Все разделы были отформатированы под ReiserFS. МЕГА-раздел тоже имеет форматирование ReiserFS.

И оно начало тормозить. Приблизительно я знаю в чём причина - градация размеров файлов в объединённом МЕГА-разделе варьируется от 1KB до 30-40GB. И файлов очень много ! Кроме того, существует много потоков чтения-записи к ней (в среднем 255 -256 процессов smbd в рабочий час-пик). Причём к этому хранилищу предъявляются достаточно жёсткие требования к сохранности информации (так что мультимедийная многопоточная и быстрая XFS не подходит).

Поэтому и решил сначала всё обкатать на втором сервере. Сначала из бекпортов Lenny поставил ядро 2.6.32. Затем выделил в LVM под это раздел. Отформатировал, как

mkfs -t ext4 -O extent /dev/_grp_/_dev_

Сейчас частично переместил некоторые директории со старого Reiser в целевой раздел. Буду монтировать поочередно. Потихонечку-полегонечку. Не торопясь, без азарта (за который я уже много раз расплачивался).

Цель - все логические ресурсы должны оказаться на МЕГА-разделе ext4.