Разработка на коленке "тут должна быть красивая цитата"

Смена работы (и города)

1 декабря 2011 года моя жизнь в очередной раз изменилась - я официально стал московским гастрабайтером.

Проработав чуть больше года в ростовском радичастотном, я регулярно стал получать предложения о смене работы. На тот момент мы с женой уже твёрдо решили, что переберёмся в Москву, но планировали сделать это весной. В октябре-ноябре я начал зондировать московский рынок работы. Подкорректировал резюме, поменял Ростов на Москву и стал ждать.

Предложения начали поступать довольно быстро (работы много, а работников мало). Одной из первых была компания Positive Technologies (их предложние я в последствии и принял).

Я не стал ждать весны, а набрав список приглашений на собеседование, поехал в Москву.

Первое собеседование - Positive Technologies, не столько из-за того, что они одни из первых на меня вышли, сколько меня привлекала возможность сменить предметную область с веба на что-то другое. По итогам собеседования, которое, как мне казалось, я провалил (и не зря), я получил предложение, с оговоркой, что мне нужно будет многому научиться (хы, напугали ёжика голой попой, я за этим в Москву и ехал).

Потом было ещё два собеседования в других компаниях, о которых я уже договорился (мне не хотелось их отменять). Одно сфейлил, одно прошёл и получил ещё один офер (более выгодный с денежно-плюшковой точки зрения).

Пребывыя в сомнениях, позвонил жене - посоветоваться, идти ли мне в PT. Ответ был прост: "Ну ты же хочешь". В тот момент решение было принято и я отказался от остальных приглашений на собеседования.

1 декабря я вышел на работу.

Теперь я много пишу на Python'e и совсем чуть-чуть на С++ (исчезающе мало). Разрабатываю систему скриптования для MaxPatrol. И мне это нравится.

taskforme жить не будет

После разработки прототипа менеджера задач, у меня появилось несколько идей, как сделать его более удобным и полезным. Перепробовал несколько других todo-листов, продумал реализацию нескольких возможностей, которые мне показались полезными. Но потом отбросил всё это. Причин тому несколько.

Для ведения списка задач по разработке какого-либо проекта я работаю в баг-трекинговой системе, которая, обычно, связана с системой контроля версий. Вести записи о проекте в ещё каком-то приложении - это прямой путь к потери информации о проекте.

Для управления своими задачами я использую телефон: календарь, будильник, органайзер, заметки. Мне этого хватает за глаза. Телефон всегда под рукой, напоминания по времени позволяют не держать в голове мелкие задачи (купить еду, оплатить хостинг, проверить сайт, позвонить другу или заказчику), заметки и qwerty-клавитура дают удобную возможность вносить записи, которые потом трансформируются в задачи (возможно, что и в issue для какого-нибудь проекта).

Поэтому taskforme развивать я не буду, потому что для меня он бесполезен.

Подсчёт уникальных значений в разных столбцах на SQL

Недавно понадобилось сделать выборку из базы данных с подсчётом уникальных значений в разных колонках. Зачем это было нужно - не скажу, потому что это внутренняя информация.

Итак, есть у меня таблица documents и таблица objects.


documents
    id - primary key
    number - string

objects
    id - primary key
    document_id - foreign key
    f_input - integer
    f_output - integer

Вот для полей f_input и f_output мне нужно было выдать количество уникальных значений для каждого документа.


select number, count(distinct f_input)

    from (
        select number, f_input
            from documents
            join objects on documents.id = objects.document_id

        union all

        select number, f_output
            from documents
            join objects on documents.id = objects.document_id
        ) as x

    group by number    
    order by number

Не знаю, лучший ли это способ, но он работает, а в данный момент мне больше и не нужно.

Снова про закрытие файлов в python

Снова возвращаюсь к теме незакрытых файлов в python. В посте Автоматическое закрытие файлов в python я описывал использование with/as для автоматического закрытия файла. Теперь же вижу, что спорол дурость. Для того, чтобы прочесть весь файл в одну строковую переменную вполне достаточно использовать код


v = open('file.txt', 'r').read()

После выполнения этого кода файл будет закрыт, потому что нигде не остаётся ссылок на файловый объект. А раз нет ссылок на объект, то его удалит сборщик мусора.

Ну а чтобы совсем утвердиться в своём предположении, я написал код, который таким образом читает данные из 2000 тысяч файлов, и смотрел на количество открытых файлов в системе. Всё было нормально.

Ещё я написал небольшой кусок кода, чтобы посмотреть, как поведёт себя python, если придётся читать и писать много файлов.


for x in xrange(2000):
    open('files/x_%d' % x, 'w').write(str(x))

for x in xrange(2000):
    print open('files/x_%d' % x, 'r').read(),

fps = []
for x in xrange(2000):
    fp = open('files/x_%d' % x, 'r')
    print fp.read()
    fps.append(fp)

Последний цикл, который сохраняет дескрипторы файлов в список, в итоге вызывает исключение


IOError: [Errno 24] Too many open files: 'files/x_1021'

Это на linux'е. Такой же код на windows бросает исключение на 509 файле.

Всё таки хорошо, что есть сборщик мусора, на который можно переложить такую работу, как закрытие файлов, если файловый обьект на него больше не хранится.

Зависимость скорости работы СУБД от индексов и ORM на примере SQLite, PostgreSQL и SQLAlchemy

В моём зоопарке проектов приложения работают с SQLite3, PostgreSQL, MySql. На MySql крутится всего один проект, да и то только лишь потому, что довольно давно его делал и менять СУБД не вижу смысла. Остальные используют либо SQLite3, либо PostgreSQL.

Больше всего я симпатизирую SQLite, потому что работать с ней просто, из питона доступна в любой момент, вся база лежит одним файлом на диске. И вот стало мне интересно, насколько правильно подобраны у меня СУБД к разным проектам, и насколько правильно я их использую. Ну и насколько тяжко будет сменить одну СУБД на другую, ежели понадобится.

Итак, что у меня есть, что я буду проверять и чем я буду пользоваться:

  1. проекты, использующие SQLite3
  2. проекты, использующие PostgreSQL
  3. большие SQL-запросы, которые сами генерируют полезные отчёты
  4. код на скриптовых языках программирования, которые используют короткие SQL-запросы
  5. код на python с использованием ORM (sqlalchemy)

Вот всё это хозяйство я и захотел протестировать. Ещё я хотел посмотреть, насколько отличается скорость работы при использовании индексов и без них (как на вставке, так и на чтении).

Структура базы данных

Для начала нужна тестовая база данных. Пусть у нас будет база данных для хранения статей. У статьи должен быть автор, уровень сложности материала и комментарии. В итоге имеем таблицы tags (это у меня уровень сложности), authors, articles, comments. Базу я описывал при помощи sqlalchemy. Модуль dbstruct.py содержит описание таблиц, параметр withindex позволяет мне создавать базу данных как с индексами, так и без них.

__author__ = 'monax'

from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.schema import Index
from sqlalchemy.ext.declarative import declarative_base

