HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p><strong>Проблемы с памятью в приложениях</strong>- явление довольно частое. Правда, в Python, где работать с памятью напрямую приходится разве что при написании CPython-расширений, сталкиваться с этим приходится реже. Ещё часть рисков снимают фреймворки.</p>
1 <p><strong>Проблемы с памятью в приложениях</strong>- явление довольно частое. Правда, в Python, где работать с памятью напрямую приходится разве что при написании CPython-расширений, сталкиваться с этим приходится реже. Ещё часть рисков снимают фреймворки.</p>
2 <p>Тем не менее понимать, как распределяется память в приложении, всегда полезно. Давайте посмотрим, какие возможности у нас есть на примере небольшого<strong>Django-проекта</strong>.</p>
2 <p>Тем не менее понимать, как распределяется память в приложении, всегда полезно. Давайте посмотрим, какие возможности у нас есть на примере небольшого<strong>Django-проекта</strong>.</p>
3 <p>Итак, у нас есть простая модель данных:</p>
3 <p>Итак, у нас есть простая модель данных:</p>
4 class Department(models.Model): title = models.CharField(max_length=120) class Course(models.Model): title = models.CharField(max_length=120) department = models.ForeignKey(Department, on_delete=models.CASCADE) description = models.TextField() class Lesson(models.Model): title = models.CharField(max_length=120) datetime = models.DateTimeField() course = models.ForeignKey(Course, on_delete=models.CASCADE)<p>И десяток вьюх, отдающих<strong>JSON</strong>. И вот где-то среди них у нас утекает память.</p>
4 class Department(models.Model): title = models.CharField(max_length=120) class Course(models.Model): title = models.CharField(max_length=120) department = models.ForeignKey(Department, on_delete=models.CASCADE) description = models.TextField() class Lesson(models.Model): title = models.CharField(max_length=120) datetime = models.DateTimeField() course = models.ForeignKey(Course, on_delete=models.CASCADE)<p>И десяток вьюх, отдающих<strong>JSON</strong>. И вот где-то среди них у нас утекает память.</p>
5 <p>Прежде всего, нам надо сузить границы поиска. Один из способов - использовать приложение<strong>memory-profiler</strong>.</p>
5 <p>Прежде всего, нам надо сузить границы поиска. Один из способов - использовать приложение<strong>memory-profiler</strong>.</p>
6 <p>В его состав входит удобный<strong>декоратор</strong>, который позволяет смотреть динамику памяти в отдельных функциях. Повесим его на одну из вьюх:</p>
6 <p>В его состав входит удобный<strong>декоратор</strong>, который позволяет смотреть динамику памяти в отдельных функциях. Повесим его на одну из вьюх:</p>
7 @profile def get_lessons(request): lessons = list(Lesson.objects.all()) data = [] for lesson in lessons: data.append({ 'title': lesson.title, 'datetime': lesson.datetime.isoformat() }) dump = json.dumps(data) return HttpResponse(dump, content_type="text/json")<p>Без дополнительных настроек<strong>profile</strong>выводит результаты прямо в стандартный вывод. И выглядит он примерно так:</p>
7 @profile def get_lessons(request): lessons = list(Lesson.objects.all()) data = [] for lesson in lessons: data.append({ 'title': lesson.title, 'datetime': lesson.datetime.isoformat() }) dump = json.dumps(data) return HttpResponse(dump, content_type="text/json")<p>Без дополнительных настроек<strong>profile</strong>выводит результаты прямо в стандартный вывод. И выглядит он примерно так:</p>
8 Line # Mem usage Increment Line Contents ================================================ 29 59.7 MiB 59.7 MiB @profile 30 def get_lessons(request): 31 66.6 MiB 6.9 MiB lessons = list(Lesson.objects.all()) 32 33 66.6 MiB 0.0 MiB data = [] 34 69.1 MiB 0.3 MiB for lesson in lessons: 35 69.1 MiB 0.3 MiB data.append({ 36 69.1 MiB 0.2 MiB 'title': lesson.title, 37 69.1 MiB 0.3 MiB 'datetime': lesson.datetime.isoformat() 38 }) 39 40 70.6 MiB 1.5 MiB dump = json.dumps(data) 41 71.3 MiB 0.7 MiB return HttpResponse(dump, content_type="text/json")<p>Абсолютные значения тут не настолько важны, как<strong>прирост памяти</strong>. Увы, он не всегда нагляден. Например, наполнение списка data-словарями в цикле даёт прирост в 2.5 мегабайта, хотя сумма инкрементов показывает всего 1.1. Кроме того, далеко не всегда (ладно, почти никогда) показатели памяти в рамках одного вызова расскажут об утечке - нужно собирать статистику. И для этого в<strong>memory-profiler</strong>есть разные инструменты. Но, допустим, мы нашли злополучную точку прироста памяти. На что же она уходит? Memory-profiler не показывает деталей, и тут на помощь нам приходит другая библиотека -<strong>pympler</strong>.</p>
8 Line # Mem usage Increment Line Contents ================================================ 29 59.7 MiB 59.7 MiB @profile 30 def get_lessons(request): 31 66.6 MiB 6.9 MiB lessons = list(Lesson.objects.all()) 32 33 66.6 MiB 0.0 MiB data = [] 34 69.1 MiB 0.3 MiB for lesson in lessons: 35 69.1 MiB 0.3 MiB data.append({ 36 69.1 MiB 0.2 MiB 'title': lesson.title, 37 69.1 MiB 0.3 MiB 'datetime': lesson.datetime.isoformat() 38 }) 39 40 70.6 MiB 1.5 MiB dump = json.dumps(data) 41 71.3 MiB 0.7 MiB return HttpResponse(dump, content_type="text/json")<p>Абсолютные значения тут не настолько важны, как<strong>прирост памяти</strong>. Увы, он не всегда нагляден. Например, наполнение списка data-словарями в цикле даёт прирост в 2.5 мегабайта, хотя сумма инкрементов показывает всего 1.1. Кроме того, далеко не всегда (ладно, почти никогда) показатели памяти в рамках одного вызова расскажут об утечке - нужно собирать статистику. И для этого в<strong>memory-profiler</strong>есть разные инструменты. Но, допустим, мы нашли злополучную точку прироста памяти. На что же она уходит? Memory-profiler не показывает деталей, и тут на помощь нам приходит другая библиотека -<strong>pympler</strong>.</p>
9 <p>Она позволяет нам получить статистику разных типов данных. Давайте посмотрим на примере:</p>
9 <p>Она позволяет нам получить статистику разных типов данных. Давайте посмотрим на примере:</p>
10 from memtest.models import Lesson from pympler import tracker tr = tracker.SummaryTracker() lessons = list(Lesson.objects.all()) tr.print_diff()<p>Давайте посмотрим, сколько всего появилось в памяти при получении из базы 10050 записей<strong>Lesson</strong>:</p>
10 from memtest.models import Lesson from pympler import tracker tr = tracker.SummaryTracker() lessons = list(Lesson.objects.all()) tr.print_diff()<p>Давайте посмотрим, сколько всего появилось в памяти при получении из базы 10050 записей<strong>Lesson</strong>:</p>
11 types | # objects | total size ========================================== | =========== | ============ &lt;class 'str | 20172 | 1.30 MB &lt;class 'dict | 20206 | 1.89 MB &lt;class 'list | 9977 | 1.00 MB &lt;class 'int | 21891 | 598.59 KB &lt;class 'django.db.models.base.ModelState | 10050 | 549.61 KB &lt;class 'memtest.models.Lesson | 10050 | 549.61 KB &lt;class 'datetime.datetime | 10050 | 471.09 KB &lt;class 'type | 9 | 9.68 KB &lt;class 'code | 41 | 5.97 KB &lt;class 'weakref | 25 | 1.95 KB function (&lt;lambda&gt;) | 9 | 1.20 KB &lt;class 'method_descriptor | 12 | 864 B &lt;class 'getset_descriptor | 10 | 720 B function (as_sql) | 5 | 680 B &lt;class 'collections.deque | 1 | 632 B<p>Итак, больше всего у нас<strong>строк</strong>. Это ожидаемо. В конце концов, в вебе почти все данные - строки. А вот на втором месте у нас<strong>словари</strong>. И их количество прямо пропорционально количеству записей, которые мы загружаем - ведь на каждый инстанс у нас создается __dict__. Кроме того, видно, что на каждый инстанс появляется еще и по одному django.db.models.base.ModelState (класс, в котором хранится состояние инстанса) и datetime.datetime - но это из-за наличия<strong>DateTimeField</strong>в модели.</p>
11 types | # objects | total size ========================================== | =========== | ============ &lt;class 'str | 20172 | 1.30 MB &lt;class 'dict | 20206 | 1.89 MB &lt;class 'list | 9977 | 1.00 MB &lt;class 'int | 21891 | 598.59 KB &lt;class 'django.db.models.base.ModelState | 10050 | 549.61 KB &lt;class 'memtest.models.Lesson | 10050 | 549.61 KB &lt;class 'datetime.datetime | 10050 | 471.09 KB &lt;class 'type | 9 | 9.68 KB &lt;class 'code | 41 | 5.97 KB &lt;class 'weakref | 25 | 1.95 KB function (&lt;lambda&gt;) | 9 | 1.20 KB &lt;class 'method_descriptor | 12 | 864 B &lt;class 'getset_descriptor | 10 | 720 B function (as_sql) | 5 | 680 B &lt;class 'collections.deque | 1 | 632 B<p>Итак, больше всего у нас<strong>строк</strong>. Это ожидаемо. В конце концов, в вебе почти все данные - строки. А вот на втором месте у нас<strong>словари</strong>. И их количество прямо пропорционально количеству записей, которые мы загружаем - ведь на каждый инстанс у нас создается __dict__. Кроме того, видно, что на каждый инстанс появляется еще и по одному django.db.models.base.ModelState (класс, в котором хранится состояние инстанса) и datetime.datetime - но это из-за наличия<strong>DateTimeField</strong>в модели.</p>
12 <p>Любопытно, что если мы достанем данные из базы не только для Lesson, но и для связанных таблиц, используя select_related, картина расхода памяти поменяется:</p>
12 <p>Любопытно, что если мы достанем данные из базы не только для Lesson, но и для связанных таблиц, используя select_related, картина расхода памяти поменяется:</p>
13 tr = tracker.SummaryTracker() lessons = list(Lesson.objects.select_related("course__department").all()) tr.print_diff() types | # objects | total size =================================================== | =========== | ============ &lt;class 'dict | 70409 | 10.01 MB &lt;class 'str | 50323 | 7.87 MB &lt;class 'django.db.models.base.ModelState | 30150 | 1.61 MB &lt;class 'int | 51233 | 1.37 MB &lt;class 'list | 9980 | 1.00 MB &lt;class 'memtest.models.Department | 10050 | 549.61 KB &lt;class 'memtest.models.Lesson | 10050 | 549.61 KB &lt;class 'memtest.models.Course | 10050 | 549.61 KB &lt;class 'datetime.datetime | 10050 | 471.09 KB &lt;class 'type | 9 | 10.05 KB &lt;class 'code | 41 | 5.97 KB &lt;class 'django.utils.datastructures.ImmutableList | 28 | 2.22 KB &lt;class 'weakref | 19 | 1.48 KB &lt;class 'method_descriptor | 12 | 864 B &lt;class 'getset_descriptor | 10 | 720 B<p>Фатально увеличилось количество словарей, что логично - ведь и экземпляров различных классов теперь в разы больше. По сути, на каждый инстанс наших моделей приходится<strong>2 словаря</strong>- для самого инстанса и для<strong>ModelState</strong>.</p>
13 tr = tracker.SummaryTracker() lessons = list(Lesson.objects.select_related("course__department").all()) tr.print_diff() types | # objects | total size =================================================== | =========== | ============ &lt;class 'dict | 70409 | 10.01 MB &lt;class 'str | 50323 | 7.87 MB &lt;class 'django.db.models.base.ModelState | 30150 | 1.61 MB &lt;class 'int | 51233 | 1.37 MB &lt;class 'list | 9980 | 1.00 MB &lt;class 'memtest.models.Department | 10050 | 549.61 KB &lt;class 'memtest.models.Lesson | 10050 | 549.61 KB &lt;class 'memtest.models.Course | 10050 | 549.61 KB &lt;class 'datetime.datetime | 10050 | 471.09 KB &lt;class 'type | 9 | 10.05 KB &lt;class 'code | 41 | 5.97 KB &lt;class 'django.utils.datastructures.ImmutableList | 28 | 2.22 KB &lt;class 'weakref | 19 | 1.48 KB &lt;class 'method_descriptor | 12 | 864 B &lt;class 'getset_descriptor | 10 | 720 B<p>Фатально увеличилось количество словарей, что логично - ведь и экземпляров различных классов теперь в разы больше. По сути, на каждый инстанс наших моделей приходится<strong>2 словаря</strong>- для самого инстанса и для<strong>ModelState</strong>.</p>
14 <p>В целом, поведение памяти в примере вполне штатно. Я лишь хотел проиллюстрировать некоторые из существующих инструментов для профилирования памяти в<strong>python-приложениях</strong>.</p>
14 <p>В целом, поведение памяти в примере вполне штатно. Я лишь хотел проиллюстрировать некоторые из существующих инструментов для профилирования памяти в<strong>python-приложениях</strong>.</p>
15 <p>Напоследок, вам "небольшая" картинка объектного графа одного инстанса<strong>Lesson</strong>, сгенерированная с помощью библиотеки<a>objgraph</a>=)</p>
15 <p>Напоследок, вам "небольшая" картинка объектного графа одного инстанса<strong>Lesson</strong>, сгенерированная с помощью библиотеки<a>objgraph</a>=)</p>
16 <p><em>Есть вопрос? Напишите в комментариях!</em></p>
16 <p><em>Есть вопрос? Напишите в комментариях!</em></p>
17  
17