[Анализ и проектирование систем, R] Опыт внедрения Shiny в качестве корпоративной отчетности

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
03-Дек-2020 17:31

Всем привет! Меня зовут Сергей, я аналитик в ГК «Везёт». Исторически так сложилось, что в нашей компании было множество систем отчетности: от платных в виде Looker и Qlick – до самописных веб-сервисов. Однажды решив, что так дальше жить нельзя, мы стали выбирать единую систему, на которой будет все, и в итоге остановились на Shiny. В этой статье я расскажу про наш опыт внедрения Shiny в качестве корпоративного BI. Эта статья будет полезна всем, кто только выбирает инструмент для корпоративной отчетности.
Содержание

Причины выбора Shiny

Краткий рассказ о том, что такое Shiny

SPL
Shiny – это пакет для R, который позволяет создавать интерактивные веб-приложения и отчеты.
Официальный сайт проекта.
Здесь можно посмотреть примеры веб-приложений и дашбордов.

  • Динамические UI – в ряде отчетов, в зависимости от условий выбора нужно было показывать дополнительные фильтры, селекторы.
  • Гибкость настройки отчетов – в разных BI использовался разный функционал работы с отчетами, конечный бизнес-пользователь хотел сохранить функционал изначального отчета.
  • Контроль изменений в отчетах с помощью гита – это для удобства, нужно было понимать кто, когда и что менял в отчете.
  • Быстрое прототипирование сложных отчетов – иногда конечный бизнес-пользователь может не до конца понимать, какой результат он хочет в итоге, поэтому желательно быстрее пройти процесс согласования конечного функционала отчета.
  • Малое потребление серверных ресурсов.
  • Удобная отладка отчетов – т.к. периодически пользователи запрашивают сложные отчеты, нужна его удобная отладка, чтобы можно было понять, в каком месте отчет не работает.
  • Бесплатность – как дополнительный плюс

Выбираем способ развертывания приложений
Созданный отчет должен как-то увидеть конечный бизнес-пользователь. Для этого есть несколько решений.
Shiny Server (Бесплатный)
Запуск всех отчетов на одном процессе R
Плюсы:
  • Неограниченный хостинг приложений.
  • Простота в настройке.

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


Источник картинки
Shiny Server Pro или RStudio Connect (Платный)
Запуск разных отчетов в разных процессах R.
Плюсы:
  • Неограниченный хостинг приложений.
  • Неограниченное количество одновременных пользователей.
  • Множество способов аутентификации пользователя.
  • Можно установить пароль на отдельное приложение.
  • Работа приложений в нескольких процессах.
  • Простота в настройке.

Минусы:
  • Не нашел.


Источник картинки
ShinyProxy (Бесплатный)
В данном продукте используется концепция запуска каждого приложения пользователя в отдельном докере.
Плюсы:
  • Неограниченный хостинг приложений.
  • Неограниченное количество одновременных пользователей.
  • Множество способов аутентификации пользователя.

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


Источник картинки
Load Balancing (Платный)
Развитие концепции ShinyProxy используется, если ваш контейнер с приложением очень тяжелый и запускается около минуты и более. Решение состоит в прединициализации контейнера, к которому подключаются пользователи.
Плюсы:
  • Неограниченный хостинг приложений.
  • Неограниченное количество одновременных пользователей.
  • Множество способов аутентификации пользователя.

Минусы:
  • Load Balancing инициализирует контейнер, поэтому он может потреблять много оперативной памяти сервера.


Источник картинки
После рассмотрения различных вариантов решили остановиться на ShinyProxy.
Настройка ShinyProxy
В целом настройка по инструкции прошла стандартно, были настроены два сервера и HAProxy в качестве балансировщика между ними.
Для аутентификации был выбран протокол LDAP через корпоративный Active Directory.
Итоговые настройки ShinyProxy на двух серверах выглядят так.
proxy:
  title: ShinyProxy
  bind-address:
    - 0.0.0.0
  heartbeat-timeout: 60000
  container-wait-time: 60000
  authentication: ldap
  admin-groups: Domain Users
# LDAP configuration
  ldap:
    url: ldap://host:port/dc=ad,dc=corp
    user-search-base:
    user-search-filter: (sAMAccountName={0})
    group-search-base: OU=Groups
    group-search-filter: (member={0})
    manager-dn: CN=shinyproxy,CN=Users,DC=ad,DC=corp
    manager-password: password
# Docker configuration
  docker:
    cert-path: /home/none
    url: http://localhost:2375
    port-range-start: 20010
    port-range-max: 20900

