APC - Alternative PHP Cache
Description
APC is a free, open, and robust framework for caching and
optimizing PHP intermediate code
Außer Code kann das Ding auch noch sonstige Inhalte im sogenannten user cache speichern - da kann man sie mit apc_store und apc_add speichern sowie mit apc_fetch holen:
Unlike many other mechanisms in PHP, variables stored using
apc_store() will persist between requests (until the value is
removed from the cache).
(es gibt auch noch TTLs).
Ich hatte vor ein paar Monaten mal den Fehler gemacht, obiges Produkt zu verwenden. Um es vorweg zu nehmen: Das muß in einem Anfall geistiger Umnachtung erfolgt sein.
Auf die Idee gekommen bin ich, weil ich die Ergebnisse eines Haufens Datenbankabfragen gerne ich einem vom mehreren PHP-Prozessen gemeinsam genutzten Cache unterbringen wollte [1]. Der Code-Cache von APC hat mich daher nicht interessiert, aber als nettes Beiwerk hätte ich den auch noch genommen.[2]
Das klappte ganz gut, auch wenn ich bei der Konfiguration etwas geflucht habe.
Daher habe ich dann die anwendungsinterne Cacheroutine für alles APC nutzen lassen. Das klappte ganz nett.
Die Anwendung hatte ihre 10 Benutzer, und davon war maximal einer gleichzeitig online [8], aber wenn der eine ich war, war der halt ungeduldig, daher hat sich die Optimierung schon gelohnt.
Zum Beispiel nutzte ich den Cache auch, um Objektdaten zu cachen. Da stand dann z.B. das Ergebnis von
SELECT * FROM objects NATURAL JOIN usr WHERE object_id=27
drin [3][4]. Das hat sich gelohnt für Daten, deren Gewinnung mehr als nur eine Millisekunde gedauert hat.
Wenn nun ein Benutzer auf den Gedanken kam, an seinen Objektdaten herumzuändern, dann wurde nicht nur die Datenbank, sondern auch der Cache geupdatet, und gut war die Sache.
Voraussetzung dafür, daß das sauber funktioniert, ist das APC einen shared cache benutzt, aber das tut es ja per default:
Aus INSTALL:
If you prefer to use mmap instead of the default IPC shared memory
support, add --enable-apc-mmap to your configure line.
Dann kam der Tag, an dem die Software ein System für 1500 Benutzer ersetzte. Und von da an trudelten die Überraschungen ein.
Gut war die Sache? Nein, gut war die Sache eben nicht. Plötzlich tauchten längst überschriebene Daten wieder auf. Oops. Hab' ich irgendwo doch noch eine Stelle, die Objektdaten ändert ohne den Cache upzudaten?
Ein paar Stunden später stand fest: Nein. [5]
Jetzt könnte man in einer solchen Situation den Fehler auf die blöden Benutzer schieben, an kaputte Hardware glauben oder an massive Konzentrationen kosmischer Strahlen am Aufstellungsort dieses Rechners.
Könnte man.
Man könnte auch PHP für Schuld halten. Schon wärmer.
Man könnte auch APC für Schuld halten. Heiß. Und so etwas von heiß.
Denn der Default ist _nicht_, wie in INSTALL gesagt, "IPC shared memory". Der Default ist non-shared mmap. Das verrät einem auch die richtige Doku,in diesem Fall config.m4:
AC_MSG_CHECKING(Checking whether we should use mmap)
AC_ARG_ENABLE(apc-mmap,
[ --disable-apc-mmap
Disable mmap support and use IPC shm instead],
[
PHP_APC_MMAP=$enableval
AC_MSG_RESULT($enableval)
], [
PHP_APC_MMAP=yes
AC_MSG_RESULT(yes)
])
Klar, es ist sicher zu viel verlangt, die Doku dieses Produktes auf dem aktuellen Stand zu halten. Kleine Fehler passieren ja immer wieder mal.
Shit happens, und ehrlich, wer der Doku glaubt, ist eh' ein Idiot.
APC wurde von Hand neu compiliert. Soweit, so gut, apc_cache_info('user') sagte nun klar und deutlich "IPC shared".
Problem gelöst? Ach, iwo, schön wäre es gewesen.
"IPC shared" auszugeben und "IPC shared memory" tatsächlich zu machen sind zwei total verschiedene Paare Schuhe.
Denn tatsächlich ist das, was APC macht, "IPC private memory". Und das wird gleich durch zwei verschiedene Effekte verursacht:
apc_shm_create verwendet ausdrücklich IPC_PRIVATE, wenn man nichts übergibt, woraus sich ein IPC-Schlüssel erzeugen läßt:
int apc_shm_create(const char* pathname, int proj, size_t size)
{
int shmid; /* shared memory id */
int oflag; /* permissions on shm */
key_t key; /* shm key returned by ftok */ key = IPC_PRIVATE;
#ifndef PHP_WIN32
/* no ftok yet for win32 */
if (pathname != NULL) {
if ((key = ftok(pathname, proj)) < 0) {
apc_eprint("apc_shm_create: ftok failed:");
}
}
#endif
oflag = IPC_CREAT | SHM_R | SHM_A;
if ((shmid = shmget(key, size, oflag)) < 0) {
apc_eprint("apc_shm_create: shmget(%d, %d, %d) failed: \
%s. It is possible that the chosen SHM segment size is \
higher than the operation system allows. Linux has usually \
a default limit of 32MB per segment.", key, size, oflag,
strerror(errno));
}
return shmid;
}
Natürlich passiert auch genau das im einzigen Aufrufer:
sma_segments = apc_shm_create(NULL, i, sma_segsize);
Sehr gut gemacht, liebe APC-Entwickler. Ihr habt verstanden, wie man möglichst große Schmerzen erzeugt.
Ich hätte ja an der Stelle /etc/passwd übergeben, wobeider Aufrufer auch mmap_file_mask übergeben könnte. Aber egal.
apc_shm_attach tut folgendes:
void apc_shm_destroy(int shmid)
{
/* we expect this call to fail often, so we do not check */
shmctl(shmid, IPC_RMID, 0);
} void* apc_shm_attach(int shmid)
{
void* shmaddr; /* the shared memory address */
if ((int)(shmaddr = shmat(shmid, 0, 0)) == -1) {
apc_eprint("apc_shm_attach: shmat failed:");
}
/*
* We set the shmid for removal immediately after attaching \
to it. The
* segment won't disappear until all processes have detached \
from it.
*/
apc_shm_destroy(shmid);
return shmaddr;
}
Also, in meinem Universum sorgt das nicht nur dafür, daß das Segment verschwindet wenn der letzte Prozess detached, sondern auch dafür, daß sich auch sonst niemand mehr an das Segment attachen kann.Shared, my ass. Schmerz maximiert, Ziel erreicht.
Daraufhin hab' ich dann genug gehabt und APC aus der Liste verwendbarer Software gestrichen. Ich würde es auch zu schätzen wissen, wenn den Hauptverantwortlichen der Kopf mit einer Axt vom Körper getrennt würde, denn mir kann keiner erzählen, daß diese Kerle nicht auch in anderen Projekten ähnliche Inkompetenz zeigen.
Aber APC zu ersetzen war nicht weiter schwierig. Es gibt ja jede Menge toller Cacheimplementierungen mit PHP-Interface.
Ich hab' daher [6] einen trivialen Daemon um dcache [7] gestrickt und den an TCP lauschen lassen - die PHP-Anbindung war auch trivial.
Was heißt das bisher Geschriebene im Klartext?
==> "IPC shared" gibt's bei APC nicht, es ist "IPC private", und damit ist Cachecoherenz zwischen verschiedenen PHP-Prozessen mit APC nicht drin.
Gruß, Uwe
--------------------------------------------------------------------------
[name=1]Ja, es laufen tatsächlich mehrere PHP-Prozesse gleichzeitig, und ja, ich hab' durchaus untriviale Abfragen, die man locker und leicht mit Gewinn cachen kann.
[2] wobei der Gewinn durch das Cachen des intermediate code während der
Entwicklung meßbar war und im wirklichen Betrieb nicht meßbar war.
Optimized for benchmark, wahrscheinlich.
[3] MySQLs Querycache bringt nicht ganz so viel, wie man denken könnte,
da in objects relativ viel geschrieben wird.
[4] MySQLs Querycache bringt gar nichts, weil MySQL nicht am Werk ist.
[5] Ganz sicher nein. object_save ist hinreichend übersichtlich. Aber
wer weiß, vielleicht gibt's ja doch eine Stelle, die das umgeht, und
die nicht zu finden kostete Zeit.
[6] Mein Vertrauen in anderer Leute Software war zu diesem Zeitpunkt auf
dem Nullpunkt angelangt (wo es auch hingehört) und lieber meine
eigenen Sachen verwendet.
[7] wobei ich ganz offen zugebe, daß dcache an der Stelle nicht optimal
ist, aber verdammt, es ist überraschungsarm.
[8] Obwohl ich nicht darauf wetten würde, daß nicht dann und wann mal
mehrere gleichzeitig da waren, aber das dürfte relativ selten
gewesen sein.