<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Яков Захаров</title><subtitle>Системный администратор Linux</subtitle><author><name>Яков Захаров</name></author><id>https://teletype.in/atom/yashumitsu</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/yashumitsu?offset=0"></link><link rel="alternate" type="text/html" href="https://yashumitsu.com/?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=yashumitsu"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/yashumitsu?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-10T22:35:58.432Z</updated><entry><id>yashumitsu:cLNAkVtW4i3</id><link rel="alternate" type="text/html" href="https://yashumitsu.com/cLNAkVtW4i3?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=yashumitsu"></link><title>Кеширующий прокси-сервер для geoipupdate</title><published>2025-12-15T06:45:41.018Z</published><updated>2025-12-17T03:10:39.313Z</updated><category term="admin" label="Admin"></category><summary type="html">Потребность в актуализации GeoIP-баз, как правило, реализуется через установку на каждом сервере утилиты geoipupdate.</summary><content type="html">
  &lt;p id=&quot;N9sy&quot;&gt;Потребность в актуализации GeoIP-баз, как правило, реализуется через установку на каждом сервере утилиты &lt;a href=&quot;https://github.com/maxmind/geoipupdate&quot; target=&quot;_blank&quot;&gt;geoipupdate&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;TmH6&quot;&gt;Но оказывается, при таком подходе легко столкнуться с суточными ограничениями на скачивание бесплатной версии GeoLite:&lt;/p&gt;
  &lt;ul id=&quot;7LY0&quot;&gt;
    &lt;li id=&quot;oVpD&quot;&gt;&lt;a href=&quot;https://www.maxmind.com/en/geolite-free-ip-geolocation-data&quot; target=&quot;_blank&quot;&gt;https://www.maxmind.com/en/geolite-free-ip-geolocation-data&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;BdHU&quot;&gt;Можно изменить архитектуру, выделив под обновления отдельный сервер и  распространять базы с него. Однако, такое решение требует самописной обвязки, для доставки обновлений на конечные хосты.&lt;/p&gt;
  &lt;p id=&quot;Raap&quot;&gt;Обратим внимание, что &lt;em&gt;geoipupdate&lt;/em&gt; поддерживает переопределение сервера обновлений:&lt;/p&gt;
  &lt;pre id=&quot;aT9p&quot;&gt;# The server to use. Defaults to &amp;quot;https://updates.maxmind.com&amp;quot;.
# Host https://updates.maxmind.com&lt;/pre&gt;
  &lt;p id=&quot;WQdh&quot;&gt;и работу через прокси-сервер:&lt;/p&gt;
  &lt;pre id=&quot;aT9p&quot;&gt;# The proxy host name or IP address. You may optionally specify a
# port number, e.g., 127.0.0.1:8888. If no port number is specified, 1080http,
# will be used.
# Proxy 127.0.0.1:8888&lt;/pre&gt;
  &lt;p id=&quot;dNAw&quot;&gt;Развёртывание полноценного прокси-сервера (например Squid) затруднительно в Kubernetes, из-за метода CONNECT, которые стандартные Ingress не поддерживают.&lt;/p&gt;
  &lt;p id=&quot;xK3N&quot;&gt;Зато, имитация API сервиса обновлений MaxMind, выглядит подходящим решением.&lt;/p&gt;
  &lt;p id=&quot;VEdZ&quot;&gt;Разберём алгоритм работы &lt;em&gt;geoipupdate&lt;/em&gt;:&lt;/p&gt;
  &lt;p id=&quot;LYL6&quot;&gt;    1. &lt;em&gt;geoipupdate&lt;/em&gt; считает контрольную сумму локальной версии базы (допустим Country) и запрашивает у &lt;em&gt;updates.maxmind.com&lt;/em&gt; информацию об актуальной версии:&lt;/p&gt;
  &lt;pre id=&quot;rv7v&quot;&gt;&amp;gt; Host: updates.maxmind.com