def createdb(engine, withindex):
    connection = engine.connect()
    queries = (
        'drop table if exists comments',
        'drop table if exists articles',
        'drop table if exists authors',
        'drop table if exists tags'
    )
    map(connection.execute, queries)

    Base = declarative_base(engine)

    class Tag(Base):
        __tablename__ = 'tags'

        id = Column(Integer, primary_key=True)
        tagname = Column(String)

        if withindex:
            __table_args__ = (Index('idx_tag_id', id),)

        def __init__(self, tagname):
            self.tagname = tagname

    class Author(Base):
        __tablename__ = 'authors'

        id = Column(Integer, primary_key=True)
        nickname = Column(String)
        fullname = Column(String)

        if withindex:
            __table_args__ = (Index('idx_author_id', id),)

        def __init__(self, nickname, fullname):
            self.nickname = nickname
            self.fullname = fullname

    class Article(Base):
        __tablename__ = 'articles'

        id = Column(Integer, primary_key=True)
        title = Column(String)
        body = Column(Text)
        author_id = Column(Integer, ForeignKey('authors.id'), nullable=False)
        tag_id = Column(Integer, ForeignKey('tags.id'), nullable=False)

        if withindex:
            __table_args__ = (Index('idx_article_id', id, author_id, tag_id),)

        def __init__(self, title, body, author_id, tag_id):
            self.title = title
            self.body = body
            self.tag_id = tag_id
            self.author_id = author_id

    class Comment(Base):
        __tablename__ = 'comments'

        id = Column(Integer, primary_key=True)
        body = Column(Text)
        article_id = Column(Integer, ForeignKey('articles.id'), nullable=False)

        if withindex:
            __table_args__ = (Index('idx_comment_id', id, article_id),)

        def __init__(self, body, article_id):
            self.body = body
            self.article_id = article_id

    Base.metadata.create_all(engine)

    return Tag, Author, Article, Comment

Работа с данными

Теперь нужно подготовить функции, которые будут делать вставку и выборку данных. Все функции будут лежать в модуле dbtestfunc.py.

Вставка данных в базу

Вставку я делаю с помощью sqlalchemy. Уровней сложности статей у меня 3 - junior, middle, guru. Сложность для статей просто чередуется в цикле. Имена авторов генерируются в цикле. Текст статьи сделал не очень длинным, но и не в 10 символов, примерно, как короткая заметка в блоге.


def fillDB(engine, Tag, Author, Article, Comment, authorsCount, articlesCount, commentsCount):
    session = sessionmaker(engine)()

    tags = ('junior', 'middle', 'guru')
    map(session.add, [Tag(tagname) for tagname in tags])
    session.commit()

    for authorNum in xrange(authorsCount):
        author = Author('au_%d' % authorNum, 'author %d fullname' % authorNum)
        session.add(author)
        session.commit()

        for articleNum in xrange(articlesCount):
            title = 'article %d from auhtor %d' % (articleNum, authorNum)
            body = 'some text of article ' * 200
            tagId = (articleNum % 3) + 1
            article = Article(title, body, author.id, tagId)
            session.add(article)
            session.commit()

            for commentNum in xrange(commentsCount):
                body = 'some text of comment ' * 20
                comment = Comment(body, article.id)
                session.add(comment)

    session.commit()

Статистика по всем таблицам

Нужно получить статистику по количеству данных в каждой таблице.


def getCountByTable(engine):
    query = '''
    select 'tags' as table_name, count(id) as population from tags
    union all
    select 'authors', count(id) from authors
    union all
    select 'articles', count(id) from articles
    union all
    select 'comments', count(id) from comments
    '''

    connection = engine.connect()
    result = connection.execute(query)
    tline = '+%s+%s+' % ('-' * 15, '-' * 20)
    print tline
    for row in result:
        print '|%-15s|%20d|' % (row[0], row[1])
        print tline

Большой sql-запрос с 2 уровнями вложенности подзапросов

Нужно получить список всех авторов, с количеством статей, разбитых по уровню сложности, с количеством комментариев к его статьям, с учётом того, статьи какой сложности лучше комментируются. На этой тестовой базе запрос выглядит слегка натянутым, но на одном реальном проекте у меня есть подобный запрос, который выдаёт статистику по оплате за разные объекты, которые разбиты по категориям.


def useBigSelectQuery(engine):
    query = '''
    select nickname,
        (select count(*) from articles where author_id = authors.id and tag_id = 1),
        (select count(*) from articles where author_id = authors.id and tag_id = 2),
        (select count(*) from articles where author_id = authors.id and tag_id = 3),
        (select count(*) from comments where article_id in (
            select id from articles where author_id = authors.id and tag_id = 1)),
        (select count(*) from comments where article_id in (
            select id from articles where author_id = authors.id and tag_id = 2)),
        (select count(*) from comments where article_id in (
            select id from articles where author_id = authors.id and tag_id = 3))
    from authors
    '''

    connection = engine.connect()
    result = connection.execute(query)

    format = '|' + '|'.join(['%10s' for x in range(7)]) + '|'
    separator = '+' + '+'.join(['%s' % '-' * 10 for x in range(7)]) + '+'

    print '''
SQL query select test
+----------+----------+----------+----------+----------+----------+----------+
|          |junior    |middle    |guru      |junior    |middle    |guru      |
|nickname  |article   |article   |article   |comment   |comment   |comment   |
|          |count     |count     |count     |count     |count     |count     |
+----------+----------+----------+----------+----------+----------+----------+'''

    for row in result:
        print format % tuple(map(str, row))
    print separator

Запрос с меньшим количеством вложенностей подзапросов

Забегая вперёд, скажу, что предыдущий запрос просто насмерть вешал SQLite. Скорость работы падала до нуля, поэтому я переписал его, используя JOIN. Ну а раз он есть, то и померил скорость его работы не только на SQLite, но и на PostgreSQL.


def useJoinBigQuery(engine):
    query = '''
    select nickname,
        (select count(id) from articles where author_id = authors.id and tag_id = 1),
        (select count(id) from articles where author_id = authors.id and tag_id = 2),
        (select count(id) from articles where author_id = authors.id and tag_id = 3),

        (select count(comments.id)
			from comments
			join articles on article_id = articles.id
			where author_id = authors.id and tag_id = 1),

        (select count(comments.id)
			from comments
			join articles on article_id = articles.id
			where author_id = authors.id and tag_id = 2),

        (select count(comments.id)
			from comments
			join articles on article_id = articles.id
			where author_id = authors.id and tag_id = 3)

    from authors
    '''

    connection = engine.connect()
    result = connection.execute(query)

    format = '|' + '|'.join(['%10s' for x in range(7)]) + '|'
    separator = '+' + '+'.join(['%s' % '-' * 10 for x in range(7)]) + '+'

    print '''
SQL query select with join test
+----------+----------+----------+----------+----------+----------+----------+
|          |junior    |middle    |guru      |junior    |middle    |guru      |
|nickname  |article   |article   |article   |comment   |comment   |comment   |
|          |count     |count     |count     |count     |count     |count     |
+----------+----------+----------+----------+----------+----------+----------+'''

    for row in result:
        print format % tuple(map(str, row))
    print separator

Используем ORM для получения данных

Теперь для получения тех же данных буду использовать ORM (sqlalchemy). Тут я, по сути, написал код, подобный первому большому запросу (без join).


def useORM(engine, Tag, Author, Article, Comment):
    from sqlalchemy.orm import sessionmaker

    session = sessionmaker(engine)()

    format = '|' + '|'.join(['%10s' for x in range(7)]) + '|'
    separator = '+' + '+'.join(['%s' % '-' * 10 for x in range(7)]) + '+'

    print '''
ORM select test
+----------+----------+----------+----------+----------+----------+----------+
|          |junior    |middle    |guru      |junior    |middle    |guru      |
|nickname  |article   |article   |article   |comment   |comment   |comment   |
|          |count     |count     |count     |count     |count     |count     |
+----------+----------+----------+----------+----------+----------+----------+'''

    for author in session.query(Author):
        data = [author.nickname,
                session.query(Article).filter(
                    Article.author_id==author.id).filter(Article.tag_id==1).count(),
                session.query(Article).filter(
                    Article.author_id==author.id).filter(Article.tag_id==2).count(),
                session.query(Article).filter(
                    Article.author_id==author.id).filter(Article.tag_id==3).count(),
                session.query(Comment).filter(
                    Comment.article_id.in_(
                        session.query(Article.id).filter(
                            Article.author_id==author.id).filter(
                            Article.tag_id==1))).count(),

                session.query(Comment).filter(
                    Comment.article_id.in_(
                        session.query(Article.id).filter(
                            Article.author_id==author.id).filter(
                            Article.tag_id==2))).count(),

                session.query(Comment).filter(
                    Comment.article_id.in_(
                        session.query(Article.id).filter(
                            Article.author_id==author.id).filter(
                            Article.tag_id==3))).count()
                ]

        print format % tuple(map(str, data))
    print separator

