Search
Write a publication
Pull to refresh

Comments 36

можно было бы просто использовать стандартный rate limiter

Стандартный - вот это имеется в виду?

Да, насколько помню он ссылался на него. На самом деле я сам его использовал для написания io rate limiter, но вообще это больше шутка была для доклада

У меня такое впечатление что в случае с rate limiter вы объявляете одну задачу("проверить, что кандидат умеет писать код"), а решаете другую - понять, есть ли у кандидата реальный опыт работы с высоконагруженными системами. Потому что если человек работал с highload - с rate limiting он точно сталкивался и знает что это и зачем.

Я согласен с тем что если человек умеет на собеседовании воспроизвести любую фичу из стандартной (околостандартной) библиотеки - код он писать точно умеет. Просто интересно - не высоковата ли планка?

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

Прямо сборник типовых задач на собес по go. «Заучите их наизусть и куда - нибудь точно пройдете»

У меня бы на понимание того что нужно в rate limiter ушло бы минут 30 и 10-15 доп вопросов. Хоть я Token Bucket и писал на атомиках и все вполне хорошо представляю. Зачем в одной задаче мешать алгоритм (rate limiter), контейнер/бэкенд ("корзину") и еще с прицелом на синхронизацию (опять же синхронизацию на каком уровне? синхронизацию состояния rate limiter для каждого токена или синхронизацию еще и контейнера)? Почему в интерфейсе только метод Allow и почему тогда у него аргумент называется key а не token как в текстовом описании задачи - чтобы ввести больше переменных? Если приводится определение этого интерфейса, то почему оно неполное? Заполнение и удаление в bucket не входит в задачу или решающий задачу может самостоятельно придумать как задавать rate и интервал "утекания" из bucket (можно это делать в момент добавления, а можно и из отдельной горутины по таймеру - выбрать одно или другое тоже задача сама по себе)? Если так, то я бы уж предложил решать ее с кода в который этот limiter предполагается вставлять.

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

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

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

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

Основные проблемы этой задачи заключаются в когнитивной сложности, большой вариативности реализаций, сложность проверки, длительности решения

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

  1. Базовая логика ограничения на один инстанс/скаляр. Простой интерфейс вида Take(tokens int) + Refill(tokens int) + Конструктор с параметрами capacity + tokenRate.

  2. Реализация Refill (или Leak в зависимости от того идем мы вниз или вверх). Можно его посадить на таймер и пополнять в одинаковые промежутки времени. А можно пополнять прямо внутри Take. За каждый вариант есть свои аргументы.

  3. Синхронизация если Take/Refill может использоваться конкурентно. Можно неплохо поговорить про каналы/очереди vs атомики, трейдоффы и прочую многопоточность.

  4. Перейти к уровню когда этих rate limiter-ов порядка 1М (допустим мы лимитим TCP соединения). Какой брать контейнер. Что делать с миллионом Ticker-ов. Что делать с time.Now() и так далее.

Получается - ИМХО - неплохое интервью с прогрессирующей сложностью часа на полтора.

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

Суть я, надеюсь, понял - история про то, как сделать интервью структурированным, конкретным и предсказуемым. Это здорово.

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

Задача-фаворит" - чем в итоге вывод в случае успеха отличается от вывода с ошибкой? Ошибка идет в stderr или что-то еще подразумевается но пропущено? Дополнительное условие по ограничению количества горутин. Предполагается что изначально решающий выберет вариант "в лоб" запустить по горутине на урл? А если я создам пул горутин и разобью список урлов между ними поровну? Или если я выберу этот вариант, то все пойдет не по скрипту и собеседование завалено?

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

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

Задача-фаворит" - чем в итоге вывод в случае успеха отличается от вывода с ошибкой? Ошибка идет в stderr или что-то еще подразумевается но пропущено? 

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

Дополнительное условие по ограничению количества горутин. Предполагается что изначально решающий выберет вариант "в лоб" запустить по горутине на урл? А если я создам пул горутин и разобью список урлов между ними поровну? Или если я выберу этот вариант, то все пойдет не по скрипту и собеседование завалено?

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

По сути ничем, кроме разного текста

Да. Но в условии разбито на два кейса. Не критично, но с толку сбивает и уходит время.

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

Да. Именно. Можно собеседование строить вокруг одной задачи итеративно увеличивая сложность. Это хорошо диалог развивает. И участвуют обе стороны а не "игра в одни ворота". Хороший был вариант на самом деле с дизайном рейт лимитера. Надо было его просто структурировать и задачу поставить менее абстрактно.

 ("корзину") <- это не нужно. из-за смешения языков у меня терминология поехала. Опять же пример интерфейса дан для контейнера а текстом задача поставлена для скалярной величины.