&amp;gt; GET /geoip/updates/metadata?edition_id=GeoLite2-Country HTTP/2
&amp;gt; Authorization: Basic &amp;lt;лицензионный ключ&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;7wLc&quot;&gt;    2.&lt;em&gt; updates.maxmind.com&lt;/em&gt; отвечает в следующем формате:&lt;/p&gt;
  &lt;pre id=&quot;mHOs&quot;&gt;{
  &amp;quot;databases&amp;quot;: [
    {
      &amp;quot;date&amp;quot;: &amp;quot;2025-11-25&amp;quot;,
      &amp;quot;edition_id&amp;quot;: &amp;quot;GeoLite2-Country&amp;quot;,
      &amp;quot;md5&amp;quot;: &amp;quot;7f97170bfe5f384a14ec6cb49a538d33&amp;quot;
    }
  ]
}&lt;/pre&gt;
  &lt;p id=&quot;jH0V&quot;&gt;    3. Если хеш не совпадает — требуется обновление, тогда geoipupdate  обращается к &lt;em&gt;download.maxmind.com&lt;/em&gt;:&lt;/p&gt;
  &lt;pre id=&quot;yao9&quot;&gt;&amp;gt; Host: download.maxmind.com
&amp;gt; GET /geoip/databases/GeoLite2-Country/download?date=20251125&amp;amp;suffix=tar.gz HTTP/2
&amp;gt; Authorization: Basic &amp;lt;лицензионный ключ&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;bVZk&quot;&gt;    4. &lt;em&gt;download.maxmind.com&lt;/em&gt; отвечает редиректом, на скачивание файла напрямую из S3&lt;/p&gt;
  &lt;p id=&quot;ynB4&quot;&gt;    5. &lt;em&gt;geoipupdate&lt;/em&gt; следует по ссылке и таким образом начинается загрузка файла&lt;/p&gt;
  &lt;p id=&quot;n5Vz&quot;&gt;Всё что нам нужно, это спроксировать запросы от &lt;em&gt;geoipupdate&lt;/em&gt; к сервисам &lt;em&gt;updates&lt;/em&gt; и &lt;em&gt;download.maxmind.com&lt;/em&gt; и закешировать их ответы.&lt;/p&gt;
  &lt;p id=&quot;VG1Q&quot;&gt;Решение этой задачи уже реализовано в виде сервиса на Go:&lt;/p&gt;
  &lt;ul id=&quot;3DPu&quot;&gt;
    &lt;li id=&quot;uxdl&quot;&gt;&lt;a href=&quot;https://github.com/gabe565/geoip-cache-proxy&quot; target=&quot;_blank&quot;&gt;https://github.com/gabe565/geoip-cache-proxy&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;kE6B&quot;&gt;однако оставалось предчувствие, что для реализации похожей функциональности достаточно базового Nginx.&lt;/p&gt;
  &lt;p id=&quot;phnx&quot;&gt;Так получилась следующая конфигурация:&lt;/p&gt;
  &lt;pre id=&quot;QDz1&quot;&gt;# Указание резолвера является требованием Nginx при использовании
# переменной ($follow_upstream_http_location) в proxy_pass
resolver ${NGINX_LOCAL_RESOLVERS};

# Логируем был ли использован кеш ($upstream_cache_status) 
log_format geoipupdate &amp;#x27;$remote_addr - $remote_user [$time_local] &amp;#x27;
                    &amp;#x27;&amp;quot;$request&amp;quot; $status $body_bytes_sent &amp;#x27;
                    &amp;#x27;&amp;quot;$http_referer&amp;quot; &amp;quot;$http_user_agent&amp;quot; &amp;quot;$upstream_cache_status&amp;quot;&amp;#x27;;

proxy_cache_path /var/cache/nginx/geoipupdate levels=1:2
    keys_zone=geoipupdate_cache:1m
    max_size=500M
    inactive=14d
    use_temp_path=off;

