Пользователь
0,1
рейтинг
14 июля 2014 в 16:19

Разработка → О плюсах и минусах Go

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

Поскольку пост не состоит в хабе «я пиарюсь» — обрисую особенности проекта лишь вкратце. Это высоконагруженное веб-приложение, обрабатывающее сейчас около 600М хитов в сутки (пиковая загрузка больше 10к запросов в секунду). Около 80% запросов можно отдать из кеша, а остальные надо полностью обрабатывать. Рабочие данные в основном лежат в базе на PostgreSQL, часть в бинарных файлах с плоской структурой (т.е. фактически массив, но не в памяти, а в файле). Кластер на Perl-е состоял из восьми 24-х ядерных машин с практических исчерпаным запасом по производительности, кластер на Go будет уже из шести с подтвержденным более чем трехкратным запасом. Причем узкое место уже не столько процессор, сколько ОС и остальное железо с софтом — обработать 10к нетривиальных запросов за одну секунду на одной машине физически не просто, каким бы ни производительным был бэкенд-софт.

Скорость разработки

Мой опыт работы с Go до начала рефакторинга был минимальным. Я уже больше года присматривался к языку, успел проштудировать спецификацию от корки до корки, изучил полезные материалы на оффициальном сайте и за его пределами и ощущал себя готовым засучить рукава и браться за работу. Первоначальная оценка сроков по работам была — 3-6 недель. Рабочая бета была готова как раз к концу 6-й недели, хотя ближе к завершению я уже начал думать, что не успею. Зачистка багов и оптимизация производительности заняли ещё целый месяц.

Вначале было особенно сложно, но с течением времени в спецификации приходилось заглядывать все реже, а код получался все чище. Если сперва мне приходилось на тот функционал, который я на Perl-е мог закодить за час, тратить на Go целый день, то потом этот разрыв значительно сократился. Но все равно на Go программировать ощутимо дольше, чем на Perl-е — приходится продумывать нужные для работы структуры, типы данных и интерфейсы, прописывать все это в коде, заботиться об инициализации слайсов, мапов и каналов, прописывать проверки на nil… В Perl-е с этим все сильно проще: для структур приходится использовать хеши, там не надо предварительно обьявлять поля, и сильно больше синтаксического сахара для программистов. Сравнить хотя бы сортировку — в Go нет возможности указать замыкание для сравнения данных, нужно прописывать отдельные функции для получения длины, и кроме функции сравнения по индексам необходимо ещё прописать отдельную функцию обмена элементов местами в массиве. А все почему? Потому что нет генериков, и функции сортировки проще вызвать специально задекларированный Swap(i, j) чем разбираться, что там ей подсунули и по каким смещениям надо делать этот обмен значений.

Кроме сортировки, мне ещё в глаза бросилось отсутствие Perl-ой конструкции for/while() {… } continue {… } (блок continue будет выполняться даже при раннем прерывании текущей итерации через оператор next). В Go для этого приходится использовать некошерный goto, который к тому же принуждает прописывать все декларации переменных перед ним, даже тех, которые не используются после метки перехода:
var cnt int
for ;; {
        goto NEXT
        a := int(0) // ./main.go:16: goto NEXT jumps over declaration of a at ./main.go:17
        cnt += a
NEXT:
        cnt ++
}

Также не до конца работает парадигма обьединения синтаксиса для указателей и не указателей — в случае использования стуктур компилятор нам дает возможность использовать один и тот же синтаксис, а для мап — уже нужно разыменовывать и использовать скобки, хотя компилятор мог бы и сам все определить:
type T struct { cnt int }
s := T{}
p := new(T)
s.cnt ++
p.cnt ++
но
m := make(map[int]T)
p := new(map[int]T)
*p = make(map[int]T)

m[1] = T{}
(*p)[1] = T{}
p[1] = T{}  // ./main.go:13: invalid operation: p[1] (type *map[int]T does not support indexing)

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

Кстати, итоговый объем кода в символах практически совпал (только для выравнивания кода в Perl использовалось два пробела, а в Go — один таб), а вот строк в Go получилось на 20% больше. Правда, функционал немного различается, в Go, например, добавлена работа с GC, зато в Perl ещё учитывается отдельная библиотека для кеширования SQL запросов во внеший файловый кеш (с доступом через mmap()). В общем, обьем кода почти равен, но у Perl все же немного компактнее. Зато в Go меньше скобок и точек с запятой — код выглядит лакониченее и легче читается.

В целом, код на Go пишется вполне быстро и аккуратно, куда быстрее чем, скажем, на C/C++, но для простых задач без особых требований к производительности я буду продолжать использовать Perl.

Производительность

Скажем прямо, особых претензий к Go в плане производительности у меня нет, но я ожидал большего. Разница с Perl (много зависит от типа вычислений, в арифметике, например, Perl совсем уж не блистает) составляет около 5-10 раз. У меня не было возможности попробовать gccgo, т.к. на FreeBSD он упорно не собирается, а жаль. Но сейчас бэкенд-софт перестал быть узким местом, потребление им cpu составляет порядка 50% одного ядра, а при росте загрузки проблемы первыми начнутся у Nginx, PostgreSQL и ОС.

В процессе оптимизации производительности профайлер показал, что, кроме моего кода, активную часть CPU потребляет и runtime (речь идет не только о пакете runtime).
Вот один из примеров top10 --cum:
Total: 1945 samples
       0   0.0%   0.0%     1309  67.3% runtime.gosched0
       1   0.1%   0.1%     1152  59.2% bitbucket.org/mjl/scgi.func·002
       1   0.1%   0.1%     1151  59.2% bitbucket.org/mjl/scgi.serve
       0   0.0%   0.1%      953  49.0% net/http.HandlerFunc.ServeHTTP
       3   0.2%   0.3%      952  48.9% main.ProcessHttpRequest
       1   0.1%   0.3%      535  27.5% main.ProcessHttpRequestFromCache
       0   0.0%   0.3%      418  21.5% main.ProcessHttpRequestFromDb
      16   0.8%   1.1%      387  19.9% main.(*RequestRecord).SelectServerInDc
       0   0.0%   1.1%      367  18.9% System
       0   0.0%   1.1%      268  13.8% GC

Как видим, на обработку собственно scgi запроса хендлером тратится всего 49% потребляемого cpu, а целых 33% тратится на System+GC

