Всем привет. На связи Сергей Окатов. Я руковожу отделом разработки в компании Datana, а также являюсь руководителем курсов Kotlin Backend Developer и Kotlin Developer. Basic в OTUS.

В компании я отвечаю за работу команды разработчиков. Команда небольшая -  всего 6 разрабов, но за последний год с небольшим мы с нуля разработали и внедрили пять проектов. Причем это были не детские проектики, а вполне промышленные проекты, которые сейчас начинают свою работу на металлургическом заводе и интегрированы со сталеплавильными установками*. Много это или мало? Чаще всего, от запуска проекта до его внедрения проходит примерно год-два. А тут средняя скорость разработки получается примерно проект за два-три месяца.

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


Мы в команде применяем для бэкенд-разработки Kotlin и Python. Эти два языка - антиподы друг друга. От правильного распределения задач, возложенных на них, зависит ключевой момент: уложимся ли мы в сроки или контракт будет сорван.

Почему вообще два языка, а не один?

Как я уже указал выше, эти два языка - антиподы друг друга. У меня вообще не укладывается в голове как они могут конкурировать друг с другом в каких-то командах. Заточены Kotlin и Python под совершенно разные задачи.

Возьмем Python. Это очень старый язык, который заметно старше даже Java, не говоря уже о Kotlin. Сам в себе он содержит следы смены эпох, и на эти следы постоянно натыкаешься. За всю свою 31-летнюю историю он не пользовался особой популярностью, решая лишь какие-то нишевые задачи. Взрывной рост его популярности случился лишь в последние лет 10 и связан был с прорывом в машинном обучении. Благодаря этому росту Python прорвался во многие смежные экосистемы, например, обработка видео.

Kotlin, напротив - возник лишь 10 лет назад, но гигантскими темпами набирает популярность. В себя он вобрал все лучшие практики опробованные на данный момент, активно поглощает в себя экосистему Java за счет интероперабельности с JVM, а также начал это делать с экосистемами JavaScript и C/C++ за счет интероперабельности с ними в Kotlin Multiplatform. Да, Kotlin явно демонстрирует амбиции в выдавливании Python из темы машинного обучения, но пока говорить о каких-то успехах в этом направлении очень рано.

При выборе задач для Питона и Котлина я выделил три ключевые особенности, которые в итоге и определили архитектуру разработанных систем.

  1. Типизация. Python характеризуется динамической типизацией, т.е., создав переменную x, ей в любой момент можно присвоить хоть целое, хоть строку, хоть любой объект. Kotlin же весь построен на парадигме статической типизации. Т.е., объявив переменную x с типом String, мы уже не сможем ей присвоить ничего кроме строки.

  2. Время старта. Не смотря на то, что Python при старте выполняет компиляцию, программы на нем стартуют гораздо быстрее, чем JVM.

  3. Экосистема. Kotlin, конечно - это очень молодой язык, но он вырос на базе экосистемы JVM. А JVM и Python - это довольно старые и развитые экосистемы. Большую часть инструментов поддерживают обе. Тем не менее, всегда существуют нюансы. В некоторых проектах больше поддерживается Python и меньше JVM, в других - наоборот. Поэтому, каждый раз приходится взвешивать все “за” и “против”, делая выбор в пользу того или иного языка на каждом новом микросервисе.

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

Типизация

Возьмем простую программу:

Этот код отлично работает вплоть до последней 9-й строки. На ней он отдает вот такую ошибку:

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

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

Вторая проблема динамической типизации - это подсказки IDE. Если явно не задан тип объекта, то и отследить его текущий тип очень часто невозможно. Раз тип неизвестен, то и подсказать доступные методы для переменной тоже невозможно. В таком случае, IDE уже не работает как помощник разработчика, а просто служит текстовым редактором.

Далеко не всегда динамическая типизация создает какие-то проблемы. Если программа небольшая и разработчик способен отследить все изменения типов, то работать с динамическими типами гораздо проще, чем явно описывать все классы и типы.

