Статьи

Использование простоты Go для легкой разработки

Язык Go очень хорошо подходит для все более популярной сервис-ориентированной архитектуры.

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

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

Будь проще

На enbrite.ly мы выявляем мошеннические действия в онлайн-рекламе. В результате мы разработали и оперируем инфраструктурой, состоящей из сервисов. Большинство из этих сервисов реализованы как сервисы Go.

Мы выбрали Go, потому что это легко учиться, просто и продуктивно. За последний год мы написали более 20 сервисов с командой разработчиков из шести человек.

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

Я читал много замечательных разработчиков Go, использующих стандартную библиотеку HTTP, и подумал: «Боже, это хардкор». Но чем больше вы узнаете о стандартной HTTP-библиотеке, тем больше понимаете, что она просто гениальна и идеально подходит для сервисов.

Посмотрите этот пример о том, как создать минимальный сервис, который можно вызвать с доменом, и вернуть IP-адреса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type IPMessage struct {
    IPs []net.IP
}
 
type ErrorMessage struct {
    Error string
}
 
func IPHandler(rw http.ResponseWriter, r *http.Request) {
    domain := r.URL.Query().Get("domain")
    if len(domain) == 0 {
     json.NewEncoder(rw).Encode(ErrorMessage{"No domain parameter"})
     return
    }
    ips, err := net.LookupIP(domain)
    if err != nil {
     json.NewEncoder(rw).Encode(ErrorMessage{"Invalid domain address."})
     return
    }
    json.NewEncoder(rw).Encode(IPMessage{ips})
}
 
func main() {
    address := ":8090"
    r := http.NewServeMux()
    r.HandleFunc("/service/ip", IPHandler)
    log.Println("IP service request at: " + address)
    log.Println(http.ListenAndServe(address, r))
}

Мы определяем функцию IPHandler которая является http.HandlerFunc . В основной функции мы добавляем это к нашему маршрутизатору.

Лучше всегда явно получать маршрутизатор, чем неявно использовать значение по умолчанию; таким образом, мы можем поменяться с другим роутером, когда захотим. Если мы решим, что нам нужны параметры пути, мы можем легко переключиться на маршрутизатор Gorilla , изменив http.NewServeMux() на mux.NewRouter() . Только одна строка изменений. Маршрутизатор Gorilla — фактически единственная сторонняя библиотека, которую мы иногда используем в наших сервисах. Это хорошо написано и идеально подходит для услуг REST.

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

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

Само промежуточное ПО:

01
02
03
04
05
06
07
08
09
10
11
12
13
type M struct {
    handler http.Handler
}
 
func (m M) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    start := time.Now()
    m.handler.ServeHTTP(rw, r)
    log.Printf("%s served in %s\n", r.URL, time.Since(start))
}
 
func NewM(h http.Handler) http.Handler {
    return M{h}
}

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

1
2
3
4
5
func createBaseHandler() http.Handler {
    r := http.NewServeMux()
    r.HandleFunc("/service/ip", IPHandler)
    return NewM(r)
}

После определения базового обработчика мы добавляем его в функцию ListenAndServe .

1
2
log.Println("Service request at: " + address)
log.Println(http.ListenAndServe(address, createBaseHandler()))

Ведение журналов — это еще одна область, такая как промежуточное ПО, где люди обращаются к сторонним библиотекам. Одной из особенностей, которая возникает как недостаток стандартного пакета ведения журнала библиотеки, являются уровни ведения журнала.

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

  1. Если он не имеет ценной информации, не регистрируйте его.
  2. Если вы использовали это сообщение журнала для целей отладки, удалите его в рабочей среде.
  3. Если это ошибка, запишите ее с помощью Fatal или Panic, в зависимости от характера ошибки.
  4. Любой другой журнал идет с log.Println .

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

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

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

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

Благодаря этому простому и понятному правилу наша регистрация содержит только ценную информацию. Это уменьшает шум, но гарантирует, что у нас есть вся ценная информация, которая нам нужна для отладки, телеметрии или вскрытия.

Чтобы установить префикс, добавьте следующее в свою основную функцию перед любым оператором регистрации:

1
log.SetPrefix("[service] ")

Процесс сборки

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

Объединение Make-файлов с инструментами, предоставленными командой go, делает полнофункциональный инструмент для сборки. Сборка, тестирование, тестовое покрытие, статический анализ и форматирование — все это часть команды go. Делая их частью процесса сборки, разработчикам не нужно думать об этих шагах. Если каждый сервис использует один и тот же способ построения, каждому разработчику легко создать любой из них.

Только с помощью команды make мы проверяем, форматируем, тестируем и создаем наш сервис. Давайте рассмотрим пример того, как добавить Makefile в наш сервис:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
default: build
 
      build:
            go fmt
            go vet
            go build
     
      test: build
            go test
     
      coverage-test:
            go test -coverprofile=coverage.out
            go tool cover -func=coverage.out
            go tool cover -html=coverage.out
            rm coverage.out

С этим унифицированным Makefile каждый член команды проверяет, тестирует и форматирует код одинаково. Это очень помогает с обзорами кода. Не нужно обсуждать форматирование. Go’s ветеринар уже проверил код на наличие распространенных ошибок. Если процесс сборки пройден, вы можете быть на 100 процентов уверены, что нет неиспользуемых переменных или импортов.

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

Чтобы проанализировать тестовое покрытие, используйте команду make coverage-test . Он запускает тесты и отображает результаты в HTML в вашем браузере, чтобы вы могли видеть, что охватывают ваши тесты, а что нет. Супер легкое тестовое покрытие, обеспечиваемое стандартными инструментами Go.

