RkBlog

Hardware, programming and astronomy tutorials and reviews.

Zużycie pamięci przez aplikacje Django - przypadki

Iterowanie po dużych zbiorach danych

Należy unikać pobierania dużej ilości pełnych rekordów, szczególnie tych o dużym rozmiarze (np. artykuły), gdy chcemy operować na niektórych kolumnach. Należy użyć wtedy metod takich jak .values() i .distinct() by nie pobierać zbędnych danych do pamięci RAM.

Whoosh

Odpalając JobMastera zastosowałem parę nowych rozwiązań, w tym Whoosha zgodnie z przykładem zaprezentowanym na blogu arnebrodowski.de. Najważniejszy jest widok wyszukiwania, gdyż zaprezentowane tam rozwiązanie powoduje wycieki pamięci (hosting Nginx/FastCGI), a dokładnie: searcher = ix.searcher(). Każde wyszukiwanie na stronie zwiększało ilość zużywanej pamięci (memstat -v). Rozwiązanie to nie tworzyć tego obiektu co żądanie, a schować jeden w settingsach aplikacji (gdzie dla pewności wrzuciłem resztę obiektów, które można ponownie wykorzystać):
WHOOSH_SCHEMA = fields.Schema(title=fields.TEXT(stored=True),
				content=fields.TEXT,
				slug=fields.ID(stored=True, unique=True))

storage = store.FileStorage(WHOOSH_INDEX)
ix = index.Index(storage, schema=WHOOSH_SCHEMA)
PARSER = QueryParser("content", schema=ix.schema)
SEARCHER = ix.searcher()
Rozwiązanie to można sprawdzić na prostych serwerach (za pomocą których wypatrzyłem problem). whoosh-client.py:
# -*- coding: utf-8 -*-
from socket import *
for i in range(0, 100):
	s = socket(AF_INET, SOCK_STREAM) #utworzenie gniazda
	s.connect(('localhost', 8889)) # nawiazanie polaczenia
	s.send('popularna fraza tutaj')
	res = s.recv(102400)
	s.close()
	print i
	print res
	print '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
whoosh-server-bad.py:
# -*- coding: utf-8 -*-
from socket import *

import simplejson as json

from whoosh import index, store, fields
from whoosh.qparser import QueryParser
from whoosh import store, fields, index

# dane indeksu whoosh
WHOOSH_INDEX = '/ściezka/do/indeksu/whoosh/'
WHOOSH_SCHEMA = fields.Schema(title=fields.TEXT(stored=True),
				content=fields.TEXT,
				slug=fields.ID(stored=True, unique=True))

# konfiguracja whoosh
storage = store.FileStorage(WHOOSH_INDEX)
ix = index.Index(storage, schema=WHOOSH_SCHEMA)
parser = QueryParser("content", schema=ix.schema)


# konfiguracja serwera
s = socket(AF_INET, SOCK_STREAM) #utworzenie gniazda
s.bind(('', 8889)) 
s.listen(5)

while 1:
	client,addr = s.accept() # odebranie polaczenia
	print 'Polaczenie z ', addr
	while 1:
		# odebranie frazy do wyszukania
		query = client.recv(1024)
		if not query: break
		
		# przygotowanie jej do wyszukiwania
		query = query.replace('+', ' AND ').replace(' -', ' NOT ').replace(', ', ' AND ')
		hits = []
		try:
			
			qry = parser.parse(query)
		except:
			qry = None
		if qry is not None:
			# TO CIEKNIE
			searcher = ix.searcher()
			hits = searcher.search(qry)
		
		# przygotowanie wyników do wysłania
		if len(hits) > 0:
			res = []
			for i in hits:
				res.append({'slug': i['slug'], 'title': i['title']})
			hits =  json.dumps(res)
		else:
			hits = '||'
		client.send(hits) # wyslanie danych do klienta
	client.close()
Oraz wersja "poprawna", whoosh-server.py różniąca się umieszczeniem "ix.searcher()":
# -*- coding: utf-8 -*-
from socket import *

import simplejson as json

from whoosh import index, store, fields
from whoosh.qparser import QueryParser
from whoosh import store, fields, index

# dane indeksu whoosh
WHOOSH_INDEX = '/ściezka/do/indeksu/whoosh/'
WHOOSH_SCHEMA = fields.Schema(title=fields.TEXT(stored=True),
				content=fields.TEXT,
				slug=fields.ID(stored=True, unique=True))

# konfiguracja whoosh
storage = store.FileStorage(WHOOSH_INDEX)
ix = index.Index(storage, schema=WHOOSH_SCHEMA)
searcher = ix.searcher()
parser = QueryParser("content", schema=ix.schema)

# konfiguracja serwera
s = socket(AF_INET, SOCK_STREAM) #utworzenie gniazda
s.bind(('', 8889))
s.listen(5)

while 1:
	client,addr = s.accept() # odebranie polaczenia
	print 'Polaczenie z ', addr
	while 1:
		# odebranie frazy do wyszukania
		query = client.recv(1024)
		if not query: break
		
		# przygotowanie jej do wyszukiwania
		query = query.replace('+', ' AND ').replace(' -', ' NOT ').replace(', ', ' AND ')
		hits = []
		try:
			qry = parser.parse(query)
		except:
			qry = None
		if qry is not None:
			hits = searcher.search(qry)
		
		# przygotowanie wyników do wysłania
		if len(hits) > 0:
			res = []
			for i in hits:
				res.append({'slug': i['slug'], 'title': i['title']})
			hits =  json.dumps(res)
		else:
			hits = '||'
		client.send(hits) # wyslanie danych do klienta
	client.close()
Wystarczy w terminalu odpalić zły serwer i przez np. "ps -aux" obserwować z innego terminala zużycie pamięci. W kolejnym terminalu odpalamy klienta i jak skończy patrzymy na pamięć. Kolejne fale wyszukiwań będą powodowały wzrost zużycia pamięci. W przypadku drugiej, "dobrej" wersji zużycie pamięci nie będzie przyrastać.

Sitemaps

Okazuje się także że generowane przez Djangowski framework Sitemaps mapy sitemap są w jakiś sposób "trzymane" w RAMie - jeżeli mamy dużą mapę, to jej wygenerowanie zajmie sporą ilość pamięci RAM. Dla przykładu moja mapa miała około 9000 elementów, co dawało plik sitemap.xml o rozmiarze 1,7MB. Jej wygenerowanie spowodowało wzrost zużycia pamięci przez aplikację z około 6 do 105MB. Kolejne żądania sitemapy nie podnosiły zużycia RAM. Rozwiązanie: używać statycznych map.
RkBlog

Django, 28 July 2009, Piotr Maliński

Comment article