А вот просто top20 из этого же профайла:
Total: 1945 samples
     179   9.2%   9.2%      186   9.6% syscall.Syscall
     117   6.0%  15.2%      117   6.0% runtime.MSpan_Sweep
     114   5.9%  21.1%      114   5.9% runtime.kevent
      93   4.8%  25.9%       96   4.9% runtime.cgocall
      93   4.8%  30.6%       93   4.8% runtime.sys_umtx_op
      67   3.4%  34.1%      152   7.8% runtime.mallocgc
      63   3.2%  37.3%       63   3.2% runtime.duffcopy
      56   2.9%  40.2%       99   5.1% hash_insert
      56   2.9%  43.1%       56   2.9% scanblock
      53   2.7%  45.8%       53   2.7% runtime.usleep
      39   2.0%  47.8%       39   2.0% markonly
      36   1.9%  49.7%       41   2.1% runtime.mapaccess2_fast32
      28   1.4%  51.1%       28   1.4% runtime.casp
      25   1.3%  52.4%       34   1.7% hash_init
      23   1.2%  53.6%       23   1.2% hash_next
      22   1.1%  54.7%       22   1.1% flushptrbuf
      22   1.1%  55.8%       22   1.1% runtime.xchg
      21   1.1%  56.9%       29   1.5% runtime.mapaccess1_fast32
      21   1.1%  58.0%       21   1.1% settype
      20   1.0%  59.0%       31   1.6% runtime.mapaccess1_faststr

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

Имхо, ещё есть огромный резерв для оптимизации компилятора и библиотек. К примеру, я не заметил inlining-а — все мои мьютексы отлично видны в развертках стека горутин. Процесс оптимизации компилятора на месте не стоит (не так давно Дмитрий Вьюков представил существенно ускоренную реализацию каналов, например), но кардинальных подвижек пока не часто заметно. Например, после перехода с Go 1.2 на Go 1.3 я разницы в производительности практически не увидел вообще.

Ещё в ходе оптимизации мне пришлось отказаться от пакета math/rand. Дело в том, что по ходу обработки запроса часто нужны были псевдослучайные числа, но с привязкой к данным, а rand.Seed() использовал слишком много CPU (профайлер показывал 13% от общего числа). Кому надо, тот нагуглит метод генерации пвсевдослучайных чисел с быстрым Seed(), но все равно — для криптографических целей есть пакет crypto/rand, а в math/rand могли бы так сильно не заморачиваться на качественное перемешивание бит при инициализации.
Кстати, я в итоге остановился на следующем алгоритме:
func RandFloat64(seed uint64) float64 {
        seed ^= seed >> 12
        seed ^= seed << 25
        seed ^= seed >> 27
        return float64((seed*2685821657736338717)&0x7fffffffffffffff) / (1 << 63)
}


Очень удобно, что все вычисления происходят в одном процессе, на Perl-е использовались отдельные процессы-воркеры и мне приходилось организовавать общий кеш — что-то через memcached, что-то через файл. На Go с этим все гораздо проще и естественней. Но теперь при отсутствии внешнего кеша встает проблема холодного старта, тут пришлось немного повозиться — сначала пробовал на nginx-е ограничить (что-бы не запустилось одновременно сто тысяч горутин и не встало всё колом) количество одновременных запросов на upstream через модуль https://github.com/cfsego/nginx-limit-upstream, но что-то он не сильно стабильно работал (когда забивался пул соединений, то вернуться к нормальному режиму ему было почему-то непросто, даже после снятия нагрузки). В итоге я немного пропатчил scgi модуль и добавил ограничитель на кол-во одновременно исполняемых запросов — пока не закончится обработка какого-то из текущих запросов — новый не будет принят Accept()-ом:
func ServeLimited(l net.Listener, handler http.Handler, limit int) error {
        if limit <= 0 {
                Serve(l, handler)
        }

        if l == nil {
                var err error
                l, err = net.FileListener(os.Stdin)
                if err != nil {
                        return err
                }
                defer l.Close()
        }
        if handler == nil {
                handler = http.DefaultServeMux
        }
        sem := make(chan struct{}, limit)
        for {
                sem <- struct{}{}
                rw, err := l.Accept()
                if err != nil {
                        return err
                }
                go func(rw net.Conn) {
                        serve(rw, handler)
                        <-sem
                }(rw)
        }
}

Модуль scgi был выбран тоже из соображений производительности — net/http/fcgi почему-то был в итоге медленнее просто net/http (и не поддерживает persistent connection), а net/http дополнительно грузил OS генерацией tcp-пакетов и поддержкой внутренних tcp-соединений (хотя технически и возможно его запустить слушать на unix-сокете) — а раз можно было от этого избавиться, то почему бы и не избавиться? Использование nginx в качестве фронтэнда дает свои плюсы — контроль таймаутов, логирование, пробрасывание failed запросов на другие сервера из кластера — и все это с минимальной дополнительной нагрузкой на сервер. Ещё один плюс такого подхода — по netstat -Lan видно, когда на сокете scgi растет заполненность очереди Accept — значит где-то у нас перегруз и надо что-то делать.

Качество кода и отладка

Пакет net/http/pprof — волшебная вещь! Это что-то типа модуля server-status от Apache, но для Go демона. И, кстати, я бы не рекомендовал включать его в продакшне, если у Вас вместо выделенного хендлера http используется DefaultServeMux — так как пакет становится доступным всем по ссылке /debug/pprof/. У меня такой проблемы нет, наоборот, чтобы получить доступ к функциям пакета через http, нужно запускать отдельный мини-сервер на localhost:
go func() {
        log.Println(http.ListenAndServe("127.0.0.1:8081", nil))
}()

Кроме получения профайла по процессору и памяти этот модуль дает возможность просмотреть по стеку список всех запущенных в данный момент горутин, всю цепочку функций которые в них в данный момент выполняются и в каком состоянии находятся: /debug/pprof/goroutine\?debug=1 дает список разных горутин и их состояний, а /debug/pprof/goroutine\?debug=2 дает список всех запущеныхгорутин, в т.ч. и дублирующихся (т.е. в полностью одинаковых состояниях). Вот пример одной из них:
goroutine 85 [IO wait]:
net.runtime_pollWait(0x800c71b38, 0x72, 0x0)
        /usr/local/go/src/pkg/runtime/netpoll.goc:146 +0x66
net.(*pollDesc).Wait(0xc20848daa0, 0x72, 0x0, 0x0)
        /usr/local/go/src/pkg/net/fd_poll_runtime.go:84 +0x46
net.(*pollDesc).WaitRead(0xc20848daa0, 0x0, 0x0)
        /usr/local/go/src/pkg/net/fd_poll_runtime.go:89 +0x42
net.(*netFD).accept(0xc20848da40, 0x8df378, 0x0, 0x800c6c518, 0x23)
        /usr/local/go/src/pkg/net/fd_unix.go:409 +0x343
net.(*UnixListener).AcceptUnix(0xc208273880, 0x8019acea8, 0x0, 0x0)
        /usr/local/go/src/pkg/net/unixsock_posix.go:293 +0x73
net.(*UnixListener).Accept(0xc208273880, 0x0, 0x0, 0x0, 0x0)
        /usr/local/go/src/pkg/net/unixsock_posix.go:304 +0x4b
bitbucket.org/mjl/scgi.ServeLimited(0x800c7ec58, 0xc208273880, 0x800c6c898, 0x8df178, 0x1f4, 0x0, 0x0)
        /home/user/go/src/bitbucket.org/mjl/scgi/scgi.go:177 +0x20d