server {
    listen       8080;
    server_name  _;

    access_log  /var/log/nginx/access.log  geoipupdate;

    # По умолчанию, proxy_cache_key основывается на $proxy_host,
    # т.е. upstream-домене.
    # В результате кеш записывается, но фактически не используется.
    # Запросы продолжают доходить до proxy_pass и проксироваться в maxmind.com.
    # Поэтому, в составе ключа кеширования используется
    # переменная $host (домен самого прокси-сервера).
    # Это позволяет проверить кеш на попадание, до обращения к proxy_pass.
    proxy_cache_key $host$uri$is_args$args;

    # (1) Проксируем запрос на получение метаданных, в сервис updates.maxmind.com.
    #
    # Кешируем его ответ не непродолжительное время.
    location /geoip/updates/ {
        proxy_pass             https://updates.maxmind.com;
        proxy_ssl_server_name  on;

        proxy_cache                    geoipupdate_cache;
        proxy_cache_valid              200 1d;
        proxy_cache_use_stale          error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update  on;
        proxy_cache_lock               on;
        proxy_cache_lock_timeout       30s;

        # Игнорируем заголовки запрещающие кеширование.
        proxy_ignore_headers Cache-Control Expires Set-Cookie;
        add_header X-Proxy-Cache $upstream_cache_status;
    }

    # (2) Проксируем запрос на скачивание определённой версии базы,
    # в сервис download.maxmind.com.
    #
    # Данный ресурс отвечает 302-редиректом,
    # содержащим динамическую ссылку на S3-хранилище.
    # Nginx перехватывает редирект и передаёт
    # его обработку в internal-location @follow_redirect.
    #
    # То есть, вместо отдачи редиректа как есть, Nginx переходит по нему
    # за нас, чтобы закешировался итоговый файл, а не ссылка на него. 
    location /geoip/databases/ {
        # Отвечаем из кеша при наличии в нём запрашиваемой версии БД.
        proxy_cache geoipupdate_cache;

        proxy_pass             https://download.maxmind.com;
        proxy_ssl_server_name  on;

        # Перехватываем редирект в ответе бекенда и передаём его в internal-location.
        proxy_intercept_errors on;
        error_page 302 = @follow_redirect;
    }

    # (3) Обработчик перехода по ссылке из заголовка Location ответа upstream.
    location @follow_redirect {
        internal;

        # При входе в location, сохраняем текущее
        # значение переменной $upstream_http_location из шага 2.
        # Поскольку после вызова proxy_pass оно будет перезаписано.
        set $follow_upstream_http_location $upstream_http_location;

        # Переходим в S3 по ссылке из редиректа.
        proxy_pass $follow_upstream_http_location;
        proxy_ssl_server_name on;

        # Исключаем проксирование заголовка Authorization в Amazon S3,
        # поскольку в обратном случае он отвечает ошибкой:
        #   &amp;gt; Missing x-amz-content-sha256
        proxy_set_header Authorization &amp;quot;&amp;quot;;

        # Сохраняем ответ S3 в кеш.
        proxy_cache                    geoipupdate_cache;
        proxy_cache_valid              200 7d;
        proxy_cache_use_stale          error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update  on;
        proxy_cache_lock               on;
        proxy_cache_lock_timeout       30s;

        proxy_ignore_headers Cache-Control Expires Set-Cookie;
        add_header X-Proxy-Cache $upstream_cache_status;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;ztG8&quot;&gt;Остаётся прописать URL-нашего прокси, в параметре Host конфигурации GeoIP.conf.&lt;/p&gt;

</content></entry><entry><id>yashumitsu:TdkOVlpkhL6</id><link rel="alternate" type="text/html" href="https://yashumitsu.com/TdkOVlpkhL6?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=yashumitsu"></link><title>Обзор реализаций конструкторов объектов в Go</title><published>2025-07-07T03:47:50.151Z</published><updated>2025-12-14T04:03:25.659Z</updated><category term="go" label="Go"></category><summary type="html">В поисках идеального конструктора: с поддержкой обязательных и опциональные аргументов, дефолтных значений, возврата ошибок и простотой кода.</summary><content type="html">
  &lt;h3 id=&quot;sPFy&quot;&gt;Functional Options&lt;/h3&gt;
  &lt;blockquote id=&quot;HpjD&quot;&gt;Источники:&lt;br /&gt;  1. &lt;a href=&quot;https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis&quot; target=&quot;_blank&quot;&gt;https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis&lt;/a&gt;&lt;br /&gt;  2. &lt;a href=&quot;https://rednafi.com/go/configure_options/&quot; target=&quot;_blank&quot;&gt;https://rednafi.com/go/configure_options/&lt;/a&gt;&lt;/blockquote&gt;
  &lt;p id=&quot;JCW6&quot;&gt;Начнём с наиболее распространённого в Go подхода — &lt;strong&gt;Functional Options&lt;/strong&gt;.&lt;/p&gt;
  &lt;pre id=&quot;0LEX&quot; data-lang=&quot;go&quot;&gt;type User struct {
	// Обязательный параметр
	Email string

	// Опциональные параметры
	Name   string
	Public bool
}

type UserOption func(*User) error

func WithName(name string) UserOption {
	return func(u *User) error {
		u.Name = name
		return nil
	}
}

func WithPublicFlag(u *User) error {
	u.Public = true
	return nil
}

func NewUser(email string, options ...UserOption) (*User, error) {
	// Инициализируем объект с обязательными полями.
	// Здесь же можно задать дефолтные значения для опциональных полей.
	u := &amp;amp;User{
		Email: email,
	}

	for _, option := range options {
		if err := option(u); err != nil {
			return nil, err
		}
	}

	return u, nil
}

func main() {
	user, _ := NewUser(&amp;quot;ivan@example.com&amp;quot;)
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com Name: Public:false}

	user, _ = NewUser(&amp;quot;ivan@example.com&amp;quot;, WithName(&amp;quot;Ivan Ivanich&amp;quot;), WithPublicFlag)
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com Name:Ivan Ivanich Public:true}
}&lt;/pre&gt;
  &lt;p id=&quot;avXX&quot;&gt;Преимущества:&lt;/p&gt;
  &lt;ul id=&quot;0xAT&quot;&gt;
    &lt;li id=&quot;yiAh&quot;&gt;Валидация ввода (поддержка возврата ошибки)&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;aZmY&quot;&gt;Недостатки:&lt;/p&gt;
  &lt;ul id=&quot;FEwY&quot;&gt;
    &lt;li id=&quot;Cq4N&quot;&gt;IDE не показывает доступные опции конструктора, нужно явно просматривать структуру &lt;em&gt;UserOption&lt;/em&gt;&lt;/li&gt;
    &lt;li id=&quot;O0X9&quot;&gt;На практике, With-опций  вызываются с префиксом названия пакета, что может выглядеть чуждо&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;TwtZ&quot;&gt;Configurable Object&lt;/h3&gt;
  &lt;blockquote id=&quot;8ucF&quot;&gt;Источники:&lt;br /&gt;  1. &lt;a href=&quot;https://rednafi.com/go/dysfunctional_options_pattern/&quot; target=&quot;_blank&quot;&gt;https://rednafi.com/go/dysfunctional_options_pattern/&lt;/a&gt;&lt;/blockquote&gt;
  &lt;p id=&quot;gAXD&quot;&gt;Реализация с отдельной структурой для хранения опциональных параметров:&lt;/p&gt;
  &lt;pre id=&quot;XeaT&quot; data-lang=&quot;go&quot;&gt;type User struct {
	// Обязательный параметр
	Email string

	UserOptions
}

// Опциональные параметры
type UserOptions struct {
	Name   string
	Public bool
}

func (userOption UserOptions) WithName(name string) UserOptions {
	userOption.Name = name
	return userOption
}

func (userOption UserOptions) WithPublic() UserOptions {
	userOption.Public = true
	return userOption
}

func NewUser(email string, options UserOptions) *User {
	// Описываем объект с обязательными полями.
	// Здесь же можно задать дефолтные значения для опциональных полей.
	u := &amp;amp;User{
		Email:       email,
		UserOptions: options,
	}

	return u
}

func main() {
	user := NewUser(&amp;quot;ivan@example.com&amp;quot;, UserOptions{})
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com UserOptions:{Name: Public:false}}

	user = NewUser(&amp;quot;ivan@example.com&amp;quot;, UserOptions{}.WithName(&amp;quot;Ivan Ivanich&amp;quot;).WithPublic())
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com UserOptions:{Name:Ivan Ivanich Public:true}}
}&lt;/pre&gt;
  &lt;p id=&quot;mL2f&quot;&gt;Выявляется менее наглядная работа с ошибками, валидацию в &lt;em&gt;With&lt;/em&gt;-опции встроить не получится, только описать на уровне &lt;em&gt;NewUser()&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;i4NY&quot;&gt;Также сигнатура конструктора обязывает передавать &lt;em&gt;options&lt;/em&gt;, даже в неиспользуемом случае. Здесь можно упомянуть альтернативу — &lt;strong&gt;Builder pattern&lt;/strong&gt;, реализующую цепочку методов, мутирующих основную структуру:&lt;/p&gt;
  &lt;pre id=&quot;HmBz&quot; data-lang=&quot;go&quot;&gt;type User struct {
	// Обязательный параметр
	Email string

	// Опциональные параметры
	Name   string
	Public bool
}