В процессе эксплуатации выяснилось следующее:
  • ShinyProxy теряет некоторые контейнеры докера и не может их найти для повторного подключения, если прошло больше получаса. Решено было повесить на крон килл контейнеров, которые работают больше 45 мин.
    */5 * * * * docker ps --format='{{.Names}}' | grep -v cadvisor | xargs -n 1 -r docker inspect -f '{{.ID}} {{.State.Running}} {{.State.StartedAt}}' | awk '$2 == "true" && $3 <= "'$(date -d '45 minutes ago' -Ins --utc | sed 's/+0000/Z/')'" { print $1 }' | xargs -r docker kill
  • Блокировщик рекламы запрещал доступ на отчеты, у которых был префикс webstat. Поэтому мы попросили всех пользователей отключить блокировщики рекламы на сайте с отчетностью.

Деплой отчетов
Исходники всех отчетов хранятся в GitLab. Его и решили использовать в качестве инструмента загрузки отчетов на сервер с помощью пайплайнов. Для этого было сделано следующее:
  • Был создан "главный" докер, в котором установлен ShinyServer, и основной набор пакетов для R. Таким образом мы уменьшим суммарный объем занимаемого дискового пространства и ускорим сборку отчетов.

Исходник главного докера

SPL
FROM rocker/r-ver:latest
RUN apt-get update -y && \
  apt-get install --no-install-recommends -y sudo gdebi-core pandoc pandoc-citeproc libcurl4-gnutls-dev libcairo2-dev libxt-dev wget libpq-dev \
    libjpeg-dev libssl-dev libprotobuf-dev libjq-dev protobuf-compiler libudunits2-dev gdal-bin proj-bin libgdal-dev libproj-dev #gnupg dirmngr
RUN apt-get update && \
  apt-get install --no-install-recommends -y java-common
RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
RUN TEMP_DEB="$(mktemp)" && \
  wget -O "$TEMP_DEB" 'https://launchpad.net/~webupd8team/+archive/ubuntu/java/+build/12469417/+files/oracle-java8-installer_8u131-1~webupd8~2_all.deb' && \
  dpkg -i "$TEMP_DEB" && \
  rm -f "$TEMP_DEB"