main.func<C2><B7>008()
        /home/user/repo/main.go:264 +0x90
created by main.main
        /home/user/repo/main.go:265 +0x1f5c

Мне это помогло выявить баг с блокировками (при определенных условиях дважды вызывался RUnlock(), а так делать нельзя) — в дампе стека я увидел всю пачку залоченых горутин и номера строк, где был вызов RUnlock().

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

go vet хоть и полезная утилита, но у меня её основная польза свелась к предупреждениям о пропущенных спецификаторах формата в всяких printf() — компилятор такое обнаружить не в состоянии. На явно плохой код
if UintValue < 0 {
        DoSomething()
}
vet никак не реагирует.

Основную работу по проверке кода выполняет компилятор. Он исправно ругается на неиспользуемые переменные и пакеты, а вот на неиспользуемые поля в структурах ни компилятор, ни vet не реагируют (хотя бы предупреждением), хотя тут тоже есть на что обратить внимание.

Стоит быть аккуратным с оператором :=. У меня был случай, когда надо было посчитать разницу двух uint, в т.ч. корректно учитывать отрицательную разницу как отрицательную, а код
  var a, b uint
 ...
  diff := a - b
посчитает не то, что Вы ожидаете — нужно использовать приведение к знаковому типу (либо не пользоваться беззнаковыми).

Ещё есть полезная практика именовать одни и те же типы данных с разным предназначением разными именами. Например, так:
type ServerIdType uint32
type CustomerIdType uint32

var ServerId ServerIdType
var CustomerId CustomerIdType
И теперь для переменной CustomerId компилятор не даст просто так записать значение ServerId (без приведения типа), несмотря на то что и там и там внутри uint32. Помогает от разного рода опечаток, хотя теперь приходится часто использовать приведение типов, особенно при инициализации переменных.

Пакеты, библиотеки и связка с C

Немаловажную роль в популярности Go сыграл эффективный (увы, не в плане производительности, с этим пока некоторые проблемы) механизм взаимодействия с C-библиотеками. По большому счету — значительная часть Go-библиотек — это просто обертки над их C аналогами. Например, пакеты github.com/abh/geoip и github.com/jbarham/gopgsqldriver компилируются с -lGeoIP и -lpq соотв (правдя, я использую нативный Go PostgreSQL driver — github.com/lib/pq).

Для примера рассмотрим практически стандартную функцию crypt() из unistd.h — эта функция есть из коробки во многих языках, например, в Perl-овом модуле Nginx-а её можно использовать без подгрузки дополнительных модулей, что бывает полезно. Но не в Go, здесь её надо пробрасывать в C самостоятельно. Это делается элементарно (в примере соль отрезается из результата):
// #cgo LDFLAGS: -lcrypt
// #include <unistd.h>
// #include <stdlib.h>
import "C"
import (
        "sync"
        "unsafe"
)

var cryptMutex sync.Mutex

func Crypt(str, salt string) string {
        cryptStr := C.CString(str)
        cryptSalt := C.CString(salt)

        cryptMutex.Lock()
        key := C.GoString(C.crypt(cryptStr, cryptSalt))[len(salt):]
        cryptMutex.Unlock()

        C.free(unsafe.Pointer(cryptStr))
        C.free(unsafe.Pointer(cryptSalt))
        return key
}
Блокировка нужна т.к. crypt() возвращает один и тот же char* на внутреннее состояние, полученную строку надо скопировать, иначе она будет перезаписана при следующем вызове, т.е. функция не является thread-safe.

database/sql

Для каждого используемого хендлера Db я рекомендую вызывать прописывать максимальный лимит коннектов и указывать какой-то ненулевой лимит idle-коннектов:
db.SetMaxOpenConns(30)
db.SetMaxIdleConns(8)
Первое позволит избежать перегруза базы и использовать её в режиме максимальной производительности (с ростом кол-ва одновременных соединений производительность баз данных начинает падать с какого-то момента, есть оптимальное значение кол-ва одновременных запросов), а второе — уберет необходимость открывать новое соединение на каждый запрос, для PostgreSQL с его fork() режимом это особенно важно. Конечно, для PostgreSQL можно ещё использовать pgpool или pgbouncer, но это всё лишний оверхед на пересылку данных ядром и дополнительные задержки — так что лучше обеспечить постоянность соединений прямо на уровне приложения.

Для исключения оверхеда на разбор запроса и построения плана стоит использовать prepared statements вместо непосредственных запросов. Но нужно иметь ввиду — в некоторых случаях планировщик выполнения запроса может использовать не самый оптимальный план, так как он строится на этапе разбора запроса (а не его выполнения) и планировщик не всегда имеет достаточно данных, чтобы знать какой индекс предпочтительнее использовать. Кстати, плейсхолдерами для переменных в Go драйвере PostgreSQL используются '$1', '$2' и т.д., вместо '?', как в Perl.

sql.(Rows).Scan() имеет одну особенность — он не понимает переименнованные строковые типы, например type DomainNameType string. Приходится заводить временную переменную типа string и в неё делать загрузку данных из базы, а потом делать присвоение с конвертацией типа. С переименованными числовыми типами почему-то такой проблемы нет.

Каналы и синхронизация

Бытует несколько ошибочное мнение, что раз у нас есть каналы в Go, то стоит использовать их и только их. Это не совсем так — каждой задаче — свой инструмент. Каналы отлично подходят для передачи различного рода сообщений, но для работы с общими ресурсами, например sql-кешем, вполне легально использовать мютексы. Для работы с кешем через каналы нам придется написать диспетчер запросов, который в итоге ограничит производительность доступа к кешу одним ядром, добавит ещё больше работы шедулеру горутин и добавит оверхеда на копирование и чтения данных в канал, плюс нужно каждый раз создать временный канал для передачи данных обратно вызывающей функции. Код с использованием каналов также зачастую становится в разы больше и сложнее кода с мютексами (как ни странно). Зато с мютексами надо быть предельно аккуратными, чтобы не попасть в дедлок.

В Go есть такая хитрая фича, как struct{}. Т.е. полностью пустая структура, без полей. Она занимает ноль места, массив любого размера таких структур тоже занимает ноль места, ну и буферизированый канал пустых структур тоже занимает ноль места (плюс внутренние данные, разумеется). Собственно, этот буферизированый канал пустых структур является семафором, в компиляторе для него даже отдельный обработчик сделан — если нужен семафор с Go синтаксисом — можно использовать chan struct{}.

