Сегодня мы немного расскажем вам о работе IT-поддержки в Ozon Tech: что мы делаем и зачем, как используем Python, и как именно он нам помогает решать рутинные проблемы и не только.

- Моральная поддержка мне не помешает.
- Вот и отлично. Но я сразу предупреждаю: я не могу гарантировать моральность моей поддержки. ©

Для начала, пожалуй, стоит рассказать, что представляет собой IT-поддержка в Ozon Tech, а точнее о работе специалиста по поддержке внутренних сервисов в домене контента и товаров.

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

Разница между пользовательской поддержкой и нами — в инструментах и возможностях их использования в проде. Речь идёт о базах данных, HTTP- запросах, UI-админках и т. д. Карточки товаров могут не создаваться, контент — не обновляться. С этими и другими проблемами мы сталкиваемся в своей практике. Чтобы всё заработало как надо, и нужны эти инструменты.

В общем и целом работа в команде IT-поддержки очень интересная. Это не только «Ну вы держитесь там!», но и решение разноплановых IT-задач, иногда связанных с обработкой больших объёмов данных.

Какое-то время мы жили и не тужили, дёргая так называемые «ручки» в Swagger и используя Postman, селектили базки и т. д. Но сервисов становилось всё больше, появлялись новые фичи — следовательно, и количество проблем увеличивалось. Появилась необходимость больше селектить, делать больше HTTP-запросов, их комбинируя. Кроме того, появились задачи по анализу большого количества данных, где, например, за один раз не передашь все нужные ID, и запросы, от которых не то что база умирала — умирала DataGrip. Подобные задачи стали решаться очень долго  — и мы поняли, что пора что-то менять... может быть даже работу.

Если провести аналогию с известной сказкой, нашей «золотой рыбкой» стал Python. К нам в команду пришёл человек, который перевернул наш подход к работе. Забегая вперёд, скажу, что последующий наём сотрудников в наш отдел был с пунктом о знании скриптовых языков и в частности Python и на текущий момент половина сотрудников пишет на нём, все умеют с ним работать, у нас реализован свой небольшой сервис, учитывающий требования информационной безопасности, а тимлиды используют язык в рамках проектного управления и аналитики проблем. Так мы очень быстро узнаём о болях клиентов и специфике работы того или иного сервиса. Подробнее об этом — ниже.

Небольшой дисклеймер

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

Выполнение HTTP-запросов с помощью модуля Requests.

Самым важным в нашей работе является использование HTTP-запросов. Это краеугольный камень нашей автоматизации.

Мы завели сервисную учётку в Jira, сделав бота, который работает с API, избавляя нас от большого количества ручной работы. С помощью скрипта на Python мы решили несколько задач:

  • дважды в день в определённое время проходить по всем созданным JIT-задачам в нашем капасити;

  • проверять прилинкованные Jira-тикеты, считать их и обновлять их количество в JIT-задаче;

  • оставлять комментарии;

  • проверять правильность связи между JIT-задачей и Jira-тикетами.

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

На примере бота мы рассмотрим основные модули и библиотеки Python, которые мы используем в написании скриптов.

В текущих реалиях работы с запросами...

Модуль Requests нужен для отправки и получения данных. Чтобы выполнить HTTP-запрос, используется стандартный метод написания функции на Python.

def get_issues(request_text):
    error = True
    url = '...'
    headers = {
        ...
        }
    payload = {...}
    request = requests.post(url=url, data=payload, headers=headers, auth=auth)
    if request.ok:
        error = False
        return request, error
    return request, error

Так выглядит структура стандартной функции, которая используется во всех наших скриптах. В поле url мы указываем строку запроса, в поле headers — заголовки авторизации, а в payload — какие данные мы хотим отправить в теле запроса. Всё довольно стандартно, но, начитавшись статей про обработку ошибок в Golang, мы позаимствовали её, задав переменную error. И для удобства написали небольшую функцию, которая возвращает текст ошибки.

def check_error(result, error):
    if error == True:
        print(f"Error, status_code: {result.status_code}, headers: {result.headers}")
    else:
        return False

Чтобы укротить время, мы воспользовались модулями datetime и time.