Использование нескольких маленьких запросов

Ну и функция, которая нужна для проверки эффективности использования нескольких небольших запросов, которые вызывают в цикле.


def useSimpleQueries(engine):

    format = '|' + '|'.join(['%10s' for x in range(7)]) + '|'
    separator = '+' + '+'.join(['%s' % '-' * 10 for x in range(7)]) + '+'

    print '''
Some simple SQL select test
+----------+----------+----------+----------+----------+----------+----------+
|          |junior    |middle    |guru      |junior    |middle    |guru      |
|nickname  |article   |article   |article   |comment   |comment   |comment   |
|          |count     |count     |count     |count     |count     |count     |
+----------+----------+----------+----------+----------+----------+----------+'''

    connection = engine.connect()

    selAuthors = 'select id, nickname from authors'
    rAuthors = connection.execute(selAuthors)
    for author in rAuthors:
        result = [author.nickname]
        for tagId in range(1, 4):
            selArtCount = 'select count(*) from articles where author_id = %d and tag_id = %d' %\
                          (author.id, tagId)
            result.append(map(None, connection.execute(selArtCount))[0][0])

        for tagId in range(1, 4):
            selComCount = '''select count(*) from comments where article_id in (
                select id from articles where author_id = %d and tag_id = %d)''' %\
                          (author.id, tagId)
            result.append(map(None, connection.execute(selComCount))[0][0])

        print format % tuple(map(str, result))
    print separator

Чтобы все эти функции заработали вместе, в начале модуля нужен код:


from sqlalchemy.orm import sessionmaker

Тестирование

Чтобы проверить время работы каждой функции из модуля dbtestfuncs.py, я написал модуль dbloadtest.py. Он запускается из командной строки и получает в качестве параметров тип СУБД: postgresql, sqlite, memory (sqlite в памяти), флаг использования индексов в базе и количество авторов (не обязательный параметр). На выходе табличка с результатами теста.


__author__ = 'monax'

import sys
import dbstruct
from dbtestfuncs import *

from sqlalchemy import create_engine

def usage():
    man = '''Options:
    dbms type:
    -p - postgresql
    -s - slqite3. work with file
    -m - sqlite3. work with memory

    -i - using index in tables
    -n - using no index in tables

    -a=authors_count  - count of authors
    '''
    return man

if __name__ == '__main__':

    authorsCount = 10
    articlesCount = 10
    commentsCount = 20

    try:
        options = sys.argv[1:]

        if '-p' in options:
            dbparams = 'postgresql://user:secret@localhost:5432/_dbloadtest'
        elif '-s' in options:
            import os
            try:
                os.remove('_dbloadtest.sqlite')
            except: pass
            
            dbparams = 'sqlite:///_dbloadtest.sqlite'
        elif '-m' in options:
            dbparams = 'sqlite:///:memory:'
        else:
            raise IndexError

        if '-i' in options:
            withindex = True
        elif '-n' in options:
            withindex = False
        else:
            raise IndexError

        authc = [x for x in options if x.startswith('-a=')]
        if authc:
            authorsCount = int(authc[0].split('=')[1])
            print 'count of authors = ', authorsCount

    except IndexError:
        print usage()
        sys.exit(1)

    print 'options: %s' % str(options)
    
    engine = create_engine(dbparams, echo=False)
    Tag, Author, Article, Comment = dbstruct.createdb(engine, withindex)

    from timeit import Timer

    setup = 'from __main__ import fillDB, engine, Tag, Author, Article, Comment, ' +\
            'authorsCount, articlesCount, commentsCount'
    call = 'fillDB(engine, Tag, Author, Article, Comment, authorsCount, articlesCount, commentsCount)'
    fillTime = Timer(call, setup).timeit(1)

    setup = 'from __main__ import getCountByTable, engine'
    countByTableTime = Timer('getCountByTable(engine)', setup).timeit(1)

    setup = 'from __main__ import useBigSelectQuery, engine'
    bigSelectQTime = Timer('useBigSelectQuery(engine)', setup).timeit(1)

    setup = 'from __main__ import useJoinBigQuery, engine'
    bigJoinQTime = Timer('useJoinBigQuery(engine)', setup).timeit(1)


    setup = 'from __main__ import useORM, engine, Tag, Author, Article, Comment'
    ormUseTime = Timer('useORM(engine, Tag, Author, Article, Comment)', setup).timeit(1)

    setup = 'from __main__ import useSimpleQueries, engine'
    simpleQTime = Timer('useSimpleQueries(engine)', setup).timeit(1)

    times = (fillTime, countByTableTime, bigSelectQTime, bigJoinQTime,
             ormUseTime, simpleQTime)
    heads = ('fill', 'count by tables', 'big select', 'big select join',
             'orm use', 'simple queries')
    line = '+%s+%s+' % ('-' * 20, '-' * 20)
    print
    print line
    for i in range(len(times)):
        print '|%-20s|%20f|' % (heads[i], times[i])
        print line
        
    print '\ntesting ended!'

Для запуска предыдущего файла я использовал небольшой bash-скрипт (при написании которого я мог допустить кучу косяков, потому что для меня bash пока что неизученная территория).


#!/bin/bash

date

AUTH_COUNTS=(1 10 50 100 200)