Привет, а можешь объяснить по поводу медленной функции. Почему в случае не буферизованного канала может возникнуть deadlock? Никак понять не могу

Не обязательно получим, а получим если выйдем по контексту раньше, чем slowFunc() выполнится. Тогда горутина заблокируется

	go func() {
		ch <- slowFunc()
	}()

Потому что из этого канала никто уже не прочитает. Это не дедлок, но просто утечка горутины.

Сорян что спрашиваю, пока плохо в этом разбираюсь.

  1. А если добавить defer close(ch) ?

  2. Как буферизованный канал спасёт от утечки?

Советую просто попробовать все сови варианты и поймешь где проблема. Главное контекст нужно отменить до медленной функции, в медленной функции поставить time.Sleep(10 * time.Second) к примеру а контекст создать с таймаутом в 2 сек.

1) Дефер внутри горутины в этом случае не вызовется. Девер в основной функции закроет канал раньше и мы можем получить панику send to closed chan

2) Мы запишем в буфер и горутина завершится. Через n времени GC уберет мусор в котором был твой канал и буфер

Спасибо! Обязательно попробую все варианты

На мой взгляд задачка прямо скажем не простая.

Если используете буфер, то это создаст лишнюю нагрузку на GC. Печальней будет если объем данных большой.

В данном примере, ИМХО, можно использовать флаг, который можно проверять перед записью данных в канал.

А по правильному конечно надо прокинуть контекст с медленную функцию и уже там танцевать.

Печальней будет если объем данных большой.

Объем каких данных?

В данном примере, ИМХО, можно использовать флаг, который можно проверять перед записью данных в канал.

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

можно использовать флаг

Можно и без флага, просто пишем в канал через select + default case

А по правильному конечно надо прокинуть контекст с медленную функцию и уже там танцевать.

Все так, но только если мы контролируем код slowFunc. Есть еще внешние зависимости

Отличная статья, тоже сталкивался с подобной задачей проверить навыки кандидата и вот что понял про практическую часть:

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

  2. Если мы сами не создаём высокоуровневые абстракции, то не стоит спрашивать про низкоуровневые в данной области. Лучше пускай человек профессионально владеет библиотекой conc и точно нигде не накосячит, чем каждый раз будет писать не до конца безопасную waitgroup.

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

Ещё у меня есть практика проверять резюме. Мы ведь не всех подряд кандидатов смотрим, а только тех, у которых подходящее резюме. Поэтому, в самом начале собеседования, я задаю вопросы прям по резюме. На этом этапе 70 процентов кандидатов сливаются. Часто пишут то, что не знают или что делали другие люди рядом с ними. Например кандидат пишет, что оптимизировал запросы к бд. Я спрашиваю каким образом он это делал. Часто отвечают что запускали explain analyse. Дальше я спрашиваю по каким данным в полученном результате они поняли что надо дальше делать и вот тут частый затуп. Выясняется что кандидат не понимает как оптимизировать запросы.

Супер круто комментарий, это почти все выводы к котоырм мы пришли, по теоретической части и к части выводов в практической, о чем статья. В записи прям есть слайд где почти как ваш пункт 2, но я скорее конкретизировал прям что generics, unsafe, atomic, reflection , вложенные мьютексы я бы не рекоммендавал давать на них задачи

Самое смешное, что оптимизация sql запроса может быть на уровне его составления, а не выполнения. Например, при загрузке страницы шел запрос с джоинами под фильтр. Но если изначально нет фильтрации, то и лишние джоины не нужны. Так что в оптимизации запросов к базе не всегда все ясно и четко.

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

Почему не используется wg.Done() вместо Add(-1)?

Скрин из пакета sync std это собственно одно и то же

Я б такое не пропустил на review, потому, что это ни разу не очевидно и ни разу не идиоматично.

IMHO, профессионал должен писать такой код, который прочтёт, особо не напрягаясь, любой джун.

Есть список правил того, что считается "идиоматичным", как вы определили, что Add(-1) не идиоматично?
На основании каких данных вы решили, что джун не прочтет код с Add(-1)? А если будет такой код: "i -= 1", джун тоже не прочтет?
Может тогда будем использовать только то подмножество языка, которое может понять сферический джун, которого мы сами придумали и за которого решили, что он поймет, а что нет?

Есть список правил того, что считается "идиоматичным", как вы определили, что Add(-1) не идиоматично?

Ну, как всегда, практика - критерий истины.

Много вы где видели в других проектах, чтобы вместо WaitGroup.Done() использовалось WaitGroup.Add(-1)?

Мне, например, ни разу такое не попадалось. Если честно, я даже не знал, что так можно делать :)

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

На Си можно написать вот так:

y = (flag ? sin : cos)(x);

Когда я был молодой и горячий, я так даже писал иногда. Сейчас бы не стал. Как показывает опыт, даже и не всякий сеньёр понимает такой код. Хотя вроде ничего особо сложного.

В Go тоже есть сложные места, и лучше без необходимости их не применять,

Если аппелировать только к личному опыту, на ваш взгляд, насколько очевидно использовать друг за другом конструкции waitgroup.Add(int) и waitgroup.Donе(), когда первая принимает произвольный int (судя по названию, добавляя его к какому-то внутреннему счетчику), а Donе() декрементирует этот счетчик на единицу, а не скажем, сбрасывает его совсем?

Ну, документация именно это и говорит делать с WaitGroup-ом. Прибавлять сколько надо (например, если мы планируем одним махом запустить три гороутинки, то сразу три и прибавить. Так даже и корректнее будет, потому что не будет ситуации, что две гороутинки запустились и завершились, а третья еще не запущена, но будет запущена - и вот в этот момент WaitGroup на мгновение находится в состоянии idle, что явно не соответствует задуманному), а уменьшать по-одному.

Но я аппелирую не только к личному опыту. Вы сможете показать мне хоть один опенсорсный проект, где Add(-1) используется вместо Done()?

Я свободно пишу на Go вот такой вот код: https://github.com/OpenPrinting/go-avahi

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

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

Я свободно пишу на Go вот такой вот код: https://github.com/OpenPrinting/go-avahi

Выглядит отлично, поставил звездочку,

А ж не держу справочную информацию в голове...

Все задачи используют std максимум, о какой справочной информации речь?

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

Не указывал этого явно, но речь идет об онлайн собеседованиях.

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

Дык пользуйтесь. Присылайте баг репорты. Если найдете баги, конечно :)

Все задачи используют std максимум, о какой справочной информации речь?

Гошная стандартная библиотека - большая и разнообразная. Местами заковыристая. Разумеется, в реальной жизни я подглядываю в документацию. Я помню, что там в принципе есть и где искать, но конкретику подглядываю.

По-моему, все так делают :-)

Да что уж там говорить, я уж больше 30 лет программирую, до сих пор не могу запомнить форматы printf за пределами самых ходовых. Особенно, где у них width, а где - precision. Что не помешало мне в своё время свой printf написать за пару дней, когда понадобилось.

Не указывал этого явно, но речь идет об онлайн собеседованиях.

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

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

Попрошу заменить псевдокодом, чтобы сэкономить время. Я тоже не помню все аргументы в каждом методе пакетов bytes, sync, strings, http.

Это просто удивительная статья!
Первый раз пробежав её глазами я подумал: "Что за фигня, опять заставляют на листочке компилировать!"
Но что-то заставило вернуться к примерам кода и посмотреть повнимательнее и мнение переменилось. Прикольно, я думаю на поведении слайсов можно народ ловить на собесах постоянно. Это гошная фишка :-)

А пример с парсингом урлов я бы реализовал наоборот :-) (И да, я знаю, что у меня контекст в http клиент не пробрасывается, это было уже лень реализовывать.)

type Result struct {
	Url string
	Code int
}

func ParseList(ctx context.Context, list []string) {
	limit := 10
	urlChan := make(chan string)
	resultChan := make(chan Result)

	wg := sync.WaitGroup{}
	wg.Add(limit)

	for i := 0; i<limit; i++ {
		go func() {
			defer wg.Done()
			for {
				url, ok := <-urlChan

				if !ok {
					return
				}

				response, err:= http.Get(url)

				if err != nil {
					resultChan <-Result{url, 0}

					continue
				}

				resultChan <-Result{url, response.StatusCode}
				response.Body.Close()
			}
		}()
	}

	go func() {
		defer close(urlChan)
		for _, url := range list {
			select {
			case <-ctx.Done():
				return
			default:
				urlChan <- url
			}
		}
	}()

	go func() {
		wg.Wait()
		close(resultChan)
	}()

	for v := range resultChan {
		fmt.Printf("%s: %d\n", v.Url, v.Code)
	}
}

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

Sign up to leave a comment.