Но, если ваша программа разрастается до больших размеров, то тут уже IDE часто сдается, перестает помогать и уходит много времени на выяснение типов и доступных методов. Также возникают постоянные проблемы с типами, которые никак не отследить и можно выявить только с помощью модульного тестирования. Модульное тестирование - это нужная и полезная вещь, но динамическая типизация предъявляет повышенные требования к нему и требует большего покрытия тестами кода. И чем больше проект, тем будут больше издержки на отладку и поддержку.

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

Для оценки могу привести грубый пример. Предположим, у нас код построен на динамической типизации. В программе используется целое, строка и два класса. Также возможен еще и None (Null). Если мы сделаем функцию с 10 аргументами, то в самом общем (возможно параноидальном) случае нам нужно будет учитывать влияние аргументов друг на друга и мы должны будем предусмотреть количество вариантов: 10 аргументов, по 5 вариантов каждый, итого 5 в 10-й степени = 9 765 625. Очевидно, что никто не будет делать такое количество проверок, а непредусмотренные варианты окажутся просто потенциальными багами.

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

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

Отдельно хочется упомянуть Java. Она тоже обладает статической типизацией, но мы выбрали Kotlin. Все дело в том, что, если забыть про большое количество сахара, Kotlin предоставляет еще и большее количество таких вот проверок. Например, нет в Java настолько развитой работы с Null. Также Kotlin более строг при преобразованиях перечислимых (Enum), требуя явно рассматривать все возможные варианты, либо использовать else. Более чувствителен Kotlin к мутабельности переменных и пр. Да, все эти строгости и проверки требуют повышенного внимания и квалификации разработчика, но позволяют прямо во время написания кода выявлять огромное количество вариантов, ветвления логики, что избавляет нас от большей части ошибок еще даже до компиляции, не говоря уж о выкате в прод.

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

Время старта

Далеко не всегда время старта является критическим. Чаще всего, микросервис один раз запускается и успешно функционирует месяцами. Но все-таки иногда время старта важно. Например, когда нагрузка на систему неожиданно возросла и планировщик принял решение поднять еще один под. Холодный старт в этом случае сколько займет времени? Одну секунду или минуту? Питон, очевидно, в этом плане явно лидирует.

Но и в мире JVM можно кое-что сделать для повышения времени старта. Например, очень сильно может помочь отказ от Spring Framework в пользу более современных разработок. Например, в Ktor не используются прокси-классы и вообще рефлексия. Это позволяет стартовать приложению в 5-10 раз быстрее, чем приложению на Spring.

Экосистема

Как я уже указал выше, есть масса нюансов в каждой экосистеме. Например, Java-обертки для PyTorch или Tensorflow все-таки не так развиты как их Python-версии. Да и найти инженера по машинному обучению/Data scientist-а, готового освоить Java/Kotlin не так просто. Большинству DS-ов просто удобнее работать на Питоне и вряд ли стоит нам - разработчикам системы - с этим что-то делать.

Видео-подсистему с gstreamer и opencv разрабатывать на Java/Kotlin тоже можно, но будет это заметно труднее, чем на Python.

С другой стороны, например, для интеграций есть изумительный Java-фреймворк Apache Camel, который покрывает более 90% всех потребностей в интеграциях с внешними системами. Но при этом, например, эмулятор контроллеров Siemens S7 написан на Python и ничего подобного в JVM стеке не сделать. Так что микросервисная архитектура и сочетание языков нередко - это не блажь, а суровая необходимость.

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

Распределение обязанностей

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

Адаптеры - Kotlin

Это микросервисы, которые отвечают за интеграцию с внешними системами. Их обязанность - принять данные, провалидировать, преобразовать во внутренние форматы системы и отправить дальше.

