Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fpm binding master and children processes to specific core(s). #10075

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

devnexen
Copy link
Member

@devnexen devnexen commented Dec 9, 2022

rational is there is git snippets and similar floating around about how to achieve this via command line.
So idea is to do it "natively" especially those above mostly focus on linux whereas more platforms are cpu affinity capable. For now at least, focusing on simple cases, one core id or a range.

@devnexen devnexen force-pushed the fpm_cpu_affinity branch 2 times, most recently from 557a88a to f55fb44 Compare December 10, 2022 15:29
@devnexen devnexen changed the title [wip] fpm binding master and children processes to specific core(s). fpm binding master and children processes to specific core(s). Dec 10, 2022
@devnexen devnexen marked this pull request as ready for review December 10, 2022 16:09
sapi/fpm/www.conf.in Outdated Show resolved Hide resolved
@iluuu1994
Copy link
Member

@devnexen Thank you! I'll have a look soon. 🙂

@devnexen devnexen force-pushed the fpm_cpu_affinity branch 2 times, most recently from 3ff190e to a0be7ff Compare December 16, 2022 16:02
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder generally whether the main use-case here would be to pin specific workers to specific CPUs, rather than pinning all workers to a range of CPUs. I've never actually used setaffinity so I'm not sure if that makes a big difference or if the scheduler is smart enough to avoid these context switches.

sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/php-fpm.conf.in Outdated Show resolved Hide resolved
sapi/fpm/tests/cpuaffinity.phpt Show resolved Hide resolved
<?php include "skipif.inc";

if (!str_contains(PHP_OS, 'Linux')) {
die('skipped supported only on Linux');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FreeBSD supports cpuset_setaffinity, is there any reason not to test this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah why is this limited to Linux only if cpuset_setaffinity is already implemented?

sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
cpu_set_t cset;
#endif
long min;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find these slightly misleading, as they are only used as temporary values for fpm_cpuaffinity_add. I'd prefer if min/max were parameters to fpm_cpuaffinity_add but it's not a big deal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah agree, probably not much point to define them in struct as it can be just param. Also agree that it's not a big deal though :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I will rephrase it. Please can you change this too.

sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
Copy link
Member

@bukka bukka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea makes sense in general. Added some comments. The ones that stand out for me are following 2 issue.

As I mentioned in one of the the comments I don't like the configuration as it seems very cryptic. I think more declarative approach (potentially introducing more options) would be less error prone and easier to understand IMO.

Support for containers (Docker) is necessary as this would likely be reported as a bug later if it doesn't work correctly. IIRC _SC_NPROCESSORS_ONLN reports number of processors in the host rather than what the container is limited to so this might need to be addressed if that's the case.

configure.ac Outdated Show resolved Hide resolved
@@ -349,6 +358,102 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */
}
/* }}} */

#if HAVE_FPM_CPUAFFINITY
struct fpm_cpuaffinity_conf {
#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check seems useless as it is covered by HAVE_FPM_CPUAFFINITY

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea is to extend this with different checks that are platform specific.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well it should be then updated or if it's not ready for review yet, then it should be probably a draft PR. I'd rather not to merge anything that has got some part with "maybe in the future"... :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment still stands.

sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/php-fpm.conf.in Outdated Show resolved Hide resolved
sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
sapi/fpm/fpm/fpm_conf.c Outdated Show resolved Hide resolved
@bukka
Copy link
Member

bukka commented Dec 22, 2022

@iluuu1994

I wonder generally whether the main use-case here would be to pin specific workers to specific CPUs, rather than pinning all workers to a range of CPUs

It sets affinity to all workers (children) in the pool. Pools are the way how the workers are differentiated in FPM. So if you need to select only specific ones, then you need multiple pools.

@devnexen
Copy link
Member Author

devnexen commented Dec 22, 2022

It sets affinity to all workers (children) in the pool.

master process and/or workers (if you do not set worker's, it will inherit master's setting).

@bukka
Copy link
Member

bukka commented Dec 23, 2022

It sets affinity to all workers (children) in the pool.

master process and/or workers (if you do not set worker's, it will inherit master's setting).

Ok finally got it - I initially thought that there's just pool config but now I see there's global config for master as well.

; Default Value: inherits master's cpu affinity
; process.cpumask = "8"
; process.cpumask = "0-3"
; process.cpumask = "4-7;10-12"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this format is chosen, it needs more description about the format.

@devnexen
Copy link
Member Author

@bukka ping :)

@bukka
Copy link
Member

bukka commented Jan 14, 2023

@devnexen I just had a look and CPU_COUNT certainly looks like a better solution. Might be better to just error if sched_getaffinity fails (returns -1) rather than trying to use sysconf though. Otherwise there are still some unresolved comments so please go through them and either fix them or replay so they all get resolved. Also the FreeBSD failure is related. Once it's all green and comments resolved, I will give a try to see if it all works as expected for me too. 😉

@devnexen devnexen force-pushed the fpm_cpu_affinity branch 2 times, most recently from 0abc26c to 64b8306 Compare January 14, 2023 22:03
@devnexen
Copy link
Member Author