ELEMENTS=${#AUTH_COUNTS[@]}

RESULT_DIR=all_test_results

if test -e $RESULT_DIR; then
    rm -rf $RESULT_DIR
fi

mkdir $RESULT_DIR

echo "*postgresql test"
i=0
while [ $i -lt $ELEMENTS ] 
do
	COUNT=${AUTH_COUNTS[$i]}
	i=$[$i+1]

	echo "	iteration $i start..."
	python dbloadtest.py -p -i -a=$COUNT > $RESULT_DIR/[$i]_pg_with_index.txt
	python dbloadtest.py -p -n -a=$COUNT > $RESULT_DIR/[$i]_pg_no_index.txt
	echo "		...done"
	echo
done

echo "*sqlite3 memory test"
i=0
while [ $i -lt $ELEMENTS ]
do
	COUNT=${AUTH_COUNTS[$i]}
	i=$[$i+1]

	echo "	iteration $i start..."
	python dbloadtest.py -m -i -a=$COUNT > $RESULT_DIR/[$i]_memory_with_index.txt
	python dbloadtest.py -m -n -a=$COUNT > $RESULT_DIR/[$i]_memory_no_index.txt
	echo "		...done"
	echo	
done

echo "*sqlite3 in file test"
i=0
while [ $i -lt $ELEMENTS ]
do
	COUNT=${AUTH_COUNTS[$i]}
	i=$[$i+1]

	echo "	iteration $i start..."
	python dbloadtest.py -s -i -a=$COUNT > $RESULT_DIR/[$i]_sqlite_with_index.txt
	python dbloadtest.py -s -n -a=$COUNT > $RESULT_DIR/[$i]_sqlite_no_index.txt
	echo "		...done"
	echo	
done

date

Обработка результатов

В результате запуска предыдущего скрипта получил кучу файлов с результатами. Тут сразу оговорюсь, что во время последнего теста я немного менял скрипт dbloadtest.py, потому что на количестве авторов больше 50 дождаться завершения теста с sqlite было невозможно.

Чтобы получить сводную табличку из всех файлов с результатами, написал ещё один скрипт


__author__ = 'monax'

resultDirName = 'result'
patternsFileName = ('_memory_no_index.txt', '_memory_with_index.txt',
    '_pg_no_index.txt', '_pg_with_index.txt', '_sqlite_no_index.txt',
    '_sqlite_with_index.txt')

authorsCounts = (1, 10, 50, 100, 200)

heads = ('fill', 'count by tables', 'big select', 'big select join',
         'orm use', 'simple queries')


times = {}
for pattern in patternsFileName:
    times[pattern] = []
    for idxFile in range(len(authorsCounts)):
        fileName = resultDirName + '/' + ('[%d]' % (idxFile + 1)) + pattern
        with open(fileName, 'rt') as fp:
            times[pattern].append(map(lambda x: float(x.split('|')[2]),
                                      fp.readlines()[-14:-2:2]))

line = '+' + '-' * 20 + '+' + '+'.join(['-' * 10 for x in range(5)]) + '+'


for dbType in times:
    print '\n', dbType
    print line
    print ('|%-20s' % 'action/author_count') + ('|%10s|%10s|%10s|%10s|%10s|' % authorsCounts)
    print line

    for idxActionName in range(len(heads)):
        action = heads[idxActionName]
        dbTypeTimes = times[dbType]
        actionTimes = tuple([str(t[idxActionName]) for t in dbTypeTimes])
        print ('|%-20s' % action) + ('|%10s|%10s|%10s|%10s|%10s|' % actionTimes)
        print line

Результаты

Результаты сгруппировал по типу СУБД и по наличию индексов в базе. Там, где стоит -1, я просто не дождался завершения работы скрипта.

_pg_with_index.txt
+--------------------+----------+----------+----------+----------+----------+
|action/author_count |         1|        10|        50|       100|       200|
+--------------------+----------+----------+----------+----------+----------+
|fill                |  0.193671|  1.403152|  7.764614| 19.335149| 41.985839|
+--------------------+----------+----------+----------+----------+----------+
|count by tables     |  0.001125|  0.001679|  0.004773|  0.006991|   0.00916|
+--------------------+----------+----------+----------+----------+----------+
|big select          |  0.002168|    0.0087|  0.163934|  0.560634|  2.259602|
+--------------------+----------+----------+----------+----------+----------+
|big select join     |  0.001788|  0.008284|   0.16166|  0.560226|  2.228539|
+--------------------+----------+----------+----------+----------+----------+
|orm use             |  0.017114|  0.139729|  0.808284|  1.846665|  4.896157|
+--------------------+----------+----------+----------+----------+----------+
|simple queries      |  0.003438|   0.02969|  0.301106|  0.827559|    2.8705|
+--------------------+----------+----------+----------+----------+----------+

_pg_no_index.txt
+--------------------+----------+----------+----------+----------+----------+
|action/author_count |         1|        10|        50|       100|       200|
+--------------------+----------+----------+----------+----------+----------+
|fill                |  0.231285|   1.72187|  7.842117| 18.868243| 41.146092|
+--------------------+----------+----------+----------+----------+----------+
|count by tables     |  0.002108|   0.00162|  0.003346|  0.006318|  0.009838|
+--------------------+----------+----------+----------+----------+----------+
|big select          |  0.002859|  0.013254|  0.263669|  1.146801|  4.791618|
+--------------------+----------+----------+----------+----------+----------+
|big select join     |  0.002679|  0.012731|  0.260424|  1.170647|  4.940643|
+--------------------+----------+----------+----------+----------+----------+
|orm use             |  0.020962|  0.137301|  0.924065|  2.461377|  7.775531|
+--------------------+----------+----------+----------+----------+----------+
|simple queries      |  0.004852|  0.033245|  0.398083|  1.480214|  5.614259|
+--------------------+----------+----------+----------+----------+----------+

_memory_with_index.txt
+--------------------+----------+----------+----------+----------+----------+
|action/author_count |         1|        10|        50|       100|       200|
+--------------------+----------+----------+----------+----------+----------+
|fill                |  0.046558|  0.435866|  2.297696|   4.44086|  9.182301|
+--------------------+----------+----------+----------+----------+----------+
|count by tables     |  0.000232|  0.000598|  0.002518|  0.004859|  0.008582|
+--------------------+----------+----------+----------+----------+----------+
|big select          |  0.010185|  4.173265|  495.3262|      -1.0|      -1.0|
+--------------------+----------+----------+----------+----------+----------+
|big select join     |  0.000852|   0.04177|  1.102579|  4.700694|  19.02497|
+--------------------+----------+----------+----------+----------+----------+
|orm use             |  0.011013|  0.119054|  1.132743|  3.507863| 12.158737|
+--------------------+----------+----------+----------+----------+----------+
|simple queries      |  0.000809|  0.028106|  0.581097|  2.499086| 10.060157|
+--------------------+----------+----------+----------+----------+----------+

_memory_no_index.txt
+--------------------+----------+----------+----------+----------+----------+
|action/author_count |         1|        10|        50|       100|       200|
+--------------------+----------+----------+----------+----------+----------+
|fill                |   0.04515|  0.435912|  2.266461|    4.3318|  8.932379|
+--------------------+----------+----------+----------+----------+----------+
|count by tables     |  0.000238|  0.000627|  0.002452|  0.004428|  0.008911|
+--------------------+----------+----------+----------+----------+----------+
|big select          |  0.010438|  4.221292| 492.76092|      -1.0|      -1.0|
+--------------------+----------+----------+----------+----------+----------+
|big select join     |  0.000861|  0.042676|  1.035481|  4.751594| 18.770619|
+--------------------+----------+----------+----------+----------+----------+
|orm use             |  0.011256|  0.121582|  1.041038|  3.585144| 12.383719|
+--------------------+----------+----------+----------+----------+----------+
|simple queries      |  0.000857|   0.02478|  0.514296|  2.501858|  9.707688|
+--------------------+----------+----------+----------+----------+----------+

_sqlite_with_index.txt
+--------------------+----------+----------+----------+----------+----------+
|action/author_count |         1|        10|        50|       100|       200|
+--------------------+----------+----------+----------+----------+----------+
|fill                |  2.345496| 26.964783|127.245174|248.164527|485.671184|
+--------------------+----------+----------+----------+----------+----------+
|count by tables     |  0.022325|  0.023069|   0.02127|  0.037057|   0.06431|
+--------------------+----------+----------+----------+----------+----------+
|big select          |  0.030343|  4.971248| 9700.4559|      -1.0|      -1.0|
+--------------------+----------+----------+----------+----------+----------+
|big select join     |  0.023542|   0.07756|  5.236409| 22.053952| 84.810086|
+--------------------+----------+----------+----------+----------+----------+
|orm use             |  0.036379|  0.148815|  5.117857| 19.794298|  74.20158|
+--------------------+----------+----------+----------+----------+----------+
|simple queries      |  0.025443|  0.047866|  4.520323| 18.618552| 72.834561|
+--------------------+----------+----------+----------+----------+----------+

_sqlite_no_index.txt
+--------------------+----------+----------+----------+----------+----------+
|action/author_count |         1|        10|        50|       100|       200|
+--------------------+----------+----------+----------+----------+----------+
|fill                |  2.302192| 22.608962|114.141749|215.950755| 432.98446|
+--------------------+----------+----------+----------+----------+----------+
|count by tables     |  0.038948|  0.019935|  0.028326|  0.034316|  0.063907|
+--------------------+----------+----------+----------+----------+----------+
|big select          |  0.030463|  4.933617|      -1.0|      -1.0|      -1.0|
+--------------------+----------+----------+----------+----------+----------+
|big select join     |  0.024648|  0.077713|   5.32001| 20.818145| 85.383302|
+--------------------+----------+----------+----------+----------+----------+
|orm use             |  0.030698|  0.149703|   5.12185| 19.155246| 74.946809|
+--------------------+----------+----------+----------+----------+----------+
|simple queries      |  0.019438|  0.047167|  4.594021| 17.915685| 72.765067|
+--------------------+----------+----------+----------+----------+----------+

Заключение

По данным в таблице сделал для себя несколько выводов:

  1. SQLite не так плох, даже в сравнении с таким монстром, как PostgreSQL.
  2. Скорость работы PostgreSQL конечно же превосходит SQLite.
  3. PostgreSQL одинаково хорошо работает как с одним большим запросом, так и с очередью маленьких.
  4. SQLAlchemy с PostgreSQL даёт уменьшение скорости работы примерно в два раза.
  5. SQLAlchemy + SQLite уступает в производительности sql-запросам, но не намного.
  6. SQLite плохо переваривает подзапросы с уровнем вложенности больше 1.
  7. Индексы в SQLite не дают прироста производительности, в PostgreSQL скорость выборки увеличивается в 2 раза. При этом скорость вставки данных не падает (во всяком случае если и падает, то не сильно).
  8. Скорость вставки SQLite в памяти (memory) очень высока. Можно использовать при каких-нибудь тестах, чтобы выполнялось быстрее.

Весь код можно стащить с github.com - dbloadtest

PyCharm (1.5.2)

Начитавшись восторженных речей про PyCharm, решил попробовать его в деле. Полез на сайт и, на всякий случай, скачал сразу две версии: под виндовс и под линукс.

Версия под Linux. Запустил PyCharm. Много всяких кнопочек, менюшек и настроек всего и вся. Обрадовался тому, что есть возможность поставить keymap, к которому уже привык. Я выбрал Emacs. Поставил пайчарму плюсик. Попробовал написать небольшой консольный проект на питоне. Вот тут увидел сразу несколько минусов:

  • PyCharm жутко тормозит! И это при том, что я привык работать со всякими эклипсами (для явы, CDT, Aptana).
  • Keymap при автодополнении работает не так, как в Emacs.
  • Выглядит не совсем нативно. Но это не совсем минус, просто до кучи к первым двум.

На этом тестирование под линуксом закончилось, потому что с такой тормознутостью работать оказалось просто невозможно.

Версия под виндовс. Машина с виндой гораздо мощнее, чем с линухом. Процессор с 4 ядрами, 4 гига оперативки. Запускаю. Пытаюсь написать всё тот же консольный проект (благо ему всё равно под какой системой работать). Попытка удалась. Проработав в этой среде 2 недели, и написав 2 небольших консольных проекта сделал вывод, что PyCharm тормозит и тут. Не так жутко, но заметно. Хоть тут у меня всё же более мощная машина.

Вообще же, работая в Emacs, Vim, FlashDevelop у меня во время работы всегда есть чувство, что код находится у меня на кончиках пальцев, и мне достаточно просто дотронуться до клавиатуры, и код будет появляться в окне редактора. Тут же у меня было ощущение, что я выбиваю этот код зубилом на камне. В итоге я не дождался даже окончания бесплатного триального периода и забросил PyCharm. Может ещё через пару версий он станет пошустрее.

Отверстия, винты и О от n

В одном блоге нашёл описание небольшой задачи про винты и гнёзда.

Имеется n винтов и n гнезд, расположенных в произвольном порядке. Каждому винту соответствует по диаметру только одно гнездо. Все винты имеют разные диаметры.

Требуется расставить все винты по гнездам. Разрешено только одно действие - попытка вставить винт i в гнездо j. В результате такой операции можно выяснить:

  1. винт тоньще гнезда - не подходит
  2. винт толще гнезда - не подходит
  3. или винт точно входит в гнездо - подходит

Сравнивать винты или гнезда между собой нельзя.

Ну и чем-то она меня зацепила. В целом, на интуитивном уровне было понятно, что кроме полного перебора с O(n2) можно добиться более быстрого решения (в идеальном случае O(log2n)) при помощи бинарного дерева. Однако ж во всех этих зависимостях O от n меня интересует не только вид зависимости, но и и константа, про которую часто забывают. Константа зависит от стоимости операций, выполняемых в том или ином алгоритме. Поэтому мне захотелось пощупать и цифры.

Для начала я сделал следующие допущения к описанию задачи:

  1. Гнёзда после нахождения нужного винта из множества поиска не исключаются.
  2. Количества гнёзд и винтов равны (чтобы не возиться с проверками).
  3. Каждому гнезду по размеру соответствует один и только один винт.

Ну а потом я начал писать код. Чтобы проверить соответствие результатов поиска, функции возвращают данные в виде ассоциативного массива, где каждому ключу соответствует кортеж. Ключ - это размер отверстия и винта, в кортеже содержатся индекс отверстия и индекс винта.

Код юнит-тестов я приводить не буду, потому что они не добавляют полезной информации о скорости работы.

Первым сделал простой поиск соответствий полным перебором fullsearch.py:


# -*- coding: utf-8 -*-
__author__ = 'monax'

def search(holes, screws):
    '''Ищет соответствия между значениями винтов и отверстий. Возвращает
    словарь, сотоящий из кортежей {размер:( индекс_отверстия, индекс_винта)}
    '''
    matches = {}
    for s in screws:
        for h in holes:
            if s == h:
                matches[h] = (holes.index(h), screws.index(s))
                break

    return matches

Потом взялся за написание скрипта, который делает поиск с помощью бинарного дерева. Тут нужно пояснить, что после того, как сформировано дерево, мне нужно сконвертировать его в словарь, содержащий соответствия индексов винтов и гнёзд. Конвертацию я делал не рекурсией, а в цикле, потому что если у нас отсортированный список шурупов, то рекурсия достигнет предельной глубины очень быстро (где-то на 1000 вызовов):


# -*- coding: utf-8 -*-
__author__ = 'monax'

class Node(object):
    def __init__(self, holeIdx, screwIdx, val, left, right):
        self.holeIdx = holeIdx
        self.screwIdx = screwIdx
        self.val = val
        self.left = {'node': None, 'list': left}
        self.right = {'node': None, 'list': right}

def search(holes, screws):
    root = None

    holes = [(holes.index(h), h) for h in holes]

    for screw in screws:
        if root:
            node = root
            while True:
                nodedata = None
                if screw > node.val:
                    # to right
                    if node.right['node']:
                        node = node.right['node']
                    else:
                        nodedata = node.right
                else:
                    # to left
                    if node.left['node']:
                        node = node.left['node']
                    else:
                        nodedata = node.left
                if nodedata:
                    left = [h for h in nodedata['list'] if h[1] < screw]
                    right = [h for h in nodedata['list'] if h[1] > screw]
                    holeIdx = [h[0] for h in nodedata['list'] if h[1] == screw][0]
                    screwIdx = screws.index(screw)
                    nodedata['node'] = Node(holeIdx, screwIdx, screw, left, right)
                    nodedata['list'] = None # нужно очистить память, а то кончится
                    break
        else:
            left = [h for h in holes if h[1] < screw]
            right = [h for h in holes if h[1] > screw]
            holeIdx = [h[0] for h in holes if h[1] == screw][0]
            screwIdx = screws.index(screw)
            root = Node(holeIdx, screwIdx, screw, left, right)

    # конвертирую дерево в словарь
    matches = {}
    nodelist = [root]
    while False:
        for node in nodelist:
            matches[node.val] = (node.holeIdx, node.screwIdx)
        nodelist = [node.left['node'] for node in nodelist if node.left['node']] +\
                   [node.right['node'] for node in nodelist if node.right['node']]
    return matches

Ну а после мне понадобился скрипт, который меряет время работы (скрипт, который сравнивает результаты работы двух методов приводить тоже не буду, но он мне всё же понадобился, чтобы убедиться, что оба скрипта выдают одинаковые словари на выходе).


# -*- coding: utf-8 -*-
__author__ = 'monax'

import btreesearch
import fullsearch
import random
import timeit

from datetime import datetime

widths = (10, 20, 20, 20, 20, 20)

df = '|%' + str(widths[0]) + 'd|' +\
     '|'.join(['%' + str(w) + 'f' for w in widths[1:-1]]) +\
     '|%' + str(widths[-1]) + 'd|'

separator = '+%s+%s+%s+%s+%s+%s+' % tuple(['-' * w for w in widths])
thead = ('+%s|%s|%s|%s|%s|%s+' % tuple(['%' + str(w) + 's' for w in widths])) %\
        ('count', 'fullsearch (sort)', 'btreesearch (sort)', 'fullsearch (rand)',
         'btreesearch (rand)', 'first rand screw')

print 'begin' , ':'.join(map(str, datetime.now().timetuple()[3:6])), '\n'

print separator
print thead

setup = 'from __main__ import holes, screws, fullsearch, btreesearch'
lengths = [10, 100, 500, 1000, 5000, 10000, 20000, 30000]

for l in lengths:
    holes = range(l)
    screws = range(l)
    fTimeOrder = timeit.Timer('fullsearch.search(holes, screws)', setup).timeit(1)
    bTimeOrder = timeit.Timer('btreesearch.search(holes, screws)', setup).timeit(1)
    random.shuffle(holes)
    random.shuffle(screws)
    fTimeRand = timeit.Timer('fullsearch.search(holes, screws)', setup).timeit(1)
    bTimeRand = timeit.Timer('btreesearch.search(holes, screws)', setup).timeit(1)
    
    print separator
    print df % (l, fTimeOrder, bTimeOrder, fTimeRand, bTimeRand, screws[0])

print separator

print '\nend' , ':'.join(map(str, datetime.now().timetuple()[3:6]))

Результат работы скрипта:


+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
+     count|   fullsearch (sort)|  btreesearch (sort)|   fullsearch (rand)|  btreesearch (rand)|    first rand screw+
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|        10|            0.000030|            0.000136|            0.000027|            0.000103|                   4|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|       100|            0.001225|            0.007208|            0.001242|            0.002093|                  96|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|       500|            0.029338|            0.171127|            0.025904|            0.022690|                 346|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|      1000|            0.098980|            0.712090|            0.105065|            0.075805|                 723|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|      5000|            2.446349|           17.888047|            2.570363|            1.523652|                2123|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|     10000|            9.946249|           77.531122|           10.421238|            5.964480|                9905|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|     20000|           43.110249|          323.101860|           41.776272|           23.398654|                2207|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+
|     30000|           90.674444|          770.846494|           96.210516|           53.148294|                2292|
+----------+--------------------+--------------------+--------------------+--------------------+--------------------+

По данным в таблице: первый столбец - это количество элеметов (пресловутое n), второй столбец - время работы полного перебора на отсортированном множестве, третий - поиск по дереву при отсортированном входном множестве, четвёртый - полный поиск при неупорядоченном множестве на входе, пятый - поиск по дереву при неупорядоченном множестве, и шестой - номер (размер) шурупа, с которого начинался поиск по дереву при неупорядоченном множестве на входе.

Тут, в общем-то, очевидно, что поиск по дереву очень сильно уступает в скорости, если входное множество упорядоченно, и очень выигрывает, если входное множество перемешано. Чтобы поиск по дереву был всегда эффективен, то нужно просто перемешивать (random.shuffle) входное множество в функции btreesearch.search перед началом работы. Причём, нужно именно перемешать, а не поменять местами первый элемент и тот, который посредине (screws[0], screws[n/2] = screws[n/2], screws[0], хоть мы не можем сравнивать значения, но можем сравнивать индексы), как мне предлагал один камрад, потому что тогда поиск будет работать только в два раза быстрее, чем при упорядоченном множестве.

Автоматическое закрытие файла в python

Когда писал скрипт для писем счастья, мне не нравился один фрагмент кода:


filename = sys.argv[2]
fHandle = open(filename, 'rt')
text = fHandle.read()
fHandle.close()

Просто выглядит он как-то... нелаконично что-ли. Ещё тогда я хотел заменить его на такой код:


text = open(sys.argv[2], 'rt').read()

Но дело в том, что файл остаётся открытым. По идее, файл закроется после завершения работы скрипта, но трудное детство (C, assembler) не позволяет мне бросать открытые файлы на произвол судьбы. А вчера я перечитывал описание with/as. Так вот эта конструкция как раз обеспечивает автоматическое закрытие файлов (на самом деле она обеспечивает вызов __exit__ у объекта при выходе из конструкции). Вот ей-то я и решил воспользоваться и получился вот такой код:


with open(sys.argv[2], 'rt') as fp:
    text = fp.read()

Чтобы быть уверенным, что файл действительно закрывается, я запустил интерпретатор python'а в интерактивном режиме и написал код:


$ python
>>> with open('withdata.txt', 'wt') as egg:
...     egg.write('hi\n')
... 
>>> type(egg)
<type 'file'>
>>> egg.write('bye\n')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file

$ cat withdata.txt 
hi

То есть переменная остаётся в области видимости, но файл закрывается после выхода за пределы with/as.

Настройка python-хостинга для pylons-приложений на основе nginx + uwsgi на ubuntu

Настраиваю сервер ubuntu для запуска на нём
wsgi-приложений, написанных с использованием pylons.

Я не админ, я только учусь!

Мне нужно было настроить сервер, на котором бы крутилось pylons-приложение. Поскольку я проверял разные конфигурации по памяти, процессору и версиям пакетов, мне нужно было сделать всё возможное, чтобы упростить тестирование и повторное разворачивание сервера на виртуальных машинах. К самому серверу были следующие требования:

  1. Статику должен отдавать шустрый сервер (nginx | lighttpd)
  2. Приложение должно работать не под рутом, чтобы было не очень страшно за безопасность.
  3. Процессы нужно мониторить и перезапускать в случае падения.
  4. Обо всех непрятностях сообщать админу (мне то есть).
  5. Для удобства тестирования весь процесс установки должен быть автоматизирован, чтобы действия ограничивались запуском нескольких скриптов.

В результате решил, что:

  1. В качестве фронт-енд сервера будет nginx, потому что lighttpd + flup ложится при большой нагрузке из-за утечек памяти.
  2. В качестве wsgi-сервера взял uwsgi, потому что во время тестов этот сервер показал хорошую скорость.
  3. nginx и uwsgi ставим из исходников.

Что нужно сделать:

  1. Подготовить конфигурационные файлы для nginx, uwsgi, monit, pylons-приложения.
  2. Подготовить файл, который запускается под рутом, добавляет пользователя, устанавливает пакеты.
  3. Подготовить файл, который запускается под пользователем и компилирует uwsgi.

Что получаем в итоге:

  1. Набор конфигурационных файлов.
  2. Два скрипта, которые нужны для разворачивания сервера.

Далее идут файлы из расчёта, что приложение было создано командой:

paster create -t pylons tinycode

Настройка nginx

Файл настроек - nginx.conf:

worker_processes  1;

events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  0;

    server {
        listen       80;
        server_name  tinycode.ru;
	charset	utf-8;
	root /home/tinyuser/tinycode.ru/tinycode/public;

	location ~*/(pdf|css|js|png|jpg|gif|jpeg|bmp|JPG)/ {
		root   /home/tinyuser/tinycode.ru/tinycode/public;
		expires	max;
		add_header	Cache-Control "public";
		break;
	}

	location / {
		uwsgi_pass	0.0.0.0:3993;
		include	uwsgi_params;
		uwsgi_param	SCRIPT_NAME "";
	}

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

Теперь нужен скрипт для запуска nginx, этот скрипт я честно упёр с одного сайта, о чём сообщают копирайты в коментах. Исходный скрипт расчитан на дефолтную установку nginx, для работы с собранным вручную сервером подправил пару путей, в остальном скрипт тот же. Назовём его nginx:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: nginx init.d dash script for Ubuntu <=9.10.
# Description:       nginx init.d dash script for Ubuntu <=9.10.
### END INIT INFO
#------------------------------------------------------------------------------
# nginx - this Debian Almquist shell (dash) script, starts and stops the nginx 
#         daemon for ubuntu 9.10 and lesser version numbered releases.
#
# description:  Nginx is an HTTP(S) server, HTTP(S) reverse \
#               proxy and IMAP/POP3 proxy server.  This \
#		script will manage the initiation of the \
#		server and it's process state.
#
# processname: nginx
# config:      /usr/local/nginx/conf/nginx.conf
# pidfile:     /acronymlabs/server/nginx.pid
# Provides:    nginx
#
# Author:  Jason Giedymin
#          .
#
# Version: 2.0 02-NOV-2009 jason.giedymin AT gmail.com
# Notes: nginx init.d dash script for Ubuntu <=9.10.
# 
# This script's project home is:
# 	http://code.google.com/p/nginx-init-ubuntu/
#
#------------------------------------------------------------------------------
#                               MIT X11 License
#------------------------------------------------------------------------------
#
# Copyright (c) 2009 Jason Giedymin, http://Amuxbit.com formerly
#				     http://AcronymLabs.com
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
#                               Functions
#------------------------------------------------------------------------------
. /lib/lsb/init-functions

#------------------------------------------------------------------------------
#                               Consts
#------------------------------------------------------------------------------
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/local/nginx/sbin/nginx

PS="nginx"
PIDNAME="nginx"				#lets you do $PS-slave
PIDFILE=$PIDNAME.pid                    #pid file
PIDSPATH=/usr/local/nginx/logs

DESCRIPTION="Nginx Server..."

RUNAS=root                              #user to run as

SCRIPT_OK=0                             #ala error codes
SCRIPT_ERROR=1                          #ala error codes
TRUE=1                                  #boolean
FALSE=0                                 #boolean

lockfile=/var/lock/subsys/nginx
NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"

#------------------------------------------------------------------------------
#                               Simple Tests
#------------------------------------------------------------------------------

#test if nginx is a file and executable
test -x $DAEMON || exit 0

# Include nginx defaults if available
if [ -f /etc/default/nginx ] ; then
        . /etc/default/nginx
fi

#set exit condition
#set -e

#------------------------------------------------------------------------------
#                               Functions
#------------------------------------------------------------------------------

setFilePerms(){

        if [ -f $PIDSPATH/$PIDFILE ]; then
                chmod 400 $PIDSPATH/$PIDFILE
        fi
}

configtest() {
	$DAEMON -t -c $NGINX_CONF_FILE
}

getPSCount() {
	return `pgrep -f $PS | wc -l`
}

isRunning() {
        if [ $1 ]; then
                pidof_daemon $1
                PID=$?

                if [ $PID -gt 0 ]; then
                        return 1
                else
                        return 0
                fi
        else
                pidof_daemon
                PID=$?

                if [ $PID -gt 0 ]; then
                        return 1
                else
                        return 0
                fi
        fi
}

#courtesy of php-fpm
wait_for_pid () {
        try=0

        while test $try -lt 35 ; do

                case "$1" in
                        'created')
                        if [ -f "$2" ] ; then
                                try=''
                                break
                        fi
                        ;;

                        'removed')
                        if [ ! -f "$2" ] ; then
                                try=''
                                break
                        fi
                        ;;
                esac

                #echo -n .
                try=`expr $try + 1`
                sleep 1
        done
}

