вторник, 21 ноября 2017 г.

Golang internals

        За что я люблю Golang - так это за то что ему сравнительно легко заглянуть под капот. Golang это единственный известный мне язык программирования runtime которого написан на нем самом а не на C(на 99%,  там есть немного ассемблера). То есть барьер между написанием программ на языке Go и заглядыванием внутрь - сильно ниже чем у того же PHP или Python. 
    Заглянув в runtime/стандартную библиотеку ты получаешь гораздо более глубокое понимание того как на самом деле работают приложения написанные на Golang. Там очень много красивых решений, которым можно поучится. Вообще когда ты читаешь как реализованы production class решения, ты на многие вещи начинаешь смотреть по другому.
         К примеру сортировка. Существует многие десятки алгоритмов сортировки, какой из них выбрать? Казалось бы - смотри на Big O и выбирай. Но это теория. А на практике оказывается что за счет малой константы многие алгоритмы с O(n2) оказываются быстрее чем алгоритмы гарантирующие O(n log n) даже в худшем случае. В общем кому интересно как работает встроенная в Golang сортировка можно почитать здесь: - https://golang.org/src/sort/sort.go  
          Другой пример - реализация map. Как на самом деле реализован тип map ? Какая хэш функция там используется ? Какой первоначальный размер хэш таблицы ? Как он увеличивается с ростом количества данных в map ? Если вам также интересно как и мне - то ответы на все вопросы можно найти здесь - https://golang.org/src/runtime/hashmap.go 

среда, 15 ноября 2017 г.

Load balancing

         В очередной раз убедился что механизм балансировки основанный на RTT(round trip time) это не самая удачная идея. Когда мы его внедряли мы получили довольно не плохой выигрыш в производительности - порядка 20%. Но потом выяснилось что это была не самая удачная идея. Почему:
          - Сетевые флуктуации случайным образом влияют на распределение нагрузки. То есть распределение запросов между экземплярами приложения определяется (или как минимум сильно зависит от сетевых флуктуаций). Фактически мы приходим к ситуации когда небольшие сетевые проблемы могут привезти к тому что отдельные экземпляры отхватывают порцию траффика не совместимую с жизнью и уходят в себя/убиваются OOM.
      - В контексте перехода на Kubernetes выяснилась еще одна интересная особенность. Экземпляры приложения запущенные в Kubernetes стабильно получали больше запросов чем аналогичные инстансы  запущенные вне Kubernetes. Объясняется это просто. Все Kubernetes сервера приехали в последней партии железа и соответсвенно были смонтированы в несколько рядом стоящих стоек. Соответсвенно RTT внутри Kubernetes облака были в среднем в 2 раза меньше пингов между старыми серверами, что приводило к соответсвующему распределению нагрузки в пользу K8s экземпляров. 
     -  При RTT-based балансировке ты не можешь пропорционально балансировать нагрузку между экземплярами приложения имеющими разное количество аппаратных ресурсов. То есть если один экземпляр запущен на сервере с 24 ядрами а другой на сервере с 40 ядрами - то они все равно будут получать примерно равное количество запросов. Все потому что RTT до сервера с 24 ядрами ничем не отличается от RTT до сервера с 40 ядрами. Вроде бы очевидный факт, но почему-то в тот момент когда мы это реализовывали мне это в голову не пришло. За что собственно и поплатился.
            Собственно вывод который я сделал для себя: балансировка должна основываться именно на ресурсах доступных экземпляру приложения (количеству/качеству CPU, RAM). То есть должен высчитываться некий индекс производительности экземпляра приложения и на его основе уже будет происходить балансировка нагрузки.           
      Еще один извечный вопрос load balancing-а: как собственно балансировать трафик ? Централизовано, через какой-нибудь прокси или через алгоритм "зашитый в клиента". Под алгоритмом зашитым в клиента я подразумеваю некий "smart connector" который сам решает к какому серверу нужно обратится на основе неких доступных ему данных (service discovery + какие-то метрики/данные).
        В первом случае мы получаем проблему в виде SPOF (single point of failure) со всей вытекающей головной болью(как обеспечить ее доступность, мониторинг и тд), нужно дополнительное железо именно под балансировщик/прокси. Именно поэтому я всегда был поклонником второго варианта. Но выяснилось что не все так просто в реальной жизни. У второго варианта также есть  весьма серьезные недостатки: невозможность быстро и единообразно поменять алгоритм балансировки, необходимость поддерживать "smart connector" для всех используемых языков/платформ.  Если у тебя 5 компонентов которые написаны на одном языке - то все более менее терпимо. Но у нас то микросервисы, блять! Это прдразумевает что: 1.  их много, 2. Они поддерживаются разными командами и за всеми хер уследишь, 3. Каждый суслик - агроном и может писать на чем ему вздумается.  В общем менять алгоритм балансировки в таких условиях - это боль и печаль.    

понедельник, 13 ноября 2017 г.

Не заметили как стали большими

     В последнюю распродажу мы поставили небольшой рекорд - 100k RPS на всех 6 странах одновременно, до 30k на страну. Что сказать - 100k RPS это уже не шутки. Мы сами не заметили как стали большими.  

четверг, 2 ноября 2017 г.

Golang runtime

       Не так давно в ходе выкладки в прод memory/cpu лимитов для компонентов наткнулся на интересную особенность поведения Golang runtime. При нехватки CPU ресурсов у приложения написанного на Golang может резко возрастать потребление памяти. Связано это с тем что при увеличении  нагрузки доля CPU выделяемая на сборку мусора уменьшается и в какой-то момент времени мусор начинает генерироваться быстрее чем идет его сборка. 
         В таком режиме go приложение начинает довольно быстро расходовать память и быстро  пристреливается OOM killer-ом. Потом оно перезапускается, но уже с пустыми кэшами. Так как кэши пустые, приложение начинает расходовать еще больше CPU и вероятность того что оно сново уйдет в крутое пике - крайне велика. 
          Первоначальное предположение было - что Go runtime не видит memory limit-ов установленных cgroup. По после чтения исходников я выяснил что go runtime и не пытается увидеть эти лимиты. Действительно, у приложения запущенного без root привилегий не так много возможностей увидеть с какими ограничениями по памяти оно запущено. И даже если у тебя есть root права - эти ограничения будут зависеть от очень многих факторов, и эти факторы будут различаться на каждой поддерживаемой платформе. В общем go runtime просто аллоцирует новые куски памяти до тех пор пока его не прикончит OOM killer или до тех пор пока  операционная система не вернет ему ошибку при очередном вызове mmap(2). В этом случае go runtime падает с паникой.