Фреймворк Apache Camel и статическая типизация Kotlin в данном случае обеспечивают максимальную скорость разработки с учетом довольно сложных структур данных, с которыми приходится работать.

Видеоподсистема и Машинное обучение - Python

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

Логические микросервисы - Kotlin

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

И вот в этих условиях лучшим образом показывает себя именно Kotlin. Да, формирование статических типов и классов требует значительного времени. Но формирование этих структур - это и есть та самая бизнес-логика, которую мы должны разработать. Формируя статические типы, мы отсекаем тысячи вариантов других возможных вариантов, которые в Python нам бы пришлось проверять вручную и с помощью модульных тестов. И когда структура классов сформирована, Kotlin даже на этапе набора текста программы обеспечивает подсказки IDE. Затем на этапе компиляции автоматически выявляются ошибки и несогласованности. Например, если у нас изменится API, то мы обнаружим ошибки не в рантайме на продуктовой площадке, а еще на этапе компиляции программ.

И именно благодаря Kotlin большая часть ошибок, которые мы исправляли в ходе тестирования и внедрения, носила именно бизнесовый характер, а не системный типа фатального падения программы из-за Null Pointer Exception. Честно говоря, за весь период работы NPE мы практически не видели.

Вспомогательные сервисные скрипты - Python

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

Итоги

JVM и Python - это две наиболее популярные экосистемы в бэкенд-разработке на сегодня. Знаю, что во многих компаниях эти два инструмента конкурируют друг с другом, что выражается в подходах “Все на Python” или “Все на Java”, но я считаю, что конкуренция здесь неуместна. Эти два языка очень разные и занимают совершенно разные рыночные ниши. Python больше подходит для прототипирования, а Kotlin - для крупной разработки со сложной и нагруженной бизнес-логикой. Сочетание преимуществ каждого из языков дает сочетание высоких темпов разработки и высокого качества кода.

Ссылки

Комментарии (13)


  1. ya_ne_znau
    01.12.2021 20:59
    +2

    фнукцию с 10 аргументами

    после этого я засомневался в способностях автора.

    Что дальше, делать вместо одного объекта кучу "полей" по типу "file_name", "file_type", "file_path", "file_open_mode" вместо "file" со всем внутри?


    1. rjhdby
      02.12.2021 12:39
      +2

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

      > ВНИМАНИЕ!!! Мы не делаем так в проде, это просто доведённый до крайности пример, на основе которого мы хотим показать проблему
      ?


  1. dplsoft
    01.12.2021 21:44
    +1

    а, собственно, как совмещать питон и котлин в одном проекте? какова архитектура такого проекта? как огранизована связь между подсистемами / модулями написанными на разных языках? не хочу обижать автора, но заголовок "немного намекает" именно на такое содержимое статьи...

    было бы интересно узнать есть ли что-то новое по данному вопросу.

    потому что единственное известное мне правильное решение , не менявшееся уже лет 5-8, связанное с "совмещением этих языков", когда надо делать акцент на свойствах этих 2-х языков языков (с моей точки зрения) - ... это какой нибудь JPython. дада "The Java Scripting API", который, я думаю, доступен и в котлине, с помощью которого можно в рантайме, без пересборки менять поведение уже скомпиленной с собранной в байткод системы.

    это, имхо, единственный случай, когда нам в принципе важно на каком языке написаны модули, и когла надо рассматривать описанные в статье свойства язвков. ( и если вдруг автор напишет о том, как отлаживать/трейсить скрипты запущенные через "Java Scripting API" - желательно на тестовом стенде или даже проде (если заказчику даны возможности настраиватт скрипты), а не на стенде разработчика - вот это будет очень интересно и соответсвовать названию. имхо.)

    во всех остальных случаях, совершенно не важно на каком языке написана подсистема/компонент - важно "как нам увязать эти 2 программы вместе". а будет это на котлине, джаве, питоне, пхп, c++ или ещё как... - это уже совершенно не важно, или это становится проблемой команды которая должна адаптировать этот продукт/решение доя укладки в систему.

    имхо.

    //кстати, автор не упомянул проблемы рефакторинга больших проектов на языках с динамической типизацией (я видел упоминание про "подсказки в ide", но этою имхо, не очень большой кусочек ситуации), необходимость наличия четко описанного стандарта на язык (привет тебе ruby с движками, работаюшими чуть по разному на разных платформах (поправьте меня? может за 4 года что ть и поменялось?)), и проблемы обратной совместимости, которые могут привести к неработоспособности проекта при попытке сборки на новых версиях компилятора (привет тебе питон 2/ питон 3. и сравните это с качеством обратной совместимости той же java.)

    но конечные выводы автора, о разных нишах языков с динамической и статической типизацией - полностью поддерживаю.


    1. beezy92
      02.12.2021 19:50

      Судя по статье, они создали и внедрили 5 проектов за последний год. Некоторые проекты могли быть на Python, некоторые на Kotlin. К тому же, в статье описывается, что при микросервисной архитектуре, в зависимости от Domain, микросервис можно реализовывать на любом языке, зависит от задач, и имеются ли библиотеки, для решения. Пример же автор приводит интеграции, что здесь лучше всего использовать Apache Camel.

      То есть, он не пишут на разных языках, в одном проекте, в одном монорепозитории, в разных модулях и тд. А на уровне микросервисов, или же проектов.


    1. svok Автор
      03.12.2021 15:26

      Америку не открывали. Просто разные микросервисы. Какие - описано в "Распределение обязанностей". Совмещение тоже просто - OpenAPI, который тоже упомянут в тексте.

      Проекты - это отдельные бизнес-проекты. Каждый проект состоял примерно из десятка микросервисов.


  1. KoteMilote
    01.12.2021 22:52

    Знаю что Тензор кодит на питоне, проекты там должны быть огромные, как тогда они выживают если всё так сложно?

    Вопрос: почему существуют крупные проекты на питоне, если с ним так сложно?

    ПС: я сам из Java


    1. beezy92
      02.12.2021 19:52

      Тензор это Tensorflow? Он написан в основном на С++. И у него есть реализации API под другие языки программирования, и самый популярный из них - Python. По сути, Python выступает в роли Wrapper.


      1. KoteMilote
        02.12.2021 21:04

        Тензор это компания, которая выпускает булгарский софт - СБИС, например. И вэб получается высоконагруженый бэк.

        Наверное есть и другие проекты, бэк которых написан на Питоне. Вот у меня и вопрос: почему пишут бэк на Питоне, если с Питоном такие проблемы, как описал их автор?


        1. beezy92
          03.12.2021 05:31
          +1

          Instagram написан на Python и Django. Хоть у Python и есть проблемы с CPU-intensive задачами, но зачастую, в веб приложениях, ориентированных на контент, основная нагрузка это - I/O intensive задачи. К тому же, большая часть проблем, решается за счет уровней кеша, CDN и тд.

          ЗЫ: основная часть DropBox также на Python, но на 2-й версии. И сам Гвидо долгое время работал на них.


  1. Naf2000
    02.12.2021 07:03

    Если мы сделаем функцию с 10 аргументами, то в самом общем (возможно параноидальном) случае нам нужно будет учитывать влияние аргументов друг на друга и мы должны будем предусмотреть количество вариантов: 10 аргументов, по 5 вариантов каждый, итого 10 в 5-й степени = 100 000

    Только несколько наоборот: 5^{10}= 9765625


    1. svok Автор
      02.12.2021 07:30

      Точно, спасибо, перепутал


  1. iALVIN_you77
    03.12.2021 15:18

    Про статичную типизацию. Есть же mypy.


    1. ndrwK
      03.12.2021 19:08

      Только если все самому писать с нуля. Поскольку либы, выдерживающие проверку mypy, можно по пальцам пересчитать.