RUN wget --no-verbose https://download3.rstudio.org/ubuntu-14.04/x86_64/VERSION  -O "version.txt" && \
    VERSION=$(cat version.txt)  && \
    wget --no-verbose "https://download3.rstudio.org/ubuntu-14.04/x86_64/shiny-server-$VERSION-amd64.deb" -O ss-latest.deb && \
    gdebi -n ss-latest.deb && \
    rm -f version.txt ss-latest.deb && \
    . /etc/environment && \
    rm -rf /var/lib/apt/lists/*
RUN R -e "install.packages(c('highcharter','clickhouse','data.table','DataCombine','DBI','devtools','dplyr','dqshiny','DT','esquisse','forcats','ggplot2','ggthemes','gridExtra','hexbin','htmlwidgets','lattice','lazyeval','leaflet','leaflet.extras','lubridate','openxlsx','pivottabler','plotly','raster','RClickhouse','reactable','readxl','reshape','reshape2','rgdal','rhandsontable','RPostgres','RPostgreSQL','RSQLite','scales','sf','shiny','shinycssloaders','shinydashboard','shinyjqui','shinyjs','shinyMatrix','shinyTime','shinyWidgets','sjmisc','sp','sqldf','stringr','tibble','tidyr','writexl','yaml','geosphere'), repos='https://cran.rstudio.com/')"
RUN apt-get update && \
  apt-get install -y r-cran-rjava
RUN R CMD javareconf
RUN R -e "install.packages(c('RJDBC'), repos='https://cran.rstudio.com/')"
RUN java -version

  • "Главный" докер импортируется в каждом отчете

Пример докер файла отчета

SPL
FROM host:port/db/for_shiny_reports/shiny_docker:latest # Главный докер
EXPOSE 3838
COPY app /srv/shiny-server/app
RUN mkdir -p /var/lib/shiny-server/bookmarks/shiny
CMD ["R", "-e", "shiny::runApp('/srv/shiny-server/app', port = 3838, host = '0.0.0.0')"]

  • В каждом проекте прописан пайпалйн сборки, таким образом каждый раз, когда мы оставляем комит в отчете, то начинается пересборка отчета.

Пример пайплайна сборки отчета(.gitlab-ci.yml&#41;

SPL
stages:
- build
variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  DOCKER_DRIVER: overlay2
build-master:
  image: docker:18.09.7
  services:
  - name: docker:18.09.7-dind
  stage: build
  before_script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - apk add --update curl
    - docker pull $CI_REGISTRY_IMAGE || true
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest --pull -t $CI_REGISTRY_IMAGE .
    - docker run -d -e SHINYPROXY_USERNAME=$SHINYPROXY_USERNAME1 -e SHINYPROXY_USERGROUPS=$SHINYPROXY_USERGROUPS1  -p 0.0.0.0:3838:3838/tcp $CI_REGISTRY_IMAGE R -e "shiny::runApp('/srv/shiny-server/app', port = 3838, host = '0.0.0.0')"
    - sleep 30 && curl -Is http://docker:3838/ | grep "200 OK"
    - docker push $CI_REGISTRY_IMAGE
  only:
    - master

  • Т.к. за день может быть несколько правок по различным отчетам и добавление какого-нибудь нового отчета, то заливку докер-образов на сервера было решено вынести в отдельный проект с отдельным CI/CD и использованием плейбуков Ansible. При добавлении нового отчета будет заново пересоздаваться конфиг для ShinyProxy. Ниже шаблон генерации конфига для Ansible

Шаблон application.yml

SPL
proxy:
  title: ShinyProxy
  bind-address:
    - 0.0.0.0
  heartbeat-timeout: 60000
  container-wait-time: {{ shinyproxy_container_wait_time }}
  authentication: {{ shinyproxy_authentication }}
  admin-groups: {{ shinyproxy_admin_group }}
  usage-stats-url: {{shinyproxy_usage_stats_url}}
  usage-stats-username: {{shinyproxy_usage_stats_username}}
  usage-stats-password: {{shinyproxy_usage_stats_password}}
# LDAP configuration
  ldap:
    url: {{ shinyproxy_ldap_server }}
    user-search-base: {{ shinyproxy_ldap_user_search_base }}
    user-search-filter: {{ shinyproxy_ldap_user_search_filter }}
    group-search-base: {{ shinyproxy_ldap_group_search_base }}
    group-search-filter: {{ shinyproxy_ldap_group_search_filter }}
    manager-dn: {{ shinyproxy_ldap_admin }}
    manager-password: {{ shinyproxy_ldap_admin_pwd }}
# Docker configuration
  docker:
    cert-path: /home/none
    url: {{ shinyproxy_docker_url }}
    port-range-start: {{ shinyproxy_docker_port_range_start }}
    port-range-max: {{ shinyproxy_docker_port_range_max }}
  specs:
{% if shinyproxy_apps is defined %}
{% for app in shinyproxy_apps %}
  - id: {{ app.id }}
    display-name: {{ app.display_name }}
{% if app.description is defined %}
    description: {{ app.description }}
{% endif %}
    container-cmd: ["R", "-e", "shiny::runApp('/srv/shiny-server/app', port = 3838, host = '0.0.0.0')"]
    container-image: {{ CI_REGISTRY }}/db/shinyproxy/{{ app.container_image }}
#    docker-memory: {{ app.docker_memory | default('2g') }}
{% if app.access_groups is defined %}
    access-groups: [{{ app.access_groups }}]
{% endif %}
    groups: [{{ app.groups }}]
{% if app.container_volumes is defined %}
    container-volumes: ["{{ app.container_volumes }}"]
{% endif %}
{% endfor %}
{% endif %}
server:
  servlet.session.timeout: 900
logging:
  file:
    shinyproxy.log

Этот шаблон подается в задачник ansible

Задачи по заливике отчетов и сборке конфига

SPL
---
- name: Log into registry and force re-authorization
  docker_login:
    registry: "{{ CI_REGISTRY }}"
    username: gitlab-ci-token
    password: "{{ CI_JOB_TOKEN }}"
    reauthorize: true
- name: Pull the docker images
  command: docker pull host:port/db/shinyproxy/{{ item.container_image }}:latest
  with_items: "{{ shinyproxy_apps }}"
- name: Install the shinyproxy configuration file
  template: src=shinyproxy-conf.yml.j2 dest=/etc/shinyproxy/application.yml
  become: true
  when: not ansible_check_mode
  notify: Restart shinyproxy
- name: Ensure that the shinyproxy service is enabled and running
  service: name=shinyproxy state=started enabled=yes
  become: true

При этом информация о приложениях (shinyproxy_apps) хранится в json, в таком виде.

Шаблон application.yml

SPL
{
  "shinyproxy_apps": [
      {
      "id": "report_one",
      "display_name": "Отчет 1",
      "container_image": "report_one",
      "access_groups": "Группа доступа 1, Группа доступа 2",
      "groups": "Domain Users"
    },
    {
      "id": "report_two",
      "display_name": "Отчет 2",
      "container_image": "report_two",
      "access_groups": "Группа доступа 1, Группа доступа 2",
      "groups": "Domain Users"
    }
  ]
}

Все это безобразие запускается таким CI

.gitlab-ci.yml

SPL
stages:
- deploy
- test
variables:
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
deploy:
  stage: deploy
  image: $CI_REGISTRY_IMAGE
  script:
    - ansible-playbook --check playbook.yml -i hosts.inil --extra-vars=@apps.json -e CI_PROJECT_NAME=$CI_PROJECT_NAME -e CI_REGISTRY_IMAGE=$IMAGE_TAG -e CI_PROJECT_NAMESPACE=$CI_PROJECT_NAMESPACE -e CI_REGISTRY=$CI_REGISTRY -eCI_JOB_TOKEN=$CI_JOB_TOKEN -vv
    - ansible-playbook playbook.yml -i hosts.ini --extra-vars=@apps.json -e CI_PROJECT_NAME=$CI_PROJECT_NAME -e CI_REGISTRY_IMAGE=$IMAGE_TAG -e CI_PROJECT_NAMESPACE=$CI_PROJECT_NAMESPACE -e CI_REGISTRY=$CI_REGISTRY -e CI_JOB_TOKEN=$CI_JOB_TOKEN -vv
  except:
    - triggers

Сбор статистики
В ShinyProxy есть средство сбора статистики, на данный момент оно поддерживает только 2 СУБД; InfluxDB и MonetDB. Собирается статистика следующего вида: время события; пользователь; тип события (логин/открытие/закрытие отчета); название отчета.
Но для наших целей мы отказались от данного инструмента, заменив его самописными событиями с отправкой в Postgres, так мы теряем возможность видеть кто логинился (но нам это и не нужно), зато получаем возможность вести расширенную статистику по пользователям. Помимо времени входа, имени пользователя и отчета, еще добавили информацию о группах пользователя в Active Directory. Так мы можем точнее понять, из-за чего у пользователя не работает отчет.

Пример сбора статистики

SPL
con_stat <- dbConnect(RPostgres::Postgres(),host = 'host', port = 5432, dbname = "dbname", user = "user", password = "password")
vizit_df <- data.frame(user = Sys.getenv("SHINYPROXY_USERNAME"), groups = Sys.getenv("SHINYPROXY_USERGROUPS"), app = 'app_name', timie_visit = Sys.time())
dbWriteTable(con_stat, "visit_apps", vizit_df, append=TRUE)
dbDisconnect(con_stat)
rm(vizit_df)

Общая схема всей системы отчетности выглядит как-то так

Общая структура отчетов и контроль доступов
Очевидно, что в рамках компании пользователи должны иметь разный доступ к отчетам и их содержимому. Набор отчетов для топ-менеджмента будет отличаться от набора отчетов для менеджера города. Аналогично и с доступом к информации: региональный менеджер может видеть только свой город, а топ менеджер может видеть все города. Чтобы реализовать такой функционал было сделано следующее.
В самом ShinyProxy есть возможность показывать разным группам пользователей разный набор отчетов, он осуществляется с помощью указания групп Active Directory в конфиге отчета, в свойстве access-groups. Для этих целей были заведены отдельные группы LDAP для разных профессий, вида: Маркетинг, Аналитика, Топ-менеджмент и пр.
Но вот для получения доступа к отдельным городам или платформам уже пришлось использовать самописный вариант.
  • У всех региональных менеджеров в группах Active Directory прописан город, а ShinyProxy при авторизации через LDAP возвращает строку с описанными группами в переменной среды SHINYPROXY_USERGROUPS. Прочитав переменную, мы сможем извлечь список доступных городов для пользователя.
  • Также все группы пользователей (Маркетинг, Аналитика, Топ-менеджмент и пр.) были разделены на тех, кому по умолчанию можно видеть все города, и тех, кто может видеть только города привязанные в LDAP. Так, при открытии отчета, мы сможем понять, нужно ли отображать пользователю один город или все (например, если это аналитик или топ-менеджер).
  • Ещё была добавлена таблица «Исключений». У тех же маркетологов или менеджеров города есть руководители, эти люди должны видеть все города при том же самом наборе отчетов. Так нам не нужно будет для подобных пользователей прописывать 100+ городов в Active Directory. Соответственно, в эту таблицу заносятся логины руководителей отделов, и при открытии отчета происходит проверка того, что пользователь может видеть все города.

Общая структура отчета Shiny
username <- Sys.getenv("SHINYPROXY_USERNAME") # Получаем имя пользователя
user_groups <- Sys.getenv("SHINYPROXY_USERGROUPS") # Получаем список групп пользователя в LDAP. Так мы узнаем, можно ли смотреть пользователю все города или только один и какой именно
# блок сбора статистики, пишем кто открыл отчет с какими правами
con_stat <- dbConnect(RPostgres::Postgres(),host = 'host', port = 5432, dbname = "dbname", user = "user", password = "password")
vizit_df <- data.frame(user = username, groups = user_groups, app = 'report_name', timie_visit = Sys.time())
dbWriteTable(con_stat, "visit_apps", vizit_df, append=TRUE)
dbDisconnect(con_stat)
rm(vizit_df)
# Определяем, можно ли пользователю смотреть все города
# Возвращаем список городов
# Определяем, можно ли пользователю видеть только один город
#   Проверяем, не является ли пользователь руководителем. Если руководитель, то показываем все города
# Возвращаем список городов
ui <- dashboardPage(
      # Описание UI
)
server <- function(input, output) {
  # Обработка событий и генератор SQL запросов
}
shinyApp(ui, server)

Процесс внедрения
Процесс перехода в данном случае можно разбить на 4 этапа:
  • По началу стояла задача закрыть операционные потребности региональных директоров и менеджеров, чтобы они могли получить расширенные сведения и статистику по водителям, пассажирам (обезличенные данные) и поездкам. Нужно было реализовать раздельный доступ к городам-платформам, чтобы каждый региональщик видел только свой город. И стояла задача перенести отчетность Looker'а.
  • Затем мы стали закрывать информационные потребности маркетологов. Так же на данном этапе был осуществлен перенос отчетов из Qlick Sense.
  • Следующим шагом мы стали переносить отчеты из самописных веб-сервисов, там уже преимущественно была задача закрыть потребности финансистов.
  • Создание общей отчетности в рамках всей группы компаний. Здесь уже стояла задача унификации показателей между различными бэкофисами, с целью оперативного понимания о том, что происходит по всей группе компаний. Создание максимально универсального отчета чем-то похожего на сводную таблицу со множеством срезов и показателей. На данном этапе заказчиками отчетности являлись продакт-менеджеры и топ-менеджмент.

Недостатки и неудобства
Теперь стоит поговорить о недостатках данной системы корпоративной отчётности:
  • Спустя время слои от докеров могут занять все дисковое пространство, даже если использовать один главный докер. Поэтому приходится постоянно удалять лишние слои, которые по какой-либо причине больше не используются.
  • При открытии тяжелых отчетов система может долго думать (секунд 5-15) пока запускается докер и выполняются запросы к БД. В этом плане хотелось бы большей бесшовности, но это, видимо, сугубо мои хотелки, т.к. конечных пользователей данный факт не смущает.
  • У некоторых пользователей при первом открытии отчета, иногда по неведомой причине, система может выдавать ошибку о том, что адрес не отвечает, т.к. докер контейнер не успел загрузиться, что конечного пользователя вводит в заблуждение. При повторном открытии отчета данная проблема пропадает.
  • Создание отчётов не получиться доверить "абы кому" т.к. нужны знания R. И если придет новый аналитик, то он должен быть либо изначально со знаниями R, либо должен будет потратить время на его изучение уже на рабочем месте.
  • После внесения правок в отчет или добавление нового отчета приходится перезагружать службу ShinyProxy на сервере.

Вывод
В целом данная система отчетности показала себя хорошо. За год ее использования не было замечено каких-то критически важных нерешаемых проблем. Конечного бизнес-пользователя все устраивает, т.к. можно реализовать любой функционал отчета. В большинстве случаев можно оптимизировать отчет и запросы в нем так, чтобы он быстро выполнялся. Кроме того, мы сэкономили средства за счет отказа от Looker'a, Qlick’a и серверов, т.к перевели разную отчётность на одну систему.
На данный момент отчетностью пользуются 30 человек ежедневно. В среднем один пользователь открывает 3 отчета. Общее количество пользователей – 200 человек, общее количество отчетов – 83 штуки.
Большинство отчетов было создано 1-3 аналитиками в течение полугода. На данный момент отчетность поддерживается преимущественно одним аналитиком.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_analiz_i_proektirovanie_sistem (Анализ и проектирование систем), #_r, #_shiny, #_shinyproxy, #_r, #_docker, #_git, #_ansible, #_korporativnaja_otchetnost (корпоративная отчетность), #_analiz_i_proektirovanie_sistem (
Анализ и проектирование систем
)
, #_r
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 23:47
Часовой пояс: UTC + 5