start_time = time()
...
print(f'\n{datetime.now().strftime("%d-%m-%Y %H:%M:%S")}\nScript has started: \n\t{team_name}\n'
...
end_time = time() - start_time

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

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

total = []
for idx, sku_ in enumerate(skus):
    if len(total) == 3:
        check_commercial_type_by_sku(total)
        ...
        total.clear()
        total.append(sku_.rstrip('\n'))
    else:
        total.append(sku_.rstrip('\n'))
check_commercial_type_by_sku(total)

Цикл формирует массив total, передаёт его в функцию, после получения ответа очищает массив и собирает новый.

Для записи данных в файл мы использовали функцию open и метод write.

with open('check_error', "a+") as wf:
    wf.write(f"Error, status_code: {result.status_code}, headers: {result.headers}\n")

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

Использование MS Excel

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

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

Подробнее вы, конечно, можете прочитать в интернете, но, если коротко, pandas — это высокоуровневая библиотека Python, которая позволяет вытворять с файлами Excel такое, что некоторым даже не снилось. Именно эту библиотеку мы и стали использовать для решения наших задач. Сразу оговорюсь, что мы не использовали её в полном объёме, так как в этом не было необходимости. В основном нам нужно было считывать значения из XLS-файла и присваивать их переменным.

honeypot = pd.read_excel('Name_file') # path to xlsx file
requestIDs = honeypot.to_dict(orient='records')
for idx, row in enumerate(requestIDs):
    request_id = [row['request_id']]
    reason_id = str(row['reason_id']).split(', ')

Прочитав с помощью функции read_excel файл и присвоив значение переменной, мы обращаемся к методу to_dict, чтобы преобразовать данные в словарь, а параметр orient = 'records' позволяет класть данные в словарь в следующем виде: [{column -> value}, ... , {column -> value}].

Ещё одним важным аспектом использования Excel является парсинг файла дежурств в нашу внутреннюю систему управления инцидентами. Согласно расписанию дежурств, сервис рассылает оповещения об инцидентах дежурным сотрудникам. Для наших коллег мы строим график в SharePoint и, если нужно что-то обновить, скармливаем скрипту файл — и через минуту получаем обновлённое расписание дежурств в системе управления инцидентами. Как вы уже, наверное, догадались, для добавления информации мы используем API этой системы. Если ты сотрудник Ozon Tech, тебе это может быть интересно — добро пожаловать во внутренний мессенджер. 

Работа с Kafka: чтение и запись

Любой уважающий себя разработчик хоть раз ходил в Kafka и работал с ней. Мы, конечно, не дотягиваем до их уровня, но себя тоже уважаем и с Kafka сталкиваемся регулярно.

Основные наши действия в Kafka — это чтение и запись. И для этого в Python есть модуль Confluent Kafka.

Для отправки сообщений в Kafka мы использовали метод Producer.

produce = Producer(...)
data_source = create_data(file_name)
for data in data_source:
    produce.poll(0)
    produce.produce(...)
produce.flash()

Алгоритм действий:

  • подключаемся к Kafka через Producer({'bootstrap.servers': 'server'});

  • создаём массив данных (сообщений) с помощью create_data(file_name);

  • отправляем сообщение в топик Kafka и ждём результат — produce.produce('topik_name', json.dumps(data), callback=delivery_report).

Для того чтобы читать сообщения из Kafka, использовали метод Consumer.

prparams = ({'bootstrap.servers': 'server', 'auto.offset.reset': 'earliest', 'session.timeout.ms': 6000, 'group.id': "mp_sup", 'error_cb': error_callback})
c = Consumer(params)
c.subscribe(['topik_name'])

'auto.offset.reset' указывает, в каком порядке читать сообщения: earliest — с самого раннего, latest — с последнего.

Подключение к PostgreSQL и выполнение запросов с помощью библиотеки Psycopg2

Использование скриптов для выполнения запросов к базе данных — вишенка на торте, учитывая, что нельзя сделать всё через HTTP-методы, а потому частенько приходится селектить.

Для работы с PostgreSQL мы использовали, наверное, самую популярную в Python библиотеку Psycopg2. Её модули и функции позволили нам выполнять подключение к базе данных, отправлять запросы и получать нужные результаты.

Для подключения к базе данных мы написали функцию, в которой содержится информация о базе, к которой мы собираемся подключиться, и о пользователе, который будет выполнять подключение, а также переменную connection. Метод cursor(cursor_factory=extras.DictCursor) позволяет формировать полученные в ответе поля базы в виде словаря.

def con():
    connection = None
    try:
        connection = psycopg2.connect(
            ...
        )
        connection.autocommit = True
        print("Connection to Postgre DB successful \n")
        return connection.cursor(cursor_factory=extras.DictCursor), connection
    except Exception as e:
        print("Connection error: " + str(e))
        exit(1)

Реализация запроса в базу данных:

get_rule_condition = f"SELECT rule_conditions from rule where id = {int(id)}"
cur.execute(get_rule_condition)
rule_cond = cur.fetchone()

Иногда нужно сделать просто огромную выгрузку по тяжелому запросу. У нас есть любимая команда разработки, у сервиса которой есть выделенная архивная реплика, на которой нет клиентов, и её можно селектить сколько душе угодно. Вы, наверное, спросите, почему не использовать для больших аналитических запросов DWH? Можно, но есть необходимость получить данные в реальном времени, в отличие от DWH, в которой данные синхронизируются с некоторой периодичностью.

У архивной реплики есть минус: если ей скормить долгий по ответу запрос, через некоторое время она начнёт отставать от мастера и данные станут неконсистентными. Поэтому, чтобы мы могли комфортно получать корректные значения в моменте, мы селектим через Python, используя батчи. Стараемся формировать их так, чтобы ответ занимал не более 200 мс, особенно если обращаться не к архивной реплике, так как она есть не у всех.

О том, как формировать батчи по списку ID, мы писали в разделе про HTTP-методы. Но бывают случаи, когда есть какое-то условие и нет входных данных. Для них мы реализовали функцию формирования батчей от минимального до максимального ID (чаще всего по ключу) нужной таблицы. Минимальный ID всегда можно указать опционально. Максимальный — формируется через обычный запрос на получение в таблице. Ну а ненужные данные отсекаются внутри условия запроса.

def create_batch():
    batchslist = []
   # slicebatch = []
    x, y = 113887310, 113888310 #минимальный, + шаг
    #maxid = get_maxId() #отдельная функция получения по запросу максимального ID
    maxid = 971815680 #явно переданный максимальный ID
    while y <= maxid:
        batchs = []
        batchs.append(x)
        batchs.append(y) 
        batchslist.append(batchs)
        x = y + 1
        y += 1000 #шаг
    if y > maxid:
        y = maxid
        batchs = []
        batchs.append(x)
        batchs.append(y)
        batchslist.append(batchs)
    return batchslist

Итоги

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

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

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

А можно этот процесс сравнить с химией: проблемы, которые мы находим, — это «лакмусовая бумажка» работы системы или сервиса.

Команд разработок в нашем домене много, микросервисов ещё больше — и со всем этим надо жить. А Python со своими прекрасными модулями, библиотеками и функциями позволил нам написать скрипты, которые очень помогают в решении многих непростых задач, с которыми команда IT-поддержки сталкивается каждый день. Освоив это ремесло, мы значительно уменьшили нагрузку на команды разработки, что позволило им погружаться в глубины мироздания, не отвлекаясь на рутину, и пилить новые фичи для бизнеса, а мы в свою очередь почувствовали себя ещё более крутыми IT-специалистами. Ведь наша главная цель — чтобы всем клиентам было удобно и легко работать на нашей площадке!

Авторы:
Сергей Исаченко и Андрей Каплей

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


  1. Thomas_Hanniball
    08.06.2023 12:11
    +1

    Мы завели сервисную учётку в Jira, сделав бота, который работает с API, избавляя нас от большого количества ручной работы. С помощью скрипта на Python мы решили несколько задач:
    • дважды в день в определённое время проходить по всем созданным JIT-задачам в нашем капасити;
    • проверять прилинкованные Jira-тикеты, считать их и обновлять их количество в JIT-задаче;
      оставлять комментарии;
    • проверять правильность связи между JIT-задачей и Jira-тикетами.

    Что-то вы всё усложняете. Зачем всё это делать, а потом всё это автоматизировать? Не проще ли упростить рабочий процесс (workflow) и выкинуть из него всё лишнее, что не приносит ценности? Всегда надо помнить, что чем проще, тем лучше.


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


    1. asnekmonk Автор
      08.06.2023 12:11
      +1

      Зачем проверять связи между JIT-задачей и Jira-тикетами и исправлять их, если можно потратить время на анализ и определить причину проблемы

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


  1. Ammiaq
    08.06.2023 12:11
    +2

    Никогда не делай, то, что можно сделать за 2 минуты, если это можно автоматизировать за 6 часов


  1. Aboo
    08.06.2023 12:11

    request = requests.post(url=url, data=payload, headers=headers, auth=auth)

    auth не объявлен

    return request, error

    Дважды return, надо ли? :)

    batchs = []
    batchs.append(x)
    batchs.append(y)
    batchslist.append(batchs)

    Я бы заменил на: batchslist.append([x, y])


    1. asnekmonk Автор
      08.06.2023 12:11

      auth не объявлен

      объявляется в отдельной функции, которую мы не описали (были на то причины)

      Дважды return, надо ли? :)

      в общем примере да, можно без него) ретернов много не бывает)))

      Я бы заменил на: batchslist.append([x, y])

      Валидно) спасибо


  1. resetsa
    08.06.2023 12:11
    +1

    Коллеги, спасибо за хорошую статью.

    Единственное, вот это гляньте

    https://www.psycopg.org/docs/usage.html#the-problem-with-the-query-parameters

    Вот так лучше не делать, а использовать параметры у execute

    get_rule_condition = f"SELECT rule_conditions from rule where id = {int(id)}" cur.execute(get_rule_condition)


    1. asnekmonk Автор
      08.06.2023 12:11

      Спасибо за отзыв, ознакомимся)