@devnexen I just had a look and CPU_COUNT certainly looks like a better solution. Might be better to just error if sched_getaffinity fails (returns -1) rather than trying to use sysconf though. Otherwise there are still some unresolved comments so please go through them and either fix them or replay so they all get resolved. Also the FreeBSD failure is related. Once it's all green and comments resolved, I will give a try to see if it all works as expected for me too. 😉

Think I ve addressed your concerns now :-) cheers

@bukka
Copy link
Member

bukka commented Jan 20, 2023

Cool I will take a proper look early in Feb

Copy link
Member

@bukka bukka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still needs a bit of work

Comment on lines 132 to 137
; the master process could be bound to a single core
; process.cpu_list = "4"
; a range from the minimum cpuid to the max required
; process.cpu_list = "0-3"
; or multiple ranges separated by a comma
; process.cpu_list = "1-4,8-16,32-48"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to have the format docs before the value rather than actual example. See other entries to get some inspiration. For example

; Valid syntaxes are:
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
; a specific port;
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
; a specific port;
; 'port' - to listen on a TCP socket to all addresses
; (IPv6 and IPv4-mapped) on a specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
.


static void fpm_cpuaffinity_init(struct fpm_cpuaffinity_conf *c)
{
#if defined(HAVE_FPM_CPUAFFINITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes it is last reminiscence of "further porting to other platforms" thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so it needs to get removed...


static void fpm_cpuaffinity_add(struct fpm_cpuaffinity_conf *c)
{
#if defined(HAVE_FPM_CPUAFFINITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still stands...

sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
<?php include "skipif.inc";

if (!str_contains(PHP_OS, 'Linux')) {
die('skipped supported only on Linux');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah why is this limited to Linux only if cpuset_setaffinity is already implemented?

Comment on lines 87 to 92
; the pool process could be bound to a single core
; process.cpu_list = "8"
; a range from the minimum cpuid to the max required
; process.cpu_list = "0-3"
; or multiple ranges separated by a comma
; process.cpu_list = "4-7,10-12"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above comment for global config - better description would be useful...

sapi/fpm/fpm/fpm_unix.c Outdated Show resolved Hide resolved
@@ -349,6 +358,102 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */
}
/* }}} */

#if HAVE_FPM_CPUAFFINITY
struct fpm_cpuaffinity_conf {
#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment still stands.

#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
cpu_set_t cset;
#endif
long min;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah agree, probably not much point to define them in struct as it can be just param. Also agree that it's not a big deal though :)

@bukka
Copy link
Member

bukka commented Feb 5, 2023

@devnexen There are still bunch of comments. Please comment on or resolve them once you address them. It will be then clearer what needs to be done. I resolved all those that were done and added some other comments.

@devnexen devnexen force-pushed the fpm_cpu_affinity branch 2 times, most recently from 0f4bb85 to 840c0d2 Compare February 6, 2023 09:54
@devnexen
Copy link
Member Author

devnexen commented Feb 6, 2023

@devnexen There are still bunch of comments. Please comment on or resolve them once you address them. It will be then clearer what needs to be done. I resolved all those that were done and added some other comments.

I think I have addressed those, unluckily, vm builders fail at the moment but at least you can try locally the multi range part :-)

Copy link
Member

@bukka bukka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more comments

pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 1
process.cpu_list = 0-1,2-3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe change it to 0,2-3 as it will make more sense...

Comment on lines 6 to 12
if (!str_contains(PHP_OS, 'Linux') && !str_contains(PHP_OS, 'FreeBSD')) {
die('skipped supported only on Linux and FreeBSD');
}

if (str_contains(PHP_OS, 'Linux')) {
$cmd = 'nproc';
} else {
$cmd = 'sysctl hw.ncpus';
}

$nproc = intval(exec($cmd));
if ($nproc < 4) {
die('skipped supported only on 4 cores or more environments');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you integrate those independently to the Tester (static methods) as done elsewhere...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point.

Comment on lines 133 to 139
; process.cpu_list = "cpu id" - to bind to a single core
; process.cpu_list = "min cpu id-max cpu id" - to bind to a core range
from minimum cpu id to max
; process.cpu_list = "[min cpu id-max cpu id],[min cpu id-max cpu id],..."
- to bind to multiple ranges
separated by a comma
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually very confusing description and a bit incorrect. Could you maybe use BNF to describe and keep some examples. I didn't mean to replace the examples completely but to add some generic description.

@@ -416,6 +521,15 @@ int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */
zlog(ZLOG_SYSERROR, "[pool %s] failed to set rlimit_core for this pool. Please check your system limits or decrease rlimit_core. setrlimit(RLIMIT_CORE, %d)", wp->config->name, wp->config->rlimit_core);
}
}
#if HAVE_FPM_CPUAFFINITY
if (wp->config->process_cpu_list) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why extra nesting here. Just use single condition.


static void fpm_cpuaffinity_add(struct fpm_cpuaffinity_conf *c)
{
#if defined(HAVE_FPM_CPUAFFINITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still stands...

#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
cpu_set_t cset;
#endif
long min;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I will rephrase it. Please can you change this too.

Comment on lines 87 to 93
; Valid syntaxes are:
; process.cpu_list = "cpu id" - to bind to a single core
; process.cpu_list = "min cpu id-max cpu id" - to bind to a core range
from minimum cpu id to max
; process.cpu_list = "[min cpu id-max cpu id],[min cpu id-max cpu id],..."
- to bind to multiple ranges
separated by a comma
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@devnexen
Copy link
Member Author

devnexen commented Feb 9, 2023

Added more comments

Thanks @bukka I did some changes let me know if I forgot something.

@devnexen
Copy link
Member Author

devnexen commented Apr 5, 2023

ping @bukka when you have a moment. 🙂

@bukka
Copy link
Member

bukka commented Apr 16, 2023

I will try to find some time for this in May. Already used almost all my FPM time for this month.

Copy link
Member

@bukka bukka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have done some testing and it contains bugs. The parser needs be fixed as the current implementation contain errors which are not that easily visible - needed some separated testing for that. It would be probably good if we had some unit testing framework (e.g. cmocka) that we could use for things like this.

Comment on lines 41 to 43
#endif

#ifdef HAVE_SYS_CPUSET_H
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: this can be elif I guess...


fpm_cpuaffinity_add(&c, min, max);

token = php_strtok_r(NULL, ";", &buf);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be comma otherwise it considers only two elements. You should add some test with two commas to test this...

while (token) {
char *cpu_listsep;

min = strtol(token, &cpu_listsep, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is too forgiving and allows latters in the string. For example abbbbb123,1 results in min and max 0 for the first element and 1 for min max second element. Obviously this should be error. I converted this to a small program:

#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

char *php_strtok_r(char *s, const char *delim, char **last)
{
    char *spanp;
    int c, sc;
    char *tok;

    if (s == NULL && (s = *last) == NULL) {
		return NULL;
    }

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0; ) {
		if (c == sc) {
			goto cont;
		}
    }

    if (c == 0)		/* no non-delimiter characters */ {
		*last = NULL;
		return NULL;
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;)
    {
		c = *s++;
		spanp = (char *)delim;
		do {
			if ((sc = *spanp++) == c) {
				if (c == 0) {
					s = NULL;
				} else {
					char *w = s - 1;
					*w = '\0';
				}
				*last = s;
				return tok;
			}
		}
		while (sc != 0);
    }
    /* NOTREACHED */
}

int parse(const char *cpu_list_const, int cpumax)
{
    char *token, *buf;
	int min, max;

	int rc = 0;

    char *cpu_list = strdup(cpu_list_const);

    printf("\nparsing %s\n", cpu_list);

	token = php_strtok_r(cpu_list, ",", &buf);

	while (token) {
		char *cpu_listsep;

		min = strtol(token, &cpu_listsep, 0);
		if (errno || min < 0 || min > cpumax) {
			rc = -1;
		}
		max = min;
		if (*cpu_listsep == '-') {
			if (strlen(cpu_listsep) > 1) {
				char *err;
				max = strtol(cpu_listsep + 1, &err, 0);
				if (errno || *err != '\0' || max < min || max > cpumax) {
                    rc = -1;
                    goto end;
				}
			} else {
				rc = -1;
                goto end;
			}
		}

		printf("min: %d, max: %d\n", min, max);

		token = php_strtok_r(NULL, ",", &buf);

    }

end:
    printf("result: %d, cpu list: %s\n", rc, cpu_list);
    free(cpu_list);

	return rc;

}

int main()
{
    parse("1,2,5", 10);
    parse("1-2", 4);
    parse("1-2,4-5,7-10", 12);
    // error cases
    parse("1-8", 4);
    parse("abbbbb123,1", 10);
    parse("123afafkalf,1ffff", 10);
}

which results to

parsing 1,2,5
min: 1, max: 1
min: 2, max: 2
min: 5, max: 5
result: 0, cpu list: 1

parsing 1-2
min: 1, max: 2
result: 0, cpu list: 1-2

parsing 1-2,4-5,7-10
min: 1, max: 2
min: 4, max: 5
min: 7, max: 10
result: 0, cpu list: 1-2

parsing 1-8
result: -1, cpu list: 1-8

parsing abbbbb123,1
min: 0, max: 0
min: 1, max: 1
result: 0, cpu list: abbbbb123

parsing 123afafkalf,1ffff
min: 123, max: 123
min: 1, max: 1
result: -1, cpu list: 123afafkalf

This needs to be fixed.

cpumax = fpm_cpumax();

fpm_cpuaffinity_init(&c);
token = php_strtok_r(cpu_list, ",", &buf);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that php_strtok_r modifies the value so you are changing the configured value in the config which should not be done. You should copy the list to a new buffer (either on heap allocated or using alloca) before calling this.

rational is there is git snippets and similar floating around about how
to achieve this via command line.
So the idea is to do it "natively" especially those above mostly
focus on linux whereas more platforms are cpu affinity "capable".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants