Optymalizacja sesji (jos_session) w Joomla

Posiadają duży serwis oparty na Joomla na pewno robicie dużo, by szybko się otwierał i nie przeciążał serwera. Bardzo dużo daje cachowanie, ale można zrobić coś więcej poza panelem administracyjnym.

Joomla domyślnie przechowuje sesje w bazie danych w tabeli jos_session, nawet zmiana sposobu przechowywania sesji na inny niż Baza danych nie zmianie tego, że Joomla nadal korzysta z tej tabeli na potrzeby pamiętania logowania się.

Warto przyjrzeć się tej tabeli ze względu na to, że Joomla za każdym razem wykonuje na niej sporo operacji, zapisuje dane sesji, aktualizuje i kasuje stare wpisy. Dobrym rozwiązaniem jest zmienić typ tabeli na szybszy. Domyślnie od wersji Joomla 3.0 to InnoDB, w poprzednich wersjach to MyISAM. My zmienimy go na MEMORY.

The MEMORY Storage Engine

W tym typie tabeli dane są przechowywane w pamięci operacyjnej serwera, która oczywiście jest najszybszą pamięcią w serwerze, szybszą nawet dobrych dysków SSD 🙂 Ten typ ma jednak swoje wady, po restarcie bazy danych wszystkie dane są czyszczone, ale dla naszego zastosowania nie ma to znaczenia bo jedyne co nam grozi to wylogowanie z panelu administracyjnego.

max_heap_table_size

Zanim zmienimy Storage Engine na MEMORY musimy się do tego przygotować. W bazie MySQL musimy zmienić wartość max_heap_table_size z domyślnego 16M na odpowiednio więcej.

mysql -p
...
mysql> SHOW VARIABLES LIKE 'max_heap_table_size';
+---------------------+----------+
| Variable_name       | Value    |
+---------------------+----------+
| max_heap_table_size | 16777216 |
+---------------------+----------+
1 row in set (0.00 sec)

Wartość powinniśmy dobrać w zależności od długości sesji i ilości użytkowników, oczywiście z zapasem, nie potrafię podać wzoru, ale na początek może być 128MB lub więcej.

W pliku /etc/mysql/my.cnf w sekcji mysqld dodajemy

[mysqld]
max_heap_table_size = 128M

restartujemy bazę

sudo service mysql restart

sprawdzamy zmnianę

mysql -p
...
mysql> SHOW VARIABLES LIKE 'max_heap_table_size';
+---------------------+-----------+
| Variable_name       | Value     |
+---------------------+-----------+
| max_heap_table_size | 134217728 |
+---------------------+-----------+
1 row in set (0.00 sec)

Wiele osób zaleca zmianę zmiennej tmp_table_size na tą samą wartość, ale ta zmienna nie dotyczy tego.

Jeżeli okaże się, że daliśmy za mało, to w pewnym momencie możemy dostać błąd z bazy MySQL

ERROR 1114 (HY000): The table `jos_session` is full

 Zmiany w tabeli jos_session

Domyślna struktura tabeli nie pozwana nam przekonwertować jej do MEMORY ze względu na to, że MEMORY nie obsługuje pól typu TEXT (TINYTEXT, TEXT, MEDIUMTEXT i LONGTEXT).

mysql -p
...
mysql> DESCRIBE jos_session;
+------------+---------------------+------+-----+---------+-------+
| Field      | Type                | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+-------+
| session_id | varchar(200)        | NO   | PRI |         |       |
| client_id  | tinyint(3) unsigned | NO   |     | 0       |       |
| guest      | tinyint(4) unsigned | YES  |     | 1       |       |
| time       | varchar(14)         | YES  | MUL |         |       |
| data       | mediumtext          | YES  |     | NULL    |       |
| userid     | int(11)             | YES  | MUL | 0       |       |
| username   | varchar(150)        | YES  |     |         |       |
+------------+---------------------+------+-----+---------+-------+
7 rows in set (0.00 sec)

Musimy to pole zamienić na VARCHAR, ale wcześniej trzeba określić maksymalną wielkość tego pola, najlepiej analizując obecne dane

