Часть 1. Введение

Зачем нужен GitLab Cluster?

В процессе роста нашей инфраструктуры мы столкнулись с тем, что Single Node (all-in-one) инсталляции GitLab стало недостаточно. Производительность начала снижаться, а любое обновление или сбой сервиса приводило к простою всей разработки. Поэтому мы приняли решение перейти на отказоустойчивый GitLab Cluster с возможностью бесшовных обновлений (zero downtime upgrade).

Для автоматизированного развёртывания и управления кластером мы выбрали Ansible, так как:

  • Он позволяет быстро и повторяемо разворачивать инфраструктуру.

  • Хорошо интегрируется с существующими CI/CD процессами.

  • Позволяет централизованно управлять конфигурацией и обновлениями.

  • Поддерживает идемпотентность, что минимизирует риски ошибок при повторных запусках.

В этой статье я подробно расскажу, как мы развернули GitLab Cluster с использованием Ansible и организовали бесшовные обновления.

Версию используем Community Edition.

Роли для раскатки PostgreSQL, HAProxy и Redis ищите отдельно, тут их нет.

Ниже представлена схема нашей исходной архитектуры GitLab, когда он был развёрнут на одном сервере:

Скрытый текст

Часть 2. Архитектура GitLab Cluster

Основные компоненты кластера

После перехода на кластерное развертывание наша архитектура значительно изменилась. Теперь GitLab состоит из нескольких сервисов, разнесённых по различным узлам:

  • 2 сервера Rails (4 vCPU, 8 RAM, 30 SSD) – основной веб-интерфейс и API.

  • 3 сервера Gitaly (4 vCPU, 4 RAM, 50 SSD) – отвечает за доступ к репозиториям Git.

  • 3 сервера Praefect (2 vCPU, 2 RAM, 30 SSD) – прокси-слой для управления реплицированными экземплярами Gitaly.

  • 2 сервера Sidekiq (4 vCPU, 8 RAM, 30 SSD) – обработка фоновых задач.

  • 1 сервер PostgreSQL (4 vCPU, 8 RAM, 50 SSD)– база данных, и для Praefect, и для Rails.

  • 1 сервер Redis (2 vCPU, 2 RAM, 20 SSD) – используется для кеширования и фоновых заданий.

  • 1 сервер HAProxy – балансировщик нагрузки.

В этой статье не рассматриваются:

  • Настройка PostgreSQL.

  • Настройка Redis.

  • Настройка HAProxy.

Схема новой архитектуры

Скрытый текст

картинку вставлю чуть позже

Как обеспечивается отказоустойчивость?

  • Rails развернут в двух экземплярах за HAProxy, что обеспечивает балансировку нагрузки и отказоустойчивость.

  • Sidekiq распределён между двумя серверами, что позволяет выполнять фоновые задачи без простоев.

  • Praefect управляет репликацией Gitaly, предотвращая потери данных.

  • Gitaly распределён между несколькими узлами для балансировки нагрузки.

  • Балансировщик нагрузки (HAProxy) направляет трафик на доступные инстансы, включая Rails и Praefect.

Часть 3. Развёртывание кластера с помощью Ansible

Подготовка окружения

Перед началом развертывания кластера необходимо подготовить серверы и настроить Ansible-инвентарь. Важно убедиться, что у вас есть доступ по SSH ко всем узлам и что на них установлены необходимые пакеты.

Для автоматизации развертывания была использована следующая Ansible-роль: gitlab-omnibus.

Для управления развертыванием мы используем следующий инвентарь, который описывает все компоненты кластера и содержит групповые переменные:

environments/dev/ybd.yml:

