Знакомство с Julia

Введение

Недавно появилось свободное время и я решил попробовать освоить что-нибудь новое. Мой выбор пал на язык программирования Julia, так как язык позиционируется как замена Python, R в области анализа данных, в которой есть полноценные параллельные вычисления (в отличие от Python, где есть GIL). Кроме того после изучения информации оказалось, что Julia может компилироваться в статическую библиотеку или бинарник (однако не все так радужно в действительности).

Для знакомства я придумал простенькое приложение, которое содержит url для получения данных данных из БД, агрегации их в приложении и вывод результат в виде json.

Так же я приведу сравнение производительности и потребляемых ресурсов с аналогичным приложением на python c использованием популярной аналитической библиотеки Pandas.

Создание проекта

В терминах Julia приложение или библиотека называются пакет (package), а менеджер пакетов - Pkg.

Каждый пакет содержит файлы Project.toml(в котором задается имя пакета, автор, uuid и зависимости) и src/ProjectName.jl (в котором содержится код приложения).

Для создания пакета необходимо выполнить следующую последовательность команд:

  • Запустить julia julia
  • Перейти в режим управления пакетами нажатием ]
  • Ввести команду generate <имя нового пакета>, где PackageName - имя нового пакета
  • Активировать окружение `activate <имя созданного пакета>. Это нужно для того чтобы установленные библиотеки попадали в файл Project.toml, относящийся к нашему проекту.

Установка зависимостей

После того, как мы создали пакет и активировали для него окружение нужно установить пакеты которые нам понадобятся. Для этого нужно выполнить команду

add <имя пакета>

В моем приложении использовались следующие пакеты:

Все библиотеки ставятся без проблем, кроме LibPQ, так как она является враппером для родной библиотеки libpq. После установки пакета:

add LibPQ
add BinaryProvider

Ошибок не было, но при этом библиотека не установилась до конца и использвать её было нельзя так как выпадала ошибка:

ERROR: LoadError: InitError: ArgumentError: Unknown time zone "Europe/Moscow"

Эта проблема решилась просто: нужно было добавить пакет TimeZones и установить локальную зону:

TimeZones.localzone()

после этого я попробовал запустить скрипт, но получил ошибку:

ERROR: LoadError: could not open file /Users/kin/.julia/packages/LibPQ/WrMcC/deps/deps.jl

После поиска я нашел issue на эту тему, но пересборка библиотеки не сработала.

Я стал копать дальше и нашел еще issue, прочитав его я попробовал выполнить команду:

include("/Users/kin/.julia/packages/LibPQ/WrMcC/deps/build.jl")

И это сработало и библиотека запустилась.

Команда include код из файл интерпретаторе.

Упрощенная установка пакетов

Кроме описанного выше сценария для установки зависимостей можно использовать упрощенный вариант. А именно в файле модуля добавить:

usage Pkg; Pkg.Add(<имя пакета>)

А чтобы они прописались в нужный файл проекта интерпретатор Julia нужно запускать с опцией --project

Работа и обработка набора данных

Когда все библиотеки установлены, можно перейти к получению данных и их агрегации. В папку src проекта добавим файл (например postgres.jl) следующего содержания:

using DataFrames, LibPQ, Statistics

conn = LibPQ.Connection("dbname=example")

function getPgData()::DataFrame
    @info "Get data"
    df = @time DataFrame(execute(conn, "SELECT client,packet_id,navigate_date,nsat FROM table"))

    @info "Aggregate data"
    result = @time by(df, :client, :packet_id => length, :navigate_date => minimum, :navigate_date => maximum, :nsat => mean)

    return result
    end;

Для начала мы импортируем нужные библиотеки, затем подключаемся к локальной БД и получаем из нее данные (~550K записей) и преобразуем их в DataFrame, а затем считаем агрегации по разным полям.

Кроме того в данном примере используются 2 макроса @info для логгирования и @time для измерения времени выполнения. Надо отметить что после выполнения @time переменной будет присвоен результат работы функции а не время её работы. Если нужно сохранить время выполнения в переменную нужно использовать макрос @elapse.

Преобразование DataFrame в json

После того как мы получили данные и провели с ними все манипуляции необходимо преобразовать их в json чтобы web приложение смогло их отдавать. После поисков в Интернете я нашел сниппет который преобразует DataFrame в json, я не стал брать его целиком, а только взял функцию которая мне нужна а именно df2json. Однако если ее запустить как указано в сниппете, то интерпретатор будет ругаться на устаревшие функции у DataFrame, поэтому я её слегка модернизировал и вынес в отдельный файл utils.jl:

function df2json(df::DataFrame)
  len = length(df[:, 1])
  indices = names(df)
  jsonarray = [Dict([string(index) => (isna(df[!, index][i]) ? nothing : df[!, index][i])
                                                    for index in indices])
                                                          for i in 1:len]
  return JSON.json(jsonarray)
end

Тут надо отметить одну особенность что к колонке в DataFrame можно обратиться 2-мя способами: df[!, col] и df[:, col].

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

Создание http api

Когда есть функции для обработки и преобразования данных можно завернуть их в эндпоинт на web сервисе. Для этого в основном файле модуля добавим код:

module AnalyticsService

using HTTP, Sockets, Dates

include("postgres.jl")
include("utils.jl")

function getPoints(req::HTTP.Request)::HTTP.Response
    result = getPgData()

    @info "Marshall time"
    resp = @time df2json(result)

    return HTTP.Response(200, resp)
    end;

const ROUTER = HTTP.Router()
HTTP.@register(ROUTER, "GET", "/api", getPoints)

@info "Run server"
HTTP.serve(ROUTER, Sockets.localhost, 8081)

end # module

В этом коде подключаются созданные выше файлы, затем объявляем функцию для обработки запросов от клиента, затем настраиванием роутинг и, наконец, запускаем web сервис.

Замер производительности и сравнение с Python

Сравнение скорости работы в секундах:

Операция Julia Python
Общее время обработки запроса 189.918 17.238
Получение и преобразование данных в df 165.57840 17.047672
Агрегация данных 15.187950 0.151542
Преобразование df в json 2.421602 0.0024690

При замере использовались только функции из документации без улучшения производительности.

Как видно по производительности Julia уступает питон очень существенно, хотя, возможно, на это очень сильно влияет реализация библиотек.

Так же скорее всего если заняться тюнингом то можно добиться аналогичных с python результатов, но тут встает вопрос зачем…

Заключение

В целом язык оставил смешанные впечатления с одной. Особенно расстроило что одно из основных его преимуществ, а именно статическая сборка, у меня не удалась, видимо по причине присутствия бинарной библиотеки, но в целом я так и не смог с этим разобраться.

Так не очень хорошее впечатление оставил инструментарий по разработке. Многие рекомендуют плагин к Intelij IDEA, но мне в нем не хватало нормальной отладки, да и переход по функциям тоже не заработал.

Документирование библиотек в целом не плохое, но для не которых (привет LibPQ.jl) оставляет желать лучшего.

По скорости тоже много вопросов так так подобный сервис на pandas отрабатывает в разы быстрее.

Так что для своих задач применение julia я вряд ли найду.

Код сервисов и данные можно найти в репозитории на GitHub

Ссылки

  1. Julia programming language
  2. Julia’s package manager
  3. JSON to dataframe input and output
 
comments powered by Disqus