Немного печалит куцость пакета sync. Например, нет спинлоков, хотя они очень полезны, так как быстры (хотя с GC использование спинлоков становится рискованным делом). Да ещё и сами операции с мютексами не инлайнятся (насколько я могу судить). Ещё больше расстраивает невозможность проапгрейдить блокировку RWMutex — если блокировка в статусе RLock и обнаружилось, что необходимо внести изменения — извольте делать RUnlock(), затем Lock() и ещё раз проверять, есть ли все ещё неоходимость делать эти изменения или какая-то горутина уже всё успела сделать. Также нет неблокирующей функции TryLock(), опять же непонятно почему — для некоторых случаев она крайне необходима. Тут разработчики языка с их «нам лучше знать, как вам нужно программировать», имхо, уже перегнули палку.

В некоторых случаях избежать использования мютексов помогает пакет sync/atomic с его атомарными операциями. Например, у меня в коде часто используется текущий uint32 timestamp — я его держу в глобальной переменной и в начале каждого запроса просто атомарно сохраняю в неё актуальное значение. Немного грязный подход, знаю, можно было и функцию-хелпер написать, но в борьбе за производительность иногда приходится идти на такие жертвы — я теперь могу использовать эту переменную в арифметических выражениях без особых ограничений.

Есть ещё один хороший метод оптимизации для случая, когда какие-то общие данные обновляются только в одном месте (например, периодически), а в остальных случаях используются в режиме «только чтение». Суть в том, что нет необходимости делать RLock()/RUnlock() на операции чтения (и Lock()/Unlock() на обновление) — функция обновления может загрузить данные в новую область памяти, а затем атомарно подменить указатель на старые данные указателем на новые. Правда, в Go функция атомарной записи указателя требует тип unsafe.Pointer, и приходится городить такую конструкцию:
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&Data)), unsafe.Pointer(&newData))
Зато можно использовать эти данные в любых выражениях, не заботясь о блокировках. Это имеет особое значение для Go, т.к. кажущиеся короткими блокировки на самом деле могут быть очень даже долгими — а все из-за GC.

GC (garbage collector)