Скрытый текст
---
    haproxy:
      children:
        haproxy_dev:
          hosts:
            ybd-bln-hpr01:
              ansible_host: 10.0.220.189
              ansible_port: 222
    gitlab_cluster:
      children:
        gitlab_rails:
          hosts:
            ybd-git-rai01:
              ansible_host: 10.0.220.180
            ybd-git-rai02:
              ansible_host: 10.0.220.40
        gitlab_sidekiq:
          hosts:
            ybd-git-sid01:
              ansible_host: 10.0.220.181
            ybd-git-sid02:
              ansible_host: 10.0.220.86
        gitlab_praefect:
          hosts:
            ybd-git-prf01:
              ansible_host: 10.0.220.182
              gitlab_omnibus_praefect_override:
                auto_migrate: true
            ybd-git-prf02:
              ansible_host: 10.0.220.183
            ybd-git-prf03:
              ansible_host: 10.0.220.184
        gitlab_gitaly:
          hosts:
            ybd-git-gtl01:
              ansible_host: 10.0.220.185
            ybd-git-gtl02:
              ansible_host: 10.0.220.186
            ybd-git-gtl03:
              ansible_host: 10.0.220.187
        gitlab_postgresql:
          hosts:
            ybd-git-psg01:
              ansible_host: 10.0.220.188
        gitlab_redis:
          hosts:
            ybd-git-red01:
              ansible_host: 10.0.220.190
              master: true

environments/dev/group_vars/haproxy_dev/variables.yml:

Скрытый текст
---
haproxy_packet: "{{ haproxy_packet_name }}"
haproxy_packet_name: "haproxy"
haproxy_defaults_log: 127.0.0.1:5140 len 65535 local0
haproxy_global_nbthread: 2
haproxy_global_raw_options:
  - stats socket /var/lib/haproxy/stats mode 660 level admin expose-fd listeners
  - stats timeout 30s

haproxy_logformat_tcp: &haproxy_logformat_tcp
  logformat: >-
    '{"appname":"haproxy","@timestamp":"%Ts","pid":%pid,"haproxy_frontend_type":"tcp","haproxy_process_concurrent_connections":%ac,
    "haproxy_frontend_concurrent_connections":%fc,"haproxy_backend_concurrent_connections":%bc,
    "haproxy_server_concurrent_connections":%sc,"haproxy_backend_queue":%bq,"haproxy_server_queue":%sq,
    "haproxy_queue_wait_time":%Tw,"haproxy_server_wait_time":%Tc,"response_time":%Td,"session_duration":%Tt,
    "request_termination_state":"%tsc","haproxy_server_connection_retries":%rc,"remote_addr":"%ci","remote_port":%cp,
    "frontend_addr":"%fi","frontend_port":%fp,"frontend_ssl_version":"%sslv","frontend_ssl_ciphers":"%sslc",
    "haproxy_frontend_name":"%f","haproxy_backend_name":"%b","haproxy_server_name":"%s","response_size":%B,
    "request_size":%U}'

haproxy_logformat_http: &haproxy_logformat_http
  logformat: >-
    '{"appname":"haproxy","@timestamp":"%Ts","pid":%pid,"haproxy_frontend_type":"http","haproxy_process_concurrent_connections":%ac,
    "haproxy_frontend_concurrent_connections":%fc,"haproxy_backend_concurrent_connections":%bc,
    "haproxy_server_concurrent_connections":%sc,"haproxy_backend_queue":%bq,"haproxy_server_queue":%sq,"haproxy_client_request_send_time":%Tq,
    "haproxy_queue_wait_time":%Tw,"haproxy_server_wait_time":%Tc,"haproxy_server_response_send_time":%Tr,
    "response_time":%Td,"session_duration":%Tt,"request_termination_state":"%tsc","haproxy_server_connection_retries":%rc,
    "remote_addr":"%ci","remote_port":%cp,"frontend_addr":"%fi","frontend_port":%fp,"frontend_ssl_version":"%sslv",
    "frontend_ssl_ciphers":"%sslc","request_method":"%HM","request_uri":"%[capture.req.uri,json(utf8s)]",
    "request_http_version":"%HV","host":"%[capture.req.hdr(0)]","referer":"%[capture.req.hdr(1),json(utf8s)]",
    "haproxy_frontend_name":"%f","haproxy_backend_name":"%b","haproxy_server_name":"%s","status":%ST,"response_size":%B,
    "request_size":%U}'
haproxy_defaults_option:
  - forwardfor
  - dontlognull

haproxy_default_raw_options:
  - backlog 10000
  - retries 3

haproxy_defaults_timeout:
  - type: connect
    timeout: 5000
  - type: client
    timeout: 50000
  - type: server
    timeout: 101000
  - type: http-request
    timeout: 10000
  - type: queue
    timeout: 30000

haproxy_frontend:
  - name: http
    description: Front-end for all HTTP traffic
    bind:
      - listen: "0.0.0.0:80"
      - listen: "0.0.0.0:443 ssl crt /opt/testlab.com.pem crt /opt/testlab.org.pem crt /opt/pages.testlab.org.pem alpn h2,http/1.1"
    mode: http
    <<: *haproxy_logformat_http
    raw_options:
      - redirect scheme https unless { ssl_fc }
      - http-request redirect scheme https code 301 unless { ssl_fc }
    acl:
      - string: host_gitlab-backup hdr(host) -i git-backup.testlab.org
      - string: host_pages-backup hdr_end(host) -i pages-backup.testlab.org

    http_request:
      - action: capture
        param: req.hdr(Host) len 1000
      - action: capture
        param: req.hdr(Referer) len 1000
    use_backend:
      - gitlab-backup if host_gitlab-backup
      - pages-backup if host_pages-backup

  - name: exporter
    bind:
      - listen: 0.0.0.0:8404
    <<: *haproxy_logformat_http
    option:
      - http-use-htx
    http_request:
      - action: use-service
        param: prometheus-exporter
        cond: "if { path /metrics }"
    raw_options:
      - stats enable
      - stats uri /stats
      - stats refresh 10s
  - name: gitlab-backup-ssh
    description: gitlab-backup-ssh
    bind:
      - listen: "0.0.0.0:22"
    mode: tcp
    option:
      - tcplog
      - clitcpka
    <<: *haproxy_logformat_tcp
    default_backend: gitlab-backup-ssh
  - name: praefect-back
    description: praefect-back
    bind:
      - listen: "0.0.0.0:2305"
    option:
      - tcplog
      - clitcpka
    mode: tcp
    <<: *haproxy_logformat_tcp
    default_backend: git-prafect-back

haproxy_backend:
  - name: git-prafect-back
    description: git-prafect-back
    mode: tcp
    balance: roundrobin
    options:
      - srvtcpka
      - tcp-check
    server:
      - name: ybd-git-prf01
        listen: ybd-git-prf01:2305 check
      - name: ybd-git-prf02
        listen: ybd-git-prf02:2305 check
      - name: ybd-git-prf03
        listen: ybd-git-prf03:2305 check
  - name: gitlab-backup
    description: gitlab-backup http
    mode: http
    balance: roundrobin
    http_check: expect string '{"status":"ok"}'
    option:
      - httpchk GET /-/liveness
      - tcp-check
      - srvtcpka
    server:
      - name: ybd-git-rai01
        listen: ybd-git-rai01:80 check
      - name: ybd-git-rai02
        listen: ybd-git-rai02:80 check
  - name: pages-backup
    description: pages-backup http
    mode: http
    balance: roundrobin
    http_check: expect string '{"status":"ok"}'
    option:
      - httpchk GET /-/liveness
      - tcp-check
      - srvtcpka
    server:
      - name: ybd-git-rai01
        listen: ybd-git-rai01:8081 check port 80
      - name: ybd-git-rai02
        listen: ybd-git-rai02:8081 check port 80
  - name: gitlab-backup-ssh
    description: gitlab-backup-ssh
    mode: tcp
    balance: roundrobin
    http_check: expect string '{"status":"ok"}'
    option:
      - httpchk GET /-/liveness
      - tcp-check
      - srvtcpka
    server:
      - name: ybd-git-rai01
        listen: ybd-git-rai01:22 check port 80
      - name: ybd-git-rai02
        listen: ybd-git-rai02:22 check port 80

vector_version: "0.31.0"
vector_configs:
  - name: haproxy
    config:
      sources:
        local_haproxy:
          address: 127.0.0.1:5140
          max_length: 102400
          mode: udp
          type: syslog
      sinks:
        sink_kafka:
          type: kafka
          inputs:
            - local_haproxy
          bootstrap_servers: "{{ vector_kafka_url }}"
          topic: "infra-haproxy"
          compression: none
          encoding:
            codec: text
          tls:
            enabled: true

environments/dev/group_vars/gitlab_cluster/variables.yml:

Скрытый текст
---
gitlab_omnibus_packet_version: 17.7.3-ce.0
gitlab_omnibus_restic_password: "{{ vault_gitlab_omnibus_restic_password }}"
gitlab_omnibus_haproxy_hosts:
  - ybd-bln-hpr01

gitlab_omnibus_gitaly:
  data_hosts:
    - ybd-git-gtl01:8075
    - ybd-git-gtl02:8075
    - ybd-git-gtl03:8075

gitlab_omnibus_shell:
  secret_token: "{{ vault_gitlab_omnibus_shell_secret_token }}"

gitlab_omnibus_pages_external_url: https://pages-backup.testlab.org

gitlab_omnibus_pages:
  gitlab_server: "https://git-backup.testlab.org"
  gitlab_secret: "{{ vault_gitlab_omnibus_pages_gitlab_secret }}"
  api_secret_key: "{{ vault_gitlab_omnibus_pages_api_secret_key }}"

gitlab_omnibus_praefect:
  external_address: tcp://git-backup.testlab.org:2305
  praefect_external_token: "{{ vault_gitlab_omnibus_praefect_praefect_external_token }}"
  praefect_internal_token: "{{ vault_gitlab_omnibus_praefect_praefect_internal_token }}"
  database_host: ybd-git-psg01
  database_user: gitlab_praefect_backup
  database_password: "{{ vault_gitlab_omnibus_praefect_database_password }}"
  database_dbname: gitlab_praefect_backup

gitlab_omnibus_rails:
  gitlab_email_from: noreply@testlab.com
  gitlab_email_reply_to: noreply@testlab.com
  smtp_authentication: login
  smtp_domain: testlab.com
  smtp_enable: false
  gitlab_email_enabled: false
  gitlab_shell_ssh_port: 22
  smtp_address: lalala.testlab.com
  smtp_port: 587
  smtp_password: "{{ vault_gitlab_omnibus_rails_smtp_password }}"
  smtp_openssl_verify_mode: peer
  smtp_enable_starttls_auto: true
  smtp_user_name: noreply@testlab.com
  ldap_servers_main_password: "{{ vault_gitlab_omnibus_rails_ldap_servers_main_password }}"
  ldap_servers_main_label: LDAP
  ldap_servers_main_host: 'testlab.com'
  ldap_servers_main_port: 636
  ldap_servers_main_uid: 'sAMAccountName'
  ldap_servers_main_encryption: 'simple_tls'
  ldap_servers_main_bind_dn: 'CN=gitlab ldap,OU=Service Accounts,OU=Organization,DC=testlab,DC=com'
  ldap_servers_main_base: 'OU=Organization,DC=testlab,DC=com'
  initial_root_password: "{{ vault_gitlab_omnibus_rails_initial_root_password }}"
  db_host: ybd-git-psg01
  db_port: 5432
  db_database: gitlab_rails_backup
  db_username: gitlab_rails_backup
  db_password: "{{ vault_gitlab_omnibus_rails_db_password }}"
  external_url: https://git-backup.testlab.org
  internal_api_url: https://git-backup.testlab.org
  secret_key_base: "{{ vault_gitlab_omnibus_rails_secret_key_base }}"
  otp_key_base: "{{ vault_gitlab_omnibus_rails_otp_key_base }}"
  db_key_base: "{{ vault_gitlab_omnibus_rails_db_key_base }}"
  encrypted_settings_key_base: "{{ vault_gitlab_omnibus_rails_encrypted_settings_key_base }}"
  openid_connect_signing_key: "{{ vault_gitlab_omnibus_rails_openid_connect_signing_key }}"
  ci_jwt_signing_key: "{{ vault_gitlab_omnibus_rails_ci_jwt_signing_key }}"
  packages_enable: false
  object_store:
    enabled: true
    proxy_download: true
    connection:
      aws_access_key_id: "{{ vault_gitlab_omnibus_rails_object_store_connection_aws_access_key_id }}"
      aws_secret_access_key: "{{ vault_gitlab_omnibus_rails_object_store_connection_aws_secret_access_key }}"
      endpoint: https://storage.yandexcloud.net
      host: storage.yandexcloud.net
    objects:
      artifacts_bucket: gitlab-prod-artifacts
      external_diffs_bucket: gitlab-prod-external-diffs
      lfs_bucket: gitlab-prod-lfs
      uploads_bucket: gitlab-prod-uploads
      packages_bucket: crutch
      dependency_proxy_bucket: crutch2
      terraform_state_bucket: gitlab-prod-terraform-state
      pages_bucket: gitlab-prod-pages
      ci_secure_files_bucket: gitlab-prod-ci-secure-files
      backup_bucket: backup-gitlab
  redis:
    sentinels:
      - host: ybd-git-red01
        port: 26379
    password: "{{ vault_gitlab_omnibus_rails_redis_password }}"
    master_name: gitlab_redis

postgresql_databases:
  - name: "{{ gitlab_omnibus_rails.db_database }}"
    owner: "{{ gitlab_omnibus_rails.db_username }}"
  - name: "{{ gitlab_omnibus_praefect.database_dbname }}"
    owner: "{{ gitlab_omnibus_praefect.database_user }}"

postgresql_users:
  - name: "{{ gitlab_omnibus_rails.db_username }}"
    role_attr_flags: superuser
    password: "{{ gitlab_omnibus_rails.db_password }}"
    db: "{{ gitlab_omnibus_rails.db_database }}"
  - name: "{{ gitlab_omnibus_praefect.database_user }}"
    role_attr_flags: createdb
    password: "{{ gitlab_omnibus_praefect.database_password }}"
    db: "{{ gitlab_omnibus_praefect.database_dbname }}"

postgresql_hba_entries:
  - type: local
    database: all
    user: postgres
    auth_method: peer
  - type: local
    database: all
    user: all
    auth_method: peer
  - type: host
    database: all
    user: all
    address: '127.0.0.1/32'
    auth_method: "{{ postgresql_auth_method }}"
  - type: host
    database: all
    user: all
    address: '10.0.0.0/8'
    auth_method: "{{ postgresql_auth_method }}"
  - type: host
    database: all
    user: all
    address: '172.16.0.0/12'
    auth_method: "{{ postgresql_auth_method }}"
  - type: host
    database: all
    user: all
    address: '::1/128'
    auth_method: "{{ postgresql_auth_method }}"

postgresql_global_config_options:
  - option: listen_addresses
    value: '*'
  - option: max_connections
    value: 500
  - option: shared_buffers
    value: 1GB
  - option: effective_cache_size
    value: 3GB
  - option: maintenance_work_mem
    value: 256MB
  - option: checkpoint_completion_target
    value: 0.9
  - option: wal_buffers
    value: 16MB
  - option: default_statistics_target
    value: 100
  - option: random_page_cost
    value: 1.1
  - option: effective_io_concurrency
    value: 200
  - option: work_mem
    value: 2621kB
  - option: huge_pages
    value: 'off'
  - option: min_wal_size
    value: 1GB
  - option: max_wal_size
    value: 4GB

gitlab_omnibius_ssh_keys:
  - path: /etc/ssh/ssh_host_dsa_key
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_dsa_key }}"
    mode: '0600'
  - path: /etc/ssh/ssh_host_dsa_key.pub
    mode: '0644'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_dsa_key_pub }}"
  - path: /etc/ssh/ssh_host_ecdsa_key
    mode: '0600'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_ecdsa_key }}"
  - path: /etc/ssh/ssh_host_ecdsa_key.pub
    mode: '0644'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_ecdsa_key_pub }}"
  - path: /etc/ssh/ssh_host_ed25519_key
    mode: '0600'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_ed25519_key }}"
  - path: /etc/ssh/ssh_host_ed25519_key.pub
    mode: '0644'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_ed25519_key_pub }}"
  - path: /etc/ssh/ssh_host_rsa_key
    mode: '0600'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_rsa_key }}"
  - path: /etc/ssh/ssh_host_rsa_key.pub
    mode: '0644'
    content: "{{ vault_gitlab_omnibus_gitlab_ssh_keys_ssh_host_rsa_key_pub }}"

redis_server_password: "{{ vault_gitlab_omnibus_rails_redis_password }}"
redis_sentinel_password: "{{ vault_gitlab_omnibus_rails_redis_password }}"
redis_node_roles:
  - master
  - sentinel
redis_mastername: gitlab_redis
redis_masteruser: gitlab_redis
redis_masterauth: "{{ vault_gitlab_omnibus_rails_redis_password }}"
redis_sentinel_monitors:
  - name: gitlab_redis
    host: ybd-git-red01
    port: 6379
    quorum: 2
    auth_pass: "{{ vault_gitlab_omnibus_rails_redis_password }}"
    down_after_milliseconds: 1000
    parallel_syncs: 1
    failover_timeout: 1000
    notification_script: false
    rename_commands: []

redis_sentinel_extra_config:
  sentinel:
    resolve_hostnames: "yes"
    announce_hostnames: "yes"

Развёртывание кластера

Развёртывание производится с помощью единого Ansible-плейбука, который автоматически настраивает все компоненты кластера. Плейбук включает:

  1. Настройку и создание баз данных PostgreSQL и Redis.

  2. Установку и настройку Rails, Gitaly, Praefect, Sidekiq.

  3. Настройку конфигурации для каждого сервиса.

  4. Запуск служб и проверку их работоспособности.

Пример плейбука:

playbooks/gitlab_cluster/gitlab_cluster.yml:

Скрытый текст
---
- name: pull gitlab secrets from vault
  hosts: gitlab_cluster
  tasks:
    - name: Read the vault
      community.hashi_vault.vault_kv2_get:
        url: 'https://vault.testlab.org'
        path: '{{ environments }}/gitlab_cluster'
        auth_method: 'approle'
        role_id: "{{ vault_role_id }}"
        secret_id: "{{ vault_secret_id }}"
        engine_mount_point: 'kv-devops'
      register: secrets
    - name: set env
      ansible.builtin.set_fact:
        "{{ item.key }}": "{{ item.value }}"
      loop: "{{ secrets.secret | dict2items }}"
      no_log: true
  tags:
    - gitlab
    - postgresql
    - redis-cluster

- hosts: gitlab_postgresql
  become: true
  roles:
    - postgresql-local
  tags:
    - postgresql

- hosts: gitlab_redis
  become: true
  roles:
    - redis-cluster
  tags:
    - redis-cluster

- hosts:
    - gitlab_cluster:!gitlab_postgresql:!gitlab_redis
  become: true
  roles:
    - gitlab-omnibus
  tags:
    - gitlab

для запуска можно использовать команду

ansible-playbook -i environments/dev playbooks/gitlab_cluster/gitlab_cluster.yml

После успешного выполнения всех шагов кластер GitLab готов к работе.

Часть 4. Zero-Downtime Upgrade (Бесшовное обновление)

Зачем нужны бесшовные обновления?

Одной из ключевых задач при эксплуатации кластера GitLab является обновление без простоев. Нам необходимо было обеспечить бесперебойную работу всех сервисов, чтобы пользователи не испытывали проблем в процессе обновления.

Рекомендую ознакомиться с статьей Zero-downtime upgrades.

В статье указан рекомендуемый порядок обновления и действия, которые необходимо соблюсти.

Порядок обновления следующий:

  1. Gitaly

    • Обновление производится поочерёдно для каждой ноды Gitaly.

    • Перед обновлением ноду временно исключают из пула, проводят обновление, затем возвращают в рабочую группу, что позволяет поддерживать доступность Git-операций.

  2. Praefect

    • После успешного обновления Gitaly переходим к обновлению Praefect, который выступает в роли маршрутизатора запросов к Gitaly.

    • Также используется поочерёдное обновление, чтобы обеспечить корректное распределение запросов во время переходного периода.

    • В процессе обновления конкретная нода выводится из roundrobin на HAProxy, чтобы в неё не поступали новые запросы. Это позволяет избежать потенциальных ошибок или некорректного распределения нагрузки.

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

  3. Rails

    • После обновления инфраструктурных компонентов (Gitaly и Praefect) начинается обновление Rails-серверов.

    • Обновление первой ноды:

      • Исключение из HAProxy: Первую ноду исключают из roundrobin HAProxy, чтобы на неё не поступали новые запросы.

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

      • Возврат в пул: После проверки работоспособности ноды её возвращают в пул HAProxy, и трафик снова распределяется между всеми серверами.

    • Обновление остальных нод:

      • Каждую последующую ноду предварительно исключают из HAProxy, затем проводят обновление (без повторного запуска миграций) и после проверки возвращают обратно в пул.

  4. Sidekiq

    • После обновления Rails происходит обновление процессов Sidekiq, отвечающих за выполнение фоновых задач.

    • Перезапуск и контроль очередей:

      • Обновление Sidekiq выполняется так, чтобы новые воркеры корректно обрабатывали как оставшиеся задачи предыдущей версии, так и новые поступающие задачи.

      • Обычно процессы Sidekiq перезапускаются после обновления Rails, чтобы обеспечить совместимость.

Для обновления можно использовать следующий плейбук:

playbooks/gitlab_cluster/gitlab_cluster_upgrade.yml:

Скрытый текст
---
- name: pull gitlab secrets from vault
  hosts: gitlab_cluster
  tasks:
    - name: Read the vault
      community.hashi_vault.vault_kv2_get:
        url: 'https://vault.testlab.org'
        path: '{{ environments }}/gitlab_cluster'
        auth_method: 'approle'
        role_id: "{{ vault_role_id }}"
        secret_id: "{{ vault_secret_id }}"
        engine_mount_point: 'kv-devops'
      register: secrets
    - name: set env
      ansible.builtin.set_fact:
        "{{ item.key }}": "{{ item.value }}"
      loop: "{{ secrets.secret | dict2items }}"
      no_log: true

- name: upgrade_gitaly
  hosts: gitlab_gitaly
  become: true
  roles:
    - gitlab-omnibus
  vars:
    gitlab_omnibus_upgrade_gitaly: true
  serial: 1

- name: upgrade_praefect
  hosts: gitlab_praefect
  become: true
  roles:
    - gitlab-omnibus
  vars:
    gitlab_omnibus_upgrade_praefect: true
  serial: 1

- name: upgrade_rails[0]
  hosts: gitlab_rails[0]
  become: true
  roles:
    - gitlab-omnibus
  vars:
    gitlab_omnibus_upgrade_rails: true
  serial: 1

- name: db_migrate over gitlab_rails[0]
  hosts: gitlab_rails[0]
  become: true
  roles:
    - gitlab-omnibus
  vars:
    gitlab_omnibus_db_migrate: true
  serial: 1
  run_once: true

