Статьи

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

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

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

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

«Простота Go означает, что разработчики могут быстрее входить на борт и лучше сотрудничать». — через @gulyasm

Исходный код примера сервиса доступен на GitHub:

https://github.com/gulyasm/goservice

Будь проще

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

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

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

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

«Чем больше вы узнаете о стандартной библиотеке HTTP, тем больше понимаете, что это чистый гений».

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

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функции вместо базового обработчика. Опять же, нет необходимости в сторонних библиотеках; простое и элегантное решение.

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

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методом. Чтобы сделать наш код чище и более читабельным, мы можем выделить сборку нашего базового обработчика в отдельную функцию.

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

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

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

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

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

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

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

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

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

«Предоставьте более подробные сообщения об ошибках. Ваша команда поблагодарит вас. — через @gulyasm

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

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

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

log.SetPrefix("[service] ")

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

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

«Создание и развертывание приложения должно быть простым и автоматическим». — через @gulyasm

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

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

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.

<!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 src="http://d2fq26gzgmfal8.cloudfront.net/jquery-1.10.2.min.js"></script>
    <script type="text/javascript" src="https://cdn.datatables.net/r/bs/dt-1.10.9/datatables.min.js"></script>
    <script src="http://d2fq26gzgmfal8.cloudfront.net/bootstrap.min.js"></script>
    <script>
        $(document).ready(function()
        {
            $('#queries').DataTable();
        });
    </script>
</body>
</html>

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

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список, чтобы мы могли отобразить его в пользовательском интерфейсе.

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

// 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функции, мы можем добавить конечную точку пользовательского интерфейса.

r.HandleFunc("/", uiHandler)

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

var queries = []Query{}

В IPHandlerфункции:

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

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

«Каждому члену команды нравится, что мы предоставляем интерфейс для наших услуг». — через @gulyasm

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

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

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

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

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

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

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

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

To set the commit SHA-1 code when building our service, we change the go build command in our Makefile to:

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

It will add the SHA-1 hash to the heartbeat library, that will return it when http://<service-address/heartbeat url is called.

Another simple solution, and again, it’s easy for anyone to remember that every service has this heartbeat. It can be used to get the running version or to check the status. Not a planned advantage, but it also helped a lot later with automation. When we introduced automatic failover, where one service is replaced with another in case of failure, we used this heartbeat feature to check for the health of the service.

Another example of later advantage was when one of our teammates created a website where all the deployed services were listed. Because we had this heartbeat service, we could just query all the services and display the page with the results.

To enable the heartbeat service listening on the address configured in the HEARTBEAT_ADDRESS environmental variable, add the following two lines to your main function.

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

To install the heartbeat service, you have to go get-it.

go get github.com/enbritely/heartbeat-golang

Configuration

I like to keep the configuration simple too. We follow the 12 Factor Application technique that describes that configuration should come from the environment. That means environmental variables.

“I like to keep configuration simple too. That means environmental variables.” – via @gulyasm

The simplicity of it makes it very flexible. It’s easy to set different configurations in production than testing, staging, or development. We use our service startup scripts to set these variables in production, so they’re available when the service starts.

Environmental variables are easy to set up on development machines: You can define them on the command line or export them in your .bashrc or .profile file. We made it even easier.

Every developer defines a .dotenv file in the root folder of the project. It contains our environmental variables; sourcing them will set the development configuration. It’s fast, easy, and every developer can adjust it.

For example, if you don’t want to run a DB on your machine, you can set the DB address to point to any location you want. Some of us use Docker to run local DB, some of us use AWS RDS for testing. With environmental variables and the .dotenv file, it’s not a problem.

Conclusion

Software development should be fun. It’s in the whole team’s best interest that we go for it if there are parts that we can make fun. Making code reviews, deployment, testing, or monitoring easier is a great place to start.

Go has great support for all of that. Making our development environment uniform across each project helps existing or new team members to get on board easier. Making services accessible helps debugging. The UI creates a human-readable interface for our services. All of these practices help our team focus on things that matter.

“It’s in a team’s best interest to make development fun where you can. Go is great for that.”

What’s your own experience with Go? Do you have other best practices? Please share it in the comments!