mysql -p
...
mysql> SELECT data FROM jos_session PROCEDURE ANALYSE()\G
*************************** 1. row ***************************
             Field_name: (...).jos_session.data
              Min_value: (...)
              Max_value: (...)
             Min_length: 425
             Max_length: 1735
       Empties_or_zeros: 0
                  Nulls: 0
Avg_value_or_avg_length: 959.7665
                    Std: NULL
      Optimal_fieldtype: TEXT
1 row in set (0.05 sec)

Jak widać w tej chwili jest to 1735, ale z doświadczenia wiem, że warto ustawić w okolicach 4096.

ALTER TABLE jos_session CHANGE data data VARCHAR(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;

Możemy teraz zmienić Storage Engine na MEMORY

ALTER TABLE jos_session ENGINE = MEMORY;

Indeksy

Joomla za każdym razem m.in. kasuje stare sesje opierając się o czas ostatniej modyfikacji rekordu sesji w polu time. Sprawdźmy jak wygląda analiza zapytania z podobnym warunkiem na tym polu

mysql -p
...
mysql> EXPLAIN SELECT * FROM jos_session WHERE time<(UNIX_TIMESTAMP()-60);
+----+-------------+-------------+------+---------------+------+---------+------+-------+-------------+
| id | select_type | table       | type | possible_keys | key  | key_len | ref  | rows  | Extra       |
+----+-------------+-------------+------+---------------+------+---------+------+-------+-------------+
|  1 | SIMPLE      | jos_session | ALL  | time          | NULL | NULL    | NULL | 12687 | Using where |
+----+-------------+-------------+------+---------------+------+---------+------+-------+-------------+
1 row in set (0.00 sec)

Jak widać mimo istnienia indeksu na polu time, to baza MySQL nie wykorzystuje go i  przeszukuje po danych, co jest dużo bardziej kosztowne.
Spójrzmy jakie mamy indeksy

mysql -p
...
mysql> SHOW INDEXES FROM jos_session;
+-------------+------------+----------+-------------+------------+---------+
| Table       | Non_unique | Key_name | Column_name | Index_type | Comment |
+-------------+------------+----------+-------------+------------+---------+
| jos_session |          0 | PRIMARY  | session_id  | HASH       |         |
| jos_session |          1 | userid   | userid      | HASH       |         |
| jos_session |          1 | time     | time        | HASH       |         |
+-------------+------------+----------+-------------+------------+---------+
5 rows in set (0.00 sec)

Przy zmianie Storage Engine baza MySQL domyślnie utworzyła nam indeksy typu HASH.

Storage Engine typu MEMORY obsługuje dwa rodzaje indeksów: HASHBTREE. Przy zapytaniach z użyciem wyrażenia „<” najlepszym wyborem będzie BTREE.
Więcej można o tym przeczytać tutaj
http://dev.mysql.com/doc/refman/5.5/en/index-btree-hash.html

Więc zmieniamy indeks:

ALTER TABLE jos_session DROP INDEX time;
CREATE INDEX time USING BTREE ON jos_session(time);
mysql -p
...
mysql> SHOW INDEXES FROM jos_session;
+-------------+-------------+------------+---------+
| Table       | Column_name | Index_type | Comment |
+-------------+-------------+------------+---------+
| jos_session | session_id  | HASH       |         |
| jos_session | guest       | HASH       |         |
| jos_session | usertype    | HASH       |         |
| jos_session | userid      | HASH       |         |
| jos_session | time        | BTREE      |         |
+-------------+-------------+------------+---------+
5 rows in set (0.00 sec)

sprawdzamy działanie indeksu

mysql -p
...
mysql> EXPLAIN SELECT * FROM jos_session WHERE time<(UNIX_TIMESTAMP()-60);
+----+-------------+-------------+-------+---------------+------+---------+------+-------+-------------+
| id | select_type | table       | type  | possible_keys | key  | key_len | ref  | rows  | Extra       |
+----+-------------+-------------+-------+---------------+------+---------+------+-------+-------------+
|  1 | SIMPLE      | jos_session | range | time          | time | 5       | NULL | 12714 | Using where |
+----+-------------+-------------+-------+---------------+------+---------+------+-------+-------------+
1 row in set (0.00 sec)

Możemy się teraz cieszyć szybkim działaniem sesji 🙂

Obsługa sesji: memcache

Kolejnym krokiem w przyśpieszeniu działania sesji jest wykorzystanie usługi cachowania memcache.

Sprawdzamy czy na naszym serwerze jest zainstalowany memcache, ważne są dla nas dwa pakiety

sudo dpkg -l | grep memcache
...
ii  memcached                           1.4.13-0.2+deb7u1             amd64        A high-performance memory object caching system
...
ii  php5-memcache                       3.0.6-6                       amd64        memcache extension module for PHP5
...

jak nie mamy to instalujemy

sudo apt-get install memcached php5-memcache

restartujemy apache2 (lub inny serwer www) i uruchamiamy memcache

sudo service apache2 restart
sudo service memcached restart

jak mamy to upewniamy się, że działa

sudo service memcached status
[ ok ] memcached is running.

Jeżeli korzystamy z memcached też do innych celów, np. do cache Joomla to warto zmienić jego domyślne ustawienia w pliku /etc/memcached.conf

sudo nano /etc/memcached.conf

Ilość maksymalnej pamięci ustawiamy opcją -m w MB (domyślnie jest 64MB)
Ilość maksymalnych jednoczesnych połączeń przychodzących opcją -c (domyślnie 1024)
Ilość wątków opcją -t (domyślnie nie ma tej opcji w pliku i możemy dopisać, np. -t 8).
Restartujemy i sprawdzamy nowe ustawienia

sudo service memcached restart
sudo ps aux | grep memcached
nobody 1155 0.6 10.7 5891436 5500348 ? Sl Jun20 12:33 /usr/bin/memcached -m 512 -p 11211 -u nobody -l 127.0.0.1 -c 1024 -t 8

Możemy teraz w panelu administracyjnym Joomla zmienić obsługę sesji na memcache zostawiając domyślne ustawienia memcache, czyli Serwer Memcache: localhostPort serwera Memcache: 11211.

Jak już pisałem na samym początku, Joomla nadal będzie zapisywała każdą sesje w tabeli jos_session, ale po zmienia obsługi sesji na inny niż Baza danych, przestaje zapisywać ciężkie dane w polu data a zaczyna je umieszczać w memcache.

mysql -p
...
mysql> SELECT COUNT(*) AS records, data FROM jos_session GROUP BY data;
+---------+------+
| records | data |
+---------+------+
|   19759 | NULL |
+---------+------+
1 row in set (0.26 sec)

Sprawdzamy wielkość tabeli jos_session

mysql -p
...
mysql> SELECT table_schema, table_name, ROUND(data_length / 1024 / 1024) AS MB FROM information_schema.TABLES WHERE table_name = 'jos_session';
+--------------+-------------+------+
| table_schema | table_name  | MB   |
+--------------+-------------+------+
| ...          | jos_session |  248 |
+--------------+-------------+------+
1 row in set, 1 warning (0.00 sec)

Jak widać przy tej ilości rekordów nasza tabela sporo waży, wszystko przez to, że pole data ma wielkość VARCHAR(4096) i mimo, że we wszystkich rekordach jest wartość NULL, to Baza MySQL alokuje tyle pamięci, ile wynika ze struktury tabeli a nie z jej zawartości. Używając gdziekolwiek Storage Engine MEMORY warto zadbać o to, by struktura tabeli nie była za bardzo „rozdmuchana”.
Skoro używamy memcache i w polu data nie potrzebujemy tyle miejsca możemy jego wielkość zmienić na VARCHAR(1)

ALTER TABLE jos_session CHANGE data data VARCHAR(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;

Ponownie sprawdzamy wielkość tabeli jos_session

mysql -p
...
mysql> SELECT table_schema, table_name, ROUND(data_length / 1024 / 1024) AS MB FROM information_schema.TABLES WHERE table_name = 'jos_session';
+--------------+-------------+------+
| table_schema | table_name  | MB   |
+--------------+-------------+------+
| ...          | jos_session |  14 |
+--------------+-------------+------+
1 row in set, 1 warning (0.00 sec)

Przy tej ilości rekordów nasza table jest prawie 18 razy mniejsza, mniejsza tabela na pewno będzie szybsza, zyskaliśmy na miejscu i możemy zmniejszyć albo się nie martwić o parametr max_heap_table_size. Trzeba tylko pamiętać o tym polu, gdyby się działo przywrócić sposób przechowywania sesji na Baza danych.