func (u *User) WithName(name string) *User {
	u.Name = name
	return u
}

func (u *User) WithPublicFlag() *User {
	u.Public = true
	return u
}

func NewUser(email string) *User {
	return &amp;amp;User{
		Email: email,
	}
}

func main() {
	user := NewUser(&amp;quot;ivan@example.com&amp;quot;)
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com Name: Public:false}

	user = NewUser(&amp;quot;ivan@example.com&amp;quot;).WithName(&amp;quot;Ivan Ivanich&amp;quot;).WithPublicFlag()
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com Name:Ivan Ivanich Public:true}
}&lt;/pre&gt;
  &lt;p id=&quot;LRhd&quot;&gt;Однако в ней полностью исключается возврат ошибок. И допускается модификация приватных полей, после создания объекта.&lt;/p&gt;
  &lt;p id=&quot;Jlul&quot;&gt;Также важным недостатком, объединяющим оба примера, является смешивание методов конструктора и основной структуры в автодополнении IDE.&lt;/p&gt;
  &lt;h3 id=&quot;A7hA&quot;&gt;Структура Options&lt;/h3&gt;
  &lt;blockquote id=&quot;sbi3&quot;&gt;Источники:&lt;br /&gt;  1. &lt;a href=&quot;https://asankov.dev/post/different-ways-to-initialize-go-structs&quot; target=&quot;_blank&quot;&gt;https://asankov.dev/post/different-ways-to-initialize-go-structs&lt;/a&gt;&lt;/blockquote&gt;
  &lt;p id=&quot;AcYl&quot;&gt;Вместо дополнительного кода ради &lt;strong&gt;Configurable Object&lt;/strong&gt;, напрашивается использование простой структуры:&lt;/p&gt;
  &lt;pre id=&quot;9Fz5&quot; data-lang=&quot;go&quot;&gt;type User struct {
	// Обязательный параметр
	Email string

	UserOptions
}

// Опциональные параметры
type UserOptions struct {
	Name   string
	Public bool
}

func NewUser(email string, options UserOptions) *User {
	u := &amp;amp;User{
		Email:       email,
		UserOptions: options,
	}

	return u
}

func main() {
	user := NewUser(&amp;quot;ivan@example.com&amp;quot;, UserOptions{})
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com UserOptions:{Name: Public:false}}

	user = NewUser(&amp;quot;ivan@example.com&amp;quot;, UserOptions{
		Name:   &amp;quot;Ivan Ivanich&amp;quot;,
		Public: true,
	})
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com UserOptions:{Name:Ivan Ivanich Public:true}}
}&lt;/pre&gt;
  &lt;p id=&quot;I1qX&quot;&gt;Если мы имеем дело с приватными полями, поможет прокси-структура:&lt;/p&gt;
  &lt;pre id=&quot;geTr&quot; data-lang=&quot;go&quot;&gt;type User struct {
	email, name string
	public      bool
}

// Опциональные параметры
type UserOptions struct {
	Name   string
	Public bool
}

func NewUser(email string, options UserOptions) *User {
	u := &amp;amp;User{
		email: email,
	}

	// Такой метод сравнения работает если структура состоит только из comparable-полей
	//   - https://stackoverflow.com/questions/28447297/how-to-check-for-an-empty-struct
	if options == (UserOptions{}) {
		return u
	}

	if options.Name != &amp;quot;&amp;quot; {
		u.name = options.Name
	}

	if options.Public != false {
		u.public = options.Public
	}

	return u
}

func main() {
	user := NewUser(&amp;quot;ivan@example.com&amp;quot;, UserOptions{})
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com UserOptions:{Name: Public:false}}

	user = NewUser(&amp;quot;ivan@example.com&amp;quot;, UserOptions{
		Name:   &amp;quot;Ivan Ivanich&amp;quot;,
		Public: true,
	})
	fmt.Printf(&amp;quot;%+v\n&amp;quot;, user)
	// &amp;amp;{Email:ivan@example.com UserOptions:{Name:Ivan Ivanich Public:true}}
}&lt;/pre&gt;
  &lt;p id=&quot;aKKG&quot;&gt;Из недостатков, сохраняется обязательность аргумента &lt;em&gt;options&lt;/em&gt; в конструкторе.&lt;/p&gt;

</content></entry></feed>