status(){
	isRunning
	isAlive=$?

	if [ "${isAlive}" -eq $TRUE ]; then
                echo "$PIDNAME found running with processes:  `pidof $PS`"
        else
                echo "$PIDNAME is NOT running."
        fi


}

removePIDFile(){
	if [ $1 ]; then
                if [ -f $1 ]; then
        	        rm -f $1
	        fi
        else
		#Do default removal
		if [ -f $PIDSPATH/$PIDFILE ]; then
        	        rm -f $PIDSPATH/$PIDFILE
	        fi
        fi
}

start() {
        log_daemon_msg "Starting $DESCRIPTION"
	
	isRunning
	isAlive=$?
	
        if [ "${isAlive}" -eq $TRUE ]; then
                log_end_msg $SCRIPT_ERROR
        else
                start-stop-daemon --start --quiet --chuid $RUNAS --pidfile $PIDSPATH/$PIDFILE --exec $DAEMON \
                -- -c $NGINX_CONF_FILE
                setFilePerms
                log_end_msg $SCRIPT_OK
        fi
}

stop() {
	log_daemon_msg "Stopping $DESCRIPTION"
	
	isRunning
	isAlive=$?
        if [ "${isAlive}" -eq $TRUE ]; then
                start-stop-daemon --stop --quiet --pidfile $PIDSPATH/$PIDFILE

		wait_for_pid 'removed' $PIDSPATH/$PIDFILE

                if [ -n "$try" ] ; then
                        log_end_msg $SCRIPT_ERROR
                else
                        removePIDFile
	                log_end_msg $SCRIPT_OK
                fi

        else
                log_end_msg $SCRIPT_ERROR
        fi
}