Всегда добавляйте пользовательский интерфейс. Без исключений

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

Доверие к тому, что каждая служба доступна и проверена по http://<service address>/ дает разработчикам уверенность и отправную точку, когда им приходится отлаживать, разрабатывать или просто знакомиться с новой службой.

щ-для-codeship-статьи

С Go очень легко добавить интерфейс к любому сервису. Шаблоны предлагают простой способ генерации HTML. Добавьте Bootstrap CSS, и у вас будет рабочий интерфейс. Начните с рутины, и вам даже не придется обрабатывать параллелизм. Запуск сайта администратора параллельно с самим сервисом удивительно прост с Go.

Во-первых, создайте базовый HTML-шаблон с некоторой загрузкой CSS, таблицей для запросов и модулем javascript datatables, который добавляет функции, такие как разбиение на страницы, поиск и сортировку, в обычные таблицы HTML.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <link rel="stylesheet" href="http://d2fq26gzgmfal8.cloudfront.net/bootstrap.min.css" media="screen">
    <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/r/bs/dt-1.10.9/datatables.min.css" />
</head>
 
<body>
    <div class="container">
        <div class="row">
            <h1>Domain checker</h1>
        </div>
        <div class="row">
            <table class="table table-bordered" id="queries">
                <thead>
                    <tr>
                        <th>Domain</th>
                        <th>IPs</th>
                    </tr>
                </thead>
                <tbody>
                    {{ range .Queries }}
                    <tr>
                        <td><a href="http://{{ .Domain }}">{{ .Domain }} </a>
                        </td>
                        <td>{{ .IPs }}</td>
                    </tr>
                    {{ end }}
                </tbody>
            </table>
        </div>
    </div>
    <script type="text/javascript" src="https://cdn.datatables.net/r/bs/dt-1.10.9/datatables.min.js"></script>
    <script>
        $(document).ready(function()
        {
            $('#queries').DataTable();
        });
    </script>
</body>
</html>

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

01
02
03
04
05
06
07
08
09
10
11
12
func uiHandler(rw http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("templates/index.html")
    if err != nil {
        log.Panic("Error occured parsing the template", err)
    }
    page := PageData{
        Queries: queries,
    }
    if err = tmpl.Execute(rw, page); err != nil {
        log.Panic("Failed to write template", err)
    }
}

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

Мы используем одну структуру для регистрации запросов и их результатов. Другой для шаблона, который описывает пользовательский интерфейс.

01
02
03
04
05
06
07
08
09
10
// The Query type represents a query against our service.
type Query struct {
    Domain string
    IPs    []net.IP
}
 
// The data struct for our basic UI
type PageData struct {
    Queries []Query
}

Где мы определили наш предыдущий обработчик в нашей функции createBaseHandler , мы можем добавить конечную точку пользовательского интерфейса.

1
r.HandleFunc("/", uiHandler)

И наконец, мы добавляем каждый запрос, который мы выполняем в нашем IPHandler к глобальной переменной. Переменная:

1
var queries = []Query{}

В функции IPHandler :

1
queries = append(queries, Query{domain, ips})

Каждому члену команды нравится, что мы предоставляем интерфейс для наших услуг. Эта политика очень помогает, когда новые члены команды пытаются понять, что делает конкретная служба. Пользовательский интерфейс дает им функциональное представление о том, для чего используется служба. Также легче увидеть, что происходит со службой, если вы можете проверить ее в своем браузере и не нужно запрашивать БД.

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

Равномерное сердцебиение

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

  • Это работает вообще?
  • Если он работает, какая сборка работает?

И мы хотим предоставлять эту информацию для каждого сервиса, который мы пишем и работаем. С Go это не может быть проще.

Мы предоставляем библиотеку, которая запускает веб-сервис на заданном порту . При вызове он возвращает код SHA-1 коммита, использованного для создания двоичного файла, времени работы и состояния. Чтобы использовать эту библиотеку, все, что нам нужно, это добавить go heartbeat.RunHeartbeatService(portnum) в нашу основную функцию.

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

Для DNS мы используем AWS Route53; он также использует этот сигнал пульса, чтобы увидеть, какие IP-адреса действительны для данной записи DNS.

Чтобы установить код commit SHA-1 при сборке нашего сервиса, мы изменили команду go build в нашем Makefile на:

1
go build --ldflags="-X github.com/enbritely/heartbeat-golang.CommitHash`git rev-parse HEAD"

Он добавит хэш SHA-1 в библиотеку сердцебиения, которая вернет его при вызове http://<service-address/heartbeat URL http://<service-address/heartbeat .

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

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

Чтобы включить службу пульса, прослушивающую адрес, настроенный в переменной среды HEARTBEAT_ADDRESS, добавьте следующие две строки в свою основную функцию.

1
2
hAddress := os.Getenv(“HEARTBEAT_ADDRESS”)
go heartbeat.RunHeartbeatService(hAddress)

Чтобы установить службу сердцебиения, вы должны пойти получить его.

1
go get github.com/enbritely/heartbeat-golang

конфигурация

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

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

Переменные среды легко настраиваются на компьютерах разработчиков: вы можете определить их в командной строке или экспортировать в файл .bashrc или .profile . Мы сделали это еще проще.

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

Например, если вы не хотите запускать БД на своем компьютере, вы можете установить адрес БД так, чтобы он указывал на любое место, которое вы хотите. Некоторые из нас используют Docker для запуска локальной БД, некоторые используют AWS RDS для тестирования. С переменными среды и файлом .dotenv это не проблема.

Вывод

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

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

Какой у тебя опыт работы с Go? У вас есть другие лучшие практики? Пожалуйста, поделитесь этим в комментариях!