- name: upgrade_rails[1]
  hosts: gitlab_rails[1]
  become: true
  roles:
    - gitlab-omnibus
  vars:
    gitlab_omnibus_upgrade_rails: true
  serial: 1

- name: upgrade_sidekiq
  hosts: gitlab_sidekiq
  become: true
  roles:
    - gitlab-omnibus
  vars:
    gitlab_omnibus_upgrade_sidekiq: true
  serial: 1

После успешного выполнения всех шагов кластер GitLab обновлен и готов к работе.

Часть 5. Выводы

Итоги проделанной работы

В ходе внедрения отказоустойчивого GitLab Cluster мы смогли:

  • Перейти от Single Node к распределённой архитектуре, улучшив надёжность и отказоустойчивость системы.

  • Настроить автоматизированное развёртывание инфраструктуры с помощью Ansible, что позволило упростить управление и сократить время на развёртывание новых узлов.

  • Реализовать бесшовное обновление (Zero-Downtime Upgrade), обеспечив непрерывную работу сервиса даже в моменты обновления критических компонентов.

Основные рекомендации

  • Планирование критично: перед каждым обновлением или изменением в инфраструктуре необходимо тщательно тестировать изменения в staging-среде.

  • Идемпотентность Ansible: важно поддерживать плейбуки в идемпотентном состоянии, чтобы минимизировать риски сбоев при повторных запусках.

  • Мониторинг и логирование: важно настроить мониторинг критических сервисов (например, через Prometheus и Grafana), чтобы оперативно выявлять возможные проблемы.

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

  • Обновляйтесь только на один минорный релиз за раз. То есть, с 17.1 до 17.2, а не до 17.3. Если пропустить релизы, изменения в базе данных могут выполняться в неправильной последовательности, что приведёт к нарушению схемы базы данных.

  • Перед обновлением убедитесь что завершены все Background migrations (admin/background_migrations) после предыдущего обновления.

Этот опыт помог нам повысить стабильность инфраструктуры, упростить её поддержку и обеспечить разработчикам бесперебойный доступ к инструментам CI/CD.

Такой подход может быть полезен для команд, которые стремятся внедрить отказоустойчивую версию GitLab и минимизировать время простоя при обновлениях.

И немного метрик

Было:

Скрытый текст

Стало:

Скрытый текст

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


  1. alexq2
    05.02.2025 17:47

    Топовая отказоустойчивость! Один редис, одна база, один балансер!


    1. BaCuJIuu Автор
      05.02.2025 17:47

      Для стейдж контура сойдёт, редис заскейлить несложно, обращение через sentinel, использовать pgbouncer и несколько инстансом postgresql тоже не запрещено, главное помнить что при обновлении гитлаба, необходимо переключиться напрямую к мастер постгре.

      Из статьи Zero-downtime upgrades:

      In addition to the above, Rails is where the main database migrations need to be executed. Like Praefect, the best approach is by using the deploy node. If PgBouncer is currently being used, it also needs to be bypassed as Rails uses an advisory lock when attempting to run a migration to prevent concurrent migrations from running on the same database. These locks are not shared across transactions, resulting in ActiveRecord::ConcurrentMigrationError and other issues when running database migrations using PgBouncer in transaction pooling mode.

      On the Rails deploy node run the post-deployment migrations:

      1. Ensure the deploy node is still pointing at the database leader directly. If the node is currently going through PgBouncer to reach the database then you must bypass it and connect directly to the database leader before running migrations.

        • To find the database leader you can run the following command on any database node - sudo gitlab-ctl patroni members.


  1. Ivan_Zalutskii
    05.02.2025 17:47

    Теперь у вас есть +13 серверов под GitLab. Не расматривали использование Kubernetes, на меньшем количестве серверов, внутрикоторого поднято все то, что описано в статье?


    1. BaCuJIuu Автор
      05.02.2025 17:47

      рассматривали, нам не подходит)


    1. pit_art
      05.02.2025 17:47

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