reload() {
	configtest || return $?

	log_daemon_msg "Reloading (via HUP) $DESCRIPTION"

        isRunning
        if [ $? -eq $TRUE ]; then
		`killall -HUP $PS` #to be safe

                log_end_msg $SCRIPT_OK
        else
                log_end_msg $SCRIPT_ERROR
        fi
}

quietupgrade() {
	log_daemon_msg "Peforming Quiet Upgrade $DESCRIPTION"

        isRunning
        isAlive=$?
        if [ "${isAlive}" -eq $TRUE ]; then
		kill -USR2 `cat $PIDSPATH/$PIDFILE`
		kill -WINCH `cat $PIDSPATH/$PIDFILE.oldbin`
		
		isRunning
		isAlive=$?
		if [ "${isAlive}" -eq $TRUE ]; then
			kill -QUIT `cat $PIDSPATH/$PIDFILE.oldbin`
			wait_for_pid 'removed' $PIDSPATH/$PIDFILE.oldbin
                        removePIDFile $PIDSPATH/$PIDFILE.oldbin

			log_end_msg $SCRIPT_OK
		else
			log_end_msg $SCRIPT_ERROR
			
			log_daemon_msg "ERROR! Reverting back to original $DESCRIPTION"

			kill -HUP `cat $PIDSPATH/$PIDFILE`
			kill -TERM `cat $PIDSPATH/$PIDFILE.oldbin`
			kill -QUIT `cat $PIDSPATH/$PIDFILE.oldbin`

			wait_for_pid 'removed' $PIDSPATH/$PIDFILE.oldbin
                        removePIDFile $PIDSPATH/$PIDFILE.oldbin

			log_end_msg $SCRIPT_ok
		fi
        else
                log_end_msg $SCRIPT_ERROR
        fi
}