Испил он моей кровушки изрядно ;(. Представьте себе ситуацию — запускаете нагрузочный тест — все ок. Пускаете живой трафик — тоже все замечательно. А потом бац — и все становится плохо либо очень-очень плохо — старые запросы висят, новые все прибывают и прибывают (несколько тысяч в секунду), приходится делать рестарт приложения, после которого все опять же тупит, т.к. кешу приходится заполняться заново, но хоть как-то работает и через какое-то время возвращается в норму. Я сделал замер времени выполнения каждой стадии обработки запроса — и вижу, что периодически время выполнения всех стадий скачет до трех секунд и выше, даже тех из них, которые не используют блокировки, не используют доступ к базе и файлам, а делают только локальные расчеты и обычно укладываются в микросекунды. Стало понятно, что источником проблемы был не какой-то внешний фактор, а сама платформа. Точнее — сборщик мусора.

Хорошо, что в Go можно посмотреть статистику работы GC через runtime/debug.ReadGCStats() — там есть чему поудивляться. В моем случае на самом незагруженном сервере GC работал в следующем режиме:
0.06
0.30
2.00
0.06
0.30
2.00

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

Решение — запускать GC() почаще, для надежности лучше самостоятельно из программы. Можно даже просто периодически, я же немного заморочился и сделал счетчик запросов, а также форсированный запуск GC() после крупных очисток устаревших данных. В итоге GC() стал запускаться каждые десять-двадцать секунд вместо раз в несколько минут, зато каждый проход занимает стабильно около 0.1с — совсем другое дело! И потребление памяти демоном заодно упало процентов на 20. Есть возможность отключить сборщик мусора вовсе, но это подойдет разве что короткоживущим программам, ни никак не для демонов. Разрабочикам языка стоит добавить настройку к GC, чтобы он не останавливал приложение дольше указаного лимита, а вместо этого начал почаще запускаться — это избавило бы многих пользователей от проблем при высокой нагрузке.

maps

Никто не будет спорить, что мапы (хеши в терминах Perl) — крайне полезная штука. Но у меня есть серьезные претензии к разработчикам языка по поводу способа их реализации и использования. Грубо говоря, для работы с мапами компилятор использует следующие три функции:
valueType, ok := map_fetch(keyType)
map_store(keyType, valueType)
map_delete(keyType)
И это накладывает ряд значительных ограничений. Пока мапы состоят из базовых типов — все отлично, но с мапой структур или типов с ссылочными методами (т.е. методами, которые работают по ссылке на данные, а не по копии данных) уже начинаются проблемы — мы не можем например написать
type T struct { cnt int }
m := make(map[int]T)
m[0] = T{}
m[0].cnt++  // ./main.go:9: cannot assign to m[0].cnt
так как компилятор не может получить адрес значения m[0], чтобы по смещению cnt сделать инкремент.

Можно либо сделать мапу ссылок на структуру
m := make(map[int]*T)
m[0] = new(T)
m[0].cnt++
либо выгружать и сохранять всю структуру целиком
m := make(map[int]T)
tmp := m[0]
tmp.cnt++
m[0] = tmp
Первый вариант добавит много лишней работы сборщику мусора, а второй — процессору (особенно если структура немаленькая)

По моему мнению, вопрос можно решить, если при работе с мапой компилятор вместо map_store будет использовать функцию
*valueType = map_allocate(keyType)
и добавить дополнительное ограничение, что однажды добавленное значение в мапу не будет перемещаться в памяти.

Функция map_allocate должна использоваться для получения указателей не только на новосозданные элементы, но и на существующие, если они будут модифицироваться. Этот указатель можно использовать для выдачи программисту, для обновления значения, для вызова ссылочного метода — и пока значение лежит на месте, все работает отлично.

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

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

Итог

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

Уже очень длительное время я не изучал новых языков программирования. В свое время я немного освоил C (на уровне чуток пропатчить ядро FreeBSD), Perl и Shell-scripting (для повсеместных задач). Погружаться в изучение Python, Ruby или JS у меня не было ни времени, ни стремления — эти языки не могли предложить мне ничего принципиально нового, а менять шило на мыло желания не было. Go же сумел существенно дополнить мой набор инструментов, чему я только рад. При всех его недочетах я ни капли не сожалею о потраченном времени на его изучение — это действительно того стоит.
Николай Зубач @zuborg
карма
36,2
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • –8
    А если будет возможность адаптировать сборщик мусора к этому случаю, чтобы память после удаления не освобождалась до тех пор, пока в программе есть живая ссылка на удаленный элемент, тогда вообще всё замечательно и нет никаких проблем с безопасностью.


    Какое-то нездоровое желание
    • +6
      Аргументируйте Ваше высказывание, если не трудно, а то не совсем понятно, что именно вызывает у Вас неприятие такого (банального, в общем-то, по отношению к GC) пожелания.
  • +3
    Отличная статья! Спасибо за подробное описание процесса миграции и возникших проблем.
  • +1
    Судя по тому, как активно то тут, то там говорят о нехватке дженериков, их вот-вот запилят… Где-то читал, что они в планах есть.
    • +1
      Rob Pike на каком-то из выступлений (кажется, с GopherCon) говорил, что менять язык за исключением небольших правок они больше не планируют, считают его законченным. Упоминал, что вероятно когда-нибудь выйдет Go 2.0 и добавят generics туда.
  • +2
    Не холивара ради, но примерно год назад писали сервис, который должен был обрабатывать стабильно примерно 5к запросов, при этом кеш физически там никуда не присунешь. Для быстрого старта выбрали mongodb + node.js. И в итоге было 4 физ. сервера и 20 виртуалок Node.js. Нужно было двигаться дальше и смотрели в разные стороны. Смотрели на Go, он действительно очень быстр оказался для нашей задачи, примерно в 4-8 раза быстрее Node.js. В итоге уже год как работает Java (Netty, Morphia) + MongoDB и горя не знаем, хватает 1 физ. сервера и запас мощности 3-4 раза, не говоря уже о гораздо большем сообществе по сравнению с Go и наличие отличной IDE, профайлеров и так далее
    • +6
      А что именно перевесило в сторону Java по сравнению с Go, если не секрет? Подозреваю, что наличие знающего Java программиста )
      • +4
        Java учили на ходу, сам язык очень простой, вот все что вокруг него заставило потрудиться, чего одно Java EE стоит для не подготовленного человека. Но не так страшен черт, как его малюют.
        1. Java реально быстрее
        2. ООП и его прелести в больших проектах никто не отменял
        3. Огромное количество готовых решений, велосипеды в прошлом, есть все
        4. Наличие отличной IDE (автокомплит, дебаг, рефакторинг)

        В итоге нет затрат времени на изобретение велосипедов, красивая объектная модель, легко поддерживать, тесты писать сказка (Spring, Mockito), есть поддержка всевозможных CI, Sonar. Если удручает сам язык — пишите на Scala, прекрасно вписывается в архитектуру
        • +5
          А каждого свой набор приоритетов )
          Я тоже сначала думал над переписыванием на С или C++, но после знакомста с Go понял, что могу сэкономить себе кучу времени. В итоге получилось пусть немного менее производительное решение, зато нет утечек, возни с тредами да и итоговый код, думаю, сильно проще.
          • +3
            На C написать производительный HTTP Server тоже нужно суметь. А вот если познакомиться с репозиторием либ Java можно сэкономить еще больше времени
  • +1
    Это высоконагруженное веб-приложение, обрабатывающее… больше 10к запросов в секунду.

    кластер на Go будет уже из шести [машин]…

    обработать 10к… запросов за одну секунду на одной машине…

    Так все-же на одной или на всем кластере?
    • +2
      Имелось ввиду, что запихнуть все на одну не просто. Обрабатываются на кластере, разумеется, запас должен быть всегда.
      • +1
        Понятно. Интересно было почитать, спасибо за пост!
  • 0
    (блок continue будет выполняться даже при раннем прерывании текущей итерации через оператор next). В Go для этого приходится использовать некошерный goto, который к тому же принуждает прописывать все декларации переменных перед ним, даже тех, которые не используются после метки перехода
    Для этого можно использовать кошерный defer, кстати, не нужно городить «for ;;», конструкция «for {}» для этого существует.

    Дальше смотреть не стал.
    • +2
      У Вас какая-то особенная версия Go? На моей работает вот так:
      println("start for{}")
      for i := 0; i < 3; i ++ {
              defer func(){ println(i) }()  
      }
      println("finish for{}")
      


      результат:

      start for{}
      finish for{}
      3
      3
      3
      • +3
        Примерно так: play.golang.org/p/lCCD-dlhhX

        Статья интересная получилась, спасибо.
        • +2
          Не спорю, можно приспособить defer как это сделали Вы через обьявление тела цикла отдельной функцией, но стоит ли…

          Лично мне жадно лишний раз на каждую итерацию делать дополнительные вызовы функций (defer хоть и вполне дешевый, но все же не бесплатный), да и назвать распухший код лучшим, чем использование goto — тоже вопрос.
          • +2
            defer и замыкание — очень не бесплатные, на самом деле.
            groups.google.com/forum/#!topic/golang-ru/MlRvTOxWvig
            Т.е. использовать такое внутри частых и длинных циклов — не очень хорошая идея.
        • –1
          Давид, я не увидел анонимной функции, лопух.
          Вывод, конечно, из вашего примера.
      • +1
        Не понял?!

        результат:
        start for{}
        0
        1
        2
        finish for{}
      • +1
        Действительно, магия. Но работает у меня по другому и вывод, как у человека выше.
        • 0
          Нет никакой магии, в примере david_mz внутри цикла for обьявляется анонимная функция, defer запускается после каждой итерации, когда заканчивается выполняться эта функция.
      • –3
        Вы точно Гоу знаете?

            println("start for{}")
            for i := 0; i < 3; i ++ {
                    defer func(i int){ println(i) }(i)  
            }
            println("finish for{}")
        
        • +3
          Не спешите писать комментарий, не проверив его.
          Ваш пример вернет что-то типа:
          start for{}
          finish for{}
          2
          1
          0

          А теперь представьте ещё, что в цикле блок continue { } должен обработать несколько разных переменных — их все придется пробрасывать в defer с соответствующим оформлением аргументов — назвать такое решение кошерным могут только сильно ортодоксальные гоферы )
          • –1
            Я программировал и на Перле (пять лет) и на Гоу (три года), языки сильно разные, сравнивать их некорректно. Да, Гоу менее синтаксически насыщен, но это по нынешним временам скорее достоинство — язык можно быстро освоить и всё, что пишут другие программисты, даже профи, читается (что не сказать про Перл, многие ленятся учить его целиком, знаю поверхностно и могут прочитать далеко не всё).

            Я не назову себя ортодоксальным гоуером, но Гоу люблю, да, не отнять. Вот вышеприведённая последовательность меня не смущает :) Ну переверну цикл и «finish» в суну в дефер выше уровнем :) Но если в цикле будет сотня итераций, я, конечно, поостерегусь использовать деферы и буду выкручиваться :)
            • 0
              Вышеприведенная последовательность могла бы Вас смутить хотя бы потому, что все defer-ы выполняются после полного завершения всех итераций цикла, что далеко от того, как работает Perl-овый continue {}.

              Языка полностью разные, я их и не сравниваю напрямую, только те вещи, которые можно сравнить. Блок continue {} вполне мог бы присутствовать в Go в каком-то виде и он не сделал бы его хуже или сложнее, а вот удобнее в некоторых моментах — да.
              • 0
                Гоу, как я его называю, — Си с человеческим лицом. Его главное достоинство — простота. Из Перла можно брать очень многое, но тогда это Перл и получится. Согласен, continue — удобно, не спорю, но если начать добавлять туда всё удобное, не остановишься.

                Авторы молодцы — сделали очень изящный и простой язык.

                За ваш опыт и статью спасибо — любой практический опыт, да ещё и на русском, бесценен — сообщество пока невелико. Когда я начинал программировать на нём, даже биндинга ни к одной графической библиотеке не было, пришлось свой писать. Сейчас дела получше, но всё равно, по сравнению с сообществом других языков — страна и деревня по размерам :)
  • +1
    А Вы уверены, что блокировки Вам действительно необходимы? В Go есть прекрасный механизм передачи сообщений — оформляйте код, нуждающийся в блокировке в горутину и шлите ей сообщения.
    • +1
      Я специально отдельно рассмотрел этот вопрос в статье ). Каналы — это не серебрянная пуля, это всего лишь специфический инструмент, который в ряде случаев позволяет эффективно избавиться от использования блокировок в коде, но он вовсе не призван их убрать вовсе. Кроме того, реализация каналов внутри сама использует блокировки, просто предоставляя программисту более удобную (опять же, во многих, но не во всех случаях) абстракцию. Например, так как блокировка канала эксклюзивная, и функция-обработчик запросов тоже выполняется на одном ядре — при использовании каналов у Вас не будет возможности воспользоваться преимуществами shared RLock() блокировок, когда производительность не ограничиватся одним ядром.
      • +1
        В статье, кстати, есть пример, где используется мютекс для реализации функции crypt(). Представьте себе код, использующий каналы — он будет гораздо сложнее, а смысл?
        • +4
          Сами разработчики Go рекомендуют использовать каналы вместо блокировок Do not communicate by sharing memory; instead, share memory by communicating.

          Есть подозрения что блокировки реализованные во внутренней кухне Go будут оптимальнее своих собственных.
          • +4
            http://golang.org/doc/effective_go.html#sharing
            Do not communicate by sharing memory; instead, share memory by communicating.

            This approach can be taken too far. Reference counts may be best done by putting a mutex around an integer variable, for instance. But as a high-level approach, using channels to control access makes it easier to write clear, correct programs.
            Я уверен, что Вы в состоянии корректно перевести эту цитату, но все же позволю себе заметить, что она утверждает по сути то же, что и я: каналы есть зер гут, но не слишком фанатейте.

            Вот другой пример: https://code.google.com/p/go-wiki/wiki/MutexOrChannel
            So which should you use?

            Use whichever is most expressive and/or most simple.

        • 0
          По моей практике, мьютексы применяются достаточно редко и только в каких-то местах, где уже надо отжать проценты производительности. Я согласен, что в примере с crypt применение мьютекса оправдано тем. что код получился проще, но он (пример) достаточно искусственный. Есть ведь функция crypt_r, лишённая этого недостатка, и никакие блокировки вообще не нужны. На реальных задачах код с использованием каналов получается более простым и читаемым.
          • 0
            Вы немного напутали, мютексы применяются в многопоточных приложениях весьма часто, и вовсе не в тех местах, где нужно отжать пару процентов производительности (в таких местах от мютексов наоборот пытаются избавиться), а в тех, которые могут одновременно работать с общими данными и нужно обеспечить последовательный (а не одновременный) доступ. В этом плане пример с crypt() совсем не искуственный, а как раз каноничный.

            *_r аналоги есть не для всех функций, например для FreeBSD нет crypt_r(). Их реализация сложнее и, бывает, требует значительных дополнительных расходов cpu и памяти для работы, не всегда это востребовано.
            • 0
              Последовательный доступ можно сделать и на каналах, и на мьютексах. Я имел в виду, что на тех же задачах мьютексы иногда дают больше производительность, чем каналы. Чтобы не быть голословным:

              for req := range ch {
                ...
                req.resp <- C.GoString(C.crypt(req.cryptStr, req.cryptSalt))[len(salt):]
                ...
              }

              Это не значит, что везде надо применять каналы. В данном случае понятно, что решение на каналах избыточно (создание и освобождение структур явно лишнее). Тут применение мьютекса оправдано. Применять надо то, что удобно в конкретной задаче. И обычно в моих задачах это оказываются каналы. Прошу прощения за неточность формулировок.
  • +1
    Интересно, как бы выглядил go, если бы в гугле решили воткнуть в него подобие ARC вместо GC.
    • +2
      Не Rust ли это получится, часом?
      • +2
        В Go слишком много чего нужно было бы добавить, чтобы получился Rust. Но с другой стороны, уровня production-ready Go во многом из-за своей простоты (а также из-за того, что удалось переиспользовать компиляторы от Plan9) достиг гораздо быстрее. А в мире Rust до сих пор пытаются нащупать, каким должен быть идеальный язык.
  • –5
    Самое плохое в Go (имхо) — это обработка исключений по возвращаемым значениям.
    • +4
      Я бы назвал это не недостатком, а, скорее, особенностью языка. Исключения тоже не самый идеальный вариант, иначе они бы присутствовали в Go в полной мере. Авторы сделали выбор, который, по их мнению, лучше удовлетворяет поставленным перед ними задачам.
    • +1
      Я не пишу на Go, но так сложилось, что последние несколько лет приходится иметь дело с Java и Node.js. Так вот, в JavaScript исключения номинально тоже есть, но на деле обработка ошибок производится не через них, т.к. try ... catch не работает с асинхронным кодом. Вместо этого объект исключения передается в колбек первым параметром, и его наличие нужно проверять отдельно руками. По началу мне это тоже казалось чем-то жутко диким и неудобным, но со временем я понял, что с точки зрения того, сколько кода занимает обработка ошибок и сколько когнитивных усилий приходится на нее затрачивать, разницы вообще никакой нет. Думаю, с Go та же история.
      • +4
        В Go все проще, нежели на жутких колбеках JS.
        • 0
          Я не про сами коллбеки, а про обработку исключений: и там, и там нужно писать
          if (error) { doErrorHandling(error); }
          И и там, и там нужно про это вспомнить, и можно «проворонить» исключение, и нельзя «прогнать» обработку исключений куда-нибудь вниз большого try-catch блока, а вместо этого нужно писать логику того, что делать с каждой возможной ошибкой прямо в точке ее появления, т.д.

          В общем, языки разные, но претензии со стороны тех, у кого в языках работающий try-catch, абсолютно одинаковые.
          • +3
            К слову, язык Rust тоже не поддерживает исключений, а факт ошибки определяется типом возвращаемого значения. Но Rust поддерживает макросы, и за счёт этого проброс ошибки наверх становится очень лаконичным:

            my_fun(val) — так записывается вызов функции
            my_macro!(val) — так записывается обращение к макросу.

            fn do_smth() -> Result {
                let res1 = try!(do_sub1());
                let res2 = try!(do_sub2());
                return Ok(res1 + res2)
            }
            

            try! — это макрос, после разворота которого функция будет выглядеть так:
            fn do_smth() -> Result {
                let res1 = match do_sub1() {
                              Ok(v) => v,
                              Err(e) => return Err(e)
                           };
                let res2 = match do_sub2() {
                              Ok(v) => v,
                              Err(e) => return Err(e)
                            };
                return Ok(res1 + res2)
            }
            

            Т.е. если do_subX() вернула ошибку, то она возвращается из функции с помощью return, а если вернулось нормальное значение — оно присваивается переменной для дальнейшего использования.
            • +4
              Rust вообще становится очень привлекательным языком. Чуваки ну вот просто все делают правильно: полноценный вывод типов, RAII, по умолчанию неизменяемые данные, unsafe-блоки, продвинутые макросы и т.д. В результате мы, если повезет, получим для наших программ корректность хаскеля и скорость выполнения плюсов.

              Даже если раст в итоге не выстрелит, я думаю, что следующим системным языком-убийцей C++ будет что-то основанное на тех же идеях.
              • 0
                Ну, косяки там тоже есть, хоть я и надеюсь, что их поправят.

                Например, сейчас если у вас есть функция, которая принимает параметром коллекцию и не меняет ее (например, считает что-то на основе данных в коллекции), то в Rust вам придется написать две версии такой функции: одну для immutable коллекции, и одну — для mutable, — потому что так сейчас устроена система типов. Конечно, код внутри можно переиспользовать, но через unsafe и прочие костыли.

                • 0
                  Ну, «детские» болезни — обычный этап в развитии чего-то сложного. :-)

                  Кроме того, у ребят правильные общее направление (необходимость type inference, никаких нулевых указателей, etc.) и подход к делу: есть внятное и полное описание языка (пусть и часто меняющееся на первых порах), очень открытая разработка, активное обсуждение в рассылке и irc, дайджесты типа TWiR'а. То, что они сразу пишут параллельно Servo, и что компилятор достаточно рано стал self-hosted, — очень круто и позволяет надеяться, что язык получится как минимум жизнеспособным.
                • +4
                  Например, сейчас если у вас есть функция, которая принимает параметром коллекцию и не меняет ее (например, считает что-то на основе данных в коллекции), то в Rust вам придется написать две версии такой функции: одну для immutable коллекции, и одну — для mutable, — потому что так сейчас устроена система типов.


                  Вы что-то путаете. В Rust мутабельность определяется не структурой самой по себе, а тем, что с ней можно сделать и в каком слоте она лежит.

                  Если вы создаёте, например, коллекцию, и хотите, чтобы кто-то мог её изменить, вы пишете методы, которые меняют коллекцию, так, чтобы они принимали &mut self. Методы, которые не меняют коллекцию, будут принимать просто &self. После этого вы можете вызывать изменяющие и неизменяющие методы, если структура лежит в мутабельной переменной, и только неизменяющие, если структура лежит в немутабельной переменной. Поэтому если вы не собираетесь менять коллекцию, вам понадобится только одна функция, которая принимает &; если же вы хотите менять коллекцию, в функцию вам понадобится передавать &mut:

                  pub struct SomeCollection<T> {
                      // ...
                  }
                  
                  impl<T: Copy> SomeCollection<T> {
                      fn get(&self, i: uint) -> T { ... }
                      fn set(&mut self, i: uint, v: T) { ... }
                  }
                  
                  fn compute_something(coll: &SomeCollection<int>) -> int {
                      // здесь можно вызвать coll.get():
                      let x = coll.get(0);
                      // но вызвать coll.set() нельзя, будет ошибка компиляции
                      // coll.set(0, x+1); 
                      x
                  }
                  
                  fn compute_and_modify(coll: &mut SomeCollection<int>) -> int {
                      // здесь можно и то, и другое
                      let x = coll.get(0);
                      coll.set(0, x+1);
                      x
                  }
                  
                  fn main() {
                      // s1 - неизменяемый слот (переменная)
                      let s1: SomeCollection<int> = SomeCollection { ... };
                  
                      // можно вызвать compute_something(), но не compute_and_modify()
                      println!("{}", compute_something(&s1));
                      // println!("{}", compute_and_modify(&mut s1));
                  
                      let mut s2 = s1;  // перемещаем в мутабельный слот
                      // можно вызвать и то, и другое
                      println!("{}", compute_something(&s2));
                      println!("{}", compute_and_modify(&mut s2));
                  }
                  
                  
                  • +1
                    Я с Растом пока что на вы и очень может быть, что что-то напутал. Вот обсуждение проблемы на Реддите.

                    A common issue I've come upon in Rust is the following: you have some structure with a fairly complicated, or at least non-trivial, search procedure. The search procedure logically requires no mutation, and returns an immutable reference. However, you would also like to provide a variant that takes in a mutable version of the structure and returns a mutable reference. This does not affect the actual search procedure at all, it just changes the signature of the function (and maybe the signature of some temporary variables). However, the only way to do this in Rust at the moment is to duplicate the implementation.

                    The implementation of std::collections::TreeMap is a great example. find and find_mut are pretty much exactly the same logic, but the logic's been written out twice.
                    • +3
                      Это другое, чем то, что вы описали. Здесь мутабельность результата вычисления зависит от мутабельности исходной коллекции. И здесь да, придётся код повторять дважды, к сожалению, потому что в Rust нельзя параметризовать функции по мутабельности.

                      Однако этот юзкейс очень ограниченный. find и find_mut и аналогичные методы, возвращающие ссылки на внутренние элементы структуры данных — это чуть ли ни единственное, что страдает в этом случае. Более того, я бы сказал, что это специфично для обобщённых коллекций вроде того TreeMap; для собственных структур данных эта проблема будет возникать ещё реже.
                      • 0
                        Да, полностью согласен. Кстати, спасибо за пример!
  • +2
    А как на счет десктопного софта? Очень нужен язык стоящий между C# и Python. Важно удобство лепки GUI приложений средней руки…
  • +11
    > Вычисления моего кода просто теряются на фоне задач, которыми приходится заниматься runtime

    Это вполне может быть из-за неоптимальности кода программы. Например, cgocall/System занимает много времени, тк вы делаете 6 cgo вызовов вместо 1 или 2.

    > Ещё больше расстраивает невозможность проапгрейдить блокировку RWMutex — если блокировка в статусе RLock и обнаружилось, что необходимо внести изменения — извольте делать RUnlock(), затем Lock() и ещё раз проверять, есть ли все ещё неоходимость делать эти изменения или какая-то горутина уже всё успела сделать.

    Это принципиально нельзя сделать. Если 2 горутины захватили RLock, и пытаются проапгрейдить его до Lock, они дедлочатся. Все, что тут можно сделать, это собственно и есть RUnlock и Lock. И, да, данные могут поменяться, но от этого никуда не деться.

    > Также нет неблокирующей функции TryLock()

    Если нужно делать только TryLock, то это легко решается с помощью atomic.Swap(&x, 0, 1). Если нужно делать и TryLock и Lock, то, да, это считается слишком сложным для мьютексов,

    > Решение — запускать GC() почаще, для надежности лучше самостоятельно из программы.

    Этого ни в коем случае не надо делать. Если нужно, что бы GC запускался чаще, то нужно ставить переменную окружения GOGC в значение меньше 100. Хотя в 1.3 на время паузы это не должно особо влиять (тк sweep фаза происходит одновременнно с работой приложения), только уменьшать объем памяти

    • 0
      Это вполне может быть из-за неоптимальности кода программы. Например, cgocall/System занимает много времени, тк вы делаете 6 cgo вызовов вместо 1 или 2.
      Или наоборот, когда код настолько прост и быстр, что узким местом являются уже библиотеки и встроенные функции )
      CGO присутствует, например для geoip, и тот же crypt(), но тут никуда не деться.

      Ещё больше расстраивает невозможность проапгрейдить блокировку RWMutex
      Это принципиально нельзя сделать.
      На самом деле есть вариант без дедлока с апгрейдом через блокирующийся TryLock(), который либо проапгрейдит блокировку, либо вылетит, если кто-то ещё захочет это сделать до того, как остальные поснимают RLock(). Хотя, согласен, в общем случае лучше просто снять RLock и перелочиться.

      Решение — запускать GC() почаще, для надежности лучше самостоятельно из программы.
      Этого ни в коем случае не надо делать.
      Пардон, но работать как-то нужно, а с паузами в несколько секунд это чертовски сложно (и дорого). Про настройку GOGC в курсе, но в доке не совсем внятно описано, как именно она работает, а экспериментировать не было времени — нужно было гарантированно работающее решение. Разброс времени паузы сейчас меньше 50%.
      • +1
        А воспользоваться чем-то типа «пула ресурсов» для целей облегчения жизни GC — не лучше? Или на архитектуру не ложится?
        • 0
          Пулы ресурсов хорошо ложатся на архитектуру, но они помогут от GC, только если создание нового объекта требует много аллокаций/деаллокаций делать.

          Топикстартеру: вы много аллокаций делаете во время обработки каждого запроса? Например, если вы в цикле много-много раз делаете append к слайсу, то у вас часто будет реаллокация буфера. Если сразу отводить запас для роста побольше при помощи make, то можно прилично сэкономить. Если вы этого уже не делаете, конечно.
          • 0
            Запас я стараюсь выделять всегда. В процессе работы довольно много работы со строками — распарсить запрос, посмотреть в кеш, дернуть базу, если что… И ещё довольно много используется временных мап, без них тяжело.
          • +3
            > Пулы ресурсов хорошо ложатся на архитектуру, но они помогут от GC, только если создание нового объекта требует много аллокаций/деаллокаций делать.

            Пулы ресурсов помогают уменьшить частоту сборок мусора независимо от количества аллокаций при создании объекта. Однако пулы не уменьшают время сборки.
            В Go сборка мусора запускается, когда объем мусора в куче равен объему живых объектов. Если переиспользуются старые объекты, то количество мусора не растет.
            В Go 1.3 для пуллинга ресурсов появился специальный компонент sync.Pool.
        • 0
          В моем случае вообще не ложится — GC тормозил на очистке мусора, остающегося после обработки каждого запроса. Пусть его и не сильно много на один запрос, но так как запросы идут с высокой частотой — мусора успевает накопиться прилично.
  • +2
    > CGO присутствует, например для geoip, и тот же crypt(), но тут никуда не деться.

    От этого никуда не деться, но это не значит, что их нужно делать в неимоверных количествах. Вызовы можно агрегировать прямо в Go файле как:

    /*
    #include «somelib.h»
    void myWrapper(...) {
    … malloc(...);
    someLibCall1(...);
    someLibCall2(...);
    free(...)
    }
    */
    import «C»

    C.myWrapper(...)

    > На самом деле есть вариант без дедлока с апгрейдом через блокирующийся TryLock()

    Не вижу, как это решает проблему изменившихся данных. И как будет выглядеть код? Если TryUpgrade провалился, то что? Мы все равно делаем RUnlock и Lock? Выглядит бессмысленно

    > Пардон, но работать как-то нужно

    Не понимаю связи. Работать — надо. Вызывать runtime.GC не надо. Вместо этого нужно уменьшить GOGC, что приведет к увеличению частоты сборок.

    • +1
      Не вижу, как это решает проблему изменившихся данных. И как будет выглядеть код? Если TryUpgrade провалился, то что? Мы все равно делаем RUnlock и Lock? Выглядит бессмысленно
      Согласен.

      Не понимаю связи. Работать — надо. Вызывать runtime.GC не надо. Вместо этого нужно уменьшить GOGC, что приведет к увеличению частоты сборок.
      А есть гарантии, что уменьшение GOGC уменьшит максимальное время работы GC и разброс длительности паузы? Из документации мне это не очевидно. Но я протестирую, просто пока не было возможности.
      • +5
        > А есть гарантии, что уменьшение GOGC уменьшит максимальное время работы GC и разброс длительности паузы? Из документации мне это не очевидно. Но я протестирую, просто пока не было возможности.

        Нет, точно так же как и для ручного вызова runtime.GC.
        В 1.3 обе эти вещи не должны особо влиять на время паузы. Пауза в основном зависит от объема живых данных в программе. Вот тут я описывал это более подробно:
        software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs
        • 0
          Пауза зависит не только объема живых данных, но и от объема мертвых данных, не так ли? В моем случае скорость накопления мертвых данных довольно высока, поэтому частый запуск GC позволяет не доводить время паузы до больших значений. Проблема с автоматической работой и тюнингом через GOGC в том, что даже при значении по умолчанию GC должен бы запускаться более-менее равномерно при более-менее равномерной работе, а этого не происходит (. Если при значении 10% вместо 100% GC будет запускаться в 10 раз чаще, то это одно, но если при этом время паузы будет так же продолжать отличаться почти в сотню раз на соседних запусках, то это все равно никуда не годится. Согласитесь — куда предпочтительней иметь настройку максимального времени паузы (желательно с учетом интервала запуска GC) с разумной настройкой по умолчанию, чем подбирать проценты соотношения свежевыделенной и занятой памяти.

          Пока что не могу ничего сообщить о том, как влияет GOGC на длительность паузы в моем случае — время выхода на стабильный режим составляет много часов.
          • 0
            > Пауза зависит не только объема живых данных, но и от объема мертвых данных, не так ли?

            Не так.
            Смотрите ссылку выше — там все описано.

            > GC должен бы запускаться более-менее равномерно при более-менее равномерной работе, а этого не происходит

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

            > Согласитесь — куда предпочтительней иметь настройку максимального времени паузы

            Не соглашусь.
            Для non-concurrent mark&sweep GC такую гарантию нельзя обеспечить в общем случае. А там где можно, ее обеспечение может привести либо к чрезмерно большому потреблению памяти, либо к чрезмерно большому оверхеду на сборку мусора.
  • 0
    Мне почему то кажется, что на C++14 было бы не хуже и не труднее…
    только вот в отличии от Python где есть yield, не красиво получается асинхронный код (а в синхронном я уже разочаровался).

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