Сегодня мы немного расскажем вам о работе 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)
Ammiaq
08.06.2023 12:11+2Никогда не делай, то, что можно сделать за 2 минуты, если это можно автоматизировать за 6 часов
Aboo
08.06.2023 12:11request = 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])
asnekmonk Автор
08.06.2023 12:11auth не объявлен
объявляется в отдельной функции, которую мы не описали (были на то причины)
Дважды return, надо ли? :)
в общем примере да, можно без него) ретернов много не бывает)))
Я бы заменил на: batchslist.append([x, y])
Валидно) спасибо
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)
Thomas_Hanniball
Что-то вы всё усложняете. Зачем всё это делать, а потом всё это автоматизировать? Не проще ли упростить рабочий процесс (workflow) и выкинуть из него всё лишнее, что не приносит ценности? Всегда надо помнить, что чем проще, тем лучше.
Зачем проверять связи между JIT-задачей и Jira-тикетами и исправлять их, если можно потратить время на анализ и определить причину проблемы, почему такие связи могут отсутствовать, быть некорректными и прочее, чтобы уже затем эти проблемы исправить, а не делать автоматизацию, которая применяет workaround.
asnekmonk Автор
Все как раз таки очень просто - проверка связи в первую очередь это средство контроля, потому что ошибки при простановке связи чаще всего человеческий фактор, сейчас уже не испытываем этих проблем, но решили о них сказать.