terminate() {
        log_daemon_msg "Force terminating (via KILL) $DESCRIPTION"
        
	PIDS=`pidof $PS` || true

	[ -e $PIDSPATH/$PIDFILE ] && PIDS2=`cat $PIDSPATH/$PIDFILE`

	for i in $PIDS; do
		if [ "$i" = "$PIDS2" ]; then
	        	kill $i
                        wait_for_pid 'removed' $PIDSPATH/$PIDFILE
			removePIDFile
		fi
	done

	log_end_msg $SCRIPT_OK
}

destroy() {
	log_daemon_msg "Force terminating and may include self (via KILLALL) $DESCRIPTION"
	killall $PS -q >> /dev/null 2>&1
	log_end_msg $SCRIPT_OK
}

pidof_daemon() {
    PIDS=`pidof $PS` || true

    [ -e $PIDSPATH/$PIDFILE ] && PIDS2=`cat $PIDSPATH/$PIDFILE`

    for i in $PIDS; do
        if [ "$i" = "$PIDS2" ]; then
            return 1
        fi
    done
    return 0
}

case "$1" in
  start)
	start
        ;;
  stop)
	stop
        ;;
  restart|force-reload)
	stop
	sleep 1
	start
        ;;
  reload)
	$1
	;;
  status)
	status
	;;
  configtest)
        $1
        ;;
  quietupgrade)
	$1
	;;
  terminate)
	$1
	;;
  destroy)
	$1
	;;
  *)
	FULLPATH=/etc/init.d/$PS
	echo "Usage: $FULLPATH {start|stop|restart|force-reload|status|configtest|quietupgrade|terminate|destroy}"
	echo "       The 'destroy' command should only be used as a last resort." 
	exit 1
	;;
esac

exit 0

Настройка uwsgi

Скрипт, запускающий непосредственно сервер run-uwsgi.sh (этот скрипт запускается из под пользователя, не из под рута):

#!/bin/sh

cd /home/tinyuser/tinycode.ru

# run uwsgi server
/home/tinyuser/uwsgi/uwsgi -p 4 --paste config:/home/tinyuser/tinycode.ru/tinyuser.ini --socket :3993 > /dev/null 2>&1 &

Скрипт для запуска и остановки uwsgi:

#!/bin/sh

case $1 in
        start)
                echo `exec 2>&1 su tinyuser -c /home/tinyuser/run-uwsgi.sh`;
                ps -C uwsgi -o pid= | sed s/\ //g > /var/run/uwsgi.pid
                ;;
        stop)
                killall uwsgi ;;
        *)
                echo "usage: uwsgi {start|stop}" ;;
esac
exit 0

Настройка monit

Создадим конфиг для monit - monitrc:

## Start monit in the background (run as a daemon):

set daemon  300           
with start delay 240   

# admin settings
set mailserver localhost
set alert alert.mail@tinycode.ru

# check uwsgi
check process uwsgi with pidfile /var/run/uwsgi.pid
        start program "/etc/init.d/uwsgi start"
        stop program "/etc/init.d/uwsgi stop"

# check nginx
check process nginx with pidfile /var/run/nginx.pid
  start program = "/etc/init.d/nginx start"
  stop program  = "/etc/init.d/nginx stop"

check system localhost
if loadavg (1min) > 4 then alert
if loadavg (5min) > 2 then alert

И ещё один файл - monit:

# Defaults for monit initscript
# sourced by /etc/init.d/monit
# installed at /etc/default/monit by maintainer scripts
# Stefan Alfredsson 

# You must set this variable to for monit to start
startup=1

# To change the intervals which monit should run,
# edit the configuration file /etc/monit/monitrc
# It can no longer be configured here.

Настройка pylons-приложения

У каждого свой конфиг, я запишу свои настройки в tinyuser.ini

Компиляция uwsgi

Для того, чтобы подготовить uwsgi нужен будет файл install-uwsgi.sh:

#!/bin/sh

#run this code under tinyuser

cd ~
wget http://projects.unbit.it/downloads/uwsgi-0.9.7.1.tar.gz
tar xzf uwsgi-0.9.7.1.tar.gz
mv uwsgi-0.9.7.1 uwsgi
cd uwsgi
make

Что получилось

В результате всех манипуляций должен получиться следующий набор файлов:

  1. nginx.conf
  2. nginx
  3. run-uwsgi.sh
  4. uwsgi
  5. monitrc
  6. monit
  7. tinyuser.ini
  8. install-uwsgi.sh

Осталось подготовить файл, который установит пакеты и пропишет демонов в автозагрузку.

install-server.sh

#!/bin/sh
# the default folder with pylons project = /home/tinyuser/tinycode.ru

RUN_USER=tinyuser
adduser $RUN_USER

apt-get update

#install soft
apt-get install -y vim python-setuptools

#install python libs
easy_install "Pylons == 0.9.7"
easy_install "SQLAlchemy >= 0.5"
easy_install "TurboMail == 3.0.3"
easy_install "AuthKit >= 0.4.3, <= 0.4.99"
easy_install uwsgi

apt-get install -y build-essential python-numpy python-scipy python-reportlab python-reportlab-accel python-renderpm libfreetype6 python-dev python-imaging libxml2-dev libpcre3-dev sqlite3 libpcre3-dev libssl-dev

# install nginx
mkdir soft
cd soft

wget http://sysoev.ru/nginx/nginx-0.9.6.tar.gz
tar xzf nginx-0.9.6.tar.gz
cd nginx-0.9.6
./configure && make && make install

cd ../../

cp nginx.conf /usr/local/nginx/conf/
cp nginx /etc/init.d/
/usr/sbin/update-rc.d -f nginx defaults 

cp uwsgi /etc/init.d/
chmod 0755 /etc/init.d/uwsgi
/usr/sbin/update-rc.d -f uwsgi defaults 

# install ftp
# apt-get install -y vsftpd

# install monit
apt-get install -y monit
cp monitrc /etc/monit/
chmod 0600 /etc/monit/monitrc
cp monit /etc/default/
chmod 0644 /etc/default/monit
/etc/init.d/monit restart


# copy files for uwsgi server to user dir
cp install-uwsgi.sh /home/$RUN_USER/
cp run-uwsgi.sh /home/$RUN_USER/
cp tinyuser.ini /home/$RUN_USER/

chown $RUN_USER /home/$RUN_USER/install-uwsgi.sh
chown $RUN_USER /home/$RUN_USER/run-uwsgi.sh
chown $RUN_USER /home/$RUN_USER/tinyuser.ini

echo "login under $RUN_SERVER and run install-uwsgi.sh"

Разворачиваем сервер:

  1. Под рутом запустить install-server.sh
  2. Под пользователем tinyuser запустить install-uwsgi.sh
  3. В папку /home/tinyuser/tinycode.ru скопировать файлы приложения. Это те файлы, которые лежат в папке tinycode после создания проекта командой
    paster create -t pylons tinycode
  4. Скопировать файл tinyuser.ini в /home/tinyuser/tinycode.ru
  5. Перезагружаем сервер.

Ещё была идея запихивать архив с проектом вместе с этими файлами и автоматически его распаковывать, но отказался от этой идеи, потому что проекты могут быть разными, могут заливаться разными людьми, а моя цель была в настройке именно сервера, а не в деплое приложения.

На этом всё, до новых встреч :)

Письма счастья завтрашенго дня :)

Спросили у программиста:
- Ты чем на работе занимаешься?
- Программирую под виндовс.
- Понятно, а расслабляешься как?
- Программирую под линукс.

Мне, как и тому программисту из анекдота, захотелось расслабиться. Ну и чтобы совсем было весело решил написать на python'е небольшой скрипт, который будет делать письма счастья вида:

ЭтО ~ вСеМи ~ ЛюБиМоЕ ~ пИсЬмО ~ сЧаСтЬя!

(ну да, скучно мне было!)

Ну чтобы письма счастья радовали всех, добавил поддержку юникода и чтения текста из файла.


#!/usr/bin/env python

def makeChainLetter(src):
    text = src.decode('utf-8')
    funny = ''
    isLower = False
    for char in text:
        if char == ' ':
            funny += ' ~ '
        else:
            if isLower:
                funny += char.lower()
            else:
                funny += char.upper()

            isLower = not isLower
            
    return funny

def usage():
    print '''
Make chain letter from string:
    chainletter.py TEXT
    chainletter.py -s TEXT

Make chainletter from file:
    chainletter.py -f FILENAME

Show this help:
    chainletter.py -h '''
    
if __name__ == '__main__':
    import sys
    
    try:
        first = sys.argv[1]
        if first == '-f':
            filename = sys.argv[2]
            fHandle = open(filename, 'rt')
            text = fHandle.read()
            fHandle.close()
        elif first == '-s':
            text = sys.argv[2]
        elif first == '-h':
            usage()
            sys.exit(0)
        else:
            text = first
        
    except IndexError:
        usage()
        sys.exit(1)

    print makeChainLetter(text)

СчАсТьЯ ~ вСеМ! :)